Skip to content

Commit

Permalink
Merge #196
Browse files Browse the repository at this point in the history
196: Add capability to goto player option r=charleskawczynski a=charleskawczynski

This is nearly finished, the only quark is that `initial_∑brs` is stateful inside `play!`, so recomputing it from a preset game ends up corrupting the initial bank rolls. I.e., this needs to be handled more carefully. Re-sampling the games from the river does show different rewards for different options against the Bot5050 🎉

Maybe we can bikeshed names a bit.

This is the last thing, AFAICT, needed to do search for training bots.

Co-authored-by: Charles Kawczynski <kawczynski.charles@gmail.com>
  • Loading branch information
bors[bot] and charleskawczynski committed Aug 10, 2023
2 parents 505a6f3 + 15cb504 commit f6b6ed4
Show file tree
Hide file tree
Showing 8 changed files with 233 additions and 74 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "TexasHoldem"
uuid = "6cef90fc-eb55-4a2a-97d0-7ecce2b738fe"
authors = ["Charles Kawczynski <kawczynski.charles@gmail.com>"]
version = "0.3.1"
version = "0.3.2"

[deps]
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
Expand Down
1 change: 1 addition & 0 deletions src/TexasHoldem.jl
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ struct Flop <: AbstractRound end
struct Turn <: AbstractRound end
struct River <: AbstractRound end

include("goto_player_option.jl")
include("player_type.jl")
include("players.jl")
include("transactions.jl")
Expand Down
198 changes: 134 additions & 64 deletions src/game.jl
Original file line number Diff line number Diff line change
Expand Up @@ -120,116 +120,184 @@ function end_preflop_actions(table::Table, player::Player, ::PreFlop)
return all((cond1, cond2, cond3))
end

function act_generic!(game::Game, round::AbstractRound)
skip_pre_option(sf::StartFrom{StartOfGame}, _) = false
skip_option(sf::StartFrom{StartOfGame}, _) = false
skip_post_option(sf::StartFrom{StartOfGame}, _) = false

skip_pre_option(sf::StartFrom{GP}, player) where {GP <: PlayerOption} =
true
skip_option(sf::StartFrom{GP}, player) where {GP <: PlayerOption} =
seat_number(sf.game_point.player) seat_number(player)
skip_post_option(sf::StartFrom{GP}, player) where {GP <: PlayerOption} =
seat_number(sf.game_point.player) seat_number(player)

function act_generic!(game::Game, round::AbstractRound, sf::StartFrom)
table = game.table
players = table.players
logger = table.logger
table.winners.declared && return
set_round!(table, round)
print_round(table, round)
reset_round_bank_rolls!(game, round)

any_actions_required(game) || return
play_out_game(table) && return
set_play_out_game!(table)
@assert sf.game_point isa StartOfGame || sf.game_point isa PlayerOption
if sf.game_point isa StartOfGame
set_round!(table, round)
print_round(table, round)
reset_round_bank_rolls!(game, round)

any_actions_required(game) || return
play_out_game(table) && return
set_play_out_game!(table)
end
reached_game_point = false # to support when StartFrom is not StartOfGame
# need a bool so that we pick the recreated
# action (in `sf.game_point.action`) once,
# and then continue with `player_option`.
past_game_point = false
for (i, sn) in enumerate(circle(table, FirstToAct()))
player = players[sn]
@cdebug logger "Checking to see if it's $(name(player))'s turn to act"
@cdebug logger " not_playing(player) = $(not_playing(player))"
@cdebug logger " all_in(player) = $(all_in(player))"
not_playing(player) && continue # skip players not playing
set_preflop_blind_raise!(table, player, round, i)
if end_of_actions(table, player)
break
if reached_game_point || !skip_pre_option(sf, player)
@cdebug logger "Checking to see if it's $(name(player))'s turn to act"
@cdebug logger " not_playing(player) = $(not_playing(player))"
@cdebug logger " all_in(player) = $(all_in(player))"
not_playing(player) && continue # skip players not playing
set_preflop_blind_raise!(table, player, round, i)
if end_of_actions(table, player)
break
end
all_in(player) && continue
@cdebug logger "$(name(player))'s turn to act"
end
if reached_game_point || !skip_option(sf, player)
reached_game_point = true
if sf.game_point isa PlayerOption && !past_game_point
action = sf.game_point.action
past_game_point = true
else
action = player_option(game, player)
end
end
if reached_game_point || !skip_post_option(sf, player)
update_given_valid_action!(table, player, action)
table.winners.declared && break
end_preflop_actions(table, player, round) && break
end
all_in(player) && continue
@cdebug logger "$(name(player))'s turn to act"
player_option(game, player)
table.winners.declared && break
end_preflop_actions(table, player, round) && break
if i > n_max_actions(table)
error("Too many actions have occured, please open an issue.")
error("Too many actions have occurred, please open an issue.")
end
end
@cinfo logger "Betting is finished."
@assert all_bets_were_called(table)
end

