The jailer works on prisoners in the torture chamber: 2.5 times the chance to defect

Jailers pick a prisoner nobody works on yet (their favourite work), stand next to it and
torment it; with a jailer about 28 of 40 prisoners defect instead of about 18. Test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-03 13:04:07 +01:00
1 parent b7d7fc2b52
commit 6e257e4034
3 files changed
+79 -2

No files matched your search

+1
View File
@@ -527,3 +527,4 @@ unit.gargoyle = Kettengargoyle
unit.owl = Arkane Eule
unit.wyrm = Glutwyrm
job.rest = Erholt sich im Horst
job.torment = Bearbeitet einen Gefangenen
+1
View File
@@ -528,3 +528,4 @@ unit.gargoyle = Chain Gargoyle
unit.owl = Arcane Owl
unit.wyrm = Ember Wyrm
job.rest = Recovering in the aerie
job.torment = Working on a prisoner
+77 -2
View File
@@ -170,6 +170,7 @@ pub enum Job {
HealCore, // flame priestess heals the soul core
TearBridge(usize), // knight tears down a bridge of the player
Rest(usize), // flying creature recovers in the aerie (flees when badly hurt)
Torment(u32), // the jailer works on a prisoner in the torture chamber
Help(usize), // imp with nothing else to do helps another imp at this tile
}
@@ -2541,6 +2542,21 @@ impl Game {
e.job = Job::None;
}
}
Job::Torment(id) => {
// working on a prisoner: makes it more likely to change sides (see captive_update)
let Some(k) = self.ents.iter().position(|o| o.id == id && o.alive && o.captive == 3) else {
e.job = Job::None;
return;
};
let px = self.ents[k].x;
e.face = sgn(px - e.x);
e.working = true;
e.xp += 2.0 * dt;
e.timer -= dt;
if e.timer <= 0.0 {
e.job = Job::None;
}
}
Job::Train(t) => {
// training is billed on payday; it stops when the bill would exceed the gold
if cur != t || self.map.t[t].kind != Kind::Training || self.training_bill + 1.0 > self.gold {
@@ -2822,6 +2838,19 @@ impl Game {
}
}
}
// the jailer's favourite work: prisoners nobody is working on yet
if e.kind == EK::Jailer && self.rng.f() < 0.85 {
let busy: Vec<u32> = self.ents.iter().filter_map(|o| if let Job::Torment(p) = o.job { Some(p) } else { None }).collect();
let prisoner = self.ents.iter().filter(|o| o.alive && o.captive == 3 && !busy.contains(&o.id)).map(|o| (o.id, o.tile())).next();
if let Some((id, pt)) = prisoner {
if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| i != pt && adj4(i, pt) || i == pt, true) {
e.path = p;
e.job = Job::Torment(id);
e.timer = 12.0 + self.rng.f() * 8.0;
return;
}
}
}
let r = self.rng.f();
if (e.kind == EK::Mage && r < 0.8) || r < 0.2 {
if let Some(p) = self.find_room_tile(cur, Kind::Library, me, false) {
@@ -2891,8 +2920,13 @@ impl Game {
e.torture_t += dt;
if e.torture_t >= 5.0 {
e.torture_t -= 5.0;
// ~40% defect over the whole torture (paladins ~25%)
let p = if e.kind == EK::Paladin { 0.0143 } else { 0.0252 };
// ~40% defect over the whole torture (paladins ~25%); a jailer working on
// the prisoner makes it 2.5 times as likely
let tormented = self.ents.iter().any(|o| o.alive && o.kind == EK::Jailer && o.job == Job::Torment(e.id) && o.path.is_empty() && (o.x - e.x).abs() < 1.6 && (o.y - e.y).abs() < 1.6);
let p = if e.kind == EK::Paladin { 0.0143 } else { 0.0252 } * if tormented { 2.5 } else { 1.0 };
if tormented {
self.fx(2, e.x, e.y, 0.0, 0.0, 0.3, 0xff3040ff);
}
if self.rng.f() < p {
self.defect(e);
return;
@@ -4066,6 +4100,7 @@ pub fn job_name(e: &Ent) -> &'static str {
Job::Attack(_) => "job.fight",
Job::TearBridge(_) => "job.tear_bridge",
Job::Rest(_) => "job.rest",
Job::Torment(_) => "job.torment",
Job::Help(_) => "job.help",
Job::AttackCore => {
if e.working {
@@ -4705,6 +4740,46 @@ mod tests {
assert!((8..=26).contains(&converted), "defect rate off: {}/40", converted);
}
#[test]
fn jailer_makes_prisoners_defect_more_often() {
let mut converted = 0;
let mut worked = 0;
for seed in 1..=40u32 {
let mut g = Game::new(seed, 64, 64);
g.gold = 5000.0;
let (cx, cy) = g.map.core;
g.build_rect(5, cx - 3, cy - 3, cx - 2, cy + 1);
g.build_rect(1, cx + 4, cy - 3, cx + 5, cy - 2); // a lair for the jailer
let jailer = g.spawn(EK::Jailer, false, (cx - 1) as f32 + 0.5, (cy - 3) as f32 + 0.5, 1);
let id = g.spawn(EK::Knight, true, (cx + 4) as f32 + 0.5, (cy + 4) as f32 + 0.5, 1);
let j = g.ents.iter().position(|e| e.id == id).unwrap();
g.kill(j);
for _ in 0..(200 * 20) {
g.update(0.05);
if let Some(jl) = g.ents.iter_mut().find(|e| e.id == jailer) {
// keep the jailer fresh so that it works
jl.hunger = 0.0;
jl.energy = 1.0;
jl.happy = 1.0;
if jl.job == Job::Torment(id) {
worked += 1;
}
}
match g.ents.iter().find(|e| e.id == id) {
Some(e) if !e.hero => {
converted += 1;
break;
}
None => break,
_ => {}
}
}
}
println!("with a jailer: {} of 40 defected (jailer worked {} ticks)", converted, worked);
assert!(worked > 0, "the jailer works on the prisoners");
assert!(converted >= 20, "defections with a jailer: {}/40", converted);
}
#[test]
fn temple_prayer_and_priestess_heals_core() {
let mut g = Game::new(9, 64, 64);