diff --git a/docs/src/api.md b/docs/src/api.md index c6e9043c..8f1df5b2 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -2,4 +2,5 @@ ```@docs NoLimitHoldem.move_button! +NoLimitHoldem.play ``` diff --git a/src/NoLimitHoldem.jl b/src/NoLimitHoldem.jl index 5ebcea3c..f8a6cb2d 100644 --- a/src/NoLimitHoldem.jl +++ b/src/NoLimitHoldem.jl @@ -6,8 +6,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..a144fc5b 100644 --- a/src/config_game.jl +++ b/src/config_game.jl @@ -60,14 +60,11 @@ 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, + return Game(players; deck=deck, blinds=blinds, ) @@ -100,8 +97,7 @@ function configure_custom_game() end end - return Game(; - players=players, + return Game(players; deck=deck, blinds=blinds, ) diff --git a/src/game.jl b/src/game.jl index 175c4b93..93728391 100644 --- a/src/game.jl +++ b/src/game.jl @@ -3,46 +3,6 @@ ##### 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 @@ -58,53 +18,65 @@ 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) + + 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 + state = table.state - targs = (table, players, blinds, winners) current_raise_amt = 0 - args = (state, table, players, blinds, winners, current_raise_amt) + args = (state, table, 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) +n_players(game::Game) = length(players_at_table(game)) +players_at_table(game::Game) = players_at_table(game.table) function declare_winners!(game::Game) - fhe = map(game.players) do player + fhe = map(players_at_table(game)) do player FullHandEval((player.cards..., observed_cards(game.table)...)) end @@ -114,7 +86,7 @@ function declare_winners!(game::Game) @show best_cards.(fhe) min_hr = min(hr...) - game.winners.players = filter(game.players) do player + game.winners.players = filter(players_at_table(game)) do player hr[player.id] == min_hr end game.winners.declared = true @@ -122,61 +94,30 @@ function declare_winners!(game::Game) end function check_for_winner!(game) - game.winners.declared = count(folded.(game.players)) == n_players(game)-1 + game.winners.declared = count(folded.(players_at_table(game))) == n_players(game)-1 if game.winners.declared - for player in game.players + for player in players_at_table(game) folded(player) && continue game.winners.players = player end 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 -end - +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)) - -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 finalize_round!(game::Game) + players = players_at_table(game) + 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 + side_pot_full!(game.table.transactions) end function set_state!(game::Game, state::AbstractGameState) @@ -189,21 +130,19 @@ function act_generic!(game::Game, state::AbstractGameState) 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) + for player in circle(game.table, SmallBlind()) + last_to_raise(player) && break + all_checked_or_folded(game.table) && break + folded(player) && continue + player_option!(game, player) game.winners.declared && break - i+=1 end end function act!(game::Game, state::AbstractGameState) act_generic!(game, state) - reset_actions_required!(game) + finalize_round!(game) end function act!(game::Game, state::River) @@ -211,25 +150,29 @@ function act!(game::Game, state::River) declare_winners!(game) 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 + + act!(game, PreFlop()) # Pre-flop bet/check/raise -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 + game.winners.declared || act!(game, Flop()) # Deal flop , then bet/check/raise + game.winners.declared || act!(game, Turn()) # Deal turn , then bet/check/raise + game.winners.declared || act!(game, River()) # Deal river, then bet/check/raise return game.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..e542bc66 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 @@ -25,24 +27,30 @@ function check!(game::Game, player::Player) push!(player.action_history, Check()) player.action_required = false end + function call!(game::Game, player::Player, amt) push!(player.action_history, Call(amt)) player.action_required = false if player.bank_roll > amt player.bank_roll -= amt - game.table.pot += amt + contribute!(game.table.transactions, player, amt) else player.bank_roll = 0 - game.table.pot += player.bank_roll + contribute!(game.table.transactions, player, player.bank_roll) player.all_in = true end end +# TODO: add assertion that raise amount must be +# greater than small blind (unless all-in). 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) + @assert !(amt ≈ 0) + if 0 ≤ amt ≤ player.bank_roll # successful transaction + contribute!(game.table.transactions, player, amt) + if player.bank_roll ≈ 0 + player.bank_roll = 0 + player.all_in = true + end game.current_raise_amt += amt else msg1 = "Player $(player.id) has insufficient bank" @@ -52,9 +60,11 @@ function raise!(game::Game, player::Player, amt) push!(player.action_history, Raise(amt)) player.action_required = false - for oponent in game.players + player.last_to_raise = true + for oponent in players_at_table(game) 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..9ae84f03 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.current_raise_amt ≤ bank_roll(player) + call!(game, player, game.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,7 +74,7 @@ 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 && error("Uncaught case") choice == 1 && call!(game, player, game.current_raise_amt) choice == 2 && raise!(game, player, input_raise_amt(player)) choice == 3 && fold!(game, player) @@ -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..68308892 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,7 @@ 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 +last_to_raise(player::Player) = player.last_to_raise +checked(player::Player) = player.checked diff --git a/src/table.jl b/src/table.jl new file mode 100644 index 00000000..c5d38d53 --- /dev/null +++ b/src/table.jl @@ -0,0 +1,163 @@ +##### +##### Table +##### + +export Button, SmallBlind, BigBlind +export Table +export move_button! + +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() + transactions::TransactionManager = TransactionManager(players) +end + +function Base.show(io::IO, table::Table, include_type = true) + include_type && println(io, typeof(table)) + show(io, blinds(table), 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 + +##### +##### 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 + +""" + 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 + +circle(table::Table, tp::TablePosition) = + CircleTable{typeof(tp)}(table.players, table.button_id, length(table.players)) + +struct CircleTable{TP <: TablePosition} + players::Tuple + button_id::Int + n_players::Int +end + +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) + +##### +##### Deal +##### + +function deal!(table::Table, blinds::Blinds) + players = players_at_table(table) + shuffle!(table.deck) + + for (i, player) in enumerate(circle(table, SmallBlind())) + if player_option(player, PayBlindSitOut()) isa SitOut + player.folded = true + @info "$(name(player)) sat out a hand." + elseif player_option(player, PayBlindSitOut()) 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.transactions, 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.transactions, 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.transactions, player, blinds.small) + elseif player.id == big_blind(players, table).id + @info "$(name(player)) paid the big blind." + contribute!(table.transactions, 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..62b9b01b 100644 --- a/src/transactions.jl +++ b/src/transactions.jl @@ -26,6 +26,11 @@ struct TransactionManager 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)) TransactionManager( @@ -45,11 +50,20 @@ side_pot_full!(tm::TransactionManager) = (tm.pot_id[1]+=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 +81,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..c67f10cc --- /dev/null +++ b/test/table.jl @@ -0,0 +1,167 @@ +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: 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 +