diff --git a/backend/.gitignore b/backend/.gitignore index f94f9c0..8a48b28 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,2 +1,3 @@ logfile.log .venv +scores \ No newline at end of file diff --git a/backend/config.py b/backend/config.py index b19a559..3ecb694 100644 --- a/backend/config.py +++ b/backend/config.py @@ -21,3 +21,6 @@ def path_constructor(loader, node): with open("config.yaml", "r") as file: config = yaml.load(file, Loader=yaml.FullLoader) config["in_tests"] = False + +if config['log_networth_delay'] is None: + config['log_networth_delay'] = 1 \ No newline at end of file diff --git a/backend/config.yaml b/backend/config.yaml index 0a6f81d..ca8d03a 100644 --- a/backend/config.yaml +++ b/backend/config.yaml @@ -4,18 +4,18 @@ server: redis: port: 6379 - -database: - url: postgresql://postgres:postgres@localhost:5432/mydatabase - -test_database: - url: postgresql://postgres:postgres@localhost:5432/test_database + host: localhost admin: secret: mojkljuc testing: ${TESTING} debug: ${DEBUG} +drop_tables: ${DROP_TABLES} +fill_datasets: ${FILL_DATASETS} +fill_tables: ${FILL_TABLES} +log_level: ${LOG_LEVEL} +log_networth_delay: ${NETWORTH_TICKS} dataset: datasets_path: ./data @@ -47,6 +47,7 @@ player: starting_money: 50_000_000 max_orders: 20 max_energy_per_player: 0.4 + log_top_players: 10 bots: team_name: bots @@ -59,9 +60,9 @@ bots: max_price: 100 expiration_ticks: 3 price_change_coeff: 0.25 - extra_orders: 5 - extra_orders_price_diff: 0.02 - extra_orders_volume_diff: 0.12 + extra_orders: 4 + extra_orders_price_diff: 0.01 + extra_orders_volume_diff: 0.1 final_volume_multiplier: 1 final_price_multiplier: 1 log_when_no_orders: ${LOG_BOTS} diff --git a/backend/db/__init__.py b/backend/db/__init__.py index 268a19f..96e7370 100644 --- a/backend/db/__init__.py +++ b/backend/db/__init__.py @@ -1,3 +1 @@ -from .table import Table -from .db import database from .rate_limit import limiter diff --git a/backend/db/db.py b/backend/db/db.py index 61999cc..4045bb6 100644 --- a/backend/db/db.py +++ b/backend/db/db.py @@ -1,8 +1,9 @@ -from databases import Database +from redis_om import get_redis_connection from config import config -if config['testing']: - database = Database(config['test_database']['url']) -else: - database = Database(config['database']['url']) +redis_port = config["redis"]["port"] + + +def get_my_redis_connection(): + return get_redis_connection(port=redis_port) \ No newline at end of file diff --git a/backend/db/fill_datasets.py b/backend/db/fill_datasets.py index baa66a5..ff22bda 100644 --- a/backend/db/fill_datasets.py +++ b/backend/db/fill_datasets.py @@ -4,99 +4,80 @@ from datetime import datetime from config import config from logger import logger +from redlock.lock import RedLock + +from model.power_plant_model import PowerPlantsModel, ResourcesModel +from model.power_plant_type import PowerPlantType +from model.resource import Resource datasets_path = config["dataset"]["datasets_path"] +price_multipliers = config["dataset"]["price_multiplier"] +energy_output_multipliers = config["dataset"]["energy_output_multiplier"] +energy_demand_multiplier = config["dataset"]["energy_demand_multiplier"] -async def fill_datasets(): +def fill_datasets(): logger.info("Filling datasets") - for dataset in os.listdir(datasets_path): - if not dataset.endswith(".csv"): + pipe = DatasetData.db().pipeline() + for dataset_name in os.listdir(datasets_path): + if not dataset_name.endswith(".csv"): continue - try: - await Datasets.get(dataset_name=dataset) - logger.debug(f"Dataset {dataset} already created") + if Datasets.find(Datasets.dataset_name == dataset_name).count() > 0: + logger.info(f"Dataset {dataset_name} already created") continue - except Exception: - pass - - df = pd.read_csv(f"{datasets_path}/{dataset}") - # TODO: asserts, async transaction - ne zelimo da se dataset kreira ako faila kreiranje redaka - dataset_id = await Datasets.create(dataset_name=dataset, dataset_description="Opis") + df = pd.read_csv(f"{datasets_path}/{dataset_name}") - price_multipliers = config["dataset"]["price_multiplier"] - energy_output_multipliers = config["dataset"]["energy_output_multiplier"] - energy_demand_multiplier = config["dataset"]["energy_demand_multiplier"] + dataset = Datasets(dataset_name=dataset_name, dataset_description="Opis") + dataset.save() - # date,COAL,URANIUM,BIOMASS,GAS,OIL,GEOTHERMAL,WIND,SOLAR,HYDRO,ENERGY_DEMAND,MAX_ENERGY_PRICE tick = 0 - dataset_data = [] - for index, row in df.iterrows(): - dataset_data.append(DatasetData( - dataset_data_id=0, - dataset_id=dataset_id, - tick=tick, - date=datetime.strptime( - row["date"], "%Y-%m-%d %H:%M:%S"), - coal=( - energy_output_multipliers["coal"] * - row["COAL"] // 1_000_000), - uranium=( - energy_output_multipliers["uranium"] * - row["URANIUM"] // 1_000_000), - biomass=( - energy_output_multipliers["biomass"] * - row["BIOMASS"] // 1_000_000), - gas=( - energy_output_multipliers["gas"] * - row["GAS"] // 1_000_000), - oil=( - energy_output_multipliers["oil"] * - row["OIL"] // 1_000_000), - geothermal=( - energy_output_multipliers["geothermal"] * - row["GEOTHERMAL"] // 1_000_000), - wind=( - energy_output_multipliers["wind"] * - row["WIND"] // 1_000_000), - solar=( - energy_output_multipliers["solar"] * - row["SOLAR"] // 1_000_000), - hydro=( - energy_output_multipliers["hydro"] * - row["HYDRO"] // 1_000_000), - energy_demand=( - energy_demand_multiplier * - row["ENERGY_DEMAND"] // 1_000_000), - max_energy_price=( - price_multipliers["energy"] * - row["MAX_ENERGY_PRICE"] // 1_000_000), - coal_price=( - price_multipliers["coal"] * - row["COAL_PRICE"] // 1_000_000), - uranium_price=( - price_multipliers["uranium"] * - row["URANIUM_PRICE"] // 1_000_000), - biomass_price=( - price_multipliers["biomass"] * - row["BIOMASS_PRICE"] // 1_000_000), - gas_price=( - price_multipliers["gas"] * - row["GAS_PRICE"] // 1_000_000), - oil_price=( - price_multipliers["oil"] * - row["OIL_PRICE"] // 1_000_000), - )) + for _, row in df.iterrows(): + from_row(dataset, tick, row).save(pipe) tick += 1 + logger.info(f"Added dataset {dataset_name}") + pipe.execute() - for x in dataset_data: - assert x.coal_price > -config["bots"]["min_price"] - assert x.uranium_price > -config["bots"]["min_price"] - assert x.biomass_price > -config["bots"]["min_price"] - assert x.gas_price > -config["bots"]["min_price"] - assert x.oil_price > -config["bots"]["min_price"] - await DatasetData.create_many(dataset_data) - logger.info(f"Added dataset {dataset}") \ No newline at end of file +def from_row(dataset: Datasets, tick: int, row: pd.Series) -> DatasetData: + power_plants_output = PowerPlantsModel( + coal=(energy_output_multipliers["coal"] * row["COAL"] // 1_000_000), + uranium=(energy_output_multipliers["uranium"] * row["URANIUM"] // 1_000_000), + biomass=(energy_output_multipliers["biomass"] * row["BIOMASS"] // 1_000_000), + gas=(energy_output_multipliers["gas"] * row["GAS"] // 1_000_000), + oil=(energy_output_multipliers["oil"] * row["OIL"] // 1_000_000), + geothermal=( + energy_output_multipliers["geothermal"] * row["GEOTHERMAL"] // 1_000_000 + ), + wind=(energy_output_multipliers["wind"] * row["WIND"] // 1_000_000), + solar=(energy_output_multipliers["solar"] * row["SOLAR"] // 1_000_000), + hydro=(energy_output_multipliers["hydro"] * row["HYDRO"] // 1_000_000), + ) + resource_prices = ResourcesModel( + coal=(price_multipliers["coal"] * row["COAL_PRICE"] // 1_000_000), + uranium=( + price_multipliers["uranium"] * row["URANIUM_PRICE"] // 1_000_000 + ), + biomass=( + price_multipliers["biomass"] * row["BIOMASS_PRICE"] // 1_000_000 + ), + gas=(price_multipliers["gas"] * row["GAS_PRICE"] // 1_000_000), + oil=(price_multipliers["oil"] * row["OIL_PRICE"] // 1_000_000), + ) + for type in PowerPlantType: + assert power_plants_output[type] >= 0 + for resource in Resource: + assert resource_prices[resource] > 0 + return DatasetData( + dataset_id=dataset.pk, + tick=tick, + date=datetime.strptime(row["date"], "%Y-%m-%d %H:%M:%S"), + + energy_demand=(energy_demand_multiplier * row["ENERGY_DEMAND"] // 1_000_000), + max_energy_price=( + price_multipliers["energy"] * row["MAX_ENERGY_PRICE"] // 1_000_000 + ), + power_plants_output = power_plants_output, + resource_prices = resource_prices + ) diff --git a/backend/db/fill_teams.py b/backend/db/fill_teams.py deleted file mode 100644 index 17512db..0000000 --- a/backend/db/fill_teams.py +++ /dev/null @@ -1,64 +0,0 @@ -from model import Team, Player, Game, Datasets -from datetime import datetime, timedelta -from config import config -from logger import logger - - -async def fill_bots(): - try: - await Team.get( - team_name=config["bots"]["team_name"], - team_secret=config["bots"]["team_secret"], - ) - logger.info("Bots team already created") - except Exception: - logger.info("Creating bots team") - await Team.create( - team_name=config["bots"]["team_name"], - team_secret=config["bots"]["team_secret"], - ) - - -async def fill_dummy_tables(): - g_team_id = await Team.create(team_name="Goranov_tim", team_secret="gogi") - k_team_id = await Team.create(team_name="Krunov_tim", team_secret="kruno") - z_team_id = await Team.create(team_name="Zvonetov_tim", team_secret="zvone") - m_team_id = await Team.create(team_name="Maja_tim", team_secret="maja") - - teams = [ - ("Goranov_tim", g_team_id), - ("Krunov_tim", k_team_id), - ("Zvonetov_tim", z_team_id), - ("Maja_tim", m_team_id), - ] - - datasets = await Datasets.list() - - not_nat_game_id = await Game.create( - game_name="Stalna igra", - is_contest=False, - dataset_id=datasets[0].dataset_id, - start_time=datetime.now() + timedelta(milliseconds=3000), - total_ticks=2300, - tick_time=3000, - ) - nat_game_id = await Game.create( - game_name="Natjecanje", - is_contest=True, - dataset_id=datasets[1].dataset_id, - start_time=datetime.now() + timedelta(milliseconds=5000), - total_ticks=1800, - tick_time=1000, - ) - - games = [nat_game_id, not_nat_game_id] - for game_id in games: - for team_name, team_id in teams: - await Player.create( - game_id=game_id, - team_id=team_id, - player_name=f"{team_name}_1", - money=config["player"]["starting_money"], - ) - - logger.info("Filled database with dummy data") diff --git a/backend/db/migration.py b/backend/db/migration.py deleted file mode 100644 index f5a13d7..0000000 --- a/backend/db/migration.py +++ /dev/null @@ -1,176 +0,0 @@ -from db.db import database -from logger import logger -from .fill_teams import fill_bots, fill_dummy_tables -from .fill_datasets import fill_datasets - - -async def delete_tables(): - await database.execute('TRUNCATE orders, players, games, teams, market, datasets, dataset_data CASCADE') - - -async def drop_tables(): - for table_name in ["orders", "players", "games", "teams", "market", "datasets", "dataset_data"]: - await database.execute(f'DROP TABLE IF EXISTS {table_name} CASCADE') - - -async def run_migrations(): - logger.info("Running migration script") - await create_tables() - await fill_datasets() - await fill_bots() - logger.info("Migrated database") - - -async def create_tables(): - await database.execute(''' - CREATE TABLE IF NOT EXISTS teams ( - team_id SERIAL PRIMARY KEY, - team_name TEXT, - team_secret TEXT UNIQUE - )''') - - await database.execute('CREATE INDEX CONCURRENTLY team_secret_idx ON teams (team_secret);') - - await database.execute(''' - CREATE TABLE IF NOT EXISTS datasets ( - dataset_id SERIAL PRIMARY KEY, - dataset_name TEXT, - dataset_description TEXT - )''') - - await database.execute(''' - CREATE TABLE IF NOT EXISTS games ( - game_id SERIAL PRIMARY KEY, - game_name TEXT, - is_contest BOOLEAN NOT NULL, - dataset_id INT, - start_time TIMESTAMP NOT NULL, - total_ticks INT NOT NULL, - tick_time INT NOT NULL, - is_finished BOOLEAN NOT NULL DEFAULT false, - current_tick INT NOT NULL DEFAULT 0, - FOREIGN KEY (dataset_id) REFERENCES datasets(dataset_id) - )''') - - await database.execute(''' - CREATE TABLE IF NOT EXISTS players ( - player_id SERIAL PRIMARY KEY, - player_name TEXT, - game_id INT NOT NULL, - team_id INT NOT NULL, - is_active BOOLEAN NOT NULL DEFAULT true, - is_bot BOOLEAN NOT NULL DEFAULT false, - - energy_price BIGINT NOT NULL DEFAULT 1e9, - - money BIGINT NOT NULL DEFAULT 0, - energy BIGINT NOT NULL DEFAULT 0, - coal BIGINT NOT NULL DEFAULT 0, - uranium BIGINT NOT NULL DEFAULT 0, - biomass BIGINT NOT NULL DEFAULT 0, - gas BIGINT NOT NULL DEFAULT 0, - oil BIGINT NOT NULL DEFAULT 0, - - coal_plants_owned INT NOT NULL DEFAULT 0, - uranium_plants_owned INT NOT NULL DEFAULT 0, - biomass_plants_owned INT NOT NULL DEFAULT 0, - gas_plants_owned INT NOT NULL DEFAULT 0, - oil_plants_owned INT NOT NULL DEFAULT 0, - geothermal_plants_owned INT NOT NULL DEFAULT 0, - wind_plants_owned INT NOT NULL DEFAULT 0, - solar_plants_owned INT NOT NULL DEFAULT 0, - hydro_plants_owned INT NOT NULL DEFAULT 0, - - coal_plants_powered INT NOT NULL DEFAULT 0, - uranium_plants_powered INT NOT NULL DEFAULT 0, - biomass_plants_powered INT NOT NULL DEFAULT 0, - gas_plants_powered INT NOT NULL DEFAULT 0, - oil_plants_powered INT NOT NULL DEFAULT 0, - geothermal_plants_powered INT NOT NULL DEFAULT 0, - wind_plants_powered INT NOT NULL DEFAULT 0, - solar_plants_powered INT NOT NULL DEFAULT 0, - hydro_plants_powered INT NOT NULL DEFAULT 0, - - FOREIGN KEY (game_id) REFERENCES games(game_id), - FOREIGN KEY (team_id) REFERENCES teams(team_id) - )''') - - await database.execute(''' - CREATE TABLE IF NOT EXISTS orders ( - order_id SERIAL PRIMARY KEY, - game_id INT NOT NULL, - player_id INT NOT NULL, - order_type TEXT NOT NULL, - order_side TEXT NOT NULL, - order_status TEXT NOT NULL, - price BIGINT NOT NULL, - size BIGINT NOT NULL, - tick INT NOT NULL, - timestamp TIMESTAMP NOT NULL, - expiration_tick INT NOT NULL, - resource TEXT NOT NULL, - - filled_size BIGINT NOT NULL DEFAULT 0, - filled_money BIGINT NOT NULL DEFAULT 0, - filled_price DOUBLE PRECISION NOT NULL DEFAULT 0, - - FOREIGN KEY (player_id) REFERENCES players(player_id), - FOREIGN KEY (game_id) REFERENCES games(game_id) - )''') - - await database.execute(''' - CREATE TABLE IF NOT EXISTS trades ( - trade_id SERIAL PRIMARY KEY, - - buy_order_id INT, - sell_order_id INT, - tick INT, - - filled_money BIGINT, - filled_size BIGINT, - filled_price BIGINT, - - FOREIGN KEY (buy_order_id) REFERENCES orders(order_id), - FOREIGN KEY (sell_order_id) REFERENCES orders(order_id) - )''') - - await database.execute(''' - CREATE TABLE IF NOT EXISTS market ( - game_id INT, - tick INT, - resource TEXT, - low BIGINT, - high BIGINT, - open BIGINT, - close BIGINT, - market BIGINT, - volume BIGINT, - PRIMARY KEY (game_id, tick, resource) - )''') - - await database.execute('CREATE INDEX CONCURRENTLY tick_idx ON market (tick);') - - await database.execute(''' - CREATE TABLE IF NOT EXISTS dataset_data ( - dataset_data_id SERIAL PRIMARY KEY, - dataset_id INT NOT NULL, - date TIMESTAMP NOT NULL, - tick INT NOT NULL, - coal BIGINT NOT NULL, - uranium BIGINT NOT NULL, - biomass BIGINT NOT NULL, - gas BIGINT NOT NULL, - oil BIGINT NOT NULL, - coal_price BIGINT NOT NULL, - uranium_price BIGINT NOT NULL, - biomass_price BIGINT NOT NULL, - gas_price BIGINT NOT NULL, - oil_price BIGINT NOT NULL, - geothermal BIGINT NOT NULL, - wind BIGINT NOT NULL, - solar BIGINT NOT NULL, - hydro BIGINT NOT NULL, - energy_demand BIGINT NOT NULL, - max_energy_price BIGINT NOT NULL, - FOREIGN KEY (dataset_id) REFERENCES datasets(dataset_id) - )''') diff --git a/backend/db/rate_limit.py b/backend/db/rate_limit.py index d5773bf..c3f6580 100644 --- a/backend/db/rate_limit.py +++ b/backend/db/rate_limit.py @@ -12,5 +12,5 @@ def team_secret(request: Request): return param -limiter = Limiter(key_func=team_secret, default_limits=["100000/second"], # TODO: vrati na 3 +limiter = Limiter(key_func=team_secret, default_limits=["10000/second"], # TODO: vrati na 3 storage_uri=f"redis://localhost:{config['redis']['port']}/0") diff --git a/backend/db/run_redis.py b/backend/db/run_redis.py new file mode 100644 index 0000000..88927ca --- /dev/null +++ b/backend/db/run_redis.py @@ -0,0 +1,86 @@ +import asyncio +from datetime import datetime, timedelta +import os +from typing import List +import psutil +from redis_om import Field, HashModel, Migrator +from db.fill_datasets import fill_datasets +from game.tick.ticker import Ticker +from model import Game, Team, Order, Player, DatasetData, Datasets +from config import config +from logger import logger + + +def drop_tables(): + pipe = DatasetData.db().pipeline() + logger.info("Deleting tables") + for cls in [DatasetData, Datasets, Order, Team, Game, Player]: + try: + cls.delete_many(cls.find().all(), pipe) + except Exception: + logger.warning(f"Class {cls.__name__} probably changed in model") + for pk in cls.all_pks(): + cls.delete(pk, pipe) + pipe.execute() + + +def create_teams_and_games(): + logger.info("Creating teams") + teams = [ + Team(team_name="Goranov_tim", team_secret="gogi"), + Team(team_name="Krunov_tim", team_secret="kruno"), + Team(team_name="Zvonetov_tim", team_secret="zvone"), + Team(team_name="Maja_tim", team_secret="maja") + ] + for team in teams: + team.save() + + bots_team_name = config["bots"]["team_name"] + bots_team_secret = config["bots"]["team_secret"] + if Team.find( + Team.team_name == bots_team_name, + Team.team_secret == bots_team_secret, + ).count() > 0: + logger.info("Bots team already created") + else: + logger.info("Creating bots team") + Team( + team_name=bots_team_name, + team_secret=bots_team_secret, + ).save() + + logger.info("Getting all pks for datasets") + datasets: List[Datasets] = Datasets.find().all() + assert len(datasets) > 0 + + logger.info("Creating games") + games = [ + Game( + game_name="Stalna igra", + is_contest=int(False), + dataset_id=datasets[0].dataset_id, + start_time=datetime.now() + timedelta(milliseconds=3000), + total_ticks=2300, + tick_time=3000 + ), + Game( + game_name="Natjecanje", + is_contest=int(False), #TODO + dataset_id=datasets[1].dataset_id, + start_time=datetime.now() + timedelta(milliseconds=5000), + total_ticks=1800, + tick_time=1000, + ) + ] + for game in games: + game.save() + logger.info("Creating players") + for game in games: + for team in teams: + Player( + game_id=game.pk, + team_id=team.pk, + player_name=f"{team.team_name}_1", + money=config["player"]["starting_money"], + ).save() + logger.info("Filled database with dummy data") diff --git a/backend/db/table.py b/backend/db/table.py deleted file mode 100644 index 1501dad..0000000 --- a/backend/db/table.py +++ /dev/null @@ -1,136 +0,0 @@ -from typing import Any -from dataclasses import fields, asdict -from .db import database -from enum import Enum - - -class Table: - table_name = None - - @classmethod - async def create(cls, col_nums: int = 1, *args, **kwargs) -> int: - data = cls(*[0 for _ in range(col_nums)], *args, **kwargs) - cols = [field.name for field in fields(data)] - query = f"""INSERT INTO {cls.table_name} - ({', '.join(cols[col_nums:])}) - VALUES ({', '.join(f':{col}' for col in cols[col_nums:])}) - RETURNING {cols[0]}""" - values = {col: data.__getattribute__(col) for col in cols[col_nums:]} - values = _transform_kwargs(values) - return await database.fetch_val(query=query, values=values) - - @classmethod - async def update(cls, **kwargs) -> int: - """ - Input: Updated values, row id must be provided - Returns: Updated rows including including rows whose values did not change - """ - cols = [field.name for field in fields(cls)] - assert set(kwargs.keys()).issubset( - cols), f"Some columns don't exist in table {cls.table_name}" - assert cols[0] in kwargs, "Row id wasn't provided" - set_query = ', '.join( - f'{col}=:{col}' for col in kwargs if col != cols[0]) - query = f"UPDATE {cls.table_name} SET {set_query} WHERE {cols[0]}=:{cols[0]} RETURNING *" - kwargs = _transform_kwargs(kwargs) - return await database.fetch_val(query, kwargs) - - @classmethod - async def create_many(cls, l: list, col_nums: int = 1) -> int: - if len(l) == 0: - return 0 - - cols = [field.name for field in fields(cls)] - values = [_transform_kwargs(asdict(obj)) for obj in l] - for val in values: - for col in cols[:col_nums]: - val.pop(col, None) - - query = f"""INSERT INTO {cls.table_name} - ({', '.join(cols[col_nums:])}) - VALUES ({', '.join(f':{col}' for col in cols[col_nums:])}) - RETURNING {cols[0]}""" - - return await database.execute_many(query, values) - - @classmethod - async def update_many(cls, l: list) -> int: - if len(l) == 0: - return 0 - - values = [_transform_kwargs(asdict(obj)) for obj in l] - - cols = [field.name for field in fields(cls)] - set_query = ', '.join( - f'{col}=:{col}' for col in values[0] if col != cols[0]) - query = f"UPDATE {cls.table_name} SET {set_query} WHERE {cols[0]}=:{cols[0]} RETURNING *" - - return await database.execute_many(query, values) - - @classmethod - async def delete(cls, **kwargs) -> int: - """ - Input: Where clause - Returns number of deleted rows - """ - cols = [field.name for field in fields(cls)] - assert set(kwargs.keys()).issubset( - cols), f"Some columns don't exist in table {cls.table_name}" - where = ' AND '.join(f'{col}=:{col}' for col in kwargs) - if where: - where = f" WHERE {where}" - query = f"DELETE FROM {cls.table_name}{where} RETURNING *" - kwargs = _transform_kwargs(kwargs) - return await database.fetch_val(query, kwargs) - - @classmethod - async def get(cls, **kwargs): - """ - Input: Where clause - Returns selected row - Throws exception if row doesn't exist - """ - kwargs = _transform_kwargs(kwargs) - query, values = cls._select(**kwargs) - result = await database.fetch_one(query, values) - assert result, f"Requested row in table {cls.__name__} doesn't exist" - return cls(**result) - - @classmethod - async def list(cls, **kwargs): - """ - Input: Where clause - Returns selected rows as a list - """ - query, values = cls._select(**kwargs) - result = await database.fetch_all(query, values) - return [cls(**obj) for obj in result] - - @classmethod - async def count(cls, **kwargs) -> int: - query, values = cls._select(selected_cols="COUNT(*)", **kwargs) - result = await database.execute(query, values) - return result - - @classmethod - def _select(cls, selected_cols="*", **kwargs): - cols = [field.name for field in fields(cls)] - assert set(kwargs.keys()).issubset( - cols), f"Some columns don't exist in table {cls.table_name}" - where = ' AND '.join(f'{col}=:{col}' for col in kwargs) - if where: - where = f" WHERE {where}" - query = f"SELECT {selected_cols} FROM {cls.table_name}{where}" - - kwargs = _transform_kwargs(kwargs) - return query, kwargs - - -def _transform_kwargs(kwargs): - return {k: _transform_enum(v) for k, v in kwargs.items()} - - -def _transform_enum(value) -> Any: - if isinstance(value, Enum): - return value.value - return value diff --git a/backend/db/test_database.py b/backend/db/test_database.py deleted file mode 100644 index 2ee8c4f..0000000 --- a/backend/db/test_database.py +++ /dev/null @@ -1,53 +0,0 @@ -import pytest -from db import database, migration -from model import Team -from config import config -import pytest_asyncio - - -# @pytest_asyncio.fixture(autouse=True) -# async def delete_db(): -# await database.connect() -# yield database -# await migration.delete_tables() -# await database.disconnect() - - -# @pytest_asyncio.fixture(scope="session", autouse=True) -# async def connect_db(): -# await database.connect() -# await migration.drop_tables() -# await migration.run_migrations() -# await database.disconnect() -# return - - -# @pytest.mark.asyncio -# async def test_create_and_get_team(): - -# assert len(await Team.list()) == 1 -# # team name is bots -# assert (await Team.get(team_id=1)).team_name == "bots" -# team_data = { -# "team_name": "Sample Team", -# "team_secret": "secretpassword" -# } -# created_team_id = await Team.create(**team_data) -# retrieved_team = await Team.get(team_id=created_team_id) - -# assert retrieved_team.team_name == team_data["team_name"] -# assert retrieved_team.team_secret == team_data["team_secret"] - - -# @pytest.mark.asyncio -# async def test_create_and_get_team_2(): -# assert len(await Team.list()) == 0 -# team_data = { -# "team_name": "Sample Team", -# "team_secret": "secretpassword" -# } -# created_team_id = await Team.create(**team_data) -# retrieved_team = await Team.get(team_id=created_team_id) - -# assert retrieved_team.team_name == team_data["team_name"] -# assert retrieved_team.team_secret == team_data["team_secret"] diff --git a/backend/docs/docs.md b/backend/docs/docs.md deleted file mode 100644 index 783177a..0000000 --- a/backend/docs/docs.md +++ /dev/null @@ -1,208 +0,0 @@ - -# Algotrade hackathon - -Algotrade hackathon game rules and technical details. - -In case of any questions, ping organizers in discord or in person! - -## Table of contents -1. [Task description](#task) - 1. [Resource market](#resource_market) - 1. [Power plants](#resource_market) - 1. [Electricity market](#electricity_market) -1. [Ticks](#ticks) -1. [Rounds and scoring](#games) -1. [Appendix](#extra) - 1. [Order matching example](#order_matching) - 1. [Bot orders mechanics](#bot_orders) - -## Task description - -There is a simulated stock exchange for resources and energy. -At the beggining of a game you are given a sum of money. -You can buy and sell your resources on *resource market*. -Resources can be stored in your storage. - -You can also process resources you own and produce electricity using *power plants*. This electricity can be sold on *electricity market*, but cannot be stored. - -Player with the biggest networth at the end of the game wins! - - - - - -## Resource market - -There are 5 types of resources in this game. These are **coal, uranium, biomass, gas and oil**. They all have different power outputs and different prices for respective power plants. -Users can buy and sell resources using *orders*. - -Game is split into [ticks](#ticks). During ticks you place your orders which will be put in the order matching engine at the end of the tick. - -Order is defined by: -- order side: BUY / SELL -- resource: resource you want to buy or sell -- order size: number of resources you are trading -- order price: price per unit of resource you are selling (not the total price of the order) -- expiration tick: tick in the game when this order expires (see api docs for details) - -### Matching engine - -will place the orders by the time of arrival - as if they were evaluated real time. They are evalueated at the end of the tick for performance reasons. - -- If the order is BUY order: engine will look for the SELL order with the lowest price that is already in the engine and if the price is lower than the price of the BUY order, they will match. -- If the order is SELL order: engine will look for the BUY order with the highest price that is already in the engine and if the price is lower than the price of the BUY order, they will match. - -If during this process there are two orders with the same price, engine will look for the first one. If no matching order is found, placed order will be saved by engine for later matching, until it expires. - -When orders match, price of the first order is set as trade price. Then the engine checks if the selling player has enough resources and if buying player has enough money. If not, the respective order is cancelled. Otherwise, both order sizes are reduced until one of them is filled. One that is not filled is matched again. See [example](#matching_example). - - - -### Bot orders - -Every 5 ticks, our bots create new resource orders (both buy and sell). Price is determined by dataset and [pricing mechanic](#bot_orders). Volume is set to keep players total resources constant. For example if players colectively have a lot of resources, our bots will have bigger buy volume, but smaller sell volume. - -## Power plants - -There are two types of power plants: renewable and non renewable. - -Every power plant you buy of one type makes the next one more expensive. You can also sell them at 70% of their original price. - -Exact formula for price of power plant is: - -$$ -Base price \times (1 + 0.1 x + 0.03 x^2) -$$ - -Where x is the number of power plants of this type you already own. - -### Non renewable - -Non renewables require resources to run, but produce a lot of stable electricity. You can set how much resources of each type you want to burn. But you cannot burn more resources than power plants of that type that you have. 1 resource burned = 1 power plant is on. - -### Renewable - -Renewable always produce electricity following the dataset. However, renewables produce less electricity and less reliably. You can use modeling to predict how much they will produce, since every tick is one hour in dataset, which means that one day is 24 ticks. -For example, solar plants will produce more electricity during daytime. - -You can see example of electricity production from one solar plant below. - - - -## Energy market - -Energy market is simpler than resource market. You will set the price for your electricity. Our market will have certain volume of electricity each tick (electricity demand) and the maximum price at which it will buy electricity. It will look for cheapest electricity from players and buy as much as it can. If it is not filled, it will look for more. If two players have the same price, we will sell it proportionaly to the electricity they produced. - -## Ticks - -In one tick players can create new orders and do other requests. - -At the end of tick following things happen in this order: - -1) Resource orders are added to match engine in time order, and -then matched on the price-time priority - -1) Power plants consume set ammount of resources and then -produces electricity - -1) Energy agent buys players energy on price priority - - If you have energy that is not sold to energy agent, it is destroyed! -So make sure you produce the right amount of energy - - - - -## Rounds and scoring - -There will be multiple games during hackathon. - -- One game will be open all night long for testing your bots, this game will have annotation `is_contest=False`. - -- There will be **three competition** rounds lasting for 30 minutes. These -rounds will be scored and they have annotation `is_contest=True`. - -- Around one hour before each competition round, we will start a **test round** that will simulate contest. They will also last 30 minutes, have the same limitations, but will not be scored. We encourage you to use your best bots here to promote good competition, however don't have to since these rounds aren't scored. These rounds will also have annotation `is_contest=True`. - -You will be able to differentiate between competition and test rounds by names, or ask organizers. - - -Normal round `is_contest=False` lasts all night long and may be reset a few times. You may have 10 bots in one game and can reset their money balance. Ticks in these games are longer so you can see more easily what is happening. - -When `is_contest=True` (including both test and competition rounds), ticks last one second, and your team is limited to one bot. You can not reset the balance of this bot! So make sure everything goes as planned and that you don't waste your resources and money. - -All games will use different datasets. - -### Team secrets and creating players - -Each team will get a unique `team_secret`. Make sure to send it as a query parameter in all requests you send! Do not share this secret with other teams. - -Each game has unique `game_id`. In games you will be able to create multiple players for testing purposes (so that each team member can create their own bots for example). This is of course restricted in contest rounds. - -Note: if you created player in one game, it is not created in all games! - -See [api docs](/docs) for more details. - -### Scoring - -You are scored by your players net worth. This is calculated as sum of sell prices of every power plant you own plus money you have plus value of resources you own in current market prices. - -## Appendix - -### Order matching example - -The table below is showing already placed orders for coal resource. - -| Order id| Side |Price | Size | -|-| -----|-----|-----| -|1| BUY | $250 | 400 | -|2| SELL | $260 | 300 | -|3| SELL | $290 | 300 | - -Now three new orders arrive in this order: -[ -(4, BUY, $270, 400), -(5, BUY, $280, 100), -(6, SELL, $220, 100) -] -During the tick, they are saved to queue. At the end of the tick, orders are matched: - -First order in queue (order 4) is matched with order 2. Trade price is set to $260 and size to 300. Order 2 is filled, and player who placed order 4 will pay 300 x $260 = $18,000. However order 4 is still not filled. Now it matches again, but all orders are too high, so it is saved by the engine. Table now looks like: - -| Order id| Side |Price | Size | -|-| -----|-----|-----| -|1| BUY | $250 | 400 | -|4| BUY | $270 | 100 | -|3| SELL | $290 | 300 | - -Order 5 is now matched, but all SELL orders are too expensive. It is also saved by engine. - - -| Order id| Side |Price | Size | -|-| -----|-----|-----| -|1| BUY | $250 | 400 | -|4| BUY | $270 | 100 | -|5| BUY | $280 | 100 | -|3| SELL | $290 | 300 | - -Order 6 is matched with order 5 with price $280 and size 100. Both orders 5 and 6 are filled. Player who placed order 5 pays $280 x 100 = $28,000, and player 6 pays 100 oil resources. In the end the table looks like this: - -| Order id| Side |Price | Size | -|-| -----|-----|-----| -|1| BUY | $250 | 400 | -|4| BUY | $270 | 100 | -|3| SELL | $290 | 300 | - -### Bot orders mechanics - -**Constants in this explanation may be changed** - -This mechanism is done for each resource seperately. - -Bot total volume is set between 100 and 400. If players have 10000 resources, then both sell and buy volume will be 200. If players have more total resources than 10000, buy volume will be reduced linearly, and sell will be increased linearly. Same is done otherwise. - -Price is taken directly from dataset for the tick (about 1000-3000 per resource), but some bot coefficient is added. This coefficient is between -100 and 100. It is different for buy and for sell, so it is possible that buy and sell prices from bots are much apart. -Buy coefficient is bigger if last bot buy orders (those from previous 5 ticks) were sold well - if they were more filled. If previous buy orders weren't traded at all, it means that bot price is too high and that it should lower it. -It is done the same for sell orders but in different direction. -These two coefficients are averaged and taken as final coefficient to price. - -Once the price and volume is determined, bot *disperses* the orders. It creates many smaller orders totaling in volume to the calculated volume from before, but with small variations in pricing from the original (about 1%). \ No newline at end of file diff --git a/backend/docs/match.drawio.svg b/backend/docs/match.drawio.svg deleted file mode 100644 index bd89242..0000000 --- a/backend/docs/match.drawio.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - -
New orders
Order queue
Old orders
Matching engine
Order created
Order created
Order created
Yes
No
Orders match
No
Both buyer and seller have required money / resources
End of the tick
Add next order from queue
Yes
No
Are there orders in the queue
End matching
Energy production and energy matching
Start new tick
New order
Yes
Reduce money / resources from players
Update order
Cancel order
\ No newline at end of file diff --git a/backend/docs/solar.svg b/backend/docs/solar.svg deleted file mode 100644 index 3b4a241..0000000 --- a/backend/docs/solar.svg +++ /dev/null @@ -1,1110 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/backend/docs/tick.drawio.svg b/backend/docs/tick.drawio.svg deleted file mode 100644 index 39a4864..0000000 --- a/backend/docs/tick.drawio.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - -
For each renewable resource
Matching resource orders
Producing electricity
Yes
No
enough resources for production?
Auto shutdown of power plants
Matching electricity orders
For each non renewable resource
Producing electricity
\ No newline at end of file diff --git a/backend/docs/trzista.drawio.svg b/backend/docs/trzista.drawio.svg deleted file mode 100644 index 952ea69..0000000 --- a/backend/docs/trzista.drawio.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - -
Energy market
Energy market
Renewable power plants
Renewable power pl...
Power plants
Power plants
Resource market
Player
Player
Resources(oil, gas, biofuel...)Money
Text is not SVG - cannot display
\ No newline at end of file diff --git a/backend/fixtures/fixtures.py b/backend/fixtures/fixtures.py index d9639ca..5d35092 100644 --- a/backend/fixtures/fixtures.py +++ b/backend/fixtures/fixtures.py @@ -8,22 +8,23 @@ from game.tick.ticker import GameData from model import Game, Order, OrderSide, Player, Resource from model.dataset_data import DatasetData +from model.power_plant_model import PowerPlantsModel, ResourcesModel @pytest.fixture def team_id(): - return 1 + return "1" @pytest.fixture def game_id(): - return 1 + return "1" @pytest.fixture def game(): return Game( - game_id=1, + pk="1", game_name=f"game_{game_id}", is_contest=False, bots="", @@ -84,26 +85,30 @@ def _get_tick_data( game=game, bots=[], dataset_row=DatasetData( - dataset_data_id=1, - dataset_id=1, + dataset_data_id="1", + dataset_id="1", date=datetime.now(), tick=1, - coal=coal, - uranium=2, - biomass=3, - gas=4, - oil=5, - coal_price=coal, - uranium_price=2, - biomass_price=3, - gas_price=4, - oil_price=5, - geothermal=6, - wind=7, - solar=8, - hydro=9, energy_demand=energy_demand, max_energy_price=max_energy_price, + resource_prices = ResourcesModel( + coal_price=coal, + uranium_price=2, + biomass_price=3, + gas_price=4, + oil_price=5, + ), + power_plants_output= PowerPlantsModel( + coal=coal, + uranium=2, + biomass=3, + gas=4, + oil=5, + geothermal=6, + wind=7, + solar=8, + hydro=9, + ), ), markets=markets, players=players, @@ -136,7 +141,7 @@ def get_player(game_id, team_id): def _get_player(**kwargs) -> Player: nonlocal player_id player = Player( - player_id=player_id, + pk=str(player_id), player_name=f"player_{player_id}", team_id=team_id, game_id=game_id, @@ -155,7 +160,7 @@ def get_player_in_game(team_id): def _get_player_in_game(**kwargs) -> Player: nonlocal player_id player = Player( - player_id=player_id, + pk=str(player_id), player_name=f"player_{player_id}", team_id=team_id, **kwargs, @@ -171,19 +176,19 @@ def get_order(): order_id = 0 def _get_order( - player_id: int, price: int, size: int, order_side: OrderSide, tick: int + player_id: str, price: int, size: int, order_side: OrderSide, tick: int ) -> Order: nonlocal order_id order = Order( - order_id=order_id, - game_id=1, + pk=str(order_id), + game_id="1", timestamp=datetime.now(), player_id=player_id, - resource=Resource.coal, + resource=Resource.COAL.value, price=price, tick=tick, size=size, - order_side=order_side, + order_side=order_side.value, expiration_tick=5, ) order_id += 1 @@ -192,13 +197,13 @@ def _get_order( return _get_order -def get_player_dict(players: List[Player]) -> Dict[int, Player]: +def get_player_dict(players: List[Player]) -> Dict[str, Player]: return {player.player_id: player for player in players} @pytest.fixture def coal_market(): - return ResourceMarket(Resource.coal) + return ResourceMarket(Resource.COAL) @pytest.fixture diff --git a/backend/fixtures/orderbook_fixtures.py b/backend/fixtures/orderbook_fixtures.py index 5f9992f..e13d62c 100644 --- a/backend/fixtures/orderbook_fixtures.py +++ b/backend/fixtures/orderbook_fixtures.py @@ -1,7 +1,10 @@ from datetime import datetime, timedelta import pytest import random +from fixtures.fixtures import get_player_dict from model import Order, OrderSide, Trade, Resource +from model.player import Player +from model.power_plant_model import ResourcesModel @pytest.fixture(autouse=True) @@ -10,21 +13,30 @@ def random_seed(): @pytest.fixture -def traders(): +def traders_list(): return [ - { - 'id': x, - 'money': 100000, - 'stocks': 1000 - } + Player( + pk=str(x), + player_name=str(x), + game_id="1", + team_id="2", + money=100000, + resources=ResourcesModel(coal=1000), + ) for x in range(1000) ] +@pytest.fixture +def traders(traders_list): + return get_player_dict(traders_list) + + @pytest.fixture def on_add_true(traders): def on_insert(order: Order): return True + return on_insert @@ -35,33 +47,46 @@ def get_timestamp(): def get_timestamp(time: int): nonlocal tm return tm + timedelta(seconds=time) + return get_timestamp @pytest.fixture -def get_random_order(get_order_id, get_order, traders): +def get_random_order(get_order, traders_list): def get_random_order(): - player_id = random.choice(traders)['id'] + player_id = random.choice(traders_list).player_id order_side = random.choice([OrderSide.BUY, OrderSide.SELL]) price = random.randint(500, 1500) size = random.randint(100, 1000) return get_order(player_id, price, size, order_side) + return get_random_order @pytest.fixture() def get_order(get_timestamp, get_order_id): - def get_order(player_id: int, price: int, size: int, order_side: OrderSide, time=0, expiration=5, tick=1): - return Order(timestamp=get_timestamp(time), - expiration_tick=expiration, - order_id=get_order_id(), - player_id=player_id, - game_id=1, - price=price, - size=size, - order_side=order_side, - tick=tick, - resource=Resource.coal) + def get_order( + player_id: str, + price: int, + size: int, + order_side: OrderSide, + time=0, + expiration=5, + tick=1, + ): + return Order( + timestamp=get_timestamp(time), + expiration_tick=expiration, + order_id=str(get_order_id()), + player_id=player_id, + game_id="1", + price=price, + size=size, + order_side=order_side.value, + tick=tick, + resource=Resource.COAL.value, + ) + return get_order @@ -73,6 +98,7 @@ def get_order_id(): nonlocal order_id order_id += 1 return order_id + return get_order_id @@ -83,25 +109,26 @@ def check_trade(trade: Trade): sell_order = trade.sell_order if buy_order is None or sell_order is None: - assert trade.filled_money == 0 # pragma: no cover - assert trade.filled_size == 0 # pragma: no cover - assert trade.filled_price is None # pragma: no cover - return {"can_buy": False, "can_sell": False} # pragma: no cover + assert trade.total_money == 0 + assert trade.trade_size == 0 + assert trade.trade_price is None + return {"can_buy": False, "can_sell": False} buyer_id = buy_order.player_id seller_id = sell_order.player_id - can_buy = traders[buyer_id]['money'] >= trade.filled_money - can_sell = traders[seller_id]['stocks'] >= trade.filled_size + can_buy = traders[buyer_id].money >= trade.total_money + can_sell = traders[seller_id].resources.coal >= trade.trade_size if not can_buy or not can_sell: return {"can_buy": can_buy, "can_sell": can_sell} - traders[buyer_id]['money'] -= trade.filled_money - traders[buyer_id]['stocks'] += trade.filled_size + traders[buyer_id].money -= trade.total_money + traders[buyer_id].resources.coal += trade.trade_size - traders[seller_id]['money'] += trade.filled_money - traders[seller_id]['stocks'] -= trade.filled_size + traders[seller_id].money += trade.total_money + traders[seller_id].resources.coal -= trade.trade_size return {"can_buy": True, "can_sell": True} + return check_trade diff --git a/backend/game/bots/bot.py b/backend/game/bots/bot.py index 376bc0e..6f7d1f4 100644 --- a/backend/game/bots/bot.py +++ b/backend/game/bots/bot.py @@ -7,6 +7,6 @@ def __init__(self, *args, **kwargs): self.game_id = None @abc.abstractmethod - async def run(self, *args, **kwargs) -> None: + def run(self, *args, **kwargs) -> None: pass diff --git a/backend/game/bots/resource_bot.py b/backend/game/bots/resource_bot.py index 3b4ba73..17c9797 100644 --- a/backend/game/bots/resource_bot.py +++ b/backend/game/bots/resource_bot.py @@ -6,8 +6,9 @@ from game.tick.tick_data import TickData from logger import logger from model import Order, OrderSide, Player, Resource, Team +from model.dataset_data import DatasetData -from . import Bot +from .bot import Bot resource_wanted_sum = config["bots"]["resource_sum"] @@ -46,27 +47,32 @@ def __init__(self, *args, **kwargs): self.last_tick = None self.player_id = None - async def run(self, tick_data: TickData): + def run(self, pipe, tick_data: TickData): + self.pipe = pipe if self.player_id is None: - await self.create_player(tick_data) + self.create_player(tick_data) - await self._log_if_no_or_duplicate(tick_data) + self._log_if_no_or_duplicate(tick_data) if not self.should_create_orders(tick_data): return + logger.info( + f" {self.game_id} Bot creating orders in tick {tick_data.game.current_tick} (last {self.last_tick})" + ) self.last_tick = tick_data.game.current_tick resources_sum = self.get_resources_sum(tick_data) - orders = await self.get_last_orders() + orders = self.get_last_orders() for resource in Resource: resource_orders = orders[resource] resource_sum = resources_sum[resource] - filled_buy_perc, filled_sell_perc = self.get_filled_perc(resource_orders) + filled_buy_perc, filled_sell_perc = self.get_filled_perc( + resource_orders) volume = self.get_volume(resource_sum) price = self.get_price(resource, filled_buy_perc, filled_sell_perc) price = self.get_mixed_price(tick_data, resource, price) - await self.create_orders( + self.create_orders( tick_data.game.current_tick, resource, price, volume ) @@ -78,14 +84,17 @@ def should_create_orders(self, tick_data: TickData): return False return True - async def create_player(self, tick_data: TickData): - team = await Team.get(team_secret=config["bots"]["team_secret"]) - self.player_id = await Player.create( + def create_player(self, tick_data: TickData): + team: Team = Team.find( + Team.team_secret == config["bots"]["team_secret"]).first() + self.player_id = Player( player_name="resource_bot", game_id=tick_data.game.game_id, team_id=team.team_id, is_bot=True, - ) + ).save().player_id + logger.game_log(tick_data.game.game_id, + f"creating bot {self.player_id}") self.game_id = tick_data.game.game_id def get_resources_sum(self, tick_data: TickData) -> Dict[Resource, int]: @@ -93,7 +102,7 @@ def get_resources_sum(self, tick_data: TickData) -> Dict[Resource, int]: resources_sum = {resource: 0 for resource in Resource} for resource in Resource: for player in tick_data.players.values(): - resources_sum[resource] += player[resource] + resources_sum[resource] += player.resources[resource] return resources_sum def get_volume(self, resource_sum: int) -> BuySellVolume: @@ -150,10 +159,13 @@ def get_filled_perc(self, orders: List[Order]) -> Tuple[int, int]: } return filled_perc[OrderSide.BUY], filled_perc[OrderSide.SELL] - async def get_last_orders(self) -> Dict[str, Order]: + def get_last_orders(self) -> Dict[str, Order]: if self.last_tick is None: return [] - orders_list = await Order.list(player_id=self.player_id, tick=self.last_tick) + orders_list: List[Order] = Order.find( + (Order.player_id == self.player_id) & + (Order.tick == self.last_tick) + ).all() orders = {resource: [] for resource in Resource} for order in orders_list: orders[order.resource].append(order) @@ -175,10 +187,10 @@ def get_mixed_price( sell_price=sell_price, ) - def mix_dataset_price(self, dataset_row, price, resource: Resource): - return dataset_row[resource.name.lower() + "_price"] + price + def mix_dataset_price(self, dataset_row: DatasetData, price: int, resource: Resource): + return dataset_row.resource_prices[resource] + price - async def create_orders( + def create_orders( self, tick, resource: Resource, price: BuySellPrice, volume: BuySellVolume ) -> None: buy_price = int(price.buy_price * final_price_multiplier) @@ -199,17 +211,18 @@ async def create_orders( for i in range(extra_orders + 1): new_buy_volume = self.get_i_price(buy_volume, i) / buy_volume_sum - new_sell_volume = self.get_i_price(sell_volume, i) / sell_volume_sum + new_sell_volume = self.get_i_price( + sell_volume, i) / sell_volume_sum new_buy_price = buy_price * (1 - i * extra_orders_price_diff) new_sell_price = sell_price * (1 + i * extra_orders_price_diff) - await self.create_order( + self.create_order( tick, resource, order_side=OrderSide.BUY, price=int(new_buy_price), volume=int(new_buy_volume), ) - await self.create_order( + self.create_order( tick, resource, order_side=OrderSide.SELL, @@ -220,19 +233,19 @@ async def create_orders( def get_i_price(self, x, i): return x * (1 - i * extra_orders_volume_diff) - async def create_order( + def create_order( self, tick, resource: Resource, order_side: OrderSide, price: int, volume: int ): logger.debug( f"({self.game_id}) Bot creating orders {tick=}, {order_side.value} {resource=}, {price=}" ) - if price <= 0: - logger.warning(f"Volume ({volume}) is less than 0!") + if volume <= 0: + # logger.warning(f"Volume ({volume}) is less than 0!") return if price <= 0: logger.warning(f"Price ({price}) is less than 0!") return - await Order.create( + Order( game_id=self.game_id, player_id=self.player_id, price=price, @@ -240,19 +253,19 @@ async def create_order( timestamp=datetime.now(), size=volume, order_side=order_side, - resource=resource, + resource=resource.value, expiration_tick=tick + expiration_ticks + 1, - ) + ).save(self.pipe) - async def _log_if_no_or_duplicate(self, tick_data: TickData): + def _log_if_no_or_duplicate(self, tick_data: TickData): if log_when_no_orders: order_count = dict() for resource in Resource: - order_count[resource] = await Order.count_player_orders( - game_id=tick_data.game.game_id, - player_id=self.player_id, - resource=resource, - ) + order_count[resource] = Order.find( + Order.game_id == tick_data.game.game_id, + Order.player_id == self.player_id, + Order.resource == resource.value, + ).count() if not self.should_create_orders(tick_data): if log_when_no_orders: for resource in Resource: @@ -268,6 +281,7 @@ async def _log_if_no_or_duplicate(self, tick_data: TickData): f"Game ({tick_data.game.game_id}) Duplicate orders for bot ({self.player_id}) in tick {tick_data.game.current_tick}, resource {resource.name}" ) + def scale(_min, _max, x): x = _min + (_max - _min) * x return clamp(_min, _max, x) diff --git a/backend/game/bots/test_bots.py b/backend/game/bots/test_bots.py index 82d94b6..4379ee8 100644 --- a/backend/game/bots/test_bots.py +++ b/backend/game/bots/test_bots.py @@ -5,7 +5,7 @@ from .bot import Bot from .bots import Bots import pytest -from model import Resource, OrderSide +from model import Resource, OrderSide, Player, Order from fixtures.fixtures import * from .resource_bot import BuySellPrice, resource_wanted_sum, min_volume, max_volume, default_volume, min_price, max_price @@ -92,11 +92,11 @@ async def test_resource_bot_init(self): def test_get_resources_sum(self, get_player, get_tick_data): bot = ResourceBot() - p1 = get_player() - p2 = get_player() + p1: Player = get_player() + p2: Player = get_player() for resource in Resource: - p1[resource] = 5 - p2[resource] = 10 + p1.resources[resource] = 5 + p2.resources[resource] = 10 players = get_player_dict([p1, p2]) tick_data = get_tick_data(players=players) @@ -155,8 +155,8 @@ def test_get_filled_perc(self): @pytest.fixture def bot(): bot = ResourceBot() - bot.last_buy_coeffs[Resource.coal] = 0.5 - bot.last_sell_coeffs[Resource.coal] = 0.5 + bot.last_buy_coeffs[Resource.COAL] = 0.5 + bot.last_sell_coeffs[Resource.COAL] = 0.5 return bot @@ -167,32 +167,32 @@ def volume(): class TestGetPrice: def test_get_price_0_0(self, bot): - price = bot.get_price(Resource.coal, 0, 0) + price = bot.get_price(Resource.COAL, 0, 0) assert_prices(bot, price) def test_get_price_0_1(self, bot): - price = bot.get_price(Resource.coal, 0, 0) + price = bot.get_price(Resource.COAL, 0, 0) assert_prices(bot, price) def test_get_price_1_0(self, bot): - price = bot.get_price(Resource.coal, 1, 0) + price = bot.get_price(Resource.COAL, 1, 0) assert_prices(bot, price) def test_get_price_2(self, bot): - price = bot.get_price(Resource.coal, 1, 0.2) + price = bot.get_price(Resource.COAL, 1, 0.2) assert_prices(bot, price) - price = bot.get_price(Resource.coal, 0.5, 0.76) + price = bot.get_price(Resource.COAL, 0.5, 0.76) assert_prices(bot, price) - price = bot.get_price(Resource.coal, 0.3, 0.2) + price = bot.get_price(Resource.COAL, 0.3, 0.2) assert_prices(bot, price) def assert_prices(bot: ResourceBot, price: BuySellPrice): assert min_price <= price.buy_price <= max_price assert min_price <= price.sell_price <= max_price - assert 0 <= bot.last_buy_coeffs[Resource.coal] <= 1 - assert 0 <= bot.last_sell_coeffs[Resource.coal] <= 1 - assert bot.last_sell_coeffs[Resource.coal] >= bot.last_buy_coeffs[Resource.coal] + assert 0 <= bot.last_buy_coeffs[Resource.COAL] <= 1 + assert 0 <= bot.last_sell_coeffs[Resource.COAL] <= 1 + assert bot.last_sell_coeffs[Resource.COAL] >= bot.last_buy_coeffs[Resource.COAL] def get_order(order_side, filled_size, size): @@ -201,7 +201,7 @@ def get_order(order_side, filled_size, size): filled_size=filled_size, size=size, tick=0, timestamp=datetime.now(), order_side=order_side, - resource=Resource.coal + resource=Resource.COAL.value ) diff --git a/backend/game/market/energy_market.py b/backend/game/market/energy_market.py index a3d5b81..1fc1012 100644 --- a/backend/game/market/energy_market.py +++ b/backend/game/market/energy_market.py @@ -1,9 +1,10 @@ from collections import defaultdict from datetime import datetime -from typing import Dict +from typing import Dict, List from game.price_tracker.price_tracker import PriceTracker from model import Trade from config import config +from model.order import Order from model.player import Player from logger import logger @@ -17,7 +18,8 @@ def __init__(self): self.orders = {} self.trades = [] - def match(self, players: Dict[int, Player], demand: int, max_price: int) -> Dict[int, int]: + def match(self, players: Dict[str, Player], tick: int, demand: int, max_price: int) -> Dict[str, int]: + """Returns mapping of players to sold energy in integer""" max_per_player = int(demand * max_energy_per_player) def get_energy(player: Player): return max_per_player if player.energy > max_per_player else player.energy @@ -37,13 +39,13 @@ def get_energy(player: Player): for player in player_group: if group_energy_sum > demand: energy_to_sell = get_energy(player) * demand / group_energy_sum - print("PRINTIG 1", player.energy, energy_to_sell) - print("PRINTIG ", demand, group_energy_sum) + # print("PRINTIG 1", player.energy, energy_to_sell) + # print("PRINTIG ", demand, group_energy_sum) else: energy_to_sell = get_energy(player) - print("PRINTIG 2", player.energy, energy_to_sell) + # print("PRINTIG 2", player.energy, energy_to_sell) energy_to_sell = int(energy_to_sell) - self.create_trade(player, energy_to_sell, price) + self.create_trade(player, tick, energy_to_sell, price) demand -= group_energy_sum if demand <= 0: break @@ -51,14 +53,14 @@ def get_energy(player: Player): self.price_tracker.on_end_match(self.trades) return self.orders - def create_trade(self, player: Player, energy, energy_price): + def create_trade(self, player: Player, tick, energy: int, energy_price: int): player.money += energy * player.energy_price self.trades.append(Trade( - buy_order=None, - sell_order=None, - filled_price=energy_price, - filled_size=energy, - filled_money=energy * energy_price, - tick=datetime.now(), + trade_price=energy_price, + trade_size=energy, + total_money=energy * energy_price, + tick=tick, + buy_order_id="energy market", + sell_order_id="energy market", )) self.orders[player.player_id] = energy \ No newline at end of file diff --git a/backend/game/market/resource_market.py b/backend/game/market/resource_market.py index 8442398..e790f48 100644 --- a/backend/game/market/resource_market.py +++ b/backend/game/market/resource_market.py @@ -8,7 +8,7 @@ class ResourceMarket: - def __init__(self, resource: Resource, game_id: int=None): + def __init__(self, resource: Resource, game_id: str=None): self.resource = resource self.orderbook = OrderBook() self.price_tracker = PriceTracker(self.orderbook) @@ -24,14 +24,14 @@ def __init__(self, resource: Resource, game_id: int=None): for callback_type, callback in callbacks.items(): self.orderbook.register_callback(callback_type, callback) - self.players: Dict[int, Player] = None - self._updated: Dict[int, Order] = {} + self.players: Dict[str, Player] = None + self._updated: Dict[str, Order] = {} self.tick_trades: List[Trade] = None - def set_players(self, players: Dict[int, Player]): + def set_players(self, players: Dict[str, Player]): self.players = players - def cancel(self, orders: List[Order]) -> Dict[int, Order]: + def cancel(self, orders: List[Order]) -> Dict[str, Order]: self._updated = {} for order in orders: try: @@ -44,7 +44,7 @@ def cancel(self, orders: List[Order]) -> Dict[int, Order]: return self._updated - def match(self, orders: List[Order], tick: int) -> Dict[int, Order]: + def match(self, orders: List[Order], tick: int) -> Dict[str, Order]: self._updated = {} for order in orders: try: @@ -52,6 +52,8 @@ def match(self, orders: List[Order], tick: int) -> Dict[int, Order]: except ValueError as e: logger.warning( f"Error adding order for order_id {order.order_id}: {e}") + order.order_status = OrderStatus.ACTIVE + self._update_order(order) logger.debug(f"Orderbook for {self.resource.name:>8s} in game ({self.game_id}) matching in tick ({tick}) - {self.orderbook.__str__()}") self.orderbook.match(tick) return self._updated @@ -71,36 +73,38 @@ def _check_trade(self, trade: Trade): elif buyer.is_bot: can_buy = True else: - can_buy = buyer.money >= trade.filled_money + can_buy = buyer.money >= trade.total_money if seller is None: - can_buy = False + can_sell = False elif seller.is_bot: can_sell = True else: - can_sell = seller[self.resource.name] >= trade.filled_size - + can_sell = seller.resources[self.resource] >= trade.trade_size return {"can_buy": can_buy, "can_sell": can_sell} def _on_trade(self, trade: Trade): buyer_id = trade.buy_order.player_id seller_id = trade.sell_order.player_id + # Check trade prevents this from being None buyer = self._get_player(buyer_id) seller = self._get_player(seller_id) if buyer.is_bot and seller.is_bot: - logger.warning(f"Trading between two bots in game ({self.game_id}): {buyer_id}-{seller_id}, resource {trade.buy_order.resource.name}, size {trade.filled_size}, price {trade.filled_price}") + logger.warning(f"Trading between two bots {buyer.player_name} {buyer_id}({buyer.game_id}) and {seller.player_name} {seller_id}({seller.game_id}) in game ({self.game_id}). This is probably due to invalid reseting of the game.") + if buyer_id != seller_id: + logger.critical(f"Trading between two different bots {buyer.player_name} {buyer_id}({buyer.game_id}) and {seller.player_name} {seller_id}({seller.game_id}) in game ({self.game_id}). This is probably due to invalid reseting of the game.") if not buyer.is_bot: - buyer.money -= trade.filled_money - buyer[self.resource.name] += trade.filled_size + buyer.money -= trade.total_money + buyer.resources[self.resource] += trade.trade_size if not seller.is_bot: - seller.money += trade.filled_money - seller[self.resource.name] -= trade.filled_size + seller.money += trade.total_money + seller.resources[self.resource] -= trade.trade_size - def _get_player(self, player_id: int) -> Player: + def _get_player(self, player_id: str) -> Player: if self.players is None: raise ValueError("Players dictionary not set") if player_id not in self.players: diff --git a/backend/game/market/test_energy_market.py b/backend/game/market/test_energy_market.py index c55c526..ff6d906 100644 --- a/backend/game/market/test_energy_market.py +++ b/backend/game/market/test_energy_market.py @@ -18,7 +18,7 @@ def test_not_sell_when_too_high_price(market: EnergyMarket, get_player): player_1: Player = get_player(money=0, energy=100, energy_price=200) player_dict = get_player_dict([player_1]) - orders = market.match(player_dict, demand=100, max_price=100) + orders = market.match(player_dict, tick=0, demand=100, max_price=100) assert player_1.money == 0 assert len(orders) == 0 @@ -29,7 +29,7 @@ def test_not_sell_when_filled(market: EnergyMarket, get_player): player_2: Player = get_player(money=0, energy=100, energy_price=60) player_dict = get_player_dict([player_1, player_2]) - orders = market.match(player_dict, demand=100, max_price=100) + orders = market.match(player_dict, tick=0, demand=100, max_price=100) assert player_1.money == 100*50 assert player_2.money == 0 @@ -41,7 +41,7 @@ def test_sell_partial(market: EnergyMarket, get_player): player_2: Player = get_player(money=0, energy=100, energy_price=60) player_dict = get_player_dict([player_1, player_2]) - orders = market.match(player_dict, demand=150, max_price=100) + orders = market.match(player_dict, tick=0, demand=150, max_price=100) assert player_1.money == 100*50 assert player_2.money == 50*60 @@ -55,7 +55,7 @@ def test_monopoly(market: EnergyMarket, get_player): player_2: Player = get_player(money=0, energy=100, energy_price=50) player_dict = get_player_dict([player_1, player_2]) - orders = market.match(player_dict, demand=100, max_price=100) + orders = market.match(player_dict, tick=0, demand=100, max_price=100) assert player_1.money == 30*50 assert player_2.money == 50*50 @@ -68,7 +68,7 @@ def test_sell_split_when_equal_price(market: EnergyMarket, get_player): player_3: Player = get_player(money=0, energy=100, energy_price=50) player_dict = get_player_dict([player_1, player_2, player_3]) - orders = market.match(player_dict, demand=100, max_price=100) + orders = market.match(player_dict, tick=0, demand=100, max_price=100) print(orders) assert player_1.money == 25*50 @@ -84,7 +84,7 @@ def test_sell_split_when_equal_price_with_monopoly(market: EnergyMarket, get_pla player_2: Player = get_player(money=0, energy=100, energy_price=50) player_dict = get_player_dict([player_1, player_2]) - orders = market.match(player_dict, demand=100, max_price=100) + orders = market.match(player_dict, tick=0, demand=100, max_price=100) assert player_1.money == 30*50 assert player_2.money == 30*50 diff --git a/backend/game/market/test_resource_market.py b/backend/game/market/test_resource_market.py index 0479673..5f1ecfb 100644 --- a/backend/game/market/test_resource_market.py +++ b/backend/game/market/test_resource_market.py @@ -1,11 +1,11 @@ -from model import Order, Player, OrderSide, OrderStatus +from model import Order, Player, OrderSide, OrderStatus, ResourcesModel from fixtures.fixtures import get_player_dict from fixtures.fixtures import * def test_when_transaction_successful(get_player, get_order, coal_market): - player_1: Player = get_player(money=100, coal=0) - player_2: Player = get_player(money=0, coal=100) + player_1: Player = get_player(money=100, resources=ResourcesModel(coal=0)) + player_2: Player = get_player(money=0, resources=ResourcesModel(coal=100)) order_1: Order = get_order( player_id=player_1.player_id, price=1, size=100, order_side=OrderSide.BUY, tick=1) order_2: Order = get_order( @@ -23,9 +23,9 @@ def test_when_transaction_successful(get_player, get_order, coal_market): assert updated[order_3.order_id].order_status == OrderStatus.ACTIVE assert player_1.money == 0 - assert player_1.coal == 100 + assert player_1.resources.coal == 100 assert player_2.money == 100 - assert player_2.coal == 0 + assert player_2.resources.coal == 0 assert updated[order_1.order_id].filled_size == 100 assert updated[order_1.order_id].filled_money == 100 @@ -33,8 +33,8 @@ def test_when_transaction_successful(get_player, get_order, coal_market): def test_cancel_before_match(get_player, get_order, coal_market): - player_1: Player = get_player(money=100, coal=0) - player_2: Player = get_player(money=0, coal=100) + player_1: Player = get_player(money=100, resources=ResourcesModel(coal=0)) + player_2: Player = get_player(money=0, resources=ResourcesModel(coal=100)) order_1: Order = get_order( player_id=player_1.player_id, price=1, size=100, order_side=OrderSide.BUY, tick=1) order_2: Order = get_order( @@ -55,9 +55,9 @@ def test_cancel_before_match(get_player, get_order, coal_market): assert updated[order_3.order_id].order_status == OrderStatus.ACTIVE assert player_1.money == 100 - assert player_1.coal == 0 + assert player_1.resources.coal == 0 assert player_2.money == 0 - assert player_2.coal == 100 + assert player_2.resources.coal == 100 assert updated[order_1.order_id].filled_size == 0 assert updated[order_1.order_id].filled_money == 0 @@ -65,8 +65,8 @@ def test_cancel_before_match(get_player, get_order, coal_market): def test_user_low_balance(get_player, get_order, coal_market): - player_1: Player = get_player(money=0, coal=0) - player_2: Player = get_player(money=0, coal=100) + player_1: Player = get_player(money=0, resources=ResourcesModel(coal=0)) + player_2: Player = get_player(money=0, resources=ResourcesModel(coal=100)) order_1: Order = get_order( player_id=player_1.player_id, price=1, size=100, order_side=OrderSide.BUY, tick=1) order_2: Order = get_order( @@ -84,9 +84,9 @@ def test_user_low_balance(get_player, get_order, coal_market): assert updated[order_3.order_id].order_status == OrderStatus.ACTIVE assert player_1.money == 0 - assert player_1.coal == 0 + assert player_1.resources.coal == 0 assert player_2.money == 0 - assert player_2.coal == 100 + assert player_2.resources.coal == 100 assert updated[order_1.order_id].filled_size == 0 assert updated[order_1.order_id].filled_money == 0 @@ -94,8 +94,8 @@ def test_user_low_balance(get_player, get_order, coal_market): def test_user_low_resources(get_player, get_order, coal_market): - player_1: Player = get_player(money=100, coal=0) - player_2: Player = get_player(money=0, coal=0) + player_1: Player = get_player(money=100, resources=ResourcesModel(coal=0)) + player_2: Player = get_player(money=0, resources=ResourcesModel(coal=0)) order_1: Order = get_order( player_id=player_1.player_id, price=1, size=100, order_side=OrderSide.BUY, tick=1) order_2: Order = get_order( @@ -113,9 +113,9 @@ def test_user_low_resources(get_player, get_order, coal_market): assert updated[order_3.order_id].order_status == OrderStatus.ACTIVE assert player_1.money == 100 - assert player_1.coal == 0 + assert player_1.resources.coal == 0 assert player_2.money == 0 - assert player_2.coal == 0 + assert player_2.resources.coal == 0 assert updated[order_1.order_id].filled_size == 0 assert updated[order_1.order_id].filled_money == 0 diff --git a/backend/game/orderbook/orderbook.py b/backend/game/orderbook/orderbook.py index 832775e..24cb391 100644 --- a/backend/game/orderbook/orderbook.py +++ b/backend/game/orderbook/orderbook.py @@ -1,7 +1,7 @@ from collections import deque from xheap import XHeap from functools import reduce -from model import Order, OrderSide, OrderStatus, OrderType, Trade +from model import Order, OrderSide, OrderStatus, Trade from logger import logger @@ -76,7 +76,7 @@ def cancel_all(self): for order_id in list(self.map_to_heaps.keys()): self.cancel_order(order_id) - def cancel_order(self, order_id: int): + def cancel_order(self, order_id: str): if order_id not in self.map_to_heaps: raise ValueError(f"Order with id {order_id} not found") order: Order = self.map_to_heaps[order_id] @@ -85,7 +85,7 @@ def cancel_order(self, order_id: int): self._invoke_callbacks('on_order_update', order) self._remove_order(order_id) - def _remove_order(self, order_id: int): + def _remove_order(self, order_id: str): order: Order = self.map_to_heaps[order_id] self.expire_heap.remove(order) @@ -156,12 +156,17 @@ def _match_condition(self): def _match_one(self, buy_order: Order, sell_order: Order, tick: int): trade_price = self._get_trade_price(buy_order, sell_order) trade_size = self._get_trade_size(buy_order, sell_order) - filled_money = trade_price * trade_size + total_money = trade_price * trade_size - trade_before = Trade(buy_order, sell_order, tick, - filled_money, trade_size, trade_price) + trade = Trade( + tick=tick, + total_money=total_money, + trade_size=trade_size, + trade_price=trade_price) + trade.buy_order = buy_order + trade.sell_order = sell_order - status = self._invoke_callbacks('check_trade', trade_before) + status = self._invoke_callbacks('check_trade', trade) status_reduced = reduce( lambda x, y: {i: x[i] and y[i] for i in x}, @@ -173,8 +178,8 @@ def _match_one(self, buy_order: Order, sell_order: Order, tick: int): buy_order.filled_size += trade_size sell_order.filled_size += trade_size - buy_order.filled_money += filled_money - sell_order.filled_money += filled_money + buy_order.filled_money += total_money + sell_order.filled_money += total_money buy_order.filled_price = buy_order.filled_money / buy_order.filled_size sell_order.filled_price = sell_order.filled_money / sell_order.filled_size @@ -185,8 +190,6 @@ def _match_one(self, buy_order: Order, sell_order: Order, tick: int): self._remove_if_filled(buy_order.order_id) self._remove_if_filled(sell_order.order_id) - trade = Trade(buy_order, sell_order, tick, - filled_money, trade_size, trade_price) self._invoke_callbacks('on_trade', trade) self.match_trades.append(trade) if not status_reduced['can_buy']: @@ -196,19 +199,19 @@ def _match_one(self, buy_order: Order, sell_order: Order, tick: int): def _get_trade_price(self, buy_order: Order, sell_order: Order): first_order = buy_order if buy_order.timestamp < sell_order.timestamp else sell_order - second_order = sell_order if buy_order.timestamp < sell_order.timestamp else buy_order - - if first_order.order_type != OrderType.MARKET: - return first_order.price - elif second_order.order_type != OrderType.MARKET: - return second_order.price - return self.prev_price + # second_order = sell_order if buy_order.timestamp < sell_order.timestamp else buy_order + # Komentar + # if first_order.order_type != OrderType.MARKET: + return first_order.price + # elif second_order.order_type != OrderType.MARKET: + # return second_order.price + # return self.prev_price def _get_trade_size(self, buy_order: Order, sell_order: Order): return min(buy_order.size - buy_order.filled_size, sell_order.size - sell_order.filled_size) - def _remove_if_filled(self, order_id: int): + def _remove_if_filled(self, order_id: str): order: Order = self.map_to_heaps[order_id] if order.filled_size == order.size: order.order_status = OrderStatus.COMPLETED diff --git a/backend/game/orderbook/test_orderbook.py b/backend/game/orderbook/test_orderbook.py index e6bba73..afea485 100644 --- a/backend/game/orderbook/test_orderbook.py +++ b/backend/game/orderbook/test_orderbook.py @@ -1,7 +1,7 @@ +from typing import Dict, List from unittest.mock import Mock -from model.order_types import OrderType from .orderbook import OrderBook -from model import OrderSide, OrderStatus, Trade +from model import OrderSide, OrderStatus, Trade, Player from fixtures.orderbook_fixtures import * @@ -44,7 +44,8 @@ def test_expire(self, get_order): class TestCheckTrade(): def test_when_both_true(self, get_order): - check_trade = lambda *kwargs: {'can_buy': True, 'can_sell': True} + def check_trade(*args, **kwargs): + return {'can_buy': True, 'can_sell': True} orderbook = OrderBook() orderbook.register_callback('check_trade', check_trade) @@ -66,12 +67,14 @@ def test_when_both_true(self, get_order): assert trade.sell_order is sell_order assert trade.buy_order.order_status == OrderStatus.COMPLETED assert trade.sell_order.order_status == OrderStatus.COMPLETED - assert trade.filled_money == price * size - assert trade.filled_size == size + assert trade.total_money == price * size + assert trade.trade_price == price + assert trade.trade_size == size assert trade.tick == 1 def test_when_both_false(self, get_order): - check_trade = lambda *kwargs: {'can_buy': False, 'can_sell': False} + def check_trade(*args, **kwargs): + return {'can_buy': False, 'can_sell': False} orderbook = OrderBook() orderbook.register_callback('check_trade', check_trade) @@ -95,8 +98,9 @@ def test_when_both_false(self, get_order): assert sell_order.filled_money == 0 assert sell_order.filled_size == 0 - def test_when_one_false(self, get_order): - check_trade = lambda *kwargs: {'can_buy': True, 'can_sell': False} + def test_when_one_false(self, get_order): + def check_trade(*args, **kwargs): + return {'can_buy': True, 'can_sell': False} orderbook = OrderBook() orderbook.register_callback('check_trade', check_trade) @@ -133,11 +137,13 @@ def test_no_check_trade_same_player_id(self, get_order): assert len(orderbook.match_trades) == 1 -def test_zero_sum(traders, on_add_true, check_trade, get_random_order): +def test_zero_sum(traders, traders_list, on_add_true, check_trade, get_random_order): on_add = on_add_true - money_sum = sum([x['money'] for x in traders]) - stocks_sum = sum([x['stocks'] for x in traders]) + traders: Dict[str, Player] + + money_sum = sum([x.money for x in traders_list]) + stocks_sum = sum([x.resources.coal for x in traders_list]) orderbook = OrderBook() orderbook.register_callback('check_add', on_add) @@ -150,8 +156,8 @@ def test_zero_sum(traders, on_add_true, check_trade, get_random_order): orderbook.match(order.tick) orderbook.cancel_all() - money_sum_after = sum([x['money'] for x in traders]) - stocks_sum_after = sum([x['stocks'] for x in traders]) + money_sum_after = sum([x.money for x in traders_list]) + stocks_sum_after = sum([x.resources.coal for x in traders_list]) assert money_sum == money_sum_after assert stocks_sum == stocks_sum_after @@ -193,7 +199,8 @@ def test_size_less_than_zero(get_order): def test_rejected(get_order): - on_add = lambda *kwargs: False + def on_add(*args, **kwargs): + return False orderbook = OrderBook() orderbook.register_callback('check_add', on_add) order = get_order(player_id=1, price=5, size=50, @@ -217,7 +224,6 @@ def test_second_order_is_market_order(get_order): order_side=OrderSide.BUY, tick=1) second_order = get_order(player_id=2, price=5, size=50, order_side=OrderSide.SELL, tick=1) - second_order.order_type = OrderType.MARKET trades = [] orderbook.register_callback('on_trade', lambda trade: trades.append(trade)) @@ -236,10 +242,8 @@ def test_prev_price(get_order): order_side=OrderSide.BUY, tick=1) second_order = get_order(player_id=2, price=5, size=50, order_side=OrderSide.SELL, tick=1) - first_order.order_type = OrderType.MARKET - second_order.order_type = OrderType.MARKET - trades = [] + trades: List[Trade] = [] orderbook.register_callback('on_trade', lambda trade: trades.append(trade)) orderbook.prev_price = 10 @@ -249,7 +253,7 @@ def test_prev_price(get_order): orderbook.match(tick=1) assert len(trades) == 1 - assert trades[0].filled_price == 10 + assert trades[0].total_money == 5 * 50 def test_invalid_callback_type(): diff --git a/backend/game/price_tracker/price_tracker.py b/backend/game/price_tracker/price_tracker.py index b5f5511..5f5e7c5 100644 --- a/backend/game/price_tracker/price_tracker.py +++ b/backend/game/price_tracker/price_tracker.py @@ -36,13 +36,13 @@ def _calculate_low_high(self, trades: List[Trade]): money_size = 0 if len(trades) != 0: - self.open = trades[0].filled_price - self.close = trades[-1].filled_price + self.open = trades[0].trade_price + self.close = trades[-1].trade_price for trade in trades: - price = trade.filled_price - money_sum += trade.filled_money - money_size += trade.filled_size + price = trade.trade_price + money_sum += trade.total_money + money_size += trade.trade_size if self.high is None: self.high = price diff --git a/backend/game/tick/test_ticker_bots.py b/backend/game/tick/test_ticker_bots.py index 7ff12ed..07751ed 100644 --- a/backend/game/tick/test_ticker_bots.py +++ b/backend/game/tick/test_ticker_bots.py @@ -35,7 +35,9 @@ async def test_run_bots(get_tick_data): ticker.game_data[game.game_id] = GameData(game) tick_data = get_tick_data(markets={}, players=players) - await ticker.run_bots(tick_data) + pipe = MagicMock() + + ticker.game_data[game.game_id].bot.run(pipe, tick_data) assert mock_run.call_count == 1 - mock_run.assert_called_with(tick_data) + mock_run.assert_called_with(pipe, tick_data) diff --git a/backend/game/tick/test_ticker_db_operations.py b/backend/game/tick/test_ticker_db_operations.py index 0efdd41..ac0fe04 100644 --- a/backend/game/tick/test_ticker_db_operations.py +++ b/backend/game/tick/test_ticker_db_operations.py @@ -3,32 +3,45 @@ from unittest.mock import MagicMock, patch, call from datetime import datetime from game.market.resource_market import ResourceMarket -from model import Order, OrderStatus, Resource, OrderSide, OrderType +from model import Order, OrderStatus, Resource, OrderSide from game.tick import Ticker, TickData from game.tick.tick_fixtures import * - - -@pytest.mark.asyncio -async def test_get_tick_data(sample_game, sample_players, - sample_pending_orders, sample_user_cancelled_orders, - sample_dataset_row, sample_game_data): +from model.market import Market +from routers.users.fixtures import set_mock_find + + +def test_get_tick_data( + sample_game, + sample_players_list, + sample_players, + sample_pending_orders, + sample_user_cancelled_orders, + sample_dataset_row, + sample_game_data, +): ticker = Ticker() ticker.game_data[sample_game.game_id] = sample_game_data - async def mock_list_players(*args, **kwargs): - return [sample_players[1], sample_players[2]] + def mock_list_players(*args, **kwargs): + return sample_players_list - async def mock_list_orders(*args, **kwargs): - if kwargs.get('order_status') == OrderStatus.PENDING: + called_times = 0 + def mock_list_orders(*args, **kwargs): + nonlocal called_times + called_times += 1 + if called_times == 1: return sample_pending_orders - elif kwargs.get('order_status') == OrderStatus.USER_CANCELLED: + elif called_times == 2: return sample_user_cancelled_orders + raise Exception() - async def mock_get_dataset_data(*args, **kwargs): - return sample_dataset_row - - with patch('model.Player.list', new=mock_list_players), patch('model.Order.list', new=mock_list_orders), patch('model.DatasetData.get', new=mock_get_dataset_data): - tick_data = await ticker.get_tick_data(sample_game) + with patch("model.Player.find", return_value=MagicMock(side_effect=mock_list_players)), patch( + "model.Order.find") as order_find_mock, patch("model.DatasetData.find") as dataset_data_find_mock: + + order_find_mock.return_value = MagicMock() + order_find_mock.return_value.all = MagicMock(side_effect=mock_list_orders) + set_mock_find(dataset_data_find_mock, "first", sample_dataset_row) + tick_data = ticker.get_tick_data(sample_game, sample_players) assert len(tick_data.players) == 2 assert len(tick_data.pending_orders) == 2 @@ -37,30 +50,45 @@ async def mock_get_dataset_data(*args, **kwargs): assert len(tick_data.markets) == len(Resource) -@patch('model.Player.update_many') -@patch('model.Order.update_many') -@pytest.mark.asyncio -async def test_save_tick_data(mock_order_update_many, - mock_player_update_many, - ticker: Ticker, sample_game, sample_players, - sample_pending_orders, sample_user_cancelled_orders, - sample_dataset_row, - sample_energy_market): +@patch("model.Player.save") +@patch("model.Order.save") +def test_save_tick_data( + mock_order_save, + mock_player_save, + ticker: Ticker, + sample_game, + sample_players, + sample_pending_orders, + sample_user_cancelled_orders, + sample_dataset_row, + sample_energy_market, +): sample_update_orders = { - 1: Order(order_id=1, game_id=1, player_id=1, - order_type=OrderType.LIMIT, - order_side=OrderSide.SELL, - order_status=OrderStatus.CANCELLED, - resource=Resource.coal, - price=50, size=100, tick=1, - timestamp=datetime.now()), - 2: Order(order_id=2, game_id=1, player_id=2, - order_type=OrderType.LIMIT, - order_side=OrderSide.BUY, - order_status=OrderStatus.ACTIVE, - resource=Resource.oil, - price=50, size=100, tick=1, - timestamp=datetime.now())} + "1": Order( + pk="1", + game_id="1", + player_id="1", + order_side=OrderSide.SELL.value, + order_status=OrderStatus.CANCELLED.value, + resource=Resource.COAL.value, + price=50, + size=100, + tick=1, + timestamp=datetime.now(), + ), + "2": Order( + pk="2", + game_id="1", + player_id="2", + order_side=OrderSide.BUY.value, + order_status=OrderStatus.ACTIVE.value, + resource=Resource.OIL.value, + price=50, + size=100, + tick=1, + timestamp=datetime.now(), + ), + } tick_data = TickData( game=sample_game, @@ -71,31 +99,33 @@ async def test_save_tick_data(mock_order_update_many, markets=[], bots="", updated_orders=sample_update_orders, - energy_market=sample_energy_market + energy_market=sample_energy_market, ) - await ticker.save_tick_data(tick_data) + ticker.pipe = MagicMock() + ticker.save_tick_data(tick_data) - assert mock_player_update_many.call_count == 1 - assert mock_order_update_many.call_count == 1 + assert mock_player_save.call_count == 2 + assert mock_order_save.call_count == 2 -@pytest.mark.asyncio -async def test_save_electricity_orders(sample_game, sample_players): +def test_save_electricity_orders(sample_game, sample_players): players = sample_players game = sample_game - energy_sold = {1: 100, 2: 200} - with patch('model.Order.create_many') as mock_order_create: + energy_sold = {"1": 100, "2": 200} + with patch("model.Order.save") as mock_order_create: ticker = Ticker() - await ticker.save_electricity_orders( - players=players, game=game, energy_sold=energy_sold, tick=1) + ticker.pipe = MagicMock() + ticker.save_electricity_orders( + players=players, game=game, energy_sold=energy_sold, tick=1 + ) - assert mock_order_create.call_count == 1 + # Created two new energy orders + assert mock_order_create.call_count == 2 -@pytest.mark.asyncio -async def test_save_market_data(ticker: Ticker, sample_game, tick_data): +def test_save_market_data(ticker: Ticker, sample_game, tick_data): for resource in Resource: price_tracker_mock: PriceTracker = MagicMock() price_tracker_mock.get_low.return_value = 50 @@ -108,15 +138,11 @@ async def test_save_market_data(ticker: Ticker, sample_game, tick_data): tick_data.markets[resource.value] = ResourceMarket(resource) tick_data.markets[resource.value].price_tracker = price_tracker_mock - with patch('model.market.Market.create') as mock_create: - await ticker.save_market_data(tick_data) - - assert mock_create.call_count == len(Resource) + 1 + with patch("model.Market.save") as mock_save: + ticker.pipe = MagicMock() + ticker.save_market_data(tick_data) - expected_calls = [ - call(game_id=sample_game.game_id, tick=1, resource=resource.value, - low=50, high=60, open=45, close=55, market=70, volume=20) - for resource in Resource - ] + assert mock_save.call_count == len(Resource) + 1 - mock_create.assert_has_calls(expected_calls, any_order=True) + # Teze se testira s cim se pozivalo jer su u pozivima objekti + # mock_save.assert_has_calls(expected_calls, any_order=True) diff --git a/backend/game/tick/test_ticker_run_all_game_ticks.py b/backend/game/tick/test_ticker_run_all_game_ticks.py index 53dd159..51b3d09 100644 --- a/backend/game/tick/test_ticker_run_all_game_ticks.py +++ b/backend/game/tick/test_ticker_run_all_game_ticks.py @@ -7,6 +7,8 @@ from fixtures.fixtures import * import tracemalloc +from routers.users.fixtures import set_mock_find + tracemalloc.start() @@ -17,7 +19,7 @@ def ticker(): def get_game(start_time, current_tick, total_ticks, is_finished): - return Game(game_id=1, game_name="Game1", + return Game(pk="1", game_name="Game1", is_finished=is_finished, start_time=start_time, current_tick=current_tick, @@ -35,9 +37,10 @@ async def test_run_tick_manager(ticker): mock_start_game = AsyncMock() mock_end_game = AsyncMock() - with patch('model.Game.list', new=mock_game_list), \ + with patch('model.Game.find') as find_mock, \ patch('game.tick.Ticker.start_game', new=mock_start_game), \ patch('game.tick.Ticker.end_game', new=mock_end_game): + set_mock_find(find_mock, "all", mock_game_list) await ticker.run_tick_manager(1) @@ -55,9 +58,10 @@ async def test_run_tick_manager_game_finished(ticker): mock_start_game = AsyncMock() mock_end_game = AsyncMock() - with patch('model.Game.list', new=mock_game_list), \ + with patch('model.Game.find') as find_mock, \ patch('game.tick.Ticker.start_game', new=mock_start_game), \ patch('game.tick.Ticker.end_game', new=mock_end_game): + set_mock_find(find_mock, "all", mock_game_list) await ticker.run_tick_manager(1) mock_start_game.assert_not_called() @@ -74,9 +78,11 @@ async def test_run_tick_manager_game_not_started(ticker): mock_start_game = AsyncMock() mock_end_game = AsyncMock() - with patch('model.Game.list', new=mock_game_list), \ + with patch('model.Game.find') as find_mock, \ patch('game.tick.Ticker.start_game', new=mock_start_game), \ patch('game.tick.Ticker.end_game', new=mock_end_game): + set_mock_find(find_mock, "all", mock_game_list) + await ticker.run_tick_manager(1) mock_start_game.assert_not_called() @@ -89,21 +95,19 @@ async def test_end_game(ticker): game = get_game(start_time=datetime.now() - timedelta(seconds=1), current_tick=10, total_ticks=10, is_finished=False) - ticker.game_data[1] = Mock() - ticker.game_futures[1] = Mock() + ticker.game_data["1"] = Mock() + ticker.game_futures["1"] = Mock() - mock_game_update = AsyncMock() + mock_game_update = Mock() with patch('model.Game.update', new=mock_game_update): - await ticker.end_game(game) - mock_game_update.assert_called_once_with( - game_id=1, is_finished=True) + mock_game_update.assert_called_once_with(is_finished=True) - assert 1 not in ticker.game_data + assert "1" not in ticker.game_data - ticker.game_futures[1].cancel.assert_called_once() + ticker.game_futures["1"].cancel.assert_called_once() @pytest.mark.asyncio @@ -116,7 +120,7 @@ async def test_start_game(ticker): patch('game.tick.Ticker.run_game') as run_game_mock: await ticker.start_game(game) - delete_all_running_bots_mock.assert_called_once_with(1) + delete_all_running_bots_mock.assert_called_once_with("1") run_game_mock.assert_called_once() load_previous_oderbook_mock.assert_called_once() @@ -129,18 +133,14 @@ async def test_run_game(ticker): game = get_game(start_time=datetime.now() - timedelta(seconds=1), current_tick=9, total_ticks=10, is_finished=False) - with patch('model.Game.get') as mock_get, \ - patch('databases.Database.transaction') as mock_transaction, \ - patch('databases.Database.execute') as mock_execute, \ + with patch('model.Game.get', return_value=game) as mock_get, \ patch('game.tick.Ticker.run_game_tick') as mock_run_game_tick, \ patch('asyncio.sleep') as mock_sleep: - mock_get.return_value = game - ticker.run_game_tick = MagicMock() await ticker.run_game(game, iters=1) - mock_get.assert_called_once_with(game_id=1) + mock_get.assert_called_once_with("1") ticker.run_game_tick.assert_called_once_with(game) diff --git a/backend/game/tick/test_ticker_run_markets.py b/backend/game/tick/test_ticker_run_markets.py index 0c6ca5b..6554dad 100644 --- a/backend/game/tick/test_ticker_run_markets.py +++ b/backend/game/tick/test_ticker_run_markets.py @@ -1,7 +1,7 @@ from copy import deepcopy import pytest from fixtures.fixtures import * -from model.order_types import OrderSide, OrderStatus +from model import OrderSide, OrderStatus, ResourcesModel def test_run_markets_no_match(get_tick_data, get_order, ticker, get_player, coal_market, get_markets): @@ -13,8 +13,8 @@ def test_run_markets_no_match(get_tick_data, get_order, ticker, get_player, coal fresh_order1 = deepcopy(order1) fresh_order2 = deepcopy(order2) - player1 = get_player(money=100, coal=0) - player2 = get_player(money=0, coal=100) + player1 = get_player(money=100, resources=ResourcesModel(coal=0)) + player2 = get_player(money=0, resources=ResourcesModel(coal=100)) player_dict = get_player_dict([player1, player2]) @@ -38,13 +38,13 @@ def test_run_markets_no_match(get_tick_data, get_order, ticker, get_player, coal def test_run_markets_match(get_tick_data, get_order, ticker, get_player, coal_market, get_markets): - order1 = get_order(player_id=1, price=5, size=50, + order1 = get_order(player_id="1", price=5, size=50, order_side=OrderSide.BUY, tick=1) - order2 = get_order(player_id=2, price=5, size=25, + order2 = get_order(player_id="2", price=5, size=25, order_side=OrderSide.SELL, tick=1) - player1 = get_player(money=1000, coal=0) - player2 = get_player(money=0, coal=100) + player1 = get_player(money=1000, resources=ResourcesModel(coal=0)) + player2 = get_player(money=0, resources=ResourcesModel(coal=100)) player_dict = get_player_dict([player1, player2]) @@ -81,8 +81,8 @@ def test_run_markets_match_insufficient_funds(get_tick_data, get_order, ticker, order2 = get_order(player_id=2, price=5, size=25, order_side=OrderSide.SELL, tick=1) - player1 = get_player(money=124, coal=0) # needs 125 - player2 = get_player(money=0, coal=100) + player1 = get_player(money=124, resources=ResourcesModel(coal=0)) # needs 125 + player2 = get_player(money=0, resources=ResourcesModel(coal=100)) player_dict = get_player_dict([player1, player2]) @@ -119,8 +119,8 @@ def test_run_markets_match_insufficient_resources(get_tick_data, get_order, tick order2 = get_order(player_id=2, price=5, size=25, order_side=OrderSide.SELL, tick=1) - player1 = get_player(money=1000, coal=0) - player2 = get_player(money=0, coal=24) # needs 25 + player1 = get_player(money=1000, resources=ResourcesModel(coal=0)) + player2 = get_player(money=0, resources=ResourcesModel(coal=24)) # needs 25 player_dict = get_player_dict([player1, player2]) @@ -159,8 +159,8 @@ def test_run_markets_cancel(get_tick_data, get_order, ticker, get_player, coal_m order2 = get_order(player_id=2, price=5, size=25, order_side=OrderSide.SELL, tick=1) - player1 = get_player(money=1000, coal=0) - player2 = get_player(money=0, coal=100) + player1 = get_player(money=1000, resources=ResourcesModel(coal=100)) + player2 = get_player(money=0, resources=ResourcesModel(coal=1000)) player_dict = get_player_dict([player1, player2]) diff --git a/backend/game/tick/test_ticker_run_power_plants.py b/backend/game/tick/test_ticker_run_power_plants.py index 6945863..c6e273b 100644 --- a/backend/game/tick/test_ticker_run_power_plants.py +++ b/backend/game/tick/test_ticker_run_power_plants.py @@ -15,8 +15,8 @@ async def test_run_power_plants(sample_game, sample_players, sample_game_data, for player_id, player in updated_tick_data.players.items(): total_energy = sum([ - player[plant_type.name.lower() + "_plants_powered"] * - plant_type.get_produced_energy(updated_tick_data.dataset_row) + player.power_plants_powered[plant_type] * + updated_tick_data.dataset_row.power_plants_output[plant_type] for plant_type in PowerPlantType ]) assert player.energy == total_energy diff --git a/backend/game/tick/test_ticker_test_run_game_tick.py b/backend/game/tick/test_ticker_test_run_game_tick.py index d615450..aaa258a 100644 --- a/backend/game/tick/test_ticker_test_run_game_tick.py +++ b/backend/game/tick/test_ticker_test_run_game_tick.py @@ -1,12 +1,12 @@ import pytest +from game.bots.resource_bot import ResourceBot from model import Game from game.tick import Ticker from unittest.mock import patch from game.tick.tick_fixtures import * -@pytest.mark.asyncio -async def test_run_game_tick( +def test_run_game_tick( sample_game, sample_game_data, tick_data, ): with patch.object(Ticker, 'get_tick_data', return_value=tick_data), \ @@ -16,22 +16,25 @@ async def test_run_game_tick( patch.object(Ticker, 'save_electricity_orders'), \ patch.object(Ticker, 'save_tick_data'), \ patch.object(Ticker, 'save_market_data'), \ + patch.object(Ticker, '_log_networth'), \ + patch.object(Game, 'get', return_value=sample_game), \ patch.object(Game, 'update'), \ - patch.object(Ticker, 'run_bots'): + patch.object(Game, 'save'), \ + patch.object(ResourceBot, 'run'): ticker = Ticker() ticker.game_data[sample_game.game_id] = sample_game_data old_tick = sample_game.current_tick - await ticker.run_game_tick(sample_game) + ticker.run_game_tick(sample_game) assert sample_game.current_tick == old_tick + 1 - Ticker.get_tick_data.assert_called_once_with(sample_game) + Ticker.get_tick_data.assert_called_once_with(sample_game, {}) Ticker.run_markets.assert_called_once() Ticker.run_power_plants.assert_called_once() Ticker.run_electricity_market.assert_called_once() Ticker.save_electricity_orders.assert_called_once() Ticker.save_tick_data.assert_called_once() - Game.update.assert_called_once_with( - game_id=sample_game.game_id, current_tick=sample_game.current_tick) - Ticker.run_bots.assert_called_once_with(tick_data) + Game.update.assert_called_once_with(is_finished=False) + Game.save.assert_called_once() + ResourceBot.run.assert_called_once() diff --git a/backend/game/tick/tick_data.py b/backend/game/tick/tick_data.py index e920816..24757d0 100644 --- a/backend/game/tick/tick_data.py +++ b/backend/game/tick/tick_data.py @@ -3,12 +3,13 @@ from model import Player, Game, Order, DatasetData from game.market import ResourceMarket, EnergyMarket from game.bots.bot import Bot +from model.trade import Trade @dataclass class TickData: game: Game - players: Dict[int, Player] + players: Dict[str, Player] markets: Dict[str, ResourceMarket] energy_market: EnergyMarket bots: List[Bot] @@ -17,5 +18,5 @@ class TickData: pending_orders: List[Order] = field(default_factory=list) user_cancelled_orders: List[Order] = field(default_factory=list) - updated_orders: Dict[int, Order] = field(default_factory=dict) - tick_trades: Dict[int, Order] = field(default_factory=list) + updated_orders: Dict[str, Order] = field(default_factory=dict) + tick_trades: List[Trade] = field(default_factory=list) diff --git a/backend/game/tick/tick_fixtures.py b/backend/game/tick/tick_fixtures.py index d43f223..467fbdc 100644 --- a/backend/game/tick/tick_fixtures.py +++ b/backend/game/tick/tick_fixtures.py @@ -1,21 +1,24 @@ from datetime import datetime import pytest +from fixtures.fixtures import get_player_dict from game.market import EnergyMarket from game.tick import TickData, Ticker, GameData from model import Game, Player, Order, OrderStatus, Resource +from model.dataset_data import DatasetData from model.order_types import OrderSide +from model.power_plant_model import ResourcesModel @pytest.fixture def sample_game(): return Game( - game_id=1, + game_id="1", game_name="Sample Game", start_time=datetime(2024, 1, 1), current_tick=1, total_ticks=10, is_finished=False, - dataset_id=1, + dataset_id="1", bots="", tick_time=1000, is_contest=False @@ -28,11 +31,15 @@ def sample_game_data(sample_game): @pytest.fixture -def sample_players(): - return { - 1: Player(player_id=1, game_id=1, player_name="Player 1", energy=0, team_id=1, wind_plants_owned=2, wind_plants_powered=2), - 2: Player(player_id=2, game_id=1, player_name="Player 2", energy=0, team_id=1, coal=100, coal_plants_powered=2, coal_plants_owned=2) - } +def sample_players_list(): + return [ + Player(pk="1", game_id="1", player_name="Player 1", energy=0, team_id="1", wind_plants_owned=2, wind_plants_powered=2), + Player(pk="2", game_id="1", player_name="Player 2", energy=0, team_id="1", coal=100, coal_plants_powered=2, coal_plants_owned=2) + ] + +@pytest.fixture +def sample_players(sample_players_list): + return get_player_dict(sample_players_list) @pytest.fixture @@ -50,26 +57,35 @@ def ticker(sample_game, sample_game_data): @pytest.fixture def sample_pending_orders(): return [ - Order(order_id=1, game_id=1, player_id=1, order_side=OrderSide.SELL, - order_status=OrderStatus.PENDING, resource=Resource.coal, price=50, size=100, tick=1, timestamp=datetime.now()), - Order(order_id=2, game_id=1, player_id=2, order_side=OrderSide.SELL, - order_status=OrderStatus.PENDING, resource=Resource.oil, price=50, size=100, tick=1, timestamp=datetime.now()) + Order(order_id="1", game_id="1", player_id="1", order_side=OrderSide.SELL.value, + order_status=OrderStatus.PENDING.value, resource=Resource.COAL.value, price=50, size=100, tick=1, timestamp=datetime.now()), + Order(order_id="2", game_id="1", player_id="2", order_side=OrderSide.SELL.value, + order_status=OrderStatus.PENDING.value, resource=Resource.OIL.value, price=50, size=100, tick=1, timestamp=datetime.now()) ] @pytest.fixture def sample_user_cancelled_orders(): return [ - Order(order_id=3, game_id=1, player_id=1, order_side=OrderSide.SELL, - order_status=OrderStatus.USER_CANCELLED, resource=Resource.coal, price=50, size=100, tick=1, timestamp=datetime.now()), - Order(order_id=4, game_id=1, player_id=2, order_side=OrderSide.SELL, - order_status=OrderStatus.USER_CANCELLED, resource=Resource.oil, price=50, size=100, tick=1, timestamp=datetime.now()) + Order(order_id="3", game_id="1", player_id="1", order_side=OrderSide.SELL.value, + order_status=OrderStatus.USER_CANCELLED.value, resource=Resource.COAL.value, price=50, size=100, tick=1, timestamp=datetime.now()), + Order(order_id="4", game_id="1", player_id="2", order_side=OrderSide.SELL.value, + order_status=OrderStatus.USER_CANCELLED.value, resource=Resource.OIL.value, price=50, size=100, tick=1, timestamp=datetime.now()) ] @pytest.fixture def sample_dataset_row(): - return {"energy_demand": 100, "max_energy_price": 50, "coal": 100, "oil": 100, "uranium": 100, 'biomass': 100, 'gas': 100, 'geothermal': 100, 'solar': 100, 'wind': 100, 'hydro': 100} + return DatasetData( + dataset_id="a", + energy_demand=100, + date=datetime.now(), + max_energy_price=50, + tick=5, + power_plants_output = ResourcesModel( + coal=100, oil=100, uranium=100, biomass=100, gas=100, geothermal=100, solar=100, wind=100, hydro=100 + ) + ) @pytest.fixture diff --git a/backend/game/tick/ticker.py b/backend/game/tick/ticker.py index f373adc..42fb4f6 100644 --- a/backend/game/tick/ticker.py +++ b/backend/game/tick/ticker.py @@ -1,89 +1,96 @@ import asyncio +from contextlib import ExitStack from itertools import chain +from operator import attrgetter, methodcaller +from pprint import pprint import sys import traceback from datetime import datetime, timedelta -from typing import Dict, List, Tuple +from typing import Dict, Iterator, List, Tuple + +from pyinstrument import Profiler + from game.bots.bot import Bot -from model import Player, PowerPlantType, Game, Order, OrderStatus, Resource, DatasetData, OrderSide, OrderType +from game.bots.resource_bot import ResourceBot +from model import Player, PowerPlantType, Game, Order, OrderStatus, Resource, DatasetData, OrderSide from game.market import ResourceMarket, EnergyMarket -from game.bots import Bots from model.market import Market from model.resource import Energy -from model.trade import TradeDb +from model.team import Team from .tick_data import TickData -from logger import logger -from db import database +from logger import logger, score_logger +from config import config +from redlock.lock import RedLock class GameData: def __init__(self, game: Game): - self.markets: Dict[int, ResourceMarket] = { + self.markets: Dict[str, ResourceMarket] = { resource.value: ResourceMarket(resource, game.game_id) for resource in Resource } self.energy_market = EnergyMarket() - self.bots: List[Bot] = Bots.create_bots("resource_bot:1") + self.bot: Bot = ResourceBot() class Ticker: def __init__(self): - self.game_data: Dict[int, GameData] = {} - self.game_futures: Dict[int, asyncio.Future] = {} + self.game_data: Dict[str, GameData] = {} + self.game_futures: Dict[str, asyncio.Future] = {} self.tick_event = None async def run_tick_manager(self, iters=None, tick_event=None): for i in range(iters or sys.maxsize): - games = await Game.list() + games: List[Game] = Game.find().all() for game in games: if game.is_finished: continue - - if datetime.now() < game.start_time: + if game.start_time > datetime.now(): continue - if game.game_id not in self.game_data: await self.start_game(game, tick_event=None) continue - await asyncio.sleep(0.1) async def end_game(self, game: Game): try: - logger.info( - f"Ending game ({game.game_id}) {game.game_name}") - await Game.update(game_id=game.game_id, is_finished=True) + logger.info(f"Ending game {game.game_id} {game.game_name}") + # TODO: check if this works + game.update(is_finished=True) if self.game_data.get(game.game_id) is not None: del self.game_data[game.game_id] self.game_futures[game.game_id].cancel() except Exception: logger.critical( - f"Failed ending game ({game.game_id}) (tick {game.current_tick}) with error:\n{traceback.format_exc()}") + f"Failed ending game {game.game_id} (tick {game.current_tick}) with error:\n{traceback.format_exc()}") + await asyncio.sleep(1) async def start_game(self, game: Game, tick_event=None): self.tick_event = tick_event try: logger.info( - f"Starting game ({game.game_id}) {game.game_name} with tick {game.current_tick}/{game.total_ticks}") + f"Starting game {game.game_id} {game.game_name} with tick {game.current_tick}/{game.total_ticks}") - await self.delete_all_running_bots(game.game_id) + self.delete_all_running_bots(game.game_id) self.game_data[game.game_id] = GameData(game) - await self.load_previous_oderbook(game.game_id) + self.load_previous_oderbook(game.game_id) self.game_futures[game.game_id] = asyncio.create_task( self.run_game(game), name=f"game_{game.game_id}") except Exception: logger.critical( - f"Failed creating game ({game.game_id}) (tick {game.current_tick}) with error:\n{traceback.format_exc()}") + f"Failed creating game {game.game_id} (tick {game.current_tick}) with error:\n{traceback.format_exc()}") + await asyncio.sleep(1) async def run_game(self, game: Game, iters=None): for i in range(iters or sys.maxsize): - game = await Game.get(game_id=game.game_id) + game = Game.get(game.pk) + try: if self.tick_event is not None: await self.tick_event.wait() @@ -93,6 +100,9 @@ async def run_game(self, game: Game, iters=None): await self.end_game(game) return + if game.is_finished: + return + # wait until the tick should start should_start = game.start_time + \ timedelta(milliseconds=game.current_tick * @@ -103,73 +113,148 @@ async def run_game(self, game: Game, iters=None): if to_wait < 0.1 and game.current_tick > 0: logger.warning( - f"({game.game_id}) {game.game_name} has short waiting time: {to_wait}s in tick ({game.current_tick}), catching up or possible overload") - await asyncio.sleep(0.1) + f"{game.game_id} {game.game_name} has short waiting time: {to_wait}s in tick ({game.current_tick}), catching up or possible overload") await asyncio.sleep(to_wait) - # run the tick - async with database.transaction(): - await database.execute( - "LOCK TABLE orders, players IN SHARE ROW EXCLUSIVE MODE") - - await self.run_game_tick(game) + start_time = datetime.now() + self.run_game_tick(game) + interval = (datetime.now() - start_time).total_seconds() + logger.info( + f"{interval:.6} Ticking game {game.game_id} {game.game_name} with tick {game.current_tick}/{game.total_ticks}") except Exception: logger.critical( - f"({game.game_id}) {game.game_name} (tick {game.current_tick}) failed with error:\n{traceback.format_exc()}") + f"{game.game_id} {game.game_name} (tick {game.current_tick}) failed with error:\n{traceback.format_exc()}") + await asyncio.sleep(1) - async def load_previous_oderbook(self, game_id: int): + def load_previous_oderbook(self, game_id: str): # in case of restart these need to be reloaded # IN_QUEUE = "IN_QUEUE" # ACTIVE = "ACTIVE" - orders = await Order.list(game_id=game_id, - order_status=OrderStatus.IN_QUEUE.value) - orders += await Order.list(game_id=game_id, - order_status=OrderStatus.ACTIVE.value) - for order in orders: + queue_orders: List[Order] = Order.find( + Order.game_id == game_id, + Order.order_status == OrderStatus.IN_QUEUE.value).all() + logger.game_log( + game_id, f"reloading IN_QUEUE orders {len(queue_orders)}") + active_orders = Order.find( + Order.game_id == game_id, + Order.order_status == OrderStatus.ACTIVE.value).all() + logger.game_log( + game_id, f"reloading ACTIVE orders {len(active_orders)}") + for order in chain(active_orders, queue_orders): markets = self.game_data[game_id].markets markets[order.resource.value].orderbook.add_order(order) - async def delete_all_running_bots(self, game_id: int): - bots = await Player.list(game_id=game_id, is_bot=True) - + def delete_all_running_bots(self, game_id: str): + bots: List[Player] = Player.find( + Player.game_id == game_id, + Player.is_bot == int(True) + ).all() + pipe = Player.db().pipeline() for bot in bots: - await Player.update(player_id=bot.player_id, is_active=False) - - await Order.delete_bot_orders(game_id=game_id) - - async def run_game_tick(self, game: Game): + with bot.lock(): + bot.is_active = False + bot.save(pipe) + canceled = bot.cancel_orders(pipe) + logger.info( + f" {bot.game_id} Deleting bot {bot.player_name} {bot.player_id} and his orders ({canceled})") + pipe.execute() + + def run_game_tick(self, game: Game): + # profiler = Profiler() + # profiler.start() + + self.pipe = Order.db().pipeline() + with ExitStack() as stack: + players = self.get_players_and_enter_context(game, stack) + tick_data = self.get_tick_data(game, players) + tick_data = self.run_markets(tick_data) + tick_data = self.run_power_plants(tick_data) + tick_data, energy_sold = self.run_electricity_market( + tick_data, self.game_data[game.game_id].energy_market) + + self.save_electricity_orders( + game, tick_data.players, energy_sold, game.current_tick) + self.save_tick_data(tick_data) + self.save_market_data(tick_data) + game.current_tick += 1 + + is_finished = Game.get(game.pk).is_finished + game.update(is_finished=is_finished) + game.save(self.pipe) + self.pipe.execute() + + if (game.current_tick % config['log_networth_delay'] == 0): + self._log_networth(game) + logger.game_log(tick_data.game.game_id, + f"updated orders {len(tick_data.updated_orders)}") + logger.game_log(tick_data.game.game_id, + f"updated trades {len(tick_data.tick_trades)}") + + self.game_data[tick_data.game.game_id].bot.run(self.pipe, tick_data) + self.pipe.execute() + + # profiler.stop() + # profiler.print() + def _log_networth(self, game: Game): + players: List[Player] = Player.find( + Player.game_id == game.game_id, + Player.is_bot == int(False) + ).all() + dataset_data = DatasetData.find( + (DatasetData.tick == game.current_tick) & + (DatasetData.dataset_id == game.dataset_id) + ).first() + teams: List[Team] = Team.find().all() + teams: Dict[str, Team] = {team.pk: team for team in teams} + + def get_name(player: Player): + team_name = teams[player.team_id].team_name + return f"{team_name}/{player.player_name}" + + def get_score_name(player: Player): + name = get_name(player) + score = player.get_networth(game, dataset_data).total + return (name, score) + scores = list(map(get_score_name, players)) + score_logger.log( + game_id=game.game_id, + game_name=game.game_name, + tick=game.current_tick, + scores=scores) + + def get_players_and_enter_context(self, game: Game, stack: ExitStack) -> Dict[str, Player]: + players = Player.find(Player.game_id == game.game_id).all() + for player_lock in list(map(methodcaller('lock'), players)): + stack.enter_context(player_lock) + player_pks = map(attrgetter('pk'), players) + players = list(map(Player.get, player_pks)) + players = {player.player_id: player for player in players} - logger.debug( - f"({game.game_id}) {game.game_name}: {game.current_tick} tick") - tick_data = await self.get_tick_data(game) - - tick_data = self.run_markets(tick_data) - - tick_data = self.run_power_plants(tick_data) - - tick_data, energy_sold = self.run_electricity_market( - tick_data, self.game_data[game.game_id].energy_market) - - await self.save_electricity_orders( - game, tick_data.players, energy_sold, game.current_tick) - - await self.save_tick_data(tick_data) - await self.save_market_data(tick_data) - await Game.update(game_id=game.game_id, current_tick=game.current_tick + 1) - tick_data.game.current_tick += 1 - await self.run_bots(tick_data) - - async def get_tick_data(self, game: Game) -> TickData: players = { - player.player_id: player - for player in await Player.list(game_id=game.game_id) + player.pk: player + for player in Player.find(Player.game_id == game.game_id).all() } - - pending_orders = await Order.list(game_id=game.game_id, order_status=OrderStatus.PENDING) - user_cancelled_orders = await Order.list(game_id=game.game_id, order_status=OrderStatus.USER_CANCELLED) - dataset_row = await DatasetData.get(dataset_id=game.dataset_id, tick=game.current_tick) + return players + + def get_tick_data(self, game: Game, players: Dict[str, Player]) -> TickData: + def is_in_players(order: Order): + return order.player_id in players + + pending_orders = Order.find( + Order.game_id == game.game_id, + Order.order_status == OrderStatus.PENDING.value).all() + pending_orders = list(filter(is_in_players, pending_orders)) + logger.game_log(game.game_id, f"pending orders {len(pending_orders)}") + user_cancelled_orders = Order.find( + Order.game_id == game.game_id, + Order.order_status == OrderStatus.USER_CANCELLED.value).all() + user_cancelled_orders = list( + filter(is_in_players, user_cancelled_orders)) + dataset_row = DatasetData.find( + DatasetData.dataset_id == game.dataset_id, + DatasetData.tick == game.current_tick).first() markets = self.game_data[game.game_id].markets tick_data = TickData( @@ -177,12 +262,12 @@ async def get_tick_data(self, game: Game) -> TickData: players=players, markets=markets, energy_market=self.game_data[game.game_id].energy_market, - bots=self.game_data[game.game_id].bots, + # TODO pretvoriti u jednog bota + bots=[self.game_data[game.game_id].bot], pending_orders=pending_orders, user_cancelled_orders=user_cancelled_orders, dataset_row=dataset_row ) - return tick_data def run_markets(self, tick_data: TickData) -> TickData: @@ -210,12 +295,9 @@ def run_markets(self, tick_data: TickData) -> TickData: tick_data.updated_orders.update(updated) - tick_data.tick_trades = [] for market in tick_data.markets.values(): tick_data.tick_trades.extend( market.get_last_tick_trades()) - tick_data.tick_trades = list(map( - TradeDb.from_trade, tick_data.tick_trades)) return tick_data @@ -226,35 +308,32 @@ def run_power_plants(self, tick_data: TickData) -> TickData: player.energy = 0 for type in PowerPlantType: - to_consume = player[type.name.lower() + "_plants_powered"] + to_consume = player.power_plants_powered[type] if not type.is_renewable(): - to_consume = min(to_consume, player[type.name.lower()]) - player[type.name.lower()] -= to_consume - - player.energy += to_consume * type.get_produced_energy( - tick_data.dataset_row) + to_consume = min(to_consume, player.resources[type]) + player.resources[type] -= to_consume + player.energy += to_consume * \ + tick_data.dataset_row.power_plants_output[type] return tick_data def run_electricity_market(self, tick_data: TickData, energy_market: EnergyMarket - ) -> Tuple[TickData, Dict[int, int]]: + ) -> Tuple[TickData, Dict[str, int]]: energy_sold = energy_market.match( - tick_data.players, tick_data.dataset_row.energy_demand, - tick_data.dataset_row.max_energy_price) - + players=tick_data.players, + tick=tick_data.game.current_tick, + demand=tick_data.dataset_row.energy_demand, + max_price=tick_data.dataset_row.max_energy_price) return tick_data, energy_sold - async def save_electricity_orders(self, game: Game, players: Dict[int, Player], - energy_sold: Dict[int, int], tick: int): - electricity_orders = [] + def save_electricity_orders(self, game: Game, players: Dict[str, Player], + energy_sold: Dict[str, int], tick: int): for player_id, energy in energy_sold.items(): - electricity_orders.append(Order( - order_id=0, + Order( game_id=game.game_id, player_id=player_id, - order_type=OrderType.LIMIT, order_side=OrderSide.SELL, timestamp=datetime.now(), order_status=OrderStatus.COMPLETED, @@ -265,24 +344,24 @@ async def save_electricity_orders(self, game: Game, players: Dict[int, Player], filled_money=players[player_id].energy_price * energy, filled_price=players[player_id].energy_price, expiration_tick=tick, - resource=Energy.energy.value - )) - await Order.create_many(electricity_orders) + resource=Energy.ENERGY.value + ).save(self.pipe) - async def save_tick_data(self, tick_data: TickData): - await Player.update_many(tick_data.players.values()) - await Order.update_many(tick_data.updated_orders.values()) - await TradeDb.create_many(tick_data.tick_trades) + def save_tick_data(self, tick_data: TickData): + list(map(methodcaller('save', self.pipe), tick_data.players.values())) + list(map(methodcaller('save', self.pipe), + tick_data.updated_orders.values())) + list(map(methodcaller('save', self.pipe), tick_data.tick_trades)) - async def save_market_data(self, tick_data: TickData): + def save_market_data(self, tick_data: TickData): tick = tick_data.game.current_tick game_id = tick_data.game.game_id for resource, market in chain( tick_data.markets.items(), - [(Energy.energy.value, tick_data.energy_market)]): + [(Energy.ENERGY.value, tick_data.energy_market)]): price_tracker = market.price_tracker - await Market.create( + Market( game_id=game_id, tick=tick, resource=resource, @@ -292,10 +371,4 @@ async def save_market_data(self, tick_data: TickData): close=price_tracker.get_close(), market=price_tracker.get_average(), volume=price_tracker.get_volume() - ) - - async def run_bots(self, tick_data: TickData): - bots: List[Bot] = self.game_data[tick_data.game.game_id].bots - - for bot in bots: - await bot.run(tick_data) + ).save(self.pipe) diff --git a/backend/logger.py b/backend/logger.py index 83607b1..56598f6 100644 --- a/backend/logger.py +++ b/backend/logger.py @@ -1,10 +1,13 @@ import logging import logging.handlers +from operator import attrgetter +import types +from typing import List, Tuple from config import config logger = logging.getLogger(__name__) -logger = logging.getLogger(__name__) + logger.setLevel(logging.DEBUG) file_formatter = logging.Formatter( @@ -13,8 +16,12 @@ console_handler = logging.StreamHandler() -if config['debug']: - console_handler.setLevel(logging.DEBUG) +if config['log_level']: + try: + console_handler.setLevel(attrgetter(config['log_level'], logging)) + except Exception: + print("Invalid log level, setting log level to info") + console_handler.setLevel(logging.INFO) else: console_handler.setLevel(logging.INFO) console_handler.setFormatter(console_formatter) @@ -24,4 +31,28 @@ file_handler.setFormatter(file_formatter) logger.addHandler(console_handler) -logger.addHandler(file_handler) +# logger.addHandler(file_handler) + +class GameLogger(logging.Logger): + def game_log(self, game_id: str, msg: object, *args, **kwargs): + self.info(f" {game_id} {msg}", *args, **kwargs) + +logger.game_log = types.MethodType(GameLogger.game_log, logger) +logger: GameLogger + + +def get_player_text(x): + player_id, score = x + return f"({player_id}, {score})" + +class ScoreLogger: + def log(self, game_id: str, game_name: str, tick: int | str, scores: List[Tuple[str, int]]): + file_name = f"scores/{game_id}_{game_name}.txt" + try: + with open(file_name, "a+", encoding="utf8") as file: + text = ', '.join(map(get_player_text, scores)) + file.write(f"{tick}, [{text}]\n") + except Exception as e: + logger.warning(f"Error writing score to file {file_name} for tick {tick}", e) + +score_logger = ScoreLogger() \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index d5a0927..231722e 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,23 +1,28 @@ import asyncio +import json import time -from fastapi import FastAPI, Request, Response +from fastapi import FastAPI, Request, Response, WebSocket from fastapi.encoders import jsonable_encoder from fastapi.responses import JSONResponse from contextlib import asynccontextmanager from config import config -from db import database +from db import limiter from game.tick import Ticker -from routers import admin_router, users_router +from routers import users_router, admin_router import psutil import os from logger import logger from docs import tags_metadata, short_description +from redis_om import Migrator + + +Migrator().run() + -# used in integration tests, only when single threaded tick_event = asyncio.Event() -async def background_tasks(): +async def run_game_ticks(): parent_process = psutil.Process(os.getppid()) children = parent_process.children( recursive=True) @@ -36,10 +41,8 @@ async def background_tasks(): @asynccontextmanager async def lifespan(app: FastAPI): - await database.connect() - asyncio.create_task(background_tasks()) + asyncio.create_task(run_game_ticks()) yield - await database.disconnect() app = FastAPI( @@ -48,14 +51,10 @@ async def lifespan(app: FastAPI): description=short_description, openapi_tags=tags_metadata, lifespan=lifespan, - # docs_url=None + # docs_url="https://github.com/x-fer/algotrade2024-docs" #TODO ) -# app.state.limiter = limiter - -# app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) - -# app.add_middleware(SlowAPIMiddleware) +app.state.limiter = limiter @app.exception_handler(Exception) diff --git a/backend/model/__init__.py b/backend/model/__init__.py index 65a2a8c..0b2d61b 100644 --- a/backend/model/__init__.py +++ b/backend/model/__init__.py @@ -1,11 +1,12 @@ from .team import Team from .game import Game -from .player import Player, PowerPlantType +from .player import Player, ResourcesModel, PowerPlantsModel from .datasets import Datasets from .order import Order -from .order_types import * +from .order_types import OrderSide, OrderStatus from .resource import Resource, Energy from .datasets import Datasets from .dataset_data import DatasetData -from .trade import Trade, TradeDb +from .trade import Trade from .market import Market +from .power_plant_type import PowerPlantType \ No newline at end of file diff --git a/backend/model/dataset_data.py b/backend/model/dataset_data.py index 06559b8..7ebd524 100644 --- a/backend/model/dataset_data.py +++ b/backend/model/dataset_data.py @@ -1,51 +1,25 @@ -from dataclasses import dataclass from datetime import datetime +from db.db import get_my_redis_connection +from redis_om import Field, JsonModel -from db.db import database -from db.table import Table +from model.power_plant_model import PowerPlantsModel, ResourcesModel -@dataclass -class DatasetData(Table): - table_name = "dataset_data" - - dataset_data_id: int - dataset_id: int +class DatasetData(JsonModel): + dataset_id: str = Field(index=True) date: datetime - tick: int + tick: int = Field(index=True) - coal: int - uranium: int - biomass: int - gas: int - oil: int - geothermal: int - wind: int - solar: int - hydro: int - coal_price: int - uranium_price: int - biomass_price: int - gas_price: int - oil_price: int energy_demand: int max_energy_price: int - def __getitem__(self, item): - return self.__getattribute__(item.lower()) + resource_prices: ResourcesModel = Field(default_factory=ResourcesModel) + power_plants_output: PowerPlantsModel = Field( + default_factory=PowerPlantsModel) - @classmethod - async def list_by_game_id_where_tick(cls, dataset_id, game_id, min_tick, max_tick): - query = f""" - SELECT dataset_data.* FROM {cls.table_name} - JOIN games ON dataset_data.dataset_id = games.dataset_id - WHERE dataset_data.dataset_id=:dataset_id AND game_id=:game_id AND tick BETWEEN :min_tick AND :max_tick - ORDER BY tick - """ - values = {"dataset_id": dataset_id, - "game_id": game_id, - "min_tick": min_tick, - "max_tick": max_tick} - result = await database.fetch_all(query, values) + @property + def dataset_data_id(self) -> str: + return self.pk - return [cls(**x) for x in result] + class Meta: + database = get_my_redis_connection() diff --git a/backend/model/datasets.py b/backend/model/datasets.py index ba3193b..868f23a 100644 --- a/backend/model/datasets.py +++ b/backend/model/datasets.py @@ -1,22 +1,27 @@ -from db.table import Table -from dataclasses import dataclass +from pprint import pprint from fastapi import HTTPException -from model.dataset_data import DatasetData +from db.db import get_my_redis_connection +from redis_om import Field, JsonModel +from model.dataset_data import DatasetData -@dataclass -class Datasets(Table): - table_name = "datasets" - dataset_id: int - dataset_name: str +class Datasets(JsonModel): + dataset_name: str = Field(index=True) dataset_description: str - @classmethod - async def validate_ticks(cls, dataset_id, min_ticks): - rows = await DatasetData.count(dataset_id=dataset_id) + dataset_id: str = Field(index=True, default=None) + + def __init__(self, **data): + super().__init__(**data) + self.dataset_id = self.pk - if rows < min_ticks: - raise HTTPException(400, "Dataset does not have enough ticks") + @staticmethod + def validate_ticks(dataset_id: str, total_ticks: int): + if Datasets.find(Datasets.pk == dataset_id).count() == 0: + raise HTTPException(400, "Dataset not found") + if DatasetData.find(DatasetData.dataset_id == dataset_id).count() < total_ticks: + raise HTTPException(400, "Not enough ticks in dataset") - return dataset_id + class Meta: + database = get_my_redis_connection() diff --git a/backend/model/game.py b/backend/model/game.py index 269eb09..9f455c5 100644 --- a/backend/model/game.py +++ b/backend/model/game.py @@ -1,17 +1,25 @@ -from dataclasses import dataclass, field -from db.table import Table + +from typing import Any, Optional +from db.db import get_my_redis_connection +from redis_om import Field, JsonModel from datetime import datetime -@dataclass -class Game(Table): - table_name = "games" - game_id: int +class Game(JsonModel): game_name: str - is_contest: bool - dataset_id: int - start_time: datetime + is_contest: int = Field(index=True) + dataset_id: str + start_time: datetime = Field(index=False) total_ticks: int tick_time: int - current_tick: int = field(default=0) - is_finished: bool = field(default=False) + current_tick: int = Field(default=0) + is_finished: bool = Field(index=False, default=False) + + game_id: str = Field(default=None) + + def __init__(self, **data): + super().__init__(**data) + self.game_id=self.pk + + class Meta: + database = get_my_redis_connection() diff --git a/backend/model/market.py b/backend/model/market.py index ce489fa..bb8aa89 100644 --- a/backend/model/market.py +++ b/backend/model/market.py @@ -1,15 +1,12 @@ -from dataclasses import dataclass -from db.table import Table +from db.db import get_my_redis_connection from model.enum_type import get_enum from .resource import Resource, Energy -from db import database +from redis_om import JsonModel, Field -@dataclass -class Market(Table): - table_name = "market" - game_id: int - tick: int +class Market(JsonModel): + game_id: str = Field(index=True) + tick: int = Field(index=True) resource: Resource | Energy low: int high: int @@ -18,29 +15,5 @@ class Market(Table): market: int volume: int - def __post_init__(self): - self.resource = get_enum(self.resource, Resource, Energy) - - @classmethod - async def create(cls, *args, **kwargs) -> int: - """ - Input: Values for new row - Returns id of created row - """ - return await super().create(*args, col_nums=0, **kwargs) - - @classmethod - async def list_by_game_id_where_tick(cls, game_id, min_tick, max_tick, resource: Resource | Energy = None): - resource_query = "" if resource is None else " AND resource=:resource" - query = f""" - SELECT * FROM {cls.table_name} - WHERE game_id=:game_id AND tick BETWEEN :min_tick AND :max_tick{resource_query} - ORDER BY tick - """ - values = {"game_id": game_id, - "min_tick": min_tick, - "max_tick": max_tick} - if resource is not None: - values["resource"] = resource.value - result = await database.fetch_all(query, values) - return [cls(**game) for game in result] + class Meta: + database = get_my_redis_connection() \ No newline at end of file diff --git a/backend/model/order.py b/backend/model/order.py index ccc3839..f89e31e 100644 --- a/backend/model/order.py +++ b/backend/model/order.py @@ -1,166 +1,52 @@ -from dataclasses import dataclass, field from datetime import datetime +from typing import Union -from db.db import database -from db.table import Table +from db.db import get_my_redis_connection from model.enum_type import get_enum -from .order_types import OrderSide, OrderStatus, OrderType -from .resource import Energy, Resource +from .order_types import OrderSide, OrderStatus +from .resource import Energy, Resource, ResourceOrEnergy +from redis_om import Field, JsonModel -@dataclass -class Order(Table): - table_name = "orders" - - order_id: int - game_id: int - player_id: int +class Order(JsonModel): + game_id: str = Field(index=True) + player_id: str = Field(index=True) price: int size: int - tick: int + tick: int = Field(index=True) timestamp: datetime - resource: Resource | Energy + resource: ResourceOrEnergy = Field(index=True) order_side: OrderSide - order_type: OrderType = field(default=OrderType.LIMIT) - order_status: OrderStatus = field(default=OrderStatus.PENDING) + order_status: OrderStatus = Field(index=True, default=OrderStatus.PENDING.value) + + filled_size: int = Field(index=False, default=0) + filled_money: int = Field(index=False, default=0) + filled_price: float = Field(index=False, default=0) - filled_size: int = field(default=0) - filled_money: int = field(default=0) - filled_price: float = field(default=0) + expiration_tick: int = Field(index=False, default=0) - expiration_tick: int = field(default=0) + order_id: str = Field(default=None) - def __post_init__(self): - self.order_side = get_enum(self.order_side, OrderSide) - self.order_type = get_enum(self.order_type, OrderType) - self.order_status = get_enum(self.order_status, OrderStatus) - self.resource = get_enum(self.resource, Resource, Energy) + def __init__(self, **data): + super().__init__(**data) + self.order_id=self.pk def __hash__(self) -> int: - return hash(self.order_id) + return hash(self.pk) def __eq__(self, other) -> bool: if not isinstance(other, type(self)): - return NotImplemented # pragma: no cover - return self.order_id == other.order_id and self.timestamp == other.timestamp - - def __lt__(self, other): # pragma: no cover - return self.timestamp < other.timestamp # pragma: no cover - - @classmethod - async def cancel_player_orders(cls, player_id): - query = f""" - UPDATE {cls.table_name} - SET order_status=:new_order_status - WHERE player_id=:player_id - AND order_status=:order_status - """ - values = { - "player_id": player_id, - "new_order_status": OrderStatus.USER_CANCELLED.value, - "order_status": OrderStatus.ACTIVE.value, - } - await database.fetch_val(query, values) - values = { - "player_id": player_id, - "new_order_status": OrderStatus.PENDING.value, - "order_status": OrderStatus.CANCELLED.value, - } - await database.fetch_val(query, values) - - @classmethod - async def count_player_orders(cls, game_id, player_id, resource: Resource): - query = f""" - SELECT COUNT(*) FROM {cls.table_name} - WHERE game_id=:game_id - AND player_id=:player_id - AND (order_status='ACTIVE' - OR order_status='PENDING' - OR order_status='IN_QUEUE') - AND resource=:resource - """ - values = { - "game_id": game_id, - "player_id": player_id, - "resource": resource.value, - } - result = await database.execute(query, values) - return result + return ValueError("Not implemented") + return self.pk == other.pk - @classmethod - async def list_orders_by_game_id(cls, game_id): - query = f""" - SELECT orders.* FROM {cls.table_name} - JOIN players ON orders.player_id = players.player_id - WHERE orders.game_id=:game_id - AND ( - orders.order_status='ACTIVE' - OR ( - orders.order_status='PENDING' - AND players.is_bot IS TRUE - )) - """ - values = {"game_id": game_id} - result = await database.fetch_all(query, values) - return [cls(**x) for x in result] - - @classmethod - async def list_bot_orders_by_game_id(cls, game_id): - query = f""" - SELECT orders.* FROM {cls.table_name} - JOIN players ON orders.player_id = players.player_id - WHERE orders.game_id=:game_id - AND players.is_bot IS TRUE - AND (orders.order_status='ACTIVE' - OR orders.order_status='PENDING') - """ - values = {"game_id": game_id} - result = await database.fetch_all(query, values) - return [cls(**x) for x in result] - - @classmethod - async def delete_bot_orders(cls, game_id): - query = f""" - DELETE FROM {cls.table_name} - USING players - WHERE orders.player_id = players.player_id - AND orders.game_id=:game_id - AND players.is_bot IS TRUE - AND (orders.order_status='PENDING' - OR orders.order_status='ACTIVE') - """ - values = {"game_id": game_id} - await database.execute(query, values) - - @classmethod - async def list_best_orders_by_game_id(cls, game_id, order_side: OrderSide): - best_orders = [] - for resource in Resource: - asc_desc = "ASC" if order_side == OrderSide.BUY else "DESC" - query = f""" - SELECT orders.* FROM {cls.table_name} - JOIN players ON orders.player_id = players.player_id - WHERE orders.game_id=:game_id - AND (orders.order_status='ACTIVE' - OR ( - orders.order_status='PENDING' - AND players.is_bot IS TRUE - )) - AND order_side=:order_side - AND resource=:resource - ORDER BY price {asc_desc}, size - filled_size DESC - LIMIT 1 - """ - values = { - "game_id": game_id, - "order_side": order_side.value, - "resource": resource.value, - } - result = await database.fetch_all(query, values) - best_orders.extend(result) + def __lt__(self, other): + if not isinstance(other, type(self)): + return ValueError("Wrong type") + return self.timestamp < other.timestamp - return [cls(**x) for x in best_orders] + class Meta: + database = get_my_redis_connection() \ No newline at end of file diff --git a/backend/model/order_types.py b/backend/model/order_types.py index e8789f8..f1b7df0 100644 --- a/backend/model/order_types.py +++ b/backend/model/order_types.py @@ -1,22 +1,17 @@ from enum import Enum -class OrderType(Enum): - LIMIT = "LIMIT" - MARKET = "MARKET" - - class OrderSide(Enum): - BUY = "BUY" - SELL = "SELL" + BUY = "buy" + SELL = "sell" class OrderStatus(Enum): - PENDING = "PENDING" - IN_QUEUE = "IN_QUEUE" - ACTIVE = "ACTIVE" - COMPLETED = "COMPLETED" - CANCELLED = "CANCELLED" - EXPIRED = "EXPIRED" - REJECTED = "REJECTED" - USER_CANCELLED = "USER_CANCELLED" + PENDING = "pending" + IN_QUEUE = "in_queue" + ACTIVE = "active" + COMPLETED = "completed" + CANCELLED = "cancelled" + EXPIRED = "expired" + REJECTED = "rejected" + USER_CANCELLED = "user_cancelled" diff --git a/backend/model/player.py b/backend/model/player.py index a171631..c3ee73b 100644 --- a/backend/model/player.py +++ b/backend/model/player.py @@ -1,136 +1,83 @@ -from dataclasses import dataclass, field -from db.table import Table -from enum import Enum -from config import config -from model.resource import Resource +from db.db import get_my_redis_connection from model.dataset_data import DatasetData +from model.game import Game +from model.order import Order +from model.power_plant_model import PowerPlantsModel, ResourcesModel +from pydantic import BaseModel +import pydantic +from redis_om import Field, JsonModel +from redlock.lock import RedLock + +from model.power_plant_type import PowerPlantType +from model.resource import Resource -class PowerPlantType(str, Enum): - COAL = "COAL" - URANIUM = "URANIUM" - BIOMASS = "BIOMASS" - GAS = "GAS" - OIL = "OIL" - GEOTHERMAL = "GEOTHERMAL" - WIND = "WIND" - SOLAR = "SOLAR" - HYDRO = "HYDRO" - - def get_name(self): - return self.name.lower() - - def get_base_price(self): - return config["power_plant"]["base_prices"][self.get_name()] +class Networth(BaseModel): + total: int = pydantic.Field(0, description="Total players networth. This is your score in competition rounds!") + money: int = pydantic.Field(0) + resources: ResourcesModel = pydantic.Field(default_factory=ResourcesModel, description="Resources owned by the player") + resources_value: ResourcesModel = pydantic.Field(default_factory=ResourcesModel, description="Players networth based on resources prices on the market") + power_plants_owned: PowerPlantsModel = pydantic.Field(default_factory=PowerPlantsModel, description="Power plants owned by the player") + power_plants_value: PowerPlantsModel = pydantic.Field(default_factory=PowerPlantsModel, description="Players networth based only on power plants sell prices") - def get_plant_price(self, power_plant_count: int): - return int(self.get_base_price() * (1 + 0.03 * (power_plant_count ** 2) + 0.1 * power_plant_count)) - def get_sell_price(self, power_plant_count: int): - plant_price = self.get_plant_price(power_plant_count - 1) - sell_plant_price = round( - plant_price * config["power_plant"]["sell_coeff"]) +class Player(JsonModel): + player_name: str + game_id: str = Field(index=True) + team_id: str = Field(index=True) + is_active: int = Field(default=int(True), index=True) + is_bot: int = Field(default=int(False), index=True) - return sell_plant_price + energy_price: int = Field(default=1e9) - def get_produced_energy(self, dataset_row: dict): - return dataset_row[self.get_name()] + money: int = Field(default=0) + energy: int = Field(default=0) - def is_renewable(self): - name = self.get_name() - return False if name in ["coal", "uranium", "biomass", "gas", "oil"] else True + resources: ResourcesModel = Field(default_factory=ResourcesModel) + power_plants_owned: PowerPlantsModel = Field(default_factory=PowerPlantsModel) + power_plants_powered: PowerPlantsModel = Field(default_factory=PowerPlantsModel) -@dataclass -class Player(Table): - table_name = "players" - player_id: int - player_name: str - game_id: int - team_id: int - is_active: bool = field(default=True) - is_bot: bool = field(default=False) - - energy_price: int = field(default=1e9) - - money: int = field(default=0) - energy: int = field(default=0) - - coal: int = field(default=0) - uranium: int = field(default=0) - biomass: int = field(default=0) - gas: int = field(default=0) - oil: int = field(default=0) - - coal_plants_owned: int = field(default=0) - uranium_plants_owned: int = field(default=0) - biomass_plants_owned: int = field(default=0) - gas_plants_owned: int = field(default=0) - oil_plants_owned: int = field(default=0) - geothermal_plants_owned: int = field(default=0) - wind_plants_owned: int = field(default=0) - solar_plants_owned: int = field(default=0) - hydro_plants_owned: int = field(default=0) - - coal_plants_powered: int = field(default=0) - uranium_plants_powered: int = field(default=0) - biomass_plants_powered: int = field(default=0) - gas_plants_powered: int = field(default=0) - oil_plants_powered: int = field(default=0) - geothermal_plants_powered: int = field(default=0) - wind_plants_powered: int = field(default=0) - solar_plants_powered: int = field(default=0) - hydro_plants_powered: int = field(default=0) - - def __getitem__(self, key): - if isinstance(key, Resource): - return self.__getattribute__(key.name) - return self.__getattribute__(key) - - def __setitem__(self, key, value): - if isinstance(key, Resource): - return self.__setattr__(key.name, value) - self.__setattr__(key, value) - - async def get_networth(self, game): - net_worth = { - "plants_owned": {}, - "money": self.money, - "resources": {}, - "total": 0 - } + player_id: str = Field(default=None) - for type in PowerPlantType: - value = 0 + def __init__(self, **data): + super().__init__(**data) + self.player_id=self.pk - for i in range(1, getattr(self, f"{type.lower()}_plants_owned") + 1): - value += type.get_sell_price(i) + def lock(self, *args): + return RedLock(self.pk, *args) - net_worth["plants_owned"][type.lower()] = { - "owned": getattr(self, f"{type.lower()}_plants_owned"), - "value_if_sold": value - } - - data = (await DatasetData.list_by_game_id_where_tick( - game.dataset_id, game.game_id, game.current_tick - 1, game.current_tick - 1))[0] + def cancel_orders(self, pipe=None) -> int: + """Returns number of canceled orders""" + return Order.delete_many(Order.find(Order.player_id == self.player_id).all(), pipe) + def get_networth(self, game: Game, dataset_data: DatasetData = None) -> Networth: + # TODO: nije pod lockom jer bi kocilo tick, a ovo stalno uzimaju igraci + if dataset_data is None: + dataset_data = DatasetData.find( + DatasetData.tick==game.current_tick, + DatasetData.dataset_id==game.dataset_id + ).first() + total = self.money + resources_value = ResourcesModel() for resource in Resource: - final_price = data[f"{resource.name.lower()}_price"] - has = getattr(self, resource.name.lower()) - - net_worth["resources"][resource.name.lower()] = { - "final_price": final_price, - "player_has": has, - "value": final_price * has - } - - net_worth["total"] += self.money + resource_value = dataset_data.resource_prices[resource] * self.resources[resource] + resources_value[resource] = resource_value + total += resource_value + power_plants_value = PowerPlantsModel() for type in PowerPlantType: - net_worth["total"] += net_worth["plants_owned"][type.lower() - ]["value_if_sold"] - - for resource in Resource: - net_worth["total"] += net_worth["resources"][resource.name.lower() - ]["value"] - - return net_worth + for i in range(1, self.power_plants_owned[type] + 1): + power_plants_value[type] += PowerPlantType.get_sell_price() + total += power_plants_value[type] + + return Networth( + total = total, + money = self.money, + resources = self.resources, + resources_value = resources_value, + power_plants_owned = self.power_plants_owned, + power_plants_value = power_plants_value, + ) + + class Meta: + database = get_my_redis_connection() diff --git a/backend/model/power_plant_model.py b/backend/model/power_plant_model.py new file mode 100644 index 0000000..6cc2159 --- /dev/null +++ b/backend/model/power_plant_model.py @@ -0,0 +1,41 @@ +from enum import Enum +from db.db import get_my_redis_connection +from redis_om import EmbeddedJsonModel, Field + + +class EnumGetterSettr: + def __getitem__(self, key): + if isinstance(key, Enum): + return self.__getattribute__(key.value) + return self.__getattribute__(key) + + def __setitem__(self, key, value): + if isinstance(key, Enum): + return self.__setattr__(key.value, value) + self.__setattr__(key, value) + + +class ResourcesModel(EmbeddedJsonModel, EnumGetterSettr): + coal: int = Field(index=False, default=0) + uranium: int = Field(index=False, default=0) + biomass: int = Field(index=False, default=0) + gas: int = Field(index=False, default=0) + oil: int = Field(index=False, default=0) + + class Meta: + database = get_my_redis_connection() + + +class PowerPlantsModel(EmbeddedJsonModel, EnumGetterSettr): + coal: int = Field(index=False, default=0) + uranium: int = Field(index=False, default=0) + biomass: int = Field(index=False, default=0) + gas: int = Field(index=False, default=0) + oil: int = Field(index=False, default=0) + geothermal: int = Field(index=False, default=0) + wind: int = Field(index=False, default=0) + solar: int = Field(index=False, default=0) + hydro: int = Field(index=False, default=0) + + class Meta: + database = get_my_redis_connection() diff --git a/backend/model/power_plant_type.py b/backend/model/power_plant_type.py new file mode 100644 index 0000000..746a78f --- /dev/null +++ b/backend/model/power_plant_type.py @@ -0,0 +1,37 @@ +from enum import Enum + +from config import config + + +class PowerPlantType(str, Enum): + COAL = "coal" + URANIUM = "uranium" + BIOMASS = "biomass" + GAS = "gas" + OIL = "oil" + GEOTHERMAL = "geothermal" + WIND = "wind" + SOLAR = "solar" + HYDRO = "hydro" + + def _get_config_name(self): + return self.name.lower() + + def get_base_price(self): + return config["power_plant"]["base_prices"][self._get_config_name()] + + def get_plant_price(self, power_plant_count: int): + return int( + self.get_base_price() + * (1 + 0.03 * (power_plant_count**2) + 0.1 * power_plant_count) + ) + + def get_sell_price(self, power_plant_count: int): + plant_price = self.get_plant_price(power_plant_count - 1) + sell_plant_price = round(plant_price * config["power_plant"]["sell_coeff"]) + + return sell_plant_price + + def is_renewable(self): + name = self._get_config_name() + return False if name in ["coal", "uranium", "biomass", "gas", "oil"] else True diff --git a/backend/model/resource.py b/backend/model/resource.py index 72bd91d..3fd1124 100644 --- a/backend/model/resource.py +++ b/backend/model/resource.py @@ -2,11 +2,30 @@ class Resource(Enum): - coal = "COAL" - uranium = "URANIUM" - biomass = "BIOMASS" - gas = "GAS" - oil = "OIL" + COAL = "coal" + URANIUM = "uranium" + BIOMASS = "biomass" + GAS = "gas" + OIL = "oil" + class Energy(Enum): - energy = "ENERGY" + ENERGY = "energy" + + +class ResourceOrEnergy(Enum): + COAL = "coal" + URANIUM = "uranium" + BIOMASS = "biomass" + GAS = "gas" + OIL = "oil" + ENERGY = "energy" + + def __eq__(self, other) -> bool: + if ( + isinstance(other, Resource) + or isinstance(other, Energy) + or isinstance(other, ResourceOrEnergy) + ): + return self.value == other.value + return False diff --git a/backend/model/team.py b/backend/model/team.py index e9d1287..516d304 100644 --- a/backend/model/team.py +++ b/backend/model/team.py @@ -1,10 +1,19 @@ -from dataclasses import dataclass -from db.table import Table +from db.db import get_my_redis_connection +from redis_om import Field, JsonModel +from redlock.lock import RedLock -@dataclass -class Team(Table): - table_name = "teams" - team_id: int - team_name: str - team_secret: str +class Team(JsonModel): + team_name: str = Field(index=True) + team_secret: str = Field(index=True) + team_id: str = Field(default=None) + + def __init__(self, **data): + super().__init__(**data) + self.team_id = self.pk + + def lock(self, *args): + return RedLock(self.pk, *args) + + class Meta: + database = get_my_redis_connection() diff --git a/backend/model/test_datasets.py b/backend/model/test_datasets.py deleted file mode 100644 index 3842911..0000000 --- a/backend/model/test_datasets.py +++ /dev/null @@ -1,35 +0,0 @@ -import pytest -from unittest.mock import AsyncMock, patch -from model import Datasets -from model.dataset_data import DatasetData -from fastapi import HTTPException - -# @classmethod -# async def ensure_ticks(cls, dataset_id, min_ticks): - -# row = await DatasetData.list(dataset_id=dataset_id) - -# if len(row) < min_ticks: -# raise Exception("Dataset does not have enough ticks") - -# return dataset_id - - -@pytest.mark.asyncio -async def test_ensure_ticks_enough_ticks(): - dataset_id = 1 - min_ticks = 10 - with patch.object(DatasetData, 'count', AsyncMock(return_value=11)): - result = await Datasets.validate_ticks(dataset_id, min_ticks) - - assert result == dataset_id - - -@pytest.mark.asyncio -async def test_ensure_ticks_not_enough_ticks(): - dataset_id = 1 - min_ticks = 10 - with patch.object(DatasetData, 'count', AsyncMock(return_value=9)): - with pytest.raises(HTTPException) as e: - await Datasets.validate_ticks(dataset_id, min_ticks) - assert e.value.status_code == 400 diff --git a/backend/model/test_datasets_data.py b/backend/model/test_datasets_data.py deleted file mode 100644 index 9bc44f8..0000000 --- a/backend/model/test_datasets_data.py +++ /dev/null @@ -1,70 +0,0 @@ -import pytest -from model import DatasetData - - -@pytest.fixture -def sample_dataset_data(): - return DatasetData( - dataset_data_id=1, - dataset_id=1, - date="2024-02-16", - tick=1, - coal=100, - uranium=50, - biomass=30, - gas=80, - oil=70, - geothermal=20, - wind=40, - solar=60, - hydro=25, - coal_price=100, - uranium_price=50, - biomass_price=30, - gas_price=80, - oil_price=70, - energy_demand=300, - max_energy_price=70 - ) - - -def test_dataset_data_initialization(sample_dataset_data): - assert sample_dataset_data.dataset_data_id == 1 - assert sample_dataset_data.dataset_id == 1 - assert sample_dataset_data.date == "2024-02-16" - assert sample_dataset_data.tick == 1 - assert sample_dataset_data.coal == 100 - assert sample_dataset_data.uranium == 50 - assert sample_dataset_data.biomass == 30 - assert sample_dataset_data.gas == 80 - assert sample_dataset_data.oil == 70 - assert sample_dataset_data.geothermal == 20 - assert sample_dataset_data.wind == 40 - assert sample_dataset_data.solar == 60 - assert sample_dataset_data.hydro == 25 - assert sample_dataset_data.energy_demand == 300 - assert sample_dataset_data.max_energy_price == 70 - assert sample_dataset_data.coal_price == 100 - assert sample_dataset_data.uranium_price == 50 - assert sample_dataset_data.biomass_price == 30 - assert sample_dataset_data.gas_price == 80 - assert sample_dataset_data.oil_price == 70 - - -def test_dataset_data_indexing(sample_dataset_data): - assert sample_dataset_data['Coal'] == 100 - assert sample_dataset_data['uranium'] == 50 - assert sample_dataset_data['biomass'] == 30 - assert sample_dataset_data['gas'] == 80 - assert sample_dataset_data['Oil'] == 70 - assert sample_dataset_data['geothermal'] == 20 - assert sample_dataset_data['Wind'] == 40 - assert sample_dataset_data['SOLAR'] == 60 - assert sample_dataset_data['Hydro'] == 25 - assert sample_dataset_data['energy_demand'] == 300 - assert sample_dataset_data['max_energy_price'] == 70 - assert sample_dataset_data['coal_price'] == 100 - assert sample_dataset_data['uranium_price'] == 50 - assert sample_dataset_data['biomass_price'] == 30 - assert sample_dataset_data['gas_price'] == 80 - assert sample_dataset_data['oil_price'] == 70 diff --git a/backend/model/test_enum_type.py b/backend/model/test_enum_type.py deleted file mode 100644 index 464e930..0000000 --- a/backend/model/test_enum_type.py +++ /dev/null @@ -1,34 +0,0 @@ -import pytest -from enum import Enum - -from model.enum_type import get_enum - - -class ExampleEnum(Enum): - VALUE1 = "VALUE1" - VALUE2 = "VALUE2" - - -class AnotherEnum(Enum): - OPTION1 = 'Option 1' - OPTION2 = 'Option 2' - - -def test_enum_type_default(): - instance = get_enum("VALUE1", ExampleEnum) - - assert instance == ExampleEnum.VALUE1 - - instance = get_enum("Option 1", ExampleEnum, AnotherEnum) - - assert instance == AnotherEnum.OPTION1 - - instance = get_enum(ExampleEnum.VALUE1, ExampleEnum) - - assert instance == ExampleEnum.VALUE1 - - with pytest.raises(ValueError): - instance = get_enum(AnotherEnum.OPTION1, ExampleEnum) - - with pytest.raises(ValueError): - instance = get_enum("blabla", ExampleEnum) \ No newline at end of file diff --git a/backend/model/test_model.py b/backend/model/test_model.py new file mode 100644 index 0000000..aab480f --- /dev/null +++ b/backend/model/test_model.py @@ -0,0 +1,27 @@ +from datetime import datetime +from model.order import Order +from model.order_types import OrderSide +from model.resource import Resource + + +def test_order_equal(): + order = Order( + game_id = "1", + player_id = "1", + price = 1, + size = 1, + tick = 1, + timestamp = datetime.now(), + resource = Resource.COAL.value, + order_side = OrderSide.BUY.value, + ) + + assert order.resource == Resource.COAL + + order.resource = Resource.OIL + + assert order.resource == Resource.OIL + + order.resource = Resource.BIOMASS + + assert order.resource == Resource.BIOMASS \ No newline at end of file diff --git a/backend/model/test_order.py b/backend/model/test_order.py deleted file mode 100644 index 0b6728b..0000000 --- a/backend/model/test_order.py +++ /dev/null @@ -1,55 +0,0 @@ -from datetime import datetime - -import pytest - -from model import Order, OrderSide, OrderStatus, OrderType, Resource - - -@pytest.fixture -def sample_order(): - return Order( - order_id=1, - game_id=1, - player_id=1, - price=50, - size=100, - tick=1, - timestamp=datetime.now(), - order_side=OrderSide.BUY, - order_type=OrderType.LIMIT, - order_status=OrderStatus.PENDING, - filled_size=0, - filled_money=0, - filled_price=0, - expiration_tick=1, - resource=Resource.coal - ) - - -def test_order_initialization(sample_order): - assert sample_order.order_id == 1 - assert sample_order.game_id == 1 - assert sample_order.player_id == 1 - assert sample_order.price == 50 - assert sample_order.size == 100 - assert sample_order.tick == 1 - assert isinstance(sample_order.timestamp, datetime) - assert sample_order.order_side == OrderSide.BUY - assert sample_order.order_type == OrderType.LIMIT - assert sample_order.order_status == OrderStatus.PENDING - assert sample_order.filled_size == 0 - assert sample_order.filled_money == 0 - assert sample_order.filled_price == 0 - assert sample_order.expiration_tick == 1 - assert sample_order.resource == Resource.coal - - -def test_order_comparison(sample_order): - order_1 = sample_order - order_2 = sample_order - assert order_1 == order_2 - - -def test_order_hashing(sample_order): - order_set = {sample_order} - assert sample_order in order_set diff --git a/backend/model/test_player.py b/backend/model/test_player.py deleted file mode 100644 index 67e6446..0000000 --- a/backend/model/test_player.py +++ /dev/null @@ -1,70 +0,0 @@ -import pytest -from model import Player - - -@pytest.fixture -def sample_player(): - return Player( - player_id=1, - player_name="John", - game_id=1, - team_id=1 - ) - - -def test_player_initialization(sample_player): - assert sample_player.player_id == 1 - assert sample_player.player_name == "John" - assert sample_player.game_id == 1 - assert sample_player.team_id == 1 - assert sample_player.is_active is True - assert sample_player.is_bot is False - assert sample_player.energy_price == 1e9 - assert sample_player.money == 0 - assert sample_player.energy == 0 - assert sample_player.coal == 0 - assert sample_player.uranium == 0 - assert sample_player.biomass == 0 - assert sample_player.gas == 0 - assert sample_player.oil == 0 - - -def test_player_indexing(sample_player): - sample_player['money'] = 100 - sample_player['energy'] = 50 - sample_player['coal'] = 20 - sample_player['uranium'] = 10 - sample_player['biomass'] = 5 - sample_player['gas'] = 30 - sample_player['oil'] = 25 - - assert sample_player.money == 100 - assert sample_player.energy == 50 - assert sample_player.coal == 20 - assert sample_player.uranium == 10 - assert sample_player.biomass == 5 - assert sample_player.gas == 30 - assert sample_player.oil == 25 - - -def test_player_getitem(sample_player): - sample_player['money'] = 100 - assert sample_player['money'] == 100 - - sample_player['energy'] = 50 - assert sample_player['energy'] == 50 - - sample_player['coal'] = 20 - assert sample_player['coal'] == 20 - - sample_player['uranium'] = 10 - assert sample_player['uranium'] == 10 - - sample_player['biomass'] = 5 - assert sample_player['biomass'] == 5 - - sample_player['gas'] = 30 - assert sample_player['gas'] == 30 - - sample_player['oil'] = 25 - assert sample_player['oil'] == 25 diff --git a/backend/model/test_power_plant_type.py b/backend/model/test_power_plant_type.py deleted file mode 100644 index d3c739c..0000000 --- a/backend/model/test_power_plant_type.py +++ /dev/null @@ -1,31 +0,0 @@ -import pytest -from model.player import PowerPlantType -from config import config - - -def test_get_name(): - assert PowerPlantType.COAL.get_name() == 'coal' - - -def test_get_base_price(): - assert PowerPlantType.COAL.get_base_price( - ) == config["power_plant"]["base_prices"]["coal"] - - -def test_get_plant_price(): - assert PowerPlantType.COAL.get_plant_price(2) >\ - PowerPlantType.COAL.get_plant_price(0) - assert PowerPlantType.COAL.get_plant_price(0) ==\ - PowerPlantType.COAL.get_base_price() - - -def test_get_produced_energy(): - # Mocking dataset_row - dataset_row = {'coal': 10, 'uranium': 20, 'biomass': 30} - assert PowerPlantType.COAL.get_produced_energy( - dataset_row) == dataset_row["coal"] - - -def test_is_renewable(): - assert PowerPlantType.COAL.is_renewable() == False - assert PowerPlantType.SOLAR.is_renewable() == True diff --git a/backend/model/trade.py b/backend/model/trade.py index d05694c..1b655e8 100644 --- a/backend/model/trade.py +++ b/backend/model/trade.py @@ -1,76 +1,31 @@ -from dataclasses import dataclass +from typing import Optional +from redis_om import JsonModel, Field -from db import Table, database +from db.db import get_my_redis_connection +from model.order import Order +from model.resource import ResourceOrEnergy -from .order import Order +class Trade(JsonModel): + tick: int = Field(index=True) -@dataclass -class Trade: - buy_order: Order - sell_order: Order - tick: int + total_money: int + trade_size: int + trade_price: int - filled_money: int - filled_size: int - filled_price: int + resource: ResourceOrEnergy = Field(index=True, default=None) + buy_order_id: str = Field(index=True, default=None) + sell_order_id: str = Field(index=True, default=None) -@dataclass -class TradeDb(Table): - table_name = "trades" - - trade_id: int - buy_order_id: int - sell_order_id: int - tick: int - - filled_money: int - filled_size: int - filled_price: int - - @staticmethod - def from_trade(trade: Trade): - return TradeDb( - trade_id=0, - buy_order_id=trade.buy_order.order_id, - sell_order_id=trade.sell_order.order_id, - tick=trade.tick, - filled_money=trade.filled_money, - filled_price=trade.filled_price, - filled_size=trade.filled_size - ) - - @classmethod - async def _list_trades_by_player_id( - cls, player_id, min_tick, max_tick, side_col, resource=None): - resource_query = "" if resource is None else " AND trades.resource=:resource" - query = f""" - SELECT trades.* FROM trades - JOIN orders ON trades.{side_col}=orders.order_id - WHERE orders.player_id=:player_id - AND trades.tick BETWEEN :min_tick AND :max_tick - {resource_query} - ORDER BY trades.tick - """ - values = {"player_id": player_id, - "min_tick": min_tick, - "max_tick": max_tick} - if resource is not None: - values["resource"] = resource.value - result = await database.fetch_all(query, values) - return [cls(**trade) for trade in result] - - @staticmethod - async def list_buy_trades_by_player_id( - player_id, min_tick, max_tick, resource=None): - return await TradeDb._list_trades_by_player_id( - player_id=player_id, min_tick=min_tick, max_tick=max_tick, - side_col="buy_order_id", resource=resource) - - @staticmethod - async def list_sell_trades_by_player_id( - player_id, min_tick, max_tick, resource=None): - return await TradeDb._list_trades_by_player_id( - player_id=player_id, min_tick=min_tick, max_tick=max_tick, - side_col="sell_order_id", resource=resource) + def __post_init__(self): + if hasattr(self, "buy_order"): + assert hasattr(self, "sell_order") + self.buy_order: Order + self.sell_order: Order + self.buy_order_id = self.buy_order.pk + self.buy_order_id = self.buy_order.pk + assert self.buy_order.resource == self.sell_order.resource + self.resource = self.buy_order.resource.value + class Meta: + database = get_my_redis_connection() \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index aefe170..e3524cf 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -12,7 +12,9 @@ asyncpg==0.29.0 uvicorn==0.26.0 coverage==7.4.0 coredis==4.16.0 -redis==5.0.1 slowapi==0.1.9 httpx==0.26.0 websockets==12.0 +redis-om==0.2.1 +pyinstrument==4.6.2 +redlock==1.2.0 \ No newline at end of file diff --git a/backend/routers/admin/admin.py b/backend/routers/admin/admin.py index d365743..eb9b92d 100644 --- a/backend/routers/admin/admin.py +++ b/backend/routers/admin/admin.py @@ -1,8 +1,8 @@ from fastapi import APIRouter, Depends, HTTPException, Query -from db import migration, limiter -from . import dataset, team, game, player +from db import limiter from config import config from routers.model import SuccessfulResponse +from . import dataset, team, game, player def admin_dep(admin_secret: str = Query(description="Admin secret", default=None)): @@ -15,18 +15,12 @@ def admin_dep(admin_secret: str = Query(description="Admin secret", default=None router = APIRouter(dependencies=[Depends(admin_dep)], include_in_schema=False) -@router.get("/migrate") -@limiter.exempt -async def migrate() -> SuccessfulResponse: - await migration.drop_tables() - await migration.run_migrations() - return SuccessfulResponse() - - -@router.get("/tick_all_games") -@limiter.exempt -async def tick_all_games() -> SuccessfulResponse: - return SuccessfulResponse() +# @router.get("/migrate") +# @limiter.exempt +# async def migrate() -> SuccessfulResponse: +# await migration.drop_tables() +# await migration.run_migrations() +# return SuccessfulResponse() router.include_router(player.router) diff --git a/backend/routers/admin/dataset.py b/backend/routers/admin/dataset.py index 8507ae1..25e25aa 100644 --- a/backend/routers/admin/dataset.py +++ b/backend/routers/admin/dataset.py @@ -4,7 +4,7 @@ from model import DatasetData from db import limiter -from model.datasets import Datasets +from model import Datasets, DatasetData router = APIRouter() @@ -12,9 +12,9 @@ @router.get("/dataset/list") @limiter.exempt -async def dataset_list() -> List[Datasets]: +def dataset_list() -> List[Datasets]: return [ - {**asdict(x), - "max_ticks": await DatasetData.count(dataset_id=x.dataset_id) - } for x in await Datasets.list() + {**x.dict(), + "max_ticks": DatasetData.find(DatasetData.dataset_id == x.dataset_id).count() + } for x in Datasets.find().all() ] diff --git a/backend/routers/admin/game.py b/backend/routers/admin/game.py index 1915fd9..e9089ba 100644 --- a/backend/routers/admin/game.py +++ b/backend/routers/admin/game.py @@ -12,7 +12,7 @@ from typing import List from model.team import Team from routers.model import SuccessfulResponse -from db import limiter, database +from db import limiter import asyncio @@ -22,7 +22,7 @@ class CreateGameParams(BaseModel): game_name: str contest: bool - dataset_id: int + dataset_id: str start_time: datetime total_ticks: int tick_time: int @@ -30,13 +30,13 @@ class CreateGameParams(BaseModel): @router.post("/game/create") @limiter.exempt -async def game_create(params: CreateGameParams) -> SuccessfulResponse: - await Datasets.validate_ticks(params.dataset_id, params.total_ticks) +def game_create(params: CreateGameParams) -> SuccessfulResponse: + Datasets.validate_ticks(params.dataset_id, params.total_ticks) if params.start_time < datetime.now(): raise HTTPException(400, "Start time must be in the future") - await Game.create( + Game( game_name=params.game_name, is_contest=params.contest, dataset_id=params.dataset_id, @@ -45,97 +45,66 @@ async def game_create(params: CreateGameParams) -> SuccessfulResponse: tick_time=params.tick_time, is_finished=False, current_tick=0 - ) + ).save() + return SuccessfulResponse() @router.get("/game/list") @limiter.exempt -async def game_list() -> List[Game]: - return await Game.list() +def game_list() -> List[Game]: + return Game.find().all() @router.get("/game/{game_id}/player/list") @limiter.exempt -async def player_list(game_id: int) -> List[Player]: - return await Player.list(game_id=game_id) +def player_list(game_id: str) -> List[Player]: + return Player.find(Player.game_id == game_id).all() @router.post("/game/{game_id}/delete") @limiter.exempt -async def game_delete(game_id: int) -> SuccessfulResponse: +def game_delete(game_id: str) -> SuccessfulResponse: # TODO ne baca exception ako je vec zavrsena - await Game.update(game_id=game_id, is_finished=True) - return SuccessfulResponse() - - -class EditGameParams(BaseModel): - game_name: str | None - contest: bool | None - dataset_id: int | None - start_time: datetime | None - total_ticks: int | None - tick_time: int | None - - -@router.post("/game/{game_id}/edit") -@limiter.exempt -async def game_edit(game_id: int, params: EditGameParams) -> SuccessfulResponse: - try: - Bots.parse_string(params.bots) - except: - raise HTTPException(400, "Invalid bots string") - if params.dataset is not None: - Datasets.validate_string(params.dataset) - - if params.total_ticks is not None: - dataset = await Game.get(game_id=game_id) - dataset = dataset.dataset - - if params.dataset is not None: - dataset = params.dataset - - await Datasets.validate_ticks(dataset, params.total_ticks) - - if params.start_time is not None and params.start_time < datetime.now(): - raise HTTPException(400, "Start time must be in the future") - - await Game.update( - game_id=game_id, - **params.dict(exclude_unset=True) - ) + # await Game.update(game_id=game_id, is_finished=True) + g = Game.find(Game.game_id == game_id).first() + g.update(is_finished=True) + g.save() return SuccessfulResponse() @dataclass class NetworthData: - team_id: int + team_id: str team_name: str - player_id: int + player_id: str player_name: str networth: int @router.get("/game/{game_id}/networth") @limiter.exempt -async def game_networth(game_id: int) -> List[NetworthData]: - game = await Game.get(game_id=game_id) +def game_networth(game_id: str) -> List[NetworthData]: + # game = await Game.get(game_id=game_id) + game = Game.find(Game.game_id == game_id).first() if game.current_tick == 0: raise HTTPException( 400, "Game has not started yet or first tick has not been processed") - players = await Player.list(game_id=game_id) + # players = await Player.list(game_id=game_id) + players = Player.find(Player.game_id == game_id).all() team_networths = [] for player in players: team_networths.append({ "team_id": player.team_id, - "team_name": (await Team.get(team_id=player.team_id)).team_name, + # "team_name": (await Team.get(team_id=player.team_id)).team_name, + "team_name": Team.find(Team.team_id == player.team_id).first().team_name, "player_id": player.player_id, "player_name": player.player_name, - "networth": (await player.get_networth(game))["total"] + "networth": (player.get_networth(game)).total }) return team_networths @@ -143,12 +112,13 @@ async def game_networth(game_id: int) -> List[NetworthData]: @router.websocket("/game/{game_id}/dashboard/graphs") @limiter.exempt -async def dashboard(websocket: WebSocket, game_id: int): +async def dashboard_graphs(websocket: WebSocket, game_id: str): await websocket.accept() try: while True: - game = await Game.get(game_id=game_id) + # game = await Game.get(game_id=game_id) + game = Game.find(Game.game_id == game_id).first() current_tick = game.current_tick @@ -156,18 +126,27 @@ async def dashboard(websocket: WebSocket, game_id: int): await asyncio.sleep(game.tick_time / 1000) continue - dataset = (await DatasetData.list_by_game_id_where_tick( - game.dataset_id, game.game_id, current_tick - 1, current_tick - 1))[0] + # dataset = (await DatasetData.list_by_game_id_where_tick( + # game.dataset_id, game.game_id, current_tick - 1, current_tick - 1))[0] + dataset = DatasetData.find( + (DatasetData.dataset_id == game.dataset_id) & + (DatasetData.tick == current_tick - 1) + ).first() - dataset = dataclasses.asdict(dataset) + dataset = dataset.dict() - all_prices = await Market.list_by_game_id_where_tick( - game_id=game.game_id, - min_tick=current_tick - 1, - max_tick=current_tick - 1, - ) + # all_prices = await Market.list_by_game_id_where_tick( + # game_id=game.game_id, + # min_tick=current_tick - 1, + # max_tick=current_tick - 1, + # ) - all_prices = [dataclasses.asdict(price) for price in all_prices] + all_prices = Market.find( + (Market.game_id == game.game_id) & + (Market.tick == current_tick - 1) + ).all() + + all_prices = [price.dict() for price in all_prices] await websocket.send_json(json.dumps({ **dataset, @@ -181,12 +160,13 @@ async def dashboard(websocket: WebSocket, game_id: int): @router.websocket("/game/{game_id}/dashboard/players") @limiter.exempt -async def dashboard(websocket: WebSocket, game_id: int): +async def dashboard_players(websocket: WebSocket, game_id: str): await websocket.accept() try: while True: - game = await Game.get(game_id=game_id) + # game = await Game.get(game_id=game_id) + game = Game.find(Game.game_id == game_id).first() current_tick = game.current_tick @@ -194,14 +174,24 @@ async def dashboard(websocket: WebSocket, game_id: int): await asyncio.sleep(game.tick_time / 1000) continue - players = await Player.list(game_id=game_id) + # players = await Player.list(game_id=game_id) + players = Player.find(Player.game_id == game_id).all() + # networths = { + # player.player_id: (await player.get_networth(game))["total"] for player in players} networths = { - player.player_id: (await player.get_networth(game))["total"] for player in players} + player.player_id: (await player.get_networth(game)).total for player in players} + + # players = [{**dataclasses.asdict(player), + # "networth": networths[player.player_id] + # } for player in players] - players = [{**dataclasses.asdict(player), - "networth": networths[player.player_id] - } for player in players] + players = [ + { + **player.dict(), + "networth": networths[player.player_id] + } for player in players + ] await websocket.send_json(json.dumps({ "current_tick": current_tick, @@ -215,16 +205,22 @@ async def dashboard(websocket: WebSocket, game_id: int): @router.websocket("/game/{game_id}/dashboard/orderbooks") @limiter.exempt -async def dashboard(websocket: WebSocket, game_id: int): +async def dashboard_orderbooks(websocket: WebSocket, game_id: str): await websocket.accept() try: while True: - game = await Game.get(game_id=game_id) + # game = await Game.get(game_id=game_id) + game = Game.find(Game.game_id == game_id).first() - orders = await Order.list(game_id=game_id, order_status=OrderStatus.ACTIVE) + # orders = await Order.list(game_id=game_id, order_status=OrderStatus.ACTIVE) + orders = Order.find( + (Order.game_id == game.game_id) & + (Order.order_status == OrderStatus.ACTIVE.value) + ).all() - orders = [dataclasses.asdict(order) for order in orders] + # orders = [dataclasses.asdict(order) for order in orders] + orders = [order.dict() for order in orders] orders_by_resource = defaultdict( lambda: {str(OrderSide.BUY): [], str(OrderSide.SELL): []}) diff --git a/backend/routers/admin/player.py b/backend/routers/admin/player.py index a2aa6a9..797258d 100644 --- a/backend/routers/admin/player.py +++ b/backend/routers/admin/player.py @@ -3,7 +3,7 @@ from model.team import Team from model import Game, Player from routers.model import SuccessfulResponse -from routers.users.dependencies import game_dep, player_dep +from routers.users.dependencies import game_dep router = APIRouter() @@ -11,9 +11,20 @@ @router.get("/game/{game_id}/player/{player_id}/delete") @limiter.exempt -async def player_delete(game: Game = Depends(game_dep), - player: Player = Depends(player_dep)) -> SuccessfulResponse: - await Player.update(player_id=player.player_id, is_active=False) +def player_delete(player_id: str, game: Game = Depends(game_dep)) -> SuccessfulResponse: + # await Player.update(player_id=player.player_id, is_active=False) + + p = Player.find(Player.pk == player_id).first() + + if p is None: + return SuccessfulResponse(message=f"Player {player_id} not found.") + + if p.game_id != game.pk: + return SuccessfulResponse(message=f"Player {player_id} does not belong to game {game.pk}.") + + p.is_active = False + p.save() + if game.is_contest: - return SuccessfulResponse(message=f"Warning: This game is a contest game! Deleted player {player.player_id}.") + return SuccessfulResponse(message=f"Warning: This game is a contest. Player {player_id} will be deleted.") return SuccessfulResponse() diff --git a/backend/routers/admin/team.py b/backend/routers/admin/team.py index 73029bd..c287da3 100644 --- a/backend/routers/admin/team.py +++ b/backend/routers/admin/team.py @@ -21,29 +21,31 @@ class CreateTeam(BaseModel): @router.post("/team/create") @limiter.exempt -async def team_create(params: CreateTeam) -> Team: +def team_create(params: CreateTeam) -> Team: team_secret = id_generator() team_name = params.team_name - team_id = await Team.create(team_name=team_name, team_secret=team_secret) - return Team( - team_id=team_id, - team_secret=team_secret, - team_name=team_name - ) + + # team_id = await Team.create(team_name=team_name, team_secret=team_secret) + t = Team(team_name=team_name, team_secret=team_secret) + t.save() + + return t @router.get("/team/list") @limiter.exempt -async def team_list() -> List[Team]: - return await Team.list() +def team_list() -> List[Team]: + return Team.find().all() -@router.get("/team/{team_id}/delete") +@router.post("/team/{team_id}/delete") @limiter.exempt -async def team_delete(team_id: int) -> SuccessfulResponse: - team_id = await Team.delete(team_id=team_id) +def team_delete(team_id: str) -> SuccessfulResponse: + # team_id = await Team.delete(team_id=team_id) - if team_id is None: + if Team.find(Team.team_id == team_id).count() == 0: raise HTTPException(status_code=400, detail="Team not found") + Team.find(Team.team_id == team_id).delete() + return SuccessfulResponse() diff --git a/backend/routers/users/dependencies.py b/backend/routers/users/dependencies.py index d8d1624..e528afd 100644 --- a/backend/routers/users/dependencies.py +++ b/backend/routers/users/dependencies.py @@ -3,9 +3,10 @@ from model import Team, Player, Game from typing import Tuple from config import config +from model.order import Order -async def team_dep( +def team_dep( team_secret: str = Query( description="Team secret - given to you at the start of the competition.", default=None, @@ -14,45 +15,56 @@ async def team_dep( if team_secret is None: raise HTTPException(status_code=403, detail="Missing team_secret") try: - return await Team.get(team_secret=team_secret) + return Team.find(Team.team_secret==team_secret).first() except Exception: raise HTTPException(status_code=403, detail="Invalid team_secret") -async def game_dep(game_id: int) -> Game: +def game_dep(game_id: str) -> Game: try: - return await Game.get(game_id=game_id) + return Game.get(game_id) except Exception: raise HTTPException(status_code=403, detail="Invalid game_id") -async def check_game_active_dep(game: Game = Depends(game_dep)) -> None: +def check_game_active_dep(game: Game = Depends(game_dep)) -> None: if game.is_finished: raise HTTPException(403, "Game is already finished") if datetime.now() < game.start_time: raise HTTPException(403, "Game has not started yet") -async def player_dep( - player_id: int, game: Game = Depends(game_dep), team: Team = Depends(team_dep) +def player_dep( + player_id: str, game: Game = Depends(game_dep), team: Team = Depends(team_dep) ) -> Player: try: - player = await Player.get(player_id=player_id) + player = Player.get(player_id) except Exception: raise HTTPException(status_code=403, detail="Invalid player_id") if player.team_id != team.team_id: raise HTTPException(403, "This player doesn't belong to your team") if player.game_id != game.game_id: raise HTTPException(400, f"This player is in game {player.game_id}") - if player.is_active is False: + if not player.is_active: raise HTTPException(400, "This player is inactive or already has been deleted") return player +def order_dep(order_id: str, game: Game = Depends(game_dep)): + try: + order = Order.get(order_id) + except Exception: + raise HTTPException(400, "Invalid order_id") + if game.game_id != order.game_id: + raise HTTPException(400, f"This order belongs to game {game.game_id}") + # TODO: dozvoliti da se vidi od drugih playera? + return order + + tick_description = "Enter negative number for relative tick e.g. -5 for current_tick-5. Leave empty for last tick." -async def start_end_tick_dep( +def start_end_tick_dep( game: Game = Depends(game_dep), start_tick: int = Query( default=None, diff --git a/backend/routers/users/fixtures.py b/backend/routers/users/fixtures.py index 51376c4..85935cb 100644 --- a/backend/routers/users/fixtures.py +++ b/backend/routers/users/fixtures.py @@ -1,40 +1,46 @@ from datetime import datetime +from unittest.mock import MagicMock from fastapi import Query, Depends from model import Game, Team, Player from routers.users.dependencies import game_dep, team_dep, check_game_active_dep, player_dep, start_end_tick_dep -async def override_game_dep(game_id: int) -> Game: - assert game_id == 1 +def override_game_dep(game_id: str) -> Game: + assert game_id == "1" if not hasattr(override_game_dep, "contest"): - override_game_dep.contest = True + override_game_dep.contest = int(True) if not hasattr(override_game_dep, "tick"): override_game_dep.tick = 0 - return Game(game_id=1, game_name="game_1", is_active=True, is_contest=override_game_dep.contest, - dataset_id=1, start_time=datetime.now(), total_ticks=1000, tick_time=1000, current_tick=override_game_dep.tick) + return Game(pk="1", game_name="game_1", is_active=int(True), is_contest=int(override_game_dep.contest), + dataset_id="1", start_time=datetime.now(), total_ticks=1000, tick_time=1000, current_tick=override_game_dep.tick) -async def override_team_dep(team_secret: str = Query(description="Team secret", default=None)): +def override_team_dep(team_secret: str = Query(description="Team secret", default=None)): assert team_secret == "secret" - return Team(team_id=1, team_name="team_1", game_id=1, team_secret="secret") + return Team(pk="1", team_name="team_1", game_id="1", team_secret="secret") -async def mock_check_game_active_dep(game: Game = Depends(game_dep)): +def mock_check_game_active_dep(game: Game = Depends(game_dep)): mock_check_game_active_dep.call_count += 1 - return await check_game_active_dep(game) + return check_game_active_dep(game) -async def mock_player_dep(game: Game = Depends(game_dep)): +def mock_player_dep(game: Game = Depends(game_dep)): if not hasattr(mock_player_dep, "call_count"): mock_player_dep.call_count = 0 mock_player_dep.call_count += 1 - return Player(player_id=1, game_id=1, team_id=1, player_name="player_1", money=100) + return Player(pk="1", game_id="1", team_id="1", player_name="player_1", money=100) -async def mock_start_end_tick_dep(game: Game = Depends(game_dep), +def mock_start_end_tick_dep(game: Game = Depends(game_dep), start_tick: int = Query(default=None), end_tick: int = Query(default=None)): if not hasattr(mock_start_end_tick_dep, "call_count"): mock_start_end_tick_dep.call_count = 0 mock_start_end_tick_dep.call_count += 1 - return await start_end_tick_dep(game, start_tick, end_tick) + return start_end_tick_dep(game, start_tick, end_tick) + + +def set_mock_find(mock: MagicMock, method: str, return_value): + mock.return_value = MagicMock() + setattr(mock.return_value, method, MagicMock(return_value=return_value)) \ No newline at end of file diff --git a/backend/routers/users/game.py b/backend/routers/users/game.py index 8d04bb3..5152068 100644 --- a/backend/routers/users/game.py +++ b/backend/routers/users/game.py @@ -12,42 +12,48 @@ router = APIRouter() -@router.get("/game/time") -async def server_time() -> datetime: - return datetime.now() - - class GameData(BaseModel): - game_id: int + game_id: str game_name: str - is_contest: bool = Field(..., description="True if game is either a contest or a competition round. False if it is a normal game") - start_time: datetime = Field(..., description="Exact time at which this game starts") + is_contest: bool = Field( + ..., + description="True if game is either a contest or a competition round. False if it is a normal game", + ) + start_time: datetime = Field( + ..., description="Exact time at which this game starts" + ) total_ticks: int tick_time: int = Field(..., description="Time in milliseconds between ticks") current_tick: int = Field(..., description="Current tick in this game") is_finished: bool = Field(..., description="True if the game is finished") +@router.get("/game/time") +def server_time() -> datetime: + return datetime.now() + + @router.get( "/game/list", summary="List all available games.", response_description="List of games", ) -async def game_list() -> List[GameData]: - games = await Game.list() - return games +def game_list() -> List[GameData]: + return Game.find().all() class GameTimeData(GameData): current_time: datetime - next_tick_time: datetime = Field(..., description="Exact time when next tick begins") + next_tick_time: datetime = Field( + ..., description="Exact time when next tick processing begins" + ) @router.get( "/game/{game_id}", summary="Current time on server and time of the next tick in this game.", ) -async def get_game(game: Game = Depends(game_dep)) -> GameTimeData: +def get_game(game: Game = Depends(game_dep)) -> GameTimeData: next_tick_time = game.start_time + timedelta( milliseconds=game.current_tick * game.tick_time ) @@ -58,7 +64,10 @@ async def get_game(game: Game = Depends(game_dep)) -> GameTimeData: class DatasetListResponseItem(BaseModel): tick: int = Field(..., description="In game tick when this data was used") - date: datetime = Field(..., description="Time when this measurment took place in the real world. Year is not accurate") + date: datetime = Field( + ..., + description="Time when this measurment took place in the real world. Year is not accurate", + ) coal: int uranium: int biomass: int @@ -68,8 +77,13 @@ class DatasetListResponseItem(BaseModel): wind: int solar: int hydro: int - energy_demand: int = Field(..., description="Volume of energy that was demanded in the tick") - max_energy_price: int = Field(..., description="Maximum price at which energy was tried to be bought in the tick") + energy_demand: int = Field( + ..., description="Volume of energy that was demanded in the tick" + ) + max_energy_price: int = Field( + ..., + description="Maximum price at which energy was tried to be bought in the tick", + ) def __post_init__(self): self.date.year = 2012 @@ -79,19 +93,17 @@ def __post_init__(self): "/game/{game_id}/dataset", summary="Get power plant production rates for all resources and energy demand for previous ticks.", ) -async def dataset_list( +def dataset_list( start_end=Depends(start_end_tick_dep), game: Game = Depends(game_dep) -) -> Dict[int, DatasetListResponseItem]: +) -> Dict[str, DatasetListResponseItem]: start_tick, end_tick = start_end - all_entries = await DatasetData.list_by_game_id_where_tick( - dataset_id=game.dataset_id, - game_id=game.game_id, - min_tick=start_tick, - max_tick=end_tick, - ) + all_entries = DatasetData.find( + (DatasetData.dataset_id == game.dataset_id) + & (DatasetData.tick >= start_tick) + & (DatasetData.tick <= end_tick) + ).all() all_entries_dict = {} for entry in all_entries: all_entries_dict[entry.tick] = entry - return all_entries_dict diff --git a/backend/routers/users/market.py b/backend/routers/users/market.py index bb89626..35ad0cc 100644 --- a/backend/routers/users/market.py +++ b/backend/routers/users/market.py @@ -1,25 +1,30 @@ from collections import defaultdict from datetime import datetime from enum import Enum -from typing import List, Dict, Optional +from functools import reduce +from itertools import chain +from operator import attrgetter +from typing import Dict, List, Optional + from fastapi import APIRouter, Depends, HTTPException, Query -from model.resource import Energy -from model.trade import TradeDb from pydantic import BaseModel, Field -from model import Order, OrderSide, OrderType, OrderStatus, Resource + +from config import config +from model import Order, OrderSide, OrderStatus, Resource from model.game import Game from model.market import Market from model.player import Player +from model.resource import Energy, ResourceOrEnergy +from model.trade import Trade +from routers.model import SuccessfulResponse + from .dependencies import ( + check_game_active_dep, game_dep, + order_dep, player_dep, - check_game_active_dep, start_end_tick_dep, ) -from db.db import database -from routers.model import SuccessfulResponse -from config import config - router = APIRouter(dependencies=[Depends(check_game_active_dep)]) @@ -30,14 +35,16 @@ class MarketPricesResponse(BaseModel): high: int = Field(..., description="highest price of all trades") open: int = Field(..., description="price of the first trade") close: int = Field(..., description="price of the last trade") - market: int = Field(..., description="average price of all trades weighted by their volume") + market: int = Field( + ..., description="average price of all trades weighted by their volume" + ) volume: int = Field(..., description="total volume traded") @router.get( "/game/{game_id}/market/prices", summary="Get market data for previous ticks." ) -async def market_prices( +def market_prices( start_end=Depends(start_end_tick_dep), resource: Resource | Energy = Query(default=None), game: Game = Depends(game_dep), @@ -48,12 +55,15 @@ async def market_prices( """ start_tick, end_tick = start_end - all_prices = await Market.list_by_game_id_where_tick( - game_id=game.game_id, - min_tick=start_tick, - max_tick=end_tick, - resource=resource, - ) + query = [ + Market.game_id == game.game_id, + Market.tick >= start_tick, + Market.tick <= end_tick, + ] + if resource is not None: + query.append(Market.resource == resource.value) + + all_prices: List[Market] = Market.find(*query) all_prices_dict = defaultdict(list) for price in all_prices: all_prices_dict[price.resource].append(price) @@ -68,7 +78,7 @@ class EnergyPrice(BaseModel): "/game/{game_id}/player/{player_id}/energy/set_price", summary="Set price at which you will sell your electricity", ) -async def energy_set_price_player( +def energy_set_price_player( price: EnergyPrice, game: Game = Depends(game_dep), player: Player = Depends(player_dep), @@ -82,22 +92,27 @@ async def energy_set_price_player( if price.price <= 0: raise HTTPException(status_code=400, detail="Price must be greater than 0") - await Player.update(player_id=player.player_id, energy_price=price.price) + with player.lock(): + player.update(energy_price=price.price) return SuccessfulResponse() class OrderResponse(BaseModel): - order_id: int - player_id: int + order_id: str + player_id: str price: int = Field(..., description="price per unit of resource") size: int = Field(..., description="total volume of this order") tick: int = Field(..., description="tick when this order was put in the market") timestamp: datetime = Field(..., description="exact time when this order was made") order_side: OrderSide order_status: OrderStatus - filled_size: int = Field(..., description="volume of this order that was already traded") - expiration_tick: int = Field(..., description="tick when this order will be cancelled") + filled_size: int = Field( + ..., description="volume of this order that was already traded" + ) + expiration_tick: int = Field( + ..., description="tick when this order will be cancelled" + ) class OrderRestriction(Enum): @@ -107,7 +122,7 @@ class OrderRestriction(Enum): @router.get("/game/{game_id}/orders", summary="Get orders in this game.") -async def order_list( +def order_list( game: Game = Depends(game_dep), restriction: OrderRestriction = Query( default=OrderRestriction.all_orders, @@ -116,30 +131,56 @@ async def order_list( "bot for only bot orders / best for those with best prices" ), ), -) -> Dict[Resource, List[OrderResponse]]: +) -> Dict[str, Dict[str, List[OrderResponse]]]: + bots = Player.find(Player.is_bot == int(True)).all() + bot_ids = set(map(attrgetter("pk"), bots)) + + active_orders = Order.find( + Order.game_id == game.game_id, + Order.order_status == OrderStatus.ACTIVE.value + ).all() + pending_orders = Order.find( + Order.game_id == game.game_id, + Order.order_status == OrderStatus.PENDING.value + ).all() + + def is_bot_order(order: Order): + return order.player_id in bot_ids + + all_orders = active_orders + list(filter(is_bot_order, pending_orders)) + if restriction == OrderRestriction.all_orders: - orders = await Order.list_orders_by_game_id( - game_id=game.game_id - ) + return orders_to_dict(all_orders) elif restriction == OrderRestriction.bot_orders: - orders = await Order.list_bot_orders_by_game_id( - game_id=game.game_id, - ) + bot_orders = list(filter(is_bot_order, chain(pending_orders, active_orders))) + return orders_to_dict(bot_orders) elif restriction == OrderRestriction.best_orders: - best_buy_order = await Order.list_best_orders_by_game_id( - game_id=game.game_id, order_side=OrderSide.BUY - ) - best_sell_order = await Order.list_best_orders_by_game_id( - game_id=game.game_id, order_side=OrderSide.SELL - ) - orders = best_buy_order + best_sell_order - return orders_to_dict(orders) - - -def orders_to_dict(orders: List[Order]) -> Dict[Resource, List[Order]]: - orders_dict = defaultdict(list) + orders = orders_to_dict(all_orders) + for resource in orders: + for order_side in orders[resource]: + orders[resource][order_side] = [reduce( + is_better_order, orders[resource][order_side] + )] + return orders + + +def is_better_order(order_1: Order, order_2: Order): + if order_1.order_side != order_2.order_side: + raise Exception("This is because order sides don't match and shouldn't happen") + if order_1.order_side == OrderSide.BUY: + return order_1 if order_1.price > order_2.price else order_2 + else: + return order_1 if order_1.price < order_2.price else order_2 + + +def orders_to_dict(orders: List[Order]) -> Dict[ResourceOrEnergy, Dict[OrderSide, List[Order]]]: + orders_dict = dict() for order in orders: - orders_dict[order.resource].append(order) + if order.resource.value not in orders_dict: + orders_dict[order.resource.value] = dict() + if order.order_side.value not in orders_dict[order.resource.value]: + orders_dict[order.resource.value][order.order_side.value] = list() + orders_dict[order.resource.value][order.order_side.value].append(order) return orders_dict @@ -147,20 +188,20 @@ def orders_to_dict(orders: List[Order]) -> Dict[Resource, List[Order]]: "/game/{game_id}/player/{player_id}/orders", summary="Get only your orders in this game", ) -async def order_list_player( +def order_list_player( game: Game = Depends(game_dep), player: Player = Depends(player_dep) -) -> Dict[Resource, List[OrderResponse]]: +) -> Dict[str, Dict[str, List[OrderResponse]]]: """List orders you placed in market that are still active.""" - active_orders = await Order.list( - game_id=game.game_id, - player_id=player.player_id, - order_status=OrderStatus.ACTIVE.value, - ) - pending_orders = await Order.list( - game_id=game.game_id, - player_id=player.player_id, - order_status=OrderStatus.PENDING.value, - ) + active_orders = Order.find( + Order.player_id == player.player_id, + Order.game_id == game.game_id, + Order.order_status == OrderStatus.ACTIVE.value + ).all() + pending_orders = Order.find( + Order.player_id == player.player_id, + Order.game_id == game.game_id, + Order.order_status == OrderStatus.PENDING.value + ).all() return orders_to_dict(active_orders + pending_orders) @@ -168,33 +209,41 @@ async def order_list_player( "/game/{game_id}/player/{player_id}/orders/{order_id}", summary="Get order details for given order_id", ) -async def order_get_player( - order_id: int, game: Game = Depends(game_dep), player: Player = Depends(player_dep) +def order_get_player( + order: Order = Depends(order_dep), + game: Game = Depends(game_dep), + player: Player = Depends(player_dep), ) -> OrderResponse: - order = await Order.get( - order_id=order_id, game_id=game.game_id, player_id=player.player_id - ) return order class UserOrder(BaseModel): resource: Resource = Field(..., description="resource you are buying or selling") - price: int = Field(..., description="price per unit of resource you are buying or selling") + price: int = Field( + ..., description="price per unit of resource you are buying or selling" + ) size: int = Field(..., description="ammount of resource you want to buy or sell") - expiration_tick: Optional[int] = Field(None, description="exact tick in which this order will expire") - expiration_length: Optional[int] = Field(None, description= "number of ticks from now when this order will expire") - side: OrderSide = Field(..., description="BUY if you want to buy a resource, SELL if you want to sell it") + expiration_tick: Optional[int] = Field( + None, description="exact tick in which this order will expire" + ) + expiration_length: Optional[int] = Field( + None, description="number of ticks from now when this order will expire" + ) + side: OrderSide = Field( + ..., + description="BUY if you want to buy a resource, SELL if you want to sell it", + ) @router.post( "/game/{game_id}/player/{player_id}/orders/create", summary="Create a new order on the market", ) -async def order_create_player( +def order_create_player( order: UserOrder, game: Game = Depends(game_dep), player: Player = Depends(player_dep), -) -> int: +) -> str: f""" - If side is buy, price is maximum price you will accept. - If side is sell, price is minimum price at which you will sell. @@ -235,9 +284,13 @@ async def order_create_player( status_code=400, detail=f"Size ({order.size}) must be greater than 0" ) - total_orders_not_processed = await Order.count_player_orders( - game_id=game.game_id, player_id=player.player_id, resource=order.resource - ) + total_orders_not_processed = Order.find( + Order.game_id == game.game_id, + Order.player_id == player.player_id, + Order.resource == order.resource.value, + (Order.order_status == OrderStatus.ACTIVE.value) + | (Order.order_status == OrderStatus.PENDING.value) + ).count() if total_orders_not_processed >= config["player"]["max_orders"]: raise HTTPException( @@ -245,7 +298,7 @@ async def order_create_player( detail=f'Maximum {config["player"]["max_orders"]} orders can be active at a time', ) - resources = player[order.resource] + resources = player.resources[order.resource] cost = order.price * order.size if order.side is OrderSide.SELL and resources < order.size: raise HTTPException( @@ -258,73 +311,71 @@ async def order_create_player( detail=f"Not enough money: has ({player.money}), order_cost({cost}) = size({order.size})*price({order.price})", ) - return await Order.create( - game_id=game.game_id, - player_id=player.player_id, - order_type=OrderType.LIMIT, - order_side=order.side.value, - order_status=OrderStatus.PENDING, - timestamp=datetime.now(), - price=order.price, - size=order.size, - tick=game.current_tick, - expiration_tick=order.expiration_tick, - resource=order.resource, + return ( + Order( + game_id=game.game_id, + player_id=player.player_id, + order_side=order.side.value, + order_status=OrderStatus.PENDING, + timestamp=datetime.now(), + price=order.price, + size=order.size, + tick=game.current_tick, + expiration_tick=order.expiration_tick, + resource=order.resource.value, + ) + .save() + .pk ) -class OrderCancel(BaseModel): - ids: List[int] - - -@router.post( - "/game/{game_id}/player/{player_id}/orders/cancel", - summary="Cancel the list of orders", +@router.get( + "/game/{game_id}/player/{player_id}/orders/{order_id}/cancel", + summary="Cancel the order", ) -async def order_cancel_player( - body: OrderCancel, +def order_cancel_player( + order: Order = Depends(order_dep), game: Game = Depends(game_dep), player: Player = Depends(player_dep), ) -> SuccessfulResponse: - async with database.transaction(): - for order_id in body.ids: - order_to_cancel = await Order.get(order_id=order_id) - if order_to_cancel.player_id != player.player_id: - raise HTTPException( - status_code=400, detail="You can only cancel your own orders" - ) - elif order_to_cancel.order_status == OrderStatus.PENDING.value: - await Order.update( - order_id=order_id, order_status=OrderStatus.CANCELLED.value - ) - elif order_to_cancel.order_status == OrderStatus.ACTIVE.value: - await Order.update( - order_id=order_id, order_status=OrderStatus.USER_CANCELLED.value - ) - else: - raise HTTPException( - status_code=400, - detail="Only pending or active orders can be cancelled", - ) + with player.lock(): + order_to_cancel = Order.get(order.order_id) + if order_to_cancel.player_id != player.player_id: + raise HTTPException( + status_code=400, detail="You can only cancel your own orders" + ) + elif order_to_cancel.order_status == OrderStatus.PENDING.value: + Order.update(order_status=OrderStatus.CANCELLED.value) + elif order_to_cancel.order_status == OrderStatus.ACTIVE.value: + Order.update(order_status=OrderStatus.USER_CANCELLED.value) + else: + raise HTTPException( + status_code=400, + detail="Only pending or active orders can be cancelled", + ) return SuccessfulResponse() class UserTrade(BaseModel): - trade_id: int - buy_order_id: int = Field(..., description="order_id of buyer side in this trade") - sell_order_id: int = Field(..., description="order_id of seller side in this trade") + trade_id: str + buy_order_id: str = Field(..., description="order_id of buyer side in this trade") + sell_order_id: str = Field(..., description="order_id of seller side in this trade") tick: int = Field(..., description="Tick when this trade took place") - filled_money: int = Field(..., description="Total value of the trade = filled_size * filled_price") + filled_money: int = Field( + ..., description="Total value of the trade = filled_size * filled_price" + ) filled_size: int = Field(..., description="Ammount of resources that was traded") - filled_price: int = Field(..., description="Price at which the unit of resource was traded") + filled_price: int = Field( + ..., description="Price at which the unit of resource was traded" + ) @router.get( "/game/{game_id}/player/{player_id}/trades", summary="Get your matched trades for previous ticks", ) -async def get_trades_player( +def get_trades_player( start_end=Depends(start_end_tick_dep), player: Player = Depends(player_dep), resource: Resource = Query(default=None), @@ -335,18 +386,16 @@ async def get_trades_player( """ start_tick, end_tick = start_end - buy_trades = await TradeDb.list_buy_trades_by_player_id( - player_id=player.player_id, - min_tick=start_tick, - max_tick=end_tick, - resource=resource, - ) - sell_trades = await TradeDb.list_sell_trades_by_player_id( - player_id=player.player_id, - min_tick=start_tick, - max_tick=end_tick, - resource=resource, + condition = ( + (Trade.tick <= end_tick) + & (Trade.tick >= end_tick) + & (Trade.resource == resource.value) ) + + buy_trades = Trade.find((Trade.buy_order_id == player.player_id) & condition).all() + sell_trades = Trade.find( + (Trade.sell_order_id == player.player_id) & condition + ).all() return { OrderSide.BUY: buy_trades, OrderSide.SELL: sell_trades, diff --git a/backend/routers/users/player.py b/backend/routers/users/player.py index c425d89..eee924d 100644 --- a/backend/routers/users/player.py +++ b/backend/routers/users/player.py @@ -1,11 +1,11 @@ from typing import List from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, Field -from db import database from model import Player, Team from config import config from model.game import Game -from model.order import Order +from model.player import Networth +from model.power_plant_model import PowerPlantsModel, ResourcesModel from .dependencies import game_dep, player_dep, check_game_active_dep, team_dep from routers.model import SuccessfulResponse @@ -14,66 +14,43 @@ class PlayerData(BaseModel): - player_id: int + player_id: str player_name: str - game_id: int = Field(..., description="game in which this player exists") + game_id: str = Field(..., description="game in which this player exists") energy_price: int = Field(..., description="energy price set by the player") + money: int + energy: int + + resources: ResourcesModel - coal: int - uranium: int - biomass: int - gas: int - oil: int - - coal_plants_owned: int - uranium_plants_owned: int - biomass_plants_owned: int - gas_plants_owned: int - oil_plants_owned: int - geothermal_plants_owned: int - wind_plants_owned: int - solar_plants_owned: int - hydro_plants_owned: int - - coal_plants_powered: int - uranium_plants_powered: int - biomass_plants_powered: int - gas_plants_powered: int - oil_plants_powered: int - geothermal_plants_powered: int - wind_plants_powered: int - solar_plants_powered: int - hydro_plants_powered: int + power_plants_owned: PowerPlantsModel + power_plants_powered: PowerPlantsModel @router.get( "/game/{game_id}/player/list", summary="Get list of your players in the game" ) -async def player_list( +def player_list( game: Game = Depends(game_dep), team: Team = Depends(team_dep) ) -> List[PlayerData]: - players = await Player.list( - game_id=game.game_id, team_id=team.team_id, is_active=True - ) - return players + return Player.find( + Player.game_id == game.game_id, + Player.team_id == team.team_id, + Player.is_active == int(True) + ).all() class PlayerCreate(BaseModel): player_name: str = None -class PlayerCreateResponse(BaseModel): - player_id: int - player_name: str - - @router.post("/game/{game_id}/player/create", summary="Create a player in the game") -async def player_create( +def player_create( game: Game = Depends(game_dep), team: Team = Depends(team_dep), - player_create: PlayerCreate | None | dict = None, -) -> PlayerCreateResponse: + params: PlayerCreate | None = None, +) -> PlayerData: f""" Create a player in the game with a given name. Name is chosen automatically if left blank. @@ -82,10 +59,12 @@ async def player_create( - You can have at most one player in contest mode - In normal game you can have at most {config["player"]["max_players_per_team"]} """ - async with database.transaction(): - team_players = await Player.count( - game_id=game.game_id, team_id=team.team_id, is_active=True - ) + with team.lock(): + team_players = Player.find( + Player.game_id == game.game_id, + Player.team_id == team.team_id, + Player.is_active == int(True) + ).count() if game.is_contest and team_players >= 1: raise HTTPException( 400, "Only one player per team can be created in contest mode" @@ -94,37 +73,31 @@ async def player_create( if team_players >= config["player"]["max_players_per_team"]: raise HTTPException(400, "Maximum number of players per team reached") - team_id = team.team_id - game_id = game.game_id - - if player_create is None or player_create.player_name is None: + if params is None or params.player_name is None: player_name = f"{team.team_name}_{team_players}" else: - player_name = player_create.player_name + player_name = params.player_name starting_money = config["player"]["starting_money"] - - player_id = await Player.create( - game_id=game_id, - team_id=team_id, + return Player( + game_id=game.game_id, + team_id=team.team_id, player_name=player_name, money=starting_money, - ) - - return PlayerCreateResponse(player_id=player_id, player_name=player_name) + is_bot=int(False) + ).save() @router.get( "/game/{game_id}/player/{player_id}", - dependencies=[Depends(check_game_active_dep)], summary="Get player data", ) -async def player_get(player: Player = Depends(player_dep)) -> PlayerData: - return await Player.get(player_id=player.player_id) +def player_get(player: Player = Depends(player_dep)) -> PlayerData: + return player @router.get("/game/{game_id}/player/{player_id}/delete", summary="Delete the player") -async def player_delete( +def player_delete( game: Game = Depends(game_dep), player: Player = Depends(player_dep) ) -> SuccessfulResponse: """ @@ -133,25 +106,22 @@ async def player_delete( """ if game.is_contest: raise HTTPException(400, "Players cannot be deleted in contest mode") - - await Player.update(player_id=player.player_id, is_active=False) + with Player.lock(): + player = Player.get(player.pk) + if not player.is_active: + raise HTTPException(400, "Cannot delete already deleted player") + player.update(is_active=False) return SuccessfulResponse() -class PlayerNetWorth(BaseModel): - plants_owned: dict[str, dict[str, int]] = Field(..., description="players networth based only on power plants") - money: int - resources: dict[str, dict[str, int]] = Field(..., description="players networth based on resources prices on the market") - total: int = Field(..., description="total players networth. this is your score in competition rounds!") - - @router.get( "/game/{game_id}/player/{player_id}/networth", summary="Get networth of your player in current market", + dependencies=[Depends(check_game_active_dep)], ) -async def player_net_worth( +def player_net_worth( player: Player = Depends(player_dep), game: Game = Depends(game_dep) -) -> PlayerNetWorth: +) -> Networth: """ Calculates the networth of your player based on owned power plants, money and resources owned. Resources are priced at current market data. @@ -165,15 +135,14 @@ async def player_net_worth( raise HTTPException( 400, "Game has not started yet or first tick has not been processed" ) - - return await player.get_networth(game) + return player.get_networth(game) @router.get( "/game/{game_id}/player/{player_id}/reset", summary="Reset the player to starting resources", ) -async def player_reset( +def player_reset( game: Game = Depends(game_dep), team: Team = Depends(team_dep), player: Player = Depends(player_dep), @@ -187,25 +156,19 @@ async def player_reset( if game.is_contest: raise HTTPException(400, "Players cannot be reset in contest mode") - fields = { - x: 0 - for x in Player.__dataclass_fields__.keys() - if x - not in [ - "player_id", - "player_name", - "game_id", - "team_id", - "is_active", - "is_bot", - "table_name", - ] - } - fields["money"] = config["player"]["starting_money"] - fields["energy_price"] = 1e9 - - async with database.transaction(): - await Player.update(player_id=player.player_id, **fields) - await Order.cancel_player_orders(player_id=player.player_id) + reset_player = Player( + pk=player.pk, + player_name=player.player_name, + game_id=player.game_id, + team_id=player.team_id, + ) + + reset_player.money = config["player"]["starting_money"] + + with Player.lock(): + pipe = Player.db().pipeline() + reset_player.cancel_orders(pipe) + reset_player.save() + pipe.execute() return SuccessfulResponse() diff --git a/backend/routers/users/power_plant.py b/backend/routers/users/power_plant.py index cd14c75..1c0b105 100644 --- a/backend/routers/users/power_plant.py +++ b/backend/routers/users/power_plant.py @@ -1,10 +1,9 @@ from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, Field -from db import database from model import Player, PowerPlantType +from model.power_plant_model import PowerPlantsModel from .dependencies import check_game_active_dep, player_dep from config import config -from typing import Dict from routers.model import SuccessfulResponse @@ -12,18 +11,26 @@ class PowerPlantData(BaseModel): - plants_powered: int = Field(..., description="number of plants of this type powered on") - plants_owned: int = Field(..., description="number of plants of this type owned by the player") - next_price: int = Field(..., description="price at which you can buy your next power plant of this type") - sell_price: int = Field(..., description="price at which you can sell this power plant") + power_plants_powered: PowerPlantsModel = Field( + ..., description="Number of plants of this type powered on" + ) + power_plants_owned: PowerPlantsModel = Field( + ..., description="Number of plants of this type owned by the player" + ) + buy_price: PowerPlantsModel = Field( + ..., description="Price at which you can buy your next power plant of this type" + ) + sell_price: PowerPlantsModel = Field( + ..., description="Price at which you can sell this power plant" + ) @router.get( "/game/{game_id}/player/{player_id}/plant/list", summary="List power plants you own" ) -async def list_plants( +def list_plants( player: Player = Depends(player_dep), -) -> Dict[str, PowerPlantData]: +) -> PowerPlantData: """ Returns number of power plants you own for each resource, and number of them that are turned on. @@ -37,18 +44,21 @@ async def list_plants( The price at which you sell the power plant is **lower** than buying price, so don't buy power plants if you don't mean it! """ - return { - x.name: PowerPlantData( - plants_powered=player[x.name.lower() + "_plants_powered"], - plants_owned=player[x.name.lower() + "_plants_owned"], - next_price=x.get_plant_price(player[x.name.lower() + "_plants_owned"]), - sell_price=round( - x.get_plant_price(player[x.name.lower() + "_plants_owned"]) - * config["power_plant"]["sell_coeff"] - ), + buy_price = PowerPlantsModel() + sell_price = PowerPlantsModel() + for type in PowerPlantType: + buy_price[type] = PowerPlantType.get_plant_price( + type, player.power_plants_owned[type] ) - for x in PowerPlantType - } + sell_price[type] = PowerPlantType.get_sell_price( + type, player.power_plants_owned[type] + ) + return PowerPlantData( + power_plants_powered=player.power_plants_powered, + power_plants_owned=player.power_plants_owned, + buy_price=buy_price, + sell_price=sell_price, + ) class PowerPlantTypeData(BaseModel): @@ -58,7 +68,7 @@ class PowerPlantTypeData(BaseModel): @router.post( "/game/{game_id}/player/{player_id}/plant/buy", summary="Buy another power plant" ) -async def buy_plant( +def buy_plant( plant: PowerPlantTypeData, player: Player = Depends(player_dep) ) -> SuccessfulResponse: """ @@ -66,20 +76,17 @@ async def buy_plant( """ type = PowerPlantType(plant.type) - async with database.transaction(): - player_id = player.player_id - player = await Player.get(player_id=player_id) - plant_count = player[type.name.lower() + "_plants_owned"] + with player.lock(): + player = Player.get(player.player_id) + plant_count = player.power_plants_owned[type] plant_price = type.get_plant_price(plant_count) if player.money < plant_price: raise HTTPException(status_code=400, detail="Not enough money") - await Player.update(player_id=player_id, money=player.money - plant_price) - await Player.update( - player_id=player_id, - **{type.name.lower() + "_plants_owned": plant_count + 1}, - ) + player.money -= plant_price + player.power_plants_owned[type] += 1 + player.save() return SuccessfulResponse() @@ -87,7 +94,7 @@ async def buy_plant( "/game/{game_id}/player/{player_id}/plant/sell", summary="Sell your last power plant", ) -async def sell_plant( +def sell_plant( plant: PowerPlantTypeData, player: Player = Depends(player_dep) ) -> SuccessfulResponse: f""" @@ -95,52 +102,50 @@ async def sell_plant( """ type = PowerPlantType(plant.type) - async with database.transaction(): - player_id = player.player_id - player = await Player.get(player_id=player_id) - plant_count = player[type.name.lower() + "_plants_owned"] - plant_powered = player[type.name.lower() + "_plants_powered"] + with player.lock(): + player = Player.get(player.player_id) + plant_count = player.power_plants_owned[type] + plant_powered = player.power_plants_powered[type] + plant_price = type.get_sell_price(plant_count) if plant_count <= 0: raise HTTPException(status_code=400, detail="No plants to sell") - await Player.update( - player_id=player_id, - money=player.money + type.get_sell_price(plant_count), - **{ - type.name.lower() + "_plants_owned": plant_count - 1, - type.name.lower() + "_plants_powered": max( - 0, min(plant_count - 1, plant_powered) - ), - }, - ) + player.money += plant_price + player.power_plants_owned[type] -= 1 + player.power_plants_powered[type] = max(0, min(plant_count - 1, plant_powered)) + player.save() return SuccessfulResponse() class PowerOn(BaseModel): type: PowerPlantType - number: int = Field(..., description="total number of plants of this type you want to have powered on. 0 to turn them all off.") + number: int = Field( + ..., + description="total number of plants of this type you want to have powered on. 0 to turn them all off.", + ) @router.post( "/game/{game_id}/player/{player_id}/plant/on", summary="Turn on number of power plants", ) -async def turn_on( +def turn_on( plant: PowerOn, player: Player = Depends(player_dep) ) -> SuccessfulResponse: - async with database.transaction(): - player_id = player.player_id - player = await Player.get(player_id=player_id) - type = PowerPlantType(plant.type) - plant_count = player[type.name.lower() + "_plants_owned"] - - if plant_count < plant.number or plant.number < 0: - raise HTTPException( - status_code=400, detail="Not enough plants or invalid number" - ) - - await Player.update( - player_id=player_id, **{type.name.lower() + "_plants_powered": plant.number} + type = PowerPlantType(plant.type) + if plant.number < 0: + raise HTTPException( + status_code=400, + detail=f"Invalid number of power plants to turn on ({plant.number})", ) + + with player.lock(): + player = Player.get(player.player_id) + plant_count = player.power_plants_owned[type] + + if plant_count < plant.number: + raise HTTPException(status_code=400, detail="Not enough power plants") + player.power_plants_powered = plant.number + player.save() return SuccessfulResponse() diff --git a/backend/routers/users/test_dependencies.py b/backend/routers/users/test_dependencies.py index c632018..f64b2a1 100644 --- a/backend/routers/users/test_dependencies.py +++ b/backend/routers/users/test_dependencies.py @@ -2,49 +2,44 @@ import pytest from .dependencies import team_dep, game_dep, check_game_active_dep, player_dep, start_end_tick_dep from model import Team, Game, Player -from unittest.mock import patch +from unittest.mock import MagicMock, patch from datetime import datetime, timedelta from config import config -@pytest.mark.asyncio -async def test_team_dep(): +def test_team_dep(): # not supplied try: - - await team_dep(None) + team_dep(None) assert False # pragma: no cover except HTTPException as e: assert e.status_code == 403 assert e.detail == "Missing team_secret" # exception raised in get - with patch("model.Team.get") as mock: - - mock.side_effect = Exception() + with patch("model.Team.find") as mock: + mock.return_value.first = MagicMock(side_effect=Exception()) try: - await team_dep() + team_dep() assert False # pragma: no cover except HTTPException as e: assert e.status_code == 403 assert e.detail == "Invalid team_secret" # valid team - with patch("model.Team.get") as mock: - t = Team( - team_id=1, team_secret="secret", team_name="name") - mock.return_value = t + with patch("model.Team.find") as mock: + t = Team(team_secret="secret", team_name="name") + mock.return_value.first = MagicMock(return_value=t) - assert await team_dep() == t + assert team_dep() == t -@pytest.mark.asyncio -async def test_game_dep(): +def test_game_dep(): # exception raised in get - with patch("model.Game.get") as mock: + with patch("model.Game.find") as mock: mock.side_effect = Exception() try: - await game_dep(1) + game_dep(1) assert False # pragma: no cover except HTTPException as e: assert e.status_code == 403 @@ -53,23 +48,22 @@ async def test_game_dep(): # valid game with patch("model.Game.get") as mock: g = Game( - game_id=1, game_name="name", start_time=datetime.now(), is_finished=False, + game_name="name", start_time=datetime.now(), is_finished=False, is_contest=False, dataset_id=1, current_tick=0, tick_time=1000, total_ticks=10) mock.return_value = g - assert await game_dep(1) == g + assert game_dep(1) == g -@pytest.mark.asyncio -async def test_check_game_active_dep(): +def test_check_game_active_dep(): # game is finished with patch("model.Game.get") as mock: g = Game( - game_id=1, game_name="name", start_time=datetime.now(), is_finished=True, + game_name="name", start_time=datetime.now(), is_finished=True, is_contest=False, dataset_id=1, current_tick=0, tick_time=1000, total_ticks=10) mock.return_value = g try: - await check_game_active_dep(g) + check_game_active_dep(g) assert False # pragma: no cover except HTTPException as e: assert e.status_code == 403 @@ -78,11 +72,11 @@ async def test_check_game_active_dep(): # game has not started yet with patch("model.Game.get") as mock: g = Game( - game_id=1, game_name="name", start_time=datetime.now() + timedelta(days=1), is_finished=False, + game_name="name", start_time=datetime.now() + timedelta(days=1), is_finished=False, is_contest=False, dataset_id=1, current_tick=0, tick_time=1000, total_ticks=10) mock.return_value = g try: - await check_game_active_dep(g) + check_game_active_dep(g) assert False # pragma: no cover except HTTPException as e: assert e.status_code == 403 @@ -91,73 +85,74 @@ async def test_check_game_active_dep(): # valid game with patch("model.Game.get") as mock: g = Game( - game_id=1, game_name="name", start_time=datetime.now(), is_finished=False, + game_name="name", start_time=datetime.now(), is_finished=False, is_contest=False, dataset_id=1, current_tick=1, tick_time=1000, total_ticks=10) mock.return_value = g - assert await check_game_active_dep(g) is None + assert check_game_active_dep(g) is None -@pytest.mark.asyncio -async def test_player_dep(): - g = Game( - game_id=1, game_name="name", start_time=datetime.now(), is_finished=False, +@pytest.fixture +def game(): + return Game( + pk=1, game_name="name", start_time=datetime.now(), is_finished=False, is_contest=False, dataset_id=1, current_tick=0, tick_time=1000, total_ticks=10) - # exception raised in get + + +def test_player_dep_get_exception(): with patch("model.Player.get") as mock: mock.side_effect = Exception() try: - await player_dep(1) + player_dep(1) assert False # pragma: no cover except HTTPException as e: assert e.status_code == 403 assert e.detail == "Invalid player_id" - # player doesn't belong to team +def test_player_dep_doesnt_belong_to_team(game): with patch("model.Player.get") as mock: p = Player( player_id=1, game_id=1, team_id=1, is_active=True, player_name="name") mock.return_value = p try: - await player_dep(1, game=g, team=Team(team_id=2, team_name="team", team_secret="aaaaa")) + player_dep(1, game=game, team=Team(pk=2, team_name="team", team_secret="aaaaa")) assert False # pragma: no cover except HTTPException as e: assert e.status_code == 403 assert e.detail == "This player doesn't belong to your team" - # player is in another game +def test_player_dep_get_another_game(game): with patch("model.Player.get") as mock: p = Player( player_id=1, game_id=2, team_id=1, is_active=True, player_name="name") mock.return_value = p try: - await player_dep(1, game=g, team=Team(team_id=1, team_name="team", team_secret="aaaaa")) + player_dep(1, game=game, team=Team(pk=1, team_name="team", team_secret="aaaaa")) assert False # pragma: no cover except HTTPException as e: assert e.status_code == 400 assert e.detail == "This player is in game 2" - # player is inactive +def test_player_dep_inactive_player(game): with patch("model.Player.get") as mock: p = Player( - player_id=1, game_id=1, team_id=1, is_active=False, player_name="name") + player_id=1, game_id=1, team_id=1, is_active=int(False), player_name="name") mock.return_value = p try: - await player_dep(1, game=g, team=Team(team_id=1, team_name="team", team_secret="aaaaa")) + player_dep(1, game=game, team=Team(pk=1, team_name="team", team_secret="aaaaa")) assert False # pragma: no cover except HTTPException as e: assert e.status_code == 400 assert e.detail == "This player is inactive or already has been deleted" - # valid player +def test_player_dep_get_valid(game): with patch("model.Player.get") as mock: p = Player( player_id=1, game_id=1, team_id=1, is_active=True, player_name="name") mock.return_value = p - assert await player_dep(1, game=g, team=Team(team_id=1, team_name="team", team_secret="aaaaa")) == p + assert player_dep(1, game=game, team=Team(pk=1, team_name="team", team_secret="aaaaa")) == p -@pytest.mark.asyncio -async def test_start_end_tick_dep(): +def test_start_end_tick_dep(): # game just started tests: g = Game( @@ -165,21 +160,21 @@ async def test_start_end_tick_dep(): is_contest=False, dataset_id=1, current_tick=0, tick_time=1000, total_ticks=10) try: - await start_end_tick_dep(g, None, None) + start_end_tick_dep(g, None, None) assert False # pragma: no cover except HTTPException as e: assert e.status_code == 400 assert e.detail == "Game just started (it is tick=0), no data to return" try: - await start_end_tick_dep(g, None, 10) + start_end_tick_dep(g, None, 10) assert False # pragma: no cover except HTTPException as e: assert e.status_code == 400 assert e.detail == "Game just started (it is tick=0), no data to return" try: - await start_end_tick_dep(g, 10, None) + start_end_tick_dep(g, 10, None) assert False # pragma: no cover except HTTPException as e: assert e.status_code == 400 @@ -191,69 +186,69 @@ async def test_start_end_tick_dep(): game_id=1, game_name="name", start_time=datetime.now(), is_finished=False, is_contest=False, dataset_id=1, current_tick=5, tick_time=1000, total_ticks=10) - start, end = await start_end_tick_dep(g, 0, 4) + start, end = start_end_tick_dep(g, 0, 4) assert start == 0 assert end == 4 - start, end = await start_end_tick_dep(g, None, None) + start, end = start_end_tick_dep(g, None, None) assert start == 4 assert end == 4 - start, end = await start_end_tick_dep(g, None, 3) + start, end = start_end_tick_dep(g, None, 3) assert start == 3 assert end == 3 - start, end = await start_end_tick_dep(g, 3, None) + start, end = start_end_tick_dep(g, 3, None) assert start == 3 assert end == 3 - start, end = await start_end_tick_dep(g, -1, None) + start, end = start_end_tick_dep(g, -1, None) assert start == 4 assert end == 4 - start, end = await start_end_tick_dep(g, None, -1) + start, end = start_end_tick_dep(g, None, -1) assert start == 4 assert end == 4 - start, end = await start_end_tick_dep(g, -2, -2) + start, end = start_end_tick_dep(g, -2, -2) assert start == 3 assert end == 3 - start, end = await start_end_tick_dep(g, -100, -100) + start, end = start_end_tick_dep(g, -100, -100) assert start == 0 assert end == 0 - start, end = await start_end_tick_dep(g, -100, None) + start, end = start_end_tick_dep(g, -100, None) assert start == 0 assert end == 0 - start, end = await start_end_tick_dep(g, None, -100) + start, end = start_end_tick_dep(g, None, -100) assert start == 0 assert end == 0 try: - await start_end_tick_dep(g, 5, None) + start_end_tick_dep(g, 5, None) assert False # pragma: no cover except HTTPException as e: assert e.status_code == 400 assert e.detail == "Start tick must be less than current tick (current_tick=5)" try: - await start_end_tick_dep(g, 5, 1000) + start_end_tick_dep(g, 5, 1000) assert False # pragma: no cover except HTTPException as e: assert e.status_code == 400 assert e.detail == "Start tick must be less than current tick (current_tick=5)" try: - await start_end_tick_dep(g, 4, -3) + start_end_tick_dep(g, 4, -3) assert False # pragma: no cover except HTTPException as e: assert e.status_code == 400 assert e.detail == "End tick must be greater than start tick" try: - await start_end_tick_dep(g, 1, 5) + start_end_tick_dep(g, 1, 5) assert False # pragma: no cover except HTTPException as e: assert e.status_code == 400 @@ -265,7 +260,7 @@ async def test_start_end_tick_dep(): max_ticks_in_request = config["dataset"]["max_ticks_in_request"] try: - await start_end_tick_dep(g, 0, max_ticks_in_request + 1) + start_end_tick_dep(g, 0, max_ticks_in_request + 1) assert False # pragma: no cover except HTTPException as e: assert e.status_code == 400 diff --git a/backend/routers/users/test_market.py b/backend/routers/users/test_market.py index 3637c4c..3333fbd 100644 --- a/backend/routers/users/test_market.py +++ b/backend/routers/users/test_market.py @@ -1,9 +1,9 @@ from fastapi import FastAPI -from unittest.mock import patch +from unittest.mock import MagicMock, patch from fastapi.testclient import TestClient -from model import Player, Market, Resource -from model.order_types import OrderStatus +from model import Player, Market, Resource, OrderSide, Order +from model.order_types import OrderStatus, OrderStatus from .market import router import pytest from fixtures.fixtures import * @@ -26,27 +26,27 @@ @pytest.fixture(scope="session", autouse=True) def mock_transaction(): - with patch("db.db.database.transaction") as mock_transaction: - yield mock_transaction + with patch("model.Player.lock") as mock: + yield mock def test_market_prices(): mock_start_end_tick_dep.call_count = 0 mock_check_game_active_dep.call_count = 0 override_game_dep.tick = 4 - with patch("model.Market.list_by_game_id_where_tick") as mock_list: + with patch("model.Market.find") as mock_list: mock_list.return_value = [ - Market(game_id=1, tick=1, resource=Resource.coal, low=10, high=20, + Market(game_id="1", tick=1, resource=Resource.COAL, low=10, high=20, open=15, close=18, market=15, volume=100), - Market(game_id=1, tick=2, resource=Resource.coal, low=10, high=20, + Market(game_id="1", tick=2, resource=Resource.COAL, low=10, high=20, open=15, close=18, market=15, volume=100), - Market(game_id=1, tick=3, resource=Resource.coal, low=10, high=20, + Market(game_id="1", tick=3, resource=Resource.COAL, low=10, high=20, open=15, close=18, market=15, volume=100), - Market(game_id=1, tick=1, resource=Resource.oil, low=10, high=20, + Market(game_id="1", tick=1, resource=Resource.OIL, low=10, high=20, open=15, close=18, market=15, volume=100), - Market(game_id=1, tick=2, resource=Resource.oil, low=10, high=20, + Market(game_id="1", tick=2, resource=Resource.OIL, low=10, high=20, open=15, close=18, market=15, volume=100), - Market(game_id=1, tick=3, resource=Resource.oil, low=10, high=20, + Market(game_id="1", tick=3, resource=Resource.OIL, low=10, high=20, open=15, close=18, market=15, volume=100), ] @@ -54,12 +54,11 @@ def test_market_prices(): assert response.status_code == 200, response.text assert mock_check_game_active_dep.call_count == 1 assert mock_start_end_tick_dep.call_count == 1 - assert len(response.json()["COAL"]) == 3 - assert len(response.json()["OIL"]) == 3 + assert len(response.json()[Resource.COAL.value]) == 3 + assert len(response.json()[Resource.OIL.value]) == 3 def test_energy_set_price_player(): - # OK mock_check_game_active_dep.call_count = 0 mock_player_dep.call_count = 0 override_game_dep.tick = 4 @@ -71,11 +70,10 @@ def test_energy_set_price_player(): assert mock_check_game_active_dep.call_count == 1 assert mock_player_dep.call_count == 1 mock_update.assert_called_once_with( - player_id=1, energy_price=10 ) - # Price <= 0 +def test_energy_set_price_player_negative_price(): mock_check_game_active_dep.call_count = 1 mock_player_dep.call_count = 0 override_game_dep.tick = 4 @@ -89,45 +87,41 @@ def test_energy_set_price_player(): assert mock_update.call_count == 0 -def test_order_list(): +def get_orders_list(order_response): + return list(ord for val in order_response.values() for ord_side in val.values() for ord in ord_side) - with patch("model.Order.list_orders_by_game_id") as mock_list, \ - patch("model.Order.list_bot_orders_by_game_id") as mock_bot_list, \ - patch("model.Order.list_best_orders_by_game_id") as mock_best_list: - - # return all orders - mock_list.return_value = [ - Order(game_id=1, order_id=1, player_id=1, price=10, size=10, tick=1, timestamp=datetime.now( - ), order_side=OrderSide.BUY, order_status=OrderStatus.ACTIVE, filled_size=0, expiration_tick=100, resource=Resource.coal), - Order(game_id=1, order_id=2, player_id=1, price=10, size=10, tick=1, timestamp=datetime.now( - ), order_side=OrderSide.SELL, order_status=OrderStatus.ACTIVE, filled_size=0, expiration_tick=100, resource=Resource.oil), - ] - - # return bot orders (koristimo i za buy i za sell) - mock_bot_list.return_value = [ - Order(game_id=1, order_id=3, player_id=1, price=10, size=10, tick=1, timestamp=datetime.now( - ), order_side=OrderSide.BUY, order_status=OrderStatus.ACTIVE, filled_size=0, expiration_tick=100, resource=Resource.coal), - Order(game_id=1, order_id=4, player_id=1, price=10, size=10, tick=1, timestamp=datetime.now( - ), order_side=OrderSide.SELL, order_status=OrderStatus.ACTIVE, filled_size=0, expiration_tick=100, resource=Resource.oil), - ] - - # return best orders (koristimo i za buy i za sell) - mock_best_list.return_value = [ - Order(game_id=1, order_id=5, player_id=1, price=10, size=10, tick=1, timestamp=datetime.now( - ), order_side=OrderSide.BUY, order_status=OrderStatus.ACTIVE, filled_size=0, expiration_tick=100, resource=Resource.coal), - Order(game_id=1, order_id=6, player_id=1, price=10, size=10, tick=1, timestamp=datetime.now( - ), order_side=OrderSide.SELL, order_status=OrderStatus.ACTIVE, filled_size=0, expiration_tick=100, resource=Resource.oil), - ] +def test_order_list_all_orders(): + with patch("model.Order.find") as mock_list: + mock_list.return_value = MagicMock() + mock_list.return_value.all = MagicMock( + return_value = [ + Order(game_id="1", pk="1", player_id="1", price=10, size=10, tick=1, timestamp=datetime.now( + ), order_side=OrderSide.BUY.value, order_status=OrderStatus.ACTIVE.value, filled_size=0, expiration_tick=100, resource=Resource.COAL.value), + Order(game_id="1", pk="2", player_id="1", price=10, size=10, tick=1, timestamp=datetime.now( + ), order_side=OrderSide.SELL.value, order_status=OrderStatus.ACTIVE.value, filled_size=0, expiration_tick=100, resource=Resource.OIL.value), + Order(game_id="1", pk="3", player_id="2", price=10, size=10, tick=1, timestamp=datetime.now( + ), order_side=OrderSide.SELL.value, order_status=OrderStatus.ACTIVE.value, filled_size=0, expiration_tick=100, resource=Resource.OIL.value), + ]) mock_check_game_active_dep.call_count = 0 override_game_dep.tick = 4 - # no argument, we get .list() as output response = client.get("/game/1/orders") assert response.status_code == 200, response.text assert mock_check_game_active_dep.call_count == 1 - assert len(sum(response.json().values(), [])) == 2 - assert "COAL" in response.json() + assert len(get_orders_list(response.json())) == 3 + assert Resource.COAL.value in response.json() + +def test_order_list_bot_orders(): + with patch("model.Order.find") as mock_list: + mock_list.return_value = MagicMock() + mock_list.return_value.all = MagicMock( + return_value = [ + Order(game_id="1", pk="3", player_id="1", price=10, size=10, tick=1, timestamp=datetime.now( + ), order_side=OrderSide.BUY.value, order_status=OrderStatus.ACTIVE.value, filled_size=0, expiration_tick=100, resource=Resource.COAL.value), + Order(game_id="1", pk="4", player_id="1", price=10, size=10, tick=1, timestamp=datetime.now( + ), order_side=OrderSide.SELL.value, order_status=OrderStatus.ACTIVE.value, filled_size=0, expiration_tick=100, resource=Resource.OIL.value), + ]) mock_check_game_active_dep.call_count = 0 override_game_dep.tick = 4 @@ -136,10 +130,20 @@ def test_order_list(): response = client.get("/game/1/orders?restriction=bot") assert response.status_code == 200, response.text assert mock_check_game_active_dep.call_count == 1 - assert len(sum(response.json().values(), [])) == 2 - assert "COAL" in response.json() - assert len(response.json()["COAL"]) == 1 - assert len(response.json()["OIL"]) == 1 + assert len(get_orders_list(response.json())) == 0 + assert Resource.COAL.value not in response.json() + assert Resource.OIL.value not in response.json() + +def test_order_list_best_orders(): + with patch("model.Order.find") as mock_list: + mock_list.return_value = MagicMock() + mock_list.return_value.all = MagicMock( + return_value = [ + Order(game_id="1", pk="5", player_id="1", price=10, size=10, tick=1, timestamp=datetime.now( + ), order_side=OrderSide.BUY.value, order_status=OrderStatus.ACTIVE.value, filled_size=0, expiration_tick=100, resource=Resource.COAL.value), + Order(game_id="1", pk="6", player_id="1", price=10, size=10, tick=1, timestamp=datetime.now( + ), order_side=OrderSide.SELL.value, order_status=OrderStatus.ACTIVE.value, filled_size=0, expiration_tick=100, resource=Resource.OIL.value), + ]) mock_check_game_active_dep.call_count = 0 override_game_dep.tick = 4 @@ -148,22 +152,26 @@ def test_order_list(): response = client.get("/game/1/orders?restriction=best") assert response.status_code == 200, response.text assert mock_check_game_active_dep.call_count == 1 - assert len(sum(response.json().values(), [])) == 4 - assert "COAL" in response.json() + assert len(get_orders_list(response.json())) == 2 + assert Resource.COAL.value in response.json() # cudno, ali jer ima sell + buy, a mi vratimo isto za oba - assert len(response.json()["COAL"]) == 2 - assert len(response.json()["OIL"]) == 2 + assert OrderSide.SELL.value not in response.json()[Resource.COAL.value] + assert OrderSide.BUY.value not in response.json()[Resource.OIL.value] + assert len(response.json()[Resource.COAL.value][OrderSide.BUY.value]) == 1 + assert len(response.json()[Resource.OIL.value][OrderSide.SELL.value]) == 1 def test_order_list_player(): - with patch("model.Order.list") as mock_list: - mock_list.return_value = [ - Order(game_id=1, order_id=1, player_id=1, price=10, size=10, tick=1, timestamp=datetime.now( - ), order_side=OrderSide.BUY, order_status=OrderStatus.ACTIVE, filled_size=0, expiration_tick=100, resource=Resource.coal), - Order(game_id=1, order_id=2, player_id=1, price=10, size=10, tick=1, timestamp=datetime.now( - ), order_side=OrderSide.SELL, order_status=OrderStatus.PENDING, filled_size=0, expiration_tick=100, resource=Resource.oil), - ] + with patch("model.Order.find") as mock_list: + mock_list.return_value = MagicMock() + mock_list.return_value.all = MagicMock( + return_value = [ + Order(game_id="1", pk="1", player_id="1", price=10, size=10, tick=1, timestamp=datetime.now( + ), order_side=OrderSide.BUY.value, order_status=OrderStatus.ACTIVE.value, filled_size=0, expiration_tick=100, resource=Resource.COAL.value), + Order(game_id="1", pk="2", player_id="1", price=10, size=10, tick=1, timestamp=datetime.now( + ), order_side=OrderSide.SELL.value, order_status=OrderStatus.PENDING.value, filled_size=0, expiration_tick=100, resource=Resource.OIL.value), + ]) mock_check_game_active_dep.call_count = 0 mock_player_dep.call_count = 0 @@ -174,18 +182,21 @@ def test_order_list_player(): assert mock_check_game_active_dep.call_count == 1 assert mock_player_dep.call_count == 1 - assert len(sum(response.json().values(), []) - ) == 4 # 2 active + 2 pending + # Orderi se uduplaju jer je mock isti za get pending i get active + assert len(get_orders_list(response.json())) == 4 - assert "COAL" in response.json() - assert len(response.json()["COAL"]) == 2 - assert len(response.json()["OIL"]) == 2 + assert Resource.COAL.value in response.json() + assert Resource.OIL.value in response.json() + assert OrderSide.BUY.value in response.json()[Resource.COAL.value] + assert OrderSide.SELL.value in response.json()[Resource.OIL.value] + assert len(response.json()[Resource.COAL.value][OrderSide.BUY.value]) == 2 + assert len(response.json()[Resource.OIL.value][OrderSide.SELL.value]) == 2 def test_order_get_player(): with patch("model.Order.get") as mock_get: - mock_get.return_value = Order(game_id=1, order_id=1, player_id=1, price=10, size=10, tick=1, timestamp=datetime.now( - ), order_side=OrderSide.BUY, order_status=OrderStatus.ACTIVE, filled_size=0, expiration_tick=100, resource=Resource.coal) + mock_get.return_value = Order(game_id="1", pk="1", player_id="1", price=10, size=10, tick=1, timestamp=datetime.now( + ), order_side=OrderSide.BUY.value, order_status=OrderStatus.ACTIVE.value, filled_size=0, expiration_tick=100, resource=Resource.COAL.value) mock_check_game_active_dep.call_count = 0 mock_player_dep.call_count = 0 @@ -196,35 +207,36 @@ def test_order_get_player(): assert response.status_code == 200, response.text assert mock_check_game_active_dep.call_count == 1 assert mock_player_dep.call_count == 1 - assert response.json()["order_id"] == 1 + assert response.json()["order_id"] == "1" -@pytest.mark.asyncio -async def test_order_create_player(): +def test_order_create_player_expiration_tick_not_set(): response = client.post("/game/1/player/1/orders/create", json={ - "resource": "COAL", + "resource": Resource.COAL.value, "price": 10, "size": 10, - "side": "BUY", + "side": "buy", }) assert response.status_code == 400, response.text +def test_order_create_player_expiration_tick_is_negative(): response = client.post("/game/1/player/1/orders/create", json={ - "resource": "COAL", + "resource": Resource.COAL.value, "price": 10, "size": 10, - "side": "BUY", - "expiration_length": -10 + "side": "buy", + "expiration_tick": -10 }) assert response.status_code == 400, response.text +def test_order_create_player_expiration_tick_is_zero(): response = client.post("/game/1/player/1/orders/create", json={ - "resource": "COAL", + "resource": Resource.COAL.value, "price": 10, "size": 10, - "side": "BUY", + "side": "buy", "expiration_tick": 0 }) diff --git a/backend/routers/users/test_player.py b/backend/routers/users/test_player.py index 9b05d9c..1876e57 100644 --- a/backend/routers/users/test_player.py +++ b/backend/routers/users/test_player.py @@ -1,14 +1,15 @@ from fastapi import FastAPI -from unittest.mock import patch +from unittest.mock import MagicMock, patch from fastapi.testclient import TestClient from model import Player +from model.player import Networth from .player import router import pytest from fixtures.fixtures import * from routers.users.dependencies import game_dep, team_dep, check_game_active_dep, player_dep -from routers.users.fixtures import mock_check_game_active_dep, override_game_dep, override_team_dep, mock_player_dep +from routers.users.fixtures import mock_check_game_active_dep, override_game_dep, override_team_dep, mock_player_dep, set_mock_find app = FastAPI() client = TestClient(app) @@ -23,118 +24,114 @@ @pytest.fixture(scope="session", autouse=True) def mock_transaction(): - with patch("db.db.database.transaction") as mock_transaction: + with patch("model.Player.lock") as mock_transaction: yield mock_transaction def test_player_list(): # mora vratiti samo is_active=True - with patch("routers.users.player.Player.list") as mock_list: - mock_list.return_value = [ - Player(player_id=1, game_id=1, team_id=1, + with patch("routers.users.player.Player.find") as mock_list: + set_mock_find(mock_list, 'all', [ + Player(pk="1", game_id="1", team_id="1", player_name="player_1", money=100), - ] + ]) - response = client.get(f"/game/1/player/list?team_secret=secret") + response = client.get("/game/1/player/list?team_secret=secret") assert response.status_code == 200 assert len(response.json()) == 1 - assert response.json()[0]["player_id"] == 1 + assert response.json()[0]["player_id"] == "1" def test_player_create_first(): - with patch("routers.users.player.Player.create") as mock_create, \ - patch("routers.users.player.Player.count") as mock_count: - mock_create.return_value = 1 - mock_count.return_value = 0 + with patch("routers.users.player.Player.find") as mock_count: + set_mock_find(mock_count, 'count', 0) - response = client.post(f"/game/1/player/create?team_secret=secret") + response = client.post("/game/1/player/create?team_secret=secret") assert response.status_code == 200 - assert response.json()["player_id"] == 1 + assert "player_id" in response.json() def test_player_create_second(): - with patch("routers.users.player.Player.create") as mock_create, \ - patch("routers.users.player.Player.count") as mock_count: - mock_create.return_value = 1 - mock_count.return_value = 1 + with patch("routers.users.player.Player.find") as mock_count: + set_mock_find(mock_count, 'count', 1) - response = client.post(f"/game/1/player/create?team_secret=secret") + response = client.post("/game/1/player/create?team_secret=secret") assert response.status_code == 400 assert response.json() == { "detail": "Only one player per team can be created in contest mode"} -def test_player_create_possible_bodies(): - # player_create: PlayerCreate | None | dict = None - with patch("routers.users.player.Player.create") as mock_create, \ - patch("routers.users.player.Player.count") as mock_count: - mock_create.return_value = 1 - mock_count.return_value = 0 +def test_player_create_no_body(): + with patch("routers.users.player.Player.find") as mock_count: + set_mock_find(mock_count, 'count', 0) - # no body - response = client.post(f"/game/1/player/create?team_secret=secret") + response = client.post("/game/1/player/create?team_secret=secret") assert response.status_code == 200 - assert mock_create.call_args.kwargs["player_name"] == "team_1_0" + assert response.json()["player_name"] == "team_1_0" - # empty dict - response = client.post( - f"/game/1/player/create?team_secret=secret", json={}) +def test_player_create_no_name(): + with patch("routers.users.player.Player.find") as mock_count: + set_mock_find(mock_count, 'count', 0) + response = client.post("/game/1/player/create?team_secret=secret", json={}) assert response.status_code == 200 - assert mock_create.call_args.kwargs["player_name"] == "team_1_0" + assert response.json()["player_name"] == "team_1_0" - # player_name in body +def test_player_create_player_name(): + with patch("routers.users.player.Player.find") as mock_count: + set_mock_find(mock_count, 'count', 0) response = client.post( - f"/game/1/player/create?team_secret=secret", json={"player_name": "player_1"}) + "/game/1/player/create?team_secret=secret", json={"player_name": "player_1"}) assert response.status_code == 200 - assert mock_create.call_args.kwargs["player_name"] == "player_1" + assert response.json()["player_name"] == "player_1" def test_player_get(): - # oponasamo pravi depencency, ali brojimo koliko puta je pozvan mock_check_game_active_dep.call_count = 0 with patch("routers.users.player.Player.get") as mock_get: + mock_get.return_value = Player( + pk="1", game_id="1", team_id="1", + player_name="player_1", money=100) - mock_get.return_value = Player(player_id=1, game_id=1, team_id=1, - player_name="player_1", money=100) - - response = client.get(f"/game/1/player/1?team_secret=secret") + response = client.get("/game/1/player/1?team_secret=secret") - assert mock_check_game_active_dep.call_count == 1 + # Za dohvatiti igraca, vise nemamo dependency da je igra upaljena + assert mock_check_game_active_dep.call_count == 0 assert response.status_code == 200 - assert response.json()["player_id"] == 1 + assert response.json()["player_id"] == "1" assert response.json()["player_name"] == "player_1" assert response.json()["money"] == 100 -def test_player_delete(): - # non contest mode - +def test_player_delete_non_contest_mode(): mock_player_dep.call_count = 0 - override_game_dep.contest = False + override_game_dep.contest = int(False) - with patch("routers.users.player.Player.update") as mock_update: - response = client.get(f"/game/1/player/1/delete?team_secret=secret") - assert mock_update.call_count == 1 + with patch("model.Player.update") as mock_update, \ + patch("model.Player.get") as mock_get, patch("model.Player.lock"): + # Player se dohvaca dvaput jer je jednom u transactionu + mock_get.return_value = Player(pk="1", game_id="1", team_id="1", player_name="player_1", money=100) + response = client.get("/game/1/player/1/delete?team_secret=secret") + assert response.status_code == 200, response.json() assert mock_player_dep.call_count == 1 assert response.status_code == 200 + assert mock_update.call_count == 1 - # contest mode - +def test_player_delete_contest_mode(): mock_player_dep.call_count = 0 - override_game_dep.contest = True + override_game_dep.contest = int(True) with patch("routers.users.player.Player.update") as mock_update: - response = client.get(f"/game/1/player/1/delete?team_secret=secret") + response = client.get("/game/1/player/1/delete?team_secret=secret") assert mock_update.call_count == 0 assert mock_player_dep.call_count == 1 @@ -144,18 +141,18 @@ def test_player_delete(): def test_player_net_worth(): - sample_return = { - "plants_owned": {"coal": {"owned": 1, "value_if_sold": 100}}, - "money": 100, - "resources": {"coal": {"coal": 100}}, - "total": 200 - } + sample_return = Networth() with patch("routers.users.player.Player.get_networth") as mock_get_networth: mock_get_networth.return_value = sample_return - response = client.get(f"/game/1/player/1/networth?team_secret=secret") + response = client.get("/game/1/player/1/networth?team_secret=secret") assert mock_get_networth.call_count == 1 assert response.status_code == 200 - assert response.json() == sample_return + assert response.json()["total"] == 0 + assert response.json()["money"] == 0 + assert "resources" in response.json() + assert "resources_value" in response.json() + assert "power_plants_owned" in response.json() + assert "power_plants_value" in response.json() diff --git a/backend/routers/users/test_power_plant.py b/backend/routers/users/test_power_plant.py index 5913fa8..01a5d45 100644 --- a/backend/routers/users/test_power_plant.py +++ b/backend/routers/users/test_power_plant.py @@ -4,6 +4,7 @@ from fastapi.testclient import TestClient from model import Player +from model.power_plant_model import PowerPlantsModel from .power_plant import router import pytest from fixtures.fixtures import * @@ -25,7 +26,7 @@ @pytest.fixture(scope="session", autouse=True) def mock_transaction(): - with patch("db.db.database.transaction") as mock_transaction: + with patch("model.Player.lock") as mock_transaction: yield mock_transaction @@ -35,7 +36,8 @@ def test_list_plants(): response = client.get("/game/1/player/1/plant/list") assert response.status_code == 200 - assert "COAL" in response.json() + assert "buy_price" in response.json() + assert "coal" in response.json()["buy_price"] assert mock_check_game_active_dep.call_count == 1 @@ -43,28 +45,27 @@ def test_buy_plant(): mock_check_game_active_dep.call_count = 0 with patch("model.Player.get") as mock_get, \ - patch("model.Player.update") as mock_update: - mock_get.return_value = Player(player_id=1, game_id=1, team_id=1, player_name="player_1", money=1e9, + patch("model.Player.save") as mock_update: + mock_get.return_value = Player(player_id="1", game_id="1", team_id="1", player_name="player_1", money=1e9, coal_plants_owned=0, oil_plants_owned=0, garbage_plants_owned=0, uranium_plants_owned=0) response = client.post( - "/game/1/player/1/plant/buy", json={"type": "COAL"}) + "/game/1/player/1/plant/buy", json={"type": "coal"}) assert mock_check_game_active_dep.call_count == 1 assert response.status_code == 200, response.text - assert mock_update.call_count == 2 + assert mock_update.call_count == 1 assert mock_get.call_count == 1 def test_buy_plant_not_enough_money(): mock_check_game_active_dep.call_count = 0 with patch("model.Player.get") as mock_get, \ - patch("model.Player.update") as mock_update: - mock_get.return_value = Player(player_id=1, game_id=1, team_id=1, player_name="player_1", money=0, - coal_plants_owned=0, oil_plants_owned=0, garbage_plants_owned=0, uranium_plants_owned=0) + patch("model.Player.save"): + mock_get.return_value = Player(player_id="1", game_id="1", team_id="1", player_name="player_1", money=0) response = client.post( - "/game/1/player/1/plant/buy", json={"type": "COAL"}) + "/game/1/player/1/plant/buy", json={"type": "coal"}) assert mock_check_game_active_dep.call_count == 1 assert response.status_code == 400, response.text @@ -74,29 +75,29 @@ def test_buy_plant_sell_plant(): mock_check_game_active_dep.call_count = 0 with patch("model.Player.get") as mock_get, \ - patch("model.Player.update") as mock_update: - mock_get.return_value = Player(player_id=1, game_id=1, team_id=1, player_name="player_1", money=1e9, - coal_plants_owned=1, oil_plants_owned=0, garbage_plants_owned=0, uranium_plants_owned=0) + patch("model.Player.save") as mock_update: + player = Player(player_id="1", game_id="1", team_id="1", player_name="player_1", money=1e9) + player.power_plants_owned.coal = 1 + mock_get.return_value = player response = client.post( - "/game/1/player/1/plant/sell", json={"type": "COAL"}) + "/game/1/player/1/plant/sell", json={"type": "coal"}) - assert mock_check_game_active_dep.call_count == 1 assert response.status_code == 200, response.text - assert mock_update.call_count == 1 + assert mock_check_game_active_dep.call_count == 1 assert mock_get.call_count == 1 + assert mock_update.call_count == 1 def test_buy_plant_sell_plant_no_plant(): mock_check_game_active_dep.call_count = 0 with patch("model.Player.get") as mock_get, \ - patch("model.Player.update") as mock_update: - mock_get.return_value = Player(player_id=1, game_id=1, team_id=1, player_name="player_1", money=1e9, - coal_plants_owned=0, oil_plants_owned=0, garbage_plants_owned=0, uranium_plants_owned=0) + patch("model.Player.save") as mock_update: + mock_get.return_value = Player(player_id="1", game_id="1", team_id="1", player_name="player_1", money=1e9) response = client.post( - "/game/1/player/1/plant/sell", json={"type": "COAL"}) + "/game/1/player/1/plant/sell", json={"type": "coal"}) assert mock_check_game_active_dep.call_count == 1 assert response.status_code == 400, response.text @@ -108,12 +109,13 @@ def test_turn_on(): mock_check_game_active_dep.call_count = 0 with patch("model.Player.get") as mock_get, \ - patch("model.Player.update") as mock_update: - mock_get.return_value = Player(player_id=1, game_id=1, team_id=1, player_name="player_1", money=1e9, - coal_plants_owned=1, oil_plants_owned=0, garbage_plants_owned=0, uranium_plants_owned=0) + patch("model.Player.save") as mock_update: + player = Player(player_id="1", game_id="1", team_id="1", player_name="player_1", money=1e9) + player.power_plants_owned.coal = 1 + mock_get.return_value = player response = client.post( - "/game/1/player/1/plant/on", json={"type": "COAL", "number": 1}) + "/game/1/player/1/plant/on", json={"type": "coal", "number": 1}) assert mock_check_game_active_dep.call_count == 1 assert response.status_code == 200, response.text @@ -125,12 +127,11 @@ def test_turn_on_no_plant(): mock_check_game_active_dep.call_count = 0 with patch("model.Player.get") as mock_get, \ - patch("model.Player.update") as mock_update: - mock_get.return_value = Player(player_id=1, game_id=1, team_id=1, player_name="player_1", money=1e9, - coal_plants_owned=0, oil_plants_owned=0, garbage_plants_owned=0, uranium_plants_owned=0) + patch("model.Player.save") as mock_update: + mock_get.return_value = Player(player_id="1", game_id="1", team_id="1", player_name="player_1", money=1e9) response = client.post( - "/game/1/player/1/plant/on", json={"type": "COAL", "number": 1}) + "/game/1/player/1/plant/on", json={"type": "coal", "number": 1}) assert mock_check_game_active_dep.call_count == 1 assert response.status_code == 400, response.text @@ -141,12 +142,13 @@ def test_turn_on_negative(): mock_check_game_active_dep.call_count = 0 with patch("model.Player.get") as mock_get, \ - patch("model.Player.update") as mock_update: - mock_get.return_value = Player(player_id=1, game_id=1, team_id=1, player_name="player_1", money=1e9, - coal_plants_owned=10, oil_plants_owned=0, garbage_plants_owned=0, uranium_plants_owned=0) - + patch("model.Player.save") as mock_update: + player = Player(player_id="1", game_id="1", team_id="1", player_name="player_1", money=1e9) + player.power_plants_owned.coal = 1 + mock_get.return_value = player + response = client.post( - "/game/1/player/1/plant/on", json={"type": "COAL", "number": -1}) + "/game/1/player/1/plant/on", json={"type": "coal", "number": -1}) assert mock_check_game_active_dep.call_count == 1 assert response.status_code == 400, response.text diff --git a/backend/run_migrations.py b/backend/run_migrations.py index d71dd4a..f467747 100644 --- a/backend/run_migrations.py +++ b/backend/run_migrations.py @@ -1,24 +1,27 @@ -from db import migration, database +import asyncio +from datetime import datetime, timedelta +import os +import psutil +from redis_om import Field, HashModel, Migrator +from db.fill_datasets import fill_datasets +from db.run_redis import create_teams_and_games, drop_tables +from game.tick.ticker import Ticker +from model import Game, Team, Order, Player, DatasetData, Datasets from config import config from logger import logger -async def main(): - await database.connect() +Migrator().run() - if config['testing']: - await migration.drop_tables() - await migration.run_migrations() - await migration.fill_dummy_tables() - else: - try: - await migration.run_migrations() - except Exception: - logger.warning("Migration script failed, dropping tables...") - await migration.drop_tables() - await migration.run_migrations() + +def run_all(): + if config["drop_tables"]: + drop_tables() + if config["fill_datasets"]: + fill_datasets() + if config["fill_tables"]: + create_teams_and_games() if __name__ == "__main__": - import asyncio - asyncio.run(main()) + run_all() diff --git a/backend/start.sh b/backend/start.sh index 2bd1ad4..eebba51 100644 --- a/backend/start.sh +++ b/backend/start.sh @@ -1,22 +1,27 @@ #!/bin/bash echo "Starting python virtual environment" -python -m venv .venv -python -m pip install --upgrade pip -nohup pip install -r requirements.txt > /dev/null 2>&1 & +# python -m venv .venv +# python -m pip install --upgrade pip +# nohup pip install -r requirements.txt > /dev/null 2>&1 & -echo "Running postgre server" -sudo service postgresql start +echo "TODO Running redis server" + + +if [[ "$TESTING" == 1 ]]; then + export DROP_TABLES=1 FILL_DATASETS=1 FILL_TABLES=1 +fi echo "Running migration script" python run_migrations.py -echo "Running redis server" -redis-server --daemonize yes +# echo "Running redis server" +# redis-server --daemonize yes if [[ "$TESTING" == 1 ]]; then -echo "Running main with reload" -TESTING=1 WATCHFILES_FORCE_POLLING=1 uvicorn main:app --reload --host=0.0.0.0 --port=3000 + echo "Running main with reload" + export WATCHFILES_FORCE_POLLING=1 + uvicorn main:app --reload --host=0.0.0.0 --port=3000 --log-level critical else -echo "Running uvicorn main with 4 workers" -TESTING=0 uvicorn main:app --workers 4 --host=0.0.0.0 --port=3000 + echo "Running uvicorn main with 4 workers" + uvicorn main:app --workers 4 --host=0.0.0.0 --port=3000 fi \ No newline at end of file diff --git a/scripts/make_teams.py b/scripts/make_teams.py index be3697f..cef746a 100644 --- a/scripts/make_teams.py +++ b/scripts/make_teams.py @@ -16,7 +16,7 @@ games = [ {"game_name": "game1", "contest": False, - "dataset_name": "df_2431_2011-11-06 03:30:00_2012-02-15 09:30:00.csv", + "dataset_name": "dataset.csv", "start_time": (datetime.now() + timedelta(seconds=5)).isoformat(), "total_ticks": 1000, "tick_time": 1000}, @@ -55,6 +55,7 @@ def list_games(): if __name__ == "__main__": datasets = get_datasets() + print(datasets) for game in games: assert any( @@ -71,6 +72,8 @@ def list_games(): ][0] del game["dataset_name"] + print(game) + make_game(game) list_games() diff --git a/testing/algotrade_api.py b/testing/algotrade_api.py index 2e43eab..f83e611 100644 --- a/testing/algotrade_api.py +++ b/testing/algotrade_api.py @@ -4,29 +4,29 @@ class Resource(Enum): - energy = "ENERGY" - coal = "COAL" - uranium = "URANIUM" - biomass = "BIOMASS" - gas = "GAS" - oil = "OIL" + energy = "energy" + coal = "coal" + uranium = "uranium" + biomass = "biomass" + gas = "gas" + oil = "oil" class PowerPlant(Enum): - COAL = "COAL" - URANIUM = "URANIUM" - BIOMASS = "BIOMASS" - GAS = "GAS" - OIL = "OIL" - GEOTHERMAL = "GEOTHERMAL" - WIND = "WIND" - SOLAR = "SOLAR" - HYDRO = "HYDRO" + COAL = "coal" + URANIUM = "uranium" + BIOMASS = "biomass" + GAS = "gas" + OIL = "oil" + GEOTHERMAL = "geothermal" + WIND = "wind" + SOLAR = "solar" + HYDRO = "hydro" class OrderSide(Enum): - BUY = "BUY" - SELL = "SELL" + BUY = "buy" + SELL = "sell" class AlgotradeApi: @@ -117,13 +117,12 @@ def create_order(self, resource, price, size, side, expiration_tick=None, expira return requests.post(f"http://{self.URL}/game/{self.game_id}/player/{self.player_id}/orders/create", params={"team_secret": self.team_secret}, json=body) - def cancel_orders(self, ids): - return requests.post(f"http://{self.URL}/game/{self.game_id}/player/{self.player_id}/orders/cancel", - params={"team_secret": self.team_secret}, - json={"ids": ids}) + def cancel_order(self, id): + return requests.get(f"http://{self.URL}/game/{self.game_id}/player/{self.player_id}/orders/{id}/cancel", + params={"team_secret": self.team_secret}) def get_trades(self, start_tick=None, end_tick=None, resource=None): - url = f"http://{URL}/game/{self.game_id}/player/{self.player_id}/trades" + url = f"http://{self.URL}/game/{self.game_id}/player/{self.player_id}/trades" params = {"team_secret": self.team_secret} if start_tick: params["start_tick"] = start_tick diff --git a/testing/bot1.py b/testing/bot1.py index 96095c6..714e843 100644 --- a/testing/bot1.py +++ b/testing/bot1.py @@ -10,8 +10,8 @@ url = "localhost:8000" -team_secret = "PWLQ0CET" -game_id = 1 +team_secret = "GUWAPRGW" +game_id = "01HV3TAMMQNK1TSF9RYP9W8VF5" player_id = -1 # we will get this later api = AlgotradeApi(url, team_secret, game_id, player_id) @@ -31,6 +31,7 @@ def play(): r = api.get_plant_prices() assert r.status_code == 200, r.text plant_prices = r.json() + pprint(plant_prices) # we set the energy price to sell our energy, if it's higher than the market, it won't sell energy_price = random.randint(400, 500) @@ -38,28 +39,30 @@ def play(): assert r.status_code == 200, r.text # we turn on as many plants as we can burn coal this turn - t = api.turn_on("COAL", player["coal_plants_owned"]) + r = api.turn_on( + "coal", player["power_plants_owned"]["coal"]) assert r.status_code == 200, r.text - print(f"Player COAL: {player['coal']}") + print(f"Player COAL: {player['resources']['coal']}") print(f"Player MONEY: {player['money']}") # if we can buy a plant we buy it (20000 is some slack to get resources) - if plant_prices["COAL"]["next_price"] + 2_000_000 <= player["money"]: - r = api.buy_plant("COAL") + if plant_prices["buy_price"]["COAL"] + 2_000_000 <= player["money"]: + r = api.buy_plant("coal") assert r.status_code == 200, r.text continue # if we can't buy a plant we buy resources - if player["coal"] < 30: + if player["resources"]["coal"] < 30: # list available market orders r = api.get_orders() assert r.status_code == 200, r.text - orders = r.json()["COAL"] + orders = r.json()["coal"] + pprint(orders) # filter for only sell orders - orders = [order for order in orders if order["order_side"] == "SELL"] + orders = [order for order in orders["sell"]] # find the cheapest order cheapest = sorted(orders, key=lambda x: x["price"])[0] @@ -67,7 +70,7 @@ def play(): size = cheapest["size"] # size is min what can be bought, we don't want to buy more than 50, and we can't buy more than we can afford - size = min(size, 50 - player["coal"], + size = min(size, 50 - player["resources"]["coal"], int(player["money"] // cheapest_price)) if size == 0: @@ -76,8 +79,8 @@ def play(): print("buying resources") print(f"Cheapest price: {cheapest_price}, size: {size}") - r = api.create_order("COAL", cheapest_price + 10, - size, "BUY", expiration_length=10) + r = api.create_order("coal", cheapest_price + 10, + size, "buy", expiration_length=10) assert r.status_code == 200, r.text continue diff --git a/testing/bot3.py b/testing/bot3.py index f69b7c9..e03057c 100644 --- a/testing/bot3.py +++ b/testing/bot3.py @@ -5,13 +5,14 @@ from pprint import pprint from datetime import datetime, timedelta +import algotrade_api from algotrade_api import AlgotradeApi url = "localhost:8000" -team_secret = "W6OKEA13" -game_id = 1 +team_secret = "F9OUM3LF" +game_id = "01HV3VG6RNF27TPR6QBPFSYET3" player_id = -1 # we will get this later api = AlgotradeApi(url, team_secret, game_id, player_id) @@ -20,61 +21,69 @@ def play(): while True: # tick time is 1 second - sleep(1) + sleep(0.9) # we get our player stats r = api.get_player() assert r.status_code == 200, r.text player = r.json() - print(f"Player COAL: {player['coal']}") - print(f"Player MONEY: {player['money']}") + # print(f"Player COAL: {player['resources']['coal']}") + print(f"{player['player_id']} money: {player['money']}") + # print(player['resources']) # list available market orders - r = api.get_orders() + r = api.get_orders(restriction="best") assert r.status_code == 200, r.text + # print(r.json()) + rjson = r.json() - orders = r.json()["COAL"] + for resource in algotrade_api.Resource: + try: + orders = rjson[resource.value] + best_order = orders["sell"][0] + # print(best_order) + best_price = best_order['price'] + best_size = best_order["size"] + except: + continue - # filter for only sell orders - orders = [order for order in orders if order["order_side"] == "SELL"] + print( + f"{player['player_id']} Buying {resource.value} price: {best_price}, size: {best_size}") - # find the cheapest order - cheapest = sorted(orders, key=lambda x: x["price"])[0] - cheapest_price = cheapest["price"] - size = cheapest["size"] + r = api.create_order("coal", best_price + 1000, + 1, "buy", expiration_length=10) + assert r.status_code == 200, r.text - print("buying resources") - print(f"Cheapest price: {cheapest_price}, size: {size}") - - r = api.create_order("COAL", cheapest_price + 1000, - 1, "BUY", expiration_length=10) - assert r.status_code == 200, r.text - - continue + continue def run(x): - # each game, we must create a new player - # in contest mode, we can make only one - r = api.create_player("bot1") - assert r.status_code == 200, r.text + games = api.get_games().json() + print(games) + game = games[0] - print("Player created") - pprint(r.json()) + api.set_game_id(game["game_id"]) - player_id = r.json()["player_id"] + print("Creating player") + response = api.create_player() + print(response.json()) + # pprint(response.json()) + player_id = response.json()["player_id"] api.set_player_id(player_id) + + print(api.get_players().json()) + play() def main(): - # with Pool(25) as p: - # p.map(run, range(25)) + with Pool(5) as p: + p.map(run, range(5)) - run(1) + # run(1) if __name__ == "__main__": diff --git a/testing/perfect_bot.py b/testing/perfect_bot.py index 8f459fa..1aa355f 100644 --- a/testing/perfect_bot.py +++ b/testing/perfect_bot.py @@ -58,7 +58,7 @@ start_money = 50_000_000 -monopole = 0.3 +monopole = 0.4 def get_plant_price(has_plants, resource): @@ -100,8 +100,8 @@ def main(): df = df[:1800] - # for resource in ["COAL", "URANIUM", "BIOMASS", "GAS", "OIL", "WIND", "SOLAR", "HYDRO"]: - for resource in ["WIND"]: + for resource in ["COAL", "URANIUM", "BIOMASS", "GAS", "OIL", "WIND", "SOLAR", "HYDRO"]: + # for resource in ["WIND"]: money = start_money burned_total = 0 has_plants = 0 @@ -113,8 +113,8 @@ def main(): max_energy_price = (x["MAX_ENERGY_PRICE"] * price_multiplier["energy"] // 1000000) - # energy_price = max_energy_price * 0.90 - energy_price = np.random.randint(400, 500) + energy_price = max_energy_price * 0.90 + # energy_price = np.random.randint(400, 500) energy_output = energy_output_multiplier[ resource.lower() @@ -147,7 +147,6 @@ def main(): print("Resource:", resource, "Has plants:", has_plants, "Networth/start:", get_networth(df, money, has_plants, resource) / start_money) - print()