VanguardReMaster/server/tests/Vanguard.Rules.Tests/GameEngineTests.cs

617 lines
21 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Vanguard.Rules.Cards;
using Vanguard.Rules.Engine;
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
[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(Barcgal, 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 == Barcgal);
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.TemporaryPower);
Assert.True(engine.Execute(new EndTurnCommand()).Accepted);
Assert.Equal(0, vanguard.TemporaryPower);
}
[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, 0)).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[0]); // цель выжила
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, 0)).Accepted);
Assert.True(engine.Execute(new PassGuardCommand()).Accepted);
var defender = engine.State.Players[1];
Assert.Null(defender.RearGuards[0]);
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, 0)).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, 0)).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, 0)).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);
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, 0)).Accepted);
Assert.False(engine.Execute(new InterceptCommand(0)).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 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());
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 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[0] = TakeFromDeck(defender, Marron);
defender.RearGuards[3] = TakeFromDeck(defender, Gallatin);
engine.Execute(new EnterBattleCommand());
return engine;
}
private List<string> BuildDeck()
{
var mustInclude = new[] { Barcgal, Marron, Gallatin, Alfred, Nike, Flogal, Govannon, Iseult, Nehalem, Overlord };
var names = mustInclude
.Select(_catalog.CardOf)
.Select(c => c.NameId)
.ToHashSet(StringComparer.Ordinal);
var deck = new List<string>();
foreach (var printing in mustInclude)
deck.AddRange(Enumerable.Repeat(printing, 4));
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 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;
}
}