Skip to content

Commit 1202241

Browse files
author
Nick Schaap
committed
updates
1 parent f3f0a8c commit 1202241

File tree

10 files changed

+274
-66
lines changed

10 files changed

+274
-66
lines changed

lib/experiments/win_stats.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
logger = logging.getLogger(__name__)
1212

1313

14-
def win_stats(num_games: int):
14+
def win_stats(num_games: int) -> None:
1515
results: list[str] = []
1616
for i in range(1, num_games + 1):
1717
game = Game()

lib/gameplay/bank.py

+29-12
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from lib.gameplay.pieces import ResourceCard, DevelopmentCard, CardType
22
from lib.gameplay.hex import ResourceType
3-
from typing import TYPE_CHECKING
3+
from typing import TYPE_CHECKING, Union
44
import numpy as np
55
import random
66

@@ -29,18 +29,31 @@ def get_cards(self, *resourceType: ResourceType) -> list[ResourceCard]:
2929
return [self.get_card(resource) for resource in resourceType]
3030

3131
def get_card(self, resourceType: ResourceType) -> ResourceCard:
32-
if resourceType == ResourceType.BRICK:
33-
return self.brick_cards.pop()
34-
if resourceType == ResourceType.WOOD:
35-
return self.wood_cards.pop()
36-
if resourceType == ResourceType.SHEEP:
37-
return self.sheep_cards.pop()
38-
if resourceType == ResourceType.WHEAT:
39-
return self.wheat_cards.pop()
40-
if resourceType == ResourceType.ORE:
41-
return self.ore_cards.pop()
32+
try:
33+
if resourceType == ResourceType.BRICK:
34+
return self.brick_cards.pop()
35+
if resourceType == ResourceType.WOOD:
36+
return self.wood_cards.pop()
37+
if resourceType == ResourceType.SHEEP:
38+
return self.sheep_cards.pop()
39+
if resourceType == ResourceType.WHEAT:
40+
return self.wheat_cards.pop()
41+
if resourceType == ResourceType.ORE:
42+
return self.ore_cards.pop()
43+
except IndexError:
44+
raise ValueError(f"Bank ran out of {resourceType}")
4245

43-
def get_dev_card(self) -> DevelopmentCard:
46+
def get_dev_card(self, type: Union[CardType, None] = None) -> DevelopmentCard:
47+
if type is not None:
48+
# Find the next card of the passed in type
49+
def is_type(card: DevelopmentCard) -> bool:
50+
return card.get_type() == type
51+
52+
index = next((i for i, v in enumerate(self.dev_cards) if is_type(v)), None)
53+
if index is None:
54+
raise ValueError(f"No {type} cards in the bank")
55+
return self.dev_cards.pop(index)
56+
# Default to a random card
4457
index = np.random.randint(0, len(self.dev_cards))
4558
return self.dev_cards.pop(index)
4659

@@ -59,6 +72,10 @@ def return_card(self, card: ResourceCard) -> None:
5972
if card.resourceType == ResourceType.ORE:
6073
self.ore_cards.append(card)
6174

