atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
e76ab808e7ad49d0b6fccd17728da137daacdb8c
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
src/game/household.rsRust192 lines6.3 KB
1
use std::collections::HashMap;
2
3
use axum::http::{StatusCode, header};
4
use rand::Rng;
5
6
use super::{
7
    Cause, Floor, Run,
8
    balance::{PATIENCE, VIRAL_ERRAND},
9
    parse_credentials,
10
};
11
use crate::problem::Problem;
12
13
const EXCUSES: [&str; 3] = ["changing", "homework", "pulled a muscle"];
14
const CHRISTMAS_EXCUSES: [&str; 3] = ["on a call", "wrapping presents", "folding the washing"];
15
const TRUTHS: [&str; 3] = ["the truth", "truth", "honesty"];
16
const TRUE_WHEN_PULLED: &str = "pulled a muscle";
17
18
const BEDROOM_ERRANDS: [&str; 4] = [
19
    "She has brought your washing.",
20
    "She wants to know if you have eaten.",
21
    "She needs the wifi password.",
22
    "She would like to talk about your future.",
23
];
24
const CHRISTMAS_ERRANDS: [&str; 4] = [
25
    "Nan would like to see you.",
26
    "Christmas dinner is in ten minutes.",
27
    "The King's speech is about to start.",
28
    "An uncle has asked where you went.",
29
];
30
31
fn digest_params(authorization: &str) -> Option<HashMap<String, String>> {
32
    let (scheme, rest) = authorization.trim().split_once(' ')?;
33
    if !scheme.eq_ignore_ascii_case("digest") {
34
        return None;
35
    }
36
    Some(
37
        rest.split(',')
38
            .filter_map(|pair| {
39
                let (key, value) = pair.split_once('=')?;
40
                Some((
41
                    key.trim().to_ascii_lowercase(),
42
                    value.trim().trim_matches('"').to_owned(),
43
                ))
44
            })
45
            .collect(),
46
    )
47
}
48
49
fn spoken(authorization: &str) -> String {
50
    authorization
51
        .split_once(' ')
52
        .map(|(_, rest)| rest.trim().trim_matches('"').to_ascii_lowercase())
53
        .unwrap_or_default()
54
}
55
56
impl Run {
57
    pub fn excuses(&self) -> Vec<&'static str> {
58
        if self.floor.number() < Floor::Nans.number() {
59
            return EXCUSES.to_vec();
60
        }
61
        EXCUSES
62
            .into_iter()
63
            .chain(CHRISTMAS_EXCUSES)
64
            .filter(|e| !self.heard.iter().any(|h| h == e))
65
            .collect()
66
    }
67
68
    pub(super) fn errand(&mut self, summoned: bool) -> &'static str {
69
        if summoned {
70
            return "She heard a noise.";
71
        }
72
        if self.filmed_at.is_some() && self.chance(VIRAL_ERRAND) {
73
            return "A neighbour has sent her something.";
74
        }
75
        let errands = if self.floor == Floor::Nans {
76
            CHRISTMAS_ERRANDS
77
        } else {
78
            BEDROOM_ERRANDS
79
        };
80
        errands[self.rng.random_range(0..errands.len())]
81
    }
82
83
    /// Mum answers every request on the run until she has been given something she believes.
84
    pub fn answer_door(&mut self, authorization: Option<&str>) -> Result<(), Problem> {
85
        let Some((errand, nonce)) = self.mum.as_ref().map(|v| (v.errand, v.nonce.clone())) else {
86
            return Ok(());
87
        };
88
        let digest = self.floor == Floor::Nans;
89
        let challenge_value = if digest {
90
            format!("Digest realm=\"household\", nonce=\"{nonce}\"")
91
        } else {
92
            "Excuse realm=\"household\"".to_owned()
93
        };
94
        let challenge = |p: Problem| p.header(header::WWW_AUTHENTICATE, &challenge_value);
95
96
        let Some(authorization) = authorization else {
97
            return Err(challenge(
98
                Problem::new(
99
                    StatusCode::UNAUTHORIZED,
100
                    "someone-at-the-door",
101
                    format!("Someone is at the door. {errand}"),
102
                )
103
                .with("inventory", self.path("/inventory")),
104
            ));
105
        };
106
107
        if TRUTHS.contains(&spoken(authorization).as_str()) {
108
            self.shame += 10;
109
            self.leave_door();
110
            return Ok(());
111
        }
112
        if parse_credentials(authorization, "basic").is_some() {
113
            return Err(self.mum_strike(challenge(Problem::new(
114
                StatusCode::UNAUTHORIZED,
115
                "basic",
116
                "Mum does not accept Basic authentication. Nobody should.",
117
            ))));
118
        }
119
120
        let excuse = if digest {
121
            let Some(params) = digest_params(authorization) else {
122
                return Err(self.mum_strike(challenge(Problem::new(
123
                    StatusCode::UNAUTHORIZED,
124
                    "digest-required",
125
                    "Mum has started using Digest authentication.",
126
                ))));
127
            };
128
            if params.get("nonce") != Some(&nonce) {
129
                return Err(self.mum_strike(challenge(Problem::new(
130
                    StatusCode::UNAUTHORIZED,
131
                    "stale-nonce",
132
                    "That was a different conversation.",
133
                ))));
134
            }
135
            params.get("username").cloned().unwrap_or_default()
136
        } else {
137
            parse_credentials(authorization, "excuse")
138
                .unwrap_or_default()
139
                .to_owned()
140
        };
141
        let excuse = excuse.to_ascii_lowercase();
142
143
        if TRUTHS.contains(&excuse.as_str()) {
144
            self.shame += 10;
145
            self.leave_door();
146
            return Ok(());
147
        }
148
        let true_now = self.conditions.pulled_muscle && excuse == TRUE_WHEN_PULLED;
149
        if digest && self.heard.contains(&excuse) && !true_now {
150
            self.shame += 3;
151
            return Err(self.mum_strike(challenge(Problem::new(
152
                StatusCode::UNAUTHORIZED,
153
                "excuse-replayed",
154
                "Mum has heard that one before. She would have preferred the truth.",
155
            ))));
156
        }
157
        if true_now || self.excuses().contains(&excuse.as_str()) {
158
            self.heard.push(excuse);
159
            self.leave_door();
160
            return Ok(());
161
        }
162
        self.shame += 3;
163
        let detail = if digest {
164
            "Mum has not heard that one before. She does not believe it. She would have preferred the truth."
165
        } else {
166
            "Mum has not heard that one before. She does not believe it."
167
        };
168
        Err(self.mum_strike(challenge(Problem::new(
169
            StatusCode::UNAUTHORIZED,
170
            "excuse-rejected",
171
            detail,
172
        ))))
173
    }
174
175
    fn mum_strike(&mut self, problem: Problem) -> Problem {
176
        self.touch();
177
        let Some(visit) = &mut self.mum else {
178
            return problem;
179
        };
180
        visit.strikes += 1;
181
        if visit.strikes < PATIENCE {
182
            return problem;
183
        }
184
        self.end(Cause::Caught)
185
    }
186
187
    fn leave_door(&mut self) {
188
        self.mum = None;
189
        self.suspicion = 0;
190
        self.touch();
191
    }
192
}