atagen

jelq

public

an http based roguelike rpg about jelqing

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