From 90a5fb0da1ecc5ba5c931e652c1b36c1cdd53168 Mon Sep 17 00:00:00 2001 From: Jonathan Lamontagne Kratz <77954172+epicgamer17@users.noreply.github.com> Date: Mon, 16 Mar 2026 16:31:48 -0400 Subject: [PATCH] Fix generate_playable_actions to return empty list once game is won Store vps_to_win on State so it can be checked in generate_playable_actions. Pass vps_to_win through Game -> State constructor chain. --- catanatron/catanatron/game.py | 7 +++++- catanatron/catanatron/models/actions.py | 5 +++++ catanatron/catanatron/state.py | 3 +++ tests/test_immediate_win.py | 30 +++++++++++++++++++++++++ 4 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 tests/test_immediate_win.py diff --git a/catanatron/catanatron/game.py b/catanatron/catanatron/game.py index 8eff1436..c3144aef 100644 --- a/catanatron/catanatron/game.py +++ b/catanatron/catanatron/game.py @@ -117,7 +117,12 @@ def __init__( self.id = str(uuid.uuid4()) self.vps_to_win = vps_to_win - self.state = State(players, catan_map, discard_limit=discard_limit) + self.state = State( + players, + catan_map, + discard_limit=discard_limit, + vps_to_win=vps_to_win, + ) self.playable_actions = generate_playable_actions(self.state) def play(self, accumulators=[], decide_fn=None): diff --git a/catanatron/catanatron/models/actions.py b/catanatron/catanatron/models/actions.py index b0b35232..70bbc9fb 100644 --- a/catanatron/catanatron/models/actions.py +++ b/catanatron/catanatron/models/actions.py @@ -43,6 +43,11 @@ def generate_playable_actions(state: State) -> List[Action]: + # If someone won, no more actions. + for color in state.colors: + if get_actual_victory_points(state, color) >= state.vps_to_win: + return [] + action_prompt = state.current_prompt color = state.current_color() diff --git a/catanatron/catanatron/state.py b/catanatron/catanatron/state.py index 0659ddba..997099ba 100644 --- a/catanatron/catanatron/state.py +++ b/catanatron/catanatron/state.py @@ -88,6 +88,7 @@ def __init__( players: Sequence[Player], catan_map=None, discard_limit=7, + vps_to_win=10, initialize=True, ): if initialize: @@ -95,6 +96,7 @@ def __init__( self.colors = tuple([player.color for player in self.players]) self.board = Board(catan_map or CatanMap.from_template(BASE_MAP_TEMPLATE)) self.discard_limit = discard_limit + self.vps_to_win = vps_to_win # feature-ready dictionary self.player_state = dict() @@ -152,6 +154,7 @@ def copy(self): state_copy = State([], None, initialize=False) state_copy.players = self.players state_copy.discard_limit = self.discard_limit # immutable + state_copy.vps_to_win = self.vps_to_win state_copy.board = self.board.copy() diff --git a/tests/test_immediate_win.py b/tests/test_immediate_win.py new file mode 100644 index 00000000..43754d76 --- /dev/null +++ b/tests/test_immediate_win.py @@ -0,0 +1,30 @@ + +from catanatron.game import Game +from catanatron.models.player import Color, SimplePlayer +from catanatron.models.actions import generate_playable_actions +from catanatron.state_functions import player_key + +def test_immediate_win_logic(): + # 1. Initialize a game + players = [SimplePlayer(Color.RED), SimplePlayer(Color.BLUE)] + vps_to_win = 10 + game = Game(players, vps_to_win=vps_to_win) + + p0 = game.state.current_player() + p0_key = player_key(game.state, p0.color) + + # 2. Initially p0 should have some actions (ROLL or initial build) + actions = generate_playable_actions(game.state) + assert len(actions) > 0 + + # 3. Buff p0 to 10 VPs manually + game.state.player_state[f"{p0_key}_ACTUAL_VICTORY_POINTS"] = 10 + + # 4. Verify generate_playable_actions returns empty list IMMEDIATELY + actions = generate_playable_actions(game.state) + assert actions == [], f"Expected 0 actions, got {len(actions)}" + + print("Test passed: generate_playable_actions returns empty list once VPs reached!") + +if __name__ == "__main__": + test_immediate_win_logic()