view react_games/src/Tictactoe/main.tsx @ 71:75de5903355c

Giagantic changes that update Dowa library to be more align with stb style array and hashmap. Updated Seobeo to be caching on server side instead of file level caching. Deleted bunch of things I don't really use.
author June Park <parkjune1995@gmail.com>
date Sun, 28 Dec 2025 20:34:22 -0800
parents fb9bcd3145cb
children
line wrap: on
line source

import { CSSProperties, memo, useCallback, useReducer } from 'react';

enum GameStatus {
  PLAYABLE,
  WON,
  TIE,
}

enum Player {
  O = "O",
  X = "X",
}

type CellValue = Player | "";

interface Cell {
  value: CellValue; 
  isWonSquare: boolean;
}

type Board = Cell[][]

interface GameState {
  board: Board;
  status: GameStatus; 
  player: Player;
  winner?: Player | null;
}

const MAX_ROW = 3;
const MAX_COL = 3;

const getInitialGameState = (): GameState => {
  return {
    board: Array.from({ length: MAX_ROW }, 
      () => Array(MAX_COL).fill(
        {
          value: "",
          isWonSquare: false,
        }) as Cell[],
    ),
    status: GameStatus.PLAYABLE,
    player: Player.X,
  }
};

interface CellComponentProp {
  cell: Cell;
  row: number;
  col: number;
  moveDispatch: (r: number, c: number) => void;
}

const cellCssStyle = (isWinningSquare: boolean): CSSProperties => ({
  display: "flex",
  justifyContent: "center",
  alignItems: "center",
  width: "100%",
  aspectRatio: "1 / 1",
  background: isWinningSquare ? "blue" : "lightblue",
  border: "1px solid black"
})

const CellComponent = memo(({ cell, row, col, moveDispatch }: CellComponentProp) => {
  const handleOnClick = useCallback(() => {
    moveDispatch(row, col);
  }, [row,col])
  return (
    <div style={cellCssStyle(cell.isWonSquare)} onClick={handleOnClick}> 
      {cell.value}
    </div>
  )
})

const boardCssStyle: CSSProperties = {
  display: "grid",
  gridTemplateColumns: `repeat(${MAX_COL}, 1fr)`,
  width: 500,
}

enum ActionType {
  PLACE,
  RESET,
};

type Action =
  | { type: ActionType.PLACE, row: number, col: number }
  | { type: ActionType.RESET };


type GameStatusAndPosition = 
  | { newStatus: GameStatus.WON, winningPositions: number[][]}
  | { newStatus: GameStatus.TIE }
  | { newStatus: GameStatus.PLAYABLE }


const getGameStatus = (board: Board): GameStatusAndPosition => {
  const direction = [
    [0, 1],
    [1, 0],
    [1, 1],
    [1, -1],
  ];
  let numberOfFilledValue = 0;
  for (let row=0; row<MAX_ROW; row++) {
    for (let col=0; col<MAX_COL; col++) {
      if (board[row][col].value === "") continue;
      numberOfFilledValue++;
      for (const [dr, dc] of direction) {
        if (
          board[row][col].value === board[row+dr]?.[col+dc]?.value &&
          board[row][col].value === board[row+(dr*2)]?.[col+(dc*2)]?.value
        ) {
          return {
            newStatus: GameStatus.WON,
            winningPositions: [
              [row, col],
              [row+dr, col+dc],
              [row+dr+dr, col+dc+dc],
            ]
          }
        }
      }
    }
  }
  return numberOfFilledValue === MAX_ROW * MAX_COL
    ? { newStatus: GameStatus.TIE }
    : { newStatus: GameStatus.PLAYABLE };
}

const gameStateReducer = (state: GameState, action: Action): GameState => {
  if (state.status === GameStatus.WON) return state;

  switch(action.type) {
    case ActionType.PLACE:
      if (state.board[action.row][action.col].value) return state;

      let newBoard: Board = state.board.map(
        (row, rowIdx) =>
          rowIdx === action.row
            ? row.map((col, c) => 
                c === action.col ? {...col, value: state.player} : col
              )
            : row
      );
      const res = getGameStatus(newBoard);
      let winner: Player | null = null;
      if (res.newStatus === GameStatus.WON) {
        winner = state.player; 
        newBoard = newBoard.map((row, rowIdx) => 
            row.map((col, colIdx) => 
              res.winningPositions.some(([wr, wc]) => wr === rowIdx && wc === colIdx)
                ? { ...col, isWonSquare: true }
                : col
            )
        )
      }
      return {
        ...state,
        board: newBoard,
        status: res.newStatus,
        player: Player.X === state.player ? Player.O : Player.X,
        winner,
      };
    case ActionType.RESET:
      return getInitialGameState();
  }
}

const TicTacToe = () => {
  const [state, dispatch] = useReducer( gameStateReducer, null, getInitialGameState);
  const moveDispatch = useCallback(
    (rowIdx: number, colIdx: number) => dispatch({type: ActionType.PLACE, row: rowIdx, col: colIdx}), [])
  return (
    <>
      <h1> TicTacToe </h1>
      <h2>
        {
          state.status === GameStatus.PLAYABLE
            ? `Player ${state.player} Turn!`
            : state.status === GameStatus.TIE
              ? "Game is tied! Please reset"
              : `Player ${state.winner} has won!`
        }
      </h2>
      <div style={boardCssStyle}>
        {
          state.board.map((row, rowIdx) => {
            return row.map((cell, colIdx) => {
              const cellComponentProp: CellComponentProp = {
                cell,
                row: rowIdx,
                col: colIdx,
                moveDispatch,
              };

              return (
                <CellComponent 
                    key={`${rowIdx}-${colIdx}`}
                    {...cellComponentProp}
                />)
            })
          })
        }
      </div>
      <button onClick={() => dispatch({ type: ActionType.RESET })}>Reset</button>
    </>
  );
}

export {
  TicTacToe,
}