view react_games/src/Tictactoe/main.tsx @ 172:0face9898d04

[PostDog] Small changes.
author MrJuneJune <me@mrjunejune.com>
date Mon, 19 Jan 2026 18:56:54 -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,
}