# retoor <retoor@molodetz.nl>
from __future__ import annotations
import os
from datetime import datetime, timedelta
from devplacepy.cache import TTLCache
from devplacepy.database import db, get_table
from devplacepy.utils import generate_uid
from .. import economy
from .common import _iso
SATURATION_TTL = int(os.environ.get("DEVPLACE_MARKET_SATURATION_TTL", "30"))
_saturation_cache = TTLCache(ttl=SATURATION_TTL, max_size=32)
def _ticks():
return get_table("game_market_ticks")
def _hour_bucket(now: datetime) -> str:
return now.strftime("%Y-%m-%dT%H")
def record_harvest_tick(crop_key: str, now: datetime) -> None:
with db:
db.query(
"INSERT INTO game_market_ticks (uid, crop_key, hour_bucket, harvests, updated_at) "
"VALUES (:uid, :crop_key, :hour_bucket, 1, :now) "
"ON CONFLICT(crop_key, hour_bucket) DO UPDATE SET "
"harvests = harvests + 1, updated_at = :now",
uid=generate_uid(),
crop_key=crop_key,
hour_bucket=_hour_bucket(now),
now=_iso(now),
)
_saturation_cache.pop(f"{crop_key}:{economy.MARKET_WINDOW_HOURS}")
def recent_harvests(crop_key: str, window_hours: int) -> int:
cache_key = f"{crop_key}:{window_hours}"
cached = _saturation_cache.get(cache_key)
if cached is not None:
return cached
from .common import _now
cutoff = _hour_bucket(_now() - timedelta(hours=window_hours))
row = db.query(
"SELECT COALESCE(SUM(harvests), 0) AS total FROM game_market_ticks "
"WHERE crop_key = :crop_key AND hour_bucket >= :cutoff",
crop_key=crop_key,
cutoff=cutoff,
)
total = 0
for result in row:
total = int(result["total"] or 0)
break
_saturation_cache.set(cache_key, total)
return total
def active_farms() -> int:
cached = _saturation_cache.get("active_farms")
if cached is not None:
return cached
rows = db.query(
"SELECT COUNT(*) AS active FROM game_farms WHERE COALESCE(harvests_week, 0) > 0"
)
total = 1
for row in rows:
total = max(1, int(row.get("active") or 0))
break
_saturation_cache.set("active_farms", total)
return total
def _saturation_for(crop_key: str) -> float:
crop = economy.crop_for(crop_key)
if crop is None:
return 1.0
recent = recent_harvests(crop_key, economy.MARKET_WINDOW_HOURS)
return economy.market_saturation_factor(
economy.supply_days(crop, recent, active_farms())
)
def market_pressure() -> float:
worst = 1.0
for key in economy.MARKET_PRESSURE_CROPS:
worst = min(worst, _saturation_for(key))
return 1.0 - worst
def market_factor_for(crop_key: str) -> float:
if economy.crop_for(crop_key) is None:
return 1.0
saturation = _saturation_for(crop_key)
if saturation < 1.0:
return saturation
return economy.market_buff_factor(crop_key, market_pressure())
def prune_ticks(older_than_hours: int = 96) -> int:
from sqlalchemy import text
from .common import _now
cutoff = _hour_bucket(_now() - timedelta(hours=older_than_hours))
with db:
result = db.executable.execute(
text("DELETE FROM game_market_ticks WHERE hour_bucket < :cutoff"),
{"cutoff": cutoff},
)
return result.rowcount
def prune_steals(older_than_days: int = 60) -> int:
from datetime import timedelta as _timedelta
from sqlalchemy import text
from .common import _iso, _now
cutoff = _iso(_now() - _timedelta(days=older_than_days))
with db:
result = db.executable.execute(
text("DELETE FROM game_steals WHERE stolen_at < :cutoff"), {"cutoff": cutoff}
)
return result.rowcount