atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
e028a4184d000c9c7b61f60e056db01c53aeea23
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
886159fd0fe7atagen2026-10-13 12:48 UTC3
use axum::http::StatusCode;
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
886159fd0fe7atagen2026-10-13 12:48 UTC86
/// Why Mum came to the door, and how she leaves once she believes you.
886159fd0fe7atagen2026-10-13 12:48 UTC87
#[derive(Clone, Copy)]
886159fd0fe7atagen2026-10-13 12:48 UTC88
pub struct Errand {
886159fd0fe7atagen2026-10-13 12:48 UTC89
    pub(crate) knock: &'static str,
886159fd0fe7atagen2026-10-13 12:48 UTC90
    pub(crate) parting: &'static str,
886159fd0fe7atagen2026-10-13 12:48 UTC91
}
886159fd0fe7atagen2026-10-13 12:48 UTC92
886159fd0fe7atagen2026-10-13 12:48 UTC93
pub const HEARD_A_NOISE: Errand = Errand {
886159fd0fe7atagen2026-10-13 12:48 UTC94
    knock: "She heard a noise.",
886159fd0fe7atagen2026-10-13 12:48 UTC95
    parting: "Mum seems satisfied. It must have been the pipes.",
886159fd0fe7atagen2026-10-13 12:48 UTC96
};
886159fd0fe7atagen2026-10-13 12:48 UTC97
const VIRAL: Errand = Errand {
886159fd0fe7atagen2026-10-13 12:48 UTC98
    knock: "A neighbour has sent her something.",
886159fd0fe7atagen2026-10-13 12:48 UTC99
    parting: "Mum seems satisfied. She has decided not to open the attachment.",
886159fd0fe7atagen2026-10-13 12:48 UTC100
};
886159fd0fe7atagen2026-10-13 12:48 UTC101
const BEDROOM_ERRANDS: [Errand; 4] = [
886159fd0fe7atagen2026-10-13 12:48 UTC102
    Errand {
886159fd0fe7atagen2026-10-13 12:48 UTC103
        knock: "She has brought your washing.",
886159fd0fe7atagen2026-10-13 12:48 UTC104
        parting: "Mum seems satisfied. She leaves the washing on the chair.",
886159fd0fe7atagen2026-10-13 12:48 UTC105
    },
886159fd0fe7atagen2026-10-13 12:48 UTC106
    Errand {
886159fd0fe7atagen2026-10-13 12:48 UTC107
        knock: "She wants to know if you have eaten.",
886159fd0fe7atagen2026-10-13 12:48 UTC108
        parting: "Mum seems satisfied. There will be a plate in the oven.",
886159fd0fe7atagen2026-10-13 12:48 UTC109
    },
886159fd0fe7atagen2026-10-13 12:48 UTC110
    Errand {
886159fd0fe7atagen2026-10-13 12:48 UTC111
        knock: "She needs the wifi password.",
886159fd0fe7atagen2026-10-13 12:48 UTC112
        parting: "Mum seems satisfied. She has written the wifi password on her hand.",
886159fd0fe7atagen2026-10-13 12:48 UTC113
    },
886159fd0fe7atagen2026-10-13 12:48 UTC114
    Errand {
886159fd0fe7atagen2026-10-13 12:48 UTC115
        knock: "She would like to talk about your future.",
886159fd0fe7atagen2026-10-13 12:48 UTC116
        parting: "Mum seems satisfied. The future can wait until dinner.",
886159fd0fe7atagen2026-10-13 12:48 UTC117
    },
782069860236atagen2026-10-10 12:44 UTC118
];
886159fd0fe7atagen2026-10-13 12:48 UTC119
const CHRISTMAS_ERRANDS: [Errand; 4] = [
886159fd0fe7atagen2026-10-13 12:48 UTC120
    Errand {
886159fd0fe7atagen2026-10-13 12:48 UTC121
        knock: "Nan would like to see you.",
886159fd0fe7atagen2026-10-13 12:48 UTC122
        parting: "Mum seems satisfied. She tells Nan you are on your way.",
886159fd0fe7atagen2026-10-13 12:48 UTC123
    },
886159fd0fe7atagen2026-10-13 12:48 UTC124
    Errand {
886159fd0fe7atagen2026-10-13 12:48 UTC125
        knock: "Christmas dinner is in ten minutes.",
886159fd0fe7atagen2026-10-13 12:48 UTC126
        parting: "Mum seems satisfied. She will save you a pig in a blanket.",
886159fd0fe7atagen2026-10-13 12:48 UTC127
    },
886159fd0fe7atagen2026-10-13 12:48 UTC128
    Errand {
886159fd0fe7atagen2026-10-13 12:48 UTC129
        knock: "The King's speech is about to start.",
886159fd0fe7atagen2026-10-13 12:48 UTC130
        parting: "Mum seems satisfied. She will tell you what the King said.",
886159fd0fe7atagen2026-10-13 12:48 UTC131
    },
886159fd0fe7atagen2026-10-13 12:48 UTC132
    Errand {
886159fd0fe7atagen2026-10-13 12:48 UTC133
        knock: "An uncle has asked where you went.",
886159fd0fe7atagen2026-10-13 12:48 UTC134
        parting: "Mum seems satisfied. She tells the uncle you are helping in the kitchen.",
886159fd0fe7atagen2026-10-13 12:48 UTC135
    },
782069860236atagen2026-10-10 12:44 UTC136
];
782069860236atagen2026-10-10 12:44 UTC137
8b8e83329a1datagen2026-10-12 12:01 UTC138
#[derive(Default)]
8b8e83329a1datagen2026-10-12 12:01 UTC139
pub struct Household {
8b8e83329a1datagen2026-10-12 12:01 UTC140
    pub(crate) mum: Option<MumVisit>,
8b8e83329a1datagen2026-10-12 12:01 UTC141
    /// Every excuse Mum has accepted this run. Digest Mum remembers them all.
8b8e83329a1datagen2026-10-12 12:01 UTC142
    pub(crate) heard: Vec<Excuse>,
8b8e83329a1datagen2026-10-12 12:01 UTC143
}
8b8e83329a1datagen2026-10-12 12:01 UTC144
8b8e83329a1datagen2026-10-12 12:01 UTC145
pub struct MumVisit {
886159fd0fe7atagen2026-10-13 12:48 UTC146
    pub(crate) errand: Errand,
8b8e83329a1datagen2026-10-12 12:01 UTC147
    pub(crate) nonce: String,
8b8e83329a1datagen2026-10-12 12:01 UTC148
    pub(crate) strikes: u8,
8b8e83329a1datagen2026-10-12 12:01 UTC149
    pub(crate) knocked: bool,
8b8e83329a1datagen2026-10-12 12:01 UTC150
}
8b8e83329a1datagen2026-10-12 12:01 UTC151
782069860236atagen2026-10-10 12:44 UTC152
fn digest_params(authorization: &str) -> Option<HashMap<String, String>> {
782069860236atagen2026-10-10 12:44 UTC153
    let (scheme, rest) = authorization.trim().split_once(' ')?;
782069860236atagen2026-10-10 12:44 UTC154
    if !scheme.eq_ignore_ascii_case("digest") {
782069860236atagen2026-10-10 12:44 UTC155
        return None;
782069860236atagen2026-10-10 12:44 UTC156
    }
782069860236atagen2026-10-10 12:44 UTC157
    Some(
782069860236atagen2026-10-10 12:44 UTC158
        rest.split(',')
782069860236atagen2026-10-10 12:44 UTC159
            .filter_map(|pair| {
782069860236atagen2026-10-10 12:44 UTC160
                let (key, value) = pair.split_once('=')?;
782069860236atagen2026-10-10 12:44 UTC161
                Some((
782069860236atagen2026-10-10 12:44 UTC162
                    key.trim().to_ascii_lowercase(),
782069860236atagen2026-10-10 12:44 UTC163
                    value.trim().trim_matches('"').to_owned(),
782069860236atagen2026-10-10 12:44 UTC164
                ))
782069860236atagen2026-10-10 12:44 UTC165
            })
782069860236atagen2026-10-10 12:44 UTC166
            .collect(),
782069860236atagen2026-10-10 12:44 UTC167
    )
782069860236atagen2026-10-10 12:44 UTC168
}
782069860236atagen2026-10-10 12:44 UTC169
8b8e83329a1datagen2026-10-12 12:01 UTC170
fn spoken(authorization: &str) -> &str {
782069860236atagen2026-10-10 12:44 UTC171
    authorization
782069860236atagen2026-10-10 12:44 UTC172
        .split_once(' ')
8b8e83329a1datagen2026-10-12 12:01 UTC173
        .map_or("", |(_, rest)| rest.trim().trim_matches('"'))
8b8e83329a1datagen2026-10-12 12:01 UTC174
}
8b8e83329a1datagen2026-10-12 12:01 UTC175
f16ab17c8ff6atagen2026-10-13 11:14 UTC176
fn names_a_scheme(authorization: &str) -> bool {
f16ab17c8ff6atagen2026-10-13 11:14 UTC177
    authorization
f16ab17c8ff6atagen2026-10-13 11:14 UTC178
        .trim()
f16ab17c8ff6atagen2026-10-13 11:14 UTC179
        .split_once(' ')
f16ab17c8ff6atagen2026-10-13 11:14 UTC180
        .is_some_and(|(scheme, _)| {
f16ab17c8ff6atagen2026-10-13 11:14 UTC181
            ["basic", "digest", "excuse"]
f16ab17c8ff6atagen2026-10-13 11:14 UTC182
                .iter()
f16ab17c8ff6atagen2026-10-13 11:14 UTC183
                .any(|known| known.eq_ignore_ascii_case(scheme))
f16ab17c8ff6atagen2026-10-13 11:14 UTC184
        })
f16ab17c8ff6atagen2026-10-13 11:14 UTC185
}
f16ab17c8ff6atagen2026-10-13 11:14 UTC186
8b8e83329a1datagen2026-10-12 12:01 UTC187
fn turned_away(code: &'static str, detail: &'static str) -> Problem {
8b8e83329a1datagen2026-10-12 12:01 UTC188
    Problem::new(StatusCode::UNAUTHORIZED, code, detail)
782069860236atagen2026-10-10 12:44 UTC189
}
782069860236atagen2026-10-10 12:44 UTC190
782069860236atagen2026-10-10 12:44 UTC191
impl Run {
8b8e83329a1datagen2026-10-12 12:01 UTC192
    pub fn excuses(&self) -> Vec<Excuse> {
9683736a2c5eatagen2026-10-10 13:15 UTC193
        if self.floor.number() < Floor::Nans.number() {
8b8e83329a1datagen2026-10-12 12:01 UTC194
            return Excuse::EVERYDAY.to_vec();
782069860236atagen2026-10-10 12:44 UTC195
        }
8b8e83329a1datagen2026-10-12 12:01 UTC196
        Excuse::all()
8b8e83329a1datagen2026-10-12 12:01 UTC197
            .filter(|excuse| !self.household.heard.contains(excuse))
782069860236atagen2026-10-10 12:44 UTC198
            .collect()
782069860236atagen2026-10-10 12:44 UTC199
    }
782069860236atagen2026-10-10 12:44 UTC200
886159fd0fe7atagen2026-10-13 12:48 UTC201
    pub(super) fn errand(&mut self, summoned: bool) -> Errand {
782069860236atagen2026-10-10 12:44 UTC202
        if summoned {
886159fd0fe7atagen2026-10-13 12:48 UTC203
            return HEARD_A_NOISE;
782069860236atagen2026-10-10 12:44 UTC204
        }
8b8e83329a1datagen2026-10-12 12:01 UTC205
        if self.online.filmed_at.is_some()
3f9091557ec3atagen2026-10-11 11:43 UTC206
            && self.chance(
3f9091557ec3atagen2026-10-11 11:43 UTC207
                self.decision("viral", "Has a neighbour already sent Mum the footage?"),
3f9091557ec3atagen2026-10-11 11:43 UTC208
                VIRAL_ERRAND,
3f9091557ec3atagen2026-10-11 11:43 UTC209
            )
3f9091557ec3atagen2026-10-11 11:43 UTC210
        {
886159fd0fe7atagen2026-10-13 12:48 UTC211
            return VIRAL;
782069860236atagen2026-10-10 12:44 UTC212
        }
9683736a2c5eatagen2026-10-10 13:15 UTC213
        let errands = if self.floor == Floor::Nans {
9683736a2c5eatagen2026-10-10 13:15 UTC214
            CHRISTMAS_ERRANDS
782069860236atagen2026-10-10 12:44 UTC215
        } else {
782069860236atagen2026-10-10 12:44 UTC216
            BEDROOM_ERRANDS
782069860236atagen2026-10-10 12:44 UTC217
        };
3f9091557ec3atagen2026-10-11 11:43 UTC218
        let weights = vec![1; errands.len()];
886159fd0fe7atagen2026-10-13 12:48 UTC219
        let knocks = errands.map(|errand| errand.knock);
3f9091557ec3atagen2026-10-11 11:43 UTC220
        let i = self.oracle.pick(
3f9091557ec3atagen2026-10-11 11:43 UTC221
            self.decision("errand", "What has brought Mum to the door?"),
3f9091557ec3atagen2026-10-11 11:43 UTC222
            &weights,
886159fd0fe7atagen2026-10-13 12:48 UTC223
            &knocks,
3f9091557ec3atagen2026-10-11 11:43 UTC224
        );
3f9091557ec3atagen2026-10-11 11:43 UTC225
        errands[i]
782069860236atagen2026-10-10 12:44 UTC226
    }
782069860236atagen2026-10-10 12:44 UTC227
9683736a2c5eatagen2026-10-10 13:15 UTC228
    /// Mum answers every request on the run until she has been given something she believes.
782069860236atagen2026-10-10 12:44 UTC229
    pub fn answer_door(&mut self, authorization: Option<&str>) -> Result<(), Problem> {
886159fd0fe7atagen2026-10-13 12:48 UTC230
        if self.household.mum.is_none() {
782069860236atagen2026-10-10 12:44 UTC231
            return Ok(());
886159fd0fe7atagen2026-10-13 12:48 UTC232
        }
886159fd0fe7atagen2026-10-13 12:48 UTC233
        match authorization {
886159fd0fe7atagen2026-10-13 12:48 UTC234
            None => Err(self.knock()),
886159fd0fe7atagen2026-10-13 12:48 UTC235
            Some(authorization) => self.hear(authorization, self.floor == Floor::Nans),
886159fd0fe7atagen2026-10-13 12:48 UTC236
        }
886159fd0fe7atagen2026-10-13 12:48 UTC237
    }
886159fd0fe7atagen2026-10-13 12:48 UTC238
886159fd0fe7atagen2026-10-13 12:48 UTC239
    /// Sent on every response while she is at the door, so a client always holds the current nonce.
886159fd0fe7atagen2026-10-13 12:48 UTC240
    pub fn mum_challenge(&self) -> Option<String> {
886159fd0fe7atagen2026-10-13 12:48 UTC241
        let visit = self
886159fd0fe7atagen2026-10-13 12:48 UTC242
            .household
886159fd0fe7atagen2026-10-13 12:48 UTC243
            .mum
886159fd0fe7atagen2026-10-13 12:48 UTC244
            .as_ref()
886159fd0fe7atagen2026-10-13 12:48 UTC245
            .filter(|_| self.ended.is_none())?;
886159fd0fe7atagen2026-10-13 12:48 UTC246
        Some(if self.floor == Floor::Nans {
8b8e83329a1datagen2026-10-12 12:01 UTC247
            format!("Digest realm=\"household\", nonce=\"{}\"", visit.nonce)
782069860236atagen2026-10-10 12:44 UTC248
        } else {
782069860236atagen2026-10-10 12:44 UTC249
            "Excuse realm=\"household\"".to_owned()
886159fd0fe7atagen2026-10-13 12:48 UTC250
        })
8b8e83329a1datagen2026-10-12 12:01 UTC251
    }
8b8e83329a1datagen2026-10-12 12:01 UTC252
8b8e83329a1datagen2026-10-12 12:01 UTC253
    /// The first knock is free. Every one after it is a strike.
8b8e83329a1datagen2026-10-12 12:01 UTC254
    fn knock(&mut self) -> Problem {
8b8e83329a1datagen2026-10-12 12:01 UTC255
        let Some(visit) = &mut self.household.mum else {
8b8e83329a1datagen2026-10-12 12:01 UTC256
            unreachable!("knock is only called while Mum is at the door");
782069860236atagen2026-10-10 12:44 UTC257
        };
886159fd0fe7atagen2026-10-13 12:48 UTC258
        let errand = visit.errand.knock;
8b8e83329a1datagen2026-10-12 12:01 UTC259
        if !std::mem::replace(&mut visit.knocked, true) {
8b8e83329a1datagen2026-10-12 12:01 UTC260
            self.touch();
8b8e83329a1datagen2026-10-12 12:01 UTC261
            return Problem::new(
8b8e83329a1datagen2026-10-12 12:01 UTC262
                StatusCode::UNAUTHORIZED,
8b8e83329a1datagen2026-10-12 12:01 UTC263
                "someone-at-the-door",
8b8e83329a1datagen2026-10-12 12:01 UTC264
                format!("Someone is at the door. {errand}"),
8b8e83329a1datagen2026-10-12 12:01 UTC265
            )
8b8e83329a1datagen2026-10-12 12:01 UTC266
            .with("inventory", self.path("/inventory"));
8b8e83329a1datagen2026-10-12 12:01 UTC267
        }
8b8e83329a1datagen2026-10-12 12:01 UTC268
        self.strike(
8b8e83329a1datagen2026-10-12 12:01 UTC269
            Problem::new(
8b8e83329a1datagen2026-10-12 12:01 UTC270
                StatusCode::UNAUTHORIZED,
8b8e83329a1datagen2026-10-12 12:01 UTC271
                "knocking-again",
8b8e83329a1datagen2026-10-12 12:01 UTC272
                format!("Mum is knocking again. {errand}"),
8b8e83329a1datagen2026-10-12 12:01 UTC273
            ),
8b8e83329a1datagen2026-10-12 12:01 UTC274
            Cause::Opened,
8b8e83329a1datagen2026-10-12 12:01 UTC275
        )
8b8e83329a1datagen2026-10-12 12:01 UTC276
    }
782069860236atagen2026-10-10 12:44 UTC277
8b8e83329a1datagen2026-10-12 12:01 UTC278
    fn hear(&mut self, authorization: &str, digest: bool) -> Result<(), Problem> {
8b8e83329a1datagen2026-10-12 12:01 UTC279
        if is_truth(spoken(authorization)) {
8b8e83329a1datagen2026-10-12 12:01 UTC280
            self.confess();
782069860236atagen2026-10-10 12:44 UTC281
            return Ok(());
782069860236atagen2026-10-10 12:44 UTC282
        }
f16ab17c8ff6atagen2026-10-13 11:14 UTC283
        if !names_a_scheme(authorization) {
f16ab17c8ff6atagen2026-10-13 11:14 UTC284
            self.touch();
f16ab17c8ff6atagen2026-10-13 11:14 UTC285
            return Err(turned_away(
f16ab17c8ff6atagen2026-10-13 11:14 UTC286
                "excuse-unheard",
f16ab17c8ff6atagen2026-10-13 11:14 UTC287
                if digest {
f16ab17c8ff6atagen2026-10-13 11:14 UTC288
                    "Mum did not catch that. At Christmas she uses Digest, with the excuse as the username."
f16ab17c8ff6atagen2026-10-13 11:14 UTC289
                } else {
f16ab17c8ff6atagen2026-10-13 11:14 UTC290
                    "Mum did not catch that. Excuses are given as Excuse \"homework\"."
f16ab17c8ff6atagen2026-10-13 11:14 UTC291
                },
f16ab17c8ff6atagen2026-10-13 11:14 UTC292
            ));
f16ab17c8ff6atagen2026-10-13 11:14 UTC293
        }
8b8e83329a1datagen2026-10-12 12:01 UTC294
        if parse_credentials(authorization, Scheme::Basic).is_some() {
8b8e83329a1datagen2026-10-12 12:01 UTC295
            return Err(self.mum_strike(turned_away(
782069860236atagen2026-10-10 12:44 UTC296
                "basic",
9683736a2c5eatagen2026-10-10 13:15 UTC297
                "Mum does not accept Basic authentication. Nobody should.",
8b8e83329a1datagen2026-10-12 12:01 UTC298
            )));
782069860236atagen2026-10-10 12:44 UTC299
        }
8b8e83329a1datagen2026-10-12 12:01 UTC300
        let said = if digest {
8b8e83329a1datagen2026-10-12 12:01 UTC301
            self.digest_username(authorization)?
782069860236atagen2026-10-10 12:44 UTC302
        } else {
8b8e83329a1datagen2026-10-12 12:01 UTC303
            parse_credentials(authorization, Scheme::Excuse)
782069860236atagen2026-10-10 12:44 UTC304
                .unwrap_or_default()
782069860236atagen2026-10-10 12:44 UTC305
                .to_owned()
782069860236atagen2026-10-10 12:44 UTC306
        };
8b8e83329a1datagen2026-10-12 12:01 UTC307
        if is_truth(&said) {
8b8e83329a1datagen2026-10-12 12:01 UTC308
            self.confess();
782069860236atagen2026-10-10 12:44 UTC309
            return Ok(());
782069860236atagen2026-10-10 12:44 UTC310
        }
8b8e83329a1datagen2026-10-12 12:01 UTC311
        let excuse = Excuse::parse(&said);
8b8e83329a1datagen2026-10-12 12:01 UTC312
        let true_now = self.body.conditions.pulled_muscle && excuse == Some(Excuse::PulledAMuscle);
8b8e83329a1datagen2026-10-12 12:01 UTC313
        if digest && !true_now && excuse.is_some_and(|e| self.household.heard.contains(&e)) {
8b8e83329a1datagen2026-10-12 12:01 UTC314
            self.standing.shame += 3;
8b8e83329a1datagen2026-10-12 12:01 UTC315
            return Err(self.mum_strike(turned_away(
782069860236atagen2026-10-10 12:44 UTC316
                "excuse-replayed",
9683736a2c5eatagen2026-10-10 13:15 UTC317
                "Mum has heard that one before. She would have preferred the truth.",
8b8e83329a1datagen2026-10-12 12:01 UTC318
            )));
782069860236atagen2026-10-10 12:44 UTC319
        }
8b8e83329a1datagen2026-10-12 12:01 UTC320
        if let Some(excuse) = excuse.filter(|e| true_now || self.excuses().contains(e)) {
8b8e83329a1datagen2026-10-12 12:01 UTC321
            self.household.heard.push(excuse);
782069860236atagen2026-10-10 12:44 UTC322
            self.leave_door();
782069860236atagen2026-10-10 12:44 UTC323
            return Ok(());
782069860236atagen2026-10-10 12:44 UTC324
        }
8b8e83329a1datagen2026-10-12 12:01 UTC325
        self.standing.shame += 3;
8b8e83329a1datagen2026-10-12 12:01 UTC326
        Err(self.mum_strike(turned_away(
782069860236atagen2026-10-10 12:44 UTC327
            "excuse-rejected",
8b8e83329a1datagen2026-10-12 12:01 UTC328
            if digest {
8b8e83329a1datagen2026-10-12 12:01 UTC329
                "Mum has not heard that one before. She does not believe it. She would have preferred the truth."
8b8e83329a1datagen2026-10-12 12:01 UTC330
            } else {
8b8e83329a1datagen2026-10-12 12:01 UTC331
                "Mum has not heard that one before. She does not believe it."
8b8e83329a1datagen2026-10-12 12:01 UTC332
            },
8b8e83329a1datagen2026-10-12 12:01 UTC333
        )))
8b8e83329a1datagen2026-10-12 12:01 UTC334
    }
8b8e83329a1datagen2026-10-12 12:01 UTC335
8b8e83329a1datagen2026-10-12 12:01 UTC336
    fn digest_username(&mut self, authorization: &str) -> Result<String, Problem> {
8b8e83329a1datagen2026-10-12 12:01 UTC337
        let Some(mut params) = digest_params(authorization) else {
8b8e83329a1datagen2026-10-12 12:01 UTC338
            return Err(self.mum_strike(turned_away(
8b8e83329a1datagen2026-10-12 12:01 UTC339
                "digest-required",
8b8e83329a1datagen2026-10-12 12:01 UTC340
                "Mum has started using Digest authentication.",
8b8e83329a1datagen2026-10-12 12:01 UTC341
            )));
8b8e83329a1datagen2026-10-12 12:01 UTC342
        };
8b8e83329a1datagen2026-10-12 12:01 UTC343
        let current = self
8b8e83329a1datagen2026-10-12 12:01 UTC344
            .household
8b8e83329a1datagen2026-10-12 12:01 UTC345
            .mum
8b8e83329a1datagen2026-10-12 12:01 UTC346
            .as_ref()
8b8e83329a1datagen2026-10-12 12:01 UTC347
            .is_some_and(|visit| params.get("nonce") == Some(&visit.nonce));
8b8e83329a1datagen2026-10-12 12:01 UTC348
        if !current {
886159fd0fe7atagen2026-10-13 12:48 UTC349
            self.touch();
886159fd0fe7atagen2026-10-13 12:48 UTC350
            return Err(turned_away(
8b8e83329a1datagen2026-10-12 12:01 UTC351
                "stale-nonce",
886159fd0fe7atagen2026-10-13 12:48 UTC352
                "That was a different conversation. Mum has started a new one, with a new nonce.",
886159fd0fe7atagen2026-10-13 12:48 UTC353
            ));
8b8e83329a1datagen2026-10-12 12:01 UTC354
        }
8b8e83329a1datagen2026-10-12 12:01 UTC355
        Ok(params.remove("username").unwrap_or_default())
8b8e83329a1datagen2026-10-12 12:01 UTC356
    }
8b8e83329a1datagen2026-10-12 12:01 UTC357
8b8e83329a1datagen2026-10-12 12:01 UTC358
    fn confess(&mut self) {
8b8e83329a1datagen2026-10-12 12:01 UTC359
        self.standing.shame += 10;
8b8e83329a1datagen2026-10-12 12:01 UTC360
        self.leave_door();
782069860236atagen2026-10-10 12:44 UTC361
    }
782069860236atagen2026-10-10 12:44 UTC362
9683736a2c5eatagen2026-10-10 13:15 UTC363
    fn mum_strike(&mut self, problem: Problem) -> Problem {
0d604ee39751atagen2026-10-10 14:15 UTC364
        self.strike(problem, Cause::Caught)
0d604ee39751atagen2026-10-10 14:15 UTC365
    }
0d604ee39751atagen2026-10-10 14:15 UTC366
0d604ee39751atagen2026-10-10 14:15 UTC367
    fn strike(&mut self, problem: Problem, cause: Cause) -> Problem {
782069860236atagen2026-10-10 12:44 UTC368
        self.touch();
8b8e83329a1datagen2026-10-12 12:01 UTC369
        let Some(visit) = &mut self.household.mum else {
782069860236atagen2026-10-10 12:44 UTC370
            return problem;
782069860236atagen2026-10-10 12:44 UTC371
        };
782069860236atagen2026-10-10 12:44 UTC372
        visit.strikes += 1;
782069860236atagen2026-10-10 12:44 UTC373
        if visit.strikes < PATIENCE {
782069860236atagen2026-10-10 12:44 UTC374
            return problem;
782069860236atagen2026-10-10 12:44 UTC375
        }
0d604ee39751atagen2026-10-10 14:15 UTC376
        self.end(cause)
782069860236atagen2026-10-10 12:44 UTC377
    }
782069860236atagen2026-10-10 12:44 UTC378
782069860236atagen2026-10-10 12:44 UTC379
    fn leave_door(&mut self) {
886159fd0fe7atagen2026-10-13 12:48 UTC380
        if let Some(visit) = self.household.mum.take() {
886159fd0fe7atagen2026-10-13 12:48 UTC381
            self.notices
886159fd0fe7atagen2026-10-13 12:48 UTC382
                .push(Notice::MumSatisfied(visit.errand.parting));
886159fd0fe7atagen2026-10-13 12:48 UTC383
        }
8b8e83329a1datagen2026-10-12 12:01 UTC384
        self.standing.suspicion = 0;
782069860236atagen2026-10-10 12:44 UTC385
        self.touch();
782069860236atagen2026-10-10 12:44 UTC386
    }
782069860236atagen2026-10-10 12:44 UTC387
}