Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 

Repository files navigation

Ebb

Tap a tile. It drains. Empty the board.

A grid logic puzzle in a single HTML file. No build step, no dependencies, no framework — open it in a browser and it runs.

Built by AnbuTech.


The game

Tap a filled tile and it removes one dot from itself and from each tile above, below, left and right of it. Clear every dot on the board.

Dots floor at zero, so draining an already-empty neighbour throws that drain away. Order matters. That single rule is the whole puzzle.

Element Meaning
Pips on a tile How many drains it still needs
Dashed outline Empty tile — nothing to tap
Par Steps the generator used to build the board

Par is a target, not a limit. A shorter route often exists.

Controls

Input Effect
Tap / click a tile Drain it and its four orthogonal neighbours
Tab / Shift+Tab Move between tiles
Enter / Space Drain the focused tile

Empty tiles are disabled, so keyboard focus skips them automatically.

Running it

git clone https://github.com/<your-org>/ebb.git
cd ebb
open ebb.html            # or just drag it into a browser

To test on a phone over your local network:

python3 -m http.server 8000
# then visit http://<your-lan-ip>:8000/ebb.html

How it works

Levels are generated backwards

The hard problem in any procedurally generated puzzle is guaranteeing a solution exists. Randomise a board and you will eventually hand a player something impossible — the fastest way to lose them.

Ebb never has that problem, because it never generates a board. It generates a solution, then plays it in reverse:

// start empty, then run the drain backwards — add dots instead of removing them
while (placed < k){
  const i = Math.floor(Math.random() * N * N);
  const group = [i].concat(neighbours(i));
  if (group.some(c => g[c] >= MAX)) continue;   // respect the pip ceiling
  group.forEach(c => g[c]++);
  placed++;
}

Whatever board falls out the other side is, by construction, the end state of a valid sequence of taps. Undoing that sequence solves it. par is the number of reverse steps used.

Two consequences worth knowing:

  • Par is an upper bound, not the optimum. Overlapping reverse steps sometimes cancel, so a shorter path frequently exists. Finding it is the actual game.
  • The pip ceiling makes generation lossy. A reverse step that would push any tile past MAX is rejected and retried, so a board occasionally lands below its requested step count. The guard counter caps the retries; par reports what was actually placed, not what was asked for.

Why DOM, not canvas

This is a static grid with no per-frame animation. Canvas would mean reimplementing focus management, hit testing, and screen-reader labels by hand, badly. Real <button> elements in a CSS grid give all of that for free, and the pip layout is just absolutely positioned spans at fixed percentages.

Why pips, not numbers

Dice-style patterns parse pre-attentively. You take in a whole board at a glance rather than reading digits cell by cell — which matters when the puzzle is about scanning for the right sequence, not the right tile.

Progress persistence

loadBest() / saveBest() currently call window.storage, an async key–value API from the environment this prototype was authored in. Both are wrapped in try/catch and fail silently, so the game works fine without it — progress just resets on reload.

Before shipping, swap in localStorage:

function loadBest(){
  const v = parseInt(localStorage.getItem('ebb:level') || '1', 10);
  if (v > 1){ best = v; load(v); }
}
function saveBest(){
  localStorage.setItem('ebb:level', String(best));
}

Note these are declared async in the current source. Drop the keyword when you swap them, or leave it — the call sites don't await.

Tuning

Constant Default Effect
N 5 Grid size. 6 roughly doubles the search space
MAX 4 Pip ceiling per tile, and the size of LAYOUT
min(14, 2 + level) Difficulty ramp: reverse steps per level, and its cap
LAYOUT Pip positions per value, as [x%, y%] pairs

Raising MAX above 4 requires adding entries to LAYOUTMath.min(v, MAX) will otherwise silently render the wrong count.

The ramp is the thing to tune first. Levels 1–4 are close to trivial by design; the puzzle gets interesting around level 7, and you may want to start players higher.

Design

Token Value Use
Pine #123F34 Background
Pine deep #0C2C25 Button text on accent
Bone #EDE7D6 Tiles, type
Ink #0F2B24 Pips
Amber #E8A33D Accent, drain flash, focus ring
Dust rgba(237,231,214,.28) Empty outlines, borders

Fraunces for display, Space Grotesk for interface. Tiles carry a hard bottom shadow that collapses on :active, so a tap reads as physical depression. prefers-reduced-motion disables all animation and transitions.

Roadmap

  • Replace window.storage with localStorage
  • Solver to compute true optimal par, not generator par
  • Level select for completed levels
  • Audio: a soft tick per drain, a chime on clear
  • Daily puzzle from a fixed seed
  • Wrap with Capacitor for an Android build

Contact

AnbuTech — an elite engineering unit that ships. anbu-tech.com · LinkedIn


Copyright

Copyright © 2026 AnbuTech. All rights reserved.

This software and its source code are the proprietary property of AnbuTech. No part of this repository may be reproduced, distributed, modified, or used to create derivative works without prior written permission from AnbuTech.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages