Files
devplacepy/devplacepy/services/game/store/farm.py
T

289 lines
9.3 KiB
Python
Raw Normal View History

# retoor <retoor@molodetz.nl>
from __future__ import annotations
from devplacepy.cache import TTLCache
from devplacepy.utils import generate_uid
from .. import economy
from .common import GameError, _farms, _iso, _lvl, _now, _plots, conditional_update_farm
_leaderboard_cache = TTLCache(ttl=15, max_size=8)
def get_farm(user_uid: str) -> dict | None:
return _farms().find_one(user_uid=user_uid)
def ensure_farm(user_uid: str) -> dict:
farm = get_farm(user_uid)
if farm:
return farm
now = _iso(_now())
uid = generate_uid()
_farms().insert(
{
"uid": uid,
"user_uid": user_uid,
"coins": economy.STARTING_COINS,
"xp": 0,
"level": 1,
"ci_tier": 1,
"plot_count": economy.STARTING_PLOTS,
"total_harvests": 0,
"created_at": now,
"updated_at": now,
}
)
for slot_index in range(economy.STARTING_PLOTS):
_create_plot(uid, user_uid, slot_index, now)
return get_farm(user_uid)
def _create_plot(farm_uid: str, user_uid: str, slot_index: int, now: str) -> None:
_plots().insert(
{
"uid": generate_uid(),
"farm_uid": farm_uid,
"user_uid": user_uid,
"slot_index": slot_index,
"crop_key": "",
"planted_at": "",
"ready_at": "",
"watered_by": "[]",
"created_at": now,
"updated_at": now,
}
)
def get_plots(farm_uid: str) -> list[dict]:
rows = list(_plots().find(farm_uid=farm_uid))
rows.sort(key=lambda row: row.get("slot_index", 0))
return rows
def _plot_at(farm_uid: str, slot_index: int) -> dict | None:
return _plots().find_one(farm_uid=farm_uid, slot_index=slot_index)
def buy_plot(user: dict) -> dict:
farm = ensure_farm(user["uid"])
plot_count = int(farm.get("plot_count", economy.STARTING_PLOTS))
if plot_count >= economy.MAX_PLOTS:
raise GameError("All plots are already unlocked.")
cost = economy.plot_cost(plot_count)
rows = conditional_update_farm(
farm["uid"],
set_clause="coins = coins - :cost, plot_count = :new_count",
where_clause="plot_count = :current_count AND coins >= :cost",
params={"cost": cost, "new_count": plot_count + 1, "current_count": plot_count},
)
if rows == 0:
farm = get_farm(user["uid"])
if int(farm.get("plot_count", economy.STARTING_PLOTS)) != plot_count:
raise GameError("Plot count already changed - refresh and try again.")
raise GameError("Not enough coins for a new plot.")
_create_plot(farm["uid"], user["uid"], plot_count, _iso(_now()))
return {"plot_count": plot_count + 1, "spent": cost}
def upgrade_ci(user: dict) -> dict:
farm = ensure_farm(user["uid"])
ci_tier = int(farm.get("ci_tier", 1))
next_tier = economy.next_ci_tier(ci_tier)
if not next_tier:
raise GameError("CI is already at the top tier.")
rows = conditional_update_farm(
farm["uid"],
set_clause="coins = coins - :cost, ci_tier = :new_tier",
where_clause="ci_tier = :current_tier AND coins >= :cost",
params={"cost": next_tier.upgrade_cost, "new_tier": next_tier.tier, "current_tier": ci_tier},
)
if rows == 0:
farm = get_farm(user["uid"])
if int(farm.get("ci_tier", 1)) != ci_tier:
raise GameError("CI tier already changed - refresh and try again.")
raise GameError("Not enough coins to upgrade CI.")
return {"ci_tier": next_tier.tier, "spent": next_tier.upgrade_cost}
def leaderboard(limit: int = 25) -> list[dict]:
cached = _leaderboard_cache.get(f"top:{limit}")
if cached is not None:
return cached
farms = sorted(
_farms().find(),
key=economy.farm_score,
reverse=True,
)[:limit]
if not farms:
return []
from devplacepy.database import get_users_by_uids
users = get_users_by_uids([farm["user_uid"] for farm in farms])
entries = []
for rank, farm in enumerate(farms, start=1):
owner = users.get(farm["user_uid"])
if not owner:
continue
entries.append(
{
"rank": rank,
"username": owner.get("username", ""),
"level": int(farm.get("level", 1)),
"xp": int(farm.get("xp", 0)),
"coins": int(farm.get("coins", 0)),
"total_harvests": int(farm.get("total_harvests", 0)),
"prestige": int(farm.get("prestige") or 0),
"score": economy.farm_score(farm),
"title": economy.cosmetic_title_name(farm.get("active_title") or ""),
}
)
_leaderboard_cache.set(f"top:{limit}", entries)
return entries
def _entry(rank: int, farm: dict, owner: dict, score: int) -> dict:
return {
"rank": rank,
"username": owner.get("username", ""),
"level": int(farm.get("level", 1)),
"xp": int(farm.get("xp", 0)),
"coins": int(farm.get("coins", 0)),
"total_harvests": int(farm.get("total_harvests", 0)),
"prestige": int(farm.get("prestige") or 0),
"score": score,
"title": economy.cosmetic_title_name(farm.get("active_title") or ""),
}
def _ranked_entries(farms: list[dict], score_fn, limit: int) -> list[dict]:
ranked = sorted(farms, key=score_fn, reverse=True)[:limit]
if not ranked:
return []
from devplacepy.database import get_users_by_uids
users = get_users_by_uids([farm["user_uid"] for farm in ranked])
entries = []
for rank, farm in enumerate(ranked, start=1):
owner = users.get(farm["user_uid"])
if not owner:
continue
entries.append(_entry(rank, farm, owner, score_fn(farm)))
return entries
def leaderboard_prestige(limit: int = 25) -> list[dict]:
return _ranked_entries(
list(_farms().find()),
lambda f: _lvl(f, "prestige") * 1_000_000 + _lvl(f, "stars"),
limit,
)
def leaderboard_harvests_week(limit: int = 25) -> list[dict]:
return _ranked_entries(list(_farms().find()), lambda f: _lvl(f, "harvests_week"), limit)
def leaderboard_fair_play(limit: int = 25) -> list[dict]:
return _ranked_entries(
list(_farms().find()),
lambda f: economy.fair_play_score(_lvl(f, "harvests_week"), int(f.get("coins", 0))),
limit,
)
def leaderboard_time_to_kernel(limit: int = 25) -> list[dict]:
farms = [
f
for f in _farms().find()
if _lvl(f, "prestige") > 0
and _lvl(f, "last_kernel_harvest_prestige") == _lvl(f, "prestige")
]
ranked = sorted(farms, key=lambda f: _lvl(f, "time_to_kernel_seconds"))[:limit]
if not ranked:
return []
from devplacepy.database import get_users_by_uids
users = get_users_by_uids([farm["user_uid"] for farm in ranked])
entries = []
for rank, farm in enumerate(ranked, start=1):
owner = users.get(farm["user_uid"])
if not owner:
continue
entry = _entry(rank, farm, owner, _lvl(farm, "time_to_kernel_seconds"))
entry["time_to_kernel_seconds"] = _lvl(farm, "time_to_kernel_seconds")
entries.append(entry)
return entries
RAID_EFFICIENCY_WINDOW_DAYS = 30
def leaderboard_raid_efficiency(limit: int = 25) -> list[dict]:
from datetime import timedelta
from devplacepy.database import db, get_users_by_uids
cutoff = _iso(_now() - timedelta(days=RAID_EFFICIENCY_WINDOW_DAYS))
rows = db.query(
"SELECT thief_uid, COUNT(*) AS raids, SUM(coins) AS total_coins "
"FROM game_steals WHERE stolen_at >= :cutoff "
"GROUP BY thief_uid HAVING COUNT(*) >= :min_raids "
"ORDER BY (SUM(coins) * 1.0 / COUNT(*)) DESC LIMIT :limit",
cutoff=cutoff,
min_raids=economy.MIN_RAIDS_FOR_EFFICIENCY_BOARD,
limit=limit,
)
ranked = [
(row["thief_uid"], int(row["raids"]), int(row["total_coins"] or 0)) for row in rows
]
if not ranked:
return []
uids = [uid for uid, _, _ in ranked]
users = get_users_by_uids(uids)
farms_table = _farms()
farms_by_uid = {
f["user_uid"]: f for f in farms_table.find(farms_table.table.columns.user_uid.in_(uids))
}
entries = []
for rank, (uid, raids, total_coins) in enumerate(ranked, start=1):
owner = users.get(uid)
if not owner:
continue
farm = farms_by_uid.get(uid, {})
avg = total_coins / raids
entry = _entry(rank, farm, owner, round(avg))
entry["raid_avg"] = round(avg, 1)
entries.append(entry)
return entries
LEADERBOARD_BOARDS = {
"score": leaderboard,
"prestige": leaderboard_prestige,
"harvests": leaderboard_harvests_week,
"raids": leaderboard_raid_efficiency,
"time_to_kernel": leaderboard_time_to_kernel,
"fair_play": leaderboard_fair_play,
}
_board_cache = TTLCache(ttl=15, max_size=32)
def leaderboard_for(board: str, limit: int = 25) -> list[dict]:
cache_key = f"{board}:{limit}"
cached = _board_cache.get(cache_key)
if cached is not None:
return cached
if board == "era":
from .era import leaderboard_era
entries = leaderboard_era(limit)
else:
entries = LEADERBOARD_BOARDS.get(board, leaderboard)(limit)
_board_cache.set(cache_key, entries)
return entries