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..b4876b7 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, @@ -59,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, @@ -71,8 +71,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 +128,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,14 +151,17 @@ 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) )''') + + 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/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/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 416588b..9558227 100644 --- a/backend/model/market.py +++ b/backend/model/market.py @@ -19,6 +19,7 @@ class Market(Table): open: int close: int market: int + volume: int @classmethod async def create(cls, *args, **kwargs) -> int: @@ -27,3 +28,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_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/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..a15e38b 100644 --- a/backend/routers/admin/admin.py +++ b/backend/routers/admin/admin.py @@ -1,27 +1,29 @@ 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)): 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) @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..8cd19f2 100644 --- a/backend/routers/admin/game.py +++ b/backend/routers/admin/game.py @@ -4,14 +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 +15,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 +22,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 +32,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 +39,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 +97,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..a2aa6a9 --- /dev/null +++ b/backend/routers/admin/player.py @@ -0,0 +1,19 @@ +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("/game/{game_id}/player/{player_id}/delete") +@limiter.exempt +async def player_delete(game: Game = Depends(game_dep), + player: Player = Depends(player_dep)) -> SuccessfulResponse: + await Player.update(player_id=player.player_id, is_active=False) + 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/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..2b2bd96 --- /dev/null +++ b/backend/routers/model.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel + + +class SuccessfulResponse(BaseModel): + successful: bool = True + message: str = "Operation done successfully" 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..bf9ef02 100644 --- a/backend/routers/users/game.py +++ b/backend/routers/users/game.py @@ -1,17 +1,76 @@ -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 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 -# 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 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 + 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..989ac66 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,15 @@ 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 @@ -87,59 +99,47 @@ class UserOrder(BaseModel): size: int expiration_tick: int side: OrderSide - 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: - if order.type == OrderType.ENERGY: - raise Exception( - "Use /game/{game_id}/player/{player_id}/market/energy/set_price to set energy price") +@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.resource == Resource.ENERGY: + 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, 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(), 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 + resource=order.resource ) + 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..1fe649a 100644 --- a/backend/routers/users/power_plant.py +++ b/backend/routers/users/power_plant.py @@ -5,41 +5,39 @@ 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"], - "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)): +async def buy_plant(plant: PowerPlantTypeData, 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: PowerPlantTypeData, 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/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 b2777a7..1816f41 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=3000 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=3000 fi \ No newline at end of file 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; diff --git a/testing/admin_player_cli.py b/testing/admin_player_cli.py new file mode 100644 index 0000000..612f2c3 --- /dev/null +++ b/testing/admin_player_cli.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/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()