542 lines
20 KiB
Python
542 lines
20 KiB
Python
|
#!/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
|