diff --git a/src/pygambit/hsel.pxi b/src/pygambit/hsel.pxi index a931bfe479..b590b6503d 100644 --- a/src/pygambit/hsel.pxi +++ b/src/pygambit/hsel.pxi @@ -300,7 +300,19 @@ def _node_last_action(node: c_GameNode, player: str) -> object: """The label of the last action `player` took on the path to `node` (a raw handle, not a `Node`), wherever it fell -- `None` if `player` hasn't acted yet. The `c_GameNode`-only core of `_last_action`, used - directly by `HistoryView`, which must never hold a `Node`.""" + directly by `HistoryView`, which must never hold a `Node`. + + Raises + ------ + KeyError + If no player in the game has label `player`. + TypeError + If `player` is not a `str`. + ValueError + If `player` is an empty string or all spaces. + """ + game: Game = Game._wrap(node.deref().GetGame()) + game._resolve_player(player, "last_action") current: c_GameNode = node parent: c_GameNode = current.deref().GetParent() while parent != cython.cast(c_GameNode, NULL): @@ -373,7 +385,17 @@ class HistoryView: def last_action(self, player: str) -> str | None: """The label of the last action `player` took on the path to this - history, wherever it fell -- `None` if `player` hasn't acted yet.""" + history, wherever it fell -- `None` if `player` hasn't acted yet. + + Raises + ------ + KeyError + If no player in the game has label `player`. + TypeError + If `player` is not a `str`. + ValueError + If `player` is an empty string or all spaces. + """ return _node_last_action(self._node, player) @property diff --git a/tests/test_hsel.py b/tests/test_hsel.py index bdd79f6586..95023e9ef0 100644 --- a/tests/test_hsel.py +++ b/tests/test_hsel.py @@ -117,3 +117,63 @@ def test_get_histories_requires_selector(): game.get_histories(()) with pytest.raises(TypeError): game.get_histories("U") + + +def test_last_action_raises_on_invalid_player(): + game = gbt.ExtensiveGame(players=["A", "B"]) + game.append_move(gbt.H.path(), "A", ["U", "D"]) + game.append_move(gbt.H.plays, "B", ["x", "y"]) + + def key(h): + with pytest.raises(KeyError): + h.last_action("NoSuchPlayer") + return None + + game._get_groups(gbt.H.plays.by(key)) + + +def test_last_action_raises_on_non_str_player(): + game = gbt.ExtensiveGame(players=["A"]) + game.append_move(gbt.H.path(), "A", ["U", "D"]) + + def key(h): + with pytest.raises(TypeError): + h.last_action(1) + return None + + game._get_groups(gbt.H.plays.by(key)) + + +def test_last_action_raises_on_empty_player(): + game = gbt.ExtensiveGame(players=["A"]) + game.append_move(gbt.H.path(), "A", ["U", "D"]) + + def key(h): + with pytest.raises(ValueError): + h.last_action(" ") + return None + + game._get_groups(gbt.H.plays.by(key)) + + +def test_with_recall_raises_on_invalid_player(): + game = gbt.ExtensiveGame(players=["A", "B"]) + game.append_move(gbt.H.path(), "A", ["U", "D"]) + game.append_move(gbt.H.plays, "B", ["x", "y"]) + + grouped = gbt.H.path().by(lambda h: None).with_recall("NoSuchPlayer").plays + with pytest.raises(KeyError): + game._get_groups(grouped) + + +def test_with_recall_refines_key_by_last_action(): + game = gbt.ExtensiveGame(players=["A", "B"]) + game.append_move(gbt.H.path(), "A", ["U", "D"]) + game.append_move(gbt.H.plays, "B", ["x", "y"]) + + grouped = gbt.H.path().by(lambda h: None).with_recall("A").plays + groups = game._get_groups(grouped) + assert {k: [h.actions for h in v] for k, v in groups.items()} == { + (None, "U"): [("U", "x"), ("U", "y")], + (None, "D"): [("D", "x"), ("D", "y")], + }