atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
e2727527c1512040a2e2b3557386595e2af08ff5
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git

Blame: src/game/household.rs

782069860236atagen2026-10-10 12:44 UTC1
use std::collections::HashMap;
782069860236atagen2026-10-10 12:44 UTC2
782069860236atagen2026-10-10 12:44 UTC3
use axum::http::{StatusCode, header};
782069860236atagen2026-10-10 12:44 UTC4
782069860236atagen2026-10-10 12:44 UTC5
use super::{
8b8e83329a1datagen2026-10-12 12:01 UTC6
    Cause, Floor, Notice, Run,
782069860236atagen2026-10-10 12:44 UTC7
    balance::{PATIENCE, VIRAL_ERRAND},
8b8e83329a1datagen2026-10-12 12:01 UTC8
    credentials::{Scheme, parse_credentials},
782069860236atagen2026-10-10 12:44 UTC9
};
782069860236atagen2026-10-10 12:44 UTC10
use crate::problem::Problem;
782069860236atagen2026-10-10 12:44 UTC11
8b8e83329a1datagen2026-10-12 12:01 UTC12
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
8b8e83329a1datagen2026-10-12 12:01 UTC13
pub enum Excuse {
8b8e83329a1datagen2026-10-12 12:01 UTC14
    Changing,
8b8e83329a1datagen2026-10-12 12:01 UTC15
    Homework,
8b8e83329a1datagen2026-10-12 12:01 UTC16
    PulledAMuscle,
8b8e83329a1datagen2026-10-12 12:01 UTC17
    Tidying,
8b8e83329a1datagen2026-10-12 12:01 UTC18
    TheWashingUp,
8b8e83329a1datagen2026-10-12 12:01 UTC19
    FedTheCat,
8b8e83329a1datagen2026-10-12 12:01 UTC20
    Revising,
8b8e83329a1datagen2026-10-12 12:01 UTC21
    OnACall,
8b8e83329a1datagen2026-10-12 12:01 UTC22
    WrappingPresents,
8b8e83329a1datagen2026-10-12 12:01 UTC23
    FoldingTheWashing,
8b8e83329a1datagen2026-10-12 12:01 UTC24
    HangingTheLights,
8b8e83329a1datagen2026-10-12 12:01 UTC25
    HelpingNan,
8b8e83329a1datagen2026-10-12 12:01 UTC26
    PeelingSprouts,
8b8e83329a1datagen2026-10-12 12:01 UTC27
}
8b8e83329a1datagen2026-10-12 12:01 UTC28
8b8e83329a1datagen2026-10-12 12:01 UTC29
impl Excuse {
8b8e83329a1datagen2026-10-12 12:01 UTC30
    const EVERYDAY: [Self; 7] = [
8b8e83329a1datagen2026-10-12 12:01 UTC31
        Self::Changing,
8b8e83329a1datagen2026-10-12 12:01 UTC32
        Self::Homework,
8b8e83329a1datagen2026-10-12 12:01 UTC33
        Self::PulledAMuscle,
8b8e83329a1datagen2026-10-12 12:01 UTC34
        Self::Tidying,
8b8e83329a1datagen2026-10-12 12:01 UTC35
        Self::TheWashingUp,
8b8e83329a1datagen2026-10-12 12:01 UTC36
        Self::FedTheCat,
8b8e83329a1datagen2026-10-12 12:01 UTC37
        Self::Revising,
8b8e83329a1datagen2026-10-12 12:01 UTC38
    ];
8b8e83329a1datagen2026-10-12 12:01 UTC39
    const CHRISTMAS: [Self; 6] = [
8b8e83329a1datagen2026-10-12 12:01 UTC40
        Self::OnACall,
8b8e83329a1datagen2026-10-12 12:01 UTC41
        Self::WrappingPresents,
8b8e83329a1datagen2026-10-12 12:01 UTC42
        Self::FoldingTheWashing,
8b8e83329a1datagen2026-10-12 12:01 UTC43
        Self::HangingTheLights,
8b8e83329a1datagen2026-10-12 12:01 UTC44
        Self::HelpingNan,
8b8e83329a1datagen2026-10-12 12:01 UTC45
        Self::PeelingSprouts,
8b8e83329a1datagen2026-10-12 12:01 UTC46
    ];
8b8e83329a1datagen2026-10-12 12:01 UTC47
8b8e83329a1datagen2026-10-12 12:01 UTC48
    pub fn phrase(self) -> &'static str {
8b8e83329a1datagen2026-10-12 12:01 UTC49
        match self {
8b8e83329a1datagen2026-10-12 12:01 UTC50
            Self::Changing => "changing",
8b8e83329a1datagen2026-10-12 12:01 UTC51
            Self::Homework => "homework",
8b8e83329a1datagen2026-10-12 12:01 UTC52
            Self::PulledAMuscle => "pulled a muscle",
8b8e83329a1datagen2026-10-12 12:01 UTC53
            Self::Tidying => "tidying",
8b8e83329a1datagen2026-10-12 12:01 UTC54
            Self::TheWashingUp => "the washing up",
8b8e83329a1datagen2026-10-12 12:01 UTC55
            Self::FedTheCat => "fed the cat",
8b8e83329a1datagen2026-10-12 12:01 UTC56
            Self::Revising => "revising",
8b8e83329a1datagen2026-10-12 12:01 UTC57
            Self::OnACall => "on a call",
8b8e83329a1datagen2026-10-12 12:01 UTC58
            Self::WrappingPresents => "wrapping presents",
8b8e83329a1datagen2026-10-12 12:01 UTC59
            Self::FoldingTheWashing => "folding the washing",
8b8e83329a1datagen2026-10-12 12:01 UTC60
            Self::HangingTheLights => "hanging the lights",
8b8e83329a1datagen2026-10-12 12:01 UTC61
            Self::HelpingNan => "helping Nan",
8b8e83329a1datagen2026-10-12 12:01 UTC62
            Self::PeelingSprouts => "peeling sprouts",
8b8e83329a1datagen2026-10-12 12:01 UTC63
        }
8b8e83329a1datagen2026-10-12 12:01 UTC64
    }
