From 594c6ead9a0074f7c46ff6493213d6191354e208 Mon Sep 17 00:00:00 2001 From: Eric Walkingshaw Date: Sun, 23 Jun 2024 01:13:43 -0700 Subject: [PATCH] Make game trees games directly --- t4t-games/src/dilemma.rs | 15 ++++----- t4t/src/game.rs | 22 +++++++------- t4t/src/lib.rs | 6 ++-- t4t/src/normal.rs | 6 +--- t4t/src/repeated.rs | 40 ++++++++++++------------ t4t/src/simultaneous.rs | 6 +--- t4t/src/tree.rs | 66 +++++++++++++++++++++++++++------------- 7 files changed, 90 insertions(+), 71 deletions(-) diff --git a/t4t-games/src/dilemma.rs b/t4t-games/src/dilemma.rs index 234e2fd..eab248b 100644 --- a/t4t-games/src/dilemma.rs +++ b/t4t-games/src/dilemma.rs @@ -78,7 +78,12 @@ impl Dilemma { Dilemma { game, utils } } - /// Get the normal form representation of this game. + /// Convert this game into its normal-form representation. + pub fn into_normal(self) -> Normal { + self.game + } + + /// Get the normal-form representation of this game. pub fn as_normal(&self) -> &Normal { &self.game } @@ -386,15 +391,11 @@ impl Game<2> for Dilemma { type State = (); type View = (); - fn game_tree(&self) -> GameTree<(), Move, SimultaneousOutcome, 2> { - self.as_normal().game_tree() + fn into_game_tree(self) -> GameTree<(), Move, i64, SimultaneousOutcome, 2> { + self.into_normal().into_game_tree() } fn state_view(&self, _state: &(), _player: PlayerIndex<2>) {} - - fn is_valid_move(&self, _state: &(), _player: PlayerIndex<2>, _the_move: Move) -> bool { - true - } } // Strategies diff --git a/t4t/src/game.rs b/t4t/src/game.rs index d6f1e5c..90b5201 100644 --- a/t4t/src/game.rs +++ b/t4t/src/game.rs @@ -36,23 +36,23 @@ pub trait Game: Clone + Sized + Send + Sync { /// state that are not visible to players while making strategic decisions. type View: State; - /// The root of the game tree for this game. + /// Convert this game into the corresponding game tree. /// /// The game tree is effectively a step-by-step executable description of how this game is /// played. - fn game_tree(&self) -> GameTree; + fn into_game_tree(self) -> GameTree; + + /// Get the corresponding game tree for this game. + /// + /// The game tree is effectively a step-by-step executable description of how this game is + /// played. + fn game_tree(&self) -> GameTree { + self.clone().into_game_tree() + } /// Produce a view of the game state for the given player. fn state_view(&self, state: &Self::State, player: PlayerIndex

) -> Self::View; - /// Is this a valid move in the given context? - fn is_valid_move( - &self, - state: &Self::State, - player: PlayerIndex

, - the_move: Self::Move, - ) -> bool; - /// The number of players this game is for. fn num_players(&self) -> usize { P @@ -96,7 +96,7 @@ pub trait Game: Clone + Sized + Send + Sync { } } - Action::End { outcome } => return Ok(outcome), + Action::End { outcome, .. } => return Ok(outcome), } } } diff --git a/t4t/src/lib.rs b/t4t/src/lib.rs index 457214f..ef27937 100644 --- a/t4t/src/lib.rs +++ b/t4t/src/lib.rs @@ -12,13 +12,15 @@ //! //! The library provides a variety of types for defining common kinds of games: //! +//! - [`GameTree`]: A very generic game-tree representation. This is not very convenient to use +//! directly, but all games are eventually translated into this type in order to be executed. //! - [`Normal`] -- A general representation of n-ary [normal-form games][normal-form-game]. //! An arbitrary number of players simultaneously play a single move selected from a finite set //! of available moves. //! - [`Simultaneous`] -- N-ary [simultaneous games][simultaneous-game]. //! Similar to [`Normal`], except the moves available to each player may be non-finite. -//! - `Extensive` (coming soon): A simple representation of [extensive-form games][extensive-form-game]. -//! Games represented as complete game trees, where players take turns making moves, +//! - `Extensive` (coming soon): A simple representation of [extensive-form games][extensive-form-game], +//! that is, games represented as complete game trees, where players take turns making moves, //! possibly with moves of chance interspersed. //! - `StateBased` (coming soon): Games that revolve around manipulating a shared state. //! - [`Repeated`]: Games where another game is played repeatedly a given number of times. diff --git a/t4t/src/normal.rs b/t4t/src/normal.rs index 7ddd3d6..c05ac49 100644 --- a/t4t/src/normal.rs +++ b/t4t/src/normal.rs @@ -944,7 +944,7 @@ impl Game

