Simon Says
by Pratik Rai
Read the full write-upBuild Simon. The computer lights a growing sequence of pads; the player repeats it. Get it right and the sequence gains a pad. Get it wrong and the round is over.
Requirements
Start begins with a one-pad sequence and plays it back.
During playback each pad in the sequence lights in turn, and the pads cannot be pressed.
After playback the player repeats the sequence, pad by pad.
A wrong pad ends the game and says how far the player got.
Completing the sequence appends one new random pad and plays the longer sequence.
The round counter shows the current sequence length.
Notes
Sound is out of scope — lighting the pad is the feedback.
The
.pad-litclass already handles the lit look.
Hints
Simon Says (reference solution)
One way to do it, not the only way. Yours passes if it renders.
Solution files
1import { useState, useEffect, useRef } from 'react';
2
3const PADS = [
4 { id: 'green', className: 'pad-green' },
5 { id: 'red', className: 'pad-red' },
6 { id: 'yellow', className: 'pad-yellow' },
7 { id: 'blue', className: 'pad-blue' },
8];
9
10const FLASH_MS = 420;
11
12const randomPad = () => PADS[Math.floor(Math.random() * PADS.length)].id;
13
14export default function App() {
15 const [sequence, setSequence] = useState([]);
16 const [lit, setLit] = useState(null);
17 const [isPlaying, setIsPlaying] = useState(false);
18 const [step, setStep] = useState(0); // how far the player has got
19 const [lost, setLost] = useState(false);
20
21 // Playback. One effect owns the whole sequence: it walks the array on a
22 // timer, and the cleanup cancels the walk if the round changes underneath it
23 // — which is what stops two sequences playing over each other after a fast
24 // restart.
25 useEffect(() => {
26 if (!isPlaying || sequence.length === 0) return undefined;
27 let index = 0;
28 let timer;
29
30 const showNext = () => {
31 if (index >= sequence.length) {
32 setLit(null);
33 setIsPlaying(false);
34 return;
35 }
36 setLit(sequence[index]);
37 index += 1;
38 timer = setTimeout(() => {
39 setLit(null);
40 timer = setTimeout(showNext, FLASH_MS / 3);
41 }, FLASH_MS);
42 };
43
44 timer = setTimeout(showNext, FLASH_MS);
45 return () => clearTimeout(timer);
46 }, [isPlaying, sequence]);
47
48 const handlePress = (id) => {
49 if (isPlaying || lost || sequence.length === 0) return;
50
51 if (id !== sequence[step]) {
52 setLost(true);
53 setLit(null);
54 return;
55 }
56
57 // Light the pressed pad briefly, so the player gets the same feedback the
58 // computer's playback gives.
59 setLit(id);
60 setTimeout(() => setLit(null), 180);
61
62 if (step === sequence.length - 1) {
63 // Round complete: extend and replay.
64 setStep(0);
65 setSequence((prev) => [...prev, randomPad()]);
66 setIsPlaying(true);
67 } else {
68 setStep(step + 1);
69 }
70 };
71
72 const start = () => {
73 setLost(false);
74 setStep(0);
75 setSequence([randomPad()]);
76 setIsPlaying(true);
77 };
78
79 return (
80 <div>
81 <h1>Simon says</h1>
82
83 <div className="hud">
84 <span>Round: <strong>{sequence.length}</strong></span>
85 <span aria-live="polite">
86 {lost ? '' : isPlaying ? 'Watch…' : sequence.length > 0 ? 'Your turn' : ''}
87 </span>
88 <button className="start" onClick={start}>
89 {sequence.length === 0 ? 'Start' : 'Restart'}
90 </button>
91 </div>
92
93 <div className="pads">
94 {PADS.map((pad) => (
95 <button
96 key={pad.id}
97 className={'pad ' + pad.className + (lit === pad.id ? ' pad-lit' : '')}
98 onClick={() => handlePress(pad.id)}
99 disabled={isPlaying || lost}
100 aria-label={pad.id}
101 />
102 ))}
103 </div>
104
105 {lost && (
106 <p className="over" role="status">
107 Wrong pad. You reached round {sequence.length}.
108 </p>
109 )}
110 </div>
111 );
112}How it works
The hard part is playback, and the reason is that "show these four things one after another" is not something a render can express. It is a sequence of side effects over time, so it belongs in an effect with its own local index — a piece of bookkeeping that exists only for the duration of one playback and would be noise in state.
That local index is also why the cleanup matters so much here. Press Restart halfway through a playback and, without clearTimeout, the old walk keeps going: two sequences light pads over each other and the player sees a pattern that was never in either one.
Disabling the pads during playback does more than prevent cheating — it stops the player from queuing presses that would be checked against a step the playback is about to reset.