diff options
| -rw-r--r-- | Cargo.lock | 343 | ||||
| -rw-r--r-- | Cargo.toml | 5 | ||||
| -rw-r--r-- | page.html | 123 | ||||
| -rw-r--r-- | src/VT323-Regular.ttf | bin | 0 -> 149688 bytes | |||
| -rw-r--r-- | src/main.rs | 447 | ||||
| -rw-r--r-- | src/minesweeper.rs | 203 | 
6 files changed, 852 insertions, 269 deletions
| @@ -3,12 +3,24 @@  version = 3  [[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]]  name = "base64"  version = "0.13.0"  source = "registry+https://github.com/rust-lang/crates.io-index"  checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"  [[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]]  name = "block-buffer"  version = "0.10.2"  source = "registry+https://github.com/rust-lang/crates.io-index" @@ -81,6 +93,35 @@ dependencies = [  ]  [[package]] +name = "format-bytes" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48942366ef93975da38e175ac9e10068c6fc08ca9e85930d4f098f4d5b14c2fd" +dependencies = [ + "format-bytes-macros", +] + +[[package]] +name = "format-bytes-macros" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "203aadebefcc73d12038296c228eabf830f99cba991b0032adf20e9fa6ce7e4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-channel" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" +dependencies = [ + "futures-core", +] + +[[package]]  name = "futures-core"  version = "0.3.21"  source = "registry+https://github.com/rust-lang/crates.io-index" @@ -134,6 +175,40 @@ dependencies = [  ]  [[package]] +name = "h2" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]]  name = "http"  version = "0.2.6"  source = "registry+https://github.com/rust-lang/crates.io-index" @@ -145,12 +220,53 @@ dependencies = [  ]  [[package]] +name = "http-body" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]]  name = "httparse"  version = "1.7.1"  source = "registry+https://github.com/rust-lang/crates.io-index"  checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c"  [[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "hyper" +version = "0.14.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]]  name = "idna"  version = "0.2.3"  source = "registry+https://github.com/rust-lang/crates.io-index" @@ -162,18 +278,44 @@ dependencies = [  ]  [[package]] +name = "indexmap" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]]  name = "itoa"  version = "1.0.1"  source = "registry+https://github.com/rust-lang/crates.io-index"  checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"  [[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]]  name = "libc"  version = "0.2.124"  source = "registry+https://github.com/rust-lang/crates.io-index"  checksum = "21a41fed9d98f27ab1c6d161da622a4fa35e8a54a8adc24bbf3ddd0ef70b0e50"  [[package]] +name = "lock_api" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]]  name = "log"  version = "0.4.16"  source = "registry+https://github.com/rust-lang/crates.io-index" @@ -227,6 +369,45 @@ dependencies = [  ]  [[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" + +[[package]] +name = "parking_lot" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "995f667a6c822200b0433ac218e05582f0e2efa1b922a3fd2fbaadc5f87bab37" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]]  name = "percent-encoding"  version = "2.1.0"  source = "registry+https://github.com/rust-lang/crates.io-index" @@ -299,6 +480,21 @@ dependencies = [  ]  [[package]] +name = "redox_syscall" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +dependencies = [ + "bitflags", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]]  name = "sha-1"  version = "0.10.0"  source = "registry+https://github.com/rust-lang/crates.io-index" @@ -310,12 +506,27 @@ dependencies = [  ]  [[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]]  name = "slab"  version = "0.4.6"  source = "registry+https://github.com/rust-lang/crates.io-index"  checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32"  [[package]] +name = "smallvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" + +[[package]]  name = "socket2"  version = "0.4.4"  source = "registry+https://github.com/rust-lang/crates.io-index" @@ -381,12 +592,28 @@ dependencies = [   "libc",   "memchr",   "mio", + "num_cpus", + "once_cell", + "parking_lot",   "pin-project-lite", + "signal-hook-registry",   "socket2", + "tokio-macros",   "winapi",  ]  [[package]] +name = "tokio-macros" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]]  name = "tokio-tungstenite"  version = "0.17.1"  source = "registry+https://github.com/rust-lang/crates.io-index" @@ -399,6 +626,64 @@ dependencies = [  ]  [[package]] +name = "tokio-util" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0edfdeb067411dba2044da6d1cb2df793dd35add7888d73c16e3381ded401764" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tower-service" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" + +[[package]] +name = "tracing" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6b8ad3567499f98a1db7a752b07a7c8c7c7c34c332ec00effb2b0027974b7c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f54c8ca710e81886d498c2fd3331b56c93aa248d49de2222ad2742247c60072f" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]]  name = "tungstenite"  version = "0.17.2"  source = "registry+https://github.com/rust-lang/crates.io-index" @@ -469,6 +754,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"  checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"  [[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]]  name = "wasi"  version = "0.10.2+wasi-snapshot-preview1"  source = "registry+https://github.com/rust-lang/crates.io-index" @@ -484,7 +779,12 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"  name = "websweeper"  version = "1.0.0"  dependencies = [ + "format-bytes", + "futures-channel", + "futures-util", + "hyper",   "rand", + "tokio",   "tokio-tungstenite",  ] @@ -509,3 +809,46 @@ name = "winapi-x86_64-pc-windows-gnu"  version = "0.4.0"  source = "registry+https://github.com/rust-lang/crates.io-index"  checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5acdd78cb4ba54c0045ac14f62d8f94a03d10047904ae2a40afa1e99d8f70825" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d" + +[[package]] +name = "windows_i686_gnu" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed" + +[[package]] +name = "windows_i686_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" @@ -7,5 +7,10 @@ edition = "2018"  # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html  [dependencies] +hyper = { version = "0.14", features = ["full"] } +tokio = { version = "1", features = ["full"] }  tokio-tungstenite = "*"  rand = "0.8" +futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] } +futures-channel = "0.3" +format-bytes = "0.3" diff --git a/page.html b/page.html new file mode 100644 index 0000000..75bb7fd --- /dev/null +++ b/page.html @@ -0,0 +1,123 @@ +<!DOCTYPE html> +<html lang="en"> +  <head> +  <meta charset="UTF-8"> +  <title>websweeper</title> +  <meta name="viewport" content="width=device-width,initial-scale=1"> +  </head> +  <style> +@font-face { +    font-family: vt323; +    src: url("font.ttf"); +} +    #board-container { +      font-family: vt323, monospace; +      font-size: 28pt; +      line-height: 0.6em; +    } +    body { +      background-color: black; +      color: white; +    } + +    .unsel { +    -webkit-touch-callout: none; +    -webkit-user-select: none; +    -khtml-user-select: none; +    -moz-user-select: none; +    -ms-user-select: none; +    user-select: none; +    } + +    .cursor * { +      font-size: 16pt; +      margin: 0 0; +    } +  </style> +  <body> +    <div class=""> +      <div id="board-container"> +      <div id="board"></div> +      </div> +      <p id="miscinfo">Loading...</p> +    </div> +  </body> +  <script src="https://p.masba.net:8443/ansispan.js"></script> +  <script> +    let id = NaN; +    let s = new WebSocket("ws://127.0.0.1:31236"); +    let info_elem = document.getElementById("miscinfo"); +    let board_elem = document.getElementById("board"); +    let last_packet = {}; +    let cursors = new Map(); +    let board = {}; +    s.onopen = function(e) { info_elem.innerHTML = "Connected"; } +    s.onmessage = function(e) { +          console.log(`got msg ${e.data}`); +          last_packet = e; +          let d = e.data; +          if (typeof d == "object") { +            d.arrayBuffer().then(acceptPacket); +          } else if (typeof e.data == "string") { +            let fields = d.split(" "); +            if (d.startsWith("pos")) { +              let oid = fields[1]; +              let name = fields[2]; +              let x = fields[3]; +              let y = fields[4]; +              if (!cursors.has(oid)) { +                createCursor(oid, name); +              } +              let celem = cursors.get(oid).elem; +              celem.style.left = x + 'px'; +              celem.style.top = y + 'px'; +            } +            else if (d.startsWith("id")) { +              id = fields[1]; +              createCursor(id, "You"); +            } +          } +      } +    s.onerror = function(e) { info_elem.innerHTML += `<br>error ${e}`; } +    s.onclose = function(e) { info_elem.innerHTML = "Closed"; } + +    function acceptPacket(data) { +          let vals = new Uint8Array(data); +          if (vals[0] == 27) { +                // starts with escape char, is a board dump +                board = Array.from(vals.subarray(1,vals.length).values()).reduce((s,c) => s + +                      String.fromCodePoint(c), ""); +                board_elem.innerHTML = ansispan(board); +              } +          else if (vals[0] == 'p'.charCodeAt(0)) { +                // starts with a p, is a cursor position update +                // unimplemented! +              } + +        } + +    function createCursor(id, name) { +          // shit doesn't line up +          let cursor = document.createElement("div"); +          cursor.innerHTML = "<p>x</p>"; +          cursor.style.position = "absolute"; +          let nametag = document.createElement("p"); +          nametag.innerHTML = name; +          cursor.appendChild(nametag); +          cursor.classList.add('cursor'); +          let cb = cursor.getBoundingClientRect(); +          let tip = cursor.firstChild.getBoundingClientRect(); +          let dx = tip.width/2 - cb.width/2; +          let dy = -(tip.height/2 - cb.height/2); +          document.getElementById('board-container').append(cursor); +          document.addEventListener('mousemove', e => { +                cursor.style.left = (e.pageX + dx) + 'px'; +                cursor.style.top = (e.pageY + dy) + 'px'; +                s.send(`pos ${e.pageX} ${e.pageY}`); +              }, +                false); +          cursors.set(id, {name: name, elem: cursor}); +          return cursor; +        } +  </script> +</html> diff --git a/src/VT323-Regular.ttf b/src/VT323-Regular.ttfBinary files differ new file mode 100644 index 0000000..6aec599 --- /dev/null +++ b/src/VT323-Regular.ttf 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");  } 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<u8>, +    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<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 mut ret = vec![27]; +        let mut cur_clr = FG; +        for y in 0..self.height { +            ret.extend_from_slice(b"<br>"); +            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 +    } +} + | 
