撤销为什么只需要 pop:用事件重放实现离线台球计分
计分板最常见的 bug不是按钮没响应而是比分、开球方、让局和撤销各维护一份状态某个分支忘记同步其中一项。台球工具选择只记录“发生了什么”把“现在是什么状态”全部重算出来。一、先把事实和结果分开以中式八球抢局制为例页面需要展示两名选手当前比分本局开球方是否已经达到抢局目标炸清、接清等特殊胜局次数撤销上一局后的所有状态。直觉实现会在点击“选手 A 获胜”时同时执行scoreA、切换开球方、更新特殊次数并判断获胜。这样做的问题是撤销时必须精确执行所有反操作新增一种开球规则又会影响多个分支。这个思路最初用于「零碎百宝箱」的台球计分功能目标是在球房断网时仍能完整记下一场比赛。项目只保存不可再分的事实frames:[{winner:A,type:normal,at:1720000000000},{winner:B,type:clear,at:1720000060000}]比分、胜者和开球方都是派生值functionscoreOf(match,side){consthandicapsideA?match.handicapA:match.handicapB;returnhandicapmatch.frames.filter(ff.winnerside).length;}functionmatchWinner(match){if(scoreOf(match,A)match.raceTo)returnA;if(scoreOf(match,B)match.raceTo)returnB;returnnull;}撤销上一局因此只需frames.pop()随后所有派生函数自然得到上一时刻的状态。二、事件重放消除了“双重真相”事件模型的核心并不是用了数组而是规定数组是唯一事实来源。如果同时持久化frames和scoreA二者迟早会不一致。frames 事件序列比分当前开球方比赛胜者特殊胜局统计结算详情开球方也从历史推导轮流开球已完成局数的奇偶决定当前开球方胜方开球取上一局赢家负方开球取上一局赢家的另一方。这些函数可以独立测试。页面组件不需要知道各种规则怎样组合只负责渲染计算结果和追加事件。三、两套玩法共用一个思想台球工具同时支持中式八球抢局和九球追分。追分不是简单的“多人比分加减”它有普胜、银九、大金、小金、犯规等事件不同事件决定谁向谁转移多少分还会影响下一局出杆顺序。追分对局保存{players:[A,B,C],initialScore:100,values:{normal:5,silver:10,golden:20},firstOrder:[0,1,2],events:[{type:normal,winner:0,payer:1,at:1720000000000},{type:golden,winner:2,payer:null,at:1720000060000}]}payernull表示其余所有人都向赢家支付。重放函数从每人的初始分开始逐条转移for(consteventofmatch.events){constvaluematch.values[event.type];if(event.payernull){for(leti0;iplayers.length;i){if(i!event.winner){scores[i]-value;scores[event.winner]value;}}}else{scores[event.winner]value;scores[event.payer]-value;}}这段算法天然维持一个不变量sum(scores) initialScore * playerCount如果任何操作后总分变化就说明转移规则实现有误。把业务规则转化为可断言的不变量是提高计分类软件可靠性的有效方法。四、出杆顺序也可以重放追分模式的当前顺序并不单纯按局数轮换。赢家应排到首位单一输家排第二其余人保持上一局相对顺序大金没有单一输家则只把赢家提到首位。犯规属于局中处罚不改变下一局顺序。实现用事件类型集合区分哪些事件会结束一局再从firstOrder依次重放constORDER_EVENTSnewSet([normal,silver,gold9,golden]);for(consteventofevents){if(!ORDER_EVENTS.has(event.type))continue;constrestorder.filter(ii!event.winneri!event.payer);orderevent.payernull?[event.winner,...order.filter(ii!event.winner)]:[event.winner,event.payer,...rest];}分数和顺序都来自同一串事件因此撤销一个犯规只恢复分数不会错误改变顺序撤销一局胜负则两者一起回到正确状态。五、兼容旧存档要看数据形状软件升级后同一个玩法代码可能改变计分模型。项目曾经存在九球抢局存档后来九球改为追分。如果只根据mode nine-ball判断旧存档会被错误送进需要players/events的追分页面。实现改为按数据形状判断exportfunctionisChase(match){return!!(matchArray.isArray(match.players));}这是一种实用的兼容策略当持久数据没有明确 schema version 时使用不会歧义的结构特征识别旧格式。更长期的方案是在新数据中加入schemaVersion并编写显式迁移但在已有用户存档无法补字段时形状检测仍是必要兜底。六、本地是主存储云端只是备份球房网络不稳定时点击记分必须立即生效。当前对局、设置和最近 100 条历史都先写入带billiards:命名空间的本地 storage。比赛结束后再静默尝试上传。每场比赛创建一个客户端幂等键m${Date.now().toString(36)}${Math.random().toString(36).slice(2,8)}服务端按 openid 和clientKey去重所以网络重试不会制造重复比赛。同步流程为处理此前离线删除留下的待删队列拉取云端记录与本地按clientKey合并推送所有未同步且已经完赛的本地记录。删除也不能只操作本地。已同步记录删除后会把clientKey放进pendingDeletes下次联网时补删云端在删除完成前拉取逻辑会跳过同 key 的远端记录防止它“复活”。这种设计明确牺牲了多人同时编辑一场比赛的能力换取球桌上最重要的体验任何时候都能记分杀进程后也能恢复当前对局。七、结算数据为何可以适度冗余进行中状态坚持事件唯一来源但历史列表需要快速展示最终比分。完赛入库时抢局模式会附加scoreA、scoreB和winner追分模式附加finalScores。这不是重新引入双重真相因为它们是事件序列在“完赛时刻”的不可变快照不再参与后续计分。历史卡片可以直接读取详情页仍能用事件复核。判断是否可以冗余的标准是源数据是否保留快照是否只在明确的生命周期节点生成快照生成后是否还会被独立修改。满足这三点适度反规范化能简化查询否则就会变成两个可写字段互相打架。八、事件溯源不等于必须上复杂基础设施这里没有消息队列、事件总线或专用事件数据库只有普通 JavaScript 数组和 JSON 持久化但已经获得了事件建模的核心收益状态计算是确定性的撤销成本低新统计可以从旧事件补算业务不变量容易测试云端可以保存完整过程而非只有最终分数。它也有边界。事件无限增长会让每次重放变慢复杂业务还要处理事件版本迁移。台球一场比赛的事件数量有限直接从头重放最简单若扩展到数万事件的长期系统再引入周期快照。九、可复用的设计判断计分工具适合事件重放通常因为它同时满足三点操作可以表达为离散事件当前状态能由事件确定计算用户有频繁撤销需求。实现时应守住以下约束只持久化事件和必要初始条件不持久化可变派生状态把所有规则写成无副作用的推导函数为零和、局数和顺序等业务不变量编写测试明确事件 schema升级时识别或迁移旧存档把离线删除也建模为需要补偿的同步动作。当撤销需要写一大串反向逻辑时通常不是撤销太复杂而是状态保存得太多了。