8b8e83329a1datagen2026-10-12 12:01 UTC65
8b8e83329a1datagen2026-10-12 12:01 UTC66
    fn all() -> impl Iterator<Item = Self> {
8b8e83329a1datagen2026-10-12 12:01 UTC67
        Self::EVERYDAY.into_iter().chain(Self::CHRISTMAS)
8b8e83329a1datagen2026-10-12 12:01 UTC68
    }
8b8e83329a1datagen2026-10-12 12:01 UTC69
8b8e83329a1datagen2026-10-12 12:01 UTC70
    fn parse(spoken: &str) -> Option<Self> {
8b8e83329a1datagen2026-10-12 12:01 UTC71
        Self::all().find(|excuse| excuse.phrase().eq_ignore_ascii_case(spoken))
8b8e83329a1datagen2026-10-12 12:01 UTC72
    }
8b8e83329a1datagen2026-10-12 12:01 UTC73
}
8b8e83329a1datagen2026-10-12 12:01 UTC74
c4d70621d377atagen2026-10-12 11:56 UTC75
/// Every excuse Mum might hear, so a localised one can be traced back to the phrase she checks.
c4d70621d377atagen2026-10-12 11:56 UTC76
pub fn vocabulary() -> impl Iterator<Item = &'static str> {
8b8e83329a1datagen2026-10-12 12:01 UTC77
    Excuse::all().map(Excuse::phrase)
