atagen

jelq

public

an http based roguelike rpg about jelqing

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