From f92d23a1efbe85318ea86368e79b1b9fb71b8f30 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Sun, 4 Aug 2024 09:28:11 -0700 Subject: [PATCH 01/37] Docs --- src/card.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/card.rs b/src/card.rs index 2bba763..e36121a 100644 --- a/src/card.rs +++ b/src/card.rs @@ -11,6 +11,8 @@ use bincode::{Decode, Encode}; /// - `card_id = 4 * rank + suit` (where `0 <= card_id < 52`) /// - `rank`: 2 => `0`, 3 => `1`, 4 => `2`, ..., A => `12` /// - `suit`: club => `0`, diamond => `1`, heart => `2`, spade => `3` +/// +/// An undealt card is represented by Card::MAX (see `NOT_DEALT`). pub type Card = u8; /// Constant representing that the card is not yet dealt. From 1d366f667ff157d8a99bb0ec61e13208fdadab32 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Sun, 4 Aug 2024 09:28:29 -0700 Subject: [PATCH 02/37] Docs --- src/game/serialization.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/game/serialization.rs b/src/game/serialization.rs index 951c785..851619c 100644 --- a/src/game/serialization.rs +++ b/src/game/serialization.rs @@ -58,7 +58,13 @@ impl PostFlopGame { } } - /// Returns the number of storage elements required for the target storage mode. + /// Returns the number of storage elements required for the target storage mode: + /// `[|storage1|, |storage2|, |storage_ip|, |storage_chance|]` + /// + /// If this is a River save (`target_storage_mode == BoardState::River`) + /// then do not store cfvalues. + /// + /// If this is a Flop save, fn num_target_storage(&self) -> [usize; 4] { if self.state <= State::TreeBuilt { return [0; 4]; From b2c12b83c2a8102b3f7325237e6e4c580bff30ca Mon Sep 17 00:00:00 2001 From: bkushigian Date: Sun, 4 Aug 2024 09:28:51 -0700 Subject: [PATCH 03/37] Added helper function --- src/game/base.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/game/base.rs b/src/game/base.rs index f1cc0f0..6e0f54e 100644 --- a/src/game/base.rs +++ b/src/game/base.rs @@ -462,6 +462,21 @@ impl PostFlopGame { Ok(()) } + pub fn print_internal_data(&self) { + println!("Printing intenral data for PostFlopGame"); + println!("- node_arena: {}", self.node_arena.len()); + println!("- storage1: {}", self.storage1.len()); + println!("- storage2: {}", self.storage2.len()); + println!("- storage_ip: {}", self.storage_ip.len()); + println!("- storage_chance: {}", self.storage_chance.len()); + println!("- locking_strategy: {}", self.locking_strategy.len()); + println!("- storage mode: {:?}", self.storage_mode()); + println!( + "- target storage mode: {:?}", + self.target_storage_mode() + ); + } + /// Initializes fields `initial_weights` and `private_cards`. #[inline] fn init_hands(&mut self) { From 8b7067e8e634b0db016ddfe8a7672711ed59bac8 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Sun, 4 Aug 2024 09:29:09 -0700 Subject: [PATCH 04/37] DESIGN.md --- DESIGN.md | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 DESIGN.md diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..ffb31ed --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,77 @@ +# Design + +This document is a description, as far as I understand it, of the inner design +of the solver and PostFlopGame. This is a working document for me to get my +bearings. + +## PostFlopGame + +### Storage + +There are several fields marked as `// global storage` in `game::mod::PostFlopGame`: + +```rust + // global storage + // `storage*` are used as a global storage and are referenced by `PostFlopNode::storage*`. + // Methods like `PostFlopNode::strategy` define how the storage is used. + node_arena: Vec>, + storage1: Vec, + storage2: Vec, + storage_ip: Vec, + storage_chance: Vec, + locking_strategy: BTreeMap>, +``` + +These are referenced from `PostFlopNode`: + +```rust + storage1: *mut u8, // strategy + storage2: *mut u8, // regrets or cfvalues + storage3: *mut u8, // IP cfvalues +``` + +- `storage1` seems to store the strategy +- `storage2` seems to store regrets/cfvalues, and +- `storage3` stores IP's cf values (does that make `storage2` store OOP's cfvalues?) + +Storage is a byte vector `Vec`, and these store floating point values. + +> [!IMPORTANT] +> Why are these stored as `Vec`s? Is this for swapping between +> `f16` and `f32`s? + +Some storage is allocated in `game::base::allocate_memory`: + +```rust + let storage_bytes = (num_bytes * self.num_storage) as usize; + let storage_ip_bytes = (num_bytes * self.num_storage_ip) as usize; + let storage_chance_bytes = (num_bytes * self.num_storage_chance) as usize; + + self.storage1 = vec![0; storage_bytes]; + self.storage2 = vec![0; storage_bytes]; + self.storage_ip = vec![0; storage_ip_bytes]; + self.storage_chance = vec![0; storage_chance_bytes]; +``` + +`node_arena` is initialized in `game::base::init_root()`: + +```rust + let num_nodes = self.count_num_nodes(); + let total_num_nodes = num_nodes[0] + num_nodes[1] + num_nodes[2]; + + if total_num_nodes > u32::MAX as u64 + || mem::size_of::() as u64 * total_num_nodes > isize::MAX as u64 + { + return Err("Too many nodes".to_string()); + } + + self.num_nodes = num_nodes; + self.node_arena = (0..total_num_nodes) + .map(|_| MutexLike::new(PostFlopNode::default())) + .collect::>(); + self.clear_storage(); +``` + +### Serialization + +Serialization relies on the `bincode` library's `Encode` and `Decode`. \ No newline at end of file From 0236e138f2026d4df18c345494efac75425acc7b Mon Sep 17 00:00:00 2001 From: bkushigian Date: Sun, 4 Aug 2024 09:29:45 -0700 Subject: [PATCH 05/37] Created debug example --- examples/file_io_debug.rs | 83 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 examples/file_io_debug.rs diff --git a/examples/file_io_debug.rs b/examples/file_io_debug.rs new file mode 100644 index 0000000..2edf5ed --- /dev/null +++ b/examples/file_io_debug.rs @@ -0,0 +1,83 @@ +use postflop_solver::*; + +fn main() { + // see `basic.rs` for the explanation of the following code + + let oop_range = "66+,A8s+,A5s-A4s,AJo+,K9s+,KQo,QTs+,JTs,96s+,85s+,75s+,65s,54s"; + let ip_range = "QQ-22,AQs-A2s,ATo+,K5s+,KJo+,Q8s+,J8s+,T7s+,96s+,86s+,75s+,64s+,53s+"; + + let card_config = CardConfig { + range: [oop_range.parse().unwrap(), ip_range.parse().unwrap()], + flop: flop_from_str("Td9d6h").unwrap(), + turn: card_from_str("Qc").unwrap(), + river: NOT_DEALT, + }; + + let bet_sizes = BetSizeOptions::try_from(("60%, e, a", "2.5x")).unwrap(); + + let tree_config = TreeConfig { + initial_state: BoardState::Turn, + starting_pot: 200, + effective_stack: 900, + rake_rate: 0.0, + rake_cap: 0.0, + flop_bet_sizes: [bet_sizes.clone(), bet_sizes.clone()], + turn_bet_sizes: [bet_sizes.clone(), bet_sizes.clone()], + river_bet_sizes: [bet_sizes.clone(), bet_sizes], + turn_donk_sizes: None, + river_donk_sizes: Some(DonkSizeOptions::try_from("50%").unwrap()), + add_allin_threshold: 1.5, + force_allin_threshold: 0.15, + merging_threshold: 0.1, + }; + + let action_tree = ActionTree::new(tree_config).unwrap(); + let mut game = PostFlopGame::with_config(card_config, action_tree).unwrap(); + game.allocate_memory(false); + + let max_num_iterations = 20; + let target_exploitability = game.tree_config().starting_pot as f32 * 0.01; + solve(&mut game, max_num_iterations, target_exploitability, true); + let r = game.set_target_storage_mode(BoardState::Turn); + println!("{r:?}"); + + // save the solved game tree to a file + // 4th argument is zstd compression level (1-22); requires `zstd` feature to use + save_data_to_file(&game, "memo string", "filename.bin", None).unwrap(); + + // load the solved game tree from a file + // 2nd argument is the maximum memory usage in bytes + let (mut game2, _memo_string): (PostFlopGame, _) = + load_data_from_file("filename.bin", None).unwrap(); + + println!("Game 1 Internal Data"); + game.print_internal_data(); + println!("Game 2 Internal Data"); + game2.print_internal_data(); + + // check if the loaded game tree is the same as the original one + game.cache_normalized_weights(); + game2.cache_normalized_weights(); + assert_eq!(game.equity(0), game2.equity(0)); + + // discard information after the river deal when serializing + // this operation does not lose any information of the game tree itself + game2.set_target_storage_mode(BoardState::Turn).unwrap(); + + // compare the memory usage for serialization + println!( + "Memory usage of the original game tree: {:.2}MB", // 11.50MB + game.target_memory_usage() as f64 / (1024.0 * 1024.0) + ); + println!( + "Memory usage of the truncated game tree: {:.2}MB", // 0.79MB + game2.target_memory_usage() as f64 / (1024.0 * 1024.0) + ); + + // overwrite the file with the truncated game tree + // game tree constructed from this file cannot access information after the river deal + save_data_to_file(&game2, "memo string", "filename.bin", None).unwrap(); + + // delete the file + std::fs::remove_file("filename.bin").unwrap(); +} From f9497a68e54e3e88d13cfecff2bc307201aa7f15 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Sun, 4 Aug 2024 11:49:24 -0700 Subject: [PATCH 06/37] DESIGN.md --- DESIGN.md | 48 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index ffb31ed..c6bdd97 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -53,7 +53,7 @@ Some storage is allocated in `game::base::allocate_memory`: self.storage_chance = vec![0; storage_chance_bytes]; ``` -`node_arena` is initialized in `game::base::init_root()`: +`node_arena` is allocated in `game::base::init_root()`: ```rust let num_nodes = self.count_num_nodes(); @@ -72,6 +72,48 @@ Some storage is allocated in `game::base::allocate_memory`: self.clear_storage(); ``` -### Serialization +`locking_strategy` maps node indexes (`PostFlopGame::node_index`) to a locked +strategy. `locking_strategy` is initialized to an empty `BTreeMap>` by deriving Default. It is inserted into via +`PostFlopGame::lock_current_strategy` -Serialization relies on the `bincode` library's `Encode` and `Decode`. \ No newline at end of file +### Serialization/Deserialization + +Serialization relies on the `bincode` library's `Encode` and `Decode`. We can set +the `target_storage_mode` to allow for a non-full save. For instance, + +```rust +game.set_target_storage_mode(BoardState::Turn); +``` + +will ensure that when `game` is encoded, it will only save Flop and Turn data. +When a serialized tree is deserialized, if it is a parital save (e.g., a Turn +save) you will not be able to navigate to unsaved streets. + +Several things break when we deserialize a partial save: +- `node_arena` is only partially populated +- `node.children()` points to raw data when `node` points to an street that is + not serialized (e.g., a chance node before the river for a Turn save). + +### Allocating `node_arena` + +We want to first allocate nodes for `node_arena`, and then run some form of +`build_tree_recursive`. This assumes that `node_arena` is already allocated, and +recursively visits children of nodes and modifies them to + + +### Data Coupling/Relations/Invariants + +- A node is locked iff it is contained in the game's locking_strategy +- `PostFlopGame.node_arena` is pointed to by `PostFlopNode.children_offset`. For + instance, this is the basic definition of the `PostFlopNode.children()` + function: + + ```rust + slice::from_raw_parts( + self_ptr.add(self.children_offset as usize), + self.num_children as usize, + ) + ``` + + We get a pointer to `self` and add children offset. \ No newline at end of file From 7aa6668e3f58ba8c0434445986ec1c87a5a7e2a8 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Sun, 4 Aug 2024 12:07:53 -0700 Subject: [PATCH 07/37] Updates to examples for debugging/documenting --- examples/file_io_debug.rs | 14 +++++++++++ examples/simple.rs | 53 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 examples/simple.rs diff --git a/examples/file_io_debug.rs b/examples/file_io_debug.rs index 2edf5ed..f0348a6 100644 --- a/examples/file_io_debug.rs +++ b/examples/file_io_debug.rs @@ -1,3 +1,5 @@ +use std::fs::File; + use postflop_solver::*; fn main() { @@ -77,6 +79,18 @@ fn main() { // overwrite the file with the truncated game tree // game tree constructed from this file cannot access information after the river deal save_data_to_file(&game2, "memo string", "filename.bin", None).unwrap(); + let (mut game3, _memo_string): (PostFlopGame, String) = + load_data_from_file("filename.bin", None).unwrap(); + + game.play(0); + game.play(0); + println!("Game X/X Actions: {:?}", game.available_actions()); + game2.play(0); + game2.play(0); + println!("Game2 X/X Actions: {:?}", game.available_actions()); + game3.play(0); + game3.play(0); + println!("Game3 X/X Actions: {:?}", game3.available_actions()); // delete the file std::fs::remove_file("filename.bin").unwrap(); diff --git a/examples/simple.rs b/examples/simple.rs new file mode 100644 index 0000000..00782d0 --- /dev/null +++ b/examples/simple.rs @@ -0,0 +1,53 @@ +use postflop_solver::*; + +fn main() { + // ranges of OOP and IP in string format + // see the documentation of `Range` for more details about the format + let oop_range = "66+"; + let ip_range = "66+"; + + let card_config = CardConfig { + range: [oop_range.parse().unwrap(), ip_range.parse().unwrap()], + flop: flop_from_str("Td9d6h").unwrap(), + turn: NOT_DEALT, + river: NOT_DEALT, + }; + + // bet sizes -> 60% of the pot, geometric size, and all-in + // raise sizes -> 2.5x of the previous bet + // see the documentation of `BetSizeOptions` for more details + let bet_sizes = BetSizeOptions::try_from(("100%", "100%")).unwrap(); + + let tree_config = TreeConfig { + initial_state: BoardState::Flop, // must match `card_config` + starting_pot: 200, + effective_stack: 200, + rake_rate: 0.0, + rake_cap: 0.0, + flop_bet_sizes: [bet_sizes.clone(), bet_sizes.clone()], // [OOP, IP] + turn_bet_sizes: [bet_sizes.clone(), bet_sizes.clone()], + river_bet_sizes: [bet_sizes.clone(), bet_sizes], + turn_donk_sizes: None, // use default bet sizes + river_donk_sizes: Some(DonkSizeOptions::try_from("100%").unwrap()), + add_allin_threshold: 1.5, // add all-in if (maximum bet size) <= 1.5x pot + force_allin_threshold: 0.15, // force all-in if (SPR after the opponent's call) <= 0.15 + merging_threshold: 0.1, + }; + + // build the game tree + // `ActionTree` can be edited manually after construction + let action_tree = ActionTree::new(tree_config).unwrap(); + let mut game = PostFlopGame::with_config(card_config, action_tree).unwrap(); + + // allocate memory without compression (use 32-bit float) + game.allocate_memory(false); + + // solve the game + let max_num_iterations = 20; + let target_exploitability = game.tree_config().starting_pot as f32 * 0.100; // 10.0% of the pot + let exploitability = solve(&mut game, max_num_iterations, target_exploitability, true); + println!("Exploitability: {:.2}", exploitability); + + // get equity and EV of a specific hand + game.cache_normalized_weights(); +} From 334315bffc87b7a8b0fdc380d37730ad615d6c47 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Mon, 5 Aug 2024 15:33:38 -0700 Subject: [PATCH 08/37] Intermediate stuff on DESIGN.md --- DESIGN.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/DESIGN.md b/DESIGN.md index ffb31ed..b7c8bcc 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -6,6 +6,60 @@ bearings. ## PostFlopGame +### Build/Allocate/Initialize + +We begin by creating a `PostFlopGame:` + +1. **Create Configurations**: + + We need a `tree_config: TreeConfig` + + We need an `action_tree: ActionTree::new(tree_config)` + + We need a `card_config: CardConfig` +2. **PostFlopGame**: We build a `PostFlopGame` from `action_tree` and `card_config`: + + ```rust + let mut game = PostFlopGame::with_config(card_config, action_tree).unwrap(); + ``` + +Once the game is created we need to allocate memory: + ++ `game.node_arena` ++ `game.storage1` ++ `game.storage2` ++ `game.storage_ip` ++ `game.storage_chance` + +These fields are not allocated at the same time. `game.node_arena` is allocated +via `with_config`, which calls `update_config`, which in turn calls `init_root`. +`init_root` is responsible for: + +1. Allocating `PostFlopNode`s in `node_arena` +2. Invoking `build_tree_recursive` which initializes each node's child/parent + relationship via `child_offset` (through calls to ). + +Each `PostFlopNode` points to node-specific data (eg., strategies and cfregrets) +that is located inside of `PostFlopGame.storage*` fields (which is currently +unallocated) via similarly named fields `PostFlopNode.storage*`. + +Additionally, each node points to the children offset with `children_offset`, +which records where in `node_arena` relative to the current node that node's +children begin. We allocate this memory via: + +```rust +game.allocate_memory(false); // pass `true` to use compressed memory +``` + +This allocates the following memory: + ++ `self.storage1` ++ `self.storage2` ++ `self.storage3` ++ `self.storage_chance` + +Next, `allocate_memory()` calls `allocate_memory_nodes(&mut self)`, which +iterates through each node in `node_arena` and sets storage pointers. + +After `allocate_memory` returns we still need to set `child_offset`s. + ### Storage There are several fields marked as `// global storage` in `game::mod::PostFlopGame`: From 478a72e6ecdaa755ad6e297638d3834680399bd8 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Mon, 5 Aug 2024 19:46:10 -0700 Subject: [PATCH 09/37] DESIGN.md --- DESIGN.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/DESIGN.md b/DESIGN.md index a53488f..2b15c18 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -34,7 +34,8 @@ via `with_config`, which calls `update_config`, which in turn calls `init_root`. 1. Allocating `PostFlopNode`s in `node_arena` 2. Invoking `build_tree_recursive` which initializes each node's child/parent - relationship via `child_offset` (through calls to ). + relationship via `child_offset` (through calls to `push_actions` and + `push_chances`). Each `PostFlopNode` points to node-specific data (eg., strategies and cfregrets) that is located inside of `PostFlopGame.storage*` fields (which is currently From 25d5ee6d3cad37c30a24a61f9e7a38f04a48035d Mon Sep 17 00:00:00 2001 From: bkushigian Date: Tue, 6 Aug 2024 11:38:46 -0700 Subject: [PATCH 10/37] docs and rename function for readability --- src/game/base.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/game/base.rs b/src/game/base.rs index 6e0f54e..3f5b615 100644 --- a/src/game/base.rs +++ b/src/game/base.rs @@ -536,7 +536,7 @@ impl PostFlopGame { /// Initializes the root node of game tree. fn init_root(&mut self) -> Result<(), String> { - let num_nodes = self.count_num_nodes(); + let num_nodes = self.count_nodes_per_street(); let total_num_nodes = num_nodes[0] + num_nodes[1] + num_nodes[2]; if total_num_nodes > u32::MAX as u64 @@ -599,9 +599,10 @@ impl PostFlopGame { self.storage_chance = Vec::new(); } - /// Counts the number of nodes in the game tree. + /// Counts the number of nodes in the game tree per street, accounting for + /// isomorphism. #[inline] - fn count_num_nodes(&self) -> [u64; 3] { + fn count_nodes_per_street(&self) -> [u64; 3] { let (turn_coef, river_coef) = match (self.card_config.turn, self.card_config.river) { (NOT_DEALT, _) => { let mut river_coef = 0; From f05a1796053151ee880749d6fbeb388511637c01 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Tue, 6 Aug 2024 11:39:30 -0700 Subject: [PATCH 11/37] rename local var for clarity --- src/game/base.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/game/base.rs b/src/game/base.rs index 3f5b615..27d5d28 100644 --- a/src/game/base.rs +++ b/src/game/base.rs @@ -536,8 +536,8 @@ impl PostFlopGame { /// Initializes the root node of game tree. fn init_root(&mut self) -> Result<(), String> { - let num_nodes = self.count_nodes_per_street(); - let total_num_nodes = num_nodes[0] + num_nodes[1] + num_nodes[2]; + let nodes_per_street = self.count_nodes_per_street(); + let total_num_nodes = nodes_per_street[0] + nodes_per_street[1] + nodes_per_street[2]; if total_num_nodes > u32::MAX as u64 || mem::size_of::() as u64 * total_num_nodes > isize::MAX as u64 @@ -545,15 +545,15 @@ impl PostFlopGame { return Err("Too many nodes".to_string()); } - self.num_nodes = num_nodes; + self.num_nodes = nodes_per_street; self.node_arena = (0..total_num_nodes) .map(|_| MutexLike::new(PostFlopNode::default())) .collect::>(); self.clear_storage(); let mut info = BuildTreeInfo { - turn_index: num_nodes[0] as usize, - river_index: (num_nodes[0] + num_nodes[1]) as usize, + turn_index: nodes_per_street[0] as usize, + river_index: (nodes_per_street[0] + nodes_per_street[1]) as usize, ..Default::default() }; From 729693c953ba56bcac1f1c92fe6571e04de33ba8 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Tue, 6 Aug 2024 11:45:13 -0700 Subject: [PATCH 12/37] Rename for readability --- src/game/base.rs | 2 +- src/game/mod.rs | 2 +- src/game/serialization.rs | 14 ++++++++------ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/game/base.rs b/src/game/base.rs index 27d5d28..ba14b4d 100644 --- a/src/game/base.rs +++ b/src/game/base.rs @@ -545,7 +545,7 @@ impl PostFlopGame { return Err("Too many nodes".to_string()); } - self.num_nodes = nodes_per_street; + self.num_nodes_per_street = nodes_per_street; self.node_arena = (0..total_num_nodes) .map(|_| MutexLike::new(PostFlopNode::default())) .collect::>(); diff --git a/src/game/mod.rs b/src/game/mod.rs index 33d9a19..09042fd 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -82,7 +82,7 @@ pub struct PostFlopGame { // store options storage_mode: BoardState, target_storage_mode: BoardState, - num_nodes: [u64; 3], + num_nodes_per_street: [u64; 3], is_compression_enabled: bool, num_storage: u64, num_storage_ip: u64, diff --git a/src/game/serialization.rs b/src/game/serialization.rs index 851619c..5931bf8 100644 --- a/src/game/serialization.rs +++ b/src/game/serialization.rs @@ -77,8 +77,8 @@ impl PostFlopGame { } let mut node_index = match self.target_storage_mode { - BoardState::Flop => self.num_nodes[0], - _ => self.num_nodes[0] + self.num_nodes[1], + BoardState::Flop => self.num_nodes_per_street[0], + _ => self.num_nodes_per_street[0] + self.num_nodes_per_street[1], } as usize; let mut num_storage = [0; 4]; @@ -134,7 +134,7 @@ impl Encode for PostFlopGame { self.removed_lines.encode(encoder)?; self.action_root.encode(encoder)?; self.target_storage_mode.encode(encoder)?; - self.num_nodes.encode(encoder)?; + self.num_nodes_per_street.encode(encoder)?; self.is_compression_enabled.encode(encoder)?; self.num_storage.encode(encoder)?; self.num_storage_ip.encode(encoder)?; @@ -146,8 +146,10 @@ impl Encode for PostFlopGame { self.storage_chance[0..num_storage[3]].encode(encoder)?; let num_nodes = match self.target_storage_mode { - BoardState::Flop => self.num_nodes[0] as usize, - BoardState::Turn => (self.num_nodes[0] + self.num_nodes[1]) as usize, + BoardState::Flop => self.num_nodes_per_street[0] as usize, + BoardState::Turn => { + (self.num_nodes_per_street[0] + self.num_nodes_per_street[1]) as usize + } BoardState::River => self.node_arena.len(), }; @@ -199,7 +201,7 @@ impl Decode for PostFlopGame { removed_lines: Decode::decode(decoder)?, action_root: Decode::decode(decoder)?, storage_mode: Decode::decode(decoder)?, - num_nodes: Decode::decode(decoder)?, + num_nodes_per_street: Decode::decode(decoder)?, is_compression_enabled: Decode::decode(decoder)?, num_storage: Decode::decode(decoder)?, num_storage_ip: Decode::decode(decoder)?, From 8d191bb2b1c08e3d0dd7e3ad8815b9227daf91d7 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Fri, 9 Aug 2024 10:26:11 -0700 Subject: [PATCH 13/37] DESIGN.md --- DESIGN.md | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index 2b15c18..99e5ca9 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -8,19 +8,30 @@ bearings. ### Build/Allocate/Initialize -We begin by creating a `PostFlopGame:` +To set up a `PostFlopGame` we need to **create a `PostFlopGame` instance**, +**allocate global storage and `PostFlopNode`s**, and **initialize the +`PostFlopNode` child/parent relationship**. This is done in several steps. + + +We begin by creating a `PostFlopGame`. A `PostFlopGame` requires an `ActionTree` +and a `CardConfig`. The `ActionTree` represents the full game tree modded out by +different runouts (so an `ActionTree` might have an abstract _line_ **Bet 10; +Call; Bet 30; Call** while the game tree would have concrete _nodes_ like +**Bet 10; Call; Th; Bet 30; Call**, etc). 1. **Create Configurations**: + We need a `tree_config: TreeConfig` + We need an `action_tree: ActionTree::new(tree_config)` + We need a `card_config: CardConfig` -2. **PostFlopGame**: We build a `PostFlopGame` from `action_tree` and `card_config`: + +2. **Create PostFlopGame**: We build a `PostFlopGame` from `action_tree` and `card_config`: ```rust let mut game = PostFlopGame::with_config(card_config, action_tree).unwrap(); ``` -Once the game is created we need to allocate memory: +Once the game is created we need to allocate the following memory and initialize +its values: + `game.node_arena` + `game.storage1` @@ -28,12 +39,25 @@ Once the game is created we need to allocate memory: + `game.storage_ip` + `game.storage_chance` -These fields are not allocated at the same time. `game.node_arena` is allocated -via `with_config`, which calls `update_config`, which in turn calls `init_root`. +These fields are not allocated/initialized at the same time; ++ `game.node_arena` is allocated and initialized via `with_config`, ++ other storage is allocated via `game.allocate_memory()`. + +#### Allocating and Initializing `node_arena` + +We construct a `PostFlopGame` by calling +`PostFlopGame::with_config(card_config, action_tree)`, which calls +`update_config`. `PostFlopGame::update_config` sets up configuration data, +sanity checks things are correct, and then calls `self.init_root()`. + `init_root` is responsible for: -1. Allocating `PostFlopNode`s in `node_arena` -2. Invoking `build_tree_recursive` which initializes each node's child/parent +1. Counting number of `PostFlopNode`s to be allocated (`self.num_nodes`), broken + up by flop, turn, and river +2. Allocating `self.num_nodes` `PostFlopNode`s in `node_arena` field +3. Clearing storage: `self.clear_storage()` sets each storage item to a new + `Vec` +4. Invoking `build_tree_recursive` which initializes each node's child/parent relationship via `child_offset` (through calls to `push_actions` and `push_chances`). @@ -111,7 +135,7 @@ Some storage is allocated in `game::base::allocate_memory`: `node_arena` is allocated in `game::base::init_root()`: ```rust - let num_nodes = self.count_num_nodes(); + let num_nodes = self.count_nodes_per_street(); let total_num_nodes = num_nodes[0] + num_nodes[1] + num_nodes[2]; if total_num_nodes > u32::MAX as u64 From 798c4aa9dae1f598a768ce93b5bb102c7f909b11 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Fri, 9 Aug 2024 10:27:19 -0700 Subject: [PATCH 14/37] Linter issues --- DESIGN.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index 99e5ca9..f5eb759 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -40,6 +40,7 @@ its values: + `game.storage_chance` These fields are not allocated/initialized at the same time; + + `game.node_arena` is allocated and initialized via `with_config`, + other storage is allocated via `game.allocate_memory()`. @@ -109,9 +110,9 @@ These are referenced from `PostFlopNode`: storage3: *mut u8, // IP cfvalues ``` -- `storage1` seems to store the strategy -- `storage2` seems to store regrets/cfvalues, and -- `storage3` stores IP's cf values (does that make `storage2` store OOP's cfvalues?) ++ `storage1` seems to store the strategy ++ `storage2` seems to store regrets/cfvalues, and ++ `storage3` stores IP's cf values (does that make `storage2` store OOP's cfvalues?) Storage is a byte vector `Vec`, and these store floating point values. @@ -170,8 +171,8 @@ When a serialized tree is deserialized, if it is a parital save (e.g., a Turn save) you will not be able to navigate to unsaved streets. Several things break when we deserialize a partial save: -- `node_arena` is only partially populated -- `node.children()` points to raw data when `node` points to an street that is ++ `node_arena` is only partially populated ++ `node.children()` points to raw data when `node` points to an street that is not serialized (e.g., a chance node before the river for a Turn save). ### Allocating `node_arena` @@ -183,8 +184,8 @@ recursively visits children of nodes and modifies them to ### Data Coupling/Relations/Invariants -- A node is locked iff it is contained in the game's locking_strategy -- `PostFlopGame.node_arena` is pointed to by `PostFlopNode.children_offset`. For ++ A node is locked iff it is contained in the game's locking_strategy ++ `PostFlopGame.node_arena` is pointed to by `PostFlopNode.children_offset`. For instance, this is the basic definition of the `PostFlopNode.children()` function: From cd790ebf9db3ea2a9025671d255deb471f95e890 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Fri, 9 Aug 2024 10:27:55 -0700 Subject: [PATCH 15/37] DESIGN.md --- DESIGN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESIGN.md b/DESIGN.md index f5eb759..16f39f8 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -184,7 +184,7 @@ recursively visits children of nodes and modifies them to ### Data Coupling/Relations/Invariants -+ A node is locked iff it is contained in the game's locking_strategy ++ A node is locked IFF it is contained in the game's locking_strategy + `PostFlopGame.node_arena` is pointed to by `PostFlopNode.children_offset`. For instance, this is the basic definition of the `PostFlopNode.children()` function: From 6377ad3cd6dbc9ce3d07032ff7abc1f0f775ce70 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Fri, 9 Aug 2024 10:28:17 -0700 Subject: [PATCH 16/37] DESIGN.md --- DESIGN.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/DESIGN.md b/DESIGN.md index 16f39f8..b64d8f9 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -167,10 +167,11 @@ game.set_target_storage_mode(BoardState::Turn); ``` will ensure that when `game` is encoded, it will only save Flop and Turn data. -When a serialized tree is deserialized, if it is a parital save (e.g., a Turn +When a serialized tree is deserialized, if it is a partial save (e.g., a Turn save) you will not be able to navigate to unsaved streets. Several things break when we deserialize a partial save: + + `node_arena` is only partially populated + `node.children()` points to raw data when `node` points to an street that is not serialized (e.g., a chance node before the river for a Turn save). From 0cd0382f47924d4bc9a4eeec4e554e4d498fe935 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Fri, 9 Aug 2024 10:28:39 -0700 Subject: [PATCH 17/37] DESIGN.md --- DESIGN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESIGN.md b/DESIGN.md index b64d8f9..97e05b8 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -197,4 +197,4 @@ recursively visits children of nodes and modifies them to ) ``` - We get a pointer to `self` and add children offset. \ No newline at end of file + We get a pointer to `self` and add children offset. From c03fb57311321c41cce66fadf2d6ae13f0fcc942 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Fri, 9 Aug 2024 11:20:04 -0700 Subject: [PATCH 18/37] Updates --- DESIGN.md | 60 +++++++++++++++++++++++++++---------------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index 97e05b8..3da8edc 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -12,26 +12,19 @@ To set up a `PostFlopGame` we need to **create a `PostFlopGame` instance**, **allocate global storage and `PostFlopNode`s**, and **initialize the `PostFlopNode` child/parent relationship**. This is done in several steps. +We begin by creating a `PostFlopGame` instance. -We begin by creating a `PostFlopGame`. A `PostFlopGame` requires an `ActionTree` -and a `CardConfig`. The `ActionTree` represents the full game tree modded out by -different runouts (so an `ActionTree` might have an abstract _line_ **Bet 10; -Call; Bet 30; Call** while the game tree would have concrete _nodes_ like -**Bet 10; Call; Th; Bet 30; Call**, etc). - -1. **Create Configurations**: - + We need a `tree_config: TreeConfig` - + We need an `action_tree: ActionTree::new(tree_config)` - + We need a `card_config: CardConfig` - -2. **Create PostFlopGame**: We build a `PostFlopGame` from `action_tree` and `card_config`: +```rust +let mut game = PostFlopGame::with_config(card_config, action_tree).unwrap(); +``` - ```rust - let mut game = PostFlopGame::with_config(card_config, action_tree).unwrap(); - ``` +A `PostFlopGame` requires an +`ActionTree` which describes all possible actions and lines (no runout +information), and a `CardConfig`, which describes player ranges and +flop/turn/river data. -Once the game is created we need to allocate the following memory and initialize -its values: +Once we have created a `PostFlopGame` instance we need to allocate the following +memory and initialize its values: + `game.node_arena` + `game.storage1` @@ -39,32 +32,40 @@ its values: + `game.storage_ip` + `game.storage_chance` -These fields are not allocated/initialized at the same time; +These fields are not allocated/initialized at the same time: -+ `game.node_arena` is allocated and initialized via `with_config`, ++ `game.node_arena` is allocated and initialized via `with_config()` (i.e., when + we created our `PostFlopGame`), + other storage is allocated via `game.allocate_memory()`. #### Allocating and Initializing `node_arena` -We construct a `PostFlopGame` by calling -`PostFlopGame::with_config(card_config, action_tree)`, which calls -`update_config`. `PostFlopGame::update_config` sets up configuration data, -sanity checks things are correct, and then calls `self.init_root()`. +We constructed a `PostFlopGame` by calling +`PostFlopGame::with_config(card_config, action_tree)`, which under the hood +actually calls: + +```rust + let mut game = Self::new(); + game.update_config(card_config, action_tree)?; +``` + +`PostFlopGame::update_config` sets up configuration data, sanity checks things +are correct, and then calls `self.init_root()`. `init_root` is responsible for: -1. Counting number of `PostFlopNode`s to be allocated (`self.num_nodes`), broken - up by flop, turn, and river -2. Allocating `self.num_nodes` `PostFlopNode`s in `node_arena` field +1. Counting number of `PostFlopNode`s to be allocated (`self.nodes_per_street`), + broken up by flop, turn, and river +2. Allocating `PostFlopNode`s in the `node_arena` field 3. Clearing storage: `self.clear_storage()` sets each storage item to a new `Vec` 4. Invoking `build_tree_recursive` which initializes each node's child/parent relationship via `child_offset` (through calls to `push_actions` and `push_chances`). -Each `PostFlopNode` points to node-specific data (eg., strategies and cfregrets) -that is located inside of `PostFlopGame.storage*` fields (which is currently -unallocated) via similarly named fields `PostFlopNode.storage*`. +Each `PostFlopNode` points to node-specific data (e.g., strategies and +cfregrets) that is located inside of `PostFlopGame.storage*` fields (which is +currently unallocated) via similarly named fields `PostFlopNode.storage*`. Additionally, each node points to the children offset with `children_offset`, which records where in `node_arena` relative to the current node that node's @@ -182,7 +183,6 @@ We want to first allocate nodes for `node_arena`, and then run some form of `build_tree_recursive`. This assumes that `node_arena` is already allocated, and recursively visits children of nodes and modifies them to - ### Data Coupling/Relations/Invariants + A node is locked IFF it is contained in the game's locking_strategy From 694c61b99a35adcfb44dc7d7aaaec412e57ec30d Mon Sep 17 00:00:00 2001 From: bkushigian Date: Wed, 14 Aug 2024 11:11:36 -0700 Subject: [PATCH 19/37] tmp commit --- src/game/base.rs | 177 ++++++++++++++++++++++++++++++++++++++-- src/game/interpreter.rs | 9 +- src/game/mod.rs | 5 +- src/game/node.rs | 16 ++++ 4 files changed, 196 insertions(+), 11 deletions(-) diff --git a/src/game/base.rs b/src/game/base.rs index ba14b4d..6dc26c8 100644 --- a/src/game/base.rs +++ b/src/game/base.rs @@ -463,7 +463,7 @@ impl PostFlopGame { } pub fn print_internal_data(&self) { - println!("Printing intenral data for PostFlopGame"); + println!("Printing internal data for PostFlopGame"); println!("- node_arena: {}", self.node_arena.len()); println!("- storage1: {}", self.storage1.len()); println!("- storage2: {}", self.storage2.len()); @@ -534,7 +534,15 @@ impl PostFlopGame { ) = self.card_config.isomorphism(&self.private_cards); } - /// Initializes the root node of game tree. + /// Initializes the root node of game tree and recursively build the tree. + /// + /// This function is responsible for computing the number of nodes required + /// for each street (via `count_nodes_per_street()`), allocating + /// `PostFlopNode`s to `self.node_arena`, and calling `build_tree_recursive`, + /// which recursively visits all nodes and, among other things, initializes + /// the child/parent relation. + /// + /// This does _not_ allocate global storage (e.g., `self.storage1`, etc). fn init_root(&mut self) -> Result<(), String> { let nodes_per_street = self.count_nodes_per_street(); let total_num_nodes = nodes_per_street[0] + nodes_per_street[1] + nodes_per_street[2]; @@ -741,6 +749,7 @@ impl PostFlopGame { node.num_children += 1; let mut child = node.children().last().unwrap().lock(); child.prev_action = Action::Chance(card); + child.parent_node_index = node_index; child.turn = card; } } @@ -804,6 +813,7 @@ impl PostFlopGame { child.prev_action = *action; child.turn = node.turn; child.river = node.river; + child.parent_node_index = node_index; } let num_private_hands = self.num_private_hands(node.player as usize); @@ -817,6 +827,159 @@ impl PostFlopGame { info.num_storage_ip += node.num_elements_ip as u64; } + /* REBUILDING AND RESOLVING TREE */ + + /// Like `init_root`, but applied to a partial save loaded from disk. This + /// reallocates missing `PostFlopNode`s to `node_arena` and reruns + /// `build_tree_recursive`. Rerunning `build_tree_recursive` will not alter + /// nodes loaded from disk. + pub fn reinit_root(&mut self) -> Result<(), String> { + let nodes_per_street = self.count_nodes_per_street(); + let total_num_nodes = nodes_per_street[0] + nodes_per_street[1] + nodes_per_street[2]; + + if total_num_nodes > u32::MAX as u64 + || mem::size_of::() as u64 * total_num_nodes > isize::MAX as u64 + { + return Err("Too many nodes".to_string()); + } + + self.num_nodes_per_street = nodes_per_street; + + let total_new_nodes_to_allocate = total_num_nodes - self.node_arena.len() as u64; + self.node_arena.append( + &mut (0..total_new_nodes_to_allocate) + .map(|_| MutexLike::new(PostFlopNode::default())) + .collect::>(), + ); + // self.clear_storage(); + + let mut info = BuildTreeInfo { + turn_index: nodes_per_street[0] as usize, + river_index: (nodes_per_street[0] + nodes_per_street[1]) as usize, + ..Default::default() + }; + + match self.tree_config.initial_state { + BoardState::Flop => info.flop_index += 1, + BoardState::Turn => info.turn_index += 1, + BoardState::River => info.river_index += 1, + } + + let mut root = self.node_arena[0].lock(); + root.turn = self.card_config.turn; + root.river = self.card_config.river; + + self.build_tree_recursive(0, &self.action_root.lock(), &mut info); + + self.num_storage = info.num_storage; + self.num_storage_ip = info.num_storage_ip; + self.num_storage_chance = info.num_storage_chance; + self.misc_memory_usage = self.memory_usage_internal(); + + Ok(()) + } + + pub fn reload_and_resolve(&mut self, enable_compression: bool) -> Result<(), String> { + self.allocate_memory_after_load(enable_compression)?; + self.reinit_root()?; + + // Collect root nodes to resolve + let nodes_to_solve = match self.storage_mode { + BoardState::Flop => { + let turn_root_nodes = self + .node_arena + .iter() + .filter(|n| { + n.lock().turn != NOT_DEALT + && n.lock().river == NOT_DEALT + && matches!(n.lock().prev_action, Action::Chance(..)) + }) + .collect::>(); + turn_root_nodes + } + BoardState::Turn => { + let river_root_nodes = self + .node_arena + .iter() + .filter(|n| { + n.lock().turn != NOT_DEALT + && matches!(n.lock().prev_action, Action::Chance(..)) + }) + .collect::>(); + river_root_nodes + } + BoardState::River => vec![], + }; + for node in nodes_to_solve { + // Get history of this node + // let mut history = vec![]; + let mut n = node.lock(); + while n.parent_node_index < usize::MAX { + let parent = self.node_arena[n.parent_node_index].lock(); + let action = n.prev_action; + } + } + + Ok(()) + } + + /// Reallocate memory for full tree after performing a partial load + pub fn allocate_memory_after_load(&mut self, enable_compression: bool) -> Result<(), String> { + if self.state <= State::Uninitialized { + return Err("Game is not successfully initialized".to_string()); + } + + if self.state == State::MemoryAllocated + && self.storage_mode == BoardState::River + && self.is_compression_enabled == enable_compression + { + return Ok(()); + } + + let num_bytes = if enable_compression { 2 } else { 4 }; + if num_bytes * self.num_storage > isize::MAX as u64 + || num_bytes * self.num_storage_chance > isize::MAX as u64 + { + return Err("Memory usage exceeds maximum size".to_string()); + } + + self.state = State::MemoryAllocated; + self.is_compression_enabled = enable_compression; + + let old_storage1 = std::mem::replace(&mut self.storage1, vec![]); + let old_storage2 = std::mem::replace(&mut self.storage2, vec![]); + let old_storage_ip = std::mem::replace(&mut self.storage_ip, vec![]); + let old_storage_chance = std::mem::replace(&mut self.storage_chance, vec![]); + + let storage_bytes = (num_bytes * self.num_storage) as usize; + let storage_ip_bytes = (num_bytes * self.num_storage_ip) as usize; + let storage_chance_bytes = (num_bytes * self.num_storage_chance) as usize; + + self.storage1 = vec![0; storage_bytes]; + self.storage2 = vec![0; storage_bytes]; + self.storage_ip = vec![0; storage_ip_bytes]; + self.storage_chance = vec![0; storage_chance_bytes]; + + self.allocate_memory_nodes(); + + self.storage_mode = BoardState::River; + self.target_storage_mode = BoardState::River; + + for (dst, src) in self.storage1.iter_mut().zip(&old_storage1) { + *dst = *src; + } + for (dst, src) in self.storage2.iter_mut().zip(&old_storage2) { + *dst = *src; + } + for (dst, src) in self.storage_ip.iter_mut().zip(&old_storage_ip) { + *dst = *src; + } + for (dst, src) in self.storage_chance.iter_mut().zip(&old_storage_chance) { + *dst = *src; + } + Ok(()) + } + /// Sets the bunching effect. fn set_bunching_effect_internal(&mut self, bunching_data: &BunchingData) -> Result<(), String> { self.bunching_num_dead_cards = bunching_data.fold_ranges().len() * 2; @@ -1004,7 +1167,7 @@ impl PostFlopGame { if self.card_config.river != NOT_DEALT { self.bunching_arena = arena; - self.assign_zero_weights(); + self.assign_zero_weights_to_dead_cards(); return Ok(()); } @@ -1119,7 +1282,7 @@ impl PostFlopGame { if self.card_config.turn != NOT_DEALT { self.bunching_arena = arena; - self.assign_zero_weights(); + self.assign_zero_weights_to_dead_cards(); return Ok(()); } @@ -1197,7 +1360,7 @@ impl PostFlopGame { } self.bunching_arena = arena; - self.assign_zero_weights(); + self.assign_zero_weights_to_dead_cards(); Ok(()) } @@ -1463,4 +1626,8 @@ impl PostFlopGame { } } } + + pub fn get_state(&self) -> &State { + return &self.state; + } } diff --git a/src/game/interpreter.rs b/src/game/interpreter.rs index d44d63a..d7845b6 100644 --- a/src/game/interpreter.rs +++ b/src/game/interpreter.rs @@ -30,7 +30,7 @@ impl PostFlopGame { self.weights[0].copy_from_slice(&self.initial_weights[0]); self.weights[1].copy_from_slice(&self.initial_weights[1]); - self.assign_zero_weights(); + self.assign_zero_weights_to_dead_cards(); } /// Returns the history of the current node. @@ -366,7 +366,7 @@ impl PostFlopGame { } // update the weights - self.assign_zero_weights(); + self.assign_zero_weights_to_dead_cards(); } // player node else { @@ -1005,8 +1005,9 @@ impl PostFlopGame { unsafe { node_ptr.offset_from(self.node_arena.as_ptr()) as usize } } - /// Assigns zero weights to the hands that are not possible. - pub(super) fn assign_zero_weights(&mut self) { + /// Assigns zero weights to the hands that are not possible (e.g, by a card + /// being removed by a turn or river). + pub(super) fn assign_zero_weights_to_dead_cards(&mut self) { if self.bunching_num_dead_cards == 0 { let mut board_mask: u64 = 0; if self.turn != NOT_DEALT { diff --git a/src/game/mod.rs b/src/game/mod.rs index 09042fd..3504adb 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -17,10 +17,10 @@ use std::collections::BTreeMap; #[cfg(feature = "bincode")] use bincode::{Decode, Encode}; -#[derive(Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)] #[repr(u8)] #[cfg_attr(feature = "bincode", derive(Decode, Encode))] -enum State { +pub enum State { ConfigError = 0, #[default] Uninitialized = 1, @@ -121,6 +121,7 @@ pub struct PostFlopGame { #[repr(C)] pub struct PostFlopNode { prev_action: Action, + parent_node_index: usize, player: u8, turn: Card, river: Card, diff --git a/src/game/node.rs b/src/game/node.rs index e6bd7e3..4c6fd4b 100644 --- a/src/game/node.rs +++ b/src/game/node.rs @@ -209,6 +209,7 @@ impl Default for PostFlopNode { fn default() -> Self { Self { prev_action: Action::None, + parent_node_index: usize::MAX, player: PLAYER_OOP, turn: NOT_DEALT, river: NOT_DEALT, @@ -240,4 +241,19 @@ impl PostFlopNode { ) } } + + /// Get a list of available actions at a given node + pub fn actions(&self) -> Vec { + self.children() + .iter() + .map(|n| n.lock().prev_action) + .collect::>() + } + + /// Find the index of a given action, if present + pub fn action_index(&self, action: Action) -> Option { + self.children() + .iter() + .position(|n| n.lock().prev_action == action) + } } From c2c52f5b18e413709f64c8d2564fa0aaac4d3166 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Wed, 14 Aug 2024 15:59:10 -0700 Subject: [PATCH 20/37] Recursively compute history --- src/game/node.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/game/node.rs b/src/game/node.rs index 4c6fd4b..400a886 100644 --- a/src/game/node.rs +++ b/src/game/node.rs @@ -256,4 +256,16 @@ impl PostFlopNode { .iter() .position(|n| n.lock().prev_action == action) } + + /// Recursively compute the current node's history + pub fn compute_history_recursive(&self, game: &PostFlopGame) -> Option> { + if self.parent_node_index == usize::MAX { + Some(vec![]) + } else { + let p = game.node_arena.get(self.parent_node_index)?; + let mut history = p.lock().compute_history_recursive(game)?; + history.push(p.lock().action_index(self.prev_action)?); + Some(history) + } + } } From a23e31906fabfb824ebb01a6bcaa8d061bd49325 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Sat, 17 Aug 2024 13:36:24 -0700 Subject: [PATCH 21/37] Tmp commit --- examples/file_io_debug.rs | 267 +++++++++++++++++++++++++++----------- src/file.rs | 42 ++++++ src/game/base.rs | 102 ++++++++++----- src/game/interpreter.rs | 20 +++ src/solver.rs | 68 ++++++++++ 5 files changed, 387 insertions(+), 112 deletions(-) diff --git a/examples/file_io_debug.rs b/examples/file_io_debug.rs index f0348a6..d5a625f 100644 --- a/examples/file_io_debug.rs +++ b/examples/file_io_debug.rs @@ -1,97 +1,210 @@ -use std::fs::File; - use postflop_solver::*; -fn main() { - // see `basic.rs` for the explanation of the following code +fn recursive_compare_strategies_helper( + saved: &mut PostFlopGame, + loaded: &mut PostFlopGame, + storage_mode: BoardState, +) { + let history = saved.history().to_vec(); + saved.cache_normalized_weights(); + loaded.cache_normalized_weights(); + + // Check if OOP hands have the same evs + let evs_oop_1 = saved.expected_values(0); + let ws_oop_1 = saved.weights(0); + let evs_oop_2 = loaded.expected_values(1); + let ws_oop_2 = saved.weights(0); + + assert!(ws_oop_1.len() == ws_oop_2.len()); + for (w1, w2) in ws_oop_1.iter().zip(ws_oop_2) { + assert!((w1 - w2).abs() < 0.001); + } + for (i, (e1, e2)) in evs_oop_1.iter().zip(&evs_oop_2).enumerate() { + assert!((e1 - e2).abs() < 0.001, "ev diff({}): {}", i, e1 - e2); + } + + let ev_oop_1 = compute_average(&evs_oop_1, &ws_oop_1); + let ev_oop_2 = compute_average(&evs_oop_2, &ws_oop_2); + + let ev_diff = (ev_oop_1 - ev_oop_2).abs(); + println!("EV Diff: {:0.2}", ev_diff); + assert!((ev_oop_1 - ev_oop_2).abs() < 0.01); + for child_index in 0..saved.available_actions().len() { + saved.play(child_index); + loaded.play(child_index); + + recursive_compare_strategies_helper(saved, loaded, storage_mode); + + saved.apply_history(&history); + loaded.apply_history(&history); + } +} + +fn compare_strategies( + saved: &mut PostFlopGame, + loaded: &mut PostFlopGame, + storage_mode: BoardState, +) { + saved.back_to_root(); + loaded.back_to_root(); + saved.cache_normalized_weights(); + loaded.cache_normalized_weights(); + for (i, ((e1, e2), cards)) in saved + .expected_values(0) + .iter() + .zip(loaded.expected_values(0)) + .zip(saved.private_cards(0)) + .enumerate() + { + println!("ev {}: {}:{}", hole_to_string(*cards).unwrap(), e1, e2); + } + for (i, ((e1, e2), cards)) in saved + .expected_values(1) + .iter() + .zip(loaded.expected_values(1)) + .zip(saved.private_cards(1)) + .enumerate() + { + println!("ev {}: {}:{}", hole_to_string(*cards).unwrap(), e1, e2); + } + recursive_compare_strategies_helper(saved, loaded, storage_mode); +} + +fn print_strats_at_current_node( + g1: &mut PostFlopGame, + g2: &mut PostFlopGame, + actions: &Vec, +) { + let action_string = actions + .iter() + .map(|a| format!("{:?}", a)) + .collect::>() + .join(":"); - let oop_range = "66+,A8s+,A5s-A4s,AJo+,K9s+,KQo,QTs+,JTs,96s+,85s+,75s+,65s,54s"; - let ip_range = "QQ-22,AQs-A2s,ATo+,K5s+,KJo+,Q8s+,J8s+,T7s+,96s+,86s+,75s+,64s+,53s+"; + let player = g1.current_player(); + + println!( + "\x1B[32;1mActions To Reach Node\x1B[0m: [{}]", + action_string + ); + // Print high level node data + if g1.is_chance_node() { + println!("\x1B[32;1mPlayer\x1B[0m: Chance"); + } else if g1.is_terminal_node() { + if player == 0 { + println!("\x1B[32;1mPlayer\x1B[0m: OOP (Terminal)"); + } else { + println!("\x1B[32;1mPlayer\x1B[0m: IP (Terminal)"); + } + } else { + if player == 0 { + println!("\x1B[32;1mPlayer\x1B[0m: OOP"); + } else { + println!("\x1B[32;1mPlayer\x1B[0m: IP"); + } + let private_cards = g1.private_cards(player); + let strat1 = g1.strategy_by_private_hand(); + let strat2 = g2.strategy_by_private_hand(); + let weights1 = g1.weights(player); + let weights2 = g2.weights(player); + let actions = g1.available_actions(); + + // Print both games strategies + for ((cards, (w1, s1)), (w2, s2)) in private_cards + .iter() + .zip(weights1.iter().zip(strat1)) + .zip(weights2.iter().zip(strat2)) + { + let hole_cards = hole_to_string(*cards).unwrap(); + print!("\x1B[34;1m{hole_cards}\x1B[0m@({:.2} v {:.2}) ", w1, w2); + let mut action_frequencies = vec![]; + for (a, (freq1, freq2)) in actions.iter().zip(s1.iter().zip(s2)) { + action_frequencies.push(format!( + "\x1B[32;1m{:?}\x1B[0m: \x1B[31m{:0.3}\x1B[0m v \x1B[33m{:0>.3}\x1B[0m", + a, freq1, freq2 + )) + } + println!("{}", action_frequencies.join(" ")); + } + } +} + +fn main() { + let oop_range = "AA,QQ"; + let ip_range = "KK"; let card_config = CardConfig { range: [oop_range.parse().unwrap(), ip_range.parse().unwrap()], - flop: flop_from_str("Td9d6h").unwrap(), - turn: card_from_str("Qc").unwrap(), - river: NOT_DEALT, + flop: flop_from_str("3h3s3d").unwrap(), + ..Default::default() }; - let bet_sizes = BetSizeOptions::try_from(("60%, e, a", "2.5x")).unwrap(); - let tree_config = TreeConfig { - initial_state: BoardState::Turn, - starting_pot: 200, - effective_stack: 900, + starting_pot: 100, + effective_stack: 100, rake_rate: 0.0, rake_cap: 0.0, - flop_bet_sizes: [bet_sizes.clone(), bet_sizes.clone()], - turn_bet_sizes: [bet_sizes.clone(), bet_sizes.clone()], - river_bet_sizes: [bet_sizes.clone(), bet_sizes], - turn_donk_sizes: None, - river_donk_sizes: Some(DonkSizeOptions::try_from("50%").unwrap()), - add_allin_threshold: 1.5, - force_allin_threshold: 0.15, - merging_threshold: 0.1, + flop_bet_sizes: [("e", "").try_into().unwrap(), ("e", "").try_into().unwrap()], + turn_bet_sizes: [("e", "").try_into().unwrap(), ("e", "").try_into().unwrap()], + river_bet_sizes: [("e", "").try_into().unwrap(), ("e", "").try_into().unwrap()], + ..Default::default() }; let action_tree = ActionTree::new(tree_config).unwrap(); - let mut game = PostFlopGame::with_config(card_config, action_tree).unwrap(); - game.allocate_memory(false); - - let max_num_iterations = 20; - let target_exploitability = game.tree_config().starting_pot as f32 * 0.01; - solve(&mut game, max_num_iterations, target_exploitability, true); - let r = game.set_target_storage_mode(BoardState::Turn); - println!("{r:?}"); - - // save the solved game tree to a file - // 4th argument is zstd compression level (1-22); requires `zstd` feature to use - save_data_to_file(&game, "memo string", "filename.bin", None).unwrap(); - - // load the solved game tree from a file - // 2nd argument is the maximum memory usage in bytes - let (mut game2, _memo_string): (PostFlopGame, _) = - load_data_from_file("filename.bin", None).unwrap(); - - println!("Game 1 Internal Data"); - game.print_internal_data(); - println!("Game 2 Internal Data"); - game2.print_internal_data(); - - // check if the loaded game tree is the same as the original one - game.cache_normalized_weights(); - game2.cache_normalized_weights(); - assert_eq!(game.equity(0), game2.equity(0)); - - // discard information after the river deal when serializing - // this operation does not lose any information of the game tree itself - game2.set_target_storage_mode(BoardState::Turn).unwrap(); - - // compare the memory usage for serialization - println!( - "Memory usage of the original game tree: {:.2}MB", // 11.50MB - game.target_memory_usage() as f64 / (1024.0 * 1024.0) - ); - println!( - "Memory usage of the truncated game tree: {:.2}MB", // 0.79MB - game2.target_memory_usage() as f64 / (1024.0 * 1024.0) - ); + let mut game1 = PostFlopGame::with_config(card_config, action_tree).unwrap(); + game1.allocate_memory(false); + + solve(&mut game1, 100, 0.01, false); + + // save (turn) + game1.set_target_storage_mode(BoardState::Turn).unwrap(); + save_data_to_file(&game1, "", "tmpfile.flop", None).unwrap(); + + // load (turn) + let mut game2: PostFlopGame = load_data_from_file("tmpfile.flop", None).unwrap().0; + // compare_strategies(&mut game, &mut game2, BoardState::Turn); + assert!(game2.rebuild_and_resolve_forgotten_streets().is_ok()); + + let mut actions_so_far = vec![]; + + // Print Root Node + print_strats_at_current_node(&mut game1, &mut game2, &actions_so_far); - // overwrite the file with the truncated game tree - // game tree constructed from this file cannot access information after the river deal - save_data_to_file(&game2, "memo string", "filename.bin", None).unwrap(); - let (mut game3, _memo_string): (PostFlopGame, String) = - load_data_from_file("filename.bin", None).unwrap(); + // OOP: Check + actions_so_far.push(game1.available_actions()[0]); + game1.play(0); + game2.play(0); + print_strats_at_current_node(&mut game1, &mut game2, &actions_so_far); + + // IP: Check + actions_so_far.push(game1.available_actions()[0]); + game1.play(0); + game2.play(0); + print_strats_at_current_node(&mut game1, &mut game2, &actions_so_far); - game.play(0); - game.play(0); - println!("Game X/X Actions: {:?}", game.available_actions()); + // Chance: 2c + actions_so_far.push(game1.available_actions()[0]); + game1.play(0); game2.play(0); + print_strats_at_current_node(&mut game1, &mut game2, &actions_so_far); + + // OOP: CHECK + actions_so_far.push(game1.available_actions()[0]); + game1.play(0); game2.play(0); - println!("Game2 X/X Actions: {:?}", game.available_actions()); - game3.play(0); - game3.play(0); - println!("Game3 X/X Actions: {:?}", game3.available_actions()); + print_strats_at_current_node(&mut game1, &mut game2, &actions_so_far); + + // IP: CHECK + actions_so_far.push(game1.available_actions()[0]); + game1.play(0); + game2.play(0); + print_strats_at_current_node(&mut game1, &mut game2, &actions_so_far); + + // CHANCE: 0 + actions_so_far.push(game1.available_actions()[1]); + game1.play(1); + game2.play(1); + print_strats_at_current_node(&mut game1, &mut game2, &actions_so_far); - // delete the file - std::fs::remove_file("filename.bin").unwrap(); + // compare_strategies(&mut game, &mut game2, BoardState::Turn); } diff --git a/src/file.rs b/src/file.rs index 17e7d5b..d7e90f7 100644 --- a/src/file.rs +++ b/src/file.rs @@ -269,6 +269,7 @@ mod tests { use crate::action_tree::*; use crate::card::*; use crate::range::*; + use crate::solver::solve; use crate::utility::*; #[test] @@ -375,4 +376,45 @@ mod tests { assert!((root_ev_oop - 45.0).abs() < 1e-4); assert!((root_ev_ip - 15.0).abs() < 1e-4); } + + #[test] + fn test_reload_and_resolve() { + let oop_range = "AA,QQ"; + let ip_range = "KK"; + + let card_config = CardConfig { + range: [oop_range.parse().unwrap(), ip_range.parse().unwrap()], + flop: flop_from_str("3h3s3d").unwrap(), + ..Default::default() + }; + + let tree_config = TreeConfig { + starting_pot: 100, + effective_stack: 100, + rake_rate: 0.0, + rake_cap: 0.0, + flop_bet_sizes: [("e", "").try_into().unwrap(), ("e", "").try_into().unwrap()], + turn_bet_sizes: [("e", "").try_into().unwrap(), ("e", "").try_into().unwrap()], + river_bet_sizes: [("e", "").try_into().unwrap(), ("e", "").try_into().unwrap()], + ..Default::default() + }; + + let action_tree = ActionTree::new(tree_config).unwrap(); + let mut game = PostFlopGame::with_config(card_config, action_tree).unwrap(); + println!( + "memory usage: {:.2}GB", + game.memory_usage().0 as f64 / (1024.0 * 1024.0 * 1024.0) + ); + game.allocate_memory(false); + + solve(&mut game, 100, 0.01, false); + + // save (turn) + game.set_target_storage_mode(BoardState::Turn).unwrap(); + save_data_to_file(&game, "", "tmpfile.flop", None).unwrap(); + + // load (turn) + let mut game: PostFlopGame = load_data_from_file("tmpfile.flop", None).unwrap().0; + assert!(game.rebuild_and_resolve_forgotten_streets().is_ok()); + } } diff --git a/src/game/base.rs b/src/game/base.rs index 6dc26c8..94931c1 100644 --- a/src/game/base.rs +++ b/src/game/base.rs @@ -1,13 +1,14 @@ use super::*; use crate::bunching::*; use crate::interface::*; +use crate::solve_with_node_as_root; use crate::utility::*; use std::mem::{self, MaybeUninit}; #[cfg(feature = "rayon")] use rayon::prelude::*; -#[derive(Default)] +#[derive(Default, Debug)] struct BuildTreeInfo { flop_index: usize, turn_index: usize, @@ -768,6 +769,7 @@ impl PostFlopGame { node.num_children += 1; let mut child = node.children().last().unwrap().lock(); child.prev_action = Action::Chance(card); + child.parent_node_index = node_index; child.turn = node.turn; child.river = card; } @@ -845,12 +847,9 @@ impl PostFlopGame { self.num_nodes_per_street = nodes_per_street; - let total_new_nodes_to_allocate = total_num_nodes - self.node_arena.len() as u64; - self.node_arena.append( - &mut (0..total_new_nodes_to_allocate) - .map(|_| MutexLike::new(PostFlopNode::default())) - .collect::>(), - ); + self.node_arena = (0..total_num_nodes) + .map(|_| MutexLike::new(PostFlopNode::default())) + .collect::>(); // self.clear_storage(); let mut info = BuildTreeInfo { @@ -879,64 +878,97 @@ impl PostFlopGame { Ok(()) } - pub fn reload_and_resolve(&mut self, enable_compression: bool) -> Result<(), String> { - self.allocate_memory_after_load(enable_compression)?; + pub fn rebuild_and_resolve_forgotten_streets(&mut self) -> Result<(), String> { + self.check_card_config()?; self.reinit_root()?; + self.allocate_memory_after_load()?; + self.resolve_reloaded_nodes(1000, 0.01, false) + } - // Collect root nodes to resolve - let nodes_to_solve = match self.storage_mode { + /// Return the node index for each root of the forgotten gametrees that were + /// omitted during a partial save. + /// + /// When we perform a partial save (e.g., a flop save), we lose + /// cfvalues/strategy data for all subtrees rooted at the forgotten street + /// (in the case of a flop save, this would be all subtrees rooted at the + /// beginning of the turn). + /// + /// To regain this information we need to resolve each of these subtrees + /// individually. This function collects the index of each such root. + pub fn collect_unsolved_roots_after_reload(&mut self) -> Result, String> { + match self.storage_mode { BoardState::Flop => { let turn_root_nodes = self .node_arena .iter() - .filter(|n| { + .enumerate() + .filter(|(_, n)| { n.lock().turn != NOT_DEALT && n.lock().river == NOT_DEALT && matches!(n.lock().prev_action, Action::Chance(..)) }) + .map(|(i, _)| i) .collect::>(); - turn_root_nodes + Ok(turn_root_nodes) } BoardState::Turn => { let river_root_nodes = self .node_arena .iter() - .filter(|n| { + .enumerate() + .filter(|(_, n)| { n.lock().turn != NOT_DEALT && matches!(n.lock().prev_action, Action::Chance(..)) }) + .map(|(i, _)| i) .collect::>(); - river_root_nodes - } - BoardState::River => vec![], - }; - for node in nodes_to_solve { - // Get history of this node - // let mut history = vec![]; - let mut n = node.lock(); - while n.parent_node_index < usize::MAX { - let parent = self.node_arena[n.parent_node_index].lock(); - let action = n.prev_action; + Ok(river_root_nodes) } + BoardState::River => Ok(vec![]), } + } + + pub fn resolve_reloaded_nodes( + &mut self, + max_num_iterations: u32, + target_exploitability: f32, + print_progress: bool, + ) -> Result<(), String> { + let nodes_to_solve = self.collect_unsolved_roots_after_reload()?; + self.state = State::MemoryAllocated; + for node_idx in nodes_to_solve { + let node = self.node_arena.get(node_idx).ok_or("Invalid node index")?; + // let history = node + // .lock() + // .compute_history_recursive(&self) + // .ok_or("Unable to compute history for node".to_string())? + // .to_vec(); + // self.apply_history(&history); + solve_with_node_as_root( + self, + node.lock(), + max_num_iterations, + target_exploitability, + print_progress, + ); + } + finalize(self); Ok(()) } - /// Reallocate memory for full tree after performing a partial load - pub fn allocate_memory_after_load(&mut self, enable_compression: bool) -> Result<(), String> { + /// Reallocate memory for full tree after performing a partial load. This + /// must be called after `init_root()` + pub fn allocate_memory_after_load(&mut self) -> Result<(), String> { if self.state <= State::Uninitialized { return Err("Game is not successfully initialized".to_string()); } - if self.state == State::MemoryAllocated - && self.storage_mode == BoardState::River - && self.is_compression_enabled == enable_compression - { + if self.state == State::MemoryAllocated && self.storage_mode == BoardState::River { return Ok(()); } - let num_bytes = if enable_compression { 2 } else { 4 }; + let num_bytes = if self.is_compression_enabled { 2 } else { 4 }; if num_bytes * self.num_storage > isize::MAX as u64 || num_bytes * self.num_storage_chance > isize::MAX as u64 { @@ -944,7 +976,7 @@ impl PostFlopGame { } self.state = State::MemoryAllocated; - self.is_compression_enabled = enable_compression; + // self.is_compression_enabled = self.is_compression_enabled; let old_storage1 = std::mem::replace(&mut self.storage1, vec![]); let old_storage2 = std::mem::replace(&mut self.storage2, vec![]); @@ -960,7 +992,7 @@ impl PostFlopGame { self.storage_ip = vec![0; storage_ip_bytes]; self.storage_chance = vec![0; storage_chance_bytes]; - self.allocate_memory_nodes(); + self.allocate_memory_nodes(); // Assign node storage pointers self.storage_mode = BoardState::River; self.target_storage_mode = BoardState::River; @@ -1595,7 +1627,7 @@ impl PostFlopGame { Ok(info) } - /// Allocates memory recursively. + /// Assigns allocated storage memory. fn allocate_memory_nodes(&mut self) { let num_bytes = if self.is_compression_enabled { 2 } else { 4 }; let mut action_counter = 0; diff --git a/src/game/interpreter.rs b/src/game/interpreter.rs index d7845b6..619011d 100644 --- a/src/game/interpreter.rs +++ b/src/game/interpreter.rs @@ -763,6 +763,7 @@ impl PostFlopGame { node.cfvalues_ip().to_vec() } } else if player == self.current_player() { + println!("BINGO"); have_actions = true; if self.is_compression_enabled { let slice = node.cfvalues_compressed(); @@ -846,6 +847,25 @@ impl PostFlopGame { ret } + pub fn strategy_by_private_hand(&self) -> Vec> { + let strat = self.strategy(); + let player = self.current_player(); + let num_hands = self.private_cards(player).len(); + let num_actions = self.available_actions().len(); + assert!(num_hands * num_actions == strat.len()); + let mut strat_by_hand: Vec> = Vec::with_capacity(num_hands); + for j in 0..num_hands { + strat_by_hand.push(Vec::with_capacity(num_actions)); + } + + for i in 0..num_actions { + for j in 0..num_hands { + strat_by_hand[j].push(strat[i * num_hands + j]); + } + } + strat_by_hand + } + /// Returns the total bet amount of each player (OOP, IP). #[inline] pub fn total_bet_amount(&self) -> [i32; 2] { diff --git a/src/solver.rs b/src/solver.rs index 5a1cc5a..02db93c 100644 --- a/src/solver.rs +++ b/src/solver.rs @@ -132,6 +132,74 @@ pub fn solve_step(game: &T, current_iteration: u32) { } } +/// Performs Discounted CFR algorithm until the given number of iterations or exploitability is +/// satisfied. +/// +/// This method returns the exploitability of the obtained strategy. +pub fn solve_with_node_as_root( + game: &mut T, + mut root: MutexGuardLike, + max_num_iterations: u32, + target_exploitability: f32, + print_progress: bool, +) -> f32 { + if game.is_solved() { + panic!("Game is already solved"); + } + + if !game.is_ready() { + panic!("Game is not ready"); + } + + let mut exploitability = compute_exploitability(game); + + if print_progress { + print!("iteration: 0 / {max_num_iterations} "); + print!("(exploitability = {exploitability:.4e})"); + io::stdout().flush().unwrap(); + } + + for t in 0..max_num_iterations { + if exploitability <= target_exploitability { + break; + } + + let params = DiscountParams::new(t); + + // alternating updates + for player in 0..2 { + let mut result = Vec::with_capacity(game.num_private_hands(player)); + solve_recursive( + result.spare_capacity_mut(), + game, + &mut root, + player, + game.initial_weights(player ^ 1), + ¶ms, + ); + } + + if (t + 1) % 10 == 0 || t + 1 == max_num_iterations { + exploitability = compute_exploitability(game); + } + + if print_progress { + print!("\riteration: {} / {} ", t + 1, max_num_iterations); + print!("(exploitability = {exploitability:.4e})"); + io::stdout().flush().unwrap(); + } + } + + if print_progress { + println!(); + io::stdout().flush().unwrap(); + } + + finalize(game); + + exploitability +} + /// Recursively solves the counterfactual values. fn solve_recursive( result: &mut [MaybeUninit], From 414361a631ed059a19cf3cc29576a0789e0d5f6c Mon Sep 17 00:00:00 2001 From: bkushigian Date: Sun, 18 Aug 2024 11:27:52 -0700 Subject: [PATCH 22/37] Documented some sliceops --- src/sliceop.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/sliceop.rs b/src/sliceop.rs index bf726b6..75d0f00 100644 --- a/src/sliceop.rs +++ b/src/sliceop.rs @@ -32,6 +32,7 @@ pub(crate) fn div_slice_uninit( }); } +/// Multiply a source slice by a scalar and store in a destination slice #[inline] pub(crate) fn mul_slice_scalar_uninit(dst: &mut [MaybeUninit], src: &[f32], scalar: f32) { dst.iter_mut().zip(src).for_each(|(d, s)| { @@ -39,6 +40,17 @@ pub(crate) fn mul_slice_scalar_uninit(dst: &mut [MaybeUninit], src: &[f32], }); } +/// Compute a _strided summation_ of `f32` elements in `src`, where the stride +/// length is `dst.len()`. +/// +/// In more detail, break source slice `src` into `N` chunks `C0...CN-1`, where +/// `N = dst.len()`, and set the `i`th element of `dst` to be the sum of the +/// `i`th element of each chunk `Ck`: +/// +/// - `dst[0] = SUM(k=0..N-1, Ck[0])` +/// - `dst[1] = SUM(k=0..N-1, Ck[1])` +/// - `dst[2] = SUM(k=0..N-1, Ck[2])` +/// - ... #[inline] pub(crate) fn sum_slices_uninit<'a>(dst: &'a mut [MaybeUninit], src: &[f32]) -> &'a mut [f32] { let len = dst.len(); @@ -54,6 +66,17 @@ pub(crate) fn sum_slices_uninit<'a>(dst: &'a mut [MaybeUninit], src: &[f32] dst } +/// Compute a _strided summation_ of `f64` elements in `src`, where the stride +/// length is `dst.len()`. +/// +/// In more detail, break source slice `src` into `N` chunks `C0...CN-1`, where +/// `N = dst.len()`, and set the `i`th element of `dst` to be the sum of the +/// `i`th element of each chunk `Ck`: +/// +/// - `dst[0] = SUM(k=0..N-1, Ck[0])` +/// - `dst[1] = SUM(k=0..N-1, Ck[1])` +/// - `dst[2] = SUM(k=0..N-1, Ck[2])` +/// - ... #[inline] pub(crate) fn sum_slices_f64_uninit<'a>( dst: &'a mut [MaybeUninit], From dd8131862ee6e59d0298758e1598bae9fcd175a5 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Sun, 18 Aug 2024 11:38:42 -0700 Subject: [PATCH 23/37] Documented sliceops --- src/sliceop.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/sliceop.rs b/src/sliceop.rs index 75d0f00..b876b5d 100644 --- a/src/sliceop.rs +++ b/src/sliceop.rs @@ -259,11 +259,27 @@ pub(crate) fn inner_product_cond( acc.iter().sum::() as f32 } +/// Extract a reference to a specific "row" from a one-dimensional slice, where +/// the data is conceptually arranged as a two-dimensional array. +/// +/// # Arguments +/// +/// * `slice` - slice to extract a reference from +/// * `index` - the index of the conceptual "row" to reference +/// * `row_size` - the size of the conceptual "row" to reference #[inline] pub(crate) fn row(slice: &[T], index: usize, row_size: usize) -> &[T] { &slice[index * row_size..(index + 1) * row_size] } +/// Extract a mutable reference to a specific "row" from a one-dimensional +/// slice, where the data is conceptually arranged as a two-dimensional array. +/// +/// # Arguments +/// +/// * `slice` - slice to extract a mutable reference from +/// * `index` - the index of the conceptual "row" to reference +/// * `row_size` - the size of the conceptual "row" to reference #[inline] pub(crate) fn row_mut(slice: &mut [T], index: usize, row_size: usize) -> &mut [T] { &mut slice[index * row_size..(index + 1) * row_size] From 87ac61ef87da1986dc23b7b02fb61a64b2a9a1e9 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Sun, 18 Aug 2024 11:50:13 -0700 Subject: [PATCH 24/37] Fixed docs in sliceop --- src/sliceop.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sliceop.rs b/src/sliceop.rs index b876b5d..e250dab 100644 --- a/src/sliceop.rs +++ b/src/sliceop.rs @@ -66,8 +66,8 @@ pub(crate) fn sum_slices_uninit<'a>(dst: &'a mut [MaybeUninit], src: &[f32] dst } -/// Compute a _strided summation_ of `f64` elements in `src`, where the stride -/// length is `dst.len()`. +/// Compute a _strided summation_ of `f32` elements in `src`, where the stride +/// length is `dst.len()`, and store as `f64` in `dst`. /// /// In more detail, break source slice `src` into `N` chunks `C0...CN-1`, where /// `N = dst.len()`, and set the `i`th element of `dst` to be the sum of the From 3c8056dfdbac084717e17f9ef829c6e38b72aea9 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Sun, 18 Aug 2024 12:16:50 -0700 Subject: [PATCH 25/37] Docstrings for sliceops --- src/sliceop.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/sliceop.rs b/src/sliceop.rs index e250dab..e6dbdc9 100644 --- a/src/sliceop.rs +++ b/src/sliceop.rs @@ -11,6 +11,22 @@ pub(crate) fn mul_slice(lhs: &mut [f32], rhs: &[f32]) { lhs.iter_mut().zip(rhs).for_each(|(l, r)| *l *= *r); } +/// Divides each element of the left-hand side (`lhs`) slice by the +/// corresponding element of the right-hand side (`rhs`) slice, modifying the +/// `lhs` slice in place. If an element in `rhs` is zero, the corresponding +/// element in `lhs` is set to a specified `default` value instead of performing +/// the division. +/// +/// # Arguments +/// +/// - `lhs`: A mutable reference to the left-hand side slice, which will be +/// modified in place. Each element of this slice is divided by the +/// corresponding element in the `rhs` slice, or set to `default` if the +/// corresponding element in `rhs` is zero. +/// - `rhs`: A reference to the right-hand side slice, which provides the +/// divisor for each element in `lhs`. +/// - `default: f32`: A fallback value that is used for elements in `lhs` where +/// the corresponding element in `rhs` is zero. #[inline] pub(crate) fn div_slice(lhs: &mut [f32], rhs: &[f32], default: f32) { lhs.iter_mut() @@ -18,6 +34,22 @@ pub(crate) fn div_slice(lhs: &mut [f32], rhs: &[f32], default: f32) { .for_each(|(l, r)| *l = if is_zero(*r) { default } else { *l / *r }); } +/// Divides each element of the left-hand side (`lhs`) slice by the +/// corresponding element of the right-hand side (`rhs`) slice, modifying the +/// `lhs` slice in place. If an element in `rhs` is zero, the corresponding +/// element in `lhs` is set to a specified `default` value instead of performing +/// the division. +/// +/// # Arguments +/// +/// - `lhs`: A mutable reference to the left-hand side slice, which will be +/// modified in place. Each element of this slice is divided by the +/// corresponding element in the `rhs` slice, or set to `default` if the +/// corresponding element in `rhs` is zero. +/// - `rhs`: A reference to the right-hand side slice, which provides the +/// divisor for each element in `lhs`. +/// - `default: f32`: A fallback value that is used for elements in `lhs` where +/// the corresponding element in `rhs` is zero. #[inline] pub(crate) fn div_slice_uninit( dst: &mut [MaybeUninit], From 3b7606bfff01f1846a65d4a61ddda66574db89f0 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Sun, 18 Aug 2024 15:26:48 -0700 Subject: [PATCH 26/37] Docs and rename --- src/game/base.rs | 10 ++++---- src/sliceop.rs | 54 ++++++++++++++++++++++++++++++++++++++ src/solver.rs | 67 ++++++++++++++++++++++++++++++++++++++++++------ src/utility.rs | 17 +++++++----- 4 files changed, 129 insertions(+), 19 deletions(-) diff --git a/src/game/base.rs b/src/game/base.rs index 94931c1..8d0cf43 100644 --- a/src/game/base.rs +++ b/src/game/base.rs @@ -1254,7 +1254,7 @@ impl PostFlopGame { let player_swap = swap_option.map(|swap| { let mut tmp = (0..player_len).collect::>(); - apply_swap(&mut tmp, &swap[player]); + apply_swap_list(&mut tmp, &swap[player]); tmp }); @@ -1276,8 +1276,8 @@ impl PostFlopGame { let slices = if let Some(swap) = swap_option { tmp.0.extend_from_slice(&arena[index..index + opponent_len]); tmp.1.extend_from_slice(opponent_strength); - apply_swap(&mut tmp.0, &swap[player ^ 1]); - apply_swap(&mut tmp.1, &swap[player ^ 1]); + apply_swap_list(&mut tmp.0, &swap[player ^ 1]); + apply_swap_list(&mut tmp.1, &swap[player ^ 1]); (tmp.0.as_slice(), &tmp.1) } else { (&arena[index..index + opponent_len], opponent_strength) @@ -1348,7 +1348,7 @@ impl PostFlopGame { let player_swap = swap_option.map(|swap| { let mut tmp = (0..player_len).collect::>(); - apply_swap(&mut tmp, &swap[player]); + apply_swap_list(&mut tmp, &swap[player]); tmp }); @@ -1365,7 +1365,7 @@ impl PostFlopGame { let slice = &arena[index..index + opponent_len]; let slice = if let Some(swap) = swap_option { tmp.extend_from_slice(slice); - apply_swap(&mut tmp, &swap[player ^ 1]); + apply_swap_list(&mut tmp, &swap[player ^ 1]); &tmp } else { slice diff --git a/src/sliceop.rs b/src/sliceop.rs index e6dbdc9..43ac497 100644 --- a/src/sliceop.rs +++ b/src/sliceop.rs @@ -1,11 +1,31 @@ use crate::utility::*; use std::mem::MaybeUninit; +/// Subtracts each element of the left-hand side (`lhs`) slice by the +/// corresponding element of the right-hand side (`rhs`) slice, modifying the +/// `lhs` slice in place. +/// +/// # Arguments +/// +/// - `lhs`: A mutable reference to the left-hand side slice, which will be +/// modified in place. +/// - `rhs`: A reference to the right-hand side slice, which provides the +/// values to be subtracted from each corresponding element in `lhs`. #[inline] pub(crate) fn sub_slice(lhs: &mut [f32], rhs: &[f32]) { lhs.iter_mut().zip(rhs).for_each(|(l, r)| *l -= *r); } +/// Multiplies each element of the left-hand side (`lhs`) slice by the +/// corresponding element of the right-hand side (`rhs`) slice, modifying the +/// `lhs` slice in place. +/// +/// # Arguments +/// +/// - `lhs`: A mutable reference to the left-hand side slice, which will be +/// modified in place. +/// - `rhs`: A reference to the right-hand side slice, which provides the +/// values to be multiplied against each corresponding element in `lhs`. #[inline] pub(crate) fn mul_slice(lhs: &mut [f32], rhs: &[f32]) { lhs.iter_mut().zip(rhs).for_each(|(l, r)| *l *= *r); @@ -127,6 +147,40 @@ pub(crate) fn sum_slices_f64_uninit<'a>( dst } +/// Performs a fused multiply-add (FMA) operation on slices, storing the result +/// in a destination slice. +/// +/// This function multiplies the first `dst.len()` corresponding elements of the +/// two source slices (`src1` and `src2`) and stores the results in the +/// destination slice (`dst`). After the initial multiplication, it continues +/// to perform additional multiply-add operations using subsequent chunks of +/// `src1` and `src2`, adding the products to the already computed values in +/// `dst`. +/// +/// # Arguments +/// +/// - `dst`: A mutable reference to a slice of uninitialized memory where the +/// results will be stored. The length of this slice dictates how many +/// elements are processed in the initial operation. +/// - `src1`: A reference to the first source slice, providing the +/// multiplicands. +/// - `src2`: A reference to the second source slice, providing the multipliers. +/// +/// # Returns +/// +/// A mutable reference to the `dst` slice, now reinterpreted as a fully +/// initialized slice of `f32` values, containing the results of the fused +/// multiply-add operations. +/// +/// # Safety +/// +/// - This function assumes that the length of `dst` is equal to or less than +/// the length of `src1` and `src2`. If the lengths are mismatched, the function +/// might cause undefined behavior due to out-of-bounds memory access. +/// - The function uses unsafe code to cast the `MaybeUninit` slice into a +/// `f32` slice after initialization. This is safe only if the `dst` slice is +/// properly initialized with valid `f32` values, as is ensured by the +/// function's implementation. #[inline] pub(crate) fn fma_slices_uninit<'a>( dst: &'a mut [MaybeUninit], diff --git a/src/solver.rs b/src/solver.rs index 02db93c..6dfe75c 100644 --- a/src/solver.rs +++ b/src/solver.rs @@ -9,8 +9,11 @@ use std::mem::MaybeUninit; use crate::alloc::*; struct DiscountParams { + // coefficient for accumulated positive regrets alpha_t: f32, + // coefficient for accumulated negative regrets beta_t: f32, + // contributions to average strategy gamma_t: f32, } @@ -200,7 +203,16 @@ pub fn solve_with_node_as_root( exploitability } -/// Recursively solves the counterfactual values. +/// Recursively solves the counterfactual values and store them in `result`. +/// +/// # Arguments +/// +/// * `result` - slice to store resulting counterfactual regret values +/// * `game` - reference to the game we are solving +/// * `node` - current node we are solving +/// * `player` - current player we are solving for +/// * `cfreach` - the probability of reaching this point with a particular private hand +/// * `params` - the DiscountParams that parametrize the solver fn solve_recursive( result: &mut [MaybeUninit], game: &T, @@ -225,7 +237,14 @@ fn solve_recursive( return; } - // allocate memory for storing the counterfactual values + // Allocate memory for storing the counterfactual values. Conceptually this + // is a `num_actions * num_hands` 2-dimensional array, where the `i`th + // row (which has length `num_hands`) corresponds to the cfvalues of each + // hand after taking the `i`th action. + // + // Rows are obtained using operations from `sliceop` (e.g., `sliceop::row_mut()`). + // + // `cfv_actions` will be written to by recursive calls to `solve_recursive`. #[cfg(feature = "custom-alloc")] let cfv_actions = MutexLike::new(Vec::with_capacity_in(num_actions * num_hands, StackAlloc)); #[cfg(not(feature = "custom-alloc"))] @@ -257,13 +276,15 @@ fn solve_recursive( ); }); - // use 64-bit floating point values + // use 64-bit floating point values for precision during summations + // before demoting back to f32 #[cfg(feature = "custom-alloc")] let mut result_f64 = Vec::with_capacity_in(num_hands, StackAlloc); #[cfg(not(feature = "custom-alloc"))] let mut result_f64 = Vec::with_capacity(num_hands); - // sum up the counterfactual values + // compute the strided summation of the counterfactual values for each + // hand and store in `result_f64` let mut cfv_actions = cfv_actions.lock(); unsafe { cfv_actions.set_len(num_actions * num_hands) }; sum_slices_f64_uninit(result_f64.spare_capacity_mut(), &cfv_actions); @@ -277,13 +298,13 @@ fn solve_recursive( let swap_list = &game.isomorphic_swap(node, i)[player]; let tmp = row_mut(&mut cfv_actions, isomorphic_index as usize, num_hands); - apply_swap(tmp, swap_list); + apply_swap_list(tmp, swap_list); result_f64.iter_mut().zip(&*tmp).for_each(|(r, &v)| { *r += v as f64; }); - apply_swap(tmp, swap_list); + apply_swap_list(tmp, swap_list); } result.iter_mut().zip(&result_f64).for_each(|(r, &v)| { @@ -304,7 +325,7 @@ fn solve_recursive( ); }); - // compute the strategy by regret-maching algorithm + // compute the strategy by regret-matching algorithm let mut strategy = if game.is_compression_enabled() { regret_matching_compressed(node.regrets_compressed(), num_actions) } else { @@ -315,7 +336,17 @@ fn solve_recursive( let locking = game.locking_strategy(node); apply_locking_strategy(&mut strategy, locking); - // sum up the counterfactual values + // Compute the counterfactual values for each hand, which for hand `h` is + // computed to be the sum over actions `a` of the frequency with which + // `h` takes action `a` and the regret of hand `h` taking action `a`. + // In pseudocode, this is: + // + // ``` + // result[h] = sum([freq(h, a) * regret(h, a) for a in actions]) + // ``` + // + // This sum-of-products us computed as a fused multiply-add using + // `fma_slices_uninit` and is stored in `result`. let mut cfv_actions = cfv_actions.lock(); unsafe { cfv_actions.set_len(num_actions * num_hands) }; let result = fma_slices_uninit(result, &strategy, &cfv_actions); @@ -367,6 +398,7 @@ fn solve_recursive( node.set_regret_scale(new_scale); } else { // update the cumulative strategy + // - `gamma` is used to discount cumulative strategy contributions let gamma = params.gamma_t; let cum_strategy = node.strategy_mut(); cum_strategy.iter_mut().zip(&strategy).for_each(|(x, y)| { @@ -374,6 +406,8 @@ fn solve_recursive( }); // update the cumulative regret + // - alpha is used to discount positive cumulative regrets + // - beta is used to discount negative cumulative regrets let (alpha, beta) = (params.alpha_t, params.beta_t); let cum_regret = node.regrets_mut(); cum_regret.iter_mut().zip(&*cfv_actions).for_each(|(x, y)| { @@ -448,6 +482,18 @@ fn regret_matching(regret: &[f32], num_actions: usize) -> Vec { } /// Computes the strategy by regret-matching algorithm. +/// +/// The resulting strategy has each element (e.g., a hand like **AdQs**) take +/// an action proportional to its regret, where negative regrets are interpreted +/// as zero. +/// +/// # Arguments +/// +/// * `regret` - slice of regrets for the current decision point, one "row" of +/// for each action. The `i`th row contains the regrets of each strategically +/// distinct element (e.g., in holdem an element would be a hole card) for +/// taking the `i`th action. +/// * `num_actions` - the number of actions represented in `regret`. #[cfg(not(feature = "custom-alloc"))] #[inline] fn regret_matching(regret: &[f32], num_actions: usize) -> Vec { @@ -459,10 +505,15 @@ fn regret_matching(regret: &[f32], num_actions: usize) -> Vec { unsafe { strategy.set_len(regret.len()) }; let row_size = regret.len() / num_actions; + + // We want to normalize each element's strategy, so compute the element-wise + // denominator by computing the strided summation of strategy let mut denom = Vec::with_capacity(row_size); sum_slices_uninit(denom.spare_capacity_mut(), &strategy); unsafe { denom.set_len(row_size) }; + // We set the default to be equally distributed across all options. This is + // used when a strategy for a particular hand is uniformly zero. let default = 1.0 / num_actions as f32; strategy.chunks_exact_mut(row_size).for_each(|row| { div_slice(row, &denom, default); diff --git a/src/utility.rs b/src/utility.rs index c834de7..d38208b 100644 --- a/src/utility.rs +++ b/src/utility.rs @@ -227,9 +227,14 @@ pub(crate) fn encode_unsigned_slice(dst: &mut [u16], slice: &[f32]) -> f32 { scale } -/// Applies the given swap to the given slice. +/// Applies the given list of swaps to the given slice. +/// +/// # Arguments +/// +/// * `slice` - mutable slice to perform swaps on +/// * `swap_list` - a list of index pairs to swap #[inline] -pub(crate) fn apply_swap(slice: &mut [T], swap_list: &[(u16, u16)]) { +pub(crate) fn apply_swap_list(slice: &mut [T], swap_list: &[(u16, u16)]) { for &(i, j) in swap_list { unsafe { ptr::swap( @@ -425,13 +430,13 @@ fn compute_cfvalue_recursive( let swap_list = &game.isomorphic_swap(node, i)[player]; let tmp = row_mut(&mut cfv_actions, isomorphic_index as usize, num_hands); - apply_swap(tmp, swap_list); + apply_swap_list(tmp, swap_list); result_f64.iter_mut().zip(&*tmp).for_each(|(r, &v)| { *r += v as f64; }); - apply_swap(tmp, swap_list); + apply_swap_list(tmp, swap_list); } result.iter_mut().zip(&result_f64).for_each(|(r, &v)| { @@ -637,13 +642,13 @@ fn compute_best_cfv_recursive( let swap_list = &game.isomorphic_swap(node, i)[player]; let tmp = row_mut(&mut cfv_actions, isomorphic_index as usize, num_hands); - apply_swap(tmp, swap_list); + apply_swap_list(tmp, swap_list); result_f64.iter_mut().zip(&*tmp).for_each(|(r, &v)| { *r += v as f64; }); - apply_swap(tmp, swap_list); + apply_swap_list(tmp, swap_list); } result.iter_mut().zip(&result_f64).for_each(|(r, &v)| { From af355788429e3bce3c43880ea6078dbb5ab1f179 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Sun, 18 Aug 2024 15:28:34 -0700 Subject: [PATCH 27/37] Docs --- src/sliceop.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/sliceop.rs b/src/sliceop.rs index 43ac497..f99ad70 100644 --- a/src/sliceop.rs +++ b/src/sliceop.rs @@ -171,16 +171,6 @@ pub(crate) fn sum_slices_f64_uninit<'a>( /// A mutable reference to the `dst` slice, now reinterpreted as a fully /// initialized slice of `f32` values, containing the results of the fused /// multiply-add operations. -/// -/// # Safety -/// -/// - This function assumes that the length of `dst` is equal to or less than -/// the length of `src1` and `src2`. If the lengths are mismatched, the function -/// might cause undefined behavior due to out-of-bounds memory access. -/// - The function uses unsafe code to cast the `MaybeUninit` slice into a -/// `f32` slice after initialization. This is safe only if the `dst` slice is -/// properly initialized with valid `f32` values, as is ensured by the -/// function's implementation. #[inline] pub(crate) fn fma_slices_uninit<'a>( dst: &'a mut [MaybeUninit], From d879d6a0a38ad8577c5a374ec4c267a515d08b4f Mon Sep 17 00:00:00 2001 From: bkushigian Date: Sun, 18 Aug 2024 17:31:35 -0700 Subject: [PATCH 28/37] Tmp: splitting branches --- src/game/base.rs | 183 ----------------------------------------------- 1 file changed, 183 deletions(-) diff --git a/src/game/base.rs b/src/game/base.rs index 8d0cf43..ac8e9a8 100644 --- a/src/game/base.rs +++ b/src/game/base.rs @@ -829,189 +829,6 @@ impl PostFlopGame { info.num_storage_ip += node.num_elements_ip as u64; } - /* REBUILDING AND RESOLVING TREE */ - - /// Like `init_root`, but applied to a partial save loaded from disk. This - /// reallocates missing `PostFlopNode`s to `node_arena` and reruns - /// `build_tree_recursive`. Rerunning `build_tree_recursive` will not alter - /// nodes loaded from disk. - pub fn reinit_root(&mut self) -> Result<(), String> { - let nodes_per_street = self.count_nodes_per_street(); - let total_num_nodes = nodes_per_street[0] + nodes_per_street[1] + nodes_per_street[2]; - - if total_num_nodes > u32::MAX as u64 - || mem::size_of::() as u64 * total_num_nodes > isize::MAX as u64 - { - return Err("Too many nodes".to_string()); - } - - self.num_nodes_per_street = nodes_per_street; - - self.node_arena = (0..total_num_nodes) - .map(|_| MutexLike::new(PostFlopNode::default())) - .collect::>(); - // self.clear_storage(); - - let mut info = BuildTreeInfo { - turn_index: nodes_per_street[0] as usize, - river_index: (nodes_per_street[0] + nodes_per_street[1]) as usize, - ..Default::default() - }; - - match self.tree_config.initial_state { - BoardState::Flop => info.flop_index += 1, - BoardState::Turn => info.turn_index += 1, - BoardState::River => info.river_index += 1, - } - - let mut root = self.node_arena[0].lock(); - root.turn = self.card_config.turn; - root.river = self.card_config.river; - - self.build_tree_recursive(0, &self.action_root.lock(), &mut info); - - self.num_storage = info.num_storage; - self.num_storage_ip = info.num_storage_ip; - self.num_storage_chance = info.num_storage_chance; - self.misc_memory_usage = self.memory_usage_internal(); - - Ok(()) - } - - pub fn rebuild_and_resolve_forgotten_streets(&mut self) -> Result<(), String> { - self.check_card_config()?; - self.reinit_root()?; - self.allocate_memory_after_load()?; - self.resolve_reloaded_nodes(1000, 0.01, false) - } - - /// Return the node index for each root of the forgotten gametrees that were - /// omitted during a partial save. - /// - /// When we perform a partial save (e.g., a flop save), we lose - /// cfvalues/strategy data for all subtrees rooted at the forgotten street - /// (in the case of a flop save, this would be all subtrees rooted at the - /// beginning of the turn). - /// - /// To regain this information we need to resolve each of these subtrees - /// individually. This function collects the index of each such root. - pub fn collect_unsolved_roots_after_reload(&mut self) -> Result, String> { - match self.storage_mode { - BoardState::Flop => { - let turn_root_nodes = self - .node_arena - .iter() - .enumerate() - .filter(|(_, n)| { - n.lock().turn != NOT_DEALT - && n.lock().river == NOT_DEALT - && matches!(n.lock().prev_action, Action::Chance(..)) - }) - .map(|(i, _)| i) - .collect::>(); - Ok(turn_root_nodes) - } - BoardState::Turn => { - let river_root_nodes = self - .node_arena - .iter() - .enumerate() - .filter(|(_, n)| { - n.lock().turn != NOT_DEALT - && matches!(n.lock().prev_action, Action::Chance(..)) - }) - .map(|(i, _)| i) - .collect::>(); - Ok(river_root_nodes) - } - BoardState::River => Ok(vec![]), - } - } - - pub fn resolve_reloaded_nodes( - &mut self, - max_num_iterations: u32, - target_exploitability: f32, - print_progress: bool, - ) -> Result<(), String> { - let nodes_to_solve = self.collect_unsolved_roots_after_reload()?; - self.state = State::MemoryAllocated; - for node_idx in nodes_to_solve { - let node = self.node_arena.get(node_idx).ok_or("Invalid node index")?; - // let history = node - // .lock() - // .compute_history_recursive(&self) - // .ok_or("Unable to compute history for node".to_string())? - // .to_vec(); - // self.apply_history(&history); - solve_with_node_as_root( - self, - node.lock(), - max_num_iterations, - target_exploitability, - print_progress, - ); - } - finalize(self); - - Ok(()) - } - - /// Reallocate memory for full tree after performing a partial load. This - /// must be called after `init_root()` - pub fn allocate_memory_after_load(&mut self) -> Result<(), String> { - if self.state <= State::Uninitialized { - return Err("Game is not successfully initialized".to_string()); - } - - if self.state == State::MemoryAllocated && self.storage_mode == BoardState::River { - return Ok(()); - } - - let num_bytes = if self.is_compression_enabled { 2 } else { 4 }; - if num_bytes * self.num_storage > isize::MAX as u64 - || num_bytes * self.num_storage_chance > isize::MAX as u64 - { - return Err("Memory usage exceeds maximum size".to_string()); - } - - self.state = State::MemoryAllocated; - // self.is_compression_enabled = self.is_compression_enabled; - - let old_storage1 = std::mem::replace(&mut self.storage1, vec![]); - let old_storage2 = std::mem::replace(&mut self.storage2, vec![]); - let old_storage_ip = std::mem::replace(&mut self.storage_ip, vec![]); - let old_storage_chance = std::mem::replace(&mut self.storage_chance, vec![]); - - let storage_bytes = (num_bytes * self.num_storage) as usize; - let storage_ip_bytes = (num_bytes * self.num_storage_ip) as usize; - let storage_chance_bytes = (num_bytes * self.num_storage_chance) as usize; - - self.storage1 = vec![0; storage_bytes]; - self.storage2 = vec![0; storage_bytes]; - self.storage_ip = vec![0; storage_ip_bytes]; - self.storage_chance = vec![0; storage_chance_bytes]; - - self.allocate_memory_nodes(); // Assign node storage pointers - - self.storage_mode = BoardState::River; - self.target_storage_mode = BoardState::River; - - for (dst, src) in self.storage1.iter_mut().zip(&old_storage1) { - *dst = *src; - } - for (dst, src) in self.storage2.iter_mut().zip(&old_storage2) { - *dst = *src; - } - for (dst, src) in self.storage_ip.iter_mut().zip(&old_storage_ip) { - *dst = *src; - } - for (dst, src) in self.storage_chance.iter_mut().zip(&old_storage_chance) { - *dst = *src; - } - Ok(()) - } - /// Sets the bunching effect. fn set_bunching_effect_internal(&mut self, bunching_data: &BunchingData) -> Result<(), String> { self.bunching_num_dead_cards = bunching_data.fold_ranges().len() * 2; From 2de67198316086c5eba931477504f388f58fbe3e Mon Sep 17 00:00:00 2001 From: bkushigian Date: Sun, 18 Aug 2024 17:36:26 -0700 Subject: [PATCH 29/37] Branch refactor: removed solve_with_node_as_root --- src/solver.rs | 68 --------------------------------------------------- 1 file changed, 68 deletions(-) diff --git a/src/solver.rs b/src/solver.rs index 6dfe75c..1548a1a 100644 --- a/src/solver.rs +++ b/src/solver.rs @@ -135,74 +135,6 @@ pub fn solve_step(game: &T, current_iteration: u32) { } } -/// Performs Discounted CFR algorithm until the given number of iterations or exploitability is -/// satisfied. -/// -/// This method returns the exploitability of the obtained strategy. -pub fn solve_with_node_as_root( - game: &mut T, - mut root: MutexGuardLike, - max_num_iterations: u32, - target_exploitability: f32, - print_progress: bool, -) -> f32 { - if game.is_solved() { - panic!("Game is already solved"); - } - - if !game.is_ready() { - panic!("Game is not ready"); - } - - let mut exploitability = compute_exploitability(game); - - if print_progress { - print!("iteration: 0 / {max_num_iterations} "); - print!("(exploitability = {exploitability:.4e})"); - io::stdout().flush().unwrap(); - } - - for t in 0..max_num_iterations { - if exploitability <= target_exploitability { - break; - } - - let params = DiscountParams::new(t); - - // alternating updates - for player in 0..2 { - let mut result = Vec::with_capacity(game.num_private_hands(player)); - solve_recursive( - result.spare_capacity_mut(), - game, - &mut root, - player, - game.initial_weights(player ^ 1), - ¶ms, - ); - } - - if (t + 1) % 10 == 0 || t + 1 == max_num_iterations { - exploitability = compute_exploitability(game); - } - - if print_progress { - print!("\riteration: {} / {} ", t + 1, max_num_iterations); - print!("(exploitability = {exploitability:.4e})"); - io::stdout().flush().unwrap(); - } - } - - if print_progress { - println!(); - io::stdout().flush().unwrap(); - } - - finalize(game); - - exploitability -} - /// Recursively solves the counterfactual values and store them in `result`. /// /// # Arguments From edeb03e2421c850401d7e5c02e63fceb38e50b94 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Sun, 18 Aug 2024 17:40:57 -0700 Subject: [PATCH 30/37] Branch refactor --- src/game/base.rs | 1 - src/game/interpreter.rs | 19 ------------------- 2 files changed, 20 deletions(-) diff --git a/src/game/base.rs b/src/game/base.rs index ac8e9a8..7ff5f8b 100644 --- a/src/game/base.rs +++ b/src/game/base.rs @@ -1,7 +1,6 @@ use super::*; use crate::bunching::*; use crate::interface::*; -use crate::solve_with_node_as_root; use crate::utility::*; use std::mem::{self, MaybeUninit}; diff --git a/src/game/interpreter.rs b/src/game/interpreter.rs index 619011d..652e990 100644 --- a/src/game/interpreter.rs +++ b/src/game/interpreter.rs @@ -847,25 +847,6 @@ impl PostFlopGame { ret } - pub fn strategy_by_private_hand(&self) -> Vec> { - let strat = self.strategy(); - let player = self.current_player(); - let num_hands = self.private_cards(player).len(); - let num_actions = self.available_actions().len(); - assert!(num_hands * num_actions == strat.len()); - let mut strat_by_hand: Vec> = Vec::with_capacity(num_hands); - for j in 0..num_hands { - strat_by_hand.push(Vec::with_capacity(num_actions)); - } - - for i in 0..num_actions { - for j in 0..num_hands { - strat_by_hand[j].push(strat[i * num_hands + j]); - } - } - strat_by_hand - } - /// Returns the total bet amount of each player (OOP, IP). #[inline] pub fn total_bet_amount(&self) -> [i32; 2] { From 1989fb68653dc94eb337836878bda6522ebacc6f Mon Sep 17 00:00:00 2001 From: bkushigian Date: Sun, 18 Aug 2024 18:02:35 -0700 Subject: [PATCH 31/37] Refactored/removed unused file_io_debug.rs --- examples/file_io_debug.rs | 210 -------------------------------------- 1 file changed, 210 deletions(-) delete mode 100644 examples/file_io_debug.rs diff --git a/examples/file_io_debug.rs b/examples/file_io_debug.rs deleted file mode 100644 index d5a625f..0000000 --- a/examples/file_io_debug.rs +++ /dev/null @@ -1,210 +0,0 @@ -use postflop_solver::*; - -fn recursive_compare_strategies_helper( - saved: &mut PostFlopGame, - loaded: &mut PostFlopGame, - storage_mode: BoardState, -) { - let history = saved.history().to_vec(); - saved.cache_normalized_weights(); - loaded.cache_normalized_weights(); - - // Check if OOP hands have the same evs - let evs_oop_1 = saved.expected_values(0); - let ws_oop_1 = saved.weights(0); - let evs_oop_2 = loaded.expected_values(1); - let ws_oop_2 = saved.weights(0); - - assert!(ws_oop_1.len() == ws_oop_2.len()); - for (w1, w2) in ws_oop_1.iter().zip(ws_oop_2) { - assert!((w1 - w2).abs() < 0.001); - } - for (i, (e1, e2)) in evs_oop_1.iter().zip(&evs_oop_2).enumerate() { - assert!((e1 - e2).abs() < 0.001, "ev diff({}): {}", i, e1 - e2); - } - - let ev_oop_1 = compute_average(&evs_oop_1, &ws_oop_1); - let ev_oop_2 = compute_average(&evs_oop_2, &ws_oop_2); - - let ev_diff = (ev_oop_1 - ev_oop_2).abs(); - println!("EV Diff: {:0.2}", ev_diff); - assert!((ev_oop_1 - ev_oop_2).abs() < 0.01); - for child_index in 0..saved.available_actions().len() { - saved.play(child_index); - loaded.play(child_index); - - recursive_compare_strategies_helper(saved, loaded, storage_mode); - - saved.apply_history(&history); - loaded.apply_history(&history); - } -} - -fn compare_strategies( - saved: &mut PostFlopGame, - loaded: &mut PostFlopGame, - storage_mode: BoardState, -) { - saved.back_to_root(); - loaded.back_to_root(); - saved.cache_normalized_weights(); - loaded.cache_normalized_weights(); - for (i, ((e1, e2), cards)) in saved - .expected_values(0) - .iter() - .zip(loaded.expected_values(0)) - .zip(saved.private_cards(0)) - .enumerate() - { - println!("ev {}: {}:{}", hole_to_string(*cards).unwrap(), e1, e2); - } - for (i, ((e1, e2), cards)) in saved - .expected_values(1) - .iter() - .zip(loaded.expected_values(1)) - .zip(saved.private_cards(1)) - .enumerate() - { - println!("ev {}: {}:{}", hole_to_string(*cards).unwrap(), e1, e2); - } - recursive_compare_strategies_helper(saved, loaded, storage_mode); -} - -fn print_strats_at_current_node( - g1: &mut PostFlopGame, - g2: &mut PostFlopGame, - actions: &Vec, -) { - let action_string = actions - .iter() - .map(|a| format!("{:?}", a)) - .collect::>() - .join(":"); - - let player = g1.current_player(); - - println!( - "\x1B[32;1mActions To Reach Node\x1B[0m: [{}]", - action_string - ); - // Print high level node data - if g1.is_chance_node() { - println!("\x1B[32;1mPlayer\x1B[0m: Chance"); - } else if g1.is_terminal_node() { - if player == 0 { - println!("\x1B[32;1mPlayer\x1B[0m: OOP (Terminal)"); - } else { - println!("\x1B[32;1mPlayer\x1B[0m: IP (Terminal)"); - } - } else { - if player == 0 { - println!("\x1B[32;1mPlayer\x1B[0m: OOP"); - } else { - println!("\x1B[32;1mPlayer\x1B[0m: IP"); - } - let private_cards = g1.private_cards(player); - let strat1 = g1.strategy_by_private_hand(); - let strat2 = g2.strategy_by_private_hand(); - let weights1 = g1.weights(player); - let weights2 = g2.weights(player); - let actions = g1.available_actions(); - - // Print both games strategies - for ((cards, (w1, s1)), (w2, s2)) in private_cards - .iter() - .zip(weights1.iter().zip(strat1)) - .zip(weights2.iter().zip(strat2)) - { - let hole_cards = hole_to_string(*cards).unwrap(); - print!("\x1B[34;1m{hole_cards}\x1B[0m@({:.2} v {:.2}) ", w1, w2); - let mut action_frequencies = vec![]; - for (a, (freq1, freq2)) in actions.iter().zip(s1.iter().zip(s2)) { - action_frequencies.push(format!( - "\x1B[32;1m{:?}\x1B[0m: \x1B[31m{:0.3}\x1B[0m v \x1B[33m{:0>.3}\x1B[0m", - a, freq1, freq2 - )) - } - println!("{}", action_frequencies.join(" ")); - } - } -} - -fn main() { - let oop_range = "AA,QQ"; - let ip_range = "KK"; - - let card_config = CardConfig { - range: [oop_range.parse().unwrap(), ip_range.parse().unwrap()], - flop: flop_from_str("3h3s3d").unwrap(), - ..Default::default() - }; - - let tree_config = TreeConfig { - starting_pot: 100, - effective_stack: 100, - rake_rate: 0.0, - rake_cap: 0.0, - flop_bet_sizes: [("e", "").try_into().unwrap(), ("e", "").try_into().unwrap()], - turn_bet_sizes: [("e", "").try_into().unwrap(), ("e", "").try_into().unwrap()], - river_bet_sizes: [("e", "").try_into().unwrap(), ("e", "").try_into().unwrap()], - ..Default::default() - }; - - let action_tree = ActionTree::new(tree_config).unwrap(); - let mut game1 = PostFlopGame::with_config(card_config, action_tree).unwrap(); - game1.allocate_memory(false); - - solve(&mut game1, 100, 0.01, false); - - // save (turn) - game1.set_target_storage_mode(BoardState::Turn).unwrap(); - save_data_to_file(&game1, "", "tmpfile.flop", None).unwrap(); - - // load (turn) - let mut game2: PostFlopGame = load_data_from_file("tmpfile.flop", None).unwrap().0; - // compare_strategies(&mut game, &mut game2, BoardState::Turn); - assert!(game2.rebuild_and_resolve_forgotten_streets().is_ok()); - - let mut actions_so_far = vec![]; - - // Print Root Node - print_strats_at_current_node(&mut game1, &mut game2, &actions_so_far); - - // OOP: Check - actions_so_far.push(game1.available_actions()[0]); - game1.play(0); - game2.play(0); - print_strats_at_current_node(&mut game1, &mut game2, &actions_so_far); - - // IP: Check - actions_so_far.push(game1.available_actions()[0]); - game1.play(0); - game2.play(0); - print_strats_at_current_node(&mut game1, &mut game2, &actions_so_far); - - // Chance: 2c - actions_so_far.push(game1.available_actions()[0]); - game1.play(0); - game2.play(0); - print_strats_at_current_node(&mut game1, &mut game2, &actions_so_far); - - // OOP: CHECK - actions_so_far.push(game1.available_actions()[0]); - game1.play(0); - game2.play(0); - print_strats_at_current_node(&mut game1, &mut game2, &actions_so_far); - - // IP: CHECK - actions_so_far.push(game1.available_actions()[0]); - game1.play(0); - game2.play(0); - print_strats_at_current_node(&mut game1, &mut game2, &actions_so_far); - - // CHANCE: 0 - actions_so_far.push(game1.available_actions()[1]); - game1.play(1); - game2.play(1); - print_strats_at_current_node(&mut game1, &mut game2, &actions_so_far); - - // compare_strategies(&mut game, &mut game2, BoardState::Turn); -} From 0ae353171ee0f74dd79b2772cbbdf0af249651a6 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Sun, 18 Aug 2024 18:04:01 -0700 Subject: [PATCH 32/37] Tmp Commit --- src/game/node.rs | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/src/game/node.rs b/src/game/node.rs index 400a886..e6bd7e3 100644 --- a/src/game/node.rs +++ b/src/game/node.rs @@ -209,7 +209,6 @@ impl Default for PostFlopNode { fn default() -> Self { Self { prev_action: Action::None, - parent_node_index: usize::MAX, player: PLAYER_OOP, turn: NOT_DEALT, river: NOT_DEALT, @@ -241,31 +240,4 @@ impl PostFlopNode { ) } } - - /// Get a list of available actions at a given node - pub fn actions(&self) -> Vec { - self.children() - .iter() - .map(|n| n.lock().prev_action) - .collect::>() - } - - /// Find the index of a given action, if present - pub fn action_index(&self, action: Action) -> Option { - self.children() - .iter() - .position(|n| n.lock().prev_action == action) - } - - /// Recursively compute the current node's history - pub fn compute_history_recursive(&self, game: &PostFlopGame) -> Option> { - if self.parent_node_index == usize::MAX { - Some(vec![]) - } else { - let p = game.node_arena.get(self.parent_node_index)?; - let mut history = p.lock().compute_history_recursive(game)?; - history.push(p.lock().action_index(self.prev_action)?); - Some(history) - } - } } From deb338ca8bcb04e6594c22552264c35f4259f37b Mon Sep 17 00:00:00 2001 From: bkushigian Date: Sun, 18 Aug 2024 18:04:38 -0700 Subject: [PATCH 33/37] tmp commit --- src/game/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/game/mod.rs b/src/game/mod.rs index 3504adb..08b23bc 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -121,7 +121,6 @@ pub struct PostFlopGame { #[repr(C)] pub struct PostFlopNode { prev_action: Action, - parent_node_index: usize, player: u8, turn: Card, river: Card, From 3227822ac862cdefc5bc53e5a7b570339d639bce Mon Sep 17 00:00:00 2001 From: bkushigian Date: Sun, 18 Aug 2024 18:07:50 -0700 Subject: [PATCH 34/37] Refactoring test --- src/file.rs | 42 ------------------------------------------ 1 file changed, 42 deletions(-) diff --git a/src/file.rs b/src/file.rs index d7e90f7..17e7d5b 100644 --- a/src/file.rs +++ b/src/file.rs @@ -269,7 +269,6 @@ mod tests { use crate::action_tree::*; use crate::card::*; use crate::range::*; - use crate::solver::solve; use crate::utility::*; #[test] @@ -376,45 +375,4 @@ mod tests { assert!((root_ev_oop - 45.0).abs() < 1e-4); assert!((root_ev_ip - 15.0).abs() < 1e-4); } - - #[test] - fn test_reload_and_resolve() { - let oop_range = "AA,QQ"; - let ip_range = "KK"; - - let card_config = CardConfig { - range: [oop_range.parse().unwrap(), ip_range.parse().unwrap()], - flop: flop_from_str("3h3s3d").unwrap(), - ..Default::default() - }; - - let tree_config = TreeConfig { - starting_pot: 100, - effective_stack: 100, - rake_rate: 0.0, - rake_cap: 0.0, - flop_bet_sizes: [("e", "").try_into().unwrap(), ("e", "").try_into().unwrap()], - turn_bet_sizes: [("e", "").try_into().unwrap(), ("e", "").try_into().unwrap()], - river_bet_sizes: [("e", "").try_into().unwrap(), ("e", "").try_into().unwrap()], - ..Default::default() - }; - - let action_tree = ActionTree::new(tree_config).unwrap(); - let mut game = PostFlopGame::with_config(card_config, action_tree).unwrap(); - println!( - "memory usage: {:.2}GB", - game.memory_usage().0 as f64 / (1024.0 * 1024.0 * 1024.0) - ); - game.allocate_memory(false); - - solve(&mut game, 100, 0.01, false); - - // save (turn) - game.set_target_storage_mode(BoardState::Turn).unwrap(); - save_data_to_file(&game, "", "tmpfile.flop", None).unwrap(); - - // load (turn) - let mut game: PostFlopGame = load_data_from_file("tmpfile.flop", None).unwrap().0; - assert!(game.rebuild_and_resolve_forgotten_streets().is_ok()); - } } From d0aaae11b0d409448a4103d79f1131a61654db7b Mon Sep 17 00:00:00 2001 From: bkushigian Date: Sun, 18 Aug 2024 18:09:38 -0700 Subject: [PATCH 35/37] Branch refactor continued --- src/game/base.rs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/game/base.rs b/src/game/base.rs index 7ff5f8b..0e69184 100644 --- a/src/game/base.rs +++ b/src/game/base.rs @@ -462,21 +462,6 @@ impl PostFlopGame { Ok(()) } - pub fn print_internal_data(&self) { - println!("Printing internal data for PostFlopGame"); - println!("- node_arena: {}", self.node_arena.len()); - println!("- storage1: {}", self.storage1.len()); - println!("- storage2: {}", self.storage2.len()); - println!("- storage_ip: {}", self.storage_ip.len()); - println!("- storage_chance: {}", self.storage_chance.len()); - println!("- locking_strategy: {}", self.locking_strategy.len()); - println!("- storage mode: {:?}", self.storage_mode()); - println!( - "- target storage mode: {:?}", - self.target_storage_mode() - ); - } - /// Initializes fields `initial_weights` and `private_cards`. #[inline] fn init_hands(&mut self) { @@ -749,7 +734,6 @@ impl PostFlopGame { node.num_children += 1; let mut child = node.children().last().unwrap().lock(); child.prev_action = Action::Chance(card); - child.parent_node_index = node_index; child.turn = card; } } @@ -768,7 +752,6 @@ impl PostFlopGame { node.num_children += 1; let mut child = node.children().last().unwrap().lock(); child.prev_action = Action::Chance(card); - child.parent_node_index = node_index; child.turn = node.turn; child.river = card; } @@ -814,7 +797,6 @@ impl PostFlopGame { child.prev_action = *action; child.turn = node.turn; child.river = node.river; - child.parent_node_index = node_index; } let num_private_hands = self.num_private_hands(node.player as usize); From 3dad7546268068018bc81f09fdc82d6e3c8d232f Mon Sep 17 00:00:00 2001 From: bkushigian Date: Sun, 18 Aug 2024 18:12:22 -0700 Subject: [PATCH 36/37] Removed println --- src/game/interpreter.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/game/interpreter.rs b/src/game/interpreter.rs index 652e990..d7845b6 100644 --- a/src/game/interpreter.rs +++ b/src/game/interpreter.rs @@ -763,7 +763,6 @@ impl PostFlopGame { node.cfvalues_ip().to_vec() } } else if player == self.current_player() { - println!("BINGO"); have_actions = true; if self.is_compression_enabled { let slice = node.cfvalues_compressed(); From f76ef3d823767079764e2a416d7a1e776fcfc67b Mon Sep 17 00:00:00 2001 From: bkushigian Date: Mon, 7 Oct 2024 00:21:56 -0700 Subject: [PATCH 37/37] return &self.state; --> &self.state --- src/game/base.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/base.rs b/src/game/base.rs index 41154a3..e7501f7 100644 --- a/src/game/base.rs +++ b/src/game/base.rs @@ -1458,6 +1458,6 @@ impl PostFlopGame { } pub fn get_state(&self) -> &State { - return &self.state; + &self.state } }