view react_games/src/2048/main.tsx @ 125:f236c895604e

[MrJuneJune] Added web socket for chat to this.
author June Park <parkjune1995@gmail.com>
date Thu, 08 Jan 2026 08:46:49 -0800
parents 2b11e0449042
children
line wrap: on
line source

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

/**
 * 2048
 * 
 * 4 X 4
 */

const MAX_WIDTH = 4;

type Color = 'white' | 'orange' | 'yellow' | 'red';

type Cell = {
  value: number;
  color: Color;
}

type Board = Cell[][];

type GameState = "in_progress" | "lost" | "won";

type Game = {
  board: Board;
  state: GameState;
  steps: number;
}

type Command = "u" | "d" | "l" | "r";

type GameAction = 
  { type: "move", command: Command } | { type: "calculate" };


interface GameStyle {
  container: CSSProperties;
  board: CSSProperties;
  cell: (color: Color) => CSSProperties;
}

const gameStyle: GameStyle = {
  container: {
    display: "flex",
    flexDirection: "column",
    justifyContent: "center",
    alignItems: "center",
    height: "100vh"
  },
  board: {
    display: "grid",
    gridTemplateColumns: "repeat(4, 50px)",
    background: "#EEFFEE",
  },
  cell: (color: Color) => ({
    display: "flex",
    justifyContent: "center",
    alignItems: "center",
    aspectRatio: "1 / 1 ",
    margin: "10px",
    background: color,
  })
}

function initializeBoard(): Board {
  const board = Array.from({ length: MAX_WIDTH }, () =>
    Array.from({ length: MAX_WIDTH }, (): Cell => ({ value: 0, color: 'orange' }))
  );
  let rowIndex: number;
  let colIndex: number;
  rowIndex = Math.floor(Math.random() * 4);
  colIndex = Math.floor(Math.random() * 4);
  board[rowIndex][colIndex].value = 2;
  board[rowIndex-1][colIndex].value = 2;
  return board;
}

function initializeGame(): Game {
  return {
    board: initializeBoard(),
    state: "in_progress",
    steps: 0,
  }
}


function handleMove(board: Board, command: Command): Board {
  // Deep copy the board and initialize the merged status for the new board
  const copiedBoard = board.map(row => 
    row.map(cell => ({ ...cell, merged: false }))
  );

  let diff: { row: number, col: number };
  let startRow: number, endRow: number, stepRow: number;
  let startCol: number, endCol: number, stepCol: number;

  const size = copiedBoard.length;

  switch (command) {
    case "u": 
      diff = { row: -1, col: 0 };
      startRow = 0; endRow = size; stepRow = 1;
      startCol = 0; endCol = size; stepCol = 1;
      break;
    case "d": 
      diff = { row: 1, col: 0 };
      startRow = size - 1; endRow = -1; stepRow = -1;
      startCol = 0; endCol = size; stepCol = 1;
      break;
    case "l":
      diff = { row: 0, col: -1 };
      startRow = 0; endRow = size; stepRow = 1;
      startCol = 0; endCol = size; stepCol = 1;
      break;
    case "r":
      diff = { row: 0, col: 1 };
      startRow = 0; endRow = size; stepRow = 1;
      startCol = size - 1; endCol = -1; stepCol = -1;
      break;
  }

  for (let rowIndex = startRow; rowIndex !== endRow; rowIndex += stepRow) {
    for (let colIndex = startCol; colIndex !== endCol; colIndex += stepCol) {
      const currentCell = copiedBoard[rowIndex][colIndex];
      
      if (currentCell.value === 0) continue;

      let r = rowIndex;
      let c = colIndex;
      let emptySlot: { r: number, c: number } = { r: rowIndex, c: colIndex };
      let finalSlot: { r: number, c: number } = { r: rowIndex, c: colIndex };

      while (true) {
        r += diff.row;
        c += diff.col;

        if (r < 0 || r >= size || c < 0 || c >= size) {
          finalSlot = emptySlot;
          break;
        }

        const nextCell = copiedBoard[r][c];
        
        if (nextCell.value === 0) {
          emptySlot = { r, c };
          finalSlot = emptySlot;
        } else if (nextCell.value === currentCell.value && !nextCell.merged) {
          finalSlot = { r, c };
          break;
        } else {
          finalSlot = emptySlot; 
          break;
        }
      }

      const targetCell = copiedBoard[finalSlot.r][finalSlot.c];

      if (finalSlot.r === rowIndex && finalSlot.c === colIndex) {
        continue;
      }
      
      if (targetCell.value === currentCell.value && !targetCell.merged) {
        targetCell.value *= 2;
        targetCell.merged = true;
        
        copiedBoard[rowIndex][colIndex].value = 0;
        
      } else if (targetCell.value === 0) {
        targetCell.value = currentCell.value;
        copiedBoard[rowIndex][colIndex].value = 0;
      }
    }
  }

  return copiedBoard;
}

function addNewItemsToTheBoard(board: Board) { 
  let randomRowIndex: number;
  let randomColIndex: number;


  let zeroPos = 0;
  board.forEach((row) => {
    row.forEach((cell) => {
      if (cell.value === 0) {
        zeroPos += 1;
      }
    })
  })
  if (zeroPos === 0) {
    return;
  }

  let curr = 0;
  const maxAddedValues = zeroPos < 2 ? 1 : (zeroPos / 2) | 0;
  while (curr < maxAddedValues) {
    randomRowIndex = Math.floor(Math.random() * board.length)
    randomColIndex = Math.floor(Math.random() * board.length)
    if (board[randomRowIndex][randomColIndex].value === 0)
    {
      board[randomRowIndex][randomColIndex].value = 2;
      curr++;
    }
  }
}

function gameDispatch(game: Game, gameAction: GameAction): Game {
  switch(gameAction.type) {
    case "move": {
      const newBoard = handleMove(game.board, gameAction.command);
      addNewItemsToTheBoard(newBoard);
      return  {
        ...game,
        board: newBoard,
      }
    }
    case "calculate": {
      return  {
        ...game,
      }
    }
  }
}

function Game() {
  const [game, dispatch] = useReducer(gameDispatch, null, initializeGame);

  useEffect(() => {
    window.addEventListener("keyup", (e) => {
      switch(e.key) {
        case "ArrowDown": {
          dispatch({ type: "move", command: "d" });
          return;
        }
        case "ArrowUp": {
          dispatch({ type: "move", command: "u" });
          return;
        }
        case "ArrowRight": {
          dispatch({ type: "move", command: "r" });
          return;
        }
        case "ArrowLeft": {
          dispatch({ type: "move", command: "l" });
          return;
        }
        default:
          return;
      }
    })

  }, [])
  return (
    <div style={gameStyle.container}>
      <h1> 2048 </h1>
      <div style={gameStyle.board}>
        {game.board.map((row: Cell[]) => {
          return row.map((cell: Cell) => (<div style={gameStyle.cell(cell.color)}> {cell.value} </div>))
        })}
      </div>
    </div>
  );
}


export {
  Game,
}