Final Polish and Optimization

Final Polish and Optimization: What You Will Build

You have a persistent profile and a dynamic menu from the previous lesson. As a quick reminder, the profile now updates after each run and survives reloads. In this lesson, you will focus on the final assembly and performance polish:

  • Drive the real-time game loop safely with React hooks.
  • Add visual feedback with a dedicated ShopView.
  • Finish the reducer logic for difficulty, auto-serve, and end conditions.
  • Tie everything together in the menu and action panels with a clean UI.

By the end, the game will feel responsive, readable, and production-ready.

Drive the Game Loop in GameScreen

src/components/GameScreen.jsx

JSX
import React, { useEffect } from 'react';
import { useGameContext } from '../context/GameContext';
import GameHUD from './GameHUD';
import GameActions from './GameActions';
import ShopView from './ShopView';
import './GameScreen.css';

export default function GameScreen() {
  const { state, dispatch } = useGameContext();

  useEffect(() => {
    if (state.phase !== 'PLAYING' || state.timers.isPaused) {
      return;
    }

    const interval = setInterval(() => {
      dispatch({ type: 'TICK' });
    }, 1000);

    return () => clearInterval(interval);
  }, [state.phase, state.timers.isPaused, dispatch]);

  const handleBackToMenu = () => {
    dispatch({ type: 'END_SHIFT' });
  };

  return (
    <div className="game-screen">
      <div className="game-container">
        <GameHUD />
        <ShopView />
        <GameActions />

        <button
          className="neutral-btn quit-btn"
          onClick={handleBackToMenu}
        >
          End Shift
        </button>
      </div>
    </div>
  );
}

Explanation:

  • The effect sets a 1-second interval that dispatches TICK only while playing and not paused. This avoids wasted work and keeps timing stable.
  • Cleanup clears the interval to prevent memory leaks.
  • The End Shift button dispatches END_SHIFT, moving to results immediately.

Visual Feedback with ShopView and CSS

src/components/ShopView.jsx

JSX
import React from 'react';
import { useGameContext } from '../context/GameContext';
import './ShopView.css';

export default function ShopView() {
  const { state } = useGameContext();

  const getShopMood = () => {
    if (state.resources.satisfaction >= 80) return '🌟 Excellent Service!';
    if (state.resources.satisfaction >= 60) return '😊 Happy Customers';
    if (state.resources.satisfaction >= 40) return '😐 Mixed Reviews';
    return '😟 Customers Unhappy';
  };

  const getQueueDescription = () => {
    if (state.shop.queueLength < 3) return 'Light crowd';
    if (state.shop.queueLength < 8) return 'Steady stream';
    if (state.shop.queueLength < 15) return 'Getting busy!';
    return 'LINE OUT THE DOOR!';
  };

  const getSupplyStatus = () => {
    const lowSupply = state.resources.coffeeBeans < 10 || state.resources.milk < 10;
    if (lowSupply) return '⚠️ Low supplies!';
    return '✅ Well stocked';
  };

  return (
    <div className="shop-view">
      <h3 className="shop-title">☕ Coffee Shop Status</h3>
      <div className="shop-info">
        <div className="shop-stat">
          <span className="shop-stat-label">Shop Mood:</span>
          <span className="shop-stat-value">{getShopMood()}</span>
        </div>
        <div className="shop-stat">
          <span className="shop-stat-label">Queue Status:</span>
          <span className="shop-stat-value">{getQueueDescription()}</span>
        </div>
        <div className="shop-stat">
          <span className="shop-stat-label">Supply Status:</span>
          <span className="shop-stat-value">{getSupplyStatus()}</span>
        </div>
        <div className="shop-stat">
          <span className="shop-stat-label">Difficulty:</span>
          <span className="shop-stat-value">{state.config.difficulty.toUpperCase()}</span>
        </div>
      </div>
      <div className="shop-visual">
        {state.shop.queueLength < 5 && '☕ 🏪 👤'}
        {state.shop.queueLength >= 5 && state.shop.queueLength < 10 && '☕ 🏪 👤👤👤'}
        {state.shop.queueLength >= 10 && state.shop.queueLength < 15 && '☕ 🏪 👥👥👥'}
        {state.shop.queueLength >= 15 && '☕ 🏪 👥👥👥👥👥'}
      </div>
    </div>
  );
}

