Skip to content

Commit

Permalink
Merge #44
Browse files Browse the repository at this point in the history
44: Several logic fixes and improvements r=charleskawczynski a=charleskawczynski

 - Fix blinds: now everyone must call to see flop
 - Improve call interface (no more need for amount)
 - Add func to validate raise amount
 - Fix raise logic (no double counting)
 - Fix player option dispatch (added CallFold)
 - Improve log (include profit in addition to pot amount)
 - Fix distribute_winnings! when only 1 player remains

Co-authored-by: Charles Kawczynski <kawczynski.charles@gmail.com>
  • Loading branch information
bors[bot] and charleskawczynski committed May 8, 2021
2 parents 881d724 + 4fdf93d commit a085650
Show file tree
Hide file tree
Showing 11 changed files with 333 additions and 117 deletions.
1 change: 1 addition & 0 deletions docs/src/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

```@docs
NoLimitHoldem
NoLimitHoldem.raise_to!
NoLimitHoldem.move_button!
NoLimitHoldem.play
```
26 changes: 23 additions & 3 deletions src/game.jl
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ function Game(players::Tuple;

n_player_cards = sum(map(x->cards(x)==nothing ? 0 : length(cards(x)), players))

@assert 2 length(players) 10

if length(deck) 52
# if the deck isn't full, then players should have been dealt cards.
@assert n_player_cards > 0
Expand Down Expand Up @@ -61,27 +63,44 @@ print_new_cards(table, state::Flop) = @info "Flop: $(repeat(" ", 44)) $(table.c
print_new_cards(table, state::Turn) = @info "Turn: $(repeat(" ", 44)) $(table.cards[4])"
print_new_cards(table, state::River) = @info "River: $(repeat(" ", 43)) $(table.cards[5])"

force_blind_raise!(table::Table, player, ::AbstractGameState, i::Int) = nothing
function force_blind_raise!(table::Table, player::Player, ::PreFlop, i::Int)
if 1 i length(players_at_table(table))
# TODO: what if only two players?
if is_first_to_act(table, player)
# everyone must call big blind to see flop:
table.current_raise_amt = blinds(table).big
end
end
end
reset_round_bank_rolls!(game::Game, state::PreFlop) = nothing # called separately prior to deal
reset_round_bank_rolls!(game::Game, state::AbstractGameState) = reset_round_bank_rolls!(game.table)

function act_generic!(game::Game, state::AbstractGameState)
table = game.table
table.winners.declared && return # TODO: is this redundant?
table.winners.declared && return
set_state!(game.table, state)
print_new_cards(game.table, state)
reset_round_bank_rolls!(game, state)

any_actions_required(game) || return
for player in circle(table, FirstToAct())
for (i, player) in enumerate(circle(table, FirstToAct()))
force_blind_raise!(table, player, state, i)
@debug "Checking to see if it's $(name(player))'s turn to act"
@debug " all_in.(players_at_table(table)) = $(all_in.(players_at_table(table)))"
@debug " last_to_raise.(players_at_table(table)) = $(last_to_raise.(players_at_table(table)))"
@debug " checked.(players_at_table(table)) = $(checked.(players_at_table(table)))"
@debug " folded.(players_at_table(table)) = $(folded.(players_at_table(table)))"
@debug " action_required.(players_at_table(table)) = $(action_required.(players_at_table(table)))"
@debug " !any(action_required.(players_at_table(table))) = $(!any(action_required.(players_at_table(table))))"
@debug " all_all_in_or_folded(table) = $(all_all_in_or_folded(table))"
@debug " all_checked_or_folded(table) = $(all_checked_or_folded(table))"
last_to_raise(player) && break
all_checked_or_folded(table) && break
all_all_in(table) && break
all_all_in_or_folded(table) && break
!any(action_required.(players_at_table(table))) && break
folded(player) && continue
all_in(player) && continue
@debug "$(name(player))'s turn to act"
player_option!(game, player)
table.winners.declared && break
Expand Down Expand Up @@ -117,6 +136,7 @@ function play(game::Game)

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

Expand Down
109 changes: 101 additions & 8 deletions src/player_actions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,20 @@ end
##### Call
#####

call!(game::Game, player::Player, amt) = call!(game.table, player, amt)
call!(game::Game, player::Player) = call!(game.table, player)

function call!(table::Table, player::Player, amt)
function call!(table::Table, player::Player)
cra = table.current_raise_amt
pc = player.round_contribution
call_amt = cra - pc
if call_amt bank_roll(player)
call_valid_amount!(table, player, call_amt)
else
call_valid_amount!(table, player, bank_roll(player))
end
end

function call_valid_amount!(table::Table, player::Player, amt)
@debug "$(name(player)) calling $(amt)."
push!(player.action_history, Call(amt))
player.action_required = false
Expand All @@ -58,15 +69,93 @@ end
##### Raise
#####

raise_to!(game::Game, player::Player, amt) = raise_to!(game.table, player, amt)
"""
bound_raise(table::Table, player::Player, amt)
Given a raise amount `amt`, return a valid raise
amount by ensuring the raise:
- is at least the small blind
- is less than the player's bank roll
- is twice the current raise amount
"""
function bound_raise(table::Table, player::Player, amt)
@debug "Bounding raise amout. Input amount = \$$(amt)"
amt = max(amt, 2*table.current_raise_amt)
amt = min(amt, round_bank_roll(player))
amt = max(amt, blinds(table).small)
@debug "Bounding raise amout. Output amount = \$$(amt)"
return amt
end

# TODO: add assertion that raise amount must be
# greater than small blind (unless all-in).
function raise_to!(table::Table, player::Player, amt)
"""
valid_raise_amount(table::Table, player::Player, amt)
Return back `amt` if `amt` is a valid raise amount.
## Scenario 1 (`2*current_raise_amt > bank_roll(player)`) - only _raise_ option is all-in
## Scenario 2 (`2*current_raise_amt < bank_roll(player)`) - raise option has a range
"""
function valid_raise_amount(table::Table, player::Player, amt)
@assert !(amt 0)
@assert amt round_bank_roll(player)
cra = table.current_raise_amt
pc = player.round_contribution
br = round_bank_roll(player)
if cra 0 # initial raise
rb = (blinds(table).small, br) # raise bounds
else # re-raise
if br > 2*cra
rb = (2*cra, br) # raise bounds
else
rb = (br, br) # raise bounds
end
end
# @assert amt_required_to_call > 0 # right?
@debug "Attempting to raise to \$$(amt), already contributed \$$(pc). Valid raise bounds: [\$$(rb[1]), \$$(rb[2])]"
if !(rb[1] amt rb[2] || amt rb[1] rb[2])
@debug "cra = $cra"
@debug "amt = $amt"
@debug "br = $br"
@debug "amt ≈ br = $(amt br)"
@debug "2*cra ≤ amt ≤ br = $(2*cra amt br)"
end
@assert rb[1] amt rb[2] || amt rb[1] rb[2]
@assert amt - pc > 0 # contribution amount must be > 0!
return amt
end

"""
raise_to!(game::Game, player::Player, amt)
Raise bet, for the _round_, to amount `amt`. Example:
```
# Flop
Player[1] raise to 10 (`amt = 10`, contribute 10)
Player[2] raise to 20 (`amt = 20`, contribute 20)
Player[3] raise to 40 (`amt = 40`, contribute 40)
Player[1] raise to 80 (`amt = 80`, contribute 80-10=70)
Player[2] call
Player[3] call
# Turn
Player[1] raise to 1 (`amt = 1`, contribute 1)
Player[2] raise to 2 (`amt = 2`, contribute 2)
Player[3] raise to 4 (`amt = 4`, contribute 4)
Player[1] raise to 8 (`amt = 8`, contribute 8-1=7)
Player[2] call
Player[3] call
```
"""
raise_to!(game::Game, player::Player, amt) = raise_to!(game.table, player, amt)

raise_to!(table::Table, player::Player, amt) =
raise_to_valid_raise_amount!(table, player, valid_raise_amount(table, player, amt))

function raise_to_valid_raise_amount!(table::Table, player::Player, amt)
@debug "$(name(player)) raising to $(amt)."
@assert !(amt 0) # more checks are performed in `contribute!`
contribute!(table, player, amt, false)
table.current_raise_amt += amt
pc = player.round_contribution
contribute!(table, player, amt - pc, false)
table.current_raise_amt = amt

push!(player.action_history, Raise(amt))
player.action_required = false
Expand All @@ -78,5 +167,9 @@ function raise_to!(table::Table, player::Player, amt)
oponent.action_required = true
oponent.last_to_raise = false
end
@info "$(name(player)) raised to $(amt)."
if bank_roll(player) 0
@info "$(name(player)) raised to $(amt) (all-in)."
else
@info "$(name(player)) raised to $(amt)."
end
end
74 changes: 50 additions & 24 deletions src/player_options.jl
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
abstract type PlayerOptions end
struct CheckRaiseFold <: PlayerOptions end
struct CallRaiseFold <: PlayerOptions end
struct CallAllInFold <: PlayerOptions end # TODO: maybe useful?
struct CallFold <: PlayerOptions end
struct PayBlindSitOut <: PlayerOptions end

function player_option!(game::Game, player::Player)
if any(last_to_raise.(players_at_table(game)))
player_option!(game, player, CallRaiseFold())
cra = game.table.current_raise_amt
pc = player.round_contribution
cra 0 && (@assert pc 0)
call_amt = cra - pc
@debug "player_option! cra = $cra, pc = $pc, call_amt = $call_amt, !(call_amt ≈ 0) = $(!(call_amt 0))"
if !(call_amt 0)
pc = player.round_contribution
amt_required_to_call = cra-pc # (i.e., call amount)
raise_possible = bank_roll(player) > amt_required_to_call
@debug "raise_possible = $raise_possible"
if raise_possible # all-in or fold
player_option!(game, player, CallRaiseFold())
else
player_option!(game, player, CallFold())
end
else
player_option!(game, player, CheckRaiseFold())
end
Expand All @@ -29,31 +44,40 @@ function player_option!(game::Game, player::Player{Human}, ::CheckRaiseFold)
choice = request("$(name(player))'s turn to act:", menu)
choice == -1 && error("Uncaught case")
choice == 1 && check!(game, player)
choice == 2 && raise_to!(game, player, input_raise_amt(player))
choice == 2 && raise_to!(game, player, input_raise_amt(game.table, player))
choice == 3 && fold!(game, player)
end
function player_option!(game::Game, player::Player{Human}, ::CallRaiseFold)
options = ["Call", "Raise", "Fold"]
menu = RadioMenu(options, pagesize=4)
choice = request("$(name(player))'s turn to act:", menu)
choice == -1 && error("Uncaught case")
choice == 1 && call!(game, player, game.table.current_raise_amt)
choice == 2 && raise_to!(game, player, input_raise_amt(player))
choice == 1 && call!(game, player)
choice == 2 && raise_to!(game, player, input_raise_amt(game.table, player))
choice == 3 && fold!(game, player)
end
function player_option!(game::Game, player::Player{Human}, ::CallFold)
options = ["Call", "Fold"]
menu = RadioMenu(options, pagesize=4)
choice = request("$(name(player))'s turn to act:", menu)
choice == -1 && error("Uncaught case")
choice == 1 && call!(game, player)
choice == 2 && fold!(game, player)
end

function input_raise_amt(player::Player{Human})
function input_raise_amt(table, player::Player{Human})
raise_amt = nothing
while true
println("Enter raise amt:")
raise_amt = readline()
try
raise_amt = parse(Float64, raise_amt)
raise_amt player.bank_roll && break
println("$(name(player)) doesn't have enough funds (\$$(player.bank_roll)) to bet \$$(raise_amt)")
# TODO: Write `is_valid_raise_amount`, and use for better error messages.
catch
println("Raise must be a Float64")
end
raise_amt = valid_raise_amount(table, player, raise_amt)
break
end
@assert raise_amt nothing
return raise_amt
Expand All @@ -72,18 +96,14 @@ player_option(player::Player{BotSitOut}, ::PayBlindSitOut) = SitOut() # no other
player_option(player::Player{BotCheckFold}, ::PayBlindSitOut) = PayBlind()
player_option!(game::Game, player::Player{BotCheckFold}, ::CheckRaiseFold) = check!(game, player)
player_option!(game::Game, player::Player{BotCheckFold}, ::CallRaiseFold) = fold!(game, player)
player_option!(game::Game, player::Player{BotCheckFold}, ::CallFold) = fold!(game, player)

##### BotCheckCall

player_option(player::Player{BotCheckCall}, ::PayBlindSitOut) = PayBlind()
player_option!(game::Game, player::Player{BotCheckCall}, ::CheckRaiseFold) = check!(game, player)
function player_option!(game::Game, player::Player{BotCheckCall}, ::CallRaiseFold)
if game.table.current_raise_amt bank_roll(player)
call!(game, player, game.table.current_raise_amt)
else
call!(game, player, bank_roll(player))
end
end
player_option!(game::Game, player::Player{BotCheckCall}, ::CallRaiseFold) = call!(game, player)
player_option!(game::Game, player::Player{BotCheckCall}, ::CallFold) = call!(game, player)

##### BotRandom

Expand All @@ -94,22 +114,28 @@ function player_option!(game::Game, player::Player{BotRandom}, ::CheckRaiseFold)
check!(game, player)
else
amt = Int(round(rand()*bank_roll(player), digits=0))
raise_to!(game, player, min(amt, blinds(game).small))
amt = bound_raise(game.table, player, amt)
raise_to!(game, player, amt)
end
end
function player_option!(game::Game, player::Player{BotRandom}, ::CallRaiseFold)
if rand() < 0.5
# Call
if game.table.current_raise_amt bank_roll(player)
call!(game, player, game.table.current_raise_amt)
else
call!(game, player, bank_roll(player))
end
if rand() < 0.5
if rand() < 0.5 # Call
call!(game, player)
else # re-raise
amt = Int(round(rand()*bank_roll(player), digits=0))
raise_to!(game, player, min(amt, blinds(game).small))
amt = bound_raise(game.table, player, amt)
raise_to!(game, player, amt)
end
else
fold!(game, player)
end
end

function player_option!(game::Game, player::Player{BotRandom}, ::CallFold)
if rand() < 0.5
call!(game, player)
else
fold!(game, player)
end
end
11 changes: 11 additions & 0 deletions src/player_types.jl
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,13 @@ mutable struct Player{LF}
action_history::Vector
action_required::Bool
all_in::Bool
round_bank_roll::Float64
folded::Bool
pot_investment::Float64 # accumulation of round_contribution, TODO: needs to be added to reset_game!
checked::Bool
last_to_raise::Bool
sat_out::Bool
round_contribution::Float64
end

function Base.show(io::IO, player::Player, include_type = true)
Expand All @@ -41,9 +44,12 @@ function Player(life_form, id, cards = nothing; bank_roll = 200)
action_history = []
action_required = true
all_in = false
round_bank_roll = Float64(bank_roll)
folded = false
pot_investment = Float64(0)
checked = false
sat_out = false
round_contribution = Float64(0)
last_to_raise = false
args = (
life_form,
Expand All @@ -53,10 +59,13 @@ function Player(life_form, id, cards = nothing; bank_roll = 200)
action_history,
action_required,
all_in,
round_bank_roll,
folded,
pot_investment,
checked,
last_to_raise,
sat_out,
round_contribution,
)
Player(args...)
end
Expand All @@ -66,9 +75,11 @@ bank_roll(player::Player) = player.bank_roll
player_id(player::Player) = player.id
name(player::Player{LF}) where {LF <: AbstractLifeForm} = "$(nameof(LF))[$(player.id)]"
folded(player::Player) = player.folded
still_playing(player::Player) = !player.folded
action_history(player::Player) = player.action_history
checked(player::Player) = player.checked
last_to_raise(player::Player) = player.last_to_raise
all_in(player::Player) = player.all_in
action_required(player::Player) = player.action_required
sat_out(player::Player) = player.sat_out
round_bank_roll(player::Player) = player.round_bank_roll
Loading

0 comments on commit a085650

Please sign in to comment.