Difficulty Levels
You have two AIs hiding in one function: a random guesser and a strategist. Today you give them names, let the player pick between them with a dropdown, and lay the slot where next lesson's unbeatable AI will drop in. The real lesson is choosing behavior at runtime.
The plan: separate "decide" from "dispatch"
Right now computerMove does two jobs tangled together: it decides which cell to play (the win/block/random logic) and it's the thing called to move. To support difficulty, split those:
- Each difficulty becomes its own strategy function that takes the board and returns an index — it decides, but doesn't move.
computerMovebecomes a thin dispatcher: look at the chosen difficulty, call the matching strategy, play its answer.
A strategy that returns an index is easy to swap, test, and reason about — it's a pure decision. Keeping the actual makeMove in one place (the dispatcher) means all three difficulties share the same "play it" path. Decide in many ways; act in one way.
Step 1: Pull out the shared helper
Both Easy and Normal need the list of empty cells, so give it a home:
function getEmptyCells() {
const empty = [];
for (let i = 0; i < board.length; i++) {
if (board[i] === "") empty.push(i);
}
return empty;
}
Step 2: Name the two strategies
Easy is your old random pick. Normal is the win/block/random logic you just built. Each returns an index:
// EASY: pure random
function easyMove() {
const empty = getEmptyCells();
return empty[Math.floor(Math.random() * empty.length)];
}
// NORMAL: win, else block, else random
function normalMove() {
let choice = findWinningMove(computer);
if (choice === null) choice = findWinningMove(human);
if (choice === null) choice = easyMove(); // reuse Easy as the fallback
return choice;
}
Notice normalMove falls back to easyMove() for its random case — the levels build on each other instead of duplicating code.
Step 3: A dispatch table (functions as values)
Here's the elegant part. You can store the strategies in an object, keyed by difficulty name — then picking a strategy is just a lookup, exactly like playerNames[currentPlayer], except the values are functions:
const strategies = {
easy: easyMove,
normal: normalMove,
crazy: normalMove // placeholder — becomes minimax in Lesson 10
};
Step 4: The dispatcher
let difficulty = "normal"; // current setting (state)
function computerMove() {
if (isGameOver) return;
const strategy = strategies[difficulty]; // pick the function
const choice = strategy(); // run it -> an index
makeMove(choice); // one shared "play it" path
}
Three difficulties, one dispatcher. Adding minimax next lesson is now a two-line change: write crazyMove, point crazy: at it.
Step 5: The dropdown
A <select> gives the player the choice. Add it to index.html:
<label for="difficulty">Difficulty:</label>
<select id="difficulty">
<option value="easy">Easy</option>
<option value="normal" selected>Normal</option>
<option value="crazy">Crazy</option>
</select>
Then listen for changes. When the player switches difficulty, update the state and reset for a clean game:
document.querySelector("#difficulty")
.addEventListener("change", (event) => {
difficulty = event.target.value; // "easy" | "normal" | "crazy"
resetGame();
});
change fires when the player picks a new option. event.target is the <select> that changed, and .value is the chosen option's value attribute — the string "easy", "normal", or "crazy". Those strings are exactly your strategies keys, which is why the dropdown wires straight into the dispatch table with zero translation.
Live demo
Switch difficulty and play. Easy guesses randomly (very beatable); Normal blocks and takes wins. (Crazy currently mirrors Normal — next lesson it becomes unbeatable.)
Build it yourself
- Add
getEmptyCells(), theneasyMove()andnormalMove()(move your existing logic into them). - Add the
strategiesobject andlet difficulty = "normal";. - Rewrite
computerMoveas the dispatcher. - Add the
<select>to your HTML and thechangelistener.
Wire it up, then verify each level behaves differently: on Easy, set up two-in-a-row and confirm the computer often doesn't block (it's guessing); on Normal, confirm it always blocks. Then switch difficulty mid-thought and confirm the board resets cleanly (your clearTimeout from the last fix matters here). Bonus: add a console.log(difficulty) in the change listener to watch the state update as you switch.
What you just learned
- Strategy functions: each difficulty is a function that returns a decision; one dispatcher does the acting.
- Dispatch table: an object mapping names to functions replaces an
if/elseladder. Functions are values. - The
<select>element and thechangeevent, readingevent.target.value. - Designing for extension: the
crazyslot is ready; minimax drops in with no other changes.
crazyMove and point crazy: at it. Everything else you built today stays exactly as is. The dropdown, the dispatcher, the shared makeMove — all of it just works the moment the new strategy exists. That's the payoff of today's architecture.