Simulated Games: how to define a game with randomness
This page presents a fully working example of a very simple SimulatedGame that involves randomness.
The game is defined as follows:
-
First, the player chooses a die among 2-sided, 4-sided and 6-sided ones
-
Next, the the chosen die is rolled
-
The player receives money equal the number rolled
-
The player may buy a weapons. Weapons cost X and have an attack value of 3*X
-
The score of the player is the attack value of the obtained weapon
The following code represents possible states. In each state, the player has different available actions:
-
C++
-
C#
enum class PlayerState
{
CHOOSE_DIE,
ROLL,
CHOOSE_WEAPON
};
public enum PlayerState
{
CHOOSE_DIE,
ROLL,
CHOOSE_WEAPON
}
The next excerpt shows how the player’s actions can be defined. The player class (derived from SimulatedGameThinkingUnit) will be provided later.
In this game, there is randomness by means of rolling a die. Therefore, we will introduce a separate player, called DiePlayer, that inherits from SimulatedGameStochasticUnit. |
The only logic implemented in ChooseDieAction is passing control to the proper DiePlayer. This kind of player has random actions. DiePlayers differ between themselves based on which type of die they roll.
The BuyWeaponAction sets the weapon attack value obtained. We do not reset the player’s money, because it does not really matter here. Since it is the last action in the game, we also set the stop condition and assign the score to the player.
-
C++
-
C#
class ChooseDieAction : public grail::simgames::ISimulatedGameAction
{
DiePlayer* ChosenDie; //we store it, in order to provide the next player to make an action in the apply method
public:
ChooseDieAction(DiePlayer* diePlayer);
grail::simgames::ISimulatedGameUnit* Apply(grail::simgames::ISimulatedGameUnit& currentUnit, grail::simgames::SimulatedGameRuntime& runtimeControl) const override;
std::string ToString() const override;
virtual ~ChooseDieAction() override;
};
ChooseDieAction::ChooseDieAction(DiePlayer* diePlayer) :
ChosenDie{ std::move(diePlayer) } { }
grail::simgames::ISimulatedGameUnit* ChooseDieAction::Apply(grail::simgames::ISimulatedGameUnit& , grail::simgames::SimulatedGameRuntime& ) const
{
return ChosenDie; //next player to act (it represents a die with random actions)
}
std::string ChooseDieAction::ToString() const
{
return "ChosenDieAction: " + ChosenDie->ToString();
}
ChooseDieAction::~ChooseDieAction() { }
class BuyWeaponAction : public grail::simgames::ISimulatedGameAction
{
public:
int WeaponAttackValue;
BuyWeaponAction(int weaponAttackvalue);
grail::simgames::ISimulatedGameUnit* Apply(grail::simgames::ISimulatedGameUnit& currentUnit, grail::simgames::SimulatedGameRuntime& runtimeControl) const override;
std::string ToString() const override;
virtual ~BuyWeaponAction() override;
};
BuyWeaponAction::BuyWeaponAction(int weaponAttackvalue) :
WeaponAttackValue ( weaponAttackvalue ) { }
grail::simgames::ISimulatedGameUnit* BuyWeaponAction::Apply(grail::simgames::ISimulatedGameUnit& currentUnit, grail::simgames::SimulatedGameRuntime& runtimeControl) const
{
runtimeControl.SetTerminationRequest(); //it's the last action in the game
Player& player = static_cast<Player&>(currentUnit);
player.weaponAttackValue = WeaponAttackValue;
runtimeControl.SetScore(0, static_cast<float>(player.weaponAttackValue));
return nullptr;
}
std::string BuyWeaponAction::ToString() const
{
return "Buy weapon with value = " + std::to_string(WeaponAttackValue);
}
BuyWeaponAction::~BuyWeaponAction()
{
}
public class ChooseDieAction : ISimulatedGameAction
{
public DiePlayer ChosenDie { get; private set; } //we store it, in order to provide the next player to make an action in the Apply method
public ChooseDieAction(DiePlayer die)
{
this.ChosenDie = die;
}
public ISimulatedGameUnit Apply(in ISimulatedGameUnit currentUnit, in SimulatedGameRuntime runtimeControl)
{
return ChosenDie; //next player to act (it represents a die with random actions)
}
public override string ToString() => $"Choose [{ChosenDie}]";
}
public class BuyWeaponAction : ISimulatedGameAction
{
public int WeaponAttackValue { get; private set; }
public BuyWeaponAction(int weaponAttackValue)
{
this.WeaponAttackValue = weaponAttackValue;
}
public ISimulatedGameUnit Apply(in ISimulatedGameUnit currentUnit, in SimulatedGameRuntime runtimeControl)
{
runtimeControl.TerminationRequest = true; //it's the last action in the game
Player player = currentUnit as Player;
player.WeaponAttackValue = WeaponAttackValue;
runtimeControl.Scores[0] = player.WeaponAttackValue;
return null;
}
public override string ToString() => $"Buy weapon [{WeaponAttackValue}]";
}
Next, we take a look at actions of the stochastic player.
Each instance corresponds to a particular number rolled.
As effect, the action sets the amount of money equal to the number rolled and sets the proper state of the player.
-
C++
-
C#
class RollAction : public grail::simgames::ISimulatedGameAction
{
public:
int dieValue;
RollAction(int value);
grail::simgames::ISimulatedGameUnit* Apply(grail::simgames::ISimulatedGameUnit & currentUnit, grail::simgames::SimulatedGameRuntime & runtimeControl) const override;
std::string ToString() const override;
virtual ~RollAction() override;
};
RollAction::RollAction(int value)
{
this->dieValue = value;
}
grail::simgames::ISimulatedGameUnit* RollAction::Apply(grail::simgames::ISimulatedGameUnit& currentUnit, grail::simgames::SimulatedGameRuntime& ) const
{
auto* player = static_cast<DiePlayer&>(currentUnit).PreviousPlayer;
player->money = dieValue;
player->state = PlayerState::CHOOSE_WEAPON;
return player;
}
RollAction::~RollAction() { }
std::string RollAction::ToString() const
{
return "Action roll = " + std::to_string(dieValue);
}
public class RollAction : ISimulatedGameAction
{
public int DieValue { get; private set; }
public RollAction(int value)
{
this.DieValue = value;
}
public ISimulatedGameUnit Apply(in ISimulatedGameUnit currentUnit, in SimulatedGameRuntime runtimeControl)
{
Player player = (currentUnit as DiePlayer).PreviousPlayer;
player.Money = DieValue;
player.State = PlayerState.CHOOSE_WEAPON;
return player;
}
public override string ToString() => $"Roll [{DieValue}]";
}
The DiePlayer inherits from SimulatedGameStochasticUnit. This is important to ensure that whenever the MCTS algorithm chooses action for this unit, it will be random.
If we used SimulatedGameThinkingUnit instead, the algorithm would be choosing the best roll 6
method more and more often.
-
C++
-
C#
class DiePlayer : public grail::simgames::SimulatedGameStochasticUnit
{
int sideCount;
public:
Player* previousPlayer;
DiePlayer(int sideCount, Player* prevPlayer);
virtual void Reset() override;
virtual std::vector<std::unique_ptr<const grail::simgames::ISimulatedGameAction>> GetAvailableActions() const override;
std::string ToString() const;
int GetSideCount() const;
};
DiePlayer::DiePlayer(int sideCount, Player* prevPlayer) :
sideCount{ sideCount },
PreviousPlayer{ prevPlayer }
{
}
//DiePlayer is never modified
void DiePlayer::Reset() { }
int DiePlayer::GetSideCount() const
{
return sideCount;
}
std::vector<std::unique_ptr<const grail::simgames::ISimulatedGameAction>> DiePlayer::GetAvailableActions() const
{
std::vector<std::unique_ptr<const grail::simgames::ISimulatedGameAction>> actions;
for(int i=0; i < sideCount;++i)
actions.emplace_back(std::make_unique<const RollAction>(i + 1));
return actions;
}
std::string DiePlayer::ToString() const
{
return "DiePlayer with " + std::to_string(sideCount) + "-sided die.";
}
public class DiePlayer : SimulatedGameStochasticUnit
{
private RollAction[] RollActions;
public Player PreviousPlayer { get; set; }
public DiePlayer(int sideCount)
{
RollActions = new RollAction[sideCount];
for (int i = 0; i < sideCount; ++i)
RollActions[i] = new RollAction(i + 1);
}
public override List<ISimulatedGameAction> GetAvailableActions()
{
return new List<ISimulatedGameAction>(RollActions);
}
//the default implementation would do the same but with creating a temporary list with all actions.
public override ISimulatedGameAction GetRandomAvailableAction(Random random)
{
return RollActions[GetRandomActionIndex(RollActions, random)];
}
public override string ToString() => $"DiePlayer with {RollActions.Length} actions";
//DiePlayer is never modified
public override void Reset() { }
}
Finally, this is an exemplar implementation of the player.
Please note, that we have to reset its state properly (money, state, weaponAttackValue) because it is changed during simulation of the game.
We did not have to do this for DiePlayer because we do not modify it.
-
C++
-
C#
class Player : public grail::simgames::SimulatedGameThinkingUnit
{
std::vector<std::unique_ptr<DiePlayer>> diePlayers;
public:
PlayerState state;
int money = 0;
int weaponAttackValue = 0;
Player();
virtual void Reset() override;
virtual int GetTeamIndex() const override;
virtual std::vector<std::unique_ptr<const grail::simgames::ISimulatedGameAction>> GetAvailableActions() const override;
std::string ToString() const;
DiePlayer* GetDiePlayer(int count) const;
};
Player::Player()
{
diePlayers.push_back(std::make_unique<DiePlayer>(2, this));
diePlayers.push_back(std::make_unique<DiePlayer>(4, this));
diePlayers.push_back(std::make_unique<DiePlayer>(6, this));
}
void Player::Reset()
{
weaponAttackValue = 0;
money = 0;
state = PlayerState::CHOOSE_DIE;
}
int Player::GetTeamIndex() const
{
return 0;
}
std::vector<std::unique_ptr<const grail::simgames::ISimulatedGameAction>> Player::GetAvailableActions() const
{
std::vector<std::unique_ptr<const grail::simgames::ISimulatdGameAction>> actions;
if (state == PlayerState::CHOOSE_DIE)
{
std::vector<std::unique_ptr<const grail::simgames::ISimulatedGameAction>> chooseDieActions;
for(size_t i=0; i < diePlayers.size();++i)
chooseDieActions.emplace_back(std::make_unique<ChooseDieAction>(diePlayers[i].get()));
return chooseDieActions;
}
else
if (state == PlayerState::CHOOSE_WEAPON)
{
for (int i = 1; i <= money; ++i)
actions.emplace_back(std::make_unique<const BuyWeaponAction>(3 * i));
return actions;
}
return actions;
}
std::string Player::ToString() const
{
return "Player. Money: " + std::to_string(money) + " WeaponValue: " + std::to_string(weaponAttackValue);
}
DiePlayer* Player::GetDiePlayer(int count) const
{
return diePlayers[count].get();
}
public class Player : SimulatedGameThinkingUnit
{
private List<ChooseDieAction> ChooseDieActions = new List<ChooseDieAction>();
private List<BuyWeaponAction> BuyWeaponAction = new List<BuyWeaponAction>();
public PlayerState State { get; set; }
public int Money { get; set; } = 0;
public int WeaponAttackValue = 0;
public override uint TeamIndex => 0;
public Player()
{
ChooseDieActions.Add(new ChooseDieAction(new DiePlayer(2) { PreviousPlayer = this }));
ChooseDieActions.Add(new ChooseDieAction(new DiePlayer(4) { PreviousPlayer = this }));
ChooseDieActions.Add(new ChooseDieAction(new DiePlayer(6) { PreviousPlayer = this }));
}
public override void Reset()
{
WeaponAttackValue = 0;
Money = 0;
State = PlayerState.CHOOSE_DIE;
}
public override List<ISimulatedGameAction> GetAvailableActions()
{
if (State == PlayerState.CHOOSE_DIE)
return new List<ISimulatedGameAction>(ChooseDieActions);
else if (State == PlayerState.CHOOSE_WEAPON)
{
List<ISimulatedGameAction> actions = new List<ISimulatedGameAction>();
for (int i = 1; i <= Money; ++i)
actions.Add(new BuyWeaponAction(3 * i));
return actions;
}
throw new InvalidOperationException();
}
public override string ToString() => $"Player";
}
The following code shows how to instantiate and run the game.
-
C++
-
C#
grail::simgames:SimulatedGame game(1, 18.0,10.0); //teamCount, maxScore, explorationBoost
auto player = std::make_unique<Player>();
game.addUnit(std::move(player));
game.Run(15000, 7000); //miliseconds, iterationCount);
std::cout << std::endl << "--- Actions with metadata:" << std::endl;
auto actionsMetadata = game.getStartingUnitActionsWithMetadata();
for (auto& action : actionsMetadata)
std::cout << action.toString() << std::endl;
std::cout << std::endl << "--- Expected playout:" << std::endl;
auto playoutPlan = game.GetExpectedPlayout();
game.DebugPrintFullPlayout(playoutPlan);
SimulatedGame game = new SimulatedGame(1, 18, 10); //teamCount, maxScore, explorationBoost
Player player = new Player();
game.AddUnit(player);
game.Run(15000, 7000); //miliseconds, iterationCount);
var dict = game.GetStartingUnitActionsMetadata();
foreach (var entry in dict)
Console.WriteLine(entry.Value.ToString());
Console.WriteLine("Plan:");
var plan = game.GetExpectedPlans()[player];
foreach (var entry in plan)
Console.WriteLine(entry);