function act!(game::Game, round::AbstractRound)
act_generic!(game, round)
skip_round(round::AbstractRound, sf::StartFrom) = skip_round(round, sf.game_point)
skip_round(round::AbstractRound, gp::StartOfGame) = false
skip_round(round::AbstractRound, po::PlayerOption) = skip_round(round, po.round)
skip_round(round::AbstractRound, ::AbstractRound) = false

# Skip to Preflop
# Nothing needed-- we automatically skip to this if `StartFrom` is not StartOfGame

# Skip to Flop
skip_round(round::PreFlop, ::Flop) = true

# Skip to Turn
skip_round(round::PreFlop, ::Turn) = true
skip_round(round::Flop, ::Turn) = true

# Skip to River
skip_round(round::PreFlop, ::River) = true
skip_round(round::Flop, ::River) = true
skip_round(round::Turn, ::River) = true

function act!(game::Game, round::AbstractRound, sf::StartFrom)
skip_round(round, sf) && return nothing
act_generic!(game, round, sf)
reset_round!(game.table)
end

metafmt(level, _module, group, id, file, line) =
Logging.default_metafmt(level, nothing, group, id, nothing, nothing)

"""
play!(game::Game)
play!(game::Game[, sf::StartFrom])
Play a game.
Optionally, users can pass in a `StartFrom`
option, to start from a game-point, specified
by `sf`.
"""
play!(game::Game) = deal_and_play!(game::Game)
play!(game::Game, sf::StartFrom = StartFrom(StartOfGame())) =
deal_and_play!(game::Game, sf)

"""
deal_and_play!(game::Game)
deal_and_play!(game::Game[, sf::StartFrom])
Deal and play a game.
Optionally, users can pass in a `StartFrom`
option, to start from a game-point, specified
by `sf`.
"""
function deal_and_play!(game::Game)
function deal_and_play!(game::Game, sf::StartFrom = StartFrom(StartOfGame()))
if game.table.logger isa DebugLogger
cl = Logging.ConsoleLogger(stderr,Logging.Debug; meta_formatter=metafmt)
Logging.with_logger(cl) do
_deal_and_play!(game)
_deal_and_play!(game, sf)
end
else
_deal_and_play!(game)
_deal_and_play!(game, sf)
end
end

function _deal_and_play!(game::Game)
function _deal_and_play!(game::Game, sf::StartFrom)
logger = game.table.logger
@cinfo logger "------ Playing game!"

table = game.table
set_active_status!(table)
winners = table.winners
players = players_at_table(table)

local initial_brs
if sf.game_point isa PlayerOption
# Cannot (or, should not) play from a point
# at which a winner has been declared
@assert !winners.declared
end

@cdebug logger begin
initial_brs = deepcopy(bank_roll.(players))
end
initial_∑brs = sum(x->bank_roll(x), players)

@cinfo logger "Initial bank roll summary: $(bank_roll.(players))"
did = dealer_pidx(table)
sb = seat_number(small_blind(table))
bb = seat_number(big_blind(table))
f2a = seat_number(first_to_act(table))
@cinfo logger "Buttons (dealer, small, big, 1ˢᵗToAct): ($did, $sb, $bb, $f2a)"

