atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
c4d70621d377d45e3d418ac9197443c1b5d36399
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
src/game/household.rsRust248 lines7.9 KB
1
use std::collections::HashMap;
2
3
use axum::http::{StatusCode, header};
4
5
use super::{
6
    Cause, Floor, Run,
7
    balance::{PATIENCE, VIRAL_ERRAND},
8
    parse_credentials,
9
};
10
use crate::problem::Problem;
11
12
const EXCUSES: [&str; 7] = [
13
    "changing",
14
    "homework",
15
    "pulled a muscle",
16
    "tidying",
17
    "the washing up",
18
    "fed the cat",
19
    "revising",
20
];
21
const CHRISTMAS_EXCUSES: [&str; 6] = [
22
    "on a call",
23
    "wrapping presents",
24
    "folding the washing",
25
    "hanging the lights",
26
    "helping Nan",
27
    "peeling sprouts",
28
];
29
/// Every excuse Mum might hear, so a localised one can be traced back to the phrase she checks.
30
pub fn vocabulary() -> impl Iterator<Item = &'static str> {
31
    EXCUSES.into_iter().chain(CHRISTMAS_EXCUSES)
32
}
33
34
const TRUTHS: [&str; 3] = ["the truth", "truth", "honesty"];
35
const TRUE_WHEN_PULLED: &str = "pulled a muscle";
36
37
const BEDROOM_ERRANDS: [&str; 4] = [
38
    "She has brought your washing.",
39
    "She wants to know if you have eaten.",
40
    "She needs the wifi password.",
41
    "She would like to talk about your future.",
42
];
43
const CHRISTMAS_ERRANDS: [&str; 4] = [
44
    "Nan would like to see you.",
45
    "Christmas dinner is in ten minutes.",
46
    "The King's speech is about to start.",
47
    "An uncle has asked where you went.",
48
];
49
50
fn digest_params(authorization: &str) -> Option<HashMap<String, String>> {
51
    let (scheme, rest) = authorization.trim().split_once(' ')?;
52
    if !scheme.eq_ignore_ascii_case("digest") {
53
        return None;
54
    }
55
    Some(
56
        rest.split(',')
57
            .filter_map(|pair| {
58
                let (key, value) = pair.split_once('=')?;
59
                Some((
60
                    key.trim().to_ascii_lowercase(),
61
                    value.trim().trim_matches('"').to_owned(),
62
                ))
63
            })
64
            .collect(),
65
    )
66
}
67
68
fn spoken(authorization: &str) -> String {
69
    authorization
70
        .split_once(' ')
71
        .map(|(_, rest)| rest.trim().trim_matches('"').to_ascii_lowercase())
72
        .unwrap_or_default()
73
}
74
75
impl Run {
76
    pub fn excuses(&self) -> Vec<&'static str> {
77
        if self.floor.number() < Floor::Nans.number() {
78
            return EXCUSES.to_vec();
79
        }
80
        EXCUSES
81
            .into_iter()
82
            .chain(CHRISTMAS_EXCUSES)
83
            .filter(|e| !self.heard.iter().any(|h| h.eq_ignore_ascii_case(e)))
84
            .collect()
85
    }
86
87
    pub(super) fn errand(&mut self, summoned: bool) -> &'static str {
88
        if summoned {
89
            return "She heard a noise.";
90
        }
91
        if self.filmed_at.is_some()
92
            && self.chance(
93
                self.decision("viral", "Has a neighbour already sent Mum the footage?"),
94
                VIRAL_ERRAND,
95
            )
96
        {
97
            return "A neighbour has sent her something.";
98
        }
99
        let errands = if self.floor == Floor::Nans {
100
            CHRISTMAS_ERRANDS
101
        } else {
102
            BEDROOM_ERRANDS
103
        };
104
        let weights = vec![1; errands.len()];
105
        let i = self.oracle.pick(
106
            self.decision("errand", "What has brought Mum to the door?"),
107
            &weights,
108
            &errands,
109
        );
110
        errands[i]
111
    }
112
113
    /// Mum answers every request on the run until she has been given something she believes.
