diff --git a/README.md b/README.md index f297838..a9be6a3 100644 --- a/README.md +++ b/README.md @@ -5,3 +5,13 @@ A base implementation of a ozai bot. Can easily be extended with new filters. If you are new here. the only thing you need to make a new bot is to create a new filter function, that returns true if any given move to it seems like a bad move. All the possible moves are generated, and passed trough the filter, and any found to not be deemed bad, will be tried at random. If no valid moves are left after filtering, a complete random move will be done. + +the main thing to do if you want to make your own bot is to make your own filter function in the ozai_bot.py, and add it to the last if else ladder for filter functions to select from. + +To know how to run the bot run this command + + python ozai_bot.py --help + + + +The ozai_lib.py is the library function. You probably do not need to interface with it other than if you want to use its logic to build your completely own bot withoul doing all the gameserver interface work yourself, or if you want to know what to use when making a filter. diff --git a/main.py b/main.py deleted file mode 100755 index 9352515..0000000 --- a/main.py +++ /dev/null @@ -1,307 +0,0 @@ -#!/usr/bin/env python3 - -from time import sleep -import requests -import numpy as np -import random -import argparse - -ozai_url = 'http://localhost:8000/api/' - - -# ozai classes (some are just storing json data because i am ) - -class Player: - def __init__(self, name, gamedata_player_json): - self.name = name - self.ready = gamedata_player_json['ready'] - self.points = gamedata_player_json['points'] - self.pattern_lines = gamedata_player_json['pattern_lines'] #'pattern_lines': [{'color': None, 'number': 0}, {'color': None, 'number': 0}, {'color': None, 'number': 0}, {'color': None, 'number': 0}, {'color': None, 'number': 0}], - self.wall = gamedata_player_json['wall'] # 'wall': [[False, False, False, False, False], [False, False, False, False, False], [False, False, False, False, False], [False, False, False, False, False], [False, False, False, False, False]] - self.floor = gamedata_player_json['floor'] - - def __str__(self): - return f"Player(name={self.name}, ready={self.ready}, points={self.points}, pattern_lines={self.pattern_lines}, wall={self.wall}, floor={self.floor})" - -class Bag: - def __init__(self, bag_json): - self.blue = bag_json['blue'] - self.yellow = bag_json['yellow'] - self.red = bag_json['red'] - self.black = bag_json['black'] - self.white = bag_json['white'] - - def __str__(self): - return f"Bag(blue={self.blue}, yellow={self.yellow}, red={self.red}, black={self.black}, white={self.white})" - -class Factory: - def __init__(self, factory_json): - self.blue = factory_json['blue'] - self.yellow = factory_json['yellow'] - self.red = factory_json['red'] - self.black = factory_json['black'] - self.white = factory_json['white'] - - def __str__(self): - return f"Factory(blue={self.blue}, yellow={self.yellow}, red={self.red}, black={self.black}, white={self.white})" - -class Market: - def __init__(self, market_json): - self.start = market_json['start'] - self.blue = market_json['blue'] - self.yellow = market_json['yellow'] - self.red = market_json['red'] - self.black = market_json['black'] - self.white = market_json['white'] - - def __str__(self): - return f"Market(start={self.start}, blue={self.blue}, yellow={self.yellow}, red={self.red}, black={self.black}, white={self.white})" - -class PatternLine: - def __init__(self, pattern_line_json): - self.color = pattern_line_json['color'] - self.number = pattern_line_json['number'] - - def __str__(self): - return f"PatternLine(color={self.color}, number={self.number})" - -class Wall: - def __init__(self, wall_json): - self.wall = wall_json - def __str__(self): - return f"Wall(wall={self.wall})" - -class Floor: - def __init__(self, floor_json): - self.start = floor_json['start'] - self.blue = floor_json['blue'] - self.yellow = floor_json['yellow'] - self.red = floor_json['red'] - self.black = floor_json['black'] - self.white = floor_json['white'] - - def __str__(self): - return f"Floor(start={self.start}, blue={self.blue}, yellow={self.yellow}, red={self.red}, black={self.black}, white={self.white})" - -class GameState: - def __init__(self, gamedata_json): - json = gamedata_json - self.n_players = json['n_players'] - self.current_player = json['current_player'] - self.starting_player = json['starting_player'] - self.player_names = json['player_names'] - self.game_end = json['game_end'] - self.rounds = json['rounds'] - self.days = json['days'] - self.bag = Bag(json['bag']) - self.lid = Bag(json['lid']) # Assuming lid has the same structure as bag - self.factories = [Factory(factory_json) for factory_json in json['factories']] - self.market = Market(json['market']) - self.players = [Player(name, player_json) for name, player_json in json['players'].items()] - def __str__(self): - return f"GameState(n_players={self.n_players}, current_player={self.current_player}, starting_player={self.starting_player}, player_names={self.player_names}, game_end={self.game_end}, rounds={self.rounds}, days={self.days}, bag={self.bag}, lid={self.lid}, factories={self.factories}, market={self.market}, players={self.players})" - -def init_game(names=["a", "b"]): - #initialize a game with the given names and return the game_id - player_names = {"player_names": names} - response = requests.post(ozai_url + 'game', json=player_names) - if response.status_code == 200: - game_id = response.json() - print("Game ID:", game_id) - return game_id - else: - return None - -def join_game(game_id, player_name): - #gets a initialized game and joins the game with the given player name - response = requests.get(ozai_url + 'game/' + game_id + '?player=' + player_name) - if response.status_code != 200: - return None - players = response.json().get('players') - return players - -def create_game(names=["a", "b"]): - # Create a game with the players given in names, by initializing it and joining the players - game_id = init_game(names) - for name in names: - join_game(game_id, name) - return game_id - -def submit_action(game_id, player_name, source=0, destination=0, color="start", policy="strict"): - # Submit an action to the game. The action is a dictionary with the keys being the action type and the values being the arguments. - # Example: {"player": 0, "market": true, "factory": 0, "color": "blue", "patternLine": 0} - if source != "market": - if source < 0 or source > 4: - return False - if destination != "floor": - if destination < 0 or destination > 4: - return False - action = { - 'player': str(player_name), - 'policy': policy, - 'color': str(color).lower(), - 'source': source, - 'destination': destination - } - response = requests.put(ozai_url + 'game/' + game_id, json=action) - if response.status_code == 200: - return True - else: - return False - -def get_gamestate(game_id) -> GameState: - response = requests.get(ozai_url + 'game/' + game_id) - if response.status_code == 200: - game_state = response.json() - game_state = GameState(game_state) - return game_state - else: - return None - -def get_score(game_id, player_name): - GameState = get_gamestate(game_id) - for player in GameState.players: - if player_name == player.name: - return player.points - -def game_over(game_id): - try: - GameState = get_gamestate(game_id) - return GameState.game_end - except: - return False - -def get_all_valid_moves(game_id, player_name): - # Get the game state - GameState = get_gamestate(game_id) - # Generate a list of all possible moves - sources = ["market"] + [i for i, factory in enumerate(GameState.factories) if any([factory.blue, factory.yellow, factory.red, factory.black, factory.white])] - colors = ["blue", "yellow", "red", "black", "white"] - destinations = ["floor"] + list(range(len(GameState.factories))) - valid_moves = [] - for source in sources: - for color in colors: - for destination in destinations: - # Check if the source has the color we want to move - if source == "market": - amount = getattr(GameState.market, color) - if amount > 0: - valid_moves.append((source, destination, color, amount)) - else: - amount = getattr(GameState.factories[source], color) - if amount > 0: - valid_moves.append((source, destination, color, amount)) - - # Filter out invalid destinations if the destinaiton is a pattern line, and the wall contains the color - wall_colors = [ - ['blue', 'yellow', 'red', 'black', 'white'], - ['white', 'blue', 'yellow', 'red', 'black'], - ['black', 'white', 'blue', 'yellow', 'red'], - ['red', 'black', 'white', 'blue', 'yellow'], - ['yellow', 'red', 'black', 'white', 'blue'], - ] - moves = [] - player = [player for player in GameState.players if player.name == player_name][0] - for move in valid_moves: - if move[1] == "floor": - moves.append(move) - else: - # print(player.pattern_lines[move[1]]) - #if element in wall is tru convert it to the correct color - player.wall = [[wall_colors[i][j] if player.wall[i][j] else False for j in range(5)] for i in range(5)] - #check that the wall does not contain the color on the same row as the pattern line, and that the pattern line does not contain another color - if not move[2] in player.wall[move[1]]: - if player.pattern_lines[move[1]]['color'] == move[2] or player.pattern_lines[move[1]]['color'] == None: - moves.append(move) - valid_moves = moves - return valid_moves - - -### -# strategies for filtering in the do_move solution. -### -def strategy_1(move, GameState,player): - #strategy 1 most fit always 530points 27-3 - if move[1] == 'floor': - return True - remaining_space = move[1] + 1 - player.pattern_lines[move[1]]['number'] - if move[3] >= remaining_space-1 and move[3] <= remaining_space +1: - return True - else: - return False - -def strategy_2(move,GameState,player): - #strategy 2 tierdoff fitting. 983 30-0 - if move[1] == 'floor': - return True - remaining_space = move[1] + 1 - player.pattern_lines[move[1]]['number'] - - if move[1] == 4: - if move[3] <= remaining_space and move[3] >= remaining_space -2: - return True - if move[1] == 3: - if move[3] <= remaining_space and move[3] >= remaining_space -1: - return True - if move[1] < 3: - if move[3] <= remaining_space and move[3] >= remaining_space: - return True - return False - -def strategy_random(move, GameState,player): - return True #do not filter any moves - -def do_move(game_id, player_name, filter_strategy=strategy_random): - moves = get_all_valid_moves(game_id, player_name) - try: - GameState = get_gamestate(game_id) - player = [player for player in GameState.players if player.name == player_name][0] - filtered_moves = [] - for move in moves: - if filter_strategy(move,GameState,player): - filtered_moves.append(move) - # Submit a random move, of the filtered ones. - move = random.choice(filtered_moves) - submit_action(game_id, player_name, move[0], move[1], move[2]) - return move - except: - #if filtered all moves, just submit a random move from the moves - result = random.choice(moves) - return result - -def play_game(gameid, players, strategy): - print(f"Playing game {gameid} with players {players} and strategy {strategy.__name__}") - while not game_over(gameid): - for player in players: - mov = do_move(gameid, player, filter_strategy=strategy) - if game_over(game_id): - #get the score of the players - score = [] - for player in players: - score.append(get_score(gameid, player)) - print(f"Game Over, scores: {score}") - return score - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description='Play a game with specified strategies.') - parser.add_argument('--game_id', type=int, default=0, help='The id of the game to play.') - parser.add_argument('--players', nargs='+', default=["a", "b"], help='The names of the players.') - parser.add_argument('--strategy', type=str, default="", help='The strategy to use. Can be "1", "2", or "" for random.') - parser.add_argument('--ozai_url', type=str, default='http://localhost:8000/api/', help='The url to the ozai server.') - args = parser.parse_args() - - - - game_id = args.game_id - players = args.players - strategy = args.strategy - #use global ozai url and update it - ozai_url = args.ozai_url - - if game_id == 0: - game_id = create_game(names=players) - if strategy == "1": - play_game(game_id, players, strategy_1) - elif strategy == "2": - play_game(game_id, players, strategy_2) - else: - play_game(game_id, players, strategy_random) \ No newline at end of file diff --git a/ozai_bot.py b/ozai_bot.py new file mode 100755 index 0000000..c907248 --- /dev/null +++ b/ozai_bot.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 + +from time import sleep +import requests +import numpy as np +import random +import argparse + +#if you wonder about anything about this library logic and what is available in the GameState object see here, or print it out. +import ozai_lib as oz + + +### +# strategies for filtering in the do_move solution. +# the move is a tuple with the first indicating the source, +# the second the color +# and the third the destination +# +# Gamestate is a large object with pretty mouch all the information about the game. +# (look at the library or print it out if you wonder what it contains) +### +def strategy_1(move, GameState,player): + #strategy 1 most fit always 530points 27-3 + if move[1] == 'floor': + return True + remaining_space = move[1] + 1 - player.pattern_lines[move[1]]['number'] + if move[3] >= remaining_space-1 and move[3] <= remaining_space +1: + return True + else: + return False + +def strategy_2(move,GameState,player): + #strategy 2 tierdoff fitting. 983 30-0 + if move[1] == 'floor': + return True + remaining_space = move[1] + 1 - player.pattern_lines[move[1]]['number'] + + if move[1] == 4: + if move[3] <= remaining_space and move[3] >= remaining_space -2: + return True + if move[1] == 3: + if move[3] <= remaining_space and move[3] >= remaining_space -1: + return True + if move[1] < 3: + if move[3] <= remaining_space and move[3] >= remaining_space: + return True + return False + +def strategy_random(move, GameState,player): + return True #do not filter any moves + + + +#taking arguments, so you can do python main.py --help to get the help message of arguments defined here. +if __name__ == "__main__": + parser = argparse.ArgumentParser(description='Play a game with specified strategies.') + parser.add_argument('--game_id', type=int, default=0, help='The id of the game to play.') + parser.add_argument('--players', nargs='+', default=["a", "b"], help='The names of the players.') + parser.add_argument('--strategy', type=str, default="", help='The strategy to use. Can be "1", "2", or "" for random.') + parser.add_argument('--ozai_url', type=str, default='http://localhost:8000/api/', help='The url to the ozai server.') + args = parser.parse_args() + + game_id = args.game_id + players = args.players + strategy = args.strategy + #use global ozai url and update it + oz.ozai_url = args.ozai_url + + #create a game with the players if the game id does not exist. + if game_id == 0: + game_id = oz.create_game(names=players) + + #apply the correct strategy from what was given at the terminal argument. + if strategy == "1": + oz.play_game(game_id, players, strategy_1) + elif strategy == "2": + oz.play_game(game_id, players, strategy_2) + else: + oz.play_game(game_id, players, strategy_random) diff --git a/ozai_lib.py b/ozai_lib.py new file mode 100755 index 0000000..8b954d2 --- /dev/null +++ b/ozai_lib.py @@ -0,0 +1,541 @@ +#!/usr/bin/env python3 + +import requests +import random +import argparse + +from ozai_bot import * + +#the url the gameserver is hosted at. +ozai_url = 'http://localhost:8000/api/' + + +### +# ozai classes (some are just storing json data in some of the subvalues) +#these are mostly only to represent the same classes the game server uses to clarify +# what they contain, and for some internal use here. +### + +class Player: + """ + A class to represent a Player in a azul game. + ... + + Attributes + ---------- + name : str + a string representing the player's name + ready : bool + a boolean indicating if the player is ready + points : int + an integer representing the player's points + pattern_lines : list + a list of dictionaries representing the player's pattern lines + wall : list + a 2D list representing the player's wall + floor : list + a list representing the player's floor + + Methods + ------- + __str__(): + Returns a string representation of the Player object. + """ + + def __init__(self, name, gamedata_player_json): + self.name = name + self.ready = gamedata_player_json['ready'] + self.points = gamedata_player_json['points'] + self.pattern_lines = gamedata_player_json['pattern_lines'] + self.wall = gamedata_player_json['wall'] + self.floor = gamedata_player_json['floor'] + + def __str__(self): + return f"Player(name={self.name}, ready={self.ready}, points={self.points}, pattern_lines={self.pattern_lines}, wall={self.wall}, floor={self.floor})" + + + +class Bag: + """ + A class to representing the bag with tiles in azul. + + Attributes + ---------- + blue : int + an integer representing the number of blue tiles in the bag + yellow : int + an integer representing the number of yellow tiles in the bag + red : int + an integer representing the number of red tiles in the bag + black : int + an integer representing the number of black tiles in the bag + white : int + an integer representing the number of white tiles in the bag + + Methods + ------- + __str__(): + Returns a string representation of the Bag object. + """ + + def __init__(self, bag_json): + self.blue = bag_json['blue'] + self.yellow = bag_json['yellow'] + self.red = bag_json['red'] + self.black = bag_json['black'] + self.white = bag_json['white'] + + def __str__(self): + return f"Bag(blue={self.blue}, yellow={self.yellow}, red={self.red}, black={self.black}, white={self.white})" + + +class Factory: + """ + A class to represent a Factory in azul. + Any given factory should not have more than 4 tiles at one point. + ... + + Attributes + ---------- + blue : int + an integer representing the number of blue tiles in the given factory + yellow : int + an integer representing the number of yellow tiles in the given factory + red : int + an integer representing the number of red tiles in the given factory + black : int + an integer representing the number of black tiles in the given factory + white : int + an integer representing the number of white tiles in the given factory + + """ + + def __init__(self, factory_json): + self.blue = factory_json['blue'] + self.yellow = factory_json['yellow'] + self.red = factory_json['red'] + self.black = factory_json['black'] + self.white = factory_json['white'] + def __str__(self): + return f"Factory(blue={self.blue}, yellow={self.yellow}, red={self.red}, black={self.black}, white={self.white})" + +class Market: + """ + A class to represent a azul Market. + + ... + + Attributes + ---------- + start : int + an integer stating the amount of start (special) tiles in the market (should be 1 or 0) + blue : int + an integer representing the number of blue tiles in the market + yellow : int + an integer representing the number of yellow tiles in the market + red : int + an integer representing the number of red tiles in the market + black : int + an integer representing the number of black tiles in the market + white : int + an integer representing the number of white tiles in the market + """ + + def __init__(self, market_json): + self.start = market_json['start'] + self.blue = market_json['blue'] + self.yellow = market_json['yellow'] + self.red = market_json['red'] + self.black = market_json['black'] + self.white = market_json['white'] + def __str__(self): + return f"Market(start={self.start}, blue={self.blue}, yellow={self.yellow}, red={self.red}, black={self.black}, white={self.white})" + +class PatternLine: + """ + A class to represent a pattern line. + + Attributes + ---------- + color : str + The color of the pattern line. + number : int + The number of the pattern line. + + Methods + ------- + __init__(self, pattern_line_json: dict) -> None: + Initializes PatternLine with the color and number from a JSON object. + """ + + def __init__(self, pattern_line_json: dict): + self.color = pattern_line_json['color'] + self.number = pattern_line_json['number'] + + def __str__(self): + return f"PatternLine(color={self.color}, number={self.number})" + +class Wall: + """ + A class to represent a azul Wall of any player. + + Attributes + ---------- + wall : dict + The JSON representation of the wall. + Can be abstracted into a 2d matrix of tiles. See api docs on ozai server for more details. + + Methods + ------- + __init__(self, wall_json: dict) -> None: + Initializes Wall with the JSON representation. + __str__(self) -> str: + Returns a string representation of the Wall instance. + """ + def __init__(self, wall_json): + self.wall = wall_json + def __str__(self): + return f"Wall(wall={self.wall})" + +class Floor: + """ + This class represents a floor in the game Azul. + + Attributes: + start (int): The number of start (special) tiles on the floor. + blue (int): The number of blue tiles on the floor. + yellow (int): The number of yellow tiles on the floor. + red (int): The number of red tiles on the floor. + black (int): The number of black tiles on the floor. + white (int): The number of white tiles on the floor. + """ + def __init__(self, floor_json): + self.start = floor_json['start'] + self.blue = floor_json['blue'] + self.yellow = floor_json['yellow'] + self.red = floor_json['red'] + self.black = floor_json['black'] + self.white = floor_json['white'] + + def __str__(self): + return f"Floor(start={self.start}, blue={self.blue}, yellow={self.yellow}, red={self.red}, black={self.black}, white={self.white})" + +class GameState: + """ + This class represents the state of a game in the Ozai library collected from the ozai server. + + Attributes: + n_players (int): The number of players in the game. + current_player: The name of the current player. + starting_player: The name of the starting player. + player_names (list): A list of strings of the names of the players. + game_end (bool): A flag indicating whether the game has ended. + rounds (int): The number of rounds that have been played. + days (int): The number of days that have passed in the game. + bag (Bag): An instance of the Bag class representing the bag of tiles. + lid (Bag): An instance of the Bag class representing the lid of the bag. + factories (list): A list of Factory instances representing the factories in the game. + market (Market): An instance of the Market class representing the market. + players (list): A list of Player instances representing the players in the game. + """ + def __init__(self, gamedata_json): + json = gamedata_json + self.n_players = json['n_players'] + self.current_player = json['current_player'] + self.starting_player = json['starting_player'] + self.player_names = json['player_names'] + self.game_end = json['game_end'] + self.rounds = json['rounds'] + self.days = json['days'] + self.bag = Bag(json['bag']) + self.lid = Bag(json['lid']) # Assuming lid has the same structure as bag + self.factories = [Factory(factory_json) for factory_json in json['factories']] + self.market = Market(json['market']) + self.players = [Player(name, player_json) for name, player_json in json['players'].items()] + def __str__(self): + return f"GameState(n_players={self.n_players}, current_player={self.current_player}, starting_player={self.starting_player}, player_names={self.player_names}, game_end={self.game_end}, rounds={self.rounds}, days={self.days}, bag={self.bag}, lid={self.lid}, factories={self.factories}, market={self.market}, players={self.players})" + +def init_game(names=["a", "b"]): + """ + Initialize a game with the given player names and return the game_id. + + This function sends a POST request to the Ozai game server to create a new game. + The server responds with a game_id which is returned by this function. + If the server responds with a status code other than 200, this function returns None. + + Parameters: + names (list of str): The names of the players in the game. Defaults to ["a", "b"]. + + Returns: + str: The game_id of the newly created game, or None if the game could not be created. + """ + player_names = {"player_names": names} + response = requests.post(ozai_url + 'game', json=player_names) + if response.status_code == 200: + game_id = response.json() + print("Game ID:", game_id) + return game_id + else: + return None + +def join_game(game_id, player_name): + """ + Join an initialized game with the given player name. + + This function sends a GET request to the Ozai game server to join an existing game. + The server responds with the current state of the game. + If the server responds with a status code other than 200, this function returns None. + + Parameters: + game_id (str): The ID of the game to join. + player_name (str): The name of the player joining the game. + + Returns: + list: The list of players in the game, or None if the game could not be joined. + """ + response = requests.get(ozai_url + 'game/' + game_id + '?player=' + player_name) + if response.status_code != 200: + return None + players = response.json().get('players') + return players + +def create_game(names=["a", "b"]): + """ + Create a game with the players given in names, by initializing it and joining the players. + + This function first initializes a game with the given player names by calling the init_game function. + Then, it joins each player to the game by calling the join_game function for each player name. + Finally, it returns the game_id of the created game. + + Parameters: + names (list of str): The names of the players. Defaults to ["a", "b"]. + + Returns: + str: The game_id of the newly created game. + """ + game_id = init_game(names) + for name in names: + join_game(game_id, name) + return game_id + +def submit_action(game_id, player_name, source=0, destination=0, color="start", policy="strict"): + """ + Submit an action to the game. + + This function sends a PUT request to the Ozai game server to submit an action for a player. + The action is a dictionary with the keys being the action type and the values being the arguments. + The server responds with the status of the action submission. + If the server responds with a status code of 200, this function returns True, otherwise it returns False. + + Parameters: + game_id (str): The ID of the game. + player_name (str): The name of the player submitting the action. + source (int or str): The source of the action. Defaults to 0. (0 = market, and increasing number for factories in order they come in) + destination (int or str): The destination of the action. Defaults to 0 (What pattern line or floor to place onto). + color (str): The color of the tile to take. Defaults to "start" (actually illegal to take the start tile though). + policy (str): The policy of the action. Defaults to "strict" (loose will drop tiles to the floor if the move was invalid.). + + Returns: + bool: True if the action was successfully submitted, False otherwise. + """ + if source != "market": + if source < 0 or source > 4: + return False + if destination != "floor": + if destination < 0 or destination > 4: + return False + action = { + 'player': str(player_name), + 'policy': policy, + 'color': str(color).lower(), + 'source': source, + 'destination': destination + } + response = requests.put(ozai_url + 'game/' + game_id, json=action) + if response.status_code == 200: + return True + else: + return False + +def get_gamestate(game_id) -> GameState: + """ + Get the current state of the game. + + This function sends a GET request to the Ozai game server to get the current state of the game. + The server responds with the game state in JSON format, which is then converted to a GameState object. + If the server responds with a status code other than 200, this function returns None. + + Parameters: + game_id (str): The ID of the game. + + Returns: + GameState: The current state of the game, or None if the game state could not be retrieved. + """ + response = requests.get(ozai_url + 'game/' + game_id) + if response.status_code == 200: + game_state = response.json() + game_state = GameState(game_state) + return game_state + else: + return None + +def get_score(game_id, player_name): + """ + Get the score of a player in the game. + + This function first gets the current state of the game by calling the get_gamestate function. + Then, it iterates over the players in the game state and returns the points of the player with the given name. + + Parameters: + game_id (str): The ID of the game. + player_name (str): The name of the player. + + Returns: + int: The points of the player, or None if the player could not be found. + """ + GameState = get_gamestate(game_id) + for player in GameState.players: + if player_name == player.name: + return player.points + +def game_over(game_id): + """ + Check if the game is over. + + This function first gets the current state of the game by calling the get_gamestate function. + Then, it returns the game_end attribute of the game state. + If the game state could not be retrieved, this function returns False. + + Parameters: + game_id (str): The ID of the game. + + Returns: + bool: True if the game is over, False otherwise. + """ + try: + GameState = get_gamestate(game_id) + return GameState.game_end + except: + return False + +def get_all_valid_moves(game_id, player_name): + """ + Get all valid moves for a player in the game. + + This function first gets the current state of the game by calling the get_gamestate function. + Then, it generates a list of all possible moves by iterating over all sources, colors, and destinations. + It checks if the source has the color we want to move, and if so, it adds the move to the list of valid moves. + Finally, it filters out invalid destinations if the destination is a pattern line and the wall contains the color. + + Parameters: + game_id (str): The ID of the game. + player_name (str): The name of the player. + + Returns: + list: The list of all valid moves for the player. + """ + # Get the game state + GameState = get_gamestate(game_id) + # Generate a list of all possible moves + sources = ["market"] + [i for i, factory in enumerate(GameState.factories) if any([factory.blue, factory.yellow, factory.red, factory.black, factory.white])] + colors = ["blue", "yellow", "red", "black", "white"] + destinations = ["floor"] + list(range(len(GameState.factories))) + valid_moves = [] + for source in sources: + for color in colors: + for destination in destinations: + # Check if the source has the color we want to move + if source == "market": + amount = getattr(GameState.market, color) + if amount > 0: + valid_moves.append((source, destination, color, amount)) + else: + amount = getattr(GameState.factories[source], color) + if amount > 0: + valid_moves.append((source, destination, color, amount)) + + # Filter out invalid destinations if the destinaiton is a pattern line, and the wall contains the color + wall_colors = [ + ['blue', 'yellow', 'red', 'black', 'white'], + ['white', 'blue', 'yellow', 'red', 'black'], + ['black', 'white', 'blue', 'yellow', 'red'], + ['red', 'black', 'white', 'blue', 'yellow'], + ['yellow', 'red', 'black', 'white', 'blue'], + ] + moves = [] + player = [player for player in GameState.players if player.name == player_name][0] + for move in valid_moves: + if move[1] == "floor": + moves.append(move) + else: + # print(player.pattern_lines[move[1]]) + #if element in wall is tru convert it to the correct color + player.wall = [[wall_colors[i][j] if player.wall[i][j] else False for j in range(5)] for i in range(5)] + #check that the wall does not contain the color on the same row as the pattern line, and that the pattern line does not contain another color + if not move[2] in player.wall[move[1]]: + if player.pattern_lines[move[1]]['color'] == move[2] or player.pattern_lines[move[1]]['color'] == None: + moves.append(move) + valid_moves = moves + return valid_moves + +def do_move(game_id, player_name, filter_strategy=strategy_random): + """ + Perform a move for a player in the game. + + This function first gets all valid moves for the player by calling the get_all_valid_moves function. + Then, it filters the moves based on the given filter strategy. + If there are any filtered moves, it randomly selects one and submits it by calling the submit_action function. + If there are no filtered moves, it randomly selects a move from all valid moves and returns it. + + Parameters: + game_id (str): The ID of the game. + player_name (str): The name of the player. + filter_strategy (function): The strategy to filter the moves. Defaults to strategy_random. + + Returns: + tuple: The move that was performed. + """ + moves = get_all_valid_moves(game_id, player_name) + try: + GameState = get_gamestate(game_id) + player = [player for player in GameState.players if player.name == player_name][0] + filtered_moves = [] + for move in moves: + if filter_strategy(move,GameState,player): + filtered_moves.append(move) + # Submit a random move, of the filtered ones. + move = random.choice(filtered_moves) + submit_action(game_id, player_name, move[0], move[1], move[2]) + return move + except: + #if filtered all moves, just submit a random move from the moves + result = random.choice(moves) + return result + +def play_game(gameid, players, strategy): + """ + Play a game with the given players and strategy. + + This function first prints the game ID, player names, and strategy name. + Then, it repeatedly performs moves for each player by calling the do_move function, until the game is over. + When the game is over, it gets the score of each player by calling the get_score function, prints the scores, and returns them. + + Parameters: + gameid (str): The ID of the game. + players (list of str): The names of the players. + strategy (function): The strategy to filter the moves. + + Returns: + list: The scores of the players. + """ + print(f"Playing game {gameid} with players {players} and strategy {strategy.__name__}") + while not game_over(gameid): + for player in players: + mov = do_move(gameid, player, filter_strategy=strategy) + if game_over(gameid): + #get the score of the players + score = [] + for player in players: + score.append(get_score(gameid, player)) + print(f"Game Over, scores: {score}") + return score