Skip to content

Commit

Permalink
Resource bots (#38)
Browse files Browse the repository at this point in the history
* started working on resource bots

* finished logic

* fixes
  • Loading branch information
rangoiv authored Feb 18, 2024
1 parent 09597ff commit de992ad
Show file tree
Hide file tree
Showing 17 changed files with 204 additions and 70 deletions.
9 changes: 9 additions & 0 deletions backend/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ player:
bots:
team_name: bots
team_secret: oiajsdpifjapsndkcapis
resource_sum: 1000
default_volume: 100
min_volume: 20
max_volume: 600
min_price: 1
max_price: 100
max_price_change: 20
expiration_ticks: 5
price_change_coeff: 0.05

max_energy_per_player: 0.2

Expand Down
4 changes: 2 additions & 2 deletions backend/db/migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ async def fill_tables():
# dataset_id = await Datasets.create(dataset_name="Dummy dataset", dataset_description="Opis")
datasets = await Datasets.list()

not_nat_game_id = await Game.create(game_name="Stalna igra", is_contest=False, bots="dummy:3", dataset_id=1, start_time=datetime.now(), total_ticks=2400, tick_time=3000)
nat_game_id = await Game.create(game_name="Natjecanje", is_contest=True, bots="dummy:2", dataset_id=1, start_time=datetime.now(), total_ticks=10, tick_time=1000)
not_nat_game_id = await Game.create(game_name="Stalna igra", is_contest=False, bots="dummy:3;resource_bot:1", dataset_id=1, start_time=datetime.now(), total_ticks=2400, tick_time=3000)
nat_game_id = await Game.create(game_name="Natjecanje", is_contest=True, bots="dummy:2;resource_bot:1", dataset_id=1, start_time=datetime.now(), total_ticks=10, tick_time=1000)

for game_id in [not_nat_game_id, nat_game_id]:
await Player.create(player_name="Goran", is_active=True, is_bot=False, game_id=game_id, team_id=g_team_id, money=15000, coal=1000)
Expand Down
4 changes: 0 additions & 4 deletions backend/db/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@
class Table:
table_name = None

def get_kwargs(self) -> dict:
cols = [field.name for field in fields(self)]
return {col: self.__getattribute__(col) for col in cols}

@classmethod
async def create(cls, col_nums: int = 1, *args, **kwargs) -> int:
data = cls(*[0 for _ in range(col_nums)], *args, **kwargs)
Expand Down
1 change: 1 addition & 0 deletions backend/game/bots/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .bot import Bot
from .bots import Bots
from .dummy import DummyBot
from .resource_bot import ResourceBot
4 changes: 2 additions & 2 deletions backend/game/bots/bot.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
from typing import List
import abc
from model import Order


class Bot():
def __init__(self, *args, **kwargs):
self.player_id = 0
self.game_id = 0

@abc.abstractmethod
async def run(self, *args, **kwargs) -> None:
pass

5 changes: 4 additions & 1 deletion backend/game/bots/bots.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
from fastapi import HTTPException
from typing import List, Tuple

from .bot import Bot
from .dummy import DummyBot
from .resource_bot import ResourceBot


class Bots:
bots = {
"dummy": DummyBot
"dummy": DummyBot,
"resource_bot": ResourceBot
}

def exists(bot_id):
Expand Down
127 changes: 127 additions & 0 deletions backend/game/bots/resource_bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import pandas as pd
from game.tick.tick_data import TickData
from model import Order, Resource, OrderSide, Team, Player
from . import Bot
from config import config


resource_wanted_sum = config['bots']['resource_sum']
default_volume = config['bots']['default_volume']
min_volume = config['bots']['min_volume']
max_volume = config['bots']['max_volume']
min_price = config['bots']['min_price']
max_price = config['bots']['max_price']
price_change_coeff = config['bots']['price_change_coeff']
max_price_change = config['bots']['max_price_change']
expiration_ticks = config['bots']['expiration_ticks']


class ResourceBot(Bot):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.buy_prices = {resource: 50 for resource in Resource}
self.sell_prices = {resource: 50 for resource in Resource}
self.last_tick = None
self.player_id = None

async def run(self, tick_data: TickData):
if self.player_id is None:
team = await Team.get(team_secret=config['bots']['team_secret'])
self.player_id = await Player.create(
player_name="resource_bot",
game_id=tick_data.game.game_id,
team_id=team.team_id,
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():
resources_sum[resource] += player[resource.name]

orders = await self.get_last_orders()

for resource in Resource:
resource_orders = orders[resource]
resource_sum = resources_sum[resource]
buy_price = self.buy_prices[resource]
sell_price = self.sell_prices[resource]

# pozitivno ako bot treba otkupiti vise nego prodati
wanted_volume_change = resource_wanted_sum - resource_sum
# koliko bot kupuje s trzista
buy_volume = default_volume + wanted_volume_change
# koliko bot stavlja na trziste
sell_volume = wanted_volume_change - wanted_volume_change
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)
buy_price -= price_change_coeff * buy_price * (1-2*filled_buy_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)
buy_price = price
sell_price = price
buy_price = clamp(min_price, max_price, int(buy_price))
sell_price = clamp(min_price, max_price, int(sell_price))
if buy_price == sell_price:
buy_price = sell_price - 1

await self.create_orders(tick_data.game.current_tick,
resource, buy_price, sell_price, buy_volume, sell_volume)
self.buy_prices[resource] = buy_price
self.sell_prices[resource] = sell_price

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]
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 []
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:
await Order.create(
game_id=self.game_id,
player_id=self.player_id,
price=buy_price,
tick=tick,
timestamp=pd.Timestamp.now(),
size=buy_volume,
order_side=OrderSide.BUY,
resource=resource,
expiration_tick=tick+expiration_ticks
)
await Order.create(
game_id=self.game_id,
player_id=self.player_id,
price=sell_price,
tick=tick,
timestamp=pd.Timestamp.now(),
size=sell_volume,
order_side=OrderSide.SELL,
resource=resource,
expiration_tick=tick+expiration_ticks
)


