Files
devplacepy/devplacepy/services/game/economy.py
T
retoor 582e37d176
DevPlace CI / test (push) Has been cancelled
pdate
2026-07-23 01:14:10 +02:00

825 lines
22 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
min_mastery: int = 0
steal_immune: bool = False
era_key: str | None = None
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("distsys", "Distributed System", "\U0001f578", 5000, 14400, 9500, 900, MAX_LEVEL, min_mastery=1),
Crop("mlpipe", "ML Pipeline", "\U0001f9e0", 12000, 21600, 21000, 1800, MAX_LEVEL, min_mastery=1),
Crop("secfort", "Security Fortress", "\U0001f510", 30000, 28800, 48000, 3200, MAX_LEVEL, min_mastery=1, steal_immune=True),
)
CROP_BY_KEY = {crop.key: crop for crop in CROPS}
MARKET_TRACKED_CROPS = ("shell", "python", "webapp", "api", "rust", "haskell", "kernel")
REGISTRY_BOOST_CROPS = ("rust", "haskell", "kernel")
@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)
REGISTRY_BOOST_FACTOR = 1.15
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,
registry_boost: bool = False,
) -> int:
speed = farm_speed(ci_tier, growth_level, legacy_speed_level)
if registry_boost and crop.key in REGISTRY_BOOST_CROPS:
speed *= REGISTRY_BOOST_FACTOR
return max(1, round(crop.grow_seconds / speed))
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, mastery_earned: int = 0, active_era_name: str | None = None
) -> list[Crop]:
return [
crop
for crop in CROPS
if crop.min_level <= level
and crop.min_mastery <= mastery_earned
and (crop.era_key is None or crop.era_key == active_era_name)
]
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,
market_factor: float = 1.0,
mastery_earned: int = 0,
active_era_name: str | None = None,
registry_boost: bool = False,
) -> dict:
era_locked = crop.era_key is not None and crop.era_key != active_era_name
market_state = "normal"
if market_factor < 1.0:
market_state = "saturated"
elif market_factor > 1.0:
market_state = "boosted"
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, market_factor
),
"reward_xp": crop.reward_xp,
"min_level": crop.min_level,
"grow_seconds": grow_seconds_for(
crop, ci_tier, growth_level, legacy_speed_level, registry_boost
),
"locked": crop.min_level > level or crop.min_mastery > mastery_earned or era_locked,
"market_state": market_state,
}
@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
REFACTOR_BASE_COST = 20_000
REFACTOR_WEALTH_PCT = 0.15
REFACTOR_CARRYOVER_BASE = 0.10
REFACTOR_CARRYOVER_MAX = 0.95
LEGACY_CARRYOVER_STEP = 0.05
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,
),
LegacyUpgrade(
"carryover",
"Golden Parachute",
"🪂",
"+5% of your post-fee coins carried through each refactor 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, extra_seconds: int = 0) -> int:
return STEAL_GRACE_SECONDS + LEGACY_DEFENSE_GRACE * max(0, defense_level) + max(0, extra_seconds)
def effective_steal_fraction(defense_level: int = 0, floor: float = 0.1) -> float:
return max(floor, 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"
if up.key == "carryover":
pct = round(refactor_carryover_fraction(level) * 100)
return f"{pct}% refactor carry-over"
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 refactor_cost(prestige: int, coins: int) -> int:
scaled = REFACTOR_BASE_COST * (1 + max(0, prestige)) * prestige_multiplier(prestige)
return round(scaled + REFACTOR_WEALTH_PCT * max(0, coins))
def refactor_carryover_fraction(carryover_level: int = 0) -> float:
return min(
REFACTOR_CARRYOVER_MAX,
REFACTOR_CARRYOVER_BASE + LEGACY_CARRYOVER_STEP * max(0, carryover_level),
)
def refactor_carryover(coins: int, fee: int, carryover_level: int = 0) -> int:
remainder = max(0, coins - fee)
return int(remainder * refactor_carryover_fraction(carryover_level))
GRANT_WEALTH_CEILING = 10_000
GRANT_MIN_WEEK_HARVESTS = 5
GRANT_MAX_PRESTIGE = 5
GRANT_CAP = 2_500
def grant_amount(treasury_balance: int) -> int:
return max(0, min(GRANT_CAP, treasury_balance))
UNDERDOG_COIN_RATIO = 10
UNDERDOG_DURATION_HOURS = 24
UNDERDOG_MULTIPLIER = 1.25
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,
market_factor: float = 1.0,
underdog: bool = False,
) -> int:
factor = (
(1 + PERK_BY_KEY["yield"].step * yield_level)
* prestige_multiplier(prestige)
* legacy_multiplier(legacy_mult_level)
* market_factor
)
if underdog:
factor *= UNDERDOG_MULTIPLIER
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,
market_factor: float = 1.0,
steal_floor: float = 0.1,
) -> int:
fraction = effective_steal_fraction(defense_level, steal_floor)
return max(
1,
round(
effective_reward_coins(crop, yield_level, prestige, legacy_mult_level, market_factor)
* 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
WEEKLY_CONTRACT_STAR_DIVISOR = 40
WEEKLY_CONTRACT_XP_DIVISOR = 4
WEEKLY_CONTRACT_BOOST_MULTIPLIER = 1.2
WEEKLY_CONTRACT_BOOST_HOURS = 48
def weekly_contract(user_uid: str, iso_week: str) -> dict:
seed = int(hashlib.sha256(f"{user_uid}:{iso_week}".encode()).hexdigest(), 16)
kind = QUEST_KINDS[seed % len(QUEST_KINDS)]
definition = QUEST_DEFS[kind]
goal = (definition.goal_max + definition.goal_min) * 4
if kind == "earn":
goal = max(definition.goal_min, (goal // 10) * 10)
reward_stars = max(1, goal // WEEKLY_CONTRACT_STAR_DIVISOR)
reward_xp = max(1, goal // WEEKLY_CONTRACT_XP_DIVISOR)
return {
"kind": kind,
"label": f"Weekly: {definition.label.format(goal=goal)}",
"goal": goal,
"reward_stars": reward_stars,
"reward_xp": reward_xp,
}
MARKET_WINDOW_HOURS = 48
MARKET_SATURATION_TIERS: tuple[tuple[int, float], ...] = (
(0, 1.00),
(40, 0.85),
(120, 0.70),
(300, 0.55),
(600, 0.40),
)
MARKET_BUFFED_CROPS = ("shell", "python", "webapp", "api")
MARKET_BUFF_CAP = 1.15
def market_saturation_factor(recent_harvests: int) -> float:
factor = MARKET_SATURATION_TIERS[0][1]
for threshold, tier_factor in MARKET_SATURATION_TIERS:
if recent_harvests >= threshold:
factor = tier_factor
return factor
def market_buff_factor(crop_key: str, saturation_factor: float) -> float:
if crop_key not in MARKET_BUFFED_CROPS:
return 1.0
relief = 1.0 - saturation_factor
return min(MARKET_BUFF_CAP, 1.0 + relief)
@dataclass(frozen=True)
class Infrastructure:
key: str
name: str
icon: str
description: str
cost: int
min_prestige: int
INFRASTRUCTURE: tuple[Infrastructure, ...] = (
Infrastructure(
"registry",
"Private Registry",
"\U0001f4e6",
"Rust, Compiler, and Kernel crops grow 15% faster",
25_000_000,
3,
),
Infrastructure(
"canary",
"Canary Deployments",
"\U0001f424",
"Every harvest has a 12% chance to double and a 6% chance to only refund its planting cost",
75_000_000,
8,
),
Infrastructure(
"observability",
"Observability Suite",
"\U0001f52d",
"Raises the minimum coins you keep when raided from 10% to 30%",
150_000_000,
15,
),
)
INFRA_BY_KEY = {i.key: i for i in INFRASTRUCTURE}
CANARY_DOUBLE_CHANCE = 0.12
CANARY_FAIL_CHANCE = 0.06
OBSERVABILITY_STEAL_FLOOR = 0.3
def infra_for(key: str) -> Infrastructure | None:
return INFRA_BY_KEY.get(key)
@dataclass(frozen=True)
class DefenseTier:
level: int
name: str
upgrade_cost: int
upkeep_daily: int
steal_fraction_floor: float
grace_bonus: int
DEFENSE_TIERS: tuple[DefenseTier, ...] = (
DefenseTier(0, "Undefended", 0, 0, 0.10, 0),
DefenseTier(1, "Firewall", 5_000, 500, 0.10, 15),
DefenseTier(2, "WAF", 40_000, 2_500, 0.08, 30),
DefenseTier(3, "SOC Monitoring", 300_000, 15_000, 0.06, 60),
DefenseTier(4, "Zero Trust Mesh", 2_000_000, 100_000, 0.04, 120),
)
MAX_DEFENSE_LEVEL = DEFENSE_TIERS[-1].level
DEFENSE_BY_LEVEL = {tier.level: tier for tier in DEFENSE_TIERS}
UPKEEP_WEALTH_PCT = 0.002
UPKEEP_GRACE_DAYS = 2
def defense_tier(level: int) -> DefenseTier:
return DEFENSE_BY_LEVEL.get(max(0, level), DEFENSE_TIERS[0])
def next_defense_tier(level: int) -> DefenseTier | None:
return DEFENSE_BY_LEVEL.get(level + 1)
def daily_upkeep(tier: DefenseTier, coins: int) -> int:
return max(tier.upkeep_daily, round(coins * UPKEEP_WEALTH_PCT))
@dataclass(frozen=True)
class Cosmetic:
key: str
name: str
icon: str
description: str
cost_coins: int
kind: str
era_key: str | None = None
COSMETICS: tuple[Cosmetic, ...] = (
Cosmetic("title_architect", "The Architect", "\U0001f3db", "A permanent title shown on the leaderboard", 500_000, "title"),
Cosmetic("title_refactorer", "Serial Refactorer", "♻", "A permanent title for the serially prestiged", 250_000, "title"),
Cosmetic("title_kernel_hacker", "Kernel Hacker", "⚙", "A permanent title for Kernel harvesters", 1_000_000, "title"),
Cosmetic("skin_neon", "Neon Terminal", "\U0001f308", "A cosmetic plot skin, no gameplay effect", 750_000, "skin"),
)
COSMETIC_BY_KEY = {c.key: c for c in COSMETICS}
def cosmetic_for(key: str) -> Cosmetic | None:
return COSMETIC_BY_KEY.get(key)
def cosmetic_title_name(key: str) -> str:
cosmetic = COSMETIC_BY_KEY.get(key or "")
return cosmetic.name if cosmetic and cosmetic.kind == "title" else ""
MASTERY_UNLOCK_PRESTIGE = 50
MASTERY_PRESTIGE_STEP = 10
def _mastery_total_for(prestige: int) -> int:
if prestige < MASTERY_UNLOCK_PRESTIGE:
return 0
return 1 + (prestige - MASTERY_UNLOCK_PRESTIGE) // MASTERY_PRESTIGE_STEP
def mastery_points_awarded(old_prestige: int, new_prestige: int) -> int:
return max(0, _mastery_total_for(new_prestige) - _mastery_total_for(old_prestige))
@dataclass(frozen=True)
class MasteryUpgrade:
key: str
name: str
icon: str
description: str
max_level: int
base_cost: int
cost_growth: float
MASTERY_UPGRADES: tuple[MasteryUpgrade, ...] = (
MasteryUpgrade(
"autoreplant",
"Continuous Delivery",
"\U0001f501",
"Automatically replant the same crop right after harvest, if affordable",
1,
3,
1.0,
),
MasteryUpgrade(
"analytics",
"Farm Analytics",
"\U0001f4ca",
"Unlocks lifetime stats on your farm HUD",
1,
2,
1.0,
),
MasteryUpgrade(
"contracts",
"Legacy Contracts",
"\U0001f4dc",
"Unlocks a weekly long-term contract slot for Stars and a temporary boost",
1,
4,
1.0,
),
)
MASTERY_BY_KEY = {m.key: m for m in MASTERY_UPGRADES}
def mastery_for(key: str) -> MasteryUpgrade | None:
return MASTERY_BY_KEY.get(key)
def mastery_cost(m: MasteryUpgrade, level: int) -> int:
return round(m.base_cost * (m.cost_growth ** level))
FAIR_PLAY_ACTIVITY_WEIGHT = 50
FAIR_PLAY_HOARD_DIVISOR = 200_000
MIN_RAIDS_FOR_EFFICIENCY_BOARD = 3
def fair_play_score(harvests_week: int, coins: int) -> int:
return harvests_week * FAIR_PLAY_ACTIVITY_WEIGHT - min(coins, 10**9) // FAIR_PLAY_HOARD_DIVISOR
ERA_PRESTIGE_CARRYOVER_PCT = 0.4
ERA_REWARD_STARS_BY_RANK: tuple[int, ...] = (50, 30, 20, 15, 10, 5, 5, 5, 5, 5)
def era_score(era_coins: int, era_harvests: int, prestige: int) -> int:
return (
era_coins // 20
+ era_harvests * 10
+ round(prestige * SCORE_PRESTIGE * ERA_PRESTIGE_CARRYOVER_PCT)
)
def era_reward_stars(rank: int) -> int:
if rank < 1 or rank > len(ERA_REWARD_STARS_BY_RANK):
return 0
return ERA_REWARD_STARS_BY_RANK[rank - 1]