Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v0.1.1 #13

Open
wants to merge 88 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
f92d23a
Docs
bkushigian Aug 4, 2024
1d366f6
Docs
bkushigian Aug 4, 2024
b2c12b8
Added helper function
bkushigian Aug 4, 2024
8b7067e
DESIGN.md
bkushigian Aug 4, 2024
0236e13
Created debug example
bkushigian Aug 4, 2024
f9497a6
DESIGN.md
bkushigian Aug 4, 2024
7aa6668
Updates to examples for debugging/documenting
bkushigian Aug 4, 2024
334315b
Intermediate stuff on DESIGN.md
bkushigian Aug 5, 2024
52878a1
Merge branch 'docs' of https://github.com/bkushigian/postflop-solver …
bkushigian Aug 6, 2024
478a72e
DESIGN.md
bkushigian Aug 6, 2024
25d5ee6
docs and rename function for readability
bkushigian Aug 6, 2024
f05a179
rename local var for clarity
bkushigian Aug 6, 2024
729693c
Rename for readability
bkushigian Aug 6, 2024
8d191bb
DESIGN.md
bkushigian Aug 9, 2024
798c4aa
Linter issues
bkushigian Aug 9, 2024
cd790eb
DESIGN.md
bkushigian Aug 9, 2024
6377ad3
DESIGN.md
bkushigian Aug 9, 2024
0cd0382
DESIGN.md
bkushigian Aug 9, 2024
c03fb57
Updates
bkushigian Aug 9, 2024
694c61b
tmp commit
bkushigian Aug 14, 2024
c2c52f5
Recursively compute history
bkushigian Aug 14, 2024
a23e319
Tmp commit
bkushigian Aug 17, 2024
414361a
Documented some sliceops
bkushigian Aug 18, 2024
dd81318
Documented sliceops
bkushigian Aug 18, 2024
87ac61e
Fixed docs in sliceop
bkushigian Aug 18, 2024
3c8056d
Docstrings for sliceops
bkushigian Aug 18, 2024
3b7606b
Docs and rename
bkushigian Aug 18, 2024
af35578
Docs
bkushigian Aug 18, 2024
d879d6a
Tmp: splitting branches
bkushigian Aug 19, 2024
c74cad1
beginning branch refactor
bkushigian Aug 19, 2024
81764c7
Branch Refactor: added file_io_debug.rs
bkushigian Aug 19, 2024
2de6719
Branch refactor: removed solve_with_node_as_root
bkushigian Aug 19, 2024
8824791
Branch refactor
bkushigian Aug 19, 2024
edeb03e
Branch refactor
bkushigian Aug 19, 2024
9821469
Strategies by private hand, plus transpose in sliceops
bkushigian Aug 19, 2024
1989fb6
Refactored/removed unused file_io_debug.rs
bkushigian Aug 19, 2024
0ae3531
Tmp Commit
bkushigian Aug 19, 2024
e0b0db7
Tmp commit
bkushigian Aug 19, 2024
deb338c
tmp commit
bkushigian Aug 19, 2024
c2459fa
tmp commit
bkushigian Aug 19, 2024
3227822
Refactoring test
bkushigian Aug 19, 2024
4159f13
Refactor: moved test into branch
bkushigian Aug 19, 2024
d0aaae1
Branch refactor continued
bkushigian Aug 19, 2024
1e0067b
Refactor continue
bkushigian Aug 19, 2024
3dad754
Removed println
bkushigian Aug 19, 2024
f6b0861
Documenting solve_recursive
bkushigian Aug 19, 2024
aa10c4c
Added documentation, renamed variables for clarity
bkushigian Aug 20, 2024
18f6d60
Fixed merge conflict
bkushigian Aug 20, 2024
a5dc463
Fixed thing that was missed in previous merge
bkushigian Aug 20, 2024
9c4a310
Tmp commit
bkushigian Aug 21, 2024
f30f70a
Removed warnings from example
bkushigian Aug 21, 2024
b5c3807
tmp commit from pair programming to fix flop and turn save-and-resolve
JacobVanGeffen Aug 22, 2024
963ed40
tmp commit for debugging
bkushigian Aug 24, 2024
59cf5f2
Cherry pick updated State, fixed some breakages
JacobVanGeffen Aug 22, 2024
818f556
Tidied imports
bkushigian Aug 27, 2024
70bde78
tmp commit
bkushigian Aug 29, 2024
9cd7ed5
tmp commit; resolve uses normalized strategy for locking
JacobVanGeffen Aug 30, 2024
01c5adb
Fixed warnings
bkushigian Aug 30, 2024
47600b9
Only lock flop and turn nodes
JacobVanGeffen Sep 26, 2024
3fbfa82
hacky_reload_and_resolve working
bkushigian Sep 29, 2024
fdda64c
Updated file_io example
bkushigian Sep 29, 2024
ab8a785
Updated file_io
bkushigian Oct 2, 2024
58365a9
renamed hacky_reload_and_resolve, implemented reload_and_resolve
bkushigian Oct 2, 2024
22e9395
Removed temporary node locks used for resolve, added docs, tests
bkushigian Oct 3, 2024
e33912c
Merge branch 'main' into bkushigian/issue2_workaround
bkushigian Oct 7, 2024
202fbd0
Merge branch 'main' into docs
bkushigian Oct 7, 2024
f76ef3d
return &self.state; --> &self.state
bkushigian Oct 7, 2024
d9dc996
Merge branch 'docs' into bkushigian/issue2_workaround
bkushigian Oct 7, 2024
af91af2
Removed unnecessary borrow
bkushigian Oct 7, 2024
2e87e66
Ticked up version: v0.1.0 -> v0.1.1
bkushigian Oct 7, 2024
f2668fb
Merge and fixed several bugs, failing tests
bkushigian Oct 7, 2024
29e67ea
Fix bug in conditional compilation rename
bkushigian Oct 7, 2024
0d9f5a7
Fixed some example comments and names
bkushigian Oct 7, 2024
68083a8
Removed print debug statements
bkushigian Oct 7, 2024
f61782d
Updated examples/file_io.rs for clarity
bkushigian Oct 7, 2024
a0251a3
Merge pull request #4 from bkushigian/docs
bkushigian Oct 7, 2024
924e3a4
Merge pull request #5 from bkushigian/bkushigian/issue2_workaround
bkushigian Oct 7, 2024
1038f63
Checked out file_io.rs from main, and moved updated version to file_i…
bkushigian Oct 7, 2024
d84bb46
Small refactor, comment for clarity
bkushigian Oct 7, 2024
a3b84e4
Resolved comments in review
bkushigian Oct 7, 2024
7c78c1e
Updated example to handle Result
bkushigian Oct 7, 2024
e56352f
Handle more uses of node_lock (added Result)
bkushigian Oct 8, 2024
07b1d85
CHANGES.md
bkushigian Oct 8, 2024
2dc57a7
CHANGES.md
bkushigian Oct 8, 2024
e974001
Changed CHANGES.md heaer
bkushigian Oct 8, 2024
62a0eee
Do/remove done TODOs
JacobVanGeffen Oct 8, 2024
3fb52a2
Updated readme
bkushigian Oct 26, 2024
ca00f7c
Merge pull request #11 from bkushigian/bkushigian/issue2
bkushigian Nov 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "postflop-solver"
version = "0.1.0"
version = "0.1.1"
authors = ["Wataru Inariba", "Ben Kushigian"]
edition = "2021"
description = "An open-source postflop solver for Texas hold'em poker"
Expand Down
200 changes: 200 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
# Design

