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

View file

@ -50,7 +50,11 @@ Alpha. One season, bandit raids plus two friendly events, six buildings.
replaced from the bank automatically.
- **Events:** raids are announced a day or two ahead. Reconnaissance from
scouts, informants and the spymaster's guild narrows down when, how strong,
and finally where their camp is, so fighters can sabotage it. Each intel
and finally where their camp is, so fighters can sabotage it. Fighters can
also drill the militia before any raid, up to half a defence per
townsperson. Favour never buys results directly: master builders,
informants and drill sergeants double what players put in for a while,
XP included. Each intel
level takes more scouting than the last and more for a bigger band, and
levels 2 and 3 only open about a day and eight hours before the raid
arrives. Defeat kills or drives

3
app.py
View file

@ -102,7 +102,7 @@ def player_view(world: dict, player: dict, now: float) -> dict:
view["blessing"] = game.blessing(player)
blessed = game.with_potion(view["blessing"], game.potion_effect(player, "combat"))
view["fight_damage"] = {a["id"]: round(game.fight_damage(player, a, blessed), 1)
for a in game.ACTIONS if a["skill"] == "combat"}
for a in game.ACTIONS if "difficulty" in a}
view["pp"] = int(player["pp"])
view["labour_per_action"] = game.labour_per_action(player)
view["levels"] = {skill: game.skill_level(player, skill) for skill in game.SKILL_IDS}
@ -116,6 +116,7 @@ def player_view(world: dict, player: dict, now: float) -> dict:
view["action"] = {**player["action"], "duration": game.action_time(world, player, action)}
view["times"] = {action["id"]: game.action_time(world, player, action) for action in game.ACTIONS}
view["blocked"] = {action["id"]: game.can_start(world, player, action) for action in game.ACTIONS}
view["drill_per_action"] = game.drill_per_action(player)
view["recon_gain"] = {action["id"]: game.recon_gain(world, action) for action in game.ACTIONS
if action.get("special") == "recon"}
return view

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"),

View file

