use crate::game::{Command, Game, Reply}; use std::cmp::Ordering; use std::collections::{HashMap, HashSet}; /// Rock, Paper, Scissors. /// A match. Considering to move this to Game. struct Match { state: HashMap>, } impl Match { fn new(h: &HashSet) -> Self { let mut n = HashMap::new(); for key in h { n.insert(key.clone(), None); } Match { state: n } } fn is_ready(&self) -> bool { self.state.values().all(|v| v.is_some()) } fn opponent(&self, s: &String) -> String { self.state .keys() .find(|k| k != &s) .expect("Getting opponent, player name not in the set.") .to_string() } // Solves the game. MUST never call this method if there aren't two plays made. fn solve(&self) -> Reply { // Our reply. let mut r = Reply::new(); // Players. let mut p = Vec::new(); // And their choices: let mut c = Vec::new(); for (i, j) in &self.state { p.push(i); c.push( j.as_ref() .expect("Trying to solve a game, a player choice was None."), ); } if c[0] == c[1] { r.push(format!( "@{} and @{} tie! You both chose {}. {}!", p[0], p[1], c[0], c[0].noise() )); } else if c[0] > c[1] { r.push(format!( "@{} is the winner! @{} lost. {} wins against {}. {}", p[0], p[1], c[0], c[1], c[0].noise() )); } else { r.push(format!( "@{} is the winner! @{} lost. {} wins against {}. {}", p[1], p[0], c[1], c[0], c[1].noise() )); } r } } /// Data structure of the game. pub struct Rps { /// A HashSet with the players waiting to play as account strings. lobby: HashSet, /// capacity determines how many people a match contains. capacity: u8, // A vector of ongoing matches. matches: Vec, // HashSet indicating for each player which match they are in. players: HashMap, } impl Game for Rps { /// Creation of a new and empty Rps game structure. fn new() -> Self { Rps { lobby: HashSet::new(), capacity: 2, matches: Vec::new(), players: HashMap::new(), } } /// State machine that accepts a command, changes state and delivers replies if required. fn next(&mut self, m: &Command) -> Reply { let mut r = Reply::new(); // The entire state depends on two factors: are we waiting to join a game, and are we playing a game? // It is possible for both to be false, and either one to be true. // If both are true, this is an error. let waiting = self.lobby.contains(&m.sender); let playing = self.players.contains_key(&m.sender); assert!(!(waiting & playing)); // It is possible for a command to be: rps, a move in the game, or cancelrps. let joining = m.content == "rps"; let quitting = m.content == "cancelrps"; let choice = match m.content.as_str() { "rock" => Some(Play::Rock), "paper" => Some(Play::Paper), "scissors" => Some(Play::Scissors), _ => None, }; // At most, one of these conditions can hold. assert!(!(joining && quitting)); assert!(!(joining && choice.is_some())); assert!(!(quitting && choice.is_some())); // At this point we have all necessary information to match. /* println!( "{}, {}, {:?}, {}, {}.", joining, quitting, choice, playing, waiting ); */ match (joining, quitting, choice, playing, waiting) { // We don't bother with the impossible cases that are already excluded by assertion. // Let's start with joining the game. // There are 3 cases we need care about: // We're joining, not playing and not waiting. (true, false, None, false, false) => { // This has two cases. // If there's nobody waiting. if self.capacity > 1 { // Put sender in the lobby. Reduce capacity. r.push(format!("@{} You've asked to join a game of Rock, Paper, Scissors. As soon as someone else wants to play, I'll send you a message so you can tell me your choice.", m.sender)); self.lobby.insert(m.sender.clone()); self.capacity -= 1; } // If Someone's waiting, start the game. else { // Put player in the lobby. This avoids us making players mut. self.lobby.insert(m.sender.clone()); // Get the playes from the lobby. let players = self.lobby.clone(); // Create this match. let this_match = Match::new(&players); // Empty the lobby. self.lobby = HashSet::new(); // Reset capacity. self.capacity = 2; // Add the match to the matches vector. self.matches.push(this_match); // Get the index. let n = self.matches.len() - 1; // Place the match index for each player. for i in players { self.players.insert(i, n); } // Prepare the replies. // Make this reply quiet, so that the move is responded in DM. r.quiet(); for i in self.matches[n].state.keys() { r.push(format!( "@{} Got a partner! Your opponent is {} Tell me your choice: *rock*, *paper*, or *scissors*?", i, self.matches[n].opponent(i) )); } } } // We're trying to join, but already in the lobby. (true, false, None, false, true) => { r.push(format!( "@{} You're already waiting for a game of Rock, Paper, Scissors. Be patient.", m.sender )); } // We're trying to join, but already playing. (true, false, None, true, false) => { r.push(format!( "@{} You're already playing a game against {} You can cancel it with *cancelrps* if you're bored of waiting.", m.sender, self.matches[*self.players.get(&m.sender).expect("Trying to get the match index for a player who sent a join command and is playing a game, but is not in the set.")].opponent(&m.sender) )); } // Now we do the two quit cases: while waiting, and while playing. // While waiting, it only affects the player. (false, true, None, false, true) => { // Remove player from lobby. self.lobby.remove(&m.sender); // Reset capacity. self.capacity = 2; // Send message. r.push(format!( "@{} You're no longer waiting for a partner. You may play again by sending *rps* any time.", m.sender)); } // While playing, it affects both players. (false, true, None, true, false) => { // Get our match index. let n = self.players.get(&m.sender).expect("Trying to get the match of a player who sent a cancel command while playing a game, but is not in the set."); // Get our opponent. let o = self.matches[*n].opponent(&m.sender); // Remove the match. self.matches.remove(*n); // Remove both players from the player list. self.players.remove(&m.sender); self.players.remove(&o); // The simplest way to do a cancellation is to tell both players. // TODO: The non-cancelling player can go to the lobby in a future version. r.push(format!( "@{} has cancelled the game with @{} You're both welcome to play again any time. Use *rps* to start a new match.", m.sender, o )); } // Now we deal with making a move. (false, false, Some(c), true, false) => { // Our name for later insertion. let name = m.sender.clone(); // Get our match index. let n = self.players.get(&name).expect("Trying to get the match of a player who made a move while playing a game, but is not in the set."); // Get our opponent. let o = self.matches[*n].opponent(&name); // If we already played: if self.matches[*n].state.get(&name).expect("Trying to get the match of a player making a move while playing a game, but is not in the set.").is_some() { // We can't play twice. r.push(format!( "@{} You already sent me your choice. You need to wait for {} If you get bored, you can send me *cancelrps* to cancel the game.", name, o )); } else { // Put our choice in the match. self.matches[*n].state.insert(name, Some(c)); // Check if the match is done. if self.matches[*n].is_ready() { // We're ready. Solve the game. r = self.matches[*n].solve(); // Clean up. self.matches.remove(*n); self.players.remove(&m.sender); self.players.remove(&o); } else { // Game isn't over, just send the message. r.push(format!( "@{} Got your move. Let's see what your opponent does.", m.sender )); } } } // And moves out of time. // When we're not in a game or waiting to play. (false, false, Some(_), false, false) => { r.push(format!("@{} You haven't joined a game yet. You can do so by sending me *rps* whenever you like.", m.sender)); } // When we're still waiting for a partner. (false, false, Some(_), false, true) => { r.push(format!("@{} You're still waiting for a partner. Be patient. If you want, you can sende me *cancelrps* to cancel the game.", m.sender)); } _ => {} // __ } r } } // Valid plays. #[derive(PartialEq, Debug)] enum Play { Rock, Paper, Scissors, } impl Play { // Silly sound effects. fn noise(&self) -> String { match self { Play::Rock => "CRUNCH!", Play::Scissors => "SNIP!", Play::Paper => "CRUMPLE!", } .to_string() } } impl PartialOrd for Play { fn partial_cmp(&self, other: &Self) -> Option { Some(match (self, other) { (x, y) if x == y => Ordering::Equal, (Play::Rock, Play::Paper) => Ordering::Less, (Play::Rock, Play::Scissors) => Ordering::Greater, (Play::Paper, Play::Rock) => Ordering::Greater, (Play::Paper, Play::Scissors) => Ordering::Less, (Play::Scissors, Play::Rock) => Ordering::Less, (Play::Scissors, Play::Paper) => Ordering::Greater, _ => Ordering::Equal, }) } } impl std::fmt::Display for Play { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Play::Rock => write!(f, "Rock"), Play::Paper => write!(f, "Paper"), Play::Scissors => write!(f, "Scissors"), } } } /// Tests. #[cfg(test)] mod test { use crate::game::{Command, Game}; use crate::rps::Rps; #[test] fn test_new() { let g = Rps::new(); assert_eq!(g.capacity, 2); assert_eq!(g.lobby.len(), 0); } #[test] fn test_nonsense() { let c: Command = Command { sender: "modulux@node.isonomia.net".to_string(), content: "nonsense".to_string(), }; let mut g = Rps::new(); assert!(g.next(&c).0.is_empty()); } #[test] fn test_first_join_game() { let mut g = Rps::new(); let c = Command { sender: "modulux@node.isonomia.net".to_string(), content: "rps".to_string(), }; let r = g.next(&c); assert_eq!(r.0.len(), 1, "Incorrect number of replies: {}.", r.0.len()); assert_eq!(r.0[0], "@modulux@node.isonomia.net You've asked to join a game of Rock, Paper, Scissors. As soon as someone else wants to play, I'll send you a message so you can tell me your choice.".to_string(), "Incorrect reply message: {}.", r.0[0]); assert_eq!( g.capacity, 1, "Capacity in lobby should be 1, is {}.", g.capacity ); } #[test] fn test_join_game_twice() { let mut g = Rps::new(); let c = Command { sender: "modulux@node.isonomia.net".to_string(), content: "rps".to_string(), }; g.next(&c); let r = g.next(&c); assert_eq!(r.0.len(), 1, "Incorrect number of replies: {:}.", r.0.len()); assert_eq!(r.0[0], "@modulux@node.isonomia.net You're already waiting for a game of Rock, Paper, Scissors. Be patient.".to_string(), "Incorrect reply message: {}.", r.0[0]); assert_eq!( g.capacity, 1, "Capacity in lobby should be 1, is {}.", g.capacity ); } #[test] fn test_join_game_complete() { let mut g = Rps::new(); let c1 = Command { sender: "modulux@node.isonomia.net".to_string(), content: "rps".to_string(), }; g.next(&c1); let c2 = Command { sender: "modulux2@node.isonomia.net".to_string(), content: "rps".to_string(), }; let r = g.next(&c2); assert_eq!(r.0.len(), 2, "Incorrect number of replies: {}.", r.0.len()); assert!(r.0.contains(&"@modulux@node.isonomia.net Got a partner! Your opponent is modulux2@node.isonomia.net Tell me your choice: *rock*, *paper*, or *scissors*?".to_string()), "Missing reply."); assert!(r.0.contains(&"@modulux2@node.isonomia.net Got a partner! Your opponent is modulux@node.isonomia.net Tell me your choice: *rock*, *paper*, or *scissors*?".to_string()), "Missing reply."); } #[test] fn test_play_twice() { let mut g = Rps::new(); let c1 = Command { sender: "modulux@node.isonomia.net".to_string(), content: "rps".to_string(), }; let c2 = Command { sender: "modulux2@node.isonomia.net".to_string(), content: "rps".to_string(), }; let c3 = Command { sender: "modulux@node.isonomia.net".to_string(), content: "rock".to_string(), }; g.next(&c1); g.next(&c2); g.next(&c3); let r = g.next(&c3); assert_eq!( r.0.len(), 1, "Incorrect number of replies. Reply: {:#?}.", r ); assert_eq!(r.0[0], "@modulux@node.isonomia.net You already sent me your choice. You need to wait for modulux2@node.isonomia.net If you get bored, you can send me *cancelrps* to cancel the game.", "Incorrect reply: {}.", r.0[0]); } #[test] fn test_play_too_early() { let mut g = Rps::new(); let c = Command { sender: "modulux@node.isonomia.net".to_string(), content: "rock".to_string(), }; let r = g.next(&c); assert_eq!(r.0.len(), 1, "Incorrect number of replies in: {:?}", r); assert_eq!(r.0[0], "@modulux@node.isonomia.net You haven't joined a game yet. You can do so by sending me *rps* whenever you like.", "{:?}", r); } #[test] fn twice_early_play() { let mut r; let mut g = Rps::new(); let c1 = Command { sender: "modulux@node.isonomia.net".to_string(), content: "rock".to_string(), }; let c2 = Command { sender: "modulux2@node.isonomia.net".to_string(), content: "rock".to_string(), }; r = g.next(&c1); assert_eq!(r.0.len(), 1, "Incorrect number of replies in: {:?}", r); assert_eq!(r.0[0], "@modulux@node.isonomia.net You haven't joined a game yet. You can do so by sending me *rps* whenever you like.", "{:?}", r); r = g.next(&c2); assert_eq!(r.0.len(), 1, "Incorrect number of replies in: {:?}", r); assert_eq!(r.0[0], "@modulux2@node.isonomia.net You haven't joined a game yet. You can do so by sending me *rps* whenever you like.", "{:?}", r); } fn command(sender: &str, content: &str) -> Command { Command { sender: sender.to_string(), content: content.to_string(), } } #[test] fn test_join_full_then_cancel() { let r; let mut g = Rps::new(); g.next(&command("modulux@node.isonomia.net", "rps")); g.next(&command("modulux2@node.isonomia.net", "rps")); r = g.next(&command("modulux@node.isonomia.net", "cancelrps")); assert_eq!(r.0.len(), 1, "Incorrect number of replies: {:?}", r); assert_eq!( r.0[0], "@modulux@node.isonomia.net has cancelled the game with @modulux2@node.isonomia.net You're both welcome to play again any time. Use *rps* to start a new match.", "Incorrect response: {}", r.0[0] ); } #[test] fn test_join_full_play_then_cancel() { let r; let mut g = Rps::new(); g.next(&command("modulux@node.isonomia.net", "rps")); g.next(&command("modulux2@node.isonomia.net", "rps")); g.next(&command("modulux@node.isonomia.net", "rock")); r = g.next(&command("modulux@node.isonomia.net", "cancelrps")); assert_eq!(r.0.len(), 1, "Incorrect length. {:?}", r); assert_eq!( r.0[0], "@modulux@node.isonomia.net has cancelled the game with @modulux2@node.isonomia.net You're both welcome to play again any time. Use *rps* to start a new match.", "Incorrect response. {}.", r.0[0] ); } }