This document is a description, as far as I understand it, of the inner design
of the solver and PostFlopGame. This is a working document for me to get my
bearings.

## PostFlopGame

### Build/Allocate/Initialize

To set up a `PostFlopGame` we need to **create a `PostFlopGame` instance**,
**allocate global storage and `PostFlopNode`s**, and **initialize the
`PostFlopNode` child/parent relationship**. This is done in several steps.

We begin by creating a `PostFlopGame` instance.

```rust
let mut game = PostFlopGame::with_config(card_config, action_tree).unwrap();
```

A `PostFlopGame` requires an
`ActionTree` which describes all possible actions and lines (no runout
information), and a `CardConfig`, which describes player ranges and
flop/turn/river data.

Once we have created a `PostFlopGame` instance we need to allocate the following
memory and initialize its values:

+ `game.node_arena`
+ `game.storage1`
+ `game.storage2`
+ `game.storage_ip`
+ `game.storage_chance`

These fields are not allocated/initialized at the same time:

+ `game.node_arena` is allocated and initialized via `with_config()` (i.e., when
we created our `PostFlopGame`),
+ other storage is allocated via `game.allocate_memory()`.

#### Allocating and Initializing `node_arena`

We constructed a `PostFlopGame` by calling
`PostFlopGame::with_config(card_config, action_tree)`, which under the hood
actually calls:

