diff --git a/backend/fixtures/orderbook_fixtures.py b/backend/fixtures/orderbook_fixtures.py index e13d62c..f49b5d0 100644 --- a/backend/fixtures/orderbook_fixtures.py +++ b/backend/fixtures/orderbook_fixtures.py @@ -109,7 +109,7 @@ def check_trade(trade: Trade): sell_order = trade.sell_order if buy_order is None or sell_order is None: - assert trade.total_money == 0 + assert trade.total_price == 0 assert trade.trade_size == 0 assert trade.trade_price is None return {"can_buy": False, "can_sell": False} @@ -117,16 +117,16 @@ def check_trade(trade: Trade): buyer_id = buy_order.player_id seller_id = sell_order.player_id - can_buy = traders[buyer_id].money >= trade.total_money + can_buy = traders[buyer_id].money >= trade.total_price can_sell = traders[seller_id].resources.coal >= trade.trade_size if not can_buy or not can_sell: return {"can_buy": can_buy, "can_sell": can_sell} - traders[buyer_id].money -= trade.total_money + traders[buyer_id].money -= trade.total_price traders[buyer_id].resources.coal += trade.trade_size - traders[seller_id].money += trade.total_money + traders[seller_id].money += trade.total_price traders[seller_id].resources.coal -= trade.trade_size return {"can_buy": True, "can_sell": True} diff --git a/backend/game/market/energy_market.py b/backend/game/market/energy_market.py index 1fc1012..ec9e255 100644 --- a/backend/game/market/energy_market.py +++ b/backend/game/market/energy_market.py @@ -58,7 +58,7 @@ def create_trade(self, player: Player, tick, energy: int, energy_price: int): self.trades.append(Trade( trade_price=energy_price, trade_size=energy, - total_money=energy * energy_price, + total_price=energy * energy_price, tick=tick, buy_order_id="energy market", sell_order_id="energy market", diff --git a/backend/game/market/resource_market.py b/backend/game/market/resource_market.py index e790f48..be0db10 100644 --- a/backend/game/market/resource_market.py +++ b/backend/game/market/resource_market.py @@ -73,7 +73,7 @@ def _check_trade(self, trade: Trade): elif buyer.is_bot: can_buy = True else: - can_buy = buyer.money >= trade.total_money + can_buy = buyer.money >= trade.total_price if seller is None: can_sell = False @@ -97,11 +97,11 @@ def _on_trade(self, trade: Trade): logger.critical(f"Trading between two different bots {buyer.player_name} {buyer_id}({buyer.game_id}) and {seller.player_name} {seller_id}({seller.game_id}) in game ({self.game_id}). This is probably due to invalid reseting of the game.") if not buyer.is_bot: - buyer.money -= trade.total_money + buyer.money -= trade.total_price buyer.resources[self.resource] += trade.trade_size if not seller.is_bot: - seller.money += trade.total_money + seller.money += trade.total_price seller.resources[self.resource] -= trade.trade_size def _get_player(self, player_id: str) -> Player: diff --git a/backend/game/orderbook/orderbook.py b/backend/game/orderbook/orderbook.py index 24cb391..33b6aa0 100644 --- a/backend/game/orderbook/orderbook.py +++ b/backend/game/orderbook/orderbook.py @@ -156,11 +156,11 @@ def _match_condition(self): def _match_one(self, buy_order: Order, sell_order: Order, tick: int): trade_price = self._get_trade_price(buy_order, sell_order) trade_size = self._get_trade_size(buy_order, sell_order) - total_money = trade_price * trade_size + total_price = trade_price * trade_size trade = Trade( tick=tick, - total_money=total_money, + total_price=total_price, trade_size=trade_size, trade_price=trade_price) trade.buy_order = buy_order @@ -178,8 +178,8 @@ def _match_one(self, buy_order: Order, sell_order: Order, tick: int): buy_order.filled_size += trade_size sell_order.filled_size += trade_size - buy_order.filled_money += total_money - sell_order.filled_money += total_money + buy_order.filled_money += total_price + sell_order.filled_money += total_price buy_order.filled_price = buy_order.filled_money / buy_order.filled_size sell_order.filled_price = sell_order.filled_money / sell_order.filled_size diff --git a/backend/game/orderbook/test_orderbook.py b/backend/game/orderbook/test_orderbook.py index afea485..0289f50 100644 --- a/backend/game/orderbook/test_orderbook.py +++ b/backend/game/orderbook/test_orderbook.py @@ -67,7 +67,7 @@ def check_trade(*args, **kwargs): assert trade.sell_order is sell_order assert trade.buy_order.order_status == OrderStatus.COMPLETED assert trade.sell_order.order_status == OrderStatus.COMPLETED - assert trade.total_money == price * size + assert trade.total_price == price * size assert trade.trade_price == price assert trade.trade_size == size assert trade.tick == 1 @@ -253,7 +253,7 @@ def test_prev_price(get_order): orderbook.match(tick=1) assert len(trades) == 1 - assert trades[0].total_money == 5 * 50 + assert trades[0].total_price == 5 * 50 def test_invalid_callback_type(): diff --git a/backend/game/price_tracker/price_tracker.py b/backend/game/price_tracker/price_tracker.py index 5f5e7c5..a886f28 100644 --- a/backend/game/price_tracker/price_tracker.py +++ b/backend/game/price_tracker/price_tracker.py @@ -41,7 +41,7 @@ def _calculate_low_high(self, trades: List[Trade]): for trade in trades: price = trade.trade_price - money_sum += trade.total_money + money_sum += trade.total_price money_size += trade.trade_size if self.high is None: diff --git a/backend/game/tick/ticker.py b/backend/game/tick/ticker.py index 42fb4f6..2e4f475 100644 --- a/backend/game/tick/ticker.py +++ b/backend/game/tick/ticker.py @@ -225,9 +225,13 @@ def get_score_name(player: Player): scores=scores) def get_players_and_enter_context(self, game: Game, stack: ExitStack) -> Dict[str, Player]: - players = Player.find(Player.game_id == game.game_id).all() - for player_lock in list(map(methodcaller('lock'), players)): - stack.enter_context(player_lock) + players: List[Player] = Player.find(Player.game_id == game.game_id).all() + for player in players: + try: + stack.enter_context(player.lock()) + except Exception: + logger.critical(f"Error getting lock for player {player.player_name} {player.player_id} in game {player.game_id}") + player_pks = map(attrgetter('pk'), players) players = list(map(Player.get, player_pks)) players = {player.player_id: player for player in players} diff --git a/backend/main.py b/backend/main.py index 357c6af..862f00a 100644 --- a/backend/main.py +++ b/backend/main.py @@ -15,11 +15,6 @@ from docs import tags_metadata, short_description from redis_om import Migrator from fastapi.middleware.cors import CORSMiddleware -import sys - - -if "pytest" not in sys.modules: - Migrator().run() tick_event = asyncio.Event() @@ -44,6 +39,7 @@ async def run_game_ticks(): @asynccontextmanager async def lifespan(app: FastAPI): + Migrator().run() asyncio.create_task(run_game_ticks()) yield @@ -62,7 +58,7 @@ async def lifespan(app: FastAPI): app.add_middleware( CORSMiddleware, - allow_origins="*", + allow_origins="Access-Control-Allow-Origin", allow_credentials=True, allow_methods=["*"], allow_headers=["*"] diff --git a/backend/model/trade.py b/backend/model/trade.py index 1b655e8..7791c73 100644 --- a/backend/model/trade.py +++ b/backend/model/trade.py @@ -9,7 +9,7 @@ class Trade(JsonModel): tick: int = Field(index=True) - total_money: int + total_price: int trade_size: int trade_price: int diff --git a/backend/routers/admin/game.py b/backend/routers/admin/game.py index 5217616..b762288 100644 --- a/backend/routers/admin/game.py +++ b/backend/routers/admin/game.py @@ -14,6 +14,7 @@ from routers.model import SuccessfulResponse from db import limiter import asyncio +from logger import logger router = APIRouter() @@ -67,10 +68,13 @@ def player_list(game_id: str) -> List[Player]: def game_delete(game_id: str) -> SuccessfulResponse: # TODO ne baca exception ako je vec zavrsena # await Game.update(game_id=game_id, is_finished=True) - g = Game.get(game_id) - g.update(is_finished=True) - g.save() - return SuccessfulResponse() + try: + g = Game.get(game_id) + g.update(is_finished=True) + g.save() + return SuccessfulResponse() + except Exception: + raise HTTPException(400, "Invalid game id") @dataclass @@ -86,7 +90,10 @@ class NetworthData: @limiter.exempt def game_networth(game_id: str) -> List[NetworthData]: # game = await Game.get(game_id=game_id) - game = Game.get(game_id) + try: + game = Game.get(game_id) + except Exception: + raise HTTPException(400, "Invalid game id") if game.current_tick == 0: raise HTTPException( @@ -110,127 +117,144 @@ def game_networth(game_id: str) -> List[NetworthData]: return team_networths +class WebSocketWrapper: + def __init__(self, websocket: WebSocket): + self.websocket = websocket + async def __aenter__(self): + await self.websocket.accept() + async def __aexit__(self, *args, **kwargs): + await self.websocket.close() + + + @router.websocket("/game/{game_id}/dashboard/graphs") @limiter.exempt async def dashboard_graphs(websocket: WebSocket, game_id: str): - await websocket.accept() - - try: - while True: - # game = await Game.get(game_id=game_id) + async with WebSocketWrapper(websocket): + try: game = Game.get(game_id) + except Exception: + raise HTTPException(400, "Invalid game id") - current_tick = game.current_tick + try: + while True: + current_tick = game.current_tick - if current_tick == 0: - await asyncio.sleep(game.tick_time / 1000) - continue + if current_tick == 0: + await asyncio.sleep(game.tick_time / 1000) + continue - # dataset = (await DatasetData.list_by_game_id_where_tick( - # game.dataset_id, game.game_id, current_tick - 1, current_tick - 1))[0] - dataset = DatasetData.find( - (DatasetData.dataset_id == game.dataset_id) & - (DatasetData.tick == current_tick - 1) - ).first() + # dataset = (await DatasetData.list_by_game_id_where_tick( + # game.dataset_id, game.game_id, current_tick - 1, current_tick - 1))[0] + dataset = DatasetData.find( + (DatasetData.dataset_id == game.dataset_id) & + (DatasetData.tick == current_tick - 1) + ).first() - dataset = dataset.dict() + dataset = dataset.dict() - # all_prices = await Market.list_by_game_id_where_tick( - # game_id=game.game_id, - # min_tick=current_tick - 1, - # max_tick=current_tick - 1, - # ) + # all_prices = await Market.list_by_game_id_where_tick( + # game_id=game.game_id, + # min_tick=current_tick - 1, + # max_tick=current_tick - 1, + # ) - all_prices = Market.find( - (Market.game_id == game.game_id) & - (Market.tick == current_tick - 1) - ).all() + all_prices = Market.find( + (Market.game_id == game.game_id) & + (Market.tick == current_tick - 1) + ).all() - all_prices = [price.dict() for price in all_prices] + all_prices = [price.dict() for price in all_prices] - await websocket.send_json(json.dumps({ - **dataset, - "prices": all_prices - }, default=str)) + await websocket.send_json(json.dumps({ + **dataset, + "prices": all_prices + }, default=str)) - await asyncio.sleep(game.tick_time / 1000) - except ConnectionClosedOK: - pass + await asyncio.sleep(game.tick_time / 1000) + except ConnectionClosedOK: + pass + except HTTPException as e: + raise e + except Exception: + logger.warning("Error in websocets") + return @router.websocket("/game/{game_id}/dashboard/players") @limiter.exempt async def dashboard_players(websocket: WebSocket, game_id: str): - await websocket.accept() - - try: - while True: - # game = await Game.get(game_id=game_id) - game = Game(game_id) - - current_tick = game.current_tick + async with WebSocketWrapper(websocket): + try: + game = Game.get(game_id) + except Exception: + raise HTTPException(400, "Invalid game id") - if current_tick == 0: - await asyncio.sleep(game.tick_time / 1000) - continue + try: + while True: + current_tick = game.current_tick - # players = await Player.list(game_id=game_id) - players = Player.find(Player.game_id == game_id).all() + if current_tick == 0: + await asyncio.sleep(game.tick_time / 1000) + continue - # networths = { - # player.player_id: (await player.get_networth(game))["total"] for player in players} - networths = { - player.player_id: (await player.get_networth(game)).total for player in players} + players = Player.find(Player.game_id == game_id).all() - # players = [{**dataclasses.asdict(player), - # "networth": networths[player.player_id] - # } for player in players] + networths = { + player.player_id: (player.get_networth(game)).total for player in players} - players = [ - { - **player.dict(), - "networth": networths[player.player_id] - } for player in players - ] + players = [ + { + **player.dict(), + "networth": networths[player.player_id] + } for player in players + ] - await websocket.send_json(json.dumps({ - "current_tick": current_tick, - "players": players, - }, default=str)) + await websocket.send_json(json.dumps({ + "current_tick": current_tick, + "players": players, + }, default=str)) - await asyncio.sleep(game.tick_time / 1000) - except ConnectionClosedOK: - pass + await asyncio.sleep(game.tick_time / 1000) + except ConnectionClosedOK: + pass + except Exception: + logger.warning("Error in websocets") + return @router.websocket("/game/{game_id}/dashboard/orderbooks") @limiter.exempt async def dashboard_orderbooks(websocket: WebSocket, game_id: str): - await websocket.accept() - - try: - while True: - # game = await Game.get(game_id=game_id) + async with WebSocketWrapper(websocket): + try: game = Game.get(game_id) + except Exception: + raise HTTPException(400, "Invalid game id") - # orders = await Order.list(game_id=game_id, order_status=OrderStatus.ACTIVE) - orders = Order.find( - (Order.game_id == game.game_id) & - (Order.order_status == OrderStatus.ACTIVE.value) - ).all() + try: + while True: + # orders = await Order.list(game_id=game_id, order_status=OrderStatus.ACTIVE) + orders = Order.find( + (Order.game_id == game.game_id) & + (Order.order_status == OrderStatus.ACTIVE.value) + ).all() - # orders = [dataclasses.asdict(order) for order in orders] - orders = [order.dict() for order in orders] + # orders = [dataclasses.asdict(order) for order in orders] + orders = [order.dict() for order in orders] - orders_by_resource = defaultdict( - lambda: {str(OrderSide.BUY): [], str(OrderSide.SELL): []}) + orders_by_resource = defaultdict( + lambda: {str(OrderSide.BUY): [], str(OrderSide.SELL): []}) - for order in orders: - orders_by_resource[str(order["resource"]) - ][str(order["order_side"])].append(order) + for order in orders: + orders_by_resource[str(order["resource"]) + ][str(order["order_side"])].append(order) - await websocket.send_json(json.dumps(orders_by_resource, default=str)) + await websocket.send_json(json.dumps(orders_by_resource, default=str)) - await asyncio.sleep(game.tick_time / 1000) - except ConnectionClosedOK: - pass + await asyncio.sleep(game.tick_time / 1000) + except ConnectionClosedOK: + pass + except Exception: + logger.warning("Error in websocets") + return diff --git a/backend/routers/users/game.py b/backend/routers/users/game.py index bdb8920..4f3020d 100644 --- a/backend/routers/users/game.py +++ b/backend/routers/users/game.py @@ -1,5 +1,6 @@ from dataclasses import asdict from datetime import datetime, timedelta +from operator import attrgetter from typing import Dict, List from fastapi import APIRouter, Depends @@ -10,6 +11,7 @@ from model.power_plant_model import PowerPlantsApiModel, ResourcesApiModel from routers.users.dependencies import game_dep, start_end_tick_dep + router = APIRouter() @@ -40,7 +42,9 @@ def server_time() -> datetime: response_description="List of games", ) def game_list() -> List[GameData]: - return Game.find().sort_by("start_time").all() + games = Game.find().all() + games.sort(key=attrgetter("start_time")) + return games class GameTimeData(GameData): diff --git a/backend/routers/users/market.py b/backend/routers/users/market.py index 35ad0cc..07728be 100644 --- a/backend/routers/users/market.py +++ b/backend/routers/users/market.py @@ -17,6 +17,7 @@ from model.resource import Energy, ResourceOrEnergy from model.trade import Trade from routers.model import SuccessfulResponse +from logger import logger from .dependencies import ( check_game_active_dep, @@ -362,11 +363,11 @@ class UserTrade(BaseModel): sell_order_id: str = Field(..., description="order_id of seller side in this trade") tick: int = Field(..., description="Tick when this trade took place") - filled_money: int = Field( + total_price: int = Field( ..., description="Total value of the trade = filled_size * filled_price" ) - filled_size: int = Field(..., description="Ammount of resources that was traded") - filled_price: int = Field( + trade_size: int = Field(..., description="Ammount of resources that was traded") + trade_price: int = Field( ..., description="Price at which the unit of resource was traded" ) @@ -386,16 +387,15 @@ def get_trades_player( """ start_tick, end_tick = start_end - condition = ( - (Trade.tick <= end_tick) - & (Trade.tick >= end_tick) - & (Trade.resource == resource.value) - ) + conditions = [Trade.tick <= end_tick, Trade.tick >= start_tick] + if resource is not None: + conditions.append(Trade.resource == resource.value) - buy_trades = Trade.find((Trade.buy_order_id == player.player_id) & condition).all() + buy_trades = Trade.find(Trade.buy_order_id == player.player_id, *conditions).all() sell_trades = Trade.find( - (Trade.sell_order_id == player.player_id) & condition + Trade.sell_order_id == player.player_id, *conditions ).all() + logger.info(f"{player.player_name} {len(buy_trades)}, {len(sell_trades)}") return { OrderSide.BUY: buy_trades, OrderSide.SELL: sell_trades, diff --git a/backend/start.sh b/backend/start.sh index eebba51..324123a 100644 --- a/backend/start.sh +++ b/backend/start.sh @@ -23,5 +23,5 @@ if [[ "$TESTING" == 1 ]]; then uvicorn main:app --reload --host=0.0.0.0 --port=3000 --log-level critical else echo "Running uvicorn main with 4 workers" - uvicorn main:app --workers 4 --host=0.0.0.0 --port=3000 + uvicorn main:app --workers 4 --host=0.0.0.0 --port=3000 --log-level warning fi \ No newline at end of file