Skip to content

Commit

Permalink
feat: adds support for buying, selling, and managing items
Browse files Browse the repository at this point in the history
  • Loading branch information
jmgilman committed May 24, 2024
1 parent 647bdb4 commit 7fa6c45
Show file tree
Hide file tree
Showing 11 changed files with 602 additions and 39 deletions.
18 changes: 13 additions & 5 deletions pymerc/api/buildings.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

from pymerc.api.base import BaseAPI
from pymerc.api.models import buildings, common
from pymerc.exceptions import SetManagerFailedException
from pymerc.util import data

BASE_URL = "https://play.mercatorio.io/api/buildings/"
Expand Down Expand Up @@ -35,18 +35,26 @@ async def get_operations(self, id: int) -> buildings.BuildingOperation:

async def set_manager(
self, id: int, item: common.Item, manager: common.InventoryManager
) -> bool:
) -> buildings.Building:
"""Set the manager for an item in a building.
Args:
item (Item): The item.
manager (InventoryManager): The manager.
Returns:
bool: Whether the manager was set.
Building: The updated building.
"""
json = data.convert_floats_to_strings(manager.model_dump(exclude_unset=True))
response = await self.client.patch(
f"{BASE_URL}{id}/storage/inventory/{item.name.lower()}", json=json
f"{BASE_URL}{id}/storage/inventory/{item.value}", json=json
)

if response.status_code == 200:
return buildings.Building.model_validate(
response.json()["_embedded"][f"/buildings/{id}"]
)

raise SetManagerFailedException(
f"Failed to set manager for {item.name} on building {id}: {response.text}"
)
return response.status_code == 200
4 changes: 4 additions & 0 deletions pymerc/api/models/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -771,6 +771,10 @@ class ItemTradeResult(BaseModel):

settlements: Optional[list[ItemTradeSettlement]] = None
order_id: Optional[int] = None
embedded: Optional[dict] = Field(alias="_embedded", default_factory=dict)

class Config:
arbitrary_types_allowed = True


class ItemTradeSettlement(BaseModel):
Expand Down
72 changes: 69 additions & 3 deletions pymerc/api/towns.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from pymerc.api.base import BaseAPI
from pymerc.api.models import common, towns
from pymerc.exceptions import BuySellOrderFailedException
from pymerc.util import data

