From 9987d7abf6acd647b4a3a00c20d15f716dce653c Mon Sep 17 00:00:00 2001 From: stale Date: Sat, 30 Apr 2022 04:44:08 -0300 Subject: we finally got moving cursors heck yeah --- src/VT323-Regular.ttf | Bin 0 -> 149688 bytes src/main.rs | 447 ++++++++++++++++++++------------------------------ src/minesweeper.rs | 203 +++++++++++++++++++++++ 3 files changed, 381 insertions(+), 269 deletions(-) create mode 100644 src/VT323-Regular.ttf create mode 100644 src/minesweeper.rs (limited to 'src') diff --git a/src/VT323-Regular.ttf b/src/VT323-Regular.ttf new file mode 100644 index 0000000..6aec599 Binary files /dev/null and b/src/VT323-Regular.ttf differ 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, - 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>; +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; +type MovReqTx = mpsc::UnboundedSender; +type PeerMap = Arc>>; +type PeerInfo = (PeerMap, Arc::); + +// Which will either perform a move, or simply request the current board +type BoardRequest = Option; + +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| { + 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, 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 { - 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::().unwrap(), fields.next().unwrap().parse::().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) -> 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 { - 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(e: T) -> Response { + 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"); } diff --git a/src/minesweeper.rs b/src/minesweeper.rs new file mode 100644 index 0000000..8c0dc77 --- /dev/null +++ b/src/minesweeper.rs @@ -0,0 +1,203 @@ +use std::convert::TryFrom; +use rand::{ thread_rng, Rng, distributions::Uniform }; +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, + pub width: usize, + pub height: usize, + pub hidden_tiles: usize, +} +#[derive(Debug)] +pub enum MoveType { + Reveal, + ToggleFlag, +} +#[derive(Debug)] +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!(), + } + }; + + 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, + }; + + 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) + }) + }); + } + } + } + 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 + } + pub fn grade(mut self) -> Board { + for i in &mut self.data { + if *i == MINE_VAL | FLAGGED_BIT | HIDDEN_BIT { + *i |= CORRECT_BIT; + } + } + 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 } + } + + pub fn render(&self, cursor: Option<(usize,usize)>) -> Vec { + 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 mut ret = vec![27]; + let mut cur_clr = FG; + for y in 0..self.height { + ret.extend_from_slice(b"
"); + 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(35); }, + _ if *c == MINE_VAL => { if cur_clr != RED { ret.extend_from_slice(RED); cur_clr = RED; } ret.push(b'O'); }, + _ => ret.push(b'?'), + } + if is_cursor { ret.extend_from_slice(REGULAR); ret.extend_from_slice(cur_clr); } + } + } + ret + } +} + -- cgit v1.2.3