atagen

jelq

public

an http based roguelike rpg about jelqing

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

Blame: src/game/mod.rs

dfa38cf4cd4batagen2026-10-10 09:17 UTC1
mod catalog;
dfa38cf4cd4batagen2026-10-10 09:17 UTC2
mod encounters;
dfa38cf4cd4batagen2026-10-10 09:17 UTC3
mod jelq;
dfa38cf4cd4batagen2026-10-10 09:17 UTC4
dfa38cf4cd4batagen2026-10-10 09:17 UTC5
pub use catalog::{LUBRICANTS, Lube, SHOP, ShopItem, Technique};
dfa38cf4cd4batagen2026-10-10 09:17 UTC6
pub use encounters::{ELDER_LAST_MODIFIED, FORUM_THREAD, PatchOp, forum_thread};
dfa38cf4cd4batagen2026-10-10 09:17 UTC7
dfa38cf4cd4batagen2026-10-10 09:17 UTC8
use std::time::{Duration, Instant};
dfa38cf4cd4batagen2026-10-10 09:17 UTC9
dfa38cf4cd4batagen2026-10-10 09:17 UTC10
use axum::{
dfa38cf4cd4batagen2026-10-10 09:17 UTC11
    Json,
dfa38cf4cd4batagen2026-10-10 09:17 UTC12
    http::{HeaderName, HeaderValue, StatusCode, header},
dfa38cf4cd4batagen2026-10-10 09:17 UTC13
    response::{IntoResponse, Response},
dfa38cf4cd4batagen2026-10-10 09:17 UTC14
};
dfa38cf4cd4batagen2026-10-10 09:17 UTC15
use rand::{Rng, SeedableRng, rngs::StdRng};
dfa38cf4cd4batagen2026-10-10 09:17 UTC16
use serde_json::{Value, json};
dfa38cf4cd4batagen2026-10-10 09:17 UTC17
dfa38cf4cd4batagen2026-10-10 09:17 UTC18
use crate::problem::Problem;
dfa38cf4cd4batagen2026-10-10 09:17 UTC19
dfa38cf4cd4batagen2026-10-10 09:17 UTC20
pub const MAX_STAMINA: i32 = 10;
dfa38cf4cd4batagen2026-10-10 09:17 UTC21
pub const SUSPICION_LIMIT: u8 = 5;
dfa38cf4cd4batagen2026-10-10 09:17 UTC22
pub const BRANCH_TURN: u32 = 10;
dfa38cf4cd4batagen2026-10-10 09:17 UTC23
pub const FLOOR_TURNS: u32 = 6;
dfa38cf4cd4batagen2026-10-10 09:17 UTC24
pub const STARTING_CASH: u32 = 20;
dfa38cf4cd4batagen2026-10-10 09:17 UTC25
pub const FLOOR_ALLOWANCE: u32 = 10;
dfa38cf4cd4batagen2026-10-10 09:17 UTC26
pub const REFRACTORY: Duration = Duration::from_secs(30);
dfa38cf4cd4batagen2026-10-10 09:17 UTC27
pub const SELF_REPORT_INFLATION: f64 = 1.5;
dfa38cf4cd4batagen2026-10-10 09:17 UTC28
pub const MM_PER_INCH: f64 = 25.4;
dfa38cf4cd4batagen2026-10-10 09:17 UTC29
pub const RUN_ALLOW: &str = "GET, HEAD, JELQ, PUT, PATCH, DELETE, OPTIONS, TRACE";
dfa38cf4cd4batagen2026-10-10 09:17 UTC30
pub const CLINIC_ALLOW: &str = "GET, HEAD, PUT, PATCH, DELETE, OPTIONS, TRACE";
dfa38cf4cd4batagen2026-10-10 09:17 UTC31
dfa38cf4cd4batagen2026-10-10 09:17 UTC32
pub const SERVER_TIMING: HeaderName = HeaderName::from_static("server-timing");
dfa38cf4cd4batagen2026-10-10 09:17 UTC33
pub const X_DIAGNOSIS: HeaderName = HeaderName::from_static("x-diagnosis");
dfa38cf4cd4batagen2026-10-10 09:17 UTC34
dfa38cf4cd4batagen2026-10-10 09:17 UTC35
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
dfa38cf4cd4batagen2026-10-10 09:17 UTC36
pub enum Floor {
dfa38cf4cd4batagen2026-10-10 09:17 UTC37
    Bedroom,
dfa38cf4cd4batagen2026-10-10 09:17 UTC38
    Bathroom,
dfa38cf4cd4batagen2026-10-10 09:17 UTC39
    LockerRoom,
dfa38cf4cd4batagen2026-10-10 09:17 UTC40
    Grandmas,
dfa38cf4cd4batagen2026-10-10 09:17 UTC41
    Hotel,
dfa38cf4cd4batagen2026-10-10 09:17 UTC42
    Forum,
dfa38cf4cd4batagen2026-10-10 09:17 UTC43
    Clinic,
dfa38cf4cd4batagen2026-10-10 09:17 UTC44
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC45
dfa38cf4cd4batagen2026-10-10 09:17 UTC46
impl Floor {
dfa38cf4cd4batagen2026-10-10 09:17 UTC47
    const ALL: [Self; 7] = [
dfa38cf4cd4batagen2026-10-10 09:17 UTC48
        Self::Bedroom,
dfa38cf4cd4batagen2026-10-10 09:17 UTC49
        Self::Bathroom,
dfa38cf4cd4batagen2026-10-10 09:17 UTC50
        Self::LockerRoom,
dfa38cf4cd4batagen2026-10-10 09:17 UTC51
        Self::Grandmas,
dfa38cf4cd4batagen2026-10-10 09:17 UTC52
        Self::Hotel,
dfa38cf4cd4batagen2026-10-10 09:17 UTC53
        Self::Forum,
dfa38cf4cd4batagen2026-10-10 09:17 UTC54
        Self::Clinic,
dfa38cf4cd4batagen2026-10-10 09:17 UTC55
    ];
dfa38cf4cd4batagen2026-10-10 09:17 UTC56
dfa38cf4cd4batagen2026-10-10 09:17 UTC57
    pub fn number(self) -> usize {
dfa38cf4cd4batagen2026-10-10 09:17 UTC58
        Self::ALL.iter().position(|f| *f == self).unwrap_or(0) + 1
dfa38cf4cd4batagen2026-10-10 09:17 UTC59
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC60
dfa38cf4cd4batagen2026-10-10 09:17 UTC61
    fn next(self) -> Option<Self> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC62
        Self::ALL.get(self.number()).copied()
dfa38cf4cd4batagen2026-10-10 09:17 UTC63
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC64
dfa38cf4cd4batagen2026-10-10 09:17 UTC65
    pub fn name(self) -> &'static str {
dfa38cf4cd4batagen2026-10-10 09:17 UTC66
        match self {
dfa38cf4cd4batagen2026-10-10 09:17 UTC67
            Self::Bedroom => "the bedroom",
dfa38cf4cd4batagen2026-10-10 09:17 UTC68
            Self::Bathroom => "the shared bathroom",
dfa38cf4cd4batagen2026-10-10 09:17 UTC69
            Self::LockerRoom => "the gym locker room",
dfa38cf4cd4batagen2026-10-10 09:17 UTC70
            Self::Grandmas => "grandma's house for Thanksgiving",
dfa38cf4cd4batagen2026-10-10 09:17 UTC71
            Self::Hotel => "a business trip hotel",
dfa38cf4cd4batagen2026-10-10 09:17 UTC72
            Self::Forum => "the forum",
dfa38cf4cd4batagen2026-10-10 09:17 UTC73
            Self::Clinic => "the urologist's office",
dfa38cf4cd4batagen2026-10-10 09:17 UTC74
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC75
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC76
dfa38cf4cd4batagen2026-10-10 09:17 UTC77
    pub fn note(self) -> &'static str {
dfa38cf4cd4batagen2026-10-10 09:17 UTC78
        match self {
dfa38cf4cd4batagen2026-10-10 09:17 UTC79
            Self::Bedroom => "The door does not lock. The cat is present.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC80
            Self::Bathroom => "The water is hot. The roommate showers at eight.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC81
            Self::LockerRoom => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC82
                "It is cold. Discretion is advised. Everyone here is lying. Pump-Bro is by the lockers."
dfa38cf4cd4batagen2026-10-10 09:17 UTC83
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC84
            Self::Grandmas => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC85
                "The walls are thin. Mom is here, and she has been reading about Digest authentication."
dfa38cf4cd4batagen2026-10-10 09:17 UTC86
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC87
            Self::Hotel => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC88
                "The wifi requires agreement. Housekeeping does not knock. Pump-Bro is in the lobby."
dfa38cf4cd4batagen2026-10-10 09:17 UTC89
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC90
            Self::Forum => "Nothing physical happens here. The Elder has a spreadsheet.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC91
            Self::Clinic => "The urologist will see you now. He is bored.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC92
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC93
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC94
dfa38cf4cd4batagen2026-10-10 09:17 UTC95
    fn observer(self) -> Option<&'static str> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC96
        match self {
dfa38cf4cd4batagen2026-10-10 09:17 UTC97
            Self::Bedroom => Some("cat"),
dfa38cf4cd4batagen2026-10-10 09:17 UTC98
            Self::LockerRoom => Some("greg"),
dfa38cf4cd4batagen2026-10-10 09:17 UTC99
            Self::Grandmas => Some("grandma"),
dfa38cf4cd4batagen2026-10-10 09:17 UTC100
            _ => None,
dfa38cf4cd4batagen2026-10-10 09:17 UTC101
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC102
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC103
dfa38cf4cd4batagen2026-10-10 09:17 UTC104
    pub fn shop_open(self) -> bool {
dfa38cf4cd4batagen2026-10-10 09:17 UTC105
        matches!(self, Self::LockerRoom | Self::Hotel)
dfa38cf4cd4batagen2026-10-10 09:17 UTC106
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC107
dfa38cf4cd4batagen2026-10-10 09:17 UTC108
    pub fn has_mom(self) -> bool {
dfa38cf4cd4batagen2026-10-10 09:17 UTC109
        matches!(self, Self::Bedroom | Self::Grandmas)
dfa38cf4cd4batagen2026-10-10 09:17 UTC110
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC111
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC112
dfa38cf4cd4batagen2026-10-10 09:17 UTC113
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
dfa38cf4cd4batagen2026-10-10 09:17 UTC114
pub enum Grip {
dfa38cf4cd4batagen2026-10-10 09:17 UTC115
    Overhand,
dfa38cf4cd4batagen2026-10-10 09:17 UTC116
    Underhand,
dfa38cf4cd4batagen2026-10-10 09:17 UTC117
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC118
dfa38cf4cd4batagen2026-10-10 09:17 UTC119
impl Grip {
dfa38cf4cd4batagen2026-10-10 09:17 UTC120
    pub fn name(self) -> &'static str {
dfa38cf4cd4batagen2026-10-10 09:17 UTC121
        match self {
dfa38cf4cd4batagen2026-10-10 09:17 UTC122
            Self::Overhand => "overhand",
dfa38cf4cd4batagen2026-10-10 09:17 UTC123
            Self::Underhand => "underhand",
dfa38cf4cd4batagen2026-10-10 09:17 UTC124
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC125
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC126
dfa38cf4cd4batagen2026-10-10 09:17 UTC127
    pub fn parse(protocol: &str) -> Option<Self> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC128
        match protocol.to_ascii_lowercase().as_str() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC129
            "overhand" => Some(Self::Overhand),
dfa38cf4cd4batagen2026-10-10 09:17 UTC130
            "underhand" => Some(Self::Underhand),
dfa38cf4cd4batagen2026-10-10 09:17 UTC131
            _ => None,
dfa38cf4cd4batagen2026-10-10 09:17 UTC132
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC133
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC134
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC135
dfa38cf4cd4batagen2026-10-10 09:17 UTC136
#[derive(Default)]
dfa38cf4cd4batagen2026-10-10 09:17 UTC137
pub struct Items {
dfa38cf4cd4batagen2026-10-10 09:17 UTC138
    pub ruler: bool,
dfa38cf4cd4batagen2026-10-10 09:17 UTC139
    pub ice_packs: u32,
dfa38cf4cd4batagen2026-10-10 09:17 UTC140
    pub door_locks: u32,
dfa38cf4cd4batagen2026-10-10 09:17 UTC141
    pub dnd_sign: bool,
dfa38cf4cd4batagen2026-10-10 09:17 UTC142
    pub pump_pro: bool,
dfa38cf4cd4batagen2026-10-10 09:17 UTC143
    pub extender: bool,
dfa38cf4cd4batagen2026-10-10 09:17 UTC144
    pub ring: bool,
dfa38cf4cd4batagen2026-10-10 09:17 UTC145
    pub supplements: u32,
dfa38cf4cd4batagen2026-10-10 09:17 UTC146
    pub cage: bool,
dfa38cf4cd4batagen2026-10-10 09:17 UTC147
    pub spreadsheet: bool,
dfa38cf4cd4batagen2026-10-10 09:17 UTC148
    pub teapot_spout: bool,
dfa38cf4cd4batagen2026-10-10 09:17 UTC149
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC150
dfa38cf4cd4batagen2026-10-10 09:17 UTC151
enum Payload {
dfa38cf4cd4batagen2026-10-10 09:17 UTC152
    Empty,
dfa38cf4cd4batagen2026-10-10 09:17 UTC153
    Json(Value),
dfa38cf4cd4batagen2026-10-10 09:17 UTC154
    Raw(&'static str, Vec<u8>),
dfa38cf4cd4batagen2026-10-10 09:17 UTC155
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC156
dfa38cf4cd4batagen2026-10-10 09:17 UTC157
pub struct Outcome {
dfa38cf4cd4batagen2026-10-10 09:17 UTC158
    status: StatusCode,
dfa38cf4cd4batagen2026-10-10 09:17 UTC159
    payload: Payload,
dfa38cf4cd4batagen2026-10-10 09:17 UTC160
    headers: Vec<(HeaderName, String)>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC161
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC162
dfa38cf4cd4batagen2026-10-10 09:17 UTC163
impl Outcome {
dfa38cf4cd4batagen2026-10-10 09:17 UTC164
    pub fn empty(status: StatusCode) -> Self {
dfa38cf4cd4batagen2026-10-10 09:17 UTC165
        Self {
dfa38cf4cd4batagen2026-10-10 09:17 UTC166
            status,
dfa38cf4cd4batagen2026-10-10 09:17 UTC167
            payload: Payload::Empty,
dfa38cf4cd4batagen2026-10-10 09:17 UTC168
            headers: Vec::new(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC169
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC170
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC171
dfa38cf4cd4batagen2026-10-10 09:17 UTC172
    pub fn json(status: StatusCode, body: Value) -> Self {
dfa38cf4cd4batagen2026-10-10 09:17 UTC173
        Self {
dfa38cf4cd4batagen2026-10-10 09:17 UTC174
            payload: Payload::Json(body),
dfa38cf4cd4batagen2026-10-10 09:17 UTC175
            ..Self::empty(status)
dfa38cf4cd4batagen2026-10-10 09:17 UTC176
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC177
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC178
dfa38cf4cd4batagen2026-10-10 09:17 UTC179
    pub fn raw(status: StatusCode, content_type: &'static str, body: impl Into<Vec<u8>>) -> Self {
dfa38cf4cd4batagen2026-10-10 09:17 UTC180
        Self {
dfa38cf4cd4batagen2026-10-10 09:17 UTC181
            payload: Payload::Raw(content_type, body.into()),
dfa38cf4cd4batagen2026-10-10 09:17 UTC182
            ..Self::empty(status)
dfa38cf4cd4batagen2026-10-10 09:17 UTC183
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC184
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC185
dfa38cf4cd4batagen2026-10-10 09:17 UTC186
    pub fn header(mut self, name: HeaderName, value: impl Into<String>) -> Self {
dfa38cf4cd4batagen2026-10-10 09:17 UTC187
        self.headers.push((name, value.into()));
dfa38cf4cd4batagen2026-10-10 09:17 UTC188
        self
dfa38cf4cd4batagen2026-10-10 09:17 UTC189
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC190
dfa38cf4cd4batagen2026-10-10 09:17 UTC191
    pub fn status(&self) -> StatusCode {
dfa38cf4cd4batagen2026-10-10 09:17 UTC192
        self.status
dfa38cf4cd4batagen2026-10-10 09:17 UTC193
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC194
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC195
dfa38cf4cd4batagen2026-10-10 09:17 UTC196
impl IntoResponse for Outcome {
dfa38cf4cd4batagen2026-10-10 09:17 UTC197
    fn into_response(self) -> Response {
dfa38cf4cd4batagen2026-10-10 09:17 UTC198
        let mut response = match self.payload {
dfa38cf4cd4batagen2026-10-10 09:17 UTC199
            // RFC 9110 §15.3.7 obliges a 206 to be a byte range of the full representation, so it is.
dfa38cf4cd4batagen2026-10-10 09:17 UTC200
            Payload::Json(body) if self.status == StatusCode::PARTIAL_CONTENT => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC201
                let full = body.to_string().into_bytes();
dfa38cf4cd4batagen2026-10-10 09:17 UTC202
                let half = full.len() / 2;
dfa38cf4cd4batagen2026-10-10 09:17 UTC203
                let range = format!("bytes 0-{}/{}", half - 1, full.len());
dfa38cf4cd4batagen2026-10-10 09:17 UTC204
                (
dfa38cf4cd4batagen2026-10-10 09:17 UTC205
                    self.status,
dfa38cf4cd4batagen2026-10-10 09:17 UTC206
                    [
dfa38cf4cd4batagen2026-10-10 09:17 UTC207
                        (header::CONTENT_TYPE, "application/json".to_owned()),
dfa38cf4cd4batagen2026-10-10 09:17 UTC208
                        (header::CONTENT_RANGE, range),
dfa38cf4cd4batagen2026-10-10 09:17 UTC209
                    ],
dfa38cf4cd4batagen2026-10-10 09:17 UTC210
                    full[..half].to_vec(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC211
                )
dfa38cf4cd4batagen2026-10-10 09:17 UTC212
                    .into_response()
dfa38cf4cd4batagen2026-10-10 09:17 UTC213
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC214
            Payload::Json(body) => (self.status, Json(body)).into_response(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC215
            Payload::Raw(content_type, body) => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC216
                (self.status, [(header::CONTENT_TYPE, content_type)], body).into_response()
dfa38cf4cd4batagen2026-10-10 09:17 UTC217
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC218
            Payload::Empty => self.status.into_response(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC219
        };
dfa38cf4cd4batagen2026-10-10 09:17 UTC220
        for (name, value) in self.headers {
dfa38cf4cd4batagen2026-10-10 09:17 UTC221
            let value = HeaderValue::from_str(&value).expect("header values are visible ascii");
dfa38cf4cd4batagen2026-10-10 09:17 UTC222
            response.headers_mut().append(name, value);
dfa38cf4cd4batagen2026-10-10 09:17 UTC223
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC224
        response
dfa38cf4cd4batagen2026-10-10 09:17 UTC225
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC226
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC227
dfa38cf4cd4batagen2026-10-10 09:17 UTC228
pub struct Pending {
dfa38cf4cd4batagen2026-10-10 09:17 UTC229
    due: u32,
dfa38cf4cd4batagen2026-10-10 09:17 UTC230
    length: f64,
dfa38cf4cd4batagen2026-10-10 09:17 UTC231
    girth: f64,
dfa38cf4cd4batagen2026-10-10 09:17 UTC232
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC233
dfa38cf4cd4batagen2026-10-10 09:17 UTC234
pub struct Ending {
dfa38cf4cd4batagen2026-10-10 09:17 UTC235
    pub cause: &'static str,
dfa38cf4cd4batagen2026-10-10 09:17 UTC236
    pub detail: String,
dfa38cf4cd4batagen2026-10-10 09:17 UTC237
    pub turn: u32,
dfa38cf4cd4batagen2026-10-10 09:17 UTC238
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC239
dfa38cf4cd4batagen2026-10-10 09:17 UTC240
pub struct Run {
dfa38cf4cd4batagen2026-10-10 09:17 UTC241
    pub id: String,
dfa38cf4cd4batagen2026-10-10 09:17 UTC242
    pub origin_etag: String,
dfa38cf4cd4batagen2026-10-10 09:17 UTC243
    pub version: u64,
dfa38cf4cd4batagen2026-10-10 09:17 UTC244
    pub turn: u32,
dfa38cf4cd4batagen2026-10-10 09:17 UTC245
    pub floor: Floor,
dfa38cf4cd4batagen2026-10-10 09:17 UTC246
    pub floor_turns: u32,
dfa38cf4cd4batagen2026-10-10 09:17 UTC247
    pub length: f64,
dfa38cf4cd4batagen2026-10-10 09:17 UTC248
    pub temp_length: f64,
dfa38cf4cd4batagen2026-10-10 09:17 UTC249
    pub girth: f64,
dfa38cf4cd4batagen2026-10-10 09:17 UTC250
    pub foreskin: Option<f64>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC251
    pub eq: i32,
dfa38cf4cd4batagen2026-10-10 09:17 UTC252
    pub stamina: i32,
dfa38cf4cd4batagen2026-10-10 09:17 UTC253
    pub suspicion: u8,
dfa38cf4cd4batagen2026-10-10 09:17 UTC254
    pub shame_base: u32,
dfa38cf4cd4batagen2026-10-10 09:17 UTC255
    pub shame: u32,
dfa38cf4cd4batagen2026-10-10 09:17 UTC256
    pub cash: u32,
dfa38cf4cd4batagen2026-10-10 09:17 UTC257
    pub grip: Grip,
dfa38cf4cd4batagen2026-10-10 09:17 UTC258
    pub items: Items,
dfa38cf4cd4batagen2026-10-10 09:17 UTC259
    pub warmed_up: bool,
dfa38cf4cd4batagen2026-10-10 09:17 UTC260
    pub teapot: bool,
dfa38cf4cd4batagen2026-10-10 09:17 UTC261
    pub peyronie: bool,
dfa38cf4cd4batagen2026-10-10 09:17 UTC262
    pub put_applied: bool,
dfa38cf4cd4batagen2026-10-10 09:17 UTC263
    pub forum_read: bool,
dfa38cf4cd4batagen2026-10-10 09:17 UTC264
    pub branch_pending: bool,
dfa38cf4cd4batagen2026-10-10 09:17 UTC265
    pub mom_present: bool,
dfa38cf4cd4batagen2026-10-10 09:17 UTC266
    pub mom_errand: &'static str,
dfa38cf4cd4batagen2026-10-10 09:17 UTC267
    pub observed: Option<&'static str>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC268
    pub measured: bool,
dfa38cf4cd4batagen2026-10-10 09:17 UTC269
    pub cured: bool,
dfa38cf4cd4batagen2026-10-10 09:17 UTC270
    pub roommate_told: bool,
dfa38cf4cd4batagen2026-10-10 09:17 UTC271
    pub wifi_accepted: bool,
dfa38cf4cd4batagen2026-10-10 09:17 UTC272
    pub verified: bool,
dfa38cf4cd4batagen2026-10-10 09:17 UTC273
    pub elder_beaten: bool,
dfa38cf4cd4batagen2026-10-10 09:17 UTC274
    pub edges: u8,
dfa38cf4cd4batagen2026-10-10 09:17 UTC275
    pub drawer_searches: u8,
dfa38cf4cd4batagen2026-10-10 09:17 UTC276
    pub intake_attempts: usize,
dfa38cf4cd4batagen2026-10-10 09:17 UTC277
    pub nonce: String,
dfa38cf4cd4batagen2026-10-10 09:17 UTC278
    pub heard: Vec<String>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC279
    pub refractory_until: Option<Instant>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC280
    pub pending: Vec<Pending>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC281
    pub ended: Option<Ending>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC282
    rng: StdRng,
dfa38cf4cd4batagen2026-10-10 09:17 UTC283
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC284
dfa38cf4cd4batagen2026-10-10 09:17 UTC285
pub fn round2(x: f64) -> f64 {
dfa38cf4cd4batagen2026-10-10 09:17 UTC286
    (x * 100.0).round() / 100.0
dfa38cf4cd4batagen2026-10-10 09:17 UTC287
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC288
dfa38cf4cd4batagen2026-10-10 09:17 UTC289
fn round3(x: f64) -> f64 {
dfa38cf4cd4batagen2026-10-10 09:17 UTC290
    (x * 1000.0).round() / 1000.0
dfa38cf4cd4batagen2026-10-10 09:17 UTC291
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC292
dfa38cf4cd4batagen2026-10-10 09:17 UTC293
fn tenths(x: f64) -> i64 {
dfa38cf4cd4batagen2026-10-10 09:17 UTC294
    (x * 10.0).round() as i64
dfa38cf4cd4batagen2026-10-10 09:17 UTC295
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC296
dfa38cf4cd4batagen2026-10-10 09:17 UTC297
fn not_extended() -> Problem {
dfa38cf4cd4batagen2026-10-10 09:17 UTC298
    Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC299
        StatusCode::NOT_EXTENDED,
dfa38cf4cd4batagen2026-10-10 09:17 UTC300
        "not-extended",
dfa38cf4cd4batagen2026-10-10 09:17 UTC301
        "The requested extension was not applied. The server has recorded the attempt.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC302
    )
dfa38cf4cd4batagen2026-10-10 09:17 UTC303
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC304
dfa38cf4cd4batagen2026-10-10 09:17 UTC305
pub fn parse_credentials<'a>(authorization: &'a str, scheme: &str) -> Option<&'a str> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC306
    let (given, rest) = authorization.trim().split_once(' ')?;
dfa38cf4cd4batagen2026-10-10 09:17 UTC307
    given
dfa38cf4cd4batagen2026-10-10 09:17 UTC308
        .eq_ignore_ascii_case(scheme)
dfa38cf4cd4batagen2026-10-10 09:17 UTC309
        .then(|| rest.trim().trim_matches('"'))
dfa38cf4cd4batagen2026-10-10 09:17 UTC310
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC311
dfa38cf4cd4batagen2026-10-10 09:17 UTC312
pub fn etag_matches(condition: &str, etag: &str) -> bool {
dfa38cf4cd4batagen2026-10-10 09:17 UTC313
    condition
dfa38cf4cd4batagen2026-10-10 09:17 UTC314
        .split(',')
dfa38cf4cd4batagen2026-10-10 09:17 UTC315
        .map(str::trim)
dfa38cf4cd4batagen2026-10-10 09:17 UTC316
        .any(|tag| tag == "*" || tag.trim_start_matches("W/") == etag)
dfa38cf4cd4batagen2026-10-10 09:17 UTC317
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC318
dfa38cf4cd4batagen2026-10-10 09:17 UTC319
pub fn shaft(mm: usize) -> String {
dfa38cf4cd4batagen2026-10-10 09:17 UTC320
    format!("8{}D", "=".repeat(mm.saturating_sub(2)))
dfa38cf4cd4batagen2026-10-10 09:17 UTC321
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC322
dfa38cf4cd4batagen2026-10-10 09:17 UTC323
impl Run {
dfa38cf4cd4batagen2026-10-10 09:17 UTC324
    pub fn new(id: String, seed: u64, shame_base: u32) -> Self {
dfa38cf4cd4batagen2026-10-10 09:17 UTC325
        let mut rng = StdRng::seed_from_u64(seed);
dfa38cf4cd4batagen2026-10-10 09:17 UTC326
        let length = round2(rng.random_range(4.5..6.5));
dfa38cf4cd4batagen2026-10-10 09:17 UTC327
        let girth = round2(rng.random_range(4.0..5.0));
dfa38cf4cd4batagen2026-10-10 09:17 UTC328
        let foreskin = round2(rng.random_range(0.4..2.4));
dfa38cf4cd4batagen2026-10-10 09:17 UTC329
        let mut run = Self {
dfa38cf4cd4batagen2026-10-10 09:17 UTC330
            id,
dfa38cf4cd4batagen2026-10-10 09:17 UTC331
            origin_etag: String::new(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC332
            version: 0,
dfa38cf4cd4batagen2026-10-10 09:17 UTC333
            turn: 0,
dfa38cf4cd4batagen2026-10-10 09:17 UTC334
            floor: Floor::Bedroom,
dfa38cf4cd4batagen2026-10-10 09:17 UTC335
            floor_turns: 0,
dfa38cf4cd4batagen2026-10-10 09:17 UTC336
            length,
dfa38cf4cd4batagen2026-10-10 09:17 UTC337
            temp_length: 0.0,
dfa38cf4cd4batagen2026-10-10 09:17 UTC338
            girth,
dfa38cf4cd4batagen2026-10-10 09:17 UTC339
            foreskin: Some(foreskin),
dfa38cf4cd4batagen2026-10-10 09:17 UTC340
            eq: 20,
dfa38cf4cd4batagen2026-10-10 09:17 UTC341
            stamina: MAX_STAMINA,
dfa38cf4cd4batagen2026-10-10 09:17 UTC342
            suspicion: 0,
dfa38cf4cd4batagen2026-10-10 09:17 UTC343
            shame_base,
dfa38cf4cd4batagen2026-10-10 09:17 UTC344
            shame: 0,
dfa38cf4cd4batagen2026-10-10 09:17 UTC345
            cash: STARTING_CASH,
dfa38cf4cd4batagen2026-10-10 09:17 UTC346
            grip: Grip::Overhand,
dfa38cf4cd4batagen2026-10-10 09:17 UTC347
            items: Items {
dfa38cf4cd4batagen2026-10-10 09:17 UTC348
                ice_packs: 1,
dfa38cf4cd4batagen2026-10-10 09:17 UTC349
                ..Items::default()
dfa38cf4cd4batagen2026-10-10 09:17 UTC350
            },
dfa38cf4cd4batagen2026-10-10 09:17 UTC351
            warmed_up: false,
dfa38cf4cd4batagen2026-10-10 09:17 UTC352
            teapot: false,
dfa38cf4cd4batagen2026-10-10 09:17 UTC353
            peyronie: false,
dfa38cf4cd4batagen2026-10-10 09:17 UTC354
            put_applied: false,
dfa38cf4cd4batagen2026-10-10 09:17 UTC355
            forum_read: false,
dfa38cf4cd4batagen2026-10-10 09:17 UTC356
            branch_pending: false,
dfa38cf4cd4batagen2026-10-10 09:17 UTC357
            mom_present: false,
dfa38cf4cd4batagen2026-10-10 09:17 UTC358
            mom_errand: "She heard a noise.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC359
            observed: None,
dfa38cf4cd4batagen2026-10-10 09:17 UTC360
            measured: false,
dfa38cf4cd4batagen2026-10-10 09:17 UTC361
            cured: false,
dfa38cf4cd4batagen2026-10-10 09:17 UTC362
            roommate_told: false,
dfa38cf4cd4batagen2026-10-10 09:17 UTC363
            wifi_accepted: false,
dfa38cf4cd4batagen2026-10-10 09:17 UTC364
            verified: false,
dfa38cf4cd4batagen2026-10-10 09:17 UTC365
            elder_beaten: false,
dfa38cf4cd4batagen2026-10-10 09:17 UTC366
            edges: 0,
dfa38cf4cd4batagen2026-10-10 09:17 UTC367
            drawer_searches: 0,
dfa38cf4cd4batagen2026-10-10 09:17 UTC368
            intake_attempts: 0,
dfa38cf4cd4batagen2026-10-10 09:17 UTC369
            nonce: String::new(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC370
            heard: Vec::new(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC371
            refractory_until: None,
dfa38cf4cd4batagen2026-10-10 09:17 UTC372
            pending: Vec::new(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC373
            ended: None,
dfa38cf4cd4batagen2026-10-10 09:17 UTC374
            rng,
dfa38cf4cd4batagen2026-10-10 09:17 UTC375
        };
dfa38cf4cd4batagen2026-10-10 09:17 UTC376
        run.origin_etag = run.etag();
dfa38cf4cd4batagen2026-10-10 09:17 UTC377
        run.nonce = run.fresh_nonce();
dfa38cf4cd4batagen2026-10-10 09:17 UTC378
        run
dfa38cf4cd4batagen2026-10-10 09:17 UTC379
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC380
dfa38cf4cd4batagen2026-10-10 09:17 UTC381
    pub fn etag(&self) -> String {
dfa38cf4cd4batagen2026-10-10 09:17 UTC382
        format!("\"{}-{}\"", self.id, self.version)
dfa38cf4cd4batagen2026-10-10 09:17 UTC383
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC384
dfa38cf4cd4batagen2026-10-10 09:17 UTC385
    pub fn touch(&mut self) {
dfa38cf4cd4batagen2026-10-10 09:17 UTC386
        self.version += 1;
dfa38cf4cd4batagen2026-10-10 09:17 UTC387
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC388
dfa38cf4cd4batagen2026-10-10 09:17 UTC389
    fn fresh_nonce(&mut self) -> String {
dfa38cf4cd4batagen2026-10-10 09:17 UTC390
        format!("{:016x}", self.rng.random::<u64>())
dfa38cf4cd4batagen2026-10-10 09:17 UTC391
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC392
dfa38cf4cd4batagen2026-10-10 09:17 UTC393
    pub fn current_length(&self) -> f64 {
dfa38cf4cd4batagen2026-10-10 09:17 UTC394
        self.length + self.temp_length
dfa38cf4cd4batagen2026-10-10 09:17 UTC395
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC396
dfa38cf4cd4batagen2026-10-10 09:17 UTC397
    pub fn self_reported(&self) -> f64 {
dfa38cf4cd4batagen2026-10-10 09:17 UTC398
        self.current_length() + SELF_REPORT_INFLATION
dfa38cf4cd4batagen2026-10-10 09:17 UTC399
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC400
dfa38cf4cd4batagen2026-10-10 09:17 UTC401
    pub fn total_shame(&self) -> u32 {
dfa38cf4cd4batagen2026-10-10 09:17 UTC402
        self.shame_base + self.shame
dfa38cf4cd4batagen2026-10-10 09:17 UTC403
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC404
dfa38cf4cd4batagen2026-10-10 09:17 UTC405
    pub fn status(&self) -> &'static str {
dfa38cf4cd4batagen2026-10-10 09:17 UTC406
        match (&self.ended, self.cured) {
dfa38cf4cd4batagen2026-10-10 09:17 UTC407
            (Some(_), _) => "gone",
dfa38cf4cd4batagen2026-10-10 09:17 UTC408
            (None, true) => "normal",
dfa38cf4cd4batagen2026-10-10 09:17 UTC409
            (None, false) => "alive",
dfa38cf4cd4batagen2026-10-10 09:17 UTC410
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC411
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC412
dfa38cf4cd4batagen2026-10-10 09:17 UTC413
    pub fn view(&self) -> Value {
dfa38cf4cd4batagen2026-10-10 09:17 UTC414
        let conditions: Vec<&str> = [
dfa38cf4cd4batagen2026-10-10 09:17 UTC415
            (self.teapot, "short and stout"),
dfa38cf4cd4batagen2026-10-10 09:17 UTC416
            (self.peyronie, "curved"),
dfa38cf4cd4batagen2026-10-10 09:17 UTC417
            (self.items.cage, "caged"),
dfa38cf4cd4batagen2026-10-10 09:17 UTC418
        ]
dfa38cf4cd4batagen2026-10-10 09:17 UTC419
        .into_iter()
dfa38cf4cd4batagen2026-10-10 09:17 UTC420
        .filter_map(|(active, name)| active.then_some(name))
dfa38cf4cd4batagen2026-10-10 09:17 UTC421
        .collect();
dfa38cf4cd4batagen2026-10-10 09:17 UTC422
        let mut view = json!({
dfa38cf4cd4batagen2026-10-10 09:17 UTC423
            "id": self.id,
dfa38cf4cd4batagen2026-10-10 09:17 UTC424
            "status": self.status(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC425
            "turn": self.turn,
dfa38cf4cd4batagen2026-10-10 09:17 UTC426
            "floor": { "number": self.floor.number(), "name": self.floor.name() },
dfa38cf4cd4batagen2026-10-10 09:17 UTC427
            "self_reported_length": round2(self.self_reported()),
dfa38cf4cd4batagen2026-10-10 09:17 UTC428
            "girth": round2(self.girth),
dfa38cf4cd4batagen2026-10-10 09:17 UTC429
            "foreskin": self.foreskin.map(round2),
dfa38cf4cd4batagen2026-10-10 09:17 UTC430
            "eq": self.eq,
dfa38cf4cd4batagen2026-10-10 09:17 UTC431
            "stamina": self.stamina,
dfa38cf4cd4batagen2026-10-10 09:17 UTC432
            "suspicion": self.suspicion,
dfa38cf4cd4batagen2026-10-10 09:17 UTC433
            "shame": self.total_shame(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC434
            "cash": self.cash,
dfa38cf4cd4batagen2026-10-10 09:17 UTC435
            "grip": self.grip.name(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC436
            "warmed_up": self.warmed_up,
dfa38cf4cd4batagen2026-10-10 09:17 UTC437
            "conditions": conditions,
dfa38cf4cd4batagen2026-10-10 09:17 UTC438
        });
dfa38cf4cd4batagen2026-10-10 09:17 UTC439
        if self.items.ruler {
dfa38cf4cd4batagen2026-10-10 09:17 UTC440
            view["length"] = json!(round2(self.current_length()));
dfa38cf4cd4batagen2026-10-10 09:17 UTC441
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC442
        view
dfa38cf4cd4batagen2026-10-10 09:17 UTC443
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC444
dfa38cf4cd4batagen2026-10-10 09:17 UTC445
    pub fn floor_view(&self) -> Value {
dfa38cf4cd4batagen2026-10-10 09:17 UTC446
        let id = &self.id;
dfa38cf4cd4batagen2026-10-10 09:17 UTC447
        let turns_required = match self.floor {
dfa38cf4cd4batagen2026-10-10 09:17 UTC448
            Floor::Forum | Floor::Clinic => Value::Null,
dfa38cf4cd4batagen2026-10-10 09:17 UTC449
            _ => json!(FLOOR_TURNS),
dfa38cf4cd4batagen2026-10-10 09:17 UTC450
        };
dfa38cf4cd4batagen2026-10-10 09:17 UTC451
        let mut view = json!({
dfa38cf4cd4batagen2026-10-10 09:17 UTC452
            "number": self.floor.number(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC453
            "name": self.floor.name(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC454
            "note": self.floor.note(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC455
            "turns": self.floor_turns,
dfa38cf4cd4batagen2026-10-10 09:17 UTC456
            "turns_required": turns_required,
dfa38cf4cd4batagen2026-10-10 09:17 UTC457
            "exit": { "href": format!("/runs/{id}/floor"), "method": "POST" },
dfa38cf4cd4batagen2026-10-10 09:17 UTC458
        });
dfa38cf4cd4batagen2026-10-10 09:17 UTC459
        match self.floor {
dfa38cf4cd4batagen2026-10-10 09:17 UTC460
            Floor::LockerRoom => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC461
                view["others"] = json!([
dfa38cf4cd4batagen2026-10-10 09:17 UTC462
                    { "name": "Greg", "self_reported_length": 9.5 },
dfa38cf4cd4batagen2026-10-10 09:17 UTC463
                    { "name": "Dmitri", "self_reported_length": 8.75 },
dfa38cf4cd4batagen2026-10-10 09:17 UTC464
                    { "name": "a man in a towel", "self_reported_length": 11.0 },
dfa38cf4cd4batagen2026-10-10 09:17 UTC465
                ]);
dfa38cf4cd4batagen2026-10-10 09:17 UTC466
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC467
            Floor::Hotel => view["portal"] = json!(format!("/runs/{id}/portal")),
dfa38cf4cd4batagen2026-10-10 09:17 UTC468
            Floor::Forum => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC469
                view["thread"] = json!(FORUM_THREAD);
dfa38cf4cd4batagen2026-10-10 09:17 UTC470
                view["elder"] = json!(format!("/runs/{id}/elder"));
dfa38cf4cd4batagen2026-10-10 09:17 UTC471
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC472
            Floor::Clinic => view["diagnosis"] = json!(format!("/runs/{id}/diagnosis")),
dfa38cf4cd4batagen2026-10-10 09:17 UTC473
            _ => {}
dfa38cf4cd4batagen2026-10-10 09:17 UTC474
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC475
        if self.floor.shop_open() {
dfa38cf4cd4batagen2026-10-10 09:17 UTC476
            view["shop"] = json!(format!("/runs/{id}/shop"));
dfa38cf4cd4batagen2026-10-10 09:17 UTC477
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC478
        view
dfa38cf4cd4batagen2026-10-10 09:17 UTC479
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC480
dfa38cf4cd4batagen2026-10-10 09:17 UTC481
    pub fn inventory(&self) -> Vec<(&'static str, &'static str)> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC482
        let i = &self.items;
dfa38cf4cd4batagen2026-10-10 09:17 UTC483
        let owned = [
dfa38cf4cd4batagen2026-10-10 09:17 UTC484
            (
dfa38cf4cd4batagen2026-10-10 09:17 UTC485
                true,
dfa38cf4cd4batagen2026-10-10 09:17 UTC486
                "warm-towel",
dfa38cf4cd4batagen2026-10-10 09:17 UTC487
                "Reusable. Satisfies the warm-up precondition. Use technique=warmup.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC488
            ),
dfa38cf4cd4batagen2026-10-10 09:17 UTC489
            (
dfa38cf4cd4batagen2026-10-10 09:17 UTC490
                i.ice_packs > 0,
dfa38cf4cd4batagen2026-10-10 09:17 UTC491
                "ice-pack",
dfa38cf4cd4batagen2026-10-10 09:17 UTC492
                "Lowers EQ by 40. Use technique=ice.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC493
            ),
dfa38cf4cd4batagen2026-10-10 09:17 UTC494
            (
dfa38cf4cd4batagen2026-10-10 09:17 UTC495
                i.ruler,
dfa38cf4cd4batagen2026-10-10 09:17 UTC496
                "ruler",
dfa38cf4cd4batagen2026-10-10 09:17 UTC497
                "Measures honestly. The length resource now carries a Content-Length.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC498
            ),
dfa38cf4cd4batagen2026-10-10 09:17 UTC499
            (
dfa38cf4cd4batagen2026-10-10 09:17 UTC500
                i.door_locks > 0,
dfa38cf4cd4batagen2026-10-10 09:17 UTC501
                "door-lock",
dfa38cf4cd4batagen2026-10-10 09:17 UTC502
                "Keeps the roommate out once.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC503
            ),
dfa38cf4cd4batagen2026-10-10 09:17 UTC504
            (
dfa38cf4cd4batagen2026-10-10 09:17 UTC505
                i.dnd_sign,
dfa38cf4cd4batagen2026-10-10 09:17 UTC506
                "do-not-disturb-sign",
dfa38cf4cd4batagen2026-10-10 09:17 UTC507
                "Works on housekeeping. Mom does not read signs.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC508
            ),
dfa38cf4cd4batagen2026-10-10 09:17 UTC509
            (
dfa38cf4cd4batagen2026-10-10 09:17 UTC510
                i.pump_pro,
dfa38cf4cd4batagen2026-10-10 09:17 UTC511
                "pump-pro",
dfa38cf4cd4batagen2026-10-10 09:17 UTC512
                "Use technique=pump. Gains are temporary.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC513
            ),
dfa38cf4cd4batagen2026-10-10 09:17 UTC514
            (
dfa38cf4cd4batagen2026-10-10 09:17 UTC515
                i.extender,
dfa38cf4cd4batagen2026-10-10 09:17 UTC516
                "extender",
dfa38cf4cd4batagen2026-10-10 09:17 UTC517
                "Passive gains each turn. Takes up room. Can be removed with DELETE.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC518
            ),
dfa38cf4cd4batagen2026-10-10 09:17 UTC519
            (
dfa38cf4cd4batagen2026-10-10 09:17 UTC520
                i.ring,
dfa38cf4cd4batagen2026-10-10 09:17 UTC521
                "cock-ring",
dfa38cf4cd4batagen2026-10-10 09:17 UTC522
                "Girth gains doubled. Rupture risk raised. Responses are compressed if you accept gzip. Can be removed with DELETE.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC523
            ),
dfa38cf4cd4batagen2026-10-10 09:17 UTC524
            (i.supplements > 0, "supplement", "Use technique=supplement."),
dfa38cf4cd4batagen2026-10-10 09:17 UTC525
            (i.cage, "chastity-cage", "Locked."),
dfa38cf4cd4batagen2026-10-10 09:17 UTC526
            (
dfa38cf4cd4batagen2026-10-10 09:17 UTC527
                i.spreadsheet,
dfa38cf4cd4batagen2026-10-10 09:17 UTC528
                "spreadsheet",
dfa38cf4cd4batagen2026-10-10 09:17 UTC529
                "Charts your progress. Every line is flat.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC530
            ),
dfa38cf4cd4batagen2026-10-10 09:17 UTC531
            (
dfa38cf4cd4batagen2026-10-10 09:17 UTC532
                i.teapot_spout,
dfa38cf4cd4batagen2026-10-10 09:17 UTC533
                "teapot-spout",
dfa38cf4cd4batagen2026-10-10 09:17 UTC534
                "Does nothing. Cannot be dropped.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC535
            ),
dfa38cf4cd4batagen2026-10-10 09:17 UTC536
        ];
dfa38cf4cd4batagen2026-10-10 09:17 UTC537
        owned
dfa38cf4cd4batagen2026-10-10 09:17 UTC538
            .into_iter()
dfa38cf4cd4batagen2026-10-10 09:17 UTC539
            .filter_map(|(have, slug, description)| have.then_some((slug, description)))
dfa38cf4cd4batagen2026-10-10 09:17 UTC540
            .collect()
dfa38cf4cd4batagen2026-10-10 09:17 UTC541
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC542
dfa38cf4cd4batagen2026-10-10 09:17 UTC543
    pub fn end(&mut self, cause: &'static str, detail: &str) {
dfa38cf4cd4batagen2026-10-10 09:17 UTC544
        self.ended = Some(Ending {
dfa38cf4cd4batagen2026-10-10 09:17 UTC545
            cause,
dfa38cf4cd4batagen2026-10-10 09:17 UTC546
            detail: detail.into(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC547
            turn: self.turn,
dfa38cf4cd4batagen2026-10-10 09:17 UTC548
        });
dfa38cf4cd4batagen2026-10-10 09:17 UTC549
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC550
dfa38cf4cd4batagen2026-10-10 09:17 UTC551
    fn check_refractory(&mut self, now: Instant) -> Result<(), Problem> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC552
        let Some(until) = self.refractory_until else {
dfa38cf4cd4batagen2026-10-10 09:17 UTC553
            return Ok(());
dfa38cf4cd4batagen2026-10-10 09:17 UTC554
        };
dfa38cf4cd4batagen2026-10-10 09:17 UTC555
        if now < until {
dfa38cf4cd4batagen2026-10-10 09:17 UTC556
            let seconds = (until - now).as_secs() + 1;
dfa38cf4cd4batagen2026-10-10 09:17 UTC557
            return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC558
                StatusCode::TOO_MANY_REQUESTS,
dfa38cf4cd4batagen2026-10-10 09:17 UTC559
                "overtrained",
dfa38cf4cd4batagen2026-10-10 09:17 UTC560
                "Stamina is exhausted. The refractory period is in effect.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC561
            )
dfa38cf4cd4batagen2026-10-10 09:17 UTC562
            .header(header::RETRY_AFTER, seconds.to_string()));
dfa38cf4cd4batagen2026-10-10 09:17 UTC563
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC564
        self.refractory_until = None;
dfa38cf4cd4batagen2026-10-10 09:17 UTC565
        self.stamina = MAX_STAMINA;
dfa38cf4cd4batagen2026-10-10 09:17 UTC566
        self.eq = 20;
dfa38cf4cd4batagen2026-10-10 09:17 UTC567
        self.warmed_up = false;
dfa38cf4cd4batagen2026-10-10 09:17 UTC568
        self.touch();
dfa38cf4cd4batagen2026-10-10 09:17 UTC569
        Ok(())
dfa38cf4cd4batagen2026-10-10 09:17 UTC570
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC571
dfa38cf4cd4batagen2026-10-10 09:17 UTC572
    fn realize(&mut self) {
dfa38cf4cd4batagen2026-10-10 09:17 UTC573
        let (due, waiting): (Vec<_>, Vec<_>) = std::mem::take(&mut self.pending)
dfa38cf4cd4batagen2026-10-10 09:17 UTC574
            .into_iter()
dfa38cf4cd4batagen2026-10-10 09:17 UTC575
            .partition(|p| p.due <= self.turn);
dfa38cf4cd4batagen2026-10-10 09:17 UTC576
        self.pending = waiting;
dfa38cf4cd4batagen2026-10-10 09:17 UTC577
        for p in due {
dfa38cf4cd4batagen2026-10-10 09:17 UTC578
            self.length += p.length;
dfa38cf4cd4batagen2026-10-10 09:17 UTC579
            self.girth += p.girth;
dfa38cf4cd4batagen2026-10-10 09:17 UTC580
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC581
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC582
dfa38cf4cd4batagen2026-10-10 09:17 UTC583
    fn gain(&mut self, length: f64, girth: f64, foreskin: f64) {
dfa38cf4cd4batagen2026-10-10 09:17 UTC584
        self.length += length;
dfa38cf4cd4batagen2026-10-10 09:17 UTC585
        self.girth += girth;
dfa38cf4cd4batagen2026-10-10 09:17 UTC586
        if let Some(f) = &mut self.foreskin {
dfa38cf4cd4batagen2026-10-10 09:17 UTC587
            *f += foreskin;
dfa38cf4cd4batagen2026-10-10 09:17 UTC588
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC589
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC590
dfa38cf4cd4batagen2026-10-10 09:17 UTC591
    fn rupture(&mut self) -> Problem {
dfa38cf4cd4batagen2026-10-10 09:17 UTC592
        let detail = if self.items.ice_packs > 0 {
dfa38cf4cd4batagen2026-10-10 09:17 UTC593
            "A vessel has exceeded its rated pressure. The run has ended. An ice pack was available and was not used."
dfa38cf4cd4batagen2026-10-10 09:17 UTC594
        } else {
dfa38cf4cd4batagen2026-10-10 09:17 UTC595
            "A vessel has exceeded its rated pressure. The run has ended."
dfa38cf4cd4batagen2026-10-10 09:17 UTC596
        };
dfa38cf4cd4batagen2026-10-10 09:17 UTC597
        self.end("vascular-event", detail);
dfa38cf4cd4batagen2026-10-10 09:17 UTC598
        Problem::new(StatusCode::INTERNAL_SERVER_ERROR, "vascular-event", detail)
dfa38cf4cd4batagen2026-10-10 09:17 UTC599
            .with("eq_at_failure", self.eq)
dfa38cf4cd4batagen2026-10-10 09:17 UTC600
            .with("turn", self.turn)
dfa38cf4cd4batagen2026-10-10 09:17 UTC601
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC602
dfa38cf4cd4batagen2026-10-10 09:17 UTC603
    fn next_floor(&mut self) {
dfa38cf4cd4batagen2026-10-10 09:17 UTC604
        let Some(next) = self.floor.next() else {
dfa38cf4cd4batagen2026-10-10 09:17 UTC605
            return;
dfa38cf4cd4batagen2026-10-10 09:17 UTC606
        };
dfa38cf4cd4batagen2026-10-10 09:17 UTC607
        self.floor = next;
dfa38cf4cd4batagen2026-10-10 09:17 UTC608
        self.floor_turns = 0;
dfa38cf4cd4batagen2026-10-10 09:17 UTC609
        self.temp_length = 0.0;
dfa38cf4cd4batagen2026-10-10 09:17 UTC610
        self.teapot = false;
dfa38cf4cd4batagen2026-10-10 09:17 UTC611
        self.warmed_up = false;
dfa38cf4cd4batagen2026-10-10 09:17 UTC612
        self.eq = 20;
dfa38cf4cd4batagen2026-10-10 09:17 UTC613
        self.suspicion = 0;
dfa38cf4cd4batagen2026-10-10 09:17 UTC614
        self.mom_present = false;
dfa38cf4cd4batagen2026-10-10 09:17 UTC615
        self.roommate_told = false;
dfa38cf4cd4batagen2026-10-10 09:17 UTC616
        self.drawer_searches = 0;
dfa38cf4cd4batagen2026-10-10 09:17 UTC617
        self.edges = 0;
dfa38cf4cd4batagen2026-10-10 09:17 UTC618
        self.cash += FLOOR_ALLOWANCE;
dfa38cf4cd4batagen2026-10-10 09:17 UTC619
        self.nonce = self.fresh_nonce();
dfa38cf4cd4batagen2026-10-10 09:17 UTC620
        self.touch();
dfa38cf4cd4batagen2026-10-10 09:17 UTC621
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC622
dfa38cf4cd4batagen2026-10-10 09:17 UTC623
    pub fn advance(&mut self) -> Result<Outcome, Problem> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC624
        match self.floor {
dfa38cf4cd4batagen2026-10-10 09:17 UTC625
            Floor::Clinic => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC626
                return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC627
                    StatusCode::CONFLICT,
dfa38cf4cd4batagen2026-10-10 09:17 UTC628
                    "no-further-floor",
dfa38cf4cd4batagen2026-10-10 09:17 UTC629
                    "There is no further floor. There is only the diagnosis.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC630
                ));
dfa38cf4cd4batagen2026-10-10 09:17 UTC631
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC632
            Floor::Forum if !self.elder_beaten => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC633
                return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC634
                    StatusCode::CONFLICT,
dfa38cf4cd4batagen2026-10-10 09:17 UTC635
                    "elder-unaddressed",
dfa38cf4cd4batagen2026-10-10 09:17 UTC636
                    "The Elder has not been addressed.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC637
                )
dfa38cf4cd4batagen2026-10-10 09:17 UTC638
                .with("elder", format!("/runs/{}/elder", self.id)));
dfa38cf4cd4batagen2026-10-10 09:17 UTC639
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC640
            Floor::Forum => {}
dfa38cf4cd4batagen2026-10-10 09:17 UTC641
            _ if self.floor_turns < FLOOR_TURNS => {
dfa38cf4cd4batagen2026-10-10 09:17 UTC642
                return Err(Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC643
                    StatusCode::TOO_EARLY,
dfa38cf4cd4batagen2026-10-10 09:17 UTC644
                    "floor-not-over",
dfa38cf4cd4batagen2026-10-10 09:17 UTC645
                    format!(
dfa38cf4cd4batagen2026-10-10 09:17 UTC646
                        "The floor is not over. {} turns remain.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC647
                        FLOOR_TURNS - self.floor_turns
dfa38cf4cd4batagen2026-10-10 09:17 UTC648
                    ),
dfa38cf4cd4batagen2026-10-10 09:17 UTC649
                ));
dfa38cf4cd4batagen2026-10-10 09:17 UTC650
            }
dfa38cf4cd4batagen2026-10-10 09:17 UTC651
            _ => {}
dfa38cf4cd4batagen2026-10-10 09:17 UTC652
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC653
        let reverted = self.temp_length;
dfa38cf4cd4batagen2026-10-10 09:17 UTC654
        self.next_floor();
dfa38cf4cd4batagen2026-10-10 09:17 UTC655
        let mut body = json!({
dfa38cf4cd4batagen2026-10-10 09:17 UTC656
            "detail": format!("You have arrived at {}.", self.floor.name()),
dfa38cf4cd4batagen2026-10-10 09:17 UTC657
            "floor": self.floor.number(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC658
            "note": self.floor.note(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC659
        });
dfa38cf4cd4batagen2026-10-10 09:17 UTC660
        if reverted > 0.0 {
dfa38cf4cd4batagen2026-10-10 09:17 UTC661
            body["reverted"] = json!(round2(reverted));
dfa38cf4cd4batagen2026-10-10 09:17 UTC662
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC663
        Ok(Outcome::json(StatusCode::SEE_OTHER, body)
dfa38cf4cd4batagen2026-10-10 09:17 UTC664
            .header(header::LOCATION, format!("/runs/{}/floor", self.id)))
dfa38cf4cd4batagen2026-10-10 09:17 UTC665
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC666
}