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