Summary
The current computer opponent (cfg.ai, see maybeAI() / aiStep() / scoreMove() in twiddle.html) picks each of its sub-moves with a single fixed heuristic: take a free capture if available, otherwise weight down squares that hang a piece and up squares that threaten one, plus a little random jitter. It has no difficulty levels, no lookahead beyond the immediate move, and no awareness of the mechanic that makes Twiddle interesting — that the square you leave rotates, which reshapes the board for whoever visits it next.
This issue proposes (1) a tiered computer opponent that actually plans a turn instead of picking moves one at a time in isolation, and (2) the UI to select and configure it.
1. Computer strategy
Why "one move at a time" isn't enough
A Twiddle turn is 3–6 individual sub-moves, and only the board state at the end of the turn is visible to the opponent — everything in between is invisible bookkeeping. A greedy per-sub-move AI can walk itself into a bad final position (e.g. spend its last move capturing a piece but land its own piece on a now-exposed square) because it never evaluates the turn as a whole. The fix is to search over short sequences of sub-moves and score the resulting end-of-turn board, not each sub-move independently.
Proposed structure: three difficulty tiers
Easy — reactive (mostly what exists today)
- Keep the current one-ply greedy scorer: prioritize free captures, penalize moves that hang a piece to an immediate recapture, small randomness for variety.
- No lookahead past the current sub-move. This tier exists to be genuinely beatable — good for learning the rules.
Normal — turn-level beam search
- At the start of the turn, generate the top-K candidate moves (K ≈ 5–6) by the Easy heuristic, then extend each into a full turn via a bounded beam search (keep the best 3–4 partial turns after each sub-move, discard the rest) rather than exhaustively enumerating every combination (branching factor is too large to search fully: up to ~4 legal directions × up to
NM pieces-worth of moves per turn).
- Score only the final board state at the end of the simulated turn, using the evaluation function below — this is what fixes the "captured a piece but left itself exposed" problem.
- Depth: 1 full own-turn lookahead. Does not simulate the opponent's reply.
Hard — shallow minimax over turns
- Same beam-search turn generation as Normal, but now also simulates the opponent's best-response turn (via a cheaper Easy/Normal-level beam search) and picks the move that maximizes (own end-of-turn score − opponent's best reply score), i.e. a 2-ply minimax where each "ply" is a full turn, not a single move.
- Iterative deepening with a time budget (e.g. 400–600ms per decision) so it degrades gracefully instead of freezing the UI — start with Normal-depth search, extend if time remains.
- Move ordering: try captures and escape-from-threat moves first, so alpha-beta pruning cuts the tree effectively within the time budget.
Evaluation function (shared by Normal/Hard)
Score a board state from the AI's perspective as a weighted sum:
| Factor |
What it measures |
Why it matters |
| Material |
(my pieces − their pieces), heavily weighted |
The actual win condition |
| Threat exposure |
count of my pieces reachable by an enemy piece next turn, minus the same for the opponent |
Avoid walking into a capture; look for forcing the opponent into one |
| Mobility |
number of legal destinations available across my pieces vs. theirs |
A piece boxed into a single-arrow square is nearly dead weight; more exits = more options |
| Wraparound tempo |
how close my home row is to fully "unlocking" (see the 🔒 badge mechanic already in the game) vs. theirs |
Unlocking wraparound early is a known strategic lever — the eval should notice and pursue it, not stumble into it by accident |
| Vacated-square awareness |
after a candidate move, check whether the square just vacated now points an arrow straight at one of my own remaining pieces |
This is the "you're handing the next visitor a rotated square" trap described in the in-game help — a decent opponent should actively avoid twiddling a square into a shape that helps the opponent's next move |
| Endgame aggression |
when either side is down to ≤2 pieces, upweight material/threat and reduce exploration randomness |
Prevents a long, aimless drought-avoidance stalemate late game |
Random jitter (small, e.g. ±3% of eval range) stays in at every tier so the AI isn't perfectly deterministic and repeat games don't play out identically.
Non-goals for this issue
- No full game-tree solver / perfect play — Twiddle's branching factor (dynamic arrows + variable turn length + wraparound) makes that impractical and unnecessary for a fun opponent.
- No persistent learning between games. Difficulty is a fixed configuration per game, not adaptive.
2. UI changes
Game mode selection (Setup screen)
Replace the current buried "Player 2: Human / Computer" dropdown with an explicit, first-choice mode selector at the top of Setup, before the other parameters:
[ ● 1 Player (vs Computer) ] [ ○ 2 Player (hot seat) ]
- 1 Player: reveals two follow-up controls —
- Difficulty: Easy / Normal / Hard (maps to the tiers above)
- Play as: Player 1 (moves first) / Player 2 (moves second) — today the human is hard-coded to Player 1; this lets the human choose either seat
- 2 Player: hides the computer-specific controls entirely (current hot-seat behavior, unchanged)
In-game affordances
- Thinking indicator: already shows "Computer is thinking…" in the message line — extend this with a small animated dot/spinner near the turn indicator so it's visible even if the user isn't looking at the message line, and to make it obvious why input is briefly disabled between the AI's own sub-moves.
- Difficulty badge: show the active difficulty next to "Player 2 (CPU)" in the turn panel (e.g. "Player 2 (CPU · Hard)") so it's always visible mid-game, not just something you configured once at setup and forgot.
- Mid-game difficulty change: not required for this issue, but flag as a natural follow-up (a settings-gear icon that reopens a mini version of the difficulty picker without resetting the board).
- New Game dialog: preserve the last-used mode/difficulty/seat choice as the new defaults when reopening Setup, so starting a rematch doesn't require re-selecting everything.
Accessibility / clarity
- Label the mode selector and difficulty controls with
<label>/aria-label pairing consistent with the rest of the Setup screen's existing <select> pattern.
- Keep Easy as the default difficulty (least likely to frustrate a first-time player who just wants to see how the game works).
Acceptance criteria
Summary
The current computer opponent (
cfg.ai, seemaybeAI()/aiStep()/scoreMove()in twiddle.html) picks each of its sub-moves with a single fixed heuristic: take a free capture if available, otherwise weight down squares that hang a piece and up squares that threaten one, plus a little random jitter. It has no difficulty levels, no lookahead beyond the immediate move, and no awareness of the mechanic that makes Twiddle interesting — that the square you leave rotates, which reshapes the board for whoever visits it next.This issue proposes (1) a tiered computer opponent that actually plans a turn instead of picking moves one at a time in isolation, and (2) the UI to select and configure it.
1. Computer strategy
Why "one move at a time" isn't enough
A Twiddle turn is 3–6 individual sub-moves, and only the board state at the end of the turn is visible to the opponent — everything in between is invisible bookkeeping. A greedy per-sub-move AI can walk itself into a bad final position (e.g. spend its last move capturing a piece but land its own piece on a now-exposed square) because it never evaluates the turn as a whole. The fix is to search over short sequences of sub-moves and score the resulting end-of-turn board, not each sub-move independently.
Proposed structure: three difficulty tiers
Easy — reactive (mostly what exists today)
Normal — turn-level beam search
NMpieces-worth of moves per turn).Hard — shallow minimax over turns
Evaluation function (shared by Normal/Hard)
Score a board state from the AI's perspective as a weighted sum:
(my pieces − their pieces), heavily weightedRandom jitter (small, e.g. ±3% of eval range) stays in at every tier so the AI isn't perfectly deterministic and repeat games don't play out identically.
Non-goals for this issue
2. UI changes
Game mode selection (Setup screen)
Replace the current buried "Player 2: Human / Computer" dropdown with an explicit, first-choice mode selector at the top of Setup, before the other parameters:
In-game affordances
Accessibility / clarity
<label>/aria-labelpairing consistent with the rest of the Setup screen's existing<select>pattern.Acceptance criteria