atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
c4d70621d377d45e3d418ac9197443c1b5d36399
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::{
782069860236atagen2026-10-10 12:44 UTC6
    Cause, Floor, Run,
782069860236atagen2026-10-10 12:44 UTC7
    balance::{PATIENCE, VIRAL_ERRAND},
782069860236atagen2026-10-10 12:44 UTC8
    parse_credentials,
782069860236atagen2026-10-10 12:44 UTC9
};
782069860236atagen2026-10-10 12:44 UTC10
use crate::problem::Problem;
782069860236atagen2026-10-10 12:44 UTC11
7d3fb63208bfatagen2026-10-11 04:38 UTC12
const EXCUSES: [&str; 7] = [
7d3fb63208bfatagen2026-10-11 04:38 UTC13
    "changing",
7d3fb63208bfatagen2026-10-11 04:38 UTC14
    "homework",
7d3fb63208bfatagen2026-10-11 04:38 UTC15
    "pulled a muscle",
7d3fb63208bfatagen2026-10-11 04:38 UTC16
    "tidying",
7d3fb63208bfatagen2026-10-11 04:38 UTC17
    "the washing up",
7d3fb63208bfatagen2026-10-11 04:38 UTC18
    "fed the cat",
7d3fb63208bfatagen2026-10-11 04:38 UTC19
    "revising",
7d3fb63208bfatagen2026-10-11 04:38 UTC20
];
7d3fb63208bfatagen2026-10-11 04:38 UTC21
const CHRISTMAS_EXCUSES: [&str; 6] = [
7d3fb63208bfatagen2026-10-11 04:38 UTC22
    "on a call",
7d3fb63208bfatagen2026-10-11 04:38 UTC23
    "wrapping presents",
7d3fb63208bfatagen2026-10-11 04:38 UTC24
    "folding the washing",
7d3fb63208bfatagen2026-10-11 04:38 UTC25
    "hanging the lights",
7d3fb63208bfatagen2026-10-11 04:38 UTC26
    "helping Nan",
7d3fb63208bfatagen2026-10-11 04:38 UTC27
    "peeling sprouts",
7d3fb63208bfatagen2026-10-11 04:38 UTC28
];
c4d70621d377atagen2026-10-12 11:56 UTC29
/// Every excuse Mum might hear, so a localised one can be traced back to the phrase she checks.
c4d70621d377atagen2026-10-12 11:56 UTC30
pub fn vocabulary() -> impl Iterator<Item = &'static str> {
c4d70621d377atagen2026-10-12 11:56 UTC31
    EXCUSES.into_iter().chain(CHRISTMAS_EXCUSES)
