use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::{Duration, SystemTime, SystemTimeError};
mod components;
use self::components::{Card, Deck, Hand};
pub(crate) use self::components::{Clue, Color, Number};
pub(crate) use self::components::{ClueError, DiscardError, PlayError};
const COLOR_ORDER: [Color; 5] = [
Color::Red,
Color::Green,
Color::White,
Color::Blue,
Color::Yellow,
];
fn dur(t: Result<Duration, SystemTimeError>) -> String {
if t.is_err() {
return "a while".to_owned();
}
let t = t.unwrap().as_secs();
if t > 24 * 60 * 60 {
format!("{} days", t / (24 * 60 * 60))
} else if t > 60 * 60 {
format!("{} hours", t / (60 * 60))
} else if t > 60 {
format!("{} minutes", t / 60)
} else {
format!("{t} seconds")
}
}
fn dur_mod(start: &mut SystemTime) -> String {
let t = start.elapsed();
*start = SystemTime::now();
dur(t)
}
#[derive(Serialize, Deserialize)]
struct Move {
player: usize,
for_public: String,
for_others: String,
}
impl Move {
pub fn new(player: usize, for_public: String, for_others: String) -> Move {
Move {
player,
for_public,
for_others,
}
}
pub fn show_to(&self, player: usize) -> &str {
if player == self.player {
&self.for_public
} else {
&self.for_others
}
}
}
#[derive(Serialize, Deserialize)]
pub(crate) struct Game {
deck: Deck,
hands: Vec<Hand>,
played: HashMap<Color, Number>,
discard: HashMap<Color, Vec<Card>>,
last_move: Move,
last_move_at: SystemTime,
clues: usize,
lives: usize,
turn: usize,
last_turns: Option<usize>,
started: SystemTime,
is_unwinnable: bool,
}
impl Game {
pub(crate) fn new<'a>(players: impl IntoIterator<Item = &'a str>) -> Self {
let mut deck = Deck::default();
let mut hands: Vec<_> = players.into_iter().map(Hand::new).collect();
let cards = match hands.len() {
0 | 1 => unreachable!(),
2 | 3 => 5,
4 | 5 => 4,
_ => unreachable!(),
};
for hand in &mut hands {
for _ in 0..cards {
let drew = hand.draw(&mut deck);
assert!(drew);
}
}
Game {
hands,
deck,
played: Default::default(),
discard: Default::default(),
last_move: Move::new(0, "".to_owned(), "".to_owned()),
last_move_at: SystemTime::now(),
clues: 8,
lives: 3,
turn: 0,
last_turns: None,
started: SystemTime::now(),
is_unwinnable: false,
}
}
pub(crate) fn score(&self) -> usize {
self.played.values().map(|num| num.as_usize()).sum()
}
pub(crate) fn players<'a>(&'a self) -> impl Iterator<Item = &'a str> + use<'a> {
self.hands.iter().map(|h| &*h.player)
}
pub(crate) fn current_player(&self) -> &str {
&self.hands[self.turn].player
}
pub(crate) fn clue(&mut self, to: &str, clue: Clue) -> Result<usize, ClueError> {
if self.clues == 0 {
return Err(ClueError::NotEnoughClues);
}
let player = self.hands[self.turn].player.clone();
if self.hands[self.turn].player == to {
return Err(ClueError::NoSuchPlayer);
}
let hands = self.hands.len();
let hand = if let Some(h) = self.hands.iter_mut().find(|hand| hand.player == to) {
h
} else {
return Err(ClueError::NoSuchPlayer);
};
match hand.clue(self.turn, clue) {
Ok(num) => {
let did = format!(
"<@{}> clued <@{}> that {} {} {} after {}",
player,
to,
num,
if num == 1 { "card is" } else { "cards are" },
clue,
dur_mod(&mut self.last_move_at),
);
self.last_move = Move::new(self.turn, did.clone(), did);
self.clues -= 1;
self.turn = (self.turn + 1) % hands;
if let Some(ref mut last_turns) = self.last_turns {
*last_turns += 1;
if *last_turns == hands {
return Err(ClueError::GameOver);
}
}
Ok(num)
}
e => e,
}
}
pub(crate) fn play(&mut self, card: usize) -> Result<(), PlayError> {
let hands = self.hands.len();
let hand = self.turn;
if let Some(card) = self.hands.get_mut(hand).unwrap().remove(card) {
self.hands.get_mut(hand).unwrap().draw(&mut self.deck);
use std::collections::hash_map::Entry;
let success = match self.played.entry(card.color) {
Entry::Vacant(e) => {
if card.number == Number::One {
e.insert(Number::One);
true
} else {
false
}
}
Entry::Occupied(mut e) => {
if card.number == *e.get() + 1 {
e.insert(card.number);
if card.number == Number::Five {
if self.clues < 8 {
self.clues += 1;
}
}
true
} else {
false
}
}
};
let drew = if self.last_turns.is_none() {
format!(
", and then drew a {}",
self.hands[hand].cards.back().unwrap()
)
} else {
"".to_owned()
};
if !success {
self.lives -= 1;
let did = format!(
"<@{}> incorrectly played a {} after {}",
self.hands[self.turn].player,
card,
dur_mod(&mut self.last_move_at),
);
self.last_move = Move::new(self.turn, did.clone(), format!("{did}{drew}"));
self.discarded(card);
if self.lives == 0 {
return Err(PlayError::GameOver);
}
} else {
let did = format!(
"<@{}> played a {} after {}",
self.hands[self.turn].player,
card,
dur_mod(&mut self.last_move_at),
);
self.last_move = Move::new(self.turn, did.clone(), format!("{did}{drew}"));
}
self.turn = (self.turn + 1) % hands;
if let Some(ref mut last_turns) = self.last_turns {
*last_turns += 1;
if *last_turns == hands {
return Err(PlayError::GameOver);
}
} else if self.deck.is_empty() {
self.last_turns = Some(0);
}
Ok(())
} else {
Err(PlayError::NoSuchCard)
}
}
pub(crate) fn discard(&mut self, card: usize) -> Result<(), DiscardError> {
if self.clues == 8 {
return Err(DiscardError::MaxClues);
}
let hands = self.hands.len();
let hand = self.turn;
if let Some(card) = self.hands.get_mut(hand).unwrap().remove(card) {
self.hands.get_mut(hand).unwrap().draw(&mut self.deck);
let drew = if self.last_turns.is_none() {
format!(
", and then drew a {}",
self.hands[hand].cards.back().unwrap()
)
} else {
"".to_owned()
};
let did = format!(
"<@{}> discarded a {} after {}",
self.hands[self.turn].player,
card,
dur_mod(&mut self.last_move_at),
);
self.last_move = Move::new(self.turn, did.clone(), format!("{did}{drew}"));
self.discarded(card);
self.clues += 1;
self.turn = (self.turn + 1) % hands;
if let Some(ref mut last_turns) = self.last_turns {
*last_turns += 1;
if *last_turns == hands {
return Err(DiscardError::GameOver);
}
} else if self.deck.is_empty() {
self.last_turns = Some(0);
}
Ok(())
} else {
Err(DiscardError::NoSuchCard)
}
}
pub(crate) fn show_hands(
&self,
user: &str,
skip_self: bool,
cli: &mut impl super::MessageProxy,
) {
let me = self
.hands
.iter()
.position(|hand| hand.player == user)
.unwrap();
cli.send(user, "The other players' hands (in turn order) are:");
for i in 0..self.hands.len() {
let hand = (me + i) % self.hands.len();
if hand == self.turn {
cli.send(
user,
&format!("<@{}> <-- current turn", self.hands[hand].player),
);
} else {
cli.send(user, &format!("<@{}>", self.hands[hand].player));
}
let (cards, known): (Vec<_>, Vec<_>) = self.hands[hand]
.cards
.iter()
.map(|card| (format!("{card}"), card.known()))
.unzip();
if hand == me {
if !skip_self {
cli.send(user, &format!("{} known", &known.join(" | ")));
}
} else {
cli.send(
user,
&format!(
"{} in hand\n{} known",
&cards.join(" | "),
&known.join(" | ")
),
);
}
}
}
pub(crate) fn show_discards(&self, user: &str, cli: &mut impl super::MessageProxy) {
if self.discard.is_empty() {
cli.send(user, "The discard pile is empty.");
return;
}
cli.send(user, "The discard pile contains the following cards:");
for color in &COLOR_ORDER {
if let Some(cards) = self.discard.get(color) {
let mut out = format!("{color} ");
for card in cards {
out.push_str(&format!("{}", card.number));
}
cli.send(user, &out);
}
}
}
pub(crate) fn show_deck(&self, user: &str, cli: &mut impl super::MessageProxy) {
if self.deck.is_empty() {
cli.send(user, "The deck is depleted.");
return;
}
let width = 10;
let left: usize =
(width as f64 * self.deck.len() as f64 / self.deck.of() as f64).round() as usize;
let progress = format!(
"`[{}{}]` {} cards left",
"-".repeat(width - left),
" ".repeat(left),
self.deck.len()
)
.replace("- ", "> ");
cli.send(user, &progress);
}
pub fn score_smiley(&self) -> &'static str {
let points = self.score();
if points >= 25 {
":tada:"
} else if points >= 24 {
":tired_face:"
} else if points >= 23 {
":slightly_smiling_face:"
} else if points >= 22 {
":neutral_face:"
} else if points >= 20 {
":confused:"
} else if points >= 15 {
":slightly_frowning_face:"
} else if points >= 10 {
":disappointed:"
} else {
":face_with_rolling_eyes:"
}
}
pub fn became_unwinnable(&mut self) -> bool {
if self.is_unwinnable {
return false;
}
for cards in self.discard.values() {
let mut number = cards[0].number;
let mut n = 0;
for card in cards {
if card.number == number {
n += 1;
} else {
number = card.number;
n = 1;
}
let total = match number {
Number::One => 3,
Number::Five => 1,
_ => 2,
};
if n == total {
self.is_unwinnable = true;
return true;
}
}
}
false
}
pub fn last_move(&self) -> &str {
&self.last_move.for_public
}
pub(crate) fn progress_game(&mut self, cli: &mut impl super::MessageProxy) -> bool {
if !self.last_move.show_to(0).is_empty() {
for (i, hand) in self.hands.iter().enumerate() {
let mut m = self
.last_move
.show_to(i)
.replace(&format!("<@{}>", hand.player), "you");
if m.starts_with("you") {
m = m.replacen("you", "You", 1);
}
let m = format!(".\n:point_right: {m}");
cli.send(&hand.player, &m);
}
}
let points: usize = self.played.values().map(|num| num.as_usize()).sum();
let mut game_over = self.lives == 0;
if let Some(last_turns) = self.last_turns {
game_over = game_over || last_turns == self.hands.len();
}
if game_over {
for hand in &self.hands {
cli.send(
&hand.player,
&format!(
"Game over after {}.\n\
You got {}/25 points {}\n\
Your hand at the end was:\n\
{}",
dur(self.started.elapsed()),
points,
self.score_smiley(),
hand.cards
.iter()
.map(|c| format!("{c}"))
.collect::<Vec<_>>()
.join(" | ")
),
);
}
return true;
}
if points == 25 {
for hand in &self.hands {
cli.send(
&hand.player,
&format!(
"You won the game with 25/25 points after {} {}",
dur(self.started.elapsed()),
self.score_smiley()
),
);
}
return true;
}
for i in 0..self.hands.len() {
self.print_game_state(i, cli);
}
false
}
fn discarded(&mut self, card: Card) {
let d = self.discard.entry(card.color).or_default();
let pos = d
.binary_search_by_key(&card.number.as_usize(), |c| c.number.as_usize())
.unwrap_or_else(|e| e);
d.insert(pos, card);
}
fn print_game_state(&mut self, hand: usize, cli: &mut impl super::MessageProxy) {
let user = &self.hands[hand].player;
let last = if self.last_turns.is_some() {
" *last*"
} else {
""
};
let setup = if self.turn == hand {
format!("It's *your*{last} turn")
} else {
format!("It's <@{}>'s{} turn", self.hands[self.turn].player, last)
};
cli.send(
user,
&format!(
":hourglass: {}; *{}* :information_source: and {} :bomb: remain.",
setup, self.clues, self.lives
),
);
if !self.deck.is_empty() && self.deck.len() < 2 * self.hands.len() {
match self.deck.len() {
0 => cli.send(
user,
":warning: *There are no cards left in the deck, so this is the last round!*",
),
1 => cli.send(
user,
":warning: *There is only a single card left in the deck!*",
),
n => cli.send(
user,
&format!(":warning: *There are only {n} cards left in the deck!*",),
),
}
}
let stacks: Vec<_> = COLOR_ORDER
.iter()
.map(|&color| {
if let Some(top) = self.played.get(&color) {
format!("{color} {top}")
} else {
format!("{color} :zero:")
}
})
.collect();
if self.turn == hand {
cli.send(user, &format!("Played:\n{}", stacks.join(" | ")));
cli.send(user, "Your hand, as far as you know, is:");
let known: Vec<_> = self.hands[hand]
.cards
.iter()
.enumerate()
.map(|(i, card)| format!("{}: {}", i + 1, card.known()))
.collect();
cli.send(user, &known.join(" | "));
cli.send(user, "");
self.show_hands(user, true, cli);
cli.send(
user,
"\nWhen you have the time, let me know here what move you want to make next!",
);
} else {
}
}
}