Building and Running Bots
Python Setup
Our game engine is run in Python 3.
- Check that you have at least Python 3.12 installed with:
--version python3
- 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:
-p1 'p1' players/random/ -p2 'p2' players/random/ -o p1p2test -n 100 python3 engine.py
The generic usage is:
-p1 {p1_name} {p1_file_path} -p2 {p2_name} {p2_file_path} -o {output_dir} -n {n_hands}" python3 engine.py
The output files are:
scores.p1.p2.txt
contains the raw scores (i.e. profits) of each playerThe
p1.p2
folder contains:
gamelog.txt
: A log of all hands playedOther 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:
random
: Every action is random. In Kuhn this means 50% ↑ actions and 50% ↓ actions.linear
: For Kuhn, every Q action is ↓, every K action is 50% ↑ and 50% ↓, and every A action is 100% ↑.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.
Using player.py
to Build a Bot
player.py
contains 3 functions:
handle_new_round()
: Gets called when a new round (i.e. hand) startshandle_round_over()
: Gets called when a new round (i.e. hand) endsget_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:
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:bankroll
: Profits over the matchgame_clock
: Your time remaining to use during the matchround_num
: The round of betting, always 1 in Kuhn Poker
Here’s an example GameState
:
=0, game_clock=29.991, round_num=1) game state GameState(bankroll
round_state
: the RoundState object, which contains all information about the current hand.
This includes :
turn
: The number of actions that have taken place this game. (turn % 2
gives the player who will act next.)street
: Current betting round (in Kuhn Poker, this will always be 0).pips
: How many chips each player has contributed to the pot on the current hand.stacks
: How many chips each player has left (not contributed to the pot).hands
: List of known hands to you, withNone
for unknown hands.deck
: This won’t be known to you, so it will probably always beNone
.action_history
: History of actions, a list ofUpAction()
orDownAction()
. (The type of this will definitely change as we work on it.)previous_state
: The previous state of the hand, as aRoundState
.
Here’s an example RoundState
:
RoundState(=1,
turn=0,
street=[1, 1],
pips=[1, 1],
stacks=[None, 1],
hands=None,
deck=[DownAction()],
action_history=
previous_state
RoundState(=0,
turn=0,
street=[1, 1],
pips=[1, 1],
stacks=[None, 1],
hands=None,
deck=[],
action_history=None
previous_state
) )
active
: your player’s index
The return is:
- 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_
= self.strategy["Q_"]
up_prob case 1: # K_
= self.strategy["K_"]
up_prob case 2: # A_
= self.strategy["A_"]
up_prob case 1:
match my_hand, round_state.action_history[0]:
case 0, DownAction(): #_Q↓
= self.strategy["_QD"]
up_prob ...
…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…