#![warn(missing_docs, clippy::pedantic)]
mod compact;
mod error;
mod history;
mod lazy;
mod regret;
mod solve;
mod split;
mod splitmix;
use compact::{Builder, OptBuilder};
pub use error::{GameError, SolveError, StratError};
pub use history::{Digest, History};
pub use lazy::LazySolver;
pub use solve::RegretParams;
use solve::{external, vanilla};
use split::{split_by, split_by_mut};
use std::borrow::Borrow;
use std::collections::hash_map;
use std::collections::{HashMap, HashSet};
use std::convert::Infallible;
use std::hash::Hash;
use std::iter::{self, FusedIterator, Once, Zip};
use std::num::NonZeroUsize;
use std::ptr;
use std::slice;
use std::thread;
#[derive(Debug, Copy, Eq, Clone, PartialEq, Hash)]
pub enum PlayerNum {
One,
Two,
}
impl PlayerNum {
fn index(self) -> usize {
match self {
PlayerNum::One => 0,
PlayerNum::Two => 1,
}
}
fn ind<T>(self, arr: &[T; 2]) -> &T {
match (self, arr) {
(PlayerNum::One, [first, _]) => first,
(PlayerNum::Two, [_, second]) => second,
}
}
fn ind_mut<T>(self, arr: &mut [T; 2]) -> &mut T {
match (self, arr) {
(PlayerNum::One, [first, _]) => first,
(PlayerNum::Two, [_, second]) => second,
}
}
fn replace<T>(self, mut arr: [T; 2], value: T) -> [T; 2] {
*self.ind_mut(&mut arr) = value;
arr
}
}
pub enum NodeType<G: Game> {
Terminal(f64),
Chance(Option<G::ChanceInfoset>, G::Chance),
Player(PlayerNum, G::Infoset, G::Player),
}
pub trait Game: Sized {
type Action;
type Infoset;
type ChanceInfoset;
type Chance: Outcomes<Self>;
type Player: Moves<Self>;
fn into_node(self) -> NodeType<Self>;
}
#[allow(clippy::len_without_is_empty)] pub trait Outcomes<G: Game> {
fn len(&self) -> usize;
fn get(&self, index: usize) -> (f64, G);
fn iter(&self) -> impl Iterator<Item = (f64, G)> + '_ {
(0..self.len()).map(|index| self.get(index))
}
}
#[allow(clippy::len_without_is_empty)] pub trait Moves<G: Game> {
fn len(&self) -> usize;
fn apply(&self, index: usize) -> G;
fn action(&self, index: usize) -> G::Action;
fn iter(&self) -> impl Iterator<Item = (G::Action, G)> + '_ {
(0..self.len()).map(|index| (self.action(index), self.apply(index)))
}
}
impl<G: Game> Outcomes<G> for Infallible {
fn len(&self) -> usize {
match *self {}
}
fn get(&self, _index: usize) -> (f64, G) {
match *self {}
}
}
#[derive(Debug)]
enum Node {
Terminal(f64),
Chance(Chance),
Player(Player),
}
#[derive(Debug)]
struct Chance {
outcomes: Box<[Node]>,
infoset: usize,
}
impl Chance {
fn new(data: impl Into<Box<[Node]>>, infoset: usize) -> Chance {
let outcomes = data.into();
Chance { outcomes, infoset }
}
}
#[derive(Debug)]
struct Player {
num: PlayerNum,
infoset: usize,
actions: Box<[Node]>,
}
#[derive(Debug)]
struct ChanceInfosetData {
probs: Box<[f64]>,
}
impl ChanceInfosetData {
fn new(data: impl Into<Box<[f64]>>) -> ChanceInfosetData {
ChanceInfosetData { probs: data.into() }
}
}
#[derive(Debug)]
struct PlayerInfosetBuilder<A> {
actions: Box<[A]>,
prev_infoset: Option<(usize, usize)>,
}
impl<A> PlayerInfosetBuilder<A> {
fn new(actions: impl Into<Box<[A]>>, prev_infoset: Option<(usize, usize)>) -> Self {
PlayerInfosetBuilder {
actions: actions.into(),
prev_infoset,
}
}
}
#[derive(Debug)]
struct PlayerInfosetData<I, A> {
infoset: I,
actions: Box<[A]>,
prev_infoset: Option<(usize, usize)>,
}
impl<I, A> PlayerInfosetData<I, A> {
fn new(infoset: I, builder: PlayerInfosetBuilder<A>) -> Self {
PlayerInfosetData {
infoset,
actions: builder.actions,
prev_infoset: builder.prev_infoset,
}
}
fn num_actions(&self) -> usize {
self.actions.len()
}
}
trait PlayerInfoset {
fn num_actions(&self) -> usize;
fn prev_infoset(&self) -> Option<usize>;
}
impl<I, A> PlayerInfoset for PlayerInfosetData<I, A> {
fn num_actions(&self) -> usize {
self.num_actions()
}
fn prev_infoset(&self) -> Option<usize> {
self.prev_infoset.map(|(infoset, _action)| infoset)
}
}
trait ChanceInfoset {
fn probs(&self) -> &[f64];
}
impl ChanceInfoset for ChanceInfosetData {
fn probs(&self) -> &[f64] {
&self.probs
}
}
#[derive(Debug)]
pub struct GameTree<Infoset, Action> {
chance_infosets: Box<[ChanceInfosetData]>,
player_infosets: [Box<[PlayerInfosetData<Infoset, Action>]>; 2],
single_infosets: [Box<[(Infoset, Action)]>; 2],
root: Node,
}
impl<I, A> PartialEq for GameTree<I, A> {
fn eq(&self, other: &Self) -> bool {
ptr::eq(self, other)
}
}
impl<I, A> Eq for GameTree<I, A> {}
impl<I: Hash + Eq, A: Hash + Eq> GameTree<I, A> {
pub fn from_game<G>(root: G) -> Result<Self, GameError>
where
G: Game<Infoset = I, Action = A>,
G::ChanceInfoset: Hash + Eq,
{
let mut chance_infosets = OptBuilder::new();
let mut player_infosets = [Builder::new(), Builder::new()];
let mut single_infosets = [HashMap::new(), HashMap::new()];
let [first_player, second_player] = &mut player_infosets;
let [first_single, second_single] = &mut single_infosets;
let root = GameTree::recurse(
&mut chance_infosets,
&mut [first_player, second_player],
&mut [first_single, second_single],
root,
[None; 2],
)?;
let chance_infosets: Box<[_]> = chance_infosets.into_iter().map(|(_, v)| v).collect();
let player_infosets = player_infosets.map(|pinfo| {
pinfo
.into_iter()
.map(|(infoset, builder)| PlayerInfosetData::new(infoset, builder))
.collect::<Box<[_]>>()
});
for (singles, multis) in single_infosets.iter().zip(player_infosets.iter()) {
let multi_keys: HashSet<&I> = multis.iter().map(|data| &data.infoset).collect();
if singles.keys().any(|key| multi_keys.contains(key)) {
return Err(GameError::ActionsNotEqual);
}
}
let fits_u32 = |len: usize| u32::try_from(len).is_ok();
if !fits_u32(chance_infosets.len())
|| !player_infosets.iter().all(|pinfo| fits_u32(pinfo.len()))
{
return Err(GameError::TooManyInfosets);
}
Ok(GameTree {
chance_infosets,
player_infosets,
single_infosets: single_infosets.map(|sinfo| sinfo.into_iter().collect()),
root,
})
}
#[allow(clippy::too_many_lines)]
fn recurse<G>(
chance_infosets: &mut OptBuilder<G::ChanceInfoset, ChanceInfosetData>,
player_infosets: &mut [&mut Builder<I, PlayerInfosetBuilder<A>>; 2],
single_infosets: &mut [&mut HashMap<I, A>; 2],
state: G,
prev_infosets: [Option<(usize, usize)>; 2],
) -> Result<Node, GameError>
where
G: Game<Infoset = I, Action = A>,
G::ChanceInfoset: Hash + Eq,
{
match state.into_node() {
NodeType::Terminal(payoff) => Ok(Node::Terminal(payoff)),
NodeType::Chance(info, chance) => {
let mut probs = Vec::new();
let mut outcomes = Vec::new();
for (prob, next) in chance.iter() {
if prob > 0.0 && prob.is_finite() {
probs.push(prob);
outcomes.push(GameTree::recurse(
chance_infosets,
player_infosets,
single_infosets,
next,
prev_infosets,
)?);
} else {
return Err(GameError::NonPositiveChance);
}
}
match outcomes.len() {
0 => Err(GameError::EmptyChance),
1 => Ok(outcomes.pop().unwrap()),
_ => {
let total: f64 = probs.iter().sum();
for prob in &mut probs {
*prob /= total;
}
let ind = match chance_infosets.entry(info) {
compact::Entry::Vacant(ent) => {
ent.insert(ChanceInfosetData::new(probs))
}
compact::Entry::Occupied(ent) => {
let (ind, data) = ent.get();
if *data.probs == *probs {
Ok(ind)
} else {
Err(GameError::ProbabilitiesNotEqual)
}?
}
};
Ok(Node::Chance(Chance::new(outcomes, ind)))
}
}
}
NodeType::Player(player_num, infoset, moves) => {
let mut actions = Vec::new();
let mut nexts = Vec::new();
for (action, next) in moves.iter() {
actions.push(action);
nexts.push(next);
}
match actions.len() {
0 => Err(GameError::EmptyPlayer),
1 => {
let action = actions.pop().unwrap();
match player_num.ind_mut(single_infosets).entry(infoset) {
hash_map::Entry::Occupied(ent) => {
if ent.get() != &action {
return Err(GameError::ActionsNotEqual);
}
}
hash_map::Entry::Vacant(ent) => {
ent.insert(action);
}
}
let next = nexts.pop().unwrap();
GameTree::recurse(
chance_infosets,
player_infosets,
single_infosets,
next,
prev_infosets,
)
}
_ => {
let info_ind = match player_num.ind_mut(player_infosets).entry(infoset) {
compact::Entry::Occupied(ent) => {
let (ind, info) = ent.get();
if *info.actions != *actions {
Err(GameError::ActionsNotEqual)
} else if &info.prev_infoset != player_num.ind(&prev_infosets) {
Err(GameError::ImperfectRecall)
} else {
Ok(ind)
}
}
compact::Entry::Vacant(ent) => {
let hash_names: HashSet<&A> = actions.iter().collect();
if hash_names.len() == actions.len() {
Ok(ent.insert(PlayerInfosetBuilder::new(
actions,
*player_num.ind(&prev_infosets),
)))
} else {
Err(GameError::ActionsNotUnique)
}
}
}?;
let next_verts: Result<Box<[_]>, _> = nexts
.into_iter()
.enumerate()
.map(|(action_ind, next)| {
let next_prev =
player_num.replace(prev_infosets, Some((info_ind, action_ind)));
GameTree::recurse(
chance_infosets,
player_infosets,
single_infosets,
next,
next_prev,
)
})
.collect();
Ok(Node::Player(Player {
num: player_num,
infoset: info_ind,
actions: next_verts?,
}))
}
}
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum SolveMethod {
Full,
Sampled,
External,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SolveParams {
pub regret: RegretParams,
pub check_interval: u64,
pub seed: u64,
pub fork_depth: u32,
}
impl Default for SolveParams {
fn default() -> Self {
SolveParams {
regret: RegretParams::default(),
check_interval: 256,
seed: 0,
fork_depth: 3,
}
}
}
impl<I, A> GameTree<I, A> {
pub fn solve(
&self,
method: SolveMethod,
max_iter: u64,
max_reg: f64,
num_threads: usize,
params: SolveParams,
) -> Result<(Strategies<'_, I, A>, RegretBound), SolveError> {
let [first_player, second_player] = &self.player_infosets;
let (regrets, probs) = match method {
SolveMethod::Full => vanilla::solve_full_single(
&self.root,
&self.chance_infosets,
[first_player, second_player],
max_iter,
max_reg,
¶ms,
),
SolveMethod::Sampled => vanilla::solve_sampled_single(
&self.root,
&self.chance_infosets,
[first_player, second_player],
max_iter,
max_reg,
¶ms,
),
SolveMethod::External => {
let threads = if num_threads == 0 {
thread::available_parallelism().map_or(1, NonZeroUsize::get)
} else {
num_threads
};
external::solve_external_single(
&self.root,
&self.chance_infosets,
[first_player, second_player],
max_iter,
max_reg,
¶ms,
threads,
)?
}
};
Ok((Strategies { game: self, probs }, RegretBound::new(regrets)))
}
#[must_use]
pub fn num_infosets(&self) -> usize {
let [one, two] = &self.player_infosets;
one.len() + two.len()
}
}
impl<I: Hash + Eq + Clone, A: Hash + Eq + Clone> GameTree<I, A> {
pub fn from_named(
&self,
strats: [impl IntoIterator<
Item = (
impl Borrow<I>,
impl IntoIterator<Item = (impl Borrow<A>, impl Borrow<f64>)>,
),
>; 2],
) -> Result<Strategies<'_, I, A>, StratError> {
let [one_strat, two_strat] = strats;
let [one_info, two_info] = &self.player_infosets;
let [one_single, two_single] = &self.single_infosets;
Ok(Strategies {
game: self,
probs: [
Self::strat_into_box(one_strat, one_info, one_single)?,
Self::strat_into_box(two_strat, two_info, two_single)?,
],
})
}
fn strat_into_box(
strat: impl IntoIterator<
Item = (
impl Borrow<I>,
impl IntoIterator<Item = (impl Borrow<A>, impl Borrow<f64>)>,
),
>,
infos: &[PlayerInfosetData<I, A>],
raw_singles: &[(I, A)],
) -> Result<Box<[f64]>, StratError> {
let mut num_inds = 0;
let mut inds: HashMap<I, HashMap<A, usize>> = HashMap::with_capacity(infos.len());
for info in infos {
let mut actions: HashMap<A, usize> = HashMap::with_capacity(info.num_actions());
for action in &info.actions {
actions.insert(action.clone(), num_inds);
num_inds += 1;
}
inds.insert(info.infoset.clone(), actions);
}
let mut dense = vec![0.0; num_inds].into_boxed_slice();
let mut singles: HashMap<_, _> = raw_singles
.iter()
.map(|(info, act)| (info, (act, false)))
.collect();
for (binfoset, actions) in strat {
let infoset = binfoset.borrow();
if let Some(action_inds) = inds.get(infoset) {
for (baction, bprob) in actions {
let action = baction.borrow();
let prob = bprob.borrow();
if prob >= &0.0 && prob.is_finite() {
let ind = action_inds.get(action).ok_or(StratError::InvalidAction)?;
dense[*ind] = *prob;
} else {
return Err(StratError::InvalidProbability);
}
}
} else if let Some((act, seen)) = singles.get_mut(infoset) {
for (baction, bprob) in actions {
let action = baction.borrow();
let prob = bprob.borrow();
if &action != act {
return Err(StratError::InvalidAction);
} else if prob >= &0.0 && prob.is_finite() {
*seen = true;
} else {
return Err(StratError::InvalidProbability);
}
}
} else {
return Err(StratError::InvalidInfoset);
}
}
for vals in split_by_mut(&mut dense, infos.iter().map(PlayerInfosetData::num_actions)) {
let total: f64 = vals.iter().sum();
if total == 0.0 {
return Err(StratError::UninitializedInfoset);
}
for val in vals.iter_mut() {
*val /= total;
}
}
if !singles.into_values().all(|(_, seen)| seen) {
return Err(StratError::UninitializedInfoset);
}
Ok(dense)
}
}
impl<I: Eq, A: Eq> GameTree<I, A> {
pub fn from_named_eq(
&self,
strats: [impl IntoIterator<
Item = (
impl Borrow<I>,
impl IntoIterator<Item = (impl Borrow<A>, impl Borrow<f64>)>,
),
>; 2],
) -> Result<Strategies<'_, I, A>, StratError> {
let [one_strat, two_strat] = strats;
let [one_info, two_info] = &self.player_infosets;
let [one_single, two_single] = &self.single_infosets;
Ok(Strategies {
game: self,
probs: [
Self::strat_into_box_slow(one_strat, one_info, one_single)?,
Self::strat_into_box_slow(two_strat, two_info, two_single)?,
],
})
}
fn strat_into_box_slow(
strat: impl IntoIterator<
Item = (
impl Borrow<I>,
impl IntoIterator<Item = (impl Borrow<A>, impl Borrow<f64>)>,
),
>,
infos: &[PlayerInfosetData<I, A>],
singles: &[(I, A)],
) -> Result<Box<[f64]>, StratError> {
let mut action_inds = Vec::with_capacity(infos.len());
let mut num_inds = 0;
for info in infos {
action_inds.push(num_inds);
num_inds += info.num_actions();
}
let mut dense = vec![0.0; num_inds].into_boxed_slice();
let mut seen_singles: Box<[_]> = vec![false; singles.len()].into();
for (binfoset, actions) in strat {
let infoset = binfoset.borrow();
if let Some((ind, info)) = infos
.iter()
.enumerate()
.find(|(_, info)| &info.infoset == infoset)
{
let info_ind = action_inds[ind];
for (baction, bprob) in actions {
let action = baction.borrow();
let prob = bprob.borrow();
if prob >= &0.0 && prob.is_finite() {
let (act_ind, _) = info
.actions
.iter()
.enumerate()
.find(|(_, act)| act == &action)
.ok_or(StratError::InvalidAction)?;
dense[info_ind + act_ind] = *prob;
} else {
return Err(StratError::InvalidProbability);
}
}
} else if let Some((ind, (_, act))) = singles
.iter()
.enumerate()
.find(|(_, (info, _))| info == infoset)
{
for (baction, bprob) in actions {
let action = baction.borrow();
let prob = bprob.borrow();
if action != act {
return Err(StratError::InvalidAction);
} else if prob >= &0.0 && prob.is_finite() {
seen_singles[ind] = true;
} else {
return Err(StratError::InvalidProbability);
}
}
} else {
return Err(StratError::InvalidInfoset);
}
}
for vals in split_by_mut(&mut dense, infos.iter().map(PlayerInfosetData::num_actions)) {
let total: f64 = vals.iter().sum();
if total == 0.0 {
return Err(StratError::UninitializedInfoset);
}
for val in vals.iter_mut() {
*val /= total;
}
}
if !Vec::from(seen_singles).into_iter().all(|seen| seen) {
return Err(StratError::UninitializedInfoset);
}
Ok(dense)
}
}
#[derive(Debug, Clone)]
pub struct Strategies<'a, Infoset, Action> {
game: &'a GameTree<Infoset, Action>,
probs: [Box<[f64]>; 2],
}
impl<I, A> PartialEq for Strategies<'_, I, A> {
fn eq(&self, other: &Self) -> bool {
self.game == other.game && self.probs == other.probs
}
}
impl<I, A> Eq for Strategies<'_, I, A> {}
impl<'a, I, A> Strategies<'a, I, A> {
#[must_use]
pub fn as_named<'b: 'a>(&'b self) -> [NamedStrategyIter<'a, I, A>; 2] {
let [info_one, info_two] = &self.game.player_infosets;
let [single_one, single_two] = &self.game.single_infosets;
let [probs_one, probs_two] = &self.probs;
[
NamedStrategyIter::new(info_one, probs_one, single_one),
NamedStrategyIter::new(info_two, probs_two, single_two),
]
}
pub fn truncate(&mut self, thresh: f64) {
for (infos, box_probs) in self.game.player_infosets.iter().zip(self.probs.iter_mut()) {
for strat in split_by_mut(
box_probs.as_mut(),
infos.iter().map(PlayerInfosetData::num_actions),
) {
let total: f64 = strat.iter().filter(|p| p > &&thresh).sum();
if total > 0.0 {
for p in strat.iter_mut() {
*p = if *p > thresh { *p / total } else { 0.0 };
}
}
}
}
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn distance(&self, other: &Self, p: f64) -> [f64; 2] {
assert!(
self.game == other.game,
"can only compare strategies for the same game"
);
assert!(p > 0.0, "`p` must be positive but got: {p}");
let dists: Vec<_> = self
.probs
.iter()
.zip(other.probs.iter())
.zip(self.game.player_infosets.iter())
.map(|((left, right), infos)| {
let num_infosets = infos.len();
if num_infosets == 0 {
return 0.0;
}
let lens = || infos.iter().map(PlayerInfosetData::num_actions);
let total: f64 = split_by(left, lens())
.zip(split_by(right, lens()))
.map(|(left_strat, right_strat)| {
let sum: f64 = left_strat
.iter()
.zip(right_strat.iter())
.map(|(left_val, right_val)| (left_val - right_val).abs().powf(p))
.sum();
(0.5 * sum).powf(1.0 / p)
})
.sum();
total / num_infosets as f64
})
.collect();
dists.try_into().unwrap()
}
#[must_use]
pub fn get_info(&self) -> StrategiesInfo {
let [one_strat, two_strat] = &self.probs;
let [one_info, two_info] = &self.game.player_infosets;
let one_split: Box<[&[f64]]> = split_by(
one_strat,
one_info.iter().map(PlayerInfosetData::num_actions),
)
.collect();
let two_split: Box<[&[f64]]> = split_by(
two_strat,
two_info.iter().map(PlayerInfosetData::num_actions),
)
.collect();
let (util, regrets) = regret::regret(
&self.game.root,
&self.game.chance_infosets,
[one_info, two_info],
[&*one_split, &*two_split],
);
StrategiesInfo { util, regrets }
}
}
#[derive(Debug, Clone)]
pub struct RegretBound {
regrets: [f64; 2],
}
impl RegretBound {
fn new(regrets: [f64; 2]) -> Self {
RegretBound { regrets }
}
#[must_use]
pub fn player_regret_bound(&self, player_num: PlayerNum) -> f64 {
*player_num.ind(&self.regrets)
}
#[must_use]
pub fn regret_bound(&self) -> f64 {
let [one, two] = self.regrets;
f64::max(one, two)
}
}
#[derive(Debug)]
pub struct StrategiesInfo {
util: f64,
regrets: [f64; 2],
}
impl StrategiesInfo {
#[must_use]
pub fn player_regret(&self, player_num: PlayerNum) -> f64 {
*player_num.ind(&self.regrets)
}
#[must_use]
pub fn regret(&self) -> f64 {
let [one, two] = self.regrets;
f64::max(one, two)
}
#[must_use]
pub fn player_utility(&self, player_num: PlayerNum) -> f64 {
match player_num {
PlayerNum::One => self.util,
PlayerNum::Two => -self.util,
}
}
}
#[derive(Debug)]
pub struct NamedStrategyIter<'a, Infoset, Action> {
info: &'a [PlayerInfosetData<Infoset, Action>],
probs: &'a [f64],
singles: slice::Iter<'a, (Infoset, Action)>,
}
impl<'a, I, A> NamedStrategyIter<'a, I, A> {
fn new(info: &'a [PlayerInfosetData<I, A>], probs: &'a [f64], singles: &'a [(I, A)]) -> Self {
NamedStrategyIter {
info,
probs,
singles: singles.iter(),
}
}
}
impl<'a, I, A> Iterator for NamedStrategyIter<'a, I, A> {
type Item = (&'a I, NamedStrategyActionIter<'a, A>);
fn next(&mut self) -> Option<Self::Item> {
if let Some((info, rest_infos)) = self.info.split_first() {
let (probs, rest_probs) = self.probs.split_at(info.num_actions());
self.info = rest_infos;
self.probs = rest_probs;
Some((
&info.infoset,
NamedStrategyActionIter {
iter: ActionType::Data(info.actions.iter().zip(probs.iter())),
},
))
} else if let Some((info, act)) = self.singles.next() {
Some((
info,
NamedStrategyActionIter {
iter: ActionType::Single(iter::once(act)),
},
))
} else {
None
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
let len = self.info.len() + self.singles.len();
(len, Some(len))
}
}
impl<I, A> FusedIterator for NamedStrategyIter<'_, I, A> {}
impl<I, A> ExactSizeIterator for NamedStrategyIter<'_, I, A> {}
#[derive(Debug)]
pub struct NamedStrategyActionIter<'a, Action> {
iter: ActionType<'a, Action>,
}
#[derive(Debug)]
enum ActionType<'a, A> {
Data(Zip<slice::Iter<'a, A>, slice::Iter<'a, f64>>),
Single(Once<&'a A>),
}
impl<'a, A> Iterator for NamedStrategyActionIter<'a, A> {
type Item = (&'a A, f64);
fn next(&mut self) -> Option<Self::Item> {
match &mut self.iter {
ActionType::Data(zip) => zip
.find(|(_, prob)| prob > &&0.0)
.map(|(act, &prob)| (act, prob)),
ActionType::Single(once) => once.next().map(|a| (a, 1.0)),
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
match &self.iter {
ActionType::Data(zip) => (0, Some(zip.len())),
ActionType::Single(once) => once.size_hint(),
}
}
}
impl<A> FusedIterator for NamedStrategyActionIter<'_, A> {}
#[cfg(test)]
#[allow(clippy::float_cmp, unused_must_use)]
mod tests {
use super::{
Game, GameTree, Moves, NodeType, PlayerNum, SolveMethod, SolveParams,
};
use std::convert::Infallible;
#[derive(Debug, Clone, Copy)]
enum Stage {
X,
Z,
Y,
Done,
}
#[derive(Debug, Clone, Copy)]
struct TreeGame(Stage);
impl Game for TreeGame {
type Action = &'static str;
type Infoset = &'static str;
type ChanceInfoset = Infallible;
type Chance = Infallible;
type Player = TreeGame;
fn into_node(self) -> NodeType<Self> {
match self.0 {
Stage::X => NodeType::Player(PlayerNum::One, "x", self),
Stage::Z => NodeType::Player(PlayerNum::Two, "z", self),
Stage::Y => NodeType::Player(PlayerNum::One, "y", self),
Stage::Done => NodeType::Terminal(0.0),
}
}
}
impl Moves<TreeGame> for TreeGame {
fn len(&self) -> usize {
match self.0 {
Stage::X => 1,
Stage::Z | Stage::Y => 2,
Stage::Done => unreachable!(),
}
}
fn action(&self, index: usize) -> &'static str {
match self.0 {
Stage::X => ["a"][index],
Stage::Z => ["b", "c"][index],
Stage::Y => ["c", "d"][index],
Stage::Done => unreachable!(),
}
}
fn apply(&self, index: usize) -> TreeGame {
TreeGame(match (self.0, index) {
(Stage::X, _) => Stage::Z,
(Stage::Z, 0) => Stage::Y,
_ => Stage::Done,
})
}
}
fn create_game() -> GameTree<&'static str, &'static str> {
GameTree::from_game(TreeGame(Stage::X)).unwrap()
}
#[test]
fn strat_names() {
let game = create_game();
let fast = game
.from_named([
vec![("x", vec![("a", 1.0)]), ("y", vec![("c", 1.0), ("d", 2.0)])],
vec![("z", vec![("b", 2.0), ("c", 3.0)])],
])
.unwrap();
let slow = game
.from_named_eq([
vec![("x", vec![("a", 1.0)]), ("y", vec![("c", 1.0), ("d", 2.0)])],
vec![("z", vec![("b", 2.0), ("c", 3.0)])],
])
.unwrap();
assert_eq!(fast, slow);
assert_eq!(fast.distance(&slow, 1.0), [0.0; 2]);
let cloned = game.from_named(fast.as_named()).unwrap();
assert_eq!(fast, cloned);
}
#[test]
fn distance_is_bounded() {
let game = create_game();
let left = game
.from_named([
vec![("x", vec![("a", 1.0)]), ("y", vec![("c", 1.0), ("d", 0.0)])],
vec![("z", vec![("b", 1.0), ("c", 0.0)])],
])
.unwrap();
let right = game
.from_named([
vec![("x", vec![("a", 1.0)]), ("y", vec![("c", 0.0), ("d", 1.0)])],
vec![("z", vec![("b", 0.0), ("c", 1.0)])],
])
.unwrap();
assert_eq!(left.distance(&left, 1.0), [0.0; 2]);
for p in [1.0, 2.0, 3.0] {
let [one, two] = left.distance(&right, p);
assert!((one - 1.0).abs() < 1e-9, "p={p} player one distance {one}");
assert!((two - 1.0).abs() < 1e-9, "p={p} player two distance {two}");
}
}
#[test]
fn named_iter_sizes_are_honest() {
let game = create_game();
let strat = game
.from_named([
vec![("x", vec![("a", 1.0)]), ("y", vec![("c", 1.0), ("d", 0.0)])],
vec![("z", vec![("b", 1.0), ("c", 1.0)])],
])
.unwrap();
let [one, two] = strat.as_named();
assert_eq!(one.len(), 2); assert_eq!(two.len(), 1);
assert_eq!(one.count(), 2); assert_eq!(two.count(), 1);
let [one, _] = strat.as_named();
for (infoset, actions) in one {
let (lower, upper) = actions.size_hint();
let yielded = actions.count();
assert!(
lower <= yielded && yielded <= upper.unwrap(),
"{infoset}: {yielded} outside [{lower}, {upper:?}]"
);
}
}
#[test]
fn truncate_keeps_infoset_when_nothing_clears_threshold() {
let game = create_game();
let mut strat = game
.from_named([
vec![("x", vec![("a", 1.0)]), ("y", vec![("c", 1.0), ("d", 1.0)])],
vec![("z", vec![("b", 1.0), ("c", 1.0)])],
])
.unwrap();
strat.truncate(0.6);
for named in strat.as_named() {
for (infoset, actions) in named {
let total: f64 = actions.map(|(_, prob)| prob).sum();
assert!((total - 1.0).abs() < 1e-9, "infoset {infoset} zeroed: {total}");
}
}
}
#[test]
#[should_panic(expected = "same game")]
fn test_distance_game_panic() {
let game_one = create_game();
let (strat_one, _) = game_one
.solve(SolveMethod::Full, 0, 0.0, 1, SolveParams::default())
.unwrap();
let game_two = create_game();
let (strat_two, _) = game_two
.solve(SolveMethod::Full, 0, 0.0, 1, SolveParams::default())
.unwrap();
strat_one.distance(&strat_two, 1.0);
}
#[test]
#[should_panic(expected = "`p` must be positive")]
fn test_distance_p_panic() {
let game = create_game();
let (strat_one, _) = game
.solve(SolveMethod::Full, 0, 0.0, 1, SolveParams::default())
.unwrap();
let (strat_two, _) = game
.solve(SolveMethod::Full, 0, 0.0, 1, SolveParams::default())
.unwrap();
strat_one.distance(&strat_two, 0.0);
}
}