114
    pub fn answer_door(&mut self, authorization: Option<&str>) -> Result<(), Problem> {
115
        let Some((errand, nonce)) = self.mum.as_ref().map(|v| (v.errand, v.nonce.clone())) else {
116
            return Ok(());
117
        };
118
        let digest = self.floor == Floor::Nans;
119
        let challenge_value = if digest {
120
            format!("Digest realm=\"household\", nonce=\"{nonce}\"")
121
        } else {
122
            "Excuse realm=\"household\"".to_owned()
123
        };
124
        let challenge = |p: Problem| p.header(header::WWW_AUTHENTICATE, &challenge_value);
125
126
        let Some(authorization) = authorization else {
127
            let knocked_before = self.mum.as_ref().is_some_and(|visit| visit.knocked);
128
            if let Some(visit) = &mut self.mum {
129
                visit.knocked = true;
130
            }
131
            if !knocked_before {
132
                self.touch();
133
                return Err(challenge(
134
                    Problem::new(
135
                        StatusCode::UNAUTHORIZED,
136
                        "someone-at-the-door",
137
                        format!("Someone is at the door. {errand}"),
138
                    )
139
                    .with("inventory", self.path("/inventory")),
140
                ));
141
            }
142
            return Err(self.strike(
143
                challenge(Problem::new(
144
                    StatusCode::UNAUTHORIZED,
145
                    "knocking-again",
146
                    format!("Mum is knocking again. {errand}"),
147
                )),
148
                Cause::Opened,
149
            ));
150
        };
151
152
        if TRUTHS.contains(&spoken(authorization).as_str()) {
153
            self.shame += 10;
154
            self.leave_door();
155
            return Ok(());
156
        }
157
        if parse_credentials(authorization, "basic").is_some() {
158
            return Err(self.mum_strike(challenge(Problem::new(
159
                StatusCode::UNAUTHORIZED,
160
                "basic",
161
                "Mum does not accept Basic authentication. Nobody should.",
162
            ))));
163
        }
164
165
        let excuse = if digest {
166
            let Some(params) = digest_params(authorization) else {
167
                return Err(self.mum_strike(challenge(Problem::new(
168
                    StatusCode::UNAUTHORIZED,
169
                    "digest-required",
170
                    "Mum has started using Digest authentication.",
171
                ))));
172
            };
173
            if params.get("nonce") != Some(&nonce) {
174
                return Err(self.mum_strike(challenge(Problem::new(
175
                    StatusCode::UNAUTHORIZED,
176
                    "stale-nonce",
177
                    "That was a different conversation.",
178
                ))));
179
            }
180
            params.get("username").cloned().unwrap_or_default()
181
        } else {
182
            parse_credentials(authorization, "excuse")
183
                .unwrap_or_default()
184
                .to_owned()
185
        };
186
        let excuse = excuse.to_ascii_lowercase();
187
188
        if TRUTHS.contains(&excuse.as_str()) {
189
            self.shame += 10;
190
            self.leave_door();
191
            return Ok(());
192
        }
193
        let true_now = self.conditions.pulled_muscle && excuse == TRUE_WHEN_PULLED;
194
        if digest && self.heard.contains(&excuse) && !true_now {
195
            self.shame += 3;
196
            return Err(self.mum_strike(challenge(Problem::new(
197
                StatusCode::UNAUTHORIZED,
198
                "excuse-replayed",
199
                "Mum has heard that one before. She would have preferred the truth.",
200
            ))));
201
        }
202
        if true_now
203
            || self
204
                .excuses()
205
                .iter()
206
                .any(|e| e.eq_ignore_ascii_case(&excuse))
207
        {
208
            self.heard.push(excuse);
209
            self.leave_door();
210
            return Ok(());
211
        }
212
        self.shame += 3;
213
        let detail = if digest {
214
            "Mum has not heard that one before. She does not believe it. She would have preferred the truth."
215
        } else {
216
            "Mum has not heard that one before. She does not believe it."
217
        };
218
        Err(self.mum_strike(challenge(Problem::new(
219
            StatusCode::UNAUTHORIZED,
220
            "excuse-rejected",
221
            detail,
222
        ))))
223
    }
224
225
    fn mum_strike(&mut self, problem: Problem) -> Problem {
226
        self.strike(problem, Cause::Caught)
227
    }
228
229
    fn strike(&mut self, problem: Problem, cause: Cause) -> Problem {
230
        self.touch();
231
        let Some(visit) = &mut self.mum else {
232
            return problem;
233
        };
234
        visit.strikes += 1;
235
        if visit.strikes < PATIENCE {
236
            return problem;
237
        }
238
        self.end(cause)
239
    }
240
241
    fn leave_door(&mut self) {
242
        self.mum = None;
243
        self.suspicion = 0;
244
        self.notices
245
            .push("Mum seems satisfied. She takes the washing with her.");
246
        self.touch();
247
    }
248
}