@assert still_playing(dealer(table)) "The button must be placed on a non-folded player"
@assert still_playing(small_blind(table)) "The small blind button must be placed on a non-folded player"
@assert still_playing(big_blind(table)) "The big blind button must be placed on a non-folded player"
@assert still_playing(first_to_act(table)) "The first-to-act button must be placed on a non-folded player"
if sf.game_point isa StartOfGame
@cinfo logger "------ Playing game!"
set_active_status!(table)
initial_∑brs = sum(x->bank_roll(x), players)

@cinfo logger "Initial bank roll summary: $(bank_roll.(players))"

did = dealer_pidx(table)
sb = seat_number(small_blind(table))
bb = seat_number(big_blind(table))
f2a = seat_number(first_to_act(table))
@cinfo logger "Buttons (dealer, small, big, 1ˢᵗToAct): ($did, $sb, $bb, $f2a)"
@assert still_playing(dealer(table)) "The button must be placed on a non-folded player"
@assert still_playing(small_blind(table)) "The small blind button must be placed on a non-folded player"
@assert still_playing(big_blind(table)) "The big blind button must be placed on a non-folded player"
@assert still_playing(first_to_act(table)) "The first-to-act button must be placed on a non-folded player"
end

@assert dealer_pidx(table) small_blind_pidx(table) "The button and small blind cannot coincide"
@assert small_blind_pidx(table) big_blind_pidx(table) "The small and big blinds cannot coincide"
@assert big_blind_pidx(table) first_to_act_pidx(table) "The big blind and first to act cannot coincide"

reset!(table.transactions, players)

@assert all(p->cards(p) == nothing, players)
@assert cards(table) == nothing
reset_round_bank_rolls!(table) # round bank-rolls must account for blinds
deal!(table, blinds(table))
@assert cards(table) nothing

winners = table.winners
if sf.game_point isa StartOfGame
reset!(table.transactions, players)
@assert all(p->cards(p) == nothing, players)
@assert cards(table) == nothing
reset_round_bank_rolls!(table) # round bank-rolls must account for blinds
deal!(table, blinds(table))
@assert cards(table) nothing
end

winners.declared || act!(game, PreFlop()) # Pre-flop bet/check/raise
winners.declared || act!(game, Flop()) # Deal flop , then bet/check/raise
winners.declared || act!(game, Turn()) # Deal turn , then bet/check/raise
winners.declared || act!(game, River()) # Deal river, then bet/check/raise
winners.declared || act!(game, PreFlop(), sf) # Pre-flop bet/check/raise
winners.declared || act!(game, Flop(), sf) # Deal flop , then bet/check/raise
winners.declared || act!(game, Turn(), sf) # Deal turn , then bet/check/raise
winners.declared || act!(game, River(), sf) # Deal river, then bet/check/raise

distribute_winnings!(players, table.transactions, cards(table), logger)
winners.declared = true
Expand All @@ -240,12 +308,14 @@ function _deal_and_play!(game::Game)
@cdebug logger "initial_brs = $(initial_brs)"
@cdebug logger "bank_roll.(players_at_table(table)) = $(bank_roll.(players_at_table(table)))"

if !(logger isa ByPassLogger)
if !(initial_∑brs == sum(x->bank_roll(x), players_at_table(table)))
@cinfo logger "initial_∑brs=$initial_∑brs, brs=$(bank_roll.(players_at_table(table)))"
if sf.game_point isa StartOfGame
if !(logger isa ByPassLogger)
if !(initial_∑brs == sum(x->bank_roll(x), players_at_table(table)))
@cinfo logger "initial_∑brs=$initial_∑brs, brs=$(bank_roll.(players_at_table(table)))"
end
end
@assert initial_∑brs == sum(x->bank_roll(x), players_at_table(table)) # eventual assertion
end
@assert initial_∑brs == sum(x->bank_roll(x), players_at_table(table)) # eventual assertion
@assert sum(sp->amount(sp), table.transactions.side_pots) == 0

