#![warn(missing_docs)]
mod compact;
mod error;
mod regret;
mod solve;
mod split;
use compact::{Builder, OptBuilder};
pub use error::{GameError, SolveError, StratError};
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::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 ind<'a, T>(&self, arr: &'a [T; 2]) -> &'a T {
match (self, arr) {
(PlayerNum::One, [first, _]) => first,
(PlayerNum::Two, [_, second]) => second,
}
}
fn ind_mut<'a, T>(&self, arr: &'a mut [T; 2]) -> &'a mut T {
match (self, arr) {
(PlayerNum::One, [first, _]) => first,
(PlayerNum::Two, [_, second]) => second,
}
}
}
#[derive(Debug)]
pub enum GameNode<T: IntoGameNode + ?Sized> {
Terminal(f64),
Chance(Option<T::ChanceInfo>, T::Outcomes),
Player(PlayerNum, T::PlayerInfo, T::Actions),
}
pub trait IntoGameNode {
type PlayerInfo: Eq;
type Action: Eq;
type ChanceInfo: Eq;
type Outcomes: IntoIterator<Item = (f64, Self)>;
type Actions: IntoIterator<Item = (Self::Action, Self)>;
fn into_game_node(self) -> GameNode<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>,
}
impl<A> PlayerInfosetBuilder<A> {
fn new(actions: impl Into<Box<[A]>>, prev_infoset: Option<usize>) -> Self {
PlayerInfosetBuilder {
actions: actions.into(),
prev_infoset,
}
}
}
#[derive(Debug)]
struct PlayerInfosetData<I, A> {
infoset: I,
actions: Box<[A]>,
prev_infoset: Option<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
}
}
trait ChanceInfoset {
fn probs(&self) -> &[f64];
}
impl ChanceInfoset for ChanceInfosetData {
fn probs(&self) -> &[f64] {
&self.probs
}
}
#[derive(Debug)]
pub struct Game<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 Game<I, A> {
fn eq(&self, other: &Self) -> bool {
ptr::eq(self, other)
}
}
impl<I, A> Eq for Game<I, A> {}
impl<I: Hash + Eq, A: Hash + Eq> Game<I, A> {
pub fn from_root<T>(root: T) -> Result<Self, GameError>
where
T: IntoGameNode<PlayerInfo = I, Action = A>,
T::ChanceInfo: 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 = Game::init_recurse(
&mut chance_infosets,
&mut [first_player, second_player],
&mut [first_single, second_single],
root,
[None; 2],
)?;
Ok(Game {
chance_infosets: chance_infosets.into_iter().map(|(_, v)| v).collect(),
player_infosets: player_infosets.map(|pinfo| {
pinfo
.into_iter()
.map(|(infoset, builder)| PlayerInfosetData::new(infoset, builder))
.collect()
}),
single_infosets: single_infosets.map(|sinfo| sinfo.into_iter().collect()),
root,
})
}
fn init_recurse<T>(
chance_infosets: &mut OptBuilder<T::ChanceInfo, ChanceInfosetData>,
player_infosets: &mut [&mut Builder<I, PlayerInfosetBuilder<A>>; 2],
single_infosets: &mut [&mut HashMap<I, A>; 2],
node: T,
mut prev_infosets: [Option<usize>; 2],
) -> Result<Node, GameError>
where
T: IntoGameNode<PlayerInfo = I, Action = A>,
T::ChanceInfo: Hash + Eq,
{
match node.into_game_node() {
GameNode::Terminal(payoff) => Ok(Node::Terminal(payoff)),
GameNode::Chance(info, raw_outcomes) => {
let mut probs = Vec::new();
let mut outcomes = Vec::new();
for (prob, next) in raw_outcomes {
if prob > 0.0 && prob.is_finite() {
probs.push(prob);
outcomes.push(Game::init_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 {
Err(GameError::ProbabilitiesNotEqual)
} else {
Ok(ind)
}?
}
};
Ok(Node::Chance(Chance::new(outcomes, ind)))
}
}
}
GameNode::Player(player_num, infoset, raw_actions) => {
let mut actions = Vec::new();
let mut nexts = Vec::new();
for (action, next) in raw_actions {
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();
Game::init_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)
}
}
}?;
*player_num.ind_mut(&mut prev_infosets) = Some(info_ind);
let next_verts: Result<Box<[_]>, _> = nexts
.into_iter()
.map(|next| {
Game::init_recurse(
chance_infosets,
player_infosets,
single_infosets,
next,
prev_infosets,
)
})
.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,
}
impl<I, A> Game<I, A> {
pub fn solve(
&self,
method: SolveMethod,
max_iter: u64,
max_reg: f64,
num_threads: usize,
params: Option<RegretParams>,
) -> Result<(Strategies<I, A>, RegretBound), SolveError> {
let [first_player, second_player] = &self.player_infosets;
let threads = NonZeroUsize::new(num_threads)
.or_else(|| thread::available_parallelism().ok())
.unwrap_or(NonZeroUsize::new(1).unwrap());
let params = params.unwrap_or_default();
let (regrets, probs) = if threads == NonZeroUsize::new(1).unwrap() {
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 => external::solve_external_single(
&self.root,
&self.chance_infosets,
[first_player, second_player],
max_iter,
max_reg,
¶ms,
),
}
} else {
let target = threads
.checked_mul(NonZeroUsize::new(3).unwrap())
.ok_or(SolveError::ThreadOverflow)?;
match method {
SolveMethod::Full => vanilla::solve_full_multi(
&self.root,
&self.chance_infosets,
[first_player, second_player],
max_iter,
max_reg,
(threads, target),
¶ms,
),
SolveMethod::Sampled => vanilla::solve_sampled_multi(
&self.root,
&self.chance_infosets,
[first_player, second_player],
max_iter,
max_reg,
(threads, target),
¶ms,
),
SolveMethod::External => external::solve_external_multi(
&self.root,
&self.chance_infosets,
[first_player, second_player],
max_iter,
max_reg,
(threads, target),
¶ms,
),
}?
};
Ok((Strategies { game: self, probs }, RegretBound::new(regrets)))
}
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> Game<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.iter() {
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(|info| info.num_actions())) {
let total: f64 = vals.iter().sum();
if total == 0.0 {
return Err(StratError::UninitializedInfoset);
} else {
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> Game<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(|info| info.num_actions())) {
let total: f64 = vals.iter().sum();
if total == 0.0 {
return Err(StratError::UninitializedInfoset);
} else {
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 Game<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> {
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(|info| info.num_actions()),
) {
let total: f64 = strat.iter().filter(|p| p > &&thresh).sum();
for p in strat.iter_mut() {
*p = if *p > thresh { *p / total } else { 0.0 }
}
}
}
}
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), info)| {
let mut dist = 0.0;
for (left_val, right_val) in left.iter().zip(right.iter()) {
dist += (left_val - right_val).abs().powf(p);
}
dist / info.len() as f64
})
.collect();
dists.try_into().unwrap()
}
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(|info| info.num_actions())).collect();
let two_split: Box<[&[f64]]> =
split_by(two_strat, two_info.iter().map(|info| info.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 }
}
pub fn player_regret_bound(&self, player_num: PlayerNum) -> f64 {
*player_num.ind(&self.regrets)
}
pub fn regret_bound(&self) -> f64 {
let [one, two] = self.regrets;
f64::max(one, two)
}
}
pub struct StrategiesInfo {
util: f64,
regrets: [f64; 2],
}
impl StrategiesInfo {
pub fn player_regret(&self, player_num: PlayerNum) -> f64 {
*player_num.ind(&self.regrets)
}
pub fn regret(&self) -> f64 {
let [one, two] = self.regrets;
f64::max(one, two)
}
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.probs.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>) {
let len = match &self.iter {
ActionType::Data(zip) => zip.len(),
ActionType::Single(once) => once.len(),
};
(len, Some(len))
}
}
impl<A> FusedIterator for NamedStrategyActionIter<'_, A> {}
impl<A> ExactSizeIterator for NamedStrategyActionIter<'_, A> {}
#[cfg(test)]
mod tests {
use super::{Game, GameNode, IntoGameNode, PlayerNum, SolveMethod};
struct Node(GameNode<Node>);
impl IntoGameNode for Node {
type PlayerInfo = &'static str;
type Action = &'static str;
type ChanceInfo = &'static str;
type Outcomes = Vec<(f64, Node)>;
type Actions = Vec<(&'static str, Node)>;
fn into_game_node(self) -> GameNode<Self> {
self.0
}
}
fn create_game() -> Game<&'static str, &'static str> {
let node = Node(GameNode::Player(
PlayerNum::One,
"x",
vec![(
"a",
Node(GameNode::Player(
PlayerNum::Two,
"z",
vec![
(
"b",
Node(GameNode::Player(
PlayerNum::One,
"y",
vec![
("c", Node(GameNode::Terminal(0.0))),
("d", Node(GameNode::Terminal(0.0))),
],
)),
),
("c", Node(GameNode::Terminal(0.0))),
],
)),
)],
));
Game::from_root(node).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]
#[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, None).unwrap();
let game_two = create_game();
let (strat_two, _) = game_two.solve(SolveMethod::Full, 0, 0.0, 1, None).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, None).unwrap();
let (strat_two, _) = game.solve(SolveMethod::Full, 0, 0.0, 1, None).unwrap();
strat_one.distance(&strat_two, 0.0);
}
}