From 3917c591b0e4a0074c93024855e671a3d2fedce3 Mon Sep 17 00:00:00 2001 From: Zutatensuppe Date: Sun, 8 Nov 2020 14:13:43 +0100 Subject: [PATCH] move puzzle generation to server --- game/Camera.js | 24 ++--- game/index.js | 201 ++++----------------------------------- server/index.js | 27 ++++-- server/package-lock.json | 16 ++++ server/package.json | 1 + server/puzzle.js | 151 +++++++++++++++++++++++++++++ {game => server}/util.js | 0 7 files changed, 207 insertions(+), 213 deletions(-) create mode 100644 server/puzzle.js rename {game => server}/util.js (100%) diff --git a/game/Camera.js b/game/Camera.js index 6c8c99b..4c919ed 100644 --- a/game/Camera.js +++ b/game/Camera.js @@ -17,24 +17,12 @@ export default class Camera { } rect() { - // when no zoom is relevant: - return new BoundingRectangle( - this.x, - this.x + this.width - 1, - this.y, - this.y + this.height - 1 - ) - - // when zoom is relevant: - // TODO: check if still true - const w_final = this.width * this.zoom - const h_final = this.height * this.zoom - return new BoundingRectangle( - this.x + (this.width - w_final) / 2, - this.x + (this.width + w_final) / 2, - this.y + (this.height - h_final) / 2, - this.y + (this.height + h_final) / 2 - ) + return new BoundingRectangle( + - this.x, + - this.x + (this.width / this.zoom), + - this.y, + - this.y + (this.height / this.zoom), + ) } move(x, y) { diff --git a/game/index.js b/game/index.js index 9c5c208..ef5dee5 100644 --- a/game/index.js +++ b/game/index.js @@ -5,23 +5,10 @@ import Bitmap from './Bitmap.js' import {run} from './gameloop.js' import Camera from './Camera.js' import EventAdapter from './EventAdapter.js' -import { choice } from './util.js' import WsClient from './WsClient.js' if (!WS_ADDRESS) throw '[ WS_ADDRESS not set ]' -const TILE_SIZE = 64 // cut size of each puzzle tile in the - // final resized version of the puzzle image -const TARGET_TILES = 1000 // desired number of tiles - // actual calculated number can be higher -const IMAGES = [ - './example-images/ima_86ec3fa.jpeg', - './example-images/bleu.png', - './example-images/saechsische_schweiz.jpg', - './example-images/132-2048x1365.jpg', -] -const IMAGE_URL = IMAGES[0] - function createCanvas(width = 0, height = 0) { const canvas = document.createElement('canvas') canvas.width = width === 0 ? window.innerWidth : width @@ -235,55 +222,6 @@ function pointInBounds(pt, rect) { && pt.y <= rect.y1 } -const determinePuzzleInfo = (w, h, targetTiles) => { - let tileSize = 0 - let tiles = 0 - do { - tileSize++ - tiles = tilesFit(w, h, tileSize) - } while (tiles >= targetTiles) - tileSize-- - - tiles = tilesFit(w, h, tileSize) - const tiles_x = Math.round(w / tileSize) - const tiles_y = Math.round(h / tileSize) - tiles = tiles_x * tiles_y - - // then resize to final TILE_SIZE (which is always the same) - tileSize = TILE_SIZE - const width = tiles_x * tileSize - const height = tiles_y * tileSize - const coords = coordsByNum({ width, height, tileSize, tiles }) - - const tileMarginWidth = tileSize * .5; - const tileDrawSize = Math.round(tileSize + tileMarginWidth * 2) - - return { - width, - height, - tileSize, - tileMarginWidth, - tileDrawSize, - tiles, - tiles_x, - tiles_y, - coords, - } -} - -const tilesFit = (w, h, size) => Math.floor(w / size) * Math.floor(h / size) - -const coordsByNum = (puzzleInfo) => { - const w_tiles = puzzleInfo.width / puzzleInfo.tileSize - const coords = new Array(puzzleInfo.tiles) - for (let i = 0; i < puzzleInfo.tiles; i++) { - const y = Math.floor(i / w_tiles) - const x = i % w_tiles - coords[i] = { x, y } - } - return coords -} - const resizeBitmap = (bitmap, width, height) => { const tmp = new Bitmap(width, height) mapBitmapToBitmap( @@ -311,21 +249,6 @@ function getSurroundingTilesByIdx(puzzle, idx) { ] } -function determinePuzzleTileShapes(info) { - const tabs = [-1, 1] - - const shapes = new Array(info.tiles) - for (let i = 0; i < info.tiles; i++) { - shapes[i] = { - top: info.coords[i].y === 0 ? 0 : shapes[i - info.tiles_x].bottom * -1, - right: info.coords[i].x === info.tiles_x - 1 ? 0 : choice(tabs), - left: info.coords[i].x === 0 ? 0 : shapes[i - 1].right * -1, - bottom: info.coords[i].y === info.tiles_y - 1 ? 0 : choice(tabs), - } - } - return shapes -} - async function createPuzzleTileBitmaps(bitmap, tiles, info) { let img = await bitmap.toImage() var tileSize = info.tileSize @@ -475,82 +398,6 @@ async function loadPuzzleBitmaps(puzzle) { return await createPuzzleTileBitmaps(bmpResized, puzzle.tiles, puzzle.info) } -async function createPuzzle(targetTiles, imageUrl) { - // load bitmap, to determine the original size of the image - let bmp = await loadImageToBitmap(imageUrl) - - // determine puzzle information from the bitmap - let info = determinePuzzleInfo(bmp.width, bmp.height, targetTiles) - - let tiles = new Array(info.tiles) - for (let i = 0; i < tiles.length; i++) { - tiles[i] = { - idx: i, - } - } - const shapes = determinePuzzleTileShapes(info) - - // Complete puzzle object - const p = { - // tiles array - tiles: tiles.map(tile => { - return { - idx: tile.idx, // index of tile in the array - group: 0, // if grouped with other tiles - z: 0, // z index of the tile - owner: 0, // who owns the tile - // 0 = free for taking - // -1 = finished - // other values: id of player who has the tile - // physical current position of the tile (x/y in pixels) - // this position is the initial position only and is the - // value that changes when moving a tile - // TODO: scatter the tiles on the table at the beginning - pos: { - x: info.coords[tile.idx].x * info.tileSize, - y: info.coords[tile.idx].y * info.tileSize, - }, - } - }), - // game data for puzzle, data changes during the game - data: { - // TODO: maybe calculate this each time? - maxZ: 0, // max z of all pieces - maxGroup: 0, // max group of all pieces - }, - // static puzzle information. stays same for complete duration of - // the game - info: { - // information that was used to create the puzzle - targetTiles: targetTiles, - imageUrl: imageUrl, - - width: info.width, // actual puzzle width (same as bitmap.width) - height: info.height, // actual puzzle height (same as bitmap.height) - tileSize: info.tileSize, // width/height of each tile (without tabs) - tileDrawSize: info.tileDrawSize, // width/height of each tile (with tabs) - tileMarginWidth: info.tileMarginWidth, - // offset in x and y when drawing tiles, so that they appear to be at pos - tileDrawOffset: (info.tileDrawSize - info.tileSize) / -2, - // max distance between tile and destination that - // makes the tile snap to destination - snapDistance: info.tileSize / 2, - tiles: info.tiles, // the final number of tiles in the puzzle - tiles_x: info.tiles_x, // number of tiles each row - tiles_y: info.tiles_y, // number of tiles each col - coords: info.coords, // map of tile index to its coordinates - // ( index => {x, y} ) - // this is not the physical coordinate, but - // the tile_coordinate - // this can be used to determine where the - // final destination of a tile is - shapes: shapes, // tile shapes - }, - } - return p -} - - function uniqId() { return Date.now().toString(36) + Math.random().toString(36).substring(2) } @@ -584,27 +431,10 @@ async function main () { conn.send(JSON.stringify({ type: 'init' })) conn.onSocket('message', async ({data}) => { const d = JSON.parse(data) - let game if (d.type === 'init') { - game = d.game - if (game.puzzle) { - console.log('loaded from server') - } else { - // The game doesnt exist yet on the server, so load puzzle - // and then give the server some info about the puzzle - // Load puzzle and determine information about it - // TODO: move puzzle creation to server - game.puzzle = await createPuzzle(TARGET_TILES, IMAGE_URL) - conn.send(JSON.stringify({ - type: 'init_puzzle', - puzzle: game.puzzle, - })) - console.log('loaded from local config') - } - - console.log('the game ', game) - let bitmaps = await loadPuzzleBitmaps(game.puzzle) - startGame(game, bitmaps, conn) + console.log('the game ', d.game) + let bitmaps = await loadPuzzleBitmaps(d.game.puzzle) + startGame(d.game, bitmaps, conn) } else { // console.log(d) } @@ -1153,22 +983,23 @@ async function main () { // TODO: improve the rendering // atm it is pretty slow (~40-50ms) - mapBitmapToAdapter(puzzleTable, new BoundingRectangle( - - cam.x, - - cam.x + (cam.width / cam.zoom), - - cam.y, - - cam.y + (cam.height / cam.zoom), - ), adapter, adapter.getBoundingRect()) + mapBitmapToAdapter( + puzzleTable, + cam.rect(), + adapter, + adapter.getBoundingRect() + ) checkpoint('to_adapter_1') } else if (rerenderPlayer) { adapter.clearRect(rectPlayer.get()) checkpoint('afterclear_2') - mapBitmapToAdapterCapped(puzzleTable, new BoundingRectangle( - - cam.x, - - cam.x + (cam.width / cam.zoom), - - cam.y, - - cam.y + (cam.height / cam.zoom), - ), adapter, adapter.getBoundingRect(), rectPlayer.get()) + mapBitmapToAdapterCapped( + puzzleTable, + cam.rect(), + adapter, + adapter.getBoundingRect(), + rectPlayer.get() + ) checkpoint('to_adapter_2') } diff --git a/server/index.js b/server/index.js index a4a65a7..f41b108 100644 --- a/server/index.js +++ b/server/index.js @@ -1,9 +1,23 @@ import WebSocketServer from './WebSocketServer.js' import express from 'express' - +import { createPuzzle } from './puzzle.js' import config from './config.js' +// desired number of tiles +// actual calculated number can be higher +const TARGET_TILES = 1000 + +const IMAGES = [ + '/example-images/ima_86ec3fa.jpeg', + '/example-images/bleu.png', + '/example-images/saechsische_schweiz.jpg', + '/example-images/132-2048x1365.jpg', +] +const IMAGE_URL = IMAGES[0] + +const games = {} + const port = config.http.port const hostname = config.http.hostname const app = express() @@ -27,8 +41,6 @@ app.use('/', (req, res, next) => { }) app.listen(port, hostname, () => console.log(`server running on http://${hostname}:${port}`)) -const games = {} - const wss = new WebSocketServer(config.ws); const notify = (data) => { @@ -37,7 +49,7 @@ const notify = (data) => { console.log('notify', data) } -wss.on('message', ({socket, data}) => { +wss.on('message', async ({socket, data}) => { try { const proto = socket.protocol.split('|') const uid = proto[0] @@ -46,7 +58,7 @@ wss.on('message', ({socket, data}) => { switch (parsed.type) { case 'init': { // a new player (or previous player) joined - games[gid] = games[gid] || {puzzle: null, players: {}} + games[gid] = games[gid] || {puzzle: await createPuzzle(TARGET_TILES, IMAGE_URL), players: {}} games[gid].players[uid] = {id: uid, x: 0, y: 0, down: false} @@ -59,11 +71,6 @@ wss.on('message', ({socket, data}) => { }, socket) } break; - // new puzzle was created and sent to us - case 'init_puzzle': { - games[gid].puzzle = parsed.puzzle - } break; - // somebody has changed the state case 'state': { for (let change of parsed.state.changes) { diff --git a/server/package-lock.json b/server/package-lock.json index f615503..3174937 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -180,6 +180,14 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "image-size": { + "version": "0.9.3", + "resolved": "https://npm.stroeermediabrands.de/image-size/-/image-size-0.9.3.tgz", + "integrity": "sha512-5SakFa79uhUVSjKeQE30GVzzLJ0QNzB53+I+/VD1vIesD6GP6uatWIlgU0uisFNLt1u0d6kBydp7yfk+lLJhLQ==", + "requires": { + "queue": "6.0.1" + } + }, "inherits": { "version": "2.0.3", "resolved": "https://npm.stroeermediabrands.de/inherits/-/inherits-2.0.3.tgz", @@ -265,6 +273,14 @@ "resolved": "https://npm.stroeermediabrands.de/qs/-/qs-6.7.0.tgz", "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, + "queue": { + "version": "6.0.1", + "resolved": "https://npm.stroeermediabrands.de/queue/-/queue-6.0.1.tgz", + "integrity": "sha512-AJBQabRCCNr9ANq8v77RJEv73DPbn55cdTb+Giq4X0AVnNVZvMHlYp7XlQiN+1npCZj1DuSmaA2hYVUUDgxFDg==", + "requires": { + "inherits": "~2.0.3" + } + }, "range-parser": { "version": "1.2.1", "resolved": "https://npm.stroeermediabrands.de/range-parser/-/range-parser-1.2.1.tgz", diff --git a/server/package.json b/server/package.json index 2d2cfa1..3b244b0 100644 --- a/server/package.json +++ b/server/package.json @@ -2,6 +2,7 @@ "type": "module", "dependencies": { "express": "^4.17.1", + "image-size": "^0.9.3", "ws": "^7.3.1" } } diff --git a/server/puzzle.js b/server/puzzle.js new file mode 100644 index 0000000..1cfbdc5 --- /dev/null +++ b/server/puzzle.js @@ -0,0 +1,151 @@ +import sizeOf from 'image-size' +import { choice } from './util.js' + +// cut size of each puzzle tile in the +// final resized version of the puzzle image +const TILE_SIZE = 64 + +async function createPuzzle(targetTiles, image) { + const imgFile = './../game' + image + const imgUrl = './' + image + // load bitmap, to determine the original size of the image + let dim = sizeOf(imgFile) + + // determine puzzle information from the bitmap + let info = determinePuzzleInfo(dim.width, dim.height, targetTiles) + + let tiles = new Array(info.tiles) + for (let i = 0; i < tiles.length; i++) { + tiles[i] = { + idx: i, + } + } + const shapes = determinePuzzleTileShapes(info) + + // Complete puzzle object + const p = { + // tiles array + tiles: tiles.map(tile => { + return { + idx: tile.idx, // index of tile in the array + group: 0, // if grouped with other tiles + z: 0, // z index of the tile + owner: 0, // who owns the tile + // 0 = free for taking + // -1 = finished + // other values: id of player who has the tile + // physical current position of the tile (x/y in pixels) + // this position is the initial position only and is the + // value that changes when moving a tile + // TODO: scatter the tiles on the table at the beginning + pos: { + x: info.coords[tile.idx].x * info.tileSize, + y: info.coords[tile.idx].y * info.tileSize, + }, + } + }), + // game data for puzzle, data changes during the game + data: { + // TODO: maybe calculate this each time? + maxZ: 0, // max z of all pieces + maxGroup: 0, // max group of all pieces + }, + // static puzzle information. stays same for complete duration of + // the game + info: { + // information that was used to create the puzzle + targetTiles: targetTiles, + imageUrl: imgUrl, + + width: info.width, // actual puzzle width (same as bitmap.width) + height: info.height, // actual puzzle height (same as bitmap.height) + tileSize: info.tileSize, // width/height of each tile (without tabs) + tileDrawSize: info.tileDrawSize, // width/height of each tile (with tabs) + tileMarginWidth: info.tileMarginWidth, + // offset in x and y when drawing tiles, so that they appear to be at pos + tileDrawOffset: (info.tileDrawSize - info.tileSize) / -2, + // max distance between tile and destination that + // makes the tile snap to destination + snapDistance: info.tileSize / 2, + tiles: info.tiles, // the final number of tiles in the puzzle + tiles_x: info.tiles_x, // number of tiles each row + tiles_y: info.tiles_y, // number of tiles each col + coords: info.coords, // map of tile index to its coordinates + // ( index => {x, y} ) + // this is not the physical coordinate, but + // the tile_coordinate + // this can be used to determine where the + // final destination of a tile is + shapes: shapes, // tile shapes + }, + } + return p +} + +function determinePuzzleTileShapes(info) { + const tabs = [-1, 1] + + const shapes = new Array(info.tiles) + for (let i = 0; i < info.tiles; i++) { + shapes[i] = { + top: info.coords[i].y === 0 ? 0 : shapes[i - info.tiles_x].bottom * -1, + right: info.coords[i].x === info.tiles_x - 1 ? 0 : choice(tabs), + left: info.coords[i].x === 0 ? 0 : shapes[i - 1].right * -1, + bottom: info.coords[i].y === info.tiles_y - 1 ? 0 : choice(tabs), + } + } + return shapes +} + +const determinePuzzleInfo = (w, h, targetTiles) => { + let tileSize = 0 + let tiles = 0 + do { + tileSize++ + tiles = tilesFit(w, h, tileSize) + } while (tiles >= targetTiles) + tileSize-- + + tiles = tilesFit(w, h, tileSize) + const tiles_x = Math.round(w / tileSize) + const tiles_y = Math.round(h / tileSize) + tiles = tiles_x * tiles_y + + // then resize to final TILE_SIZE (which is always the same) + tileSize = TILE_SIZE + const width = tiles_x * tileSize + const height = tiles_y * tileSize + const coords = coordsByNum({ width, height, tileSize, tiles }) + + const tileMarginWidth = tileSize * .5; + const tileDrawSize = Math.round(tileSize + tileMarginWidth * 2) + + return { + width, + height, + tileSize, + tileMarginWidth, + tileDrawSize, + tiles, + tiles_x, + tiles_y, + coords, + } +} + +const tilesFit = (w, h, size) => Math.floor(w / size) * Math.floor(h / size) + +const coordsByNum = (puzzleInfo) => { + const w_tiles = puzzleInfo.width / puzzleInfo.tileSize + const coords = new Array(puzzleInfo.tiles) + for (let i = 0; i < puzzleInfo.tiles; i++) { + const y = Math.floor(i / w_tiles) + const x = i % w_tiles + coords[i] = { x, y } + } + return coords +} + +export { + createPuzzle +} diff --git a/game/util.js b/server/util.js similarity index 100% rename from game/util.js rename to server/util.js