Skip to content

Commit

Permalink
Feature/disallow immediate dev cards (#292)
Browse files Browse the repository at this point in the history
* Disallow playing dev cards immediately

* Allow dev cards pre-roll

* Hide buttons while playing dev cards
  • Loading branch information
zarns authored Nov 16, 2024
1 parent 76442c7 commit 6f16b7d
Show file tree
Hide file tree
Showing 6 changed files with 66 additions and 36 deletions.
38 changes: 17 additions & 21 deletions catanatron_core/catanatron/models/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions catanatron_core/catanatron/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions catanatron_core/catanatron/state_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
)


Expand Down Expand Up @@ -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
)
8 changes: 4 additions & 4 deletions tests/test_game.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
19 changes: 17 additions & 2 deletions tests/test_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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]
)
Expand Down Expand Up @@ -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)

Expand All @@ -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)
Expand Down
19 changes: 10 additions & 9 deletions ui/src/pages/ActionsToolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down Expand Up @@ -198,51 +199,51 @@ function PlayButtons() {
return (
<>
<OptionsButton
disabled={playableDevCardTypes.size === 0 || isPlayingYearOfPlenty}
disabled={playableDevCardTypes.size === 0 || isPlayingDevCard}
menuListId="use-menu-list"
icon={<SimCardIcon />}
items={useItems}
>
Use
</OptionsButton>
<OptionsButton
disabled={buildActionTypes.size === 0 || isPlayingYearOfPlenty}
disabled={buildActionTypes.size === 0 || isPlayingDevCard}
menuListId="build-menu-list"
icon={<BuildIcon />}
items={buildItems}
>
Buy
</OptionsButton>
<OptionsButton
disabled={tradeItems.length === 0 || isPlayingYearOfPlenty}
disabled={tradeItems.length === 0 || isPlayingDevCard}
menuListId="trade-menu-list"
icon={<AccountBalanceIcon />}
items={tradeItems}
>
Trade
</OptionsButton>
<Button
disabled={gameState.is_initial_build_phase}
disabled={gameState.is_initial_build_phase || isRoadBuilding}
variant="contained"
color="primary"
startIcon={<NavigateNextIcon />}
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"
}
</Button>
Expand Down

0 comments on commit 6f16b7d

Please sign in to comment.