feat(battle-node): reveal copy tokens on play via baseIdx resolution
PlayActionsHandler + EchoHandler now call RecordCopyTokensFrom (ordered after plain/choice mining) to resolve a copy add's baseIdx against the side's live idx->cardId map and record copyIdx->cardId. A copy played in a later (or same) frame synthesizes a knownList instead of degrading. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -67,4 +67,21 @@ internal sealed class BattleSessionState
|
||||
foreach (var (idx, cardId, isSelf) in KnownListBuilder.MineChoicePicks(orderList, keyAction))
|
||||
RecordToken(isSelf == 1 ? from : other, idx, cardId);
|
||||
}
|
||||
|
||||
/// <summary>Mine + record copy/clone-token identities (<see cref="KnownListBuilder.MineCopyTokens"/>)
|
||||
/// into the correct side's map. A copy's source lives at <c>baseIdx</c> in the actor's own index
|
||||
/// space, so the resolution side == the record side, both selected by the same <c>isSelf</c> routing
|
||||
/// as <see cref="RecordTokensFrom"/>. Passing the LIVE per-side maps (via
|
||||
/// <see cref="GetOrSeedDeckMap"/>, not snapshots) lets a copy that references a plain/choice token
|
||||
/// added earlier THIS frame resolve — provided this runs AFTER
|
||||
/// <see cref="RecordTokensFrom"/>/<see cref="RecordChoicePicksFrom"/> (the handler orders it last).
|
||||
/// Seeding both maps up front matters because a copy-only frame (no concrete/choice add) would never
|
||||
/// have hit <see cref="RecordToken"/> yet, leaving the maps unseeded.</summary>
|
||||
public void RecordCopyTokensFrom(IBattleParticipant from, IBattleParticipant other, object? orderList)
|
||||
{
|
||||
var selfMap = GetOrSeedDeckMap(from);
|
||||
var otherMap = GetOrSeedDeckMap(other);
|
||||
foreach (var (idx, cardId, isSelf) in KnownListBuilder.MineCopyTokens(orderList, selfMap, otherMap))
|
||||
RecordToken(isSelf == 1 ? from : other, idx, cardId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,9 @@ namespace SVSim.BattleNode.Sessions.Dispatch.Handlers;
|
||||
/// node never relays it (bullet-2 audit — relaying would risk an echo->echo storm). It IS mined,
|
||||
/// though: an Echo's orderList carries the same add-op shape as PlayActions
|
||||
/// (SendCardDataMaker.MakeEchoData -> MakeCommonSendAndEchoCardData), so it can hold a token's real
|
||||
/// identity — notably the receiver's own (isSelf:1) view of a cross-side gift. We mine it into the
|
||||
/// right side's idx->cardId map and still return no routes (mining != relaying).</summary>
|
||||
/// identity — notably the receiver's own (isSelf:1) view of a cross-side gift. We mine it (concrete
|
||||
/// tokens and baseIdx copies) into the right side's idx->cardId map and still return no routes
|
||||
/// (mining != relaying).</summary>
|
||||
internal sealed class EchoHandler : IFrameHandler
|
||||
{
|
||||
public IReadOnlyList<DispatchRoute> Handle(FrameDispatchContext ctx)
|
||||
@@ -16,6 +17,8 @@ internal sealed class EchoHandler : IFrameHandler
|
||||
{
|
||||
var orderList = (ctx.Env.Body as RawBody)?.Entries.GetValueOrDefault("orderList");
|
||||
ctx.State.RecordTokensFrom(ctx.From, ctx.Other, orderList);
|
||||
// Copy tokens ride Echo too (same add-op shape); resolve baseIdx against the side's map.
|
||||
ctx.State.RecordCopyTokensFrom(ctx.From, ctx.Other, orderList);
|
||||
// No RecordChoicePicksFrom here: choice picks ride keyAction.selectCard on the generating
|
||||
// SEND, not the receiver's Echo (Echo carries orderList only) — the pick is already
|
||||
// recorded by PlayActionsHandler. MineChoicePicks(orderList, null) would yield nothing.
|
||||
|
||||
@@ -6,9 +6,10 @@ namespace SVSim.BattleNode.Sessions.Dispatch.Handlers;
|
||||
/// <summary>PvP PlayActions translator. Synthesizes the opponent-facing knownList from the sender's
|
||||
/// idx->cardId map + the orderList move op, renames targetList -> oppoTargetList, drops orderList,
|
||||
/// and forwards a stripped keyAction for choice/Discover plays ({type,cardId}; selectCard dropped
|
||||
/// for a hidden open:0 pick). Token plays resolve their cardId from add ops (concrete tokens) or
|
||||
/// keyAction.selectCard (choice picks) mined on earlier frames; an un-generated token idx still
|
||||
/// degrades to {playIdx,type} (no knownList). Bot drop (no rule).</summary>
|
||||
/// for a hidden open:0 pick). Token plays resolve their cardId from add ops (concrete tokens),
|
||||
/// keyAction.selectCard (choice picks), or a baseIdx copy resolved against the side's map — all mined
|
||||
/// on earlier (or the same) frames; an un-generated token idx still degrades to {playIdx,type}
|
||||
/// (no knownList). Bot drop (no rule).</summary>
|
||||
internal sealed class PlayActionsHandler : IFrameHandler
|
||||
{
|
||||
public IReadOnlyList<DispatchRoute> Handle(FrameDispatchContext ctx)
|
||||
@@ -32,6 +33,11 @@ internal sealed class PlayActionsHandler : IFrameHandler
|
||||
// choiceAdd carries candidates only). Record idx->chosenCardId now so the later play reveals it.
|
||||
ctx.State.RecordChoicePicksFrom(ctx.From, ctx.Other, orderList, keyAction);
|
||||
|
||||
// Copy/clone tokens: card:{baseIdx} points at a card in the actor's own index space; resolve it
|
||||
// against that side's map and record copyIdx->cardId so the later play reveals it. Ordered after
|
||||
// the plain/choice mining so a same-frame copy of a just-added token resolves against the live map.
|
||||
ctx.State.RecordCopyTokensFrom(ctx.From, ctx.Other, orderList);
|
||||
|
||||
var deckMap = ctx.State.GetOrSeedDeckMap(ctx.From);
|
||||
var played = KnownListBuilder.BuildPlayedCard(deckMap, playIdx, orderList);
|
||||
var oppoTargets = KnownListBuilder.RenameTargets(entries.GetValueOrDefault("targetList"));
|
||||
|
||||
Reference in New Issue
Block a user