优化结果
This commit is contained in:
79
InfoGenie-frontend/public/smallgame/扫雷/css/style.css
Normal file
79
InfoGenie-frontend/public/smallgame/扫雷/css/style.css
Normal 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)}
|
||||
}
|
||||
56
InfoGenie-frontend/public/smallgame/扫雷/index.html
Normal file
56
InfoGenie-frontend/public/smallgame/扫雷/index.html
Normal 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>
|
||||
273
InfoGenie-frontend/public/smallgame/扫雷/js/main.js
Normal file
273
InfoGenie-frontend/public/smallgame/扫雷/js/main.js
Normal 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();
|
||||
Reference in New Issue
Block a user