c4d70621d377atagen2026-10-12 11:56 UTC78
}
c4d70621d377atagen2026-10-12 11:56 UTC79
8b8e83329a1datagen2026-10-12 12:01 UTC80
fn is_truth(spoken: &str) -> bool {
8b8e83329a1datagen2026-10-12 12:01 UTC81
    ["the truth", "truth", "honesty"]
8b8e83329a1datagen2026-10-12 12:01 UTC82
        .iter()
8b8e83329a1datagen2026-10-12 12:01 UTC83
        .any(|truth| truth.eq_ignore_ascii_case(spoken))
8b8e83329a1datagen2026-10-12 12:01 UTC84
}
782069860236atagen2026-10-10 12:44 UTC85
782069860236atagen2026-10-10 12:44 UTC86
const BEDROOM_ERRANDS: [&str; 4] = [
9683736a2c5eatagen2026-10-10 13:15 UTC87
    "She has brought your washing.",
782069860236atagen2026-10-10 12:44 UTC88
    "She wants to know if you have eaten.",
782069860236atagen2026-10-10 12:44 UTC89
    "She needs the wifi password.",
782069860236atagen2026-10-10 12:44 UTC90
    "She would like to talk about your future.",
782069860236atagen2026-10-10 12:44 UTC91
];
9683736a2c5eatagen2026-10-10 13:15 UTC92
const CHRISTMAS_ERRANDS: [&str; 4] = [
9683736a2c5eatagen2026-10-10 13:15 UTC93
    "Nan would like to see you.",
9683736a2c5eatagen2026-10-10 13:15 UTC94
    "Christmas dinner is in ten minutes.",
9683736a2c5eatagen2026-10-10 13:15 UTC95
    "The King's speech is about to start.",
782069860236atagen2026-10-10 12:44 UTC96
    "An uncle has asked where you went.",
782069860236atagen2026-10-10 12:44 UTC97
];
782069860236atagen2026-10-10 12:44 UTC98
8b8e83329a1datagen2026-10-12 12:01 UTC99
#[derive(Default)]
8b8e83329a1datagen2026-10-12 12:01 UTC100
pub struct Household {
8b8e83329a1datagen2026-10-12 12:01 UTC101
    pub(crate) mum: Option<MumVisit>,
8b8e83329a1datagen2026-10-12 12:01 UTC102
    /// Every excuse Mum has accepted this run. Digest Mum remembers them all.
8b8e83329a1datagen2026-10-12 12:01 UTC103
    pub(crate) heard: Vec<Excuse>,
8b8e83329a1datagen2026-10-12 12:01 UTC104
}
8b8e83329a1datagen2026-10-12 12:01 UTC105
8b8e83329a1datagen2026-10-12 12:01 UTC106
pub struct MumVisit {
8b8e83329a1datagen2026-10-12 12:01 UTC107
    pub(crate) errand: &'static str,
8b8e83329a1datagen2026-10-12 12:01 UTC108
    pub(crate) nonce: String,
8b8e83329a1datagen2026-10-12 12:01 UTC109
    pub(crate) strikes: u8,
8b8e83329a1datagen2026-10-12 12:01 UTC110
    pub(crate) knocked: bool,
8b8e83329a1datagen2026-10-12 12:01 UTC111
}
8b8e83329a1datagen2026-10-12 12:01 UTC112
782069860236atagen2026-10-10 12:44 UTC113
fn digest_params(authorization: &str) -> Option<HashMap<String, String>> {
782069860236atagen2026-10-10 12:44 UTC114
    let (scheme, rest) = authorization.trim().split_once(' ')?;
782069860236atagen2026-10-10 12:44 UTC115
    if !scheme.eq_ignore_ascii_case("digest") {
782069860236atagen2026-10-10 12:44 UTC116
        return None;
782069860236atagen2026-10-10 12:44 UTC117
    }
782069860236atagen2026-10-10 12:44 UTC118
    Some(
782069860236atagen2026-10-10 12:44 UTC119
        rest.split(',')
782069860236atagen2026-10-10 12:44 UTC120
            .filter_map(|pair| {
782069860236atagen2026-10-10 12:44 UTC121
                let (key, value) = pair.split_once('=')?;
782069860236atagen2026-10-10 12:44 UTC122
                Some((
782069860236atagen2026-10-10 12:44 UTC123
                    key.trim().to_ascii_lowercase(),
782069860236atagen2026-10-10 12:44 UTC124
                    value.trim().trim_matches('"').to_owned(),
782069860236atagen2026-10-10 12:44 UTC125
                ))
782069860236atagen2026-10-10 12:44 UTC126
            })
782069860236atagen2026-10-10 12:44 UTC127
            .collect(),
782069860236atagen2026-10-10 12:44 UTC128
    )
782069860236atagen2026-10-10 12:44 UTC129
}
782069860236atagen2026-10-10 12:44 UTC130
8b8e83329a1datagen2026-10-12 12:01 UTC131
fn spoken(authorization: &str) -> &str {
782069860236atagen2026-10-10 12:44 UTC132
    authorization
782069860236atagen2026-10-10 12:44 UTC133
        .split_once(' ')
8b8e83329a1datagen2026-10-12 12:01 UTC134
        .map_or("", |(_, rest)| rest.trim().trim_matches('"'))
8b8e83329a1datagen2026-10-12 12:01 UTC135
}
8b8e83329a1datagen2026-10-12 12:01 UTC136
f16ab17c8ff6atagen2026-10-13 11:14 UTC137
fn names_a_scheme(authorization: &str) -> bool {
f16ab17c8ff6atagen2026-10-13 11:14 UTC138
    authorization
f16ab17c8ff6atagen2026-10-13 11:14 UTC139
        .trim()
f16ab17c8ff6atagen2026-10-13 11:14 UTC140
        .split_once(' ')
f16ab17c8ff6atagen2026-10-13 11:14 UTC141
        .is_some_and(|(scheme, _)| {
f16ab17c8ff6atagen2026-10-13 11:14 UTC142
            ["basic", "digest", "excuse"]
f16ab17c8ff6atagen2026-10-13 11:14 UTC143
                .iter()
f16ab17c8ff6atagen2026-10-13 11:14 UTC144
                .any(|known| known.eq_ignore_ascii_case(scheme))
f16ab17c8ff6atagen2026-10-13 11:14 UTC145
        })
f16ab17c8ff6atagen2026-10-13 11:14 UTC146
}
f16ab17c8ff6atagen2026-10-13 11:14 UTC147
8b8e83329a1datagen2026-10-12 12:01 UTC148
fn turned_away(code: &'static str, detail: &'static str) -> Problem {
8b8e83329a1datagen2026-10-12 12:01 UTC149
    Problem::new(StatusCode::UNAUTHORIZED, code, detail)
782069860236atagen2026-10-10 12:44 UTC150
}
782069860236atagen2026-10-10 12:44 UTC151
782069860236atagen2026-10-10 12:44 UTC152
impl Run {
8b8e83329a1datagen2026-10-12 12:01 UTC153
    pub fn excuses(&self) -> Vec<Excuse> {
9683736a2c5eatagen2026-10-10 13:15 UTC154
        if self.floor.number() < Floor::Nans.number() {
8b8e83329a1datagen2026-10-12 12:01 UTC155
            return Excuse::EVERYDAY.to_vec();
782069860236atagen2026-10-10 12:44 UTC156
        }
8b8e83329a1datagen2026-10-12 12:01 UTC157
        Excuse::all()
8b8e83329a1datagen2026-10-12 12:01 UTC158
            .filter(|excuse| !self.household.heard.contains(excuse))
782069860236atagen2026-10-10 12:44 UTC159
            .collect()
782069860236atagen2026-10-10 12:44 UTC160
    }
782069860236atagen2026-10-10 12:44 UTC161
782069860236atagen2026-10-10 12:44 UTC162
    pub(super) fn errand(&mut self, summoned: bool) -> &'static str {
782069860236atagen2026-10-10 12:44 UTC163
        if summoned {
782069860236atagen2026-10-10 12:44 UTC164
            return "She heard a noise.";
782069860236atagen2026-10-10 12:44 UTC165
        }
8b8e83329a1datagen2026-10-12 12:01 UTC166
        if self.online.filmed_at.is_some()
3f9091557ec3atagen2026-10-11 11:43 UTC167
            && self.chance(
3f9091557ec3atagen2026-10-11 11:43 UTC168
                self.decision("viral", "Has a neighbour already sent Mum the footage?"),
3f9091557ec3atagen2026-10-11 11:43 UTC169
                VIRAL_ERRAND,
3f9091557ec3atagen2026-10-11 11:43 UTC170
            )
3f9091557ec3atagen2026-10-11 11:43 UTC171
        {
782069860236atagen2026-10-10 12:44 UTC172
            return "A neighbour has sent her something.";
782069860236atagen2026-10-10 12:44 UTC173
        }
9683736a2c5eatagen2026-10-10 13:15 UTC174
        let errands = if self.floor == Floor::Nans {
9683736a2c5eatagen2026-10-10 13:15 UTC175
            CHRISTMAS_ERRANDS
782069860236atagen2026-10-10 12:44 UTC176
        } else {
782069860236atagen2026-10-10 12:44 UTC177
            BEDROOM_ERRANDS
782069860236atagen2026-10-10 12:44 UTC178
        };
3f9091557ec3atagen2026-10-11 11:43 UTC179
        let weights = vec![1; errands.len()];
3f9091557ec3atagen2026-10-11 11:43 UTC180
        let i = self.oracle.pick(
3f9091557ec3atagen2026-10-11 11:43 UTC181
            self.decision("errand", "What has brought Mum to the door?"),
3f9091557ec3atagen2026-10-11 11:43 UTC182
            &weights,
3f9091557ec3atagen2026-10-11 11:43 UTC183
            &errands,
3f9091557ec3atagen2026-10-11 11:43 UTC184
        );
3f9091557ec3atagen2026-10-11 11:43 UTC185
        errands[i]
782069860236atagen2026-10-10 12:44 UTC186
    }
