Spaces:
Configuration error
Configuration error
| 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 | |