Untitled

 avatar
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 &nbsp;<span class="kbd">F</span> flag &nbsp;<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