atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
782069860236703e2dd3c52bb589b9e2d8d48176
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git

Blame: src/game/household.rs

782069860236atagen2026-10-10 12:44 UTC1
use std::collections::HashMap;
782069860236atagen2026-10-10 12:44 UTC2
782069860236atagen2026-10-10 12:44 UTC3
use axum::http::{StatusCode, header};
782069860236atagen2026-10-10 12:44 UTC4
use rand::Rng;
782069860236atagen2026-10-10 12:44 UTC5
782069860236atagen2026-10-10 12:44 UTC6
use super::{
782069860236atagen2026-10-10 12:44 UTC7
    Cause, Floor, Run,
782069860236atagen2026-10-10 12:44 UTC8
    balance::{PATIENCE, VIRAL_ERRAND},
782069860236atagen2026-10-10 12:44 UTC9
    parse_credentials,
782069860236atagen2026-10-10 12:44 UTC10
};
782069860236atagen2026-10-10 12:44 UTC11
use crate::problem::Problem;
782069860236atagen2026-10-10 12:44 UTC12
782069860236atagen2026-10-10 12:44 UTC13
const EXCUSES: [&str; 3] = ["changing", "homework", "pulled a muscle"];
782069860236atagen2026-10-10 12:44 UTC14
const THANKSGIVING_EXCUSES: [&str; 3] = ["on a call", "folding laundry", "praying"];
782069860236atagen2026-10-10 12:44 UTC15
const TRUTHS: [&str; 3] = ["the truth", "truth", "honesty"];
782069860236atagen2026-10-10 12:44 UTC16
const TRUE_WHEN_PULLED: &str = "pulled a muscle";
782069860236atagen2026-10-10 12:44 UTC17
782069860236atagen2026-10-10 12:44 UTC18
const BEDROOM_ERRANDS: [&str; 4] = [
782069860236atagen2026-10-10 12:44 UTC19
    "She has brought laundry.",
782069860236atagen2026-10-10 12:44 UTC20
    "She wants to know if you have eaten.",
782069860236atagen2026-10-10 12:44 UTC21
    "She needs the wifi password.",
782069860236atagen2026-10-10 12:44 UTC22
    "She would like to talk about your future.",
782069860236atagen2026-10-10 12:44 UTC23
];
782069860236atagen2026-10-10 12:44 UTC24
const THANKSGIVING_ERRANDS: [&str; 4] = [
782069860236atagen2026-10-10 12:44 UTC25
    "Grandma would like to see you.",
782069860236atagen2026-10-10 12:44 UTC26
    "Dinner is in ten minutes.",
782069860236atagen2026-10-10 12:44 UTC27
    "She needs help with the folding table.",
782069860236atagen2026-10-10 12:44 UTC28
    "An uncle has asked where you went.",
782069860236atagen2026-10-10 12:44 UTC29
];
782069860236atagen2026-10-10 12:44 UTC30
782069860236atagen2026-10-10 12:44 UTC31
fn digest_params(authorization: &str) -> Option<HashMap<String, String>> {
782069860236atagen2026-10-10 12:44 UTC32
    let (scheme, rest) = authorization.trim().split_once(' ')?;
782069860236atagen2026-10-10 12:44 UTC33
    if !scheme.eq_ignore_ascii_case("digest") {
782069860236atagen2026-10-10 12:44 UTC34
        return None;
782069860236atagen2026-10-10 12:44 UTC35
    }
782069860236atagen2026-10-10 12:44 UTC36
    Some(
782069860236atagen2026-10-10 12:44 UTC37
        rest.split(',')
782069860236atagen2026-10-10 12:44 UTC38
            .filter_map(|pair| {
782069860236atagen2026-10-10 12:44 UTC39
                let (key, value) = pair.split_once('=')?;
782069860236atagen2026-10-10 12:44 UTC40
                Some((
782069860236atagen2026-10-10 12:44 UTC41
                    key.trim().to_ascii_lowercase(),
782069860236atagen2026-10-10 12:44 UTC42
                    value.trim().trim_matches('"').to_owned(),
782069860236atagen2026-10-10 12:44 UTC43
                ))
782069860236atagen2026-10-10 12:44 UTC44
            })
782069860236atagen2026-10-10 12:44 UTC45
            .collect(),
782069860236atagen2026-10-10 12:44 UTC46
    )
782069860236atagen2026-10-10 12:44 UTC47
}
782069860236atagen2026-10-10 12:44 UTC48
782069860236atagen2026-10-10 12:44 UTC49
fn spoken(authorization: &str) -> String {
782069860236atagen2026-10-10 12:44 UTC50
    authorization
782069860236atagen2026-10-10 12:44 UTC51
        .split_once(' ')
782069860236atagen2026-10-10 12:44 UTC52
        .map(|(_, rest)| rest.trim().trim_matches('"').to_ascii_lowercase())
782069860236atagen2026-10-10 12:44 UTC53
        .unwrap_or_default()
782069860236atagen2026-10-10 12:44 UTC54
}
782069860236atagen2026-10-10 12:44 UTC55
782069860236atagen2026-10-10 12:44 UTC56
impl Run {
782069860236atagen2026-10-10 12:44 UTC57
    pub fn excuses(&self) -> Vec<&'static str> {
782069860236atagen2026-10-10 12:44 UTC58
        if self.floor.number() < Floor::Grandmas.number() {
782069860236atagen2026-10-10 12:44 UTC59
            return EXCUSES.to_vec();
782069860236atagen2026-10-10 12:44 UTC60
        }
782069860236atagen2026-10-10 12:44 UTC61
        EXCUSES
782069860236atagen2026-10-10 12:44 UTC62
            .into_iter()
782069860236atagen2026-10-10 12:44 UTC63
            .chain(THANKSGIVING_EXCUSES)
782069860236atagen2026-10-10 12:44 UTC64
            .filter(|e| !self.heard.iter().any(|h| h == e))
782069860236atagen2026-10-10 12:44 UTC65
            .collect()
782069860236atagen2026-10-10 12:44 UTC66
    }
