2026-06-22 16:41:53 +00:00
|
|
|
# retoor <retoor@molodetz.nl>
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-06-27 22:31:34 +00:00
|
|
|
import hashlib
|
|
|
|
|
import math
|
2026-06-22 16:41:53 +00:00
|
|
|
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
|
|
|
|
|
|
2026-06-22 17:33:03 +00:00
|
|
|
STEAL_GRACE_SECONDS = 60
|
|
|
|
|
STEAL_FRACTION = 0.5
|
2026-06-27 22:31:34 +00:00
|
|
|
STEAL_COOLDOWN_SECONDS = 3600
|
|
|
|
|
|
|
|
|
|
GOLDEN_CHANCE = 0.05
|
|
|
|
|
GOLDEN_MULTIPLIER = 5
|
2026-06-22 17:33:03 +00:00
|
|
|
|
2026-06-22 16:41:53 +00:00
|
|
|
|
|
|
|
|
@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)
|
|
|
|
|
|
|
|
|
|
|
2026-06-27 22:31:34 +00:00
|
|
|
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)
|
|
|
|
|
)
|
2026-06-22 16:41:53 +00:00
|
|
|
|
|
|
|
|
|
2026-06-27 22:31:34 +00:00
|
|
|
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))
|
|
|
|
|
)
|
2026-06-22 16:41:53 +00:00
|
|
|
|
|
|
|
|
|
2026-06-27 22:31:34 +00:00
|
|
|
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
|
|
|
|
|
),
|
|
|
|
|
)
|
2026-06-22 16:41:53 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-06-22 20:52:02 +00:00
|
|
|
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
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-06-22 16:41:53 +00:00
|
|
|
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,
|
2026-06-27 22:31:34 +00:00
|
|
|
legacy_mult_level: int = 0,
|
|
|
|
|
legacy_speed_level: int = 0,
|
2026-06-22 16:41:53 +00:00
|
|
|
) -> dict:
|
|
|
|
|
return {
|
|
|
|
|
"key": crop.key,
|
|
|
|
|
"name": crop.name,
|
|
|
|
|
"icon": crop.icon,
|
|
|
|
|
"cost": effective_plant_cost(crop, discount_level),
|
2026-06-27 22:31:34 +00:00
|
|
|
"reward_coins": effective_reward_coins(
|
|
|
|
|
crop, yield_level, prestige, legacy_mult_level
|
|
|
|
|
),
|
2026-06-22 16:41:53 +00:00
|
|
|
"reward_xp": crop.reward_xp,
|
|
|
|
|
"min_level": crop.min_level,
|
2026-06-27 22:31:34 +00:00
|
|
|
"grow_seconds": grow_seconds_for(crop, ci_tier, growth_level, legacy_speed_level),
|
2026-06-22 16:41:53 +00:00
|
|
|
"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
|
|
|
|
|
|
2026-06-27 22:31:34 +00:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
2026-06-22 16:41:53 +00:00
|
|
|
DAILY_BASE = 20
|
|
|
|
|
DAILY_STREAK_STEP = 12
|
|
|
|
|
DAILY_STREAK_CAP = 7
|
|
|
|
|
|
|
|
|
|
FERTILIZE_FRACTION = 0.5
|
2026-06-27 22:31:34 +00:00
|
|
|
FERTILIZE_TAX = 1.05
|
2026-06-22 16:41:53 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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))
|
|
|
|
|
|
|
|
|
|
|
2026-06-27 22:31:34 +00:00
|
|
|
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)
|
|
|
|
|
)
|
2026-06-22 16:41:53 +00:00
|
|
|
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))
|
|
|
|
|
|
|
|
|
|
|
2026-06-27 22:31:34 +00:00
|
|
|
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
|
|
|
|
|
),
|
|
|
|
|
)
|
2026-06-22 17:33:03 +00:00
|
|
|
|
|
|
|
|
|
2026-06-22 16:41:53 +00:00
|
|
|
def daily_reward(streak: int) -> int:
|
|
|
|
|
effective = min(max(streak, 1), DAILY_STREAK_CAP)
|
|
|
|
|
return DAILY_BASE + DAILY_STREAK_STEP * (effective - 1)
|
|
|
|
|
|
|
|
|
|
|
2026-06-27 22:31:34 +00:00
|
|
|
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
|
|
|
|
|
),
|
|
|
|
|
)
|
2026-06-22 16:41:53 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@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
|