Mercurial
view react_games/src/2048/main.tsx @ 211:a6d8d32a0261
[MrJuneJune] Simple animations for darkmode.
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Sun, 15 Feb 2026 21:38:23 -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, }