test(battlenode): capture-replay helper + battle_test fixtures (Phase 2 N1)
CaptureReplay normalizes the capture's send/receive envelope asymmetry (send frames carry uri at top level + bare payload body; receive frames carry a full envelope body) and extracts selfDeck + master seed from Matched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
40
SVSim.BattleEngine.Tests/Fixtures/battle_test_cl1.ndjson
Normal file
40
SVSim.BattleEngine.Tests/Fixtures/battle_test_cl1.ndjson
Normal file
@@ -0,0 +1,40 @@
|
||||
{"ts":"2026-06-05T16:36:19.3503474Z","direction":"receive","uri":null,"body":{"uri":"InitNetwork","viewerId":999999999,"uuid":"node-stub","try":0,"cat":99,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:36:19.3573466Z","direction":"receive","uri":null,"body":{"uri":"Matched","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"bid":"889788596105","playSeq":1,"selfInfo":{"country_code":"KOR","userName":"SVSim1","sleeveId":"3000011","emblemId":"100000000","degreeId":"300003","fieldId":43,"isOfficial":0,"oppoId":6,"seed":508806643},"oppoInfo":{"country_code":"KOR","userName":"SVSim2","sleeveId":"3000011","emblemId":"100000000","degreeId":"300003","fieldId":43,"isOfficial":0,"oppoId":7,"seed":508806643,"oppoDeckCount":40},"selfDeck":[{"idx":1,"cardId":100314010},{"idx":2,"cardId":100314020},{"idx":3,"cardId":102324040},{"idx":4,"cardId":101324050},{"idx":5,"cardId":101024010},{"idx":6,"cardId":101314020},{"idx":7,"cardId":101311050},{"idx":8,"cardId":101311010},{"idx":9,"cardId":100314020},{"idx":10,"cardId":101321040},{"idx":11,"cardId":101024010},{"idx":12,"cardId":127011010},{"idx":13,"cardId":100314040},{"idx":14,"cardId":101314020},{"idx":15,"cardId":102331010},{"idx":16,"cardId":102324040},{"idx":17,"cardId":101334040},{"idx":18,"cardId":100321010},{"idx":19,"cardId":101324040},{"idx":20,"cardId":100314030},{"idx":21,"cardId":101324040},{"idx":22,"cardId":101311050},{"idx":23,"cardId":701341011},{"idx":24,"cardId":101324050},{"idx":25,"cardId":100314030},{"idx":26,"cardId":101311010},{"idx":27,"cardId":101321070},{"idx":28,"cardId":101024010},{"idx":29,"cardId":100314040},{"idx":30,"cardId":127011010},{"idx":31,"cardId":127011010},{"idx":32,"cardId":100314010},{"idx":33,"cardId":102334020},{"idx":34,"cardId":101334030},{"idx":35,"cardId":101341010},{"idx":36,"cardId":101321040},{"idx":37,"cardId":101314020},{"idx":38,"cardId":101321070},{"idx":39,"cardId":100321010},{"idx":40,"cardId":101334020}],"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:36:21.2805258Z","direction":"receive","uri":null,"body":{"uri":"BattleStart","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":2,"turnState":1,"battleType":11,"selfInfo":{"rank":"10","battlePoint":"6270","classId":"3","charaId":"3","cardMasterName":"card_master_node_10015"},"oppoInfo":{"rank":"1","isMasterRank":"0","battlePoint":0,"masterPoint":"0","classId":"1","charaId":"1","cardMasterName":"card_master_node_10015"},"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:36:21.2820523Z","direction":"receive","uri":null,"body":{"uri":"Deal","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":3,"self":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":3}],"oppo":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":3}],"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:36:45.4884447Z","direction":"send","uri":"Swap","body":{"idxList":[3]}}
|
||||
{"ts":"2026-06-05T16:36:45.4909435Z","direction":"receive","uri":null,"body":{"uri":"Swap","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":4,"self":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":4}],"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:36:46.8360545Z","direction":"receive","uri":null,"body":{"uri":"Ready","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":5,"self":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":4}],"oppo":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":3}],"idxChangeSeed":857671914,"spin":243,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:36:46.9530582Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":5,"playSeq":6,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:36:49.0622004Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[39],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":1}}]}}
|
||||
{"ts":"2026-06-05T16:36:53.9257769Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":6,"playSeq":7}}
|
||||
{"ts":"2026-06-05T16:36:53.9473080Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[1,2,3,39],"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":0}}]}}
|
||||
{"ts":"2026-06-05T16:36:54.4348349Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":7,"playSeq":8,"turnState":0,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:36:54.4458360Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"143","key2":"17","key3":"0","key4":"141","key5":"170","key6":"0"}}}
|
||||
{"ts":"2026-06-05T16:36:54.4643354Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":7,"playSeq":9,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:36:54.5198350Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[23,14],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"avarice":1}}],"actionSeq":2}}
|
||||
{"ts":"2026-06-05T16:36:59.8031059Z","direction":"send","uri":"PlayActions","body":{"playIdx":1,"orderList":[{"move":{"idx":[1],"isSelf":1,"from":10,"to":30}},{"alter":{"idx":[2,4,23,14],"isSelf":1,"type":"add","spellboost":"a1","attachTarget":"0"}},{"move":{"idx":[8],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":1}}],"type":30}}
|
||||
{"ts":"2026-06-05T16:37:02.5213012Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[2,4,23,14,8],"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":0}},{"trigger":{"isSelf":1,"avarice":0}}]}}
|
||||
{"ts":"2026-06-05T16:37:03.0188508Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"141","key2":"175","key3":"0","key4":"141","key5":"170","key6":"0"},"type":0,"actionSeq":5,"cemetery":[1,0]}}
|
||||
{"ts":"2026-06-05T16:37:03.1346446Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":12,"playSeq":10,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:37:03.1561609Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[37],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":0}}]}}
|
||||
{"ts":"2026-06-05T16:37:07.8849014Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":13,"playSeq":11,"playIdx":37,"type":30,"knownList":[{"idx":37,"cardId":101121020,"to":20,"spellboost":0,"attachTarget":""}]}}
|
||||
{"ts":"2026-06-05T16:37:08.1357329Z","direction":"send","uri":"Echo","body":{"playIdx":37,"orderList":[{"move":{"idx":[37],"isSelf":0,"from":10,"to":20}},{"playerParam":{"isSelf":0,"buffUnit":1}}],"type":30}}
|
||||
{"ts":"2026-06-05T16:37:09.1078628Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":14,"playSeq":12}}
|
||||
{"ts":"2026-06-05T16:37:09.6087702Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":15,"playSeq":13,"turnState":0,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:37:11.0449391Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[1,2,3,39],"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":2}}]}}
|
||||
{"ts":"2026-06-05T16:37:11.4765571Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"141","key2":"175","key3":"0","key4":"143","key5":"170","key6":"101121070"}}}
|
||||
{"ts":"2026-06-05T16:37:11.4925578Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":15,"playSeq":14,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:37:11.5190593Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[24],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":0}}],"actionSeq":8}}
|
||||
{"ts":"2026-06-05T16:37:25.1553015Z","direction":"send","uri":"PlayActions","body":{"playIdx":2,"targetList":[{"targetIdx":37,"isSelf":0,"selectSkillIndex":[1]}],"orderList":[{"move":{"idx":[2],"isSelf":1,"from":10,"to":30}},{"alter":{"idx":[4,23,14,8,24],"isSelf":1,"type":"add","spellboost":"a1","attachTarget":"3"}},{"move":{"idx":[37],"isSelf":0,"from":20,"to":30}},{"move":{"idx":[15],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":1}},{"trigger":{"isSelf":1,"avarice":1}}],"type":31}}
|
||||
{"ts":"2026-06-05T16:37:26.1829531Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[4,23,14,8,24,15],"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":0}},{"trigger":{"isSelf":1,"avarice":0}}]}}
|
||||
{"ts":"2026-06-05T16:37:26.6838102Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"142","key2":"334","key3":"0","key4":"145","key5":"170","key6":"0"},"type":0,"actionSeq":11,"cemetery":[2,1]}}
|
||||
{"ts":"2026-06-05T16:37:28.3338739Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":20,"playSeq":15,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:37:28.3556277Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[19],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":1}}]}}
|
||||
{"ts":"2026-06-05T16:37:33.2699751Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":21,"playSeq":16}}
|
||||
{"ts":"2026-06-05T16:37:33.2873251Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[1,2,3,39,19],"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":0}}]}}
|
||||
{"ts":"2026-06-05T16:37:33.7738440Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":22,"playSeq":17,"turnState":0,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:37:33.7898440Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"142","key2":"334","key3":"0","key4":"147","key5":"265","key6":"0"}}}
|
||||
{"ts":"2026-06-05T16:37:33.8063464Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":24,"playSeq":18,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:37:33.8323438Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[37],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":0}}],"actionSeq":13}}
|
||||
{"ts":"2026-06-05T16:37:38.6691412Z","direction":"send","uri":"PlayActions","body":{"playIdx":14,"orderList":[{"move":{"idx":[14],"isSelf":1,"from":10,"to":30}},{"alter":{"idx":[4,23,8,24,15,37],"isSelf":1,"type":"add","spellboost":"a1","attachTarget":"6"}},{"move":{"idx":[36,18],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"avarice":1}}],"type":30}}
|
||||
38
SVSim.BattleEngine.Tests/Fixtures/battle_test_cl2.ndjson
Normal file
38
SVSim.BattleEngine.Tests/Fixtures/battle_test_cl2.ndjson
Normal file
@@ -0,0 +1,38 @@
|
||||
{"ts":"2026-06-05T16:36:19.3388464Z","direction":"receive","uri":null,"body":{"uri":"InitNetwork","viewerId":999999999,"uuid":"node-stub","try":0,"cat":99,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:36:19.3458471Z","direction":"receive","uri":null,"body":{"uri":"Matched","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"bid":"889788596105","playSeq":1,"selfInfo":{"country_code":"KOR","userName":"SVSim2","sleeveId":"3000011","emblemId":"100000000","degreeId":"300003","fieldId":43,"isOfficial":0,"oppoId":7,"seed":508806643},"oppoInfo":{"country_code":"KOR","userName":"SVSim1","sleeveId":"3000011","emblemId":"100000000","degreeId":"300003","fieldId":43,"isOfficial":0,"oppoId":6,"seed":508806643,"oppoDeckCount":40},"selfDeck":[{"idx":1,"cardId":100114010},{"idx":2,"cardId":101121080},{"idx":3,"cardId":101114010},{"idx":4,"cardId":113011010},{"idx":5,"cardId":101121020},{"idx":6,"cardId":100111010},{"idx":7,"cardId":102141010},{"idx":8,"cardId":102111060},{"idx":9,"cardId":100111070},{"idx":10,"cardId":113011010},{"idx":11,"cardId":101131050},{"idx":12,"cardId":101121080},{"idx":13,"cardId":100111010},{"idx":14,"cardId":102121010},{"idx":15,"cardId":701141011},{"idx":16,"cardId":100114010},{"idx":17,"cardId":101114050},{"idx":18,"cardId":102131020},{"idx":19,"cardId":102111060},{"idx":20,"cardId":100114010},{"idx":21,"cardId":102121030},{"idx":22,"cardId":102121030},{"idx":23,"cardId":101114050},{"idx":24,"cardId":100111070},{"idx":25,"cardId":100111020},{"idx":26,"cardId":101121110},{"idx":27,"cardId":102131030},{"idx":28,"cardId":113011010},{"idx":29,"cardId":102131010},{"idx":30,"cardId":100111020},{"idx":31,"cardId":101131020},{"idx":32,"cardId":101114050},{"idx":33,"cardId":101121010},{"idx":34,"cardId":101121080},{"idx":35,"cardId":101121110},{"idx":36,"cardId":101114010},{"idx":37,"cardId":101121020},{"idx":38,"cardId":100111020},{"idx":39,"cardId":102121010},{"idx":40,"cardId":101121010}],"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:36:21.2050506Z","direction":"receive","uri":null,"body":{"uri":"BattleStart","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":2,"turnState":0,"battleType":11,"selfInfo":{"rank":"10","battlePoint":"6270","classId":"1","charaId":"1","cardMasterName":"card_master_node_10015"},"oppoInfo":{"rank":"1","isMasterRank":"0","battlePoint":0,"masterPoint":"0","classId":"3","charaId":"3","cardMasterName":"card_master_node_10015"},"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:36:21.2065539Z","direction":"receive","uri":null,"body":{"uri":"Deal","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":3,"self":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":3}],"oppo":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":3}],"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:36:46.8260552Z","direction":"send","uri":"Swap","body":{"idxList":[]}}
|
||||
{"ts":"2026-06-05T16:36:46.8285526Z","direction":"receive","uri":null,"body":{"uri":"Swap","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":4,"self":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":3}],"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:36:46.8295526Z","direction":"receive","uri":null,"body":{"uri":"Ready","viewerId":999999999,"uuid":"node-stub","try":0,"cat":1,"playSeq":5,"self":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":3}],"oppo":[{"pos":0,"idx":1},{"pos":1,"idx":2},{"pos":2,"idx":4}],"idxChangeSeed":224055814,"spin":243,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:36:46.9460536Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[39],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":1}}],"actionSeq":0}}
|
||||
{"ts":"2026-06-05T16:36:53.9137786Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[1,2,3,39],"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":0}}]}}
|
||||
{"ts":"2026-06-05T16:36:54.4108350Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"141","key2":"170","key3":"0","key4":"143","key5":"17","key6":"0"},"type":0,"actionSeq":2,"cemetery":[0,0]}}
|
||||
{"ts":"2026-06-05T16:36:54.5258347Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":8,"playSeq":6,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:36:54.5523350Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[23,14],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"avarice":1}}]}}
|
||||
{"ts":"2026-06-05T16:36:59.8136078Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":9,"playSeq":7,"playIdx":1,"type":30,"knownList":[{"idx":1,"cardId":100314010,"to":30,"spellboost":0,"attachTarget":""}]}}
|
||||
{"ts":"2026-06-05T16:37:00.0026151Z","direction":"send","uri":"Echo","body":{"playIdx":1,"orderList":[{"move":{"idx":[1],"isSelf":0,"from":10,"to":30}},{"alter":{"idx":[2,4,23,14],"isSelf":0,"type":"add","spellboost":"a1","attachTarget":"0"}},{"move":{"idx":[8],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":1}}],"type":30}}
|
||||
{"ts":"2026-06-05T16:37:02.5313002Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":10,"playSeq":8}}
|
||||
{"ts":"2026-06-05T16:37:02.5503289Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[2,4,23,14,8],"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":0}},{"trigger":{"isSelf":0,"avarice":0}}]}}
|
||||
{"ts":"2026-06-05T16:37:03.0339655Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":11,"playSeq":9,"turnState":0,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:37:03.0510647Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"141","key2":"170","key3":"0","key4":"141","key5":"175","key6":"0"}}}
|
||||
{"ts":"2026-06-05T16:37:03.0670774Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":11,"playSeq":10,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:37:03.1321443Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[37],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":0}}],"actionSeq":5}}
|
||||
{"ts":"2026-06-05T16:37:07.8809043Z","direction":"send","uri":"PlayActions","body":{"playIdx":37,"orderList":[{"move":{"idx":[37],"isSelf":1,"from":10,"to":20}},{"playerParam":{"isSelf":1,"buffUnit":1}}],"type":30}}
|
||||
{"ts":"2026-06-05T16:37:09.0943648Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[1,2,3,39],"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":2}}]}}
|
||||
{"ts":"2026-06-05T16:37:09.5927718Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"143","key2":"170","key3":"101121070","key4":"141","key5":"175","key6":"0"},"type":0,"actionSeq":8,"cemetery":[0,1]}}
|
||||
{"ts":"2026-06-05T16:37:11.5305571Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":16,"playSeq":11,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:37:11.5519635Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[24],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":0}}]}}
|
||||
{"ts":"2026-06-05T16:37:25.1769841Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":19,"playSeq":12,"playIdx":2,"type":31,"knownList":[{"idx":2,"cardId":100314020,"to":30,"spellboost":1,"attachTarget":""}],"oppoTargetList":[{"targetIdx":37,"isSelf":0}]}}
|
||||
{"ts":"2026-06-05T16:37:25.3675799Z","direction":"send","uri":"Echo","body":{"playIdx":2,"orderList":[{"move":{"idx":[2],"isSelf":0,"from":10,"to":30}},{"alter":{"idx":[4,23,14,8,24],"isSelf":0,"type":"add","spellboost":"a1","attachTarget":"3"}},{"move":{"idx":[37],"isSelf":1,"from":20,"to":30}},{"move":{"idx":[15],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":1}},{"trigger":{"isSelf":0,"avarice":1}}],"type":31}}
|
||||
{"ts":"2026-06-05T16:37:26.1899527Z","direction":"receive","uri":null,"body":{"uri":"TurnEndActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":20,"playSeq":13}}
|
||||
{"ts":"2026-06-05T16:37:26.6913132Z","direction":"receive","uri":null,"body":{"uri":"TurnEnd","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":21,"playSeq":14,"turnState":0,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:37:28.1438230Z","direction":"send","uri":"Echo","body":{"orderList":[{"trigger":{"isSelf":0,"turnEndRestore":[4,23,14,8,24,15],"isUnlimited":1}},{"trigger":{"isSelf":0,"maxAtk":0}},{"trigger":{"isSelf":0,"avarice":0}}]}}
|
||||
{"ts":"2026-06-05T16:37:28.2597994Z","direction":"send","uri":"Judge","body":{"battleCode":{"key1":"145","key2":"170","key3":"0","key4":"142","key5":"334","key6":"0"}}}
|
||||
{"ts":"2026-06-05T16:37:28.2755229Z","direction":"receive","uri":null,"body":{"uri":"Judge","viewerId":6,"uuid":"10d8e723-ac93-47a5-b307-cf3a0d6026e4","try":1,"cat":1,"bid":"889788596105","pubSeq":19,"playSeq":15,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:37:28.3213347Z","direction":"send","uri":"TurnStart","body":{"orderList":[{"playerParam":{"isSelf":1,"maxPP":1}},{"trigger":{"isSelf":1,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":0,"turnDamageFromUnit":0}},{"move":{"idx":[19],"isSelf":1,"from":0,"to":10}},{"trigger":{"isSelf":1,"resonance":1}}],"actionSeq":11}}
|
||||
{"ts":"2026-06-05T16:37:33.2604742Z","direction":"send","uri":"TurnEndActions","body":{"orderList":[{"trigger":{"isSelf":1,"turnEndRestore":[1,2,3,39,19],"isUnlimited":1}},{"trigger":{"isSelf":1,"maxAtk":0}}]}}
|
||||
{"ts":"2026-06-05T16:37:33.7603450Z","direction":"send","uri":"TurnEnd","body":{"battleCode":{"key1":"147","key2":"265","key3":"0","key4":"142","key5":"334","key6":"0"},"type":0,"actionSeq":13,"cemetery":[1,2]}}
|
||||
{"ts":"2026-06-05T16:37:33.8438435Z","direction":"receive","uri":null,"body":{"uri":"TurnStart","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":25,"playSeq":16,"spin":0,"resultCode":1}}
|
||||
{"ts":"2026-06-05T16:37:33.8648584Z","direction":"send","uri":"Echo","body":{"orderList":[{"playerParam":{"isSelf":0,"maxPP":1}},{"trigger":{"isSelf":0,"turnStartRestore":1,"isUnlimited":1}},{"trigger":{"isSelf":1,"turnDamageFromUnit":0}},{"move":{"idx":[37],"isSelf":0,"from":0,"to":10}},{"trigger":{"isSelf":0,"resonance":0}}]}}
|
||||
{"ts":"2026-06-05T16:37:38.6786420Z","direction":"receive","uri":null,"body":{"uri":"PlayActions","viewerId":7,"uuid":"a7ee9b5f-b0c5-48de-82b1-1a2468cfc696","try":1,"cat":1,"bid":"889788596105","pubSeq":26,"playSeq":17,"playIdx":14,"type":30,"knownList":[{"idx":14,"cardId":101314020,"to":30,"spellboost":2,"attachTarget":""}]}}
|
||||
@@ -21,6 +21,11 @@
|
||||
<ProjectReference Include="..\SVSim.BattleNode\SVSim.BattleNode.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Captured PvP battle (both clients) replayed through the engine in the N1 shadow test. -->
|
||||
<None Include="Fixtures\**\*.ndjson" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- The loader's card-master dump (serialized CardCSVData objects). The headless fixture
|
||||
reflects these into CardMaster so the resolution path can look up real card stats. -->
|
||||
|
||||
86
SVSim.BattleEngine.Tests/SessionEngine/CaptureReplay.cs
Normal file
86
SVSim.BattleEngine.Tests/SessionEngine/CaptureReplay.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using SVSim.BattleNode.Protocol;
|
||||
|
||||
namespace SVSim.BattleEngine.Tests.SessionEngine
|
||||
{
|
||||
internal sealed record CapturedFrame(string Direction, string Uri, MsgEnvelope Env, string RawBody);
|
||||
|
||||
/// <summary>Parses a battle_test ndjson capture into MsgEnvelopes the engine can ingest.
|
||||
///
|
||||
/// Capture quirk (verified against data_dumps/captures/battle_test): the authoritative URI lives at
|
||||
/// the TOP LEVEL for SEND frames (the body omits uri/viewerId/uuid and carries only the play
|
||||
/// payload) and in the BODY for RECEIVE frames (top-level uri is null). We resolve uri as
|
||||
/// top ?? body, then normalize the body into a full envelope (injecting the fields a send-frame body
|
||||
/// lacks) so MsgEnvelope.FromJson — which requires uri/viewerId/uuid — succeeds for both.</summary>
|
||||
internal static class CaptureReplay
|
||||
{
|
||||
public static IReadOnlyList<CapturedFrame> Load(string fixtureFileName)
|
||||
{
|
||||
var path = Path.Combine(AppContext.BaseDirectory, "Fixtures", fixtureFileName);
|
||||
var frames = new List<CapturedFrame>();
|
||||
foreach (var line in File.ReadLines(path))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line)) continue;
|
||||
using var doc = JsonDocument.Parse(line);
|
||||
var root = doc.RootElement;
|
||||
var direction = root.TryGetProperty("direction", out var dEl) ? dEl.GetString() ?? "" : "";
|
||||
if (!root.TryGetProperty("body", out var bodyEl) || bodyEl.ValueKind != JsonValueKind.Object)
|
||||
continue;
|
||||
|
||||
string uri =
|
||||
root.TryGetProperty("uri", out var tu) && tu.ValueKind == JsonValueKind.String
|
||||
? tu.GetString()!
|
||||
: bodyEl.TryGetProperty("uri", out var bu) && bu.ValueKind == JsonValueKind.String
|
||||
? bu.GetString()!
|
||||
: "None";
|
||||
|
||||
// Normalize: send-frame bodies are bare payloads (no envelope fields). Inject the keys
|
||||
// FromJson requires; set the resolved uri.
|
||||
var obj = JsonNode.Parse(bodyEl.GetRawText())!.AsObject();
|
||||
obj["uri"] = uri;
|
||||
if (!obj.ContainsKey("viewerId")) obj["viewerId"] = 0L;
|
||||
if (!obj.ContainsKey("uuid")) obj["uuid"] = "";
|
||||
var normalized = obj.ToJsonString();
|
||||
|
||||
MsgEnvelope env;
|
||||
try { env = MsgEnvelope.FromJson(normalized); }
|
||||
catch { continue; } // out-of-model / unparseable line
|
||||
frames.Add(new CapturedFrame(direction, uri, env, normalized));
|
||||
}
|
||||
return frames;
|
||||
}
|
||||
|
||||
/// <summary>The selfDeck idx->cardId order from the Matched frame (the order the node also
|
||||
/// computed and handed the client). This is the deck the engine seats for that side.</summary>
|
||||
public static IReadOnlyList<long> SelfDeckFrom(IEnumerable<CapturedFrame> frames)
|
||||
{
|
||||
var matched = frames.FirstOrDefault(f => f.Uri == nameof(NetworkBattleUri.Matched));
|
||||
if (matched is null) return Array.Empty<long>();
|
||||
using var doc = JsonDocument.Parse(matched.RawBody);
|
||||
if (!doc.RootElement.TryGetProperty("selfDeck", out var deck)) return Array.Empty<long>();
|
||||
return deck.EnumerateArray()
|
||||
.OrderBy(e => e.GetProperty("idx").GetInt32())
|
||||
.Select(e => e.GetProperty("cardId").GetInt64())
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>The per-battle master seed the capture carries (Matched.selfInfo.seed) — the seed the
|
||||
/// node generated and both clients used (F-N-5). Falls back to 0 if absent.</summary>
|
||||
public static int SeedFrom(IEnumerable<CapturedFrame> frames)
|
||||
{
|
||||
var matched = frames.FirstOrDefault(f => f.Uri == nameof(NetworkBattleUri.Matched));
|
||||
if (matched is null) return 0;
|
||||
using var doc = JsonDocument.Parse(matched.RawBody);
|
||||
if (doc.RootElement.TryGetProperty("selfInfo", out var si)
|
||||
&& si.TryGetProperty("seed", out var seed)
|
||||
&& seed.TryGetInt32(out var v))
|
||||
return v;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
27
SVSim.BattleEngine.Tests/SessionEngine/CaptureReplayTests.cs
Normal file
27
SVSim.BattleEngine.Tests/SessionEngine/CaptureReplayTests.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace SVSim.BattleEngine.Tests.SessionEngine
|
||||
{
|
||||
[TestFixture]
|
||||
public class CaptureReplayTests
|
||||
{
|
||||
[Test]
|
||||
public void Load_parses_frames_and_extracts_self_deck()
|
||||
{
|
||||
var frames = CaptureReplay.Load("battle_test_cl1.ndjson");
|
||||
Assert.That(frames, Is.Not.Empty);
|
||||
|
||||
var deck = CaptureReplay.SelfDeckFrom(frames);
|
||||
Assert.That(deck, Is.Not.Empty, "Matched.selfDeck should parse");
|
||||
Assert.That(deck.Count, Is.EqualTo(40), "a standard deck is 40 cards");
|
||||
|
||||
// Send PlayActions carry their URI at the top level (body.uri == None); the helper must
|
||||
// resolve it correctly, not drop it to None.
|
||||
Assert.That(frames.Any(f => f.Direction == "send" && f.Uri == "PlayActions"),
|
||||
Is.True, "send PlayActions URI resolved from top level");
|
||||
|
||||
Assert.That(CaptureReplay.SeedFrom(frames), Is.GreaterThan(0), "Matched.selfInfo.seed parsed");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user