```rust
let mut game = Self::new();
game.update_config(card_config, action_tree)?;
```

`PostFlopGame::update_config` sets up configuration data, sanity checks things
are correct, and then calls `self.init_root()`.

`init_root` is responsible for:

1. Counting number of `PostFlopNode`s to be allocated (`self.nodes_per_street`),
broken up by flop, turn, and river
2. Allocating `PostFlopNode`s in the `node_arena` field
3. Clearing storage: `self.clear_storage()` sets each storage item to a new
`Vec`
4. Invoking `build_tree_recursive` which initializes each node's child/parent
relationship via `child_offset` (through calls to `push_actions` and
`push_chances`).

Each `PostFlopNode` points to node-specific data (e.g., strategies and
cfregrets) that is located inside of `PostFlopGame.storage*` fields (which is
currently unallocated) via similarly named fields `PostFlopNode.storage*`.

Additionally, each node points to the children offset with `children_offset`,
which records where in `node_arena` relative to the current node that node's
children begin. We allocate this memory via:

```rust
game.allocate_memory(false); // pass `true` to use compressed memory
```

This allocates the following memory:

+ `self.storage1`
+ `self.storage2`
+ `self.storage3`
+ `self.storage_chance`

Next, `allocate_memory()` calls `allocate_memory_nodes(&mut self)`, which
iterates through each node in `node_arena` and sets storage pointers.

After `allocate_memory` returns we still need to set `child_offset`s.

### Storage

There are several fields marked as `// global storage` in `game::mod::PostFlopGame`:

```rust
// global storage
// `storage*` are used as a global storage and are referenced by `PostFlopNode::storage*`.
// Methods like `PostFlopNode::strategy` define how the storage is used.
node_arena: Vec<MutexLike<PostFlopNode>>,
storage1: Vec<u8>,
storage2: Vec<u8>,
storage_ip: Vec<u8>,
storage_chance: Vec<u8>,
locking_strategy: BTreeMap<usize, Vec<f32>>,
```

These are referenced from `PostFlopNode`:

```rust
storage1: *mut u8, // strategy
storage2: *mut u8, // regrets or cfvalues
storage3: *mut u8, // IP cfvalues
```

+ `storage1` seems to store the strategy
+ `storage2` seems to store regrets/cfvalues, and
+ `storage3` stores IP's cf values (does that make `storage2` store OOP's cfvalues?)

Storage is a byte vector `Vec<u8>`, and these store floating point values.

> [!IMPORTANT]
> Why are these stored as `Vec<u8>`s? Is this for swapping between
> `f16` and `f32`s?

Some storage is allocated in `game::base::allocate_memory`:

