Files
mines-game/mines.js
2026-02-15 10:58:58 +01:00

384 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* MINES - Casino Game
* Elegant browser-based Mines game simulation
*/
class MinesGame {
constructor(config = {}) {
// Grid configuration (easily changeable)
this.gridSize = config.gridSize || 5;
this.totalTiles = this.gridSize * this.gridSize;
// Game state
this.balance = 1000;
this.bet = 10;
this.minesCount = 3;
this.isPlaying = false;
this.revealedCount = 0;
this.currentMultiplier = 1.0;
this.minePositions = [];
this.history = [];
// DOM Elements
this.elements = {
balance: document.getElementById('balance'),
betInput: document.getElementById('bet-amount'),
minesCount: document.getElementById('mines-count'),
minesSlider: document.getElementById('mines-slider'),
minesDecrease: document.getElementById('mines-decrease'),
minesIncrease: document.getElementById('mines-increase'),
tilesInfo: document.getElementById('tiles-info'),
safeTiles: document.getElementById('safe-tiles'),
nextMultiplier: document.getElementById('next-multiplier'),
currentMultiplier: document.getElementById('current-multiplier'),
potentialWin: document.getElementById('potential-win'),
startBtn: document.getElementById('start-btn'),
cashoutBtn: document.getElementById('cashout-btn'),
gameGrid: document.getElementById('game-grid'),
gridOverlay: document.getElementById('grid-overlay'),
overlayIcon: document.getElementById('overlay-icon'),
overlayText: document.getElementById('overlay-text'),
revealedCount: document.getElementById('revealed-count'),
totalSafe: document.getElementById('total-safe'),
historyList: document.getElementById('history-list'),
modal: document.getElementById('result-modal'),
modalContent: document.getElementById('modal-content'),
modalIcon: document.getElementById('modal-icon'),
modalTitle: document.getElementById('modal-title'),
modalAmount: document.getElementById('modal-amount'),
modalSubtitle: document.getElementById('modal-subtitle'),
modalClose: document.getElementById('modal-close')
};
// Initialize
this.init();
}
init() {
this.updateGridCSS();
this.createGrid();
this.bindEvents();
this.updateUI();
this.showOverlay('💎', 'Place your bet and start!');
}
updateGridCSS() {
document.documentElement.style.setProperty('--grid-size', this.gridSize);
this.elements.tilesInfo.textContent = `${this.totalTiles} (${this.gridSize}×${this.gridSize})`;
}
createGrid() {
this.elements.gameGrid.innerHTML = '';
for (let i = 0; i < this.totalTiles; i++) {
const tile = document.createElement('div');
tile.className = 'tile disabled';
tile.dataset.index = i;
tile.innerHTML = '<span class="tile-icon"></span>';
this.elements.gameGrid.appendChild(tile);
}
}
bindEvents() {
// Bet input
this.elements.betInput.addEventListener('input', (e) => {
this.bet = Math.max(1, parseFloat(e.target.value) || 1);
this.updateUI();
});
// Bet buttons (half/double)
document.querySelectorAll('.bet-btn').forEach(btn => {
btn.addEventListener('click', () => {
const action = btn.dataset.action;
if (action === 'half') {
this.bet = Math.max(1, Math.floor(this.bet / 2));
} else if (action === 'double') {
this.bet = Math.min(this.balance, this.bet * 2);
}
this.elements.betInput.value = this.bet;
this.updateUI();
});
});
// Mines selector
this.elements.minesDecrease.addEventListener('click', () => {
this.minesCount = Math.max(1, this.minesCount - 1);
this.elements.minesSlider.value = this.minesCount;
this.updateUI();
});
this.elements.minesIncrease.addEventListener('click', () => {
this.minesCount = Math.min(this.totalTiles - 1, this.minesCount + 1);
this.elements.minesSlider.value = this.minesCount;
this.updateUI();
});
this.elements.minesSlider.addEventListener('input', (e) => {
this.minesCount = parseInt(e.target.value);
this.updateUI();
});
// Update slider max based on grid size
this.elements.minesSlider.max = this.totalTiles - 1;
// Start button
this.elements.startBtn.addEventListener('click', () => this.startRound());
// Cashout button
this.elements.cashoutBtn.addEventListener('click', () => this.cashOut());
// Modal close
this.elements.modalClose.addEventListener('click', () => this.hideModal());
// Tile clicks
this.elements.gameGrid.addEventListener('click', (e) => {
const tile = e.target.closest('.tile');
if (tile && this.isPlaying && !tile.classList.contains('revealed') && !tile.classList.contains('disabled')) {
this.revealTile(parseInt(tile.dataset.index));
}
});
}
/**
* Calculate multiplier based on mines count and revealed tiles
* Formula: Each safe reveal increases multiplier based on probability
* The fewer safe tiles remaining, the higher the reward
*/
calculateMultiplier(revealed) {
if (revealed === 0) return 1.0;
const safeTiles = this.totalTiles - this.minesCount;
let multiplier = 1.0;
// House edge (around 3%)
const houseEdge = 0.97;
for (let i = 0; i < revealed; i++) {
const remainingTiles = this.totalTiles - i;
const remainingSafe = safeTiles - i;
// Fair odds * house edge
const stepMultiplier = (remainingTiles / remainingSafe) * houseEdge;
multiplier *= stepMultiplier;
}
return Math.round(multiplier * 100) / 100;
}
getNextMultiplier() {
return this.calculateMultiplier(this.revealedCount + 1);
}
updateUI() {
// Update mines count display
this.elements.minesCount.textContent = this.minesCount;
// Update safe tiles count
const safeTiles = this.totalTiles - this.minesCount;
this.elements.safeTiles.textContent = safeTiles;
this.elements.totalSafe.textContent = safeTiles;
// Update multipliers
this.elements.currentMultiplier.textContent = `×${this.currentMultiplier.toFixed(2)}`;
this.elements.nextMultiplier.textContent = `×${this.getNextMultiplier().toFixed(2)}`;
// Update potential win
const potentialWin = this.bet * this.currentMultiplier;
this.elements.potentialWin.textContent = this.formatCurrency(potentialWin);
// Update revealed count
this.elements.revealedCount.textContent = this.revealedCount;
// Update balance
this.elements.balance.textContent = this.formatCurrency(this.balance);
// Update start button state
this.elements.startBtn.disabled = this.bet > this.balance || this.bet <= 0;
}
formatCurrency(amount) {
return '$' + amount.toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
}
showOverlay(icon, text) {
this.elements.overlayIcon.textContent = icon;
this.elements.overlayText.textContent = text;
this.elements.gridOverlay.classList.remove('hidden');
}
hideOverlay() {
this.elements.gridOverlay.classList.add('hidden');
}
generateMinePositions() {
const positions = [];
while (positions.length < this.minesCount) {
const pos = Math.floor(Math.random() * this.totalTiles);
if (!positions.includes(pos)) {
positions.push(pos);
}
}
return positions;
}
startRound() {
if (this.bet > this.balance) return;
// Deduct bet from balance
this.balance -= this.bet;
// Reset game state
this.isPlaying = true;
this.revealedCount = 0;
this.currentMultiplier = 1.0;
this.minePositions = this.generateMinePositions();
// Update UI
this.hideOverlay();
this.elements.startBtn.classList.add('hidden');
this.elements.cashoutBtn.classList.remove('hidden');
// Reset all tiles
document.querySelectorAll('.tile').forEach(tile => {
tile.className = 'tile';
tile.querySelector('.tile-icon').textContent = '';
});
// Disable bet controls during game
this.elements.betInput.disabled = true;
this.elements.minesSlider.disabled = true;
this.elements.minesDecrease.disabled = true;
this.elements.minesIncrease.disabled = true;
this.updateUI();
}
revealTile(index) {
const tile = document.querySelectorAll('.tile')[index];
const icon = tile.querySelector('.tile-icon');
if (this.minePositions.includes(index)) {
// Hit a mine - GAME OVER
tile.classList.add('revealed', 'mine');
icon.textContent = '💣';
this.endRound(false);
} else {
// Safe tile
tile.classList.add('revealed', 'safe');
icon.textContent = '💎';
this.revealedCount++;
this.currentMultiplier = this.calculateMultiplier(this.revealedCount);
this.updateUI();
// Check if all safe tiles are revealed
const safeTiles = this.totalTiles - this.minesCount;
if (this.revealedCount >= safeTiles) {
this.cashOut();
}
}
}
cashOut() {
if (!this.isPlaying) return;
const winnings = this.bet * this.currentMultiplier;
this.balance += winnings;
this.endRound(true, winnings);
}
endRound(isWin, winnings = 0) {
this.isPlaying = false;
// Reveal all mines
this.minePositions.forEach(pos => {
const tile = document.querySelectorAll('.tile')[pos];
if (!tile.classList.contains('revealed')) {
tile.classList.add('show-mine');
tile.querySelector('.tile-icon').textContent = '💣';
}
});
// Disable all tiles
document.querySelectorAll('.tile').forEach(tile => {
tile.classList.add('disabled');
});
// Re-enable controls
this.elements.betInput.disabled = false;
this.elements.minesSlider.disabled = false;
this.elements.minesDecrease.disabled = false;
this.elements.minesIncrease.disabled = false;
// Update buttons
this.elements.cashoutBtn.classList.add('hidden');
this.elements.startBtn.classList.remove('hidden');
// Add to history
this.addToHistory(isWin, winnings);
// Show result modal
this.showResultModal(isWin, winnings);
this.updateUI();
}
showResultModal(isWin, winnings) {
this.elements.modalContent.className = 'modal-content ' + (isWin ? 'win' : 'loss');
if (isWin) {
this.elements.modalIcon.textContent = '💎';
this.elements.modalTitle.textContent = 'You Won!';
this.elements.modalAmount.textContent = '+' + this.formatCurrency(winnings);
this.elements.modalSubtitle.textContent = `×${this.currentMultiplier.toFixed(2)} multiplier • ${this.revealedCount} tiles revealed`;
} else {
this.elements.modalIcon.textContent = '💣';
this.elements.modalTitle.textContent = 'Mine Hit!';
this.elements.modalAmount.textContent = '-' + this.formatCurrency(this.bet);
this.elements.modalSubtitle.textContent = `${this.revealedCount} tiles revealed before explosion`;
}
this.elements.modal.classList.remove('hidden');
}
hideModal() {
this.elements.modal.classList.add('hidden');
}
addToHistory(isWin, winnings) {
const historyItem = {
isWin,
bet: this.bet,
winnings: isWin ? winnings : -this.bet,
multiplier: this.currentMultiplier,
mines: this.minesCount,
revealed: this.revealedCount
};
this.history.unshift(historyItem);
if (this.history.length > 20) this.history.pop();
this.renderHistory();
}
renderHistory() {
if (this.history.length === 0) {
this.elements.historyList.innerHTML = '<div class="history-empty">No rounds played yet</div>';
return;
}
this.elements.historyList.innerHTML = this.history.map(item => `
<div class="history-item ${item.isWin ? 'win' : 'loss'}">
<div class="history-bet">Bet: ${this.formatCurrency(item.bet)}</div>
<div class="history-result">${item.isWin ? '+' : ''}${this.formatCurrency(item.winnings)}</div>
<div class="history-details">×${item.multiplier.toFixed(2)}${item.mines} mines • ${item.revealed} revealed</div>
</div>
`).join('');
}
}
// Initialize game when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
// You can change grid size here: 5, 6, 7, etc.
window.minesGame = new MinesGame({ gridSize: 5 });
});