Files
devplacepy/devplacepy/services/game/economy.py
T
retoor 6f340a6818 feat: add per-user avatar seed regeneration with irreversible random avatar replacement
Implement a new `avatar_seed` column on the users table that overrides the username-based seed for Multiavatar generation. Introduce a null-safe `avatar_seed(user)` choke point in `avatar.py` that resolves `user.get("avatar_seed") or user.get("username")`, registered as a Jinja global so every render site (`_avatar_link.html`, `avatar_url(...)` calls, SEO `og_image`, issues ad-hoc dicts, devRant payload/PNG) propagates a regenerated seed. Add `POST /profile/{username}/regenerate-avatar` endpoint (owner-or-admin only) that writes a fresh `generate_uid()` to `avatar_seed`, invalidates the target's user cache, and audits `profile.avatar.regenerate`. The previous seed is overwritten and never stored, making regeneration irreversible. Document the feature in `AGENTS.md` and `README.md`, add the API endpoint to `docs_api.py`, and include the `regenerate_avatar` Devii tool in `CONFIRM_REQUIRED`.
2026-06-27 22:31:34 +00:00

475 lines
12 KiB
Python

# retoor <retoor@molodetz.nl>
from __future__ import annotations
import hashlib
import math
from dataclasses import dataclass
STARTING_COINS = 50
STARTING_PLOTS = 4
MAX_PLOTS = 12
MAX_LEVEL = 20
PLOT_BASE_COST = 100
WATER_BONUS_PCT = 0.08
MAX_WATERS_PER_PLOT = 3
WATER_REWARD_COINS = 6
WATER_REWARD_XP = 3
STEAL_GRACE_SECONDS = 60
STEAL_FRACTION = 0.5
STEAL_COOLDOWN_SECONDS = 3600
GOLDEN_CHANCE = 0.05
GOLDEN_MULTIPLIER = 5
@dataclass(frozen=True)
class Crop:
key: str
name: str
icon: str
cost: int
grow_seconds: int
reward_coins: int
reward_xp: int
min_level: int
CROPS: tuple[Crop, ...] = (
Crop("shell", "Shell Script", "\U0001f41a", 5, 30, 11, 2, 1),
Crop("python", "Python Script", "\U0001f40d", 15, 120, 36, 5, 1),
Crop("webapp", "Web App", "\U0001f4dc", 40, 300, 98, 12, 2),
Crop("api", "Go Service", "\U0001f439", 90, 600, 224, 25, 3),
Crop("rust", "Rust Engine", "\U0001f980", 200, 1800, 520, 60, 4),
Crop("haskell", "Compiler", "λ", 500, 3600, 1380, 150, 6),
Crop("kernel", "Kernel", "⚙️", 1200, 7200, 3600, 400, 8),
)
CROP_BY_KEY = {crop.key: crop for crop in CROPS}
@dataclass(frozen=True)
class CiTier:
tier: int
label: str
speed: float
upgrade_cost: int
CI_TIERS: tuple[CiTier, ...] = (
CiTier(1, "Local Build", 1.0, 0),
CiTier(2, "Shared Runner", 1.25, 150),
CiTier(3, "Fast Runner", 1.6, 400),
CiTier(4, "Parallel Matrix", 2.0, 1000),
CiTier(5, "Distributed Cache", 2.5, 2600),
)
CI_BY_TIER = {tier.tier: tier for tier in CI_TIERS}
MAX_CI_TIER = CI_TIERS[-1].tier
def crop_for(key: str) -> Crop | None:
return CROP_BY_KEY.get(key)
def ci_speed(tier: int) -> float:
entry = CI_BY_TIER.get(tier)
return entry.speed if entry else 1.0
def next_ci_tier(tier: int) -> CiTier | None:
return CI_BY_TIER.get(tier + 1)
def farm_speed(ci_tier: int, growth_level: int = 0, legacy_speed_level: int = 0) -> float:
return (
ci_speed(ci_tier)
* (1 + PERK_BY_KEY["growth"].step * growth_level)
* (1 + LEGACY_SPEED_STEP * legacy_speed_level)
)
def grow_seconds_for(
crop: Crop, ci_tier: int, growth_level: int = 0, legacy_speed_level: int = 0
) -> int:
return max(
1, round(crop.grow_seconds / farm_speed(ci_tier, growth_level, legacy_speed_level))
)
def water_bonus_seconds(
crop: Crop, ci_tier: int, growth_level: int = 0, legacy_speed_level: int = 0
) -> int:
return max(
1,
round(
grow_seconds_for(crop, ci_tier, growth_level, legacy_speed_level)
* WATER_BONUS_PCT
),
)
def plot_cost(current_plot_count: int) -> int:
extra = current_plot_count - STARTING_PLOTS
return PLOT_BASE_COST * (2 ** max(0, extra))
def xp_threshold(level: int) -> int:
return 50 * (level - 1) * (level - 1)
def level_for_xp(xp: int) -> int:
level = 1
while level < MAX_LEVEL and xp >= xp_threshold(level + 1):
level += 1
return level
def level_progress(xp: int) -> dict:
level = level_for_xp(xp)
floor_xp = xp_threshold(level)
if level >= MAX_LEVEL:
return {
"level": level,
"xp": xp,
"into_level": xp - floor_xp,
"span": 0,
"next_level_xp": floor_xp,
"is_max": True,
}
ceil_xp = xp_threshold(level + 1)
return {
"level": level,
"xp": xp,
"into_level": xp - floor_xp,
"span": ceil_xp - floor_xp,
"next_level_xp": ceil_xp,
"is_max": False,
}
SCORE_PRESTIGE = 5000
SCORE_HARVEST = 10
SCORE_COIN_DIVISOR = 20
SCORE_CI = 250
SCORE_PLOT = 200
SCORE_PERK = 120
SCORE_STREAK = 15
SCORE_STREAK_CAP = 30
def farm_score(farm: dict) -> int:
def value(key: str, default: int = 0) -> int:
raw = farm.get(key)
return int(raw) if raw is not None else default
perk_total = sum(value(f"perk_{perk.key}") for perk in PERKS)
return (
value("xp")
+ value("prestige") * SCORE_PRESTIGE
+ value("total_harvests") * SCORE_HARVEST
+ value("coins") // SCORE_COIN_DIVISOR
+ (value("ci_tier", 1) - 1) * SCORE_CI
+ (value("plot_count", STARTING_PLOTS) - STARTING_PLOTS) * SCORE_PLOT
+ perk_total * SCORE_PERK
+ min(value("streak"), SCORE_STREAK_CAP) * SCORE_STREAK
)
def unlocked_crops(level: int) -> list[Crop]:
return [crop for crop in CROPS if crop.min_level <= level]
def crop_payload(
crop: Crop,
ci_tier: int,
level: int,
growth_level: int = 0,
discount_level: int = 0,
yield_level: int = 0,
prestige: int = 0,
legacy_mult_level: int = 0,
legacy_speed_level: int = 0,
) -> dict:
return {
"key": crop.key,
"name": crop.name,
"icon": crop.icon,
"cost": effective_plant_cost(crop, discount_level),
"reward_coins": effective_reward_coins(
crop, yield_level, prestige, legacy_mult_level
),
"reward_xp": crop.reward_xp,
"min_level": crop.min_level,
"grow_seconds": grow_seconds_for(crop, ci_tier, growth_level, legacy_speed_level),
"locked": crop.min_level > level,
}
@dataclass(frozen=True)
class Perk:
key: str
name: str
icon: str
description: str
max_level: int
base_cost: int
cost_growth: float
step: float
PERKS: tuple[Perk, ...] = (
Perk("yield", "Optimizer", "📈", "+5% harvest coins per level", 10, 120, 1.6, 0.05),
Perk("growth", "Build Cache", "⚡", "+4% build speed per level", 10, 150, 1.7, 0.04),
Perk("discount", "Bulk Licenses", "🏷️", "-3% planting cost per level", 8, 100, 1.7, 0.03),
Perk("xp", "Mentorship", "🎓", "+5% harvest XP per level", 10, 140, 1.6, 0.05),
)
PERK_BY_KEY = {perk.key: perk for perk in PERKS}
PRESTIGE_MIN_LEVEL = 10
PRESTIGE_BONUS = 0.25
STAR_BASE = 1
LEGACY_SPEED_STEP = 0.05
LEGACY_MULT_STEP = 0.10
LEGACY_DEFENSE_GRACE = 30
LEGACY_DEFENSE_FRACTION = 0.05
@dataclass(frozen=True)
class LegacyUpgrade:
key: str
name: str
icon: str
description: str
max_level: int
base_cost: int
cost_growth: float
LEGACY_UPGRADES: tuple[LegacyUpgrade, ...] = (
LegacyUpgrade(
"autoharvest",
"CI Bot",
"🤖",
"Auto-collect ready builds when you open your farm",
1,
3,
1.0,
),
LegacyUpgrade(
"multiplier",
"Tech Debt Payoff",
"💎",
"+10% coins per level, stacks with prestige",
10,
1,
1.6,
),
LegacyUpgrade(
"speed", "Bare-Metal", "🏎️", "+5% base build speed per level", 8, 1, 1.7
),
LegacyUpgrade(
"plots", "Monorepo", "🗂️", "+1 starting plot after refactor per level", 4, 3, 2.0
),
LegacyUpgrade(
"defense",
"Branch Protection",
"🛡️",
"+30s steal grace and -5% steal loss per level",
5,
2,
1.8,
),
)
LEGACY_BY_KEY = {up.key: up for up in LEGACY_UPGRADES}
def legacy_for(key: str) -> LegacyUpgrade | None:
return LEGACY_BY_KEY.get(key)
def legacy_cost(up: LegacyUpgrade, level: int) -> int:
return round(up.base_cost * (up.cost_growth ** level))
def legacy_multiplier(level: int) -> float:
return 1 + LEGACY_MULT_STEP * max(0, level)
def stars_for_refactor(level: int, prestige: int) -> int:
return STAR_BASE + level // 5 + max(0, prestige)
def effective_steal_grace(defense_level: int = 0) -> int:
return STEAL_GRACE_SECONDS + LEGACY_DEFENSE_GRACE * max(0, defense_level)
def effective_steal_fraction(defense_level: int = 0) -> float:
return max(0.1, STEAL_FRACTION - LEGACY_DEFENSE_FRACTION * max(0, defense_level))
def prestige_base_plots(legacy_plots_level: int = 0) -> int:
return STARTING_PLOTS + max(0, legacy_plots_level)
def legacy_value_text(up: LegacyUpgrade, level: int) -> str:
if up.key == "autoharvest":
return "Active" if level > 0 else "Inactive"
if up.key == "multiplier":
return f"+{round(LEGACY_MULT_STEP * level * 100)}% coins"
if up.key == "speed":
return f"+{round(LEGACY_SPEED_STEP * level * 100)}% build speed"
if up.key == "plots":
return f"+{level} starting plots"
grace = LEGACY_DEFENSE_GRACE * level
loss = round(LEGACY_DEFENSE_FRACTION * level * 100)
return f"+{grace}s grace, -{loss}% steal loss"
def is_golden(plot_uid: str, planted_at: str) -> bool:
if not plot_uid or not planted_at:
return False
digest = hashlib.sha256(f"{plot_uid}:{planted_at}".encode()).hexdigest()
return (int(digest, 16) % 1000) < round(GOLDEN_CHANCE * 1000)
DAILY_BASE = 20
DAILY_STREAK_STEP = 12
DAILY_STREAK_CAP = 7
FERTILIZE_FRACTION = 0.5
FERTILIZE_TAX = 1.05
def perk_for(key: str) -> Perk | None:
return PERK_BY_KEY.get(key)
def perk_cost(perk: Perk, current_level: int) -> int:
return round(perk.base_cost * (perk.cost_growth ** current_level))
def perk_value_text(perk: Perk, level: int) -> str:
if perk.key == "discount":
return f"-{round(perk.step * level * 100)}% planting cost"
if perk.key == "growth":
return f"+{round(perk.step * level * 100)}% build speed"
if perk.key == "yield":
return f"+{round(perk.step * level * 100)}% harvest coins"
return f"+{round(perk.step * level * 100)}% harvest XP"
def prestige_multiplier(prestige: int) -> float:
return 1 + PRESTIGE_BONUS * max(0, prestige)
def effective_plant_cost(crop: Crop, discount_level: int = 0) -> int:
factor = max(0.0, 1 - PERK_BY_KEY["discount"].step * discount_level)
return max(1, round(crop.cost * factor))
def effective_reward_coins(
crop: Crop, yield_level: int = 0, prestige: int = 0, legacy_mult_level: int = 0
) -> int:
factor = (
(1 + PERK_BY_KEY["yield"].step * yield_level)
* prestige_multiplier(prestige)
* legacy_multiplier(legacy_mult_level)
)
return round(crop.reward_coins * factor)
def effective_reward_xp(crop: Crop, xp_level: int = 0) -> int:
return round(crop.reward_xp * (1 + PERK_BY_KEY["xp"].step * xp_level))
def steal_reward_coins(
crop: Crop,
yield_level: int = 0,
prestige: int = 0,
legacy_mult_level: int = 0,
defense_level: int = 0,
) -> int:
fraction = effective_steal_fraction(defense_level)
return max(
1,
round(
effective_reward_coins(crop, yield_level, prestige, legacy_mult_level)
* fraction
),
)
def daily_reward(streak: int) -> int:
effective = min(max(streak, 1), DAILY_STREAK_CAP)
return DAILY_BASE + DAILY_STREAK_STEP * (effective - 1)
def fertilize_click_cost(
effective_reward_coins: int, reduce_seconds: int, full_grow_seconds: int
) -> int:
if reduce_seconds < 1 or full_grow_seconds < 1:
return 0
return max(
1,
math.ceil(
effective_reward_coins * reduce_seconds / full_grow_seconds * FERTILIZE_TAX
),
)
@dataclass(frozen=True)
class QuestDef:
kind: str
label: str
goal_min: int
goal_max: int
coin: int
xp: int
QUEST_DEFS: dict[str, QuestDef] = {
"plant": QuestDef("plant", "Plant {goal} crops", 3, 6, 8, 2),
"harvest": QuestDef("harvest", "Harvest {goal} builds", 3, 5, 14, 4),
"water": QuestDef("water", "Water {goal} neighbour builds", 2, 4, 16, 5),
"earn": QuestDef("earn", "Earn {goal} coins harvesting", 150, 400, 0, 0),
}
QUEST_KINDS = tuple(QUEST_DEFS.keys())
DAILY_QUEST_COUNT = 3
def daily_quests(user_uid: str, day: str) -> list[dict]:
import hashlib
seed = int(hashlib.sha256(f"{user_uid}:{day}".encode()).hexdigest(), 16)
start = seed % len(QUEST_KINDS)
quests = []
for index in range(DAILY_QUEST_COUNT):
kind = QUEST_KINDS[(start + index) % len(QUEST_KINDS)]
definition = QUEST_DEFS[kind]
span = definition.goal_max - definition.goal_min + 1
goal = definition.goal_min + ((seed >> (index * 7)) % span)
if kind == "earn":
goal = max(definition.goal_min, (goal // 10) * 10)
reward_coins = round(goal * 0.15)
reward_xp = max(1, round(goal / 30))
else:
reward_coins = goal * definition.coin
reward_xp = goal * definition.xp
quests.append(
{
"kind": kind,
"label": definition.label.format(goal=goal),
"goal": goal,
"reward_coins": reward_coins,
"reward_xp": reward_xp,
}
)
return quests