Drilling scales with the threat, like scouting

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-19 15:35:35 +01:00
parent 1c80bd80f7
commit b248a030ea
4 changed files with 50 additions and 31 deletions

37
game.py
View file

@ -371,7 +371,7 @@ ACTIONS = [
_action("cook_meat", "cooking", "Cooked meat", 5, 20, 7, {"raw_meat": 1, "logs": 1}, {"cooked_meat": 1}),
_action("cook_stew", "cooking", "Stew", 15, 35, 16,
{"raw_meat": 1, "vegetables": 2, "logs": 1}, {"stew": 2}),
_action("drill_militia", "combat", "Drill the militia", 1, 50, 10, special="drill"),
_action("drill_militia", "combat", "Drill the militia", 1, 60, 12, special="drill"),
_action("fight_wolves", "combat", "Wolves in the woods", 1, 60, 14, difficulty=2, damage=2, gold=2,
drops=[["raw_meat", 1, 0.6], ["hide", 1, 0.5], ["bones", 1, 1.0]]),
_action("fight_scouts", "combat", "Bandit scouts", 10, 75, 30, difficulty=14, damage=6, gold=10,
@ -1021,7 +1021,7 @@ def can_start(world: dict, player: dict, action: dict) -> str | None:
threat = upcoming_raid(world)
if not threat:
return "No raid to prepare for"
if threat.get("drilled", 0) >= drill_cap(world):
if threat.get("drilled", 0) >= drill_needed(threat):
return "The militia is as drilled as it can be"
if "difficulty" in action and player["hp"] < max_hp(player) * TOO_HURT and not edible(player):
return "Too hurt to fight. Rest, or bring food"
@ -1120,7 +1120,7 @@ def _complete(world: dict, player: dict, action: dict, now: float, rng: random.R
boost = min(city.get("drill_boost", 0), gain)
city["drill_boost"] = city.get("drill_boost", 0) - boost
xp = DRILL_XP * (gain + boost) * mods["xp"]
threat["drilled"] = min(drill_cap(world), threat.get("drilled", 0) + gain + boost)
threat["drilled"] = min(drill_needed(threat), threat.get("drilled", 0) + gain + boost)
if action.get("special") == "labour":
city = world["city"]
site = city["construction"]
@ -2510,17 +2510,22 @@ def happenings_view(world: dict, now: float) -> dict:
MASTER_BUILDER_LABOUR = 50 # boosted labour per hire: doubled labour and XP for whoever works it
MASTER_BUILDER_FAVOUR = 150 # +50% for every further hire on the same site
INFORMANT_FAVOUR = 250
# Drilling: fighters train the townsfolk before a raid, a little defence per
# session (more for a seasoned fighter), up to half the population.
DRILL_PER_ACTION = 0.5 # plus as much again every 20 combat levels
DRILL_XP = 20 # combat XP per point of defence drilled
DRILL_SHARE = 0.5 # most drilled defence: this much per townsperson
DRILL_SERGEANT_POINTS = 15 # boosted drilling per hire: doubled defence and XP
# Drilling: fighters train the townsfolk before a raid. A full drill takes as
# many points as full intel on the same threat, so fighters and scouts put in
# about the same time; the defence it's worth grows with the population.
DRILL_PER_ACTION = 1.0 # drill points per session, +25% every 20 combat levels
DRILL_XP = 12 # combat XP per drill point
DRILL_SHARE = 1.0 # defence of a fully drilled militia, per townsperson
DRILL_SERGEANT_POINTS = 30 # boosted drilling per hire: doubled points and XP
DRILL_SERGEANT_FAVOUR = 150 # +50% for every further hire against the same threat
def drill_per_action(player: dict) -> float:
return DRILL_PER_ACTION * (1 + skill_level(player, "combat") // 20)
return DRILL_PER_ACTION * (1 + 0.25 * (skill_level(player, "combat") // 20))
def drill_needed(threat: dict) -> int:
return intel_costs(threat)[-1]
def drill_cap(world: dict) -> float:
@ -2528,8 +2533,11 @@ def drill_cap(world: dict) -> float:
def drilled(world: dict) -> float:
"""Defence the drilling is worth so far: a share of the cap, as far as the drill has got."""
threat = upcoming_raid(world)
return min(drill_cap(world), threat.get("drilled", 0)) if threat else 0
if not threat:
return 0
return drill_cap(world) * min(1.0, threat.get("drilled", 0) / drill_needed(threat))
def favours(world: dict, now: float) -> dict:
@ -2556,10 +2564,11 @@ def favours(world: dict, now: float) -> dict:
"sergeant": {
"cost": round(DRILL_SERGEANT_FAVOUR * (1 + 0.5 * (raid or {}).get("sergeants", 0))),
"why": "No raid to prepare for" if not raid else
"The militia is as drilled as it can be" if drilled(world) >= drill_cap(world) else None,
"The militia is as drilled as it can be" if raid.get("drilled", 0) >= drill_needed(raid) else None,
"drill": DRILL_SERGEANT_POINTS,
"boost": round(city.get("drill_boost", 0), 1),
"drilled": round(drilled(world), 1), "cap": drill_cap(world),
"points": int((raid or {}).get("drilled", 0)), "needed": drill_needed(raid) if raid else 0,
"defence": round(drilled(world), 1), "cap": drill_cap(world),
},
}
@ -2593,7 +2602,7 @@ def spend_favour(world: dict, player: dict, kind: str, now: float) -> str | None
raid["sergeants"] = raid.get("sergeants", 0) + 1
city["drill_boost"] = city.get("drill_boost", 0) + DRILL_SERGEANT_POINTS
log(world, now, "favour", f"{player['name']} hired a drill sergeant: the next {DRILL_SERGEANT_POINTS} "
f"drilled defence counts double", player["id"])
f"drill points count double", player["id"])
return None