Ian Watkins iankwatkins
Lesson 9 · Game Dev Fundamentals

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:

Why "return the choice" instead of "make the move"

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();
});
The "change" event and event.target.value

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

  1. Add getEmptyCells(), then easyMove() and normalMove() (move your existing logic into them).
  2. Add the strategies object and let difficulty = "normal";.
  3. Rewrite computerMove as the dispatcher.
  4. Add the <select> to your HTML and the change listener.
Your task

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