Drill the militia; drill sergeant replaces rallying

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

69
game.py
View file

@ -371,6 +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("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,
@ -1016,7 +1017,13 @@ def can_start(world: dict, player: dict, action: dict) -> str | None:
blocked = skill_mods(world, action)["blocked"]
if blocked:
return blocked
if action["skill"] == "combat" and player["hp"] < max_hp(player) * TOO_HURT and not edible(player):
if action.get("special") == "drill":
threat = upcoming_raid(world)
if not threat:
return "No raid to prepare for"
if threat.get("drilled", 0) >= drill_cap(world):
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"
return None
@ -1047,7 +1054,7 @@ def _complete(world: dict, player: dict, action: dict, now: float, rng: random.R
outputs[item] = int(exact) + (1 if rng.random() < exact - int(exact) else 0)
xp *= mods["xp"]
if action["skill"] == "combat":
if "difficulty" in action:
blessed = blessing(player)
if player["prayers"] and not blessed["cost"]:
summary["prayer_out"] = True
@ -1105,6 +1112,15 @@ def _complete(world: dict, player: dict, action: dict, now: float, rng: random.R
event["recon"] += gain + boost
if action.get("special") == "problem" and action["skill"] != "combat":
_work_problem(world, player, action, now)
threat = upcoming_raid(world) if action.get("special") == "drill" else None
if threat:
city = world["city"]
gain = drill_per_action(player)
# A drill sergeant doubles the drilling (and the XP) of whoever does it while the boost lasts.
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)
if action.get("special") == "labour":
city = world["city"]
site = city["construction"]
@ -1125,7 +1141,8 @@ def _complete(world: dict, player: dict, action: dict, now: float, rng: random.R
if action.get("special") == "offer":
city = world["city"]
city["devotion"] = city.get("devotion", 0) + action["devotion"] * (1 + 0.25 * building_level(world, "temple")) * bonus
_use_charge(player, action["skill"], summary)
if action.get("special") != "drill":
_use_charge(player, action["skill"], summary)
for item, qty in outputs.items():
kept = bank_add(world, player, item, qty, now, summary)
@ -1514,7 +1531,8 @@ def defence(world: dict) -> dict:
"weapons": round(0.35 * best_kit(stock, "weapon", "power", armed), 1),
"shields": round(0.5 * best_kit(stock, "shield", "armour", armed), 1),
"arrows": min(100, stock.get("arrows", 0) // 10),
"rallied": city.get("rally", 0),
"drilled": round(drilled(world), 1),
"rallied": city.get("rally", 0), # favour bought before drilling existed; gone after the next raid
"ward": ward(world),
"mercenaries": city.get("hired", 0),
"garrison": 0,
@ -2492,8 +2510,26 @@ 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
RALLY_FAVOUR = 150
RALLY_DEFENCE = 10
# 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
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)
def drill_cap(world: dict) -> float:
return round(DRILL_SHARE * world["city"]["population"], 1)
def drilled(world: dict) -> float:
threat = upcoming_raid(world)
return min(drill_cap(world), threat.get("drilled", 0)) if threat else 0
def favours(world: dict, now: float) -> dict:
@ -2517,10 +2553,13 @@ def favours(world: dict, now: float) -> dict:
"recon": INFORMANT_RECON,
"boost": round(city.get("recon_boost", 0)),
},
"militia": {
"cost": RALLY_FAVOUR * (city.get("rallies", 0) + 1),
"why": None if raid else "No raid to prepare for",
"rallied": city.get("rally", 0),
"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,
"drill": DRILL_SERGEANT_POINTS,
"boost": round(city.get("drill_boost", 0), 1),
"drilled": round(drilled(world), 1), "cap": drill_cap(world),
},
}
@ -2550,9 +2589,11 @@ def spend_favour(world: dict, player: dict, kind: str, now: float) -> str | None
log(world, now, "favour", f"{player['name']} paid an informant: the next {INFORMANT_RECON} "
f"reconnaissance counts double", player["id"])
else:
city["rally"] = city.get("rally", 0) + RALLY_DEFENCE
city["rallies"] = city.get("rallies", 0) + 1
log(world, now, "favour", f"{player['name']} rallied the militia (+{RALLY_DEFENCE} defence for the coming raid)", player["id"])
raid = upcoming_raid(world)
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"])
return None
@ -2625,7 +2666,7 @@ ACTION_GLYPHS = {
"fight_ruins": ("castle-ruins", "#a8a29e"), "fight_stronghold": ("castle", "#8a8f98"),
"fight_green_dragon": ("dragon-head", "#5fae5a"), "fight_blue_dragon": ("dragon-head", "#4f86d6"),
"fight_red_dragon": ("dragon-head", "#d0503c"), "fight_black_dragon": ("dragon-head", "#8a8f98"),
"sabotage": ("fire-bomb", "#e0703a"), "recon_roads": ("spyglass", "#6f8fec"), "recon_camps": ("spyglass", "#8a7fd0"),
"sabotage": ("fire-bomb", "#e0703a"), "drill_militia": ("sword-brandish", "#e0a050"), "recon_roads": ("spyglass", "#6f8fec"), "recon_camps": ("spyglass", "#8a7fd0"),
"labour": ("hammer-nails", "#d1a46a"), "clear_wolves": ("wolf-head", "#9aa1ab"),
"catch_goblins": ("goblin-head", "#8fbf6a"), "drive_bear": ("bear-head", "#8a5a32"), "cull_boars": ("boar", "#a06a4a"),
"catch_rats": ("rat", "#9aa1ab"), "clear_rockslide": ("falling-rocks", "#a8a29e"),