feat: add OECS.PlayTest project
Introduce a new PlayTest project containing tools for simulating gameplay, including: - Agent abstractions (IAgent, GreedyAgent, WeightedAgentPool) - ObservableCapture for logging entity changes - PlayLog for generating and saving formatted play logs
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
using OECS;
|
||||
|
||||
namespace OECS.PlayTest;
|
||||
|
||||
/// <summary>
|
||||
/// Abstract agent that picks the action with the highest score.
|
||||
/// When <see cref="ScoreAction"/> returns 0 for all actions (the default),
|
||||
/// the agent behaves as a uniform random agent over legal actions.
|
||||
/// Ties are broken randomly.
|
||||
/// </summary>
|
||||
public abstract class GreedyAgent<TDecision> : IAgent<TDecision>
|
||||
{
|
||||
private static readonly Random _rng = new();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the list of currently legal actions.
|
||||
/// </summary>
|
||||
protected abstract List<TDecision> GetLegalActions(World world);
|
||||
|
||||
/// <summary>
|
||||
/// Scores an action. Defaults to 0 (uniform random).
|
||||
/// </summary>
|
||||
protected virtual float ScoreAction(World world, TDecision action) => 0f;
|
||||
|
||||
public TDecision Decide(World world)
|
||||
{
|
||||
var actions = GetLegalActions(world);
|
||||
if (actions.Count == 0)
|
||||
throw new InvalidOperationException(
|
||||
$"{GetType().Name}: no legal actions available");
|
||||
|
||||
if (actions.Count == 1)
|
||||
return actions[0];
|
||||
|
||||
float bestScore = float.MinValue;
|
||||
var bestActions = new List<TDecision>();
|
||||
foreach (var action in actions)
|
||||
{
|
||||
float score = ScoreAction(world, action);
|
||||
if (score > bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
bestActions.Clear();
|
||||
bestActions.Add(action);
|
||||
}
|
||||
else if (score == bestScore)
|
||||
{
|
||||
bestActions.Add(action);
|
||||
}
|
||||
}
|
||||
|
||||
return bestActions[_rng.Next(bestActions.Count)];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using OECS;
|
||||
|
||||
namespace OECS.PlayTest;
|
||||
|
||||
/// <summary>
|
||||
/// An AI player that inspects the world and returns a decision.
|
||||
/// </summary>
|
||||
public interface IAgent<TDecision>
|
||||
{
|
||||
TDecision Decide(World world);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
namespace OECS.PlayTest;
|
||||
|
||||
/// <summary>
|
||||
/// A pool of agents selected by weight. Higher weight = more likely to be picked.
|
||||
/// </summary>
|
||||
public class WeightedAgentPool<TDecision>
|
||||
{
|
||||
private readonly List<(IAgent<TDecision> Agent, int Weight)> _agents = new();
|
||||
private int _totalWeight;
|
||||
private static readonly Random _rng = new();
|
||||
|
||||
public void Add(IAgent<TDecision> agent, int weight)
|
||||
{
|
||||
_agents.Add((agent, weight));
|
||||
_totalWeight += weight;
|
||||
}
|
||||
|
||||
public IAgent<TDecision> Pick()
|
||||
{
|
||||
if (_agents.Count == 0)
|
||||
throw new InvalidOperationException("WeightedAgentPool is empty");
|
||||
|
||||
int roll = _rng.Next(_totalWeight);
|
||||
int cumulative = 0;
|
||||
foreach (var (agent, weight) in _agents)
|
||||
{
|
||||
cumulative += weight;
|
||||
if (roll < cumulative)
|
||||
return agent;
|
||||
}
|
||||
return _agents[^1].Agent;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user