撤銷為什么只需要 pop:用事件重放實(shí)現(xiàn)離線臺(tái)球計(jì)分
計(jì)分板最常見(jiàn)的 bug不是按鈕沒(méi)響應(yīng)而是比分、開(kāi)球方、讓局和撤銷各維護(hù)一份狀態(tài)某個(gè)分支忘記同步其中一項(xiàng)。臺(tái)球工具選擇只記錄“發(fā)生了什么”把“現(xiàn)在是什么狀態(tài)”全部重算出來(lái)。一、先把事實(shí)和結(jié)果分開(kāi)以中式八球搶局制為例頁(yè)面需要展示兩名選手當(dāng)前比分本局開(kāi)球方是否已經(jīng)達(dá)到搶局目標(biāo)炸清、接清等特殊勝局次數(shù)撤銷上一局后的所有狀態(tài)。直覺(jué)實(shí)現(xiàn)會(huì)在點(diǎn)擊“選手 A 獲勝”時(shí)同時(shí)執(zhí)行scoreA、切換開(kāi)球方、更新特殊次數(shù)并判斷獲勝。這樣做的問(wèn)題是撤銷時(shí)必須精確執(zhí)行所有反操作新增一種開(kāi)球規(guī)則又會(huì)影響多個(gè)分支。這個(gè)思路最初用于「零碎百寶箱」的臺(tái)球計(jì)分功能目標(biāo)是在球房斷網(wǎng)時(shí)仍能完整記下一場(chǎng)比賽。項(xiàng)目只保存不可再分的事實(shí)frames:[{winner:A,type:normal,at:1720000000000},{winner:B,type:clear,at:1720000060000}]比分、勝者和開(kāi)球方都是派生值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()隨后所有派生函數(shù)自然得到上一時(shí)刻的狀態(tài)。二、事件重放消除了“雙重真相”事件模型的核心并不是用了數(shù)組而是規(guī)定數(shù)組是唯一事實(shí)來(lái)源。如果同時(shí)持久化frames和scoreA二者遲早會(huì)不一致。frames 事件序列比分當(dāng)前開(kāi)球方比賽勝者特殊勝局統(tǒng)計(jì)結(jié)算詳情開(kāi)球方也從歷史推導(dǎo)輪流開(kāi)球已完成局?jǐn)?shù)的奇偶決定當(dāng)前開(kāi)球方勝方開(kāi)球取上一局贏家負(fù)方開(kāi)球取上一局贏家的另一方。這些函數(shù)可以獨(dú)立測(cè)試。頁(yè)面組件不需要知道各種規(guī)則怎樣組合只負(fù)責(zé)渲染計(jì)算結(jié)果和追加事件。三、兩套玩法共用一個(gè)思想臺(tái)球工具同時(shí)支持中式八球搶局和九球追分。追分不是簡(jiǎn)單的“多人比分加減”它有普勝、銀九、大金、小金、犯規(guī)等事件不同事件決定誰(shuí)向誰(shuí)轉(zhuǎn)移多少分還會(huì)影響下一局出桿順序。追分對(duì)局保存{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表示其余所有人都向贏家支付。重放函數(shù)從每人的初始分開(kāi)始逐條轉(zhuǎn)移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;}}這段算法天然維持一個(gè)不變量sum(scores) initialScore * playerCount如果任何操作后總分變化就說(shuō)明轉(zhuǎn)移規(guī)則實(shí)現(xiàn)有誤。把業(yè)務(wù)規(guī)則轉(zhuǎn)化為可斷言的不變量是提高計(jì)分類軟件可靠性的有效方法。四、出桿順序也可以重放追分模式的當(dāng)前順序并不單純按局?jǐn)?shù)輪換。贏家應(yīng)排到首位單一輸家排第二其余人保持上一局相對(duì)順序大金沒(méi)有單一輸家則只把贏家提到首位。犯規(guī)屬于局中處罰不改變下一局順序。實(shí)現(xiàn)用事件類型集合區(qū)分哪些事件會(huì)結(jié)束一局再?gòu)膄irstOrder依次重放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];}分?jǐn)?shù)和順序都來(lái)自同一串事件因此撤銷一個(gè)犯規(guī)只恢復(fù)分?jǐn)?shù)不會(huì)錯(cuò)誤改變順序撤銷一局勝負(fù)則兩者一起回到正確狀態(tài)。五、兼容舊存檔要看數(shù)據(jù)形狀軟件升級(jí)后同一個(gè)玩法代碼可能改變計(jì)分模型。項(xiàng)目曾經(jīng)存在九球搶局存檔后來(lái)九球改為追分。如果只根據(jù)mode nine-ball判斷舊存檔會(huì)被錯(cuò)誤送進(jìn)需要players/events的追分頁(yè)面。實(shí)現(xiàn)改為按數(shù)據(jù)形狀判斷exportfunctionisChase(match){return!!(matchArray.isArray(match.players));}這是一種實(shí)用的兼容策略當(dāng)持久數(shù)據(jù)沒(méi)有明確 schema version 時(shí)使用不會(huì)歧義的結(jié)構(gòu)特征識(shí)別舊格式。更長(zhǎng)期的方案是在新數(shù)據(jù)中加入schemaVersion并編寫顯式遷移但在已有用戶存檔無(wú)法補(bǔ)字段時(shí)形狀檢測(cè)仍是必要兜底。六、本地是主存儲(chǔ)云端只是備份球房網(wǎng)絡(luò)不穩(wěn)定時(shí)點(diǎn)擊記分必須立即生效。當(dāng)前對(duì)局、設(shè)置和最近 100 條歷史都先寫入帶billiards:命名空間的本地 storage。比賽結(jié)束后再靜默嘗試上傳。每場(chǎng)比賽創(chuàng)建一個(gè)客戶端冪等鍵m${Date.now().toString(36)}${Math.random().toString(36).slice(2,8)}服務(wù)端按 openid 和clientKey去重所以網(wǎng)絡(luò)重試不會(huì)制造重復(fù)比賽。同步流程為處理此前離線刪除留下的待刪隊(duì)列拉取云端記錄與本地按clientKey合并推送所有未同步且已經(jīng)完賽的本地記錄。刪除也不能只操作本地。已同步記錄刪除后會(huì)把clientKey放進(jìn)pendingDeletes下次聯(lián)網(wǎng)時(shí)補(bǔ)刪云端在刪除完成前拉取邏輯會(huì)跳過(guò)同 key 的遠(yuǎn)端記錄防止它“復(fù)活”。這種設(shè)計(jì)明確犧牲了多人同時(shí)編輯一場(chǎng)比賽的能力換取球桌上最重要的體驗(yàn)任何時(shí)候都能記分殺進(jìn)程后也能恢復(fù)當(dāng)前對(duì)局。七、結(jié)算數(shù)據(jù)為何可以適度冗余進(jìn)行中狀態(tài)堅(jiān)持事件唯一來(lái)源但歷史列表需要快速展示最終比分。完賽入庫(kù)時(shí)搶局模式會(huì)附加scoreA、scoreB和winner追分模式附加finalScores。這不是重新引入雙重真相因?yàn)樗鼈兪鞘录蛄性凇巴曩悤r(shí)刻”的不可變快照不再參與后續(xù)計(jì)分。歷史卡片可以直接讀取詳情頁(yè)仍能用事件復(fù)核。判斷是否可以冗余的標(biāo)準(zhǔn)是源數(shù)據(jù)是否保留快照是否只在明確的生命周期節(jié)點(diǎn)生成快照生成后是否還會(huì)被獨(dú)立修改。滿足這三點(diǎn)適度反規(guī)范化能簡(jiǎn)化查詢否則就會(huì)變成兩個(gè)可寫字段互相打架。八、事件溯源不等于必須上復(fù)雜基礎(chǔ)設(shè)施這里沒(méi)有消息隊(duì)列、事件總線或?qū)S檬录?shù)據(jù)庫(kù)只有普通 JavaScript 數(shù)組和 JSON 持久化但已經(jīng)獲得了事件建模的核心收益狀態(tài)計(jì)算是確定性的撤銷成本低新統(tǒng)計(jì)可以從舊事件補(bǔ)算業(yè)務(wù)不變量容易測(cè)試云端可以保存完整過(guò)程而非只有最終分?jǐn)?shù)。它也有邊界。事件無(wú)限增長(zhǎng)會(huì)讓每次重放變慢復(fù)雜業(yè)務(wù)還要處理事件版本遷移。臺(tái)球一場(chǎng)比賽的事件數(shù)量有限直接從頭重放最簡(jiǎn)單若擴(kuò)展到數(shù)萬(wàn)事件的長(zhǎng)期系統(tǒng)再引入周期快照。九、可復(fù)用的設(shè)計(jì)判斷計(jì)分工具適合事件重放通常因?yàn)樗瑫r(shí)滿足三點(diǎn)操作可以表達(dá)為離散事件當(dāng)前狀態(tài)能由事件確定計(jì)算用戶有頻繁撤銷需求。實(shí)現(xiàn)時(shí)應(yīng)守住以下約束只持久化事件和必要初始條件不持久化可變派生狀態(tài)把所有規(guī)則寫成無(wú)副作用的推導(dǎo)函數(shù)為零和、局?jǐn)?shù)和順序等業(yè)務(wù)不變量編寫測(cè)試明確事件 schema升級(jí)時(shí)識(shí)別或遷移舊存檔把離線刪除也建模為需要補(bǔ)償?shù)耐絼?dòng)作。當(dāng)撤銷需要寫一大串反向邏輯時(shí)通常不是撤銷太復(fù)雜而是狀態(tài)保存得太多了。

