Zkt Timer
大家好,我是 Ibrahim。我玩魔方已经很久了,开始参加比赛之后,它对我来说就不再只是一个爱好,而是变成了一件认真得多的事情。随着训练越来越认真,每次还原之后我都会问自己同样的问题:我在哪里浪费了时间?哪个衔接拖慢了我?哪个公式我还没有真正练熟?我当时用的工具计时做得很好,但在帮我找到这些答案方面却帮不上太多忙。我想要的是这样一个工具:能深入分析我的还原,能有条理地练习公式,并且能在同一个地方完成这一切。我试过不同的应用,却没有一个完全符合我的需求,最后我决定自己动手把它做出来。
起初我的目标很朴素:只是想做一个自己用的、每个细节都了如指掌的简单计时器。但每加入一个部分,都会随之带来新的需求。我先写了分析工具来研究自己的还原,然后是用来练习公式的训练器,之后又加入了智能魔方连接和跨设备同步。这些都远没有听起来那么简单:每个魔方品牌的蓝牙协议本身就是一道谜题,在一台设备上完美运行的功能,换到另一台设备上可能会在完全意想不到的地方出问题,有时为了追一个 bug,我要花上好几个小时,甚至好几天。尽管如此,每解决一个问题,我就往前多走一步,那个小小的计时器也随着时间慢慢成长为一个完整的生态。
这个项目始于 2025 年,从那时起,它已经走出了很远,和最初的样子大不相同。最初在 Web 上起步的项目,后来逐渐扩展到了 iOS 和 Android;如今同一套代码运行在浏览器、手机和平板上。这条路的大部分,我都是一个人走过来的。从应用商店审核到支付系统,从原生代码到实时对战,每一步对我来说都是一次全新的学习,几乎没有一样是第一次就做对的。在这段时间里,我的工作从来不只是添加新功能;真正艰难的,是一次又一次地重新审视各个系统,把其中一些不止一次地从头设计,并让所有部分和谐地协同工作。
一个最初只为自己编写的工具,如今已成为其他魔方爱好者每天训练、彼此较量、关注比赛的地方。看到素不相识的人在 Zkt Timer 上打破个人纪录,我就知道这一切付出没有白费。现在相当一部分新功能也来自使用者的反馈。我并不把 Zkt Timer 看作一个已经完成的产品;它是我至今仍每天投入其中、持续成长的项目,说实话,我觉得自己还只是站在这条路的起点。

