用 Astro 与 Vue 3 打造浏览器版 Boggle 游戏
译注:本文编译自 dev.to 上的一篇技术实践文章,原文作者介绍了其使用 Astro 与 Vue 3 构建浏览器版 Boggle 拼字游戏(Puzzle Boggle)时的工程决策。原文链接见文末。
做一个文字游戏,乍看之下并不复杂。一个 Boggle 风格的游戏似乎只需要字母网格、词库、输入处理和计分器就够了。但一旦游戏需要同时兼顾桌面端和移动端,实现就会变成 UI 状态管理、图遍历、单词校验、触摸交互与性能优化的组合体。
作者最近用 Astro 和 Vue 3 构建了一个浏览器版 Boggle 游戏,并撰文分享了背后的部分工程决策。目标并非打造一个庞大的游戏引擎,而是采用轻量架构:由 Astro 负责网站与内容型页面,Vue 负责交互式游戏本身。最终项目为 Puzzle Boggle。
为什么选择 Astro + Vue 3?
对于同时包含静态内容和交互式应用的项目,Astro 尤其值得关注。一个文字游戏网站通常有两类截然不同的页面:内容与信息类页面,以及高度交互的游戏页面。传统 SPA 能很好地处理后者,但对前者而言可能过于沉重。
Astro 在两者之间提供了有用的分离:站点静态部分由 Astro 渲染,需要交互行为的地方再引入 Vue 组件。基本架构大致如下:
Astro
│
├── Layout
├── SEO / Metadata
├── Content Pages
├── Game Pages
│
└── Vue
├── GameBoard
├── LetterTile
├── WordInput
├── ScorePanel
└── GameResult核心思路是:并非所有东西都需要变成一个 Vue 应用。只有真正需要客户端状态的部分才需要 hydration。
将游戏 UI 与网站分离
最早的架构决策之一,是把网站层与游戏层分开。Astro 侧负责页面结构、导航、元数据、信息内容、SEO 和静态布局;Vue 侧负责当前游戏状态、已选字母、用户输入、单词校验、计分、计时器、动画和游戏结束。
这种分离让项目更易理解——游戏可以像大型内容型网站内部的一个小型应用那样运作。例如:
src/
├── components/
│ ├── GameBoard.vue
│ ├── LetterTile.vue
│ ├── ScorePanel.vue
│ └── GameResult.vue
│
├── composables/
│ ├── useGame.ts
│ ├── useBoard.ts
│ └── useTimer.ts
│
├── data/
│ ├── dictionaries/
│ └── puzzles/
│
├── pages/
│ ├── index.astro
│ ├── daily-challenge/
│ └── boggle/
│
└── layouts/
└── Layout.astro这并非唯一合理的结构,但尽早分离 UI 组件、游戏逻辑与数据,会让项目维护起来轻松得多。
为 Boggle 棋盘建模
棋盘可以用二维数组表示,例如一个 4 × 4 棋盘:
const board = [
['T', 'R', 'A', 'P'],
['E', 'S', 'I', 'N'],
['L', 'O', 'G', 'D'],
['M', 'E', 'T', 'A']
]游戏过程中,每个格子需要承载的不只是字母。一种有用的内部表示是:
interface Cell {
row: number
col: number
letter: string
}行列坐标很重要,因为 Boggle 本质上是一个路径搜索问题。玩家并非单纯地选择字母,而是在相邻格子之间创建一条路径。
核心玩法问题是图遍历
这大概是实现 Boggle 时最有趣的部分。每个字母块都可视为图中的一个节点,它可以连接到相邻的字母块:
A B C
D E F
G H I从 E 出发,玩家可能移动到:
A B C
D F
G H I也就是说,棋盘可被当作一张图,每个格子最多有八个邻居。八个方向为:
const directions = [
[-1, -1],
[-1, 0],
[-1, 1],
[ 0, -1],
[ 0, 1],
[ 1, -1],
[ 1, 0],
[ 1, 1]
]当玩家选中一个格子时,游戏需要判断下一个格子是否与前一个相邻。一个简单的辅助函数即可处理:
function isAdjacent(
a: Cell,
b: Cell
): boolean {
const rowDistance = Math.abs(a.row - b.row)
const colDistance = Math.abs(a.col - b.col)
return (
rowDistance <= 1 &&
colDistance <= 1 &&
!(rowDistance === 0 && colDistance === 0)
)
}另一条重要规则是:同一个格子通常不能在一个单词中重复使用。因此游戏状态需要维护路径:
const selectedCells = ref<Cell[]>([])当玩家选中新格子时,游戏会检查:该格子是否与前一个格子相邻?是否已被选中?若合法则加入当前路径,否则拒绝该选择。这些规则在移动端尤为重要——用户是用手指而非鼠标与棋盘交互。
使用 Vue 3 Composition API
Vue 3 的 Composition API 很适合这类游戏,因为游戏逻辑可以被拆分为可复用的 composable。例如:
const score = ref(0)
const currentWord = ref('')
const selectedCells = ref<Cell[]>([])
const foundWords = ref<string[]>([])
const gameOver = ref(false)随后逻辑可以组织成函数:
function selectCell(cell: Cell) {
if (!canSelect(cell)) {
return
}
selectedCells.value.push(cell)
currentWord.value += cell.letter
}以及:
function submitWord() {
const word = currentWord.value.toLowerCase()
if (!isValidWord(word)) {
resetSelection()
return
}
if (foundWords.value.includes(word)) {
resetSelection()
return
}
foundWords.value.push(word)
score.value += calculateScore(word)
resetSelection()
}这也是作者偏好用 Composition API 编写交互式游戏组件的原因之一:状态与行为可以保持在一起,而不必把整个游戏塞进一个巨型组件。
单词校验是另一个独立问题
棋盘遍历算法与词典系统不应紧密耦合,这是两个不同的问题:
玩家能否拼出这串字母?
↓
currentWord
↓
这串字母是合法单词吗?
↓
dictionary将它们分开,游戏会更容易修改。例如词典可以加载进一个 Set:
const dictionary = new Set([
'apple',
'orange',
'stone',
'game'
])校验因此变得非常快:
function isValidWord(word: string) {
return dictionary.has(word)
}真实游戏中的词典显然大得多。因此一个有趣的工程挑战不只是「找词」,而是决定应该把多少词典数据加载进浏览器。
客户端校验 vs 服务端校验
对于休闲类浏览器文字游戏,客户端校验可以非常快,流程为:玩家 → 选择格子 → 构建单词 → 查词典 → 计算得分 → 更新 UI。这避免了玩家每次提交单词都发送网络请求。
但对竞技类游戏而言还有另一层考量:如果客户端负责一切,技术高超的玩家有可能篡改游戏状态。对休闲游戏来说这或许可以接受;但涉及竞技计分或排行榜时,重要结果应在服务端验证。由此可得一个有用的区分:
本地游玩
↓
快速的客户端校验
竞技结果
↓
服务端验证这种架构也让日后引入每日挑战或排行榜更加容易。
生成有趣的棋盘
随机字母未必能生成有趣的 Boggle 棋盘。完全随机的棋盘可能产生过多无解组合,或只有极少数可玩单词。更好的做法是把棋盘生成视为一个带约束的问题:
随机字母
↓
生成候选棋盘
↓
评估棋盘
↓
检查单词数量
↓
检查难度
↓
接受 / 重新生成棋盘生成器可以用词典评估候选棋盘,估算有多少合法单词可用。这在棋盘生成、单词发现、难度评估和玩法之间形成了有趣的分离。
确定性种子也很有用,例如 generateBoard(seed)。如果相同种子产生相同棋盘,每日谜题就能被稳定复现——这对每日挑战系统很有价值,因为每位玩家都能拿到同一道题。
设计每日挑战
每日谜题引入了另一个有趣的工程问题。最简单的模型是:日期 → 确定性种子 → 棋盘生成器 → 每日棋盘。例如:
const seed = createDailySeed('2026-09-26')
const board = generateBoard(seed)如果生成算法是确定性的,服务端未必需要存储每一个生成的棋盘,日期本身即可作为种子来源。这能显著简化数据模型。同样的思路也可用于每日数独、每日 Wordle 类游戏、每日填字游戏和每日逻辑谜题。
移动端交互比桌面端更难
桌面端实现可以从 mousedown、mousemove、mouseup 起步,但移动端用户是用手指交互的,这会带来若干问题:手指比鼠标光标大得多;手指可能遮挡所选格子;触摸移动……(原文在此处截断)
原文链接
https://dev.to/cliffwang/building-a-browser-based-boggle-game-with-astro-and-vue-3-1m6l