825 lines
22 KiB
Python
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]
|