Zkt Timer — ibrhyyme
设计和开发 Zkt Timer 的人。
致谢
前面我说过,这条路的大部分是我一个人走过来的,但说实话,我从来都不是真正的孤身一人。速拧魔方圈多年来形成了一种非常慷慨的开源文化,在开发 Zkt Timer 的过程中,我一直从中汲取养分。我花了很长时间研究许多项目:有些在自己的电脑上运行试用,有些则一行一行地读,只为真正弄懂它们是如何工作的。
每个项目都让我学到了不同的东西。有的在打乱生成、求解器思路等技术问题上为我指明了方向;有的让我看清了在训练器和用户体验方面什么才真正有效。有时是一个算法思路,有时是一个数据模型,有时则是看待整个系统的一种视角,在我卡住的时候为我打开了局面。
我没有把这些部分原样拿来使用,而是重新审视每一部分,让它们契合 Zkt Timer 自身的结构。大部分系统我都从头重写了,有些做了简化,有些则根据需要进行了扩展。把来自不同来源的思路统一到一个架构之下,让它们彼此和谐地协同工作,是这个过程中最困难、也最有收获的部分。
因此,今天的这套结构并不依赖于某一个来源;它是把我从许多不同项目中学到的东西汇集起来、重新构建而成的。但我必须坦白地说:如果没有下面这些开发者的付出,Zkt Timer 绝不可能在这么短的时间里走到今天。
我相信,推动一件事向前发展,从来不只是添加新的东西;真正理解前人所做的工作、尊重它,并在其基础上做出诚实的贡献,同样重要。因此,对每一位直接或间接影响过这个项目的人给予应有的认可,对我来说与其说是一种礼节,不如说是一笔必须偿还的债。
下面提到的每一个项目和每一位开发者,都在 Zkt Timer 的成长中扮演了不同的角色。有的是一个想法的起点,有的帮我解决了困扰我很久的问题,有的则开阔了我的视野,让我看到了更好的方法。这些成果大多是人们牺牲自己的业余时间开发出来,并且不求任何回报地与所有人分享的;直到我在自己的项目中扛起同样的担子,才更深刻地体会到这有多么珍贵。
向每一位把自己的心血、时间和知识分享给这个社区的人,致以我最深的敬意和最诚挚的感谢。有你们真好。
— ibrhyyme
Zkt Timer 的新功能
本节收录为 Zkt Timer 设计并开发的功能。每一项都源于平台自身的需求以及用户提出的反馈。
比赛通知与倒计时系统
Zkt Timer 会为你关注的比赛生成自动通知。报名开放、分组与任务分配的发布、成绩录入以及被打破的纪录都会以通知形式送达,同时会显示距离比赛日期的倒计时。该系统在服务器端以定时任务运行,并同时产生应用内通知和推送通知。
纪录雷达
提前计算你所参加的比赛中可能被打破的纪录。系统会比对选手名单、项目日程与现行的全国纪录,判断哪些纪录面临风险,并在比赛页面上展示结果。
原生移动应用
Zkt Timer 以原生应用的形式在 iOS 和 Android 上发布。智能魔方连接通过原生 BLE 层运行,加速度计与触觉反馈由原生插件提供。边缘手势、安全区域和键盘行为在每个平台上分别处理。
Magnet Timer
一种仅在 iOS 和 Android 应用中提供的计时方式,无需触碰屏幕即可计时。手机平放在桌面上,魔方放在手机后置摄像头一端旁边;手机的指南针传感器会感应魔方内部的磁铁。拿起魔方即开始计时,放回桌面即停止。由于每部手机的传感器表现各不相同,校准需要针对每个魔方和每个手机型号分别进行,阈值根据传感器的原始数值计算得出。读取传感器的原生层分别为 iOS 和 Android 编写,检测逻辑也在真实设备的录制数据上经过了测试。
完整离线模式
在没有网络连接的情况下,依然可以记录还原、开启分组并查看统计数据。连接恢复后,待处理的还原会进入队列并与服务器进行核对,冲突会被解决,重复记录会依据指纹被过滤掉。
Offline Battle(1v1)
两个人在同一台设备上正面对战的模式。在手机和平板上,屏幕分成相对的两半;在桌面端,双方并排而坐,使用 A 键和 L 键。双方各自独立开始,每人都可以连接自己的智能魔方。罚时与得分分别记录,回合历史也会被详细保存。
Online Battle
使用智能魔方,与同一时刻正在寻找对手的另一位玩家进行的 1v1 对局。双方还原同一个打乱,先赢下三局者获胜。时间由魔方自身的时钟测量,而不是通过网络,因此网络延迟不会影响结果。等待越久,匹配的分数范围就放得越宽;对局由服务器主导,每次还原都会依据转动记录和魔方时钟进行验证。分数采用 Glicko-2 计算,并在排名页面中列出。
公式集
把想练习的公式收集到自己的公式集中。OLL、PLL、F2L 等不同类别的情形可以放在同一个公式集里。公式集可以设为私密、仅凭链接分享或公开,分享的链接打开时带有自己的预览。公开的公式集会出现在社区公式集中,其他人可以保存。修改会即时保存,公式集也会存储在设备上以便离线使用。
私信
用户之间一对一交流的私信系统。陌生人发来的第一条消息不会直接进入收件箱,而是在“请求”标签中等待确认。对话中可以附上一次还原,对方能看到成绩并打开还原详情。已发送的消息可以编辑或撤回。谁可以给你发消息、已读回执、正在输入提示和在线圆点都取决于每位用户自己的设置,屏蔽和举报功能也内置其中。
个性化
各类魔方的配色按组分别设置,从 2x2 到 7x7,以及 Square-1、Clock 和 FTO,应用中的所有图形都会随之变化:打乱图、智能魔方视图等。计时器背后可以放一张图片或一段最大 40 MB 的 MP4 视频;还原过程中视频会暂停,该设置按设备分别保存。在桌面端,计时界面上的模块可以自由拖放到侧栏、底部条带或任意空白处,并可拖动边缘调整大小。
Cuber Card
把个人资料汇集成一张图片的可分享卡片。头像、姓名、参赛次数、奖牌以及 WCA 纪录都会出现在卡片上。在移动端它接入原生分享菜单,在桌面端则直接下载。
参考项目
以下项目在 Zkt Timer 的开发过程中以不同方式充当了参考。每一条都说明两件事:从该项目中借鉴了什么,以及在此基础上构建了什么。

Zkt Timer 在软件架构方面最重要的参考。React + SSR、Node.js + GraphQL、PostgreSQL + Prisma 与 Redis 如何在一个魔方平台中协同工作,是通过研究 kash 的 cubedesk 项目梳理出来的。它在服务组织与目录结构上提供了方向。其影响并不限于服务端:cubedesk 确立的界面语言,也成为计时器界面布局、还原列表与模块排布的参考。我们分道扬镳之处在于:cubedesk 是一个网页应用。我希望同一套代码能通过 Capacitor 发布到 iOS 与 Android,因此重新构建了 SSR 层、会话管理以及跨设备同步,使其同时支撑原生端。

打乱生成的基础。各类魔方的打乱算法与子集结构、min2phase Kociemba 求解器以及群论库,都是参考 cs0x7f 的 csTimer 与 min2phase 构建的。在把这部分代码从 JavaScript 迁移到 TypeScript 的过程中,它被重构为基于注册表的生成器架构;现在每种魔方类型都会注册自己的生成器,客户端与服务器可以产生相同的打乱。在此基础上,还加入了 40 多个 CFOP、Roux、Mehta 与 ZZ 子集、我自己为 Square-1 和 Clock 编写的 2D 渲染器,以及一个让重型求解器在不卡界面的前提下运行的 Web Worker 层。

