diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/conn.rs | 62 | ||||
-rw-r--r-- | src/ircbot.rs | 79 | ||||
-rw-r--r-- | src/main.rs | 37 | ||||
-rw-r--r-- | src/minesweeper.rs | 42 | ||||
-rw-r--r-- | src/types.rs | 5 |
5 files changed, 175 insertions, 50 deletions
diff --git a/src/conn.rs b/src/conn.rs index 2b6ee80..c8a0a82 100644 --- a/src/conn.rs +++ b/src/conn.rs @@ -7,6 +7,7 @@ use tokio::sync::RwLock; use futures::{SinkExt, TryStreamExt, StreamExt, stream::SplitStream}; use warp::ws::{ WebSocket, Message }; use crate::livepos; +use crate::ircbot; pub async fn setup_conn(socket: WebSocket, addr: SocketAddr, rinfo: (RoomId,Arc<RwLock<Room>>), max_in: usize) { let (room_id, room) = rinfo; @@ -61,9 +62,9 @@ pub async fn setup_conn(socket: WebSocket, addr: SocketAddr, rinfo: (RoomId,Arc< pub async fn drive_conn(conn: (Conn, SplitStream<WebSocket>), rinfo: (RoomId, Arc<RwLock<Room>>), max_in: usize) { let (conn, mut incoming) = conn; let (room_id, room) = rinfo; - let (players, cmd_tx, pos_tx, room_conf) = { + let (players, cmd_tx, pos_tx, irc_tx, room_conf) = { let room = room.read().await; - (room.players.clone(), room.cmd_stream.clone(), room.pos_stream.clone(), room.conf.clone()) + (room.players.clone(), room.cmd_stream.clone(), room.pos_stream.clone(), room.irc_stream.clone(), room.conf.clone()) }; while let Ok(cmd) = incoming.try_next().await { if let Some(cmd) = cmd { @@ -144,33 +145,42 @@ pub async fn drive_conn(conn: (Conn, SplitStream<WebSocket>), rinfo: (RoomId, Ar if n.is_empty() { def } else { n } } }; - println!("{room_id} I: registered \"{name}@{}\"", conn.addr); - drop(players_lock); - let uid = { - // new scope cuz paranoid bout deadlocks - room.write().await.players.write().await.insert_conn(conn.clone(), name.clone(), clr) - }; - let players_lock = players.read().await; - let me = players_lock.get(&conn.addr).unwrap(); - conn.tx.send(Message::text(format!("regack {} {} {} {}", - room_conf.name.replace(' ', " "), name.replace(' ', " "), uid, room_conf.board_conf)) - ).expect("couldn't send register ack"); - { - let msg = Message::text(format!("players {}", - jsonenc_players(players_lock.values()) - .expect("couldn't JSONify players"))); - for p in players_lock.values() { - if let Err(e) = p.conn.tx.send(msg.clone()) { - println!("{room_id} E: couldn't dump players for {me}: {e}"); + let (nameq_tx, nameq_rx) = tokio::sync::oneshot::channel(); + irc_tx.send(ircbot::IrcCmd::NameTakenQuery(name.clone(), nameq_tx)).expect("couldn't check for name collision"); + + if nameq_rx.await.unwrap() { + println!("{room_id} I: name collision \"{name}@{}\"", conn.addr); + conn.tx.send(Message::text("namecoll")).expect("couldn't send name collision report"); + } else { + println!("{room_id} I: registered \"{name}@{}\"", conn.addr); + drop(players_lock); + let uid = { + // new scope cuz paranoid bout deadlocks + room.write().await.players.write().await.insert_conn(conn.clone(), name.clone(), clr) + }; + let players_lock = players.read().await; + let me = players_lock.get(&conn.addr).unwrap(); + conn.tx.send(Message::text(format!("regack {} {} {} {}", + room_conf.name.replace(' ', " "), name.replace(' ', " "), uid, room_conf.board_conf)) + ).expect("couldn't send register ack"); + + { + let msg = Message::text(format!("players {}", + jsonenc_players(players_lock.values()) + .expect("couldn't JSONify players"))); + for p in players_lock.values() { + if let Err(e) = p.conn.tx.send(msg.clone()) { + println!("{room_id} E: couldn't dump players for {me}: {e}"); + } } } - } - if let Err(e) = pos_tx.send(livepos::Req { id: uid, data: livepos::ReqData::StateDump }) { - println!("{room_id} E: couldn't request position dump for {me}: {e}"); - } - if let Err(e) = cmd_tx.send(MetaMove::StateDump) { - println!("{room_id} E: couldn't request game dump for {me}: {e}"); + if let Err(e) = pos_tx.send(livepos::Req { id: uid, data: livepos::ReqData::StateDump }) { + println!("{room_id} E: couldn't request position dump for {me}: {e}"); + } + if let Err(e) = cmd_tx.send(MetaMove::StateDump) { + println!("{room_id} E: couldn't request game dump for {me}: {e}"); + } } } } diff --git a/src/ircbot.rs b/src/ircbot.rs new file mode 100644 index 0000000..b61e951 --- /dev/null +++ b/src/ircbot.rs @@ -0,0 +1,79 @@ +//use irc::client::prelude::*; +use crate::types::{RoomConf, CmdTx}; +use tokio::sync::mpsc as tokio_mpsc; +use serde::Deserialize; +//use futures::prelude::*; + +#[derive(Debug)] +pub enum IrcCmd { + NameTakenQuery(String, tokio::sync::oneshot::Sender<bool>), + GameWin(String), + GameLose(String), +} + +pub type IrcCmdTx = tokio_mpsc::UnboundedSender<IrcCmd>; + +#[derive(Deserialize, Clone)] +pub struct IrcConf { + pub server: String, + pub port: u16, +} + +pub async fn manage_irc_channel(_irc_conf: IrcConf, _room_conf: RoomConf, _game_tx: CmdTx, mut irc_rx: tokio_mpsc::UnboundedReceiver<IrcCmd>) { + // turns out none of the irc libs i tried worked and i lost interest + // + // let channel_name = format!("#mines-{}", room_conf.name); + // let bot_name = format!("mines-bot-{}", room_conf.name); + // let config = Config { + // nickname: Some(bot_name.clone()), + // username: Some(bot_name.clone()), + // realname: Some(bot_name.clone()), + // server: Some(irc_conf.server), + // port: Some(irc_conf.port), + // encoding: Some("UTF-8".to_string()), + // channels: vec![channel_name.clone()], + // umodes: Some("+B-x".to_string()), + // user_info: Some("websweeper channel manager bot".to_string()), + // use_tls: Some(true), + // ping_time: Some(20), + // ping_timeout: Some(15), + // ..Default::default() + // }; + + // let mut client = Client::from_config(config).await.expect("couldn't create an irc client"); + // client.identify().expect("couldn't identify irc bot"); + + // println!("irc bot {:#?}", client); + + // if !room_conf.public { + // client.send_mode(&channel_name, &[Mode::Plus(ChannelMode::Secret, None)]).expect("couldn't set irc channel mode"); + // } + // client.send_mode(&channel_name, + // &[Mode::Plus(ChannelMode::Limit, Some(room_conf.player_cap.to_string()))] + // ).expect("couldn't set irc channel mode"); + + while let Some(req) = irc_rx.recv().await { + match req { + IrcCmd::NameTakenQuery(_nick, res_tx) => { + // let taken: bool = client.list_users(&channel_name) + // .and_then(|userlist| { + // userlist.iter().position(|u| u.get_nickname() == nick) + // }).is_some(); + // res_tx.send(taken).unwrap(); + res_tx.send(false).unwrap(); + }, + IrcCmd::GameWin(_nick) => { + // println!("irc {nick} win"); + // if let Err(e) = client.send(Command::PRIVMSG(channel_name.clone(), format!("You win! {nick} made the winning move."))) { + // println!("couldn't send irc win message: {e}"); + // } + }, + IrcCmd::GameLose(_nick) => { + // println!("irc {nick} lose"); + // if let Err(e) = client.send(Command::PRIVMSG(channel_name.clone(), format!("You win! {nick} made the winning move."))) { + // println!("couldn't send irc lose message: {e}"); + // } + }, + } + } +} diff --git a/src/main.rs b/src/main.rs index 0c81844..29a847c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,7 @@ mod types; mod livepos; mod conn; mod minesweeper; +mod ircbot; use types::*; const CONF_FILE: &str = "./conf.json"; @@ -42,6 +43,7 @@ struct Conf { pub paths: ConfPaths, pub server: ConfServer, pub limits: ConfLimits, + pub irc: ircbot::IrcConf, } fn main() -> Result<(), Box<dyn Error>> { @@ -215,7 +217,7 @@ async fn tokio_main(conf: Conf) -> Result<(), Box<dyn Error>> { // If a move is made, broadcast new board, else just send current board type MoveStreamHandles = (tokio::sync::mpsc::UnboundedSender<MetaMove>, tokio::sync::mpsc::UnboundedReceiver<MetaMove>); -async fn gameloop(moves: MoveStreamHandles, players: Arc<RwLock<PlayerMap>>, bconf: minesweeper::BoardConf) { +async fn gameloop(moves: MoveStreamHandles, irc_tx: ircbot::IrcCmdTx, players: Arc<RwLock<PlayerMap>>, bconf: minesweeper::BoardConf) { // FIXME: push new board if and only if there aren't any remaining commands in the queue use minesweeper::*; use flate2::{ Compression, write::DeflateEncoder }; @@ -245,10 +247,20 @@ async fn gameloop(moves: MoveStreamHandles, players: Arc<RwLock<PlayerMap>>, bco board_encoder.write_all(&game.board.render()).unwrap(); let compressed_board = board_encoder.finish().unwrap(); let mut reply = vec![Message::binary(compressed_board)]; - let lpname = final_player_name.as_deref().unwrap_or("unknown player").replace(' ', " "); + let lpname = final_player_name.as_deref().unwrap_or("unknown player"); match game.phase { - Phase::Win => { reply.push(Message::text(format!("win {lpname}"))); }, - Phase::Die => { reply.push(Message::text(format!("lose {lpname}"))); }, + Phase::Win => { + reply.push(Message::text(format!("win {lpname}"))); + if let Err(e) = irc_tx.send(ircbot::IrcCmd::GameWin(lpname.to_string())) { + println!("couldn't send irc win message: {e}"); + } + }, + Phase::Die => { + reply.push(Message::text(format!("lose {lpname}"))); + if let Err(e) = irc_tx.send(ircbot::IrcCmd::GameLose(lpname.to_string())) { + println!("couldn't send irc lose message: {e}"); + } + }, _ => (), } let peers = players.read().await; @@ -336,18 +348,21 @@ fn room_from_form(uid: RoomId, rinfo: &HashMap<String,String>, conf: &Conf) -> R let players = Arc::new(RwLock::new(PlayerMap::default())); - let (cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel(); - let game_handle = tokio::spawn(gameloop((cmd_tx.clone(), cmd_rx), players.clone(), board_conf)); - - let (pos_tx, pos_rx) = tokio::sync::mpsc::unbounded_channel(); - let livepos_handle = tokio::spawn(livepos::livepos(players.clone(), pos_rx)); - let room_conf = RoomConf { name, player_cap: limit, public, board_conf, }; + + let (cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel(); + let (irc_tx, irc_rx) = tokio::sync::mpsc::unbounded_channel(); + let (pos_tx, pos_rx) = tokio::sync::mpsc::unbounded_channel(); + + let irc_handle = tokio::spawn(ircbot::manage_irc_channel(conf.irc.clone(), room_conf.clone(), cmd_tx.clone(), irc_rx)); + let game_handle = tokio::spawn(gameloop((cmd_tx.clone(), cmd_rx), irc_tx.clone(), players.clone(), board_conf)); + let livepos_handle = tokio::spawn(livepos::livepos(players.clone(), pos_rx)); + Ok((Room { conf: room_conf, players, @@ -355,6 +370,8 @@ fn room_from_form(uid: RoomId, rinfo: &HashMap<String,String>, conf: &Conf) -> R cmd_stream: cmd_tx, livepos_driver: livepos_handle, pos_stream: pos_tx, + irc_driver: irc_handle, + irc_stream: irc_tx, }, public)) } else { Err(warp::reject::custom(BadFormData)) } } diff --git a/src/minesweeper.rs b/src/minesweeper.rs index 1e95f0d..c733d19 100644 --- a/src/minesweeper.rs +++ b/src/minesweeper.rs @@ -7,10 +7,13 @@ use serde::Serialize; const HIDDEN_BIT: u8 = 1 << 7; pub const FLAGGED_BIT: u8 = 1 << 6; -const CORRECT_BIT: u8 = 1 << 5; // grading for a rightly flagged mine +const SPECIAL_BIT: u8 = 1 << 5; // grading for a rightly flagged mine, or the question flag // all the bits that aren't flags -const TILE_NUMBITS: u8 = !(HIDDEN_BIT | FLAGGED_BIT | CORRECT_BIT); -const MINED: u8 = HIDDEN_BIT | TILE_NUMBITS; +const NUMBITS: u8 = !(HIDDEN_BIT | FLAGGED_BIT | SPECIAL_BIT); +const MINED: u8 = HIDDEN_BIT | NUMBITS; +const QUESTION: u8 = FLAGGED_BIT | SPECIAL_BIT; +const CORRECT: u8 = MINED | SPECIAL_BIT; + #[derive(PartialEq)] pub enum Phase { SafeFirstMove, @@ -182,7 +185,9 @@ impl Board { while let Some(pos) = queue.pop() { let off = pos.rel_offset_unchecked(&self); let c = &mut self.data[off]; - if *c & HIDDEN_BIT > 0 { + // don't reveal the already revealed or the flagged, but reveal the questionings + let unrevealable = (*c & FLAGGED_BIT > 0) ^ (*c & SPECIAL_BIT > 0); + if *c & HIDDEN_BIT > 0 && !unrevealable { *c = unhide(*c); self.hidden_tiles -= 1; if is_mine(*c) { return true; } @@ -198,7 +203,7 @@ impl Board { if 1 <= count && count <= 8 { let mut neighs = self.neighs(pos); let total_neighs = neighs.len(); - neighs.retain(|pos| self.data[pos.rel_offset_unchecked(&self)] & FLAGGED_BIT == 0); + neighs.retain(|pos| self.data[pos.rel_offset_unchecked(&self)] & (FLAGGED_BIT | SPECIAL_BIT) != FLAGGED_BIT); if (total_neighs - neighs.len()) == count { for pos in neighs.iter() { if self.flood_reveal(*pos) { @@ -223,14 +228,23 @@ impl Board { pub fn grade(&mut self) { for i in &mut self.data { - if *i == TILE_NUMBITS | FLAGGED_BIT | HIDDEN_BIT { - *i |= CORRECT_BIT; + if *i == MINED | FLAGGED_BIT { + *i = CORRECT; } } } pub fn flag(&mut self, pos: BoardPos) { if let Some(off) = pos.rel_offset(&self) { - self.data[off] ^= FLAGGED_BIT; + const TOPBIT_MASK: u8 = !(NUMBITS | HIDDEN_BIT); + let c = &mut self.data[off]; + if *c & HIDDEN_BIT > 0 { + let new_topbits = match *c & (TOPBIT_MASK) { + FLAGGED_BIT => QUESTION, + QUESTION => 0, + _ => FLAGGED_BIT, + } | HIDDEN_BIT; + *c = (*c & NUMBITS) | new_topbits; + } } } @@ -240,13 +254,15 @@ impl Board { for x in 0..self.width.get() { let pos: BoardPos = (x,y).try_into().unwrap(); let c = &self.data[pos.rel_offset_unchecked(&self)]; + const QUESTION_MASK: u8 = SPECIAL_BIT | FLAGGED_BIT; match *c { 0 => ret.push(b' '), _ if *c <= 8 => ret.push(b'0' + c), - _ if (*c & CORRECT_BIT) > 0 => ret.push(b'C'), + _ if (*c & QUESTION_MASK) == QUESTION_MASK => ret.push(b'Q'), + _ if (*c & SPECIAL_BIT) > 0 => ret.push(b'C'), _ if (*c & FLAGGED_BIT) > 0 => ret.push(b'F'), _ if (*c & HIDDEN_BIT) > 0 => ret.push(b'#'), - _ if *c == TILE_NUMBITS => ret.push(b'O'), + _ if *c == NUMBITS => ret.push(b'O'), _ => ret.push(b'?'), } } @@ -268,7 +284,7 @@ impl Board { let vacant_pos = { let v = self.data.iter() .enumerate() - .filter(|(_,val)| (*val & TILE_NUMBITS) != TILE_NUMBITS) + .filter(|(_,val)| (*val & NUMBITS) != NUMBITS) .map(|(p,_)| p) .next() .unwrap(); // there must be at least one @@ -315,10 +331,10 @@ impl<T: TryInto<u32>> TryFrom<(T,T)> for BoardPos { } pub fn is_mine(v: u8) -> bool { - (v & TILE_NUMBITS) == TILE_NUMBITS + (v & NUMBITS) == NUMBITS } pub fn unhide(tile: u8) -> u8 { - tile & !(HIDDEN_BIT | FLAGGED_BIT) + tile & NUMBITS } diff --git a/src/types.rs b/src/types.rs index a8a7742..b2bc93e 100644 --- a/src/types.rs +++ b/src/types.rs @@ -4,7 +4,7 @@ use std::{ sync::{ Arc, atomic::{ AtomicUsize, Ordering }, - }, +}, fmt::Display, ops::{ Deref, DerefMut }, num::NonZeroUsize, @@ -14,6 +14,7 @@ use tokio::sync::RwLock; use serde::Serialize; use crate::minesweeper; use crate::livepos; +use crate::ircbot; #[derive(Debug, Serialize, Clone)] pub struct RoomConf { @@ -30,6 +31,8 @@ pub struct Room { pub cmd_stream: CmdTx, pub livepos_driver: tokio::task::JoinHandle<()>, pub pos_stream: tokio::sync::mpsc::UnboundedSender<livepos::Req>, + pub irc_driver: tokio::task::JoinHandle<()>, + pub irc_stream: tokio::sync::mpsc::UnboundedSender<ircbot::IrcCmd>, } #[derive(Debug)] |