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