import { getValue } from "../../shared/game"; import type { ServerGame } from "../../shared/game"; interface CreateGameOptions { uuid: string; user: string; width: number; height: number; mines: number; theme: string; } const isValid = (game: ServerGame, x: number, y: number) => { const { width, height } = game; return x >= 0 && x < width && y >= 0 && y < height; }; const getNeighborFlagCount = (game: ServerGame, x: number, y: number) => { const { isFlagged } = game; const neighbors = [ isFlagged[x - 1]?.[y - 1], isFlagged[x]?.[y - 1], isFlagged[x + 1]?.[y - 1], isFlagged[x - 1]?.[y], isFlagged[x + 1]?.[y], isFlagged[x - 1]?.[y + 1], isFlagged[x]?.[y + 1], isFlagged[x + 1]?.[y + 1], ]; return neighbors.filter((n) => n).length; }; const hasWon = (serverGame: ServerGame) => { const { mines, isRevealed, isFlagged, finished, width, height } = serverGame; if (finished) return false; for (let i = 0; i < width; i++) { for (let j = 0; j < height; j++) { if (!isRevealed[i][j] && !isFlagged[i][j]) return false; if (mines[i][j] && !isFlagged[i][j]) return false; if (isFlagged[i][j] && !mines[i][j]) return false; } } return true; }; const expandBoard = (serverGame: ServerGame) => { const { width, height, stage, mines, isFlagged, isRevealed, isQuestionMark } = serverGame; let dir = stage % 2 === 0 ? "down" : "right"; if (stage > 13) { dir = "down"; } // Expand the board by the current board size 8x8 -> 16x8 if (dir === "down") { const newHeight = Math.floor(Math.min(height + 7, height * 1.5)); const newWidth = width; const newMinesCount = Math.floor( width * height * 0.5 * (0.2 + 0.0015 * stage), ); // expand mines array const newMines = Array.from({ length: newWidth }, () => new Array(newHeight).fill(false), ); const newIsRevealed = Array.from({ length: newWidth }, () => new Array(newHeight).fill(false), ); const newIsFlagged = Array.from({ length: newWidth }, () => new Array(newHeight).fill(false), ); const newIsQuestionMark = Array.from({ length: newWidth }, () => new Array(newHeight).fill(false), ); for (let i = 0; i < newWidth; i++) { for (let j = 0; j < newHeight; j++) { const x = i; const y = j; if (mines[x]?.[y]) { newMines[i][j] = true; } if (isRevealed[x]?.[y]) { newIsRevealed[i][j] = true; } if (isFlagged[x]?.[y]) { newIsFlagged[i][j] = true; } if (isQuestionMark[x]?.[y]) { newIsQuestionMark[i][j] = true; } } } // generate new mines let remainingMines = newMinesCount; while (remainingMines > 0) { const x = Math.floor(Math.random() * width); const y = height + Math.floor(Math.random() * (newHeight - height)); if (!newMines[x][y]) { newMines[x][y] = true; remainingMines--; } } Object.assign(serverGame, { width: newWidth, height: newHeight, mines: newMines, minesCount: newMinesCount, stage: stage + 1, isRevealed: newIsRevealed, isFlagged: newIsFlagged, isQuestionMark: newIsQuestionMark, }); } if (dir === "right") { const newWidth = Math.floor(Math.min(width + 7, width * 1.5)); const newHeight = height; const newMinesCount = Math.floor( width * height * 0.5 * (0.2 + 0.0015 * stage), ); // expand mines array const newMines = Array.from({ length: newWidth }, () => new Array(newHeight).fill(false), ); const newIsRevealed = Array.from({ length: newWidth }, () => new Array(newHeight).fill(false), ); const newIsFlagged = Array.from({ length: newWidth }, () => new Array(newHeight).fill(false), ); const newIsQuestionMark = Array.from({ length: newWidth }, () => new Array(newHeight).fill(false), ); for (let i = 0; i < newWidth; i++) { for (let j = 0; j < newHeight; j++) { const x = i; const y = j; if (mines[x]?.[y]) { newMines[i][j] = true; } if (isRevealed[x]?.[y]) { newIsRevealed[i][j] = true; } if (isFlagged[x]?.[y]) { newIsFlagged[i][j] = true; } if (isQuestionMark[x]?.[y]) { newIsQuestionMark[i][j] = true; } } } // generate new mines let remainingMines = newMinesCount; while (remainingMines > 0) { const x = width + Math.floor(Math.random() * (newWidth - width)); const y = Math.floor(Math.random() * height); if (!newMines[x][y]) { newMines[x][y] = true; remainingMines--; } } Object.assign(serverGame, { width: newWidth, height: newHeight, mines: newMines, minesCount: newMinesCount, stage: stage + 1, isRevealed: newIsRevealed, isFlagged: newIsFlagged, isQuestionMark: newIsQuestionMark, }); } const newMinesCount = serverGame.mines.flat().filter((m) => m).length; Object.assign(serverGame, { minesCount: newMinesCount }); }; export const game = { createGame: (options: CreateGameOptions): ServerGame => { const { uuid, user, width, height, mines } = options; if (mines > width * height) { throw new Error("Too many mines"); } const minesArray = Array.from({ length: width }, () => new Array(height).fill(false), ); const isRevealedArray = Array.from({ length: width }, () => new Array(height).fill(false), ); const isFlaggedArray = Array.from({ length: width }, () => new Array(height).fill(false), ); const isQuestionMarkArray = Array.from({ length: width }, () => new Array(height).fill(false), ); let remainingMines = mines; while (remainingMines > 0) { const x = Math.floor(Math.random() * width); const y = Math.floor(Math.random() * height); if (!minesArray[x][y]) { minesArray[x][y] = true; remainingMines--; } } return { uuid, user, finished: 0, started: Date.now(), width, height, mines: minesArray, isRevealed: isRevealedArray, isFlagged: isFlaggedArray, isQuestionMark: isQuestionMarkArray, stage: 1, lastClick: [-1, -1], minesCount: mines, theme: options.theme, }; }, reveal: (serverGame: ServerGame, x: number, y: number, initial = false) => { const aux = ( serverGame: ServerGame, x: number, y: number, initial: boolean = false, ) => { const { mines, isRevealed, isFlagged, isQuestionMark, finished } = serverGame; if (finished) return; if (!isValid(serverGame, x, y)) return; if (isQuestionMark[x][y]) return; if (isFlagged[x][y]) return; serverGame.lastClick = [x, y]; if (mines[x][y]) { serverGame.finished = Date.now(); return; } const value = getValue(serverGame.mines, x, y); const neighborFlagCount = getNeighborFlagCount(serverGame, x, y); if (isRevealed[x][y] && value === neighborFlagCount && initial) { if (!isFlagged[x - 1]?.[y]) aux(serverGame, x - 1, y); if (!isFlagged[x - 1]?.[y - 1]) aux(serverGame, x - 1, y - 1); if (!isFlagged[x - 1]?.[y + 1]) aux(serverGame, x - 1, y + 1); if (!isFlagged[x]?.[y - 1]) aux(serverGame, x, y - 1); if (!isFlagged[x]?.[y + 1]) aux(serverGame, x, y + 1); if (!isFlagged[x + 1]?.[y - 1]) aux(serverGame, x + 1, y - 1); if (!isFlagged[x + 1]?.[y]) aux(serverGame, x + 1, y); if (!isFlagged[x + 1]?.[y + 1]) aux(serverGame, x + 1, y + 1); } serverGame.isRevealed[x][y] = true; if (value === 0 && neighborFlagCount === 0) { const revealNeighbors = (nx: number, ny: number) => { if (isValid(serverGame, nx, ny) && !isRevealed[nx]?.[ny]) { aux(serverGame, nx, ny); } }; revealNeighbors(x - 1, y - 1); revealNeighbors(x, y - 1); revealNeighbors(x + 1, y - 1); revealNeighbors(x - 1, y); revealNeighbors(x + 1, y); revealNeighbors(x - 1, y + 1); revealNeighbors(x, y + 1); revealNeighbors(x + 1, y + 1); } }; aux(serverGame, x, y, initial); if (hasWon(serverGame)) { expandBoard(serverGame); } }, placeFlag: (serverGame: ServerGame, x: number, y: number) => { const { isRevealed, finished } = serverGame; if (finished) return; if (!isValid(serverGame, x, y)) return; if (isRevealed[x][y]) return; serverGame.isFlagged[x][y] = true; if (hasWon(serverGame)) { expandBoard(serverGame); } }, placeQuestionMark: (serverGame: ServerGame, x: number, y: number) => { const { isRevealed, finished } = serverGame; if (finished) return; if (!isValid(serverGame, x, y)) return; if (isRevealed[x][y]) return; serverGame.isFlagged[x][y] = false; serverGame.isQuestionMark[x][y] = true; }, clearTile: (serverGame: ServerGame, x: number, y: number) => { const { isRevealed, finished } = serverGame; if (finished) return; if (!isValid(serverGame, x, y)) return; if (isRevealed[x][y]) return; serverGame.isFlagged[x][y] = false; serverGame.isQuestionMark[x][y] = false; if (hasWon(serverGame)) { expandBoard(serverGame); } }, getRewards: (serverGame: ServerGame) => { const { finished, stage } = serverGame; if (finished == 0) return 0; if (stage < 2) return 0; return Math.floor(Math.pow(2, stage * 0.9 + stage * 1.2)); }, };