atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0

Commit 2176d9a94bd9

tarball

atagen <boss@atagen.co>2026-10-14 11:12 UTC

4 files changed+98-41
game: announce Mum at the door and keep her away on the turn she left

Dismissing Mum in answer_door ran before the turn's settle, so the same request could roll a fresh summon and send her straight back. Her arrival also had no telling, so the errand only surfaced on the next knock. Each errand is now one knock/parting pair so the arrival Warning and the parting always match.
Modified file: src/game/household.rs+52 additions−40 deletions
@@ -86,53 +86,61 @@ fn is_truth(spoken: &str) -> bool {
8686 /// Why Mum came to the door, and how she leaves once she believes you.
8787 #[derive(Clone, Copy)]
8888 pub struct Errand {
89+ pub(crate) arrival: &'static str,
8990 pub(crate) knock: &'static str,
9091 pub(crate) parting: &'static str,
9192 }
9293
93-pub const HEARD_A_NOISE: Errand = Errand {
94- knock: "She heard a noise.",
95- parting: "Mum seems satisfied. It must have been the pipes.",
96-};
97-const VIRAL: Errand = Errand {
98- knock: "A neighbour has sent her something.",
99- parting: "Mum seems satisfied. She has decided not to open the attachment.",
100-};
94+macro_rules! errand {
95+ ($knock:literal, $parting:literal) => {
96+ Errand {
97+ arrival: concat!("Mum is at the door. ", $knock),
98+ knock: $knock,
99+ parting: concat!("Mum seems satisfied. ", $parting),
100+ }
101+ };
102+}
103+
104+pub const HEARD_A_NOISE: Errand = errand!("She heard a noise.", "It must have been the pipes.");
105+const VIRAL: Errand = errand!(
106+ "A neighbour has sent her something.",
107+ "She has decided not to open the attachment."
108+);
101109 const BEDROOM_ERRANDS: [Errand; 4] = [
102- Errand {
103- knock: "She has brought your washing.",
104- parting: "Mum seems satisfied. She leaves the washing on the chair.",
105- },
106- Errand {
107- knock: "She wants to know if you have eaten.",
108- parting: "Mum seems satisfied. There will be a plate in the oven.",
109- },
110- Errand {
111- knock: "She needs the wifi password.",
112- parting: "Mum seems satisfied. She has written the wifi password on her hand.",
113- },
114- Errand {
115- knock: "She would like to talk about your future.",
116- parting: "Mum seems satisfied. The future can wait until dinner.",
117- },
110+ errand!(
111+ "She has brought your washing.",
112+ "She leaves the washing on the chair."
113+ ),
114+ errand!(
115+ "She wants to know if you have eaten.",
116+ "There will be a plate in the oven."
117+ ),
118+ errand!(
119+ "She wants the wifi password.",
120+ "She has written the wifi password on her hand."
121+ ),
122+ errand!(
123+ "She would like to talk about your future.",
124+ "The future can wait until dinner."
125+ ),
118126 ];
119127 const CHRISTMAS_ERRANDS: [Errand; 4] = [
120- Errand {
121- knock: "Nan would like to see you.",
122- parting: "Mum seems satisfied. She tells Nan you are on your way.",
123- },
124- Errand {
125- knock: "Christmas dinner is in ten minutes.",
126- parting: "Mum seems satisfied. She will save you a pig in a blanket.",
127- },
128- Errand {
129- knock: "The King's speech is about to start.",
130- parting: "Mum seems satisfied. She will tell you what the King said.",
131- },
132- Errand {
133- knock: "An uncle has asked where you went.",
134- parting: "Mum seems satisfied. She tells the uncle you are helping in the kitchen.",
135- },
128+ errand!(
129+ "Nan would like to see you.",
130+ "She tells Nan you are on your way."
131+ ),
132+ errand!(
133+ "Christmas dinner is in ten minutes.",
134+ "She will save you a pig in a blanket."
135+ ),
136+ errand!(
137+ "The King's speech is about to start.",
138+ "She will tell you what the King said."
139+ ),
140+ errand!(
141+ "An uncle has asked where you went.",
142+ "She tells the uncle you are helping in the kitchen."
143+ ),
136144 ];
137145
138146 #[derive(Default)]
@@ -140,6 +148,8 @@ pub struct Household {
140148 pub(crate) mum: Option<MumVisit>,
141149 /// Every excuse Mum has accepted this run. Digest Mum remembers them all.
142150 pub(crate) heard: Vec<Excuse>,
151+ /// Set when the request now being answered sent Mum away, so the same turn cannot fetch her back.
152+ pub(crate) just_left: bool,
143153 }
144154
145155 pub struct MumVisit {
@@ -227,6 +237,7 @@ impl Run {
227237
228238 /// Mum answers every request on the run until she has been given something she believes.
229239 pub fn answer_door(&mut self, authorization: Option<&str>) -> Result<(), Problem> {
240+ self.household.just_left = false;
230241 if self.household.mum.is_none() {
231242 return Ok(());
232243 }
@@ -380,6 +391,7 @@ impl Run {
380391 if let Some(visit) = self.household.mum.take() {
381392 self.notices
382393 .push(Notice::MumSatisfied(visit.errand.parting));
394+ self.household.just_left = true;
383395 }
384396 self.standing.suspicion = 0;
385397 self.touch();
Modified file: src/game/notice.rs+2 additions
@@ -13,6 +13,7 @@ pub enum Notice {
1313 DrawerComesLoose,
1414 ThreadHasReplies,
1515 Reflection,
16+ MumAtTheDoor(&'static str),
1617 MumSatisfied(&'static str),
1718 ColdStart,
1819 RunningLong(Floor),
@@ -34,6 +35,7 @@ impl Notice {
3435 Self::DrawerComesLoose => "The drawer comes loose. That took a while.",
3536 Self::ThreadHasReplies => "Somewhere, a forum thread from 2009 still has replies.",
3637 Self::Reflection => "the count reached ten, and then the reflection started",
38+ Self::MumAtTheDoor(arrival) => arrival,
3739 Self::MumSatisfied(parting) => parting,
3840 Self::ColdStart => "Cold start. The server has noted this.",
3941 Self::RunningLong(floor) => match floor {
Modified file: src/game/progress.rs+2 additions−1 deletions
@@ -171,7 +171,7 @@ impl Run {
171171
172172 pub(super) fn maybe_summon_mum(&mut self) {
173173 let was_below = self.standing.at_turn_start < SUSPICION_LIMIT;
174- if !self.floor.has_mum() || self.household.mum.is_some() {
174+ if !self.floor.has_mum() || self.household.mum.is_some() || self.household.just_left {
175175 return;
176176 }
177177 let summoned = was_below && self.standing.suspicion == SUSPICION_LIMIT;
@@ -190,6 +190,7 @@ impl Run {
190190 )
191191 {
192192 let errand = self.errand(summoned);
193+ self.notices.push(Notice::MumAtTheDoor(errand.arrival));
193194 let nonce = self.fresh_nonce();
194195 self.household.mum = Some(MumVisit {
195196 errand,
Modified file: src/tests/household.rs+42 additions
@@ -446,3 +446,45 @@ async fn a_stale_nonce_is_explained_for_free() {
446446 .await;
447447 assert_eq!(current.status(), StatusCode::OK);
448448 }
449+
450+#[tokio::test]
451+async fn mum_announces_herself_but_never_returns_on_the_turn_she_left() {
452+ let mut arrivals = 0;
453+ for seed in 0..200u64 {
454+ let app = app_with(Floor::Bedroom, seed, |run| {
455+ mum_at_the_door(run);
456+ run.body.eq = 60;
457+ run.here.brother_told = true;
458+ run.standing.suspicion = 4;
459+ });
460+ let excused = send(
461+ &app,
462+ "JELQ",
463+ "/runs/cafebabe?technique=stretch&intensity=10",
464+ &[("authorization", "Excuse \"homework\"")],
465+ "",
466+ )
467+ .await;
468+ assert!(
469+ excused.headers().get("www-authenticate").is_none(),
470+ "seed {seed} fetched Mum straight back"
471+ );
472+ let again = send(
473+ &app,
474+ "JELQ",
475+ "/runs/cafebabe?technique=stretch&intensity=10",
476+ &[],
477+ "",
478+ )
479+ .await;
480+ if again.headers().get("www-authenticate").is_some() {
481+ arrivals += 1;
482+ assert!(
483+ warnings(&again)
484+ .iter()
485+ .any(|w| w.contains("Mum is at the door. She"))
486+ );
487+ }
488+ }
489+ assert!(arrivals > 0, "Mum never came back at all");
490+}