Mercurial
view react_games/src/LightsOut/main.tsx @ 192:b818a4561a3c hg-web
Added AI genreated README.md. Needed to be read.
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Sat, 24 Jan 2026 21:52:14 -0800 |
| parents | fb9bcd3145cb |
| children |
line wrap: on
line source
import { ActionDispatch, CSSProperties, memo, useCallback, useReducer } from 'react'; /** * create lights out game * * - 5 x 5 grid * - when click it will turn on lights on top left right bottom. * - count number of moves * - timers * - when every thing is on then tthey win */ interface Cell { value: boolean; // true on and false off. } type Board = Cell[][]; enum GameStatus { PLAYABLE, WON, } interface GameState { board: Board; status: GameStatus; moves: number; } const MAX_COL = 5; const MAX_ROW = 5; const DIRECTION = [ [0, 0], [0, 1], [1, 0], [0, -1], [-1, 0], ] const constructGameState = (): GameState => { const board = Array.from( { length: MAX_ROW }, () => Array.from({ length: MAX_COL}, () => ({ value: false }) ) ); for (let i = 0; i < 30; i++) { const r = Math.floor(Math.random() * MAX_ROW); const c = Math.floor(Math.random() * MAX_COL); for (const [dr, dc] of DIRECTION) { if (board[r+dr]?.[c+dc]) { board[r+dr][c+dc].value = !board[r+dr]?.[c+dc].value } } } return { board, status: GameStatus.PLAYABLE, moves: 0, } } interface Styles { board: CSSProperties; cell: (isLightOn: boolean) => CSSProperties; } const styles: Styles = { board: { display: "grid", gridTemplateColumns: `repeat(${MAX_COL}, 1fr)`, width: MAX_COL * 30, }, cell: (isLightOn: boolean) => ({ width: "100%", aspectRatio: "1 / 1", border: `1px solid #222`, backgroundColor: isLightOn ? "white" : "black", transition: "background-color 120ms ease", cursor: "pointer", }) } interface CellComponentProp { cellValue: Cell; rowPos: number; colPos: number; dispatch: ActionDispatch<[action: Action]>; } const CellComponent = memo(({ cellValue, rowPos, colPos, dispatch }: CellComponentProp) => { const onClickHandler = useCallback(()=>{ dispatch({type: "place", newRowPos: rowPos, newColPos: colPos}); }, [rowPos, colPos, dispatch]); return ( <div onClick={onClickHandler} style={styles.cell(cellValue.value)}></div> ) }) type Action = | { type: "place", newRowPos: number, newColPos: number } | { type: "reset" } const gameStateReducer = (state: GameState, action: Action): GameState => { if (state.status === GameStatus.WON) return state; switch(action.type) { case "place": const allDirection: number[][] = []; for (const [dr, dc] of DIRECTION) { allDirection.push([action.newRowPos + dr, action.newColPos + dc]); } const newBoard: Board = state.board.map( (row, rowIdx) => row.map((col, colIdx) => (allDirection.some( ([nr, nc]) => rowIdx === nr && colIdx === nc )) ? { ...col, value: !col.value } : col) ) const newStatus = (newBoard.flat().filter((cell) => cell.value).length === MAX_COL * MAX_ROW) ? GameStatus.WON : GameStatus.PLAYABLE; return { ...state, status: newStatus, board: newBoard, moves: state.moves + 1, }; case "reset": return constructGameState(); } } const LightsOut = () => { const [gameState, dispatch] = useReducer(gameStateReducer, null, constructGameState); return ( <> <h1> Game Status {gameState.status}</h1> <h2> Number of moves so far {gameState.moves}</h2> <div style={styles.board}> { gameState.board.map( (row, rowIdx) => row.map( (cellValue, colIdx) => (<CellComponent key={`${rowIdx}-${colIdx}`} cellValue={cellValue} rowPos={rowIdx} colPos={colIdx} dispatch={dispatch}/>) ) ) } </div> </> ); } export { LightsOut }