refactor: simplify entity lookups and clean up state updates

- Replace manual entity iteration with `world.FindEntity<T>()`
- Remove redundant `MarkModified<GameState>` calls
- Add safety counters to prevent infinite loops in tests and systems
- Call `FlushPendingMutations` after card transfers to ensure
  consistency
- Rename `Run` to `RunImpl` in several systems
This commit is contained in:
hypercross 2026-07-21 13:57:11 +08:00
parent 4cdfe9c957
commit cbb7edd472
12 changed files with 35 additions and 69 deletions

View File

@ -232,13 +232,7 @@ public class GameFlowTests
private static Entity FindEntity<T>(World world) where T : struct private static Entity FindEntity<T>(World world) where T : struct
{ {
using var iter = world.Select<T>(); return world.FindEntity<T>();
while (iter.MoveNext())
{
if (iter.CurrentEntity != World.SingletonEntity)
return iter.CurrentEntity;
}
return Entity.Null;
} }
private static int CountCardsInHand(World world, Entity handEntity) private static int CountCardsInHand(World world, Entity handEntity)
@ -251,11 +245,14 @@ public class GameFlowTests
private static List<(Suit, Rank)> GetAllCards(World world) private static List<(Suit, Rank)> GetAllCards(World world)
{ {
var cards = new List<(Suit, Rank)>(); var cards = new List<(Suit, Rank)>();
var query = world.Query().With<Card>().Build(); using (var iter = world.Select<Card>())
world.ForEach(query, (Entity e, ref Card card) =>
{ {
cards.Add((card.Suit, card.Rank)); while (iter.MoveNext())
}); {
var card = iter.Val1;
cards.Add((card.Suit, card.Rank));
}
}
cards.Sort((a, b) => cards.Sort((a, b) =>
{ {
int cmp = a.Item1.CompareTo(b.Item1); int cmp = a.Item1.CompareTo(b.Item1);

View File

@ -199,8 +199,10 @@ public class PlayTests
// Player turn: agent decides hit/stand. // Player turn: agent decides hit/stand.
int roundHits = 0; int roundHits = 0;
while (world.ReadSingleton<GameState>().Phase == GamePhase.PlayerTurn) int playerTurnSafety = 0;
while (world.ReadSingleton<GameState>().Phase == GamePhase.PlayerTurn && playerTurnSafety < 52)
{ {
playerTurnSafety++;
var total = HandUtil.CalculateHand(world, new PlayerHand()); var total = HandUtil.CalculateHand(world, new PlayerHand());
var decision = agent.Decide(world); var decision = agent.Decide(world);
log.Decisions.Add($"R{totalRounds + 1} Hand={total}, Decision={decision}"); log.Decisions.Add($"R{totalRounds + 1} Hand={total}, Decision={decision}");
@ -282,11 +284,7 @@ public class PlayTests
private static Entity FindEntity<T>(World world) where T : struct private static Entity FindEntity<T>(World world) where T : struct
{ {
using var iter = world.Select<T>(); return world.FindEntity<T>();
while (iter.MoveNext())
if (iter.CurrentEntity != World.SingletonEntity)
return iter.CurrentEntity;
return Entity.Null;
} }
private static List<string> GetHandCards(World world, Entity hand) private static List<string> GetHandCards(World world, Entity hand)

View File

@ -248,12 +248,6 @@ public class SnapshotTests
private static Entity FindEntity<T>(World world) where T : struct private static Entity FindEntity<T>(World world) where T : struct
{ {
using var iter = world.Select<T>(); return world.FindEntity<T>();
while (iter.MoveNext())
{
if (iter.CurrentEntity != World.SingletonEntity)
return iter.CurrentEntity;
}
return Entity.Null;
} }
} }

View File

@ -11,7 +11,7 @@ public struct HitCommand : ICommand
{ {
public void Execute(World world) public void Execute(World world)
{ {
ref var state = ref world.GetSingleton<GameState>(); var state = world.ReadSingleton<GameState>();
if (state.Phase != GamePhase.PlayerTurn) if (state.Phase != GamePhase.PlayerTurn)
return; return;
@ -25,9 +25,8 @@ public struct HitCommand : ICommand
internal static void DrawCard<THand>(World world) internal static void DrawCard<THand>(World world)
where THand : struct where THand : struct
{ {
var singletonEntity = World.SingletonEntity; var deckEntity = world.FindEntity<Deck>();
var deckEntity = FindEntity<Deck>(world, singletonEntity); var handEntity = world.FindEntity<THand>();
var handEntity = FindEntity<THand>(world, singletonEntity);
if (deckEntity == Entity.Null || handEntity == Entity.Null) if (deckEntity == Entity.Null || handEntity == Entity.Null)
return; return;
@ -42,17 +41,14 @@ public struct HitCommand : ICommand
// Remove from deck, add to hand. // Remove from deck, add to hand.
world.RemoveComponent<InDeck>(cardEntity); world.RemoveComponent<InDeck>(cardEntity);
world.AddComponent(cardEntity, new Holds { Target = handEntity }); world.AddComponent(cardEntity, new Holds { Target = handEntity });
// Flush so subsequent DrawCard calls see the updated state.
world.FlushPendingMutations();
} }
internal static Entity FindEntity<T>(World world, Entity singletonEntity) internal static Entity FindEntity<T>(World world)
where T : struct where T : struct
{ {
using var iter = world.Select<T>(); return world.FindEntity<T>();
while (iter.MoveNext())
{
if (iter.CurrentEntity != singletonEntity)
return iter.CurrentEntity;
}
return Entity.Null;
} }
} }

View File

@ -17,28 +17,25 @@ public struct NewRoundCommand : ICommand
return; return;
// Clear hands from previous round. // Clear hands from previous round.
var singletonEntity = World.SingletonEntity;
var handEntities = new List<Entity>(); var handEntities = new List<Entity>();
using (var iter = world.Select<PlayerHand>()) using (var iter = world.Select<PlayerHand>())
{ {
while (iter.MoveNext()) while (iter.MoveNext())
{ {
if (iter.CurrentEntity != singletonEntity) handEntities.Add(iter.Entity);
handEntities.Add(iter.CurrentEntity);
} }
} }
using (var iter = world.Select<DealerHand>()) using (var iter = world.Select<DealerHand>())
{ {
while (iter.MoveNext()) while (iter.MoveNext())
{ {
if (iter.CurrentEntity != singletonEntity) handEntities.Add(iter.Entity);
handEntities.Add(iter.CurrentEntity);
} }
} }
foreach (var hand in handEntities) foreach (var hand in handEntities)
{ {
var cards = world.GetSources<Holds>(hand); var cards = world.GetSources<Holds>(hand);
var deckEntity = HitCommand.FindEntity<Deck>(world, singletonEntity); var deckEntity = world.FindEntity<Deck>();
foreach (var card in cards) foreach (var card in cards)
{ {
world.RemoveComponent<Holds>(card); world.RemoveComponent<Holds>(card);
@ -49,6 +46,5 @@ public struct NewRoundCommand : ICommand
state.RoundNumber++; state.RoundNumber++;
state.Phase = GamePhase.Betting; state.Phase = GamePhase.Betting;
state.Result = RoundResult.None; state.Result = RoundResult.None;
world.MarkModified<GameState>(World.SingletonEntity);
} }
} }

View File

@ -24,6 +24,5 @@ public struct PlaceBetCommand : ICommand
state.CurrentBet = Amount; state.CurrentBet = Amount;
state.Chips -= Amount; state.Chips -= Amount;
state.Phase = GamePhase.Dealing; state.Phase = GamePhase.Dealing;
world.MarkModified<GameState>(World.SingletonEntity);
} }
} }

View File

@ -17,6 +17,5 @@ public struct StandCommand : ICommand
return; return;
state.Phase = GamePhase.DealerTurn; state.Phase = GamePhase.DealerTurn;
world.MarkModified<GameState>(World.SingletonEntity);
} }
} }

