atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
98432b6057aa79c009e5831b314585cd777f39c2
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
src/tests/turns.rsRust424 lines15.5 KB
1
use super::*;
2
3
#[tokio::test]
4
async fn meditation_is_free_but_only_once_per_floor() {
5
    let app = seeded_with(Floor::Bedroom, |run| run.online.meditation_taught = true);
6
    let response = send(&app, "JELQ", "/runs/cafebabe?technique=meditate", &[], "").await;
7
    assert_eq!(response.status(), StatusCode::OK);
8
    assert_eq!(header(&response, "x-eq"), "10");
9
    assert_eq!(header(&response, "x-stamina"), "10/10");
10
    let again = send(&app, "JELQ", "/runs/cafebabe?technique=meditate", &[], "").await;
11
    assert_eq!(again.status(), StatusCode::CONFLICT);
12
}
13
14
#[tokio::test]
15
async fn meditation_respects_the_priapism_floor() {
16
    let app = seeded_with(Floor::Bedroom, |run| {
17
        run.body.eq = 80;
18
        run.body.conditions.priapism_turns = 3;
19
        run.online.meditation_taught = true;
20
    });
21
    let calmed = send(&app, "JELQ", "/runs/cafebabe?technique=meditate", &[], "").await;
22
    assert_eq!(calmed.status(), StatusCode::OK);
23
    let view = send(&app, "GET", "/runs/cafebabe", &[], "").await;
24
    assert_eq!(header(&view, "x-eq"), "75");
25
}
26
27
#[tokio::test]
28
async fn priapism_holds_the_floor_through_every_reset() {
29
    let exit = seeded_with(Floor::Bedroom, |run| {
30
        run.body.eq = 90;
31
        run.here.turns = 99;
32
        run.body.conditions.priapism_turns = 3;
33
    });
34
    let out = send(&exit, "POST", "/runs/cafebabe/floor", &[], "").await;
35
    assert_eq!(out.status(), StatusCode::SEE_OTHER);
36
    let view = send(&exit, "GET", "/runs/cafebabe", &[], "").await;
37
    assert_eq!(header(&view, "x-eq"), "75");
38
39
    let melon = seeded_with(Floor::Bedroom, |run| {
40
        run.body.eq = 95;
41
        run.here.kitchen.melon_offered = true;
42
        run.body.conditions.priapism_turns = 2;
43
    });
44
    let bite = send(&melon, "POST", "/runs/cafebabe/kitchen/watermelon", &[], "").await;
45
    assert_eq!(bite.status(), StatusCode::OK);
46
    let view = send(&melon, "GET", "/runs/cafebabe", &[], "").await;
47
    assert_eq!(header(&view, "x-eq"), "75");
48
}
49
50
#[tokio::test]
51
async fn the_towel_warms_once_until_the_kettle_boils() {
52
    let app = seeded_with(Floor::Bedroom, |_| {});
53
    let warm = send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
54
    assert_eq!(warm.status(), StatusCode::OK);
55
    let again = send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
56
    assert_eq!(again.status(), StatusCode::CONFLICT);
57
58
    let app = seeded_with(Floor::Bedroom, |run| {
59
        run.here.towel_warmed = true;
60
        run.here.kitchen.kettle_boiled = true;
61
    });
62
    let reheated = send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
63
    assert_eq!(reheated.status(), StatusCode::OK);
64
    let cold = send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
65
    assert_eq!(cold.status(), StatusCode::CONFLICT);
66
}
67
68
#[tokio::test]
69
async fn the_refractory_period_is_the_next_hold() {
70
    let app = seeded_with(Floor::Hotel, |run| {
71
        run.here.wifi_accepted = true;
72
        run.here.towel_warmed = true;
73
        run.body.eq = 60;
74
        run.body.stamina = 0;
75
        run.body.exhausted = true;
76
    });
77
    let tired = send(&app, "JELQ", "/runs/cafebabe?technique=edge", &[], "").await;
78
    assert_eq!(tired.status(), StatusCode::TOO_MANY_REQUESTS);
79
    let hold = send(&app, "HOLD", "/runs/cafebabe", &[], "").await;
80
    assert_eq!(hold.status(), StatusCode::OK);
81
    assert!(["4/10", "5/10", "6/10", "10/10"].contains(&header(&hold, "x-stamina")));
82
    let towel = send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
83
    assert_eq!(towel.status(), StatusCode::CONFLICT, "the towel stays cold");
84
}
85
86
#[tokio::test]
87
async fn a_refused_jelq_does_not_break_the_edging_streak() {
88
    let app = seeded_with(Floor::ChangingRoom, |run| {
89
        run.body.eq = 45;
90
        run.here.edges = 2;
91
    });
92
    let refused = send(
93
        &app,
94
        "JELQ",
95
        "/runs/cafebabe?technique=dry",
96
        &[("content-type", "application/baby-oil")],
97
        "",
98
    )
99
    .await;
100
    assert_eq!(refused.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
101
    let third = send(&app, "JELQ", "/runs/cafebabe?technique=edge", &[], "").await;
102
    assert_eq!(third.status(), StatusCode::LOOP_DETECTED);
103
}
104
105
#[tokio::test]
106
async fn an_interrupted_edge_keeps_no_gains() {
107
    let app = seeded_with(Floor::Hotel, |run| {
108
        run.here.wifi_accepted = true;
109
        run.body.eq = 100;
110
    });
111
    let before = send(&app, "GET", "/runs/cafebabe", &[], "").await;
112
    let girth = header(&before, "x-girth").to_owned();
113
    let edge = send(&app, "JELQ", "/runs/cafebabe?technique=edge", &[], "").await;
114
    assert!(!edge.status().is_success());
115
    assert_eq!(header(&edge, "x-girth"), girth);
116
}
117
118
#[tokio::test]
119
async fn greed_multiplies_gains_above_the_band() {
120
    let lube = [("content-type", "application/baby-oil")];
121
    for (eq, greed) in [(75, 1.5), (88, 2.0)] {
122
        let mut compared = false;
123
        for seed in 0..120u64 {
124
            let app = app_with(Floor::Bedroom, seed, |run| {
125
                run.body.eq = 60;
126
            });
127
            let in_band = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &lube, "").await;
128
            let app = app_with(Floor::Bedroom, seed, |run| {
129
                run.body.eq = eq;
130
            });
131
            let greedy = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &lube, "").await;
132
            if in_band.status() != StatusCode::OK || greedy.status() != StatusCode::OK {
133
                continue;
134
            }
135
            let steady = json(in_band).await["length"].as_f64().unwrap();
136
            let eager = json(greedy).await["length"].as_f64().unwrap();
137
            assert!(
138
                (eager - steady * greed).abs() < 1e-9,
139
                "seed {seed} gained {eager} at {eq} EQ against {steady} inside the band"
140
            );
141
            compared = true;
142
            break;
143
        }
144
        assert!(compared, "no seed produced two clean rolls at {eq} EQ");
145
    }
