我们构建 Cards4.net,是为了让人们无需账号、不看广告就能打开一款纸牌游戏开始玩。游戏开始后,他们应该能够撤销一步,或稍后再回到这局游戏,而不必担心牌面是否变了。保存的游戏应当像保留好的决定一样,忠实地保留他们糟糕的决定。

满足这一期望,塑造了我们在后端使用 Phoenix 和 Elixir、在浏览器中使用 React、用 PostgreSQL 做持久化的方式。规则、操作和存档都必须对玩家做过什么保持一致。

把游戏状态放在服务器上

在经典纸牌(Klondike)中,移动一张牌可能会露出并翻开它下面的牌。如果玩家按下撤销,这两项变化都必须撤回。如果玩家随后离开游戏,保存的局面需要反映他们撤回后的牌面。我们把游戏状态放在服务器上,这样移动处理、撤销和持久化都基于该局面的同一种表示。

当玩家放下一张牌时,浏览器会把想要进行的移动发送到我们的 Phoenix 后端,由规则根据当前状态进行检查。如果移动合法,服务器会执行它,为撤销记录之前的局面,并返回更新后的牌面。随后浏览器就可以显示结果,包括所有被翻开的牌。由于服务器把这些变化放在一起处理,前端不需要根据屏幕上可见的内容去重建之前的局面。

对于多人游戏,这种安排也让我们无需依赖玩家的浏览器来判定发生了什么,就能协调一张牌桌。Phoenix Channels 把操作传给服务器,并把由此产生的更新发送给玩家。由于服务器掌握完整的游戏状态,它可以向每个座位发送该玩家被允许看到的信息,同时不在响应中包含其他人的手牌。

会话结束后保留游戏

游戏过程中,每个纸牌会话都把局面保存在一个 GenServer 中,这是一种按顺序处理传入调用的 Elixir 进程。会话把规则检查委托给相应的游戏模块,从而让经典纸牌、空当接龙(FreeCell)和蜘蛛纸牌(Spider)共用管理会话和存档的代码。

该进程只在内存中保存局面,所以我们还会把快照写入 PostgreSQL。当一个已停止的会话收到请求时,应用程序可以把它保存的状态加载到一个新会话中。恢复是通过这段代码实现的;这些临时会话进程崩溃后,监督者(supervisor)不会自动重启它们。

我们把写入与移动处理分开调度,以免每次移动都要等待数据库。因此,内存中的局面可能领先于最后一次成功保存的内容。测试恢复意味着检查实际写入了什么、能恢复什么,包括撤销历史。

保持操作的响应速度

把规则放在服务器上,意味着每次移动在被接受之前都需要一次网络往返。在慢速连接下这会延迟结果,也使离线游戏无法实现。完全在浏览器中运行的纸牌游戏可以避免这种依赖。我们接受了它,以便规则验证、撤销和进度保存可以使用同一套后端实现。

在玩家拖动一张牌时,界面仍然需要即时响应。我们的牌面使用 React 和 TypeScript,并由 Vite 负责前端构建。React 管理牌面和交互状态,而一个 requestAnimationFrame 循环直接移动拖动预览。这样,即使接受放置仍需要服务器响应,预览也能跟随指针移动,而无需每一帧都更新一次 React 状态。

在手机上,手势本身也需要关注。牌更小,列可能很长,一次触摸可能是要选中一张牌,也可能是要开始拖动。我们把这些交互与规则分开测试,因为知道一叠牌可以合法移动,并不能告诉我们别人拿起它有多容易。被取消的手势和非法放置也需要让牌面保持在玩家能够理解的状态。

重现出错的情况

当玩家报告牌面出现异常时,我们需要重建他们当时所玩的局面。支持的纸牌牌局由种子生成,因此在同一实现下,相同的游戏、变体和种子会产生相同的开局布局。

对于后来才出现的问题,我们还需要导致它的那些移动。截图可以显示哪里看起来不对,而种子和移动序列能让我们把游戏一路跟踪到那一刻。把这些输入与回归测试放在一起,让我们在修复后有办法再次检查这一行为。这也意味着,如果要让旧种子继续产生相同的布局,对洗牌或发牌代码的改动就需要做兼容性检查。

我们还利用错误报告和性能测量来调查故障和缓慢的页面。它们的用途与 Plausible 中的流量数据不同,我们只在获得分析同意后才加载 Plausible。页面浏览量可以告诉我们有人访问过,却无法证明一局游戏被正确地保存或恢复。隐私政策说明了这些独立系统收集的数据。

我们会更早做的事

在另一个纸牌游戏项目中,我们会先让一款游戏走完游玩、撤销、保存和恢复的完整循环,再扩充游戏目录。我们会用一个固定牌局把这个流程保留为测试,以便之后的改动都能对照同一个起点进行检查。这会给我们一个可重复的方法,来发现那种让某步移动在屏幕上正常、却破坏了重新加载后恢复的局面的改动。对于一个人们会在忙其他事情的间隙回来访问的网站,把回访体验做好,值得在开发早期就投入时间。

常见问题

Cards4.net 使用什么技术栈?

后端使用 Phoenix 和 Elixir,搭配 PostgreSQL。前端使用 React、TypeScript 和 Vite。纸牌游戏的操作使用 REST,而多人牌桌使用 Phoenix Channels。游戏牌面不使用 LiveView。

不注册账号可以玩吗?

可以。访客可以进行休闲游戏。如果你想要一个账号,登录时会使用一次性的电子邮件链接。本站不会要求你创建密码。

游戏可以离线运行吗?

不可以。移动在服务器上进行验证,因此玩游戏需要联网。

网站使用什么分析工具?

Plausible 只在你同意分析后才会加载。应用程序另有独立的错误报告和性能监控。详情请参阅隐私政策。

延伸阅读