Explanation:

  • Translates state into friendly labels: mood (by satisfaction), queue status (by length), and supply status (beans/milk thresholds).
  • The emoji “visual” reacts to queue length, giving instant, lightweight feedback.

Core Logic: State and Helpers

src/reducers/gameReducer.js

JavaScript
const RUSH_DURATION = 180;
const MAX_QUEUE = 20;
const MAX_HISTORY = 5;

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
    },
    history: { past: [], actionLog: [] },
    result: null
  };
}

function getDifficultySettings(state) {
  const diff = state.config.difficulty;
  return {
    startingResources: state.config.startingResources[diff],
    customerArrivalRate: state.config.customerArrivalRate[diff],
    maxQueue: state.config.maxQueue
  };
}

function addToHistory(state, action) {
  const past = [...state.history.past, state];
  if (past.length > MAX_HISTORY) past.shift();

  const actionLog = [...state.history.actionLog, {
    type: action.type,
    timestamp: Date.now()
  }];
  if (actionLog.length > 10) actionLog.shift();

  return { past, actionLog };
}

function updateGameTime(timeRemaining) {
  const totalMinutes = 180 - timeRemaining;
  const gameMinutesElapsed = totalMinutes * 3;
  const gameHour = 6 + Math.floor(gameMinutesElapsed / 60);
  const gameMinute = gameMinutesElapsed % 60;
  return { gameHour, gameMinute };
}

function getDifficultyRank(difficulty) {
  const ranks = { easy: 1, normal: 2, hard: 3 };
  return ranks[difficulty] || 0;
}

Explanation:

  • Constants and getInitialState define the "Day 0" shape of our world.
  • Helper functions like getDifficultySettings and updateGameTime keep the reducer lean and focused on state transitions rather than math.

Core Logic: The Reducer

src/reducers/gameReducer.js

