atagen

jelq

public

an http based roguelike rpg about jelqing

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