melvin_ob/objective/
beacon_objective_done.rs

1use super::BeaconObjective;
2use crate::util::Vec2D;
3use crate::http_handler::{
4    http_client::HTTPClient,
5    http_request::{
6        beacon_position_put::BeaconPositionRequest, request_common::NoBodyHTTPRequestType,
7    },
8};
9use crate::{error, obj};
10use chrono::{DateTime, Utc};
11use fixed::types::I32F32;
12use rand::Rng;
13use std::io::{Error, ErrorKind};
14use std::sync::Arc;
15
16/// Represents a completed beacon objective.
17///
18/// Stores relevant details such as the objective's ID, name, start and end time,
19/// and additional metadata like guesses and submission status.
20#[derive(Clone)]
21pub struct BeaconObjectiveDone {
22    /// The unique identifier of the objective.
23    id: usize,
24    /// The name of the objective.
25    name: String,
26    /// The start time of the objective.
27    start: DateTime<Utc>,
28    /// The end time of the objective.
29    end: DateTime<Utc>,
30    /// A collection of guesses made for the beacon position.
31    guesses: Vec<Vec2D<I32F32>>,
32    /// Status indicating whether the guesses have been submitted.
33    submitted: bool,
34}
35
36impl BeaconObjectiveDone {
37    /// The allowed range for map width values.
38    const MAP_WIDTH_RANGE: std::ops::Range<u32> = 0..21600;
39    /// The allowed range for map height values.
40    const MAP_HEIGHT_RANGE: std::ops::Range<u32> = 0..10800;
41    /// The minimum allowable distance between random guesses.
42    const MIN_DISTANCE_RAND_GUESSES: f32 = 75.0;
43
44    /// Returns the ID of the beacon objective.
45    pub fn id(&self) -> usize { self.id }
46    /// Returns the name of the beacon objective.
47    pub fn name(&self) -> &str { &self.name }
48    /// Returns the start time of the beacon objective.
49    pub fn start(&self) -> DateTime<Utc> { self.start }
50    /// Returns the end time of the beacon objective.
51    pub fn end(&self) -> DateTime<Utc> { self.end }
52    /// Returns a reference to the guesses for the beacon objective.
53    pub fn guesses(&self) -> &Vec<Vec2D<I32F32>> { &self.guesses }
54    /// Returns whether the guesses have been submitted.
55    pub fn submitted(&self) -> bool { self.submitted }
56    /// Sets the submission status of the guesses to true.
57    pub fn set_submitted(&mut self) { self.submitted = true }
58
59    /// Sends all guesses for the beacon to the DRS.
60    ///
61    /// # Arguments
62    ///
63    /// * `client` - HTTP client used to send requests.
64    #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
65    pub async fn guess_max(&self, client: Arc<HTTPClient>) {
66        obj!(
67            "Guessing max for {}: {} guesses...",
68            self.id,
69            self.guesses.len()
70        );
71        let id_u16 = self.id() as u16;
72        let guess_cloned = self.guesses().clone();
73        for (i, guess) in guess_cloned.iter().enumerate() {
74            let width = guess.x().abs().to_num::<u32>();
75            let height = guess.y().abs().to_num::<u32>();
76            let req = BeaconPositionRequest { beacon_id: id_u16, width, height };
77            obj!("Sending request for beacon {id_u16} with width {width} and height {height}...");
78            if self.submit_guess(req, client.clone(), guess, i).await.is_err() {
79                return;
80            };
81        }
82    }
83
84    /// Randomizes guesses for the beacon if none are provided and submits them.
85    ///
86    /// # Arguments
87    ///
88    /// * `client` - HTTP client used to send requests.
89    #[allow(clippy::cast_possible_truncation)]
90    pub async fn randomize_no_meas_guesses(&self, client: Arc<HTTPClient>) {
91        if !self.guesses.is_empty() {
92            obj!("Guesses are provided already, skipping randomization.");
93            return self.guess_max(client).await;
94        }
95        obj!("No guesses for {}, randomizing 10 guesses.", self.id);
96
97        let random_guesses = Self::generate_random_guesses();
98        for (i, guess) in random_guesses.iter().enumerate() {
99            let guess_req = BeaconPositionRequest {
100                beacon_id: self.id as u16,
101                width: guess.x().abs().to_num::<u32>(),
102                height: guess.y().abs().to_num::<u32>(),
103            };
104            let res = self.submit_guess(guess_req, Arc::clone(&client), guess, i).await;
105            match res {
106                Ok(done) => {
107                    if done.is_some() { return; };
108                }
109                Err(_) => return,
110            }
111        }
112    }
113
114    /// Submits a single guess to the server.
115    ///
116    /// # Arguments
117    ///
118    /// * `req` - The request containing the guess information.
119    /// * `client` - HTTP client used to send the request.
120    /// * `guess` - The guessed position.
121    /// * `guess_num` - The number of the guess to provide contextual information.
122    ///
123    /// # Returns
124    ///
125    /// * `Ok(Some(()))` if the guess is successful.
126    /// * `Ok(None)` if the guess fails but the process is not over.
127    /// * `Err` if an error occurs or all attempts are exhausted.
128    async fn submit_guess(
129        &self,
130        req: BeaconPositionRequest,
131        client: Arc<HTTPClient>,
132        guess: &Vec2D<I32F32>,
133        guess_num: usize,
134    ) -> Result<Option<()>, Error> {
135        if let Ok(msg) = req.send_request(&client).await {
136            if msg.is_success() {
137                obj!(
138                    "And Rohan will answer! Mustered Rohirrim {} at {}!",
139                    req.beacon_id,
140                    guess
141                );
142                return Ok(Some(()));
143            } else if msg.is_fail() {
144                obj!(
145                    "What can men do against such reckless hate? Still searching beacon {} after {} tries!",
146                    req.beacon_id,
147                    guess_num
148                );
149                return Ok(None);
150            } else if msg.is_last() {
151                obj!(
152                    "Where was Gondor when the Westfold fell! Could not find beacon {} after {} tries!",
153                    req.beacon_id,
154                    guess_num
155                );
156                return Err(Error::new(ErrorKind::Other, "Beacon over!"));
157            } else if msg.is_unknown() {
158                obj!("Beacon {} is unknown!", req.beacon_id);
159                return Err(Error::new(ErrorKind::Other, "Beacon unknown!"));
160            }
161            obj!("Unknown Message: {}! Returning!", msg.msg());
162            return Err(Error::new(ErrorKind::Other, "Unknown Message!"));
163        }
164        error!("Unnoticed HTTP Error in submit_guess()");
165        Err(Error::new(ErrorKind::Other, "HTTP Error!"))
166    }
167
168    /// Generates a vector of random guesses, ensuring each guess
169    /// is sufficiently spaced apart from the others.
170    ///
171    /// # Returns
172    ///
173    /// A vector of random beacon position guesses.
174    fn generate_random_guesses() -> Vec<Vec2D<I32F32>> {
175        let mut rng = rand::rng();
176        let mut random_guesses = Vec::new();
177        while random_guesses.len() <= 10 {
178            let random_width = rng.random_range(Self::MAP_WIDTH_RANGE);
179            let random_height = rng.random_range(Self::MAP_HEIGHT_RANGE);
180            let rand_guess = Vec2D::new(
181                I32F32::from_num(random_width),
182                I32F32::from_num(random_height),
183            );
184
185            let too_close = random_guesses.iter().any(|prev_guesses: &Vec2D<I32F32>| {
186                prev_guesses.euclid_distance(&rand_guess)
187                    <= I32F32::from_num(Self::MIN_DISTANCE_RAND_GUESSES)
188            });
189
190            if too_close {
191                continue;
192            }
193
194            random_guesses.push(rand_guess);
195        }
196        random_guesses
197    }
198}
199
200impl From<BeaconObjective> for BeaconObjectiveDone {
201    /// Converts a `BeaconObjective` into a `BeaconObjectiveDone`.
202    ///
203    /// # Arguments
204    ///
205    /// * `obj` - The original objective to be converted.
206    fn from(obj: BeaconObjective) -> Self {
207        let guesses =
208            if let Some(meas) = obj.measurements() { meas.pack_perfect_circles() } else { vec![] };
209        Self {
210            id: obj.id(),
211            name: String::from(obj.name()),
212            start: obj.start(),
213            end: obj.end(),
214            guesses,
215            submitted: false,
216        }
217    }
218}