Skip to content

Instantly share code, notes, and snippets.

@senko
Created March 26, 2025 16:47
Show Gist options
  • Save senko/7d692fcb9e6553744212964a440f1cb2 to your computer and use it in GitHub Desktop.
Save senko/7d692fcb9e6553744212964a440f1cb2 to your computer and use it in GitHub Desktop.
Minesweeper - single shot, Google Gemini 2.5 Pro
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Simple Minesweeper</title>
<style>
body {
font-family: sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: #f0f0f0;
}
#game-container {
text-align: center;
background-color: #c0c0c0;
padding: 20px;
border: 3px outset #eee;
}
#status-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding: 10px;
border: 2px inset #aaa;
background-color: #bbb;
}
#mine-count, #timer, #game-status { /* Added #timer, though not implemented fully */
font-size: 1.2em;
font-weight: bold;
background-color: #333;
color: #ff0000;
padding: 5px 10px;
min-width: 50px;
text-align: center;
}
#game-status {
color: #0f0; /* Green for status initially */
}
#reset-button {
font-size: 1.5em;
padding: 5px;
cursor: pointer;
border: 2px outset #ddd;
background-color: #d0d0d0;
}
#reset-button:active {
border-style: inset;
}
#board {
display: grid;
border: 3px inset #aaa;
/* grid-template-columns will be set by JS */
}
.cell {
width: 30px;
height: 30px;
border: 1px solid #999;
background-color: #bdbdbd;
display: flex;
justify-content: center;
align-items: center;
font-size: 1em;
font-weight: bold;
cursor: pointer;
box-shadow: inset 2px 2px 2px rgba(255,255,255,0.5),
inset -2px -2px 2px rgba(0,0,0,0.3);
user-select: none; /* Prevent text selection */
}
.cell.revealed {
background-color: #aaa;
border: 1px solid #888;
box-shadow: none;
cursor: default;
}
.cell.flagged {
background-color: #bdbdbd; /* Keep background for flag */
box-shadow: inset 2px 2px 2px rgba(255,255,255,0.5),
inset -2px -2px 2px rgba(0,0,0,0.3); /* Keep bevel */
font-size: 1.2em;
}
.cell.flagged::before {
content: '🚩'; /* Flag emoji */
}
.cell.mine {
background-color: red;
}
.cell.mine::before {
content: 'πŸ’£'; /* Bomb emoji */
font-size: 1.2em;
}
/* Number Colors */
.cell[data-count="1"] { color: blue; }
.cell[data-count="2"] { color: green; }
.cell[data-count="3"] { color: red; }
.cell[data-count="4"] { color: navy; }
.cell[data-count="5"] { color: brown; }
.cell[data-count="6"] { color: teal; }
.cell[data-count="7"] { color: black; }
.cell[data-count="8"] { color: grey; }
</style>
</head>
<body>
<div id="game-container">
<h1>Minesweeper</h1>
<div id="status-bar">
<span id="mine-count">0</span>
<button id="reset-button">😊</button>
<span id="game-status">Ready</span>
<!-- <span id="timer">0</span> --> <!-- Timer element (functionality not added) -->
</div>
<div id="board">
<!-- Cells will be generated by JavaScript -->
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// --- Game Settings ---
const BOARD_WIDTH = 10;
const BOARD_HEIGHT = 10;
const NUM_MINES = 15;
// --- Game State Variables ---
let board = []; // 2D array representing the board state
let mineLocations = []; // Array of {row, col} for mines
let flagsPlaced = 0;
let revealedCells = 0;
let gameOver = false;
let firstClick = true;
// --- DOM Elements ---
const boardElement = document.getElementById('board');
const mineCountElement = document.getElementById('mine-count');
const statusElement = document.getElementById('game-status');
const resetButton = document.getElementById('reset-button');
// --- Cell Object Structure ---
// Each element in the `board` array will be an object like:
// {
// isMine: false,
// isRevealed: false,
// isFlagged: false,
// neighborMineCount: 0,
// element: DOM_Element // Reference to the cell's div
// }
// --- Game Initialization ---
function initGame() {
// Reset state
board = [];
mineLocations = [];
flagsPlaced = 0;
revealedCells = 0;
gameOver = false;
firstClick = true;
statusElement.textContent = 'Ready';
statusElement.style.color = '#0f0'; // Green
resetButton.textContent = '😊';
mineCountElement.textContent = String(NUM_MINES).padStart(3, '0');
// Clear previous board
boardElement.innerHTML = '';
boardElement.style.gridTemplateColumns = `repeat(${BOARD_WIDTH}, 30px)`;
// Create board data structure and DOM elements
for (let r = 0; r < BOARD_HEIGHT; r++) {
const row = [];
for (let c = 0; c < BOARD_WIDTH; c++) {
const cellElement = document.createElement('div');
cellElement.classList.add('cell');
cellElement.dataset.row = r;
cellElement.dataset.col = c;
// Add event listeners
cellElement.addEventListener('click', handleCellClick);
cellElement.addEventListener('contextmenu', handleCellContextMenu);
boardElement.appendChild(cellElement);
row.push({
isMine: false,
isRevealed: false,
isFlagged: false,
neighborMineCount: 0,
element: cellElement
});
}
board.push(row);
}
console.log("Board initialized.");
}
// --- Place Mines (after first click) ---
function placeMines(firstClickRow, firstClickCol) {
console.log(`Placing mines, avoiding ${firstClickRow}, ${firstClickCol}`);
let minesToPlace = NUM_MINES;
while (minesToPlace > 0) {
const r = Math.floor(Math.random() * BOARD_HEIGHT);
const c = Math.floor(Math.random() * BOARD_WIDTH);
// Don't place on the first clicked cell or if already a mine
if ((r === firstClickRow && c === firstClickCol) || board[r][c].isMine) {
continue;
}
board[r][c].isMine = true;
mineLocations.push({ row: r, col: c });
minesToPlace--;
}
// Calculate neighbor counts
calculateNeighborCounts();
console.log("Mines placed and counts calculated.");
}
// --- Calculate Neighbor Mine Counts ---
function calculateNeighborCounts() {
for (let r = 0; r < BOARD_HEIGHT; r++) {
for (let c = 0; c < BOARD_WIDTH; c++) {
if (!board[r][c].isMine) {
const neighbors = getNeighbors(r, c);
let count = 0;
neighbors.forEach(([nr, nc]) => {
if (board[nr][nc].isMine) {
count++;
}
});
board[r][c].neighborMineCount = count;
}
}
}
}
// --- Get Valid Neighbors ---
function getNeighbors(row, col) {
const neighbors = [];
for (let rOffset = -1; rOffset <= 1; rOffset++) {
for (let cOffset = -1; cOffset <= 1; cOffset++) {
if (rOffset === 0 && cOffset === 0) continue; // Skip self
const nr = row + rOffset;
const nc = col + cOffset;
// Check bounds
if (nr >= 0 && nr < BOARD_HEIGHT && nc >= 0 && nc < BOARD_WIDTH) {
neighbors.push([nr, nc]);
}
}
}
return neighbors;
}
// --- Handle Left Click ---
function handleCellClick(event) {
if (gameOver) return;
const row = parseInt(event.target.dataset.row);
const col = parseInt(event.target.dataset.col);
const cell = board[row][col];
// Ignore clicks on revealed or flagged cells
if (cell.isRevealed || cell.isFlagged) return;
// First click logic
if (firstClick) {
placeMines(row, col);
firstClick = false;
console.log("First click processed.");
}
// Clicked on a mine?
if (cell.isMine) {
endGame(false); // Lose
return;
}
// Reveal the cell
revealCell(row, col);
// Check for win condition
checkWin();
}
// --- Handle Right Click (Flagging) ---
function handleCellContextMenu(event) {
event.preventDefault(); // Prevent browser context menu
if (gameOver) return;
const row = parseInt(event.target.dataset.row);
const col = parseInt(event.target.dataset.col);
const cell = board[row][col];
// Cannot flag revealed cells
if (cell.isRevealed) return;
// Toggle flag
cell.isFlagged = !cell.isFlagged;
if (cell.isFlagged) {
flagsPlaced++;
cell.element.classList.add('flagged');
// cell.element.textContent = '🚩'; // Or use CSS ::before
} else {
flagsPlaced--;
cell.element.classList.remove('flagged');
// cell.element.textContent = '';
}
// Update mine count display
mineCountElement.textContent = String(NUM_MINES - flagsPlaced).padStart(3, '0');
}
// --- Reveal Cell Logic (Recursive for empty cells) ---
function revealCell(row, col) {
// Boundary check (shouldn't be needed if called internally, but good practice)
if (row < 0 || row >= BOARD_HEIGHT || col < 0 || col >= BOARD_WIDTH) return;
const cell = board[row][col];
// Stop if already revealed or flagged
if (cell.isRevealed || cell.isFlagged) return;
cell.isRevealed = true;
cell.element.classList.add('revealed');
revealedCells++;
// If it was flagged, unflag it visually and update count
if (cell.isFlagged) {
cell.isFlagged = false; // Update data model
flagsPlaced--;
cell.element.classList.remove('flagged');
mineCountElement.textContent = String(NUM_MINES - flagsPlaced).padStart(3, '0');
}
if (cell.neighborMineCount > 0) {
// Display number
cell.element.textContent = cell.neighborMineCount;
cell.element.dataset.count = cell.neighborMineCount; // For CSS styling
} else {
// Empty cell - reveal neighbors recursively (Flood Fill)
cell.element.textContent = ''; // Ensure it's empty visually
const neighbors = getNeighbors(row, col);
neighbors.forEach(([nr, nc]) => {
revealCell(nr, nc); // Recursive call
});
}
}
// --- Check Win Condition ---
function checkWin() {
const totalNonMineCells = BOARD_WIDTH * BOARD_HEIGHT - NUM_MINES;
if (revealedCells === totalNonMineCells) {
endGame(true); // Win
}
}
// --- End Game ---
function endGame(isWin) {
gameOver = true;
console.log(`Game Over. Win: ${isWin}`);
if (isWin) {
statusElement.textContent = 'You Win!';
statusElement.style.color = '#00ff00'; // Bright Green
resetButton.textContent = '😎';
// Optionally flag all remaining mines automatically
mineLocations.forEach(({ row, col }) => {
const cell = board[row][col];
if (!cell.isFlagged && !cell.isRevealed) {
cell.isFlagged = true;
cell.element.classList.add('flagged');
}
});
flagsPlaced = NUM_MINES; // Update count
mineCountElement.textContent = String(NUM_MINES - flagsPlaced).padStart(3, '0');
} else {
statusElement.textContent = 'You Lose!';
statusElement.style.color = '#ff0000'; // Bright Red
resetButton.textContent = '😡';
// Reveal all mines
mineLocations.forEach(({ row, col }) => {
const cell = board[row][col];
if (!cell.isRevealed) {
// Don't reveal flagged mines that were correct
// if (!cell.isFlagged) { // Optional: Only show unflagged mines
cell.element.classList.add('revealed', 'mine');
// cell.element.textContent = 'πŸ’£'; // Or use CSS ::before
// }
}
// Show incorrectly placed flags (optional)
if (cell.isFlagged && !cell.isMine) {
// Maybe add a specific style for wrong flags
cell.element.style.backgroundColor = '#ffaaaa'; // Light red background
}
});
}
// Disable further interaction (optional, could also be done by checking gameOver flag in handlers)
// boardElement.style.pointerEvents = 'none';
}
// --- Event Listeners ---
resetButton.addEventListener('click', initGame);
// --- Initial Game Setup ---
initGame();
}); // End DOMContentLoaded
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment