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