/* Hurry Curry! - a game about cooking Copyright (C) 2025 Hurry Curry! Contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3 of the License only. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ use hurrycurry_locale::{ GLOBAL_LOCALE, TrError, message::{COLORED, MessageDisplayExt}, tre, trm, }; use hurrycurry_protocol::{Message, PacketC, PacketS, PlayerID, VoteSubject}; use log::info; use std::collections::{HashMap, HashSet, VecDeque}; #[derive(Default)] pub struct VoteState { initiate_cooldown: HashMap, cast_cooldown: HashMap, voting_players: HashSet, active: Option, pub ratified: Option, pub packet_out: VecDeque, } pub struct ActiveVote { timeout: f32, initiated_by: PlayerID, ballots: HashMap, subject: VoteSubject, } impl VoteState { pub fn join(&mut self, player: PlayerID) { self.cast_cooldown.insert(player, 30.); self.initiate_cooldown.insert(player, 40.); self.update(); } pub fn leave(&mut self, player: PlayerID) { self.cast_cooldown.remove(&player); self.initiate_cooldown.remove(&player); self.voting_players.remove(&player); if let Some(a) = &mut self.active { a.ballots.remove(&player); } self.update(); } pub fn prime_client(&self, init: &mut Vec) { if let Some(active) = &self.active { init.push(PacketC::VoteStarted { initiated_by: active.initiated_by, subject: active.subject.clone(), message: subject_to_message(&active.subject), timeout: active.timeout, }); } } pub fn packet_in(&mut self, p: PacketS) -> Result<(), TrError> { match p { PacketS::InitiateVote { player, subject } => { if self.active.is_some() { return Err(tre!("s.vote.error.active")); } if let Some(t) = self.initiate_cooldown.get(&player) { return Err(tre!( "s.vote.error.initiate_cooldown", s = format!("{t:.0}") )); } info!( "{player} initiates vote for {}", subject_to_message(&subject).display_nodata(&GLOBAL_LOCALE, &COLORED) ); self.initiate_cooldown.insert(player, 40.); self.active = Some(ActiveVote { timeout: 30., initiated_by: player, ballots: [(player, true)].into_iter().collect(), // assuming initiating player agrees with their own vote subject: subject.clone(), }); self.packet_out.push_back(PacketC::VoteStarted { initiated_by: player, message: subject_to_message(&subject), subject, timeout: 30., }); self.update(); } PacketS::CastVote { player, agree } => { let Some(active) = &mut self.active else { return Err(tre!("s.vote.error.not_active")); }; if let Some(t) = self.cast_cooldown.get(&player) { return Err(tre!("s.vote.error.cast_cooldown", s = format!("{t:.0}"))); } if active.initiated_by == player && !agree { self.active = None; self.packet_out .push_back(PacketC::VoteEnded { result: false }); return Ok(()); } active.ballots.insert(player, agree); self.update(); } _ => (), } Ok(()) } pub fn tick(&mut self, dt: f32) { let mut need_update = false; self.initiate_cooldown.retain(|_, c| { *c -= dt; *c > 0. }); self.cast_cooldown.retain(|p, c| { *c -= dt; if *c <= 0. { self.voting_players.insert(*p); need_update = true; } *c > 0. }); if let Some(v) = &mut self.active { v.timeout -= dt; need_update |= v.timeout <= 0. } if need_update { self.update(); } } pub fn update(&mut self) { // skip cooldowns if single player if self.voting_players.len() + self.cast_cooldown.len() == 1 { self.voting_players.extend(self.cast_cooldown.keys()); self.cast_cooldown.clear(); self.initiate_cooldown.clear(); } if let Some(a) = &self.active { let total = self.voting_players.len(); let mut agree = 0; let mut reject = 0; for (_, v) in &a.ballots { if *v { agree += 1; } else { reject += 1; } } if agree * 2 > total || reject * 2 > total || a.timeout <= 0. { let result = agree > reject; if result { info!("Vote passed"); self.ratified = Some(a.subject.clone()); } else { info!("Vote rejected"); } self.active = None; self.packet_out.push_back(PacketC::VoteEnded { result }); } else { self.packet_out.push_back(PacketC::VoteUpdated { total, agree, reject, }); } } } } fn subject_to_message(s: &VoteSubject) -> Message { match s { VoteSubject::StartGame { config } => { trm!("s.vote.subject.start_game", s = config.map.clone()) } VoteSubject::EndGame => trm!("s.vote.subject.end_game"), VoteSubject::RestartGame => trm!("s.vote.subject.restart_game"), } }