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:
@@ -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(); }
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user