comparison react_games/src/Wordle/main.tsx @ 37:fb9bcd3145cb

[ReactGames] Few games I made using react just to practice few things.
author MrJuneJune <me@mrjunejune.com>
date Mon, 01 Dec 2025 20:22:47 -0800
parents
children
comparison
equal deleted inserted replaced
36:84672efec192 37:fb9bcd3145cb
1 import { CSSProperties, useCallback, useEffect, useReducer } from 'react';
2
3 /**
4 * Question:
5
6 * Create a playable Wordle game in the browser using React within one hour. The game should allow the player to guess a hidden 5-letter word in at most 6 tries. After each guess, display feedback for each letter: correct letter and position (green), correct letter wrong position (yellow), or not in the word (gray). The game ends when the player guesses the word or runs out of tries.
7 *
8 * Requirements:
9 *
10 * Choose a fixed 5-letter target word in your code.
11 *
12 * Render a grid showing guesses made and remaining attempts.
13 *
14 * Accept input from the user (keyboard or on-screen).
15 *
16 * Color letters according to correctness after each guess.
17 *
18 * Display a win or lose message at the end of the game.
19 */
20
21 const MAX_WORD_LEN = 5;
22 const MAX_ATTEMP_LEN = 6;
23
24 interface Position {
25 letterPos: number;
26 attempPos: number;
27 }
28
29 enum WordStatus {
30 CORRECT_IN_POSITION,
31 CORRECT_IN_WRONG_POSITION,
32 DEFAULT,
33 }
34
35 interface Letter {
36 value: string;
37 status: WordStatus;
38 }
39
40 type Word = Letter[]
41 type Board = Word[];
42
43 enum GameStatus {
44 PLAYABLE="Try to guess the word!",
45 WON="You won!",
46 LOST="You Lost!",
47 }
48
49 interface GameState {
50 board: Board;
51 targetWord: string[];
52 position: Position;
53 status: GameStatus;
54 }
55
56 const constructGameState = (): GameState => {
57 const board: Board = Array.from(
58 { length: MAX_ATTEMP_LEN }, () => {
59 return Array.from({ length: MAX_WORD_LEN }, () => {
60 return {
61 value: "",
62 status: WordStatus.DEFAULT,
63 }
64 })
65 }
66 );
67 // dummy will get replace when it hits db
68 const targetWord: string[] = "hello".split('');
69 const position: Position = {
70 letterPos: 0,
71 attempPos: 0,
72 }
73 return {
74 board,
75 status: GameStatus.PLAYABLE,
76 targetWord,
77 position,
78 }
79 }
80
81 interface LetterComponentProp {
82 letter: Letter;
83 }
84
85 interface StyleProp {
86 board: CSSProperties;
87 letter: (wordStatus: WordStatus) => CSSProperties;
88 }
89
90 const styles: StyleProp = {
91 board: {
92 display: "grid",
93 gridTemplateColumns: `repeat(${MAX_WORD_LEN}, 1fr)`,
94 width: 400,
95 gap: 8,
96 },
97 letter: (wordStatus: WordStatus): CSSProperties => {
98 let backgroundColor: string = "#e6e6e6";
99 switch(wordStatus) {
100 case WordStatus.CORRECT_IN_POSITION:
101 backgroundColor = "green";
102 break;
103 case WordStatus.CORRECT_IN_WRONG_POSITION:
104 backgroundColor = "yellow";
105 break;
106 case WordStatus.DEFAULT:
107 break;
108 }
109 return {
110 display: "flex",
111 justifyContent: "center",
112 alignItems: "center",
113 backgroundColor,
114 width: "100%",
115 aspectRatio: "1 / 1",
116 border: "1px solid black",
117 }
118 }
119 }
120
121 const LetterComponent = ({letter}: LetterComponentProp) => {
122 return (
123 <div style={styles.letter(letter.status)}>
124 {letter.value}
125 </div>
126 )
127 }
128
129 enum GameActionEnum {
130 INITIALZE,
131 PLACE,
132 };
133
134 type GameAction =
135 | { type: GameActionEnum.PLACE, character: string }
136 | { type: GameActionEnum.INITIALZE, targetWord: string };
137
138 const gameStateReducer = (state: GameState, action: GameAction): GameState => {
139 if (state.status != GameStatus.PLAYABLE) return state;
140
141 switch(action.type) {
142 case GameActionEnum.INITIALZE:
143 return {
144 ...state,
145 targetWord: action.targetWord.split(''),
146 };
147 case GameActionEnum.PLACE:
148 const { character } = action;
149 let newBoard = state.board.map(
150 (word, attempIdx) => attempIdx === state.position.attempPos ?
151 word.map(
152 (letter, letterIdx) => letterIdx === state.position.letterPos ?
153 {...letter, value: character} : letter) : word);
154 const newPosition = calculatePosition(state.position);
155 let newStatus: GameStatus = state.status;
156 if (newPosition.letterPos === 0) {
157 const {correctLetterIdxes, correctWrongPositionLetterIndex} = gameLogic(newBoard[state.position.attempPos], state.targetWord);
158 newBoard = newBoard.map(
159 (word, attempIdx) => attempIdx === state.position.attempPos ?
160 word.map(
161 (letter, letterIdx) => correctLetterIdxes.some((value) => value === letterIdx) ?
162 {...letter, status: WordStatus.CORRECT_IN_POSITION } :
163 (correctWrongPositionLetterIndex.some((value) => value === letterIdx) ?
164 {...letter, status: WordStatus.CORRECT_IN_WRONG_POSITION} : letter)
165 )
166 : word);
167 newStatus = correctLetterIdxes.length === MAX_WORD_LEN ?
168 GameStatus.WON : (
169 state.position.attempPos + 1 === MAX_ATTEMP_LEN ?
170 GameStatus.LOST : GameStatus.PLAYABLE
171 );
172 }
173 return {
174 ...state,
175 board: newBoard,
176 position: newPosition,
177 status: newStatus,
178 };
179 }
180 }
181
182 function gameLogic(word: Word, targetWord: string[]) {
183 const map: Record<string, number> = {};
184 const correctLetterIdxes: number[] = []
185 const correctWrongPositionLetterIndex: number[] = []
186 for (let i = 0; i < MAX_WORD_LEN; i++) {
187 map[targetWord[i]] ? map[targetWord[i]]++ : map[targetWord[i]] = 1;
188 }
189 for (let i = 0; i < MAX_WORD_LEN; i++) {
190 if (word[i].value === targetWord[i]) {
191 correctLetterIdxes.push(i);
192 map[targetWord[i]]--;
193 }
194 }
195 for (let i = 0; i < MAX_WORD_LEN; i++) {
196 if (correctLetterIdxes.some((value) => value===i)) continue;
197 if (
198 targetWord.some((value) => value===word[i].value && map[value] > 0)
199 ) {
200 correctWrongPositionLetterIndex.push(i);
201 map[word[i].value]--;
202 }
203 }
204 return {
205 correctLetterIdxes,
206 correctWrongPositionLetterIndex,
207 }
208 }
209
210 function calculatePosition(position: Position): Position {
211 const letterPos = (position.letterPos + 1) % MAX_WORD_LEN;
212 const attempPos = (
213 (position.letterPos + 1) === MAX_WORD_LEN ?
214 position.attempPos + 1 : position.attempPos) % MAX_ATTEMP_LEN;
215 return {
216 letterPos,
217 attempPos,
218 }
219 }
220
221 const Wordle = () => {
222 const [gameState, gameDispatch] = useReducer(gameStateReducer, null, constructGameState);
223
224 useEffect(() => {
225 (async() => {
226 const res = await fetch('/api/v1/wordle');
227 if (!res.ok) return;
228 const response = await res.json();
229 gameDispatch({ type: GameActionEnum.INITIALZE, targetWord: response.targetWord });
230 })();
231 }, []);
232
233 const placeLetters = useCallback((event: KeyboardEvent) => {
234 const letters = event.key.trim().toLowerCase();
235 const regex = new RegExp('[a-z]');
236 if (
237 !regex.exec(letters) ||
238 letters === "enter"
239 ) {
240 return;
241 }
242 gameDispatch({ type: GameActionEnum.PLACE, character: event.key.trim() })
243 }, [gameDispatch]);
244
245 useEffect(() => {
246 document.addEventListener("keypress", placeLetters)
247 }, [])
248
249 return (
250 <>
251 <h1> Wordle </h1>
252 <h2> {gameState.status} </h2>
253 <div style={styles.board}>
254 {gameState.board.map(
255 (word, wordIdx) => word.map(
256 (letter, letterIdx) => (
257 <LetterComponent key={`${wordIdx}-${letterIdx}`} letter={letter} />
258 ))
259 )}
260 </div>
261 </>
262 );
263 }
264
265 export {
266 Wordle,
267 }