JavaScript
export function gameReducer(state, action) {
  switch (action.type) {
    case 'SET_SCREEN': return { ...state, ui: { ...state.ui, screen: action.payload } };
    case 'SET_DIFFICULTY': return { ...state, config: { ...state.config, difficulty: action.payload } };

    case 'START_GAME': {
      const settings = getDifficultySettings(state);
      return {
        ...state,
        phase: 'PLAYING',
        ui: { ...state.ui, screen: 'game' },
        timers: { timeRemaining: RUSH_DURATION, isPaused: false, gameHour: 6, gameMinute: 0 },
        resources: { ...settings.startingResources, satisfaction: 100 },
        shop: { queueLength: 0, customersServed: 0, ordersCompleted: 0, revenue: 0, tempBaristaActive: 0 },
        history: { past: [], actionLog: [] },
        result: null
      };
    }

    case 'TICK': {
      if (state.phase !== 'PLAYING' || state.timers.isPaused) return state;

      const newTime = Math.max(0, state.timers.timeRemaining - 1);
      const settings = getDifficultySettings(state);
      const { gameHour, gameMinute } = updateGameTime(newTime);

      const arrivalChance = gameHour === 7 ? settings.customerArrivalRate * 1.5 : settings.customerArrivalRate;
      const newCustomer = Math.random() < arrivalChance ? 1 : 0;
      const newQueue = Math.min(state.config.maxQueue, state.shop.queueLength + newCustomer);

      let autoServed = 0;
      let tempBaristaTime = state.shop.tempBaristaActive;
      if (tempBaristaTime > 0) {
        tempBaristaTime -= 1;
        if (newQueue > 0 && Math.random() < 0.5) autoServed = 1;
      }

      const finalQueue = Math.max(0, newQueue - autoServed);
      const satisfactionLoss = finalQueue > 10 ? 2 : finalQueue > 5 ? 1 : 0;
      const newSatisfaction = Math.max(0, state.resources.satisfaction - satisfactionLoss);
      const autoRevenue = autoServed * 5;

      const currentStats = {
        revenue: state.shop.revenue + autoRevenue,
        customersServed: state.shop.customersServed + autoServed,
        ordersCompleted: state.shop.ordersCompleted + autoServed,
        satisfaction: newSatisfaction
      };

      // Handle Win/Loss Conditions
      if (finalQueue >= state.config.maxQueue || newSatisfaction <= 0 || newTime === 0) {
        const won = newTime === 0 && finalQueue < state.config.maxQueue * 0.5 && newSatisfaction >= 50;
        const reason = finalQueue >= state.config.maxQueue ? 'Queue too long!' :
                       newSatisfaction <= 0 ? 'Customer satisfaction zero!' :
                       won ? 'Rush hour complete!' : 'Too many unhappy customers!';

        return {
          ...state,
          phase: 'RESULTS',
          ui: { ...state.ui, screen: 'results' },
          result: { won, reason, stats: currentStats },
          profile: {
            ...state.profile,
            totalGamesPlayed: state.profile.totalGamesPlayed + 1,
            totalRevenue: state.profile.totalRevenue + currentStats.revenue,
            totalCustomersServed: state.profile.totalCustomersServed + currentStats.customersServed,
            bestRevenue: won && currentStats.revenue > (state.profile.bestRevenue || 0) ? currentStats.revenue : state.profile.bestRevenue
          }
        };
      }

      return {
        ...state,
        timers: { ...state.timers, timeRemaining: newTime, gameHour, gameMinute },
        shop: { ...state.shop, queueLength: finalQueue, customersServed: state.shop.customersServed + autoServed, revenue: state.shop.revenue + autoRevenue, tempBaristaActive: tempBaristaTime },
        resources: { ...state.resources, satisfaction: newSatisfaction, money: state.resources.money + autoRevenue }
      };
    }

    case 'SERVE_COFFEE': {
      if (state.resources.coffeeBeans < 1 || state.resources.energy < 5 || state.shop.queueLength < 1) return state;
      return {
        ...state,
        resources: { ...state.resources, coffeeBeans: state.resources.coffeeBeans - 1, energy: state.resources.energy - 5, money: state.resources.money + 5, satisfaction: Math.min(100, state.resources.satisfaction + 3) },
        shop: { ...state.shop, queueLength: state.shop.queueLength - 1, customersServed: state.shop.customersServed + 1, revenue: state.shop.revenue + 5 },
        history: addToHistory(state, action)
      };
    }

    case 'RESTOCK_SUPPLIES': {
      if (state.resources.money < 30) return state;
      return {
        ...state,
        resources: { ...state.resources, money: state.resources.money - 30, coffeeBeans: state.resources.coffeeBeans + 20, milk: state.resources.milk + 15 },
        history: addToHistory(state, action)
      };
    }

    case 'HIRE_TEMP_BARISTA': {
      if (state.resources.money < 50 || state.shop.tempBaristaActive > 0) return state;
      return {
        ...state,
        resources: { ...state.resources, money: state.resources.money - 50 },
        shop: { ...state.shop, tempBaristaActive: 30 },
        history: addToHistory(state, action)
      };
    }

    case 'END_SHIFT': {
      return {
        ...state,
        phase: 'RESULTS',
        ui: { ...state.ui, screen: 'results' },
        result: { won: false, reason: 'Shift ended early.', stats: state.shop },
        profile: { ...state.profile, totalGamesPlayed: state.profile.totalGamesPlayed + 1, totalRevenue: state.profile.totalRevenue + state.shop.revenue }
      };
    }
    case 'RESET': return getInitialState(state.profile);
    default: return state;
  }
}

Explanation:

  • TICK manages the core simulation: random arrivals (boosted during 7 AM peak), auto-serving baristas, and win/loss checks.
  • Resource management actions (SERVE_COFFEE, RESTOCK) check availability before applying changes.
  • Profile updates occur within the TICK (at game end) or END_SHIFT, ensuring stats are always captured.

