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 }