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]
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 #[allow(clippy::len_without_is_empty)]
486 #[must_use]
487 pub fn len(self) -> usize {
488 self.raw.children.len()
489 }
490
491 #[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 #[must_use]
505 pub fn outcome(self) -> u64 {
506 self.raw.outcome
507 }
508
509 #[must_use]
513 pub fn outcome_name(self) -> Option<&'a EscapedStr> {
514 self.game.outcome_name(self.raw.outcome)
515 }
516
517 #[must_use]
522 pub fn outcome_payoffs(self) -> Option<&'g [BigRational]> {
523 self.game.outcome_payoffs(self.raw.outcome)
524 }
525}
526
527#[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 #[must_use]
543 pub fn name(self) -> &'a EscapedStr {
544 self.raw.name
545 }
546
547 #[must_use]
551 pub fn player_num(self) -> usize {
552 self.raw.player_num
553 }
554
555 #[must_use]
557 pub fn infoset(self) -> u64 {
558 self.raw.infoset
559 }
560
561 #[must_use]
563 pub fn infoset_name(self) -> &'a EscapedStr {
564 self.entry().0
565 }
566
567 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 #[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 #[allow(clippy::len_without_is_empty)]
590 #[must_use]
591 pub fn len(self) -> usize {
592 self.raw.children.len()
593 }
594
595 #[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 #[must_use]
606 pub fn outcome(self) -> u64 {
607 self.raw.outcome
608 }
609
610 #[must_use]
614 pub fn outcome_name(self) -> Option<&'a EscapedStr> {
615 self.game.outcome_name(self.raw.outcome)
616 }
617
618 #[must_use]
622 pub fn outcome_payoffs(self) -> Option<&'g [BigRational]> {
623 self.game.outcome_payoffs(self.raw.outcome)
624 }
625}
626
627#[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 #[must_use]
639 pub fn name(self) -> &'a EscapedStr {
640 self.raw.name
641 }
642
643 #[must_use]
645 pub fn outcome(self) -> u64 {
646 self.raw.outcome
647 }
648
649 #[must_use]
653 pub fn outcome_name(self) -> Option<&'a EscapedStr> {
654 self.game.outcome_name(self.raw.outcome)
655 }
656
657 #[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
675const 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 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 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
761struct 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 RawNode::Terminal(_) => unreachable!("terminal nodes are never pending"),
778 }
779 node
780 }
781}
782
783fn 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 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
818fn 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 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 Err(ValidationError::UndefinedOutcome.into())
856 } else {
857 Ok(())
858 }
859}
860
861fn 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 let mut nodes: Vec<RawNode<'a>> = Vec::new();
870 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 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 _ => unreachable!(),
907 };
908
909 loop {
911 let Some(pending) = stack.last_mut() else {
912 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
924fn 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
931fn 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 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 children: Box::default(),
968 outcome,
969 outcome_declared,
970 },
971 child_count,
972 ))
973}
974
975fn 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 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 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 children: Box::default(),
1017 outcome,
1018 outcome_declared,
1019 },
1020 child_count,
1021 ))
1022}
1023
1024fn 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 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 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 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 let (input, label) = super::label(r#""esca\"ped" "#).unwrap();
1152 assert_eq!(input, " ");
1153 assert_eq!(label.escape(), r#"esca\"ped"#);
1154
1155 let (input, label) = super::label(r#""back\slash" "#).unwrap();
1157 assert_eq!(input, " ");
1158 assert_eq!(label.escape(), r"back\slash");
1159
1160 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 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 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 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 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 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 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 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 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 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 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 assert!(super::big_float("1e99999999999 ").is_err());
1556 }
1557
1558 #[test]
1559 fn rejects_huge_exponent() {
1560 assert!(super::big_float("1e2000000000 ").is_err());
1562 assert!(super::big_float("1e-2000000000 ").is_err());
1563 assert!(super::big_float("1e100 ").is_ok());
1565 }
1566
1567 #[test]
1568 fn rejects_zero_denominator() {
1569 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 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 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 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 drop(parsed);
1618 }
1619
1620 #[test]
1621 fn tolerates_flexible_whitespace() {
1622 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 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 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 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 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 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 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 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 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 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 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 let minimal = game.display(WriteMode::Minimal).to_string();
1742 assert!(minimal.len() < faithful.len());
1743 assert!(faithful.len() < canonical.len());
1744 }
1745}