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 }