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
}
onClick={
- isRoll
- ? rollAction
- : isDiscard
+ isDiscard
? proceedAction
: isMoveRobber
? setIsMovingRobber
: isPlayingYearOfPlenty || isPlayingMonopoly
? handleOpenResourceSelector
+ : isRoll
+ ? rollAction
: endTurnAction
}
>
{
- isRoll ? "ROLL" :
isDiscard ? "DISCARD" :
isMoveRobber ? "ROB" :
isPlayingYearOfPlenty || isPlayingMonopoly ? "SELECT" :
+ isRoll ? "ROLL" :
"END"
}