atagen

jelq

public

an http based roguelike rpg about jelqing

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