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    /// `label` is matched against the unescaped form of each action's label. Labels need not be
476    /// unique within an infoset, so this returns the first match.
477    #[must_use]
478    pub fn action(self, label: &str) -> Option<(&'g BigRational, Node<'a, 'g>)> {
479        self.actions()
480            .find(|(name, _, _)| name.unescape().eq(label.chars()))
481            .map(|(_, prob, next)| (prob, next))
482    }
483
484    /// The number of actions (always at least one)
485    #[allow(clippy::len_without_is_empty)]
486    #[must_use]
487    pub fn len(self) -> usize {
488        self.raw.children.len()
489    }
490
491    /// The name, probability, and child for the action at the given index
492    #[must_use]
493    pub fn action_at(
494        self,
495        index: usize,
496    ) -> Option<(&'a EscapedStr, &'g BigRational, Node<'a, 'g>)> {
497        let (_, actions) = self.entry();
498        let (label, prob) = actions.get(index)?;
499        let &child = self.raw.children.get(index)?;
500        Some((*label, prob, self.game.wrap(child)))
501    }
502
503    /// The outcome id
504    #[must_use]
505    pub fn outcome(self) -> u64 {
506        self.raw.outcome
507    }
508
509    /// The name of the outcome
510    ///
511    /// `None` for the null (0) outcome.
512    #[must_use]
513    pub fn outcome_name(self) -> Option<&'a EscapedStr> {
514        self.game.outcome_name(self.raw.outcome)
515    }
516
517    /// Outcome payoffs for this node
518    ///
519    /// These are added to every player's payoff for traversing through this node. `None` only for
520    /// the null (0) outcome.
521    #[must_use]
522    pub fn outcome_payoffs(self) -> Option<&'g [BigRational]> {
523        self.game.outcome_payoffs(self.raw.outcome)
524    }
525}
526
527/// A player node in the game tree
528///
529/// A player node represents a place where one of the players chooses what happens next.
530#[derive(Clone, Copy)]
531pub struct Player<'a, 'g> {
532    game: &'g ExtensiveFormGame<'a>,
533    raw: &'g RawPlayer<'a>,
534}
535
536impl<'a, 'g> Player<'a, 'g> {
537    fn entry(self) -> &'g PlayerInfoset<'a> {
538        &self.game.infosets.player[self.raw.player_num - 1][&self.raw.infoset]
539    }
540
541    /// The name of the node
542    #[must_use]
543    pub fn name(self) -> &'a EscapedStr {
544        self.raw.name
545    }
546
547    /// The player acting at this node
548    ///
549    /// This will always be between 1 and the number of players.
550    #[must_use]
551    pub fn player_num(self) -> usize {
552        self.raw.player_num
553    }
554
555    /// The infoset id for this node and player
556    #[must_use]
557    pub fn infoset(self) -> u64 {
558        self.raw.infoset
559    }
560
561    /// The infoset's label
562    #[must_use]
563    pub fn infoset_name(self) -> &'a EscapedStr {
564        self.entry().0
565    }
566
567    /// All the actions a player can take with their names and resulting nodes
568    pub fn actions(self) -> impl Iterator<Item = (&'a EscapedStr, Node<'a, 'g>)> + 'g {
569        let (_, labels) = self.entry();
570        let game = self.game;
571        labels
572            .iter()
573            .zip(self.raw.children.iter())
574            .map(move |(label, &child)| (*label, game.wrap(child)))
575    }
576
577    /// The child reached by the action with the given label
578    ///
579    /// `label` is matched against the unescaped form of each action's label. Labels need not be
580    /// unique within an infoset, so this returns the first match.
581    #[must_use]
582    pub fn action(self, label: &str) -> Option<Node<'a, 'g>> {
583        self.actions()
584            .find(|(name, _)| name.unescape().eq(label.chars()))
585            .map(|(_, next)| next)
586    }
587
588    /// The number of actions (always at least one)
589    #[allow(clippy::len_without_is_empty)]
590    #[must_use]
591    pub fn len(self) -> usize {
592        self.raw.children.len()
593    }
594
595    /// The name and child for the action at the given index
596    #[must_use]
597    pub fn action_at(self, index: usize) -> Option<(&'a EscapedStr, Node<'a, 'g>)> {
598        let (_, actions) = self.entry();
599        let &label = actions.get(index)?;
600        let &child = self.raw.children.get(index)?;
601        Some((label, self.game.wrap(child)))
602    }
603
604    /// The outcome id
605    #[must_use]
606    pub fn outcome(self) -> u64 {
607        self.raw.outcome
608    }
609
610    /// The name of the outcome
611    ///
612    /// `None` for the null (0) outcome.
613    #[must_use]
614    pub fn outcome_name(self) -> Option<&'a EscapedStr> {
615        self.game.outcome_name(self.raw.outcome)
616    }
617
618    /// Payoffs associated with the outcome
619    ///
620    /// `None` only for the null (0) outcome.
621    #[must_use]
622    pub fn outcome_payoffs(self) -> Option<&'g [BigRational]> {
623        self.game.outcome_payoffs(self.raw.outcome)
624    }
625}
626
627/// A terminal node represents the end of a game
628///
629/// Terminal nodes simply assign payoffs to every player in the game
630#[derive(Clone, Copy)]
631pub struct Terminal<'a, 'g> {
632    game: &'g ExtensiveFormGame<'a>,
633    raw: &'g RawTerminal<'a>,
634}
635
636impl<'a, 'g> Terminal<'a, 'g> {
637    /// The name of this node
638    #[must_use]
639    pub fn name(self) -> &'a EscapedStr {
640        self.raw.name
641    }
642
643    /// The outcome id
644    #[must_use]
645    pub fn outcome(self) -> u64 {
646        self.raw.outcome
647    }
648
649    /// The name of this outcome
650    ///
651    /// `None` for the null (0) outcome.
652    #[must_use]
653    pub fn outcome_name(self) -> Option<&'a EscapedStr> {
654        self.game.outcome_name(self.raw.outcome)
655    }
656
657    /// The payoffs to every player
658    ///
659    /// `None` only for the null (0) outcome — a terminal with no outcome attached.
660    #[must_use]
661    pub fn outcome_payoffs(self) -> Option<&'g [BigRational]> {
662        self.game.outcome_payoffs(self.raw.outcome)
663    }
664}
665
666fn negate(input: &str) -> IResult<&str, bool> {
667    let (input, res) = opt(one_of("+-")).parse(input)?;
668    Ok((input, res == Some('-')))
669}
670
671fn fail(input: &str) -> nom::Err<nom::error::Error<&str>> {
672    nom::Err::Error(nom::error::Error::new(input, ErrorKind::Fail))
673}
674
675/// The largest exponent magnitude accepted, bounding the cost of the exact `10^exp` below.
676const MAX_ABS_EXPONENT: i32 = 10_000;
677
678fn big_float(input: &str) -> IResult<&str, BigRational> {
679    let (res_input, (main_neg, (int, dec), exp)) = (
680        negate,
681        alt((
682            pair(
683                digit1,
684                map(opt(preceded(char('.'), digit0)), Option::unwrap_or_default),
685            ),
686            separated_pair(digit0, char('.'), digit1),
687        )),
688        opt(preceded(one_of("eE"), pair(negate, digit1))),
689    )
690        .parse(input)?;
691    let mut res = if int.is_empty() {
692        BigRational::zero()
693    } else {
694        BigRational::from_integer(int.parse().unwrap())
695    };
696    if !dec.is_empty() {
697        let pow: u32 = dec.len().try_into().map_err(|_| fail(input))?;
698        res += BigRational::new(dec.parse().unwrap(), BigInt::from(10).pow(pow));
699    }
700    if let Some((neg, exp)) = exp {
701        let exp: i32 = exp.parse().map_err(|_| fail(input))?;
702        if exp > MAX_ABS_EXPONENT {
703            return Err(fail(input));
704        }
705        res *= BigRational::from_integer(10.into()).pow(if neg { -exp } else { exp });
706    }
707    if main_neg {
708        res = -res;
709    }
710    Ok((res_input, res))
711}
712
713fn big_rational(input: &str) -> IResult<&str, BigRational> {
714    let (rest, (num, denom)) = pair(big_float, opt(preceded(char('/'), big_float))).parse(input)?;
715    match denom {
716        // a zero denominator would panic in num-rational's `Div`; reject it as a parse error
717        Some(denom) if denom.is_zero() => Err(fail(input)),
718        Some(denom) => Ok((rest, num / denom)),
719        None => Ok((rest, num)),
720    }
721}
722
723fn label(input: &str) -> IResult<&str, &EscapedStr> {
724    map(
725        delimited(
726            char('"'),
727            // the body is any mix of the `\"` escape and ordinary non-quote characters; matching
728            // `\"` first means a lone `\` is ordinary and only a `"` after a `\` is escaped
729            recognize(many0(alt((tag(r#"\""#), recognize(none_of("\"")))))),
730            char('"'),
731        ),
732        EscapedStr::new,
733    )
734    .parse(input)
735}
736
737fn spacelist<'a, O, E, F>(f: F) -> impl Parser<&'a str, Output = Vec<O>, Error = E>
738where
739    F: Parser<&'a str, Output = O, Error = E>,
740    E: ParseError<&'a str>,
741{
742    delimited(
743        pair(char('{'), multispace0),
744        separated_list1(multispace1, f),
745        pair(multispace0, char('}')),
746    )
747}
748
749fn commalist<'a, O, E, F>(f: F) -> impl Parser<&'a str, Output = Vec<O>, Error = E>
750where
751    F: Parser<&'a str, Output = O, Error = E>,
752    E: ParseError<&'a str>,
753{
754    delimited(
755        pair(char('{'), multispace0),
756        separated_list1((multispace0, opt(char(',')), multispace0), f),
757        pair(multispace0, char('}')),
758    )
759}
760
761/// A parent node whose header is parsed but whose children are still being collected.
762struct PendingNode<'a> {
763    node: RawNode<'a>,
764    child_count: usize,
765    children: Vec<NodeId>,
766}
767
768impl<'a> PendingNode<'a> {
769    fn finish(self) -> RawNode<'a> {
770        let PendingNode {
771            mut node, children, ..
772        } = self;
773        match &mut node {
774            RawNode::Chance(chance) => chance.children = children.into(),
775            RawNode::Player(player) => player.children = children.into(),
776            // only chance and player nodes gather children, so only they are ever pending
777            RawNode::Terminal(_) => unreachable!("terminal nodes are never pending"),
778        }
779        node
780    }
781}
782
783/// Record or check an infoset declaration, returning whether the block was written here and the
784/// action count. A first declaration is inserted, a repeat must match exactly, an omission inherits.
785fn resolve_infoset<'a, A: PartialEq>(
786    map: &mut HashMap<u64, (&'a EscapedStr, Box<[A]>)>,
787    infoset: u64,
788    declared: Option<(&'a EscapedStr, Vec<A>)>,
789) -> Result<(bool, usize), Error<'a>> {
790    if let Some((name, actions)) = declared {
791        match map.entry(infoset) {
792            // a first declaration is stored (boxed), a repeat is only compared against the stored
793            // one, so the parsed `Vec` is never boxed just to be dropped
794            Entry::Vacant(ent) => {
795                let count = actions.len();
796                ent.insert((name, actions.into()));
797                Ok((true, count))
798            }
799            Entry::Occupied(ent) => {
800                let (stored_name, stored_actions) = ent.get();
801                if *stored_name != name {
802                    Err(ValidationError::NonMatchingInfosetNames.into())
803                } else if **stored_actions != *actions {
804                    Err(ValidationError::NonMatchingInfosetActions.into())
805                } else {
806                    Ok((true, actions.len()))
807                }
808            }
809        }
810    } else {
811        let (_, actions) = map
812            .get(&infoset)
813            .ok_or(ValidationError::UndeclaredInfoset)?;
814        Ok((false, actions.len()))
815    }
816}
817
818/// Record or check a node's outcome, mirroring [`resolve_infoset`]. A node either defines the
819/// outcome with a name and payoffs, or references it by bare id: the first definition is stored, a
820/// repeat definition must match it exactly, and a reference must name an already-defined outcome.
821/// The null (0) outcome carries no data and is never stored.
822fn resolve_outcome<'a>(
823    outcomes: &mut Outcomes<'a>,
824    num_players: usize,
825    outcome: u64,
826    definition: Option<(&'a EscapedStr, Vec<BigRational>)>,
827) -> Result<(), Error<'a>> {
828    if let Some((name, payoffs)) = definition {
829        if outcome == 0 {
830            Err(ValidationError::NullOutcomePayoffs.into())
831        } else if payoffs.len() != num_players {
832            Err(ValidationError::InvalidNumberOfPayoffs.into())
833        } else {
834            match outcomes.entry(outcome) {
835                // a first definition is stored (boxed), a repeat is only compared against the
836                // stored one, so the parsed `Vec` is never boxed just to be dropped
837                Entry::Vacant(ent) => {
838                    ent.insert((name, payoffs.into()));
839                    Ok(())
840                }
841                Entry::Occupied(ent) => {
842                    let (stored_name, stored_payoffs) = ent.get();
843                    if *stored_name != name {
844                        Err(ValidationError::NonMatchingOutcomeNames.into())
845                    } else if **stored_payoffs != *payoffs {
846                        Err(ValidationError::NonMatchingOutcomePayoffs.into())
847                    } else {
848                        Ok(())
849                    }
850                }
851            }
852        }
853    } else if outcome != 0 && !outcomes.contains_key(&outcome) {
854        // a bare id references an outcome that must already be defined; the null (0) id is fine
855        Err(ValidationError::UndefinedOutcome.into())
856    } else {
857        Ok(())
858    }
859}
860
861/// Parse the whole game tree into a flat arena, returning the nodes and the root's id.
862fn parse_tree<'a>(
863    mut input: &'a str,
864    infosets: &mut Infosets<'a>,
865    outcomes: &mut Outcomes<'a>,
866    num_players: usize,
867) -> Result<(&'a str, Box<[RawNode<'a>]>, NodeId), Error<'a>> {
868    // finished nodes, in the post-order they complete (every child precedes its parent)
869    let mut nodes: Vec<RawNode<'a>> = Vec::new();
870    // parents still gathering their children
871    let mut stack: Vec<PendingNode<'a>> = Vec::new();
872
873    loop {
874        let (rest, style) = preceded(multispace1, one_of("cpt")).parse(input)?;
875        input = rest;
876        // a chance or player node opens a frame; a terminal completes immediately
877        let mut completed = match style {
878            'c' => {
879                let (rest, chance, child_count) =
880                    parse_chance(input, infosets, outcomes, num_players)?;
881                input = rest;
882                stack.push(PendingNode {
883                    node: RawNode::Chance(chance),
884                    child_count,
885                    children: Vec::with_capacity(child_count),
886                });
887                continue;
888            }
889            'p' => {
890                let (rest, player, child_count) =
891                    parse_player(input, infosets, outcomes, num_players)?;
892                input = rest;
893                stack.push(PendingNode {
894                    node: RawNode::Player(player),
895                    child_count,
896                    children: Vec::with_capacity(child_count),
897                });
898                continue;
899            }
900            't' => {
901                let (rest, term) = parse_terminal(input, outcomes, num_players)?;
902                input = rest;
903                push_node(&mut nodes, RawNode::Terminal(term))
904            }
905            // `one_of("cpt")` only ever yields one of these three characters
906            _ => unreachable!(),
907        };
908
909        // attach the finished node to its waiting parent, finishing parents that fill up in turn
910        loop {
911            let Some(pending) = stack.last_mut() else {
912                // nothing is waiting, so this node is the root and the tree is complete
913                return Ok((input, nodes.into(), completed));
914            };
915            pending.children.push(completed);
916            if pending.children.len() < pending.child_count {
917                break;
918            }
919            completed = push_node(&mut nodes, stack.pop().unwrap().finish());
920        }
921    }
922}
923
924/// Append a finished node to the arena and return its id
925fn push_node<'a>(nodes: &mut Vec<RawNode<'a>>, node: RawNode<'a>) -> NodeId {
926    let id = NodeId(nodes.len());
927    nodes.push(node);
928    id
929}
930
931/// Parse a chance node's header, resolving its outcome and returning the node (children still
932/// empty) and its child count
933fn parse_chance<'a>(
934    input: &'a str,
935    infosets: &mut Infosets<'a>,
936    outcomes: &mut Outcomes<'a>,
937    num_players: usize,
938) -> Result<(&'a str, RawChance<'a>, usize), Error<'a>> {
939    let (input, (name, infoset, declared, outcome, definition)) = (
940        preceded(multispace1, label),
941        preceded(multispace1, u64),
942        opt((
943            preceded(multispace1, label),
944            preceded(
945                multispace1,
946                spacelist(separated_pair(label, multispace1, big_rational)),
947            ),
948        )),
949        preceded(multispace1, u64),
950        // an outcome is either a bare id or a name paired with payoffs (see resolve_outcome)
951        opt((
952            preceded(multispace1, label),
953            preceded(multispace1, commalist(big_rational)),
954        )),
955    )
956        .parse(input)?;
957    let (declared, child_count) = resolve_infoset(&mut infosets.chance, infoset, declared)?;
958    let outcome_declared = definition.is_some();
959    resolve_outcome(outcomes, num_players, outcome, definition)?;
960    Ok((
961        input,
962        RawChance {
963            name,
964            infoset,
965            declared,
966            // filled once the following child nodes are parsed (see PendingNode::finish)
967            children: Box::default(),
968            outcome,
969            outcome_declared,
970        },
971        child_count,
972    ))
973}
974
975/// Parse a player node's header, resolving its outcome and returning the node (children still
976/// empty) and its child count
977fn parse_player<'a>(
978    input: &'a str,
979    infosets: &mut Infosets<'a>,
980    outcomes: &mut Outcomes<'a>,
981    num_players: usize,
982) -> Result<(&'a str, RawPlayer<'a>, usize), Error<'a>> {
983    let (input, (name, player_num, infoset, declared, outcome, definition)) = (
984        preceded(multispace1, label),
985        preceded(multispace1, u64),
986        preceded(multispace1, u64),
987        opt((
988            preceded(multispace1, label),
989            preceded(multispace1, spacelist(label)),
990        )),
991        preceded(multispace1, u64),
992        // an outcome is either a bare id or a name paired with payoffs (see resolve_outcome)
993        opt((
994            preceded(multispace1, label),
995            preceded(multispace1, commalist(big_rational)),
996        )),
997    )
998        .parse(input)?;
999    let player_num: usize = player_num.try_into().map_err(|_| fail(input))?;
1000    // checked here, since the per-player infoset map is indexed by it
1001    if player_num == 0 || player_num > infosets.player.len() {
1002        return Err(ValidationError::InvalidPlayerNum.into());
1003    }
1004    let (declared, child_count) =
1005        resolve_infoset(&mut infosets.player[player_num - 1], infoset, declared)?;
1006    let outcome_declared = definition.is_some();
1007    resolve_outcome(outcomes, num_players, outcome, definition)?;
1008    Ok((
1009        input,
1010        RawPlayer {
1011            name,
1012            player_num,
1013            infoset,
1014            declared,
1015            // filled once the following child nodes are parsed (see PendingNode::finish)
1016            children: Box::default(),
1017            outcome,
1018            outcome_declared,
1019        },
1020        child_count,
1021    ))
1022}
1023
1024/// Parse a terminal node, resolving its outcome
1025fn parse_terminal<'a>(
1026    input: &'a str,
1027    outcomes: &mut Outcomes<'a>,
1028    num_players: usize,
1029) -> Result<(&'a str, RawTerminal<'a>), Error<'a>> {
1030    let (input, (name, outcome, definition)) = (
1031        preceded(multispace1, label),
1032        preceded(multispace1, u64),
1033        // an outcome is either a bare id or a name paired with payoffs (see resolve_outcome)
1034        opt((
1035            preceded(multispace1, label),
1036            preceded(multispace1, commalist(big_rational)),
1037        )),
1038    )
1039        .parse(input)?;
1040    let outcome_declared = definition.is_some();
1041    resolve_outcome(outcomes, num_players, outcome, definition)?;
1042    Ok((
1043        input,
1044        RawTerminal {
1045            name,
1046            outcome,
1047            outcome_declared,
1048        },
1049    ))
1050}
1051
1052fn parse_game(input: &str) -> Result<(&str, ExtensiveFormGame<'_>), Error<'_>> {
1053    let (input, (name, player_names, comment)) = (
1054        preceded(
1055            (
1056                multispace0,
1057                tag("EFG"),
1058                multispace1,
1059                tag("2"),
1060                multispace1,
1061                // Gambit accepts either data-type letter; `D` is legacy but still circulates
1062                one_of("RD"),
1063                multispace1,
1064            ),
1065            label,
1066        ),
1067        preceded(multispace1, spacelist(label)),
1068        opt(preceded(multispace1, label)),
1069    )
1070        .parse(input)?;
1071    let num_players = player_names.len();
1072    let mut infosets = Infosets {
1073        player: (0..num_players).map(|_| HashMap::new()).collect(),
1074        chance: HashMap::new(),
1075    };
1076    let mut outcomes = Outcomes::new();
1077    let (input, nodes, root) = parse_tree(input, &mut infosets, &mut outcomes, num_players)?;
1078    Ok((
1079        input,
1080        ExtensiveFormGame {
1081            name,
1082            player_names: player_names.into(),
1083            comment,
1084            infosets,
1085            outcomes,
1086            nodes,
1087            root,
1088        },
1089    ))
1090}
1091
1092#[cfg(test)]
1093mod tests {
1094    use super::{Error, EscapedStr, ExtensiveFormGame, Node, ValidationError, WriteMode};
1095    use num_rational::BigRational;
1096    use num_traits::One;
1097
1098    /// Parse a game expected to fail validation (or a parse-time infoset check) and return the error
1099    fn validation_err(game: &str) -> ValidationError {
1100        match ExtensiveFormGame::try_from_str(game) {
1101            Err(Error::Validation(err)) => err,
1102            other => panic!("expected a validation error, got {other:?}"),
1103        }
1104    }
1105
1106    #[test]
1107    fn test_big_float() {
1108        let (input, num) = super::big_float("3 ").unwrap();
1109        assert_eq!(input, " ");
1110        assert_eq!(num, BigRational::from_integer(3.into()));
1111
1112        let (input, num) = super::big_float("-2. ").unwrap();
1113        assert_eq!(input, " ");
1114        assert_eq!(num, BigRational::from_integer((-2).into()));
1115
1116        let (input, num) = super::big_float("+.56 ").unwrap();
1117        assert_eq!(input, " ");
1118        assert_eq!(num, BigRational::new(56.into(), 100.into()));
1119
1120        let (input, num) = super::big_float("3.14e-1 ").unwrap();
1121        assert_eq!(input, " ");
1122        assert_eq!(num, BigRational::new(314.into(), 1000.into()));
1123    }
1124
1125    #[test]
1126    fn test_big_rational() {
1127        let (input, num) = super::big_rational("3 ").unwrap();
1128        assert_eq!(input, " ");
1129        assert_eq!(num, BigRational::from_integer(3.into()));
1130
1131        let (input, num) = super::big_rational("99/100 ").unwrap();
1132        assert_eq!(input, " ");
1133        assert_eq!(num, BigRational::new(99.into(), 100.into()));
1134
1135        let (input, num) = super::big_rational(".1e3/+1.e2 ").unwrap();
1136        assert_eq!(input, " ");
1137        assert_eq!(num, BigRational::one());
1138    }
1139
1140    #[test]
1141    fn test_label() {
1142        let (input, label) = super::label(r#""" "#).unwrap();
1143        assert_eq!(input, " ");
1144        assert_eq!(label.escape(), "");
1145
1146        let (input, label) = super::label(r#""normal" "#).unwrap();
1147        assert_eq!(input, " ");
1148        assert_eq!(label.escape(), "normal");
1149
1150        // `\"` is an escaped quote and does not close the label
1151        let (input, label) = super::label(r#""esca\"ped" "#).unwrap();
1152        assert_eq!(input, " ");
1153        assert_eq!(label.escape(), r#"esca\"ped"#);
1154
1155        // a backslash before a non-quote is kept; the final `"` (preceded by `h`) closes the label
1156        let (input, label) = super::label(r#""back\slash" "#).unwrap();
1157        assert_eq!(input, " ");
1158        assert_eq!(label.escape(), r"back\slash");
1159
1160        // a `\` always escapes the immediately following `"`, so a label whose closing quote is
1161        // preceded by a backslash is unterminated (matching gambit)
1162        assert!(super::label(r#""pair\\" "#).is_err());
1163        assert!(super::label(r#""unterminated"#).is_err());
1164        assert!(super::label("noquote").is_err());
1165    }
1166
1167    #[test]
1168    fn simple_test() {
1169        let game_str = r#"
1170        EFG 2 R "General Bayes game, one stage" { "Player 1" "Player 2" }
1171        "A single stage General Bayes Game"
1172
1173        c "ROOT" 1 "(0,1)" { "1G" 0.500000 "1B" 0.500000 } 0
1174        p "" 1 1 "(1,1)" { "H" "L" } 0
1175        t "" 1 "Outcome 1" { 10.000000 2.000000 }
1176        t "" 2 "Outcome 2" { 0.000000 10.000000 }
1177        p "" 2 1 "(2,1)" { "h" "l" } 0
1178        t "" 3 "Outcome 3" { 2.000000 4.000000 }
1179        t "" 4 "Outcome 4" { 4.000000 0.000000 }
1180        "#;
1181        let game = ExtensiveFormGame::try_from_str(game_str).unwrap();
1182        assert_eq!(
1183            game.to_string(),
1184            r#"EFG 2 R "General Bayes game, one stage" { "Player 1" "Player 2" }
1185"A single stage General Bayes Game"
1186
1187c "ROOT" 1 "(0,1)" { "1G" 1/2 "1B" 1/2 } 0
1188p "" 1 1 "(1,1)" { "H" "L" } 0
1189t "" 1 "Outcome 1" { 10 2 }
1190t "" 2 "Outcome 2" { 0 10 }
1191p "" 2 1 "(2,1)" { "h" "l" } 0
1192t "" 3 "Outcome 3" { 2 4 }
1193t "" 4 "Outcome 4" { 4 0 }
1194"#
1195        );
1196
1197        // spot-check a few handle accessors
1198        assert_eq!(game.name().to_string(), "General Bayes game, one stage");
1199        assert_eq!(game.player_names().len(), 2);
1200        let Node::Chance(root) = game.root() else {
1201            panic!("expected a chance root");
1202        };
1203        let labels: Vec<_> = root.actions().map(|(label, _, _)| label.escape()).collect();
1204        assert_eq!(labels, ["1G", "1B"]);
1205    }
1206
1207    #[test]
1208    fn navigates_handles() {
1209        let game_str = r#"EFG 2 R "g" { "Player 1" "Player 2" }
1210p "root" 1 1 "iset" { "L" "R" } 0
1211t "tl" 1 "o1" { 1 2 }
1212t "tr" 2 "o2" { 3 4 }
1213"#;
1214        let game = ExtensiveFormGame::try_from_str(game_str).unwrap();
1215        let Node::Player(root) = game.root() else {
1216            panic!("expected a player root");
1217        };
1218        assert_eq!(root.player_num(), 1);
1219        assert_eq!(root.infoset(), 1);
1220        assert_eq!(root.infoset_name().escape(), "iset");
1221        let labels: Vec<_> = root.actions().map(|(label, _)| label.escape()).collect();
1222        assert_eq!(labels, ["L", "R"]);
1223
1224        let Some(Node::Terminal(left)) = root.action("L") else {
1225            panic!("expected a terminal after action L");
1226        };
1227        assert_eq!(left.name().escape(), "tl");
1228        assert_eq!(left.outcome(), 1);
1229        assert_eq!(left.outcome_name().map(EscapedStr::escape), Some("o1"));
1230        let payoffs: Vec<_> = left
1231            .outcome_payoffs()
1232            .unwrap()
1233            .iter()
1234            .map(BigRational::to_string)
1235            .collect();
1236        assert_eq!(payoffs, ["1", "2"]);
1237    }
1238
1239    #[test]
1240    fn chance_probabilities_need_not_sum_to_one() {
1241        // matching Gambit, chance probabilities are kept as written and not checked as a distribution
1242        let game = "EFG 2 R \"\" { \"1\" \"2\" }
1243c \"\" 1 \"a\" { \"x\" 9/10 } 0
1244t \"\" 1 \"\" { 0 0 }
1245";
1246        let parsed = ExtensiveFormGame::try_from_str(game).unwrap();
1247        let Node::Chance(root) = parsed.root() else {
1248            panic!("expected a chance root");
1249        };
1250        let (prob, _) = root.action("x").unwrap();
1251        assert_eq!(prob.to_string(), "9/10");
1252    }
1253
1254    #[test]
1255    fn invalid_player_num() {
1256        // a player number above the player count is rejected at parse time
1257        assert_eq!(
1258            validation_err(
1259                "EFG 2 R \"\" { \"1\" \"2\" }
1260p \"\" 3 1 \"a\" { \"x\" } 0
1261t \"\" 1 { 0 0 }
1262"
1263            ),
1264            ValidationError::InvalidPlayerNum
1265        );
1266    }
1267
1268    #[test]
1269    fn invalid_infoset_names() {
1270        assert_eq!(
1271            validation_err(
1272                "EFG 2 R \"\" { \"1\" \"2\" }
1273p \"\" 1 1 \"a\" { \"x\" } 0
1274p \"\" 1 1 \"b\" { \"x\" } 0
1275t \"\" 1 { 0 0 }
1276"
1277            ),
1278            ValidationError::NonMatchingInfosetNames
1279        );
1280    }
1281
1282    #[test]
1283    fn invalid_chance_infoset_names() {
1284        assert_eq!(
1285            validation_err(
1286                "EFG 2 R \"\" { \"1\" \"2\" }
1287c \"\" 1 \"a\" { \"x\" 1 } 0
1288c \"\" 1 \"b\" { \"x\" 1 } 0
1289t \"\" 1 { 0 0 }
1290"
1291            ),
1292            ValidationError::NonMatchingInfosetNames
1293        );
1294    }
1295
1296    #[test]
1297    fn invalid_infoset_actions() {
1298        // a reordered list no longer matches the first declaration, since order is significant
1299        assert_eq!(
1300            validation_err(
1301                "EFG 2 R \"\" { \"1\" \"2\" }
1302p \"\" 1 1 \"a\" { \"L\" \"R\" } 0
1303t \"\" 1 \"\" { 0 0 }
1304p \"\" 1 1 \"a\" { \"R\" \"L\" } 0
1305t \"\" 2 \"\" { 0 0 }
1306t \"\" 3 \"\" { 0 0 }
1307"
1308            ),
1309            ValidationError::NonMatchingInfosetActions
1310        );
1311    }
1312
1313    #[test]
1314    fn invalid_chance_infoset_actions() {
1315        assert_eq!(
1316            validation_err(
1317                "EFG 2 R \"\" { \"1\" \"2\" }
1318c \"\" 1 \"a\" { \"x\" 1 } 0
1319c \"\" 1 \"a\" { \"y\" 1 } 0
1320t \"\" 1 { 0 0 }
1321"
1322            ),
1323            ValidationError::NonMatchingInfosetActions
1324        );
1325    }
1326
1327    #[test]
1328    fn null_outcome_payoffs() {
1329        assert_eq!(
1330            validation_err(
1331                "EFG 2 R \"\" { \"1\" \"2\" }
1332p \"\" 1 1 \"a\" { \"x\" } 0 \"n\" { 0 0 }
1333t \"\" 1 { 0 0 }
1334"
1335            ),
1336            ValidationError::NullOutcomePayoffs
1337        );
1338    }
1339
1340    #[test]
1341    fn invalid_payoff_number() {
1342        assert_eq!(
1343            validation_err(
1344                "EFG 2 R \"\" { \"1\" \"2\" }
1345t \"\" 1 \"\" { 0 }
1346"
1347            ),
1348            ValidationError::InvalidNumberOfPayoffs
1349        );
1350    }
1351
1352    #[test]
1353    fn non_matching_outcome_names() {
1354        assert_eq!(
1355            validation_err(
1356                "EFG 2 R \"\" { \"1\" \"2\" }
1357p \"\" 1 1 \"a\" { \"x\" } 1 \"b\" { 0 0 }
1358t \"\" 1 \"c\" { 0 0 }
1359"
1360            ),
1361            ValidationError::NonMatchingOutcomeNames
1362        );
1363    }
1364
1365    #[test]
1366    fn non_matching_outcome_payoffs() {
1367        assert_eq!(
1368            validation_err(
1369                "EFG 2 R \"\" { \"1\" \"2\" }
1370p \"\" 1 1 \"a\" { \"x\" } 1 \"\" { 0 0 }
1371t \"\" 1 \"\" { 1 1 }
1372"
1373            ),
1374            ValidationError::NonMatchingOutcomePayoffs
1375        );
1376    }
1377
1378    #[test]
1379    fn undefined_outcome() {
1380        // referencing an outcome by bare id that was never defined is an error
1381        assert_eq!(
1382            validation_err(
1383                "EFG 2 R \"\" { \"1\" \"2\" }
1384t \"\" 5
1385"
1386            ),
1387            ValidationError::UndefinedOutcome
1388        );
1389    }
1390
1391    #[test]
1392    fn undeclared_infoset() {
1393        // omitting the action list before the infoset has ever been declared is an error
1394        assert_eq!(
1395            validation_err(
1396                "EFG 2 R \"\" { \"1\" \"2\" }
1397p \"\" 1 1 0
1398t \"\" 1 { 0 0 }
1399"
1400            ),
1401            ValidationError::UndeclaredInfoset
1402        );
1403    }
1404
1405    #[test]
1406    fn fills_omitted_action_list() {
1407        let game_str = "EFG 2 R \"\" { \"1\" \"2\" }
1408p \"\" 1 1 \"a\" { \"L\" \"R\" } 0
1409t \"\" 1 \"\" { 0 0 }
1410p \"\" 1 1 0
1411t \"\" 2 \"\" { 0 0 }
1412t \"\" 3 \"\" { 0 0 }
1413";
1414        let game = ExtensiveFormGame::try_from_str(game_str).unwrap();
1415        let Node::Player(root) = game.root() else {
1416            panic!("expected a player root");
1417        };
1418        let Some(Node::Player(omitted)) = root.action("R") else {
1419            panic!("expected a player after action R");
1420        };
1421        // the omitted node inherits the declared label and actions
1422        assert_eq!(omitted.infoset_name().escape(), "a");
1423        let labels: Vec<_> = omitted.actions().map(|(label, _)| label.escape()).collect();
1424        assert_eq!(labels, ["L", "R"]);
1425        // and the omitted form round-trips (the omission is preserved)
1426        let written = game.to_string();
1427        let reparsed = ExtensiveFormGame::try_from_str(written.as_str()).unwrap();
1428        assert_eq!(game, reparsed);
1429    }
1430
1431    #[test]
1432    fn handle_accessors() {
1433        let game_str = r#"EFG 2 R "game" { "P1" "P2" } "the comment"
1434c "chance" 1 "ci" { "a" 1/2 "b" 1/2 } 5 "co" { 1 2 }
1435p "pl1" 1 1 "pi1" { "x" "y" } 6 "po1" { 3 4 }
1436t "ta" 1 "oa" { 7 8 }
1437t "tb" 2 "ob" { 9 10 }
1438p "pl2" 2 2 "pi2" { "x" "y" } 7 "po2" { 5 6 }
1439t "tc" 3 "oc" { 11 12 }
1440t "td" 4 "od" { 13 14 }
1441"#;
1442        let game = ExtensiveFormGame::try_from_str(game_str).unwrap();
1443        assert_eq!(game.comment().map(EscapedStr::escape), Some("the comment"));
1444        // Display covers every node kind's formatting and round-trips
1445        let written = game.to_string();
1446        let reparsed = ExtensiveFormGame::try_from_str(written.as_str()).unwrap();
1447        assert_eq!(game, reparsed);
1448
1449        let Node::Chance(chance) = game.root() else {
1450            panic!("expected a chance root");
1451        };
1452        assert_eq!(chance.name().escape(), "chance");
1453        assert_eq!(chance.infoset(), 1);
1454        assert_eq!(chance.infoset_name().escape(), "ci");
1455        assert_eq!(chance.len(), 2);
1456        assert_eq!(chance.outcome(), 5);
1457        let chance_payoffs: Vec<_> = chance
1458            .outcome_payoffs()
1459            .unwrap()
1460            .iter()
1461            .map(ToString::to_string)
1462            .collect();
1463        assert_eq!(chance_payoffs, ["1", "2"]);
1464        let chance_labels: Vec<_> = chance
1465            .actions()
1466            .map(|(label, _, _)| label.escape())
1467            .collect();
1468        assert_eq!(chance_labels, ["a", "b"]);
1469        assert!(chance.action_at(0).is_some());
1470        assert!(chance.action_at(2).is_none());
1471        assert!(chance.action("none").is_none());
1472        let (prob, first_child) = chance.action("a").unwrap();
1473        assert_eq!(prob.to_string(), "1/2");
1474
1475        let Node::Player(player) = first_child else {
1476            panic!("expected a player after chance action a");
1477        };
1478        assert_eq!(player.name().escape(), "pl1");
1479        assert_eq!(player.player_num(), 1);
1480        assert_eq!(player.infoset(), 1);
1481        assert_eq!(player.infoset_name().escape(), "pi1");
1482        assert_eq!(player.len(), 2);
1483        assert_eq!(player.outcome(), 6);
1484        assert_eq!(player.outcome_name().map(EscapedStr::escape), Some("po1"));
1485        let player_payoffs: Vec<_> = player
1486            .outcome_payoffs()
1487            .unwrap()
1488            .iter()
1489            .map(ToString::to_string)
1490            .collect();
1491        assert_eq!(player_payoffs, ["3", "4"]);
1492        let player_labels: Vec<_> = player.actions().map(|(label, _)| label.escape()).collect();
1493        assert_eq!(player_labels, ["x", "y"]);
1494        assert!(player.action("none").is_none());
1495        assert!(player.action("y").is_some());
1496        let (label, leaf) = player.action_at(0).unwrap();
1497        assert_eq!(label.escape(), "x");
1498        assert!(player.action_at(2).is_none());
1499
1500        let Node::Terminal(terminal) = leaf else {
1501            panic!("expected a terminal after player action x");
1502        };
1503        assert_eq!(terminal.name().escape(), "ta");
1504        assert_eq!(terminal.outcome(), 1);
1505        assert_eq!(terminal.outcome_name().map(EscapedStr::escape), Some("oa"));
1506        let terminal_payoffs: Vec<_> = terminal
1507            .outcome_payoffs()
1508            .unwrap()
1509            .iter()
1510            .map(ToString::to_string)
1511            .collect();
1512        assert_eq!(terminal_payoffs, ["7", "8"]);
1513    }
1514
1515    #[test]
1516    fn error_display() {
1517        let parse_err = ExtensiveFormGame::try_from_str("not an efg").unwrap_err();
1518        assert!(parse_err.to_string().starts_with("error parsing game at:"));
1519
1520        let bad = "EFG 2 R \"\" { \"1\" \"2\" }\np \"\" 3 1 \"a\" { \"x\" } 0\nt \"\" 1 { 0 0 }\n";
1521        assert_eq!(
1522            ExtensiveFormGame::try_from_str(bad)
1523                .unwrap_err()
1524                .to_string(),
1525            "invalid efg: InvalidPlayerNum"
1526        );
1527        assert_eq!(
1528            ValidationError::UndefinedOutcome.to_string(),
1529            "UndefinedOutcome"
1530        );
1531    }
1532
1533    #[test]
1534    fn accepts_d_data_type() {
1535        // Gambit reads either the R or legacy D data-type letter; Display normalizes to R
1536        let game = ExtensiveFormGame::try_from_str(
1537            "EFG 2 D \"\" { \"1\" \"2\" }\nt \"\" 1 \"\" { 1 2 }\n",
1538        )
1539        .unwrap();
1540        assert!(game.to_string().starts_with("EFG 2 R "));
1541    }
1542
1543    #[test]
1544    fn trailing_input_is_rejected() {
1545        let game = r#"EFG 2 R "" { "1" "2" } t "" 1 "" { 1 2 } trailing"#;
1546        assert!(matches!(
1547            ExtensiveFormGame::try_from_str(game),
1548            Err(Error::Parse("trailing"))
1549        ));
1550    }
1551
1552    #[test]
1553    fn rejects_overflowing_exponent() {
1554        // an exponent that doesn't fit an i32 fails the number parse
1555        assert!(super::big_float("1e99999999999 ").is_err());
1556    }
1557
1558    #[test]
1559    fn rejects_huge_exponent() {
1560        // a huge but i32-valid exponent is capped, not materialized
1561        assert!(super::big_float("1e2000000000 ").is_err());
1562        assert!(super::big_float("1e-2000000000 ").is_err());
1563        // an exponent within the cap still parses
1564        assert!(super::big_float("1e100 ").is_ok());
1565    }
1566
1567    #[test]
1568    fn rejects_zero_denominator() {
1569        // a zero denominator must surface as a parse error rather than panicking in `Div`
1570        assert!(super::big_rational("1/0 ").is_err());
1571        assert!(
1572            ExtensiveFormGame::try_from_str(
1573                "EFG 2 R \"\" { \"1\" \"2\" }\nt \"\" 1 \"\" { 1/0 2 }\n"
1574            )
1575            .is_err()
1576        );
1577    }
1578
1579    #[test]
1580    fn outcome_defined_then_referenced() {
1581        // once an outcome is defined, later nodes may reference it by bare id
1582        let game = "EFG 2 R \"\" { \"1\" \"2\" }
1583p \"\" 1 1 \"i\" { \"x\" } 1 \"named\" { 3 4 }
1584t \"\" 1
1585";
1586        assert!(ExtensiveFormGame::try_from_str(game).is_ok());
1587    }
1588
1589    #[test]
1590    fn chance_null_outcome_with_payoffs() {
1591        // outcome validation also runs for chance nodes
1592        assert_eq!(
1593            validation_err(
1594                "EFG 2 R \"\" { \"1\" \"2\" }
1595c \"\" 1 \"i\" { \"x\" 1 } 0 \"n\" { 1 2 }
1596t \"\" 1 { 0 0 }
1597"
1598            ),
1599            ValidationError::NullOutcomePayoffs
1600        );
1601    }
1602
1603    #[test]
1604    fn deep_tree_parses_and_drops() {
1605        // a tree far deeper than any call stack could hold must parse, validate, navigate, and drop
1606        // without overflowing, now that the arena makes every path flat rather than recursive
1607        let depth = 200_000;
1608        let mut game = String::with_capacity(depth * 24 + 64);
1609        game.push_str("EFG 2 R \"\" { \"1\" \"2\" }\n");
1610        for _ in 0..depth {
1611            game.push_str("p \"\" 1 1 \"i\" { \"a\" } 0\n");
1612        }
1613        game.push_str("t \"\" 1 \"\" { 0 0 }\n");
1614        let parsed = ExtensiveFormGame::try_from_str(&game).unwrap();
1615        assert!(matches!(parsed.root(), Node::Player(_)));
1616        // dropping the deep tree is itself a flat pass over the arena
1617        drop(parsed);
1618    }
1619
1620    #[test]
1621    fn tolerates_flexible_whitespace() {
1622        // whitespace is not significant: braces need no padding, and payoff commas need no space
1623        let game =
1624            ExtensiveFormGame::try_from_str("EFG 2 R \"\" {\"1\" \"2\"}\nt \"\" 1 \"\" {1,2}\n")
1625                .unwrap();
1626        assert_eq!(game.player_names().len(), 2);
1627        let Node::Terminal(root) = game.root() else {
1628            panic!("expected a terminal root");
1629        };
1630        let payoffs: Vec<_> = root
1631            .outcome_payoffs()
1632            .unwrap()
1633            .iter()
1634            .map(BigRational::to_string)
1635            .collect();
1636        assert_eq!(payoffs, ["1", "2"]);
1637        // a comma padded with spaces is equally acceptable
1638        assert!(
1639            ExtensiveFormGame::try_from_str(
1640                "EFG 2 R \"\" { \"1\" \"2\" }\nt \"\" 1 \"\" { 1 , 2 }\n"
1641            )
1642            .is_ok()
1643        );
1644    }
1645
1646    #[test]
1647    fn chance_outcome_name() {
1648        // a chance node may carry an outcome name (Gambit writes and reads one)
1649        let game_str = "EFG 2 R \"\" { \"1\" \"2\" }
1650c \"\" 1 \"i\" { \"a\" 1/2 \"b\" 1/2 } 1 \"oname\" { 3 4 }
1651t \"\" 1
1652t \"\" 1
1653";
1654        let game = ExtensiveFormGame::try_from_str(game_str).unwrap();
1655        let Node::Chance(root) = game.root() else {
1656            panic!("expected a chance root");
1657        };
1658        assert_eq!(root.outcome_name().map(EscapedStr::escape), Some("oname"));
1659        // the name is preserved through a Display round-trip
1660        let written = game.to_string();
1661        let reparsed = ExtensiveFormGame::try_from_str(written.as_str()).unwrap();
1662        assert_eq!(game, reparsed);
1663    }
1664
1665    #[test]
1666    fn terminal_null_and_referenced_outcomes() {
1667        // `t "" 0` (null outcome, no payoffs) and a terminal that only references an outcome
1668        // defined on another node both parse and round-trip
1669        let game_str = "EFG 2 R \"\" { \"1\" \"2\" }
1670p \"\" 1 1 \"i\" { \"L\" \"M\" \"R\" } 0
1671t \"a\" 0
1672t \"b\" 1 \"obname\" { 3 4 }
1673t \"c\" 1
1674";
1675        let game = ExtensiveFormGame::try_from_str(game_str).unwrap();
1676        let Node::Player(root) = game.root() else {
1677            panic!("expected a player root");
1678        };
1679        let Some(Node::Terminal(null_term)) = root.action("L") else {
1680            panic!("expected a terminal after action L");
1681        };
1682        // the null outcome resolves to no payoffs and no name
1683        assert_eq!(null_term.outcome(), 0);
1684        assert!(null_term.outcome_payoffs().is_none());
1685        assert!(null_term.outcome_name().is_none());
1686        let Some(Node::Terminal(referenced)) = root.action("R") else {
1687            panic!("expected a terminal after action R");
1688        };
1689        // the referencing terminal resolves through the shared outcome to the payoffs "b" defined
1690        assert_eq!(referenced.outcome(), 1);
1691        let payoffs: Vec<_> = referenced
1692            .outcome_payoffs()
1693            .unwrap()
1694            .iter()
1695            .map(BigRational::to_string)
1696            .collect();
1697        assert_eq!(payoffs, ["3", "4"]);
1698        // and the game round-trips
1699        let written = game.to_string();
1700        let reparsed = ExtensiveFormGame::try_from_str(written.as_str()).unwrap();
1701        assert_eq!(game, reparsed);
1702    }
1703
1704    #[test]
1705    fn write_modes() {
1706        // infoset 1 and outcome 1 are each declared more than minimally (root+mid repeat the
1707        // infoset block, "a"+"b" repeat the outcome), and "c" references the outcome by id
1708        let input = "EFG 2 R \"\" { \"1\" \"2\" }
1709p \"root\" 1 1 \"iset\" { \"L\" \"R\" } 0
1710p \"mid\" 1 1 \"iset\" { \"L\" \"R\" } 0
1711t \"a\" 1 \"out\" { 1 2 }
1712t \"b\" 1 \"out\" { 1 2 }
1713t \"c\" 1
1714";
1715        let game = ExtensiveFormGame::try_from_str(input).unwrap();
1716
1717        // Display defaults to Faithful, which reproduces the parsed declare/reference structure
1718        assert_eq!(
1719            game.to_string(),
1720            game.display(WriteMode::Faithful).to_string()
1721        );
1722        let faithful = game.display(WriteMode::Faithful).to_string();
1723        assert_eq!(ExtensiveFormGame::try_from_str(&faithful).unwrap(), game);
1724
1725        // every mode is valid and resolves to the same game, so its exhaustive rendering matches
1726        let canonical = game.display(WriteMode::Exhaustive).to_string();
1727        for mode in [
1728            WriteMode::Minimal,
1729            WriteMode::Faithful,
1730            WriteMode::Exhaustive,
1731        ] {
1732            let out = game.display(mode).to_string();
1733            let reparsed = ExtensiveFormGame::try_from_str(&out).unwrap();
1734            assert_eq!(
1735                reparsed.display(WriteMode::Exhaustive).to_string(),
1736                canonical
1737            );
1738        }
1739
1740        // Minimal declares each block once; Exhaustive writes them on every node
1741        let minimal = game.display(WriteMode::Minimal).to_string();
1742        assert!(minimal.len() < faithful.len());
1743        assert!(faithful.len() < canonical.len());
1744    }
1745}