782069860236atagen2026-10-10 12:44 UTC187
9683736a2c5eatagen2026-10-10 13:15 UTC188
    /// Mum answers every request on the run until she has been given something she believes.
782069860236atagen2026-10-10 12:44 UTC189
    pub fn answer_door(&mut self, authorization: Option<&str>) -> Result<(), Problem> {
8b8e83329a1datagen2026-10-12 12:01 UTC190
        let Some(visit) = &self.household.mum else {
782069860236atagen2026-10-10 12:44 UTC191
            return Ok(());
782069860236atagen2026-10-10 12:44 UTC192
        };
9683736a2c5eatagen2026-10-10 13:15 UTC193
        let digest = self.floor == Floor::Nans;
8b8e83329a1datagen2026-10-12 12:01 UTC194
        let challenge = if digest {
8b8e83329a1datagen2026-10-12 12:01 UTC195
            format!("Digest realm=\"household\", nonce=\"{}\"", visit.nonce)
782069860236atagen2026-10-10 12:44 UTC196
        } else {
782069860236atagen2026-10-10 12:44 UTC197
            "Excuse realm=\"household\"".to_owned()
782069860236atagen2026-10-10 12:44 UTC198
        };
8b8e83329a1datagen2026-10-12 12:01 UTC199
        let result = match authorization {
8b8e83329a1datagen2026-10-12 12:01 UTC200
            None => Err(self.knock()),
8b8e83329a1datagen2026-10-12 12:01 UTC201
            Some(authorization) => self.hear(authorization, digest),
8b8e83329a1datagen2026-10-12 12:01 UTC202
        };
8b8e83329a1datagen2026-10-12 12:01 UTC203
        if self.ended.is_some() {
8b8e83329a1datagen2026-10-12 12:01 UTC204
            return result;
8b8e83329a1datagen2026-10-12 12:01 UTC205
        }
8b8e83329a1datagen2026-10-12 12:01 UTC206
        result.map_err(|problem| problem.header(header::WWW_AUTHENTICATE, &challenge))
8b8e83329a1datagen2026-10-12 12:01 UTC207
    }
