From 885ae9459cb3da2c934baf55c71673a3a58a632d Mon Sep 17 00:00:00 2001 From: Charles Kawczynski Date: Fri, 30 Apr 2021 12:04:08 -0700 Subject: [PATCH] Restructure, fix deal and split pot logic --- docs/src/api.md | 2 + src/NoLimitHoldem.jl | 22 ++++ src/config_game.jl | 24 ++-- src/game.jl | 256 +++++++++++++----------------------------- src/game_viz.jl | 29 +++-- src/player_actions.jl | 44 ++++---- src/player_options.jl | 58 +++++----- src/player_types.jl | 16 ++- src/table.jl | 227 +++++++++++++++++++++++++++++++++++++ src/transactions.jl | 118 +++++++++++++++++-- test/game.jl | 103 +++-------------- test/runtests.jl | 1 + test/table.jl | 255 +++++++++++++++++++++++++++++++++++++++++ test/transactions.jl | 205 +++++++++++++++++++++------------ 14 files changed, 937 insertions(+), 423 deletions(-) create mode 100644 src/table.jl create mode 100644 test/table.jl diff --git a/docs/src/api.md b/docs/src/api.md index c6e9043c..5beb6637 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -1,5 +1,7 @@ # API ```@docs +NoLimitHoldem NoLimitHoldem.move_button! +NoLimitHoldem.play ``` diff --git a/src/NoLimitHoldem.jl b/src/NoLimitHoldem.jl index 5ebcea3c..7cb4b373 100644 --- a/src/NoLimitHoldem.jl +++ b/src/NoLimitHoldem.jl @@ -1,3 +1,16 @@ +""" + NoLimitHoldem + +A no-limit hold-em simulator. + +# Terminology + - `game` a single "game", where players are dealt hands, + winner(s) are declared once. + - `state` a point or process in the game, including + `PreFlop`, `Flop`, `Turn`, `River`. + - `round` the process of each player deciding which + actions to take, until no further actions are taking. +""" module NoLimitHoldem using PlayingCards @@ -6,8 +19,17 @@ using PokerHandEvaluator.HandTypes using UnPack using Printf +export PreFlop, Flop, Turn, River + +abstract type AbstractGameState end +struct PreFlop <: AbstractGameState end +struct Flop <: AbstractGameState end +struct Turn <: AbstractGameState end +struct River <: AbstractGameState end + include("player_types.jl") include("transactions.jl") +include("table.jl") include("game.jl") include("player_actions.jl") include("player_options.jl") diff --git a/src/config_game.jl b/src/config_game.jl index 2780bb6c..2c986991 100644 --- a/src/config_game.jl +++ b/src/config_game.jl @@ -60,22 +60,18 @@ end function configure_basic_heads_up_game() bank_roll = 100 blinds = Blinds(1, 2) - deck = ordered_deck() - shuffle!(deck) players = ( - Player(Human(), 1, pop!(deck, 2); bank_roll=bank_roll), - Player(BotRandom(), 2, pop!(deck, 2); bank_roll=bank_roll) + Player(Human(), 1; bank_roll=bank_roll), + Player(BotRandom(), 2; bank_roll=bank_roll) ) - return Game(; - players=players, - deck=deck, + return Game(players; blinds=blinds, ) end function configure_human_players(n_players) options = string.(1:n_players) - menu = MultiSelectMenu(options; charset=:unicode) + menu = MultiSelectMenu(options) # charset=:unicode is not supported in earlier Julia versions choices = request("Select which players are human:", menu) println("$(length(choices)) human players ($(join(sort(collect(choices)), ", ")))") length(choices) > 0 || println("Menu canceled.") @@ -90,21 +86,15 @@ function configure_custom_game() human_player_ids = configure_human_players(n_players) bank_roll = cofigure_bank_roll(blinds) - deck = ordered_deck() - shuffle!(deck) players = ntuple(n_players) do i if i in human_player_ids - Player(Human(), i, pop!(deck, 2); bank_roll=bank_roll) + Player(Human(), i; bank_roll=bank_roll) else - Player(BotRandom(), i, pop!(deck, 2); bank_roll=bank_roll) + Player(BotRandom(), i; bank_roll=bank_roll) end end - return Game(; - players=players, - deck=deck, - blinds=blinds, - ) + return Game(players; blinds=blinds) end function configure_game() diff --git a/src/game.jl b/src/game.jl index 175c4b93..5f710fa3 100644 --- a/src/game.jl +++ b/src/game.jl @@ -3,180 +3,76 @@ ##### export Game, play -export Deal, PayBlinds, Flop, Turn, River -export move_button! - -abstract type AbstractGameState end -struct Deal <: AbstractGameState end -struct PayBlinds <: AbstractGameState end -struct Flop <: AbstractGameState end -struct Turn <: AbstractGameState end -struct River <: AbstractGameState end - -mutable struct Table{C, P} - cards::C - pot::P - state::AbstractGameState - button_id::Int -end - -function Base.show(io::IO, table::Table, include_type = true) - include_type && println(io, typeof(table)) - println(io, "Button = $(table.button_id)") - println(io, "Pot = $(table.pot)") - println(io, "All cards = $(table.cards)") - println(io, "Observed cards = $(observed_cards(table))") -end - -Table(cards::Tuple) = Table(cards, 0, Deal(), 1) -Table!(deck::Deck) = - Table(Iterators.flatten(ntuple(i->pop!(deck, 1), 5)) |> collect |> Tuple) - -observed_cards(table::Table) = observed_cards(table, table.state) -observed_cards(table::Table, ::Deal) = () -observed_cards(table::Table, ::PayBlinds) = () -observed_cards(table::Table, ::Flop) = table.cards[1:3] -observed_cards(table::Table, ::Turn) = table.cards[1:4] -observed_cards(table::Table, ::River) = table.cards - -struct Blinds{S,B} - small::S - big::B -end - -Base.@kwdef mutable struct Winners - declared::Bool = false - players::Union{Nothing,Tuple,Player} = nothing -end - -function Base.show(io::IO, winners::Winners, include_type = true) - include_type && println(io, typeof(winners)) - println(io, "Winners declared = $(winners.declared)") - println(io, "Winners = $(winners.players)") -end mutable struct Game state::AbstractGameState table::Table - players::Tuple - blinds::Blinds - winners::Winners - current_raise_amt::Float64 -end - -function Base.show(io::IO, blinds::Blinds, include_type = true) - include_type && println(io, typeof(blinds)) - println(io, "Blinds = (small=$(blinds.small),big=$(blinds.big))") -end - -function Base.show(io::IO, player::Player, include_type = true) - include_type && println(io, typeof(player)) - println(io, "Player[$(player.id)] = $(player.cards)") end function Base.show(io::IO, game::Game) println(io, "\n----------------------- Poker game") - show(io, game.blinds, false) show(io, game.table, false) - for player in game.players + for player in players_at_table(game) show(io, player, false) end - show(io, game.winners, false) println(io, "-----------------------") end -function Game(; - deck, - players, +function Game(players; + deck = ordered_deck(), + table_in = nothing, + button_id::Int = button_id(), blinds::Blinds = Blinds(1,2), - winners::Winners = Winners() ) - table = Table!(deck) - state = table.state - targs = (table, players, blinds, winners) - current_raise_amt = 0 - args = (state, table, players, blinds, winners, current_raise_amt) - return Game(args...) -end - -folded(player::Player) = player.folded -action_history(player::Player) = player.action_history -n_players(game::Game) = length(game.players) - -function declare_winners!(game::Game) - fhe = map(game.players) do player - FullHandEval((player.cards..., observed_cards(game.table)...)) - end - - hr = hand_rank.(fhe) - @show hand_type.(fhe) - @show hand_rank.(fhe) - @show best_cards.(fhe) - - min_hr = min(hr...) - game.winners.players = filter(game.players) do player - hr[player.id] == min_hr - end - game.winners.declared = true - game.current_raise_amt = 0 -end -function check_for_winner!(game) - game.winners.declared = count(folded.(game.players)) == n_players(game)-1 - if game.winners.declared - for player in game.players - folded(player) && continue - game.winners.players = player - end + n_player_cards = sum(map(x->cards(x)==nothing ? 0 : length(cards(x)), players)) + + if length(deck) ≠ 52 + # if the deck isn't full, then players should have been dealt cards. + @assert n_player_cards > 0 + + # If the user is specifying the player cards, then the + # user should probably also handle the table cards too. + @assert table_in ≠ nothing + table = table_in + @assert cards(table) isa Tuple{Card,Card,Card,Card,Card} + @assert legnth(cards(table)) == 5 + @assert length(deck) + n_player_cards + length(cards(table)) == 52 + else # nobody has been dealt yet + table = Table(; + deck=deck, + players=players, + button_id=button_id, + blinds=blinds + ) + + @assert length(deck) == 52 + @assert n_player_cards == 0 + @assert cards(table) == nothing end -end - -move_button!(game::Game) = move_button!(game.table, game.players) - -""" - move_button!(table::Table) -Move the button to the next player on -the table. -""" -function move_button!(table::Table, players) - table.button_id = mod(table.button_id, length(players))+1 + state = table.state + args = (state, table) + return Game(args...) end +players_at_table(game::Game) = players_at_table(game.table) +small_blind(game::Game) = small_blind(players_at_table(game), game.table) +big_blind(game::Game) = big_blind(players_at_table(game), game.table) +blinds(game::Game) = blinds(game.table) +any_actions_required(game::Game) = any_actions_required(game.table) -""" - action_id(n_players, button_id, state) - -The player ID whose action it is, given - - `state` the global iteration state (starting from 1) - - `n_players` the total number of players - - `button_id` the dealer ID (from `1:n_players`) -""" -action_id(n_players, button_id, state) = - mod(state + button_id+1, n_players)+1 - -action_id(game::Game, state) = - action_id(length(game.players), game.table.button_id, state) - -acting_player(game::Game, state) = - game.players[action_id(game, state)] - -any_actions_required(game::Game) = - any(map(player -> player.action_required, game.players)) +reset_round!(game::Game) = reset_round!(game.table) -player_button_star(table::Table, player::Player) = - table.button_id == player.id ? "*" : "" - -small_blind(game::Game) = game.players[action_id(game, -1)] -big_blind(game::Game) = game.players[action_id(game, 0)] -blinds(game::Game) = game.blinds - -function reset_actions_required!(game::Game) - for player in game.players +function reset_round!(table::Table) + players = players_at_table(table) + for player in players + player.checked = false folded(player) && continue player.action_required = true + player.last_to_raise = false end - game.current_raise_amt = 0 + table.current_raise_amt = 0 end function set_state!(game::Game, state::AbstractGameState) @@ -185,51 +81,55 @@ function set_state!(game::Game, state::AbstractGameState) end function act_generic!(game::Game, state::AbstractGameState) - game.winners.declared && return # TODO: is this redundant? + table = game.table + table.winners.declared && return # TODO: is this redundant? set_state!(game, state) print_state(game) - # TODO: incorporate winners.declared into logic - any_actions_required(game) || return - i = 1 - while any_actions_required(game) - player = acting_player(game, i) - folded(player) || player_option!(game, player) - game.winners.declared && break - i+=1 + for player in circle(table, SmallBlind()) + last_to_raise(player) && break + all_checked_or_folded(table) && break + folded(player) && continue + player_option!(game, player) + table.winners.declared && break end end function act!(game::Game, state::AbstractGameState) act_generic!(game, state) - reset_actions_required!(game) + reset_round!(game) end function act!(game::Game, state::River) act_generic!(game, state) - declare_winners!(game) + declare_winners!(game.table) end -function act!(game::Game, state::PayBlinds) - set_state!(game, state) - # Always call blinds: - call!(game, small_blind(game), blinds(game).small) - call!(game, big_blind(game), blinds(game).big) - print_state(game) - reset_actions_required!(game) -end +""" + play(::Game) + +Play a game. Note that this method +expects no cards (players and table) +to be dealt. +""" +function play(game::Game; debug = false) + table = game.table + players = players_at_table(table) + + table.transactions = TransactionManager(players) + + @assert all(cards.(players) .== nothing) + @assert cards(table) == nothing + deal!(table, blinds(table)) + @assert cards(table) ≠ nothing + + winners = table.winners -function play(game::Game) - # TODO: deal cards here instead of cards dealt into Game before play - # Also: Players who cannot afford the blinds are all-in, which is likely - # not caught here. - # @assert all(cards.(game.players) .== nothing) - act!(game, Deal()) # Deal player cards, then bet/check/raise based on player cards - act!(game, PayBlinds()) # - game.winners.declared || act!(game, Flop()) # Deal flop , then bet/check/raise based on flop - game.winners.declared || act!(game, Turn()) # Deal turn , then bet/check/raise based on turn - game.winners.declared || act!(game, River()) # Deal river , then bet/check/raise based on river - return game.winners + 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 + return winners end diff --git a/src/game_viz.jl b/src/game_viz.jl index 71e76318..11e615e0 100644 --- a/src/game_viz.jl +++ b/src/game_viz.jl @@ -17,17 +17,17 @@ print_state(game::Game) = print_state(game, game.state) include("print_row.jl") -function print_state(game::Game, ::Deal) - players = map(game.players) do player +function print_state(game::Game, ::PreFlop) + players = map(players_at_table(game)) do player star = player_button_star(game.table, player) star*name(player) end players = hcat("Players", players...) - player_cards = map(game.players) do player + player_cards = map(players_at_table(game)) do player join(string.(player.cards), ", ") end player_cards = hcat("Cards", player_cards...) - println(repeat("-", (15+1)*(length(game.players)+1))) + println(repeat("-", (15+1)*(length(players_at_table(game))+1))) println(sprint_row(players)) println(sprint_row(player_cards)) end @@ -48,26 +48,25 @@ function print_state(game::Game, ::River) table_cards = hcat("River", table_cards) println() println(sprint_row(table_cards)) - println(repeat("-", (15+1)*(length(game.players)+1))) + println(repeat("-", (15+1)*(length(players_at_table(game))+1))) end -print_state(game::Game, ::PayBlinds) = nothing function action_table_data(game::Game) - n_players = length(game.players) - n_rounds = maximum(map(player -> length(player.action_history), game.players)) + n_players = length(players_at_table(game)) + n_rounds = maximum(map(player -> length(player.action_history), players_at_table(game))) rounds = map(i->"Round $i", 1:n_rounds) header = reshape(vcat("Player", "Cards", rounds), 1,n_rounds+2) - rows = map(game.players) do player + rows = map(players_at_table(game)) do player star = player_button_star(game.table, player) - star*"Player[$(player.id)]" + star*name(player) end rows = collect(rows) - data = vcat(collect(map(game.players) do player + data = vcat(collect(map(players_at_table(game)) do player arr = collect(padded_array(player.action_history, n_rounds)) reshape(arr, 1, size(arr, 1)) end)...) - cards = vcat(collect(map(game.players) do player + cards = vcat(collect(map(players_at_table(game)) do player join(string.(player.cards), ", ") end)...) @@ -78,12 +77,12 @@ end function results_table_data(game::Game) header = ["Player" "Hand"] - rows = map(game.players) do player + rows = map(players_at_table(game)) do player star = player_button_star(game.table, player) - star*"Player[$(player.id)]" + star*name(player) end rows = collect(rows) - data = vcat(collect(map(game.players) do player + data = vcat(collect(map(players_at_table(game)) do player join(string.(player.cards), ", ") end)...) all_data = hcat(rows, data) diff --git a/src/player_actions.jl b/src/player_actions.jl index 326fd274..4c1da62e 100644 --- a/src/player_actions.jl +++ b/src/player_actions.jl @@ -6,6 +6,8 @@ export fold!, check!, raise!, call! export Fold, Check, Call, Raise abstract type AbstractAction end +struct PayBlind <: AbstractAction end +struct SitOut <: AbstractAction end struct Fold <: AbstractAction end struct Check <: AbstractAction end struct Call{T} <: AbstractAction @@ -19,42 +21,38 @@ function fold!(game::Game, player::Player) push!(player.action_history, Fold()) player.action_required = false player.folded = true - check_for_winner!(game) + check_for_winner!(game.table) end function check!(game::Game, player::Player) push!(player.action_history, Check()) player.action_required = false end -function call!(game::Game, player::Player, amt) + +call!(game::Game, player::Player, amt) = call!(game.table, player, amt) + +function call!(table::Table, player::Player, amt;debug=false) push!(player.action_history, Call(amt)) player.action_required = false - - if player.bank_roll > amt - player.bank_roll -= amt - game.table.pot += amt - else - player.bank_roll = 0 - game.table.pot += player.bank_roll - player.all_in = true - end + contribute!(table, player, amt, true;debug=debug) end -function raise!(game::Game, player::Player, amt) - if player.bank_roll >= amt - player.bank_roll -= amt - game.table.pot += amt - player.bank_roll == amt && (player.all_in = true) - game.current_raise_amt += amt - else - msg1 = "Player $(player.id) has insufficient bank" - msg2 = "roll ($(player.bank_roll)) to add $amt to pot." - error(msg1*msg2) - end + +raise!(game::Game, player::Player, amt) = raise!(game.table, player, amt) + +# TODO: add assertion that raise amount must be +# greater than small blind (unless all-in). +function raise!(table::Table, player::Player, amt;debug=false) + @assert !(amt ≈ 0) # more checks are performed in `contribute!` + contribute!(table, player, amt, false;debug=debug) + table.current_raise_amt += amt push!(player.action_history, Raise(amt)) player.action_required = false - for oponent in game.players + player.last_to_raise = true + players = players_at_table(table) + for oponent in players oponent.id == player.id && continue folded(oponent) && continue oponent.action_required = true + oponent.last_to_raise = false end end diff --git a/src/player_options.jl b/src/player_options.jl index f8bdf370..dd4f3985 100644 --- a/src/player_options.jl +++ b/src/player_options.jl @@ -1,9 +1,10 @@ abstract type PlayerOptions end struct CheckRaiseFold <: PlayerOptions end struct CallRaiseFold <: PlayerOptions end +struct PayBlindSitOut <: PlayerOptions end function player_option!(game::Game, player::Player) - if raise_needs_call(game, player) + if any(last_to_raise.(players_at_table(game))) player_option!(game, player, CallRaiseFold()) else player_option!(game, player, CheckRaiseFold()) @@ -14,29 +15,37 @@ end ##### Bot player options (ask via prompts) ##### +player_option(player::Player{BotRandom}, ::PayBlindSitOut) = PayBlind() + function player_option!(game::Game, player::Player{BotRandom}, ::CheckRaiseFold) if rand() < 0.5 check!(game, player) - @info "(Bot) player $(player.id) checked" + @info "$(name(player)) checked" else - amt = Int(round(rand()*player.bank_roll, digits=0)) - raise!(game, player, amt) - @info "(Bot) player $(player.id) raised \$$(amt)!" + amt = Int(round(rand()*bank_roll(player), digits=0)) + raise!(game, player, min(amt, blinds(game).small)) + @info "$(name(player)) raised \$$(amt)!" end end function player_option!(game::Game, player::Player{BotRandom}, ::CallRaiseFold) if rand() < 0.5 if rand() < 0.5 - call!(game, player, game.current_raise_amt) - @info "(Bot) player $(player.id) called!" - else - amt = Int(round(rand()*player.bank_roll, digits=0)) - raise!(game, player, amt) - @info "(Bot) player $(player.id) re-raised \$$(amt)!" + 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 + amt = Int(round(rand()*bank_roll(player), digits=0)) + raise!(game, player, min(amt, blinds(game).small)) + @info "$(name(player)) re-raised \$$(amt)!" + else + @info "$(name(player)) called!" + end end else fold!(game, player) - @info "(Bot) player $(player.id) folded!" + @info "$(name(player)) folded!" end end @@ -44,11 +53,19 @@ end ##### Human player options (ask via prompts) ##### +function player_option(player::Player{Human}, ::PayBlindSitOut) + options = ["Pay blind", "Sit out a hand"] + menu = RadioMenu(options, pagesize=4) + choice = request("Player $(player.id)'s turn to act:", menu) + choice == -1 && error("Uncaught case") + choice == 1 && return PayBlind() + choice == 2 && return SitOut() +end function player_option!(game::Game, player::Player{Human}, ::CheckRaiseFold) options = ["Check", "Raise", "Fold"] menu = RadioMenu(options, pagesize=4) choice = request("Player $(player.id)'s turn to act:", menu) - choice == -1 && error("Uncaught case in `act!(game::Game, player::Player{Human}, ::CheckRaiseFold)`") + choice == -1 && error("Uncaught case") choice == 1 && check!(game, player) choice == 2 && raise!(game, player, input_raise_amt(player)) choice == 3 && fold!(game, player) @@ -57,8 +74,8 @@ function player_option!(game::Game, player::Player{Human}, ::CallRaiseFold) options = ["Call", "Raise", "Fold"] menu = RadioMenu(options, pagesize=4) choice = request("Player $(player.id)'s turn to act:", menu) - choice == -1 && error("Uncaught case in `act!(game::Game, player::Player{Human}, ::CallRaiseFold)`") - choice == 1 && call!(game, player, game.current_raise_amt) + choice == -1 && error("Uncaught case") + choice == 1 && call!(game, player, game.table.current_raise_amt) choice == 2 && raise!(game, player, input_raise_amt(player)) choice == 3 && fold!(game, player) end @@ -84,14 +101,3 @@ function input_raise_amt(player::Player{Human}) @info "Player $(player.id) bets \$$(raise_amt)" return raise_amt end - -# TODO: fix logic (this is currently broken) -function raise_needs_call(game::Game, player::Player) - for ah in action_history.(game.players) - length(ah) == 0 && continue - if last(ah) isa Raise - return true - end - end - return false -end diff --git a/src/player_types.jl b/src/player_types.jl index c404d9ca..033ebe52 100644 --- a/src/player_types.jl +++ b/src/player_types.jl @@ -21,6 +21,13 @@ mutable struct Player{LF} action_required::Bool all_in::Bool folded::Bool + checked::Bool + last_to_raise::Bool +end + +function Base.show(io::IO, player::Player, include_type = true) + include_type && println(io, typeof(player)) + println(io, "$(name(player)) = $(player.cards)") end function Player(life_form, id, cards = nothing; bank_roll = 200) @@ -28,7 +35,9 @@ function Player(life_form, id, cards = nothing; bank_roll = 200) action_required = true all_in = false folded = false - args = (life_form, id, cards, Float64(bank_roll), action_history, action_required, all_in, folded) + checked = false + last_to_raise = false + args = (life_form, id, cards, Float64(bank_roll), action_history, action_required, all_in, folded, checked, last_to_raise) Player(args...) end @@ -36,3 +45,8 @@ cards(player::Player) = player.cards 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 +action_history(player::Player) = player.action_history +checked(player::Player) = player.checked +last_to_raise(player::Player) = + player.last_to_raise diff --git a/src/table.jl b/src/table.jl new file mode 100644 index 00000000..38d91352 --- /dev/null +++ b/src/table.jl @@ -0,0 +1,227 @@ +##### +##### Table +##### + +export Button, SmallBlind, BigBlind +export Table +export move_button! + +Base.@kwdef mutable struct Winners + declared::Bool = false + players::Union{Nothing,Tuple,Player} = nothing +end + +function Base.show(io::IO, winners::Winners, include_type = true) + include_type && println(io, typeof(winners)) + println(io, "Winners declared = $(winners.declared)") + println(io, "Winners = $(winners.players)") +end + +struct Blinds{S,B} + small::S + big::B +end + +Blinds() = Blinds(1,2) # default +button_id() = 1 # default + +function Base.show(io::IO, blinds::Blinds, include_type = true) + include_type && println(io, typeof(blinds)) + println(io, "Blinds = (small=$(blinds.small),big=$(blinds.big))") +end + +Base.@kwdef mutable struct Table + deck::PlayingCards.Deck = ordered_deck() + players::Tuple + cards::Union{Nothing,Tuple{<:Card,<:Card,<:Card,<:Card,<:Card}} = nothing + blinds::Blinds = Blinds() + pot::Float64 = Float64(0) + state::AbstractGameState = PreFlop() + button_id::Int = button_id() + current_raise_amt::Float64 = Float64(0) + transactions::TransactionManager = TransactionManager(players) + winners::Winners = Winners() +end + +function Base.show(io::IO, table::Table, include_type = true) + include_type && println(io, typeof(table)) + show(io, blinds(table), false) + show(io, table.winners, false) + println(io, "Button = $(table.button_id)") + println(io, "Pot = $(table.transactions)") + println(io, "All cards = $(table.cards)") + println(io, "Observed cards = $(observed_cards(table))") +end + +get_table_cards!(deck::PlayingCards.Deck) = + Iterators.flatten(ntuple(i->pop!(deck, 1), 5)) |> collect |> Tuple +cards(table::Table) = table.cards + +observed_cards(table::Table) = observed_cards(table, table.state) +observed_cards(table::Table, ::PreFlop) = () +observed_cards(table::Table, ::Flop) = table.cards[1:3] +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))) + +blinds(table::Table) = table.blinds + +function declare_winners!(table::Table) + fhe = map(players_at_table(table)) do player + FullHandEval((player.cards..., observed_cards(table)...)) + end + + hr = hand_rank.(fhe) + @show hand_type.(fhe) + @show hand_rank.(fhe) + @show best_cards.(fhe) + + min_hr = min(hr...) + table.winners.players = filter(players_at_table(table)) do player + hr[player.id] == min_hr + end + table.winners.declared = true + table.current_raise_amt = 0 +end + +function check_for_winner!(table::Table) + players = players_at_table(table) + n_players = length(players) + table.winners.declared = count(folded.(players)) == n_players-1 + if table.winners.declared + for player in players + folded(player) && continue + table.winners.players = player + end + end +end + + +##### +##### Circling the table +##### + +""" + move_button!(table::Table) + +Move the button to the next player on +the table. +""" +function move_button!(table::Table) + table.button_id = mod(table.button_id, length(table.players))+1 +end + +""" + position(table, player::Player, relative) + +Player position, given + - `table` the table + - `player` the player + - `relative::Int = 0` the relative location to the player +""" +position(table, player::Player, relative=0) = + mod(relative + player.id - 1, length(table.players))+1 + +""" + circle_table(n_players, button_id, state) + +Circle the table, starting from the `button_id` +which corresponds to `state = 1`. + - `state` the global iteration state (starting from 1) + - `n_players` the total number of players + - `button_id` the dealer ID (from `1:n_players`) +""" +circle_table(n_players, button_id, state) = + mod(state + button_id-2, n_players)+1 + +circle_table(table::Table, state) = + circle_table(length(table.players), table.button_id, state) + +button(players::Tuple, table::Table) = players[circle_table(table, 1)] +small_blind(players::Tuple, table::Table) = players[circle_table(table, 2)] +big_blind(players::Tuple, table::Table) = players[circle_table(table, 3)] + +any_actions_required(table::Table) = + any(map(player -> player.action_required, players_at_table(table))) + +player_button_star(table::Table, player::Player) = + table.button_id == player.id ? "*" : "" + +abstract type TablePosition end +struct Button <: TablePosition end +struct SmallBlind <: TablePosition end +struct BigBlind <: TablePosition end + +struct CircleTable{CircType,P} + players::Tuple + button_id::Int + n_players::Int + player::P +end + +circle(table::Table, tp::TablePosition) = + CircleTable{typeof(tp),Nothing}(table.players, table.button_id, length(table.players), nothing) + +circle(table::Table, player::Player) = + CircleTable{typeof(player),typeof(player)}(table.players, table.button_id, length(table.players), player) + +Base.iterate(ct::CircleTable{Button}, state = 1) = + (ct.players[circle_table(ct.n_players, ct.button_id, state)], state+1) + +Base.iterate(ct::CircleTable{SmallBlind}, state = 2) = + (ct.players[circle_table(ct.n_players, ct.button_id, state)], state+1) + +Base.iterate(ct::CircleTable{BigBlind}, state = 3) = + (ct.players[circle_table(ct.n_players, ct.button_id, state)], state+1) + +Base.iterate(ct::CircleTable{P}, state = 1) where {P <: Player} = + (ct.players[circle_table(ct.n_players, ct.player.id, state)], state+1) + +##### +##### Deal +##### + +function deal!(table::Table, blinds::Blinds) + players = players_at_table(table) + shuffle!(table.deck) + for (i, player) in enumerate(circle(table, SmallBlind())) + po = player_option(player, PayBlindSitOut()) + if po isa SitOut + player.folded = true + @info "$(name(player)) sat out a hand." + check_for_winner!(table) + elseif po isa PayBlind + if player.id == small_blind(players, table).id && bank_roll(player) ≤ blinds.small + player.cards = pop!(table.deck, 2) + player.all_in = true + @info "$(name(player)) is all in on small blind!" + contribute!(table, player, bank_roll(player)) + elseif player.id == big_blind(players, table).id && bank_roll(player) ≤ blinds.big + player.cards = pop!(table.deck, 2) + player.all_in = true + @info "$(name(player)) is all in on big blind!" + contribute!(table, player, bank_roll(player)) + else + player.cards = pop!(table.deck, 2) + if player.id == small_blind(players, table).id + @info "$(name(player)) paid the small blind." + contribute!(table, player, blinds.small) + elseif player.id == big_blind(players, table).id + @info "$(name(player)) paid the big blind." + contribute!(table, player, blinds.big) + else + @info "$(name(player)) dealt free cards." + end + end + else + error("Uncaught case") + end + i==length(players) && break + end + + table.cards = get_table_cards!(table.deck) + @info "Table cards dealt (but not yet revealed)." +end + diff --git a/src/transactions.jl b/src/transactions.jl index 7dfac616..db790e8c 100644 --- a/src/transactions.jl +++ b/src/transactions.jl @@ -11,8 +11,11 @@ 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 end player_id(sp::SidePot) = sp.player_id +amount(sp::SidePot) = sp.amt +cap(sp::SidePot) = sp.cap """ TransactionManager @@ -22,34 +25,133 @@ among multiple players. """ struct TransactionManager sorted_players::Vector{Player} - pot_id::Vector{Int} + pot_id::Union{Nothing,Vector{Int}} side_pots::Vector{SidePot} end +function Base.show(io::IO, tm::TransactionManager, include_type = true) + include_type && println(io, typeof(tm)) + println(io, "Pot(s) = $(tm.side_pots)") +end + function TransactionManager(players) sorted_players = sort(collect(players); by = x->bank_roll(x)) + + cap = zeros(length(players)) + for i in 1:length(players) + if i == 1 + cap[i] = bank_roll(sorted_players[i]) + else + cap[i] = bank_roll(sorted_players[i]) - sum(cap[1:i-1]) + end + end + TransactionManager( sorted_players, Int[1], - [SidePot(pid, 0) for (pid, amt) in zip(player_id.(sorted_players), bank_roll.(sorted_players))] + [SidePot(pid, 0, cap_i) for (cap_i, pid, amt) in zip(cap, player_id.(sorted_players), bank_roll.(sorted_players))], ) end -function contribute!(tm::TransactionManager, player, amt) - @assert bank_roll(player) ≥ amt - tm.side_pots[tm.pot_id[1]].amt+=amt - player.bank_roll -= amt +function last_call_of_round(table, player) + for (i,oponent) in enumerate(circle(table, player)) + oponent.id == player.id && continue + folded(oponent) && continue + oponent.all_in && !last_to_raise(oponent) && continue + return last_to_raise(oponent) + i > length(players_at_table(table)) && error("Broken logic in last_call_of_round") + end +end + +""" + contribute!(table, player, amt, call=false;debug=false) + +Player `player` contributes amount `amt` to the +appropriate side-pot(s) for calls (for `call = true`) +and raises (for `call = false`). All side-pots are +internally handled. + +To properly handle side-pots, each raise and call +is decomposed into side-pots based on the bank roll +of the (sorted) players at the start of the game. + + +``` + [1300] __ all-in for sorted_player_6 + | [] + | [1100] __ all-in for sorted_player_5 + | | [] + | | [] + | | [] + | | [] + | | [] + | | [500] __ all-in for sorted_player_4 + | | | [400] __ all-in for sorted_player_3 + | | | | [300] __ all-in for sorted_player_2 + | | | | | [] + | | | | | [100] __ all-in for sorted_player_1 +| | | | | | [] +--------------------- +1 2 3 4 5 6 +``` +""" +function contribute!(table, player, amt, call=false;debug=false) + tm = table.transactions + if !(0 ≤ amt ≤ bank_roll(player)) + msg1 = "Player $(player.id) has insufficient bank" + msg2 = "roll ($(player.bank_roll)) to add $amt to pot." + error(msg1*msg2) + end + @assert player.all_in == false + + amt_remaining = amt + + if amt ≈ bank_roll(player) + player.all_in = true + end + + for i in 1:length(tm.side_pots) + @assert 0 ≤ amt_remaining + side_pot_full(tm, i) && continue + cap_i = tm.side_pots[i].cap + @assert 0 ≤ amt_remaining + cond = amt_remaining < cap_i + amt_contrib = cond ? amt_remaining : cap_i + debug && @info "$(name(player)) contributes $amt_contrib to last side-pot $(i) ($cond)" + tm.side_pots[i].amt += amt_contrib + player.bank_roll -= amt_contrib + amt_remaining -= amt_contrib + amt_remaining ≈ 0 && break + end + + if bank_roll(player) ≈ 0 # went all-in, set exactly. + player.bank_roll = 0 + end + + if last_call_of_round(table, player) && call + side_pot_full!(tm) + end end 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 print_winner(player, n_side_pots, amt) + if n_side_pots==1 + @info "$(name(player)) wins \$$(amt)!" + else + @info "$(name(player)) wins \$$(amt) side-pot!" + end +end + function distribute_winnings!(players, tm::TransactionManager, table_cards) hand_evals = map(enumerate(tm.sorted_players)) do (j, player) (player.id, hand_rank(FullHandEval((player.cards..., table_cards...))), j) end hand_evals_filtered = deepcopy(hand_evals) + n_side_pots = count(x->!(x.amt≈0), tm.side_pots) for i in 1:length(tm.side_pots) @@ -67,7 +169,9 @@ function distribute_winnings!(players, tm::TransactionManager, table_cards) for winner_id in winners win_id = tm.sorted_players[winner_id].id winer_pl = tm.sorted_players[winner_id] - players[win_id].bank_roll += sidepot_winnings(tm, i) / n_winners + amt = sidepot_winnings(tm, i) / n_winners + players[win_id].bank_roll += amt + print_winner(players[win_id], n_side_pots, amt) end for j in 1:i tm.side_pots[j].amt = 0 # empty out distributed winnings diff --git a/test/game.jl b/test/game.jl index f3daf659..980def4e 100644 --- a/test/game.jl +++ b/test/game.jl @@ -4,79 +4,32 @@ using NoLimitHoldem using PrettyTables NLH = NoLimitHoldem -@testset "Action ID" begin - @test NLH.action_id(5, 1, 1) == 4 - @test NLH.action_id(5, 1, 2) == 5 - @test NLH.action_id(5, 1, 3) == 1 - @test NLH.action_id(5, 1, 4) == 2 - @test NLH.action_id(5, 1, 5) == 3 - - @test NLH.action_id(5, 4, 1) == 2 - @test NLH.action_id(5, 4, 2) == 3 - @test NLH.action_id(5, 4, 3) == 4 - @test NLH.action_id(5, 4, 4) == 5 - @test NLH.action_id(5, 4, 5) == 1 - - @test NLH.action_id(2, 1, 1) == 2 - @test NLH.action_id(2, 1, 2) == 1 - - @test NLH.action_id(2, 2, 1) == 1 - @test NLH.action_id(2, 2, 2) == 2 -end - -@testset "Table" begin - deck = ordered_deck() - - shuffle!(deck) - players = ntuple(2) do i - NLH.Player(BotRandom(), i, pop!(deck, 2)) - end - table = NLH.Table!(deck) - - table.state = Deal() - @test NLH.observed_cards(table) == () - table.state = PayBlinds(); - @test NLH.observed_cards(table) == () - table.state = Flop() - @test NLH.observed_cards(table) == table.cards[1:3] - table.state = Turn() - @test NLH.observed_cards(table) == table.cards[1:4] - table.state = River() - @test NLH.observed_cards(table) == table.cards -end - -@testset "Print row" begin +@testset "Game: Print row" begin for i in 1:10 NLH.sprint_row(repeat(["a"], i)) end end -@testset "Play" begin - deck = ordered_deck() - shuffle!(deck) +@testset "Game: show" begin players = ntuple(2) do i - NLH.Player(BotRandom(), i, pop!(deck, 2)) + NLH.Player(BotRandom(), i) end - game = Game(;deck=deck,players=players) + game = Game(players) sprint(show, game) - game.table.state = Deal() - - NLH.act!(game, PayBlinds()) + game.table.state = PreFlop() sprint(show, game) - NLH.act!(game, PayBlinds()) end pretty_table_header(header) = tuple([header[i, :] for i = 1:size(header, 1)]...) -@testset "Game" begin - deck = ordered_deck() - shuffle!(deck) +@testset "Game: contrived game" begin players = ntuple(3) do i - NLH.Player(BotRandom(), i, pop!(deck, 2)) + NLH.Player(BotRandom(), i) end - game = Game(;deck = deck, players = players) - players = game.players + game = Game(players) + players = NLH.players_at_table(game) + NLH.deal!(game.table, NLH.blinds(game.table)) # Round 1 check!(game, players[1]) check!(game, players[2]) @@ -104,13 +57,12 @@ pretty_table_header(header) = tuple([header[i, :] for i = 1:size(header, 1)]...) ) # All-in cases - deck = ordered_deck() - shuffle!(deck) players = ntuple(3) do i - NLH.Player(BotRandom(), i, pop!(deck, 2)) + NLH.Player(BotRandom(), i) end - game = Game(;deck = deck, players = players) - players = game.players + game = Game(players) + players = NLH.players_at_table(game) + NLH.deal!(game.table, NLH.blinds(game.table)) # Round 1 check!(game, players[1]) check!(game, players[2]) @@ -132,15 +84,13 @@ pretty_table_header(header) = tuple([header[i, :] for i = 1:size(header, 1)]...) ) end -@testset "Play" begin - deck = ordered_deck() - shuffle!(deck) +@testset "Game: Play 2" begin players = ntuple(3) do i - NLH.Player(BotRandom(), i, pop!(deck, 2)) + NLH.Player(BotRandom(), i) end - game = Game(;deck = deck, players = players) + game = Game(players) - play(game) + play(game; debug = true) data, header, table_cards = NLH.action_table_data(game) @info table_cards @@ -159,20 +109,3 @@ end crop = :none, ) end - -@testset "Move button" begin - deck = ordered_deck() - shuffle!(deck) - players = ntuple(3) do i - NLH.Player(BotRandom(), i, pop!(deck, 2)) - end - game = Game(;deck = deck, players = players) - @test game.table.button_id == 1 - move_button!(game) - @test game.table.button_id == 2 - move_button!(game) - @test game.table.button_id == 3 - move_button!(game) - @test game.table.button_id == 1 -end - diff --git a/test/runtests.jl b/test/runtests.jl index f8bad935..28832dbb 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,7 @@ for submodule in [ "players", "transactions", + "table", "game", ] diff --git a/test/table.jl b/test/table.jl new file mode 100644 index 00000000..a6ef26a0 --- /dev/null +++ b/test/table.jl @@ -0,0 +1,255 @@ +using Test +using PlayingCards +using NoLimitHoldem +using PrettyTables +NLH = NoLimitHoldem + +@testset "Table: constructors / observed cards" begin + players = ntuple(i-> NLH.Player(BotRandom(), i), 2) + deck = ordered_deck() + shuffle!(deck) + blinds = NLH.Blinds(1,2) + cards = NLH.get_table_cards!(deck) + table = NLH.Table(;deck=deck, cards=cards, players=players) + NLH.deal!(table, blinds) + + table.state = PreFlop() + @test NLH.observed_cards(table) == () + table.state = Flop() + @test NLH.observed_cards(table) == table.cards[1:3] + table.state = Turn() + @test NLH.observed_cards(table) == table.cards[1:4] + table.state = River() + @test NLH.observed_cards(table) == table.cards +end + +@testset "Table: Move button" begin + players = ntuple(i-> NLH.Player(BotRandom(), i), 3) + table = Table(;players = players) + @test table.button_id == 1 + move_button!(table) + @test table.button_id == 2 + move_button!(table) + @test table.button_id == 3 + move_button!(table) + @test table.button_id == 1 +end + +@testset "Table: Circle table" begin + @test NLH.circle_table(5, 1, 1) == 1 + @test NLH.circle_table(5, 1, 2) == 2 + @test NLH.circle_table(5, 1, 3) == 3 + @test NLH.circle_table(5, 1, 4) == 4 + @test NLH.circle_table(5, 1, 5) == 5 + + @test NLH.circle_table(5, 4, 1) == 4 + @test NLH.circle_table(5, 4, 2) == 5 + @test NLH.circle_table(5, 4, 3) == 1 + @test NLH.circle_table(5, 4, 4) == 2 + @test NLH.circle_table(5, 4, 5) == 3 + + @test NLH.circle_table(2, 1, 1) == 1 + @test NLH.circle_table(2, 1, 2) == 2 + + @test NLH.circle_table(2, 2, 1) == 2 + @test NLH.circle_table(2, 2, 2) == 1 +end + +@testset "Table: player position" begin + players = ntuple(i-> NLH.Player(BotRandom(), i), 5) + table = Table(;players = players, button_id = NLH.button_id()) + + @test NLH.position(table, players[1], -5) == 1 + @test NLH.position(table, players[1], -4) == 2 + @test NLH.position(table, players[1], -3) == 3 + @test NLH.position(table, players[1], -2) == 4 + @test NLH.position(table, players[1], -1) == 5 + @test NLH.position(table, players[1], 0) == 1 + @test NLH.position(table, players[1], 1) == 2 + @test NLH.position(table, players[1], 2) == 3 + @test NLH.position(table, players[1], 3) == 4 + @test NLH.position(table, players[1], 4) == 5 + @test NLH.position(table, players[1], 5) == 1 + + @test NLH.position(table, players[2], -5) == 2 + @test NLH.position(table, players[2], -4) == 3 + @test NLH.position(table, players[2], -3) == 4 + @test NLH.position(table, players[2], -2) == 5 + @test NLH.position(table, players[2], -1) == 1 + @test NLH.position(table, players[2], 0) == 2 + @test NLH.position(table, players[2], 1) == 3 + @test NLH.position(table, players[2], 2) == 4 + @test NLH.position(table, players[2], 3) == 5 + @test NLH.position(table, players[2], 4) == 1 + @test NLH.position(table, players[2], 5) == 2 +end + +@testset "Table: Button iterator" begin + + @test NLH.button_id() == 1 + + players = ntuple(i-> NLH.Player(BotRandom(), i), 5) + table = Table(;players = players, button_id = NLH.button_id()) + NLH.deal!(table, NLH.blinds(table)) + + # button_id = 1 + state = 0 + for player in NLH.circle(table, Button()) + state+=1 + @test player.id == state + state == length(players) && break + end + @test state==length(players) + + # button_id = 2 + players = ntuple(i-> NLH.Player(BotRandom(), i), 5) + table = Table(;players = players, button_id = 2) + NLH.deal!(table, NLH.blinds(table)) + state = 0 + for player in NLH.circle(table, Button()) + state+=1 + state == 1 && @test player.id == 2 + state == 2 && @test player.id == 3 + state == 3 && @test player.id == 4 + state == 4 && @test player.id == 5 + state == 5 && @test player.id == 1 + state == length(players) && break + end + @test state==length(players) +end + +@testset "Table: SmallBlind iterator" begin + + @test NLH.button_id() == 1 + + players = ntuple(i-> NLH.Player(BotRandom(), i), 5) + table = Table(;players = players, button_id = NLH.button_id()) + NLH.deal!(table, NLH.blinds(table)) + + # button_id = 1 + state = 0 + for player in NLH.circle(table, SmallBlind()) + state+=1 + state == 1 && @test player.id == 2 + state == 2 && @test player.id == 3 + state == 3 && @test player.id == 4 + state == 4 && @test player.id == 5 + state == 5 && @test player.id == 1 + state == length(players) && break + end + @test state==length(players) + + # button_id = 2 + players = ntuple(i-> NLH.Player(BotRandom(), i), 5) + table = Table(;players = players, button_id = 2) + NLH.deal!(table, NLH.blinds(table)) + state = 0 + for player in NLH.circle(table, SmallBlind()) + state+=1 + state == 1 && @test player.id == 3 + state == 2 && @test player.id == 4 + state == 3 && @test player.id == 5 + state == 4 && @test player.id == 1 + state == 5 && @test player.id == 2 + state == length(players) && break + end + @test state==length(players) +end + +@testset "Table: BigBlind iterator" begin + + @test NLH.button_id() == 1 + + players = ntuple(i-> NLH.Player(BotRandom(), i), 5) + table = Table(;players = players, button_id = NLH.button_id()) + NLH.deal!(table, NLH.blinds(table)) + + # button_id = 1 + state = 0 + for player in NLH.circle(table, BigBlind()) + state+=1 + state == 1 && @test player.id == 3 + state == 2 && @test player.id == 4 + state == 3 && @test player.id == 5 + state == 4 && @test player.id == 1 + state == 5 && @test player.id == 2 + state == length(players) && break + end + @test state==length(players) + + # button_id = 2 + players = ntuple(i-> NLH.Player(BotRandom(), i), 5) + table = Table(;players = players, button_id = 2) + NLH.deal!(table, NLH.blinds(table)) + state = 0 + for player in NLH.circle(table, BigBlind()) + state+=1 + state == 1 && @test player.id == 4 + state == 2 && @test player.id == 5 + state == 3 && @test player.id == 1 + state == 4 && @test player.id == 2 + state == 5 && @test player.id == 3 + state == length(players) && break + end + @test state==length(players) +end + +@testset "Table: iterate from player" begin + + @test NLH.button_id() == 1 + players = ntuple(i-> NLH.Player(BotRandom(), i), 5) + table = Table(;players = players, button_id = NLH.button_id()) + NLH.deal!(table, NLH.blinds(table)) + # button_id = 1 + state = 0 + for player in NLH.circle(table, players[1]) + state+=1 + @test player.id == state + state == length(players) && break + end + @test state==length(players) + + players = ntuple(i-> NLH.Player(BotRandom(), i), 5) + table = Table(;players = players, button_id = 2) + NLH.deal!(table, NLH.blinds(table)) + # button_id = 2 + state = 0 + for player in NLH.circle(table, players[1]) + state+=1 + @test player.id == state + state == length(players) && break + end + @test state==length(players) + + players = ntuple(i-> NLH.Player(BotRandom(), i), 5) + table = Table(;players = players, button_id = NLH.button_id()) + NLH.deal!(table, NLH.blinds(table)) + # button_id = 1 + state = 0 + for player in NLH.circle(table, players[2]) + state+=1 + state == 1 && @test player.id == 2 + state == 2 && @test player.id == 3 + state == 3 && @test player.id == 4 + state == 4 && @test player.id == 5 + state == 5 && @test player.id == 1 + state == length(players) && break + end + @test state==length(players) + + players = ntuple(i-> NLH.Player(BotRandom(), i), 5) + table = Table(;players = players, button_id = 2) + NLH.deal!(table, NLH.blinds(table)) + # button_id = 2 + state = 0 + for player in NLH.circle(table, players[2]) + state+=1 + state == 1 && @test player.id == 2 + state == 2 && @test player.id == 3 + state == 3 && @test player.id == 4 + state == 4 && @test player.id == 5 + state == 5 && @test player.id == 1 + state == length(players) && break + end + @test state==length(players) +end diff --git a/test/transactions.jl b/test/transactions.jl index fdf89661..e8590d02 100644 --- a/test/transactions.jl +++ b/test/transactions.jl @@ -1,6 +1,6 @@ using Test using PlayingCards -using NoLimitHoldem +using NoLimitHoldem: Player, BotRandom, TransactionManager, button_id, Table NLH = NoLimitHoldem @testset "TransactionManagers - Lowest bank roll goes all-in and wins it all" begin @@ -11,15 +11,17 @@ NLH = NoLimitHoldem NLH.Player(BotRandom(), 3, (Q♠, Q♣); bank_roll = 3*100), ) tm = NLH.TransactionManager(players) + table = Table(;players=players,cards=table_cards,transactions=tm) @test NLH.player_id.(tm.side_pots) == [1,2,3] - NLH.contribute!(tm, players[1], 100) # all-in - NLH.contribute!(tm, players[2], 100) # call - NLH.contribute!(tm, players[3], 100) # call - NLH.side_pot_full!(tm) - @test NLH.sidepot_winnings(tm, 1) == 300 - @test NLH.sidepot_winnings(tm, 2) == 300 # no increase + + NLH.raise!(table, players[1], 100) # raise all-in + NLH.call!(table, players[2], 100) # call + NLH.call!(table, players[3], 100) # call + + @test NLH.amount.(tm.side_pots) == [300.0, 0.0, 0.0] NLH.distribute_winnings!(players, tm, table_cards) - @test NLH.sidepot_winnings(tm, 3) == 0 + @test NLH.amount.(tm.side_pots) == [0.0, 0.0, 0.0] + @test bank_roll(players[1]) == 300 @test bank_roll(players[2]) == 100 @test bank_roll(players[3]) == 200 @@ -33,14 +35,16 @@ end NLH.Player(BotRandom(), 3, (Q♠, Q♣); bank_roll = 1*100), ) tm = NLH.TransactionManager(players) - NLH.contribute!(tm, players[1], 100) # call - NLH.contribute!(tm, players[2], 100) # call - NLH.contribute!(tm, players[3], 100) # all-in - NLH.side_pot_full!(tm) - @test NLH.sidepot_winnings(tm, 1) == 300 - @test NLH.sidepot_winnings(tm, 2) == 300 # no increase + table = Table(;players=players,cards=table_cards,transactions=tm) + + NLH.raise!(table, players[1], 100) # Raise + NLH.call!(table, players[2], 100) # call + NLH.call!(table, players[3], 100) # all-in + + @test NLH.amount.(tm.side_pots) == [300.0, 0.0, 0.0] NLH.distribute_winnings!(players, tm, table_cards) - @test NLH.sidepot_winnings(tm, 3) == 0 + @test NLH.amount.(tm.side_pots) == [0.0, 0.0, 0.0] + @test bank_roll(players[1]) == 500 @test bank_roll(players[2]) == 100 @test bank_roll(players[3]) == 0 @@ -54,22 +58,25 @@ end NLH.Player(BotRandom(), 3, (Q♠, Q♣); bank_roll = 3*100), ) tm = NLH.TransactionManager(players) - NLH.contribute!(tm, players[1], 100) # all-in - NLH.contribute!(tm, players[2], 100) # call - NLH.contribute!(tm, players[3], 100) # call - NLH.side_pot_full!(tm) - @test NLH.sidepot_winnings(tm, 1) == 300 - @test NLH.sidepot_winnings(tm, 2) == 300 # no increase + table = Table(;players=players,cards=table_cards,transactions=tm) + + NLH.raise!(table, players[1], 100) # Raise all-in + NLH.call!(table, players[2], 100) # call + NLH.call!(table, players[3], 100) # call - @test_throws AssertionError NLH.contribute!(tm, players[1], 100) # already all-in! - NLH.contribute!(tm, players[2], 100) # all-in - NLH.contribute!(tm, players[3], 100) # call + @test NLH.amount.(tm.side_pots) == [300.0, 0.0, 0.0] - @test NLH.sidepot_winnings(tm, 1) == 300 - @test NLH.sidepot_winnings(tm, 2) == 500 + @test_throws ErrorException NLH.call!(table, players[1], 100) # already all-in! + NLH.reset_round!(table) + + NLH.raise!(table, players[2], 100) # Raise all-in + NLH.call!(table, players[3], 100) # call + + @test NLH.amount.(tm.side_pots) == [300.0, 200.0, 0.0] NLH.distribute_winnings!(players, tm, table_cards) - @test NLH.sidepot_winnings(tm, 3) == 0 + @test NLH.amount.(tm.side_pots) == [0.0, 0.0, 0.0] + @test bank_roll(players[1]) == 300 @test bank_roll(players[2]) == 200 @test bank_roll(players[3]) == 100 @@ -83,22 +90,25 @@ end NLH.Player(BotRandom(), 3, (Q♠, Q♣); bank_roll = 1*100), ) tm = NLH.TransactionManager(players) - NLH.contribute!(tm, players[1], 100) # call - NLH.contribute!(tm, players[2], 100) # call - NLH.contribute!(tm, players[3], 100) # all-in - NLH.side_pot_full!(tm) - @test NLH.sidepot_winnings(tm, 1) == 300 - @test NLH.sidepot_winnings(tm, 2) == 300 # no increase + table = Table(;players=players,cards=table_cards,transactions=tm) + + NLH.raise!(table, players[1], 100) # Raise + NLH.call!(table, players[2], 100) # call + NLH.call!(table, players[3], 100) # all-in + + @test NLH.amount.(tm.side_pots) == [300.0, 0.0, 0.0] + + NLH.reset_round!(table) - NLH.contribute!(tm, players[1], 100) # call - NLH.contribute!(tm, players[2], 100) # all-in - @test_throws AssertionError NLH.contribute!(tm, players[3], 100) # already all-in! + NLH.raise!(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! - @test NLH.sidepot_winnings(tm, 1) == 300 - @test NLH.sidepot_winnings(tm, 2) == 500 + @test NLH.amount.(tm.side_pots) == [300.0, 200.0, 0.0] NLH.distribute_winnings!(players, tm, table_cards) - @test NLH.sidepot_winnings(tm, 3) == 0 + @test NLH.amount.(tm.side_pots) == [0.0, 0.0, 0.0] + @test bank_roll(players[1]) == 600 @test bank_roll(players[2]) == 0 @test bank_roll(players[3]) == 0 @@ -115,40 +125,93 @@ end NLH.Player(BotRandom(), 6, (2♠, 3♣); bank_roll = 6*100), # lose, but not bust ) tm = NLH.TransactionManager(players) - NLH.contribute!(tm, players[1], 100) # all-in - NLH.contribute!(tm, players[2], 100) # call - NLH.contribute!(tm, players[3], 100) # call - NLH.contribute!(tm, players[4], 100) # call - NLH.contribute!(tm, players[5], 100) # call - NLH.contribute!(tm, players[6], 100) # call - NLH.side_pot_full!(tm) - NLH.contribute!(tm, players[2], 100) # all-in - NLH.contribute!(tm, players[3], 100) # call - NLH.contribute!(tm, players[4], 100) # call - NLH.contribute!(tm, players[5], 100) # call - NLH.contribute!(tm, players[6], 100) # call - NLH.side_pot_full!(tm) - NLH.contribute!(tm, players[3], 100) # all-in - NLH.contribute!(tm, players[4], 100) # call - NLH.contribute!(tm, players[5], 100) # call - NLH.contribute!(tm, players[6], 100) # call - NLH.side_pot_full!(tm) - NLH.contribute!(tm, players[4], 100) # all-in - NLH.contribute!(tm, players[5], 100) # call - NLH.contribute!(tm, players[6], 100) # call - NLH.side_pot_full!(tm) - NLH.contribute!(tm, players[5], 100) # all-in - NLH.contribute!(tm, players[6], 100) # call - - @test NLH.sidepot_winnings(tm, 1) ≈ 600.0 - @test NLH.sidepot_winnings(tm, 2) ≈ 1100.0 - @test NLH.sidepot_winnings(tm, 3) ≈ 1500.0 - @test NLH.sidepot_winnings(tm, 4) ≈ 1800.0 - @test NLH.sidepot_winnings(tm, 5) ≈ 2000.0 - @test NLH.sidepot_winnings(tm, 6) ≈ 2000.0 + table = Table(;players=players,cards=table_cards,transactions=tm) + + NLH.raise!(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 + @test NLH.amount.(tm.side_pots) == [600.0, 0.0, 0.0, 0.0, 0.0, 0.0] + + NLH.reset_round!(table) + + NLH.raise!(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 + @test NLH.amount.(tm.side_pots) == [600.0, 500.0, 0.0, 0.0, 0.0, 0.0] + + NLH.reset_round!(table) + + NLH.raise!(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 + @test NLH.amount.(tm.side_pots) == [600.0, 500.0, 400.0, 0.0, 0.0, 0.0] + + NLH.reset_round!(table) + + NLH.raise!(table, players[4], 100) # raise all-in + NLH.call!(table, players[5], 100) # call + NLH.call!(table, players[6], 100) # 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!(table, players[5], 100) # raise all-in + NLH.call!(table, players[6], 100) # 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) + @test NLH.amount.(tm.side_pots) == [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + + @test bank_roll(players[1]) == 0 # bust + @test bank_roll(players[2]) == 550 # = 600/2+500/2 + @test bank_roll(players[3]) == 950 # = 600/2+500/2+400 + @test bank_roll(players[4]) == 0 # bust + @test bank_roll(players[5]) == 500 # all contributions after 3rd all-in (3*100+2*100) + @test bank_roll(players[6]) == 100 # lost (but not all-in) + @test sum(bank_roll.(players)) == 2100 +end + +@testset "TransactionManagers - Single round split pot (shared winners), with simple re-raises" begin + table_cards = (T♢, Q♢, A♠, 8♠, 9♠) + players = ( + NLH.Player(BotRandom(), 1, (4♠, 5♣); bank_roll = 1*100), # bust + NLH.Player(BotRandom(), 2, (K♠, K♣); bank_roll = 2*100), # win, split with player 3 + NLH.Player(BotRandom(), 3, (K♡,K♢); bank_roll = 3*100), # win, split with player 2 + NLH.Player(BotRandom(), 4, (2♡, 3♢); bank_roll = 4*100), # bust + NLH.Player(BotRandom(), 5, (7♠, 7♣); bank_roll = 5*100), # 2nd to players 2 and 3, win remaining pot + NLH.Player(BotRandom(), 6, (2♠, 3♣); bank_roll = 6*100), # lose, but not bust + ) + tm = NLH.TransactionManager(players) + table = Table(;players=players,cards=table_cards,transactions=tm) + @test NLH.amount.(tm.side_pots) == [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + + NLH.raise!(table, players[1], 100) # raise all-in + @test NLH.amount.(tm.side_pots) == [100.0, 0.0, 0.0, 0.0, 0.0, 0.0] + + NLH.raise!(table, players[2], 200) # raise all-in + @test NLH.amount.(tm.side_pots) == [200.0, 100.0, 0.0, 0.0, 0.0, 0.0] + + NLH.raise!(table, players[3], 300) # raise all-in + @test NLH.amount.(tm.side_pots) == [300.0, 200.0, 100.0, 0.0, 0.0, 0.0] + + NLH.raise!(table, players[4], 400;debug=true) # raise all-in + @test NLH.amount.(tm.side_pots) == [400.0, 300.0, 200.0, 100.0, 0.0, 0.0] + + NLH.raise!(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 + @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) - @test NLH.sidepot_winnings(tm, 6) == 0 + @test NLH.amount.(tm.side_pots) == [0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + @test bank_roll(players[1]) == 0 # bust @test bank_roll(players[2]) == 550 # = 600/2+500/2 @test bank_roll(players[3]) == 950 # = 600/2+500/2+400