相關(guān)新聞

RaZ引擎資源管理最佳實(shí)踐:模型、紋理與動(dòng)畫加載優(yōu)化策略

RaZ引擎資源管理最佳實(shí)踐:模型、紋理與動(dòng)畫加載優(yōu)化策略

RaZ引擎資源管理最佳實(shí)踐:模型、紋理與動(dòng)畫加載優(yōu)化策略 【免費(fèi)下載鏈接】RaZ Modern & multiplatform 3D game engine 項(xiàng)目地址: https://gitcode.com/gh_mirrors/ra/RaZ RaZ是一款現(xiàn)代化跨平臺(tái)3D游戲引擎,提供高效的資源管理系統(tǒng)幫助開(kāi)發(fā)者…

2026/7/29 19:18:14 閱讀更多
6款A(yù)I論文網(wǎng)站盤點(diǎn)

6款A(yù)I論文網(wǎng)站盤點(diǎn)

真正的學(xué)術(shù) AI,從不替你代筆,而是做你的選題軍師、文獻(xiàn)管家、邏輯教練、潤(rùn)色專家。從中文畢業(yè)論文到英文期刊發(fā)表,從框架搭建到降重合規(guī),這 6 款工具覆蓋全場(chǎng)景,幫你用最低時(shí)間成本,寫出高質(zhì)量、高原創(chuàng)、高…

