From ef545c309fb12189bdf396738afd2a9bcb3b3119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Goran=20Ivankovi=C4=87?= <40663576+rangoiv@users.noreply.github.com> Date: Wed, 28 Feb 2024 21:51:16 +0100 Subject: [PATCH 1/2] API fixes, docs, rate limiter... (#50) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Nitko Nitkić --- backend/cli_player/admin_player.py | 257 ++++++++++++ backend/cli_player/player.py | 397 ++++++++++++++++++ backend/config.yaml | 3 + backend/db/__init__.py | 1 + backend/db/migration.py | 16 +- backend/db/rate_limit.py | 16 + backend/db/table.py | 19 +- backend/game/bots/resource_bot.py | 32 +- backend/game/fixtures/fixtures.py | 5 +- backend/game/market/energy_market.py | 3 +- backend/game/market/resource_market.py | 4 +- backend/game/price_tracker/price_tracker.py | 24 +- .../game/price_tracker/test_price_tracker.py | 10 +- backend/game/tick/test_ticker_bots.py | 24 +- .../game/tick/test_ticker_db_operations.py | 55 ++- .../tick/test_ticker_run_all_game_ticks.py | 27 -- .../tick/test_ticker_test_run_game_tick.py | 57 +-- backend/game/tick/tick_data.py | 13 +- backend/game/tick/ticker.py | 54 +-- backend/main.py | 19 +- backend/model/dataset_data.py | 18 + backend/model/enum_type.py | 31 +- backend/model/game.py | 1 - backend/model/market.py | 24 +- backend/model/order.py | 24 +- backend/model/order_types.py | 24 +- backend/model/player.py | 18 +- backend/model/resource.py | 12 +- backend/model/test_enum_type.py | 34 +- backend/requirements.txt | 3 +- backend/routers/admin/admin.py | 13 +- backend/routers/admin/bot.py | 14 - backend/routers/admin/dataset.py | 8 +- backend/routers/admin/game.py | 74 ++-- backend/routers/admin/player.py | 14 + backend/routers/admin/team.py | 38 +- backend/routers/model.py | 5 + backend/routers/users/dependencies.py | 40 +- backend/routers/users/game.py | 61 ++- backend/routers/users/market.py | 155 ++++--- backend/routers/users/player.py | 77 ++-- backend/routers/users/power_plant.py | 29 +- backend/start.sh | 7 +- backend/test.ipynb | 259 ++++++++++++ frontend/src/components/Smoke.jsx | 119 ------ frontend/src/views/Home.jsx | 14 +- 46 files changed, 1556 insertions(+), 596 deletions(-) create mode 100644 backend/cli_player/admin_player.py create mode 100644 backend/cli_player/player.py create mode 100644 backend/db/rate_limit.py delete mode 100644 backend/routers/admin/bot.py create mode 100644 backend/routers/admin/player.py create mode 100644 backend/routers/model.py create mode 100644 backend/test.ipynb diff --git a/backend/cli_player/admin_player.py b/backend/cli_player/admin_player.py new file mode 100644 index 0000000..612f2c3 --- /dev/null +++ b/backend/cli_player/admin_player.py @@ -0,0 +1,257 @@ +from datetime import datetime, timedelta +from pprint import pprint +import requests + +URL = "localhost:8000" +admin_secret = "mojkljuc" +game_id = "1" + + +def set_admin_secret(): + global admin_secret + + new_admin_secret = input("Enter new admin secret: ") + + if new_admin_secret: + admin_secret = new_admin_secret + print(f"Admin secret set to: {admin_secret}") + else: + print("Admin secret not set") + + +def set_game_id(): + global game_id + + new_game_id = input("Enter new game id: ") + + if new_game_id: + game_id = new_game_id + print(f"Game id set to: {game_id}") + else: + print("Game id not set") + + +def migrate(): + response = requests.get( + f"http://{URL}/admin/migrate", params={"admin_secret": admin_secret}) + + if response.status_code != 200: + print("Error " + response.status_code) + pprint(response.json()) + + +def list_datasets(): + response = requests.get( + f"http://{URL}/admin/dataset/list", params={"admin_secret": admin_secret}) + + if response.status_code != 200: + print("Error " + response.status_code) + pprint(response.json()) + + +def create_game(): + game_name = input("Enter game name: ") + contest = input("Is contest? (true/false): ") + + if contest not in ["true", "false"]: + print("Invalid input") + return + contest = contest == "true" + + dataset_id = input("Enter dataset id: ") + + start_time = input( + "Enter start time (YYYY-MM-DDTHH:MM:SS) or now_Xmin to start X mins from now: ") + if start_time.startswith("now_"): + start_time = int(start_time[4:-3]) + start_time = datetime.now() + timedelta(minutes=start_time) + else: + start_time = datetime.fromisoformat(start_time) + + total_ticks = int(input("Enter total ticks: ")) + tick_time = int(input("Enter tick time (ms): ")) + + response = requests.post( + f"http://{URL}/admin/game/create", + params={"admin_secret": admin_secret}, + json={ + "game_name": game_name, + "contest": contest, + "dataset_id": dataset_id, + "start_time": start_time.isoformat(), + "total_ticks": total_ticks, + "tick_time": tick_time + } + ) + + if response.status_code != 200: + print("Error " + response.status_code) + pprint(response.json()) + + +def list_games(): + response = requests.get( + f"http://{URL}/admin/game/list", params={"admin_secret": admin_secret}) + + if response.status_code != 200: + print("Error " + response.status_code) + pprint(response.json()) + else: + pprint(response.json()) + + +def list_players(): + response = requests.get( + f"http://{URL}/admin/game/{game_id}/player/list", params={"admin_secret": admin_secret}) + + if response.status_code != 200: + print("Error " + response.status_code) + pprint(response.json()) + + +def delete_game(): + global game_id + + response = requests.get( + f"http://{URL}/admin/game/{game_id}/delete", params={"admin_secret": admin_secret}) + + if response.status_code != 200: + print("Error " + response.status_code) + pprint(response.json()) + + +def edit_game(): + + game_name = input("Enter game name: ") + contest = input("Is contest? (true/false): ") + + if contest not in ["true", "false"]: + print("Invalid input") + return + + contest = contest == "true" + + bots = input("Enter bots: ") + dataset_id = input("Enter dataset id: ") + + start_time = input( + "Enter start time (YYYY-MM-DDTHH:MM:SS) or now_Xmin to start X mins from now: ") + + if start_time.startswith("now_"): + start_time = int(start_time[4:-3]) + start_time = datetime.now() + timedelta(minutes=start_time) + else: + start_time = datetime.fromisoformat(start_time) + + total_ticks = int(input("Enter total ticks: ")) + tick_time = int(input("Enter tick time (ms): ")) + + body = {} + + for key, value in [ + ("game_name", game_name), + ("contest", contest), + ("bots", bots), + ("dataset_id", dataset_id), + ("start_time", start_time.isoformat()), + ("total_ticks", total_ticks), + ("tick_time", tick_time) + ]: + if value: + body[key] = value + + response = requests.post( + f"http://{URL}/admin/game/{game_id}/edit", + params={"admin_secret": admin_secret}, + json=body + ) + + if response.status_code != 200: + print("Error " + response.status_code) + pprint(response.json()) + + +def create_team(): + team_name = input("Enter team name: ") + + response = requests.post( + f"http://{URL}/admin/team/create", + params={"admin_secret": admin_secret}, + json={"team_name": team_name} + ) + + if response.status_code != 200: + print("Error " + response.status_code) + pprint(response.json()) + + +def list_teams(): + response = requests.get( + f"http://{URL}/admin/team/list", params={"admin_secret": admin_secret}) + + if response.status_code != 200: + print("Error " + response.status_code) + pprint(response.json()) + + +def delete_team(): + team_id = input("Enter team id: ") + + response = requests.get( + f"http://{URL}/admin/team/{team_id}/delete", + params={"admin_secret": admin_secret} + ) + + if response.status_code != 200: + print("Error " + response.status_code) + pprint(response.json()) + + +def main(): + global admin_secret + + while True: + print() + print("Choose action:") + print(f"1. Set Admin Secret, current: {admin_secret}") + print(f"2. Set Game ID, current: {game_id}") + print(f"3. Migrate database") + print(f"4. List datasets") + print(f"5. Create game") + print(f"6. List games") + print(f"7. List players") + print(f"8. Delete game") + print(f"9. Edit game") + print(f"10. Create team") + print(f"11. List teams") + print(f"12. Delete team") + print(f"13. Exit") + print() + + action = input(">") + + case = { + "1": lambda: set_admin_secret(), + "2": lambda: set_game_id(), + "3": lambda: migrate(), + "4": lambda: list_datasets(), + "5": lambda: create_game(), + "6": lambda: list_games(), + "7": lambda: list_players(), + "8": lambda: delete_game(), + "9": lambda: edit_game(), + "10": lambda: create_team(), + "11": lambda: list_teams(), + "12": lambda: delete_team(), + "13": lambda: exit() + } + + try: + case[action]() + print() + except KeyError: + print("Invalid action") + + +if __name__ == "__main__": + main() diff --git a/backend/cli_player/player.py b/backend/cli_player/player.py new file mode 100644 index 0000000..69ba527 --- /dev/null +++ b/backend/cli_player/player.py @@ -0,0 +1,397 @@ +from pprint import pprint +import requests + +# GET +# /game/list +# Game List + + +# GET +# /game/{game_id}/dataset +# Dataset List + +# Player +# You will be able to + +# Create player in a game +# Get all created players in a game +# Get player info - player resources, money, power plants etc. + + +# GET +# /game/{game_id}/player/list +# Player List + + +# POST +# /game/{game_id}/player/create +# Player Create + + +# GET +# /game/{game_id}/player/{player_id} +# Player Get + + +# GET +# /game/{game_id}/player/{player_id}/delete +# Player Delete + +# Market +# You can: + +# Create buy and sell orders for resources that will be matched at the end of every tick. +# List your orders and cancel ones you do not longer need +# Set your price for produced electricity + + +# GET +# /game/{game_id}/market/prices +# Market Prices + + +# POST +# /game/{game_id}/player/{player_id}/energy/set_price +# Energy Set Price Player + + +# GET +# /game/{game_id}/orders +# Order List + + +# GET +# /game/{game_id}/player/{player_id}/orders +# Order List Player + + +# POST +# /game/{game_id}/player/{player_id}/orders/create +# Order Create Player + + +# POST +# /game/{game_id}/player/{player_id}/orders/cancel +# Order Cancel Player + +# Power plants +# You can: + +# Buy more power plants +# Sell power plants +# You can process at most the number of resources per tick as you have power plants of that type +# You can also buy and sell renewables that will produce energy passively + + +# GET +# /game/{game_id}/player/{player_id}/plant/list +# List Plants + + +# POST +# /game/{game_id}/player/{player_id}/plant/buy +# Buy Plant + + +# POST +# /game/{game_id}/player/{player_id}/plant/sell +# Sell Plant + + +# POST +# /game/{game_id}/player/{player_id}/plant/on +# Turn On + + +URL = "localhost:8000" + +team_secret = "" +game_id = "1" +player_id = "1" + + +def set_team_secret(): + global team_secret + + new_team_secret = input("Enter new team secret: ") + + if new_team_secret: + team_secret = new_team_secret + print(f"Team secret set to: {team_secret}") + else: + print("Team secret not set") + + +def set_game_id(): + global game_id + + new_game_id = input("Enter new game id: ") + + if new_game_id: + game_id = new_game_id + print(f"Game id set to: {game_id}") + else: + print("Game id not set") + + +def set_player_id(): + global player_id + + new_player_id = input("Enter new player id: ") + + if new_player_id: + player_id = new_player_id + print(f"Player id set to: {player_id}") + else: + print("Player id not set") + + +def list_games(): + response = requests.get( + f"http://{URL}/game/list", params={"team_secret": team_secret}) + + return response + + +def list_players(): + response = requests.get( + f"http://{URL}/game/{game_id}/player/list", params={"team_secret": team_secret}) + + return response + + +def create_player(): + player_name = input("Enter player name: ") + + response = requests.post(f"http://{URL}/game/{game_id}/player/create", + params={"team_secret": team_secret}, + json={"player_name": player_name}) + + return response + + +def get_player(): + response = requests.get(f"http://{URL}/game/{game_id}/player/{player_id}", + params={"team_secret": team_secret}) + + return response + + +def delete_player(): + response = requests.get(f"http://{URL}/game/{game_id}/player/{player_id}/delete", + params={"team_secret": team_secret}) + + return response + + +def list_orders(): + response = requests.get(f"http://{URL}/game/{game_id}/market/order/list", + params={"team_secret": team_secret}) + + return response + + +def list_prices(): + + # @router.get("/game/{game_id}/market/prices") + # async def market_prices(start_tick: int = Query(default=None), + # end_tick: int = Query(default=None), + # resource: Resource = Query(default=None), + # game: Game = Depends(game_dep)) -> Dict[Resource, List[MarketPricesResponse]]: + + start_tick = input("Enter start tick: ") + end_tick = input("Enter end tick: ") + + response = requests.get(f"http://{URL}/game/{game_id}/market/prices", + params={"team_secret": team_secret, "start_tick": start_tick, "end_tick": end_tick}) + + return response + + +def set_energy_price(): + price = input("Enter price: ") + + response = requests.post(f"http://{URL}/game/{game_id}/player/{player_id}/market/energy/set_price", + params={"team_secret": team_secret}, + json={"price": price}) + + return response + + +def create_order(): + available = ["COAL", "URANIUM", "BIOMASS", "GAS", "OIL", "ENERGY"] + print("Available resources: ", available) + resource = input("Enter resource: ") + if resource not in available: + print("Invalid resource") + return + + price = input("Enter price: ") + size = input("Enter size: ") + expiration_tick = input("Enter expiration tick: ") + side = input("Enter side (BUY/SELL): ") + if side not in ["BUY", "SELL"]: + print("Invalid side") + return + + order_type = input("Enter order type (LIMIT/MARKET): ") + if order_type not in ["LIMIT", "MARKET"]: + print("Invalid order type") + return + + response = requests.post(f"http://{URL}/game/{game_id}/player/{player_id}/market/order/create", + params={"team_secret": team_secret}, + json={ + "resource": resource, + "price": price, + "size": size, + "expiration_tick": expiration_tick, + "side": side, + "type": order_type + }) + + return response + + +def list_player_orders(): + response = requests.get(f"http://{URL}/game/{game_id}/player/{player_id}/market/order/list", + params={"team_secret": team_secret}) + + return response + + +def cancel_order(): + ids = input("Enter order ids (0,1,2): ") + ids = list(map(int, ids.split(","))) + + response = requests.post(f"http://{URL}/game/{game_id}/player/{player_id}/market/order/cancel", + params={"team_secret": team_secret}, + json={"ids": ids}) + + return response + + +def list_plants(): + response = requests.get(f"http://{URL}/game/{game_id}/player/{player_id}/plant/list", + params={"team_secret": team_secret}) + + return response + + +def buy_plant(): + type = input("Enter plant type: ") + response = requests.post(f"http://{URL}/game/{game_id}/player/{player_id}/plant/buy", + params={"team_secret": team_secret}, + json={"type": type}) + + return response + + +def sell_plant(): + type = input("Enter plant type: ") + response = requests.post(f"http://{URL}/game/{game_id}/player/{player_id}/plant/sell", + params={"team_secret": team_secret}, + json={"type": type}) + + return response + + +def turn_on_plant(): + type = input("Enter plant type: ") + response = requests.post(f"http://{URL}/game/{game_id}/player/{player_id}/plant/on", + params={"team_secret": team_secret}, + json={"type": type}) + + return response + + +def get_dataset(): + start_tick = input("Enter start tick: ") + end_tick = input("Enter end tick: ") + + response = requests.get(f"http://{URL}/game/{game_id}/dataset", + params={"team_secret": team_secret, "start_tick": start_tick, "end_tick": end_tick}) + + return response + + +def main(): + global team_secret, game_id, player_id + + while True: + try: + response = get_player() + if response.status_code != 200: + raise Exception( + f"Player fetching code: {response.status_code}\n{response.json()}") + except Exception as e: + print("Error when fetching player") + print("Please set game id, team secret and player id") + print("Error: ", e) + + print() + print("Choose action:") + print("Current game id: ", game_id) + print("Current team secret: ", team_secret) + print("Current player id: ", player_id) + print(f"1. Enter game id") + print(f"2. Enter team secret") + print(f"3. Enter player id") + print(f"4. List games") + print(f"5. List players") + print(f"6. Create player") + print(f"7. Get player") + print(f"8. Delete player") + print(f"9. List orders in market") + print(f"10. List prices in time period") + print(f"11. Set energy price for player") + print(f"12. Create order in market") + print(f"13. List player orders in market") + print(f"14. Cancel order in market") + print(f"15. List plants") + print(f"16. Buy plant") + print(f"17. Sell plant") + print(f"18. Turn on plant") + print(f"19. Dataset") + print(f"20. Exit") + print() + + action = input(">") + + case = { + "1": set_game_id, + "2": set_team_secret, + "3": set_player_id, + "4": list_games, + "5": list_players, + "6": create_player, + "7": get_player, + "8": delete_player, + "9": list_orders, + "10": list_prices, + "11": set_energy_price, + "12": create_order, + "13": list_player_orders, + "14": cancel_order, + "15": list_plants, + "16": buy_plant, + "17": sell_plant, + "18": turn_on_plant, + "29": get_dataset, + "20": lambda: exit() + } + + try: + response = case[action]() + + if response.status_code != 200: + print("Error " + response.status_code) + pprint(response.json()) + except Exception as e: + print("Error: ", e) + + print() + + +if __name__ == "__main__": + main() diff --git a/backend/config.yaml b/backend/config.yaml index 295e57d..343e946 100644 --- a/backend/config.yaml +++ b/backend/config.yaml @@ -4,6 +4,9 @@ server: datasets_path: ./data +redis: + port: 6379 + database: url: postgresql://postgres:postgres@localhost:5432/mydatabase diff --git a/backend/db/__init__.py b/backend/db/__init__.py index 637d4aa..268a19f 100644 --- a/backend/db/__init__.py +++ b/backend/db/__init__.py @@ -1,2 +1,3 @@ from .table import Table from .db import database +from .rate_limit import limiter diff --git a/backend/db/migration.py b/backend/db/migration.py index 3972520..7139ba2 100644 --- a/backend/db/migration.py +++ b/backend/db/migration.py @@ -19,7 +19,6 @@ async def fill_tables(): not_nat_game_id = await Game.create( game_name="Stalna igra", is_contest=False, - bots="dummy:3;resource_bot:1", dataset_id=dataset_id, start_time=datetime.now(), total_ticks=2300, @@ -27,7 +26,6 @@ async def fill_tables(): nat_game_id = await Game.create( game_name="Natjecanje", is_contest=True, - bots="dummy:2;resource_bot:1", dataset_id=dataset_id, start_time=datetime.now(), total_ticks=100, @@ -71,8 +69,7 @@ async def run_migrations(): game_id SERIAL PRIMARY KEY, game_name TEXT, is_contest BOOLEAN NOT NULL, - bots TEXT, - dataset_id int, + dataset_id INT, start_time TIMESTAMP NOT NULL, total_ticks INT NOT NULL, tick_time INT NOT NULL, @@ -129,15 +126,15 @@ async def run_migrations(): order_id SERIAL PRIMARY KEY, game_id INT NOT NULL, player_id INT NOT NULL, - order_type INT NOT NULL, - order_side INT NOT NULL, - order_status INT NOT NULL, + order_type TEXT NOT NULL, + order_side TEXT NOT NULL, + order_status TEXT NOT NULL, price INT NOT NULL, size INT NOT NULL, tick INT NOT NULL, timestamp TIMESTAMP NOT NULL, expiration_tick INT NOT NULL, - resource INT NOT NULL, + resource TEXT NOT NULL, filled_size INT NOT NULL DEFAULT 0, filled_money INT NOT NULL DEFAULT 0, @@ -152,12 +149,13 @@ async def run_migrations(): CREATE TABLE IF NOT EXISTS market ( game_id INT, tick INT, - resource INT, + resource TEXT, low INT, high INT, open INT, close INT, market INT, + volume INT, PRIMARY KEY (game_id, tick, resource) )''') diff --git a/backend/db/rate_limit.py b/backend/db/rate_limit.py new file mode 100644 index 0000000..51dcfb0 --- /dev/null +++ b/backend/db/rate_limit.py @@ -0,0 +1,16 @@ +from fastapi import Request +from slowapi import Limiter +from slowapi.util import get_remote_address +from config import config + + +def team_secret(request: Request): + param = request.query_params.get("team_secret") + if param is None: + return get_remote_address(request) + + return param + + +limiter = Limiter(key_func=team_secret, default_limits=["3/second"], + storage_uri=f"redis://localhost:{config['redis']['port']}/0") diff --git a/backend/db/table.py b/backend/db/table.py index cf30200..d211224 100644 --- a/backend/db/table.py +++ b/backend/db/table.py @@ -1,8 +1,9 @@ from typing import Any from databases import Database -from dataclasses import fields +from dataclasses import fields, asdict from .db import database from enum import Enum +from logger import logger class Table: @@ -36,6 +37,20 @@ async def update(cls, **kwargs) -> int: kwargs = _transform_kwargs(kwargs) return await database.fetch_val(query, kwargs) + @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: """ @@ -73,7 +88,7 @@ async def list(cls, **kwargs): """ query, values = cls._select(**kwargs) result = await database.fetch_all(query, values) - return [cls(**team) for team in result] + return [cls(**obj) for obj in result] @classmethod async def count(cls, **kwargs) -> int: diff --git a/backend/game/bots/resource_bot.py b/backend/game/bots/resource_bot.py index 8bb5475..0d362b0 100644 --- a/backend/game/bots/resource_bot.py +++ b/backend/game/bots/resource_bot.py @@ -1,3 +1,4 @@ +from typing import Dict, List import pandas as pd from game.tick.tick_data import TickData from model import Order, Resource, OrderSide, Team, Player @@ -18,7 +19,7 @@ class ResourceBot(Bot): - def __init__(self, *args, **kwargs): + def __init__(self, player_id=None, *args, **kwargs): super().__init__(*args, **kwargs) self.buy_prices = {resource: 50 for resource in Resource} self.sell_prices = {resource: 50 for resource in Resource} @@ -35,11 +36,11 @@ async def run(self, tick_data: TickData): is_bot=True ) self.game_id = tick_data.game.game_id - + if self.last_tick is not None and tick_data.game.current_tick < self.last_tick + expiration_ticks: return self.last_tick = tick_data.game.current_tick - + resources_sum = {resource: 0 for resource in Resource} for resource in Resource: for player in tick_data.players.values(): @@ -62,12 +63,15 @@ async def run(self, tick_data: TickData): buy_volume = clamp(min_volume, max_volume, int(buy_volume)) sell_volume = clamp(min_volume, max_volume, int(sell_volume)) - filled_buy_perc, filled_sell_perc = self.get_filled_perc(resource_orders) + filled_buy_perc, filled_sell_perc = self.get_filled_perc( + resource_orders) buy_price -= price_change_coeff * buy_price * (1-2*filled_buy_perc) - sell_price += price_change_coeff * sell_price * (1-2*filled_sell_perc) + sell_price += price_change_coeff * \ + sell_price * (1-2*filled_sell_perc) if buy_price >= sell_price: - price = (buy_price * buy_volume + sell_price * sell_volume) / (buy_volume + sell_volume) + price = (buy_price * buy_volume + sell_price * + sell_volume) / (buy_volume + sell_volume) buy_price = price sell_price = price buy_price = clamp(min_price, max_price, int(buy_price)) @@ -80,27 +84,29 @@ async def run(self, tick_data: TickData): self.buy_prices[resource] = buy_price self.sell_prices[resource] = sell_price - def get_filled_perc(self, orders: list[Order]): + def get_filled_perc(self, orders: List[Order]): size = {side: 0 for side in OrderSide} filled_size = {side: 0 for side in OrderSide} for order in orders: size[order.order_side] += order.size filled_size[order.order_side] += order.filled_size - filled_perc = {side: filled_size[side] / size[side] + filled_perc = {side: filled_size[side] / size[side] if size[side] > 0 else 0 for side in OrderSide} return filled_perc[OrderSide.BUY], filled_perc[OrderSide.SELL] - async def get_last_orders(self) -> dict[str, Order]: - if self.last_tick is None: return [] + async 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 = {resource: [] for resource in Resource} for order in orders_list: orders[order.resource].append(order) return orders - + async def create_orders(self, tick, resource, buy_price, sell_price, buy_volume, sell_volume) -> None: - logger.debug(f"({self.game_id}) Bot creating orders {tick=}, {resource=}, {buy_price=}, {sell_price=}, {buy_volume=}, {sell_volume=}") + logger.debug( + f"({self.game_id}) Bot creating orders {tick=}, {resource=}, {buy_price=}, {sell_price=}, {buy_volume=}, {sell_volume=}") await Order.create( game_id=self.game_id, player_id=self.player_id, @@ -126,4 +132,4 @@ async def create_orders(self, tick, resource, buy_price, sell_price, buy_volume, def clamp(_min, _max, x): - return max(_min, min(_max, x)) \ No newline at end of file + return max(_min, min(_max, x)) diff --git a/backend/game/fixtures/fixtures.py b/backend/game/fixtures/fixtures.py index f65ce29..fb527c6 100644 --- a/backend/game/fixtures/fixtures.py +++ b/backend/game/fixtures/fixtures.py @@ -1,3 +1,4 @@ +from typing import Dict, List import pandas as pd from game.tick import TickData, Ticker from game.tick.ticker import GameData @@ -172,13 +173,13 @@ def get_order(player_id: int, price: int, size: int, order_side: OrderSide, tick return get_order -def get_player_dict(players: list[Player]) -> dict[int, Player]: +def get_player_dict(players: List[Player]) -> Dict[int, Player]: return {player.player_id: player for player in players} @pytest.fixture def coal_market(): - def get_coal_market(players: dict[int, Player] = {}) -> ResourceMarket: + def get_coal_market(players: Dict[int, Player] = {}) -> ResourceMarket: return ResourceMarket(Resource.coal, players) return get_coal_market diff --git a/backend/game/market/energy_market.py b/backend/game/market/energy_market.py index d35fe72..5a78f5c 100644 --- a/backend/game/market/energy_market.py +++ b/backend/game/market/energy_market.py @@ -1,3 +1,4 @@ +from typing import Dict from model import Trade, Resource from model.order import Order from config import config @@ -5,7 +6,7 @@ class EnergyMarket: - def match(self, players: dict[int, Player], demand: int, max_price: int) -> dict[int, int]: + def match(self, players: Dict[int, Player], demand: int, max_price: int) -> Dict[int, int]: players_sorted = sorted(players.values(), key=lambda x: x.energy_price) players_sorted = [ player for player in players_sorted if player.energy_price <= max_price] diff --git a/backend/game/market/resource_market.py b/backend/game/market/resource_market.py index 0c4ac7c..b5e704e 100644 --- a/backend/game/market/resource_market.py +++ b/backend/game/market/resource_market.py @@ -1,5 +1,5 @@ from pprint import pprint -from typing import List +from typing import List, Dict from game.orderbook.orderbook import OrderBook from game.price_tracker.price_tracker import PriceTracker from model import Resource, Trade, Order @@ -7,7 +7,7 @@ class ResourceMarket: - def __init__(self, resource: Resource, players: dict[int, Player]): + def __init__(self, resource: Resource, players: Dict[int, Player]): self.resource = resource self.orderbook = OrderBook() self.price_tracker = PriceTracker(self.orderbook) diff --git a/backend/game/price_tracker/price_tracker.py b/backend/game/price_tracker/price_tracker.py index 414ae5c..94e1f1e 100644 --- a/backend/game/price_tracker/price_tracker.py +++ b/backend/game/price_tracker/price_tracker.py @@ -1,3 +1,4 @@ +from typing import List from game.orderbook import OrderBook from model import Trade @@ -8,21 +9,22 @@ def __init__(self, orderbook: OrderBook): self.last_low = 0 self.last_open = 0 self.last_close = 0 - self.last_market = 0 + self.last_average = 0 + self.volume = 0 self.high = None self.low = None self.open = None self.close = None - self.market = None + self.average = None orderbook.register_callback('on_end_match', self._calculate_low_high) - def _calculate_low_high(self, trades: list[Trade]): + def _calculate_low_high(self, trades: List[Trade]): self.high = None self.low = None self.open = None self.close = None - self.market = None + self.average = None money_sum = 0 money_size = 0 @@ -47,13 +49,14 @@ def _calculate_low_high(self, trades: list[Trade]): self.low = price if money_size > 0: - self.market = money_sum / money_size + self.average = money_sum / money_size + self.volume = money_size self._save_last() def _save_last(self): - if self.market is not None: - self.last_market = self.market + if self.average is not None: + self.last_average = self.average self.last_high = self.high self.last_low = self.low self.last_open = self.open @@ -65,11 +68,14 @@ def get_low(self): def get_high(self): return self.high if self.high is not None else self.last_high - def get_market(self): - return self.market if self.market is not None else self.last_market + def get_average(self): + return self.average if self.average is not None else self.last_average def get_open(self): return self.open if self.open is not None else self.last_open def get_close(self): return self.close if self.close is not None else self.last_close + + def get_volume(self): + return self.volume diff --git a/backend/game/price_tracker/test_price_tracker.py b/backend/game/price_tracker/test_price_tracker.py index 7ea7934..5ba9974 100644 --- a/backend/game/price_tracker/test_price_tracker.py +++ b/backend/game/price_tracker/test_price_tracker.py @@ -28,7 +28,7 @@ def test_price_tracker(get_order, get_timestamp): assert price_tracker.get_high() == 15 assert price_tracker.get_low() == 3 - assert price_tracker.get_market() == 8.25 + assert price_tracker.get_average() == 8.25 assert price_tracker.get_open() == 5 assert price_tracker.get_close() == 3 @@ -41,7 +41,7 @@ def test_price_tracker(get_order, get_timestamp): assert price_tracker.get_high() == 30 assert price_tracker.get_low() == 30 - assert price_tracker.get_market() == 30 + assert price_tracker.get_average() == 30 assert price_tracker.get_open() == 30 assert price_tracker.get_close() == 30 @@ -62,7 +62,7 @@ def test_price_tracker_market_weighted(get_order, get_timestamp): assert price_tracker.get_high() == 25 assert price_tracker.get_low() == 5 - assert price_tracker.get_market() == 20 + assert price_tracker.get_average() == 20 def test_price_tracker_market_no_trades(get_order, get_timestamp): @@ -76,7 +76,7 @@ def test_price_tracker_market_no_trades(get_order, get_timestamp): orderbook.match(timestamp=get_timestamp(1)) assert len(orderbook.match_trades) == 1 - assert price_tracker.get_market() == 5 + assert price_tracker.get_average() == 5 orderbook.match(timestamp=get_timestamp(1)) assert len(orderbook.match_trades) == 0 - assert price_tracker.get_market() == 5 + assert price_tracker.get_average() == 5 diff --git a/backend/game/tick/test_ticker_bots.py b/backend/game/tick/test_ticker_bots.py index d5c3116..d2398eb 100644 --- a/backend/game/tick/test_ticker_bots.py +++ b/backend/game/tick/test_ticker_bots.py @@ -5,11 +5,11 @@ from model import Game from game.bots import DummyBot, ResourceBot from game.fixtures.fixtures import * +from model.team import Team @pytest.mark.asyncio async def test_run_bots(get_tick_data): - # Create sample game game = Game( game_id=1, game_name="Sample Game", @@ -28,22 +28,14 @@ async def test_run_bots(get_tick_data): 3: MagicMock() } - # Create sample bots - bots = [DummyBot(), DummyBot(), DummyBot()] - # Mock the Bot.run method with patch.object(DummyBot, 'run') as mock_run: - # Create a Ticker instance - ticker = Ticker() + with patch.object(ResourceBot, 'run') as mock_run: + ticker = Ticker() - # Set the bots for the game - ticker.game_data[game.game_id] = GameData(game, players) - tick_data = get_tick_data(markets={}, players={}) + ticker.game_data[game.game_id] = GameData(game, players) + tick_data = get_tick_data(markets={}, players={}) - # Run the method being tested - await ticker.run_bots(tick_data) + await ticker.run_bots(tick_data) - # Assertions - # Ensure Bot.run is called once for each bot - assert mock_run.call_count == len(bots) - # Ensure Bot.run is called with no arguments - mock_run.assert_called_with(tick_data) + assert mock_run.call_count == 1 + mock_run.assert_called_with(tick_data) diff --git a/backend/game/tick/test_ticker_db_operations.py b/backend/game/tick/test_ticker_db_operations.py index d0c5253..720f75e 100644 --- a/backend/game/tick/test_ticker_db_operations.py +++ b/backend/game/tick/test_ticker_db_operations.py @@ -1,22 +1,21 @@ +from pprint import pprint import pandas as pd +from game.price_tracker.price_tracker import PriceTracker import pytest -from unittest.mock import patch +from unittest.mock import MagicMock, patch, call from datetime import datetime -from model import Order, OrderStatus, Resource -from model.dataset_data import DatasetData -from model.order_types import OrderSide, OrderType +from game.market.resource_market import ResourceMarket +from model import Order, OrderStatus, Resource, OrderSide, OrderType from game.tick import Ticker, TickData from tick.test_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): - # Setup ticker ticker = Ticker() ticker.game_data[sample_game.game_id] = GameData( sample_game, sample_players) - # Mocking database interaction async def mock_list_players(*args, **kwargs): return [sample_players[1], sample_players[2]] @@ -33,7 +32,6 @@ async def mock_get_dataset_data(*args, **kwargs): # Execute get_tick_data method tick_data = await ticker.get_tick_data(sample_game) - # Assertions assert len(tick_data.players) == 2 # Assuming 2 pending orders in sample_pending_orders fixture assert len(tick_data.pending_orders) == 2 @@ -54,10 +52,14 @@ def sample_update_orders(): } -@patch('model.Player.update') -@patch('model.Order.update') +@patch('model.Player.update_many') +@patch('model.Order.update_many') @pytest.mark.asyncio -async def test_save_tick_data(mock_order_update, mock_player_update, ticker, sample_game, sample_players, sample_pending_orders, sample_user_cancelled_orders, sample_dataset_row, sample_update_orders): +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_update_orders): tick_data = TickData( game=sample_game, players=sample_players, @@ -71,8 +73,8 @@ async def test_save_tick_data(mock_order_update, mock_player_update, ticker, sam await ticker.save_tick_data(tick_data) - assert mock_player_update.call_count == len(sample_players) - assert mock_order_update.call_count == len(sample_update_orders) + assert mock_player_update_many.call_count == 1 + assert mock_order_update_many.call_count == 1 @pytest.mark.asyncio @@ -110,3 +112,32 @@ async def test_save_electricity_orders(sample_game, sample_players): assert kwargs["filled_price"] == players[kwargs["player_id"]].energy_price assert kwargs["expiration_tick"] == 1 assert kwargs["resource"] == Resource.energy.value + + +@pytest.mark.asyncio +async def test_save_market_data(ticker: Ticker, sample_game, tick_data, sample_players): + for resource in Resource: + price_tracker_mock: PriceTracker = MagicMock() + price_tracker_mock.get_low.return_value = 50 + price_tracker_mock.get_high.return_value = 60 + price_tracker_mock.get_open.return_value = 45 + price_tracker_mock.get_close.return_value = 55 + price_tracker_mock.get_average.return_value = 70 + price_tracker_mock.get_volume.return_value = 20 + + tick_data.markets[resource.value] = ResourceMarket( + resource, sample_players) + 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) + + 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 + ] + + mock_create.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 0a921d4..0ae725f 100644 --- a/backend/game/tick/test_ticker_run_all_game_ticks.py +++ b/backend/game/tick/test_ticker_run_all_game_ticks.py @@ -60,33 +60,6 @@ async def test_run_all_game_ticks_game_started(): mock_run_game_tick.assert_called_once_with(game) -# if self.game_data.get(game.game_id) is None: -# self.game_data[game.game_id] = GameData(game, {}) - - -@pytest.mark.asyncio -async def test_run_all_game_ticks_game_data_not_exist(): - # Prepare - game = Game(game_id=4, game_name="Test Game 4", start_time=datetime.now() - timedelta(hours=1), - current_tick=1, total_ticks=10, is_finished=False, dataset_id=1, bots="", tick_time=1000, is_contest=False) - ticker = Ticker() - - # Execute - with patch.object(Database, 'transaction') as mock_transaction: - with patch.object(Database, 'execute') as mock_execute: - with patch.object(Game, 'list') as mock_game_list: - mock_game_list.return_value = [game] - with patch.object(Ticker, 'run_game_tick') as mock_run_game_tick: - await ticker.run_all_game_ticks() - - # Verify - assert game.game_id in ticker.game_data - mock_run_game_tick.assert_called_once_with(game) - -# if game.is_finished: -# logger.info(f" {game.game_name} is finished") -# continue - @pytest.mark.asyncio async def test_run_all_game_ticks_game_finished(): 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 9bd062d..337dc11 100644 --- a/backend/game/tick/test_ticker_test_run_game_tick.py +++ b/backend/game/tick/test_ticker_test_run_game_tick.py @@ -5,34 +5,35 @@ from tick.test_tick_fixtures import * -# @pytest.mark.asyncio -# async def test_run_game_tick( -# sample_game, sample_players, tick_data, -# sample_pending_orders, -# sample_user_cancelled_orders, sample_dataset_row -# ): -# with patch.object(Ticker, 'get_tick_data', return_value=tick_data), \ -# patch.object(Ticker, 'run_markets'), \ -# patch.object(Ticker, 'run_power_plants'), \ -# patch.object(Ticker, 'run_electricity_market', return_value=(tick_data, {})), \ -# patch.object(Ticker, 'save_electricity_orders'), \ -# patch.object(Ticker, 'save_tick_data'), \ -# patch.object(Game, 'update'), \ -# patch.object(Ticker, 'run_bots'): +@pytest.mark.asyncio +async def test_run_game_tick( + sample_game, sample_players, tick_data, + sample_pending_orders, + sample_user_cancelled_orders, sample_dataset_row +): + with patch.object(Ticker, 'get_tick_data', return_value=tick_data), \ + patch.object(Ticker, 'run_markets'), \ + patch.object(Ticker, 'run_power_plants'), \ + patch.object(Ticker, 'run_electricity_market', return_value=(tick_data, {})), \ + patch.object(Ticker, 'save_electricity_orders'), \ + patch.object(Ticker, 'save_tick_data'), \ + patch.object(Ticker, 'save_market_data'), \ + patch.object(Game, 'update'), \ + patch.object(Ticker, 'run_bots'): -# ticker = Ticker() -# ticker.game_data[sample_game.game_id] = GameData( -# sample_game, sample_players) + ticker = Ticker() + ticker.game_data[sample_game.game_id] = GameData( + sample_game, sample_players) -# await ticker.run_game_tick(sample_game) + await ticker.run_game_tick(sample_game) -# # Assertions -# 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 + 1) -# Ticker.run_bots.assert_called_once_with(tick_data) + # Assertions + 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 + 1) + Ticker.run_bots.assert_called_once_with(tick_data) diff --git a/backend/game/tick/tick_data.py b/backend/game/tick/tick_data.py index 1196df0..8831cef 100644 --- a/backend/game/tick/tick_data.py +++ b/backend/game/tick/tick_data.py @@ -1,4 +1,5 @@ from dataclasses import dataclass, field +from typing import Dict, List from model import Player, Game, Order from game.market import ResourceMarket from game.bots.bot import Bot @@ -7,12 +8,12 @@ @dataclass class TickData: game: Game - players: dict[int, Player] - markets: dict[int, ResourceMarket] - bots: list[Bot] + players: Dict[int, Player] + markets: Dict[int, ResourceMarket] + bots: List[Bot] dataset_row: dict = field(default_factory=dict) - 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) + 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) diff --git a/backend/game/tick/ticker.py b/backend/game/tick/ticker.py index 50bbe30..eaa38de 100644 --- a/backend/game/tick/ticker.py +++ b/backend/game/tick/ticker.py @@ -1,7 +1,7 @@ -import dataclasses +import traceback from datetime import datetime from pprint import pprint -from typing import Tuple +from typing import Dict, Tuple import pandas as pd from model import Player, PowerPlantType, Game, Order, OrderStatus, Resource, DatasetData, OrderSide, OrderType from game.market import ResourceMarket, EnergyMarket @@ -13,18 +13,18 @@ class GameData: - def __init__(self, game: Game, players: dict[int, Player]): - self.markets: dict[int, ResourceMarket] = { + def __init__(self, game: Game, players: Dict[int, Player]): + self.markets: Dict[int, ResourceMarket] = { resource.value: ResourceMarket(resource, players) for resource in Resource } self.energy_market = EnergyMarket() - self.bots = Bots.create_bots(game.bots) + self.bots = Bots.create_bots("resource_bot:1") class Ticker: def __init__(self): - self.game_data: dict[int, GameData] = {} + self.game_data: Dict[int, GameData] = {} async def run_all_game_ticks(self): games = await Game.list() @@ -38,28 +38,28 @@ async def run_all_game_ticks(self): if game.current_tick >= game.total_ticks: try: + logger.info( + f"Ending game ({game.game_id}) {game.game_name}") await Game.update(game_id=game.game_id, is_finished=True) if self.game_data.get(game.game_id) is not None: del self.game_data[game.game_id] - logger.info( - f"Finished game ({game.game_id}) {game.game_name}") except Exception as e: logger.critical( - f"Failed finishing game ({game.game_id}) {game.current_tick} with error: " + str(e)) + f"Failed ending game ({game.game_id}) (tick {game.current_tick}) with error:\n{traceback.format_exc()}") continue if self.game_data.get(game.game_id) is None: try: logger.info( f"Starting game ({game.game_id}) {game.game_name}") + await self.delete_all_running_bots(game.game_id) self.game_data[game.game_id] = GameData(game, {}) except Exception as e: logger.critical( - f"Failed creating game ({game.game_id}) {game.current_tick} with error: " + str(e)) + f"Failed creating game ({game.game_id}) (tick {game.current_tick}) with error:\n{traceback.format_exc()}") continue try: - async with database.transaction(): await database.execute( f"LOCK TABLE orders, players IN SHARE ROW EXCLUSIVE MODE") @@ -68,7 +68,13 @@ async def run_all_game_ticks(self): except Exception as e: logger.critical( - f"({game.game_id}) {game.game_name} tick {game.current_tick} failed with error: " + str(e)) + f"({game.game_id}) {game.game_name} (tick {game.current_tick}) failed with error:\n{traceback.format_exc()}") + + async def delete_all_running_bots(self, game_id: int): + bots = await Player.list(game_id=game_id, is_bot=True) + + for bot in bots: + await Player.update(player_id=bot.player_id, is_active=False) async def run_game_tick(self, game: Game): logger.debug( @@ -153,13 +159,13 @@ def run_power_plants(self, tick_data: TickData): return tick_data - def run_electricity_market(self, tick_data: TickData, energy_market: EnergyMarket) -> Tuple[TickData, dict[int, int]]: + def run_electricity_market(self, tick_data: TickData, energy_market: EnergyMarket) -> Tuple[TickData, Dict[int, int]]: energy_sold = energy_market.match( tick_data.players, tick_data.dataset_row.energy_demand, tick_data.dataset_row.max_energy_price) return tick_data, energy_sold - async def save_electricity_orders(self, players: dict[int, Player], game: Game, energy_sold: dict[int, int], tick: int): + async def save_electricity_orders(self, players: Dict[int, Player], game: Game, energy_sold: Dict[int, int], tick: int): for player_id, energy in energy_sold.items(): await Order.create( game_id=game.game_id, @@ -179,11 +185,8 @@ async def save_electricity_orders(self, players: dict[int, Player], game: Game, ) async def save_tick_data(self, tick_data: TickData): - for player in tick_data.players.values(): - await Player.update(**dataclasses.asdict(player)) - - for order in tick_data.updated_orders.values(): - await Order.update(**dataclasses.asdict(order)) + await Player.update_many(tick_data.players.values()) + await Order.update_many(tick_data.updated_orders.values()) async def save_market_data(self, tick_data: TickData): tick = tick_data.game.current_tick @@ -195,14 +198,11 @@ async def save_market_data(self, tick_data: TickData): tick=tick, resource=resource.value, low=tick_data.markets[resource.value].price_tracker.get_low(), - high=tick_data.markets[resource.value].price_tracker.get_high( - ), - open=tick_data.markets[resource.value].price_tracker.get_open( - ), - close=tick_data.markets[resource.value].price_tracker.get_close( - ), - market=tick_data.markets[resource.value].price_tracker.get_market( - ) + high=tick_data.markets[resource.value].price_tracker.get_high(), + open=tick_data.markets[resource.value].price_tracker.get_open(), + close=tick_data.markets[resource.value].price_tracker.get_close(), + market=tick_data.markets[resource.value].price_tracker.get_average(), + volume=tick_data.markets[resource.value].price_tracker.get_volume() ) async def run_bots(self, tick_data: TickData): diff --git a/backend/main.py b/backend/main.py index f707096..334e3ef 100644 --- a/backend/main.py +++ b/backend/main.py @@ -5,17 +5,17 @@ from fastapi.responses import JSONResponse from contextlib import asynccontextmanager from config import config -from db import database, migration +from db import database from game.tick import Ticker from routers import admin_router, users_router import psutil import os from logger import logger -from slowapi import Limiter, _rate_limit_exceeded_handler -from slowapi.util import get_remote_address +from slowapi import _rate_limit_exceeded_handler from slowapi.middleware import SlowAPIMiddleware from slowapi.errors import RateLimitExceeded from docs import tags_metadata, description +from db import limiter async def background_tasks(): @@ -50,24 +50,13 @@ async def lifespan(app: FastAPI): await database.disconnect() -def team_secret(request: Request): - param = request.query_params.get("team_secret") - if param is None: - return get_remote_address(request) - - return param - - -limiter = Limiter(key_func=team_secret, default_limits=[ - "10/second"], storage_uri="redis://localhost:6379/0") - app = FastAPI( title="Algotrade API", version="0.0.1", description=description, openapi_tags=tags_metadata, lifespan=lifespan, - docs_url=None + # docs_url=None ) app.state.limiter = limiter diff --git a/backend/model/dataset_data.py b/backend/model/dataset_data.py index 923de8c..98400ae 100644 --- a/backend/model/dataset_data.py +++ b/backend/model/dataset_data.py @@ -1,5 +1,7 @@ +from pprint import pprint from db.table import Table from dataclasses import dataclass +from db.db import database @dataclass @@ -25,3 +27,19 @@ class DatasetData(Table): def __getitem__(self, item): return self.__getattribute__(item.lower()) + + @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) + + return [cls(**x) for x in result] diff --git a/backend/model/enum_type.py b/backend/model/enum_type.py index 945d6d1..f3b1e7f 100644 --- a/backend/model/enum_type.py +++ b/backend/model/enum_type.py @@ -1,23 +1,22 @@ from enum import Enum -def enum_type(cls): - class ClsField(): - def __init__(self, *, default=None): - self._default = default +class EnumType(): + cls = None - def __set_name__(self, owner, name): - self._name = "_" + name + def __init__(self, *, default=None): + self._default = default - def __get__(self, obj, type): - if obj is None: - return self._default - return cls(getattr(obj, self._name)) + def __set_name__(self, owner, name): + self._name = "_" + name - def __set__(self, obj, value): - if isinstance(value, Enum): - setattr(obj, self._name, value.value) - else: - setattr(obj, self._name, value) # pragma: no cover + def __get__(self, obj, type): + if obj is None: + return self._default + return self.cls(getattr(obj, self._name)) - return ClsField + def __set__(self, obj, value): + if isinstance(value, Enum): + setattr(obj, self._name, value.value) + else: + setattr(obj, self._name, value) diff --git a/backend/model/game.py b/backend/model/game.py index 918ff3a..c860d28 100644 --- a/backend/model/game.py +++ b/backend/model/game.py @@ -9,7 +9,6 @@ class Game(Table): game_id: int game_name: str is_contest: bool - bots: str dataset_id: int start_time: datetime total_ticks: int diff --git a/backend/model/market.py b/backend/model/market.py index 43fb241..8af7cc4 100644 --- a/backend/model/market.py +++ b/backend/model/market.py @@ -1,11 +1,12 @@ -from dataclasses import dataclass, fields +from dataclasses import dataclass from db.table import Table from db.db import database from .resource import Resource -from .enum_type import enum_type +from .enum_type import EnumType -ResourceField = enum_type(Resource) +class ResourceField(EnumType): + cls = Resource @dataclass @@ -19,6 +20,7 @@ class Market(Table): open: int close: int market: int + volume: int @classmethod async def create(cls, *args, **kwargs) -> int: @@ -27,3 +29,19 @@ async def create(cls, *args, **kwargs) -> int: 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=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 not resource is None: + values["resource"] = resource.value + result = await database.fetch_all(query, values) + return [cls(**game) for game in result] diff --git a/backend/model/order.py b/backend/model/order.py index 0c1a81d..9c7472b 100644 --- a/backend/model/order.py +++ b/backend/model/order.py @@ -1,17 +1,25 @@ from dataclasses import dataclass, field - -from pydantic import BaseModel from db.table import Table import pandas as pd -from .order_types import * -from .enum_type import enum_type +from .order_types import OrderSide, OrderStatus, OrderType +from .enum_type import EnumType from .resource import Resource -OrderSideField = enum_type(OrderSide) -OrderTypeField = enum_type(OrderType) -OrderStatusField = enum_type(OrderStatus) -ResourceField = enum_type(Resource) +class ResourceField(EnumType): + cls = Resource + + +class OrderSideField(EnumType): + cls = OrderSide + + +class OrderStatusField(EnumType): + cls = OrderStatus + + +class OrderTypeField(EnumType): + cls = OrderType @dataclass diff --git a/backend/model/order_types.py b/backend/model/order_types.py index 9710e5c..e8789f8 100644 --- a/backend/model/order_types.py +++ b/backend/model/order_types.py @@ -2,21 +2,21 @@ class OrderType(Enum): - LIMIT = 0 - MARKET = 1 + LIMIT = "LIMIT" + MARKET = "MARKET" class OrderSide(Enum): - BUY = 0 - SELL = 1 + BUY = "BUY" + SELL = "SELL" class OrderStatus(Enum): - PENDING = 0 - IN_QUEUE = 1 - ACTIVE = 2 - COMPLETED = 3 - CANCELLED = 4 - EXPIRED = 5 - REJECTED = 6 - USER_CANCELLED = 7 + 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 a4585b5..a5500af 100644 --- a/backend/model/player.py +++ b/backend/model/player.py @@ -5,15 +5,15 @@ class PowerPlantType(str, Enum): - COAL = 1 - URANIUM = 2 - BIOMASS = 3 - GAS = 4 - OIL = 5 - GEOTHERMAL = 6 - WIND = 7 - SOLAR = 8 - HYDRO = 9 + 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() diff --git a/backend/model/resource.py b/backend/model/resource.py index e723dd9..8e2b467 100644 --- a/backend/model/resource.py +++ b/backend/model/resource.py @@ -2,9 +2,9 @@ class Resource(Enum): - energy = 0 - coal = 1 - uranium = 2 - biomass = 3 - gas = 4 - oil = 5 + energy = "ENERGY" + coal = "COAL" + uranium = "URANIUM" + biomass = "BIOMASS" + gas = "GAS" + oil = "OIL" diff --git a/backend/model/test_enum_type.py b/backend/model/test_enum_type.py index ae3ae3f..8bcbb56 100644 --- a/backend/model/test_enum_type.py +++ b/backend/model/test_enum_type.py @@ -1,11 +1,12 @@ from enum import Enum import pytest -from model.enum_type import enum_type +from dataclasses import dataclass +from model.enum_type import EnumType class ExampleEnum(Enum): - VALUE1 = 1 - VALUE2 = 2 + VALUE1 = "VALUE1" + VALUE2 = "VALUE2" class AnotherEnum(Enum): @@ -13,29 +14,22 @@ class AnotherEnum(Enum): OPTION2 = 'Option 2' -@pytest.fixture -def example_class(): - class ExampleClass: - example_field = enum_type(ExampleEnum) - another_field = enum_type(AnotherEnum) +class ExampleField(EnumType): + cls = ExampleEnum - def __init__(self, example_field=None, another_field=None): - self.example_field = example_field - self.another_field = another_field - return ExampleClass +class AnotherField(EnumType): + cls = AnotherEnum -def test_enum_type_default(example_class): - instance = example_class() +@dataclass +class ExampleClass(): + example_field: ExampleField + another_field: AnotherField - assert instance.example_field is None - assert instance.another_field is None - instance.example_field = 'VALUE1' - instance.another_field = 'Option 1' - - instance = example_class(example_field='VALUE2', another_field='Option 2') +def test_enum_type_default(): + instance = ExampleClass(example_field='VALUE2', another_field='Option 2') assert instance.example_field == 'VALUE2' assert instance.another_field == 'Option 2' diff --git a/backend/requirements.txt b/backend/requirements.txt index 915c351..f6c276f 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -12,4 +12,5 @@ asyncpg==0.29.0 uvicorn==0.26.0 coverage==7.4.0 coredis==4.16.0 -redis==5.0.1 \ No newline at end of file +redis==5.0.1 +slowapi==0.1.9 \ No newline at end of file diff --git a/backend/routers/admin/admin.py b/backend/routers/admin/admin.py index 9ef2dac..dfc332f 100644 --- a/backend/routers/admin/admin.py +++ b/backend/routers/admin/admin.py @@ -1,7 +1,8 @@ from fastapi import APIRouter, Depends, HTTPException, Query -from db import migration -from . import dataset, bot, team, game +from db import migration, limiter +from . import dataset, team, game, player from config import config +from routers.model import SuccessfulResponse def admin_dep(admin_secret: str = Query(description="Admin secret", default=None)): @@ -15,13 +16,13 @@ def admin_dep(admin_secret: str = Query(description="Admin secret", default=None @router.get("/migrate") -async def migrate(): +@limiter.exempt +async def migrate() -> SuccessfulResponse: await migration.drop_tables() await migration.run_migrations() - return {"message": "succesfully migrated database"} - + return SuccessfulResponse() +router.include_router(player.router) router.include_router(dataset.router) -router.include_router(bot.router) router.include_router(game.router) router.include_router(team.router) diff --git a/backend/routers/admin/bot.py b/backend/routers/admin/bot.py deleted file mode 100644 index e9d6fcb..0000000 --- a/backend/routers/admin/bot.py +++ /dev/null @@ -1,14 +0,0 @@ -from fastapi import APIRouter - -from game.bots.bots import Bots - -# BOT PATHS - -# GET /admin/bot/list - [{"id": [id], "name": [name]}, {}, {}, {}] - -router = APIRouter() - - -@router.get("/bot/list") -def bot_list(): - return Bots.list() diff --git a/backend/routers/admin/dataset.py b/backend/routers/admin/dataset.py index 6f8ef48..508afdd 100644 --- a/backend/routers/admin/dataset.py +++ b/backend/routers/admin/dataset.py @@ -1,14 +1,14 @@ from fastapi import APIRouter +from typing import List +from db import limiter from model.datasets import Datasets -# DATASET PATHS - -# GET /admin/dataset/list - [{"id": [id], "name": [name]}, {}, {}, {}] router = APIRouter() @router.get("/dataset/list") -async def dataset_list(): +@limiter.exempt +async def dataset_list() -> List[Datasets]: return await Datasets.list() diff --git a/backend/routers/admin/game.py b/backend/routers/admin/game.py index db94ef2..8c6780e 100644 --- a/backend/routers/admin/game.py +++ b/backend/routers/admin/game.py @@ -4,15 +4,9 @@ from game.bots import Bots from pydantic import BaseModel from datetime import datetime -from logger import logger - -# GAME PATHS - -# POST /admin/game/create {"game_name": [name], "start_time": [time], "bots": [[id], [id], [id]], "dataset": [id], "tick_time": [tick_time], "length": [len], "contest": [bool]} // contest brani delete i ogranicava pravljenje playera na 1 {"game_id":[game_id]} -# GET /admin/game/list - [{"start_time": TIME, "game_id": game_id, "contest": bool}] -# GET /admin/game/[id]/delete - {"success": [success]} -# POST /admin/game/[id]/edit {} {"success": [success]} - +from typing import List +from routers.model import SuccessfulResponse +from db import limiter router = APIRouter() @@ -20,17 +14,6 @@ class CreateGameParams(BaseModel): game_name: str contest: bool - bots: str - dataset_id: int - start_time: datetime - total_ticks: int - tick_time: int - - -class EditGameParams(BaseModel): - game_name: str - contest: bool - bots: str dataset_id: int start_time: datetime total_ticks: int @@ -38,17 +21,8 @@ class EditGameParams(BaseModel): @router.post("/game/create") -async def game_create(params: CreateGameParams): - try: - Bots.parse_string(params.bots) - except: - raise HTTPException(400, "Invalid bots string") - - try: - await Datasets.get(dataset_id=params.dataset_id) - except Exception as e: - raise HTTPException(400, "Dataset does not exist") - +@limiter.exempt +async def game_create(params: CreateGameParams) -> SuccessfulResponse: await Datasets.validate_ticks(params.dataset_id, params.total_ticks) if params.start_time < datetime.now(): @@ -57,7 +31,6 @@ async def game_create(params: CreateGameParams): await Game.create( game_name=params.game_name, is_contest=params.contest, - bots=params.bots, dataset_id=params.dataset_id, start_time=params.start_time, total_ticks=params.total_ticks, @@ -65,31 +38,41 @@ async def game_create(params: CreateGameParams): is_finished=False, current_tick=0 ) - - return {"message": "success"} + return SuccessfulResponse() @router.get("/game/list") -async def game_list(): - games = await Game.list() - return {"games": games} +@limiter.exempt +async def game_list() -> List[Game]: + return await Game.list() @router.get("/game/{game_id}/player/list") -async def player_list(game_id: int): - players = await Player.list(game_id=game_id) - return {"players": players} +@limiter.exempt +async def player_list(game_id: int) -> List[Player]: + return await Player.list(game_id=game_id) -@router.get("/game/{game_id}/delete") -async def game_delete(game_id: int): +@router.post("/game/{game_id}/delete") +@limiter.exempt +async def game_delete(game_id: int) -> SuccessfulResponse: # TODO ne baca exception ako je vec zavrsena await Game.update(game_id=game_id, is_finished=True) - return {"message": "success"} + 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") -async def game_edit(game_id: int, params: EditGameParams): +@limiter.exempt +async def game_edit(game_id: int, params: EditGameParams) -> SuccessfulResponse: try: Bots.parse_string(params.bots) except: @@ -113,5 +96,4 @@ async def game_edit(game_id: int, params: EditGameParams): game_id=game_id, **params.dict(exclude_unset=True) ) - - return {"message": "success"} + return SuccessfulResponse() diff --git a/backend/routers/admin/player.py b/backend/routers/admin/player.py new file mode 100644 index 0000000..87d7af3 --- /dev/null +++ b/backend/routers/admin/player.py @@ -0,0 +1,14 @@ +from fastapi import APIRouter +from db import limiter +from model.team import Team +from routers.model import SuccessfulResponse + + +router = APIRouter() + + +@router.get("/team/{team_id}/delete") +@limiter.exempt +async def team_delete(team_id: int) -> SuccessfulResponse: + await Team.update(team_id=team_id, is_active=False) + return SuccessfulResponse() diff --git a/backend/routers/admin/team.py b/backend/routers/admin/team.py index 8451111..73029bd 100644 --- a/backend/routers/admin/team.py +++ b/backend/routers/admin/team.py @@ -3,43 +3,47 @@ from fastapi import APIRouter, HTTPException from model import Team from pydantic import BaseModel - -# TEAM PATHS - -# POST /admin/team/create {"name": [name]} {"team_id":[team_id], "team_secret": [team_secret]} -# GET /admin/team/list - [{}, {}, {}] -# GET /admin/team/[team_id]/delete - {"success": [success]} +from routers.model import SuccessfulResponse +from typing import List +from db import limiter router = APIRouter() -class CreateTeam(BaseModel): - team_name: str - - def id_generator(size=8, chars=string.ascii_uppercase + string.digits): # TODO: Nemoguce da se dvaput stvori tim s istim idjem... return ''.join(random.choice(chars) for _ in range(size)) +class CreateTeam(BaseModel): + team_name: str + + @router.post("/team/create") -async def team_create(params: CreateTeam): +@limiter.exempt +async def team_create(params: CreateTeam) -> Team: team_secret = id_generator() - team_id = await Team.create(team_name=params.team_name, team_secret=team_secret) - - return {"team_id": team_id, "team_secret": team_secret} + 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 + ) @router.get("/team/list") -async def team_list(): +@limiter.exempt +async def team_list() -> List[Team]: return await Team.list() @router.get("/team/{team_id}/delete") -async def team_delete(team_id: int): +@limiter.exempt +async def team_delete(team_id: int) -> SuccessfulResponse: team_id = await Team.delete(team_id=team_id) if team_id is None: raise HTTPException(status_code=400, detail="Team not found") - return {"success": True} + return SuccessfulResponse() diff --git a/backend/routers/model.py b/backend/routers/model.py new file mode 100644 index 0000000..b300a2a --- /dev/null +++ b/backend/routers/model.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class SuccessfulResponse(BaseModel): + successful: bool = True diff --git a/backend/routers/users/dependencies.py b/backend/routers/users/dependencies.py index 3d2f8fb..99dd42b 100644 --- a/backend/routers/users/dependencies.py +++ b/backend/routers/users/dependencies.py @@ -1,6 +1,7 @@ from datetime import datetime from fastapi import HTTPException, Query, Depends from model import Team, Player, Game +from typing import Tuple async def team_dep(team_secret: str = Query(description="Team secret", default=None)) -> Team: @@ -26,7 +27,9 @@ async def check_game_active_dep(game: Game = Depends(game_dep)): raise HTTPException(403, "Game has not started yet") -async def player_dep(player_id: int, game: Game = Depends(game_dep), team: int = Depends(team_dep)) -> Player: +async def player_dep(player_id: int, + game: Game = Depends(game_dep), + team: Team = Depends(team_dep)) -> Player: try: player = await Player.get(player_id=player_id) except: @@ -39,3 +42,38 @@ async def player_dep(player_id: int, game: Game = Depends(game_dep), team: int = raise HTTPException( 400, f"This player is inactive or already has been deleted") return player + + +async def start_end_tick_dep(game: Game = Depends(game_dep), + start_tick: int = Query(default=None), + end_tick: int = Query(default=None)) -> Tuple[int, int]: + if start_tick is None and end_tick is None: + current_tick = game.current_tick - 1 + start_tick = current_tick + end_tick = current_tick + if start_tick is None: + start_tick = end_tick + if end_tick is None: + end_tick = start_tick + + if game.current_tick == 0: + raise HTTPException( + status_code=400, detail="Game just started (it is tick=0), no data to return") + + if start_tick < 0 or end_tick < 0: + raise HTTPException( + status_code=400, detail="Start and end tick must both be greater than 0") + + if end_tick < start_tick: + raise HTTPException( + status_code=400, detail="End tick must be greater than start tick") + + if start_tick >= game.current_tick: + raise HTTPException( + status_code=400, detail=f"Start tick must be less than current tick (current_tick={game.current_tick})") + + if end_tick >= game.current_tick: + raise HTTPException( + status_code=400, detail=f"End tick must be less than current tick (current_tick={game.current_tick})") + + return start_tick, end_tick diff --git a/backend/routers/users/game.py b/backend/routers/users/game.py index cac384f..e6c2dbe 100644 --- a/backend/routers/users/game.py +++ b/backend/routers/users/game.py @@ -1,17 +1,64 @@ -from typing import List -from fastapi import APIRouter +from collections import defaultdict +from pprint import pprint +from typing import Dict, List +from fastapi import APIRouter, Depends +from pydantic import BaseModel from model import Game +from model.dataset_data import DatasetData +from routers.users.dependencies import game_dep, start_end_tick_dep +from datetime import datetime -# GAME PATHS -# /game/list?team_secret= -# /game/[id]/time?team_secret= +router = APIRouter() -router = APIRouter() +class GameData(BaseModel): + game_id: int + game_name: str + is_contest: bool + start_time: datetime + total_ticks: int + tick_time: int + current_tick: int + is_finished: bool @router.get("/game/list") -async def game_list() -> List[Game]: +async def game_list() -> List[GameData]: games = await Game.list() return games + + +class DatasetListResponseItem(BaseModel): + tick: int + date: str + coal: int + uranium: int + biomass: int + gas: int + oil: int + geothermal: int + wind: int + solar: int + hydro: int + energy_demand: int + max_energy_price: int + + +@router.get("/game/{game_id}/dataset") +async def dataset_list(start_end=Depends(start_end_tick_dep), + game: Game = Depends(game_dep)) -> Dict[int, 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_dict = {} + for entry in all_entries: + entry.date = str(entry.date) + 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 8097018..ad3576c 100644 --- a/backend/routers/users/market.py +++ b/backend/routers/users/market.py @@ -1,39 +1,81 @@ -from dataclasses import dataclass -from typing import List -from fastapi import APIRouter, Depends, HTTPException +from collections import defaultdict +from typing import List, Dict +from fastapi import APIRouter, Depends, HTTPException, Query import pandas as pd from pydantic import BaseModel from model import Order, OrderSide, OrderType, OrderStatus, Resource from model.game import Game from model.market import Market from model.player import Player -from .dependencies import game_dep, player_dep, check_game_active_dep - -# GAME PATHS +from .dependencies import game_dep, player_dep, check_game_active_dep, start_end_tick_dep +from db.db import database +from routers.model import SuccessfulResponse router = APIRouter(dependencies=[Depends(check_game_active_dep)]) +class MarketPricesResponse(BaseModel): + tick: int + low: int + high: int + open: int + close: int + market: int + volume: int + + +@router.get("/game/{game_id}/market/prices") +async def market_prices(start_end=Depends(start_end_tick_dep), + resource: Resource = Query(default=None), + game: Game = Depends(game_dep)) -> Dict[Resource, List[MarketPricesResponse]]: + 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, + ) + all_prices_dict = defaultdict(list) + for price in all_prices: + all_prices_dict[price.resource].append(price) + return all_prices_dict + + +class EnergyPrice(BaseModel): + price: int + + +@router.post("/game/{game_id}/player/{player_id}/energy/set_price") +async def energy_set_price_player(price: EnergyPrice, game: Game = Depends(game_dep), player: int = Depends(player_dep)) -> SuccessfulResponse: + if 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 + ) + + return SuccessfulResponse() + + class OrderResponse(BaseModel): order_id: int - game_id: int player_id: int price: int size: int tick: int timestamp: pd.Timestamp order_side: OrderSide - order_type: OrderType order_status: OrderStatus filled_size: int - filled_money: int - filled_price: int expiration_tick: int resource: Resource -@router.get("/game/{game_id}/market/order/list") +@router.get("/game/{game_id}/orders") async def order_list(game: Game = Depends(game_dep)) -> List[OrderResponse]: return await Order.list( game_id=game.game_id, @@ -41,45 +83,14 @@ async def order_list(game: Game = Depends(game_dep)) -> List[OrderResponse]: ) -@router.get("/game/{game_id}/market/order/prices/from/{start_tick}/to/{end_tick}") -async def orded_list(game: Game = Depends(game_dep), start_tick: int = 0, end_tick: int = 0) -> List[OrderResponse]: - if start_tick < 0 or end_tick < 0: - raise HTTPException( - status_code=400, detail="Tick must be greater than 0") - - if end_tick < start_tick: - raise HTTPException( - status_code=400, detail="End tick must be greater than start tick") - - # TODO: add new method - all_market = await Market.list( - game_id=Game.game_id, - ) - - return list(filter(lambda x: start_tick <= x.tick <= end_tick, all_market)) - - -class EnergyPrice(BaseModel): - price: int - - -class EnergyPriceResponse(BaseModel): - success: bool - - -@router.post("/game/{game_id}/player/{player_id}/market/energy/set_price") -async def energy_set_price_player(price: EnergyPrice, game: Game = Depends(game_dep), player: int = Depends(player_dep)) -> EnergyPriceResponse: - if price <= 0: - raise HTTPException( - status_code=400, detail="Price must be greater than 0") - - await Player.update( +@router.get("/game/{game_id}/player/{player_id}/orders") +async def order_list_player(game: Game = Depends(game_dep), player: Player = Depends(player_dep)) -> List[OrderResponse]: + return await Order.list( + game_id=game.game_id, player_id=player.player_id, - energy_price=price.price + order_status=OrderStatus.ACTIVE.value ) - return {"success": True} - class UserOrder(BaseModel): resource: Resource @@ -90,15 +101,12 @@ class UserOrder(BaseModel): type: OrderType -class OrderCreateResponse(BaseModel): - success: bool - - -@router.post("/game/{game_id}/player/{player_id}/market/order/create") -async def order_create_player(order: UserOrder, game: Game = Depends(game_dep), player: Player = Depends(player_dep)) -> OrderCreateResponse: +@router.post("/game/{game_id}/player/{player_id}/orders/create") +async def order_create_player(order: UserOrder, game: Game = Depends(game_dep), player: Player = Depends(player_dep)) -> SuccessfulResponse: if order.type == OrderType.ENERGY: - raise Exception( - "Use /game/{game_id}/player/{player_id}/market/energy/set_price to set energy price") + raise HTTPException( + status_code=400, + detail="Use /game/{game_id}/player/{player_id}/energy/set_price to set energy price") await Order.create( game_id=game.game_id, @@ -109,37 +117,28 @@ async def order_create_player(order: UserOrder, game: Game = Depends(game_dep), timestamp=pd.Timestamp.now(), price=order.price, size=order.size, - tick=(await Game.get(game_id=game.game_id)).current_tick, + tick=game.current_tick, expiration_tick=order.expiration_tick, resource=order.resource.value ) - return {"success": True} - - -@router.post("/game/{game_id}/player/{player_id}/market/order/list") -async def order_list_player(game: Game = Depends(game_dep), player: Player = Depends(player_dep)) -> List[OrderResponse]: - return await Order.list( - game_id=game.game_id, - player_id=player.player_id, - order_status=OrderStatus.ACTIVE.value - ) + return SuccessfulResponse() class OrderCancel(BaseModel): ids: List[int] -class OrderCancelResponse(BaseModel): - success: bool - - -@router.post("/game/{game_id}/player/{player_id}/market/order/cancel") -async def order_cancel_player(body: OrderCancel, game: Game = Depends(game_dep), player: Player = Depends(player_dep)) -> OrderCancelResponse: - for order_id in body.ids: - await Order.update( - order_id=order_id, - order_status=OrderStatus.CANCELED.value - ) +@router.post("/game/{game_id}/player/{player_id}/orders/cancel") +async def order_cancel_player(body: OrderCancel, game: Game = Depends(game_dep), player: Player = Depends(player_dep)) -> SuccessfulResponse: + async with database.transaction(): + for order_id in body.ids: + if (await Order.get(order_id=order_id)).player_id != player.player_id: + raise HTTPException( + status_code=400, detail="You can only cancel your own orders") - return {"success": True} + await Order.update( + order_id=order_id, + order_status=OrderStatus.CANCELED.value + ) + return SuccessfulResponse() diff --git a/backend/routers/users/player.py b/backend/routers/users/player.py index 672371b..3eb9adc 100644 --- a/backend/routers/users/player.py +++ b/backend/routers/users/player.py @@ -1,77 +1,92 @@ from typing import List -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from db import database from model import Player, Team from config import config from model.game import Game from .dependencies import game_dep, player_dep, check_game_active_dep, team_dep - -# PLAYER PATHS - -# /game/[id]/player/create?team_secret= -# /game/[id]/player/list?team_secret= -# /game/[id]/player/[player_id]/delete?team_secret= +from routers.model import SuccessfulResponse router = APIRouter(dependencies=[]) -class PlayerListResponseItem(BaseModel): +class PlayerCreateResponse(BaseModel): player_id: int player_name: str @router.get("/game/{game_id}/player/list") async def player_list(game: Game = Depends(game_dep), - team: Team = Depends(team_dep)) -> List[PlayerListResponseItem]: + team: Team = Depends(team_dep)) -> List[PlayerCreateResponse]: players = await Player.list(game_id=game.game_id, team_id=team.team_id, is_active=True) - return [{ - "player_id": x.player_id, - "player_name": x.player_name, - } for x in players] + return [PlayerCreateResponse( + player_id=x.player_id, + player_name=x.player_name + ) for x in players] class PlayerCreate(BaseModel): player_name: str = None -class PlayerResponse(BaseModel): - player_id: int - player_name: str - - @router.post("/game/{game_id}/player/create") async def player_create(game: Game = Depends(game_dep), team: Team = Depends(team_dep), - player_create: PlayerCreate | None | dict = None) -> PlayerResponse: + player_create: PlayerCreate | None | dict = None) -> PlayerCreateResponse: async with database.transaction(): + team_players = await Player.count( + game_id=game.game_id, + team_id=team.team_id, + is_active=True) + if game.is_contest and team_players >= 1: + raise HTTPException( + 400, "Only one player per team can be created in contest mode") team_id = team.team_id game_id = game.game_id if player_create is None or player_create.player_name is None: - team_players_len = await Player.count(team_id=team_id, game_id=game_id) - player_name = f"{team.team_name}_{team_players_len}" + player_name = f"{team.team_name}_{team_players}" else: player_name = player_create.player_name - player_id = await Player.create(game_id=game_id, team_id=team_id, player_name=player_name, money=config["player"]["starting_money"]) + starting_money = config["player"]["starting_money"] - return {"player_id": player_id, "player_name": player_name} + player_id = await Player.create( + game_id=game_id, + team_id=team_id, + player_name=player_name, + money=starting_money) + return PlayerCreateResponse(player_id=player_id, player_name=player_name) -@router.get("/game/{game_id}/player/{player_id}", dependencies=[Depends(check_game_active_dep)]) -async def player_get(game: Game = Depends(game_dep), - player: Player = Depends(player_dep)) -> Player: - return await Player.get(player_id=player.player_id) +class PlayerData(BaseModel): + player_id: int + player_name: str + game_id: int + energy_price: int + money: int -class PlayerDeleteResponse(BaseModel): - successfull: bool = False + coal: int + uranium: int + biomass: int + gas: int + oil: int + + +@router.get("/game/{game_id}/player/{player_id}", dependencies=[Depends(check_game_active_dep)]) +async def player_get(player: Player = Depends(player_dep)) -> PlayerData: + return await Player.get(player_id=player.player_id) @router.get("/game/{game_id}/player/{player_id}/delete") async def player_delete(game: Game = Depends(game_dep), - player: Player = Depends(player_dep)) -> PlayerDeleteResponse: + player: Player = Depends(player_dep)) -> SuccessfulResponse: + 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) - return {"successfull": True} + return SuccessfulResponse() diff --git a/backend/routers/users/power_plant.py b/backend/routers/users/power_plant.py index d709bab..aaf3441 100644 --- a/backend/routers/users/power_plant.py +++ b/backend/routers/users/power_plant.py @@ -5,24 +5,22 @@ from model import Player, PowerPlantType from .dependencies import check_game_active_dep, player_dep from config import config +from typing import Dict +from routers.model import SuccessfulResponse -# POWER_PLANT PATHS -# /game/[id]/player/[player_id]/plant/prices?team_secret= -# /game/[id]/player/[player_id]/plant/buy?team_secret= -# /game/[id]/player/[player_id]/plant/[plant_id]/sell?team_secret= -# /game/[id]/player/[player_id]/plant/[plant_id]/on?team_secret= -# /game/[id]/player/[player_id]/plant/[plant_id]/off?team_secret= -# /game/[id]/player/[player_id]/plant/[plant_id]/state?team_secret= -# /game/[id]/player/[player_id]/plant/list?team_secret= -# /game/[id]/player/[player_id]/state +router = APIRouter(dependencies=[Depends(check_game_active_dep)]) -router = APIRouter(dependencies=[Depends(check_game_active_dep)]) +class PowerPlantData(BaseModel): + plants_powered: int + plants_owned: int + next_price: int + sell_price: int @router.get("/game/{game_id}/player/{player_id}/plant/list") -async def list_plants(player: Player = Depends(player_dep)): +async def list_plants(player: Player = Depends(player_dep)) -> Dict[str, PowerPlantData]: return { x.name: { "plants_powered": player[x.name.lower() + "_plants_powered"], @@ -39,7 +37,7 @@ class PlantBuySell(BaseModel): @router.post("/game/{game_id}/player/{player_id}/plant/buy") -async def buy_plant(plant: PlantBuySell, player: Player = Depends(player_dep)): +async def buy_plant(plant: PlantBuySell, player: Player = Depends(player_dep)) -> SuccessfulResponse: type = PowerPlantType(plant.type) async with database.transaction(): @@ -53,10 +51,11 @@ async def buy_plant(plant: PlantBuySell, player: Player = Depends(player_dep)): 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}) + return SuccessfulResponse() @router.post("/game/{game_id}/player/{player_id}/plant/sell") -async def sell_plant(plant: PlantBuySell, player: Player = Depends(player_dep)): +async def sell_plant(plant: PlantBuySell, player: Player = Depends(player_dep)) -> SuccessfulResponse: type = PowerPlantType(plant.type) async with database.transaction(): @@ -67,6 +66,7 @@ async def sell_plant(plant: PlantBuySell, player: Player = Depends(player_dep)): await Player.update(player_id=player_id, money=player.money + round(plant_price * config["power_plant"]["sell_coeff"])) await Player.update(player_id=player_id, **{type.name.lower() + "_plants_owned": plant_count - 1}) + return SuccessfulResponse() class PowerOn(BaseModel): @@ -74,7 +74,7 @@ class PowerOn(BaseModel): @router.post("/game/{game_id}/player/{player_id}/plant/on") -async def turn_on(plant: PowerOn, player: Player = Depends(player_dep)): +async 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) @@ -85,3 +85,4 @@ async def turn_on(plant: PowerOn, player: Player = Depends(player_dep)): status_code=400, detail="Not enough plants or invalid number") await Player.update(player_id=player_id, **{type.name.lower() + "_plants_powered": plant.number}) + return SuccessfulResponse() diff --git a/backend/start.sh b/backend/start.sh index b2777a7..aea45b7 100644 --- a/backend/start.sh +++ b/backend/start.sh @@ -10,10 +10,13 @@ sudo service postgresql start echo "Running migration script" python run_migrations.py +echo "Running redis server" +redis-server --daemonize yes + if [[ "$TESTING" == 1 ]]; then echo "Running main with reload" -TESTING=1 uvicorn main:app --reload +TESTING=1 uvicorn main:app --reload --host=0.0.0.0 --port=3004 else echo "Running uvicorn main with 4 workers" -TESTING=0 uvicorn main:app --workers 4 +TESTING=0 uvicorn main:app --workers 4 --host=0.0.0.0 --port=3004 fi \ No newline at end of file diff --git a/backend/test.ipynb b/backend/test.ipynb new file mode 100644 index 0000000..539e589 --- /dev/null +++ b/backend/test.ipynb @@ -0,0 +1,259 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 69, + "id": "25d31bcd-1861-4583-ac64-45f421e99440", + "metadata": {}, + "outputs": [], + "source": [ + "import requests" + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "id": "b5b58cce-d433-4f3d-8f84-42aae47ee77e", + "metadata": {}, + "outputs": [], + "source": [ + "team_secret = \"gogi\"" + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "id": "d7081395-6abc-460c-ad08-9b081e2ecfa1", + "metadata": {}, + "outputs": [], + "source": [ + "addr = \"https://4903-93-141-154-92.ngrok-free.app\"" + ] + }, + { + "cell_type": "code", + "execution_count": 72, + "id": "9af62673-65be-4e1c-b75e-629e9c3973e5", + "metadata": {}, + "outputs": [], + "source": [ + "def game_list():\n", + " url = f\"{addr}/game/list?team_secret={team_secret}\"\n", + " print(url)\n", + " response = requests.get(url)\n", + " return response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": 73, + "id": "2d4a7e04-4e17-43d8-97d5-829efe28718b", + "metadata": {}, + "outputs": [], + "source": [ + "def list_player():\n", + " url = f\"{addr}/game/{game_id}/player/list\"\n", + " return requests.get(url, params={\"team_secret\": team_secret}).json()" + ] + }, + { + "cell_type": "code", + "execution_count": 74, + "id": "6d683616-e891-493e-b7ea-9656bd1aad62", + "metadata": {}, + "outputs": [], + "source": [ + "def create_player():\n", + " url = f\"{addr}/game/{game_id}/player/create\"\n", + " return requests.post(url, params={\"team_secret\": team_secret}).json()" + ] + }, + { + "cell_type": "code", + "execution_count": 75, + "id": "e4df7b4e-210f-43e9-998a-252be2cd4629", + "metadata": {}, + "outputs": [], + "source": [ + "def get_player():\n", + " url = f\"{addr}/game/{game_id}/player/{player_id}\"\n", + " return requests.get(url, params={\"team_secret\": team_secret}).json()" + ] + }, + { + "cell_type": "code", + "execution_count": 76, + "id": "84721896-603e-4e57-bebf-92f767410b98", + "metadata": {}, + "outputs": [], + "source": [ + "def get_prices(start_tick=None, end_tick=None, resource=None):\n", + " url = f\"{addr}/game/{game_id}/market/prices\"\n", + " params = {\"team_secret\": team_secret}\n", + " if start_tick: params[\"start_tick\"] = start_tick\n", + " if end_tick: params[\"end_tick\"] = end_tick\n", + " if resource: params[\"resource\"] = resource\n", + " return requests.get(url, params=params).json()" + ] + }, + { + "cell_type": "code", + "execution_count": 88, + "id": "f0502559-2a71-4b08-9960-5800cfe3b4f8", + "metadata": {}, + "outputs": [], + "source": [ + "def get_dataset(start_tick=None, end_tick=None):\n", + " url = f\"{addr}/game/{game_id}/dataset\"\n", + " params = {\"team_secret\": team_secret}\n", + " if start_tick: params[\"start_tick\"] = start_tick\n", + " if end_tick: params[\"end_tick\"] = end_tick\n", + " return requests.get(url, params=params).json()" + ] + }, + { + "cell_type": "code", + "execution_count": 79, + "id": "0d4836fb-959d-45d5-9f99-29e6134dfe94", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'player_id': 11, 'player_name': 'Goranov_tim_1'}" + ] + }, + "execution_count": 79, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "game_id = 1\n", + "create_player()" + ] + }, + { + "cell_type": "code", + "execution_count": 81, + "id": "568ba6c9-24a2-4732-bfc0-4da63d4b5479", + "metadata": {}, + "outputs": [], + "source": [ + "player_id = 11" + ] + }, + { + "cell_type": "code", + "execution_count": 82, + "id": "c38850b8-d12f-4d22-adea-c4ed86aef7d3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'player_id': 11, 'player_name': 'Goranov_tim_1'},\n", + " {'player_id': 1, 'player_name': 'Goran'}]" + ] + }, + "execution_count": 82, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "list_player()" + ] + }, + { + "cell_type": "code", + "execution_count": 112, + "id": "79fb7bd3-a2a2-4e49-b25e-8cd9f9575650", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'player_id': 11,\n", + " 'player_name': 'Goranov_tim_1',\n", + " 'game_id': 1,\n", + " 'energy_price': 1000000000,\n", + " 'money': 15000,\n", + " 'coal': 0,\n", + " 'uranium': 0,\n", + " 'biomass': 0,\n", + " 'gas': 0,\n", + " 'oil': 0}" + ] + }, + "execution_count": 112, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "get_player()" + ] + }, + { + "cell_type": "code", + "execution_count": 104, + "id": "1071a2e9-18d2-47c7-a87a-fcbd46131e00", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'detail': \"'NoneType' object is not callable\"}" + ] + }, + "execution_count": 104, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# get_prices(1, 5, resource=\"COAL\")\n", + "get_dataset(1,5)" + ] + }, + { + "cell_type": "markdown", + "id": "a28cb91b-f4e5-44a5-a9ab-e9c86fc2e9cc", + "metadata": {}, + "source": [ + "#\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a31b28d2-9e04-4cac-84ee-4d91dd9c1df9", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/frontend/src/components/Smoke.jsx b/frontend/src/components/Smoke.jsx index 3e6b387..e7b4203 100644 --- a/frontend/src/components/Smoke.jsx +++ b/frontend/src/components/Smoke.jsx @@ -47,125 +47,6 @@ function Smoke() { } }, [canvasMouse]); - useEffect(() => { - const noise3D = createNoise3D(); - const canvas = canvasRef.current; - const ctx = canvas.getContext("2d"); - - // clear - - const particleX = 500; - const particleY = 500; - const particleStd = 50; - - let particles = Array(100) - .fill() - .map(() => { - return { - x: particleX + Math.random() * particleStd, - y: particleY + Math.random() * particleStd, - vx: 0, - vy: 0, - radius: Math.random() * 50, - time: Math.random() * 10, - }; - }); - - let z = 0; - // animation - const animate = () => { - // ctx.clearRect(0, 0, canvas.width, canvas.height); - - ctx.fillStyle = "rgba(0, 0, 0, 1)"; - ctx.fillRect(0, 0, canvas.width, canvas.height); - for (let i = 0; i < canvas.height; i += 10) { - for (let j = 0; j < canvas.width; j += 10) { - const x = noise3D(j / 100, i / 100, z); - const y = noise3D(j / 100, i / 100, z + 1000); - - // plot vector field - - ctx.beginPath(); - ctx.moveTo(j, i); - ctx.lineTo(j + x * 10, i + y * 10); - ctx.strokeStyle = "white"; - ctx.stroke(); - } - } - - z += 0.01; - // console.log(z); - - for (let particle of particles) { - ctx.beginPath(); - // radial gradient - const gradient = ctx.createRadialGradient( - particle.x, - particle.y, - 0, - particle.x, - particle.y, - particle.radius - ); - gradient.addColorStop( - 0, - `rgba(255, 255, 255, ${particle.time / 6000})` - ); - gradient.addColorStop(1, "rgba(255, 255, 255, 0)"); - ctx.fillStyle = gradient; - - let new_radius = particle.radius / (particle.time / 1000); - - ctx.arc(particle.x, particle.y, new_radius, 0, Math.PI * 2); - - ctx.fill(); - - particle.x += particle.vx; - particle.y += particle.vy; - - particle.vx += noise3D(particle.x / 100, particle.y / 100, z) / 5; - particle.vy += - noise3D(particle.x / 100, particle.y / 100, z + 1000) / 5 - 0.3; - - particle.vx *= 0.9; - particle.vy *= 0.9; - - particle.vx *= 1 + Math.random() * 0.2; - // particle.vy *= 1 + Math.random() * 0.4; - - particle.time -= 1; - } - - particles = particles.filter((p) => p.time > 0); - particles = particles.filter( - (p) => - p.x > 0 && - p.x < canvas.width && - p.y > 0 && - p.y < canvas.height && - p.radius > 0 - ); - - console.log(particles.length); - - let i = 0; - while (particles.length < 1000 && i++ < 5) { - particles.push({ - x: particleX + Math.random() * particleStd, - y: particleY + Math.random() * particleStd, - vx: 0, - vy: 0, - radius: Math.random() * 100, - time: Math.random() * 1000, - }); - } - - requestAnimationFrame(animate); - }; - - animate(); - }, []); - return ( -

Home

- +
+

Home

+
- ) + ); } -export default Home +export default Home; From 73031dece39e53fc428294bcd0f7e14ec9fc318d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Goran=20Ivankovi=C4=87?= <40663576+rangoiv@users.noreply.github.com> Date: Wed, 28 Feb 2024 23:44:23 +0100 Subject: [PATCH 2/2] Added indexes, API for players (#51) - Added indexes - api module for players - Jupyter notebook - Moved player CLI - delete player admin --- backend/cli_player/player.py | 397 ------------------ backend/db/migration.py | 4 + backend/routers/admin/admin.py | 5 +- backend/routers/admin/game.py | 1 + backend/routers/admin/player.py | 13 +- backend/routers/model.py | 1 + backend/routers/users/game.py | 12 + backend/routers/users/market.py | 15 +- backend/routers/users/power_plant.py | 18 +- backend/routers/users/users.py | 1 + backend/start.sh | 4 +- backend/test.ipynb | 259 ------------ .../admin_player_cli.py | 0 testing/algotrade_api.py | 167 ++++++++ testing/cheat_sheet.ipynb | 266 ++++++++++++ testing/player_cli.py | 194 +++++++++ 16 files changed, 677 insertions(+), 680 deletions(-) delete mode 100644 backend/cli_player/player.py delete mode 100644 backend/test.ipynb rename backend/cli_player/admin_player.py => testing/admin_player_cli.py (100%) create mode 100644 testing/algotrade_api.py create mode 100644 testing/cheat_sheet.ipynb create mode 100644 testing/player_cli.py diff --git a/backend/cli_player/player.py b/backend/cli_player/player.py deleted file mode 100644 index 69ba527..0000000 --- a/backend/cli_player/player.py +++ /dev/null @@ -1,397 +0,0 @@ -from pprint import pprint -import requests - -# GET -# /game/list -# Game List - - -# GET -# /game/{game_id}/dataset -# Dataset List - -# Player -# You will be able to - -# Create player in a game -# Get all created players in a game -# Get player info - player resources, money, power plants etc. - - -# GET -# /game/{game_id}/player/list -# Player List - - -# POST -# /game/{game_id}/player/create -# Player Create - - -# GET -# /game/{game_id}/player/{player_id} -# Player Get - - -# GET -# /game/{game_id}/player/{player_id}/delete -# Player Delete - -# Market -# You can: - -# Create buy and sell orders for resources that will be matched at the end of every tick. -# List your orders and cancel ones you do not longer need -# Set your price for produced electricity - - -# GET -# /game/{game_id}/market/prices -# Market Prices - - -# POST -# /game/{game_id}/player/{player_id}/energy/set_price -# Energy Set Price Player - - -# GET -# /game/{game_id}/orders -# Order List - - -# GET -# /game/{game_id}/player/{player_id}/orders -# Order List Player - - -# POST -# /game/{game_id}/player/{player_id}/orders/create -# Order Create Player - - -# POST -# /game/{game_id}/player/{player_id}/orders/cancel -# Order Cancel Player - -# Power plants -# You can: - -# Buy more power plants -# Sell power plants -# You can process at most the number of resources per tick as you have power plants of that type -# You can also buy and sell renewables that will produce energy passively - - -# GET -# /game/{game_id}/player/{player_id}/plant/list -# List Plants - - -# POST -# /game/{game_id}/player/{player_id}/plant/buy -# Buy Plant - - -# POST -# /game/{game_id}/player/{player_id}/plant/sell -# Sell Plant - - -# POST -# /game/{game_id}/player/{player_id}/plant/on -# Turn On - - -URL = "localhost:8000" - -team_secret = "" -game_id = "1" -player_id = "1" - - -def set_team_secret(): - global team_secret - - new_team_secret = input("Enter new team secret: ") - - if new_team_secret: - team_secret = new_team_secret - print(f"Team secret set to: {team_secret}") - else: - print("Team secret not set") - - -def set_game_id(): - global game_id - - new_game_id = input("Enter new game id: ") - - if new_game_id: - game_id = new_game_id - print(f"Game id set to: {game_id}") - else: - print("Game id not set") - - -def set_player_id(): - global player_id - - new_player_id = input("Enter new player id: ") - - if new_player_id: - player_id = new_player_id - print(f"Player id set to: {player_id}") - else: - print("Player id not set") - - -def list_games(): - response = requests.get( - f"http://{URL}/game/list", params={"team_secret": team_secret}) - - return response - - -def list_players(): - response = requests.get( - f"http://{URL}/game/{game_id}/player/list", params={"team_secret": team_secret}) - - return response - - -def create_player(): - player_name = input("Enter player name: ") - - response = requests.post(f"http://{URL}/game/{game_id}/player/create", - params={"team_secret": team_secret}, - json={"player_name": player_name}) - - return response - - -def get_player(): - response = requests.get(f"http://{URL}/game/{game_id}/player/{player_id}", - params={"team_secret": team_secret}) - - return response - - -def delete_player(): - response = requests.get(f"http://{URL}/game/{game_id}/player/{player_id}/delete", - params={"team_secret": team_secret}) - - return response - - -def list_orders(): - response = requests.get(f"http://{URL}/game/{game_id}/market/order/list", - params={"team_secret": team_secret}) - - return response - - -def list_prices(): - - # @router.get("/game/{game_id}/market/prices") - # async def market_prices(start_tick: int = Query(default=None), - # end_tick: int = Query(default=None), - # resource: Resource = Query(default=None), - # game: Game = Depends(game_dep)) -> Dict[Resource, List[MarketPricesResponse]]: - - start_tick = input("Enter start tick: ") - end_tick = input("Enter end tick: ") - - response = requests.get(f"http://{URL}/game/{game_id}/market/prices", - params={"team_secret": team_secret, "start_tick": start_tick, "end_tick": end_tick}) - - return response - - -def set_energy_price(): - price = input("Enter price: ") - - response = requests.post(f"http://{URL}/game/{game_id}/player/{player_id}/market/energy/set_price", - params={"team_secret": team_secret}, - json={"price": price}) - - return response - - -def create_order(): - available = ["COAL", "URANIUM", "BIOMASS", "GAS", "OIL", "ENERGY"] - print("Available resources: ", available) - resource = input("Enter resource: ") - if resource not in available: - print("Invalid resource") - return - - price = input("Enter price: ") - size = input("Enter size: ") - expiration_tick = input("Enter expiration tick: ") - side = input("Enter side (BUY/SELL): ") - if side not in ["BUY", "SELL"]: - print("Invalid side") - return - - order_type = input("Enter order type (LIMIT/MARKET): ") - if order_type not in ["LIMIT", "MARKET"]: - print("Invalid order type") - return - - response = requests.post(f"http://{URL}/game/{game_id}/player/{player_id}/market/order/create", - params={"team_secret": team_secret}, - json={ - "resource": resource, - "price": price, - "size": size, - "expiration_tick": expiration_tick, - "side": side, - "type": order_type - }) - - return response - - -def list_player_orders(): - response = requests.get(f"http://{URL}/game/{game_id}/player/{player_id}/market/order/list", - params={"team_secret": team_secret}) - - return response - - -def cancel_order(): - ids = input("Enter order ids (0,1,2): ") - ids = list(map(int, ids.split(","))) - - response = requests.post(f"http://{URL}/game/{game_id}/player/{player_id}/market/order/cancel", - params={"team_secret": team_secret}, - json={"ids": ids}) - - return response - - -def list_plants(): - response = requests.get(f"http://{URL}/game/{game_id}/player/{player_id}/plant/list", - params={"team_secret": team_secret}) - - return response - - -def buy_plant(): - type = input("Enter plant type: ") - response = requests.post(f"http://{URL}/game/{game_id}/player/{player_id}/plant/buy", - params={"team_secret": team_secret}, - json={"type": type}) - - return response - - -def sell_plant(): - type = input("Enter plant type: ") - response = requests.post(f"http://{URL}/game/{game_id}/player/{player_id}/plant/sell", - params={"team_secret": team_secret}, - json={"type": type}) - - return response - - -def turn_on_plant(): - type = input("Enter plant type: ") - response = requests.post(f"http://{URL}/game/{game_id}/player/{player_id}/plant/on", - params={"team_secret": team_secret}, - json={"type": type}) - - return response - - -def get_dataset(): - start_tick = input("Enter start tick: ") - end_tick = input("Enter end tick: ") - - response = requests.get(f"http://{URL}/game/{game_id}/dataset", - params={"team_secret": team_secret, "start_tick": start_tick, "end_tick": end_tick}) - - return response - - -def main(): - global team_secret, game_id, player_id - - while True: - try: - response = get_player() - if response.status_code != 200: - raise Exception( - f"Player fetching code: {response.status_code}\n{response.json()}") - except Exception as e: - print("Error when fetching player") - print("Please set game id, team secret and player id") - print("Error: ", e) - - print() - print("Choose action:") - print("Current game id: ", game_id) - print("Current team secret: ", team_secret) - print("Current player id: ", player_id) - print(f"1. Enter game id") - print(f"2. Enter team secret") - print(f"3. Enter player id") - print(f"4. List games") - print(f"5. List players") - print(f"6. Create player") - print(f"7. Get player") - print(f"8. Delete player") - print(f"9. List orders in market") - print(f"10. List prices in time period") - print(f"11. Set energy price for player") - print(f"12. Create order in market") - print(f"13. List player orders in market") - print(f"14. Cancel order in market") - print(f"15. List plants") - print(f"16. Buy plant") - print(f"17. Sell plant") - print(f"18. Turn on plant") - print(f"19. Dataset") - print(f"20. Exit") - print() - - action = input(">") - - case = { - "1": set_game_id, - "2": set_team_secret, - "3": set_player_id, - "4": list_games, - "5": list_players, - "6": create_player, - "7": get_player, - "8": delete_player, - "9": list_orders, - "10": list_prices, - "11": set_energy_price, - "12": create_order, - "13": list_player_orders, - "14": cancel_order, - "15": list_plants, - "16": buy_plant, - "17": sell_plant, - "18": turn_on_plant, - "29": get_dataset, - "20": lambda: exit() - } - - try: - response = case[action]() - - if response.status_code != 200: - print("Error " + response.status_code) - pprint(response.json()) - except Exception as e: - print("Error: ", e) - - print() - - -if __name__ == "__main__": - main() diff --git a/backend/db/migration.py b/backend/db/migration.py index 7139ba2..b4876b7 100644 --- a/backend/db/migration.py +++ b/backend/db/migration.py @@ -57,6 +57,8 @@ async def run_migrations(): 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, @@ -158,6 +160,8 @@ async def run_migrations(): volume INT, 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 ( diff --git a/backend/routers/admin/admin.py b/backend/routers/admin/admin.py index dfc332f..a15e38b 100644 --- a/backend/routers/admin/admin.py +++ b/backend/routers/admin/admin.py @@ -7,9 +7,9 @@ def admin_dep(admin_secret: str = Query(description="Admin secret", default=None)): if admin_secret is None: - raise HTTPException(status_code=403, detail="Missing admin_secret") + raise HTTPException(status_code=403, detail="Unauthorized") if admin_secret != config["admin"]["secret"]: - raise HTTPException(status_code=403, detail="Invalid admin_secret") + raise HTTPException(status_code=403, detail="Unauthorized") router = APIRouter(dependencies=[Depends(admin_dep)], include_in_schema=False) @@ -22,6 +22,7 @@ async def migrate() -> SuccessfulResponse: await migration.run_migrations() return SuccessfulResponse() + router.include_router(player.router) router.include_router(dataset.router) router.include_router(game.router) diff --git a/backend/routers/admin/game.py b/backend/routers/admin/game.py index 8c6780e..8cd19f2 100644 --- a/backend/routers/admin/game.py +++ b/backend/routers/admin/game.py @@ -8,6 +8,7 @@ from routers.model import SuccessfulResponse from db import limiter + router = APIRouter() diff --git a/backend/routers/admin/player.py b/backend/routers/admin/player.py index 87d7af3..a2aa6a9 100644 --- a/backend/routers/admin/player.py +++ b/backend/routers/admin/player.py @@ -1,14 +1,19 @@ -from fastapi import APIRouter +from fastapi import APIRouter, Depends from db import limiter from model.team import Team +from model import Game, Player from routers.model import SuccessfulResponse +from routers.users.dependencies import game_dep, player_dep router = APIRouter() -@router.get("/team/{team_id}/delete") +@router.get("/game/{game_id}/player/{player_id}/delete") @limiter.exempt -async def team_delete(team_id: int) -> SuccessfulResponse: - await Team.update(team_id=team_id, is_active=False) +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) + if game.is_contest: + return SuccessfulResponse(message=f"Warning: This game is a contest game! Deleted player {player.player_id}.") return SuccessfulResponse() diff --git a/backend/routers/model.py b/backend/routers/model.py index b300a2a..2b2bd96 100644 --- a/backend/routers/model.py +++ b/backend/routers/model.py @@ -3,3 +3,4 @@ class SuccessfulResponse(BaseModel): successful: bool = True + message: str = "Operation done successfully" diff --git a/backend/routers/users/game.py b/backend/routers/users/game.py index e6c2dbe..bf9ef02 100644 --- a/backend/routers/users/game.py +++ b/backend/routers/users/game.py @@ -3,10 +3,12 @@ from typing import Dict, List from fastapi import APIRouter, Depends from pydantic import BaseModel +from dataclasses import asdict from model import Game from model.dataset_data import DatasetData from routers.users.dependencies import game_dep, start_end_tick_dep from datetime import datetime +import pandas as pd router = APIRouter() @@ -29,6 +31,16 @@ async def game_list() -> List[GameData]: return games +class GameTimeData(GameData): + current_time: datetime + + +@router.get("/game/{game_id}") +async def get_game(game_id: int) -> GameTimeData: + game = await Game.get(game_id=game_id) + return GameTimeData(**asdict(game), current_time=pd.Timestamp.now()) + + class DatasetListResponseItem(BaseModel): tick: int date: str diff --git a/backend/routers/users/market.py b/backend/routers/users/market.py index ad3576c..989ac66 100644 --- a/backend/routers/users/market.py +++ b/backend/routers/users/market.py @@ -84,7 +84,8 @@ async def order_list(game: Game = Depends(game_dep)) -> List[OrderResponse]: @router.get("/game/{game_id}/player/{player_id}/orders") -async def order_list_player(game: Game = Depends(game_dep), player: Player = Depends(player_dep)) -> List[OrderResponse]: +async def order_list_player(game: Game = Depends(game_dep), + player: Player = Depends(player_dep)) -> List[OrderResponse]: return await Order.list( game_id=game.game_id, player_id=player.player_id, @@ -98,12 +99,13 @@ class UserOrder(BaseModel): size: int expiration_tick: int side: OrderSide - type: OrderType @router.post("/game/{game_id}/player/{player_id}/orders/create") -async def order_create_player(order: UserOrder, game: Game = Depends(game_dep), player: Player = Depends(player_dep)) -> SuccessfulResponse: - if order.type == OrderType.ENERGY: +async def order_create_player(order: UserOrder, + game: Game = Depends(game_dep), + player: Player = Depends(player_dep)) -> SuccessfulResponse: + if order.resource == Resource.ENERGY: raise HTTPException( status_code=400, detail="Use /game/{game_id}/player/{player_id}/energy/set_price to set energy price") @@ -111,7 +113,7 @@ async def order_create_player(order: UserOrder, game: Game = Depends(game_dep), await Order.create( game_id=game.game_id, player_id=player.player_id, - order_type=order.type, + order_type=OrderType.LIMIT, order_side=order.side, order_status=OrderStatus.PENDING, timestamp=pd.Timestamp.now(), @@ -119,9 +121,8 @@ async def order_create_player(order: UserOrder, game: Game = Depends(game_dep), size=order.size, tick=game.current_tick, expiration_tick=order.expiration_tick, - resource=order.resource.value + resource=order.resource ) - return SuccessfulResponse() diff --git a/backend/routers/users/power_plant.py b/backend/routers/users/power_plant.py index aaf3441..1fe649a 100644 --- a/backend/routers/users/power_plant.py +++ b/backend/routers/users/power_plant.py @@ -22,22 +22,22 @@ class PowerPlantData(BaseModel): @router.get("/game/{game_id}/player/{player_id}/plant/list") async def list_plants(player: Player = Depends(player_dep)) -> Dict[str, PowerPlantData]: return { - x.name: { - "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"]), - } + 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"]), + ) for x in PowerPlantType } -class PlantBuySell(BaseModel): +class PowerPlantTypeData(BaseModel): type: PowerPlantType @router.post("/game/{game_id}/player/{player_id}/plant/buy") -async def buy_plant(plant: PlantBuySell, player: Player = Depends(player_dep)) -> SuccessfulResponse: +async def buy_plant(plant: PowerPlantTypeData, player: Player = Depends(player_dep)) -> SuccessfulResponse: type = PowerPlantType(plant.type) async with database.transaction(): @@ -55,7 +55,7 @@ async def buy_plant(plant: PlantBuySell, player: Player = Depends(player_dep)) - @router.post("/game/{game_id}/player/{player_id}/plant/sell") -async def sell_plant(plant: PlantBuySell, player: Player = Depends(player_dep)) -> SuccessfulResponse: +async def sell_plant(plant: PowerPlantTypeData, player: Player = Depends(player_dep)) -> SuccessfulResponse: type = PowerPlantType(plant.type) async with database.transaction(): diff --git a/backend/routers/users/users.py b/backend/routers/users/users.py index 5c560db..a212f7f 100644 --- a/backend/routers/users/users.py +++ b/backend/routers/users/users.py @@ -1,6 +1,7 @@ from fastapi import APIRouter from . import player, game, power_plant, market + router = APIRouter() diff --git a/backend/start.sh b/backend/start.sh index aea45b7..1816f41 100644 --- a/backend/start.sh +++ b/backend/start.sh @@ -15,8 +15,8 @@ redis-server --daemonize yes if [[ "$TESTING" == 1 ]]; then echo "Running main with reload" -TESTING=1 uvicorn main:app --reload --host=0.0.0.0 --port=3004 +TESTING=1 uvicorn main:app --reload --host=0.0.0.0 --port=3000 else echo "Running uvicorn main with 4 workers" -TESTING=0 uvicorn main:app --workers 4 --host=0.0.0.0 --port=3004 +TESTING=0 uvicorn main:app --workers 4 --host=0.0.0.0 --port=3000 fi \ No newline at end of file diff --git a/backend/test.ipynb b/backend/test.ipynb deleted file mode 100644 index 539e589..0000000 --- a/backend/test.ipynb +++ /dev/null @@ -1,259 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 69, - "id": "25d31bcd-1861-4583-ac64-45f421e99440", - "metadata": {}, - "outputs": [], - "source": [ - "import requests" - ] - }, - { - "cell_type": "code", - "execution_count": 70, - "id": "b5b58cce-d433-4f3d-8f84-42aae47ee77e", - "metadata": {}, - "outputs": [], - "source": [ - "team_secret = \"gogi\"" - ] - }, - { - "cell_type": "code", - "execution_count": 71, - "id": "d7081395-6abc-460c-ad08-9b081e2ecfa1", - "metadata": {}, - "outputs": [], - "source": [ - "addr = \"https://4903-93-141-154-92.ngrok-free.app\"" - ] - }, - { - "cell_type": "code", - "execution_count": 72, - "id": "9af62673-65be-4e1c-b75e-629e9c3973e5", - "metadata": {}, - "outputs": [], - "source": [ - "def game_list():\n", - " url = f\"{addr}/game/list?team_secret={team_secret}\"\n", - " print(url)\n", - " response = requests.get(url)\n", - " return response.json()" - ] - }, - { - "cell_type": "code", - "execution_count": 73, - "id": "2d4a7e04-4e17-43d8-97d5-829efe28718b", - "metadata": {}, - "outputs": [], - "source": [ - "def list_player():\n", - " url = f\"{addr}/game/{game_id}/player/list\"\n", - " return requests.get(url, params={\"team_secret\": team_secret}).json()" - ] - }, - { - "cell_type": "code", - "execution_count": 74, - "id": "6d683616-e891-493e-b7ea-9656bd1aad62", - "metadata": {}, - "outputs": [], - "source": [ - "def create_player():\n", - " url = f\"{addr}/game/{game_id}/player/create\"\n", - " return requests.post(url, params={\"team_secret\": team_secret}).json()" - ] - }, - { - "cell_type": "code", - "execution_count": 75, - "id": "e4df7b4e-210f-43e9-998a-252be2cd4629", - "metadata": {}, - "outputs": [], - "source": [ - "def get_player():\n", - " url = f\"{addr}/game/{game_id}/player/{player_id}\"\n", - " return requests.get(url, params={\"team_secret\": team_secret}).json()" - ] - }, - { - "cell_type": "code", - "execution_count": 76, - "id": "84721896-603e-4e57-bebf-92f767410b98", - "metadata": {}, - "outputs": [], - "source": [ - "def get_prices(start_tick=None, end_tick=None, resource=None):\n", - " url = f\"{addr}/game/{game_id}/market/prices\"\n", - " params = {\"team_secret\": team_secret}\n", - " if start_tick: params[\"start_tick\"] = start_tick\n", - " if end_tick: params[\"end_tick\"] = end_tick\n", - " if resource: params[\"resource\"] = resource\n", - " return requests.get(url, params=params).json()" - ] - }, - { - "cell_type": "code", - "execution_count": 88, - "id": "f0502559-2a71-4b08-9960-5800cfe3b4f8", - "metadata": {}, - "outputs": [], - "source": [ - "def get_dataset(start_tick=None, end_tick=None):\n", - " url = f\"{addr}/game/{game_id}/dataset\"\n", - " params = {\"team_secret\": team_secret}\n", - " if start_tick: params[\"start_tick\"] = start_tick\n", - " if end_tick: params[\"end_tick\"] = end_tick\n", - " return requests.get(url, params=params).json()" - ] - }, - { - "cell_type": "code", - "execution_count": 79, - "id": "0d4836fb-959d-45d5-9f99-29e6134dfe94", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'player_id': 11, 'player_name': 'Goranov_tim_1'}" - ] - }, - "execution_count": 79, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "game_id = 1\n", - "create_player()" - ] - }, - { - "cell_type": "code", - "execution_count": 81, - "id": "568ba6c9-24a2-4732-bfc0-4da63d4b5479", - "metadata": {}, - "outputs": [], - "source": [ - "player_id = 11" - ] - }, - { - "cell_type": "code", - "execution_count": 82, - "id": "c38850b8-d12f-4d22-adea-c4ed86aef7d3", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[{'player_id': 11, 'player_name': 'Goranov_tim_1'},\n", - " {'player_id': 1, 'player_name': 'Goran'}]" - ] - }, - "execution_count": 82, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "list_player()" - ] - }, - { - "cell_type": "code", - "execution_count": 112, - "id": "79fb7bd3-a2a2-4e49-b25e-8cd9f9575650", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'player_id': 11,\n", - " 'player_name': 'Goranov_tim_1',\n", - " 'game_id': 1,\n", - " 'energy_price': 1000000000,\n", - " 'money': 15000,\n", - " 'coal': 0,\n", - " 'uranium': 0,\n", - " 'biomass': 0,\n", - " 'gas': 0,\n", - " 'oil': 0}" - ] - }, - "execution_count": 112, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "get_player()" - ] - }, - { - "cell_type": "code", - "execution_count": 104, - "id": "1071a2e9-18d2-47c7-a87a-fcbd46131e00", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'detail': \"'NoneType' object is not callable\"}" - ] - }, - "execution_count": 104, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# get_prices(1, 5, resource=\"COAL\")\n", - "get_dataset(1,5)" - ] - }, - { - "cell_type": "markdown", - "id": "a28cb91b-f4e5-44a5-a9ab-e9c86fc2e9cc", - "metadata": {}, - "source": [ - "#\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a31b28d2-9e04-4cac-84ee-4d91dd9c1df9", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.1" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/backend/cli_player/admin_player.py b/testing/admin_player_cli.py similarity index 100% rename from backend/cli_player/admin_player.py rename to testing/admin_player_cli.py diff --git a/testing/algotrade_api.py b/testing/algotrade_api.py new file mode 100644 index 0000000..c404082 --- /dev/null +++ b/testing/algotrade_api.py @@ -0,0 +1,167 @@ +from pprint import pprint +import requests + + +URL = "localhost:8000" + +team_secret = "" +game_id = "1" +player_id = "1" + + +def set_url(new_URL): + global URL + URL = new_URL + + +def set_team_secret(new_team_secret): + global team_secret + team_secret = new_team_secret + + +def set_game_id(new_game_id): + global game_id + game_id = new_game_id + + +def set_player_id(new_player_id): + global player_id + player_id = new_player_id + + +class Resource(): + energy = "ENERGY" + coal = "COAL" + uranium = "URANIUM" + biomass = "BIOMASS" + gas = "GAS" + oil = "OIL" + + +class PowerPlant(): + COAL = "COAL" + URANIUM = "URANIUM" + BIOMASS = "BIOMASS" + GAS = "GAS" + OIL = "OIL" + GEOTHERMAL = "GEOTHERMAL" + WIND = "WIND" + SOLAR = "SOLAR" + HYDRO = "HYDRO" + + +class OrderSide(): + BUY = "BUY" + SELL = "SELL" + + +def get_games(): + return requests.get(f"http://{URL}/game/list", + params={"team_secret": team_secret}) + + +def get_game(): + return requests.get(f"http://{URL}/game/{game_id}", + params={"team_secret": team_secret}) + + +def get_players(): + return requests.get(f"http://{URL}/game/{game_id}/player/list", + params={"team_secret": team_secret}) + + +def create_player(player_name: str = None): + return requests.post( + f"http://{URL}/game/{game_id}/player/create", + params={"team_secret": team_secret}, + json={"player_name": player_name}) + + +def get_player(): + return requests.get(f"http://{URL}/game/{game_id}/player/{player_id}", + params={"team_secret": team_secret}) + + +def delete_player(): + return requests.get(f"http://{URL}/game/{game_id}/player/{player_id}/delete", + params={"team_secret": team_secret}) + + +def get_orders(): + return requests.get(f"http://{URL}/game/{game_id}/orders", + params={"team_secret": team_secret}) + + +def get_player_orders(): + return requests.get(f"http://{URL}/game/{game_id}/player/{player_id}/orders", + params={"team_secret": team_secret}) + + +def get_prices(start_tick=None, end_tick=None, resource=None): + url = f"{URL}/game/{game_id}/market/prices" + params = {"team_secret": team_secret} + if start_tick: params["start_tick"] = start_tick + if end_tick: params["end_tick"] = end_tick + if resource: params["resource"] = resource + return requests.get(url, params=params) + + +def set_energy_price(price): + requests.post(f"http://{URL}/game/{game_id}/player/{player_id}/energy/set_price", + params={"team_secret": team_secret}, + json={"price": price}) + + +def create_order(resource, price, size, expiration_tick, side): + body = { + "resource": resource, + "price": price, + "size": size, + "expiration_tick": expiration_tick, + "side": side + } + requests.post(f"http://{URL}/game/{game_id}/player/{player_id}/orders/create", + params={"team_secret": team_secret}, json=body) + + +def cancel_orders(ids): + return requests.post(f"http://{URL}/game/{game_id}/player/{player_id}/market/order/cancel", + params={"team_secret": team_secret}, + json={"ids": ids}) + + +def get_plants(): + return requests.get(f"http://{URL}/game/{game_id}/player/{player_id}/plant/list", + params={"team_secret": team_secret}) + + +def buy_plant(type): + return requests.post(f"http://{URL}/game/{game_id}/player/{player_id}/plant/buy", + params={"team_secret": team_secret}, + json={"type": type}) + + +def sell_plant(type): + return requests.post(f"http://{URL}/game/{game_id}/player/{player_id}/plant/sell", + params={"team_secret": team_secret}, + json={"type": type}) + + +def turn_on_plant(type): + return requests.post(f"http://{URL}/game/{game_id}/player/{player_id}/plant/on", + params={"team_secret": team_secret}, + json={"type": type}) + + +def turn_off_plant(type): + return requests.post(f"http://{URL}/game/{game_id}/player/{player_id}/plant/off", + params={"team_secret": team_secret}, + json={"type": type}) + + +def get_dataset(start_tick=None, end_tick=None): + url = f"{URL}/game/{game_id}/dataset" + params = {"team_secret": team_secret} + if start_tick: params["start_tick"] = start_tick + if end_tick: params["end_tick"] = end_tick + return requests.get(url, params=params).json() \ No newline at end of file diff --git a/testing/cheat_sheet.ipynb b/testing/cheat_sheet.ipynb new file mode 100644 index 0000000..5e45650 --- /dev/null +++ b/testing/cheat_sheet.ipynb @@ -0,0 +1,266 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 76, + "id": "25d31bcd-1861-4583-ac64-45f421e99440", + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "from enum import Enum\n", + "import algotrade_api\n", + "from algotrade_api import PowerPlant\n", + "from time import sleep" + ] + }, + { + "cell_type": "code", + "execution_count": 77, + "id": "b5b58cce-d433-4f3d-8f84-42aae47ee77e", + "metadata": {}, + "outputs": [], + "source": [ + "algotrade_api.set_team_secret(\"gogi\")" + ] + }, + { + "cell_type": "code", + "execution_count": 78, + "id": "d7081395-6abc-460c-ad08-9b081e2ecfa1", + "metadata": {}, + "outputs": [], + "source": [ + "algotrade_api.set_url(\"localhost:3000\")" + ] + }, + { + "cell_type": "code", + "execution_count": 79, + "id": "0d4836fb-959d-45d5-9f99-29e6134dfe94", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'player_id': 23, 'player_name': '5'}" + ] + }, + "execution_count": 79, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "algotrade_api.set_game_id(1)\n", + "algotrade_api.create_player(5).json()" + ] + }, + { + "cell_type": "code", + "execution_count": 80, + "id": "568ba6c9-24a2-4732-bfc0-4da63d4b5479", + "metadata": {}, + "outputs": [], + "source": [ + "algotrade_api.set_player_id(20)" + ] + }, + { + "cell_type": "code", + "execution_count": 81, + "id": "c38850b8-d12f-4d22-adea-c4ed86aef7d3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'detail': 'Game is already finished'}" + ] + }, + "execution_count": 81, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "algotrade_api.get_player().json()" + ] + }, + { + "cell_type": "code", + "execution_count": 82, + "id": "6b0d2f54-8518-4490-9270-427986507a6b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'game_id': 1,\n", + " 'game_name': 'Stalna igra',\n", + " 'is_contest': False,\n", + " 'start_time': '2024-02-28T22:15:43.889216',\n", + " 'total_ticks': 2300,\n", + " 'tick_time': 3000,\n", + " 'current_tick': 2300,\n", + " 'is_finished': True,\n", + " 'current_time': '2024-02-28T23:32:02.596224'}" + ] + }, + "execution_count": 82, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "algotrade_api.get_game().json()" + ] + }, + { + "cell_type": "code", + "execution_count": 83, + "id": "a82358c8-1c3c-4f8b-9b48-12b7a53155ca", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'detail': 'Game is already finished'}" + ] + }, + "execution_count": 83, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "algotrade_api.get_player_orders().json()" + ] + }, + { + "cell_type": "code", + "execution_count": 84, + "id": "c6a44c69-844d-40cc-8bce-bad147d802d4", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'detail': 'Game is already finished'}" + ] + }, + "execution_count": 84, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "algotrade_api.get_orders().json()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "fd6ce0b2-34d1-4714-9f0b-c98f0838ef6c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'detail': 'Game is already finished'}" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "algotrade_api.get_plants().json()" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "6746a7ee-f518-40ee-b90a-f34e3fa604dc", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'detail': 'Game is already finished'}" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "algotrade_api.buy_plant(PowerPlant.COAL).json()" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "id": "e5773e31-1620-4d2f-95b3-85e62926e05e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'detail': 'Game is already finished'}\n", + "{'detail': 'Game is already finished'}\n", + "{'detail': 'Game is already finished'}\n" + ] + }, + { + "ename": "KeyboardInterrupt", + "evalue": "", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[60], line 3\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;28;01mwhile\u001b[39;00m \u001b[38;5;28;01mTrue\u001b[39;00m:\n\u001b[1;32m 2\u001b[0m \u001b[38;5;28mprint\u001b[39m(algotrade_api\u001b[38;5;241m.\u001b[39mget_player()\u001b[38;5;241m.\u001b[39mjson())\n\u001b[0;32m----> 3\u001b[0m \u001b[43msleep\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mKeyboardInterrupt\u001b[0m: " + ] + } + ], + "source": [ + "while True:\n", + " print(algotrade_api.get_player().json())\n", + " sleep(1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41d5c9ad-be0c-48f4-94b6-57efd515ee21", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/testing/player_cli.py b/testing/player_cli.py new file mode 100644 index 0000000..3da3ec9 --- /dev/null +++ b/testing/player_cli.py @@ -0,0 +1,194 @@ +from pprint import pprint +import algotrade_api + + +def input_team_secret(): + new_team_secret = input("Enter new team secret: ") + + if new_team_secret: + algotrade_api.set_team_secret(new_team_secret) + print(f"Team secret set to: {new_team_secret}") + else: + print("Team secret not set") + + +def input_game_id(): + new_game_id = input("Enter new game id: ") + + if new_game_id: + algotrade_api.set_game_id(new_game_id) + print(f"Game id set to: {new_game_id}") + else: + print("Game id not set") + + +def input_player_id(): + new_player_id = input("Enter new player id: ") + + if new_player_id: + algotrade_api.set_player_id(new_player_id) + print(f"Player id set to: {new_player_id}") + else: + print("Player id not set") + + + + +def create_player(): + player_name = input("Enter player name: ") + return algotrade_api.create_player(player_name) + + +def list_prices(): + start_tick = int(input("Enter start tick: ")) + end_tick = int(input("Enter end tick: ")) + return algotrade_api.get_prices(start_tick, end_tick) + + +def set_energy_price(): + price = input("Enter price: ") + return algotrade_api.set_energy_price(price) + + +def create_order(): + available = ["COAL", "URANIUM", "BIOMASS", "GAS", "OIL", "ENERGY"] + print("Available resources: ", available) + resource = input("Enter resource: ") + if resource not in available: + print("Invalid resource") + return + + price = input("Enter price: ") + size = input("Enter size: ") + expiration_tick = input("Enter expiration tick: ") + side = input("Enter side (BUY/SELL): ") + if side not in ["BUY", "SELL"]: + print("Invalid side") + return + + # order_type = input("Enter order type (LIMIT/MARKET): ") + # if order_type not in ["LIMIT", "MARKET"]: + # print("Invalid order type") + # return + + return algotrade_api.create_order(price=price, side=side, size=size, + expiration_tick=expiration_tick, + resource=resource) + + +def list_player_orders(): + return algotrade_api.get_player_orders() + + +def cancel_order(): + ids = input("Enter order ids (0,1,2): ") + ids = list(map(int, ids.split(","))) + return algotrade_api.cancel_orders(ids) + + +def buy_plant(): + type = input("Enter plant type: ") + return algotrade_api.buy_plant(type) + + +def sell_plant(): + type = input("Enter plant type: ") + return algotrade_api.sell_plant(type) + + +def turn_on_plant(): + type = input("Enter plant type: ") + return algotrade_api.turn_on_plant(type) + + +def turn_off_plant(): + type = input("Enter plant type: ") + return algotrade_api.turn_off_plant(type) + + +def get_dataset(): + start_tick = input("Enter start tick: ") + end_tick = input("Enter end tick: ") + return get_dataset(start_tick, end_tick) + + +def main(): + global team_secret, game_id, player_id + + while True: + try: + response = algotrade_api.get_player() + if response.status_code != 200: + raise Exception( + f"Player fetching code: {response.status_code}\n{response.json()}") + except Exception as e: + print("Error when fetching player") + print("Please set game id, team secret and player id") + print("Error: ", e) + + print() + print("Choose action:") + print("Current game id: ", algotrade_api.game_id) + print("Current team secret: ", algotrade_api.team_secret) + print("Current player id: ", algotrade_api.player_id) + print(f"1. Enter game id") + print(f"2. Enter team secret") + print(f"3. Enter player id") + print(f"4. List games") + print(f"5. List players") + print(f"6. Create player") + print(f"7. Get player") + print(f"8. Delete player") + print(f"9. List orders in market") + print(f"10. List prices in time period") + print(f"11. Set energy price for player") + print(f"12. Create order in market") + print(f"13. List player orders in market") + print(f"14. Cancel order in market") + print(f"15. List plants") + print(f"16. Buy plant") + print(f"17. Sell plant") + print(f"18. Turn on plant") + print(f"19. Dataset") + print(f"20. Exit") + print() + + action = input(">") + + case = { + "1": input_game_id, + "2": input_team_secret, + "3": input_player_id, + "4": algotrade_api.get_games, + "5": algotrade_api.get_players, + "6": create_player, + "7": algotrade_api.get_player, + "8": algotrade_api.delete_player, + "9": algotrade_api.get_orders, + "10": list_prices, + "11": set_energy_price, + "12": create_order, + "13": list_player_orders, + "14": cancel_order, + "15": algotrade_api.get_plants, + "16": buy_plant, + "17": sell_plant, + "18": turn_on_plant, + "29": get_dataset, + "20": lambda: exit() + } + + try: + response = case[action]() + + if response.status_code != 200: + print("Error " + response.status_code) + pprint(response.json()) + except Exception as e: + print("Error: ", e) + + print() + + +if __name__ == "__main__": + main()