Trainer 模块用户体验方面的主要参考之一。智能魔方如何接入训练流程,是通过研究 Pau Oliva Fora 的项目梳理出来的。在 Zkt Timer 中,这一结构被拆分为四种独立模式(标准、智能魔方、识别、效率);系统为每条公式单独记录统计数据,队列顺序会根据用户较弱的情况自适应调整,并加入了 2D 与 3D 图案可视化以及用于打印公式表的 PDF 导出。

Trainer 中使用的公式集合有很大一部分来自 spencerchubb 创建的 cubingapp 数据集,并被适配到 Zkt Timer 的数据模型。其中的还原准备公式由我自己的脚本重新生成,图案图像经过我自己的渲染流程处理,整个集合被划分为 31 个类别,并用我自己补充的公式进行了扩展。

Cross、XCross、EOCross 以及 F2L 配对求解逻辑的起点。or18 开发的基于坐标的 IDA* 实现,是 Zkt Timer 求解器系统设计中的基础参考。这一模块中的工作是移植与适配:求解器被迁移到 Web Worker 架构,并接入了逐步求解界面。算法背后的数学属于 or18。

1v1 房间与友谊赛形式的概念基础来自这个项目。同步共享的还原模型如何构建,是通过研究 coder13 的作品梳理出来的。Zkt Timer 的房间系统是在这一思路之上从零编写的,并加入了我认为 LetsCube 中所缺少的部分:房间内的智能魔方支持、实时聊天、观战模式、房间管理与用户封禁、1v1 对决模式,以及通过 Redis 在多台服务器之间广播的 Socket.IO 层。

这是一个让选手在 WCA 比赛中查看自己分组与任务的项目,而且做得非常好。我不希望选手为了这些信息还要去另一个网站,因此把同样的体验搬进了 Zkt Timer。选手自己的日程、担任裁判与打乱员的任务、各项目的个人最佳成绩以及轮次成绩,现在都集中在同一个界面上。分组界面的布局正是在这个项目的影响下成形的。

一个为实时跟踪 WCA 比赛而打造的平台,它把逐轮成绩流这件事解决得非常出色。WCA Software Team 的工作,是 Zkt Timer 中 WCA Live 集成在结构方法上的直接来源。而 ZKT 比赛平台本身则是为 Zkt Timer 从零编写的:报名、成绩卡录入、批量录入模式、轮次管理、裁判与打乱员及跑者的任务分配、全国纪录跟踪、代表管理、A/B 分日比赛以及 WCIF 生成。从 wca-live 借鉴的是代表工作流程的逻辑。

首页上的交互式 3D 魔方是从 Dev-tanay 的这个项目一比一移植而来的。原始代码基于 Three.js r95 编写;迁移到 r125 时适配了发生变化的 API,并将代码转换为 TypeScript。类结构与方法命名与原始代码保持一致。我所增加的部分是把魔方接入 Zkt Timer 的计时器:打乱生成、还原检测与计时。这一条中代码的骨架属于 Dev-tanay。

PLL 识别训练模块的直接基础。键盘驱动的 PLL 识别练习、双面识别指南以及自适应重复逻辑均来自 Roman Strakhov 的项目。原始代码使用 Vue 编写;该模块被用 React 与 TypeScript 重写。在重写过程中,识别指南被补全至覆盖全部 73 种情况,并加入了设备端历史记录层、术语表视图以及一段 13 步的引导任务。如今该模块的体量约为原始项目的四倍。
使用的库
直接用于 Zkt Timer 基础设施的核心速拧库。

cubing.js — Lucas Garron & the cubing.js team 用于智能魔方状态跟踪、WCA 符号和 KPattern 匹配的核心库。由 Lucas Garron 和 cubing.js 贡献者开发。

提供 GAN 智能魔方的蓝牙连接管理和陀螺仪/运动逻辑的库。智能魔方模块构建在此库之上。

gan-web-bluetooth 的持续维护版本。我们 GAN、QiYi、MoYu、Giiker 和 GoCube 驱动中的许多修复(从一个蓝牙数据包读取多个 GAN 转动、数据包校验、陀螺仪支持、MAC 候选地址)都来自这个库,它的真实魔方录制数据也用于我们的测试。
开源
Zkt Timer 构建在数百个开源软件包之上,包括 React、Apollo、Prisma 和 Three.js。完整的依赖列表可以在 package.json 文件中找到。衷心感谢所有多年来构建和维护这些库的人。
致社区
致 WCA 和全球速拧社区。围绕一个单一的谜题建立了一个非凡的全球社区。Zkt Timer 有意义的真正原因在于它能够成为这个社区的一部分。
版权声明
本软件的部分内容引用了上述项目的代码或概念。每个项目保留其原始版权。Zkt Timer 自身的架构、系统和功能受以下版权保护:
本项目根据 GNU General Public License v3.0 发布。