for Normal { type State = (); type View = (); - fn game_tree(&self) -> GameTree<(), M, SimultaneousOutcome, P> { + fn into_game_tree(self) -> GameTree<(), M, U, SimultaneousOutcome, P> { let state = Arc::new(()); GameTree::all_players(state.clone(), move |_, profile| { for ply in profile.plies() { @@ -961,10 +961,6 @@ impl Game

for Normal { } fn state_view(&self, _state: &(), _player: PlayerIndex

) {} - - fn is_valid_move(&self, _state: &(), player: PlayerIndex

, the_move: M) -> bool { - self.is_valid_move_for_player(player, the_move) - } } impl FiniteGame

for Normal { diff --git a/t4t/src/repeated.rs b/t4t/src/repeated.rs index a534f2f..0f16115 100644 --- a/t4t/src/repeated.rs +++ b/t4t/src/repeated.rs @@ -72,11 +72,11 @@ impl, const P: usize> RepeatedState { } } -fn lift_node<'g, G: Game

+ 'g, const P: usize>( - stage_game: &'g G, +fn lift_node + 'static, const P: usize>( + stage_game: Arc, state: Arc>, - node: GameTree<'g, G::State, G::Move, G::Outcome, P>, -) -> GameTree<'g, RepeatedState, G::Move, History, P> { + node: GameTree, +) -> GameTree, G::Move, G::Utility, History, P> { match node.action { Action::Turns { to_move: players, @@ -92,7 +92,11 @@ fn lift_node<'g, G: Game

+ 'g, const P: usize>( let mut next_state = (*state).clone(); next_state.stage_state = stage_node.state.clone(); - Ok(lift_node(stage_game, Arc::new(next_state), stage_node)) + Ok(lift_node( + stage_game.clone(), + Arc::new(next_state), + stage_node, + )) } Err(kind) => Err(kind), @@ -110,14 +114,18 @@ fn lift_node<'g, G: Game

+ 'g, const P: usize>( let mut next_state = (*state).clone(); next_state.stage_state = stage_node.state.clone(); - Ok(lift_node(stage_game, Arc::new(next_state), stage_node)) + Ok(lift_node( + stage_game.clone(), + Arc::new(next_state), + stage_node, + )) } Err(kind) => Err(kind), }, ), - Action::End { outcome } if state.remaining > 0 => { + Action::End { outcome, .. } if state.remaining > 0 => { let stage_node = stage_game.game_tree(); let mut next_state = (*state).clone(); @@ -129,7 +137,7 @@ fn lift_node<'g, G: Game

+ 'g, const P: usize>( lift_node(stage_game, Arc::new(next_state), stage_node) } - Action::End { outcome } => { + Action::End { outcome, .. } => { let mut history = state.completed.clone(); // TODO avoid this clone history.add(outcome); @@ -145,14 +153,16 @@ impl + 'static, const P: usize> Game

for Repeated { type State = RepeatedState; type View = RepeatedState; // TODO add RepeatedStateView or some other solution - fn game_tree(&self) -> GameTree, G::Move, History, P> { + fn into_game_tree( + self, + ) -> GameTree, G::Move, G::Utility, History, P> { let init_state = Arc::new(RepeatedState::new( self.stage_game.clone(), self.repetitions - 1, )); lift_node( - self.stage_game.as_ref(), + self.stage_game.clone(), init_state, self.stage_game.game_tree(), ) @@ -165,16 +175,6 @@ impl + 'static, const P: usize> Game

for Repeated { ) -> RepeatedState { state.clone() // TODO } - - fn is_valid_move( - &self, - state: &Self::State, - player: PlayerIndex

, - the_move: Self::Move, - ) -> bool { - self.stage_game - .is_valid_move(&state.stage_state, player, the_move) - } } impl + 'static, const P: usize> FiniteGame

for Repeated { diff --git a/t4t/src/simultaneous.rs b/t4t/src/simultaneous.rs index c1daae2..e3aac3c 100644 --- a/t4t/src/simultaneous.rs +++ b/t4t/src/simultaneous.rs @@ -125,7 +125,7 @@ impl Game

for Simultaneous { type State = (); type View = (); - fn game_tree(&self) -> GameTree<(), M, SimultaneousOutcome, P> { + fn into_game_tree(self) -> GameTree<(), M, U, SimultaneousOutcome, P> { let state = Arc::new(()); GameTree::all_players(state.clone(), move |_, profile| { for ply in profile.plies() { @@ -142,10 +142,6 @@ impl Game

for Simultaneous { } fn state_view(&self, _state: &(), _player: PlayerIndex

) {} - - fn is_valid_move(&self, _state: &(), player: PlayerIndex

, the_move: M) -> bool { - self.is_valid_move_for_player(player, the_move) - } } #[cfg(test)] diff --git a/t4t/src/tree.rs b/t4t/src/tree.rs index 54d237f..733de73 100644 --- a/t4t/src/tree.rs +++ b/t4t/src/tree.rs @@ -1,3 +1,4 @@ +use std::marker::PhantomData; use std::sync::Arc; use crate::{Distribution, ErrorKind, Game, Move, Outcome, PlayerIndex, Profile, State, Utility}; @@ -9,13 +10,13 @@ use crate::{Distribution, ErrorKind, Game, Move, Outcome, PlayerIndex, Profile, /// /// This trait is effectively a type synonym for the function type it extends. A blanket /// implementation covers all possible instances, so it should not be implemented directly. -pub trait NextGameTree<'g, T, S, M, O, const P: usize>: - Fn(Arc, T) -> Result, ErrorKind> + 'g +pub trait NextGameTree: + Fn(Arc, T) -> Result, ErrorKind> + Send + Sync + 'static { } -impl<'g, F, T, S, M, O, const P: usize> NextGameTree<'g, T, S, M, O, P> for F where - F: Fn(Arc, T) -> Result, ErrorKind> + 'g +impl NextGameTree for F where + F: Fn(Arc, T) -> Result, ErrorKind> + Send + Sync + 'static { } @@ -23,22 +24,22 @@ impl<'g, F, T, S, M, O, const P: usize> NextGameTree<'g, T, S, M, O, P> for F wh /// /// Subsequent nodes, if applicable, are reachable via the action's `next` function. #[derive(Clone)] -pub struct GameTree<'g, S, M, O, const P: usize> { +pub struct GameTree { /// The game state at this node. pub state: Arc, /// The action to take at this node. - pub action: Action<'g, S, M, O, P>, + pub action: Action, } /// The game action to perform at a given node in the game tree. #[derive(Clone)] -pub enum Action<'g, S, M, O, const P: usize> { +pub enum Action { /// One or more players play a move simultaneously. Turns { /// The players to move simultaneously. to_move: Vec>, /// Compute the next node from the moves played by the players. - next: Arc, S, M, O, P>>, + next: Arc, S, M, U, O, P>>, }, /// Make a move of chance according to the given distribution. @@ -46,20 +47,22 @@ pub enum Action<'g, S, M, O, const P: usize> { /// The distribution to draw a move from. distribution: Distribution, /// Compute the next node from the move drawn from the distribution. - next: Arc>, + next: Arc>, }, /// End a game and return the outcome, which includes the game's payoff. End { /// The final outcome of the game. outcome: O, + /// Phantom data to specify the utility value type. + utility_type: PhantomData, }, } -impl<'g, S, M: Move, O, const P: usize> Action<'g, S, M, O, P> { +impl Action { /// Construct an action where a single player must make a move and the next node is computed /// from the move they choose. - pub fn player(to_move: PlayerIndex

, next: impl NextGameTree<'g, M, S, M, O, P>) -> Self { + pub fn player(to_move: PlayerIndex

, next: impl NextGameTree) -> Self { Action::players(vec![to_move], move |state, moves| { assert_eq!(moves.len(), 1); next(state, moves[0]) @@ -70,7 +73,7 @@ impl<'g, S, M: Move, O, const P: usize> Action<'g, S, M, O, P> { /// is computed from the moves they choose. pub fn players( to_move: Vec>, - next: impl NextGameTree<'g, Vec, S, M, O, P>, + next: impl NextGameTree, S, M, U, O, P>, ) -> Self { Action::Turns { to_move, @@ -80,7 +83,7 @@ impl<'g, S, M: Move, O, const P: usize> Action<'g, S, M, O, P> { /// Construct an action where all players must make a move simultaneously and the next node is /// computed from the moves they choose. - pub fn all_players(next: impl NextGameTree<'g, Profile, S, M, O, P>) -> Self { + pub fn all_players(next: impl NextGameTree, S, M, U, O, P>) -> Self { Action::players(PlayerIndex::all().collect(), move |state, moves| { assert_eq!(moves.len(), P); next(state, Profile::new(moves.try_into().unwrap())) @@ -91,7 +94,7 @@ impl<'g, S, M: Move, O, const P: usize> Action<'g, S, M, O, P> { /// computed from the selected move. pub fn chance( distribution: Distribution, - next: impl NextGameTree<'g, M, S, M, O, P>, + next: impl NextGameTree, ) -> Self { Action::Chance { distribution, @@ -101,13 +104,16 @@ impl<'g, S, M: Move, O, const P: usize> Action<'g, S, M, O, P> { /// Construct an action ending the game with the given outcome. pub fn end(outcome: O) -> Self { - Action::End { outcome } + Action::End { + outcome, + utility_type: PhantomData, + } } } -impl<'g, S, M: Move, O, const P: usize> GameTree<'g, S, M, O, P> { +impl GameTree { /// Construct a new game node with the given state and action. - pub fn new(state: Arc, action: Action<'g, S, M, O, P>) -> Self { + pub fn new(state: Arc, action: Action) -> Self { GameTree { state, action } } @@ -116,7 +122,7 @@ impl<'g, S, M: Move, O, const P: usize> GameTree<'g, S, M, O, P> { pub fn player( state: Arc, player: PlayerIndex

, - next: impl NextGameTree<'g, M, S, M, O, P>, + next: impl NextGameTree, ) -> Self { GameTree::new(state, Action::player(player, next)) } @@ -126,7 +132,7 @@ impl<'g, S, M: Move, O, const P: usize> GameTree<'g, S, M, O, P> { pub fn players( state: Arc, players: Vec>, - next: impl NextGameTree<'g, Vec, S, M, O, P>, + next: impl NextGameTree, S, M, U, O, P>, ) -> Self { GameTree::new(state, Action::players(players, next)) } @@ -135,7 +141,7 @@ impl<'g, S, M: Move, O, const P: usize> GameTree<'g, S, M, O, P> { /// is computed from the moves they choose. pub fn all_players( state: Arc, - next: impl NextGameTree<'g, Profile, S, M, O, P>, + next: impl NextGameTree, S, M, U, O, P>, ) -> Self { GameTree::new(state, Action::all_players(next)) } @@ -145,7 +151,7 @@ impl<'g, S, M: Move, O, const P: usize> GameTree<'g, S, M, O, P> { pub fn chance( state: Arc, distribution: Distribution, - next: impl NextGameTree<'g, M, S, M, O, P>, + next: impl NextGameTree, ) -> Self { GameTree::new(state, Action::chance(distribution, next)) } @@ -155,3 +161,21 @@ impl<'g, S, M: Move, O, const P: usize> GameTree<'g, S, M, O, P> { GameTree::new(state, Action::end(outcome)) } } + +impl, const P: usize> Game

+ for GameTree +{ + type Move = M; + type Utility = U; + type Outcome = O; + type State = S; + type View = S; + + fn into_game_tree(self) -> GameTree { + self + } + + fn state_view(&self, state: &Self::State, _player: PlayerIndex

) -> Self::View { + state.clone() + } +}