atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
545ad65826dde9afbbcd3f3c23f9f0c01b5adce8
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git

Blame: src/tests/turns.rs

8b8e83329a1datagen2026-10-12 12:01 UTC1
use super::*;
8b8e83329a1datagen2026-10-12 12:01 UTC2
8b8e83329a1datagen2026-10-12 12:01 UTC3
#[tokio::test]
8b8e83329a1datagen2026-10-12 12:01 UTC4
async fn meditation_is_free_but_only_once_per_floor() {
e028a4184d00atagen2026-10-14 10:49 UTC5
    let held = harness(Floor::Bedroom, SEED, |run| {
e028a4184d00atagen2026-10-14 10:49 UTC6
        run.online.meditation_taught = true
e028a4184d00atagen2026-10-14 10:49 UTC7
    });
e028a4184d00atagen2026-10-14 10:49 UTC8
    let app = &held.app;
e028a4184d00atagen2026-10-14 10:49 UTC9
    let response = send(app, "JELQ", "/runs/cafebabe?technique=meditate", &[], "").await;
8b8e83329a1datagen2026-10-12 12:01 UTC10
    assert_eq!(response.status(), StatusCode::OK);
e028a4184d00atagen2026-10-14 10:49 UTC11
    assert_eq!(header(&response, "x-eq"), "Cold");
e028a4184d00atagen2026-10-14 10:49 UTC12
    assert_eq!(held.eq(), 10);
8b8e83329a1datagen2026-10-12 12:01 UTC13
    assert_eq!(header(&response, "x-stamina"), "10/10");
e028a4184d00atagen2026-10-14 10:49 UTC14
    let again = send(app, "JELQ", "/runs/cafebabe?technique=meditate", &[], "").await;
8b8e83329a1datagen2026-10-12 12:01 UTC15
    assert_eq!(again.status(), StatusCode::CONFLICT);
8b8e83329a1datagen2026-10-12 12:01 UTC16
}
8b8e83329a1datagen2026-10-12 12:01 UTC17
8b8e83329a1datagen2026-10-12 12:01 UTC18
#[tokio::test]
8b8e83329a1datagen2026-10-12 12:01 UTC19
async fn meditation_respects_the_priapism_floor() {
e028a4184d00atagen2026-10-14 10:49 UTC20
    let held = harness(Floor::Bedroom, SEED, |run| {
8b8e83329a1datagen2026-10-12 12:01 UTC21
        run.body.eq = 80;
8b8e83329a1datagen2026-10-12 12:01 UTC22
        run.body.conditions.priapism_turns = 3;
880b29a0c11eatagen2026-10-13 09:27 UTC23
        run.online.meditation_taught = true;
8b8e83329a1datagen2026-10-12 12:01 UTC24
    });
e028a4184d00atagen2026-10-14 10:49 UTC25
    let calmed = send(
e028a4184d00atagen2026-10-14 10:49 UTC26
        &held.app,
e028a4184d00atagen2026-10-14 10:49 UTC27
        "JELQ",
e028a4184d00atagen2026-10-14 10:49 UTC28
        "/runs/cafebabe?technique=meditate",
e028a4184d00atagen2026-10-14 10:49 UTC29
        &[],
e028a4184d00atagen2026-10-14 10:49 UTC30
        "",
e028a4184d00atagen2026-10-14 10:49 UTC31
    )
e028a4184d00atagen2026-10-14 10:49 UTC32
    .await;
8b8e83329a1datagen2026-10-12 12:01 UTC33
    assert_eq!(calmed.status(), StatusCode::OK);
e028a4184d00atagen2026-10-14 10:49 UTC34
    assert_eq!(header(&calmed, "x-eq"), "Priapic");
e028a4184d00atagen2026-10-14 10:49 UTC35
    assert_eq!(held.eq(), 75);
8b8e83329a1datagen2026-10-12 12:01 UTC36
}
8b8e83329a1datagen2026-10-12 12:01 UTC37
8b8e83329a1datagen2026-10-12 12:01 UTC38
#[tokio::test]
8b8e83329a1datagen2026-10-12 12:01 UTC39
async fn priapism_holds_the_floor_through_every_reset() {
e028a4184d00atagen2026-10-14 10:49 UTC40
    let exit = harness(Floor::Bedroom, SEED, |run| {
8b8e83329a1datagen2026-10-12 12:01 UTC41
        run.body.eq = 90;
8b8e83329a1datagen2026-10-12 12:01 UTC42
        run.here.turns = 99;
8b8e83329a1datagen2026-10-12 12:01 UTC43
        run.body.conditions.priapism_turns = 3;
8b8e83329a1datagen2026-10-12 12:01 UTC44
    });
e028a4184d00atagen2026-10-14 10:49 UTC45
    let out = send(&exit.app, "POST", "/runs/cafebabe/floor", &[], "").await;
8b8e83329a1datagen2026-10-12 12:01 UTC46
    assert_eq!(out.status(), StatusCode::SEE_OTHER);
e028a4184d00atagen2026-10-14 10:49 UTC47
    assert_eq!(exit.eq(), 75);
8b8e83329a1datagen2026-10-12 12:01 UTC48
e028a4184d00atagen2026-10-14 10:49 UTC49
    let melon = harness(Floor::Bedroom, SEED, |run| {
8b8e83329a1datagen2026-10-12 12:01 UTC50
        run.body.eq = 95;
8b8e83329a1datagen2026-10-12 12:01 UTC51
        run.here.kitchen.melon_offered = true;
8b8e83329a1datagen2026-10-12 12:01 UTC52
        run.body.conditions.priapism_turns = 2;
8b8e83329a1datagen2026-10-12 12:01 UTC53
    });
e028a4184d00atagen2026-10-14 10:49 UTC54
    let bite = send(
e028a4184d00atagen2026-10-14 10:49 UTC55
        &melon.app,
e028a4184d00atagen2026-10-14 10:49 UTC56
        "POST",
e028a4184d00atagen2026-10-14 10:49 UTC57
        "/runs/cafebabe/kitchen/watermelon",
e028a4184d00atagen2026-10-14 10:49 UTC58
        &[],
e028a4184d00atagen2026-10-14 10:49 UTC59
        "",
e028a4184d00atagen2026-10-14 10:49 UTC60
    )
e028a4184d00atagen2026-10-14 10:49 UTC61
    .await;
8b8e83329a1datagen2026-10-12 12:01 UTC62
    assert_eq!(bite.status(), StatusCode::OK);
e028a4184d00atagen2026-10-14 10:49 UTC63
    assert_eq!(melon.eq(), 75);
8b8e83329a1datagen2026-10-12 12:01 UTC64
}
8b8e83329a1datagen2026-10-12 12:01 UTC65
8b8e83329a1datagen2026-10-12 12:01 UTC66
#[tokio::test]
8b8e83329a1datagen2026-10-12 12:01 UTC67
async fn the_towel_warms_once_until_the_kettle_boils() {
8b8e83329a1datagen2026-10-12 12:01 UTC68
    let app = seeded_with(Floor::Bedroom, |_| {});
8b8e83329a1datagen2026-10-12 12:01 UTC69
    let warm = send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
8b8e83329a1datagen2026-10-12 12:01 UTC70
    assert_eq!(warm.status(), StatusCode::OK);
8b8e83329a1datagen2026-10-12 12:01 UTC71
    let again = send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
8b8e83329a1datagen2026-10-12 12:01 UTC72
    assert_eq!(again.status(), StatusCode::CONFLICT);
8b8e83329a1datagen2026-10-12 12:01 UTC73
8b8e83329a1datagen2026-10-12 12:01 UTC74
    let app = seeded_with(Floor::Bedroom, |run| {
4b1f5f88827datagen2026-10-13 10:18 UTC75
        run.here.towel_warmed = true;
8b8e83329a1datagen2026-10-12 12:01 UTC76
        run.here.kitchen.kettle_boiled = true;
8b8e83329a1datagen2026-10-12 12:01 UTC77
    });
8b8e83329a1datagen2026-10-12 12:01 UTC78
    let reheated = send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
8b8e83329a1datagen2026-10-12 12:01 UTC79
    assert_eq!(reheated.status(), StatusCode::OK);
8b8e83329a1datagen2026-10-12 12:01 UTC80
    let cold = send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
8b8e83329a1datagen2026-10-12 12:01 UTC81
    assert_eq!(cold.status(), StatusCode::CONFLICT);
8b8e83329a1datagen2026-10-12 12:01 UTC82
}
8b8e83329a1datagen2026-10-12 12:01 UTC83
8b8e83329a1datagen2026-10-12 12:01 UTC84
#[tokio::test]
8b8e83329a1datagen2026-10-12 12:01 UTC85
async fn the_refractory_period_is_the_next_hold() {
8b8e83329a1datagen2026-10-12 12:01 UTC86
    let app = seeded_with(Floor::Hotel, |run| {
8b8e83329a1datagen2026-10-12 12:01 UTC87
        run.here.wifi_accepted = true;
4b1f5f88827datagen2026-10-13 10:18 UTC88
        run.here.towel_warmed = true;
8b8e83329a1datagen2026-10-12 12:01 UTC89
        run.body.eq = 60;
8b8e83329a1datagen2026-10-12 12:01 UTC90
        run.body.stamina = 0;
8b8e83329a1datagen2026-10-12 12:01 UTC91
        run.body.exhausted = true;
8b8e83329a1datagen2026-10-12 12:01 UTC92
    });
8b8e83329a1datagen2026-10-12 12:01 UTC93
    let tired = send(&app, "JELQ", "/runs/cafebabe?technique=edge", &[], "").await;
8b8e83329a1datagen2026-10-12 12:01 UTC94
    assert_eq!(tired.status(), StatusCode::TOO_MANY_REQUESTS);
8b8e83329a1datagen2026-10-12 12:01 UTC95
    let hold = send(&app, "HOLD", "/runs/cafebabe", &[], "").await;
8b8e83329a1datagen2026-10-12 12:01 UTC96
    assert_eq!(hold.status(), StatusCode::OK);
f48daa8adf59atagen2026-10-13 12:51 UTC97
    assert!(["4/10", "5/10", "6/10", "10/10"].contains(&header(&hold, "x-stamina")));
4b1f5f88827datagen2026-10-13 10:18 UTC98
    let towel = send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
4b1f5f88827datagen2026-10-13 10:18 UTC99
    assert_eq!(towel.status(), StatusCode::CONFLICT, "the towel stays cold");
8b8e83329a1datagen2026-10-12 12:01 UTC100
}
8b8e83329a1datagen2026-10-12 12:01 UTC101
8b8e83329a1datagen2026-10-12 12:01 UTC102
#[tokio::test]
8b8e83329a1datagen2026-10-12 12:01 UTC103
async fn a_refused_jelq_does_not_break_the_edging_streak() {
8b8e83329a1datagen2026-10-12 12:01 UTC104
    let app = seeded_with(Floor::ChangingRoom, |run| {
8b8e83329a1datagen2026-10-12 12:01 UTC105
        run.body.eq = 45;
8b8e83329a1datagen2026-10-12 12:01 UTC106
        run.here.edges = 2;
8b8e83329a1datagen2026-10-12 12:01 UTC107
    });
8b8e83329a1datagen2026-10-12 12:01 UTC108
    let refused = send(
8b8e83329a1datagen2026-10-12 12:01 UTC109
        &app,
8b8e83329a1datagen2026-10-12 12:01 UTC110
        "JELQ",
8b8e83329a1datagen2026-10-12 12:01 UTC111
        "/runs/cafebabe?technique=dry",
8b8e83329a1datagen2026-10-12 12:01 UTC112
        &[("content-type", "application/baby-oil")],
8b8e83329a1datagen2026-10-12 12:01 UTC113
        "",
8b8e83329a1datagen2026-10-12 12:01 UTC114
    )
8b8e83329a1datagen2026-10-12 12:01 UTC115
    .await;
8b8e83329a1datagen2026-10-12 12:01 UTC116
    assert_eq!(refused.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
8b8e83329a1datagen2026-10-12 12:01 UTC117
    let third = send(&app, "JELQ", "/runs/cafebabe?technique=edge", &[], "").await;
8b8e83329a1datagen2026-10-12 12:01 UTC118
    assert_eq!(third.status(), StatusCode::LOOP_DETECTED);
8b8e83329a1datagen2026-10-12 12:01 UTC119
}
8b8e83329a1datagen2026-10-12 12:01 UTC120
8b8e83329a1datagen2026-10-12 12:01 UTC121
#[tokio::test]
8b8e83329a1datagen2026-10-12 12:01 UTC122
async fn an_interrupted_edge_keeps_no_gains() {
bf5f8cb6f60fatagen2026-10-13 12:10 UTC123
    let app = seeded_with(Floor::Hotel, |run| {
bf5f8cb6f60fatagen2026-10-13 12:10 UTC124
        run.here.wifi_accepted = true;
bf5f8cb6f60fatagen2026-10-13 12:10 UTC125
        run.body.eq = 100;
8b8e83329a1datagen2026-10-12 12:01 UTC126
    });
8b8e83329a1datagen2026-10-12 12:01 UTC127
    let before = send(&app, "GET", "/runs/cafebabe", &[], "").await;
8b8e83329a1datagen2026-10-12 12:01 UTC128
    let girth = header(&before, "x-girth").to_owned();
8b8e83329a1datagen2026-10-12 12:01 UTC129
    let edge = send(&app, "JELQ", "/runs/cafebabe?technique=edge", &[], "").await;
bf5f8cb6f60fatagen2026-10-13 12:10 UTC130
    assert!(!edge.status().is_success());
8b8e83329a1datagen2026-10-12 12:01 UTC131
    assert_eq!(header(&edge, "x-girth"), girth);
8b8e83329a1datagen2026-10-12 12:01 UTC132
}
8b8e83329a1datagen2026-10-12 12:01 UTC133
8b8e83329a1datagen2026-10-12 12:01 UTC134
#[tokio::test]
8b8e83329a1datagen2026-10-12 12:01 UTC135
async fn greed_multiplies_gains_above_the_band() {
8b8e83329a1datagen2026-10-12 12:01 UTC136
    let lube = [("content-type", "application/baby-oil")];
8b8e83329a1datagen2026-10-12 12:01 UTC137
    for (eq, greed) in [(75, 1.5), (88, 2.0)] {
8b8e83329a1datagen2026-10-12 12:01 UTC138
        let mut compared = false;
8b8e83329a1datagen2026-10-12 12:01 UTC139
        for seed in 0..120u64 {
8b8e83329a1datagen2026-10-12 12:01 UTC140
            let app = app_with(Floor::Bedroom, seed, |run| {
8b8e83329a1datagen2026-10-12 12:01 UTC141
                run.body.eq = 60;
8b8e83329a1datagen2026-10-12 12:01 UTC142
            });
8b8e83329a1datagen2026-10-12 12:01 UTC143
            let in_band = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &lube, "").await;
8b8e83329a1datagen2026-10-12 12:01 UTC144
            let app = app_with(Floor::Bedroom, seed, |run| {
8b8e83329a1datagen2026-10-12 12:01 UTC145
                run.body.eq = eq;
8b8e83329a1datagen2026-10-12 12:01 UTC146
            });
8b8e83329a1datagen2026-10-12 12:01 UTC147
            let greedy = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &lube, "").await;
8b8e83329a1datagen2026-10-12 12:01 UTC148
            if in_band.status() != StatusCode::OK || greedy.status() != StatusCode::OK {
8b8e83329a1datagen2026-10-12 12:01 UTC149
                continue;
8b8e83329a1datagen2026-10-12 12:01 UTC150
            }
8b8e83329a1datagen2026-10-12 12:01 UTC151
            let steady = json(in_band).await["length"].as_f64().unwrap();
8b8e83329a1datagen2026-10-12 12:01 UTC152
            let eager = json(greedy).await["length"].as_f64().unwrap();
8b8e83329a1datagen2026-10-12 12:01 UTC153
            assert!(
8b8e83329a1datagen2026-10-12 12:01 UTC154
                (eager - steady * greed).abs() < 1e-9,
8b8e83329a1datagen2026-10-12 12:01 UTC155
                "seed {seed} gained {eager} at {eq} EQ against {steady} inside the band"
8b8e83329a1datagen2026-10-12 12:01 UTC156
            );
8b8e83329a1datagen2026-10-12 12:01 UTC157
            compared = true;
8b8e83329a1datagen2026-10-12 12:01 UTC158
            break;
8b8e83329a1datagen2026-10-12 12:01 UTC159
        }
8b8e83329a1datagen2026-10-12 12:01 UTC160
        assert!(compared, "no seed produced two clean rolls at {eq} EQ");
8b8e83329a1datagen2026-10-12 12:01 UTC161
    }
