在线体验:zhimalab.tech/games/redbl... 一个纯前端实现的空间推理解谜游戏------「赤金秘径 Gilded Secret Path」。玩家操控一块有双面能力的赤金方块,在悬空浮岛上规划路线、踩踏机关、收集钥匙,最终抵达金色祭坛。没有后端,没有 React,一个静态 HTML + 一段 TypeScript + Phaser 4 就完成了棋盘引擎、双语界面、存档和结算动画。
最近给儿童启蒙站点(芝麻园地)做空间思维训练的小游戏。对标 Math Playground 的 Red Block Returns 系列:操控方块一格一格走,绕过虚空、踩机关、到祭坛。这个品类核心乐趣是**「想清楚再走」**------高思考密度、低操作频率,特别适合做给孩子。
为了追求极致的加载速度和可维护性,这一版我做了一个比较"克制"的技术选型:不引 React,不用状态库。整页是静态 HTML,Phaser 负责渲染,棋盘逻辑抽成一个纯函数引擎,UI 用一个极简的发布订阅 store 驱动。下面把关键设计拆开讲讲。
一、架构总览:静态骨架 + Phaser 渲染,零框架依赖
页面是一个 .astro 静态页面:主菜单、选关、HUD、结算层全部是写死的 HTML 骨架,游戏脚本在 DOMContentLoaded 后创建 Phaser 实例:
ts
const config: Phaser.Types.Core.GameConfig = {
type: Phaser.AUTO,
parent: 'game-container',
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH,
},
render: { transparent: true, antialias: true },
input: { activePointers: 2 },
scene: [BootScene, BoardScene],
};
三个关键点:
transparent: true:Phaser 画布是透明的,星空、星云、浮岛这些氛围背景全部用页面 CSS 画在画布下面。棋盘像浮在夜空中,视觉效果丰富,却一点不占 Phaser 的渲染开销。activePointers: 2:多点触控支持,孩子和家长可以同时操作。- 没有任何 UI 库 :菜单/选关/结算都是 DOM,用
classList.toggle('hidden')控制显示。好处是状态切换肉眼可见、好调试,E2E 也好写。
二、核心:纯函数棋盘引擎
棋盘逻辑我特意写成与 Phaser 完全解耦的纯函数模块 ,方便单测和回溯。createState(level) 构建初始状态,move(state, dir) 返回新状态,computeStars(state) 算星级:
ts
export function move(state: EngineState, dir: Dir): MoveOutcome {
if (state.status !== 'playing') return { ok: false, reason: 'already-over' };
const { dx, dy } = DIR_DELTA[dir];
const tx = state.blockX + dx, ty = state.blockY + dy;
const target = cellAt(state, tx, ty);
// 锁门未开:挡住,不算步数
if (target && target.type === 'door' && !target.doorOpen) {
return { ok: false, reason: 'blocked' };
}
// 虚空 / 出界 → 死亡
if (!isWalkable(target)) {
return { ok: true, next: { ...state, status: 'dead', deaths: state.deaths + 1, steps: state.steps + 1, deathReason: 'fell' } };
}
// 浅拷贝 grid,只改被影响的格子
const grid = state.grid.map((row) => [...row]);
const next = { ...state, grid, blockX: tx, blockY: ty, steps: state.steps + 1 };
// ... 落地处理:开关激活门 / 拾钥匙 / 塌陷砖离开即塌 / 到祭坛胜利
}
设计要点:
- 状态不可变 :每次移动返回全新状态,天然支持撤销------把旧状态压进历史栈,
undo()出栈即可。 - 撤销限次 :每关 5 次(
UNDO_LIMIT = 5),给容错但不至于无脑试。 - 塌陷砖不可逆 :踩上去能站,离开后立即
crumbled = true,变回虚空,不许回头------"每一步都算数"的紧张感就来自这里。
星级评价沿用经典三条件:
ts
export function computeStars(state: EngineState): StarRating {
if (state.status !== 'won') return 0;
let stars: StarRating = 1;
if (state.steps <= state.level.par_steps) stars = 2;
if (state.level.star_conditions.no_death && state.deaths === 0) stars = 3;
return stars;
}
三、提示系统:BFS 寻路,只告诉你"下一步"
怕孩子卡关,做了提示功能。但提示不直接帮走,而是从当前位置 BFS 寻路到祭坛,返回"下一步"该踩的格子------保留思考空间:
ts
export function nextHintStep(state: EngineState): { x: number; y: number } | null {
const queue: Node[] = [{ x: state.blockX, y: state.blockY }];
visited.add(`${state.blockX},${state.blockY}`);
while (queue.length > 0) {
const cur = queue.shift()!;
for (const d of Object.values(DIR_DELTA)) {
const nx = cur.x + d.dx, ny = cur.y + d.dy;
// 虚空不可通过,门视为可通过(避免把玩家锁死在视野内)
const passable = cell.type !== 'void' && !(cell.type === 'crumble' && cell.crumbled);
if (!passable) continue;
const first = cur.first ?? { x: nx, y: ny };
if (cell.type === 'altar') return first;
queue.push({ x: nx, y: ny, first });
}
}
return null;
}
每关提示只能用一次(hintUsed 标记),用完即止,逼玩家自己动脑。
四、UI 与引擎之间:极简状态总线
不用 zustand,也不用 React。一个十几行的类搞定:
ts
class RedBlockStore {
private state: RedBlockState = initialState();
private listeners = new Set<Listener>();
set(partial: Partial<RedBlockState>): void {
this.state = { ...this.state, ...partial };
this.listeners.forEach((fn) => fn(this.state, prev));
}
subscribe(fn: Listener): () => void { /* ... */ }
}
UI 写 store.set(...),引擎读订阅。好处是无依赖、无 SSR 水合顾虑,还顺手暴露给 window.__RBC_STORE__ 给 E2E 测试读状态。
五、轻量双语:data-i18n + 一个字典
这个游戏面向中英双语(还能当英语启蒙用),但我不想为一个字典功能上 i18n 框架。方案是:静态文本用 data-i18n 属性,动态文本用 t()/tpl() 函数:
ts
zh.menuHintDesc = '操控双面赤金方块穿越浮空岛链,踩踏机关、收集钥匙,抵达金色祭坛。';
en.menuHintDesc = 'Guide the two-sided gilded block across floating islands --- step on switches, collect keys, and reach the golden altar.';
function applyStaticI18n(root: ParentNode = document): void {
root.querySelectorAll<HTMLElement>('[data-i18n]').forEach((el) => {
const key = el.dataset.i18n as MessageKey | undefined;
if (key && messages[currentLang][key]) el.textContent = t(key);
});
syncDocument(); // 同步 <html lang> / document.title / meta
}
语言选择持久化到 localStorage('redblock.lang'),默认跟随浏览器语言,页面右上角有中 / EN 一键切换。连 document.title 和 meta description 都会跟着换------对 SEO 和分享卡片友好。
六、进度存档与结算反馈
- 进度 :每关最佳星级存
localStorage('redblock.bestStars'),选关页实时显示星数、解锁下一关。 - 结算动画 :通关后金色粒子飘落、皇冠弹出、三星逐个点亮,再给"步数 / 死亡"统计。死亡则慢下来,用文案告诉你**"哪里走错了"**------
fell(掉虚空)、crumble(塌陷砖撑不住)、exhausted(步数耗尽),失败也变成学习信号。
七、移动端:触控方向键 + 手势
PC 用方向键 / WASD,Z 撤销、R 重置。移动端专门加了屏幕方向键(只在 pointer: coarse 的小屏显示),避免误触;同时 prefers-reduced-motion: reduce 时关掉所有动画,照顾动效敏感的孩子。
写在最后
整个项目仍是纯静态:一个 HTML + 十几个 TypeScript 文件 + Phaser 4,没有后端、没有 UI 框架。核心思路是**"逻辑与渲染彻底分离"**------棋盘引擎是纯函数、UI 是静态 DOM、Phaser 只负责把状态画出来。这套组合对做"交互式小游戏/互动教程"这类内容特别顺手,也特别好测。
在线体验(免费,浏览器打开即玩):
- 赤金秘径:zhimalab.tech/games/redbl...
- 芝麻园地(儿童启蒙,更多游戏与互动教程):zhimalab.tech
目前上线的是第一章「觉醒之石」3 个教学关(纯移动 → 绕虚空 → 塌陷砖),第二章「冰封回廊」的冰面滑行机制已在路上。如果你也在做网页解谜或给孩子的互动内容,欢迎去玩两关提提意见------尤其是"提示要不要再给多一点"这种手感问题,得让真玩家敲键盘说了算。😄