Beginner / Game state & scoring
Model a Two-Card Turn as a Small State Machine

A memory game does not need a large state-machine library to benefit from explicit states. Its most important transitions fit inside one array and one countdown. The challenge is deciding which actions are legal at each point, especially when a player clicks faster than expected.
What you'll build
We will describe and test the turn model already implemented by Memory Match. A turn accepts two distinct available cards, resolves a match immediately, or displays a mismatch before returning to the ready state. This article concentrates on input invariants rather than the full Canvas setup.
Prerequisites
Read the createMemory factory in memory.js. It returns a state object and a choose method. You should understand array membership checks and object properties. The game uses card indexes to identify positions and symbols to identify matching pairs.
Name the states before writing transitions
An empty selection is ready for a first card. One selected index is waiting for a second. Two mismatching indexes with a positive hideIn value are temporarily locked. Completion is represented separately by finished, because a completed board must reject every further choice.
Matched cards are not part of the temporary selection. Their own flags keep them visible permanently. That distinction prevents a later turn from accidentally hiding cards that were already solved.
You could encode these states with named strings, but the current data already determines them unambiguously. Adding a second phase variable would require keeping the string and selection array synchronized. For a small model, deriving the phase from existing state avoids that extra source of inconsistency.
Put guards at the input boundary
function choose(index) {
const card = state.deck[index];
if (!card || state.finished || state.hideIn > 0 || card.matched ||
state.selected.includes(index)) return;
state.selected.push(index);
if (state.selected.length !== 2) return;
state.moves++;
const [a, b] = state.selected;
if (state.deck[a].symbol === state.deck[b].symbol) {
state.deck[a].matched = state.deck[b].matched = true;
state.selected = [];
state.pairs++;
state.finished = state.won = state.pairs === pairCount;
} else {
state.hideIn = 0.85;
}
}
The early return is a compact list of illegal transitions. It rejects an invalid index, a completed game, a locked mismatch, a previously matched card, and an index already selected. Both pointer and keyboard controls call this method, so neither input path can bypass the rules.
The duplicate-index guard is essential. Selecting the same position twice would otherwise compare a symbol with itself and count a false pair. A disabled HTML button helps the interface communicate availability, but the engine still needs its own checks because rules should not depend on one particular UI.
Count a move when the second card is accepted
The move counter increases after two legal selections. A first flip is not yet a complete move. Rejected clicks do not count. This gives the displayed number a precise meaning: how many two-card comparisons the player has attempted.
On a match, the engine marks both objects, clears the temporary selection, and increments the pair counter. On a mismatch, it leaves the two indexes selected and starts the delay. No transition accepts a third index, so rendering can safely assume that at most two unmatched cards are temporarily visible.
Completion compares the pair counter with the configured number of pairs. The same transition works for six-, seven-, and eight-pair boards. Avoid checking a hard-coded score such as 600; that would entangle victory with a presentation or scoring convention.
Resolve delayed transitions through update
The mismatch countdown decreases during active simulation. When it reaches zero, update clears the temporary selection. The engine does not need to ask the DOM which cards are currently showing symbols. Pixels and button labels follow the model, rather than deciding what the model should be.
Pausing preserves the selection and remaining delay. Restarting constructs a fresh model, so the old turn no longer exists. These behaviors follow naturally from storing the delay inside the instance instead of scheduling an unrelated callback that can outlive a round.
Test the actions a player should not be able to take
A useful test sequence chooses index zero twice, chooses a different symbol, then attempts a third choice. The duplicate must not increment moves, the legal second choice must increment it once, and the attempted third choice must leave the selection unchanged until the delay expires.
After the delay, select a real matching pair and try choosing one of those matched indexes again. The pair count and move count should remain unchanged. Finish the board and verify that additional input cannot create another move or exceed the pair target.
These checks are stronger than playing only a perfect solution. The perfect path never exercises the guards that protect the model from fast, repeated, or stale input.
Next steps
Open Memory Match's source and write down each legal transition in plain language before adding another mechanic. If you introduce a preview phase or limited lives, extend the transition rules first. Then update controls and rendering to reflect the model, keeping the input boundary as the final authority on what a player can do.