8b8e83329a1datagen2026-10-12 12:01 UTC162
}
8b8e83329a1datagen2026-10-12 12:01 UTC163
8b8e83329a1datagen2026-10-12 12:01 UTC164
#[tokio::test]
8b8e83329a1datagen2026-10-12 12:01 UTC165
async fn queued_gains_arrive_on_their_turn() {
8b8e83329a1datagen2026-10-12 12:01 UTC166
    let mut run = Run::new("cafebabe".into(), "cafe".into(), SEED);
8b8e83329a1datagen2026-10-12 12:01 UTC167
    run.body.eq = 75;
8b8e83329a1datagen2026-10-12 12:01 UTC168
    let before = run.body.length;
8b8e83329a1datagen2026-10-12 12:01 UTC169
    run.body.pending.push(crate::game::Pending {
8b8e83329a1datagen2026-10-12 12:01 UTC170
        due: run.turn + 1,
8b8e83329a1datagen2026-10-12 12:01 UTC171
        length: 0.5,
8b8e83329a1datagen2026-10-12 12:01 UTC172
        girth: 0.0,
8b8e83329a1datagen2026-10-12 12:01 UTC173
        foreskin: 0.0,
8b8e83329a1datagen2026-10-12 12:01 UTC174
    });
8b8e83329a1datagen2026-10-12 12:01 UTC175
    assert!(run.hold().is_ok());
8b8e83329a1datagen2026-10-12 12:01 UTC176
    assert!((run.body.length - before - 0.5).abs() < 1e-9);
8b8e83329a1datagen2026-10-12 12:01 UTC177
    assert!(run.body.pending.is_empty());
8b8e83329a1datagen2026-10-12 12:01 UTC178
}
8b8e83329a1datagen2026-10-12 12:01 UTC179
8b8e83329a1datagen2026-10-12 12:01 UTC180
#[tokio::test]
8b8e83329a1datagen2026-10-12 12:01 UTC181
async fn hold_keeps_the_shop_and_the_melon_open() {
8b8e83329a1datagen2026-10-12 12:01 UTC182
    let hotel = seeded_with(Floor::Hotel, |run| {
8b8e83329a1datagen2026-10-12 12:01 UTC183
        run.here.wifi_accepted = true;
8b8e83329a1datagen2026-10-12 12:01 UTC184
        run.body.eq = 80;
8b8e83329a1datagen2026-10-12 12:01 UTC185
    });
8b8e83329a1datagen2026-10-12 12:01 UTC186
    send(&hotel, "GET", "/runs/cafebabe/shop", &[], "").await;
8b8e83329a1datagen2026-10-12 12:01 UTC187
    let held = send(&hotel, "HOLD", "/runs/cafebabe", &[], "").await;
8b8e83329a1datagen2026-10-12 12:01 UTC188
    assert!(linked_rels(&held).contains(&"shop-item".to_owned()));
8b8e83329a1datagen2026-10-12 12:01 UTC189
8b8e83329a1datagen2026-10-12 12:01 UTC190
    let mut run = Run::new("cafebabe".into(), "cafe".into(), SEED);
8b8e83329a1datagen2026-10-12 12:01 UTC191
    run.body.eq = 80;
8b8e83329a1datagen2026-10-12 12:01 UTC192
    run.here.kitchen.melon_offered = true;
8b8e83329a1datagen2026-10-12 12:01 UTC193
    assert!(run.hold().is_ok());
8b8e83329a1datagen2026-10-12 12:01 UTC194
    assert!(run.here.kitchen.melon_offered, "the offer survives a HOLD");
8b8e83329a1datagen2026-10-12 12:01 UTC195
}
8b8e83329a1datagen2026-10-12 12:01 UTC196
8b8e83329a1datagen2026-10-12 12:01 UTC197
#[tokio::test]
8b8e83329a1datagen2026-10-12 12:01 UTC198
async fn leaving_the_band_warms_the_next_floor() {
8b8e83329a1datagen2026-10-12 12:01 UTC199
    let app = seeded_with(Floor::Bedroom, |run| {
8b8e83329a1datagen2026-10-12 12:01 UTC200
        run.body.eq = 60;
8b8e83329a1datagen2026-10-12 12:01 UTC201
        run.here.turns = 6;
8b8e83329a1datagen2026-10-12 12:01 UTC202
        run.items.lubes.baby_oil = 9;
8b8e83329a1datagen2026-10-12 12:01 UTC203
    });
8b8e83329a1datagen2026-10-12 12:01 UTC204
    let left = send(&app, "POST", "/runs/cafebabe/floor", &[], "").await;
8b8e83329a1datagen2026-10-12 12:01 UTC205
    assert_eq!(left.status(), StatusCode::SEE_OTHER);
8b8e83329a1datagen2026-10-12 12:01 UTC206
    let detail = json(left).await["detail"].as_str().unwrap().to_owned();
8b8e83329a1datagen2026-10-12 12:01 UTC207
    assert!(detail.contains("the warmth came with you"));
8b8e83329a1datagen2026-10-12 12:01 UTC208
8b8e83329a1datagen2026-10-12 12:01 UTC209
    let lube = [("content-type", "application/baby-oil")];
8b8e83329a1datagen2026-10-12 12:01 UTC210
    send(&app, "JELQ", "/runs/cafebabe?technique=warmup", &[], "").await;
8b8e83329a1datagen2026-10-12 12:01 UTC211
    // The shower is a +35 warm-up, so EQ lands in the band and the band bonus rides along.
8b8e83329a1datagen2026-10-12 12:01 UTC212
    // The arrival bonus is the one point the second jelq loses.
8b8e83329a1datagen2026-10-12 12:01 UTC213
    let first = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &lube, "").await;
