PyCatan-AI / pycatan /core /game.py
EZTIME2025
fix game state
eedd6d8
from .default_board import DefaultBoard
from .player import Player
from .statuses import Statuses
from .card import ResCard, DevCard
from .building import Building
from .harbor import Harbor
from pycatan.config.board_definition import board_definition
import random
import math
class Game:
# initializes the game
def __init__(self, num_of_players=3, on_win=None, starting_board=False):
# creates a board
self.board = DefaultBoard(game=self);
# creates players
self.players = []
for i in range(num_of_players):
self.players.append(Player(num=i, game=self))
# Set onWin method
self.on_win = on_win
# creates a new Developement deck
self.dev_deck = []
for i in range(14):
# Add 2 Road, Monopoly and Year of Plenty cards
if i < 2:
self.dev_deck.append(DevCard.Road)
self.dev_deck.append(DevCard.Monopoly)
self.dev_deck.append(DevCard.YearOfPlenty)
# Add 5 Victory Point cards
if i < 5:
self.dev_deck.append(DevCard.VictoryPoint)
# Add 14 knight cards
self.dev_deck.append(DevCard.Knight)
# Shuffle the developement deck
random.shuffle(self.dev_deck)
# the longest road owner and largest army owner
self.longest_road_owner = None
self.largest_army = None
# whether the game has finished or not
self.has_ended = False
# creates a new settlement belong to the player at the coodinates
def add_settlement(self, player, point, is_starting=False):
# builds the settlement
status = self.players[player].build_settlement(point=point, is_starting=is_starting)
# If successful, check if the player has now won
if status == Statuses.ALL_GOOD:
if self.players[player].get_VP() >= 10:
# End the game
self.has_ended = True
self.winner = player
return status
# builds a road going from point start to point end
def add_road(self, player, start, end, is_starting=False):
# builds the road
stat = self.players[player].build_road(start=start, end=end, is_starting=is_starting)
# checks for a new longest road segment
self.set_longest_road()
# returns the status
return stat
# builds a new developement cards for the player
def build_dev(self, player):
# makes sure there is still at least one development card left
if len(self.dev_deck) < 1:
return Statuses.ERR_DECK
# makes sure the player has the right cards
needed_cards = [
ResCard.Wheat,
ResCard.Ore,
ResCard.Sheep
]
if not self.players[player].has_cards(needed_cards):
return Statuses.ERR_CARDS
# removes the cards
self.players[player].remove_cards(needed_cards)
self.players[player].add_dev_card(self.dev_deck[0])
# removes that dev card from the deck
del self.dev_deck[0] # Commented out: not removing from deck in debug mode
return Statuses.ALL_GOOD
# gives players the proper cards for a given roll
def add_yield_for_roll(self, roll):
return self.board.add_yield(roll)
# trades cards (given in an array) between two players
def trade(self, player_one, player_two, cards_one, cards_two):
# check if they players have the cards they are trading
# Needs to do this before deleting because one might have the cards while the other does not
if not self.players[player_one].has_cards(cards_one):
return Statuses.ERR_CARDS
elif not self.players[player_two].has_cards(cards_two):
return Statuses.ERR_CARDS
else:
# removes the cards
self.players[player_one].remove_cards(cards_one)
self.players[player_two].remove_cards(cards_two)
# add the new cards
self.players[player_one].add_cards(cards_two)
self.players[player_two].add_cards(cards_one)
return Statuses.ALL_GOOD
# moves the robber
# Note that player is the player moving the robber
# and victim is the player whose card is being taken
def move_robber(self, tile, player, victim):
"""
Move robber to a new tile and optionally steal from a player.
Returns:
tuple: (Statuses, stolen_card) where stolen_card is the ResCard that was stolen (or None)
"""
# checks the player wants to take a card from somebody
stolen_card = None
if victim != None:
# checks the victim has a settlement on the tile
has_settlement = False
# Use tile.points directly - these are the 6 points surrounding the hex tile
# (NOT get_connected_points which returns points connected to a point!)
points = tile.points
for p in points:
if p != None and p.building != None:
# Check the victim owns the settlement/city
if p.building.owner == victim:
has_settlement = True
if not has_settlement:
return (Statuses.ERR_INPUT, None)
# moves the robber (pass tile position, not tile object)
self.board.move_robber(tile.position)
# takes a random card from the victim
if victim != None and len(self.players[victim].cards) > 0:
# removes a random card from the victim
index = round(random.random() * (len(self.players[victim].cards) - 1))
stolen_card = self.players[victim].cards[index]
self.players[victim].remove_cards([stolen_card])
# adds it to the player
self.players[player].add_cards([stolen_card])
return (Statuses.ALL_GOOD, stolen_card)
# trades cards from a player to the bank
# either by 4 for 1 or using a harbor
def trade_to_bank(self, player, cards, request):
# makes sure the player has the cards
if not self.players[player].has_cards(cards):
return Statuses.ERR_CARDS
# checks all the cards are the same type
card_type = cards[0]
for c in cards[1:]:
if c != card_type:
return Statuses.ERR_CARDS
# if there are not four cards
if len(cards) != 4:
# checks if the player has a settlement on the right type of harbor
has_harbor = False
harbor_types = self.players[player].get_connected_harbor_types()
print(harbor_types)
for h_type in harbor_types:
if Harbor.get_card_from_harbor_type(h_type) == card_type and len(cards) == 2:
has_harbor = True
break
elif Harbor.get_card_from_harbor_type(h_type) == None and len(cards) == 3:
has_harbor = True
break
if not has_harbor:
return Statuses.ERR_HARBOR
# removes cards
self.players[player].remove_cards(cards)
# adds the new card
self.players[player].add_cards([request])
return Statuses.ALL_GOOD
# gives the longest road to the correct player
def set_longest_road(self):
# The length of the current longest road segment
longest = 0
owner = self.longest_road_owner
for p in self.players:
# longest road needs to be longer than anbody else's
# and at least 5 road segments long
if p.longest_road_length > longest and p.longest_road_length >= 5:
longest = p.longest_road_length
owner = self.players.index(p)
if self.longest_road_owner != owner:
self.longest_road_owner = owner
# checks if the player has won now that they has longest road
if self.players[owner].get_VP() >= 10:
self.has_ended = True
self.winner = owner
# changes a settlement on the board for a city
def add_city(self, point, player):
# Upgrade settlement to city using the point object
status = self.board.upgrade_settlement(player, point)
if status == Statuses.ALL_GOOD:
# checks if the player won
if self.players[player].get_VP() >= 10:
self.has_ended = True
self.winner = player
return status
# uses a developement card
# the required args will vary between different dev cards
def use_dev_card(self, player, card, args):
# checks the player has the development card
if not self.players[player].has_dev_cards([card]):
return Statuses.ERR_CARDS
# applies the action
if card == DevCard.Road:
# checks the correct arguments are given
road_names = [
"road_one",
"road_two"
]
for r in road_names:
if not r in args:
return Statuses.ERR_INPUT
else:
# Check the roads have a start and an end
if not "start" in args[r] or not "end" in args[r]:
return Statuses.ERR_INPUT
# checks the road location is valid
# whether the other road is completely isolated but is connected to this road
other_road_is_isolated = False
for r in road_names:
location_status = self.players[player].road_location_is_valid(args[r]['start'], args[r]['end'])
# if the road location is not OK
# since the player can build two roads, some
# locations that would be invalid are valid depending on the other road location
if not location_status == Statuses.ALL_GOOD:
# checks if it is isolated, but would be connected to the other road
if location_status == Statuses.ERR_ISOLATED:
# if the other road is also isolated, just return an error
if other_road_is_isolated:
return location_status
# checks if the two roads are connected
# (since the other one is connected, this road is connected through it)
road_points = [
"start",
"end"
]
roads_are_connected = False
for p_one in road_points:
for p_two in road_points:
if args["road_one"][p_one] == args['road_two'][p_two]:
other_road_is_isolated = True
# doesn't return an isolated error
roads_are_connected = True
if not roads_are_connected:
return location_status
else:
return location_status
# builds the roads
for r in road_names:
self.board.add_road(Building(point_one=args[r]["start"], point_two=args[r]["end"], owner=player, type=Building.BUILDING_ROAD))
# Don't return here - let it continue to remove the card at the end
elif card == DevCard.Knight:
# checks there are the right arguments
if not ("robber_pos" in args and "victim" in args):
return Statuses.ERR_INPUT
# checks the victim input is valid
if args["victim"] != None:
if args["victim"] < 0 or args["victim"] >= len(self.players) or args["victim"] == player:
return Statuses.ERR_INPUT
# Get the tile object from coordinates
r, i = args["robber_pos"]
tile = self.board.tiles[r][i]
# moves the robber and get stolen card info
result, stolen_card = self.move_robber(tile=tile, player=player, victim=args["victim"])
# Store stolen card info in args for GameManager to use
if stolen_card:
args["stolen_card"] = stolen_card
if result != Statuses.ALL_GOOD:
return result
# adds one to the player's knight count
(self.players[player]).knight_cards += 1
# checks for the largest army
if self.largest_army == None:
# if nobody has the largest army, the player needs at least 3 cards
if self.players[player].knight_cards >= 3:
self.largest_army = player
else:
# the player needs to have more than anybody else
current_longest = self.players[self.largest_army].knight_cards
if self.players[player].knight_cards > current_longest:
self.largest_army = player
elif card == DevCard.Monopoly:
# gets the type of card
card_type = args['card_type']
# for each player, checks if they have the card
for p in self.players:
if p.has_cards([card_type]):
# gets how many this player has
number_of_cards = p.cards.count(card_type)
cards_to_give = [card_type] * number_of_cards
# removes the cards
p.remove_cards(cards_to_give)
# adds them to the user's cards
self.players[player].add_cards(cards_to_give)
elif card == DevCard.VictoryPoint:
# players do not play developement cards, so it returns an error
return Statuses.ERR_INPUT
elif card == DevCard.YearOfPlenty:
# checks the player gave two development cards
if not 'card_one' in args and not 'card_two' in args:
return Statuses.ERR_INPUT
# gives the player 2 resource cards of their choice
self.players[player].add_cards([
args['card_one'],
args['card_two']
])
else:
# error here
return Statuses.ERR_INPUT
# removes the card
self.players[player].remove_dev_card(card)
return Statuses.ALL_GOOD
# simulates 2 dice rolling
def get_roll(self):
return round(random.random() * 6) + round(random.random() * 6)
def get_full_state(self):
"""
Get the complete current state of the game.
This method extracts all relevant game state information
and returns it in a GameState object for use by the
GameManager and visualization systems.
Returns:
GameState: Complete current game state
"""
from pycatan.management.actions import GameState, PlayerState, BoardState, GamePhase, TurnPhase
# Create player states
players_state = []
for i, player in enumerate(self.players):
player_state = PlayerState(
player_id=i,
name=f"Player {i}", # Default name - can be enhanced later
cards=[card.name.lower() for card in player.cards], # Convert enum to string
dev_cards=[card.name.lower() for card in player.dev_cards],
settlements=self._get_player_settlements(player),
cities=self._get_player_cities(player),
roads=self._get_player_roads(player),
victory_points=player.get_VP(),
longest_road_length=player.longest_road_length,
has_longest_road=(self.longest_road_owner == i),
has_largest_army=(self.largest_army == i),
knights_played=player.knight_cards
)
players_state.append(player_state)
# Create board state
board_state = BoardState(
tiles=self._get_tiles_info(),
robber_position=tuple(self._get_robber_position()),
harbors=self._get_ports_info(),
buildings=self._get_all_buildings(),
roads=self._get_all_roads(),
points=self._get_points_info() # Add points for AI optimization
)
# Create and return game state
return GameState(
game_id="current_game", # Default ID - can be enhanced
turn_number=0, # Basic - can be enhanced
current_player=0, # Basic - managed by GameManager
game_phase=GamePhase.NORMAL_PLAY, # Default to normal play
turn_phase=TurnPhase.PLAYER_ACTIONS, # Default to player actions
players_state=players_state,
board_state=board_state
)
def _get_player_settlements(self, player):
"""Get list of settlement coordinates for a player."""
settlements = []
for point in self.board.points:
for p in point:
if p and p.building and p.building.type == 0 and p.building.owner == player.num: # BUILDING_SETTLEMENT = 0
settlements.append(p.position)
return settlements
def _get_player_cities(self, player):
"""Get list of city coordinates for a player."""
cities = []
for point in self.board.points:
for p in point:
if p and p.building and p.building.type == 2 and p.building.owner == player.num: # BUILDING_CITY = 2
cities.append(p.position)
return cities
def _get_player_roads(self, player):
"""Get list of road connections for a player."""
roads = []
for road in self.board.roads:
if road and road.owner == player.num and road.type == 1: # BUILDING_ROAD = 1
# Roads connect two points
start_pos = road.point_one.position if road.point_one else [0, 0]
end_pos = road.point_two.position if road.point_two else [0, 0]
roads.append(start_pos + end_pos) # [start_row, start_col, end_row, end_col]
return roads
def _get_robber_position(self):
"""Get current robber position as [row, col] list."""
if hasattr(self.board, 'robber') and self.board.robber:
robber = self.board.robber
# Handle case where robber might be a Tile object (old bug)
if hasattr(robber, 'position'):
return robber.position
# Normal case - robber is already a list [row, col]
return robber
return [3, 3] # Default center position if not found
def _get_tiles_info(self):
"""Get information about all tiles using BoardDefinition."""
tiles = []
for i, tile_row in enumerate(self.board.tiles):
for j, tile in enumerate(tile_row):
if tile:
# Get hex ID from BoardDefinition
hex_id = board_definition.game_coords_to_hex_id(i, j)
# Get axial coordinates for web display
axial_coords = board_definition.hex_id_to_axial_coords(hex_id) if hex_id else (i, j)
tile_info = {
'id': hex_id or (i * 10 + j), # Fallback ID if not found
'position': [i, j], # Game coordinates
'axial_coords': axial_coords, # Web display coordinates
'type': tile.type.name.lower() if hasattr(tile.type, 'name') else 'desert',
'token': getattr(tile, 'token_num', None),
'has_robber': (self.board.robber == [i, j]) if hasattr(self.board, 'robber') else False
}
tiles.append(tile_info)
return tiles
def _get_ports_info(self):
"""Get information about all ports/harbors."""
ports = []
for harbor in self.board.harbors:
if harbor:
# Get point coordinates for harbor location
point_one_coords = harbor.point_one.position if hasattr(harbor.point_one, 'position') else [0, 0]
point_two_coords = harbor.point_two.position if hasattr(harbor.point_two, 'position') else [0, 0]
# Convert points to point IDs using BoardDefinition
from pycatan.config.board_definition import board_definition
point_one_id = board_definition.game_coords_to_point_id(point_one_coords[0], point_one_coords[1])
point_two_id = board_definition.game_coords_to_point_id(point_two_coords[0], point_two_coords[1])
# Determine ratio based on harbor type
ratio = 2 if harbor.type.name.lower() != 'any' else 3
port_info = {
'point_one': point_one_id,
'point_two': point_two_id,
'resource': harbor.type.name.lower() if hasattr(harbor.type, 'name') else 'any',
'ratio': ratio
}
ports.append(port_info)
return ports
def _get_all_buildings(self):
"""Get all buildings on the board using point IDs."""
buildings = {}
for point_row in self.board.points:
for point in point_row:
if point and point.building:
# Convert coordinates to point ID using BoardDefinition
point_id = board_definition.game_coords_to_point_id(point.position[0], point.position[1])
if point_id:
buildings[point_id] = {
'type': 'settlement' if point.building.type == Building.BUILDING_SETTLEMENT else 'city',
'owner': point.building.owner,
'game_coords': point.position # Keep for debugging
}
return buildings
def _get_all_roads(self):
"""Get all roads on the board using point IDs."""
roads = []
for road in self.board.roads:
if road and road.type == Building.BUILDING_ROAD:
# Convert coordinates to point IDs using BoardDefinition
start_coords = road.point_one.position if road.point_one else [0, 0]
end_coords = road.point_two.position if road.point_two else [0, 0]
start_point_id = board_definition.game_coords_to_point_id(start_coords[0], start_coords[1])
end_point_id = board_definition.game_coords_to_point_id(end_coords[0], end_coords[1])
if start_point_id and end_point_id:
roads.append({
'start_point_id': start_point_id,
'end_point_id': end_point_id,
'owner': road.owner,
'start_coords': start_coords, # Keep for debugging
'end_coords': end_coords # Keep for debugging
})
return roads
def _get_points_info(self):
"""
Get information about all points/nodes on the board.
Returns info needed for AI optimization: neighbors, adjacent hexes, ports.
"""
points = []
for point_row in self.board.points:
for point in point_row:
if point:
# Get point ID
point_id = board_definition.game_coords_to_point_id(point.position[0], point.position[1])
if point_id:
# Get neighbor node IDs
neighbors = []
if hasattr(point, 'neighbors'):
for neighbor in point.neighbors:
if neighbor:
neighbor_id = board_definition.game_coords_to_point_id(
neighbor.position[0], neighbor.position[1]
)
if neighbor_id:
neighbors.append(neighbor_id)
# Get adjacent hex IDs
hex_ids = []
if hasattr(point, 'tiles'):
for tile in point.tiles:
if tile:
hex_id = board_definition.game_coords_to_hex_id(
tile.position[0], tile.position[1]
)
if hex_id:
hex_ids.append(hex_id)
# Check if point has a port
port_info = None
for harbor in self.board.harbors:
if harbor:
point_one_coords = harbor.point_one.position if hasattr(harbor.point_one, 'position') else None
point_two_coords = harbor.point_two.position if hasattr(harbor.point_two, 'position') else None
if (point_one_coords == point.position or point_two_coords == point.position):
# This point has a port
ratio = 2 if harbor.type.name.lower() != 'any' else 3
port_info = {
'type': harbor.type.name.lower(),
'ratio': ratio
}
break
point_info = {
'id': point_id,
'position': point.position,
'neighbors': neighbors,
'hexes': hex_ids,
'port': port_info
}
points.append(point_info)
return points