打地鼠小游戏
3×3 地鼠洞,30 秒限时敲打随机冒头的地鼠,统计命中 / 漏打 / 命中率。
⏱ 30s
得分 0
命中率 -
准备好后点击开始,30 秒内敲地鼠!
使用体验
- 点击“开始游戏”,地鼠会随机从某个洞口冒头,持续约 0.8~1.4 秒后缩回。
- 点击地鼠得 10 分;打空 / 地鼠消失各记一次漏打。倒计时归零结算。
- 全部逻辑为纯 DOM + CSS transition,无 Canvas、无外部依赖,适合作为游戏开发入门模板。
代码要点
js
const HOLES = 9;
let active = null, score = 0, time = 30, hits = 0, misses = 0, timer = null;
function spawnMole() {
if (time <= 0) return end();
const i = Math.floor(Math.random() * HOLES); // 随机洞口
holes[i].classList.add("up");
active = i;
// 冒头持续 0.8~1.4s 后缩回,然后继续生成下一只
setTimeout(() => {
holes[i].classList.remove("up");
misses += 1;
active = null;
spawnMole();
}, 800 + Math.random() * 600);
}
function whack(i) { // 点击洞
if (active === null || i !== active) { misses += 1; return; }
score += 10; hits += 1; active = null;
holes[i].classList.remove("up");
holes[i].classList.add("hit"); // 打击反馈
setTimeout(() => holes[i].classList.remove("hit"), 160);
spawnMole();
}完整可运行工程
可直接运行纯前端 · 零依赖双击 index.html 即用
把演示区的「打地鼠」抽成独立工程:出洞调度难度随时间收紧,命中 / 漏打 / 命中率统计与演示区一致。
依赖清单:零第三方依赖(仅浏览器原生 DOM + CSS transition),无需 npm install;用现代浏览器打开 index.html 即可运行。
mole-rush/
├─ index.html 3×3 网格 + 状态
├─ app.js 出洞调度 + 统计(含可测纯函数)
├─ styles.css 样式(跟随系统深浅色)
└─ README.md 运行方式与扩展点
index.html
3×3 网格 + 状态栏
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>打地鼠 · 独立工程版</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<main class="wrap">
<h1>打地鼠</h1>
<p class="sub">3×3 地鼠洞,30 秒限时,统计命中 / 漏打 / 命中率。</p>
<section class="panel">
<div class="row">
<button id="start" type="button" class="btn">开始游戏</button>
<span class="msg" id="stat" style="margin:0">时间 30s · 命中 0 · 漏打 0</span>
</div>
<div id="grid" style="display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:14px"></div>
<p class="msg" id="result"></p>
</section>
</main>
<script src="app.js"></script>
</body>
</html>
app.js
出洞调度 + 统计(pickIndex / accuracy 可测)
javascript
/* app.js —— 打地鼠核心逻辑(纯函数可在 node 中自测) */
var HOLES = 9, DURATION = 30;
/* pickIndex:随机挑一个没被占用的洞(rand 可注入) */
function pickIndex(occupied, rand) {
rand = rand || Math.random;
var free = [];
for (var i = 0; i < HOLES; i++) if (occupied.indexOf(i) < 0) free.push(i);
if (!free.length) return -1;
return free[Math.floor(rand() * free.length) % free.length];
}
/* accuracy:命中率(百分比,保留 1 位) */
function accuracy(hit, miss) {
var total = hit + miss;
if (!total) return 0;
return Math.round(hit / total * 1000) / 10;
}
/* nextSpawnDelay:难度随时间收紧 */
function nextSpawnDelay(elapsedSec, rand) {
rand = rand || Math.random;
var base = Math.max(380, 980 - elapsedSec * 20);
return base + rand() * 260;
}
if (typeof module !== "undefined" && module.exports) {
module.exports = { HOLES: HOLES, DURATION: DURATION, pickIndex: pickIndex, accuracy: accuracy, nextSpawnDelay: nextSpawnDelay };
}
if (typeof document !== "undefined") {
document.addEventListener("DOMContentLoaded", function () {
var grid = document.getElementById("grid"), startBtn = document.getElementById("start"),
statEl = document.getElementById("stat"), resultEl = document.getElementById("result");
var cells = [], occupied = [], timers = [], hit = 0, miss = 0, left = DURATION, playing = false, ticker = 0;
for (var i = 0; i < HOLES; i++) {
var c = document.createElement("button");
c.type = "button";
c.className = "hole";
c.style.cssText = "height:74px;border-radius:12px;border:1px solid var(--line);background:rgba(255,255,255,.04);font-size:26px;cursor:pointer";
c.dataset.idx = String(i);
c.textContent = "🕳";
(function (idx, cell) {
cell.addEventListener("click", function () {
if (!playing) return;
if (occupied.indexOf(idx) >= 0) {
hit++;
occupied = occupied.filter(function (x) { return x !== idx; });
cell.textContent = "💥";
setTimeout(function () { cell.textContent = "🕳"; }, 180);
} else {
miss++;
}
update();
});
})(i, c);
cells.push(c);
grid.appendChild(c);
}
function update() {
statEl.textContent = "时间 " + left + "s · 命中 " + hit + " · 漏打 " + miss;
}
function spawnOnce() {
if (!playing) return;
var idx = pickIndex(occupied, Math.random);
if (idx >= 0) {
occupied.push(idx);
cells[idx].textContent = "🐹";
var hold = 620 + Math.random() * 420;
setTimeout(function () {
if (occupied.indexOf(idx) >= 0) {
occupied = occupied.filter(function (x) { return x !== idx; });
cells[idx].textContent = "🕳";
miss++;
update();
}
}, hold);
}
timers.push(setTimeout(spawnOnce, nextSpawnDelay(DURATION - left, Math.random)));
}
function stop() {
playing = false;
timers.forEach(clearTimeout);
timers = [];
clearInterval(ticker);
occupied = [];
cells.forEach(function (c) { c.textContent = "🕳"; });
resultEl.textContent = "本局:命中 " + hit + " · 漏打 " + miss + " · 命中率 " + accuracy(hit, miss) + "%";
startBtn.textContent = "再来一局";
}
startBtn.addEventListener("click", function () {
hit = 0; miss = 0; left = DURATION; playing = true;
startBtn.textContent = "进行中…";
resultEl.textContent = "";
update();
spawnOnce();
ticker = setInterval(function () {
left--;
update();
if (left <= 0) stop();
}, 1000);
});
update();
});
}
styles.css
样式:跟随系统深浅色
css
:root {
--bg: #0e1014; --panel: #171a20; --ink: #e8ecf1; --muted: #9aa4b2; --line: #262b33; --brand: #6fb3a8;
}
@media (prefers-color-scheme: light) {
:root { --bg: #f6f7f8; --panel: #ffffff; --ink: #1a1d21; --muted: #5b636e; --line: #e3e6ea; }
}
* { box-sizing: border-box; }
body {
margin: 0; padding: 24px 16px 48px; background: var(--bg); color: var(--ink);
font: 15px/1.6 system-ui, -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif;
}
.wrap { max-width: 760px; margin: 0 auto; }
h1 { font-size: 1.5rem; margin: 0 0 6px; }
h2 { font-size: 1.05rem; margin: 22px 0 10px; }
.sub { color: var(--muted); margin: 0 0 18px; }
.panel { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 16px; }
.row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.btn {
padding: 8px 16px; border-radius: 8px; border: 1px solid var(--brand);
background: var(--brand); color: #0b0f12; cursor: pointer; font-size: .92rem;
}
.btn.ghost { background: transparent; color: var(--brand); }
.msg { color: var(--muted); margin: 12px 0 0; font-size: .92rem; }
.bars { display: grid; gap: 6px; margin-top: 10px; }
.bar { display: grid; grid-template-columns: 64px 1fr 44px; gap: 8px; align-items: center; font-size: .88rem; }
.bar i { display: block; height: 8px; border-radius: 6px; background: var(--brand); }
.bar span:first-child { color: var(--muted); }
input, select {
padding: 8px 10px; border-radius: 8px; border: 1px solid var(--line);
background: var(--bg); color: var(--ink); font: inherit;
}
textarea { width: 100%; min-height: 90px; padding: 10px; border-radius: 8px; border: 1px solid var(--line); background: var(--bg); color: var(--ink); font: inherit; }
canvas { width: 100%; display: block; border-radius: 10px; }
.chat { max-height: 260px; overflow: auto; display: grid; gap: 10px; }
.chat .me, .chat .pet { padding: 9px 12px; border-radius: 12px; max-width: 82%; font-size: .93rem; }
.chat .me { background: rgba(111,179,168,.16); justify-self: end; }
.chat .pet { background: rgba(255,255,255,.06); justify-self: start; }
README.md
运行方式与扩展点
markdown
# 打地鼠(独立可运行工程)
作品站 fun 区「打地鼠小游戏」的独立工程版:纯前端、零依赖。
## 文件
- `index.html`:3×3 网格 + 状态栏
- `app.js`:出洞调度(难度随时间收紧)、命中 / 漏打统计
- `styles.css`:样式
## 运行
1. 双击 `index.html`,点「开始游戏」
2. 逻辑自测:`node -e "const m=require('./app.js');console.log(m.accuracy(7,3), m.pickIndex([0,1], () => 0))"` → 期望 `70 2`
## 扩展点
- 加关卡:把 `DURATION` 与 `nextSpawnDelay` 的基准值挂到关卡配置上
- 加连击:在命中时维护 `combo` 并给分数乘系数
运行方式
- 新建一个目录,把上面全部文件按文件名分别保存(用「一键复制整份工程」可一次拿全)。
- 前端部分:直接双击
index.html即可运行;若浏览器限制本地文件读取,执行python -m http.server 8000后访问http://localhost:8000/。 - 逻辑自测:各文件
README.md中给出了对应的 Node / Python 自测命令(均为真实可跑的最小用例)。
标注:可直接运行、无需构建;要改造只需动 app.js 顶部常量与 styles.css 里的变量。
来源参考
实现涉及的官方文档(可直接访问):