BASE_URL = "https://play.mercatorio.io/api/towns"
Expand Down Expand Up @@ -60,6 +61,35 @@ async def get_market_item(
response = await self.client.get(f"{BASE_URL}/{town_id}/markets/{item.value}")
return towns.TownMarketItemDetails.model_validate(response.json())

async def send_buy_order(
self,
item: common.Item,
id: int,
expected_balance: int,
operation: str,
price: float,
volume: int,
) -> common.ItemTradeResult:
"""Send a buy order to a town.
Args:
item (Item): The item to buy
id (int): The ID of the town
expected_balance (int): The expected balance after the purchase
operation (str): The operation to use for the purchase
price (float): The price of the item
volume (int): The volume of the item to buy
Returns:
ItemTradeResult: The result of the purchase
Raises:
BuySellOrderFailedException: If the order failed to send
"""
return await self._send_order(
item, id, expected_balance, operation, price, volume, "bid"
)

async def send_sell_order(
self,
item: common.Item,
Expand All @@ -80,10 +110,44 @@ async def send_sell_order(
volume (int): The volume of the item to sell
Returns:
bool: Whether the order was successfully sent
ItemTradeResult: The result of the sale
Raises:
BuySellOrderFailedException: If the order failed to send
"""
return await self._send_order(
item, id, expected_balance, operation, price, volume, "ask"
)

async def _send_order(
self,
item: common.Item,
id: int,
expected_balance: int,
operation: str,
price: float,
volume: int,
direction: str,
) -> common.ItemTradeResult:
"""Send a buy or sell order to a town.
Args:
item (Item): The item to buy or sell
id (int): The ID of the town
expected_balance (int): The expected balance after the trade
operation (str): The operation to use for the trade
price (float): The price of the item
volume (int): The volume of the item to trade
direction (str): The direction of the trade
Returns:
ItemTradeResult: The result of the trade
Raises:
BuySellOrderFailedException: If the order failed to send
"""
trade = common.ItemTrade(
direction="ask",
direction=direction,
expected_balance=expected_balance,
operation=operation,
price=price,
Expand All @@ -97,4 +161,6 @@ async def send_sell_order(
if response.status_code == 200:
return common.ItemTradeResult.model_validate(response.json())
else:
raise ValueError(f"Failed to send sell order: {response.text}")
raise BuySellOrderFailedException(
f"Failed to send {direction} order: {response.text}"
)
12 changes: 9 additions & 3 deletions pymerc/api/transports.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from pymerc.api.base import BaseAPI
from pymerc.api.models import common, transports
from pymerc.exceptions import SetManagerFailedException
from pymerc.util import data

BASE_URL = "https://play.mercatorio.io/api/transports"
Expand All @@ -22,7 +23,7 @@ async def get(self, id: int) -> transports.Transport:

async def set_manager(
self, id: int, item: common.Item, manager: common.InventoryManager
):
) -> transports.TransportRoute:
"""Sets the manager for the item.
Args:
Expand All @@ -32,6 +33,11 @@ async def set_manager(
"""
json = data.convert_floats_to_strings(manager.model_dump(exclude_unset=True))
response = await self.client.patch(
f"{BASE_URL}{id}/route/inventory/{item.name.lower()}", json=json
f"{BASE_URL}/{id}/route/inventory/{item.value}", json=json
)
if response.status_code == 200:
return transports.TransportRoute.model_validate(response.json())

raise SetManagerFailedException(
f"Failed to set manager for {item.name} on transport {id}: {response.text}"
)
return response.status_code == 200
12 changes: 12 additions & 0 deletions pymerc/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,15 @@ class TurnInProgressException(Exception):
"""Exception raised when a turn is in progress."""

pass


class BuySellOrderFailedException(Exception):
"""Exception raised when a buy/sell order fails."""

pass


class SetManagerFailedException(Exception):
"""Exception raised when setting a manager fails."""

pass
27 changes: 23 additions & 4 deletions pymerc/game/building.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,17 +147,36 @@ def manager(self, item: common.Item) -> Optional[common.InventoryManager]:
else:
return None

def set_manager(self, item: common.Item, manager: common.InventoryManager) -> bool:
async def patch_manager(self, item: common.Item, **kwargs):
"""Patch the manager for an item in the building.
Args:
item (Item): The item.
**kwargs: The manager data to patch.
Raises:
SetManagerFailedException: If the manager could not be patched.
"""
if item not in self.data.storage.inventory.managers:
raise ValueError(f"Item {item} does not have a manager.")

manager = self.data.storage.inventory.managers[item]
for key, value in kwargs.items():
setattr(manager, key, value)

self = await self._client.buildings_api.set_manager(self.id, item, manager)

async def set_manager(self, item: common.Item, manager: common.InventoryManager):
"""Set the manager for an item in the building.
Args:
item (Item): The item.
manager (InventoryManager): The manager.
Returns:
bool: Whether the manager was set.
Raises:
SetManagerFailedException: If the manager could not be set.
"""
return self._client.buildings_api.set_manager(self.id, item, manager)
self = await self._client.buildings_api.set_manager(self.id, item, manager)

async def calculate_current_labor_need(self) -> float:
"""Calculates the current labor need based on the building's production recipe.
Expand Down
55 changes: 54 additions & 1 deletion pymerc/game/exports.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing import TYPE_CHECKING

from pymerc.api.models import common
from pymerc.api.models.towns import TownMarketItem, TownMarketItemDetails
from pymerc.game.town import Town

if TYPE_CHECKING:
Expand All @@ -17,10 +18,16 @@ class Export:

asset: common.InventoryAccountAsset
flow: common.InventoryFlow
item: common.Item
manager: common.InventoryManager
town: Town
transport: Transport

@property
def market_data(self) -> TownMarketItem:
"""The market data for the export."""
return self.town.market[self.item]

@property
def flowed(self) -> int:
"""How much of the import flowed in the last turn."""
Expand All @@ -42,7 +49,36 @@ def value_flowed(self) -> float:
@property
def volume(self) -> int:
"""The volume of the export if it was sold at max volume."""
return self.manager.buy_volume
return self.manager.sell_volume

@property
def volume_flowed(self) -> int:
"""The volume of the export that flowed in the last turn."""
return self.flow.exported or 0

async def fetch_market_details(self) -> TownMarketItemDetails:
"""Fetches the market details for the export."""
return await self.town.fetch_market_item(self.item)

async def sell(self, volume: int, price: float) -> common.ItemTradeResult:
"""Places a sell order against the export.
Args:
volume: The volume to sell.
price: The price to sell at
Returns:
ItemTradeResult: The result of the sell order.
"""
await self.transport.sell(self.item, volume, price)

async def patch_manager(self, **kwargs):
"""Patches the export's manager
Args:
**kwargs: The fields to patch.
"""
await self.transport.patch_manager(self.item, **kwargs)


class Exports(UserDict[common.Item, Export]):
Expand All @@ -68,6 +104,11 @@ def volume(self) -> int:
"""The total volume of all exports if they were sold at max volume."""
return sum([exp.volume for exp in self.data.values()])

@property
def volume_flowed(self) -> int:
"""The total volume of all exports that flowed in the last turn."""
return sum([exp.volume_flowed for exp in self.data.values()])


class ExportsList(UserList[Export]):
"""A collection of exports for a transport in the game."""
Expand All @@ -92,6 +133,11 @@ def volume(self) -> int:
"""The total volume of all exports if they were sold at max volume."""
return sum([exp.volume for exp in self.data])

@property
def volume_flowed(self) -> int:
"""The total volume of all exports that flowed in the last turn."""
return sum([exp.volume_flowed for exp in self.data])

def by_town_id(self, id: int) -> ExportsList:
"""Returns the exports for a town by id."""
return ExportsList([exp for exp in self.data if exp.town.data.id == id])
Expand Down Expand Up @@ -132,6 +178,13 @@ def volume(self) -> int:
"""The total volume of all exports if they were sold at max volume."""
return sum([sum([exp.volume for exp in exps]) for exps in self.data.values()])

@property
def volume_flowed(self) -> int:
"""The total volume of all exports that flowed in the last turn."""
return sum(
[sum([exp.volume_flowed for exp in exps]) for exps in self.data.values()]
)

def by_town_id(self, town_id: int) -> ExportsSummed:
"""Returns the exports for a town by id."""
return ExportsSummed(
Expand Down
Loading

0 comments on commit 7fa6c45

Please sign in to comment.