2026/7/29 19:08:13 閱讀更多
貓抓插件終極指南:三步免費(fèi)下載網(wǎng)頁(yè)視頻音頻,開(kāi)啟資源獲取新體驗(yàn)!

貓抓插件終極指南:三步免費(fèi)下載網(wǎng)頁(yè)視頻音頻,開(kāi)啟資源獲取新體驗(yàn)!

貓抓插件終極指南:三步免費(fèi)下載網(wǎng)頁(yè)視頻音頻,開(kāi)啟資源獲取新體驗(yàn)! 【免費(fèi)下載鏈接】cat-catch 貓抓 瀏覽器資源嗅探擴(kuò)展 / cat-catch Browser Resource Sniffing Extension 項(xiàng)目地址: https://gitcode.com/GitHub_Trending/ca/cat-catch …

2026/7/29 20:08:44 閱讀更多
taskt:零代碼桌面自動(dòng)化工具完全指南

taskt:零代碼桌面自動(dòng)化工具完全指南

taskt:零代碼桌面自動(dòng)化工具完全指南 【免費(fèi)下載鏈接】taskt taskt (pronounced tasked and formely sharpRPA) is free and open-source robotic process automation (rpa) built in C# powered by the .NET Framework 項(xiàng)目地址: https://gitcode.com/gh_mirrors…

2026/7/29 20:08:44 閱讀更多
3步打造智能媒體庫(kù):MetaTube插件完全指南

3步打造智能媒體庫(kù):MetaTube插件完全指南

3步打造智能媒體庫(kù):MetaTube插件完全指南 【免費(fèi)下載鏈接】jellyfin-plugin-metatube MetaTube Plugin for Jellyfin/Emby 項(xiàng)目地址: https://gitcode.com/gh_mirrors/je/jellyfin-plugin-metatube MetaTube插件是專為Jellyfin和Emby媒體服務(wù)器設(shè)計(jì)的智能元數(shù)…

2026/7/29 20:08:44 閱讀更多
面試官大笑:“一個(gè)任務(wù)拆給 5 個(gè) Subagent 并行跑,不比 1 個(gè)快 5 倍?“我搖頭:“快不了,還可能更慢“

面試官大笑:“一個(gè)任務(wù)拆給 5 個(gè) Subagent 并行跑,不比 1 個(gè)快 5 倍?“我搖頭:“快不了,還可能更慢“

前兩個(gè)月,我在重構(gòu) AlgoMooc 網(wǎng)站過(guò)程中,發(fā)現(xiàn)一個(gè)問(wèn)題:在 Claude Code 里把一個(gè)任務(wù)拆給 5 個(gè) Subagent 并行跑,結(jié)果可能比 1 個(gè) agent 從頭干到尾還慢? 大多數(shù)人的第一反應(yīng)是反過(guò)來(lái)的:活是并行干的&#…

2026/7/29 0:15:24 閱讀更多
# 鴻蒙 HarmonyOS 應(yīng)用開(kāi)發(fā)實(shí)戰(zhàn)(第25期)|骰子(Dice Roller)— Unicode 符號(hào)與動(dòng)畫渲染精講

# 鴻蒙 HarmonyOS 應(yīng)用開(kāi)發(fā)實(shí)戰(zhàn)(第25期)|骰子(Dice Roller)— Unicode 符號(hào)與動(dòng)畫渲染精講

一、應(yīng)用概述 骰子(Dice Roller) 是一款經(jīng)典的休閑娛樂(lè)應(yīng)用,模擬了真實(shí)擲骰子的過(guò)程。應(yīng)用投擲兩個(gè)骰子(六面標(biāo)準(zhǔn)骰),使用 Unicode 骰面符號(hào)直觀展示每個(gè)骰子的點(diǎn)數(shù),并伴有快速滾動(dòng)的動(dòng)畫效果?!?/p>

2026/7/29 0:15:24 閱讀更多