atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
8b8e83329a1daec70177de3e129964094a711509
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
src/game/run.rsRust364 lines10.6 KB
1
use std::{
2
    hash::{DefaultHasher, Hash, Hasher},
3
    time::{Instant, SystemTime},
4
};
5
6
use axum::http::StatusCode;
7
use serde_json::{Value, json};
8
9
use super::{
10
    Body, Cause, Clinic, Ending, Floor, FloorState, Household, Items, Notice, Observer, Online,
11
    RunStatus, Standing, Story,
12
    balance::{
13
        BRANCH_TURN, MAX_STAMINA, PRIAPISM_EQ, PULLED_STAMINA, SELF_REPORT_INFLATION, STARTING_EQ,
14
    },
15
    measure::round2,
16
    oracle::{Oracle, OracleTemplate, Roll},
17
};
18
use crate::problem::Problem;
19
20
pub struct Identity {
21
    /// The private id in the run's URL. Knowing it is the only authorisation a run has.
22
    pub(crate) id: String,
23
    pub(crate) public_id: String,
24
    pub(crate) seeded: bool,
25
    etag_salt: u64,
26
    pub(crate) version: u64,
27
    pub(crate) last_seen: Instant,
28
}
29
30
pub struct Run {
31
    pub(crate) ident: Identity,
32
    pub(crate) body: Body,
33
    pub(crate) standing: Standing,
34
    pub(crate) household: Household,
35
    pub(crate) online: Online,
36
    pub(crate) items: Items,
37
    pub(crate) clinic: Clinic,
38
    pub(crate) turn: u32,
39
    pub(crate) floor: Floor,
40
    pub(crate) here: FloorState,
41
    pub(crate) observed: Option<Observer>,
42
    pub(crate) notices: Vec<Notice>,
43
    pub(crate) ended: Option<Ending>,
44
    pub(super) oracle: Oracle,
45
}
46
47
impl Run {
48
    #[cfg(test)]
49
    pub fn new(id: String, public_id: String, seed: u64) -> Self {
50
        Self::with_oracle(id, public_id, seed, false, &OracleTemplate::Rng)
51
    }
52
53
    pub fn with_oracle(
54
        id: String,
55
        public_id: String,
56
        seed: u64,
57
        seeded: bool,
58
        template: &OracleTemplate,
59
    ) -> Self {
60
        let mut oracle = Oracle::build(template, seed);
61
        let length = round2(oracle.span(
62
            Run::unborn("length", "How long is it, in inches?"),
63
            4.5,
64
            6.5,
65
        ));
66
        let girth = round2(oracle.span(
67
            Run::unborn("girth", "How thick is it, in inches?"),
68
            4.0,
69
            5.0,
70
        ));
71
        let foreskin = round2(oracle.span(
72
            Run::unborn("foreskin", "How much foreskin is there, in inches?"),
73
            0.4,
74
            2.4,
75
        ));
76
        let etag_salt = oracle.u64();
77
        Self {
78
            ident: Identity {
79
                id,
80
                public_id,
81
                seeded,
82
                etag_salt,
83
                version: 0,
84
                last_seen: Instant::now(),
85
            },
86
            body: Body::new(length, girth, foreskin),
87
            standing: Standing::default(),
88
            household: Household::default(),
89
            online: Online::default(),
90
            items: Items::default(),
91
            clinic: Clinic::default(),
92
            turn: 0,
93
            floor: Floor::Bedroom,
94
            here: FloorState::default(),
95
            observed: None,
96
            notices: Vec::new(),
97
            ended: None,
98
            oracle,
99
        }
100
    }
101
102
    fn unborn(site: &'static str, ask: &'static str) -> Roll {
103
        Roll {
104
            state: "A new run is being created. No game has been played yet.".into(),
105
            site,
106
            ask,
107
        }
108
    }
109
110
    fn etag_at(&self, version: u64) -> String {
111
        let mut hasher = DefaultHasher::new();
112
        (self.ident.etag_salt, version).hash(&mut hasher);
113
        format!("\"{:016x}\"", hasher.finish())
114
    }
115
116
    pub fn etag(&self) -> String {
117
        self.etag_at(self.ident.version)
118
    }
119
120
    /// The ETag the run had when it was created, which the true ending asks for.
121
    pub fn origin_etag(&self) -> String {
122
        self.etag_at(0)
123
    }
124
125
    pub fn touch(&mut self) {
126
        self.ident.version += 1;
127
    }
128
129
    pub(super) fn begin_turn(&mut self) {
130
        self.touch();
131
        self.turn += 1;
132
        self.here.turns += 1;
133
        self.standing.at_turn_start = self.standing.suspicion;
134
        self.realise();
135
    }
136
137
    pub(super) fn shed(&mut self, friction: i32) {
138
        self.body.friction = (self.body.friction - friction).max(0);
139
    }
140
141
    /// Priapism holds EQ up no matter what else moves it.
142
    pub(super) fn reset_eq(&mut self, eq: i32) {
143
        let floor = if self.body.conditions.priapism_turns > 0 {
144
            PRIAPISM_EQ
145
        } else {
146
            0
147
        };
148
        self.body.eq = eq.max(floor);
149
    }
150
151
    pub(super) fn end_kitchen_visit(&mut self) {
152
        self.here.kitchen.melon_offered = false;
153
    }
154
155
    /// Going back to the jelq walks away from both the fridge and Pump-Bro.
156
    pub(super) fn step_away(&mut self) {
157
        self.end_kitchen_visit();
158
        self.here.shopping = false;
159
    }
160
161
    pub(super) fn fresh_nonce(&mut self) -> String {
162
        format!("{:016x}", self.oracle.u64())
163
    }
164
165
    /// One game decision. The dice ignore the state prose, so it is only written for the model.
166
    pub(super) fn decision(&self, site: &'static str, ask: &'static str) -> Roll {
167
        Roll {
168
            state: if self.oracle.listens() {
169
                self.oracle_state()
170
            } else {
171
                String::new()
172
            },
173
            site,
174
            ask,
175
        }
176
    }
177
178
    fn oracle_state(&self) -> String {
179
        let mut s = format!(
180
            "Floor {}, {}. Turn {}.\n\neq: {}\nstamina: {}\nfriction: {}\n",
181
            self.floor.number(),
182
            self.floor.place(),
183
            self.turn,
184
            self.body.eq,
185
            self.body.stamina,
186
            self.body.friction
187
        );
188
        let conditions = &self.body.conditions;
189
        for (active, line) in [
190
            (conditions.friction_burn, "friction burn: yes, untreated\n"),
191
            (conditions.priapism_turns > 0, "priapism: yes\n"),
192
            (conditions.peyronie, "peyronie's disease: yes\n"),
193
            (conditions.pulled_muscle, "pulled muscle: yes\n"),
194
        ] {
195
            if active {
196
                s.push_str(line);
197
            }
198
        }
199
        s.push_str(&format!(
200
            "suspicion: {}\nshame: {}\nwarmed up: {}\ngrip: {}\n",
201
            self.standing.suspicion,
202
            self.standing.shame,
203
            if self.body.warmed_up { "yes" } else { "no" },
204
            self.body.grip.name(),
205
        ));
206
        let held: Vec<&str> = self.items.owned().iter().map(|item| item.slug()).collect();
207
        if !held.is_empty() {
208
            s.push_str(&format!("items: {}\n", held.join(", ")));
209
        }
210
        if self.household.mum.is_some() {
211
            s.push_str("Mum is at the door\n");
212
        }
213
        s
214
    }
215
216
    pub(super) fn chance(&mut self, roll: Roll, odds: u32) -> bool {
217
        self.oracle.chance(roll, odds)
218
    }
219
220
    pub fn path(&self, sub: &str) -> String {
221
        format!("/runs/{}{sub}", self.ident.id)
222
    }
223
224
    pub fn current_length(&self) -> f64 {
225
        self.body.length + self.here.temp_length
226
    }
227
228
    pub fn self_reported(&self) -> f64 {
229
        self.current_length() + SELF_REPORT_INFLATION
230
    }
231
232
    pub fn shown_length(&self) -> f64 {
233
        if self.items.kit.ruler {
234
            self.current_length()
235
        } else {
236
            self.self_reported()
237
        }
238
    }
239
240
    pub fn max_stamina(&self) -> i32 {
241
        if self.body.conditions.pulled_muscle {
242
            PULLED_STAMINA
243
        } else {
244
            MAX_STAMINA
245
        }
246
    }
247
248
    pub fn forum_read(&self) -> bool {
249
        self.online.story == Story::ReadForum
250
    }
251
252
    pub fn thread_reachable(&self) -> bool {
253
        self.turn >= BRANCH_TURN
254
            || self.forum_read()
255
            || self.floor.number() >= Floor::Forum.number()
256
    }
257
258
    pub fn footage(&self) -> Option<(SystemTime, u64)> {
259
        let filmed = self.online.filmed_at?;
260
        let age = filmed.elapsed().map_or(0, |d| d.as_secs());
261
        Some((filmed, age))
262
    }
263
264
    pub fn status(&self) -> RunStatus {
265
        if self.ended.is_some() {
266
            RunStatus::Gone
267
        } else if self.clinic.cured {
268
            RunStatus::Normal
269
        } else if self.clinic.prescribed {
270
            RunStatus::Prescribed
271
        } else {
272
            RunStatus::Alive
273
        }
274
    }
275
276
    /// The parts of the run that its response headers do not already carry.
277
    pub fn view(&self) -> Value {
278
        let conditions: Vec<&str> = [
279
            (self.here.teapot, "short and stout"),
280
            (self.body.conditions.peyronie, "curved"),
281
            (self.items.kit.cage, "caged"),
282
            (self.body.conditions.priapism_turns > 0, "priapism"),
283
            (self.body.conditions.pulled_muscle, "pulled muscle"),
284
            (self.online.filmed_at.is_some(), "viral"),
285
        ]
286
        .into_iter()
287
        .filter_map(|(active, name)| active.then_some(name))
288
        .collect();
289
        json!({
290
            "public_id": self.ident.public_id,
291
            "status": self.status().name(),
292
            "turn": self.turn,
293
            "run_shame": self.standing.shame,
294
            "grip": self.body.grip.name(),
295
            "warmed_up": self.body.warmed_up,
296
            "ranked": !self.ident.seeded,
297
            "conditions": conditions,
298
        })
299
    }
300
301
    pub fn end(&mut self, cause: Cause) -> Problem {
302
        self.ended = Some(Ending {
303
            cause,
304
            turn: self.turn,
305
        });
306
        cause.problem().with("turn", self.turn)
307
    }
308
309
    /// The refractory period is the next HOLD. It comes back with full stamina and nothing else.
310
    pub(super) fn recover(&mut self) {
311
        self.body.exhausted = false;
312
        self.body.stamina = self.max_stamina();
313
        self.reset_eq(STARTING_EQ);
314
        self.body.warmed_up = false;
315
    }
316
317
    pub(super) fn check_exhausted(&self) -> Result<(), Problem> {
318
        if !self.body.exhausted {
319
            return Ok(());
320
        }
321
        Err(Problem::new(
322
            StatusCode::TOO_MANY_REQUESTS,
323
            "overtrained",
324
            "Stamina is exhausted. HOLD for the refractory period.",
325
        ))
326
    }
327
328
    pub(super) fn exhaust(&mut self) {
329
        self.body.stamina = 0;
330
        self.body.exhausted = true;
331
    }
332
333
    pub(super) fn realise(&mut self) {
334
        let (due, waiting): (Vec<_>, Vec<_>) = std::mem::take(&mut self.body.pending)
335
            .into_iter()
336
            .partition(|p| p.due <= self.turn);
337
        self.body.pending = waiting;
338
        for p in due {
339
            self.gain(p.length, p.girth, p.foreskin);
340
        }
341
    }
342
343
    pub(super) fn gain(&mut self, length: f64, girth: f64, foreskin: f64) {
344
        self.body.length += length;
345
        self.body.girth += girth;
346
        if let Some(f) = &mut self.body.foreskin {
347
            *f += foreskin;
348
        }
349
    }
350
351
    pub(super) fn rupture(&mut self) -> Problem {
352
        let cause = Cause::Ruptured {
353
            ice_unused: self.items.stores.ice_packs > 0,
354
        };
355
        self.end(cause)
356
    }
357
358
    /// A run whose lock was poisoned by a panic mid-turn cannot be trusted to carry on.
359
    pub fn lose(&mut self) {
360
        if self.ended.is_none() {
361
            self.end(Cause::Lost);
362
        }
363
    }
364
}