fix(battlenode): translate live isSelf target frames to engine vid shape on ingest (live PvP fidelity)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-07 07:44:53 -04:00
parent 97e4664cc4
commit 25751462f4
3 changed files with 205 additions and 27 deletions

View File

@@ -611,6 +611,110 @@ public class HeadlessConductorTests
"the NON-targeted follower must be UNTOUCHED (full life) — proves the wire target was honored");
}
// === isSelf->vid owner-mapping is DIRECTIONAL across BOTH sender perspectives =================
[Test]
public void Attack_from_seat_B_on_seat_A_follower_resolves_isSelf_reversed()
{
// The reversed-perspective half of the live isSelf->vid translation: a frame sent BY SEAT B
// (isPlayerSeat:false) targeting a SEAT A follower carries isSelf:0 (the target is NOT on the
// sender's seat). TranslateTargetOwners must map (isPlayerSeat:false, isSelf:0) -> the seat-A
// engine vid (ThisViewerId), so the attack resolves on seat A's follower — NOT seat B's own. The
// seat-A-sender M-HC-4c test proves the forward direction; this proves the mapping isn't
// accidentally symmetric (a translation that ignored isPlayerSeat would mis-route a seat-B frame).
//
// Driven via an ATTACK (no hand-identity dependency): seat A plays a 1/2 vanilla turn 1; seat B
// reveals a 1/2 vanilla turn 2 and on turn 4 attacks seat A's follower. Both are 1/2 so each
// survives the single trade and the life DROP (2 -> 1) is readable on the seat-A target.
var vanilla = NodeNativeBattleHarness.VanillaFollowerId; // 1/2
var seatADeck = Enumerable.Repeat(vanilla, 30).ToList();
var seatBDeck = Enumerable.Repeat(vanilla, 30).ToList();
using var harness = NodeNativeBattleHarness.Create(seatADeck: seatADeck, seatBDeck: seatBDeck);
// seat A turn 1: play a 1/2 onto seat A's board.
Assert.That(harness.Push(NetworkBattleUri.Deal, DealBody(), isPlayerSeat: true).Accepted, Is.True, "Deal");
Assert.That(harness.Push(NetworkBattleUri.Swap, SwapBody(), isPlayerSeat: true).Accepted, Is.True, "Swap");
Assert.That(harness.Push(NetworkBattleUri.Ready, ReadyBody(), isPlayerSeat: true).Accepted, Is.True, "Ready");
Assert.That(harness.Push(NetworkBattleUri.TurnStart, TurnStartBody(), isPlayerSeat: true).Accepted, Is.True, "turn1 TurnStart (A)");
Assert.That(harness.Push(NetworkBattleUri.PlayActions, PlayBody(1), isPlayerSeat: true).Accepted, Is.True, "turn1 play 1/2 (A)");
Assert.That(harness.BoardCount(playerSeat: true), Is.EqualTo(1), "one seat A follower on board");
int targetIdx = harness.InPlayCardIndex(playerSeat: true, boardPos: 0);
int targetLifeBefore = harness.InPlayCardLife(playerSeat: true, boardPos: 0);
Assert.That(targetLifeBefore, Is.EqualTo(2), "seat A 1/2 at full life before the attack");
// seat B turn 2: reveal a 1/2 onto seat B's board (so it exists; it gains summoning sickness).
Assert.That(harness.Push(NetworkBattleUri.TurnEnd, TurnEndBody(), isPlayerSeat: true).Accepted, Is.True, "turn1 TurnEnd (A)");
Assert.That(harness.Push(NetworkBattleUri.TurnStart, TurnStartBody(), isPlayerSeat: false).Accepted, Is.True, "turn2 TurnStart (B)");
Assert.That(harness.Push(NetworkBattleUri.PlayActions, RevealPlayBody(idx: 1, cardId: vanilla), isPlayerSeat: false).Accepted,
Is.True, "seat B reveal-play 1/2");
Assert.That(harness.BoardCount(playerSeat: false), Is.EqualTo(1), "one seat B follower on board");
int attackerIdx = harness.InPlayCardIndex(playerSeat: false, boardPos: 0);
// advance to seat B's NEXT turn (turn 4) so seat B's follower is past summoning sickness.
Assert.That(harness.Push(NetworkBattleUri.TurnEnd, TurnEndBody(), isPlayerSeat: false).Accepted, Is.True, "turn2 TurnEnd (B)");
Assert.That(harness.Push(NetworkBattleUri.TurnStart, TurnStartBody(), isPlayerSeat: true).Accepted, Is.True, "turn3 TurnStart (A)");
Assert.That(harness.Push(NetworkBattleUri.TurnEnd, TurnEndBody(), isPlayerSeat: true).Accepted, Is.True, "turn3 TurnEnd (A)");
Assert.That(harness.Push(NetworkBattleUri.TurnStart, TurnStartBody(), isPlayerSeat: false).Accepted, Is.True, "turn4 TurnStart (B)");
Assert.That(harness.InPlayCardAttackable(playerSeat: false, boardPos: 0), Is.True, "seat B attacker past summoning sickness");
// isPlayerSeat:false (seat B sends), targetOnEnemySeat:true -> isSelf:0 -> the SEAT-A engine vid.
var attack = harness.Push(
NetworkBattleUri.PlayActions,
NodeNativeBattleHarness.AttackBody(attackerIdx, targetIdx, targetOnEnemySeat: true),
isPlayerSeat: false);
Assert.That(attack.Accepted, Is.True, $"seat-B attack rejected: {attack.RejectReason}");
// The attack resolved onto the SEAT A follower (the reversed-perspective owner mapping worked):
// the 1/2 target took 1 -> life 1.
Assert.That(harness.InPlayCardLife(playerSeat: true, boardPos: 0), Is.EqualTo(targetLifeBefore - 1),
"the seat-A target took the attack's damage (isPlayerSeat:false, isSelf:0 -> seat A vid)");
}
[Test]
public void Attack_with_wrong_owner_flag_does_not_hit_the_enemy_follower()
{
// Negative / wrong-owner discriminator: seat A attacks but the targetList flags the target as the
// SENDER's OWN (targetOnEnemySeat:false -> isSelf:1 -> the seat-A engine vid), while pointing at the
// index where the ENEMY (seat B) follower sits. The translation must route that to seat A, so seat
// B's follower is NOT hit — proving the owner mapping is directional, not "hit whatever sits at the
// idx". (Mirrors the M-HC-4c target-discriminating pattern, on the OWNER axis.)
var oneOne = NodeNativeBattleHarness.VanillaOneOneFollowerId;
var seatADeck = Enumerable.Repeat(oneOne, 30).ToList();
var seatBDeck = Enumerable.Repeat(oneOne, 30).ToList();
using var harness = NodeNativeBattleHarness.Create(seatADeck: seatADeck, seatBDeck: seatBDeck);
// seat A turn 1: play a 1/1.
Assert.That(harness.Push(NetworkBattleUri.Deal, DealBody(), isPlayerSeat: true).Accepted, Is.True, "Deal");
Assert.That(harness.Push(NetworkBattleUri.Swap, SwapBody(), isPlayerSeat: true).Accepted, Is.True, "Swap");
Assert.That(harness.Push(NetworkBattleUri.Ready, ReadyBody(), isPlayerSeat: true).Accepted, Is.True, "Ready");
Assert.That(harness.Push(NetworkBattleUri.TurnStart, TurnStartBody(), isPlayerSeat: true).Accepted, Is.True, "turn1 TurnStart");
Assert.That(harness.Push(NetworkBattleUri.PlayActions, PlayBody(1), isPlayerSeat: true).Accepted, Is.True, "turn1 play 1/1");
int attackerIdx = harness.InPlayCardIndex(playerSeat: true, boardPos: 0);
// seat B turn 2: reveal a 1/1 onto seat B's board.
Assert.That(harness.Push(NetworkBattleUri.TurnEnd, TurnEndBody(), isPlayerSeat: true).Accepted, Is.True, "turn1 TurnEnd");
Assert.That(harness.Push(NetworkBattleUri.TurnStart, TurnStartBody(), isPlayerSeat: false).Accepted, Is.True, "turn2 TurnStart (B)");
Assert.That(harness.Push(NetworkBattleUri.PlayActions, RevealPlayBody(idx: 1, cardId: oneOne), isPlayerSeat: false).Accepted,
Is.True, "seat B reveal-play 1/1");
int enemyIdx = harness.InPlayCardIndex(playerSeat: false, boardPos: 0);
int enemyLifeBefore = harness.InPlayCardLife(playerSeat: false, boardPos: 0);
// back to seat A (turn 3): attacker past summoning sickness.
Assert.That(harness.Push(NetworkBattleUri.TurnEnd, TurnEndBody(), isPlayerSeat: false).Accepted, Is.True, "turn2 TurnEnd (B)");
Assert.That(harness.Push(NetworkBattleUri.TurnStart, TurnStartBody(), isPlayerSeat: true).Accepted, Is.True, "turn3 TurnStart (A)");
Assert.That(harness.InPlayCardAttackable(playerSeat: true, boardPos: 0), Is.True, "attacker past summoning sickness");
// WRONG owner: targetOnEnemySeat:false (isSelf:1) but pointing at the enemy follower's idx. The
// attack resolves against seat A's own space, so seat B's follower is NOT damaged.
harness.Push(
NetworkBattleUri.PlayActions,
NodeNativeBattleHarness.AttackBody(attackerIdx, targetIdx: enemyIdx, targetOnEnemySeat: false),
isPlayerSeat: true);
Assert.That(harness.InPlayCardLife(playerSeat: false, boardPos: 0), Is.EqualTo(enemyLifeBefore),
"the enemy follower must be UNTOUCHED when the attack flags the target as the sender's own (wrong owner)");
}
[Test]
public void Choice_play_resolves_chosen_branch_on_engine_state_headless()
{