diff 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
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/react_games/src/Wordle/main.tsx	Mon Dec 01 20:22:47 2025 -0800
@@ -0,0 +1,267 @@
+import { CSSProperties, useCallback, useEffect, useReducer } from 'react';
+
+/**
+ * Question:
+
+ * 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.
+ * 
+ * Requirements:
+ * 
+ * Choose a fixed 5-letter target word in your code.
+ * 
+ * Render a grid showing guesses made and remaining attempts.
+ * 
+ * Accept input from the user (keyboard or on-screen).
+ * 
+ * Color letters according to correctness after each guess.
+ * 
+ * Display a win or lose message at the end of the game.
+ */
+
+const MAX_WORD_LEN = 5;
+const MAX_ATTEMP_LEN = 6;
+
+interface Position {
+  letterPos: number;
+  attempPos: number;
+}
+
+enum WordStatus {
+  CORRECT_IN_POSITION,
+  CORRECT_IN_WRONG_POSITION,
+  DEFAULT,
+}
+
+interface Letter {
+  value: string;
+  status: WordStatus;
+}
+
+type Word = Letter[]
+type Board = Word[];
+
+enum GameStatus {
+  PLAYABLE="Try to guess the word!",
+  WON="You won!",
+  LOST="You Lost!",
+}
+
+interface GameState {
+  board: Board;
+  targetWord: string[];
+  position: Position;
+  status: GameStatus;
+}
+
+const constructGameState = (): GameState => {
+  const board: Board = Array.from(
+    { length: MAX_ATTEMP_LEN }, () => {
+      return Array.from({ length: MAX_WORD_LEN }, () => {
+        return {
+          value: "",
+          status: WordStatus.DEFAULT,
+        }
+      })
+    }
+  );
+  // dummy will get replace when it hits db
+  const targetWord: string[] = "hello".split('');
+  const position: Position = {
+    letterPos: 0,
+    attempPos: 0,
+  }
+  return {
+    board,
+    status: GameStatus.PLAYABLE,
+    targetWord,
+    position,
+  }
+}
+
+interface LetterComponentProp {
+  letter: Letter;
+}
+
+interface StyleProp {
+  board: CSSProperties;
+  letter: (wordStatus: WordStatus) =>  CSSProperties;
+}
+
+const styles: StyleProp = {
+  board: {
+    display: "grid",
+    gridTemplateColumns: `repeat(${MAX_WORD_LEN}, 1fr)`,
+    width: 400,
+    gap: 8,
+  },
+  letter: (wordStatus: WordStatus): CSSProperties => {
+    let backgroundColor: string = "#e6e6e6";
+    switch(wordStatus) {
+      case WordStatus.CORRECT_IN_POSITION:
+        backgroundColor = "green";
+        break;
+      case WordStatus.CORRECT_IN_WRONG_POSITION:
+        backgroundColor = "yellow";
+        break;
+      case WordStatus.DEFAULT:
+        break;
+    }
+    return {
+      display: "flex",
+      justifyContent: "center",
+      alignItems: "center",
+      backgroundColor,
+      width: "100%",
+      aspectRatio: "1 / 1",
+      border: "1px solid black",
+    }
+  }
+}
+
+const LetterComponent = ({letter}: LetterComponentProp) => {
+  return (
+    <div style={styles.letter(letter.status)}> 
+     {letter.value}
+    </div>
+  )
+}
+
+enum GameActionEnum {
+  INITIALZE,
+  PLACE,
+};
+
+type GameAction =
+  | { type: GameActionEnum.PLACE, character: string }
+  | { type: GameActionEnum.INITIALZE, targetWord: string };
+
+const gameStateReducer = (state: GameState, action: GameAction): GameState => {
+  if (state.status != GameStatus.PLAYABLE) return state;
+
+  switch(action.type) {
+    case GameActionEnum.INITIALZE:
+      return {
+        ...state,
+        targetWord: action.targetWord.split(''),
+      };
+    case GameActionEnum.PLACE:
+      const { character } = action;
+      let newBoard = state.board.map(
+        (word, attempIdx) => attempIdx === state.position.attempPos ?
+          word.map(
+            (letter, letterIdx) => letterIdx === state.position.letterPos ?
+              {...letter, value: character} : letter) : word);
+      const newPosition = calculatePosition(state.position);
+      let newStatus: GameStatus = state.status;
+      if (newPosition.letterPos === 0) {
+        const {correctLetterIdxes, correctWrongPositionLetterIndex} = gameLogic(newBoard[state.position.attempPos], state.targetWord);
+        newBoard = newBoard.map(
+          (word, attempIdx) => attempIdx === state.position.attempPos ?
+            word.map(
+              (letter, letterIdx) => correctLetterIdxes.some((value) => value === letterIdx) ?
+                {...letter, status: WordStatus.CORRECT_IN_POSITION } : 
+                  (correctWrongPositionLetterIndex.some((value) => value === letterIdx) ? 
+                    {...letter, status: WordStatus.CORRECT_IN_WRONG_POSITION} : letter)
+            )
+         : word);
+         newStatus = correctLetterIdxes.length === MAX_WORD_LEN ? 
+           GameStatus.WON : (
+             state.position.attempPos + 1 === MAX_ATTEMP_LEN ? 
+               GameStatus.LOST : GameStatus.PLAYABLE
+         );
+      }
+      return {
+        ...state,
+        board: newBoard,
+        position: newPosition,
+        status: newStatus,
+      };
+  }
+}
+
+function gameLogic(word: Word, targetWord: string[]) {
+  const map: Record<string, number> = {};
+  const correctLetterIdxes: number[] = []
+  const correctWrongPositionLetterIndex: number[] = []
+  for (let i = 0; i < MAX_WORD_LEN; i++) {
+    map[targetWord[i]] ? map[targetWord[i]]++ : map[targetWord[i]] = 1;
+  }
+  for (let i = 0; i < MAX_WORD_LEN; i++) {
+    if (word[i].value === targetWord[i]) {
+      correctLetterIdxes.push(i);
+      map[targetWord[i]]--;
+    }
+  }
+  for (let i = 0; i < MAX_WORD_LEN; i++) {
+    if (correctLetterIdxes.some((value) => value===i)) continue;
+    if (
+      targetWord.some((value) => value===word[i].value && map[value] > 0)
+    ) {
+      correctWrongPositionLetterIndex.push(i);
+      map[word[i].value]--;
+    }
+  }
+  return {
+    correctLetterIdxes,
+    correctWrongPositionLetterIndex,
+  }
+}
+
+function calculatePosition(position: Position): Position {
+  const letterPos = (position.letterPos + 1) % MAX_WORD_LEN;
+  const attempPos = (
+    (position.letterPos + 1) === MAX_WORD_LEN ? 
+      position.attempPos + 1 : position.attempPos) % MAX_ATTEMP_LEN;
+  return {
+    letterPos,
+    attempPos,
+  }
+}
+
+const Wordle = () => {
+  const [gameState, gameDispatch] = useReducer(gameStateReducer, null, constructGameState);
+
+  useEffect(() => {
+    (async() => {
+      const res = await fetch('/api/v1/wordle');
+      if (!res.ok) return;
+      const response = await res.json();
+      gameDispatch({ type: GameActionEnum.INITIALZE, targetWord: response.targetWord });
+    })();
+  }, []);
+
+  const placeLetters = useCallback((event: KeyboardEvent) => {
+    const letters = event.key.trim().toLowerCase();
+    const regex = new RegExp('[a-z]');
+    if (
+      !regex.exec(letters) ||
+      letters === "enter"
+    ) {
+      return;
+    }
+    gameDispatch({ type: GameActionEnum.PLACE, character: event.key.trim() })
+  }, [gameDispatch]);
+
+  useEffect(() => {
+    document.addEventListener("keypress", placeLetters)
+  }, [])
+
+  return (
+    <>
+      <h1> Wordle </h1>
+      <h2> {gameState.status} </h2>
+      <div style={styles.board}>
+        {gameState.board.map(
+          (word, wordIdx) => word.map(
+            (letter, letterIdx) => (
+              <LetterComponent key={`${wordIdx}-${letterIdx}`} letter={letter} />
+            ))
+        )} 
+      </div>
+    </>
+  );
+}
+
+export {
+  Wordle,
+}