View File

@ -8,7 +8,7 @@ namespace Game.Blackjack;
/// </summary> /// </summary>
public class DealSystem : ISystem public class DealSystem : ISystem
{ {
public void Run(World world) public void RunImpl(World world)
{ {
var state = world.ReadSingleton<GameState>(); var state = world.ReadSingleton<GameState>();
if (state.Phase != GamePhase.Dealing) if (state.Phase != GamePhase.Dealing)
@ -23,7 +23,6 @@ public class DealSystem : ISystem
HitCommand.DrawCard<DealerHand>(world); HitCommand.DrawCard<DealerHand>(world);
mutableState.Phase = GamePhase.PlayerTurn; mutableState.Phase = GamePhase.PlayerTurn;
world.MarkModified<GameState>(World.SingletonEntity);
} }
} }

View File

@ -8,7 +8,7 @@ namespace Game.Blackjack;
/// </summary> /// </summary>
public class DealerSystem : ISystem public class DealerSystem : ISystem
{ {
public void Run(World world) public void RunImpl(World world)
{ {
var state = world.ReadSingleton<GameState>(); var state = world.ReadSingleton<GameState>();
if (state.Phase != GamePhase.DealerTurn) if (state.Phase != GamePhase.DealerTurn)
@ -18,10 +18,12 @@ public class DealerSystem : ISystem
// Dealer must hit on 16 and below, stand on 17+. // Dealer must hit on 16 and below, stand on 17+.
int dealerTotal = HandUtil.CalculateHand(world, DealerHandTag.Instance); int dealerTotal = HandUtil.CalculateHand(world, DealerHandTag.Instance);
while (dealerTotal < 17) int safety = 0;
while (dealerTotal < 17 && safety < 52)
{ {
HitCommand.DrawCard<DealerHand>(world); HitCommand.DrawCard<DealerHand>(world);
dealerTotal = HandUtil.CalculateHand(world, DealerHandTag.Instance); dealerTotal = HandUtil.CalculateHand(world, DealerHandTag.Instance);
safety++;
} }
int playerTotal = HandUtil.CalculateHand(world, PlayerHandTag.Instance); int playerTotal = HandUtil.CalculateHand(world, PlayerHandTag.Instance);
@ -49,6 +51,5 @@ public class DealerSystem : ISystem
mutableState.Chips += mutableState.CurrentBet; mutableState.Chips += mutableState.CurrentBet;
} }
world.MarkModified<GameState>(World.SingletonEntity);
} }
} }

