comparison react_games/src/Connectfour/latest.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 5e6a5d3c6868
comparison
equal deleted inserted replaced
36:84672efec192 37:fb9bcd3145cb
1 import { CSSProperties, memo, useCallback, useReducer } from "react";
2 import ReactDOM from "react-dom/client";
3
4 /**
5 * Connect4 game is also known as Four Up, Plot Four, Find Four, Captain’s Mistress, Four in a Row, Drop Four, and Gravitrips in the Soviet Union.
6 * It is a two-player connection board game, in which the players choose a color and then take turns dropping colored discs into a seven-column, six-row vertically suspended grid.
7 * There could be human and bot players.
8 * The pieces fall straight down, occupying the lowest available space within the column.
9 * The objective of the game is to be the first to form a horizontal, vertical, or diagonal line of four of one’s own discs. Connect Four is a solved game.
10 * The first player can always win by playing the right moves.
11 *
12 */
13
14 interface Cell {
15 value: CellValue;
16 isHighlighted: boolean;
17 }
18
19 type CellValue = Player | "";
20
21 type Board = Cell[][];
22
23 enum Player {
24 RED="🔴",
25 BLUE="🔵",
26 }
27
28 enum GameStatus {
29 PLAYABLE = "Your Turn!",
30 RED_WON = "RED WON!",
31 BLUE_WON = "BLUE WON!",
32 }
33
34 interface GameState {
35 board: Board;
36 player: Player;
37 status: GameStatus;
38 }
39
40 const MAX_ROW = 6;
41 const MAX_COL = 7;
42
43 const constructGameState = (): GameState => {
44 return {
45 board: Array.from(
46 { length: MAX_ROW }, () =>
47 Array.from({ length: MAX_COL }, (): Cell => ({ value: "", isHighlighted: false }) )),
48 player: Player.RED,
49 status: GameStatus.PLAYABLE
50 }
51 }
52
53 interface CellComponentProp {
54 cell: Cell;
55 placeDispatch: (col: number) => void;
56 col: number;
57 }
58
59 interface StylesProp {
60 board: CSSProperties;
61 cell: (isHighlighted: boolean) => CSSProperties;
62 circle: CSSProperties;
63 }
64
65 const styles: StylesProp = {
66 board: {
67 display: "grid",
68 gridTemplateColumns: `repeat(${MAX_COL}, 1fr)`,
69 width: 600,
70 },
71 cell: (isHighlighted: boolean) => ({
72 width: "100%",
73 display: "flex",
74 justifyContent: "center",
75 alignItems: "center",
76 aspectRatio: "1 / 1",
77 border: "1px solid black",
78 backgroundColor: isHighlighted ? "blue": "lightblue",
79 }),
80 circle: {
81 width: "60%",
82 borderRadius: 50,
83 backgroundColor: "aqua",
84 aspectRatio: "1 / 1",
85 display: "flex",
86 justifyContent: "center",
87 alignItems: "center",
88 }
89 }
90
91 type Action =
92 | { type: "place", col: number }
93 | { type: "reset" };
94
95
96 const DIRECTION = [
97 [0, 1],
98 [1, 0],
99 [1, 1],
100 [1, -1],
101 ]
102 const calculateGameStatus = (board: Board): { status: GameStatus, winningIndex: number[][] } => {
103 for (let rowIdx = 0; rowIdx < MAX_ROW; rowIdx++) {
104 for (let colIdx = 0; colIdx < MAX_COL; colIdx++) {
105 if (board[rowIdx][colIdx].value === "") continue;
106
107 for (const [dr, dc] of DIRECTION) {
108 if (
109 board[rowIdx][colIdx].value === board[rowIdx+dr]?.[colIdx+dc]?.value &&
110 board[rowIdx][colIdx].value === board[rowIdx+(dr*2)]?.[colIdx+(dc*2)]?.value &&
111 board[rowIdx][colIdx].value === board[rowIdx+(dr*3)]?.[colIdx+(dc*3)]?.value
112 ) {
113 return {
114 status: board[rowIdx][colIdx].value === Player.RED ? GameStatus.RED_WON : GameStatus.BLUE_WON,
115 winningIndex: [
116 [rowIdx,colIdx],
117 [rowIdx+dr,colIdx+dc],
118 [rowIdx+(dr*2),colIdx+(dc*2)],
119 [rowIdx+(dr*3),colIdx+(dc*3)],
120 ]
121 }
122 }
123 }
124 }
125 }
126
127 return {
128 status: GameStatus.PLAYABLE,
129 winningIndex: []
130 }
131 }
132
133 const gameStateReducer = (state: GameState, action: Action): GameState => {
134 if (state.status !== GameStatus.PLAYABLE) return state;
135
136 switch(action.type) {
137 case "place": {
138 let selectedRow: number = -1
139 for (let rowIdx = MAX_ROW - 1; rowIdx >= 0; rowIdx--) {
140 if (state.board[rowIdx][action.col].value === "") {
141 selectedRow = rowIdx;
142 break;
143 }
144 }
145 if (selectedRow === -1) return state;
146 let board = state.board.map(
147 (row, rowIdx) => rowIdx === selectedRow ?
148 row.map((col, colIdx) =>
149 colIdx === action.col ? { ...col, value: state.player } : col) : row
150 );
151 const player = Player.RED === state.player ? Player.BLUE : Player.RED;
152 const {status, winningIndex} = calculateGameStatus(board);
153 if (winningIndex.length > 0) {
154 board = board.map(
155 (row, rowIdx) =>
156 row.map((col, colIdx): Cell =>
157 winningIndex.some(
158 ([r, c]) => (rowIdx === r && colIdx === c)) ?
159 { ...col, isHighlighted: true } : col
160 )
161 );
162 }
163 return {
164 board,
165 player,
166 status,
167 };
168 }
169 case "reset":
170 return constructGameState();
171 }
172 }
173
174 const CellComponent = memo(({cell, col, placeDispatch}: CellComponentProp) => {
175 const handleOnClick = useCallback(() => {
176 placeDispatch(col);
177 },[col, placeDispatch]);
178
179 return (
180 <div style={styles.cell(cell.isHighlighted)} onClick={handleOnClick}>
181 <div style={styles.circle}>
182 {cell.value}
183 </div>
184 </div>
185 )
186 })
187
188 const ConnectFour = () => {
189 const [state, dispatch] = useReducer(gameStateReducer, null, constructGameState);
190
191 const placeDispatch = useCallback(
192 (col: number) => {
193 dispatch({type: "place", col})
194 }, [dispatch]);
195
196 return (
197 <>
198 <h1> {state.status} </h1>
199 {state.status === GameStatus.PLAYABLE &&(<h2> {state.player} </h2>)}
200 <div style={styles.board}>
201 {state.board.map(
202 (row, rowIdx) => row.map(
203 (cell, colIdx) => (
204 <CellComponent
205 key={`${rowIdx}-${colIdx}`}
206 cell={cell}
207 col={colIdx}
208 placeDispatch={placeDispatch}/>
209 )
210 )
211 )}
212 </div>
213 </>
214 )
215 }
216
217 export {
218 ConnectFour
219 }