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