收規(guī)則)
上午十點(diǎn)新接手的任務(wù)單里躺著一行字abd 3deathq3*1 dc*2。沒有設(shè)計(jì)文檔沒有驗(yàn)收說明上一任開發(fā)只留下一句“之前的需求就是這么寫的”。我盯著這串由前綴、數(shù)字、星號和括號組成的內(nèi)容看了很久。abd到底是指哪個(gè)場景還是某個(gè)系統(tǒng)的縮寫3death是允許玩家死亡三次還是要求觸發(fā)三次死亡q3*1是任務(wù)編號 q3 完成一次還是第 3 季度內(nèi)的 1 個(gè)指標(biāo)dc*2又到底是通關(guān)兩次、參與兩次還是數(shù)據(jù)采集兩次這類暗號式需求在一些快速迭代的項(xiàng)目里并不罕見。它看起來像一行配置實(shí)際卻是一個(gè)團(tuán)隊(duì)把一個(gè)需要多方確認(rèn)的驗(yàn)收規(guī)則壓縮成了只有少數(shù)人能讀懂的“壓縮包”。而真正危險(xiǎn)的并不是這行字本身是壓縮包丟失了上下文之后接手的開發(fā)只能靠猜。1. 一行暗號并不是需求它只是需求的“壓縮包”先把一個(gè)觀點(diǎn)放前面拿到這種字符串時(shí)最重要的不是去猜它到底代表什么而是把猜測變成一份可以被對方確認(rèn)的清單。猜得對那只是運(yùn)氣好猜錯了后面所有實(shí)現(xiàn)、測試、驗(yàn)收都會跟著錯而且錯得很隱蔽。1.1 先把符號拆開逐項(xiàng)列出可能含義我習(xí)慣先按分隔符把整串拆開。這里的顯式結(jié)構(gòu)是abd—— 一個(gè)沒有花括號、沒有下劃線的前綴詞3death—— 數(shù)字開頭后面跟英文單詞q3*1—— 短編號加乘號加次數(shù)dc*2—— 兩個(gè)字母縮寫加乘號加次數(shù)外層括號把q3*1 dc*2圈在了一起。如果這是游戲服務(wù)器配置里的日常任務(wù)、挑戰(zhàn)副本或成就條件的寫法那么名稱*次數(shù)在不少項(xiàng)目里確實(shí)表示“目標(biāo)完成 N 次”。q3可能是任務(wù) IDdc可能是某個(gè)副本的通關(guān)縮寫。括號則表示這兩個(gè)目標(biāo)是一個(gè)組合關(guān)系。但這只能作為“待確認(rèn)假設(shè)”不能作為事實(shí)。同樣一帶3death在 A 團(tuán)隊(duì)里可能是“累計(jì)死亡達(dá)到 3 次后判定失敗”在 B 團(tuán)隊(duì)里可能是“最多允許 3 次死亡超過則挑戰(zhàn)失敗”在 C 團(tuán)隊(duì)里甚至可能是“玩家死亡 3 次后觸發(fā)一個(gè)特殊增益狀態(tài)”。三種理解對程序邏輯的要求完全不同。片段常見可能含義對邏輯的影響abd副本、活動、任務(wù)組代號決定觸發(fā)場景和校驗(yàn)范圍3death死亡次數(shù)限制 / 達(dá)成條件 / 狀態(tài)觸發(fā)器決定它是“紅線”還是“進(jìn)度目標(biāo)”q3*1任務(wù) q3 完成 1 次計(jì)數(shù)型目標(biāo)可能需要事件驅(qū)動dc*2通關(guān)或參與 dc 共 2 次計(jì)數(shù)型目標(biāo)需要確認(rèn)是否要求通關(guān)成功括號組合條件區(qū)域決定目標(biāo)是并列還是嵌套關(guān)系遇到類似的字符串我會先把這張表寫出來隨需求單一起發(fā)給真正知道業(yè)務(wù)的人去確認(rèn)。這樣做的價(jià)值在于別人可以看到“我理解成了什么哪些我拿不準(zhǔn)”而不是等我做完了才說“不對3death 是限制不能超過三次”。1.2 先確認(rèn)語義再碰代碼很多人拿到這種需求會直接開始寫代碼因?yàn)樽址旧砗芟褚欢慰山馕龅呐渲谩5馕霾皇亲铍y的解析后的語義映射才是。舉個(gè)最典型的例子3death是條件還是限制如果是“允許死亡三次”的挑戰(zhàn)模式那么玩家死亡三次是不會失敗的反而可能要在三次死亡內(nèi)完成q3一次和dc兩次如果是“死亡超過三次挑戰(zhàn)失敗”那它就是一條負(fù)向的失敗規(guī)則不參與進(jìn)度累計(jì)只在事件發(fā)生時(shí)做判斷。這兩種寫法的代碼結(jié)構(gòu)、測試用例、埋點(diǎn)日志全部不一樣。沒有確認(rèn)語義之前代碼寫得越快返工成本越高。所以我在這個(gè)階段通常會做三件事把整條字符串拆成 token 列表寫上自己的猜測。每一個(gè) token 至少列出兩種可能含義特別標(biāo)出彼此矛盾的地方。把假設(shè)清單發(fā)給需求方并請對方做一次“逐行打勾”的確認(rèn)而不是問“這個(gè)需求是什么意思”這種開放式問題。只問開放問題對方大概率會回一句“你自己看下舊配置”。把假設(shè)列出來對方只需要回復(fù)“對、不對、改成什么”溝通效率完全不一樣。2. 拆完暗號之后把它回寫成一張驗(yàn)收規(guī)格假設(shè)我們假設(shè)性確認(rèn)了一版語義abd是某個(gè)挑戰(zhàn)場景的代號場景內(nèi)有一個(gè)挑戰(zhàn)任務(wù)允許的失敗次數(shù)和死亡相關(guān)玩家需要在挑戰(zhàn)期間完成q3一次、dc兩次。那么下一步不是建表而是把這句話回寫成可驗(yàn)收的規(guī)則表。2.1 一句話假設(shè)和一張規(guī)則表我常用一個(gè)固定句式來濃縮需求在什么場景下玩家做什么事做多少次中間什么情況會導(dǎo)致失敗最后怎么結(jié)算。套到演示例子里可以寫成在 abd 挑戰(zhàn)開放期間玩家需要在死亡次數(shù)不超過 3 次的前提下完成 q3 任務(wù) 1 次并且通關(guān) dc 副本 2 次全部滿足則挑戰(zhàn)成功。這只是一個(gè)演示假設(shè)不代表原串的真實(shí)含義但它已經(jīng)足夠支撐我們設(shè)計(jì)一張規(guī)格表項(xiàng)目規(guī)則觸發(fā)范圍abd對應(yīng)場景激活后開始記錄目標(biāo) A完成q3任務(wù)次數(shù) ≥ 1目標(biāo) B通關(guān)dc次數(shù) ≥ 2失敗條件挑戰(zhàn)期間累計(jì)死亡次數(shù) ≥ 3目標(biāo)關(guān)系A(chǔ) 與 B 同時(shí)達(dá)成才算完成順序要求默認(rèn)不做順序要求待確認(rèn)死亡是否重置進(jìn)度待確認(rèn)這張表不一定正確但它是一個(gè)“完成了思考過程”的產(chǎn)物。它把字符串里被壓掉的信息展開成了每個(gè)字段任何后續(xù)參與的人都能看懂并且指出問題。2.2 真正要對齊的是“邊界條件”規(guī)格表里最容易漏的不是正常路徑而是邊界條件。正常路徑只要照著規(guī)則寫就行邊界條件才會暴露實(shí)現(xiàn)差異。圍繞演示規(guī)則至少要追問這些問題玩家先完成了q3一次再進(jìn)挑戰(zhàn)算不算數(shù)如果死亡三次立刻失敗失敗之后已經(jīng)完成的q3和dc進(jìn)度還保留嗎dc中途退出算不算一次通關(guān)組隊(duì)時(shí)隊(duì)員完成算不算隊(duì)長的進(jìn)度q3和dc如果指向同一場戰(zhàn)斗同一個(gè)事件到達(dá)計(jì)數(shù)層時(shí)是各自加一還是會被去重挑戰(zhàn)到期或被重置時(shí)進(jìn)度是否清零有沒有發(fā)現(xiàn)這串暗號里根本不含這些信息。這些都需要靠另外的文檔、經(jīng)驗(yàn)或在線問答去對齊。對齊之后才能真正進(jìn)入編碼階段。這也是為什么我會說一行這樣的字符串不是一個(gè)可交付的需求它只是一條引子。3. 別把規(guī)則寫死在流程里優(yōu)先做成配置就算一次只接一個(gè)任務(wù)也不要在一堆業(yè)務(wù)方法里硬編碼條件判斷。原因不是“這樣更優(yōu)雅”而是這類玩法需求通常不會只改一次。今天3death明天可能變成1death今天要求q3一次下個(gè)版本可能還要加一個(gè)q4*1。我見過大量線上改版事故本質(zhì)都不是算法多難而是把條件寫死在龐大的邏輯分支里改一個(gè)數(shù)字要動一大片代碼最后沒人敢動。3.1 一個(gè)最小配置結(jié)構(gòu)如果讓我為演示規(guī)則設(shè)計(jì)一個(gè)最小配置結(jié)構(gòu)會更偏事件驅(qū)動和條件校驗(yàn)分離的方式。下面這段 JSON 只是示例結(jié)構(gòu)不代表對原始暗號的官方解釋{ challengeId: abd_challenge_001, name: abd_3death_limit, zone: abd, deathLimit: 3, goals: [ { type: quest_complete, targetId: q3, requiredCount: 1 }, { type: dungeon_clear, targetId: dc, requiredCount: 2 } ], conditionMode: AND, progressResetOnDeath: false, progressResetOnFail: false }字段含義不復(fù)雜challengeId唯一標(biāo)識。zone只在abd場景內(nèi)統(tǒng)計(jì)。deathLimit死亡上限超過即失敗。goals一組完成目標(biāo)。conditionMode目標(biāo)之間是 AND 還是 OR。progressResetOnDeath死亡后是否清空已完成的進(jìn)度。progressResetOnFail失敗后是否允許重新挑戰(zhàn)并重新累計(jì)。其中兩個(gè) reset 字段非常關(guān)鍵。很多線上 bug 都出在這里策劃以為失敗后進(jìn)度重置開發(fā)卻默認(rèn)玩家退出重進(jìn)后會保留進(jìn)度兩邊各做各的。3.2 配置化不是銀彈需要配套校驗(yàn)但要注意把規(guī)則搬進(jìn)配置表不等于規(guī)則一定正確。配置化的價(jià)值有兩個(gè)前提一是語義已經(jīng)對齊二是配置有校驗(yàn)和日志。如果語義沒對齊配置只是讓錯誤規(guī)則更容易批量發(fā)布而已。所以我建議至少做三層校驗(yàn)靜態(tài)校驗(yàn)配置格式、字段類型、目標(biāo) ID 是否存在、requiredCount 是否大于 0。運(yùn)行時(shí)校驗(yàn)事件類型是否能被目標(biāo)正確消費(fèi)未知事件是否要告警??捎^測性校驗(yàn)每次達(dá)成、重置、失敗都有日志便于事后回溯。配置化真正解決的是“不斷變化的需求能不能低成本地安全落地”而不是“需求本身有沒有理解對”。這兩件事不能混為一談。提醒一下別上來就把配置平臺做得很大。先用一個(gè) JSON 或一張表支撐本次需求等出現(xiàn)第二個(gè)同類需求時(shí)再抽象公共字段因?yàn)檫^早抽象只會讓配置變得比硬編碼代碼更難讀。4. 判定邏輯最容易出錯的三個(gè)位置配置結(jié)構(gòu)定了流程代碼反而好寫。真正讓開發(fā)頭疼的往往集中在下面三個(gè)位置。每一個(gè)都能讓一個(gè)看似簡單的挑戰(zhàn)任務(wù)變成線上事故。4.1 死亡限制是“紅線”還是“目標(biāo)”這是最容易踩的坑。如果3death是失敗線那么它不會帶來任何“目標(biāo)達(dá)成”的正面反饋它只負(fù)責(zé)在玩家死亡事件出現(xiàn)時(shí)判斷是否超過限制從而把挑戰(zhàn)狀態(tài)置為失敗。如果把失敗線和目標(biāo)混在一個(gè)進(jìn)度表里就會出現(xiàn)很滑稽的結(jié)果玩家還沒完成任務(wù)系統(tǒng)先獎勵了一個(gè)“已死亡三次”的進(jìn)度。所以實(shí)現(xiàn)上應(yīng)該把兩類條件分開達(dá)成條件需要累計(jì)并判斷是否滿足。失敗條件只做狀態(tài)攔截不參與正常完成結(jié)算。很多新人不理解為什么3death不該被放進(jìn)goals數(shù)組。因?yàn)槟繕?biāo)列表在語義上是“要做到的事”死亡失敗線在語義上是“不能踩的雷”。前者沒完成是進(jìn)度不足后者觸發(fā)了是即時(shí)失敗兩者的業(yè)務(wù)含義和用戶體驗(yàn)完全不同。4.2 多個(gè)條件同時(shí)滿足時(shí)判定順序和事件時(shí)序第二個(gè)常見問題是多事件并發(fā)。比如玩家在挑戰(zhàn)結(jié)束前最后一瞬間完成了q3同時(shí)副本通關(guān)事件dc也到達(dá)兩個(gè)事件擠在同一幀里。此時(shí)計(jì)數(shù)順序不同可能影響任務(wù)是否完成。這種問題在設(shè)計(jì)時(shí)最容易低估。事件系統(tǒng)如果天然有序那么還好如果是異步亂序就會出現(xiàn)“任務(wù)完成了但獎勵沒發(fā)”“明明通關(guān)了卻沒計(jì)數(shù)”之類的現(xiàn)象。我的建議是把結(jié)算邏輯設(shè)計(jì)得“冪等”。也就是說同一個(gè)事件處理兩次結(jié)果不能是計(jì)數(shù)加兩次。事件里最好帶上唯一事件 ID或者至少用“事件類型 目標(biāo) ID 當(dāng)前次數(shù)”做去重鍵。否則測試環(huán)境跑不出來一到高并發(fā)或低延遲環(huán)境就重復(fù)發(fā)獎。4.3 重置策略不一致線上才出詭異 bug第三種常見問題是重置策略。挑戰(zhàn)系統(tǒng)經(jīng)常和天數(shù)、周數(shù)、角色死亡、隊(duì)伍重開等因素耦合。一個(gè)暗號式需求里沒有說明“什么時(shí)候清零”實(shí)現(xiàn)時(shí)就要特別小心。比如玩家在挑戰(zhàn)中死亡了一次系統(tǒng)判定沒有超過三次允許繼續(xù)。那么已經(jīng)完成的q3一次算不算保留如果策劃認(rèn)為“死亡即重來”那么progressResetOnDeath就是 true如果認(rèn)為“只要沒到三次進(jìn)度就還在”就是 false。這兩套邏輯在單人測試時(shí)不一定能試出來但一旦組隊(duì)、換人、角色離場就會出現(xiàn)一個(gè)隊(duì)員死了其他隊(duì)員還在打可任務(wù)狀態(tài)已經(jīng)被重置的情況。線上玩家會立刻感受到“不公平”。建議在需求確認(rèn)階段把重置策略集中列成一張小表逐項(xiàng)問清楚時(shí)機(jī)是否重置已完成目標(biāo)是否重置死亡次數(shù)玩家死亡待確認(rèn)通常不重置組隊(duì)解散待確認(rèn)待確認(rèn)挑戰(zhàn)失敗待確認(rèn)待確認(rèn)每日刷新待確認(rèn)待確認(rèn)掉線重連待確認(rèn)待確認(rèn)問完這一輪再寫邏輯也不遲。5. 遇到“任務(wù)沒完成/沒發(fā)獎”的排查順序這類功能線上跑一段時(shí)間后最常見的反饋是玩家說“我明明完成了為什么沒有發(fā)獎”或者“我死了三次但沒有判定失敗”。每次接到這類反饋別急著去翻業(yè)務(wù)邏輯先按固定順序排查。5.1 從現(xiàn)象到事件日志第一步不是看代碼而是看現(xiàn)象屬于哪一類是根本沒有進(jìn)入挑戰(zhàn)狀態(tài)是計(jì)數(shù)沒增加是目標(biāo)全部滿足了但沒有發(fā)獎還是發(fā)獎發(fā)了兩次現(xiàn)象不同排查入口完全不同。連現(xiàn)象都沒分類就開始改代碼往往會把一個(gè)早期日志問題誤判成判定邏輯問題。第二步是查看事件日志。確認(rèn)q3和dc對應(yīng)的事件到底有沒有被服務(wù)端收到。有時(shí)候客戶端顯示任務(wù)完成了但服務(wù)端根本沒有相關(guān)事件上報(bào)計(jì)數(shù)從源頭就是零。這種問題再怎么寫判定邏輯都沒有用。5.2 檢查配置與重置日志顯示事件收到了接著看配置實(shí)例。重點(diǎn)檢查三處goals里的requiredCount是不是被配置成了 0 或負(fù)數(shù)。當(dāng)前實(shí)例的progressResetOnDeath是不是被改過。dc的type寫的是dungeon_clear但服務(wù)端發(fā)的是dungeon_join兩邊文本不一致導(dǎo)致計(jì)數(shù)永遠(yuǎn)收不到。第三處尤其隱蔽。我見過不少次因?yàn)槭录愋兔笮懖灰恢聦?dǎo)致配置總是匹配不到目標(biāo)。5.3 邊界驗(yàn)證的三種測試路徑為了防止這種問題滾到線上我會在每個(gè)挑戰(zhàn)規(guī)則上線前跑三類用例正常路徑按規(guī)則完成全部目標(biāo)確認(rèn)狀態(tài)變?yōu)槌晒Σl(fā)放獎勵。邊界路徑死亡次數(shù)剛好卡在上限、倒數(shù)第二次和第三次連在一起、刷新前最后一秒完成。異常路徑中途掉線重連、組隊(duì)中途解散、同一個(gè)事件重復(fù)上報(bào)兩次、服務(wù)器時(shí)間跳變。這三類用例不需要寫很多。每類里挑最典型的 3 到 5 條能覆蓋絕大多數(shù)回歸風(fēng)險(xiǎn)。實(shí)操經(jīng)驗(yàn)不要只在本地用單機(jī)賬號測。至少準(zhǔn)備一個(gè)組隊(duì)環(huán)境因?yàn)橛写罅恐刂妙?bug 只有在多人共享進(jìn)度時(shí)才會暴露。6. 這類需求背后真正值得長期堅(jiān)持的事處理abd 3deathq3*1 dc*2這串文字的最后收獲不是把它做出來而是意識到項(xiàng)目里缺少了一層?xùn)|西把一句話需求變成可執(zhí)行驗(yàn)收規(guī)格的過程不能省。6.1 給代碼補(bǔ)充“需求規(guī)格層”我以前接手過一個(gè)項(xiàng)目規(guī)則散落在注釋里、需求單里和策劃的聊天記錄里。代碼寫得再清楚也沒法還原“當(dāng)時(shí)為什么這樣定”。后來我們做了個(gè)改變每個(gè)挑戰(zhàn) / 任務(wù)配置的字段旁邊必須寫一句業(yè)務(wù)含義并且放到代碼倉庫里隨版本管理。規(guī)則變配置和人話說明一起變。這套做法聽起來不炫但效果立竿見影。至少三個(gè)月后再有人問“3death 到底是什么意思”不需要去翻幾百條聊天記錄直接看配置注釋和驗(yàn)收單就行。落到實(shí)際工作中可以這樣做在 ticket 描述區(qū)補(bǔ)齊語義假設(shè)和確認(rèn)結(jié)論。在配置字段附近寫清單位和邊界。在上線備注里寫明這個(gè)規(guī)則影響了哪些舊行為。在測試用例名稱里帶上業(yè)務(wù)描述。6.2 測試就成了可執(zhí)行的驗(yàn)收單為什么建議用測試用例去表達(dá)需求因?yàn)樗热魏挝臋n都更嚴(yán)格。文檔寫“死亡次數(shù)不能超過三次”測試用例會寫成“死兩次正常推進(jìn)死第三次觸發(fā)失敗”。測試不只驗(yàn)證代碼也驗(yàn)證需求本身是不是自洽。寫測試的時(shí)候如果發(fā)現(xiàn)無法通過簡潔的用例表達(dá)這條規(guī)則那大概率是需求本身還有漏洞。這也回應(yīng)了開頭那句話一行暗號不是需求它只是需求的壓縮包。我們要做的是在解壓時(shí)補(bǔ)齊所有缺失字段、邊界和異常策略。補(bǔ)齊的過程才是交付真正的價(jià)值。所以下次再看到類似abd 3deathq3*1 dc*2這種需求串別急著打開代碼編輯器。先花半天把 token 拆開、把語義列清楚、把邊界問題記下來去找人逐條確認(rèn)然后寫一張可驗(yàn)收的規(guī)則表再考慮配置怎么寫。你省下的那點(diǎn)溝通時(shí)間最后都會在返工、線上問題和團(tuán)隊(duì)摩擦里還回去。真正靠譜的開發(fā)不是看到暗號就能猜中的人而是能證明自己不需要靠猜的人。