写博客的人通常会把精力放在正文排版、图片压缩和交互样式上但很少有人想到在某个没有链接的页面角落里还可以给读者留一个可以亲自走进去的“地下世界”。这个标题为 “Theres a dungeon under this blog” 的彩蛋项目目标就是在博客底部或隐藏入口背后放一个可探索的地牢读者按下一个快捷键页面切换成字符地图用方向键在房间与走廊之间移动离开后进度自动保存在浏览器本地。下面会从零实现这个项目并给出地图数据、渲染、碰撞检测、存档与排错等完整链路。学完以后你可以把它嵌入静态博客、个人主页甚至用同样的思路做产品官网的隐藏互动页。1. 先理解“博客底下的地牢”到底是什么1.1 这不是游戏博客的专利而是一种内容彩蛋从用户视角看它像一个小游戏从工程视角看它更像一个需要自己管理状态的前端交互模块。它不需要服务器、数据库或账号系统只需要把地图数据用二维数组保存下来把玩家坐标保存在内存里再通过 DOM 渲染到页面上。很多成熟网站都做过类似彩蛋技术支持页面藏终端、个人主页隐藏文字冒险、官网设置可进入的迷宫。共同点是入口不必放在显眼位置但一旦进入体验必须是完整的。项目标题里的 “under this blog” 强调的正是这种隐藏感它不是一个落地页而是博客内容之外的第二层空间。1.2 技术主线隐藏入口、地图数据、渲染与状态持久化把项目拆开看一共有四个核心模块。入口决定用户如何进入地牢。地图数据用二维数组描述房间、走廊、墙壁、出口和物品。渲染层把二维数组变成可以看的游戏画面。状态持久化在刷新或离开后保存玩家位置和背包。这四个模块可以对应到多个文件也可以统一集中在一段脚本里。对于学习版本不要先急着接入后端或框架也不要一开始就做多层关卡。正确的切入顺序是先让入口能打开地牢再让地牢能渲染再让角色能移动最后加入存档和异常处理。只要这条主线清楚后面扩展房间事件、怪物、掉落物都有位置可放。1.3 为什么用纯前端方案而不是后端游戏服务静态博客通常没有应用服务器部署在 GitHub Pages、Nginx 或对象存储上无法稳定运行 Java、Python 或 Node 后端。如果为了一个彩蛋单独买一台服务器成本太高维护也麻烦。地牢的数据量在早期其实很小。一张地图哪怕做到 50 × 50 个格子也不过几千个字符完全可以作为 JavaScript 常量存在。交互逻辑也只有移动、碰撞、拾取和存档几个动作不需要实时同步。浏览器自带的 localStorage 已经能保存玩家坐标、背包和部分状态。Canvas 或 WebGL 在大型游戏里有用但字符地图用 DOM 和 CSS 更轻视觉效果也符合地牢主题。后续想改成 Canvas只需要替换渲染函数地图数据和碰撞逻辑不用动。真正的取舍点是如果以后需要多玩家排行榜、跨设备同步、动态地图更新那才值得引入服务端或云数据库否则纯前端就是最合适的最小方案。2. 环境准备与项目结构静态博客里塞进一个地牢2.1 本案例的依赖与运行环境这个项目对运行环境要求不高但仍建议按标准静态站点的方式准备。直接把 HTML 文件放在桌面双击打开有时候会因为本地文件限制导致部分现代特性不可用所以本地开发时最好起一个静态服务器。项目推荐最低要求Node.js18用于跑地图校验脚本16浏览器Chrome / Edge / Safari / Firefox 最新版支持 ES6 的现代浏览器静态服务器npx serve或 VSCode Live Server任意静态服务器构建工具不需要不需要当前方案不引入依赖所以没有安装成本。纯 HTML、CSS、JavaScript 三件套足够完成。如果你打算把地牢嵌入 Hexo、VuePress 或 Hugo只需要把后面的代码复制到对应模板和静态资源目录入口逻辑保持一致即可。2.2 目录与文件划分建议把地牢做成一个独立目录而不是把代码直接塞进博客全局样式或脚本里。这样能减少样式污染以后想删除彩蛋也方便。blog/ ├── index.html └── dungeon/ ├── index.html ├── css/ │ └── dungeon.css ├── js/ │ └── dungeon.js ├── data/ │ └── map.js └── tools/ └── validate-map.jsindex.html地牢的独立页面也可以作为博客页面里的一个隐藏区块。dungeon.css地图格子、玩家、墙壁、出口、物品的样式。dungeon.js入口检测、渲染、移动、碰撞、存档逻辑。map.js地图数据单独放文件方便更新地图而不动逻辑。validate-map.js本地 Node 脚本用于检查地图是否有墙、出口和可达路径。如果博客本身是单页应用也可以把dungeon目录里的 JS/CSS 打包进主构建流程。但初始阶段不建议先考虑构建配置先把最小功能跑通再决定如何集成。2.3 初始化一个可运行的 HTML 入口下面是一个最小的 HTML 入口。地牢应用默认隐藏由入口函数控制显示。!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleBlog Dungeon/title link relstylesheet hrefcss/dungeon.css /head body div iddungeon-app classdungeon-app hidden div iddungeon-map classdungeon-map/div div iddungeon-status classdungeon-status/div button iddungeon-close typebutton离开地牢/button button iddungeon-reset typebutton重新开始/button /div script srcjs/dungeon.js/script /body /html这里的hidden属性很重要防止入口还没触发时地牢就出现在页面上。JS 里通过openDungeon()将hidden设为false玩家才能看到地图。检查点在浏览器里打开这个页面应该只看到空页面或底部按钮不能看到未初始化的字符地图。如果直接看到了地图说明初始化顺序写错了比如渲染函数在入口函数之前无条件执行。3. 用 JSON 定义地牢地图让房间和走廊可视化3.1 地图数据结构房间、墙壁、出口与物品地图用字符串数组表示每一行代表地图中的一行格子。字符串里的每个字符代表一个格子类型。const DUNGEON_MAP [ ##########, #S.......#, #...##...#, #...#....#, #..$G....#, ########## ];约定如下#表示墙壁玩家不能进入。.表示地板玩家可以行走。S表示出生点渲染时不显示玩家初始出现在这里。G表示出口玩家走到这里触发通关。$表示物品玩家踩到后拾取。玩家不写进地图数据里而是单独保存一个坐标对象。这样做的原因是玩家位置是动态状态地图是静态数据。如果直接把玩家写进地图数组每次移动都要先清理旧位置代码会复杂很多。启动时扫描S的位置把坐标交给玩家对象。function findMark(map, mark) { for (let y 0; y map.length; y) { const row map[y]; for (let x 0; x row.length; x) { if (row[x] mark) { return { x, y }; } } } return null; }3.2 用字符阵列代替画布降低实现成本字符阵列地图有几个实际优点。首先是可读性强打开map.js就能直接看出地牢形状不需要打开地图编辑器。其次是 Git diff 友好改动一行就能看到这是一个房间还是一堵墙。最后是校验简单二维数组的边界、长度和特殊标记都能用脚本检查。缺点是它只能表达“格子”类地图不适合做自由角度、多层级视觉的游戏。但对于博客彩蛋来说字符地图反而更有复古冒险感。不要在一开始就引入对象矩阵或 JSON 嵌套结构。过度设计的典型表现是地图里每个格子都写成{ type: wall, walkable: false }结果手写地图变得非常累。先用字符约定跑通流程等确实需要给每个格子附加更多属性时再考虑升级数据结构。3.3 地图编辑与快速验证用 Node 脚本生成合法地图手工地图很容易出现两个问题某些行长度不一致导致渲染时格子错位出口放在封闭区域玩家走不过去。这些都可以用脚本提前校验。下面是一个 Node 脚本的核心逻辑它能检查地图是否矩形、是否有起点和出口、起点和出口是否可达。// tools/validate-map.js const DUNGEON_MAP [ ##########, #S.......#, #...##...#, #...#....#, #..$G....#, ########## ]; function findMark(map, mark) { for (let y 0; y map.length; y) { const row map[y]; for (let x 0; x row.length; x) { if (row[x] mark) { return { x, y }; } } } return null; } function isReachable(map, start, goal) { const queue [start]; const visited new Set([${start.x},${start.y}]); while (queue.length)