From 79ada6e615adfb5fab87131db698e7f015896cf6 Mon Sep 17 00:00:00 2001 From: rahulsavani Date: Wed, 11 Jun 2025 08:24:57 +0200 Subject: [PATCH] Tests for the creation of the reduced strategic form from an extensive form game --- ChangeLog | 8 +- tests/games.py | 121 ++++++++- tests/test_extensive.py | 250 +++++++++++++++++- .../binary_3_levels_generic_payoffs.efg | 18 ++ tests/test_games/nature_leaves_generic.efg | 18 ++ tests/test_games/nature_leaves_nongeneric.efg | 18 ++ tests/test_games/nature_rooted_generic.efg | 18 ++ tests/test_games/nature_rooted_nongeneric.efg | 18 ++ tests/test_games/wichardt.efg | 18 ++ 9 files changed, 469 insertions(+), 18 deletions(-) create mode 100644 tests/test_games/binary_3_levels_generic_payoffs.efg create mode 100644 tests/test_games/nature_leaves_generic.efg create mode 100644 tests/test_games/nature_leaves_nongeneric.efg create mode 100644 tests/test_games/nature_rooted_generic.efg create mode 100644 tests/test_games/nature_rooted_nongeneric.efg create mode 100644 tests/test_games/wichardt.efg diff --git a/ChangeLog b/ChangeLog index f7bf21387..c9ab9f6ac 100644 --- a/ChangeLog +++ b/ChangeLog @@ -10,10 +10,12 @@ been removed as planned. (#357) ### Added -- Implement `GetPlays()` (C++) and `get_plays` (Python) to compute the set of terminal nodes - consistent with a node, information set, or action (#517) +- Implement `GetPlays()` (C++) and `get_plays` (Python) to compute the set of terminal nodes consistent + with a node, information set, or action (#517) - Implement `GameStrategyRep::GetAction` (C++) and `Strategy.action` (Python) retrieving the action - prescribed by a strategy at an information set + prescribed by a strategy at an information set +- Tests for creation of the reduced strategic form from an extensive-form game (currently only + for games with perfect recall) ## [16.3.1] - unreleased diff --git a/tests/games.py b/tests/games.py index 652dafc73..361333af3 100644 --- a/tests/games.py +++ b/tests/games.py @@ -1,14 +1,17 @@ """A utility module to create/load games for the test suite.""" + import pathlib +import numpy as np + import pygambit as gbt def read_from_file(fn: str) -> gbt.Game: if fn.endswith(".efg"): - return gbt.read_efg(pathlib.Path("tests/test_games")/fn) + return gbt.read_efg(pathlib.Path("tests/test_games") / fn) elif fn.endswith(".nfg"): - return gbt.read_nfg(pathlib.Path("tests/test_games")/fn) + return gbt.read_nfg(pathlib.Path("tests/test_games") / fn) else: raise ValueError(f"Unknown file extension in {fn}") @@ -16,6 +19,7 @@ def read_from_file(fn: str) -> gbt.Game: ################################################################################################ # Normal-form (aka strategic-form) games (nfg) + def create_2x2_zero_nfg() -> gbt.Game: """ Returns @@ -73,6 +77,7 @@ def create_coord_4x4_nfg(outcome_version: bool = False) -> gbt.Game: ################################################################################################ # Extensive-form games (efg) + def create_mixed_behav_game_efg() -> gbt.Game: """ Returns @@ -89,7 +94,8 @@ def create_myerson_2_card_poker_efg() -> gbt.Game: Returns ------- Game - Myerson 2-card poker: Two-player extensive poker game with a chance move with two moves, + Simplied "stripped down" version of Myerson 2-card poker: + Two-player extensive poker game with a chance move with two moves, then player 1 can raise or fold; after raising player 2 is in an infoset with two nodes and can choose to meet or pass """ @@ -124,3 +130,112 @@ def create_selten_horse_game_efg() -> gbt.Game: 5-player Selten's Horse Game """ return read_from_file("e01.efg") + + +def create_reduction_generic_payoffs_efg() -> gbt.Game: + # tree with only root + g = gbt.Game.new_tree( + players=["1", "2"], title="2 player reduction generic payoffs" + ) + + # add four children + g.append_move(g.root, "2", ["a", "b", "c", "d"]) + + # add L and R after a + g.append_move(g.root.children[0], "1", ["L", "R"]) + + # add C and D to single infoset after b and c + nodes = [g.root.children[1], g.root.children[2]] + g.append_move(nodes, "1", ["C", "D"]) + + # add s and t from single infoset after rightmost C and D + g.append_move(g.root.children[2].children, "2", ["s", "t"]) + + # add p and q + g.append_move(g.root.children[0].children[1], "2", ["p", "q"]) + + # add U and V in a single infoset after p and q + g.append_move(g.root.children[0].children[1].children, "1", ["U", "V"]) + + # Set outcomes + + g.set_outcome(g.root.children[0].children[0], g.add_outcome([1, -1], label="aL")) + g.set_outcome( + g.root.children[0].children[1].children[0].children[0], + g.add_outcome([2, -2], label="aRpU"), + ) + g.set_outcome( + g.root.children[0].children[1].children[0].children[1], + g.add_outcome([3, -3], label="aRpV"), + ) + g.set_outcome( + g.root.children[0].children[1].children[1].children[0], + g.add_outcome([4, -4], label="aRqU"), + ) + g.set_outcome( + g.root.children[0].children[1].children[1].children[1], + g.add_outcome([5, -5], label="aRqV"), + ) + + g.set_outcome(g.root.children[1].children[0], g.add_outcome([6, -6], label="bC")) + g.set_outcome(g.root.children[1].children[1], g.add_outcome([7, -7], label="bD")) + + g.set_outcome( + g.root.children[2].children[0].children[0], g.add_outcome([8, -8], label="cCs") + ) + g.set_outcome( + g.root.children[2].children[0].children[1], g.add_outcome([9, -9], label="cCt") + ) + g.set_outcome( + g.root.children[2].children[1].children[0], + g.add_outcome([10, -10], label="cDs"), + ) + g.set_outcome( + g.root.children[2].children[1].children[1], + g.add_outcome([11, -11], label="cDt"), + ) + + g.set_outcome(g.root.children[3], g.add_outcome([12, -12], label="d")) + + return g + + +def create_reduction_one_player_generic_payoffs_efg() -> gbt.Game: + g = gbt.Game.new_tree(players=["1"], title="One player reduction generic payoffs") + g.append_move(g.root, "1", ["a", "b", "c", "d"]) + g.append_move(g.root.children[0], "1", ["e", "f"]) + g.set_outcome(g.root.children[0].children[0], g.add_outcome([1])) + g.set_outcome(g.root.children[0].children[1], g.add_outcome([2])) + g.set_outcome(g.root.children[1], g.add_outcome([3])) + g.set_outcome(g.root.children[2], g.add_outcome([4])) + g.set_outcome(g.root.children[3], g.add_outcome([5])) + return g + + +def create_reduction_both_players_payoff_ties_efg() -> gbt.Game: + g = gbt.Game.new_tree(players=["1", "2"], title="From GTE survey") + g.append_move(g.root, "1", ["A", "B", "C", "D"]) + g.append_move(g.root.children[0], "2", ["a", "b"]) + g.append_move(g.root.children[1], "2", ["c", "d"]) + g.append_move(g.root.children[2], "2", ["e", "f"]) + g.append_move(g.root.children[0].children[1], "2", ["g", "h"]) + g.append_move(g.root.children[2].children, "1", ["E", "F"]) + + g.set_outcome(g.root.children[0].children[0], g.add_outcome([2, 8])) + g.set_outcome(g.root.children[0].children[1].children[0], g.add_outcome([0, 1])) + g.set_outcome(g.root.children[0].children[1].children[1], g.add_outcome([5, 2])) + g.set_outcome(g.root.children[1].children[0], g.add_outcome([7, 6])) + g.set_outcome(g.root.children[1].children[1], g.add_outcome([4, 2])) + g.set_outcome(g.root.children[2].children[0].children[0], g.add_outcome([3, 7])) + g.set_outcome(g.root.children[2].children[0].children[1], g.add_outcome([8, 3])) + g.set_outcome(g.root.children[2].children[1].children[0], g.add_outcome([7, 8])) + g.set_outcome(g.root.children[2].children[1].children[1], g.add_outcome([2, 2])) + g.set_outcome(g.root.children[3], g.add_outcome([6, 4])) + return g + + +def make_rational(input: str): + return gbt.Rational(input) + + +vectorized_make_rational = np.vectorize(make_rational) diff --git a/tests/test_extensive.py b/tests/test_extensive.py index acde88e93..6cf9c420b 100644 --- a/tests/test_extensive.py +++ b/tests/test_extensive.py @@ -1,5 +1,6 @@ import typing +import numpy as np import pytest import pygambit as gbt @@ -8,21 +9,17 @@ @pytest.mark.parametrize( - "players,title", - [([], "New game"), - (["Alice", "Bob"], "A poker game")] + "players,title", [([], "New game"), (["Alice", "Bob"], "A poker game")] ) def test_new_tree(players: list, title: typing.Optional[str]): game = gbt.Game.new_tree(players=players, title=title) assert len(game.players) == len(players) - for (player, label) in zip(game.players, players): + for player, label in zip(game.players, players): assert player.label == label assert game.title == title -@pytest.mark.parametrize( - "title", ["My game's new title"] -) +@pytest.mark.parametrize("title", ["My game's new title"]) def test_game_title(title: str): game = gbt.Game.new_tree() game.title = title @@ -38,15 +35,12 @@ def test_game_comment(comment: str): assert game.comment == comment -@pytest.mark.parametrize( - "players", - [["Alice"], ["Oscar", "Felix"]] -) +@pytest.mark.parametrize("players", [["Alice"], ["Oscar", "Felix"]]) def test_game_add_players_label(players: list): game = gbt.Game.new_tree() for player in players: game.add_player(player) - for (player, label) in zip(game.players, players): + for player, label in zip(game.players, players): assert player.label == label @@ -95,3 +89,235 @@ def test_outcome_index_exception_label(): game = games.read_from_file("sample_extensive_game.efg") with pytest.raises(KeyError): _ = game[[0, 0]]["Not a player"] + + +@pytest.mark.parametrize( + "game,strategy_labels,np_arrays_of_rsf", + [ + ############################################################################### + # # 1 player; reduction; generic payoffs + ( + games.create_reduction_one_player_generic_payoffs_efg(), + [["11", "12", "2*", "3*", "4*"]], + [np.array(range(1, 6))], + ), + # 2 players; reduction possible for player 1; payoff ties + ( + games.read_from_file("e02.efg"), + [["1*", "21", "22"], ["1", "2"]], + [ + np.array([[1, 1], [0, 0], [0, 2]]), + np.array([[1, 1], [2, 3], [2, 0]]), + ], + ), + # 2 players; 1 move each so no reduction possible + ( + games.read_from_file("sample_extensive_game.efg"), + [["1", "2"], ["11", "12", "21", "22"]], + [ + np.array([[2, 2, 2, 2], [4, 6, 4, 6]]), + np.array([[3, 3, 3, 3], [5, 7, 5, 7]]), + ], + ), + # Selten's Horse: game with three players + ( + games.read_from_file("e01.efg"), + [["1", "2"], ["1", "2"], ["1", "2"]], + [ + np.array([[[1, 1], [4, 0]], [[3, 0], [3, 0]]]), + np.array([[[1, 1], [4, 0]], [[2, 0], [2, 0]]]), + np.array([[[1, 1], [0, 1]], [[2, 0], [2, 0]]]), + ], + ), + # 2-player (zero-sum) game; reduction for both players; generic payoffs + ( + games.create_reduction_generic_payoffs_efg(), + [ + ["11*", "12*", "211", "221", "212", "222"], + ["1*1", "1*2", "2**", "31*", "32*", "4**"], + ], + [ + np.array( + [ + [1, 1, 6, 8, 9, 12], + [1, 1, 7, 10, 11, 12], + [2, 4, 6, 8, 9, 12], + [2, 4, 7, 10, 11, 12], + [3, 5, 6, 8, 9, 12], + [3, 5, 7, 10, 11, 12], + ] + ), + np.array( + [ + [-1, -1, -6, -8, -9, -12], + [-1, -1, -7, -10, -11, -12], + [-2, -4, -6, -8, -9, -12], + [-2, -4, -7, -10, -11, -12], + [-3, -5, -6, -8, -9, -12], + [-3, -5, -7, -10, -11, -12], + ] + ), + ], + ), + # 2-player (zero-sum) game; binary tree; reduction for player 1; generic payoffs + ( + games.read_from_file("binary_3_levels_generic_payoffs.efg"), + [ + ["11*", "12*", "2*1", "2*2"], + ["1", "2"], + ], + [ + np.array([[1, 3], [2, 4], [5, 7], [6, 8]]), + np.array([[-1, -3], [-2, -4], [-5, -7], [-6, -8]]), + ], + ), + # # 2-player game from GTE survey; reduction for both players; payoff ties + ( + games.create_reduction_both_players_payoff_ties_efg(), + [ + ["1*", "2*", "31", "32", "4*"], + [ + "111*", + "112*", + "121*", + "122*", + "2111", + "2121", + "2211", + "2221", + "2112", + "2122", + "2212", + "2222", + ], + ], + [ + np.array( + [ + [2, 2, 2, 2, 0, 0, 0, 0, 5, 5, 5, 5], + [7, 7, 4, 4, 7, 7, 4, 4, 7, 7, 4, 4], + [3, 7, 3, 7, 3, 7, 3, 7, 3, 7, 3, 7], + [8, 2, 8, 2, 8, 2, 8, 2, 8, 2, 8, 2], + [6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6], + ] + ), + np.array( + [ + [8, 8, 8, 8, 1, 1, 1, 1, 2, 2, 2, 2], + [6, 6, 2, 2, 6, 6, 2, 2, 6, 6, 2, 2], + [7, 8, 7, 8, 7, 8, 7, 8, 7, 8, 7, 8], + [3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2], + [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4], + ] + ), + ], + ), + ########################################################################### + # Games with chance nodes + ########################################################################### + # 2-player long centipede game with chance playing at the root twice + ( + games.read_from_file("cent3.efg"), + [ + ["1**111", "21*111", "221111", "222111"], + ["1**111", "21*111", "221111", "222111"], + ], + [ + np.array( + [ + ["20027/25000", "5081/6250", "2689/3125", "163/125"], + ["541/1250", "19931/6250", "10114/3125", "92/25"], + ["3299/6250", "5362/3125", "39814/3125", "1648/125"], + ["227/250", "262/125", "856/125", "256/5"], + ] + ), + np.array( + [ + ["2689/12500", "3283/12500", "5659/12500", "163/500"], + ["3983/2500", "2677/3125", "3271/3125", "23/25"], + ["5053/3125", "19903/3125", "10696/3125", "412/125"], + ["214/125", "808/125", "3184/125", "64/5"], + ] + ), + ], + ), + # Stripped down "Myerson" 2-card poker; 2 player zero-sum game with chance at the root + ( + games.create_myerson_2_card_poker_efg(), + [["11", "12", "21", "22"], ["1", "2"]], + [ + np.array([[-1, 0], ["-1/2", -1], ["-5/2", -1], [-2, -2]]), + np.array([[1, 0], ["1/2", 1], ["5/2", 1], [2, 2]]), + ], + ), + # Nature playing at the root, 2 players, no reduction, non-generic payoffs + ( + games.read_from_file("nature_rooted_nongeneric.efg"), + [["1", "2"], ["11", "12", "21", "22"]], + [ + np.array([[-1, -1, 2, 2], [0, 0, 0, 0]]), + np.array([[-1, -1, 2, 2], [3, 4, 3, 4]]), + ], + ), + # Nature playing at the root, 2 players, no reduction, generic payoffs + ( + games.read_from_file("nature_rooted_generic.efg"), + [["1", "2"], ["11", "12", "21", "22"]], + [ + np.array([[3, 3, 4, 4], [5, 6, 5, 6]]), + np.array([[-3, -3, -4, -4], [-5, -6, -5, -6]]), + ], + ), + # Nature playing last determining the payoffs, 2 players, no reduction, non-generic payoffs + ( + games.read_from_file("nature_leaves_nongeneric.efg"), + [["1", "2"], ["11", "12", "21", "22"]], + [ + np.array([[-1, -1, 2, 2], [0, 0, 0, 0]]), + np.array([[-1, -1, 2, 2], [3, 4, 3, 4]]), + ], + ), + # Nature playing last determining the payoffs, 2 players, no reduction, generic payoffs + ( + games.read_from_file("nature_leaves_generic.efg"), + [["1", "2"], ["11", "12", "21", "22"]], + [ + np.array( + [["3/2", "3/2", "7/2", "7/2"], ["11/2", "15/2", "11/2", "15/2"]] + ), + np.array( + [ + ["-3/2", "-3/2", "-7/2", "-7/2"], + ["-11/2", "-15/2", "-11/2", "-15/2"], + ] + ), + ], + ), + # I M P E R F E C T R E C A L L --- commented out in the test suite + # Wichardt (2008): binary tree of height 3; 2 players; the root player forgets the action + # ( + # games.read_from_file("wichardt.efg"), + # [["11", "12", "21", "22"], ["1", "2"]], + # [ + # np.array([[1, -1], [-5, -5], [-5, -5], [-1, 1]]), + # np.array([[-1, 1], [5, 5], [5, 5], [1, -1]]), + # ], + # ), + ], +) +def test_reduced_strategic_form( + game: gbt.Game, strategy_labels: list, np_arrays_of_rsf: list +): + """ + We test two things: + - that the strategy labels are as expected + (these use positive integers and '*'s, rather than labels of moves even if they exist) + - that the payoff tables are correct, which is done via game.to_arrays() + """ + arrays = game.to_arrays() + + for i, player in enumerate(game.players): + assert strategy_labels[i] == [s.label for s in player.strategies] + # convert strings to rationals + exp_array = games.vectorized_make_rational(np_arrays_of_rsf[i]) + assert (arrays[i] == exp_array).all() diff --git a/tests/test_games/binary_3_levels_generic_payoffs.efg b/tests/test_games/binary_3_levels_generic_payoffs.efg new file mode 100644 index 000000000..ea33e53ba --- /dev/null +++ b/tests/test_games/binary_3_levels_generic_payoffs.efg @@ -0,0 +1,18 @@ +EFG 2 R "Binary Tree Game (L=3)" { "Player 1" "Player 2" } +"" + +p "" 1 1 "" { "Left" "Right" } 0 +p "" 2 1 "" { "Left" "Right" } 0 +p "" 1 2 "" { "Left" "Right" } 0 +t "" 1 "" { 1, -1 } +t "" 2 "" { 2, -2 } +p "" 1 2 "" { "Left" "Right" } 0 +t "" 3 "" { 3, -3 } +t "" 4 "" { 4, -4 } +p "" 2 1 "" { "Left" "Right" } 0 +p "" 1 3 "" { "Left" "Right" } 0 +t "" 5 "" { 5, -5 } +t "" 6 "" { 6, -6 } +p "" 1 3 "" { "Left" "Right" } 0 +t "" 7 "" { 7, -7 } +t "" 8 "" { 8, -8 } diff --git a/tests/test_games/nature_leaves_generic.efg b/tests/test_games/nature_leaves_generic.efg new file mode 100644 index 000000000..bda6d926d --- /dev/null +++ b/tests/test_games/nature_leaves_generic.efg @@ -0,0 +1,18 @@ +EFG 2 R "2 player game: chance plays last" { "Player 1" "Player 2" } +"" + +p "" 1 1 "" { "B" "T" } 0 +p "" 2 1 "" { "r" "l" } 0 +c "" 1 "" { "1" 1/2 "2" 1/2 } 0 +t "" 1 "Outcome 1" { 1, -1 } +t "" 2 "Outcome 2" { 2, -2 } +c "" 2 "" { "1" 1/2 "2" 1/2 } 0 +t "" 3 "Outcome 3" { 3, -3 } +t "" 4 "Outcome 4" { 4, -4 } +p "" 2 2 "" { "R" "L" } 0 +c "" 3 "" { "1" 1/2 "2" 1/2 } 0 +t "" 5 "Outcome 5" { 5, -5 } +t "" 6 "Outcome 6" { 6, -6 } +c "" 4 "" { "1" 1/2 "2" 1/2 } 0 +t "" 7 "Outcome 7" { 7, -7 } +t "" 8 "Outcome 8" { 8, -8 } diff --git a/tests/test_games/nature_leaves_nongeneric.efg b/tests/test_games/nature_leaves_nongeneric.efg new file mode 100644 index 000000000..3abbbf4e1 --- /dev/null +++ b/tests/test_games/nature_leaves_nongeneric.efg @@ -0,0 +1,18 @@ +EFG 2 R "2 player game: chance plays last" { "Player 1" "Player 2" } +"" + +p "" 1 1 "" { "B" "T" } 0 +p "" 2 1 "" { "r" "l" } 0 +c "" 1 "" { "1" 1/2 "2" 1/2 } 0 +t "" 1 "Outcome 1" { 0, 0 } +t "" 2 "Outcome 2" { -2, -2 } +c "" 2 "" { "1" 1/2 "2" 1/2 } 0 +t "" 3 "Outcome 3" { 3, 3 } +t "" 4 "Outcome 4" { 1, 1 } +p "" 2 2 "" { "R" "L" } 0 +c "" 3 "" { "1" 1/2 "2" 1/2 } 0 +t "" 5 "Outcome 5" { 0, 4 } +t "" 6 "Outcome 6" { 0, 2 } +c "" 4 "" { "1" 1/2 "2" 1/2 } 0 +t "" 7 "Outcome 7" { 0, 5 } +t "" 8 "Outcome 8" { 0, 3 } diff --git a/tests/test_games/nature_rooted_generic.efg b/tests/test_games/nature_rooted_generic.efg new file mode 100644 index 000000000..c66f76052 --- /dev/null +++ b/tests/test_games/nature_rooted_generic.efg @@ -0,0 +1,18 @@ +EFG 2 R "2 player game: chance plays at the root" { "Player 1" "Player 2" } +"" + +c "" 1 "" { "1" 1/2 "2" 1/2 } 0 +p "" 1 1 "" { "B" "T" } 0 +p "" 2 1 "" { "r" "l" } 0 +t "" 1 "Outcome 1" { 1, -1 } +t "" 2 "Outcome 2" { 2, -2 } +p "" 2 2 "" { "R" "L" } 0 +t "" 3 "Outcome 3" { 3, -3 } +t "" 4 "Outcome 4" { 4, -4 } +p "" 1 1 "" { "B" "T" } 0 +p "" 2 1 "" { "r" "l" } 0 +t "" 5 "Outcome 5" { 5, -5 } +t "" 6 "Outcome 6" { 6, -6 } +p "" 2 2 "" { "R" "L" } 0 +t "" 7 "Outcome 7" { 7, -7 } +t "" 8 "Outcome 8" { 8, -8 } diff --git a/tests/test_games/nature_rooted_nongeneric.efg b/tests/test_games/nature_rooted_nongeneric.efg new file mode 100644 index 000000000..ee54da784 --- /dev/null +++ b/tests/test_games/nature_rooted_nongeneric.efg @@ -0,0 +1,18 @@ +EFG 2 R "2 player game: chance plays at the root" { "Player 1" "Player 2" } +"" + +c "" 1 "" { "1" 1/2 "2" 1/2 } 0 +p "" 1 1 "" { "B" "T" } 0 +p "" 2 2 "" { "r" "l" } 0 +t "" 1 "Outcome 1" { 0, 0 } +t "" 2 "Outcome 2" { 3, 3 } +p "" 2 1 "" { "R" "L" } 0 +t "" 3 "Outcome 3" { 0, 4 } +t "" 4 "Outcome 4" { 0, 5 } +p "" 1 1 "" { "B" "T" } 0 +p "" 2 2 "" { "r" "l" } 0 +t "" 5 "Outcome 5" { -2, -2 } +t "" 6 "Outcome 6" { 1, 1 } +p "" 2 1 "" { "R" "L" } 0 +t "" 7 "Outcome 7" { 0, 2 } +t "" 8 "Outcome 8" { 0, 3 } diff --git a/tests/test_games/wichardt.efg b/tests/test_games/wichardt.efg new file mode 100644 index 000000000..03e4b05de --- /dev/null +++ b/tests/test_games/wichardt.efg @@ -0,0 +1,18 @@ +EFG 2 R "Wichardt (2008): 2 players, imperfect recall" { "Player 1" "Player 2" } +"" + +p "" 1 1 "" { "R" "L" } 0 +p "" 1 2 "" { "r" "l" } 0 +p "" 2 1 "" { "B" "T" } 0 +t "" 1 "outcome 1" { 1, -1 } +t "" 2 "outcome 2" { -1, 1 } +p "" 2 1 "" { "B" "T" } 0 +t "" 3 "outcome 3" { -5, 5 } +t "" 4 "outcome 4" { -5, 5 } +p "" 1 2 "" { "r" "l" } 0 +p "" 2 1 "" { "B" "T" } 0 +t "" 5 "outcome 5" { -5, 5 } +t "" 6 "outcome 6" { -5, 5 } +p "" 2 1 "" { "B" "T" } 0 +t "" 7 "outcome 7" { -1, 1 } +t "" 8 "outcome 8" { 1, -1 }