Mercurial
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, +}