an http based roguelike rpg about jelqing
Commit 058650ec79d6
tarball11 files changed+222-26
@@ -0,0 +1 @@
1
+load flake@@ -39,6 +39,11 @@ pub const MM_PER_INCH: f64 = 25.4;
3939
pub const HONEST_TOLERANCE: f64 = 0.06;4040
pub const EXAGGERATION: f64 = 1.0;4141
pub const STORAGE_GIRTH: f64 = 5.0;42
+pub const GIRTH_RESPECTED: f64 = 5.0;43
+pub const GIRTH_THICK: f64 = 5.5;44
+pub const FORESKIN_GLIDE: f64 = 1.0;45
+pub const LOCKER_ROOM_BAR: f64 = 8.0;46
+pub const LOCKER_ROOM_SHAME: u32 = 3;4247
pub const PUT_GAIN: f64 = 0.01;4348
pub const PUMP_GAIN: f64 = 0.5;4449
pub const EXTENDER_GAIN: f64 = 0.004;@@ -4,7 +4,7 @@ use serde_json::{Value, json};
44
55
use super::{66
Grip, Outcome, Run,7
- balance::{EXAGGERATION, PUT_GAIN},7
+ balance::{EXAGGERATION, GIRTH_RESPECTED, GIRTH_THICK, LOCKER_ROOM_BAR, PUT_GAIN},88
etag_matches, honest,99
};1010
use crate::problem::Problem;@@ -114,8 +114,39 @@ impl Run {
114114
Ok(Outcome::empty(StatusCode::NO_CONTENT))115115
}116116
117
- pub fn set_grip(&mut self, grip: Grip) {117
+ pub fn set_grip(&mut self, grip: Grip) -> Result<(), Problem> {118
+ self.grip_closes(grip)?;118119
self.grip = grip;119120
self.touch();121
+ Ok(())122
+ }123
+124
+ pub(super) fn grip_closes(&self, grip: Grip) -> Result<(), Problem> {125
+ if grip == Grip::Underhand && self.girth > GIRTH_THICK {126
+ return Err(Problem::new(127
+ StatusCode::PAYLOAD_TOO_LARGE,128
+ "grip-too-small",129
+ format!(130
+ "The underhand grip no longer closes around {:.2} inches of girth.",131
+ self.girth132
+ ),133
+ )134
+ .with("maximum", GIRTH_THICK));135
+ }136
+ Ok(())137
+ }138
+139
+ /// Everyone in the changing room self-reports, so the comparison is self-report against self-report.140
+ pub fn holds_own_in_the_locker_room(&self) -> bool {141
+ self.self_reported() >= LOCKER_ROOM_BAR142
+ }143
+144
+ pub fn girth_respected(&self) -> bool {145
+ self.girth >= GIRTH_RESPECTED146
+ }147
+148
+ /// A cylinder whose circumference is the girth.149
+ pub fn volume(&self, length: f64) -> f64 {150
+ self.girth * self.girth * length / (4.0 * std::f64::consts::PI)120151
}121152
}@@ -2,7 +2,10 @@ use axum::http::{StatusCode, header};
22
use serde_json::json;33
44
use super::{Floor, Outcome, Run, etag_matches, parse_credentials, round2};5
-use crate::{headers::X_DIAGNOSIS, problem::Problem};5
+use crate::{6
+ headers::{SERVER_TIMING, X_DIAGNOSIS},7
+ problem::Problem,8
+};69
710
const INTAKE: [&str; 3] = [811
"The clinic does not accept excuses.",@@ -60,14 +63,23 @@ impl Run {
6063
self.shame += 15;6164
}6265
}66
+ let volume = self.volume(self.length);67
+ let foreskin = self.foreskin.unwrap_or(0.0);68
+ let timing = format!(69
+ "length;dur={:.2}, girth;dur={:.2}, foreskin;dur={foreskin:.2}, volume;desc=\"cubic inches\";dur={volume:.2}",70
+ self.length, self.girth71
+ );6372
Ok(Outcome::json(6473
StatusCode::OK,6574
json!({6675
"diagnosis": "within normal range",6776
"measured_length": round2(self.length),77
+ "measured_girth": round2(self.girth),78
+ "volume": round2(volume),6879
"prescription": "Please stop.",6980
}),7081
)71
- .header(header::ETAG, etag))82
+ .header(header::ETAG, etag)83
+ .header(SERVER_TIMING, timing))7284
}7385
}@@ -5,7 +5,8 @@ use super::{
55
Cause, Elder, Floor, FloorState, Item, Outcome, Run,66
balance::{77
DRAWER_JAMS, EQ_BAND, FLOOR_ALLOWANCE, FLOOR_TURNS, FRICTION_FLOOR, GREGS_LOCKER,8
- KEY_IN_DRAWER, RULER_IN_DRAWER, STARTING_EQ, SUSPICION_LIMIT, ZIP,8
+ KEY_IN_DRAWER, LOCKER_ROOM_BAR, LOCKER_ROOM_SHAME, RULER_IN_DRAWER, STARTING_EQ,9
+ SUSPICION_LIMIT, ZIP,910
},1011
round2,1112
};@@ -27,6 +28,11 @@ impl Run {
2728
self.suspicion = 0;2829
self.shed(FRICTION_FLOOR);2930
self.mum = None;31
+ if self.floor == Floor::ChangingRoom && !self.holds_own_in_the_locker_room() {32
+ self.shame += LOCKER_ROOM_SHAME;33
+ self.notices34
+ .push("Kev is 8 inches, he says. Nobody asked you, which is worse.");35
+ }3036
if paid {3137
self.cash += FLOOR_ALLOWANCE;3238
}@@ -106,6 +112,7 @@ impl Run {
106112
{ "name": "Greg", "self_reported_length": 9.5 },107113
{ "name": "Dmitri", "self_reported_length": 8.75 },108114
{ "name": "a man in a towel", "self_reported_length": 11.0 },115
+ { "name": "Kev", "self_reported_length": LOCKER_ROOM_BAR },109116
]);110117
}111118
Floor::Hotel => view["portal"] = json!(self.path("/portal")),@@ -195,7 +195,14 @@ impl Run {
195195
}196196
if !honest(claimed, actual) {197197
self.shame += 1;198
- return Err(self.elder_strike(verification("Verification failed. The forum can tell.")));198
+ let failed = verification("Verification failed. The forum can tell.");199
+ if self.girth_respected() {200
+ return Err(failed.header(201
+ X_FORUM_NOTE,202
+ "The mods have let this one go. Girth is also a dimension.",203
+ ));204
+ }205
+ return Err(self.elder_strike(failed));199206
}200207
self.elder = Elder::Verified;201208
self.touch();@@ -176,8 +176,13 @@ impl Run {
176176
}177177
Floor::ChangingRoom => {178178
self.suspicion = 0;179
- self.shame += 5;180179
self.touch();180
+ if self.girth_respected() {181
+ self.notices182
+ .push("the gym staff have seen worse, and they leave it there");183
+ return Ok(());184
+ }185
+ self.shame += 5;181186
Err(Problem::new(182187
StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS,183188
"gym-staff",@@ -7,11 +7,11 @@ use super::{
77
Floor, Grip, LUBRICANTS, Lube, Outcome, Payload, Run, Story, Technique,88
balance::{99
ARRIVAL_BONUS, BAND_BONUS, BRANCH_TURN, CAT_TAKES_TOWEL, CHANGING_ROOM_CHILL, DANGER_EQ,10
- EQ_BAND, EQ_WOBBLE, EXTENDER_GAIN, FRICTION_BURN, FRICTION_BURN_AT, FRICTION_DRY,11
- FRICTION_GRIP, FRICTION_HOLD, FRICTION_MAX, FRICTION_MEDITATE, FRICTION_SPIT, GREED_HOT,12
- GREED_WARM, HOLD_EQ, HOT_EQ, MEDITATION_EQ, OBSERVED, PRIAPISM_EQ, PRIAPISM_TURNS,13
- PULLED_MUSCLE, PULLED_MUSCLE_INTENSITY, REFLECTION, RING_PRIAPISM, RING_RUPTURE_LINE,14
- RUPTURE_HOLD, RUPTURE_LINE, STORAGE_GIRTH, SUSPICION_LIMIT,10
+ EQ_BAND, EQ_WOBBLE, EXTENDER_GAIN, FORESKIN_GLIDE, FRICTION_BURN, FRICTION_BURN_AT,11
+ FRICTION_DRY, FRICTION_GRIP, FRICTION_HOLD, FRICTION_MAX, FRICTION_MEDITATE, FRICTION_SPIT,12
+ GIRTH_THICK, GREED_HOT, GREED_WARM, HOLD_EQ, HOT_EQ, MEDITATION_EQ, OBSERVED, PRIAPISM_EQ,13
+ PRIAPISM_TURNS, PULLED_MUSCLE, PULLED_MUSCLE_INTENSITY, REFLECTION, RING_PRIAPISM,14
+ RING_RUPTURE_LINE, RUPTURE_HOLD, RUPTURE_LINE, STORAGE_GIRTH, SUSPICION_LIMIT,1515
},1616
not_extended, round3,1717
state::{MumVisit, Observer, Pending},@@ -194,6 +194,9 @@ impl Run {
194194
)195195
.with("technique", Utility::Warmup.name()));196196
}197
+ if matches!(technique, Technique::Training(_)) {198
+ self.grip_closes(self.grip)?;199
+ }197200
match technique {198201
Technique::Utility(Utility::Pump) if !self.items.pump_pro => Err(Problem::new(199202
StatusCode::FAILED_DEPENDENCY,@@ -342,7 +345,10 @@ impl Run {
342345
6..=8 => 1,343346
_ => 2,344347
};345
- let mut friction = (base + vigour - slip).max(0);348
+ let unlubed = matches!(lube, None | Some(Lube::Spit));349
+ let thick = i32::from(unlubed && self.girth > GIRTH_THICK);350
+ let glide = i32::from(unlubed && self.foreskin.is_some_and(|f| f >= FORESKIN_GLIDE));351
+ let mut friction = (base + vigour + thick - slip - glide).max(0);346352
if friction > 0 && self.jelq_grip.is_some_and(|grip| grip != self.grip) {347353
friction -= FRICTION_GRIP;348354
}@@ -391,7 +397,12 @@ impl Run {
391397
self.eq = (self.eq + intensity * 2 - 6 - chill + wobble).clamp(0, 100);392398
393399
let mut suspicion = intensity / 5 + i32::from(lube == Some(Lube::Vaseline));394
- if matches!(self.floor, Floor::ChangingRoom | Floor::Nans) {400
+ let doubled = match self.floor {401
+ Floor::Nans => true,402
+ Floor::ChangingRoom => !self.holds_own_in_the_locker_room(),403
+ _ => false,404
+ };405
+ if doubled {395406
suspicion *= 2;396407
}397408
if self.floor == Floor::Nans && lube == Some(Lube::CoconutOil) {@@ -162,11 +162,22 @@ fn graveyard(state: &mut Shared, _: &Ctx) -> Response {
162162
Json(json!({ "runs": dead })).into_response()163163
}164164
165
+#[derive(Clone, Copy, PartialEq, Eq)]166
+enum Sort {167
+ SelfReported,168
+ ContentLength,169
+ /// Self-reported length against real girth, the only metric the forum respects.170
+ Volume,171
+}172
+165173
fn leaderboard(state: &mut Shared, ctx: &Ctx) -> Response {166
- let truthful = match ctx.query("sort") {167
- None | Some("self-reported-length") => false,174
+ let sort = match ctx.query("sort") {175
+ None | Some("self-reported-length") => Sort::SelfReported,176
+ Some("volume") => Sort::Volume,168177
Some("content-length") => match ctx.cookie("ruler") {169
- Some(ruler) if state.config.secret.verify("ruler", ruler) == Some("1") => true,178
+ Some(ruler) if state.config.secret.verify("ruler", ruler) == Some("1") => {179
+ Sort::ContentLength180
+ }170181
Some(_) => {171182
return Problem::new(172183
StatusCode::BAD_REQUEST,@@ -189,7 +200,7 @@ fn leaderboard(state: &mut Shared, ctx: &Ctx) -> Response {
189200
return Problem::new(190201
StatusCode::BAD_REQUEST,191202
"unknown-sort",192
- "The leaderboard sorts by self-reported-length or content-length.",203
+ "The leaderboard sorts by self-reported-length, volume or content-length.",193204
)194205
.into_response();195206
}@@ -200,12 +211,10 @@ fn leaderboard(state: &mut Shared, ctx: &Ctx) -> Response {
200211
.values()201212
.filter(|run| include_seeded || !run.seeded)202213
.collect();203
- let key = |run: &Run| {204
- if truthful {205
- run.current_length()206
- } else {207
- run.self_reported()208
- }214
+ let key = |run: &Run| match sort {215
+ Sort::SelfReported => run.self_reported(),216
+ Sort::ContentLength => run.current_length(),217
+ Sort::Volume => run.volume(run.self_reported()),209218
};210219
entries.sort_by(|a, b| key(b).total_cmp(&key(a)));211220
let entries: Vec<Value> = entries@@ -219,7 +228,10 @@ fn leaderboard(state: &mut Shared, ctx: &Ctx) -> Response {
219228
"ranked": !run.seeded,220229
"viral": run.filmed_at.is_some(),221230
});222
- if truthful {231
+ if sort == Sort::Volume {232
+ entry["volume"] = json!((run.volume(run.self_reported()) * 100.0).round() / 100.0);233
+ }234
+ if sort == Sort::ContentLength {223235
entry["content_length"] = json!((run.current_length() * MM_PER_INCH).round());224236
}225237
entry@@ -513,7 +513,9 @@ fn requested_grip(ctx: &Ctx) -> Option<Grip> {
513513
514514
fn get_grip(run: &mut Run, ctx: &Ctx) -> Response {515515
if let Some(grip) = requested_grip(ctx) {516
- run.set_grip(grip);516
+ if let Err(problem) = run.set_grip(grip) {517
+ return problem.into_response();518
+ }517519
let mut response = (518520
StatusCode::SWITCHING_PROTOCOLS,519521
[@@ -1421,6 +1421,7 @@ async fn the_working_supplement_resets_stamina() {
14211421
async fn dry_jelqs_build_friction_and_real_lube_does_not() {14221422
let app = seeded_with(Floor::Bedroom, |run| {14231423
run.warmed_up = true;1424
+ run.foreskin = Some(0.9);14241425
});14251426
let dry = send(&app, "JELQ", "/runs/cafebabe?technique=dry", &[], "").await;14261427
assert_eq!(header(&dry, "x-friction"), "2/10");@@ -1450,6 +1451,7 @@ async fn dry_jelqs_build_friction_and_real_lube_does_not() {
14501451
async fn switching_grips_makes_the_friction_cheaper() {14511452
let app = seeded_with(Floor::Bedroom, |run| {14521453
run.warmed_up = true;1454
+ run.foreskin = Some(0.9);14531455
});14541456
let dry = send(&app, "JELQ", "/runs/cafebabe?technique=dry", &[], "").await;14551457
assert_eq!(header(&dry, "x-friction"), "2/10");@@ -1936,6 +1938,7 @@ async fn friction_rises_with_vigour_and_edging_chafes() {
19361938
run.here.wifi_accepted = true;19371939
run.warmed_up = true;19381940
run.eq = 55;1941
+ run.foreskin = Some(0.5);19391942
});19401943
let uri = format!("/runs/cafebabe?technique=wet&intensity={intensity}");19411944
let jelq = send(&app, "JELQ", &uri, &spit, "").await;@@ -1949,7 +1952,107 @@ async fn friction_rises_with_vigour_and_edging_chafes() {
19491952
run.here.wifi_accepted = true;19501953
run.warmed_up = true;19511954
run.eq = 55;1955
+ run.foreskin = Some(0.5);19521956
});19531957
let edge = send(&app, "JELQ", "/runs/cafebabe?technique=edge", &[], "").await;19541958
assert_eq!(header(&edge, "x-friction"), "2/10");19551959
}1960
+1961
+#[tokio::test]1962
+async fn the_body_changes_how_the_skin_takes_it() {1963
+ let spit = [("content-type", "text/plain; charset=spit")];1964
+ for (foreskin, girth, friction) in [(0.5, 4.5, "1/10"), (1.2, 4.5, "0/10"), (0.5, 5.8, "2/10")]1965
+ {1966
+ let app = seeded_with(Floor::Hotel, |run| {1967
+ run.here.wifi_accepted = true;1968
+ run.warmed_up = true;1969
+ run.eq = 55;1970
+ run.foreskin = Some(foreskin);1971
+ run.girth = girth;1972
+ });1973
+ let jelq = send(&app, "JELQ", "/runs/cafebabe?technique=wet", &spit, "").await;1974
+ assert_eq!(header(&jelq, "x-friction"), friction, "{foreskin} {girth}");1975
+ }1976
+}1977
+1978
+#[tokio::test]1979
+async fn a_thick_girth_outgrows_the_underhand_grip() {1980
+ let app = seeded_with(Floor::Bedroom, |run| run.girth = 5.8);1981
+ let grip = send(1982
+ &app,1983
+ "GET",1984
+ "/runs/cafebabe/grip",1985
+ &[("upgrade", "underhand"), ("connection", "upgrade")],1986
+ "",1987
+ )1988
+ .await;1989
+ assert_eq!(grip.status(), StatusCode::PAYLOAD_TOO_LARGE);1990
+}1991
+1992
+#[tokio::test]1993
+async fn respectable_girth_waves_off_the_gym_staff() {1994
+ for (girth, status) in [1995
+ (4.5, StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS),1996
+ (5.2, StatusCode::OK),1997
+ ] {1998
+ let app = seeded_with(Floor::ChangingRoom, |run| {1999
+ run.warmed_up = true;2000
+ run.eq = 45;2001
+ run.girth = girth;2002
+ run.suspicion = SUSPICION_LIMIT;2003
+ });2004
+ let edge = send(&app, "JELQ", "/runs/cafebabe?technique=edge", &[], "").await;2005
+ if girth > 5.0 {2006
+ assert_ne!(edge.status(), StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS);2007
+ assert!(header(&edge, "warning").contains("gym staff"));2008
+ } else {2009
+ assert_eq!(edge.status(), status);2010
+ }2011
+ }2012
+}2013
+2014
+#[tokio::test]2015
+async fn kev_is_the_bar_in_the_changing_room() {2016
+ for (length, shamed) in [(5.0, true), (7.0, false)] {2017
+ let app = seeded_with(Floor::Bathroom, |run| {2018
+ run.length = length;2019
+ run.here.turns = 6;2020
+ });2021
+ let left = send(&app, "POST", "/runs/cafebabe/floor", &[], "").await;2022
+ assert_eq!(header(&left, "x-floor"), "3");2023
+ let warning = left2024
+ .headers()2025
+ .get("warning")2026
+ .map(|w| w.to_str().unwrap().to_owned());2027
+ assert_eq!(2028
+ warning.is_some_and(|w| w.contains("Kev")),2029
+ shamed,2030
+ "length {length}"2031
+ );2032
+ }2033
+}2034
+2035
+#[tokio::test]2036
+async fn the_forum_respects_volume() {2037
+ let state = Arc::new(AppState::default());2038
+ for (id, length, girth) in [("slim", 7.0, 4.0), ("stout", 5.0, 5.5)] {2039
+ let mut run = Run::new(id.into(), id.into(), 1);2040
+ run.length = length;2041
+ run.girth = girth;2042
+ state.insert(run);2043
+ }2044
+ let app = router(state);2045
+ let board = json(2046
+ send(2047
+ &app,2048
+ "GET",2049
+ "/leaderboard?sort=volume&seeded=include",2050
+ &[],2051
+ "",2052
+ )2053
+ .await,2054
+ )2055
+ .await;2056
+ assert_eq!(board["entries"][0]["public_id"], "stout");2057
+ assert!(board["entries"][0]["volume"].as_f64().unwrap() > 0.0);2058
+}