• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

bcollazo / catanatron / 9729177722

30 Jun 2024 03:54AM UTC coverage: 95.696% (-0.2%) from 95.871%
9729177722

Pull #274

github

web-flow
Merge 655b0c47e into 2a4f078ba
Pull Request #274: Remove dead coordinate system code

20 of 20 new or added lines in 2 files covered. (100.0%)

1 existing line in 1 file now uncovered.

1334 of 1394 relevant lines covered (95.7%)

0.96 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.03
/catanatron_core/catanatron/models/actions.py
1
"""
2
Move-generation functions (these return a list of actions that can be taken 
3
by current player). Main function is generate_playable_actions.
4
"""
5
import operator as op
1✔
6
from functools import reduce
1✔
7
from typing import Any, Dict, List, Set, Tuple, Union
1✔
8

9
from catanatron.models.decks import (
1✔
10
    CITY_COST_FREQDECK,
11
    ROAD_COST_FREQDECK,
12
    SETTLEMENT_COST_FREQDECK,
13
    freqdeck_can_draw,
14
    freqdeck_contains,
15
    freqdeck_count,
16
    freqdeck_from_listdeck,
17
)
18
from catanatron.models.enums import (
1✔
19
    RESOURCES,
20
    Action,
21
    ActionPrompt,
22
    ActionType,
23
    BRICK,
24
    ORE,
25
    FastResource,
26
    SETTLEMENT,
27
    SHEEP,
28
    WHEAT,
29
    WOOD,
30
)
31
from catanatron.state_functions import (
1✔
32
    get_player_buildings,
33
    get_player_freqdeck,
34
    player_can_afford_dev_card,
35
    player_can_play_dev,
36
    player_has_rolled,
37
    player_key,
38
    player_num_resource_cards,
39
    player_resource_freqdeck_contains,
40
)
41

42

43
def generate_playable_actions(state) -> List[Action]:
1✔
44
    action_prompt = state.current_prompt
1✔
45
    color = state.current_color()
1✔
46

47
    if action_prompt == ActionPrompt.BUILD_INITIAL_SETTLEMENT:
1✔
48
        return settlement_possibilities(state, color, True)
1✔
49
    elif action_prompt == ActionPrompt.BUILD_INITIAL_ROAD:
1✔
50
        return initial_road_possibilities(state, color)
1✔
51
    elif action_prompt == ActionPrompt.MOVE_ROBBER:
1✔
52
        return robber_possibilities(state, color)
1✔
53
    elif action_prompt == ActionPrompt.PLAY_TURN:
1✔
54
        if state.is_road_building:
1✔
55
            actions = road_building_possibilities(state, color, False)
1✔
56
        elif not player_has_rolled(state, color):
1✔
57
            actions = [Action(color, ActionType.ROLL, None)]
1✔
58
            if player_can_play_dev(state, color, "KNIGHT"):
1✔
59
                actions.append(Action(color, ActionType.PLAY_KNIGHT_CARD, None))
1✔
60
        else:
61
            actions = [Action(color, ActionType.END_TURN, None)]
1✔
62
            actions.extend(road_building_possibilities(state, color))
1✔
63
            actions.extend(settlement_possibilities(state, color))
1✔
64
            actions.extend(city_possibilities(state, color))
1✔
65

66
            can_buy_dev_card = (
1✔
67
                player_can_afford_dev_card(state, color)
68
                and len(state.development_listdeck) > 0
69
            )
70
            if can_buy_dev_card:
1✔
71
                actions.append(Action(color, ActionType.BUY_DEVELOPMENT_CARD, None))
1✔
72

73
            # Play Dev Cards
74
            if player_can_play_dev(state, color, "YEAR_OF_PLENTY"):
1✔
75
                actions.extend(
1✔
76
                    year_of_plenty_possibilities(color, state.resource_freqdeck)
77
                )
78
            if player_can_play_dev(state, color, "MONOPOLY"):
1✔
79
                actions.extend(monopoly_possibilities(color))
1✔
80
            if player_can_play_dev(state, color, "KNIGHT"):
1✔
81
                actions.append(Action(color, ActionType.PLAY_KNIGHT_CARD, None))
1✔
82
            if (
1✔
83
                player_can_play_dev(state, color, "ROAD_BUILDING")
84
                and len(road_building_possibilities(state, color, False)) > 0
85
            ):
86
                actions.append(Action(color, ActionType.PLAY_ROAD_BUILDING, None))
1✔
87

88
            # Trade
89
            actions.extend(maritime_trade_possibilities(state, color))
1✔
90
        return actions
1✔
91
    elif action_prompt == ActionPrompt.DISCARD:
1✔
92
        return discard_possibilities(color)
1✔
93
    elif action_prompt == ActionPrompt.DECIDE_TRADE:
1✔
94
        actions = [Action(color, ActionType.REJECT_TRADE, state.current_trade)]
1✔
95

96
        # can only accept if have enough cards
97
        freqdeck = get_player_freqdeck(state, color)
1✔
98
        asked = state.current_trade[5:10]
