DEV Community

Cover image for 🔥 From O(n) to O(1): Smarter Game State for Smarter Code
Tara Timmerman
Tara Timmerman

Posted on

🔥 From O(n) to O(1): Smarter Game State for Smarter Code

What started as a simple feature in a Rock-Paper-Scissors game (tracking the most common move) quickly revealed a classic performance pitfall.

Here's how I refactored an inefficient O(n) approach into a snappy O(1) solution.


🐢 The Slow Way: Recalculate from history every time

My state looked like this:

history: {
  player: ['rock', 'paper', 'rock', ...],
  computer: ['scissors', 'rock', ...]
}
Enter fullscreen mode Exit fullscreen mode

To get the most common move, I was doing this:

function determineMostCommonMove(moves: StandardMove[]): StandardMove | null {
  if (moves.length === 0) return null;

  const counts: Record<StandardMove, number> = { rock: 0, paper: 0, scissors: 0 };

  for (const move of moves) {
    counts[move]++;
  }

  return Object.entries(counts).reduce((a, b) => (b[1] > a[1] ? b : a))[0] as StandardMove;
}
Enter fullscreen mode Exit fullscreen mode

Totally functional. Totally unnecessary. Every time I needed the most common move, I was reprocessing the entire move history.

That’s O(n) time complexity, where n is the number of moves played. For short games, no big deal. But if players go hundreds or thousands of rounds, the cost adds up fast.

To visualize the difference this makes:
📈 Performance Growth (Time)
^
|                        O(n) 🐌
|                       /
|                      /
|                     /
|                    /
|                   /
|                  /
|====================== O(1) ⚡️
+----------------------------------> Input Size (n)


⚡ The Fix: Track move counts as I go

Instead of recalculating, the core idea was to cache the counts as moves were made.

I added a new moveCounts object to my state:

moveCounts: {
  player: { rock: 0, paper: 0, scissors: 0 },
  computer: { rock: 0, paper: 0, scissors: 0 }
}
Enter fullscreen mode Exit fullscreen mode

Now, every time a move is played, I just increment the relevant count:

private incrementMoveCount(key: Participant, move: StandardMove): void {
  this.state.moveCounts[key][move] += 1;
}
Enter fullscreen mode Exit fullscreen mode

And do a quick pass over the counts to get the most common move:

private determineMostCommonMove(moveCounts: MoveCount): StandardMove | null {
  let mostCommonMove: StandardMove | null = null;
  let highestCount = 0;
  let hasMoveFrequencyTie = false;

  // Iterate over the fixed number of possible moves (rock, paper, scissors)
  // not the entire, ever-growing, game history.
  for (const [move, count] of Object.entries(moveCounts)) {
    if (count > highestCount) {
      highestCount = count;
      mostCommonMove = move as StandardMove;
      hasMoveFrequencyTie = false;
    } else if (count === highestCount && count !== 0) {
      hasMoveFrequencyTie = true;
    }
  }

  // In case of a tie in move frequency (e.g., rock: 5, paper: 5),
  // return null as there's no single "most" common move.
  return hasMoveFrequencyTie ? null : mostCommonMove;
}
Enter fullscreen mode Exit fullscreen mode

This change also addresses a later-found bug related to handling ties in move frequency (where multiple moves occurred an equal number of times).

Now, the determineMostCommonMove function iterates only over the fixed number of possible moves (rock, paper, scissors), which is always 3. While technically O(k) where k is the number of possible moves, since k is a tiny constant and does not grow with the game history, this operation is effectively constant time, O(1)!

This means its execution time is entirely independent of how long a game may go.


✅ Before & After

Before:

Most common move was recalculated every time from the full history. O(n). Clunky.

After:

Counts are updated in real time and checked instantly. O(1). More efficient, more scalable, and simpler to reason about.

Top comments (2)

Collapse
 
kris_chou_5f6deb607e8cb75 profile image
Kris Chou

Smart move, this is what true optimization looks like.

It’s not just about working harder, but working smarter: making thoughtful decisions that maximize results with minimal wasted effort.

Optimization means constantly refining your approach, learning from experience, and finding the most effective path forward.

In both coding and career, that mindset separates good from great.

Collapse
 
taratimmerman profile image
Tara Timmerman

Couldn’t have said it better, love that mindset.