782069860236atagen2026-10-10 12:44 UTC67
782069860236atagen2026-10-10 12:44 UTC68
    pub(super) fn errand(&mut self, summoned: bool) -> &'static str {
782069860236atagen2026-10-10 12:44 UTC69
        if summoned {
782069860236atagen2026-10-10 12:44 UTC70
            return "She heard a noise.";
782069860236atagen2026-10-10 12:44 UTC71
        }
782069860236atagen2026-10-10 12:44 UTC72
        if self.filmed_at.is_some() && self.chance(VIRAL_ERRAND) {
782069860236atagen2026-10-10 12:44 UTC73
            return "A neighbour has sent her something.";
782069860236atagen2026-10-10 12:44 UTC74
        }
782069860236atagen2026-10-10 12:44 UTC75
        let errands = if self.floor == Floor::Grandmas {
782069860236atagen2026-10-10 12:44 UTC76
            THANKSGIVING_ERRANDS
782069860236atagen2026-10-10 12:44 UTC77
        } else {
782069860236atagen2026-10-10 12:44 UTC78
            BEDROOM_ERRANDS
782069860236atagen2026-10-10 12:44 UTC79
        };
782069860236atagen2026-10-10 12:44 UTC80
        errands[self.rng.random_range(0..errands.len())]
782069860236atagen2026-10-10 12:44 UTC81
    }
782069860236atagen2026-10-10 12:44 UTC82
782069860236atagen2026-10-10 12:44 UTC83
    /// Mom answers every request on the run until she has been given something she believes.
