# retoor 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