an http based roguelike rpg about jelqing
Blame: src/game/oracle.rs
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 1 | //! Where the game's decisions come from. The seeded rng is the default; an |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 2 | //! operator can instead point the server at a System One service and let the |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 3 | //! model answer every roll over HTTP (JELQD_ORACLE=clm). |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 4 | //! |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 5 | //! Identity randomness (run ids, etag salts, nonces) never goes to the model. |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 6 | //! The Clm arm carries its own rng for those, seeded like the rng arm's. |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 7 | |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 8 | use rand::{Rng, SeedableRng, rngs::StdRng}; |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 9 | use serde_json::{Value, json}; |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 10 | |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 11 | pub(crate) struct Roll { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 12 | pub(crate) state: String, |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 13 | pub(crate) site: &'static str, |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 14 | pub(crate) ask: &'static str, |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 15 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 16 | |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 17 | pub(crate) enum Oracle { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 18 | Rng(StdRng), |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 19 | Clm { clm: Clm, technical: StdRng }, |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 20 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 21 | |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 22 | pub(crate) enum OracleTemplate { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 23 | Rng, |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 24 | Clm { url: String }, |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 25 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 26 | |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 27 | pub(crate) struct Clm { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 28 | url: String, |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 29 | agent: ureq::Agent, |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 30 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 31 | |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 32 | impl Oracle { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 33 | pub(crate) fn build(template: &OracleTemplate, seed: u64) -> Self { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 34 | match template { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 35 | OracleTemplate::Rng => Self::Rng(StdRng::seed_from_u64(seed)), |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 36 | OracleTemplate::Clm { url } => Self::Clm { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 37 | clm: Clm::new(url), |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 38 | technical: StdRng::seed_from_u64(seed), |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 39 | }, |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 40 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 41 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 42 | |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 43 | fn technical(&mut self) -> &mut StdRng { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 44 | match self { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 45 | Self::Rng(rng) => rng, |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 46 | Self::Clm { technical, .. } => technical, |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 47 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 48 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 49 | |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 50 | pub(crate) fn listens(&self) -> bool { |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 51 | matches!(self, Self::Clm { .. }) |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 52 | } |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 53 | |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 54 | /// Identity randomness, never a game decision. |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 55 | pub(crate) fn u64(&mut self) -> u64 { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 56 | self.technical().random() |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 57 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 58 | |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 59 | pub(crate) fn chance(&mut self, roll: Roll, odds: u32) -> bool { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 60 | match self { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 61 | Self::Rng(rng) => rng.random_ratio(1, odds), |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 62 | Self::Clm { clm, .. } => { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 63 | let ask = format!("{}, with odds of 1 in {odds}.", roll.ask); |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 64 | clm.decide(&roll.state, roll.site, &ask) > 0.5 |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 65 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 66 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 67 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 68 | |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 69 | /// The model believes dull statements more readily than rare warnings, so the ask states the calm outcome. |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 70 | pub(crate) fn chance_not(&mut self, roll: Roll, odds: u32) -> bool { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 71 | match self { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 72 | Self::Rng(rng) => rng.random_ratio(1, odds), |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 73 | Self::Clm { clm, .. } => { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 74 | let ask = format!("{}, with odds of 1 in {odds}.", roll.ask); |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 75 | clm.decide(&roll.state, roll.site, &ask) <= 0.5 |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 76 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 77 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 78 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 79 | |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 80 | /// The noul head answers interrupt questions with yes, and the choice head, offered both answers, does not. |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 81 | pub(crate) fn ratio_choice( |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 82 | &mut self, |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 83 | roll: Roll, |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 84 | num: u32, |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 85 | den: u32, |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 86 | calm: &str, |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 87 | event: &str, |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 88 | ) -> bool { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 89 | match self { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 90 | Self::Rng(rng) => rng.random_ratio(num, den), |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 91 | Self::Clm { clm, .. } => { |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 92 | let i = clm.choose(&roll.state, roll.site, roll.ask, &[calm, event]); |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 93 | i == 1 |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 94 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 95 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 96 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 97 | |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 98 | pub(crate) fn ratio(&mut self, roll: Roll, num: u32, den: u32) -> bool { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 99 | match self { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 100 | Self::Rng(rng) => rng.random_ratio(num, den), |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 101 | Self::Clm { clm, .. } => { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 102 | let ask = format!("{}, with odds of about {num} in {den}.", roll.ask); |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 103 | clm.decide(&roll.state, roll.site, &ask) > 0.5 |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 104 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 105 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 106 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 107 | |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 108 | /// The model picks among the descriptions and never sees the weights. |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 109 | pub(crate) fn pick(&mut self, roll: Roll, weights: &[u32], options: &[&str]) -> usize { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 110 | match self { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 111 | Self::Rng(rng) => { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 112 | let total: u32 = weights.iter().sum(); |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 113 | let mut draw = rng.random_range(0..total); |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 114 | for (i, w) in weights.iter().enumerate() { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 115 | if draw < *w { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 116 | return i; |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 117 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 118 | draw -= *w; |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 119 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 120 | weights.len() - 1 |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 121 | } |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 122 | Self::Clm { clm, .. } => clm.choose(&roll.state, roll.site, roll.ask, options), |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 123 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 124 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 125 | |
8b8e83329a1d | atagen | 2026-10-12 12:01 UTC | 126 | /// The model picks from a 0.1-step grid, since it can only choose among described options. |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 127 | pub(crate) fn span(&mut self, roll: Roll, lo: f64, hi: f64) -> f64 { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 128 | match self { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 129 | Self::Rng(rng) => rng.random_range(lo..hi), |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 130 | Self::Clm { clm, .. } => { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 131 | let mut last = format!("{lo:.1}"); |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 132 | let mut values = Vec::new(); |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 133 | let mut v = lo; |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 134 | while v < hi { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 135 | values.push(last); |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 136 | v += 0.1; |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 137 | last = format!("{v:.1}"); |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 138 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 139 | let options: Vec<&str> = values.iter().map(String::as_str).collect(); |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 140 | let ask = format!("{}. Answer with a value.", roll.ask); |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 141 | let i = clm.choose(&roll.state, roll.site, &ask, &options); |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 142 | lo + 0.1 * i as f64 |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 143 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 144 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 145 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 146 | |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 147 | pub(crate) fn span_int(&mut self, roll: Roll, lo: i32, hi: i32) -> i32 { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 148 | match self { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 149 | Self::Rng(rng) => rng.random_range(lo..=hi), |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 150 | Self::Clm { clm, .. } => { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 151 | let values: Vec<String> = (lo..=hi).map(|v| v.to_string()).collect(); |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 152 | let options: Vec<&str> = values.iter().map(String::as_str).collect(); |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 153 | let ask = format!("{}. Answer with a whole number.", roll.ask); |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 154 | let i = clm.choose(&roll.state, roll.site, &ask, &options); |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 155 | lo + i as i32 |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 156 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 157 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 158 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 159 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 160 | |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 161 | impl Clm { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 162 | fn new(url: &str) -> Self { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 163 | Self { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 164 | url: url.trim_end_matches('/').to_owned(), |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 165 | agent: ureq::AgentBuilder::new() |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 166 | .timeout(std::time::Duration::from_secs(60)) |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 167 | .build(), |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 168 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 169 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 170 | |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 171 | /// POST /v1/systemone with a single question; panics on any failure, which |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 172 | /// surfaces as a 500 through the catch_panic layer. A dead oracle must not |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 173 | /// quietly fall back to the rng. |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 174 | fn ask(&self, state: &str, site: &str, question: Value) -> Value { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 175 | let body = json!({ |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 176 | "state": state, |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 177 | "model": "clm-latest", |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 178 | "questions": { site: question }, |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 179 | "temperature": 1.0, |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 180 | }); |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 181 | let response = self |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 182 | .agent |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 183 | .post(&self.url) |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 184 | .send_json(&body) |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 185 | .unwrap_or_else(|e| panic!("oracle unreachable at {}: {e}", self.url)); |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 186 | let answer: Value = response |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 187 | .into_json() |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 188 | .unwrap_or_else(|e| panic!("oracle answer unreadable: {e}")); |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 189 | answer["answers"][site].clone() |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 190 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 191 | |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 192 | fn decide(&self, state: &str, site: &str, ask: &str) -> f64 { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 193 | self.ask(state, site, json!({ "type": "noul", "instructions": ask }))["noul"] |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 194 | .as_f64() |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 195 | .unwrap_or_else(|| panic!("oracle noul answer for {site} missing")) |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 196 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 197 | |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 198 | fn choose(&self, state: &str, site: &str, ask: &str, options: &[&str]) -> usize { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 199 | let criteria: serde_json::Map<String, Value> = options |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 200 | .iter() |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 201 | .enumerate() |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 202 | .map(|(i, text)| (i.to_string(), json!(text))) |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 203 | .collect(); |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 204 | let q = if ask.trim_end().ends_with('?') { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 205 | "" |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 206 | } else { |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 207 | "?" |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 208 | }; |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 209 | let instructions = format!("{ask}{q}"); |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 210 | let answer = self.ask( |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 211 | state, |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 212 | site, |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 213 | json!({ "type": "choice", "instructions": instructions, "criteria": criteria }), |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 214 | ); |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 215 | let choice = answer["choice"] |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 216 | .as_str() |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 217 | .unwrap_or_else(|| panic!("oracle choice answer for {site} missing")); |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 218 | choice |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 219 | .parse::<usize>() |
e807266c323b | atagen | 2026-10-12 09:46 UTC | 220 | .ok() |
e807266c323b | atagen | 2026-10-12 09:46 UTC | 221 | .filter(|&i| i < options.len()) |
e807266c323b | atagen | 2026-10-12 09:46 UTC | 222 | .unwrap_or_else(|| panic!("oracle chose {choice:?} for {site}, not an option index")) |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 223 | } |
3f9091557ec3 | atagen | 2026-10-11 11:43 UTC | 224 | } |