diff options
author | pommicket <pommicket@gmail.com> | 2024-08-08 21:20:25 -0400 |
---|---|---|
committer | pommicket <pommicket@gmail.com> | 2024-08-08 21:20:25 -0400 |
commit | 39d0135b0fd9c67d673c958b4eaf4fecc5f1358c (patch) | |
tree | 0ef61986c80d4f94c539c7b4815059aa887f522f | |
parent | 9a1addcd1fd596df21b155263110e97add9903ab (diff) |
connectivity synchronization
-rw-r--r-- | TODO.txt | 1 | ||||
-rw-r--r-- | game.js | 26 | ||||
-rw-r--r-- | server/src/main.rs | 153 |
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) @@ -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; |