Skip to content

Commit

Permalink
Merge #195
Browse files Browse the repository at this point in the history
195: Add `recreate_game` r=charleskawczynski a=charleskawczynski

`recreate_game` creates an exact copy of a given game, and then re-samples unobserved cards (from a given player's perspective).

Co-authored-by: Charles Kawczynski <kawczynski.charles@gmail.com>
  • Loading branch information
bors[bot] and charleskawczynski committed Aug 6, 2023
2 parents 9a6dea5 + e2f7a8b commit 505a6f3
Show file tree
Hide file tree
Showing 9 changed files with 149 additions and 3 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "TexasHoldem"
uuid = "6cef90fc-eb55-4a2a-97d0-7ecce2b738fe"
authors = ["Charles Kawczynski <kawczynski.charles@gmail.com>"]
version = "0.3.0"
version = "0.3.1"

[deps]
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
Expand Down
8 changes: 8 additions & 0 deletions docs/src/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,11 @@ TexasHoldem.CallAllInFold
TexasHoldem.CallFold
TexasHoldem.player_option
```

## Training

There are additional functionalities added for training purposes

```@docs
TexasHoldem.recreate_game
```
1 change: 1 addition & 0 deletions src/TexasHoldem.jl
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,6 @@ include("game.jl")
include("player_actions.jl")
include("player_options.jl")
include("config_game.jl")
include("recreate.jl")

end # module
64 changes: 64 additions & 0 deletions src/recreate.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@

function resample_unobserved_table_cards!(table::Table, round::PreFlop)
for c in table.cards
PlayingCards.restore!(table.deck, c)
end
table.cards = ntuple(i->SB.sample!(table.deck), 5)
return nothing
end
function resample_unobserved_table_cards!(table::Table, round::Flop)
@inbounds PlayingCards.restore!(table.deck, table.cards[4])
@inbounds PlayingCards.restore!(table.deck, table.cards[5])
@inbounds table.cards = (
table.cards[1:3]...,
SB.sample!(table.deck),
SB.sample!(table.deck),
)
return nothing
end
function resample_unobserved_table_cards!(table::Table, round::Turn)
@inbounds PlayingCards.restore!(table.deck, table.cards[5])
@inbounds table.cards = (
table.cards[1:4]...,
SB.sample!(table.deck),
)
return nothing
end
resample_unobserved_table_cards!(table::Table, round::River) = nothing

function resample_player_cards!(table::Table, player::Player)
@assert player.cards (nothing, nothing)
for c in player.cards
PlayingCards.restore!(table.deck, c)
end
player.cards = (SB.sample!(table.deck), SB.sample!(table.deck))
return nothing
end
function resample_cards!(game::Game, player::Player)
table = game.table
for opponent in players_at_table(table)
seat_number(opponent) == seat_number(player) && continue
resample_player_cards!(table, opponent)
end
resample_unobserved_table_cards!(table, table.round)
end

"""
recreate_game(game, player)
Creates an exact (deep)copy of the input game,
and then re-samples the _unobserved_ cards (i.e.,
the opponent cards and unobserved table cards).
This is useful for repeatedly sampling a specific
scenario so that we can compute the expected value
of a particular action.
TODO: we could make a mutating, in-place version of
this to reduce allocations.
"""
function recreate_game(game, player)
rgame = deepcopy(game)
resample_cards!(rgame, player)
return rgame
end
10 changes: 9 additions & 1 deletion src/table.jl
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,19 @@ end
get_table_cards!(deck::PlayingCards.MaskedDeck) = ntuple(_->SB.sample!(deck), Val(5))::Tuple{Card, Card, Card, Card, Card}
cards(table::Table) = table.cards

observed_cards(table::Table) = observed_cards(table, table.round)
observed_cards(table::Table) = observed_cards(table, round(table))
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

# for testing
unobserved_cards(table::Table) = unobserved_cards(table, round(table))
unobserved_cards(table::Table, ::PreFlop) = table.cards
unobserved_cards(table::Table, ::Flop) = table.cards[4:5]
unobserved_cards(table::Table, ::Turn) = (table.cards[5],)
unobserved_cards(table::Table, ::River) = ()

current_raise_amt(table::Table) = table.current_raise_amt
initial_round_raise_amt(table::Table) = table.initial_round_raise_amt
minimum_raise_amt(table::Table) = blinds(table).small
Expand Down
1 change: 0 additions & 1 deletion test/perf.jl
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,4 @@ nef = get(n_expected_failures, VERSION, minimum(values(n_expected_failures)))
n = @n_failures do_work!(game)
@test n nef
n < nef && @show n
@test_broken n < nef
end
59 changes: 59 additions & 0 deletions test/recreate.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using Test
using PlayingCards
using TexasHoldem
import TexasHoldem
const TH = TexasHoldem
import Random
Random.seed!(1234)

include("tester_bots.jl")

sort_cards(cards) =
sort(sort(collect(cards); by=x->PlayingCards.rank(x));by=x->PlayingCards.suit(x).i)

@testset "Recreate game" begin
players = TH.Players(ntuple(i->Player(BotCheckCall(), i), 3))
game = Game(players)
TH.deal!(game.table, TH.blinds(game.table))
player = players[1]
rgame = TH.recreate_game(game, player)

# deep copy, should be pointing to separate memory
@test !(player === rgame.table.players[1])

# For a recreated game:
# - player[1]'s cards should be the same for every sample
# - Opponents cards should be different upon different sampling
# - visible table cards should be the same
# - unobserved table cards should be different
opponent_cards = players[2].cards
n_player_cards_variation = 0
n_unobserved_opponent_cards_variation = 0
n_observed_table_variation = 0
n_unobserved_table_variation = 0
n_samples = 10_000
for i in 1:n_samples
rgame = TH.recreate_game(game, player)
if !all(sort_cards(rgame.table.players[1].cards) .== sort_cards(player.cards))
n_player_cards_variation+=1
end
if !all(sort_cards(rgame.table.players[2].cards) .== sort_cards(opponent_cards))
n_unobserved_opponent_cards_variation+=1
end
if !all(sort_cards(TH.observed_cards(rgame.table)) .== sort_cards(TH.observed_cards(game.table)))
n_observed_table_variation+=1
end
if !all(sort_cards(TH.unobserved_cards(rgame.table)) .== sort_cards(TH.unobserved_cards(game.table)))
n_unobserved_table_variation+=1
end
end
@test n_player_cards_variation == 0
@test n_unobserved_opponent_cards_variation 0
@test n_unobserved_opponent_cards_variation > n_samples/25 # we shouldn't be getting the same hand more than 25% of the time
@test n_unobserved_opponent_cards_variation n_samples # sometimes (but very rarely) we get the same hand!
@test n_observed_table_variation == 0

@test n_unobserved_table_variation 0
@test n_unobserved_table_variation > n_samples/25 # we shouldn't be getting the same table cards more than 25% of the time
# @test n_unobserved_table_variation ≠ n_samples # we need n_samples ~ 3*10^9 to test this, and that would take ~10 hours on my local machine :(
end
3 changes: 3 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ end
@safetestset "game" begin
Δt = @elapsed include("game.jl"); @info "Completed tests for game in $Δt seconds"
end
@safetestset "recreate" begin
Δt = @elapsed include("recreate.jl"); @info "Completed tests for recreate in $Δt seconds"
end
@safetestset "play" begin
Δt = @elapsed include("play.jl"); @info "Completed tests for play in $Δt seconds"
end
Expand Down
4 changes: 4 additions & 0 deletions test/table.jl
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,16 @@ const TH = TexasHoldem

table.round = PreFlop()
@test TH.observed_cards(table) == ()
@test TH.unobserved_cards(table) == table.cards
table.round = Flop()
@test TH.observed_cards(table) == table.cards[1:3]
@test TH.unobserved_cards(table) == table.cards[4:5]
table.round = Turn()
@test TH.observed_cards(table) == table.cards[1:4]
@test TH.unobserved_cards(table) == (table.cards[5],)
table.round = River()
@test TH.observed_cards(table) == table.cards
@test TH.unobserved_cards(table) == ()
end

@testset "Table: Move button" begin
Expand Down

2 comments on commit 505a6f3

@charleskawczynski
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/89154

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.3.1 -m "<description of version>" 505a6f354d98194d3cc68c74994632f3d366b08a
git push origin v0.3.1

Please sign in to comment.