an http based roguelike rpg about jelqing
Blame: src/game/body.rs
782069860236 | atagen | 2026-10-10 12:44 UTC | 1 | use axum::http::StatusCode; |
782069860236 | atagen | 2026-10-10 12:44 UTC | 2 | use serde::Deserialize; |
782069860236 | atagen | 2026-10-10 12:44 UTC | 3 | use serde_json::{Value, json}; |
782069860236 | atagen | 2026-10-10 12:44 UTC | 4 | |
782069860236 | atagen | 2026-10-10 12:44 UTC | 5 | use super::{ |
782069860236 | atagen | 2026-10-10 12:44 UTC | 6 | Grip, Outcome, Run, |
c4d70621d377 | atagen | 2026-10-12 11:56 UTC | 7 | balance::{EXAGGERATION, GIRTH_RESPECTED, GIRTH_THICK, LOCKER_ROOM_BAR, PATCH_EQ, PUT_GAIN}, |
782069860236 | atagen | 2026-10-10 12:44 UTC | 8 | etag_matches, honest, |
782069860236 | atagen | 2026-10-10 12:44 UTC | 9 | }; |
782069860236 | atagen | 2026-10-10 12:44 UTC | 10 | use crate::problem::Problem; |
782069860236 | atagen | 2026-10-10 12:44 UTC | 11 | |
782069860236 | atagen | 2026-10-10 12:44 UTC | 12 | #[derive(Deserialize)] |
782069860236 | atagen | 2026-10-10 12:44 UTC | 13 | pub struct PatchOp { |
782069860236 | atagen | 2026-10-10 12:44 UTC | 14 | pub op: String, |
782069860236 | atagen | 2026-10-10 12:44 UTC | 15 | pub path: String, |
782069860236 | atagen | 2026-10-10 12:44 UTC | 16 | #[serde(default)] |
782069860236 | atagen | 2026-10-10 12:44 UTC | 17 | pub value: Value, |
782069860236 | atagen | 2026-10-10 12:44 UTC | 18 | } |
782069860236 | atagen | 2026-10-10 12:44 UTC | 19 | |
782069860236 | atagen | 2026-10-10 12:44 UTC | 20 | pub(super) fn exaggeration() -> Problem { |
782069860236 | atagen | 2026-10-10 12:44 UTC | 21 | Problem::new( |
782069860236 | atagen | 2026-10-10 12:44 UTC | 22 | StatusCode::BAD_REQUEST, |
782069860236 | atagen | 2026-10-10 12:44 UTC | 23 | "exaggeration", |
782069860236 | atagen | 2026-10-10 12:44 UTC | 24 | "The server has measured. Lying is not a technique.", |
782069860236 | atagen | 2026-10-10 12:44 UTC | 25 | ) |
782069860236 | atagen | 2026-10-10 12:44 UTC | 26 | } |
782069860236 | atagen | 2026-10-10 12:44 UTC | 27 | |
782069860236 | atagen | 2026-10-10 12:44 UTC | 28 | impl Run { |
782069860236 | atagen | 2026-10-10 12:44 UTC | 29 | pub fn put(&mut self) -> Outcome { |
782069860236 | atagen | 2026-10-10 12:44 UTC | 30 | if self.put_applied { |
782069860236 | atagen | 2026-10-10 12:44 UTC | 31 | return Outcome::empty(StatusCode::NO_CONTENT); |
782069860236 | atagen | 2026-10-10 12:44 UTC | 32 | } |
782069860236 | atagen | 2026-10-10 12:44 UTC | 33 | self.put_applied = true; |
782069860236 | atagen | 2026-10-10 12:44 UTC | 34 | self.length += PUT_GAIN; |
782069860236 | atagen | 2026-10-10 12:44 UTC | 35 | self.touch(); |
782069860236 | atagen | 2026-10-10 12:44 UTC | 36 | Outcome::json(StatusCode::OK, json!({ "detail": "Applied." })) |
782069860236 | atagen | 2026-10-10 12:44 UTC | 37 | } |
782069860236 | atagen | 2026-10-10 12:44 UTC | 38 | |
c4d70621d377 | atagen | 2026-10-12 11:56 UTC | 39 | /// Being measured is not arousing, so every PATCH costs a little EQ whatever it finds. |
782069860236 | atagen | 2026-10-10 12:44 UTC | 40 | pub fn patch(&mut self, ops: &[PatchOp]) -> Result<Outcome, Problem> { |
c4d70621d377 | atagen | 2026-10-12 11:56 UTC | 41 | self.reset_eq(self.eq - PATCH_EQ); |
c4d70621d377 | atagen | 2026-10-12 11:56 UTC | 42 | self.touch(); |
782069860236 | atagen | 2026-10-10 12:44 UTC | 43 | for op in ops { |
782069860236 | atagen | 2026-10-10 12:44 UTC | 44 | if op.op != "test" { |
782069860236 | atagen | 2026-10-10 12:44 UTC | 45 | return Err(Problem::new( |
782069860236 | atagen | 2026-10-10 12:44 UTC | 46 | StatusCode::UNPROCESSABLE_ENTITY, |
782069860236 | atagen | 2026-10-10 12:44 UTC | 47 | "wishful-thinking", |
782069860236 | atagen | 2026-10-10 12:44 UTC | 48 | "The server accepts only the truth.", |
782069860236 | atagen | 2026-10-10 12:44 UTC | 49 | ) |
782069860236 | atagen | 2026-10-10 12:44 UTC | 50 | .with("op", op.op.clone())); |
782069860236 | atagen | 2026-10-10 12:44 UTC | 51 | } |
782069860236 | atagen | 2026-10-10 12:44 UTC | 52 | let actual = match op.path.as_str() { |
782069860236 | atagen | 2026-10-10 12:44 UTC | 53 | "/length" => self.current_length(), |
782069860236 | atagen | 2026-10-10 12:44 UTC | 54 | "/girth" => self.girth, |
782069860236 | atagen | 2026-10-10 12:44 UTC | 55 | "/foreskin" => self.foreskin.unwrap_or(0.0), |
782069860236 | atagen | 2026-10-10 12:44 UTC | 56 | _ => { |
782069860236 | atagen | 2026-10-10 12:44 UTC | 57 | return Err(Problem::new( |
782069860236 | atagen | 2026-10-10 12:44 UTC | 58 | StatusCode::UNPROCESSABLE_ENTITY, |
782069860236 | atagen | 2026-10-10 12:44 UTC | 59 | "no-such-measurement", |
782069860236 | atagen | 2026-10-10 12:44 UTC | 60 | "There is no such measurement.", |
782069860236 | atagen | 2026-10-10 12:44 UTC | 61 | ) |
782069860236 | atagen | 2026-10-10 12:44 UTC | 62 | .with("path", op.path.clone())); |
782069860236 | atagen | 2026-10-10 12:44 UTC | 63 | } |
782069860236 | atagen | 2026-10-10 12:44 UTC | 64 | }; |
782069860236 | atagen | 2026-10-10 12:44 UTC | 65 | let Some(claimed) = op.value.as_f64() else { |
782069860236 | atagen | 2026-10-10 12:44 UTC | 66 | return Err(Problem::new( |
782069860236 | atagen | 2026-10-10 12:44 UTC | 67 | StatusCode::BAD_REQUEST, |
782069860236 | atagen | 2026-10-10 12:44 UTC | 68 | "malformed-patch", |
782069860236 | atagen | 2026-10-10 12:44 UTC | 69 | "A test requires a numeric value.", |
782069860236 | atagen | 2026-10-10 12:44 UTC | 70 | )); |
782069860236 | atagen | 2026-10-10 12:44 UTC | 71 | }; |
782069860236 | atagen | 2026-10-10 12:44 UTC | 72 | if op.path == "/length" && claimed >= actual + EXAGGERATION { |
782069860236 | atagen | 2026-10-10 12:44 UTC | 73 | self.shame += 2; |
782069860236 | atagen | 2026-10-10 12:44 UTC | 74 | self.touch(); |
782069860236 | atagen | 2026-10-10 12:44 UTC | 75 | return Err(exaggeration()); |
782069860236 | atagen | 2026-10-10 12:44 UTC | 76 | } |
782069860236 | atagen | 2026-10-10 12:44 UTC | 77 | if !honest(claimed, actual) { |
782069860236 | atagen | 2026-10-10 12:44 UTC | 78 | self.shame += 1; |
782069860236 | atagen | 2026-10-10 12:44 UTC | 79 | self.touch(); |
782069860236 | atagen | 2026-10-10 12:44 UTC | 80 | return Err( |
782069860236 | atagen | 2026-10-10 12:44 UTC | 81 | Problem::new(StatusCode::CONFLICT, "test-failed", "The test failed.") |
782069860236 | atagen | 2026-10-10 12:44 UTC | 82 | .with("path", op.path.clone()), |
782069860236 | atagen | 2026-10-10 12:44 UTC | 83 | ); |
782069860236 | atagen | 2026-10-10 12:44 UTC | 84 | } |
782069860236 | atagen | 2026-10-10 12:44 UTC | 85 | } |
782069860236 | atagen | 2026-10-10 12:44 UTC | 86 | Ok(Outcome::json( |
782069860236 | atagen | 2026-10-10 12:44 UTC | 87 | StatusCode::OK, |
782069860236 | atagen | 2026-10-10 12:44 UTC | 88 | json!({ "detail": "Correct." }), |
782069860236 | atagen | 2026-10-10 12:44 UTC | 89 | )) |
782069860236 | atagen | 2026-10-10 12:44 UTC | 90 | } |
782069860236 | atagen | 2026-10-10 12:44 UTC | 91 | |
782069860236 | atagen | 2026-10-10 12:44 UTC | 92 | pub fn delete_foreskin(&mut self, if_match: Option<&str>) -> Result<Outcome, Problem> { |
782069860236 | atagen | 2026-10-10 12:44 UTC | 93 | if self.foreskin.is_none() { |
782069860236 | atagen | 2026-10-10 12:44 UTC | 94 | return Err(Problem::new( |
782069860236 | atagen | 2026-10-10 12:44 UTC | 95 | StatusCode::GONE, |
782069860236 | atagen | 2026-10-10 12:44 UTC | 96 | "foreskin-gone", |
782069860236 | atagen | 2026-10-10 12:44 UTC | 97 | "The foreskin is gone. It will not be returning.", |
782069860236 | atagen | 2026-10-10 12:44 UTC | 98 | )); |
782069860236 | atagen | 2026-10-10 12:44 UTC | 99 | } |
782069860236 | atagen | 2026-10-10 12:44 UTC | 100 | let Some(if_match) = if_match else { |
782069860236 | atagen | 2026-10-10 12:44 UTC | 101 | return Err(Problem::new( |
782069860236 | atagen | 2026-10-10 12:44 UTC | 102 | StatusCode::PRECONDITION_REQUIRED, |
782069860236 | atagen | 2026-10-10 12:44 UTC | 103 | "confirmation-required", |
782069860236 | atagen | 2026-10-10 12:44 UTC | 104 | "The server requires you to be sure. Send If-Match.", |
782069860236 | atagen | 2026-10-10 12:44 UTC | 105 | )); |
782069860236 | atagen | 2026-10-10 12:44 UTC | 106 | }; |
782069860236 | atagen | 2026-10-10 12:44 UTC | 107 | if !etag_matches(if_match, &self.etag()) { |
782069860236 | atagen | 2026-10-10 12:44 UTC | 108 | return Err(Problem::new( |
782069860236 | atagen | 2026-10-10 12:44 UTC | 109 | StatusCode::PRECONDITION_FAILED, |
782069860236 | atagen | 2026-10-10 12:44 UTC | 110 | "stale-foreskin", |
782069860236 | atagen | 2026-10-10 12:44 UTC | 111 | "The foreskin has changed since you last looked at it.", |
782069860236 | atagen | 2026-10-10 12:44 UTC | 112 | )); |
782069860236 | atagen | 2026-10-10 12:44 UTC | 113 | } |
782069860236 | atagen | 2026-10-10 12:44 UTC | 114 | self.foreskin = None; |
782069860236 | atagen | 2026-10-10 12:44 UTC | 115 | self.shame += 20; |
782069860236 | atagen | 2026-10-10 12:44 UTC | 116 | self.touch(); |
782069860236 | atagen | 2026-10-10 12:44 UTC | 117 | Ok(Outcome::empty(StatusCode::NO_CONTENT)) |
782069860236 | atagen | 2026-10-10 12:44 UTC | 118 | } |
782069860236 | atagen | 2026-10-10 12:44 UTC | 119 | |
058650ec79d6 | atagen | 2026-10-12 10:49 UTC | 120 | pub fn set_grip(&mut self, grip: Grip) -> Result<(), Problem> { |
058650ec79d6 | atagen | 2026-10-12 10:49 UTC | 121 | self.grip_closes(grip)?; |
782069860236 | atagen | 2026-10-10 12:44 UTC | 122 | self.grip = grip; |
782069860236 | atagen | 2026-10-10 12:44 UTC | 123 | self.touch(); |
058650ec79d6 | atagen | 2026-10-12 10:49 UTC | 124 | Ok(()) |
058650ec79d6 | atagen | 2026-10-12 10:49 UTC | 125 | } |
058650ec79d6 | atagen | 2026-10-12 10:49 UTC | 126 | |
058650ec79d6 | atagen | 2026-10-12 10:49 UTC | 127 | pub(super) fn grip_closes(&self, grip: Grip) -> Result<(), Problem> { |
058650ec79d6 | atagen | 2026-10-12 10:49 UTC | 128 | if grip == Grip::Underhand && self.girth > GIRTH_THICK { |
058650ec79d6 | atagen | 2026-10-12 10:49 UTC | 129 | return Err(Problem::new( |
058650ec79d6 | atagen | 2026-10-12 10:49 UTC | 130 | StatusCode::PAYLOAD_TOO_LARGE, |
058650ec79d6 | atagen | 2026-10-12 10:49 UTC | 131 | "grip-too-small", |
058650ec79d6 | atagen | 2026-10-12 10:49 UTC | 132 | format!( |
058650ec79d6 | atagen | 2026-10-12 10:49 UTC | 133 | "The underhand grip no longer closes around {:.2} inches of girth.", |
058650ec79d6 | atagen | 2026-10-12 10:49 UTC | 134 | self.girth |
058650ec79d6 | atagen | 2026-10-12 10:49 UTC | 135 | ), |
058650ec79d6 | atagen | 2026-10-12 10:49 UTC | 136 | ) |
058650ec79d6 | atagen | 2026-10-12 10:49 UTC | 137 | .with("maximum", GIRTH_THICK)); |
058650ec79d6 | atagen | 2026-10-12 10:49 UTC | 138 | } |
058650ec79d6 | atagen | 2026-10-12 10:49 UTC | 139 | Ok(()) |
058650ec79d6 | atagen | 2026-10-12 10:49 UTC | 140 | } |
058650ec79d6 | atagen | 2026-10-12 10:49 UTC | 141 | |
058650ec79d6 | atagen | 2026-10-12 10:49 UTC | 142 | /// Everyone in the changing room self-reports, so the comparison is self-report against self-report. |
058650ec79d6 | atagen | 2026-10-12 10:49 UTC | 143 | pub fn holds_own_in_the_locker_room(&self) -> bool { |
058650ec79d6 | atagen | 2026-10-12 10:49 UTC | 144 | self.self_reported() >= LOCKER_ROOM_BAR |
058650ec79d6 | atagen | 2026-10-12 10:49 UTC | 145 | } |
058650ec79d6 | atagen | 2026-10-12 10:49 UTC | 146 | |
058650ec79d6 | atagen | 2026-10-12 10:49 UTC | 147 | pub fn girth_respected(&self) -> bool { |
058650ec79d6 | atagen | 2026-10-12 10:49 UTC | 148 | self.girth >= GIRTH_RESPECTED |
058650ec79d6 | atagen | 2026-10-12 10:49 UTC | 149 | } |
159f0998263b | atagen | 2026-10-12 11:01 UTC | 150 | } |
058650ec79d6 | atagen | 2026-10-12 10:49 UTC | 151 | |
159f0998263b | atagen | 2026-10-12 11:01 UTC | 152 | /// A cylinder whose circumference is the girth. |
159f0998263b | atagen | 2026-10-12 11:01 UTC | 153 | pub fn volume(length: f64, girth: f64) -> f64 { |
159f0998263b | atagen | 2026-10-12 11:01 UTC | 154 | girth * girth * length / (4.0 * std::f64::consts::PI) |
782069860236 | atagen | 2026-10-10 12:44 UTC | 155 | } |