view react_games/src/Wordle/main.tsx @ 93:be91a73d801a

[MrJuneJune] Updated my website.
author June Park <parkjune1995@gmail.com>
date Fri, 02 Jan 2026 18:02:22 -0800
parents fb9bcd3145cb
children
line wrap: on
line source

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,
}