8b8e83329a1datagen2026-10-12 12:01 UTC208
8b8e83329a1datagen2026-10-12 12:01 UTC209
    /// The first knock is free. Every one after it is a strike.
8b8e83329a1datagen2026-10-12 12:01 UTC210
    fn knock(&mut self) -> Problem {
8b8e83329a1datagen2026-10-12 12:01 UTC211
        let Some(visit) = &mut self.household.mum else {
8b8e83329a1datagen2026-10-12 12:01 UTC212
            unreachable!("knock is only called while Mum is at the door");
782069860236atagen2026-10-10 12:44 UTC213
        };
8b8e83329a1datagen2026-10-12 12:01 UTC214
        let errand = visit.errand;
8b8e83329a1datagen2026-10-12 12:01 UTC215
        if !std::mem::replace(&mut visit.knocked, true) {
8b8e83329a1datagen2026-10-12 12:01 UTC216
            self.touch();
8b8e83329a1datagen2026-10-12 12:01 UTC217
            return Problem::new(
8b8e83329a1datagen2026-10-12 12:01 UTC218
                StatusCode::UNAUTHORIZED,
8b8e83329a1datagen2026-10-12 12:01 UTC219
                "someone-at-the-door",
8b8e83329a1datagen2026-10-12 12:01 UTC220
                format!("Someone is at the door. {errand}"),
8b8e83329a1datagen2026-10-12 12:01 UTC221
            )
8b8e83329a1datagen2026-10-12 12:01 UTC222
            .with("inventory", self.path("/inventory"));
8b8e83329a1datagen2026-10-12 12:01 UTC223
        }
8b8e83329a1datagen2026-10-12 12:01 UTC224
        self.strike(
8b8e83329a1datagen2026-10-12 12:01 UTC225
            Problem::new(
8b8e83329a1datagen2026-10-12 12:01 UTC226
                StatusCode::UNAUTHORIZED,
8b8e83329a1datagen2026-10-12 12:01 UTC227
                "knocking-again",
8b8e83329a1datagen2026-10-12 12:01 UTC228
                format!("Mum is knocking again. {errand}"),
8b8e83329a1datagen2026-10-12 12:01 UTC229
            ),
8b8e83329a1datagen2026-10-12 12:01 UTC230
            Cause::Opened,
8b8e83329a1datagen2026-10-12 12:01 UTC231
        )
8b8e83329a1datagen2026-10-12 12:01 UTC232
    }
