|
49
|
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 }
|