Mercurial
view react_games/src/Connectfour/latest.tsx @ 213:60918f88070e
Simple change regarding to accessibility.
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Sun, 15 Feb 2026 21:39:43 -0800 |
| parents | 5e6a5d3c6868 |
| children |
line wrap: on
line source
import { CSSProperties, memo, useCallback, useReducer } from "react"; /** * 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 }