Simulated Games: how to define a game with randomness

This page presents a fully working example of a very simple SimulatedGame that involve randomness.

The game is defined as follows:

  • First, the player chooses a die among 2-sided, 4-sided an 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 costs X and has 3*X attack value

  • 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 ChoseDieAction 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 score to the player.

  • C++

  • C#

class ChooseDieAction : public 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);
  ISimulatedGameUnit* Apply(ISimulatedGameUnit& currentUnit, SimulatedGameRuntime& runtimeControl) const override;
  std::string ToString() const override;
  virtual ~ChooseDieAction() override;
};

ChooseDieAction::ChooseDieAction(DiePlayer* diePlayer) :
  ChosenDie{ std::move(diePlayer) } { }

ISimulatedGameUnit* ChooseDieAction::Apply(ISimulatedGameUnit& , 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 ISimulatedGameAction
{
public:
  int WeaponAttackValue;

  BuyWeaponAction(int weaponAttackvalue);
  ISimulatedGameUnit* Apply(ISimulatedGameUnit& currentUnit, SimulatedGameRuntime& runtimeControl) const override;
  std::string ToString() const override;
  virtual ~BuyWeaponAction() override;

};

BuyWeaponAction::BuyWeaponAction(int weaponAttackvalue) :
  WeaponAttackValue ( weaponAttackvalue ) { }

ISimulatedGameUnit* BuyWeaponAction::Apply(ISimulatedGameUnit& currentUnit, 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 player.

  • C++

  • C#

class RollAction : public ISimulatedGameAction
{
public:
  int dieValue;

  RollAction(int value);
  IAbstractGameUnit* Apply(IAbstractGameUnit & currentUnit, AbstractGameRuntime & runtimeControl) const override;
  std::string ToString() const override;
  virtual ~RollAction() override;
};

RollAction::RollAction(int value)
{
  this->dieValue = value;
}

ISimulatedGameUnit* RollAction::Apply(ISimulatedGameUnit& currentUnit, 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 will be choosing the best roll 6 method more and more often.

  • C++

  • C#

class DiePlayer : public SimulatedGameStochasticUnit
{
  int sideCount;
public:
  Player* previousPlayer;

  DiePlayer(int sideCount, Player* prevPlayer);
  virtual void Reset() override;
  virtual std::vector<std::unique_ptr<const 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 ISimulatedGameAction>> DiePlayer::GetAvailableActions() const
{
  std::vector<std::unique_ptr<const 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 it 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 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 IAbstractGameAction>> 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 ISimulatedGameAction>> Player::GetAvailableActions() const
{
  std::vector<std::unique_ptr<const ISimulatdGameAction>> actions;
  if (state == PlayerState::CHOOSE_DIE)
  {
    std::vector<std::unique_ptr<const 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#

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);