diff react_games/src/Tictactoe/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/Tictactoe/main.tsx	Mon Dec 01 20:22:47 2025 -0800
@@ -0,0 +1,214 @@
+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,
+}
+