changeset 49:2b11e0449042

[React Game] 2048 game.
author MrJuneJune <me@mrjunejune.com>
date Sat, 13 Dec 2025 14:24:20 -0800
parents 46daba6e3cf4
children 983769fba767 3e0e27684e6b
files react_games/src/2048/main.tsx
diffstat 1 files changed, 267 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/react_games/src/2048/main.tsx	Sat Dec 13 14:24:20 2025 -0800
@@ -0,0 +1,267 @@
+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,
+}