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::Zero;
27use std::collections::hash_map::Entry;
28use std::collections::{HashMap, HashSet};
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 outcome defined while parsing, keyed by id. Each entry holds the outcome's name and its
39/// payoffs, which the tree nodes reference by id. The null (0) outcome is never stored.
40type Outcomes<'a> = HashMap<u64, (&'a EscapedStr, Box<[BigRational]>)>;
41
42/// Every infoset seen while parsing, keyed by id. Player infosets are split per player (index =
43/// `player_num - 1`); chance is its own namespace. Each entry holds the infoset's label and ordered
44/// actions, which the tree nodes reference by id.
45#[derive(Debug, PartialEq, Clone)]
46struct Infosets<'a> {
47    player: Box<[HashMap<u64, PlayerInfoset<'a>>]>,
48    chance: HashMap<u64, ChanceInfoset<'a>>,
49}
50
51/// An index into the game's flat node arena ([`ExtensiveFormGame`]'s `nodes`).
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53struct NodeId(usize);
54
55/// A node in the raw, id-referenced game tree. Infoset payloads live on the game, not here.
56#[derive(Debug, PartialEq, Clone)]
57enum RawNode<'a> {
58    Chance(RawChance<'a>),
59    Player(RawPlayer<'a>),
60    Terminal(RawTerminal<'a>),
61}
62
63#[derive(Debug, PartialEq, Eq, Clone)]
64struct RawChance<'a> {
65    name: &'a EscapedStr,
66    infoset: u64,
67    // did THIS node write the infoset block, or inherit it by omission?
68    declared: bool,
69    children: Box<[NodeId]>,
70    outcome: u64,
71    // did THIS node write the outcome's name and payoffs, or reference it by id?
72    outcome_declared: bool,
73}
74
75#[derive(Debug, PartialEq, Eq, Clone)]
76struct RawPlayer<'a> {
77    name: &'a EscapedStr,
78    player_num: usize,
79    infoset: u64,
80    // did THIS node write the infoset block, or inherit it by omission?
81    declared: bool,
82    children: Box<[NodeId]>,
83    outcome: u64,
84    // did THIS node write the outcome's name and payoffs, or reference it by id?
85    outcome_declared: bool,
86}
87
88#[derive(Debug, PartialEq, Eq, Clone)]
89struct RawTerminal<'a> {
90    name: &'a EscapedStr,
91    outcome: u64,
92    // did THIS node write the outcome's name and payoffs, or reference it by id?
93    outcome_declared: bool,
94}
95
96/// A full extensive form game
97///
98/// This can be parsed from a [str] reference using [`ExtensiveFormGame::try_from_str`] or using the
99/// [`TryFrom`] / [`TryInto`] traits. It implements [Display] for formatting.
100///
101/// # Example
102///
103/// ```
104/// # use gambit_parser::ExtensiveFormGame;
105/// let gambit = r#"EFG 2 R "" { "1" "2" } t "" 1 "" { 1 2 }"#;
106/// let game: ExtensiveFormGame<'_> = gambit.try_into().unwrap();
107/// let output = game.to_string();
108/// ```
109#[derive(Debug, PartialEq, Clone)]
110pub struct ExtensiveFormGame<'a> {
111    name: &'a EscapedStr,
112    player_names: Box<[&'a EscapedStr]>,
113    comment: Option<&'a EscapedStr>,
114    infosets: Infosets<'a>,
115    outcomes: Outcomes<'a>,
116    nodes: Box<[RawNode<'a>]>,
117    root: NodeId,
118}
119
120impl<'a> ExtensiveFormGame<'a> {
121    /// The name of the game
122    #[must_use]
123    pub fn name(&self) -> &'a EscapedStr {
124        self.name
125    }
126
127    /// Names for every player, in order
128    #[must_use]
129    pub fn player_names(&self) -> &[&'a EscapedStr] {
130        &self.player_names
131    }
132
133    /// An optional game comment
134    #[must_use]
135    pub fn comment(&self) -> Option<&'a EscapedStr> {
136        self.comment
137    }
138
139    /// The root node of the game tree
140    #[must_use]
141    pub fn root<'g>(&'g self) -> Node<'a, 'g> {
142        self.wrap(self.root)
143    }
144
145    /// Adapt this game for writing in a given [`WriteMode`]; the result implements [`Display`].
146    ///
147    /// Plain `Display` (and `to_string`) uses [`WriteMode::Faithful`].
148    #[must_use]
149    pub fn display<'g>(&'g self, mode: WriteMode) -> GameDisplay<'a, 'g> {
150        GameDisplay { game: self, mode }
151    }
152
153    fn wrap<'g>(&'g self, id: NodeId) -> Node<'a, 'g> {
154        match &self.nodes[id.0] {
155            RawNode::Chance(raw) => Node::Chance(Chance { game: self, raw }),
156            RawNode::Player(raw) => Node::Player(Player { game: self, raw }),
157            RawNode::Terminal(raw) => Node::Terminal(Terminal { game: self, raw }),
158        }
159    }
160
161    /// The outcome's name, or `None` for the null (0) outcome
162    fn outcome_name(&self, outcome: u64) -> Option<&'a EscapedStr> {
163        self.outcomes.get(&outcome).map(|(name, _)| *name)
164    }
165
166    /// The outcome's payoffs, or `None` for the null (0) outcome
167    fn outcome_payoffs(&self, outcome: u64) -> Option<&[BigRational]> {
168        self.outcomes.get(&outcome).map(|(_, payoffs)| &payoffs[..])
169    }
170}
171
172impl Display for ExtensiveFormGame<'_> {
173    fn fmt(&self, out: &mut Formatter<'_>) -> Result<(), FmtError> {
174        self.display(WriteMode::Faithful).fmt(out)
175    }
176}
177
178/// How much of each shared infoset and outcome to write when serializing a game.
179///
180/// Both are declared once and referenced by id, so a given node may or may not repeat the block.
181/// This selects which nodes write it. See [`ExtensiveFormGame::display`].
182#[derive(Debug, Clone, Copy, PartialEq, Eq)]
183pub enum WriteMode {
184    /// Declare each infoset and outcome only on its first appearance; reference it by id after.
185    Minimal,
186    /// Reproduce the parsed input: write a block exactly where the source node wrote one.
187    Faithful,
188    /// Write every infoset and outcome in full on every node, as Gambit's own writer does.
189    Exhaustive,
190}
191
192/// A [`Display`] adapter that writes a game in a chosen [`WriteMode`], returned by
193/// [`ExtensiveFormGame::display`].
194#[derive(Clone, Copy)]
195pub struct GameDisplay<'a, 'g> {
196    game: &'g ExtensiveFormGame<'a>,
197    mode: WriteMode,
198}
199
200impl GameDisplay<'_, '_> {
201    /// Whether a node should write its outcome's full definition rather than reference it by id
202    fn declares_outcome(self, outcome: u64, declared: bool, seen: &mut HashSet<u64>) -> bool {
203        match self.mode {
204            WriteMode::Minimal => outcome != 0 && seen.insert(outcome),
205            WriteMode::Faithful => declared,
206            WriteMode::Exhaustive => outcome != 0,
207        }
208    }
209}
210
211impl Display for GameDisplay<'_, '_> {
212    fn fmt(&self, out: &mut Formatter<'_>) -> Result<(), FmtError> {
213        let game = self.game;
214        write!(out, "EFG 2 R \"{}\" {{ ", game.name.escape())?;
215        for name in &game.player_names {
216            write!(out, "\"{}\" ", name.escape())?;
217        }
218        writeln!(out, "}}")?;
219        if let Some(comment) = game.comment {
220            writeln!(out, "\"{}\"", comment.escape())?;
221        }
222
223        // Minimal writes each block the first time its id is seen and references it afterward; only
224        // it reads these sets, so reserve their exact block counts for Minimal and leave the other
225        // modes' sets unallocated
226        let mut chance_seen = HashSet::new();
227        let mut player_seen = HashSet::new();
228        let mut outcome_seen = HashSet::new();
229        if self.mode == WriteMode::Minimal {
230            chance_seen.reserve(game.infosets.chance.len());
231            player_seen.reserve(game.infosets.player.iter().map(HashMap::len).sum());
232            outcome_seen.reserve(game.outcomes.len());
233        }
234        let mut stack = vec![game.root];
235        while let Some(id) = stack.pop() {
236            match &game.nodes[id.0] {
237                RawNode::Chance(raw) => {
238                    write!(out, "\nc \"{}\" {}", raw.name.escape(), raw.infoset)?;
239                    let block = match self.mode {
240                        WriteMode::Minimal => chance_seen.insert(raw.infoset),
241                        WriteMode::Faithful => raw.declared,
242                        WriteMode::Exhaustive => true,
243                    };
244                    if block {
245                        let (label, actions) = &game.infosets.chance[&raw.infoset];
246                        write!(out, " \"{}\" {{ ", label.escape())?;
247                        for (action, prob) in actions {
248                            write!(out, "\"{}\" {} ", action.escape(), prob)?;
249                        }
250                        write!(out, "}}")?;
251                    }
252                    let declared =
253                        self.declares_outcome(raw.outcome, raw.outcome_declared, &mut outcome_seen);
254                    write_outcome(out, game, raw.outcome, declared)?;
255                    stack.extend(raw.children.iter().rev().copied());
256                }
257                RawNode::Player(raw) => {
258                    write!(
259                        out,
260                        "\np \"{}\" {} {}",
261                        raw.name.escape(),
262                        raw.player_num,
263                        raw.infoset
264                    )?;
265                    let block = match self.mode {
266                        WriteMode::Minimal => player_seen.insert((raw.player_num, raw.infoset)),
267                        WriteMode::Faithful => raw.declared,
268                        WriteMode::Exhaustive => true,
269                    };
270                    if block {
271                        let (label, actions) =
272                            &game.infosets.player[raw.player_num - 1][&raw.infoset];
273                        write!(out, " \"{}\" {{ ", label.escape())?;
274                        for action in actions {
275                            write!(out, "\"{}\" ", action.escape())?;
276                        }
277                        write!(out, "}}")?;
278                    }
279                    let declared =
280                        self.declares_outcome(raw.outcome, raw.outcome_declared, &mut outcome_seen);
281                    write_outcome(out, game, raw.outcome, declared)?;
282                    stack.extend(raw.children.iter().rev().copied());
283                }
284                RawNode::Terminal(raw) => {
285                    write!(out, "\nt \"{}\"", raw.name.escape())?;
286                    let declared =
287                        self.declares_outcome(raw.outcome, raw.outcome_declared, &mut outcome_seen);
288                    write_outcome(out, game, raw.outcome, declared)?;
289                }
290            }
291        }
292        writeln!(out)
293    }
294}
295
296/// An error that happens while trying to turn a string into an [`ExtensiveFormGame`]
297#[derive(Debug)]
298#[non_exhaustive]
299pub enum Error<'a> {
300    /// A problem with parsing
301    ///
302    /// This will show the remainder of the string where the parse error occurred
303    Parse(&'a str),
304    /// A well-formed line that makes the game inconsistent (a mismatched infoset, an undefined
305    /// outcome, and so on)
306    Validation(ValidationError),
307}
308
309impl Display for Error<'_> {
310    fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), FmtError> {
311        match self {
312            Error::Parse(rem) => write!(fmt, "error parsing game at: '{rem}'"),
313            Error::Validation(err) => write!(fmt, "invalid efg: {err}"),
314        }
315    }
316}
317
318impl StdError for Error<'_> {}
319
320/// An error that results from something invalid about the parsed extensive form game
321#[derive(Debug, PartialEq, Eq)]
322#[non_exhaustive]
323pub enum ValidationError {
324    /// A player's number wasn't between one and the number of players
325    InvalidPlayerNum,
326    /// An infoset had different names attached to it
327    NonMatchingInfosetNames,
328    /// An infoset had different sets of associated actions
329    NonMatchingInfosetActions,
330    /// A name or payoffs were attached to the null (0) outcome
331    NullOutcomePayoffs,
332    /// The number of specified payoffs did not match the number of players
333    InvalidNumberOfPayoffs,
334    /// An outcome was defined with a name that didn't match its first definition
335    NonMatchingOutcomeNames,
336    /// An outcome was defined with payoffs that didn't match its first definition
337    NonMatchingOutcomePayoffs,
338    /// A node referenced an outcome that was never defined
339    UndefinedOutcome,
340    /// A node omitted its action list for an infoset that was never declared
341    UndeclaredInfoset,
342}
343
344impl Display for ValidationError {
345    fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), FmtError> {
346        write!(fmt, "{self:?}")
347    }
348}
349
350impl From<ValidationError> for Error<'_> {
351    fn from(err: ValidationError) -> Self {
352        Error::Validation(err)
353    }
354}
355
356impl<'a> From<nom::Err<nom::error::Error<&'a str>>> for Error<'a> {
357    fn from(err: nom::Err<nom::error::Error<&'a str>>) -> Self {
358        match err {
359            nom::Err::Incomplete(_) => panic!("internal error: incomplete parsing"),
360            nom::Err::Error(err) | nom::Err::Failure(err) => Error::Parse(err.input),
361        }
362    }
363}
364
365impl<'a> ExtensiveFormGame<'a> {
366    /// Try to parse a game from a string
367    ///
368    /// This is identical to `ExtensiveFormGame::try_from` or `"...".try_into()`.
369    ///
370    /// # Errors
371    ///
372    /// Returns an [`Error`] if the input isn't a syntactically valid and self-consistent game.
373    pub fn try_from_str(input: &'a str) -> Result<Self, Error<'a>> {
374        let (rest, game) = parse_game(input)?;
375        let rest = rest.trim_start();
376        if !rest.is_empty() {
377            return Err(Error::Parse(rest));
378        }
379        Ok(game)
380    }
381}
382
383impl<'a> TryFrom<&'a str> for ExtensiveFormGame<'a> {
384    type Error = Error<'a>;
385
386    fn try_from(input: &'a str) -> Result<Self, Self::Error> {
387        Self::try_from_str(input)
388    }
389}
390
391/// An arbitrary node in the game tree
392///
393/// A handle that pairs a node with its game so it can resolve its infoset. These are cheap to copy.
394#[derive(Clone, Copy)]
395pub enum Node<'a, 'g> {
396    /// A chance node
397    Chance(Chance<'a, 'g>),
398    /// A player node
399    Player(Player<'a, 'g>),
400    /// A terminal node
401    Terminal(Terminal<'a, 'g>),
402}
403
404/// Write a node's outcome: always the id, plus the name and payoffs when this node declared them
405/// (a node that only referenced the outcome by id writes just the id, matching the file)
406fn write_outcome(
407    out: &mut Formatter<'_>,
408    game: &ExtensiveFormGame<'_>,
409    outcome: u64,
410    declared: bool,
411) -> Result<(), FmtError> {
412    write!(out, " {outcome}")?;
413    if declared {
414        if let Some(name) = game.outcome_name(outcome) {
415            write!(out, " \"{}\"", name.escape())?;
416        }
417        if let Some(payoffs) = game.outcome_payoffs(outcome) {
418            write!(out, " {{ ")?;
419            for payoff in payoffs {
420                write!(out, "{payoff} ")?;
421            }
422            write!(out, "}}")?;
423        }
424    }
425    Ok(())
426}
427
428/// A chance node
429///
430/// A chance node represents a point in the game where things advance randomly, or alternatively,
431/// where "nature" takes a turn.
432#[derive(Clone, Copy)]
433pub struct Chance<'a, 'g> {
434    game: &'g ExtensiveFormGame<'a>,
435    raw: &'g RawChance<'a>,
436}
437
438impl<'a, 'g> Chance<'a, 'g> {
439    fn entry(self) -> &'g ChanceInfoset<'a> {
440        &self.game.infosets.chance[&self.raw.infoset]
441    }
442
443    /// The name of the node
444    #[must_use]
445    pub fn name(self) -> &'a EscapedStr {
446        self.raw.name
447    }
448
449    /// The id of the node's infoset
450    #[must_use]
451    pub fn infoset(self) -> u64 {
452        self.raw.infoset
453    }
454
455    /// The infoset's label
456    #[must_use]
457    pub fn infoset_name(self) -> &'a EscapedStr {
458        self.entry().0
459    }
460
461    /// All possible actions with their names, probabilities, and resulting nodes
462    pub fn actions(
463        self,
464    ) -> impl Iterator<Item = (&'a EscapedStr, &'g BigRational, Node<'a, 'g>)> + 'g {
465        let (_, actions) = self.entry();
466        let game = self.game;
467        actions
468            .iter()
469            .zip(self.raw.children.iter())
470            .map(move |((label, prob), &child)| (*label, prob, game.wrap(child)))
471    }
472
473    /// The probability and child for the action with the given label
474    ///
475    /// Labels need not be unique within an infoset, so this returns the first match.
476    #[must_use]
477    pub fn action(self, label: &EscapedStr) -> Option<(&'g BigRational, Node<'a, 'g>)> {
478        self.actions()
479            .find(|(name, _, _)| *name == label)
480            .map(|(_, prob, next)| (prob, next))
481    }
482
483    /// The number of actions (always at least one)
484    #[allow(clippy::len_without_is_empty)]
485    #[must_use]
486    pub fn len(self) -> usize {
487        self.raw.children.len()
488    }
489
490    /// The name, probability, and child for the action at the given index
491    #[must_use]
492    pub fn action_at(
493        self,
494        index: usize,
495    ) -> Option<(&'a EscapedStr, &'g BigRational, Node<'a, 'g>)> {
496        let (_, actions) = self.entry();
497        let (label, prob) = actions.get(index)?;
498        let &child = self.raw.children.get(index)?;
499        Some((*label, prob, self.game.wrap(child)))
500    }
501
502    /// The outcome id
503    #[must_use]
504    pub fn outcome(self) -> u64 {
505        self.raw.outcome
506    }
507
508    /// The name of the outcome
509    ///
510    /// `None` for the null (0) outcome or an outcome with no name.
511    #[must_use]
512    pub fn outcome_name(self) -> Option<&'a EscapedStr> {
513        self.game.outcome_name(self.raw.outcome)
514    }
515
516    /// Outcome payoffs for this node
517    ///
518    /// These are added to every player's payoff for traversing through this node. `None` only for
519    /// the null (0) outcome.
520    #[must_use]
521    pub fn outcome_payoffs(self) -> Option<&'g [BigRational]> {
522        self.game.outcome_payoffs(self.raw.outcome)
523    }
524}
525
526/// A player node in the game tree
527///
528/// A player node represents a place where one of the players chooses what happens next.
529#[derive(Clone, Copy)]
530pub struct Player<'a, 'g> {
531    game: &'g ExtensiveFormGame<'a>,
532    raw: &'g RawPlayer<'a>,
533}
534
535impl<'a, 'g> Player<'a, 'g> {
536    fn entry(self) -> &'g PlayerInfoset<'a> {
537        &self.game.infosets.player[self.raw.player_num - 1][&self.raw.infoset]
538    }
539
540    /// The name of the node
541    #[must_use]
542    pub fn name(self) -> &'a EscapedStr {
543        self.raw.name
544    }
545
546    /// The player acting at this node
547    ///
548    /// This will always be between 1 and the number of players.
549    #[must_use]
550    pub fn player_num(self) -> usize {
551        self.raw.player_num
552    }
553
554    /// The infoset id for this node and player
555    #[must_use]
556    pub fn infoset(self) -> u64 {
557        self.raw.infoset
558    }
559
560    /// The infoset's label
561    #[must_use]
562    pub fn infoset_name(self) -> &'a EscapedStr {
563        self.entry().0
564    }
565
566    /// All the actions a player can take with their names and resulting nodes
567    pub fn actions(self) -> impl Iterator<Item = (&'a EscapedStr, Node<'a, 'g>)> + 'g {
568        let (_, labels) = self.entry();
569        let game = self.game;
570        labels
571            .iter()
572            .zip(self.raw.children.iter())
573            .map(move |(label, &child)| (*label, game.wrap(child)))
574    }
575
576    /// The child reached by the action with the given label
577    ///
578    /// Labels need not be unique within an infoset, so this returns the first match.
579    #[must_use]
580    pub fn action(self, label: &EscapedStr) -> Option<Node<'a, 'g>> {
581        self.actions()
582            .find(|(name, _)| *name == label)
583            .map(|(_, next)| next)
584    }
585
586    /// The number of actions (always at least one)
587    #[allow(clippy::len_without_is_empty)]
588    #[must_use]
589    pub fn len(self) -> usize {
590        self.raw.children.len()
591    }
592
593    /// The name and child for the action at the given index
594    #[must_use]
595    pub fn action_at(self, index: usize) -> Option<(&'a EscapedStr, Node<'a, 'g>)> {
596        let (_, actions) = self.entry();
597        let &label = actions.get(index)?;
598        let &child = self.raw.children.get(index)?;
599        Some((label, self.game.wrap(child)))
600    }
601
602    /// The outcome id
603    #[must_use]
604    pub fn outcome(self) -> u64 {
605        self.raw.outcome
606    }
607
608    /// The name of the outcome
609    ///
610    /// `None` for the null (0) outcome or an outcome with no name.
611    #[must_use]
612    pub fn outcome_name(self) -> Option<&'a EscapedStr> {
613        self.game.outcome_name(self.raw.outcome)
614    }
615
616    /// Payoffs associated with the outcome
617    ///
618    /// `None` only for the null (0) outcome.
619    #[must_use]
620    pub fn outcome_payoffs(self) -> Option<&'g [BigRational]> {
621        self.game.outcome_payoffs(self.raw.outcome)
622    }
623}
624
625/// A terminal node represents the end of a game
626///
627/// Terminal nodes simply assign payoffs to every player in the game
628#[derive(Clone, Copy)]
629pub struct Terminal<'a, 'g> {
630    game: &'g ExtensiveFormGame<'a>,
631    raw: &'g RawTerminal<'a>,
632}
633
634impl<'a, 'g> Terminal<'a, 'g> {
635    /// The name of this node
636    #[must_use]
637    pub fn name(self) -> &'a EscapedStr {
638        self.raw.name
639    }
640
641    /// The outcome id
642    #[must_use]
643    pub fn outcome(self) -> u64 {
644        self.raw.outcome
645    }
646
647    /// The name of this outcome
648    ///
649    /// `None` for the null (0) outcome or an outcome with no name.
650    #[must_use]
651    pub fn outcome_name(self) -> Option<&'a EscapedStr> {
652        self.game.outcome_name(self.raw.outcome)
653    }
654
655    /// The payoffs to every player
656    ///
657    /// `None` only for the null (0) outcome — a terminal with no outcome attached.
658    #[must_use]
659    pub fn outcome_payoffs(self) -> Option<&'g [BigRational]> {
660        self.game.outcome_payoffs(self.raw.outcome)
661    }
662}
663
664fn negate(input: &str) -> IResult<&str, bool> {
665    let (input, res) = opt(one_of("+-")).parse(input)?;
666    Ok((input, res == Some('-')))
667}
668
669fn fail(input: &str) -> nom::Err<nom::error::Error<&str>> {
670    nom::Err::Error(nom::error::Error::new(input, ErrorKind::Fail))
671}
672
673fn big_float(input: &str) -> IResult<&str, BigRational> {
674    let (res_input, (main_neg, (int, dec), exp)) = (
675        negate,
676        alt((
677            pair(
678                digit1,
679                map(opt(preceded(char('.'), digit0)), Option::unwrap_or_default),
680            ),
681            separated_pair(digit0, char('.'), digit1),
682        )),
683        opt(preceded(one_of("eE"), pair(negate, digit1))),
684    )
685        .parse(input)?;
686    let mut res = if int.is_empty() {
687        BigRational::zero()
688    } else {
689        BigRational::from_integer(int.parse().unwrap())
690    };
691    if !dec.is_empty() {
692        let pow: u32 = dec.len().try_into().map_err(|_| fail(input))?;
693        res += BigRational::new(dec.parse().unwrap(), BigInt::from(10).pow(pow));
694    }
695    if let Some((neg, exp)) = exp {
696        let exp: i32 = exp.parse().map_err(|_| fail(input))?;
697        res *= BigRational::from_integer(10.into()).pow(if neg { -exp } else { exp });
698    }
699    if main_neg {
700        res = -res;
701    }
702    Ok((res_input, res))
703}
704
705fn big_rational(input: &str) -> IResult<&str, BigRational> {
706    let (rest, (num, denom)) = pair(big_float, opt(preceded(char('/'), big_float))).parse(input)?;
707    match denom {
708        // a zero denominator would panic in num-rational's `Div`; reject it as a parse error
709        Some(denom) if denom.is_zero() => Err(fail(input)),
710        Some(denom) => Ok((rest, num / denom)),
711        None => Ok((rest, num)),
712    }
713}
714
715fn label(input: &str) -> IResult<&str, &EscapedStr> {
716    map(
717        delimited(
718            char('"'),
719            // the body is any mix of the `\"` escape and ordinary non-quote characters; matching
720            // `\"` first means a lone `\` is ordinary and only a `"` after a `\` is escaped
721            recognize(many0(alt((tag(r#"\""#), recognize(none_of("\"")))))),
722            char('"'),
723        ),
724        EscapedStr::new,
725    )
726    .parse(input)
727}
728
729fn spacelist<'a, O, E, F>(f: F) -> impl Parser<&'a str, Output = Vec<O>, Error = E>
730where
731    F: Parser<&'a str, Output = O, Error = E>,
732    E: ParseError<&'a str>,
733{
734    delimited(
735        pair(char('{'), multispace0),
736        separated_list1(multispace1, f),
737        pair(multispace0, char('}')),
738    )
739}
740
741fn commalist<'a, O, E, F>(f: F) -> impl Parser<&'a str, Output = Vec<O>, Error = E>
742where
743    F: Parser<&'a str, Output = O, Error = E>,
744    E: ParseError<&'a str>,
745{
746    delimited(
747        pair(char('{'), multispace0),
748        separated_list1((multispace0, opt(char(',')), multispace0), f),
749        pair(multispace0, char('}')),
750    )
751}
752
753/// A parent node whose header is parsed but whose children are still being collected.
754struct PendingNode<'a> {
755    node: RawNode<'a>,
756    child_count: usize,
757    children: Vec<NodeId>,
758}
759
760impl<'a> PendingNode<'a> {
761    fn finish(self) -> RawNode<'a> {
762        let PendingNode {
763            mut node, children, ..
764        } = self;
765        match &mut node {
766            RawNode::Chance(chance) => chance.children = children.into(),
767            RawNode::Player(player) => player.children = children.into(),
768            // only chance and player nodes gather children, so only they are ever pending
769            RawNode::Terminal(_) => unreachable!("terminal nodes are never pending"),
770        }
771        node
772    }
773}
774
775/// Record or check an infoset declaration, returning whether the block was written here and the
776/// action count. A first declaration is inserted, a repeat must match exactly, an omission inherits.
777fn resolve_infoset<'a, A: PartialEq>(
778    map: &mut HashMap<u64, (&'a EscapedStr, Box<[A]>)>,
779    infoset: u64,
780    declared: Option<(&'a EscapedStr, Vec<A>)>,
781) -> Result<(bool, usize), Error<'a>> {
782    if let Some((name, actions)) = declared {
783        match map.entry(infoset) {
784            // a first declaration is stored (boxed), a repeat is only compared against the stored
785            // one, so the parsed `Vec` is never boxed just to be dropped
786            Entry::Vacant(ent) => {
787                let count = actions.len();
788                ent.insert((name, actions.into()));
789                Ok((true, count))
790            }
791            Entry::Occupied(ent) => {
792                let (stored_name, stored_actions) = ent.get();
793                if *stored_name != name {
794                    Err(ValidationError::NonMatchingInfosetNames.into())
795                } else if **stored_actions != *actions {
796                    Err(ValidationError::NonMatchingInfosetActions.into())
797                } else {
798                    Ok((true, actions.len()))
799                }
800            }
801        }
802    } else {
803        let (_, actions) = map
804            .get(&infoset)
805            .ok_or(ValidationError::UndeclaredInfoset)?;
806        Ok((false, actions.len()))
807    }
808}
809
810/// Record or check a node's outcome, mirroring [`resolve_infoset`]. A node either defines the
811/// outcome with a name and payoffs, or references it by bare id: the first definition is stored, a
812/// repeat definition must match it exactly, and a reference must name an already-defined outcome.
813/// The null (0) outcome carries no data and is never stored.
814fn resolve_outcome<'a>(
815    outcomes: &mut Outcomes<'a>,
816    num_players: usize,
817    outcome: u64,
818    definition: Option<(&'a EscapedStr, Vec<BigRational>)>,
819) -> Result<(), Error<'a>> {
820    if let Some((name, payoffs)) = definition {
821        if outcome == 0 {
822            Err(ValidationError::NullOutcomePayoffs.into())
823        } else if payoffs.len() != num_players {
824            Err(ValidationError::InvalidNumberOfPayoffs.into())
825        } else {
826            match outcomes.entry(outcome) {
827                // a first definition is stored (boxed), a repeat is only compared against the
828                // stored one, so the parsed `Vec` is never boxed just to be dropped
829                Entry::Vacant(ent) => {
830                    ent.insert((name, payoffs.into()));
831                    Ok(())
832                }
833                Entry::Occupied(ent) => {
834                    let (stored_name, stored_payoffs) = ent.get();
835                    if *stored_name != name {
836                        Err(ValidationError::NonMatchingOutcomeNames.into())
837                    } else if **stored_payoffs != *payoffs {
838                        Err(ValidationError::NonMatchingOutcomePayoffs.into())
839                    } else {
840                        Ok(())
841                    }
842                }
843            }
844        }
845    } else if outcome != 0 && !outcomes.contains_key(&outcome) {
846        // a bare id references an outcome that must already be defined; the null (0) id is fine
847        Err(ValidationError::UndefinedOutcome.into())
848    } else {
849        Ok(())
850    }
851}
852
853/// Parse the whole game tree into a flat arena, returning the nodes and the root's id.
854fn parse_tree<'a>(
855    mut input: &'a str,
856    infosets: &mut Infosets<'a>,
857    outcomes: &mut Outcomes<'a>,
858    num_players: usize,
859) -> Result<(&'a str, Box<[RawNode<'a>]>, NodeId), Error<'a>> {
860    // finished nodes, in the post-order they complete (every child precedes its parent)
861    let mut nodes: Vec<RawNode<'a>> = Vec::new();
862    // parents still gathering their children
863    let mut stack: Vec<PendingNode<'a>> = Vec::new();
864
865    loop {
866        let (rest, style) = preceded(multispace1, one_of("cpt")).parse(input)?;
867        input = rest;
868        // a chance or player node opens a frame; a terminal completes immediately
869        let mut completed = match style {
870            'c' => {
871                let (rest, chance, child_count) =
872                    parse_chance(input, infosets, outcomes, num_players)?;
873                input = rest;
874                stack.push(PendingNode {
875                    node: RawNode::Chance(chance),
876                    child_count,
877                    children: Vec::with_capacity(child_count),
878                });
879                continue;
880            }
881            'p' => {
882                let (rest, player, child_count) =
883                    parse_player(input, infosets, outcomes, num_players)?;
884                input = rest;
885                stack.push(PendingNode {
886                    node: RawNode::Player(player),
887                    child_count,
888                    children: Vec::with_capacity(child_count),
889                });
890                continue;
891            }
892            't' => {
893                let (rest, term) = parse_terminal(input, outcomes, num_players)?;
894                input = rest;
895                push_node(&mut nodes, RawNode::Terminal(term))
896            }
897            // `one_of("cpt")` only ever yields one of these three characters
898            _ => unreachable!(),
899        };
900
901        // attach the finished node to its waiting parent, finishing parents that fill up in turn
902        loop {
903            let Some(pending) = stack.last_mut() else {
904                // nothing is waiting, so this node is the root and the tree is complete
905                return Ok((input, nodes.into(), completed));
906            };
907            pending.children.push(completed);
908            if pending.children.len() < pending.child_count {
909                break;
910            }
911            completed = push_node(&mut nodes, stack.pop().unwrap().finish());
912        }
913    }
914}
915
916/// Append a finished node to the arena and return its id
917fn push_node<'a>(nodes: &mut Vec<RawNode<'a>>, node: RawNode<'a>) -> NodeId {
918    let id = NodeId(nodes.len());
919    nodes.push(node);
920    id
921}
922
923/// Parse a chance node's header, resolving its outcome and returning the node (children still
924/// empty) and its child count
925fn parse_chance<'a>(
926    input: &'a str,
927    infosets: &mut Infosets<'a>,
928    outcomes: &mut Outcomes<'a>,
929    num_players: usize,
930) -> Result<(&'a str, RawChance<'a>, usize), Error<'a>> {
931    let (input, (name, infoset, declared, outcome, definition)) = (
932        preceded(multispace1, label),
933        preceded(multispace1, u64),
934        opt((
935            preceded(multispace1, label),
936            preceded(
937                multispace1,
938                spacelist(separated_pair(label, multispace1, big_rational)),
939            ),
940        )),
941        preceded(multispace1, u64),
942        // an outcome is either a bare id or a name paired with payoffs (see resolve_outcome)
943        opt((
944            preceded(multispace1, label),
945            preceded(multispace1, commalist(big_rational)),
946        )),
947    )
948        .parse(input)?;
949    let (declared, child_count) = resolve_infoset(&mut infosets.chance, infoset, declared)?;
950    let outcome_declared = definition.is_some();
951    resolve_outcome(outcomes, num_players, outcome, definition)?;
952    Ok((
953        input,
954        RawChance {
955            name,
956            infoset,
957            declared,
958            // filled once the following child nodes are parsed (see PendingNode::finish)
959            children: Box::default(),
960            outcome,
961            outcome_declared,
962        },
963        child_count,
964    ))
965}
966
967/// Parse a player node's header, resolving its outcome and returning the node (children still
968/// empty) and its child count
969fn parse_player<'a>(
970    input: &'a str,
971    infosets: &mut Infosets<'a>,
972    outcomes: &mut Outcomes<'a>,
973    num_players: usize,
974) -> Result<(&'a str, RawPlayer<'a>, usize), Error<'a>> {
975    let (input, (name, player_num, infoset, declared, outcome, definition)) = (
976        preceded(multispace1, label),
977        preceded(multispace1, u64),
978        preceded(multispace1, u64),
979        opt((
980            preceded(multispace1, label),
981            preceded(multispace1, spacelist(label)),
982        )),
983        preceded(multispace1, u64),
984        // an outcome is either a bare id or a name paired with payoffs (see resolve_outcome)
985        opt((
986            preceded(multispace1, label),
987            preceded(multispace1, commalist(big_rational)),
988        )),
989    )
990        .parse(input)?;
991    let player_num: usize = player_num.try_into().map_err(|_| fail(input))?;
992    // checked here, since the per-player infoset map is indexed by it
993    if player_num == 0 || player_num > infosets.player.len() {
994        return Err(ValidationError::InvalidPlayerNum.into());
995    }
996    let (declared, child_count) =
997        resolve_infoset(&mut infosets.player[player_num - 1], infoset, declared)?;
998    let outcome_declared = definition.is_some();
999    resolve_outcome(outcomes, num_players, outcome, definition)?;
1000    Ok((
1001        input,
1002        RawPlayer {
1003            name,
1004            player_num,
1005            infoset,
1006            declared,
1007            // filled once the following child nodes are parsed (see PendingNode::finish)
1008            children: Box::default(),
1009            outcome,
1010            outcome_declared,
1011        },
1012        child_count,
1013    ))
1014}
1015
1016/// Parse a terminal node, resolving its outcome
1017fn parse_terminal<'a>(
1018    input: &'a str,
1019    outcomes: &mut Outcomes<'a>,
1020    num_players: usize,
1021) -> Result<(&'a str, RawTerminal<'a>), Error<'a>> {
1022    let (input, (name, outcome, definition)) = (
1023        preceded(multispace1, label),
1024        preceded(multispace1, u64),
1025        // an outcome is either a bare id or a name paired with payoffs (see resolve_outcome)
1026        opt((
1027            preceded(multispace1, label),
1028            preceded(multispace1, commalist(big_rational)),
1029        )),
1030    )
1031        .parse(input)?;
1032    let outcome_declared = definition.is_some();
1033    resolve_outcome(outcomes, num_players, outcome, definition)?;
1034    Ok((
1035        input,
1036        RawTerminal {
1037            name,
1038            outcome,
1039            outcome_declared,
1040        },
1041    ))
1042}
1043
1044fn parse_game(input: &str) -> Result<(&str, ExtensiveFormGame<'_>), Error<'_>> {
1045    let (input, (name, player_names, comment)) = (
1046        preceded(
1047            (
1048                multispace0,
1049                tag("EFG"),
1050                multispace1,
1051                tag("2"),
1052                multispace1,
1053                // Gambit accepts either data-type letter; `D` is legacy but still circulates
1054                one_of("RD"),
1055                multispace1,
1056            ),
1057            label,
1058        ),
1059        preceded(multispace1, spacelist(label)),
1060        opt(preceded(multispace1, label)),
1061    )
1062        .parse(input)?;
1063    let num_players = player_names.len();
1064    let mut infosets = Infosets {
1065        player: (0..num_players).map(|_| HashMap::new()).collect(),
1066        chance: HashMap::new(),
1067    };
1068    let mut outcomes = Outcomes::new();
1069    let (input, nodes, root) = parse_tree(input, &mut infosets, &mut outcomes, num_players)?;
1070    Ok((
1071        input,
1072        ExtensiveFormGame {
1073            name,
1074            player_names: player_names.into(),
1075            comment,
1076            infosets,
1077            outcomes,
1078            nodes,
1079            root,
1080        },
1081    ))
1082}
1083
1084#[cfg(test)]
1085mod tests {
1086    use super::{Error, EscapedStr, ExtensiveFormGame, Node, ValidationError, WriteMode};
1087    use num_rational::BigRational;
1088    use num_traits::One;
1089
1090    /// Parse a game expected to fail validation (or a parse-time infoset check) and return the error
1091    fn validation_err(game: &str) -> ValidationError {
1092        match ExtensiveFormGame::try_from_str(game) {
1093            Err(Error::Validation(err)) => err,
1094            other => panic!("expected a validation error, got {other:?}"),
1095        }
1096    }
1097
1098    #[test]
1099    fn test_big_float() {
1100        let (input, num) = super::big_float("3 ").unwrap();
1101        assert_eq!(input, " ");
1102        assert_eq!(num, BigRational::from_integer(3.into()));
1103
1104        let (input, num) = super::big_float("-2. ").unwrap();
1105        assert_eq!(input, " ");
1106        assert_eq!(num, BigRational::from_integer((-2).into()));
1107
1108        let (input, num) = super::big_float("+.56 ").unwrap();
1109        assert_eq!(input, " ");
1110        assert_eq!(num, BigRational::new(56.into(), 100.into()));
1111
1112        let (input, num) = super::big_float("3.14e-1 ").unwrap();
1113        assert_eq!(input, " ");
1114        assert_eq!(num, BigRational::new(314.into(), 1000.into()));
1115    }
1116
1117    #[test]
1118    fn test_big_rational() {
1119        let (input, num) = super::big_rational("3 ").unwrap();
1120        assert_eq!(input, " ");
1121        assert_eq!(num, BigRational::from_integer(3.into()));
1122
1123        let (input, num) = super::big_rational("99/100 ").unwrap();
1124        assert_eq!(input, " ");
1125        assert_eq!(num, BigRational::new(99.into(), 100.into()));
1126
1127        let (input, num) = super::big_rational(".1e3/+1.e2 ").unwrap();
1128        assert_eq!(input, " ");
1129        assert_eq!(num, BigRational::one());
1130    }
1131
1132    #[test]
1133    fn test_label() {
1134        let (input, label) = super::label(r#""" "#).unwrap();
1135        assert_eq!(input, " ");
1136        assert_eq!(label.escape(), "");
1137
1138        let (input, label) = super::label(r#""normal" "#).unwrap();
1139        assert_eq!(input, " ");
1140        assert_eq!(label.escape(), "normal");
1141
1142        // `\"` is an escaped quote and does not close the label
1143        let (input, label) = super::label(r#""esca\"ped" "#).unwrap();
1144        assert_eq!(input, " ");
1145        assert_eq!(label.escape(), r#"esca\"ped"#);
1146
1147        // a backslash before a non-quote is kept; the final `"` (preceded by `h`) closes the label
1148        let (input, label) = super::label(r#""back\slash" "#).unwrap();
1149        assert_eq!(input, " ");
1150        assert_eq!(label.escape(), r"back\slash");
1151
1152        // a `\` always escapes the immediately following `"`, so a label whose closing quote is
1153        // preceded by a backslash is unterminated (matching gambit)
1154        assert!(super::label(r#""pair\\" "#).is_err());
1155        assert!(super::label(r#""unterminated"#).is_err());
1156        assert!(super::label("noquote").is_err());
1157    }
1158
1159    #[test]
1160    fn simple_test() {
1161        let game_str = r#"
1162        EFG 2 R "General Bayes game, one stage" { "Player 1" "Player 2" }
1163        "A single stage General Bayes Game"
1164
1165        c "ROOT" 1 "(0,1)" { "1G" 0.500000 "1B" 0.500000 } 0
1166        p "" 1 1 "(1,1)" { "H" "L" } 0
1167        t "" 1 "Outcome 1" { 10.000000 2.000000 }
1168        t "" 2 "Outcome 2" { 0.000000 10.000000 }
1169        p "" 2 1 "(2,1)" { "h" "l" } 0
1170        t "" 3 "Outcome 3" { 2.000000 4.000000 }
1171        t "" 4 "Outcome 4" { 4.000000 0.000000 }
1172        "#;
1173        let game = ExtensiveFormGame::try_from_str(game_str).unwrap();
1174        assert_eq!(
1175            game.to_string(),
1176            r#"EFG 2 R "General Bayes game, one stage" { "Player 1" "Player 2" }
1177"A single stage General Bayes Game"
1178
1179c "ROOT" 1 "(0,1)" { "1G" 1/2 "1B" 1/2 } 0
1180p "" 1 1 "(1,1)" { "H" "L" } 0
1181t "" 1 "Outcome 1" { 10 2 }
1182t "" 2 "Outcome 2" { 0 10 }
1183p "" 2 1 "(2,1)" { "h" "l" } 0
1184t "" 3 "Outcome 3" { 2 4 }
1185t "" 4 "Outcome 4" { 4 0 }
1186"#
1187        );
1188
1189        // spot-check a few handle accessors
1190        assert_eq!(game.name().to_string(), "General Bayes game, one stage");
1191        assert_eq!(game.player_names().len(), 2);
1192        let Node::Chance(root) = game.root() else {
1193            panic!("expected a chance root");
1194        };
1195        let labels: Vec<_> = root.actions().map(|(label, _, _)| label.escape()).collect();
1196        assert_eq!(labels, ["1G", "1B"]);
1197    }
1198
1199    #[test]
1200    fn navigates_handles() {
1201        let game_str = r#"EFG 2 R "g" { "Player 1" "Player 2" }
1202p "root" 1 1 "iset" { "L" "R" } 0
1203t "tl" 1 "o1" { 1 2 }
1204t "tr" 2 "o2" { 3 4 }
1205"#;
1206        let game = ExtensiveFormGame::try_from_str(game_str).unwrap();
1207        let Node::Player(root) = game.root() else {
1208            panic!("expected a player root");
1209        };
1210        assert_eq!(root.player_num(), 1);
1211        assert_eq!(root.infoset(), 1);
1212        assert_eq!(root.infoset_name().escape(), "iset");
1213        let labels: Vec<_> = root.actions().map(|(label, _)| label.escape()).collect();
1214        assert_eq!(labels, ["L", "R"]);
1215
1216        let Some(Node::Terminal(left)) = root.action(EscapedStr::new("L")) else {
1217            panic!("expected a terminal after action L");
1218        };
1219        assert_eq!(left.name().escape(), "tl");
1220        assert_eq!(left.outcome(), 1);
1221        assert_eq!(left.outcome_name().map(EscapedStr::escape), Some("o1"));
1222        let payoffs: Vec<_> = left
1223            .outcome_payoffs()
1224            .unwrap()
1225            .iter()
1226            .map(BigRational::to_string)
1227            .collect();
1228        assert_eq!(payoffs, ["1", "2"]);
1229    }
1230
1231    #[test]
1232    fn chance_probabilities_need_not_sum_to_one() {
1233        // matching Gambit, chance probabilities are kept as written and not checked as a distribution
1234        let game = "EFG 2 R \"\" { \"1\" \"2\" }
1235c \"\" 1 \"a\" { \"x\" 9/10 } 0
1236t \"\" 1 \"\" { 0 0 }
1237";
1238        let parsed = ExtensiveFormGame::try_from_str(game).unwrap();
1239        let Node::Chance(root) = parsed.root() else {
1240            panic!("expected a chance root");
1241        };
1242        let (prob, _) = root.action(EscapedStr::new("x")).unwrap();
1243        assert_eq!(prob.to_string(), "9/10");
1244    }
1245
1246    #[test]
1247    fn invalid_player_num() {
1248        // a player number above the player count is rejected at parse time
1249        assert_eq!(
1250            validation_err(
1251                "EFG 2 R \"\" { \"1\" \"2\" }
1252p \"\" 3 1 \"a\" { \"x\" } 0
1253t \"\" 1 { 0 0 }
1254"
1255            ),
1256            ValidationError::InvalidPlayerNum
1257        );
1258    }
1259
1260    #[test]
1261    fn invalid_infoset_names() {
1262        assert_eq!(
1263            validation_err(
1264                "EFG 2 R \"\" { \"1\" \"2\" }
1265p \"\" 1 1 \"a\" { \"x\" } 0
1266p \"\" 1 1 \"b\" { \"x\" } 0
1267t \"\" 1 { 0 0 }
1268"
1269            ),
1270            ValidationError::NonMatchingInfosetNames
1271        );
1272    }
1273
1274    #[test]
1275    fn invalid_chance_infoset_names() {
1276        assert_eq!(
1277            validation_err(
1278                "EFG 2 R \"\" { \"1\" \"2\" }
1279c \"\" 1 \"a\" { \"x\" 1 } 0
1280c \"\" 1 \"b\" { \"x\" 1 } 0
1281t \"\" 1 { 0 0 }
1282"
1283            ),
1284            ValidationError::NonMatchingInfosetNames
1285        );
1286    }
1287
1288    #[test]
1289    fn invalid_infoset_actions() {
1290        // a reordered list no longer matches the first declaration, since order is significant
1291        assert_eq!(
1292            validation_err(
1293                "EFG 2 R \"\" { \"1\" \"2\" }
1294p \"\" 1 1 \"a\" { \"L\" \"R\" } 0
1295t \"\" 1 \"\" { 0 0 }
1296p \"\" 1 1 \"a\" { \"R\" \"L\" } 0
1297t \"\" 2 \"\" { 0 0 }
1298t \"\" 3 \"\" { 0 0 }
1299"
1300            ),
1301            ValidationError::NonMatchingInfosetActions
1302        );
1303    }
1304
1305    #[test]
1306    fn invalid_chance_infoset_actions() {
1307        assert_eq!(
1308            validation_err(
1309                "EFG 2 R \"\" { \"1\" \"2\" }
1310c \"\" 1 \"a\" { \"x\" 1 } 0
1311c \"\" 1 \"a\" { \"y\" 1 } 0
1312t \"\" 1 { 0 0 }
1313"
1314            ),
1315            ValidationError::NonMatchingInfosetActions
1316        );
1317    }
1318
1319    #[test]
1320    fn null_outcome_payoffs() {
1321        assert_eq!(
1322            validation_err(
1323                "EFG 2 R \"\" { \"1\" \"2\" }
1324p \"\" 1 1 \"a\" { \"x\" } 0 \"n\" { 0 0 }
1325t \"\" 1 { 0 0 }
1326"
1327            ),
1328            ValidationError::NullOutcomePayoffs
1329        );
1330    }
1331
1332    #[test]
1333    fn invalid_payoff_number() {
1334        assert_eq!(
1335            validation_err(
1336                "EFG 2 R \"\" { \"1\" \"2\" }
1337t \"\" 1 \"\" { 0 }
1338"
1339            ),
1340            ValidationError::InvalidNumberOfPayoffs
1341        );
1342    }
1343
1344    #[test]
1345    fn non_matching_outcome_names() {
1346        assert_eq!(
1347            validation_err(
1348                "EFG 2 R \"\" { \"1\" \"2\" }
1349p \"\" 1 1 \"a\" { \"x\" } 1 \"b\" { 0 0 }
1350t \"\" 1 \"c\" { 0 0 }
1351"
1352            ),
1353            ValidationError::NonMatchingOutcomeNames
1354        );
1355    }
1356
1357    #[test]
1358    fn non_matching_outcome_payoffs() {
1359        assert_eq!(
1360            validation_err(
1361                "EFG 2 R \"\" { \"1\" \"2\" }
1362p \"\" 1 1 \"a\" { \"x\" } 1 \"\" { 0 0 }
1363t \"\" 1 \"\" { 1 1 }
1364"
1365            ),
1366            ValidationError::NonMatchingOutcomePayoffs
1367        );
1368    }
1369
1370    #[test]
1371    fn undefined_outcome() {
1372        // referencing an outcome by bare id that was never defined is an error
1373        assert_eq!(
1374            validation_err(
1375                "EFG 2 R \"\" { \"1\" \"2\" }
1376t \"\" 5
1377"
1378            ),
1379            ValidationError::UndefinedOutcome
1380        );
1381    }
1382
1383    #[test]
1384    fn undeclared_infoset() {
1385        // omitting the action list before the infoset has ever been declared is an error
1386        assert_eq!(
1387            validation_err(
1388                "EFG 2 R \"\" { \"1\" \"2\" }
1389p \"\" 1 1 0
1390t \"\" 1 { 0 0 }
1391"
1392            ),
1393            ValidationError::UndeclaredInfoset
1394        );
1395    }
1396
1397    #[test]
1398    fn fills_omitted_action_list() {
1399        let game_str = "EFG 2 R \"\" { \"1\" \"2\" }
1400p \"\" 1 1 \"a\" { \"L\" \"R\" } 0
1401t \"\" 1 \"\" { 0 0 }
1402p \"\" 1 1 0
1403t \"\" 2 \"\" { 0 0 }
1404t \"\" 3 \"\" { 0 0 }
1405";
1406        let game = ExtensiveFormGame::try_from_str(game_str).unwrap();
1407        let Node::Player(root) = game.root() else {
1408            panic!("expected a player root");
1409        };
1410        let Some(Node::Player(omitted)) = root.action(EscapedStr::new("R")) else {
1411            panic!("expected a player after action R");
1412        };
1413        // the omitted node inherits the declared label and actions
1414        assert_eq!(omitted.infoset_name().escape(), "a");
1415        let labels: Vec<_> = omitted.actions().map(|(label, _)| label.escape()).collect();
1416        assert_eq!(labels, ["L", "R"]);
1417        // and the omitted form round-trips (the omission is preserved)
1418        let written = game.to_string();
1419        let reparsed = ExtensiveFormGame::try_from_str(written.as_str()).unwrap();
1420        assert_eq!(game, reparsed);
1421    }
1422
1423    #[test]
1424    fn handle_accessors() {
1425        let game_str = r#"EFG 2 R "game" { "P1" "P2" } "the comment"
1426c "chance" 1 "ci" { "a" 1/2 "b" 1/2 } 5 "co" { 1 2 }
1427p "pl1" 1 1 "pi1" { "x" "y" } 6 "po1" { 3 4 }
1428t "ta" 1 "oa" { 7 8 }
1429t "tb" 2 "ob" { 9 10 }
1430p "pl2" 2 2 "pi2" { "x" "y" } 7 "po2" { 5 6 }
1431t "tc" 3 "oc" { 11 12 }
1432t "td" 4 "od" { 13 14 }
1433"#;
1434        let game = ExtensiveFormGame::try_from_str(game_str).unwrap();
1435        assert_eq!(game.comment().map(EscapedStr::escape), Some("the comment"));
1436        // Display covers every node kind's formatting and round-trips
1437        let written = game.to_string();
1438        let reparsed = ExtensiveFormGame::try_from_str(written.as_str()).unwrap();
1439        assert_eq!(game, reparsed);
1440
1441        let Node::Chance(chance) = game.root() else {
1442            panic!("expected a chance root");
1443        };
1444        assert_eq!(chance.name().escape(), "chance");
1445        assert_eq!(chance.infoset(), 1);
1446        assert_eq!(chance.infoset_name().escape(), "ci");
1447        assert_eq!(chance.len(), 2);
1448        assert_eq!(chance.outcome(), 5);
1449        let chance_payoffs: Vec<_> = chance
1450            .outcome_payoffs()
1451            .unwrap()
1452            .iter()
1453            .map(ToString::to_string)
1454            .collect();
1455        assert_eq!(chance_payoffs, ["1", "2"]);
1456        let chance_labels: Vec<_> = chance
1457            .actions()
1458            .map(|(label, _, _)| label.escape())
1459            .collect();
1460        assert_eq!(chance_labels, ["a", "b"]);
1461        assert!(chance.action_at(0).is_some());
1462        assert!(chance.action_at(2).is_none());
1463        assert!(chance.action(EscapedStr::new("none")).is_none());
1464        let (prob, first_child) = chance.action(EscapedStr::new("a")).unwrap();
1465        assert_eq!(prob.to_string(), "1/2");
1466
1467        let Node::Player(player) = first_child else {
1468            panic!("expected a player after chance action a");
1469        };
1470        assert_eq!(player.name().escape(), "pl1");
1471        assert_eq!(player.player_num(), 1);
1472        assert_eq!(player.infoset(), 1);
1473        assert_eq!(player.infoset_name().escape(), "pi1");
1474        assert_eq!(player.len(), 2);
1475        assert_eq!(player.outcome(), 6);
1476        assert_eq!(player.outcome_name().map(EscapedStr::escape), Some("po1"));
1477        let player_payoffs: Vec<_> = player
1478            .outcome_payoffs()
1479            .unwrap()
1480            .iter()
1481            .map(ToString::to_string)
1482            .collect();
1483        assert_eq!(player_payoffs, ["3", "4"]);
1484        let player_labels: Vec<_> = player.actions().map(|(label, _)| label.escape()).collect();
1485        assert_eq!(player_labels, ["x", "y"]);
1486        assert!(player.action(EscapedStr::new("none")).is_none());
1487        assert!(player.action(EscapedStr::new("y")).is_some());
1488        let (label, leaf) = player.action_at(0).unwrap();
1489        assert_eq!(label.escape(), "x");
1490        assert!(player.action_at(2).is_none());
1491
1492        let Node::Terminal(terminal) = leaf else {
1493            panic!("expected a terminal after player action x");
1494        };
1495        assert_eq!(terminal.name().escape(), "ta");
1496        assert_eq!(terminal.outcome(), 1);
1497        assert_eq!(terminal.outcome_name().map(EscapedStr::escape), Some("oa"));
1498        let terminal_payoffs: Vec<_> = terminal
1499            .outcome_payoffs()
1500            .unwrap()
1501            .iter()
1502            .map(ToString::to_string)
1503            .collect();
1504        assert_eq!(terminal_payoffs, ["7", "8"]);
1505    }
1506
1507    #[test]
1508    fn error_display() {
1509        let parse_err = ExtensiveFormGame::try_from_str("not an efg").unwrap_err();
1510        assert!(parse_err.to_string().starts_with("error parsing game at:"));
1511
1512        let bad = "EFG 2 R \"\" { \"1\" \"2\" }\np \"\" 3 1 \"a\" { \"x\" } 0\nt \"\" 1 { 0 0 }\n";
1513        assert_eq!(
1514            ExtensiveFormGame::try_from_str(bad)
1515                .unwrap_err()
1516                .to_string(),
1517            "invalid efg: InvalidPlayerNum"
1518        );
1519        assert_eq!(
1520            ValidationError::UndefinedOutcome.to_string(),
1521            "UndefinedOutcome"
1522        );
1523    }
1524
1525    #[test]
1526    fn accepts_d_data_type() {
1527        // Gambit reads either the R or legacy D data-type letter; Display normalizes to R
1528        let game = ExtensiveFormGame::try_from_str(
1529            "EFG 2 D \"\" { \"1\" \"2\" }\nt \"\" 1 \"\" { 1 2 }\n",
1530        )
1531        .unwrap();
1532        assert!(game.to_string().starts_with("EFG 2 R "));
1533    }
1534
1535    #[test]
1536    fn trailing_input_is_rejected() {
1537        let game = r#"EFG 2 R "" { "1" "2" } t "" 1 "" { 1 2 } trailing"#;
1538        assert!(matches!(
1539            ExtensiveFormGame::try_from_str(game),
1540            Err(Error::Parse("trailing"))
1541        ));
1542    }
1543
1544    #[test]
1545    fn rejects_overflowing_exponent() {
1546        // an exponent that doesn't fit an i32 fails the number parse
1547        assert!(super::big_float("1e99999999999 ").is_err());
1548    }
1549
1550    #[test]
1551    fn rejects_zero_denominator() {
1552        // a zero denominator must surface as a parse error rather than panicking in `Div`
1553        assert!(super::big_rational("1/0 ").is_err());
1554        assert!(
1555            ExtensiveFormGame::try_from_str(
1556                "EFG 2 R \"\" { \"1\" \"2\" }\nt \"\" 1 \"\" { 1/0 2 }\n"
1557            )
1558            .is_err()
1559        );
1560    }
1561
1562    #[test]
1563    fn outcome_defined_then_referenced() {
1564        // once an outcome is defined, later nodes may reference it by bare id
1565        let game = "EFG 2 R \"\" { \"1\" \"2\" }
1566p \"\" 1 1 \"i\" { \"x\" } 1 \"named\" { 3 4 }
1567t \"\" 1
1568";
1569        assert!(ExtensiveFormGame::try_from_str(game).is_ok());
1570    }
1571
1572    #[test]
1573    fn chance_null_outcome_with_payoffs() {
1574        // outcome validation also runs for chance nodes
1575        assert_eq!(
1576            validation_err(
1577                "EFG 2 R \"\" { \"1\" \"2\" }
1578c \"\" 1 \"i\" { \"x\" 1 } 0 \"n\" { 1 2 }
1579t \"\" 1 { 0 0 }
1580"
1581            ),
1582            ValidationError::NullOutcomePayoffs
1583        );
1584    }
1585
1586    #[test]
1587    fn deep_tree_parses_and_drops() {
1588        // a tree far deeper than any call stack could hold must parse, validate, navigate, and drop
1589        // without overflowing, now that the arena makes every path flat rather than recursive
1590        let depth = 200_000;
1591        let mut game = String::with_capacity(depth * 24 + 64);
1592        game.push_str("EFG 2 R \"\" { \"1\" \"2\" }\n");
1593        for _ in 0..depth {
1594            game.push_str("p \"\" 1 1 \"i\" { \"a\" } 0\n");
1595        }
1596        game.push_str("t \"\" 1 \"\" { 0 0 }\n");
1597        let parsed = ExtensiveFormGame::try_from_str(&game).unwrap();
1598        assert!(matches!(parsed.root(), Node::Player(_)));
1599        // dropping the deep tree is itself a flat pass over the arena
1600        drop(parsed);
1601    }
1602
1603    #[test]
1604    fn tolerates_flexible_whitespace() {
1605        // whitespace is not significant: braces need no padding, and payoff commas need no space
1606        let game =
1607            ExtensiveFormGame::try_from_str("EFG 2 R \"\" {\"1\" \"2\"}\nt \"\" 1 \"\" {1,2}\n")
1608                .unwrap();
1609        assert_eq!(game.player_names().len(), 2);
1610        let Node::Terminal(root) = game.root() else {
1611            panic!("expected a terminal root");
1612        };
1613        let payoffs: Vec<_> = root
1614            .outcome_payoffs()
1615            .unwrap()
1616            .iter()
1617            .map(BigRational::to_string)
1618            .collect();
1619        assert_eq!(payoffs, ["1", "2"]);
1620        // a comma padded with spaces is equally acceptable
1621        assert!(
1622            ExtensiveFormGame::try_from_str(
1623                "EFG 2 R \"\" { \"1\" \"2\" }\nt \"\" 1 \"\" { 1 , 2 }\n"
1624            )
1625            .is_ok()
1626        );
1627    }
1628
1629    #[test]
1630    fn chance_outcome_name() {
1631        // a chance node may carry an outcome name (Gambit writes and reads one)
1632        let game_str = "EFG 2 R \"\" { \"1\" \"2\" }
1633c \"\" 1 \"i\" { \"a\" 1/2 \"b\" 1/2 } 1 \"oname\" { 3 4 }
1634t \"\" 1
1635t \"\" 1
1636";
1637        let game = ExtensiveFormGame::try_from_str(game_str).unwrap();
1638        let Node::Chance(root) = game.root() else {
1639            panic!("expected a chance root");
1640        };
1641        assert_eq!(root.outcome_name().map(EscapedStr::escape), Some("oname"));
1642        // the name is preserved through a Display round-trip
1643        let written = game.to_string();
1644        let reparsed = ExtensiveFormGame::try_from_str(written.as_str()).unwrap();
1645        assert_eq!(game, reparsed);
1646    }
1647
1648    #[test]
1649    fn terminal_null_and_referenced_outcomes() {
1650        // `t "" 0` (null outcome, no payoffs) and a terminal that only references an outcome
1651        // defined on another node both parse and round-trip
1652        let game_str = "EFG 2 R \"\" { \"1\" \"2\" }
1653p \"\" 1 1 \"i\" { \"L\" \"M\" \"R\" } 0
1654t \"a\" 0
1655t \"b\" 1 \"obname\" { 3 4 }
1656t \"c\" 1
1657";
1658        let game = ExtensiveFormGame::try_from_str(game_str).unwrap();
1659        let Node::Player(root) = game.root() else {
1660            panic!("expected a player root");
1661        };
1662        let Some(Node::Terminal(null_term)) = root.action(EscapedStr::new("L")) else {
1663            panic!("expected a terminal after action L");
1664        };
1665        // the null outcome resolves to no payoffs and no name
1666        assert_eq!(null_term.outcome(), 0);
1667        assert!(null_term.outcome_payoffs().is_none());
1668        assert!(null_term.outcome_name().is_none());
1669        let Some(Node::Terminal(referenced)) = root.action(EscapedStr::new("R")) else {
1670            panic!("expected a terminal after action R");
1671        };
1672        // the referencing terminal resolves through the shared outcome to the payoffs "b" defined
1673        assert_eq!(referenced.outcome(), 1);
1674        let payoffs: Vec<_> = referenced
1675            .outcome_payoffs()
1676            .unwrap()
1677            .iter()
1678            .map(BigRational::to_string)
1679            .collect();
1680        assert_eq!(payoffs, ["3", "4"]);
1681        // and the game round-trips
1682        let written = game.to_string();
1683        let reparsed = ExtensiveFormGame::try_from_str(written.as_str()).unwrap();
1684        assert_eq!(game, reparsed);
1685    }
1686
1687    #[test]
1688    fn write_modes() {
1689        // infoset 1 and outcome 1 are each declared more than minimally (root+mid repeat the
1690        // infoset block, "a"+"b" repeat the outcome), and "c" references the outcome by id
1691        let input = "EFG 2 R \"\" { \"1\" \"2\" }
1692p \"root\" 1 1 \"iset\" { \"L\" \"R\" } 0
1693p \"mid\" 1 1 \"iset\" { \"L\" \"R\" } 0
1694t \"a\" 1 \"out\" { 1 2 }
1695t \"b\" 1 \"out\" { 1 2 }
1696t \"c\" 1
1697";
1698        let game = ExtensiveFormGame::try_from_str(input).unwrap();
1699
1700        // Display defaults to Faithful, which reproduces the parsed declare/reference structure
1701        assert_eq!(
1702            game.to_string(),
1703            game.display(WriteMode::Faithful).to_string()
1704        );
1705        let faithful = game.display(WriteMode::Faithful).to_string();
1706        assert_eq!(ExtensiveFormGame::try_from_str(&faithful).unwrap(), game);
1707
1708        // every mode is valid and resolves to the same game, so its exhaustive rendering matches
1709        let canonical = game.display(WriteMode::Exhaustive).to_string();
1710        for mode in [
1711            WriteMode::Minimal,
1712            WriteMode::Faithful,
1713            WriteMode::Exhaustive,
1714        ] {
1715            let out = game.display(mode).to_string();
1716            let reparsed = ExtensiveFormGame::try_from_str(&out).unwrap();
1717            assert_eq!(
1718                reparsed.display(WriteMode::Exhaustive).to_string(),
1719                canonical
1720            );
1721        }
1722
1723        // Minimal declares each block once; Exhaustive writes them on every node
1724        let minimal = game.display(WriteMode::Minimal).to_string();
1725        assert!(minimal.len() < faithful.len());
1726        assert!(faithful.len() < canonical.len());
1727    }
1728}