c4d70621d377atagen2026-10-12 11:56 UTC32
}
c4d70621d377atagen2026-10-12 11:56 UTC33
782069860236atagen2026-10-10 12:44 UTC34
const TRUTHS: [&str; 3] = ["the truth", "truth", "honesty"];
782069860236atagen2026-10-10 12:44 UTC35
const TRUE_WHEN_PULLED: &str = "pulled a muscle";
782069860236atagen2026-10-10 12:44 UTC36
782069860236atagen2026-10-10 12:44 UTC37
const BEDROOM_ERRANDS: [&str; 4] = [
9683736a2c5eatagen2026-10-10 13:15 UTC38
    "She has brought your washing.",
782069860236atagen2026-10-10 12:44 UTC39
    "She wants to know if you have eaten.",
782069860236atagen2026-10-10 12:44 UTC40
    "She needs the wifi password.",
782069860236atagen2026-10-10 12:44 UTC41
    "She would like to talk about your future.",
782069860236atagen2026-10-10 12:44 UTC42
];
9683736a2c5eatagen2026-10-10 13:15 UTC43
const CHRISTMAS_ERRANDS: [&str; 4] = [
9683736a2c5eatagen2026-10-10 13:15 UTC44
    "Nan would like to see you.",
9683736a2c5eatagen2026-10-10 13:15 UTC45
    "Christmas dinner is in ten minutes.",
9683736a2c5eatagen2026-10-10 13:15 UTC46
    "The King's speech is about to start.",
782069860236atagen2026-10-10 12:44 UTC47
    "An uncle has asked where you went.",
782069860236atagen2026-10-10 12:44 UTC48
];
782069860236atagen2026-10-10 12:44 UTC49
782069860236atagen2026-10-10 12:44 UTC50
fn digest_params(authorization: &str) -> Option<HashMap<String, String>> {
782069860236atagen2026-10-10 12:44 UTC51
    let (scheme, rest) = authorization.trim().split_once(' ')?;
782069860236atagen2026-10-10 12:44 UTC52
    if !scheme.eq_ignore_ascii_case("digest") {
782069860236atagen2026-10-10 12:44 UTC53
        return None;
782069860236atagen2026-10-10 12:44 UTC54
    }
782069860236atagen2026-10-10 12:44 UTC55
    Some(
782069860236atagen2026-10-10 12:44 UTC56
        rest.split(',')
782069860236atagen2026-10-10 12:44 UTC57
            .filter_map(|pair| {
782069860236atagen2026-10-10 12:44 UTC58
                let (key, value) = pair.split_once('=')?;
782069860236atagen2026-10-10 12:44 UTC59
                Some((
782069860236atagen2026-10-10 12:44 UTC60
                    key.trim().to_ascii_lowercase(),
782069860236atagen2026-10-10 12:44 UTC61
                    value.trim().trim_matches('"').to_owned(),
782069860236atagen2026-10-10 12:44 UTC62
                ))
782069860236atagen2026-10-10 12:44 UTC63
            })
782069860236atagen2026-10-10 12:44 UTC64
            .collect(),
782069860236atagen2026-10-10 12:44 UTC65
    )
782069860236atagen2026-10-10 12:44 UTC66
}
782069860236atagen2026-10-10 12:44 UTC67
782069860236atagen2026-10-10 12:44 UTC68
fn spoken(authorization: &str) -> String {
782069860236atagen2026-10-10 12:44 UTC69
    authorization
782069860236atagen2026-10-10 12:44 UTC70
        .split_once(' ')
782069860236atagen2026-10-10 12:44 UTC71
        .map(|(_, rest)| rest.trim().trim_matches('"').to_ascii_lowercase())
782069860236atagen2026-10-10 12:44 UTC72
        .unwrap_or_default()
782069860236atagen2026-10-10 12:44 UTC73
}
782069860236atagen2026-10-10 12:44 UTC74
782069860236atagen2026-10-10 12:44 UTC75
impl Run {
782069860236atagen2026-10-10 12:44 UTC76
    pub fn excuses(&self) -> Vec<&'static str> {
9683736a2c5eatagen2026-10-10 13:15 UTC77
        if self.floor.number() < Floor::Nans.number() {
782069860236atagen2026-10-10 12:44 UTC78
            return EXCUSES.to_vec();
782069860236atagen2026-10-10 12:44 UTC79
        }
782069860236atagen2026-10-10 12:44 UTC80
        EXCUSES
782069860236atagen2026-10-10 12:44 UTC81
            .into_iter()
9683736a2c5eatagen2026-10-10 13:15 UTC82
            .chain(CHRISTMAS_EXCUSES)
c4d70621d377atagen2026-10-12 11:56 UTC83
            .filter(|e| !self.heard.iter().any(|h| h.eq_ignore_ascii_case(e)))
782069860236atagen2026-10-10 12:44 UTC84
            .collect()
782069860236atagen2026-10-10 12:44 UTC85
    }
782069860236atagen2026-10-10 12:44 UTC86
782069860236atagen2026-10-10 12:44 UTC87
    pub(super) fn errand(&mut self, summoned: bool) -> &'static str {
782069860236atagen2026-10-10 12:44 UTC88
        if summoned {
782069860236atagen2026-10-10 12:44 UTC89
            return "She heard a noise.";
782069860236atagen2026-10-10 12:44 UTC90
        }
3f9091557ec3atagen2026-10-11 11:43 UTC91
        if self.filmed_at.is_some()
3f9091557ec3atagen2026-10-11 11:43 UTC92
            && self.chance(
3f9091557ec3atagen2026-10-11 11:43 UTC93
                self.decision("viral", "Has a neighbour already sent Mum the footage?"),
3f9091557ec3atagen2026-10-11 11:43 UTC94
                VIRAL_ERRAND,
3f9091557ec3atagen2026-10-11 11:43 UTC95
            )
3f9091557ec3atagen2026-10-11 11:43 UTC96
        {
782069860236atagen2026-10-10 12:44 UTC97
            return "A neighbour has sent her something.";
782069860236atagen2026-10-10 12:44 UTC98
        }
9683736a2c5eatagen2026-10-10 13:15 UTC99
        let errands = if self.floor == Floor::Nans {
9683736a2c5eatagen2026-10-10 13:15 UTC100
            CHRISTMAS_ERRANDS
782069860236atagen2026-10-10 12:44 UTC101
        } else {
782069860236atagen2026-10-10 12:44 UTC102
            BEDROOM_ERRANDS
782069860236atagen2026-10-10 12:44 UTC103
        };