View File

@ -9,18 +9,17 @@ namespace Game.Blackjack;
/// </summary> /// </summary>
public class DeckSetupSystem : ISystem public class DeckSetupSystem : ISystem
{ {
public void Run(World world) public void RunImpl(World world)
{ {
var state = world.ReadSingleton<GameState>(); var state = world.ReadSingleton<GameState>();
if (state.Phase != GamePhase.Dealing) if (state.Phase != GamePhase.Dealing)
return; return;
// Only create deck entities if they don't exist yet. // Only create deck entities if they don't exist yet.
var singletonEntity = World.SingletonEntity;
bool hasDeck = false; bool hasDeck = false;
using (var iter = world.Select<Deck>()) using (var iter = world.Select<Deck>())
{ {
hasDeck = iter.MoveNext() && iter.CurrentEntity != singletonEntity; hasDeck = iter.MoveNext();
} }
if (!hasDeck) if (!hasDeck)
@ -50,7 +49,7 @@ public class DeckSetupSystem : ISystem
// Shuffle the deck using mulberry32 with the current seed. // Shuffle the deck using mulberry32 with the current seed.
ref var mutableState = ref world.GetSingleton<GameState>(); ref var mutableState = ref world.GetSingleton<GameState>();
var deckEntity2 = HitCommand.FindEntity<Deck>(world, singletonEntity); var deckEntity2 = world.FindEntity<Deck>();
var cards = world.GetSources<InDeck>(deckEntity2).ToArray(); var cards = world.GetSources<InDeck>(deckEntity2).ToArray();
Shuffle(world, deckEntity2, cards, ref mutableState.Seed); Shuffle(world, deckEntity2, cards, ref mutableState.Seed);
} }

View File

@ -22,18 +22,7 @@ public static class HandUtil
where T : struct where T : struct
{ {
// Find the hand entity. // Find the hand entity.
Entity handEntity = Entity.Null; var handEntity = world.FindEntity<T>();
using (var iter = world.Select<T>())
{
while (iter.MoveNext())
{
if (iter.CurrentEntity != World.SingletonEntity)
{
handEntity = iter.CurrentEntity;
break;
}
}
}
if (handEntity == Entity.Null) if (handEntity == Entity.Null)
return 0; return 0;

View File

@ -8,7 +8,7 @@ namespace Game.Blackjack;
/// </summary> /// </summary>
public class PlayerBustCheckSystem : ISystem public class PlayerBustCheckSystem : ISystem
{ {
public void Run(World world) public void RunImpl(World world)
{ {
var state = world.ReadSingleton<GameState>(); var state = world.ReadSingleton<GameState>();
if (state.Phase != GamePhase.PlayerTurn) if (state.Phase != GamePhase.PlayerTurn)
@ -21,6 +21,5 @@ public class PlayerBustCheckSystem : ISystem
ref var mutableState = ref world.GetSingleton<GameState>(); ref var mutableState = ref world.GetSingleton<GameState>();
mutableState.Phase = GamePhase.RoundOver; mutableState.Phase = GamePhase.RoundOver;
mutableState.Result = RoundResult.PlayerBust; mutableState.Result = RoundResult.PlayerBust;
world.MarkModified<GameState>(World.SingletonEntity);
} }
} }