atagen

jelq

public

an http based roguelike rpg about jelqing

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