Skip to content

Repository files navigation

Numberweave

A one-finger puzzle game for mobile. Installable PWA, no runtime dependencies, ~15 kB gzipped, works offline.

Play it: https://smashinggoodtime.github.io/numberweave/ — every push to main tests, builds and redeploys it via GitHub Actions.

Connect same or next-numbered tiles. Bigger weaves clear more. Don't let the stack reach the top.

Running it

npm install
npm run dev

Then open the printed URL on your phone (same Wi-Fi) or in a browser's device mode. Other scripts:

Script Does
npm run dev Dev server with HMR
npm run build Typecheck, then build to dist/
npm run preview Serve the production build
npm test Vitest over the logic core
npm run icons Regenerate every icon, splash and store graphic
npm run build:native Build to dist-native/ with no service worker
npm run sync Native build, then cap sync

To install it on a phone, open the built site and use "Add to Home Screen" — it runs standalone, portrait, and offline.

The rules

Weave. Drag across touching tiles showing the same number or the next number up. Dragging back onto the previous tile undoes that step. Release with two or more tiles to commit.

Grow. The whole path folds into the tile you finished on, which gains floor(log2(pathLength)) ranks — double the tiles, gain another rank.

Path Gain
2–3 tiles +1
4–7 tiles +2
8–15 tiles +3
16+ tiles +4

Resonate. If the new tile matches an orthogonal neighbour, it absorbs it and ranks up — then checks again, recursively. Each link raises the score multiplier. Because you choose where the path ends, you choose where the result lands, and therefore what it resonates with. That is the skill ceiling: a beginner sees "connect matching numbers", an expert is deciding where to detonate a chain they set up two moves ago.

Survive. A new row rises from the bottom every few weaves — one tick per weave, whether that weave was two tiles or twelve. Small weaves cost the same as big ones, so hunt the big ones. If the stack crosses the top, the run ends.

Scoring is rank² × tiles × chain × 10.

Modes

  • Endless — the main mode; escalating difficulty, persistent best.
  • Daily — seeded from the UTC date, so everyone gets the same board. Best result per day is kept, with a shareable summary.
  • Zen — rows still arrive, but gently, and you cannot lose.

Twenty-eight achievements unlock six alternate colour palettes, and the daily tracks a streak.

Architecture

src/
  game/     pure logic — no DOM, no canvas, fully deterministic
    rules.ts    every tunable constant
    rng.ts      mulberry32; seeds the Daily and the tests
    board.ts    grid, gravity, rises, deadlock detection
    path.ts     step legality, backtracking, the ghost preview
    resolve.ts  merge + recursive resonance -> animation steps
    engine.ts   run state machine, level curve, playability guarantees
  render/   canvas 2D — sprite cache, particles, rings, shake
  input/    pointer events, with coalesced samples so fast swipes don't skip
  audio/    procedural WebAudio; no audio files
  ui/       DOM overlays (menus, HUD, coach marks)

  platform.ts      storage backend — native Preferences, or localStorage
  haptics.ts       game events -> Taptic Engine, or navigator.vibrate
  leaderboards.ts  Game Center / Play Games, absent unless signed in

Those last three all follow the same rule: __NATIVE__ is a compile-time constant, so every Capacitor import sits inside a branch that is statically dead on the web and shakes out entirely. The deployed PWA contains no Capacitor code at all.

The engine resolves an entire turn synchronously and returns an ordered list of animation steps. The renderer plays that queue while input is locked, so the logic stays turn-based and testable while the presentation stays fluid. When the queue drains, the view reconciles against the board, so visual drift cannot accumulate.

npm test covers step legality, backtracking, the growth formula, recursive resonance, gravity, rises, game over, deadlock recovery, snapshot round-tripping, and seed determinism.

Shipping it

npm run build produces the PWA. npm run build:native produces the same game without a service worker, into dist-native/, which is what Capacitor wraps — separate output directories so cap sync can never pick up a worker that would serve stale HTML after a store update.

Tuning the difficulty

Every balance number lives in src/game/rules.ts — grid size, opening rows, level pacing, the rise clock, the spawn spread, the growth formula and scoring. Retuning after playtesting is a one-file edit.

Two findings from simulating full runs against bots of varying strength, worth knowing before you change them:

  • The rise clock must not reach one weave per row. At that rate the board demands a 7-tile path every move, which nobody can sustain, so every run ended at the same point regardless of skill. It floors at 2; past that, difficulty comes from spawnWeights widening the spread of ranks dealt, which makes long paths genuinely harder to find.
  • The board can drain faster than it refills. Strong play clears more tiles per weave than a rise supplies, so MIN_TILES pulls a row in early rather than letting the board dwindle to a few scattered tiles.

Current curve, 16 simulated runs per skill tier:

Player Weaves (min/med/max) Score (min/med/max) Top rank
Weak (≤3-tile paths) 74 / 101 / 131 49k / 88k / 167k 8–10
Mid (≤6) 84 / 124 / 171 82k / 194k / 340k 9–13
Strong (≤16) 99 / 124 / 202 142k / 195k / 457k 10–13

Skill shows up mostly as a raised score floor — a strong player's worst run still beats a mid player's median.

Notes

  • The live run is written back to storage after every weave, so when a mobile OS evicts the backgrounded app the menu offers Continue and the run picks up exactly where it was — board, score and RNG sequence included. A daily interrupted on an earlier day is banked into your stats instead.
  • Colour never carries rank alone: every tile shows its numeral, and the ramp steps ~40° of hue per rank so neighbours stay distinguishable. The Signal palette goes further and is built for colour-blind play — it alternates blue against amber rather than walking the hue wheel, holding ΔE 39+ between adjacent ranks under all three common kinds of colour blindness, where the decorative palettes drop as low as 0.4. It is never locked.
  • Respects prefers-reduced-motion, with a manual toggle in Settings that disables shake and particles.
  • Sound and haptics can both be turned off. On the web haptics use navigator.vibrate, which iOS Safari does not implement; a native build routes them to the Taptic Engine instead — selection ticks while dragging, weighted impacts on a commit.
  • The frame loop only draws when something moved: nothing at all behind a full-screen overlay, half rate while you are thinking, full rate while dragging or animating.
  • Icons are generated procedurally by scripts/gen-icons.mjs, which writes real PNGs using only zlib. No binary art assets in the repo. One run produces three sets: public/icons/ for the PWA (committed, the web build needs it), resources/ as source art for npx @capacitor/assets generate, and store/ for listing artwork. The last two are gitignored — they are only needed locally, and regenerate in about a second.
  • Anything bound for iOS is written as a 3-channel PNG. App Store Connect rejects an app icon that has an alpha channel, even a fully opaque one, so the encoder drops the channel rather than filling it.
  • Android's adaptive icon is emitted as separate foreground and background layers, with the artwork inside the 0.62 safe zone — the only part of the square guaranteed to survive whatever mask a launcher applies.

About

Numberweave — a one-finger number puzzle for mobile. Connect same or next-numbered tiles; bigger weaves clear more.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages