From c74cad1b46c92db7daf32e78da15b61bf6f1f8d6 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Sun, 18 Aug 2024 17:33:45 -0700 Subject: [PATCH 01/40] beginning branch refactor --- src/game/base.rs | 182 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) diff --git a/src/game/base.rs b/src/game/base.rs index f1cc0f0..51dcfc4 100644 --- a/src/game/base.rs +++ b/src/game/base.rs @@ -801,6 +801,188 @@ 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_num_nodes(); + 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 = 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 81764c7ba0ce7dca37f4261b7fa488f7296a3a22 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Sun, 18 Aug 2024 17:35:02 -0700 Subject: [PATCH 02/40] Branch Refactor: added file_io_debug.rs --- examples/file_io_debug.rs | 210 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 210 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..d5a625f --- /dev/null +++ b/examples/file_io_debug.rs @@ -0,0 +1,210 @@ +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 8824791f2fdf5d0d1888c68797432f36648454b7 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Sun, 18 Aug 2024 17:39:47 -0700 Subject: [PATCH 03/40] Branch refactor --- src/game/base.rs | 23 +++++----------- src/solver.rs | 69 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 17 deletions(-) diff --git a/src/game/base.rs b/src/game/base.rs index 51dcfc4..ede476f 100644 --- a/src/game/base.rs +++ b/src/game/base.rs @@ -902,27 +902,16 @@ impl PostFlopGame { pub fn resolve_reloaded_nodes( &mut self, - max_num_iterations: u32, - target_exploitability: f32, - print_progress: bool, + _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, - ); + let _node = self.node_arena.get(node_idx).ok_or("Invalid node index")?; + // TODO: Get and Apply History + // TODO: Solve with node as root } finalize(self); diff --git a/src/solver.rs b/src/solver.rs index 5a1cc5a..26a2534 100644 --- a/src/solver.rs +++ b/src/solver.rs @@ -132,6 +132,75 @@ 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. +/// TODO: Should we move this to the `crate::base` module? +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 9821469067dc20a82f7e8ce2865ba70021146590 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Sun, 18 Aug 2024 17:57:24 -0700 Subject: [PATCH 04/40] Strategies by private hand, plus transpose in sliceops --- src/game/interpreter.rs | 11 +++++++++++ src/sliceop.rs | 16 ++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/game/interpreter.rs b/src/game/interpreter.rs index d44d63a..a9b95fa 100644 --- a/src/game/interpreter.rs +++ b/src/game/interpreter.rs @@ -846,6 +846,17 @@ impl PostFlopGame { ret } + pub fn strategy_by_private_hand(&self) -> Vec> { + let strategy = self.strategy(); + let n_actions = self.available_actions().len(); + let n_hands = self.private_cards.len(); + let strategy_t = transpose(&strategy, n_actions, n_hands); + let strategy_by_hand = (0..n_hands) + .map(|j| row(&strategy_t, j, n_actions).to_vec()) + .collect(); + strategy_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/sliceop.rs b/src/sliceop.rs index bf726b6..ebd0018 100644 --- a/src/sliceop.rs +++ b/src/sliceop.rs @@ -245,3 +245,19 @@ pub(crate) fn row(slice: &[T], index: usize, row_size: usize) -> &[T] { pub(crate) fn row_mut(slice: &mut [T], index: usize, row_size: usize) -> &mut [T] { &mut slice[index * row_size..(index + 1) * row_size] } + +#[inline] +pub(crate) fn transpose(input: &[T], m: usize, n: usize) -> Vec { + let mut output = Vec::with_capacity(m * n); + unsafe { + output.set_len(m * n); + } + + for i in 0..m { + for j in 0..n { + output[j * m + i] = input[i * n + j]; + } + } + + output +} From e0b0db78a97885d2f90b946a80ef0b59222faebf Mon Sep 17 00:00:00 2001 From: bkushigian Date: Sun, 18 Aug 2024 18:04:20 -0700 Subject: [PATCH 05/40] Tmp commit --- src/game/node.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/game/node.rs b/src/game/node.rs index e6bd7e3..3357e16 100644 --- a/src/game/node.rs +++ b/src/game/node.rs @@ -240,4 +240,31 @@ 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 c2459fab7e580fa154207986fab2e95fa86c5b39 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Sun, 18 Aug 2024 18:05:16 -0700 Subject: [PATCH 06/40] tmp commit --- src/game/mod.rs | 1 + src/game/node.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/game/mod.rs b/src/game/mod.rs index 33d9a19..18383f0 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -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 3357e16..400a886 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, From 4159f134c708e94c4c9ad2d4cdb7a59dfb3ef732 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Sun, 18 Aug 2024 18:08:10 -0700 Subject: [PATCH 07/40] Refactor: moved test into branch --- src/file.rs | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/file.rs b/src/file.rs index 17e7d5b..b4e64ef 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::*; 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()); + } } From 1e0067b5e57fd40f741a8f8e647e703f6edd9d13 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Sun, 18 Aug 2024 18:11:08 -0700 Subject: [PATCH 08/40] Refactor continue --- src/game/base.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/game/base.rs b/src/game/base.rs index ede476f..d0d8bcd 100644 --- a/src/game/base.rs +++ b/src/game/base.rs @@ -725,6 +725,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; } } @@ -743,6 +744,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; } @@ -786,6 +788,7 @@ impl PostFlopGame { for (child, action) in node.children().iter().zip(action_node.actions.iter()) { let mut child = child.lock(); child.prev_action = *action; + child.parent_node_index = node_index; child.turn = node.turn; child.river = node.river; } From f6b0861d6e85f50d89537bc2c48b0bc097dd94c1 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Mon, 19 Aug 2024 10:26:52 -0700 Subject: [PATCH 09/40] Documenting solve_recursive --- src/solver.rs | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/solver.rs b/src/solver.rs index 1548a1a..d7c2554 100644 --- a/src/solver.rs +++ b/src/solver.rs @@ -243,7 +243,27 @@ fn solve_recursive( r.write(v as f32); }); } - // if the current player is `player` + // IF THE CURRENT PLAYER IS `player`: + // + // 1. Recursively compute cfvalues for each action/hand combination with + // `solve_recursive`. + // - `cfvalues` for action index `action` are stored in `cfv_actions` in + // row `action` + // - In particular, `cfv_actions[a * #(num_hands) + h]` gives the cfvalue + // for hand `h` taking action `a` + // 2. Compute the strategy using regret matching (`regret_matching` or + // `regret_matching_compressed`) + // 3. Apply any node locking + // 4. Compute the cfvalues for each hand (that is, accumulate + // across different actions taken). This is handled separately if + // the game is compressed or not. + // 1. Update the cumulative strategy with the new strategy, using + // `params.gamma_t` to discount previous cumulative strategy + // 2. Update the cumulative regret using `params.alpha_t` and + // `beta.alpha_t` + // - Question: strategy is not normalized: why? + // - Question: we are subtracting `result` from `row` (rows of + // `cum_regret`): why? else if node.player() == player { // compute the counterfactual values of each action for_each_child(node, |action| { @@ -270,12 +290,9 @@ fn solve_recursive( // 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: + // `h` takes action `a` times the regret of hand `h` taking action `a`: // - // ``` - // result[h] = sum([freq(h, a) * regret(h, a) for a in actions]) - // ``` + // 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`. From aa10c4ca7c834a5a5ad1ff73b92efe7526917ad9 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Tue, 20 Aug 2024 14:10:10 -0700 Subject: [PATCH 10/40] Added documentation, renamed variables for clarity --- src/solver.rs | 117 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 79 insertions(+), 38 deletions(-) diff --git a/src/solver.rs b/src/solver.rs index d7c2554..73715a9 100644 --- a/src/solver.rs +++ b/src/solver.rs @@ -145,6 +145,9 @@ pub fn solve_step(game: &T, current_iteration: u32) { /// * `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 +/// +/// This modifies `node`'s strategies and regrets when `node.player() == +/// player`. fn solve_recursive( result: &mut [MaybeUninit], game: &T, @@ -180,7 +183,7 @@ fn 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"))] - let cfv_actions = MutexLike::new(Vec::with_capacity(num_actions * num_hands)); + let cfv_actions_hands = MutexLike::new(Vec::with_capacity(num_actions * num_hands)); // if the `node` is chance if node.is_chance() { @@ -199,7 +202,11 @@ fn solve_recursive( // compute the counterfactual values of each action for_each_child(node, |action| { solve_recursive( - row_mut(cfv_actions.lock().spare_capacity_mut(), action, num_hands), + row_mut( + cfv_actions_hands.lock().spare_capacity_mut(), + action, + num_hands, + ), game, &mut node.play(action), player, @@ -217,7 +224,7 @@ fn solve_recursive( // compute the strided summation of the counterfactual values for each // hand and store in `result_f64` - let mut cfv_actions = cfv_actions.lock(); + let mut cfv_actions = cfv_actions_hands.lock(); unsafe { cfv_actions.set_len(num_actions * num_hands) }; sum_slices_f64_uninit(result_f64.spare_capacity_mut(), &cfv_actions); unsafe { result_f64.set_len(num_hands) }; @@ -268,7 +275,11 @@ fn solve_recursive( // compute the counterfactual values of each action for_each_child(node, |action| { solve_recursive( - row_mut(cfv_actions.lock().spare_capacity_mut(), action, num_hands), + row_mut( + cfv_actions_hands.lock().spare_capacity_mut(), + action, + num_hands, + ), game, &mut node.play(action), player, @@ -288,17 +299,20 @@ fn solve_recursive( let locking = game.locking_strategy(node); apply_locking_strategy(&mut strategy, locking); - // 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` times the regret of hand `h` taking action `a`: + // Compute the counterfactual values for each hand by 'fusing' together + // hands' cfvs across all actions. + // + // For hand `h` this is computed to be the sum over actions `a` of the + // frequency with which `h` takes action `a` times the regret of hand + // `h` taking action `a`: // // 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); + let mut cfv_actions_hands = cfv_actions_hands.lock(); + unsafe { cfv_actions_hands.set_len(num_actions * num_hands) }; + let cfv_hands = fma_slices_uninit(result, &strategy, &cfv_actions_hands); if game.is_compression_enabled() { // update the cumulative strategy @@ -327,26 +341,34 @@ fn solve_recursive( let beta_decoder = params.beta_t * scale / i16::MAX as f32; let cum_regret = node.regrets_compressed_mut(); - cfv_actions.iter_mut().zip(&*cum_regret).for_each(|(x, y)| { - *x += *y as f32 * if *y >= 0 { alpha_decoder } else { beta_decoder }; - }); + cfv_actions_hands + .iter_mut() + .zip(&*cum_regret) + .for_each(|(x, y)| { + *x += *y as f32 * if *y >= 0 { alpha_decoder } else { beta_decoder }; + }); - cfv_actions.chunks_exact_mut(num_hands).for_each(|row| { - sub_slice(row, result); - }); + cfv_actions_hands + .chunks_exact_mut(num_hands) + .for_each(|row| { + sub_slice(row, cfv_hands); + }); if !locking.is_empty() { - cfv_actions.iter_mut().zip(locking).for_each(|(d, s)| { - if s.is_sign_positive() { - *d = 0.0; - } - }) + cfv_actions_hands + .iter_mut() + .zip(locking) + .for_each(|(d, s)| { + if s.is_sign_positive() { + *d = 0.0; + } + }) } - let new_scale = encode_signed_slice(cum_regret, &cfv_actions); + let new_scale = encode_signed_slice(cum_regret, &cfv_actions_hands); node.set_regret_scale(new_scale); } else { - // update the cumulative strategy + // update the node's cumulative strategy (`node.strategy_mut()`) // - `gamma` is used to discount cumulative strategy contributions let gamma = params.gamma_t; let cum_strategy = node.strategy_mut(); @@ -354,24 +376,37 @@ fn solve_recursive( *x = *x * gamma + *y; }); - // update the cumulative regret + // update the node's cumulative regret `node.regrets_mut()` // - 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)| { - let coef = if x.is_sign_positive() { alpha } else { beta }; - *x = *x * coef + *y; - }); + cum_regret + .iter_mut() + .zip(&*cfv_actions_hands) + .for_each(|(x, y)| { + let coef = if x.is_sign_positive() { alpha } else { beta }; + *x = *x * coef + *y; + }); cum_regret.chunks_exact_mut(num_hands).for_each(|row| { - sub_slice(row, result); + sub_slice(row, cfv_hands); }); } } - // if the current player is not `player` + // IF THE CURRENT PLAYER IS NOT `player` + // + // 1. Compute strategy for villain with regret matching. We store the + // strategy in `cfreach_actions` (rather than, say, `strategy`) because + // this will be updated with to store the `cfreach` for each individual + // action (step 3) + // 2. Apply any locking to villain's strategy + // 3. Update strategy in `cfreach_actions` with reach probabilities in + // `cfreach`; `cfreach_actions` now stores rows of per-hand reach + // probabilities, where each row corresponds to an action. + // 4. recursively solve for `player` and store in `cfv_actions` else { // compute the strategy by regret-matching algorithm - let mut cfreach_actions = if game.is_compression_enabled() { + let mut cfreach_actions_hands = if game.is_compression_enabled() { regret_matching_compressed(node.regrets_compressed(), num_actions) } else { regret_matching(node.regrets(), num_actions) @@ -379,28 +414,34 @@ fn solve_recursive( // node-locking let locking = game.locking_strategy(node); - apply_locking_strategy(&mut cfreach_actions, locking); + apply_locking_strategy(&mut cfreach_actions_hands, locking); // update the reach probabilities let row_size = cfreach.len(); - cfreach_actions.chunks_exact_mut(row_size).for_each(|row| { - mul_slice(row, cfreach); - }); + cfreach_actions_hands + .chunks_exact_mut(row_size) + .for_each(|row| { + mul_slice(row, cfreach); + }); // compute the counterfactual values of each action for_each_child(node, |action| { solve_recursive( - row_mut(cfv_actions.lock().spare_capacity_mut(), action, num_hands), + row_mut( + cfv_actions_hands.lock().spare_capacity_mut(), + action, + num_hands, + ), game, &mut node.play(action), player, - row(&cfreach_actions, action, row_size), + row(&cfreach_actions_hands, action, row_size), params, ); }); // sum up the counterfactual values - let mut cfv_actions = cfv_actions.lock(); + let mut cfv_actions = cfv_actions_hands.lock(); unsafe { cfv_actions.set_len(num_actions * num_hands) }; sum_slices_uninit(result, &cfv_actions); } From a5dc4630c28ae742069308af8675784c17764f7c Mon Sep 17 00:00:00 2001 From: bkushigian Date: Tue, 20 Aug 2024 14:42:32 -0700 Subject: [PATCH 11/40] Fixed thing that was missed in previous merge --- src/game/base.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/game/base.rs b/src/game/base.rs index 5b02de8..53dcc9a 100644 --- a/src/game/base.rs +++ b/src/game/base.rs @@ -820,7 +820,7 @@ impl PostFlopGame { /// `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_num_nodes(); + 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 @@ -829,7 +829,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())) From 9c4a31022c04488734596a8b193497c0e07ea142 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Wed, 21 Aug 2024 11:30:14 -0700 Subject: [PATCH 12/40] Tmp commit --- examples/file_io_debug.rs | 18 ++++++++++-------- src/file.rs | 2 +- src/game/base.rs | 31 +++++++++++++++++++++++++------ src/game/interpreter.rs | 2 +- 4 files changed, 37 insertions(+), 16 deletions(-) diff --git a/examples/file_io_debug.rs b/examples/file_io_debug.rs index d5a625f..598af23 100644 --- a/examples/file_io_debug.rs +++ b/examples/file_io_debug.rs @@ -116,7 +116,7 @@ fn print_strats_at_current_node( .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); + 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!( @@ -127,6 +127,8 @@ fn print_strats_at_current_node( println!("{}", action_frequencies.join(" ")); } } + + println!(); } fn main() { @@ -158,10 +160,10 @@ fn main() { // save (turn) game1.set_target_storage_mode(BoardState::Turn).unwrap(); - save_data_to_file(&game1, "", "tmpfile.flop", None).unwrap(); + save_data_to_file(&game1, "", "tmpfile.pfs", None).unwrap(); // load (turn) - let mut game2: PostFlopGame = load_data_from_file("tmpfile.flop", None).unwrap().0; + let mut game2: PostFlopGame = load_data_from_file("tmpfile.pfs", None).unwrap().0; // compare_strategies(&mut game, &mut game2, BoardState::Turn); assert!(game2.rebuild_and_resolve_forgotten_streets().is_ok()); @@ -174,31 +176,31 @@ fn main() { 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); + // 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); + // 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); + // 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); + // 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); + // print_strats_at_current_node(&mut game1, &mut game2, &actions_so_far); // CHANCE: 0 actions_so_far.push(game1.available_actions()[1]); diff --git a/src/file.rs b/src/file.rs index b4e64ef..6cd2a1d 100644 --- a/src/file.rs +++ b/src/file.rs @@ -166,7 +166,7 @@ pub fn load_data_from_std_read( ) -> Result<(T, String), String> { let magic: u32 = decode_from_std_read(reader, "Failed to read magic number")?; if magic != MAGIC { - return Err("Magic number is invalid".to_string()); + return Err("Unrecognized file format".to_string()); } let version: u8 = decode_from_std_read(reader, "Failed to read version number")?; diff --git a/src/game/base.rs b/src/game/base.rs index 53dcc9a..26f19e2 100644 --- a/src/game/base.rs +++ b/src/game/base.rs @@ -1,6 +1,7 @@ use super::*; use crate::bunching::*; use crate::interface::*; +use crate::solve_with_node_as_root; use crate::utility::*; use std::mem::{self, MaybeUninit}; @@ -880,6 +881,8 @@ impl PostFlopGame { /// 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> { + println!("storage_mode: {:?}", self.storage_mode); + println!("target_storage_mode: {:?}", self.target_storage_mode); match self.storage_mode { BoardState::Flop => { let turn_root_nodes = self @@ -914,16 +917,32 @@ impl PostFlopGame { pub fn resolve_reloaded_nodes( &mut self, - _max_num_iterations: u32, - _target_exploitability: f32, - _print_progress: bool, + 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; + println!("Found {} nodes to solve", nodes_to_solve.len()); for node_idx in nodes_to_solve { - let _node = self.node_arena.get(node_idx).ok_or("Invalid node index")?; - // TODO: Get and Apply History - // TODO: Solve with node as root + let node = self.node_arena.get(node_idx).ok_or("Invalid node index")?; + let history = match node.lock().compute_history_recursive(self) { + Some(history) => history, + None => Err("Couldn't parse history from node")?, + }; + + self.apply_history(&history); + // NOTE: + // I _think_ this works. We don't actually modify the node, only + // data that is point to by the node. + let n = MutexLike::new(self.node().clone()); + solve_with_node_as_root( + self, + n.lock(), + max_num_iterations, + target_exploitability, + print_progress, + ); } finalize(self); diff --git a/src/game/interpreter.rs b/src/game/interpreter.rs index 34bb2c7..f61bb82 100644 --- a/src/game/interpreter.rs +++ b/src/game/interpreter.rs @@ -1005,7 +1005,7 @@ impl PostFlopGame { /// Returns the reference to the current node. #[inline] - fn node(&self) -> MutexGuardLike { + pub fn node(&self) -> MutexGuardLike { self.node_arena[self.node_history.last().cloned().unwrap_or(0)].lock() } From f30f70af026018b6a44dc68ca86bc323c7022ecd Mon Sep 17 00:00:00 2001 From: bkushigian Date: Wed, 21 Aug 2024 11:41:59 -0700 Subject: [PATCH 13/40] Removed warnings from example --- examples/file_io_debug.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/file_io_debug.rs b/examples/file_io_debug.rs index 598af23..98108db 100644 --- a/examples/file_io_debug.rs +++ b/examples/file_io_debug.rs @@ -1,5 +1,6 @@ use postflop_solver::*; +#[allow(dead_code)] fn recursive_compare_strategies_helper( saved: &mut PostFlopGame, loaded: &mut PostFlopGame, @@ -40,6 +41,7 @@ fn recursive_compare_strategies_helper( } } +#[allow(dead_code)] fn compare_strategies( saved: &mut PostFlopGame, loaded: &mut PostFlopGame, @@ -49,7 +51,7 @@ fn compare_strategies( loaded.back_to_root(); saved.cache_normalized_weights(); loaded.cache_normalized_weights(); - for (i, ((e1, e2), cards)) in saved + for (_i, ((e1, e2), cards)) in saved .expected_values(0) .iter() .zip(loaded.expected_values(0)) @@ -58,7 +60,7 @@ fn compare_strategies( { println!("ev {}: {}:{}", hole_to_string(*cards).unwrap(), e1, e2); } - for (i, ((e1, e2), cards)) in saved + for (_i, ((e1, e2), cards)) in saved .expected_values(1) .iter() .zip(loaded.expected_values(1)) From b5c3807350888009e1ba9436549572547ddff299 Mon Sep 17 00:00:00 2001 From: Jacob Van Geffen Date: Thu, 22 Aug 2024 07:44:53 -0500 Subject: [PATCH 14/40] tmp commit from pair programming to fix flop and turn save-and-resolve --- src/game/base.rs | 28 +++++++++++++++++----------- src/game/mod.rs | 5 ++++- src/game/node.rs | 1 + src/game/serialization.rs | 19 +++++++++++++++++-- 4 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/game/base.rs b/src/game/base.rs index 26f19e2..75053f0 100644 --- a/src/game/base.rs +++ b/src/game/base.rs @@ -881,37 +881,37 @@ impl PostFlopGame { /// 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> { - println!("storage_mode: {:?}", self.storage_mode); - println!("target_storage_mode: {:?}", self.target_storage_mode); - match self.storage_mode { - BoardState::Flop => { + match self.state { + State::SolvedFlop => { 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(..)) + && n.lock().river == NOT_DEALT + && matches!(n.lock().prev_action, Action::Chance(..)) }) .map(|(i, _)| i) .collect::>(); Ok(turn_root_nodes) } - BoardState::Turn => { + State::SolvedTurn => { let river_root_nodes = self .node_arena .iter() .enumerate() .filter(|(_, n)| { n.lock().turn != NOT_DEALT - && matches!(n.lock().prev_action, Action::Chance(..)) + && n.lock().river != NOT_DEALT + && matches!(n.lock().prev_action, Action::Chance(..)) }) .map(|(i, _)| i) .collect::>(); Ok(river_root_nodes) } - BoardState::River => Ok(vec![]), + State::Solved => Ok(vec![]), + _ => unreachable!(), } } @@ -936,6 +936,9 @@ impl PostFlopGame { // I _think_ this works. We don't actually modify the node, only // data that is point to by the node. let n = MutexLike::new(self.node().clone()); + + println!("While resolve_reload_nodes, node_idx: {:?}", node_idx); + solve_with_node_as_root( self, n.lock(), @@ -956,7 +959,7 @@ impl PostFlopGame { return Err("Game is not successfully initialized".to_string()); } - if self.state == State::MemoryAllocated && self.storage_mode == BoardState::River { + if self.storage_mode == BoardState::River { return Ok(()); } @@ -967,7 +970,10 @@ impl PostFlopGame { return Err("Memory usage exceeds maximum size".to_string()); } - self.state = State::MemoryAllocated; + // Only update the state when the loaded game has not been solved at all + if self.state < State::MemoryAllocated { + self.state = State::MemoryAllocated; + } // self.is_compression_enabled = self.is_compression_enabled; let old_storage1 = std::mem::replace(&mut self.storage1, vec![]); diff --git a/src/game/mod.rs b/src/game/mod.rs index 3504adb..a7180aa 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -26,7 +26,9 @@ pub enum State { Uninitialized = 1, TreeBuilt = 2, MemoryAllocated = 3, - Solved = 4, + SolvedFlop = 4, + SolvedTurn = 5, + Solved = 6, } /// A struct representing a postflop game. @@ -81,6 +83,7 @@ pub struct PostFlopGame { // store options storage_mode: BoardState, + // NOTE: Only used for encoding target_storage_mode: BoardState, num_nodes_per_street: [u64; 3], is_compression_enabled: bool, diff --git a/src/game/node.rs b/src/game/node.rs index 400a886..6a2fab8 100644 --- a/src/game/node.rs +++ b/src/game/node.rs @@ -51,6 +51,7 @@ impl GameNode for PostFlopNode { #[inline] fn regrets(&self) -> &[f32] { + // TODO(Jacob): Something is causing either storage2 pointer to be off, or num_elements to be too big unsafe { slice::from_raw_parts(self.storage2 as *const f32, self.num_elements as usize) } } diff --git a/src/game/serialization.rs b/src/game/serialization.rs index 5931bf8..31194bf 100644 --- a/src/game/serialization.rs +++ b/src/game/serialization.rs @@ -64,7 +64,7 @@ impl PostFlopGame { /// If this is a River save (`target_storage_mode == BoardState::River`) /// then do not store cfvalues. /// - /// If this is a Flop save, + /// If this is a Flop save, only store flop nodes fn num_target_storage(&self) -> [usize; 4] { if self.state <= State::TreeBuilt { return [0; 4]; @@ -126,8 +126,21 @@ impl Encode for PostFlopGame { // version VERSION_STR.to_string().encode(encoder)?; + // Update state based on target storage whenever the state is solved + // TODO(Jacob): May be better to add is_solved() function onto state + // That would prevent bugs caused by new states being added + let saved_state = if self.state >= State::SolvedFlop { + match self.target_storage_mode { + BoardState::Flop => State::SolvedFlop, + BoardState::Turn => State::SolvedTurn, + BoardState::River => State::Solved, + } + } else { + self.state + }; + // contents - self.state.encode(encoder)?; + saved_state.encode(encoder)?; self.card_config.encode(encoder)?; self.tree_config.encode(encoder)?; self.added_lines.encode(encoder)?; @@ -255,6 +268,8 @@ impl Decode for PostFlopGame { // restore the counterfactual values if game.storage_mode == BoardState::River && game.state == State::Solved { + // TODO(Jacob): Want to restructure finalize so that this hacky + // setting of game.state isn't necessary game.state = State::MemoryAllocated; finalize(&mut game); } From 963ed40d1f17d16d2b92f51bc8294a9e7bf44114 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Sat, 24 Aug 2024 16:33:39 -0700 Subject: [PATCH 15/40] tmp commit for debugging --- src/game/base.rs | 57 +++++------------------------------------------- src/game/node.rs | 11 +++++++--- 2 files changed, 13 insertions(+), 55 deletions(-) diff --git a/src/game/base.rs b/src/game/base.rs index 75053f0..de4b9a5 100644 --- a/src/game/base.rs +++ b/src/game/base.rs @@ -816,56 +816,9 @@ impl PostFlopGame { /* 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.init_root()?; self.allocate_memory_after_load()?; self.resolve_reloaded_nodes(1000, 0.01, false) } @@ -889,8 +842,8 @@ impl PostFlopGame { .enumerate() .filter(|(_, n)| { n.lock().turn != NOT_DEALT - && n.lock().river == NOT_DEALT - && matches!(n.lock().prev_action, Action::Chance(..)) + && n.lock().river == NOT_DEALT + && matches!(n.lock().prev_action, Action::Chance(..)) }) .map(|(i, _)| i) .collect::>(); @@ -903,8 +856,8 @@ impl PostFlopGame { .enumerate() .filter(|(_, n)| { n.lock().turn != NOT_DEALT - && n.lock().river != NOT_DEALT - && matches!(n.lock().prev_action, Action::Chance(..)) + && n.lock().river != NOT_DEALT + && matches!(n.lock().prev_action, Action::Chance(..)) }) .map(|(i, _)| i) .collect::>(); diff --git a/src/game/node.rs b/src/game/node.rs index 6a2fab8..2e5fa27 100644 --- a/src/game/node.rs +++ b/src/game/node.rs @@ -263,9 +263,14 @@ impl PostFlopNode { 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)?); + let indx_parent = game.node_arena.get(self.parent_node_index)?; + let node_parent = indx_parent.lock(); + let mut history = node_parent.compute_history_recursive(game)?; + let idx = match self.prev_action { + Action::Chance(card_idx) => card_idx as usize, + _ => node_parent.action_index(self.prev_action)?, + }; + history.push(idx); Some(history) } } From 59cf5f26ec1775be37f52ad1db800dee529ab930 Mon Sep 17 00:00:00 2001 From: Jacob Van Geffen Date: Thu, 22 Aug 2024 07:44:53 -0500 Subject: [PATCH 16/40] Cherry pick updated State, fixed some breakages --- src/file.rs | 2 +- src/game/base.rs | 5 +++++ src/game/interpreter.rs | 4 ++-- src/game/mod.rs | 5 ++++- src/game/node.rs | 1 + src/game/serialization.rs | 19 +++++++++++++++++-- 6 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/file.rs b/src/file.rs index 17e7d5b..ac10510 100644 --- a/src/file.rs +++ b/src/file.rs @@ -241,7 +241,7 @@ impl FileData for PostFlopGame { } fn is_ready_to_save(&self) -> bool { - self.is_solved() + self.is_partially_solved() } fn estimated_memory_usage(&self) -> u64 { diff --git a/src/game/base.rs b/src/game/base.rs index 0e69184..bab4b6b 100644 --- a/src/game/base.rs +++ b/src/game/base.rs @@ -1460,4 +1460,9 @@ impl PostFlopGame { pub fn get_state(&self) -> &State { return &self.state; } + + #[inline] + pub fn is_partially_solved(&self) -> bool { + self.state >= State::SolvedFlop + } } diff --git a/src/game/interpreter.rs b/src/game/interpreter.rs index d7845b6..7a9c8f5 100644 --- a/src/game/interpreter.rs +++ b/src/game/interpreter.rs @@ -659,7 +659,7 @@ impl PostFlopGame { /// [`cache_normalized_weights`]: #method.cache_normalized_weights /// [`expected_values_detail`]: #method.expected_values_detail pub fn expected_values(&self, player: usize) -> Vec { - if self.state != State::Solved { + if !self.is_partially_solved() { panic!("Game is not solved"); } @@ -711,7 +711,7 @@ impl PostFlopGame { /// [`expected_values`]: #method.expected_value /// [`cache_normalized_weights`]: #method.cache_normalized_weights pub fn expected_values_detail(&self, player: usize) -> Vec { - if self.state != State::Solved { + if !self.is_partially_solved() { panic!("Game is not solved"); } diff --git a/src/game/mod.rs b/src/game/mod.rs index 08b23bc..ac8dcb6 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -26,7 +26,9 @@ pub enum State { Uninitialized = 1, TreeBuilt = 2, MemoryAllocated = 3, - Solved = 4, + SolvedFlop = 4, + SolvedTurn = 5, + Solved = 6, } /// A struct representing a postflop game. @@ -81,6 +83,7 @@ pub struct PostFlopGame { // store options storage_mode: BoardState, + // NOTE: Only used for encoding target_storage_mode: BoardState, num_nodes_per_street: [u64; 3], is_compression_enabled: bool, diff --git a/src/game/node.rs b/src/game/node.rs index e6bd7e3..c5bbe95 100644 --- a/src/game/node.rs +++ b/src/game/node.rs @@ -51,6 +51,7 @@ impl GameNode for PostFlopNode { #[inline] fn regrets(&self) -> &[f32] { + // TODO(Jacob): Something is causing either storage2 pointer to be off, or num_elements to be too big unsafe { slice::from_raw_parts(self.storage2 as *const f32, self.num_elements as usize) } } diff --git a/src/game/serialization.rs b/src/game/serialization.rs index 5931bf8..31194bf 100644 --- a/src/game/serialization.rs +++ b/src/game/serialization.rs @@ -64,7 +64,7 @@ impl PostFlopGame { /// If this is a River save (`target_storage_mode == BoardState::River`) /// then do not store cfvalues. /// - /// If this is a Flop save, + /// If this is a Flop save, only store flop nodes fn num_target_storage(&self) -> [usize; 4] { if self.state <= State::TreeBuilt { return [0; 4]; @@ -126,8 +126,21 @@ impl Encode for PostFlopGame { // version VERSION_STR.to_string().encode(encoder)?; + // Update state based on target storage whenever the state is solved + // TODO(Jacob): May be better to add is_solved() function onto state + // That would prevent bugs caused by new states being added + let saved_state = if self.state >= State::SolvedFlop { + match self.target_storage_mode { + BoardState::Flop => State::SolvedFlop, + BoardState::Turn => State::SolvedTurn, + BoardState::River => State::Solved, + } + } else { + self.state + }; + // contents - self.state.encode(encoder)?; + saved_state.encode(encoder)?; self.card_config.encode(encoder)?; self.tree_config.encode(encoder)?; self.added_lines.encode(encoder)?; @@ -255,6 +268,8 @@ impl Decode for PostFlopGame { // restore the counterfactual values if game.storage_mode == BoardState::River && game.state == State::Solved { + // TODO(Jacob): Want to restructure finalize so that this hacky + // setting of game.state isn't necessary game.state = State::MemoryAllocated; finalize(&mut game); } From 818f556f2eaba1d5d2c566f1315dc4750d91e01e Mon Sep 17 00:00:00 2001 From: bkushigian Date: Tue, 27 Aug 2024 15:19:13 -0700 Subject: [PATCH 17/40] Tidied imports --- src/file.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/file.rs b/src/file.rs index ac10510..9380256 100644 --- a/src/file.rs +++ b/src/file.rs @@ -11,7 +11,6 @@ use crate::bunching::*; use crate::game::*; -use crate::interface::*; use bincode::{Decode, Encode}; use std::fs::File; use std::io::{BufReader, BufWriter, Read, Write}; From 70bde7865a5940656fbacb1078b3a5cadb4f8920 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Thu, 29 Aug 2024 00:32:47 -0700 Subject: [PATCH 18/40] tmp commit --- examples/file_io.rs | 30 ++++++++++++++++++++++ examples/node_locking.rs | 6 ++--- src/file.rs | 34 +++++++++++++++++++++++++ src/game/base.rs | 54 ++++++++++++++++++++++++++++++++++++++++ src/game/interpreter.rs | 36 ++++++++++++++++++++++++++- src/game/tests.rs | 8 +++--- 6 files changed, 160 insertions(+), 8 deletions(-) diff --git a/examples/file_io.rs b/examples/file_io.rs index e483de1..a265444 100644 --- a/examples/file_io.rs +++ b/examples/file_io.rs @@ -71,6 +71,36 @@ fn main() { // game tree constructed from this file cannot access information after the river deal save_data_to_file(&game2, "memo string", "filename.bin", None).unwrap(); + println!("Reloading and Resolving..."); + let mut game3 = PostFlopGame::hacky_reload_and_resolve(&game2, 100, 0.01, true).unwrap(); + println!("game2[0]: {}", game2.strategy()[0]); + println!("game3[0]: {}", game3.strategy()[0]); + for (i, (a, b)) in game2.strategy().iter().zip(game3.strategy()).enumerate() { + if (a - b).abs() > 0.001 { + println!("{i}: Oh no"); + } + } + + // game3.back_to_root(); + // game3.cache_normalized_weights(); + // for (a, b) in game2.equity(0).iter().zip(game3.equity(0)) { + // if (a - b).abs() > 0.001 { + // println!("Oh no"); + // } + // } + // game2.back_to_root(); + // game2.cache_normalized_weights(); + + // for (a, b) in game2 + // .expected_values(0) + // .iter() + // .zip(game3.expected_values(0)) + // { + // if (a - b).abs() > 0.001 { + // println!("{} {} {}", a, b, (a - b).abs()); + // } + // } + // delete the file std::fs::remove_file("filename.bin").unwrap(); } diff --git a/examples/node_locking.rs b/examples/node_locking.rs index 74ff464..143ecb9 100644 --- a/examples/node_locking.rs +++ b/examples/node_locking.rs @@ -27,7 +27,7 @@ fn normal_node_locking() { // node locking must be performed after allocating memory and before solving game.play(1); // OOP all-in - game.lock_current_strategy(&[0.25, 0.75]); // lock IP's strategy: 25% fold, 75% call + game.lock_current_node(&[0.25, 0.75]); // lock IP's strategy: 25% fold, 75% call game.back_to_root(); solve(&mut game, 1000, 0.001, false); @@ -42,7 +42,7 @@ fn normal_node_locking() { game.allocate_memory(false); game.play(1); - game.lock_current_strategy(&[0.5, 0.5]); // lock IP's strategy: 50% fold, 50% call + game.lock_current_node(&[0.5, 0.5]); // lock IP's strategy: 50% fold, 50% call game.back_to_root(); solve(&mut game, 1000, 0.001, false); @@ -77,7 +77,7 @@ fn partial_node_locking() { game.allocate_memory(false); // lock OOP's strategy: only JJ is locked and the rest is not - game.lock_current_strategy(&[0.8, 0.0, 0.0, 0.2, 0.0, 0.0]); // JJ: 80% check, 20% all-in + game.lock_current_node(&[0.8, 0.0, 0.0, 0.2, 0.0, 0.0]); // JJ: 80% check, 20% all-in solve(&mut game, 1000, 0.001, false); game.cache_normalized_weights(); diff --git a/src/file.rs b/src/file.rs index 9380256..2497bef 100644 --- a/src/file.rs +++ b/src/file.rs @@ -329,6 +329,40 @@ mod tests { assert!((root_ev_ip - 15.0).abs() < 1e-4); } + #[test] + fn hacky_reload_and_resolve() { + let card_config = CardConfig { + range: [Range::ones(); 2], + flop: flop_from_str("Td9d6h").unwrap(), + ..Default::default() + }; + + let tree_config = TreeConfig { + starting_pot: 60, + effective_stack: 970, + flop_bet_sizes: [("50%", "").try_into().unwrap(), Default::default()], + turn_bet_sizes: [("50%", "").try_into().unwrap(), Default::default()], + ..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); + + crate::solve(&mut game, 10, 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 turn_game: PostFlopGame = load_data_from_file("tmpfile.flop", None).unwrap().0; + + let mut reloaded = + PostFlopGame::hacky_reload_and_resolve(&turn_game, 10, 0.01, false).unwrap(); + } + #[test] #[cfg(feature = "zstd")] fn save_and_load_file_compressed() { diff --git a/src/game/base.rs b/src/game/base.rs index bab4b6b..3aea07f 100644 --- a/src/game/base.rs +++ b/src/game/base.rs @@ -810,6 +810,60 @@ impl PostFlopGame { info.num_storage_ip += node.num_elements_ip as u64; } + fn reload_and_resolve(game: &mut PostFlopGame) { + todo!("Not Implemented!") + } + + pub fn hacky_reload_and_resolve( + game: &PostFlopGame, + max_iterations: u32, + target_exploitability: f32, + print_progress: bool, + ) -> Result { + println!("Start hacky_reload_and_resolve"); + let card_config = game.card_config.clone(); + let action_tree = ActionTree::new(game.tree_config.clone())?; + print!("Building new game..."); + let mut new_game = PostFlopGame::with_config(card_config, action_tree)?; + println!("Done!"); + + print!("Allocating memory..."); + new_game.allocate_memory(game.is_compression_enabled()); + println!("Done!"); + + // Copy data into new game + print!("Copying memory..."); + for (dst, src) in new_game.storage1.iter_mut().zip(&game.storage1) { + *dst = *src; + } + for (dst, src) in new_game.storage2.iter_mut().zip(&game.storage2) { + *dst = *src; + } + for (dst, src) in new_game.storage_chance.iter_mut().zip(&game.storage_chance) { + *dst = *src; + } + for (dst, src) in new_game.storage_ip.iter_mut().zip(&game.storage_ip) { + *dst = *src; + } + // Nodelock and resolve + // for node_index in 0..game.node_arena.len() { + // let _ = new_game.lock_node_at_index(node_index); + // } + + let s1 = game.strategy(); + let s2 = new_game.strategy(); + println!("game2[0]: {}", s1[0]); + println!("game3[0]: {}", s2[0]); + // crate::solve( + // &mut new_game, + // max_iterations, + // target_exploitability, + // print_progress, + // ); + + Ok(new_game) + } + /// 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; diff --git a/src/game/interpreter.rs b/src/game/interpreter.rs index 7a9c8f5..02c22af 100644 --- a/src/game/interpreter.rs +++ b/src/game/interpreter.rs @@ -852,6 +852,40 @@ impl PostFlopGame { self.total_bet_amount } + /// Locks the strategy of the current node to the current strategy already in memory. + pub fn lock_current_strategy(&mut self) { + if self.is_terminal_node() { + panic!("Terminal node is not allowed"); + } + + if self.is_chance_node() { + panic!("Chance node is not allowed"); + } + + let mut node = self.node(); + + node.is_locked = true; + let index = self.node_index(&node); + self.locking_strategy + .insert(index, node.strategy().to_vec()); + } + + pub fn lock_node_at_index(&mut self, index: usize) -> Result<(), String> { + let mut node = self.node_arena[index].lock(); + if node.is_terminal() { + return Err("Cannot lock terminal node".to_string()); + } + + if node.is_chance() { + return Err("Cannot lock chance node".to_string()); + } + + node.is_locked = true; + self.locking_strategy + .insert(index, node.strategy().to_vec()); + Ok(()) + } + /// Locks the strategy of the current node. /// /// The `strategy` argument must be a slice of the length of `#(actions) * #(private hands)`. @@ -867,7 +901,7 @@ impl PostFlopGame { /// This method must be called after allocating memory and before solving the game. /// Panics if the memory is not yet allocated or the game is already solved. /// Also, panics if the current node is a terminal node or a chance node. - pub fn lock_current_strategy(&mut self, strategy: &[f32]) { + pub fn lock_current_node(&mut self, strategy: &[f32]) { if self.state < State::MemoryAllocated { panic!("Memory is not allocated"); } diff --git a/src/game/tests.rs b/src/game/tests.rs index c00c947..527cd42 100644 --- a/src/game/tests.rs +++ b/src/game/tests.rs @@ -869,7 +869,7 @@ fn node_locking() { game.allocate_memory(false); game.play(1); // all-in - game.lock_current_strategy(&[0.25, 0.75]); // 25% fold, 75% call + game.lock_current_node(&[0.25, 0.75]); // 25% fold, 75% call game.back_to_root(); solve(&mut game, 1000, 0.0, false); @@ -889,7 +889,7 @@ fn node_locking() { game.allocate_memory(false); game.play(1); // all-in - game.lock_current_strategy(&[0.5, 0.5]); // 50% fold, 50% call + game.lock_current_node(&[0.5, 0.5]); // 50% fold, 50% call game.back_to_root(); solve(&mut game, 1000, 0.0, false); @@ -929,7 +929,7 @@ fn node_locking_partial() { let mut game = PostFlopGame::with_config(card_config, action_tree).unwrap(); game.allocate_memory(false); - game.lock_current_strategy(&[0.8, 0.0, 0.0, 0.2, 0.0, 0.0]); // JJ -> 80% check, 20% all-in + game.lock_current_node(&[0.8, 0.0, 0.0, 0.2, 0.0, 0.0]); // JJ -> 80% check, 20% all-in solve(&mut game, 1000, 0.0, false); game.cache_normalized_weights(); @@ -970,7 +970,7 @@ fn node_locking_isomorphism() { game.allocate_memory(false); game.apply_history(&[0, 0, 15, 0, 0, 14]); // Turn: Spades, River: Hearts - game.lock_current_strategy(&[0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0]); // AhKh -> check + game.lock_current_node(&[0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0]); // AhKh -> check finalize(&mut game); From 9cd7ed5d150bbf5b4b11d021afaa0836492b8c46 Mon Sep 17 00:00:00 2001 From: Jacob Van Geffen Date: Fri, 30 Aug 2024 11:54:45 -0500 Subject: [PATCH 19/40] tmp commit; resolve uses normalized strategy for locking --- src/game/base.rs | 26 +++++++++++++++----------- src/game/interpreter.rs | 34 +++++++++++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/src/game/base.rs b/src/game/base.rs index 3aea07f..f3821fd 100644 --- a/src/game/base.rs +++ b/src/game/base.rs @@ -846,20 +846,24 @@ impl PostFlopGame { *dst = *src; } // Nodelock and resolve - // for node_index in 0..game.node_arena.len() { - // let _ = new_game.lock_node_at_index(node_index); - // } + for node_index in 0..game.node_arena.len() { + let _ = new_game.lock_node_at_index(node_index); + } let s1 = game.strategy(); let s2 = new_game.strategy(); - println!("game2[0]: {}", s1[0]); - println!("game3[0]: {}", s2[0]); - // crate::solve( - // &mut new_game, - // max_iterations, - // target_exploitability, - // print_progress, - // ); + println!("game2[{}]: {}", game.node_index(&game.node()), s1[0]); + println!( + "game3[{}]: {}", + new_game.node_index(&new_game.node()), + s2[0] + ); + crate::solve( + &mut new_game, + max_iterations, + target_exploitability, + print_progress, + ); Ok(new_game) } diff --git a/src/game/interpreter.rs b/src/game/interpreter.rs index 02c22af..aef3e9c 100644 --- a/src/game/interpreter.rs +++ b/src/game/interpreter.rs @@ -660,6 +660,7 @@ impl PostFlopGame { /// [`expected_values_detail`]: #method.expected_values_detail pub fn expected_values(&self, player: usize) -> Vec { if !self.is_partially_solved() { + println!("{:?}", self.state); panic!("Game is not solved"); } @@ -846,6 +847,32 @@ impl PostFlopGame { ret } + // TODO use in .strategy() + pub fn strategy_at_node(&self, node: &PostFlopNode) -> Vec { + if self.state < State::MemoryAllocated { + panic!("Memory is not allocated"); + } + + let player = self.current_player(); + let num_actions = node.num_actions(); + let num_hands = self.num_private_hands(player); + + let mut ret = if self.is_compression_enabled { + normalized_strategy_compressed(node.strategy_compressed(), num_actions) + } else { + normalized_strategy(node.strategy(), num_actions) + }; + + let locking = self.locking_strategy(&node); + apply_locking_strategy(&mut ret, locking); + + ret.chunks_exact_mut(num_hands).for_each(|chunk| { + self.apply_swap(chunk, player, false); + }); + + ret + } + /// Returns the total bet amount of each player (OOP, IP). #[inline] pub fn total_bet_amount(&self) -> [i32; 2] { @@ -880,9 +907,10 @@ impl PostFlopGame { return Err("Cannot lock chance node".to_string()); } + let strategy = self.strategy_at_node(&node); + node.is_locked = true; - self.locking_strategy - .insert(index, node.strategy().to_vec()); + self.locking_strategy.insert(index, strategy); Ok(()) } @@ -1028,7 +1056,7 @@ impl PostFlopGame { /// Returns the reference to the current node. #[inline] - fn node(&self) -> MutexGuardLike { + pub fn node(&self) -> MutexGuardLike { self.node_arena[self.node_history.last().cloned().unwrap_or(0)].lock() } From 01c5adb5ba6ce679fc98cd05eded9b05a8c0aa19 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Fri, 30 Aug 2024 15:43:17 -0700 Subject: [PATCH 20/40] Fixed warnings --- 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 f3821fd..e0d7bb8 100644 --- a/src/game/base.rs +++ b/src/game/base.rs @@ -810,7 +810,7 @@ impl PostFlopGame { info.num_storage_ip += node.num_elements_ip as u64; } - fn reload_and_resolve(game: &mut PostFlopGame) { + pub fn reload_and_resolve(_game: &mut PostFlopGame) { todo!("Not Implemented!") } From 47600b95e132d564f66adbd67da9d5c5109d8981 Mon Sep 17 00:00:00 2001 From: Jacob Van Geffen Date: Thu, 26 Sep 2024 12:55:45 -0500 Subject: [PATCH 21/40] Only lock flop and turn nodes --- src/game/base.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/game/base.rs b/src/game/base.rs index e0d7bb8..d89d8e8 100644 --- a/src/game/base.rs +++ b/src/game/base.rs @@ -845,19 +845,15 @@ impl PostFlopGame { for (dst, src) in new_game.storage_ip.iter_mut().zip(&game.storage_ip) { *dst = *src; } + println!("Done!"); + // Nodelock and resolve for node_index in 0..game.node_arena.len() { - let _ = new_game.lock_node_at_index(node_index); + if new_game.node_arena[node_index].lock().river == NOT_DEALT { + let _ = new_game.lock_node_at_index(node_index); + } } - let s1 = game.strategy(); - let s2 = new_game.strategy(); - println!("game2[{}]: {}", game.node_index(&game.node()), s1[0]); - println!( - "game3[{}]: {}", - new_game.node_index(&new_game.node()), - s2[0] - ); crate::solve( &mut new_game, max_iterations, From 3fbfa82510f983e98a24b45c09f98f974d0387d8 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Sun, 29 Sep 2024 12:37:04 -0700 Subject: [PATCH 22/40] hacky_reload_and_resolve working --- src/game/base.rs | 20 +++++++++++++++----- src/solver.rs | 17 +++++++++++++++-- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/game/base.rs b/src/game/base.rs index d89d8e8..3d451c2 100644 --- a/src/game/base.rs +++ b/src/game/base.rs @@ -823,6 +823,8 @@ impl PostFlopGame { println!("Start hacky_reload_and_resolve"); let card_config = game.card_config.clone(); let action_tree = ActionTree::new(game.tree_config.clone())?; + let target_storage_mode = game.target_storage_mode(); + print!("Building new game..."); let mut new_game = PostFlopGame::with_config(card_config, action_tree)?; println!("Done!"); @@ -847,13 +849,21 @@ impl PostFlopGame { } println!("Done!"); - // Nodelock and resolve - for node_index in 0..game.node_arena.len() { - if new_game.node_arena[node_index].lock().river == NOT_DEALT { - let _ = new_game.lock_node_at_index(node_index); - } + if target_storage_mode == BoardState::River { + return Ok(new_game); } + // Nodelock and resolve + let num_nodes_to_lock = match target_storage_mode { + BoardState::River => 0, + BoardState::Turn => game.num_nodes_per_street[0] + game.num_nodes_per_street[1], + BoardState::Flop => game.num_nodes_per_street[0], + }; + + for node_index in 0..num_nodes_to_lock as usize { + // We can't ? because this tries to lock chance nodes + let _ = new_game.lock_node_at_index(node_index); + } crate::solve( &mut new_game, max_iterations, diff --git a/src/solver.rs b/src/solver.rs index 1548a1a..44405f6 100644 --- a/src/solver.rs +++ b/src/solver.rs @@ -58,6 +58,19 @@ pub fn solve( } let mut root = game.root(); + + // Run solve_recursive once to initialize uninitialized strat memory + 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), + &DiscountParams::new(0), + ); + } let mut exploitability = compute_exploitability(game); if print_progress { @@ -66,7 +79,7 @@ pub fn solve( io::stdout().flush().unwrap(); } - for t in 0..max_num_iterations { + for t in 1..max_num_iterations { if exploitability <= target_exploitability { break; } @@ -86,7 +99,7 @@ pub fn solve( ); } - if (t + 1) % 10 == 0 || t + 1 == max_num_iterations { + if t % 10 == 0 || t + 1 == max_num_iterations { exploitability = compute_exploitability(game); } From fdda64cd3ad2e709ddbadc48a1012ed2202fdfd1 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Sun, 29 Sep 2024 12:37:19 -0700 Subject: [PATCH 23/40] Updated file_io example --- examples/file_io.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/file_io.rs b/examples/file_io.rs index a265444..f9a3025 100644 --- a/examples/file_io.rs +++ b/examples/file_io.rs @@ -72,9 +72,8 @@ fn main() { save_data_to_file(&game2, "memo string", "filename.bin", None).unwrap(); println!("Reloading and Resolving..."); - let mut game3 = PostFlopGame::hacky_reload_and_resolve(&game2, 100, 0.01, true).unwrap(); - println!("game2[0]: {}", game2.strategy()[0]); - println!("game3[0]: {}", game3.strategy()[0]); + let game3 = + PostFlopGame::hacky_reload_and_resolve(&game2, 100, target_exploitability, true).unwrap(); for (i, (a, b)) in game2.strategy().iter().zip(game3.strategy()).enumerate() { if (a - b).abs() > 0.001 { println!("{i}: Oh no"); From ab8a78599fc75e5325ad68dbe6f5b5b161b86c31 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Wed, 2 Oct 2024 15:03:21 -0700 Subject: [PATCH 24/40] Updated file_io --- examples/file_io.rs | 83 +++++++++++++++++++++++++++++++-------------- 1 file changed, 57 insertions(+), 26 deletions(-) diff --git a/examples/file_io.rs b/examples/file_io.rs index f9a3025..1759df4 100644 --- a/examples/file_io.rs +++ b/examples/file_io.rs @@ -1,22 +1,24 @@ use postflop_solver::*; +use std::time; 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 file_save_name = "test_save.pfs"; 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(), + turn: NOT_DEALT, //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, + initial_state: BoardState::Flop, starting_pot: 200, effective_stack: 900, rake_rate: 0.0, @@ -37,22 +39,30 @@ fn main() { let max_num_iterations = 1000; let target_exploitability = game.tree_config().starting_pot as f32 * 0.005; + let full_solve_start = time::Instant::now(); solve(&mut game, max_num_iterations, target_exploitability, true); + println!( + "Full solve: {} seconds", + full_solve_start.elapsed().as_secs() + ); + // 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(); + save_data_to_file(&game, "memo string", file_save_name, 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(); + load_data_from_file(file_save_name, None).unwrap(); // 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)); + println!("\n-----------------------------------------"); + println!("Saving [Turn Save] to {}", file_save_name); // 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(); @@ -69,9 +79,10 @@ 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(); + save_data_to_file(&game2, "memo string", file_save_name, None).unwrap(); - println!("Reloading and Resolving..."); + println!("Reloading from Turn Save and Resolving..."); + let turn_solve_start = time::Instant::now(); let game3 = PostFlopGame::hacky_reload_and_resolve(&game2, 100, target_exploitability, true).unwrap(); for (i, (a, b)) in game2.strategy().iter().zip(game3.strategy()).enumerate() { @@ -79,27 +90,47 @@ fn main() { println!("{i}: Oh no"); } } + println!( + "Turn solve: {} seconds", + turn_solve_start.elapsed().as_secs() + ); + + println!("\n-----------------------------------------"); + println!("Saving [Flop Save] to {}", file_save_name); + // 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::Flop).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", file_save_name, None).unwrap(); + + println!("Reloading from River Save and Resolving..."); + let river_solve_start = time::Instant::now(); + let game3 = + PostFlopGame::hacky_reload_and_resolve(&game2, 100, target_exploitability, true).unwrap(); - // game3.back_to_root(); - // game3.cache_normalized_weights(); - // for (a, b) in game2.equity(0).iter().zip(game3.equity(0)) { - // if (a - b).abs() > 0.001 { - // println!("Oh no"); - // } - // } - // game2.back_to_root(); - // game2.cache_normalized_weights(); - - // for (a, b) in game2 - // .expected_values(0) - // .iter() - // .zip(game3.expected_values(0)) - // { - // if (a - b).abs() > 0.001 { - // println!("{} {} {}", a, b, (a - b).abs()); - // } - // } + println!( + "River solve: {} seconds", + river_solve_start.elapsed().as_secs() + ); + + for (i, (a, b)) in game2.strategy().iter().zip(game3.strategy()).enumerate() { + if (a - b).abs() > 0.001 { + println!("{i}: Oh no"); + } + } // delete the file - std::fs::remove_file("filename.bin").unwrap(); + std::fs::remove_file(file_save_name).unwrap(); } From 58365a9e2878c82ac4ac0be6eb2f4b8378194fed Mon Sep 17 00:00:00 2001 From: bkushigian Date: Wed, 2 Oct 2024 16:31:39 -0700 Subject: [PATCH 25/40] renamed hacky_reload_and_resolve, implemented reload_and_resolve --- examples/file_io.rs | 6 ++++-- src/file.rs | 2 +- src/game/base.rs | 18 +++++++++++++++--- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/examples/file_io.rs b/examples/file_io.rs index 1759df4..d4689d2 100644 --- a/examples/file_io.rs +++ b/examples/file_io.rs @@ -84,7 +84,7 @@ fn main() { println!("Reloading from Turn Save and Resolving..."); let turn_solve_start = time::Instant::now(); let game3 = - PostFlopGame::hacky_reload_and_resolve(&game2, 100, target_exploitability, true).unwrap(); + PostFlopGame::copy_reload_and_resolve(&game2, 100, target_exploitability, true).unwrap(); for (i, (a, b)) in game2.strategy().iter().zip(game3.strategy()).enumerate() { if (a - b).abs() > 0.001 { println!("{i}: Oh no"); @@ -118,7 +118,7 @@ fn main() { println!("Reloading from River Save and Resolving..."); let river_solve_start = time::Instant::now(); let game3 = - PostFlopGame::hacky_reload_and_resolve(&game2, 100, target_exploitability, true).unwrap(); + PostFlopGame::copy_reload_and_resolve(&game2, 100, target_exploitability, true).unwrap(); println!( "River solve: {} seconds", @@ -131,6 +131,8 @@ fn main() { } } + let _ = PostFlopGame::reload_and_resolve(&mut game2, 1000, target_exploitability, true); + // delete the file std::fs::remove_file(file_save_name).unwrap(); } diff --git a/src/file.rs b/src/file.rs index 2497bef..27ea728 100644 --- a/src/file.rs +++ b/src/file.rs @@ -360,7 +360,7 @@ mod tests { let mut turn_game: PostFlopGame = load_data_from_file("tmpfile.flop", None).unwrap().0; let mut reloaded = - PostFlopGame::hacky_reload_and_resolve(&turn_game, 10, 0.01, false).unwrap(); + PostFlopGame::copy_reload_and_resolve(&turn_game, 10, 0.01, false).unwrap(); } #[test] diff --git a/src/game/base.rs b/src/game/base.rs index 3d451c2..9ad07ec 100644 --- a/src/game/base.rs +++ b/src/game/base.rs @@ -810,11 +810,23 @@ impl PostFlopGame { info.num_storage_ip += node.num_elements_ip as u64; } - pub fn reload_and_resolve(_game: &mut PostFlopGame) { - todo!("Not Implemented!") + pub fn reload_and_resolve( + game: &mut PostFlopGame, + max_iterations: u32, + target_exploitability: f32, + print_progress: bool, + ) -> Result<(), String> { + *game = PostFlopGame::copy_reload_and_resolve( + game, + max_iterations, + target_exploitability, + print_progress, + )?; + Ok(()) } - pub fn hacky_reload_and_resolve( + /// copy_reload_and_resolve + pub fn copy_reload_and_resolve( game: &PostFlopGame, max_iterations: u32, target_exploitability: f32, From 22e93959b9a1f1201c9d943519e58c7dbc753627 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Wed, 2 Oct 2024 17:56:22 -0700 Subject: [PATCH 26/40] Removed temporary node locks used for resolve, added docs, tests --- src/file.rs | 26 ++++++++++++++++++++------ src/game/base.rs | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/src/file.rs b/src/file.rs index 27ea728..ae99de0 100644 --- a/src/file.rs +++ b/src/file.rs @@ -330,7 +330,7 @@ mod tests { } #[test] - fn hacky_reload_and_resolve() { + fn reload_and_resolve() { let card_config = CardConfig { range: [Range::ones(); 2], flop: flop_from_str("Td9d6h").unwrap(), @@ -351,16 +351,30 @@ mod tests { game.allocate_memory(false); crate::solve(&mut game, 10, 0.01, false); + let file_save_name = "tmpfile.pfs"; // save (turn) game.set_target_storage_mode(BoardState::Turn).unwrap(); - save_data_to_file(&game, "", "tmpfile.flop", None).unwrap(); + save_data_to_file(&game, "", file_save_name, None).unwrap(); // load (turn) - let mut turn_game: PostFlopGame = load_data_from_file("tmpfile.flop", None).unwrap().0; - - let mut reloaded = - PostFlopGame::copy_reload_and_resolve(&turn_game, 10, 0.01, false).unwrap(); + let mut turn_game: PostFlopGame = load_data_from_file(file_save_name, None).unwrap().0; + turn_game.cache_normalized_weights(); + let ev_oop = turn_game.expected_values(0); + let ev_ip = turn_game.expected_values(1); + + assert!(PostFlopGame::reload_and_resolve(&mut turn_game, 10, 0.01, false).is_ok()); + turn_game.cache_normalized_weights(); + let resolved_ev_oop = turn_game.expected_values(0); + let resolved_ev_ip = turn_game.expected_values(1); + + for (ev1, ev2) in ev_oop.iter().zip(resolved_ev_oop) { + assert!((ev1 - ev2).abs() < 0.00001); + } + for (ev1, ev2) in ev_ip.iter().zip(resolved_ev_ip) { + assert!((ev1 - ev2).abs() < 0.00001); + } + std::fs::remove_file(file_save_name).unwrap(); } #[test] diff --git a/src/game/base.rs b/src/game/base.rs index 9ad07ec..cebc378 100644 --- a/src/game/base.rs +++ b/src/game/base.rs @@ -2,6 +2,7 @@ use super::*; use crate::bunching::*; use crate::interface::*; use crate::utility::*; +use std::collections::HashSet; use std::mem::{self, MaybeUninit}; #[cfg(feature = "rayon")] @@ -810,6 +811,12 @@ impl PostFlopGame { info.num_storage_ip += node.num_elements_ip as u64; } + /// reload_and_resolve + /// + /// Reload forgotten streets and resolve to target exploitability. + /// + /// Note: This currently wraps [`Self::copy_reload_and_resolve`] which is + /// not as memory efficient as it could be. pub fn reload_and_resolve( game: &mut PostFlopGame, max_iterations: u32, @@ -826,6 +833,18 @@ impl PostFlopGame { } /// copy_reload_and_resolve + /// + /// Copy `game` into a new `PostFlopGame` and rebuild/resolve any forgotten + /// streets. + /// + /// The solver will run until either `max_iterations` iterations have passed or + /// + /// # Arguments + /// + /// * `game` - the game to copy, rebuild, and resolve + /// * `max_iterations` - the maximum number of iterations to run the solver for + /// * `target_exploitability` - target exploitability for a solution + /// * `print_progress` - print progress during the solve pub fn copy_reload_and_resolve( game: &PostFlopGame, max_iterations: u32, @@ -872,10 +891,15 @@ impl PostFlopGame { BoardState::Flop => game.num_nodes_per_street[0], }; + // We are about to node lock a bunch of nodes to resolve more + // efficiently, so we preserve which keys were already locked to the + // current strategy + let already_locked_nodes = game.locking_strategy.keys().collect::>(); for node_index in 0..num_nodes_to_lock as usize { // We can't ? because this tries to lock chance nodes let _ = new_game.lock_node_at_index(node_index); } + crate::solve( &mut new_game, max_iterations, @@ -883,6 +907,19 @@ impl PostFlopGame { print_progress, ); + // Remove node locking from resolving but retain node locking passed in + // with `game`. Note that we _have to maintain the invariant that a + // node's index is in the game's locking_strategy if and only if + // node.is_locked == true._ That is: + // + // game.node_arena[node_index].is_locked <=> node_index in game.locking_strategy.keys() + for node_index in 0..num_nodes_to_lock as usize { + if !already_locked_nodes.contains(&node_index) { + new_game.node_arena[node_index].lock().is_locked = false; + new_game.locking_strategy.remove(&node_index); + } + } + Ok(new_game) } From af91af2ff8f3852f17e1135dc26d1cbec151cc32 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Mon, 7 Oct 2024 00:24:51 -0700 Subject: [PATCH 27/40] Removed unnecessary borrow --- src/game/interpreter.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/interpreter.rs b/src/game/interpreter.rs index aef3e9c..2e0187e 100644 --- a/src/game/interpreter.rs +++ b/src/game/interpreter.rs @@ -863,7 +863,7 @@ impl PostFlopGame { normalized_strategy(node.strategy(), num_actions) }; - let locking = self.locking_strategy(&node); + let locking = self.locking_strategy(node); apply_locking_strategy(&mut ret, locking); ret.chunks_exact_mut(num_hands).for_each(|chunk| { From 29e67ea755e719e18781aba8d13e542643330e78 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Mon, 7 Oct 2024 12:07:38 -0700 Subject: [PATCH 28/40] Fix bug in conditional compilation rename --- src/solver.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/solver.rs b/src/solver.rs index b8765e8..86fa129 100644 --- a/src/solver.rs +++ b/src/solver.rs @@ -192,9 +192,10 @@ fn solve_recursive( // // Rows are obtained using operations from `sliceop` (e.g., `sliceop::row_mut()`). // - // `cfv_actions` will be written to by recursive calls to `solve_recursive`. + // `cfv_actions_hands` 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)); + let cfv_actions_hands = + MutexLike::new(Vec::with_capacity_in(num_actions * num_hands, StackAlloc)); #[cfg(not(feature = "custom-alloc"))] let cfv_actions_hands = MutexLike::new(Vec::with_capacity(num_actions * num_hands)); From 0d9f5a77c914715135ceacd2b3d9e7f3011f24e7 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Mon, 7 Oct 2024 12:17:04 -0700 Subject: [PATCH 29/40] Fixed some example comments and names --- examples/file_io.rs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/examples/file_io.rs b/examples/file_io.rs index d4689d2..c072a9f 100644 --- a/examples/file_io.rs +++ b/examples/file_io.rs @@ -77,8 +77,9 @@ fn main() { 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 + // Overwrite the file with the truncated game tree. The game tree + // constructed from this file cannot access information after the turn; this + // data will need to be recomputed via `PostFlopGame::reload_and_resolve`. save_data_to_file(&game2, "memo string", file_save_name, None).unwrap(); println!("Reloading from Turn Save and Resolving..."); @@ -97,7 +98,7 @@ fn main() { println!("\n-----------------------------------------"); println!("Saving [Flop Save] to {}", file_save_name); - // discard information after the river deal when serializing + // discard information after the flop deal when serializing // this operation does not lose any information of the game tree itself game2.set_target_storage_mode(BoardState::Flop).unwrap(); @@ -112,17 +113,17 @@ 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", file_save_name, None).unwrap(); + // game tree constructed from this file cannot access information after the flop deal + save_data_to_file(&game2, "This is a flop save", file_save_name, None).unwrap(); - println!("Reloading from River Save and Resolving..."); - let river_solve_start = time::Instant::now(); + println!("Reloading from Flop Save and Resolving..."); + let flop_solve_start = time::Instant::now(); let game3 = PostFlopGame::copy_reload_and_resolve(&game2, 100, target_exploitability, true).unwrap(); println!( - "River solve: {} seconds", - river_solve_start.elapsed().as_secs() + "\nFlop solve: {} seconds", + flop_solve_start.elapsed().as_secs() ); for (i, (a, b)) in game2.strategy().iter().zip(game3.strategy()).enumerate() { From 68083a8d0566e919a78582271a1a0bb81be0d970 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Mon, 7 Oct 2024 12:17:18 -0700 Subject: [PATCH 30/40] Removed print debug statements --- src/game/base.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/game/base.rs b/src/game/base.rs index 4ae8e08..d16234b 100644 --- a/src/game/base.rs +++ b/src/game/base.rs @@ -854,21 +854,14 @@ impl PostFlopGame { target_exploitability: f32, print_progress: bool, ) -> Result { - println!("Start hacky_reload_and_resolve"); let card_config = game.card_config.clone(); let action_tree = ActionTree::new(game.tree_config.clone())?; let target_storage_mode = game.target_storage_mode(); - print!("Building new game..."); let mut new_game = PostFlopGame::with_config(card_config, action_tree)?; - println!("Done!"); - - print!("Allocating memory..."); new_game.allocate_memory(game.is_compression_enabled()); - println!("Done!"); // Copy data into new game - print!("Copying memory..."); for (dst, src) in new_game.storage1.iter_mut().zip(&game.storage1) { *dst = *src; } @@ -881,7 +874,6 @@ impl PostFlopGame { for (dst, src) in new_game.storage_ip.iter_mut().zip(&game.storage_ip) { *dst = *src; } - println!("Done!"); if target_storage_mode == BoardState::River { return Ok(new_game); From f61782ded3fe4c93284771919dd418897746524a Mon Sep 17 00:00:00 2001 From: bkushigian Date: Mon, 7 Oct 2024 12:20:31 -0700 Subject: [PATCH 31/40] Updated examples/file_io.rs for clarity --- examples/file_io.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/examples/file_io.rs b/examples/file_io.rs index c072a9f..35868b3 100644 --- a/examples/file_io.rs +++ b/examples/file_io.rs @@ -118,6 +118,7 @@ fn main() { println!("Reloading from Flop Save and Resolving..."); let flop_solve_start = time::Instant::now(); + println!("Using copy_reload_and_resolve: this results in a new game"); let game3 = PostFlopGame::copy_reload_and_resolve(&game2, 100, target_exploitability, true).unwrap(); @@ -132,7 +133,14 @@ fn main() { } } + println!(); + println!("Using reload_and_resolve: this overwrites the existing game"); + let flop_solve_start = time::Instant::now(); let _ = PostFlopGame::reload_and_resolve(&mut game2, 1000, target_exploitability, true); + println!( + "\nFlop solve: {} seconds", + flop_solve_start.elapsed().as_secs() + ); // delete the file std::fs::remove_file(file_save_name).unwrap(); From 1038f63b167112a4f566381153a142c1d3a5ea74 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Mon, 7 Oct 2024 13:34:42 -0700 Subject: [PATCH 32/40] Checked out file_io.rs from main, and moved updated version to file_io_big.rs --- examples/file_io.rs | 85 ++--------------------- examples/file_io_big.rs | 147 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+), 78 deletions(-) create mode 100644 examples/file_io_big.rs diff --git a/examples/file_io.rs b/examples/file_io.rs index 35868b3..e483de1 100644 --- a/examples/file_io.rs +++ b/examples/file_io.rs @@ -1,24 +1,22 @@ use postflop_solver::*; -use std::time; 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 file_save_name = "test_save.pfs"; let card_config = CardConfig { range: [oop_range.parse().unwrap(), ip_range.parse().unwrap()], flop: flop_from_str("Td9d6h").unwrap(), - turn: NOT_DEALT, //card_from_str("Qc").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::Flop, + initial_state: BoardState::Turn, starting_pot: 200, effective_stack: 900, rake_rate: 0.0, @@ -39,30 +37,22 @@ fn main() { let max_num_iterations = 1000; let target_exploitability = game.tree_config().starting_pot as f32 * 0.005; - let full_solve_start = time::Instant::now(); solve(&mut game, max_num_iterations, target_exploitability, true); - println!( - "Full solve: {} seconds", - full_solve_start.elapsed().as_secs() - ); - // 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", file_save_name, None).unwrap(); + 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(file_save_name, None).unwrap(); + load_data_from_file("filename.bin", None).unwrap(); // 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)); - println!("\n-----------------------------------------"); - println!("Saving [Turn Save] to {}", file_save_name); // 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(); @@ -77,71 +67,10 @@ fn main() { game2.target_memory_usage() as f64 / (1024.0 * 1024.0) ); - // Overwrite the file with the truncated game tree. The game tree - // constructed from this file cannot access information after the turn; this - // data will need to be recomputed via `PostFlopGame::reload_and_resolve`. - save_data_to_file(&game2, "memo string", file_save_name, None).unwrap(); - - println!("Reloading from Turn Save and Resolving..."); - let turn_solve_start = time::Instant::now(); - let game3 = - PostFlopGame::copy_reload_and_resolve(&game2, 100, target_exploitability, true).unwrap(); - for (i, (a, b)) in game2.strategy().iter().zip(game3.strategy()).enumerate() { - if (a - b).abs() > 0.001 { - println!("{i}: Oh no"); - } - } - println!( - "Turn solve: {} seconds", - turn_solve_start.elapsed().as_secs() - ); - - println!("\n-----------------------------------------"); - println!("Saving [Flop Save] to {}", file_save_name); - // discard information after the flop deal when serializing - // this operation does not lose any information of the game tree itself - game2.set_target_storage_mode(BoardState::Flop).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 flop deal - save_data_to_file(&game2, "This is a flop save", file_save_name, None).unwrap(); - - println!("Reloading from Flop Save and Resolving..."); - let flop_solve_start = time::Instant::now(); - println!("Using copy_reload_and_resolve: this results in a new game"); - let game3 = - PostFlopGame::copy_reload_and_resolve(&game2, 100, target_exploitability, true).unwrap(); - - println!( - "\nFlop solve: {} seconds", - flop_solve_start.elapsed().as_secs() - ); - - for (i, (a, b)) in game2.strategy().iter().zip(game3.strategy()).enumerate() { - if (a - b).abs() > 0.001 { - println!("{i}: Oh no"); - } - } - - println!(); - println!("Using reload_and_resolve: this overwrites the existing game"); - let flop_solve_start = time::Instant::now(); - let _ = PostFlopGame::reload_and_resolve(&mut game2, 1000, target_exploitability, true); - println!( - "\nFlop solve: {} seconds", - flop_solve_start.elapsed().as_secs() - ); + // 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(file_save_name).unwrap(); + std::fs::remove_file("filename.bin").unwrap(); } diff --git a/examples/file_io_big.rs b/examples/file_io_big.rs new file mode 100644 index 0000000..a6df5ec --- /dev/null +++ b/examples/file_io_big.rs @@ -0,0 +1,147 @@ +use postflop_solver::*; +use std::time; + +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 file_save_name = "test_save.pfs"; + + let card_config = CardConfig { + range: [oop_range.parse().unwrap(), ip_range.parse().unwrap()], + flop: flop_from_str("Td9d6h").unwrap(), + turn: NOT_DEALT, //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::Flop, + 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 = 1000; + let target_exploitability = game.tree_config().starting_pot as f32 * 0.005; + let full_solve_start = time::Instant::now(); + solve(&mut game, max_num_iterations, target_exploitability, true); + + println!( + "Full solve: {:5.3} seconds", + full_solve_start.elapsed().as_secs_f64() + ); + + // 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", file_save_name, 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(file_save_name, None).unwrap(); + + // 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)); + + println!("\n-----------------------------------------"); + println!("Saving [Turn Save] to {}", file_save_name); + // 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. The game tree + // constructed from this file cannot access information after the turn; this + // data will need to be recomputed via `PostFlopGame::reload_and_resolve`. + save_data_to_file(&game2, "memo string", file_save_name, None).unwrap(); + + println!("Reloading from Turn Save and Resolving..."); + let turn_solve_start = time::Instant::now(); + let game3 = + PostFlopGame::copy_reload_and_resolve(&game2, 100, target_exploitability, true).unwrap(); + for (i, (a, b)) in game2.strategy().iter().zip(game3.strategy()).enumerate() { + if (a - b).abs() > 0.001 { + println!("{i}: Oh no"); + } + } + println!( + "Turn solve: {:5.3} seconds", + turn_solve_start.elapsed().as_secs_f64() + ); + + println!("\n-----------------------------------------"); + println!("Saving [Flop Save] to {}", file_save_name); + // discard information after the flop deal when serializing + // this operation does not lose any information of the game tree itself + game2.set_target_storage_mode(BoardState::Flop).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 flop deal + save_data_to_file(&game2, "This is a flop save", file_save_name, None).unwrap(); + + println!("Reloading from Flop Save and Resolving..."); + let flop_solve_start = time::Instant::now(); + println!("Using copy_reload_and_resolve: this results in a new game"); + let game3 = + PostFlopGame::copy_reload_and_resolve(&game2, 100, target_exploitability, true).unwrap(); + + println!( + "\nFlop solve: {:5.3} seconds", + flop_solve_start.elapsed().as_secs_f64() + ); + + for (i, (a, b)) in game2.strategy().iter().zip(game3.strategy()).enumerate() { + if (a - b).abs() > 0.001 { + println!("{i}: Oh no"); + } + } + + println!(); + println!("Using reload_and_resolve: this overwrites the existing game"); + let flop_solve_start = time::Instant::now(); + let _ = PostFlopGame::reload_and_resolve(&mut game2, 1000, target_exploitability, true); + println!( + "\nFlop solve: {:5.3} seconds", + flop_solve_start.elapsed().as_secs_f64() + ); + + // delete the file + std::fs::remove_file(file_save_name).unwrap(); +} From d84bb469c63faf4bc28e9018d97bccaa0bf03856 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Mon, 7 Oct 2024 16:44:46 -0700 Subject: [PATCH 33/40] Small refactor, comment for clarity --- src/game/interpreter.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/game/interpreter.rs b/src/game/interpreter.rs index 2e0187e..7c415fd 100644 --- a/src/game/interpreter.rs +++ b/src/game/interpreter.rs @@ -891,8 +891,10 @@ impl PostFlopGame { let mut node = self.node(); - node.is_locked = true; let index = self.node_index(&node); + // Note: node.is_locked is tightly coupled with locking_strategy + // containing the node's index + node.is_locked = true; self.locking_strategy .insert(index, node.strategy().to_vec()); } From a3b84e4d6e9e5d1f0cca426bfdfc8b7bdb0825ba Mon Sep 17 00:00:00 2001 From: bkushigian Date: Mon, 7 Oct 2024 16:53:22 -0700 Subject: [PATCH 34/40] Resolved comments in review --- src/file.rs | 43 ----------------------------------------- src/game/interpreter.rs | 20 +++++++++---------- 2 files changed, 9 insertions(+), 54 deletions(-) diff --git a/src/file.rs b/src/file.rs index 930290c..bde9421 100644 --- a/src/file.rs +++ b/src/file.rs @@ -469,47 +469,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_resolve2() { - 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); - - let file_save_location = "test_reload_and_resolve2.flop"; - // save (turn) - game.set_target_storage_mode(BoardState::Turn).unwrap(); - save_data_to_file(&game, "", file_save_location, None).unwrap(); - - // load (turn) - let mut game: PostFlopGame = load_data_from_file(file_save_location, None).unwrap().0; - assert!(PostFlopGame::reload_and_resolve(&mut game, 100, 0.01, true).is_ok()); - std::fs::remove_file(file_save_location).unwrap(); - } } diff --git a/src/game/interpreter.rs b/src/game/interpreter.rs index 7c415fd..7597aae 100644 --- a/src/game/interpreter.rs +++ b/src/game/interpreter.rs @@ -880,13 +880,13 @@ impl PostFlopGame { } /// Locks the strategy of the current node to the current strategy already in memory. - pub fn lock_current_strategy(&mut self) { + pub fn lock_current_strategy(&mut self) -> Result<(), String> { if self.is_terminal_node() { - panic!("Terminal node is not allowed"); + return Err("Cannot lock terminal nodes".to_string()); } if self.is_chance_node() { - panic!("Chance node is not allowed"); + return Err("Cannot lock chance nodes".to_string()); } let mut node = self.node(); @@ -897,6 +897,7 @@ impl PostFlopGame { node.is_locked = true; self.locking_strategy .insert(index, node.strategy().to_vec()); + Ok(()) } pub fn lock_node_at_index(&mut self, index: usize) -> Result<(), String> { @@ -931,21 +932,17 @@ impl PostFlopGame { /// This method must be called after allocating memory and before solving the game. /// Panics if the memory is not yet allocated or the game is already solved. /// Also, panics if the current node is a terminal node or a chance node. - pub fn lock_current_node(&mut self, strategy: &[f32]) { + pub fn lock_current_node(&mut self, strategy: &[f32]) -> Result<(), String> { if self.state < State::MemoryAllocated { - panic!("Memory is not allocated"); - } - - if self.state == State::Solved { - panic!("Game is already solved"); + return Err("Memory is not allocated".to_string()); } if self.is_terminal_node() { - panic!("Terminal node is not allowed"); + return Err("Cannot lock terminal nodes".to_string()); } if self.is_chance_node() { - panic!("Chance node is not allowed"); + return Err("Cannot lock chance nodes".to_string()); } let mut node = self.node(); @@ -986,6 +983,7 @@ impl PostFlopGame { node.is_locked = true; let index = self.node_index(&node); self.locking_strategy.insert(index, locking); + Ok(()) } /// Unlocks the strategy of the current node. From 7c78c1e1377d4e4aeff79992e041cfe30037c06f Mon Sep 17 00:00:00 2001 From: bkushigian Date: Mon, 7 Oct 2024 16:58:29 -0700 Subject: [PATCH 35/40] Updated example to handle Result --- examples/node_locking.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/node_locking.rs b/examples/node_locking.rs index 143ecb9..f71c259 100644 --- a/examples/node_locking.rs +++ b/examples/node_locking.rs @@ -27,7 +27,7 @@ fn normal_node_locking() { // node locking must be performed after allocating memory and before solving game.play(1); // OOP all-in - game.lock_current_node(&[0.25, 0.75]); // lock IP's strategy: 25% fold, 75% call + let _ = game.lock_current_node(&[0.25, 0.75]); // lock IP's strategy: 25% fold, 75% call game.back_to_root(); solve(&mut game, 1000, 0.001, false); @@ -42,7 +42,7 @@ fn normal_node_locking() { game.allocate_memory(false); game.play(1); - game.lock_current_node(&[0.5, 0.5]); // lock IP's strategy: 50% fold, 50% call + let _ = game.lock_current_node(&[0.5, 0.5]); // lock IP's strategy: 50% fold, 50% call game.back_to_root(); solve(&mut game, 1000, 0.001, false); @@ -77,7 +77,7 @@ fn partial_node_locking() { game.allocate_memory(false); // lock OOP's strategy: only JJ is locked and the rest is not - game.lock_current_node(&[0.8, 0.0, 0.0, 0.2, 0.0, 0.0]); // JJ: 80% check, 20% all-in + let _ = game.lock_current_node(&[0.8, 0.0, 0.0, 0.2, 0.0, 0.0]); // JJ: 80% check, 20% all-in solve(&mut game, 1000, 0.001, false); game.cache_normalized_weights(); From e56352f544cb0e893065b4fb7c595cd5ce573c78 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Mon, 7 Oct 2024 17:00:13 -0700 Subject: [PATCH 36/40] Handle more uses of node_lock (added Result) --- src/file.rs | 1 - src/game/tests.rs | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/file.rs b/src/file.rs index bde9421..ccc0343 100644 --- a/src/file.rs +++ b/src/file.rs @@ -268,7 +268,6 @@ mod tests { use crate::action_tree::*; use crate::card::*; use crate::range::*; - use crate::solver::*; use crate::utility::*; #[test] diff --git a/src/game/tests.rs b/src/game/tests.rs index 527cd42..a76ba7c 100644 --- a/src/game/tests.rs +++ b/src/game/tests.rs @@ -869,7 +869,7 @@ fn node_locking() { game.allocate_memory(false); game.play(1); // all-in - game.lock_current_node(&[0.25, 0.75]); // 25% fold, 75% call + let _ = game.lock_current_node(&[0.25, 0.75]); // 25% fold, 75% call game.back_to_root(); solve(&mut game, 1000, 0.0, false); @@ -889,7 +889,7 @@ fn node_locking() { game.allocate_memory(false); game.play(1); // all-in - game.lock_current_node(&[0.5, 0.5]); // 50% fold, 50% call + let _ = game.lock_current_node(&[0.5, 0.5]); // 50% fold, 50% call game.back_to_root(); solve(&mut game, 1000, 0.0, false); @@ -929,7 +929,7 @@ fn node_locking_partial() { let mut game = PostFlopGame::with_config(card_config, action_tree).unwrap(); game.allocate_memory(false); - game.lock_current_node(&[0.8, 0.0, 0.0, 0.2, 0.0, 0.0]); // JJ -> 80% check, 20% all-in + let _ = game.lock_current_node(&[0.8, 0.0, 0.0, 0.2, 0.0, 0.0]); // JJ -> 80% check, 20% all-in solve(&mut game, 1000, 0.0, false); game.cache_normalized_weights(); @@ -970,7 +970,7 @@ fn node_locking_isomorphism() { game.allocate_memory(false); game.apply_history(&[0, 0, 15, 0, 0, 14]); // Turn: Spades, River: Hearts - game.lock_current_node(&[0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0]); // AhKh -> check + let _ = game.lock_current_node(&[0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0]); // AhKh -> check finalize(&mut game); From 07b1d85b82b2bf61fef3ef108654c817f3fe9f08 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Mon, 7 Oct 2024 17:00:30 -0700 Subject: [PATCH 37/40] CHANGES.md --- CHANGES.md | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 2a751bb..b3c3659 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,70 @@ # List of breaking changes +## v0.1.1 ++ **Resolving and Reloading** v0.1.1 introduces capabilities to reload and + resolve. Partial saves and reloading were already possible, but there were not + good mechanisms in place to rebuild and resolve forgotten streets. Updates + include: + + - **Updated Invariants for `PostFlopGame`**: + + Previously `PostFlopGame::state` stored if a game had been allocated or solved. + This update expands `State` to account for partial solve information loaded + from disk. In particular, `State::Solved` has been expanded to + + + `State::SolvedFlop` + + `State::SolvedTurn` + + `State::Solved` + + While `PostFlopGame::state` tracks the current solve status, + `PostFlopGame::storage_mode` tracks how much memory is allocated. After a + reload, `storage_mode` might be less than `BoardState::River`, meaning that + some memory has not been allocated. + + For instance, if we run a flop solve (so a full tree startingat the flop) + and save it as a flop save (save flop data, discarding turn and river data), + only the flop data will be written to disk. After reloading from disk, say + into variable `game`, the following will be true: + + 1. `game.storage_mode == BoardState::Flop`: This represents that only flop + memory is allocated (though not necessarily solved). + + + 2. `game.state == State::SolvedFlop`: This represents that flop data is + solved (but not turn/river). + + Allocating memory to store turn and river will update `storage_mode` to be + `BoardState::River`. Thus `storage_mode == BoardState::Flop` together with + `state == State::SolvedFlop` can be interpreted as "we've allocated the full + game tree but only flop nodes have real data. + + - **Removed requirements for game to not be solved**: There were a lot of + places that panicked if the game was already solved (e.g., trying to solve + again, or node locking, etc). This felt like an unrealistic burden: we might + want to nodelock a game after solving it, for instance, to compute some + other results. + + - **Added `reload_and_resolve_copy()`**: This function does the following: + 1. Takes an input `g: PostFlopGame` that may be paritally loaded. + 2. Creates a new game `ng` from `g`'s configuration + 3. Initializes nodes and allocates memory for `ng` + 4. Copies loaded data from `g` (i.e., if `g.state == State::SolvedTurn`, + then copy all flop and turn data) + 5. Locks copied nodes + 6. Solves `ng` + 7. Unlocks (restoring previous locking to whatever was passed in from `g`) + + - **Added `reload_and_resolve()`**: Similar to `reload_and_resolve_copy`, this + modifies the supplied game in place. This is currently implemented using + `reload_and_resolve_copy()`, and required memory for both the input game and + the rebuilt game. This process overwrites the input game, so that memory + will be released. + + - **Began replacing panics with Result<(), String>**: we should be able to + handle many instances of errors gracefully, so we've begun replacing + `panic!()`s with `Result<>`s + + ## 2023-10-01 - `BetSizeCandidates` and `DonkSizeCandidates` are renamed to `BetSizeOptions` and `DonkSizeOptions`, respectively. From 2dc57a76540567aa1f8ca30dc92dd6b45e9ecabf Mon Sep 17 00:00:00 2001 From: bkushigian Date: Mon, 7 Oct 2024 17:14:18 -0700 Subject: [PATCH 38/40] CHANGES.md --- CHANGES.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index b3c3659..44e476f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -60,10 +60,20 @@ the rebuilt game. This process overwrites the input game, so that memory will be released. - - **Began replacing panics with Result<(), String>**: we should be able to ++ **Replacing panics with Result<(), String>**: we should be able to handle many instances of errors gracefully, so we've begun replacing `panic!()`s with `Result<>`s ++ **Helper Functions**: We've added several helper functions, including + + - `PostFlopNode::action_index(action: Action) -> Option`: return the index into + this node of the specified action if it exists, and `None` otherwise. + + - `PostFlopNode::compute_history_recursive(&self, &PostFlopGame) -> Option>`: + Recursively compute the history of the given node as a path of action indices. + + - `PostFlopNode::actions() -> Vec`: compute the available actions of a given node + ## 2023-10-01 From e97400118ce8042c8569fd76f57d91eac3f7c003 Mon Sep 17 00:00:00 2001 From: bkushigian Date: Mon, 7 Oct 2024 17:14:55 -0700 Subject: [PATCH 39/40] Changed CHANGES.md heaer --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 44e476f..a1b8061 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,4 @@ -# List of breaking changes +# Changelog ## v0.1.1 + **Resolving and Reloading** v0.1.1 introduces capabilities to reload and From 62a0eee37247e908cad883ccedd361ae9eafd226 Mon Sep 17 00:00:00 2001 From: Jacob Van Geffen Date: Mon, 7 Oct 2024 21:14:11 -0500 Subject: [PATCH 40/40] Do/remove done TODOs --- src/game/interpreter.rs | 24 +----------------------- src/game/node.rs | 1 - src/game/serialization.rs | 4 +--- 3 files changed, 2 insertions(+), 27 deletions(-) diff --git a/src/game/interpreter.rs b/src/game/interpreter.rs index 7597aae..8bafa4a 100644 --- a/src/game/interpreter.rs +++ b/src/game/interpreter.rs @@ -814,10 +814,6 @@ impl PostFlopGame { /// /// **Time complexity:** *O*(#(actions) * #(private hands)). pub fn strategy(&self) -> Vec { - if self.state < State::MemoryAllocated { - panic!("Memory is not allocated"); - } - if self.is_terminal_node() { panic!("Terminal node is not allowed"); } @@ -827,27 +823,9 @@ impl PostFlopGame { } let node = self.node(); - let player = self.current_player(); - let num_actions = node.num_actions(); - let num_hands = self.num_private_hands(player); - - let mut ret = if self.is_compression_enabled { - normalized_strategy_compressed(node.strategy_compressed(), num_actions) - } else { - normalized_strategy(node.strategy(), num_actions) - }; - - let locking = self.locking_strategy(&node); - apply_locking_strategy(&mut ret, locking); - - ret.chunks_exact_mut(num_hands).for_each(|chunk| { - self.apply_swap(chunk, player, false); - }); - - ret + self.strategy_at_node(&node) } - // TODO use in .strategy() pub fn strategy_at_node(&self, node: &PostFlopNode) -> Vec { if self.state < State::MemoryAllocated { panic!("Memory is not allocated"); diff --git a/src/game/node.rs b/src/game/node.rs index 2e5fa27..c69e9cc 100644 --- a/src/game/node.rs +++ b/src/game/node.rs @@ -51,7 +51,6 @@ impl GameNode for PostFlopNode { #[inline] fn regrets(&self) -> &[f32] { - // TODO(Jacob): Something is causing either storage2 pointer to be off, or num_elements to be too big unsafe { slice::from_raw_parts(self.storage2 as *const f32, self.num_elements as usize) } } diff --git a/src/game/serialization.rs b/src/game/serialization.rs index b5c462a..ccc46cb 100644 --- a/src/game/serialization.rs +++ b/src/game/serialization.rs @@ -127,9 +127,7 @@ impl Encode for PostFlopGame { VERSION_STR.to_string().encode(encoder)?; // Update state based on target storage whenever the state is solved - // TODO(Jacob): May be better to add is_solved() function onto state - // That would prevent bugs caused by new states being added - let saved_state = if self.state >= State::SolvedFlop { + let saved_state = if self.is_partially_solved() { match self.target_storage_mode { BoardState::Flop => State::SolvedFlop, BoardState::Turn => State::SolvedTurn,