1✔
99
        if freqdeck_contains(freqdeck, asked):
1✔
100
            actions.append(Action(color, ActionType.ACCEPT_TRADE, state.current_trade))
1✔
101

102
        return actions
1✔
103
    elif action_prompt == ActionPrompt.DECIDE_ACCEPTEES:
1✔
104
        # you should be able to accept for each of the "accepting players"
105
        actions = [Action(color, ActionType.CANCEL_TRADE, None)]
1✔
106

107
        for other_color, accepted in zip(state.colors, state.acceptees):
1✔
108
            if accepted:
1✔
109
                actions.append(
1✔
110
                    Action(
111
                        color,
112
                        ActionType.CONFIRM_TRADE,
113
                        (*state.current_trade[:10], other_color),
114
                    )
115
                )
116
        return actions
1✔
117
    else:
118
        raise RuntimeError("Unknown ActionPrompt: " + str(action_prompt))
×
119

120

121
def monopoly_possibilities(color) -> List[Action]:
1✔
122
    return [Action(color, ActionType.PLAY_MONOPOLY, card) for card in RESOURCES]
1✔
123

124

125
def year_of_plenty_possibilities(color, freqdeck: List[int]) -> List[Action]:
1✔
126
    options: Set[Union[Tuple[FastResource, FastResource], Tuple[FastResource]]] = set()
1✔
127
    for i, first_card in enumerate(RESOURCES):
1✔
128
        for j in range(i, len(RESOURCES)):
1✔
129
            second_card = RESOURCES[j]  # doing it this way to not repeat
1✔
130

131
            to_draw = freqdeck_from_listdeck([first_card, second_card])
1✔
132
            if freqdeck_contains(freqdeck, to_draw):
1✔
133
                options.add((first_card, second_card))
1✔
134
            else:  # try allowing player select 1 card only.
135
                if freqdeck_can_draw(freqdeck, 1, first_card):
1✔
136
                    options.add((first_card,))
1✔
137
                if freqdeck_can_draw(freqdeck, 1, second_card):
1✔
138
                    options.add((second_card,))
1✔
139

140
    return list(
1✔
141
        map(
142
            lambda cards: Action(color, ActionType.PLAY_YEAR_OF_PLENTY, tuple(cards)),
143
            options,
144
        )
145
    )
146

147

148
def road_building_possibilities(state, color, check_money=True) -> List[Action]:
1✔
149
    key = player_key(state, color)
1✔
150

151
    # Check if can't build any more roads.
152
    has_roads_available = state.player_state[f"{key}_ROADS_AVAILABLE"] > 0
1✔
153
    if not has_roads_available:
1✔
154
        return []
1✔
155

156
    # Check if need to pay for roads but can't afford them.
157
    has_money = player_resource_freqdeck_contains(state, color, ROAD_COST_FREQDECK)
1✔
158
    if check_money and not has_money:
1✔
159
        return []
1✔
160

161
    buildable_edges = state.board.buildable_edges(color)
1✔
162
    return [Action(color, ActionType.BUILD_ROAD, edge) for edge in buildable_edges]
1✔
163

164

165
def settlement_possibilities(state, color, initial_build_phase=False) -> List[Action]:
1✔
166
    if initial_build_phase:
1✔
167
        buildable_node_ids = state.board.buildable_node_ids(
1✔
168
            color, initial_build_phase=True
169
        )
170
        return [
1✔
171
            Action(color, ActionType.BUILD_SETTLEMENT, node_id)
172
            for node_id in buildable_node_ids
173
        ]
174
    else:
175
        key = player_key(state, color)
1✔
176
        has_money = player_resource_freqdeck_contains(
1✔
177
            state, color, SETTLEMENT_COST_FREQDECK
178
        )
179
        has_settlements_available = (
1✔
180
            state.player_state[f"{key}_SETTLEMENTS_AVAILABLE"] > 0
181
        )
182
        if has_money and has_settlements_available:
1✔
183
            buildable_node_ids = state.board.buildable_node_ids(color)
1✔
184
            return [
1✔
185
                Action(color, ActionType.BUILD_SETTLEMENT, node_id)
186
                for node_id in buildable_node_ids
187
            ]
188
        else:
189
            return []
1✔
190

191

192
def city_possibilities(state, color) -> List[Action]:
1✔
193
    key = player_key(state, color)
1✔
194

195
    can_buy_city = player_resource_freqdeck_contains(state, color, CITY_COST_FREQDECK)
1✔
196
    if not can_buy_city:
1✔
197
        return []
1✔
198

199
    has_cities_available = state.player_state[f"{key}_CITIES_AVAILABLE"] > 0
1✔
200
    if not has_cities_available:
1✔
UNCOV
201
        return []
×
202

203
    return [
1✔
204
        Action(color, ActionType.BUILD_CITY, node_id)
205
        for node_id in get_player_buildings(state, color, SETTLEMENT)
206
    ]
207

208

209
def robber_possibilities(state, color) -> List[Action]:
1✔
210
    actions = []
