落地无头规则引擎与局内内容,便于命令行复现对战。
规则、卡包与测试放在同一套核心里,避免表现层自行结算。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
using CardGame.Core;
|
||||
using Xunit;
|
||||
|
||||
namespace Game.Core.Tests;
|
||||
|
||||
public sealed class AiSearchTests
|
||||
{
|
||||
[Fact]
|
||||
public void Prefers_face_damage_when_lethal_or_high_value()
|
||||
{
|
||||
var state = new GameState(9) { Catalog = Cards.Catalog };
|
||||
MatchSetup.PlaceSystemField(state, 0, Cards.BaseField);
|
||||
MatchSetup.PlaceSystemField(state, 1, Cards.BaseField);
|
||||
state.Players[1].Life = 1;
|
||||
var attacker = state.NewEntity(Cards.Wisp, 0, Zone.Board, Factions.Guard);
|
||||
attacker.EnteredOnTurn = 0;
|
||||
attacker.AttacksRemaining = 1;
|
||||
state.Board.Add(attacker);
|
||||
state.ActivePlayerIndex = 0;
|
||||
state.TurnNumber = 2;
|
||||
|
||||
var legal = RulesEngine.GetLegalActions(state);
|
||||
var (action, top) = TurnBeamSearch.ChooseWithTrace(state, legal);
|
||||
Assert.IsType<AttackAction>(action);
|
||||
Assert.Contains(top, t => t.Chosen && t.Label.Contains("敌方主战者", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlayUntil_returns_step_trace()
|
||||
{
|
||||
var state = MatchSetup.CreateStartingBattle(11, Cards.Catalog, shuffle: false);
|
||||
state = RulesEngine.Execute(state, new EndTurnAction(0));
|
||||
var (_, trace) = TurnBeamSearch.PlayUntil(state, untilPlayerIndex: 0);
|
||||
Assert.NotEmpty(trace.Steps);
|
||||
Assert.All(trace.Steps, s => Assert.NotEmpty(s.TopCandidates));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChooseWithTrace_works_when_all_scores_are_negative()
|
||||
{
|
||||
var state = MatchSetup.CreateStartingBattle(13, Cards.Catalog, shuffle: false);
|
||||
state.Players[0].Life = 1;
|
||||
state.Players[1].Life = 20;
|
||||
var legal = RulesEngine.GetLegalActions(state);
|
||||
var (action, top) = TurnBeamSearch.ChooseWithTrace(state, legal);
|
||||
Assert.NotNull(action);
|
||||
Assert.Contains(top, t => t.Chosen);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using CardGame.Core;
|
||||
|
||||
namespace Game.Core.Tests;
|
||||
|
||||
internal static class Cards
|
||||
{
|
||||
private static readonly Dictionary<string, CardDefinition> ById = CardJson
|
||||
.LoadCatalog(Path.Combine(AppContext.BaseDirectory, "content", "cards"));
|
||||
|
||||
public static IReadOnlyDictionary<string, CardDefinition> Catalog => ById;
|
||||
|
||||
public static CardDefinition Get(string id) => ById[id];
|
||||
|
||||
public static CardDefinition Spark => ById["card.example.spark"];
|
||||
public static CardDefinition Wisp => ById["card.example.wisp"];
|
||||
public static CardDefinition Salve => ById["card.example.salve"];
|
||||
public static CardDefinition Ward => ById["card.example.ward"];
|
||||
public static CardDefinition Lantern => ById["card.example.lantern"];
|
||||
public static CardDefinition Fuse => ById["card.example.fuse"];
|
||||
public static CardDefinition BaseField => ById["card.system.base_field"];
|
||||
public static CardDefinition Patrol => ById["card.guard.patrol"];
|
||||
public static CardDefinition Captain => ById["card.guard.captain"];
|
||||
public static CardDefinition Believer => ById["card.cult.believer"];
|
||||
public static CardDefinition Camp => ById["card.cult.camp"];
|
||||
public static CardDefinition Crossbow => ById["card.guard.crossbow"];
|
||||
public static CardDefinition Conscript => ById["card.guard.conscript"];
|
||||
|
||||
public static List<CardDefinition> Repeat(CardDefinition card, int count) =>
|
||||
Enumerable.Repeat(card, count).ToList();
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
<RootNamespace>Game.Core.Tests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Game.Core\Game.Core.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\..\content\**\*" CopyToOutputDirectory="PreserveNewest" Link="content\%(RecursiveDir)%(Filename)%(Extension)" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,57 @@
|
||||
using CardGame.Core;
|
||||
using Xunit;
|
||||
|
||||
namespace Game.Core.Tests;
|
||||
|
||||
public sealed class PackTests
|
||||
{
|
||||
private static List<PackDefinition> Catalog() =>
|
||||
PackJson.LoadDirectory(Path.Combine(AppContext.BaseDirectory, "content", "packs"));
|
||||
|
||||
[Fact]
|
||||
public void Base_pack_is_always_in_the_run_pool()
|
||||
{
|
||||
var state = MatchSetup.Create(
|
||||
1,
|
||||
Cards.Repeat(Cards.Wisp, 10),
|
||||
Cards.Repeat(Cards.Wisp, 10),
|
||||
Cards.Catalog,
|
||||
shuffle: false);
|
||||
var unlocked = Packs.Unlocked(state, 0, Catalog());
|
||||
Assert.Contains(unlocked, p => p.Id == "pack.example.base");
|
||||
Assert.DoesNotContain(unlocked, p => p.Id == "pack.example.vitality");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Healing_unlocks_vitality_pack()
|
||||
{
|
||||
var state = MatchSetup.Create(
|
||||
1,
|
||||
Cards.Repeat(Cards.Salve, 10),
|
||||
Cards.Repeat(Cards.Wisp, 10),
|
||||
Cards.Catalog,
|
||||
shuffle: false);
|
||||
state.Players[0].Life = 10;
|
||||
state.Players[0].Energy = 1;
|
||||
var salve = state.Players[0].Hand[0];
|
||||
state = RulesEngine.Execute(state, new PlayCardAction(0, salve.Id));
|
||||
Assert.Equal(2, state.Players[0].Run.HealReceived);
|
||||
Assert.Contains(Packs.Unlocked(state, 0, Catalog()), p => p.Id == "pack.example.vitality");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fuse_expiry_unlocks_aftermath_for_the_owner()
|
||||
{
|
||||
var state = MatchSetup.Create(
|
||||
1,
|
||||
Cards.Repeat(Cards.Fuse, 10),
|
||||
Cards.Repeat(Cards.Wisp, 10),
|
||||
Cards.Catalog,
|
||||
shuffle: false);
|
||||
var fuse = state.Players[0].Hand[0];
|
||||
state = RulesEngine.Execute(state, new PlayCardAction(0, fuse.Id));
|
||||
state = RulesEngine.Execute(state, new EndTurnAction(0));
|
||||
Assert.Equal(1, state.Players[0].Run.FriendlyDestroyed);
|
||||
Assert.Contains(Packs.Unlocked(state, 0, Catalog()), p => p.Id == "pack.example.aftermath");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using CardGame.Core;
|
||||
using Xunit;
|
||||
|
||||
namespace Game.Core.Tests;
|
||||
|
||||
public sealed class PhaseATests
|
||||
{
|
||||
[Fact]
|
||||
public void Clone_is_independent()
|
||||
{
|
||||
var state = new GameState(seed: 1) { Catalog = Cards.Catalog };
|
||||
state.Players[0].Life = 12;
|
||||
var clone = RulesEngine.CloneState(state);
|
||||
clone.Players[0].Life = 7;
|
||||
Assert.Equal(12, state.Players[0].Life);
|
||||
Assert.Equal(7, clone.Players[0].Life);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rng_is_deterministic_for_same_seed()
|
||||
{
|
||||
var a = new GameRng(42);
|
||||
var b = new GameRng(42);
|
||||
Assert.Equal(a.NextUInt32(), b.NextUInt32());
|
||||
Assert.Equal(a.NextInt(10), b.NextInt(10));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EndTurn_passes_priority_and_base_field_supplies()
|
||||
{
|
||||
var state = MatchSetup.Create(
|
||||
7,
|
||||
Cards.Repeat(Cards.Wisp, 10),
|
||||
Cards.Repeat(Cards.Wisp, 10),
|
||||
Cards.Catalog,
|
||||
shuffle: false);
|
||||
Assert.Equal(1, state.Players[0].Energy);
|
||||
state = RulesEngine.Execute(state, new EndTurnAction(0));
|
||||
Assert.Equal(1, state.ActivePlayerIndex);
|
||||
Assert.Equal(1, state.Players[1].Energy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetLegalActions_offers_end_turn()
|
||||
{
|
||||
var state = MatchSetup.Create(
|
||||
1,
|
||||
Cards.Repeat(Cards.Wisp, 10),
|
||||
Cards.Repeat(Cards.Wisp, 10),
|
||||
Cards.Catalog,
|
||||
shuffle: false);
|
||||
Assert.Contains(RulesEngine.GetLegalActions(state), a => a is EndTurnAction);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using CardGame.Core;
|
||||
using Xunit;
|
||||
|
||||
namespace Game.Core.Tests;
|
||||
|
||||
public sealed class PhaseBTests
|
||||
{
|
||||
private static GameState NewMatch(CardDefinition card, int count = 10) =>
|
||||
MatchSetup.Create(11, Cards.Repeat(card, count), Cards.Repeat(card, count), Cards.Catalog, shuffle: false);
|
||||
|
||||
[Fact]
|
||||
public void Opening_draw_and_base_supply()
|
||||
{
|
||||
var state = NewMatch(Cards.Wisp);
|
||||
Assert.Equal(4, state.Players[0].Hand.Count);
|
||||
Assert.Equal(3, state.Players[1].Hand.Count);
|
||||
Assert.Equal(1, state.Players[0].Energy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Play_creature_pays_current_cost()
|
||||
{
|
||||
var state = NewMatch(Cards.Wisp);
|
||||
var wisp = state.Players[0].Hand[0];
|
||||
state = RulesEngine.Execute(state, new PlayCardAction(0, wisp.Id));
|
||||
Assert.Equal(0, state.Players[0].Energy);
|
||||
Assert.Contains(state.Board, e => e.Id == wisp.Id && e.Zone == Zone.Board);
|
||||
Assert.Equal(0, state.Find(wisp.Id)!.AttacksRemaining);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Spell_damages_and_goes_to_graveyard()
|
||||
{
|
||||
var state = NewMatch(Cards.Spark);
|
||||
var spark = state.Players[0].Hand[0];
|
||||
state = RulesEngine.Execute(state, new PlayCardAction(0, spark.Id, new PlayerTarget(1)));
|
||||
Assert.Equal(19, state.Players[1].Life);
|
||||
Assert.Contains(state.Players[0].Graveyard, e => e.Id == spark.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Creature_attacks_after_summoning_sickness()
|
||||
{
|
||||
var state = NewMatch(Cards.Wisp, 20);
|
||||
var wisp = state.Players[0].Hand[0];
|
||||
state = RulesEngine.Execute(state, new PlayCardAction(0, wisp.Id));
|
||||
Assert.DoesNotContain(RulesEngine.GetLegalActions(state), a => a is AttackAction);
|
||||
|
||||
state = RulesEngine.Execute(state, new EndTurnAction(0));
|
||||
state = RulesEngine.Execute(state, new EndTurnAction(1));
|
||||
|
||||
var attacker = state.Board.Single(e => e.ControllerIndex == 0 && e.Definition.Id == Cards.Wisp.Id);
|
||||
state = RulesEngine.Execute(state, new AttackAction(0, attacker.Id, new PlayerTarget(1)));
|
||||
Assert.Equal(19, state.Players[1].Life);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Shared_board_caps_at_ten()
|
||||
{
|
||||
var state = NewMatch(Cards.Wisp, 30);
|
||||
for (var turn = 0; turn < 8 && !state.IsOver; turn++)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var play = RulesEngine.GetLegalActions(state).OfType<PlayCardAction>().FirstOrDefault();
|
||||
if (play is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
state = RulesEngine.Execute(state, play);
|
||||
}
|
||||
|
||||
if (state.IsOver)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
state = RulesEngine.Execute(state, new EndTurnAction(state.ActivePlayerIndex));
|
||||
}
|
||||
|
||||
Assert.True(state.Board.Count(e => e.Definition.Type != CardType.FieldCard || !e.IsSystem) <= GameState.BoardCapacity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using CardGame.Core;
|
||||
using Xunit;
|
||||
|
||||
namespace Game.Core.Tests;
|
||||
|
||||
public sealed class PhaseCTests
|
||||
{
|
||||
[Fact]
|
||||
public void Lantern_supplies_on_turn_start()
|
||||
{
|
||||
var state = MatchSetup.Create(
|
||||
9,
|
||||
Cards.Repeat(Cards.Lantern, 10),
|
||||
Cards.Repeat(Cards.Wisp, 10),
|
||||
Cards.Catalog,
|
||||
shuffle: false);
|
||||
var lantern = state.Players[0].Hand[0];
|
||||
Assert.Equal(1, state.Players[0].Energy);
|
||||
state = RulesEngine.Execute(state, new PlayCardAction(0, lantern.Id));
|
||||
Assert.Equal(0, state.Players[0].Energy);
|
||||
state = RulesEngine.Execute(state, new EndTurnAction(0));
|
||||
state = RulesEngine.Execute(state, new EndTurnAction(1));
|
||||
// base field +1, lantern +1
|
||||
Assert.Equal(2, state.Players[0].Energy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fuse_expires_and_damages()
|
||||
{
|
||||
var state = MatchSetup.Create(
|
||||
4,
|
||||
Cards.Repeat(Cards.Fuse, 10),
|
||||
Cards.Repeat(Cards.Wisp, 10),
|
||||
Cards.Catalog,
|
||||
shuffle: false);
|
||||
var fuse = state.Players[0].Hand[0];
|
||||
state = RulesEngine.Execute(state, new PlayCardAction(0, fuse.Id));
|
||||
state = RulesEngine.Execute(state, new EndTurnAction(0));
|
||||
Assert.DoesNotContain(state.Board, e => e.Definition.Id == Cards.Fuse.Id);
|
||||
Assert.Equal(18, state.Players[1].Life);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using CardGame.Core;
|
||||
using Xunit;
|
||||
|
||||
namespace Game.Core.Tests;
|
||||
|
||||
public sealed class RunTests
|
||||
{
|
||||
[Fact]
|
||||
public void Start_builds_starting_package_with_holders()
|
||||
{
|
||||
var run = RunProgress.Start(42, Cards.Catalog);
|
||||
Assert.Equal(16, run.Owned.Count);
|
||||
Assert.Contains(run.Owned, c => c.DefinitionId == "card.guard.patrol" && c.HolderId == Factions.Guard);
|
||||
Assert.Contains(run.Owned, c => c.DefinitionId == "card.cult.believer" && c.HolderId == Factions.Cult);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateFromRun_applies_owned_stat_overrides()
|
||||
{
|
||||
var run = RunProgress.Start(7, Cards.Catalog);
|
||||
var patrol = run.Owned.First(c => c.DefinitionId == "card.guard.patrol");
|
||||
patrol.Cost = 0;
|
||||
patrol.Attack = 9;
|
||||
|
||||
var state = MatchSetup.CreateFromRun(run, Cards.Catalog, shuffle: false);
|
||||
var inDeckOrHand = state.Players[0].Deck.Concat(state.Players[0].Hand)
|
||||
.First(e => e.Definition.Id == "card.guard.patrol" && e.Attack == 9);
|
||||
Assert.Equal(0, inDeckOrHand.Cost);
|
||||
Assert.Equal(9, inDeckOrHand.Attack);
|
||||
Assert.Equal(Factions.Guard, inDeckOrHand.HolderId);
|
||||
Assert.Equal(run.Stats.HealReceived, state.Players[0].Run.HealReceived);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reward_pool_includes_base_pack_cards()
|
||||
{
|
||||
var run = RunProgress.Start(1, Cards.Catalog);
|
||||
var state = MatchSetup.CreateFromRun(run, Cards.Catalog, shuffle: false);
|
||||
var packs = PackJson.LoadDirectory(Path.Combine(AppContext.BaseDirectory, "content", "packs"));
|
||||
var pool = RunRewards.ActivePool(state, 0, packs);
|
||||
Assert.Contains("card.example.spark", pool);
|
||||
Assert.Contains("card.example.wisp", pool);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Adding_reward_with_holder_grows_owned_and_survives_next_fight_setup()
|
||||
{
|
||||
var run = RunProgress.Start(3, Cards.Catalog);
|
||||
var before = run.Owned.Count;
|
||||
RunRewards.AddCard(run, Cards.Spark, Factions.Guard);
|
||||
Assert.Equal(before + 1, run.Owned.Count);
|
||||
|
||||
var upgraded = run.Owned[^1];
|
||||
Assert.True(RunRewards.ApplyUpgrade(upgraded, Cards.Spark, SimpleUpgrade.ReduceCost));
|
||||
Assert.Equal(Cards.Spark.Cost - 1, upgraded.Cost);
|
||||
|
||||
var state = MatchSetup.CreateFromRun(run, Cards.Catalog, shuffle: false);
|
||||
var spark = state.Players[0].Deck.Concat(state.Players[0].Hand)
|
||||
.First(e => e.Definition.Id == Cards.Spark.Id);
|
||||
Assert.Equal(Cards.Spark.Cost - 1, spark.Cost);
|
||||
Assert.Equal(Factions.Guard, spark.HolderId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SyncStatsFrom_carries_pack_progress_into_next_fight()
|
||||
{
|
||||
var run = RunProgress.Start(9, Cards.Catalog);
|
||||
var state = MatchSetup.CreateFromRun(run, Cards.Catalog, shuffle: false);
|
||||
state.Players[0].Run.HealReceived = 4;
|
||||
state.Players[0].Run.AttacksDeclared = 2;
|
||||
run.SyncStatsFrom(state, 0);
|
||||
|
||||
var next = MatchSetup.CreateFromRun(run, Cards.Catalog, shuffle: false);
|
||||
Assert.Equal(4, next.Players[0].Run.HealReceived);
|
||||
Assert.Equal(2, next.Players[0].Run.AttacksDeclared);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RollOffer_is_deterministic_for_same_rng_stream()
|
||||
{
|
||||
var pool = new[] { "a", "b", "c", "d" };
|
||||
var a = RunRewards.RollOffer(new GameRng(99), pool, 3);
|
||||
var b = RunRewards.RollOffer(new GameRng(99), pool, 3);
|
||||
Assert.Equal(a, b);
|
||||
Assert.Equal(3, a.Count);
|
||||
Assert.Equal(a.Distinct().Count(), a.Count);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
using CardGame.Core;
|
||||
using Xunit;
|
||||
|
||||
namespace Game.Core.Tests;
|
||||
|
||||
public sealed class StartingCombatTests
|
||||
{
|
||||
[Fact]
|
||||
public void Starting_package_is_sixteen_cards()
|
||||
{
|
||||
var deck = StartingPackage.BuildSharedDeck(Cards.Catalog);
|
||||
Assert.Equal(16, deck.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Patrol_summons_militia_on_play()
|
||||
{
|
||||
var state = MatchSetup.Create(
|
||||
1,
|
||||
Cards.Repeat(Cards.Patrol, 10),
|
||||
Cards.Repeat(Cards.Wisp, 10),
|
||||
Cards.Catalog,
|
||||
shuffle: false);
|
||||
var patrol = state.Players[0].Hand[0];
|
||||
state = RulesEngine.Execute(state, new PlayCardAction(0, patrol.Id));
|
||||
Assert.Equal(2, state.Board.Count(e => e.ControllerIndex == 0 && e.Definition.Type == CardType.Creature));
|
||||
Assert.Contains(state.Board, e => e.Definition.Id == "card.guard.militia");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Conscript_creates_1_cost_crossbow_that_keeps_cost_in_zones()
|
||||
{
|
||||
var state = MatchSetup.Create(
|
||||
1,
|
||||
new List<CardDefinition> { Cards.Conscript }.Concat(Cards.Repeat(Cards.Wisp, 15)).ToList(),
|
||||
Cards.Repeat(Cards.Wisp, 10),
|
||||
Cards.Catalog,
|
||||
shuffle: false);
|
||||
state.Players[0].Energy = 5;
|
||||
var spell = state.Players[0].Hand.First(c => c.Definition.Id == Cards.Conscript.Id);
|
||||
state = RulesEngine.Execute(state, new PlayCardAction(0, spell.Id));
|
||||
var bow = state.Players[0].Hand.Single(c => c.Definition.Id == Cards.Crossbow.Id);
|
||||
Assert.Equal(1, bow.Cost);
|
||||
Assert.Equal(3, bow.Definition.Cost);
|
||||
state = RulesEngine.Execute(state, new PlayCardAction(0, bow.Id));
|
||||
var onBoard = state.Board.Single(e => e.Definition.Id == Cards.Crossbow.Id);
|
||||
Assert.Equal(1, onBoard.Cost);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Captain_aura_buffs_current_1_cost_creatures()
|
||||
{
|
||||
var state = MatchSetup.Create(
|
||||
1,
|
||||
new List<CardDefinition> { Cards.Captain, Cards.Patrol }.Concat(Cards.Repeat(Cards.Wisp, 14)).ToList(),
|
||||
Cards.Repeat(Cards.Wisp, 10),
|
||||
Cards.Catalog,
|
||||
shuffle: false);
|
||||
state.Players[0].Energy = 10;
|
||||
var captain = state.Players[0].Hand.First(c => c.Definition.Id == Cards.Captain.Id);
|
||||
var patrol = state.Players[0].Hand.First(c => c.Definition.Id == Cards.Patrol.Id);
|
||||
state = RulesEngine.Execute(state, new PlayCardAction(0, captain.Id));
|
||||
state = RulesEngine.Execute(state, new PlayCardAction(0, patrol.Id));
|
||||
var militia = state.Board.Single(e => e.Definition.Id == "card.guard.militia");
|
||||
Assert.Equal(2, state.EffectiveAttack(militia));
|
||||
Assert.Equal(2, state.EffectiveAttack(state.Find(patrol.Id)!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Believer_value_chain_attacks_then_transfers()
|
||||
{
|
||||
var state = new GameState(3) { Catalog = Cards.Catalog };
|
||||
MatchSetup.PlaceSystemField(state, 0, Cards.BaseField);
|
||||
MatchSetup.PlaceSystemField(state, 1, Cards.BaseField);
|
||||
|
||||
var a = state.NewEntity(Cards.Believer, 0, Zone.Board, Factions.Cult);
|
||||
var b = state.NewEntity(Cards.Believer, 0, Zone.Board, Factions.Cult);
|
||||
a.EnteredOnTurn = 0;
|
||||
b.EnteredOnTurn = 0;
|
||||
a.AttacksRemaining = 1;
|
||||
b.AttacksRemaining = 1;
|
||||
a.ActivatesRemaining = 1;
|
||||
b.ActivatesRemaining = 1;
|
||||
state.Board.Add(a);
|
||||
state.Board.Add(b);
|
||||
|
||||
var civilian = state.NewEntity(Cards.Get("card.cult.civilian"), 0, Zone.Board, Factions.Cult);
|
||||
civilian.EnteredOnTurn = 0;
|
||||
civilian.AttacksRemaining = 1;
|
||||
state.Board.Add(civilian);
|
||||
|
||||
state.ActivePlayerIndex = 0;
|
||||
state.TurnNumber = 2;
|
||||
|
||||
state = RulesEngine.Execute(state, new AttackAction(0, a.Id, new PlayerTarget(1)));
|
||||
Assert.Equal(19, state.Players[1].Life);
|
||||
|
||||
state = RulesEngine.Execute(state, new ActivateAction(0, a.Id, new EntityTarget(b.Id)));
|
||||
Assert.DoesNotContain(state.Board, e => e.Id == a.Id);
|
||||
Assert.Equal(2, state.Find(b.Id)!.Attack);
|
||||
Assert.Equal(4, state.Find(b.Id)!.Health);
|
||||
|
||||
state = RulesEngine.Execute(state, new AttackAction(0, b.Id, new PlayerTarget(1)));
|
||||
Assert.Equal(17, state.Players[1].Life);
|
||||
|
||||
state = RulesEngine.Execute(state, new ActivateAction(0, b.Id, new EntityTarget(civilian.Id)));
|
||||
var civ = state.Find(civilian.Id)!;
|
||||
Assert.Equal(2, civ.Attack);
|
||||
Assert.Equal(5, civ.Health);
|
||||
Assert.Equal(1, civ.AttacksRemaining);
|
||||
|
||||
state = RulesEngine.Execute(state, new AttackAction(0, civilian.Id, new PlayerTarget(1)));
|
||||
Assert.Equal(15, state.Players[1].Life);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Can_activate_on_enter_turn_but_not_attack()
|
||||
{
|
||||
var state = MatchSetup.Create(
|
||||
1,
|
||||
Cards.Repeat(Cards.Believer, 10),
|
||||
Cards.Repeat(Cards.Believer, 10),
|
||||
Cards.Catalog,
|
||||
shuffle: false);
|
||||
var first = state.Players[0].Hand[0];
|
||||
state = RulesEngine.Execute(state, new PlayCardAction(0, first.Id));
|
||||
var played = state.Find(first.Id)!;
|
||||
Assert.Equal(0, played.AttacksRemaining);
|
||||
Assert.Equal(1, played.ActivatesRemaining);
|
||||
Assert.DoesNotContain(RulesEngine.GetLegalActions(state), a => a is AttackAction);
|
||||
Assert.DoesNotContain(
|
||||
RulesEngine.GetLegalActions(state),
|
||||
a => a is ActivateAction act && act.SourceId == first.Id);
|
||||
// only one believer on board — activate needs another friendly
|
||||
var second = state.Players[0].Hand[0];
|
||||
state.Players[0].Energy = 1;
|
||||
state = RulesEngine.Execute(state, new PlayCardAction(0, second.Id));
|
||||
Assert.Contains(
|
||||
RulesEngine.GetLegalActions(state),
|
||||
a => a is ActivateAction act && act.SourceId == first.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Starting_battle_boots()
|
||||
{
|
||||
var state = MatchSetup.CreateStartingBattle(20261006, Cards.Catalog, shuffle: false);
|
||||
Assert.Equal(16 - GameState.OpeningHand - 1, state.Players[0].Deck.Count);
|
||||
Assert.Equal(GameState.OpeningHand + 1, state.Players[0].Hand.Count);
|
||||
Assert.Equal(1, state.Players[0].Energy);
|
||||
Assert.Contains(state.Board, e => e.IsSystem);
|
||||
Assert.NotEmpty(RulesEngine.GetLegalActions(state));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user