atagen

jelq

public

an http based roguelike rpg about jelqing

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