Mercurial
comparison react_games/src/LightsOut/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 { ActionDispatch, CSSProperties, memo, useCallback, useReducer } from 'react'; | |
| 2 | |
| 3 /** | |
| 4 * create lights out game | |
| 5 * | |
| 6 * - 5 x 5 grid | |
| 7 * - when click it will turn on lights on top left right bottom. | |
| 8 * - count number of moves | |
| 9 * - timers | |
| 10 * - when every thing is on then tthey win | |
| 11 */ | |
| 12 | |
| 13 interface Cell { | |
| 14 value: boolean; // true on and false off. | |
| 15 } | |
| 16 | |
| 17 type Board = Cell[][]; | |
| 18 | |
| 19 enum GameStatus { | |
| 20 PLAYABLE, | |
| 21 WON, | |
| 22 } | |
| 23 | |
| 24 interface GameState { | |
| 25 board: Board; | |
| 26 status: GameStatus; | |
| 27 moves: number; | |
| 28 } | |
| 29 | |
| 30 const MAX_COL = 5; | |
| 31 const MAX_ROW = 5; | |
| 32 const DIRECTION = [ | |
| 33 [0, 0], | |
| 34 [0, 1], | |
| 35 [1, 0], | |
| 36 [0, -1], | |
| 37 [-1, 0], | |
| 38 ] | |
| 39 const constructGameState = (): GameState => { | |
| 40 const board = Array.from( | |
| 41 { length: MAX_ROW }, | |
| 42 () => Array.from({ length: MAX_COL}, | |
| 43 () => ({ value: false }) | |
| 44 ) | |
| 45 ); | |
| 46 | |
| 47 for (let i = 0; i < 30; i++) { | |
| 48 const r = Math.floor(Math.random() * MAX_ROW); | |
| 49 const c = Math.floor(Math.random() * MAX_COL); | |
| 50 for (const [dr, dc] of DIRECTION) { | |
| 51 if (board[r+dr]?.[c+dc]) { | |
| 52 board[r+dr][c+dc].value = !board[r+dr]?.[c+dc].value | |
| 53 } | |
| 54 } | |
| 55 } | |
| 56 return { | |
| 57 board, | |
| 58 status: GameStatus.PLAYABLE, | |
| 59 moves: 0, | |
| 60 } | |
| 61 } | |
| 62 | |
| 63 | |
| 64 interface Styles { | |
| 65 board: CSSProperties; | |
| 66 cell: (isLightOn: boolean) => CSSProperties; | |
| 67 } | |
| 68 | |
| 69 const styles: Styles = { | |
| 70 board: { | |
| 71 display: "grid", | |
| 72 gridTemplateColumns: `repeat(${MAX_COL}, 1fr)`, | |
| 73 width: MAX_COL * 30, | |
| 74 }, | |
| 75 cell: (isLightOn: boolean) => ({ | |
| 76 width: "100%", | |
| 77 aspectRatio: "1 / 1", | |
| 78 border: `1px solid #222`, | |
| 79 backgroundColor: isLightOn ? "white" : "black", | |
| 80 transition: "background-color 120ms ease", | |
| 81 cursor: "pointer", | |
| 82 }) | |
| 83 } | |
| 84 | |
| 85 interface CellComponentProp { | |
| 86 cellValue: Cell; | |
| 87 rowPos: number; | |
| 88 colPos: number; | |
| 89 dispatch: ActionDispatch<[action: Action]>; | |
| 90 } | |
| 91 | |
| 92 const CellComponent = memo(({ cellValue, rowPos, colPos, dispatch }: CellComponentProp) => { | |
| 93 const onClickHandler = useCallback(()=>{ | |
| 94 dispatch({type: "place", newRowPos: rowPos, newColPos: colPos}); | |
| 95 }, [rowPos, colPos, dispatch]); | |
| 96 | |
| 97 return ( | |
| 98 <div onClick={onClickHandler} style={styles.cell(cellValue.value)}></div> | |
| 99 ) | |
| 100 }) | |
| 101 | |
| 102 type Action = | |
| 103 | { type: "place", newRowPos: number, newColPos: number } | |
| 104 | { type: "reset" } | |
| 105 | |
| 106 const gameStateReducer = (state: GameState, action: Action): GameState => { | |
| 107 if (state.status === GameStatus.WON) return state; | |
| 108 switch(action.type) { | |
| 109 case "place": | |
| 110 const allDirection: number[][] = []; | |
| 111 for (const [dr, dc] of DIRECTION) { | |
| 112 allDirection.push([action.newRowPos + dr, action.newColPos + dc]); | |
| 113 } | |
| 114 const newBoard: Board = state.board.map( | |
| 115 (row, rowIdx) => | |
| 116 row.map((col, colIdx) => | |
| 117 (allDirection.some( | |
| 118 ([nr, nc]) => rowIdx === nr && colIdx === nc | |
| 119 )) ? { ...col, value: !col.value } : col) | |
| 120 ) | |
| 121 const newStatus = | |
| 122 (newBoard.flat().filter((cell) => cell.value).length === MAX_COL * MAX_ROW) ? | |
| 123 GameStatus.WON : GameStatus.PLAYABLE; | |
| 124 | |
| 125 return { | |
| 126 ...state, | |
| 127 status: newStatus, | |
| 128 board: newBoard, | |
| 129 moves: state.moves + 1, | |
| 130 }; | |
| 131 case "reset": | |
| 132 return constructGameState(); | |
| 133 } | |
| 134 } | |
| 135 | |
| 136 const LightsOut = () => { | |
| 137 const [gameState, dispatch] = useReducer(gameStateReducer, null, constructGameState); | |
| 138 return ( | |
| 139 <> | |
| 140 <h1> Game Status {gameState.status}</h1> | |
| 141 <h2> Number of moves so far {gameState.moves}</h2> | |
| 142 <div style={styles.board}> | |
| 143 { | |
| 144 gameState.board.map( | |
| 145 (row, rowIdx) => | |
| 146 row.map( | |
| 147 (cellValue, colIdx) => | |
| 148 (<CellComponent | |
| 149 key={`${rowIdx}-${colIdx}`} | |
| 150 cellValue={cellValue} | |
| 151 rowPos={rowIdx} | |
| 152 colPos={colIdx} | |
| 153 dispatch={dispatch}/>) | |
| 154 ) | |
| 155 ) | |
| 156 } | |
| 157 </div> | |
| 158 </> | |
| 159 ); | |
| 160 } | |
| 161 | |
| 162 export { | |
| 163 LightsOut | |
| 164 } |