248 lines
7.7 KiB
C#
248 lines
7.7 KiB
C#
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
|
||
|
||
[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 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 List<string> BuildDeck()
|
||
{
|
||
var mustInclude = new[] { Barcgal, Marron, Gallatin, Alfred };
|
||
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 CardInstance TakeAnyFromDeck(PlayerState player)
|
||
{
|
||
Assert.NotEmpty(player.Deck);
|
||
var card = player.Deck[0];
|
||
player.Deck.RemoveAt(0);
|
||
return 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;
|
||
}
|
||
}
|