Single-page browser game inspired by the Countdown numbers round.
- A round starts with six drawn numbers and one target.
- Number draws come from this pool (without replacement per round):
- two of each
1..10 - one each of
25,50,75,100
- two of each
- All values in gameplay are whole positive integers only (
>= 1). - Supported operators are
+,-,×,÷. - Operators are never consumed: each can be used any number of times.
- Each number token is single-use and tracked by unique
id(not by value). - Completed step results become new single-use tokens for later steps.
- Win condition: any completed step value equals the target.
- After winning, gameplay controls are locked until reset/new game.
A step is formed from left, operator, right. Input order is flexible:
left->right->operatoroperator->left->rightleft->operator->right
When all three are present:
- valid expression: step completes, is appended to history, and its value is added as a new token
- invalid expression: step shows
= Errorand does not complete
Invalid means:
- subtraction result would be
<= 0 - division would be non-integer (remainder)
number-selected(from eachnumber-token):{ id: string, value: number }valueis the whole positive integer represented by that token.
operator-selected(from globaloperator-buttons):{ operator: '+' | '-' | '×' | '÷' }step-complete(from active step):{ id, left, operator, right, value }valueis the evaluated result of that step equation.
steps-changed(from steps list):{ steps: StepData[] }- Contains valid completed steps only; steps showing
Errorare excluded.
- Contains valid completed steps only; steps showing
- Tokens are identity-based (
id), not value-based. Duplicate values are valid when ids differ. - Clicking a selected operand chip in the active step removes that operand assignment.
- Removing a completed step removes that step and all later steps.
- A completed step always contributes exactly one new result token.
- Hints are generated from the token values currently available to the player; consumed tokens and nonexistent duplicates must not be suggested.
- Hint requests are on-demand. Each press of the
Hintbutton advances through:- next operands
- next operator
- full solution
- Completing any step resets hint progression back to the first level for the next request.
- Each successful hint request starts a 30-second cooldown; during cooldown the
Hintbutton is disabled and shows the remaining seconds. - For daily puzzles, the first hint level is free. Escalating to the operator hint (and anything beyond it for that in-progress step) counts as one hint used.
- If no hint is available but completed steps exist, the UI suggests removing the latest step and highlights that step's row/remove control as a rollback cue.
- Hint solutions prefer the fewest steps first; among equally short solutions they prefer simpler arithmetic and smaller intermediate values so the maths is easier to follow.
- Example chain: with
[1, 5, 7, 9, 50, 75],5 × 50 = 250, then250 - 75 = 175. - Keyboard support includes Tab/Shift+Tab for control traversal, Enter/Space to activate controls, arrow-key navigation within number-token and operator groups, and Up/Down movement between numbers, operators, and game controls.
- Within the bottom game-controls row, Left/Right/Home/End move focus across
Reset,Hint,New game, and the difficulty selector. - Up/Down group navigation intentionally does not override native ArrowUp/ArrowDown behavior on the difficulty select control.
New gameshows a loading state while a fresh round is generated.- Gameplay controls are disabled while generation/validation is running.
- The app retries target generation to prefer solvable rounds.
- Difficulty can be chosen as
NormalorEasyfrom the game UI. - Changing difficulty from the selector immediately generates a fresh round at the new difficulty without
changing the game mode (daily vs random), unless a valid
difficultyattribute is controlling the component. - Difficulty bands are based on shortest-solution length from the solver:
Easy: shortest path must be< 4stepsNormal: shortest path must be> 3steps
- If retries exhaust without finding an in-band round, the game uses the best solvable candidate found and logs a console diagnostic with attempt counts and elapsed time.
- If no solvable candidate is found within retries, the game falls back to a guaranteed-solvable target.
- Validation currently runs on the main thread; moving it to a worker remains a future performance improvement.
- Interactive regions include explicit ARIA labels (numbers, operators, controls, steps, target, and hint/status).
- Hint/loading messages are announced as polite live regions.
- Winning triggers a decorative celebration on the game board and a temporary target animation (grow + color-gradient shift); it is visual-only (no extra live-region announcements).
- Under
prefers-reduced-motion: reduce, win-celebration motion is disabled. - Mobile/touch sizing keeps interactive controls at touch-friendly heights.
New gameis styled as the primary call-to-action while keeping reset/hint as secondary controls.- Easy mode shows a small visual badge beside the target; normal mode intentionally omits it.
- On win, the board also shows a rating based on efficiency versus the shortest solver path:
3/3stars: matched shortest path2/3stars: shortest + 1..2 moves1/3stars: shortest + 3 or more moves
- The active mode can be preselected with hash params:
#difficulty=easy#difficulty=normal
- Daily mode can also be preselected:
#mode=daily#difficulty=easy&mode=daily
- Resolution precedence is:
difficultyattribute on<numbers-game>> URL hash > default (normal). modeis hash-driven (dailyorrandom), andrandomis omitted from hash serialization as the default.- Changing the selector updates the hash with
history.replaceStateso links can be shared without page reload, except when a validdifficultyattribute is authoritative. - Selector changes also start a new generated round for the newly selected mode, except when attribute control causes the selector change to be ignored.
- Hash changes that alter mode/difficulty trigger round regeneration, while a valid
difficultyattribute still overrides hash difficulty. - Daily puzzle generation is deterministic by UTC date key (
YYYY-MM-DD) + difficulty so everyone gets the same puzzle regardless of locale/timezone. - Daily completion is persisted per
date + difficultyinlocalStorage(easy/normal tracked independently). - Daily completion stores move count, shortest-path length, star rating, and hint count so restored daily wins keep the same summary.
- Re-opening a completed daily puzzle restores the completed steps, lock state, and win celebration.
- Switching difficulty in daily mode re-checks persisted completion for that difficulty and restores win state when applicable.
- Daily wins expose a
Share resultaction that prefers Web Share API and falls back to clipboard copy when available. - Shared daily text includes stars, move count vs best, and the number of hints used.
npm install
npm run devnpm run lint
npm run build
npm test
npm run css:unusedThis repo is configured to deploy to https://dom111.github.io/numbers-game/ using GitHub Actions.
- In GitHub, open Settings -> Pages.
- Set Source to GitHub Actions.
- The workflow at
.github/workflows/deploy.ymlruns on pushes tomain. - It runs lint + tests, then builds the app with Vite and publishes
distto GitHub Pages. - Deployment sets
VITE_BASE_PATH=/numbers-game/so asset URLs resolve correctly on project pages.
- The workflow at
.github/workflows/ci.ymlruns on pull requests tomain. - It runs format check, a non-blocking CSS unused-selector scan, lint, build, and tests so PRs can be gated by required checks.
- Current hash support includes difficulty + mode selection.
- Next phase extends the same parser/serializer layer for full round/state sharing via URL.