@ -487,12 +487,17 @@ function actionCard(a) {
}
io += Object.entries(a.outputs).map(([id, q]) => itemChip(id, q)).join("");
let extra = "";
if (a.skill === "combat") {
if ("difficulty" in a) {
const chance = Math.min(0.97, Math.max(0.15, 0.5 + (me.power - a.difficulty) * 0.04));
extra = `<div class="small">Win chance <b>${Math.round(chance * 100)}%</b> · about <b>${me.fight_damage[a.id]}</b> damage a fight${a.gold ? ` · ${a.gold} gold` : ""}</div>`;
if (a.drops) io += a.drops.map(([id, q, odds]) => `<span class="item-chip" title="${esc(item(id).name)}: ${Math.round(odds * 100)}% chance">${item(id).icon} ${q} <span class="have">${Math.round(odds * 100)}%</span></span>`).join("");
if (a.special === "sabotage") extra += `<div class="small muted">Each win weakens the coming raid by 3%, up to 30%.</div>`;
}
if (a.special === "drill") {
const s = S.favours.sergeant, boost = s.boost;
extra = `<div class="small makes">${skillOf("combat").icon} +${me.drill_per_action} defence for the coming raid · ${fmt(s.drilled)} / ${fmt(s.cap)} drilled${boost ? ` <b class="boosted">×2 defence and XP for the next ${fmt(boost)}</b>` : ""}</div>
<div class="small muted">More per session every 20 combat levels. The militia can take up to half a defence per townsperson.</div>`;
}
if (a.special === "recon") {
const boost = S.favours.informant.boost;
extra = `<div class="small makes">${skillOf("reconnaissance").icon} +${+me.recon_gain[a.id].toFixed(2)} recon on the coming threat${boost ? ` <b class="boosted">×2 recon and XP for the next ${fmt(boost)}</b>` : ""}</div>`;
@ -519,7 +524,7 @@ function actionCard(a) {
<div class="foot">
<span class="why">${!active && why ? esc(why) : ""}</span>
${active ? `<button class="btn danger" data-stop>Stop</button>`
: `<span class="btns"><button class="btn" data-once="${a.id}" ${why ? "disabled" : ""} title="Do this once, then go back to what you were doing">${a.skill === "combat" ? "Fight once" : Object.keys(a.outputs).length ? "Make one" : "Once"}</button>
: `<span class="btns"><button class="btn" data-once="${a.id}" ${why ? "disabled" : ""} title="Do this once, then go back to what you were doing">${"difficulty" in a ? "Fight once" : Object.keys(a.outputs).length ? "Make one" : "Once"}</button>
<button class="btn primary" data-start="${a.id}" ${why ? "disabled" : ""}>Start</button></span>`}
</div>
</div>`;
@ -617,7 +622,7 @@ function favourPanel() {
return `<div class="panel"><h2>Favour with the council</h2>
<p class="favour">${FAVOUR} ${fmt(me.favour)} favour · ${fmt(me.donated)} given in total</p>
<div class="btns"><button class="btn" data-perk="bank_slot" ${me.favour < me.perk_costs.bank_slot ? "disabled" : ""}>+1 bank slot (${fmt(me.perk_costs.bank_slot)} favour)</button></div>
<p class="small muted">Favour can also hire builders, pay informants and rally the militia: see <button class="linkish" data-nav="council">Council &amp; threats</button>.</p></div>`;
<p class="small muted">Favour can also hire master builders, informants and drill sergeants: see <button class="linkish" data-nav="council">Council &amp; threats</button>.</p></div>`;
}
// --- town -----------------------------------------------------------------
@ -857,13 +862,13 @@ function favourSpends() {
return `<div class="panel"><h2>Spend favour on the city · you have ${FAVOUR} ${fmt(me.favour)}</h2>
${option("builders", "🏗️", "Hire a master builder", f.builders.why ? esc(f.builders.why) + "." : `The next ${f.builders.labour} labour anyone puts into the site counts double, with double Construction XP${f.builders.boost ? ` (${fmt(f.builders.boost)} still to use)` : ""}. Costs more each time on the same site.`)}
${option("informant", "🕵️", "Pay an informant", f.informant.why ? esc(f.informant.why) + "." : `Leads for the scouts: the next ${f.informant.recon} reconnaissance on the coming threat counts double, with double XP${f.informant.boost ? ` (${fmt(f.informant.boost)} still to use)` : ""}. Costs more each time for the same threat.`)}
${option("militia", "📯", "Rally the militia", f.militia.why ? esc(f.militia.why) + "." : `+10 defence for the coming raid${f.militia.rallied ? ` (+${f.militia.rallied} so far)` : ""}. Costs more each time.`)}
${option("sergeant", "📯", "Hire a drill sergeant", f.sergeant.why ? esc(f.sergeant.why) + "." : `The next ${f.sergeant.drill} defence fighters drill into the militia counts double, with double combat XP${f.sergeant.boost ? ` (${fmt(f.sergeant.boost)} still to use)` : ""}. Costs more each time for the same threat.`)}
</div>`;
}
function raidPrep(ev) {
const city = S.city, me = S.me, d = city.defence;
const names = { mercenaries: "Hired sellswords", ward: "Divine ward (temple)", rallied: "Rallied militia (favour)", townsfolk: "Townsfolk with pitchforks", walls: "Walls", militia: "Militia (barracks)", weapons: "Swords in the armoury", shields: "Shields in the armoury", arrows: "Arrows", garrison: "Garrison" };
const names = { mercenaries: "Hired sellswords", ward: "Divine ward (temple)", rallied: "Rallied militia (favour)", drilled: "Drilled militia", townsfolk: "Townsfolk with pitchforks", walls: "Walls", militia: "Militia (barracks)", weapons: "Swords in the armoury", shields: "Shields in the armoury", arrows: "Arrows", garrison: "Garrison" };
const reqs = (ev.requests || []).map((r) => {
const mine = me.bank[r.item] || 0;
return `<div class="req"><span>${item(r.item).icon}</span><span>${esc(r.label)} <span class="muted small">${fmt(r.have)} / ${fmt(r.want)}</span></span>
@ -874,7 +879,7 @@ function raidPrep(ev) {
<div class="panel"><h2>Council requests</h2>${reqs}
<p class="small muted">Arrows, swords and shields in the stockpile arm the defenders: one sword and shield each, best first. Every fight uses up arrows and breaks some of the gear, more when it's close. Food keeps them fighting.</p></div>
<div class="panel"><h2>Defence</h2>
<table class="parts">${Object.entries(d.parts).map(([k, v]) => `<tr><td>${names[k]}</td><td>${fmt(v)}</td></tr>`).join("")}
<table class="parts">${Object.entries(d.parts).filter(([k, v]) => k !== "rallied" || v).map(([k, v]) => `<tr><td>${names[k]}</td><td>${fmt(v)}</td></tr>`).join("")}
${d.starving ? `<tr><td class="bad">Hungry (×0.8)</td><td></td></tr>` : ""}</table>
<h2 style="margin-top:16px">Garrison</h2>
<p class="small">${d.garrison.length ? d.garrison.map((g) => `${esc(g.name)} (${g.power})`).join(", ") : `<span class="muted">Nobody has signed up yet.</span>`}</p>

View file

@ -91,6 +91,7 @@
<symbol id="stone-block" viewBox="0 0 512 512"><path fill="currentColor" d="m209.875 44.156l-182 106.47l119.625 54.31l148.344 11.72l41.97-24.312l17.342 11.562L309 230.656V379.53l53.563-14.624l-64.625 51.97l-110.875-59.626l-2.157-1.53l-71.28 6.56l75.936-31.967l100.75 52.125v-147.5l-145.906-11.5l-1.625-.125l-1.5-.688l-121.093-55V391.47L44 423.186l82 20.97l21.875-21.282l11.156 29.72l131.282 33.592V434l4.25 2.28l5.47 2.94l4.812-3.908L309 431.97v52.155L491.375 377.78v-96.405L466.78 269.47l24.595-38.75V125l-90.25 52.28l-1.094 34.095l-88-58.688l84.97 5.375L476.5 112L291.562 64.937l1.625.563l-64.406 5.78l5.345-20.936l-24.25-6.188z"/></symbol>
<symbol id="stone-stack" viewBox="0 0 512 512"><path fill="currentColor" d="M278.735 112.385c-6.45-.022-13.001.204-19.541.683c-26.16 1.918-52.24 7.912-71.295 17.338c-19.054 9.426-30.102 21.32-31.21 35.473c-.491 6.258 1.674 12.378 6.886 18.9s13.484 13.055 23.86 18.725c20.75 11.34 49.723 19.217 77.925 20.578s55.456-3.89 73.447-16.592c17.992-12.701 28.163-31.942 23.477-64.853c-.647-4.546-3.149-8.605-8.393-12.748c-5.244-4.144-13.144-7.946-22.834-10.828c-14.534-4.324-32.974-6.611-52.322-6.676m-58.385 27.773a15.021 30.242 71.137 0 1 16.541 7.426a15.021 30.242 71.137 0 1-23.646 24.008a15.021 30.242 71.137 0 1-33.557-4.356a15.021 30.242 71.137 0 1 23.647-24.006a15.021 30.242 71.137 0 1 17.015-3.072m41.203 101.729c-37.759-.033-75.243 5.18-102.252 16.006c-13.504 5.412-24.3 12.2-31.423 19.904c-7.123 7.703-10.792 16.021-10.834 26.406c-.027 6.585 2.827 11.537 10.013 16.832c7.187 5.295 18.653 9.967 32.887 13.277c28.467 6.621 67.726 8.057 106.684 5.051c38.957-3.005 77.792-10.437 105.572-20.668c13.89-5.115 24.986-11.006 31.701-16.691c6.715-5.686 8.59-10.002 8.066-14.201c-.602-4.834-4.196-10.34-12.113-16.06s-19.702-11.174-33.931-15.651c-28.459-8.955-66.61-14.172-104.37-14.205m-76.722 28a15.623 30.966 72.083 0 1 16.52 8.297a15.623 30.966 72.083 0 1-25.08 24.324a15.623 30.966 72.083 0 1-33.964-5.639a15.623 30.966 72.083 0 1 25.078-24.324a15.623 30.966 72.083 0 1 17.446-2.658m105.074 87.545a737 737 0 0 0-36.129.824c-48.367 2.29-96.637 9.383-132.682 20.898c-18.022 5.758-32.96 12.664-43.14 20.12c-10.181 7.455-15.225 14.927-16.135 22.431c-.771 6.358 2.69 13.296 12.693 21.38c10.004 8.086 25.876 16.155 45.407 23.048c39.06 13.786 92.694 23.167 145.267 24.806c52.574 1.64 104.231-4.626 139.022-20.488c17.395-7.93 30.393-18.095 37.974-30.289c7.582-12.194 10.317-26.587 6.166-45.414c-.984-4.464-5.912-10.175-16.506-15.58c-10.593-5.405-25.998-10.126-44.365-13.654c-27.55-5.292-61.777-8.017-97.572-8.082m-159.727 42.4a18.747 35.889 83.498 0 1 28.702 14.086a18.747 35.889 83.498 0 1-31.659 23.371a18.747 35.889 83.498 0 1-39.367-13.441a18.747 35.889 83.498 0 1 31.658-23.371a18.747 35.889 83.498 0 1 10.666-.645"/></symbol>
<symbol id="stone-wall" viewBox="0 0 512 512"><path fill="currentColor" d="m18.08 23l.1 18H494V23zM254.3 54.93L133 61.6l30.1 68.5l98.1-3.6zm115.9.28l-92.9.53l3 71.96l96.8-.9zM18.23 56.43l.35 74.67l72.27-2.4l24.35-72.27zM393 64.24l-.5 47.16l15.7 21.5l85.8-6.3V64.93zM120.8 84.68l-17.6 49.82l41.1-1l-8.2-34.02zm354.7 56.02l-4 75.5l22.5-.6v-74.9zm-25.7 3.5l-97.3 10.7L346 219l110.7-3.2zm-243.8.7l-4 75.5l126.1-3.9l7-71.1zm-145.33 2.7l-3.9 75.5l126.13-3.9l-6.9-71.6zm-42 1.5l.33 72.4l24.77-.8l-6.9-71.6zm278.93 84.3L276 277.2l2.1 42l128.7-1.9l-7.6-79.7zm-40.2 1.4l-127.2 6l8 69.5l124.7 6.6zm236.6 3.1l-66.5 2.9l-7.5 75.8l74-5.2zm-474.89 7l.31 65.8l100.78-3.1l-6.9-62.7zm470.29 81.2l-115.9 10.5l-3.3 65l121.5-3.5zm-422.36 4.1l-47.52 2l.34 72.9l51.1-1.3zm17.84 0l3.98 68.5l138.64 9.6l-3-78.1zm161.22 4.3l-5.5 69l107.5 5.4l-1-73.5zm185.4 81.1l-4 75.5l66.5-.6v-73.8zm-9.8 1.2l-39 3.6l27.4 28zm-399.46 1.8l-2.13 40.6l.16 34.8H145.4v-68.9l-46.56-2.5l-18.4 14L68.56 421zm247.96.3l-111.3 1.5l3.5 72.5l114.7-2.4zm101.2 4.3l-72.2 5.9l-8.9 62.1l125.6.6l-6.2-27z"/></symbol>
<symbol id="sword-brandish" viewBox="0 0 512 512"><path fill="currentColor" d="M122.1 17.52L186.6 161l104.5-44.9l-44.5-98.58h-51.1l35.8 80.75l-16.4 7.23l-39-87.98zm265.2 77.75L110.4 215.2l12 28.2l278.1-119.8zm-69.7 98.53l-23.9 11.5c12.8 5.4 26 13.7 38.3 15.6c18.1-13.1-4.9-30.5-14.4-27.1m-60.7 11.1l-36.2 16.7c-11.9 18.4-14.8 46-18.2 69.7c20.6 30.1 30.5 62.4 39.5 94c17.9-3.7 32.2-6.8 48.7-11.5c-24.4-38.7-42.2-78.4-53.6-118.9c0 0 5.6-10.5 8.4-15.8c11.9 7.3 24.9 12.4 36.6 9.9c10-4.1 21.5-7.9 27.7-16.6c-17.6-6.9-37.1-17.5-52.9-27.5m92.6 26.6c-10.8 5.8-23 11.2-31.9 18.9c-5.3 4.7-12.1 8.8-20.5 12.3c1.1 7.3-.1 19.6 8.1 18.8c20.5-6.2 40.5-15 59.7-28.4c-.1-9.2-8-24.1-15.4-21.6m-90.2 34.2c11.3 34.8 27.6 69 49.3 102.6c4.7-1.6 9.3-3.3 13.5-5c-7.3-6.2-11.7-15.9-12-25.3c-16.6-4.1-20.8-29.4-17.2-41.6c-9.5-7-13.6-18.1-14.2-28.7c-7.2.7-13.6-.1-19.4-2m103.3 10.4c-13.6 8.1-27.6 14.3-41.6 19.2l-9 4.4c-4.4 4.6-4.5 15.6 1 20.3c9.7 3.5 50.1-21.2 57.7-27.9c1.3-5.8-3.5-14.9-8.1-16m-178.1 21.2L19.1 335.8V495c95.7-25.3 146.9-47.1 209.7-90.2c-10.8-38-20.5-74.8-44.3-107.5m187.8 16.9c-13.6 10.2-27.2 16.2-42.9 21.4c-.4 6.7 2.5 18.1 12.1 16c19.9-4.3 36.3-17.5 36.6-24.6c.2-3.7-2-10.5-5.8-12.8m-7.5 48.6c-14.3 8.2-31.5 15.9-46.7 21.4l18.7 31.8l39.3-18.8zm22.2 49.1L342.9 433l3.1 21.9c15.1.5 46.6-11.3 55.4-26.1v-.1z"/></symbol>
<symbol id="temple-gate" viewBox="0 0 512 512"><path fill="currentColor" d="M256 26.92L28.98 183H483zm0 26.29L427.9 167H84.1zm0 21.58L143.9 149h224.2zM73 201v286h30V201zm48 0v286h126V377H137v-54h110V201zm144 0v122h110v54H265v110h126V201zm144 0v286h30V201zM155 341v18h202v-18zm37 50c13.7 0 25 11.3 25 25s-11.3 25-25 25s-25-11.3-25-25s11.3-25 25-25m128 0c13.7 0 25 11.3 25 25s-11.3 25-25 25s-25-11.3-25-25s11.3-25 25-25m-128 18c-4 0-7 3-7 7s3 7 7 7s7-3 7-7s-3-7-7-7m128 0c-4 0-7 3-7 7s3 7 7 7s7-3 7-7s-3-7-7-7"/></symbol>
<symbol id="tombstone" viewBox="0 0 512 512"><path fill="currentColor" d="M256 46c-81.822 0-150 63.366-150 150v150c0 6.668-.757 23.558 0 30h300c.757-6.442 0-23.332 0-30V196c0-86.634-68.178-150-150-150M16 406v60h480v-60z"/></symbol>
<symbol id="treasure-map" viewBox="0 0 512 512"><path fill="currentColor" d="M227.4 34.7c-10.1 0-20.2.2-30.2.5l6.1 65.6l-61.1-62.5c-31.3 2.5-62.5 6.6-93.8 12.5l34.2 28.4l-48-.6c35.1 100.2 6.9 182.6-.3 292.1L130 476.5c10-1.3 19.9-2.4 29.6-3.3l21.5-42.2l18.6 28.8l41.5-33.5l.8 43c82.9-.2 157.7 9.1 235.7 7.9c-28.2-73-31.2-143.6-31.9-209.2l-33.3-19.1l32.7-33.9c-.4-21.3-1.3-42-3.6-61.9l-57.4.7l50.2-41.7c-3.8-15.5-9-30.4-16.1-44.7l-29.5-23.9C335 38 281.2 34.6 227.4 34.7m58.7 37c10.6 24.75 21.1 49.5 31.7 74.3c7.5-10.5 14.9-21 22.4-31.5c16 27.2 32 54.3 48 81.5l-16.2 9.5l-33.3-56.7l-42.5 59.4l-15.2-10.9l24-33.5l-21.9-51.5l-24.6 40.1l12 22.6l-16.5 8.8l-18.3-34.5l-24.8 58.2l-17.2-7.4l32.5-76.2l7.7-18c4.8 9.2 9.6 18.3 14.5 27.4c12.5-20.6 25.1-41.11 37.7-61.6M91.2 128c6.72 1.6 13.4 3.4 19.2 5.3c-2.1 5.9-4.1 11.8-6.2 17.6c-5.79-1.6-11.72-3.4-16.9-4.7c1.39-6 2.62-12.1 3.9-18.2m37.9 13.4c6.3 3.8 12 7.2 17 12.8L132.6 167c-4-3.7-8.6-7-12.8-9.4zm28.7 32.3c2.1 7.4 2.1 15.7 1.6 22.5l-18.5-2.4c.1-5.1.3-10-1-14.5zm-21.2 35.7l17.2 7.1c-3.3 6.6-5.1 12.7-8.6 17.8l-16.3-9c2.6-5.4 5.6-10.8 7.7-15.9m-16.5 34.1l17.7 6.1c-1.5 5.4-3 11.2-3.6 16.2l-18.6-2c1.3-7.5 2.1-14 4.5-20.3m207.8 17.4c8.5 1 14.6 3 21.7 7.1l-9.8 16c-4.1-2.8-9.4-3.8-13.5-4.5zm-21.2 1.5c1.1 6.1 2.5 12.2 3.9 18.3c-5.9 1.3-11.7 3.3-16.5 5.1l-6.8-17.4c6.7-2.4 13.5-4.7 19.4-6m-37.9 15.9l11 15.1c-5.6 4-11.8 7.8-16.8 10.6l-8.9-16.4c5.1-2.9 10.6-6.3 14.7-9.3M135.3 281c1.5 4.7 4.2 9.2 6.9 12.1l-13.8 12.6c-5.5-5.7-9.5-13.5-11.2-20.1zm230.3 3.3c3.5 6.4 6.8 12.7 8.7 19.1l-17.8 5.6c-2-5.4-4.3-10.8-6.8-14.8zm-127.4 10.9l6.9 17.3c-6.4 2.7-12.9 4.8-18.6 6.5l-5-18c5.9-1.6 11.3-3.8 16.7-5.8m-83.8 6.2c5.3 1.7 10.8 3.4 15.7 4.2c-1.2 6.1-2 12.3-2.8 18.5c-7-1-14.5-3.3-20.5-5.7zm50 3.5l2.8 18.5c-7.2 1.3-13.4 1.6-19.8 1.9l-.4-18.7c5.9-.2 11.6-.8 17.4-1.7m174.5 18c1 6.4 1.6 12.9 2.2 19.3l-18.7 1.5c-.4-6-.9-11.9-2-17.8zm-67.6 30.8c18.9 3.5 44.9 16.2 68.9 33.9c7.4-9.9 14.4-20.4 21.3-31.1l30.1 12.9c-4.7 12.3-15 25.6-28.6 37.2c17 16.2 30.9 34.5 37 53c-13.8-18.1-31.1-31.8-50.3-42.8c-23.4 15.8-52.7 25.9-79.6 20.4c22.9-4.4 40.6-16.6 55.8-32.6c-16.5-7.5-33.8-13.9-51.3-20.1z"/></symbol>

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 164 KiB

Before After
Before After

View file

@ -592,20 +592,39 @@ class FavourTest(unittest.TestCase):
event["recon"] = game.intel_costs(event)[-1]
self.assertEqual(game.spend_favour(world, p, "informant", T0), "Nothing more to learn")
def test_rallies_cost_more_each_time_and_end_with_the_raid(self):
def test_drilling_adds_defence_up_to_half_the_population(self):
world, p = self.world, self.p
self.assertEqual(game.spend_favour(world, p, "militia", T0), "No raid to prepare for")
self.assertEqual(game.start_action(world, p, "drill_militia", T0), "No raid to prepare for")
world["next_event_at"] = T0
game.tick(world, T0, random.Random(2))
world["city"]["population"] = 20.0
before = game.defence(world)["total"]
game.spend_favour(world, p, "militia", T0)
self.assertEqual(game.favours(world, T0)["militia"]["cost"], 300)
game.spend_favour(world, p, "militia", T0)
self.assertEqual(game.defence(world)["total"], before + 20)
self.assertEqual(p["favour"], 5000 - 150 - 300)
self.assertIsNone(game.start_action(world, p, "drill_militia", T0))
game.process_player(world, p, T0 + 50 * 4 + 1, random.Random(1))
self.assertEqual(game.defence(world)["parts"]["drilled"], 2.0)
self.assertEqual(game.defence(world)["total"], before + 2)
self.assertEqual(p["xp"]["combat"], 40)
self.assertEqual(p["hp"], 44) # drilling is not a fight
game.process_player(world, p, T0 + 50 * 40, random.Random(1))
self.assertEqual(game.defence(world)["parts"]["drilled"], 10.0)
self.assertIsNone(p["action"]) # stops once the militia is fully drilled
game.tick(world, game.upcoming_raid(world)["fire_at"], random.Random(1))
self.assertEqual(game.defence(world)["parts"]["rallied"], 0)
self.assertEqual(game.favours(world, T0)["militia"]["cost"], 150)
self.assertEqual(game.defence(world)["parts"]["drilled"], 0)
def test_drill_sergeant_doubles_drilling_for_a_while(self):
world, p = self.world, self.p
self.assertEqual(game.spend_favour(world, p, "sergeant", T0), "No raid to prepare for")
world["next_event_at"] = T0
game.tick(world, T0, random.Random(2))
self.assertIsNone(game.spend_favour(world, p, "sergeant", T0))
self.assertEqual(game.defence(world)["parts"]["drilled"], 0) # a sergeant drills nobody by himself
self.assertEqual(game.favours(world, T0)["sergeant"]["cost"], 225)
game.start_action(world, p, "drill_militia", T0)
game.process_player(world, p, T0 + 50 * 4 + 1, random.Random(1))
self.assertEqual(game.upcoming_raid(world)["drilled"], 4.0)
self.assertEqual(p["xp"]["combat"], 80)
self.assertEqual(world["city"]["drill_boost"], 13)
self.assertEqual(p["favour"], 5000 - 150)
def test_not_enough_favour(self):
self.p["favour"] = 10