refactor: reorganize project structure and move examples

- Move examples (Blackjack, TicTacToe) to the root directory
- Convert example projects from Console applications to Libraries
- Add Directory.Build.props for shared build settings
- Update solution file and project references to reflect new paths
This commit is contained in:
2026-07-20 16:30:02 +08:00
parent 5594515a53
commit 7946f4bafd
80 changed files with 61 additions and 549 deletions
+31
View File
@@ -0,0 +1,31 @@
using OECS;
namespace Blackjack;
/// <summary>
/// Deals initial two cards to player and dealer when entering the dealing phase.
/// Advances to player turn after dealing.
/// </summary>
public class DealSystem : ISystem
{
public void Run(World world)
{
var state = world.ReadSingleton<GameState>();
if (state.Phase != GamePhase.Dealing)
return;
ref var mutableState = ref world.GetSingleton<GameState>();
// Deal two cards to player, then two to dealer.
HitCommand.DrawCard<PlayerHand>(world);
HitCommand.DrawCard<PlayerHand>(world);
HitCommand.DrawCard<DealerHand>(world);
HitCommand.DrawCard<DealerHand>(world);
mutableState.Phase = GamePhase.PlayerTurn;
world.MarkModified<GameState>(World.SingletonEntity);
}
}
internal static class PlayerHandTag { public static readonly PlayerHand Instance = new(); }
internal static class DealerHandTag { public static readonly DealerHand Instance = new(); }
+54
View File
@@ -0,0 +1,54 @@
using OECS;
namespace Blackjack;
/// <summary>
/// Dealer draws cards until reaching 17 or higher,
/// then resolves the round result.
/// </summary>
public class DealerSystem : ISystem
{
public void Run(World world)
{
var state = world.ReadSingleton<GameState>();
if (state.Phase != GamePhase.DealerTurn)
return;
ref var mutableState = ref world.GetSingleton<GameState>();
// Dealer must hit on 16 and below, stand on 17+.
int dealerTotal = HandUtil.CalculateHand(world, DealerHandTag.Instance);
while (dealerTotal < 17)
{
HitCommand.DrawCard<DealerHand>(world);
dealerTotal = HandUtil.CalculateHand(world, DealerHandTag.Instance);
}
int playerTotal = HandUtil.CalculateHand(world, PlayerHandTag.Instance);
mutableState.Phase = GamePhase.RoundOver;
if (dealerTotal > 21)
{
mutableState.Result = RoundResult.DealerBust;
mutableState.Chips += mutableState.CurrentBet * 2;
}
else if (playerTotal > dealerTotal)
{
mutableState.Result = RoundResult.PlayerWin;
mutableState.Chips += mutableState.CurrentBet * 2;
}
else if (dealerTotal > playerTotal)
{
mutableState.Result = RoundResult.DealerWin;
}
else
{
// Push: return the bet.
mutableState.Result = RoundResult.Push;
mutableState.Chips += mutableState.CurrentBet;
}
world.MarkModified<GameState>(World.SingletonEntity);
}
}
+87
View File
@@ -0,0 +1,87 @@
using OECS;
namespace Blackjack;
/// <summary>
/// Creates the deck (52 cards + deck/hand entities) and shuffles
/// using mulberry32 with the seed from the GameState singleton.
/// Runs once at the start of each round (during Dealing phase).
/// </summary>
public class DeckSetupSystem : ISystem
{
public void Run(World world)
{
var state = world.ReadSingleton<GameState>();
if (state.Phase != GamePhase.Dealing)
return;
// Only create deck entities if they don't exist yet.
var singletonEntity = World.SingletonEntity;
bool hasDeck = false;
using (var iter = world.Select<Deck>())
{
hasDeck = iter.MoveNext() && iter.CurrentEntity != singletonEntity;
}
if (!hasDeck)
{
// Create deck entity.
var deckEntity = world.CreateEntity();
world.AddComponent(deckEntity, new Deck());
// Create all 52 cards.
foreach (Suit suit in Enum.GetValues<Suit>())
{
foreach (Rank rank in Enum.GetValues<Rank>())
{
var cardEntity = world.CreateEntity();
world.AddComponent(cardEntity, new Card { Suit = suit, Rank = rank });
world.AddComponent(cardEntity, new InDeck { Source = cardEntity, Target = deckEntity });
}
}
// Create hand entities.
var playerHand = world.CreateEntity();
world.AddComponent(playerHand, new PlayerHand());
var dealerHand = world.CreateEntity();
world.AddComponent(dealerHand, new DealerHand());
}
// Shuffle the deck using mulberry32 with the current seed.
ref var mutableState = ref world.GetSingleton<GameState>();
var deckEntity2 = HitCommand.FindEntity<Deck>(world, singletonEntity);
var cards = world.GetSources<InDeck>(deckEntity2).ToArray();
Shuffle(world, deckEntity2, cards, ref mutableState.Seed);
}
private static void Shuffle(World world, Entity deckEntity, Entity[] cardEntities, ref uint seed)
{
// Fisher-Yates shuffle using mulberry32.
for (int i = cardEntities.Length - 1; i > 0; i--)
{
int j = Mulberry32.NextInt(ref seed, 0, i);
// Swap the InDeck relationship targets (cards are always in the deck,
// so there's nothing to swap except the cards themselves — but we
// shuffle the card order conceptually by removing and re-adding
// InDeck components in shuffled order). Actually, since InDeck
// is just a tag, we just re-shuffle the order in the source collection.
// The simplest approach: no need to swap component data; we just
// need to ensure the cards are iterated in shuffled order.
// We'll swap the card entities in the array.
(cardEntities[i], cardEntities[j]) = (cardEntities[j], cardEntities[i]);
}
// Now remove all InDeck and re-add in shuffled order so GetSources
// returns them in shuffled order.
for (int i = cardEntities.Length - 1; i >= 0; i--)
{
world.RemoveComponent<InDeck>(cardEntities[i]);
}
for (int i = 0; i < cardEntities.Length; i++)
{
world.AddComponent(cardEntities[i], new InDeck { Source = cardEntities[i], Target = deckEntity });
}
}
}
+72
View File
@@ -0,0 +1,72 @@
using OECS;
namespace Blackjack;
/// <summary>
/// Helper to calculate the blackjack value of a hand.
/// Aces count as 11 unless that would bust, then they count as 1.
/// </summary>
public static class HandUtil
{
public static int CalculateHand(World world, DealerHand hand)
{
return CalculateHandImpl(world, hand);
}
public static int CalculateHand(World world, PlayerHand hand)
{
return CalculateHandImpl(world, hand);
}
private static int CalculateHandImpl<T>(World world, T handTag)
where T : struct
{
// Find the hand entity.
Entity handEntity = Entity.Null;
using (var iter = world.Select<T>())
{
while (iter.MoveNext())
{
if (iter.CurrentEntity != World.SingletonEntity)
{
handEntity = iter.CurrentEntity;
break;
}
}
}
if (handEntity == Entity.Null)
return 0;
var cards = world.GetSources<Holds>(handEntity);
int total = 0;
int aceCount = 0;
foreach (var cardEntity in cards)
{
var card = world.ReadComponent<Card>(cardEntity);
int value = card.Rank switch
{
Rank.Ace => 11,
Rank.Jack => 10,
Rank.Queen => 10,
Rank.King => 10,
_ => (int)card.Rank
};
if (card.Rank == Rank.Ace)
aceCount++;
total += value;
}
// Downgrade aces from 11 to 1 as needed.
while (total > 21 && aceCount > 0)
{
total -= 10;
aceCount--;
}
return total;
}
}
@@ -0,0 +1,26 @@
using OECS;
namespace Blackjack;
/// <summary>
/// Evaluates the player's hand total after each hit.
/// Busts the player if they exceed 21.
/// </summary>
public class PlayerBustCheckSystem : ISystem
{
public void Run(World world)
{
var state = world.ReadSingleton<GameState>();
if (state.Phase != GamePhase.PlayerTurn)
return;
var total = HandUtil.CalculateHand(world, PlayerHandTag.Instance);
if (total <= 21)
return;
ref var mutableState = ref world.GetSingleton<GameState>();
mutableState.Phase = GamePhase.RoundOver;
mutableState.Result = RoundResult.PlayerBust;
world.MarkModified<GameState>(World.SingletonEntity);
}
}