From f85ca92036e98d92340c640a771db04afb801bb6 Mon Sep 17 00:00:00 2001 From: Max Fierro Date: Thu, 18 Apr 2024 14:40:45 -0700 Subject: [PATCH 01/15] Updated docs --- doc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc b/doc index 2f08bac..e5a27ec 160000 --- a/doc +++ b/doc @@ -1 +1 @@ -Subproject commit 2f08bacec3d6a62018c5b730efda7021377a1832 +Subproject commit e5a27ec42b584950b5f58887b9a533972c613820 From eb432dc2587bb4288cf951e4be0c3b298f2ff7c2 Mon Sep 17 00:00:00 2001 From: Max Fierro Date: Mon, 22 Apr 2024 17:09:47 -0700 Subject: [PATCH 02/15] Restructured database traits --- src/database/mod.rs | 126 +++++++++++++++++++------ src/database/vector/mod.rs | 85 ++++++++++------- src/database/volatile/mod.rs | 58 ++++++++---- src/model.rs | 16 +++- src/solver/algorithm/strong/acyclic.rs | 19 ++-- 5 files changed, 214 insertions(+), 90 deletions(-) diff --git a/src/database/mod.rs b/src/database/mod.rs index a4d291b..052a5b3 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -1,17 +1,17 @@ -//! # Database Module [WIP] +//! # Database Module //! //! This module contains memory and I/O mechanisms used to store and fetch -//! solution set data, hopefully in an efficient and scalable way. +//! analysis data, hopefully in an efficient and scalable way. //! //! #### Authorship //! - Max Fierro, 4/14/2023 (maxfierro@berkeley.edu) use anyhow::Result; -use bitvec::prelude::{BitSlice, Msb0}; -use std::path::Path; +use std::path::{Path, PathBuf}; -use crate::{model::State, solver::RecordType}; +use crate::model::{Key, RawRecord, TableID}; +use crate::solver::RecordType; /* RE-EXPORTS */ @@ -34,8 +34,8 @@ pub mod lsmt; /// Indicates whether the database implementation should store the data it is /// managing to disk, or keep it entirely in memory. -pub enum Persistence<'a> { - On(&'a Path), +pub enum Persistence { + On(PathBuf), Off, } @@ -79,41 +79,105 @@ pub enum Datatype { CSTR, } -/* INTERFACE DEFINITIONS */ +/* DATABASE INTERFACES */ -/// 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. +/// Represents the behavior of a Key-Value Store generic over a [`Record`] type. pub trait KVStore { - fn put(&mut self, key: State, record: &R); - fn get(&self, key: State) -> Option<&BitSlice>; - fn del(&mut self, key: State); + /// Replaces the value associated with `key` with the bits of `record`, + /// creating one if it does not already exist. Fails if under any violation + /// of implementation-specific assumptions of record size or contents. + fn put(&mut self, key: Key, record: &R) -> Result<()> + where + R: Record; + + /// Returns the bits associated with the value of `key`, or `None` if there + /// is no such association. Infallible due to all possible values of `key` + /// being considered valid (but not necessarily existent). + fn get(&self, key: Key) -> Option<&RawRecord>; + + /// Removes the association of `key` to whatever value it is currently bound + /// to, or does nothing if there is no such value. + fn delete(&mut self, key: Key); } /// Allows a database to be evicted to persistent media. Implementing this trait /// requires custom handling of what happens when the database is closed; if it -/// has data on memory, then it should persist any dirty pages to ensure -/// consistency. In terms of file structure, each implementation decides how to -/// organize its persistent content. The only overarching requisite is that it -/// be provided an existing directory's path. -pub trait Persistent { - fn bind_path(&self, path: &Path) -> Result<()>; - fn materialize(&self) -> Result<()>; +/// has data on memory, then it should persist dirty data to ensure consistency +/// via [`Drop`]. Database file structure is implementation-specific. +pub trait Persistent<'a, T> +where + Self: Tabular<'a, T> + Drop, + T: Table, +{ + /// Interprets the contents of a directory at `path` to be the contents of + /// a persistent database. Fails if the contents of `path` are unexpected. + fn from(path: &Path) -> Result + where + Self: Sized; + + /// Binds the contents of the database to a particular `path` for the sake + /// of persistence. It is undefined behavior to forego calling this function + /// before pushing data to the underlying database. Fails if the database is + /// already bound to another path, or if `path` is non-empty, or under any + /// I/O failure. + fn bind(&self, path: &Path) -> Result<()>; + + /// Evict the contents of `table` to disk in a batch operation, potentially + /// leaving cache space for other table's usage. Calling this on all tables + /// in a database should be equivalent to dropping the database reference. + fn flush(&self, table: &mut T) -> Result<()>; } -/// Allows for grouping data into collections of fixed-length records called -/// tables. Because of this application's requirements, this does not mean that -/// a database should be optimized for inter-table operations. In fact, this -/// interface's semantics are such that its implementations optimize performance -/// for cases of sequential operations on a single table. -pub trait Tabular { - fn create_table(&self, id: &str, schema: Schema) -> Result<()>; - fn select_table(&self, id: &str) -> Result<()>; - fn delete_table(&self, id: &str) -> Result<()>; +/// Allows for grouping data into [`Table`] implementations, which contain many +/// fixed-length records that share attributes under a single [`Schema`]. This +/// allows consumers of this implementation to have simultaneous references to +/// different mutable tables. +pub trait Tabular<'a, T> +where + T: Table, +{ + /// Creates a new table with `id` and `schema`. Fails if another table with + /// the same `id` already exists, or under any I/O failure. + fn create_table(&self, id: &TableID, schema: Schema) -> Result<&mut T>; + + /// Obtains a mutable reference to the [`Table`] with `id`. Fails if no such + /// table exists in the underlying database, or under any I/O failure. + fn select_table(&self, id: &TableID) -> Result<&mut T>; + + /// Forgets about the association of `id` to any existing table, doing + /// nothing if there is no such table. Fails under any I/O failure. + fn delete_table(&self, table: &mut T) -> Result<()>; } -/// Allows a database implementation to read raw data from a record buffer. +/* TABLE INTERFACE */ + +/// A grouping of fixed-length records which share a table [`Schema`] that can +/// be used as a handle to mutate them via [`KVStore`] semantics, in addition +/// to keeping track of useful metadata. +pub trait Table +where + Self: KVStore, +{ + /// Returns a reference to the schema associated with `self`. + fn schema(&self) -> &Schema; + + /// Returns the number of records currently contained in `self`. + fn count(&self) -> u64; + + /// Returns the total number of bytes being used to store the contents of + /// `self`, excluding metadata (both in memory and persistent media). + fn size(&self) -> u64; + + /// Returns the identifier associated with `self`. + fn id(&self) -> &TableID; +} + +/* RECORD INTERFACE */ + +/// Represents an in-memory sequence of bits that can be directly accessed. pub trait Record { - fn raw(&self) -> &BitSlice; + /// Returns a reference to the sequence of bits in `self`. + fn raw(&self) -> &RawRecord; } /* IMPLEMENTATIONS */ diff --git a/src/database/vector/mod.rs b/src/database/vector/mod.rs index e5dcff5..3a8c3e6 100644 --- a/src/database/vector/mod.rs +++ b/src/database/vector/mod.rs @@ -16,69 +16,90 @@ //! - 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; +use std::path::Path; -/* CONSTANTS */ +use crate::{ + database::{self, KVStore, Persistent, Record, Schema, Tabular}, + model::{Key, RawRecord, TableID}, +}; -const METADATA_TABLE: &'static str = ".metadata"; +/* DEFINITIONS */ -/* DATABASE DEFINITION */ +pub struct Database {} -pub struct Database<'a> { - buffer: Vec, - table: Table<'a>, - mode: Persistence<'a>, -} +pub struct Table {} + +/* IMPLEMENTATIONS */ + +impl<'a> Persistent<'a, Table> for Database { + fn from(path: &Path) -> Result + where + Self: Sized, + { + todo!() + } -struct Table<'a> { - dirty: bool, - width: u32, - name: &'a str, - size: u128, + fn bind(&self, path: &Path) -> Result<()> { + todo!() + } + + fn flush(&self, table: &mut Table) -> Result<()> { + todo!() + } } -pub struct Parameters<'a> { - persistence: Persistence<'a>, +impl Drop for Database { + fn drop(&mut self) { + todo!() + } } -/* IMPLEMENTATION */ +impl<'a> Tabular<'a, Table> for Database { + fn create_table(&self, id: &TableID, schema: Schema) -> Result<&mut Table> { + todo!() + } + + fn select_table(&self, id: &TableID) -> Result<&mut Table> { + todo!() + } -impl Database<'_> { - fn initialize(params: Parameters) -> Result { + fn delete_table(&self, id: &mut Table) -> Result<()> { todo!() } } -impl KVStore for Database<'_> { - fn put(&mut self, key: State, value: &R) { +impl database::Table for Table { + fn schema(&self) -> &Schema { + todo!() + } + + fn count(&self) -> u64 { todo!() } - fn get(&self, key: State) -> Option<&BitSlice> { + fn size(&self) -> u64 { todo!() } - fn del(&mut self, key: State) { + fn id(&self) -> &TableID { todo!() } } -impl Tabular for Database<'_> { - fn create_table(&self, id: &str, schema: Schema) -> Result<()> { +impl KVStore for Table { + fn put(&mut self, key: Key, value: &R) -> Result<()> + where + R: Record, + { todo!() } - fn select_table(&self, id: &str) -> Result<()> { + fn get(&self, key: Key) -> Option<&RawRecord> { todo!() } - fn delete_table(&self, id: &str) -> Result<()> { + fn delete(&mut self, key: Key) { todo!() } } diff --git a/src/database/volatile/mod.rs b/src/database/volatile/mod.rs index 39e5e51..0cf8052 100644 --- a/src/database/volatile/mod.rs +++ b/src/database/volatile/mod.rs @@ -7,51 +7,69 @@ //! - Max Fierro, 2/24/2024 (maxfierro@berkeley.edu) use anyhow::Result; -use bitvec::{order::Msb0, slice::BitSlice, store::BitStore}; - -use std::collections::HashMap; +use bitvec::{order::Msb0, slice::BitSlice}; use crate::{ - database::{KVStore, Record, Schema, Tabular}, - model::State, + database::{self, KVStore, Record, Schema, Tabular}, + model::{State, TableID}, }; -pub struct Database<'a> { - memory: HashMap, -} +/* DEFINITIONS */ + +pub struct Database {} + +pub struct Table {} + +/* IMPLEMENTATIONS */ -impl Database<'_> { +impl Database { pub fn initialize() -> Self { - Self { - memory: HashMap::new(), - } + Database {} } } -impl KVStore for Database<'_> { - fn put(&mut self, key: State, value: &R) { +impl<'a> Tabular<'a, Table> for Database { + fn create_table(&self, id: &TableID, schema: Schema) -> Result<&mut Table> { todo!() } - fn get(&self, key: State) -> Option<&BitSlice> { + fn select_table(&self, id: &TableID) -> Result<&mut Table> { + todo!() + } + + fn delete_table(&self, id: &mut Table) -> Result<()> { + todo!() + } +} + +impl database::Table for Table { + fn schema(&self) -> &Schema { + todo!() + } + + fn count(&self) -> u64 { todo!() } - fn del(&mut self, key: State) { + fn size(&self) -> u64 { + todo!() + } + + fn id(&self) -> &TableID { todo!() } } -impl Tabular for Database<'_> { - fn create_table(&self, id: &str, schema: Schema) -> Result<()> { +impl KVStore for Table { + fn put(&mut self, key: State, value: &R) -> Result<()> { todo!() } - fn select_table(&self, id: &str) -> Result<()> { + fn get(&self, key: State) -> Option<&BitSlice> { todo!() } - fn delete_table(&self, id: &str) -> Result<()> { + fn delete(&mut self, key: crate::model::Key) { todo!() } } diff --git a/src/model.rs b/src/model.rs index 55228d2..e2352ed 100644 --- a/src/model.rs +++ b/src/model.rs @@ -10,6 +10,8 @@ /* STATES */ +use bitvec::{order::Msb0, slice::BitSlice}; + /// Encodes the state of a game in a 64-bit unsigned integer. This also /// sets a limiting upper bound on the amount of possible non-equivalent states /// that can be achieved in a game. @@ -50,7 +52,19 @@ pub type DrawDepth = u64; pub type Remoteness = u64; /// Please refer to [this](https://en.wikipedia.org/wiki/Mex_(mathematics)). -pub type MinEV = u64; +pub type Mex = u64; + +/* DATABASE */ + +/// The type of an identifier used to differentiate database tables. +pub type TableID = str; + +/// The type of a raw sequence of bits encoding a database record, backed by +/// a [`BitSlice`] with [`u8`] big-endian storage. +pub type RawRecord = BitSlice; + +/// The type of a database key per an implementation of [`KVStore`]. +pub type Key = State; /* AUXILIARY */ diff --git a/src/solver/algorithm/strong/acyclic.rs b/src/solver/algorithm/strong/acyclic.rs index bbc07ba..c38b268 100644 --- a/src/solver/algorithm/strong/acyclic.rs +++ b/src/solver/algorithm/strong/acyclic.rs @@ -23,10 +23,14 @@ pub fn dynamic_solver(game: &G, mode: IOMode) -> Result<()> where G: DTransition + Bounded + GeneralSum + Extensive + Game, { - let mut db = volatile_database(game) + let db = volatile_database(game) .context("Failed to initialize volatile database.")?; - dynamic_backward_induction(&mut db, game) + let table = db + .select_table(&game.id()) + .context("Failed to select solution set database table.")?; + + dynamic_backward_induction(table, game) .context("Failed solving algorithm execution.")?; Ok(()) @@ -40,9 +44,14 @@ where + Extensive + Game, { - let mut db = volatile_database(game) + let db = volatile_database(game) .context("Failed to initialize volatile database.")?; - static_backward_induction(&mut db, game) + + let table = db + .select_table(&game.id()) + .context("Failed to select solution set database table.")?; + + static_backward_induction(table, game) .context("Failed solving algorithm execution.")?; Ok(()) } @@ -64,8 +73,6 @@ where .context("Failed to create table schema for solver records.")?; 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) } From 005505aff06850ff70228d1d843d03488f715317 Mon Sep 17 00:00:00 2001 From: Max Fierro Date: Mon, 22 Apr 2024 17:12:43 -0700 Subject: [PATCH 03/15] Minor fixes --- src/solver/algorithm/strong/acyclic.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/solver/algorithm/strong/acyclic.rs b/src/solver/algorithm/strong/acyclic.rs index c38b268..4759dc6 100644 --- a/src/solver/algorithm/strong/acyclic.rs +++ b/src/solver/algorithm/strong/acyclic.rs @@ -98,7 +98,7 @@ where let mut buf = RecordBuffer::new(game.players()) .context("Failed to create placeholder record.")?; if db.get(curr).is_none() { - db.put(curr, &buf); + db.put(curr, &buf)?; if game.end(curr) { buf = RecordBuffer::new(game.players()) .context("Failed to create record for end state.")?; @@ -106,7 +106,7 @@ where .context("Failed to copy utility values to record.")?; buf.set_remoteness(0) .context("Failed to set remoteness for end state.")?; - db.put(curr, &buf); + db.put(curr, &buf)?; } else { stack.push(curr); stack.extend( @@ -135,7 +135,7 @@ where optimal .set_remoteness(min_rem + 1) .context("Failed to set remoteness for solved record.")?; - db.put(curr, &optimal); + db.put(curr, &optimal)?; } } Ok(()) @@ -160,7 +160,7 @@ where let mut buf = RecordBuffer::new(game.players()) .context("Failed to create placeholder record.")?; if db.get(curr).is_none() { - db.put(curr, &buf); + db.put(curr, &buf)?; if game.end(curr) { buf = RecordBuffer::new(game.players()) .context("Failed to create record for end state.")?; @@ -168,7 +168,7 @@ where .context("Failed to copy utility values to record.")?; buf.set_remoteness(0) .context("Failed to set remoteness for end state.")?; - db.put(curr, &buf); + db.put(curr, &buf)?; } else { stack.push(curr); stack.extend( @@ -202,7 +202,7 @@ where optimal .set_remoteness(min_rem + 1) .context("Failed to set remoteness for solved record.")?; - db.put(curr, &optimal); + db.put(curr, &optimal)?; } } Ok(()) From f0c57e39eaf57976e7678f6c3ae66ed9ba916c81 Mon Sep 17 00:00:00 2001 From: Max Fierro Date: Mon, 6 May 2024 00:56:26 -0700 Subject: [PATCH 04/15] Added compile-time-variable length keys, removed static transition interface, restructured data models, fixed KVStore interface, style changes --- src/database/mod.rs | 24 ++-- src/database/util.rs | 4 +- src/database/vector/mod.rs | 22 ++-- src/database/volatile/mod.rs | 20 +-- src/game/crossteaser/mod.rs | 27 ++-- src/game/mock/builder.rs | 6 +- src/game/mock/mod.rs | 79 +++++------- src/game/mod.rs | 164 +++---------------------- src/game/util.rs | 161 +++--------------------- src/game/zero_by/mod.rs | 126 ++++++++++--------- src/game/zero_by/states.rs | 30 ++--- src/game/zero_by/variants.rs | 28 +++-- src/main.rs | 15 +-- src/model.rs | 146 ++++++++++++---------- src/solver/algorithm/strong/acyclic.rs | 147 ++++++---------------- src/solver/mod.rs | 153 ++++++++++++++++++++++- src/solver/record/mur.rs | 41 ++++--- src/solver/record/rem.rs | 17 +-- src/solver/record/sur.rs | 53 ++++---- src/solver/util.rs | 105 ++++------------ src/util.rs | 55 +++++++++ 21 files changed, 616 insertions(+), 807 deletions(-) diff --git a/src/database/mod.rs b/src/database/mod.rs index 052a5b3..e04e080 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -10,7 +10,7 @@ use anyhow::Result; use std::path::{Path, PathBuf}; -use crate::model::{Key, RawRecord, TableID}; +use crate::model::database::{Identifier, Key, Value}; use crate::solver::RecordType; /* RE-EXPORTS */ @@ -86,27 +86,25 @@ pub trait KVStore { /// Replaces the value associated with `key` with the bits of `record`, /// creating one if it does not already exist. Fails if under any violation /// of implementation-specific assumptions of record size or contents. - fn put(&mut self, key: Key, record: &R) -> Result<()> - where - R: Record; + fn put(&mut self, key: &Key, record: &R) -> Result<()>; /// Returns the bits associated with the value of `key`, or `None` if there /// is no such association. Infallible due to all possible values of `key` /// being considered valid (but not necessarily existent). - fn get(&self, key: Key) -> Option<&RawRecord>; + fn get(&self, key: &Key) -> Option<&Value>; /// Removes the association of `key` to whatever value it is currently bound /// to, or does nothing if there is no such value. - fn delete(&mut self, key: Key); + fn delete(&mut self, key: &Key); } /// Allows a database to be evicted to persistent media. Implementing this trait /// requires custom handling of what happens when the database is closed; if it /// has data on memory, then it should persist dirty data to ensure consistency /// via [`Drop`]. Database file structure is implementation-specific. -pub trait Persistent<'a, T> +pub trait Persistent where - Self: Tabular<'a, T> + Drop, + Self: Tabular + Drop, T: Table, { /// Interprets the contents of a directory at `path` to be the contents of @@ -132,17 +130,17 @@ where /// fixed-length records that share attributes under a single [`Schema`]. This /// allows consumers of this implementation to have simultaneous references to /// different mutable tables. -pub trait Tabular<'a, T> +pub trait Tabular where T: Table, { /// Creates a new table with `id` and `schema`. Fails if another table with /// the same `id` already exists, or under any I/O failure. - fn create_table(&self, id: &TableID, schema: Schema) -> Result<&mut T>; + fn create_table(&self, id: Identifier, schema: Schema) -> Result<&mut T>; /// Obtains a mutable reference to the [`Table`] with `id`. Fails if no such /// table exists in the underlying database, or under any I/O failure. - fn select_table(&self, id: &TableID) -> Result<&mut T>; + fn select_table(&self, id: Identifier) -> Result<&mut T>; /// Forgets about the association of `id` to any existing table, doing /// nothing if there is no such table. Fails under any I/O failure. @@ -169,7 +167,7 @@ where fn size(&self) -> u64; /// Returns the identifier associated with `self`. - fn id(&self) -> &TableID; + fn id(&self) -> Identifier; } /* RECORD INTERFACE */ @@ -177,7 +175,7 @@ where /// Represents an in-memory sequence of bits that can be directly accessed. pub trait Record { /// Returns a reference to the sequence of bits in `self`. - fn raw(&self) -> &RawRecord; + fn raw(&self) -> &Value; } /* IMPLEMENTATIONS */ diff --git a/src/database/util.rs b/src/database/util.rs index a328b33..919acfd 100644 --- a/src/database/util.rs +++ b/src/database/util.rs @@ -107,9 +107,7 @@ impl SchemaBuilder { 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.") - }, + Datatype::UINT | Datatype::ENUM => s == 0, } { Err(DatabaseError::InvalidSize { size: new.size(), diff --git a/src/database/vector/mod.rs b/src/database/vector/mod.rs index 3a8c3e6..95b7879 100644 --- a/src/database/vector/mod.rs +++ b/src/database/vector/mod.rs @@ -21,7 +21,7 @@ use std::path::Path; use crate::{ database::{self, KVStore, Persistent, Record, Schema, Tabular}, - model::{Key, RawRecord, TableID}, + model::database::{Identifier, Key, Value}, }; /* DEFINITIONS */ @@ -32,7 +32,7 @@ pub struct Table {} /* IMPLEMENTATIONS */ -impl<'a> Persistent<'a, Table> for Database { +impl Persistent for Database { fn from(path: &Path) -> Result where Self: Sized, @@ -55,12 +55,16 @@ impl Drop for Database { } } -impl<'a> Tabular<'a, Table> for Database { - fn create_table(&self, id: &TableID, schema: Schema) -> Result<&mut Table> { +impl Tabular
for Database { + fn create_table( + &self, + id: Identifier, + schema: Schema, + ) -> Result<&mut Table> { todo!() } - fn select_table(&self, id: &TableID) -> Result<&mut Table> { + fn select_table(&self, id: Identifier) -> Result<&mut Table> { todo!() } @@ -82,24 +86,24 @@ impl database::Table for Table { todo!() } - fn id(&self) -> &TableID { + fn id(&self) -> Identifier { todo!() } } impl KVStore for Table { - fn put(&mut self, key: Key, value: &R) -> Result<()> + fn put(&mut self, key: &Key, value: &R) -> Result<()> where R: Record, { todo!() } - fn get(&self, key: Key) -> Option<&RawRecord> { + fn get(&self, key: &Key) -> Option<&Value> { todo!() } - fn delete(&mut self, key: Key) { + fn delete(&mut self, key: &Key) { todo!() } } diff --git a/src/database/volatile/mod.rs b/src/database/volatile/mod.rs index 0cf8052..c81c4fd 100644 --- a/src/database/volatile/mod.rs +++ b/src/database/volatile/mod.rs @@ -11,7 +11,7 @@ use bitvec::{order::Msb0, slice::BitSlice}; use crate::{ database::{self, KVStore, Record, Schema, Tabular}, - model::{State, TableID}, + model::database::{Identifier, Key}, }; /* DEFINITIONS */ @@ -28,12 +28,16 @@ impl Database { } } -impl<'a> Tabular<'a, Table> for Database { - fn create_table(&self, id: &TableID, schema: Schema) -> Result<&mut Table> { +impl Tabular
for Database { + fn create_table( + &self, + id: Identifier, + schema: Schema, + ) -> Result<&mut Table> { todo!() } - fn select_table(&self, id: &TableID) -> Result<&mut Table> { + fn select_table(&self, id: Identifier) -> Result<&mut Table> { todo!() } @@ -55,21 +59,21 @@ impl database::Table for Table { todo!() } - fn id(&self) -> &TableID { + fn id(&self) -> Identifier { todo!() } } impl KVStore for Table { - fn put(&mut self, key: State, value: &R) -> Result<()> { + fn put(&mut self, key: &Key, value: &R) -> Result<()> { todo!() } - fn get(&self, key: State) -> Option<&BitSlice> { + fn get(&self, key: &Key) -> Option<&BitSlice> { todo!() } - fn delete(&mut self, key: crate::model::Key) { + fn delete(&mut self, key: &Key) { todo!() } } diff --git a/src/game/crossteaser/mod.rs b/src/game/crossteaser/mod.rs index 994672e..a8af8b7 100644 --- a/src/game/crossteaser/mod.rs +++ b/src/game/crossteaser/mod.rs @@ -22,23 +22,18 @@ use anyhow::{Context, Result}; use crate::game::Bounded; use crate::game::Codec; -use crate::game::DTransition; -use crate::game::Extensive; use crate::game::Forward; use crate::game::Game; use crate::game::GameData; -use crate::game::GeneralSum; +use crate::game::Transition; use crate::interface::IOMode; use crate::interface::SolutionMode; -use crate::model::SimpleUtility; -use crate::model::State; -use crate::model::Turn; -use crate::model::Utility; +use crate::model::database::Identifier; +use crate::model::game::State; +use crate::model::solver::SUtility; +use crate::solver::ClassicPuzzle; use variants::*; -use super::ClassicPuzzle; -use super::SimpleSum; - /* SUBMODULES */ mod states; @@ -94,12 +89,8 @@ impl Game for Session { } } - fn id(&self) -> String { - if let Some(variant) = self.variant.clone() { - format!("{}.{}", NAME, variant) - } else { - NAME.to_owned() - } + fn id(&self) -> Identifier { + todo!() } fn info(&self) -> GameData { @@ -113,7 +104,7 @@ impl Game for Session { /* TRAVERSAL IMPLEMENTATIONS */ -impl DTransition for Session { +impl Transition for Session { fn prograde(&self, state: State) -> Vec { todo!() } @@ -154,7 +145,7 @@ impl Forward for Session { /* SOLVING IMPLEMENTATIONS */ impl ClassicPuzzle for Session { - fn utility(&self, state: State) -> SimpleUtility { + fn utility(&self, state: State) -> SUtility { todo!() } } diff --git a/src/game/mock/builder.rs b/src/game/mock/builder.rs index f43d83e..387f54a 100644 --- a/src/game/mock/builder.rs +++ b/src/game/mock/builder.rs @@ -16,7 +16,7 @@ use std::collections::{HashMap, HashSet}; use crate::game::mock::Node; use crate::game::mock::Session; -use crate::model::PlayerCount; +use crate::model::game::PlayerCount; /* DEFINITIONS */ @@ -137,7 +137,7 @@ impl<'a> SessionBuilder<'a> { pub fn build(self) -> Result> { let start = self.check_starting_state()?; self.check_terminal_state(start)?; - self.check_outgoing_edges(start)?; + self.check_outgoing_edges()?; let (players, _) = self.players; Ok(Session { inserted: self.inserted, @@ -273,7 +273,7 @@ impl<'a> SessionBuilder<'a> { /// Fails if there exists a node marked as medial in the game graph which /// does not have any outgoing edges. - fn check_outgoing_edges(&self, start: NodeIndex) -> Result<()> { + fn check_outgoing_edges(&self) -> Result<()> { if self.game.node_indices().any(|i| { self.game[i].medial() && self diff --git a/src/game/mock/mod.rs b/src/game/mock/mod.rs index 0ad08e0..b3a2345 100644 --- a/src/game/mock/mod.rs +++ b/src/game/mock/mod.rs @@ -8,19 +8,19 @@ //! #### Authorship //! - Max Fierro 3/31/2024 (maxfierro@berkeley.edu) +use bitvec::field::BitField; +use petgraph::csr::DefaultIx; use petgraph::Direction; use petgraph::{graph::NodeIndex, Graph}; use std::collections::HashMap; use crate::game::Bounded; -use crate::game::DTransition; -use crate::game::STransition; -use crate::model::PlayerCount; -use crate::model::State; -use crate::model::Turn; -use crate::model::Utility; -use crate::solver::MAX_TRANSITIONS; +use crate::game::Transition; +use crate::model::game::Player; +use crate::model::game::PlayerCount; +use crate::model::game::State; +use crate::model::solver::IUtility; /* RE-EXPORTS */ @@ -38,7 +38,7 @@ mod builder; pub struct Session<'a> { inserted: HashMap<*const Node, NodeIndex>, players: PlayerCount, - start: NodeIndex, + start: NodeIndex, game: Graph<&'a Node, ()>, name: &'static str, } @@ -49,8 +49,8 @@ pub struct Session<'a> { /// turn encoding whose player's action is pending. #[derive(Debug)] pub enum Node { - Terminal(Vec), - Medial(Turn), + Terminal(Vec), + Medial(Player), } /* API IMPLEMENTATION */ @@ -72,7 +72,9 @@ impl<'a> Session<'a> { .inserted .get(&(node as *const Node)) { - Some(index.index() as State) + let mut state = State::ZERO; + state.store_be::(index.index() as DefaultIx); + Some(state) } else { None } @@ -91,21 +93,28 @@ impl<'a> Session<'a> { /// they should be connected by incoming or outgoing edges. fn transition(&self, state: State, dir: Direction) -> Vec { self.game - .neighbors_directed(NodeIndex::from(state as u32), dir) - .map(|n| n.index() as State) + .neighbors_directed( + NodeIndex::from(state.load_be::()), + dir, + ) + .map(|n| { + let mut state = State::ZERO; + state.store_be(n.index()); + state + }) .collect() } /// Returns a reference to the game node with `state`, or panics if there is /// no such node. fn node(&self, state: State) -> &Node { - self.game[NodeIndex::from(state as u32)] + self.game[NodeIndex::from(state.load_be::())] } } /* UTILITY IMPLEMENTATIONS */ -impl DTransition for Session<'_> { +impl Transition for Session<'_> { fn prograde(&self, state: State) -> Vec { self.transition(state, Direction::Outgoing) } @@ -115,43 +124,11 @@ impl DTransition for Session<'_> { } } -impl STransition for Session<'_> { - fn prograde(&self, state: State) -> [Option; MAX_TRANSITIONS] { - let adjacent = self - .transition(state, Direction::Outgoing) - .iter() - .map(|&h| Some(h)) - .collect::>>(); - - if adjacent.len() > MAX_TRANSITIONS { - panic!("Exceeded maximum transition count.") - } - - let mut result = [None; MAX_TRANSITIONS]; - result.copy_from_slice(&adjacent[..MAX_TRANSITIONS]); - result - } - - fn retrograde(&self, state: State) -> [Option; MAX_TRANSITIONS] { - let adjacent = self - .transition(state, Direction::Incoming) - .iter() - .map(|&h| Some(h)) - .collect::>>(); - - if adjacent.len() > MAX_TRANSITIONS { - panic!("Exceeded maximum transition count.") - } - - let mut result = [None; MAX_TRANSITIONS]; - result.copy_from_slice(&adjacent[..MAX_TRANSITIONS]); - result - } -} - impl Bounded for Session<'_> { fn start(&self) -> State { - self.start.index() as State + let mut state = State::ZERO; + state.store_be::(self.start.index() as DefaultIx); + state } fn end(&self, state: State) -> bool { @@ -214,7 +191,7 @@ mod tests { .collect(); let repeats = states.iter().any(|&i| { - states[(1 + i as usize)..] + states[(1 + i.load_be::())..] .iter() .any(|&j| i == j) }); diff --git a/src/game/mod.rs b/src/game/mod.rs index 74c0770..4ba7b3b 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -15,7 +15,8 @@ use anyhow::Result; use crate::{ interface::{IOMode, SolutionMode}, model::{ - Partition, PlayerCount, SimpleUtility, State, StateCount, Turn, Utility, + database::Identifier, + game::{State, DEFAULT_STATE_BYTES}, }, }; @@ -35,7 +36,7 @@ mod util; pub mod crossteaser; pub mod zero_by; -/* DATA CONSTRUCTS */ +/* DEFINITIONS */ /// Contains useful data about a game, intended to provide users of the program /// information they can use to understand the output of solving algorithms, @@ -77,7 +78,7 @@ pub struct GameData { pub state_default: &'static str, } -/* ACCESS INTERFACES */ +/* INTERFACES */ /// Defines miscellaneous behavior of a deterministic economic game object. Note /// that player count is arbitrary; puzzles are semantically one-player games, @@ -95,7 +96,7 @@ pub trait Game { /// from different games and variants. As such, it can be thought of as a /// string hash whose input is the game and variant (although it is not at /// all necessary that it conforms to any measure of hashing performance). - fn id(&self) -> String; + fn id(&self) -> Identifier; /// Returns useful information about the game, such as the type of game it /// is, who implemented it, and an explanation of how to specify different @@ -110,8 +111,6 @@ pub trait Game { fn solve(&self, mode: IOMode, method: SolutionMode) -> Result<()>; } -/* STATE RESOLUTION INTERFACES */ - /// Provides a way to retrieve a unique starting state from which to begin a /// traversal, and a way to tell when a traversal can no longer continue from /// a state. This does not necessarily imply that the underlying structure being @@ -138,14 +137,14 @@ pub trait Game { /// These facts motivate that the logic which determines the starting and ending /// states of games should be independent of the logic that transitions from /// valid states to other valid states. -pub trait Bounded { +pub trait Bounded { /// Returns the starting state of the underlying structure. This is used to /// deterministically initialize a traversal. - fn start(&self) -> S; + fn start(&self) -> State; /// Returns true if and only if there are no possible transitions from the /// provided `state`. Inputting an invalid `state` is undefined behavior. - fn end(&self, state: S) -> bool; + fn end(&self, state: State) -> bool; } /// Defines behavior to encode and decode a state type **S** to and from a @@ -164,20 +163,20 @@ pub trait Bounded { /// drawing; a lot of the utility obtained from implementing this interface is /// having access to understandable yet compact game state representations. As /// a rule of thumb, all strings should be single-lined and have no whitespace. -pub trait Codec { +pub trait Codec { /// Transforms a string representation of a game state into a type **S**. /// The `string` representation should conform to the `state_protocol` /// specified in the `GameData` object returned by `Game::info`. If it does /// not, an error containing a message with a brief explanation on what is /// wrong with `string` should be returned. - fn decode(&self, string: String) -> Result; + fn decode(&self, string: String) -> Result>; /// Transforms a game state type **S** into a string representation. The /// string returned should conform to the `state_protocol` specified in the /// `GameData` object returned by `Game::info`. If the `state` is malformed, /// this function should panic with a useful debug message. No two `state`s /// should return the same string representation (ideally). - fn encode(&self, state: S) -> String; + fn encode(&self, state: State) -> String; } /// Provides a way to fast-forward a game state from its starting state (as @@ -196,9 +195,9 @@ pub trait Codec { /// from the start of a game, it is necessary to demand a sequence of states /// that begin in a starting state and end in the desired state, such that each /// transition between states is valid per the game's ruleset. -pub trait Forward +pub trait Forward where - Self: Bounded + Codec, + Self: Bounded + Codec, { /// Advances the game's starting state to the last state in `history`. All /// all of the `String`s in `history` must conform to the `state_protocol` @@ -215,150 +214,19 @@ where fn forward(&mut self, history: Vec) -> Result<()>; } -/* DETERMINISTIC TRAVERSAL INTERFACES */ - /// TODO -pub trait DTransition { +pub trait Transition { /// Given a `state` at time `t`, returns all states that are possible at /// time `t + 1`. This should only guarantee that if `state` is feasible and /// not an end state, then all returned states are also feasible; therefore, /// inputting an invalid or end `state` is undefined behavior. The order of /// the values returned is insignificant. - fn prograde(&self, state: S) -> Vec; + fn prograde(&self, state: State) -> Vec>; /// Given a `state` at time `t`, returns all states that are possible at /// time `t - 1`. This should only guarantee that if `state` is feasible, /// then all returned states are also feasible; therefore, inputting an /// invalid `state` is undefined behavior. The order of the values returned /// is insignificant. - fn retrograde(&self, state: S) -> Vec; -} - -/// TODO -pub trait STransition { - /// Given a `state` at time `t`, returns all states that are possible at - /// time `t + 1`. This should only guarantee that if `state` is feasible and - /// not an end state, then all returned states are also feasible; therefore, - /// inputting an invalid or end `state` is undefined behavior. In the return - /// value, `Some(S)` represents a valid state. The order of these values is - /// insignificant. - fn prograde(&self, state: S) -> [Option; F]; - - /// Given a `state` at time `t`, returns all states that are possible at - /// time `t - 1`. This should only guarantee that if `state` is feasible, - /// then all returned states are also feasible; therefore, inputting an - /// invalid `state` is undefined behavior. In the return value, `Some(S)` - /// represents a valid state. The order of these values is insignificant. - fn retrograde(&self, state: S) -> [Option; F]; -} - -/* STRUCTURAL TRAITS */ - -/// TODO -pub trait Extensive { - /// 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 - } -} - -/// TODO -pub trait Composite -where - Self: Extensive, -{ - /// Returns a unique identifier for the partition that `state` is an element - /// of within the game variant specified by `self`. This implies no notion - /// of ordering between identifiers. - fn partition(&self, state: State) -> Partition; - - /// Provides an arbitrarily precise notion of the number of states that are - /// elements of `partition`. This can be used to distribute the work of - /// analyzing different partitions concurrently across different consumers - /// in a way that is equitable to improve efficiency. - fn size(&self, partition: Partition) -> StateCount; -} - -/* UTILITY INTERFACES */ - -/// TODO -pub trait GeneralSum { - /// If `state` is terminal, returns the utility vector associated with that - /// state, where `utility[i]` is the utility of the state for player `i`. If - /// the state is not terminal it is recommended that this function panics. - fn utility(&self, state: State) -> [Utility; N]; -} - -/// TODO -pub trait SimpleSum { - /// If `state` is terminal, returns the utility vector associated with that - /// state, where `utility[i]` is the utility of the state for player `i`. If - /// the state is not terminal, it is recommended that this function panics. - fn utility(&self, state: State) -> [SimpleUtility; N]; -} - -/* FAMILIAR INTERFACES */ - -/// Indicates that a game is 2-player, simple-sum, and zero-sum; this restricts -/// the possible utilities for a position to the following cases: -/// * `[Draw, Draw]` -/// * `[Lose, Win]` -/// * `[Win, Lose]` -/// * `[Tie, Tie]` -/// -/// Since either entry determines the other, knowing one of the entries and the -/// turn information for a given state provides enough information to determine -/// both players' utilities. -pub trait ClassicGame { - /// If `state` is terminal, returns the utility of the player whose turn it - /// is at that state. If the state is not terminal, it is recommended that - /// this function panics. - fn utility(&self, state: State) -> SimpleUtility; -} - -/// Indicates that a game is a puzzle with simple outcomes. This implies that it -/// is 1-player and the only possible utilities obtainable for the player are: -/// * `Lose` -/// * `Draw` -/// * `Tie` -/// * `Win` -/// -/// A winning state is usually one where there exists a sequence of moves that -/// will lead to the puzzle being fully solved. A losing state is one where any -/// sequence of moves will always take the player to either another losing state -/// or a state with no further moves available (with the puzzle still unsolved). -/// A draw state is one where there is no way to reach a winning state but it is -/// possible to play forever without reaching a losing state. A tie state is any -/// state that does not subjectively fit into any of the above categories. -pub trait ClassicPuzzle { - /// If `state` is terminal, returns the utility of the puzzle's player. If - /// the state is not terminal, it is recommended that this function panics. - fn utility(&self, state: State) -> SimpleUtility; -} - -/* BLANKET IMPLEMENTATIONS */ - -impl GeneralSum for G -where - G: SimpleSum, -{ - fn utility(&self, state: State) -> [Utility; N] { - todo!() - } -} - -impl SimpleSum<1> for G -where - G: ClassicPuzzle, -{ - fn utility(&self, state: State) -> [SimpleUtility; 1] { - todo!() - } + fn retrograde(&self, state: State) -> Vec>; } diff --git a/src/game/util.rs b/src/game/util.rs index a2ee8bb..60130b8 100644 --- a/src/game/util.rs +++ b/src/game/util.rs @@ -9,53 +9,10 @@ use anyhow::{Context, Result}; use crate::{ - game::error::GameError, - game::{Codec, DTransition, STransition}, - model::{PlayerCount, State, Turn}, - solver::MAX_TRANSITIONS, + game::{error::GameError, Bounded, Codec, Game, Transition}, + model::game::State, }; -use super::{Bounded, Game}; - -/* TURN ENCODING */ - -/// Minimally encodes turn information into the 64-bit integer `state` by -/// shifting the integer in `state` just enough bits to allow `turn` to be -/// expressed, where `turn` is upper-bounded by `player_count`. -/// -/// For example, if `player_count` is 3, `state` is `0b00...01`, and we want to -/// encode that it is player `2`'s turn (where players are 0-indexed), we would -/// return `0b00...00111`, whereas if `player_count` was 2 we would return -/// `0b00...0011`. This is because you need two bits to enumerate `{0, 1, 2}`, -/// but only one to enumerate `{0, 1}`. -pub fn pack_turn(state: State, turn: Turn, player_count: PlayerCount) -> State { - if player_count == 0 { - return state; - } else { - let turn_bits = Turn::BITS - (player_count - 1).leading_zeros(); - (state << turn_bits) | (turn as State) - } -} - -/// Given a state and a player count, determines the player whose turn it is by -/// taking note of the integer in the rightmost bits of `state`. The number of -/// bits considered turn information are determined by `player_count`. This is -/// the inverse function of `pack_turn`. -pub fn unpack_turn( - encoding: State, - player_count: PlayerCount, -) -> (State, Turn) { - if player_count == 0 { - return (encoding, 0); - } else { - let turn_bits = Turn::BITS - (player_count - 1).leading_zeros(); - let turn_mask = (1 << turn_bits) - 1; - let state = (encoding & !turn_mask) >> turn_bits; - let turn = (encoding & turn_mask) as usize; - (state, turn) - } -} - /* STATE HISTORY VERIFICATION */ /// Returns the latest state in a sequential `history` of state string encodings @@ -64,12 +21,12 @@ pub fn unpack_turn( /// `game`'s transition function. If these conditions are not met, it returns an /// error message signaling the pair of states that are not connected by the /// transition function, with a reminder of the current game variant. -pub fn verify_history_dynamic( +pub fn verify_history_dynamic( game: &G, history: Vec, -) -> Result +) -> Result> where - G: Game + Codec + Bounded + DTransition, + G: Game + Codec + Bounded + Transition, { if let Some(s) = history.first() { let mut prev = game.decode(s.clone())?; @@ -91,39 +48,9 @@ where } } -/// Returns the latest state in a sequential `history` of state string encodings -/// by verifying that the first state in the history is the same as the `game`'s -/// start and that each state can be reached from its predecessor through the -/// `game`'s transition function. If these conditions are not met, it returns an -/// error message signaling the pair of states that are not connected by the -/// transition function, with a reminder of the current game variant. -pub fn verify_history_static(game: &G, history: Vec) -> Result -where - G: Game + Codec + Bounded + STransition, -{ - if let Some(s) = history.first() { - let mut prev = game.decode(s.clone())?; - if prev == game.start() { - for i in 1..history.len() { - let next = game.decode(history[i].clone())?; - let transitions = game.prograde(prev); - if !transitions.contains(&Some(next)) { - return transition_history_error(game, prev, next); - } - prev = next; - } - Ok(prev) - } else { - start_history_error(game, game.start()) - } - } else { - empty_history_error(game) - } -} - -fn empty_history_error(game: &G) -> Result +fn empty_history_error(game: &G) -> Result> where - G: Game + Codec, + G: Game + Codec, { Err(GameError::InvalidHistory { game_name: game.info().name, @@ -132,9 +59,12 @@ where .context("Invalid game history.") } -fn start_history_error(game: &G, start: State) -> Result +fn start_history_error( + game: &G, + start: State, +) -> Result> where - G: Game + Codec, + G: Game + Codec, { Err(GameError::InvalidHistory { game_name: game.info().name, @@ -148,13 +78,13 @@ where .context("Invalid game history.") } -fn transition_history_error( +fn transition_history_error( game: &G, - prev: State, - next: State, -) -> Result + prev: State, + next: State, +) -> Result> where - G: Game + Codec, + G: Game + Codec, { Err(GameError::InvalidHistory { game_name: game.info().name, @@ -168,60 +98,3 @@ where }) .context("Invalid game history.") } - -#[cfg(test)] -mod tests { - - use super::*; - - #[test] - fn pack_turn_correctness() { - // Require three turn bits (8 players = {0b000, 0b001, ..., 0b111}) - let player_count: Turn = 8; - // 5 in decimal - let turn: Turn = 0b0000_0101; - // 31 in decimal - let state: State = 0b0001_1111; - // 0b00...00_1111_1101 in binary = 0b[state bits][player bits] - assert_eq!(0b1111_1101, pack_turn(state, turn, player_count)); - } - - #[test] - fn unpack_turn_correctness() { - // Require six turn bits (players = {0b0, 0b1, ..., 0b100101}) - let player_count: Turn = 38; - // 346 in decimal - let encoding: State = 0b0001_0101_1010; - // 0b00...00_0001_0101_1010 -> 0b00...00_0101 and 0b0001_1010, which - // means that 346 should be decoded to a state of 5 and a turn of 26 - assert_eq!((5, 26), unpack_turn(encoding, player_count)); - } - - #[test] - fn unpack_is_inverse_of_pack() { - // Require two turn bits (players = {0b00, 0b01, 0b10}) - let player_count: Turn = 3; - // 0b00...01 in binary - let turn: Turn = 2; - // 0b00...0111 in binary - let state: State = 7; - // 0b00...011101 in binary - let packed: State = pack_turn(state, turn, player_count); - // Packing and unpacking should yield equivalent results - assert_eq!((state, turn), unpack_turn(packed, player_count)); - - // About 255 * 23^2 iterations - for p in Turn::MIN..=255 { - let turn_bits = Turn::BITS - p.leading_zeros(); - let max_state: State = State::MAX / ((1 << turn_bits) as u64); - let state_step = ((max_state / 23) + 1) as usize; - let turn_step = ((p / 23) + 1) as usize; - - for s in (State::MIN..max_state).step_by(state_step) { - for t in (Turn::MIN..p).step_by(turn_step) { - assert_eq!((s, t), unpack_turn(pack_turn(s, t, p), p)); - } - } - } - } -} diff --git a/src/game/zero_by/mod.rs b/src/game/zero_by/mod.rs index dcbb1e9..6336a95 100644 --- a/src/game/zero_by/mod.rs +++ b/src/game/zero_by/mod.rs @@ -13,24 +13,30 @@ //! - Max Fierro, 4/6/2023 (maxfierro@berkeley.edu) use anyhow::{Context, Result}; +use bitvec::field::BitField; use states::*; use crate::game::error::GameError; -use crate::game::util::unpack_turn; use crate::game::zero_by::variants::*; use crate::game::{util, Bounded, Codec, Forward}; -use crate::game::{DTransition, Extensive, Game, GameData, GeneralSum}; +use crate::game::{Game, GameData, Transition}; use crate::interface::{IOMode, SolutionMode}; -use crate::model::PlayerCount; -use crate::model::Utility; -use crate::model::{State, Turn}; +use crate::model::database::Identifier; +use crate::model::game::{Player, PlayerCount, State}; +use crate::model::solver::SUtility; use crate::solver::algorithm::strong; +use crate::solver::{Extensive, SimpleUtility}; /* SUBMODULES */ mod states; mod variants; +/* DEFINITIONS */ + +/// The number of elements in the pile (see the game rules). +type Elements = u64; + /* GAME DATA */ const NAME: &'static str = "zero-by"; @@ -46,10 +52,12 @@ currently available in the set."; /* GAME IMPLEMENTATION */ pub struct Session { - variant: String, + start_elems: Elements, + start_state: State, + player_bits: usize, players: PlayerCount, - start: State, - by: Vec, + variant: String, + by: Vec, } impl Game for Session { @@ -61,8 +69,8 @@ impl Game for Session { } } - fn id(&self) -> String { - format!("{}.{}", NAME, self.variant) + fn id(&self) -> Identifier { + 5 } fn info(&self) -> GameData { @@ -86,11 +94,11 @@ impl Game for Session { fn solve(&self, mode: IOMode, method: SolutionMode) -> Result<()> { match (self.players, method) { (2, SolutionMode::Strong) => { - strong::acyclic::dynamic_solver::<2, Self>(self, mode) + strong::acyclic::solver::<2, 8, Self>(self, mode) .context("Failed solver run.")? }, (10, SolutionMode::Strong) => { - strong::acyclic::dynamic_solver::<10, Self>(self, mode) + strong::acyclic::solver::<10, 8, Self>(self, mode) .context("Failed solver run.")? }, _ => { @@ -106,19 +114,15 @@ impl Game for Session { /* TRAVERSAL IMPLEMENTATIONS */ -impl DTransition for Session { +impl Transition for Session { fn prograde(&self, state: State) -> Vec { - let (state, turn) = util::unpack_turn(state, self.players); + let (turn, elements) = self.decode_state(state); let mut next = self .by .iter() - .map(|&choice| if state <= choice { state } else { choice }) + .map(|&choice| if elements <= choice { elements } else { choice }) .map(|choice| { - util::pack_turn( - state - choice, - (turn + 1) % self.players, - self.players, - ) + self.encode_state((turn + 1) % self.players, elements - choice) }) .collect::>(); next.sort(); @@ -127,25 +131,21 @@ impl DTransition for Session { } fn retrograde(&self, state: State) -> Vec { - let (state, turn) = util::unpack_turn(state, self.players); - let mut next = - self.by - .iter() - .map(|&choice| { - if state + choice <= self.start { - choice - } else { - self.start - } - }) - .map(|choice| { - util::pack_turn( - state + choice, - (turn - 1) % self.players, - self.players, - ) - }) - .collect::>(); + let (turn, elements) = self.decode_state(state); + let mut next = self + .by + .iter() + .map(|&choice| { + if elements + choice <= self.start_elems { + choice + } else { + self.start_elems + } + }) + .map(|choice| { + self.encode_state((turn - 1) % self.players, elements + choice) + }) + .collect::>(); next.sort(); next.dedup(); next @@ -156,11 +156,12 @@ impl DTransition for Session { impl Bounded for Session { fn start(&self) -> State { - self.start + self.start_state } fn end(&self, state: State) -> bool { - state == 0 + let (_, elements) = self.decode_state(state); + elements <= 0 } } @@ -170,14 +171,14 @@ impl Codec for Session { } fn encode(&self, state: State) -> String { - let (elements, turn) = util::unpack_turn(state, self.players); + let (turn, elements) = self.decode_state(state); format!("{}-{}", elements, turn) } } impl Forward for Session { fn forward(&mut self, history: Vec) -> Result<()> { - self.start = util::verify_history_dynamic(self, history) + self.start_state = util::verify_history_dynamic(self, history) .context("Malformed game state encoding.")?; Ok(()) } @@ -185,32 +186,35 @@ impl Forward for Session { /* SOLVING IMPLEMENTATIONS */ -impl Extensive<2> for Session { - fn turn(&self, state: State) -> Turn { - util::unpack_turn(state, 2).1 +impl Extensive for Session { + fn turn(&self, state: State) -> Player { + let (turn, _) = self.decode_state(state); + turn } } -impl GeneralSum<2> for Session { - fn utility(&self, state: State) -> [Utility; 2] { - let (_, turn) = unpack_turn(state, 2); - let mut payoffs = [-1; 2]; - payoffs[turn] = 1; +impl SimpleUtility for Session { + fn utility(&self, state: State) -> [SUtility; N] { + let (turn, _) = self.decode_state(state); + let mut payoffs = [SUtility::LOSE; N]; + payoffs[turn] = SUtility::WIN; payoffs } } -impl Extensive<10> for Session { - fn turn(&self, state: State) -> Turn { - util::unpack_turn(state, 10).1 +/* UTILITY FUNCTIONS */ + +impl Session { + fn encode_state(&self, turn: Player, elements: Elements) -> State { + let mut state = State::ZERO; + state[..self.player_bits].store_be(turn); + state[self.player_bits..].store_be(elements); + state } -} -impl GeneralSum<10> for Session { - fn utility(&self, state: State) -> [Utility; 10] { - let (_, turn) = unpack_turn(state, 10); - let mut payoffs = [-1; 10]; - payoffs[turn] = 9; - payoffs + fn decode_state(&self, state: State) -> (Player, Elements) { + let player = state[..self.player_bits].load_be::(); + let elements = state[self.player_bits..].load_be::(); + (player, elements) } } diff --git a/src/game/zero_by/states.rs b/src/game/zero_by/states.rs index ca01a3c..60eb937 100644 --- a/src/game/zero_by/states.rs +++ b/src/game/zero_by/states.rs @@ -10,12 +10,12 @@ use regex::Regex; 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; +use crate::model::game::Player; +use crate::model::game::State; + +use super::Elements; /* ZERO-BY STATE ENCODING */ @@ -41,10 +41,9 @@ pub fn parse_state( ) -> Result { check_state_pattern(&from)?; let params = parse_parameters(&from)?; - let (from, turn) = check_param_count(¶ms)?; - check_variant_coherence(from, turn, &session)?; - let state = pack_turn(from, turn, session.players); - Ok(state) + let (elements, turn) = check_param_count(¶ms)?; + check_variant_coherence(elements, turn, &session)?; + Ok(session.encode_state(turn, elements)) } /* STATE STRING VERIFICATION */ @@ -77,7 +76,9 @@ fn parse_parameters(from: &String) -> Result, GameError> { .collect() } -fn check_param_count(params: &Vec) -> Result<(State, Turn), GameError> { +fn check_param_count( + params: &Vec, +) -> Result<(Elements, Player), GameError> { if params.len() != 2 { Err(GameError::StateMalformed { game_name: NAME, @@ -92,18 +93,17 @@ fn check_param_count(params: &Vec) -> Result<(State, Turn), GameError> { } fn check_variant_coherence( - from: State, - turn: Turn, + from: Elements, + turn: Player, session: &Session, ) -> Result<(), GameError> { - let (session_from, _) = unpack_turn(session.start, session.players); - if from > session_from { + if from > session.start_elems { Err(GameError::StateMalformed { game_name: NAME, hint: format!( "Specified more starting elements ({}) than variant allows \ ({}).", - from, session.start, + from, session.start_elems, ), }) } else if turn >= session.players { @@ -147,7 +147,7 @@ mod test { let with_default = Session::new(None).unwrap(); assert_eq!( - with_none.start, + with_none.start_state, parse_state(&with_default, STATE_DEFAULT.to_string()).unwrap() ); } diff --git a/src/game/zero_by/variants.rs b/src/game/zero_by/variants.rs index e52e7f5..45facf4 100644 --- a/src/game/zero_by/variants.rs +++ b/src/game/zero_by/variants.rs @@ -6,12 +6,13 @@ //! #### Authorship //! - Max Fierro, 11/2/2023 (maxfierro@berkeley.edu) +use bitvec::field::BitField; use regex::Regex; use crate::game::error::GameError; -use crate::game::util::pack_turn; use crate::game::zero_by::{Session, NAME}; -use crate::model::Turn; +use crate::model::game::{Player, State}; +use crate::util::min_ubits; /* ZERO-BY VARIANT ENCODING */ @@ -39,10 +40,19 @@ pub fn parse_variant(variant: String) -> Result { check_param_count(¶ms)?; check_params_are_positive(¶ms)?; let players = parse_player_count(¶ms)?; + + let start_elems = params[1]; + let mut start_state = State::ZERO; + let player_bits = min_ubits(players as u64); + start_state[..player_bits].store_be(Player::default()); + start_state[player_bits..].store_be(start_elems); + Ok(Session { - variant, + start_state, + start_elems, + player_bits, players, - start: pack_turn(params[1], 0, players), + variant, by: Vec::from(¶ms[2..]), }) } @@ -103,17 +113,17 @@ fn check_params_are_positive(params: &Vec) -> Result<(), GameError> { } } -fn parse_player_count(params: &Vec) -> Result { - if params[0] > (Turn::MAX as u64) { +fn parse_player_count(params: &Vec) -> Result { + if params[0] > (Player::MAX as u64) { Err(GameError::VariantMalformed { game_name: NAME, hint: format!( "The number of players in the game must be lower than {}.", - Turn::MAX + Player::MAX ), }) } else { - Ok(Turn::try_from(params[0]).unwrap()) + Ok(Player::try_from(params[0]).unwrap()) } } @@ -150,7 +160,7 @@ mod test { let with_default = Session::new(Some(VARIANT_DEFAULT.to_owned())).unwrap(); assert_eq!(with_none.variant, with_default.variant); - assert_eq!(with_none.start, with_default.start); + assert_eq!(with_none.start_state, with_default.start_state); assert_eq!(with_none.by, with_default.by); } diff --git a/src/main.rs b/src/main.rs index 6c5284d..5c48777 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,11 +12,11 @@ //! #### Authorship //! - Max Fierro, 4/6/2023 (maxfierro@berkeley.edu) +use std::process; + use anyhow::Result; use clap::Parser; -use std::process; - use crate::interface::terminal::cli::*; /* MODULES */ @@ -33,21 +33,18 @@ mod test; /* PROGRAM ENTRY */ -fn main() { +fn main() -> Result<()> { let cli = Cli::parse(); - let ret = match &cli.command { + let res = match &cli.command { Commands::Tui(args) => tui(args), Commands::Info(args) => info(args), Commands::Solve(args) => solve(args), Commands::Analyze(args) => analyze(args), }; - if let Err(e) = ret { - if !cli.quiet { - eprintln!("{}", e); - } + if res.is_err() && cli.quiet { process::exit(exitcode::USAGE) } - process::exit(exitcode::OK) + res } /* SUBCOMMAND EXECUTORS */ diff --git a/src/model.rs b/src/model.rs index e2352ed..c242cd0 100644 --- a/src/model.rs +++ b/src/model.rs @@ -8,77 +8,99 @@ //! - Max Fierro, 4/9/2023 (maxfierro@berkeley.edu) //! - Ishir Garg, 4/1/2024 (ishirgarg@berkeley.edu) -/* STATES */ - -use bitvec::{order::Msb0, slice::BitSlice}; - -/// Encodes the state of a game in a 64-bit unsigned integer. This also -/// sets a limiting upper bound on the amount of possible non-equivalent states -/// that can be achieved in a game. -pub type State = u64; - -/// Expresses whose turn it is in a game, where every player is assigned to a -/// different integer. Note that the type imposes a natural (but unknown) -/// limitation to player count that is dependent on the target architecture. -pub type Turn = usize; - -/* UTILITY */ - -/// A measure of how "good" an outcome is for a given player in a game. Positive -/// values indicate an overall gain from having played the game, and negative -/// values are net losses. The metric over abstract utility is subjective. -pub type Utility = i64; - -/// TODO -#[derive(Clone, Copy)] -pub enum SimpleUtility { - WIN = 0, - LOSE = 1, - DRAW = 2, - TIE = 3, -} +/// # Game Data Models Module +/// +/// Provides definitions for types used in game interfaces. +/// +/// #### Authorship +/// - Max Fierro, 4/30/2024 (maxfierro@berkeley.edu) +pub mod game { -/* ATTRIBUTES */ + use bitvec::{array::BitArray, order::Msb0}; -/// Indicates the "depth of draw" which a drawing position corresponds to. For -/// more information, see [this whitepaper](TODO). This value should be 0 for -/// non-drawing positions. -pub type DrawDepth = u64; + /// The default number of bytes used to encode states. + pub const DEFAULT_STATE_BYTES: usize = 8; -/// Indicates the number of choices that players have to make to reach a -/// terminal state in a game under perfect play. For drawing positions, -/// indicates the number of choices players can make to bring the game to a -/// state which can transition to a non-drawing state. -pub type Remoteness = u64; + /// Unique identifier of a particular state in a game. + pub type State = + BitArray<[u8; B], Msb0>; -/// Please refer to [this](https://en.wikipedia.org/wiki/Mex_(mathematics)). -pub type Mex = u64; + /// Unique identifier for a player in a game. + pub type Player = usize; -/* DATABASE */ + /// Unique identifier of a subset of states of a game. + pub type Partition = u64; -/// The type of an identifier used to differentiate database tables. -pub type TableID = str; + /// Count of the number of states in a game. + pub type StateCount = u64; -/// The type of a raw sequence of bits encoding a database record, backed by -/// a [`BitSlice`] with [`u8`] big-endian storage. -pub type RawRecord = BitSlice; + /// Count of the number of players in a game. + pub type PlayerCount = Player; +} -/// The type of a database key per an implementation of [`KVStore`]. -pub type Key = State; +/// # Solver Data Models Module +/// +/// Provides definitions for types used in solver implementations. +/// +/// #### Authorship +/// - Max Fierro, 5/5/2024 (maxfierro@berkeley.edu) +pub mod solver { + /// Indicates the "depth of draw" which a drawing position corresponds to. + /// For more information, see [this whitepaper](TODO). This value should be + /// 0 for non-drawing positions. + pub type DrawDepth = u64; + + /// Indicates the number of choices that players have to make to reach a + /// terminal state in a game under perfect play. For drawing positions, + /// indicates the number of choices players can make to bring the game to a + /// state which can transition to a non-drawing state. + pub type Remoteness = u64; + + /// Please refer to [this](https://en.wikipedia.org/wiki/Mex_(mathematics)). + pub type MinExclusion = u64; + + /// A measure of how "good" an outcome is for a given player in a game. + /// Positive values indicate an overall gain from having played the game, + /// and negative values are net losses. The metric over abstract utility is + /// subjective. + pub type RUtility = f64; + + /// A discrete measure of how "good" an outcome is for a given player. + /// Positive values indicate an overall gain from having played the game, + /// and negative values are net losses. The metric over abstract utility is + /// subjective. + pub type IUtility = i64; + + /// A simple measure of hoe "good" an outcome is for a given player in a + /// game. The specific meaning of each variant can change based on the game + /// in consideration, but this is ultimately an intuitive notion. + #[derive(Clone, Copy)] + pub enum SUtility { + WIN = 0, + LOSE = 1, + DRAW = 2, + TIE = 3, + } +} -/* AUXILIARY */ +/// # Database Data Models Module +/// +/// Provides definitions for types used in database interfaces. +/// +/// #### Authorship +/// - Max Fierro, 4/30/2024 (maxfierro@berkeley.edu) +pub mod database { -/// Used to count the number of states in a set. Although this has an identical -/// underlying type as `State`, it is semantically different, which is why it is -/// declared under a different type. -pub type StateCount = State; + use bitvec::order::Msb0; + use bitvec::slice::BitSlice; -/// Used to count the number of players in a game. Although this has a type that -/// is identical to `Turn`, it is semantically different, which is why it has -/// its own type declaration. -pub type PlayerCount = Turn; + /// A generic number used to differentiate between objects. + pub type Identifier = u64; -/// Encodes an identifier for a given partition within the space of states of a -/// game. This is a secondary type because the maximum number of partitions is -/// the number of states itself. -pub type Partition = State; + /// The type of a raw sequence of bits encoding a database value associated + /// with a key, backed by a [`BitSlice`] with [`u8`] big-endian storage. + pub type Value = BitSlice; + + /// The type of a database key per an implementation of [`KVStore`]. + pub type Key = BitSlice; +} diff --git a/src/solver/algorithm/strong/acyclic.rs b/src/solver/algorithm/strong/acyclic.rs index 4759dc6..099df8a 100644 --- a/src/solver/algorithm/strong/acyclic.rs +++ b/src/solver/algorithm/strong/acyclic.rs @@ -9,50 +9,36 @@ use anyhow::{Context, Result}; use crate::database::volatile; use crate::database::{KVStore, Tabular}; -use crate::game::{ - Bounded, DTransition, Extensive, Game, GeneralSum, STransition, -}; +use crate::game::{Bounded, Game, Transition}; use crate::interface::IOMode; -use crate::model::{PlayerCount, Remoteness, Utility}; +use crate::model::game::PlayerCount; +use crate::model::solver::{IUtility, Remoteness}; use crate::solver::record::mur::RecordBuffer; -use crate::solver::{RecordType, MAX_TRANSITIONS}; +use crate::solver::{Extensive, IntegerUtility, RecordType}; /* SOLVERS */ -pub fn dynamic_solver(game: &G, mode: IOMode) -> Result<()> -where - G: DTransition + Bounded + GeneralSum + Extensive + Game, -{ - let db = volatile_database(game) - .context("Failed to initialize volatile database.")?; - - let table = db - .select_table(&game.id()) - .context("Failed to select solution set database table.")?; - - dynamic_backward_induction(table, game) - .context("Failed solving algorithm execution.")?; - - Ok(()) -} - -pub fn static_solver(game: &G, mode: IOMode) -> Result<()> +pub fn solver( + game: &G, + mode: IOMode, +) -> Result<()> where - G: STransition - + Bounded - + GeneralSum - + Extensive + G: Transition + + Bounded + + IntegerUtility + + Extensive + Game, { let db = volatile_database(game) .context("Failed to initialize volatile database.")?; let table = db - .select_table(&game.id()) + .select_table(game.id()) .context("Failed to select solution set database table.")?; - static_backward_induction(table, game) + backward_induction(table, game) .context("Failed solving algorithm execution.")?; + Ok(()) } @@ -61,17 +47,20 @@ where /// 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 +fn volatile_database( + game: &G, +) -> Result where - G: Extensive + Game, + G: Extensive + Game, { let id = game.id(); let db = volatile::Database::initialize(); - let schema = RecordType::RUR(N) + let schema = RecordType::MUR(N) .try_into() .context("Failed to create table schema for solver records.")?; - db.create_table(&id, schema) + + db.create_table(id, schema) .context("Failed to create database table for solution set.")?; Ok(db) @@ -83,44 +72,51 @@ where /// 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( +fn backward_induction( db: &mut D, game: &G, ) -> Result<()> where D: KVStore, - G: DTransition + Bounded + GeneralSum + Extensive, + G: Transition + Bounded + IntegerUtility + Extensive, { let mut stack = Vec::new(); stack.push(game.start()); + while let Some(curr) = stack.pop() { let children = game.prograde(curr); let mut buf = RecordBuffer::new(game.players()) .context("Failed to create placeholder record.")?; - if db.get(curr).is_none() { - db.put(curr, &buf)?; + + 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)?; + + db.put(&curr, &buf)?; } else { stack.push(curr); stack.extend( children .iter() - .filter(|&x| db.get(*x).is_none()), + .filter(|&x| db.get(x).is_none()), ); } } else { let mut optimal = buf; - let mut max_val = Utility::MIN; + let mut max_val = IUtility::MIN; let mut min_rem = Remoteness::MAX; + for state in children { - let buf = RecordBuffer::from(db.get(state).unwrap()) + let buf = RecordBuffer::from(db.get(&state).unwrap()) .context("Failed to create record for middle state.")?; let val = buf .get_utility(game.turn(state)) @@ -132,77 +128,12 @@ where optimal = buf; } } - optimal - .set_remoteness(min_rem + 1) - .context("Failed to set remoteness for solved record.")?; - db.put(curr, &optimal)?; - } - } - Ok(()) -} -/// 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: STransition + Bounded + GeneralSum + Extensive, -{ - let mut stack = Vec::new(); - stack.push(game.start()); - while let Some(curr) = stack.pop() { - let children = game.prograde(curr); - 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( - children - .iter() - .filter_map(|&x| x) - .filter(|&x| db.get(x).is_none()), - ); - } - } else { - 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)?; + + db.put(&curr, &optimal)?; } } Ok(()) diff --git a/src/solver/mod.rs b/src/solver/mod.rs index 9077e9c..f621992 100644 --- a/src/solver/mod.rs +++ b/src/solver/mod.rs @@ -10,7 +10,13 @@ //! - Max Fierro, 4/6/2023 (maxfierro@berkeley.edu) //! - Ishir Garg, 4/3/2024 (ishirgarg@berkeley.edu) -use crate::model::PlayerCount; +use crate::model::{ + game::{ + Partition, Player, PlayerCount, State, StateCount, + DEFAULT_STATE_BYTES as DBYTES, + }, + solver::{IUtility, RUtility, SUtility}, +}; /* CONSTANTS */ @@ -18,7 +24,7 @@ use crate::model::PlayerCount; /// within a game. Used to allocate statically-sized arrays on the stack for /// faster execution of solving algorithms. If this limit is violated by a game /// implementation, this program should panic. -pub const MAX_TRANSITIONS: usize = 128; +pub const MAX_TRANSITIONS: usize = 512 / 8; /* MODULES */ @@ -67,17 +73,154 @@ mod test; mod error; mod util; -/* RECORD MODULES */ +/* SOLVER DATABASE RECORDS */ /// A record layout that can be used to encode and decode the attributes stored /// in serialized records. This is stored in database table schemas so that it /// can be retrieved later for deserialization. #[derive(Clone, Copy)] pub enum RecordType { - /// Real Utility Remoteness record for a specific number of players. - RUR(PlayerCount), + /// Multi-Utility Remoteness record for a specific number of players. + MUR(PlayerCount), /// Simple Utility Remoteness record for a specific number of players. SUR(PlayerCount), /// Remoteness record (no utilities). REM, } + +/* STRUCTURAL INTERFACES */ + +/// TODO +pub trait Extensive { + /// 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) -> Player; + + /// 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 + } +} + +/// TODO +pub trait Composite +where + Self: Extensive, +{ + /// Returns a unique identifier for the partition that `state` is an element + /// of within the game variant specified by `self`. This implies no notion + /// of ordering between identifiers. + fn partition(&self, state: State) -> Partition; + + /// Provides an arbitrarily precise notion of the number of states that are + /// elements of `partition`. This can be used to distribute the work of + /// analyzing different partitions concurrently across different consumers + /// in a way that is equitable to improve efficiency. + fn size(&self, partition: Partition) -> StateCount; +} + +/* UTILITY MEASURE INTERFACES */ + +/// TODO +pub trait RealUtility { + /// If `state` is terminal, returns the utility vector associated with that + /// state, where `utility[i]` is the utility of the state for player `i`. If + /// the state is not terminal, it is recommended that this function panics. + fn utility(&self, state: State) -> [RUtility; N]; +} + +/// TODO +pub trait IntegerUtility { + /// If `state` is terminal, returns the utility vector associated with that + /// state, where `utility[i]` is the utility of the state for player `i`. If + /// the state is not terminal it is recommended that this function panics. + fn utility(&self, state: State) -> [IUtility; N]; +} + +/// TODO +pub trait SimpleUtility { + /// If `state` is terminal, returns the utility vector associated with that + /// state, where `utility[i]` is the utility of the state for player `i`. If + /// the state is not terminal, it is recommended that this function panics. + fn utility(&self, state: State) -> [SUtility; N]; +} + +/* UTILITY STRUCTURE INTERFACES */ + +/// Indicates that a game is 2-player, simple-sum, and zero-sum; this restricts +/// the possible utilities for a position to the following cases: +/// * `[Draw, Draw]` +/// * `[Lose, Win]` +/// * `[Win, Lose]` +/// * `[Tie, Tie]` +/// +/// Since either entry determines the other, knowing one of the entries and the +/// turn information for a given state provides enough information to determine +/// both players' utilities. +pub trait ClassicGame { + /// If `state` is terminal, returns the utility of the player whose turn it + /// is at that state. If the state is not terminal, it is recommended that + /// this function panics. + fn utility(&self, state: State) -> SUtility; +} + +/// Indicates that a game is a puzzle with simple outcomes. This implies that it +/// is 1-player and the only possible utilities obtainable for the player are: +/// * `Lose` +/// * `Draw` +/// * `Tie` +/// * `Win` +/// +/// A winning state is usually one where there exists a sequence of moves that +/// will lead to the puzzle being fully solved. A losing state is one where any +/// sequence of moves will always take the player to either another losing state +/// or a state with no further moves available (with the puzzle still unsolved). +/// A draw state is one where there is no way to reach a winning state but it is +/// possible to play forever without reaching a losing state. A tie state is any +/// state that does not subjectively fit into any of the above categories. +pub trait ClassicPuzzle { + /// If `state` is terminal, returns the utility of the puzzle's player. If + /// the state is not terminal, it is recommended that this function panics. + fn utility(&self, state: State) -> SUtility; +} + +/* BLANKET IMPLEMENTATIONS */ + +impl RealUtility for G +where + G: IntegerUtility, +{ + fn utility(&self, state: State) -> [RUtility; N] { + todo!() + } +} + +impl IntegerUtility for G +where + G: SimpleUtility, +{ + fn utility(&self, state: State) -> [IUtility; N] { + todo!() + } +} + +impl SimpleUtility<2, B> for G +where + G: ClassicGame, +{ + fn utility(&self, state: State) -> [SUtility; 2] { + todo!() + } +} + +impl SimpleUtility<1, B> for G +where + G: ClassicPuzzle, +{ + fn utility(&self, state: State) -> [SUtility; 1] { + todo!() + } +} diff --git a/src/solver/record/mur.rs b/src/solver/record/mur.rs index 6a4e680..72a0ef6 100644 --- a/src/solver/record/mur.rs +++ b/src/solver/record/mur.rs @@ -14,10 +14,11 @@ use bitvec::slice::BitSlice; use bitvec::{bitarr, BitArr}; use crate::database::{Attribute, Datatype, Record, Schema, SchemaBuilder}; -use crate::model::{PlayerCount, Remoteness, Turn, Utility}; +use crate::model::game::{Player, PlayerCount}; +use crate::model::solver::{IUtility, Remoteness}; use crate::solver::error::SolverError::RecordViolation; -use crate::solver::util; use crate::solver::RecordType; +use crate::util; /* CONSTANTS */ @@ -37,7 +38,7 @@ pub const UTILITY_SIZE: usize = 8; pub fn schema(players: PlayerCount) -> Result { if RecordBuffer::bit_size(players) > BUFFER_SIZE { Err(RecordViolation { - name: RecordType::RUR(players).into(), + name: RecordType::MUR(players).into(), hint: format!( "This record can only hold utility values for up to {} \ players, but there was an attempt to create a schema that \ @@ -47,7 +48,7 @@ pub fn schema(players: PlayerCount) -> Result { ), })? } else { - let mut schema = SchemaBuilder::new().of(RecordType::RUR(players)); + let mut schema = SchemaBuilder::new().of(RecordType::MUR(players)); for i in 0..players { let name = &format!("P{} utility", i); @@ -110,7 +111,7 @@ impl RecordBuffer { pub fn new(players: PlayerCount) -> Result { if Self::bit_size(players) > BUFFER_SIZE { Err(RecordViolation { - name: RecordType::RUR(players).into(), + name: RecordType::MUR(players).into(), hint: format!( "The record can only hold utility values for up to {} \ players, but there was an attempt to instantiate one for \ @@ -134,7 +135,7 @@ impl RecordBuffer { let len = bits.len(); if len > BUFFER_SIZE { Err(RecordViolation { - name: RecordType::RUR(0).into(), + name: RecordType::MUR(0).into(), hint: format!( "The record implementation operates on a buffer of {} \ bits, but there was an attempt to instantiate one from a \ @@ -144,7 +145,7 @@ impl RecordBuffer { })? } else if len < Self::minimum_bit_size() { Err(RecordViolation { - name: RecordType::RUR(0).into(), + name: RecordType::MUR(0).into(), hint: format!( "This record implementation stores utility values, but \ there was an attempt to instantiate one with from a buffer \ @@ -166,10 +167,10 @@ impl RecordBuffer { /// Parse and return the utility value corresponding to `player`. Fails if /// the `player` index passed in is incoherent with player count. #[inline(always)] - pub fn get_utility(&self, player: Turn) -> Result { + pub fn get_utility(&self, player: Player) -> Result { if player >= self.players { Err(RecordViolation { - name: RecordType::RUR(self.players).into(), + name: RecordType::MUR(self.players).into(), hint: format!( "A record was instantiated with {} utility entries, and \ there was an attempt to fetch the utility of player {} \ @@ -180,7 +181,7 @@ impl RecordBuffer { } else { let start = Self::utility_index(player); let end = start + UTILITY_SIZE; - Ok(self.buf[start..end].load_be::()) + Ok(self.buf[start..end].load_be::()) } } @@ -202,11 +203,11 @@ impl RecordBuffer { #[inline(always)] pub fn set_utility( &mut self, - v: [Utility; N], + v: [IUtility; N], ) -> Result<()> { if N != self.players { Err(RecordViolation { - name: RecordType::RUR(self.players).into(), + name: RecordType::MUR(self.players).into(), hint: format!( "A record was instantiated with {} utility entries, and \ there was an attempt to use a {}-entry utility list to \ @@ -220,7 +221,7 @@ impl RecordBuffer { let size = util::min_sbits(utility); if size > UTILITY_SIZE { Err(RecordViolation { - name: RecordType::RUR(self.players).into(), + name: RecordType::MUR(self.players).into(), hint: format!( "This record implementation uses {} bits to store \ signed integers representing utility values, but \ @@ -246,7 +247,7 @@ impl RecordBuffer { let size = util::min_ubits(value); if size > REMOTENESS_SIZE { Err(RecordViolation { - name: RecordType::RUR(self.players).into(), + name: RecordType::MUR(self.players).into(), hint: format!( "This record implementation uses {} bits to store unsigned \ integers representing remoteness values, but there was an \ @@ -286,7 +287,7 @@ impl RecordBuffer { /// Return the bit index of the 'i'th player's utility entry start. #[inline(always)] - const fn utility_index(player: Turn) -> usize { + const fn utility_index(player: Player) -> usize { player * UTILITY_SIZE } @@ -312,8 +313,8 @@ mod tests { // * `MIN_UTILITY = 0b10000000 = -128 = -127 - 1` // // Useful: https://www.omnicalculator.com/math/twos-complement - const MAX_UTILITY: Utility = 2_i64.pow(UTILITY_SIZE as u32 - 1) - 1; - const MIN_UTILITY: Utility = (-MAX_UTILITY) - 1; + const MAX_UTILITY: IUtility = 2_i64.pow(UTILITY_SIZE as u32 - 1) - 1; + const MIN_UTILITY: IUtility = (-MAX_UTILITY) - 1; // The maximum numeric remoteness value that can be expressed with exactly // REMOTENESS_SIZE bits in an unsigned integer. @@ -362,10 +363,10 @@ mod tests { let v1 = [-24; 7]; let v2 = [113; 4]; - let v3: [Utility; 0] = []; + let v3: [IUtility; 0] = []; - let v4 = [Utility::MAX; 7]; - let v5 = [-Utility::MAX; 4]; + let v4 = [IUtility::MAX; 7]; + let v5 = [IUtility::MIN; 4]; let v6 = [1]; let good = Remoteness::MIN; diff --git a/src/solver/record/rem.rs b/src/solver/record/rem.rs index 5d1027a..e62c61b 100644 --- a/src/solver/record/rem.rs +++ b/src/solver/record/rem.rs @@ -1,7 +1,6 @@ //! # Remoteness (SUR) Record Module //! -//! Implementation of a database record buffer for storing only remoteness values -//! Note that this record does not store any utilities, making it useful for puzzles +//! TODO //! //! #### Authorship //! @@ -14,10 +13,10 @@ use bitvec::slice::BitSlice; use bitvec::{bitarr, BitArr}; use crate::database::{Attribute, Datatype, Record, Schema, SchemaBuilder}; -use crate::model::Remoteness; +use crate::model::solver::Remoteness; use crate::solver::error::SolverError::RecordViolation; -use crate::solver::util; use crate::solver::RecordType; +use crate::util; /* CONSTANTS */ @@ -45,15 +44,7 @@ pub fn schema() -> Result { /* RECORD IMPLEMENTATION */ -/// Solver-specific record entry, meant to communicate the remoteness at a corresponding game -/// state. -/// -/// ```none -/// [REMOTENESS_SIZE bits: Remoteness] -/// [0b0 until BUFFER_SIZE] -/// ``` -/// -/// The remoteness values are encoded in big-endian as unsigned integers +/// TODO pub struct RecordBuffer { buf: BitArr!(for BUFFER_SIZE, in u8, Msb0), } diff --git a/src/solver/record/sur.rs b/src/solver/record/sur.rs index 02e3e3b..3b28ddd 100644 --- a/src/solver/record/sur.rs +++ b/src/solver/record/sur.rs @@ -15,10 +15,11 @@ use bitvec::slice::BitSlice; use bitvec::{bitarr, BitArr}; use crate::database::{Attribute, Datatype, Record, Schema, SchemaBuilder}; -use crate::model::{PlayerCount, Remoteness, SimpleUtility, Turn}; +use crate::model::game::{Player, PlayerCount}; +use crate::model::solver::{Remoteness, SUtility}; use crate::solver::error::SolverError::RecordViolation; -use crate::solver::util; use crate::solver::RecordType; +use crate::util; /* CONSTANTS */ @@ -167,7 +168,7 @@ impl RecordBuffer { /// Parse and return the utility value corresponding to `player`. Fails if /// the `player` index passed in is incoherent with player count. #[inline(always)] - pub fn get_utility(&self, player: Turn) -> Result { + pub fn get_utility(&self, player: Player) -> Result { if player >= self.players { Err(RecordViolation { name: RecordType::SUR(self.players).into(), @@ -182,7 +183,7 @@ impl RecordBuffer { let start = Self::utility_index(player); let end = start + UTILITY_SIZE; let val = self.buf[start..end].load_be::(); - if let Ok(utility) = SimpleUtility::try_from(val) { + if let Ok(utility) = SUtility::try_from(val) { Ok(utility) } else { Err(RecordViolation { @@ -215,7 +216,7 @@ impl RecordBuffer { #[inline(always)] pub fn set_utility( &mut self, - v: [SimpleUtility; N], + v: [SUtility; N], ) -> Result<()> { if N != self.players { Err(RecordViolation { @@ -299,7 +300,7 @@ impl RecordBuffer { /// Return the bit index of the 'i'th player's utility entry start. #[inline(always)] - const fn utility_index(player: Turn) -> usize { + const fn utility_index(player: Player) -> usize { player * UTILITY_SIZE } @@ -324,8 +325,8 @@ mod tests { // * `MIN_UTILITY = 0b10000000 = -128 = -127 - 1` // // Useful: https://www.omnicalculator.com/math/twos-complement - const MAX_UTILITY: SimpleUtility = SimpleUtility::TIE; - const MIN_UTILITY: SimpleUtility = SimpleUtility::WIN; + const MAX_UTILITY: SUtility = SUtility::TIE; + const MIN_UTILITY: SUtility = SUtility::WIN; // The maximum numeric remoteness value that can be expressed with exactly // REMOTENESS_SIZE bits in an unsigned integer. @@ -372,13 +373,13 @@ mod tests { let mut r2 = RecordBuffer::new(4).unwrap(); let mut r3 = RecordBuffer::new(0).unwrap(); - let v1 = [SimpleUtility::WIN; 7]; - let v2 = [SimpleUtility::TIE; 4]; - let v3: [SimpleUtility; 0] = []; + let v1 = [SUtility::WIN; 7]; + let v2 = [SUtility::TIE; 4]; + let v3: [SUtility; 0] = []; let v4 = [MAX_UTILITY; 7]; let v5 = [MIN_UTILITY; 4]; - let v6 = [SimpleUtility::DRAW]; + let v6 = [SUtility::DRAW]; let good = Remoteness::MIN; let bad = Remoteness::MAX; @@ -404,11 +405,11 @@ mod tests { fn data_is_valid_after_round_trip() { let mut record = RecordBuffer::new(5).unwrap(); let payoffs = [ - SimpleUtility::LOSE, - SimpleUtility::WIN, - SimpleUtility::LOSE, - SimpleUtility::LOSE, - SimpleUtility::LOSE, + SUtility::LOSE, + SUtility::WIN, + SUtility::LOSE, + SUtility::LOSE, + SUtility::LOSE, ]; let remoteness = 790; @@ -442,19 +443,15 @@ mod tests { let mut record = RecordBuffer::new(6).unwrap(); let good = [ - SimpleUtility::WIN, - SimpleUtility::LOSE, - SimpleUtility::TIE, - SimpleUtility::TIE, - SimpleUtility::DRAW, - SimpleUtility::WIN, + SUtility::WIN, + SUtility::LOSE, + SUtility::TIE, + SUtility::TIE, + SUtility::DRAW, + SUtility::WIN, ]; - let bad = [ - SimpleUtility::DRAW, - SimpleUtility::WIN, - SimpleUtility::TIE, - ]; + let bad = [SUtility::DRAW, SUtility::WIN, SUtility::TIE]; assert!(record.set_utility(good).is_ok()); assert!(record diff --git a/src/solver/util.rs b/src/solver/util.rs index c0085b9..a44b214 100644 --- a/src/solver/util.rs +++ b/src/solver/util.rs @@ -7,35 +7,16 @@ //! - Max Fierro, 2/24/2024 (maxfierro@berkeley.edu) use crate::database::Schema; -use crate::model::{SimpleUtility, Utility}; -use crate::solver::error::SolverError::RecordViolation; +use crate::model::solver::{IUtility, SUtility}; +use crate::solver::error::SolverError; use crate::solver::{record, RecordType}; -/* BIT FIELDS */ - -/// Returns the minimum number of bits required to represent unsigned `val`. -#[inline(always)] -pub const fn min_ubits(val: u64) -> usize { - (u64::BITS - val.leading_zeros()) as usize -} - -/// Return the minimum number of bits necessary to encode `utility`, which -/// should be a signed integer in two's complement. -#[inline(always)] -pub fn min_sbits(utility: i64) -> usize { - if utility >= 0 { - min_ubits(utility as u64) + 1 - } else { - min_ubits(((-utility) - 1) as u64) + 1 - } -} - /* RECORD TYPE IMPLEMENTATIONS */ impl Into for RecordType { fn into(self) -> String { match self { - RecordType::RUR(players) => { + RecordType::MUR(players) => { format!("Real Utility Remoteness ({} players)", players) }, RecordType::SUR(players) => { @@ -53,7 +34,7 @@ impl TryInto for RecordType { fn try_into(self) -> Result { match self { - RecordType::RUR(players) => record::mur::schema(players), + RecordType::MUR(players) => record::mur::schema(players), RecordType::SUR(players) => record::sur::schema(players), RecordType::REM => record::rem::schema(), } @@ -62,77 +43,41 @@ impl TryInto for RecordType { /* UTILITY CONVERSION */ -impl TryFrom for SimpleUtility { - type Error = (); +impl TryFrom for SUtility { + type Error = SolverError; - fn try_from(v: Utility) -> Result { + fn try_from(v: IUtility) -> Result { match v { - v if v == SimpleUtility::LOSE as i64 => Ok(SimpleUtility::LOSE), - v if v == SimpleUtility::DRAW as i64 => Ok(SimpleUtility::DRAW), - v if v == SimpleUtility::TIE as i64 => Ok(SimpleUtility::TIE), - v if v == SimpleUtility::WIN as i64 => Ok(SimpleUtility::WIN), - _ => Err(()), + v if v == SUtility::LOSE as i64 => Ok(SUtility::LOSE), + v if v == SUtility::DRAW as i64 => Ok(SUtility::DRAW), + v if v == SUtility::TIE as i64 => Ok(SUtility::TIE), + v if v == SUtility::WIN as i64 => Ok(SUtility::WIN), + _ => Err(todo!()), } } } -impl TryFrom for SimpleUtility { - type Error = (); +impl TryFrom for SUtility { + type Error = SolverError; fn try_from(v: u64) -> Result { match v { - v if v == SimpleUtility::LOSE as u64 => Ok(SimpleUtility::LOSE), - v if v == SimpleUtility::DRAW as u64 => Ok(SimpleUtility::DRAW), - v if v == SimpleUtility::TIE as u64 => Ok(SimpleUtility::TIE), - v if v == SimpleUtility::WIN as u64 => Ok(SimpleUtility::WIN), - _ => Err(()), + v if v == SUtility::LOSE as u64 => Ok(SUtility::LOSE), + v if v == SUtility::DRAW as u64 => Ok(SUtility::DRAW), + v if v == SUtility::TIE as u64 => Ok(SUtility::TIE), + v if v == SUtility::WIN as u64 => Ok(SUtility::WIN), + _ => Err(todo!()), } } } -impl Into for SimpleUtility { - fn into(self) -> Utility { +impl Into for SUtility { + fn into(self) -> IUtility { match self { - SimpleUtility::LOSE => -1, - SimpleUtility::DRAW => 0, - SimpleUtility::TIE => 0, - SimpleUtility::WIN => 1, + SUtility::LOSE => -1, + SUtility::DRAW => 0, + SUtility::TIE => 0, + SUtility::WIN => 1, } } } - -#[cfg(test)] -mod tests { - - use super::*; - - #[test] - fn minimum_bits_for_unsigned_integer() { - assert_eq!(min_ubits(0), 0); - assert_eq!(min_ubits(0b1111_1111), 8); - assert_eq!(min_ubits(0b1001_0010), 8); - assert_eq!(min_ubits(0b0010_1001), 6); - assert_eq!(min_ubits(0b0000_0110), 3); - assert_eq!(min_ubits(0b0000_0001), 1); - assert_eq!(min_ubits(0xF000_0A00_0C00_00F5), 64); - assert_eq!(min_ubits(0x0000_F100_DEB0_A002), 48); - assert_eq!(min_ubits(0x0000_0000_F00B_1351), 32); - assert_eq!(min_ubits(0x0000_0000_F020_0DE0), 32); - assert_eq!(min_ubits(0x0000_0000_0000_FDE0), 16); - } - - #[test] - fn minimum_bits_for_positive_signed_integer() { - assert_eq!(min_sbits(0x0000_8000_2222_0001), 49); - assert_eq!(min_sbits(0x0070_DEAD_0380_7DE0), 56); - assert_eq!(min_sbits(0x0000_0000_F00B_1351), 33); - assert_eq!(min_sbits(0x0000_0000_0000_0700), 12); - assert_eq!(min_sbits(0x0000_0000_0000_0001), 2); - - assert_eq!(min_sbits(-10000), 15); - assert_eq!(min_sbits(-1000), 11); - assert_eq!(min_sbits(-255), 9); - assert_eq!(min_sbits(-128), 8); - assert_eq!(min_sbits(0), 1); - } -} diff --git a/src/util.rs b/src/util.rs index be4c7ff..05f39ee 100644 --- a/src/util.rs +++ b/src/util.rs @@ -145,6 +145,25 @@ impl Display for GameData { } } +/* BIT FIELDS */ + +/// Returns the minimum number of bits required to represent unsigned `val`. +#[inline(always)] +pub const fn min_ubits(val: u64) -> usize { + (u64::BITS - val.leading_zeros()) as usize +} + +/// Return the minimum number of bits necessary to encode `utility`, which +/// should be a signed integer in two's complement. +#[inline(always)] +pub fn min_sbits(utility: i64) -> usize { + if utility >= 0 { + min_ubits(utility as u64) + 1 + } else { + min_ubits(((-utility) - 1) as u64) + 1 + } +} + /* DECLARATIVE MACROS */ /// Syntax sugar. Implements multiple traits for a single concrete type. The @@ -266,3 +285,39 @@ macro_rules! node { Node::Terminal(vec![$($u),*]) }; } + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn minimum_bits_for_unsigned_integer() { + assert_eq!(min_ubits(0), 0); + assert_eq!(min_ubits(0b1111_1111), 8); + assert_eq!(min_ubits(0b1001_0010), 8); + assert_eq!(min_ubits(0b0010_1001), 6); + assert_eq!(min_ubits(0b0000_0110), 3); + assert_eq!(min_ubits(0b0000_0001), 1); + assert_eq!(min_ubits(0xF000_0A00_0C00_00F5), 64); + assert_eq!(min_ubits(0x0000_F100_DEB0_A002), 48); + assert_eq!(min_ubits(0x0000_0000_F00B_1351), 32); + assert_eq!(min_ubits(0x0000_0000_F020_0DE0), 32); + assert_eq!(min_ubits(0x0000_0000_0000_FDE0), 16); + } + + #[test] + fn minimum_bits_for_positive_signed_integer() { + assert_eq!(min_sbits(0x0000_8000_2222_0001), 49); + assert_eq!(min_sbits(0x0070_DEAD_0380_7DE0), 56); + assert_eq!(min_sbits(0x0000_0000_F00B_1351), 33); + assert_eq!(min_sbits(0x0000_0000_0000_0700), 12); + assert_eq!(min_sbits(0x0000_0000_0000_0001), 2); + + assert_eq!(min_sbits(-10000), 15); + assert_eq!(min_sbits(-1000), 11); + assert_eq!(min_sbits(-255), 9); + assert_eq!(min_sbits(-128), 8); + assert_eq!(min_sbits(0), 1); + } +} From 1d65478de1fe331552a69365b4c10233f4912b42 Mon Sep 17 00:00:00 2001 From: Max Fierro Date: Mon, 6 May 2024 01:02:29 -0700 Subject: [PATCH 05/15] Fixed test failure --- src/solver/record/mur.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/solver/record/mur.rs b/src/solver/record/mur.rs index 72a0ef6..a2f88fa 100644 --- a/src/solver/record/mur.rs +++ b/src/solver/record/mur.rs @@ -366,7 +366,7 @@ mod tests { let v3: [IUtility; 0] = []; let v4 = [IUtility::MAX; 7]; - let v5 = [IUtility::MIN; 4]; + let v5 = [-IUtility::MAX; 4]; let v6 = [1]; let good = Remoteness::MIN; From 72c98f510a49513a4705d088860bf25004a6fddf Mon Sep 17 00:00:00 2001 From: Max Fierro Date: Mon, 6 May 2024 01:05:38 -0700 Subject: [PATCH 06/15] Unimplemented id function for zero_by --- src/game/zero_by/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/zero_by/mod.rs b/src/game/zero_by/mod.rs index 6336a95..651dec3 100644 --- a/src/game/zero_by/mod.rs +++ b/src/game/zero_by/mod.rs @@ -70,7 +70,7 @@ impl Game for Session { } fn id(&self) -> Identifier { - 5 + todo!() } fn info(&self) -> GameData { From 016107f3c39722094c41a466f35d18a599de794b Mon Sep 17 00:00:00 2001 From: Max Fierro Date: Mon, 6 May 2024 01:16:06 -0700 Subject: [PATCH 07/15] Minor style change --- src/game/util.rs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/game/util.rs b/src/game/util.rs index 60130b8..bb4dae4 100644 --- a/src/game/util.rs +++ b/src/game/util.rs @@ -21,12 +21,12 @@ use crate::{ /// `game`'s transition function. If these conditions are not met, it returns an /// error message signaling the pair of states that are not connected by the /// transition function, with a reminder of the current game variant. -pub fn verify_history_dynamic( +pub fn verify_history_dynamic( game: &G, history: Vec, -) -> Result> +) -> Result> where - G: Game + Codec + Bounded + Transition, + G: Game + Codec + Bounded + Transition, { if let Some(s) = history.first() { let mut prev = game.decode(s.clone())?; @@ -48,9 +48,9 @@ where } } -fn empty_history_error(game: &G) -> Result> +fn empty_history_error(game: &G) -> Result> where - G: Game + Codec, + G: Game + Codec, { Err(GameError::InvalidHistory { game_name: game.info().name, @@ -59,12 +59,12 @@ where .context("Invalid game history.") } -fn start_history_error( +fn start_history_error( game: &G, - start: State, -) -> Result> + start: State, +) -> Result> where - G: Game + Codec, + G: Game + Codec, { Err(GameError::InvalidHistory { game_name: game.info().name, @@ -78,13 +78,13 @@ where .context("Invalid game history.") } -fn transition_history_error( +fn transition_history_error( game: &G, - prev: State, - next: State, -) -> Result> + prev: State, + next: State, +) -> Result> where - G: Game + Codec, + G: Game + Codec, { Err(GameError::InvalidHistory { game_name: game.info().name, From 8397a6a4bab0fbef2d57acf9cbb342c63c11dc09 Mon Sep 17 00:00:00 2001 From: Max Fierro Date: Mon, 6 May 2024 01:22:13 -0700 Subject: [PATCH 08/15] Fixed unreachable error --- src/database/error.rs | 6 +- src/database/test.rs | 222 ------------------------------------------ 2 files changed, 1 insertion(+), 227 deletions(-) diff --git a/src/database/error.rs b/src/database/error.rs index 0a197f5..0b673f4 100644 --- a/src/database/error.rs +++ b/src/database/error.rs @@ -110,11 +110,7 @@ impl fmt::Display for DatabaseError { Datatype::SPFP => "of exactly 32 bits", Datatype::SINT => "greater than 1 bit", Datatype::CSTR => "divisible by 8 bits", - Datatype::UINT | Datatype::ENUM => { - unreachable!( - "UINTs and ENUMs can be of any nonzero size." - ) - }, + Datatype::UINT | Datatype::ENUM => "greater than 0 bits", }; let data = data.to_string(); if let Some(t) = table { diff --git a/src/database/test.rs b/src/database/test.rs index 6cbc2a8..f5fae5d 100644 --- a/src/database/test.rs +++ b/src/database/test.rs @@ -4,225 +4,3 @@ //! //! #### Authorship //! - Benjamin Riley Zimmerman, 3/8/2024 (bz931@berkely.edu) - -// #[test] -// fn parse_unsigned_correctness() { -// let data1 = vec![0xDE, 0xAD, 0xBE, 0xEF]; -// let expected_parse1: u128 = 0xDEAD_BEEF_0000_0000; -// assert_eq!( -// parse_unsigned(&data1, data1.len()), -// expected_parse1 -// ); -// let data2 = vec![0x00, 0x00, 0x0F, 0xF0, 0x0F, 0x0F]; -// let expected_parse2: u128 = 0x0000_0FF0_0FF0_0000; -// assert_eq!(parse_unsigned(&data2, data2.len())); -// let data3 = vec![ -// 0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF, 0x00, -// ]; -// let expected_parse3: u128 = 0xDEAD_BEEF_DEAD_BEEF; -// assert_eq!(parse_unsigned(&data3, 128)); -// } - -// #[test] -// fn parse_unsigned_error_correctness() { -// let mut data = vec![0xDE, 0xAD, 0xBE, 0xEF]; -// let mut result = panic::catch_unwind(|| parse_unsigned(&data, -1)); -// assert!(result.is_err()); -// result = panic::catch_unwind(|| parse_unsigned(&data, 127)); -// assert!(result.is_err()); -// data = vec![0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF]; -// result = panic::catch_unwind(|| parse_unsigned(&data, 127)); -// assert!(result.is_err()); -// } - -// #[test] -// fn parse_signed_correctness() { -// let data1 = vec![0xDE, 0xAD, 0xBE, 0xEF]; -// let expected_parse1: i128 = 0xDEAD_BEEF_0000_0000 as i128; -// assert_eq!(parse_signed(&data1, data1.len), expected_parse1); -// let data2 = vec![0x00, 0x00, 0x0F, 0xF0, 0x0F, 0x0F]; -// let expected_parse2: i128 = 0x0000_0FF0_0FF0_0000 as i128; -// assert_eq!(parse_signed(&data2, data2.len)); -// let data3 = vec![ -// 0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF, 0x00, -// ]; -// let expected_parse3: i128 = 0xDEAD_BEEF_DEAD_BEEF as i128; -// assert_eq!(parse_signed(&data3, 128)); -// } - -// #[test] -// fn parse_string_correctness() { -// let data1 = vec![0x73, 0x75, 0x6D, 0x6D, 0x72, 0x73]; -// let expected_parse1: String = "summrs"; -// assert_eq!(parse_string(&data1, data1.len), expected_parse1); -// let data2 = vec![ -// 0x4C, 0x61, 0x4E, 0x61, 0x20, 0x64, 0x33, 0x31, 0x20, 0x72, 0x33, 0x59, -// ]; -// let expected_parse2: String = "LaNa d31 r3Y"; -// assert_eq!(parse_signed(&data2, data2.len)); -// let data3 = vec![ -// 0x49, 0x20, 0x67, 0x65, 0x74, 0x20, 0x24, 0x24, 0x24, -// ]; -// let expected_parse3: String = "i get $$$"; -// assert_eq!(parse_signed(&data3, data3.len)); -// } - -// #[test] -// fn parse_string_error_correctness() { -// let mut data = vec![0xDE, 0xAD, 0xBE, 0xEF]; -// let mut result = -// panic::catch_unwind(|| parse_unsigned(&data, data.len - 1)); -// assert!(result.is_err()); -// result = panic::catch_unwind(|| parse_unsigned(&data, data.len + 8)); -// assert!(result.is_err()); -// } - -// #[test] -// fn parse_f32_correctness() { -// let data1 = vec![0xDE, 0xAD, 0xBE, 0xEF]; -// let expected_parse1: f32 = -6.25985e+18; -// assert_eq!(parse_string(&data1, data1.len), expected_parse1); -// let data2 = vec![0x01, 0x23, 0x45, 0x67]; -// let expected_parse2: f32 = 2.99882e-38; -// assert_eq!(parse_signed(&data2, data2.len)); -// } - -// #[test] -// fn parse_f32_error_correctness() { -// let mut data = vec![0xDE, 0xAD, 0xBE, 0xEF]; -// let mut result = -// panic::catch_unwind(|| parse_unsigned(&data, data.len - 1)); -// assert!(result.is_err()); -// data = vec![]; -// result = panic::catch_unwind(|| parse_unsigned(&data, 32)); -// assert!(result.is_err()); -// } - -// #[test] -// fn parse_f64_correctness() { -// let data1 = vec![0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00]; -// let expected_parse1: f64 = -1.71465e+38; -// assert_eq!(parse_string(&data1, data1.len), expected_parse1); -// let data2 = vec![0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01]; -// let expected_parse2: f64 = 2.36943e-38; -// assert_eq!(parse_signed(&data2, data2.len)); -// } - -// #[test] -// fn parse_f64_error_correctness() { -// let mut data1 = vec![0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00]; -// let mut result = -// panic::catch_unwind(|| parse_unsigned(&data, data.len - 1)); -// assert!(result.is_err()); -// data = vec![]; -// result = panic::catch_unwind(|| parse_unsigned(&data, 32)); -// assert!(result.is_err()); -// } - -// #[test] -// fn parse_enum_correctness() { -// const enum_map: &[(u8, [u8; 8]); 2] = &[ -// (1, [b'A', b'R', b'G', b'_', b'1', b'\0']), -// (2, [b'A', b'R', b'G', b'_', b'2', b'\0']), -// ( -// 3, -// [b'U', b'N', b'D', b'E', b'F', b'I', b'N', b'E'], -// ), -// ]; -// let data1: &[u8] = &[1]; -// let data2: &[u8] = &[2]; -// assert_eq!(parse_enum(data1, enum_map), "ARG_1"); -// assert_eq!(parse_enum(data2, enum_map), "ARG_2"); -// } - -// #[test] -// fn parse_enum_error_correctness() { -// const enum_map: &[(u8, [u8; 8]); 2] = &[ -// (1, [b'A', b'R', b'G', b'_', b'1', b'\0']), -// (2, [b'A', b'R', b'G', b'_', b'2', b'\0']), -// ( -// 3, -// [b'U', b'N', b'D', b'E', b'F', b'I', b'N', b'E'], -// ), -// ]; -// let invalid_size_data: &[u8] = &[]; -// let invalid_variant_data: &[u8] = &[4]; -// let mut result = -// panic::catch_unwind(|| parse_unsigned(invalid_size_data, enum_map)); -// assert!(result.is_err()); -// result = -// panic::catch_unwind(|| parse_unsigned(invalid_variant_data, enum_map)); -// assert!(result.is_err()); -// } - -// /* SCHEMA TESTS */ -// #[test] -// fn test_schema_builder_empty() { -// let builder = SchemaBuilder::new(); -// let schema = builder.build(); -// assert_eq!(schema.size(), 0); -// assert!(schema.iter().next().is_none()); -// } - -// #[test] -// fn test_schema_builder_single_attribute() { -// let builder = SchemaBuilder::new() -// .add(Attribute::new("age", Datatype::UINT, 8)) -// .unwrap(); -// let schema = builder.build(); -// assert_eq!(schema.size(), 8); -// assert_eq!( -// schema -// .iter() -// .next() -// .unwrap() -// .name(), -// "age" -// ); -// } - -// #[test] -// fn test_schema_builder_multiple_attributes() { -// let builder = SchemaBuilder::new() -// .add(Attribute::new("name", Datatype::CSTR, 16)) -// .unwrap() -// .add(Attribute::new("score", Datatype::SPFP, 32)) -// .unwrap(); -// let schema = builder.build(); -// assert_eq!(schema.size(), 48); -// let mut iter = schema.iter(); -// assert_eq!(iter.next().unwrap().name(), "name"); -// assert_eq!(iter.next().unwrap().name(), "score"); -// } - -// #[test] -// #[should_panic] -// fn test_schema_builder_invalid_attribute_size() { -// let builder = SchemaBuilder::new() -// .add(Attribute::new("name", Datatype::CSTR, -1)) -// .unwrap(); -// } - -// #[test] -// #[should_panic] -// fn test_check_attribute_empty_name() { -// let existing: Vec = vec![]; -// let new_attr = Attribute::new("", Datatype::UINT, 8); -// check_attribute_validity(&existing, &new_attr).unwrap(); -// } - -// #[test] -// #[should_panic] -// fn test_check_attribute_duplicate_name() { -// let existing = vec![Attribute::new("age", Datatype::UINT, 8)]; -// let new_attr = Attribute::new("age", Datatype::SINT, 16); -// check_attribute_validity(&existing, &new_attr).unwrap(); -// } - -// #[test] -// fn test_check_attribute_valid() { -// let existing: Vec = vec![]; -// let new_attr = Attribute::new("score", Datatype::SPFP, 32); -// let result = check_attribute_validity(&existing, &new_attr); -// assert!(result.is_ok()); -// } From 788bdfe02f0f79f4b4759ac7f861cd08442a5fd3 Mon Sep 17 00:00:00 2001 From: Max Fierro Date: Mon, 6 May 2024 21:45:18 -0700 Subject: [PATCH 09/15] Significant documentation work, refactored interfaces --- src/game/crossteaser/mod.rs | 55 ++- src/game/crossteaser/variants.rs | 66 +-- src/game/mod.rs | 570 ++++++++++++++++++------- src/game/util.rs | 61 ++- src/game/zero_by/mod.rs | 127 +++--- src/game/zero_by/states.rs | 150 ++++--- src/game/zero_by/variants.rs | 15 +- src/interface/mod.rs | 95 ++++- src/interface/terminal/cli.rs | 133 +++--- src/main.rs | 27 +- src/model.rs | 10 + src/solver/algorithm/strong/acyclic.rs | 7 +- src/solver/error.rs | 21 + src/solver/mod.rs | 36 +- src/solver/record/sur.rs | 4 +- src/solver/util.rs | 65 ++- src/util.rs | 122 +----- 17 files changed, 978 insertions(+), 586 deletions(-) diff --git a/src/game/crossteaser/mod.rs b/src/game/crossteaser/mod.rs index a8af8b7..504a5ff 100644 --- a/src/game/crossteaser/mod.rs +++ b/src/game/crossteaser/mod.rs @@ -20,19 +20,22 @@ use anyhow::{Context, Result}; +use crate::game::crossteaser::variants::*; use crate::game::Bounded; use crate::game::Codec; use crate::game::Forward; -use crate::game::Game; use crate::game::GameData; +use crate::game::Information; use crate::game::Transition; +use crate::game::Variable; use crate::interface::IOMode; -use crate::interface::SolutionMode; +use crate::interface::Solution; use crate::model::database::Identifier; use crate::model::game::State; +use crate::model::game::Variant; use crate::model::solver::SUtility; use crate::solver::ClassicPuzzle; -use variants::*; +use crate::util::Identify; /* SUBMODULES */ @@ -74,34 +77,54 @@ enum Orientation { /// Represents an instance of a Crossteaser game session, which is specific to /// a valid variant of the game. pub struct Session { - variant: Option, + variant: String, length: u64, width: u64, free: u64, } -impl Game for Session { - fn new(variant: Option) -> Result { - if let Some(v) = variant { - parse_variant(v).context("Malformed game variant.") - } else { - Ok(parse_variant(VARIANT_DEFAULT.to_owned()).unwrap()) - } +impl Session { + fn new() -> Self { + parse_variant(VARIANT_DEFAULT.to_owned()) + .expect("Failed to parse default game variant.") } - fn id(&self) -> Identifier { + fn solve(&self, mode: IOMode, method: Solution) -> Result<()> { todo!() } +} + +/* INFORMATION IMPLEMENTATIONS */ - fn info(&self) -> GameData { +impl Information for Session { + fn info() -> GameData { todo!() } +} - fn solve(&self, mode: IOMode, method: SolutionMode) -> Result<()> { +impl Identify for Session { + fn id(&self) -> Identifier { todo!() } } +/* VARIANCE IMPLEMENTATION */ + +impl Variable for Session { + fn into_variant(self, variant: Option) -> Result { + if let Some(v) = variant { + parse_variant(v).context("Malformed game variant.") + } else { + parse_variant(VARIANT_DEFAULT.to_owned()) + .context("Failed to parse default game variant.") + } + } + + fn variant(&self) -> Variant { + self.variant.to_owned() + } +} + /* TRAVERSAL IMPLEMENTATIONS */ impl Transition for Session { @@ -131,13 +154,13 @@ impl Codec for Session { todo!() } - fn encode(&self, state: State) -> String { + fn encode(&self, state: State) -> Result { todo!() } } impl Forward for Session { - fn forward(&mut self, history: Vec) -> Result<()> { + fn set_verified_start(&mut self, state: State) { todo!() } } diff --git a/src/game/crossteaser/variants.rs b/src/game/crossteaser/variants.rs index 790958e..1718d16 100644 --- a/src/game/crossteaser/variants.rs +++ b/src/game/crossteaser/variants.rs @@ -36,7 +36,7 @@ pub fn parse_variant(variant: String) -> Result { check_param_count(¶ms)?; check_params_are_positive(¶ms)?; Ok(Session { - variant: Some(variant), + variant, length: params[0], width: params[1], free: params[2], @@ -111,8 +111,9 @@ fn check_params_are_positive(params: &Vec) -> Result<(), GameError> { #[cfg(test)] mod test { + use crate::game::Variable; + use super::*; - use crate::game::Game; #[test] fn variant_pattern_is_valid_regex() { @@ -127,44 +128,45 @@ mod test { #[test] fn initialization_success_with_no_variant() { - let with_none = Session::new(None); - let with_default = Session::new(Some(VARIANT_DEFAULT.to_owned())); - - assert!(with_none.is_ok()); + let _ = Session::new(); + let with_default = + Session::new().into_variant(Some(VARIANT_DEFAULT.to_owned())); assert!(with_default.is_ok()); } #[test] fn invalid_variants_fail_checks() { - let some_variant_1 = Session::new(Some("None".to_owned())); - let some_variant_2 = Session::new(Some("x4-".to_owned())); - let some_variant_3 = Session::new(Some("-".to_owned())); - let some_variant_4 = Session::new(Some("1x2-5".to_owned())); - let some_variant_5 = Session::new(Some("0x2-5".to_owned())); - let some_variant_6 = Session::new(Some("1x1-1".to_owned())); - let some_variant_7 = Session::new(Some("8x2.6-5".to_owned())); - let some_variant_8 = Session::new(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()); + let v = vec![ + Some("None".to_owned()), + Some("x4-".to_owned()), + Some("-".to_owned()), + Some("1x2-5".to_owned()), + Some("0x2-5".to_owned()), + Some("1x1-1".to_owned()), + Some("8x2.6-5".to_owned()), + Some("3x4-0".to_owned()), + ]; + + for variant in v { + assert!(Session::new() + .into_variant(variant) + .is_err()); + } } #[test] fn valid_variants_pass_checks() { - let some_variant_1 = Session::new(Some("4x3-2".to_owned())); - let some_variant_2 = Session::new(Some("5x4-2".to_owned())); - let some_variant_3 = Session::new(Some("2x4-1".to_owned())); - let some_variant_4 = Session::new(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()); + let v = vec![ + Some("4x3-2".to_owned()), + Some("5x4-2".to_owned()), + Some("2x4-1".to_owned()), + Some("4x2-1".to_owned()), + ]; + + for variant in v { + assert!(Session::new() + .into_variant(variant) + .is_ok()); + } } } diff --git a/src/game/mod.rs b/src/game/mod.rs index 4ba7b3b..8191454 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -1,24 +1,88 @@ -//! # Game Implementations Module +#![forbid(unsafe_code)] +//! # Game Module //! -//! The `games` module includes implementations for games intended to be -//! solved. To be able to solve a game with GamesmanNova, it must satisfy -//! the following characteristics/constraints: -//! * It must be reasonably "sized" (number of equivalent states). -//! * It must have states which can be efficiently represented. +//! This module provides interfaces and implementations for sequential games. +//! +//! ## Working model +//! +//! The Nova project takes an unrestricted approach to the categories of games +//! it considers, but given its focus on efficient search, it is convenient to +//! specify some common assumptions and constructs created for ergonomics. +//! +//! In particular the following choices orient the project towards discrete +//! deterministic perfect-information sequential games: +//! +//! * [`State`] is a bit-packed array of bytes used to identify +//! game states (or in other words, equivalent game histories). This is backed +//! by [`bitvec::array::BitArray`], which allows implementers to easily +//! manipulate [`State`] instances. +//! +//! * The [`Transition`] interface encodes the rules of a discrete game by +//! allowing implementers to specify which transitions between which states +//! are legal according to the underlying ruleset. +//! +//! ## Provided traits +//! +//! The [`Bounded`] interface provides a way to begin and end a traversal. Such +//! a traversal can be carried out using the methods in [`Transition`]. Families +//! of games with common logic (e.g., the same board game played on bigger or +//! smaller boards) can be expressed as "variants" of each other through the +//! [`Variable`] interface. +//! +//! The [`GameData`] struct provides a structured way to communicate information +//! about a game, which is enabled by the [`Information`] trait. Furthermore, it +//! is possible to express native concepts like variants and states through +//! encodings specified by game implementations through the [`Codec`] interface +//! where necessary. +//! +//! For more complex tasks such as end-game analysis in large board games, it +//! can be desireable to artificially change the starting position of a game +//! without incurring the algorithmic cost of computation. The [`Forward`] +//! interface provides a verifiably correct way to do this. +//! +//! ## Implementing a new game +//! +//! The overarching hope is to make implementing a new game a matter of +//! selecting which structural interfaces it can satisfy, and of implementing +//! enough of the other interfaces to give it access to other functionality +//! (such as a solving algorithm in [`crate::solver::algorithm`]). +//! +//! Here are some concrete steps you can take to realize this: +//! +//! 1. **Determine the characteristics of your game:** Ascertain whether you are +//! dealing with a chance game, a discrete game, a perfect-information game, +//! etc. If you are dealing with anything that does not fit into the current +//! working model, this is more of an infrastructure question, and you should +//! reach out to a maintainer to talk about supporting a new game category. +//! +//! 2. **Set up a code skeleton:** Create a new submodule under this one, and +//! give it the name of your game. Declare some kind of `Session` struct to +//! represent the necessary information to encode an instance of your game. +//! You should not need to mutate its state beyond initialization. +//! +//! 3. **Declare a set of interfaces:** Take a look at the provided traits, and +//! declare the ones that seem to best fit the structure of your game and +//! what you want to do with it. Reading documentation should help out a lot +//! here. +//! +//! 4. **Reference existing implementations:** To actually implement the game, +//! it will be very helpful to take a look at existing implementations. In +//! particular, take a look at the [`zero_by`] module, which is an simple +//! yet full-featured game implementation that we constantly make sure is +//! up to standard. +//! +//! 5. **Write testing modules where appropriate:** If it happens that you have +//! to implement anything that requires non-trivial logic, you should make +//! sure to test it. This includes any kind of verification of encodings. +//! Taking a look at existing unit tests will help significantly. //! //! #### Authorship //! - Max Fierro, 4/6/2023 (maxfierro@berkeley.edu) //! - Ishir Garg, 4/1/2024 (ishirgarg@berkeley.edu) -use anyhow::Result; +use anyhow::{Context, Result}; -use crate::{ - interface::{IOMode, SolutionMode}, - model::{ - database::Identifier, - game::{State, DEFAULT_STATE_BYTES}, - }, -}; +use crate::model::game::{State, Variant, DEFAULT_STATE_BYTES}; /* UTILITY MODULES */ @@ -38,24 +102,39 @@ pub mod zero_by; /* DEFINITIONS */ -/// Contains useful data about a game, intended to provide users of the program -/// information they can use to understand the output of solving algorithms, -/// in addition to specifying game variants. +/// Contains useful data about a game. +/// +/// The information here is intended to provide users of the program information +/// they can use to understand the output of solving algorithms, in addition to +/// specifying formats/protocols for communicating with game implementations, +/// and providing descriptive error outputs. See [`Information::info`] for how +/// to expose this information. +/// +/// # Example +/// +/// ```none +/// * Name: "zero-by" +/// * Authors: "John Doe , Jane Doe " +/// * About: "Zero By is a multiplayer zero-sum game where N players ..." +/// * Variant protocol: "Three or more dash-separated strings, where..." +/// * Variant pattern: r"^[1-9]\d*(?:-[1-9]\d*)+$" +/// * Variant default: "2-10-1-2" +/// * State protocol: "The state string should be two dash-separated ..." +/// * State pattern: r"^\d+-\d+$" +/// * State default: "10-0" +/// ``` pub struct GameData { - /* INSTANCE */ - /// The variant string used to initialize the `Game` instance which returned - /// this `GameData` object from its `info` associated method. - pub variant: String, - /* GENERAL */ /// Known name for the game. This should return a string that can be used as /// a command-line argument to the CLI endpoints which require a game name /// as a target (e.g. `nova solve `). pub name: &'static str, + /// The names of the people who implemented the game listed out, optionally /// including their contact. For example: "John Doe , /// Ricardo L. , Quin Bligh". pub authors: &'static str, + /// General introduction to the game's rules and setup, including any facts /// that are interesting about it. pub about: &'static str, @@ -64,169 +143,362 @@ pub struct GameData { /// Explanation of how to use strings to communicate which variant a user /// wishes to play to the game's implementation. pub variant_protocol: &'static str, + /// Regular expression pattern that all variant strings must match. pub variant_pattern: &'static str, + /// Default variant string to be used when none is specified. pub variant_default: &'static str, /* STATES */ /// Explanation of how to use a string to encode a game state. pub state_protocol: &'static str, + /// Regular expression pattern that all state encodings must match. pub state_pattern: &'static str, + /// Default state encoding to be used when none is specified. pub state_default: &'static str, } /* INTERFACES */ -/// Defines miscellaneous behavior of a deterministic economic game object. Note -/// that player count is arbitrary; puzzles are semantically one-player games, -/// although they are more alike to optimization problems than cases of games. -pub trait Game { - /// Returns `Result::Ok(Self)` if the specified `variant` is not malformed. - /// Otherwise, returns a `Result::Err(String)` containing a text string - /// explaining why it could not be parsed. - fn new(variant: Option) -> Result - where - Self: Sized; - - /// Returns an ID unique to this game. The return value should be consistent - /// across calls from the same game and variant, but differing across calls - /// from different games and variants. As such, it can be thought of as a - /// string hash whose input is the game and variant (although it is not at - /// all necessary that it conforms to any measure of hashing performance). - fn id(&self) -> Identifier; - - /// Returns useful information about the game, such as the type of game it - /// is, who implemented it, and an explanation of how to specify different - /// variants for initialization. - fn info(&self) -> GameData; - - /// Runs a solving algorithm which consumes the callee, generating side - /// effects specified by the `mode` parameter. This should return an error - /// if solving the specific game variant is not supported (among other - /// possibilities for an error), and a unit type if everything goes per - /// specification. See `IOMode` for specifics on intended side effects. - fn solve(&self, mode: IOMode, method: SolutionMode) -> Result<()>; +/// Provides a method to obtain information about a game. +pub trait Information { + /// Provides a way to retrieve useful information about a game for both + /// internal and user-facing modules. + /// + /// The information included here should be broadly applicable to any + /// variant of the underlying game type (hence why it is a static method). + /// For specifics on the information to provide, see [`GameData`]. + /// + /// # Example + /// + /// Using the game [`zero_by`] as an example: + /// + /// ``` + /// use crate::game::zero_by; + /// let game = zero_by::Session::new(); + /// assert_eq!(game.info().name, "zero-by"); + /// ``` + fn info() -> GameData; } -/// Provides a way to retrieve a unique starting state from which to begin a -/// traversal, and a way to tell when a traversal can no longer continue from -/// a state. This does not necessarily imply that the underlying structure being -/// traversed over is finite; just that there exist finite traversals over it. -/// Generic over a state type **S**. -/// -/// ## Explanation -/// -/// In the example of games, there often exist ways to arrange their elements -/// in a way that unexpectedly invalidates game state. For example, there is no -/// valid game of chess with no kings remaining on the board. However, the most -/// intuitive implementations of `Transition` interfaces would not bat an eye at -/// this, and would simply return more states without any kings (this is one of -/// the more obvious examples of an invalid state, but there are subtler ones). -/// -/// In addition, not all valid states may be reachable from other valid states. -/// For example, the empty board of Tic Tac Toe is not reachable from a board -/// with any number of pieces on the board. In some games, though, these states -/// become valid by simply changing the starting state (which is within the -/// realm of game variants). For example, in the game 10 to 0 by 1 or 3, it is -/// not valid to have a state of 8, but it becomes valid when the starting state -/// is made to be 11. A similar line of reasoning applies to end states. -/// -/// These facts motivate that the logic which determines the starting and ending -/// states of games should be independent of the logic that transitions from -/// valid states to other valid states. +/// Provides a method of bounding exploration of game states. pub trait Bounded { - /// Returns the starting state of the underlying structure. This is used to - /// deterministically initialize a traversal. + /// Returns the starting state of the underlying game. + /// + /// Starting states are usually determined by game variants, but it is + /// possible to alter them while remaining in the same game variant through + /// the [`Forward`] interface. Such antics are necessary to ensure state + /// validity at a variant-specific level. See [`Forward::forward`] for more. + /// + /// # Example + /// + /// Using the game [`zero_by`] with default state `"10-0"`: + /// + /// ``` + /// use crate::game::zero_by; + /// let game = zero_by::Session::new(); + /// assert_eq!(game.encode(game.start())?, "10-0".into()); + /// ``` fn start(&self) -> State; - /// Returns true if and only if there are no possible transitions from the - /// provided `state`. Inputting an invalid `state` is undefined behavior. + /// Returns true if `state` is a terminal state of the underlying game. + /// + /// Note that this function could return `true` for an invalid `state`, so + /// it is recommended that consumers verify that `state` is reachable in the + /// first place through a traversal interface (see [`Transition`]). + /// + /// # Example + /// + /// Using the game [`zero_by`] as an example, which ends at any state with + /// zero elements left: + /// + /// ``` + /// use crate::game::zero_by; + /// let game = zero_by::Session::new(); + /// assert!(game.end(game.decode("0-0")?)); + /// ``` fn end(&self, state: State) -> bool; } -/// Defines behavior to encode and decode a state type **S** to and from a -/// `String`. This is related to the `GameData` object, which should contain -/// information about how game states can be represented using a string. -/// -/// ## Explanation -/// -/// Efficient game state hashes are rarely intuitive to understand due to being -/// highly optimized. Providing a way to transform them to and from a string -/// gives a representation that is easier to understand. This, in turn, can be -/// used throughout the project's interfaces to do things like fast-forwarding -/// a game to a user-provided state, providing readable debug output, etc. -/// -/// Note that this is not supposed to provide a "fancy" printable game board -/// drawing; a lot of the utility obtained from implementing this interface is -/// having access to understandable yet compact game state representations. As -/// a rule of thumb, all strings should be single-lined and have no whitespace. +/// Provides methods to encode and decode bit-packed [`State`] instances to +/// and from [`String`]s to facilitate manual interfaces. pub trait Codec { - /// Transforms a string representation of a game state into a type **S**. - /// The `string` representation should conform to the `state_protocol` - /// specified in the `GameData` object returned by `Game::info`. If it does - /// not, an error containing a message with a brief explanation on what is - /// wrong with `string` should be returned. + /// Decodes a game `string` encoding into a bit-packed [`State`]. + /// + /// This function (and [`Codec::encode`]) effectively specifies a protocol + /// for turning a [`String`] into a [`State`]. See [`Information::info`] + /// to make this protocol explicit. + /// + /// # Example + /// + /// Using the game [`zero_by`] with default state of `"10-0"`: + /// + /// ``` + /// use crate::game::zero_by; + /// let default_variant = zero_by::Session::new(); + /// assert_eq!( + /// default_variant.decode("10-0".into())?, + /// default_variant.start() + /// ); + /// ``` + /// + /// # Errors + /// + /// Fails if `state` is detectably invalid or unreachable in the underlying + /// game variant. fn decode(&self, string: String) -> Result>; - /// Transforms a game state type **S** into a string representation. The - /// string returned should conform to the `state_protocol` specified in the - /// `GameData` object returned by `Game::info`. If the `state` is malformed, - /// this function should panic with a useful debug message. No two `state`s - /// should return the same string representation (ideally). - fn encode(&self, state: State) -> String; + /// Encodes a game `state` into a compact string representation. + /// + /// The output representation is not designed to be space efficient. It is + /// used for manual input/output. This function (and [`Codec::decode`]) + /// effectively specifies a protocol for translating a [`State`] into + /// a [`String`]. See [`Information::info`] to make this protocol explicit. + /// + /// # Example + /// + /// Using the game [`zero_by`] with a default state of `"10-0"`: + /// + /// ``` + /// use crate::game::zero_by; + /// let default_variant = zero_by::Session::new(); + /// assert_eq!( + /// default_variant.encode(default_variant.start())?, + /// "10-0".into() + /// ); + /// ``` + /// + /// # Errors + /// + /// Fails if `state` is detectably invalid or unreachable in the underlying + /// game variant. + fn encode(&self, state: State) -> Result; } -/// Provides a way to fast-forward a game state from its starting state (as -/// defined by `Bounded::start`) to a future state by playing a sequence of -/// string-encoded state transitions one after another. Generic over a state -/// type **S**. -/// -/// # Explanation -/// -/// For certain purposes, it is useful to skip a small or big part of a game's -/// states to go straight to exploring a subgame of interest, or because the -/// game is simply too large to explore in its entirety. In order to skip to -/// this part of a game, a valid state in that subgame must be provided. -/// -/// Since it is generally impossible to verify that a given state is reachable -/// from the start of a game, it is necessary to demand a sequence of states -/// that begin in a starting state and end in the desired state, such that each -/// transition between states is valid per the game's ruleset. +/// Provides methods to obtain a working instance of a game variant and to +/// retrieve a [`String`]-encoded specification of the variant. +pub trait Variable { + /// Returns a version of the underlying game as the specified `variant`, + /// resetting to the default variant if it is `None`. + /// + /// This does not preserve any kind of state held by the game object, + /// including the starting state (even if it was set through [`Forward`]). + /// In this sense, game variants specify starting states. + /// + /// # Example + /// + /// Consider the following example on a game of [`zero_by`], which has a + /// default starting state encoding of `"10-0"`: + /// + /// ``` + /// use crate::game::zero_by; + /// + /// let state = "100-0".into(); + /// let variant = "3-100-3-4".into(); + /// + /// let default_variant = zero_by::Session::new(); + /// assert_ne!(default_variant.encode(default_variant.start())?, state); + /// + /// let custom_variant = session.into_variant(variant)?; + /// assert_eq!(custom_variant.encode(custom_variant.start())?, state); + /// ``` + /// + /// # Errors + /// + /// Fails if `variant` does not conform to the game's protocol for encoding + /// variants as strings, or if the game does not support variants in the + /// first place (but has a placeholder [`Variable`] implementation). + fn into_variant(self, variant: Option) -> Result + where + Self: Sized; + + /// Returns a string representing the underlying game variant. + /// + /// This does not provide a certain way of differentiating between the + /// starting state of the game (see [`Bounded::start`] for this), but it + /// does provide a sufficient identifier of the game's structure. + /// + /// # Example + /// + /// Consider the following example on a game of [`zero_by`], which has the + /// default variant of `"2-10-1-2"`: + /// + /// ``` + /// use crate::game::zero_by; + /// + /// let variant = "3-100-3-4".into(); + /// let default_variant = zero_by::Session::new(); + /// assert_eq!(default_variant.variant(), "2-10-1-2".into()); + /// + /// let custom_variant = session.into_variant(variant.clone())?; + /// assert_eq!(custom_variant.variant(), variant); + /// ``` + fn variant(&self) -> Variant; +} + +/// Provides methods to safely fast-forward the starting state of a game to +/// a desired state in the future. pub trait Forward where - Self: Bounded + Codec, + Self: Information + Bounded + Codec + Transition + Sized, { - /// Advances the game's starting state to the last state in `history`. All - /// all of the `String`s in `history` must conform to the `state_protocol` - /// defined in the `GameData` object returned by `info`. The states in - /// `history` should be verified by ensuring that the following is true: - /// - /// - `history[0]` is the start state specified by the game variant. - /// - The set `transition(history[i])` contains `history[i + 1]`. - /// - /// If these conditions are not satisfied, this function should return a - /// useful error containing information about why the provided `history` - /// is not possible for the game variant. Otherwise, it should mutate `self` - /// to have a starting state whose string encoding is `history.pop()`. - fn forward(&mut self, history: Vec) -> Result<()>; + /// Sets the game's starting state to a pre-verified `state`. + /// + /// This function is an auxiliary item for [`Forward::forward`]. While it + /// needs to be implemented for [`Forward::forward`] to work, there should + /// never be a need to call this directly from any other place. This would + /// produce potentially incorrect behavior, as it is not possible to verify + /// whether a state encoding is valid statically (in the general case). + /// + /// # Deprecated + /// + /// This function is marked deprecated to discourage direct usage, not + /// because it is an actually deprecated interface item. + /// + /// # Example + /// + /// Using the game [`zero_by`] with a default state of `"10-0"`: + /// + /// ``` + /// use crate::game::zero_by; + /// + /// let mut game = zero_by::Session::new(); + /// let start = game.decode("9-1".into())?; + /// game.set_verified_start(start); + /// + /// assert_eq!(forwarded.encode(game.start)?, "9-1".into()); + /// ``` + #[deprecated( + note = "This function should not be used directly; any modification of \ + initial states should be done through [`Forward::forward`], which is \ + fallible and provides verification for game states." + )] + fn set_verified_start(&mut self, state: State); + + /// Advances the game's starting state to the last state in `history`. + /// + /// This function needs an implementation of [`Forward::set_verified_start`] + /// to ultimately change the starting state after `history` is verified. It + /// consumes `self` with the intent of making it difficult to make errors + /// regarding variant incoherency, and to make the semantics clearer. + /// + /// # Example + /// + /// Using the game [`zero_by`] with a default state of `"10-0"`: + /// + /// ``` + /// use crate::game::zero_by; + /// + /// let mut game = zero_by::Session::new(); + /// let history = vec![ + /// "10-0".into(), + /// "9-1".into(), + /// "8-0".into(), + /// "6-1".into(), + /// ]; + /// + /// let forwarded = game.forward(history)?; + /// assert_eq!(forwarded.encode(forwarded.start())?, "6-1".into()); + /// ``` + /// + /// # Errors + /// + /// Here are some of the reasons this could fail: + /// * `history` is empty. + /// * A state encoding in `history` is not valid. + /// * The provided `history` plays beyond a terminal state. + /// * `history` begins at a state other than the variant's starting state. + /// * An invalid transition is made between subsequent states in `history`. + #[allow(deprecated)] + fn forward(mut self, history: Vec) -> Result { + let to = util::verify_state_history(&self, history) + .context("Specified invalid state history.")?; + self.set_verified_start(to); + Ok(self) + } } -/// TODO +/// Provides methods to obtain game state transitions, enabling state search. pub trait Transition { - /// Given a `state` at time `t`, returns all states that are possible at - /// time `t + 1`. This should only guarantee that if `state` is feasible and - /// not an end state, then all returned states are also feasible; therefore, - /// inputting an invalid or end `state` is undefined behavior. The order of - /// the values returned is insignificant. + /// Returns all possible legal states that could follow `state`. + /// + /// In a discrete game, we represent points in history that have equivalent + /// strategic value using a [`State`] encoding. This is a + /// bit-packed representation of the state of the game at a point in time + /// (up to whatever attributes we may care about). This function returns the + /// collection of all states that could follow `state` according to the + /// underlying game's rules. + /// + /// # Example + /// + /// Using the game [`zero_by`], whose default variant involves two players + /// alternate turns removing items from a pile that starts out with 10 items + /// (where Player 0 starts), we can provide the following example: + /// + /// ``` + /// use crate::game::zero_by; + /// + /// let mut game = zero_by::Session::new(); + /// let possible_next_states = vec![ + /// "9-1".into(), // 9 items left, player 1's turn + /// "8-1".into(), // 8 items left, player 1's turn + /// ]; + /// + /// assert_eq!(game.prograde(game.start()), possible_next_states); + /// ``` + /// + /// # Warning + /// + /// In practice, it is extremely difficult to make it impossible for this + /// function to always return an empty collection if `state` is invalid, as + /// it is hard to statically verify the validity of a state. Hence, this + /// behavior is only guaranteed when `state` is valid. See [`Bounded::end`] + /// and [`Bounded::start`] to bound exploration to only valid states. fn prograde(&self, state: State) -> Vec>; - /// Given a `state` at time `t`, returns all states that are possible at - /// time `t - 1`. This should only guarantee that if `state` is feasible, - /// then all returned states are also feasible; therefore, inputting an - /// invalid `state` is undefined behavior. The order of the values returned - /// is insignificant. + /// Returns all possible legal states that could have come before `state`. + /// + /// In a discrete game, we represent points in history that have equivalent + /// strategic value using a [`State`] encoding. This is a + /// bit-packed representation of the state of the game at a point in time + /// (up to whatever attributes we may care about). This function returns the + /// collection of all states that could have preceded `state` according to + /// the underlying game's rules. + /// + /// # Example + /// + /// Using the game [`zero_by`], whose default variant involves two players + /// alternate turns removing items from a pile that starts out with 10 items + /// (where Player 0 starts), we can provide the following example: + /// + /// ``` + /// use crate::game::zero_by; + /// + /// // Get state with 8 items left and player 1 to move + /// let mut game = zero_by::Session::new(); + /// let state = game.decode("8-1".into())?; + /// + /// let possible_previous_states = vec![ + /// "9-0".into(), // 9 items left, player 0's turn (invalid state) + /// "10-0".into(), // 8 items left, player 0's turn + /// ]; + /// + /// assert_eq!(game.retrograde(state), possible_previous_states); + /// ``` + /// + /// # Warning + /// + /// As you can see from the example, this function provides no guarantees + /// about the validity of the states that it returns, because in the general + /// case, it is impossible to verify whether or not a preceding state is + /// actually valid. + /// + /// This obstacle is usually overcome by keeping track of observed states + /// through a prograde exploration (using [`Transition::prograde`] and the + /// functions provided by [`Bounded`]), and cross-referencing the outputs of + /// this function with those observed states to validate them. fn retrograde(&self, state: State) -> Vec>; } diff --git a/src/game/util.rs b/src/game/util.rs index bb4dae4..298a7f2 100644 --- a/src/game/util.rs +++ b/src/game/util.rs @@ -8,25 +8,20 @@ use anyhow::{Context, Result}; +use crate::game::Information; use crate::{ - game::{error::GameError, Bounded, Codec, Game, Transition}, + game::{error::GameError, Bounded, Codec, Transition}, model::game::State, }; /* STATE HISTORY VERIFICATION */ -/// Returns the latest state in a sequential `history` of state string encodings -/// by verifying that the first state in the history is the same as the `game`'s -/// start and that each state can be reached from its predecessor through the -/// `game`'s transition function. If these conditions are not met, it returns an -/// error message signaling the pair of states that are not connected by the -/// transition function, with a reminder of the current game variant. -pub fn verify_history_dynamic( +pub fn verify_state_history( game: &G, history: Vec, ) -> Result> where - G: Game + Codec + Bounded + Transition, + G: Information + Bounded + Codec + Transition, { if let Some(s) = history.first() { let mut prev = game.decode(s.clone())?; @@ -35,28 +30,32 @@ where let next = game.decode(history[i].clone())?; let transitions = game.prograde(prev); if !transitions.contains(&next) { - return transition_history_error(game, prev, next); + return transition_history_error(game, prev, next) + .context("Specified invalid state transition."); } prev = next; } Ok(prev) } else { start_history_error(game, game.start()) + .context("Specified invalid first state.") } } else { - empty_history_error(game) + empty_history_error::() + .context("Provided state history is empty.") } } -fn empty_history_error(game: &G) -> Result> +/* HISTORY VERIFICATION ERRORS */ + +fn empty_history_error() -> Result> where - G: Game + Codec, + G: Information + Codec + Bounded, { Err(GameError::InvalidHistory { - game_name: game.info().name, + game_name: G::info().name, hint: format!("State history must contain at least one state."), - }) - .context("Invalid game history.") + })? } fn start_history_error( @@ -64,18 +63,16 @@ fn start_history_error( start: State, ) -> Result> where - G: Game + Codec, + G: Information + Codec + Bounded, { Err(GameError::InvalidHistory { - game_name: game.info().name, + game_name: G::info().name, hint: format!( - "The state history must begin with the starting state for this \ - variant ({}), which is {}.", - game.info().variant, - game.encode(start) + "The state history must begin with the starting state for the \ + provided game variant, which is {}.", + game.encode(start)? ), - }) - .context("Invalid game history.") + })? } fn transition_history_error( @@ -84,17 +81,15 @@ fn transition_history_error( next: State, ) -> Result> where - G: Game + Codec, + G: Information + Codec + Bounded, { Err(GameError::InvalidHistory { - game_name: game.info().name, + game_name: G::info().name, hint: format!( - "Transitioning from the state '{}' to the sate '{}' is \ - illegal in the current game variant ({}).", - game.encode(prev), - game.encode(next), - game.info().variant + "Transitioning from the state '{}' to the sate '{}' is illegal in \ + the provided game variant.", + game.encode(prev)?, + game.encode(next)?, ), - }) - .context("Invalid game history.") + })? } diff --git a/src/game/zero_by/mod.rs b/src/game/zero_by/mod.rs index 651dec3..2c7acdd 100644 --- a/src/game/zero_by/mod.rs +++ b/src/game/zero_by/mod.rs @@ -14,18 +14,22 @@ use anyhow::{Context, Result}; use bitvec::field::BitField; -use states::*; use crate::game::error::GameError; +use crate::game::zero_by::states::*; use crate::game::zero_by::variants::*; -use crate::game::{util, Bounded, Codec, Forward}; -use crate::game::{Game, GameData, Transition}; -use crate::interface::{IOMode, SolutionMode}; +use crate::game::Information; +use crate::game::Variable; +use crate::game::{Bounded, Codec, Forward}; +use crate::game::{GameData, Transition}; +use crate::interface::{IOMode, Solution}; use crate::model::database::Identifier; +use crate::model::game::Variant; use crate::model::game::{Player, PlayerCount, State}; use crate::model::solver::SUtility; use crate::solver::algorithm::strong; use crate::solver::{Extensive, SimpleUtility}; +use crate::util::Identify; /* SUBMODULES */ @@ -56,27 +60,54 @@ pub struct Session { start_state: State, player_bits: usize, players: PlayerCount, - variant: String, + variant: Variant, by: Vec, } -impl Game for Session { - fn new(variant: Option) -> Result { - if let Some(v) = variant { - parse_variant(v).context("Malformed game variant.") - } else { - Ok(parse_variant(VARIANT_DEFAULT.to_owned()).unwrap()) +impl Session { + pub fn new() -> Self { + parse_variant(VARIANT_DEFAULT.to_owned()).unwrap() + } + + pub fn solve(&self, mode: IOMode, method: Solution) -> Result<()> { + match (self.players, method) { + (2, Solution::Strong) => { + strong::acyclic::solver::<2, 8, Self>(self, mode) + .context("Failed solver run.")? + }, + (10, Solution::Strong) => { + strong::acyclic::solver::<10, 8, Self>(self, mode) + .context("Failed solver run.")? + }, + _ => { + return Err(GameError::SolverNotFound { + input_game_name: NAME, + }) + .context("Solver not found."); + }, } + Ok(()) } - fn id(&self) -> Identifier { - todo!() + fn encode_state(&self, turn: Player, elements: Elements) -> State { + let mut state = State::ZERO; + state[..self.player_bits].store_be(turn); + state[self.player_bits..].store_be(elements); + state } - fn info(&self) -> GameData { - GameData { - variant: self.variant.clone(), + fn decode_state(&self, state: State) -> (Player, Elements) { + let player = state[..self.player_bits].load_be::(); + let elements = state[self.player_bits..].load_be::(); + (player, elements) + } +} + +/* INFORMATION IMPLEMENTATIONS */ +impl Information for Session { + fn info() -> GameData { + GameData { name: NAME, authors: AUTHORS, about: ABOUT, @@ -90,25 +121,28 @@ impl Game for Session { state_protocol: STATE_PROTOCOL, } } +} - fn solve(&self, mode: IOMode, method: SolutionMode) -> Result<()> { - match (self.players, method) { - (2, SolutionMode::Strong) => { - strong::acyclic::solver::<2, 8, Self>(self, mode) - .context("Failed solver run.")? - }, - (10, SolutionMode::Strong) => { - strong::acyclic::solver::<10, 8, Self>(self, mode) - .context("Failed solver run.")? - }, - _ => { - return Err(GameError::SolverNotFound { - input_game_name: NAME, - }) - .context("Solver not found."); - }, +impl Identify for Session { + fn id(&self) -> Identifier { + todo!() + } +} + +/* VARIANCE IMPLEMENTATION */ + +impl Variable for Session { + fn into_variant(self, variant: Option) -> Result { + if let Some(v) = variant { + parse_variant(v).context("Malformed game variant.") + } else { + parse_variant(VARIANT_DEFAULT.to_owned()) + .context("Failed to parse default game variant.") } - Ok(()) + } + + fn variant(&self) -> Variant { + self.variant.clone() } } @@ -170,17 +204,15 @@ impl Codec for Session { Ok(parse_state(&self, string)?) } - fn encode(&self, state: State) -> String { + fn encode(&self, state: State) -> Result { let (turn, elements) = self.decode_state(state); - format!("{}-{}", elements, turn) + Ok(format!("{}-{}", elements, turn)) } } impl Forward for Session { - fn forward(&mut self, history: Vec) -> Result<()> { - self.start_state = util::verify_history_dynamic(self, history) - .context("Malformed game state encoding.")?; - Ok(()) + fn set_verified_start(&mut self, state: State) { + self.start_state = state; } } @@ -201,20 +233,3 @@ impl SimpleUtility for Session { payoffs } } - -/* UTILITY FUNCTIONS */ - -impl Session { - fn encode_state(&self, turn: Player, elements: Elements) -> State { - let mut state = State::ZERO; - state[..self.player_bits].store_be(turn); - state[self.player_bits..].store_be(elements); - state - } - - fn decode_state(&self, state: State) -> (Player, Elements) { - let player = state[..self.player_bits].load_be::(); - let elements = state[self.player_bits..].load_be::(); - (player, elements) - } -} diff --git a/src/game/zero_by/states.rs b/src/game/zero_by/states.rs index 60eb937..736f5de 100644 --- a/src/game/zero_by/states.rs +++ b/src/game/zero_by/states.rs @@ -10,13 +10,12 @@ use regex::Regex; use crate::game::error::GameError; +use crate::game::zero_by::Elements; use crate::game::zero_by::Session; use crate::game::zero_by::NAME; use crate::model::game::Player; use crate::model::game::State; -use super::Elements; - /* ZERO-BY STATE ENCODING */ pub const STATE_DEFAULT: &'static str = "10-0"; @@ -126,7 +125,7 @@ fn check_variant_coherence( mod test { use super::*; - use crate::game::{util::verify_history_dynamic, Game}; + use crate::game::*; /* STATE STRING PARSING */ @@ -143,8 +142,8 @@ mod test { #[test] fn no_state_equals_default_state() { - let with_none = Session::new(None).unwrap(); - let with_default = Session::new(None).unwrap(); + let with_none = Session::new(); + let with_default = Session::new(); assert_eq!( with_none.start_state, @@ -164,7 +163,7 @@ mod test { fn f() -> Session { // 2-player 10-to-zero by 1 or 2 - Session::new(None).unwrap() + Session::new() } assert!(parse_state(&f(), s1).is_err()); @@ -187,7 +186,7 @@ mod test { let s7 = "1-0".to_owned(); fn f() -> Session { - Session::new(None).unwrap() + Session::new() } assert!(parse_state(&f(), s1).is_ok()); @@ -200,7 +199,7 @@ mod test { } #[test] - fn compatible_variants_and_states_pass_checks() { + fn compatible_variants_and_states_pass_checks() -> Result<()> { let v1 = "50-10-12-1-4"; let v2 = "5-100-6-2-7"; let v3 = "10-200-1-5"; @@ -209,21 +208,18 @@ mod test { let s2 = "150-9".to_owned(); let s3 = "200-0".to_owned(); - fn f(v: &str) -> Session { - Session::new(Some(v.to_owned())).unwrap() - } - - assert!(parse_state(&f(v1), s1.clone()).is_ok()); - assert!(parse_state(&f(v1), s2.clone()).is_err()); - assert!(parse_state(&f(v1), s3.clone()).is_err()); + assert!(parse_state(&variant(v1)?, s1.clone()).is_ok()); + assert!(parse_state(&variant(v1)?, s2.clone()).is_err()); + assert!(parse_state(&variant(v1)?, s3.clone()).is_err()); - assert!(parse_state(&f(v2), s1.clone()).is_ok()); - assert!(parse_state(&f(v2), s2.clone()).is_err()); - assert!(parse_state(&f(v2), s3.clone()).is_err()); + assert!(parse_state(&variant(v2)?, s1.clone()).is_ok()); + assert!(parse_state(&variant(v2)?, s2.clone()).is_err()); + assert!(parse_state(&variant(v2)?, s3.clone()).is_err()); - assert!(parse_state(&f(v3), s1.clone()).is_ok()); - assert!(parse_state(&f(v3), s2.clone()).is_ok()); - assert!(parse_state(&f(v3), s3.clone()).is_ok()); + assert!(parse_state(&variant(v3)?, s1.clone()).is_ok()); + assert!(parse_state(&variant(v3)?, s2.clone()).is_ok()); + assert!(parse_state(&variant(v3)?, s3.clone()).is_ok()); + Ok(()) } /* GAME HISTORY VERIFICATION */ @@ -239,14 +235,37 @@ mod test { let i7: Vec<&str> = vec![]; // No history let i8 = vec![""]; // Empty string - assert!(verify_history_dynamic(&session(None), owned(i1)).is_err()); - assert!(verify_history_dynamic(&session(None), owned(i2)).is_err()); - assert!(verify_history_dynamic(&session(None), owned(i3)).is_err()); - assert!(verify_history_dynamic(&session(None), owned(i4)).is_err()); - assert!(verify_history_dynamic(&session(None), owned(i5)).is_err()); - assert!(verify_history_dynamic(&session(None), owned(i6)).is_err()); - assert!(verify_history_dynamic(&session(None), owned(i7)).is_err()); - assert!(verify_history_dynamic(&session(None), owned(i8)).is_err()); + assert!(Session::new() + .forward(owned(i1)) + .is_err()); + + assert!(Session::new() + .forward(owned(i2)) + .is_err()); + + assert!(Session::new() + .forward(owned(i3)) + .is_err()); + + assert!(Session::new() + .forward(owned(i4)) + .is_err()); + + assert!(Session::new() + .forward(owned(i5)) + .is_err()); + + assert!(Session::new() + .forward(owned(i6)) + .is_err()); + + assert!(Session::new() + .forward(owned(i7)) + .is_err()); + + assert!(Session::new() + .forward(owned(i8)) + .is_err()); } #[test] @@ -258,19 +277,34 @@ mod test { let c5 = vec!["10-0", "9-1"]; let c6 = vec!["10-0"]; - assert!(verify_history_dynamic(&session(None), owned(c1)).is_ok()); - assert!(verify_history_dynamic(&session(None), owned(c2)).is_ok()); - assert!(verify_history_dynamic(&session(None), owned(c3)).is_ok()); - assert!(verify_history_dynamic(&session(None), owned(c4)).is_ok()); - assert!(verify_history_dynamic(&session(None), owned(c5)).is_ok()); - assert!(verify_history_dynamic(&session(None), owned(c6)).is_ok()); + assert!(Session::new() + .forward(owned(c1)) + .is_ok()); + + assert!(Session::new() + .forward(owned(c2)) + .is_ok()); + + assert!(Session::new() + .forward(owned(c3)) + .is_ok()); + + assert!(Session::new() + .forward(owned(c4)) + .is_ok()); + + assert!(Session::new() + .forward(owned(c5)) + .is_ok()); + + assert!(Session::new() + .forward(owned(c6)) + .is_ok()); } #[test] - fn verify_zero_by_history_compatibility() { - fn v() -> Option { - Some(format!("8-200-30-70-15-1")) - } + fn verify_zero_by_history_compatibility() -> Result<()> { + let v = "8-200-30-70-15-1"; let c1 = vec![ "200-0", "185-1", "115-2", "114-3", "113-4", "83-5", "82-6", @@ -281,29 +315,47 @@ mod test { "110-7", "80-0", "79-1", ]; - assert!(verify_history_dynamic(&session(v()), owned(c1)).is_ok()); - assert!(verify_history_dynamic(&session(v()), owned(c2)).is_ok()); + assert!(&variant(v)? + .forward(owned(c1)) + .is_ok()); + + assert!(&variant(v)? + .forward(owned(c2)) + .is_ok()); let i1 = vec!["200-0", "184-1", "115-2", "114-3"]; // Illegal move let i2 = vec!["200-0", "185-1", "115-1", "114-2"]; // Turns don't switch let i3 = vec!["200-2", "185-3", "115-4", "114-5"]; // Bad initial turn let i4 = vec!["201-0", "186-1", "116-2", "115-3"]; // Bad initial state - assert!(verify_history_dynamic(&session(v()), owned(i1)).is_err()); - assert!(verify_history_dynamic(&session(v()), owned(i2)).is_err()); - assert!(verify_history_dynamic(&session(v()), owned(i3)).is_err()); - assert!(verify_history_dynamic(&session(v()), owned(i4)).is_err()); + assert!(&variant(v)? + .forward(owned(i1)) + .is_err()); + + assert!(&variant(v)? + .forward(owned(i2)) + .is_err()); + + assert!(&variant(v)? + .forward(owned(i3)) + .is_err()); + + assert!(&variant(v)? + .forward(owned(i4)) + .is_err()); + + Ok(()) } /* UTILITIES */ - fn session(v: Option) -> Session { - Session::new(v).unwrap() + fn variant(v: &str) -> Result { + Session::new().into_variant(Some(v.to_string())) } - fn owned(v: Vec<&str>) -> Vec { + fn owned(v: Vec<&'static str>) -> Vec { v.iter() - .map(|&s| s.to_owned()) + .map(|s| s.to_string()) .collect() } } diff --git a/src/game/zero_by/variants.rs b/src/game/zero_by/variants.rs index 45facf4..2ab2336 100644 --- a/src/game/zero_by/variants.rs +++ b/src/game/zero_by/variants.rs @@ -133,7 +133,7 @@ fn parse_player_count(params: &Vec) -> Result { mod test { use super::*; - use crate::game::Game; + use crate::game::*; #[test] fn variant_pattern_is_valid_regex() { @@ -148,20 +148,21 @@ mod test { #[test] fn initialization_success_with_no_variant() { - let with_none = Session::new(None); - let with_default = Session::new(Some(VARIANT_DEFAULT.to_owned())); - assert!(with_none.is_ok()); + let _ = Session::new(); + let with_default = + Session::new().into_variant(Some(VARIANT_DEFAULT.to_owned())); assert!(with_default.is_ok()); } #[test] - fn no_variant_equals_default_variant() { - let with_none = Session::new(None).unwrap(); + fn no_variant_equals_default_variant() -> Result<()> { + let with_none = Session::new(); let with_default = - Session::new(Some(VARIANT_DEFAULT.to_owned())).unwrap(); + Session::new().into_variant(Some(VARIANT_DEFAULT.to_owned()))?; assert_eq!(with_none.variant, with_default.variant); assert_eq!(with_none.start_state, with_default.start_state); assert_eq!(with_none.by, with_default.by); + Ok(()) } #[test] diff --git a/src/interface/mod.rs b/src/interface/mod.rs index 77815d3..b65cdb5 100644 --- a/src/interface/mod.rs +++ b/src/interface/mod.rs @@ -23,18 +23,26 @@ pub mod terminal { /* DEFINITIONS */ -/// Allows calls to return output in different formats for different purposes, -/// such as web API calls, scripting, or human-readable output. +/// Describes the format in which calls to the `info` CLI command to the binary +/// should print its output, which should be mostly human-readable. #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] -pub enum OutputMode { - /// Extra content or formatting where appropriate. - Extra, +pub enum InfoFormat { + /// Legible output intended for human eyes. + Legible, /// Multi-platform compatible JSON format. Json, +} + +/// Describes the format in which calls to the `query` CLI command to the binary +/// should print its output. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] +pub enum QueryFormat { + /// Comma-separated list of record attributes separated by line breaks. + CSV, - /// Output nothing (side-effects only). - None, + /// JSON list of record objects containing attribute sub-objects. + Json, } /// Specifies how exhaustive a solving algorithm should be when computing a @@ -46,7 +54,7 @@ pub enum OutputMode { /// strong alternative. The relative convenience of a weak solution relies on /// the structure of the underlying game. #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] -pub enum SolutionMode { +pub enum Solution { /// Minimally prove an optimal strategy beginning from a starting state. Weak, @@ -54,6 +62,43 @@ pub enum SolutionMode { Strong, } +/// Specifies a category of information kept about a game. Used for finding +/// specific information about game implementations through the `info` CLI +/// command. See [`crate::game::GameData`] for the provider data structure. +/// +/// This level of granularity is supported for clients to automate +/// the creation of custom objects containing any choice of these without the +/// need to mangle this program's output. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] +pub enum GameAttribute { + /// The conventional name of the game, formatted to be unique. + Name, + + /// The people involved in adding the game to the system. + Authors, + + /// General introduction to the game's rules and setup. + About, + + /// Explanation of how to encode a variant for the game. + VariantProtocol, + + /// Regex pattern that all encodings of the game's variants must satisfy. + VariantPattern, + + /// Default variant encoding the game uses when none is specified. + VariantDefault, + + /// Explanation of how to encode a state for the game. + StateProtocol, + + /// Regex pattern all encodings of the game's states must satisfy. + StatePattern, + + /// The encoding of the game's default starting state. + StateDefault, +} + /// Specifies a mode of operation for solving algorithms in regard to database /// usage and solution set persistence. There are a few cases to consider about /// database files every time a command is received: @@ -85,11 +130,11 @@ pub enum SolutionMode { /// computed again up to the number of states associated with the request. #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] pub enum IOMode { - /// Attempt to find an existing solution set to use or expand upon. - Find, + /// Use existing resources and compute whatever is missing. + Constructive, - /// Overwrite any existing solution set that could contain the request. - Write, + /// Compute request from scratch overwriting existing resources. + Overwrite, } /* AUXILIARY IMPLEMENTATIONS */ @@ -97,27 +142,35 @@ pub enum IOMode { impl fmt::Display for IOMode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - IOMode::Find => write!(f, "find"), - IOMode::Write => write!(f, "write"), + IOMode::Constructive => write!(f, "constructive"), + IOMode::Overwrite => write!(f, "overwrite"), + } + } +} + +impl fmt::Display for Solution { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Solution::Strong => write!(f, "strong"), + Solution::Weak => write!(f, "weak"), } } } -impl fmt::Display for SolutionMode { +impl fmt::Display for InfoFormat { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - SolutionMode::Weak => write!(f, "weak"), - SolutionMode::Strong => write!(f, "strong"), + InfoFormat::Legible => write!(f, "legible"), + InfoFormat::Json => write!(f, "json"), } } } -impl fmt::Display for OutputMode { +impl fmt::Display for QueryFormat { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - OutputMode::Json => write!(f, "json"), - OutputMode::Extra => write!(f, "extra"), - OutputMode::None => write!(f, "none"), + QueryFormat::Json => write!(f, "json"), + QueryFormat::CSV => write!(f, "csv"), } } } diff --git a/src/interface/terminal/cli.rs b/src/interface/terminal/cli.rs index eb6162a..9c4c5f6 100644 --- a/src/interface/terminal/cli.rs +++ b/src/interface/terminal/cli.rs @@ -1,24 +1,27 @@ //! # Command Line Module //! //! This module offers UNIX-like CLI tooling in order to facilitate scripting -//! and ergonomic use of GamesmanNova. This uses the [clap](https://docs.rs/clap/latest/clap/) -//! crate to provide standard behavior, which is outlined in -//! [this](https://clig.dev/) great guide. +//! and ergonomic use of GamesmanNova. This uses the +//! [clap](https://docs.rs/clap/latest/clap/) crate to provide standard +//! behavior, which is outlined in [this](https://clig.dev/) great guide. //! //! #### Authorship //! - Max Fierro, 4/6/2023 (maxfierro@berkeley.edu) use clap::{Args, Parser, Subcommand}; -use crate::interface::{IOMode, OutputMode, SolutionMode}; -use crate::util::GameModule; +use crate::interface::{ + GameAttribute, IOMode, InfoFormat, QueryFormat, Solution, +}; +use crate::model::database::Identifier; +use crate::model::game::GameModule; /* COMMAND LINE INTERFACE */ -/// GamesmanNova is a project for solving finite-state, deterministic, abstract -/// strategy games. In addition to being able to solve implemented games, Nova -/// provides analyzers and databases to generate insights about games and to -/// persist their full solutions efficiently. +/// GamesmanNova is a project for searching sequential games. In addition to +/// being able to analyze games whose implementations are included distributed +/// along with the binary, the project also has database implementations that +/// can persist these analyses, which can later be queried. #[derive(Parser)] #[command(author, version, about, long_about = None, propagate_version = true)] pub struct Cli { @@ -36,14 +39,11 @@ pub struct Cli { /// Subcommand choices, specified as `nova `. #[derive(Subcommand)] pub enum Commands { - /// Start the terminal user interface. - Tui(TuiArgs), - - /// Solve a game from the start position. + /// Solve some game through some specific method. Solve(SolveArgs), - /// Analyze a game's state graph. - Analyze(AnalyzeArgs), + /// Run a query on an existing table in the system. + Query(QueryArgs), /// Provide information about offerings. Info(InfoArgs), @@ -51,35 +51,16 @@ pub enum Commands { /* ARGUMENT AND OPTION DEFINITIONS */ -/// Specifies the way in which the TUI is initialized. By default, this will -/// open a main menu which allows the user to choose which game to play among -/// the list of available games, in addition to other miscellaneous offerings, -/// and prompt the user for confirmation before executing any potentially -/// destructive operations. -#[derive(Args)] -pub struct TuiArgs { - /* DEFAULTS PROVIDED */ - /// Game to display (optional). - #[arg(short, long)] - pub target: Option, - /// Enter TUI in debug mode. - #[arg(short, long)] - pub debug: bool, - /// Skips prompts for confirming destructive operations. - #[arg(short, long)] - pub yes: bool, -} - /// Ensures a specific game variant's solution set exists. Default behavior: /// /// - Uses the target's default variant (see `variant` argument). /// - Attempts to read from a database file, computing and writing one only if -/// needed (see `cli::IOMode` for specifics). +/// needed (see [`IOMode`] for specifics). /// - Formats output aesthetically (see `output` argument). -/// - Uses the game's default solver to create state graph (see `solver` -/// argument). +/// - Finds an existing solution table to the game (see `solution` argument). +/// - Does not forward the game's starting state (see `from` argument). /// - Prompts the user before executing any potentially destructive operations -/// such as overwriting a database file (see `yes` flag). +/// such as overwriting a database file (see `yes` flag). #[derive(Args)] pub struct SolveArgs { /* REQUIRED ARGUMENTS */ @@ -90,58 +71,64 @@ pub struct SolveArgs { /// Solve a specific variant of target. #[arg(short, long)] pub variant: Option, - /// Compute solution starting after a file-provided state history. - #[arg(short, long)] - pub from: Option, + /// Specify what type of solution to compute. - #[arg(short, long, default_value_t = SolutionMode::Strong)] - pub solver: SolutionMode, - /// Specify whether the solution should be fetched or generated. - #[arg(short, long, default_value_t = IOMode::Find)] + #[arg(short, long, default_value_t = Solution::Strong)] + pub solution: Solution, + + /// Specify whether the solution should be fetched or re-generated. + #[arg(short, long, default_value_t = IOMode::Constructive)] pub mode: IOMode, + + /// Compute solution starting after a state history read from STDIN. + #[arg(short, long)] + pub forward: bool, + /// Skips prompts for confirming destructive operations. #[arg(short, long)] pub yes: bool, } -/// Specifies the way in which a game's analysis happens. Uses the provided -/// `analyzer` to analyze the `target` game. This uses the same logic on finding -/// or generating missing data as the solving routine; see `cli::IOMode` for -/// specifics. +/// Accepts a query string to be compiled and ran on a specific database table, +/// whose output table is printed to STDOUT. High-level behavior: +/// +/// - `nova query` outputs the the global catalog table in `output` format. +/// - `nova query -t ` outputs the schema of table `` in `output` format. +/// - `nova query -t -q ` outputs the result of the query `` on table +/// `` in `output` format (but does not store it as a table). #[derive(Args)] -pub struct AnalyzeArgs { - /* REQUIRED ARGUMENTS */ - /// Target game name. - pub target: GameModule, - +pub struct QueryArgs { /* DEFAULTS PROVIDED */ - /// Analyzer module to use. + /// Numeric identifier for the table that the query should be run on. #[arg(short, long)] - pub analyzer: Option, - /// Analyze a specific variant of target. - #[arg(short, long)] - pub variant: Option, - /// Set output in a specific format. - #[arg(short, long, default_value_t = OutputMode::Extra)] - pub output: OutputMode, - /// Skips prompts for confirming destructive operations. + pub table: Option, + + /// Query specification string, conforming to ExQL syntax. #[arg(short, long)] - pub yes: bool, + pub query: Option, + + /// Format in which to send output to STDOUT. + #[arg(short, long, default_value_t = QueryFormat::CSV)] + pub output: QueryFormat, } -/// Provides information about available games (or about their specifications, -/// if provided a `--target` argument). Default behavior: +/// Provides information about games in the system. High-level behavior: /// -/// - Provides a list of implemented games (which are valid `--target`s). -/// - Provides output unformatted. +/// - `nova info ` outputs all known information about game `` in +/// `output` format. +/// - `nova info -a ` outputs the game ``'s `` attribute in +/// `output` format. #[derive(Args)] pub struct InfoArgs { - /* REQUIRED ARGUMENTS */ - /// Specify game for which to provide information about. + /// Dump all information about a target game. pub target: GameModule, /* DEFAULTS PROVIDED */ - /// Set output in a specific format. - #[arg(short, long, default_value_t = OutputMode::Extra)] - pub output: OutputMode, + /// Specify which of the game's attributes to provide information about. + #[arg(short, long)] + pub attribute: Option, + + /// Format in which to send output to STDOUT. + #[arg(short, long, default_value_t = InfoFormat::Legible)] + pub output: InfoFormat, } diff --git a/src/main.rs b/src/main.rs index 5c48777..2f77017 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -#![warn(missing_docs)] +#![warn(missing_docs, deprecated)] //! # Execution Module //! //! The module which aggregates the libraries provided in `core`, `games`, and @@ -14,10 +14,12 @@ use std::process; -use anyhow::Result; +use anyhow::{Context, Result}; use clap::Parser; +use game::Variable; use crate::interface::terminal::cli::*; +use crate::model::game::GameModule; /* MODULES */ @@ -36,10 +38,9 @@ mod test; fn main() -> Result<()> { let cli = Cli::parse(); let res = match &cli.command { - Commands::Tui(args) => tui(args), Commands::Info(args) => info(args), Commands::Solve(args) => solve(args), - Commands::Analyze(args) => analyze(args), + Commands::Query(args) => query(args), }; if res.is_err() && cli.quiet { process::exit(exitcode::USAGE) @@ -49,26 +50,26 @@ fn main() -> Result<()> { /* SUBCOMMAND EXECUTORS */ -fn tui(args: &TuiArgs) -> Result<()> { +fn tui(args: &QueryArgs) -> Result<()> { todo!() } -fn analyze(args: &AnalyzeArgs) -> Result<()> { +fn query(args: &QueryArgs) -> Result<()> { todo!() } fn solve(args: &SolveArgs) -> Result<()> { util::confirm_potential_overwrite(args.yes, args.mode); - let game = util::find_game( - args.target, - args.variant.to_owned(), - args.from.to_owned(), - )?; - game.solve(args.mode, args.solver)?; + match args.target { + GameModule::ZeroBy => { + let session = game::zero_by::Session::new() + .into_variant(args.variant.clone()) + .context("Failed to initialize zero-by game session.")?; + }, + } Ok(()) } fn info(args: &InfoArgs) -> Result<()> { - util::print_game_info(args.target, args.output)?; Ok(()) } diff --git a/src/model.rs b/src/model.rs index c242cd0..f476bab 100644 --- a/src/model.rs +++ b/src/model.rs @@ -17,6 +17,7 @@ pub mod game { use bitvec::{array::BitArray, order::Msb0}; + use clap::ValueEnum; /// The default number of bytes used to encode states. pub const DEFAULT_STATE_BYTES: usize = 8; @@ -25,6 +26,9 @@ pub mod game { pub type State = BitArray<[u8; B], Msb0>; + /// String encoding some specific game's variant. + pub type Variant = String; + /// Unique identifier for a player in a game. pub type Player = usize; @@ -36,6 +40,12 @@ pub mod game { /// Count of the number of players in a game. pub type PlayerCount = Player; + + // Specifies the game offerings available through all interfaces. + #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] + pub enum GameModule { + ZeroBy, + } } /// # Solver Data Models Module diff --git a/src/solver/algorithm/strong/acyclic.rs b/src/solver/algorithm/strong/acyclic.rs index 099df8a..1ac3734 100644 --- a/src/solver/algorithm/strong/acyclic.rs +++ b/src/solver/algorithm/strong/acyclic.rs @@ -9,12 +9,13 @@ use anyhow::{Context, Result}; use crate::database::volatile; use crate::database::{KVStore, Tabular}; -use crate::game::{Bounded, Game, Transition}; +use crate::game::{Bounded, Transition}; use crate::interface::IOMode; use crate::model::game::PlayerCount; use crate::model::solver::{IUtility, Remoteness}; use crate::solver::record::mur::RecordBuffer; use crate::solver::{Extensive, IntegerUtility, RecordType}; +use crate::util::Identify; /* SOLVERS */ @@ -27,7 +28,7 @@ where + Bounded + IntegerUtility + Extensive - + Game, + + Identify, { let db = volatile_database(game) .context("Failed to initialize volatile database.")?; @@ -51,7 +52,7 @@ fn volatile_database( game: &G, ) -> Result where - G: Extensive + Game, + G: Extensive + Identify, { let id = game.id(); let db = volatile::Database::initialize(); diff --git a/src/solver/error.rs b/src/solver/error.rs index 336d978..e8168f0 100644 --- a/src/solver/error.rs +++ b/src/solver/error.rs @@ -24,6 +24,15 @@ pub enum SolverError { /// An error to indicate that the assumptions of a solving algorithm were /// detectably violated during execution. SolverViolation { name: String, hint: String }, + + /// An error to indicate that there was an attempt to translate one measure + /// into another incompatible measure. Provides hints about the input type, + /// output type, and the reason behind the incompatibility. + InvalidConversion { + input_t: String, + output_t: String, + hint: String, + }, } impl Error for SolverError {} @@ -47,6 +56,18 @@ impl fmt::Display for SolverError { name, hint, ) }, + Self::InvalidConversion { + input_t: input, + output_t: output, + hint, + } => { + write!( + f, + "There was an attempt to translate a value of type '{}' \ + into a value of type '{}': {}", + input, output, hint, + ) + }, } } } diff --git a/src/solver/mod.rs b/src/solver/mod.rs index f621992..8cf6153 100644 --- a/src/solver/mod.rs +++ b/src/solver/mod.rs @@ -18,14 +18,6 @@ use crate::model::{ solver::{IUtility, RUtility, SUtility}, }; -/* CONSTANTS */ - -/// Describes the maximum number of states that are one move away from any state -/// within a game. Used to allocate statically-sized arrays on the stack for -/// faster execution of solving algorithms. If this limit is violated by a game -/// implementation, this program should panic. -pub const MAX_TRANSITIONS: usize = 512 / 8; - /* MODULES */ /// Implementations of records that can be used by solving algorithms to store @@ -194,7 +186,13 @@ where G: IntegerUtility, { fn utility(&self, state: State) -> [RUtility; N] { - todo!() + let iutility = self.utility(state); + let mut rutility = [0.0; N]; + rutility + .iter_mut() + .enumerate() + .for_each(|(i, u)| *u = iutility[i] as RUtility); + rutility } } @@ -203,16 +201,28 @@ where G: SimpleUtility, { fn utility(&self, state: State) -> [IUtility; N] { - todo!() + let sutility = self.utility(state); + let mut iutility = [0; N]; + iutility + .iter_mut() + .enumerate() + .for_each(|(i, u)| *u = sutility[i].into()); + iutility } } impl SimpleUtility<2, B> for G where - G: ClassicGame, + G: Extensive<2, B> + ClassicGame, { fn utility(&self, state: State) -> [SUtility; 2] { - todo!() + let mut sutility = [SUtility::TIE; 2]; + let utility = self.utility(state); + let turn = self.turn(state); + let them = (turn + 1) % 2; + sutility[them] = !utility; + sutility[turn] = utility; + sutility } } @@ -221,6 +231,6 @@ where G: ClassicPuzzle, { fn utility(&self, state: State) -> [SUtility; 1] { - todo!() + [self.utility(state)] } } diff --git a/src/solver/record/sur.rs b/src/solver/record/sur.rs index 3b28ddd..4ca8161 100644 --- a/src/solver/record/sur.rs +++ b/src/solver/record/sur.rs @@ -16,7 +16,7 @@ use bitvec::{bitarr, BitArr}; use crate::database::{Attribute, Datatype, Record, Schema, SchemaBuilder}; use crate::model::game::{Player, PlayerCount}; -use crate::model::solver::{Remoteness, SUtility}; +use crate::model::solver::{IUtility, Remoteness, SUtility}; use crate::solver::error::SolverError::RecordViolation; use crate::solver::RecordType; use crate::util; @@ -182,7 +182,7 @@ impl RecordBuffer { } else { let start = Self::utility_index(player); let end = start + UTILITY_SIZE; - let val = self.buf[start..end].load_be::(); + let val = self.buf[start..end].load_be::(); if let Ok(utility) = SUtility::try_from(val) { Ok(utility) } else { diff --git a/src/solver/util.rs b/src/solver/util.rs index a44b214..2e8b032 100644 --- a/src/solver/util.rs +++ b/src/solver/util.rs @@ -6,8 +6,10 @@ //! #### Authorship //! - Max Fierro, 2/24/2024 (maxfierro@berkeley.edu) +use std::ops::Not; + use crate::database::Schema; -use crate::model::solver::{IUtility, SUtility}; +use crate::model::solver::{IUtility, RUtility, SUtility}; use crate::solver::error::SolverError; use crate::solver::{record, RecordType}; @@ -41,7 +43,7 @@ impl TryInto for RecordType { } } -/* UTILITY CONVERSION */ +/* CONVERSIONS INTO SIMPLE UTILITY */ impl TryFrom for SUtility { type Error = SolverError; @@ -52,25 +54,43 @@ impl TryFrom for SUtility { v if v == SUtility::DRAW as i64 => Ok(SUtility::DRAW), v if v == SUtility::TIE as i64 => Ok(SUtility::TIE), v if v == SUtility::WIN as i64 => Ok(SUtility::WIN), - _ => Err(todo!()), + _ => Err(SolverError::InvalidConversion { + input_t: "Integer Utility".into(), + output_t: "Simple Utility".into(), + hint: + "Down-casting from integer to simple utility values is not \ + stable, and relies on the internal representation used for \ + simple utility values (which is not intuitive). As of \ + right now though, WIN = 0, TIE = 3, DRAW = 2, and LOSE = 1." + .into(), + }), } } } -impl TryFrom for SUtility { +impl TryFrom for SUtility { type Error = SolverError; - fn try_from(v: u64) -> Result { + fn try_from(v: RUtility) -> Result { match v { - v if v == SUtility::LOSE as u64 => Ok(SUtility::LOSE), - v if v == SUtility::DRAW as u64 => Ok(SUtility::DRAW), - v if v == SUtility::TIE as u64 => Ok(SUtility::TIE), - v if v == SUtility::WIN as u64 => Ok(SUtility::WIN), - _ => Err(todo!()), + v if v as i64 == SUtility::LOSE as i64 => Ok(SUtility::LOSE), + v if v as i64 == SUtility::DRAW as i64 => Ok(SUtility::DRAW), + v if v as i64 == SUtility::TIE as i64 => Ok(SUtility::TIE), + v if v as i64 == SUtility::WIN as i64 => Ok(SUtility::WIN), + _ => Err(SolverError::InvalidConversion { + input_t: "Real Utility".into(), + output_t: "Simple Utility".into(), + hint: + "Simple Utility values can only have pre-specified values \ + (which are subject to change)." + .into(), + }), } } } +/* CONVERSIONS FROM SIMPLE UTILITY */ + impl Into for SUtility { fn into(self) -> IUtility { match self { @@ -81,3 +101,28 @@ impl Into for SUtility { } } } + +impl Into for SUtility { + fn into(self) -> RUtility { + match self { + SUtility::LOSE => -1.0, + SUtility::DRAW => 0.0, + SUtility::TIE => 0.0, + SUtility::WIN => 1.0, + } + } +} + +/* SIMPLE UTILITY NEGATION */ + +impl Not for SUtility { + type Output = SUtility; + fn not(self) -> Self::Output { + match self { + SUtility::DRAW => SUtility::DRAW, + SUtility::LOSE => SUtility::WIN, + SUtility::WIN => SUtility::LOSE, + SUtility::TIE => SUtility::TIE, + } + } +} diff --git a/src/util.rs b/src/util.rs index 05f39ee..97bdca4 100644 --- a/src/util.rs +++ b/src/util.rs @@ -6,53 +6,30 @@ //! #### Authorship //! - Max Fierro, 4/9/2023 (maxfierro@berkeley.edu) -use anyhow::{Context, Result}; -use clap::ValueEnum; -use serde_json::json; +use std::process; -use std::{fmt::Display, process}; +use crate::{interface::IOMode, model::database::Identifier}; -use crate::{ - game::{zero_by, Game, GameData}, - interface::{IOMode, OutputMode}, -}; +/* INTERFACES */ -/* DATA STRUCTURES */ - -// Specifies the game offerings available through all interfaces. -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] -pub enum GameModule { - ZeroBy, +/// Provides a way to loosely identify objects that is not as concrete as a +/// hash function. The precise semantics of this interface are undefined. +pub trait Identify { + /// Returns an ID that is unique in some degree to the state of this object. + /// The semantics of when variations are acceptable are implicit, and should + /// be enforced by an API consuming the [`Identify`] trait. + fn id(&self) -> Identifier; } -/* SUBROUTINES */ - -/// Fetches and initializes the correct game session based on an indicated -/// `GameModule`, with the provided `variant`. -pub fn find_game( - game: GameModule, - variant: Option, - from: Option, -) -> Result> { - match game { - GameModule::ZeroBy => { - let session = zero_by::Session::new(variant) - .context("Failed to initialize zero-by game session.")?; - if let Some(path) = from { - todo!() - } - Ok(Box::new(session)) - }, - } -} +/* USER INPUT */ /// Prompts the user to confirm their operation as appropriate according to /// the arguments of the solve command. Only asks for confirmation for /// potentially destructive operations. pub fn confirm_potential_overwrite(yes: bool, mode: IOMode) { if match mode { - IOMode::Write => !yes, - IOMode::Find => false, + IOMode::Overwrite => !yes, + IOMode::Constructive => false, } { println!( "This may overwrite an existing solution database. Are you sure? \ @@ -72,79 +49,6 @@ pub fn confirm_potential_overwrite(yes: bool, mode: IOMode) { } } -/// Prints the formatted game information according to a specified output -/// format. Game information is provided by game implementations. -pub fn print_game_info(game: GameModule, format: OutputMode) -> Result<()> { - find_game(game, None, None) - .context("Failed to initialize game session.")? - .info() - .print(format); - Ok(()) -} - -/* IMPLEMENTATIONS */ - -impl GameData { - fn print(&self, format: OutputMode) { - match format { - OutputMode::Extra => { - let content = format!( - "\tGame:\n{}\n\n\tAuthor:\n{}\n\n\tDescription:\n{}\n\n\t\ - Variant Protocol:\n{}\n\n\tVariant Default:\n{}\n\n\t\ - Variant Pattern:\n{}\n\n\tState Protocol:\n{}\n\n\tState \ - Default:\n{}\n\n\tState Pattern:\n{}\n", - self.name, - self.authors, - self.about, - self.variant_protocol, - self.variant_default, - self.variant_pattern, - self.state_protocol, - self.state_default, - self.state_pattern - ); - println!("{}", content); - }, - OutputMode::Json => { - let content = json!({ - "game": self.name, - "author": self.authors, - "about": self.about, - "variant-protocol": self.variant_protocol, - "variant-default": self.variant_default, - "variant-pattern": self.variant_pattern, - "state-protocol": self.state_protocol, - "state-default": self.state_default, - "state-pattern": self.state_pattern, - }); - println!("{}", content); - }, - OutputMode::None => (), - } - } -} - -impl Display for GameData { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "\tGame:\n{}\n\n\tAuthor:\n{}\n\n\tDescription:\n{}\n\n\tVariant \ - Protocol:\n{}\n\n\tVariant Default:\n{}\n\n\tVariant Pattern:\n{}\ - \n\n\tState Protocol:\n{}\n\n\tState Default:\n{}\n\n\tState \ - Pattern:\n{}\n", - self.name, - self.authors, - self.about, - self.variant_protocol, - self.variant_default, - self.variant_pattern, - self.state_protocol, - self.state_default, - self.state_pattern - ) - } -} - /* BIT FIELDS */ /// Returns the minimum number of bits required to represent unsigned `val`. From da46b6c3df82d9c4c7dcb58243581618dd7f1f41 Mon Sep 17 00:00:00 2001 From: Max Fierro Date: Tue, 7 May 2024 01:46:44 -0700 Subject: [PATCH 10/15] Added info command implementation --- src/game/crossteaser/mod.rs | 27 ++---- src/game/crossteaser/variants.rs | 40 ++++---- src/game/error.rs | 12 +-- src/game/mod.rs | 48 +++++----- src/game/util.rs | 101 ++++++++++++++++++-- src/game/zero_by/mod.rs | 32 +++---- src/game/zero_by/states.rs | 65 ++++++------- src/game/zero_by/variants.rs | 10 +- src/interface/mod.rs | 4 +- src/interface/{terminal => standard}/cli.rs | 4 +- src/interface/standard/mod.rs | 79 +++++++++++++++ src/interface/util.rs | 75 +++++++++++++++ src/main.rs | 36 ++++--- src/solver/algorithm/strong/acyclic.rs | 3 +- src/solver/error.rs | 8 +- src/solver/record/sur.rs | 49 ++++------ src/solver/util.rs | 21 ++++ src/util.rs | 38 +++----- 18 files changed, 435 insertions(+), 217 deletions(-) rename src/interface/{terminal => standard}/cli.rs (97%) create mode 100644 src/interface/standard/mod.rs diff --git a/src/game/crossteaser/mod.rs b/src/game/crossteaser/mod.rs index 504a5ff..24d2316 100644 --- a/src/game/crossteaser/mod.rs +++ b/src/game/crossteaser/mod.rs @@ -84,11 +84,6 @@ pub struct Session { } impl Session { - fn new() -> Self { - parse_variant(VARIANT_DEFAULT.to_owned()) - .expect("Failed to parse default game variant.") - } - fn solve(&self, mode: IOMode, method: Solution) -> Result<()> { todo!() } @@ -102,25 +97,21 @@ impl Information for Session { } } -impl Identify for Session { - fn id(&self) -> Identifier { - todo!() +/* VARIANCE IMPLEMENTATION */ + +impl Default for Session { + fn default() -> Self { + parse_variant(VARIANT_DEFAULT.to_owned()) + .expect("Failed to parse default game variant.") } } -/* VARIANCE IMPLEMENTATION */ - impl Variable for Session { - fn into_variant(self, variant: Option) -> Result { - if let Some(v) = variant { - parse_variant(v).context("Malformed game variant.") - } else { - parse_variant(VARIANT_DEFAULT.to_owned()) - .context("Failed to parse default game variant.") - } + fn variant(variant: Variant) -> Result { + parse_variant(variant).context("Malformed game variant.") } - fn variant(&self) -> Variant { + fn variant_string(&self) -> Variant { self.variant.to_owned() } } diff --git a/src/game/crossteaser/variants.rs b/src/game/crossteaser/variants.rs index 1718d16..4037de9 100644 --- a/src/game/crossteaser/variants.rs +++ b/src/game/crossteaser/variants.rs @@ -111,9 +111,8 @@ fn check_params_are_positive(params: &Vec) -> Result<(), GameError> { #[cfg(test)] mod test { - use crate::game::Variable; - use super::*; + use crate::game::Variable; #[test] fn variant_pattern_is_valid_regex() { @@ -128,45 +127,40 @@ mod test { #[test] fn initialization_success_with_no_variant() { - let _ = Session::new(); - let with_default = - Session::new().into_variant(Some(VARIANT_DEFAULT.to_owned())); + let _ = Session::default(); + let with_default = Session::variant(VARIANT_DEFAULT.to_owned()); assert!(with_default.is_ok()); } #[test] fn invalid_variants_fail_checks() { let v = vec![ - Some("None".to_owned()), - Some("x4-".to_owned()), - Some("-".to_owned()), - Some("1x2-5".to_owned()), - Some("0x2-5".to_owned()), - Some("1x1-1".to_owned()), - Some("8x2.6-5".to_owned()), - Some("3x4-0".to_owned()), + "None".to_owned(), + "x4-".to_owned(), + "-".to_owned(), + "1x2-5".to_owned(), + "0x2-5".to_owned(), + "1x1-1".to_owned(), + "8x2.6-5".to_owned(), + "3x4-0".to_owned(), ]; for variant in v { - assert!(Session::new() - .into_variant(variant) - .is_err()); + assert!(Session::variant(variant).is_err()); } } #[test] fn valid_variants_pass_checks() { let v = vec![ - Some("4x3-2".to_owned()), - Some("5x4-2".to_owned()), - Some("2x4-1".to_owned()), - Some("4x2-1".to_owned()), + "4x3-2".to_owned(), + "5x4-2".to_owned(), + "2x4-1".to_owned(), + "4x2-1".to_owned(), ]; for variant in v { - assert!(Session::new() - .into_variant(variant) - .is_ok()); + assert!(Session::variant(variant).is_ok()); } } } diff --git a/src/game/error.rs b/src/game/error.rs index 2f68a13..7baccd8 100644 --- a/src/game/error.rs +++ b/src/game/error.rs @@ -65,24 +65,24 @@ impl fmt::Display for GameError { Self::VariantMalformed { game_name, hint } => { write!( f, - "{}\n\n\tMore information on how the game expects you to \ - format it can be found with 'nova info {} --output extra'.", + "{}\n\nMore information on how the game expects you to \ + format variant encodings can be found with 'nova info {}'.", hint, game_name ) }, Self::StateMalformed { game_name, hint } => { write!( f, - "{}\n\n\tMore information on how the game expects you to \ - format it can be found with 'nova info {} --output extra'.", + "{}\n\nMore information on how the game expects you to \ + format state encodings can be found with 'nova info {}'.", hint, game_name ) }, Self::InvalidHistory { game_name, hint } => { write!( f, - "{}\n\n\tMore information on the game's rules can be found \ - with 'nova info {} --output extra'.", + "{}\n\nMore information on the game's rules can be found \ + with 'nova info {}'.", hint, game_name ) }, diff --git a/src/game/mod.rs b/src/game/mod.rs index 8191454..86788ea 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -281,12 +281,12 @@ pub trait Codec { /// Provides methods to obtain a working instance of a game variant and to /// retrieve a [`String`]-encoded specification of the variant. pub trait Variable { - /// Returns a version of the underlying game as the specified `variant`, - /// resetting to the default variant if it is `None`. + /// Initializes a version of the underlying game as the specified `variant`. /// - /// This does not preserve any kind of state held by the game object, - /// including the starting state (even if it was set through [`Forward`]). - /// In this sense, game variants specify starting states. + /// A variant is a member of a family of games whose structure is very + /// similar. It is convenient to be able to express this because it saves + /// a lot of needless re-writing of game logic, while allowing for a lot + /// of generality in game implementations. /// /// # Example /// @@ -296,14 +296,12 @@ pub trait Variable { /// ``` /// use crate::game::zero_by; /// - /// let state = "100-0".into(); - /// let variant = "3-100-3-4".into(); - /// - /// let default_variant = zero_by::Session::new(); - /// assert_ne!(default_variant.encode(default_variant.start())?, state); + /// let default = zero_by::Session::new(); + /// assert_ne!(default.encode(default.start())?, state); /// - /// let custom_variant = session.into_variant(variant)?; - /// assert_eq!(custom_variant.encode(custom_variant.start())?, state); + /// let state = "100-0".into(); + /// let variant = zero_by::Session::variant("3-100-3-4".into())?; + /// assert_eq!(variant.encode(variant.start())?, state); /// ``` /// /// # Errors @@ -311,7 +309,7 @@ pub trait Variable { /// Fails if `variant` does not conform to the game's protocol for encoding /// variants as strings, or if the game does not support variants in the /// first place (but has a placeholder [`Variable`] implementation). - fn into_variant(self, variant: Option) -> Result + fn variant(variant: Variant) -> Result where Self: Sized; @@ -336,7 +334,7 @@ pub trait Variable { /// let custom_variant = session.into_variant(variant.clone())?; /// assert_eq!(custom_variant.variant(), variant); /// ``` - fn variant(&self) -> Variant; + fn variant_string(&self) -> Variant; } /// Provides methods to safely fast-forward the starting state of a game to @@ -380,10 +378,10 @@ where /// Advances the game's starting state to the last state in `history`. /// - /// This function needs an implementation of [`Forward::set_verified_start`] - /// to ultimately change the starting state after `history` is verified. It - /// consumes `self` with the intent of making it difficult to make errors - /// regarding variant incoherency, and to make the semantics clearer. + /// This can be useful for skipping a significant amount of computation in + /// the process of performing subgame analysis. Requires an implementation + /// of [`Forward::set_verified_start`] to ultimately change the starting + /// state after `history` is verified. /// /// # Example /// @@ -407,17 +405,17 @@ where /// # Errors /// /// Here are some of the reasons this could fail: - /// * `history` is empty. - /// * A state encoding in `history` is not valid. - /// * The provided `history` plays beyond a terminal state. - /// * `history` begins at a state other than the variant's starting state. /// * An invalid transition is made between subsequent states in `history`. + /// * `history` begins at a state other than the variant's starting state. + /// * The provided `history` plays beyond a terminal state. + /// * A state encoding in `history` is not valid. + /// * `history` is empty. #[allow(deprecated)] - fn forward(mut self, history: Vec) -> Result { - let to = util::verify_state_history(&self, history) + fn forward(&mut self, history: Vec) -> Result<()> { + let to = util::verify_state_history(self, history) .context("Specified invalid state history.")?; self.set_verified_start(to); - Ok(self) + Ok(()) } } diff --git a/src/game/util.rs b/src/game/util.rs index 298a7f2..01bc071 100644 --- a/src/game/util.rs +++ b/src/game/util.rs @@ -6,9 +6,13 @@ //! #### Authorship //! - Max Fierro, 11/2/2023 (maxfierro@berkeley.edu) +use std::fmt::Display; + use anyhow::{Context, Result}; +use crate::game::GameData; use crate::game::Information; +use crate::interface::GameAttribute; use crate::{ game::{error::GameError, Bounded, Codec, Transition}, model::game::State, @@ -16,6 +20,8 @@ use crate::{ /* STATE HISTORY VERIFICATION */ +/// Verifies that the elements of `history` are a valid sequence of states under +/// the rules of `game`, failing if this is not true. pub fn verify_state_history( game: &G, history: Vec, @@ -23,15 +29,33 @@ pub fn verify_state_history( where G: Information + Bounded + Codec + Transition, { - if let Some(s) = history.first() { - let mut prev = game.decode(s.clone())?; + let history = sanitize_input(history); + if let Some((l, s)) = history.first() { + let mut prev = game + .decode(s.clone()) + .context(format!("Failed to parse line #{}.", l))?; if prev == game.start() { for i in 1..history.len() { - let next = game.decode(history[i].clone())?; + let (l, s) = history[i].clone(); + let next = game + .decode(s) + .context(format!("Failed to parse line #{}.", l))?; + if game.end(prev) { + return terminal_history_error(game, prev, next).context( + format!( + "Invalid state transition found at line #{}.", + l + ), + ); + } let transitions = game.prograde(prev); if !transitions.contains(&next) { - return transition_history_error(game, prev, next) - .context("Specified invalid state transition."); + return transition_history_error(game, prev, next).context( + format!( + "Invalid state transition found at line #{}.", + l + ), + ); } prev = next; } @@ -46,6 +70,16 @@ where } } +/// Enumerates lines and trims whitespace from input. +fn sanitize_input(mut input: Vec) -> Vec<(usize, String)> { + input + .iter_mut() + .enumerate() + .map(|(i, s)| (i, s.trim().to_owned())) + .filter(|(_, s)| !s.is_empty()) + .collect() +} + /* HISTORY VERIFICATION ERRORS */ fn empty_history_error() -> Result> @@ -54,7 +88,7 @@ where { Err(GameError::InvalidHistory { game_name: G::info().name, - hint: format!("State history must contain at least one state."), + hint: "State history must contain at least one state.".into(), })? } @@ -93,3 +127,58 @@ where ), })? } + +fn terminal_history_error( + game: &G, + prev: State, + next: State, +) -> Result> +where + G: Information + Codec + Bounded, +{ + Err(GameError::InvalidHistory { + game_name: G::info().name, + hint: format!( + "Transitioning from the state '{}' to the sate '{}' is illegal in \ + the provided game variant, because '{}' is a terminal state.", + game.encode(prev)?, + game.encode(next)?, + game.encode(prev)?, + ), + })? +} + +/* GAME DATA UTILITIES */ + +impl Display for GameAttribute { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let content = match self { + GameAttribute::VariantProtocol => "variant-protocol", + GameAttribute::VariantPattern => "variant-pattern", + GameAttribute::VariantDefault => "variant-default", + GameAttribute::StateProtocol => "state-protocol", + GameAttribute::StateDefault => "state-default", + GameAttribute::StatePattern => "state-pattern", + GameAttribute::Authors => "authors", + GameAttribute::About => "about", + GameAttribute::Name => "name", + }; + write!(f, "{}", content) + } +} + +impl GameData { + pub fn find(&self, attribute: GameAttribute) -> &str { + match attribute { + GameAttribute::VariantProtocol => self.variant_protocol, + GameAttribute::VariantPattern => self.variant_pattern, + GameAttribute::VariantDefault => self.variant_default, + GameAttribute::StateProtocol => self.state_protocol, + GameAttribute::StateDefault => self.state_default, + GameAttribute::StatePattern => self.state_pattern, + GameAttribute::Authors => self.authors, + GameAttribute::About => self.about, + GameAttribute::Name => self.name, + } + } +} diff --git a/src/game/zero_by/mod.rs b/src/game/zero_by/mod.rs index 2c7acdd..2511715 100644 --- a/src/game/zero_by/mod.rs +++ b/src/game/zero_by/mod.rs @@ -23,13 +23,11 @@ use crate::game::Variable; use crate::game::{Bounded, Codec, Forward}; use crate::game::{GameData, Transition}; use crate::interface::{IOMode, Solution}; -use crate::model::database::Identifier; use crate::model::game::Variant; use crate::model::game::{Player, PlayerCount, State}; use crate::model::solver::SUtility; use crate::solver::algorithm::strong; use crate::solver::{Extensive, SimpleUtility}; -use crate::util::Identify; /* SUBMODULES */ @@ -65,8 +63,12 @@ pub struct Session { } impl Session { - pub fn new() -> Self { - parse_variant(VARIANT_DEFAULT.to_owned()).unwrap() + pub fn new(variant: Option) -> Result { + if let Some(v) = variant { + Self::variant(v) + } else { + Ok(Self::default()) + } } pub fn solve(&self, mode: IOMode, method: Solution) -> Result<()> { @@ -123,25 +125,21 @@ impl Information for Session { } } -impl Identify for Session { - fn id(&self) -> Identifier { - todo!() +/* VARIANCE IMPLEMENTATION */ + +impl Default for Session { + fn default() -> Self { + parse_variant(VARIANT_DEFAULT.to_owned()) + .expect("Failed to parse default state.") } } -/* VARIANCE IMPLEMENTATION */ - impl Variable for Session { - fn into_variant(self, variant: Option) -> Result { - if let Some(v) = variant { - parse_variant(v).context("Malformed game variant.") - } else { - parse_variant(VARIANT_DEFAULT.to_owned()) - .context("Failed to parse default game variant.") - } + fn variant(variant: Variant) -> Result { + parse_variant(variant).context("Malformed game variant.") } - fn variant(&self) -> Variant { + fn variant_string(&self) -> Variant { self.variant.clone() } } diff --git a/src/game/zero_by/states.rs b/src/game/zero_by/states.rs index 736f5de..398e6b3 100644 --- a/src/game/zero_by/states.rs +++ b/src/game/zero_by/states.rs @@ -53,8 +53,8 @@ fn check_state_pattern(from: &String) -> Result<(), GameError> { Err(GameError::StateMalformed { game_name: NAME, hint: format!( - "String does not match the pattern '{}'.", - STATE_PATTERN + "Input string '{}' does not match the pattern '{}'.", + from, STATE_PATTERN ), }) } else { @@ -142,8 +142,8 @@ mod test { #[test] fn no_state_equals_default_state() { - let with_none = Session::new(); - let with_default = Session::new(); + let with_none = Session::default(); + let with_default = Session::default(); assert_eq!( with_none.start_state, @@ -163,7 +163,7 @@ mod test { fn f() -> Session { // 2-player 10-to-zero by 1 or 2 - Session::new() + Session::default() } assert!(parse_state(&f(), s1).is_err()); @@ -186,7 +186,7 @@ mod test { let s7 = "1-0".to_owned(); fn f() -> Session { - Session::new() + Session::default() } assert!(parse_state(&f(), s1).is_ok()); @@ -230,74 +230,69 @@ mod test { let i2 = vec!["10-0", "8-0", "7-0", "5-1"]; // Turns don't switch let i3 = vec!["10-1", "8-0", "7-1", "5-0"]; // Starting turn wrong let i4 = vec!["1-10", "0-9", "1-7", "0-5"]; // Turn and state switched - let i5 = vec!["10-0", "", "9-1", "", "8-0"]; // Empty states - let i6 = vec!["ten-zero", "nine-one"]; // Malformed - let i7: Vec<&str> = vec![]; // No history - let i8 = vec![""]; // Empty string + let i5 = vec!["ten-zero", "nine-one"]; // Malformed + let i6: Vec<&str> = vec![]; // No history + let i7 = vec![""]; // Empty string - assert!(Session::new() + assert!(Session::default() .forward(owned(i1)) .is_err()); - assert!(Session::new() + assert!(Session::default() .forward(owned(i2)) .is_err()); - assert!(Session::new() + assert!(Session::default() .forward(owned(i3)) .is_err()); - assert!(Session::new() + assert!(Session::default() .forward(owned(i4)) .is_err()); - assert!(Session::new() + assert!(Session::default() .forward(owned(i5)) .is_err()); - assert!(Session::new() + assert!(Session::default() .forward(owned(i6)) .is_err()); - assert!(Session::new() + assert!(Session::default() .forward(owned(i7)) .is_err()); - - assert!(Session::new() - .forward(owned(i8)) - .is_err()); } #[test] fn verify_correct_default_zero_by_history_passes() { - let c1 = vec!["10-0", "8-1", "6-0", "4-1", "2-0", "0-1"]; - let c2 = vec!["10-0", "8-1", "6-0", "4-1", "2-0"]; - let c3 = vec!["10-0", "9-1", "7-0", "6-1"]; - let c4 = vec!["10-0", "8-1", "6-0"]; - let c5 = vec!["10-0", "9-1"]; - let c6 = vec!["10-0"]; - - assert!(Session::new() + let c1 = vec!["10-0", "8-1", " ", "6-0", "4-1", "2-0", "0-1"]; + let c2 = vec!["", "10-0", "8-1", "6-0", "4-1", "2-0"]; + let c3 = vec!["10-0", "9-1", "", "", "7-0", "6-1"]; + let c4 = vec!["10-0", "8-1", "6-0", " "]; + let c5 = vec!["10-0", " ", "9-1"]; + let c6 = vec!["", "10-0", " "]; + + assert!(Session::default() .forward(owned(c1)) .is_ok()); - assert!(Session::new() + assert!(Session::default() .forward(owned(c2)) .is_ok()); - assert!(Session::new() + assert!(Session::default() .forward(owned(c3)) .is_ok()); - assert!(Session::new() + assert!(Session::default() .forward(owned(c4)) .is_ok()); - assert!(Session::new() + assert!(Session::default() .forward(owned(c5)) .is_ok()); - assert!(Session::new() + assert!(Session::default() .forward(owned(c6)) .is_ok()); } @@ -350,7 +345,7 @@ mod test { /* UTILITIES */ fn variant(v: &str) -> Result { - Session::new().into_variant(Some(v.to_string())) + Session::variant(v.to_string()) } fn owned(v: Vec<&'static str>) -> Vec { diff --git a/src/game/zero_by/variants.rs b/src/game/zero_by/variants.rs index 2ab2336..41359e6 100644 --- a/src/game/zero_by/variants.rs +++ b/src/game/zero_by/variants.rs @@ -148,17 +148,15 @@ mod test { #[test] fn initialization_success_with_no_variant() { - let _ = Session::new(); - let with_default = - Session::new().into_variant(Some(VARIANT_DEFAULT.to_owned())); + let _ = Session::default(); + let with_default = Session::variant(VARIANT_DEFAULT.to_owned()); assert!(with_default.is_ok()); } #[test] fn no_variant_equals_default_variant() -> Result<()> { - let with_none = Session::new(); - let with_default = - Session::new().into_variant(Some(VARIANT_DEFAULT.to_owned()))?; + let with_none = Session::default(); + let with_default = Session::variant(VARIANT_DEFAULT.to_owned())?; assert_eq!(with_none.variant, with_default.variant); assert_eq!(with_none.start_state, with_default.start_state); assert_eq!(with_none.by, with_default.by); diff --git a/src/interface/mod.rs b/src/interface/mod.rs index b65cdb5..c5c9ae4 100644 --- a/src/interface/mod.rs +++ b/src/interface/mod.rs @@ -17,9 +17,7 @@ mod util; /* INTERFACE IMPLEMENTATIONS */ -pub mod terminal { - pub mod cli; -} +pub mod standard; /* DEFINITIONS */ diff --git a/src/interface/terminal/cli.rs b/src/interface/standard/cli.rs similarity index 97% rename from src/interface/terminal/cli.rs rename to src/interface/standard/cli.rs index 9c4c5f6..8a4391d 100644 --- a/src/interface/terminal/cli.rs +++ b/src/interface/standard/cli.rs @@ -120,13 +120,13 @@ pub struct QueryArgs { /// `output` format. #[derive(Args)] pub struct InfoArgs { - /// Dump all information about a target game. + /// Specify the game to provide information about. pub target: GameModule, /* DEFAULTS PROVIDED */ /// Specify which of the game's attributes to provide information about. #[arg(short, long)] - pub attribute: Option, + pub attributes: Option>, /// Format in which to send output to STDOUT. #[arg(short, long, default_value_t = InfoFormat::Legible)] diff --git a/src/interface/standard/mod.rs b/src/interface/standard/mod.rs new file mode 100644 index 0000000..d6425ef --- /dev/null +++ b/src/interface/standard/mod.rs @@ -0,0 +1,79 @@ +//! # Standard Interface Module +//! +//! This module defines the behavior of the project's standard (STDIN/STDOUT) +//! interfaces. The CLI module is the primary entry point for the program. +//! +//! #### Authorship +//! - Max Fierro, 7/5/2024 + +use anyhow::{anyhow, Context, Result}; + +use std::{io::BufRead, process}; + +use crate::interface::util; +use crate::interface::{GameAttribute, InfoFormat}; +use crate::{game::GameData, interface::IOMode}; + +/* SPECIFIC INTERFACES */ + +pub mod cli; + +/* STANDARD INPUT API */ + +/// Prompts the user to confirm their operation as appropriate according to the +/// arguments of the solve command. Only asks for confirmation for potentially +/// destructive operations. +pub fn confirm_potential_overwrite(yes: bool, mode: IOMode) { + if match mode { + IOMode::Overwrite => !yes, + IOMode::Constructive => false, + } { + println!( + "This may overwrite an existing solution database. Are you sure? \ + [y/n]: " + ); + let mut yn: String = "".to_owned(); + while !["n", "N", "y", "Y"].contains(&&yn[..]) { + yn = String::new(); + std::io::stdin() + .read_line(&mut yn) + .expect("Failed to read user confirmation."); + yn = yn.trim().to_string(); + } + if yn == "n" || yn == "N" { + process::exit(exitcode::OK) + } + } +} + +/// Parses STDIN into a line-by-line vector of its contents without any form of +/// sanitation or formatting. +pub fn stdin_lines() -> Result> { + std::io::stdin() + .lock() + .lines() + .into_iter() + .map(|l| l.map_err(|e| anyhow!(e))) + .collect() +} + +/* STANDARD OUTPUT API */ + +/// Collects the attributes specified in `attrs` from the provided game `data` +/// into a specific `format`, and prints them to STDOUT. If `attrs` is `None`, +/// all possible game attributes are sent to STDOUT. +pub fn format_and_output_game_attributes( + data: GameData, + attrs: Option>, + format: InfoFormat, +) -> Result<()> { + let out = if let Some(attrs) = attrs { + util::aggregate_and_format_attributes(data, attrs, format) + .context("Failed format specified game data attributes.")? + } else { + util::aggregate_and_format_all_attributes(data, format) + .context("Failed to format game data attributes.")? + }; + print!("{}", out); + Ok(()) +} diff --git a/src/interface/util.rs b/src/interface/util.rs index c3718aa..5af9354 100644 --- a/src/interface/util.rs +++ b/src/interface/util.rs @@ -5,3 +5,78 @@ //! //! #### Authorship //! - Max Fierro, 2/24/2024 (maxfierro@berkeley.edu) + +use anyhow::{Context, Result}; +use serde_json::{Map, Value}; + +use crate::game::GameData; + +use super::{GameAttribute, InfoFormat}; + +/* CONSTANTS */ + +/// All available game attributes uniquely listed. +const ALL_GAME_ATTRIBUTES: [GameAttribute; 9] = [ + GameAttribute::VariantProtocol, + GameAttribute::VariantDefault, + GameAttribute::VariantPattern, + GameAttribute::StateProtocol, + GameAttribute::StateDefault, + GameAttribute::StatePattern, + GameAttribute::Authors, + GameAttribute::About, + GameAttribute::Name, +]; + +/* OUTPUT UTILITIES */ + +/// Collects the attributes specified in `attr` from the provided game `data` +/// to a single string in a specific `format`. +pub fn aggregate_and_format_attributes( + data: GameData, + attrs: Vec, + format: InfoFormat, +) -> Result { + match format { + InfoFormat::Legible => { + let mut output = String::new(); + attrs.iter().for_each(|&a| { + output += &format!("\t{}:\n{}\n\n", a, data.find(a)) + }); + Ok(output) + }, + InfoFormat::Json => { + let mut map = Map::new(); + attrs.iter().for_each(|&a| { + map.insert(a.to_string(), Value::String(data.find(a).into())); + }); + serde_json::to_string(&map) + .context("Failed to generate JSON object from game data.") + }, + } +} + +/// Collects all possible game attributes from the provided game `data` to a +/// single string in a specific `format`. +pub fn aggregate_and_format_all_attributes( + data: GameData, + format: InfoFormat, +) -> Result { + aggregate_and_format_attributes(data, ALL_GAME_ATTRIBUTES.to_vec(), format) +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn no_duplicates_in_game_attrs_list() { + let mut attrs = ALL_GAME_ATTRIBUTES.to_vec(); + let s1 = attrs.len(); + attrs.sort(); + attrs.dedup(); + let s2 = attrs.len(); + assert_eq!(s1, s2); + } +} diff --git a/src/main.rs b/src/main.rs index 2f77017..db418f6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,9 +16,9 @@ use std::process; use anyhow::{Context, Result}; use clap::Parser; -use game::Variable; -use crate::interface::terminal::cli::*; +use crate::game::{Forward, Information}; +use crate::interface::standard::cli::*; use crate::model::game::GameModule; /* MODULES */ @@ -37,7 +37,7 @@ mod test; fn main() -> Result<()> { let cli = Cli::parse(); - let res = match &cli.command { + let res = match cli.command { Commands::Info(args) => info(args), Commands::Solve(args) => solve(args), Commands::Query(args) => query(args), @@ -50,26 +50,34 @@ fn main() -> Result<()> { /* SUBCOMMAND EXECUTORS */ -fn tui(args: &QueryArgs) -> Result<()> { +fn query(args: QueryArgs) -> Result<()> { todo!() } -fn query(args: &QueryArgs) -> Result<()> { - todo!() -} - -fn solve(args: &SolveArgs) -> Result<()> { - util::confirm_potential_overwrite(args.yes, args.mode); +fn solve(args: SolveArgs) -> Result<()> { + interface::standard::confirm_potential_overwrite(args.yes, args.mode); match args.target { GameModule::ZeroBy => { - let session = game::zero_by::Session::new() - .into_variant(args.variant.clone()) - .context("Failed to initialize zero-by game session.")?; + let mut session = game::zero_by::Session::new(args.variant)?; + if args.forward { + let history = interface::standard::stdin_lines()?; + session.forward(history)?; + } + session.solve(args.mode, args.solution)? }, } Ok(()) } -fn info(args: &InfoArgs) -> Result<()> { +fn info(args: InfoArgs) -> Result<()> { + let data = match args.target { + GameModule::ZeroBy => game::zero_by::Session::info(), + }; + interface::standard::format_and_output_game_attributes( + data, + args.attributes, + args.output, + ) + .context("Failed to format and output game attributes.")?; Ok(()) } diff --git a/src/solver/algorithm/strong/acyclic.rs b/src/solver/algorithm/strong/acyclic.rs index 1ac3734..099c8a4 100644 --- a/src/solver/algorithm/strong/acyclic.rs +++ b/src/solver/algorithm/strong/acyclic.rs @@ -30,7 +30,7 @@ where + Extensive + Identify, { - let db = volatile_database(game) + let db = volatile_database(game, mode) .context("Failed to initialize volatile database.")?; let table = db @@ -50,6 +50,7 @@ where /// to that table before returning the database handle. fn volatile_database( game: &G, + mode: IOMode, ) -> Result where G: Extensive + Identify, diff --git a/src/solver/error.rs b/src/solver/error.rs index e8168f0..c293fee 100644 --- a/src/solver/error.rs +++ b/src/solver/error.rs @@ -29,8 +29,8 @@ pub enum SolverError { /// into another incompatible measure. Provides hints about the input type, /// output type, and the reason behind the incompatibility. InvalidConversion { - input_t: String, output_t: String, + input_t: String, hint: String, }, } @@ -57,15 +57,15 @@ impl fmt::Display for SolverError { ) }, Self::InvalidConversion { - input_t: input, - output_t: output, + output_t, + input_t, hint, } => { write!( f, "There was an attempt to translate a value of type '{}' \ into a value of type '{}': {}", - input, output, hint, + input_t, output_t, hint, ) }, } diff --git a/src/solver/record/sur.rs b/src/solver/record/sur.rs index 4ca8161..2e7dc58 100644 --- a/src/solver/record/sur.rs +++ b/src/solver/record/sur.rs @@ -16,7 +16,7 @@ use bitvec::{bitarr, BitArr}; use crate::database::{Attribute, Datatype, Record, Schema, SchemaBuilder}; use crate::model::game::{Player, PlayerCount}; -use crate::model::solver::{IUtility, Remoteness, SUtility}; +use crate::model::solver::{Remoteness, SUtility}; use crate::solver::error::SolverError::RecordViolation; use crate::solver::RecordType; use crate::util; @@ -182,19 +182,8 @@ impl RecordBuffer { } else { let start = Self::utility_index(player); let end = start + UTILITY_SIZE; - let val = self.buf[start..end].load_be::(); - if let Ok(utility) = SUtility::try_from(val) { - Ok(utility) - } else { - Err(RecordViolation { - name: RecordType::SUR(self.players).into(), - hint: format!( - "There was an attempt to deserialize a utility value \ - of '{}' into a simple utility type.", - val, - ), - })? - } + let val = self.buf[start..end].load_be::(); + Ok(SUtility::try_from(val)?) } } @@ -368,10 +357,10 @@ mod tests { } #[test] - fn set_record_attributes() { - let mut r1 = RecordBuffer::new(7).unwrap(); - let mut r2 = RecordBuffer::new(4).unwrap(); - let mut r3 = RecordBuffer::new(0).unwrap(); + fn set_record_attributes() -> Result<()> { + let mut r1 = RecordBuffer::new(7)?; + let mut r2 = RecordBuffer::new(4)?; + let mut r3 = RecordBuffer::new(0)?; let v1 = [SUtility::WIN; 7]; let v2 = [SUtility::TIE; 4]; @@ -399,11 +388,13 @@ mod tests { assert!(r1.set_remoteness(bad).is_err()); assert!(r2.set_remoteness(bad).is_err()); assert!(r3.set_remoteness(bad).is_err()); + + Ok(()) } #[test] - fn data_is_valid_after_round_trip() { - let mut record = RecordBuffer::new(5).unwrap(); + fn data_is_valid_after_round_trip() -> Result<()> { + let mut record = RecordBuffer::new(5)?; let payoffs = [ SUtility::LOSE, SUtility::WIN, @@ -413,17 +404,13 @@ mod tests { ]; let remoteness = 790; - record - .set_utility(payoffs) - .unwrap(); + record.set_utility(payoffs)?; - record - .set_remoteness(remoteness) - .unwrap(); + record.set_remoteness(remoteness)?; // Utilities unchanged after insert and fetch for i in 0..5 { - let fetched_utility = record.get_utility(i).unwrap(); + let fetched_utility = record.get_utility(i)?; let actual_utility = payoffs[i]; assert!(matches!(fetched_utility, actual_utility)); } @@ -436,11 +423,12 @@ mod tests { // Fetching utility entries of invalid players assert!(record.get_utility(5).is_err()); assert!(record.get_utility(10).is_err()); + Ok(()) } #[test] - fn extreme_data_is_valid_after_round_trip() { - let mut record = RecordBuffer::new(6).unwrap(); + fn extreme_data_is_valid_after_round_trip() -> Result<()> { + let mut record = RecordBuffer::new(6)?; let good = [ SUtility::WIN, @@ -459,12 +447,13 @@ mod tests { .is_ok()); for i in 0..6 { - let fetched_utility = record.get_utility(i).unwrap(); + let fetched_utility = record.get_utility(i)?; let actual_utility = good[i]; assert!(matches!(fetched_utility, actual_utility)); } assert_eq!(record.get_remoteness(), MAX_REMOTENESS); assert!(record.set_utility(bad).is_err()); + Ok(()) } } diff --git a/src/solver/util.rs b/src/solver/util.rs index 2e8b032..a0a8230 100644 --- a/src/solver/util.rs +++ b/src/solver/util.rs @@ -89,6 +89,27 @@ impl TryFrom for SUtility { } } +impl TryFrom for SUtility { + type Error = SolverError; + + fn try_from(v: u64) -> Result { + match v { + v if v as i64 == SUtility::LOSE as i64 => Ok(SUtility::LOSE), + v if v as i64 == SUtility::DRAW as i64 => Ok(SUtility::DRAW), + v if v as i64 == SUtility::TIE as i64 => Ok(SUtility::TIE), + v if v as i64 == SUtility::WIN as i64 => Ok(SUtility::WIN), + _ => Err(SolverError::InvalidConversion { + input_t: "Real Utility".into(), + output_t: "Simple Utility".into(), + hint: + "Simple Utility values can only have pre-specified values \ + (which are subject to change)." + .into(), + }), + } + } +} + /* CONVERSIONS FROM SIMPLE UTILITY */ impl Into for SUtility { diff --git a/src/util.rs b/src/util.rs index 97bdca4..c67ec2e 100644 --- a/src/util.rs +++ b/src/util.rs @@ -6,9 +6,9 @@ //! #### Authorship //! - Max Fierro, 4/9/2023 (maxfierro@berkeley.edu) -use std::process; +use std::hash::{DefaultHasher, Hash, Hasher}; -use crate::{interface::IOMode, model::database::Identifier}; +use crate::{game::Variable, model::database::Identifier}; /* INTERFACES */ @@ -21,31 +21,15 @@ pub trait Identify { fn id(&self) -> Identifier; } -/* USER INPUT */ - -/// Prompts the user to confirm their operation as appropriate according to -/// the arguments of the solve command. Only asks for confirmation for -/// potentially destructive operations. -pub fn confirm_potential_overwrite(yes: bool, mode: IOMode) { - if match mode { - IOMode::Overwrite => !yes, - IOMode::Constructive => false, - } { - println!( - "This may overwrite an existing solution database. Are you sure? \ - [y/n]: " - ); - let mut yn: String = "".to_owned(); - while !["n", "N", "y", "Y"].contains(&&yn[..]) { - yn = String::new(); - std::io::stdin() - .read_line(&mut yn) - .expect("Failed to read user confirmation."); - yn = yn.trim().to_string(); - } - if yn == "n" || yn == "N" { - process::exit(exitcode::OK) - } +impl Identify for G +where + G: Variable, +{ + fn id(&self) -> Identifier { + let mut hasher = DefaultHasher::new(); + self.variant_string() + .hash(&mut hasher); + hasher.finish() } } From faf0adaf9fb45954765a1e79450ca7ee243c20c8 Mon Sep 17 00:00:00 2001 From: Max Fierro Date: Tue, 7 May 2024 03:04:22 -0700 Subject: [PATCH 11/15] Implemented about 60 clippy lints --- src/database/mod.rs | 1 + src/database/util.rs | 25 ++++---- src/game/crossteaser/mod.rs | 2 - src/game/crossteaser/variants.rs | 12 ++-- src/game/mock/builder.rs | 40 +++++-------- src/game/mock/mod.rs | 2 +- src/game/test.rs | 4 +- src/game/util.rs | 4 +- src/game/zero_by/mod.rs | 14 ++--- src/game/zero_by/states.rs | 18 +++--- src/game/zero_by/variants.rs | 27 +++++---- src/interface/standard/cli.rs | 6 +- src/interface/standard/mod.rs | 11 ++-- src/main.rs | 17 ++++-- src/model.rs | 8 +-- src/solver/mod.rs | 2 +- src/solver/record/mur.rs | 51 +++++++---------- src/solver/record/rem.rs | 12 ++-- src/solver/record/sur.rs | 70 ++++++++++------------- src/solver/util.rs | 98 ++++++++++++++++---------------- 20 files changed, 202 insertions(+), 222 deletions(-) diff --git a/src/database/mod.rs b/src/database/mod.rs index e04e080..ca95dcb 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -1,3 +1,4 @@ +#![allow(drop_bounds)] //! # Database Module //! //! This module contains memory and I/O mechanisms used to store and fetch diff --git a/src/database/util.rs b/src/database/util.rs index 919acfd..5fb0e75 100644 --- a/src/database/util.rs +++ b/src/database/util.rs @@ -8,6 +8,8 @@ //! #### Authorship //! - Max Fierro, 2/24/2024 (maxfierro@berkeley.edu) +use std::fmt::Display; + use anyhow::Result; use crate::database::error::DatabaseError; @@ -123,17 +125,18 @@ impl SchemaBuilder { /* UTILITY IMPLEMENTATIONS */ -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::UINT => "Unsigned Integer".to_string(), - Datatype::SINT => "Signed Integer".to_string(), - Datatype::ENUM => "Enumeration".to_string(), - Datatype::BOOL => "Boolean".to_string(), - } +impl Display for Datatype { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let content = match self { + Datatype::DPFP => "Double-Precision Floating Point", + Datatype::SPFP => "Single-Precision Floating Point", + Datatype::CSTR => "C-Style ASCII String", + Datatype::UINT => "Unsigned Integer", + Datatype::SINT => "Signed Integer", + Datatype::ENUM => "Enumeration", + Datatype::BOOL => "Boolean", + }; + write!(f, "{}", content) } } diff --git a/src/game/crossteaser/mod.rs b/src/game/crossteaser/mod.rs index 24d2316..f76d392 100644 --- a/src/game/crossteaser/mod.rs +++ b/src/game/crossteaser/mod.rs @@ -30,12 +30,10 @@ use crate::game::Transition; use crate::game::Variable; use crate::interface::IOMode; use crate::interface::Solution; -use crate::model::database::Identifier; use crate::model::game::State; use crate::model::game::Variant; use crate::model::solver::SUtility; use crate::solver::ClassicPuzzle; -use crate::util::Identify; /* SUBMODULES */ diff --git a/src/game/crossteaser/variants.rs b/src/game/crossteaser/variants.rs index 4037de9..dd12054 100644 --- a/src/game/crossteaser/variants.rs +++ b/src/game/crossteaser/variants.rs @@ -45,9 +45,9 @@ pub fn parse_variant(variant: String) -> Result { /* VARIANT STRING VERIFICATION */ -fn check_variant_pattern(variant: &String) -> Result<(), GameError> { +fn check_variant_pattern(variant: &str) -> Result<(), GameError> { let re = Regex::new(VARIANT_PATTERN).unwrap(); - if !re.is_match(&variant) { + if !re.is_match(variant) { Err(GameError::VariantMalformed { game_name: NAME, hint: format!( @@ -68,13 +68,13 @@ fn parse_parameters(variant: &str) -> Result, GameError> { .parse::() .map_err(|e| GameError::VariantMalformed { game_name: NAME, - hint: format!("{}", e.to_string()), + hint: e.to_string(), }) }) .collect() } -fn check_param_count(params: &Vec) -> Result<(), GameError> { +fn check_param_count(params: &[u64]) -> Result<(), GameError> { if params.len() != 3 { Err(GameError::VariantMalformed { game_name: NAME, @@ -86,8 +86,8 @@ fn check_param_count(params: &Vec) -> Result<(), GameError> { } } -fn check_params_are_positive(params: &Vec) -> Result<(), GameError> { - if params.iter().any(|&x| x <= 0) { +fn check_params_are_positive(params: &[u64]) -> 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(), diff --git a/src/game/mock/builder.rs b/src/game/mock/builder.rs index 387f54a..374894a 100644 --- a/src/game/mock/builder.rs +++ b/src/game/mock/builder.rs @@ -198,21 +198,19 @@ impl<'a> SessionBuilder<'a> { ), })? } - } else { - if new.terminal() && new_count < old_count { - Err(anyhow! { - format!( - "While constructing the game '{}', a medial node was \ - added with a 0-indexed turn of {}, but then a new \ - terminal node was added with {} entries. All turn \ - indicators must be able to index terminal nodes'\ - utility entries.", - self.name, - old_count - 1, - new_count, - ), - })? - } + } else if new.terminal() && new_count < old_count { + Err(anyhow! { + format!( + "While constructing the game '{}', a medial node was \ + added with a 0-indexed turn of {}, but then a new \ + terminal node was added with {} entries. All turn \ + indicators must be able to index terminal nodes'\ + utility entries.", + self.name, + old_count - 1, + new_count, + ), + })? } if new.terminal() { @@ -301,21 +299,13 @@ impl Node { /// Returns true if and only if `self` is a terminal node. #[inline] pub const fn terminal(&self) -> bool { - if let Node::Terminal(_) = self { - true - } else { - false - } + matches!(self, Node::Terminal(_)) } /// Returns true if and only if `self` is a medial node. #[inline] pub const fn medial(&self) -> bool { - if let Node::Medial(_) = self { - true - } else { - false - } + matches!(self, Node::Medial(_)) } } diff --git a/src/game/mock/mod.rs b/src/game/mock/mod.rs index b3a2345..da78399 100644 --- a/src/game/mock/mod.rs +++ b/src/game/mock/mod.rs @@ -172,7 +172,7 @@ mod tests { .build()?; g.visualize(MODULE_NAME)?; - let states = vec![ + let states = [ g.state(&s1), g.state(&s2), g.state(&s3), diff --git a/src/game/test.rs b/src/game/test.rs index 207e91e..ed0af70 100644 --- a/src/game/test.rs +++ b/src/game/test.rs @@ -34,7 +34,7 @@ impl mock::Session<'_> { let subdir = PathBuf::from(module); let mut dir = get_directory(DevelopmentData::Visuals, subdir)?; - let name = format!("{}.svg", self.name()).replace(" ", "-"); + let name = format!("{}.svg", self.name()).replace(' ', "-"); dir.push(name); let file = File::create(dir)?; @@ -71,7 +71,7 @@ impl Display for mock::Session<'_> { mock::Node::Medial(turn) => { attrs += &format!("label=P{} ", turn); attrs += "style=filled "; - if self.start() == self.state(&node).unwrap() { + if self.start() == self.state(node).unwrap() { attrs += "shape=doublecircle "; attrs += "fillcolor=navajowhite3 "; } else { diff --git a/src/game/util.rs b/src/game/util.rs index 01bc071..6febc89 100644 --- a/src/game/util.rs +++ b/src/game/util.rs @@ -35,8 +35,8 @@ where .decode(s.clone()) .context(format!("Failed to parse line #{}.", l))?; if prev == game.start() { - for i in 1..history.len() { - let (l, s) = history[i].clone(); + for h in history.iter().skip(1) { + let (l, s) = h.clone(); let next = game .decode(s) .context(format!("Failed to parse line #{}.", l))?; diff --git a/src/game/zero_by/mod.rs b/src/game/zero_by/mod.rs index 2511715..808e898 100644 --- a/src/game/zero_by/mod.rs +++ b/src/game/zero_by/mod.rs @@ -41,9 +41,9 @@ type Elements = u64; /* GAME DATA */ -const NAME: &'static str = "zero-by"; -const AUTHORS: &'static str = "Max Fierro "; -const ABOUT: &'static str = +const NAME: &str = "zero-by"; +const AUTHORS: &str = "Max Fierro "; +const ABOUT: &str = "Many players take turns removing a number of elements from a set of arbitrary \ size. The game variant determines how many players are in the game, how many \ elements are in the set to begin with, and the options players have in the \ @@ -193,13 +193,13 @@ impl Bounded for Session { fn end(&self, state: State) -> bool { let (_, elements) = self.decode_state(state); - elements <= 0 + elements == 0 } } impl Codec for Session { fn decode(&self, string: String) -> Result { - Ok(parse_state(&self, string)?) + Ok(parse_state(self, string)?) } fn encode(&self, state: State) -> Result { @@ -226,8 +226,8 @@ impl Extensive for Session { impl SimpleUtility for Session { fn utility(&self, state: State) -> [SUtility; N] { let (turn, _) = self.decode_state(state); - let mut payoffs = [SUtility::LOSE; N]; - payoffs[turn] = SUtility::WIN; + let mut payoffs = [SUtility::Lose; N]; + payoffs[turn] = SUtility::Win; payoffs } } diff --git a/src/game/zero_by/states.rs b/src/game/zero_by/states.rs index 398e6b3..c488ac4 100644 --- a/src/game/zero_by/states.rs +++ b/src/game/zero_by/states.rs @@ -18,9 +18,9 @@ use crate::model::game::State; /* ZERO-BY STATE ENCODING */ -pub const STATE_DEFAULT: &'static str = "10-0"; -pub const STATE_PATTERN: &'static str = r"^\d+-\d+$"; -pub const STATE_PROTOCOL: &'static str = +pub const STATE_DEFAULT: &str = "10-0"; +pub const STATE_PATTERN: &str = r"^\d+-\d+$"; +pub const STATE_PROTOCOL: &str = "The state string should be two dash-separated positive integers without any \ decimal points. The first integer will indicate the amount of elements left to \ remove from the set, and the second indicates whose turn it is to remove an \ @@ -41,7 +41,7 @@ pub fn parse_state( check_state_pattern(&from)?; let params = parse_parameters(&from)?; let (elements, turn) = check_param_count(¶ms)?; - check_variant_coherence(elements, turn, &session)?; + check_variant_coherence(elements, turn, session)?; Ok(session.encode_state(turn, elements)) } @@ -49,7 +49,7 @@ pub fn parse_state( fn check_state_pattern(from: &String) -> Result<(), GameError> { let re = Regex::new(STATE_PATTERN).unwrap(); - if !re.is_match(&from) { + if !re.is_match(from) { Err(GameError::StateMalformed { game_name: NAME, hint: format!( @@ -62,22 +62,20 @@ fn check_state_pattern(from: &String) -> Result<(), GameError> { } } -fn parse_parameters(from: &String) -> Result, GameError> { +fn parse_parameters(from: &str) -> Result, GameError> { from.split('-') .map(|int_string| { int_string .parse::() .map_err(|e| GameError::StateMalformed { game_name: NAME, - hint: format!("{}", e.to_string()), + hint: e.to_string(), }) }) .collect() } -fn check_param_count( - params: &Vec, -) -> Result<(Elements, Player), GameError> { +fn check_param_count(params: &[u64]) -> Result<(Elements, Player), GameError> { if params.len() != 2 { Err(GameError::StateMalformed { game_name: NAME, diff --git a/src/game/zero_by/variants.rs b/src/game/zero_by/variants.rs index 41359e6..185089b 100644 --- a/src/game/zero_by/variants.rs +++ b/src/game/zero_by/variants.rs @@ -16,9 +16,9 @@ use crate::util::min_ubits; /* ZERO-BY VARIANT ENCODING */ -pub const VARIANT_DEFAULT: &'static str = "2-10-1-2"; -pub const VARIANT_PATTERN: &'static str = r"^[1-9]\d*(?:-[1-9]\d*)+$"; -pub const VARIANT_PROTOCOL: &'static str = +pub const VARIANT_DEFAULT: &str = "2-10-1-2"; +pub const VARIANT_PATTERN: &str = r"^[1-9]\d*(?:-[1-9]\d*)+$"; +pub const VARIANT_PROTOCOL: &str = "The variant string should be a dash-separated group of three or more positive \ integers. For example, '4-232-23-6-3-6' is valid but '598', '-23-1-5', and \ 'fifteen-2-5' are not. The first integer represents the number of players in \ @@ -67,16 +67,16 @@ fn parse_parameters(variant: &str) -> Result, GameError> { .parse::() .map_err(|e| GameError::VariantMalformed { game_name: NAME, - hint: format!("{}", e.to_string()), + hint: e.to_string(), }) }) .collect(); params } -fn check_variant_pattern(variant: &String) -> Result<(), GameError> { +fn check_variant_pattern(variant: &str) -> Result<(), GameError> { let re = Regex::new(VARIANT_PATTERN).unwrap(); - if !re.is_match(&variant) { + if !re.is_match(variant) { Err(GameError::VariantMalformed { game_name: NAME, hint: format!( @@ -89,31 +89,30 @@ fn check_variant_pattern(variant: &String) -> Result<(), GameError> { } } -fn check_param_count(params: &Vec) -> Result<(), GameError> { +fn check_param_count(params: &[u64]) -> Result<(), GameError> { if params.len() < 3 { Err(GameError::VariantMalformed { game_name: NAME, - hint: format!( - "String needs to have at least 3 dash-separated integers." - ), + hint: "String needs to have at least 3 dash-separated integers." + .to_string(), }) } else { Ok(()) } } -fn check_params_are_positive(params: &Vec) -> Result<(), GameError> { - if params.iter().any(|&x| x <= 0) { +fn check_params_are_positive(params: &[u64]) -> Result<(), GameError> { + if params.iter().any(|&x| x == 0) { Err(GameError::VariantMalformed { game_name: NAME, - hint: format!("All integers in the string must be positive."), + hint: "All integers in the string must be positive.".to_string(), }) } else { Ok(()) } } -fn parse_player_count(params: &Vec) -> Result { +fn parse_player_count(params: &[u64]) -> Result { if params[0] > (Player::MAX as u64) { Err(GameError::VariantMalformed { game_name: NAME, diff --git a/src/interface/standard/cli.rs b/src/interface/standard/cli.rs index 8a4391d..f033dd8 100644 --- a/src/interface/standard/cli.rs +++ b/src/interface/standard/cli.rs @@ -45,7 +45,7 @@ pub enum Commands { /// Run a query on an existing table in the system. Query(QueryArgs), - /// Provide information about offerings. + /// Provides information about the system's offerings. Info(InfoArgs), } @@ -125,8 +125,8 @@ pub struct InfoArgs { /* DEFAULTS PROVIDED */ /// Specify which of the game's attributes to provide information about. - #[arg(short, long)] - pub attributes: Option>, + #[arg(short, long, value_delimiter = ',')] + pub attributes: Vec, /// Format in which to send output to STDOUT. #[arg(short, long, default_value_t = InfoFormat::Legible)] diff --git a/src/interface/standard/mod.rs b/src/interface/standard/mod.rs index d6425ef..60d40a2 100644 --- a/src/interface/standard/mod.rs +++ b/src/interface/standard/mod.rs @@ -52,7 +52,6 @@ pub fn stdin_lines() -> Result> { std::io::stdin() .lock() .lines() - .into_iter() .map(|l| l.map_err(|e| anyhow!(e))) .collect() } @@ -64,15 +63,15 @@ pub fn stdin_lines() -> Result> { /// all possible game attributes are sent to STDOUT. pub fn format_and_output_game_attributes( data: GameData, - attrs: Option>, + attrs: Vec, format: InfoFormat, ) -> Result<()> { - let out = if let Some(attrs) = attrs { - util::aggregate_and_format_attributes(data, attrs, format) - .context("Failed format specified game data attributes.")? - } else { + let out = if attrs.is_empty() { util::aggregate_and_format_all_attributes(data, format) .context("Failed to format game data attributes.")? + } else { + util::aggregate_and_format_attributes(data, attrs, format) + .context("Failed format specified game data attributes.")? }; print!("{}", out); Ok(()) diff --git a/src/main.rs b/src/main.rs index db418f6..8ddf314 100644 --- a/src/main.rs +++ b/src/main.rs @@ -58,12 +58,21 @@ fn solve(args: SolveArgs) -> Result<()> { interface::standard::confirm_potential_overwrite(args.yes, args.mode); match args.target { GameModule::ZeroBy => { - let mut session = game::zero_by::Session::new(args.variant)?; + let mut session = game::zero_by::Session::new(args.variant) + .context("Failed to initialize zero-by game session.")?; + if args.forward { - let history = interface::standard::stdin_lines()?; - session.forward(history)?; + let history = interface::standard::stdin_lines() + .context("Failed to read input lines from STDIN.")?; + + session + .forward(history) + .context("Failed to forward game state.")?; } - session.solve(args.mode, args.solution)? + + session + .solve(args.mode, args.solution) + .context("Failed to execute solving algorithm.")? }, } Ok(()) diff --git a/src/model.rs b/src/model.rs index f476bab..009cb71 100644 --- a/src/model.rs +++ b/src/model.rs @@ -86,10 +86,10 @@ pub mod solver { /// in consideration, but this is ultimately an intuitive notion. #[derive(Clone, Copy)] pub enum SUtility { - WIN = 0, - LOSE = 1, - DRAW = 2, - TIE = 3, + Lose = 0, + Draw = 1, + Tie = 2, + Win = 3, } } diff --git a/src/solver/mod.rs b/src/solver/mod.rs index 8cf6153..07362ad 100644 --- a/src/solver/mod.rs +++ b/src/solver/mod.rs @@ -216,7 +216,7 @@ where G: Extensive<2, B> + ClassicGame, { fn utility(&self, state: State) -> [SUtility; 2] { - let mut sutility = [SUtility::TIE; 2]; + let mut sutility = [SUtility::Tie; 2]; let utility = self.utility(state); let turn = self.turn(state); let them = (turn + 1) % 2; diff --git a/src/solver/record/mur.rs b/src/solver/record/mur.rs index a2f88fa..186e6a6 100644 --- a/src/solver/record/mur.rs +++ b/src/solver/record/mur.rs @@ -38,7 +38,7 @@ pub const UTILITY_SIZE: usize = 8; pub fn schema(players: PlayerCount) -> Result { if RecordBuffer::bit_size(players) > BUFFER_SIZE { Err(RecordViolation { - name: RecordType::MUR(players).into(), + name: RecordType::MUR(players).to_string(), hint: format!( "This record can only hold utility values for up to {} \ players, but there was an attempt to create a schema that \ @@ -111,7 +111,7 @@ impl RecordBuffer { pub fn new(players: PlayerCount) -> Result { if Self::bit_size(players) > BUFFER_SIZE { Err(RecordViolation { - name: RecordType::MUR(players).into(), + name: RecordType::MUR(players).to_string(), hint: format!( "The record can only hold utility values for up to {} \ players, but there was an attempt to instantiate one for \ @@ -135,7 +135,7 @@ impl RecordBuffer { let len = bits.len(); if len > BUFFER_SIZE { Err(RecordViolation { - name: RecordType::MUR(0).into(), + name: RecordType::MUR(0).to_string(), hint: format!( "The record implementation operates on a buffer of {} \ bits, but there was an attempt to instantiate one from a \ @@ -145,7 +145,7 @@ impl RecordBuffer { })? } else if len < Self::minimum_bit_size() { Err(RecordViolation { - name: RecordType::MUR(0).into(), + name: RecordType::MUR(0).to_string(), hint: format!( "This record implementation stores utility values, but \ there was an attempt to instantiate one with from a buffer \ @@ -170,7 +170,7 @@ impl RecordBuffer { pub fn get_utility(&self, player: Player) -> Result { if player >= self.players { Err(RecordViolation { - name: RecordType::MUR(self.players).into(), + name: RecordType::MUR(self.players).to_string(), hint: format!( "A record was instantiated with {} utility entries, and \ there was an attempt to fetch the utility of player {} \ @@ -207,7 +207,7 @@ impl RecordBuffer { ) -> Result<()> { if N != self.players { Err(RecordViolation { - name: RecordType::MUR(self.players).into(), + name: RecordType::MUR(self.players).to_string(), hint: format!( "A record was instantiated with {} utility entries, and \ there was an attempt to use a {}-entry utility list to \ @@ -221,7 +221,7 @@ impl RecordBuffer { let size = util::min_sbits(utility); if size > UTILITY_SIZE { Err(RecordViolation { - name: RecordType::MUR(self.players).into(), + name: RecordType::MUR(self.players).to_string(), hint: format!( "This record implementation uses {} bits to store \ signed integers representing utility values, but \ @@ -247,7 +247,7 @@ impl RecordBuffer { let size = util::min_ubits(value); if size > REMOTENESS_SIZE { Err(RecordViolation { - name: RecordType::MUR(self.players).into(), + name: RecordType::MUR(self.players).to_string(), hint: format!( "This record implementation uses {} bits to store unsigned \ integers representing remoteness values, but there was an \ @@ -390,7 +390,7 @@ mod tests { } #[test] - fn data_is_valid_after_round_trip() { + fn data_is_valid_after_round_trip() -> Result<()> { let mut record = RecordBuffer::new(5).unwrap(); let payoffs = [10, -2, -8, 100, 0]; let remoteness = 790; @@ -403,25 +403,19 @@ mod tests { .set_remoteness(remoteness) .unwrap(); - // Utilities unchanged after insert and fetch - for i in 0..5 { - let fetched_utility = record.get_utility(i).unwrap(); - let actual_utility = payoffs[i]; - assert_eq!(fetched_utility, actual_utility); + for (i, actual) in payoffs.iter().enumerate() { + assert_eq!(record.get_utility(i)?, *actual); } - // Remoteness unchanged after insert and fetch - let fetched_remoteness = record.get_remoteness(); - let actual_remoteness = remoteness; - assert_eq!(fetched_remoteness, actual_remoteness); - - // Fetching utility entries of invalid players + assert_eq!(record.get_remoteness(), remoteness); assert!(record.get_utility(5).is_err()); assert!(record.get_utility(10).is_err()); + + Ok(()) } #[test] - fn extreme_data_is_valid_after_round_trip() { + fn extreme_data_is_valid_after_round_trip() -> Result<()> { let mut record = RecordBuffer::new(6).unwrap(); let good = [ @@ -442,18 +436,15 @@ mod tests { MIN_UTILITY - 1, ]; - assert!(record.set_utility(good).is_ok()); - assert!(record - .set_remoteness(MAX_REMOTENESS) - .is_ok()); - - for i in 0..6 { - let fetched_utility = record.get_utility(i).unwrap(); - let actual_utility = good[i]; - assert_eq!(fetched_utility, actual_utility); + record.set_utility(good)?; + record.set_remoteness(MAX_REMOTENESS)?; + for (i, actual) in good.iter().enumerate() { + assert_eq!(record.get_utility(i)?, *actual); } assert_eq!(record.get_remoteness(), MAX_REMOTENESS); assert!(record.set_utility(bad).is_err()); + + Ok(()) } } diff --git a/src/solver/record/rem.rs b/src/solver/record/rem.rs index e62c61b..022441f 100644 --- a/src/solver/record/rem.rs +++ b/src/solver/record/rem.rs @@ -73,7 +73,7 @@ impl RecordBuffer { let len = bits.len(); if len > BUFFER_SIZE { Err(RecordViolation { - name: RecordType::REM.into(), + name: RecordType::REM.to_string(), hint: format!( "The record implementation operates on a buffer of {} \ bits, but there was an attempt to instantiate one from a \ @@ -83,7 +83,7 @@ impl RecordBuffer { })? } else if len < Self::minimum_bit_size() { Err(RecordViolation { - name: RecordType::REM.into(), + name: RecordType::REM.to_string(), hint: format!( "This record implementation stores remoteness values, but \ there was an attempt to instantiate one with from a buffer \ @@ -119,7 +119,7 @@ impl RecordBuffer { let size = util::min_ubits(value); if size > REMOTENESS_SIZE { Err(RecordViolation { - name: RecordType::REM.into(), + name: RecordType::REM.to_string(), hint: format!( "This record implementation uses {} bits to store unsigned \ integers representing remoteness values, but there was an \ @@ -163,14 +163,14 @@ mod tests { use super::*; - // The maximum numeric remoteness value that can be expressed with exactly - // REMOTENESS_SIZE bits in an unsigned integer. + /// The maximum numeric remoteness value that can be expressed with exactly + /// REMOTENESS_SIZE bits in an unsigned integer. const MAX_REMOTENESS: Remoteness = 2_u64.pow(REMOTENESS_SIZE as u32) - 1; #[test] fn initialize_from_valid_buffer() { let buf = bitarr!(u8, Msb0; 0; BUFFER_SIZE); - for i in REMOTENESS_SIZE..BUFFER_SIZE { + for i in 1..BUFFER_SIZE { assert!(RecordBuffer::from(&buf[0..i]).is_ok()); } } diff --git a/src/solver/record/sur.rs b/src/solver/record/sur.rs index 2e7dc58..45b7d63 100644 --- a/src/solver/record/sur.rs +++ b/src/solver/record/sur.rs @@ -39,7 +39,7 @@ pub const UTILITY_SIZE: usize = 2; pub fn schema(players: PlayerCount) -> Result { if RecordBuffer::bit_size(players) > BUFFER_SIZE { Err(RecordViolation { - name: RecordType::SUR(players).into(), + name: RecordType::SUR(players).to_string(), hint: format!( "This record can only hold utility values for up to {} \ players, but there was an attempt to create a schema that \ @@ -112,7 +112,7 @@ impl RecordBuffer { pub fn new(players: PlayerCount) -> Result { if Self::bit_size(players) > BUFFER_SIZE { Err(RecordViolation { - name: RecordType::SUR(players).into(), + name: RecordType::SUR(players).to_string(), hint: format!( "The record can only hold utility values for up to {} \ players, but there was an attempt to instantiate one for \ @@ -136,7 +136,7 @@ impl RecordBuffer { let len = bits.len(); if len > BUFFER_SIZE { Err(RecordViolation { - name: RecordType::SUR(0).into(), + name: RecordType::SUR(0).to_string(), hint: format!( "The record implementation operates on a buffer of {} \ bits, but there was an attempt to instantiate one from a \ @@ -146,7 +146,7 @@ impl RecordBuffer { })? } else if len < Self::minimum_bit_size() { Err(RecordViolation { - name: RecordType::SUR(0).into(), + name: RecordType::SUR(0).to_string(), hint: format!( "This record implementation stores utility values, but \ there was an attempt to instantiate one with from a buffer \ @@ -171,7 +171,7 @@ impl RecordBuffer { pub fn get_utility(&self, player: Player) -> Result { if player >= self.players { Err(RecordViolation { - name: RecordType::SUR(self.players).into(), + name: RecordType::SUR(self.players).to_string(), hint: format!( "A record was instantiated with {} utility entries, and \ there was an attempt to fetch the utility of player {} \ @@ -209,7 +209,7 @@ impl RecordBuffer { ) -> Result<()> { if N != self.players { Err(RecordViolation { - name: RecordType::SUR(self.players).into(), + name: RecordType::SUR(self.players).to_string(), hint: format!( "A record was instantiated with {} utility entries, and \ there was an attempt to use a {}-entry utility list to \ @@ -223,7 +223,7 @@ impl RecordBuffer { let size = util::min_ubits(utility); if size > UTILITY_SIZE { Err(RecordViolation { - name: RecordType::SUR(self.players).into(), + name: RecordType::SUR(self.players).to_string(), hint: format!( "This record implementation uses {} bits to store \ signed integers representing utility values, but \ @@ -249,7 +249,7 @@ impl RecordBuffer { let size = util::min_ubits(value); if size > REMOTENESS_SIZE { Err(RecordViolation { - name: RecordType::SUR(self.players).into(), + name: RecordType::SUR(self.players).to_string(), hint: format!( "This record implementation uses {} bits to store unsigned \ integers representing remoteness values, but there was an \ @@ -314,8 +314,8 @@ mod tests { // * `MIN_UTILITY = 0b10000000 = -128 = -127 - 1` // // Useful: https://www.omnicalculator.com/math/twos-complement - const MAX_UTILITY: SUtility = SUtility::TIE; - const MIN_UTILITY: SUtility = SUtility::WIN; + const MAX_UTILITY: SUtility = SUtility::Tie; + const MIN_UTILITY: SUtility = SUtility::Win; // The maximum numeric remoteness value that can be expressed with exactly // REMOTENESS_SIZE bits in an unsigned integer. @@ -362,13 +362,13 @@ mod tests { let mut r2 = RecordBuffer::new(4)?; let mut r3 = RecordBuffer::new(0)?; - let v1 = [SUtility::WIN; 7]; - let v2 = [SUtility::TIE; 4]; + let v1 = [SUtility::Win; 7]; + let v2 = [SUtility::Tie; 4]; let v3: [SUtility; 0] = []; let v4 = [MAX_UTILITY; 7]; let v5 = [MIN_UTILITY; 4]; - let v6 = [SUtility::DRAW]; + let v6 = [SUtility::Draw]; let good = Remoteness::MIN; let bad = Remoteness::MAX; @@ -396,31 +396,25 @@ mod tests { fn data_is_valid_after_round_trip() -> Result<()> { let mut record = RecordBuffer::new(5)?; let payoffs = [ - SUtility::LOSE, - SUtility::WIN, - SUtility::LOSE, - SUtility::LOSE, - SUtility::LOSE, + SUtility::Lose, + SUtility::Win, + SUtility::Lose, + SUtility::Lose, + SUtility::Lose, ]; - let remoteness = 790; + let remoteness = 790; record.set_utility(payoffs)?; - record.set_remoteness(remoteness)?; - // Utilities unchanged after insert and fetch - for i in 0..5 { - let fetched_utility = record.get_utility(i)?; - let actual_utility = payoffs[i]; - assert!(matches!(fetched_utility, actual_utility)); + for (i, _actual) in payoffs.iter().enumerate() { + assert!(matches!(record.get_utility(i)?, _actual)); } - // Remoteness unchanged after insert and fetch let fetched_remoteness = record.get_remoteness(); let actual_remoteness = remoteness; assert_eq!(fetched_remoteness, actual_remoteness); - // Fetching utility entries of invalid players assert!(record.get_utility(5).is_err()); assert!(record.get_utility(10).is_err()); Ok(()) @@ -429,27 +423,23 @@ mod tests { #[test] fn extreme_data_is_valid_after_round_trip() -> Result<()> { let mut record = RecordBuffer::new(6)?; - let good = [ - SUtility::WIN, - SUtility::LOSE, - SUtility::TIE, - SUtility::TIE, - SUtility::DRAW, - SUtility::WIN, + SUtility::Win, + SUtility::Lose, + SUtility::Tie, + SUtility::Tie, + SUtility::Draw, + SUtility::Win, ]; - let bad = [SUtility::DRAW, SUtility::WIN, SUtility::TIE]; - + let bad = [SUtility::Draw, SUtility::Win, SUtility::Tie]; assert!(record.set_utility(good).is_ok()); assert!(record .set_remoteness(MAX_REMOTENESS) .is_ok()); - for i in 0..6 { - let fetched_utility = record.get_utility(i)?; - let actual_utility = good[i]; - assert!(matches!(fetched_utility, actual_utility)); + for (i, _actual) in good.iter().enumerate() { + assert!(matches!(record.get_utility(i)?, _actual)); } assert_eq!(record.get_remoteness(), MAX_REMOTENESS); diff --git a/src/solver/util.rs b/src/solver/util.rs index a0a8230..26ffc6e 100644 --- a/src/solver/util.rs +++ b/src/solver/util.rs @@ -6,6 +6,7 @@ //! #### Authorship //! - Max Fierro, 2/24/2024 (maxfierro@berkeley.edu) +use std::fmt::Display; use std::ops::Not; use crate::database::Schema; @@ -15,18 +16,20 @@ use crate::solver::{record, RecordType}; /* RECORD TYPE IMPLEMENTATIONS */ -impl Into for RecordType { - fn into(self) -> String { +impl Display for RecordType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { RecordType::MUR(players) => { - format!("Real Utility Remoteness ({} players)", players) + write!(f, "Real Utility Remoteness ({} players)", players) }, RecordType::SUR(players) => { - format!("Simple Utility Remoteness ({} players)", players) - }, - RecordType::REM => { - format!("Remoteness (no utility)") + write!( + f, + "Simple Utility Remoteness ({} players)", + players + ) }, + RecordType::REM => write!(f, "Remoteness (no utility)"), } } } @@ -50,18 +53,17 @@ impl TryFrom for SUtility { fn try_from(v: IUtility) -> Result { match v { - v if v == SUtility::LOSE as i64 => Ok(SUtility::LOSE), - v if v == SUtility::DRAW as i64 => Ok(SUtility::DRAW), - v if v == SUtility::TIE as i64 => Ok(SUtility::TIE), - v if v == SUtility::WIN as i64 => Ok(SUtility::WIN), + v if v == SUtility::Lose as i64 => Ok(SUtility::Lose), + v if v == SUtility::Draw as i64 => Ok(SUtility::Draw), + v if v == SUtility::Tie as i64 => Ok(SUtility::Tie), + v if v == SUtility::Win as i64 => Ok(SUtility::Win), _ => Err(SolverError::InvalidConversion { input_t: "Integer Utility".into(), output_t: "Simple Utility".into(), hint: "Down-casting from integer to simple utility values is not \ stable, and relies on the internal representation used for \ - simple utility values (which is not intuitive). As of \ - right now though, WIN = 0, TIE = 3, DRAW = 2, and LOSE = 1." + simple utility values (which is not intuitive)." .into(), }), } @@ -73,17 +75,17 @@ impl TryFrom for SUtility { fn try_from(v: RUtility) -> Result { match v { - v if v as i64 == SUtility::LOSE as i64 => Ok(SUtility::LOSE), - v if v as i64 == SUtility::DRAW as i64 => Ok(SUtility::DRAW), - v if v as i64 == SUtility::TIE as i64 => Ok(SUtility::TIE), - v if v as i64 == SUtility::WIN as i64 => Ok(SUtility::WIN), + v if v as i64 == SUtility::Lose as i64 => Ok(SUtility::Lose), + v if v as i64 == SUtility::Draw as i64 => Ok(SUtility::Draw), + v if v as i64 == SUtility::Tie as i64 => Ok(SUtility::Tie), + v if v as i64 == SUtility::Win as i64 => Ok(SUtility::Win), _ => Err(SolverError::InvalidConversion { input_t: "Real Utility".into(), output_t: "Simple Utility".into(), - hint: - "Simple Utility values can only have pre-specified values \ - (which are subject to change)." - .into(), + hint: "Down-casting from real-valued to simple utility values \ + is not stable, and relies on the internal representation \ + used for simple utility values (which is not intuitive)." + .into(), }), } } @@ -94,17 +96,17 @@ impl TryFrom for SUtility { fn try_from(v: u64) -> Result { match v { - v if v as i64 == SUtility::LOSE as i64 => Ok(SUtility::LOSE), - v if v as i64 == SUtility::DRAW as i64 => Ok(SUtility::DRAW), - v if v as i64 == SUtility::TIE as i64 => Ok(SUtility::TIE), - v if v as i64 == SUtility::WIN as i64 => Ok(SUtility::WIN), + v if v as i64 == SUtility::Lose as i64 => Ok(SUtility::Lose), + v if v as i64 == SUtility::Draw as i64 => Ok(SUtility::Draw), + v if v as i64 == SUtility::Tie as i64 => Ok(SUtility::Tie), + v if v as i64 == SUtility::Win as i64 => Ok(SUtility::Win), _ => Err(SolverError::InvalidConversion { - input_t: "Real Utility".into(), + input_t: "u64".into(), output_t: "Simple Utility".into(), - hint: - "Simple Utility values can only have pre-specified values \ - (which are subject to change)." - .into(), + hint: "Down-casting from integer to simple utility values \ + is not stable, and relies on the internal representation \ + used for simple utility values (which is not intuitive)." + .into(), }), } } @@ -112,24 +114,24 @@ impl TryFrom for SUtility { /* CONVERSIONS FROM SIMPLE UTILITY */ -impl Into for SUtility { - fn into(self) -> IUtility { - match self { - SUtility::LOSE => -1, - SUtility::DRAW => 0, - SUtility::TIE => 0, - SUtility::WIN => 1, +impl From for IUtility { + fn from(v: SUtility) -> Self { + match v { + SUtility::Lose => -1, + SUtility::Draw => 0, + SUtility::Tie => 0, + SUtility::Win => 1, } } } -impl Into for SUtility { - fn into(self) -> RUtility { - match self { - SUtility::LOSE => -1.0, - SUtility::DRAW => 0.0, - SUtility::TIE => 0.0, - SUtility::WIN => 1.0, +impl From for RUtility { + fn from(v: SUtility) -> Self { + match v { + SUtility::Lose => -1.0, + SUtility::Draw => 0.0, + SUtility::Tie => 0.0, + SUtility::Win => 1.0, } } } @@ -140,10 +142,10 @@ impl Not for SUtility { type Output = SUtility; fn not(self) -> Self::Output { match self { - SUtility::DRAW => SUtility::DRAW, - SUtility::LOSE => SUtility::WIN, - SUtility::WIN => SUtility::LOSE, - SUtility::TIE => SUtility::TIE, + SUtility::Draw => SUtility::Draw, + SUtility::Lose => SUtility::Win, + SUtility::Win => SUtility::Lose, + SUtility::Tie => SUtility::Tie, } } } From 69d4ba0a4ac2fd7925b3ae7674f671a3a27573b5 Mon Sep 17 00:00:00 2001 From: Max Fierro Date: Tue, 7 May 2024 03:16:34 -0700 Subject: [PATCH 12/15] Fixed flaky test --- src/main.rs | 3 +-- src/solver/record/rem.rs | 47 +++++++++++++--------------------------- 2 files changed, 16 insertions(+), 34 deletions(-) diff --git a/src/main.rs b/src/main.rs index 8ddf314..c3b3e91 100644 --- a/src/main.rs +++ b/src/main.rs @@ -86,7 +86,6 @@ fn info(args: InfoArgs) -> Result<()> { data, args.attributes, args.output, - ) - .context("Failed to format and output game attributes.")?; + )?; Ok(()) } diff --git a/src/solver/record/rem.rs b/src/solver/record/rem.rs index 022441f..c26c5de 100644 --- a/src/solver/record/rem.rs +++ b/src/solver/record/rem.rs @@ -87,7 +87,7 @@ impl RecordBuffer { hint: format!( "This record implementation stores remoteness values, but \ there was an attempt to instantiate one with from a buffer \ - with {} bits, which is not enough to store a remoteness \ + with {} bit(s), which is not enough to store a remoteness \ value (which takes {} bits).", len, REMOTENESS_SIZE, ), @@ -167,14 +167,6 @@ mod tests { /// REMOTENESS_SIZE bits in an unsigned integer. const MAX_REMOTENESS: Remoteness = 2_u64.pow(REMOTENESS_SIZE as u32) - 1; - #[test] - fn initialize_from_valid_buffer() { - let buf = bitarr!(u8, Msb0; 0; BUFFER_SIZE); - for i in 1..BUFFER_SIZE { - assert!(RecordBuffer::from(&buf[0..i]).is_ok()); - } - } - #[test] fn initialize_from_invalid_buffer_size() { let buf1 = bitarr!(u8, Msb0; 0; BUFFER_SIZE + 1); @@ -187,44 +179,35 @@ mod tests { } #[test] - fn set_record_attributes() { + fn set_record_attributes() -> Result<()> { let mut r1 = RecordBuffer::new().unwrap(); let good = Remoteness::MIN; let bad = Remoteness::MAX; - assert!(r1.set_remoteness(good).is_ok()); - assert!(r1.set_remoteness(good).is_ok()); - assert!(r1.set_remoteness(good).is_ok()); - + r1.set_remoteness(good)?; + r1.set_remoteness(good)?; + r1.set_remoteness(good)?; assert!(r1.set_remoteness(bad).is_err()); assert!(r1.set_remoteness(bad).is_err()); assert!(r1.set_remoteness(bad).is_err()); + + Ok(()) } #[test] - fn data_is_valid_after_round_trip() { - let mut record = RecordBuffer::new().unwrap(); - let remoteness = 790; - - record - .set_remoteness(remoteness) - .unwrap(); - - // Remoteness unchanged after insert and fetch - let fetched_remoteness = record.get_remoteness(); - let actual_remoteness = remoteness; - assert_eq!(fetched_remoteness, actual_remoteness); + fn data_is_valid_after_round_trip() -> Result<()> { + let mut record = RecordBuffer::new()?; + record.set_remoteness(790)?; + assert_eq!(record.get_remoteness(), 790); + Ok(()) } #[test] - fn extreme_data_is_valid_after_round_trip() { + fn extreme_data_is_valid_after_round_trip() -> Result<()> { let mut record = RecordBuffer::new().unwrap(); - - assert!(record - .set_remoteness(MAX_REMOTENESS) - .is_ok()); - + record.set_remoteness(MAX_REMOTENESS)?; assert_eq!(record.get_remoteness(), MAX_REMOTENESS); + Ok(()) } } From 59c9e320901a4eff5f074eb9fc4b81d88974ee96 Mon Sep 17 00:00:00 2001 From: Max Fierro Date: Tue, 7 May 2024 04:21:08 -0700 Subject: [PATCH 13/15] Progress on solver interfaces docs --- Cargo.lock | 241 +------------------------ Cargo.toml | 2 - src/game/zero_by/mod.rs | 4 +- src/solver/algorithm/strong/acyclic.rs | 8 +- src/solver/mod.rs | 218 +++++++++++++++------- 5 files changed, 166 insertions(+), 307 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4ce8425..e3571a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,27 +65,6 @@ version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" -[[package]] -name = "approx" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" -dependencies = [ - "num-traits", -] - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "bitflags" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" - [[package]] name = "bitvec" version = "1.0.1" @@ -98,12 +77,6 @@ dependencies = [ "wyz", ] -[[package]] -name = "bytemuck" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" - [[package]] name = "clap" version = "4.4.6" @@ -135,7 +108,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.57", + "syn", ] [[package]] @@ -150,33 +123,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" -[[package]] -name = "colored" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6" -dependencies = [ - "is-terminal", - "lazy_static", - "windows-sys", -] - [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" -[[package]] -name = "errno" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" -dependencies = [ - "libc", - "windows-sys", -] - [[package]] name = "exitcode" version = "1.1.2" @@ -202,9 +154,7 @@ dependencies = [ "anyhow", "bitvec", "clap", - "colored", "exitcode", - "nalgebra", "petgraph", "regex", "serde_json", @@ -224,12 +174,6 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" -[[package]] -name = "hermit-abi" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" - [[package]] name = "indexmap" version = "2.2.6" @@ -240,129 +184,18 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "is-terminal" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" -dependencies = [ - "hermit-abi", - "rustix", - "windows-sys", -] - [[package]] name = "itoa" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "libc" -version = "0.2.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" - -[[package]] -name = "linux-raw-sys" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" - -[[package]] -name = "matrixmultiply" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7574c1cf36da4798ab73da5b215bbf444f50718207754cb522201d78d1cd0ff2" -dependencies = [ - "autocfg", - "rawpointer", -] - [[package]] name = "memchr" version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" -[[package]] -name = "nalgebra" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "307ed9b18cc2423f29e83f84fd23a8e73628727990181f18641a8b5dc2ab1caa" -dependencies = [ - "approx", - "matrixmultiply", - "nalgebra-macros", - "num-complex", - "num-rational", - "num-traits", - "simba", - "typenum", -] - -[[package]] -name = "nalgebra-macros" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91761aed67d03ad966ef783ae962ef9bbaca728d2dd7ceb7939ec110fffad998" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "num-complex" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" -dependencies = [ - "autocfg", -] - -[[package]] -name = "paste" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" - [[package]] name = "petgraph" version = "0.6.4" @@ -397,12 +230,6 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" -[[package]] -name = "rawpointer" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" - [[package]] name = "regex" version = "1.9.6" @@ -432,19 +259,6 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" -[[package]] -name = "rustix" -version = "0.38.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys", -] - [[package]] name = "rustversion" version = "1.0.15" @@ -457,15 +271,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" -[[package]] -name = "safe_arch" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f398075ce1e6a179b46f51bd88d0598b92b00d3551f1a2d4ac49e771b56ac354" -dependencies = [ - "bytemuck", -] - [[package]] name = "serde" version = "1.0.188" @@ -483,7 +288,7 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.57", + "syn", ] [[package]] @@ -497,19 +302,6 @@ dependencies = [ "serde", ] -[[package]] -name = "simba" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061507c94fc6ab4ba1c9a0305018408e312e17c041eb63bef8aa726fa33aceae" -dependencies = [ - "approx", - "num-complex", - "num-traits", - "paste", - "wide", -] - [[package]] name = "strsim" version = "0.10.0" @@ -532,18 +324,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.57", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "syn", ] [[package]] @@ -563,12 +344,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" -[[package]] -name = "typenum" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" - [[package]] name = "unicode-ident" version = "1.0.12" @@ -581,16 +356,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" -[[package]] -name = "wide" -version = "0.7.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c68938b57b33da363195412cfc5fc37c9ed49aa9cfe2156fde64b8d2c9498242" -dependencies = [ - "bytemuck", - "safe_arch", -] - [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index 947506c..57800dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,8 +25,6 @@ lto = "fat" clap = { version = "^4", features = ["derive"] } serde_json = "^1" exitcode = "^1" -nalgebra = "^0" -colored = "^2" anyhow = "^1" bitvec = "^1" regex = "^1" diff --git a/src/game/zero_by/mod.rs b/src/game/zero_by/mod.rs index 808e898..c47ea44 100644 --- a/src/game/zero_by/mod.rs +++ b/src/game/zero_by/mod.rs @@ -27,7 +27,7 @@ use crate::model::game::Variant; use crate::model::game::{Player, PlayerCount, State}; use crate::model::solver::SUtility; use crate::solver::algorithm::strong; -use crate::solver::{Extensive, SimpleUtility}; +use crate::solver::{Sequential, SimpleUtility}; /* SUBMODULES */ @@ -216,7 +216,7 @@ impl Forward for Session { /* SOLVING IMPLEMENTATIONS */ -impl Extensive for Session { +impl Sequential for Session { fn turn(&self, state: State) -> Player { let (turn, _) = self.decode_state(state); turn diff --git a/src/solver/algorithm/strong/acyclic.rs b/src/solver/algorithm/strong/acyclic.rs index 099c8a4..774e243 100644 --- a/src/solver/algorithm/strong/acyclic.rs +++ b/src/solver/algorithm/strong/acyclic.rs @@ -14,7 +14,7 @@ use crate::interface::IOMode; use crate::model::game::PlayerCount; use crate::model::solver::{IUtility, Remoteness}; use crate::solver::record::mur::RecordBuffer; -use crate::solver::{Extensive, IntegerUtility, RecordType}; +use crate::solver::{IntegerUtility, RecordType, Sequential}; use crate::util::Identify; /* SOLVERS */ @@ -27,7 +27,7 @@ where G: Transition + Bounded + IntegerUtility - + Extensive + + Sequential + Identify, { let db = volatile_database(game, mode) @@ -53,7 +53,7 @@ fn volatile_database( mode: IOMode, ) -> Result where - G: Extensive + Identify, + G: Sequential + Identify, { let id = game.id(); let db = volatile::Database::initialize(); @@ -80,7 +80,7 @@ fn backward_induction( ) -> Result<()> where D: KVStore, - G: Transition + Bounded + IntegerUtility + Extensive, + G: Transition + Bounded + IntegerUtility + Sequential, { let mut stack = Vec::new(); stack.push(game.start()); diff --git a/src/solver/mod.rs b/src/solver/mod.rs index 07362ad..7f7b36d 100644 --- a/src/solver/mod.rs +++ b/src/solver/mod.rs @@ -82,100 +82,196 @@ pub enum RecordType { /* STRUCTURAL INTERFACES */ -/// TODO -pub trait Extensive { - /// 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. +/// Indicates that a discrete game is played sequentially, which allows for +/// representing histories as discrete [`State`]s. +pub trait Sequential { + /// Returns the player `i` whose turn it is at the given `state`, with + /// Player 0 always going first. + /// + /// TODO + /// + /// # Example + /// + /// TODO + /// + /// # Warning + /// + /// The player identifier `i` should never be greater than `N - 1`, where + /// `N` is the number of players in the game. Violating this will definitely + /// result in a program panic at some point. Unfortunately, there are not + /// many good ways of enforcing this restriction at compilation time. fn turn(&self, state: State) -> Player; - /// Returns the number of players in the underlying game. This should be at - /// least one higher than the maximum value returned by `turn`. + /// Returns the number of players in the underlying game. #[inline(always)] fn players(&self) -> PlayerCount { N } } -/// TODO -pub trait Composite -where - Self: Extensive, -{ - /// Returns a unique identifier for the partition that `state` is an element - /// of within the game variant specified by `self`. This implies no notion - /// of ordering between identifiers. +/// Indicates that a game can be partitioned into subsets that are convenient +/// for computation purposes. +pub trait Composite { + /// Returns an identifier for a subset of the space of states which `state` + /// belongs to. + /// + /// TODO + /// + /// # Example + /// + /// TODO fn partition(&self, state: State) -> Partition; - /// Provides an arbitrarily precise notion of the number of states that are - /// elements of `partition`. This can be used to distribute the work of - /// analyzing different partitions concurrently across different consumers - /// in a way that is equitable to improve efficiency. + /// Returns an estimate of the number of states in `partition`. + /// + /// It can be useful to know how many states are in a given partition for + /// practical purposes, such as distributing the load of exploring different + /// partitions in a balanced way; counting the states precisely would more + /// often than not defeat the purpose of saving computation. + /// + /// For many purposes, it is also enough for the return value of this + /// function to be accurate relative to other partitions, and not in actual + /// magnitude. The precise semantics of the output are left unspecified. + /// + /// # Example + /// + /// An example heuristic that could be used to estimate the number of states + /// in a game partition could be the amount of information required to be + /// able to differentiate among states within it. For example, if there are + /// more pieces on a board game partition than another, it is likely that it + /// has more states in it. fn size(&self, partition: Partition) -> StateCount; } /* UTILITY MEASURE INTERFACES */ -/// TODO +/// Indicates that a multiplayer game's players can only obtain utility values +/// that are real numbers. pub trait RealUtility { - /// If `state` is terminal, returns the utility vector associated with that - /// state, where `utility[i]` is the utility of the state for player `i`. If - /// the state is not terminal, it is recommended that this function panics. + /// Returns the utility vector associated with a terminal `state` whose + /// `i`'th entry is the utility of the state for player `i`. + /// + /// The behavior of this function is undefined in cases where `state` is not + /// terminal. No assumptions are made about the possible utility values that + /// players can obtain through playing the game, except that they can be + /// represented with real numbers (see [`RUtility`]). + /// + /// # Example + /// + /// An extreme example of such a game would be a discrete auction, where + /// players can gain arbitrary amounts of money, but can only act at known + /// discrete points in time. fn utility(&self, state: State) -> [RUtility; N]; } -/// TODO +/// Indicates that a multiplayer game's players can only obtain utility values +/// that are integers. pub trait IntegerUtility { - /// If `state` is terminal, returns the utility vector associated with that - /// state, where `utility[i]` is the utility of the state for player `i`. If - /// the state is not terminal it is recommended that this function panics. + /// Returns the utility vector associated with a terminal `state` where + /// whose `i`'th entry is the utility of the state for player `i`. + /// + /// The behavior of this function is undefined in cases where `state` is not + /// terminal. No assumptions are made about the possible utility values that + /// players can obtain through playing the game, except that they can be + /// represented with integers; the [`IUtility`] type serves this purpose. + /// + /// # Example + /// + /// An extreme example of such a game would be ten people fighting over + /// each other's coins. Since the coins are discrete, it is only possible + /// to gain utility in specific increments. We can model this hypothetical + /// game through this interface. fn utility(&self, state: State) -> [IUtility; N]; } -/// TODO +/// Indicates that a multiplayer game's players can only obtain utility values +/// within specific known categories. pub trait SimpleUtility { - /// If `state` is terminal, returns the utility vector associated with that - /// state, where `utility[i]` is the utility of the state for player `i`. If - /// the state is not terminal, it is recommended that this function panics. + /// Returns the utility vector associated with a terminal `state` where + /// whose `i`'th entry is the utility of the state for player `i`. + /// + /// The behavior of this function is undefined in cases where `state` is not + /// terminal. This assumes that utility players' utility values can only + /// be within the following categories: + /// * [`SUtility::Lose`] + /// * [`SUtility::Draw`] + /// * [`SUtility::Tie`] + /// * [`SUtility::Win`] + /// + /// # Example + /// + /// In a 6-player game of Chinese Checkers, it is possible for one player + /// to obtain a [`SUtility::Win`] by finishing first (in the event where + /// utility is defined without 2nd through 6th places), and everyone else + /// would be assigned a [`SUtility::Lose`]. fn utility(&self, state: State) -> [SUtility; N]; } /* UTILITY STRUCTURE INTERFACES */ -/// Indicates that a game is 2-player, simple-sum, and zero-sum; this restricts -/// the possible utilities for a position to the following cases: -/// * `[Draw, Draw]` -/// * `[Lose, Win]` -/// * `[Win, Lose]` -/// * `[Tie, Tie]` -/// -/// Since either entry determines the other, knowing one of the entries and the -/// turn information for a given state provides enough information to determine -/// both players' utilities. +/// Indicates that a game is 2-player, has categorical utility values, and is +/// zero-sum, allowing the implementer to eschew providing a player's utility. pub trait ClassicGame { - /// If `state` is terminal, returns the utility of the player whose turn it - /// is at that state. If the state is not terminal, it is recommended that - /// this function panics. + /// Returns the utility of the only player whose turn it is at `state`. + /// + /// This assumes that `state` is terminal, that the underlying game is + /// two-player and zero-sum. In other words, the only attainable pairs of + /// utility values should be the following: + /// * [`SUtility::Draw`] and [`SUtility::Draw`] + /// * [`SUtility::Lose`] and [`SUtility::Win`] + /// * [`SUtility::Win`] and [`SUtility::Lose`] + /// * [`SUtility::Tie`] and [`SUtility::Tie`] + /// + /// # Example + /// + /// This game category is fairly intuitive in that most two-player board + /// games fall into it. For example, in a game of Chess a [`SUtility::Win`] + /// is recognized to be the taking of the opponent's king, where this also + /// implies that the player who lost it is assigned [`SUtility::Lose`], and + /// where any other ending is classified as a [`SUtility::Tie`]. + /// + /// In other games where it is possible to play forever, both players can + /// achieve a [`SUtility::Draw`] by constantly avoiding reaching a terminal + /// state (perhaps motivated by avoiding realizing imminent losses). + /// + /// # Warning + /// + /// While the games that implement this interface should be zero-sum, the + /// type system is not sufficiently rich to enforce such a constraint at + /// compilation time, so sum specifications are generally left to semantics. fn utility(&self, state: State) -> SUtility; } -/// Indicates that a game is a puzzle with simple outcomes. This implies that it -/// is 1-player and the only possible utilities obtainable for the player are: -/// * `Lose` -/// * `Draw` -/// * `Tie` -/// * `Win` -/// -/// A winning state is usually one where there exists a sequence of moves that -/// will lead to the puzzle being fully solved. A losing state is one where any -/// sequence of moves will always take the player to either another losing state -/// or a state with no further moves available (with the puzzle still unsolved). -/// A draw state is one where there is no way to reach a winning state but it is -/// possible to play forever without reaching a losing state. A tie state is any -/// state that does not subjectively fit into any of the above categories. +/// Provides a method to find the utility of terminal values in "classic" +/// puzzles, which are single-player games with categorical [`SUtility`] values. pub trait ClassicPuzzle { - /// If `state` is terminal, returns the utility of the puzzle's player. If - /// the state is not terminal, it is recommended that this function panics. + /// Returns the utility of the only player in the puzzle at `state`. + /// + /// This assumes that `state` is terminal. The utility structure implies + /// that the game is 1-player, and that the only utility values attainable + /// for the player are: + /// * [`SUtility::Lose`] + /// * [`SUtility::Draw`] + /// * [`SUtility::Tie`] + /// * [`SUtility::Win`] + /// + /// # Example + /// + /// As a theoretical example, consider a Rubik's Cube. Here, we say that + /// the solved state of the cube is a [`SUtility::Win`] for the player, as + /// they have completed their objective. + /// + /// Now consider a crossword puzzle where you cannot erase words. It would + /// be possible for the player to achieve a [`SUtility::Lose`] by filling + /// out incorrect words and having no possible words left to write. + /// + /// For a [`SUtility::Draw`], we can consider a puzzle where it is possible + /// for a loss to be the only material option, but for there to also be the + /// option to continue playing forever (as to not realize this outcome). + /// + /// Finally, a [`SUtility::Tie`] can be interpreted as reaching an outcome + /// of the puzzle where it is impossible to back out of, but that presents + /// no positive or negative impact on the player. fn utility(&self, state: State) -> SUtility; } @@ -213,7 +309,7 @@ where impl SimpleUtility<2, B> for G where - G: Extensive<2, B> + ClassicGame, + G: Sequential<2, B> + ClassicGame, { fn utility(&self, state: State) -> [SUtility; 2] { let mut sutility = [SUtility::Tie; 2]; From 298ba5695710ee8c62452395062e0a24dd82cee6 Mon Sep 17 00:00:00 2001 From: Max Fierro Date: Wed, 8 May 2024 22:47:34 -0700 Subject: [PATCH 14/15] Added documentation for solving module --- src/solver/mod.rs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/solver/mod.rs b/src/solver/mod.rs index 7f7b36d..0ec6734 100644 --- a/src/solver/mod.rs +++ b/src/solver/mod.rs @@ -109,17 +109,31 @@ pub trait Sequential { } } -/// Indicates that a game can be partitioned into subsets that are convenient -/// for computation purposes. +/// Indicates that a game can be partitioned into sub-games that can be solved +/// somewhat independently of each other. pub trait Composite { /// Returns an identifier for a subset of the space of states which `state` /// belongs to. /// - /// TODO + /// This method is generally useful for computing different sub-games in + /// parallel, which is why it is desireable that partitions are independent + /// of each other (meaning that we do not need to know information about one + /// to generate information about the other). + /// + /// Additional properties are desirable. For example, we can try to create + /// partitions of low "conductance," or in other words, which have a lot of + /// states within partitions, but not many state transitions between them. + /// Knowing how to do this effectively is hard, and it is an active area of + /// research when talking about general graphs. /// /// # Example /// - /// TODO + /// Consider a standard game of Tic-Tac-Toe. Here, the rules prevent players + /// from removing any pieces placed on the board. This tells us that there + /// are no transitions between states with the same number of pieces on the + /// board. Hence, one way to implement this function would be to simply + /// return the number of pieces left on the board. (This is an illustrative + /// example; it would not provide very much of a computational benefit.) fn partition(&self, state: State) -> Partition; /// Returns an estimate of the number of states in `partition`. From 3e8478f4e2ecebbea05e617d5f2853892beaf5ca Mon Sep 17 00:00:00 2001 From: Max Fierro Date: Thu, 9 May 2024 04:30:04 -0700 Subject: [PATCH 15/15] Added some documentation --- src/solver/mod.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/solver/mod.rs b/src/solver/mod.rs index 0ec6734..7c9eedb 100644 --- a/src/solver/mod.rs +++ b/src/solver/mod.rs @@ -85,14 +85,11 @@ pub enum RecordType { /// Indicates that a discrete game is played sequentially, which allows for /// representing histories as discrete [`State`]s. pub trait Sequential { - /// Returns the player `i` whose turn it is at the given `state`, with - /// Player 0 always going first. + /// Returns the player `i` whose turn it is at the given `state`. /// - /// TODO - /// - /// # Example - /// - /// TODO + /// In general, it can be assumed that the player whose turn it is at there + /// starting state is Player 0, with the sole exception of games whose state + /// has been forwarded. /// /// # Warning ///