summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--game.js105
-rw-r--r--server/src/main.rs42
2 files changed, 94 insertions, 53 deletions
diff --git a/game.js b/game.js
index 66165d4..07ecd52 100644
--- a/game.js
+++ b/game.js
@@ -102,6 +102,14 @@ window.addEventListener('load', function () {
let x2 = randomSeed >> 16;
return (x1 << 15 | x2) * (1 / (1 << 30));
}
+ function shuffle(l) {
+ for (let i = 0; i < l.length; i++) {
+ let j = Math.floor(random() * (i + 1));
+ let temp = l[i];
+ l[i] = l[j];
+ l[j] = temp;
+ }
+ }
const TOP_IN = 0;
const TOP_OUT = 1;
const RIGHT_IN = 2;
@@ -334,51 +342,59 @@ window.addEventListener('load', function () {
return clipPath.join(' ');
}
}
- window.addEventListener('mouseup', function() {
- if (draggingPiece) {
- let anyConnected = false;
- for (const piece of draggingPiece.connectedComponent) {
- piece.setAnimate(true);
- piece.element.style.zIndex = pieceZIndexCounter;
- if (solved) break;
- piece.needsServerUpdate = true;
- const col = piece.col();
- const row = piece.row();
- const bbox = piece.boundingBox();
- for (const [nx, ny] of [[0, -1], [0, 1], [1, 0], [-1, 0]]) {
- if (col + nx < 0 || col + nx >= puzzleWidth
- || row + ny < 0 || row + ny >= puzzleHeight) {
- continue;
- }
- let neighbour = pieces[piece.id + nx + ny * puzzleWidth];
- if (neighbour.connectedComponent === piece.connectedComponent)
+ function stopDraggingPiece() {
+ let anyConnected = false;
+ for (const piece of draggingPiece.connectedComponent) {
+ piece.element.style.zIndex = pieceZIndexCounter;
+ if (solved) break;
+ piece.needsServerUpdate = true;
+ const col = piece.col();
+ const row = piece.row();
+ const bbox = piece.boundingBox();
+ for (const [nx, ny] of [[0, -1], [0, 1], [1, 0], [-1, 0]]) {
+ if (col + nx < 0 || col + nx >= puzzleWidth
+ || row + ny < 0 || row + ny >= puzzleHeight) {
continue;
- let neighbourBBox = neighbour.boundingBox();
- let keyPointMe = [nx === -1 ? bbox.left + nibSize : bbox.right - nibSize,
- ny === -1 ? bbox.top + nibSize : bbox.bottom - nibSize];
- let keyPointNeighbour = [nx === 1 ? neighbourBBox.left + nibSize : neighbourBBox.right - nibSize,
- ny === 1 ? neighbourBBox.top + nibSize : neighbourBBox.bottom - nibSize];
- let diff = [keyPointMe[0] - keyPointNeighbour[0], keyPointMe[1] - keyPointNeighbour[1]];
- let sqDist = diff[0] * diff[0] + diff[1] * diff[1];
- if (sqDist < connectRadius * connectRadius) {
- anyConnected = true;
- connectPieces(piece, neighbour, true);
- if (multiplayer) {
- socket.send(new Uint32Array([ACTION_CONNECT, piece.id, neighbour.id]));
- }
+ }
+ let neighbour = pieces[piece.id + nx + ny * puzzleWidth];
+ if (neighbour.connectedComponent === piece.connectedComponent)
+ continue;
+ let neighbourBBox = neighbour.boundingBox();
+ let keyPointMe = [nx === -1 ? bbox.left + nibSize : bbox.right - nibSize,
+ ny === -1 ? bbox.top + nibSize : bbox.bottom - nibSize];
+ let keyPointNeighbour = [nx === 1 ? neighbourBBox.left + nibSize : neighbourBBox.right - nibSize,
+ ny === 1 ? neighbourBBox.top + nibSize : neighbourBBox.bottom - nibSize];
+ let diff = [keyPointMe[0] - keyPointNeighbour[0], keyPointMe[1] - keyPointNeighbour[1]];
+ let sqDist = diff[0] * diff[0] + diff[1] * diff[1];
+ if (sqDist < connectRadius * connectRadius) {
+ anyConnected = true;
+ connectPieces(piece, neighbour, true);
+ if (multiplayer) {
+ socket.send(new Uint32Array([ACTION_CONNECT, piece.id, neighbour.id]));
}
}
}
- draggingPiece.element.style.removeProperty('cursor');
- draggingPiece = null;
- if (anyConnected)
- connectAudio.play();
+ }
+ draggingPiece.element.style.removeProperty('cursor');
+ draggingPiece = null;
+ if (anyConnected)
+ connectAudio.play();
+ setTimeout(() => {
+ for (const piece of draggingPiece.connectedComponent)
+ piece.setAnimate(true);
+ }, 1);
+ }
+ window.addEventListener('mouseup', function() {
+ if (draggingPiece) {
+ stopDraggingPiece();
}
});
window.addEventListener('mousemove', function(e) {
if (draggingPiece) {
let dx = (e.clientX - draggingPieceLastPos.x) / playArea.clientWidth;
let dy = (e.clientY - draggingPieceLastPos.y) / playArea.clientHeight;
+ let originalDx = dx;
+ let originalDy = dy;
for (const piece of draggingPiece.connectedComponent) {
// ensure pieces don't go past left edge
dx = Math.max(dx, 0.0001 - piece.x);
@@ -396,6 +412,10 @@ window.addEventListener('load', function () {
}
draggingPieceLastPos.x = e.clientX;
draggingPieceLastPos.y = e.clientY;
+ if (dx !== originalDx || dy !== originalDy) {
+ // stop dragging piece if it was dragged past edge
+ stopDraggingPiece();
+ }
}
});
function loadImage() {
@@ -439,7 +459,7 @@ window.addEventListener('load', function () {
pieceWidth = pieceHeight * (puzzleHeight / puzzleWidth) * (image.width / image.height);
}
// ensure full puzzle doesn't take up too much screen space
- while (pieceWidth * puzzleWidth * pieceHeight * puzzleHeight > Math.max(1000, 0.5 * playArea.clientWidth * playArea.clientHeight)) {
+ while (pieceWidth * puzzleWidth * pieceHeight * puzzleHeight > Math.max(1000, 0.4 * playArea.clientWidth * playArea.clientHeight)) {
pieceWidth *= 0.9;
pieceHeight *= 0.9;
}
@@ -486,6 +506,19 @@ window.addEventListener('load', function () {
getById('host-multiplayer').style.display = 'inline-block';
setRandomSeed(puzzleSeed);
createPieces();
+ const piecePositions = [];
+ for (let y = 0; y < puzzleHeight; y++) {
+ for (let x = 0; x < puzzleWidth; x++) {
+ piecePositions.push([(x + random() * 0.3) / (puzzleWidth + 1),
+ (y + random() * 0.3) / (puzzleHeight + 1)]);
+ }
+ }
+ shuffle(piecePositions);
+ for (const piece of pieces) {
+ piece.x = piecePositions[piece.id][0];
+ piece.y = piecePositions[piece.id][1];
+ piece.updatePosition();
+ }
// a bit janky, but it stops the pieces from animating to their starting positions
setTimeout(() => {
for (const piece of pieces) {
diff --git a/server/src/main.rs b/server/src/main.rs
index a0a1d39..5e62213 100644
--- a/server/src/main.rs
+++ b/server/src/main.rs
@@ -83,7 +83,7 @@ impl Server {
self.database
.execute(
&self.set_puzzle_data,
- &[&width, &height, &url, &connectivity, &positions, &id, &seed],
+ &[&width, &height, &url, &connectivity, &positions, &seed, &id],
)
.await?;
Ok(())
@@ -174,7 +174,7 @@ enum Error {
UTF8(std::str::Utf8Error),
BadPuzzleID,
BadPieceID,
- BadSyntax,
+ BadSyntax(&'static str),
ImageURLTooLong,
TooManyPieces,
TooManyPlayers,
@@ -186,7 +186,7 @@ impl std::fmt::Display for Error {
match self {
Error::BadPieceID => write!(f, "bad piece ID"),
Error::BadPuzzleID => write!(f, "bad puzzle ID"),
- Error::BadSyntax => write!(f, "bad syntax"),
+ Error::BadSyntax(s) => write!(f, "bad syntax: {s}"),
Error::ImageURLTooLong => write!(f, "image URL too long"),
Error::TooManyPieces => write!(f, "too many pieces"),
Error::NotJoined => write!(f, "haven't joined a puzzle"),
@@ -267,29 +267,32 @@ async fn handle_websocket(
let mut parts = dimensions.split(' ');
let width: u8 = parts
.next()
- .ok_or(Error::BadSyntax)?
+ .ok_or(Error::BadSyntax("no width"))?
.parse()
- .map_err(|_| Error::BadSyntax)?;
+ .map_err(|_| Error::BadSyntax("width not integer"))?;
let height: u8 = parts
.next()
- .ok_or(Error::BadSyntax)?
+ .ok_or(Error::BadSyntax("no height"))?
.parse()
- .map_err(|_| Error::BadSyntax)?;
+ .map_err(|_| Error::BadSyntax("height not integer"))?;
if width < 3 || height < 3 {
- return Err(Error::BadSyntax);
+ return Err(Error::BadSyntax("dimensions too small"));
}
if usize::from(width) * usize::from(height) > MAX_PIECES {
return Err(Error::TooManyPieces);
}
- let url: String = parts.next().ok_or(Error::BadSyntax)?.replace(';', " ");
+ let url: String = parts
+ .next()
+ .ok_or(Error::BadSyntax("no URL"))?
+ .replace(';', " ");
if url.len() > 2048 {
return Err(Error::ImageURLTooLong);
}
let seed = parts
.next()
- .ok_or(Error::BadSyntax)?
+ .ok_or(Error::BadSyntax("no seed"))?
.parse()
- .map_err(|_| Error::BadSyntax)?;
+ .map_err(|_| Error::BadSyntax("seed not integer"))?;
let piece_positions = vec![0.0f32; 2 * (width as usize) * (height as usize)];
let mut connectivity_data: Vec<u16> =
Vec::with_capacity(usize::from(width) * usize::from(height));
@@ -319,7 +322,10 @@ async fn handle_websocket(
ws.send(Message::Text(format!("id: {}", std::str::from_utf8(&id)?)))
.await?;
} else if let Some(id) = text.strip_prefix("join ") {
- let id = id.as_bytes().try_into().map_err(|_| Error::BadSyntax)?;
+ let id = id
+ .as_bytes()
+ .try_into()
+ .map_err(|_| Error::BadSyntax("bad join ID"))?;
let mut player_counts = server.player_counts.lock().await;
let entry = player_counts.entry(id).or_default();
if *entry >= MAX_PLAYERS {
@@ -367,14 +373,16 @@ async fn handle_websocket(
}
} else if let Message::Binary(data) = &message {
if data.len() % 4 != 0 {
- return Err(Error::BadSyntax);
+ return Err(Error::BadSyntax("binary message not multiple of 4 bytes"));
}
let puzzle_id = puzzle_id.ok_or(Error::NotJoined)?;
let mut reader_data = std::io::Cursor::new(data);
let reader = &mut reader_data;
fn read<const N: usize>(reader: &mut std::io::Cursor<&Vec<u8>>) -> Result<[u8; N]> {
let mut data = [0; N];
- reader.read_exact(&mut data).map_err(|_| Error::BadSyntax)?;
+ reader
+ .read_exact(&mut data)
+ .map_err(|_| Error::BadSyntax("unexpected EOF in action sequence"))?;
Ok(data)
}
fn read_u32(reader: &mut std::io::Cursor<&Vec<u8>>) -> Result<u32> {
@@ -384,7 +392,7 @@ async fn handle_websocket(
Ok(f32::from_le_bytes(read(reader)?))
}
let message_id = read_u32(reader)?;
- while !reader.get_ref().is_empty() {
+ while reader.position() < reader.get_ref().len() as u64 {
let action = read_u32(reader)?;
if action == ACTION_MOVE {
let piece: usize = read_u32(reader)? as _;
@@ -392,7 +400,7 @@ async fn handle_websocket(
let y: f32 = read_f32(reader)?;
for coord in [x, y] {
if !coord.is_finite() || coord < 0.0 || coord > 2.0 {
- return Err(Error::BadSyntax);
+ return Err(Error::BadSyntax("piece position out of bounds"));
}
}
server.move_piece(puzzle_id, piece, x, y).await?;
@@ -401,7 +409,7 @@ async fn handle_websocket(
let piece2: usize = read_u32(reader)? as _;
server.connect_pieces(puzzle_id, piece1, piece2).await?;
} else {
- return Err(Error::BadSyntax);
+ return Err(Error::BadSyntax("bad action"));
}
}
ws.send(Message::Text(format!("ack {message_id}"))).await?;