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

相關(guān)新聞

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

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

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

2026/7/29 19:08:13 閱讀更多
中小型加工廠管理系統(tǒng)軟件開發(fā)

中小型加工廠管理系統(tǒng)軟件開發(fā)

中小型加工廠管理系統(tǒng)軟件開發(fā)的關(guān)鍵要點編輯:araolin(私域邦網(wǎng)絡(luò)土土哥)開發(fā)中小型加工廠管理系統(tǒng)需要結(jié)合生產(chǎn)流程、庫存管理、訂單跟蹤等核心需求。以下為關(guān)鍵開發(fā)方向:系統(tǒng)功能模塊設(shè)計生產(chǎn)管理模塊實現(xiàn)工單創(chuàng)建、任務(wù)分配、進(jìn)…

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

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

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

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

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

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

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