diff --git a/catanatron_core/catanatron/models/actions.py b/catanatron_core/catanatron/models/actions.py index 9bb414dc..33675043 100644 --- a/catanatron_core/catanatron/models/actions.py +++ b/catanatron_core/catanatron/models/actions.py @@ -52,13 +52,24 @@ def generate_playable_actions(state) -> List[Action]: return robber_possibilities(state, color) elif action_prompt == ActionPrompt.PLAY_TURN: if state.is_road_building: - actions = road_building_possibilities(state, color, False) - elif not player_has_rolled(state, color): - actions = [Action(color, ActionType.ROLL, None)] - if player_can_play_dev(state, color, "KNIGHT"): - actions.append(Action(color, ActionType.PLAY_KNIGHT_CARD, None)) + return road_building_possibilities(state, color, False) + actions = [] + # Allow playing dev cards before and after rolling + if player_can_play_dev(state, color, "YEAR_OF_PLENTY"): + actions.extend(year_of_plenty_possibilities(color, state.resource_freqdeck)) + if player_can_play_dev(state, color, "MONOPOLY"): + actions.extend(monopoly_possibilities(color)) + if player_can_play_dev(state, color, "KNIGHT"): + actions.append(Action(color, ActionType.PLAY_KNIGHT_CARD, None)) + if ( + player_can_play_dev(state, color, "ROAD_BUILDING") + and len(road_building_possibilities(state, color, False)) > 0 + ): + actions.append(Action(color, ActionType.PLAY_ROAD_BUILDING, None)) + if not player_has_rolled(state, color): + actions.append(Action(color, ActionType.ROLL, None)) else: - actions = [Action(color, ActionType.END_TURN, None)] + actions.append(Action(color, ActionType.END_TURN, None)) actions.extend(road_building_possibilities(state, color)) actions.extend(settlement_possibilities(state, color)) actions.extend(city_possibilities(state, color)) @@ -70,21 +81,6 @@ def generate_playable_actions(state) -> List[Action]: if can_buy_dev_card: actions.append(Action(color, ActionType.BUY_DEVELOPMENT_CARD, None)) - # Play Dev Cards - if player_can_play_dev(state, color, "YEAR_OF_PLENTY"): - actions.extend( - year_of_plenty_possibilities(color, state.resource_freqdeck) - ) - if player_can_play_dev(state, color, "MONOPOLY"): - actions.extend(monopoly_possibilities(color)) - if player_can_play_dev(state, color, "KNIGHT"): - actions.append(Action(color, ActionType.PLAY_KNIGHT_CARD, None)) - if ( - player_can_play_dev(state, color, "ROAD_BUILDING") - and len(road_building_possibilities(state, color, False)) > 0 - ): - actions.append(Action(color, ActionType.PLAY_ROAD_BUILDING, None)) - # Trade actions.extend(maritime_trade_possibilities(state, color)) return actions diff --git a/catanatron_core/catanatron/state.py b/catanatron_core/catanatron/state.py index 665eab8c..b6fabef7 100644 --- a/catanatron_core/catanatron/state.py +++ b/catanatron_core/catanatron/state.py @@ -76,6 +76,10 @@ # de-normalized features (for performance since we think they are good features) "ACTUAL_VICTORY_POINTS": 0, "LONGEST_ROAD_LENGTH": 0, + "KNIGHT_OWNED_AT_START": False, + "MONOPOLY_OWNED_AT_START": False, + "YEAR_OF_PLENTY_OWNED_AT_START": False, + "ROAD_BUILDING_OWNED_AT_START": False, } for resource in RESOURCES: PLAYER_INITIAL_STATE[f"{resource}_IN_HAND"] = 0 diff --git a/catanatron_core/catanatron/state_functions.py b/catanatron_core/catanatron/state_functions.py index acf71799..5bec89e5 100644 --- a/catanatron_core/catanatron/state_functions.py +++ b/catanatron_core/catanatron/state_functions.py @@ -228,6 +228,7 @@ def player_can_play_dev(state, color, dev_card): return ( not state.player_state[f"{key}_HAS_PLAYED_DEVELOPMENT_CARD_IN_TURN"] and state.player_state[f"{key}_{dev_card}_IN_HAND"] >= 1 + and state.player_state[f"{key}_{dev_card}_OWNED_AT_START"] ) @@ -334,3 +335,16 @@ def player_clean_turn(state, color): key = player_key(state, color) state.player_state[f"{key}_HAS_PLAYED_DEVELOPMENT_CARD_IN_TURN"] = False state.player_state[f"{key}_HAS_ROLLED"] = False + # Dev cards owned this turn will be playable next turn + state.player_state[f"{key}_KNIGHT_OWNED_AT_START"] = ( + state.player_state[f"{key}_KNIGHT_IN_HAND"] > 0 + ) + state.player_state[f"{key}_MONOPOLY_OWNED_AT_START"] = ( + state.player_state[f"{key}_MONOPOLY_IN_HAND"] > 0 + ) + state.player_state[f"{key}_YEAR_OF_PLENTY_OWNED_AT_START"] = ( + state.player_state[f"{key}_YEAR_OF_PLENTY_IN_HAND"] > 0 + ) + state.player_state[f"{key}_ROAD_BUILDING_OWNED_AT_START"] = ( + state.player_state[f"{key}_ROAD_BUILDING_IN_HAND"] > 0 + ) diff --git a/tests/test_game.py b/tests/test_game.py index c00d7561..b6326537 100644 --- a/tests/test_game.py +++ b/tests/test_game.py @@ -4,6 +4,7 @@ from catanatron.state_functions import ( get_actual_victory_points, get_player_freqdeck, + player_clean_turn, player_has_rolled, ) from catanatron.game import Game, is_valid_trade @@ -321,16 +322,15 @@ def test_play_road_building(fake_roll_dice): p0 = game.state.players[0] player_deck_replenish(game.state, p0.color, ROAD_BUILDING) + # Simulate end of turn which updates the OWNED_AT_START flags + player_clean_turn(game.state, p0.color) + # play initial phase while not any( a.action_type == ActionType.ROLL for a in game.state.playable_actions ): game.play_tick() - # roll not a 7 - fake_roll_dice.return_value = (1, 2) - game.play_tick() # roll - game.execute(Action(p0.color, ActionType.PLAY_ROAD_BUILDING, None)) assert game.state.is_road_building assert game.state.free_roads_available == 2 diff --git a/tests/test_state.py b/tests/test_state.py index cb4414f8..31e49085 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -3,6 +3,7 @@ from catanatron.state import State, apply_action from catanatron.state_functions import ( get_dev_cards_in_hand, + player_clean_turn, player_freqdeck_add, player_deck_replenish, player_num_dev_cards, @@ -122,6 +123,10 @@ def test_play_year_of_plenty_gives_player_resources(): player_to_act = players[0] player_deck_replenish(state, player_to_act.color, YEAR_OF_PLENTY, 1) + # Simulate end of turn which updates the OWNED_AT_START flags + player_clean_turn(state, player_to_act.color) + + # Now try to play the card (as if it's next turn) action_to_execute = Action( player_to_act.color, ActionType.PLAY_YEAR_OF_PLENTY, [ORE, WHEAT] ) @@ -151,6 +156,10 @@ def test_play_monopoly_player_steals_cards(): player_deck_replenish(state, player_to_steal_from_2.color, ORE, 2) player_deck_replenish(state, player_to_steal_from_2.color, WHEAT, 1) + # Simulate end of turn which updates the OWNED_AT_START flags + player_clean_turn(state, player_to_act.color) + + # Now try to play the card (as if it's next turn) action_to_execute = Action(player_to_act.color, ActionType.PLAY_MONOPOLY, ORE) apply_action(state, action_to_execute) @@ -170,8 +179,14 @@ def test_can_only_play_one_dev_card_per_turn(): ] state = State(players) - player_deck_replenish(state, players[0].color, YEAR_OF_PLENTY, 2) - action = Action(players[0].color, ActionType.PLAY_YEAR_OF_PLENTY, 2 * [BRICK]) + player_to_act = players[0] + player_deck_replenish(state, player_to_act.color, YEAR_OF_PLENTY, 2) + + # Simulate end of turn which updates the OWNED_AT_START flags + player_clean_turn(state, player_to_act.color) + + # Now try to play the card (as if it's next turn) + action = Action(player_to_act.color, ActionType.PLAY_YEAR_OF_PLENTY, 2 * [BRICK]) apply_action(state, action) with pytest.raises(ValueError): # shouldnt be able to play two dev cards apply_action(state, action) diff --git a/ui/src/pages/ActionsToolbar.js b/ui/src/pages/ActionsToolbar.js index 74285758..cdb18270 100644 --- a/ui/src/pages/ActionsToolbar.js +++ b/ui/src/pages/ActionsToolbar.js @@ -47,13 +47,14 @@ function PlayButtons() { [enqueueSnackbar, closeSnackbar] ); - const { gameState, isPlayingMonopoly, isPlayingYearOfPlenty } = state; + const { gameState, isPlayingMonopoly, isPlayingYearOfPlenty, isRoadBuilding } = state; const key = playerKey(gameState, gameState.current_color); const isRoll = gameState.current_prompt === "PLAY_TURN" && !gameState.player_state[`${key}_HAS_ROLLED`]; const isDiscard = gameState.current_prompt === "DISCARD"; const isMoveRobber = gameState.current_prompt === "MOVE_ROBBER"; + const isPlayingDevCard = isPlayingMonopoly || isPlayingYearOfPlenty || isRoadBuilding; const playableDevCardTypes = new Set( gameState.current_playable_actions .filter((action) => action[1].startsWith("PLAY")) @@ -198,7 +199,7 @@ function PlayButtons() { return ( <> } items={useItems} @@ -206,7 +207,7 @@ function PlayButtons() { Use } items={buildItems} @@ -214,7 +215,7 @@ function PlayButtons() { Buy } items={tradeItems} @@ -222,27 +223,27 @@ function PlayButtons() { Trade