3f9091557ec3atagen2026-10-11 11:43 UTC104
        let weights = vec![1; errands.len()];
3f9091557ec3atagen2026-10-11 11:43 UTC105
        let i = self.oracle.pick(
3f9091557ec3atagen2026-10-11 11:43 UTC106
            self.decision("errand", "What has brought Mum to the door?"),
3f9091557ec3atagen2026-10-11 11:43 UTC107
            &weights,
3f9091557ec3atagen2026-10-11 11:43 UTC108
            &errands,
3f9091557ec3atagen2026-10-11 11:43 UTC109
        );
3f9091557ec3atagen2026-10-11 11:43 UTC110
        errands[i]
782069860236atagen2026-10-10 12:44 UTC111
    }
782069860236atagen2026-10-10 12:44 UTC112
9683736a2c5eatagen2026-10-10 13:15 UTC113
    /// Mum answers every request on the run until she has been given something she believes.
782069860236atagen2026-10-10 12:44 UTC114
    pub fn answer_door(&mut self, authorization: Option<&str>) -> Result<(), Problem> {
9683736a2c5eatagen2026-10-10 13:15 UTC115
        let Some((errand, nonce)) = self.mum.as_ref().map(|v| (v.errand, v.nonce.clone())) else {
782069860236atagen2026-10-10 12:44 UTC116
            return Ok(());
782069860236atagen2026-10-10 12:44 UTC117
        };
9683736a2c5eatagen2026-10-10 13:15 UTC118
        let digest = self.floor == Floor::Nans;
782069860236atagen2026-10-10 12:44 UTC119
        let challenge_value = if digest {
782069860236atagen2026-10-10 12:44 UTC120
            format!("Digest realm=\"household\", nonce=\"{nonce}\"")
782069860236atagen2026-10-10 12:44 UTC121
        } else {
782069860236atagen2026-10-10 12:44 UTC122
            "Excuse realm=\"household\"".to_owned()
782069860236atagen2026-10-10 12:44 UTC123
        };
782069860236atagen2026-10-10 12:44 UTC124
        let challenge = |p: Problem| p.header(header::WWW_AUTHENTICATE, &challenge_value);
782069860236atagen2026-10-10 12:44 UTC125
782069860236atagen2026-10-10 12:44 UTC126
        let Some(authorization) = authorization else {
0d604ee39751atagen2026-10-10 14:15 UTC127
            let knocked_before = self.mum.as_ref().is_some_and(|visit| visit.knocked);
0d604ee39751atagen2026-10-10 14:15 UTC128
            if let Some(visit) = &mut self.mum {
0d604ee39751atagen2026-10-10 14:15 UTC129
                visit.knocked = true;
0d604ee39751atagen2026-10-10 14:15 UTC130
            }
0d604ee39751atagen2026-10-10 14:15 UTC131
            if !knocked_before {
0d604ee39751atagen2026-10-10 14:15 UTC132
                self.touch();
0d604ee39751atagen2026-10-10 14:15 UTC133
                return Err(challenge(
0d604ee39751atagen2026-10-10 14:15 UTC134
                    Problem::new(
0d604ee39751atagen2026-10-10 14:15 UTC135
                        StatusCode::UNAUTHORIZED,
0d604ee39751atagen2026-10-10 14:15 UTC136
                        "someone-at-the-door",
0d604ee39751atagen2026-10-10 14:15 UTC137
                        format!("Someone is at the door. {errand}"),
0d604ee39751atagen2026-10-10 14:15 UTC138
                    )
0d604ee39751atagen2026-10-10 14:15 UTC139
                    .with("inventory", self.path("/inventory")),
0d604ee39751atagen2026-10-10 14:15 UTC140
                ));
0d604ee39751atagen2026-10-10 14:15 UTC141
            }
0d604ee39751atagen2026-10-10 14:15 UTC142
            return Err(self.strike(
0d604ee39751atagen2026-10-10 14:15 UTC143
                challenge(Problem::new(
782069860236atagen2026-10-10 12:44 UTC144
                    StatusCode::UNAUTHORIZED,
0d604ee39751atagen2026-10-10 14:15 UTC145
                    "knocking-again",
0d604ee39751atagen2026-10-10 14:15 UTC146
                    format!("Mum is knocking again. {errand}"),
0d604ee39751atagen2026-10-10 14:15 UTC147
                )),
0d604ee39751atagen2026-10-10 14:15 UTC148
                Cause::Opened,
782069860236atagen2026-10-10 12:44 UTC149
            ));
782069860236atagen2026-10-10 12:44 UTC150
        };
782069860236atagen2026-10-10 12:44 UTC151
782069860236atagen2026-10-10 12:44 UTC152
        if TRUTHS.contains(&spoken(authorization).as_str()) {
782069860236atagen2026-10-10 12:44 UTC153
            self.shame += 10;
782069860236atagen2026-10-10 12:44 UTC154
            self.leave_door();
782069860236atagen2026-10-10 12:44 UTC155
            return Ok(());
782069860236atagen2026-10-10 12:44 UTC156
        }
782069860236atagen2026-10-10 12:44 UTC157
        if parse_credentials(authorization, "basic").is_some() {
9683736a2c5eatagen2026-10-10 13:15 UTC158
            return Err(self.mum_strike(challenge(Problem::new(
782069860236atagen2026-10-10 12:44 UTC159
                StatusCode::UNAUTHORIZED,
782069860236atagen2026-10-10 12:44 UTC160
                "basic",
9683736a2c5eatagen2026-10-10 13:15 UTC161
                "Mum does not accept Basic authentication. Nobody should.",
782069860236atagen2026-10-10 12:44 UTC162
            ))));
782069860236atagen2026-10-10 12:44 UTC163
        }
782069860236atagen2026-10-10 12:44 UTC164
782069860236atagen2026-10-10 12:44 UTC165
        let excuse = if digest {
782069860236atagen2026-10-10 12:44 UTC166
            let Some(params) = digest_params(authorization) else {
9683736a2c5eatagen2026-10-10 13:15 UTC167
                return Err(self.mum_strike(challenge(Problem::new(
782069860236atagen2026-10-10 12:44 UTC168
                    StatusCode::UNAUTHORIZED,
782069860236atagen2026-10-10 12:44 UTC169
                    "digest-required",
9683736a2c5eatagen2026-10-10 13:15 UTC170
                    "Mum has started using Digest authentication.",
782069860236atagen2026-10-10 12:44 UTC171
                ))));
782069860236atagen2026-10-10 12:44 UTC172
            };
782069860236atagen2026-10-10 12:44 UTC173
            if params.get("nonce") != Some(&nonce) {
9683736a2c5eatagen2026-10-10 13:15 UTC174
                return Err(self.mum_strike(challenge(Problem::new(
782069860236atagen2026-10-10 12:44 UTC175
                    StatusCode::UNAUTHORIZED,
782069860236atagen2026-10-10 12:44 UTC176
                    "stale-nonce",
782069860236atagen2026-10-10 12:44 UTC177
                    "That was a different conversation.",
782069860236atagen2026-10-10 12:44 UTC178
                ))));
782069860236atagen2026-10-10 12:44 UTC179
            }
782069860236atagen2026-10-10 12:44 UTC180
            params.get("username").cloned().unwrap_or_default()
782069860236atagen2026-10-10 12:44 UTC181
        } else {
782069860236atagen2026-10-10 12:44 UTC182
            parse_credentials(authorization, "excuse")
782069860236atagen2026-10-10 12:44 UTC183
                .unwrap_or_default()
782069860236atagen2026-10-10 12:44 UTC184
                .to_owned()
782069860236atagen2026-10-10 12:44 UTC185
        };
782069860236atagen2026-10-10 12:44 UTC186
        let excuse = excuse.to_ascii_lowercase();
782069860236atagen2026-10-10 12:44 UTC187
782069860236atagen2026-10-10 12:44 UTC188
        if TRUTHS.contains(&excuse.as_str()) {
782069860236atagen2026-10-10 12:44 UTC189
            self.shame += 10;
782069860236atagen2026-10-10 12:44 UTC190
            self.leave_door();
782069860236atagen2026-10-10 12:44 UTC191
            return Ok(());
782069860236atagen2026-10-10 12:44 UTC192
        }
782069860236atagen2026-10-10 12:44 UTC193
        let true_now = self.conditions.pulled_muscle && excuse == TRUE_WHEN_PULLED;
782069860236atagen2026-10-10 12:44 UTC194
        if digest && self.heard.contains(&excuse) && !true_now {
782069860236atagen2026-10-10 12:44 UTC195
            self.shame += 3;
9683736a2c5eatagen2026-10-10 13:15 UTC196
            return Err(self.mum_strike(challenge(Problem::new(
782069860236atagen2026-10-10 12:44 UTC197
                StatusCode::UNAUTHORIZED,
782069860236atagen2026-10-10 12:44 UTC198
                "excuse-replayed",
9683736a2c5eatagen2026-10-10 13:15 UTC199
                "Mum has heard that one before. She would have preferred the truth.",
782069860236atagen2026-10-10 12:44 UTC200
            ))));
782069860236atagen2026-10-10 12:44 UTC201
        }
c4d70621d377atagen2026-10-12 11:56 UTC202
        if true_now
c4d70621d377atagen2026-10-12 11:56 UTC203
            || self
c4d70621d377atagen2026-10-12 11:56 UTC204
                .excuses()
c4d70621d377atagen2026-10-12 11:56 UTC205
                .iter()
c4d70621d377atagen2026-10-12 11:56 UTC206
                .any(|e| e.eq_ignore_ascii_case(&excuse))
c4d70621d377atagen2026-10-12 11:56 UTC207
        {
782069860236atagen2026-10-10 12:44 UTC208
            self.heard.push(excuse);
782069860236atagen2026-10-10 12:44 UTC209
            self.leave_door();
782069860236atagen2026-10-10 12:44 UTC210
            return Ok(());
782069860236atagen2026-10-10 12:44 UTC211
        }
782069860236atagen2026-10-10 12:44 UTC212
        self.shame += 3;
782069860236atagen2026-10-10 12:44 UTC213
        let detail = if digest {
9683736a2c5eatagen2026-10-10 13:15 UTC214
            "Mum has not heard that one before. She does not believe it. She would have preferred the truth."
782069860236atagen2026-10-10 12:44 UTC215
        } else {
9683736a2c5eatagen2026-10-10 13:15 UTC216
            "Mum has not heard that one before. She does not believe it."
782069860236atagen2026-10-10 12:44 UTC217
        };
9683736a2c5eatagen2026-10-10 13:15 UTC218
        Err(self.mum_strike(challenge(Problem::new(
782069860236atagen2026-10-10 12:44 UTC219
            StatusCode::UNAUTHORIZED,
782069860236atagen2026-10-10 12:44 UTC220
            "excuse-rejected",
782069860236atagen2026-10-10 12:44 UTC221
            detail,
782069860236atagen2026-10-10 12:44 UTC222
        ))))
782069860236atagen2026-10-10 12:44 UTC223
    }
