atagen

jelq

public

an http based roguelike rpg about jelqing

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