75+
def return_cards(self, cards: list[ResourceCard]) -> None:
76+
for card in cards:
77+
self.return_card(card)
78+
6279
def purchase_road(self, player: "Player"):
6380
cards = player.take_resources_from_player(
6481
[ResourceType.BRICK, ResourceType.WOOD]

lib/gameplay/game.py

+40-9
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,11 @@ def __init__(self, num_players: int = NUM_PLAYERS, game_delay: int = 0):
5757
self.board = Board()
5858
self.dice = Dice()
5959
self.players = self.setup_players(num_players)
60+
self.num_players = num_players
6061
self.listeners = []
6162
self.game_delay = game_delay
63+
self.player_with_largest_army: Union["Player", None] = None
64+
self.player_with_longest_road: Union["Player", None] = None
6265

6366
def listen(self, callback: Callable[[GameEvent], None]) -> None:
6467
self.listeners.append(callback)
@@ -93,21 +96,50 @@ def get_player_by_color(self, color: str) -> Player:
9396
raise ValueError("Player not found")
9497

9598
def get_player_with_longest_road(self) -> Union[Player, None]:
96-
player_with_longest_road = None
97-
longest_road = 4
99+
player_with_longest_road = self.player_with_longest_road
100+
101+
# Minimum length for longest road is greater than 4
102+
longest_road = (
103+
4
104+
if player_with_longest_road is None
105+
else self.board.longest_road(player_with_longest_road)
106+
)
98107
for player in self.players:
108+
if player == player_with_longest_road:
109+
continue
99110
road_length = self.board.longest_road(player)
111+
# Must be explicitly larger than the longest so far
100112
if road_length > longest_road:
101113
player_with_longest_road = player
102114
longest_road = road_length
103-
return player_with_longest_road
115+
self.player_with_longest_road = player_with_longest_road
116+
return self.player_with_longest_road
117+
118+
def get_player_with_largest_army(self) -> Union[Player, None]:
119+
player_with_largest_army = self.player_with_largest_army
120+
# Minimum length for largest army is greater than 3
121+
largest_army = (
122+
2
123+
if self.player_with_largest_army is None
124+
else self.player_with_largest_army.largest_army_size()
125+
)
126+
for player in self.players:
127+
if player == player_with_largest_army:
128+
continue
129+
army_size = player.largest_army_size()
130+
if army_size > largest_army:
131+
player_with_largest_army = player
132+
largest_army = army_size
133+
self.player_with_largest_army = player_with_largest_army
134+
return self.player_with_largest_army
104135

105136
def step(self):
106137
curr_player = self.get_current_player()
107-
138+
playerWithLargestArmy = self.get_player_with_largest_army()
139+
playerWithLongestRoad = self.get_player_with_longest_road()
108140
for player in self.players:
109141
logger.info(
110-
f"{player} has {player.points(self.players, self.get_player_with_longest_road())} points"
142+
f"{player} has {player.points(playerWithLongestRoad, playerWithLargestArmy)} points"
111143
)
112144

113145
logger.info(f"{curr_player}'s turn")
@@ -119,7 +151,7 @@ def step(self):
119151
for player in self.players:
120152
if len(player.resources) > 7:
121153
player.split_cards(self.bank)
122-
curr_player.rob(self.board, self.bank)
154+
curr_player.move_robber(self.board, self.bank)
123155
else:
124156
for player in self.players:
125157
logger.info(f"{player} has {len(player.resources)} resources")
@@ -131,7 +163,7 @@ def step(self):
131163
if curr_player.points(self.players, self.get_player_with_longest_road()) >= 10:
132164
self.winning_player = curr_player
133165

134-
self.current_player = (self.current_player + 1) % NUM_PLAYERS
166+
self.current_player = (self.current_player + 1) % self.num_players
135167
if self.game_delay > 0:
136168
time.sleep(self.game_delay / 1000)
137169

@@ -142,9 +174,8 @@ def play(self):
142174

143175
while self.winning_player is None:
144176
self.step()
145-
# TODO: Implement end game condition
146177
round_num += 1
147-
if round_num == 300:
178+
if round_num == 100 * self.num_players:
148179
self.winning_player = self.get_current_player()
149180
logger.warning("Game ended in a draw")
150181
logger.info(f"{self.winning_player} wins in {round_num} rounds!")

lib/gameplay/hex.py

+17
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,23 @@ def attach_value(self, value: int):
118118
raise ValueError("Invalid value")
119119
self.value = value
120120

121+
def likelihood(self) -> float:
122+
if self.value is None:
123+
return 0
124+
if self.value in [2, 12]:
125+
return 0.03
126+
if self.value in [3, 11]:
127+
return 0.06
128+
if self.value in [4, 10]:
129+
return 0.08
130+
if self.value in [5, 9]:
131+
return 0.11
132+
if self.value in [6, 8]:
133+
return 0.14
134+
if self.value == 7:
135+
return 0.17
136+
raise ValueError("Invalid value")
137+
121138
def attach_edges(self, edges: list[Edge]):
122139
if len(edges) != 6:
123140
raise ValueError("Must have 6 edges")

lib/gameplay/player.py

+63-24
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from lib.gameplay.bank import Bank
1010
from lib.gameplay.board import Board
1111
from functools import reduce
12+
from lib.gameplay.hex import Hex
1213
from typing import Union
1314
from lib.gameplay.hex import ResourceType
1415
import logging
@@ -57,25 +58,39 @@ def largest_army_size(self) -> int:
5758
)
5859

5960
def points(
60-
self, players: list["Player"], playerWithLongestRoad: Union["Player", None]
61+
self,
62+
playerWithLongestRoad: Union["Player", None],
63+
playerWithLargestArmy: Union["Player", None],
6164
) -> int:
62-
# TODO: Add support for largest army and longest road
6365
return (
6466
reduce(lambda acc, curr: acc + curr.get_points(), self.cities, 0)
6567
+ reduce(lambda acc, curr: acc + curr.get_points(), self.settlements, 0)
6668
+ reduce(
6769
lambda acc, curr: acc + curr.get_points(), self.development_cards, 0
6870
)
6971
+ (2 if playerWithLongestRoad == self else 0)
72+
+ (2 if playerWithLargestArmy == self else 0)
7073
)
7174

75+
def give_development_card(self, card: DevelopmentCard) -> None:
76+
self.development_cards.append(card)
77+
7278
def get_active_settlements(self) -> list[Settlement]:
7379
return [
7480
settlement
7581
for settlement in self.settlements
7682
if settlement.vertex is not None
7783
]
7884

