summaryrefslogtreecommitdiff
path: root/src/main.rs
diff options
context:
space:
mode:
authorstale <redkugelblitzin@gmail.com>2022-04-30 04:44:08 -0300
committerstale <redkugelblitzin@gmail.com>2022-04-30 04:44:08 -0300
commit9987d7abf6acd647b4a3a00c20d15f716dce653c (patch)
tree1d87a1024c67c0aa67d83b96414ade735b53cb95 /src/main.rs
parentf8deba936e62e5c9b4f7487f656c92c253427d42 (diff)
we finally got moving cursors heck yeah
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs447
1 files changed, 178 insertions, 269 deletions
diff --git a/src/main.rs b/src/main.rs
index b19eafe..28a10ed 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,294 +1,203 @@
-use std::{ io, io::Read };
+use std::{
+ error::Error,
+ sync::{ Arc, atomic::{ self, AtomicUsize }},
+ net::SocketAddr,
+ convert::Infallible,
+ collections::HashMap,
+};
-mod minesweeper {
- use std::convert::TryFrom;
- use rand::{ thread_rng, Rng, distributions::Uniform };
+mod minesweeper;
+use minesweeper::*;
+//use std::convert::{ TryFrom, TryInto };
- 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 MINE_VAL: u8 = !(HIDDEN_BIT | FLAGGED_BIT | CORRECT_BIT);
- const NEIGH_OFFS: &[(isize,isize)] = &[
- (-1,-1),(0,-1),(1,-1),
- (-1, 0), (1, 0),
- (-1, 1),(0, 1),(1, 1),
- ];
- #[derive(PartialEq)]
- pub enum Phase {
- SafeFirstMove,
- FirstMoveFail,
- Run,
- Die,
- Win,
- Leave,
- }
- pub struct Game {
- pub phase: Phase,
- pub board: Board,
- pub mine_count: usize,
- }
- pub struct Board {
- pub data: Vec<u8>,
- pub width: usize,
- pub height: usize,
- pub hidden_tiles: usize,
- }
- pub enum MoveType {
- Reveal,
- ToggleFlag,
- }
- pub struct Move {
- pub t: MoveType,
- pub pos: (usize,usize),
- }
- pub struct MoveResult(pub Board, pub bool);
- impl Game {
- pub fn new(board: Board, mine_count: usize) -> Self {
- let mut g = Game { phase: Phase::SafeFirstMove, board, mine_count };
- g.board = g.board.spread_mines(mine_count);
- g
- }
- pub fn act(mut self, m: Move) -> Self {
- let lost_phase = | phase | {
- match phase {
- Phase::SafeFirstMove => Phase::FirstMoveFail,
- Phase::Run => Phase::Die,
- _ => unreachable!(),
- }
- };
+use hyper::{ Method, StatusCode, Body, Request, Response, Server };
+use hyper::service::{make_service_fn, service_fn};
+use tokio::sync::{
+ RwLock,
+ mpsc,
+};
- match m.t {
- MoveType::Reveal => {
- let kaboom: bool;
- self.board = {
- let mr = self.board.reveal(m.pos);
- kaboom = mr.1;
- mr.0
- };
- if kaboom { self.phase = lost_phase(self.phase) }
- if self.phase == Phase::SafeFirstMove { self.phase = Phase::Run }
- },
- MoveType::ToggleFlag => self.board = self.board.flag(m.pos).0,
- };
+type HtmlResult = Result<Response<Body>, Response<Body>>;
+use futures_channel::mpsc::{unbounded, UnboundedSender};
+use futures_util::{future, pin_mut, stream::TryStreamExt, StreamExt, sink::SinkExt};
- if self.board.hidden_tiles == self.mine_count {
- self.phase = Phase::Win;
- }
- if self.phase == Phase::FirstMoveFail {
- self.board = Board::new(self.board.width, self.board.height);
- self.mine_count -= 1;
- self.board = self.board.spread_mines(self.mine_count);
- self.phase = Phase::SafeFirstMove;
- self = self.act(m);
- }
- self
- }
- }
- impl Board {
- pub fn new(w: usize, h: usize) -> Self {
- Board {
- data: [HIDDEN_BIT].repeat(w*h),
- width: w,
- height: h,
- hidden_tiles: w*h,
- }
- }
- pub fn spread_mines(mut self, count: usize) -> Self {
- let mut rng = thread_rng();
- let mut c = count;
- let w = self.width;
- let h = self.height;
- while c > 0 {
- let randpos: (usize, usize) = (rng.sample(Uniform::new(0,w-1)), rng.sample(Uniform::new(0,h-1)));
- let o = self.pos_to_off(randpos);
- if self.data[o] == MINE_VAL | HIDDEN_BIT { continue }
- else {
- self.data[o] |= MINE_VAL;
- c -= 1;
- for (nx,ny) in NEIGH_OFFS {
- let x = randpos.0;
- let y = randpos.1;
- let nxc = usize::try_from(isize::try_from(x).unwrap() + nx);
- let nyc = usize::try_from(isize::try_from(y).unwrap() + ny);
- let _ = nxc.and_then(|nx: usize| { nyc.and_then(|ny: usize| {
- if nx > w - 1 || ny > h - 1 { return usize::try_from(-1) };
- let off = self.pos_to_off((nx,ny));
- let c = &mut self.data[off];
- if *c != HIDDEN_BIT | MINE_VAL {
- *c += 1;
- }
- Ok(0)
- })
- });
- }
- }
- }
- self
- }
- pub fn pos_to_off(&self, pos: (usize, usize)) -> usize {
- pos.0 + pos.1 * self.width
- }
- pub fn flood_reveal(&mut self, pos: (usize, usize)) {
- if pos.0 > self.width || pos.1 > self.height { return; }
- let off = self.pos_to_off(pos);
- let c = &mut self.data[off];
- if *c & HIDDEN_BIT > 0 {
- *c &= !(HIDDEN_BIT | FLAGGED_BIT);
- self.hidden_tiles -= 1;
- if *c > 0 { return }
- drop(c);
- for (ox,oy) in NEIGH_OFFS {
- let nxr = usize::try_from(isize::try_from(pos.0).unwrap() + ox);
- let nyr = usize::try_from(isize::try_from(pos.1).unwrap() + oy);
- let _ = nxr.and_then(|nx: usize| { nyr.and_then(|ny: usize| {
- if nx > self.width - 1 || ny > self.height - 1 { return usize::try_from(-1) };
- self.flood_reveal((nx, ny));
- Ok(0)
- })
- });
+use tokio::net::{TcpListener, TcpStream};
+use tokio::fs;
+use tokio_tungstenite::tungstenite::protocol::Message;
+
+type Tx = UnboundedSender<Message>;
+type MovReqTx = mpsc::UnboundedSender<BoardRequest>;
+type PeerMap = Arc<RwLock<HashMap<SocketAddr, (Tx, usize, String, (usize, usize))>>>;
+type PeerInfo = (PeerMap, Arc::<AtomicUsize>);
+
+// Which will either perform a move, or simply request the current board
+type BoardRequest = Option<Move>;
+
+const PAGE_RELPATH: &str = "./page.html";
+const FONT_FILE_FUCKIT: &[u8] = include_bytes!("./VT323-Regular.ttf");
+
+#[tokio::main]
+async fn main() {
+ let sequential_id = Arc::new(AtomicUsize::new(0));
+ let peers = PeerMap::new(RwLock::new(HashMap::new()));
+ let peer_info: PeerInfo = (peers.clone(), sequential_id.clone());
+ let http_addr = SocketAddr::from(([0, 0, 0, 0], 31235));
+ println!("Http on {}", http_addr);
+
+ let (cmd_tx, cmd_rx) = mpsc::unbounded_channel();
+ let game_t = tokio::spawn(gameloop(cmd_rx, peers.clone()));
+ // need to await this one at some point
+ let conn_l = tokio::spawn(conn_listener(peer_info.clone(), cmd_tx.clone()));
+
+ let http_serv = make_service_fn(|_| {
+ async move {
+ Ok::<_, Infallible>(service_fn(move |req: Request<Body>| {
+ async move {
+ Ok::<_,Infallible>(match handle_http_req(req).await {
+ Ok(r) => r,
+ Err(r) => r,
+ })
}
- }
+ }))
}
- pub fn reveal(mut self, pos: (usize, usize)) -> MoveResult {
- if pos.0 > self.width - 1 || pos.1 > self.height - 1 { panic!("OOB reveal"); }
- let off = self.pos_to_off(pos);
- self.flood_reveal(pos);
- let c = self.data[off];
- MoveResult { 0: self, 1: (c & !(FLAGGED_BIT | CORRECT_BIT)) == MINE_VAL } // Kaboom
+ });
+
+ let server = Server::bind(&http_addr)
+ .serve(http_serv)
+ .with_graceful_shutdown(shutdown_signal());
+ if let Err(e) = server.await {
+ eprint!("server error: {}", e);
+ }
+}
+
+// If a move is made, broadcast new board, else just send current board
+async fn gameloop(mut move_rx: mpsc::UnboundedReceiver<BoardRequest>, peers: PeerMap) {
+ let mut game = Game::new(Board::new(30,10), (30*10)/8);
+ while let Some(req) = move_rx.recv().await {
+ let m = req;
+ if let Some(m) = m {
+ game = game.act(m);
}
- pub fn grade(mut self) -> Board {
- for i in &mut self.data {
- if *i == MINE_VAL | FLAGGED_BIT | HIDDEN_BIT {
- *i |= CORRECT_BIT;
- }
+ let reply = Message::binary(game.board.render(None));
+ {
+ let peers = peers.read().await;
+ for (_, (tx, _, _, _)) in peers.iter() {
+ tx.unbounded_send(reply.clone()).unwrap();
}
- self
- }
- pub fn flag(mut self, pos: (usize, usize)) -> MoveResult {
- let off = self.pos_to_off(pos);
- self.data[off] ^= FLAGGED_BIT;
- MoveResult { 0: self, 1: false }
}
+ }
+}
+
+async fn conn_listener(peer_info: PeerInfo, cmd_tx: MovReqTx) {
+ let ws_addr = SocketAddr::from(([0, 0, 0, 0], 31236));
+ let ws_socket = TcpListener::bind(&ws_addr).await;
+ let ws_listener = ws_socket.expect("Failed to bind");
+ // Let's spawn the handling of each connection in a separate task.
+ println!("Websocket on {}", ws_addr);
+ while let Ok((stream, addr)) = ws_listener.accept().await {
+ tokio::spawn(peer_connection(peer_info.clone(), cmd_tx.clone(), stream, addr));
+ }
+}
+
+async fn peer_connection(peer_info: PeerInfo, cmd_tx: MovReqTx, raw_stream: TcpStream, addr: SocketAddr) {
+ println!("Incoming TCP connection from: {}", addr);
+
+ let ws_stream = tokio_tungstenite::accept_async(raw_stream)
+ .await
+ .expect("Error during the websocket handshake occurred");
+ println!("WebSocket connection established: {}", addr);
+
+ let peer_map = peer_info.0;
+ let peer_seqid = peer_info.1.fetch_add(1, atomic::Ordering::AcqRel);
+
+ // Insert the write part of this peer to the peer map.
+ let (tx, rx) = unbounded();
+ peer_map.write().await.insert(addr, (tx.clone(), peer_seqid, "Dummy".to_string(), (0,0)));
+
+ let (outgoing, mut incoming) = ws_stream.split();
+
+ { // 1 - Inform the new player its id
+ tx.unbounded_send(Message::text(format!("id {}", peer_seqid))).unwrap();
+ }
+
+ { // 2 - Queue up a current board broadcast
+ cmd_tx.send(None).unwrap();
+ }
- pub fn render(&self, cursor: Option<(usize,usize)>) -> Vec<u8> {
- const CYAN: &[u8] = &[27,b'[',b'3',b'6',b'm'];
- const YELLOW: &[u8] = &[27,b'[',b'3',b'3',b'm'];
- const GREEN: &[u8] = &[27,b'[',b'3',b'2',b'm'];
- const RED: &[u8] = &[27,b'[',b'3',b'1',b'm'];
- const FG: &[u8] = &[27,b'[',b'3',b'9',b'm'];
- const ULINE: &[u8] = &[27,b'[',b'4',b'm'];
- const REGULAR: &[u8] = &[27,b'[',b'0',b'm'];
+ let process_incoming = async {
+ while let Some(cmd) = incoming.try_next().await.unwrap() {
+ let cmd = cmd.to_text().unwrap();
+ println!("Received a message from {}: {}", addr, cmd);
- let mut ret = vec![27,b'[',b'2',b'J',27,b'[',b'0',b'm'];
- let mut cur_clr = FG;
- for y in 0..self.height {
- ret.push(b'\r');
- ret.push(b'\n');
- for x in 0..self.width {
- let c = &self.data[self.pos_to_off((x,y))];
- let is_cursor = if let Some(cursor) = cursor { cursor.0 == x && cursor.1 == y } else { false };
- if is_cursor { ret.extend_from_slice(ULINE); }
- match *c {
- 0 => ret.push(b' '),
- _ if *c <= 8 => { if cur_clr != FG { ret.extend_from_slice(FG); cur_clr = FG; } ret.push(b'0' + c); },
- _ if (*c & CORRECT_BIT) > 0 => { if cur_clr != GREEN { ret.extend_from_slice(GREEN); cur_clr = GREEN; } ret.push(b'F') },
- _ if (*c & FLAGGED_BIT) > 0 => { if cur_clr != YELLOW { ret.extend_from_slice(YELLOW); cur_clr = YELLOW; } ret.push(b'F'); },
- _ if (*c & HIDDEN_BIT) > 0 => { if cur_clr != CYAN { ret.extend_from_slice(CYAN); cur_clr = CYAN; } ret.push(b'-'); },
- _ if *c == MINE_VAL => { if cur_clr != RED { ret.extend_from_slice(RED); cur_clr = RED; } ret.push(b'O'); },
- _ => ret.push(b'?'),
+ if cmd.starts_with("pos") {
+ let mut fields = cmd.split(" ").skip(1);
+ let pos = (fields.next().unwrap().parse::<usize>().unwrap(), fields.next().unwrap().parse::<usize>().unwrap());
+ let (name, id) = {
+ let mut peers = peer_map.write().await;
+ let mut entry = peers.get_mut(&addr).unwrap();
+ entry.3 = pos.clone();
+ (entry.2.clone(), entry.1)
+ };
+ {
+ let peers = peer_map.read().await;
+ for peer_tx in peers.iter().filter(|(s, _)| **s != addr).map(|(_,(peer_tx,_,_,_))| peer_tx) {
+ peer_tx.unbounded_send(Message::text(format!("pos {} {} {} {}", id, name, pos.0, pos.1))).unwrap();
}
- if is_cursor { ret.extend_from_slice(REGULAR); ret.extend_from_slice(cur_clr); }
}
}
- ret
}
- }
-}
+ };
+ let send_to_browser = rx.map(Ok).forward(outgoing);
-use minesweeper::*;
-use std::convert::{ TryFrom, TryInto };
-use std::io::Write;
+ pin_mut!(process_incoming, send_to_browser);
+ future::select(process_incoming, send_to_browser).await;
+
+ println!("{} disconnected", &addr);
+ peer_map.write().await.remove(&addr);
+}
-fn main() -> Result<(), io::Error> {
- let board = Board::new(30,10);
- let mut game = Game::new(board, 300/8);
- let mut cursor: (usize, usize) = (0,0);
- let stdout = io::stdout();
- let mut lstdout = stdout.lock();
- while game.phase == Phase::Run || game.phase == Phase::SafeFirstMove {
- let screen = game.board.render(Some(cursor));
- lstdout.write_all(&screen)?;
- lstdout.flush()?;
+async fn handle_http_req(request: Request<Body>) -> HtmlResult {
+ let page = fs::read_to_string(PAGE_RELPATH).await.unwrap();
+ let mut uri_path = request.uri().path().split('/').skip(1);
+ let actual_path = uri_path.next();
- let mm = playerctrl(
- (cursor.0.try_into().unwrap(), cursor.1.try_into().unwrap()),
- (game.board.width.try_into().unwrap(), game.board.height.try_into().unwrap()))?;
- match mm {
- MetaMove::Move(m) => game = game.act(m),
- MetaMove::CursorMove(newc) => cursor = newc,
- MetaMove::Quit => game.phase = Phase::Leave,
- MetaMove::Noop => (),
+ match (request.method(), actual_path) {
+ (&Method::GET, None | Some("")) => {
+ Response::builder()
+ .status(StatusCode::OK)
+ .body(Body::from(page))
+ .map_err(errpage)
+ },
+ (&Method::GET, Some("saddr")) => {
+ Response::builder()
+ .status(StatusCode::OK)
+ .body(Body::from("placeholder"))
+ .map_err(errpage)
+ },
+ (&Method::GET, Some("font.ttf")) => {
+ Response::builder()
+ .status(StatusCode::OK)
+ .body(Body::from(FONT_FILE_FUCKIT))
+ .map_err(errpage)
+ },
+ _ => {
+ Response::builder()
+ .status(StatusCode::METHOD_NOT_ALLOWED)
+ .header("ALLOW", "GET, POST")
+ .body(Body::empty()).map_err(errpage)
}
}
- game.board = game.board.grade();
- lstdout.write_all(&game.board.render(None))?;
- Ok(())
}
-enum MetaMove {
- Move(Move),
- CursorMove((usize, usize)),
- Noop,
- Quit,
-}
-fn playerctrl(mut cursor: (isize, isize), bounds: (isize, isize)) -> io::Result<MetaMove> {
- const ERR_STR: &str = "bad/no input";
- let mut mov_dir = | dir: (isize,isize) | {
- cursor.0 += dir.0;
- cursor.1 += dir.1;
- MetaMove::CursorMove(cursor_bounds(cursor, bounds))
- };
- let stdin = io::stdin();
- let lstdin = stdin.lock();
- let mut input = lstdin.bytes();
- Ok(match input.next().expect(ERR_STR)? {
- b'w' => mov_dir((0,-1)),
- b'a' => mov_dir((-1,0)),
- b's' => mov_dir((0,1)),
- b'd' => mov_dir((1,0)),
- 13 => MetaMove::Move(Move { t: MoveType::Reveal, pos: cursor_bounds(cursor, bounds) }),
- b'f' => MetaMove::Move(Move { t: MoveType::ToggleFlag, pos: cursor_bounds(cursor, bounds) }),
- b'q' => MetaMove::Quit,
- 27 => {
- if input.next().expect(ERR_STR)? == b'[' {
- match input.next().expect(ERR_STR)? {
- b'A' => mov_dir((0,-1)),
- b'B' => mov_dir((0,1)),
- b'C' => mov_dir((1,0)),
- b'D' => mov_dir((-1,0)),
- _ => MetaMove::Noop,
- }
- } else { MetaMove::Noop }
- }
- v => { println!("{:?}", v); MetaMove::Noop },
- })
+fn errpage<T: Error>(e: T) -> Response<Body> {
+ Response::builder()
+ .status(StatusCode::INTERNAL_SERVER_ERROR)
+ .body(e.to_string().into())
+ .unwrap()
}
-fn cursor_bounds(mut c: (isize, isize), b: (isize, isize)) -> (usize, usize) {
- if c.0 > b.0 - 1 {
- c.0 = 0;
- }
- else if c.0 < 0 {
- c.0 = b.0 - 1;
- }
- if c.1 > b.1 - 1 {
- c.1 = 0;
- }
- else if c.1 < 0 {
- c.1 = b.1 - 1;
- }
- (
- usize::try_from(c.0).unwrap(),
- usize::try_from(c.1).unwrap()
- )
+async fn shutdown_signal() {
+ tokio::signal::ctrl_c()
+ .await
+ .expect("failed to install CTRL+C signal handler");
}