782069860236atagen2026-10-10 12:44 UTC233
8b8e83329a1datagen2026-10-12 12:01 UTC234
    fn hear(&mut self, authorization: &str, digest: bool) -> Result<(), Problem> {
8b8e83329a1datagen2026-10-12 12:01 UTC235
        if is_truth(spoken(authorization)) {
8b8e83329a1datagen2026-10-12 12:01 UTC236
            self.confess();
782069860236atagen2026-10-10 12:44 UTC237
            return Ok(());
782069860236atagen2026-10-10 12:44 UTC238
        }
f16ab17c8ff6atagen2026-10-13 11:14 UTC239
        if !names_a_scheme(authorization) {
f16ab17c8ff6atagen2026-10-13 11:14 UTC240
            self.touch();
f16ab17c8ff6atagen2026-10-13 11:14 UTC241
            return Err(turned_away(
f16ab17c8ff6atagen2026-10-13 11:14 UTC242
                "excuse-unheard",
f16ab17c8ff6atagen2026-10-13 11:14 UTC243
                if digest {
f16ab17c8ff6atagen2026-10-13 11:14 UTC244
                    "Mum did not catch that. At Christmas she uses Digest, with the excuse as the username."
f16ab17c8ff6atagen2026-10-13 11:14 UTC245
                } else {
f16ab17c8ff6atagen2026-10-13 11:14 UTC246
                    "Mum did not catch that. Excuses are given as Excuse \"homework\"."
f16ab17c8ff6atagen2026-10-13 11:14 UTC247
                },
f16ab17c8ff6atagen2026-10-13 11:14 UTC248
            ));
f16ab17c8ff6atagen2026-10-13 11:14 UTC249
        }
8b8e83329a1datagen2026-10-12 12:01 UTC250
        if parse_credentials(authorization, Scheme::Basic).is_some() {
8b8e83329a1datagen2026-10-12 12:01 UTC251
            return Err(self.mum_strike(turned_away(
782069860236atagen2026-10-10 12:44 UTC252
                "basic",
9683736a2c5eatagen2026-10-10 13:15 UTC253
                "Mum does not accept Basic authentication. Nobody should.",
8b8e83329a1datagen2026-10-12 12:01 UTC254
            )));
782069860236atagen2026-10-10 12:44 UTC255
        }
8b8e83329a1datagen2026-10-12 12:01 UTC256
        let said = if digest {
8b8e83329a1datagen2026-10-12 12:01 UTC257
            self.digest_username(authorization)?
782069860236atagen2026-10-10 12:44 UTC258
        } else {
8b8e83329a1datagen2026-10-12 12:01 UTC259
            parse_credentials(authorization, Scheme::Excuse)
782069860236atagen2026-10-10 12:44 UTC260
                .unwrap_or_default()
782069860236atagen2026-10-10 12:44 UTC261
                .to_owned()
782069860236atagen2026-10-10 12:44 UTC262
        };
8b8e83329a1datagen2026-10-12 12:01 UTC263
        if is_truth(&said) {
8b8e83329a1datagen2026-10-12 12:01 UTC264
            self.confess();
782069860236atagen2026-10-10 12:44 UTC265
            return Ok(());
782069860236atagen2026-10-10 12:44 UTC266
        }
8b8e83329a1datagen2026-10-12 12:01 UTC267
        let excuse = Excuse::parse(&said);
8b8e83329a1datagen2026-10-12 12:01 UTC268
        let true_now = self.body.conditions.pulled_muscle && excuse == Some(Excuse::PulledAMuscle);
8b8e83329a1datagen2026-10-12 12:01 UTC269
        if digest && !true_now && excuse.is_some_and(|e| self.household.heard.contains(&e)) {
8b8e83329a1datagen2026-10-12 12:01 UTC270
            self.standing.shame += 3;
8b8e83329a1datagen2026-10-12 12:01 UTC271
            return Err(self.mum_strike(turned_away(
782069860236atagen2026-10-10 12:44 UTC272
                "excuse-replayed",
9683736a2c5eatagen2026-10-10 13:15 UTC273
                "Mum has heard that one before. She would have preferred the truth.",
8b8e83329a1datagen2026-10-12 12:01 UTC274
            )));
782069860236atagen2026-10-10 12:44 UTC275
        }
8b8e83329a1datagen2026-10-12 12:01 UTC276
        if let Some(excuse) = excuse.filter(|e| true_now || self.excuses().contains(e)) {
8b8e83329a1datagen2026-10-12 12:01 UTC277
            self.household.heard.push(excuse);
782069860236atagen2026-10-10 12:44 UTC278
            self.leave_door();
782069860236atagen2026-10-10 12:44 UTC279
            return Ok(());
782069860236atagen2026-10-10 12:44 UTC280
        }
8b8e83329a1datagen2026-10-12 12:01 UTC281
        self.standing.shame += 3;
8b8e83329a1datagen2026-10-12 12:01 UTC282
        Err(self.mum_strike(turned_away(
782069860236atagen2026-10-10 12:44 UTC283
            "excuse-rejected",
8b8e83329a1datagen2026-10-12 12:01 UTC284
            if digest {
8b8e83329a1datagen2026-10-12 12:01 UTC285
                "Mum has not heard that one before. She does not believe it. She would have preferred the truth."
8b8e83329a1datagen2026-10-12 12:01 UTC286
            } else {
8b8e83329a1datagen2026-10-12 12:01 UTC287
                "Mum has not heard that one before. She does not believe it."
8b8e83329a1datagen2026-10-12 12:01 UTC288
            },
8b8e83329a1datagen2026-10-12 12:01 UTC289
        )))
8b8e83329a1datagen2026-10-12 12:01 UTC290
    }
