Mercurial
comparison 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 |
comparison
equal
deleted
inserted
replaced
| 36:84672efec192 | 37:fb9bcd3145cb |
|---|---|
| 1 import { CSSProperties, memo, useCallback, useReducer } from 'react'; | |
| 2 | |
| 3 enum GameStatus { | |
| 4 PLAYABLE, | |
| 5 WON, | |
| 6 TIE, | |
| 7 } | |
| 8 | |
| 9 enum Player { | |
| 10 O = "O", | |
| 11 X = "X", | |
| 12 } | |
| 13 | |
| 14 type CellValue = Player | ""; | |
| 15 | |
| 16 interface Cell { | |
| 17 value: CellValue; | |
| 18 isWonSquare: boolean; | |
| 19 } | |
| 20 | |
| 21 type Board = Cell[][] | |
| 22 | |
| 23 interface GameState { | |
| 24 board: Board; | |
| 25 status: GameStatus; | |
| 26 player: Player; | |
| 27 winner?: Player | null; | |
| 28 } | |
| 29 | |
| 30 const MAX_ROW = 3; | |
| 31 const MAX_COL = 3; | |
| 32 | |
| 33 const getInitialGameState = (): GameState => { | |
| 34 return { | |
| 35 board: Array.from({ length: MAX_ROW }, | |
| 36 () => Array(MAX_COL).fill( | |
| 37 { | |
| 38 value: "", | |
| 39 isWonSquare: false, | |
| 40 }) as Cell[], | |
| 41 ), | |
| 42 status: GameStatus.PLAYABLE, | |
| 43 player: Player.X, | |
| 44 } | |
| 45 }; | |
| 46 | |
| 47 interface CellComponentProp { | |
| 48 cell: Cell; | |
| 49 row: number; | |
| 50 col: number; | |
| 51 moveDispatch: (r: number, c: number) => void; | |
| 52 } | |
| 53 | |
| 54 const cellCssStyle = (isWinningSquare: boolean): CSSProperties => ({ | |
| 55 display: "flex", | |
| 56 justifyContent: "center", | |
| 57 alignItems: "center", | |
| 58 width: "100%", | |
| 59 aspectRatio: "1 / 1", | |
| 60 background: isWinningSquare ? "blue" : "lightblue", | |
| 61 border: "1px solid black" | |
| 62 }) | |
| 63 | |
| 64 const CellComponent = memo(({ cell, row, col, moveDispatch }: CellComponentProp) => { | |
| 65 const handleOnClick = useCallback(() => { | |
| 66 moveDispatch(row, col); | |
| 67 }, [row,col]) | |
| 68 return ( | |
| 69 <div style={cellCssStyle(cell.isWonSquare)} onClick={handleOnClick}> | |
| 70 {cell.value} | |
| 71 </div> | |
| 72 ) | |
| 73 }) | |
| 74 | |
| 75 const boardCssStyle: CSSProperties = { | |
| 76 display: "grid", | |
| 77 gridTemplateColumns: `repeat(${MAX_COL}, 1fr)`, | |
| 78 width: 500, | |
| 79 } | |
| 80 | |
| 81 enum ActionType { | |
| 82 PLACE, | |
| 83 RESET, | |
| 84 }; | |
| 85 | |
| 86 type Action = | |
| 87 | { type: ActionType.PLACE, row: number, col: number } | |
| 88 | { type: ActionType.RESET }; | |
| 89 | |
| 90 | |
| 91 type GameStatusAndPosition = | |
| 92 | { newStatus: GameStatus.WON, winningPositions: number[][]} | |
| 93 | { newStatus: GameStatus.TIE } | |
| 94 | { newStatus: GameStatus.PLAYABLE } | |
| 95 | |
| 96 | |
| 97 const getGameStatus = (board: Board): GameStatusAndPosition => { | |
| 98 const direction = [ | |
| 99 [0, 1], | |
| 100 [1, 0], | |
| 101 [1, 1], | |
| 102 [1, -1], | |
| 103 ]; | |
| 104 let numberOfFilledValue = 0; | |
| 105 for (let row=0; row<MAX_ROW; row++) { | |
| 106 for (let col=0; col<MAX_COL; col++) { | |
| 107 if (board[row][col].value === "") continue; | |
| 108 numberOfFilledValue++; | |
| 109 for (const [dr, dc] of direction) { | |
| 110 if ( | |
| 111 board[row][col].value === board[row+dr]?.[col+dc]?.value && | |
| 112 board[row][col].value === board[row+(dr*2)]?.[col+(dc*2)]?.value | |
| 113 ) { | |
| 114 return { | |
| 115 newStatus: GameStatus.WON, | |
| 116 winningPositions: [ | |
| 117 [row, col], | |
| 118 [row+dr, col+dc], | |
| 119 [row+dr+dr, col+dc+dc], | |
| 120 ] | |
| 121 } | |
| 122 } | |
| 123 } | |
| 124 } | |
| 125 } | |
| 126 return numberOfFilledValue === MAX_ROW * MAX_COL | |
| 127 ? { newStatus: GameStatus.TIE } | |
| 128 : { newStatus: GameStatus.PLAYABLE }; | |
| 129 } | |
| 130 | |
| 131 const gameStateReducer = (state: GameState, action: Action): GameState => { | |
| 132 if (state.status === GameStatus.WON) return state; | |
| 133 | |
| 134 switch(action.type) { | |
| 135 case ActionType.PLACE: | |
| 136 if (state.board[action.row][action.col].value) return state; | |
| 137 | |
| 138 let newBoard: Board = state.board.map( | |
| 139 (row, rowIdx) => | |
| 140 rowIdx === action.row | |
| 141 ? row.map((col, c) => | |
| 142 c === action.col ? {...col, value: state.player} : col | |
| 143 ) | |
| 144 : row | |
| 145 ); | |
| 146 const res = getGameStatus(newBoard); | |
| 147 let winner: Player | null = null; | |
| 148 if (res.newStatus === GameStatus.WON) { | |
| 149 winner = state.player; | |
| 150 newBoard = newBoard.map((row, rowIdx) => | |
| 151 row.map((col, colIdx) => | |
| 152 res.winningPositions.some(([wr, wc]) => wr === rowIdx && wc === colIdx) | |
| 153 ? { ...col, isWonSquare: true } | |
| 154 : col | |
| 155 ) | |
| 156 ) | |
| 157 } | |
| 158 return { | |
| 159 ...state, | |
| 160 board: newBoard, | |
| 161 status: res.newStatus, | |
| 162 player: Player.X === state.player ? Player.O : Player.X, | |
| 163 winner, | |
| 164 }; | |
| 165 case ActionType.RESET: | |
| 166 return getInitialGameState(); | |
| 167 } | |
| 168 } | |
| 169 | |
| 170 const TicTacToe = () => { | |
| 171 const [state, dispatch] = useReducer( gameStateReducer, null, getInitialGameState); | |
| 172 const moveDispatch = useCallback( | |
| 173 (rowIdx: number, colIdx: number) => dispatch({type: ActionType.PLACE, row: rowIdx, col: colIdx}), []) | |
| 174 return ( | |
| 175 <> | |
| 176 <h1> TicTacToe </h1> | |
| 177 <h2> | |
| 178 { | |
| 179 state.status === GameStatus.PLAYABLE | |
| 180 ? `Player ${state.player} Turn!` | |
| 181 : state.status === GameStatus.TIE | |
| 182 ? "Game is tied! Please reset" | |
| 183 : `Player ${state.winner} has won!` | |
| 184 } | |
| 185 </h2> | |
| 186 <div style={boardCssStyle}> | |
| 187 { | |
| 188 state.board.map((row, rowIdx) => { | |
| 189 return row.map((cell, colIdx) => { | |
| 190 const cellComponentProp: CellComponentProp = { | |
| 191 cell, | |
| 192 row: rowIdx, | |
| 193 col: colIdx, | |
| 194 moveDispatch, | |
| 195 }; | |
| 196 | |
| 197 return ( | |
| 198 <CellComponent | |
| 199 key={`${rowIdx}-${colIdx}`} | |
| 200 {...cellComponentProp} | |
| 201 />) | |
| 202 }) | |
| 203 }) | |
| 204 } | |
| 205 </div> | |
| 206 <button onClick={() => dispatch({ type: ActionType.RESET })}>Reset</button> | |
| 207 </> | |
| 208 ); | |
| 209 } | |
| 210 | |
| 211 export { | |
| 212 TicTacToe, | |
| 213 } | |
| 214 |