782069860236atagen2026-10-10 12:44 UTC84
    pub fn answer_door(&mut self, authorization: Option<&str>) -> Result<(), Problem> {
782069860236atagen2026-10-10 12:44 UTC85
        let Some((errand, nonce)) = self.mom.as_ref().map(|v| (v.errand, v.nonce.clone())) else {
782069860236atagen2026-10-10 12:44 UTC86
            return Ok(());
782069860236atagen2026-10-10 12:44 UTC87
        };
782069860236atagen2026-10-10 12:44 UTC88
        let digest = self.floor == Floor::Grandmas;
782069860236atagen2026-10-10 12:44 UTC89
        let challenge_value = if digest {
782069860236atagen2026-10-10 12:44 UTC90
            format!("Digest realm=\"household\", nonce=\"{nonce}\"")
782069860236atagen2026-10-10 12:44 UTC91
        } else {
782069860236atagen2026-10-10 12:44 UTC92
            "Excuse realm=\"household\"".to_owned()
782069860236atagen2026-10-10 12:44 UTC93
        };
782069860236atagen2026-10-10 12:44 UTC94
        let challenge = |p: Problem| p.header(header::WWW_AUTHENTICATE, &challenge_value);
782069860236atagen2026-10-10 12:44 UTC95
782069860236atagen2026-10-10 12:44 UTC96
        let Some(authorization) = authorization else {
782069860236atagen2026-10-10 12:44 UTC97
            return Err(challenge(
782069860236atagen2026-10-10 12:44 UTC98
                Problem::new(
782069860236atagen2026-10-10 12:44 UTC99
                    StatusCode::UNAUTHORIZED,
782069860236atagen2026-10-10 12:44 UTC100
                    "someone-at-the-door",
782069860236atagen2026-10-10 12:44 UTC101
                    format!("Someone is at the door. {errand}"),
782069860236atagen2026-10-10 12:44 UTC102
                )
782069860236atagen2026-10-10 12:44 UTC103
                .with("inventory", self.path("/inventory")),
782069860236atagen2026-10-10 12:44 UTC104
            ));
782069860236atagen2026-10-10 12:44 UTC105
        };
782069860236atagen2026-10-10 12:44 UTC106
782069860236atagen2026-10-10 12:44 UTC107
        if TRUTHS.contains(&spoken(authorization).as_str()) {
782069860236atagen2026-10-10 12:44 UTC108
            self.shame += 10;
782069860236atagen2026-10-10 12:44 UTC109
            self.leave_door();
782069860236atagen2026-10-10 12:44 UTC110
            return Ok(());
782069860236atagen2026-10-10 12:44 UTC111
        }
782069860236atagen2026-10-10 12:44 UTC112
        if parse_credentials(authorization, "basic").is_some() {
782069860236atagen2026-10-10 12:44 UTC113
            return Err(self.mom_strike(challenge(Problem::new(
782069860236atagen2026-10-10 12:44 UTC114
                StatusCode::UNAUTHORIZED,
782069860236atagen2026-10-10 12:44 UTC115
                "basic",
782069860236atagen2026-10-10 12:44 UTC116
                "Mom does not accept Basic authentication. Nobody should.",
782069860236atagen2026-10-10 12:44 UTC117
            ))));
782069860236atagen2026-10-10 12:44 UTC118
        }
782069860236atagen2026-10-10 12:44 UTC119
782069860236atagen2026-10-10 12:44 UTC120
        let excuse = if digest {
782069860236atagen2026-10-10 12:44 UTC121
            let Some(params) = digest_params(authorization) else {
782069860236atagen2026-10-10 12:44 UTC122
                return Err(self.mom_strike(challenge(Problem::new(
782069860236atagen2026-10-10 12:44 UTC123
                    StatusCode::UNAUTHORIZED,
782069860236atagen2026-10-10 12:44 UTC124
                    "digest-required",
782069860236atagen2026-10-10 12:44 UTC125
                    "Mom has started using Digest authentication.",
782069860236atagen2026-10-10 12:44 UTC126
                ))));
782069860236atagen2026-10-10 12:44 UTC127
            };
782069860236atagen2026-10-10 12:44 UTC128
            if params.get("nonce") != Some(&nonce) {
782069860236atagen2026-10-10 12:44 UTC129
                return Err(self.mom_strike(challenge(Problem::new(
782069860236atagen2026-10-10 12:44 UTC130
                    StatusCode::UNAUTHORIZED,
782069860236atagen2026-10-10 12:44 UTC131
                    "stale-nonce",
782069860236atagen2026-10-10 12:44 UTC132
                    "That was a different conversation.",
782069860236atagen2026-10-10 12:44 UTC133
                ))));
782069860236atagen2026-10-10 12:44 UTC134
            }
782069860236atagen2026-10-10 12:44 UTC135
            params.get("username").cloned().unwrap_or_default()
782069860236atagen2026-10-10 12:44 UTC136
        } else {
782069860236atagen2026-10-10 12:44 UTC137
            parse_credentials(authorization, "excuse")
782069860236atagen2026-10-10 12:44 UTC138
                .unwrap_or_default()
782069860236atagen2026-10-10 12:44 UTC139
                .to_owned()
782069860236atagen2026-10-10 12:44 UTC140
        };
782069860236atagen2026-10-10 12:44 UTC141
        let excuse = excuse.to_ascii_lowercase();
782069860236atagen2026-10-10 12:44 UTC142
782069860236atagen2026-10-10 12:44 UTC143
        if TRUTHS.contains(&excuse.as_str()) {
782069860236atagen2026-10-10 12:44 UTC144
            self.shame += 10;
782069860236atagen2026-10-10 12:44 UTC145
            self.leave_door();
782069860236atagen2026-10-10 12:44 UTC146
            return Ok(());
782069860236atagen2026-10-10 12:44 UTC147
        }
782069860236atagen2026-10-10 12:44 UTC148
        let true_now = self.conditions.pulled_muscle && excuse == TRUE_WHEN_PULLED;
782069860236atagen2026-10-10 12:44 UTC149
        if digest && self.heard.contains(&excuse) && !true_now {
782069860236atagen2026-10-10 12:44 UTC150
            self.shame += 3;
782069860236atagen2026-10-10 12:44 UTC151
            return Err(self.mom_strike(challenge(Problem::new(
782069860236atagen2026-10-10 12:44 UTC152
                StatusCode::UNAUTHORIZED,
782069860236atagen2026-10-10 12:44 UTC153
                "excuse-replayed",
782069860236atagen2026-10-10 12:44 UTC154
                "Mom has heard that one before. She would have preferred the truth.",
782069860236atagen2026-10-10 12:44 UTC155
            ))));
782069860236atagen2026-10-10 12:44 UTC156
        }
782069860236atagen2026-10-10 12:44 UTC157
        if true_now || self.excuses().contains(&excuse.as_str()) {
782069860236atagen2026-10-10 12:44 UTC158
            self.heard.push(excuse);
782069860236atagen2026-10-10 12:44 UTC159
            self.leave_door();
782069860236atagen2026-10-10 12:44 UTC160
            return Ok(());
782069860236atagen2026-10-10 12:44 UTC161
        }
782069860236atagen2026-10-10 12:44 UTC162
        self.shame += 3;
782069860236atagen2026-10-10 12:44 UTC163
        let detail = if digest {
782069860236atagen2026-10-10 12:44 UTC164
            "Mom has not heard that one before. She does not believe it. She would have preferred the truth."
782069860236atagen2026-10-10 12:44 UTC165
        } else {
782069860236atagen2026-10-10 12:44 UTC166
            "Mom has not heard that one before. She does not believe it."
782069860236atagen2026-10-10 12:44 UTC167
        };
782069860236atagen2026-10-10 12:44 UTC168
        Err(self.mom_strike(challenge(Problem::new(
782069860236atagen2026-10-10 12:44 UTC169
            StatusCode::UNAUTHORIZED,
782069860236atagen2026-10-10 12:44 UTC170
            "excuse-rejected",
782069860236atagen2026-10-10 12:44 UTC171
            detail,
782069860236atagen2026-10-10 12:44 UTC172
        ))))
782069860236atagen2026-10-10 12:44 UTC173
    }
