diff options
-rw-r--r-- | TODO.txt | 4 | ||||
-rw-r--r-- | game.html | 1 | ||||
-rw-r--r-- | game.js | 180 | ||||
-rw-r--r-- | server/src/main.rs | 227 |
4 files changed, 207 insertions, 205 deletions
@@ -1,6 +1,2 @@ - single player mode with no server -- congratualations - handle server errors on the client side -- let user select piece size -- change piece.x,y to relative (% of play-area) -- webgl? maybe? @@ -13,6 +13,7 @@ <div id="header"> <a href="index.html">← Back</a> <a id="image-link" style="visibility: hidden;">🖼️ Link to image</a> + <button style="display: none;" id="host-multiplayer">👥 Host multiplayer puzzle</button> <a id="join-link" style="display: none;">🔗 Invite others to join</a> <button style="width:3em;overflow:hidden;" id="piece-size-minus">🔍−</button> <button style="width:3em;overflow:hidden;" id="piece-size-plus">🔍+</button> </div> @@ -1,8 +1,11 @@ 'use strict'; window.addEventListener('load', function () { + const ACTION_MOVE = 3; + const ACTION_CONNECT = 4; const socket = new WebSocket(location.protocol === "file:" || location.hostname === "localhost" ? "ws://localhost:54472" : "wss://jigsaw.pommicket.com"); const searchParams = new URL(location.href).searchParams; socket.binaryType = "arraybuffer"; + let puzzleSeed = Math.floor(Math.random() * 0x7fffffff); // direct URL to image file let imageUrl = searchParams.has('image') ? encodeURI(searchParams.get('image')) : undefined; // link to page with info about image (e.g. https://commons.wikimedia.org/wiki/File:Foo.jpg) @@ -32,7 +35,8 @@ window.addEventListener('load', function () { let nibSize; let pieceWidth; let pieceHeight; - let receivedAck = true; + let multiplayer = !joinLink; + let waitingForAck = 0; if (imageUrl && imageUrl.startsWith('http')) { // make sure we use https let url = new URL(imageUrl); @@ -231,7 +235,7 @@ window.addEventListener('load', function () { this.x = x; this.y = y; this.nibTypes = nibTypes; - this.needsServerUpdate = false; + this.needsServerUpdate = true; this.connectedComponent = [this]; const element = this.element = document.createElement('div'); element.classList.add('piece'); @@ -359,7 +363,9 @@ window.addEventListener('load', function () { if (sqDist < connectRadius * connectRadius) { anyConnected = true; connectPieces(piece, neighbour, true); - socket.send(`connect ${piece.id} ${neighbour.id}`); + if (multiplayer) { + socket.send(new Uint32Array([ACTION_CONNECT, piece.id, neighbour.id])); + } } } } @@ -424,33 +430,7 @@ window.addEventListener('load', function () { deriveConnectedPiecePositions(); if (anyConnected) connectAudio.play(); } - async function initPuzzle(payload) { - const data = new Uint8Array(payload, payload.length); - if (joinPuzzle) { - puzzleWidth = data[8]; - puzzleHeight = data[9]; - } else { - console.assert(puzzleWidth === data[8]); - console.assert(puzzleHeight === data[9]); - } - const nibTypesOffset = 10; - const nibTypeCount = 2 * puzzleWidth * puzzleHeight - puzzleWidth - puzzleHeight; - const nibTypes = new Uint16Array(payload, nibTypesOffset, nibTypeCount); - const imageUrlOffset = nibTypesOffset + nibTypeCount * 2; - const imageUrlLen = new Uint8Array(payload, imageUrlOffset, data.length - imageUrlOffset).indexOf(0); - const imageUrlBytes = new Uint8Array(payload, imageUrlOffset, imageUrlLen); - let piecePositionsOffset = imageUrlOffset + imageUrlLen + 1; - piecePositionsOffset = Math.floor((piecePositionsOffset + 7) / 8) * 8; // align to 8 bytes - const piecePositions = new Float32Array(payload, piecePositionsOffset, puzzleWidth * puzzleHeight * 2); - const connectivityOffset = piecePositionsOffset + piecePositions.length * 4; - const connectivity = new Uint16Array(payload, connectivityOffset, puzzleWidth * puzzleHeight); - if (joinPuzzle) { - const parts = new TextDecoder().decode(imageUrlBytes).split(' '); - imageUrl = parts[0]; - imageLink = parts.length > 1 ? parts[1] : parts[0]; - await loadImage(); - } - let nibTypeIndex = 0; + function createPieces() { if (playArea.clientWidth / puzzleWidth < playArea.clientHeight / puzzleHeight) { pieceWidth = 0.8 * playArea.clientWidth / puzzleWidth; pieceHeight = pieceWidth * (puzzleWidth / puzzleHeight) * (image.height / image.width); @@ -470,18 +450,68 @@ window.addEventListener('load', function () { let id = pieces.length; if (v > 0) nibs[0] = pieces[id - puzzleWidth].nibTypes[2].inverse(); if (u < puzzleWidth - 1) { - setRandomSeed(nibTypes[nibTypeIndex++]); nibs[1] = NibType.random(Math.floor(random() * 2) ? RIGHT_IN : RIGHT_OUT); } if (v < puzzleHeight - 1) { - setRandomSeed(nibTypes[nibTypeIndex++]); nibs[2] = NibType.random(Math.floor(random() * 2) ? BOTTOM_IN : BOTTOM_OUT); } if (u > 0) nibs[3] = pieces[id - 1].nibTypes[1].inverse(); pieces.push(new Piece(id, 0, 0, nibs)); } } - console.assert(nibTypeIndex === nibTypeCount); + } + async function createPuzzle() { + await loadImage(); + if (isNaN(roughPieceCount) || roughPieceCount < 10 || roughPieceCount > 1000) { + // TODO : better error reporting + console.error('bad piece count'); + return; + } + let bestWidth = 1; + let bestDiff = Infinity; + function heightFromWidth(w) { + return Math.min(255, Math.max(2, Math.round(w * image.height / image.width))); + } + for (let width = 2; width < 256; width++) { + const height = heightFromWidth(width); + if (width * height > 1000) break; + const diff = Math.abs(width * height - roughPieceCount); + if (diff < bestDiff) { + bestDiff = diff; + bestWidth = width; + } + } + puzzleWidth = bestWidth; + puzzleHeight = heightFromWidth(puzzleWidth); + getById('host-multiplayer').style.display = 'inline-block'; + setRandomSeed(puzzleSeed); + createPieces(); + // a bit janky, but it stops the pieces from animating to their starting positions + setTimeout(() => { + for (const piece of pieces) { + piece.setAnimate(true); + } + }, 100); + } + async function initPuzzle(payload) { + const data = new Uint8Array(payload, payload.length); + puzzleSeed = new Uint32Array(payload, 8, 1)[0]; + setRandomSeed(puzzleSeed); + puzzleWidth = data[12]; + puzzleHeight = data[13]; + const imageUrlOffset = 14; + const imageUrlLen = new Uint8Array(payload, imageUrlOffset, data.length - imageUrlOffset).indexOf(0); + const imageUrlBytes = new Uint8Array(payload, imageUrlOffset, imageUrlLen); + let piecePositionsOffset = imageUrlOffset + imageUrlLen + 1; + piecePositionsOffset = Math.floor((piecePositionsOffset + 7) / 8) * 8; // align to 8 bytes + const piecePositions = new Float32Array(payload, piecePositionsOffset, puzzleWidth * puzzleHeight * 2); + const connectivityOffset = piecePositionsOffset + piecePositions.length * 4; + const connectivity = new Uint16Array(payload, connectivityOffset, puzzleWidth * puzzleHeight); + const parts = new TextDecoder().decode(imageUrlBytes).split(' '); + imageUrl = parts[0]; + imageLink = parts.length > 1 ? parts[1] : parts[0]; + await loadImage(); + createPieces(); for (let id = 0; id < pieces.length; id++) { if (id !== connectivity[id]) continue; // only set one piece positions per piece group pieces[id].x = piecePositions[2 * connectivity[id]]; @@ -496,6 +526,10 @@ window.addEventListener('load', function () { } }, 100); } + async function hostPuzzle() { + socket.send(`new ${puzzleWidth} ${puzzleHeight} ${imageUrl};${imageLink} ${puzzleSeed}`); + multiplayer = true; + } function applyUpdate(update) { const piecePositions = new Float32Array(update, 8, puzzleWidth * puzzleHeight * 2); const connectivity = new Uint16Array(update, 8 + piecePositions.length * 4, puzzleWidth * puzzleHeight); @@ -524,48 +558,34 @@ window.addEventListener('load', function () { } function sendServerUpdate() { // send update to server - if (!receivedAck) return; // last update hasn't been acknowledged yet - const motions = []; + if (!multiplayer) return; + if (waitingForAck) return; // last update hasn't been acknowledged yet + let actions = new Uint32Array(pieces.length * 4 + 1); + const pos = new Float32Array(2); + const posU8 = new Uint8Array(pos.buffer); + let i = 0; + let messageID = 0x12345678; // message ID for regular updates + actions[i++] = messageID; for (const piece of pieces) { if (!piece.needsServerUpdate) continue; - motions.push(`move ${piece.id} ${piece.x} ${piece.y}`); + actions[i++] = ACTION_MOVE; + actions[i++] = piece.id; + pos[0] = piece.x; + pos[1] = piece.y; + new Uint8Array(actions.buffer, 4 * i, 8).set(posU8); + i += 2; } - if (motions.length) { - receivedAck = false; - socket.send(motions.join('\n')); + if (i > 1) { + waitingForAck = messageID; + socket.send(new Uint32Array(actions.buffer, 0, i)); } - } - async function hostPuzzle() { - await loadImage(); - if (isNaN(roughPieceCount) || roughPieceCount < 10 || roughPieceCount > 1000) { - // TODO : better error reporting - console.error('bad piece count'); - return; - } - let bestWidth = 1; - let bestDiff = Infinity; - function heightFromWidth(w) { - return Math.min(255, Math.max(2, Math.round(w * image.height / image.width))); - } - for (let width = 2; width < 256; width++) { - const height = heightFromWidth(width); - if (width * height > 1000) break; - const diff = Math.abs(width * height - roughPieceCount); - if (diff < bestDiff) { - bestDiff = diff; - bestWidth = width; - } - } - puzzleWidth = bestWidth; - puzzleHeight = heightFromWidth(puzzleWidth); - socket.send(`new ${puzzleWidth} ${puzzleHeight} ${imageUrl};${imageLink}`); } let waitingForServerToGiveUsImageUrl = false; socket.addEventListener('open', async () => { if (joinPuzzle) { socket.send(`join ${joinPuzzle}`); } else if (imageUrl.startsWith('http')) { - hostPuzzle(); + createPuzzle(); } else if (imageUrl === 'randomFeaturedWikimedia') { socket.send('randomFeaturedWikimedia'); waitingForServerToGiveUsImageUrl = true; @@ -581,19 +601,30 @@ window.addEventListener('load', function () { if (typeof e.data === 'string') { if (e.data.startsWith('id: ')) { let puzzleID = e.data.split(' ')[1]; + sendServerUpdate(); // send piece positions + const connectivityUpdate = [0 /* message ID */]; + for (const piece of pieces) { + if (piece !== piece.connectedComponent[0]) { + connectivityUpdate.push(ACTION_CONNECT, piece.connectedComponent[0].id, piece.id); + } + } + socket.send(new Uint32Array(connectivityUpdate)); history.pushState({}, null, `?join=${puzzleID}`); setJoinLink(puzzleID); - } else if (e.data === 'ack') { - for (const piece of pieces) { - piece.needsServerUpdate = false; + } else if (e.data.startsWith('ack')) { + const messageID = parseInt(e.data.split(' ')[1]); + if (messageID === waitingForAck) { + for (const piece of pieces) { + piece.needsServerUpdate = false; + } + waitingForAck = 0; } - receivedAck = true; } else if (waitingForServerToGiveUsImageUrl && e.data.startsWith('useImage ')) { waitingForServerToGiveUsImageUrl = false; const parts = e.data.substring('useImage '.length).split(' '); imageUrl = parts[0]; imageLink = parts.length > 1 ? parts[1] : imageUrl; - hostPuzzle(); + createPuzzle(); } else if (e.data.startsWith('error ')) { const error = e.data.substring('error '.length); console.error(error); // TODO : better error handling @@ -602,7 +633,11 @@ window.addEventListener('load', function () { const opcode = new Uint8Array(e.data, 0, 1)[0]; if (opcode === 1 && !pieces.length) { // init puzzle await initPuzzle(e.data); - setInterval(() => socket.send('poll'), 1000); + setInterval(() => { + if (multiplayer) { + socket.send('poll'); + } + }, 1000); setInterval(sendServerUpdate, 1000); } else if (opcode === 2) { // update puzzle applyUpdate(e.data); @@ -634,5 +669,8 @@ window.addEventListener('load', function () { getById('piece-size-minus').addEventListener('click', () => { setPieceSize(pieceWidth / 1.2, pieceHeight / 1.2); }); + getById('host-multiplayer').addEventListener('click', () => { + hostPuzzle(); + }); requestAnimationFrame(everyFrame); }); diff --git a/server/src/main.rs b/server/src/main.rs index 7de3d68..a0a1d39 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -17,6 +17,8 @@ const PUZZLE_ID_CHARSET: &[u8] = b"23456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLM const PUZZLE_ID_LEN: usize = 7; const MAX_PLAYERS: u16 = 20; const MAX_PIECES: usize = 1000; +const ACTION_MOVE: u32 = 3; +const ACTION_CONNECT: u32 = 4; fn generate_puzzle_id() -> [u8; PUZZLE_ID_LEN] { let mut rng = rand::thread_rng(); @@ -47,41 +49,11 @@ struct PuzzleInfo { width: u8, height: u8, url: String, - nib_types: Vec<i16>, + seed: u32, piece_info: PieceInfo, } impl Server { - async fn create_table_if_not_exists(&self) -> Result<()> { - if self - .database - .query("SELECT FROM puzzles", &[]) - .await - .is_err() - { - self.database - .execute( - &format!( - "CREATE TABLE puzzles ( - id char({PUZZLE_ID_LEN}) PRIMARY KEY, - url text, - width int4, - height int4, - create_time timestamp DEFAULT CURRENT_TIMESTAMP, - nib_types int2[], - connectivity int2[], - positions float4[] - )" - ), - &[], - ) - .await?; - self.database - .execute("CREATE INDEX by_id ON puzzles (id)", &[]) - .await?; - } - Ok(()) - } async fn try_register_id(&self, id: [u8; PUZZLE_ID_LEN]) -> Result<bool> { let id = std::str::from_utf8(&id)?; Ok(self @@ -96,31 +68,22 @@ impl Server { width: u8, height: u8, url: &str, - nib_types: Vec<u16>, piece_positions: &[f32], connectivity: Vec<u16>, + seed: u32, ) -> Result<()> { let id = std::str::from_utf8(&id)?; let width = i32::from(width); let height = i32::from(height); + let seed = seed as i32; // transmuting u16 to i16 should never give an error. they have the same alignment. - let nib_types: &[i16] = - transmute_many_pedantic(transmute_to_bytes(&nib_types[..])).unwrap(); let connectivity: &[i16] = transmute_many_pedantic(transmute_to_bytes(&connectivity[..])).unwrap(); let positions = &piece_positions; self.database .execute( &self.set_puzzle_data, - &[ - &width, - &height, - &url, - &nib_types, - &connectivity, - &positions, - &id, - ], + &[&width, &height, &url, &connectivity, &positions, &id, &seed], ) .await?; Ok(()) @@ -179,13 +142,13 @@ impl Server { let height: i32 = row.try_get(1)?; let url: String = row.try_get(2)?; let positions: Vec<f32> = row.try_get(3)?; - let nib_types: Vec<i16> = row.try_get(4)?; + let seed: i32 = row.try_get(4)?; let connectivity: Vec<i16> = row.try_get(5)?; Ok(PuzzleInfo { width: width as u8, height: height as u8, url, - nib_types, + seed: seed as u32, piece_info: PieceInfo { positions, connectivity, @@ -267,15 +230,15 @@ async fn get_puzzle_info(server: &Server, id: &[u8]) -> Result<Vec<u8>> { width, height, url, - nib_types, + seed, piece_info: PieceInfo { positions, connectivity, }, } = server.get_puzzle_info(id).await?; + data.extend(seed.to_le_bytes()); data.push(width); data.push(height); - data.extend(transmute_to_bytes(&nib_types[..])); data.extend(url.as_bytes()); data.push(0); while data.len() % 8 != 0 { @@ -315,37 +278,19 @@ async fn handle_websocket( if width < 3 || height < 3 { return Err(Error::BadSyntax); } - let url: String = parts.next().ok_or(Error::BadSyntax)?.replace(';', " "); - if url.len() > 2048 { - return Err(Error::ImageURLTooLong); - } if usize::from(width) * usize::from(height) > MAX_PIECES { return Err(Error::TooManyPieces); } - let nib_count = 2 * usize::from(width) * usize::from(height) - - usize::from(width) - usize::from(height); - let mut nib_types: Vec<u16> = Vec::with_capacity(nib_count); - let mut piece_positions: Vec<[f32; 2]> = - Vec::with_capacity((width as usize) * (height as usize)); - { - let mut rng = rand::thread_rng(); - // pick nib types - for _ in 0..nib_count { - nib_types.push(rng.gen()); - } - // pick piece positions - for y in 0..u16::from(height) { - for x in 0..u16::from(width) { - let dx: f32 = rng.gen_range(0.0..0.3); - let dy: f32 = rng.gen_range(0.0..0.3); - piece_positions.push([ - (f32::from(x) + dx) / (f32::from(width) + 1.0), - (f32::from(y) + dy) / (f32::from(height) + 1.0), - ]); - } - } - piece_positions.shuffle(&mut rng); + let url: String = parts.next().ok_or(Error::BadSyntax)?.replace(';', " "); + if url.len() > 2048 { + return Err(Error::ImageURLTooLong); } + let seed = parts + .next() + .ok_or(Error::BadSyntax)? + .parse() + .map_err(|_| Error::BadSyntax)?; + 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)); for i in 0..u16::from(width) * u16::from(height) { @@ -364,17 +309,15 @@ async fn handle_websocket( width, height, &url, - nib_types, - piece_positions.as_flattened(), + &piece_positions, connectivity_data, + seed, ) .await?; server.player_counts.lock().await.insert(id, 1); *puzzle_id = Some(id); ws.send(Message::Text(format!("id: {}", std::str::from_utf8(&id)?))) .await?; - let info = get_puzzle_info(server, &id).await?; - ws.send(Message::Binary(info)).await?; } else if let Some(id) = text.strip_prefix("join ") { let id = id.as_bytes().try_into().map_err(|_| Error::BadSyntax)?; let mut player_counts = server.player_counts.lock().await; @@ -387,48 +330,6 @@ async fn handle_websocket( *puzzle_id = Some(id); let info = get_puzzle_info(server, &id).await?; ws.send(Message::Binary(info)).await?; - } else if text.starts_with("move ") { - let puzzle_id = puzzle_id.ok_or(Error::NotJoined)?; - for line in text.split('\n') { - let mut parts = line.split(' '); - parts.next(); // skip "move" - let piece: usize = parts - .next() - .ok_or(Error::BadSyntax)? - .parse() - .map_err(|_| Error::BadSyntax)?; - let x: f32 = parts - .next() - .ok_or(Error::BadSyntax)? - .parse() - .map_err(|_| Error::BadSyntax)?; - let y: f32 = parts - .next() - .ok_or(Error::BadSyntax)? - .parse() - .map_err(|_| Error::BadSyntax)?; - for coord in [x, y] { - if !coord.is_finite() || coord < 0.0 || coord > 2.0 { - return Err(Error::BadSyntax); - } - } - server.move_piece(puzzle_id, piece, x, y).await?; - } - 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(Error::NotJoined)?; - let piece1: usize = parts - .next() - .ok_or(Error::BadSyntax)? - .parse() - .map_err(|_| Error::BadSyntax)?; - let piece2: usize = parts - .next() - .ok_or(Error::BadSyntax)? - .parse() - .map_err(|_| Error::BadSyntax)?; - server.connect_pieces(puzzle_id, piece1, piece2).await?; } else if text == "poll" { let puzzle_id = puzzle_id.ok_or(Error::NotJoined)?; let PieceInfo { @@ -464,6 +365,46 @@ async fn handle_websocket( ))) .await?; } + } else if let Message::Binary(data) = &message { + if data.len() % 4 != 0 { + return Err(Error::BadSyntax); + } + 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)?; + Ok(data) + } + fn read_u32(reader: &mut std::io::Cursor<&Vec<u8>>) -> Result<u32> { + Ok(u32::from_le_bytes(read(reader)?)) + } + fn read_f32(reader: &mut std::io::Cursor<&Vec<u8>>) -> Result<f32> { + Ok(f32::from_le_bytes(read(reader)?)) + } + let message_id = read_u32(reader)?; + while !reader.get_ref().is_empty() { + let action = read_u32(reader)?; + if action == ACTION_MOVE { + let piece: usize = read_u32(reader)? as _; + let x: f32 = read_f32(reader)?; + 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); + } + } + server.move_piece(puzzle_id, piece, x, y).await?; + } else if action == ACTION_CONNECT { + let piece1: usize = read_u32(reader)? as _; + let piece2: usize = read_u32(reader)? as _; + server.connect_pieces(puzzle_id, piece1, piece2).await?; + } else { + return Err(Error::BadSyntax); + } + } + ws.send(Message::Text(format!("ack {message_id}"))).await?; } } Ok(()) @@ -474,8 +415,8 @@ async fn handle_connection(server: &Server, conn: &mut tokio::net::TcpStream) -> let mut ws = tokio_tungstenite::accept_async_with_config( conn, Some(tungstenite::protocol::WebSocketConfig { - max_message_size: Some(65536), - max_frame_size: Some(65536), + max_message_size: Some(128 << 10), + max_frame_size: Some(128 << 10), ..Default::default() }), ) @@ -525,6 +466,32 @@ async fn get_potd() -> String { } } +async fn create_table_if_doesnt_exist(database: &tokio_postgres::Client) -> Result<()> { + if database.query("SELECT FROM puzzles", &[]).await.is_err() { + database + .execute( + &format!( + "CREATE TABLE puzzles ( + id char({PUZZLE_ID_LEN}) PRIMARY KEY, + url text, + width int4, + height int4, + create_time timestamp DEFAULT CURRENT_TIMESTAMP, + seed int4, + connectivity int2[], + positions float4[] + )" + ), + &[], + ) + .await?; + database + .execute("CREATE INDEX by_id ON puzzles (id)", &[]) + .await?; + } + Ok(()) +} + #[tokio::main] async fn main() { let port = 54472; @@ -554,6 +521,10 @@ async fn main() { eprintln!("connection error: {}", e); } }); + if let Err(e) = create_table_if_doesnt_exist(&client).await { + eprintln!("couldn't create table: {e}"); + return; + }; use tokio_postgres::types::Type; let create_puzzle = client .prepare_typed("INSERT INTO puzzles (id) VALUES ($1)", &[Type::BPCHAR]) @@ -561,15 +532,15 @@ async fn main() { .expect("couldn't prepare create_puzzle statement"); let set_puzzle_data = client .prepare_typed( - "UPDATE puzzles SET width = $1, height = $2, url = $3, nib_types = $4, - connectivity = $5, positions = $6 WHERE id = $7", + "UPDATE puzzles SET width = $1, height = $2, url = $3, + connectivity = $4, positions = $5, seed = $6 WHERE id = $7", &[ Type::INT4, Type::INT4, Type::TEXT, Type::INT2_ARRAY, - Type::INT2_ARRAY, Type::FLOAT4_ARRAY, + Type::INT4, Type::BPCHAR, ], ) @@ -589,7 +560,7 @@ async fn main() { ) .await .expect("couldn't prepare get_piece_info statement"); - let get_puzzle_info = client.prepare_typed("SELECT width, height, url, positions, nib_types, connectivity FROM puzzles WHERE id = $1", &[Type::BPCHAR]) + let get_puzzle_info = client.prepare_typed("SELECT width, height, url, positions, seed, connectivity FROM puzzles WHERE id = $1", &[Type::BPCHAR]) .await.expect("couldn't prepare get_puzzle_info statement"); Server { player_counts: Mutex::new(HashMap::new()), @@ -604,10 +575,6 @@ async fn main() { wikimedia_featured, } })); - server - .create_table_if_not_exists() - .await - .expect("error creating table"); tokio::task::spawn(async move { fn next_day(t: SystemTime) -> SystemTime { let day = 60 * 60 * 24; |