summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpommicket <pommicket@gmail.com>2024-08-08 21:20:25 -0400
committerpommicket <pommicket@gmail.com>2024-08-08 21:20:25 -0400
commit39d0135b0fd9c67d673c958b4eaf4fecc5f1358c (patch)
tree0ef61986c80d4f94c539c7b4815059aa887f522f
parent9a1addcd1fd596df21b155263110e97add9903ab (diff)
connectivity synchronization
-rw-r--r--TODO.txt1
-rw-r--r--game.js26
-rw-r--r--server/src/main.rs153
3 files changed, 142 insertions, 38 deletions
diff --git a/TODO.txt b/TODO.txt
new file mode 100644
index 0000000..2d5bd52
--- /dev/null
+++ b/TODO.txt
@@ -0,0 +1 @@
+- limit number of playres connected to a game (prevent contention)
diff --git a/game.js b/game.js
index 811f23c..70f3686 100644
--- a/game.js
+++ b/game.js
@@ -83,7 +83,7 @@ window.addEventListener('load', function () {
console.assert(false);
}
function connectPieces(piece1, piece2) {
- if (piece1.connectedComponent === piece2.connectedComponent) return;
+ if (piece1.connectedComponent === piece2.connectedComponent) return false;
piece1.connectedComponent.push(...piece2.connectedComponent);
let piece1Col = piece1.col();
let piece1Row = piece1.row();
@@ -95,6 +95,11 @@ window.addEventListener('load', function () {
piece.y = (row - piece1Row) * pieceHeight + piece1.y;
piece.updatePosition();
}
+ if (!solved && piece1.connectedComponent.length === puzzleWidth * puzzleHeight) {
+ solveAudio.play();
+ solved = true;
+ }
+ return true;
}
class NibType {
orientation;
@@ -289,13 +294,10 @@ window.addEventListener('load', function () {
if (sqDist < connectRadius * connectRadius) {
anyConnected = true;
connectPieces(piece, neighbour);
+ socket.send(`connect ${piece.id} ${neighbour.id}`);
}
}
}
- if (!solved && draggingPiece.connectedComponent.length === puzzleWidth * puzzleHeight) {
- solveAudio.play();
- solved = true;
- }
draggingPiece.element.style.removeProperty('cursor');
draggingPiece = null;
if (anyConnected)
@@ -326,10 +328,13 @@ window.addEventListener('load', function () {
}
let imageLoaded = joinPuzzle ? null : loadImage();
function updateConnectivity(connectivity) {
+ console.log(connectivity);
console.assert(connectivity.length === pieces.length);
+ let anyConnected = false;
for (let i = 0; i < pieces.length; i++) {
- connectPieces(pieces[i], pieces[connectivity[i]]);
+ anyConnected |= connectPieces(pieces[i], pieces[connectivity[i]]);
}
+ if (anyConnected) connectAudio.play();
}
async function initPuzzle(payload) {
const data = new Uint8Array(payload, payload.length);
@@ -399,7 +404,7 @@ window.addEventListener('load', function () {
const connectivity = new Uint16Array(update, 8 + piecePositions.length * 4, puzzleWidth * puzzleHeight);
updateConnectivity(connectivity);
for (let i = 0; i < pieces.length; i++) {
- // only udpate the position of one piece per equivalence class mod is-connected-to
+ // only receive the position of one piece per equivalence class mod is-connected-to
if (connectivity[i] !== i) continue;
const piece = pieces[i];
if (!piece.upToDateWithServer) continue;
@@ -411,6 +416,13 @@ window.addEventListener('load', function () {
piece.x = newPos.x;
piece.y = newPos.y;
piece.updatePosition();
+ // derive all other pieces' position in this connected component from piece.
+ for (const other of piece.connectedComponent) {
+ if (other === piece) continue;
+ other.x = piece.x + (other.col() - piece.col()) * pieceWidth;
+ other.y = piece.y + (other.row() - piece.row()) * pieceHeight;
+ other.updatePosition();
+ }
}
}
function sendServerUpdate() {
diff --git a/server/src/main.rs b/server/src/main.rs
index 32220a9..2693d5b 100644
--- a/server/src/main.rs
+++ b/server/src/main.rs
@@ -1,12 +1,12 @@
+use anyhow::anyhow;
use futures_util::{SinkExt, StreamExt};
+use rand::Rng;
use std::net::SocketAddr;
+use std::sync::LazyLock;
use tokio::io::AsyncWriteExt;
use tungstenite::protocol::Message;
-use rand::Rng;
-use std::sync::LazyLock;
-use anyhow::anyhow;
-const PUZZLE_ID_CHARSET: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+const PUZZLE_ID_CHARSET: &[u8] = b"23456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ";
const PUZZLE_ID_LEN: usize = 6;
fn generate_puzzle_id() -> [u8; PUZZLE_ID_LEN] {
@@ -17,23 +17,40 @@ fn generate_puzzle_id() -> [u8; PUZZLE_ID_LEN] {
struct Database {
puzzles: sled::Tree,
pieces: sled::Tree,
+ connectivity: sled::Tree,
}
fn get_puzzle_info(database: &Database, id: &[u8]) -> anyhow::Result<Vec<u8>> {
- if id.len() != PUZZLE_ID_LEN { return Err(anyhow!("bad puzzle ID")); }
+ if id.len() != PUZZLE_ID_LEN {
+ return Err(anyhow!("bad puzzle ID"));
+ }
let mut data = vec![1, 0, 0, 0, 0, 0, 0, 0]; // opcode + padding
- let puzzle = database.puzzles.get(id)?.ok_or_else(|| anyhow!("bad puzzle ID"))?;
+ let puzzle = database
+ .puzzles
+ .get(id)?
+ .ok_or_else(|| anyhow!("bad puzzle ID"))?;
data.extend_from_slice(&puzzle);
while data.len() % 8 != 0 {
// padding
data.push(0);
}
- let pieces = database.pieces.get(id)?.ok_or_else(|| anyhow!("bad puzzle ID"))?;
+ let pieces = database
+ .pieces
+ .get(id)?
+ .ok_or_else(|| anyhow!("bad puzzle ID"))?;
data.extend_from_slice(&pieces);
+ let connectivity = database
+ .connectivity
+ .get(id)?
+ .ok_or_else(|| anyhow!("bad puzzle ID"))?;
+ data.extend_from_slice(&connectivity);
Ok(data)
}
-async fn handle_connection(database: &Database, conn: &mut tokio::net::TcpStream) -> anyhow::Result<()> {
+async fn handle_connection(
+ database: &Database,
+ conn: &mut tokio::net::TcpStream,
+) -> anyhow::Result<()> {
let mut ws = tokio_tungstenite::accept_async_with_config(
conn,
Some(tungstenite::protocol::WebSocketConfig {
@@ -66,7 +83,9 @@ async fn handle_connection(database: &Database, conn: &mut tokio::net::TcpStream
// pick nib types
{
let mut rng = rand::thread_rng();
- for _ in 0..2u16 * (width as u16) * (height as u16) - (width as u16) - (height as u16) {
+ for _ in 0..2u16 * (width as u16) * (height as u16)
+ - (width as u16) - (height as u16)
+ {
puzzle_data.push(rng.gen());
puzzle_data.push(rng.gen());
}
@@ -79,7 +98,11 @@ async fn handle_connection(database: &Database, conn: &mut tokio::net::TcpStream
loop {
id = generate_puzzle_id();
let data = std::mem::take(&mut puzzle_data);
- if database.puzzles.compare_and_swap(id, None::<&'static [u8; 0]>, Some(&data[..]))?.is_ok() {
+ if database
+ .puzzles
+ .compare_and_swap(id, None::<&'static [u8; 0]>, Some(&data[..]))?
+ .is_ok()
+ {
break;
}
}
@@ -89,7 +112,7 @@ async fn handle_connection(database: &Database, conn: &mut tokio::net::TcpStream
{
let mut rng = rand::thread_rng();
pieces_data = Vec::new();
- pieces_data.resize((width as usize) * (height as usize) * 10, 0);
+ pieces_data.resize((width as usize) * (height as usize) * 8, 0);
// positions
let mut it = pieces_data.iter_mut();
for _ in 0..(width as u16) * (height as u16) * 2 {
@@ -100,15 +123,19 @@ async fn handle_connection(database: &Database, conn: &mut tokio::net::TcpStream
*it.next().unwrap() = c;
*it.next().unwrap() = d;
}
- // connectivity
- for i in 0..(width as u16) * (height as u16) {
- let [a, b] = i.to_le_bytes();
- *it.next().unwrap() = a;
- *it.next().unwrap() = b;
- }
}
database.pieces.insert(id, pieces_data)?;
- ws.send(Message::Text(format!("id: {}", std::str::from_utf8(&id)?))).await?;
+ let mut connectivity_data = Vec::new();
+ connectivity_data.resize((width as usize) * (height as usize) * 2, 0);
+ let mut it = connectivity_data.iter_mut();
+ for i in 0..(width as u16) * (height as u16) {
+ let [a, b] = i.to_le_bytes();
+ *it.next().unwrap() = a;
+ *it.next().unwrap() = b;
+ }
+ database.connectivity.insert(id, connectivity_data)?;
+ ws.send(Message::Text(format!("id: {}", std::str::from_utf8(&id)?)))
+ .await?;
let info = get_puzzle_info(&database, &id)?;
ws.send(Message::Binary(info)).await?;
} else if let Some(id) = text.strip_prefix("join ") {
@@ -128,36 +155,96 @@ async fn handle_connection(database: &Database, conn: &mut tokio::net::TcpStream
for line in text.split('\n') {
let mut parts = line.split(' ');
parts.next(); // skip "move"
- let piece: usize = parts.next().ok_or_else(|| anyhow!("bad syntax"))?.parse()?;
+ let piece: usize =
+ parts.next().ok_or_else(|| anyhow!("bad syntax"))?.parse()?;
let x: f32 = parts.next().ok_or_else(|| anyhow!("bad syntax"))?.parse()?;
let y: f32 = parts.next().ok_or_else(|| anyhow!("bad syntax"))?.parse()?;
- motions.push(Motion {
- piece,
- x,
- y,
- });
+ motions.push(Motion { piece, x, y });
}
loop {
- let curr_pieces = database.pieces.get(&puzzle_id)?
+ let curr_pieces = database
+ .pieces
+ .get(&puzzle_id)?
.ok_or_else(|| anyhow!("bad puzzle ID"))?;
let mut new_pieces = curr_pieces.to_vec();
- for Motion {piece, x, y} in motions.iter().copied() {
- new_pieces.get_mut(8 * piece..8 * piece + 4).ok_or_else(|| anyhow!("bad piece ID"))?
+ for Motion { piece, x, y } in motions.iter().copied() {
+ new_pieces
+ .get_mut(8 * piece..8 * piece + 4)
+ .ok_or_else(|| anyhow!("bad piece ID"))?
.copy_from_slice(&x.to_le_bytes());
- new_pieces.get_mut(8 * piece + 4..8 * piece + 8).ok_or_else(|| anyhow!("bad piece ID"))?
+ new_pieces
+ .get_mut(8 * piece + 4..8 * piece + 8)
+ .ok_or_else(|| anyhow!("bad piece ID"))?
.copy_from_slice(&y.to_le_bytes());
}
- if database.pieces.compare_and_swap(&puzzle_id, Some(curr_pieces), Some(new_pieces))?.is_ok() {
+ if database
+ .pieces
+ .compare_and_swap(&puzzle_id, Some(curr_pieces), Some(new_pieces))?
+ .is_ok()
+ {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(1)).await; // yield maybe (don't let contention hog resources)
}
ws.send(Message::Text("ack".to_string())).await?;
+ } else if let Some(data) = text.strip_prefix("connect ") {
+ let mut parts = data.split(' ');
+ let puzzle_id = puzzle_id.ok_or_else(|| anyhow!("connect without puzzle ID"))?;
+ let piece1: usize = parts.next().ok_or_else(|| anyhow!("bad syntax"))?.parse()?;
+ let piece2: usize = parts.next().ok_or_else(|| anyhow!("bad syntax"))?.parse()?;
+ loop {
+ let curr_connectivity = database
+ .connectivity
+ .get(&puzzle_id)?
+ .ok_or_else(|| anyhow!("bad puzzle ID"))?;
+ let mut new_connectivity = curr_connectivity.to_vec();
+ if piece1 >= curr_connectivity.len() / 2
+ || piece2 >= curr_connectivity.len() / 2
+ {
+ return Err(anyhow!("bad piece ID"));
+ }
+ let piece2_group = u16::from_le_bytes([
+ curr_connectivity[piece2 * 2],
+ curr_connectivity[piece2 * 2 + 1],
+ ]);
+ let a = curr_connectivity[piece1 * 2];
+ let b = curr_connectivity[piece1 * 2 + 1];
+ for piece in 0..curr_connectivity.len() / 2 {
+ let piece_group = u16::from_le_bytes([
+ curr_connectivity[piece * 2],
+ curr_connectivity[piece * 2 + 1],
+ ]);
+ if piece_group == piece2_group {
+ new_connectivity[piece * 2] = a;
+ new_connectivity[piece * 2 + 1] = b;
+ }
+ }
+ if database
+ .connectivity
+ .compare_and_swap(
+ &puzzle_id,
+ Some(curr_connectivity),
+ Some(new_connectivity),
+ )?
+ .is_ok()
+ {
+ break;
+ }
+ tokio::time::sleep(std::time::Duration::from_millis(1)).await; // yield maybe (don't let contention hog resources)
+ }
} else if text == "poll" {
let puzzle_id = puzzle_id.ok_or_else(|| anyhow!("poll without puzzle ID"))?;
- let pieces = database.pieces.get(&puzzle_id)?.ok_or_else(|| anyhow!("bad puzzle ID"))?;
+ let pieces = database
+ .pieces
+ .get(&puzzle_id)?
+ .ok_or_else(|| anyhow!("bad puzzle ID"))?;
+ let connectivity = database
+ .connectivity
+ .get(&puzzle_id)?
+ .ok_or_else(|| anyhow!("bad puzzle ID"))?;
let mut data = vec![2, 0, 0, 0, 0, 0, 0, 0]; // opcode / version number + padding
data.extend_from_slice(&pieces);
+ data.extend_from_slice(&connectivity);
ws.send(Message::Binary(data)).await?;
}
}
@@ -186,9 +273,13 @@ async fn main() {
let db = sled::open("database.sled").expect("error opening database");
let puzzles = db.open_tree("PUZZLES").expect("error opening puzzles tree");
let pieces = db.open_tree("PIECES").expect("error opening pieces tree");
+ let connectivity = db
+ .open_tree("CONNECTIVITY")
+ .expect("error opening connectivity tree");
Database {
puzzles,
- pieces
+ pieces,
+ connectivity,
}
});
let database: &Database = &DATABASE_VALUE;