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