Game State and Reducer Structure

Defining the Initial State and Reducer Structure

In the last two lessons, you built the global store with useReducer + Context and used it to drive screen changes. As a quick reminder, our UI now depends on state, and navigation is just dispatching actions. In this lesson, you will shape that state for a real game and set up the reducer to handle the first transitions.

Designing the Initial Game State

We start by defining constants and a single source of truth for all game data. This makes future updates predictable and testable.

// src/reducers/gameReducer.js
const RUSH_DURATION = 180; 
const MAX_QUEUE = 20;

export function getInitialState(profile = null) {
  return {
    phase: 'IDLE',
    ui: { screen: 'menu' },
    config: {
      difficulty: 'normal',
      maxQueue: MAX_QUEUE,
      startingResources: {
        easy: { money: 100, coffeeBeans: 50, milk: 40, energy: 100 },
        normal: { money: 80, coffeeBeans: 40, milk: 30, energy: 100 },
        hard: { money: 60, coffeeBeans: 30, milk: 20, energy: 100 }
      },
      customerArrivalRate: { easy: 0.3, normal: 0.5, hard: 0.7 }
    },
    timers: {
      timeRemaining: RUSH_DURATION,
      isPaused: false,
      gameHour: 6,
      gameMinute: 0
    },
    resources: {
      money: 80,
      coffeeBeans: 40,
      milk: 30,
      energy: 100,
      satisfaction: 100
    },
    shop: {
      queueLength: 0,
      customersServed: 0,
      ordersCompleted: 0,
      revenue: 0,
      tempBaristaActive: 0
    },
    profile: profile || {
      totalRevenue: 0,
      totalCustomersServed: 0,
      bestRevenue: 0,
      hardestDifficultyBeaten: null,
      totalGamesPlayed: 0,
      winStreak: 0
    },
    history: { past: [], actionLog: [] },
    result: null
  };
}

Explanation:

  • RUSH_DURATION and MAX_QUEUE are constants that keep magic numbers out of the state.
  • ui.screen controls which view is visible (from the previous lesson’s pattern).
  • config holds difficulty, queue limits, starting resources, and arrival rates — perfect for tuning without touching UI code.
  • timers model the game clock and pause state.
  • resources and shop hold the core gameplay stats you will read and update in later units.
  • profile persists player-wide stats; passing profile into getInitialState lets you reset a run without losing the player’s progress.
  • history and result are placeholders for undo/logging and end-of-run data.

Writing the Reducer Skeleton

Now wire up the first actions. Keep state updates pure and focused. Each case does one thing, and you can grow this over time.

// ...
export function gameReducer(state, action) {
  switch (action.type) {
    case 'SET_SCREEN': {
      return {
        ...state,
        ui: { ...state.ui, screen: action.payload }
      };
    }
    case 'START_GAME': {
      // Reset state but keep profile
      return {
        ...getInitialState(state.profile),
        phase: 'PLAYING',
        ui: { screen: 'game' }
      };
    }
    default:
      return state;
  }
}

Explanation:

  • SET_SCREEN changes only ui.screen. It keeps the reducer predictable and aligns with the state-driven view approach you used earlier.
  • START_GAME builds a fresh run by calling getInitialState(state.profile). This resets the run but preserves the player profile. It also sets phase to PLAYING and switches the screen to game.
  • The default returns the current state untouched, which is required for unknown actions.

Summary and Next Steps

You defined a clear, nested game state and a reducer that handles the first two transitions. You now have:

  • A single source of truth for timers, resources, shop, UI, and player profile.
  • A clean reducer with focused actions for screen changes and starting a new run.

You are ready to practice. In the next section, you will put this structure to work and see how a solid state model makes new features easier and safer to build.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal