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:
rope 2026-10-19 15:09:51 +01:00
parent 366a195c6d
commit 27bdb6a154
5 changed files with 113 additions and 33 deletions

View file

@ -48,9 +48,12 @@ Alpha. One season, bandit raids plus two friendly events, six buildings.
time: faster gathering, a chance to keep materials or double ore, more time: faster gathering, a chance to keep materials or double ore, more
prayer from bones, or combat power, armour and healing. An empty potion is prayer from bones, or combat power, armour and healing. An empty potion is
replaced from the bank automatically. replaced from the bank automatically.
- **Events:** raids are announced a day or two ahead. Intel (spymaster's - **Events:** raids are announced a day or two ahead. Reconnaissance from
guild + scout reconnaissance) narrows down when, how strong, and finally scouts, informants and the spymaster's guild narrows down when, how strong,
where their camp is, so fighters can sabotage it. Defeat kills or drives and finally where their camp is, so fighters can sabotage it. 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
off people, loots the stockpile and treasury, and may damage a building. off people, loots the stockpile and treasury, and may damage a building.
Damaged buildings stop working until repaired but keep their level. Damaged buildings stop working until repaired but keep their level.
Every fight uses up arrows and breaks some of the swords and shields Every fight uses up arrows and breaks some of the swords and shields

2
app.py
View file

@ -116,6 +116,8 @@ def player_view(world: dict, player: dict, now: float) -> dict:
view["action"] = {**player["action"], "duration": game.action_time(world, player, action)} 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["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["blocked"] = {action["id"]: game.can_start(world, player, action) for action in game.ACTIONS}
view["recon_gain"] = {action["id"]: game.recon_gain(world, action) for action in game.ACTIONS
if action.get("special") == "recon"}
return view return view

67
game.py
View file

@ -20,7 +20,6 @@ BASE_STACK = 1000
CLASS_SPEED = 0.8 CLASS_SPEED = 0.8
LUCKY_CHANCE = 0.1 LUCKY_CHANCE = 0.1
OVERFLOW_FAVOUR = 0.5 # favour for items a full bank spills to the city, vs. giving on purpose 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 MAX_LOG = 200
REGEN_PER_HOUR = 0.25 # share of max HP recovered per game hour 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 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, "name": "Spymaster's guild", "icon": "🕵️", "max": 3, "gold": 400,
"base": {"logs": 500, "bricks": 300, "hide": 100, "feathers": 300}, "advanced": {"cut_stone": 150}, "base": {"logs": 500, "bricks": 300, "hide": 100, "feathers": 300}, "advanced": {"cut_stone": 150},
"late": {"bear_leather": 40, "oak_plank": 40}, "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": { "storehouse": {
"name": "Storehouse", "icon": "📦", "max": 5, "gold": 200, "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 summary["losses"] += 1
xp = xp // 3 xp = xp // 3
if action.get("special") == "recon": if action.get("special") == "recon":
event = upcoming_event(world) event = upcoming_raid(world)
if event: 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": 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":
@ -1572,6 +1571,9 @@ def _city_hour(world: dict, now: float) -> None:
pop = city["population"] pop = city["population"]
if city["construction"]: if city["construction"]:
city["construction"]["labour"] += pop * TOWNSFOLK_LABOUR 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) 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)) * (0.9 if fitted(world, "grain_bins") else 1))
stock = city["stockpile"] stock = city["stockpile"]
@ -1663,8 +1665,45 @@ def _announce(world: dict, now: float, rng: random.Random) -> None:
del world["events"][:-30] del world["events"][:-30]
def intel_level(world: dict, event: dict) -> int: # Intel: each level takes more reconnaissance than the last, a bigger band is
return min(3, building_level(world, "spymaster") + event["recon"] // RECON_PER_INTEL) # 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")] 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: def event_view(world: dict, event: dict, now: float) -> dict:
"""What the city knows about an upcoming event, filtered by intel.""" """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) half = WINDOW_HOURS[level] * HOUR / speed(world)
costs = intel_costs(event)
centre = event["fire_at"] + event["jitter"] * half * 0.6 centre = event["fire_at"] + event["jitter"] * half * 0.6
view = { view = {
"id": event["id"], "kind": event["kind"], "intel": level, "id": event["id"], "kind": event["kind"], "intel": level,
"earliest": max(now, centre - half), "latest": max(now, centre + half), "earliest": max(now, centre - half), "latest": max(now, centre + half),
"exact": event["fire_at"] if level >= 3 else None, "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": if event["kind"] == "dragon":
strength = event["strength"] 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.""" """What favour can buy for the city right now: cost, and why not if it can't."""
city = world["city"] city = world["city"]
work = city["construction"] work = city["construction"]
event = upcoming_event(world)
raid = upcoming_raid(world) raid = upcoming_raid(world)
return { return {
"builders": { "builders": {
@ -2464,8 +2504,8 @@ def favours(world: dict, now: float) -> dict:
}, },
"informant": { "informant": {
"cost": INFORMANT_FAVOUR, "cost": INFORMANT_FAVOUR,
"why": "No news to dig into" if not event else "why": "No news to dig into" if not raid else
"Nothing more to learn" if intel_level(world, event) >= 3 else None, "Nothing more to learn" if raid["recon"] >= intel_costs(raid)[-1] else None,
}, },
"militia": { "militia": {
"cost": RALLY_FAVOUR * (city.get("rallies", 0) + 1), "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}: " 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_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"]) log(world, now, "favour", f"{player['name']} paid an informant for news from the roads", player["id"])
else: else:
city["rally"] = city.get("rally", 0) + RALLY_DEFENCE 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}, "city_uses": {item: city_uses(item) for item in ITEMS},
"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, "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,
} }

View file

@ -344,7 +344,7 @@ function skillPage(id) {
: `<div class="panel muted">Nothing is being built right now. Fund a building on the Town square and its site opens here.</div>`; : `<div class="panel muted">Nothing is being built right now. Fund a building on the Town square and its site opens here.</div>`;
html += potionPanel(id); html += potionPanel(id);
if (`tool_${id}` in me.equip) html += toolPanel(id); if (`tool_${id}` in me.equip) html += toolPanel(id);
if (id === "reconnaissance") html += `<div class="panel small muted">Every ${8} reconnaissance points on an upcoming event raises its intel by one level, on top of what the spymaster's guild provides. The council & threats page shows the progress.</div>`; if (id === "reconnaissance") html += `<div class="panel small muted">Reconnaissance on a coming threat raises its intel. Each level takes more than the last, a bigger band takes longer to read, and the sharpest news (the exact day and their camp) only comes in the last hours before they strike. The spymaster's guild adds to every point and gathers some on its own. The council & threats page shows the progress.</div>`;
html += skillHappenings(id); html += skillHappenings(id);
const live = new Set(S.happenings.problems.map((p) => p.kind)); const live = new Set(S.happenings.problems.map((p) => p.kind));
const actions = C.actions.filter((a) => a.skill === id && (a.special !== "problem" || live.has(a.problem))); const actions = C.actions.filter((a) => a.skill === id && (a.special !== "problem" || live.has(a.problem)));
@ -493,7 +493,7 @@ 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">+${a.recon} recon on the next event</div>`; if (a.special === "recon") extra = `<div class="small muted">+${+me.recon_gain[a.id].toFixed(2)} recon on the coming threat</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;
@ -789,6 +789,13 @@ function pips(level) {
return `<span class="pips" title="Intel level ${level} of 3">${[1, 2, 3].map((i) => `<i class="${i <= level ? "on" : ""}"></i>`).join("")}</span>`; return `<span class="pips" title="Intel level ${level} of 3">${[1, 2, 3].map((i) => `<i class="${i <= level ? "on" : ""}"></i>`).join("")}</span>`;
} }
function reconText(ev) {
const points = `${fmt(ev.recon)} / ${fmt(ev.recon_full)} points`;
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`;
return `${points} · ${fmt(ev.recon_next)} more for the next intel level`;
}
function council() { function council() {
const ev = S.upcoming, city = S.city, me = S.me; const ev = S.upcoming, city = S.city, me = S.me;
let html = ""; let html = "";
@ -804,9 +811,9 @@ function council() {
${ev.range ? `<span class="k">Strength</span><span>${ev.strength ?? `${ev.range[0]}–${ev.range[1]}`}</span>` : ""} ${ev.range ? `<span class="k">Strength</span><span>${ev.strength ?? `${ev.range[0]}–${ev.range[1]}`}</span>` : ""}
${ev.trait ? `<span class="k">Scouts say</span><span>${esc(ev.trait)}</span>` : ""} ${ev.trait ? `<span class="k">Scouts say</span><span>${esc(ev.trait)}</span>` : ""}
${ev.camp ? `<span class="k">Their camp</span><span>Located. Fighters can sabotage it from the combat page${ev.sabotage ? ` (weakened ${ev.sabotage}% so far)` : ""}.</span>` : ""} ${ev.camp ? `<span class="k">Their camp</span><span>Located. Fighters can sabotage it from the combat page${ev.sabotage ? ` (weakened ${ev.sabotage}% so far)` : ""}.</span>` : ""}
${me.cls === "scout" || ev.recon ? `<span class="k">Reconnaissance</span><span>${ev.intel < 3 ? `${ev.recon} points · ${ev.recon_next} more for the next intel level` : `${ev.recon} points · nothing more to learn`}</span>` : ""} ${me.cls === "scout" || ev.recon ? `<span class="k">Reconnaissance</span><span>${reconText(ev)}</span>` : ""}
</div> </div>
${ev.intel < 3 ? `<p class="small muted">Better intel comes from the spymaster's guild and scouts' reconnaissance.</p>` : ""}`; ${ev.intel < 3 ? `<p class="small muted">Better intel comes from the spymaster's guild and scouts' reconnaissance, and the sharpest only as the threat draws near.</p>` : ""}`;
if (threat) { if (threat) {
const strength = ev.strength ?? (ev.range ? `${ev.range[0]}–${ev.range[1]}` : "?"); const strength = ev.strength ?? (ev.range ? `${ev.range[0]}–${ev.range[1]}` : "?");
html += `<div class="vs"><div class="side"><span class="muted small">Our defence</span><b>${fmt(city.defence.total)}</b></div><span class="muted">vs</span><div class="side"><span class="muted small">Raiders</span><b>${strength}</b></div></div>`; html += `<div class="vs"><div class="side"><span class="muted small">Our defence</span><b>${fmt(city.defence.total)}</b></div><span class="muted">vs</span><div class="side"><span class="muted small">Raiders</span><b>${strength}</b></div></div>`;
@ -845,7 +852,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) + "." : "One more level of intel on what's coming.")} ${option("informant", "🕵️", "Pay an informant", f.informant.why ? esc(f.informant.why) + "." : `${C.informant_recon} reconnaissance on what's coming.`)}
${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>`;
} }

View file

@ -343,21 +343,45 @@ class EventTest(unittest.TestCase):
self.assertLessEqual(view["earliest"], raid["fire_at"]) self.assertLessEqual(view["earliest"], raid["fire_at"])
self.assertGreaterEqual(view["latest"], raid["fire_at"]) self.assertGreaterEqual(view["latest"], raid["fire_at"])
def test_spymaster_and_recon_sharpen_intel(self): def test_recon_sharpens_intel_as_the_raid_draws_near(self):
world = world_with(("sam", "scout")) world = world_with(("sam", "scout"))
raid = self.announce(world) raid = self.announce(world)
world["city"]["buildings"]["spymaster"]["level"] = 1
self.assertIn("size", game.event_view(world, raid, T0))
p = world["players"]["sam"] p = world["players"]["sam"]
p["xp"]["reconnaissance"] = game.XP_TABLE[20] p["xp"]["reconnaissance"] = game.XP_TABLE[20]
game.start_action(world, p, "recon_camps", T0) game.start_action(world, p, "recon_camps", T0)
game.process_player(world, p, T0 + 8 * 72, random.Random(1)) game.process_player(world, p, T0 + 4 * game.HOUR, random.Random(1))
view = game.event_view(world, raid, T0) self.assertGreaterEqual(raid["recon"], game.intel_costs(raid)[-1])
# Plenty of scouting, but a day or more out only the first level is knowable.
view = game.event_view(world, raid, T0 + 4 * game.HOUR)
self.assertEqual(view["intel"], 1)
self.assertIn("size", view)
self.assertNotIn("strength", view)
self.assertEqual(view["recon_next"], 0)
late = raid["fire_at"] - 12 * game.HOUR
self.assertEqual(game.intel_level(world, raid, late), 2)
view = game.event_view(world, raid, raid["fire_at"] - game.HOUR)
self.assertEqual(view["intel"], 3) self.assertEqual(view["intel"], 3)
self.assertEqual(view["exact"], raid["fire_at"]) self.assertEqual(view["exact"], raid["fire_at"])
self.assertIn("strength", view)
self.assertTrue(view["camp"]) self.assertTrue(view["camp"])
def test_bigger_bands_take_more_scouting(self):
small = {"kind": "raid", "strength": 20}
huge = {"kind": "raid", "strength": 150}
dragon = {"kind": "dragon", "strength": 20}
self.assertEqual(game.intel_costs(small), [15, 45, 90])
self.assertEqual(game.intel_costs(huge), [30, 90, 180])
self.assertEqual(game.intel_costs(dragon), [8, 22, 45])
def test_the_spymasters_guild_boosts_and_gathers_recon(self):
world = world_with()
raid = self.announce(world)
action = game.ACTION_BY_ID["recon_roads"]
self.assertEqual(game.recon_gain(world, action), 1)
world["city"]["buildings"]["spymaster"]["level"] = 2
self.assertEqual(game.recon_gain(world, action), 1.5)
game.tick(world, T0 + 2 * game.HOUR, random.Random(2))
self.assertAlmostEqual(raid["recon"], 2.0)
def test_victory_rewards_garrison(self): def test_victory_rewards_garrison(self):
world = world_with(("wil", "warrior")) world = world_with(("wil", "warrior"))
raid = self.announce(world) raid = self.announce(world)
@ -404,9 +428,11 @@ class EventTest(unittest.TestCase):
p["hp"] = game.max_hp(p) p["hp"] = game.max_hp(p)
raid = self.announce(world) raid = self.announce(world)
self.assertEqual(game.start_action(world, p, "sabotage", T0), "No bandit camp has been located") self.assertEqual(game.start_action(world, p, "sabotage", T0), "No bandit camp has been located")
raid["recon"] = 3 * game.RECON_PER_INTEL raid["recon"] = game.intel_costs(raid)[-1]
self.assertIsNone(game.start_action(world, p, "sabotage", T0)) now = raid["fire_at"] - game.HOUR
game.process_player(world, p, T0 + 96 * 20, random.Random(1)) world["city"]["last_tick"] = p["last_active"] = now
self.assertIsNone(game.start_action(world, p, "sabotage", now))
game.process_player(world, p, now + 96 * 20, random.Random(1))
self.assertGreater(raid["sabotage"], 0) self.assertGreater(raid["sabotage"], 0)
self.assertLessEqual(raid["sabotage"], 0.3) self.assertLessEqual(raid["sabotage"], 0.3)
@ -552,11 +578,12 @@ class FavourTest(unittest.TestCase):
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_event(world) event = game.upcoming_raid(world)
for level in (1, 2, 3): while event["recon"] < game.intel_costs(event)[-1]:
self.assertIsNone(game.spend_favour(world, p, "informant", T0)) self.assertIsNone(game.spend_favour(world, p, "informant", T0))
self.assertEqual(game.intel_level(world, event), level) self.assertEqual(event["recon"] % game.INFORMANT_RECON, 0)
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