cards/server/src/game.rs

339 lines
11 KiB
Rust

use crate::AppState;
use crate::User;
use lib::*;
use rand::prelude::IteratorRandom;
use rand::thread_rng;
use std::{
collections::HashMap,
sync::{Arc, RwLock},
};
use uuid::Uuid;
// Game logic
// NOTE: Don't try to send messages from here, that's the job of the associated handler
/// Internal manifest for making a new game
#[derive(Debug)]
pub struct NewGameManifest {
/// Game name
pub name: String,
/// Game host
pub host: Arc<RwLock<User>>,
/// Selected game packs
pub packs: Vec<u8>,
}
/// A processed white card for use server-side
#[derive(Debug)]
pub struct CardWhiteWithID {
/// Unique identifier
pub uuid: Uuid,
/// Card text
pub text: String,
}
/// A processed black card for use server-side
#[derive(Debug)]
pub struct CardBlackWithID {
/// Unique identifier
pub uuid: Uuid,
/// Card text
pub text: String,
/// Amount of cards to submit for judging
pub pick: u8,
}
/// A player
#[derive(Debug)]
pub struct Player {
/// Pointer to user
pub user: Arc<RwLock<User>>,
/// The player's hand
pub white: Vec<Arc<CardWhiteWithID>>,
/// The player's wins
pub black: Vec<Arc<CardBlackWithID>>,
}
/// The game object
#[derive(Debug)]
pub struct Game {
/// Game's UUID
pub uuid: Uuid,
/// The name of the game
pub name: String,
/// The host user of the game
pub host: Arc<RwLock<User>>,
/// Current card czar
pub czar: Arc<RwLock<User>>,
/// Packs selected for this game
pub packs: Vec<u8>,
/// List of players in the game
pub players: HashMap<String, Player>,
/// Black card for the current round
pub current_black: Arc<CardBlackWithID>,
/// Judge pool
pub judge_pile_meta: HashMap<Vec<String>, String>,
/// Judge pile that contains cards
judge_pile: HashMap<String, Arc<CardWhiteWithID>>,
/// White draw pile
pub white: Vec<Arc<CardWhiteWithID>>,
/// White discard pile
pub white_discard: Vec<Arc<CardWhiteWithID>>,
/// Black draw pile
pub black: Vec<Arc<CardBlackWithID>>,
// TODO: do this better
rotation: Vec<String>,
rotation_index: usize,
}
impl Game {
/// Returns a new game object
pub fn new(state: Arc<AppState>, request: NewGameManifest) -> Self {
// Build the decks
let mut white = vec![];
let mut black = vec![];
for pack_num in &request.packs {
if let Some(pack) = state.packs.official.get(&pack_num) {
if let Some(card) = &pack.white {
white.extend(card.clone())
}
if let Some(card) = &pack.black {
black.extend(card.clone())
}
} else if let Some(pack) = state.packs.unofficial.get(&pack_num) {
if let Some(card) = &pack.white {
white.extend(card.clone())
}
if let Some(card) = &pack.black {
black.extend(card.clone())
}
}
}
// Draw first black card
let current_black = black.swap_remove(
(0..black.len())
.choose(&mut thread_rng())
.expect("No black cards to draw from!"),
);
// These are at the largest size they should ever be
white.shrink_to_fit();
black.shrink_to_fit();
// Return game object
Game {
uuid: Uuid::now_v7(),
name: request.name,
host: request.host.clone(),
czar: request.host.clone(),
white,
black,
players: HashMap::new(),
current_black,
packs: request.packs.clone(),
judge_pile_meta: HashMap::new(),
judge_pile: HashMap::new(),
white_discard: vec![],
rotation: vec![], // this gets set in create_user()
rotation_index: 0,
}
}
/// Judge Game
pub fn judge_round(&mut self, request: &JudgeDecisionRequest, player_user_id: String) {
// Check if player is czar
if self.czar.read().unwrap().uuid.to_string() == player_user_id {
if let Some(winner_id) = self.judge_pile_meta.get(&request.winning_cards) {
self.end_round(winner_id.to_string())
} else {
tracing::error!("Attempted to look up nonexistent cards for judging!");
}
} else {
tracing::error!("Player is not czar!");
}
}
/// Process player move
pub fn player_move(
&mut self,
request: &PlayerMoveRequest,
player_user_id: String,
) -> Result<Option<(JudgeRound, String)>, String> {
if self.czar.read().unwrap().uuid == player_user_id {
tracing::error!("Player submitting move is czar!");
Err("You can't submit cards to judge, you ARE the judge!".to_string())
} else {
// Error if not enough cards
if request.card_ids.len() < self.current_black.pick.into() {
return Err("You didn't pick enough cards!".to_string());
}
// Ignore extra cards
let trimmed = request.card_ids[..self.current_black.pick.into()].to_vec();
// Move card from player's hand to judge pile
for id in &trimmed {
if let Some(player) = self.players.get_mut(&player_user_id) {
if let Some(index) = player
.white
.iter()
.position(|card| card.uuid.to_string() == *id)
{
let card = player.white.remove(index);
self.judge_pile.insert(card.uuid.to_string(), card);
} else {
tracing::error!("Judging: Can't get index");
}
} else {
tracing::error!("Judging: Can't find player");
}
}
// Meta for convenience
// TODO: don't do this extra work
self.judge_pile_meta.insert(trimmed, player_user_id.clone());
// Start judging if this is last player to submit
if self.judge_pile_meta.len() == self.players.len() - 1 {
for group in self.judge_pile_meta.keys() {
for card_id in group {
if !self.judge_pile.contains_key(card_id) {
tracing::error!("Trying to judge a card not in play");
return Err("Trying to judge a card not in play".to_string());
}
}
}
Ok(Some((
JudgeRound {
cards_to_judge: self
.judge_pile_meta
.keys()
.into_iter()
.map(|group| {
group
.iter()
.map(|id| {
let card = self.judge_pile.get(id).unwrap().clone();
// TODO: Make sure card exists
WhiteCardMeta {
uuid: card.uuid.to_string(),
text: card.text.clone(),
}
})
.collect()
})
.collect(),
},
self.czar.read().unwrap().uuid.clone(),
)))
} else {
// User submitted cards
// TODO: player submitted move indication
Ok(None)
}
}
}
/// Tick forward
fn end_round(&mut self, winner_id: String) {
// Top off player hands
let user_ids: Vec<String> = self.judge_pile_meta.drain().map(|pair| pair.1).collect();
for id in user_ids {
for _ in 0..self.current_black.pick.into() {
let card = self.draw_one_white().unwrap();
let player = self.players.get_mut(&id).unwrap();
player.white.push(card);
}
}
// Award card/point to winning player
self.players
.get_mut(&winner_id)
.unwrap()
.black
.push(self.current_black.clone());
// Draw new black card
self.current_black = self.draw_one_black().unwrap();
// End of round clean-up
self.white_discard.extend(
self.judge_pile
.drain()
.map(|pair| pair.1)
.collect::<Vec<Arc<CardWhiteWithID>>>(),
);
// Choose new czar
if self.rotation_index == self.rotation.len() - 1 {
self.rotation_index = 0;
} else {
self.rotation_index += 1;
}
self.czar = self
.players
.get(&self.rotation[self.rotation_index])
.unwrap()
.user
.clone();
}
/// Draw one black card at random from play deck.
fn draw_one_black(&mut self) -> Option<Arc<CardBlackWithID>> {
let deck = &mut self.black;
if let Some(index) = (0..deck.len()).choose(&mut thread_rng()) {
Some(deck.swap_remove(index))
} else {
tracing::error!("Tried to draw white card that doesn't exist!");
None
}
}
/// Draw one white card at random from play deck.
fn draw_one_white(&mut self) -> Option<Arc<CardWhiteWithID>> {
let deck = &mut self.white;
if let Some(index) = (0..deck.len()).choose(&mut thread_rng()) {
Some(deck.swap_remove(index))
} else {
tracing::error!("Tried to draw white card that doesn't exist!");
None
}
}
/// Create a new player and add them to the game.
pub fn create_player(&mut self, user: Arc<RwLock<User>>) {
// Check if player already exists
if !self.players.contains_key(&user.read().unwrap().uuid) {
// Build hand of 10 white cards
let mut white = vec![];
for _ in 0..10 {
if let Some(card) = self.draw_one_white() {
white.push(card);
}
}
// New player object
let new_player = Player {
user: user.clone(),
white,
black: vec![],
};
// Add player to game
self.players
.insert(user.read().unwrap().uuid.clone(), new_player);
// Add player to rotation
self.rotation.push(user.read().unwrap().uuid.to_string());
} else {
tracing::error!("User already has a player in this game!");
}
}
}