1#![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
33type ChanceInfoset<'a> = (&'a EscapedStr, Box<[(&'a EscapedStr, BigRational)]>);
35type PlayerInfoset<'a> = (&'a EscapedStr, Box<[&'a EscapedStr]>);
37
38type Outcomes<'a> = HashMap<u64, (&'a EscapedStr, Box<[BigRational]>)>;
41
42#[derive(Debug, PartialEq, Clone)]
46struct Infosets<'a> {
47 player: Box<[HashMap<u64, PlayerInfoset<'a>>]>,
48 chance: HashMap<u64, ChanceInfoset<'a>>,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53struct NodeId(usize);
54
55#[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 declared: bool,
69 children: Box<[NodeId]>,
70 outcome: u64,
71 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 declared: bool,
82 children: Box<[NodeId]>,
83 outcome: u64,
84 outcome_declared: bool,
86}
87
88#[derive(Debug, PartialEq, Eq, Clone)]
89struct RawTerminal<'a> {
90 name: &'a EscapedStr,
91 outcome: u64,
92 outcome_declared: bool,
94}
95
96#[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 #[must_use]
123 pub fn name(&self) -> &'a EscapedStr {
124 self.name
125 }
126
127 #[must_use]
129 pub fn player_names(&self) -> &[&'a EscapedStr] {
130 &self.player_names
131 }
132
133 #[must_use]
135 pub fn comment(&self) -> Option<&'a EscapedStr> {
136 self.comment
137 }
138
139 #[must_use]
141 pub fn root<'g>(&'g self) -> Node<'a, 'g> {
142 self.wrap(self.root)
143 }
144
145 #[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 fn outcome_name(&self, outcome: u64) -> Option<&'a EscapedStr> {
163 self.outcomes.get(&outcome).map(|(name, _)| *name)
164 }
165
166 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
183pub enum WriteMode {
184 Minimal,
186 Faithful,
188 Exhaustive,
190}
191
192#[derive(Clone, Copy)]
195pub struct GameDisplay<'a, 'g> {
196 game: &'g ExtensiveFormGame<'a>,
197 mode: WriteMode,
198}
199
200impl GameDisplay<'_, '_> {
201 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 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#[derive(Debug)]
298#[non_exhaustive]
299pub enum Error<'a> {
300 Parse(&'a str),
304 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#[derive(Debug, PartialEq, Eq)]
322#[non_exhaustive]
323pub enum ValidationError {
324 InvalidPlayerNum,
326 NonMatchingInfosetNames,
328 NonMatchingInfosetActions,
330 NullOutcomePayoffs,
332 InvalidNumberOfPayoffs,
334 NonMatchingOutcomeNames,
336 NonMatchingOutcomePayoffs,
338 UndefinedOutcome,
340 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 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#[derive(Clone, Copy)]
395pub enum Node<'a, 'g> {
396 Chance(Chance<'a, 'g>),
398 Player(Player<'a, 'g>),
400 Terminal(Terminal<'a, 'g>),
402}
403
404fn 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#[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 #[must_use]
445 pub fn name(self) -> &'a EscapedStr {
446 self.raw.name
447 }
448
449 #[must_use]
451 pub fn infoset(self) -> u64 {
452 self.raw.infoset
453 }
454
455 #[must_use]
457 pub fn infoset_name(self) -> &'a EscapedStr {
458 self.entry().0
459 }
460
461 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 #[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 #[allow(clippy::len_without_is_empty)]
485 #[must_use]
486 pub fn len(self) -> usize {
487 self.raw.children.len()
488 }
489
490 #[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 #[must_use]
504 pub fn outcome(self) -> u64 {
505 self.raw.outcome
506 }
507
508 #[must_use]
512 pub fn outcome_name(self) -> Option<&'a EscapedStr> {
513 self.game.outcome_name(self.raw.outcome)
514 }
515
516 #[must_use]
521 pub fn outcome_payoffs(self) -> Option<&'g [BigRational]> {
522 self.game.outcome_payoffs(self.raw.outcome)
523 }
524}
525
526#[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 #[must_use]
542 pub fn name(self) -> &'a EscapedStr {
543 self.raw.name
544 }
545
546 #[must_use]
550 pub fn player_num(self) -> usize {
551 self.raw.player_num
552 }
553
554 #[must_use]
556 pub fn infoset(self) -> u64 {
557 self.raw.infoset
558 }
559
560 #[must_use]
562 pub fn infoset_name(self) -> &'a EscapedStr {
563 self.entry().0
564 }
565
566 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 #[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 #[allow(clippy::len_without_is_empty)]
588 #[must_use]
589 pub fn len(self) -> usize {
590 self.raw.children.len()
591 }
592
593 #[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 #[must_use]
604 pub fn outcome(self) -> u64 {
605 self.raw.outcome
606 }
607
608 #[must_use]
612 pub fn outcome_name(self) -> Option<&'a EscapedStr> {
613 self.game.outcome_name(self.raw.outcome)
614 }
615
616 #[must_use]
620 pub fn outcome_payoffs(self) -> Option<&'g [BigRational]> {
621 self.game.outcome_payoffs(self.raw.outcome)
622 }
623}
624
625#[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 #[must_use]
637 pub fn name(self) -> &'a EscapedStr {
638 self.raw.name
639 }
640
641 #[must_use]
643 pub fn outcome(self) -> u64 {
644 self.raw.outcome
645 }
646
647 #[must_use]
651 pub fn outcome_name(self) -> Option<&'a EscapedStr> {
652 self.game.outcome_name(self.raw.outcome)
653 }
654
655 #[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 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 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
753struct 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 RawNode::Terminal(_) => unreachable!("terminal nodes are never pending"),
770 }
771 node
772 }
773}
774
775fn 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 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
810fn 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 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 Err(ValidationError::UndefinedOutcome.into())
848 } else {
849 Ok(())
850 }
851}
852
853fn 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 let mut nodes: Vec<RawNode<'a>> = Vec::new();
862 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 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 _ => unreachable!(),
899 };
900
901 loop {
903 let Some(pending) = stack.last_mut() else {
904 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
916fn 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
923fn 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 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 children: Box::default(),
960 outcome,
961 outcome_declared,
962 },
963 child_count,
964 ))
965}
966
967fn 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 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 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 children: Box::default(),
1009 outcome,
1010 outcome_declared,
1011 },
1012 child_count,
1013 ))
1014}
1015
1016fn 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 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 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 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 let (input, label) = super::label(r#""esca\"ped" "#).unwrap();
1144 assert_eq!(input, " ");
1145 assert_eq!(label.escape(), r#"esca\"ped"#);
1146
1147 let (input, label) = super::label(r#""back\slash" "#).unwrap();
1149 assert_eq!(input, " ");
1150 assert_eq!(label.escape(), r"back\slash");
1151
1152 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 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 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 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 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 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 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 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 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 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 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 assert!(super::big_float("1e99999999999 ").is_err());
1548 }
1549
1550 #[test]
1551 fn rejects_zero_denominator() {
1552 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 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 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 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 drop(parsed);
1601 }
1602
1603 #[test]
1604 fn tolerates_flexible_whitespace() {
1605 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 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 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 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 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 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 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 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 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 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 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 let minimal = game.display(WriteMode::Minimal).to_string();
1725 assert!(minimal.len() < faithful.len());
1726 assert!(faithful.len() < canonical.len());
1727 }
1728}