atagen

jelq

public

an http based roguelike rpg about jelqing

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