```rust
let storage_bytes = (num_bytes * self.num_storage) as usize;
let storage_ip_bytes = (num_bytes * self.num_storage_ip) as usize;
let storage_chance_bytes = (num_bytes * self.num_storage_chance) as usize;

self.storage1 = vec![0; storage_bytes];
self.storage2 = vec![0; storage_bytes];
self.storage_ip = vec![0; storage_ip_bytes];
self.storage_chance = vec![0; storage_chance_bytes];
```

`node_arena` is allocated in `game::base::init_root()`:

```rust
let num_nodes = self.count_nodes_per_street();
let total_num_nodes = num_nodes[0] + num_nodes[1] + num_nodes[2];

if total_num_nodes > u32::MAX as u64
|| mem::size_of::<PostFlopNode>() as u64 * total_num_nodes > isize::MAX as u64
{
return Err("Too many nodes".to_string());
}

self.num_nodes = num_nodes;
self.node_arena = (0..total_num_nodes)
.map(|_| MutexLike::new(PostFlopNode::default()))
.collect::<Vec<_>>();
self.clear_storage();
```

`locking_strategy` maps node indexes (`PostFlopGame::node_index`) to a locked
strategy. `locking_strategy` is initialized to an empty `BTreeMap<usize,
Vec<f32>>` by deriving Default. It is inserted into via
`PostFlopGame::lock_current_strategy`

### Serialization/Deserialization

Serialization relies on the `bincode` library's `Encode` and `Decode`. We can set
the `target_storage_mode` to allow for a non-full save. For instance,

```rust
game.set_target_storage_mode(BoardState::Turn);
```

will ensure that when `game` is encoded, it will only save Flop and Turn data.
When a serialized tree is deserialized, if it is a partial save (e.g., a Turn
save) you will not be able to navigate to unsaved streets.

Several things break when we deserialize a partial save:

+ `node_arena` is only partially populated
+ `node.children()` points to raw data when `node` points to an street that is
not serialized (e.g., a chance node before the river for a Turn save).

### Allocating `node_arena`

We want to first allocate nodes for `node_arena`, and then run some form of
`build_tree_recursive`. This assumes that `node_arena` is already allocated, and
recursively visits children of nodes and modifies them to

### Data Coupling/Relations/Invariants

+ A node is locked IFF it is contained in the game's locking_strategy
+ `PostFlopGame.node_arena` is pointed to by `PostFlopNode.children_offset`. For
instance, this is the basic definition of the `PostFlopNode.children()`
function:

```rust
slice::from_raw_parts(
self_ptr.add(self.children_offset as usize),
self.num_children as usize,
)
```

We get a pointer to `self` and add children offset.
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ An open-source postflop solver library written in Rust
Documentation: https://b-inary.github.io/postflop_solver/postflop_solver/

**Related repositories**
- Web app (WASM Postflop): https://github.com/b-inary/wasm-postflop
- Desktop app (Desktop Postflop): https://github.com/b-inary/desktop-postflop

- Desktop app (Desktop Postflop): https://github.com/bkushigian/desktop-postflop (supported)
- Web app (WASM Postflop): https://github.com/b-inary/wasm-postflop (not supported)

**Note:**
The primary purpose of this library is to serve as a backend engine for the GUI applications ([WASM Postflop] and [Desktop Postflop]).
Expand All @@ -25,7 +26,7 @@ Therefore, breaking changes are often made without version changes.
See [CHANGES.md](CHANGES.md) for details about breaking changes.

[WASM Postflop]: https://github.com/b-inary/wasm-postflop
[Desktop Postflop]: https://github.com/b-inary/desktop-postflop
[Desktop Postflop]: https://github.com/bkushigian/desktop-postflop

## Usage

Expand All @@ -43,7 +44,7 @@ You can find examples in the [examples](examples) directory.
If you have cloned this repository, you can run the example with the following command:

```sh
$ cargo run --release --example basic
cargo run --release --example basic
```

## Implementation details
Expand Down
Loading
Loading