diff --git a/.github/workflows/push_branch.yml b/.github/workflows/push_branch.yml index 803609d..783fc7b 100644 --- a/.github/workflows/push_branch.yml +++ b/.github/workflows/push_branch.yml @@ -15,6 +15,8 @@ jobs: steps: - uses: actions/checkout@v3 - uses: actions-rs/toolchain@v1 + with: + toolchain: stable - name: Build run: cargo build --verbose @@ -24,6 +26,8 @@ jobs: steps: - uses: actions/checkout@v3 - uses: actions-rs/toolchain@v1 + with: + toolchain: stable - name: Test run: cargo test --verbose @@ -34,6 +38,8 @@ jobs: steps: - uses: actions/checkout@v3 - uses: actions-rs/toolchain@v1 + with: + toolchain: stable - name: Create Formatting Commit uses: mbrobbel/rustfmt-check@master with: diff --git a/.github/workflows/push_main.yml b/.github/workflows/push_main.yml index 2d2931b..dc50392 100644 --- a/.github/workflows/push_main.yml +++ b/.github/workflows/push_main.yml @@ -19,6 +19,8 @@ jobs: steps: - uses: actions/checkout@v3 - uses: actions-rs/toolchain@v1 + with: + toolchain: stable - name: Build run: cargo build --verbose @@ -28,6 +30,8 @@ jobs: steps: - uses: actions/checkout@v3 - uses: actions-rs/toolchain@v1 + with: + toolchain: stable - name: Test run: cargo test --verbose @@ -36,6 +40,8 @@ jobs: steps: - uses: actions/checkout@v3 - uses: actions-rs/toolchain@v1 + with: + toolchain: stable - name: Format Check run: cargo fmt --verbose -- --check diff --git a/Cargo.lock b/Cargo.lock index 6e9b86b..c5988a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,6 +86,18 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "bytemuck" version = "1.14.0" @@ -165,11 +177,18 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de853764b47027c2e862a995c34978ffa63c1501f2e15f987ba11bd4f9bba193" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "gamesman-nova" version = "0.1.5" dependencies = [ "anyhow", + "bitvec", "clap", "colored", "exitcode", @@ -331,6 +350,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rawpointer" version = "0.2.1" @@ -466,6 +491,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "typenum" version = "1.17.0" @@ -559,3 +590,12 @@ name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] diff --git a/Cargo.toml b/Cargo.toml index 9e4dc98..1c4563e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "gamesman-nova" version = "0.1.5" -description = "System for efficient search of extensive-form games." +description = "System facilitating the search of extensive-form games." authors = ["Max Fierro "] license = "GPL-3.0" repository = "https://github.com/GamesCrafters/GamesmanNova" @@ -28,4 +28,5 @@ exitcode = "^1" nalgebra = "^0" colored = "^2" anyhow = "^1" +bitvec = "^1" regex = "^1" diff --git a/src/database/engine/simple/mod.rs b/src/database/engine/simple/mod.rs deleted file mode 100644 index 368cce5..0000000 --- a/src/database/engine/simple/mod.rs +++ /dev/null @@ -1,114 +0,0 @@ -//! # Simple Database -//! -//! This module contains a very simple implementation of a persistent in-memory -//! key-value store. It works by indexing into an allocated vector through keys, -//! always making sure that it is large enough to house the record with the -//! highest key. This means that its top capacity is the amount of memory that -//! can be allocated by the operating system, without considering the usage of -//! virtual memory. -//! -//! For persistence, a file is created containing a bit-accurate representation -//! of the in-memory vector. Table logic is handled by switching which of these -//! files is currently being targeted, with the understanding that the contents -//! of memory are materialized every time there is a table switch. -//! -//! #### Authorship -//! -//! - Max Fierro, 4/14/2023 (maxfierro@berkeley.edu) - -use anyhow::Result; - -use std::fs::File; - -use crate::database::object::schema::Schema; -use crate::database::Persistence; -use crate::database::{KVStore, Tabular}; -use crate::model::State; - -/* CONSTANTS */ - -const METADATA_TABLE: &'static str = ".metadata"; - -/* DATABASE DEFINITION */ - -pub struct Database<'a> { - buffer: Vec, - table: Table<'a>, - mode: Persistence<'a>, -} - -struct Table<'a> { - dirty: bool, - width: u32, - name: &'a str, - size: u128, -} - -pub struct Parameters<'a> { - persistence: Persistence<'a>, -} - -/* IMPLEMENTATION */ - -impl Database<'_> { - fn initialize(params: Parameters) -> Result { - let mode = params.persistence; - let buffer = Vec::new(); - let table = Table { - dirty: false, - width: 0, - name: METADATA_TABLE, - size: 0, - }; - - if let Persistence::On(path) = params.persistence { - assert!(path.exists() && path.is_dir()); - let path = path.join(METADATA_TABLE); - let meta = if !path.is_file() { - let f = File::create(path).unwrap(); - initialize_metadata_table(f)?; - f - } else { - File::open(path).unwrap() - }; - } - - Ok(Database { - mode, - buffer, - table, - }) - } -} - -impl KVStore for Database<'_> { - fn put(&mut self, key: State, value: &[u8]) { - todo!() - } - - fn get(&self, key: State) -> Option<&[u8]> { - todo!() - } - - fn del(&self, key: State) { - todo!() - } -} - -impl Tabular for Database<'_> { - fn create_table(&self, id: &str, schema: Schema) -> Result<()> { - todo!() - } - - fn select_table(&self, id: &str) -> Result<()> { - todo!() - } - - fn delete_table(&self, id: &str) -> Result<()> { - todo!() - } -} - -fn initialize_metadata_table(file: File) -> Result<()> { - todo!() -} diff --git a/src/database/error.rs b/src/database/error.rs index ff7ee20..cd4b109 100644 --- a/src/database/error.rs +++ b/src/database/error.rs @@ -11,7 +11,7 @@ use std::{error::Error, fmt}; -use crate::database::object::schema::Datatype; +use crate::database::Datatype; /* ERROR WRAPPER */ @@ -21,7 +21,7 @@ use crate::database::object::schema::Datatype; /// of the variants of this wrapper include a field for a schema; this allows /// consumers to provide specific errors when deserializing persisted schemas. #[derive(Debug)] -pub enum DatabaseError<'a> { +pub enum DatabaseError { /// An error to indicate that there was an attempt to construct a schema /// containing two attributes with the same name. RepeatedAttribute { name: String, table: Option }, @@ -40,15 +40,15 @@ pub enum DatabaseError<'a> { InvalidSize { size: usize, name: String, - data: &'a Datatype<'a>, + data: Datatype, table: Option, }, } -impl Error for DatabaseError<'_> {} +impl Error for DatabaseError {} -impl fmt::Display for DatabaseError<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +impl fmt::Display for DatabaseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Self::RepeatedAttribute { name, table } => { if let Some(t) = table { @@ -106,12 +106,15 @@ impl fmt::Display for DatabaseError<'_> { table, } => { let rule = match data { - Datatype::CSTR => "divisible by 8 bits", Datatype::DPFP => "of exactly 64 bits", Datatype::SPFP => "of exactly 32 bits", Datatype::SINT => "greater than 1 bit", - Datatype::ENUM { map } => "of up to 8 bits", - Datatype::UINT => unreachable!("UINTs can be of any size."), + Datatype::CSTR => "divisible by 8 bits", + Datatype::UINT | Datatype::ENUM => { + unreachable!( + "UINTs and ENUMs can be of any nonzero size." + ) + }, }; let data = data.to_string(); if let Some(t) = table { diff --git a/src/database/engine/lsmt/mod.rs b/src/database/lsmt/mod.rs similarity index 100% rename from src/database/engine/lsmt/mod.rs rename to src/database/lsmt/mod.rs diff --git a/src/database/mod.rs b/src/database/mod.rs index 6b05e8d..9a66927 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -8,10 +8,10 @@ //! - Max Fierro, 4/14/2023 (maxfierro@berkeley.edu) use anyhow::Result; +use bitvec::prelude::{BitSlice, Msb0}; use std::path::Path; -use crate::database::object::schema::Schema; use crate::model::State; /* UTILITY MODULES */ @@ -21,17 +21,9 @@ mod util; /* IMPLEMENTATION MODULES */ -pub mod engine { - pub mod volatile; - pub mod simple; - pub mod lsmt; -} - -pub mod object { - pub mod record; - pub mod schema; - pub mod table; -} +pub mod volatile; +pub mod vector; +pub mod lsmt; /* DATABASE PARAMETERS */ @@ -46,9 +38,9 @@ pub enum Persistence<'a> { /// Represents the behavior of a Key-Value Store. No assumptions are made about /// the size of the records being used, but keys are taken to be fixed-length. -pub trait KVStore { - fn put(&mut self, key: State, value: &[u8]); - fn get(&self, key: State) -> Option<&[u8]>; +pub trait KVStore { + fn put(&mut self, key: State, record: &R); + fn get(&self, key: State) -> Option<&BitSlice>; fn del(&self, key: State); } @@ -75,3 +67,139 @@ pub trait Tabular { fn select_table(&self, id: &str) -> Result<()>; fn delete_table(&self, id: &str) -> Result<()>; } + +/// Allows a database implementation to read raw data from a record buffer. +pub trait Record { + fn raw(&self) -> &BitSlice; +} + +/* SCHEMA DEFINITIONS */ + +/// Represents a list of tuples including a name and a size (called attributes), +/// where each name is unique and the size is a number of bits. This is used to +/// "interpret" the raw data within records into meaningful features. +pub struct Schema { + attributes: Vec, + size: usize, +} + +/// Builder pattern intermediary for constructing a schema declaratively out of +/// provided attributes. This is here to help ensure schemas are not changed +/// accidentally after being instantiated. +pub struct SchemaBuilder { + attributes: Vec, + size: usize, +} + +/// Represents a triad of a name string, a size in bits corresponding to an +/// "attribute" or "feature" associated with a database record, and the type +/// of the data it represents. +#[derive(Clone)] +pub struct Attribute { + data: Datatype, + name: String, + size: usize, +} + +/// Specifies the type of data being stored within a record within a specific +/// contiguous subset of bits. This is used for interpretation. Here is the +/// meaning of each variant, with its possible sizes in bits: +/// - `ENUM`: Enumeration of arbitrary size. +/// - `UINT`: Unsigned integer of arbitrary size. +/// - `SINT`: Signed integer of size greater than 1. +/// - `SPFP`: Single-precision floating point per IEEE 754 of size exactly 32. +/// - `DPFP`: Double-precision floating point per IEEE 754 of size exactly 64. +/// - `CSTR`: C-style string (ASCII character array) of a size divisible by 8. +#[derive(Debug, Copy, Clone)] +pub enum Datatype { + ENUM, + UINT, + SINT, + SPFP, + DPFP, + CSTR, +} + +pub struct SchemaIterator<'a> { + schema: &'a Schema, + index: usize, +} + +impl Schema { + /// Returns the sum of the sizes of the schema's attributes. + pub fn size(&self) -> usize { + self.size + } + + /// Returns an iterator over the attributes in the schema. + pub fn iter(&self) -> SchemaIterator { + SchemaIterator { + schema: &self, + index: 0, + } + } +} + +impl Attribute { + /// Returns a new `Attribute` with `name` and `size`. + pub fn new(name: &str, data: Datatype, size: usize) -> Self { + Attribute { + data, + name: name.to_owned(), + size, + } + } + + /// Returns the name associated with the attribute. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns the size (in bits) of this attribute. + pub fn size(&self) -> usize { + self.size + } + + /// Returns the data type of this attribute. + pub fn datatype(&self) -> Datatype { + self.data + } +} + +impl SchemaBuilder { + /// Returns a new instance of a `SchemaBuilder`, which can be used to + /// declaratively construct a new record `Schema`. + pub fn new() -> Self { + SchemaBuilder { + attributes: Vec::new(), + size: 0, + } + } + + /// Associates `attr` to the schema under construction. Returns an error + /// if adding `attr` to the schema would result in an invalid state. + pub fn add(mut self, attr: Attribute) -> Result { + util::check_attribute_validity(&self.attributes, &attr)?; + self.size += attr.size(); + Ok(self) + } + + /// Constructs the schema using the current state of the `SchemaBuilder`. + pub fn build(self) -> Schema { + Schema { + attributes: self.attributes, + size: self.size, + } + } +} + +impl<'a> Iterator for SchemaIterator<'a> { + type Item = &'a Attribute; + + fn next(&mut self) -> Option { + self.index += 1; + self.schema + .attributes + .get(self.index - 1) + } +} diff --git a/src/database/object/record.rs b/src/database/object/record.rs deleted file mode 100644 index 460e015..0000000 --- a/src/database/object/record.rs +++ /dev/null @@ -1,200 +0,0 @@ -//! # Database Record Module -//! -//! This module defines the interface of a database record, and provides some -//! handy implementations for them. -//! -//! #### Authorship -//! -//! - Max Fierro, 11/4/2023 (maxfierro@berkeley.edu) - -use std::fmt::Display; - -use crate::database::object::schema::Schema; -use crate::database::object::schema::MAX_ENUM_NAME_SIZE; - -/* DEFINITION */ - -/// Provides common behavior to custom record types for serialization, -/// deserialization, and representation. -trait Record<'a> { - /// Returns a reference to the schema associated with this particular record - /// type. This provides a way of interpreting the return value of `data`. - fn schema(&self) -> &'a Schema<'a>; - - /// Returns a slice of bytes with the raw data contained by this instance. - /// This is given meaning by the return value of `schema`. - fn data(&self) -> &[u8]; -} - -/* IMPLEMENTATIONS */ - -impl Display for dyn Record<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut curr = 0; - let data = self.data(); - let schema = self.schema(); - for attribute in schema.iter() { - let dt = attribute.datatype(); - let size = attribute.size(); - let name = attribute.name(); - match dt { - super::schema::Datatype::ENUM { map } => { - write!( - f, - "{}:\t{}\n", - name, - parse_enum(&data[curr..], map) - ) - }, - super::schema::Datatype::SPFP => { - write!( - f, - "{}:\t{}\n", - name, - parse_f32(&data[curr..], size) - ) - }, - super::schema::Datatype::DPFP => { - write!( - f, - "{}:\t{}\n", - name, - parse_f64(&data[curr..], size) - ) - }, - super::schema::Datatype::UINT => { - write!( - f, - "{}:\t{}\n", - name, - parse_unsigned(&data[curr..], size) - ) - }, - super::schema::Datatype::SINT => { - write!( - f, - "{}:\t{}\n", - name, - parse_signed(&data[curr..], size) - ) - }, - super::schema::Datatype::CSTR => { - write!( - f, - "{}:\t{}\n", - name, - parse_string(&data[curr..], size) - ) - }, - }; - curr += size as usize; - } - Ok(()) - } -} - -/* RAW DATA PARSING */ - -const fn parse_unsigned(data: &[u8], size: usize) -> u128 { - if size > 128 { - panic!("Only integers of up to 128 bits are supported."); - } - if size > data.len() { - panic!("Attempted to parse undersized record buffer."); - } - - let mut curr = 0; - let mut result: u128 = 0; - while curr < size { - let index: usize = curr / 8; - if (size - curr) >= 8 { - let byte: u128 = data[index] as u128; - result <<= 8; - result |= byte; - curr += 8; - } else { - let remaining = size - curr; - let byte: u128 = data[index] as u128; - result <<= remaining; - result |= byte >> (8 - remaining); - curr += remaining; - } - } - result -} - -const fn parse_signed(data: &[u8], size: usize) -> i128 { - let unsigned: u128 = parse_unsigned(data, size); - let zeros = unsigned.leading_zeros(); - let sign = ((unsigned << zeros) >> 127) & 1; - let body = (unsigned << (zeros + 1)) >> (zeros + 1); - let result: i128 = ((sign << 127) | body) as i128; - result -} - -const fn parse_string(data: &[u8], size: usize) -> String { - if size > data.len() { - panic!("Attempted to parse undersized record buffer."); - } - if size % 8 != 0 { - panic!("Attempted to parse partial character into string."); - } - - String::from_utf8(Vec::from(&data[..(size / 8)])).unwrap() -} - -const fn parse_f32(data: &[u8], size: usize) -> f32 { - if size > data.len() { - panic!("Attempted to parse undersized record buffer."); - } - if size != 32 { - panic!( - "Attempted to parse single-precision float from {} bits.", - size - ); - } - - let i = 0; - let mut result: u32 = 0; - while i < 4 { - result <<= 8; - result |= data[i] as u32; - } - result as f32 -} - -const fn parse_f64(data: &[u8], size: usize) -> f64 { - if size > data.len() { - panic!("Attempted to parse undersized record buffer."); - } - if size != 64 { - panic!( - "Attempted to parse double-precision float from {} bits.", - size - ); - } - - let i = 0; - let mut result: u64 = 0; - while i < 8 { - result <<= 8; - result |= data[i] as u64; - } - result as f64 -} - -const fn parse_enum( - data: &[u8], - map: &[(u8, [u8; MAX_ENUM_NAME_SIZE]); u8::MAX as usize], -) -> String { - if data.len() < 1 { - panic!("Not enough bits in data to parse enumeration."); - } - - let entry = map.iter().find(|x| x.0 == data[0]); - if let Some((_, bytes)) = entry { - String::from_utf8(Vec::from(bytes)).unwrap() - } else { - String::from("Undefined Variant") - } -} diff --git a/src/database/object/schema.rs b/src/database/object/schema.rs deleted file mode 100644 index 3a1b319..0000000 --- a/src/database/object/schema.rs +++ /dev/null @@ -1,156 +0,0 @@ -//! # Database Schema Module -//! -//! This module provisions database schematics for fixed-width records, allowing -//! the translation of raw data stored as contiguous sequences of bits to and -//! from meaningful attributes. -//! -//! #### Authorship -//! -//! - Max Fierro, 2/24/2024 (maxfierro@berkeley.edu) - -use anyhow::Result; - -use crate::database::util; - -/* CONSTANTS */ - -/// The maximum size (in ASCII characters or bytes) of an enumeration variant -/// name corresponding to the data type of an attribute in a schema. -pub const MAX_ENUM_NAME_SIZE: usize = 15; - -/* PUBLIC DEFINITIONS */ - -/// Represents a list of tuples including a name and a size (called attributes), -/// where each name is unique and the size is a number of bits. This is used to -/// "interpret" the raw data within records into meaningful features. -pub struct Schema<'a> { - attributes: Vec>, - size: usize, -} - -/// Builder pattern intermediary for constructing a schema declaratively out of -/// provided attributes. This is here to help ensure schemas are not changed -/// accidentally after being instantiated. -pub struct SchemaBuilder<'a> { - attributes: Vec>, - size: usize, -} - -/// Represents a triad of a name string, a size in bits corresponding to an -/// "attribute" or "feature" associated with a database record, and the type -/// of the data it represents. -pub struct Attribute<'a> { - data: Datatype<'a>, - name: String, - size: usize, -} - -/// Specifies the type of data being stored within a record within a specific -/// contiguous subset of bits. This is used for interpretation. Here is the -/// meaning of each variant, with its possible sizes in bits: -/// - `ENUM`: Enumeration with size up to 8. -/// - `UINT`: Unsigned integer of arbitrary size. -/// - `SINT`: Signed integer of size greater than 1. -/// - `SPFP`: Single-precision floating point per IEEE 754 of size exactly 32. -/// - `DPFP`: Double-precision floating point per IEEE 754 of size exactly 64. -/// - `CSTR`: C-style string (ASCII character array) of a size divisible by 8. -#[derive(Debug)] -pub enum Datatype<'a> { - ENUM { - map: &'a [(u8, [u8; MAX_ENUM_NAME_SIZE]); u8::MAX as usize], - }, - UINT, - SINT, - SPFP, - DPFP, - CSTR, -} - -/* PRIVATE DEFINITIONS */ - -struct SchemaIterator<'a> { - schema: &'a Schema<'a>, - index: usize, -} - -/* PUBLIC INTERFACES */ - -impl Schema<'_> { - /// Returns the sum of the sizes of the schema's attributes. - pub fn size(&self) -> usize { - self.size - } - - /// Returns an iterator over the attributes in the schema. - pub fn iter(&self) -> SchemaIterator { - SchemaIterator { - schema: &self, - index: 0, - } - } -} - -impl<'a> Attribute<'_> { - /// Returns a new `Attribute` with `name` and `size`. - pub fn new(name: &str, data: Datatype, size: usize) -> Self { - Attribute { - data, - name: name.to_owned(), - size, - } - } - - /// Returns the name associated with the attribute. - pub fn name(&self) -> &str { - &self.name - } - - /// Returns the size (in bits) of this attribute. - pub fn size(&self) -> usize { - self.size - } - - /// Returns the data type of this attribute. - pub fn datatype(&self) -> &'a Datatype<'a> { - &self.data - } -} - -impl SchemaBuilder<'_> { - /// Returns a new instance of a `SchemaBuilder`, which can be used to - /// declaratively construct a new record `Schema`. - pub fn new() -> Self { - SchemaBuilder { - attributes: Vec::new(), - size: 0, - } - } - - /// Associates `attr` to the schema under construction. Returns an error - /// if adding `attr` to the schema would result in an invalid state. - pub fn add(mut self, attr: Attribute) -> Result { - util::check_attribute_validity(&self.attributes, &attr)?; - self.size += attr.size(); - self.attributes.push(attr); - Ok(self) - } - - /// Constructs the schema using the current state of the `SchemaBuilder`. - pub fn build(self) -> Schema<'static> { - Schema { - attributes: self.attributes, - size: self.size, - } - } -} - -impl<'a> Iterator for SchemaIterator<'a> { - type Item = &'a Attribute<'a>; - - fn next(&mut self) -> Option { - self.index += 1; - self.schema - .attributes - .get(self.index - 1) - } -} diff --git a/src/database/object/table.rs b/src/database/object/table.rs deleted file mode 100644 index 844d4bb..0000000 --- a/src/database/object/table.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! # Database Table Module -//! -//! This module provides the implementation of a database table that can be used -//! to encapsulate related data under a single record schema. This is generic; -//! it does not assume anything about what kind of attributes are stored within -//! table records. -//! -//! #### Authorship -//! -//! - Max Fierro, 2/24/2024 (maxfierro@berkeley.edu) diff --git a/src/database/util.rs b/src/database/util.rs index c853254..f66e410 100644 --- a/src/database/util.rs +++ b/src/database/util.rs @@ -12,16 +12,16 @@ use anyhow::Result; use crate::database::error::DatabaseError; -use crate::database::object::schema::Attribute; -use crate::database::object::schema::Datatype; +use crate::database::Attribute; +use crate::database::Datatype; /// Verifies that adding a `new` attribute to an `existing` set of attributes /// would not result in an invalid state for the schema who owns `existing`, /// and that the added attribute does not break any datatype sizing rules. -pub fn check_attribute_validity<'a>( +pub fn check_attribute_validity( existing: &Vec, new: &Attribute, -) -> Result<(), DatabaseError<'a>> { +) -> Result<(), DatabaseError> { if new.name().is_empty() { Err(DatabaseError::UnnamedAttribute { table: None }) } else if new.size() == 0 { @@ -40,17 +40,16 @@ pub fn check_attribute_validity<'a>( } } -fn check_datatype_validity<'a>( - new: &Attribute, -) -> Result<(), DatabaseError<'a>> { +fn check_datatype_validity(new: &Attribute) -> Result<(), DatabaseError> { let s = new.size(); if match new.datatype() { - Datatype::CSTR => s % 8 != 0, - Datatype::DPFP => s != 64, - Datatype::SPFP => s != 32, Datatype::SINT => s < 2, - Datatype::ENUM { map } => s > 8, - Datatype::UINT => unreachable!("UINTs can have any size."), + Datatype::SPFP => s != 32, + Datatype::DPFP => s != 64, + Datatype::CSTR => s % 8 != 0, + Datatype::UINT | Datatype::ENUM => { + unreachable!("UINTs and ENUMs can be of any nonzero size.") + }, } { Err(DatabaseError::InvalidSize { size: new.size(), @@ -65,15 +64,15 @@ fn check_datatype_validity<'a>( /* UTILITY IMPLEMENTATIONS */ -impl ToString for Datatype<'_> { +impl ToString for Datatype { fn to_string(&self) -> String { match self { Datatype::DPFP => "Double-Precision Floating Point".to_string(), Datatype::SPFP => "Single-Precision Floating Point".to_string(), Datatype::CSTR => "C-Style ASCII String".to_string(), - Datatype::ENUM { map } => "Enumeration".to_string(), Datatype::UINT => "Unsigned Integer".to_string(), Datatype::SINT => "Signed Integer".to_string(), + Datatype::ENUM => "Enumeration".to_string(), } } } diff --git a/src/database/vector/mod.rs b/src/database/vector/mod.rs new file mode 100644 index 0000000..05bf93b --- /dev/null +++ b/src/database/vector/mod.rs @@ -0,0 +1,85 @@ +//! # Vector Database +//! +//! This module contains a very simple implementation of a persistent key-value +//! store. It works by indexing into an allocated vector through keys, always +//! making sure that it is large enough to house the record with the highest +//! key. This means that its top capacity is the amount of memory that can be +//! allocated by the operating system, without considering the usage of virtual +//! memory. +//! +//! For persistence, a file is created containing a bit-accurate representation +//! of the in-memory vector. Table logic is handled by switching which of these +//! files is currently being targeted, with the understanding that the contents +//! of memory may be materialized on arbitrary operations. +//! +//! #### Authorship +//! +//! - Max Fierro, 4/14/2023 (maxfierro@berkeley.edu) + +use anyhow::Result; +use bitvec::order::Msb0; +use bitvec::slice::BitSlice; + +use crate::database::Persistence; +use crate::database::Schema; +use crate::database::{KVStore, Record, Tabular}; +use crate::model::State; + +/* CONSTANTS */ + +const METADATA_TABLE: &'static str = ".metadata"; + +/* DATABASE DEFINITION */ + +pub struct Database<'a> { + buffer: Vec, + table: Table<'a>, + mode: Persistence<'a>, +} + +struct Table<'a> { + dirty: bool, + width: u32, + name: &'a str, + size: u128, +} + +pub struct Parameters<'a> { + persistence: Persistence<'a>, +} + +/* IMPLEMENTATION */ + +impl Database<'_> { + fn initialize(params: Parameters) -> Result { + todo!() + } +} + +impl KVStore for Database<'_> { + fn put(&mut self, key: State, value: &R) { + todo!() + } + + fn get(&self, key: State) -> Option<&BitSlice> { + todo!() + } + + fn del(&self, key: State) { + todo!() + } +} + +impl Tabular for Database<'_> { + fn create_table(&self, id: &str, schema: Schema) -> Result<()> { + todo!() + } + + fn select_table(&self, id: &str) -> Result<()> { + todo!() + } + + fn delete_table(&self, id: &str) -> Result<()> { + todo!() + } +} diff --git a/src/database/engine/volatile/mod.rs b/src/database/volatile/mod.rs similarity index 76% rename from src/database/engine/volatile/mod.rs rename to src/database/volatile/mod.rs index 09b4f25..818a740 100644 --- a/src/database/engine/volatile/mod.rs +++ b/src/database/volatile/mod.rs @@ -8,11 +8,12 @@ //! - Max Fierro, 2/24/2024 (maxfierro@berkeley.edu) use anyhow::Result; +use bitvec::{order::Msb0, slice::BitSlice, store::BitStore}; use std::collections::HashMap; use crate::{ - database::{object::schema::Schema, KVStore, Tabular}, + database::{KVStore, Record, Schema, Tabular}, model::State, }; @@ -28,12 +29,12 @@ impl Database<'_> { } } -impl KVStore for Database<'_> { - fn put(&mut self, key: State, value: &[u8]) { +impl KVStore for Database<'_> { + fn put(&mut self, key: State, value: &R) { todo!() } - fn get(&self, key: State) -> Option<&[u8]> { + fn get(&self, key: State) -> Option<&BitSlice> { todo!() } diff --git a/src/game/crossteaser/mod.rs b/src/game/crossteaser/mod.rs new file mode 100644 index 0000000..0b99fa2 --- /dev/null +++ b/src/game/crossteaser/mod.rs @@ -0,0 +1,162 @@ +//! # Crossteaser Game Module +//! +//! The following definition is from this great website [1]: Crossteaser is a +//! puzzle which consists of a transparent frame enclosing 8 identical coloured +//! pieces arranged as if in a 3 by 3 grid. These pieces are three dimensional +//! crosses, each with six perpendicular arms of different colors. The frame +//! has three vertical slots at the front and three horizontal slots at the back +//! through which the front and back arms of the pieces stick. There is one +//! space in the 3 by 3 grid, and any adjacent piece can be moved into the +//! vacant space, but the slots in the frame force a piece to roll over as it +//! moves. By rolling the pieces around they get mixed up. The aim is to arrange +//! them so that the space is in the middle and that the crosses all have +//! matching orientation. +//! +//! [1]: https://www.jaapsch.net/puzzles/crosstsr.htm +//! +//! #### Authorship +//! +//! - Max Fierro, 11/5/2023 (maxfierro@berkeley.edu) +//! - Cindy Xu, 11/28/2023 + +use anyhow::{Context, Result}; + +use crate::game::Acyclic; +use crate::game::Bounded; +use crate::game::DTransition; +use crate::game::Game; +use crate::game::GameData; +use crate::game::Legible; +use crate::game::Solvable; +use crate::implement; +use crate::interface::IOMode; +use crate::interface::SolutionMode; +use crate::model::State; +use crate::model::Utility; +use variants::*; + +/* SUBMODULES */ + +mod states; +mod variants; + +/* GAME DATA */ + +const NAME: &str = "crossteaser"; +const AUTHORS: &str = "Max Fierro "; +const CATEGORY: &str = "Single-player puzzle"; +const ABOUT: &str = "PLACEHOLDER"; + +/* GAME IMPLEMENTATION */ + +/// Encodes the state of a piece in the game board. For reference, a cube has +/// six faces (up, down, etc.), and a cube with face A on top can be oriented +/// in one of four ways (north, south, etc.). +enum Face { + Up(Orientation), + Down(Orientation), + Left(Orientation), + Right(Orientation), + Front(Orientation), + Back(Orientation), + None, +} + +/// Encodes the orientation information about each piece in the game. Since each +/// piece is cube-like, it is not enough to just have a face, since a cube with +/// its "Front" face up could still be oriented in one of four ways. +enum Orientation { + North, + East, + South, + West, +} + +/// Represents an instance of a Crossteaser game session, which is specific to +/// a valid variant of the game. +pub struct Session { + variant: Option, + length: u64, + width: u64, + free: u64, +} + +impl Game for Session { + fn initialize(variant: Option) -> Result { + if let Some(v) = variant { + parse_variant(v).context("Malformed game variant.") + } else { + Ok(parse_variant(VARIANT_DEFAULT.to_owned()).unwrap()) + } + } + + fn id(&self) -> String { + if let Some(variant) = self.variant.clone() { + format!("{}.{}", NAME, variant) + } else { + NAME.to_owned() + } + } + + fn info(&self) -> GameData { + todo!() + } + + fn solve(&self, mode: IOMode, method: SolutionMode) -> Result<()> { + todo!() + } + + fn forward(&mut self, history: Vec) -> Result<()> { + todo!() + } +} + +/* TRAVERSAL DECLARATIONS */ + +impl Bounded for Session { + fn start(&self) -> State { + todo!() + } + + fn end(&self, state: State) -> bool { + todo!() + } +} + +impl DTransition for Session { + fn prograde(&self, state: State) -> Vec { + todo!() + } + + fn retrograde(&self, state: State) -> Vec { + todo!() + } +} + +/* SUPPLEMENTAL DECLARATIONS */ + +impl Legible for Session { + fn decode(&self, string: String) -> Result { + todo!() + } + + fn encode(&self, state: State) -> String { + todo!() + } +} + +/* SOLVING DECLARATIONS */ + +implement! { for Session => + Acyclic<1> +} + +impl Solvable<1> for Session { + fn utility(&self, state: State) -> [Utility; 1] { + todo!() + } + + fn turn(&self, state: crate::model::State) -> crate::model::Turn { + todo!() + } +} diff --git a/src/game/crossteaser/states.rs b/src/game/crossteaser/states.rs new file mode 100644 index 0000000..ea1de9b --- /dev/null +++ b/src/game/crossteaser/states.rs @@ -0,0 +1,9 @@ +//! # Zero-By State Handling Module +//! +//! This module helps parse the a string encoding of a crossteaser game state +//! into a more efficient binary representation, performing a series of checks +//! which partially ensure compatibility with a game variant. +//! +//! #### Authorship +//! +//! - Max Fierro, 3/7/2023 (maxfierro@berkeley.edu) diff --git a/src/game/crossteaser/variants.rs b/src/game/crossteaser/variants.rs new file mode 100644 index 0000000..6044016 --- /dev/null +++ b/src/game/crossteaser/variants.rs @@ -0,0 +1,172 @@ +//! # Crossteaser Variant Handling Module +//! +//! This module helps parse the `Variant` string provided to the Crossteaser +//! game into parameters that can help build a game session. +//! +//! #### Authorship +//! +//! - Max Fierro, 11/5/2023 (maxfierro@berkeley.edu) +//! - Atharva Gupta, 11/28/2023 +//! - Cindy Xu, 11/28/2023 + +use regex::Regex; + +use crate::game::crossteaser::{Session, NAME}; +use crate::game::error::GameError; + +/* CROSSTEASER VARIANT DEFINITION */ + +pub const VARIANT_DEFAULT: &str = "3x3-1"; +pub const VARIANT_PATTERN: &str = r"^\d+x\d+\-\d+$"; +pub const VARIANT_PROTOCOL: &str = "The variant string allows users to define \ +any size of the puzzle and the number of free slots. The string should follow \ +the format LxW-F, with L representing the length and W representing the width \ +of the puzzle, and F representing the number of free slots, all positive \ +integers. Note that L and W must be greater than 1, or it would be possible \ +for the resulting variant to not be solvable."; + +/* API */ + +/// Returns a crossteaser session set up using the parameters specified by +/// `variant`. Returns a `GameError::VariantMalformed` if the variant string +/// does not conform to the variant protocol specified, which should contain +/// useful information about why it was not parsed/accepted. +pub fn parse_variant(variant: String) -> Result { + check_variant_pattern(&variant)?; + let params = parse_parameters(&variant)?; + check_param_count(¶ms)?; + check_params_are_positive(¶ms)?; + Ok(Session { + variant: Some(variant), + length: params[0], + width: params[1], + free: params[2], + }) +} + +/* VARIANT STRING VERIFICATION */ + +fn check_variant_pattern(variant: &String) -> Result<(), GameError> { + let re = Regex::new(VARIANT_PATTERN).unwrap(); + if !re.is_match(&variant) { + Err(GameError::VariantMalformed { + game_name: NAME, + hint: format!( + "String does not match the pattern '{}'.", + VARIANT_PATTERN + ), + }) + } else { + Ok(()) + } +} + +fn parse_parameters(variant: &str) -> Result, GameError> { + variant + .split(['x', '-']) + .map(|int_string| { + int_string + .parse::() + .map_err(|e| GameError::VariantMalformed { + game_name: NAME, + hint: format!("{}", e.to_string()), + }) + }) + .collect() +} + +fn check_param_count(params: &Vec) -> Result<(), GameError> { + if params.len() != 3 { + Err(GameError::VariantMalformed { + game_name: NAME, + hint: "String needs to have exactly 3 dash-separated integers." + .to_owned(), + }) + } else { + Ok(()) + } +} + +fn check_params_are_positive(params: &Vec) -> Result<(), GameError> { + if params.iter().any(|&x| x <= 0) { + Err(GameError::VariantMalformed { + game_name: NAME, + hint: "All integers in the string must be positive.".to_owned(), + }) + } else if params + .iter() + .take(2) + .any(|&x| x <= 1) + { + Err(GameError::VariantMalformed { + game_name: NAME, + hint: "L and W must both be strictly greater than 1.".to_owned(), + }) + } else { + Ok(()) + } +} + +/* TESTS */ + +#[cfg(test)] +mod test { + + use super::*; + use crate::game::Game; + + #[test] + fn variant_pattern_is_valid_regex() { + assert!(Regex::new(VARIANT_PATTERN).is_ok()); + } + + #[test] + fn default_variant_matches_variant_pattern() { + let re = Regex::new(VARIANT_PATTERN).unwrap(); + assert!(re.is_match(VARIANT_DEFAULT)); + } + + #[test] + fn initialization_success_with_no_variant() { + let with_none = Session::initialize(None); + let with_default = + Session::initialize(Some(VARIANT_DEFAULT.to_owned())); + + assert!(with_none.is_ok()); + assert!(with_default.is_ok()); + } + + #[test] + fn invalid_variants_fail_checks() { + let some_variant_1 = Session::initialize(Some("None".to_owned())); + let some_variant_2 = Session::initialize(Some("x4-".to_owned())); + let some_variant_3 = Session::initialize(Some("-".to_owned())); + let some_variant_4 = Session::initialize(Some("1x2-5".to_owned())); + let some_variant_5 = Session::initialize(Some("0x2-5".to_owned())); + let some_variant_6 = Session::initialize(Some("1x1-1".to_owned())); + let some_variant_7 = Session::initialize(Some("8x2.6-5".to_owned())); + let some_variant_8 = Session::initialize(Some("3x4-0".to_owned())); + + assert!(some_variant_1.is_err()); + assert!(some_variant_2.is_err()); + assert!(some_variant_3.is_err()); + assert!(some_variant_4.is_err()); + assert!(some_variant_5.is_err()); + assert!(some_variant_6.is_err()); + assert!(some_variant_7.is_err()); + assert!(some_variant_8.is_err()); + } + + #[test] + fn valid_variants_pass_checks() { + let some_variant_1 = Session::initialize(Some("4x3-2".to_owned())); + let some_variant_2 = Session::initialize(Some("5x4-2".to_owned())); + let some_variant_3 = Session::initialize(Some("2x4-1".to_owned())); + let some_variant_4 = Session::initialize(Some("4x2-1".to_owned())); + + assert!(some_variant_1.is_ok()); + assert!(some_variant_2.is_ok()); + assert!(some_variant_3.is_ok()); + assert!(some_variant_4.is_ok()); + } +} diff --git a/src/game/mod.rs b/src/game/mod.rs index 049a116..48bd6ba 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -12,7 +12,7 @@ //! - Max Fierro, 4/6/2023 (maxfierro@berkeley.edu) use anyhow::Result; -use nalgebra::{SMatrix, SVector}; +use nalgebra::SMatrix; use crate::{ interface::{IOMode, SolutionMode}, @@ -27,6 +27,7 @@ mod util; /* IMPLEMENTED GAMES */ pub mod zero_by; +pub mod crossteaser; /* DATA CONSTRUCTS */ @@ -321,12 +322,19 @@ where /// the state is not terminal, it is recommended that this function panics /// with a message indicating that an attempt was made to calculate the /// utility of a non-primitive state. - fn utility(&self, state: State) -> SVector; + fn utility(&self, state: State) -> [Utility; N]; /// Returns the player `i` whose turn it is at the given `state`. The player /// identifier `i` should never be greater than `N - 1`, where `N` is the /// number of players in the underlying game. fn turn(&self, state: State) -> Turn; + + /// Returns the number of players in the underlying game. This should be at + /// least one higher than the maximum value returned by `turn`. + #[inline(always)] + fn players(&self) -> PlayerCount { + N + } } /// Indicates that the directed graph _G_ induced by the structure of the diff --git a/src/game/zero_by/mod.rs b/src/game/zero_by/mod.rs index f4f4fd9..2fc6990 100644 --- a/src/game/zero_by/mod.rs +++ b/src/game/zero_by/mod.rs @@ -14,7 +14,6 @@ //! - Max Fierro, 4/6/2023 (maxfierro@berkeley.edu) use anyhow::{Context, Result}; -use nalgebra::SVector; use states::*; use crate::game::error::GameError; @@ -28,6 +27,8 @@ use crate::model::Utility; use crate::model::{State, Turn}; use crate::solver::strong; +use super::util::unpack_turn; + /* SUBMODULES */ mod states; @@ -191,12 +192,11 @@ implement! { for Session => } impl Solvable<2> for Session { - fn utility(&self, state: State) -> SVector { - let (_, turn) = util::unpack_turn(state, 2); - let mut result = SVector::::zeros(); - result.fill(-1); - result[turn] = 1; - result + fn utility(&self, state: State) -> [Utility; 2] { + let (_, turn) = unpack_turn(state, 2); + let mut payoffs = [-1; 2]; + payoffs[turn] = 1; + payoffs } fn turn(&self, state: State) -> Turn { @@ -205,12 +205,11 @@ impl Solvable<2> for Session { } impl Solvable<10> for Session { - fn utility(&self, state: State) -> SVector { - let (_, turn) = util::unpack_turn(state, 10); - let mut result = SVector::::zeros(); - result.fill(-1); - result[turn] = 9; - result + fn utility(&self, state: State) -> [Utility; 10] { + let (_, turn) = unpack_turn(state, 10); + let mut payoffs = [-1; 10]; + payoffs[turn] = 9; + payoffs } fn turn(&self, state: State) -> Turn { diff --git a/src/game/zero_by/states.rs b/src/game/zero_by/states.rs index 7ab72cc..06af900 100644 --- a/src/game/zero_by/states.rs +++ b/src/game/zero_by/states.rs @@ -10,12 +10,13 @@ use regex::Regex; -use crate::game::zero_by::{Session, NAME}; -use crate::{ - game::error::GameError, - game::util::{pack_turn, unpack_turn}, - model::{State, Turn}, -}; +use crate::game::error::GameError; +use crate::game::util::pack_turn; +use crate::game::util::unpack_turn; +use crate::game::zero_by::Session; +use crate::game::zero_by::NAME; +use crate::model::State; +use crate::model::Turn; /* ZERO-BY STATE ENCODING */ diff --git a/src/solver/stochastic/acyclic.rs b/src/solver/stochastic/acyclic.rs index e69de29..8b13789 100644 --- a/src/solver/stochastic/acyclic.rs +++ b/src/solver/stochastic/acyclic.rs @@ -0,0 +1 @@ + diff --git a/src/solver/stochastic/cyclic.rs b/src/solver/stochastic/cyclic.rs index e69de29..8b13789 100644 --- a/src/solver/stochastic/cyclic.rs +++ b/src/solver/stochastic/cyclic.rs @@ -0,0 +1 @@ + diff --git a/src/solver/strong/acyclic.rs b/src/solver/strong/acyclic.rs index 8c3f120..756cecc 100644 --- a/src/solver/strong/acyclic.rs +++ b/src/solver/strong/acyclic.rs @@ -1,23 +1,32 @@ //! # Strong Acyclic Solving Module //! -//! This module implements strong acyclic solvers for all applicable types of -//! games through blanket implementations of the `acyclic::Solver` trait, -//! optimizing for specific game characteristics wherever possible. +//! This module implements strong acyclic solving routines. //! //! #### Authorship //! //! - Max Fierro, 12/3/2023 (maxfierro@berkeley.edu) use anyhow::{Context, Result}; +use bitvec::prelude::*; -use crate::database::engine::volatile; -use crate::database::object::schema::{Attribute, Datatype, SchemaBuilder}; +use crate::database::{volatile, Record}; +use crate::database::{Attribute, Datatype, SchemaBuilder}; use crate::database::{KVStore, Tabular}; use crate::game::{Acyclic, Bounded, DTransition, STransition, Solvable}; use crate::interface::IOMode; -use crate::model::{PlayerCount, State}; -use crate::schema; -use crate::solver::MAX_TRANSITIONS; +use crate::model::{PlayerCount, Remoteness, State, Turn, Utility}; +use crate::solver::{util, MAX_TRANSITIONS}; + +/* CONSTANTS */ + +/// The exact number of bits that are used to encode remoteness. +const REMOTENESS_SIZE: usize = 16; + +/// The maximum number of bits that can be used to encode a record. +const BUFFER_SIZE: usize = 128; + +/// The exact number of bits that are used to encode utility for one player. +const UTILITY_SIZE: usize = 8; /* SOLVERS */ @@ -25,10 +34,10 @@ pub fn dynamic_solver(game: &G, mode: IOMode) -> Result<()> where G: Acyclic + DTransition + Bounded + Solvable, { - let mut db = volatile_database(&game.id()) - .context("Failed to initialize database.")?; - - dynamic_backward_induction(&mut db, game); + let mut db = volatile_database(game) + .context("Failed to initialize volatile database.")?; + dynamic_backward_induction(&mut db, game) + .context("Failed solving algorithm execution.")?; Ok(()) } @@ -39,31 +48,81 @@ where + Bounded + Solvable, { - let mut db = volatile_database(&game.id()) - .context("Failed to initialize database.")?; - - static_backward_induction(&mut db, game); + let mut db = volatile_database(game) + .context("Failed to initialize volatile database.")?; + static_backward_induction(&mut db, game) + .context("Failed solving algorithm execution.")?; Ok(()) } +/* DATABASE INITIALIZATION */ + +/// Initializes a volatile database, creating a table schema according to the +/// solver record layout, initializing a table with that schema, and switching +/// to that table before returning the database handle. +fn volatile_database(game: &G) -> Result +where + G: Solvable, +{ + let db = volatile::Database::initialize(); + let mut schema = SchemaBuilder::new(); + + for i in 0..game.players() { + let name = &format!("P{} utility", i); + let data = Datatype::SINT; + let size = UTILITY_SIZE; + schema = schema + .add(Attribute::new(name, data, size)) + .context("Failed to add utility attribute to database schema.")?; + } + + let name = "State remoteness"; + let data = Datatype::UINT; + let size = REMOTENESS_SIZE; + schema = schema + .add(Attribute::new(name, data, size)) + .context("Failed to add remoteness attribute to database schema.")?; + + let id = game.id(); + let schema = schema.build(); + db.create_table(&id, schema) + .context("Failed to create database table for solution set.")?; + db.select_table(&id) + .context("Failed to select solution set database table.")?; + + Ok(db) +} + /* SOLVING ALGORITHMS */ -fn dynamic_backward_induction(db: &mut D, game: &G) +/// Performs an iterative depth-first traversal of the game tree, assigning to +/// each game `state` a remoteness and utility values for each player within +/// `db`. This uses heap-allocated memory for keeping a stack of positions to +/// facilitate DFS, as well as for communicating state transitions. +fn dynamic_backward_induction( + db: &mut D, + game: &G, +) -> Result<()> where - G: Acyclic + Bounded + DTransition + Solvable, - D: KVStore, + D: KVStore, + G: Acyclic + DTransition + Bounded + Solvable, { let mut stack = Vec::new(); stack.push(game.start()); while let Some(curr) = stack.pop() { let children = game.prograde(curr); - if let None = db.get(curr) { - db.put(curr, Record::default()); - if children.is_empty() { - let record = Record::default() - .with_utility(game.utility(curr)) - .with_remoteness(0); - db.put(curr, record) + let mut buf = RecordBuffer::new(game.players()) + .context("Failed to create placeholder record.")?; + if db.get(curr).is_none() { + db.put(curr, &buf); + if game.end(curr) { + buf = RecordBuffer::new(game.players()) + .context("Failed to create record for end state.")?; + buf.set_utility(game.utility(curr)) + .context("Failed to copy utility values to record.")?; + buf.set_remoteness(0) + .context("Failed to set remoteness for end state.")?; + db.put(curr, &buf); } else { stack.push(curr); stack.extend( @@ -73,40 +132,62 @@ where ); } } else { - db.put( - curr, - children - .iter() - .map(|&x| db.get(x).unwrap()) - .max_by(|r1, r2| r1.cmp(&r2, game.turn(curr))) - .unwrap(), - ); + let mut optimal = buf; + let mut max_val = Utility::MIN; + let mut min_rem = Remoteness::MAX; + for state in children { + let buf = RecordBuffer::from(db.get(state).unwrap()) + .context("Failed to create record for middle state.")?; + let val = buf + .get_utility(game.turn(state)) + .context("Failed to get utility from record.")?; + let rem = buf.get_remoteness(); + if val > max_val || (val == max_val && rem < min_rem) { + max_val = val; + min_rem = rem; + optimal = buf; + } + } + optimal + .set_remoteness(min_rem + 1) + .context("Failed to set remoteness for solved record.")?; + db.put(curr, &optimal); } } + Ok(()) } -fn static_backward_induction(db: &mut D, game: &G) +/// Performs an iterative depth-first traversal of the `game` tree, assigning to +/// each `game` state a remoteness and utility values for each player within +/// `db`. This uses heap-allocated memory for keeping a stack of positions to +/// facilitate DFS, and stack memory for communicating state transitions. +fn static_backward_induction( + db: &mut D, + game: &G, +) -> Result<()> where + D: KVStore, G: Acyclic + STransition + Bounded + Solvable, - D: KVStore, { let mut stack = Vec::new(); stack.push(game.start()); while let Some(curr) = stack.pop() { let children = game.prograde(curr); - if let None = db.get(curr) { - db.put(curr, Record::default()); - if children - .iter() - .all(|x| x.is_none()) - { - let record = Record::default() - .with_utility(game.utility(curr)) - .with_remoteness(0); - db.put(curr, record) + let mut buf = RecordBuffer::new(game.players()) + .context("Failed to create placeholder record.")?; + if db.get(curr).is_none() { + db.put(curr, &buf); + if game.end(curr) { + buf = RecordBuffer::new(game.players()) + .context("Failed to create record for end state.")?; + buf.set_utility(game.utility(curr)) + .context("Failed to copy utility values to record.")?; + buf.set_remoteness(0) + .context("Failed to set remoteness for end state.")?; + db.put(curr, &buf); } else { stack.push(curr); stack.extend( @@ -117,31 +198,192 @@ where ); } } else { - db.put( - curr, - children - .iter() - .filter_map(|&x| x) - .map(|x| db.get(x).unwrap()) - .max_by(|r1, r2| r1.cmp(&r2, game.turn(curr))) - .unwrap(), - ); + let mut cur = 0; + let mut optimal = buf; + let mut max_val = Utility::MIN; + let mut min_rem = Remoteness::MAX; + while cur < MAX_TRANSITIONS { + cur += 1; + if let Some(state) = children[cur] { + let buf = RecordBuffer::from(db.get(state).unwrap()) + .context("Failed to create record for middle state.")?; + let val = buf + .get_utility(game.turn(state)) + .context("Failed to get utility from record.")?; + let rem = buf.get_remoteness(); + if val > max_val || (val == max_val && rem < min_rem) { + max_val = val; + min_rem = rem; + optimal = buf; + } + } + } + optimal + .set_remoteness(min_rem + 1) + .context("Failed to set remoteness for solved record.")?; + db.put(curr, &optimal); } } + Ok(()) } -/* HELPERS */ +/* RECORD IMPLEMENTATION */ -fn volatile_database(table: &str) -> Result { - let schema = schema! { - "Player 1 Utility"; Datatype::SINT; 8, - "Player 2 Utility"; Datatype::SINT; 8, - "Total Remoteness"; Datatype::UINT; 8 - }; +/// Solver-specific record entry, meant to communicate the remoteness and each +/// player's utility at a corresponding game state. The layout is as follows: +/// +/// ```none +/// [UTILITY_SIZE bits: P0 utility] +/// ... +/// [UTILITY_SIZE bits: P(N-1) utility] +/// [REMOTENESS_SIZE bits: Remoteness] +/// [0b0 until BUFFER_SIZE] +/// ``` +/// +/// The number of players `N` is limited by `BUFFER_SIZE`, because a statically +/// sized buffer is used for intermediary storage. The utility and remoteness +/// values are encoded in big-endian, with utility being a signed two's +/// complement integer and remoteness an unsigned integer. +struct RecordBuffer { + buf: BitArr!(for BUFFER_SIZE, in u8, Msb0), + players: PlayerCount, +} - let mut db = volatile::Database::initialize(); - db.create_table(table, schema); - db.select_table(table); +impl Record for RecordBuffer { + #[inline(always)] + fn raw(&self) -> &BitSlice { + &self.buf[..Self::bit_size(self.players)] + } +} - Ok(db) +impl RecordBuffer { + /// Returns a new instance of a bit-packed record buffer that is able to + /// store utility values for `players`. Fails if `players` is too high for + /// the underlying buffer's capacity. + #[inline(always)] + fn new(players: PlayerCount) -> Result { + if Self::bit_size(players) > BUFFER_SIZE { + todo!() + } else { + Ok(Self { + buf: bitarr!(u8, Msb0; 0; BUFFER_SIZE), + players, + }) + } + } + + /// Return a new instance with `bits` as the underlying buffer. Fails in the + /// event that the size of `bits` is incoherent with the record. + #[inline(always)] + fn from(bits: &BitSlice) -> Result { + let len = bits.len(); + if len > BUFFER_SIZE { + todo!() + } else if len < Self::minimum_bit_size() { + todo!() + } else { + let players = Self::player_count(len); + let mut buf = bitarr!(u8, Msb0; 0; BUFFER_SIZE); + buf[..len].copy_from_bitslice(bits); + Ok(Self { players, buf }) + } + } + + /* GET METHODS */ + + /// Parse and return the utility value corresponding to `player`. Fails if + /// the `player` index passed in is incoherent with player count. + #[inline(always)] + fn get_utility(&self, player: Turn) -> Result { + if player >= self.players { + todo!() + } else { + let start = Self::utility_index(self.players); + let end = start + UTILITY_SIZE; + Ok(self.buf[start..end].load_be::()) + } + } + + /// Parse and return the remoteness value in the record encoding. Failure + /// here indicates corrupted state. + #[inline(always)] + fn get_remoteness(&self) -> Remoteness { + let start = Self::remoteness_index(self.players); + let end = start + REMOTENESS_SIZE; + self.buf[start..end].load_be::() + } + + /* SET METHODS */ + + /// Set this entry to have the utility values in `v` for each player. Fails + /// if any of the utility values are too high to fit in the space dedicated + /// for each player's utility, or if there is a mismatch between player + /// count and the number of utility values passed in. + #[inline(always)] + fn set_utility(&mut self, v: [Utility; N]) -> Result<()> { + if N >= self.players { + todo!() + } else { + let player = 0; + while player < self.players { + let utility = v[player]; + if util::min_sbits(utility) > UTILITY_SIZE { + todo!() + } + + let start = Self::utility_index(player); + let end = start + UTILITY_SIZE; + self.buf[start..end].store_be(utility); + } + Ok(()) + } + } + + /// Set this entry to have `value` remoteness. Fails if `value` is too high + /// to fit in the space dedicated for remoteness within the record. + #[inline(always)] + fn set_remoteness(&mut self, value: Remoteness) -> Result<()> { + if util::min_ubits(value) > REMOTENESS_SIZE { + todo!() + } else { + let start = Self::remoteness_index(self.players); + let end = start + REMOTENESS_SIZE; + self.buf[start..end].store_be(value); + Ok(()) + } + } + + /* LAYOUT HELPER METHODS */ + + /// Return the number of bits that would be needed to store a record + /// containing utility information for `players` as well as remoteness. + #[inline(always)] + const fn bit_size(players: usize) -> usize { + players * UTILITY_SIZE + REMOTENESS_SIZE + } + + /// Return the minimum number of bits needed for a valid record buffer. + #[inline(always)] + const fn minimum_bit_size() -> usize { + REMOTENESS_SIZE + } + + /// Return the bit index of the remoteness entry start in the record buffer. + #[inline(always)] + const fn remoteness_index(players: usize) -> usize { + players * UTILITY_SIZE + } + + /// Return the bit index of the 'i'th player's utility entry start. + #[inline(always)] + const fn utility_index(player: Turn) -> usize { + player * UTILITY_SIZE + } + + /// Return the maximum number of utility entries supported by a dense record + /// (one that maximizes bit usage) with `length`. Ignores unused bits. + #[inline(always)] + const fn player_count(length: usize) -> usize { + length - REMOTENESS_SIZE / UTILITY_SIZE + } } diff --git a/src/solver/util.rs b/src/solver/util.rs index 898af5d..4e5b537 100644 --- a/src/solver/util.rs +++ b/src/solver/util.rs @@ -6,3 +6,29 @@ //! #### Authorship //! //! - Max Fierro, 2/24/2024 (maxfierro@berkeley.edu) + +use crate::model::Utility; + +/* BIT FIELDS */ + +/// Returns the minimum number of bits required to represent unsigned `val`. +#[inline(always)] +pub const fn min_ubits(val: u64) -> usize { + let mut x = 0; + while (val >> x != 0b1) && (x != u64::BITS) { + x += 1; + } + (u64::BITS - x) as usize +} + +/// Return the minimum number of bits necessary to encode `utility`. +#[inline(always)] +pub const fn min_sbits(utility: Utility) -> usize { + let mut res = 0; + let mut inter = utility.abs(); + while inter != 0 { + inter >>= 1; + res += 1; + } + res + 1 +} diff --git a/src/util.rs b/src/util.rs index 75859c4..4adc62c 100644 --- a/src/util.rs +++ b/src/util.rs @@ -33,11 +33,16 @@ pub enum GameModule { pub fn find_game( game: GameModule, variant: Option, - state: Option, + from: Option, ) -> Result> { match game { GameModule::ZeroBy => { - Ok(Box::new(zero_by::Session::initialize(variant)?)) + let session = zero_by::Session::initialize(variant) + .context("Failed to initialize zero-by game session.")?; + if let Some(path) = from { + todo!() + } + Ok(Box::new(session)) }, } }