/*
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"),
}
}