From 81a14c7ef64e42bda83ea1f45c792e19294a56cd Mon Sep 17 00:00:00 2001 From: Charles Kawczynski Date: Sat, 5 Aug 2023 14:37:41 -0700 Subject: [PATCH 1/2] Add recreate --- docs/src/api.md | 8 ++++++ src/TexasHoldem.jl | 1 + src/recreate.jl | 64 ++++++++++++++++++++++++++++++++++++++++++++++ src/table.jl | 10 +++++++- test/perf.jl | 1 - test/recreate.jl | 59 ++++++++++++++++++++++++++++++++++++++++++ test/runtests.jl | 3 +++ test/table.jl | 4 +++ 8 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 src/recreate.jl create mode 100644 test/recreate.jl diff --git a/docs/src/api.md b/docs/src/api.md index fb7eda79..675a5bc8 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -42,3 +42,11 @@ TexasHoldem.CallAllInFold TexasHoldem.CallFold TexasHoldem.player_option ``` + +## Training + +There are additional functionalities added for training purposes + +```@docs +TexasHoldem.recreate_game +``` diff --git a/src/TexasHoldem.jl b/src/TexasHoldem.jl index 77bae374..3769b141 100644 --- a/src/TexasHoldem.jl +++ b/src/TexasHoldem.jl @@ -36,5 +36,6 @@ include("game.jl") include("player_actions.jl") include("player_options.jl") include("config_game.jl") +include("recreate.jl") end # module diff --git a/src/recreate.jl b/src/recreate.jl new file mode 100644 index 00000000..ed8e34b8 --- /dev/null +++ b/src/recreate.jl @@ -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 diff --git a/src/table.jl b/src/table.jl index 249f204d..ea217e0a 100644 --- a/src/table.jl +++ b/src/table.jl @@ -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 diff --git a/test/perf.jl b/test/perf.jl index 7dd595a5..77ed2730 100644 --- a/test/perf.jl +++ b/test/perf.jl @@ -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 diff --git a/test/recreate.jl b/test/recreate.jl new file mode 100644 index 00000000..58cac658 --- /dev/null +++ b/test/recreate.jl @@ -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 diff --git a/test/runtests.jl b/test/runtests.jl index 851f9a5f..54a5cd38 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -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 diff --git a/test/table.jl b/test/table.jl index 7cf888b8..66eba7c8 100644 --- a/test/table.jl +++ b/test/table.jl @@ -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 From e2f7a8bd96e305f0d1218c70a797245e58ba70a4 Mon Sep 17 00:00:00 2001 From: Charles Kawczynski Date: Sun, 6 Aug 2023 15:54:31 -0700 Subject: [PATCH 2/2] Bump patch --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 6d50435e..9bfb267f 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "TexasHoldem" uuid = "6cef90fc-eb55-4a2a-97d0-7ecce2b738fe" authors = ["Charles Kawczynski "] -version = "0.3.0" +version = "0.3.1" [deps] Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"