From 32a62cfcb925225359a5c90761b795f2f737ebd2 Mon Sep 17 00:00:00 2001 From: LLLL Colonq Date: Tue, 18 Aug 2026 00:08:28 -0400 Subject: Re-populate games --- 2026/games/rpc2dot0/src/state.rs | 484 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 484 insertions(+) create mode 100644 2026/games/rpc2dot0/src/state.rs (limited to '2026/games/rpc2dot0/src/state.rs') diff --git a/2026/games/rpc2dot0/src/state.rs b/2026/games/rpc2dot0/src/state.rs new file mode 100644 index 0000000..c084bfc --- /dev/null +++ b/2026/games/rpc2dot0/src/state.rs @@ -0,0 +1,484 @@ +use crate::collision::process_collisions; +use crate::color::EntityColor; +use crate::computer::ComputerPhase; +use crate::constants::{ + BASE_SPEED, ENTITY_RADIUS, ENTITY_SCALE_FACTOR, GAME_DURATION_MS, INITIAL_SPAWN, + INITIAL_SPAWN_SPREAD_MS, JITTER_STRENGTH, MAX_ENTITIES, MAX_SPEED, PACMAN_CHASE_PROB, + PACMAN_CHASE_SPEED_MULT, RED_COUNTDOWN_MS, ROGUE_DURATION_MS, SPAWN_PADDING, SPAWN_RATE_MAX_MS, + SPAWN_RATE_MIN_MS, +}; +use crate::effects::{PacmanChase, Rogue}; +use crate::entity::{Entity, Update}; +use crate::errors::ClickOutcome; +use crate::math::Vec2; +use crate::random::RandomState; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum GamePhase { + Waiting, + Playing, + Computer(ComputerPhase), + Finished, +} + +impl GamePhase { + #[allow(dead_code)] + pub fn phase(&self) -> &'static str { + match self { + GamePhase::Waiting => "waiting", + GamePhase::Playing => "playing", + GamePhase::Computer(_) => "computer", + GamePhase::Finished => "finished", + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum RoundResult { + Ok, + Ko, +} + +impl From for &'static str { + fn from(value: RoundResult) -> Self { + match value { + RoundResult::Ok => "Ok", + RoundResult::Ko => "Ko", + } + } +} + +impl From for bool { + fn from(value: RoundResult) -> Self { + match value { + RoundResult::Ok => true, + RoundResult::Ko => false, + } + } +} + +#[derive(Debug, Clone)] +pub struct GameState { + consumer_score: i32, + pub(crate) elapsed_ms: f32, + phase: GamePhase, + pub(crate) entities: Vec, + pub(crate) next_entity_id: u32, + pub(crate) rng: RandomState, + spawn_timer_ms: f32, + initial_spawn_remaining: u32, + next_spawn_delay_ms: f32, + pub(crate) collision_buffer: Vec<(usize, usize)>, + pub(crate) events: Vec, + pub(crate) difficulty: f32, +} + +impl Default for GameState { + fn default() -> Self { + Self::new() + } +} + +impl GameState { + pub fn new() -> Self { + Self { + consumer_score: 0, + elapsed_ms: 0.0, + phase: GamePhase::Waiting, + entities: Vec::new(), + next_entity_id: 1, + #[cfg(debug_assertions)] + rng: RandomState::new(0x67676767), + #[cfg(not(debug_assertions))] + rng: RandomState::new((js_sys::Date::now() as u64) as usize), + spawn_timer_ms: 0.0, + initial_spawn_remaining: INITIAL_SPAWN, + next_spawn_delay_ms: (INITIAL_SPAWN_SPREAD_MS / INITIAL_SPAWN as f32).max(1.0), + collision_buffer: Vec::new(), + events: Vec::new(), + difficulty: 1.0, + } + } + + pub fn set_difficulty(&mut self, difficulty: f32) { + self.difficulty = difficulty; + } + + pub fn game_duration_ms(&self) -> f32 { + GAME_DURATION_MS as f32 / self.difficulty + } + + pub fn required_greens(&self) -> i32 { + 5.max((15.0 / self.difficulty.sqrt()).round() as i32) + } + + pub const fn base_speed(&self) -> f32 { + BASE_SPEED * self.difficulty + } + + pub fn max_speed(&self) -> f32 { + MAX_SPEED * self.difficulty + } + + pub fn spawn_rate_min_ms(&self) -> u32 { + (SPAWN_RATE_MIN_MS as f32 / self.difficulty) as u32 + } + + pub fn spawn_rate_max_ms(&self) -> u32 { + (SPAWN_RATE_MAX_MS as f32 / self.difficulty) as u32 + } + + pub fn entity_radius(&self) -> f32 { + ENTITY_RADIUS / (1.0 + (self.difficulty - 1.0) * 0.25) + } + + pub fn start_round(&mut self) { + self.consumer_score = 0; + self.elapsed_ms = 0.0; + self.phase = GamePhase::Playing; + self.entities.clear(); + self.next_entity_id = 1; + self.spawn_timer_ms = 0.0; + self.initial_spawn_remaining = INITIAL_SPAWN; + self.next_spawn_delay_ms = (INITIAL_SPAWN_SPREAD_MS / INITIAL_SPAWN as f32).max(1.0); + self.events.clear(); + } + pub fn set_phase(&mut self, phase: GamePhase) { + self.phase = phase; + } + /* + pub fn boot_computer(&mut self) { + if self.phase == GamePhase::Playing { + self.phase = GamePhase::Computer(ComputerPhase::Booting); + } + } + + pub fn shutdown_computer(&mut self) { + if self.phase == GamePhase::Computer(ComputerPhase::On) { + self.phase = GamePhase::Computer(ComputerPhase::ShutDown); + } + } + */ + + pub fn spawn_entity( + &mut self, + pos: Vec2, + vel: Vec2, + color: EntityColor, + disquette_inserted: bool, + ) { + if self.get_entity_count() >= MAX_ENTITIES { + return; + } + let speed = self.base_speed(); + let radius = self.entity_radius() * ENTITY_SCALE_FACTOR; + let mut entity = Entity::new(self.next_entity_id, pos, vel, color, speed, radius); + if color == EntityColor::Red { + entity.spawn_time_ms = Some(self.elapsed_ms); + } + if color == EntityColor::Green { + let base_prob = 0.25; + let prob = (base_prob * self.difficulty).min(0.85); + if self.rng.next_bool(prob as f64) { + let rogue_duration = ROGUE_DURATION_MS / self.difficulty; + let mut hover_color = self.rng.random_color(); + while hover_color == EntityColor::Green { + hover_color = self.rng.random_color(); + } + entity.rogue = Some(Rogue::new(self.elapsed_ms + rogue_duration, hover_color)); + } + } + + if !disquette_inserted + && color == EntityColor::Bleu + && self.rng.next_bool(PACMAN_CHASE_PROB) + { + let chase_speed = self.base_speed() * PACMAN_CHASE_SPEED_MULT; + entity.pacman_chase = Some(PacmanChase::new(chase_speed)); + } + self.next_entity_id = self.next_entity_id.wrapping_add(1); + self.entities.push(entity); + } + pub fn update_entities( + &mut self, + delta_ms: f32, + area_width: f32, + area_height: f32, + disquette_inserted: bool, + disquette_pos: Option, + ) { + self.update_spawning(delta_ms, area_width, area_height, disquette_inserted); + + let current_max_speed = self.max_speed(); + for entity in &mut self.entities { + if !entity.alive { + continue; + } + if let Some(chase) = &entity.pacman_chase { + if let Some(target) = disquette_pos { + let dx = target.x - entity.pos.x; + let dy = target.y - entity.pos.y; + let dist = (dx * dx + dy * dy).sqrt(); + if dist > f32::EPSILON { + entity.vel.x = (dx / dist) * chase.speed; + entity.vel.y = (dy / dist) * chase.speed; + } + } else { + entity.apply_jitter(Vec2::new( + self.rng.next_range(-JITTER_STRENGTH, JITTER_STRENGTH), + self.rng.next_range(-JITTER_STRENGTH, JITTER_STRENGTH), + )); + } + } else { + entity.apply_jitter(Vec2::new( + self.rng.next_range(-JITTER_STRENGTH, JITTER_STRENGTH), + self.rng.next_range(-JITTER_STRENGTH, JITTER_STRENGTH), + )); + } + + entity.update(delta_ms); + entity.dampen_speed(current_max_speed); + entity.bounds_bounce(area_width, area_height); + } + if let Some(target) = disquette_pos { + const DISQUETTE_HALF_SIZE: f32 = 3.2; + let mut consumed = false; + for entity in &self.entities { + if entity.alive + && entity.pacman_chase.is_some() + && entity.pos.distance_squared(target) + <= (entity.radius + DISQUETTE_HALF_SIZE).powi(2) + { + consumed = true; + break; + } + } + if consumed { + self.events.push("disquette_consumed".to_string()); + } + } + + process_collisions( + &mut self.entities, + &mut self.rng, + &mut self.next_entity_id, + &mut self.collision_buffer, + self.elapsed_ms, + &mut self.events, + ); + + // Check red countdown timers (explode if expired) + let mut greens_to_kill = false; + for entity in &mut self.entities { + if entity.alive + && entity.color == EntityColor::Red + && let Some(spawn_time) = entity.spawn_time_ms + && self.elapsed_ms >= spawn_time + RED_COUNTDOWN_MS + { + entity.alive = false; + self.events + .push(format!("explode:{},{}", entity.pos.x, entity.pos.y)); + greens_to_kill = true; + } + } + if greens_to_kill { + for entity in &mut self.entities { + if entity.alive && entity.color == EntityColor::Green { + entity.alive = false; + } + } + } + + self.entities.retain(|e| e.alive); + } + + pub fn entities(&self) -> &[Entity] { + &self.entities + } + + fn schedule_next_spawn(&mut self) { + self.next_spawn_delay_ms = if self.initial_spawn_remaining > 0 { + (INITIAL_SPAWN_SPREAD_MS / INITIAL_SPAWN as f32).max(1.0) + } else { + self.rng + .next_range( + self.spawn_rate_min_ms() as f32, + self.spawn_rate_max_ms() as f32, + ) + .round() + .max(1.0) + }; + } + + fn position_overlaps(&self, pos: Vec2) -> bool { + let scaled_radius = self.entity_radius() * ENTITY_SCALE_FACTOR; + self.entities.iter().any(|entity| { + let distance_sq = entity.pos.distance_squared(pos); + distance_sq < (entity.radius + scaled_radius + SPAWN_PADDING).powi(2) + }) + } + + fn generate_spawn_position(&mut self, area_width: f32, area_height: f32) -> Vec2 { + let scaled_radius = self.entity_radius() * ENTITY_SCALE_FACTOR; + const MAX_ATTEMPTS: usize = 16; + for _ in 0..MAX_ATTEMPTS { + let pos = Vec2::new( + self.rng + .next_range(scaled_radius, area_width - scaled_radius), + self.rng + .next_range(scaled_radius, area_height - scaled_radius), + ); + if !self.position_overlaps(pos) { + return pos; + } + } + Vec2::new( + self.rng + .next_range(scaled_radius, area_width - scaled_radius), + self.rng + .next_range(scaled_radius, area_height - scaled_radius), + ) + } + + fn spawn_random_entity(&mut self, area_width: f32, area_height: f32, disquette_inserted: bool) { + if self.get_entity_count() >= MAX_ENTITIES { + return; + } + + let pos = self.generate_spawn_position(area_width, area_height); + let dir = self.rng.next_unit_vector(); + let vel = dir * self.base_speed(); + + let mut color_counts = [0u32; 4]; + for entity in &self.entities { + if entity.alive { + color_counts[entity.color as usize] += 1; + } + } + let color = self.rng.random_color_biased(color_counts, self.difficulty); + self.spawn_entity(pos, vel, color, disquette_inserted); + } + + fn update_spawning( + &mut self, + delta_ms: f32, + area_width: f32, + area_height: f32, + disquette_inserted: bool, + ) { + if self.phase != GamePhase::Playing || area_width <= 0.0 || area_height <= 0.0 { + return; + } + + self.spawn_timer_ms += delta_ms; + while self.spawn_timer_ms >= self.next_spawn_delay_ms { + self.spawn_timer_ms -= self.next_spawn_delay_ms; + if self.get_entity_count() >= MAX_ENTITIES { + self.spawn_timer_ms = 0.0; + break; + } + + if self.initial_spawn_remaining > 0 { + self.spawn_random_entity(area_width, area_height, disquette_inserted); + self.initial_spawn_remaining = self.initial_spawn_remaining.saturating_sub(1); + } else { + self.spawn_random_entity(area_width, area_height, disquette_inserted); + } + + self.schedule_next_spawn(); + } + } + + fn entity_at_point(&self, point: Vec2) -> Option { + self.entities + .iter() + .enumerate() + .rev() + .find(|(_, e)| e.alive && e.pos.distance_squared(point) <= e.radius * e.radius) + .map(|(idx, _)| idx) + } + + pub fn click_at(&mut self, x: f32, y: f32) -> Option { + //log::info!("eatyourgreens:: click_at(x={x},y={y})"); + if self.phase != GamePhase::Playing { + return None; + } + + let target = self.entity_at_point(Vec2::new(x, y)); + if let Some(idx) = target { + let color = self.entities[idx].color; + + let mut effective_color = color; + if color == EntityColor::Green + && let Some(rogue) = &self.entities[idx].rogue + && self.elapsed_ms < rogue.until_ms + { + effective_color = rogue.hover_color; + } + + self.entities[idx].alive = false; + match effective_color { + EntityColor::Green => self.change_score(1), + EntityColor::Yellow | EntityColor::Red | EntityColor::Bleu => self.change_score(-1), + } + + if effective_color == EntityColor::Red { + self.events.push(format!("click_red:{},{}", x, y)); + } + return Some(ClickOutcome::Hit(effective_color)); + } + Some(ClickOutcome::Miss) + } + + pub fn consumer_score(&self) -> i32 { + self.consumer_score + } + + pub fn elapsed_ms(&self) -> f32 { + self.elapsed_ms + } + + pub fn remaining_ms(&self) -> f32 { + self.game_duration_ms() - self.elapsed_ms + } + + pub fn phase(&self) -> GamePhase { + self.phase + } + + pub fn round_result(&self) -> RoundResult { + if self.consumer_score >= self.required_greens() { + RoundResult::Ok + } else { + RoundResult::Ko + } + } + + pub fn advance_time(&mut self, delta_ms: f32) { + if self.phase != GamePhase::Playing { + return; + } + + self.elapsed_ms += delta_ms; + let duration = self.game_duration_ms(); + if self.elapsed_ms >= duration { + self.elapsed_ms = duration; + self.phase = GamePhase::Finished; + } + } + + pub fn drain_events(&mut self) -> Vec { + std::mem::take(&mut self.events) + } + + pub fn get_entity_count(&self) -> u32 { + self.entities.iter().filter(|e| e.alive).count() as u32 + } + + pub fn change_score(&mut self, delta: i32) { + if self.phase != GamePhase::Playing { + return; + } + self.consumer_score = self.consumer_score.saturating_add(delta); + } +} -- cgit v1.3.1