refactor(battlenode): engine-first token identity (cardId); keep wire-mining fallback (M-HC-4f, partial)
Source the played card's opponent-facing knownList[].cardId off the shadow engine (SessionBattleEngine.PlayedCardId -> BattleCardBase.CardId), engine-first with the wire-mined idx->cardId map as the fallback. PROVEN engine-resolved (each backed by a HeadlessConductorTests PlayedCardId_* test): deck cards and receive-path substituted/ revealed tokens (engine seats the wire id at the wire idx). PARTIAL retirement: the wire-mining bookkeeping (MineAddOps/MineChoicePicks/MineCopyTokens + Record*From) is KEPT as the load-bearing fallback. The choice/Discover, copy/clone and cross-side (isSelf:0) token cases are NOT proven to resolve at a wire idx headless — the autonomous token_draw path seats a chosen token at engine Index 0 (would collide with the leader), and copy/cross-side aren't cheaply fixturable. Deleting their mining on faith would silently corrupt opponent reveals, so it stays behind a TODO(M-HC-4f) gate. - SessionBattleEngine.PlayedCardId: new accessor mirroring PlayedCardClan/Tribe. - BuildPlayedCard: signature deckMap->explicit cardId; null on cardId==0 (no engine id AND no mined/deck-map fallback). - PlayActionsHandler: cardId = engine.PlayedCardId(seat, idx, fallback: mapped) ; mining retained. - Tests: PlayedCardId_* (deck/substituted/degrade pass; choice-gap [Explicit] documents the Index-0 finding). KnownListBuilder + CaptureConformance call-sites updated to new signature. Full BattleNode suite 263/263 green; HeadlessConductorTests 27/27; drift clean; no Engine edits. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -10,23 +10,29 @@ namespace SVSim.BattleNode.Sessions.Dispatch;
|
||||
internal static class KnownListBuilder
|
||||
{
|
||||
/// <summary>The played card's knownList entry, or null when its identity can't be synthesized
|
||||
/// (token idx not in the deck map, or no matching move op). <paramref name="cost"/>,
|
||||
/// <paramref name="spellboost"/>, <paramref name="clan"/> and <paramref name="tribe"/> are ALL
|
||||
/// ENGINE-SOURCED (M-HC-3a/3b/4e) — the handler reads them off the shadow engine
|
||||
/// (<c>SessionBattleEngine.PlayedCardCost</c>/<c>PlayedCardSpellboost</c>/<c>PlayedCardClan</c>/
|
||||
/// <c>PlayedCardTribe</c>) and passes them in; all land on the entry verbatim. The wire-derived
|
||||
/// spellboost bookkeeping is retired — the engine owns cost and count by construction (cost folds the
|
||||
/// spellboost discount in already; the count rides the entry only to stay prod-faithful, prod sends the
|
||||
/// real count here). <paramref name="clan"/>/<paramref name="tribe"/> are the LIVE (skill-applied)
|
||||
/// values the engine resolved — prod always sends both on every knownList entry (clan int, tribe the
|
||||
/// comma-joined int string, "0" for none). Prod's client reads cost straight into the card's cost model
|
||||
/// (<c>NetworkBattleReceiver</c>), so a vanilla play resolves to its base cost and count 0. attachTarget
|
||||
/// stays "".</summary>
|
||||
/// (the played idx resolves to no card identity, or there's no matching move op). <paramref name="cardId"/>,
|
||||
/// <paramref name="cost"/>, <paramref name="spellboost"/>, <paramref name="clan"/> and <paramref name="tribe"/>
|
||||
/// are ALL ENGINE-SOURCED at the call site (M-HC-3a/3b/4e/4f) — the handler reads them off the shadow engine
|
||||
/// (<c>SessionBattleEngine.PlayedCardId</c>/<c>PlayedCardCost</c>/<c>PlayedCardSpellboost</c>/<c>PlayedCardClan</c>/
|
||||
/// <c>PlayedCardTribe</c>) and passes them in; all land on the entry verbatim.
|
||||
/// <para><paramref name="cardId"/> is the engine-resolved TRUE identity of the played card (M-HC-4f). The
|
||||
/// handler computes it engine-first with a fallback: <c>engine.PlayedCardId(seat, playIdx, fallback: deckMapId)</c>,
|
||||
/// where <c>deckMapId</c> is the wire-mined idx→cardId entry (deck card or recorded token). So an engine-backed
|
||||
/// session emits the engine identity; a non-engine session (or an idx the engine doesn't headlessly resolve at a
|
||||
/// wire idx — choice/copy/cross-side tokens, still wire-mined; see TODO(M-HC-4f) in PlayActionsHandler) falls back
|
||||
/// to the mined map. A <paramref name="cardId"/> of 0 (no engine id AND no mined entry) means an un-synthesizable
|
||||
/// play → null (no knownList; the play degrades to {playIdx,type}).</para>
|
||||
/// The wire-derived spellboost bookkeeping is retired — the engine owns cost and count by construction (cost folds
|
||||
/// the spellboost discount in already; the count rides the entry only to stay prod-faithful, prod sends the real
|
||||
/// count here). <paramref name="clan"/>/<paramref name="tribe"/> are the LIVE (skill-applied) values the engine
|
||||
/// resolved — prod always sends both on every knownList entry (clan int, tribe the comma-joined int string, "0"
|
||||
/// for none). Prod's client reads cost straight into the card's cost model (<c>NetworkBattleReceiver</c>), so a
|
||||
/// vanilla play resolves to its base cost and count 0. attachTarget stays "".</summary>
|
||||
public static KnownCardEntry? BuildPlayedCard(
|
||||
IReadOnlyDictionary<int, long> deckMap, int playIdx, object? orderList,
|
||||
int playIdx, long cardId, object? orderList,
|
||||
int cost = 0, int spellboost = 0, int clan = 0, string tribe = "0")
|
||||
{
|
||||
if (!deckMap.TryGetValue(playIdx, out var cardId)) return null;
|
||||
if (cardId == 0) return null; // no engine id AND no mined/deck-map fallback → can't synthesize an identity
|
||||
var to = ExtractMoveTo(orderList, playIdx);
|
||||
if (to is null) return null;
|
||||
return new KnownCardEntry(
|
||||
|
||||
Reference in New Issue
Block a user