Skip to main content

gambit_parser/
lib.rs

1//! A library for parsing [gambit extensive form
2//! game](https://gambitproject.readthedocs.io/en/v16.0.2/formats.html) (`.efg`) files
3//!
4//! This library produces an [`ExtensiveFormGame`], which can then be easily used to model an
5//! extensive form game.
6//!
7//! In order to minimize memory consumption, this stores references to the underlying string where
8//! possible. One side effect is that this is a borrowed struct, and any quoted labels will still
9//! have escape sequences in them in the form of [`EscapedStr`]s.
10#![warn(missing_docs, clippy::pedantic)]
11
12mod unescaped;
13
14use nom::{
15    IResult, Parser,
16    branch::alt,
17    bytes::complete::tag,
18    character::complete::{char, digit0, digit1, multispace0, multispace1, none_of, one_of, u64},
19    combinator::{map, opt, recognize},
20    error::{ErrorKind, ParseError},
21    multi::{many0, separated_list1},
22    sequence::{delimited, pair, preceded, separated_pair},
23};
24use num_bigint::BigInt;
25use num_rational::BigRational;
26use num_traits::{One, Zero};
27use std::collections::HashMap;
28use std::collections::hash_map::Entry;
29use std::error::Error as StdError;
30use std::fmt::{Display, Error as FmtError, Formatter};
31pub use unescaped::{EscapedStr, Unescaped};
32
33/// A chance infoset's label paired with its ordered actions and their probabilities
34type ChanceInfoset<'a> = (&'a EscapedStr, Box<[(&'a EscapedStr, BigRational)]>);
35/// A player infoset's label paired with its ordered action labels
36type PlayerInfoset<'a> = (&'a EscapedStr, Box<[&'a EscapedStr]>);
37
38/// Every infoset seen while parsing, keyed by id. Player infosets are split per player (index =
39/// `player_num - 1`); chance is its own namespace. Each entry holds the infoset's label and ordered
40/// actions, which the tree nodes reference by id.
41#[derive(Debug, PartialEq, Clone)]
42struct Infosets<'a> {
43    player: Box<[HashMap<u64, PlayerInfoset<'a>>]>,
44    chance: HashMap<u64, ChanceInfoset<'a>>,
45}
46
47/// A node in the raw, id-referenced game tree. Infoset payloads live on the game, not here.
48#[derive(Debug, PartialEq, Clone)]
49enum RawNode<'a> {
50    Chance(RawChance<'a>),
51    Player(RawPlayer<'a>),
52    Terminal(RawTerminal<'a>),
53}
54
55#[derive(Debug, PartialEq, Clone)]
56struct RawChance<'a> {
57    name: &'a EscapedStr,
58    infoset: u64,
59    // did THIS node write the infoset block, or inherit it by omission?
60    declared: bool,
61    children: Box<[RawNode<'a>]>,
62    outcome: u64,
63    outcome_payoffs: Option<Box<[BigRational]>>,
64}
65
66#[derive(Debug, PartialEq, Clone)]
67struct RawPlayer<'a> {
68    name: &'a EscapedStr,
69    player_num: usize,
70    infoset: u64,
71    // did THIS node write the infoset block, or inherit it by omission?
72    declared: bool,
73    children: Box<[RawNode<'a>]>,
74    outcome: u64,
75    outcome_name: Option<&'a EscapedStr>,
76    outcome_payoffs: Option<Box<[BigRational]>>,
77}
78
79#[derive(Debug, PartialEq, Eq, Clone)]
80struct RawTerminal<'a> {
81    name: &'a EscapedStr,
82    outcome: u64,
83    outcome_name: Option<&'a EscapedStr>,
84    outcome_payoffs: Box<[BigRational]>,
85}
86
87/// A full extensive form game
88///
89/// This can be parsed from a [str] reference using [`ExtensiveFormGame::try_from_str`] or using the
90/// [`TryFrom`] / [`TryInto`] traits. It implements [Display] for formatting.
91///
92/// # Example
93///
94/// ```
95/// # use gambit_parser::ExtensiveFormGame;
96/// let gambit = r#"EFG 2 R "" { "1" "2" } t "" 1 { 1 2 }"#;
97/// let game: ExtensiveFormGame<'_> = gambit.try_into().unwrap();
98/// let output = game.to_string();
99/// ```
100#[derive(Debug, PartialEq, Clone)]
101pub struct ExtensiveFormGame<'a> {
102    name: &'a EscapedStr,
103    player_names: Box<[&'a EscapedStr]>,
104    comment: Option<&'a EscapedStr>,
105    infosets: Infosets<'a>,
106    root: RawNode<'a>,
107}
108
109impl<'a> ExtensiveFormGame<'a> {
110    /// The name of the game
111    #[must_use]
112    pub fn name(&self) -> &'a EscapedStr {
113        self.name
114    }
115
116    /// Names for every player, in order
117    #[must_use]
118    pub fn player_names(&self) -> &[&'a EscapedStr] {
119        &self.player_names
120    }
121
122    /// An optional game comment
123    #[must_use]
124    pub fn comment(&self) -> Option<&'a EscapedStr> {
125        self.comment
126    }
127
128    /// The root node of the game tree
129    #[must_use]
130    pub fn root<'g>(&'g self) -> Node<'a, 'g> {
131        self.wrap(&self.root)
132    }
133
134    fn wrap<'g>(&'g self, raw: &'g RawNode<'a>) -> Node<'a, 'g> {
135        match raw {
136            RawNode::Chance(raw) => Node::Chance(Chance { game: self, raw }),
137            RawNode::Player(raw) => Node::Player(Player { game: self, raw }),
138            RawNode::Terminal(raw) => Node::Terminal(Terminal { raw }),
139        }
140    }
141}
142
143impl Display for ExtensiveFormGame<'_> {
144    fn fmt(&self, out: &mut Formatter<'_>) -> Result<(), FmtError> {
145        write!(out, "EFG 2 R \"{}\" {{ ", self.name.escape())?;
146        for name in &self.player_names {
147            write!(out, "\"{}\" ", name.escape())?;
148        }
149        writeln!(out, "}}")?;
150        if let Some(comment) = self.comment {
151            writeln!(out, "\"{}\"", comment.escape())?;
152        }
153        writeln!(out, "{}", self.root())
154    }
155}
156
157/// An error that happens while trying to turn a string into an [`ExtensiveFormGame`]
158#[derive(Debug)]
159#[non_exhaustive]
160pub enum Error<'a> {
161    /// A problem with parsing
162    ///
163    /// This will show the remainder of the string where the parse error occurred
164    Parse(&'a str),
165    /// A problem validating the tree after parsing
166    Validation(ValidationError),
167}
168
169impl Display for Error<'_> {
170    fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), FmtError> {
171        match self {
172            Error::Parse(rem) => write!(fmt, "error parsing game at: '{rem}'"),
173            Error::Validation(err) => write!(fmt, "invalid efg: {err}"),
174        }
175    }
176}
177
178impl StdError for Error<'_> {}
179
180/// An error that results from something invalid about the parsed extensive form game
181#[derive(Debug, PartialEq, Eq)]
182#[non_exhaustive]
183pub enum ValidationError {
184    /// The probabilities of actions associated with a chance node don't sum to one
185    ChanceNotDistribution,
186    /// A player's number wasn't between one and the number of players
187    InvalidPlayerNum,
188    /// An infoset had different names attached to it
189    NonMatchingInfosetNames,
190    /// An infoset had different sets of associated actions
191    NonMatchingInfosetActions,
192    /// There was payoff data associated with the null (0) outcome
193    NullOutcomePayoffs,
194    /// The number of specified payoffs did not match the number of players
195    InvalidNumberOfPayoffs,
196    /// An outcome had different names attached to it
197    NonMatchingOutcomeNames,
198    /// An outcome had different associated payoffs
199    NonMatchingOutcomePayoffs,
200    /// An outcome was defined without payoffs
201    NoOutcomePayoffs,
202    /// A node omitted its action list for an infoset that was never declared
203    UndeclaredInfoset,
204}
205
206impl Display for ValidationError {
207    fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), FmtError> {
208        write!(fmt, "{self:?}")
209    }
210}
211
212impl From<ValidationError> for Error<'_> {
213    fn from(err: ValidationError) -> Self {
214        Error::Validation(err)
215    }
216}
217
218impl<'a> From<nom::Err<nom::error::Error<&'a str>>> for Error<'a> {
219    fn from(err: nom::Err<nom::error::Error<&'a str>>) -> Self {
220        match err {
221            nom::Err::Incomplete(_) => panic!("internal error: incomplete parsing"),
222            nom::Err::Error(err) | nom::Err::Failure(err) => Error::Parse(err.input),
223        }
224    }
225}
226
227impl<'a> ExtensiveFormGame<'a> {
228    /// Try to parse a game from a string
229    ///
230    /// This is identical to `ExtensiveFormGame::try_from` or `"...".try_into()`.
231    ///
232    /// # Errors
233    ///
234    /// Returns an [`Error`] if the input isn't a syntactically valid and self-consistent game.
235    pub fn try_from_str(input: &'a str) -> Result<Self, Error<'a>> {
236        let (rest, game) = parse_game(input)?;
237        let rest = rest.trim_start();
238        if !rest.is_empty() {
239            return Err(Error::Parse(rest));
240        }
241        game.validate()?;
242        Ok(game)
243    }
244
245    /// Infoset consistency (matching names and actions across a shared id) and player number ranges
246    /// are enforced while parsing, so this only covers what the tree shape can't: chance
247    /// distributions and outcome agreement.
248    fn validate(&self) -> Result<(), ValidationError> {
249        for (_, actions) in self.infosets.chance.values() {
250            let total: BigRational = actions.iter().map(|(_, prob)| prob).sum();
251            if total != BigRational::one() {
252                return Err(ValidationError::ChanceNotDistribution);
253            }
254        }
255
256        let mut outcomes = HashMap::new();
257        let mut queue = vec![&self.root];
258        while let Some(node) = queue.pop() {
259            match node {
260                RawNode::Chance(chance) => {
261                    self.validate_outcome(
262                        chance.outcome,
263                        None,
264                        chance.outcome_payoffs.as_deref(),
265                        &mut outcomes,
266                    )?;
267                    queue.extend(chance.children.iter());
268                }
269                RawNode::Player(player) => {
270                    self.validate_outcome(
271                        player.outcome,
272                        player.outcome_name,
273                        player.outcome_payoffs.as_deref(),
274                        &mut outcomes,
275                    )?;
276                    queue.extend(player.children.iter());
277                }
278                RawNode::Terminal(term) => {
279                    self.validate_outcome(
280                        term.outcome,
281                        term.outcome_name,
282                        Some(&term.outcome_payoffs),
283                        &mut outcomes,
284                    )?;
285                }
286            }
287        }
288
289        for (_, (_, pays)) in outcomes {
290            if pays.is_none() {
291                return Err(ValidationError::NoOutcomePayoffs);
292            }
293        }
294
295        Ok(())
296    }
297
298    fn validate_outcome<'b>(
299        &self,
300        outcome: u64,
301        outcome_name: Option<&'a EscapedStr>,
302        outcome_payoffs: Option<&'b [BigRational]>,
303        outcomes: &mut HashMap<u64, (Option<&'a EscapedStr>, Option<&'b [BigRational]>)>,
304    ) -> Result<(), ValidationError> {
305        if outcome == 0 {
306            if outcome_payoffs.is_some() {
307                return Err(ValidationError::NullOutcomePayoffs);
308            }
309        } else {
310            match outcomes.entry(outcome) {
311                Entry::Vacant(ent) => match outcome_payoffs {
312                    Some(pays) => {
313                        if pays.len() == self.player_names.len() {
314                            ent.insert((outcome_name, Some(pays)));
315                        } else {
316                            return Err(ValidationError::InvalidNumberOfPayoffs);
317                        }
318                    }
319                    None => {
320                        ent.insert((outcome_name, None));
321                    }
322                },
323                Entry::Occupied(mut ent) => {
324                    let (name, payoffs) = ent.get_mut();
325                    match (name, outcome_name) {
326                        (Some(old), Some(new)) if old != &new => {
327                            return Err(ValidationError::NonMatchingOutcomeNames);
328                        }
329                        (old @ None, Some(new)) => {
330                            *old = Some(new);
331                        }
332                        _ => (),
333                    }
334                    match (payoffs, outcome_payoffs) {
335                        (Some(old), Some(new)) if old != &new => {
336                            return Err(ValidationError::NonMatchingOutcomePayoffs);
337                        }
338                        (old @ None, Some(new)) => {
339                            *old = Some(new);
340                        }
341                        _ => (),
342                    }
343                }
344            }
345        }
346        Ok(())
347    }
348}
349
350impl<'a> TryFrom<&'a str> for ExtensiveFormGame<'a> {
351    type Error = Error<'a>;
352
353    fn try_from(input: &'a str) -> Result<Self, Self::Error> {
354        Self::try_from_str(input)
355    }
356}
357
358/// An arbitrary node in the game tree
359///
360/// A handle that pairs a node with its game so it can resolve its infoset. These are cheap to copy.
361#[derive(Clone, Copy)]
362pub enum Node<'a, 'g> {
363    /// A chance node
364    Chance(Chance<'a, 'g>),
365    /// A player node
366    Player(Player<'a, 'g>),
367    /// A terminal node
368    Terminal(Terminal<'a, 'g>),
369}
370
371impl Display for Node<'_, '_> {
372    fn fmt(&self, out: &mut Formatter<'_>) -> Result<(), FmtError> {
373        let mut queue = vec![*self];
374        while let Some(node) = queue.pop() {
375            match node {
376                Node::Chance(chance) => {
377                    queue.extend(
378                        chance
379                            .raw
380                            .children
381                            .iter()
382                            .rev()
383                            .map(|c| chance.game.wrap(c)),
384                    );
385                    write!(out, "\nc {chance}")?;
386                }
387                Node::Player(player) => {
388                    queue.extend(
389                        player
390                            .raw
391                            .children
392                            .iter()
393                            .rev()
394                            .map(|c| player.game.wrap(c)),
395                    );
396                    write!(out, "\np {player}")?;
397                }
398                Node::Terminal(terminal) => write!(out, "\nt {terminal}")?,
399            }
400        }
401        Ok(())
402    }
403}
404
405/// A chance node
406///
407/// A chance node represents a point in the game where things advance randomly, or alternatively,
408/// where "nature" takes a turn.
409#[derive(Clone, Copy)]
410pub struct Chance<'a, 'g> {
411    game: &'g ExtensiveFormGame<'a>,
412    raw: &'g RawChance<'a>,
413}
414
415impl<'a, 'g> Chance<'a, 'g> {
416    fn entry(self) -> &'g ChanceInfoset<'a> {
417        &self.game.infosets.chance[&self.raw.infoset]
418    }
419
420    /// The name of the node
421    #[must_use]
422    pub fn name(self) -> &'a EscapedStr {
423        self.raw.name
424    }
425
426    /// The id of the node's infoset
427    #[must_use]
428    pub fn infoset(self) -> u64 {
429        self.raw.infoset
430    }
431
432    /// The infoset's label
433    #[must_use]
434    pub fn infoset_name(self) -> &'a EscapedStr {
435        self.entry().0
436    }
437
438    /// All possible actions with their names, probabilities, and resulting nodes
439    pub fn actions(
440        self,
441    ) -> impl Iterator<Item = (&'a EscapedStr, &'g BigRational, Node<'a, 'g>)> + 'g {
442        let (_, actions) = self.entry();
443        let game = self.game;
444        actions
445            .iter()
446            .zip(self.raw.children.iter())
447            .map(move |((label, prob), child)| (*label, prob, game.wrap(child)))
448    }
449
450    /// The probability and child for the action with the given label
451    ///
452    /// Labels need not be unique within an infoset, so this returns the first match.
453    #[must_use]
454    pub fn action(self, label: &EscapedStr) -> Option<(&'g BigRational, Node<'a, 'g>)> {
455        self.actions()
456            .find(|(name, _, _)| *name == label)
457            .map(|(_, prob, next)| (prob, next))
458    }
459
460    /// The number of actions (always at least one)
461    #[allow(clippy::len_without_is_empty)]
462    #[must_use]
463    pub fn len(self) -> usize {
464        self.raw.children.len()
465    }
466
467    /// The name, probability, and child for the action at the given index
468    #[must_use]
469    pub fn action_at(
470        self,
471        index: usize,
472    ) -> Option<(&'a EscapedStr, &'g BigRational, Node<'a, 'g>)> {
473        let (_, actions) = self.entry();
474        let (label, prob) = actions.get(index)?;
475        Some((*label, prob, self.game.wrap(self.raw.children.get(index)?)))
476    }
477
478    /// The outcome id
479    #[must_use]
480    pub fn outcome(self) -> u64 {
481        self.raw.outcome
482    }
483
484    /// Outcome payoffs for this node
485    ///
486    /// Outcome payoffs are added to every players' payoffs for traversing through this node. Note
487    /// that if these are missing, they be defined at another node sharing the same outcome.
488    #[must_use]
489    pub fn outcome_payoffs(self) -> Option<&'g [BigRational]> {
490        self.raw.outcome_payoffs.as_deref()
491    }
492}
493
494impl Display for Chance<'_, '_> {
495    fn fmt(&self, out: &mut Formatter<'_>) -> Result<(), FmtError> {
496        write!(out, "\"{}\" {}", self.raw.name.escape(), self.raw.infoset)?;
497        // the label and action list are written together, or both omitted, matching the file
498        if self.raw.declared {
499            let (label, actions) = self.entry();
500            write!(out, " \"{}\" {{ ", label.escape())?;
501            for (action, prob) in actions {
502                write!(out, "\"{}\" {} ", action.escape(), prob)?;
503            }
504            write!(out, "}}")?;
505        }
506        write!(out, " {}", self.raw.outcome)?;
507        if let Some(payoffs) = &self.raw.outcome_payoffs {
508            write!(out, " {{ ")?;
509            for payoff in payoffs {
510                write!(out, "{payoff} ")?;
511            }
512            write!(out, "}}")?;
513        }
514        Ok(())
515    }
516}
517
518/// A player node in the game tree
519///
520/// A player node represents a place where one of the players chooses what happens next.
521#[derive(Clone, Copy)]
522pub struct Player<'a, 'g> {
523    game: &'g ExtensiveFormGame<'a>,
524    raw: &'g RawPlayer<'a>,
525}
526
527impl<'a, 'g> Player<'a, 'g> {
528    fn entry(self) -> &'g PlayerInfoset<'a> {
529        &self.game.infosets.player[self.raw.player_num - 1][&self.raw.infoset]
530    }
531
532    /// The name of the node
533    #[must_use]
534    pub fn name(self) -> &'a EscapedStr {
535        self.raw.name
536    }
537
538    /// The player acting at this node
539    ///
540    /// This will always be between 1 and the number of players.
541    #[must_use]
542    pub fn player_num(self) -> usize {
543        self.raw.player_num
544    }
545
546    /// The infoset id for this node and player
547    #[must_use]
548    pub fn infoset(self) -> u64 {
549        self.raw.infoset
550    }
551
552    /// The infoset's label
553    #[must_use]
554    pub fn infoset_name(self) -> &'a EscapedStr {
555        self.entry().0
556    }
557
558    /// All the actions a player can take with their names and resulting nodes
559    pub fn actions(self) -> impl Iterator<Item = (&'a EscapedStr, Node<'a, 'g>)> + 'g {
560        let (_, labels) = self.entry();
561        let game = self.game;
562        labels
563            .iter()
564            .zip(self.raw.children.iter())
565            .map(move |(label, child)| (*label, game.wrap(child)))
566    }
567
568    /// The child reached by the action with the given label
569    ///
570    /// Labels need not be unique within an infoset, so this returns the first match.
571    #[must_use]
572    pub fn action(self, label: &EscapedStr) -> Option<Node<'a, 'g>> {
573        self.actions()
574            .find(|(name, _)| *name == label)
575            .map(|(_, next)| next)
576    }
577
578    /// The number of actions (always at least one)
579    #[allow(clippy::len_without_is_empty)]
580    #[must_use]
581    pub fn len(self) -> usize {
582        self.raw.children.len()
583    }
584
585    /// The name and child for the action at the given index
586    #[must_use]
587    pub fn action_at(self, index: usize) -> Option<(&'a EscapedStr, Node<'a, 'g>)> {
588        let (_, actions) = self.entry();
589        let &label = actions.get(index)?;
590        Some((label, self.game.wrap(self.raw.children.get(index)?)))
591    }
592
593    /// The outcome id
594    #[must_use]
595    pub fn outcome(self) -> u64 {
596        self.raw.outcome
597    }
598
599    /// The name of the outcome
600    ///
601    /// If omitted it may still be defined on another node.
602    #[must_use]
603    pub fn outcome_name(self) -> Option<&'a EscapedStr> {
604        self.raw.outcome_name
605    }
606
607    /// Payoffs associated with the outcome
608    ///
609    /// If omitted they may be defined on another node.
610    #[must_use]
611    pub fn outcome_payoffs(self) -> Option<&'g [BigRational]> {
612        self.raw.outcome_payoffs.as_deref()
613    }
614}
615
616impl Display for Player<'_, '_> {
617    fn fmt(&self, out: &mut Formatter<'_>) -> Result<(), FmtError> {
618        write!(
619            out,
620            "\"{}\" {} {}",
621            self.raw.name.escape(),
622            self.raw.player_num,
623            self.raw.infoset
624        )?;
625        // the label and action list are written together, or both omitted, matching the file
626        if self.raw.declared {
627            let (label, actions) = self.entry();
628            write!(out, " \"{}\" {{ ", label.escape())?;
629            for action in actions {
630                write!(out, "\"{}\" ", action.escape())?;
631            }
632            write!(out, "}}")?;
633        }
634        write!(out, " {}", self.raw.outcome)?;
635        if let Some(name) = self.raw.outcome_name {
636            write!(out, " \"{}\"", name.escape())?;
637        }
638        if let Some(payoffs) = &self.raw.outcome_payoffs {
639            write!(out, " {{ ")?;
640            for payoff in payoffs {
641                write!(out, "{payoff} ")?;
642            }
643            write!(out, "}}")?;
644        }
645        Ok(())
646    }
647}
648
649/// A terminal node represents the end of a game
650///
651/// Terminal nodes simply assign payoffs to every player in the game
652#[derive(Clone, Copy)]
653pub struct Terminal<'a, 'g> {
654    raw: &'g RawTerminal<'a>,
655}
656
657impl<'a, 'g> Terminal<'a, 'g> {
658    /// The name of this node
659    #[must_use]
660    pub fn name(self) -> &'a EscapedStr {
661        self.raw.name
662    }
663
664    /// The outcome id
665    #[must_use]
666    pub fn outcome(self) -> u64 {
667        self.raw.outcome
668    }
669
670    /// The name of this outcome
671    ///
672    /// Note that if omitted it may be specified on a different node with the same outcome.
673    #[must_use]
674    pub fn outcome_name(self) -> Option<&'a EscapedStr> {
675        self.raw.outcome_name
676    }
677
678    /// The payoffs to every player
679    #[must_use]
680    pub fn outcome_payoffs(self) -> &'g [BigRational] {
681        &self.raw.outcome_payoffs
682    }
683}
684
685impl Display for Terminal<'_, '_> {
686    fn fmt(&self, out: &mut Formatter<'_>) -> Result<(), FmtError> {
687        write!(out, "\"{}\" {}", self.raw.name.escape(), self.raw.outcome)?;
688        if let Some(name) = self.raw.outcome_name {
689            write!(out, " \"{}\"", name.escape())?;
690        }
691        write!(out, " {{ ")?;
692        for payoff in &self.raw.outcome_payoffs {
693            write!(out, "{payoff} ")?;
694        }
695        write!(out, "}}")
696    }
697}
698
699fn negate(input: &str) -> IResult<&str, bool> {
700    let (input, res) = opt(one_of("+-")).parse(input)?;
701    Ok((input, res == Some('-')))
702}
703
704fn fail(input: &str) -> nom::Err<nom::error::Error<&str>> {
705    nom::Err::Error(nom::error::Error::new(input, ErrorKind::Fail))
706}
707
708fn big_float(input: &str) -> IResult<&str, BigRational> {
709    let (res_input, (main_neg, (int, dec), exp)) = (
710        negate,
711        alt((
712            pair(
713                digit1,
714                map(opt(preceded(char('.'), digit0)), Option::unwrap_or_default),
715            ),
716            separated_pair(digit0, char('.'), digit1),
717        )),
718        opt(preceded(one_of("eE"), pair(negate, digit1))),
719    )
720        .parse(input)?;
721    let mut res = if int.is_empty() {
722        BigRational::zero()
723    } else {
724        BigRational::from_integer(int.parse().unwrap())
725    };
726    if !dec.is_empty() {
727        let pow: u32 = dec.len().try_into().map_err(|_| fail(input))?;
728        res += BigRational::new(dec.parse().unwrap(), BigInt::from(10).pow(pow));
729    }
730    if let Some((neg, exp)) = exp {
731        let exp: i32 = exp.parse().map_err(|_| fail(input))?;
732        res *= BigRational::from_integer(10.into()).pow(if neg { -exp } else { exp });
733    }
734    if main_neg {
735        res = -res;
736    }
737    Ok((res_input, res))
738}
739
740fn big_rational(input: &str) -> IResult<&str, BigRational> {
741    let (input, (num, denom)) =
742        pair(big_float, opt(preceded(char('/'), big_float))).parse(input)?;
743    Ok((
744        input,
745        match denom {
746            Some(denom) => num / denom,
747            None => num,
748        },
749    ))
750}
751
752fn label(input: &str) -> IResult<&str, &EscapedStr> {
753    map(
754        delimited(
755            char('"'),
756            // the body is any mix of the `\"` escape and ordinary non-quote characters; matching
757            // `\"` first means a lone `\` is ordinary and only a `"` after a `\` is escaped
758            recognize(many0(alt((tag(r#"\""#), recognize(none_of("\"")))))),
759            char('"'),
760        ),
761        EscapedStr::new,
762    )
763    .parse(input)
764}
765
766fn spacelist<'a, O, E, F>(f: F) -> impl Parser<&'a str, Output = Vec<O>, Error = E>
767where
768    F: Parser<&'a str, Output = O, Error = E>,
769    E: ParseError<&'a str>,
770{
771    delimited(
772        pair(char('{'), multispace1),
773        separated_list1(multispace1, f),
774        pair(multispace1, char('}')),
775    )
776}
777
778fn commalist<'a, O, E, F>(f: F) -> impl Parser<&'a str, Output = Vec<O>, Error = E>
779where
780    F: Parser<&'a str, Output = O, Error = E>,
781    E: ParseError<&'a str>,
782{
783    delimited(
784        pair(char('{'), multispace1),
785        separated_list1(pair(opt(char(',')), multispace1), f),
786        pair(multispace1, char('}')),
787    )
788}
789
790/// Parse `count` child nodes in sequence (they follow a node, one per action)
791fn parse_children<'a>(
792    mut input: &'a str,
793    count: usize,
794    infosets: &mut Infosets<'a>,
795) -> Result<(&'a str, Box<[RawNode<'a>]>), Error<'a>> {
796    let mut children = Vec::with_capacity(count);
797    for _ in 0..count {
798        let (rest, next) = parse_node(input, infosets)?;
799        input = rest;
800        children.push(next);
801    }
802    Ok((input, children.into()))
803}
804
805/// Record or check an infoset declaration, returning whether the block was written here and the
806/// action count. A first declaration is inserted, a repeat must match exactly, an omission inherits.
807fn resolve_infoset<'a, A: PartialEq>(
808    map: &mut HashMap<u64, (&'a EscapedStr, Box<[A]>)>,
809    infoset: u64,
810    declared: Option<(&'a EscapedStr, Vec<A>)>,
811) -> Result<(bool, usize), Error<'a>> {
812    if let Some((name, actions)) = declared {
813        match map.entry(infoset) {
814            // a first declaration is stored (boxed), a repeat is only compared against the stored
815            // one, so the parsed `Vec` is never boxed just to be dropped
816            Entry::Vacant(ent) => {
817                let count = actions.len();
818                ent.insert((name, actions.into()));
819                Ok((true, count))
820            }
821            Entry::Occupied(ent) => {
822                let (stored_name, stored_actions) = ent.get();
823                if *stored_name != name {
824                    Err(ValidationError::NonMatchingInfosetNames.into())
825                } else if **stored_actions != *actions {
826                    Err(ValidationError::NonMatchingInfosetActions.into())
827                } else {
828                    Ok((true, actions.len()))
829                }
830            }
831        }
832    } else {
833        let (_, actions) = map
834            .get(&infoset)
835            .ok_or(ValidationError::UndeclaredInfoset)?;
836        Ok((false, actions.len()))
837    }
838}
839
840fn parse_node<'a>(
841    input: &'a str,
842    infosets: &mut Infosets<'a>,
843) -> Result<(&'a str, RawNode<'a>), Error<'a>> {
844    let (input, style) = preceded(multispace1, one_of("cpt")).parse(input)?;
845    match style {
846        'c' => {
847            let (input, chance) = parse_chance(input, infosets)?;
848            Ok((input, RawNode::Chance(chance)))
849        }
850        'p' => {
851            let (input, player) = parse_player(input, infosets)?;
852            Ok((input, RawNode::Player(player)))
853        }
854        't' => {
855            let (input, term) = parse_terminal(input)?;
856            Ok((input, RawNode::Terminal(term)))
857        }
858        // `one_of("cpt")` only ever yields one of these three characters
859        _ => unreachable!(),
860    }
861}
862
863fn parse_chance<'a>(
864    input: &'a str,
865    infosets: &mut Infosets<'a>,
866) -> Result<(&'a str, RawChance<'a>), Error<'a>> {
867    let (input, (name, infoset, declared, outcome, outcome_payoffs)) = (
868        preceded(multispace1, label),
869        preceded(multispace1, u64),
870        opt((
871            preceded(multispace1, label),
872            preceded(
873                multispace1,
874                spacelist(separated_pair(label, multispace1, big_rational)),
875            ),
876        )),
877        preceded(multispace1, u64),
878        opt(preceded(multispace1, commalist(big_rational))),
879    )
880        .parse(input)?;
881    let (declared, child_count) = resolve_infoset(&mut infosets.chance, infoset, declared)?;
882    let (input, children) = parse_children(input, child_count, infosets)?;
883    Ok((
884        input,
885        RawChance {
886            name,
887            infoset,
888            declared,
889            children,
890            outcome,
891            outcome_payoffs: outcome_payoffs.map(Into::into),
892        },
893    ))
894}
895
896fn parse_player<'a>(
897    input: &'a str,
898    infosets: &mut Infosets<'a>,
899) -> Result<(&'a str, RawPlayer<'a>), Error<'a>> {
900    let (input, (name, player_num, infoset, declared, outcome, outcome_name, outcome_payoffs)) = (
901        preceded(multispace1, label),
902        preceded(multispace1, u64),
903        preceded(multispace1, u64),
904        opt((
905            preceded(multispace1, label),
906            preceded(multispace1, spacelist(label)),
907        )),
908        preceded(multispace1, u64),
909        opt(preceded(multispace1, label)),
910        opt(preceded(multispace1, commalist(big_rational))),
911    )
912        .parse(input)?;
913    let player_num: usize = player_num.try_into().map_err(|_| fail(input))?;
914    // checked here, since the per-player infoset map is indexed by it
915    if player_num == 0 || player_num > infosets.player.len() {
916        return Err(ValidationError::InvalidPlayerNum.into());
917    }
918    let (declared, child_count) =
919        resolve_infoset(&mut infosets.player[player_num - 1], infoset, declared)?;
920    let (input, children) = parse_children(input, child_count, infosets)?;
921    Ok((
922        input,
923        RawPlayer {
924            name,
925            player_num,
926            infoset,
927            declared,
928            children,
929            outcome,
930            outcome_name,
931            outcome_payoffs: outcome_payoffs.map(Into::into),
932        },
933    ))
934}
935
936fn parse_terminal(input: &str) -> IResult<&str, RawTerminal<'_>> {
937    let (input, (name, outcome, outcome_name, payoffs)) = (
938        preceded(multispace1, label),
939        preceded(multispace1, u64),
940        opt(preceded(multispace1, label)),
941        preceded(multispace1, commalist(big_rational)),
942    )
943        .parse(input)?;
944    Ok((
945        input,
946        RawTerminal {
947            name,
948            outcome,
949            outcome_name,
950            outcome_payoffs: payoffs.into(),
951        },
952    ))
953}
954
955fn parse_game(input: &str) -> Result<(&str, ExtensiveFormGame<'_>), Error<'_>> {
956    let (input, (name, player_names, comment)) = (
957        preceded(
958            (
959                multispace0,
960                tag("EFG"),
961                multispace1,
962                tag("2"),
963                multispace1,
964                tag("R"),
965                multispace1,
966            ),
967            label,
968        ),
969        preceded(multispace1, spacelist(label)),
970        opt(preceded(multispace1, label)),
971    )
972        .parse(input)?;
973    let num_players = player_names.len();
974    let mut infosets = Infosets {
975        player: (0..num_players).map(|_| HashMap::new()).collect(),
976        chance: HashMap::new(),
977    };
978    let (input, root) = parse_node(input, &mut infosets)?;
979    Ok((
980        input,
981        ExtensiveFormGame {
982            name,
983            player_names: player_names.into(),
984            comment,
985            infosets,
986            root,
987        },
988    ))
989}
990
991#[cfg(test)]
992mod tests {
993    use super::{Error, EscapedStr, ExtensiveFormGame, Node, ValidationError};
994    use num_rational::BigRational;
995    use num_traits::One;
996
997    /// Parse a game expected to fail validation (or a parse-time infoset check) and return the error
998    fn validation_err(game: &str) -> ValidationError {
999        match ExtensiveFormGame::try_from_str(game) {
1000            Err(Error::Validation(err)) => err,
1001            other => panic!("expected a validation error, got {other:?}"),
1002        }
1003    }
1004
1005    #[test]
1006    fn test_big_float() {
1007        let (input, num) = super::big_float("3 ").unwrap();
1008        assert_eq!(input, " ");
1009        assert_eq!(num, BigRational::from_integer(3.into()));
1010
1011        let (input, num) = super::big_float("-2. ").unwrap();
1012        assert_eq!(input, " ");
1013        assert_eq!(num, BigRational::from_integer((-2).into()));
1014
1015        let (input, num) = super::big_float("+.56 ").unwrap();
1016        assert_eq!(input, " ");
1017        assert_eq!(num, BigRational::new(56.into(), 100.into()));
1018
1019        let (input, num) = super::big_float("3.14e-1 ").unwrap();
1020        assert_eq!(input, " ");
1021        assert_eq!(num, BigRational::new(314.into(), 1000.into()));
1022    }
1023
1024    #[test]
1025    fn test_big_rational() {
1026        let (input, num) = super::big_rational("3 ").unwrap();
1027        assert_eq!(input, " ");
1028        assert_eq!(num, BigRational::from_integer(3.into()));
1029
1030        let (input, num) = super::big_rational("99/100 ").unwrap();
1031        assert_eq!(input, " ");
1032        assert_eq!(num, BigRational::new(99.into(), 100.into()));
1033
1034        let (input, num) = super::big_rational(".1e3/+1.e2 ").unwrap();
1035        assert_eq!(input, " ");
1036        assert_eq!(num, BigRational::one());
1037    }
1038
1039    #[test]
1040    fn test_label() {
1041        let (input, label) = super::label(r#""" "#).unwrap();
1042        assert_eq!(input, " ");
1043        assert_eq!(label.escape(), "");
1044
1045        let (input, label) = super::label(r#""normal" "#).unwrap();
1046        assert_eq!(input, " ");
1047        assert_eq!(label.escape(), "normal");
1048
1049        // `\"` is an escaped quote and does not close the label
1050        let (input, label) = super::label(r#""esca\"ped" "#).unwrap();
1051        assert_eq!(input, " ");
1052        assert_eq!(label.escape(), r#"esca\"ped"#);
1053
1054        // a backslash before a non-quote is kept; the final `"` (preceded by `h`) closes the label
1055        let (input, label) = super::label(r#""back\slash" "#).unwrap();
1056        assert_eq!(input, " ");
1057        assert_eq!(label.escape(), r"back\slash");
1058
1059        // a `\` always escapes the immediately following `"`, so a label whose closing quote is
1060        // preceded by a backslash is unterminated (matching gambit)
1061        assert!(super::label(r#""pair\\" "#).is_err());
1062        assert!(super::label(r#""unterminated"#).is_err());
1063        assert!(super::label("noquote").is_err());
1064    }
1065
1066    #[test]
1067    fn simple_test() {
1068        let game_str = r#"
1069        EFG 2 R "General Bayes game, one stage" { "Player 1" "Player 2" }
1070        "A single stage General Bayes Game"
1071
1072        c "ROOT" 1 "(0,1)" { "1G" 0.500000 "1B" 0.500000 } 0
1073        p "" 1 1 "(1,1)" { "H" "L" } 0
1074        t "" 1 "Outcome 1" { 10.000000 2.000000 }
1075        t "" 2 "Outcome 2" { 0.000000 10.000000 }
1076        p "" 2 1 "(2,1)" { "h" "l" } 0
1077        t "" 3 "Outcome 3" { 2.000000 4.000000 }
1078        t "" 4 "Outcome 4" { 4.000000 0.000000 }
1079        "#;
1080        let game = ExtensiveFormGame::try_from_str(game_str).unwrap();
1081        assert_eq!(
1082            game.to_string(),
1083            r#"EFG 2 R "General Bayes game, one stage" { "Player 1" "Player 2" }
1084"A single stage General Bayes Game"
1085
1086c "ROOT" 1 "(0,1)" { "1G" 1/2 "1B" 1/2 } 0
1087p "" 1 1 "(1,1)" { "H" "L" } 0
1088t "" 1 "Outcome 1" { 10 2 }
1089t "" 2 "Outcome 2" { 0 10 }
1090p "" 2 1 "(2,1)" { "h" "l" } 0
1091t "" 3 "Outcome 3" { 2 4 }
1092t "" 4 "Outcome 4" { 4 0 }
1093"#
1094        );
1095
1096        // spot-check a few handle accessors
1097        assert_eq!(game.name().to_string(), "General Bayes game, one stage");
1098        assert_eq!(game.player_names().len(), 2);
1099        let Node::Chance(root) = game.root() else {
1100            panic!("expected a chance root");
1101        };
1102        let labels: Vec<_> = root.actions().map(|(label, _, _)| label.escape()).collect();
1103        assert_eq!(labels, ["1G", "1B"]);
1104    }
1105
1106    #[test]
1107    fn navigates_handles() {
1108        let game_str = r#"EFG 2 R "g" { "Player 1" "Player 2" }
1109p "root" 1 1 "iset" { "L" "R" } 0
1110t "tl" 1 "o1" { 1 2 }
1111t "tr" 2 "o2" { 3 4 }
1112"#;
1113        let game = ExtensiveFormGame::try_from_str(game_str).unwrap();
1114        let Node::Player(root) = game.root() else {
1115            panic!("expected a player root");
1116        };
1117        assert_eq!(root.player_num(), 1);
1118        assert_eq!(root.infoset(), 1);
1119        assert_eq!(root.infoset_name().escape(), "iset");
1120        let labels: Vec<_> = root.actions().map(|(label, _)| label.escape()).collect();
1121        assert_eq!(labels, ["L", "R"]);
1122
1123        let Some(Node::Terminal(left)) = root.action(EscapedStr::new("L")) else {
1124            panic!("expected a terminal after action L");
1125        };
1126        assert_eq!(left.name().escape(), "tl");
1127        assert_eq!(left.outcome(), 1);
1128        assert_eq!(left.outcome_name().map(EscapedStr::escape), Some("o1"));
1129        let payoffs: Vec<_> = left
1130            .outcome_payoffs()
1131            .iter()
1132            .map(BigRational::to_string)
1133            .collect();
1134        assert_eq!(payoffs, ["1", "2"]);
1135    }
1136
1137    #[test]
1138    fn not_distribution() {
1139        assert_eq!(
1140            validation_err(
1141                "EFG 2 R \"\" { \"1\" \"2\" }
1142c \"\" 1 \"a\" { \"x\" 9/10 } 0
1143t \"\" 1 { 0 0 }
1144"
1145            ),
1146            ValidationError::ChanceNotDistribution
1147        );
1148    }
1149
1150    #[test]
1151    fn invalid_player_num() {
1152        // a player number above the player count is rejected at parse time
1153        assert_eq!(
1154            validation_err(
1155                "EFG 2 R \"\" { \"1\" \"2\" }
1156p \"\" 3 1 \"a\" { \"x\" } 0
1157t \"\" 1 { 0 0 }
1158"
1159            ),
1160            ValidationError::InvalidPlayerNum
1161        );
1162    }
1163
1164    #[test]
1165    fn invalid_infoset_names() {
1166        assert_eq!(
1167            validation_err(
1168                "EFG 2 R \"\" { \"1\" \"2\" }
1169p \"\" 1 1 \"a\" { \"x\" } 0
1170p \"\" 1 1 \"b\" { \"x\" } 0
1171t \"\" 1 { 0 0 }
1172"
1173            ),
1174            ValidationError::NonMatchingInfosetNames
1175        );
1176    }
1177
1178    #[test]
1179    fn invalid_chance_infoset_names() {
1180        assert_eq!(
1181            validation_err(
1182                "EFG 2 R \"\" { \"1\" \"2\" }
1183c \"\" 1 \"a\" { \"x\" 1 } 0
1184c \"\" 1 \"b\" { \"x\" 1 } 0
1185t \"\" 1 { 0 0 }
1186"
1187            ),
1188            ValidationError::NonMatchingInfosetNames
1189        );
1190    }
1191
1192    #[test]
1193    fn invalid_infoset_actions() {
1194        // a reordered list no longer matches the first declaration, since order is significant
1195        assert_eq!(
1196            validation_err(
1197                "EFG 2 R \"\" { \"1\" \"2\" }
1198p \"\" 1 1 \"a\" { \"L\" \"R\" } 0
1199t \"\" 1 { 0 0 }
1200p \"\" 1 1 \"a\" { \"R\" \"L\" } 0
1201t \"\" 2 { 0 0 }
1202t \"\" 3 { 0 0 }
1203"
1204            ),
1205            ValidationError::NonMatchingInfosetActions
1206        );
1207    }
1208
1209    #[test]
1210    fn invalid_chance_infoset_actions() {
1211        assert_eq!(
1212            validation_err(
1213                "EFG 2 R \"\" { \"1\" \"2\" }
1214c \"\" 1 \"a\" { \"x\" 1 } 0
1215c \"\" 1 \"a\" { \"y\" 1 } 0
1216t \"\" 1 { 0 0 }
1217"
1218            ),
1219            ValidationError::NonMatchingInfosetActions
1220        );
1221    }
1222
1223    #[test]
1224    fn null_outcome_payoffs() {
1225        assert_eq!(
1226            validation_err(
1227                "EFG 2 R \"\" { \"1\" \"2\" }
1228p \"\" 1 1 \"a\" { \"x\" } 0 { 0 0 }
1229t \"\" 1 { 0 0 }
1230"
1231            ),
1232            ValidationError::NullOutcomePayoffs
1233        );
1234    }
1235
1236    #[test]
1237    fn invalid_payoff_number() {
1238        assert_eq!(
1239            validation_err(
1240                "EFG 2 R \"\" { \"1\" \"2\" }
1241t \"\" 1 { 0 }
1242"
1243            ),
1244            ValidationError::InvalidNumberOfPayoffs
1245        );
1246    }
1247
1248    #[test]
1249    fn non_matching_outcome_names() {
1250        assert_eq!(
1251            validation_err(
1252                "EFG 2 R \"\" { \"1\" \"2\" }
1253p \"\" 1 1 \"a\" { \"x\" } 1 \"b\" { 0 0 }
1254t \"\" 1 \"c\" { 0 0 }
1255"
1256            ),
1257            ValidationError::NonMatchingOutcomeNames
1258        );
1259    }
1260
1261    #[test]
1262    fn non_matching_outcome_payoffs() {
1263        assert_eq!(
1264            validation_err(
1265                "EFG 2 R \"\" { \"1\" \"2\" }
1266p \"\" 1 1 \"a\" { \"x\" } 1 { 0 0 }
1267t \"\" 1 { 1 1 }
1268"
1269            ),
1270            ValidationError::NonMatchingOutcomePayoffs
1271        );
1272    }
1273
1274    #[test]
1275    fn no_outcome_payoffs() {
1276        assert_eq!(
1277            validation_err(
1278                "EFG 2 R \"\" { \"1\" \"2\" }
1279p \"\" 1 1 \"a\" { \"x\" } 1
1280t \"\" 2 { 0 0 }
1281"
1282            ),
1283            ValidationError::NoOutcomePayoffs
1284        );
1285    }
1286
1287    #[test]
1288    fn undeclared_infoset() {
1289        // omitting the action list before the infoset has ever been declared is an error
1290        assert_eq!(
1291            validation_err(
1292                "EFG 2 R \"\" { \"1\" \"2\" }
1293p \"\" 1 1 0
1294t \"\" 1 { 0 0 }
1295"
1296            ),
1297            ValidationError::UndeclaredInfoset
1298        );
1299    }
1300
1301    #[test]
1302    fn fills_omitted_action_list() {
1303        let game_str = "EFG 2 R \"\" { \"1\" \"2\" }
1304p \"\" 1 1 \"a\" { \"L\" \"R\" } 0
1305t \"\" 1 { 0 0 }
1306p \"\" 1 1 0
1307t \"\" 2 { 0 0 }
1308t \"\" 3 { 0 0 }
1309";
1310        let game = ExtensiveFormGame::try_from_str(game_str).unwrap();
1311        let Node::Player(root) = game.root() else {
1312            panic!("expected a player root");
1313        };
1314        let Some(Node::Player(omitted)) = root.action(EscapedStr::new("R")) else {
1315            panic!("expected a player after action R");
1316        };
1317        // the omitted node inherits the declared label and actions
1318        assert_eq!(omitted.infoset_name().escape(), "a");
1319        let labels: Vec<_> = omitted.actions().map(|(label, _)| label.escape()).collect();
1320        assert_eq!(labels, ["L", "R"]);
1321        // and the omitted form round-trips (the omission is preserved)
1322        let written = game.to_string();
1323        let reparsed = ExtensiveFormGame::try_from_str(written.as_str()).unwrap();
1324        assert_eq!(game, reparsed);
1325    }
1326
1327    #[test]
1328    fn handle_accessors() {
1329        let game_str = r#"EFG 2 R "game" { "P1" "P2" } "the comment"
1330c "chance" 1 "ci" { "a" 1/2 "b" 1/2 } 5 { 1 2 }
1331p "pl1" 1 1 "pi1" { "x" "y" } 6 "po1" { 3 4 }
1332t "ta" 1 "oa" { 7 8 }
1333t "tb" 2 "ob" { 9 10 }
1334p "pl2" 2 2 "pi2" { "x" "y" } 7 "po2" { 5 6 }
1335t "tc" 3 "oc" { 11 12 }
1336t "td" 4 "od" { 13 14 }
1337"#;
1338        let game = ExtensiveFormGame::try_from_str(game_str).unwrap();
1339        assert_eq!(game.comment().map(EscapedStr::escape), Some("the comment"));
1340        // Display covers every node kind's formatting and round-trips
1341        let written = game.to_string();
1342        let reparsed = ExtensiveFormGame::try_from_str(written.as_str()).unwrap();
1343        assert_eq!(game, reparsed);
1344
1345        let Node::Chance(chance) = game.root() else {
1346            panic!("expected a chance root");
1347        };
1348        assert_eq!(chance.name().escape(), "chance");
1349        assert_eq!(chance.infoset(), 1);
1350        assert_eq!(chance.infoset_name().escape(), "ci");
1351        assert_eq!(chance.len(), 2);
1352        assert_eq!(chance.outcome(), 5);
1353        let chance_payoffs: Vec<_> = chance
1354            .outcome_payoffs()
1355            .unwrap()
1356            .iter()
1357            .map(ToString::to_string)
1358            .collect();
1359        assert_eq!(chance_payoffs, ["1", "2"]);
1360        let chance_labels: Vec<_> = chance
1361            .actions()
1362            .map(|(label, _, _)| label.escape())
1363            .collect();
1364        assert_eq!(chance_labels, ["a", "b"]);
1365        assert!(chance.action_at(0).is_some());
1366        assert!(chance.action_at(2).is_none());
1367        assert!(chance.action(EscapedStr::new("none")).is_none());
1368        let (prob, first_child) = chance.action(EscapedStr::new("a")).unwrap();
1369        assert_eq!(prob.to_string(), "1/2");
1370
1371        let Node::Player(player) = first_child else {
1372            panic!("expected a player after chance action a");
1373        };
1374        assert_eq!(player.name().escape(), "pl1");
1375        assert_eq!(player.player_num(), 1);
1376        assert_eq!(player.infoset(), 1);
1377        assert_eq!(player.infoset_name().escape(), "pi1");
1378        assert_eq!(player.len(), 2);
1379        assert_eq!(player.outcome(), 6);
1380        assert_eq!(player.outcome_name().map(EscapedStr::escape), Some("po1"));
1381        let player_payoffs: Vec<_> = player
1382            .outcome_payoffs()
1383            .unwrap()
1384            .iter()
1385            .map(ToString::to_string)
1386            .collect();
1387        assert_eq!(player_payoffs, ["3", "4"]);
1388        let player_labels: Vec<_> = player.actions().map(|(label, _)| label.escape()).collect();
1389        assert_eq!(player_labels, ["x", "y"]);
1390        assert!(player.action(EscapedStr::new("none")).is_none());
1391        assert!(player.action(EscapedStr::new("y")).is_some());
1392        let (label, leaf) = player.action_at(0).unwrap();
1393        assert_eq!(label.escape(), "x");
1394        assert!(player.action_at(2).is_none());
1395
1396        let Node::Terminal(terminal) = leaf else {
1397            panic!("expected a terminal after player action x");
1398        };
1399        assert_eq!(terminal.name().escape(), "ta");
1400        assert_eq!(terminal.outcome(), 1);
1401        assert_eq!(terminal.outcome_name().map(EscapedStr::escape), Some("oa"));
1402        let terminal_payoffs: Vec<_> = terminal
1403            .outcome_payoffs()
1404            .iter()
1405            .map(ToString::to_string)
1406            .collect();
1407        assert_eq!(terminal_payoffs, ["7", "8"]);
1408    }
1409
1410    #[test]
1411    fn error_display() {
1412        let parse_err = ExtensiveFormGame::try_from_str("not an efg").unwrap_err();
1413        assert!(parse_err.to_string().starts_with("error parsing game at:"));
1414
1415        let bad = "EFG 2 R \"\" { \"1\" \"2\" }\np \"\" 3 1 \"a\" { \"x\" } 0\nt \"\" 1 { 0 0 }\n";
1416        assert_eq!(
1417            ExtensiveFormGame::try_from_str(bad)
1418                .unwrap_err()
1419                .to_string(),
1420            "invalid efg: InvalidPlayerNum"
1421        );
1422        assert_eq!(
1423            ValidationError::ChanceNotDistribution.to_string(),
1424            "ChanceNotDistribution"
1425        );
1426    }
1427
1428    #[test]
1429    fn trailing_input_is_rejected() {
1430        let game = r#"EFG 2 R "" { "1" "2" } t "" 1 { 1 2 } trailing"#;
1431        assert!(matches!(
1432            ExtensiveFormGame::try_from_str(game),
1433            Err(Error::Parse("trailing"))
1434        ));
1435    }
1436
1437    #[test]
1438    fn rejects_overflowing_exponent() {
1439        // an exponent that doesn't fit an i32 fails the number parse
1440        assert!(super::big_float("1e99999999999 ").is_err());
1441    }
1442
1443    #[test]
1444    fn outcome_data_filled_across_nodes() {
1445        // an outcome's name and payoffs may be supplied on a later node than where it first appears
1446        let game = "EFG 2 R \"\" { \"1\" \"2\" }
1447p \"\" 1 1 \"i\" { \"x\" } 1
1448t \"\" 1 \"named\" { 3 4 }
1449";
1450        assert!(ExtensiveFormGame::try_from_str(game).is_ok());
1451    }
1452
1453    #[test]
1454    fn chance_null_outcome_with_payoffs() {
1455        // outcome validation also runs for chance nodes
1456        assert_eq!(
1457            validation_err(
1458                "EFG 2 R \"\" { \"1\" \"2\" }
1459c \"\" 1 \"i\" { \"x\" 1 } 0 { 1 2 }
1460t \"\" 1 { 0 0 }
1461"
1462            ),
1463            ValidationError::NullOutcomePayoffs
1464        );
1465    }
1466}