85+
def num_active_settlements(self) -> int:
86+
return len(self.get_active_settlements())
87+
88+
def get_active_cities(self) -> list[City]:
89+
return [city for city in self.cities if city.vertex is not None]
90+
91+
def num_active_cities(self) -> int:
92+
return len(self.get_active_cities())
93+
7994
def get_unplaced_settlement(self) -> Union[Settlement, None]:
8095
for settlement in self.settlements:
8196
if settlement.position is None:
@@ -142,6 +157,9 @@ def take_resources_from_player(
142157
raise ValueError("Player does not have required resources")
143158
return cards
144159

160+
def give_resource_to_player(self, card: ResourceCard) -> None:
161+
self.resources.append(card)
162+
145163
def resource_counts(self) -> dict[ResourceType, int]:
146164
counts = {resource: 0 for resource in ResourceType}
147165
for card in self.resources:
@@ -162,14 +180,11 @@ def can_build_city(self) -> bool:
162180
resource_counts = self.resource_counts()
163181
return (
164182
self.unplaced_city_count() > 0
165-
and len(self.current_settlements()) > 0
183+
and len(self.get_active_cities()) > 0
166184
and resource_counts[ResourceType.WHEAT] >= 2
167185
and resource_counts[ResourceType.ORE] >= 3
168186
)
169187

170-
def current_settlements(self) -> list[Settlement]:
171-
return [s for s in self.settlements if s.position is not None]
172-
173188
def can_build_road(self) -> bool:
174189
resource_counts = self.resource_counts()
175190
return (
@@ -178,11 +193,17 @@ def can_build_road(self) -> bool:
178193
and resource_counts[ResourceType.WOOD] >= 1
179194
)
180195

181-
def pop_least_valuable_resource(self) -> ResourceCard:
182-
# TODO: Implement better logic for choosing which cards to return for manual mode
196+
def pop_least_valuable_resource(self) -> Union[ResourceCard, None]:
183197
if len(self.resources) == 0:
184-
raise ValueError("Player has no resources")
185-
return self.resources.pop()
198+
return None
199+
200+
resource_rankings = reversed(self.rank_resource_values())
201+
resource_counts = self.resource_counts()
202+
203+
for resource in resource_rankings:
204+
if resource_counts[resource] > 0:
205+
[card] = self.take_resources_from_player([resource])
206+
return card
186207

187208
def split_cards(self, bank: Bank):
188209
if len(self.resources) > 7:
@@ -193,16 +214,9 @@ def split_cards(self, bank: Bank):
193214
def play_development_card(self, card: DevelopmentCard):
194215
pass
195216

196-
def trade(
197-
self, other: "Player", offer: list[ResourceCard], request: list[ResourceCard]
198-
):
199-
pass
200-
201-
def trade_bank(self, offer: list[ResourceCard], request: list[ResourceCard]):
202-
pass
203-
204-
def trade_port(self, offer: list[ResourceCard], request: list[ResourceCard]):
205-
pass
217+
def trade_bank(self, offer: list[ResourceType], request: ResourceType, bank: Bank):
218+
bank.return_cards(self.take_resources_from_player(offer))
219+
self.resources.append(bank.get_card(request))
206220

207221
def build_settlement(self, board: Board, vertexLoc: int, bank: Bank):
208222
bank.purchase_settlement(self)
@@ -219,10 +233,35 @@ def build_road(self, board: Board, edgeLoc: int, bank: Bank):
219233
def buy_development_card(self, bank: Bank):
220234
bank.purchase_dev_card(self)
221235

222-
def rob(self, board: Board, bank: Bank):
223-
random_hex = random.choice(board.get_hexes())
224-
board.move_robber(random_hex.id)
225-
# TODO: Implement logic for what happens when robber moves to a hex with a player's settlement
236+
def get_hex_to_rob(self, board: Board, bank: Bank) -> Hex:
237+
return random.choice(board.get_hexes())
238+
239+
def choose_player_to_rob(
240+
self, players: list["Player"], board: Board, bank: Bank
241+
) -> Union["Player", None]:
242+
return None if len(players) == 0 else players[0]
243+
244+
def rank_resource_values(self) -> list[ResourceType]:
245+
"""Returns resources valuable to the player from most valuable to least valuable"""
246+
return [
247+
ResourceType.ORE,
248+
ResourceType.BRICK,
249+
ResourceType.SHEEP,
250+
ResourceType.WHEAT,
251+
ResourceType.WOOD,
252+
]
253+
254+
def rob(self) -> Union[ResourceCard, None]:
255+
return self.pop_least_valuable_resource()
256+
257+
def move_robber(self, board: Board, bank: Bank) -> None:
258+
hex = self.get_hex_to_rob(board, bank)
259+
board.move_robber(hex.id)
260+
261+
settled_players = hex.get_settled_players()
262+
player_to_rob = self.choose_player_to_rob(settled_players, board, bank)
263+
if player_to_rob is not None:
264+
player_to_rob.rob()
226265

227266
def take_turn(self, board: Board, bank: Bank, players: list["Player"]):
228267
pass

0 commit comments

Comments
 (0)