Building and Running Bots

Python Setup

Our game engine is run in Python 3.

  1. Check that you have at least Python 3.12 installed with:
python3 --version
  1. The game engine uses the eval7 package, which is a Python Texas Hold’em hand evaluation library:
pip3 install eval7

Poker Camp Game Engine

7/18 Note: The game engine is under development and you might see changes, especially over the first couple of weeks of the AIPCS24.

Engine

The engine is in engine.py. You can use python3 engine.py to test two agents playing against each other.

To run a 100 hand match with two bots that are named p1 and p2 and run the logic from players/random/ folder and output results to the p1p2test folder, do this:

python3 engine.py -p1 'p1' players/random/ -p2 'p2' players/random/ -o p1p2test -n 100

The generic usage is:

python3 engine.py -p1 {p1_name} {p1_file_path} -p2 {p2_name} {p2_file_path} -o {output_dir} -n {n_hands}"

The output files are:

  1. scores.p1.p2.txt contains the raw scores (i.e. profits) of each player

  2. The p1.p2 folder contains:

  • gamelog.txt: A log of all hands played

  • Other log files for each player

Config

The config.py file contains various parameters to control the game engine. You should not need to modify this in normal use.

Build a Bot

The player.py file is where you write your poker bot.

Note that for Kuhn Poker, the cards are assigned as follows:

Card Engine
Q 0
K 1
A 2

There are three preconfigured bots that you can see to get a sense of how they work:

  1. random: Every action is random. In Kuhn this means 50% actions and 50% actions.

  2. linear: For Kuhn, every Q action is , every K action is 50% and 50% , and every A action is 100% .

  3. from-weights: This is how the Kuhn Challenge works. Each infoset is assigned a specific weight and the bot always plays according to those strategy probabilities.

  1. actions.py: The actions a player can take

  2. bot.py: Defines the interface of the player.py functions

  3. runner.py: The infrastructure for interacting with the engine

  4. states.py: Encapsulates game and round state information for the player

Using player.py to Build a Bot

player.py contains 3 functions:

  1. handle_new_round(): Gets called when a new round (i.e. hand) starts

  2. handle_round_over(): Gets called when a new round (i.e. hand) ends

  3. get_action(): The main function to implement, which is called any time the engine needs an action from your bot.

You should write these functions so that get_action() returns the actions that you want in the situations it faces.

The get_action() function

The arguments coming in to get_action() are:

  1. game_state: the GameState object, which is the state of the entire match of hands. This was 100 in the above example. The game state gives:

  2. bankroll: Profits over the match

  3. game_clock: Your time remaining to use during the match

  4. round_num: The round of betting, always 1 in Kuhn Poker

Here’s an example GameState:

game state GameState(bankroll=0, game_clock=29.991, round_num=1)
  1. round_state: the RoundState object, which contains all information about the current hand.

This includes :

  1. turn: The number of actions that have taken place this game. (turn % 2 gives the player who will act next.)

  2. street: Current betting round (in Kuhn Poker, this will always be 0).

  3. pips: How many chips each player has contributed to the pot on the current hand.

  4. stacks: How many chips each player has left (not contributed to the pot).

  5. hands: List of known hands to you, with None for unknown hands.

  6. deck: This won’t be known to you, so it will probably always be None.

  7. action_history: History of actions, a list of UpAction() or DownAction(). (The type of this will definitely change as we work on it.)

  8. previous_state: The previous state of the hand, as a RoundState.

Here’s an example RoundState:

RoundState(
    turn=1,
    street=0,
    pips=[1, 1],
    stacks=[1, 1],
    hands=[None, 1],
    deck=None,
    action_history=[DownAction()],
    previous_state=
        RoundState(
            turn=0,
            street=0,
            pips=[1, 1],
            stacks=[1, 1],
            hands=[None, 1],
            deck=None,
            action_history=[],
            previous_state=None
        )
    )
  1. active: your player’s index

The return is:

  1. Your action

Debugging your broken bot

Because of the way engine.py captures the output of the bots it runs, you probably don’t get the printed output of your broken bot failing. If you comment out the line of stdout=subprocess.PIPE, stderr=subprocess.STDOUT, in this function call, you can (probably?) disable this behavior:

proc = subprocess.Popen(
    self.commands['run'] + [str(port)],
    stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
    cwd=self.path,
)

Sample Bots (for Challenge 1 / Kuhn Poker)

Random

The most simple random agent doesn’t care about the GameState or RoundState and implements the simple action:

return random.choice([UpAction(), DownAction()])

Linear Agent

The linear agent also doesn’t use GameState or RoundState, but does change its actions depending on its own hand. It uses this code to match my_hand to the appropriate linear case.

match my_hand:
    case 0:
        return DownAction()
    case 1:
        return random.choice([UpAction(), DownAction()])
    case 2:
        return UpAction()

Weights (Probabilities) Agent

The from-weights agent does need to use the round_state to first see whose turn it is to act (round_state.turn) and then to match the hand to the appropriate infoset using match my_hand. From there, the strategy can be defined according to the strategy probabilities (weights) for that infoset.

match round_state.turn:
    case 0:
        match my_hand:
            case 0: # Q_
                up_prob = self.strategy["Q_"]
            case 1: # K_
                up_prob = self.strategy["K_"]
            case 2: # A_
                up_prob = self.strategy["A_"]
    case 1:
        match my_hand, round_state.action_history[0]:
            case 0, DownAction(): #_Q↓
                up_prob = self.strategy["_QD"]
...

…and beyond?

The from-weights agent gives you the tools to implement any fixed strategy that you want. If you want to do better than Nash, though, you’ll have to do something that remembers what your opponent has played in previous rounds, and use it to do something differently in the future…

Beta Note

We expect to add a handle_observed_action() function in the bot/runner framework, to make certain kinds of tracking easier. For now, you can do this by adding logging logic to the get_action() and/or handle_round_over().