普通视图

发现新文章,点击刷新页面。
今天 — 2026年1月24日首页

冲上了 Hacker News 第 5 名,竟然是我的 Svelte 练手项目

作者 ougt
2026年1月24日 13:28

今天早上醒来,发生了一件让我有点懵的事情。我前段时间为了学习 Svelte 而写的一个“练手项目”—— Zsweep,竟然冲上了 Hacker News (HN) 首页的第 5 名

(这是后面看到时的截图,最开始上了前5,可惜没有截屏😭) image.png

(此图为证)

image.png

(小站下午游戏时长暴涨100小时🤯) image.png

对于独立开发者来说,HN 的首页就像是“奥斯卡红毯”。看着自己写的代码被全球各地的极客讨论, 我想趁热打铁,在掘金复盘一下这个项目的开发思路技术栈选择,以及我为了让它“好玩”而死磕的一些技术细节

HN:news.ycombinator.com/item?id=466…

Repo: github.com/oug-t/zswee…

🎮 什么是 Zsweep?

简单来说,Zsweep 是一个 Vim 键位驱动的扫雷游戏

zsweep.com

它的灵感来源有两个:

  1. Monkeytype:我很喜欢 Monkeytype 那种极简、无广告、纯粹追求速度的打字体验。
  2. Vim/Neovim:作为一名开发者,我想把 h j k l 的肌肉记忆延伸到游戏里。

所以 Zsweep 的设计哲学就是:极简 UI + 极致手速 + 全键盘操作

image.png

🛠️ 技术栈:为什么选择 SvelteKit + Supabase?

作为一个全栈项目,我没有选择我最熟悉的 React,而是选择了 SvelteKit,搭配 Supabase

1. 前端:SvelteKit + Tailwind CSS

Svelte 真的太爽了。在这个项目里,我深刻体会到了“Write less code”的含义。

  • 状态管理:不需要复杂的 Context 或 Redux,Svelte 的响应式变量让处理游戏状态(比如计时器、剩余雷数、当前选中的格子)变得异常简单。
  • 动画:Svelte 内置的 transitionanimate 指令,让我几行代码就实现了“踩雷”时的屏幕震动和结算界面的数字跳动效果。
  • Vim 键位绑定:我写了一个全局的键盘监听器,配合 Svelte 的 store,实现了丝滑的光标移动体验。

2. 后端 & 数据库:Supabase

因为是独立开发,我不想花时间在配运维环境上。Supabase 提供了 PostgreSQL 数据库和开箱即用的 Auth(认证)服务。

  • 登录:直接集成了 GitHub 和 Google OAuth,几行配置就搞定。
  • 排行榜:利用 Postgres 的强大查询能力,我能很快算出全球排名。

💻 那些让我“掉头发”的技术细节

虽然是扫雷,但为了追求极致体验,我在数据处理上花了不少心思。

1. 核心算法:Mines/Min (扫雷效率)

传统的扫雷只看时间,但不同难度的雷数不一样。为了衡量玩家的真实水平,我参考了 Monkeytype 的 WPM (Words Per Minute),设计了 Mines/Min (每分钟扫雷数) 指标。

(也implement了3BV,但考虑到time mode,还需后续更新)

这里有个坑:如果是通过点击复位(重开)太快,可能会导致除以零或者时间极短的数据异常。 我在前端加了一个健壮的计算逻辑:

TypeScript

// 核心计算逻辑片段
if (timeTaken > 0) {
  const minesPerMin = parseFloat(((mines / timeTaken) * 60).toFixed(1));
  // 只有当成绩更优时才更新本地的最佳记录
  if (!calculatedBests[cat] || minesPerMin > calculatedBests[cat].value) {
    calculatedBests[cat] = {
      value: minesPerMin,
      date: g.created_at
    };
  }
}

2. 全球排行榜与“防作弊”

为了做 Leaderboard,我利用 Supabase 的 Foreign Key 把 game_results 表和 profiles 表关联起来。

刚才上线后发现一个小插曲:数据库里出现了一些 0秒 的通关记录(大概是调试时的残留数据,或者是 API 被人用 Postman 刷了)。

为了保证公平,我在后端查询时加了严格的过滤器,利用 SQL 直接过滤掉异常数据:

TypeScript

const { data } = await supabase
  .from('game_results')
  .select('time, profiles(username)')
  .eq('win', true)
  .gt('time', 0) // 过滤掉 0s 的异常数据
  .order('time', { ascending: true })
  .limit(50);

现在,排行榜终于干净了,还能显示“Your Rank”高亮自己的排名。(下一个PR,的ploy)

3. 用户体验细节

  • Glitch 风格:当踩雷失败时,我没有用普通的弹窗,而是写了一个 CSS Glitch(故障风)特效,配合 "FATAL_ERR" 的文案,更有极客感。
  • 热力图:参考 GitHub Contribution,我在个人主页做了一个扫雷热力图,记录玩家每天的活跃度。

🚀 总结与开源

这次冲上 Hacker News 第 5 名,给我最大的启示是:不要等到项目完美了才发布。

Zsweep 其实还有很多 Issues(比如之前的 Joined Date 显示 Invalid Date,刚刚才修好 😂),UI 也不够完美。但因为它解决了一个小痛点(想用 Vim 玩游戏),并且做得足够简单纯粹,就获得了很多开发者的喜爱。

目前项目完全开源,如果你对 Svelte、Vim 或者扫雷感兴趣,欢迎来 GitHub 给个 Star,或者提 PR 一起改进它!

如果你也喜欢 Vim 或者 Svelte,欢迎在评论区交流!

❌
❌