782069860236atagen2026-10-10 12:44 UTC174
782069860236atagen2026-10-10 12:44 UTC175
    fn mom_strike(&mut self, problem: Problem) -> Problem {
782069860236atagen2026-10-10 12:44 UTC176
        self.touch();
782069860236atagen2026-10-10 12:44 UTC177
        let Some(visit) = &mut self.mom else {
782069860236atagen2026-10-10 12:44 UTC178
            return problem;
782069860236atagen2026-10-10 12:44 UTC179
        };
782069860236atagen2026-10-10 12:44 UTC180
        visit.strikes += 1;
782069860236atagen2026-10-10 12:44 UTC181
        if visit.strikes < PATIENCE {
782069860236atagen2026-10-10 12:44 UTC182
            return problem;
782069860236atagen2026-10-10 12:44 UTC183
        }
782069860236atagen2026-10-10 12:44 UTC184
        self.end(Cause::Caught)
782069860236atagen2026-10-10 12:44 UTC185
    }
782069860236atagen2026-10-10 12:44 UTC186
782069860236atagen2026-10-10 12:44 UTC187
    fn leave_door(&mut self) {
782069860236atagen2026-10-10 12:44 UTC188
        self.mom = None;
782069860236atagen2026-10-10 12:44 UTC189
        self.suspicion = 0;
782069860236atagen2026-10-10 12:44 UTC190
        self.touch();
782069860236atagen2026-10-10 12:44 UTC191
    }
782069860236atagen2026-10-10 12:44 UTC192
}