view react_games/src/LightsOut/main.tsx @ 102:1065c226e52b

[MrJuneJune] Optimize the binary.
author June Park <parkjune1995@gmail.com>
date Sat, 03 Jan 2026 08:58:58 -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
}