Skip to content

Commit

Permalink
Merge pull request #11 from bkushigian/bkushigian/issue2
Browse files Browse the repository at this point in the history
Issue2: reload and resolve
  • Loading branch information
bkushigian authored Nov 26, 2024
2 parents 3fb52a2 + 62a0eee commit ca00f7c
Show file tree
Hide file tree
Showing 11 changed files with 674 additions and 84 deletions.
77 changes: 76 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,79 @@
# List of breaking changes
# Changelog

## 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.

+ **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<usize>`: return the index into
this node of the specified action if it exists, and `None` otherwise.

- `PostFlopNode::compute_history_recursive(&self, &PostFlopGame) -> Option<Vec<usize>>`:
Recursively compute the history of the given node as a path of action indices.

- `PostFlopNode::actions() -> Vec<Action>`: compute the available actions of a given node


## 2023-10-01

Expand Down
147 changes: 147 additions & 0 deletions examples/file_io_big.rs
Original file line number Diff line number Diff line change
@@ -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();
}
6 changes: 3 additions & 3 deletions examples/node_locking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
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);
Expand All @@ -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
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);
Expand Down Expand Up @@ -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
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();
Expand Down
Loading

0 comments on commit ca00f7c

Please sign in to comment.