Skip to content

Commit

Permalink
Merge #203
Browse files Browse the repository at this point in the history
203: Fairly distribute exact deterministic pot remainder r=charleskawczynski a=charleskawczynski

Closes #202.

Co-authored-by: Charles Kawczynski <kawczynski.charles@gmail.com>
  • Loading branch information
bors[bot] and charleskawczynski committed Aug 25, 2023
2 parents f6b6ed4 + 9a8c841 commit 061b919
Show file tree
Hide file tree
Showing 14 changed files with 153 additions and 55 deletions.
10 changes: 9 additions & 1 deletion docs/src/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,20 @@ play!
tournament!
```

## Player type
## Chips

```@docs
Chips
```

## Player type and methods
```@docs
AbstractStrategy
Human
Bot5050
Player
bank_roll
round_bank_roll
```

## Player actions
Expand Down
2 changes: 2 additions & 0 deletions src/TexasHoldem.jl
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ using PokerHandEvaluator
using Printf

export AbstractRound, PreFlop, Flop, Turn, River
export Chips

include("custom_logger.jl")

Expand All @@ -28,6 +29,7 @@ struct Flop <: AbstractRound end
struct Turn <: AbstractRound end
struct River <: AbstractRound end

include("chips.jl")
include("goto_player_option.jl")
include("player_type.jl")
include("players.jl")
Expand Down
47 changes: 47 additions & 0 deletions src/chips.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""
Chips(n, frac)
A stack of chips struct. This type is
backed by `Int`s and performs exact
arithmetic operations by using Rational
numbers to track remainders (fractions
of a chip). `frac` is always internally
stored as less than 1.
Players can only bet/call with whole
chips (`n`), and are not allowed to
bet/call with a fraction of a chip.
We track the fractions of chips so that
we can assert exact money conservation
on the table until a player busts, at which
point, that money is lost.
"""
struct Chips
n::Int
frac::Rational{Int}
function Chips(n::Int, frac::Rational)
if frac 1
whole = div(frac.num, frac.den)
r = Rational(rem(frac.num, frac.den), frac.den)
return new(n+whole, r)
else
return new(n, frac)
end
end
end

Chips(c::Chips) = c
Chips(n::Int) = Chips(n, Rational(0))

Base.isless(a::Chips, b::Chips) = Base.isless(a.n+a.frac, b.n+b.frac)
Base.isless(a::Int, b::Chips) = Base.isless(a, b.n+b.frac)
Base.isless(a::Chips, b::Int) = Base.isless(a.n+a.frac, b)

Base.:(+)(a::Chips, b::Chips) = Chips(a.n+b.n, a.frac+b.frac)
Base.:(-)(a::Chips, b::Chips) = Chips(a.n-b.n, a.frac-b.frac)
Base.:(+)(a::Chips, b::Int) = Chips(a.n+b, a.frac)
Base.:(-)(a::Chips, b::Int) = Chips(a.n-b, a.frac)

Base.zero(::Type{Chips}) = Chips(0)
Base.zero(::Chips) = Chips(0)
12 changes: 6 additions & 6 deletions src/game.jl
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ function _deal_and_play!(game::Game, sf::StartFrom)
if sf.game_point isa StartOfGame
@cinfo logger "------ Playing game!"
set_active_status!(table)
initial_∑brs = sum(x->bank_roll(x), players)
initial_∑brs = sum(x->bank_roll_chips(x), players;init=Chips(0))

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

Expand Down Expand Up @@ -310,11 +310,11 @@ function _deal_and_play!(game::Game, sf::StartFrom)

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)))"
if !(initial_∑brs == sum(x->bank_roll_chips(x), players_at_table(table); init=Chips(0)))
@cinfo logger "initial_∑brs=$initial_∑brs, brs=$(bank_roll_chips.(players_at_table(table)))"
end
end
@assert initial_∑brs == sum(x->bank_roll(x), players_at_table(table)) # eventual assertion
@assert initial_∑brs == sum(x->bank_roll_chips(x), players_at_table(table);init=Chips(0)) # eventual assertion
end
@assert sum(sp->amount(sp), table.transactions.side_pots) == 0

Expand Down Expand Up @@ -359,9 +359,9 @@ function reset_game!(game::Game)
for player in players
player.cards = nothing
player.pot_investment = 0
player.game_profit = 0
player.game_profit = Chips(0)
player.all_in = false
player.round_bank_roll = bank_roll(player)
player.round_bank_roll = bank_roll_chips(player)
player.checked = false
player.last_to_raise = false
player.round_contribution = 0
Expand Down
40 changes: 32 additions & 8 deletions src/player_type.jl
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,11 @@ mutable struct Player{S #=<: AbstractStrategy=#}
strategy::S
seat_number::Int
cards::Union{Nothing,Tuple{<:Card,<:Card}}
bank_roll::Int
game_profit::Int
bank_roll::Chips
game_profit::Chips
action_required::Bool
all_in::Bool
round_bank_roll::Int # bank roll at the beginning of the round
round_bank_roll::Chips # bank roll at the beginning of the round
folded::Bool
pot_investment::Int # accumulation of round_contribution
checked::Bool
Expand Down Expand Up @@ -86,11 +86,11 @@ function Player(strategy, seat_number, cards = nothing; bank_roll = 200)
strategy,
seat_number,
cards,
bank_roll,
game_profit,
Chips(bank_roll),
Chips(game_profit),
action_required,
all_in,
round_bank_roll,
Chips(round_bank_roll),
folded,
pot_investment,
checked,
Expand All @@ -104,7 +104,32 @@ end
name(s::AbstractStrategy) = nameof(typeof(s))
name(player::Player) = "$(name(strategy(player)))[$(seat_number(player))]"
cards(player::Player) = player.cards
bank_roll(player::Player) = player.bank_roll

"""
bank_roll(::Player)
The player's instantaneous
bank roll.
We access the `Int` in Chips
as the fractional chips are only
handled by the TransactionManager.
"""
bank_roll(player::Player) = player.bank_roll.n
bank_roll_chips(player::Player) = player.bank_roll

"""
round_bank_roll(::Player)
The player's bank roll at the
beginning of the round
We access the `Int` in Chips
as the fractional chips are only
handled by the TransactionManager.
"""
round_bank_roll(player::Player) = player.round_bank_roll.n

seat_number(player::Player) = player.seat_number
folded(player::Player) = player.folded
zero_bank_roll(player::Player) = bank_roll(player) == 0
Expand All @@ -116,7 +141,6 @@ all_in(player::Player) = player.all_in
action_required(player::Player) = player.action_required
active(player::Player) = player.active
inactive(player::Player) = !active(player)
round_bank_roll(player::Player) = player.round_bank_roll
pot_investment(player::Player) = player.pot_investment
round_contribution(player::Player) = player.round_contribution
strategy(player::Player) = player.strategy
Expand Down
2 changes: 1 addition & 1 deletion src/table.jl
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ end
function reset_round_bank_rolls!(table::Table)
players = players_at_table(table)
for player in players
player.round_bank_roll = bank_roll(player)
player.round_bank_roll = bank_roll_chips(player)
end
end

Expand Down
61 changes: 33 additions & 28 deletions src/transactions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ among multiple players.
"""
struct TransactionManager
perm::Vector{Int}
bank_rolls::Vector{Int} # cache
initial_brs::Vector{Int}
bank_rolls::Vector{Chips} # cache
initial_brs::Vector{Chips}
pot_id::Vector{Int}
side_pots::Vector{SidePot}
side_pot_winnings::Vector{Vector{Int}}
side_pot_winnings::Vector{Vector{Chips}}
unsorted_to_sorted_map::Vector{Int}
sorted_hand_evals::Vector{HandEval}
end
Expand All @@ -53,7 +53,7 @@ end
TransactionManager(players) = TransactionManager(Players(players))
function TransactionManager(players::Players)
perm = collect(sortperm(players))
bank_rolls = collect(map(x->bank_roll(x), players))
bank_rolls = collect(map(x->bank_roll_chips(x), players))

cap = zeros(Int,length(players))
for i in 1:length(players)
Expand All @@ -69,7 +69,7 @@ function TransactionManager(players::Players)
end)
side_pots = [SidePot(seat_number(players[p]), 0, cap_i) for (cap_i, p) in zip(cap, perm)]

initial_brs = deepcopy(collect(bank_roll.(players)))
initial_brs = deepcopy(collect(bank_roll_chips.(players)))
sorted_hand_evals = map(x->HandEval(), 1:length(players))
pot_id = Int[1]
FT = eltype(initial_brs)
Expand All @@ -89,7 +89,7 @@ end
function reset!(tm::TransactionManager, players::Players)
perm = tm.perm
@inbounds for i in 1:length(players)
tm.bank_rolls[i] = bank_roll(players[i])
tm.bank_rolls[i] = bank_roll_chips(players[i])
end
sortperm!(perm, tm.bank_rolls)
@inbounds for i in 1:length(players)
Expand All @@ -102,11 +102,11 @@ function reset!(tm::TransactionManager, players::Players)
tm.side_pots[i].amt = 0
tm.side_pots[i].cap = cap_i
for k in 1:length(players)
tm.side_pot_winnings[i][k] = 0
tm.side_pot_winnings[i][k] = Chips(0)
end
j = findfirst(p->seat_number(players[p]) == seat_number(player), perm)::Int
tm.unsorted_to_sorted_map[i] = j
tm.initial_brs[i] = bank_roll(player)
tm.initial_brs[i] = bank_roll_chips(player)
end
@inbounds tm.pot_id[1] = 1
return nothing
Expand Down Expand Up @@ -221,12 +221,13 @@ Base.@propagate_inbounds function sidepot_winnings(tm::TransactionManager, id::I
end

function profit(player, tm)
# TODO: this is wrong (but only used for log / game_profit)
if not_playing(player)
return -pot_investment(player)
return Chips(-pot_investment(player))
else
n = length(tm.side_pots)
∑spw = sidepot_winnings(tm, n)
return ∑spw-pot_investment(player)
return Chips(∑spw-pot_investment(player))
end
end

Expand Down Expand Up @@ -310,7 +311,7 @@ function distribute_winnings!(players, tm::TransactionManager, table_cards, logg

@inbounds for i in 1:length(players)
for k in 1:length(players)
tm.side_pot_winnings[i][k] = 0
tm.side_pot_winnings[i][k] = Chips(0)
end
end

Expand Down Expand Up @@ -344,21 +345,21 @@ function distribute_winnings!(players, tm::TransactionManager, table_cards, logg
end

# Compute the least winnings per player:
amt_total = sidepot_winnings(tm, i)
amt_base = div(amt_total, n_winners)
remainder = rem(amt_total, n_winners) # ∈ 1:(amt_base-1)
eligible_pidxs = Int[]
for (ssn, she) in enumerate(sorted_hand_evals)
player = players[perm[ssn]]
if (still_playing(player) && ssn i ? she.hand_rank==mvhr : false) || is_largest_pot_investment(player, players)
push!(eligible_pidxs, perm[ssn])
end
end
# amt_total = sidepot_winnings(tm, i)
# amt_base = div(amt_total, n_winners)
# remainder = rem(amt_total, n_winners) # ∈ 1:(amt_base-1)
# eligible_pidxs = Int[]
# for (ssn, she) in enumerate(sorted_hand_evals)
# player = players[perm[ssn]]
# if (still_playing(player) && ssn ≥ i ? she.hand_rank==mvhr : false) || is_largest_pot_investment(player, players)
# push!(eligible_pidxs, perm[ssn])
# end
# end
# TODO: distribute remaining more uniformly / with less variance
lucky_pidx = isempty(eligible_pidxs) ? -1 : rand(eligible_pidxs)
@cdebug logger "eligible_pidxs = $eligible_pidxs"
@cdebug logger "lucky_pidx = $lucky_pidx"
@cdebug logger "remainder = $remainder"
# lucky_pidx = isempty(eligible_pidxs) ? -1 : rand(eligible_pidxs)
# @cdebug logger "eligible_pidxs = $eligible_pidxs"
# @cdebug logger "lucky_pidx = $lucky_pidx"
# @cdebug logger "remainder = $remainder"

for (ssn, she) in enumerate(sorted_hand_evals)
player = players[perm[ssn]]
Expand All @@ -368,9 +369,13 @@ function distribute_winnings!(players, tm::TransactionManager, table_cards, logg
@cdebug logger "winning pidx = $(tm.perm[winner_id])"
win_seat = seat_number(players[tm.perm[winner_id]])
# we do a coin flip to see who gets the remaining chips:
@cdebug logger "ssn, lucky_pidx: $ssn, $lucky_pidx"
amt = perm[ssn] == lucky_pidx ? amt_base + remainder : amt_base
tm.side_pot_winnings[win_seat][i] = amt
# @cdebug logger "ssn, lucky_pidx: $ssn, $lucky_pidx"
# amt = perm[ssn] == lucky_pidx ? amt_base + remainder : amt_base
amt_total = sidepot_winnings(tm, i)
amt_base = div(amt_total, n_winners)
remainder = rem(amt_total, n_winners) # ∈ 1:(amt_base-1)
amt_chips = Chips(amt_base, Rational(remainder,n_winners))
tm.side_pot_winnings[win_seat][i] = amt_chips
end
for j in 1:i
tm.side_pots[j].amt = 0 # empty out distributed winnings
Expand Down
10 changes: 5 additions & 5 deletions test/call_raise_validation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -116,35 +116,35 @@ end
table.initial_round_raise_amt = 20
table.current_raise_amt = 20
players[1].round_contribution = 200
players[1].round_bank_roll = 500 # oops
players[1].round_bank_roll = Chips(500) # oops
@test TH.is_valid_raise_amount(table, players[1], 200) == (false, "Cannot contribute 0 to the pot.")

players = (Player(Human(), 1), Player(Bot5050(), 2))
table = QuietGame(players).table
table.initial_round_raise_amt = 10
table.current_raise_amt = 10
players[1].round_bank_roll = 20
players[1].round_bank_roll = Chips(20)
@test TH.is_valid_raise_amount(table, players[1], 10) == (false, "Only allowable raise is 20 (all-in)")

players = (Player(Human(), 1), Player(Bot5050(), 2))
table = QuietGame(players).table
table.initial_round_raise_amt = 10
table.current_raise_amt = 10
players[1].round_bank_roll = 20
players[1].round_bank_roll = Chips(20)
@test TH.is_valid_raise_amount(table, players[1], 20) == (true, "")

players = (Player(Human(), 1), Player(Bot5050(), 2))
table = QuietGame(players).table
table.initial_round_raise_amt = 5
table.current_raise_amt = 20
players[1].round_bank_roll = 30
players[1].round_bank_roll = Chips(30)
@test TH.is_valid_raise_amount(table, players[1], 25) == (true, "")

players = (Player(Human(), 1), Player(Bot5050(), 2))
table = QuietGame(players).table
table.initial_round_raise_amt = 5
table.current_raise_amt = 20
players[1].round_bank_roll = 30
players[1].round_bank_roll = Chips(30)
@test TH.is_valid_raise_amount(table, players[1], 22) == (false, "Cannot raise 22. Raise must be between [25, 30]")
end

Expand Down
8 changes: 8 additions & 0 deletions test/chips.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using TexasHoldem
using Test

@testset "Chips" begin
a = Chips(1, 0//1)
b = Chips(2, 3//2)
@test a + b == Chips(4, 1//2)
end
2 changes: 1 addition & 1 deletion test/fuzz_play.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ fuzz_debug(;fun=tournament!,n_players=10,bank_roll=30,n_games=3788)
where `3788` was found from
fuzz(;fun=tournament!,n_players=10,bank_roll=30,n_games=10000)
fuzz(; fun = play!, n_players = 3, bank_roll = 200, n_games = 2373)
fuzz_debug(; fun = tournament!, n_players = 2, bank_roll = 6, n_games = 1)
fuzz_debug(; fun = tournament!, n_players = 3, bank_roll = 6, n_games = 38)
fuzz_debug(; fun = play!, n_players = 3, bank_roll = 200, n_games = 2373)
=#
Expand Down
Loading

0 comments on commit 061b919

Please sign in to comment.