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. -->
|
||||||
77
OECS.sln
77
OECS.sln
|
|
@ -16,6 +16,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Game.Blackjack.Tests", "Gam
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Game.TicTacToe.Tests", "Game.TicTacToe.Tests\Game.TicTacToe.Tests.csproj", "{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Game.TicTacToe.Tests", "Game.TicTacToe.Tests\Game.TicTacToe.Tests.csproj", "{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}"
|
||||||
EndProject
|
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
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
|
@ -86,32 +90,55 @@ Global
|
||||||
{D4E5F6A7-B8C9-0123-DEF4-567890ABCDEF}.Release|x64.Build.0 = Release|Any CPU
|
{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.ActiveCfg = Release|Any CPU
|
||||||
{D4E5F6A7-B8C9-0123-DEF4-567890ABCDEF}.Release|x86.Build.0 = Release|Any CPU
|
{D4E5F6A7-B8C9-0123-DEF4-567890ABCDEF}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{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
|
||||||
|
{E5F6A7B8-C9D0-1234-EF56-7890ABCDEF01}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{E5F6A7B8-C9D0-1234-EF56-7890ABCDEF01}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{E5F6A7B8-C9D0-1234-EF56-7890ABCDEF01}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{E5F6A7B8-C9D0-1234-EF56-7890ABCDEF01}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{E5F6A7B8-C9D0-1234-EF56-7890ABCDEF01}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{E5F6A7B8-C9D0-1234-EF56-7890ABCDEF01}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{E5F6A7B8-C9D0-1234-EF56-7890ABCDEF01}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{E5F6A7B8-C9D0-1234-EF56-7890ABCDEF01}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{E5F6A7B8-C9D0-1234-EF56-7890ABCDEF01}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{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
|
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
|
|
||||||
{E5F6A7B8-C9D0-1234-EF56-7890ABCDEF01}.Debug|x64.Build.0 = Debug|Any CPU
|
|
||||||
{E5F6A7B8-C9D0-1234-EF56-7890ABCDEF01}.Debug|x86.ActiveCfg = Debug|Any CPU
|
|
||||||
{E5F6A7B8-C9D0-1234-EF56-7890ABCDEF01}.Debug|x86.Build.0 = Debug|Any CPU
|
|
||||||
{E5F6A7B8-C9D0-1234-EF56-7890ABCDEF01}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{E5F6A7B8-C9D0-1234-EF56-7890ABCDEF01}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{E5F6A7B8-C9D0-1234-EF56-7890ABCDEF01}.Release|x64.ActiveCfg = Release|Any CPU
|
|
||||||
{E5F6A7B8-C9D0-1234-EF56-7890ABCDEF01}.Release|x64.Build.0 = Release|Any CPU
|
|
||||||
{E5F6A7B8-C9D0-1234-EF56-7890ABCDEF01}.Release|x86.ActiveCfg = Release|Any CPU
|
|
||||||
{E5F6A7B8-C9D0-1234-EF56-7890ABCDEF01}.Release|x86.Build.0 = Release|Any CPU
|
|
||||||
{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}.Debug|x64.ActiveCfg = Debug|Any CPU
|
|
||||||
{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}.Debug|x64.Build.0 = Debug|Any CPU
|
|
||||||
{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}.Debug|x86.ActiveCfg = Debug|Any CPU
|
|
||||||
{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}.Debug|x86.Build.0 = Debug|Any CPU
|
|
||||||
{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{F6A7B8C9-D0E1-2345-F678-90ABCDEF0123}.Release|x64.ActiveCfg = Release|Any CPU
|
|
||||||
{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
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue