atagen

jelq

public

an http based roguelike rpg about jelqing

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