384 lines
14 KiB
JavaScript
384 lines
14 KiB
JavaScript
/**
|
||
* 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 });
|
||
}); |