summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock343
-rw-r--r--Cargo.toml5
-rw-r--r--page.html123
-rw-r--r--src/VT323-Regular.ttfbin0 -> 149688 bytes
-rw-r--r--src/main.rs447
-rw-r--r--src/minesweeper.rs203
6 files changed, 852 insertions, 269 deletions
diff --git a/Cargo.lock b/Cargo.lock
index fb3aed7..d4df158 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index 2b6fee6..28dec21 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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.ttf
new file mode 100644
index 0000000..6aec599
--- /dev/null
+++ b/src/VT323-Regular.ttf
Binary files 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<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
+ }
+}
+