Mercurial
diff react_games/src/Connectfour/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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/react_games/src/Connectfour/main.tsx Mon Dec 01 20:22:47 2025 -0800 @@ -0,0 +1,141 @@ +import { + useReducer, createContext, useContext, memo, Dispatch +} from 'react' + +enum P { R = '🔴', Y = '🟡', _ = '' } + +type Board = P[][] // 6 rows × 7 cols +type Status = 'PLAYING' | 'TIE' | 'R_WON' | 'Y_WON' + +type Action = + | { type: 'DROP'; col: number } + | { type: 'RESET' } + +interface State { + board: Board + turn: P.R | P.Y + status: Status +} + +const ROWS = 6, COLS = 7 +const emptyBoard = (): Board => + Array.from({ length: ROWS }, () => Array(COLS).fill(P._)) + +const init: State = { board: emptyBoard(), turn: P.R, status: 'PLAYING' } + +function firstEmptyRow(board: Board, col: number): number | null { + for (let r = ROWS - 1; r >= 0; r--) if (board[r][col] === P._) return r + return null +} + +function scanWinner(b: Board): Status { + const lines = [ + [ 1, 0], [0, 1], // vertical, horizontal + [ 1, 1], [1, -1], // two diagonals + ] + for (let r = 0; r < ROWS; r++) + for (let c = 0; c < COLS; c++) + if (b[r][c] !== P._) + for (const [dr, dc] of lines) + if ( + b[r + dr]?.[c + dc] === b[r][c] && + b[r + 2*dr]?.[c + 2*dc] === b[r][c] && + b[r + 3*dr]?.[c + 3*dc] === b[r][c] + ) + return b[r][c] === P.R ? 'R_WON' : 'Y_WON' + + return b.flat().every(p => p !== P._) ? 'TIE' : 'PLAYING' +} + + +function reducer(s: State, a: Action): State { + if (a.type === 'RESET') return init + if (s.status !== 'PLAYING') return s // game over + + const row = firstEmptyRow(s.board, a.col) + if (row == null) return s // full column + + // clone touched row only + const newRow = [...s.board[row]] + newRow[a.col] = s.turn + const newBoard = s.board.map((r, i) => (i === row ? newRow : r)) + + const nextTurn = s.turn === P.R ? P.Y : P.R + const status = scanWinner(newBoard) + + return { board: newBoard, turn: nextTurn, status } +} + +const DispatchCtx = createContext<Dispatch<Action> | null>(null) +const useGameDispatch = () => { + const d = useContext(DispatchCtx) + if (!d) throw new Error('outside provider') + return d +} + +/* ------- Leaf ------- */ +interface CellProps { v: P } +const Cell = memo<CellProps>(({ v }) => ( + <div style={{ + width: 52, height: 52, margin: 2, borderRadius: '50%', + background: '#0e2a5a', display: 'grid', placeItems: 'center', + fontSize: 30 + }}> + {v} + </div> +)) + +/* ------- Column button ------- */ +const ColBtn = ({ col }: { col: number }) => { + const dispatch = useGameDispatch() + return ( + <button + style={{ flex: 1, height: 20, cursor: 'pointer' }} + onClick={() => dispatch({ type: 'DROP', col })} + /> + ) +} + +/* ------- Board ------- */ +const BoardView = memo(({ board }: { board: Board }) => ( + <div> + {/* clickable top buttons */} + <div style={{ display: 'flex' }}> + {Array.from({ length: COLS }, (_, c) => <ColBtn key={c} col={c} />)} + </div> + + {/* grid */} + {board.map((row, r) => ( + <div key={r} style={{ display: 'flex' }}> + {row.map((v, c) => <Cell key={c} v={v} />)} + </div> + ))} + </div> +)) + +function ConnectFour() { + const [state, dispatch] = useReducer(reducer, init) + + return ( + <DispatchCtx.Provider value={dispatch}> + <h2 style={{ textAlign: 'center' }}> + {state.status === 'PLAYING' && `Turn: ${state.turn}`} + {state.status === 'TIE' && 'Tie game'} + {state.status === 'R_WON' && 'Red wins!'} + {state.status === 'Y_WON' && 'Yellow wins!'} + </h2> + + <BoardView board={state.board} /> + + {state.status !== 'PLAYING' && ( + <button style={{ marginTop: 16 }} onClick={() => dispatch({ type: 'RESET' })}> + Play again + </button> + )} + </DispatchCtx.Provider> + ) +} + +export { + ConnectFour, +}