diff --git a/examples/Blackjack/Blackjack.csproj b/examples/Blackjack/Blackjack.csproj
new file mode 100644
index 0000000..a91f1df
--- /dev/null
+++ b/examples/Blackjack/Blackjack.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ Blackjack
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/Blackjack/Commands/HitCommand.cs b/examples/Blackjack/Commands/HitCommand.cs
new file mode 100644
index 0000000..3228876
--- /dev/null
+++ b/examples/Blackjack/Commands/HitCommand.cs
@@ -0,0 +1,58 @@
+using MessagePack;
+using OECS;
+
+namespace Blackjack;
+
+///
+/// Player hits: draws one card from the deck to the player's hand.
+///
+[MessagePackObject]
+public struct HitCommand : ICommand
+{
+ public void Execute(World world)
+ {
+ ref var state = ref world.GetSingleton();
+
+ if (state.Phase != GamePhase.PlayerTurn)
+ return;
+
+ DrawCard(world);
+ }
+
+ ///
+ /// Draws the top card from the deck and adds it to the given hand.
+ ///
+ internal static void DrawCard(World world)
+ where THand : struct
+ {
+ var singletonEntity = World.SingletonEntity;
+ var deckEntity = FindEntity(world, singletonEntity);
+ var handEntity = FindEntity(world, singletonEntity);
+
+ if (deckEntity == Entity.Null || handEntity == Entity.Null)
+ return;
+
+ // Find a card still in the deck.
+ var cardsInDeck = world.GetSources(deckEntity);
+ if (cardsInDeck.Count == 0)
+ return;
+
+ var cardEntity = cardsInDeck.First();
+
+ // Remove from deck, add to hand.
+ world.RemoveComponent(cardEntity);
+ world.AddComponent(cardEntity, new Holds { Source = cardEntity, Target = handEntity });
+ }
+
+ internal static Entity FindEntity(World world, Entity singletonEntity)
+ where T : struct
+ {
+ using var iter = world.Select();
+ while (iter.MoveNext())
+ {
+ if (iter.CurrentEntity != singletonEntity)
+ return iter.CurrentEntity;
+ }
+ return Entity.Null;
+ }
+}
\ No newline at end of file
diff --git a/examples/Blackjack/Commands/NewRoundCommand.cs b/examples/Blackjack/Commands/NewRoundCommand.cs
new file mode 100644
index 0000000..afe61b9
--- /dev/null
+++ b/examples/Blackjack/Commands/NewRoundCommand.cs
@@ -0,0 +1,53 @@
+using MessagePack;
+using OECS;
+
+namespace Blackjack;
+
+///
+/// Starts a new round after the previous one ended.
+///
+[MessagePackObject]
+public struct NewRoundCommand : ICommand
+{
+ public void Execute(World world)
+ {
+ ref var state = ref world.GetSingleton();
+
+ if (state.Phase != GamePhase.RoundOver)
+ return;
+
+ // Clear hands from previous round.
+ var singletonEntity = World.SingletonEntity;
+ var handEntities = new List();
+ using (var iter = world.Select())
+ {
+ while (iter.MoveNext())
+ {
+ if (iter.CurrentEntity != singletonEntity)
+ handEntities.Add(iter.CurrentEntity);
+ }
+ }
+ using (var iter = world.Select())
+ {
+ while (iter.MoveNext())
+ {
+ if (iter.CurrentEntity != singletonEntity)
+ handEntities.Add(iter.CurrentEntity);
+ }
+ }
+ foreach (var hand in handEntities)
+ {
+ var cards = world.GetSources(hand);
+ var deckEntity = HitCommand.FindEntity(world, singletonEntity);
+ foreach (var card in cards)
+ {
+ world.RemoveComponent(card);
+ world.AddComponent(card, new InDeck { Source = card, Target = deckEntity });
+ }
+ }
+
+ state.Phase = GamePhase.Betting;
+ state.Result = RoundResult.None;
+ world.MarkModified(World.SingletonEntity);
+ }
+}
\ No newline at end of file
diff --git a/examples/Blackjack/Commands/PlaceBetCommand.cs b/examples/Blackjack/Commands/PlaceBetCommand.cs
new file mode 100644
index 0000000..527afad
--- /dev/null
+++ b/examples/Blackjack/Commands/PlaceBetCommand.cs
@@ -0,0 +1,29 @@
+using MessagePack;
+using OECS;
+
+namespace Blackjack;
+
+///
+/// Places a bet and advances the game to the dealing phase.
+///
+[MessagePackObject]
+public struct PlaceBetCommand : ICommand
+{
+ [Key(0)] public int Amount;
+
+ public void Execute(World world)
+ {
+ ref var state = ref world.GetSingleton();
+
+ if (state.Phase != GamePhase.Betting)
+ return;
+
+ if (Amount < 1 || Amount > state.Chips)
+ return;
+
+ state.CurrentBet = Amount;
+ state.Chips -= Amount;
+ state.Phase = GamePhase.Dealing;
+ world.MarkModified(World.SingletonEntity);
+ }
+}
\ No newline at end of file
diff --git a/examples/Blackjack/Commands/StandCommand.cs b/examples/Blackjack/Commands/StandCommand.cs
new file mode 100644
index 0000000..97d1ed8
--- /dev/null
+++ b/examples/Blackjack/Commands/StandCommand.cs
@@ -0,0 +1,22 @@
+using MessagePack;
+using OECS;
+
+namespace Blackjack;
+
+///
+/// Player stands: advance to the dealer's turn.
+///
+[MessagePackObject]
+public struct StandCommand : ICommand
+{
+ public void Execute(World world)
+ {
+ ref var state = ref world.GetSingleton();
+
+ if (state.Phase != GamePhase.PlayerTurn)
+ return;
+
+ state.Phase = GamePhase.DealerTurn;
+ world.MarkModified(World.SingletonEntity);
+ }
+}
\ No newline at end of file
diff --git a/examples/Blackjack/Components/Card.cs b/examples/Blackjack/Components/Card.cs
new file mode 100644
index 0000000..9c87084
--- /dev/null
+++ b/examples/Blackjack/Components/Card.cs
@@ -0,0 +1,14 @@
+using MessagePack;
+
+namespace Blackjack;
+
+///
+/// A playing card with a suit and rank.
+/// One entity per card.
+///
+[MessagePackObject]
+public struct Card
+{
+ [Key(0)] public Suit Suit;
+ [Key(1)] public Rank Rank;
+}
\ No newline at end of file
diff --git a/examples/Blackjack/Components/DealerHand.cs b/examples/Blackjack/Components/DealerHand.cs
new file mode 100644
index 0000000..5dd8fd0
--- /dev/null
+++ b/examples/Blackjack/Components/DealerHand.cs
@@ -0,0 +1,9 @@
+using MessagePack;
+
+namespace Blackjack;
+
+///
+/// Tag component marking the dealer's hand entity.
+///
+[MessagePackObject]
+public struct DealerHand { }
\ No newline at end of file
diff --git a/examples/Blackjack/Components/Deck.cs b/examples/Blackjack/Components/Deck.cs
new file mode 100644
index 0000000..9754ee7
--- /dev/null
+++ b/examples/Blackjack/Components/Deck.cs
@@ -0,0 +1,9 @@
+using MessagePack;
+
+namespace Blackjack;
+
+///
+/// Tag component marking the deck entity.
+///
+[MessagePackObject]
+public struct Deck { }
\ No newline at end of file
diff --git a/examples/Blackjack/Components/Holds.cs b/examples/Blackjack/Components/Holds.cs
new file mode 100644
index 0000000..d704e03
--- /dev/null
+++ b/examples/Blackjack/Components/Holds.cs
@@ -0,0 +1,15 @@
+using MessagePack;
+using OECS;
+
+namespace Blackjack;
+
+///
+/// Relationship from a hand entity to a card entity,
+/// representing that the hand holds this card.
+///
+[MessagePackObject]
+public struct Holds : IRelationship
+{
+ [Key(0)] public Entity Source { get; set; }
+ [Key(1)] public Entity Target { get; set; }
+}
\ No newline at end of file
diff --git a/examples/Blackjack/Components/InDeck.cs b/examples/Blackjack/Components/InDeck.cs
new file mode 100644
index 0000000..13b54d0
--- /dev/null
+++ b/examples/Blackjack/Components/InDeck.cs
@@ -0,0 +1,15 @@
+using MessagePack;
+using OECS;
+
+namespace Blackjack;
+
+///
+/// Relationship from a card entity to the deck entity,
+/// representing that the card is in the deck.
+///
+[MessagePackObject]
+public struct InDeck : IRelationship
+{
+ [Key(0)] public Entity Source { get; set; }
+ [Key(1)] public Entity Target { get; set; }
+}
\ No newline at end of file
diff --git a/examples/Blackjack/Components/PlayerHand.cs b/examples/Blackjack/Components/PlayerHand.cs
new file mode 100644
index 0000000..cd35db0
--- /dev/null
+++ b/examples/Blackjack/Components/PlayerHand.cs
@@ -0,0 +1,9 @@
+using MessagePack;
+
+namespace Blackjack;
+
+///
+/// Tag component marking the player's hand entity.
+///
+[MessagePackObject]
+public struct PlayerHand { }
\ No newline at end of file
diff --git a/examples/Blackjack/Components/Rank.cs b/examples/Blackjack/Components/Rank.cs
new file mode 100644
index 0000000..3458aa0
--- /dev/null
+++ b/examples/Blackjack/Components/Rank.cs
@@ -0,0 +1,18 @@
+namespace Blackjack;
+
+public enum Rank : byte
+{
+ Ace = 1,
+ Two = 2,
+ Three = 3,
+ Four = 4,
+ Five = 5,
+ Six = 6,
+ Seven = 7,
+ Eight = 8,
+ Nine = 9,
+ Ten = 10,
+ Jack = 11,
+ Queen = 12,
+ King = 13
+}
\ No newline at end of file
diff --git a/examples/Blackjack/Components/Suit.cs b/examples/Blackjack/Components/Suit.cs
new file mode 100644
index 0000000..ab16b9b
--- /dev/null
+++ b/examples/Blackjack/Components/Suit.cs
@@ -0,0 +1,9 @@
+namespace Blackjack;
+
+public enum Suit : byte
+{
+ Clubs = 0,
+ Diamonds = 1,
+ Hearts = 2,
+ Spades = 3
+}
\ No newline at end of file
diff --git a/examples/Blackjack/Mulberry32.cs b/examples/Blackjack/Mulberry32.cs
new file mode 100644
index 0000000..406bb48
--- /dev/null
+++ b/examples/Blackjack/Mulberry32.cs
@@ -0,0 +1,28 @@
+namespace Blackjack;
+
+///
+/// Mulberry32 PRNG — a fast, high-quality 32-bit random number generator.
+/// Seed is stored in the ECS GameState singleton.
+///
+public static class Mulberry32
+{
+ ///
+ /// Advances the state and returns a random float in [0, 1).
+ ///
+ 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;
+ }
+
+ ///
+ /// Advances the state and returns a random integer in [min, max].
+ ///
+ public static int NextInt(ref uint state, int min, int max)
+ {
+ return (int)(NextFloat(ref state) * (max - min + 1)) + min;
+ }
+}
\ No newline at end of file
diff --git a/examples/Blackjack/Program.cs b/examples/Blackjack/Program.cs
new file mode 100644
index 0000000..0b97dcc
--- /dev/null
+++ b/examples/Blackjack/Program.cs
@@ -0,0 +1,114 @@
+using OECS;
+
+namespace Blackjack;
+
+public static class Program
+{
+ public static void Main()
+ {
+ var world = new World();
+
+ // ── Initialize game state ─────────────────────────────────────
+
+ // Use a seed based on current time, or a fixed seed for reproducibility.
+ uint seed = (uint)Environment.TickCount;
+ // Uncomment the next line for a deterministic game:
+ // seed = 12345u;
+
+ world.SetSingleton(new GameState
+ {
+ Phase = GamePhase.Betting,
+ Result = RoundResult.None,
+ Chips = 100,
+ CurrentBet = 0,
+ RoundNumber = 1,
+ Seed = seed
+ });
+ world.PostChanges();
+
+ // ── Wire up systems ───────────────────────────────────────────
+
+ var group = new SystemGroup(world);
+ group.Add(new DeckSetupSystem());
+ group.Add(new DealSystem());
+ group.Add(new PlayerBustCheckSystem());
+ group.Add(new DealerSystem());
+ group.Add(new RenderSystem());
+
+ // ── Game loop ─────────────────────────────────────────────────
+
+ group.RunLogical();
+
+ while (true)
+ {
+ var state = world.ReadSingleton();
+
+ // Check for game over (out of chips).
+ if (state.Chips <= 0 && state.Phase == GamePhase.Betting)
+ {
+ Console.WriteLine();
+ Console.WriteLine(" You're out of chips! Game over.");
+ break;
+ }
+
+ switch (state.Phase)
+ {
+ case GamePhase.Betting:
+ var input = Console.ReadLine();
+ if (string.IsNullOrWhiteSpace(input))
+ continue;
+ if (input.Trim().ToLower() == "quit")
+ return;
+
+ if (int.TryParse(input.Trim(), out var bet))
+ {
+ world.Commands.Enqueue(new PlaceBetCommand { Amount = bet });
+ }
+ else
+ {
+ Console.WriteLine(" Invalid bet. Enter a number.");
+ continue;
+ }
+ break;
+
+ case GamePhase.PlayerTurn:
+ var choice = Console.ReadLine();
+ if (string.IsNullOrWhiteSpace(choice))
+ continue;
+ if (choice.Trim().ToLower() == "quit")
+ return;
+
+ switch (choice.Trim().ToLower())
+ {
+ case "h":
+ case "hit":
+ world.Commands.Enqueue(new HitCommand());
+ break;
+ case "s":
+ case "stand":
+ world.Commands.Enqueue(new StandCommand());
+ break;
+ default:
+ Console.WriteLine(" Invalid choice. Enter H or S.");
+ continue;
+ }
+ break;
+
+ case GamePhase.RoundOver:
+ Console.ReadLine();
+ ref var roundState = ref world.GetSingleton();
+ roundState.RoundNumber++;
+ world.MarkModified(World.SingletonEntity);
+ world.PostChanges();
+ world.Commands.Enqueue(new NewRoundCommand());
+ break;
+
+ default:
+ // Dealing, DealerTurn — systems handle these automatically.
+ break;
+ }
+
+ group.RunLogical();
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/Blackjack/Singletons/GamePhase.cs b/examples/Blackjack/Singletons/GamePhase.cs
new file mode 100644
index 0000000..4267941
--- /dev/null
+++ b/examples/Blackjack/Singletons/GamePhase.cs
@@ -0,0 +1,10 @@
+namespace Blackjack;
+
+public enum GamePhase : byte
+{
+ Betting = 0,
+ Dealing = 1,
+ PlayerTurn = 2,
+ DealerTurn = 3,
+ RoundOver = 4
+}
\ No newline at end of file
diff --git a/examples/Blackjack/Singletons/GameState.cs b/examples/Blackjack/Singletons/GameState.cs
new file mode 100644
index 0000000..19ebe5f
--- /dev/null
+++ b/examples/Blackjack/Singletons/GameState.cs
@@ -0,0 +1,17 @@
+using MessagePack;
+
+namespace Blackjack;
+
+///
+/// Global game state stored on the singleton entity.
+///
+[MessagePackObject]
+public struct GameState
+{
+ [Key(0)] public GamePhase Phase;
+ [Key(1)] public RoundResult Result;
+ [Key(2)] public int Chips;
+ [Key(3)] public int CurrentBet;
+ [Key(4)] public int RoundNumber;
+ [Key(5)] public uint Seed;
+}
\ No newline at end of file
diff --git a/examples/Blackjack/Singletons/RoundResult.cs b/examples/Blackjack/Singletons/RoundResult.cs
new file mode 100644
index 0000000..ef350f4
--- /dev/null
+++ b/examples/Blackjack/Singletons/RoundResult.cs
@@ -0,0 +1,12 @@
+namespace Blackjack;
+
+public enum RoundResult : byte
+{
+ None = 0,
+ PlayerBust = 1,
+ DealerBust = 2,
+ PlayerWin = 3,
+ DealerWin = 4,
+ Push = 5,
+ Blackjack = 6
+}
\ No newline at end of file
diff --git a/examples/Blackjack/Systems/DealSystem.cs b/examples/Blackjack/Systems/DealSystem.cs
new file mode 100644
index 0000000..81c0715
--- /dev/null
+++ b/examples/Blackjack/Systems/DealSystem.cs
@@ -0,0 +1,31 @@
+using OECS;
+
+namespace Blackjack;
+
+///
+/// Deals initial two cards to player and dealer when entering the dealing phase.
+/// Advances to player turn after dealing.
+///
+public class DealSystem : ISystem
+{
+ public void Run(World world)
+ {
+ var state = world.ReadSingleton();
+ if (state.Phase != GamePhase.Dealing)
+ return;
+
+ ref var mutableState = ref world.GetSingleton();
+
+ // Deal two cards to player, then two to dealer.
+ HitCommand.DrawCard(world);
+ HitCommand.DrawCard(world);
+ HitCommand.DrawCard(world);
+ HitCommand.DrawCard(world);
+
+ mutableState.Phase = GamePhase.PlayerTurn;
+ world.MarkModified(World.SingletonEntity);
+ }
+}
+
+internal static class PlayerHandTag { public static readonly PlayerHand Instance = new(); }
+internal static class DealerHandTag { public static readonly DealerHand Instance = new(); }
\ No newline at end of file
diff --git a/examples/Blackjack/Systems/DealerSystem.cs b/examples/Blackjack/Systems/DealerSystem.cs
new file mode 100644
index 0000000..d3caec4
--- /dev/null
+++ b/examples/Blackjack/Systems/DealerSystem.cs
@@ -0,0 +1,54 @@
+using OECS;
+
+namespace Blackjack;
+
+///
+/// Dealer draws cards until reaching 17 or higher,
+/// then resolves the round result.
+///
+public class DealerSystem : ISystem
+{
+ public void Run(World world)
+ {
+ var state = world.ReadSingleton();
+ if (state.Phase != GamePhase.DealerTurn)
+ return;
+
+ ref var mutableState = ref world.GetSingleton();
+
+ // Dealer must hit on 16 and below, stand on 17+.
+ int dealerTotal = HandUtil.CalculateHand(world, DealerHandTag.Instance);
+ while (dealerTotal < 17)
+ {
+ HitCommand.DrawCard(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(World.SingletonEntity);
+ }
+}
\ No newline at end of file
diff --git a/examples/Blackjack/Systems/DeckSetupSystem.cs b/examples/Blackjack/Systems/DeckSetupSystem.cs
new file mode 100644
index 0000000..87c54d7
--- /dev/null
+++ b/examples/Blackjack/Systems/DeckSetupSystem.cs
@@ -0,0 +1,87 @@
+using OECS;
+
+namespace Blackjack;
+
+///
+/// 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).
+///
+public class DeckSetupSystem : ISystem
+{
+ public void Run(World world)
+ {
+ var state = world.ReadSingleton();
+ 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())
+ {
+ 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())
+ {
+ foreach (Rank rank in Enum.GetValues())
+ {
+ 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();
+ var deckEntity2 = HitCommand.FindEntity(world, singletonEntity);
+ var cards = world.GetSources(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(cardEntities[i]);
+ }
+ for (int i = 0; i < cardEntities.Length; i++)
+ {
+ world.AddComponent(cardEntities[i], new InDeck { Source = cardEntities[i], Target = deckEntity });
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/Blackjack/Systems/HandUtil.cs b/examples/Blackjack/Systems/HandUtil.cs
new file mode 100644
index 0000000..abf5da3
--- /dev/null
+++ b/examples/Blackjack/Systems/HandUtil.cs
@@ -0,0 +1,72 @@
+using OECS;
+
+namespace Blackjack;
+
+///
+/// Helper to calculate the blackjack value of a hand.
+/// Aces count as 11 unless that would bust, then they count as 1.
+///
+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(World world, T handTag)
+ where T : struct
+ {
+ // Find the hand entity.
+ Entity handEntity = Entity.Null;
+ using (var iter = world.Select())
+ {
+ while (iter.MoveNext())
+ {
+ if (iter.CurrentEntity != World.SingletonEntity)
+ {
+ handEntity = iter.CurrentEntity;
+ break;
+ }
+ }
+ }
+
+ if (handEntity == Entity.Null)
+ return 0;
+
+ var cards = world.GetSources(handEntity);
+ int total = 0;
+ int aceCount = 0;
+
+ foreach (var cardEntity in cards)
+ {
+ var card = world.ReadComponent(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;
+ }
+}
\ No newline at end of file
diff --git a/examples/Blackjack/Systems/PlayerBustCheckSystem.cs b/examples/Blackjack/Systems/PlayerBustCheckSystem.cs
new file mode 100644
index 0000000..a05ca13
--- /dev/null
+++ b/examples/Blackjack/Systems/PlayerBustCheckSystem.cs
@@ -0,0 +1,26 @@
+using OECS;
+
+namespace Blackjack;
+
+///
+/// Evaluates the player's hand total after each hit.
+/// Busts the player if they exceed 21.
+///
+public class PlayerBustCheckSystem : ISystem
+{
+ public void Run(World world)
+ {
+ var state = world.ReadSingleton();
+ if (state.Phase != GamePhase.PlayerTurn)
+ return;
+
+ var total = HandUtil.CalculateHand(world, PlayerHandTag.Instance);
+ if (total <= 21)
+ return;
+
+ ref var mutableState = ref world.GetSingleton();
+ mutableState.Phase = GamePhase.RoundOver;
+ mutableState.Result = RoundResult.PlayerBust;
+ world.MarkModified(World.SingletonEntity);
+ }
+}
\ No newline at end of file
diff --git a/examples/Blackjack/Systems/RenderSystem.cs b/examples/Blackjack/Systems/RenderSystem.cs
new file mode 100644
index 0000000..f325e41
--- /dev/null
+++ b/examples/Blackjack/Systems/RenderSystem.cs
@@ -0,0 +1,148 @@
+using OECS;
+
+namespace Blackjack;
+
+///
+/// Renders the current game state to the console.
+///
+public class RenderSystem : ISystem
+{
+ public void Run(World world)
+ {
+ var state = world.ReadSingleton();
+
+ Console.WriteLine();
+ Console.WriteLine(" Blackjack");
+ Console.WriteLine(" ═════════");
+ Console.WriteLine();
+ Console.WriteLine($" Chips: {state.Chips} | Round: {state.RoundNumber}");
+ Console.WriteLine();
+
+ // Show dealer's hand.
+ var dealerTotal = HandUtil.CalculateHand(world, DealerHandTag.Instance);
+ Console.Write(" Dealer: ");
+ if (state.Phase == GamePhase.PlayerTurn)
+ {
+ // Hide first card (hole card) during player's turn.
+ Console.Write("?? ");
+ PrintHand(world, DealerHandTag.Instance, skipFirst: true);
+ }
+ else
+ {
+ PrintHand(world, DealerHandTag.Instance);
+ Console.Write($" ({dealerTotal})");
+ }
+ Console.WriteLine();
+
+ // Show player's hand.
+ var playerTotal = HandUtil.CalculateHand(world, PlayerHandTag.Instance);
+ Console.Write(" Player: ");
+ PrintHand(world, PlayerHandTag.Instance);
+ Console.Write($" ({playerTotal})");
+ Console.WriteLine();
+ Console.WriteLine();
+
+ // Status line.
+ switch (state.Phase)
+ {
+ case GamePhase.Betting:
+ Console.Write($" Enter bet (1–{state.Chips}): ");
+ break;
+ case GamePhase.PlayerTurn:
+ Console.Write(" [H]it or [S]tand: ");
+ break;
+ case GamePhase.RoundOver:
+ Console.WriteLine($" {ResultText(state.Result)}");
+ Console.WriteLine();
+ Console.Write(" Press Enter for next round...");
+ break;
+ default:
+ break;
+ }
+ }
+
+ private static void PrintHand(World world, DealerHand hand, bool skipFirst = false)
+ {
+ PrintHandImpl(world, hand, skipFirst);
+ }
+
+ private static void PrintHand(World world, PlayerHand hand)
+ {
+ PrintHandImpl(world, hand, skipFirst: false);
+ }
+
+ private static void PrintHandImpl(World world, T handTag, bool skipFirst)
+ where T : struct
+ {
+ Entity handEntity = Entity.Null;
+ using (var iter = world.Select())
+ {
+ while (iter.MoveNext())
+ {
+ if (iter.CurrentEntity != World.SingletonEntity)
+ {
+ handEntity = iter.CurrentEntity;
+ break;
+ }
+ }
+ }
+
+ if (handEntity == Entity.Null)
+ return;
+
+ var cards = world.GetSources(handEntity);
+ var ordered = cards.OrderByDescending(c =>
+ {
+ var card = world.ReadComponent(c);
+ return (int)card.Rank;
+ }).ToList();
+
+ bool first = true;
+ foreach (var cardEntity in ordered)
+ {
+ if (skipFirst && first)
+ {
+ first = false;
+ continue;
+ }
+ first = false;
+ var card = world.ReadComponent(cardEntity);
+ Console.Write(CardToString(card));
+ Console.Write(" ");
+ }
+ }
+
+ private static string CardToString(Card card)
+ {
+ var suit = card.Suit switch
+ {
+ Suit.Hearts => "♥",
+ Suit.Diamonds => "♦",
+ Suit.Clubs => "♣",
+ Suit.Spades => "♠",
+ _ => "?"
+ };
+
+ var rank = card.Rank switch
+ {
+ Rank.Ace => "A",
+ Rank.Jack => "J",
+ Rank.Queen => "Q",
+ Rank.King => "K",
+ _ => ((int)card.Rank).ToString()
+ };
+
+ return $"{rank}{suit}";
+ }
+
+ private static string ResultText(RoundResult result) => result switch
+ {
+ RoundResult.PlayerBust => "You bust! Dealer wins.",
+ RoundResult.DealerBust => "Dealer busts! You win!",
+ RoundResult.PlayerWin => "You win!",
+ RoundResult.DealerWin => "Dealer wins.",
+ RoundResult.Push => "Push — it's a tie!",
+ RoundResult.Blackjack => "Blackjack!",
+ _ => ""
+ };
+}
\ No newline at end of file