view react_games/src/Robot/main.tsx @ 164:ce05514681c6

[MrJuneJune] Resume links aren't broken due to previous resume.pdf links.
author June Park <parkjune1995@gmail.com>
date Mon, 19 Jan 2026 05:40:22 -0800
parents fb9bcd3145cb
children
line wrap: on
line source

/**
 * Scenario
 *  Design a miniature browser game in which a single “robot” sprite navigates a 
 *  rectangular grid to reach a goal while avoiding obstacles. Everything—from bootstrapping the project
 *  to shipping a playable, restartable game—must be accomplished within one hour.
 *
 *
 * Grid 10 x 10
 * Robot top left corner
 * Obstical is going to be 10
 *
 * one of the grid is going to be the goal
 *
 * Movement logic keyboard or WASD okay
 */

import React, { CSSProperties, memo, useEffect, useReducer } from "react";

enum CellValue {
  PLAYER,
  GOAL,
  OBSTICAL,
  EMPTY
}

interface Cell {
  value: CellValue;
}

interface RowProp {
  rowValue: Cell[];
}

interface CellComponentProp {
  cell: Cell;
}

enum GameStatus {
  PLAYABLE,
  WON,
}

interface RobotState {
  x: number;
  y: number,
}

interface GameState {
  board: Cell[][];
  status: GameStatus;
  robotState: RobotState;
}

enum GameActionType {
  MOVE,
  RESET,
}

type GameAction = {type: GameActionType.MOVE, command: string } | { type: GameActionType.RESET }

const MAX_WIDTH: number = 10;
const MAX_COLUMN: number  = 10;
const BOARD: Cell[][] = Array.from(
  { length: MAX_WIDTH },
  () => Array(MAX_COLUMN).fill({value: CellValue.EMPTY})
);

BOARD[0][0] = {
  value: CellValue.PLAYER
}
BOARD[0][5] = {
  value: CellValue.OBSTICAL
}
BOARD[6][5] = {
  value: CellValue.GOAL
}


const getCellValue = (cell: Cell) => {
  switch(cell.value) {
    case CellValue.EMPTY:
      return (
        <></>
      )
    case CellValue.PLAYER:
      return (
        <>🤖</>
      )
    case CellValue.OBSTICAL:
      return (
        <>🧱</>
      )
    case CellValue.GOAL:
      return (
        <>🪙</>
      )
  }
}

const CellComponent = memo(({cell}: CellComponentProp) => {
  const cellStyle: CSSProperties = {
    padding: "10px",
    width: "50px",
    height: "50px",
    border: "1px solid #ccc",
  }
  return (
    <div style={cellStyle}>
      {getCellValue(cell)}
    </div>
  )
})

const Row = memo(({rowValue}: RowProp) => {
  const rowStyle: CSSProperties = {
    display: "flex",
  }
  return (
    <div style={rowStyle}>
      {rowValue.map((cell, idx) => {return <CellComponent key={idx} cell={cell} />})}
    </div>
  )
})

function boardReducer(state: GameState, action: GameAction): GameState {
  if (action.type === GameActionType.RESET) return INITIAL_GAME_STATE;

  const { x, y } = state.robotState;
  let newX = x;
  let newY = y;

  switch (action.command) {
    case "W": newX = Math.max(0, x - 1); break;
    case "A": newY = Math.max(0, y - 1); break;
    case "S": newX = Math.min(MAX_WIDTH - 1, x + 1); break;
    case "D": newY = Math.min(MAX_COLUMN - 1, y + 1); break;
    default:  return state;
  }

  const newBoard = state.board.map(row => row.slice());

  newBoard[x][y]       = { value: CellValue.EMPTY  };   // old spot
  newBoard[newX][newY] = { value: CellValue.PLAYER };   // new spot

  return {
    board: newBoard,
    status: GameStatus.PLAYABLE,
    robotState: { x: newX, y: newY },
  };
}

const INITIAL_GAME_STATE: GameState = {
  board: BOARD,
  status: GameStatus.PLAYABLE,
  robotState: { x: 0, y: 0 },
}

const Current = () => {
  const [state, dispatch] = useReducer(boardReducer, INITIAL_GAME_STATE);

  useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      dispatch({type: GameActionType.MOVE, command: event.key.toUpperCase()})
    };

    document.addEventListener("keydown", handleKeyDown);
  }, [])



  return (
    <>
      {state.board.map((row, idx) => { return <Row key={idx} rowValue={row} />})}
    </>
  );
}

export {
  Current
}