#![warn(missing_docs, clippy::pedantic)]
mod unescaped;
use nom::{
IResult, Parser,
branch::alt,
bytes::complete::tag,
character::complete::{char, digit0, digit1, multispace0, multispace1, none_of, one_of, u64},
combinator::{map, opt, recognize},
error::{ErrorKind, ParseError},
multi::{many0, separated_list1},
sequence::{delimited, pair, preceded, separated_pair},
};
use num_bigint::BigInt;
use num_rational::BigRational;
use num_traits::Zero;
use std::collections::hash_map::Entry;
use std::collections::{HashMap, HashSet};
use std::error::Error as StdError;
use std::fmt::{Display, Error as FmtError, Formatter};
pub use unescaped::{EscapedStr, Unescaped};
type ChanceInfoset<'a> = (&'a EscapedStr, Box<[(&'a EscapedStr, BigRational)]>);
type PlayerInfoset<'a> = (&'a EscapedStr, Box<[&'a EscapedStr]>);
type Outcomes<'a> = HashMap<u64, (&'a EscapedStr, Box<[BigRational]>)>;
#[derive(Debug, PartialEq, Clone)]
struct Infosets<'a> {
player: Box<[HashMap<u64, PlayerInfoset<'a>>]>,
chance: HashMap<u64, ChanceInfoset<'a>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct NodeId(usize);
#[derive(Debug, PartialEq, Clone)]
enum RawNode<'a> {
Chance(RawChance<'a>),
Player(RawPlayer<'a>),
Terminal(RawTerminal<'a>),
}
#[derive(Debug, PartialEq, Eq, Clone)]
struct RawChance<'a> {
name: &'a EscapedStr,
infoset: u64,
declared: bool,
children: Box<[NodeId]>,
outcome: u64,
outcome_declared: bool,
}
#[derive(Debug, PartialEq, Eq, Clone)]
struct RawPlayer<'a> {
name: &'a EscapedStr,
player_num: usize,
infoset: u64,
declared: bool,
children: Box<[NodeId]>,
outcome: u64,
outcome_declared: bool,
}
#[derive(Debug, PartialEq, Eq, Clone)]
struct RawTerminal<'a> {
name: &'a EscapedStr,
outcome: u64,
outcome_declared: bool,
}
#[derive(Debug, PartialEq, Clone)]
pub struct ExtensiveFormGame<'a> {
name: &'a EscapedStr,
player_names: Box<[&'a EscapedStr]>,
comment: Option<&'a EscapedStr>,
infosets: Infosets<'a>,
outcomes: Outcomes<'a>,
nodes: Box<[RawNode<'a>]>,
root: NodeId,
}
impl<'a> ExtensiveFormGame<'a> {
#[must_use]
pub fn name(&self) -> &'a EscapedStr {
self.name
}
#[must_use]
pub fn player_names(&self) -> &[&'a EscapedStr] {
&self.player_names
}
#[must_use]
pub fn comment(&self) -> Option<&'a EscapedStr> {
self.comment
}
#[must_use]
pub fn root<'g>(&'g self) -> Node<'a, 'g> {
self.wrap(self.root)
}
#[must_use]
pub fn display<'g>(&'g self, mode: WriteMode) -> GameDisplay<'a, 'g> {
GameDisplay { game: self, mode }
}
fn wrap<'g>(&'g self, id: NodeId) -> Node<'a, 'g> {
match &self.nodes[id.0] {
RawNode::Chance(raw) => Node::Chance(Chance { game: self, raw }),
RawNode::Player(raw) => Node::Player(Player { game: self, raw }),
RawNode::Terminal(raw) => Node::Terminal(Terminal { game: self, raw }),
}
}
fn outcome_name(&self, outcome: u64) -> Option<&'a EscapedStr> {
self.outcomes.get(&outcome).map(|(name, _)| *name)
}
fn outcome_payoffs(&self, outcome: u64) -> Option<&[BigRational]> {
self.outcomes.get(&outcome).map(|(_, payoffs)| &payoffs[..])
}
}
impl Display for ExtensiveFormGame<'_> {
fn fmt(&self, out: &mut Formatter<'_>) -> Result<(), FmtError> {
self.display(WriteMode::Faithful).fmt(out)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WriteMode {
Minimal,
Faithful,
Exhaustive,
}
#[derive(Clone, Copy)]
pub struct GameDisplay<'a, 'g> {
game: &'g ExtensiveFormGame<'a>,
mode: WriteMode,
}
impl GameDisplay<'_, '_> {
fn declares_outcome(self, outcome: u64, declared: bool, seen: &mut HashSet<u64>) -> bool {
match self.mode {
WriteMode::Minimal => outcome != 0 && seen.insert(outcome),
WriteMode::Faithful => declared,
WriteMode::Exhaustive => outcome != 0,
}
}
}
impl Display for GameDisplay<'_, '_> {
fn fmt(&self, out: &mut Formatter<'_>) -> Result<(), FmtError> {
let game = self.game;
write!(out, "EFG 2 R \"{}\" {{ ", game.name.escape())?;
for name in &game.player_names {
write!(out, "\"{}\" ", name.escape())?;
}
writeln!(out, "}}")?;
if let Some(comment) = game.comment {
writeln!(out, "\"{}\"", comment.escape())?;
}
let mut chance_seen = HashSet::new();
let mut player_seen = HashSet::new();
let mut outcome_seen = HashSet::new();
if self.mode == WriteMode::Minimal {
chance_seen.reserve(game.infosets.chance.len());
player_seen.reserve(game.infosets.player.iter().map(HashMap::len).sum());
outcome_seen.reserve(game.outcomes.len());
}
let mut stack = vec![game.root];
while let Some(id) = stack.pop() {
match &game.nodes[id.0] {
RawNode::Chance(raw) => {
write!(out, "\nc \"{}\" {}", raw.name.escape(), raw.infoset)?;
let block = match self.mode {
WriteMode::Minimal => chance_seen.insert(raw.infoset),
WriteMode::Faithful => raw.declared,
WriteMode::Exhaustive => true,
};
if block {
let (label, actions) = &game.infosets.chance[&raw.infoset];
write!(out, " \"{}\" {{ ", label.escape())?;
for (action, prob) in actions {
write!(out, "\"{}\" {} ", action.escape(), prob)?;
}
write!(out, "}}")?;
}
let declared =
self.declares_outcome(raw.outcome, raw.outcome_declared, &mut outcome_seen);
write_outcome(out, game, raw.outcome, declared)?;
stack.extend(raw.children.iter().rev().copied());
}
RawNode::Player(raw) => {
write!(
out,
"\np \"{}\" {} {}",
raw.name.escape(),
raw.player_num,
raw.infoset
)?;
let block = match self.mode {
WriteMode::Minimal => player_seen.insert((raw.player_num, raw.infoset)),
WriteMode::Faithful => raw.declared,
WriteMode::Exhaustive => true,
};
if block {
let (label, actions) =
&game.infosets.player[raw.player_num - 1][&raw.infoset];
write!(out, " \"{}\" {{ ", label.escape())?;
for action in actions {
write!(out, "\"{}\" ", action.escape())?;
}
write!(out, "}}")?;
}
let declared =
self.declares_outcome(raw.outcome, raw.outcome_declared, &mut outcome_seen);
write_outcome(out, game, raw.outcome, declared)?;
stack.extend(raw.children.iter().rev().copied());
}
RawNode::Terminal(raw) => {
write!(out, "\nt \"{}\"", raw.name.escape())?;
let declared =
self.declares_outcome(raw.outcome, raw.outcome_declared, &mut outcome_seen);
write_outcome(out, game, raw.outcome, declared)?;
}
}
}
writeln!(out)
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum Error<'a> {
Parse(&'a str),
Validation(ValidationError),
}
impl Display for Error<'_> {
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), FmtError> {
match self {
Error::Parse(rem) => write!(fmt, "error parsing game at: '{rem}'"),
Error::Validation(err) => write!(fmt, "invalid efg: {err}"),
}
}
}
impl StdError for Error<'_> {}
#[derive(Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ValidationError {
InvalidPlayerNum,
NonMatchingInfosetNames,
NonMatchingInfosetActions,
NullOutcomePayoffs,
InvalidNumberOfPayoffs,
NonMatchingOutcomeNames,
NonMatchingOutcomePayoffs,
UndefinedOutcome,
UndeclaredInfoset,
}
impl Display for ValidationError {
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), FmtError> {
write!(fmt, "{self:?}")
}
}
impl From<ValidationError> for Error<'_> {
fn from(err: ValidationError) -> Self {
Error::Validation(err)
}
}
impl<'a> From<nom::Err<nom::error::Error<&'a str>>> for Error<'a> {
fn from(err: nom::Err<nom::error::Error<&'a str>>) -> Self {
match err {
nom::Err::Incomplete(_) => panic!("internal error: incomplete parsing"),
nom::Err::Error(err) | nom::Err::Failure(err) => Error::Parse(err.input),
}
}
}
impl<'a> ExtensiveFormGame<'a> {
pub fn try_from_str(input: &'a str) -> Result<Self, Error<'a>> {
let (rest, game) = parse_game(input)?;
let rest = rest.trim_start();
if !rest.is_empty() {
return Err(Error::Parse(rest));
}
Ok(game)
}
}
impl<'a> TryFrom<&'a str> for ExtensiveFormGame<'a> {
type Error = Error<'a>;
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
Self::try_from_str(input)
}
}
#[derive(Clone, Copy)]
pub enum Node<'a, 'g> {
Chance(Chance<'a, 'g>),
Player(Player<'a, 'g>),
Terminal(Terminal<'a, 'g>),
}
fn write_outcome(
out: &mut Formatter<'_>,
game: &ExtensiveFormGame<'_>,
outcome: u64,
declared: bool,
) -> Result<(), FmtError> {
write!(out, " {outcome}")?;
if declared {
if let Some(name) = game.outcome_name(outcome) {
write!(out, " \"{}\"", name.escape())?;
}
if let Some(payoffs) = game.outcome_payoffs(outcome) {
write!(out, " {{ ")?;
for payoff in payoffs {
write!(out, "{payoff} ")?;
}
write!(out, "}}")?;
}
}
Ok(())
}
#[derive(Clone, Copy)]
pub struct Chance<'a, 'g> {
game: &'g ExtensiveFormGame<'a>,
raw: &'g RawChance<'a>,
}
impl<'a, 'g> Chance<'a, 'g> {
fn entry(self) -> &'g ChanceInfoset<'a> {
&self.game.infosets.chance[&self.raw.infoset]
}
#[must_use]
pub fn name(self) -> &'a EscapedStr {
self.raw.name
}
#[must_use]
pub fn infoset(self) -> u64 {
self.raw.infoset
}
#[must_use]
pub fn infoset_name(self) -> &'a EscapedStr {
self.entry().0
}
pub fn actions(
self,
) -> impl Iterator<Item = (&'a EscapedStr, &'g BigRational, Node<'a, 'g>)> + 'g {
let (_, actions) = self.entry();
let game = self.game;
actions
.iter()
.zip(self.raw.children.iter())
.map(move |((label, prob), &child)| (*label, prob, game.wrap(child)))
}
#[must_use]
pub fn action(self, label: &str) -> Option<(&'g BigRational, Node<'a, 'g>)> {
self.actions()
.find(|(name, _, _)| name.unescape().eq(label.chars()))
.map(|(_, prob, next)| (prob, next))
}
#[allow(clippy::len_without_is_empty)]
#[must_use]
pub fn len(self) -> usize {
self.raw.children.len()
}
#[must_use]
pub fn action_at(
self,
index: usize,
) -> Option<(&'a EscapedStr, &'g BigRational, Node<'a, 'g>)> {
let (_, actions) = self.entry();
let (label, prob) = actions.get(index)?;
let &child = self.raw.children.get(index)?;
Some((*label, prob, self.game.wrap(child)))
}
#[must_use]
pub fn outcome(self) -> u64 {
self.raw.outcome
}
#[must_use]
pub fn outcome_name(self) -> Option<&'a EscapedStr> {
self.game.outcome_name(self.raw.outcome)
}
#[must_use]
pub fn outcome_payoffs(self) -> Option<&'g [BigRational]> {
self.game.outcome_payoffs(self.raw.outcome)
}
}
#[derive(Clone, Copy)]
pub struct Player<'a, 'g> {
game: &'g ExtensiveFormGame<'a>,
raw: &'g RawPlayer<'a>,
}
impl<'a, 'g> Player<'a, 'g> {
fn entry(self) -> &'g PlayerInfoset<'a> {
&self.game.infosets.player[self.raw.player_num - 1][&self.raw.infoset]
}
#[must_use]
pub fn name(self) -> &'a EscapedStr {
self.raw.name
}
#[must_use]
pub fn player_num(self) -> usize {
self.raw.player_num
}
#[must_use]
pub fn infoset(self) -> u64 {
self.raw.infoset
}
#[must_use]
pub fn infoset_name(self) -> &'a EscapedStr {
self.entry().0
}
pub fn actions(self) -> impl Iterator<Item = (&'a EscapedStr, Node<'a, 'g>)> + 'g {
let (_, labels) = self.entry();
let game = self.game;
labels
.iter()
.zip(self.raw.children.iter())
.map(move |(label, &child)| (*label, game.wrap(child)))
}
#[must_use]
pub fn action(self, label: &str) -> Option<Node<'a, 'g>> {
self.actions()
.find(|(name, _)| name.unescape().eq(label.chars()))
.map(|(_, next)| next)
}
#[allow(clippy::len_without_is_empty)]
#[must_use]
pub fn len(self) -> usize {
self.raw.children.len()
}
#[must_use]
pub fn action_at(self, index: usize) -> Option<(&'a EscapedStr, Node<'a, 'g>)> {
let (_, actions) = self.entry();
let &label = actions.get(index)?;
let &child = self.raw.children.get(index)?;
Some((label, self.game.wrap(child)))
}
#[must_use]
pub fn outcome(self) -> u64 {
self.raw.outcome
}
#[must_use]
pub fn outcome_name(self) -> Option<&'a EscapedStr> {
self.game.outcome_name(self.raw.outcome)
}
#[must_use]
pub fn outcome_payoffs(self) -> Option<&'g [BigRational]> {
self.game.outcome_payoffs(self.raw.outcome)
}
}
#[derive(Clone, Copy)]
pub struct Terminal<'a, 'g> {
game: &'g ExtensiveFormGame<'a>,
raw: &'g RawTerminal<'a>,
}
impl<'a, 'g> Terminal<'a, 'g> {
#[must_use]
pub fn name(self) -> &'a EscapedStr {
self.raw.name
}
#[must_use]
pub fn outcome(self) -> u64 {
self.raw.outcome
}
#[must_use]
pub fn outcome_name(self) -> Option<&'a EscapedStr> {
self.game.outcome_name(self.raw.outcome)
}
#[must_use]
pub fn outcome_payoffs(self) -> Option<&'g [BigRational]> {
self.game.outcome_payoffs(self.raw.outcome)
}
}
fn negate(input: &str) -> IResult<&str, bool> {
let (input, res) = opt(one_of("+-")).parse(input)?;
Ok((input, res == Some('-')))
}
fn fail(input: &str) -> nom::Err<nom::error::Error<&str>> {
nom::Err::Error(nom::error::Error::new(input, ErrorKind::Fail))
}
const MAX_ABS_EXPONENT: i32 = 10_000;
fn big_float(input: &str) -> IResult<&str, BigRational> {
let (res_input, (main_neg, (int, dec), exp)) = (
negate,
alt((
pair(
digit1,
map(opt(preceded(char('.'), digit0)), Option::unwrap_or_default),
),
separated_pair(digit0, char('.'), digit1),
)),
opt(preceded(one_of("eE"), pair(negate, digit1))),
)
.parse(input)?;
let mut res = if int.is_empty() {
BigRational::zero()
} else {
BigRational::from_integer(int.parse().unwrap())
};
if !dec.is_empty() {
let pow: u32 = dec.len().try_into().map_err(|_| fail(input))?;
res += BigRational::new(dec.parse().unwrap(), BigInt::from(10).pow(pow));
}
if let Some((neg, exp)) = exp {
let exp: i32 = exp.parse().map_err(|_| fail(input))?;
if exp > MAX_ABS_EXPONENT {
return Err(fail(input));
}
res *= BigRational::from_integer(10.into()).pow(if neg { -exp } else { exp });
}
if main_neg {
res = -res;
}
Ok((res_input, res))
}
fn big_rational(input: &str) -> IResult<&str, BigRational> {
let (rest, (num, denom)) = pair(big_float, opt(preceded(char('/'), big_float))).parse(input)?;
match denom {
Some(denom) if denom.is_zero() => Err(fail(input)),
Some(denom) => Ok((rest, num / denom)),
None => Ok((rest, num)),
}
}
fn label(input: &str) -> IResult<&str, &EscapedStr> {
map(
delimited(
char('"'),
recognize(many0(alt((tag(r#"\""#), recognize(none_of("\"")))))),
char('"'),
),
EscapedStr::new,
)
.parse(input)
}
fn spacelist<'a, O, E, F>(f: F) -> impl Parser<&'a str, Output = Vec<O>, Error = E>
where
F: Parser<&'a str, Output = O, Error = E>,
E: ParseError<&'a str>,
{
delimited(
pair(char('{'), multispace0),
separated_list1(multispace1, f),
pair(multispace0, char('}')),
)
}
fn commalist<'a, O, E, F>(f: F) -> impl Parser<&'a str, Output = Vec<O>, Error = E>
where
F: Parser<&'a str, Output = O, Error = E>,
E: ParseError<&'a str>,
{
delimited(
pair(char('{'), multispace0),
separated_list1((multispace0, opt(char(',')), multispace0), f),
pair(multispace0, char('}')),
)
}
struct PendingNode<'a> {
node: RawNode<'a>,
child_count: usize,
children: Vec<NodeId>,
}
impl<'a> PendingNode<'a> {
fn finish(self) -> RawNode<'a> {
let PendingNode {
mut node, children, ..
} = self;
match &mut node {
RawNode::Chance(chance) => chance.children = children.into(),
RawNode::Player(player) => player.children = children.into(),
RawNode::Terminal(_) => unreachable!("terminal nodes are never pending"),
}
node
}
}
fn resolve_infoset<'a, A: PartialEq>(
map: &mut HashMap<u64, (&'a EscapedStr, Box<[A]>)>,
infoset: u64,
declared: Option<(&'a EscapedStr, Vec<A>)>,
) -> Result<(bool, usize), Error<'a>> {
if let Some((name, actions)) = declared {
match map.entry(infoset) {
Entry::Vacant(ent) => {
let count = actions.len();
ent.insert((name, actions.into()));
Ok((true, count))
}
Entry::Occupied(ent) => {
let (stored_name, stored_actions) = ent.get();
if *stored_name != name {
Err(ValidationError::NonMatchingInfosetNames.into())
} else if **stored_actions != *actions {
Err(ValidationError::NonMatchingInfosetActions.into())
} else {
Ok((true, actions.len()))
}
}
}
} else {
let (_, actions) = map
.get(&infoset)
.ok_or(ValidationError::UndeclaredInfoset)?;
Ok((false, actions.len()))
}
}
fn resolve_outcome<'a>(
outcomes: &mut Outcomes<'a>,
num_players: usize,
outcome: u64,
definition: Option<(&'a EscapedStr, Vec<BigRational>)>,
) -> Result<(), Error<'a>> {
if let Some((name, payoffs)) = definition {
if outcome == 0 {
Err(ValidationError::NullOutcomePayoffs.into())
} else if payoffs.len() != num_players {
Err(ValidationError::InvalidNumberOfPayoffs.into())
} else {
match outcomes.entry(outcome) {
Entry::Vacant(ent) => {
ent.insert((name, payoffs.into()));
Ok(())
}
Entry::Occupied(ent) => {
let (stored_name, stored_payoffs) = ent.get();
if *stored_name != name {
Err(ValidationError::NonMatchingOutcomeNames.into())
} else if **stored_payoffs != *payoffs {
Err(ValidationError::NonMatchingOutcomePayoffs.into())
} else {
Ok(())
}
}
}
}
} else if outcome != 0 && !outcomes.contains_key(&outcome) {
Err(ValidationError::UndefinedOutcome.into())
} else {
Ok(())
}
}
fn parse_tree<'a>(
mut input: &'a str,
infosets: &mut Infosets<'a>,
outcomes: &mut Outcomes<'a>,
num_players: usize,
) -> Result<(&'a str, Box<[RawNode<'a>]>, NodeId), Error<'a>> {
let mut nodes: Vec<RawNode<'a>> = Vec::new();
let mut stack: Vec<PendingNode<'a>> = Vec::new();
loop {
let (rest, style) = preceded(multispace1, one_of("cpt")).parse(input)?;
input = rest;
let mut completed = match style {
'c' => {
let (rest, chance, child_count) =
parse_chance(input, infosets, outcomes, num_players)?;
input = rest;
stack.push(PendingNode {
node: RawNode::Chance(chance),
child_count,
children: Vec::with_capacity(child_count),
});
continue;
}
'p' => {
let (rest, player, child_count) =
parse_player(input, infosets, outcomes, num_players)?;
input = rest;
stack.push(PendingNode {
node: RawNode::Player(player),
child_count,
children: Vec::with_capacity(child_count),
});
continue;
}
't' => {
let (rest, term) = parse_terminal(input, outcomes, num_players)?;
input = rest;
push_node(&mut nodes, RawNode::Terminal(term))
}
_ => unreachable!(),
};
loop {
let Some(pending) = stack.last_mut() else {
return Ok((input, nodes.into(), completed));
};
pending.children.push(completed);
if pending.children.len() < pending.child_count {
break;
}
completed = push_node(&mut nodes, stack.pop().unwrap().finish());
}
}
}
fn push_node<'a>(nodes: &mut Vec<RawNode<'a>>, node: RawNode<'a>) -> NodeId {
let id = NodeId(nodes.len());
nodes.push(node);
id
}
fn parse_chance<'a>(
input: &'a str,
infosets: &mut Infosets<'a>,
outcomes: &mut Outcomes<'a>,
num_players: usize,
) -> Result<(&'a str, RawChance<'a>, usize), Error<'a>> {
let (input, (name, infoset, declared, outcome, definition)) = (
preceded(multispace1, label),
preceded(multispace1, u64),
opt((
preceded(multispace1, label),
preceded(
multispace1,
spacelist(separated_pair(label, multispace1, big_rational)),
),
)),
preceded(multispace1, u64),
opt((
preceded(multispace1, label),
preceded(multispace1, commalist(big_rational)),
)),
)
.parse(input)?;
let (declared, child_count) = resolve_infoset(&mut infosets.chance, infoset, declared)?;
let outcome_declared = definition.is_some();
resolve_outcome(outcomes, num_players, outcome, definition)?;
Ok((
input,
RawChance {
name,
infoset,
declared,
children: Box::default(),
outcome,
outcome_declared,
},
child_count,
))
}
fn parse_player<'a>(
input: &'a str,
infosets: &mut Infosets<'a>,
outcomes: &mut Outcomes<'a>,
num_players: usize,
) -> Result<(&'a str, RawPlayer<'a>, usize), Error<'a>> {
let (input, (name, player_num, infoset, declared, outcome, definition)) = (
preceded(multispace1, label),
preceded(multispace1, u64),
preceded(multispace1, u64),
opt((
preceded(multispace1, label),
preceded(multispace1, spacelist(label)),
)),
preceded(multispace1, u64),
opt((
preceded(multispace1, label),
preceded(multispace1, commalist(big_rational)),
)),
)
.parse(input)?;
let player_num: usize = player_num.try_into().map_err(|_| fail(input))?;
if player_num == 0 || player_num > infosets.player.len() {
return Err(ValidationError::InvalidPlayerNum.into());
}
let (declared, child_count) =
resolve_infoset(&mut infosets.player[player_num - 1], infoset, declared)?;
let outcome_declared = definition.is_some();
resolve_outcome(outcomes, num_players, outcome, definition)?;
Ok((
input,
RawPlayer {
name,
player_num,
infoset,
declared,
children: Box::default(),
outcome,
outcome_declared,
},
child_count,
))
}
fn parse_terminal<'a>(
input: &'a str,
outcomes: &mut Outcomes<'a>,
num_players: usize,
) -> Result<(&'a str, RawTerminal<'a>), Error<'a>> {
let (input, (name, outcome, definition)) = (
preceded(multispace1, label),
preceded(multispace1, u64),
opt((
preceded(multispace1, label),
preceded(multispace1, commalist(big_rational)),
)),
)
.parse(input)?;
let outcome_declared = definition.is_some();
resolve_outcome(outcomes, num_players, outcome, definition)?;
Ok((
input,
RawTerminal {
name,
outcome,
outcome_declared,
},
))
}
fn parse_game(input: &str) -> Result<(&str, ExtensiveFormGame<'_>), Error<'_>> {
let (input, (name, player_names, comment)) = (
preceded(
(
multispace0,
tag("EFG"),
multispace1,
tag("2"),
multispace1,
one_of("RD"),
multispace1,
),
label,
),
preceded(multispace1, spacelist(label)),
opt(preceded(multispace1, label)),
)
.parse(input)?;
let num_players = player_names.len();
let mut infosets = Infosets {
player: (0..num_players).map(|_| HashMap::new()).collect(),
chance: HashMap::new(),
};
let mut outcomes = Outcomes::new();
let (input, nodes, root) = parse_tree(input, &mut infosets, &mut outcomes, num_players)?;
Ok((
input,
ExtensiveFormGame {
name,
player_names: player_names.into(),
comment,
infosets,
outcomes,
nodes,
root,
},
))
}
#[cfg(test)]
mod tests {
use super::{Error, EscapedStr, ExtensiveFormGame, Node, ValidationError, WriteMode};
use num_rational::BigRational;
use num_traits::One;
fn validation_err(game: &str) -> ValidationError {
match ExtensiveFormGame::try_from_str(game) {
Err(Error::Validation(err)) => err,
other => panic!("expected a validation error, got {other:?}"),
}
}
#[test]
fn test_big_float() {
let (input, num) = super::big_float("3 ").unwrap();
assert_eq!(input, " ");
assert_eq!(num, BigRational::from_integer(3.into()));
let (input, num) = super::big_float("-2. ").unwrap();
assert_eq!(input, " ");
assert_eq!(num, BigRational::from_integer((-2).into()));
let (input, num) = super::big_float("+.56 ").unwrap();
assert_eq!(input, " ");
assert_eq!(num, BigRational::new(56.into(), 100.into()));
let (input, num) = super::big_float("3.14e-1 ").unwrap();
assert_eq!(input, " ");
assert_eq!(num, BigRational::new(314.into(), 1000.into()));
}
#[test]
fn test_big_rational() {
let (input, num) = super::big_rational("3 ").unwrap();
assert_eq!(input, " ");
assert_eq!(num, BigRational::from_integer(3.into()));
let (input, num) = super::big_rational("99/100 ").unwrap();
assert_eq!(input, " ");
assert_eq!(num, BigRational::new(99.into(), 100.into()));
let (input, num) = super::big_rational(".1e3/+1.e2 ").unwrap();
assert_eq!(input, " ");
assert_eq!(num, BigRational::one());
}
#[test]
fn test_label() {
let (input, label) = super::label(r#""" "#).unwrap();
assert_eq!(input, " ");
assert_eq!(label.escape(), "");
let (input, label) = super::label(r#""normal" "#).unwrap();
assert_eq!(input, " ");
assert_eq!(label.escape(), "normal");
let (input, label) = super::label(r#""esca\"ped" "#).unwrap();
assert_eq!(input, " ");
assert_eq!(label.escape(), r#"esca\"ped"#);
let (input, label) = super::label(r#""back\slash" "#).unwrap();
assert_eq!(input, " ");
assert_eq!(label.escape(), r"back\slash");
assert!(super::label(r#""pair\\" "#).is_err());
assert!(super::label(r#""unterminated"#).is_err());
assert!(super::label("noquote").is_err());
}
#[test]
fn simple_test() {
let game_str = r#"
EFG 2 R "General Bayes game, one stage" { "Player 1" "Player 2" }
"A single stage General Bayes Game"
c "ROOT" 1 "(0,1)" { "1G" 0.500000 "1B" 0.500000 } 0
p "" 1 1 "(1,1)" { "H" "L" } 0
t "" 1 "Outcome 1" { 10.000000 2.000000 }
t "" 2 "Outcome 2" { 0.000000 10.000000 }
p "" 2 1 "(2,1)" { "h" "l" } 0
t "" 3 "Outcome 3" { 2.000000 4.000000 }
t "" 4 "Outcome 4" { 4.000000 0.000000 }
"#;
let game = ExtensiveFormGame::try_from_str(game_str).unwrap();
assert_eq!(
game.to_string(),
r#"EFG 2 R "General Bayes game, one stage" { "Player 1" "Player 2" }
"A single stage General Bayes Game"
c "ROOT" 1 "(0,1)" { "1G" 1/2 "1B" 1/2 } 0
p "" 1 1 "(1,1)" { "H" "L" } 0
t "" 1 "Outcome 1" { 10 2 }
t "" 2 "Outcome 2" { 0 10 }
p "" 2 1 "(2,1)" { "h" "l" } 0
t "" 3 "Outcome 3" { 2 4 }
t "" 4 "Outcome 4" { 4 0 }
"#
);
assert_eq!(game.name().to_string(), "General Bayes game, one stage");
assert_eq!(game.player_names().len(), 2);
let Node::Chance(root) = game.root() else {
panic!("expected a chance root");
};
let labels: Vec<_> = root.actions().map(|(label, _, _)| label.escape()).collect();
assert_eq!(labels, ["1G", "1B"]);
}
#[test]
fn navigates_handles() {
let game_str = r#"EFG 2 R "g" { "Player 1" "Player 2" }
p "root" 1 1 "iset" { "L" "R" } 0
t "tl" 1 "o1" { 1 2 }
t "tr" 2 "o2" { 3 4 }
"#;
let game = ExtensiveFormGame::try_from_str(game_str).unwrap();
let Node::Player(root) = game.root() else {
panic!("expected a player root");
};
assert_eq!(root.player_num(), 1);
assert_eq!(root.infoset(), 1);
assert_eq!(root.infoset_name().escape(), "iset");
let labels: Vec<_> = root.actions().map(|(label, _)| label.escape()).collect();
assert_eq!(labels, ["L", "R"]);
let Some(Node::Terminal(left)) = root.action("L") else {
panic!("expected a terminal after action L");
};
assert_eq!(left.name().escape(), "tl");
assert_eq!(left.outcome(), 1);
assert_eq!(left.outcome_name().map(EscapedStr::escape), Some("o1"));
let payoffs: Vec<_> = left
.outcome_payoffs()
.unwrap()
.iter()
.map(BigRational::to_string)
.collect();
assert_eq!(payoffs, ["1", "2"]);
}
#[test]
fn chance_probabilities_need_not_sum_to_one() {
let game = "EFG 2 R \"\" { \"1\" \"2\" }
c \"\" 1 \"a\" { \"x\" 9/10 } 0
t \"\" 1 \"\" { 0 0 }
";
let parsed = ExtensiveFormGame::try_from_str(game).unwrap();
let Node::Chance(root) = parsed.root() else {
panic!("expected a chance root");
};
let (prob, _) = root.action("x").unwrap();
assert_eq!(prob.to_string(), "9/10");
}
#[test]
fn invalid_player_num() {
assert_eq!(
validation_err(
"EFG 2 R \"\" { \"1\" \"2\" }
p \"\" 3 1 \"a\" { \"x\" } 0
t \"\" 1 { 0 0 }
"
),
ValidationError::InvalidPlayerNum
);
}
#[test]
fn invalid_infoset_names() {
assert_eq!(
validation_err(
"EFG 2 R \"\" { \"1\" \"2\" }
p \"\" 1 1 \"a\" { \"x\" } 0
p \"\" 1 1 \"b\" { \"x\" } 0
t \"\" 1 { 0 0 }
"
),
ValidationError::NonMatchingInfosetNames
);
}
#[test]
fn invalid_chance_infoset_names() {
assert_eq!(
validation_err(
"EFG 2 R \"\" { \"1\" \"2\" }
c \"\" 1 \"a\" { \"x\" 1 } 0
c \"\" 1 \"b\" { \"x\" 1 } 0
t \"\" 1 { 0 0 }
"
),
ValidationError::NonMatchingInfosetNames
);
}
#[test]
fn invalid_infoset_actions() {
assert_eq!(
validation_err(
"EFG 2 R \"\" { \"1\" \"2\" }
p \"\" 1 1 \"a\" { \"L\" \"R\" } 0
t \"\" 1 \"\" { 0 0 }
p \"\" 1 1 \"a\" { \"R\" \"L\" } 0
t \"\" 2 \"\" { 0 0 }
t \"\" 3 \"\" { 0 0 }
"
),
ValidationError::NonMatchingInfosetActions
);
}
#[test]
fn invalid_chance_infoset_actions() {
assert_eq!(
validation_err(
"EFG 2 R \"\" { \"1\" \"2\" }
c \"\" 1 \"a\" { \"x\" 1 } 0
c \"\" 1 \"a\" { \"y\" 1 } 0
t \"\" 1 { 0 0 }
"
),
ValidationError::NonMatchingInfosetActions
);
}
#[test]
fn null_outcome_payoffs() {
assert_eq!(
validation_err(
"EFG 2 R \"\" { \"1\" \"2\" }
p \"\" 1 1 \"a\" { \"x\" } 0 \"n\" { 0 0 }
t \"\" 1 { 0 0 }
"
),
ValidationError::NullOutcomePayoffs
);
}
#[test]
fn invalid_payoff_number() {
assert_eq!(
validation_err(
"EFG 2 R \"\" { \"1\" \"2\" }
t \"\" 1 \"\" { 0 }
"
),
ValidationError::InvalidNumberOfPayoffs
);
}
#[test]
fn non_matching_outcome_names() {
assert_eq!(
validation_err(
"EFG 2 R \"\" { \"1\" \"2\" }
p \"\" 1 1 \"a\" { \"x\" } 1 \"b\" { 0 0 }
t \"\" 1 \"c\" { 0 0 }
"
),
ValidationError::NonMatchingOutcomeNames
);
}
#[test]
fn non_matching_outcome_payoffs() {
assert_eq!(
validation_err(
"EFG 2 R \"\" { \"1\" \"2\" }
p \"\" 1 1 \"a\" { \"x\" } 1 \"\" { 0 0 }
t \"\" 1 \"\" { 1 1 }
"
),
ValidationError::NonMatchingOutcomePayoffs
);
}
#[test]
fn undefined_outcome() {
assert_eq!(
validation_err(
"EFG 2 R \"\" { \"1\" \"2\" }
t \"\" 5
"
),
ValidationError::UndefinedOutcome
);
}
#[test]
fn undeclared_infoset() {
assert_eq!(
validation_err(
"EFG 2 R \"\" { \"1\" \"2\" }
p \"\" 1 1 0
t \"\" 1 { 0 0 }
"
),
ValidationError::UndeclaredInfoset
);
}
#[test]
fn fills_omitted_action_list() {
let game_str = "EFG 2 R \"\" { \"1\" \"2\" }
p \"\" 1 1 \"a\" { \"L\" \"R\" } 0
t \"\" 1 \"\" { 0 0 }
p \"\" 1 1 0
t \"\" 2 \"\" { 0 0 }
t \"\" 3 \"\" { 0 0 }
";
let game = ExtensiveFormGame::try_from_str(game_str).unwrap();
let Node::Player(root) = game.root() else {
panic!("expected a player root");
};
let Some(Node::Player(omitted)) = root.action("R") else {
panic!("expected a player after action R");
};
assert_eq!(omitted.infoset_name().escape(), "a");
let labels: Vec<_> = omitted.actions().map(|(label, _)| label.escape()).collect();
assert_eq!(labels, ["L", "R"]);
let written = game.to_string();
let reparsed = ExtensiveFormGame::try_from_str(written.as_str()).unwrap();
assert_eq!(game, reparsed);
}
#[test]
fn handle_accessors() {
let game_str = r#"EFG 2 R "game" { "P1" "P2" } "the comment"
c "chance" 1 "ci" { "a" 1/2 "b" 1/2 } 5 "co" { 1 2 }
p "pl1" 1 1 "pi1" { "x" "y" } 6 "po1" { 3 4 }
t "ta" 1 "oa" { 7 8 }
t "tb" 2 "ob" { 9 10 }
p "pl2" 2 2 "pi2" { "x" "y" } 7 "po2" { 5 6 }
t "tc" 3 "oc" { 11 12 }
t "td" 4 "od" { 13 14 }
"#;
let game = ExtensiveFormGame::try_from_str(game_str).unwrap();
assert_eq!(game.comment().map(EscapedStr::escape), Some("the comment"));
let written = game.to_string();
let reparsed = ExtensiveFormGame::try_from_str(written.as_str()).unwrap();
assert_eq!(game, reparsed);
let Node::Chance(chance) = game.root() else {
panic!("expected a chance root");
};
assert_eq!(chance.name().escape(), "chance");
assert_eq!(chance.infoset(), 1);
assert_eq!(chance.infoset_name().escape(), "ci");
assert_eq!(chance.len(), 2);
assert_eq!(chance.outcome(), 5);
let chance_payoffs: Vec<_> = chance
.outcome_payoffs()
.unwrap()
.iter()
.map(ToString::to_string)
.collect();
assert_eq!(chance_payoffs, ["1", "2"]);
let chance_labels: Vec<_> = chance
.actions()
.map(|(label, _, _)| label.escape())
.collect();
assert_eq!(chance_labels, ["a", "b"]);
assert!(chance.action_at(0).is_some());
assert!(chance.action_at(2).is_none());
assert!(chance.action("none").is_none());
let (prob, first_child) = chance.action("a").unwrap();
assert_eq!(prob.to_string(), "1/2");
let Node::Player(player) = first_child else {
panic!("expected a player after chance action a");
};
assert_eq!(player.name().escape(), "pl1");
assert_eq!(player.player_num(), 1);
assert_eq!(player.infoset(), 1);
assert_eq!(player.infoset_name().escape(), "pi1");
assert_eq!(player.len(), 2);
assert_eq!(player.outcome(), 6);
assert_eq!(player.outcome_name().map(EscapedStr::escape), Some("po1"));
let player_payoffs: Vec<_> = player
.outcome_payoffs()
.unwrap()
.iter()
.map(ToString::to_string)
.collect();
assert_eq!(player_payoffs, ["3", "4"]);
let player_labels: Vec<_> = player.actions().map(|(label, _)| label.escape()).collect();
assert_eq!(player_labels, ["x", "y"]);
assert!(player.action("none").is_none());
assert!(player.action("y").is_some());
let (label, leaf) = player.action_at(0).unwrap();
assert_eq!(label.escape(), "x");
assert!(player.action_at(2).is_none());
let Node::Terminal(terminal) = leaf else {
panic!("expected a terminal after player action x");
};
assert_eq!(terminal.name().escape(), "ta");
assert_eq!(terminal.outcome(), 1);
assert_eq!(terminal.outcome_name().map(EscapedStr::escape), Some("oa"));
let terminal_payoffs: Vec<_> = terminal
.outcome_payoffs()
.unwrap()
.iter()
.map(ToString::to_string)
.collect();
assert_eq!(terminal_payoffs, ["7", "8"]);
}
#[test]
fn error_display() {
let parse_err = ExtensiveFormGame::try_from_str("not an efg").unwrap_err();
assert!(parse_err.to_string().starts_with("error parsing game at:"));
let bad = "EFG 2 R \"\" { \"1\" \"2\" }\np \"\" 3 1 \"a\" { \"x\" } 0\nt \"\" 1 { 0 0 }\n";
assert_eq!(
ExtensiveFormGame::try_from_str(bad)
.unwrap_err()
.to_string(),
"invalid efg: InvalidPlayerNum"
);
assert_eq!(
ValidationError::UndefinedOutcome.to_string(),
"UndefinedOutcome"
);
}
#[test]
fn accepts_d_data_type() {
let game = ExtensiveFormGame::try_from_str(
"EFG 2 D \"\" { \"1\" \"2\" }\nt \"\" 1 \"\" { 1 2 }\n",
)
.unwrap();
assert!(game.to_string().starts_with("EFG 2 R "));
}
#[test]
fn trailing_input_is_rejected() {
let game = r#"EFG 2 R "" { "1" "2" } t "" 1 "" { 1 2 } trailing"#;
assert!(matches!(
ExtensiveFormGame::try_from_str(game),
Err(Error::Parse("trailing"))
));
}
#[test]
fn rejects_overflowing_exponent() {
assert!(super::big_float("1e99999999999 ").is_err());
}
#[test]
fn rejects_huge_exponent() {
assert!(super::big_float("1e2000000000 ").is_err());
assert!(super::big_float("1e-2000000000 ").is_err());
assert!(super::big_float("1e100 ").is_ok());
}
#[test]
fn rejects_zero_denominator() {
assert!(super::big_rational("1/0 ").is_err());
assert!(
ExtensiveFormGame::try_from_str(
"EFG 2 R \"\" { \"1\" \"2\" }\nt \"\" 1 \"\" { 1/0 2 }\n"
)
.is_err()
);
}
#[test]
fn outcome_defined_then_referenced() {
let game = "EFG 2 R \"\" { \"1\" \"2\" }
p \"\" 1 1 \"i\" { \"x\" } 1 \"named\" { 3 4 }
t \"\" 1
";
assert!(ExtensiveFormGame::try_from_str(game).is_ok());
}
#[test]
fn chance_null_outcome_with_payoffs() {
assert_eq!(
validation_err(
"EFG 2 R \"\" { \"1\" \"2\" }
c \"\" 1 \"i\" { \"x\" 1 } 0 \"n\" { 1 2 }
t \"\" 1 { 0 0 }
"
),
ValidationError::NullOutcomePayoffs
);
}
#[test]
fn deep_tree_parses_and_drops() {
let depth = 200_000;
let mut game = String::with_capacity(depth * 24 + 64);
game.push_str("EFG 2 R \"\" { \"1\" \"2\" }\n");
for _ in 0..depth {
game.push_str("p \"\" 1 1 \"i\" { \"a\" } 0\n");
}
game.push_str("t \"\" 1 \"\" { 0 0 }\n");
let parsed = ExtensiveFormGame::try_from_str(&game).unwrap();
assert!(matches!(parsed.root(), Node::Player(_)));
drop(parsed);
}
#[test]
fn tolerates_flexible_whitespace() {
let game =
ExtensiveFormGame::try_from_str("EFG 2 R \"\" {\"1\" \"2\"}\nt \"\" 1 \"\" {1,2}\n")
.unwrap();
assert_eq!(game.player_names().len(), 2);
let Node::Terminal(root) = game.root() else {
panic!("expected a terminal root");
};
let payoffs: Vec<_> = root
.outcome_payoffs()
.unwrap()
.iter()
.map(BigRational::to_string)
.collect();
assert_eq!(payoffs, ["1", "2"]);
assert!(
ExtensiveFormGame::try_from_str(
"EFG 2 R \"\" { \"1\" \"2\" }\nt \"\" 1 \"\" { 1 , 2 }\n"
)
.is_ok()
);
}
#[test]
fn chance_outcome_name() {
let game_str = "EFG 2 R \"\" { \"1\" \"2\" }
c \"\" 1 \"i\" { \"a\" 1/2 \"b\" 1/2 } 1 \"oname\" { 3 4 }
t \"\" 1
t \"\" 1
";
let game = ExtensiveFormGame::try_from_str(game_str).unwrap();
let Node::Chance(root) = game.root() else {
panic!("expected a chance root");
};
assert_eq!(root.outcome_name().map(EscapedStr::escape), Some("oname"));
let written = game.to_string();
let reparsed = ExtensiveFormGame::try_from_str(written.as_str()).unwrap();
assert_eq!(game, reparsed);
}
#[test]
fn terminal_null_and_referenced_outcomes() {
let game_str = "EFG 2 R \"\" { \"1\" \"2\" }
p \"\" 1 1 \"i\" { \"L\" \"M\" \"R\" } 0
t \"a\" 0
t \"b\" 1 \"obname\" { 3 4 }
t \"c\" 1
";
let game = ExtensiveFormGame::try_from_str(game_str).unwrap();
let Node::Player(root) = game.root() else {
panic!("expected a player root");
};
let Some(Node::Terminal(null_term)) = root.action("L") else {
panic!("expected a terminal after action L");
};
assert_eq!(null_term.outcome(), 0);
assert!(null_term.outcome_payoffs().is_none());
assert!(null_term.outcome_name().is_none());
let Some(Node::Terminal(referenced)) = root.action("R") else {
panic!("expected a terminal after action R");
};
assert_eq!(referenced.outcome(), 1);
let payoffs: Vec<_> = referenced
.outcome_payoffs()
.unwrap()
.iter()
.map(BigRational::to_string)
.collect();
assert_eq!(payoffs, ["3", "4"]);
let written = game.to_string();
let reparsed = ExtensiveFormGame::try_from_str(written.as_str()).unwrap();
assert_eq!(game, reparsed);
}
#[test]
fn write_modes() {
let input = "EFG 2 R \"\" { \"1\" \"2\" }
p \"root\" 1 1 \"iset\" { \"L\" \"R\" } 0
p \"mid\" 1 1 \"iset\" { \"L\" \"R\" } 0
t \"a\" 1 \"out\" { 1 2 }
t \"b\" 1 \"out\" { 1 2 }
t \"c\" 1
";
let game = ExtensiveFormGame::try_from_str(input).unwrap();
assert_eq!(
game.to_string(),
game.display(WriteMode::Faithful).to_string()
);
let faithful = game.display(WriteMode::Faithful).to_string();
assert_eq!(ExtensiveFormGame::try_from_str(&faithful).unwrap(), game);
let canonical = game.display(WriteMode::Exhaustive).to_string();
for mode in [
WriteMode::Minimal,
WriteMode::Faithful,
WriteMode::Exhaustive,
] {
let out = game.display(mode).to_string();
let reparsed = ExtensiveFormGame::try_from_str(&out).unwrap();
assert_eq!(
reparsed.display(WriteMode::Exhaustive).to_string(),
canonical
);
}
let minimal = game.display(WriteMode::Minimal).to_string();
assert!(minimal.len() < faithful.len());
assert!(faithful.len() < canonical.len());
}
}