Informants give scouts boosted recon instead of intel
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
27bdb6a154
commit
fd66b69401
3 changed files with 40 additions and 16 deletions
27
game.py
27
game.py
|
|
@ -1096,7 +1096,13 @@ def _complete(world: dict, player: dict, action: dict, now: float, rng: random.R
|
||||||
if action.get("special") == "recon":
|
if action.get("special") == "recon":
|
||||||
event = upcoming_raid(world)
|
event = upcoming_raid(world)
|
||||||
if event:
|
if event:
|
||||||
event["recon"] += recon_gain(world, action)
|
city = world["city"]
|
||||||
|
gain = recon_gain(world, action)
|
||||||
|
# An informant's leads double the reconnaissance (and the XP) of whoever follows them up.
|
||||||
|
boost = min(city.get("recon_boost", 0), gain)
|
||||||
|
city["recon_boost"] = city.get("recon_boost", 0) - boost
|
||||||
|
xp *= 1 + boost / gain
|
||||||
|
event["recon"] += gain + boost
|
||||||
if action.get("special") == "problem" and action["skill"] != "combat":
|
if action.get("special") == "problem" and action["skill"] != "combat":
|
||||||
_work_problem(world, player, action, now)
|
_work_problem(world, player, action, now)
|
||||||
if action.get("special") == "labour":
|
if action.get("special") == "labour":
|
||||||
|
|
@ -1673,7 +1679,7 @@ 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
|
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_RECON = 0.25 # more recon per guild level
|
||||||
SPYMASTER_AGENTS = 0.5 # recon per guild level per game hour, with nobody scouting
|
SPYMASTER_AGENTS = 0.5 # recon per guild level per game hour, with nobody scouting
|
||||||
INFORMANT_RECON = 15
|
INFORMANT_RECON = 30 # boosted recon per informant: doubled recon and XP for the scout who works it
|
||||||
|
|
||||||
|
|
||||||
def recon_gain(world: dict, action: dict) -> float:
|
def recon_gain(world: dict, action: dict) -> float:
|
||||||
|
|
@ -2503,9 +2509,13 @@ def favours(world: dict, now: float) -> dict:
|
||||||
"boost": round(city.get("site_boost", 0)),
|
"boost": round(city.get("site_boost", 0)),
|
||||||
},
|
},
|
||||||
"informant": {
|
"informant": {
|
||||||
"cost": INFORMANT_FAVOUR,
|
"cost": round(INFORMANT_FAVOUR * (1 + 0.5 * (raid or {}).get("informants", 0))),
|
||||||
"why": "No news to dig into" if not raid else
|
"why": "No news to dig into" if not raid else
|
||||||
"Nothing more to learn" if raid["recon"] >= intel_costs(raid)[-1] else None,
|
"Nothing more to learn" if raid["recon"] >= intel_costs(raid)[-1] else
|
||||||
|
"No scout in the guild to follow up the leads"
|
||||||
|
if not any(p["cls"] == "scout" for p in world["players"].values()) else None,
|
||||||
|
"recon": INFORMANT_RECON,
|
||||||
|
"boost": round(city.get("recon_boost", 0)),
|
||||||
},
|
},
|
||||||
"militia": {
|
"militia": {
|
||||||
"cost": RALLY_FAVOUR * (city.get("rallies", 0) + 1),
|
"cost": RALLY_FAVOUR * (city.get("rallies", 0) + 1),
|
||||||
|
|
@ -2534,8 +2544,11 @@ 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}: "
|
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"])
|
f"the next {MASTER_BUILDER_LABOUR} labour counts double", player["id"])
|
||||||
elif kind == "informant":
|
elif kind == "informant":
|
||||||
upcoming_raid(world)["recon"] += INFORMANT_RECON
|
raid = upcoming_raid(world)
|
||||||
log(world, now, "favour", f"{player['name']} paid an informant for news from the roads", player["id"])
|
raid["informants"] = raid.get("informants", 0) + 1
|
||||||
|
city["recon_boost"] = city.get("recon_boost", 0) + INFORMANT_RECON
|
||||||
|
log(world, now, "favour", f"{player['name']} paid an informant: the next {INFORMANT_RECON} "
|
||||||
|
f"reconnaissance counts double", player["id"])
|
||||||
else:
|
else:
|
||||||
city["rally"] = city.get("rally", 0) + RALLY_DEFENCE
|
city["rally"] = city.get("rally", 0) + RALLY_DEFENCE
|
||||||
city["rallies"] = city.get("rallies", 0) + 1
|
city["rallies"] = city.get("rallies", 0) + 1
|
||||||
|
|
@ -2697,5 +2710,5 @@ def content() -> dict:
|
||||||
"prayers": [{**prayer, "text": describe_prayer(prayer)} for prayer in PRAYERS], "max_prayers": MAX_PRAYERS,
|
"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()),
|
"potion_charges": POTION_CHARGES, "problems": PROBLEMS, "fittings": list(FITTING_BY_ID.values()),
|
||||||
"equip_slots": EQUIP_SLOTS, "eat_below": EAT_BELOW, "class_speed": CLASS_SPEED,
|
"equip_slots": EQUIP_SLOTS, "eat_below": EAT_BELOW, "class_speed": CLASS_SPEED,
|
||||||
"informant_recon": INFORMANT_RECON, "regen_per_hour": REGEN_PER_HOUR,
|
"regen_per_hour": REGEN_PER_HOUR,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -493,7 +493,10 @@ function actionCard(a) {
|
||||||
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.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 === "sabotage") extra += `<div class="small muted">Each win weakens the coming raid by 3%, up to 30%.</div>`;
|
||||||
}
|
}
|
||||||
if (a.special === "recon") extra = `<div class="small muted">+${+me.recon_gain[a.id].toFixed(2)} recon on the coming threat</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>`;
|
||||||
|
}
|
||||||
const temple = S.city.buildings.temple?.level || 0;
|
const temple = S.city.buildings.temple?.level || 0;
|
||||||
if (a.special === "labour") {
|
if (a.special === "labour") {
|
||||||
const boost = S.favours.builders.boost;
|
const boost = S.favours.builders.boost;
|
||||||
|
|
@ -792,8 +795,9 @@ function pips(level) {
|
||||||
function reconText(ev) {
|
function reconText(ev) {
|
||||||
const points = `${fmt(ev.recon)} / ${fmt(ev.recon_full)} points`;
|
const points = `${fmt(ev.recon)} / ${fmt(ev.recon_full)} points`;
|
||||||
if (ev.intel >= 3) return `${points} · nothing more to learn`;
|
if (ev.intel >= 3) return `${points} · nothing more to learn`;
|
||||||
if (ev.recon_next <= 0) return `${points} · enough for the next level, but they're too far off to read yet`;
|
const boost = S.favours.informant.boost ? ` · <b class="boosted">informant's leads: ×2 for the next ${fmt(S.favours.informant.boost)}</b>` : "";
|
||||||
return `${points} · ${fmt(ev.recon_next)} more for the next intel level`;
|
if (ev.recon_next <= 0) return `${points} · enough for the next level, but they're too far off to read yet${boost}`;
|
||||||
|
return `${points} · ${fmt(ev.recon_next)} more for the next intel level${boost}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function council() {
|
function council() {
|
||||||
|
|
@ -852,7 +856,7 @@ function favourSpends() {
|
||||||
};
|
};
|
||||||
return `<div class="panel"><h2>Spend favour on the city · you have ${FAVOUR} ${fmt(me.favour)}</h2>
|
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("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) + "." : `${C.informant_recon} reconnaissance on what's coming.`)}
|
${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("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.`)}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -573,17 +573,24 @@ class FavourTest(unittest.TestCase):
|
||||||
self.assertEqual(game.favours(world, T0)["builders"]["cost"], 150)
|
self.assertEqual(game.favours(world, T0)["builders"]["cost"], 150)
|
||||||
self.assertEqual(city["site_boost"], 50) # unused boost carries over
|
self.assertEqual(city["site_boost"], 50) # unused boost carries over
|
||||||
|
|
||||||
def test_informant_raises_intel(self):
|
def test_informant_doubles_a_scouts_recon_for_a_while(self):
|
||||||
world, p = self.world, self.p
|
world, p = self.world, self.p
|
||||||
self.assertEqual(game.spend_favour(world, p, "informant", T0), "No news to dig into")
|
self.assertEqual(game.spend_favour(world, p, "informant", T0), "No news to dig into")
|
||||||
world["next_event_at"] = T0
|
world["next_event_at"] = T0
|
||||||
game.tick(world, T0, random.Random(2))
|
game.tick(world, T0, random.Random(2))
|
||||||
event = game.upcoming_raid(world)
|
event = game.upcoming_raid(world)
|
||||||
while event["recon"] < game.intel_costs(event)[-1]:
|
self.assertEqual(game.spend_favour(world, p, "informant", T0), "No scout in the guild to follow up the leads")
|
||||||
|
world["players"]["sam"] = scout = game.new_player("sam", "Sam", "scout", T0)
|
||||||
self.assertIsNone(game.spend_favour(world, p, "informant", T0))
|
self.assertIsNone(game.spend_favour(world, p, "informant", T0))
|
||||||
self.assertEqual(event["recon"] % game.INFORMANT_RECON, 0)
|
self.assertEqual(event["recon"], 0) # leads, not intel
|
||||||
|
self.assertEqual(game.favours(world, T0)["informant"]["cost"], 375)
|
||||||
|
game.start_action(world, scout, "recon_roads", T0)
|
||||||
|
game.process_player(world, scout, T0 + 48 * 10 + 1, random.Random(1)) # ten scouting runs
|
||||||
|
self.assertEqual(event["recon"], 20)
|
||||||
|
self.assertEqual(scout["xp"]["reconnaissance"], 240)
|
||||||
|
self.assertEqual(world["city"]["recon_boost"], 20)
|
||||||
|
event["recon"] = game.intel_costs(event)[-1]
|
||||||
self.assertEqual(game.spend_favour(world, p, "informant", T0), "Nothing more to learn")
|
self.assertEqual(game.spend_favour(world, p, "informant", T0), "Nothing more to learn")
|
||||||
self.assertEqual(game.intel_level(world, event, T0), 1) # the rest waits for the raid to draw near
|
|
||||||
|
|
||||||
def test_rallies_cost_more_each_time_and_end_with_the_raid(self):
|
def test_rallies_cost_more_each_time_and_end_with_the_raid(self):
|
||||||
world, p = self.world, self.p
|
world, p = self.world, self.p
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue