an http based roguelike rpg about jelqing
Commit 886159fd0fe7
tarball7 files changed+143-41
@@ -25,7 +25,7 @@ pub struct Gauges {
2525
pub(crate) cash: u32,2626
pub(crate) floor: u8,2727
pub(crate) observed: Option<Observer>,28
- pub(crate) mum_at_door: bool,28
+ pub(crate) mum_challenge: Option<String>,2929
pub(crate) warnings: Vec<&'static str>,3030
pub(crate) arrived: Option<Arrival>,3131
pub(crate) ruler: bool,@@ -81,7 +81,7 @@ impl Run {
8181
cash: self.standing.cash,8282
floor: self.floor.number(),8383
observed: self.observed,84
- mum_at_door: self.household.mum.is_some(),84
+ mum_challenge: self.mum_challenge(),8585
warnings,8686
arrived,8787
ruler: self.items.kit.ruler,@@ -1,6 +1,6 @@
11
use std::collections::HashMap;22
3
-use axum::http::{StatusCode, header};3
+use axum::http::StatusCode;44
55
use super::{66
Cause, Floor, Notice, Run,@@ -83,17 +83,56 @@ fn is_truth(spoken: &str) -> bool {
8383
.any(|truth| truth.eq_ignore_ascii_case(spoken))8484
}8585
86
-const BEDROOM_ERRANDS: [&str; 4] = [87
- "She has brought your washing.",88
- "She wants to know if you have eaten.",89
- "She needs the wifi password.",90
- "She would like to talk about your future.",86
+/// Why Mum came to the door, and how she leaves once she believes you.87
+#[derive(Clone, Copy)]88
+pub struct Errand {89
+ pub(crate) knock: &'static str,90
+ pub(crate) parting: &'static str,91
+}92
+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
+};101
+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
+ },91118
];92
-const CHRISTMAS_ERRANDS: [&str; 4] = [93
- "Nan would like to see you.",94
- "Christmas dinner is in ten minutes.",95
- "The King's speech is about to start.",96
- "An uncle has asked where you went.",119
+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
+ },97136
];98137
99138
#[derive(Default)]@@ -104,7 +143,7 @@ pub struct Household {
104143
}105144
106145
pub struct MumVisit {107
- pub(crate) errand: &'static str,146
+ pub(crate) errand: Errand,108147
pub(crate) nonce: String,109148
pub(crate) strikes: u8,110149
pub(crate) knocked: bool,@@ -159,9 +198,9 @@ impl Run {
159198
.collect()160199
}161200
162
- pub(super) fn errand(&mut self, summoned: bool) -> &'static str {201
+ pub(super) fn errand(&mut self, summoned: bool) -> Errand {163202
if summoned {164
- return "She heard a noise.";203
+ return HEARD_A_NOISE;165204
}166205
if self.online.filmed_at.is_some()167206
&& self.chance(@@ -169,7 +208,7 @@ impl Run {
169208
VIRAL_ERRAND,170209
)171210
{172
- return "A neighbour has sent her something.";211
+ return VIRAL;173212
}174213
let errands = if self.floor == Floor::Nans {175214
CHRISTMAS_ERRANDS@@ -177,33 +216,38 @@ impl Run {
177216
BEDROOM_ERRANDS178217
};179218
let weights = vec![1; errands.len()];219
+ let knocks = errands.map(|errand| errand.knock);180220
let i = self.oracle.pick(181221
self.decision("errand", "What has brought Mum to the door?"),182222
&weights,183
- &errands,223
+ &knocks,184224
);185225
errands[i]186226
}187227
188228
/// Mum answers every request on the run until she has been given something she believes.189229
pub fn answer_door(&mut self, authorization: Option<&str>) -> Result<(), Problem> {190
- let Some(visit) = &self.household.mum else {230
+ if self.household.mum.is_none() {191231
return Ok(());192
- };193
- let digest = self.floor == Floor::Nans;194
- let challenge = if digest {232
+ }233
+ match authorization {234
+ None => Err(self.knock()),235
+ Some(authorization) => self.hear(authorization, self.floor == Floor::Nans),236
+ }237
+ }238
+239
+ /// Sent on every response while she is at the door, so a client always holds the current nonce.240
+ pub fn mum_challenge(&self) -> Option<String> {241
+ let visit = self242
+ .household243
+ .mum244
+ .as_ref()245
+ .filter(|_| self.ended.is_none())?;246
+ Some(if self.floor == Floor::Nans {195247
format!("Digest realm=\"household\", nonce=\"{}\"", visit.nonce)196248
} else {197249
"Excuse realm=\"household\"".to_owned()198
- };199
- let result = match authorization {200
- None => Err(self.knock()),201
- Some(authorization) => self.hear(authorization, digest),202
- };203
- if self.ended.is_some() {204
- return result;205
- }206
- result.map_err(|problem| problem.header(header::WWW_AUTHENTICATE, &challenge))250
+ })207251
}208252
209253
/// The first knock is free. Every one after it is a strike.@@ -211,7 +255,7 @@ impl Run {
211255
let Some(visit) = &mut self.household.mum else {212256
unreachable!("knock is only called while Mum is at the door");213257
};214
- let errand = visit.errand;258
+ let errand = visit.errand.knock;215259
if !std::mem::replace(&mut visit.knocked, true) {216260
self.touch();217261
return Problem::new(@@ -302,10 +346,11 @@ impl Run {
302346
.as_ref()303347
.is_some_and(|visit| params.get("nonce") == Some(&visit.nonce));304348
if !current {305
- return Err(self.mum_strike(turned_away(349
+ self.touch();350
+ return Err(turned_away(306351
"stale-nonce",307
- "That was a different conversation.",308
- )));352
+ "That was a different conversation. Mum has started a new one, with a new nonce.",353
+ ));309354
}310355
Ok(params.remove("username").unwrap_or_default())311356
}@@ -332,9 +377,11 @@ impl Run {
332377
}333378
334379
fn leave_door(&mut self) {335
- self.household.mum = None;380
+ if let Some(visit) = self.household.mum.take() {381
+ self.notices382
+ .push(Notice::MumSatisfied(visit.errand.parting));383
+ }336384
self.standing.suspicion = 0;337
- self.notices.push(Notice::MumSatisfied);338385
self.touch();339386
}340387
}@@ -37,6 +37,8 @@ pub use clinic::Clinic;
3737
pub use floor::{Floor, FloorState, Observer};3838
pub use forum::{Branch, ELDER_LAST_MODIFIED, Elder, FORUM_THREAD, Online, Story, forum_thread};3939
pub use gauges::{Gauges, Shown};40
+#[cfg(test)]41
+pub use household::HEARD_A_NOISE;4042
pub use household::{Excuse, Household, MumVisit, vocabulary as excuse_vocabulary};4143
pub use inventory::Items;4244
pub use items::{Item, SHOP};@@ -13,7 +13,7 @@ pub enum Notice {
1313
DrawerComesLoose,1414
ThreadHasReplies,1515
Reflection,16
- MumSatisfied,16
+ MumSatisfied(&'static str),1717
ColdStart,1818
RunningLong(Floor),1919
LastTurn,@@ -34,7 +34,7 @@ impl Notice {
3434
Self::DrawerComesLoose => "The drawer comes loose. That took a while.",3535
Self::ThreadHasReplies => "Somewhere, a forum thread from 2009 still has replies.",3636
Self::Reflection => "the count reached ten, and then the reflection started",37
- Self::MumSatisfied => "Mum seems satisfied. She takes the washing with her.",37
+ Self::MumSatisfied(parting) => parting,3838
Self::ColdStart => "Cold start. The server has noted this.",3939
Self::RunningLong(floor) => match floor {4040
Floor::Bedroom => "Mum is coming up the stairs.",@@ -162,9 +162,10 @@ fn stats(gauges: &Gauges, shame: u32) -> Vec<(HeaderName, String)> {
162162
if let Some(observer) = gauges.observed {163163
stats.push((X_OBSERVED_BY, observer.name().into()));164164
}165
- if gauges.mum_at_door {165
+ if let Some(challenge) = &gauges.mum_challenge {166166
stats.push((header::VIA, "1.1 mum".into()));167167
stats.push((header::VARY, "Mum".into()));168
+ stats.push((header::WWW_AUTHENTICATE, challenge.clone()));168169
}169170
if let Some(arrived) = &gauges.arrived {170171
let mut gains = format!("length={:.3}, girth={:.3}", arrived.length, arrived.girth);@@ -393,3 +393,55 @@ async fn an_excuse_without_a_scheme_is_explained_for_free() {
393393
.await;394394
assert_eq!(excused.status(), StatusCode::OK);395395
}396
+397
+#[tokio::test]398
+async fn mum_leaves_the_way_her_errand_suggests() {399
+ let app = seeded_with(Floor::Bedroom, mum_at_the_door);400
+ let pockets = send(&app, "GET", "/runs/cafebabe/inventory", &[], "").await;401
+ assert_eq!(402
+ header(&pockets, "www-authenticate"),403
+ "Excuse realm=\"household\""404
+ );405
+ let excused = send(406
+ &app,407
+ "GET",408
+ "/runs/cafebabe",409
+ &[("authorization", "Excuse \"homework\"")],410
+ "",411
+ )412
+ .await;413
+ assert!(warnings(&excused).iter().any(|w| w.contains("the pipes")));414
+ assert!(excused.headers().get("www-authenticate").is_none());415
+}416
+417
+#[tokio::test]418
+async fn a_stale_nonce_is_explained_for_free() {419
+ let app = seeded_with(Floor::Nans, mum_at_the_door);420
+ for _ in 0..4 {421
+ let stale = send(422
+ &app,423
+ "GET",424
+ "/runs/cafebabe",425
+ &[(426
+ "authorization",427
+ "Digest username=\"helping Nan\", nonce=\"old\"",428
+ )],429
+ "",430
+ )431
+ .await;432
+ assert_eq!(stale.status(), StatusCode::UNAUTHORIZED);433
+ assert!(header(&stale, "www-authenticate").contains("nonce=\"abc\""));434
+ }435
+ let current = send(436
+ &app,437
+ "GET",438
+ "/runs/cafebabe",439
+ &[(440
+ "authorization",441
+ "Digest username=\"helping Nan\", nonce=\"abc\"",442
+ )],443
+ "",444
+ )445
+ .await;446
+ assert_eq!(current.status(), StatusCode::OK);447
+}@@ -108,7 +108,7 @@ pub fn app() -> Router {
108108
109109
pub fn mum_at_the_door(run: &mut Run) {110110
run.household.mum = Some(MumVisit {111
- errand: "She heard a noise.",111
+ errand: crate::game::HEARD_A_NOISE,112112
nonce: "abc".into(),113113
strikes: 0,114114
knocked: false,