in src/gemini_95/index.ts [1176:1563]
function initMinesweeperGame(windowElement: HTMLDivElement): void {
console.log("Initializing Minesweeper...");
const boardElement = windowElement.querySelector('#minesweeper-board') as HTMLDivElement;
const flagCountElement = windowElement.querySelector('.minesweeper-flag-count') as HTMLDivElement;
const timerElement = windowElement.querySelector('.minesweeper-timer') as HTMLDivElement;
const resetButton = windowElement.querySelector('.minesweeper-reset-button') as HTMLButtonElement;
// >>> Get Hint elements <<< //
const hintButton = windowElement.querySelector('.minesweeper-hint-button') as HTMLButtonElement;
const commentaryElement = windowElement.querySelector('.minesweeper-commentary') as HTMLDivElement;
if (!boardElement || !flagCountElement || !timerElement || !resetButton || !hintButton || !commentaryElement) { // Add hint elements to check
console.error("Minesweeper UI elements not found!");
return;
}
let grid: MinesweeperCell[][] = [];
function resetGame() {
console.log("Resetting Minesweeper game.");
// Reset state variables
if (minesweeperTimerInterval) {
clearInterval(minesweeperTimerInterval);
minesweeperTimerInterval = null;
}
minesweeperTimeElapsed = 0;
minesweeperFlagsPlaced = 0;
minesweeperGameOver = false;
minesweeperFirstClick = true;
minesweeperMineCount = 10;
minesweeperGridSize = { rows: 9, cols: 9 };
// Update UI
timerElement.textContent = `⏱️ 0`;
flagCountElement.textContent = `🚩 ${minesweeperMineCount}`; // Show total mines initially
resetButton.textContent = '🙂';
// Create the grid
createGrid();
}
function createGrid() {
boardElement.innerHTML = ''; // Clear previous grid
grid = []; // Reset internal grid state
boardElement.style.gridTemplateColumns = `repeat(${minesweeperGridSize.cols}, 20px)`;
boardElement.style.gridTemplateRows = `repeat(${minesweeperGridSize.rows}, 20px)`;
for (let r = 0; r < minesweeperGridSize.rows; r++) {
const row: MinesweeperCell[] = [];
for (let c = 0; c < minesweeperGridSize.cols; c++) {
const cellElement = document.createElement('div');
cellElement.classList.add('minesweeper-cell');
const cellData: MinesweeperCell = {
isMine: false,
isRevealed: false,
isFlagged: false,
adjacentMines: 0,
element: cellElement,
row: r,
col: c,
};
cellElement.addEventListener('click', () => handleCellClick(cellData));
cellElement.addEventListener('contextmenu', (e) => {
e.preventDefault();
handleCellRightClick(cellData);
});
row.push(cellData);
boardElement.appendChild(cellElement);
}
grid.push(row);
}
console.log(`Grid created (${minesweeperGridSize.rows}x${minesweeperGridSize.cols})`);
// Mines will be placed on the first click
}
function placeMines(firstClickRow: number, firstClickCol: number) {
console.log(`Placing ${minesweeperMineCount} mines, avoiding ${firstClickRow},${firstClickCol}`);
let minesPlaced = 0;
while (minesPlaced < minesweeperMineCount) {
const r = Math.floor(Math.random() * minesweeperGridSize.rows);
const c = Math.floor(Math.random() * minesweeperGridSize.cols);
// Don't place a mine on the first clicked cell or if it already has a mine
if ((r === firstClickRow && c === firstClickCol) || grid[r][c].isMine) {
continue;
}
grid[r][c].isMine = true;
minesPlaced++;
}
// Calculate adjacent mines for all cells
for (let r = 0; r < minesweeperGridSize.rows; r++) {
for (let c = 0; c < minesweeperGridSize.cols; c++) {
if (!grid[r][c].isMine) {
grid[r][c].adjacentMines = countAdjacentMines(r, c);
}
}
}
console.log("Mines placed and adjacent counts calculated.");
}
function countAdjacentMines(row: number, col: number): number {
let count = 0;
for (let dr = -1; dr <= 1; dr++) {
for (let dc = -1; dc <= 1; dc++) {
if (dr === 0 && dc === 0) continue; // Skip self
const nr = row + dr;
const nc = col + dc;
if (
nr >= 0 && nr < minesweeperGridSize.rows &&
nc >= 0 && nc < minesweeperGridSize.cols &&
grid[nr][nc].isMine
) {
count++;
}
}
}
return count;
}
function handleCellClick(cell: MinesweeperCell) {
if (minesweeperGameOver || cell.isRevealed || cell.isFlagged) {
return;
}
// Start timer on first click
if (minesweeperFirstClick && !minesweeperTimerInterval) {
// Place mines *after* knowing the first click location
placeMines(cell.row, cell.col);
minesweeperFirstClick = false;
startTimer();
}
if (cell.isMine) {
gameOver(cell); // Pass the clicked mine
} else {
revealCell(cell);
checkWinCondition(); // Check win after revealing
}
}
function handleCellRightClick(cell: MinesweeperCell) {
if (minesweeperGameOver || cell.isRevealed) {
return;
}
if (!minesweeperFirstClick && !minesweeperTimerInterval) {
// Prevent flagging before the game starts (timer starts)
return;
}
cell.isFlagged = !cell.isFlagged;
cell.element.textContent = cell.isFlagged ? '🚩' : '';
// Update flag count display
if (cell.isFlagged) {
minesweeperFlagsPlaced++;
} else {
minesweeperFlagsPlaced--;
}
updateFlagCount();
checkWinCondition(); // Check if flagging the last mine wins
}
function revealCell(cell: MinesweeperCell) {
if (cell.isRevealed || cell.isFlagged || cell.isMine) {
return; // Should not happen if called correctly, but safe check
}
cell.isRevealed = true;
cell.element.classList.add('revealed');
cell.element.textContent = ''; // Clear flag if it was mistakenly revealed
if (cell.adjacentMines > 0) {
cell.element.textContent = cell.adjacentMines.toString();
cell.element.dataset.number = cell.adjacentMines.toString(); // For CSS coloring
} else {
// Flood fill for empty cells
for (let dr = -1; dr <= 1; dr++) {
for (let dc = -1; dc <= 1; dc++) {
if (dr === 0 && dc === 0) continue;
const nr = cell.row + dr;
const nc = cell.col + dc;
if (
nr >= 0 && nr < minesweeperGridSize.rows &&
nc >= 0 && nc < minesweeperGridSize.cols
) {
const neighbor = grid[nr][nc];
if (!neighbor.isRevealed && !neighbor.isFlagged) {
// Delay slightly to show cascade effect (optional)
// setTimeout(() => revealCell(neighbor), 10);
revealCell(neighbor); // Recursive call
}
}
}
}
}
}
function startTimer() {
if (minesweeperTimerInterval) return; // Already running
minesweeperTimeElapsed = 0;
timerElement.textContent = `⏱️ ${minesweeperTimeElapsed}`;
minesweeperTimerInterval = window.setInterval(() => {
minesweeperTimeElapsed++;
timerElement.textContent = `⏱️ ${minesweeperTimeElapsed}`;
}, 1000);
console.log("Minesweeper timer started.");
}
function updateFlagCount() {
const remainingFlags = minesweeperMineCount - minesweeperFlagsPlaced;
flagCountElement.textContent = `🚩 ${remainingFlags}`;
}
function gameOver(clickedMine: MinesweeperCell) {
console.log("Game Over!");
minesweeperGameOver = true;
if (minesweeperTimerInterval) {
clearInterval(minesweeperTimerInterval);
minesweeperTimerInterval = null;
}
resetButton.textContent = '😵';
// Reveal all mines
grid.forEach(row => {
row.forEach(cell => {
if (cell.isMine) {
cell.element.classList.add('mine');
cell.element.textContent = '💣';
if (cell !== clickedMine) { // Don't override the exploded one
cell.element.classList.add('revealed'); // Show the bomb
}
}
// Optionally show incorrectly placed flags
if (!cell.isMine && cell.isFlagged) {
cell.element.textContent = '❌';
}
});
});
// Highlight the mine that was clicked
clickedMine.element.classList.add('exploded');
clickedMine.element.textContent = '💥';
}
function checkWinCondition() {
if (minesweeperGameOver) return;
let revealedCount = 0;
let correctlyFlaggedMines = 0;
for (let r = 0; r < minesweeperGridSize.rows; r++) {
for (let c = 0; c < minesweeperGridSize.cols; c++) {
const cell = grid[r][c];
if (cell.isRevealed && !cell.isMine) {
revealedCount++;
}
if (cell.isFlagged && cell.isMine) {
correctlyFlaggedMines++;
}
}
}
const totalNonMineCells = (minesweeperGridSize.rows * minesweeperGridSize.cols) - minesweeperMineCount;
const allNonMinesRevealed = revealedCount === totalNonMineCells;
const allMinesFlagged = correctlyFlaggedMines === minesweeperMineCount && minesweeperFlagsPlaced === minesweeperMineCount;
if (allNonMinesRevealed || allMinesFlagged) {
console.log("Game Won!");
minesweeperGameOver = true;
if (minesweeperTimerInterval) {
clearInterval(minesweeperTimerInterval);
minesweeperTimerInterval = null;
}
resetButton.textContent = '😎';
// Optionally auto-flag remaining mines if won by revealing
if (allNonMinesRevealed) {
grid.forEach(row => row.forEach(cell => {
if (cell.isMine && !cell.isFlagged) {
cell.isFlagged = true;
cell.element.textContent = '🚩';
minesweeperFlagsPlaced++;
}
}));
updateFlagCount();
}
}
}
// >>> Function to get board state as text <<< //
function getBoardStateAsText(): string {
let boardString = "Current Minesweeper Board:\n";
boardString += `Flags Remaining: ${minesweeperMineCount - minesweeperFlagsPlaced}\n`;
boardString += `Time Elapsed: ${minesweeperTimeElapsed}s\n`;
boardString += "Grid (H=Hidden, F=Flagged, B=Bomb, Number=Adjacent Mines):\n";
for (let r = 0; r < minesweeperGridSize.rows; r++) {
let rowStr = "";
for (let c = 0; c < minesweeperGridSize.cols; c++) {
const cell = grid[r][c];
if (cell.isFlagged) {
rowStr += " F ";
} else if (!cell.isRevealed) {
rowStr += " H ";
} else if (cell.isMine) { // Should only show if game over, but include for context
rowStr += " B ";
} else if (cell.adjacentMines > 0) {
rowStr += ` ${cell.adjacentMines} `;
} else { // Revealed empty cell
rowStr += " _ ";
}
}
boardString += rowStr + "\n";
}
return boardString;
}
// >>> Function to get AI Hint <<< //
async function getAiHint() {
if (minesweeperGameOver || minesweeperFirstClick) {
commentaryElement.textContent = "Click a square first!";
return;
}
hintButton.disabled = true;
hintButton.textContent = '🤔';
commentaryElement.textContent = 'Thinking...';
// --- Initialize Gemini using the required pattern --- //
if (!geminiInstance) {
if (!await initializeGeminiIfNeeded('getAiHint')) {
commentaryElement.textContent = 'AI Init Error: Could not connect to Gemini';
hintButton.disabled = false;
hintButton.textContent = '💡 Hint';
return;
}
}
// --- End Gemini Initialization ---
try {
const boardState = getBoardStateAsText();
const prompt = `
You are a witty, slightly sarcastic Minesweeper expert playing along.
Based on the following Minesweeper board state, provide a short (1-2 sentence) hint or observation about a potentially safe move or a dangerous area. Don't give away exact mine locations unless it's logically certain from the revealed numbers. Format the hint as playful commentary.
${boardState}
Hint:`;
// --- Call Gemini API using existing pattern --- //
const result = await geminiInstance.models.generateContent({
model: "gemini-1.5-flash", // Use a fast model
contents: [{ role: "user", parts: [{ text: prompt }] }],
config: {
temperature: 0.7, // Allow for some creativity
}
});
const hintText = result?.candidates?.[0]?.content?.parts?.[0]?.text?.trim() || "My circuits are buzzing... maybe try clicking somewhere?";
commentaryElement.textContent = hintText;
console.log("Minesweeper Hint:", hintText);
} catch (error) {
console.error("Error getting Minesweeper hint:", error);
let errorMessage = 'Unknown error';
if (error instanceof Error) errorMessage = error.message;
else try {errorMessage = JSON.stringify(error); } catch {} // Simple stringify
commentaryElement.textContent = `Hint Error: ${errorMessage}`;
} finally {
hintButton.disabled = false;
hintButton.textContent = '💡 Hint';
}
}
// --- Event Listeners --- //
resetButton.addEventListener('click', resetGame);
hintButton.addEventListener('click', getAiHint); // <<< Add listener for hint button
// Initial setup
resetGame();
}