diff --git a/common/GameCommon.js b/common/GameCommon.js index 37448b7..7b055a3 100644 --- a/common/GameCommon.js +++ b/common/GameCommon.js @@ -7,28 +7,70 @@ function exists(gameId) { return (!!GAMES[gameId]) || false } +function createGame(id, puzzle, players, sockets, evtInfos) { + return { + id: id, + puzzle: puzzle, + players: players, + sockets: sockets, + evtInfos: evtInfos, + } +} + +function createPlayer(id, ts) { + return { + id: id, + x: 0, + y: 0, + d: 0, // mouse down + name: 'anon', + color: '#ffffff', + bgcolor: '#222222', + points: 0, + ts: ts, + } +} + +function newGame({id, puzzle, players, sockets, evtInfos}) { + const game = createGame(id, puzzle, players, sockets, evtInfos) + setGame(id, game) + return game +} + function setGame(gameId, game) { GAMES[gameId] = game } +function getPlayer(gameId, playerId) { + return Util.decodePlayer(GAMES[gameId].players[playerId]) +} + +function setPlayer(gameId, playerId, player) { + GAMES[gameId].players[playerId] = Util.encodePlayer(player) +} + +function setTile(gameId, tileIdx, tile) { + GAMES[gameId].puzzle.tiles[tileIdx] = Util.encodeTile(tile) +} + +function setPuzzleData(gameId, data) { + GAMES[gameId].puzzle.data = data +} + function playerExists(gameId, playerId) { return !!GAMES[gameId].players[playerId] } +function getActivePlayers(gameId) { + const ts = Util.timestamp() + const minTs = ts - 30000 + return getAllPlayers(gameId).filter(player => player.ts >= minTs) +} + function addPlayer(gameId, playerId) { const ts = Util.timestamp() if (!GAMES[gameId].players[playerId]) { - GAMES[gameId].players[playerId] = { - id: playerId, - x: 0, - y: 0, - d: 0, // mouse down - name: 'anon', - color: '#ffffff', - bgcolor: '#222222', - points: 0, - ts, - } + setPlayer(gameId, playerId, createPlayer(playerId, ts)) } else { changePlayer(gameId, playerId, { ts }) } @@ -56,21 +98,52 @@ function removeSocket(gameId, socket) { } function getAllGames() { - return GAMES + return Object.values(GAMES) +} + +function getAllPlayers(gameId) { + return GAMES[gameId] + ? Object.values(GAMES[gameId].players).map(Util.decodePlayer) + : [] } function get(gameId) { return GAMES[gameId] } +function getTileCount(gameId) { + return GAMES[gameId].puzzle.tiles.length +} + +function getImageUrl(gameId) { + return GAMES[gameId].puzzle.info.imageUrl +} + +function getFinishedTileCount(gameId) { + let count = 0 + for (let t of GAMES[gameId].puzzle.tiles) { + if (Util.decodeTile(t).owner === -1) { + count++ + } + } + return count +} + +function getTilesSortedByZIndex(gameId) { + const tiles = GAMES[gameId].puzzle.tiles.map(Util.decodeTile) + return tiles.sort((t1, t2) => t1.z - t2.z) +} + function getSockets(gameId) { return GAMES[gameId].sockets } function changePlayer(gameId, playerId, change) { + const player = getPlayer(gameId, playerId) for (let k of Object.keys(change)) { - GAMES[gameId].players[playerId][k] = change[k] + player[k] = change[k] } + setPlayer(gameId, playerId, player) } function changeData(gameId, change) { @@ -81,12 +154,14 @@ function changeData(gameId, change) { function changeTile(gameId, tileIdx, change) { for (let k of Object.keys(change)) { - GAMES[gameId].puzzle.tiles[tileIdx][k] = change[k] + const tile = Util.decodeTile(GAMES[gameId].puzzle.tiles[tileIdx]) + tile[k] = change[k] + GAMES[gameId].puzzle.tiles[tileIdx] = Util.encodeTile(tile) } } const getTile = (gameId, tileIdx) => { - return GAMES[gameId].puzzle.tiles[tileIdx] + return Util.decodeTile(GAMES[gameId].puzzle.tiles[tileIdx]) } const getTileGroup = (gameId, tileIdx) => { @@ -116,13 +191,27 @@ const getTileZIndex = (gameId, tileIdx) => { const getFirstOwnedTileIdx = (gameId, userId) => { for (let t of GAMES[gameId].puzzle.tiles) { - if (t.owner === userId) { - return t.idx + const tile = Util.decodeTile(t) + if (tile.owner === userId) { + return tile.idx } } return -1 } +const getFirstOwnedTile = (gameId, userId) => { + const idx = getFirstOwnedTileIdx(gameId, userId) + return idx < 0 ? null : GAMES[gameId].puzzle.tiles[idx] +} + +const getTileDrawOffset = (gameId) => { + return GAMES[gameId].puzzle.info.tileDrawOffset +} + +const getTileDrawSize = (gameId) => { + return GAMES[gameId].puzzle.info.tileDrawSize +} + const getMaxGroup = (gameId) => { return GAMES[gameId].puzzle.data.maxGroup } @@ -145,7 +234,7 @@ const getMaxZIndexByTileIdxs = (gameId, tileIdxs) => { function srcPosByTileIdx(gameId, tileIdx) { const info = GAMES[gameId].puzzle.info - const c = info.coords[tileIdx] + const c = Util.coordByTileIdx(info, tileIdx) const cx = c.x * info.tileSize const cy = c.y * info.tileSize @@ -155,18 +244,17 @@ function srcPosByTileIdx(gameId, tileIdx) { function getSurroundingTilesByIdx(gameId, tileIdx) { const info = GAMES[gameId].puzzle.info - const _X = info.coords[tileIdx].x - const _Y = info.coords[tileIdx].y + const c = Util.coordByTileIdx(info, tileIdx) return [ // top - (_Y > 0) ? (tileIdx - info.tilesX) : -1, + (c.y > 0) ? (tileIdx - info.tilesX) : -1, // right - (_X < info.tilesX - 1) ? (tileIdx + 1) : -1, + (c.x < info.tilesX - 1) ? (tileIdx + 1) : -1, // bottom - (_Y < info.tilesY - 1) ? (tileIdx + info.tilesX) : -1, + (c.y < info.tilesY - 1) ? (tileIdx + info.tilesX) : -1, // left - (_X > 0) ? (tileIdx - 1) : -1, + (c.x > 0) ? (tileIdx - 1) : -1, ] } @@ -203,13 +291,14 @@ const setTilesOwner = (gameId, tileIdxs, owner) => { // get all grouped tiles for a tile function getGroupedTileIdxs(gameId, tileIdx) { const tiles = GAMES[gameId].puzzle.tiles - const tile = tiles[tileIdx] + const tile = Util.decodeTile(tiles[tileIdx]) const grouped = [] if (tile.group) { for (let other of tiles) { - if (other.group === tile.group) { - grouped.push(other.idx) + const otherTile = Util.decodeTile(other) + if (otherTile.group === tile.group) { + grouped.push(otherTile.idx) } } } else { @@ -227,7 +316,7 @@ const freeTileIdxByPos = (gameId, pos) => { let maxZ = -1 let tileIdx = -1 for (let idx = 0; idx < tiles.length; idx++) { - const tile = tiles[idx] + const tile = Util.decodeTile(tiles[idx]) if (tile.owner !== 0) { continue } @@ -248,6 +337,22 @@ const freeTileIdxByPos = (gameId, pos) => { return tileIdx } +const getPlayerBgColor = (gameId, playerId) => { + return getPlayer(gameId, playerId).bgcolor +} + +const getPlayerColor = (gameId, playerId) => { + return getPlayer(gameId, playerId).color +} + +const getPlayerName = (gameId, playerId) => { + return getPlayer(gameId, playerId).name +} + +const getPlayerPoints = (gameId, playerId) => { + return getPlayer(gameId, playerId).points +} + // determine if two tiles are grouped together const areGrouped = (gameId, tileIdx1, tileIdx2) => { const g1 = getTileGroup(gameId, tileIdx1) @@ -255,9 +360,24 @@ const areGrouped = (gameId, tileIdx1, tileIdx2) => { return g1 && g1 === g2 } +const getTableWidth = (gameId) => { + return GAMES[gameId].puzzle.info.table.width +} + +const getTableHeight = (gameId) => { + return GAMES[gameId].puzzle.info.table.height +} + +const getPuzzleWidth = (gameId) => { + return GAMES[gameId].puzzle.info.width +} + +const getPuzzleHeight = (gameId) => { + return GAMES[gameId].puzzle.info.height +} + function handleInput(gameId, playerId, input) { - let puzzle = GAMES[gameId].puzzle - let players = GAMES[gameId].players + const puzzle = GAMES[gameId].puzzle let evtInfo = GAMES[gameId].evtInfos[playerId] let changes = [] @@ -277,7 +397,7 @@ function handleInput(gameId, playerId, input) { } const _playerChange = () => { - changes.push(['player', players[playerId]]) + changes.push(['player', GAMES[gameId].players[playerId]]) } // put both tiles (and their grouped tiles) in the same group @@ -312,7 +432,8 @@ function handleInput(gameId, playerId, input) { // TODO: strange if (searchGroups.length > 0) { - for (let tile of tiles) { + for (let t of tiles) { + const tile = Util.decodeTile(t) if (searchGroups.includes(tile.group)) { changeTile(gameId, tile.idx, { group }) _tileChange(tile.idx) @@ -404,7 +525,7 @@ function handleInput(gameId, playerId, input) { // Snap the tile to the final destination moveTilesDiff(gameId, tileIdxs, diff) finishTiles(gameId, tileIdxs) - changePlayer(gameId, playerId, { points: players[playerId].points + tileIdxs.length }) + changePlayer(gameId, playerId, { points: getPlayerPoints(gameId, playerId) + tileIdxs.length }) _tileChanges(tileIdxs) } else { // Snap to other tiles @@ -457,17 +578,35 @@ function handleInput(gameId, playerId, input) { return changes } - export default { - setGame, + newGame, exists, playerExists, + getActivePlayers, addPlayer, socketExists, addSocket, removeSocket, + getFinishedTileCount, + getTileCount, + getImageUrl, get, getAllGames, getSockets, + getPlayerBgColor, + getPlayerColor, + getPlayerName, + changePlayer, + setPlayer, + setTile, + setPuzzleData, + getTableWidth, + getTableHeight, + getPuzzleWidth, + getPuzzleHeight, + getTilesSortedByZIndex, + getFirstOwnedTile, + getTileDrawOffset, + getTileDrawSize, handleInput, } diff --git a/common/Util.js b/common/Util.js index 5980f4c..f6f3313 100644 --- a/common/Util.js +++ b/common/Util.js @@ -46,6 +46,100 @@ export const timestamp = () => { ) } +function encodeShape(data) { + if (typeof data === 'number') { + return data + } + /* encoded in 1 byte: + 00000000 + ^^ top + ^^ right + ^^ bottom + ^^ left + */ + return ((data.top + 1) << 0) + | ((data.right + 1) << 2) + | ((data.bottom + 1) << 4) + | ((data.left + 1) << 6) +} + +function decodeShape(data) { + if (typeof data !== 'number') { + return data + } + return { + top: (data >> 0 & 0b11) - 1, + right: (data >> 2 & 0b11) - 1, + bottom: (data >> 4 & 0b11) - 1, + left: (data >> 6 & 0b11) - 1, + } +} + +function encodeTile(data) { + if (Array.isArray(data)) { + return data + } + return [data.idx, data.pos.x, data.pos.y, data.z, data.owner, data.group] +} + +function decodeTile(data) { + if (!Array.isArray(data)) { + return data + } + return { + idx: data[0], + pos: { + x: data[1], + y: data[2], + }, + z: data[3], + owner: data[4], + group: data[5], + } +} + +function encodePlayer(data) { + if (Array.isArray(data)) { + return data + } + return [ + data.id, + data.x, + data.y, + data.d, + data.name, + data.color, + data.bgcolor, + data.points, + data.ts, + ] +} + +function decodePlayer(data) { + if (!Array.isArray(data)) { + return data + } + return { + id: data[0], + x: data[1], + y: data[2], + d: data[3], // mouse down + name: data[4], + color: data[5], + bgcolor: data[6], + points: data[7], + ts: data[8], + } +} + +function coordByTileIdx(info, tileIdx) { + const wTiles = info.width / info.tileSize + return { + x: tileIdx % wTiles, + y: Math.floor(tileIdx / wTiles), + } +} + export default { uniqId, randomInt, @@ -53,4 +147,15 @@ export default { throttle, shuffle, timestamp, + + encodeShape, + decodeShape, + + encodeTile, + decodeTile, + + encodePlayer, + decodePlayer, + + coordByTileIdx, } diff --git a/game/Game.js b/game/Game.js index 93800b7..1bc2a05 100644 --- a/game/Game.js +++ b/game/Game.js @@ -1,6 +1,23 @@ import GameCommon from './../common/GameCommon.js' export default { - createGame: GameCommon.setGame, + newGame: GameCommon.newGame, + getActivePlayers: GameCommon.getActivePlayers, handleInput: GameCommon.handleInput, + getPlayerBgColor: GameCommon.getPlayerBgColor, + getPlayerColor: GameCommon.getPlayerColor, + getPlayerName: GameCommon.getPlayerName, + changePlayer: GameCommon.changePlayer, + setPlayer: GameCommon.setPlayer, + setTile: GameCommon.setTile, + getImageUrl: GameCommon.getImageUrl, + setPuzzleData: GameCommon.setPuzzleData, + getTableWidth: GameCommon.getTableWidth, + getTableHeight: GameCommon.getTableHeight, + getPuzzleWidth: GameCommon.getPuzzleWidth, + getPuzzleHeight: GameCommon.getPuzzleHeight, + getTilesSortedByZIndex: GameCommon.getTilesSortedByZIndex, + getFirstOwnedTile: GameCommon.getFirstOwnedTile, + getTileDrawOffset: GameCommon.getTileDrawOffset, + getTileDrawSize: GameCommon.getTileDrawSize, } diff --git a/game/PuzzleGraphics.js b/game/PuzzleGraphics.js index e8eb610..270d493 100644 --- a/game/PuzzleGraphics.js +++ b/game/PuzzleGraphics.js @@ -1,5 +1,6 @@ import Geometry from '../common/Geometry.js' import Graphics from './Graphics.js' +import Util from './../common/Util.js' async function createPuzzleTileBitmaps(img, tiles, info) { var tileSize = info.tileSize @@ -76,9 +77,10 @@ async function createPuzzleTileBitmaps(img, tiles, info) { return path } - for (let tile of tiles) { + for (let t of tiles) { + const tile = Util.decodeTile(t) const srcRect = srcRectByIdx(info, tile.idx) - const path = pathForShape(info.shapes[tile.idx]) + const path = pathForShape(Util.decodeShape(info.shapes[tile.idx])) const c = Graphics.createCanvas(tileDrawSize, tileDrawSize) const ctx = c.getContext('2d') @@ -192,7 +194,7 @@ async function createPuzzleTileBitmaps(img, tiles, info) { } function srcRectByIdx(puzzleInfo, idx) { - const c = puzzleInfo.coords[idx] + const c = Util.coordByTileIdx(puzzleInfo, idx) return { x: c.x * puzzleInfo.tileSize, y: c.y * puzzleInfo.tileSize, diff --git a/game/game.js b/game/game.js index 12c8823..9a05d2c 100644 --- a/game/game.js +++ b/game/game.js @@ -27,18 +27,6 @@ function addCanvasToDom(canvas) { return canvas } -function getActivePlayers(players) { - const ts = Util.timestamp() - const activePlayers = [] - for (let id of Object.keys(players)) { - const player = players[id] - if (player.ts >= ts - 30000) { - activePlayers.push(player) - } - } - return activePlayers -} - function addMenuToDom(previewImageUrl) { function row (...elements) { const row = document.createElement('tr') @@ -141,8 +129,8 @@ function addMenuToDom(previewImageUrl) { scoresTitleEl.appendChild(document.createTextNode('Scores')) const scoresListEl = document.createElement('table') - const updateScores = (players) => { - const activePlayers = getActivePlayers(players) + const updateScores = (gameId) => { + const activePlayers = Game.getActivePlayers(gameId) const scores = activePlayers.map(p => ({ name: p.name, points: p.points, @@ -188,15 +176,6 @@ function initme() { return ID } -const getFirstOwnedTile = (puzzle, userId) => { - for (let t of puzzle.tiles) { - if (t.owner === userId) { - return t - } - } - return null -} - export default class EventAdapter { constructor(canvas, viewport) { this.events = [] @@ -279,20 +258,12 @@ async function main() { } const game = await Communication.connect(gameId, CLIENT_ID) - Game.createGame(GAME_ID, game); + Game.newGame(game) const bitmaps = await PuzzleGraphics.loadPuzzleBitmaps(game.puzzle) - const puzzle = game.puzzle - const players = game.players - const changePlayer = (change) => { - for (let k of Object.keys(change)) { - players[CLIENT_ID][k] = change[k] - } - } - - const {bgColorPickerEl, playerColorPickerEl, nameChangeEl, updateScores} = addMenuToDom(game.puzzle.info.imageUrl) - updateScores(players) + const {bgColorPickerEl, playerColorPickerEl, nameChangeEl, updateScores} = addMenuToDom(Game.getImageUrl(gameId)) + updateScores(gameId) // Create a dom and attach adapters to it so we can work with it const canvas = addCanvasToDom(Graphics.createCanvas()) @@ -303,21 +274,21 @@ async function main() { const viewport = new Camera(canvas) // center viewport viewport.move( - -(puzzle.info.table.width - viewport.width) /2, - -(puzzle.info.table.height - viewport.height) /2 + -(Game.getTableWidth(gameId) - viewport.width) /2, + -(Game.getTableHeight(gameId) - viewport.height) /2 ) const evts = new EventAdapter(canvas, viewport) - bgColorPickerEl.value = players[CLIENT_ID].bgcolor + bgColorPickerEl.value = Game.getPlayerBgColor(gameId, CLIENT_ID) bgColorPickerEl.addEventListener('change', () => { evts.addEvent(['bg_color', bgColorPickerEl.value]) }) - playerColorPickerEl.value = players[CLIENT_ID].color + playerColorPickerEl.value = Game.getPlayerBgColor(gameId, CLIENT_ID) playerColorPickerEl.addEventListener('change', () => { evts.addEvent(['player_color', playerColorPickerEl.value]) }) - nameChangeEl.value = players[CLIENT_ID].name + nameChangeEl.value = Game.getPlayerName(gameId, CLIENT_ID) nameChangeEl.addEventListener('change', () => { evts.addEvent(['player_name', nameChangeEl.value]) }) @@ -330,28 +301,25 @@ async function main() { for(let [changeType, changeData] of evChanges) { switch (changeType) { case 'player': { - if (changeData.id !== CLIENT_ID) { - players[changeData.id] = changeData + const p = Util.decodePlayer(changeData) + if (p.id !== CLIENT_ID) { + Game.setPlayer(gameId, p.id, p) RERENDER = true } } break; case 'tile': { - puzzle.tiles[changeData.idx] = changeData + const t = Util.decodeTile(changeData) + Game.setTile(gameId, t.idx, t) RERENDER = true } break; case 'data': { - puzzle.data = changeData + Game.setPuzzleData(gameId, changeData) RERENDER = true } break; } } }) - const tilesSortedByZIndex = () => { - const sorted = puzzle.tiles.slice() - return sorted.sort((t1, t2) => t1.z - t2.z) - } - let _last_mouse_down = null const onUpdate = () => { for (let evt of evts.consumeAll()) { @@ -360,12 +328,9 @@ async function main() { // ------------------------------------------------------------- const type = evt[0] if (type === 'move') { - const pos = { x: evt[1], y: evt[2] } - RERENDER = true - changePlayer(pos) - - if (_last_mouse_down && !getFirstOwnedTile(puzzle, CLIENT_ID)) { + if (_last_mouse_down && !Game.getFirstOwnedTile(gameId, CLIENT_ID)) { // move the cam + const pos = { x: evt[1], y: evt[2] } const mouse = viewport.worldToViewport(pos) const diffX = Math.round(mouse.x - _last_mouse_down.x) const diffY = Math.round(mouse.y - _last_mouse_down.y) @@ -382,13 +347,13 @@ async function main() { if (viewport.zoomIn()) { const pos = { x: evt[1], y: evt[2] } RERENDER = true - changePlayer(pos) + Game.changePlayer(gameId, CLIENT_ID, pos) } } else if (type === 'zoomout') { if (viewport.zoomOut()) { const pos = { x: evt[1], y: evt[2] } RERENDER = true - changePlayer(pos) + Game.changePlayer(gameId, CLIENT_ID, pos) } } @@ -414,7 +379,7 @@ async function main() { // CLEAR CTX // --------------------------------------------------------------- - ctx.fillStyle = players[CLIENT_ID].bgcolor || '#222222' + ctx.fillStyle = Game.getPlayerBgColor(gameId, CLIENT_ID) || '#222222' ctx.fillRect(0, 0, canvas.width, canvas.height) if (DEBUG) Debug.checkpoint('clear done') // --------------------------------------------------------------- @@ -423,12 +388,12 @@ async function main() { // DRAW BOARD // --------------------------------------------------------------- pos = viewport.worldToViewport({ - x: (puzzle.info.table.width - puzzle.info.width) / 2, - y: (puzzle.info.table.height - puzzle.info.height) / 2 + x: (Game.getTableWidth(gameId) - Game.getPuzzleWidth(gameId)) / 2, + y: (Game.getTableHeight(gameId) - Game.getPuzzleHeight(gameId)) / 2 }) dim = viewport.worldDimToViewport({ - w: puzzle.info.width, - h: puzzle.info.height, + w: Game.getPuzzleWidth(gameId), + h: Game.getPuzzleHeight(gameId), }) ctx.fillStyle = 'rgba(255, 255, 255, .5)' ctx.fillRect(pos.x, pos.y, dim.w, dim.h) @@ -438,15 +403,15 @@ async function main() { // DRAW TILES // --------------------------------------------------------------- - for (let tile of tilesSortedByZIndex()) { + for (let tile of Game.getTilesSortedByZIndex(gameId)) { const bmp = bitmaps[tile.idx] pos = viewport.worldToViewport({ - x: puzzle.info.tileDrawOffset + tile.pos.x, - y: puzzle.info.tileDrawOffset + tile.pos.y, + x: Game.getTileDrawOffset(gameId) + tile.pos.x, + y: Game.getTileDrawOffset(gameId) + tile.pos.y, }) dim = viewport.worldDimToViewport({ - w: puzzle.info.tileDrawSize, - h: puzzle.info.tileDrawSize, + w: Game.getTileDrawSize(gameId), + h: Game.getTileDrawSize(gameId), }) ctx.drawImage(bmp, 0, 0, bmp.width, bmp.height, @@ -459,18 +424,18 @@ async function main() { // DRAW PLAYERS // --------------------------------------------------------------- - for (let p of getActivePlayers(players)) { - const cursor = await getPlayerCursor(p) - const pos = viewport.worldToViewport(p) + for (let player of Game.getActivePlayers(gameId)) { + const cursor = await getPlayerCursor(player) + const pos = viewport.worldToViewport(player) ctx.drawImage(cursor, Math.round(pos.x - cursor.width/2), Math.round(pos.y - cursor.height/2) ) - if (p.id !== CLIENT_ID) { + if (player.id !== CLIENT_ID) { ctx.fillStyle = 'white' ctx.font = '10px sans-serif' ctx.textAlign = 'center' - ctx.fillText(p.name + ' (' + p.points + ')', + ctx.fillText(player.name + ' (' + player.points + ')', Math.round(pos.x), Math.round(pos.y) + cursor.height ) @@ -480,7 +445,7 @@ async function main() { // DRAW PLAYERS // --------------------------------------------------------------- - updateScores(players) + updateScores(gameId) if (DEBUG) Debug.checkpoint('scores done') // --------------------------------------------------------------- diff --git a/game/index.js b/game/index.js index 191027d..ec42324 100644 --- a/game/index.js +++ b/game/index.js @@ -38,12 +38,16 @@ export default {