atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0

Commit 177535ac8abf

tarball

atagen <boss@atagen.co>2026-10-10 15:15 UTC

5 files changed+69-11
jelqd: fix issues found by exploratory play

Honesty checks rounded the raw length to tenths while the run displays it to hundredths, so a length like 5.2496 shown as 5.25 failed verification against itself and softlocked the Elder. Reckless play almost never died because the 425 premature exit always fired before the 500 rupture could, so EQ over 95 now ruptures one time in three.
Modified file: src/game/encounters.rs+23 additions−6 deletions
@@ -6,8 +6,8 @@ use serde::Deserialize;
66 use serde_json::{Value, json};
77
88 use super::{
9- Floor, Grip, Outcome, Run, SHOP, SUSPICION_LIMIT, X_DIAGNOSIS, parse_credentials, round2,
10- tenths,
9+ Floor, Grip, Outcome, Run, SHOP, SUSPICION_LIMIT, X_DIAGNOSIS, honest, parse_credentials,
10+ round2,
1111 };
1212 use crate::problem::Problem;
1313
@@ -114,6 +114,23 @@ impl Run {
114114 ));
115115 };
116116
117+ let spoken = authorization
118+ .split_once(' ')
119+ .map(|(_, rest)| rest.trim().trim_matches('"').to_ascii_lowercase())
120+ .unwrap_or_default();
121+ if TRUTHS.contains(&spoken.as_str()) {
122+ self.shame += 10;
123+ self.leave_door();
124+ return Ok(());
125+ }
126+ if parse_credentials(authorization, "basic").is_some() {
127+ return Err(challenge(Problem::new(
128+ StatusCode::UNAUTHORIZED,
129+ "basic",
130+ "Mom does not accept Basic authentication. Nobody should.",
131+ )));
132+ }
133+
117134 let excuse = if digest {
118135 let Some(params) = digest_params(authorization) else {
119136 return Err(challenge(Problem::new(
@@ -525,7 +542,7 @@ impl Run {
525542 self.touch();
526543 return Err(exaggeration());
527544 }
528- if tenths(claimed) != tenths(actual) {
545+ if !honest(claimed, actual) {
529546 self.shame += 1;
530547 self.touch();
531548 return Err(
@@ -625,7 +642,7 @@ impl Run {
625642 self.touch();
626643 return Err(exaggeration());
627644 }
628- if tenths(claimed) != tenths(actual) {
645+ if !honest(claimed, actual) {
629646 self.shame += 1;
630647 self.touch();
631648 return Err(verification("Verification failed. The forum can tell."));
@@ -672,9 +689,9 @@ impl Run {
672689 let Some(authorization) = authorization else {
673690 return Err(intake.header(header::WWW_AUTHENTICATE, "Bearer realm=\"clinic\""));
674691 };
675- let honest = parse_credentials(authorization, "bearer")
692+ let truthful = parse_credentials(authorization, "bearer")
676693 .is_some_and(|token| token.eq_ignore_ascii_case("honesty"));
677- if !honest {
694+ if !truthful {
678695 let hint = INTAKE[self.intake_attempts.min(INTAKE.len() - 1)];
679696 self.intake_attempts += 1;
680697 return Err(intake.header(
Modified file: src/game/jelq.rs+3 additions
@@ -339,6 +339,9 @@ impl Run {
339339 .header(header::CONNECTION, "close"));
340340 }
341341 if self.eq > 95 {
342+ if self.rng.random_ratio(1, 3) {
343+ return Err(self.rupture());
344+ }
342345 self.shame += 5;
343346 self.next_floor();
344347 return Err(Problem::new(
Modified file: src/game/mod.rs+2 additions−2 deletions
@@ -290,8 +290,8 @@ fn round3(x: f64) -> f64 {
290290 (x * 1000.0).round() / 1000.0
291291 }
292292
293-fn tenths(x: f64) -> i64 {
294- (x * 10.0).round() as i64
293+fn honest(claimed: f64, actual: f64) -> bool {
294+ (claimed - actual).abs() < 0.06
295295 }
296296
297297 fn not_extended() -> Problem {
Modified file: src/http.rs+27 additions−3 deletions
@@ -190,6 +190,24 @@ fn unsupported(method: &Method, allow: &'static str) -> Response {
190190 method.as_str(),
191191 "GET" | "HEAD" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS" | "TRACE" | "JELQ"
192192 );
193+ if method.as_str() == "BREW" {
194+ return Problem::new(
195+ StatusCode::IM_A_TEAPOT,
196+ "brew",
197+ "The server is a teapot. RFC 2324 is clear on this. It is not yet short and stout.",
198+ )
199+ .into_response();
200+ }
201+ if method.as_str().eq_ignore_ascii_case("jelq") {
202+ return Problem::new(
203+ StatusCode::NOT_IMPLEMENTED,
204+ "not-implemented",
205+ format!(
206+ "The server does not implement {method}. Methods are case-sensitive. JELQ is shouted for a reason."
207+ ),
208+ )
209+ .into_response();
210+ }
193211 if !known {
194212 return Problem::new(
195213 StatusCode::NOT_IMPLEMENTED,
@@ -478,7 +496,7 @@ fn jelq(
478496 now: Instant,
479497 ) -> Response {
480498 let techniques_href = format!("/runs/{}/techniques", run.id);
481- let Some(names) = query.get("technique") else {
499+ let Some(names) = query.get("technique").filter(|n| !n.trim().is_empty()) else {
482500 return Problem::new(
483501 StatusCode::BAD_REQUEST,
484502 "technique-required",
@@ -488,7 +506,7 @@ fn jelq(
488506 .into_response();
489507 };
490508 let mut techniques = Vec::new();
491- for name in names.split(',').map(str::trim) {
509+ for name in names.split(',').map(str::trim).filter(|n| !n.is_empty()) {
492510 let Some(technique) = Technique::parse(name) else {
493511 return Problem::new(
494512 StatusCode::BAD_REQUEST,
@@ -881,6 +899,12 @@ async fn grip(
881899 let headers = request.headers().clone();
882900 let wanted = requested_grip(&headers);
883901 let upgrade = wanted.map(|_| hyper::upgrade::on(&mut request));
902+ let detail = match header_str(&headers, header::UPGRADE) {
903+ Some(protocol) if wanted.is_none() => {
904+ format!("The server does not know the {protocol} grip. Grips are protocols.")
905+ }
906+ _ => "Grips are protocols. Change grip by upgrading the connection.".to_owned(),
907+ };
884908 let response = with_run(&state, &id, &headers, Scope::Home, |run| {
885909 match (method.as_str(), wanted) {
886910 ("GET" | "HEAD", Some(grip)) => {
@@ -899,7 +923,7 @@ async fn grip(
899923 [(header::UPGRADE, "overhand, underhand")],
900924 Json(json!({
901925 "grip": run.grip.name(),
902- "detail": "Grips are protocols. Change grip by upgrading the connection.",
926+ "detail": detail,
903927 })),
904928 )
905929 .into_response(),
Modified file: tests/http.rs+14 additions
@@ -425,3 +425,17 @@ async fn http_1_0_gets_the_pdf_treatment() {
425425 let response = jelqd::app().oneshot(request).await.unwrap();
426426 assert_eq!(response.status(), StatusCode::HTTP_VERSION_NOT_SUPPORTED);
427427 }
428+
429+#[tokio::test]
430+async fn the_displayed_length_is_accepted_as_honest() {
431+ let app = seeded_with(Floor::Forum, |run| run.length = 5.2496);
432+ let response = send(
433+ &app,
434+ "GET",
435+ "/runs/cafebabe/elder",
436+ &[("proxy-authorization", "Ruler 5.25")],
437+ "",
438+ )
439+ .await;
440+ assert_eq!(response.status(), StatusCode::OK);
441+}