8b8e83329a1datagen2026-10-12 12:01 UTC291
8b8e83329a1datagen2026-10-12 12:01 UTC292
    fn digest_username(&mut self, authorization: &str) -> Result<String, Problem> {
8b8e83329a1datagen2026-10-12 12:01 UTC293
        let Some(mut params) = digest_params(authorization) else {
8b8e83329a1datagen2026-10-12 12:01 UTC294
            return Err(self.mum_strike(turned_away(
8b8e83329a1datagen2026-10-12 12:01 UTC295
                "digest-required",
8b8e83329a1datagen2026-10-12 12:01 UTC296
                "Mum has started using Digest authentication.",
8b8e83329a1datagen2026-10-12 12:01 UTC297
            )));
8b8e83329a1datagen2026-10-12 12:01 UTC298
        };
8b8e83329a1datagen2026-10-12 12:01 UTC299
        let current = self
8b8e83329a1datagen2026-10-12 12:01 UTC300
            .household
8b8e83329a1datagen2026-10-12 12:01 UTC301
            .mum
8b8e83329a1datagen2026-10-12 12:01 UTC302
            .as_ref()
8b8e83329a1datagen2026-10-12 12:01 UTC303
            .is_some_and(|visit| params.get("nonce") == Some(&visit.nonce));
8b8e83329a1datagen2026-10-12 12:01 UTC304
        if !current {
8b8e83329a1datagen2026-10-12 12:01 UTC305
            return Err(self.mum_strike(turned_away(
8b8e83329a1datagen2026-10-12 12:01 UTC306
                "stale-nonce",
8b8e83329a1datagen2026-10-12 12:01 UTC307
                "That was a different conversation.",
8b8e83329a1datagen2026-10-12 12:01 UTC308
            )));
8b8e83329a1datagen2026-10-12 12:01 UTC309
        }
8b8e83329a1datagen2026-10-12 12:01 UTC310
        Ok(params.remove("username").unwrap_or_default())
8b8e83329a1datagen2026-10-12 12:01 UTC311
    }