146
}
147
148
#[tokio::test]
149
async fn queued_gains_arrive_on_their_turn() {
150
    let mut run = Run::new("cafebabe".into(), "cafe".into(), SEED);
151
    run.body.eq = 75;
152
    let before = run.body.length;
153
    run.body.pending.push(crate::game::Pending {
154
        due: run.turn + 1,
155
        length: 0.5,
156
        girth: 0.0,
157
        foreskin: 0.0,
158
    });
159
    assert!(run.hold().is_ok());
160
    assert!((run.body.length - before - 0.5).abs() < 1e-9);
161
    assert!(run.body.pending.is_empty());
162
}
163
164
#[tokio::test]
165
async fn hold_keeps_the_shop_and_the_melon_open() {
166
    let hotel = seeded_with(Floor::Hotel, |run| {
167
        run.here.wifi_accepted = true;
168
        run.body.eq = 80;
169
    });
170
    send(&hotel, "GET", "/runs/cafebabe/shop", &[], "").await;
171
    let held = send(&hotel, "HOLD", "/runs/cafebabe", &[], "").await;
172
    assert!(linked_rels(&held).contains(&"shop-item".to_owned()));
173
174
    let mut run = Run::new("cafebabe".into(), "cafe".into(), SEED);
175
    run.body.eq = 80;
176
    run.here.kitchen.melon_offered = true;
177
    assert!(run.hold().is_ok());
178
    assert!(run.here.kitchen.melon_offered, "the offer survives a HOLD");
179
}
180
181
#[tokio::test]
182
async fn leaving_the_band_warms_the_next_floor() {
183
    let app = seeded_with(Floor::Bedroom, |run| {
184
        run.body.eq = 60;
185
        run.here.turns = 6;
186
        run.items.lubes.baby_oil = 9;
187
    });
188
    let left = send(&app, "POST", "/runs/cafebabe/floor", &[], "").await;
189
    assert_eq!(left.status(), StatusCode::SEE_OTHER);
190
    let detail = json(left).await["detail"].as_str().unwrap().to_owned();
191
    assert!(detail.contains("the warmth came with you"));
192
193
    let lube = [("content-type", "application/baby-oil")];
194
    send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
195
    // The shower is a +35 warm-up, so EQ lands in the band and the band bonus rides along.
196
    // The arrival bonus is the one point the second jelq loses.
197
    let first = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &lube, "").await;
