atagen

jelq

public

an http based roguelike rpg about jelqing

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

Blame: src/game/mod.rs

782069860236atagen2026-10-10 12:44 UTC1
pub mod balance;
782069860236atagen2026-10-10 12:44 UTC2
mod body;
782069860236atagen2026-10-10 12:44 UTC3
mod clinic;
782069860236atagen2026-10-10 12:44 UTC4
mod floors;
782069860236atagen2026-10-10 12:44 UTC5
mod forum;
782069860236atagen2026-10-10 12:44 UTC6
mod hazards;
782069860236atagen2026-10-10 12:44 UTC7
mod household;
782069860236atagen2026-10-10 12:44 UTC8
mod items;
0a0bfee8329eatagen2026-10-11 05:11 UTC9
mod kitchen;
3f9091557ec3atagen2026-10-11 11:43 UTC10
pub(crate) mod oracle;
782069860236atagen2026-10-10 12:44 UTC11
mod outcome;
782069860236atagen2026-10-10 12:44 UTC12
mod shop;
782069860236atagen2026-10-10 12:44 UTC13
mod state;
782069860236atagen2026-10-10 12:44 UTC14
mod techniques;
782069860236atagen2026-10-10 12:44 UTC15
mod turn;
782069860236atagen2026-10-10 12:44 UTC16
mod utility;
782069860236atagen2026-10-10 12:44 UTC17
159f0998263batagen2026-10-12 11:01 UTC18
pub use body::{PatchOp, volume};
782069860236atagen2026-10-10 12:44 UTC19
pub use forum::{ELDER_LAST_MODIFIED, FORUM_THREAD, forum_thread};
c4d70621d377atagen2026-10-12 11:56 UTC20
pub use household::vocabulary as excuse_vocabulary;
7d3fb63208bfatagen2026-10-11 04:38 UTC21
pub use items::{Item, Items, SHOP};
782069860236atagen2026-10-10 12:44 UTC22
pub use outcome::{Outcome, Payload};
782069860236atagen2026-10-10 12:44 UTC23
pub use state::{
c4d70621d377atagen2026-10-12 11:56 UTC24
    Cause, Clinic, Conditions, Elder, Ending, Floor, FloorState, Grip, MumVisit, Observer,
c4d70621d377atagen2026-10-12 11:56 UTC25
    RunStatus, Story,
782069860236atagen2026-10-10 12:44 UTC26
};
782069860236atagen2026-10-10 12:44 UTC27
pub use techniques::{LUBRICANTS, Lube, Technique};
dfa38cf4cd4batagen2026-10-10 09:17 UTC28
99524cf9f17catagen2026-10-10 10:53 UTC29
use std::{
782069860236atagen2026-10-10 12:44 UTC30
    hash::{DefaultHasher, Hash, Hasher},
a6caa2f99786atagen2026-10-12 10:38 UTC31
    time::{Instant, SystemTime},
99524cf9f17catagen2026-10-10 10:53 UTC32
};
dfa38cf4cd4batagen2026-10-10 09:17 UTC33
a6caa2f99786atagen2026-10-12 10:38 UTC34
use axum::http::StatusCode;
dfa38cf4cd4batagen2026-10-10 09:17 UTC35
use serde_json::{Value, json};
dfa38cf4cd4batagen2026-10-10 09:17 UTC36
dfa38cf4cd4batagen2026-10-10 09:17 UTC37
use crate::problem::Problem;
782069860236atagen2026-10-10 12:44 UTC38
use balance::{
e807266c323batagen2026-10-12 09:46 UTC39
    BRANCH_TURN, HONEST_TOLERANCE, MAX_STAMINA, PRIAPISM_EQ, PULLED_STAMINA, SELF_REPORT_INFLATION,
14de3e1e8dc2atagen2026-10-11 06:19 UTC40
    STARTING_CASH, STARTING_EQ,
782069860236atagen2026-10-10 12:44 UTC41
};
3f9091557ec3atagen2026-10-11 11:43 UTC42
use oracle::{Oracle, OracleTemplate};
782069860236atagen2026-10-10 12:44 UTC43
use state::Pending;
dfa38cf4cd4batagen2026-10-10 09:17 UTC44
dfa38cf4cd4batagen2026-10-10 09:17 UTC45
pub struct Run {
9683736a2c5eatagen2026-10-10 13:15 UTC46
    /// The private id in the run's URL. Knowing it is the only authorisation a run has.
dfa38cf4cd4batagen2026-10-10 09:17 UTC47
    pub id: String,
782069860236atagen2026-10-10 12:44 UTC48
    pub public_id: String,
782069860236atagen2026-10-10 12:44 UTC49
    etag_salt: u64,
dfa38cf4cd4batagen2026-10-10 09:17 UTC50
    pub version: u64,
dfa38cf4cd4batagen2026-10-10 09:17 UTC51
    pub turn: u32,
dfa38cf4cd4batagen2026-10-10 09:17 UTC52
    pub floor: Floor,
782069860236atagen2026-10-10 12:44 UTC53
    pub here: FloorState,
dfa38cf4cd4batagen2026-10-10 09:17 UTC54
    pub length: f64,
dfa38cf4cd4batagen2026-10-10 09:17 UTC55
    pub girth: f64,
dfa38cf4cd4batagen2026-10-10 09:17 UTC56
    pub foreskin: Option<f64>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC57
    pub eq: i32,
dfa38cf4cd4batagen2026-10-10 09:17 UTC58
    pub stamina: i32,
dfa38cf4cd4batagen2026-10-10 09:17 UTC59
    pub suspicion: u8,
dfa38cf4cd4batagen2026-10-10 09:17 UTC60
    pub shame: u32,
782069860236atagen2026-10-10 12:44 UTC61
    pub shame_reported: u32,
782069860236atagen2026-10-10 12:44 UTC62
    pub shame_total: u32,
dfa38cf4cd4batagen2026-10-10 09:17 UTC63
    pub cash: u32,
dfa38cf4cd4batagen2026-10-10 09:17 UTC64
    pub grip: Grip,
5dfebcc0ac43atagen2026-10-11 05:28 UTC65
    pub friction: i32,
5dfebcc0ac43atagen2026-10-11 05:28 UTC66
    pub jelq_grip: Option<Grip>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC67
    pub items: Items,
782069860236atagen2026-10-10 12:44 UTC68
    pub conditions: Conditions,
dfa38cf4cd4batagen2026-10-10 09:17 UTC69
    pub warmed_up: bool,
dfa38cf4cd4batagen2026-10-10 09:17 UTC70
    pub put_applied: bool,
782069860236atagen2026-10-10 12:44 UTC71
    pub story: Story,
b85a0d4536c3atagen2026-10-11 08:50 UTC72
    pub uli_taught: bool,
0a0bfee8329eatagen2026-10-11 05:11 UTC73
    pub kitchen_tellings: u8,
9683736a2c5eatagen2026-10-10 13:15 UTC74
    pub mum: Option<MumVisit>,
782069860236atagen2026-10-10 12:44 UTC75
    pub heard: Vec<String>,
782069860236atagen2026-10-10 12:44 UTC76
    pub elder: Elder,
782069860236atagen2026-10-10 12:44 UTC77
    pub clinic: Clinic,
99524cf9f17catagen2026-10-10 10:53 UTC78
    pub seeded: bool,
c3e0c27cf8fdatagen2026-10-10 11:28 UTC79
    pub filmed_at: Option<SystemTime>,
782069860236atagen2026-10-10 12:44 UTC80
    pub observed: Option<Observer>,
a6caa2f99786atagen2026-10-12 10:38 UTC81
    pub exhausted: bool,
dfa38cf4cd4batagen2026-10-10 09:17 UTC82
    pub pending: Vec<Pending>,
d46f9d0b953aatagen2026-10-10 14:41 UTC83
    pub notices: Vec<&'static str>,
dfa38cf4cd4batagen2026-10-10 09:17 UTC84
    pub ended: Option<Ending>,
782069860236atagen2026-10-10 12:44 UTC85
    pub last_seen: Instant,
3f9091557ec3atagen2026-10-11 11:43 UTC86
    oracle: Oracle,
dfa38cf4cd4batagen2026-10-10 09:17 UTC87
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC88
dfa38cf4cd4batagen2026-10-10 09:17 UTC89
pub fn round2(x: f64) -> f64 {
dfa38cf4cd4batagen2026-10-10 09:17 UTC90
    (x * 100.0).round() / 100.0
dfa38cf4cd4batagen2026-10-10 09:17 UTC91
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC92
dfa38cf4cd4batagen2026-10-10 09:17 UTC93
fn round3(x: f64) -> f64 {
dfa38cf4cd4batagen2026-10-10 09:17 UTC94
    (x * 1000.0).round() / 1000.0
dfa38cf4cd4batagen2026-10-10 09:17 UTC95
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC96
177535ac8abfatagen2026-10-10 10:39 UTC97
fn honest(claimed: f64, actual: f64) -> bool {
782069860236atagen2026-10-10 12:44 UTC98
    (claimed - actual).abs() < HONEST_TOLERANCE
dfa38cf4cd4batagen2026-10-10 09:17 UTC99
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC100
dfa38cf4cd4batagen2026-10-10 09:17 UTC101
fn not_extended() -> Problem {
dfa38cf4cd4batagen2026-10-10 09:17 UTC102
    Problem::new(
dfa38cf4cd4batagen2026-10-10 09:17 UTC103
        StatusCode::NOT_EXTENDED,
dfa38cf4cd4batagen2026-10-10 09:17 UTC104
        "not-extended",
dfa38cf4cd4batagen2026-10-10 09:17 UTC105
        "The requested extension was not applied. The server has recorded the attempt.",
dfa38cf4cd4batagen2026-10-10 09:17 UTC106
    )
dfa38cf4cd4batagen2026-10-10 09:17 UTC107
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC108
dfa38cf4cd4batagen2026-10-10 09:17 UTC109
pub fn parse_credentials<'a>(authorization: &'a str, scheme: &str) -> Option<&'a str> {
dfa38cf4cd4batagen2026-10-10 09:17 UTC110
    let (given, rest) = authorization.trim().split_once(' ')?;
dfa38cf4cd4batagen2026-10-10 09:17 UTC111
    given
dfa38cf4cd4batagen2026-10-10 09:17 UTC112
        .eq_ignore_ascii_case(scheme)
dfa38cf4cd4batagen2026-10-10 09:17 UTC113
        .then(|| rest.trim().trim_matches('"'))
dfa38cf4cd4batagen2026-10-10 09:17 UTC114
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC115
dfa38cf4cd4batagen2026-10-10 09:17 UTC116
pub fn etag_matches(condition: &str, etag: &str) -> bool {
dfa38cf4cd4batagen2026-10-10 09:17 UTC117
    condition
dfa38cf4cd4batagen2026-10-10 09:17 UTC118
        .split(',')
dfa38cf4cd4batagen2026-10-10 09:17 UTC119
        .map(str::trim)
dfa38cf4cd4batagen2026-10-10 09:17 UTC120
        .any(|tag| tag == "*" || tag.trim_start_matches("W/") == etag)
dfa38cf4cd4batagen2026-10-10 09:17 UTC121
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC122
dfa38cf4cd4batagen2026-10-10 09:17 UTC123
pub fn shaft(mm: usize) -> String {
dfa38cf4cd4batagen2026-10-10 09:17 UTC124
    format!("8{}D", "=".repeat(mm.saturating_sub(2)))
dfa38cf4cd4batagen2026-10-10 09:17 UTC125
}
dfa38cf4cd4batagen2026-10-10 09:17 UTC126
dfa38cf4cd4batagen2026-10-10 09:17 UTC127
impl Run {
3f9091557ec3atagen2026-10-11 11:43 UTC128
    #[cfg(test)]
782069860236atagen2026-10-10 12:44 UTC129
    pub fn new(id: String, public_id: String, seed: u64) -> Self {
3f9091557ec3atagen2026-10-11 11:43 UTC130
        Self::with_oracle(id, public_id, seed, &OracleTemplate::Rng)
3f9091557ec3atagen2026-10-11 11:43 UTC131
    }
3f9091557ec3atagen2026-10-11 11:43 UTC132
3f9091557ec3atagen2026-10-11 11:43 UTC133
    pub fn with_oracle(
3f9091557ec3atagen2026-10-11 11:43 UTC134
        id: String,
3f9091557ec3atagen2026-10-11 11:43 UTC135
        public_id: String,
3f9091557ec3atagen2026-10-11 11:43 UTC136
        seed: u64,
3f9091557ec3atagen2026-10-11 11:43 UTC137
        template: &OracleTemplate,
3f9091557ec3atagen2026-10-11 11:43 UTC138
    ) -> Self {
3f9091557ec3atagen2026-10-11 11:43 UTC139
        let mut oracle = Oracle::build(template, seed);
3f9091557ec3atagen2026-10-11 11:43 UTC140
        let length = round2(oracle.span(
3f9091557ec3atagen2026-10-11 11:43 UTC141
            Run::unborn("length", "How long is it, in inches?"),
3f9091557ec3atagen2026-10-11 11:43 UTC142
            4.5,
3f9091557ec3atagen2026-10-11 11:43 UTC143
            6.5,
3f9091557ec3atagen2026-10-11 11:43 UTC144
        ));
3f9091557ec3atagen2026-10-11 11:43 UTC145
        let girth = round2(oracle.span(
3f9091557ec3atagen2026-10-11 11:43 UTC146
            Run::unborn("girth", "How thick is it, in inches?"),
3f9091557ec3atagen2026-10-11 11:43 UTC147
            4.0,
3f9091557ec3atagen2026-10-11 11:43 UTC148
            5.0,
3f9091557ec3atagen2026-10-11 11:43 UTC149
        ));
3f9091557ec3atagen2026-10-11 11:43 UTC150
        let foreskin = round2(oracle.span(
3f9091557ec3atagen2026-10-11 11:43 UTC151
            Run::unborn("foreskin", "How much foreskin is there, in inches?"),
3f9091557ec3atagen2026-10-11 11:43 UTC152
            0.4,
3f9091557ec3atagen2026-10-11 11:43 UTC153
            2.4,
3f9091557ec3atagen2026-10-11 11:43 UTC154
        ));
3f9091557ec3atagen2026-10-11 11:43 UTC155
        let etag_salt = oracle.u64();
782069860236atagen2026-10-10 12:44 UTC156
        Self {
dfa38cf4cd4batagen2026-10-10 09:17 UTC157
            id,
782069860236atagen2026-10-10 12:44 UTC158
            public_id,
782069860236atagen2026-10-10 12:44 UTC159
            etag_salt,
dfa38cf4cd4batagen2026-10-10 09:17 UTC160
            version: 0,
dfa38cf4cd4batagen2026-10-10 09:17 UTC161
            turn: 0,
dfa38cf4cd4batagen2026-10-10 09:17 UTC162
            floor: Floor::Bedroom,
782069860236atagen2026-10-10 12:44 UTC163
            here: FloorState::default(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC164
            length,
dfa38cf4cd4batagen2026-10-10 09:17 UTC165
            girth,
dfa38cf4cd4batagen2026-10-10 09:17 UTC166
            foreskin: Some(foreskin),
782069860236atagen2026-10-10 12:44 UTC167
            eq: STARTING_EQ,
dfa38cf4cd4batagen2026-10-10 09:17 UTC168
            stamina: MAX_STAMINA,
dfa38cf4cd4batagen2026-10-10 09:17 UTC169
            suspicion: 0,
dfa38cf4cd4batagen2026-10-10 09:17 UTC170
            shame: 0,
782069860236atagen2026-10-10 12:44 UTC171
            shame_reported: 0,
782069860236atagen2026-10-10 12:44 UTC172
            shame_total: 0,
dfa38cf4cd4batagen2026-10-10 09:17 UTC173
            cash: STARTING_CASH,
dfa38cf4cd4batagen2026-10-10 09:17 UTC174
            grip: Grip::Overhand,
5dfebcc0ac43atagen2026-10-11 05:28 UTC175
            friction: 0,
5dfebcc0ac43atagen2026-10-11 05:28 UTC176
            jelq_grip: None,
782069860236atagen2026-10-10 12:44 UTC177
            items: Items::default(),
782069860236atagen2026-10-10 12:44 UTC178
            conditions: Conditions::default(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC179
            warmed_up: false,
dfa38cf4cd4batagen2026-10-10 09:17 UTC180
            put_applied: false,
782069860236atagen2026-10-10 12:44 UTC181
            story: Story::Unopened,
b85a0d4536c3atagen2026-10-11 08:50 UTC182
            uli_taught: false,
0a0bfee8329eatagen2026-10-11 05:11 UTC183
            kitchen_tellings: 0,
9683736a2c5eatagen2026-10-10 13:15 UTC184
            mum: None,
782069860236atagen2026-10-10 12:44 UTC185
            heard: Vec::new(),
d46f9d0b953aatagen2026-10-10 14:41 UTC186
            notices: Vec::new(),
782069860236atagen2026-10-10 12:44 UTC187
            elder: Elder::Unverified { strikes: 0 },
782069860236atagen2026-10-10 12:44 UTC188
            clinic: Clinic::default(),
99524cf9f17catagen2026-10-10 10:53 UTC189
            seeded: false,
c3e0c27cf8fdatagen2026-10-10 11:28 UTC190
            filmed_at: None,
dfa38cf4cd4batagen2026-10-10 09:17 UTC191
            observed: None,
a6caa2f99786atagen2026-10-12 10:38 UTC192
            exhausted: false,
dfa38cf4cd4batagen2026-10-10 09:17 UTC193
            pending: Vec::new(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC194
            ended: None,
782069860236atagen2026-10-10 12:44 UTC195
            last_seen: Instant::now(),
3f9091557ec3atagen2026-10-11 11:43 UTC196
            oracle,
3f9091557ec3atagen2026-10-11 11:43 UTC197
        }
3f9091557ec3atagen2026-10-11 11:43 UTC198
    }
3f9091557ec3atagen2026-10-11 11:43 UTC199
3f9091557ec3atagen2026-10-11 11:43 UTC200
    /// A roll for a run that does not exist yet, so there is no state to describe.
3f9091557ec3atagen2026-10-11 11:43 UTC201
    fn unborn(site: &'static str, ask: &str) -> oracle::Roll {
3f9091557ec3atagen2026-10-11 11:43 UTC202
        oracle::Roll {
3f9091557ec3atagen2026-10-11 11:43 UTC203
            state: "A new run is being created. No game has been played yet.".into(),
3f9091557ec3atagen2026-10-11 11:43 UTC204
            site,
3f9091557ec3atagen2026-10-11 11:43 UTC205
            ask: ask.into(),
782069860236atagen2026-10-10 12:44 UTC206
        }
782069860236atagen2026-10-10 12:44 UTC207
    }
782069860236atagen2026-10-10 12:44 UTC208
782069860236atagen2026-10-10 12:44 UTC209
    fn etag_at(&self, version: u64) -> String {
782069860236atagen2026-10-10 12:44 UTC210
        let mut hasher = DefaultHasher::new();
782069860236atagen2026-10-10 12:44 UTC211
        (self.etag_salt, version).hash(&mut hasher);
782069860236atagen2026-10-10 12:44 UTC212
        format!("\"{:016x}\"", hasher.finish())
dfa38cf4cd4batagen2026-10-10 09:17 UTC213
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC214
dfa38cf4cd4batagen2026-10-10 09:17 UTC215
    pub fn etag(&self) -> String {
782069860236atagen2026-10-10 12:44 UTC216
        self.etag_at(self.version)
782069860236atagen2026-10-10 12:44 UTC217
    }
782069860236atagen2026-10-10 12:44 UTC218
782069860236atagen2026-10-10 12:44 UTC219
    /// The ETag the run had when it was created, which the true ending asks for.
782069860236atagen2026-10-10 12:44 UTC220
    pub fn origin_etag(&self) -> String {
782069860236atagen2026-10-10 12:44 UTC221
        self.etag_at(0)
dfa38cf4cd4batagen2026-10-10 09:17 UTC222
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC223
dfa38cf4cd4batagen2026-10-10 09:17 UTC224
    pub fn touch(&mut self) {
dfa38cf4cd4batagen2026-10-10 09:17 UTC225
        self.version += 1;
dfa38cf4cd4batagen2026-10-10 09:17 UTC226
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC227
5dfebcc0ac43atagen2026-10-11 05:28 UTC228
    pub(super) fn shed(&mut self, friction: i32) {
5dfebcc0ac43atagen2026-10-11 05:28 UTC229
        self.friction = (self.friction - friction).max(0);
5dfebcc0ac43atagen2026-10-11 05:28 UTC230
    }
5dfebcc0ac43atagen2026-10-11 05:28 UTC231
14de3e1e8dc2atagen2026-10-11 06:19 UTC232
    /// Priapism holds EQ up no matter what else moves it.
14de3e1e8dc2atagen2026-10-11 06:19 UTC233
    pub(super) fn reset_eq(&mut self, eq: i32) {
14de3e1e8dc2atagen2026-10-11 06:19 UTC234
        let floor = if self.conditions.priapism_turns > 0 {
14de3e1e8dc2atagen2026-10-11 06:19 UTC235
            PRIAPISM_EQ
14de3e1e8dc2atagen2026-10-11 06:19 UTC236
        } else {
14de3e1e8dc2atagen2026-10-11 06:19 UTC237
            0
14de3e1e8dc2atagen2026-10-11 06:19 UTC238
        };
14de3e1e8dc2atagen2026-10-11 06:19 UTC239
        self.eq = eq.max(floor);
14de3e1e8dc2atagen2026-10-11 06:19 UTC240
    }
14de3e1e8dc2atagen2026-10-11 06:19 UTC241
e807266c323batagen2026-10-12 09:46 UTC242
    /// A melon offer stands only while the raiding streak continues. Any other action ends it.
5cc6ab966f30atagen2026-10-11 11:23 UTC243
    pub(super) fn end_kitchen_visit(&mut self) {
5cc6ab966f30atagen2026-10-11 11:23 UTC244
        self.here.melon_offered = false;
5cc6ab966f30atagen2026-10-11 11:23 UTC245
    }
5cc6ab966f30atagen2026-10-11 11:23 UTC246
e807266c323batagen2026-10-12 09:46 UTC247
    /// Going back to the jelq walks away from both the fridge and Pump-Bro.
e807266c323batagen2026-10-12 09:46 UTC248
    pub(super) fn step_away(&mut self) {
e807266c323batagen2026-10-12 09:46 UTC249
        self.end_kitchen_visit();
e807266c323batagen2026-10-12 09:46 UTC250
        self.here.shopping = false;
e807266c323batagen2026-10-12 09:46 UTC251
    }
e807266c323batagen2026-10-12 09:46 UTC252
dfa38cf4cd4batagen2026-10-10 09:17 UTC253
    fn fresh_nonce(&mut self) -> String {
3f9091557ec3atagen2026-10-11 11:43 UTC254
        format!("{:016x}", self.oracle.u64())
3f9091557ec3atagen2026-10-11 11:43 UTC255
    }
3f9091557ec3atagen2026-10-11 11:43 UTC256
3f9091557ec3atagen2026-10-11 11:43 UTC257
    /// One game decision for the oracle, phrased for the model and stamped
3f9091557ec3atagen2026-10-11 11:43 UTC258
    /// with the state as the player sees it.
3f9091557ec3atagen2026-10-11 11:43 UTC259
    fn decision(&self, site: &'static str, ask: impl std::fmt::Display) -> oracle::Roll {
3f9091557ec3atagen2026-10-11 11:43 UTC260
        oracle::Roll {
3f9091557ec3atagen2026-10-11 11:43 UTC261
            state: self.oracle_state(),
3f9091557ec3atagen2026-10-11 11:43 UTC262
            site,
3f9091557ec3atagen2026-10-11 11:43 UTC263
            ask: ask.to_string(),
3f9091557ec3atagen2026-10-11 11:43 UTC264
        }
3f9091557ec3atagen2026-10-11 11:43 UTC265
    }
3f9091557ec3atagen2026-10-11 11:43 UTC266
3f9091557ec3atagen2026-10-11 11:43 UTC267
    fn oracle_state(&self) -> String {
3f9091557ec3atagen2026-10-11 11:43 UTC268
        let mut s = String::new();
3f9091557ec3atagen2026-10-11 11:43 UTC269
        let place = match self.floor {
3f9091557ec3atagen2026-10-11 11:43 UTC270
            Floor::Bedroom => "the bedroom",
3f9091557ec3atagen2026-10-11 11:43 UTC271
            Floor::Bathroom => "the bathroom",
3f9091557ec3atagen2026-10-11 11:43 UTC272
            Floor::ChangingRoom => "the changing room",
3f9091557ec3atagen2026-10-11 11:43 UTC273
            Floor::Nans => "Nan's house",
3f9091557ec3atagen2026-10-11 11:43 UTC274
            Floor::Hotel => "the hotel room",
3f9091557ec3atagen2026-10-11 11:43 UTC275
            Floor::Forum => "the forum floor",
3f9091557ec3atagen2026-10-11 11:43 UTC276
            Floor::Clinic => "the clinic",
3f9091557ec3atagen2026-10-11 11:43 UTC277
        };
3f9091557ec3atagen2026-10-11 11:43 UTC278
        s.push_str(&format!(
3f9091557ec3atagen2026-10-11 11:43 UTC279
            "Floor {}, {place}. Turn {}.\n\n",
3f9091557ec3atagen2026-10-11 11:43 UTC280
            self.floor.number(),
3f9091557ec3atagen2026-10-11 11:43 UTC281
            self.turn
3f9091557ec3atagen2026-10-11 11:43 UTC282
        ));
3f9091557ec3atagen2026-10-11 11:43 UTC283
        s.push_str(&format!(
3f9091557ec3atagen2026-10-11 11:43 UTC284
            "eq: {}\nstamina: {}\nfriction: {}\n",
3f9091557ec3atagen2026-10-11 11:43 UTC285
            self.eq, self.stamina, self.friction
3f9091557ec3atagen2026-10-11 11:43 UTC286
        ));
3f9091557ec3atagen2026-10-11 11:43 UTC287
        if self.conditions.friction_burn {
3f9091557ec3atagen2026-10-11 11:43 UTC288
            s.push_str("friction burn: yes, untreated\n");
3f9091557ec3atagen2026-10-11 11:43 UTC289
        }
3f9091557ec3atagen2026-10-11 11:43 UTC290
        if self.conditions.priapism_turns > 0 {
3f9091557ec3atagen2026-10-11 11:43 UTC291
            s.push_str("priapism: yes\n");
3f9091557ec3atagen2026-10-11 11:43 UTC292
        }
3f9091557ec3atagen2026-10-11 11:43 UTC293
        if self.conditions.peyronie {
3f9091557ec3atagen2026-10-11 11:43 UTC294
            s.push_str("peyronie's disease: yes\n");
3f9091557ec3atagen2026-10-11 11:43 UTC295
        }
3f9091557ec3atagen2026-10-11 11:43 UTC296
        if self.conditions.pulled_muscle {
3f9091557ec3atagen2026-10-11 11:43 UTC297
            s.push_str("pulled muscle: yes\n");
3f9091557ec3atagen2026-10-11 11:43 UTC298
        }
3f9091557ec3atagen2026-10-11 11:43 UTC299
        s.push_str(&format!(
3f9091557ec3atagen2026-10-11 11:43 UTC300
            "suspicion: {}\nshame: {}\nwarmed up: {}\ngrip: {}\n",
3f9091557ec3atagen2026-10-11 11:43 UTC301
            self.suspicion,
3f9091557ec3atagen2026-10-11 11:43 UTC302
            self.shame,
3f9091557ec3atagen2026-10-11 11:43 UTC303
            if self.warmed_up { "yes" } else { "no" },
3f9091557ec3atagen2026-10-11 11:43 UTC304
            self.grip.name(),
3f9091557ec3atagen2026-10-11 11:43 UTC305
        ));
3f9091557ec3atagen2026-10-11 11:43 UTC306
        let held: Vec<&str> = self.items.owned().iter().map(|item| item.slug()).collect();
3f9091557ec3atagen2026-10-11 11:43 UTC307
        if !held.is_empty() {
3f9091557ec3atagen2026-10-11 11:43 UTC308
            s.push_str(&format!("items: {}\n", held.join(", ")));
3f9091557ec3atagen2026-10-11 11:43 UTC309
        }
3f9091557ec3atagen2026-10-11 11:43 UTC310
        if self.mum.is_some() {
3f9091557ec3atagen2026-10-11 11:43 UTC311
            s.push_str("Mum is at the door\n");
3f9091557ec3atagen2026-10-11 11:43 UTC312
        }
3f9091557ec3atagen2026-10-11 11:43 UTC313
        s
dfa38cf4cd4batagen2026-10-10 09:17 UTC314
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC315
3f9091557ec3atagen2026-10-11 11:43 UTC316
    fn chance(&mut self, roll: oracle::Roll, odds: u32) -> bool {
3f9091557ec3atagen2026-10-11 11:43 UTC317
        self.oracle.chance(roll, odds)
782069860236atagen2026-10-10 12:44 UTC318
    }
782069860236atagen2026-10-10 12:44 UTC319
782069860236atagen2026-10-10 12:44 UTC320
    pub fn path(&self, sub: &str) -> String {
782069860236atagen2026-10-10 12:44 UTC321
        format!("/runs/{}{sub}", self.id)
782069860236atagen2026-10-10 12:44 UTC322
    }
782069860236atagen2026-10-10 12:44 UTC323
dfa38cf4cd4batagen2026-10-10 09:17 UTC324
    pub fn current_length(&self) -> f64 {
782069860236atagen2026-10-10 12:44 UTC325
        self.length + self.here.temp_length
dfa38cf4cd4batagen2026-10-10 09:17 UTC326
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC327
dfa38cf4cd4batagen2026-10-10 09:17 UTC328
    pub fn self_reported(&self) -> f64 {
dfa38cf4cd4batagen2026-10-10 09:17 UTC329
        self.current_length() + SELF_REPORT_INFLATION
dfa38cf4cd4batagen2026-10-10 09:17 UTC330
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC331
c3e0c27cf8fdatagen2026-10-10 11:28 UTC332
    pub fn max_stamina(&self) -> i32 {
782069860236atagen2026-10-10 12:44 UTC333
        if self.conditions.pulled_muscle {
782069860236atagen2026-10-10 12:44 UTC334
            PULLED_STAMINA
782069860236atagen2026-10-10 12:44 UTC335
        } else {
782069860236atagen2026-10-10 12:44 UTC336
            MAX_STAMINA
782069860236atagen2026-10-10 12:44 UTC337
        }
782069860236atagen2026-10-10 12:44 UTC338
    }
782069860236atagen2026-10-10 12:44 UTC339
782069860236atagen2026-10-10 12:44 UTC340
    pub fn forum_read(&self) -> bool {
782069860236atagen2026-10-10 12:44 UTC341
        self.story == Story::ReadForum
c3e0c27cf8fdatagen2026-10-10 11:28 UTC342
    }
c3e0c27cf8fdatagen2026-10-10 11:28 UTC343
e807266c323batagen2026-10-12 09:46 UTC344
    pub fn thread_reachable(&self) -> bool {
e807266c323batagen2026-10-12 09:46 UTC345
        self.turn >= BRANCH_TURN
e807266c323batagen2026-10-12 09:46 UTC346
            || self.forum_read()
e807266c323batagen2026-10-12 09:46 UTC347
            || self.floor.number() >= Floor::Forum.number()
e807266c323batagen2026-10-12 09:46 UTC348
    }
e807266c323batagen2026-10-12 09:46 UTC349
c3e0c27cf8fdatagen2026-10-10 11:28 UTC350
    pub fn footage(&self) -> Option<(SystemTime, u64)> {
c3e0c27cf8fdatagen2026-10-10 11:28 UTC351
        let filmed = self.filmed_at?;
c3e0c27cf8fdatagen2026-10-10 11:28 UTC352
        let age = filmed.elapsed().map_or(0, |d| d.as_secs());
c3e0c27cf8fdatagen2026-10-10 11:28 UTC353
        Some((filmed, age))
c3e0c27cf8fdatagen2026-10-10 11:28 UTC354
    }
c3e0c27cf8fdatagen2026-10-10 11:28 UTC355
c4d70621d377atagen2026-10-12 11:56 UTC356
    pub fn status(&self) -> RunStatus {
c4d70621d377atagen2026-10-12 11:56 UTC357
        if self.ended.is_some() {
c4d70621d377atagen2026-10-12 11:56 UTC358
            RunStatus::Gone
c4d70621d377atagen2026-10-12 11:56 UTC359
        } else if self.clinic.cured {
c4d70621d377atagen2026-10-12 11:56 UTC360
            RunStatus::Normal
c4d70621d377atagen2026-10-12 11:56 UTC361
        } else if self.clinic.prescribed {
c4d70621d377atagen2026-10-12 11:56 UTC362
            RunStatus::Prescribed
c4d70621d377atagen2026-10-12 11:56 UTC363
        } else {
c4d70621d377atagen2026-10-12 11:56 UTC364
            RunStatus::Alive
dfa38cf4cd4batagen2026-10-10 09:17 UTC365
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC366
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC367
782069860236atagen2026-10-10 12:44 UTC368
    /// The parts of the run that its response headers do not already carry.
dfa38cf4cd4batagen2026-10-10 09:17 UTC369
    pub fn view(&self) -> Value {
dfa38cf4cd4batagen2026-10-10 09:17 UTC370
        let conditions: Vec<&str> = [
782069860236atagen2026-10-10 12:44 UTC371
            (self.here.teapot, "short and stout"),
782069860236atagen2026-10-10 12:44 UTC372
            (self.conditions.peyronie, "curved"),
dfa38cf4cd4batagen2026-10-10 09:17 UTC373
            (self.items.cage, "caged"),
782069860236atagen2026-10-10 12:44 UTC374
            (self.conditions.priapism_turns > 0, "priapism"),
782069860236atagen2026-10-10 12:44 UTC375
            (self.conditions.pulled_muscle, "pulled muscle"),
c3e0c27cf8fdatagen2026-10-10 11:28 UTC376
            (self.filmed_at.is_some(), "viral"),
dfa38cf4cd4batagen2026-10-10 09:17 UTC377
        ]
dfa38cf4cd4batagen2026-10-10 09:17 UTC378
        .into_iter()
dfa38cf4cd4batagen2026-10-10 09:17 UTC379
        .filter_map(|(active, name)| active.then_some(name))
dfa38cf4cd4batagen2026-10-10 09:17 UTC380
        .collect();
782069860236atagen2026-10-10 12:44 UTC381
        json!({
782069860236atagen2026-10-10 12:44 UTC382
            "public_id": self.public_id,
c4d70621d377atagen2026-10-12 11:56 UTC383
            "status": self.status().name(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC384
            "turn": self.turn,
782069860236atagen2026-10-10 12:44 UTC385
            "run_shame": self.shame,
dfa38cf4cd4batagen2026-10-10 09:17 UTC386
            "grip": self.grip.name(),
dfa38cf4cd4batagen2026-10-10 09:17 UTC387
            "warmed_up": self.warmed_up,
99524cf9f17catagen2026-10-10 10:53 UTC388
            "ranked": !self.seeded,
dfa38cf4cd4batagen2026-10-10 09:17 UTC389
            "conditions": conditions,
782069860236atagen2026-10-10 12:44 UTC390
        })
dfa38cf4cd4batagen2026-10-10 09:17 UTC391
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC392
782069860236atagen2026-10-10 12:44 UTC393
    pub fn end(&mut self, cause: Cause) -> Problem {
dfa38cf4cd4batagen2026-10-10 09:17 UTC394
        self.ended = Some(Ending {
dfa38cf4cd4batagen2026-10-10 09:17 UTC395
            cause,
dfa38cf4cd4batagen2026-10-10 09:17 UTC396
            turn: self.turn,
dfa38cf4cd4batagen2026-10-10 09:17 UTC397
        });
782069860236atagen2026-10-10 12:44 UTC398
        cause.problem().with("turn", self.turn)
dfa38cf4cd4batagen2026-10-10 09:17 UTC399
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC400
a6caa2f99786atagen2026-10-12 10:38 UTC401
    /// The refractory period is the next HOLD. It comes back with full stamina and nothing else.
a6caa2f99786atagen2026-10-12 10:38 UTC402
    fn recover(&mut self) {
a6caa2f99786atagen2026-10-12 10:38 UTC403
        self.exhausted = false;
a6caa2f99786atagen2026-10-12 10:38 UTC404
        self.stamina = self.max_stamina();
a6caa2f99786atagen2026-10-12 10:38 UTC405
        self.reset_eq(STARTING_EQ);
a6caa2f99786atagen2026-10-12 10:38 UTC406
        self.warmed_up = false;
e76ab808e7adatagen2026-10-10 13:46 UTC407
    }
e76ab808e7adatagen2026-10-10 13:46 UTC408
a6caa2f99786atagen2026-10-12 10:38 UTC409
    fn check_exhausted(&self) -> Result<(), Problem> {
a6caa2f99786atagen2026-10-12 10:38 UTC410
        if !self.exhausted {
dfa38cf4cd4batagen2026-10-10 09:17 UTC411
            return Ok(());
a6caa2f99786atagen2026-10-12 10:38 UTC412
        }
e76ab808e7adatagen2026-10-10 13:46 UTC413
        Err(Problem::new(
e76ab808e7adatagen2026-10-10 13:46 UTC414
            StatusCode::TOO_MANY_REQUESTS,
e76ab808e7adatagen2026-10-10 13:46 UTC415
            "overtrained",
a6caa2f99786atagen2026-10-12 10:38 UTC416
            "Stamina is exhausted. HOLD for the refractory period.",
a6caa2f99786atagen2026-10-12 10:38 UTC417
        ))
dfa38cf4cd4batagen2026-10-10 09:17 UTC418
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC419
a6caa2f99786atagen2026-10-12 10:38 UTC420
    fn exhaust(&mut self) {
782069860236atagen2026-10-10 12:44 UTC421
        self.stamina = 0;
a6caa2f99786atagen2026-10-12 10:38 UTC422
        self.exhausted = true;
782069860236atagen2026-10-10 12:44 UTC423
    }
782069860236atagen2026-10-10 12:44 UTC424
9683736a2c5eatagen2026-10-10 13:15 UTC425
    fn realise(&mut self) {
dfa38cf4cd4batagen2026-10-10 09:17 UTC426
        let (due, waiting): (Vec<_>, Vec<_>) = std::mem::take(&mut self.pending)
dfa38cf4cd4batagen2026-10-10 09:17 UTC427
            .into_iter()
dfa38cf4cd4batagen2026-10-10 09:17 UTC428
            .partition(|p| p.due <= self.turn);
dfa38cf4cd4batagen2026-10-10 09:17 UTC429
        self.pending = waiting;
dfa38cf4cd4batagen2026-10-10 09:17 UTC430
        for p in due {
dfa38cf4cd4batagen2026-10-10 09:17 UTC431
            self.length += p.length;
dfa38cf4cd4batagen2026-10-10 09:17 UTC432
            self.girth += p.girth;
dfa38cf4cd4batagen2026-10-10 09:17 UTC433
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC434
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC435
dfa38cf4cd4batagen2026-10-10 09:17 UTC436
    fn gain(&mut self, length: f64, girth: f64, foreskin: f64) {
dfa38cf4cd4batagen2026-10-10 09:17 UTC437
        self.length += length;
dfa38cf4cd4batagen2026-10-10 09:17 UTC438
        self.girth += girth;
dfa38cf4cd4batagen2026-10-10 09:17 UTC439
        if let Some(f) = &mut self.foreskin {
dfa38cf4cd4batagen2026-10-10 09:17 UTC440
            *f += foreskin;
dfa38cf4cd4batagen2026-10-10 09:17 UTC441
        }
dfa38cf4cd4batagen2026-10-10 09:17 UTC442
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC443
dfa38cf4cd4batagen2026-10-10 09:17 UTC444
    fn rupture(&mut self) -> Problem {
782069860236atagen2026-10-10 12:44 UTC445
        let cause = Cause::Ruptured {
782069860236atagen2026-10-10 12:44 UTC446
            ice_unused: self.items.ice_packs > 0,
dfa38cf4cd4batagen2026-10-10 09:17 UTC447
        };
782069860236atagen2026-10-10 12:44 UTC448
        self.end(cause)
dfa38cf4cd4batagen2026-10-10 09:17 UTC449
    }
dfa38cf4cd4batagen2026-10-10 09:17 UTC450
}