2058 lines
71 KiB
C#
2058 lines
71 KiB
C#
using System.Text.Json;
|
||
using Vanguard.Rules.Cards;
|
||
using Vanguard.Rules.Engine;
|
||
using Vanguard.Rules.Engine.Abilities;
|
||
using Vanguard.Rules.Replay;
|
||
using Xunit;
|
||
|
||
namespace Vanguard.Rules.Tests;
|
||
|
||
public class GameEngineTests
|
||
{
|
||
private readonly CardCatalog _catalog = TestCatalog.Instance;
|
||
|
||
private const string Barcgal = "BT01/003EN"; // grade 0, без триггера — стартовый Авангард
|
||
private const string Marron = "BT01/042EN"; // grade 1, 8000
|
||
private const string Gallatin = "BT01/021EN"; // grade 2, 10000
|
||
private const string Alfred = "BT01/001EN"; // grade 3
|
||
private const string Nike = "BT01/056EN"; // Critical
|
||
private const string Flogal = "BT01/046EN"; // Stand
|
||
private const string Govannon = "BT01/045EN"; // Draw
|
||
private const string Iseult = "BT01/011EN"; // Sentinel, Royal Paladin
|
||
private const string Nehalem = "BT01/022EN"; // Kagero
|
||
private const string Overlord = "BT01/004EN"; // Kagero, 11000
|
||
private const string Conroe = "BT01/016EN"; // Kagero, grade 0 без триггера
|
||
private const string BlasterBlade = "BT01/002EN";
|
||
private const string Wingal = "BT01/044EN";
|
||
private const string Lien = "BT01/043EN"; // activated: rest + discard → draw
|
||
private const string Cocoa = "BT01/007EN"; // OTT, смотрит верхнюю карту
|
||
private const string Libra = "BT01/017EN"; // OTT, grade 2
|
||
private const string Deathrex = "BT01/033EN"; // Tachikaze, 10000
|
||
private const string DemonEater = "BT01/036EN"; // Dark Irregulars, 10000
|
||
private const string Morgana = "TD01/011EN"; // Royal Paladin, grade 1, discard +4000
|
||
private const string Mocha = "BT01/018EN"; // OTT, +3000 при четырёх картах в руке
|
||
private const string HellSpider = "BT01/039EN"; // Megacolony, запрет вставать
|
||
private const string BrutalJack = "BT01/029EN"; // Nova Grappler, Сдерживание
|
||
private const string ToughBoy = "BT01/060EN"; // Nova Grappler, grade 1
|
||
private const string Asura = "BT01/008EN"; // Nova Grappler, grade 3
|
||
private const string Juggernaut = "BT01/020EN"; // Spike Brothers, возврат в колоду
|
||
private const string RockTheWall = "BT01/073EN";
|
||
private const string GuidingZombie = "BT01/078EN";
|
||
private const string Blightops = "BT01/034EN";
|
||
private const string MonsterFrank = "BT01/037EN";
|
||
private const string Shieldon = "BT01/067EN";
|
||
private const string Dka = "BT01/014EN";
|
||
private const string Bahr = "BT01/048EN";
|
||
private const string Tahr = "BT01/024EN";
|
||
private const string AlephVictory = "BT01/005EN";
|
||
private const string Gancelot = "BT01/010EN";
|
||
private const string Voidmaster = "BT01/035EN";
|
||
private const string BlasterBladePrint = "BT01/002EN";
|
||
private const string Bors = "TD01/002EN";
|
||
private const string Unicorn = "TD01/010EN";
|
||
private const string Goku = "TD02/002EN";
|
||
private const string Joka = "TD02/010EN";
|
||
private const string Tejas = "BT01/023EN";
|
||
private const string Yaksha = "TD02/003EN";
|
||
private const string Hagakure = "BT01/070EN";
|
||
private const string Chigasumi = "BT01/068EN";
|
||
|
||
[Fact]
|
||
public void Start_Places_Starting_Vanguard_And_Draws_Five()
|
||
{
|
||
var engine = NewEngine();
|
||
engine.Start();
|
||
|
||
var player = engine.State.Players[0];
|
||
Assert.NotNull(player.Vanguard);
|
||
Assert.Equal(0, _catalog.CardOf(player.Vanguard!.PrintingId).Grade);
|
||
Assert.Equal(TriggerType.None, _catalog.CardOf(player.Vanguard.PrintingId).Trigger);
|
||
Assert.Equal(GameEngine.StartingHand, player.Hand.Count);
|
||
Assert.Equal(44, player.Deck.Count);
|
||
Assert.Equal(Phase.Mulligan, engine.State.Phase);
|
||
}
|
||
|
||
[Fact]
|
||
public void Ride_Rejects_Grade_Jump()
|
||
{
|
||
var engine = StartedEngine();
|
||
Give(engine, 0, Alfred);
|
||
|
||
var result = engine.Execute(new RideCommand(Alfred));
|
||
|
||
Assert.False(result.Accepted);
|
||
Assert.Equal(Conroe, engine.State.Players[0].Vanguard!.PrintingId);
|
||
}
|
||
|
||
[Fact]
|
||
public void Ride_Moves_Previous_Vanguard_To_Soul()
|
||
{
|
||
var engine = StartedEngine();
|
||
Give(engine, 0, Marron);
|
||
|
||
var result = engine.Execute(new RideCommand(Marron));
|
||
|
||
Assert.True(result.Accepted);
|
||
Assert.Equal(Marron, engine.State.Players[0].Vanguard!.PrintingId);
|
||
Assert.Contains(engine.State.Players[0].Soul, c => c.PrintingId == Conroe);
|
||
Assert.Equal(Phase.Main, engine.State.Phase);
|
||
}
|
||
|
||
[Fact]
|
||
public void Called_RearGuard_Enters_Rested_And_Slot_Is_Busy()
|
||
{
|
||
var engine = InMainPhase();
|
||
Give(engine, 0, Marron);
|
||
|
||
Assert.True(engine.Execute(new CallCommand(Marron, 0)).Accepted);
|
||
Assert.True(engine.State.Players[0].RearGuards[0]!.Rested);
|
||
|
||
Give(engine, 0, Marron);
|
||
var second = engine.Execute(new CallCommand(Marron, 0));
|
||
|
||
Assert.False(second.Accepted);
|
||
}
|
||
|
||
[Fact]
|
||
public void Attack_Without_Guard_Deals_Damage()
|
||
{
|
||
var engine = ReadyToAttack();
|
||
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
Assert.Single(engine.State.Players[1].Damage);
|
||
Assert.Equal(Step.None, engine.State.Step);
|
||
}
|
||
|
||
[Fact]
|
||
public void Guard_Prevents_Damage()
|
||
{
|
||
var engine = ReadyToAttack();
|
||
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||
Give(engine, 1, Barcgal);
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||
Assert.True(engine.Execute(new GuardCommand(Barcgal)).Accepted);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
Assert.Empty(engine.State.Players[1].Damage);
|
||
Assert.Single(engine.State.Players[1].Drop);
|
||
}
|
||
|
||
[Fact]
|
||
public void Sixth_Damage_Ends_The_Game()
|
||
{
|
||
var engine = ReadyToAttack();
|
||
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||
|
||
var defender = engine.State.Players[1];
|
||
for (var i = 0; i < GameEngine.MaxDamage - 1; i++)
|
||
defender.Damage.Add(TakeAnyFromDeck(defender));
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
Assert.True(engine.State.IsFinished);
|
||
Assert.Equal(0, engine.State.Winner);
|
||
Assert.Contains(engine.Events, e => e is GameFinishedEvent);
|
||
}
|
||
|
||
[Fact]
|
||
public void Critical_Trigger_Grants_Power_And_Crit_To_Chosen_Unit()
|
||
{
|
||
var engine = ReadyToAttack();
|
||
StackDeck(engine.State.Players[0], Nike, Gallatin);
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
Assert.NotNull(engine.State.PendingTrigger);
|
||
Assert.Equal(Step.Trigger, engine.State.Step);
|
||
|
||
Assert.True(engine.Execute(new ResolveTriggerCommand(-1, -1)).Accepted);
|
||
|
||
Assert.Null(engine.State.PendingTrigger);
|
||
Assert.Equal(2, engine.State.Players[1].Damage.Count);
|
||
}
|
||
|
||
[Fact]
|
||
public void Stand_Trigger_Stands_Chosen_RearGuard()
|
||
{
|
||
var engine = ReadyToAttackWithRearGuard();
|
||
StackDeck(engine.State.Players[0], Flogal, Gallatin);
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
Assert.NotNull(engine.State.PendingTrigger);
|
||
|
||
var rearGuard = engine.State.Players[0].RearGuards[0]!;
|
||
Assert.True(rearGuard.Rested);
|
||
|
||
Assert.True(engine.Execute(new ResolveTriggerCommand(-1, 0)).Accepted);
|
||
Assert.False(rearGuard.Rested);
|
||
}
|
||
|
||
[Fact]
|
||
public void Damage_Trigger_Also_Asks_For_Targets_And_Applies_Effect()
|
||
{
|
||
var engine = ReadyToAttack();
|
||
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||
StackDeck(engine.State.Players[1], Nike);
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
var pending = engine.State.PendingTrigger;
|
||
Assert.NotNull(pending);
|
||
Assert.Equal(TriggerContext.DamageCheck, pending!.Context);
|
||
|
||
// до разрешения карта лежит в зоне триггера, а не в уроне
|
||
Assert.Empty(engine.State.Players[1].Damage);
|
||
Assert.Contains(engine.State.Players[1].Trigger, c => c.PrintingId == Nike);
|
||
|
||
Assert.True(engine.Execute(new ResolveTriggerCommand(-1, -1)).Accepted);
|
||
|
||
Assert.Null(engine.State.PendingTrigger);
|
||
Assert.Null(engine.State.Battle);
|
||
Assert.Single(engine.State.Players[1].Damage);
|
||
Assert.Empty(engine.State.Players[1].Trigger);
|
||
Assert.Contains(engine.State.Players[1].Damage, c => c.PrintingId == Nike);
|
||
Assert.Contains(engine.Events, e => e is TriggerAppliedEvent { Player: 1 });
|
||
}
|
||
|
||
[Fact]
|
||
public void Trigger_Power_Lasts_Until_End_Of_Turn()
|
||
{
|
||
var engine = ReadyToAttack();
|
||
StackDeck(engine.State.Players[0], Nike, Gallatin);
|
||
|
||
engine.Execute(new AttackCommand(-1, -1));
|
||
engine.Execute(new PassGuardCommand());
|
||
engine.Execute(new ResolveTriggerCommand(-1, -1));
|
||
|
||
var vanguard = engine.State.Players[0].Vanguard!;
|
||
Assert.Equal(GameEngine.TriggerPowerBonus, vanguard.Modifiers.Sum(m => m.Power));
|
||
|
||
Assert.True(engine.Execute(new EndTurnCommand()).Accepted);
|
||
Assert.Empty(vanguard.Modifiers);
|
||
}
|
||
|
||
[Fact]
|
||
public void Boost_Adds_Back_Row_Power_To_Attacker()
|
||
{
|
||
var engine = ReadyToBoost();
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(3, -1, 0)).Accepted);
|
||
Assert.True(engine.State.Players[0].RearGuards[0]!.Rested);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
Assert.Single(engine.State.Players[1].Damage);
|
||
Assert.Contains(engine.Events, e => e is BoostedEvent { BoosterSlot: 0, AttackerSlot: 3 });
|
||
}
|
||
|
||
[Fact]
|
||
public void Without_Boost_The_Same_Attack_Misses()
|
||
{
|
||
var engine = ReadyToBoost();
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(3, -1)).Accepted);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
Assert.Empty(engine.State.Players[1].Damage);
|
||
Assert.False(engine.State.Players[0].RearGuards[0]!.Rested);
|
||
}
|
||
|
||
[Fact]
|
||
public void Boost_Rejects_Wrong_Column()
|
||
{
|
||
var engine = ReadyToBoost();
|
||
|
||
var result = engine.Execute(new AttackCommand(3, -1, 2));
|
||
|
||
Assert.False(result.Accepted);
|
||
}
|
||
|
||
[Fact]
|
||
public void Grade_Two_Cannot_Boost()
|
||
{
|
||
var engine = ReadyToBoost();
|
||
|
||
engine.State.Players[0].RearGuards[0] = TakeFromDeck(engine.State.Players[0], Gallatin);
|
||
|
||
var result = engine.Execute(new AttackCommand(3, -1, 0));
|
||
|
||
Assert.False(result.Accepted);
|
||
}
|
||
|
||
[Fact]
|
||
public void Effect_Can_Grant_Boost_To_Grade_Two()
|
||
{
|
||
var engine = ReadyToBoost();
|
||
var booster = TakeFromDeck(engine.State.Players[0], Gallatin);
|
||
engine.State.Players[0].RearGuards[0] = booster;
|
||
|
||
Assert.False(engine.Execute(new AttackCommand(3, -1, 0)).Accepted);
|
||
|
||
booster.BoostOverride = true;
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(3, -1, 0)).Accepted);
|
||
}
|
||
|
||
[Fact]
|
||
public void Effect_Can_Remove_Boost_From_Grade_One()
|
||
{
|
||
var engine = ReadyToBoost();
|
||
var booster = engine.State.Players[0].RearGuards[0]!;
|
||
|
||
booster.BoostOverride = false;
|
||
|
||
Assert.False(engine.Execute(new AttackCommand(3, -1, 0)).Accepted);
|
||
}
|
||
|
||
[Fact]
|
||
public void Perfect_Guard_Nullifies_Attack_When_Cost_Paid()
|
||
{
|
||
var engine = ReadyToAttack();
|
||
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||
Give(engine, 1, Iseult);
|
||
Give(engine, 1, Marron);
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||
Assert.True(engine.Execute(new GuardCommand(Iseult, Marron)).Accepted);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
Assert.Empty(engine.State.Players[1].Damage);
|
||
Assert.Contains(engine.State.Players[1].Drop, c => c.PrintingId == Marron);
|
||
Assert.Contains(engine.Events, e => e is PerfectGuardEvent);
|
||
}
|
||
|
||
[Fact]
|
||
public void Perfect_Guard_Without_Cost_Adds_No_Shield()
|
||
{
|
||
var engine = ReadyToAttack();
|
||
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||
Give(engine, 1, Iseult);
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||
Assert.True(engine.Execute(new GuardCommand(Iseult)).Accepted);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
Assert.Single(engine.State.Players[1].Damage);
|
||
Assert.DoesNotContain(engine.Events, e => e is PerfectGuardEvent);
|
||
}
|
||
|
||
[Fact]
|
||
public void Perfect_Guard_Rejects_Wrong_Clan_Cost()
|
||
{
|
||
var engine = ReadyToAttack();
|
||
Give(engine, 1, Iseult);
|
||
Give(engine, 1, Nehalem);
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||
Assert.False(engine.Execute(new GuardCommand(Iseult, Nehalem)).Accepted);
|
||
}
|
||
|
||
[Fact]
|
||
public void Intercept_Saves_Target_And_Retires_Interceptor()
|
||
{
|
||
var engine = ReadyToAttackRearGuard();
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(3, 4)).Accepted);
|
||
Assert.True(engine.Execute(new InterceptCommand(3)).Accepted);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
var defender = engine.State.Players[1];
|
||
Assert.NotNull(defender.RearGuards[4]); // цель выжила
|
||
Assert.Null(defender.RearGuards[3]); // перехватчик покинул круг
|
||
Assert.Contains(defender.Drop, c => c.PrintingId == Gallatin);
|
||
Assert.Contains(engine.Events, e => e is InterceptedEvent);
|
||
Assert.Contains(engine.Events, e => e is BattleResolvedEvent { Hit: false });
|
||
}
|
||
|
||
[Fact]
|
||
public void Without_Intercept_RearGuard_Is_Retired()
|
||
{
|
||
var engine = ReadyToAttackRearGuard();
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(3, 4)).Accepted);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
var defender = engine.State.Players[1];
|
||
Assert.Null(defender.RearGuards[4]);
|
||
Assert.Contains(defender.Drop, c => c.PrintingId == Marron);
|
||
Assert.Contains(engine.Events, e => e is UnitRetiredEvent);
|
||
}
|
||
|
||
[Fact]
|
||
public void Grade_One_Cannot_Intercept()
|
||
{
|
||
var engine = ReadyToAttackRearGuard();
|
||
var defender = engine.State.Players[1];
|
||
defender.RearGuards[3] = TakeFromDeck(defender, Marron);
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(3, 4)).Accepted);
|
||
Assert.False(engine.Execute(new InterceptCommand(3)).Accepted);
|
||
}
|
||
|
||
[Fact]
|
||
public void Back_Row_Cannot_Intercept_By_Default()
|
||
{
|
||
var engine = ReadyToAttackRearGuard();
|
||
var defender = engine.State.Players[1];
|
||
defender.RearGuards[1] = TakeFromDeck(defender, Gallatin);
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(3, 4)).Accepted);
|
||
Assert.False(engine.Execute(new InterceptCommand(1)).Accepted);
|
||
}
|
||
|
||
[Fact]
|
||
public void Effect_Can_Grant_Back_Row_Intercept()
|
||
{
|
||
var engine = ReadyToAttackRearGuard();
|
||
var defender = engine.State.Players[1];
|
||
var unit = TakeFromDeck(defender, Gallatin);
|
||
unit.InterceptOverride = true;
|
||
unit.InterceptFromBackRow = true;
|
||
defender.RearGuards[1] = unit;
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(3, 4)).Accepted);
|
||
Assert.True(engine.Execute(new InterceptCommand(1)).Accepted);
|
||
}
|
||
|
||
[Fact]
|
||
public void Intercept_Works_Against_Vanguard_Attack()
|
||
{
|
||
var engine = StartedEngine();
|
||
engine.Execute(new SkipRideCommand());
|
||
SetVanguard(engine, 0, Overlord);
|
||
// второй Kagero, чтобы не срабатывал штраф −2000
|
||
engine.State.Players[0].RearGuards[0] = TakeFromDeck(engine.State.Players[0], Nehalem);
|
||
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||
engine.Execute(new EnterBattleCommand());
|
||
|
||
var defender = engine.State.Players[1];
|
||
defender.RearGuards[3] = TakeFromDeck(defender, Gallatin);
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||
Assert.True(engine.Execute(new InterceptCommand(3)).Accepted);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
Assert.Empty(defender.Damage);
|
||
Assert.NotNull(defender.Vanguard);
|
||
Assert.Contains(defender.Drop, c => c.PrintingId == Gallatin);
|
||
Assert.Contains(engine.Events, e => e is InterceptorRetiredEvent);
|
||
}
|
||
|
||
[Fact]
|
||
public void Attacked_RearGuard_Cannot_Intercept_Itself()
|
||
{
|
||
var engine = ReadyToAttackRearGuard();
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(3, 4)).Accepted);
|
||
Assert.False(engine.Execute(new InterceptCommand(4)).Accepted);
|
||
}
|
||
|
||
[Fact]
|
||
public void Move_To_Empty_Circle_In_Same_Column()
|
||
{
|
||
var engine = InMainPhase();
|
||
Give(engine, 0, Marron);
|
||
engine.Execute(new CallCommand(Marron, 0));
|
||
|
||
Assert.True(engine.Execute(new MoveCommand(0, 3)).Accepted);
|
||
|
||
Assert.Null(engine.State.Players[0].RearGuards[0]);
|
||
Assert.NotNull(engine.State.Players[0].RearGuards[3]);
|
||
Assert.Contains(engine.Events, e => e is UnitMovedEvent { FromSlot: 0, ToSlot: 3 });
|
||
}
|
||
|
||
[Fact]
|
||
public void Move_Swaps_Occupied_Circles()
|
||
{
|
||
var engine = InMainPhase();
|
||
Give(engine, 0, Marron);
|
||
engine.Execute(new CallCommand(Marron, 0));
|
||
Give(engine, 0, Gallatin);
|
||
engine.Execute(new CallCommand(Gallatin, 3));
|
||
|
||
Assert.True(engine.Execute(new MoveCommand(0, 3)).Accepted);
|
||
|
||
Assert.Equal(Marron, engine.State.Players[0].RearGuards[3]!.PrintingId);
|
||
Assert.Equal(Gallatin, engine.State.Players[0].RearGuards[0]!.PrintingId);
|
||
}
|
||
|
||
[Fact]
|
||
public void Move_Rejects_Other_Column()
|
||
{
|
||
var engine = InMainPhase();
|
||
Give(engine, 0, Marron);
|
||
engine.Execute(new CallCommand(Marron, 0));
|
||
|
||
Assert.False(engine.Execute(new MoveCommand(0, 4)).Accepted);
|
||
}
|
||
|
||
[Fact]
|
||
public void Center_Back_Row_Has_No_Pair()
|
||
{
|
||
var engine = InMainPhase();
|
||
Give(engine, 0, Marron);
|
||
engine.Execute(new CallCommand(Marron, 1));
|
||
|
||
Assert.False(engine.Execute(new MoveCommand(1, 3)).Accepted);
|
||
Assert.False(engine.Execute(new MoveCommand(1, 0)).Accepted);
|
||
}
|
||
|
||
[Fact]
|
||
public void Move_Is_Available_Only_In_Main_Phase()
|
||
{
|
||
var engine = StartedEngine(); // фаза Смены
|
||
|
||
Assert.False(engine.Execute(new MoveCommand(0, 3)).Accepted);
|
||
}
|
||
|
||
[Fact]
|
||
public void Grade_One_Vanguard_Performs_One_Check()
|
||
{
|
||
var engine = ReadyToAttack();
|
||
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||
|
||
engine.Execute(new AttackCommand(-1, -1));
|
||
engine.Execute(new PassGuardCommand());
|
||
|
||
Assert.Single(engine.Events.OfType<DriveCheckedEvent>());
|
||
}
|
||
|
||
[Fact]
|
||
public void Grade_Three_Vanguard_Performs_Two_Checks()
|
||
{
|
||
var engine = StartedEngine();
|
||
engine.Execute(new SkipRideCommand());
|
||
SetVanguard(engine, 0, Overlord);
|
||
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||
engine.Execute(new EnterBattleCommand());
|
||
|
||
engine.Execute(new AttackCommand(-1, -1));
|
||
engine.Execute(new PassGuardCommand());
|
||
|
||
Assert.Equal(2, engine.Events.OfType<DriveCheckedEvent>().Count());
|
||
}
|
||
|
||
[Fact]
|
||
public void Twin_Drive_Can_Be_Removed_By_Effect()
|
||
{
|
||
var engine = StartedEngine();
|
||
engine.Execute(new SkipRideCommand());
|
||
SetVanguard(engine, 0, Overlord);
|
||
engine.State.Players[0].Vanguard!.DriveChecksOverride = 1;
|
||
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||
engine.Execute(new EnterBattleCommand());
|
||
|
||
engine.Execute(new AttackCommand(-1, -1));
|
||
engine.Execute(new PassGuardCommand());
|
||
|
||
Assert.Single(engine.Events.OfType<DriveCheckedEvent>());
|
||
}
|
||
|
||
[Fact]
|
||
public void Rear_Guard_Performs_No_Checks_By_Default()
|
||
{
|
||
var engine = ReadyToAttackRearGuard();
|
||
|
||
engine.Execute(new AttackCommand(3, 4));
|
||
engine.Execute(new PassGuardCommand());
|
||
|
||
Assert.Empty(engine.Events.OfType<DriveCheckedEvent>());
|
||
}
|
||
|
||
[Fact]
|
||
public void Effect_Can_Give_Drive_Check_To_Rear_Guard()
|
||
{
|
||
var engine = ReadyToAttackRearGuard();
|
||
engine.State.Players[0].RearGuards[3]!.DriveChecksOverride = 1;
|
||
StackDeck(engine.State.Players[0], Gallatin);
|
||
|
||
engine.Execute(new AttackCommand(3, 4));
|
||
engine.Execute(new PassGuardCommand());
|
||
|
||
Assert.Single(engine.Events.OfType<DriveCheckedEvent>());
|
||
}
|
||
|
||
[Fact]
|
||
public void Barcgal_Offers_Optional_Call_When_Ridden_Over()
|
||
{
|
||
var engine = StartedEngine();
|
||
SetVanguard(engine, 0, Barcgal);
|
||
Give(engine, 0, Marron);
|
||
|
||
Assert.True(engine.Execute(new RideCommand(Marron)).Accepted);
|
||
Assert.NotNull(engine.State.PendingAbility);
|
||
Assert.Equal(Step.Ability, engine.State.Step);
|
||
|
||
Assert.True(engine.Execute(new ResolveAbilityCommand(true)).Accepted);
|
||
|
||
Assert.Null(engine.State.PendingAbility);
|
||
Assert.Contains(engine.State.Players[0].RearGuards, u => u?.PrintingId == Barcgal);
|
||
Assert.DoesNotContain(engine.State.Players[0].Soul, c => c.PrintingId == Barcgal);
|
||
}
|
||
|
||
[Fact]
|
||
public void Barcgal_Decline_Keeps_Card_In_Soul()
|
||
{
|
||
var engine = StartedEngine();
|
||
SetVanguard(engine, 0, Barcgal);
|
||
Give(engine, 0, Marron);
|
||
|
||
engine.Execute(new RideCommand(Marron));
|
||
Assert.True(engine.Execute(new ResolveAbilityCommand(false)).Accepted);
|
||
|
||
Assert.Contains(engine.State.Players[0].Soul, c => c.PrintingId == Barcgal);
|
||
Assert.DoesNotContain(engine.State.Players[0].RearGuards, u => u?.PrintingId == Barcgal);
|
||
}
|
||
|
||
[Fact]
|
||
public void Wingal_Gives_BlasterBlade_4000_During_Battle()
|
||
{
|
||
var engine = ReadyToAttackWithWingal(BlasterBlade);
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(3, -1, 0)).Accepted);
|
||
Assert.Equal(4000, engine.State.Players[0].RearGuards[3]!.Modifiers.Where(m => m.EffectId == "ability").Sum(m => m.Power));
|
||
}
|
||
|
||
[Fact]
|
||
public void Wingal_Does_Not_Boost_Other_Units()
|
||
{
|
||
var engine = ReadyToAttackWithWingal(Gallatin);
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(3, -1, 0)).Accepted);
|
||
Assert.Equal(0, engine.State.Players[0].RearGuards[3]!.Modifiers.Where(m => m.EffectId == "ability").Sum(m => m.Power));
|
||
}
|
||
|
||
[Fact]
|
||
public void Alfred_Gets_2000_Per_RoyalPaladin_RearGuard()
|
||
{
|
||
var engine = StartedEngine();
|
||
engine.Execute(new SkipRideCommand());
|
||
SetVanguard(engine, 0, Alfred);
|
||
|
||
Give(engine, 0, Marron);
|
||
engine.Execute(new CallCommand(Marron, 0));
|
||
Give(engine, 0, Marron);
|
||
engine.Execute(new CallCommand(Marron, 3));
|
||
|
||
var alfred = engine.State.Players[0].Vanguard!;
|
||
Assert.Equal(14000, engine.EffectivePower(alfred));
|
||
}
|
||
|
||
[Fact]
|
||
public void Guard_Protects_RearGuard_From_Retirement()
|
||
{
|
||
var engine = ReadyToAttackRearGuard();
|
||
Give(engine, 1, Barcgal);
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(3, 4)).Accepted);
|
||
Assert.True(engine.Execute(new GuardCommand(Barcgal)).Accepted);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
var defender = engine.State.Players[1];
|
||
Assert.NotNull(defender.RearGuards[4]);
|
||
Assert.Contains(defender.Drop, c => c.PrintingId == Barcgal);
|
||
Assert.DoesNotContain(engine.Events, e => e is UnitRetiredEvent);
|
||
}
|
||
|
||
[Fact]
|
||
public void BlasterBlade_On_Vanguard_Retires_Chosen_RearGuard()
|
||
{
|
||
var engine = StartedEngine();
|
||
SetVanguard(engine, 0, Marron);
|
||
GiveDamage(engine, 0, 2);
|
||
|
||
var opponent = engine.State.Players[1];
|
||
opponent.RearGuards[0] = TakeFromDeck(opponent, Gallatin);
|
||
|
||
Give(engine, 0, BlasterBlade);
|
||
Assert.True(engine.Execute(new RideCommand(BlasterBlade)).Accepted);
|
||
|
||
Assert.NotNull(engine.State.PendingAbility);
|
||
Assert.True(engine.Execute(new ResolveAbilityCommand(true)).Accepted);
|
||
|
||
Assert.NotNull(engine.State.PendingChoice);
|
||
Assert.Single(engine.State.PendingChoice!.Options);
|
||
|
||
Assert.True(engine.Execute(new ChooseCommand(0)).Accepted);
|
||
|
||
Assert.Null(opponent.RearGuards[0]);
|
||
Assert.Equal(2, engine.State.Players[0].Damage.Count(c => c.FaceUp));
|
||
}
|
||
|
||
[Fact]
|
||
public void BlasterBlade_Is_Not_Offered_Without_Counter_Blast()
|
||
{
|
||
var engine = StartedEngine();
|
||
SetVanguard(engine, 0, Marron);
|
||
Give(engine, 0, BlasterBlade);
|
||
|
||
Assert.True(engine.Execute(new RideCommand(BlasterBlade)).Accepted);
|
||
|
||
Assert.Null(engine.State.PendingAbility);
|
||
}
|
||
|
||
[Fact]
|
||
public void BlasterBlade_On_RearGuard_Requires_RoyalPaladin_Vanguard()
|
||
{
|
||
var kagero = StartedEngine();
|
||
kagero.Execute(new SkipRideCommand());
|
||
Give(kagero, 0, BlasterBlade);
|
||
kagero.Execute(new CallCommand(BlasterBlade, 0));
|
||
|
||
Assert.Null(kagero.State.PendingAbility);
|
||
|
||
var royal = StartedEngine();
|
||
SetVanguard(royal, 0, Marron);
|
||
royal.Execute(new SkipRideCommand());
|
||
GiveDamage(royal, 0, 2);
|
||
Give(royal, 0, BlasterBlade);
|
||
royal.Execute(new CallCommand(BlasterBlade, 0));
|
||
|
||
Assert.NotNull(royal.State.PendingAbility);
|
||
}
|
||
|
||
[Fact]
|
||
public void Activated_Ability_Rests_Self_Discards_And_Draws()
|
||
{
|
||
var engine = InMainPhase();
|
||
Give(engine, 0, Lien);
|
||
engine.Execute(new CallCommand(Lien, 0));
|
||
|
||
var lien = engine.State.Players[0].RearGuards[0]!;
|
||
lien.Rested = false;
|
||
|
||
Give(engine, 0, Marron);
|
||
var handBefore = engine.State.Players[0].Hand.Count;
|
||
|
||
var legal = engine.LegalCommands();
|
||
Assert.Contains(legal, command =>
|
||
command is ActivateAbilityCommand activate
|
||
&& activate.InstanceId == lien.Id
|
||
&& activate.DiscardPrintingId == Marron);
|
||
Assert.DoesNotContain(legal, command =>
|
||
command is ActivateAbilityCommand activate
|
||
&& activate.InstanceId == lien.Id
|
||
&& activate.DiscardPrintingId is null);
|
||
|
||
Assert.True(engine.Execute(new ActivateAbilityCommand(lien.Id, Marron)).Accepted);
|
||
|
||
Assert.True(lien.Rested);
|
||
Assert.Contains(engine.State.Players[0].Drop, c => c.PrintingId == Marron);
|
||
Assert.Equal(handBefore, engine.State.Players[0].Hand.Count); // -1 сброс, +1 добор
|
||
}
|
||
|
||
[Fact]
|
||
public void Activated_Ability_Requires_Discard()
|
||
{
|
||
var engine = InMainPhase();
|
||
Give(engine, 0, Lien);
|
||
engine.Execute(new CallCommand(Lien, 0));
|
||
var lien = engine.State.Players[0].RearGuards[0]!;
|
||
lien.Rested = false;
|
||
|
||
Assert.False(engine.Execute(new ActivateAbilityCommand(lien.Id)).Accepted);
|
||
}
|
||
|
||
[Fact]
|
||
public void Activated_Ability_Rejects_Rested_Card()
|
||
{
|
||
var engine = InMainPhase();
|
||
Give(engine, 0, Lien);
|
||
engine.Execute(new CallCommand(Lien, 0));
|
||
var lien = engine.State.Players[0].RearGuards[0]!;
|
||
Give(engine, 0, Marron);
|
||
|
||
Assert.False(engine.Execute(new ActivateAbilityCommand(lien.Id, Marron)).Accepted);
|
||
}
|
||
|
||
[Fact]
|
||
public void Search_Without_Matches_Only_Shuffles()
|
||
{
|
||
var engine = StartedEngine();
|
||
var handBefore = engine.State.Players[0].Hand.Count;
|
||
|
||
engine.SearchDeckToHand(0, "Карта X", d => d.NameId == "no-such-card");
|
||
|
||
Assert.Contains(engine.Events, e => e is SearchFailedEvent);
|
||
Assert.Null(engine.State.PendingChoice);
|
||
Assert.Equal(handBefore, engine.State.Players[0].Hand.Count);
|
||
}
|
||
|
||
[Fact]
|
||
public void Search_With_Single_Match_Puts_Card_Into_Hand()
|
||
{
|
||
var engine = StartedEngine();
|
||
KeepOnly(engine, 0, "wingal", 1);
|
||
|
||
engine.SearchDeckToHand(0, "Wingal", d => d.NameId == "wingal");
|
||
|
||
Assert.Null(engine.State.PendingChoice);
|
||
Assert.Contains(engine.State.Players[0].Hand, c => c.PrintingId == Wingal);
|
||
Assert.DoesNotContain(engine.State.Players[0].Deck, c => c.PrintingId == Wingal);
|
||
}
|
||
|
||
[Fact]
|
||
public void Search_With_Two_Arts_Offers_Two_Options()
|
||
{
|
||
var engine = StartedEngine();
|
||
KeepOnly(engine, 0, "blaster-blade", 1);
|
||
|
||
var parallel = new CardInstance { Id = 9001, PrintingId = "BT01/S02EN" };
|
||
engine.State.Players[0].Deck.Add(parallel);
|
||
|
||
engine.SearchDeckToHand(0, "Blaster Blade", d => d.NameId == "blaster-blade");
|
||
|
||
var choice = engine.State.PendingChoice;
|
||
Assert.NotNull(choice);
|
||
Assert.Equal(2, choice!.Options.Count);
|
||
|
||
Assert.True(engine.Execute(new ChooseCommand(1)).Accepted);
|
||
Assert.Contains(engine.State.Players[0].Hand, c => c.PrintingId == "BT01/S02EN");
|
||
}
|
||
|
||
[Fact]
|
||
public void Conroe_Retires_Itself_Pays_Counter_Blast_And_Searches()
|
||
{
|
||
var engine = InMainPhase();
|
||
Give(engine, 0, Conroe);
|
||
engine.Execute(new CallCommand(Conroe, 0));
|
||
|
||
var conroe = engine.State.Players[0].RearGuards[0]!;
|
||
conroe.Rested = false;
|
||
GiveDamage(engine, 0, 1);
|
||
|
||
Assert.True(engine.Execute(new ActivateAbilityCommand(conroe.Id)).Accepted);
|
||
|
||
Assert.Null(engine.State.Players[0].RearGuards[0]);
|
||
Assert.Contains(engine.State.Players[0].Drop, c => c.PrintingId == Conroe);
|
||
Assert.Equal(1, engine.State.Players[0].Damage.Count(c => c.FaceUp));
|
||
|
||
if (engine.State.PendingChoice is not null)
|
||
Assert.True(engine.Execute(new ChooseCommand(0)).Accepted);
|
||
|
||
var added = engine.State.Players[0].Hand[^1];
|
||
var def = _catalog.CardOf(added.PrintingId);
|
||
Assert.Equal(Clan.Kagero, def.Clan);
|
||
Assert.True(def.Grade <= 1);
|
||
}
|
||
|
||
[Fact]
|
||
public void Cocoa_Can_Keep_The_Top_Card_On_Top()
|
||
{
|
||
var engine = InMainPhase();
|
||
SetVanguard(engine, 0, Libra);
|
||
Give(engine, 0, Cocoa);
|
||
engine.Execute(new CallCommand(Cocoa, 0));
|
||
|
||
var choice = engine.State.PendingChoice;
|
||
Assert.NotNull(choice);
|
||
Assert.Equal(2, choice!.Options.Count);
|
||
|
||
var topBefore = engine.State.Players[0].Deck[0].PrintingId;
|
||
Assert.True(engine.Execute(new ChooseCommand(0)).Accepted);
|
||
|
||
Assert.Equal(topBefore, engine.State.Players[0].Deck[0].PrintingId);
|
||
}
|
||
|
||
[Fact]
|
||
public void Cocoa_Can_Put_The_Top_Card_On_Bottom()
|
||
{
|
||
var engine = InMainPhase();
|
||
SetVanguard(engine, 0, Libra);
|
||
Give(engine, 0, Cocoa);
|
||
engine.Execute(new CallCommand(Cocoa, 0));
|
||
|
||
var topBefore = engine.State.Players[0].Deck[0].PrintingId;
|
||
Assert.True(engine.Execute(new ChooseCommand(1)).Accepted);
|
||
|
||
Assert.Equal(topBefore, engine.State.Players[0].Deck[^1].PrintingId);
|
||
}
|
||
|
||
[Fact]
|
||
public void Alfred_Searches_And_Calls_A_RoyalPaladin()
|
||
{
|
||
var engine = StartedEngine();
|
||
engine.Execute(new SkipRideCommand());
|
||
SetVanguard(engine, 0, Alfred);
|
||
GiveDamage(engine, 0, 3);
|
||
|
||
var alfred = engine.State.Players[0].Vanguard!;
|
||
Assert.True(engine.Execute(new ActivateAbilityCommand(alfred.Id)).Accepted);
|
||
|
||
Assert.Equal(3, engine.State.Players[0].Damage.Count(c => c.FaceUp));
|
||
|
||
if (engine.State.PendingChoice is not null)
|
||
Assert.True(engine.Execute(new ChooseCommand(0)).Accepted);
|
||
|
||
if (engine.State.PendingChoice is not null)
|
||
Assert.True(engine.Execute(new ChooseCommand(0)).Accepted);
|
||
|
||
var called = engine.State.Players[0].RearGuards.FirstOrDefault(u => u is not null);
|
||
Assert.NotNull(called);
|
||
|
||
var def = _catalog.CardOf(called!.PrintingId);
|
||
Assert.Equal(Clan.RoyalPaladin, def.Clan);
|
||
Assert.True(def.Grade <= 2);
|
||
}
|
||
|
||
[Fact]
|
||
public void Search_Can_Ride_The_Found_Card()
|
||
{
|
||
var engine = InMainPhase();
|
||
|
||
engine.SearchDeckToRide(0, "Blaster Blade", d => d.NameId == "blaster-blade");
|
||
|
||
if (engine.State.PendingChoice is not null)
|
||
Assert.True(engine.Execute(new ChooseCommand(0)).Accepted);
|
||
|
||
var vanguard = engine.State.Players[0].Vanguard!;
|
||
Assert.Equal("blaster-blade", _catalog.CardOf(vanguard.PrintingId).NameId);
|
||
}
|
||
|
||
[Fact]
|
||
public void Deathrex_Gains_5000_When_Attacking()
|
||
{
|
||
var engine = StartedEngine();
|
||
engine.Execute(new SkipRideCommand());
|
||
SetVanguard(engine, 0, Deathrex);
|
||
engine.Execute(new EnterBattleCommand());
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||
|
||
Assert.Equal(5000, engine.State.Players[0].Vanguard!.Modifiers.Sum(m => m.Power));
|
||
}
|
||
|
||
[Fact]
|
||
public void DemonEater_Charges_Soul_At_Main_Phase_Start()
|
||
{
|
||
var engine = StartedEngine();
|
||
SetVanguard(engine, 0, DemonEater);
|
||
engine.Execute(new SkipRideCommand());
|
||
|
||
var vanguard = engine.State.Players[0].Vanguard!;
|
||
Assert.Single(engine.State.Players[0].Soul);
|
||
Assert.Equal(12000, engine.EffectivePower(vanguard));
|
||
}
|
||
|
||
[Fact]
|
||
public void MaidenOfLibra_Draws_On_Hit_When_Cost_Paid()
|
||
{
|
||
var engine = StartedEngine();
|
||
engine.Execute(new SkipRideCommand());
|
||
SetVanguard(engine, 0, Libra);
|
||
GiveDamage(engine, 0, 2);
|
||
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||
engine.Execute(new EnterBattleCommand());
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
Assert.NotNull(engine.State.PendingAbility);
|
||
var handBefore = engine.State.Players[0].Hand.Count;
|
||
|
||
Assert.True(engine.Execute(new ResolveAbilityCommand(true)).Accepted);
|
||
|
||
Assert.Equal(2, engine.State.Players[0].Damage.Count(c => c.FaceUp));
|
||
Assert.Equal(handBefore + 1, engine.State.Players[0].Hand.Count);
|
||
Assert.Single(engine.State.Players[1].Damage);
|
||
}
|
||
|
||
[Fact]
|
||
public void Attack_Hit_Effects_Run_After_Damage()
|
||
{
|
||
var engine = StartedEngine();
|
||
engine.Execute(new SkipRideCommand());
|
||
SetVanguard(engine, 0, Libra);
|
||
GiveDamage(engine, 0, 2);
|
||
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||
engine.Execute(new EnterBattleCommand());
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
// урон уже нанесён, и только теперь спрашивают про эффект попадания
|
||
Assert.Single(engine.State.Players[1].Damage);
|
||
Assert.NotNull(engine.State.PendingAbility);
|
||
}
|
||
|
||
[Fact]
|
||
public void Morgana_Discards_To_Gain_4000_When_Attacking()
|
||
{
|
||
var engine = StartedEngine();
|
||
engine.Execute(new SkipRideCommand());
|
||
SetVanguard(engine, 0, Morgana);
|
||
engine.Execute(new EnterBattleCommand());
|
||
|
||
var morgana = engine.State.Players[0].Vanguard!;
|
||
var handBefore = engine.State.Players[0].Hand.Count;
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||
Assert.NotNull(engine.State.PendingAbility);
|
||
|
||
Assert.True(engine.Execute(new ResolveAbilityCommand(true)).Accepted);
|
||
Assert.NotNull(engine.State.PendingChoice);
|
||
|
||
Assert.True(engine.Execute(new ChooseCommand(0)).Accepted);
|
||
|
||
Assert.Equal(4000, morgana.Modifiers.Sum(m => m.Power));
|
||
Assert.Equal(handBefore - 1, engine.State.Players[0].Hand.Count);
|
||
Assert.Equal(Step.Guard, engine.State.Step);
|
||
}
|
||
|
||
[Fact]
|
||
public void Battle_Modifiers_Expire_But_Turn_Modifiers_Stay()
|
||
{
|
||
var engine = StartedEngine();
|
||
SetVanguard(engine, 0, DemonEater);
|
||
engine.Execute(new SkipRideCommand());
|
||
|
||
var vanguard = engine.State.Players[0].Vanguard!;
|
||
Assert.Equal(2000, vanguard.Modifiers.Sum(m => m.Power));
|
||
|
||
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||
engine.Execute(new EnterBattleCommand());
|
||
engine.Execute(new AttackCommand(-1, -1));
|
||
engine.Execute(new PassGuardCommand());
|
||
|
||
Assert.Equal(2000, vanguard.Modifiers.Sum(m => m.Power));
|
||
|
||
engine.Execute(new EndTurnCommand());
|
||
Assert.Empty(vanguard.Modifiers);
|
||
}
|
||
|
||
[Fact]
|
||
public void Granted_Ability_Triggers()
|
||
{
|
||
var engine = StartedEngine();
|
||
engine.Execute(new SkipRideCommand());
|
||
SetVanguard(engine, 0, Gallatin);
|
||
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||
engine.Execute(new EnterBattleCommand());
|
||
|
||
var vanguard = engine.State.Players[0].Vanguard!;
|
||
engine.GrantAbility(
|
||
vanguard,
|
||
new AbilityDefinition
|
||
{
|
||
Trigger = AbilityTrigger.OnAttackDeclared,
|
||
Resolve = ctx => ctx.Engine.AddBattlePower(ctx.Source, 3000),
|
||
},
|
||
vanguard,
|
||
"test",
|
||
ModifierDuration.UntilEndOfTurn);
|
||
|
||
engine.Execute(new AttackCommand(-1, -1));
|
||
|
||
Assert.Equal(3000, vanguard.Modifiers.Where(m => m.EffectId == "ability").Sum(m => m.Power));
|
||
}
|
||
|
||
[Fact]
|
||
public void Granted_Ability_Can_Be_Removed()
|
||
{
|
||
var engine = StartedEngine();
|
||
engine.Execute(new SkipRideCommand());
|
||
SetVanguard(engine, 0, Gallatin);
|
||
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||
engine.Execute(new EnterBattleCommand());
|
||
|
||
var vanguard = engine.State.Players[0].Vanguard!;
|
||
engine.GrantAbility(
|
||
vanguard,
|
||
new AbilityDefinition
|
||
{
|
||
Trigger = AbilityTrigger.OnAttackDeclared,
|
||
Resolve = ctx => ctx.Engine.AddBattlePower(ctx.Source, 3000),
|
||
},
|
||
vanguard,
|
||
"test",
|
||
ModifierDuration.UntilEndOfTurn);
|
||
|
||
engine.RemoveGrantedAbilities(g => g.EffectId == "test");
|
||
engine.Execute(new AttackCommand(-1, -1));
|
||
|
||
Assert.Equal(0, vanguard.Modifiers.Where(m => m.EffectId == "ability").Sum(m => m.Power));
|
||
}
|
||
|
||
[Fact]
|
||
public void MultiChoice_Stops_When_Player_Says_Done()
|
||
{
|
||
var engine = StartedEngine();
|
||
var opponent = engine.State.Players[1];
|
||
opponent.RearGuards[0] = TakeFromDeck(opponent, Marron);
|
||
opponent.RearGuards[1] = TakeFromDeck(opponent, Gallatin);
|
||
opponent.RearGuards[3] = TakeFromDeck(opponent, Gallatin);
|
||
|
||
IReadOnlyList<UnitTarget>? result = null;
|
||
engine.AskUnitMultiChoice(
|
||
0,
|
||
"Выберите до двух",
|
||
new[] { new UnitTarget(1, 0), new UnitTarget(1, 1), new UnitTarget(1, 3) },
|
||
0,
|
||
2,
|
||
chosen => result = chosen);
|
||
|
||
Assert.Equal(Step.Choice, engine.State.Step);
|
||
Assert.Equal(4, engine.State.PendingChoice!.Options.Count); // три цели и «Готово»
|
||
|
||
engine.Execute(new ChooseCommand(0));
|
||
engine.Execute(new ChooseCommand(2)); // «Готово» среди двух оставшихся
|
||
|
||
Assert.NotNull(result);
|
||
Assert.Single(result!);
|
||
Assert.Equal(0, result![0].Slot);
|
||
Assert.Equal(Step.None, engine.State.Step);
|
||
}
|
||
|
||
[Fact]
|
||
public void MultiChoice_Resolves_Automatically_When_Max_Reached()
|
||
{
|
||
var engine = StartedEngine();
|
||
var opponent = engine.State.Players[1];
|
||
opponent.RearGuards[0] = TakeFromDeck(opponent, Marron);
|
||
opponent.RearGuards[1] = TakeFromDeck(opponent, Gallatin);
|
||
|
||
IReadOnlyList<UnitTarget>? result = null;
|
||
engine.AskUnitMultiChoice(
|
||
0,
|
||
"Выберите ровно две",
|
||
new[] { new UnitTarget(1, 0), new UnitTarget(1, 1) },
|
||
2,
|
||
2,
|
||
chosen => result = chosen);
|
||
|
||
Assert.Equal(2, engine.State.PendingChoice!.Options.Count); // две цели, «Готово» ещё нет
|
||
|
||
engine.Execute(new ChooseCommand(0));
|
||
Assert.Null(result); // после первой цели выбор ещё продолжается
|
||
engine.Execute(new ChooseCommand(0));
|
||
|
||
Assert.NotNull(result);
|
||
Assert.Equal(2, result!.Count);
|
||
}
|
||
|
||
[Fact]
|
||
public void CannotStand_Skips_Stand_And_Expires_After_Stand_Phase()
|
||
{
|
||
var engine = StartedEngine();
|
||
SetVanguard(engine, 0, Gallatin);
|
||
engine.Execute(new SkipRideCommand());
|
||
|
||
var vanguard = engine.State.Players[0].Vanguard!;
|
||
engine.AddRestriction(
|
||
vanguard,
|
||
RestrictionType.CannotStand,
|
||
vanguard,
|
||
"test",
|
||
ModifierDuration.UntilOwnerNextStand);
|
||
vanguard.Rested = true;
|
||
|
||
engine.Execute(new EndTurnCommand()); // ход переходит к игроку 1
|
||
engine.Execute(new SkipRideCommand()); // игрок 1 выходит в Основную фазу
|
||
engine.Execute(new EndTurnCommand()); // и обратно
|
||
|
||
Assert.True(vanguard.Rested);
|
||
Assert.Empty(vanguard.Restrictions);
|
||
}
|
||
|
||
[Fact]
|
||
public void CannotAttack_Restriction_Blocks_Attack()
|
||
{
|
||
var engine = ReadyToAttackRearGuard();
|
||
var attacker = engine.State.Players[0].RearGuards[3]!;
|
||
engine.AddRestriction(
|
||
attacker,
|
||
RestrictionType.CannotAttack,
|
||
attacker,
|
||
"test",
|
||
ModifierDuration.UntilEndOfTurn);
|
||
|
||
Assert.False(engine.Execute(new AttackCommand(3, 0)).Accepted);
|
||
}
|
||
|
||
[Fact]
|
||
public void Attack_Hit_On_RearGuard_Triggers_OnAttackHit()
|
||
{
|
||
var engine = ReadyToAttackRearGuard();
|
||
var attacker = engine.State.Players[0].RearGuards[3]!;
|
||
|
||
engine.GrantAbility(
|
||
attacker,
|
||
new AbilityDefinition
|
||
{
|
||
Trigger = AbilityTrigger.OnAttackHit,
|
||
Resolve = ctx => ctx.Engine.AddPower(
|
||
ctx.Source,
|
||
2000,
|
||
ctx.Source,
|
||
"hit",
|
||
ModifierDuration.UntilEndOfTurn),
|
||
},
|
||
attacker,
|
||
"test",
|
||
ModifierDuration.UntilEndOfTurn);
|
||
|
||
engine.Execute(new AttackCommand(3, 4));
|
||
engine.Execute(new PassGuardCommand());
|
||
|
||
Assert.Null(engine.State.Players[1].RearGuards[4]);
|
||
Assert.Equal(2000, attacker.Modifiers.Where(m => m.EffectId == "hit").Sum(m => m.Power));
|
||
}
|
||
|
||
[Fact]
|
||
public void Mocha_Gets_Power_When_Hand_Has_Four_Cards()
|
||
{
|
||
var engine = StartedEngine();
|
||
engine.Execute(new SkipRideCommand());
|
||
|
||
var mocha = MakeCard(Mocha);
|
||
engine.State.Players[0].Vanguard = mocha;
|
||
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||
|
||
Assert.True(engine.State.Players[0].Hand.Count >= 4);
|
||
|
||
engine.Execute(new EnterBattleCommand());
|
||
engine.Execute(new AttackCommand(-1, -1));
|
||
|
||
Assert.Equal(3000, mocha.Modifiers.Where(m => m.EffectId == "ability").Sum(m => m.Power));
|
||
}
|
||
|
||
[Fact]
|
||
public void HellSpider_Prevents_Chosen_RearGuard_From_Standing()
|
||
{
|
||
var engine = InMainPhase();
|
||
GiveDamage(engine, 0, 2);
|
||
|
||
var opponent = engine.State.Players[1];
|
||
var target = MakeCard(Gallatin);
|
||
opponent.RearGuards[0] = target;
|
||
|
||
var spider = MakeCard(HellSpider);
|
||
engine.State.Players[0].Hand.Add(spider);
|
||
|
||
Assert.True(engine.Execute(new CallCommand(HellSpider, 0)).Accepted);
|
||
Assert.NotNull(engine.State.PendingAbility);
|
||
Assert.True(engine.Execute(new ResolveAbilityCommand(true)).Accepted);
|
||
Assert.NotNull(engine.State.PendingChoice);
|
||
Assert.True(engine.Execute(new ChooseCommand(0)).Accepted);
|
||
|
||
Assert.True(engine.State.HasRestriction(target, RestrictionType.CannotStand));
|
||
Assert.Equal(2, engine.State.Players[0].Damage.Count(c => c.FaceUp));
|
||
}
|
||
|
||
[Fact]
|
||
public void BrutalJack_Loses_Restraint_And_Gets_Power_From_Nova_Grappler_Boost()
|
||
{
|
||
var engine = InMainPhase();
|
||
GiveDamage(engine, 0, 1);
|
||
|
||
var jack = MakeCard(BrutalJack);
|
||
engine.State.Players[0].Hand.Add(jack);
|
||
engine.Execute(new CallCommand(BrutalJack, 3));
|
||
|
||
var toughBoy = MakeCard(ToughBoy);
|
||
engine.State.Players[0].Hand.Add(toughBoy);
|
||
engine.Execute(new CallCommand(ToughBoy, 0));
|
||
|
||
jack.Rested = false;
|
||
toughBoy.Rested = false;
|
||
|
||
Assert.True(engine.State.IsRestraint(jack));
|
||
Assert.True(engine.Execute(new ActivateAbilityCommand(jack.Id)).Accepted);
|
||
Assert.False(engine.State.IsRestraint(jack));
|
||
|
||
Assert.True(engine.Execute(new EnterBattleCommand()).Accepted);
|
||
Assert.True(engine.Execute(new AttackCommand(3, -1, 0)).Accepted);
|
||
|
||
Assert.Equal(5000, jack.Modifiers.Where(m => m.EffectId == "ability").Sum(m => m.Power));
|
||
}
|
||
|
||
[Fact]
|
||
public void Overlord_Activation_Adds_Power_Removes_Twin_Drive_And_Stands_On_RearGuard_Hit()
|
||
{
|
||
var engine = InMainPhase();
|
||
GiveDamage(engine, 0, 3);
|
||
|
||
var overlord = MakeCard(Overlord);
|
||
engine.State.Players[0].Vanguard = overlord;
|
||
|
||
Assert.True(engine.Execute(new ActivateAbilityCommand(overlord.Id)).Accepted);
|
||
Assert.Equal(5000, overlord.Modifiers.Where(m => m.EffectId == "dragonic-overlord").Sum(m => m.Power));
|
||
Assert.Equal(1, overlord.DriveChecksOverride);
|
||
Assert.Single(overlord.GrantedAbilities);
|
||
|
||
StackDeck(engine.State.Players[0], Gallatin);
|
||
engine.Execute(new EnterBattleCommand());
|
||
|
||
var opponent = engine.State.Players[1];
|
||
opponent.RearGuards[4] = MakeCard(Marron);
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(-1, 4)).Accepted);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
Assert.False(overlord.Rested);
|
||
Assert.Null(opponent.RearGuards[4]);
|
||
|
||
engine.Execute(new EndTurnCommand());
|
||
Assert.Null(overlord.DriveChecksOverride);
|
||
Assert.Empty(overlord.GrantedAbilities);
|
||
}
|
||
|
||
[Fact]
|
||
public void Asura_Kaiser_Stands_RearGuard_On_Grade3_Drive_Check()
|
||
{
|
||
var engine = StartedEngine();
|
||
engine.Execute(new SkipRideCommand());
|
||
|
||
var asura = MakeCard(Asura);
|
||
engine.State.Players[0].Vanguard = asura;
|
||
|
||
var rear = MakeCard(ToughBoy);
|
||
engine.State.Players[0].RearGuards[0] = rear;
|
||
rear.Rested = true;
|
||
|
||
// проверка открывает Mr. Invincible (Nova Grappler 3 ранга), затем обычную карту
|
||
engine.State.Players[0].Deck.Insert(0, MakeCard("BT01/028EN"));
|
||
engine.State.Players[0].Deck.Insert(1, MakeCard(Gallatin));
|
||
engine.State.Players[1].Deck.Insert(0, MakeCard(Gallatin)); // проверка урона без триггера
|
||
|
||
engine.Execute(new EnterBattleCommand());
|
||
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
Assert.NotNull(engine.State.PendingChoice);
|
||
Assert.True(engine.Execute(new ChooseCommand(0)).Accepted);
|
||
|
||
Assert.False(rear.Rested);
|
||
}
|
||
|
||
[Fact]
|
||
public void Juggernaut_Returns_To_Deck_After_Battle()
|
||
{
|
||
var engine = InMainPhase();
|
||
var juggernaut = MakeCard(Juggernaut);
|
||
engine.State.Players[0].RearGuards[3] = juggernaut;
|
||
juggernaut.Rested = false;
|
||
engine.SoulCharge(0, 1);
|
||
|
||
StackDeck(engine.State.Players[1], Gallatin);
|
||
|
||
engine.Execute(new EnterBattleCommand());
|
||
Assert.True(engine.Execute(new AttackCommand(3, -1)).Accepted);
|
||
Assert.NotNull(engine.State.PendingAbility);
|
||
Assert.True(engine.Execute(new ResolveAbilityCommand(true)).Accepted);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
Assert.Null(engine.State.Players[0].RearGuards[3]);
|
||
Assert.Contains(engine.State.Players[0].Deck, c => c.PrintingId == Juggernaut);
|
||
}
|
||
|
||
[Fact]
|
||
public void Rock_The_Wall_Moves_To_Soul_After_Leaving_Guardian()
|
||
{
|
||
var engine = ReadyToAttack();
|
||
StackDeck(engine.State.Players[0], Gallatin, Gallatin);
|
||
|
||
engine.State.Players[1].Hand.Add(MakeCard(RockTheWall));
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||
Assert.True(engine.Execute(new GuardCommand(RockTheWall)).Accepted);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
var defender = engine.State.Players[1];
|
||
Assert.Contains(defender.Soul, c => c.PrintingId == RockTheWall);
|
||
Assert.DoesNotContain(defender.Drop, c => c.PrintingId == RockTheWall);
|
||
}
|
||
|
||
[Fact]
|
||
public void Guiding_Zombie_Puts_Itself_Into_Soul_And_Mills_Three()
|
||
{
|
||
var engine = InMainPhase();
|
||
var zombie = MakeCard(GuidingZombie);
|
||
engine.State.Players[0].Hand.Add(zombie);
|
||
|
||
Assert.True(engine.Execute(new CallCommand(GuidingZombie, 0)).Accepted);
|
||
zombie.Rested = false;
|
||
|
||
var deckBefore = engine.State.Players[0].Deck.Count;
|
||
|
||
Assert.True(engine.Execute(new ActivateAbilityCommand(zombie.Id)).Accepted);
|
||
|
||
Assert.Null(engine.State.Players[0].RearGuards[0]);
|
||
Assert.Contains(engine.State.Players[0].Soul, c => c.PrintingId == GuidingZombie);
|
||
Assert.Equal(3, deckBefore - engine.State.Players[0].Deck.Count);
|
||
}
|
||
|
||
[Fact]
|
||
public void Monster_Frank_Rides_From_Drop_Zone()
|
||
{
|
||
var engine = InMainPhase();
|
||
engine.State.Players[0].Vanguard = MakeCard(Gallatin);
|
||
GiveDamage(engine, 0, 3);
|
||
|
||
var frank = MakeCard(MonsterFrank);
|
||
engine.State.Players[0].Drop.Add(frank);
|
||
|
||
Assert.True(engine.Execute(new ActivateAbilityCommand(frank.Id)).Accepted);
|
||
|
||
Assert.Same(frank, engine.State.Players[0].Vanguard);
|
||
Assert.DoesNotContain(engine.State.Players[0].Drop, c => c.Id == frank.Id);
|
||
Assert.Equal(3, engine.State.Players[0].Damage.Count(c => c.FaceUp));
|
||
}
|
||
|
||
[Fact]
|
||
public void Gancelot_Hand_Ability_Puts_Itself_On_Top_And_Searches_Blaster_Blade()
|
||
{
|
||
var engine = InMainPhase();
|
||
KeepOnly(engine, 0, "blaster-blade", 0);
|
||
|
||
var gancelot = MakeCard(Gancelot);
|
||
engine.State.Players[0].Hand.Add(gancelot);
|
||
|
||
var blade = MakeCard(BlasterBladePrint);
|
||
engine.State.Players[0].Deck.Insert(0, blade);
|
||
|
||
Assert.True(engine.Execute(new ActivateAbilityCommand(gancelot.Id)).Accepted);
|
||
|
||
Assert.Contains(engine.State.Players[0].Hand, c => c.PrintingId == BlasterBladePrint);
|
||
Assert.DoesNotContain(engine.State.Players[0].Hand, c => c.Id == gancelot.Id);
|
||
}
|
||
|
||
[Fact]
|
||
public void Blightops_Searches_Shieldon_When_Retired()
|
||
{
|
||
var engine = InMainPhase();
|
||
KeepOnly(engine, 0, "ironclad-dragon-shieldon", 0);
|
||
|
||
var blightops = MakeCard(Blightops);
|
||
engine.State.Players[0].RearGuards[0] = blightops;
|
||
GiveDamage(engine, 0, 1);
|
||
|
||
engine.State.Players[0].Deck.Insert(0, MakeCard(Shieldon));
|
||
|
||
engine.RetireRearGuard(0, 0);
|
||
|
||
Assert.NotNull(engine.State.PendingAbility);
|
||
Assert.True(engine.Execute(new ResolveAbilityCommand(true)).Accepted);
|
||
|
||
Assert.Contains(engine.State.Players[0].Hand, c => c.PrintingId == Shieldon);
|
||
Assert.Equal(1, engine.State.Players[0].Damage.Count(c => c.FaceUp));
|
||
}
|
||
|
||
[Fact]
|
||
public void Dragon_Knight_Aleph_Rides_Aleph_After_Putting_Cost_Into_Soul()
|
||
{
|
||
var engine = InMainPhase();
|
||
KeepOnly(engine, 0, "embodiment-of-victory-aleph", 0);
|
||
|
||
var dka = MakeCard(Dka);
|
||
engine.State.Players[0].Vanguard = dka;
|
||
|
||
var bahr = MakeCard(Bahr);
|
||
var tahr = MakeCard(Tahr);
|
||
engine.State.Players[0].RearGuards[0] = bahr;
|
||
engine.State.Players[0].RearGuards[1] = tahr;
|
||
|
||
GiveDamage(engine, 0, 1);
|
||
|
||
var aleph = MakeCard(AlephVictory);
|
||
engine.State.Players[0].Deck.Insert(0, aleph);
|
||
|
||
Assert.True(engine.Execute(new ActivateAbilityCommand(dka.Id)).Accepted);
|
||
|
||
Assert.Same(aleph, engine.State.Players[0].Vanguard);
|
||
Assert.Contains(engine.State.Players[0].Soul, c => c.Id == bahr.Id);
|
||
Assert.Contains(engine.State.Players[0].Soul, c => c.Id == tahr.Id);
|
||
Assert.Null(engine.State.Players[0].RearGuards[0]);
|
||
Assert.Null(engine.State.Players[0].RearGuards[1]);
|
||
}
|
||
|
||
[Fact]
|
||
public void Voidmaster_Makes_Opponent_Discard_When_Hand_Is_Smaller()
|
||
{
|
||
var engine = StartedEngine();
|
||
engine.Execute(new SkipRideCommand());
|
||
|
||
var voidmaster = MakeCard(Voidmaster);
|
||
engine.State.Players[0].Vanguard = voidmaster;
|
||
|
||
var p0 = engine.State.Players[0];
|
||
var p1 = engine.State.Players[1];
|
||
|
||
while (p0.Hand.Count > 0)
|
||
{
|
||
p0.Drop.Add(p0.Hand[0]);
|
||
p0.Hand.RemoveAt(0);
|
||
}
|
||
|
||
p1.Hand.Add(MakeCard(Gallatin));
|
||
p1.Hand.Add(MakeCard(Gallatin));
|
||
|
||
GiveDamage(engine, 0, 1);
|
||
|
||
p0.Deck.Insert(0, MakeCard(Gallatin));
|
||
p0.Deck.Insert(1, MakeCard(Gallatin));
|
||
p1.Deck.Insert(0, MakeCard(Gallatin));
|
||
|
||
engine.Execute(new EnterBattleCommand());
|
||
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
Assert.NotNull(engine.State.PendingAbility);
|
||
Assert.True(engine.Execute(new ResolveAbilityCommand(true)).Accepted);
|
||
|
||
Assert.NotNull(engine.State.PendingChoice);
|
||
var handBefore = p1.Hand.Count;
|
||
Assert.True(engine.Execute(new ChooseCommand(0)).Accepted);
|
||
|
||
Assert.Equal(handBefore - 1, p1.Hand.Count);
|
||
Assert.Equal(1, p0.Damage.Count(c => c.FaceUp));
|
||
}
|
||
|
||
[Fact]
|
||
public void Bors_Gets_Power_For_Counter_Blast()
|
||
{
|
||
var engine = StartedEngine();
|
||
engine.Execute(new SkipRideCommand());
|
||
|
||
var bors = MakeCard(Bors);
|
||
engine.State.Players[0].Vanguard = bors;
|
||
GiveDamage(engine, 0, 1);
|
||
|
||
engine.Execute(new EnterBattleCommand());
|
||
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||
|
||
Assert.NotNull(engine.State.PendingAbility);
|
||
Assert.True(engine.Execute(new ResolveAbilityCommand(true)).Accepted);
|
||
|
||
Assert.Equal(3000, bors.Modifiers.Where(m => m.EffectId == "ability").Sum(m => m.Power));
|
||
}
|
||
|
||
[Fact]
|
||
public void Goku_Retires_Small_RearGuard_After_Grade3_Drive_Check()
|
||
{
|
||
var engine = StartedEngine();
|
||
engine.Execute(new SkipRideCommand());
|
||
|
||
var goku = MakeCard(Goku);
|
||
engine.State.Players[0].Vanguard = goku;
|
||
|
||
var opponent = engine.State.Players[1];
|
||
opponent.RearGuards[0] = MakeCard(Marron);
|
||
|
||
engine.State.Players[0].Deck.Insert(0, MakeCard(Overlord)); // Kagero 3 ранга
|
||
engine.State.Players[0].Deck.Insert(1, MakeCard(Gallatin));
|
||
opponent.Deck.Insert(0, MakeCard(Gallatin)); // проверка урона без триггера
|
||
|
||
engine.Execute(new EnterBattleCommand());
|
||
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
Assert.NotNull(engine.State.PendingChoice);
|
||
Assert.True(engine.Execute(new ChooseCommand(0)).Accepted);
|
||
|
||
Assert.Null(opponent.RearGuards[0]);
|
||
}
|
||
|
||
[Fact]
|
||
public void Starlight_Unicorn_Gives_Another_Royal_Paladin_2000()
|
||
{
|
||
var engine = InMainPhase();
|
||
engine.State.Players[0].Vanguard = MakeCard(Marron);
|
||
|
||
var target = MakeCard(Gallatin);
|
||
engine.State.Players[0].RearGuards[0] = target;
|
||
|
||
engine.State.Players[0].Hand.Add(MakeCard(Unicorn));
|
||
Assert.True(engine.Execute(new CallCommand(Unicorn, 1)).Accepted);
|
||
|
||
Assert.NotNull(engine.State.PendingChoice);
|
||
Assert.True(engine.Execute(new ChooseCommand(1)).Accepted); // Авангард, затем Gallatin
|
||
|
||
Assert.Equal(
|
||
2000,
|
||
target.Modifiers.Where(m => m.EffectId == "starlight-unicorn").Sum(m => m.Power));
|
||
}
|
||
|
||
[Fact]
|
||
public void Joka_Gets_Power_When_Opponent_RearGuard_Is_Retired()
|
||
{
|
||
var engine = InMainPhase();
|
||
|
||
var joka = MakeCard(Joka);
|
||
engine.State.Players[0].RearGuards[0] = joka;
|
||
|
||
engine.State.Players[1].RearGuards[1] = MakeCard(Gallatin);
|
||
|
||
engine.RetireRearGuard(1, 1);
|
||
|
||
Assert.Equal(
|
||
3000,
|
||
joka.Modifiers.Where(m => m.EffectId == "opponent-retire").Sum(m => m.Power));
|
||
}
|
||
|
||
[Fact]
|
||
public void Rear_Guard_In_Back_Row_Cannot_Be_Attacked_Normally()
|
||
{
|
||
var engine = ReadyToAttackRearGuard();
|
||
engine.State.Players[1].RearGuards[0] = MakeCard(Marron);
|
||
|
||
Assert.False(engine.Execute(new AttackCommand(3, 0)).Accepted);
|
||
}
|
||
|
||
[Fact]
|
||
public void Tejas_Attacks_Back_Row_In_Its_Column()
|
||
{
|
||
var engine = ReadyToAttackRearGuard();
|
||
|
||
var tejas = MakeCard(Tejas);
|
||
engine.State.Players[0].RearGuards[3] = tejas;
|
||
tejas.Rested = false;
|
||
|
||
var defender = engine.State.Players[1];
|
||
defender.RearGuards[0] = MakeCard(Barcgal);
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(3, 0)).Accepted);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
|
||
Assert.Null(defender.RearGuards[0]);
|
||
}
|
||
|
||
[Fact]
|
||
public void Yaksha_Rides_From_Hand_When_Opponent_RearGuard_Is_Retired()
|
||
{
|
||
var engine = InMainPhase();
|
||
|
||
var oldVanguard = MakeCard(Gallatin);
|
||
engine.State.Players[0].Vanguard = oldVanguard;
|
||
|
||
var yaksha = MakeCard(Yaksha);
|
||
engine.State.Players[0].Hand.Add(yaksha);
|
||
|
||
engine.State.Players[1].RearGuards[0] = MakeCard(Marron);
|
||
|
||
engine.RetireRearGuard(1, 0);
|
||
|
||
Assert.NotNull(engine.State.PendingAbility);
|
||
Assert.True(engine.Execute(new ResolveAbilityCommand(true)).Accepted);
|
||
|
||
Assert.Same(yaksha, engine.State.Players[0].Vanguard);
|
||
Assert.DoesNotContain(engine.State.Players[0].Hand, c => c.Id == yaksha.Id);
|
||
Assert.Contains(engine.State.Players[0].Soul, c => c.Id == oldVanguard.Id);
|
||
}
|
||
|
||
[Fact]
|
||
public void Replay_Restores_The_Match_And_Event_Hash()
|
||
{
|
||
var engine = NewEngine(777);
|
||
var recorder = new MatchRecorder(engine, BuildDeck(), BuildDeck());
|
||
engine.Start();
|
||
var random = new Random(777);
|
||
|
||
for (var step = 0; step < 3000 && !engine.State.IsFinished; step++)
|
||
{
|
||
var legal = engine.LegalCommands();
|
||
recorder.Execute(legal[random.Next(legal.Count)]);
|
||
}
|
||
|
||
var record = recorder.ToRecord();
|
||
var json = JsonSerializer.Serialize(record);
|
||
var restored = JsonSerializer.Deserialize<MatchRecord>(json);
|
||
|
||
Assert.NotNull(restored);
|
||
ReplayRunner.Verify(_catalog, restored);
|
||
Assert.Equal(record.EventHash, restored.EventHash);
|
||
}
|
||
|
||
[Fact]
|
||
public void Guard_Trigger_Keeps_Guard_Step_After_Decline()
|
||
{
|
||
var engine = ReadyToAttack();
|
||
|
||
engine.State.Players[1].Vanguard = MakeCard(Chigasumi); // Nubatama
|
||
GiveDamage(engine, 1, 1);
|
||
engine.State.Players[1].Hand.Add(MakeCard(Hagakure));
|
||
engine.State.Players[0].Hand.Add(MakeCard(Gallatin)); // у атакующего больше карт
|
||
|
||
Assert.True(engine.Execute(new AttackCommand(-1, -1)).Accepted);
|
||
Assert.True(engine.Execute(new GuardCommand(Hagakure)).Accepted);
|
||
|
||
Assert.NotNull(engine.State.PendingAbility);
|
||
Assert.True(engine.Execute(new ResolveAbilityCommand(false)).Accepted);
|
||
|
||
Assert.Equal(Step.Guard, engine.State.Step);
|
||
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
|
||
}
|
||
|
||
[Fact]
|
||
public void ResolveTrigger_Rejects_When_Nothing_Is_Pending()
|
||
{
|
||
var engine = StartedEngine();
|
||
|
||
Assert.False(engine.Execute(new ResolveTriggerCommand(-1, -1)).Accepted);
|
||
}
|
||
|
||
[Fact]
|
||
public void Random_Bot_Games_Keep_Invariants_And_Terminate()
|
||
{
|
||
for (var seed = 1; seed <= 50; seed++)
|
||
{
|
||
var engine = NewEngine((ulong)seed);
|
||
engine.Start();
|
||
var random = new Random(seed);
|
||
GameCommand? lastCommand = null;
|
||
|
||
for (var step = 0; step < 3000 && !engine.State.IsFinished; step++)
|
||
{
|
||
var legal = engine.LegalCommands();
|
||
Assert.True(
|
||
legal.Count > 0,
|
||
$"{seed}/{step}: нет команд; фаза {engine.State.Phase}, шаг {engine.State.Step}, "
|
||
+ $"ожидание триггера {engine.State.PendingTrigger is not null}, "
|
||
+ $"способности {engine.State.PendingAbility is not null}, "
|
||
+ $"выбора {engine.State.PendingChoice is not null}");
|
||
|
||
var command = legal[random.Next(legal.Count)];
|
||
lastCommand = command;
|
||
var result = engine.Execute(command);
|
||
Assert.True(result.Accepted, $"{command} -> {result.Reason}");
|
||
|
||
AssertInvariants(engine.State, $"{seed}/{step}: {lastCommand}");
|
||
}
|
||
|
||
Assert.True(engine.State.IsFinished, $"Партия {seed} не завершилась за 3000 шагов");
|
||
}
|
||
}
|
||
|
||
[Fact]
|
||
public void Finish_Awards_Winner_And_Rejects_Later_Commands()
|
||
{
|
||
var engine = InMainPhase();
|
||
|
||
engine.Finish(1);
|
||
|
||
Assert.True(engine.State.IsFinished);
|
||
Assert.Equal(1, engine.State.Winner);
|
||
Assert.True(engine.State.Players[0].IsDefeated);
|
||
Assert.IsType<GameFinishedEvent>(engine.Events[^1]);
|
||
|
||
var result = engine.Execute(new EndTurnCommand());
|
||
Assert.False(result.Accepted);
|
||
Assert.Equal("Партия завершена.", result.Reason);
|
||
Assert.Empty(engine.LegalCommands());
|
||
}
|
||
|
||
[Fact]
|
||
public void Finish_Is_Idempotent()
|
||
{
|
||
var engine = InMainPhase();
|
||
var before = engine.Events.Count;
|
||
|
||
engine.Finish(0);
|
||
engine.Finish(1);
|
||
|
||
Assert.Equal(0, engine.State.Winner);
|
||
Assert.Equal(before + 1, engine.Events.Count);
|
||
}
|
||
|
||
private static void AssertInvariants(GameState state, string context)
|
||
{
|
||
var ids = new HashSet<int>();
|
||
|
||
for (var player = 0; player < 2; player++)
|
||
{
|
||
var p = state.Players[player];
|
||
var zones = new List<IEnumerable<CardInstance>>
|
||
{
|
||
p.Deck,
|
||
p.Hand,
|
||
p.Soul,
|
||
p.Damage,
|
||
p.Drop,
|
||
p.Trigger,
|
||
p.Guardian,
|
||
p.Vanguard is null ? Enumerable.Empty<CardInstance>() : new[] { p.Vanguard },
|
||
p.RearGuards.OfType<CardInstance>(),
|
||
state.InTransit.Where(t => t.Player == player).Select(t => t.Card),
|
||
};
|
||
|
||
var total = 0;
|
||
foreach (var card in zones.SelectMany(zone => zone))
|
||
{
|
||
Assert.True(ids.Add(card.Id), $"Повтор instanceId {card.Id}");
|
||
total++;
|
||
}
|
||
|
||
Assert.True(
|
||
total == 50,
|
||
$"Игрок {player}: {total} карт (deck {p.Deck.Count}, hand {p.Hand.Count}, "
|
||
+ $"soul {p.Soul.Count}, damage {p.Damage.Count}, drop {p.Drop.Count}, "
|
||
+ $"trigger {p.Trigger.Count}, guardian {p.Guardian.Count}, "
|
||
+ $"vg {(p.Vanguard is null ? 0 : 1)}, rg {p.RearGuards.Count(u => u is not null)}) [{context}]");
|
||
Assert.True(p.Damage.Count <= GameEngine.MaxDamage);
|
||
}
|
||
|
||
if (state.IsFinished)
|
||
Assert.NotNull(state.Winner);
|
||
}
|
||
|
||
[Fact]
|
||
public void Same_Seed_And_Commands_Produce_Same_Events()
|
||
{
|
||
var first = RunScriptedMatch(seed: 12345);
|
||
var second = RunScriptedMatch(seed: 12345);
|
||
|
||
Assert.Equal(first, second);
|
||
}
|
||
|
||
private List<GameEvent> RunScriptedMatch(ulong seed)
|
||
{
|
||
var engine = NewEngine(seed);
|
||
engine.Start();
|
||
engine.Execute(new FinishMulliganCommand());
|
||
Give(engine, 0, Marron);
|
||
engine.Execute(new RideCommand(Marron));
|
||
engine.Execute(new AttackCommand(-1, -1));
|
||
engine.Execute(new PassGuardCommand());
|
||
return engine.Events.ToList();
|
||
}
|
||
|
||
// ---------- helpers ----------
|
||
|
||
private GameEngine NewEngine(ulong seed = 7) =>
|
||
new(_catalog, seed, BuildDeck(), BuildDeck());
|
||
|
||
private GameEngine StartedEngine()
|
||
{
|
||
var engine = NewEngine();
|
||
engine.Start();
|
||
engine.Execute(new FinishMulliganCommand());
|
||
|
||
// фиксируем стартовый Авангард, чтобы Barcgal не срабатывал в обычных тестах
|
||
SetVanguard(engine, 0, Conroe);
|
||
return engine;
|
||
}
|
||
|
||
private GameEngine InMainPhase()
|
||
{
|
||
var engine = StartedEngine();
|
||
engine.Execute(new SkipRideCommand());
|
||
return engine;
|
||
}
|
||
|
||
private GameEngine ReadyToAttack()
|
||
{
|
||
var engine = StartedEngine();
|
||
Give(engine, 0, Marron);
|
||
engine.Execute(new RideCommand(Marron));
|
||
engine.Execute(new EnterBattleCommand());
|
||
return engine;
|
||
}
|
||
|
||
private GameEngine ReadyToAttackWithWingal(string attackerPrintingId)
|
||
{
|
||
var engine = StartedEngine();
|
||
engine.Execute(new SkipRideCommand());
|
||
|
||
Give(engine, 0, attackerPrintingId);
|
||
engine.Execute(new CallCommand(attackerPrintingId, 3));
|
||
|
||
Give(engine, 0, Wingal);
|
||
engine.Execute(new CallCommand(Wingal, 0));
|
||
|
||
engine.State.Players[0].RearGuards[0]!.Rested = false;
|
||
engine.State.Players[0].RearGuards[3]!.Rested = false;
|
||
|
||
engine.Execute(new EnterBattleCommand());
|
||
return engine;
|
||
}
|
||
|
||
private GameEngine ReadyToAttackWithRearGuard()
|
||
{
|
||
var engine = StartedEngine();
|
||
Give(engine, 0, Marron);
|
||
engine.Execute(new RideCommand(Marron));
|
||
Give(engine, 0, Gallatin);
|
||
engine.Execute(new CallCommand(Gallatin, 0));
|
||
engine.Execute(new EnterBattleCommand());
|
||
return engine;
|
||
}
|
||
|
||
private GameEngine ReadyToBoost()
|
||
{
|
||
var engine = StartedEngine();
|
||
Give(engine, 0, Marron);
|
||
engine.Execute(new RideCommand(Marron));
|
||
|
||
// сзади слева — поддержка (1 ранг), впереди слева — атакующий (2 ранг)
|
||
Give(engine, 0, Marron);
|
||
engine.Execute(new CallCommand(Marron, 0));
|
||
|
||
Give(engine, 0, Gallatin);
|
||
engine.Execute(new CallCommand(Gallatin, 3));
|
||
|
||
// призванные карты входят повёрнутыми — для теста поддержки поднимаем обе
|
||
engine.State.Players[0].RearGuards[0]!.Rested = false;
|
||
engine.State.Players[0].RearGuards[3]!.Rested = false;
|
||
|
||
SetVanguard(engine, 1, Gallatin);
|
||
engine.Execute(new EnterBattleCommand());
|
||
return engine;
|
||
}
|
||
|
||
private GameEngine ReadyToAttackRearGuard()
|
||
{
|
||
var engine = StartedEngine();
|
||
Give(engine, 0, Marron);
|
||
engine.Execute(new RideCommand(Marron));
|
||
|
||
Give(engine, 0, Gallatin);
|
||
engine.Execute(new CallCommand(Gallatin, 3));
|
||
engine.State.Players[0].RearGuards[3]!.Rested = false;
|
||
|
||
var defender = engine.State.Players[1];
|
||
defender.RearGuards[4] = TakeFromDeck(defender, Marron); // цель в переднем ряду
|
||
defender.RearGuards[3] = TakeFromDeck(defender, Gallatin); // перехватчик
|
||
|
||
engine.Execute(new EnterBattleCommand());
|
||
return engine;
|
||
}
|
||
|
||
private static readonly (string PrintingId, int Count)[] Seeds =
|
||
{
|
||
(Barcgal, 4), (Marron, 4), (Gallatin, 4), (Alfred, 4),
|
||
(Nike, 2), (Flogal, 2), (Govannon, 2),
|
||
(Iseult, 2), (Nehalem, 2), (Overlord, 2), (Conroe, 4),
|
||
(BlasterBlade, 2), (Wingal, 2),
|
||
(Lien, 2),
|
||
(Cocoa, 2), (Libra, 2),
|
||
(Deathrex, 2), (DemonEater, 2),
|
||
(Morgana, 2),
|
||
};
|
||
|
||
private List<string> BuildDeck()
|
||
{
|
||
var names = Seeds
|
||
.Select(s => _catalog.CardOf(s.PrintingId).NameId)
|
||
.ToHashSet(StringComparer.Ordinal);
|
||
|
||
var deck = new List<string>();
|
||
foreach (var (printingId, count) in Seeds)
|
||
deck.AddRange(Enumerable.Repeat(printingId, count));
|
||
|
||
foreach (var group in _catalog.Printings
|
||
.Where(p => !p.IsParallel)
|
||
.GroupBy(p => p.NameId)
|
||
.OrderBy(g => g.Key, StringComparer.Ordinal))
|
||
{
|
||
if (deck.Count >= 50)
|
||
break;
|
||
|
||
if (names.Contains(group.Key))
|
||
continue;
|
||
|
||
var card = _catalog.Card(group.Key);
|
||
if (card.IsSentinel || card.Trigger == TriggerType.Heal)
|
||
continue;
|
||
|
||
var printingId = group.First().PrintingId;
|
||
for (var i = 0; i < 4 && deck.Count < 50; i++)
|
||
deck.Add(printingId);
|
||
}
|
||
|
||
return deck;
|
||
}
|
||
|
||
private static void Give(GameEngine engine, int player, string printingId)
|
||
{
|
||
var p = engine.State.Players[player];
|
||
var index = p.Deck.FindIndex(c => c.PrintingId == printingId);
|
||
Assert.True(index >= 0, $"В колоде нет {printingId}");
|
||
|
||
var card = p.Deck[index];
|
||
p.Deck.RemoveAt(index);
|
||
p.Hand.Add(card);
|
||
}
|
||
|
||
private static void StackDeck(PlayerState player, params string[] printingIds)
|
||
{
|
||
var cards = new List<CardInstance>();
|
||
foreach (var printingId in printingIds)
|
||
{
|
||
var index = player.Deck.FindIndex(c => c.PrintingId == printingId);
|
||
Assert.True(index >= 0, $"В колоде нет {printingId}");
|
||
|
||
cards.Add(player.Deck[index]);
|
||
player.Deck.RemoveAt(index);
|
||
}
|
||
|
||
for (var i = cards.Count - 1; i >= 0; i--)
|
||
player.Deck.Insert(0, cards[i]);
|
||
}
|
||
|
||
private static void KeepOnly(GameEngine engine, int player, string nameId, int keep)
|
||
{
|
||
var p = engine.State.Players[player];
|
||
var matches = p.Deck
|
||
.Where(c => engine.State.Catalog.CardOf(c.PrintingId).NameId == nameId)
|
||
.ToList();
|
||
|
||
foreach (var card in matches.Skip(keep))
|
||
p.Deck.Remove(card);
|
||
}
|
||
|
||
private static void GiveDamage(GameEngine engine, int player, int amount)
|
||
{
|
||
var p = engine.State.Players[player];
|
||
for (var i = 0; i < amount; i++)
|
||
{
|
||
Assert.NotEmpty(p.Deck);
|
||
var card = p.Deck[0];
|
||
p.Deck.RemoveAt(0);
|
||
card.FaceUp = false;
|
||
p.Damage.Add(card);
|
||
}
|
||
}
|
||
|
||
private static void SetVanguard(GameEngine engine, int player, string printingId)
|
||
{
|
||
var p = engine.State.Players[player];
|
||
var index = p.Deck.FindIndex(c => c.PrintingId == printingId);
|
||
Assert.True(index >= 0, $"В колоде нет {printingId}");
|
||
|
||
var card = p.Deck[index];
|
||
p.Deck.RemoveAt(index);
|
||
p.Vanguard = card;
|
||
}
|
||
|
||
private static CardInstance TakeFromDeck(PlayerState player, string printingId)
|
||
{
|
||
var index = player.Deck.FindIndex(c => c.PrintingId == printingId);
|
||
Assert.True(index >= 0, $"В колоде нет {printingId}");
|
||
|
||
var card = player.Deck[index];
|
||
player.Deck.RemoveAt(index);
|
||
return card;
|
||
}
|
||
|
||
private static CardInstance TakeAnyFromDeck(PlayerState player)
|
||
{
|
||
Assert.NotEmpty(player.Deck);
|
||
var card = player.Deck[0];
|
||
player.Deck.RemoveAt(0);
|
||
return card;
|
||
}
|
||
|
||
private static int _nextInstanceId = 20_000;
|
||
|
||
/// <summary>Карта, которой нет в тестовой колоде: кладём в руку или на поле вручную.</summary>
|
||
private static CardInstance MakeCard(string printingId) =>
|
||
new() { Id = Interlocked.Increment(ref _nextInstanceId), PrintingId = printingId };
|
||
}
|