相关新闻

WordPress过时功能检测:如何识别和替换废弃类与方法

WordPress过时功能检测:如何识别和替换废弃类与方法

WordPress过时功能检测:如何识别和替换废弃类与方法 【免费下载链接】WordPress-Coding-Standards PHP_CodeSniffer rules (sniffs) to enforce WordPress coding conventions 项目地址: https://gitcode.com/GitHub_Trending/wo/WordPress-Coding-Standards …

2026/10/8 13:35:37 阅读更多 →
Input Leap跨设备控制解决方案:告别多套键盘鼠标的烦恼

Input Leap跨设备控制解决方案:告别多套键盘鼠标的烦恼

Input Leap跨设备控制解决方案:告别多套键盘鼠标的烦恼 【免费下载链接】input-leap Open-source KVM software 项目地址: https://gitcode.com/gh_mirrors/in/input-leap 你是否曾经在办公桌上同时使用Windows台式机、MacBook和Linux工作站,却不…

2026/10/8 3:17:21 阅读更多 →
ClosedXML完整指南:快速掌握.NET Excel操作终极教程

ClosedXML完整指南:快速掌握.NET Excel操作终极教程

ClosedXML完整指南:快速掌握.NET Excel操作终极教程 【免费下载链接】ClosedXML ClosedXML is a .NET library for reading, manipulating and writing Excel 2007 (.xlsx, .xlsm) files. It aims to provide an intuitive and user-friendly interface to dealing …

2026/10/5 14:52:30 阅读更多 →

最新新闻

Eclipse Mars绿色版使用指南:老IDE的现状与配置实战

Eclipse Mars绿色版使用指南:老IDE的现状与配置实战

简介:Eclipse-jee-mars-R-win32-x86_64位官方绿色版.zip 是一份面向 Windows 64 位系统的 Eclipse JEE 官方正式版压缩包,定位 Java 企业级与 Web 开发,适合需要快速搭建集成开发环境的 Java 工程师、教学演示及团队协作场景。它基于 2015 年…

2026/10/13 13:09:21 阅读更多 →
钱不是隔离武器也不是自由选择,而是兑换选择权的工具

钱不是隔离武器也不是自由选择,而是兑换选择权的工具

这标题一看就带着网味,但里面藏的问题,其实很实在。我在这一行干了十几年,见过太多人把钱当成一种情绪出口,要么恨它,要么膜拜它,很少有人愿意坐下来把它当一件普通工具拆开研究。今天借这个标题里的两个关…

2026/10/13 13:09:21 阅读更多 →
2026年程序员学习避坑指南:哪些技术别学,精力该放哪

2026年程序员学习避坑指南:哪些技术别学,精力该放哪

2026 年程序员还能学点什么?这个问题的另一面,恰恰是“哪些技术真的不要再花时间去学了”。我这两年被问过太多次类似的问题,很多人在学习清单上拼命做加法,最后发现投入和回报完全不成正比。尤其到了 2026 年,AI 已经…

2026/10/13 13:09:21 阅读更多 →
PyCharm配置Python环境避坑指南:解释器、虚拟环境与依赖管理

PyCharm配置Python环境避坑指南:解释器、虚拟环境与依赖管理

简介:围绕PyCharm配置Python环境的图文操作文档,面向刚开始使用PyCharm或需要理顺解释器、虚拟环境与依赖库管理的开发者。文档系统梳理了从安装Python时勾选PATH,到新建项目时选择解释器的完整思路,详细区分了Virtualenv/Conda创…

2026/10/13 13:09:21 阅读更多 →
mmap高级用法:零拷贝、共享内存与虚拟内存机制实战精解

mmap高级用法:零拷贝、共享内存与虚拟内存机制实战精解

说到内存映射文件,很多人的印象还停留在“mmap 就是让读写文件更快”这个层面。真正把 mmap 玩明白的人,往往会用它做一些看起来不大常规的事,比如拿它当进程间通信通道、让大文件做到秒开级别的随机访问、甚至把一个可变长结构体的存储和加载…

2026/10/13 13:09:21 阅读更多 →
机械臂运动规划实战:MoveIt!从入门到真机避障

机械臂运动规划实战:MoveIt!从入门到真机避障

搞机器人开发的人,大约都有过这么一段:URDF 建好了,模型在 RViz 里能拖能转,但真想让机械臂末端按指令走到某个位姿,才发现事情没那么简单。要算逆解、要躲开周围障碍物、要考虑关节限位和奇异点,还得把算出…

2026/10/13 13:08:20 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →