common_game/components/
planet.rs

1//! # Planet module
2//! This module provides common definitions for planets and their associated types
3//! that will be used by a group to construct its own planet.
4//! The [Planet] struct is the **main component**: an instance of it represents the
5//! actual planet and contains all the logic and state (see [`PlanetState`]) needed to work as one, in fact
6//! this is what the orchestrator will interact with.
7//!
8//! You can instantiate a new planet by calling the [`Planet::new`] constructor method and passing
9//! valid construction parameters to it (look into its documentation to learn more).
10//!
11//! One of the construction parameters is a planet is a group-defined struct that implements the [`PlanetAI`] trait,
12//! which defines several methods for handling messages coming from the orchestrator and the explorers. This is
13//! the core of each group's planet implementation, as it defines the planet *behavior*, that is
14//! how a planet "reacts" to the possible events or requests.
15//!
16//! ## Examples
17//! Intended usage (for planet definition, by groups):
18//!
19//! ```
20//! use crossbeam_channel::{Sender, Receiver};
21//! use common_game::components::planet::{Planet, PlanetAI, PlanetState, PlanetType, DummyPlanetState};
22//! use common_game::components::resource::{Combinator, Generator};
23//! use common_game::components::rocket::Rocket;
24//! use common_game::components::sunray::Sunray;
25//! use common_game::protocols::planet_explorer;
26//! use common_game::protocols::orchestrator_planet;
27//! use common_game::protocols::planet_explorer::ExplorerToPlanet;
28//! // Group-defined AI struct
29//! struct AI { /* your AI state here */ };
30//!
31//! impl PlanetAI for AI {
32//!     fn handle_sunray(
33//!         &mut self,
34//!         state: &mut PlanetState,
35//!         generator: &Generator,
36//!         combinator: &Combinator,
37//!         sunray: Sunray
38//!     ) {
39//!         // your handler code here...
40//!     }
41//!
42//!     fn handle_internal_state_req(
43//!         &mut self,
44//!         state: &mut PlanetState,
45//!         generator: &Generator,
46//!         combinator: &Combinator
47//!     ) -> DummyPlanetState {
48//!         // your handler code here...
49//!         state.to_dummy()
50//!     }
51//!
52//!     fn handle_explorer_msg(
53//!         &mut self,
54//!         state: &mut PlanetState,
55//!         generator: &Generator,
56//!         combinator: &Combinator,
57//!         msg: ExplorerToPlanet
58//!     ) -> Option<planet_explorer::PlanetToExplorer> {
59//!         // your handler code here...
60//!         None
61//!     }
62//!
63//!     fn handle_asteroid(
64//!         &mut self,
65//!         state: &mut PlanetState,
66//!         generator: &Generator,
67//!         combinator: &Combinator,
68//!     ) -> Option<Rocket> {
69//!         // your handler code here...
70//!         None
71//!     }
72//! }
73//!
74//! // This is the group's "export" function. It will be called by
75//! // the orchestrator to spawn your planet.
76//! pub fn create_planet(
77//!     id: u32,
78//!     rx_orchestrator: Receiver<orchestrator_planet::OrchestratorToPlanet>,
79//!     tx_orchestrator: Sender<orchestrator_planet::PlanetToOrchestrator>,
80//!     rx_explorer: Receiver<planet_explorer::ExplorerToPlanet>,
81//! ) -> Planet {
82//!     let ai = AI {};
83//!     let gen_rules = vec![/* your recipes */];
84//!     let comb_rules = vec![/* your recipes */];
85//!
86//!     // Construct the planet and return it
87//!     Planet::new(
88//!         id,
89//!         PlanetType::A,
90//!         Box::new(ai),
91//!         gen_rules,
92//!         comb_rules,
93//!         (rx_orchestrator, tx_orchestrator),
94//!         rx_explorer,
95//!     ).unwrap() // Don't call .unwrap()! You should do error checking instead.
96//! }
97//! ```
98
99use crate::components::energy_cell::EnergyCell;
100use crate::components::resource::{BasicResourceType, Combinator, ComplexResourceType, Generator};
101use crate::components::rocket::Rocket;
102use crate::components::sunray::Sunray;
103use crate::protocols::orchestrator_planet::{OrchestratorToPlanet, PlanetToOrchestrator};
104use crate::protocols::planet_explorer::{ExplorerToPlanet, PlanetToExplorer};
105use crate::utils::ID;
106use crossbeam_channel::{Receiver, Sender, select_biased};
107use std::collections::HashMap;
108use std::slice::{Iter, IterMut};
109
110/// The trait that defines the **behavior** of a planet, meaning how it reacts
111/// to messages coming from the orchestrator and explorers. This is done through trait methods
112/// acting as *handlers* for the messages.
113///
114/// Structs implementing this trait are intended to be passed to the
115/// [Planet] constructor, so that the handlers (methods of the trait) can be invoked by the planet
116/// default logic when certain messages are received on the planet channels.
117///
118/// The handlers can alter the planet state by accessing the
119/// `state` parameter, which is passed to the methods as a mutable borrow.
120/// The [Generator] and [Combinator] of the planet are also passed as parameters.
121pub trait PlanetAI: Send {
122    /// This handler will be invoked when a [`OrchestratorToPlanet::Sunray`]
123    /// message is received. The `sunray` parameter is the actual [Sunray] struct
124    /// used to charged energy cells.
125    fn handle_sunray(
126        &mut self,
127        state: &mut PlanetState,
128        generator: &Generator,
129        combinator: &Combinator,
130        sunray: Sunray,
131    );
132
133    /// This handler will be invoked when a [`OrchestratorToPlanet::Asteroid`]
134    /// message is received. It's important to handle *Asteroid* messages
135    /// correctly, as this will the determine the planet survival.
136    ///
137    /// # Returns
138    /// In order to survive, an owned [Rocket] **must** be returned from this method;
139    /// if `None` is returned instead, the planet will (or *should*) be **destroyed** by the orchestrator
140    fn handle_asteroid(
141        &mut self,
142        state: &mut PlanetState,
143        generator: &Generator,
144        combinator: &Combinator,
145    ) -> Option<Rocket>;
146
147    /// This handler will be invoked when a [`OrchestratorToPlanet::InternalStateRequest`]
148    /// message is received.
149    ///
150    /// # Returns
151    /// A [`DummyPlanetState`] instance that *should* represent
152    /// the current state of the planet.
153    fn handle_internal_state_req(
154        &mut self,
155        state: &mut PlanetState,
156        generator: &Generator,
157        combinator: &Combinator,
158    ) -> DummyPlanetState;
159
160    /// Handler for **all** messages received by explorers (receiving
161    /// end of the [`ExplorerToPlanet`] channel). The id of the sender explorer
162    /// is part of the `msg` struct.
163    ///
164    /// # Returns
165    /// This method can return an optional response to the message, which will
166    /// be delivered to the explorer that sent the message.
167    fn handle_explorer_msg(
168        &mut self,
169        state: &mut PlanetState,
170        generator: &Generator,
171        combinator: &Combinator,
172        msg: ExplorerToPlanet,
173    ) -> Option<PlanetToExplorer>;
174
175    /// This method will be invoked when an explorer (identified by the `explorer_id`
176    /// parameter) lands on the planet.
177    #[allow(unused_variables)]
178    fn on_explorer_arrival(
179        &mut self,
180        state: &mut PlanetState,
181        generator: &Generator,
182        combinator: &Combinator,
183        explorer_id: ID,
184    ) {
185    }
186
187    /// This method will be invoked when an explorer (identified by the `explorer_id`
188    /// parameter) leaves the planet.
189    #[allow(unused_variables)]
190    fn on_explorer_departure(
191        &mut self,
192        state: &mut PlanetState,
193        generator: &Generator,
194        combinator: &Combinator,
195        explorer_id: ID,
196    ) {
197    }
198
199    /// This method will be invoked when a [`OrchestratorToPlanet::StartPlanetAI`]
200    /// is received, but **only if** the planet is currently in a *stopped* state.
201    ///
202    /// Start messages received when planet is already running are **ignored**.
203    #[allow(unused_variables)]
204    fn on_start(&mut self, state: &PlanetState, generator: &Generator, combinator: &Combinator) {}
205
206    /// This method will be invoked when a [`OrchestratorToPlanet::StopPlanetAI`]
207    /// is received, but **only if** the planet is currently in a *running* state.
208    ///
209    /// Stop messages received when planet is already stopped are **ignored**.
210    #[allow(unused_variables)]
211    fn on_stop(&mut self, state: &PlanetState, generator: &Generator, combinator: &Combinator) {}
212}
213
214/// Contains planet rules constraints (see [`PlanetType`]).
215pub struct PlanetConstraints {
216    n_energy_cells: usize,
217    unbounded_gen_rules: bool,
218    can_have_rocket: bool,
219    n_comb_rules: usize,
220}
221
222/// Planet types definitions, intended to be passed
223/// to the planet constructor. Identifies the planet rules constraints,
224/// with each type having its own rules.
225#[derive(Debug, Clone, Copy)]
226pub enum PlanetType {
227    A,
228    B,
229    C,
230    D,
231}
232
233impl PlanetType {
234    const N_ENERGY_CELLS: usize = 5;
235    const N_RESOURCE_COMB_RULES: usize = 6;
236
237    /// Returns the constraints associated to the planet type,
238    /// as described in the project specifications.
239    #[must_use]
240    pub fn constraints(&self) -> PlanetConstraints {
241        match self {
242            PlanetType::A => PlanetConstraints {
243                n_energy_cells: Self::N_ENERGY_CELLS,
244                unbounded_gen_rules: false,
245                can_have_rocket: true,
246                n_comb_rules: 0,
247            },
248            PlanetType::B => PlanetConstraints {
249                n_energy_cells: 1,
250                unbounded_gen_rules: true,
251                can_have_rocket: false,
252                n_comb_rules: 1,
253            },
254            PlanetType::C => PlanetConstraints {
255                n_energy_cells: 1,
256                unbounded_gen_rules: false,
257                can_have_rocket: true,
258                n_comb_rules: Self::N_RESOURCE_COMB_RULES,
259            },
260            PlanetType::D => PlanetConstraints {
261                n_energy_cells: Self::N_ENERGY_CELLS,
262                unbounded_gen_rules: true,
263                can_have_rocket: false,
264                n_comb_rules: 0,
265            },
266        }
267    }
268}
269
270/// This struct is a representation of the internal state
271/// of the planet. Through its public methods, it gives access to the
272/// energy cells and rocket construction of the planet.
273pub struct PlanetState {
274    id: ID,
275    energy_cells: Vec<EnergyCell>,
276    rocket: Option<Rocket>,
277    can_have_rocket: bool,
278}
279
280impl PlanetState {
281    /// Returns the planet id.
282    #[must_use]
283    pub fn id(&self) -> ID {
284        self.id
285    }
286
287    /// Indexed getter accessor for the [`EnergyCell`] vec.
288    ///
289    /// # Returns
290    /// An immutable borrow of the *i-th* energy cell.
291    ///
292    /// # Panics
293    /// This method will panic if the index `i` is out of bounds.
294    /// Always check the number of energy cells available with [`PlanetState::cells_count`].
295    #[must_use]
296    pub fn cell(&self, i: usize) -> &EnergyCell {
297        &self.energy_cells[i]
298    }
299
300    /// Indexed *mutable* getter accessor for the [`EnergyCell`] vec.
301    ///
302    /// # Returns
303    /// An mutable borrow of the *i-th* energy cell.
304    ///
305    /// # Panics
306    /// This method will panic if the index `i` is out of bounds.
307    /// Always check the number of energy cells available with [`PlanetState::cells_count`].
308    pub fn cell_mut(&mut self, i: usize) -> &mut EnergyCell {
309        &mut self.energy_cells[i]
310    }
311
312    /// Returns the number of energy cells owned by
313    /// the planet. This is the actual size of the internal
314    /// vec containing the cells.
315    #[must_use]
316    pub fn cells_count(&self) -> usize {
317        self.energy_cells.len()
318    }
319
320    /// Returns an *immutable* iterator over the energy cells owned by the planet.
321    pub fn cells_iter(&self) -> Iter<'_, EnergyCell> {
322        self.energy_cells.iter()
323    }
324
325    /// Returns a *mutable* iterator over the energy cells owned by the planet.
326    pub fn cells_iter_mut(&mut self) -> IterMut<'_, EnergyCell> {
327        self.energy_cells.iter_mut()
328    }
329
330    /// Charges the first empty (discharged) cell.
331    /// Returns an optional [Sunray] if there's no cell to charge.
332    pub fn charge_cell(&mut self, sunray: Sunray) -> Option<Sunray> {
333        match self.empty_cell() {
334            None => Some(sunray),
335            Some((cell, _)) => {
336                cell.charge(sunray);
337                None
338            }
339        }
340    }
341
342    /// Returns a tuple containing a *mutable* borrow of the first empty (discharged) cell
343    /// and its index, or `None` if there isn't any.
344    pub fn empty_cell(&mut self) -> Option<(&mut EnergyCell, usize)> {
345        let idx = self.energy_cells.iter().position(|cell| !cell.is_charged());
346        idx.map(|i| (&mut self.energy_cells[i], i))
347    }
348
349    /// Returns a tuple containing a *mutable* borrow of the first full (charged) cell
350    /// and its index, or `None` if there isn't any.
351    pub fn full_cell(&mut self) -> Option<(&mut EnergyCell, usize)> {
352        let idx = self
353            .energy_cells
354            .iter()
355            .position(super::energy_cell::EnergyCell::is_charged);
356        idx.map(|i| (&mut self.energy_cells[i], i))
357    }
358
359    /// Returns `true` if the planet can have a rocket.
360    #[must_use]
361    pub fn can_have_rocket(&self) -> bool {
362        self.can_have_rocket
363    }
364
365    /// Returns `true` if the planet has a rocket built and ready to launch.
366    #[must_use]
367    pub fn has_rocket(&self) -> bool {
368        self.rocket.is_some()
369    }
370
371    /// Takes the rocket out of the planet state (if there is one), leaving
372    /// `None` in its place.
373    pub fn take_rocket(&mut self) -> Option<Rocket> {
374        self.rocket.take()
375    }
376
377    /// Constructs a rocket using the *i-th* [`EnergyCell`] of the planet and stores it
378    /// inside the planet, taking ownership of it.
379    ///
380    /// # Panics
381    /// This method will panic if the index `i` is out of bounds.
382    /// Always check the number of energy cells available with [`PlanetState::cells_count`].
383    ///
384    /// # Errors
385    /// Returns an error if:
386    /// - The planet type prohibits the storing of rockets.
387    /// - The planet already has a rocket built.
388    /// - The energy cell is not charged
389    pub fn build_rocket(&mut self, i: usize) -> Result<(), String> {
390        if !self.can_have_rocket {
391            Err("This planet type can't have rockets.".to_string())
392        } else if self.has_rocket() {
393            Err("This planet already has a rocket.".to_string())
394        } else {
395            let energy_cell = self.cell_mut(i);
396            Rocket::new(energy_cell).map(|rocket| {
397                self.rocket = Some(rocket);
398            })
399        }
400    }
401
402    /// Returns a *dummy* clone of this state.
403    #[must_use]
404    pub fn to_dummy(&self) -> DummyPlanetState {
405        DummyPlanetState {
406            energy_cells: self
407                .energy_cells
408                .iter()
409                .map(super::energy_cell::EnergyCell::is_charged)
410                .collect(),
411            charged_cells_count: self
412                .energy_cells
413                .iter()
414                .filter(|cell| cell.is_charged())
415                .count(),
416            has_rocket: self.has_rocket(),
417        }
418    }
419}
420
421/// This is a dummy struct containing an overview of the internal state of a planet.
422/// Use [`PlanetState::to_dummy`] to construct one.
423///
424/// Used in [`PlanetToOrchestrator::InternalStateResponse`].
425#[derive(Debug, Clone)]
426pub struct DummyPlanetState {
427    pub energy_cells: Vec<bool>,
428    pub charged_cells_count: usize,
429    pub has_rocket: bool,
430}
431
432/// Main, top-level planet definition. This type is built on top of
433/// [`PlanetState`], [`PlanetType`] and [`PlanetAI`], through composition.
434///
435/// It needs to be constructed by each group as it represents the actual planet
436/// and contains the base logic that runs the AI. Also, this is what should be
437/// returned to the orchestrator.
438///
439/// See module-level docs for more general info.
440pub struct Planet {
441    state: PlanetState,
442    type_: PlanetType,
443    pub ai: Box<dyn PlanetAI>,
444    generator: Generator,
445    combinator: Combinator,
446
447    from_orchestrator: Receiver<OrchestratorToPlanet>,
448    to_orchestrator: Sender<PlanetToOrchestrator>,
449    from_explorers: Receiver<ExplorerToPlanet>,
450    to_explorers: HashMap<ID, Sender<PlanetToExplorer>>,
451}
452
453impl Planet {
454    const ORCH_DISCONNECT_ERR: &'static str = "Orchestrator disconnected.";
455
456    /// Constructor for the [Planet] type.
457    ///
458    /// # Errors
459    /// Returns an error if the construction parameters are *invalid* (they violate the `planet_type` constraints).
460    ///
461    /// # Arguments
462    /// - `id` - The identifier to assign to the planet.
463    /// - `planet_type` - Type of the planet. Constraints the rules of the planet.
464    /// - `ai` - A group-defined struct implementing the [`PlanetAI`] trait.
465    /// - `gen_rules` - A vec of [`BasicResourceType`] containing the basic resources the planet will be able to generate.
466    /// - `comb_rules` - A vec of [`ComplexResourceType`] containing the complex resources the planet will be able to make.
467    /// - `orchestrator_channels` - A pair containing the receiver and sender half
468    ///   of the channels [`OrchestratorToPlanet`] and [`PlanetToOrchestrator`].
469    /// - `explorers_receiver` - The receiver half of the [`ExplorerToPlanet`] channel
470    ///   where all explorers send messages to this planet (when they're visiting it).
471    pub fn new(
472        id: ID,
473        type_: PlanetType,
474        ai: Box<dyn PlanetAI>,
475        gen_rules: Vec<BasicResourceType>,
476        comb_rules: Vec<ComplexResourceType>,
477        orchestrator_channels: (Receiver<OrchestratorToPlanet>, Sender<PlanetToOrchestrator>),
478        explorers_receiver: Receiver<ExplorerToPlanet>,
479    ) -> Result<Planet, String> {
480        let PlanetConstraints {
481            n_energy_cells,
482            unbounded_gen_rules,
483            can_have_rocket,
484            n_comb_rules,
485        } = type_.constraints();
486        let (from_orchestrator, to_orchestrator) = orchestrator_channels;
487
488        if gen_rules.is_empty() {
489            Err("gen_rules is empty".to_string())
490        } else if !unbounded_gen_rules && gen_rules.len() > 1 {
491            Err(format!(
492                "Too many generation rules (Planet type {type_:?} is limited to 1)"
493            ))
494        } else if comb_rules.len() > n_comb_rules {
495            Err(format!(
496                "Too many combination rules (Planet type {type_:?} is limited to {n_comb_rules})"
497            ))
498        } else {
499            let mut generator = Generator::new();
500            let mut combinator = Combinator::new();
501
502            // add gen and comb rules to the planet generator and combinator
503            for r in gen_rules {
504                let _ = generator.add(r);
505            }
506            for r in comb_rules {
507                let _ = combinator.add(r);
508            }
509
510            Ok(Planet {
511                state: PlanetState {
512                    id,
513                    energy_cells: (0..n_energy_cells).map(|_| EnergyCell::new()).collect(),
514                    can_have_rocket,
515                    rocket: None,
516                },
517                type_,
518                ai,
519                generator,
520                combinator,
521                from_orchestrator,
522                to_orchestrator,
523                from_explorers: explorers_receiver,
524                to_explorers: HashMap::new(),
525            })
526        }
527    }
528
529    // Extracted helper to reduce the size of `run` and keep Clippy happy.
530    // Returns `Ok(Some(true))` when the planet should exit (killed),
531    // `Ok(None)` to continue running, or `Err` on channel errors.
532    fn handle_orchestrator_msg(
533        &mut self,
534        msg: OrchestratorToPlanet,
535    ) -> Result<Option<bool>, String> {
536        match msg {
537            OrchestratorToPlanet::StartPlanetAI => Ok(None),
538
539            OrchestratorToPlanet::StopPlanetAI => {
540                self.to_orchestrator
541                    .send(PlanetToOrchestrator::StopPlanetAIResult {
542                        planet_id: self.id(),
543                    })
544                    .map_err(|_| Self::ORCH_DISCONNECT_ERR.to_string())?;
545
546                self.ai
547                    .on_stop(&self.state, &self.generator, &self.combinator);
548
549                let kill = self.wait_for_start()?; // blocking wait
550                if kill {
551                    return Ok(Some(true));
552                }
553
554                // restart AI
555                self.ai
556                    .on_start(&self.state, &self.generator, &self.combinator);
557                Ok(None)
558            }
559
560            OrchestratorToPlanet::KillPlanet => {
561                self.to_orchestrator
562                    .send(PlanetToOrchestrator::KillPlanetResult {
563                        planet_id: self.id(),
564                    })
565                    .map_err(|_| Self::ORCH_DISCONNECT_ERR.to_string())?;
566
567                Ok(Some(true))
568            }
569
570            OrchestratorToPlanet::Sunray(sunray) => {
571                self.ai
572                    .handle_sunray(&mut self.state, &self.generator, &self.combinator, sunray);
573
574                self.to_orchestrator
575                    .send(PlanetToOrchestrator::SunrayAck {
576                        planet_id: self.id(),
577                    })
578                    .map_err(|_| Self::ORCH_DISCONNECT_ERR.to_string())?;
579
580                Ok(None)
581            }
582
583            OrchestratorToPlanet::Asteroid(_) => {
584                let rocket =
585                    self.ai
586                        .handle_asteroid(&mut self.state, &self.generator, &self.combinator);
587
588                self.to_orchestrator
589                    .send(PlanetToOrchestrator::AsteroidAck {
590                        planet_id: self.id(),
591                        rocket,
592                    })
593                    .map_err(|_| Self::ORCH_DISCONNECT_ERR.to_string())?;
594
595                Ok(None)
596            }
597
598            OrchestratorToPlanet::IncomingExplorerRequest {
599                explorer_id,
600                new_sender,
601            } => {
602                self.to_explorers.insert(explorer_id, new_sender);
603                self.ai.on_explorer_arrival(
604                    &mut self.state,
605                    &self.generator,
606                    &self.combinator,
607                    explorer_id,
608                );
609
610                self.to_orchestrator
611                    .send(PlanetToOrchestrator::IncomingExplorerResponse {
612                        planet_id: self.id(),
613                        explorer_id,
614                        res: Ok(()),
615                    })
616                    .map_err(|_| Self::ORCH_DISCONNECT_ERR.to_string())?;
617
618                Ok(None)
619            }
620
621            OrchestratorToPlanet::OutgoingExplorerRequest { explorer_id } => {
622                self.to_explorers.remove(&explorer_id);
623                self.ai.on_explorer_departure(
624                    &mut self.state,
625                    &self.generator,
626                    &self.combinator,
627                    explorer_id,
628                );
629
630                self.to_orchestrator
631                    .send(PlanetToOrchestrator::OutgoingExplorerResponse {
632                        planet_id: self.id(),
633                        explorer_id,
634                        res: Ok(()),
635                    })
636                    .map_err(|_| Self::ORCH_DISCONNECT_ERR.to_string())?;
637
638                Ok(None)
639            }
640
641            OrchestratorToPlanet::InternalStateRequest => {
642                let dummy_state = self.ai.handle_internal_state_req(
643                    &mut self.state,
644                    &self.generator,
645                    &self.combinator,
646                );
647
648                self.to_orchestrator
649                    .send(PlanetToOrchestrator::InternalStateResponse {
650                        planet_id: self.id(),
651                        planet_state: dummy_state,
652                    })
653                    .map_err(|_| Self::ORCH_DISCONNECT_ERR.to_string())?;
654
655                Ok(None)
656            }
657        }
658    }
659
660    /// Starts the planet in a *stopped* state, waiting for a [`OrchestratorToPlanet::StartPlanetAI`] message,
661    /// then invokes [`PlanetAI::on_start`] and runs the main message polling loop.
662    /// See [`PlanetAI`] docs to know more about when message handlers are invoked and how the planet reacts
663    /// to the different messages.
664    ///
665    /// This method is *blocking* and should be called by the orchestrator in a separate thread.
666    /// It returns with an empty [Ok] when the planet has been **killed** (destroyed).
667    ///
668    /// # Errors
669    /// If the orchestrator disconnects from the channel, this will return an [Err].
670    pub fn run(&mut self) -> Result<(), String> {
671        // run the planet stopped by default
672        // and wait for a StartPlanetAI message
673        let kill = self.wait_for_start()?;
674        if kill {
675            return Ok(());
676        }
677
678        self.ai
679            .on_start(&self.state, &self.generator, &self.combinator);
680
681        loop {
682            select_biased! {
683                // wait for orchestrator message (prioritized operation)
684                recv(self.from_orchestrator) -> msg => match msg {
685                    Ok(m) => {
686                        if let Some(true) = self.handle_orchestrator_msg(m)? {
687                            return Ok(());
688                        }
689                    }
690
691                    Err(_) => {
692                        return Err(Self::ORCH_DISCONNECT_ERR.to_string())
693                    }
694                },
695
696                // wait for explorer message (ignore disconnections)
697                recv(self.from_explorers) -> msg => if let Ok(msg) = msg {
698                    let explorer_id = msg.explorer_id();
699
700                    // if requesting explorer is currently
701                    // on the planet respond to it
702                    if let Some(to_explorer) = self.to_explorers.get(&explorer_id)
703                        && let Some(response) = self.ai.handle_explorer_msg(
704                            &mut self.state,
705                            &self.generator,
706                            &self.combinator,
707                            msg,
708                        )
709                    {
710                        to_explorer
711                            .send(response)
712                            .map_err(|_| format!("Explorer {explorer_id} disconnected."))?;
713                    }
714                }
715            }
716        }
717    }
718
719    // private helper function that blocks until
720    // a StartPlanetAI message is received
721    fn wait_for_start(&self) -> Result<bool, String> {
722        loop {
723            select_biased! {
724                // orch messages
725                recv(self.from_orchestrator) -> msg => match msg {
726                    // if `Start` is received, return false
727                    Ok(OrchestratorToPlanet::StartPlanetAI) => {
728                        self.to_orchestrator
729                            .send(PlanetToOrchestrator::StartPlanetAIResult {
730                                planet_id: self.id(),
731                            })
732                            .map_err(|_| Self::ORCH_DISCONNECT_ERR.to_string())?;
733
734                        return Ok(false);
735                    }
736                    // if `Kill` is received, return true
737                    Ok(OrchestratorToPlanet::KillPlanet) => {
738                        self.to_orchestrator
739                            .send(PlanetToOrchestrator::KillPlanetResult { planet_id: self.id() })
740                            .map_err(|_| Self::ORCH_DISCONNECT_ERR.to_string())?;
741
742                        return Ok(true)
743                    }
744                    // every other message we respond with `Stopped`
745                    Ok(_) => {
746                        self.to_orchestrator
747                            .send(PlanetToOrchestrator::Stopped {
748                                planet_id: self.id(),
749                            })
750                            .map_err(|_| Self::ORCH_DISCONNECT_ERR.to_string())?;
751                    }
752
753                    Err(_) => return Err(Self::ORCH_DISCONNECT_ERR.to_string()),
754                },
755
756                // explorers messages
757                recv(self.from_explorers) -> msg => if let Ok(msg) = msg &&
758                    let Some(to_explorer) = self.to_explorers.get(&msg.explorer_id())
759                {
760                    let _ = to_explorer.send(PlanetToExplorer::Stopped);
761                }
762            }
763        }
764    }
765
766    /// Returns the planet id.
767    #[must_use]
768    pub fn id(&self) -> ID {
769        self.state.id
770    }
771
772    /// Returns the planet type.
773    #[must_use]
774    pub fn planet_type(&self) -> PlanetType {
775        self.type_
776    }
777
778    /// Returns an immutable borrow of planet's internal state.
779    #[must_use]
780    pub fn state(&self) -> &PlanetState {
781        &self.state
782    }
783
784    /// Returns an immutable borrow of the planet generator.
785    #[must_use]
786    pub fn generator(&self) -> &Generator {
787        &self.generator
788    }
789
790    /// Returns an immutable borrow of the planet combinator.
791    #[must_use]
792    pub fn combinator(&self) -> &Combinator {
793        &self.combinator
794    }
795}
796
797#[cfg(test)]
798mod tests {
799    use super::*;
800    use crossbeam_channel::{Receiver, Sender, unbounded};
801    use std::thread;
802    use std::time::Duration;
803
804    use crate::components::asteroid::Asteroid;
805    use crate::components::energy_cell::EnergyCell;
806    use crate::components::resource::{BasicResourceType, Combinator, Generator};
807    use crate::components::rocket::Rocket;
808    use crate::components::sunray::Sunray;
809    use crate::protocols::orchestrator_planet::{OrchestratorToPlanet, PlanetToOrchestrator};
810
811    // --- Mock AI ---
812    struct MockAI {
813        start_called: bool,
814        stop_called: bool,
815        sunray_count: ID,
816    }
817
818    impl MockAI {
819        fn new() -> Self {
820            Self {
821                start_called: false,
822                stop_called: false,
823                sunray_count: 0,
824            }
825        }
826    }
827
828    impl PlanetAI for MockAI {
829        fn handle_sunray(
830            &mut self,
831            state: &mut PlanetState,
832            _generator: &Generator,
833            _combinator: &Combinator,
834            sunray: Sunray,
835        ) {
836            self.sunray_count += 1;
837
838            if let Some(cell) = state.cells_iter_mut().next() {
839                cell.charge(sunray);
840            }
841        }
842
843        fn handle_asteroid(
844            &mut self,
845            state: &mut PlanetState,
846            _generator: &Generator,
847            _combinator: &Combinator,
848        ) -> Option<Rocket> {
849            match state.full_cell() {
850                None => None,
851                Some((_cell, i)) => {
852                    // assert!(cell.is_charged());
853                    let _ = state.build_rocket(i);
854                    state.take_rocket()
855                }
856            }
857        }
858
859        fn handle_internal_state_req(
860            &mut self,
861            state: &mut PlanetState,
862            _generator: &Generator,
863            _combinator: &Combinator,
864        ) -> DummyPlanetState {
865            state.to_dummy()
866        }
867
868        fn handle_explorer_msg(
869            &mut self,
870            _state: &mut PlanetState,
871            _generator: &Generator,
872            _combinator: &Combinator,
873            msg: ExplorerToPlanet,
874        ) -> Option<PlanetToExplorer> {
875            match msg {
876                ExplorerToPlanet::AvailableEnergyCellRequest { .. } => {
877                    Some(PlanetToExplorer::AvailableEnergyCellResponse { available_cells: 5 })
878                }
879                _ => None,
880            }
881        }
882
883        fn on_start(
884            &mut self,
885            _state: &PlanetState,
886            _generator: &Generator,
887            _combinator: &Combinator,
888        ) {
889            self.start_called = true;
890        }
891
892        fn on_stop(
893            &mut self,
894            _state: &PlanetState,
895            _generator: &Generator,
896            _combinator: &Combinator,
897        ) {
898            self.stop_called = true;
899        }
900    }
901
902    // --- Helper for creating dummy channels ---
903    // Returns the halves required by Planet::new
904    type PlanetOrchHalfChannels = (Receiver<OrchestratorToPlanet>, Sender<PlanetToOrchestrator>);
905
906    type PlanetExplHalfChannels = (Receiver<ExplorerToPlanet>, Sender<PlanetToExplorer>);
907
908    type OrchPlanetHalfChannels = (Sender<OrchestratorToPlanet>, Receiver<PlanetToOrchestrator>);
909
910    type ExplPlanetHalfChannels = (Sender<ExplorerToPlanet>, Receiver<PlanetToExplorer>);
911
912    fn get_test_channels() -> (
913        PlanetOrchHalfChannels,
914        PlanetExplHalfChannels,
915        OrchPlanetHalfChannels,
916        ExplPlanetHalfChannels,
917    ) {
918        // Channel 1: Orchestrator -> Planet
919        let (tx_orch_in, rx_orch_in) = unbounded::<OrchestratorToPlanet>();
920        // Channel 2: Planet -> Orchestrator
921        let (tx_orch_out, rx_orch_out) = unbounded::<PlanetToOrchestrator>();
922
923        // Channel 3: Explorer -> Planet
924        let (tx_expl_in, rx_expl_in) = unbounded::<ExplorerToPlanet>();
925        // Channel 4: Planet -> Explorer
926        let (tx_expl_out, rx_expl_out) = unbounded::<PlanetToExplorer>();
927
928        (
929            (rx_orch_in, tx_orch_out),
930            (rx_expl_in, tx_expl_out),
931            (tx_orch_in, rx_orch_out),
932            (tx_expl_in, rx_expl_out),
933        )
934    }
935
936    // --- Unit Tests: Planet State Logic ---
937
938    #[test]
939    fn test_planet_state_rocket_construction() {
940        let mut state = PlanetState {
941            id: 0,
942            energy_cells: vec![EnergyCell::new()],
943            rocket: None,
944            can_have_rocket: true,
945        };
946
947        let cell = state.cell_mut(0);
948        let sunray = Sunray::new();
949        cell.charge(sunray);
950
951        // Build Rocket
952        let res = state.build_rocket(0);
953        assert!(res.is_ok());
954        assert!(state.has_rocket());
955        assert!(!state.cell(0).is_charged());
956
957        // Take Rocket
958        let rocket = state.take_rocket();
959        assert!(rocket.is_some());
960        assert!(!state.has_rocket());
961    }
962
963    #[test]
964    fn test_planet_state_type_b_no_rocket() {
965        let mut state = PlanetState {
966            id: 0,
967            energy_cells: vec![EnergyCell::new()],
968            rocket: None,
969            can_have_rocket: false, // Type B
970        };
971
972        let cell = state.cell_mut(0);
973        cell.charge(Sunray::new());
974
975        let res = state.build_rocket(0);
976        assert!(res.is_err(), "Type B should not be able to build rockets");
977    }
978
979    // --- Integration Tests: Constructor ---
980
981    #[test]
982    fn test_planet_construction_constraints() {
983        // 1. Valid Construction
984        let (orch_ch, expl_ch, _, _) = get_test_channels();
985        let valid_gen = vec![BasicResourceType::Oxygen];
986
987        let valid_planet = Planet::new(
988            1,
989            PlanetType::A,
990            Box::new(MockAI::new()),
991            valid_gen,
992            vec![],
993            orch_ch,
994            expl_ch.0,
995        );
996        assert!(valid_planet.is_ok());
997
998        // 2. Invalid: Empty Gen Rules
999        let (orch_ch, expl_ch, _, _) = get_test_channels();
1000        let invalid_empty = Planet::new(
1001            1,
1002            PlanetType::A,
1003            Box::new(MockAI::new()),
1004            vec![], // Error
1005            vec![],
1006            orch_ch,
1007            expl_ch.0,
1008        );
1009        assert!(invalid_empty.is_err());
1010
1011        // 3. Invalid: Too Many Gen Rules for Type A
1012        let (orch_ch, expl_ch, _, _) = get_test_channels();
1013        let invalid_gen = Planet::new(
1014            1,
1015            PlanetType::A,
1016            Box::new(MockAI::new()),
1017            vec![BasicResourceType::Oxygen, BasicResourceType::Hydrogen], // Error for Type A
1018            vec![],
1019            orch_ch,
1020            expl_ch.0,
1021        );
1022        assert!(invalid_gen.is_err());
1023    }
1024
1025    // --- Integration Tests: Loop ---
1026
1027    #[test]
1028    fn test_planet_run_loop_survival() {
1029        let (planet_orch_ch, planet_expl_ch, orch_planet_ch, _) = get_test_channels();
1030
1031        let (rx_from_orch, tx_from_planet_orch) = planet_orch_ch;
1032        let (rx_from_expl, _) = planet_expl_ch;
1033        let (tx_to_planet_orch, rx_to_orch) = orch_planet_ch;
1034
1035        // Build Planet
1036        let mut planet = Planet::new(
1037            100,
1038            PlanetType::A,
1039            Box::new(MockAI::new()),
1040            vec![BasicResourceType::Oxygen],
1041            vec![],
1042            (rx_from_orch, tx_from_planet_orch),
1043            rx_from_expl,
1044        )
1045        .expect("Failed to create planet");
1046
1047        // Spawn thread
1048        let handle = thread::spawn(move || {
1049            let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1050                let res = planet.run();
1051                match res {
1052                    Ok(()) => {}
1053                    Err(err) => {
1054                        dbg!(err);
1055                    }
1056                }
1057            }));
1058        });
1059
1060        // 1. Start AI
1061        tx_to_planet_orch
1062            .send(OrchestratorToPlanet::StartPlanetAI)
1063            .unwrap();
1064        match rx_to_orch.recv_timeout(Duration::from_millis(50)) {
1065            Ok(PlanetToOrchestrator::StartPlanetAIResult { .. }) => {}
1066            _ => panic!("Planet sent incorrect response"),
1067        }
1068        thread::sleep(Duration::from_millis(50));
1069
1070        // 2. Send Sunray
1071        tx_to_planet_orch
1072            .send(OrchestratorToPlanet::Sunray(Sunray::new()))
1073            .unwrap();
1074
1075        // Expect Ack
1076        if let Ok(PlanetToOrchestrator::SunrayAck { planet_id, .. }) =
1077            rx_to_orch.recv_timeout(Duration::from_millis(200))
1078        {
1079            assert_eq!(planet_id, 100);
1080        } else {
1081            panic!("Did not receive SunrayAck");
1082        }
1083
1084        // 3. Send Asteroid (AI should build rocket using the charged cell)
1085        tx_to_planet_orch
1086            .send(OrchestratorToPlanet::Asteroid(Asteroid::new()))
1087            .unwrap();
1088
1089        // 4. Expect Survival (Ack with Some(Rocket))
1090        match rx_to_orch.recv_timeout(Duration::from_millis(200)) {
1091            Ok(PlanetToOrchestrator::AsteroidAck {
1092                planet_id, rocket, ..
1093            }) => {
1094                assert_eq!(planet_id, 100);
1095                assert!(rocket.is_some(), "Planet failed to build rocket!");
1096            }
1097            Ok(_) => panic!("Wrong message type"),
1098            Err(e) => panic!("Timeout waiting for AsteroidAck: {e}"),
1099        }
1100
1101        // 5. Stop
1102        tx_to_planet_orch
1103            .send(OrchestratorToPlanet::StopPlanetAI)
1104            .unwrap();
1105        match rx_to_orch.recv_timeout(Duration::from_millis(200)) {
1106            Ok(PlanetToOrchestrator::StopPlanetAIResult { .. }) => {}
1107            _ => panic!("Planet sent incorrect response"),
1108        }
1109
1110        // 6. Try to send a request while stopped
1111        tx_to_planet_orch
1112            .send(OrchestratorToPlanet::InternalStateRequest)
1113            .unwrap();
1114        match rx_to_orch.recv_timeout(Duration::from_millis(200)) {
1115            Ok(PlanetToOrchestrator::Stopped { .. }) => {}
1116            _ => panic!("Planet sent incorrect response"),
1117        }
1118
1119        // 7. Kill planet while stopped
1120        tx_to_planet_orch
1121            .send(OrchestratorToPlanet::KillPlanet)
1122            .unwrap();
1123        match rx_to_orch.recv_timeout(Duration::from_millis(200)) {
1124            Ok(PlanetToOrchestrator::KillPlanetResult { .. }) => {}
1125            _ => panic!("Planet sent incorrect response"),
1126        }
1127
1128        // should return immediately
1129        assert!(handle.join().is_ok(), "Planet thread exited with an error");
1130    }
1131
1132    #[test]
1133    fn test_resource_creation() {
1134        let (orch_ch, expl_ch, _, _) = get_test_channels();
1135        let gen_rules = vec![BasicResourceType::Oxygen, BasicResourceType::Hydrogen];
1136        let comb_rules = vec![ComplexResourceType::Water];
1137        let mut planet = Planet::new(
1138            0,
1139            PlanetType::B,
1140            Box::new(MockAI::new()),
1141            gen_rules,
1142            comb_rules,
1143            orch_ch,
1144            expl_ch.0,
1145        )
1146        .unwrap();
1147
1148        // aliases for planet internals
1149        let state = &mut planet.state;
1150        let generator = &planet.generator;
1151        let combinator = &planet.combinator;
1152
1153        // gen oxygen
1154        let cell = state.cell_mut(0);
1155        cell.charge(Sunray::new());
1156
1157        let oxygen = generator.make_oxygen(cell);
1158        assert!(oxygen.is_ok());
1159        let oxygen = oxygen.unwrap();
1160
1161        // gen hydrogen
1162        let cell = state.cell_mut(0);
1163        cell.charge(Sunray::new());
1164
1165        let hydrogen = generator.make_hydrogen(cell);
1166        assert!(hydrogen.is_ok());
1167        let hydrogen = hydrogen.unwrap();
1168
1169        // combine the two elements into water
1170        let cell = state.cell_mut(0);
1171        cell.charge(Sunray::new());
1172
1173        let diamond = combinator.make_water(hydrogen, oxygen, cell);
1174        assert!(diamond.is_ok());
1175
1176        // try to gen resource not contained in the planet recipes
1177        let carbon = generator.make_carbon(cell);
1178        assert!(carbon.is_err());
1179    }
1180
1181    #[test]
1182    fn test_explorer_comms() {
1183        // 1. Setup Channels using the new helper
1184        let (
1185            planet_orch_channels,
1186            planet_expl_channels,
1187            (orch_tx, orch_rx),
1188            (expl_tx_global, _expl_rx_global),
1189        ) = get_test_channels();
1190
1191        // 2. Setup Planet
1192        // Note: Planet::new only takes the Receiver half for explorers,
1193        // so we extract it from the tuple. The Sender half in the tuple is unused
1194        // by the planet itself (since it uses dynamic senders), but kept for type consistency.
1195        let (planet_expl_rx, _) = planet_expl_channels;
1196
1197        let mut planet = Planet::new(
1198            1,
1199            PlanetType::A,
1200            Box::new(MockAI::new()),
1201            vec![BasicResourceType::Oxygen],
1202            vec![],
1203            planet_orch_channels,
1204            planet_expl_rx,
1205        )
1206        .expect("Failed to create planet");
1207
1208        // Spawn planet thread
1209        let handle = thread::spawn(move || {
1210            let res = planet.run();
1211            match res {
1212                Ok(()) => {}
1213                Err(err) => {
1214                    dbg!(err);
1215                }
1216            }
1217        });
1218
1219        // 3. Start Planet
1220        orch_tx.send(OrchestratorToPlanet::StartPlanetAI).unwrap();
1221        match orch_rx.recv_timeout(Duration::from_millis(50)) {
1222            Ok(PlanetToOrchestrator::StartPlanetAIResult { .. }) => {}
1223            _ => panic!("Planet sent incorrect response"),
1224        }
1225        thread::sleep(Duration::from_millis(50));
1226
1227        // 4. Setup Local Explorer Channels (Simulating Explorer 101)
1228        // We create a dedicated channel for this specific explorer interaction
1229        let explorer_id = 101;
1230        let (expl_dedicated_tx, expl_dedicated_rx) = unbounded::<PlanetToExplorer>();
1231
1232        // 5. Send IncomingExplorerRequest (Orchestrator -> Planet)
1233        orch_tx
1234            .send(OrchestratorToPlanet::IncomingExplorerRequest {
1235                explorer_id,
1236                new_sender: expl_dedicated_tx,
1237            })
1238            .unwrap();
1239
1240        // 6. Verify Ack from Planet
1241        match orch_rx.recv_timeout(Duration::from_millis(200)) {
1242            Ok(PlanetToOrchestrator::IncomingExplorerResponse { planet_id, res, .. }) => {
1243                assert_eq!(planet_id, 1);
1244                assert!(res.is_ok());
1245            }
1246            _ => panic!("Expected IncomingExplorerResponse"),
1247        }
1248
1249        // 7. Test Interaction (Explorer -> Planet -> Explorer)
1250        // Explorer sends a request using the GLOBAL channel, but includes its ID
1251        expl_tx_global
1252            .send(ExplorerToPlanet::AvailableEnergyCellRequest { explorer_id })
1253            .unwrap();
1254
1255        // Verify Explorer receives response on the LOCAL channel
1256        match expl_dedicated_rx.recv_timeout(Duration::from_millis(200)) {
1257            Ok(PlanetToExplorer::AvailableEnergyCellResponse { available_cells }) => {
1258                assert_eq!(available_cells, 5);
1259            }
1260            _ => panic!("Expected AvailableEnergyCellResponse"),
1261        }
1262
1263        // Stop Planet AI
1264        orch_tx.send(OrchestratorToPlanet::StopPlanetAI).unwrap();
1265        match orch_rx.recv_timeout(Duration::from_millis(200)) {
1266            Ok(PlanetToOrchestrator::StopPlanetAIResult { .. }) => {}
1267            _ => panic!("Planet sent incorrect response"),
1268        }
1269
1270        // Try to send request from explorer to stopped planet
1271        expl_tx_global
1272            .send(ExplorerToPlanet::AvailableEnergyCellRequest { explorer_id })
1273            .unwrap();
1274        match expl_dedicated_rx.recv_timeout(Duration::from_millis(200)) {
1275            Ok(PlanetToExplorer::Stopped) => {}
1276            _ => panic!("Planet sent incorrect response"),
1277        }
1278
1279        // Restart planet AI
1280        orch_tx.send(OrchestratorToPlanet::StartPlanetAI).unwrap();
1281        match orch_rx.recv_timeout(Duration::from_millis(200)) {
1282            Ok(PlanetToOrchestrator::StartPlanetAIResult { .. }) => {}
1283            _ => panic!("Planet sent incorrect response"),
1284        }
1285
1286        // 8. Send OutgoingExplorerRequest (Orchestrator -> Planet)
1287        orch_tx
1288            .send(OrchestratorToPlanet::OutgoingExplorerRequest { explorer_id })
1289            .unwrap();
1290
1291        // 9. Verify Ack from Planet
1292        match orch_rx.recv_timeout(Duration::from_millis(200)) {
1293            Ok(PlanetToOrchestrator::OutgoingExplorerResponse { planet_id, res, .. }) => {
1294                assert_eq!(planet_id, 1);
1295                assert!(res.is_ok());
1296            }
1297            _ => panic!("Expected OutgoingExplorerResponse"),
1298        }
1299
1300        // 10. Verify Isolation
1301        // Explorer sends another request
1302        expl_tx_global
1303            .send(ExplorerToPlanet::AvailableEnergyCellRequest { explorer_id })
1304            .unwrap();
1305
1306        // We expect NO response on expl_rx_local
1307        let result = expl_dedicated_rx.recv_timeout(Duration::from_millis(200));
1308        assert!(
1309            result.is_err(),
1310            "Planet responded to explorer after it left!"
1311        );
1312
1313        // 11. Cleanup
1314        drop(orch_tx);
1315        let _ = handle.join();
1316    }
1317}