198
    assert!(
199
        header(&first, "server-timing").contains("d20+4"),
200
        "the first jelq of a warm arrival rolls with +1, got {}",
201
        header(&first, "server-timing")
202
    );
203
    let second = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &lube, "").await;
204
    assert!(header(&second, "server-timing").contains("d20+3"));
205
}
206
207
#[tokio::test]
208
async fn energy_drinks_add_three_stamina() {
209
    let app = seeded_with(Floor::Bedroom, |run| {
210
        run.body.stamina = 2;
211
        run.items.stores.energy_drinks = 1;
212
    });
213
    let view = send(&app, "GET", "/runs/cafebabe", &[], "").await;
214
    assert!(header(&view, "link").contains("technique=energy"));
215
    let drink = send(&app, "JELQ", "/runs/cafebabe?technique=energy", &[], "").await;
216
    assert_eq!(drink.status(), StatusCode::OK);
217
    let view = send(&app, "GET", "/runs/cafebabe", &[], "").await;
218
    assert_eq!(header(&view, "x-stamina"), "5/10");
219
    let again = send(&app, "JELQ", "/runs/cafebabe?technique=energy", &[], "").await;
220
    assert_eq!(again.status(), StatusCode::FAILED_DEPENDENCY);
221
}
222
223
#[tokio::test]
224
async fn queued_gains_announce_their_arrival() {
225
    let app = seeded_with(Floor::Bedroom, |run| {
226
        run.body.eq = 75;
227
        run.body.foreskin = Some(1.0);
228
        run.body.pending.push(crate::game::Pending {
229
            due: run.turn + 1,
230
            length: 0.105,
231
            girth: 0.0,
232
            foreskin: 0.03,
233
        });
234
    });
235
    let held = send(&app, "HOLD", "/runs/cafebabe", &[], "").await;
236
    assert_eq!(
237
        header(&held, "x-gains-arrived"),
238
        "length=0.105, girth=0.000, foreskin=0.030"
239
    );
240
    assert!(
241
        warnings(&held)
242
            .iter()
243
            .any(|w| w.contains("the queued gains have arrived"))
244
    );
245
    let later = send(&app, "GET", "/runs/cafebabe", &[], "").await;
246
    assert!(later.headers().get("x-gains-arrived").is_none());
247
}
248
249
#[tokio::test]
250
async fn an_energy_drink_ends_exhaustion_without_the_refractory_period() {
251
    let app = seeded_with(Floor::Bedroom, |run| {
252
        run.body.eq = 60;
253
        run.body.stamina = 0;
254
        run.body.exhausted = true;
255
        run.items.stores.energy_drinks = 1;
256
    });
257
    let tired = send(&app, "GET", "/runs/cafebabe", &[], "").await;
258
    assert_eq!(advertised(&tired), ["energy"]);
259
    let drink = send(&app, "JELQ", "/runs/cafebabe?technique=energy", &[], "").await;
260
    assert_eq!(drink.status(), StatusCode::OK);
261
    assert_eq!(header(&drink, "x-stamina"), "3/10");
262
    assert_eq!(
263
        header(&drink, "x-eq"),
264
        "60",
265
        "still warm, unlike the refractory HOLD"
266
    );
267
    let jelq = send(&app, "JELQ", "/runs/cafebabe?technique=edge", &[], "").await;
268
    assert_ne!(jelq.status(), StatusCode::TOO_MANY_REQUESTS);
269
}
270
271
#[tokio::test]
272
async fn edging_climbs_out_of_the_cold() {
273
    let app = seeded_with(Floor::ChangingRoom, |run| run.body.eq = 20);
274
    let edge = send(&app, "JELQ", "/runs/cafebabe?technique=edge", &[], "").await;
275
    assert_eq!(edge.status(), StatusCode::OK);
276
    assert!(warnings(&edge).iter().all(|w| !w.contains("Cold start")));
277
    assert_eq!(
278
        header(&edge, "x-friction"),
279
        "2/10",
280
        "edging pays no cold tax"
281
    );
282
}
283
284
#[tokio::test]
285
async fn overstaying_a_floor_is_warned_then_punished() {
286
    let mut busted = 0;
287
    let mut moved = 0;
288
    for seed in 0..60u64 {
289
        let app = app_with(Floor::ChangingRoom, seed, |run| {
290
            run.body.eq = 80;
291
            run.body.conditions.priapism_turns = 9;
292
            run.here.turns = 7;
293
        });
294
        let eighth = send(&app, "HOLD", "/runs/cafebabe", &[], "").await;
295
        assert!(
296
            warnings(&eighth)
297
                .iter()
298
                .any(|w| w.contains("The gym closes"))
299
        );
300
        let ninth = send(&app, "HOLD", "/runs/cafebabe", &[], "").await;
301
        assert!(warnings(&ninth).iter().any(|w| w.contains("last turn")));
302
        let tenth = send(&app, "HOLD", "/runs/cafebabe", &[], "").await;
303
        match tenth.status() {
304
            StatusCode::GONE => busted += 1,
305
            StatusCode::REQUEST_TIMEOUT => {
306
                assert_eq!(header(&tenth, "x-floor"), "4");
307
                assert_eq!(
308
                    header(&tenth, "x-eq"),
309
                    "75",
310
                    "priapism holds the deflation at 75"
311
                );
312
                moved += 1;
313
            }
314
            other => panic!("seed {seed} overstayed into {other}"),
315
        }
316
    }
317
    assert!(
318
        busted > 15 && moved > 15,
319
        "{busted} busted, {moved} moved along"
320
    );
321
}
322
323
#[tokio::test]
324
async fn friction_at_the_limit_tears_the_foreskin() {
325
    let app = seeded_with(Floor::ChangingRoom, |run| {
326
        run.body.eq = 60;
327
        run.body.friction = 10;
328
        run.body.foreskin = Some(1.0);
329
    });
330
    let raw = send(&app, "JELQ", "/runs/cafebabe?technique=stretch", &[], "").await;
331
    assert!(warnings(&raw).iter().any(|w| w.contains("giving way")));
332
    let left: f64 = header(&raw, "x-foreskin").parse().unwrap();
333
    assert!(left < 1.0, "the foreskin is down to {left}");
334
335
    let thin = seeded_with(Floor::ChangingRoom, |run| {
336
        run.body.eq = 60;
337
        run.body.friction = 10;
338
        run.body.foreskin = Some(0.05);
339
    });
340
    let gone = send(&thin, "JELQ", "/runs/cafebabe?technique=stretch", &[], "").await;
341
    assert_eq!(gone.status(), StatusCode::GONE);
342
    assert_eq!(header(&gone, "x-foreskin"), "gone");
343
    assert_eq!(json(gone).await["foreskin"], -0.05);
344
}
345
346
#[tokio::test]
347
async fn ice_treats_a_burn_even_through_priapism() {
348
    let app = seeded_with(Floor::ChangingRoom, |run| {
349
        run.body.eq = 80;
350
        run.body.conditions.priapism_turns = 3;
351
        run.body.conditions.friction_burn = true;
352
        run.items.stores.ice_packs = 1;
353
    });
354
    let iced = send(&app, "JELQ", "/runs/cafebabe?technique=ice", &[], "").await;
355
    assert_eq!(header(&iced, "x-eq"), "80");
356
    assert!(warnings(&iced).iter().all(|w| !w.contains("friction burn")));
357
}
358
359
#[tokio::test]
360
async fn leaving_a_floor_delivers_every_queued_gain() {
361
    let app = seeded_with(Floor::ChangingRoom, |run| {
362
        run.here.turns = 6;
363
        run.body.pending.push(crate::game::Pending {
364
            due: run.turn + 4,
365
            length: 0.2,
366
            girth: 0.1,
367
            foreskin: 0.0,
368
        });
369
    });
370
    let left = send(&app, "POST", "/runs/cafebabe/floor", &[], "").await;
371
    assert_eq!(left.status(), StatusCode::SEE_OTHER);
372
    assert_eq!(
373
        header(&left, "x-gains-arrived"),
374
        "length=0.200, girth=0.100"
375
    );
376
}
377
378
#[tokio::test]
379
async fn an_overstayed_jelq_spends_no_lube() {
380
    let app = seeded_with(Floor::ChangingRoom, |run| {
381
        run.body.eq = 60;
382
        run.here.turns = 9;
383
        run.items.lubes.baby_oil = 1;
384
    });
385
    let late = send(
386
        &app,
387
        "JELQ",
388
        "/runs/cafebabe?technique=wet",
389
        &[("content-type", "application/baby-oil")],
390
        "",
391
    )
392
    .await;
393
    assert!([StatusCode::GONE, StatusCode::REQUEST_TIMEOUT].contains(&late.status()));
394
    if late.status() == StatusCode::REQUEST_TIMEOUT {
395
        let held = json(send(&app, "GET", "/runs/cafebabe/inventory", &[], "").await).await;
396
        assert!(
397
            held["items"]
398
                .as_array()
399
                .unwrap()
400
                .iter()
401
                .any(|i| i["name"] == "baby-oil")
402
        );
403
    }
404
}
405
406
#[tokio::test]
407
async fn the_refractory_period_gives_back_some_stamina_and_rarely_all() {
408
    let mut full = 0;
409
    for seed in 0..400u64 {
410
        let mut run = Run::new("cafebabe".into(), "cafe".into(), seed);
411
        run.floor = Floor::ChangingRoom;
412
        run.body.stamina = 0;
413
        run.body.exhausted = true;
414
        if run.hold().is_err() {
415
            continue;
416
        }
417
        match run.body.stamina {
418
            4..=6 => {}
419
            10 => full += 1,
420
            other => panic!("seed {seed} recovered {other} stamina"),
421
        }
422
    }
423
    assert!((8..=35).contains(&full), "{full} second winds in 400");
424
}