ozai-bot/ozai_lib.py

542 lines
20 KiB
Python
Executable File

#!/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