Mercurial
comparison react_games/src/2048/main.tsx @ 49:2b11e0449042
[React Game] 2048 game.
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Sat, 13 Dec 2025 14:24:20 -0800 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| 48:46daba6e3cf4 | 49:2b11e0449042 |
|---|---|
| 1 import React, { CSSProperties, useEffect, useReducer, useState } from "react"; | |
| 2 | |
| 3 /** | |
| 4 * 2048 | |
| 5 * | |
| 6 * 4 X 4 | |
| 7 */ | |
| 8 | |
| 9 const MAX_WIDTH = 4; | |
| 10 | |
| 11 type Color = 'white' | 'orange' | 'yellow' | 'red'; | |
| 12 | |
| 13 type Cell = { | |
| 14 value: number; | |
| 15 color: Color; | |
| 16 } | |
| 17 | |
| 18 type Board = Cell[][]; | |
| 19 | |
| 20 type GameState = "in_progress" | "lost" | "won"; | |
| 21 | |
| 22 type Game = { | |
| 23 board: Board; | |
| 24 state: GameState; | |
| 25 steps: number; | |
| 26 } | |
| 27 | |
| 28 type Command = "u" | "d" | "l" | "r"; | |
| 29 | |
| 30 type GameAction = | |
| 31 { type: "move", command: Command } | { type: "calculate" }; | |
| 32 | |
| 33 | |
| 34 interface GameStyle { | |
| 35 container: CSSProperties; | |
| 36 board: CSSProperties; | |
| 37 cell: (color: Color) => CSSProperties; | |
| 38 } | |
| 39 | |
| 40 const gameStyle: GameStyle = { | |
| 41 container: { | |
| 42 display: "flex", | |
| 43 flexDirection: "column", | |
| 44 justifyContent: "center", | |
| 45 alignItems: "center", | |
| 46 height: "100vh" | |
| 47 }, | |
| 48 board: { | |
| 49 display: "grid", | |
| 50 gridTemplateColumns: "repeat(4, 50px)", | |
| 51 background: "#EEFFEE", | |
| 52 }, | |
| 53 cell: (color: Color) => ({ | |
| 54 display: "flex", | |
| 55 justifyContent: "center", | |
| 56 alignItems: "center", | |
| 57 aspectRatio: "1 / 1 ", | |
| 58 margin: "10px", | |
| 59 background: color, | |
| 60 }) | |
| 61 } | |
| 62 | |
| 63 function initializeBoard(): Board { | |
| 64 const board = Array.from({ length: MAX_WIDTH }, () => | |
| 65 Array.from({ length: MAX_WIDTH }, (): Cell => ({ value: 0, color: 'orange' })) | |
| 66 ); | |
| 67 let rowIndex: number; | |
| 68 let colIndex: number; | |
| 69 rowIndex = Math.floor(Math.random() * 4); | |
| 70 colIndex = Math.floor(Math.random() * 4); | |
| 71 board[rowIndex][colIndex].value = 2; | |
| 72 board[rowIndex-1][colIndex].value = 2; | |
| 73 return board; | |
| 74 } | |
| 75 | |
| 76 function initializeGame(): Game { | |
| 77 return { | |
| 78 board: initializeBoard(), | |
| 79 state: "in_progress", | |
| 80 steps: 0, | |
| 81 } | |
| 82 } | |
| 83 | |
| 84 | |
| 85 function handleMove(board: Board, command: Command): Board { | |
| 86 // Deep copy the board and initialize the merged status for the new board | |
| 87 const copiedBoard = board.map(row => | |
| 88 row.map(cell => ({ ...cell, merged: false })) | |
| 89 ); | |
| 90 | |
| 91 let diff: { row: number, col: number }; | |
| 92 let startRow: number, endRow: number, stepRow: number; | |
| 93 let startCol: number, endCol: number, stepCol: number; | |
| 94 | |
| 95 const size = copiedBoard.length; | |
| 96 | |
| 97 switch (command) { | |
| 98 case "u": | |
| 99 diff = { row: -1, col: 0 }; | |
| 100 startRow = 0; endRow = size; stepRow = 1; | |
| 101 startCol = 0; endCol = size; stepCol = 1; | |
| 102 break; | |
| 103 case "d": | |
| 104 diff = { row: 1, col: 0 }; | |
| 105 startRow = size - 1; endRow = -1; stepRow = -1; | |
| 106 startCol = 0; endCol = size; stepCol = 1; | |
| 107 break; | |
| 108 case "l": | |
| 109 diff = { row: 0, col: -1 }; | |
| 110 startRow = 0; endRow = size; stepRow = 1; | |
| 111 startCol = 0; endCol = size; stepCol = 1; | |
| 112 break; | |
| 113 case "r": | |
| 114 diff = { row: 0, col: 1 }; | |
| 115 startRow = 0; endRow = size; stepRow = 1; | |
| 116 startCol = size - 1; endCol = -1; stepCol = -1; | |
| 117 break; | |
| 118 } | |
| 119 | |
| 120 for (let rowIndex = startRow; rowIndex !== endRow; rowIndex += stepRow) { | |
| 121 for (let colIndex = startCol; colIndex !== endCol; colIndex += stepCol) { | |
| 122 const currentCell = copiedBoard[rowIndex][colIndex]; | |
| 123 | |
| 124 if (currentCell.value === 0) continue; | |
| 125 | |
| 126 let r = rowIndex; | |
| 127 let c = colIndex; | |
| 128 let emptySlot: { r: number, c: number } = { r: rowIndex, c: colIndex }; | |
| 129 let finalSlot: { r: number, c: number } = { r: rowIndex, c: colIndex }; | |
| 130 | |
| 131 while (true) { | |
| 132 r += diff.row; | |
| 133 c += diff.col; | |
| 134 | |
| 135 if (r < 0 || r >= size || c < 0 || c >= size) { | |
| 136 finalSlot = emptySlot; | |
| 137 break; | |
| 138 } | |
| 139 | |
| 140 const nextCell = copiedBoard[r][c]; | |
| 141 | |
| 142 if (nextCell.value === 0) { | |
| 143 emptySlot = { r, c }; | |
| 144 finalSlot = emptySlot; | |
| 145 } else if (nextCell.value === currentCell.value && !nextCell.merged) { | |
| 146 finalSlot = { r, c }; | |
| 147 break; | |
| 148 } else { | |
| 149 finalSlot = emptySlot; | |
| 150 break; | |
| 151 } | |
| 152 } | |
| 153 | |
| 154 const targetCell = copiedBoard[finalSlot.r][finalSlot.c]; | |
| 155 | |
| 156 if (finalSlot.r === rowIndex && finalSlot.c === colIndex) { | |
| 157 continue; | |
| 158 } | |
| 159 | |
| 160 if (targetCell.value === currentCell.value && !targetCell.merged) { | |
| 161 targetCell.value *= 2; | |
| 162 targetCell.merged = true; | |
| 163 | |
| 164 copiedBoard[rowIndex][colIndex].value = 0; | |
| 165 | |
| 166 } else if (targetCell.value === 0) { | |
| 167 targetCell.value = currentCell.value; | |
| 168 copiedBoard[rowIndex][colIndex].value = 0; | |
| 169 } | |
| 170 } | |
| 171 } | |
| 172 | |
| 173 return copiedBoard; | |
| 174 } | |
| 175 | |
| 176 function addNewItemsToTheBoard(board: Board) { | |
| 177 let randomRowIndex: number; | |
| 178 let randomColIndex: number; | |
| 179 | |
| 180 | |
| 181 let zeroPos = 0; | |
| 182 board.forEach((row) => { | |
| 183 row.forEach((cell) => { | |
| 184 if (cell.value === 0) { | |
| 185 zeroPos += 1; | |
| 186 } | |
| 187 }) | |
| 188 }) | |
| 189 if (zeroPos === 0) { | |
| 190 return; | |
| 191 } | |
| 192 | |
| 193 let curr = 0; | |
| 194 const maxAddedValues = zeroPos < 2 ? 1 : (zeroPos / 2) | 0; | |
| 195 while (curr < maxAddedValues) { | |
| 196 randomRowIndex = Math.floor(Math.random() * board.length) | |
| 197 randomColIndex = Math.floor(Math.random() * board.length) | |
| 198 if (board[randomRowIndex][randomColIndex].value === 0) | |
| 199 { | |
| 200 board[randomRowIndex][randomColIndex].value = 2; | |
| 201 curr++; | |
| 202 } | |
| 203 } | |
| 204 } | |
| 205 | |
| 206 function gameDispatch(game: Game, gameAction: GameAction): Game { | |
| 207 switch(gameAction.type) { | |
| 208 case "move": { | |
| 209 const newBoard = handleMove(game.board, gameAction.command); | |
| 210 addNewItemsToTheBoard(newBoard); | |
| 211 return { | |
| 212 ...game, | |
| 213 board: newBoard, | |
| 214 } | |
| 215 } | |
| 216 case "calculate": { | |
| 217 return { | |
| 218 ...game, | |
| 219 } | |
| 220 } | |
| 221 } | |
| 222 } | |
| 223 | |
| 224 function Game() { | |
| 225 const [game, dispatch] = useReducer(gameDispatch, null, initializeGame); | |
| 226 | |
| 227 useEffect(() => { | |
| 228 window.addEventListener("keyup", (e) => { | |
| 229 switch(e.key) { | |
| 230 case "ArrowDown": { | |
| 231 dispatch({ type: "move", command: "d" }); | |
| 232 return; | |
| 233 } | |
| 234 case "ArrowUp": { | |
| 235 dispatch({ type: "move", command: "u" }); | |
| 236 return; | |
| 237 } | |
| 238 case "ArrowRight": { | |
| 239 dispatch({ type: "move", command: "r" }); | |
| 240 return; | |
| 241 } | |
| 242 case "ArrowLeft": { | |
| 243 dispatch({ type: "move", command: "l" }); | |
| 244 return; | |
| 245 } | |
| 246 default: | |
| 247 return; | |
| 248 } | |
| 249 }) | |
| 250 | |
| 251 }, []) | |
| 252 return ( | |
| 253 <div style={gameStyle.container}> | |
| 254 <h1> 2048 </h1> | |
| 255 <div style={gameStyle.board}> | |
| 256 {game.board.map((row: Cell[]) => { | |
| 257 return row.map((cell: Cell) => (<div style={gameStyle.cell(cell.color)}> {cell.value} </div>)) | |
| 258 })} | |
| 259 </div> | |
| 260 </div> | |
| 261 ); | |
| 262 } | |
| 263 | |
| 264 | |
| 265 export { | |
| 266 Game, | |
| 267 } |