@cinfo logger "Final bank roll summary: $(bank_roll.(players))"
Expand Down
12 changes: 12 additions & 0 deletions src/goto_player_option.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
abstract type AbstractGamePoint end
struct StartOfGame <: AbstractGamePoint end
struct PlayerOption{P, R, A} <: AbstractGamePoint
player::P
round::R
action::A
end

struct StartFrom{GP}
game_point::GP
end

2 changes: 1 addition & 1 deletion src/player_options.jl
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ function player_option(game::Game, player::Player)
action = player_option(game, player, CheckRaiseFold())::Action
validate_action(action, CheckRaiseFold())
end
update_given_valid_action!(table, player, action)
return action
end

# By default, forward to `player_option` with
Expand Down
27 changes: 19 additions & 8 deletions src/transactions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -220,18 +220,29 @@ Base.@propagate_inbounds function sidepot_winnings(tm::TransactionManager, id::I
mapreduce(i->tm.side_pots[i].amt, +, 1:id; init=0)
end

function profit(player, tm)
if not_playing(player)
return -pot_investment(player)
else
n = length(tm.side_pots)
∑spw = sidepot_winnings(tm, n)
return ∑spw-pot_investment(player)
end
end

function distribute_winnings_1_player_left!(players, tm::TransactionManager, table_cards, logger)
@assert count(x->still_playing(x), players) == 1
n = length(tm.side_pots)
for (player, initial_br) in zip(players, tm.initial_brs)
player.game_profit = profit(player, tm)
end
for (player, initial_br) in zip(players, tm.initial_brs)
∑spw = sidepot_winnings(tm, n)
not_playing(player) && continue
amt_contributed = initial_br - bank_roll(player)
∑spw = sidepot_winnings(tm, n)
prof = ∑spw-amt_contributed
player.game_profit = prof
player.bank_roll += ∑spw
if !(∑spw == 0 && player.bank_roll == 0 && amt_contributed == 0)
@cinfo logger "$(name(player)) wins $(∑spw) ($(prof) profit) (all opponents folded)"
@cinfo logger "$(name(player)) wins $(∑spw) ($(profit(player, tm)) profit) (all opponents folded)"
end
@inbounds for j in 1:n
tm.side_pots[j].amt = 0 # empty out distributed winnings
Expand All @@ -255,7 +266,7 @@ end

#=largest pot investment, excluding player=#
function largest_pot_investment(player, players)
lpi = typeof(pot_investment(player))(0)
lpi = 0
for i in 1:length(players)
seat_number(players[i]) == seat_number(player) && continue
lpi = max(lpi, pot_investment(players[i]))
Expand Down Expand Up @@ -368,6 +379,9 @@ function distribute_winnings!(players, tm::TransactionManager, table_cards, logg
for (player, initial_br, player_winnings) in zip(players, tm.initial_brs, tm.side_pot_winnings)
∑spw = sum(player_winnings)
ssn = tm.unsorted_to_sorted_map[seat_number(player)]
amt_contributed = initial_br - bank_roll(player)
prof = profit(player, tm)
player.game_profit = prof
if ∑spw == 0 && active(player)
@cinfo logger begin
she = sorted_hand_evals[ssn]
Expand All @@ -378,8 +392,6 @@ function distribute_winnings!(players, tm::TransactionManager, table_cards, logg
end
else
@cdebug logger "$(name(player))'s side-pot wins: $(player_winnings)!"
amt_contributed = initial_br - bank_roll(player)
prof = ∑spw-amt_contributed
if amt_contributed == 0 && ∑spw == 0 && prof == 0 && !still_playing(player)
continue
end
Expand All @@ -389,7 +401,6 @@ function distribute_winnings!(players, tm::TransactionManager, table_cards, logg
bc = she.best_cards
"$(name(player)) wins $∑spw ($prof profit) with $bc ($hand_name)!"
end
player.game_profit = prof
player.bank_roll += ∑spw
end
end
Expand Down
Loading

2 comments on commit f6b6ed4

@charleskawczynski
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/89370

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.3.2 -m "<description of version>" f6b6ed4356d101416738afe50627fd0cfc33d9a1
git push origin v0.3.2

Please sign in to comment.