feat: implement CardWars game engine
Implement the core CardWars game logic using an ECS-based architecture. This includes: - Card effect registry and various card effect implementations. - Game phase management (Setup, Play, Flip, Scoring, Cleanup). - Command system for player actions (PlayCard, FlipCard, etc.). - Component-based game state and entity relationships. - Automated game setup and scoring systems. - Unit tests for game setup, card effects, and play logs.
This commit is contained in:
parent
3b08138580
commit
2de735498c
|
|
@ -0,0 +1,139 @@
|
|||
using FluentAssertions;
|
||||
using Game.CardWars;
|
||||
using OECS;
|
||||
using Xunit;
|
||||
|
||||
namespace Game.CardWars.Tests;
|
||||
|
||||
public class CardEffectTests
|
||||
{
|
||||
private static (World World, SystemGroup Group) Setup()
|
||||
{
|
||||
CardEffectRegistry.ClearForTests();
|
||||
return GameFactory.Create(playerCount: 2, seed: 42);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarriorEffect_AddsHornWhenAnotherWarriorExists()
|
||||
{
|
||||
var (world, group) = Setup();
|
||||
|
||||
var state = world.ReadSingleton<GameState>();
|
||||
var player = PlayCardCommand.FindPlayerByIndex(world, state.CurrentPlayerIndex);
|
||||
|
||||
// Play two warriors.
|
||||
var hand = GameUtil.GetHandCards(world, player);
|
||||
var warrior1 = FindCardWithEffect(world, hand, CardEffect.Warrior);
|
||||
var warrior2 = FindSecondCardWithEffect(world, hand, CardEffect.Warrior);
|
||||
|
||||
if (warrior1 == Entity.Null || warrior2 == Entity.Null) return; // Not enough warriors in deck.
|
||||
|
||||
var def = world.ReadComponent<CardDef>(warrior1);
|
||||
|
||||
world.Commands.Enqueue(new PlayCardCommand
|
||||
{
|
||||
CardEntity = warrior1,
|
||||
Rank = def.Ranks[0],
|
||||
FaceDown = false,
|
||||
TargetPlayer = null
|
||||
});
|
||||
group.RunLogical();
|
||||
|
||||
// First warrior: no other warrior, no horn.
|
||||
GameUtil.GetFieldCards(world, player).Should().Contain(warrior1);
|
||||
world.HasComponent<Horn>(warrior1).Should().BeFalse(because: "no other warrior on field");
|
||||
|
||||
// Play second warrior.
|
||||
var def2 = world.ReadComponent<CardDef>(warrior2);
|
||||
world.Commands.Enqueue(new PlayCardCommand
|
||||
{
|
||||
CardEntity = warrior2,
|
||||
Rank = def2.Ranks[0],
|
||||
FaceDown = false,
|
||||
TargetPlayer = null
|
||||
});
|
||||
group.RunLogical();
|
||||
|
||||
// Second warrior has another warrior, should get horn.
|
||||
// But Wait — the effect checks "other" warriors on field. The first warrior
|
||||
// is on field, so the second warrior should get a horn.
|
||||
world.HasComponent<Horn>(warrior2).Should().BeTrue(because: "another warrior exists on field");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MercenaryEffect_SetsPendingChoice()
|
||||
{
|
||||
var (world, group) = Setup();
|
||||
|
||||
var state = world.ReadSingleton<GameState>();
|
||||
var player = PlayCardCommand.FindPlayerByIndex(world, state.CurrentPlayerIndex);
|
||||
|
||||
// Play first mercenary (no effect).
|
||||
var hand = GameUtil.GetHandCards(world, player);
|
||||
var merc = FindCardWithEffect(world, hand, CardEffect.Mercenary);
|
||||
if (merc == Entity.Null) return;
|
||||
|
||||
var def = world.ReadComponent<CardDef>(merc);
|
||||
world.Commands.Enqueue(new PlayCardCommand
|
||||
{
|
||||
CardEntity = merc,
|
||||
Rank = def.Ranks[0],
|
||||
FaceDown = false,
|
||||
TargetPlayer = null
|
||||
});
|
||||
group.RunLogical();
|
||||
|
||||
// Play second mercenary. We need another one from a drawn card.
|
||||
var publicDeck = GameUtil.FindEntity<PublicDeck>(world);
|
||||
GameUtil.DrawCard(world, publicDeck, player);
|
||||
var newHand = GameUtil.GetHandCards(world, player);
|
||||
var merc2 = FindCardWithEffect(world, newHand, CardEffect.Mercenary);
|
||||
if (merc2 == Entity.Null) return;
|
||||
|
||||
var def2 = world.ReadComponent<CardDef>(merc2);
|
||||
world.Commands.Enqueue(new PlayCardCommand
|
||||
{
|
||||
CardEntity = merc2,
|
||||
Rank = def2.Ranks[0],
|
||||
FaceDown = false,
|
||||
TargetPlayer = null
|
||||
});
|
||||
group.RunLogical();
|
||||
|
||||
// Should have a pending Mercenary choice.
|
||||
world.HasSingleton<PendingMercenary>().Should().BeTrue();
|
||||
var pending = world.ReadSingleton<PendingMercenary>();
|
||||
pending.CardEntity.Should().Be(merc2);
|
||||
|
||||
// Resolve the choice (pick a target).
|
||||
world.Commands.Enqueue(new ResolveChoiceCommand { Target = merc });
|
||||
group.RunLogical();
|
||||
|
||||
// The pending should be cleared.
|
||||
world.HasSingleton<PendingMercenary>().Should().BeFalse();
|
||||
}
|
||||
|
||||
private static Entity FindCardWithEffect(World world, List<Entity> cards, CardEffect effect)
|
||||
{
|
||||
foreach (var c in cards)
|
||||
{
|
||||
if (world.HasComponent<CardDef>(c) && world.ReadComponent<CardDef>(c).Effect == effect)
|
||||
return c;
|
||||
}
|
||||
return Entity.Null;
|
||||
}
|
||||
|
||||
private static Entity FindSecondCardWithEffect(World world, List<Entity> cards, CardEffect effect)
|
||||
{
|
||||
Entity first = Entity.Null;
|
||||
foreach (var c in cards)
|
||||
{
|
||||
if (world.HasComponent<CardDef>(c) && world.ReadComponent<CardDef>(c).Effect == effect)
|
||||
{
|
||||
if (first == Entity.Null) first = c;
|
||||
else return c;
|
||||
}
|
||||
}
|
||||
return Entity.Null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Game.CardWars.Tests</RootNamespace>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.1" />
|
||||
<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="..\Game.CardWars\CardWars.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
using FluentAssertions;
|
||||
using Game.CardWars;
|
||||
using OECS;
|
||||
using Xunit;
|
||||
|
||||
namespace Game.CardWars.Tests;
|
||||
|
||||
public class GameSetupTests
|
||||
{
|
||||
private static (World World, SystemGroup Group) Setup()
|
||||
{
|
||||
// Reset registry state between tests.
|
||||
CardEffectRegistry.ClearForTests();
|
||||
return GameFactory.Create(playerCount: 2, seed: 42);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateGame_CreatesPlayersAndDecks()
|
||||
{
|
||||
var (world, _) = Setup();
|
||||
|
||||
var state = world.ReadSingleton<GameState>();
|
||||
state.Phase.Should().Be(GamePhase.PlayPhase);
|
||||
state.PlayerCount.Should().Be(2);
|
||||
state.RoundNumber.Should().Be(1);
|
||||
|
||||
GameUtil.FindEntity<PublicDeck>(world).Should().NotBe(Entity.Null);
|
||||
GameUtil.FindAllEntities<Player>(world).Should().HaveCount(2);
|
||||
GameUtil.FindAllEntities<Leader>(world).Should().HaveCount(2);
|
||||
GameUtil.FindAllEntities<Banner>(world).Should().HaveCount(2);
|
||||
GameUtil.FindAllEntities<Castle>(world).Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StartingHand_HasThreeCards()
|
||||
{
|
||||
var (world, _) = Setup();
|
||||
|
||||
foreach (var player in GameUtil.FindAllEntities<Player>(world))
|
||||
{
|
||||
var hand = GameUtil.GetHandCards(world, player);
|
||||
hand.Should().HaveCount(3);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlayCard_MovesCardFromHandToField()
|
||||
{
|
||||
var (world, group) = Setup();
|
||||
|
||||
var state = world.ReadSingleton<GameState>();
|
||||
var player = PlayCardCommand.FindPlayerByIndex(world, state.CurrentPlayerIndex);
|
||||
|
||||
var hand = GameUtil.GetHandCards(world, player);
|
||||
var cardInHand = hand[0];
|
||||
var def = world.ReadComponent<CardDef>(cardInHand);
|
||||
|
||||
world.Commands.Enqueue(new PlayCardCommand
|
||||
{
|
||||
CardEntity = cardInHand,
|
||||
Rank = def.Ranks[0],
|
||||
FaceDown = false,
|
||||
TargetPlayer = null
|
||||
});
|
||||
group.RunLogical();
|
||||
|
||||
GameUtil.GetFieldCards(world, player).Should().Contain(cardInHand);
|
||||
var cardData = world.ReadComponent<Card>(cardInHand);
|
||||
cardData.Rank.Should().Be(def.Ranks[0]);
|
||||
cardData.FaceDown.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlayCardFaceDown_CardIsHidden()
|
||||
{
|
||||
var (world, group) = Setup();
|
||||
|
||||
var state = world.ReadSingleton<GameState>();
|
||||
var player = PlayCardCommand.FindPlayerByIndex(world, state.CurrentPlayerIndex);
|
||||
|
||||
var hand = GameUtil.GetHandCards(world, player);
|
||||
var cardInHand = hand[0];
|
||||
var def = world.ReadComponent<CardDef>(cardInHand);
|
||||
|
||||
world.Commands.Enqueue(new PlayCardCommand
|
||||
{
|
||||
CardEntity = cardInHand,
|
||||
Rank = def.Ranks[0],
|
||||
FaceDown = true,
|
||||
TargetPlayer = null
|
||||
});
|
||||
group.RunLogical();
|
||||
|
||||
world.ReadComponent<Card>(cardInHand).FaceDown.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FlipCard_RevealsFaceDownCard()
|
||||
{
|
||||
var (world, group) = Setup();
|
||||
|
||||
var state = world.ReadSingleton<GameState>();
|
||||
var player = PlayCardCommand.FindPlayerByIndex(world, state.CurrentPlayerIndex);
|
||||
|
||||
var hand = GameUtil.GetHandCards(world, player);
|
||||
var cardInHand = hand[0];
|
||||
var def = world.ReadComponent<CardDef>(cardInHand);
|
||||
|
||||
// Play face-down.
|
||||
world.Commands.Enqueue(new PlayCardCommand
|
||||
{
|
||||
CardEntity = cardInHand,
|
||||
Rank = def.Ranks[0],
|
||||
FaceDown = true,
|
||||
TargetPlayer = null
|
||||
});
|
||||
group.RunLogical();
|
||||
|
||||
// Advance to flip phase: both players skip.
|
||||
world.Commands.Enqueue(new SkipPlayCommand());
|
||||
group.RunLogical();
|
||||
world.Commands.Enqueue(new SkipPlayCommand());
|
||||
group.RunLogical();
|
||||
|
||||
world.ReadSingleton<GameState>().Phase.Should().Be(GamePhase.FlipPhase);
|
||||
|
||||
// Flip it.
|
||||
world.Commands.Enqueue(new FlipCardCommand { CardEntity = cardInHand });
|
||||
group.RunLogical();
|
||||
|
||||
world.ReadComponent<Card>(cardInHand).FaceDown.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SkipPlay_AdvancesTurn()
|
||||
{
|
||||
var (world, group) = Setup();
|
||||
|
||||
var state = world.ReadSingleton<GameState>();
|
||||
int originalPlayer = state.CurrentPlayerIndex;
|
||||
|
||||
world.Commands.Enqueue(new SkipPlayCommand());
|
||||
group.RunLogical();
|
||||
|
||||
state = world.ReadSingleton<GameState>();
|
||||
state.CurrentPlayerIndex.Should().Be((originalPlayer + 1) % 2);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,288 @@
|
|||
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);
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
using OECS;
|
||||
|
||||
namespace Game.CardWars;
|
||||
|
||||
/// <summary>
|
||||
/// Loads card definitions from a CSV and creates card entities in the world.
|
||||
/// </summary>
|
||||
public static class CardDataLoader
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates all card definition entities in the world from CSV data.
|
||||
/// Returns a list of (entity, CardDef) pairs.
|
||||
/// </summary>
|
||||
public static List<(Entity Entity, CardDef Def)> LoadDefinitions(World world, string csv)
|
||||
{
|
||||
var results = new List<(Entity, CardDef)>();
|
||||
var lines = csv.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
|
||||
if (lines.Length < 2)
|
||||
return results;
|
||||
|
||||
// Skip header line.
|
||||
for (int i = 1; i < lines.Length; i++)
|
||||
{
|
||||
var line = lines[i].Trim();
|
||||
if (string.IsNullOrEmpty(line)) continue;
|
||||
|
||||
var def = ParseLine(line);
|
||||
if (def.Name == null) continue;
|
||||
|
||||
var entity = world.CreateEntity();
|
||||
world.AddComponent(entity, def);
|
||||
results.Add((entity, def));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static CardDef ParseLine(string line)
|
||||
{
|
||||
// Simple CSV parsing: name,ranks,effect
|
||||
// Ranks are semicolon-separated.
|
||||
var parts = SplitCsv(line);
|
||||
if (parts.Length < 3)
|
||||
return default;
|
||||
|
||||
var name = parts[0].Trim();
|
||||
var rankStrs = parts[1].Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
var ranks = new int[rankStrs.Length];
|
||||
for (int i = 0; i < rankStrs.Length; i++)
|
||||
ranks[i] = int.Parse(rankStrs[i]);
|
||||
|
||||
var effect = ParseEffect(parts[2].Trim());
|
||||
|
||||
return new CardDef
|
||||
{
|
||||
Name = name,
|
||||
Ranks = ranks,
|
||||
Effect = effect,
|
||||
Kind = CardKind.Public
|
||||
};
|
||||
}
|
||||
|
||||
private static string[] SplitCsv(string line)
|
||||
{
|
||||
var result = new List<string>();
|
||||
var current = new System.Text.StringBuilder();
|
||||
bool inQuotes = false;
|
||||
|
||||
for (int i = 0; i < line.Length; i++)
|
||||
{
|
||||
char c = line[i];
|
||||
if (c == '"')
|
||||
{
|
||||
inQuotes = !inQuotes;
|
||||
}
|
||||
else if (c == ',' && !inQuotes)
|
||||
{
|
||||
result.Add(current.ToString());
|
||||
current.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
current.Append(c);
|
||||
}
|
||||
}
|
||||
result.Add(current.ToString());
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
private static CardEffect ParseEffect(string effect)
|
||||
{
|
||||
return effect switch
|
||||
{
|
||||
string s when s.Contains("战士") => CardEffect.Warrior,
|
||||
string s when s.Contains("弓手") => CardEffect.Archer,
|
||||
string s when s.Contains("佣兵") => CardEffect.Mercenary,
|
||||
string s when s.Contains("商人") => CardEffect.Merchant,
|
||||
string s when s.Contains("舞姬") => CardEffect.Dancer,
|
||||
string s when s.Contains("圣骑士") => CardEffect.Paladin,
|
||||
string s when s.Contains("飞马") => CardEffect.Pegasus,
|
||||
string s when s.Contains("诅咒师") => CardEffect.CurseMaster,
|
||||
string s when s.Contains("魔导士") => CardEffect.Mage,
|
||||
string s when s.Contains("公主") => CardEffect.Princess,
|
||||
string s when s.Contains("决斗家") => CardEffect.Duelist,
|
||||
string s when s.Contains("修女") => CardEffect.Nun,
|
||||
string s when s.Contains("斥候") => CardEffect.Scout,
|
||||
string s when s.Contains("女巫") => CardEffect.Witch,
|
||||
string s when s.Contains("盗贼") => CardEffect.Thief,
|
||||
_ => CardEffect.None
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
using OECS;
|
||||
|
||||
namespace Game.CardWars;
|
||||
|
||||
/// <summary>
|
||||
/// A card effect that can be registered with the effect dispatcher.
|
||||
/// </summary>
|
||||
public interface ICardEffect
|
||||
{
|
||||
CardEffect Effect { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Called when the card is played face-up or flipped face-up.
|
||||
/// Return true if fully resolved. Return false if a pending component
|
||||
/// was set and the effect needs a player command to continue.
|
||||
/// </summary>
|
||||
bool Resolve(World world, Entity card, Entity player, Entity owner);
|
||||
|
||||
/// <summary>
|
||||
/// Called when the player provides a choice to continue a pending effect.
|
||||
/// The effect handler is responsible for reading the correct pending
|
||||
/// component type from the singleton and removing it when done.
|
||||
/// </summary>
|
||||
bool ResolveChoice(World world, Entity target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registry of card effects, ordered by registration.
|
||||
/// </summary>
|
||||
public static class CardEffectRegistry
|
||||
{
|
||||
private static readonly List<ICardEffect> _effects = new();
|
||||
private static bool _frozen;
|
||||
|
||||
public static void Add<TEffect>() where TEffect : ICardEffect, new()
|
||||
{
|
||||
if (_frozen)
|
||||
throw new InvalidOperationException("Cannot add effects after the registry is frozen.");
|
||||
_effects.Add(new TEffect());
|
||||
}
|
||||
|
||||
public static void Freeze() => _frozen = true;
|
||||
|
||||
/// <summary>
|
||||
/// Resolve a card effect. Returns true if fully resolved.
|
||||
/// </summary>
|
||||
public static bool Dispatch(World world, CardEffect effect, Entity card, Entity player, Entity owner)
|
||||
{
|
||||
foreach (var e in _effects)
|
||||
{
|
||||
if (e.Effect == effect)
|
||||
return e.Resolve(world, card, player, owner);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Look up the handler for the currently pending effect and resolve it.
|
||||
/// </summary>
|
||||
public static bool DispatchChoice(World world, Entity target)
|
||||
{
|
||||
// Find which pending component is active and dispatch to the matching handler.
|
||||
if (world.HasSingleton<PendingMercenary>())
|
||||
return DispatchByEffect(world, CardEffect.Mercenary, target);
|
||||
if (world.HasSingleton<PendingDancer>())
|
||||
return DispatchByEffect(world, CardEffect.Dancer, target);
|
||||
if (world.HasSingleton<PendingPaladin>())
|
||||
return DispatchByEffect(world, CardEffect.Paladin, target);
|
||||
if (world.HasSingleton<PendingNun>())
|
||||
return DispatchByEffect(world, CardEffect.Nun, target);
|
||||
if (world.HasSingleton<PendingScout>())
|
||||
return DispatchByEffect(world, CardEffect.Scout, target);
|
||||
if (world.HasSingleton<PendingPegasus>())
|
||||
return DispatchByEffect(world, CardEffect.Pegasus, target);
|
||||
if (world.HasSingleton<PendingCurseMaster>())
|
||||
return DispatchByEffect(world, CardEffect.CurseMaster, target);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool DispatchByEffect(World world, CardEffect effect, Entity target)
|
||||
{
|
||||
foreach (var e in _effects)
|
||||
{
|
||||
if (e.Effect == effect)
|
||||
return e.ResolveChoice(world, target);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void ClearForTests()
|
||||
{
|
||||
_effects.Clear();
|
||||
_frozen = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,402 @@
|
|||
using OECS;
|
||||
|
||||
namespace Game.CardWars;
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// Simple effects that resolve immediately.
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
public class WarriorEffect : ICardEffect
|
||||
{
|
||||
public CardEffect Effect => CardEffect.Warrior;
|
||||
|
||||
public bool Resolve(World world, Entity card, Entity player, Entity owner)
|
||||
{
|
||||
if (HasOtherOnField(world, owner, card, CardEffect.Warrior))
|
||||
CardEffectHelpers.AddHorn(world, card);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ResolveChoice(World world, Entity target) => true;
|
||||
|
||||
private static bool HasOtherOnField(World world, Entity player, Entity self, CardEffect effect)
|
||||
{
|
||||
return GameUtil.GetFieldCards(world, player)
|
||||
.Any(c => c != self && world.HasComponent<CardDef>(c)
|
||||
&& world.ReadComponent<CardDef>(c).Effect == effect);
|
||||
}
|
||||
}
|
||||
|
||||
public class ArcherEffect : ICardEffect
|
||||
{
|
||||
public CardEffect Effect => CardEffect.Archer;
|
||||
|
||||
public bool Resolve(World world, Entity card, Entity player, Entity owner)
|
||||
{
|
||||
if (HasOtherOnField(world, owner, card, CardEffect.Archer))
|
||||
{
|
||||
var banner = CardEffectHelpers.GetBanner(world, owner);
|
||||
if (banner != Entity.Null) CardEffectHelpers.AddHorn(world, banner);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ResolveChoice(World world, Entity target) => true;
|
||||
|
||||
private static bool HasOtherOnField(World world, Entity player, Entity self, CardEffect effect)
|
||||
{
|
||||
return GameUtil.GetFieldCards(world, player)
|
||||
.Any(c => c != self && world.HasComponent<CardDef>(c)
|
||||
&& world.ReadComponent<CardDef>(c).Effect == effect);
|
||||
}
|
||||
}
|
||||
|
||||
public class MercenaryEffect : ICardEffect
|
||||
{
|
||||
public CardEffect Effect => CardEffect.Mercenary;
|
||||
|
||||
public bool Resolve(World world, Entity card, Entity player, Entity owner)
|
||||
{
|
||||
if (!HasOtherOnField(world, owner, card, CardEffect.Mercenary))
|
||||
return true;
|
||||
|
||||
world.SetSingleton(new PendingMercenary { CardEntity = card, Player = player });
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool ResolveChoice(World world, Entity target)
|
||||
{
|
||||
var pending = world.ReadSingleton<PendingMercenary>();
|
||||
if (target != Entity.Null)
|
||||
CardEffectHelpers.AddSkull(world, target);
|
||||
world.RemoveSingleton<PendingMercenary>();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool HasOtherOnField(World world, Entity player, Entity self, CardEffect effect)
|
||||
{
|
||||
return GameUtil.GetFieldCards(world, player)
|
||||
.Any(c => c != self && world.HasComponent<CardDef>(c)
|
||||
&& world.ReadComponent<CardDef>(c).Effect == effect);
|
||||
}
|
||||
}
|
||||
|
||||
public class MerchantEffect : ICardEffect
|
||||
{
|
||||
public CardEffect Effect => CardEffect.Merchant;
|
||||
|
||||
public bool Resolve(World world, Entity card, Entity player, Entity owner)
|
||||
{
|
||||
world.Commands.Enqueue(new DrawCardCommand { Player = player, Kind = null });
|
||||
world.Commands.Enqueue(new ExtraActionCommand());
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ResolveChoice(World world, Entity target) => true;
|
||||
}
|
||||
|
||||
public class DancerEffect : ICardEffect
|
||||
{
|
||||
public CardEffect Effect => CardEffect.Dancer;
|
||||
|
||||
public bool Resolve(World world, Entity card, Entity player, Entity owner)
|
||||
{
|
||||
var powered = GameUtil.GetFieldCards(world, owner)
|
||||
.Where(c => world.HasComponent<Card>(c) && world.ReadComponent<Card>(c).Rank > 0)
|
||||
.ToList();
|
||||
|
||||
if (powered.Count == 0) return true;
|
||||
if (powered.Count <= 2)
|
||||
{
|
||||
foreach (var c in powered)
|
||||
CardEffectHelpers.AddHorn(world, c);
|
||||
return true;
|
||||
}
|
||||
|
||||
world.SetSingleton(new PendingDancer { CardEntity = card, Player = player, Step = 0 });
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool ResolveChoice(World world, Entity target)
|
||||
{
|
||||
var pending = world.ReadSingleton<PendingDancer>();
|
||||
|
||||
if (target != Entity.Null)
|
||||
CardEffectHelpers.AddHorn(world, target);
|
||||
|
||||
if (pending.Step == 0)
|
||||
{
|
||||
world.SetSingleton(new PendingDancer { CardEntity = pending.CardEntity, Player = pending.Player, Step = 1 });
|
||||
return false;
|
||||
}
|
||||
|
||||
world.RemoveSingleton<PendingDancer>();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class PaladinEffect : ICardEffect
|
||||
{
|
||||
public CardEffect Effect => CardEffect.Paladin;
|
||||
|
||||
public bool Resolve(World world, Entity card, Entity player, Entity owner)
|
||||
{
|
||||
world.SetSingleton(new PendingPaladin { CardEntity = card, Player = player });
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool ResolveChoice(World world, Entity target)
|
||||
{
|
||||
if (target != Entity.Null)
|
||||
CardEffectHelpers.AddHorn(world, target);
|
||||
world.RemoveSingleton<PendingPaladin>();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class PrincessEffect : ICardEffect
|
||||
{
|
||||
public CardEffect Effect => CardEffect.Princess;
|
||||
|
||||
public bool Resolve(World world, Entity card, Entity player, Entity owner)
|
||||
{
|
||||
world.Commands.Enqueue(new ReplaceCastleCommand());
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ResolveChoice(World world, Entity target) => true;
|
||||
}
|
||||
|
||||
public class DuelistEffect : ICardEffect
|
||||
{
|
||||
public CardEffect Effect => CardEffect.Duelist;
|
||||
|
||||
public bool Resolve(World world, Entity card, Entity player, Entity owner)
|
||||
{
|
||||
world.Commands.Enqueue(new EndPlayPhaseCommand());
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ResolveChoice(World world, Entity target) => true;
|
||||
}
|
||||
|
||||
public class NunEffect : ICardEffect
|
||||
{
|
||||
public CardEffect Effect => CardEffect.Nun;
|
||||
|
||||
public bool Resolve(World world, Entity card, Entity player, Entity owner)
|
||||
{
|
||||
var fieldCards = GameUtil.GetFieldCards(world, owner);
|
||||
if (fieldCards.Count == 0)
|
||||
{
|
||||
world.Commands.Enqueue(new DrawCardCommand { Player = player, Kind = null });
|
||||
return true;
|
||||
}
|
||||
|
||||
world.SetSingleton(new PendingNun { CardEntity = card, Player = player });
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool ResolveChoice(World world, Entity target)
|
||||
{
|
||||
var pending = world.ReadSingleton<PendingNun>();
|
||||
if (target != Entity.Null)
|
||||
{
|
||||
var discardDeck = CardEffectHelpers.GetDiscardDeck(world, pending.Player);
|
||||
world.RemoveComponent<OnField>(target);
|
||||
if (world.HasComponent<Card>(target))
|
||||
world.RemoveComponent<Card>(target);
|
||||
world.AddComponent(target, new InDeck { Source = target, Target = discardDeck });
|
||||
}
|
||||
world.Commands.Enqueue(new DrawCardCommand { Player = pending.Player, Kind = null });
|
||||
world.RemoveSingleton<PendingNun>();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class ScoutEffect : ICardEffect
|
||||
{
|
||||
public CardEffect Effect => CardEffect.Scout;
|
||||
|
||||
public bool Resolve(World world, Entity card, Entity player, Entity owner)
|
||||
{
|
||||
world.SetSingleton(new PendingScout { CardEntity = card, Player = player });
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool ResolveChoice(World world, Entity target)
|
||||
{
|
||||
var pending = world.ReadSingleton<PendingScout>();
|
||||
if (target != Entity.Null && world.HasComponent<Player>(target))
|
||||
{
|
||||
foreach (var c in GameUtil.GetFieldCards(world, target))
|
||||
{
|
||||
if (world.TryGetComponent<Card>(c, out var cd) && cd.FaceDown)
|
||||
{
|
||||
ref var cardRef = ref world.GetComponent<Card>(c);
|
||||
cardRef.FaceDown = false;
|
||||
world.MarkModified<Card>(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
world.RemoveSingleton<PendingScout>();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class WitchEffect : ICardEffect
|
||||
{
|
||||
public CardEffect Effect => CardEffect.Witch;
|
||||
|
||||
public bool Resolve(World world, Entity card, Entity player, Entity owner)
|
||||
{
|
||||
var banner = CardEffectHelpers.GetBanner(world, owner);
|
||||
if (banner != Entity.Null)
|
||||
CardEffectHelpers.AddSkull(world, banner);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ResolveChoice(World world, Entity target) => true;
|
||||
}
|
||||
|
||||
public class ThiefEffect : ICardEffect
|
||||
{
|
||||
public CardEffect Effect => CardEffect.Thief;
|
||||
|
||||
public bool Resolve(World world, Entity card, Entity player, Entity owner)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ResolveChoice(World world, Entity target) => true;
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// Face-down flip effects
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
public class PegasusEffect : ICardEffect
|
||||
{
|
||||
public CardEffect Effect => CardEffect.Pegasus;
|
||||
|
||||
public bool Resolve(World world, Entity card, Entity player, Entity owner)
|
||||
{
|
||||
world.SetSingleton(new PendingPegasus { CardEntity = card, Player = player });
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool ResolveChoice(World world, Entity target)
|
||||
{
|
||||
var pending = world.ReadSingleton<PendingPegasus>();
|
||||
if (target != Entity.Null && target != pending.CardEntity)
|
||||
{
|
||||
world.RemoveComponent<OnField>(target);
|
||||
if (world.HasComponent<Card>(target))
|
||||
world.RemoveComponent<Card>(target);
|
||||
world.AddComponent(target, new HeldBy { Source = target, Target = pending.Player });
|
||||
}
|
||||
world.RemoveSingleton<PendingPegasus>();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class CurseMasterEffect : ICardEffect
|
||||
{
|
||||
public CardEffect Effect => CardEffect.CurseMaster;
|
||||
|
||||
public bool Resolve(World world, Entity card, Entity player, Entity owner)
|
||||
{
|
||||
world.SetSingleton(new PendingCurseMaster { CardEntity = card, Player = player });
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool ResolveChoice(World world, Entity target)
|
||||
{
|
||||
var pending = world.ReadSingleton<PendingCurseMaster>();
|
||||
if (target != Entity.Null && world.HasComponent<Horn>(target))
|
||||
{
|
||||
var placedTargets = world.GetSources<PlacedOn>(target).ToList();
|
||||
world.DestroyEntity(target);
|
||||
foreach (var pt in placedTargets)
|
||||
{
|
||||
var skull = world.CreateEntity();
|
||||
world.AddComponent(skull, new Skull());
|
||||
world.AddComponent(skull, new PlacedOn { Source = skull, Target = pt });
|
||||
}
|
||||
}
|
||||
world.RemoveSingleton<PendingCurseMaster>();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class MageEffect : ICardEffect
|
||||
{
|
||||
public CardEffect Effect => CardEffect.Mage;
|
||||
|
||||
public bool Resolve(World world, Entity card, Entity player, Entity owner)
|
||||
{
|
||||
Entity best = Entity.Null;
|
||||
int bestRank = int.MinValue;
|
||||
foreach (var p in GameUtil.FindAllEntities<Player>(world))
|
||||
{
|
||||
foreach (var c in GameUtil.GetFieldCards(world, p))
|
||||
{
|
||||
if (world.TryGetComponent<Card>(c, out var cd) && !cd.FaceDown && cd.Rank > bestRank)
|
||||
{
|
||||
bestRank = cd.Rank;
|
||||
best = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (best != Entity.Null)
|
||||
CardEffectHelpers.AddSkull(world, best);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ResolveChoice(World world, Entity target) => true;
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// Shared helpers
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
public static class CardEffectHelpers
|
||||
{
|
||||
public static void AddHorn(World world, Entity target)
|
||||
{
|
||||
var horn = world.CreateEntity();
|
||||
world.AddComponent(horn, new Horn());
|
||||
world.AddComponent(horn, new PlacedOn { Source = horn, Target = target });
|
||||
}
|
||||
|
||||
public static void AddSkull(World world, Entity target)
|
||||
{
|
||||
var skull = world.CreateEntity();
|
||||
world.AddComponent(skull, new Skull());
|
||||
world.AddComponent(skull, new PlacedOn { Source = skull, Target = target });
|
||||
}
|
||||
|
||||
public static Entity GetBanner(World world, Entity player)
|
||||
{
|
||||
using var iter = world.Select<Banner>();
|
||||
while (iter.MoveNext())
|
||||
{
|
||||
if (iter.CurrentEntity == World.SingletonEntity) continue;
|
||||
var onField = world.GetSources<OnField>(player);
|
||||
if (onField.Contains(iter.CurrentEntity))
|
||||
return iter.CurrentEntity;
|
||||
}
|
||||
return Entity.Null;
|
||||
}
|
||||
|
||||
public static Entity GetDiscardDeck(World world, Entity player)
|
||||
{
|
||||
using var iter = world.Select<FactionDeck>();
|
||||
while (iter.MoveNext())
|
||||
{
|
||||
if (iter.CurrentEntity != World.SingletonEntity)
|
||||
return iter.CurrentEntity;
|
||||
}
|
||||
return Entity.Null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
<RootNamespace>Game.CardWars</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\OECS\OECS.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\OECS.SourceGen\OECS.SourceGen.csproj"
|
||||
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
using MessagePack;
|
||||
using OECS;
|
||||
|
||||
namespace Game.CardWars;
|
||||
|
||||
[MessagePackObject]
|
||||
public struct SkipPlayCommand : ICommand
|
||||
{
|
||||
public void Execute(World world)
|
||||
{
|
||||
var state = world.ReadSingleton<GameState>();
|
||||
if (state.Phase != GamePhase.PlayPhase) return;
|
||||
|
||||
ref var mutable = ref world.GetSingleton<GameState>();
|
||||
mutable.PlayPhaseEnded = true;
|
||||
world.MarkModified<GameState>(World.SingletonEntity);
|
||||
}
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public struct FlipCardCommand : ICommand
|
||||
{
|
||||
[Key(0)] public Entity CardEntity;
|
||||
|
||||
public void Execute(World world)
|
||||
{
|
||||
var state = world.ReadSingleton<GameState>();
|
||||
if (state.Phase != GamePhase.FlipPhase) return;
|
||||
if (!world.IsAlive(CardEntity)) return;
|
||||
if (!world.HasComponent<Card>(CardEntity)) return;
|
||||
|
||||
ref var card = ref world.GetComponent<Card>(CardEntity);
|
||||
if (!card.FaceDown) return;
|
||||
|
||||
card.FaceDown = false;
|
||||
world.MarkModified<Card>(CardEntity);
|
||||
|
||||
var def = world.ReadComponent<CardDef>(CardEntity);
|
||||
Entity player = PlayCardCommand.FindPlayerByIndex(world, state.CurrentPlayerIndex);
|
||||
|
||||
// Resolve flip effects via registry.
|
||||
CardEffectRegistry.Dispatch(world, def.Effect, CardEntity, player, player);
|
||||
}
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public struct SkipFlipCommand : ICommand
|
||||
{
|
||||
public void Execute(World world)
|
||||
{
|
||||
// Skip is only valid if no face-down cards remain.
|
||||
var state = world.ReadSingleton<GameState>();
|
||||
if (state.Phase != GamePhase.FlipPhase) return;
|
||||
|
||||
Entity player = PlayCardCommand.FindPlayerByIndex(world, state.CurrentPlayerIndex);
|
||||
var fieldCards = GameUtil.GetFieldCards(world, player);
|
||||
bool hasFaceDown = fieldCards.Any(c =>
|
||||
world.HasComponent<Card>(c) && world.ReadComponent<Card>(c).FaceDown);
|
||||
if (hasFaceDown) return;
|
||||
|
||||
// Advance is handled by FlipPhaseSystem.
|
||||
}
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public struct DrawCardCommand : ICommand
|
||||
{
|
||||
[Key(0)] public Entity Player;
|
||||
[Key(1)] public CardKind? Kind;
|
||||
|
||||
public void Execute(World world)
|
||||
{
|
||||
if (!world.IsAlive(Player)) return;
|
||||
|
||||
var kind = Kind ?? CardKind.Public;
|
||||
var deck = GetDeck(world, kind);
|
||||
if (deck == Entity.Null) return;
|
||||
|
||||
var cards = world.GetSources<InDeck>(deck);
|
||||
if (cards.Count == 0)
|
||||
{
|
||||
// Recycle: for now, faction discards recycle to the same deck.
|
||||
GameUtil.RecycleDiscard(world, deck, deck);
|
||||
cards = world.GetSources<InDeck>(deck);
|
||||
if (cards.Count == 0) return;
|
||||
}
|
||||
|
||||
GameUtil.DrawCard(world, deck, Player);
|
||||
}
|
||||
|
||||
private static Entity GetDeck(World world, CardKind kind)
|
||||
{
|
||||
if (kind == CardKind.Public)
|
||||
{
|
||||
using var iter = world.Select<PublicDeck>();
|
||||
while (iter.MoveNext())
|
||||
{
|
||||
if (iter.CurrentEntity != World.SingletonEntity)
|
||||
return iter.CurrentEntity;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using var iter = world.Select<FactionDeck>();
|
||||
while (iter.MoveNext())
|
||||
{
|
||||
if (iter.CurrentEntity != World.SingletonEntity)
|
||||
return iter.CurrentEntity;
|
||||
}
|
||||
}
|
||||
return Entity.Null;
|
||||
}
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public struct ExtraActionCommand : ICommand
|
||||
{
|
||||
public void Execute(World world) { }
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public struct EndPlayPhaseCommand : ICommand
|
||||
{
|
||||
public void Execute(World world)
|
||||
{
|
||||
ref var state = ref world.GetSingleton<GameState>();
|
||||
if (state.Phase != GamePhase.PlayPhase) return;
|
||||
world.SetSingleton(new PendingDuelist { PlayerIndex = state.CurrentPlayerIndex });
|
||||
}
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public struct ReplaceCastleCommand : ICommand
|
||||
{
|
||||
public void Execute(World world)
|
||||
{
|
||||
var state = world.ReadSingleton<GameState>();
|
||||
var castles = GameUtil.FindAllEntities<Castle>(world);
|
||||
foreach (var c in castles)
|
||||
world.DestroyEntity(c);
|
||||
|
||||
var colors = new[] { CastleColor.Black, CastleColor.White, CastleColor.Blue,
|
||||
CastleColor.Brown, CastleColor.Yellow, CastleColor.Indigo, CastleColor.Gold };
|
||||
var seed = state.Seed;
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
int idx = Mulberry32.NextInt(ref seed, 0, colors.Length - 1);
|
||||
var castle = world.CreateEntity();
|
||||
world.AddComponent(castle, new Castle { Color = colors[idx] });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Player makes a choice to resolve a pending card effect.
|
||||
/// The target entity depends on the effect:
|
||||
/// - Mercenary, Dancer, Paladin, Nun: a field card entity
|
||||
/// - Scout: a player entity
|
||||
/// - Pegasus: a field card to return (or null)
|
||||
/// - CurseMaster: a horn token entity
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public struct ResolveChoiceCommand : ICommand
|
||||
{
|
||||
[Key(0)] public Entity Target;
|
||||
|
||||
public void Execute(World world)
|
||||
{
|
||||
if (!PendingChoice.Any(world)) return;
|
||||
CardEffectRegistry.DispatchChoice(world, Target);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
using MessagePack;
|
||||
using OECS;
|
||||
|
||||
namespace Game.CardWars;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a fresh game with the given player count and seed.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public struct NewGameCommand : ICommand
|
||||
{
|
||||
[Key(0)] public int PlayerCount;
|
||||
[Key(1)] public uint Seed;
|
||||
[Key(2)] public string CardDataCsv;
|
||||
|
||||
public void Execute(World world)
|
||||
{
|
||||
world.SetSingleton(new GameState
|
||||
{
|
||||
Phase = GamePhase.Setup,
|
||||
PlayerCount = PlayerCount,
|
||||
CurrentPlayerIndex = 0,
|
||||
StartingPlayerIndex = 0,
|
||||
RoundNumber = 0,
|
||||
Seed = Seed
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
using MessagePack;
|
||||
using OECS;
|
||||
|
||||
namespace Game.CardWars;
|
||||
|
||||
/// <summary>
|
||||
/// Play a card from hand to the field, choosing a rank.
|
||||
/// Resolves on-play effects via the CardEffectRegistry.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public struct PlayCardCommand : ICommand
|
||||
{
|
||||
[Key(0)] public Entity CardEntity;
|
||||
[Key(1)] public int Rank;
|
||||
[Key(2)] public bool FaceDown;
|
||||
[Key(3)] public Entity? TargetPlayer;
|
||||
|
||||
public void Execute(World world)
|
||||
{
|
||||
var state = world.ReadSingleton<GameState>();
|
||||
if (state.Phase is not GamePhase.PlayPhase) return;
|
||||
if (!world.IsAlive(CardEntity)) return;
|
||||
|
||||
Entity player = FindPlayerByIndex(world, state.CurrentPlayerIndex);
|
||||
if (player == Entity.Null) return;
|
||||
|
||||
// Verify the card is in the player's hand.
|
||||
if (!world.HasComponent<HeldBy>(CardEntity)) return;
|
||||
var heldBy = world.ReadComponent<HeldBy>(CardEntity);
|
||||
if (heldBy.Target != player) return;
|
||||
|
||||
var def = world.ReadComponent<CardDef>(CardEntity);
|
||||
|
||||
// Determine target: Witch/Thief are played to another player's field.
|
||||
Entity fieldOwner = def.Effect is CardEffect.Witch or CardEffect.Thief
|
||||
? (TargetPlayer ?? player)
|
||||
: player;
|
||||
|
||||
// Move from hand to field.
|
||||
world.RemoveComponent<HeldBy>(CardEntity);
|
||||
world.AddComponent(CardEntity, new OnField { Source = CardEntity, Target = fieldOwner });
|
||||
world.AddComponent(CardEntity, new Card { Rank = Rank, FaceDown = FaceDown });
|
||||
|
||||
// Resolve on-play effects via registry.
|
||||
if (!FaceDown)
|
||||
CardEffectRegistry.Dispatch(world, def.Effect, CardEntity, player, fieldOwner);
|
||||
|
||||
world.MarkModified<GameState>(World.SingletonEntity);
|
||||
}
|
||||
|
||||
public static Entity FindPlayerByIndex(World world, int index)
|
||||
{
|
||||
using var iter = world.Select<Player>();
|
||||
while (iter.MoveNext())
|
||||
{
|
||||
if (iter.CurrentEntity == World.SingletonEntity) continue;
|
||||
if (iter.Current1.Index == index)
|
||||
return iter.CurrentEntity;
|
||||
}
|
||||
return Entity.Null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
using MessagePack;
|
||||
|
||||
namespace Game.CardWars;
|
||||
|
||||
/// <summary>
|
||||
/// Tag component for a player's banner entity.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public struct Banner { }
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
using MessagePack;
|
||||
|
||||
namespace Game.CardWars;
|
||||
|
||||
/// <summary>
|
||||
/// Instance data for a card that has been played to the field.
|
||||
/// Cards in hand/deck only have CardDef; this is added when played.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public record struct Card
|
||||
{
|
||||
/// <summary>The rank chosen for this play (one of CardDef.Ranks).</summary>
|
||||
[Key(0)] public int Rank;
|
||||
|
||||
/// <summary>Whether the card is face-down on the field.</summary>
|
||||
[Key(1)] public bool FaceDown;
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
using MessagePack;
|
||||
|
||||
namespace Game.CardWars;
|
||||
|
||||
/// <summary>
|
||||
/// Static card definition — name, possible ranks, effect, and kind.
|
||||
/// Cards in the deck/hand only have CardDef. When played to the field,
|
||||
/// they also get a Card component with the chosen rank.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public record struct CardDef
|
||||
{
|
||||
[Key(0)] public string Name;
|
||||
[Key(1)] public int[] Ranks;
|
||||
[Key(2)] public CardEffect Effect;
|
||||
[Key(3)] public CardKind Kind;
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
using MessagePack;
|
||||
|
||||
namespace Game.CardWars;
|
||||
|
||||
/// <summary>
|
||||
/// A castle that can be won each round.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public record struct Castle
|
||||
{
|
||||
[Key(0)] public CastleColor Color;
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
using MessagePack;
|
||||
|
||||
namespace Game.CardWars;
|
||||
|
||||
/// <summary>
|
||||
/// Tag component for a faction deck entity.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public struct FactionDeck { }
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
using MessagePack;
|
||||
using OECS;
|
||||
|
||||
namespace Game.CardWars;
|
||||
|
||||
/// <summary>
|
||||
/// Relationship from a card entity to the player who holds it in hand.
|
||||
/// Added to the card: Source=card, Target=player.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public record struct HeldBy : IRelationship
|
||||
{
|
||||
[Key(0)] public Entity Source { get; set; }
|
||||
[Key(1)] public Entity Target { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
using MessagePack;
|
||||
|
||||
namespace Game.CardWars;
|
||||
|
||||
/// <summary>
|
||||
/// Tag component for a horn token. Can be placed on cards, banners, or leaders.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public struct Horn { }
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
using MessagePack;
|
||||
using OECS;
|
||||
|
||||
namespace Game.CardWars;
|
||||
|
||||
/// <summary>
|
||||
/// Relationship from a card entity to a deck entity.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public record struct InDeck : IRelationship
|
||||
{
|
||||
[Key(0)] public Entity Source { get; set; }
|
||||
[Key(1)] public Entity Target { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
using MessagePack;
|
||||
|
||||
namespace Game.CardWars;
|
||||
|
||||
/// <summary>
|
||||
/// Tag component marking a card entity as the leader card.
|
||||
/// The leader is always on the field and contributes its rank to power.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public struct Leader { }
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
using MessagePack;
|
||||
using OECS;
|
||||
|
||||
namespace Game.CardWars;
|
||||
|
||||
/// <summary>
|
||||
/// Relationship from a card entity to the player entity whose field it's on.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public record struct OnField : IRelationship
|
||||
{
|
||||
[Key(0)] public Entity Source { get; set; }
|
||||
[Key(1)] public Entity Target { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
using MessagePack;
|
||||
using OECS;
|
||||
|
||||
namespace Game.CardWars;
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// Per-effect pending choice components.
|
||||
// Each is placed on the singleton entity when a card effect
|
||||
// requires player input. Their presence blocks game advancement.
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Mercenary: choose a combatant to give a skull.</summary>
|
||||
[MessagePackObject]
|
||||
public record struct PendingMercenary
|
||||
{
|
||||
[Key(0)] public Entity CardEntity;
|
||||
[Key(1)] public Entity Player;
|
||||
}
|
||||
|
||||
/// <summary>Dancer: choose up to 2 combatants for horn. Step tracks progress.</summary>
|
||||
[MessagePackObject]
|
||||
public record struct PendingDancer
|
||||
{
|
||||
[Key(0)] public Entity CardEntity;
|
||||
[Key(1)] public Entity Player;
|
||||
[Key(2)] public int Step; // 0 = first choice, 1 = second choice
|
||||
}
|
||||
|
||||
/// <summary>Paladin: choose a combatant or banner for horn.</summary>
|
||||
[MessagePackObject]
|
||||
public record struct PendingPaladin
|
||||
{
|
||||
[Key(0)] public Entity CardEntity;
|
||||
[Key(1)] public Entity Player;
|
||||
}
|
||||
|
||||
/// <summary>Nun: choose a field card to discard.</summary>
|
||||
[MessagePackObject]
|
||||
public record struct PendingNun
|
||||
{
|
||||
[Key(0)] public Entity CardEntity;
|
||||
[Key(1)] public Entity Player;
|
||||
}
|
||||
|
||||
/// <summary>Scout: choose an opponent player to scout.</summary>
|
||||
[MessagePackObject]
|
||||
public record struct PendingScout
|
||||
{
|
||||
[Key(0)] public Entity CardEntity;
|
||||
[Key(1)] public Entity Player;
|
||||
}
|
||||
|
||||
/// <summary>Pegasus: choose a field card to return to hand (or null to pass).</summary>
|
||||
[MessagePackObject]
|
||||
public record struct PendingPegasus
|
||||
{
|
||||
[Key(0)] public Entity CardEntity;
|
||||
[Key(1)] public Entity Player;
|
||||
}
|
||||
|
||||
/// <summary>CurseMaster: choose a horn token to turn into skull.</summary>
|
||||
[MessagePackObject]
|
||||
public record struct PendingCurseMaster
|
||||
{
|
||||
[Key(0)] public Entity CardEntity;
|
||||
[Key(1)] public Entity Player;
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// Helper to detect any pending component on the singleton.
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
public static class PendingChoice
|
||||
{
|
||||
/// <summary>Returns true if any pending choice component exists on the singleton.</summary>
|
||||
public static bool Any(World world)
|
||||
{
|
||||
return world.HasSingleton<PendingMercenary>()
|
||||
|| world.HasSingleton<PendingDancer>()
|
||||
|| world.HasSingleton<PendingPaladin>()
|
||||
|| world.HasSingleton<PendingNun>()
|
||||
|| world.HasSingleton<PendingScout>()
|
||||
|| world.HasSingleton<PendingPegasus>()
|
||||
|| world.HasSingleton<PendingCurseMaster>();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
using MessagePack;
|
||||
|
||||
namespace Game.CardWars;
|
||||
|
||||
/// <summary>
|
||||
/// Placed on the singleton when a 决斗家 (Duelist) is played.
|
||||
/// The play phase ends when the turn comes back to this player.
|
||||
/// Removed automatically when the phase transitions.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public struct PendingDuelist
|
||||
{
|
||||
[Key(0)] public int PlayerIndex;
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
using MessagePack;
|
||||
using OECS;
|
||||
|
||||
namespace Game.CardWars;
|
||||
|
||||
/// <summary>
|
||||
/// Relationship from a horn/skull token entity to the target entity it's placed on.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public record struct PlacedOn : IRelationship
|
||||
{
|
||||
[Key(0)] public Entity Source { get; set; }
|
||||
[Key(1)] public Entity Target { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
using MessagePack;
|
||||
|
||||
namespace Game.CardWars;
|
||||
|
||||
/// <summary>
|
||||
/// Tag + index for a player entity.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public record struct Player
|
||||
{
|
||||
[Key(0)] public int Index;
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
using MessagePack;
|
||||
|
||||
namespace Game.CardWars;
|
||||
|
||||
/// <summary>
|
||||
/// Tag component for the public deck entity.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public struct PublicDeck { }
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
using MessagePack;
|
||||
|
||||
namespace Game.CardWars;
|
||||
|
||||
/// <summary>
|
||||
/// Tag component for a skull token. Can be placed on cards, banners, or leaders.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public struct Skull { }
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
namespace Game.CardWars;
|
||||
|
||||
public enum CardKind : byte
|
||||
{
|
||||
Public = 0,
|
||||
Faction = 1
|
||||
}
|
||||
|
||||
public enum CardEffect : byte
|
||||
{
|
||||
None = 0,
|
||||
|
||||
// On-play effects
|
||||
Warrior,
|
||||
Archer,
|
||||
Mercenary,
|
||||
Merchant,
|
||||
Dancer,
|
||||
Paladin,
|
||||
Princess,
|
||||
Duelist,
|
||||
Nun,
|
||||
Scout,
|
||||
Witch,
|
||||
Thief,
|
||||
|
||||
// On-flip effects (played face-down)
|
||||
Pegasus,
|
||||
CurseMaster,
|
||||
Mage
|
||||
}
|
||||
|
||||
public enum CastleColor : byte
|
||||
{
|
||||
Black = 0,
|
||||
White = 1,
|
||||
Blue = 2,
|
||||
Brown = 3,
|
||||
Yellow = 4,
|
||||
Indigo = 5,
|
||||
Gold = 6
|
||||
}
|
||||
|
||||
public enum GamePhase : byte
|
||||
{
|
||||
Setup = 0,
|
||||
PlayPhase = 1,
|
||||
FlipPhase = 2,
|
||||
Scoring = 3,
|
||||
Cleanup = 4,
|
||||
GameOver = 5
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
using OECS;
|
||||
|
||||
namespace Game.CardWars;
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating a fully configured game world and system group.
|
||||
/// </summary>
|
||||
public static class GameFactory
|
||||
{
|
||||
public static string DefaultCardData =>
|
||||
"name,ranks,effect\n" +
|
||||
"战士,3;4;4;5,战士\n" +
|
||||
"弓手,3;4;4;5,弓手\n" +
|
||||
"佣兵,4;5;5;6,佣兵\n" +
|
||||
"商人,0;1,商人\n" +
|
||||
"舞姬,0;1,舞姬\n" +
|
||||
"圣骑士,2;3,圣骑士\n" +
|
||||
"飞马,2;3,飞马\n" +
|
||||
"诅咒师,1;2,诅咒师\n" +
|
||||
"魔导士,0;1,魔导士\n" +
|
||||
"公主,4,公主\n" +
|
||||
"决斗家,4,决斗家\n" +
|
||||
"修女,3,修女\n" +
|
||||
"斥候,2,斥候\n" +
|
||||
"女巫,-1,女巫\n" +
|
||||
"盗贼,-1,盗贼\n";
|
||||
|
||||
public static (World World, SystemGroup Group) Create(int playerCount, uint seed = 42)
|
||||
{
|
||||
var world = new World();
|
||||
var group = new SystemGroup(world);
|
||||
|
||||
world.Commands.Enqueue(new NewGameCommand
|
||||
{
|
||||
PlayerCount = playerCount,
|
||||
Seed = seed,
|
||||
CardDataCsv = DefaultCardData
|
||||
});
|
||||
|
||||
group.Add(new GameSetupSystem(playerCount, DefaultCardData));
|
||||
group.Add(new PlayPhaseSystem());
|
||||
group.Add(new FlipPhaseSystem());
|
||||
group.Add(new ScoringSystem());
|
||||
group.Add(new CleanupSystem());
|
||||
|
||||
// Run setup.
|
||||
group.RunLogical();
|
||||
|
||||
return (world, group);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
using OECS;
|
||||
|
||||
namespace Game.CardWars;
|
||||
|
||||
/// <summary>
|
||||
/// Shared utility methods for querying and manipulating the game world.
|
||||
/// </summary>
|
||||
public static class GameUtil
|
||||
{
|
||||
public static Entity FindEntity<T>(World world) where T : struct
|
||||
{
|
||||
using var iter = world.Select<T>();
|
||||
while (iter.MoveNext())
|
||||
{
|
||||
if (iter.CurrentEntity != World.SingletonEntity)
|
||||
return iter.CurrentEntity;
|
||||
}
|
||||
return Entity.Null;
|
||||
}
|
||||
|
||||
public static List<Entity> FindAllEntities<T>(World world) where T : struct
|
||||
{
|
||||
var list = new List<Entity>();
|
||||
using var iter = world.Select<T>();
|
||||
while (iter.MoveNext())
|
||||
{
|
||||
if (iter.CurrentEntity != World.SingletonEntity)
|
||||
list.Add(iter.CurrentEntity);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>Draw the top card from a deck and add it to the player's hand.</summary>
|
||||
public static bool DrawCard(World world, Entity deck, Entity player)
|
||||
{
|
||||
var cards = world.GetSources<InDeck>(deck);
|
||||
if (cards.Count == 0) return false;
|
||||
|
||||
var cardEntity = cards.First();
|
||||
world.RemoveComponent<InDeck>(cardEntity);
|
||||
world.AddComponent(cardEntity, new HeldBy { Source = cardEntity, Target = player });
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Shuffle cards in a deck using Fisher-Yates with the given seed.</summary>
|
||||
public static void ShuffleDeck(World world, Entity deck, ref uint seed)
|
||||
{
|
||||
var cards = world.GetSources<InDeck>(deck).ToArray();
|
||||
for (int i = cards.Length - 1; i > 0; i--)
|
||||
{
|
||||
int j = Mulberry32.NextInt(ref seed, 0, i);
|
||||
(cards[i], cards[j]) = (cards[j], cards[i]);
|
||||
}
|
||||
for (int i = cards.Length - 1; i >= 0; i--)
|
||||
world.RemoveComponent<InDeck>(cards[i]);
|
||||
for (int i = 0; i < cards.Length; i++)
|
||||
world.AddComponent(cards[i], new InDeck { Source = cards[i], Target = deck });
|
||||
}
|
||||
|
||||
/// <summary>Get all field cards for a player.</summary>
|
||||
public static List<Entity> GetFieldCards(World world, Entity player)
|
||||
{
|
||||
return world.GetSources<OnField>(player).ToList();
|
||||
}
|
||||
|
||||
/// <summary>Get all cards in a player's hand.</summary>
|
||||
public static List<Entity> GetHandCards(World world, Entity player)
|
||||
{
|
||||
return world.GetSources<HeldBy>(player).ToList();
|
||||
}
|
||||
|
||||
/// <summary>Calculate a player's total battle power.</summary>
|
||||
public static int CalculatePower(World world, Entity player)
|
||||
{
|
||||
int total = 0;
|
||||
|
||||
// Leader power.
|
||||
using var leaderIter = world.Select<Leader>();
|
||||
while (leaderIter.MoveNext())
|
||||
{
|
||||
if (leaderIter.CurrentEntity == World.SingletonEntity) continue;
|
||||
var onFieldSources = world.GetSources<OnField>(player);
|
||||
if (onFieldSources.Contains(leaderIter.CurrentEntity))
|
||||
{
|
||||
if (world.TryGetComponent<Card>(leaderIter.CurrentEntity, out var leaderCard))
|
||||
total += leaderCard.Rank;
|
||||
}
|
||||
}
|
||||
|
||||
// Find the player's banner.
|
||||
var banner = CardEffectHelpers.GetBanner(world, player);
|
||||
bool bannerHasHorn = banner != Entity.Null && world.HasComponent<Horn>(banner);
|
||||
bool bannerHasSkull = banner != Entity.Null && world.HasComponent<Skull>(banner);
|
||||
|
||||
// Field cards.
|
||||
var fieldCards = GetFieldCards(world, player);
|
||||
int poweredCount = 0;
|
||||
foreach (var card in fieldCards)
|
||||
{
|
||||
if (!world.TryGetComponent<Card>(card, out var cardData)) continue;
|
||||
if (cardData.FaceDown) continue;
|
||||
if (cardData.Rank <= 0) continue;
|
||||
|
||||
poweredCount++;
|
||||
int rank = cardData.Rank;
|
||||
if (world.HasComponent<Horn>(card))
|
||||
rank *= 2;
|
||||
if (world.HasComponent<Skull>(card))
|
||||
rank = 0;
|
||||
|
||||
total += rank;
|
||||
}
|
||||
|
||||
if (bannerHasHorn) total += poweredCount;
|
||||
if (bannerHasSkull) total -= poweredCount;
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
/// <summary>Move all field cards (except leaders and banners) to the discard pile.</summary>
|
||||
public static void DiscardField(World world, Entity player, Entity discardDeck)
|
||||
{
|
||||
var fieldCards = GetFieldCards(world, player);
|
||||
|
||||
var toDiscard = new List<Entity>();
|
||||
foreach (var card in fieldCards)
|
||||
{
|
||||
// Keep leaders and banners on the field.
|
||||
if (world.HasComponent<Leader>(card)) continue;
|
||||
if (world.HasComponent<Banner>(card)) continue;
|
||||
toDiscard.Add(card);
|
||||
}
|
||||
|
||||
foreach (var card in toDiscard)
|
||||
{
|
||||
world.RemoveComponent<OnField>(card);
|
||||
world.RemoveComponent<Card>(card);
|
||||
world.AddComponent(card, new InDeck { Source = card, Target = discardDeck });
|
||||
}
|
||||
|
||||
// Destroy horns and skulls placed on discarded cards.
|
||||
var tokenTargets = new HashSet<Entity>();
|
||||
foreach (var card in toDiscard)
|
||||
{
|
||||
foreach (var token in world.GetSources<PlacedOn>(card))
|
||||
tokenTargets.Add(token);
|
||||
}
|
||||
foreach (var token in tokenTargets)
|
||||
world.DestroyEntity(token);
|
||||
}
|
||||
|
||||
/// <summary>Recycle discard pile into draw pile.</summary>
|
||||
public static void RecycleDiscard(World world, Entity drawDeck, Entity discardDeck)
|
||||
{
|
||||
var cards = world.GetSources<InDeck>(discardDeck).ToList();
|
||||
foreach (var card in cards)
|
||||
{
|
||||
world.RemoveComponent<InDeck>(card);
|
||||
world.AddComponent(card, new InDeck { Source = card, Target = drawDeck });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
namespace Game.CardWars;
|
||||
|
||||
/// <summary>
|
||||
/// Mulberry32 PRNG — a fast, high-quality 32-bit random number generator.
|
||||
/// </summary>
|
||||
public static class Mulberry32
|
||||
{
|
||||
public static float NextFloat(ref uint state)
|
||||
{
|
||||
state += 0x6D2B79F5u;
|
||||
uint z = state;
|
||||
z = (z ^ (z >> 15)) * (z | 1u);
|
||||
z ^= z + (z ^ (z >> 7)) * (z | 61u);
|
||||
return (z ^ (z >> 14)) / (float)uint.MaxValue;
|
||||
}
|
||||
|
||||
public static int NextInt(ref uint state, int min, int max)
|
||||
{
|
||||
return (int)(NextFloat(ref state) * (max - min + 1)) + min;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
using MessagePack;
|
||||
|
||||
namespace Game.CardWars;
|
||||
|
||||
/// <summary>
|
||||
/// Global game state on the singleton entity.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public record struct GameState
|
||||
{
|
||||
[Key(0)] public GamePhase Phase;
|
||||
[Key(1)] public int PlayerCount;
|
||||
[Key(2)] public int CurrentPlayerIndex;
|
||||
[Key(3)] public int StartingPlayerIndex;
|
||||
[Key(4)] public int RoundNumber;
|
||||
[Key(5)] public uint Seed;
|
||||
[Key(6)] public bool PlayPhaseEnded;
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
using MessagePack;
|
||||
|
||||
namespace Game.CardWars;
|
||||
|
||||
/// <summary>
|
||||
/// Per-player score tracking.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public record struct PlayerScores
|
||||
{
|
||||
[Key(0)] public int[] Scores;
|
||||
[Key(1)] public CastleColor[] WonCastles;
|
||||
[Key(2)] public int Count;
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
using OECS;
|
||||
|
||||
namespace Game.CardWars;
|
||||
|
||||
/// <summary>
|
||||
/// Discards field cards, clears tokens, draws new hands, checks game over.
|
||||
/// </summary>
|
||||
public class CleanupSystem : ISystem
|
||||
{
|
||||
public void Run(World world)
|
||||
{
|
||||
var state = world.ReadSingleton<GameState>();
|
||||
if (state.Phase != GamePhase.Cleanup) return;
|
||||
|
||||
ref var mutable = ref world.GetSingleton<GameState>();
|
||||
|
||||
var players = GameUtil.FindAllEntities<Player>(world);
|
||||
var publicDeck = GameUtil.FindEntity<PublicDeck>(world);
|
||||
|
||||
foreach (var player in players)
|
||||
{
|
||||
GameUtil.DiscardField(world, player, publicDeck);
|
||||
|
||||
// Clear tokens on banners.
|
||||
var banner = CardEffectHelpers.GetBanner(world, player);
|
||||
if (banner != Entity.Null)
|
||||
{
|
||||
var tokens = world.GetSources<PlacedOn>(banner).ToList();
|
||||
foreach (var token in tokens)
|
||||
world.DestroyEntity(token);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw 2 public cards per player.
|
||||
foreach (var player in players)
|
||||
{
|
||||
for (int i = 0; i < 2; i++)
|
||||
GameUtil.DrawCard(world, publicDeck, player);
|
||||
}
|
||||
|
||||
// Check game over.
|
||||
int maxScore = 0;
|
||||
foreach (var player in players)
|
||||
{
|
||||
var castles = world.GetSources<HeldBy>(player)
|
||||
.Where(c => world.HasComponent<Castle>(c))
|
||||
.ToList();
|
||||
maxScore = Math.Max(maxScore, castles.Count);
|
||||
}
|
||||
|
||||
if (maxScore >= 4)
|
||||
{
|
||||
mutable.Phase = GamePhase.GameOver;
|
||||
}
|
||||
else
|
||||
{
|
||||
mutable.Phase = GamePhase.PlayPhase;
|
||||
mutable.RoundNumber++;
|
||||
mutable.CurrentPlayerIndex = (mutable.StartingPlayerIndex + 1) % mutable.PlayerCount;
|
||||
mutable.StartingPlayerIndex = mutable.CurrentPlayerIndex;
|
||||
mutable.PlayPhaseEnded = false;
|
||||
}
|
||||
|
||||
world.MarkModified<GameState>(World.SingletonEntity);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
using OECS;
|
||||
|
||||
namespace Game.CardWars;
|
||||
|
||||
/// <summary>
|
||||
/// Manages the flip phase: players take turns flipping cards.
|
||||
/// Blocks if a PendingChoice exists.
|
||||
/// </summary>
|
||||
public class FlipPhaseSystem : ISystem
|
||||
{
|
||||
public void Run(World world)
|
||||
{
|
||||
var state = world.ReadSingleton<GameState>();
|
||||
if (state.Phase != GamePhase.FlipPhase) return;
|
||||
|
||||
// Block if a pending choice exists.
|
||||
if (PendingChoice.Any(world))
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
using OECS;
|
||||
|
||||
namespace Game.CardWars;
|
||||
|
||||
/// <summary>
|
||||
/// One-time setup: creates players, leaders, banners, decks, castles, and deals starting hands.
|
||||
/// </summary>
|
||||
public class GameSetupSystem : ISystem
|
||||
{
|
||||
private readonly int _playerCount;
|
||||
private readonly string _cardDataCsv;
|
||||
private bool _hasRun;
|
||||
|
||||
public GameSetupSystem(int playerCount, string cardDataCsv)
|
||||
{
|
||||
_playerCount = playerCount;
|
||||
_cardDataCsv = cardDataCsv;
|
||||
}
|
||||
|
||||
public void Run(World world)
|
||||
{
|
||||
if (_hasRun) return;
|
||||
_hasRun = true;
|
||||
|
||||
var state = world.ReadSingleton<GameState>();
|
||||
ref var mutable = ref world.GetSingleton<GameState>();
|
||||
mutable.PlayerCount = _playerCount;
|
||||
mutable.Phase = GamePhase.PlayPhase;
|
||||
mutable.CurrentPlayerIndex = 0;
|
||||
mutable.StartingPlayerIndex = 0;
|
||||
mutable.RoundNumber = 1;
|
||||
world.MarkModified<GameState>(World.SingletonEntity);
|
||||
|
||||
// Register card effects.
|
||||
CardEffectRegistry.Add<WarriorEffect>();
|
||||
CardEffectRegistry.Add<ArcherEffect>();
|
||||
CardEffectRegistry.Add<MercenaryEffect>();
|
||||
CardEffectRegistry.Add<MerchantEffect>();
|
||||
CardEffectRegistry.Add<DancerEffect>();
|
||||
CardEffectRegistry.Add<PaladinEffect>();
|
||||
CardEffectRegistry.Add<PrincessEffect>();
|
||||
CardEffectRegistry.Add<DuelistEffect>();
|
||||
CardEffectRegistry.Add<NunEffect>();
|
||||
CardEffectRegistry.Add<ScoutEffect>();
|
||||
CardEffectRegistry.Add<WitchEffect>();
|
||||
CardEffectRegistry.Add<ThiefEffect>();
|
||||
CardEffectRegistry.Add<PegasusEffect>();
|
||||
CardEffectRegistry.Add<CurseMasterEffect>();
|
||||
CardEffectRegistry.Add<MageEffect>();
|
||||
CardEffectRegistry.Freeze();
|
||||
|
||||
// Create public deck.
|
||||
var publicDeck = world.CreateEntity();
|
||||
world.AddComponent(publicDeck, new PublicDeck());
|
||||
|
||||
// Load card definitions and create card instances for the public deck.
|
||||
var defs = CardDataLoader.LoadDefinitions(world, _cardDataCsv);
|
||||
foreach (var (defEntity, _) in defs)
|
||||
{
|
||||
world.AddComponent(defEntity, new InDeck { Source = defEntity, Target = publicDeck });
|
||||
}
|
||||
|
||||
// Shuffle public deck.
|
||||
var seed = state.Seed;
|
||||
GameUtil.ShuffleDeck(world, publicDeck, ref seed);
|
||||
|
||||
// Create the first castle.
|
||||
var castle = world.CreateEntity();
|
||||
world.AddComponent(castle, new Castle { Color = CastleColor.Black });
|
||||
|
||||
// Create players.
|
||||
for (int i = 0; i < _playerCount; i++)
|
||||
{
|
||||
var player = world.CreateEntity();
|
||||
world.AddComponent(player, new Player { Index = i });
|
||||
|
||||
// Create leader card (always on field, rank 0 placeholder).
|
||||
var leader = world.CreateEntity();
|
||||
world.AddComponent(leader, new Leader());
|
||||
world.AddComponent(leader, new CardDef { Name = $"领袖{i}", Ranks = new[] { 0 }, Effect = CardEffect.None, Kind = CardKind.Faction });
|
||||
world.AddComponent(leader, new Card { Rank = 0, FaceDown = false });
|
||||
world.AddComponent(leader, new OnField { Source = leader, Target = player });
|
||||
|
||||
// Create banner.
|
||||
var banner = world.CreateEntity();
|
||||
world.AddComponent(banner, new Banner());
|
||||
world.AddComponent(banner, new OnField { Source = banner, Target = player });
|
||||
|
||||
// Create faction deck.
|
||||
var factionDeck = world.CreateEntity();
|
||||
world.AddComponent(factionDeck, new FactionDeck());
|
||||
}
|
||||
|
||||
// Deal starting hands: 3 public cards per player.
|
||||
foreach (var player in GameUtil.FindAllEntities<Player>(world))
|
||||
{
|
||||
for (int i = 0; i < 3; i++)
|
||||
GameUtil.DrawCard(world, publicDeck, player);
|
||||
}
|
||||
|
||||
mutable.Seed = seed;
|
||||
world.MarkModified<GameState>(World.SingletonEntity);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
using OECS;
|
||||
|
||||
namespace Game.CardWars;
|
||||
|
||||
/// <summary>
|
||||
/// Manages the play phase: players take turns. If a PendingChoice exists,
|
||||
/// blocks advancement until the player resolves it.
|
||||
/// </summary>
|
||||
public class PlayPhaseSystem : ISystem
|
||||
{
|
||||
public void Run(World world)
|
||||
{
|
||||
ref var state = ref world.GetSingleton<GameState>();
|
||||
if (state.Phase != GamePhase.PlayPhase) return;
|
||||
|
||||
// Block if a pending choice exists.
|
||||
if (PendingChoice.Any(world))
|
||||
return;
|
||||
|
||||
// If the current player has skipped, move to the next player.
|
||||
if (state.PlayPhaseEnded)
|
||||
{
|
||||
state.PlayPhaseEnded = false;
|
||||
AdvanceTurn(world, ref state);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static void AdvanceTurn(World world, ref GameState state)
|
||||
{
|
||||
int next = (state.CurrentPlayerIndex + 1) % state.PlayerCount;
|
||||
|
||||
// Duelist: if the next player is the one who played duelist, end the phase.
|
||||
if (world.HasSingleton<PendingDuelist>())
|
||||
{
|
||||
var duelist = world.ReadSingleton<PendingDuelist>();
|
||||
if (next == duelist.PlayerIndex)
|
||||
{
|
||||
world.RemoveSingleton<PendingDuelist>();
|
||||
state.Phase = GamePhase.FlipPhase;
|
||||
state.CurrentPlayerIndex = state.StartingPlayerIndex;
|
||||
world.MarkModified<GameState>(World.SingletonEntity);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
state.CurrentPlayerIndex = next;
|
||||
|
||||
// If we've come back to the starting player, everyone has skipped.
|
||||
if (state.CurrentPlayerIndex == state.StartingPlayerIndex)
|
||||
{
|
||||
state.Phase = GamePhase.FlipPhase;
|
||||
world.RemoveSingleton<PendingDuelist>(); // Clean up in case duelist was never triggered.
|
||||
}
|
||||
|
||||
world.MarkModified<GameState>(World.SingletonEntity);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
using OECS;
|
||||
|
||||
namespace Game.CardWars;
|
||||
|
||||
/// <summary>
|
||||
/// Calculates each player's battle power, awards the castle to the winner,
|
||||
/// and checks for game end.
|
||||
/// </summary>
|
||||
public class ScoringSystem : ISystem
|
||||
{
|
||||
public void Run(World world)
|
||||
{
|
||||
var state = world.ReadSingleton<GameState>();
|
||||
if (state.Phase != GamePhase.Scoring) return;
|
||||
|
||||
ref var mutable = ref world.GetSingleton<GameState>();
|
||||
|
||||
var players = GameUtil.FindAllEntities<Player>(world);
|
||||
if (players.Count == 0) return;
|
||||
|
||||
int bestPower = int.MinValue;
|
||||
Entity winner = Entity.Null;
|
||||
int winnerIndex = -1;
|
||||
|
||||
for (int i = 0; i < players.Count; i++)
|
||||
{
|
||||
int idx = (state.StartingPlayerIndex + i) % players.Count;
|
||||
var player = players[idx];
|
||||
int power = GameUtil.CalculatePower(world, player);
|
||||
|
||||
if (power > bestPower)
|
||||
{
|
||||
bestPower = power;
|
||||
winner = player;
|
||||
winnerIndex = idx;
|
||||
}
|
||||
}
|
||||
|
||||
if (winner != Entity.Null)
|
||||
{
|
||||
var castles = GameUtil.FindAllEntities<Castle>(world);
|
||||
if (castles.Count > 0)
|
||||
{
|
||||
var castle = castles[0];
|
||||
world.AddComponent(castle, new HeldBy { Source = castle, Target = winner });
|
||||
world.RemoveComponent<Castle>(castle);
|
||||
}
|
||||
|
||||
var colors = new[] { CastleColor.Black, CastleColor.White, CastleColor.Blue,
|
||||
CastleColor.Brown, CastleColor.Yellow, CastleColor.Indigo, CastleColor.Gold };
|
||||
int colorIdx = Mulberry32.NextInt(ref mutable.Seed, 0, colors.Length - 1);
|
||||
var newCastle = world.CreateEntity();
|
||||
world.AddComponent(newCastle, new Castle { Color = colors[colorIdx] });
|
||||
}
|
||||
|
||||
mutable.Phase = GamePhase.Cleanup;
|
||||
world.MarkModified<GameState>(World.SingletonEntity);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# CardWars — Design Document
|
||||
|
||||
<!-- Fill in the rules, mechanics, and design notes here. -->
|
||||
29
OECS.sln
29
OECS.sln
|
|
@ -16,6 +16,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Game.Blackjack.Tests", "Gam
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Game.TicTacToe.Tests", "Game.TicTacToe.Tests\Game.TicTacToe.Tests.csproj", "{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Game.CardWars", "Game.CardWars\CardWars.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567891}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Game.CardWars.Tests", "Game.CardWars.Tests\Game.CardWars.Tests.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678902}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -86,7 +90,6 @@ Global
|
|||
{D4E5F6A7-B8C9-0123-DEF4-567890ABCDEF}.Release|x64.Build.0 = Release|Any CPU
|
||||
{D4E5F6A7-B8C9-0123-DEF4-567890ABCDEF}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{D4E5F6A7-B8C9-0123-DEF4-567890ABCDEF}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
{E5F6A7B8-C9D0-1234-EF56-7890ABCDEF01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E5F6A7B8-C9D0-1234-EF56-7890ABCDEF01}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E5F6A7B8-C9D0-1234-EF56-7890ABCDEF01}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
|
|
@ -111,6 +114,30 @@ Global
|
|||
{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}.Release|x64.Build.0 = Release|Any CPU
|
||||
{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}.Release|x86.Build.0 = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567891}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567891}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567891}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567891}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567891}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567891}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567891}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567891}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567891}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567891}.Release|x64.Build.0 = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567891}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567891}.Release|x86.Build.0 = Release|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678902}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678902}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678902}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678902}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678902}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678902}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678902}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678902}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678902}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678902}.Release|x64.Build.0 = Release|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678902}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{B2C3D4E5-F6A7-8901-BCDE-F12345678902}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
|
|||
Loading…
Reference in New Issue