oecs-sharp/Game.CardWars.Tests/PlayLogTests.cs

288 lines
11 KiB
C#
Raw Permalink Normal View History

using System.Text;
using FluentAssertions;
using Game.CardWars;
using OECS;
using Xunit;
using Xunit.Abstractions;
namespace Game.CardWars.Tests;
public class PlayLogTests
{
private readonly ITestOutputHelper _output;
public PlayLogTests(ITestOutputHelper output)
{
_output = output;
}
[Fact]
public void OneRound_ThreePlayers_PlayLog()
{
CardEffectRegistry.ClearForTests();
var (world, group) = GameFactory.Create(playerCount: 3, seed: 12345);
var log = new StringBuilder();
var players = GameUtil.FindAllEntities<Player>(world);
var state = world.ReadSingleton<GameState>();
LogSection(log, "=== CardWars Play Log — Round 1 ===");
log.AppendLine($"Seed: {state.Seed} | Players: {state.PlayerCount} | Starting: P{state.StartingPlayerIndex}");
log.AppendLine();
// ── Initial hands ──
LogSection(log, "── Starting Hands ──");
for (int i = 0; i < players.Count; i++)
log.AppendLine($" P{i}: {DescribeHand(world, GameUtil.GetHandCards(world, players[i]))}");
log.AppendLine();
// ── Play Phase ──
LogSection(log, "── Play Phase ──");
RunPlayPhase(world, group, players, log);
// ── Flip Phase ──
LogSection(log, "── Flip Phase ──");
RunFlipPhase(world, group, players, log);
// ── Scoring (capture powers BEFORE cleanup) ──
LogSection(log, "── Scoring ──");
var powers = new int[players.Count];
for (int i = 0; i < players.Count; i++)
powers[i] = GameUtil.CalculatePower(world, players[i]);
// Advance to scoring phase and let systems run.
ref var mutable = ref world.GetSingleton<GameState>();
mutable.Phase = GamePhase.Scoring;
world.MarkModified<GameState>(World.SingletonEntity);
group.RunLogical(); // ScoringSystem → CleanupSystem
for (int i = 0; i < players.Count; i++)
log.AppendLine($" P{i} power: {powers[i]}");
int winnerIdx = Array.IndexOf(powers, powers.Max());
log.AppendLine($" Winner: P{winnerIdx} (power={powers[winnerIdx]})");
// Find the castle that was just awarded (it has HeldBy but no Castle component).
var wonCastleEntity = world.GetSources<HeldBy>(players[winnerIdx])
.FirstOrDefault(c => !world.HasComponent<Castle>(c) && !world.HasComponent<CardDef>(c));
log.AppendLine($" Castle awarded to P{winnerIdx}");
log.AppendLine();
// ── Cleanup already ran ──
LogSection(log, "── After Cleanup ──");
state = world.ReadSingleton<GameState>();
log.AppendLine($" Phase: {state.Phase} | Round: {state.RoundNumber}");
for (int i = 0; i < players.Count; i++)
{
var hand = GameUtil.GetHandCards(world, players[i]);
log.AppendLine($" P{i} hand: {DescribeHand(world, hand)}");
}
log.AppendLine();
// ── Castle counts ──
LogSection(log, "── Castle Tally ──");
for (int i = 0; i < players.Count; i++)
{
// A won castle: has HeldBy to this player, no CardDef, no Card, no Leader, no Banner.
var castleEntities = world.GetSources<HeldBy>(players[i])
.Where(c => !world.HasComponent<CardDef>(c)
&& !world.HasComponent<Card>(c)
&& !world.HasComponent<Leader>(c)
&& !world.HasComponent<Banner>(c))
.ToList();
log.AppendLine($" P{i}: {castleEntities.Count} castle(s)");
}
log.AppendLine();
_output.WriteLine(log.ToString());
state.Phase.Should().BeOneOf(GamePhase.PlayPhase, GamePhase.GameOver);
}
// ── Play phase: greedy agent, plays best card each turn ──
private static void RunPlayPhase(World world, SystemGroup group, List<Entity> players, StringBuilder log)
{
var state = world.ReadSingleton<GameState>();
int startIdx = state.StartingPlayerIndex;
int current = startIdx;
int safety = 0;
while (safety++ < 100)
{
state = world.ReadSingleton<GameState>();
if (state.Phase != GamePhase.PlayPhase) break;
var player = players[state.CurrentPlayerIndex];
var hand = GameUtil.GetHandCards(world, player);
if (hand.Count > 0)
{
var best = PickBestCard(world, hand);
var def = world.ReadComponent<CardDef>(best);
int rank = def.Ranks.Max();
world.Commands.Enqueue(new PlayCardCommand
{
CardEntity = best,
Rank = rank,
FaceDown = false,
TargetPlayer = null
});
group.RunLogical();
log.AppendLine($" P{state.CurrentPlayerIndex} plays [{def.Name}] rank={rank}");
// Resolve any pending choices automatically.
ResolvePendingChoices(world, group, players, log);
}
else
{
world.Commands.Enqueue(new SkipPlayCommand());
group.RunLogical();
log.AppendLine($" P{state.CurrentPlayerIndex} skips (hand empty)");
}
}
state = world.ReadSingleton<GameState>();
log.AppendLine($" Phase → {state.Phase}");
log.AppendLine();
}
// ── Flip phase ──
private static void RunFlipPhase(World world, SystemGroup group, List<Entity> players, StringBuilder log)
{
var state = world.ReadSingleton<GameState>();
if (state.Phase != GamePhase.FlipPhase)
{
ref var m = ref world.GetSingleton<GameState>();
m.Phase = GamePhase.FlipPhase;
m.CurrentPlayerIndex = m.StartingPlayerIndex;
world.MarkModified<GameState>(World.SingletonEntity);
}
for (int i = 0; i < players.Count; i++)
{
state = world.ReadSingleton<GameState>();
var player = players[state.CurrentPlayerIndex];
var field = GameUtil.GetFieldCards(world, player);
bool anyFaceDown = field.Any(c =>
world.HasComponent<Card>(c) && world.ReadComponent<Card>(c).FaceDown);
if (anyFaceDown)
{
var fd = field.First(c =>
world.HasComponent<Card>(c) && world.ReadComponent<Card>(c).FaceDown);
world.Commands.Enqueue(new FlipCardCommand { CardEntity = fd });
group.RunLogical();
var def = world.ReadComponent<CardDef>(fd);
log.AppendLine($" P{state.CurrentPlayerIndex} flips [{def.Name}]");
ResolvePendingChoices(world, group, players, log);
}
else
{
log.AppendLine($" P{state.CurrentPlayerIndex} skips flip (no face-down)");
}
ref var m2 = ref world.GetSingleton<GameState>();
m2.CurrentPlayerIndex = (m2.CurrentPlayerIndex + 1) % m2.PlayerCount;
world.MarkModified<GameState>(World.SingletonEntity);
}
log.AppendLine();
}
// ── Pending choice auto-resolver ──
private static void ResolvePendingChoices(World world, SystemGroup group, List<Entity> players, StringBuilder log)
{
while (PendingChoice.Any(world))
{
Entity target = PickDefaultTarget(world, players);
world.Commands.Enqueue(new ResolveChoiceCommand { Target = target });
group.RunLogical();
log.AppendLine($" ↳ auto-resolve → {DescribeEntity(world, target)}");
}
}
private static Entity PickDefaultTarget(World world, List<Entity> players)
{
if (world.HasSingleton<PendingMercenary>())
{
var p = world.ReadSingleton<PendingMercenary>();
return GameUtil.GetFieldCards(world, p.Player)
.FirstOrDefault(c => world.HasComponent<Card>(c) && world.ReadComponent<Card>(c).Rank > 0);
}
if (world.HasSingleton<PendingDancer>())
{
var p = world.ReadSingleton<PendingDancer>();
return GameUtil.GetFieldCards(world, p.Player)
.FirstOrDefault(c => world.HasComponent<Card>(c) && world.ReadComponent<Card>(c).Rank > 0);
}
if (world.HasSingleton<PendingPaladin>())
{
var p = world.ReadSingleton<PendingPaladin>();
var powered = GameUtil.GetFieldCards(world, p.Player)
.FirstOrDefault(c => world.HasComponent<Card>(c) && world.ReadComponent<Card>(c).Rank > 0);
return powered != Entity.Null ? powered : CardEffectHelpers.GetBanner(world, p.Player);
}
if (world.HasSingleton<PendingNun>())
{
var p = world.ReadSingleton<PendingNun>();
return GameUtil.GetFieldCards(world, p.Player).FirstOrDefault();
}
if (world.HasSingleton<PendingScout>())
{
var p = world.ReadSingleton<PendingScout>();
return players.FirstOrDefault(pl => pl != p.Player);
}
if (world.HasSingleton<PendingPegasus>())
{
var p = world.ReadSingleton<PendingPegasus>();
return GameUtil.GetFieldCards(world, p.Player).FirstOrDefault(c => c != p.CardEntity);
}
if (world.HasSingleton<PendingCurseMaster>())
{
return GameUtil.FindAllEntities<Horn>(world).FirstOrDefault();
}
return Entity.Null;
}
// ── Helpers ──
private static Entity PickBestCard(World world, List<Entity> hand)
{
Entity best = Entity.Null;
int bestRank = -999;
foreach (var c in hand)
{
int maxRank = world.ReadComponent<CardDef>(c).Ranks.Max();
if (maxRank > bestRank) { bestRank = maxRank; best = c; }
}
return best;
}
private static string DescribeHand(World world, List<Entity> hand)
{
var cards = hand.Where(c => world.HasComponent<CardDef>(c)).ToList();
if (cards.Count == 0) return "(empty)";
return string.Join(" ", cards.Select(c =>
{
var def = world.ReadComponent<CardDef>(c);
return $"[{def.Name} {string.Join("/", def.Ranks)}]";
}));
}
private static string DescribeEntity(World world, Entity e)
{
if (e == Entity.Null) return "(none)";
if (world.HasComponent<CardDef>(e)) return $"[{world.ReadComponent<CardDef>(e).Name}]";
if (world.HasComponent<Player>(e)) return $"P{world.ReadComponent<Player>(e).Index}";
if (world.HasComponent<Horn>(e)) return "[Horn token]";
if (world.HasComponent<Skull>(e)) return "[Skull token]";
if (world.HasComponent<Banner>(e)) return "[Banner]";
return e.ToString();
}
private static void LogSection(StringBuilder log, string title) => log.AppendLine(title);
}