Mercurial
diff 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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/react_games/src/Connectfour/latest.tsx Mon Dec 01 20:22:47 2025 -0800 @@ -0,0 +1,219 @@ +import { CSSProperties, memo, useCallback, useReducer } from "react"; +import ReactDOM from "react-dom/client"; + +/** + * 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. + * 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. + * There could be human and bot players. + * The pieces fall straight down, occupying the lowest available space within the column. + * 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. + * The first player can always win by playing the right moves. + * + */ + +interface Cell { + value: CellValue; + isHighlighted: boolean; +} + +type CellValue = Player | ""; + +type Board = Cell[][]; + +enum Player { + RED="🔴", + BLUE="🔵", +} + +enum GameStatus { + PLAYABLE = "Your Turn!", + RED_WON = "RED WON!", + BLUE_WON = "BLUE WON!", +} + +interface GameState { + board: Board; + player: Player; + status: GameStatus; +} + +const MAX_ROW = 6; +const MAX_COL = 7; + +const constructGameState = (): GameState => { + return { + board: Array.from( + { length: MAX_ROW }, () => + Array.from({ length: MAX_COL }, (): Cell => ({ value: "", isHighlighted: false }) )), + player: Player.RED, + status: GameStatus.PLAYABLE + } +} + +interface CellComponentProp { + cell: Cell; + placeDispatch: (col: number) => void; + col: number; +} + +interface StylesProp { + board: CSSProperties; + cell: (isHighlighted: boolean) => CSSProperties; + circle: CSSProperties; +} + +const styles: StylesProp = { + board: { + display: "grid", + gridTemplateColumns: `repeat(${MAX_COL}, 1fr)`, + width: 600, + }, + cell: (isHighlighted: boolean) => ({ + width: "100%", + display: "flex", + justifyContent: "center", + alignItems: "center", + aspectRatio: "1 / 1", + border: "1px solid black", + backgroundColor: isHighlighted ? "blue": "lightblue", + }), + circle: { + width: "60%", + borderRadius: 50, + backgroundColor: "aqua", + aspectRatio: "1 / 1", + display: "flex", + justifyContent: "center", + alignItems: "center", + } +} + +type Action = + | { type: "place", col: number } + | { type: "reset" }; + + +const DIRECTION = [ + [0, 1], + [1, 0], + [1, 1], + [1, -1], +] +const calculateGameStatus = (board: Board): { status: GameStatus, winningIndex: number[][] } => { + for (let rowIdx = 0; rowIdx < MAX_ROW; rowIdx++) { + for (let colIdx = 0; colIdx < MAX_COL; colIdx++) { + if (board[rowIdx][colIdx].value === "") continue; + + for (const [dr, dc] of DIRECTION) { + if ( + board[rowIdx][colIdx].value === board[rowIdx+dr]?.[colIdx+dc]?.value && + board[rowIdx][colIdx].value === board[rowIdx+(dr*2)]?.[colIdx+(dc*2)]?.value && + board[rowIdx][colIdx].value === board[rowIdx+(dr*3)]?.[colIdx+(dc*3)]?.value + ) { + return { + status: board[rowIdx][colIdx].value === Player.RED ? GameStatus.RED_WON : GameStatus.BLUE_WON, + winningIndex: [ + [rowIdx,colIdx], + [rowIdx+dr,colIdx+dc], + [rowIdx+(dr*2),colIdx+(dc*2)], + [rowIdx+(dr*3),colIdx+(dc*3)], + ] + } + } + } + } + } + + return { + status: GameStatus.PLAYABLE, + winningIndex: [] + } +} + +const gameStateReducer = (state: GameState, action: Action): GameState => { + if (state.status !== GameStatus.PLAYABLE) return state; + + switch(action.type) { + case "place": { + let selectedRow: number = -1 + for (let rowIdx = MAX_ROW - 1; rowIdx >= 0; rowIdx--) { + if (state.board[rowIdx][action.col].value === "") { + selectedRow = rowIdx; + break; + } + } + if (selectedRow === -1) return state; + let board = state.board.map( + (row, rowIdx) => rowIdx === selectedRow ? + row.map((col, colIdx) => + colIdx === action.col ? { ...col, value: state.player } : col) : row + ); + const player = Player.RED === state.player ? Player.BLUE : Player.RED; + const {status, winningIndex} = calculateGameStatus(board); + if (winningIndex.length > 0) { + board = board.map( + (row, rowIdx) => + row.map((col, colIdx): Cell => + winningIndex.some( + ([r, c]) => (rowIdx === r && colIdx === c)) ? + { ...col, isHighlighted: true } : col + ) + ); + } + return { + board, + player, + status, + }; + } + case "reset": + return constructGameState(); + } +} + +const CellComponent = memo(({cell, col, placeDispatch}: CellComponentProp) => { + const handleOnClick = useCallback(() => { + placeDispatch(col); + },[col, placeDispatch]); + + return ( + <div style={styles.cell(cell.isHighlighted)} onClick={handleOnClick}> + <div style={styles.circle}> + {cell.value} + </div> + </div> + ) +}) + +const ConnectFour = () => { + const [state, dispatch] = useReducer(gameStateReducer, null, constructGameState); + + const placeDispatch = useCallback( + (col: number) => { + dispatch({type: "place", col}) + }, [dispatch]); + + return ( + <> + <h1> {state.status} </h1> + {state.status === GameStatus.PLAYABLE &&(<h2> {state.player} </h2>)} + <div style={styles.board}> + {state.board.map( + (row, rowIdx) => row.map( + (cell, colIdx) => ( + <CellComponent + key={`${rowIdx}-${colIdx}`} + cell={cell} + col={colIdx} + placeDispatch={placeDispatch}/> + ) + ) + )} + </div> + </> + ) +} + +export { + ConnectFour +}