Skip to content

Commit

Permalink
Add matchup type and cartesian product tournament
Browse files Browse the repository at this point in the history
  • Loading branch information
walkie committed May 27, 2024
1 parent d6a9be2 commit 7deaad3
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 26 deletions.
6 changes: 3 additions & 3 deletions t4t/src/game.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::fmt::Debug;

use crate::{Action, Context, Error, Move, Outcome, PlayerIndex, Players, Turn, Utility};
use crate::{Action, Context, Error, Matchup, Move, Outcome, PlayerIndex, Turn, Utility};

/// A trait that collects the trait requirements of a game state.
///
Expand Down Expand Up @@ -61,9 +61,9 @@ pub trait Game<const P: usize>: Clone + Sized + Send + Sync {

/// Play this game with the given players, producing a value of the game's outcome type on
/// success.
fn play(&self, players: &Players<Self, P>) -> PlayResult<Self, P> {
fn play(&self, matchup: &Matchup<Self, P>) -> PlayResult<Self, P> {
let mut turn = self.first_turn();
let mut strategies = players.map(|player| player.new_strategy());
let mut strategies = matchup.strategies();

loop {
match turn.action {
Expand Down
16 changes: 9 additions & 7 deletions t4t/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
//! }
//!
//! // Play the game!
//! let result = pd.play(&PerPlayer::new([nice(), mean()]));
//! let result = pd.play(&Matchup::from_players([nice(), mean()]));
//! assert_eq!(result.unwrap().payoff(), &Payoff::from([0, 3]));
//!
//! // Define the repeated prisoner's dilemma.
Expand All @@ -97,22 +97,22 @@
//!
//! // Play every combination of players against each other.
//! // TODO: Direct support for this with cumulative scores coming soon!
//! let result = rpd.play(&PerPlayer::new([nice(), nice()]));
//! let result = rpd.play(&Matchup::from_players([nice(), nice()]));
//! assert_eq!(result.unwrap().payoff(), &Payoff::from([200, 200]));
//!
//! let result = rpd.play(&PerPlayer::new([nice(), mean()]));
//! let result = rpd.play(&Matchup::from_players([nice(), mean()]));
//! assert_eq!(result.unwrap().payoff(), &Payoff::from([0, 300]));
//!
//! let result = rpd.play(&PerPlayer::new([nice(), tit_for_tat.clone()]));
//! let result = rpd.play(&Matchup::from_players([nice(), tit_for_tat.clone()]));
//! assert_eq!(result.unwrap().payoff(), &Payoff::from([200, 200]));
//!
//! let result = rpd.play(&PerPlayer::new([mean(), mean()]));
//! let result = rpd.play(&Matchup::from_players([mean(), mean()]));
//! assert_eq!(result.unwrap().payoff(), &Payoff::from([100, 100]));
//!
//! let result = rpd.play(&PerPlayer::new([mean(), tit_for_tat.clone()]));
//! let result = rpd.play(&Matchup::from_players([mean(), tit_for_tat.clone()]));
//! assert_eq!(result.unwrap().payoff(), &Payoff::from([102, 99]));
//!
//! let result = rpd.play(&PerPlayer::new([tit_for_tat.clone(), tit_for_tat]));
//! let result = rpd.play(&Matchup::from_players([tit_for_tat.clone(), tit_for_tat]));
//! assert_eq!(result.unwrap().payoff(), &Payoff::from([200, 200]));
//! ```
//!
Expand Down Expand Up @@ -147,6 +147,7 @@ pub(crate) mod dominated;
pub(crate) mod error;
pub(crate) mod game;
pub(crate) mod history;
pub(crate) mod matchup;
pub(crate) mod moves;
pub(crate) mod normal;
pub(crate) mod outcome;
Expand All @@ -172,6 +173,7 @@ pub use dominated::*;
pub use error::*;
pub use game::*;
pub use history::*;
pub use matchup::*;
pub use moves::*;
pub use normal::*;
pub use outcome::*;
Expand Down
35 changes: 35 additions & 0 deletions t4t/src/matchup.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use crate::{Game, PerPlayer, Player, Strategy};
use std::sync::Arc;

/// A collection of players ready to play a game.
#[derive(Clone)]
pub struct Matchup<G: Game<P>, const P: usize> {
players: PerPlayer<Arc<Player<G, P>>, P>,
}

impl<G: Game<P>, const P: usize> Matchup<G, P> {
/// Construct a new matchup.
pub fn new(players: PerPlayer<Arc<Player<G, P>>, P>) -> Self {
Matchup { players }
}

/// Construct a new matchup from an array of players.
pub fn from_players(players: [Player<G, P>; P]) -> Self {
Matchup::new(PerPlayer::new(players.map(Arc::new)))
}

/// Get the players in this matchup.
pub fn players(&self) -> &PerPlayer<Arc<Player<G, P>>, P> {
&self.players
}

/// Get the names of all players in this matchup.
pub fn names(&self) -> PerPlayer<String, P> {
self.players.map(|player| player.name().to_owned())
}

/// Get fresh copies of each player's strategy for playing the game.
pub fn strategies(&self) -> PerPlayer<Strategy<G::View, G::Move, P>, P> {
self.players.map(|player| player.new_strategy())
}
}
8 changes: 4 additions & 4 deletions t4t/src/normal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,19 @@ use crate::{
/// let mean = Player::new("Mean".to_string(), || Strategy::pure('D'));
///
/// assert_eq!(
/// pd.play(&PerPlayer::new([nice.clone(), nice.clone()])),
/// pd.play(&Matchup::from_players([nice.clone(), nice.clone()])),
/// Ok(SimultaneousOutcome::new(Profile::new(['C', 'C']), Payoff::from([2, 2]))),
/// );
/// assert_eq!(
/// pd.play(&PerPlayer::new([nice.clone(), mean.clone()])),
/// pd.play(&Matchup::from_players([nice.clone(), mean.clone()])),
/// Ok(SimultaneousOutcome::new(Profile::new(['C', 'D']), Payoff::from([0, 3]))),
/// );
/// assert_eq!(
/// pd.play(&PerPlayer::new([mean.clone(), nice])),
/// pd.play(&Matchup::from_players([mean.clone(), nice])),
/// Ok(SimultaneousOutcome::new(Profile::new(['D', 'C']), Payoff::from([3, 0]))),
/// );
/// assert_eq!(
/// pd.play(&PerPlayer::new([mean.clone(), mean])),
/// pd.play(&Matchup::from_players([mean.clone(), mean])),
/// Ok(SimultaneousOutcome::new(Profile::new(['D', 'D']), Payoff::from([1, 1]))),
/// );
/// ```
Expand Down
12 changes: 8 additions & 4 deletions t4t/src/player.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
use crate::{Game, PerPlayer, Strategy};
use crate::{Game, Strategy};
use std::fmt;
use std::sync::Arc;

/// A [per-player](PerPlayer) collection of [players](Player), ready to play a game.
pub type Players<G, const P: usize> = PerPlayer<Player<G, P>, P>;

/// A player consists of a name and a function that produces its [strategy](Strategy).
///
/// A player's name must be unique with respect to all other players playing the same game (e.g.
Expand Down Expand Up @@ -37,6 +35,12 @@ impl<G: Game<P>, const P: usize> Player<G, P> {
}
}

impl<G: Game<P>, const P: usize> std::fmt::Debug for Player<G, P> {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(fmt, "Player({})", self.name)
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
37 changes: 29 additions & 8 deletions t4t/src/tournament.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::{Game, Outcome, PerPlayer, PlayResult, Players};
use crate::{Game, Matchup, Outcome, PerPlayer, PlayResult, Player};
use itertools::Itertools;
use log::error;
use rayon::prelude::*;
use std::collections::HashMap;
Expand All @@ -7,7 +8,7 @@ use std::sync::Arc;
/// A tournament in which several players play a game against each other in a series of matchups.
pub struct Tournament<G: Game<P>, const P: usize> {
game: Arc<G>,
matchups: Vec<Players<G, P>>,
matchups: Vec<Matchup<G, P>>,
}

/// The collected results from running a tournament.
Expand All @@ -18,11 +19,32 @@ pub struct TournamentResult<G: Game<P>, const P: usize> {
}

impl<G: Game<P>, const P: usize> Tournament<G, P> {
/// Construct a new tournament for the given game with the given matchups.
pub fn new(game: Arc<G>, matchups: Vec<Players<G, P>>) -> Self {
/// Construct a new tournament for the given game with the given explicit list of matchups.
pub fn new(game: Arc<G>, matchups: Vec<Matchup<G, P>>) -> Self {
Tournament { game, matchups }
}

/// Construct a new tournament for the given game where the matchups are the cartesian product
/// of the given per-player collection of lists of players, that is, every combination where one
/// player is drawn from each list.
///
/// This constructor is particularly useful for non-symmetric games where different groups of
/// players are designed for different roles in the game.
pub fn product(game: Arc<G>, players_per_slot: PerPlayer<Vec<Arc<Player<G, P>>>, P>) -> Self {
Tournament::new(
game,
players_per_slot
.into_iter()
.multi_cartesian_product()
.map(|player_vec| Matchup::new(PerPlayer::new(player_vec.try_into().unwrap())))
.collect(),
)
}

// pub fn symmetric(game: Arc<G>, players: Vec<Player<G, P>>) -> Self {
// itertools
// }

/// Run the tournament and collect the results.
pub fn play(&self) -> TournamentResult<G, P> {
let mut results = HashMap::new();
Expand All @@ -33,10 +55,9 @@ impl<G: Game<P>, const P: usize> Tournament<G, P> {

self.matchups
.par_iter()
.for_each_with(sender, |s, players| {
let names = players.map(|player| player.name().to_owned());
let result = self.game.play(players);
let send_result = s.send((names, result));
.for_each_with(sender, |s, matchup| {
let result = self.game.play(matchup);
let send_result = s.send((matchup.names(), result));
if let Err(err) = send_result {
error!("error sending result: {:?}", err);
}
Expand Down

0 comments on commit 7deaad3

Please sign in to comment.