fork download
  1. <!doctype html>
  2. <html lang="th">
  3. <head>
  4. <meta charset="utf-8" />
  5. <meta name="viewport" content="width=device-width,initial-scale=1" />
  6. <title>Minesweeper - ไรส์สวีปเปอร์ (Playable)</title>
  7. <style>
  8. :root{--bg:#0f1724;--card:#0b1220;--accent:#f59e0b;--text:#e6eef8}
  9. *{box-sizing:border-box}
  10. body{margin:0;font-family:Inter,ui-sans-serif,system-ui,Segoe UI,Roboto,'Noto Sans',Helvetica,Arial;color:var(--text);background:linear-gradient(180deg,#021124 0%, #051026 100%);min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
  11. .app{width:100%;max-width:920px}
  12. .header{display:flex;gap:12px;align-items:center;justify-content:space-between;margin-bottom:14px}
  13. .title{font-size:20px;font-weight:700}
  14. .controls{display:flex;gap:8px;align-items:center}
  15. select,input[type=number]{background:transparent;border:1px solid rgba(255,255,255,0.08);padding:6px 8px;border-radius:8px;color:var(--text)}
  16. button{background:var(--card);border:1px solid rgba(255,255,255,0.04);color:var(--text);padding:8px 12px;border-radius:10px;cursor:pointer}
  17. .status{display:flex;gap:8px;align-items:center}
  18. .chip{background:rgba(255,255,255,0.03);padding:6px 10px;border-radius:8px;font-weight:600}
  19. .board-wrap{background:linear-gradient(180deg, rgba(255,255,255,0.02), transparent);padding:16px;border-radius:12px;border:1px solid rgba(255,255,255,0.03)}
  20. .board{display:grid;gap:4px;margin-top:8px}
  21. .cell{width:34px;height:34px;display:flex;align-items:center;justify-content:center;background:linear-gradient(180deg,#0d2236,#08151f);border-radius:6px;user-select:none;font-weight:700;cursor:pointer}
  22. .cell.open{background:linear-gradient(180deg,#cfe8ff,#eaf6ff);color:#0b2233;cursor:default}
  23. .cell.flag{background:linear-gradient(180deg,#2b3a4b,#283240);}
  24. .cell.mine{background:linear-gradient(180deg,#ffccd5,#ffdce5);color:#7b021f}
  25. .row{display:flex}
  26. .controls-legend{display:flex;gap:8px;align-items:center}
  27. .small{font-size:13px;opacity:0.9}
  28. .footer-note{margin-top:10px;font-size:13px;color:rgba(230,238,248,0.8)}
  29. @media (max-width:520px){.cell{width:28px;height:28px;font-size:13px}.header{flex-direction:column;align-items:stretch}.controls{flex-wrap:wrap}}
  30. </style>
  31. </head>
  32. <body>
  33. <div class="app">
  34. <div class="header">
  35. <div>
  36. <div class="title">Minesweeper — ไรส์สวีปเปอร์</div>
  37. <div class="small">คลิกซ้ายเพื่อเปิด, คลิกขวาเพื่อปักธง (บนมือถือ: แตะค้าง)</div>
  38. </div>
  39. <div class="controls">
  40. <label>
  41. ระดับ:
  42. <select id="preset">
  43. <option value="easy">ง่าย (9×9, 10 ระเบิด)</option>
  44. <option value="medium" selected>กลาง (16×16, 40 ระเบิด)</option>
  45. <option value="hard">ยาก (30×16, 99 ระเบิด)</option>
  46. <option value="custom">กำหนดเอง</option>
  47. </select>
  48. </label>
  49. <label id="customInputs" style="display:none">
  50. W <input id="w" type="number" value="16" min="5" max="60" style="width:64px" />
  51. H <input id="h" type="number" value="16" min="5" max="40" style="width:64px" />
  52. M <input id="m" type="number" value="40" min="1" max="500" style="width:64px" />
  53. </label>
  54. <button id="newBtn">เริ่มใหม่</button>
  55. </div>
  56. </div>
  57.  
  58. <div class="status">
  59. <div class="chip">ระเบิด: <span id="mineCount">40</span></div>
  60. <div class="chip">เวลา: <span id="timer">0</span>s</div>
  61. <div class="chip" id="result">สถานะ: กำลังเล่น</div>
  62. </div>
  63.  
  64. <div class="board-wrap">
  65. <div id="board" class="board" aria-hidden="false"></div>
  66. <div class="footer-note">ทิป: ดับเบิลคลิกที่ช่องเปิดเพื่อเปิดช่องรอบ ๆ ถ้าจำนวนธงรอบตรงกับตัวเลข</div>
  67. </div>
  68. </div>
  69.  
  70. <script>
  71. (function(){
  72. // Minesweeper implementation
  73. const boardEl = document.getElementById('board');
  74. const preset = document.getElementById('preset');
  75. const newBtn = document.getElementById('newBtn');
  76. const mineCountEl = document.getElementById('mineCount');
  77. const timerEl = document.getElementById('timer');
  78. const resultEl = document.getElementById('result');
  79. const customInputs = document.getElementById('customInputs');
  80. const wInput = document.getElementById('w');
  81. const hInput = document.getElementById('h');
  82. const mInput = document.getElementById('m');
  83.  
  84. let W=16,H=16,M=40;
  85. let cells=[], openCount=0, flagged=0, started=false, timer=null, seconds=0, gameOver=false;
  86.  
  87. function setPreset(p){
  88. if(p==='easy'){W=9;H=9;M=10}
  89. else if(p==='medium'){W=16;H=16;M=40}
  90. else if(p==='hard'){W=30;H=16;M=99}
  91. }
  92.  
  93. preset.addEventListener('change',()=>{
  94. if(preset.value==='custom'){customInputs.style.display='inline-block'} else {customInputs.style.display='none';setPreset(preset.value);}
  95. });
  96.  
  97. function clearTimer(){clearInterval(timer);timer=null;seconds=0;timerEl.textContent='0'}
  98. function startTimer(){if(timer) return;timer=setInterval(()=>{seconds++;timerEl.textContent=seconds},1000)}
  99.  
  100. function buildBoard(){
  101. boardEl.innerHTML='';
  102. boardEl.style.gridTemplateColumns = `repeat(${W}, auto)`;
  103. boardEl.style.gridTemplateRows = `repeat(${H}, auto)`;
  104. cells = Array(H).fill(0).map(()=>Array(W).fill(null));
  105. openCount=0;flagged=0;started=false;gameOver=false;clearTimer();resultEl.textContent='สถานะ: กำลังเล่น';mineCountEl.textContent=M;
  106.  
  107. for(let r=0;r<H;r++){
  108. for(let c=0;c<W;c++){
  109. const el = document.createElement('div');
  110. el.className='cell';
  111. el.dataset.r=r;el.dataset.c=c;el.dataset.open='0';el.dataset.flag='0';
  112. el.addEventListener('click',onLeft);
  113. el.addEventListener('contextmenu',onRight);
  114. el.addEventListener('mousedown',onDown);
  115. el.addEventListener('touchstart',onTouchStart,{passive:true});
  116. el.addEventListener('touchend',onTouchEnd);
  117. boardEl.appendChild(el);
  118. cells[r][c]={el, mine:false, number:0, r, c, open:false, flag:false};
  119. }
  120. }
  121. }
  122.  
  123. function placeMines(sr,sc){
  124. // place M mines excluding first click (sr,sc) and neighbours
  125. const forbidden = new Set();
  126. for(let r=sr-1;r<=sr+1;r++) for(let c=sc-1;c<=sc+1;c++) if(inBounds(r,c)) forbidden.add(`${r},${c}`);
  127. let placed=0;
  128. while(placed<M){
  129. const r = Math.floor(Math.random()*H);
  130. const c = Math.floor(Math.random()*W);
  131. const key=`${r},${c}`;
  132. if(forbidden.has(key)) continue;
  133. if(cells[r][c].mine) continue;
  134. cells[r][c].mine=true;placed++;
  135. }
  136. // compute numbers
  137. for(let r=0;r<H;r++){
  138. for(let c=0;c<W;c++){
  139. if(cells[r][c].mine) continue;
  140. let cnt=0;
  141. for(let rr=r-1;rr<=r+1;rr++) for(let cc=c-1;cc<=c+1;cc++) if(inBounds(rr,cc) && cells[rr][cc].mine) cnt++;
  142. cells[r][c].number=cnt;
  143. }
  144. }
  145. }
  146.  
  147. function inBounds(r,c){return r>=0 && r<H && c>=0 && c<W}
  148.  
  149. function reveal(r,c){
  150. const cell = cells[r][c];
  151. if(!cell || cell.open || cell.flag) return;
  152. cell.open=true;cell.el.dataset.open='1';cell.el.classList.add('open');
  153. openCount++;
  154. if(cell.mine){
  155. cell.el.classList.add('mine');cell.el.textContent='💣';
  156. return;
  157. }
  158. if(cell.number>0){cell.el.textContent=cell.number; setNumberColor(cell.el, cell.number)}
  159. else{ // flood fill
  160. for(let rr=r-1;rr<=r+1;rr++) for(let cc=c-1;cc<=c+1;cc++) if(inBounds(rr,cc)) reveal(rr,cc);
  161. }
  162. }
  163.  
  164. function setNumberColor(el,num){
  165. const map = {1:'#0b3b6b',2:'#0b6b3b',3:'#6b0b3b',4:'#2b2b6b',5:'#6b2b2b',6:'#2b6b6b',7:'#1a1a1a',8:'#666'};
  166. el.style.color = map[num] || '#0b2233';
  167. }
  168.  
  169. function onLeft(e){
  170. if(gameOver) return;
  171. const el = e.currentTarget;
  172. const r = +el.dataset.r; const c = +el.dataset.c;
  173. if(!started){placeMines(r,c);started=true;startTimer();}
  174. const cell = cells[r][c];
  175. if(cell.flag || cell.open) return;
  176. if(cell.mine){
  177. // reveal all mines and lose
  178. revealAllMines(r,c);
  179. endGame(false);
  180. return;
  181. }
  182. reveal(r,c);
  183. checkWin();
  184. }
  185.  
  186. function revealAllMines(hitR,hitC){
  187. for(let r=0;r<H;r++) for(let c=0;c<W;c++){
  188. const cell=cells[r][c];
  189. if(cell.mine){cell.el.classList.add('mine');cell.el.textContent='💣';}
  190. }
  191. if(inBounds(hitR,hitC)) cells[hitR][hitC].el.classList.add('mine');
  192. }
  193.  
  194. function onRight(e){
  195. e.preventDefault();
  196. if(gameOver) return;
  197. const el = e.currentTarget;
  198. const r = +el.dataset.r; const c = +el.dataset.c;
  199. const cell=cells[r][c];
  200. if(cell.open) return;
  201. toggleFlag(cell);
  202. checkWin();
  203. }
  204.  
  205. function toggleFlag(cell){
  206. if(cell.flag){cell.flag=false;cell.el.dataset.flag='0';cell.el.classList.remove('flag');cell.el.textContent='';flagged--;}
  207. else{cell.flag=true;cell.el.dataset.flag='1';cell.el.classList.add('flag');cell.el.textContent='🚩';flagged++;}
  208. mineCountEl.textContent = Math.max(0, M - flagged);
  209. }
  210.  
  211. // double-click chord: open neighbours if flags equal number
  212. boardEl.addEventListener('dblclick',(ev)=>{
  213. if(gameOver) return;
  214. const el = ev.target.closest('.cell'); if(!el) return;
  215. const r=+el.dataset.r,c=+el.dataset.c; const cell=cells[r][c];
  216. if(!cell.open || cell.number===0) return;
  217. let flags=0; const neighbours=[];
  218. for(let rr=r-1;rr<=r+1;rr++) for(let cc=c-1;cc<=c+1;cc++) if(inBounds(rr,cc) && !(rr===r && cc===c)){
  219. neighbours.push(cells[rr][cc]); if(cells[rr][cc].flag) flags++;
  220. }
  221. if(flags===cell.number){
  222. neighbours.forEach(n=>{ if(!n.flag && !n.open){ if(n.mine){revealAllMines(n.r,n.c); endGame(false); } else reveal(n.r,n.c); }});
  223. checkWin();
  224. }
  225. });
  226.  
  227. function checkWin(){
  228. if(gameOver) return;
  229. if(openCount === W*H - M){ endGame(true); }
  230. }
  231.  
  232. function endGame(won){
  233. gameOver=true;clearTimer();resultEl.textContent = won? 'สถานะ: ชนะ 🎉' : 'สถานะ: แพ้ 💥';
  234. if(won){ // reveal flags nicely
  235. for(let r=0;r<H;r++) for(let c=0;c<W;c++){ const cell=cells[r][c]; if(cell.mine) cell.el.textContent='🚩'; }
  236. }
  237. }
  238.  
  239. // Support for mobile long-press to flag
  240. let touchTimer=null; let touchCell=null;
  241. function onTouchStart(e){
  242. if(gameOver) return;
  243. const touch = e.target.closest('.cell'); if(!touch) return;
  244. touchCell = touch;
  245. touchTimer = setTimeout(()=>{ // long press 600ms -> flag
  246. const r=+touchCell.dataset.r, c=+touchCell.dataset.c; const cell=cells[r][c]; if(!cell.open) toggleFlag(cell); touchTimer=null; touchCell=null; },600);
  247. }
  248. function onTouchEnd(e){ if(touchTimer){clearTimeout(touchTimer); touchTimer=null; touchCell=null;} }
  249.  
  250. // mouse down for potential chord-style interactions (visual)
  251. function onDown(e){ /* placeholder if we want press effects */ }
  252.  
  253. // keyboard accessibility: space to open, f to flag
  254. document.addEventListener('keydown',(e)=>{
  255. const active = document.activeElement;
  256. if(active && active.classList && active.classList.contains('cell')){
  257. const r=+active.dataset.r,c=+active.dataset.c, cell=cells[r][c];
  258. if(e.key===' '){e.preventDefault(); if(!cell.open) onLeft({currentTarget:active});}
  259. if(e.key.toLowerCase()==='f'){e.preventDefault(); if(!cell.open) toggleFlag(cell);} }
  260. });
  261.  
  262. // build new game with current settings
  263. function newGame(){
  264. if(preset.value==='custom'){
  265. W = Math.max(5, Math.min(60, +wInput.value||16));
  266. H = Math.max(5, Math.min(40, +hInput.value||16));
  267. M = Math.max(1, Math.min(W*H-1, +mInput.value||10));
  268. } else setPreset(preset.value);
  269. buildBoard();
  270. }
  271.  
  272. newBtn.addEventListener('click', ()=>{ newGame(); });
  273.  
  274. // initial
  275. setPreset('medium'); buildBoard();
  276.  
  277. // allow clicking cells via keyboard focus for accessibility
  278. boardEl.addEventListener('click', (ev)=>{ const c=ev.target.closest('.cell'); if(c) c.focus(); });
  279.  
  280. })();
  281. </script>
  282. </body>
  283. </html>
  284.  
Success #stdin #stdout 0.03s 25520KB
stdin
Standard input is empty
stdout
<!doctype html>
<html lang="th">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>Minesweeper - ไรส์สวีปเปอร์ (Playable)</title>
  <style>
    :root{--bg:#0f1724;--card:#0b1220;--accent:#f59e0b;--text:#e6eef8}
    *{box-sizing:border-box}
    body{margin:0;font-family:Inter,ui-sans-serif,system-ui,Segoe UI,Roboto,'Noto Sans',Helvetica,Arial;color:var(--text);background:linear-gradient(180deg,#021124 0%, #051026 100%);min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
    .app{width:100%;max-width:920px}
    .header{display:flex;gap:12px;align-items:center;justify-content:space-between;margin-bottom:14px}
    .title{font-size:20px;font-weight:700}
    .controls{display:flex;gap:8px;align-items:center}
    select,input[type=number]{background:transparent;border:1px solid rgba(255,255,255,0.08);padding:6px 8px;border-radius:8px;color:var(--text)}
    button{background:var(--card);border:1px solid rgba(255,255,255,0.04);color:var(--text);padding:8px 12px;border-radius:10px;cursor:pointer}
    .status{display:flex;gap:8px;align-items:center}
    .chip{background:rgba(255,255,255,0.03);padding:6px 10px;border-radius:8px;font-weight:600}
    .board-wrap{background:linear-gradient(180deg, rgba(255,255,255,0.02), transparent);padding:16px;border-radius:12px;border:1px solid rgba(255,255,255,0.03)}
    .board{display:grid;gap:4px;margin-top:8px}
    .cell{width:34px;height:34px;display:flex;align-items:center;justify-content:center;background:linear-gradient(180deg,#0d2236,#08151f);border-radius:6px;user-select:none;font-weight:700;cursor:pointer}
    .cell.open{background:linear-gradient(180deg,#cfe8ff,#eaf6ff);color:#0b2233;cursor:default}
    .cell.flag{background:linear-gradient(180deg,#2b3a4b,#283240);}
    .cell.mine{background:linear-gradient(180deg,#ffccd5,#ffdce5);color:#7b021f}
    .row{display:flex}
    .controls-legend{display:flex;gap:8px;align-items:center}
    .small{font-size:13px;opacity:0.9}
    .footer-note{margin-top:10px;font-size:13px;color:rgba(230,238,248,0.8)}
    @media (max-width:520px){.cell{width:28px;height:28px;font-size:13px}.header{flex-direction:column;align-items:stretch}.controls{flex-wrap:wrap}}
  </style>
</head>
<body>
  <div class="app">
    <div class="header">
      <div>
        <div class="title">Minesweeper — ไรส์สวีปเปอร์</div>
        <div class="small">คลิกซ้ายเพื่อเปิด, คลิกขวาเพื่อปักธง (บนมือถือ: แตะค้าง)</div>
      </div>
      <div class="controls">
        <label>
          ระดับ:
          <select id="preset">
            <option value="easy">ง่าย (9×9, 10 ระเบิด)</option>
            <option value="medium" selected>กลาง (16×16, 40 ระเบิด)</option>
            <option value="hard">ยาก (30×16, 99 ระเบิด)</option>
            <option value="custom">กำหนดเอง</option>
          </select>
        </label>
        <label id="customInputs" style="display:none">
          W <input id="w" type="number" value="16" min="5" max="60" style="width:64px" />
          H <input id="h" type="number" value="16" min="5" max="40" style="width:64px" />
          M <input id="m" type="number" value="40" min="1" max="500" style="width:64px" />
        </label>
        <button id="newBtn">เริ่มใหม่</button>
      </div>
    </div>

    <div class="status">
      <div class="chip">ระเบิด: <span id="mineCount">40</span></div>
      <div class="chip">เวลา: <span id="timer">0</span>s</div>
      <div class="chip" id="result">สถานะ: กำลังเล่น</div>
    </div>

    <div class="board-wrap">
      <div id="board" class="board" aria-hidden="false"></div>
      <div class="footer-note">ทิป: ดับเบิลคลิกที่ช่องเปิดเพื่อเปิดช่องรอบ ๆ ถ้าจำนวนธงรอบตรงกับตัวเลข</div>
    </div>
  </div>

<script>
(function(){
  // Minesweeper implementation
  const boardEl = document.getElementById('board');
  const preset = document.getElementById('preset');
  const newBtn = document.getElementById('newBtn');
  const mineCountEl = document.getElementById('mineCount');
  const timerEl = document.getElementById('timer');
  const resultEl = document.getElementById('result');
  const customInputs = document.getElementById('customInputs');
  const wInput = document.getElementById('w');
  const hInput = document.getElementById('h');
  const mInput = document.getElementById('m');

  let W=16,H=16,M=40;
  let cells=[], openCount=0, flagged=0, started=false, timer=null, seconds=0, gameOver=false;

  function setPreset(p){
    if(p==='easy'){W=9;H=9;M=10}
    else if(p==='medium'){W=16;H=16;M=40}
    else if(p==='hard'){W=30;H=16;M=99}
  }

  preset.addEventListener('change',()=>{
    if(preset.value==='custom'){customInputs.style.display='inline-block'} else {customInputs.style.display='none';setPreset(preset.value);}
  });

  function clearTimer(){clearInterval(timer);timer=null;seconds=0;timerEl.textContent='0'}
  function startTimer(){if(timer) return;timer=setInterval(()=>{seconds++;timerEl.textContent=seconds},1000)}

  function buildBoard(){
    boardEl.innerHTML='';
    boardEl.style.gridTemplateColumns = `repeat(${W}, auto)`;
    boardEl.style.gridTemplateRows = `repeat(${H}, auto)`;
    cells = Array(H).fill(0).map(()=>Array(W).fill(null));
    openCount=0;flagged=0;started=false;gameOver=false;clearTimer();resultEl.textContent='สถานะ: กำลังเล่น';mineCountEl.textContent=M;

    for(let r=0;r<H;r++){
      for(let c=0;c<W;c++){
        const el = document.createElement('div');
        el.className='cell';
        el.dataset.r=r;el.dataset.c=c;el.dataset.open='0';el.dataset.flag='0';
        el.addEventListener('click',onLeft);
        el.addEventListener('contextmenu',onRight);
        el.addEventListener('mousedown',onDown);
        el.addEventListener('touchstart',onTouchStart,{passive:true});
        el.addEventListener('touchend',onTouchEnd);
        boardEl.appendChild(el);
        cells[r][c]={el, mine:false, number:0, r, c, open:false, flag:false};
      }
    }
  }

  function placeMines(sr,sc){
    // place M mines excluding first click (sr,sc) and neighbours
    const forbidden = new Set();
    for(let r=sr-1;r<=sr+1;r++) for(let c=sc-1;c<=sc+1;c++) if(inBounds(r,c)) forbidden.add(`${r},${c}`);
    let placed=0;
    while(placed<M){
      const r = Math.floor(Math.random()*H);
      const c = Math.floor(Math.random()*W);
      const key=`${r},${c}`;
      if(forbidden.has(key)) continue;
      if(cells[r][c].mine) continue;
      cells[r][c].mine=true;placed++;
    }
    // compute numbers
    for(let r=0;r<H;r++){
      for(let c=0;c<W;c++){
        if(cells[r][c].mine) continue;
        let cnt=0;
        for(let rr=r-1;rr<=r+1;rr++) for(let cc=c-1;cc<=c+1;cc++) if(inBounds(rr,cc) && cells[rr][cc].mine) cnt++;
        cells[r][c].number=cnt;
      }
    }
  }

  function inBounds(r,c){return r>=0 && r<H && c>=0 && c<W}

  function reveal(r,c){
    const cell = cells[r][c];
    if(!cell || cell.open || cell.flag) return;
    cell.open=true;cell.el.dataset.open='1';cell.el.classList.add('open');
    openCount++;
    if(cell.mine){
      cell.el.classList.add('mine');cell.el.textContent='💣';
      return;
    }
    if(cell.number>0){cell.el.textContent=cell.number; setNumberColor(cell.el, cell.number)}
    else{ // flood fill
      for(let rr=r-1;rr<=r+1;rr++) for(let cc=c-1;cc<=c+1;cc++) if(inBounds(rr,cc)) reveal(rr,cc);
    }
  }

  function setNumberColor(el,num){
    const map = {1:'#0b3b6b',2:'#0b6b3b',3:'#6b0b3b',4:'#2b2b6b',5:'#6b2b2b',6:'#2b6b6b',7:'#1a1a1a',8:'#666'};
    el.style.color = map[num] || '#0b2233';
  }

  function onLeft(e){
    if(gameOver) return;
    const el = e.currentTarget;
    const r = +el.dataset.r; const c = +el.dataset.c;
    if(!started){placeMines(r,c);started=true;startTimer();}
    const cell = cells[r][c];
    if(cell.flag || cell.open) return;
    if(cell.mine){
      // reveal all mines and lose
      revealAllMines(r,c);
      endGame(false);
      return;
    }
    reveal(r,c);
    checkWin();
  }

  function revealAllMines(hitR,hitC){
    for(let r=0;r<H;r++) for(let c=0;c<W;c++){
      const cell=cells[r][c];
      if(cell.mine){cell.el.classList.add('mine');cell.el.textContent='💣';}
    }
    if(inBounds(hitR,hitC)) cells[hitR][hitC].el.classList.add('mine');
  }

  function onRight(e){
    e.preventDefault();
    if(gameOver) return;
    const el = e.currentTarget;
    const r = +el.dataset.r; const c = +el.dataset.c;
    const cell=cells[r][c];
    if(cell.open) return;
    toggleFlag(cell);
    checkWin();
  }

  function toggleFlag(cell){
    if(cell.flag){cell.flag=false;cell.el.dataset.flag='0';cell.el.classList.remove('flag');cell.el.textContent='';flagged--;}
    else{cell.flag=true;cell.el.dataset.flag='1';cell.el.classList.add('flag');cell.el.textContent='🚩';flagged++;}
    mineCountEl.textContent = Math.max(0, M - flagged);
  }

  // double-click chord: open neighbours if flags equal number
  boardEl.addEventListener('dblclick',(ev)=>{
    if(gameOver) return;
    const el = ev.target.closest('.cell'); if(!el) return;
    const r=+el.dataset.r,c=+el.dataset.c; const cell=cells[r][c];
    if(!cell.open || cell.number===0) return;
    let flags=0; const neighbours=[];
    for(let rr=r-1;rr<=r+1;rr++) for(let cc=c-1;cc<=c+1;cc++) if(inBounds(rr,cc) && !(rr===r && cc===c)){
      neighbours.push(cells[rr][cc]); if(cells[rr][cc].flag) flags++;
    }
    if(flags===cell.number){
      neighbours.forEach(n=>{ if(!n.flag && !n.open){ if(n.mine){revealAllMines(n.r,n.c); endGame(false); } else reveal(n.r,n.c); }});
      checkWin();
    }
  });

  function checkWin(){
    if(gameOver) return;
    if(openCount === W*H - M){ endGame(true); }
  }

  function endGame(won){
    gameOver=true;clearTimer();resultEl.textContent = won? 'สถานะ: ชนะ 🎉' : 'สถานะ: แพ้ 💥';
    if(won){ // reveal flags nicely
      for(let r=0;r<H;r++) for(let c=0;c<W;c++){ const cell=cells[r][c]; if(cell.mine) cell.el.textContent='🚩'; }
    }
  }

  // Support for mobile long-press to flag
  let touchTimer=null; let touchCell=null;
  function onTouchStart(e){
    if(gameOver) return;
    const touch = e.target.closest('.cell'); if(!touch) return;
    touchCell = touch;
    touchTimer = setTimeout(()=>{ // long press 600ms -> flag
      const r=+touchCell.dataset.r, c=+touchCell.dataset.c; const cell=cells[r][c]; if(!cell.open) toggleFlag(cell); touchTimer=null; touchCell=null; },600);
  }
  function onTouchEnd(e){ if(touchTimer){clearTimeout(touchTimer); touchTimer=null; touchCell=null;} }

  // mouse down for potential chord-style interactions (visual)
  function onDown(e){ /* placeholder if we want press effects */ }

  // keyboard accessibility: space to open, f to flag
  document.addEventListener('keydown',(e)=>{
    const active = document.activeElement;
    if(active && active.classList && active.classList.contains('cell')){
      const r=+active.dataset.r,c=+active.dataset.c, cell=cells[r][c];
      if(e.key===' '){e.preventDefault(); if(!cell.open) onLeft({currentTarget:active});}
      if(e.key.toLowerCase()==='f'){e.preventDefault(); if(!cell.open) toggleFlag(cell);}    }
  });

  // build new game with current settings
  function newGame(){
    if(preset.value==='custom'){
      W = Math.max(5, Math.min(60, +wInput.value||16));
      H = Math.max(5, Math.min(40, +hInput.value||16));
      M = Math.max(1, Math.min(W*H-1, +mInput.value||10));
    } else setPreset(preset.value);
    buildBoard();
  }

  newBtn.addEventListener('click', ()=>{ newGame(); });

  // initial
  setPreset('medium'); buildBoard();

  // allow clicking cells via keyboard focus for accessibility
  boardEl.addEventListener('click', (ev)=>{ const c=ev.target.closest('.cell'); if(c) c.focus(); });

})();
</script>
</body>
</html>