782069860236atagen2026-10-10 12:44 UTC224
9683736a2c5eatagen2026-10-10 13:15 UTC225
    fn mum_strike(&mut self, problem: Problem) -> Problem {
0d604ee39751atagen2026-10-10 14:15 UTC226
        self.strike(problem, Cause::Caught)
0d604ee39751atagen2026-10-10 14:15 UTC227
    }
0d604ee39751atagen2026-10-10 14:15 UTC228
0d604ee39751atagen2026-10-10 14:15 UTC229
    fn strike(&mut self, problem: Problem, cause: Cause) -> Problem {
782069860236atagen2026-10-10 12:44 UTC230
        self.touch();
9683736a2c5eatagen2026-10-10 13:15 UTC231
        let Some(visit) = &mut self.mum else {
782069860236atagen2026-10-10 12:44 UTC232
            return problem;
782069860236atagen2026-10-10 12:44 UTC233
        };
782069860236atagen2026-10-10 12:44 UTC234
        visit.strikes += 1;
782069860236atagen2026-10-10 12:44 UTC235
        if visit.strikes < PATIENCE {
782069860236atagen2026-10-10 12:44 UTC236
            return problem;
782069860236atagen2026-10-10 12:44 UTC237
        }
0d604ee39751atagen2026-10-10 14:15 UTC238
        self.end(cause)
782069860236atagen2026-10-10 12:44 UTC239
    }
782069860236atagen2026-10-10 12:44 UTC240
782069860236atagen2026-10-10 12:44 UTC241
    fn leave_door(&mut self) {
9683736a2c5eatagen2026-10-10 13:15 UTC242
        self.mum = None;
782069860236atagen2026-10-10 12:44 UTC243
        self.suspicion = 0;
7d3fb63208bfatagen2026-10-11 04:38 UTC244
        self.notices
7d3fb63208bfatagen2026-10-11 04:38 UTC245
            .push("Mum seems satisfied. She takes the washing with her.");
782069860236atagen2026-10-10 12:44 UTC246
        self.touch();
782069860236atagen2026-10-10 12:44 UTC247
    }
782069860236atagen2026-10-10 12:44 UTC248
}