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