def clamp(_min, _max, x):
return max(_min, min(_max, x))
3 changes: 2 additions & 1 deletion backend/game/tick/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .ticker import Ticker, TickData, GameData
from .ticker import Ticker, GameData
from .tick_data import TickData
2 changes: 1 addition & 1 deletion backend/game/tick/test_tick_fixtures.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from datetime import datetime
import pytest
from game.tick.ticker import TickData, Ticker, GameData
from game.tick import TickData, Ticker, GameData
from model import Game, Player, PowerPlant, Order, OrderStatus, Resource


Expand Down
12 changes: 7 additions & 5 deletions backend/game/tick/test_ticker_bots.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import pytest
from unittest.mock import MagicMock, patch
from datetime import datetime
from game.tick.ticker import Ticker, GameData
from game.tick import Ticker, GameData
from model import Game
from game.bots import DummyBot
from game.bots import DummyBot, ResourceBot
from game.fixtures.fixtures import *


@pytest.mark.asyncio
async def test_run_bots():
async def test_run_bots(get_tick_data):
# Create sample game
game = Game(
game_id=1,
Expand Down Expand Up @@ -36,11 +37,12 @@ async def test_run_bots():

# Set the bots for the game
ticker.game_data[game.game_id] = GameData(game, players)
tick_data = get_tick_data(power_plants={}, markets={}, players={})

# Run the method being tested
await ticker.run_bots(game)
await ticker.run_bots(tick_data)

# Assertions
# Ensure Bot.run is called once for each bot
assert mock_run.call_count == len(bots)
mock_run.assert_called_with() # Ensure Bot.run is called with no arguments
mock_run.assert_called_with(tick_data) # Ensure Bot.run is called with no arguments
9 changes: 4 additions & 5 deletions backend/game/tick/test_ticker_db_operations.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import pandas as pd
import pytest
from unittest.mock import MagicMock, patch
from unittest.mock import patch
from datetime import datetime
from game.tick.ticker import GameData
from model import Player, PowerPlant, Game, Order, OrderStatus, Resource
from model import Order, OrderStatus, Resource
from model.order_types import OrderSide, OrderType
from tick import Ticker, TickData
from game.tick import Ticker, TickData
from tick.test_tick_fixtures import *


Expand Down Expand Up @@ -58,7 +57,7 @@ async def test_save_tick_data(mock_order_update, mock_powerplant_update, mock_pl
updated_orders=sample_update_orders
)

await ticker.save_tick_data(sample_game, tick_data)
await ticker.save_tick_data(tick_data)

assert mock_player_update.call_count == len(sample_players)
assert mock_powerplant_update.call_count == len(sample_power_plants[1]) + len(
Expand Down
4 changes: 2 additions & 2 deletions backend/game/tick/test_ticker_run_all_game_ticks.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pytest
from datetime import datetime, timedelta
from unittest.mock import MagicMock, patch
from game.tick.ticker import Ticker, GameData
from unittest.mock import patch
from game.tick import Ticker, GameData
from model import Game


Expand Down
17 changes: 10 additions & 7 deletions backend/game/tick/test_ticker_run_markets.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def test_run_markets_no_match(get_tick_data, get_order, get_ticker, get_player,
}
)

tick_data = ticker.run_markets(tick_data, 1)
tick_data = ticker.run_markets(tick_data)

assert tick_data.updated_orders[order1.order_id] == fresh_order1
assert tick_data.updated_orders[order2.order_id] == fresh_order2
Expand Down Expand Up @@ -71,7 +71,7 @@ def test_run_markets_match(get_tick_data, get_order, get_ticker, get_player, get
}
)

