const board = []; const canvas = document.getElementById("minesweeper"); canvas.width = window.innerWidth; canvas.height = window.innerHeight; const context = canvas.getContext("2d"); let scrollX = 0, scrollY = 0; const CELL_SIZE = 20; const STROKE_SIZE = 2; const MINE_CHANCE = 0.2; class Cell { constructor(isRevealed, isMine, isFlagged) { this.isRevealed = isRevealed; this.isMine = isMine; this.isFlagged = isFlagged; } } // Initialize board for (let x = 0; x < Math.floor(window.innerWidth / CELL_SIZE) + 1; x++) { board.push([]); for (let y = 0; y < Math.floor(window.innerHeight / CELL_SIZE) + 1; y++) { board[x].push(new Cell(false, Math.random() < MINE_CHANCE, false)); } } function draw() { context.clearRect(0, 0, canvas.width, canvas.height); context.translate(-scrollX, -scrollY); for (let x = 0; x < board.length; x++) { for (let y = 0; y < board[x].length; y++) { const cell = board[x][y]; context.fillStyle = cell.isRevealed ? "#AAA" : "#BDBDBD"; context.fillRect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE); context.fillStyle = cell.isRevealed ? "#424242" : "#EEE"; context.fillRect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE - STROKE_SIZE, STROKE_SIZE); context.fillRect(x * CELL_SIZE, y * CELL_SIZE, STROKE_SIZE, CELL_SIZE - STROKE_SIZE); context.fillStyle = cell.isRevealed ? "#EEE" : "#424242"; context.fillRect(x * CELL_SIZE + CELL_SIZE - STROKE_SIZE, y * CELL_SIZE + STROKE_SIZE, STROKE_SIZE, CELL_SIZE - STROKE_SIZE); context.fillRect(x * CELL_SIZE + STROKE_SIZE, y * CELL_SIZE + CELL_SIZE - STROKE_SIZE, CELL_SIZE - STROKE_SIZE, STROKE_SIZE); if (cell.isFlagged) { context.fillStyle = "red"; context.beginPath(); context.arc(x * CELL_SIZE + CELL_SIZE / 2, y * CELL_SIZE + CELL_SIZE / 2, CELL_SIZE / 3, 0, 2 * Math.PI); context.fill(); } if (cell.isRevealed) { if (cell.isMine) { context.fillStyle = "black"; context.beginPath(); context.arc(x * CELL_SIZE + CELL_SIZE / 2, y * CELL_SIZE + CELL_SIZE / 2, CELL_SIZE / 3, 0, 2 * Math.PI); context.fill(); } else if (getCellNumber(x, y) > 0) { context.font = "18px monospace"; context.textAlign = "center"; context.fillStyle = ["#0000ff", "#00ff00", "#ff0000", "#010280", "#7e0000", "#027f80", "#000000", "#808080"][getCellNumber(x, y) - 1]; context.fillText(getCellNumber(x, y), x * CELL_SIZE + CELL_SIZE / 2, y * CELL_SIZE + CELL_SIZE * 0.8); } } } } context.setTransform(1, 0, 0, 1, 0, 0); } function screenCoordsToBoardCoords(x, y) { return { x: Math.floor((x - scrollX) / CELL_SIZE), y: Math.floor((y - scrollY) / CELL_SIZE), }; } function createColumnAtRight() { const newArr = []; for (let i = 0; i < board[0].length; i++) { newArr.push(new Cell(false, Math.random() < MINE_CHANCE, false)); } board.push(newArr); } function createColumnAtLeft() { const newArr = []; for (let i = 0; i < board[0].length; i++) { newArr.push(new Cell(false, Math.random() < MINE_CHANCE, false)); } board.unshift(newArr); scrollX += CELL_SIZE; } function createRowAtBottom() { for (let i = 0; i < board.length; i++) { board[i].push(new Cell(false, Math.random() < MINE_CHANCE, false)); } } function createRowAtTop() { for (let i = 0; i < board.length; i++) { board[i].unshift(new Cell(false, Math.random() < MINE_CHANCE, false)); } scrollY += CELL_SIZE; } function getCellsAround(x, y) { if (x + 1 >= board.length) { createColumnAtRight(); } if (y + 1 >= board[0].length) { createRowAtBottom(); } if (x <= 0) { createColumnAtLeft(); x++; } if (y <= 0) { createRowAtTop(); y++; } return [ [x - 1, y - 1], [x, y - 1], [x + 1, y - 1], [x - 1, y], [x + 1, y], [x - 1, y + 1], [x, y + 1], [x + 1, y + 1], ]; } function getCellNumber(x, y) { return getCellsAround(x, y) .reduce((acc, coords) => acc += board[coords[0]][coords[1]].isMine ? 1 : 0, 0); } function revealCell(x, y) { if (board[x][y].isFlagged) { return; } board[x][y].isRevealed = true; if (getCellNumber(x, y) === 0) { getCellsAround(x, y, false).forEach(coords => { const cell = board[coords[0]][coords[1]]; if (!cell.isRevealed && getCellNumber(x, y) === 0) revealCell(coords[0], coords[1]); }); } } function cellMarkedAsMine(cell) { return cell.isFlagged || (cell.isRevealed && cell.isMine); } function revealSafeCell(x, y) { if (!board[x][y].isRevealed || board[x][y].isMine) { return; } const targetValue = getCellNumber(x, y); const neighbours = getCellsAround(x, y); const revealedBombs = neighbours .map(([x, y]) => board[x][y]) .map(cellMarkedAsMine) .reduce((acc, isMine) => acc + (isMine ? 1 : 0), 0); if (revealedBombs === targetValue) { neighbours.forEach(([x, y]) => revealCell(x, y)) } } function flagCell(x, y) { if (board[x][y].isRevealed) { return; } board[x][y].isFlagged = !board[x][y].isFlagged; } window.addEventListener("contextmenu", event => { event.preventDefault(); }) const MouseButton = Object.freeze({ Left: 0, Middle: 1, Right: 2, }) window.addEventListener("mousedown", event => { const { x, y } = screenCoordsToBoardCoords(event.pageX + scrollX * 2, event.pageY + scrollY * 2); switch (event.button) { case MouseButton.Left: revealCell(x, y); break; case MouseButton.Middle: event.preventDefault(); revealSafeCell(x, y); break; case MouseButton.Right: event.preventDefault(); flagCell(x, y); break; default: return true; } draw(); }); function columnContainsOnlyUnrevealed(column) { return column.every(cell => !cell.isRevealed); } window.addEventListener("wheel", event => { let deltaX = 0, deltaY = 0; if (event.shiftKey) { deltaX = event.deltaY; } else { deltaX = event.deltaX; deltaY = event.deltaY; } console.log(scrollY + deltaY + window.innerHeight, board.length * CELL_SIZE); if (scrollY + deltaY + window.innerHeight > board.length * CELL_SIZE) { if (board.slice(-20).every(columnContainsOnlyUnrevealed)) { console.log("ignoring"); return; } const diff = scrollY + window.innerHeight - board.length * CELL_SIZE; for (let i = 0; i < diff + CELL_SIZE; i += CELL_SIZE) createRowAtBottom(); } scrollX += deltaX; scrollY += deltaY; draw(); }); draw();