Mercurial
comparison 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 |
comparison
equal
deleted
inserted
replaced
| 36:84672efec192 | 37:fb9bcd3145cb |
|---|---|
| 1 import { | |
| 2 useReducer, createContext, useContext, memo, Dispatch | |
| 3 } from 'react' | |
| 4 | |
| 5 enum P { R = '🔴', Y = '🟡', _ = '' } | |
| 6 | |
| 7 type Board = P[][] // 6 rows × 7 cols | |
| 8 type Status = 'PLAYING' | 'TIE' | 'R_WON' | 'Y_WON' | |
| 9 | |
| 10 type Action = | |
| 11 | { type: 'DROP'; col: number } | |
| 12 | { type: 'RESET' } | |
| 13 | |
| 14 interface State { | |
| 15 board: Board | |
| 16 turn: P.R | P.Y | |
| 17 status: Status | |
| 18 } | |
| 19 | |
| 20 const ROWS = 6, COLS = 7 | |
| 21 const emptyBoard = (): Board => | |
| 22 Array.from({ length: ROWS }, () => Array(COLS).fill(P._)) | |
| 23 | |
| 24 const init: State = { board: emptyBoard(), turn: P.R, status: 'PLAYING' } | |
| 25 | |
| 26 function firstEmptyRow(board: Board, col: number): number | null { | |
| 27 for (let r = ROWS - 1; r >= 0; r--) if (board[r][col] === P._) return r | |
| 28 return null | |
| 29 } | |
| 30 | |
| 31 function scanWinner(b: Board): Status { | |
| 32 const lines = [ | |
| 33 [ 1, 0], [0, 1], // vertical, horizontal | |
| 34 [ 1, 1], [1, -1], // two diagonals | |
| 35 ] | |
| 36 for (let r = 0; r < ROWS; r++) | |
| 37 for (let c = 0; c < COLS; c++) | |
| 38 if (b[r][c] !== P._) | |
| 39 for (const [dr, dc] of lines) | |
| 40 if ( | |
| 41 b[r + dr]?.[c + dc] === b[r][c] && | |
| 42 b[r + 2*dr]?.[c + 2*dc] === b[r][c] && | |
| 43 b[r + 3*dr]?.[c + 3*dc] === b[r][c] | |
| 44 ) | |
| 45 return b[r][c] === P.R ? 'R_WON' : 'Y_WON' | |
| 46 | |
| 47 return b.flat().every(p => p !== P._) ? 'TIE' : 'PLAYING' | |
| 48 } | |
| 49 | |
| 50 | |
| 51 function reducer(s: State, a: Action): State { | |
| 52 if (a.type === 'RESET') return init | |
| 53 if (s.status !== 'PLAYING') return s // game over | |
| 54 | |
| 55 const row = firstEmptyRow(s.board, a.col) | |
| 56 if (row == null) return s // full column | |
| 57 | |
| 58 // clone touched row only | |
| 59 const newRow = [...s.board[row]] | |
| 60 newRow[a.col] = s.turn | |
| 61 const newBoard = s.board.map((r, i) => (i === row ? newRow : r)) | |
| 62 | |
| 63 const nextTurn = s.turn === P.R ? P.Y : P.R | |
| 64 const status = scanWinner(newBoard) | |
| 65 | |
| 66 return { board: newBoard, turn: nextTurn, status } | |
| 67 } | |
| 68 | |
| 69 const DispatchCtx = createContext<Dispatch<Action> | null>(null) | |
| 70 const useGameDispatch = () => { | |
| 71 const d = useContext(DispatchCtx) | |
| 72 if (!d) throw new Error('outside provider') | |
| 73 return d | |
| 74 } | |
| 75 | |
| 76 /* ------- Leaf ------- */ | |
| 77 interface CellProps { v: P } | |
| 78 const Cell = memo<CellProps>(({ v }) => ( | |
| 79 <div style={{ | |
| 80 width: 52, height: 52, margin: 2, borderRadius: '50%', | |
| 81 background: '#0e2a5a', display: 'grid', placeItems: 'center', | |
| 82 fontSize: 30 | |
| 83 }}> | |
| 84 {v} | |
| 85 </div> | |
| 86 )) | |
| 87 | |
| 88 /* ------- Column button ------- */ | |
| 89 const ColBtn = ({ col }: { col: number }) => { | |
| 90 const dispatch = useGameDispatch() | |
| 91 return ( | |
| 92 <button | |
| 93 style={{ flex: 1, height: 20, cursor: 'pointer' }} | |
| 94 onClick={() => dispatch({ type: 'DROP', col })} | |
| 95 /> | |
| 96 ) | |
| 97 } | |
| 98 | |
| 99 /* ------- Board ------- */ | |
| 100 const BoardView = memo(({ board }: { board: Board }) => ( | |
| 101 <div> | |
| 102 {/* clickable top buttons */} | |
| 103 <div style={{ display: 'flex' }}> | |
| 104 {Array.from({ length: COLS }, (_, c) => <ColBtn key={c} col={c} />)} | |
| 105 </div> | |
| 106 | |
| 107 {/* grid */} | |
| 108 {board.map((row, r) => ( | |
| 109 <div key={r} style={{ display: 'flex' }}> | |
| 110 {row.map((v, c) => <Cell key={c} v={v} />)} | |
| 111 </div> | |
| 112 ))} | |
| 113 </div> | |
| 114 )) | |
| 115 | |
| 116 function ConnectFour() { | |
| 117 const [state, dispatch] = useReducer(reducer, init) | |
| 118 | |
| 119 return ( | |
| 120 <DispatchCtx.Provider value={dispatch}> | |
| 121 <h2 style={{ textAlign: 'center' }}> | |
| 122 {state.status === 'PLAYING' && `Turn: ${state.turn}`} | |
| 123 {state.status === 'TIE' && 'Tie game'} | |
| 124 {state.status === 'R_WON' && 'Red wins!'} | |
| 125 {state.status === 'Y_WON' && 'Yellow wins!'} | |
| 126 </h2> | |
| 127 | |
| 128 <BoardView board={state.board} /> | |
| 129 | |
| 130 {state.status !== 'PLAYING' && ( | |
| 131 <button style={{ marginTop: 16 }} onClick={() => dispatch({ type: 'RESET' })}> | |
| 132 Play again | |
| 133 </button> | |
| 134 )} | |
| 135 </DispatchCtx.Provider> | |
| 136 ) | |
| 137 } | |
| 138 | |
| 139 export { | |
| 140 ConnectFour, | |
| 141 } |