From 0778aaab3d40dc41fc979b4c15737cf6000f0c77 Mon Sep 17 00:00:00 2001 From: mzntori <44904735+ElektroEnte@users.noreply.github.com> Date: Sat, 23 Mar 2024 19:50:45 +0100 Subject: [PATCH] uno YAAAY --- Cargo.toml | 1 + src/bot.rs | 37 ++--- src/command.rs | 8 +- tests/integration_test.rs | 3 +- tests/uno.rs | 277 ++++++++++++++++++++++++++++++++++++++ tests/uno_test.rs | 256 +++++++++++++++++++++++++++++++++++ 6 files changed, 562 insertions(+), 20 deletions(-) create mode 100644 tests/uno.rs create mode 100644 tests/uno_test.rs diff --git a/Cargo.toml b/Cargo.toml index 5207308..709f8c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,5 +6,6 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +rand = "0.9.0-alpha.1" twitch-irc = "5.0.1" tokio = { version = "1.33.0", features = ["full"] } diff --git a/src/bot.rs b/src/bot.rs index 2b812f4..dd202eb 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -32,6 +32,21 @@ pub struct Bot<'a, P> { impl<'a, P> Bot<'a, P> where P: Send + 'static { + /// Creates a `Bot` instance using the given username and oauth-token. + /// `payload` can be any type and will be wrapped to `Arc>` and passed to any command executed, + /// where the user can access the contents mutable. + pub fn new<'b>(username: &'b str, oauth_token: &'b str, payload: P) -> Bot<'a, P> + where + 'b : 'a + { + Bot { + username, + oauth_token, + payload: Arc::new(StdMutex::new(payload)), + commands: Arc::new(Mutex::new(HashMap::new())), + } + } + /// Return a message stream and a client wrapped in `Arc>` fn incoming_messages_and_client(&self) -> (IncomingMessages, ClientAM) { let login = self.username.to_owned(); @@ -48,21 +63,6 @@ impl<'a, P> Bot<'a, P> return (incoming_messages, client_am); } - /// Creates a `Bot` instance using the given username and oauth-token. - /// `payload` can be any type and will be wrapped to `Arc>` and passed to any command executed, - /// where the user can access the contents mutable. - pub fn new<'b>(username: &'b str, oauth_token: &'b str, payload: P) -> Bot<'a, P> - where - 'b : 'a - { - Bot { - username, - oauth_token, - payload: Arc::new(StdMutex::new(payload)), - commands: Arc::new(Mutex::new(HashMap::new())), - } - } - pub async fn add_command(&mut self, identifier: String, command: Box>) { let mut commands = self.commands.lock().await; commands.insert(identifier, command); @@ -73,7 +73,7 @@ impl<'a, P> Bot<'a, P> let (mut incoming_messages, client_am) = self.incoming_messages_and_client(); let commands = Arc::clone(&self.commands); let payload = Arc::clone(&self.payload); - let initial_channel = self.username.to_owned(); + let initial_channel = "daph".to_string(); let msg_processor = tokio::spawn(async move { { @@ -95,7 +95,10 @@ impl<'a, P> Bot<'a, P> ServerMessage::Pong(_) => {} ServerMessage::Privmsg(msg) => { let mut cmd = commands.lock().await; - if let Some(command) = cmd.get_mut(&msg.message_text) { + + let args: Vec = msg.message_text.split(' ').map(|s| s.to_string()).collect(); + + if let Some(command) = cmd.get_mut(args.get(0).unwrap()) { let mut queue = command.execute(Arc::clone(&payload), msg); queue.execute(Arc::clone(&client_am)).await; } diff --git a/src/command.rs b/src/command.rs index e55e4b0..cab1063 100644 --- a/src/command.rs +++ b/src/command.rs @@ -8,6 +8,10 @@ pub trait Command: Send { type CommandPayLoad; fn execute(&self, pl_am: Arc>, ctx: PrivmsgMessage) -> ClientQueue; - fn help(&self) -> String; - fn info(&self) -> String; + fn help(&self) -> String { + "help".to_string() + } + fn info(&self) -> String { + "info".to_string() + } } diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 8589def..74b4840 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -18,7 +18,8 @@ impl Command for PingCommand { let mut queue = ClientQueue::new(); let pl = pl_am.lock().unwrap(); - queue.say("mzntori".to_string(), format!("Pong! {} {}", pl.content, ctx.sender.name)); + queue.say(ctx.channel_login.to_owned(), format!("Pong! {} {}", pl.content, ctx.sender.name)); + queue.say("gaygebot".to_string(), format!("Ponged in {}", ctx.channel_login)); println!("Pong executed! {}", pl.content); queue diff --git a/tests/uno.rs b/tests/uno.rs new file mode 100644 index 0000000..ff069ab --- /dev/null +++ b/tests/uno.rs @@ -0,0 +1,277 @@ +use rand::prelude::*; + + +use std::fmt::{Display, Formatter, write}; +use std::mem; +use crate::uno::UnoState::NotStarted; + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Color { + Wild, + Blue, + Green, + Red, + Yellow, +} + + +impl Display for Color { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Color::Wild => { write!(f, "Wild") } + Color::Blue => { write!(f, "Blue") } + Color::Green => { write!(f, "Green") } + Color::Red => { write!(f, "Red") } + Color::Yellow => { write!(f, "Yellow") } + } + } +} + + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Type { + Number(usize), + DrawTwo, + DrawFour, + ChooseColor, + Reverse, + Skip, +} + + +impl Display for Type { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Type::Number(n) => { write!(f, "{}", n) } + Type::DrawTwo => { write!(f, "+2") } + Type::DrawFour => { write!(f, "+4") } + Type::ChooseColor => { write!(f, "Choose Color") } + Type::Reverse => { write!(f, "Reverse") } + Type::Skip => { write!(f, "Skip") } + } + } +} + + +#[derive(Debug, Eq, PartialEq)] +pub struct UnoCard { + typ: Type, + col: Color, +} + + +impl UnoCard { + pub fn new(typ: Type, col: Color) -> UnoCard { + UnoCard { typ, col } + } + + pub fn can_stack(&self, other: &UnoCard) -> bool { + return if self.typ == other.typ || self.col == other.col || self.col == Color::Wild || other.col == Color::Wild { + true + } else { + false + }; + } +} + +impl Default for UnoCard { + fn default() -> Self { + UnoCard::new(Type::Number(0), Color::Wild) + } +} + +impl Display for UnoCard { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{} {}", self.col, self.typ) + } +} + +#[derive(Debug)] +pub struct UnoPlayer { + pub name: String, + pub hand: Vec, +} + + +impl UnoPlayer { + pub fn new(name: String) -> Self { + UnoPlayer { name, hand: vec![] } + } + + pub fn take_hand(&mut self) -> Vec { + mem::take(&mut self.hand) + } + + pub fn give_hand(&mut self, hand: Vec) { + self.hand = hand; + } + + pub fn take_card(&mut self, idx: usize) -> UnoCard { + let mut hand = self.take_hand(); + let result = hand.remove(idx); + self.give_hand(hand); + + result + } +} + + +#[derive(Debug, Eq, PartialEq)] +pub enum UnoState { + NotStarted, + Started, + Ended, +} + + +fn new_uno_stack() -> Vec { + let mut deck: Vec = vec![]; + + for col in [Color::Blue, Color::Red, Color::Green, Color::Yellow] { + deck.push(UnoCard::new(Type::Number(0), col)); + + for n in 1..=9 { + deck.push(UnoCard::new(Type::Number(n), col)); + deck.push(UnoCard::new(Type::Number(n), col)); + } + + for typ in [Type::Reverse, Type::Skip, Type::DrawTwo] { + deck.push(UnoCard::new(typ, col)); + } + + for _ in 0..4 { + deck.push(UnoCard::new(Type::DrawFour, Color::Wild)); + deck.push(UnoCard::new(Type::ChooseColor, Color::Wild)); + } + } + + deck +} + + +#[derive(Debug)] +pub struct Uno { + card_stack: Vec, + pub players: Vec, + pub admin: String, + pub active_player: usize, + pub direction: i32, + pub draw_amount: u32, + pub state: UnoState, + pub top: UnoCard, +} + +impl Uno { + pub fn new(admin: String) -> Self { + Uno { + card_stack: new_uno_stack(), + players: vec![], + admin, + active_player: 0, + direction: 1, + draw_amount: 0, + state: UnoState::Ended, + top: UnoCard::default(), + } + } + + pub fn draw(&mut self) -> UnoCard { + self.card_stack.pop().unwrap() + } + + pub fn end(&mut self) { + self.state = UnoState::Ended; + } + + pub fn init(&mut self) { + self.state = UnoState::Started; + self.shuffle(); + + let mut card_buf: Vec = vec![]; + + for _ in 0..(7 * self.players.len()) { + card_buf.push(self.draw()); + } + + for player in self.players.iter_mut() { + for _ in 0..7 { + player.hand.push(card_buf.pop().unwrap()); + } + } + + self.top = self.draw(); + } + + pub fn join(&mut self, name: String) { + if self.state == UnoState::NotStarted { + self.players.push(UnoPlayer::new(name)); + } + } + + pub fn kick(&mut self, name: String) { + let mut kick_idx: usize = usize::MAX; + + for (i, p) in self.players.iter_mut().enumerate() { + if p.name == name { + let mut hand = p.take_hand(); + self.card_stack.append(&mut hand); + kick_idx = i; + + if i < self.active_player { + self.active_player -= 1; + } + } + } + + if kick_idx < self.players.len() { + self.players.remove(kick_idx); + } + self.shuffle(); + } + + pub fn next_player(&mut self) { + let mult: i32 = match self.top.typ { + Type::Skip => { 2 } + _ => { 1 } + }; + + self.active_player = (self.active_player + self.players.len() + (self.direction * mult) as usize) % self.players.len(); + } + + pub fn place(&mut self, card: UnoCard) { + self.card_stack.push(mem::take(&mut self.top)); + self.top = card; + self.shuffle(); + + match self.top.typ { + Type::Number(_) => { self.draw_amount = 0; } + Type::DrawTwo => { self.draw_amount += 2; } + Type::DrawFour => { self.draw_amount += 4; } + Type::ChooseColor => { self.draw_amount = 0; } + Type::Reverse => { + self.direction *= -1; + self.draw_amount = 0; + } + Type::Skip => { self.draw_amount = 0; } + } + + self.next_player(); + } + + pub fn shuffle(&mut self) { + let mut rng = thread_rng(); + self.card_stack.shuffle(&mut rng); + } + + pub fn reset(&mut self, admin: String) { + self.admin = admin; + self.players = vec![]; + self.card_stack = new_uno_stack(); + self.active_player = 0; + self.direction = 1; + self.draw_amount = 0; + self.state = NotStarted; + self.top = UnoCard::default(); + } +} + diff --git a/tests/uno_test.rs b/tests/uno_test.rs new file mode 100644 index 0000000..6f20dc8 --- /dev/null +++ b/tests/uno_test.rs @@ -0,0 +1,256 @@ +pub mod uno; + +use std::sync::{Arc, Mutex as StdMutex}; +use twitch_irc::message::PrivmsgMessage; + +use twitchbot_rs::{Bot, ClientQueue, Command, Context}; +use crate::uno::{Uno, UnoPlayer, UnoState}; +use crate::uno::UnoState::Ended; + + +pub struct PingCommand; + +impl Command for PingCommand { + type CommandPayLoad = Uno; + + fn execute(&self, pl_am: Arc>, ctx: Context) -> ClientQueue { + let mut queue = ClientQueue::new(); + + let pl = pl_am.lock().unwrap(); + queue.say(ctx.channel_login.to_owned(), format!("@{} Pong!", ctx.sender.name)); + + queue + } +} + + +pub struct UnoCommand; + +impl Command for UnoCommand { + type CommandPayLoad = Uno; + + fn execute(&self, pl_am: Arc>, ctx: PrivmsgMessage) -> ClientQueue { + let mut queue = ClientQueue::new(); + + let mut pl = pl_am.lock().unwrap(); + if pl.state == Ended { + pl.reset(ctx.sender.name.clone()); + queue.me(ctx.channel_login, format!("{} started a new uno game, type !join to join.", ctx.sender.name)); + } else { + queue.me(ctx.channel_login, "Another game going on rn".to_string()); + } + + queue + } +} + + +pub struct DrawCommand; + +impl Command for DrawCommand { + type CommandPayLoad = Uno; + + fn execute(&self, pl_am: Arc>, ctx: PrivmsgMessage) -> ClientQueue { + let mut queue = ClientQueue::new(); + let args: Vec = ctx.message_text.split(' ').map(|s| s.to_string()).collect(); + + let mut pl = pl_am.lock().unwrap(); + + let idx = pl.active_player; + if pl.players.get_mut(idx).unwrap().name == ctx.sender.name && args.len() > 1 { + let amount = args + .get(1) + .unwrap() + .parse::() + .unwrap_or(0); + + for _ in 0..amount { + let card = pl.draw(); + pl.players.get_mut(idx).unwrap().hand.push(card); + } + + queue.me(ctx.channel_login, format!("{} drew {} cards.", ctx.sender.name, amount)); + } else { + queue.me(ctx.channel_login, "Sender probably not active player.".to_string()) + } + + queue + } +} + + +pub struct PlaceCommand; + +impl Command for PlaceCommand { + type CommandPayLoad = Uno; + + fn execute(&self, pl_am: Arc>, ctx: PrivmsgMessage) -> ClientQueue { + let mut queue = ClientQueue::new(); + let args: Vec = ctx.message_text.split(' ').map(|s| s.to_string()).collect(); + + let mut pl = pl_am.lock().unwrap(); + + let idx = pl.active_player; + if pl.players.get_mut(idx).unwrap().name == ctx.sender.name && args.len() > 1 { + let hand_idx = args + .get(1) + .unwrap() + .parse::() + .unwrap_or(0); + + if hand_idx < pl.players.get_mut(idx).unwrap().hand.len() { + let card = pl.players.get_mut(idx).unwrap().take_card(hand_idx); + + if card.can_stack(&pl.top) { + queue.me(ctx.channel_login, format!("{} placed {}", pl.players.get_mut(idx).unwrap().name, &card)); + pl.place(card); + } else { + queue.me(ctx.channel_login, "Cant stack on that card.".to_string()) + } + + } else { + queue.me(ctx.channel_login, "Hand index out of range".to_string()); + } + } else { + queue.me(ctx.channel_login, "Sender probably not active player.".to_string()) + } + + queue + } +} + + +pub struct JoinCommand; + +impl Command for JoinCommand { + type CommandPayLoad = Uno; + + fn execute(&self, pl_am: Arc>, ctx: PrivmsgMessage) -> ClientQueue { + let mut queue = ClientQueue::new(); + + let mut pl = pl_am.lock().unwrap(); + + if pl.state == UnoState::NotStarted { + pl.players.push(UnoPlayer::new(ctx.sender.name.clone())); + queue.me(ctx.channel_login, format!("{} Joined the game.", ctx.sender.name)); + } + + queue + } +} + + +pub struct StartCommand; + +impl Command for StartCommand { + type CommandPayLoad = Uno; + + fn execute(&self, pl_am: Arc>, ctx: PrivmsgMessage) -> ClientQueue { + let mut queue = ClientQueue::new(); + + let mut pl = pl_am.lock().unwrap(); + if pl.admin == ctx.sender.name { + pl.init(); + queue.me(ctx.channel_login, "Game started!".to_string()); + } + + queue + } +} + + +pub struct EndCommand; + +impl Command for EndCommand { + type CommandPayLoad = Uno; + + fn execute(&self, pl_am: Arc>, ctx: PrivmsgMessage) -> ClientQueue { + let mut queue = ClientQueue::new(); + + let mut pl = pl_am.lock().unwrap(); + if pl.admin == ctx.sender.name { + pl.end(); + queue.me(ctx.channel_login, "Game ended!".to_string()); + } + + queue + } +} + +pub struct HandCommand; + +impl Command for HandCommand { + type CommandPayLoad = Uno; + + fn execute(&self, pl_am: Arc>, ctx: PrivmsgMessage) -> ClientQueue { + let mut queue = ClientQueue::new(); + let args: Vec = ctx.message_text.split(' ').map(|s| s.to_string()).collect(); + + let mut pl = pl_am.lock().unwrap(); + + let mut index = usize::MAX; + + for (i, player) in pl.players.iter().enumerate() { + if player.name == ctx.sender.name { + index = i; + } + } + + let mut card_list = String::new(); + for card in pl.players.get(index).unwrap().hand.iter().enumerate() { + card_list.push_str(format!("{}: {}, ", card.0, card.1).as_str()); + } + + let channel: String = if ctx.sender.name == "daph" { + "daphbot".to_string() + } else { + ctx.sender.name + }; + + queue.say(channel, card_list); + + queue + } +} + + +pub struct TopCommand; + +impl Command for TopCommand { + type CommandPayLoad = Uno; + + fn execute(&self, pl_am: Arc>, ctx: PrivmsgMessage) -> ClientQueue { + let mut queue = ClientQueue::new(); + + let mut pl = pl_am.lock().unwrap(); + + queue.me(ctx.channel_login, format!("Top: {}", &pl.top)); + + queue + } +} + +#[tokio::test] +async fn main() { + let mut u = Uno::new("mzntori".to_string()); + + let login = std::env::var("LOGIN").unwrap(); + let oauth = std::env::var("OAUTH").unwrap(); + let mut bot = Bot::new( + login.as_str(), + oauth.as_str(), + u, + ); + + bot.add_command("!ping".to_owned(), Box::new(PingCommand {})).await; + bot.add_command("!uno".to_owned(), Box::new(UnoCommand {})).await; + bot.add_command("!join".to_owned(), Box::new(JoinCommand {})).await; + bot.add_command("!draw".to_owned(), Box::new(DrawCommand {})).await; + bot.add_command("!start".to_owned(), Box::new(StartCommand {})).await; + bot.add_command("!end".to_owned(), Box::new(EndCommand {})).await; + bot.add_command("!place".to_owned(), Box::new(PlaceCommand {})).await; + bot.add_command("!hand".to_owned(), Box::new(HandCommand {})).await; + bot.add_command("!top".to_owned(), Box::new(TopCommand {})).await; + + bot.run().await; +} \ No newline at end of file