From e3f3c368549f5267a129c99eeab63d8cc6486aea Mon Sep 17 00:00:00 2001 From: Charles Kawczynski Date: Sat, 8 May 2021 13:45:04 -0700 Subject: [PATCH] Several logic fixes and improvements - Fix blinds: now everyone must call to see flop - Improve call interface (no more need for amount) - Add func to validate raise amount - Fix raise logic (no double counting) - Fix player option dispatch (added CallFold) - Improve log (include profit in addition to pot amount) - Fix distribute_winnings! when only 1 player remains --- src/game.jl | 26 ++++++++-- src/player_actions.jl | 109 ++++++++++++++++++++++++++++++++++++++---- src/player_options.jl | 74 ++++++++++++++++++---------- src/player_types.jl | 11 +++++ src/table.jl | 19 +++++++- src/transactions.jl | 96 +++++++++++++++++++++++++++---------- test/game.jl | 8 ++-- test/play.jl | 35 +++++++------- test/runtests.jl | 5 +- test/transactions.jl | 66 ++++++++++++------------- 10 files changed, 332 insertions(+), 117 deletions(-) diff --git a/src/game.jl b/src/game.jl index 05ea1da5..9d2aef48 100644 --- a/src/game.jl +++ b/src/game.jl @@ -26,6 +26,8 @@ function Game(players::Tuple; n_player_cards = sum(map(x->cards(x)==nothing ? 0 : length(cards(x)), players)) + @assert 2 ≤ length(players) ≤ 10 + if length(deck) ≠ 52 # if the deck isn't full, then players should have been dealt cards. @assert n_player_cards > 0 @@ -61,14 +63,29 @@ print_new_cards(table, state::Flop) = @info "Flop: $(repeat(" ", 44)) $(table.c print_new_cards(table, state::Turn) = @info "Turn: $(repeat(" ", 44)) $(table.cards[4])" print_new_cards(table, state::River) = @info "River: $(repeat(" ", 43)) $(table.cards[5])" +force_blind_raise!(table::Table, player, ::AbstractGameState, i::Int) = nothing +function force_blind_raise!(table::Table, player::Player, ::PreFlop, i::Int) + if 1 ≤ i ≤ length(players_at_table(table)) + # TODO: what if only two players? + if is_first_to_act(table, player) + # everyone must call big blind to see flop: + table.current_raise_amt = blinds(table).big + end + end +end +reset_round_bank_rolls!(game::Game, state::PreFlop) = nothing # called separately prior to deal +reset_round_bank_rolls!(game::Game, state::AbstractGameState) = reset_round_bank_rolls!(game.table) + function act_generic!(game::Game, state::AbstractGameState) table = game.table - table.winners.declared && return # TODO: is this redundant? + table.winners.declared && return set_state!(game.table, state) print_new_cards(game.table, state) + reset_round_bank_rolls!(game, state) any_actions_required(game) || return - for player in circle(table, FirstToAct()) + for (i, player) in enumerate(circle(table, FirstToAct())) + force_blind_raise!(table, player, state, i) @debug "Checking to see if it's $(name(player))'s turn to act" @debug " all_in.(players_at_table(table)) = $(all_in.(players_at_table(table)))" @debug " last_to_raise.(players_at_table(table)) = $(last_to_raise.(players_at_table(table)))" @@ -76,12 +93,14 @@ function act_generic!(game::Game, state::AbstractGameState) @debug " folded.(players_at_table(table)) = $(folded.(players_at_table(table)))" @debug " action_required.(players_at_table(table)) = $(action_required.(players_at_table(table)))" @debug " !any(action_required.(players_at_table(table))) = $(!any(action_required.(players_at_table(table))))" + @debug " all_all_in_or_folded(table) = $(all_all_in_or_folded(table))" @debug " all_checked_or_folded(table) = $(all_checked_or_folded(table))" last_to_raise(player) && break all_checked_or_folded(table) && break - all_all_in(table) && break + all_all_in_or_folded(table) && break !any(action_required.(players_at_table(table))) && break folded(player) && continue + all_in(player) && continue @debug "$(name(player))'s turn to act" player_option!(game, player) table.winners.declared && break @@ -117,6 +136,7 @@ function play(game::Game) @assert all(cards.(players) .== nothing) @assert cards(table) == nothing + reset_round_bank_rolls!(table) # round bank-rolls must account for blinds deal!(table, blinds(table)) @assert cards(table) ≠ nothing diff --git a/src/player_actions.jl b/src/player_actions.jl index 8cc8470d..83ce49fb 100644 --- a/src/player_actions.jl +++ b/src/player_actions.jl @@ -44,9 +44,20 @@ end ##### Call ##### -call!(game::Game, player::Player, amt) = call!(game.table, player, amt) +call!(game::Game, player::Player) = call!(game.table, player) -function call!(table::Table, player::Player, amt) +function call!(table::Table, player::Player) + cra = table.current_raise_amt + pc = player.round_contribution + call_amt = cra - pc + if call_amt ≤ bank_roll(player) + call_valid_amount!(table, player, call_amt) + else + call_valid_amount!(table, player, bank_roll(player)) + end +end + +function call_valid_amount!(table::Table, player::Player, amt) @debug "$(name(player)) calling $(amt)." push!(player.action_history, Call(amt)) player.action_required = false @@ -58,15 +69,93 @@ end ##### Raise ##### -raise_to!(game::Game, player::Player, amt) = raise_to!(game.table, player, amt) +""" + bound_raise(table::Table, player::Player, amt) + +Given a raise amount `amt`, return a valid raise +amount by ensuring the raise: + - is at least the small blind + - is less than the player's bank roll + - is twice the current raise amount +""" +function bound_raise(table::Table, player::Player, amt) + @debug "Bounding raise amout. Input amount = \$$(amt)" + amt = max(amt, 2*table.current_raise_amt) + amt = min(amt, round_bank_roll(player)) + amt = max(amt, blinds(table).small) + @debug "Bounding raise amout. Output amount = \$$(amt)" + return amt +end # TODO: add assertion that raise amount must be # greater than small blind (unless all-in). -function raise_to!(table::Table, player::Player, amt) +""" + valid_raise_amount(table::Table, player::Player, amt) + +Return back `amt` if `amt` is a valid raise amount. +## Scenario 1 (`2*current_raise_amt > bank_roll(player)`) - only _raise_ option is all-in +## Scenario 2 (`2*current_raise_amt < bank_roll(player)`) - raise option has a range +""" +function valid_raise_amount(table::Table, player::Player, amt) + @assert !(amt ≈ 0) + @assert amt ≤ round_bank_roll(player) + cra = table.current_raise_amt + pc = player.round_contribution + br = round_bank_roll(player) + if cra ≈ 0 # initial raise + rb = (blinds(table).small, br) # raise bounds + else # re-raise + if br > 2*cra + rb = (2*cra, br) # raise bounds + else + rb = (br, br) # raise bounds + end + end + # @assert amt_required_to_call > 0 # right? + @debug "Attempting to raise to \$$(amt), already contributed \$$(pc). Valid raise bounds: [\$$(rb[1]), \$$(rb[2])]" + if !(rb[1] ≤ amt ≤ rb[2] || amt ≈ rb[1] ≈ rb[2]) + @debug "cra = $cra" + @debug "amt = $amt" + @debug "br = $br" + @debug "amt ≈ br = $(amt ≈ br)" + @debug "2*cra ≤ amt ≤ br = $(2*cra ≤ amt ≤ br)" + end + @assert rb[1] ≤ amt ≤ rb[2] || amt ≈ rb[1] ≈ rb[2] + @assert amt - pc > 0 # contribution amount must be > 0! + return amt +end + +""" + raise_to!(game::Game, player::Player, amt) + +Raise bet, for the _round_, to amount `amt`. Example: +``` +# Flop +Player[1] raise to 10 (`amt = 10`, contribute 10) +Player[2] raise to 20 (`amt = 20`, contribute 20) +Player[3] raise to 40 (`amt = 40`, contribute 40) +Player[1] raise to 80 (`amt = 80`, contribute 80-10=70) +Player[2] call +Player[3] call +# Turn +Player[1] raise to 1 (`amt = 1`, contribute 1) +Player[2] raise to 2 (`amt = 2`, contribute 2) +Player[3] raise to 4 (`amt = 4`, contribute 4) +Player[1] raise to 8 (`amt = 8`, contribute 8-1=7) +Player[2] call +Player[3] call +``` +""" +raise_to!(game::Game, player::Player, amt) = raise_to!(game.table, player, amt) + +raise_to!(table::Table, player::Player, amt) = + raise_to_valid_raise_amount!(table, player, valid_raise_amount(table, player, amt)) + +function raise_to_valid_raise_amount!(table::Table, player::Player, amt) @debug "$(name(player)) raising to $(amt)." - @assert !(amt ≈ 0) # more checks are performed in `contribute!` - contribute!(table, player, amt, false) - table.current_raise_amt += amt + pc = player.round_contribution + contribute!(table, player, amt - pc, false) + table.current_raise_amt = amt push!(player.action_history, Raise(amt)) player.action_required = false @@ -78,5 +167,9 @@ function raise_to!(table::Table, player::Player, amt) oponent.action_required = true oponent.last_to_raise = false end - @info "$(name(player)) raised to $(amt)." + if bank_roll(player) ≈ 0 + @info "$(name(player)) raised to $(amt) (all-in)." + else + @info "$(name(player)) raised to $(amt)." + end end diff --git a/src/player_options.jl b/src/player_options.jl index 3d352b3b..cf3df581 100644 --- a/src/player_options.jl +++ b/src/player_options.jl @@ -1,11 +1,26 @@ abstract type PlayerOptions end struct CheckRaiseFold <: PlayerOptions end struct CallRaiseFold <: PlayerOptions end +struct CallAllInFold <: PlayerOptions end # TODO: maybe useful? +struct CallFold <: PlayerOptions end struct PayBlindSitOut <: PlayerOptions end function player_option!(game::Game, player::Player) - if any(last_to_raise.(players_at_table(game))) - player_option!(game, player, CallRaiseFold()) + cra = game.table.current_raise_amt + pc = player.round_contribution + cra ≈ 0 && (@assert pc ≈ 0) + call_amt = cra - pc + @debug "player_option! cra = $cra, pc = $pc, call_amt = $call_amt, !(call_amt ≈ 0) = $(!(call_amt ≈ 0))" + if !(call_amt ≈ 0) + pc = player.round_contribution + amt_required_to_call = cra-pc # (i.e., call amount) + raise_possible = bank_roll(player) > amt_required_to_call + @debug "raise_possible = $raise_possible" + if raise_possible # all-in or fold + player_option!(game, player, CallRaiseFold()) + else + player_option!(game, player, CallFold()) + end else player_option!(game, player, CheckRaiseFold()) end @@ -29,7 +44,7 @@ function player_option!(game::Game, player::Player{Human}, ::CheckRaiseFold) choice = request("$(name(player))'s turn to act:", menu) choice == -1 && error("Uncaught case") choice == 1 && check!(game, player) - choice == 2 && raise_to!(game, player, input_raise_amt(player)) + choice == 2 && raise_to!(game, player, input_raise_amt(game.table, player)) choice == 3 && fold!(game, player) end function player_option!(game::Game, player::Player{Human}, ::CallRaiseFold) @@ -37,23 +52,32 @@ function player_option!(game::Game, player::Player{Human}, ::CallRaiseFold) menu = RadioMenu(options, pagesize=4) choice = request("$(name(player))'s turn to act:", menu) choice == -1 && error("Uncaught case") - choice == 1 && call!(game, player, game.table.current_raise_amt) - choice == 2 && raise_to!(game, player, input_raise_amt(player)) + choice == 1 && call!(game, player) + choice == 2 && raise_to!(game, player, input_raise_amt(game.table, player)) choice == 3 && fold!(game, player) end +function player_option!(game::Game, player::Player{Human}, ::CallFold) + options = ["Call", "Fold"] + menu = RadioMenu(options, pagesize=4) + choice = request("$(name(player))'s turn to act:", menu) + choice == -1 && error("Uncaught case") + choice == 1 && call!(game, player) + choice == 2 && fold!(game, player) +end -function input_raise_amt(player::Player{Human}) +function input_raise_amt(table, player::Player{Human}) raise_amt = nothing while true println("Enter raise amt:") raise_amt = readline() try raise_amt = parse(Float64, raise_amt) - raise_amt ≤ player.bank_roll && break - println("$(name(player)) doesn't have enough funds (\$$(player.bank_roll)) to bet \$$(raise_amt)") + # TODO: Write `is_valid_raise_amount`, and use for better error messages. catch println("Raise must be a Float64") end + raise_amt = valid_raise_amount(table, player, raise_amt) + break end @assert raise_amt ≠ nothing return raise_amt @@ -72,18 +96,14 @@ player_option(player::Player{BotSitOut}, ::PayBlindSitOut) = SitOut() # no other player_option(player::Player{BotCheckFold}, ::PayBlindSitOut) = PayBlind() player_option!(game::Game, player::Player{BotCheckFold}, ::CheckRaiseFold) = check!(game, player) player_option!(game::Game, player::Player{BotCheckFold}, ::CallRaiseFold) = fold!(game, player) +player_option!(game::Game, player::Player{BotCheckFold}, ::CallFold) = fold!(game, player) ##### BotCheckCall player_option(player::Player{BotCheckCall}, ::PayBlindSitOut) = PayBlind() player_option!(game::Game, player::Player{BotCheckCall}, ::CheckRaiseFold) = check!(game, player) -function player_option!(game::Game, player::Player{BotCheckCall}, ::CallRaiseFold) - if game.table.current_raise_amt ≤ bank_roll(player) - call!(game, player, game.table.current_raise_amt) - else - call!(game, player, bank_roll(player)) - end -end +player_option!(game::Game, player::Player{BotCheckCall}, ::CallRaiseFold) = call!(game, player) +player_option!(game::Game, player::Player{BotCheckCall}, ::CallFold) = call!(game, player) ##### BotRandom @@ -94,22 +114,28 @@ function player_option!(game::Game, player::Player{BotRandom}, ::CheckRaiseFold) check!(game, player) else amt = Int(round(rand()*bank_roll(player), digits=0)) - raise_to!(game, player, min(amt, blinds(game).small)) + amt = bound_raise(game.table, player, amt) + raise_to!(game, player, amt) end end function player_option!(game::Game, player::Player{BotRandom}, ::CallRaiseFold) if rand() < 0.5 - # Call - if game.table.current_raise_amt ≤ bank_roll(player) - call!(game, player, game.table.current_raise_amt) - else - call!(game, player, bank_roll(player)) - end - if rand() < 0.5 + if rand() < 0.5 # Call + call!(game, player) + else # re-raise amt = Int(round(rand()*bank_roll(player), digits=0)) - raise_to!(game, player, min(amt, blinds(game).small)) + amt = bound_raise(game.table, player, amt) + raise_to!(game, player, amt) end else fold!(game, player) end end + +function player_option!(game::Game, player::Player{BotRandom}, ::CallFold) + if rand() < 0.5 + call!(game, player) + else + fold!(game, player) + end +end diff --git a/src/player_types.jl b/src/player_types.jl index 270b3188..b2e022eb 100644 --- a/src/player_types.jl +++ b/src/player_types.jl @@ -26,10 +26,13 @@ mutable struct Player{LF} action_history::Vector action_required::Bool all_in::Bool + round_bank_roll::Float64 folded::Bool + pot_investment::Float64 # accumulation of round_contribution, TODO: needs to be added to reset_game! checked::Bool last_to_raise::Bool sat_out::Bool + round_contribution::Float64 end function Base.show(io::IO, player::Player, include_type = true) @@ -41,9 +44,12 @@ function Player(life_form, id, cards = nothing; bank_roll = 200) action_history = [] action_required = true all_in = false + round_bank_roll = Float64(bank_roll) folded = false + pot_investment = Float64(0) checked = false sat_out = false + round_contribution = Float64(0) last_to_raise = false args = ( life_form, @@ -53,10 +59,13 @@ function Player(life_form, id, cards = nothing; bank_roll = 200) action_history, action_required, all_in, + round_bank_roll, folded, + pot_investment, checked, last_to_raise, sat_out, + round_contribution, ) Player(args...) end @@ -66,9 +75,11 @@ bank_roll(player::Player) = player.bank_roll player_id(player::Player) = player.id name(player::Player{LF}) where {LF <: AbstractLifeForm} = "$(nameof(LF))[$(player.id)]" folded(player::Player) = player.folded +still_playing(player::Player) = !player.folded action_history(player::Player) = player.action_history checked(player::Player) = player.checked last_to_raise(player::Player) = player.last_to_raise all_in(player::Player) = player.all_in action_required(player::Player) = player.action_required sat_out(player::Player) = player.sat_out +round_bank_roll(player::Player) = player.round_bank_roll diff --git a/src/table.jl b/src/table.jl index 82f39366..3a7a0513 100644 --- a/src/table.jl +++ b/src/table.jl @@ -64,11 +64,18 @@ observed_cards(table::Table, ::Turn) = table.cards[1:4] observed_cards(table::Table, ::River) = table.cards players_at_table(table::Table) = table.players -all_checked_or_folded(table::Table) = all(map(player -> folded(player) || checked(player), players_at_table(table))) -all_all_in(table::Table) = all(all_in.(players_at_table(table))) +all_checked_or_folded(table::Table) = all(map(x -> folded(x) || checked(x), players_at_table(table))) +all_all_in_or_folded(table::Table) = all(map(x -> folded(x) || all_in(x), players_at_table(table))) blinds(table::Table) = table.blinds +function reset_round_bank_rolls!(table::Table) + players = players_at_table(table) + for player in players + player.round_bank_roll = bank_roll(player) + end +end + function reset_round!(table::Table) players = players_at_table(table) for player in players @@ -76,6 +83,7 @@ function reset_round!(table::Table) folded(player) && continue player.action_required = true player.last_to_raise = false + player.round_contribution = 0 end table.current_raise_amt = 0 end @@ -139,6 +147,11 @@ circle_table(table::Table, state) = small_blind(table::Table) = players_at_table(table)[circle_table(table, 2)] big_blind(table::Table) = players_at_table(table)[circle_table(table, 3)] +first_to_act(table::Table) = players_at_table(table)[circle_table(table, 4)] + +is_small_blind(table::Table, player::Player) = player.id == small_blind(table).id +is_big_blind(table::Table, player::Player) = player.id == big_blind(table).id +is_first_to_act(table::Table, player::Player) = player.id == first_to_act(table).id any_actions_required(table::Table) = any(action_required.(players_at_table(table))) @@ -185,6 +198,8 @@ function deal!(table::Table, blinds::Blinds) shuffle!(table.deck) for (i, player) in enumerate(circle(table, SmallBlind())) po = player_option(player, PayBlindSitOut()) + # TODO: move sit-out option to before deal! to allow earlier + # error checking, and avoiding situations with too few players. if po isa SitOut player.folded = true player.sat_out = true diff --git a/src/transactions.jl b/src/transactions.jl index 1bdcc287..63fec2b3 100644 --- a/src/transactions.jl +++ b/src/transactions.jl @@ -11,7 +11,7 @@ who has gone all-in with amount `amt`. mutable struct SidePot player_id::Int amt::Float64 - cap::Float64 # total possible amount any player can contribute to this side-pot + cap::Float64 # total amount any individual player can contribute to this side-pot end player_id(sp::SidePot) = sp.player_id amount(sp::SidePot) = sp.amt @@ -54,14 +54,16 @@ function TransactionManager(players) [SidePot(pid, 0, cap_i) for (cap_i, pid, amt) in zip(cap, player_id.(sorted_players), bank_roll.(sorted_players))], ) end +amount(tm::TransactionManager) = amount(tm.side_pots[tm.pot_id[1]]) +cap(tm::TransactionManager) = cap(tm.side_pots[tm.pot_id[1]]) -function last_call_of_round(table, player) +function last_action_of_round(table, player, call) for (i,oponent) in enumerate(circle(table, player)) oponent.id == player.id && continue folded(oponent) && continue all_in(oponent) && !last_to_raise(oponent) && continue - return last_to_raise(oponent) - i > length(players_at_table(table)) && error("Broken logic in last_call_of_round") + return last_to_raise(oponent) && call + i > length(players_at_table(table)) && error("Broken logic in last_action_of_round") end end @@ -99,63 +101,105 @@ of the (sorted) players at the start of the game. """ function contribute!(table, player, amt, call=false) tm = table.transactions + @debug "$(name(player))'s bank roll (pre-contribute) = \$$(bank_roll(player))" if !(0 ≤ amt ≤ bank_roll(player)) msg1 = "$(name(player)) has insufficient bank" msg2 = "roll (\$$(bank_roll(player))) to add \$$amt to pot." error(msg1*msg2) end @assert all_in(player) == false + @assert !(amt ≈ 0) + player.round_contribution += amt + player.pot_investment += amt amt_remaining = amt - - if amt ≈ bank_roll(player) - player.all_in = true - end + @debug "$(name(player)) contributing $amt to side-pots" + @debug "caps = $(cap.(tm.side_pots))" + @debug "$(name(player))'s pot_investment = $(player.pot_investment)" for i in 1:length(tm.side_pots) @assert 0 ≤ amt_remaining - side_pot_full(tm, i) && continue cap_i = cap(tm.side_pots[i]) - @assert 0 ≤ amt_remaining + sp_amt = amount(tm.side_pots[i]) cond = amt_remaining < cap_i amt_contrib = cond ? amt_remaining : cap_i - @debug "$(name(player)) contributes $amt_contrib to last side-pot $(i) ($cond)" + contributing = !side_pot_full(tm, i) && !(cap_i ≈ 0) + # This is a bit noisy: + # @debug "$(name(player)) potentially contributing $amt_contrib to side-pot $(i) ($cond). cap_i=$cap_i, amt_remaining=$amt_remaining" + @debug "contributing = $contributing" + contributing || continue + @assert !(amt_contrib ≈ 0) tm.side_pots[i].amt += amt_contrib player.bank_roll -= amt_contrib amt_remaining -= amt_contrib amt_remaining ≈ 0 && break end + if !(amt_remaining ≈ 0) + @show amt_remaining + end + @assert amt_remaining ≈ 0 # pots better be emptied if bank_roll(player) ≈ 0 # went all-in, set exactly. + player.all_in = true player.bank_roll = 0 end - if last_call_of_round(table, player) && call - side_pot_full!(tm) + if is_side_pot_full(tm, table, player, call) + set_side_pot_full!(tm) end + @debug "$(name(player))'s bank roll (post-contribute) = \$$(bank_roll(player))" + @debug "all_in($(name(player))) = $(all_in(player))" +end + +function is_side_pot_full(tm::TransactionManager, table, player, call) + players = players_at_table(table) + # To switch from pot_id = 1 to pot_id = 2, then exactly 1 player should be all-in: + # To switch from pot_id = 2 to pot_id = 3, then exactly 2 players should be all-in: + # ... + return last_action_of_round(table, player, call) && count(all_in.(players)) == tm.pot_id[1] end -side_pot_full!(tm::TransactionManager) = (tm.pot_id[1]+=1) +set_side_pot_full!(tm::TransactionManager) = (tm.pot_id[1]+=1) side_pot_full(tm::TransactionManager, i) = i < tm.pot_id[1] sidepot_winnings(tm::TransactionManager, id::Int) = sum(map(x->x.amt, tm.side_pots[1:id])) +function distribute_winnings_1_player_left!(players, tm::TransactionManager, table_cards) + @assert count(still_playing.(players)) == 1 + n = length(tm.side_pots) + for (player, initial_br) in zip(players, tm.initial_brs) + folded(player) && continue + amt_contributed = initial_br - bank_roll(player) + ∑spw = sidepot_winnings(tm, n) + prof = ∑spw-amt_contributed + @info "$(name(player)) wins \$$(∑spw) (\$$(prof) profit) (all opponents folded)" + player.bank_roll += ∑spw + for j in 1:n + tm.side_pots[j].amt = 0 # empty out distributed winnings + end + break + end + return nothing +end + function distribute_winnings!(players, tm::TransactionManager, table_cards) @debug "Distributing winnings..." @debug "Pot amounts = $(amount.(tm.side_pots))" + if count(still_playing.(players)) == 1 + return distribute_winnings_1_player_left!(players, tm, table_cards) + end hand_evals_sorted = map(enumerate(tm.sorted_players)) do (spid, player) fhe = sat_out(player) ? nothing : FullHandEval((player.cards..., table_cards...)) eligible = !folded(player) && !sat_out(player) (; eligible=eligible, player=player, fhe=fhe, spid=spid) end - player_winnings = map(players) do player + side_pot_winnings = map(players) do player zeros(length(tm.side_pots)) end + winning_hands = Vector{AbstractHandType}(undef, length(players)) for i in 1:length(tm.side_pots) - @debug "Distributing side-pot $i" - @debug "sidepot_winnings(tm, length(players)) ≈ 0 = $(sidepot_winnings(tm, length(players)) ≈ 0)" sidepot_winnings(tm, length(players)) ≈ 0 && continue # no money left to distribute hand_evals_sorted = map(hand_evals_sorted) do (eligible, player, fhe, spid) @@ -189,20 +233,22 @@ function distribute_winnings!(players, tm::TransactionManager, table_cards) winning_player = players[win_id] folded(winning_player) && continue amt = sidepot_winnings(tm, i) / n_winners - player_winnings[win_id][i] = amt + side_pot_winnings[win_id][i] = amt + winning_hands[win_id] = hand_type(hand_evals_sorted[winner_id].fhe) end for j in 1:i tm.side_pots[j].amt = 0 # empty out distributed winnings end end - - for (player, initial_br, side_pot_winnings) in zip(players, tm.initial_brs, player_winnings) - ∑side_pot_winnings = sum(side_pot_winnings) - if !(∑side_pot_winnings ≈ 0) + for (i, (player, initial_br, player_winnings)) in enumerate(zip(players, tm.initial_brs, side_pot_winnings)) + ∑spw = sum(player_winnings) + if !(∑spw ≈ 0) + winning_hand = nameof(typeof(winning_hands[i])) amt_contributed = initial_br - bank_roll(player) - @debug "$(name(player))'s side-pot wins: \$$(side_pot_winnings)!" - @info "$(name(player)) wins \$$(∑side_pot_winnings) (\$$(∑side_pot_winnings-amt_contributed) profit)!" - player.bank_roll += ∑side_pot_winnings + @debug "$(name(player))'s side-pot wins: \$$(player_winnings)!" + prof = ∑spw-amt_contributed + @info "$(name(player)) wins \$$∑spw (\$$prof profit) with $(winning_hand)!" + player.bank_roll += ∑spw end end diff --git a/test/game.jl b/test/game.jl index 60048d48..e4779989 100644 --- a/test/game.jl +++ b/test/game.jl @@ -42,10 +42,10 @@ end # Round 3 raise_to!(game, players[1], 10) - call!(game, players[2], 10) + call!(game, players[2]) # Round 4 - raise_to!(game, players[1], 10) + raise_to!(game, players[1], 20) fold!(game, players[2]) # All-in cases @@ -62,8 +62,8 @@ end # Round 2 raise_to!(game, players[1], players[1].bank_roll) - call!(game, players[2], players[2].bank_roll) + call!(game, players[2]) - @test_throws ErrorException raise_to!(game, players[1], 10000) + @test_throws AssertionError raise_to!(game, players[1], 10000) # raise exceeds bank roll! end diff --git a/test/play.jl b/test/play.jl index 43077f31..3a35e927 100644 --- a/test/play.jl +++ b/test/play.jl @@ -3,13 +3,17 @@ using PlayingCards using NoLimitHoldem NLH = NoLimitHoldem -@testset "Game: Play (3 BotRandom's)" begin - players = ntuple(3) do i - NLH.Player(BotRandom(), i) - end - game = Game(players) +@testset "Game: Play (invalid number of players)" begin + @test_throws AssertionError Game(ntuple(i->NLH.Player(BotRandom(), i), 1)) + @test_throws AssertionError Game(ntuple(i->NLH.Player(BotRandom(), i), 11)) +end - play(game) +@testset "Game: Play (BotCheckCall)" begin + play(Game(ntuple(i->NLH.Player(BotCheckCall(), i), 3))) +end + +@testset "Game: Play (BotCheckFold)" begin + play(Game(ntuple(i->NLH.Player(BotCheckFold(), i), 3))) end @testset "Game: Play (BotSitOut)" begin @@ -18,16 +22,15 @@ end NLH.Player(BotRandom(), 2), NLH.Player(BotRandom(), 3), ) - game = Game(players) - play(game) + play(Game(players)) end -@testset "Game: Play (BotCheckFold)" begin - players = ( - NLH.Player(BotCheckFold(), 1), - NLH.Player(BotCheckFold(), 2), - NLH.Player(BotCheckFold(), 3), - ) - game = Game(players) - play(game) +@testset "Game: Play (3 BotRandom's)" begin + for n in 1:100 + play(Game(ntuple(i->NLH.Player(BotRandom(), i), 3))) + end +end + +@testset "Game: Play (10 BotRandom's)" begin + play(Game(ntuple(i->NLH.Player(BotRandom(), i), 10))) end diff --git a/test/runtests.jl b/test/runtests.jl index de57bd73..7ec35173 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -12,14 +12,15 @@ submodules = [ ] tests_to_debug = ["play"] +# tests_to_debug = submodules for submodule in submodules println("Starting tests for $submodule") t = 0 local logger if any(submodule in tests_to_debug) - # logger = ConsoleLogger(stderr,Logging.Info) - logger = ConsoleLogger(stderr,Logging.Debug;meta_formatter=metafmt) + logger = ConsoleLogger(stderr,Logging.Info) + # logger = ConsoleLogger(stderr,Logging.Debug;meta_formatter=metafmt) else logger = NullLogger() end diff --git a/test/transactions.jl b/test/transactions.jl index 5f090cd8..7aec63f4 100644 --- a/test/transactions.jl +++ b/test/transactions.jl @@ -15,8 +15,8 @@ NLH = NoLimitHoldem @test NLH.player_id.(tm.side_pots) == [1,2,3] NLH.raise_to!(table, players[1], 100) # raise all-in - NLH.call!(table, players[2], 100) # call - NLH.call!(table, players[3], 100) # call + NLH.call!(table, players[2]) # call + NLH.call!(table, players[3]) # call @test NLH.amount.(tm.side_pots) == [300.0, 0.0, 0.0] NLH.distribute_winnings!(players, tm, table_cards) @@ -38,8 +38,8 @@ end table = Table(;players=players,cards=table_cards,transactions=tm) NLH.raise_to!(table, players[1], 100) # Raise - NLH.call!(table, players[2], 100) # call - NLH.call!(table, players[3], 100) # all-in + NLH.call!(table, players[2]) # call + NLH.call!(table, players[3]) # all-in @test NLH.amount.(tm.side_pots) == [300.0, 0.0, 0.0] NLH.distribute_winnings!(players, tm, table_cards) @@ -61,17 +61,17 @@ end table = Table(;players=players,cards=table_cards,transactions=tm) NLH.raise_to!(table, players[1], 100) # Raise all-in - NLH.call!(table, players[2], 100) # call - NLH.call!(table, players[3], 100) # call + NLH.call!(table, players[2]) # call + NLH.call!(table, players[3]) # call @test NLH.amount.(tm.side_pots) == [300.0, 0.0, 0.0] - @test_throws ErrorException NLH.call!(table, players[1], 100) # already all-in! + @test_throws AssertionError NLH.call!(table, players[1]) # already all-in! NLH.reset_round!(table) NLH.raise_to!(table, players[2], 100) # Raise all-in - NLH.call!(table, players[3], 100) # call + NLH.call!(table, players[3]) # call @test NLH.amount.(tm.side_pots) == [300.0, 200.0, 0.0] NLH.distribute_winnings!(players, tm, table_cards) @@ -93,16 +93,16 @@ end table = Table(;players=players,cards=table_cards,transactions=tm) NLH.raise_to!(table, players[1], 100) # Raise - NLH.call!(table, players[2], 100) # call - NLH.call!(table, players[3], 100) # all-in + NLH.call!(table, players[2]) # call + NLH.call!(table, players[3]) # all-in @test NLH.amount.(tm.side_pots) == [300.0, 0.0, 0.0] NLH.reset_round!(table) NLH.raise_to!(table, players[1], 100) # call - NLH.call!(table, players[2], 100) # all-in - @test_throws ErrorException NLH.call!(table, players[3], 100) # already all-in! + NLH.call!(table, players[2]) # all-in + @test_throws AssertionError NLH.call!(table, players[3]) # already all-in! @test NLH.amount.(tm.side_pots) == [300.0, 200.0, 0.0] @@ -128,41 +128,41 @@ end table = Table(;players=players,cards=table_cards,transactions=tm) NLH.raise_to!(table, players[1], 100) # raise all-in - NLH.call!(table, players[2], 100) # call - NLH.call!(table, players[3], 100) # call - NLH.call!(table, players[4], 100) # call - NLH.call!(table, players[5], 100) # call - NLH.call!(table, players[6], 100) # call + NLH.call!(table, players[2]) # call + NLH.call!(table, players[3]) # call + NLH.call!(table, players[4]) # call + NLH.call!(table, players[5]) # call + NLH.call!(table, players[6]) # call @test NLH.amount.(tm.side_pots) == [600.0, 0.0, 0.0, 0.0, 0.0, 0.0] NLH.reset_round!(table) NLH.raise_to!(table, players[2], 100) # raise all-in - NLH.call!(table, players[3], 100) # call - NLH.call!(table, players[4], 100) # call - NLH.call!(table, players[5], 100) # call - NLH.call!(table, players[6], 100) # call + NLH.call!(table, players[3]) # call + NLH.call!(table, players[4]) # call + NLH.call!(table, players[5]) # call + NLH.call!(table, players[6]) # call @test NLH.amount.(tm.side_pots) == [600.0, 500.0, 0.0, 0.0, 0.0, 0.0] NLH.reset_round!(table) NLH.raise_to!(table, players[3], 100) # raise all-in - NLH.call!(table, players[4], 100) # call - NLH.call!(table, players[5], 100) # call - NLH.call!(table, players[6], 100) # call + NLH.call!(table, players[4]) # call + NLH.call!(table, players[5]) # call + NLH.call!(table, players[6]) # call @test NLH.amount.(tm.side_pots) == [600.0, 500.0, 400.0, 0.0, 0.0, 0.0] NLH.reset_round!(table) NLH.raise_to!(table, players[4], 100) # raise all-in - NLH.call!(table, players[5], 100) # call - NLH.call!(table, players[6], 100) # call + NLH.call!(table, players[5]) # call + NLH.call!(table, players[6]) # call @test NLH.amount.(tm.side_pots) == [600.0, 500.0, 400.0, 300.0, 0.0, 0.0] NLH.reset_round!(table) NLH.raise_to!(table, players[5], 100) # raise all-in - NLH.call!(table, players[6], 100) # call + NLH.call!(table, players[6]) # call @test NLH.amount.(tm.side_pots) ≈ [600.0, 500.0, 400.0, 300.0, 200.0, 0.0] NLH.distribute_winnings!(players, tm, table_cards) @@ -206,7 +206,7 @@ end NLH.raise_to!(table, players[5], 500) # raise all-in @test NLH.amount.(tm.side_pots) == [500.0, 400.0, 300.0, 200.0, 100.0, 0.0] - NLH.call!(table, players[6], 500) # call + NLH.call!(table, players[6]) # call @test NLH.amount.(tm.side_pots) ≈ [600.0, 500.0, 400.0, 300.0, 200.0, 0.0] NLH.distribute_winnings!(players, tm, table_cards) @@ -238,19 +238,19 @@ end NLH.raise_to!(table, players[1], 500) # raise to 500 @test NLH.amount.(tm.side_pots) == [100.0, 100.0, 100.0, 100.0, 100.0, 0.0] - NLH.call!(table, players[2], 500) # call + NLH.call!(table, players[2]) # call @test NLH.amount.(tm.side_pots) == [200.0, 200.0, 200.0, 200.0, 200.0, 0.0] - NLH.call!(table, players[3], 400) # call + NLH.call!(table, players[3]) # call @test NLH.amount.(tm.side_pots) == [300.0, 300.0, 300.0, 300.0, 200.0, 0.0] - NLH.call!(table, players[4], 300) # call + NLH.call!(table, players[4]) # call @test NLH.amount.(tm.side_pots) == [400.0, 400.0, 400.0, 300.0, 200.0, 0.0] - NLH.call!(table, players[5], 200) # call + NLH.call!(table, players[5]) # call @test NLH.amount.(tm.side_pots) == [500.0, 500.0, 400.0, 300.0, 200.0, 0.0] - NLH.call!(table, players[6], 100) # call + NLH.call!(table, players[6]) # call @test NLH.amount.(tm.side_pots) == [600.0, 500.0, 400.0, 300.0, 200.0, 0.0] NLH.distribute_winnings!(players, tm, table_cards)