UI Integration: Menu Screen

src/components/MenuScreen.jsx

JSX
import React from 'react';
import { useGameContext } from '../context/GameContext';
import './MenuScreen.css';

export default function MenuScreen() {
  const { state, dispatch } = useGameContext();

  return (
    <div className="menu-screen">
      <div className="menu-container">
        <h1 className="game-title">☕ Brew Rush ☕</h1>
        <div className="profile-stats">
          <h3>Your Stats</h3>
          <div className="stats-grid">
            <div className="stat-item">
              <div className="stat-value">{state.profile.totalGamesPlayed}</div>
              <div className="stat-label">Games Played</div>
            </div>
            <div className="stat-item">
              <div className="stat-value">${state.profile.totalRevenue}</div>
              <div className="stat-label">Total Revenue</div>
            </div>
          </div>
        </div>

        <div className="difficulty-selector">
          <h3>Select Difficulty</h3>
          <div className="difficulty-buttons">
            {['easy', 'normal', 'hard'].map(diff => (
              <button
                key={diff}
                className={`difficulty-btn ${state.config.difficulty === diff ? 'active' : ''}`}
                onClick={() => dispatch({ type: 'SET_DIFFICULTY', payload: diff })}
              >
                {diff.toUpperCase()}
              </button>
            ))}
          </div>
        </div>

        <div className="menu-actions">
          <button className="primary-btn large-btn" onClick={() => dispatch({ type: 'START_GAME' })}>
            Start Shift
          </button>
        </div>
      </div>
    </div>
  );
}

Explanation:

  • Shows lifetime stats pulled from the persistent profile.
  • Difficulty buttons adjust game parameters via SET_DIFFICULTY before starting.

UI Integration: Game Actions

src/components/GameActions.jsx

JSX
import React from 'react';
import { useGameContext } from '../context/GameContext';
import './GameActions.css';

export default function GameActions() {
  const { state, dispatch } = useGameContext();

  const actions = [
    {
      name: 'Serve Coffee',
      cost: '1 bean, 5 energy',
      enabled: state.resources.coffeeBeans >= 1 && state.resources.energy >= 5 && state.shop.queueLength >= 1,
      action: () => dispatch({ type: 'SERVE_COFFEE' }),
      class: 'primary-btn'
    },
    {
      name: 'Restock Supplies',
      cost: '$30',
      enabled: state.resources.money >= 30,
      action: () => dispatch({ type: 'RESTOCK_SUPPLIES' }),
      class: 'neutral-btn'
    },
    {
      name: 'Hire Temp Barista',
      cost: '$50',
      enabled: state.resources.money >= 50 && state.shop.tempBaristaActive === 0,
      action: () => dispatch({ type: 'HIRE_TEMP_BARISTA' }),
      class: 'primary-btn'
    }
  ];

  return (
    <div className="game-actions">
      <h3 className="actions-title">☕ Actions</h3>
      <div className="actions-grid">
        {actions.map(action => (
          <div key={action.name} className="action-card">
            <h4>{action.name}</h4>
            <p><strong>Cost:</strong> {action.cost}</p>
            <button
              className={action.class}
              disabled={!action.enabled || state.timers.isPaused}
              onClick={() => action.action()}
            >
              {action.enabled ? 'Perform' : 'Locked'}
            </button>
          </div>
        ))}
      </div>
    </div>
  );
}

Explanation:

  • Each card is a declarative source of truth for costs and logic.
  • Buttons use the enabled flag to visually communicate constraints (like not enough money or beans).

Summary and Next Steps

Great work. You:

  • Drove the real-time loop with a clean, safe interval.
  • Added a responsive ShopView with clear, immediate feedback.
  • Finalized the reducer for difficulty, auto-serving, satisfaction, and end states.
  • Polished the menu and actions so stats and controls feel cohesive.

You are ready to practice and make these patterns second nature. Let’s put the finishing touches into action.

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