1✔
211
    for coordinate, tile in state.board.map.land_tiles.items():
1✔
212
        if coordinate == state.board.robber_coordinate:
1✔
213
            continue  # ignore. must move robber.
1✔
214

215
        # each tile can yield a (move-but-cant-steal) action or
216
        #   several (move-and-steal-from-x) actions.
217
        to_steal_from = set()  # set of player_indexs
1✔
218
        for node_id in tile.nodes.values():
1✔
219
            building = state.board.buildings.get(node_id, None)
1✔
220
            if building is not None:
1✔
221
                candidate_color = building[0]
1✔
222
                if (
1✔
223
                    player_num_resource_cards(state, candidate_color) >= 1
224
                    and color != candidate_color  # can't play yourself
225
                ):
226
                    to_steal_from.add(candidate_color)
1✔
227

228
        if len(to_steal_from) == 0:
1✔
229
            actions.append(
1✔
230
                Action(color, ActionType.MOVE_ROBBER, (coordinate, None, None))
231
            )
232
        else:
233
            for enemy_color in to_steal_from:
1✔
234
                actions.append(
1✔
235
                    Action(
236
                        color, ActionType.MOVE_ROBBER, (coordinate, enemy_color, None)
237
                    )
238
                )
239

240
    return actions
1✔
241

242

243
def initial_road_possibilities(state, color) -> List[Action]:
1✔
244
    # Must be connected to last settlement
245
    last_settlement_node_id = state.buildings_by_color[color][SETTLEMENT][-1]
1✔
246

247
    buildable_edges = filter(
1✔
248
        lambda edge: last_settlement_node_id in edge,
249
        state.board.buildable_edges(color),
250
    )
251
    return [Action(color, ActionType.BUILD_ROAD, edge) for edge in buildable_edges]
1✔
252

253

254
def discard_possibilities(color) -> List[Action]:
1✔
255
    return [Action(color, ActionType.DISCARD, None)]
1✔
256
    # TODO: Be robust to high dimensionality of DISCARD
257
    # hand = player.resource_deck.to_array()
258
    # num_cards = player.resource_deck.num_cards()
259
    # num_to_discard = num_cards // 2
260

261
    # num_possibilities = ncr(num_cards, num_to_discard)
262
    # if num_possibilities > 100:  # if too many, just take first N
263
    #     return [Action(player, ActionType.DISCARD, hand[:num_to_discard])]
264

265
    # to_discard = itertools.combinations(hand, num_to_discard)
266
    # return list(
267
    #     map(
268
    #         lambda combination: Action(player, ActionType.DISCARD, combination),
269
    #         to_discard,
270
    #     )
271
    # )
272

273

274
def ncr(n, r):
1✔
275
    """n choose r. helper for discard_possibilities"""
276
    r = min(r, n - r)
×
277
    numer = reduce(op.mul, range(n, n - r, -1), 1)
×
278
    denom = reduce(op.mul, range(1, r + 1), 1)
×
279
    return numer // denom
×
280

281

282
def maritime_trade_possibilities(state, color) -> List[Action]:
1✔
283
    hand_freqdeck = [
1✔
284
        player_num_resource_cards(state, color, resource) for resource in RESOURCES
285
    ]
286
    port_resources = state.board.get_player_port_resources(color)
1✔
287
    trade_offers = inner_maritime_trade_possibilities(
1✔
288
        hand_freqdeck, state.resource_freqdeck, port_resources
289
    )
290

291
    return list(
1✔
292
        map(lambda t: Action(color, ActionType.MARITIME_TRADE, t), trade_offers)
293
    )
294

295

296
def inner_maritime_trade_possibilities(hand_freqdeck, bank_freqdeck, port_resources):
1✔
297
    """This inner function is to make this logic more shareable"""
298
    trade_offers = set()
1✔
299

300
    # Get lowest rate per resource
301
    rates: Dict[FastResource, int] = {WOOD: 4, BRICK: 4, SHEEP: 4, WHEAT: 4, ORE: 4}
1✔
302
    if None in port_resources:
1✔
303
        rates = {WOOD: 3, BRICK: 3, SHEEP: 3, WHEAT: 3, ORE: 3}
1✔
304
    for resource in port_resources:
1✔
305
        if resource != None:
1✔
306
            rates[resource] = 2
1✔
307

308
    # For resource in hand
309
    for index, resource in enumerate(RESOURCES):
1✔
310
        amount = hand_freqdeck[index]
1✔
311
        if amount >= rates[resource]:
1✔
312
            resource_out: List[Any] = [resource] * rates[resource]
1✔
313
            resource_out += [None] * (4 - rates[resource])
1✔
314
            for j_resource in RESOURCES:
1✔
315
                if (
1✔
316
                    resource != j_resource
317
                    and freqdeck_count(bank_freqdeck, j_resource) > 0
318
                ):
319
                    trade_offer = tuple(resource_out + [j_resource])
1✔
320
                    trade_offers.add(trade_offer)
1✔
321

322
    return trade_offers
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc