优化结果

This commit is contained in:
2025-09-15 19:08:47 +08:00
parent 72084a8782
commit dcfa89e63c
357 changed files with 16156 additions and 1589 deletions

View File

@@ -0,0 +1,79 @@
/* 经典扫雷 - 手机竖屏优先 + 电脑端适配 */
:root{
--bg:#0f172a;
--panel:#111827;
--accent:#22d3ee;
--accent-2:#60a5fa;
--text:#e5e7eb;
--muted:#94a3b8;
--danger:#ef4444;
--success:#22c55e;
--warn:#f59e0b;
--cell:#1f2937;
--cell-hover:#273244;
--flag:#fb7185;
}
*{box-sizing:border-box}
html,body{height:100%;}
body{margin:0;background:linear-gradient(180deg,#0b1220,#0f172a);color:var(--text);font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,"PingFang SC","Microsoft Yahei",sans-serif;-webkit-tap-highlight-color:transparent}
.app{min-height:100dvh;display:flex;flex-direction:column;gap:12px;padding:12px;}
.header{display:flex;flex-direction:column;gap:10px;background:rgba(255,255,255,0.02);border:1px solid rgba(255,255,255,0.06);border-radius:14px;padding:12px 12px 10px;backdrop-filter:blur(6px)}
.title{margin:0;font-size:20px;letter-spacing:1px}
.hud{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;align-items:center}
.hud-item{display:flex;flex-direction:column;align-items:center;justify-content:center;background:var(--panel);border:1px solid rgba(255,255,255,0.06);border-radius:10px;padding:8px 6px}
.hud-item .label{font-size:12px;color:var(--muted)}
.hud-item .value{font-size:18px;font-weight:700;color:#fff}
.btn{appearance:none;border:none;background:#1e293b;color:#fff;padding:10px 12px;border-radius:10px;cursor:pointer;outline:none;transition:.15s transform,.15s background;display:inline-flex;align-items:center;justify-content:center}
.btn:active{transform:scale(.98)}
.btn.primary{background:linear-gradient(90deg,var(--accent),var(--accent-2))}
.btn.primary:active{filter:brightness(.95)}
.main{display:flex;flex-direction:column;gap:12px}
.board-wrapper{display:flex;justify-content:center;align-items:center}
.board{display:grid;gap:4px;touch-action:manipulation;user-select:none;background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.06);border-radius:12px;padding:6px;width:100%;max-width:92vw}
.cell{display:grid;place-items:center;background:var(--cell);border-radius:8px;border:1px solid rgba(255,255,255,0.06);font-weight:700;color:#9ca3af;box-shadow:inset 0 -1px 0 rgba(255,255,255,0.04);aspect-ratio:1/1;font-size:clamp(12px, 2.2vw, 18px)}
.cell.revealed{background:#0b1220;color:#e5e7eb}
.cell:hover{background:var(--cell-hover)}
.cell.flag::after{content:"🚩"}
.cell.mine.revealed{background:#3b0d0d;color:#fff}
.cell.mine.revealed::after{content:"💣"}
.cell[data-n="1"].revealed{color:#60a5fa}
.cell[data-n="2"].revealed{color:#34d399}
.cell[data-n="3"].revealed{color:#f87171}
.cell[data-n="4"].revealed{color:#a78bfa}
.cell[data-n="5"].revealed{color:#fbbf24}
.cell[data-n="6"].revealed{color:#22d3ee}
.cell[data-n="7"].revealed{color:#e879f9}
.cell[data-n="8"].revealed{color:#cbd5e1}
.tips{font-size:12px;color:var(--muted);text-align:center}
.toast{position:fixed;left:50%;bottom:18px;transform:translateX(-50%);background:rgba(17,24,39,.95);border:1px solid rgba(255,255,255,.08);padding:10px 14px;border-radius:10px}
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.45);display:grid;place-items:center;padding:14px}
.modal{width:min(520px,92vw);background:linear-gradient(180deg,#0f172a,#0b1320);border:1px solid rgba(255,255,255,0.08);border-radius:14px;padding:16px 14px}
.modal h2{margin:4px 0 8px;font-size:20px}
.stats{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin:8px 0 14px}
.stats .card{background:var(--panel);border:1px solid rgba(255,255,255,0.06);border-radius:10px;padding:10px}
.stats .card .k{font-size:12px;color:var(--muted)}
.stats .card .v{font-size:18px;font-weight:700}
.modal-actions{display:flex;gap:10px;justify-content:flex-end}
/* 响应式:手机竖屏优先 */
@media (min-width: 480px){
.title{font-size:22px}
}
@media (min-width: 640px){
.app{padding:18px}
.hud{grid-template-columns:repeat(5,minmax(0,1fr))}
}
@media (min-width: 1024px){
.board{ max-width: 420px; }
.header{ padding:10px 10px 8px; }
.hud-item{ padding:6px 4px; }
.hud-item .value{ font-size:16px; }
.title{ font-size:18px; }
}
@media (orientation: landscape) and (max-width: 900px){
.board{transform:scale(.9)}
}

View File

@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="theme-color" content="#2c3e50" />
<title>经典扫雷(手机竖屏适配)</title>
<meta name="description" content="经典Windows扫雷手机竖屏与电脑端自适应支持触摸与键盘操作闯关无尽模式难度逐步增加。" />
<link rel="preload" href="./css/style.css" as="style" />
<link rel="stylesheet" href="./css/style.css" />
</head>
<body>
<div class="app">
<header class="header">
<h1 class="title">经典扫雷</h1>
<div class="hud">
<div class="hud-item"><span class="label">关卡</span><span id="level" class="value">1</span></div>
<div class="hud-item"><span class="label">雷数</span><span id="mines" class="value">0</span></div>
<div class="hud-item"><span class="label">计时</span><span id="timer" class="value">00:00</span></div>
<button id="btn-restart" class="btn primary" aria-label="重新开始">重开</button>
</div>
</header>
<main class="main">
<section class="board-wrapper">
<div id="board" class="board" role="grid" aria-label="扫雷棋盘"></div>
</section>
<section class="tips">
<p>
手机:点开格子,长按插旗;电脑:左键开格,右键插旗;键盘:方向键移动,空格/回车开格F 插旗。
</p>
</section>
</main>
<!-- 关卡完成提示 -->
<div id="toast-level" class="toast" aria-live="polite" style="display:none;"></div>
<!-- 结束统计弹窗 -->
<div id="modal-overlay" class="modal-overlay" style="display:none;">
<div class="modal" role="dialog" aria-labelledby="gameover-title" aria-modal="true">
<h2 id="gameover-title">游戏结束</h2>
<div id="stats" class="stats"></div>
<div class="modal-actions">
<button id="btn-retry" class="btn primary">重新开始</button>
<button id="btn-close" class="btn">关闭</button>
</div>
</div>
</div>
</div>
<script type="module" src="./js/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,273 @@
// 经典扫雷(手机竖屏适配 + 无尽模式 + 键盘操作)
// 模块:状态、生成、交互、键盘、统计
class RNG {
constructor(seed = Date.now()) { this.seed = seed >>> 0; }
next() { // xorshift32
let x = this.seed;
x ^= x << 13; x ^= x >>> 17; x ^= x << 5;
this.seed = x >>> 0; return this.seed / 0xffffffff;
}
range(min, max){ return Math.floor(this.next() * (max - min + 1)) + min; }
}
const GameConfig = {
start: { rows: 10, cols: 8, mineRatio: 0.12 }, // 竖屏优先:更多行
levelStep(cfg){
// 难度递增:逐步增加行列与雷密度,控制在移动端也能点击
const next = { ...cfg };
if (next.rows < 16) next.rows++;
if (next.cols < 12) next.cols += (next.rows % 2 === 0 ? 1 : 0);
next.mineRatio = Math.min(0.24, +(next.mineRatio + 0.02).toFixed(2));
return next;
}
}
const State = {
level: 1,
rows: 0,
cols: 0,
mineCount: 0,
revealed: 0,
flags: 0,
grid: [], // { mine, r, c, around, revealed, flag }
timer: 0,
timerId: null,
startTs: 0,
rng: new RNG(),
stats: { opened:0, flagged:0, mistakes:0, time:0 }
};
function el(sel){ return document.querySelector(sel); }
function make(tag, cls){ const e = document.createElement(tag); if(cls) e.className = cls; return e; }
function updateMinesHud(){ el('#mines').textContent = String(Math.max(0, State.mineCount - State.flags)); }
function startTimer(){
State.startTs = Date.now();
const timerEl = el('#timer');
clearInterval(State.timerId);
State.timerId = setInterval(() => {
State.timer = Math.floor((Date.now() - State.startTs)/1000);
const m = String(Math.floor(State.timer/60)).padStart(2,'0');
const s = String(State.timer%60).padStart(2,'0');
timerEl.textContent = `${m}:${s}`;
}, 250);
}
function stopTimer(){ clearInterval(State.timerId); State.timerId = null; }
function setupBoard(cfg){
State.rows = cfg.rows; State.cols = cfg.cols;
const total = cfg.rows * cfg.cols;
State.mineCount = Math.max(1, Math.floor(total * cfg.mineRatio));
State.revealed = 0; State.flags = 0; State.grid = [];
State.stats = { opened:0, flagged:0, mistakes:0, time:0 };
// 更新HUD
el('#level').textContent = String(State.level);
updateMinesHud();
const board = el('#board');
board.innerHTML = '';
board.style.gridTemplateColumns = `repeat(${State.cols}, 1fr)`;
// 生成空格子
for(let r=0;r<State.rows;r++){
State.grid[r] = [];
for(let c=0;c<State.cols;c++){
const cell = { mine:false, r, c, around:0, revealed:false, flag:false, el: null };
const div = make('button','cell');
div.type = 'button';
div.setAttribute('role','gridcell');
div.setAttribute('aria-label', `r${r} c${c}`);
div.addEventListener('contextmenu', e=> e.preventDefault());
// 触摸长按
let pressTimer = null; let longPressed=false;
div.addEventListener('touchstart', e => {
longPressed=false;
pressTimer = setTimeout(()=>{ longPressed=true; toggleFlag(cell); }, 420);
}, {passive:true});
div.addEventListener('touchend', e => { if(pressTimer){ clearTimeout(pressTimer); if(!longPressed) openCell(cell); } });
// 鼠标
div.addEventListener('mousedown', e => {
if(e.button===2){ toggleFlag(cell); }
else if(e.button===0){ if(cell.revealed && cell.around>0) chord(cell); else openCell(cell); }
});
cell.el = div;
State.grid[r][c] = cell;
board.appendChild(div);
}
}
// 随机埋雷
let placed=0;
const setMine = (r,c)=>{ if(!State.grid[r][c].mine){ State.grid[r][c].mine=true; placed++; } };
while(placed < State.mineCount){ setMine(State.rng.range(0,State.rows-1), State.rng.range(0,State.cols-1)); }
recomputeArounds();
// 启动计时
startTimer();
}
function visitNeighbors(r,c, cb){
for(let dr=-1; dr<=1; dr++){
for(let dc=-1; dc<=1; dc++){
if(dr===0 && dc===0) continue;
const nr=r+dr, nc=c+dc;
if(nr>=0 && nr<State.rows && nc>=0 && nc<State.cols) cb(nr,nc);
}
}
}
function countFlagsAround(r,c){ let n=0; visitNeighbors(r,c,(nr,nc)=>{ if(State.grid[nr][nc].flag) n++; }); return n; }
function chord(cell){
if(!cell.revealed || cell.around<=0) return;
const flagged = countFlagsAround(cell.r, cell.c);
if(flagged === cell.around){
visitNeighbors(cell.r, cell.c, (nr,nc)=>{
const ncell = State.grid[nr][nc];
if(!ncell.revealed && !ncell.flag){ openCell(ncell); }
});
}
}
function recomputeArounds(){
for(let r=0;r<State.rows;r++){
for(let c=0;c<State.cols;c++){
if(State.grid[r][c].mine){ State.grid[r][c].around = 0; continue; }
let n=0; visitNeighbors(r,c,(nr,nc)=>{ if(State.grid[nr][nc].mine) n++; });
State.grid[r][c].around = n;
}
}
}
function safeFirstClick(badCell){
// 移除当前雷并将其放到其他非雷位置
badCell.mine = false;
while(true){
const r = State.rng.range(0, State.rows-1);
const c = State.rng.range(0, State.cols-1);
const target = State.grid[r][c];
if(target!==badCell && !target.mine){ target.mine = true; break; }
}
recomputeArounds();
}
function openCell(cell){
if(cell.revealed || cell.flag) return;
// 首次点击必定安全:若第一次就点到雷,则移动该雷并重算数字
if(State.revealed===0 && cell.mine){
safeFirstClick(cell);
}
cell.revealed = true; State.revealed++; State.stats.opened++;
cell.el.classList.add('revealed');
if(cell.mine){
cell.el.classList.add('mine');
endGame(false);
return;
}
if(cell.around>0){ cell.el.dataset.n = cell.around; cell.el.textContent = cell.around; }
else{
// flood fill
visitNeighbors(cell.r, cell.c, (nr,nc)=>{
const ncell = State.grid[nr][nc];
if(!ncell.revealed && !ncell.mine) openCell(ncell);
});
}
checkWin();
}
function toggleFlag(cell){
if(cell.revealed) return;
cell.flag = !cell.flag;
State.flags += cell.flag ? 1 : -1;
State.stats.flagged += cell.flag ? 1 : 0;
cell.el.classList.toggle('flag', cell.flag);
updateMinesHud();
}
function checkWin(){
const totalSafe = State.rows*State.cols - State.mineCount;
if(State.revealed >= totalSafe){
// 通关 -> 进入下一关
showToast(`${State.level} 关完成!`);
stopTimer();
setTimeout(()=>{
State.level++;
const nextCfg = GameConfig.levelStep({ rows: State.rows, cols: State.cols, mineRatio: State.mineCount/(State.rows*State.cols) });
setupBoard(nextCfg);
}, 600);
}
}
function showToast(msg){
const t = el('#toast-level');
t.textContent = msg; t.style.display='block';
t.animate([
{ transform:'translate(-50%, 20px)', opacity:0 },
{ transform:'translate(-50%, 0)', opacity:1, offset:.2 },
{ transform:'translate(-50%, 0)', opacity:1, offset:.8 },
{ transform:'translate(-50%, 10px)', opacity:0 }
], { duration:1200, easing:'ease' }).onfinish = ()=> t.style.display='none';
}
function endGame(win){
stopTimer();
// 展示所有雷
for(let r=0;r<State.rows;r++){
for(let c=0;c<State.cols;c++){
const cell = State.grid[r][c];
if(cell.mine){ cell.el.classList.add('revealed','mine'); }
}
}
State.stats.time = State.timer;
const statsHtml = `
<div class="stats">
<div class="card"><div class="k">关卡</div><div class="v">${State.level}</div></div>
<div class="card"><div class="k">总用时</div><div class="v">${formatTime(State.timer)}</div></div>
<div class="card"><div class="k">开格</div><div class="v">${State.stats.opened}</div></div>
<div class="card"><div class="k">插旗</div><div class="v">${State.stats.flagged}</div></div>
</div>
<p style="color:#94a3b8;margin:6px 0 10px">再接再厉,挑战更高难度!</p>
`;
el('#stats').innerHTML = statsHtml;
el('#modal-overlay').style.display = 'grid';
}
function formatTime(sec){ const m=Math.floor(sec/60), s=sec%60; return `${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`; }
function bindUI(){
el('#btn-restart').addEventListener('click', ()=> restart());
el('#btn-retry').addEventListener('click', ()=> restart());
el('#btn-close').addEventListener('click', ()=>{ el('#modal-overlay').style.display='none'; });
}
function restart(){
el('#modal-overlay').style.display='none';
State.level = 1; setupBoard(GameConfig.start);
}
// 键盘操作(电脑端)
let kb = { r:0, c:0 };
function bindKeyboard(){
document.addEventListener('keydown', (e)=>{
const key = e.key.toLowerCase();
if(['arrowup','w'].includes(key)) move(-1,0);
else if(['arrowdown','s'].includes(key)) move(1,0);
else if(['arrowleft','a'].includes(key)) move(0,-1);
else if(['arrowright','d'].includes(key)) move(0,1);
else if(key==='f'){ toggleFlag(State.grid[kb.r][kb.c]); highlightFocus(); }
else if(key===' ' || key==='enter'){ openCell(State.grid[kb.r][kb.c]); highlightFocus(); }
});
}
function move(dr,dc){ kb.r = clamp(kb.r+dr,0,State.rows-1); kb.c = clamp(kb.c+dc,0,State.cols-1); highlightFocus(); }
function clamp(v,min,max){ return Math.max(min, Math.min(max, v)); }
function highlightFocus(){
// 简单高亮当前聚焦格
for(let r=0;r<State.rows;r++){
for(let c=0;c<State.cols;c++){
const el = State.grid[r][c].el;
el.style.outline = (r===kb.r && c===kb.c) ? '2px solid rgba(96,165,250,.9)' : 'none';
}
}
}
// 初始化
bindUI(); bindKeyboard();
setupBoard(GameConfig.start);
highlightFocus();