tick_data = ticker.run_markets(tick_data, 1)
tick_data = ticker.run_markets(tick_data)

assert tick_data.updated_orders[order1.order_id].order_status == OrderStatus.ACTIVE
assert tick_data.updated_orders[order2.order_id].order_status == OrderStatus.COMPLETED
Expand Down Expand Up @@ -114,7 +114,7 @@ def test_run_markets_match_insufficient_funds(get_tick_data, get_order, get_tick
}
)

tick_data = ticker.run_markets(tick_data, 1)
tick_data = ticker.run_markets(tick_data)

assert tick_data.updated_orders[order1.order_id].order_status == OrderStatus.CANCELLED
assert tick_data.updated_orders[order2.order_id].order_status == OrderStatus.ACTIVE
Expand Down Expand Up @@ -157,7 +157,7 @@ def test_run_markets_match_insufficient_resources(get_tick_data, get_order, get_
}
)

tick_data = ticker.run_markets(tick_data, 1)
tick_data = ticker.run_markets(tick_data)

assert tick_data.updated_orders[order1.order_id].order_status == OrderStatus.ACTIVE
assert tick_data.updated_orders[order2.order_id].order_status == OrderStatus.CANCELLED
Expand Down Expand Up @@ -202,25 +202,28 @@ def test_run_markets_cancel(get_tick_data, get_order, get_ticker, get_player, ge

tick_data.pending_orders = [order1]

tick_data = ticker.run_markets(tick_data, 1)
tick_data.game.current_tick = 1
tick_data = ticker.run_markets(tick_data)

assert tick_data.updated_orders[order1.order_id].order_status == OrderStatus.ACTIVE
assert len(tick_data.updated_orders) == 1

tick_data.game.current_tick = 2
tick_data.pending_orders = []
tick_data.updated_orders = {}
tick_data.user_cancelled_orders = [order1_cancelled]

tick_data = ticker.run_markets(tick_data, 2)
tick_data = ticker.run_markets(tick_data)

assert tick_data.updated_orders[order1.order_id].order_status == OrderStatus.CANCELLED
assert len(tick_data.updated_orders) == 1

tick_data.game.current_tick = 3
tick_data.pending_orders = [order2]
tick_data.updated_orders = {}
tick_data.user_cancelled_orders = []

tick_data = ticker.run_markets(tick_data, 3)
tick_data = ticker.run_markets(tick_data)

assert tick_data.updated_orders[order2.order_id].order_status == OrderStatus.ACTIVE
assert len(tick_data.updated_orders) == 1
Expand Down
8 changes: 3 additions & 5 deletions backend/game/tick/test_ticker_test_run_game_tick.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import pytest
from datetime import datetime
from model import Game, Player
from game.tick.ticker import Ticker, TickData
from model import Game
from game.tick import Ticker
from unittest.mock import patch
from model.power_plant import PowerPlant
from tick.test_tick_fixtures import *


Expand Down Expand Up @@ -37,4 +35,4 @@ async def test_run_game_tick(
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(sample_game)
Ticker.run_bots.assert_called_once_with(tick_data)
19 changes: 19 additions & 0 deletions backend/game/tick/tick_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from dataclasses import dataclass, field
from model import Player, PowerPlant, Game, Order
from game.market import ResourceMarket
from game.bots.bot import Bot


@dataclass
class TickData:
game: Game
players: dict[int, Player]
power_plants: dict[int, list[PowerPlant]]
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)
Loading

0 comments on commit de992ad

Please sign in to comment.