8b8e83329a1datagen2026-10-12 12:01 UTC312
8b8e83329a1datagen2026-10-12 12:01 UTC313
    fn confess(&mut self) {
8b8e83329a1datagen2026-10-12 12:01 UTC314
        self.standing.shame += 10;
8b8e83329a1datagen2026-10-12 12:01 UTC315
        self.leave_door();
782069860236atagen2026-10-10 12:44 UTC316
    }
782069860236atagen2026-10-10 12:44 UTC317
9683736a2c5eatagen2026-10-10 13:15 UTC318
    fn mum_strike(&mut self, problem: Problem) -> Problem {
0d604ee39751atagen2026-10-10 14:15 UTC319
        self.strike(problem, Cause::Caught)
0d604ee39751atagen2026-10-10 14:15 UTC320
    }
0d604ee39751atagen2026-10-10 14:15 UTC321
0d604ee39751atagen2026-10-10 14:15 UTC322
    fn strike(&mut self, problem: Problem, cause: Cause) -> Problem {
782069860236atagen2026-10-10 12:44 UTC323
        self.touch();
8b8e83329a1datagen2026-10-12 12:01 UTC324
        let Some(visit) = &mut self.household.mum else {
782069860236atagen2026-10-10 12:44 UTC325
            return problem;
782069860236atagen2026-10-10 12:44 UTC326
        };
782069860236atagen2026-10-10 12:44 UTC327
        visit.strikes += 1;
782069860236atagen2026-10-10 12:44 UTC328
        if visit.strikes < PATIENCE {
782069860236atagen2026-10-10 12:44 UTC329
            return problem;
782069860236atagen2026-10-10 12:44 UTC330
        }
0d604ee39751atagen2026-10-10 14:15 UTC331
        self.end(cause)
782069860236atagen2026-10-10 12:44 UTC332
    }
782069860236atagen2026-10-10 12:44 UTC333
782069860236atagen2026-10-10 12:44 UTC334
    fn leave_door(&mut self) {
8b8e83329a1datagen2026-10-12 12:01 UTC335
        self.household.mum = None;
8b8e83329a1datagen2026-10-12 12:01 UTC336
        self.standing.suspicion = 0;
8b8e83329a1datagen2026-10-12 12:01 UTC337
        self.notices.push(Notice::MumSatisfied);
782069860236atagen2026-10-10 12:44 UTC338
        self.touch();
782069860236atagen2026-10-10 12:44 UTC339
    }
782069860236atagen2026-10-10 12:44 UTC340
}