Every purchase/upgrade path (new and pre-existing) is now race-safe against concurrent requests via atomic conditional SQL updates. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
78 lines
2.2 KiB
Python
78 lines
2.2 KiB
Python
# retoor <retoor@molodetz.nl>
|
|
|
|
from __future__ import annotations
|
|
|
|
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_cache = TTLCache(ttl=30, 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.clear()
|
|
|
|
|
|
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 market_factor_for(crop_key: str) -> float:
|
|
recent = recent_harvests(crop_key, economy.MARKET_WINDOW_HOURS)
|
|
saturation = economy.market_saturation_factor(recent)
|
|
return saturation * economy.market_buff_factor(crop_key, saturation)
|
|
|
|
|
|
def prune_ticks(older_than_hours: int = 96) -> int:
|
|
from .common import _now
|
|
|
|
cutoff = _hour_bucket(_now() - timedelta(hours=older_than_hours))
|
|
table = _ticks()
|
|
stale = [row["uid"] for row in table.find() if row.get("hour_bucket", "") < cutoff]
|
|
for uid in stale:
|
|
table.delete(uid=uid)
|
|
return len(stale)
|