8b8e83329a1datagen2026-10-12 12:01 UTC214
    assert!(
8b8e83329a1datagen2026-10-12 12:01 UTC215
        header(&first, "server-timing").contains("d20+4"),
8b8e83329a1datagen2026-10-12 12:01 UTC216
        "the first jelq of a warm arrival rolls with +1, got {}",
8b8e83329a1datagen2026-10-12 12:01 UTC217
        header(&first, "server-timing")
8b8e83329a1datagen2026-10-12 12:01 UTC218
    );
8b8e83329a1datagen2026-10-12 12:01 UTC219
    let second = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &lube, "").await;
8b8e83329a1datagen2026-10-12 12:01 UTC220
    assert!(header(&second, "server-timing").contains("d20+3"));
8b8e83329a1datagen2026-10-12 12:01 UTC221
}
8b8e83329a1datagen2026-10-12 12:01 UTC222
8b8e83329a1datagen2026-10-12 12:01 UTC223
#[tokio::test]
8b8e83329a1datagen2026-10-12 12:01 UTC224
async fn energy_drinks_add_three_stamina() {
8b8e83329a1datagen2026-10-12 12:01 UTC225
    let app = seeded_with(Floor::Bedroom, |run| {
8b8e83329a1datagen2026-10-12 12:01 UTC226
        run.body.stamina = 2;
8b8e83329a1datagen2026-10-12 12:01 UTC227
        run.items.stores.energy_drinks = 1;
8b8e83329a1datagen2026-10-12 12:01 UTC228
    });
8b8e83329a1datagen2026-10-12 12:01 UTC229
    let view = send(&app, "GET", "/runs/cafebabe", &[], "").await;
8b8e83329a1datagen2026-10-12 12:01 UTC230
    assert!(header(&view, "link").contains("technique=energy"));
8b8e83329a1datagen2026-10-12 12:01 UTC231
    let drink = send(&app, "JELQ", "/runs/cafebabe?technique=energy", &[], "").await;
8b8e83329a1datagen2026-10-12 12:01 UTC232
    assert_eq!(drink.status(), StatusCode::OK);
8b8e83329a1datagen2026-10-12 12:01 UTC233
    let view = send(&app, "GET", "/runs/cafebabe", &[], "").await;
8b8e83329a1datagen2026-10-12 12:01 UTC234
    assert_eq!(header(&view, "x-stamina"), "5/10");
8b8e83329a1datagen2026-10-12 12:01 UTC235
    let again = send(&app, "JELQ", "/runs/cafebabe?technique=energy", &[], "").await;
8b8e83329a1datagen2026-10-12 12:01 UTC236
    assert_eq!(again.status(), StatusCode::FAILED_DEPENDENCY);
8b8e83329a1datagen2026-10-12 12:01 UTC237
}
fcc6cf6d5edcatagen2026-10-13 11:31 UTC238
fcc6cf6d5edcatagen2026-10-13 11:31 UTC239
#[tokio::test]
fcc6cf6d5edcatagen2026-10-13 11:31 UTC240
async fn queued_gains_announce_their_arrival() {
fcc6cf6d5edcatagen2026-10-13 11:31 UTC241
    let app = seeded_with(Floor::Bedroom, |run| {
fcc6cf6d5edcatagen2026-10-13 11:31 UTC242
        run.body.eq = 75;
fcc6cf6d5edcatagen2026-10-13 11:31 UTC243
        run.body.foreskin = Some(1.0);
fcc6cf6d5edcatagen2026-10-13 11:31 UTC244
        run.body.pending.push(crate::game::Pending {
fcc6cf6d5edcatagen2026-10-13 11:31 UTC245
            due: run.turn + 1,
fcc6cf6d5edcatagen2026-10-13 11:31 UTC246
            length: 0.105,
fcc6cf6d5edcatagen2026-10-13 11:31 UTC247
            girth: 0.0,
fcc6cf6d5edcatagen2026-10-13 11:31 UTC248
            foreskin: 0.03,
fcc6cf6d5edcatagen2026-10-13 11:31 UTC249
        });
fcc6cf6d5edcatagen2026-10-13 11:31 UTC250
    });
fcc6cf6d5edcatagen2026-10-13 11:31 UTC251
    let held = send(&app, "HOLD", "/runs/cafebabe", &[], "").await;
fcc6cf6d5edcatagen2026-10-13 11:31 UTC252
    assert_eq!(
fcc6cf6d5edcatagen2026-10-13 11:31 UTC253
        header(&held, "x-gains-arrived"),
fcc6cf6d5edcatagen2026-10-13 11:31 UTC254
        "length=0.105, girth=0.000, foreskin=0.030"
fcc6cf6d5edcatagen2026-10-13 11:31 UTC255
    );
fcc6cf6d5edcatagen2026-10-13 11:31 UTC256
    assert!(
fcc6cf6d5edcatagen2026-10-13 11:31 UTC257
        warnings(&held)
fcc6cf6d5edcatagen2026-10-13 11:31 UTC258
            .iter()
fcc6cf6d5edcatagen2026-10-13 11:31 UTC259
            .any(|w| w.contains("the queued gains have arrived"))
fcc6cf6d5edcatagen2026-10-13 11:31 UTC260
    );
fcc6cf6d5edcatagen2026-10-13 11:31 UTC261
    let later = send(&app, "GET", "/runs/cafebabe", &[], "").await;
fcc6cf6d5edcatagen2026-10-13 11:31 UTC262
    assert!(later.headers().get("x-gains-arrived").is_none());
fcc6cf6d5edcatagen2026-10-13 11:31 UTC263
}
a11497af08aaatagen2026-10-13 12:04 UTC264
a11497af08aaatagen2026-10-13 12:04 UTC265
#[tokio::test]
a11497af08aaatagen2026-10-13 12:04 UTC266
async fn an_energy_drink_ends_exhaustion_without_the_refractory_period() {
a11497af08aaatagen2026-10-13 12:04 UTC267
    let app = seeded_with(Floor::Bedroom, |run| {
a11497af08aaatagen2026-10-13 12:04 UTC268
        run.body.eq = 60;
a11497af08aaatagen2026-10-13 12:04 UTC269
        run.body.stamina = 0;
a11497af08aaatagen2026-10-13 12:04 UTC270
        run.body.exhausted = true;
a11497af08aaatagen2026-10-13 12:04 UTC271
        run.items.stores.energy_drinks = 1;
a11497af08aaatagen2026-10-13 12:04 UTC272
    });
a11497af08aaatagen2026-10-13 12:04 UTC273
    let tired = send(&app, "GET", "/runs/cafebabe", &[], "").await;
a11497af08aaatagen2026-10-13 12:04 UTC274
    assert_eq!(advertised(&tired), ["energy"]);
a11497af08aaatagen2026-10-13 12:04 UTC275
    let drink = send(&app, "JELQ", "/runs/cafebabe?technique=energy", &[], "").await;
a11497af08aaatagen2026-10-13 12:04 UTC276
    assert_eq!(drink.status(), StatusCode::OK);
a11497af08aaatagen2026-10-13 12:04 UTC277
    assert_eq!(header(&drink, "x-stamina"), "3/10");
a11497af08aaatagen2026-10-13 12:04 UTC278
    assert_eq!(
a11497af08aaatagen2026-10-13 12:04 UTC279
        header(&drink, "x-eq"),
e028a4184d00atagen2026-10-14 10:49 UTC280
        "Ideal",
a11497af08aaatagen2026-10-13 12:04 UTC281
        "still warm, unlike the refractory HOLD"
a11497af08aaatagen2026-10-13 12:04 UTC282
    );
a11497af08aaatagen2026-10-13 12:04 UTC283
    let jelq = send(&app, "JELQ", "/runs/cafebabe?technique=edge", &[], "").await;
a11497af08aaatagen2026-10-13 12:04 UTC284
    assert_ne!(jelq.status(), StatusCode::TOO_MANY_REQUESTS);
a11497af08aaatagen2026-10-13 12:04 UTC285
}
bf5f8cb6f60fatagen2026-10-13 12:10 UTC286
bf5f8cb6f60fatagen2026-10-13 12:10 UTC287
#[tokio::test]
bf5f8cb6f60fatagen2026-10-13 12:10 UTC288
async fn edging_climbs_out_of_the_cold() {
bf5f8cb6f60fatagen2026-10-13 12:10 UTC289
    let app = seeded_with(Floor::ChangingRoom, |run| run.body.eq = 20);
bf5f8cb6f60fatagen2026-10-13 12:10 UTC290
    let edge = send(&app, "JELQ", "/runs/cafebabe?technique=edge", &[], "").await;
bf5f8cb6f60fatagen2026-10-13 12:10 UTC291
    assert_eq!(edge.status(), StatusCode::OK);
bf5f8cb6f60fatagen2026-10-13 12:10 UTC292
    assert!(warnings(&edge).iter().all(|w| !w.contains("Cold start")));
bf5f8cb6f60fatagen2026-10-13 12:10 UTC293
    assert_eq!(
bf5f8cb6f60fatagen2026-10-13 12:10 UTC294
        header(&edge, "x-friction"),
bf5f8cb6f60fatagen2026-10-13 12:10 UTC295
        "2/10",
bf5f8cb6f60fatagen2026-10-13 12:10 UTC296
        "edging pays no cold tax"
bf5f8cb6f60fatagen2026-10-13 12:10 UTC297
    );
bf5f8cb6f60fatagen2026-10-13 12:10 UTC298
}
3ac624339c98atagen2026-10-13 12:21 UTC299
3ac624339c98atagen2026-10-13 12:21 UTC300
#[tokio::test]
3ac624339c98atagen2026-10-13 12:21 UTC301
async fn overstaying_a_floor_is_warned_then_punished() {
3ac624339c98atagen2026-10-13 12:21 UTC302
    let mut busted = 0;
3ac624339c98atagen2026-10-13 12:21 UTC303
    let mut moved = 0;
3ac624339c98atagen2026-10-13 12:21 UTC304
    for seed in 0..60u64 {
e028a4184d00atagen2026-10-14 10:49 UTC305
        let held = harness(Floor::ChangingRoom, seed, |run| {
3ac624339c98atagen2026-10-13 12:21 UTC306
            run.body.eq = 80;
3ac624339c98atagen2026-10-13 12:21 UTC307
            run.body.conditions.priapism_turns = 9;
3ac624339c98atagen2026-10-13 12:21 UTC308
            run.here.turns = 7;
3ac624339c98atagen2026-10-13 12:21 UTC309
        });
e028a4184d00atagen2026-10-14 10:49 UTC310
        let app = &held.app;
e028a4184d00atagen2026-10-14 10:49 UTC311
        let eighth = send(app, "HOLD", "/runs/cafebabe", &[], "").await;
3ac624339c98atagen2026-10-13 12:21 UTC312
        assert!(
3ac624339c98atagen2026-10-13 12:21 UTC313
            warnings(&eighth)
3ac624339c98atagen2026-10-13 12:21 UTC314
                .iter()
3ac624339c98atagen2026-10-13 12:21 UTC315
                .any(|w| w.contains("The gym closes"))
3ac624339c98atagen2026-10-13 12:21 UTC316
        );
e028a4184d00atagen2026-10-14 10:49 UTC317
        let ninth = send(app, "HOLD", "/runs/cafebabe", &[], "").await;
3ac624339c98atagen2026-10-13 12:21 UTC318
        assert!(warnings(&ninth).iter().any(|w| w.contains("last turn")));
e028a4184d00atagen2026-10-14 10:49 UTC319
        let tenth = send(app, "HOLD", "/runs/cafebabe", &[], "").await;
3ac624339c98atagen2026-10-13 12:21 UTC320
        match tenth.status() {
3ac624339c98atagen2026-10-13 12:21 UTC321
            StatusCode::GONE => busted += 1,
3ac624339c98atagen2026-10-13 12:21 UTC322
            StatusCode::REQUEST_TIMEOUT => {
3ac624339c98atagen2026-10-13 12:21 UTC323
                assert_eq!(header(&tenth, "x-floor"), "4");
e028a4184d00atagen2026-10-14 10:49 UTC324
                assert_eq!(held.eq(), 75, "priapism holds the deflation at 75");
3ac624339c98atagen2026-10-13 12:21 UTC325
                moved += 1;
3ac624339c98atagen2026-10-13 12:21 UTC326
            }
3ac624339c98atagen2026-10-13 12:21 UTC327
            other => panic!("seed {seed} overstayed into {other}"),
3ac624339c98atagen2026-10-13 12:21 UTC328
        }
3ac624339c98atagen2026-10-13 12:21 UTC329
    }
3ac624339c98atagen2026-10-13 12:21 UTC330
    assert!(
3ac624339c98atagen2026-10-13 12:21 UTC331
        busted > 15 && moved > 15,
3ac624339c98atagen2026-10-13 12:21 UTC332
        "{busted} busted, {moved} moved along"
3ac624339c98atagen2026-10-13 12:21 UTC333
    );
3ac624339c98atagen2026-10-13 12:21 UTC334
}
3ac624339c98atagen2026-10-13 12:21 UTC335
3ac624339c98atagen2026-10-13 12:21 UTC336
#[tokio::test]
3ac624339c98atagen2026-10-13 12:21 UTC337
async fn friction_at_the_limit_tears_the_foreskin() {
3ac624339c98atagen2026-10-13 12:21 UTC338
    let app = seeded_with(Floor::ChangingRoom, |run| {
3ac624339c98atagen2026-10-13 12:21 UTC339
        run.body.eq = 60;
3ac624339c98atagen2026-10-13 12:21 UTC340
        run.body.friction = 10;
3ac624339c98atagen2026-10-13 12:21 UTC341
        run.body.foreskin = Some(1.0);
3ac624339c98atagen2026-10-13 12:21 UTC342
    });
3ac624339c98atagen2026-10-13 12:21 UTC343
    let raw = send(&app, "JELQ", "/runs/cafebabe?technique=stretch", &[], "").await;
3ac624339c98atagen2026-10-13 12:21 UTC344
    assert!(warnings(&raw).iter().any(|w| w.contains("giving way")));
3ac624339c98atagen2026-10-13 12:21 UTC345
    let left: f64 = header(&raw, "x-foreskin").parse().unwrap();
3ac624339c98atagen2026-10-13 12:21 UTC346
    assert!(left < 1.0, "the foreskin is down to {left}");
3ac624339c98atagen2026-10-13 12:21 UTC347
3ac624339c98atagen2026-10-13 12:21 UTC348
    let thin = seeded_with(Floor::ChangingRoom, |run| {
3ac624339c98atagen2026-10-13 12:21 UTC349
        run.body.eq = 60;
3ac624339c98atagen2026-10-13 12:21 UTC350
        run.body.friction = 10;
3ac624339c98atagen2026-10-13 12:21 UTC351
        run.body.foreskin = Some(0.05);
3ac624339c98atagen2026-10-13 12:21 UTC352
    });
3ac624339c98atagen2026-10-13 12:21 UTC353
    let gone = send(&thin, "JELQ", "/runs/cafebabe?technique=stretch", &[], "").await;
3ac624339c98atagen2026-10-13 12:21 UTC354
    assert_eq!(gone.status(), StatusCode::GONE);
3ac624339c98atagen2026-10-13 12:21 UTC355
    assert_eq!(header(&gone, "x-foreskin"), "gone");
3ac624339c98atagen2026-10-13 12:21 UTC356
    assert_eq!(json(gone).await["foreskin"], -0.05);
3ac624339c98atagen2026-10-13 12:21 UTC357
}
e2727527c151atagen2026-10-13 12:38 UTC358
e2727527c151atagen2026-10-13 12:38 UTC359
#[tokio::test]
e2727527c151atagen2026-10-13 12:38 UTC360
async fn ice_treats_a_burn_even_through_priapism() {
e028a4184d00atagen2026-10-14 10:49 UTC361
    let held = harness(Floor::ChangingRoom, SEED, |run| {
e2727527c151atagen2026-10-13 12:38 UTC362
        run.body.eq = 80;
e2727527c151atagen2026-10-13 12:38 UTC363
        run.body.conditions.priapism_turns = 3;
e2727527c151atagen2026-10-13 12:38 UTC364
        run.body.conditions.friction_burn = true;
d3c4718d061eatagen2026-10-14 09:03 UTC365
        run.body.friction = 9;
e2727527c151atagen2026-10-13 12:38 UTC366
        run.items.stores.ice_packs = 1;
e2727527c151atagen2026-10-13 12:38 UTC367
    });
e028a4184d00atagen2026-10-14 10:49 UTC368
    let iced = send(&held.app, "JELQ", "/runs/cafebabe?technique=ice", &[], "").await;
e028a4184d00atagen2026-10-14 10:49 UTC369
    assert_eq!(held.eq(), 80);
d3c4718d061eatagen2026-10-14 09:03 UTC370
    assert_eq!(header(&iced, "x-friction"), "6/10");
e2727527c151atagen2026-10-13 12:38 UTC371
    assert!(warnings(&iced).iter().all(|w| !w.contains("friction burn")));
e2727527c151atagen2026-10-13 12:38 UTC372
}
6033b0db186catagen2026-10-13 12:44 UTC373
6033b0db186catagen2026-10-13 12:44 UTC374
#[tokio::test]
6033b0db186catagen2026-10-13 12:44 UTC375
async fn leaving_a_floor_delivers_every_queued_gain() {
6033b0db186catagen2026-10-13 12:44 UTC376
    let app = seeded_with(Floor::ChangingRoom, |run| {
6033b0db186catagen2026-10-13 12:44 UTC377
        run.here.turns = 6;
6033b0db186catagen2026-10-13 12:44 UTC378
        run.body.pending.push(crate::game::Pending {
6033b0db186catagen2026-10-13 12:44 UTC379
            due: run.turn + 4,
6033b0db186catagen2026-10-13 12:44 UTC380
            length: 0.2,
6033b0db186catagen2026-10-13 12:44 UTC381
            girth: 0.1,
6033b0db186catagen2026-10-13 12:44 UTC382
            foreskin: 0.0,
6033b0db186catagen2026-10-13 12:44 UTC383
        });
6033b0db186catagen2026-10-13 12:44 UTC384
    });
6033b0db186catagen2026-10-13 12:44 UTC385
    let left = send(&app, "POST", "/runs/cafebabe/floor", &[], "").await;
6033b0db186catagen2026-10-13 12:44 UTC386
    assert_eq!(left.status(), StatusCode::SEE_OTHER);
6033b0db186catagen2026-10-13 12:44 UTC387
    assert_eq!(
6033b0db186catagen2026-10-13 12:44 UTC388
        header(&left, "x-gains-arrived"),
6033b0db186catagen2026-10-13 12:44 UTC389
        "length=0.200, girth=0.100"
6033b0db186catagen2026-10-13 12:44 UTC390
    );
6033b0db186catagen2026-10-13 12:44 UTC391
}
6033b0db186catagen2026-10-13 12:44 UTC392
6033b0db186catagen2026-10-13 12:44 UTC393
#[tokio::test]
6033b0db186catagen2026-10-13 12:44 UTC394
async fn an_overstayed_jelq_spends_no_lube() {
6033b0db186catagen2026-10-13 12:44 UTC395
    let app = seeded_with(Floor::ChangingRoom, |run| {
6033b0db186catagen2026-10-13 12:44 UTC396
        run.body.eq = 60;
6033b0db186catagen2026-10-13 12:44 UTC397
        run.here.turns = 9;
6033b0db186catagen2026-10-13 12:44 UTC398
        run.items.lubes.baby_oil = 1;
6033b0db186catagen2026-10-13 12:44 UTC399
    });
6033b0db186catagen2026-10-13 12:44 UTC400
    let late = send(
6033b0db186catagen2026-10-13 12:44 UTC401
        &app,
6033b0db186catagen2026-10-13 12:44 UTC402
        "JELQ",
6033b0db186catagen2026-10-13 12:44 UTC403
        "/runs/cafebabe?technique=wet",
6033b0db186catagen2026-10-13 12:44 UTC404
        &[("content-type", "application/baby-oil")],
6033b0db186catagen2026-10-13 12:44 UTC405
        "",
6033b0db186catagen2026-10-13 12:44 UTC406
    )
6033b0db186catagen2026-10-13 12:44 UTC407
    .await;
6033b0db186catagen2026-10-13 12:44 UTC408
    assert!([StatusCode::GONE, StatusCode::REQUEST_TIMEOUT].contains(&late.status()));
6033b0db186catagen2026-10-13 12:44 UTC409
    if late.status() == StatusCode::REQUEST_TIMEOUT {
6033b0db186catagen2026-10-13 12:44 UTC410
        let held = json(send(&app, "GET", "/runs/cafebabe/inventory", &[], "").await).await;
6033b0db186catagen2026-10-13 12:44 UTC411
        assert!(
6033b0db186catagen2026-10-13 12:44 UTC412
            held["items"]
6033b0db186catagen2026-10-13 12:44 UTC413
                .as_array()
6033b0db186catagen2026-10-13 12:44 UTC414
                .unwrap()
6033b0db186catagen2026-10-13 12:44 UTC415
                .iter()
6033b0db186catagen2026-10-13 12:44 UTC416
                .any(|i| i["name"] == "baby-oil")
6033b0db186catagen2026-10-13 12:44 UTC417
        );
6033b0db186catagen2026-10-13 12:44 UTC418
    }
6033b0db186catagen2026-10-13 12:44 UTC419
}
f48daa8adf59atagen2026-10-13 12:51 UTC420
f48daa8adf59atagen2026-10-13 12:51 UTC421
#[tokio::test]
f48daa8adf59atagen2026-10-13 12:51 UTC422
async fn the_refractory_period_gives_back_some_stamina_and_rarely_all() {
f48daa8adf59atagen2026-10-13 12:51 UTC423
    let mut full = 0;
f48daa8adf59atagen2026-10-13 12:51 UTC424
    for seed in 0..400u64 {
f48daa8adf59atagen2026-10-13 12:51 UTC425
        let mut run = Run::new("cafebabe".into(), "cafe".into(), seed);
f48daa8adf59atagen2026-10-13 12:51 UTC426
        run.floor = Floor::ChangingRoom;
f48daa8adf59atagen2026-10-13 12:51 UTC427
        run.body.stamina = 0;
f48daa8adf59atagen2026-10-13 12:51 UTC428
        run.body.exhausted = true;
f48daa8adf59atagen2026-10-13 12:51 UTC429
        if run.hold().is_err() {
f48daa8adf59atagen2026-10-13 12:51 UTC430
            continue;
f48daa8adf59atagen2026-10-13 12:51 UTC431
        }
f48daa8adf59atagen2026-10-13 12:51 UTC432
        match run.body.stamina {
f48daa8adf59atagen2026-10-13 12:51 UTC433
            4..=6 => {}
f48daa8adf59atagen2026-10-13 12:51 UTC434
            10 => full += 1,
f48daa8adf59atagen2026-10-13 12:51 UTC435
            other => panic!("seed {seed} recovered {other} stamina"),
f48daa8adf59atagen2026-10-13 12:51 UTC436
        }
f48daa8adf59atagen2026-10-13 12:51 UTC437
    }
f48daa8adf59atagen2026-10-13 12:51 UTC438
    assert!((8..=35).contains(&full), "{full} second winds in 400");
f48daa8adf59atagen2026-10-13 12:51 UTC439
}
e028a4184d00atagen2026-10-14 10:49 UTC440
e028a4184d00atagen2026-10-14 10:49 UTC441
#[tokio::test]
e028a4184d00atagen2026-10-14 10:49 UTC442
async fn eq_is_named_rather_than_numbered() {
e028a4184d00atagen2026-10-14 10:49 UTC443
    for (eq, setup, named) in [
e028a4184d00atagen2026-10-14 10:49 UTC444
        (0, None, "Flaccid"),
e028a4184d00atagen2026-10-14 10:49 UTC445
        (20, None, "Cold"),
e028a4184d00atagen2026-10-14 10:49 UTC446
        (40, None, "Warm"),
e028a4184d00atagen2026-10-14 10:49 UTC447
        (60, None, "Ideal"),
e028a4184d00atagen2026-10-14 10:49 UTC448
        (60, Some("burn"), "Swollen"),
e028a4184d00atagen2026-10-14 10:49 UTC449
        (80, None, "Swollen"),
e028a4184d00atagen2026-10-14 10:49 UTC450
        (88, None, "Throbbing"),
e028a4184d00atagen2026-10-14 10:49 UTC451
        (85, Some("ring"), "Straining"),
e028a4184d00atagen2026-10-14 10:49 UTC452
        (93, None, "Straining"),
e028a4184d00atagen2026-10-14 10:49 UTC453
        (97, None, "Imminent"),
e028a4184d00atagen2026-10-14 10:49 UTC454
        (40, Some("priapism"), "Priapic"),
e028a4184d00atagen2026-10-14 10:49 UTC455
    ] {
e028a4184d00atagen2026-10-14 10:49 UTC456
        let app = seeded_with(Floor::Bedroom, |run| {
e028a4184d00atagen2026-10-14 10:49 UTC457
            run.body.eq = eq;
e028a4184d00atagen2026-10-14 10:49 UTC458
            match setup {
e028a4184d00atagen2026-10-14 10:49 UTC459
                Some("burn") => run.body.conditions.friction_burn = true,
e028a4184d00atagen2026-10-14 10:49 UTC460
                Some("ring") => run.items.kit.ring = true,
e028a4184d00atagen2026-10-14 10:49 UTC461
                Some("priapism") => run.body.conditions.priapism_turns = 3,
e028a4184d00atagen2026-10-14 10:49 UTC462
                _ => {}
e028a4184d00atagen2026-10-14 10:49 UTC463
            }
e028a4184d00atagen2026-10-14 10:49 UTC464
        });
e028a4184d00atagen2026-10-14 10:49 UTC465
        let view = send(&app, "GET", "/runs/cafebabe", &[], "").await;
e028a4184d00atagen2026-10-14 10:49 UTC466
        assert_eq!(header(&view, "x-eq"), named, "{eq} with {setup:?}");
e028a4184d00atagen2026-10-14 10:49 UTC467
    }
e028a4184d00atagen2026-10-14 10:49 UTC468
}