atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
ec6fd4bb5b6028e1fe0ec1bc286e57748b9728dc
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
src/game/household.rsRust211 lines7.0 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
            let knocked_before = self.mum.as_ref().is_some_and(|visit| visit.knocked);
98
            if let Some(visit) = &mut self.mum {
99
                visit.knocked = true;
100
            }
101
            if !knocked_before {
102
                self.touch();
103
                return Err(challenge(
104
                    Problem::new(
105
                        StatusCode::UNAUTHORIZED,
106
                        "someone-at-the-door",
107
                        format!("Someone is at the door. {errand}"),
108
                    )
109
                    .with("inventory", self.path("/inventory")),
110
                ));
111
            }
112
            return Err(self.strike(
113
                challenge(Problem::new(
114
                    StatusCode::UNAUTHORIZED,
115
                    "knocking-again",
116
                    format!("Mum is knocking again. {errand}"),
117
                )),
118
                Cause::Opened,
119
            ));
120
        };
121
122
        if TRUTHS.contains(&spoken(authorization).as_str()) {
123
            self.shame += 10;
124
            self.leave_door();
125
            return Ok(());
126
        }
127
        if parse_credentials(authorization, "basic").is_some() {
128
            return Err(self.mum_strike(challenge(Problem::new(
129
                StatusCode::UNAUTHORIZED,
130
                "basic",
131
                "Mum does not accept Basic authentication. Nobody should.",
132
            ))));
133
        }
134
135
        let excuse = if digest {
136
            let Some(params) = digest_params(authorization) else {
137
                return Err(self.mum_strike(challenge(Problem::new(
138
                    StatusCode::UNAUTHORIZED,
139
                    "digest-required",
140
                    "Mum has started using Digest authentication.",
141
                ))));
142
            };
143
            if params.get("nonce") != Some(&nonce) {
144
                return Err(self.mum_strike(challenge(Problem::new(
145
                    StatusCode::UNAUTHORIZED,
146
                    "stale-nonce",
147
                    "That was a different conversation.",
148
                ))));
149
            }
150
            params.get("username").cloned().unwrap_or_default()
151
        } else {
152
            parse_credentials(authorization, "excuse")
153
                .unwrap_or_default()
154
                .to_owned()
155
        };
156
        let excuse = excuse.to_ascii_lowercase();
157
158
        if TRUTHS.contains(&excuse.as_str()) {
159
            self.shame += 10;
160
            self.leave_door();
161
            return Ok(());
162
        }
163
        let true_now = self.conditions.pulled_muscle && excuse == TRUE_WHEN_PULLED;
164
        if digest && self.heard.contains(&excuse) && !true_now {
165
            self.shame += 3;
166
            return Err(self.mum_strike(challenge(Problem::new(
167
                StatusCode::UNAUTHORIZED,
168
                "excuse-replayed",
169
                "Mum has heard that one before. She would have preferred the truth.",
170
            ))));
171
        }
172
        if true_now || self.excuses().contains(&excuse.as_str()) {
173
            self.heard.push(excuse);
174
            self.leave_door();
175
            return Ok(());
176
        }
177
        self.shame += 3;
178
        let detail = if digest {
179
            "Mum has not heard that one before. She does not believe it. She would have preferred the truth."
180
        } else {
181
            "Mum has not heard that one before. She does not believe it."
182
        };
183
        Err(self.mum_strike(challenge(Problem::new(
184
            StatusCode::UNAUTHORIZED,
185
            "excuse-rejected",
186
            detail,
187
        ))))
188
    }
189
190
    fn mum_strike(&mut self, problem: Problem) -> Problem {
191
        self.strike(problem, Cause::Caught)
192
    }
193
194
    fn strike(&mut self, problem: Problem, cause: Cause) -> Problem {
195
        self.touch();
196
        let Some(visit) = &mut self.mum else {
197
            return problem;
198
        };
199
        visit.strikes += 1;
200
        if visit.strikes < PATIENCE {
201
            return problem;
202
        }
203
        self.end(cause)
204
    }
205
206
    fn leave_door(&mut self) {
207
        self.mum = None;
208
        self.suspicion = 0;
209
        self.touch();
210
    }
211
}