Intel: scaled recon costs, time-gated levels, spymaster multiplies recon
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
366a195c6d
commit
27bdb6a154
5 changed files with 113 additions and 33 deletions
67
game.py
67
game.py
|
|
@ -20,7 +20,6 @@ BASE_STACK = 1000
|
|||
CLASS_SPEED = 0.8
|
||||
LUCKY_CHANCE = 0.1
|
||||
OVERFLOW_FAVOUR = 0.5 # favour for items a full bank spills to the city, vs. giving on purpose
|
||||
RECON_PER_INTEL = 8
|
||||
MAX_LOG = 200
|
||||
REGEN_PER_HOUR = 0.25 # share of max HP recovered per game hour
|
||||
EAT_BELOW = 0.5 # auto-eat when HP drops under this share of max
|
||||
|
|
@ -457,7 +456,7 @@ BUILDINGS = {
|
|||
"name": "Spymaster's guild", "icon": "🕵️", "max": 3, "gold": 400,
|
||||
"base": {"logs": 500, "bricks": 300, "hide": 100, "feathers": 300}, "advanced": {"cut_stone": 150},
|
||||
"late": {"bear_leather": 40, "oak_plank": 40},
|
||||
"effect": "One more level of intel on every event per level",
|
||||
"effect": "+25% reconnaissance per level, and its agents gather some every hour on their own",
|
||||
},
|
||||
"storehouse": {
|
||||
"name": "Storehouse", "icon": "📦", "max": 5, "gold": 200,
|
||||
|
|
@ -1095,9 +1094,9 @@ def _complete(world: dict, player: dict, action: dict, now: float, rng: random.R
|
|||
summary["losses"] += 1
|
||||
xp = xp // 3
|
||||
if action.get("special") == "recon":
|
||||
event = upcoming_event(world)
|
||||
event = upcoming_raid(world)
|
||||
if event:
|
||||
event["recon"] += action["recon"] + fitted(world, "map_table")
|
||||
event["recon"] += recon_gain(world, action)
|
||||
if action.get("special") == "problem" and action["skill"] != "combat":
|
||||
_work_problem(world, player, action, now)
|
||||
if action.get("special") == "labour":
|
||||
|
|
@ -1572,6 +1571,9 @@ def _city_hour(world: dict, now: float) -> None:
|
|||
pop = city["population"]
|
||||
if city["construction"]:
|
||||
city["construction"]["labour"] += pop * TOWNSFOLK_LABOUR
|
||||
threat = upcoming_raid(world)
|
||||
if threat and building_level(world, "spymaster"):
|
||||
threat["recon"] += SPYMASTER_AGENTS * building_level(world, "spymaster")
|
||||
city["food_due"] += (pop * 0.5 / 24 * city_rate(world, "food") * (1.5 if city.get("cold") else 1)
|
||||
* (0.9 if fitted(world, "grain_bins") else 1))
|
||||
stock = city["stockpile"]
|
||||
|
|
@ -1663,8 +1665,45 @@ def _announce(world: dict, now: float, rng: random.Random) -> None:
|
|||
del world["events"][:-30]
|
||||
|
||||
|
||||
def intel_level(world: dict, event: dict) -> int:
|
||||
return min(3, building_level(world, "spymaster") + event["recon"] // RECON_PER_INTEL)
|
||||
# Intel: each level takes more reconnaissance than the last, a bigger band is
|
||||
# harder to read (a dragon is hard to miss), and the sharper levels only become
|
||||
# knowable as the threat draws near, however much scouting is done.
|
||||
INTEL_POINTS = [15, 30, 45] # recon for intel levels 1, 2 and 3 against a small band
|
||||
INTEL_SIZE = [1.0, 1.33, 1.67, 2.0] # small, medium, large, huge
|
||||
INTEL_OPENS = [None, 24, 8] # game hours before arrival that levels 2 and 3 open, ±25% per threat
|
||||
SPYMASTER_RECON = 0.25 # more recon per guild level
|
||||
SPYMASTER_AGENTS = 0.5 # recon per guild level per game hour, with nobody scouting
|
||||
INFORMANT_RECON = 15
|
||||
|
||||
|
||||
def recon_gain(world: dict, action: dict) -> float:
|
||||
return (action["recon"] + fitted(world, "map_table")) * (1 + SPYMASTER_RECON * building_level(world, "spymaster"))
|
||||
|
||||
|
||||
def intel_costs(event: dict) -> list[int]:
|
||||
"""Total recon needed for each intel level."""
|
||||
size = next(i for i, (limit, _label) in enumerate(SIZE_LABELS) if event["strength"] < limit)
|
||||
scale = INTEL_SIZE[size] * (0.5 if event["kind"] == "dragon" else 1)
|
||||
return [round(sum(INTEL_POINTS[:level]) * scale) for level in (1, 2, 3)]
|
||||
|
||||
|
||||
def intel_opens(world: dict, event: dict, level: int) -> float:
|
||||
"""When an intel level becomes knowable. The ±25% is fixed per threat, so it can't be timed."""
|
||||
hours = INTEL_OPENS[level - 1]
|
||||
if hours is None:
|
||||
return 0
|
||||
wobble = random.Random(f"{event['id']}-{level}").uniform(0.75, 1.25)
|
||||
return event["fire_at"] - hours * wobble * HOUR / speed(world)
|
||||
|
||||
|
||||
def intel_level(world: dict, event: dict, now: float | None = None) -> int:
|
||||
now = world["city"]["last_tick"] if now is None else now
|
||||
level = 0
|
||||
for next_level, need in enumerate(intel_costs(event), 1):
|
||||
if event["recon"] < need or now < intel_opens(world, event, next_level):
|
||||
break
|
||||
level = next_level
|
||||
return level
|
||||
|
||||
|
||||
SIZE_LABELS = [(30, "small"), (60, "medium"), (100, "large"), (math.inf, "huge")]
|
||||
|
|
@ -1673,14 +1712,16 @@ WINDOW_HOURS = [24, 8, 2, 0]
|
|||
|
||||
def event_view(world: dict, event: dict, now: float) -> dict:
|
||||
"""What the city knows about an upcoming event, filtered by intel."""
|
||||
level = intel_level(world, event)
|
||||
level = intel_level(world, event, now)
|
||||
half = WINDOW_HOURS[level] * HOUR / speed(world)
|
||||
costs = intel_costs(event)
|
||||
centre = event["fire_at"] + event["jitter"] * half * 0.6
|
||||
view = {
|
||||
"id": event["id"], "kind": event["kind"], "intel": level,
|
||||
"earliest": max(now, centre - half), "latest": max(now, centre + half),
|
||||
"exact": event["fire_at"] if level >= 3 else None,
|
||||
"recon": event["recon"], "recon_next": RECON_PER_INTEL - event["recon"] % RECON_PER_INTEL,
|
||||
"recon": int(event["recon"]), "recon_full": costs[-1],
|
||||
"recon_next": max(0, round(costs[level] - event["recon"])) if level < 3 else 0,
|
||||
}
|
||||
if event["kind"] == "dragon":
|
||||
strength = event["strength"]
|
||||
|
|
@ -2453,7 +2494,6 @@ def favours(world: dict, now: float) -> dict:
|
|||
"""What favour can buy for the city right now: cost, and why not if it can't."""
|
||||
city = world["city"]
|
||||
work = city["construction"]
|
||||
event = upcoming_event(world)
|
||||
raid = upcoming_raid(world)
|
||||
return {
|
||||
"builders": {
|
||||
|
|
@ -2464,8 +2504,8 @@ def favours(world: dict, now: float) -> dict:
|
|||
},
|
||||
"informant": {
|
||||
"cost": INFORMANT_FAVOUR,
|
||||
"why": "No news to dig into" if not event else
|
||||
"Nothing more to learn" if intel_level(world, event) >= 3 else None,
|
||||
"why": "No news to dig into" if not raid else
|
||||
"Nothing more to learn" if raid["recon"] >= intel_costs(raid)[-1] else None,
|
||||
},
|
||||
"militia": {
|
||||
"cost": RALLY_FAVOUR * (city.get("rallies", 0) + 1),
|
||||
|
|
@ -2494,7 +2534,7 @@ def spend_favour(world: dict, player: dict, kind: str, now: float) -> str | None
|
|||
log(world, now, "favour", f"{player['name']} hired a master builder for the {name}: "
|
||||
f"the next {MASTER_BUILDER_LABOUR} labour counts double", player["id"])
|
||||
elif kind == "informant":
|
||||
upcoming_event(world)["recon"] += RECON_PER_INTEL
|
||||
upcoming_raid(world)["recon"] += INFORMANT_RECON
|
||||
log(world, now, "favour", f"{player['name']} paid an informant for news from the roads", player["id"])
|
||||
else:
|
||||
city["rally"] = city.get("rally", 0) + RALLY_DEFENCE
|
||||
|
|
@ -2656,5 +2696,6 @@ def content() -> dict:
|
|||
"city_uses": {item: city_uses(item) for item in ITEMS},
|
||||
"prayers": [{**prayer, "text": describe_prayer(prayer)} for prayer in PRAYERS], "max_prayers": MAX_PRAYERS,
|
||||
"potion_charges": POTION_CHARGES, "problems": PROBLEMS, "fittings": list(FITTING_BY_ID.values()),
|
||||
"equip_slots": EQUIP_SLOTS, "eat_below": EAT_BELOW, "class_speed": CLASS_SPEED, "regen_per_hour": REGEN_PER_HOUR,
|
||||
"equip_slots": EQUIP_SLOTS, "eat_below": EAT_BELOW, "class_speed": CLASS_SPEED,
|
||||
"informant_recon": INFORMANT_RECON, "regen_per_hour": REGEN_PER_HOUR,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue