Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Several logic fixes and improvements #44

Merged
merged 1 commit into from
May 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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