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:
@@ -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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user