Untitled
unknown
plain_text
10 months ago
19 kB
16
Indexable
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Minesweeper β Farty Edition</title>
<style>
:root{
--bg:#0f1221;--panel:#171a2b;--panel-2:#1f2340;--accent:#6ee7ff;--accent-2:#9affc9;
--text:#e8ecff;--muted:#9aa3b2;--gap:4px;--radius:10px;--shadow:0 10px 30px rgba(0,0,0,.35);
}
*{box-sizing:border-box}
html,body{height:100%;margin:0}
body{
background:
radial-gradient(1200px 800px at 10% 10%, #1a1f3a, transparent 60%),
radial-gradient(1200px 800px at 90% 20%, #10222e, transparent 60%),
var(--bg);
color:var(--text);
font:16px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial;
display:flex;align-items:center;justify-content:center;padding:20px;
}
.app{width:min(100%,1100px);position:relative}
header{
display:flex;gap:12px;align-items:center;justify-content:space-between;
background:linear-gradient(180deg,var(--panel),var(--panel-2));
border-radius:var(--radius);padding:12px;box-shadow:var(--shadow);flex-wrap:wrap;
}
.brand{display:flex;align-items:center;gap:10px;font-weight:700}
.logo{width:34px;height:34px;border-radius:8px;background:conic-gradient(from 220deg,var(--accent),var(--accent-2));box-shadow:0 8px 16px rgba(0,0,0,.35)}
.ctrls{display:flex;gap:8px;align-items:center}
select,input[type=number]{background:var(--panel-2);color:var(--text);border:1px solid #2b315c;padding:8px;border-radius:8px}
button{background:linear-gradient(180deg,#2c315c,#22264a);color:var(--text);border:1px solid #333a75;padding:8px 12px;border-radius:10px;cursor:pointer}
.stats{display:flex;gap:10px;align-items:center;background:linear-gradient(180deg,var(--panel),var(--panel-2));padding:8px;border-radius:10px;box-shadow:var(--shadow)}
.stat{background:rgba(255,255,255,.03);padding:6px 10px;border-radius:8px;border:1px solid rgba(255,255,255,.03)}
#gridWrap{background:linear-gradient(180deg,var(--panel),var(--panel-2));padding:12px;border-radius:12px;box-shadow:var(--shadow);margin-top:12px}
#grid{display:grid;gap:var(--gap);touch-action:manipulation}
.cell{
width:36px;height:36px;border-radius:8px;background:linear-gradient(180deg,#1a1e36,#13162a);
border:1px solid #2a2f59;display:flex;align-items:center;justify-content:center;font-weight:700;cursor:pointer;user-select:none;
}
.cell:hover{background:#22264a}
.cell.revealed{background:linear-gradient(180deg,#0f1328,#0b0e1f);border-color:#2b315c;cursor:default}
.cell.flagged::after{content:"π©";position:absolute;font-size:20px}
.cell.mine.revealed::after{content:"π£";font-size:20px}
.cell.revealed.zero{color:#7683a6}
.cell.revealed.n1{color:#72b7ff}
.cell.revealed.n2{color:#7efc9a}
.cell.revealed.n3{color:#ffd166}
.cell.revealed.n4{color:#ff9a6b}
.cell.revealed.n5{color:#ff6b6b}
.footer{display:flex;justify-content:space-between;align-items:center;color:var(--muted);margin-top:10px}
.kbd{background:#2d325f;border-radius:6px;padding:2px 6px;font-weight:700;color:#cfe2ff}
#startOverlay{
position:absolute;inset:0;background:rgba(0,0,0,.86);display:flex;flex-direction:column;justify-content:center;align-items:center;gap:18px;border-radius:12px;z-index:99;
color:#fff;text-align:center;padding:20px;font-size:20px;
}
#startOverlay button{background:#ff6b6b;padding:14px 28px;border-radius:12px;font-size:18px;border:none;color:#fff;cursor:pointer}
.hidden{display:none!important}
</style>
</head>
<body>
<div class="app">
<div id="startOverlay">
<div style="font-size:28px;font-weight:800">Minesweeper π¨</div>
<div>Grand Opening Fart Edition</div>
<button id="startBtn">Tap to Start!</button>
<div style="font-size:13px;color:#ddd;margin-top:8px">Tap once to unlock audio on iPhone β then every reveal farts.</div>
</div>
<header id="gameHeader" class="hidden">
<div style="display:flex;align-items:center;gap:12px">
<div class="logo" aria-hidden="true"></div>
<div>
<div style="font-weight:700">Minesweeper π¨</div>
<div style="font-size:12px;color:var(--muted)">Safe first click β’ Rapid-fire farts β’ Touch friendly</div>
</div>
</div>
<div style="display:flex;gap:8px;align-items:center">
<div class="ctrls">
<select id="difficulty" title="Difficulty">
<option value="beginner">Beginner (9Γ9 β’ 10)</option>
<option value="intermediate">Intermediate (16Γ16 β’ 40)</option>
<option value="expert">Expert (30Γ16 β’ 99)</option>
<option value="custom">Customβ¦</option>
</select>
<input id="rows" type="number" min="5" max="50" value="9" class="hidden" />
<input id="cols" type="number" min="5" max="60" value="9" class="hidden" />
<input id="mines" type="number" min="1" max="999" value="10" class="hidden" />
<button id="newGameBtn" title="New Game (R)">π New Game</button>
</div>
<div class="stats" style="margin-left:12px">
<div class="stat"><div style="font-size:11px;color:var(--muted)">Mines</div><div id="mineCounter">10</div></div>
<div class="stat"><div style="font-size:11px;color:var(--muted)">Flags</div><div id="flagCounter">0</div></div>
<div class="stat"><div style="font-size:11px;color:var(--muted)">Time</div><div id="timer">0.0s</div></div>
</div>
</div>
</header>
<div id="gridWrap" class="hidden">
<div id="grid" role="grid" aria-label="Minesweeper board"></div>
</div>
<div class="footer hidden" id="footer">
<div style="opacity:.9">Tap to reveal, long-press to flag. Chord by tapping a number when surrounding flags equal the number.</div>
<div><span class="kbd">R</span> new <span class="kbd">F</span> flag <span class="kbd">Space</span> reveal</div>
</div>
</div>
<script>
(() => {
// Elements
const startBtn = document.getElementById('startBtn');
const startOverlay = document.getElementById('startOverlay');
const gameHeader = document.getElementById('gameHeader');
const gridWrap = document.getElementById('gridWrap');
const footer = document.getElementById('footer');
const difficulty = document.getElementById('difficulty');
const rowsInput = document.getElementById('rows');
const colsInput = document.getElementById('cols');
const minesInput = document.getElementById('mines');
const mineCounterEl = document.getElementById('mineCounter');
const flagCounterEl = document.getElementById('flagCounter');
const timerEl = document.getElementById('timer');
const newGameBtn = document.getElementById('newGameBtn');
const gridEl = document.getElementById('grid');
// Audio (WebAudio synth) state
let audioCtx = null;
let unlocked = false;
let currentVoices = 0;
const MAX_VOICES = 40;
function unlockAudio() {
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
if (audioCtx.state === 'suspended') audioCtx.resume();
unlocked = true;
}
function playFart(grand=false) {
if (!unlocked || !audioCtx) return;
if (currentVoices >= MAX_VOICES) return;
currentVoices++;
const sr = audioCtx.sampleRate;
const dur = grand ? 0.5 : (0.10 + Math.random() * 0.14); // ~100-240ms
const len = Math.floor(sr * dur);
const buf = audioCtx.createBuffer(1, len, sr);
const data = buf.getChannelData(0);
// brown-ish noise
let last = 0;
for (let i = 0; i < len; i++) {
const w = Math.random() * 2 - 1;
last = (last + 0.02 * w) / 1.02;
data[i] = last * 3.2 * (grand ? 1.6 : 1);
}
const src = audioCtx.createBufferSource();
src.buffer = buf;
const lp = audioCtx.createBiquadFilter();
lp.type = 'lowpass';
lp.frequency.value = 160 + Math.random() * 360;
lp.Q.value = 0.25;
const gain = audioCtx.createGain();
const now = audioCtx.currentTime;
const maxVol = 0.18 + Math.random() * 0.12;
gain.gain.setValueAtTime(0.0001, now);
gain.gain.exponentialRampToValueAtTime(maxVol, now + 0.01);
gain.gain.exponentialRampToValueAtTime(0.0001, now + dur);
// sub oscillator for rasp
const osc = audioCtx.createOscillator();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(48 + Math.random() * 28, now);
osc.frequency.exponentialRampToValueAtTime(28 + Math.random() * 12, now + dur);
const og = audioCtx.createGain();
og.gain.setValueAtTime(0.03, now);
og.gain.exponentialRampToValueAtTime(0.0015, now + dur);
src.connect(lp);
osc.connect(og);
const mix = audioCtx.createGain();
lp.connect(mix);
og.connect(mix);
mix.connect(gain).connect(audioCtx.destination);
src.start(now);
osc.start(now);
src.stop(now + dur + 0.02);
osc.stop(now + dur + 0.02);
setTimeout(() => { currentVoices = Math.max(0, currentVoices - 1); }, (dur * 1000) | 0 + 40);
}
// Start overlay handling
startBtn.addEventListener('click', () => {
unlockAudio();
playFart(true); // grand opening
startOverlay.classList.add('hidden');
gameHeader.classList.remove('hidden');
gridWrap.classList.remove('hidden');
footer.classList.remove('hidden');
setTimeout(initGame, 80);
}, {passive:true});
// --- Minesweeper logic ---
function initGame() {
// local references (re-query to ensure elements exist)
const grid = gridEl;
let rows = 9, cols = 9, mines = 10;
let board = [];
let firstClick = true;
let flags = 0;
let revealedCount = 0;
let timer = null;
let startTime = 0;
let gameOver = false;
const idx = (r,c) => r*cols + c;
const inBounds = (r,c) => r>=0 && r<rows && c>=0 && c<cols;
const neighbors = (r,c) => {
const arr = [];
for (let dr=-1; dr<=1; dr++){
for (let dc=-1; dc<=1; dc++){
if (!dr && !dc) continue;
const rr = r+dr, cc = c+dc;
if (inBounds(rr,cc)) arr.push([rr,cc]);
}
}
return arr;
};
function clamp(n, lo, hi){ return Math.min(Math.max(n, lo), hi); }
function refreshStats(){ mineCounterEl.textContent = mines; flagCounterEl.textContent = flags; timerEl.textContent = '0.0s'; }
function ensureTimer(){
if (!timer){
startTime = performance.now();
timer = setInterval(()=>{
const t = (performance.now() - startTime)/1000;
timerEl.textContent = t.toFixed(1) + 's';
}, 100);
}
}
function stopTimer(){ if (timer){ clearInterval(timer); timer = null; } }
function setDifficulty(){
const v = difficulty.value;
[rowsInput, colsInput, minesInput].forEach(el => el.classList.add('hidden'));
if (v === 'beginner'){ rows=9; cols=9; mines=10; }
else if (v === 'intermediate'){ rows=16; cols=16; mines=40; }
else if (v === 'expert'){ rows=16; cols=30; mines=99; }
else {
[rowsInput, colsInput, minesInput].forEach(el => el.classList.remove('hidden'));
rows = clamp(parseInt(rowsInput.value||rows), 5, 50);
cols = clamp(parseInt(colsInput.value||cols), 5, 60);
mines = clamp(parseInt(minesInput.value||mines), 1, Math.max(1, rows*cols-1));
}
refreshStats();
newGame();
}
function newGame(){
grid.style.gridTemplateColumns = `repeat(${cols}, 36px)`;
grid.style.gridTemplateRows = `repeat(${rows}, 36px)`;
grid.innerHTML = '';
board = [];
firstClick = true;
flags = 0;
revealedCount = 0;
gameOver = false;
refreshStats();
stopTimer();
for (let r=0;r<rows;r++){
for (let c=0;c<cols;c++){
const cell = { r, c, mine:false, adj:0, state:'hidden', el:null };
const btn = document.createElement('button');
btn.className = 'cell';
btn.setAttribute('role','gridcell');
btn.setAttribute('aria-label','Hidden');
btn.setAttribute('data-r', r);
btn.setAttribute('data-c', c);
btn.addEventListener('click', ()=> reveal(r,c));
btn.addEventListener('contextmenu', (e)=> { e.preventDefault(); toggleFlag(r,c); });
// touch long-press to flag
let touchTimer = null;
btn.addEventListener('touchstart', ()=>{
if (gameOver) return;
touchTimer = setTimeout(()=> { toggleFlag(r,c); touchTimer = null; }, 420);
}, {passive:true});
btn.addEventListener('touchend', ()=>{
if (touchTimer){ clearTimeout(touchTimer); reveal(r,c); }
touchTimer = null;
});
// keyboard support
btn.tabIndex = 0;
btn.addEventListener('keydown', (e)=>{
if (gameOver) return;
const k = e.key;
if (k === 'ArrowLeft'){ focusRC(r, c-1); e.preventDefault(); }
else if (k === 'ArrowRight'){ focusRC(r, c+1); e.preventDefault(); }
else if (k === 'ArrowUp'){ focusRC(r-1, c); e.preventDefault(); }
else if (k === 'ArrowDown'){ focusRC(r+1, c); e.preventDefault(); }
else if (k === ' ' || k === 'Enter'){ reveal(r,c); e.preventDefault(); }
else if (k.toLowerCase() === 'f'){ toggleFlag(r,c); e.preventDefault(); }
});
grid.appendChild(btn);
cell.el = btn;
board.push(cell);
}
}
// focus first cell
if (board[0]) board[0].el.focus();
}
function focusRC(r,c){ if (inBounds(r,c)) board[idx(r,c)].el.focus(); }
function placeMines(safeR, safeC){
let placed = 0;
const total = rows*cols;
const safeIdx = idx(safeR, safeC);
const forbidden = new Set([safeIdx, ...neighbors(safeR,safeC).map(([rr,cc])=> idx(rr,cc))]); // protect neighbors
while (placed < mines){
const k = Math.floor(Math.random()*total);
if (forbidden.has(k)) continue;
const cell = board[k];
if (!cell.mine){ cell.mine = true; placed++; }
}
for (const cell of board){
if (cell.mine){ cell.adj = -1; continue; }
let n = 0;
for (const [rr,cc] of neighbors(cell.r, cell.c)) if (board[idx(rr,cc)].mine) n++;
cell.adj = n;
}
}
function updateCellUI(cell){
const el = cell.el;
el.classList.toggle('revealed', cell.state === 'revealed');
el.classList.toggle('flagged', cell.state === 'flagged');
el.classList.toggle('mine', cell.mine && cell.state === 'revealed');
if (cell.state === 'revealed'){
el.setAttribute('aria-label', cell.mine ? 'Mine' : (cell.adj === 0 ? 'Empty' : `${cell.adj}`));
if (!cell.mine){
el.textContent = cell.adj > 0 ? cell.adj : '';
el.classList.remove('n1','n2','n3','n4','n5','n6','n7','n8','zero');
el.classList.add(cell.adj === 0 ? 'zero' : ('n' + cell.adj));
} else {
el.textContent = '';
}
} else {
el.textContent = '';
el.setAttribute('aria-label','Hidden');
el.classList.remove('n1','n2','n3','n4','n5','n6','n7','n8','zero');
}
}
function showMine(cell){ cell.state = 'revealed'; updateCellUI(cell); }
function endGame(win){
gameOver = true;
stopTimer();
for (const cell of board){
if (cell.mine && cell.state !== 'revealed'){ cell.state = 'revealed'; updateCellUI(cell); }
if (!cell.mine && cell.state === 'flagged'){ cell.state = 'hidden'; updateCellUI(cell); }
}
setTimeout(()=> alert(win ? 'You win! π' : 'Boom! You hit a mine. π₯'), 20);
}
function checkWin(){
if (revealedCount === rows*cols - mines){
// fanfare of toots
for (let i=0;i<6;i++) setTimeout(playFart, 60*i);
endGame(true);
}
}
function floodReveal(r,c){
const stack = [[r,c]];
while (stack.length){
const [rr,cc] = stack.pop();
const cell = board[idx(rr,cc)];
if (cell.state !== 'hidden' || cell.mine) continue;
cell.state = 'revealed';
revealedCount++;
updateCellUI(cell);
playFart(); // fart per revealed cell
if (cell.adj === 0){
for (const [nr,nc] of neighbors(rr,cc)){
const ncell = board[idx(nr,nc)];
if (ncell.state === 'hidden' && !ncell.mine) stack.push([nr,nc]);
}
}
}
}
function chord(r,c){
const cell = board[idx(r,c)];
if (cell.adj <= 0) return;
let flagCount = 0;
const hiddenNeighbors = [];
for (const [rr,cc] of neighbors(r,c)){
const n = board[idx(rr,cc)];
if (n.state === 'flagged') flagCount++;
if (n.state === 'hidden') hiddenNeighbors.push([rr,cc]);
}
if (flagCount === cell.adj){
for (const [rr,cc] of hiddenNeighbors){
if (gameOver) break;
reveal(rr,cc);
}
}
}
function reveal(r,c){
if (gameOver) return;
const cell = board[idx(r,c)];
if (!cell || cell.state === 'flagged' || cell.state === 'revealed'){
if (cell && cell.state === 'revealed' && cell.adj > 0) chord(r,c);
return;
}
if (firstClick){
placeMines(r,c);
firstClick = false;
ensureTimer();
}
if (cell.mine){
showMine(cell);
playFart(); // single toot on boom
endGame(false);
return;
}
floodReveal(r,c);
checkWin();
}
function toggleFlag(r,c){
if (gameOver) return;
const cell = board[idx(r,c)];
if (!cell || cell.state === 'revealed') return;
if (cell.state === 'flagged'){ cell.state = 'hidden'; flags = Math.max(0, flags-1); }
else { cell.state = 'flagged'; flags++; }
updateCellUI(cell);
flagCounterEl.textContent = flags;
}
// Controls
difficulty.addEventListener('change', () => {
if (difficulty.value === 'custom'){
rowsInput.classList.remove('hidden'); colsInput.classList.remove('hidden'); minesInput.classList.remove('hidden');
}
setDifficulty();
});
[rowsInput, colsInput, minesInput].forEach(el => {
el.addEventListener('input', () => {
rows = clamp(parseInt(rowsInput.value||rows), 5, 50);
cols = clamp(parseInt(colsInput.value||cols), 5, 60);
mines = clamp(parseInt(minesInput.value||mines), 1, Math.max(1, rows*cols-1));
mineCounterEl.textContent = mines;
});
});
newGameBtn.addEventListener('click', newGame);
window.addEventListener('keydown', (e) => {
if (e.key.toLowerCase() === 'r') newGame();
if (e.key.toLowerCase() === 'f') {
// flag focused cell if any
const active = document.activeElement;
if (active && active.classList.contains('cell')) {
const r = parseInt(active.getAttribute('data-r')), c = parseInt(active.getAttribute('data-c'));
toggleFlag(r,c);
}
}
});
// initialize
setDifficulty();
}
})();
</script>
</body>
</html>
Editor is loading...
Leave a Comment