Compare commits

..

1 commit

Author SHA1 Message Date
Zutatensuppe
08b332ac6f add import script for existing game logs 2021-05-29 09:06:14 +02:00
73 changed files with 1627 additions and 5548 deletions

View file

@ -1,3 +0,0 @@
node_modules
build
src/frontend/shims-vue.d.ts

View file

@ -1,17 +0,0 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: [
'@typescript-eslint',
],
parserOptions: {
ecmaVersion: 2020,
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
],
rules: {
'@typescript-eslint/no-inferrable-types': 'off'
}
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -4,9 +4,9 @@
<meta charset="UTF-8">
<title>🧩 jigsaw.hyottoko.club</title>
<script type="module" crossorigin src="/assets/index.63ff8630.js"></script>
<link rel="modulepreload" href="/assets/vendor.684f7bc8.js">
<link rel="stylesheet" href="/assets/index.22dc307c.css">
<script type="module" crossorigin src="/assets/index.50ee8245.js"></script>
<link rel="modulepreload" href="/assets/vendor.b622ee49.js">
<link rel="stylesheet" href="/assets/index.f7304069.css">
</head>
<body>
<div id="app"></div>

File diff suppressed because it is too large Load diff

1981
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -2,6 +2,7 @@
"type": "module",
"dependencies": {
"better-sqlite3": "^7.4.0",
"body-parser": "^1.19.0",
"exif": "^0.6.0",
"express": "^4.17.1",
"image-size": "^0.9.3",
@ -13,24 +14,19 @@
},
"devDependencies": {
"@types/better-sqlite3": "^5.4.1",
"@types/compression": "^1.7.0",
"@types/exif": "^0.6.2",
"@types/express": "^4.17.11",
"@types/multer": "^1.4.5",
"@types/sharp": "^0.28.1",
"@types/ws": "^7.4.4",
"@typescript-eslint/eslint-plugin": "^4.25.0",
"@typescript-eslint/parser": "^4.25.0",
"@vitejs/plugin-vue": "^1.2.2",
"@vuedx/typescript-plugin-vue": "^0.6.3",
"compression": "^1.7.4",
"eslint": "^7.27.0",
"jest": "^26.6.3",
"rollup": "^2.48.0",
"rollup-plugin-typescript2": "^0.30.0",
"rollup-plugin-vue": "^6.0.0-beta.10",
"ts-node": "^9.1.1",
"typescript": "^4.3.2",
"typescript": "^4.2.4",
"vite": "^2.3.2"
},
"engines": {
@ -39,7 +35,6 @@
},
"scripts": {
"rollup": "rollup",
"vite": "vite",
"eslint": "eslint"
"vite": "vite"
}
}

View file

@ -8,18 +8,17 @@ export default {
format: 'es',
},
external: [
"better-sqlite3",
"compression",
"exif",
"express",
"fs",
"image-size",
"multer",
"path",
"body-parser",
"v8",
"fs",
"ws",
"image-size",
"exif",
"sharp",
"url",
"v8",
"ws",
"path",
],
plugins: [typescript()],
};

View file

@ -1,90 +0,0 @@
import GameCommon from '../src/common/GameCommon'
import GameLog from '../src/server/GameLog'
import { Game } from '../src/common/Types'
import { logger } from '../src/common/Util'
import { DB_FILE, DB_PATCHES_DIR, UPLOAD_DIR } from '../src/server/Dirs'
import Db from '../src/server/Db'
import GameStorage from '../src/server/GameStorage'
import fs from 'fs'
const log = logger('fix_games_image_info.ts')
import Images from '../src/server/Images'
console.log(DB_FILE)
const db = new Db(DB_FILE, DB_PATCHES_DIR)
db.patch(true)
// ;(async () => {
// let images = db.getMany('images')
// for (let image of images) {
// console.log(image.filename)
// let dim = await Images.getDimensions(`${UPLOAD_DIR}/${image.filename}`)
// console.log(await Images.getDimensions(`${UPLOAD_DIR}/${image.filename}`))
// image.width = dim.w
// image.height = dim.h
// db.upsert('images', image, { id: image.id })
// }
// })()
function fixOne(gameId: string) {
let g = GameCommon.get(gameId)
if (!g) {
return
}
if (!g.puzzle.info.image && g.puzzle.info.imageUrl) {
log.log('game id: ', gameId)
const parts = g.puzzle.info.imageUrl.split('/')
const fileName = parts[parts.length - 1]
const imageRow = db.get('images', {filename: fileName})
if (!imageRow) {
return
}
g.puzzle.info.image = Images.imageFromDb(db, imageRow.id)
log.log(g.puzzle.info.image.title, imageRow.id)
GameStorage.persistGameToDb(db, gameId)
} else if (g.puzzle.info.image?.id) {
const imageId = g.puzzle.info.image.id
g.puzzle.info.image = Images.imageFromDb(db, imageId)
log.log(g.puzzle.info.image.title, imageId)
GameStorage.persistGameToDb(db, gameId)
}
// fix log
const file = GameLog.filename(gameId, 0)
if (!fs.existsSync(file)) {
return
}
const lines = fs.readFileSync(file, 'utf-8').split("\n")
const l = lines.filter(line => !!line).map(line => {
return JSON.parse(`[${line}]`)
})
if (l && l[0] && !l[0][3].id) {
log.log(l[0][3])
l[0][3] = g.puzzle.info.image
const newlines = l.map(ll => {
return JSON.stringify(ll).slice(1, -1)
}).join("\n") + "\n"
console.log(g.puzzle.info.image)
// process.exit(0)
fs.writeFileSync(file, newlines)
}
}
function fix() {
GameStorage.loadGamesFromDisk()
GameCommon.getAllGames().forEach((game: Game) => {
fixOne(game.id)
})
}
fix()

23
scripts/fix_image.ts Normal file
View file

@ -0,0 +1,23 @@
import GameCommon from '../src/common/GameCommon'
import { logger } from '../src/common/Util'
import GameStorage from '../src/server/GameStorage'
const log = logger('fix_image.js')
function fix(gameId) {
GameStorage.loadGame(gameId)
let changed = false
let imgUrl = GameCommon.getImageUrl(gameId)
if (imgUrl.match(/^\/example-images\//)) {
log.log(`found bad imgUrl: ${imgUrl}`)
imgUrl = imgUrl.replace(/^\/example-images\//, '/uploads/')
GameCommon.setImageUrl(gameId, imgUrl)
changed = true
}
if (changed) {
GameStorage.persistGame(gameId)
}
}
fix(process.argv[2])

View file

@ -1,38 +1,33 @@
import GameCommon from '../src/common/GameCommon'
import { logger } from '../src/common/Util'
import Db from '../src/server/Db'
import { DB_FILE, DB_PATCHES_DIR } from '../src/server/Dirs'
import GameStorage from '../src/server/GameStorage'
const log = logger('fix_tiles.js')
const db = new Db(DB_FILE, DB_PATCHES_DIR)
db.patch(true)
function fix_tiles(gameId) {
GameStorage.loadGameFromDb(db, gameId)
GameStorage.loadGame(gameId)
let changed = false
const tiles = GameCommon.getPiecesSortedByZIndex(gameId)
const tiles = GameCommon.getTilesSortedByZIndex(gameId)
for (let tile of tiles) {
if (tile.owner === -1) {
const p = GameCommon.getFinalPiecePos(gameId, tile.idx)
const p = GameCommon.getFinalTilePos(gameId, tile.idx)
if (p.x === tile.pos.x && p.y === tile.pos.y) {
// log.log('all good', tile.pos)
} else {
log.log('bad tile pos', tile.pos, 'should be: ', p)
tile.pos = p
GameCommon.setPiece(gameId, tile.idx, tile)
GameCommon.setTile(gameId, tile.idx, tile)
changed = true
}
} else if (tile.owner !== 0) {
tile.owner = 0
log.log('unowning tile', tile.idx)
GameCommon.setPiece(gameId, tile.idx, tile)
GameCommon.setTile(gameId, tile.idx, tile)
changed = true
}
}
if (changed) {
GameStorage.persistGameToDb(db, gameId)
GameStorage.persistGame(gameId)
}
}

View file

@ -0,0 +1,47 @@
import { DB_FILE, DB_PATCHES_DIR, DATA_DIR } from '../src/server/Dirs'
import Db from '../src/server/Db'
import fs from 'fs'
import { logger } from '../src/common/Util'
const log = logger('import_game_logs.ts')
const db = new Db(DB_FILE, DB_PATCHES_DIR)
db.patch(true)
for (const file of fs.readdirSync(DATA_DIR)) {
const m = file.match(/^log_(.*)\.log$/)
if (!m) {
continue
}
const gameId = m[1]
log.info(`Importing log for game ${file}`)
const contents = fs.readFileSync(`${DATA_DIR}/${file}`, 'utf-8')
let t = 0
let datas = []
for (const line of contents.split("\n")) {
if (line === '') {
continue
}
let parsed
try {
parsed = JSON.parse(line)
} catch (e) {
log.error('bad game', e)
break
}
if (t === 0) {
t = parsed[4]
} else {
t += parsed[parsed.length - 1]
}
datas.push({
game_id: gameId,
created: t / 1000,
entry: line,
})
}
db.insertMany('game_log', datas)
log.info(`Done.`)
}

View file

@ -1,27 +0,0 @@
import GameCommon from '../src/common/GameCommon'
import { Game } from '../src/common/Types'
import { logger } from '../src/common/Util'
import { DB_FILE, DB_PATCHES_DIR } from '../src/server/Dirs'
import Db from '../src/server/Db'
import GameStorage from '../src/server/GameStorage'
const log = logger('import_games.ts')
console.log(DB_FILE)
const db = new Db(DB_FILE, DB_PATCHES_DIR)
db.patch(true)
function run() {
GameStorage.loadGamesFromDisk()
GameCommon.getAllGames().forEach((game: Game) => {
if (!game.puzzle.info.image?.id) {
log.error(game.id + " has no image")
log.error(game.puzzle.info.image)
return
}
GameStorage.persistGameToDb(db, game.id)
})
}
run()

View file

@ -1,18 +0,0 @@
import { DB_FILE, DB_PATCHES_DIR, UPLOAD_DIR } from '../src/server/Dirs'
import Db from '../src/server/Db'
import Images from '../src/server/Images'
const db = new Db(DB_FILE, DB_PATCHES_DIR)
db.patch(true)
;(async () => {
let images = db.getMany('images')
for (let image of images) {
console.log(image.filename)
let dim = await Images.getDimensions(`${UPLOAD_DIR}/${image.filename}`)
console.log(await Images.getDimensions(`${UPLOAD_DIR}/${image.filename}`))
image.width = dim.w
image.height = dim.h
db.upsert('images', image, { id: image.id })
}
})()

View file

@ -1,5 +0,0 @@
#!/bin/sh -e
cd "$RUN_DIR"
npm run eslint src

82
scripts/rewrite_logs.ts Normal file
View file

@ -0,0 +1,82 @@
import fs from 'fs'
import Protocol from '../src/common/Protocol'
import { logger } from '../src/common/Util'
import { DATA_DIR } from '../src/server/Dirs'
const log = logger('rewrite_logs')
const filename = (gameId) => `${DATA_DIR}/log_${gameId}.log`
const rewrite = (gameId) => {
const file = filename(gameId)
log.log(file)
if (!fs.existsSync(file)) {
return []
}
let playerIds = [];
let startTs = null
const lines = fs.readFileSync(file, 'utf-8').split("\n")
const linesNew = lines.filter(line => !!line).map((line) => {
const json = JSON.parse(line)
const m = {
createGame: Protocol.LOG_HEADER,
addPlayer: Protocol.LOG_ADD_PLAYER,
handleInput: Protocol.LOG_HANDLE_INPUT,
}
const action = json[0]
if (action in m) {
json[0] = m[action]
if (json[0] === Protocol.LOG_HANDLE_INPUT) {
const inputm = {
down: Protocol.INPUT_EV_MOUSE_DOWN,
up: Protocol.INPUT_EV_MOUSE_UP,
move: Protocol.INPUT_EV_MOUSE_MOVE,
zoomin: Protocol.INPUT_EV_ZOOM_IN,
zoomout: Protocol.INPUT_EV_ZOOM_OUT,
bg_color: Protocol.INPUT_EV_BG_COLOR,
player_color: Protocol.INPUT_EV_PLAYER_COLOR,
player_name: Protocol.INPUT_EV_PLAYER_NAME,
}
const inputa = json[2][0]
if (inputa in inputm) {
json[2][0] = inputm[inputa]
} else {
throw '[ invalid input log line: "' + line + '" ]'
}
}
} else {
throw '[ invalid general log line: "' + line + '" ]'
}
if (json[0] === Protocol.LOG_ADD_PLAYER) {
if (playerIds.indexOf(json[1]) === -1) {
playerIds.push(json[1])
} else {
json[0] = Protocol.LOG_UPDATE_PLAYER
json[1] = playerIds.indexOf(json[1])
}
}
if (json[0] === Protocol.LOG_HANDLE_INPUT) {
json[1] = playerIds.indexOf(json[1])
if (json[1] === -1) {
throw '[ invalid player ... "' + line + '" ]'
}
}
if (json[0] === Protocol.LOG_HEADER) {
startTs = json[json.length - 1]
json[4] = json[3]
json[3] = json[2]
json[2] = json[1]
json[1] = 1
} else {
json[json.length - 1] = json[json.length - 1] - startTs
}
return JSON.stringify(json)
})
fs.writeFileSync(file, linesNew.join("\n") + "\n")
}
rewrite(process.argv[2])

View file

@ -1,4 +1,4 @@
#!/bin/sh
# server for built files
nodemon --watch build --max-old-space-size=64 -e js build/server/main.js -c config.json
nodemon --max-old-space-size=64 build/server/main.js -c config.json

View file

@ -1,73 +0,0 @@
import fs from 'fs'
import { logger } from '../src/common/Util'
import { DATA_DIR } from '../src/server/Dirs'
import { filename } from '../src/server/GameLog'
const log = logger('rewrite_logs')
interface IdxOld {
total: number
currentFile: string
perFile: number
}
interface Idx {
gameId: string
total: number
lastTs: number
currentFile: string
perFile: number
}
const doit = (idxfile: string): void => {
const gameId: string = (idxfile.match(/^log_([a-z0-9]+)\.idx\.log$/) as any[])[1]
const idxOld: IdxOld = JSON.parse(fs.readFileSync(DATA_DIR + '/' + idxfile, 'utf-8'))
let currentFile = filename(gameId, 0)
const idxNew: Idx = {
gameId: gameId,
total: 0,
lastTs: 0,
currentFile: currentFile,
perFile: idxOld.perFile
}
let firstTs = 0
while (fs.existsSync(currentFile)) {
idxNew.currentFile = currentFile
const log = fs.readFileSync(currentFile, 'utf-8').split("\n")
const newLines = []
const lines = log.filter(line => !!line).map(line => {
return JSON.parse(line)
})
for (const l of lines) {
if (idxNew.total === 0) {
firstTs = l[4]
idxNew.lastTs = l[4]
newLines.push(JSON.stringify(l).slice(1, -1))
} else {
const ts = firstTs + l[l.length - 1]
const diff = ts - idxNew.lastTs
idxNew.lastTs = ts
const newL = l.slice(0, -1)
newL.push(diff)
newLines.push(JSON.stringify(newL).slice(1, -1))
}
idxNew.total++
}
fs.writeFileSync(idxNew.currentFile, newLines.join("\n") + "\n")
currentFile = filename(gameId, idxNew.total)
}
fs.writeFileSync(DATA_DIR + '/' + idxfile, JSON.stringify(idxNew))
console.log('done: ' + gameId)
}
let indexfiles = fs.readdirSync(DATA_DIR)
.filter(f => f.toLowerCase().match(/^log_[a-z0-9]+\.idx\.log$/))
;(async () => {
for (const file of indexfiles) {
await doit(file)
}
})()

View file

@ -1,3 +1,3 @@
#!/bin/sh -e
#!/bin/sh
node --experimental-vm-modules node_modules/.bin/jest

View file

@ -1,3 +1,3 @@
#!/bin/sh -e
node --max-old-space-size=256 --experimental-specifier-resolution=node --loader ts-node/esm $@
node --experimental-specifier-resolution=node --loader ts-node/esm $@

View file

@ -2,35 +2,139 @@ import Geometry, { Point, Rect } from './Geometry'
import Protocol from './Protocol'
import { Rng } from './Rng'
import Time from './Time'
import {
Change,
EncodedPiece,
EvtInfo,
Game,
Input,
Piece,
PieceChange,
Player,
PlayerChange,
Puzzle,
PuzzleData,
PuzzleDataChange,
ScoreMode,
SnapMode,
Timestamp
} from './Types'
import Util from './Util'
export type Timestamp = number
export type EncodedPlayer = Array<any>
export type EncodedPiece = Array<any>
export type EncodedPieceShape = number
export interface Tag {
id: number
slug: string
title: string
}
interface GameRng {
obj: Rng
type?: string
}
interface Game {
id: string
players: Array<EncodedPlayer>
puzzle: Puzzle
evtInfos: Record<string, EvtInfo>
scoreMode?: ScoreMode
rng: GameRng
}
export interface Image {
id: number
filename: string
file: string
url: string
title: string
tags: Array<Tag>
created: number
}
export interface GameSettings {
tiles: number
image: Image
scoreMode: ScoreMode
}
export interface Puzzle {
tiles: Array<EncodedPiece>
data: PuzzleData
info: PuzzleInfo
}
interface PuzzleData {
started: number
finished: number
maxGroup: number
maxZ: number
}
interface PuzzleTable {
width: number
height: number
}
enum PieceEdge {
Flat = 0,
Out = 1,
In = -1,
}
export interface PieceShape {
top: PieceEdge
bottom: PieceEdge
left: PieceEdge
right: PieceEdge
}
export interface Piece {
owner: string|number
idx: number
pos: Point
z: number
group: number
}
export interface PuzzleInfo {
table: PuzzleTable
targetTiles: number,
imageUrl: string
width: number
height: number
tileSize: number
tileDrawSize: number
tileMarginWidth: number
tileDrawOffset: number
snapDistance: number
tiles: number
tilesX: number
tilesY: number
shapes: Array<EncodedPieceShape>
}
export interface Player {
id: string
x: number
y: number
d: 0|1
name: string|null
color: string|null
bgcolor: string|null
points: number
ts: number
}
interface EvtInfo {
_last_mouse: Point|null
_last_mouse_down: Point|null
}
export enum ScoreMode {
FINAL = 0,
ANY = 1,
}
const IDLE_TIMEOUT_SEC = 30
// Map<gameId, Game>
const GAMES: Record<string, Game> = {}
function exists(gameId: string): boolean {
function exists(gameId: string) {
return (!!GAMES[gameId]) || false
}
function __createPlayerObject(id: string, ts: Timestamp): Player {
function __createPlayerObject(id: string, ts: number): Player {
return {
id: id,
x: 0,
@ -50,7 +154,7 @@ function setGame(gameId: string, game: Game): void {
function getPlayerIndexById(gameId: string, playerId: string): number {
let i = 0;
for (const player of GAMES[gameId].players) {
for (let player of GAMES[gameId].players) {
if (Util.decodePlayer(player).id === playerId) {
return i
}
@ -66,11 +170,8 @@ function getPlayerIdByIndex(gameId: string, playerIndex: number): string|null {
return null
}
function getPlayer(gameId: string, playerId: string): Player|null {
function getPlayer(gameId: string, playerId: string): Player {
const idx = getPlayerIndexById(gameId, playerId)
if (idx === -1) {
return null
}
return Util.decodePlayer(GAMES[gameId].players[idx])
}
@ -87,8 +188,8 @@ function setPlayer(
}
}
function setPiece(gameId: string, pieceIdx: number, piece: Piece): void {
GAMES[gameId].puzzle.tiles[pieceIdx] = Util.encodePiece(piece)
function setTile(gameId: string, tileIdx: number, tile: Piece): void {
GAMES[gameId].puzzle.tiles[tileIdx] = Util.encodeTile(tile)
}
function setPuzzleData(gameId: string, data: PuzzleData): void {
@ -110,7 +211,7 @@ function getIdlePlayers(gameId: string, ts: number): Array<Player> {
return getAllPlayers(gameId).filter((p: Player) => p.ts < minTs && p.points > 0)
}
function addPlayer(gameId: string, playerId: string, ts: Timestamp): void {
function addPlayer(gameId: string, playerId: string, ts: number): void {
if (!playerExists(gameId, playerId)) {
setPlayer(gameId, playerId, __createPlayerObject(playerId, ts))
} else {
@ -138,16 +239,12 @@ function setEvtInfo(
function getAllGames(): Array<Game> {
return Object.values(GAMES).sort((a: Game, b: Game) => {
const finished = isFinished(a.id)
// when both have same finished state, sort by started
if (finished === isFinished(b.id)) {
if (finished) {
return b.puzzle.data.finished - a.puzzle.data.finished
}
if (isFinished(a.id) === isFinished(b.id)) {
return b.puzzle.data.started - a.puzzle.data.started
}
// otherwise, sort: unfinished, finished
return finished ? 1 : -1
return isFinished(a.id) ? 1 : -1
})
}
@ -157,107 +254,84 @@ function getAllPlayers(gameId: string): Array<Player> {
: []
}
function get(gameId: string): Game|null {
return GAMES[gameId] || null
function get(gameId: string) {
return GAMES[gameId]
}
function getPieceCount(gameId: string): number {
function getTileCount(gameId: string): number {
return GAMES[gameId].puzzle.tiles.length
}
function getImageUrl(gameId: string): string {
const imageUrl = GAMES[gameId].puzzle.info.image?.url
|| GAMES[gameId].puzzle.info.imageUrl
if (!imageUrl) {
throw new Error('[2021-07-11] no image url set')
}
return imageUrl
return GAMES[gameId].puzzle.info.imageUrl
}
function setImageUrl(gameId: string, imageUrl: string): void {
GAMES[gameId].puzzle.info.imageUrl = imageUrl
}
function getScoreMode(gameId: string): ScoreMode {
return GAMES[gameId].scoreMode
}
function getSnapMode(gameId: string): SnapMode {
return GAMES[gameId].snapMode
return GAMES[gameId].scoreMode || ScoreMode.FINAL
}
function isFinished(gameId: string): boolean {
return getFinishedPiecesCount(gameId) === getPieceCount(gameId)
return getFinishedTileCount(gameId) === getTileCount(gameId)
}
function getFinishedPiecesCount(gameId: string): number {
function getFinishedTileCount(gameId: string): number {
let count = 0
for (const t of GAMES[gameId].puzzle.tiles) {
if (Util.decodePiece(t).owner === -1) {
for (let t of GAMES[gameId].puzzle.tiles) {
if (Util.decodeTile(t).owner === -1) {
count++
}
}
return count
}
function getPiecesSortedByZIndex(gameId: string): Piece[] {
const pieces = GAMES[gameId].puzzle.tiles.map(Util.decodePiece)
return pieces.sort((t1, t2) => t1.z - t2.z)
function getTilesSortedByZIndex(gameId: string): Piece[] {
const tiles = GAMES[gameId].puzzle.tiles.map(Util.decodeTile)
return tiles.sort((t1, t2) => t1.z - t2.z)
}
function changePlayer(
gameId: string,
playerId: string,
change: PlayerChange
change: any
): void {
const player = getPlayer(gameId, playerId)
if (player === null) {
return
}
for (const k of Object.keys(change)) {
for (let k of Object.keys(change)) {
// @ts-ignore
player[k] = change[k]
}
setPlayer(gameId, playerId, player)
}
function changeData(gameId: string, change: PuzzleDataChange): void {
for (const k of Object.keys(change)) {
function changeData(gameId: string, change: any): void {
for (let k of Object.keys(change)) {
// @ts-ignore
GAMES[gameId].puzzle.data[k] = change[k]
}
}
function changePiece(
gameId: string,
pieceIdx: number,
change: PieceChange
): void {
for (const k of Object.keys(change)) {
const piece = Util.decodePiece(GAMES[gameId].puzzle.tiles[pieceIdx])
function changeTile(gameId: string, tileIdx: number, change: any): void {
for (let k of Object.keys(change)) {
const tile = Util.decodeTile(GAMES[gameId].puzzle.tiles[tileIdx])
// @ts-ignore
piece[k] = change[k]
GAMES[gameId].puzzle.tiles[pieceIdx] = Util.encodePiece(piece)
tile[k] = change[k]
GAMES[gameId].puzzle.tiles[tileIdx] = Util.encodeTile(tile)
}
}
const getPiece = (gameId: string, pieceIdx: number): Piece => {
return Util.decodePiece(GAMES[gameId].puzzle.tiles[pieceIdx])
const getTile = (gameId: string, tileIdx: number): Piece => {
return Util.decodeTile(GAMES[gameId].puzzle.tiles[tileIdx])
}
const getPieceGroup = (gameId: string, tileIdx: number): number => {
const tile = getPiece(gameId, tileIdx)
const getTileGroup = (gameId: string, tileIdx: number): number => {
const tile = getTile(gameId, tileIdx)
return tile.group
}
const isCornerPiece = (gameId: string, tileIdx: number): boolean => {
const info = GAMES[gameId].puzzle.info
return (
tileIdx === 0 // top left corner
|| tileIdx === (info.tilesX - 1) // top right corner
|| tileIdx === (info.tiles - info.tilesX) // bottom left corner
|| tileIdx === (info.tiles - 1) // bottom right corner
)
}
const getFinalPiecePos = (gameId: string, tileIdx: number): Point => {
const getFinalTilePos = (gameId: string, tileIdx: number): Point => {
const info = GAMES[gameId].puzzle.info
const boardPos = {
x: (info.table.width - info.width) / 2,
@ -267,8 +341,8 @@ const getFinalPiecePos = (gameId: string, tileIdx: number): Point => {
return Geometry.pointAdd(boardPos, srcPos)
}
const getPiecePos = (gameId: string, tileIdx: number): Point => {
const tile = getPiece(gameId, tileIdx)
const getTilePos = (gameId: string, tileIdx: number): Point => {
const tile = getTile(gameId, tileIdx)
return tile.pos
}
@ -287,9 +361,9 @@ const getBounds = (gameId: string): Rect => {
}
}
const getPieceBounds = (gameId: string, tileIdx: number): Rect => {
const s = getPieceSize(gameId)
const tile = getPiece(gameId, tileIdx)
const getTileBounds = (gameId: string, tileIdx: number): Rect => {
const s = getTileSize(gameId)
const tile = getTile(gameId, tileIdx)
return {
x: tile.pos.x,
y: tile.pos.y,
@ -298,13 +372,14 @@ const getPieceBounds = (gameId: string, tileIdx: number): Rect => {
}
}
const getPieceZIndex = (gameId: string, pieceIdx: number): number => {
return getPiece(gameId, pieceIdx).z
const getTileZIndex = (gameId: string, tileIdx: number): number => {
const tile = getTile(gameId, tileIdx)
return tile.z
}
const getFirstOwnedPieceIdx = (gameId: string, playerId: string): number => {
for (const t of GAMES[gameId].puzzle.tiles) {
const tile = Util.decodePiece(t)
const getFirstOwnedTileIdx = (gameId: string, playerId: string): number => {
for (let t of GAMES[gameId].puzzle.tiles) {
const tile = Util.decodeTile(t)
if (tile.owner === playerId) {
return tile.idx
}
@ -312,23 +387,20 @@ const getFirstOwnedPieceIdx = (gameId: string, playerId: string): number => {
return -1
}
const getFirstOwnedPiece = (
gameId: string,
playerId: string
): EncodedPiece|null => {
const idx = getFirstOwnedPieceIdx(gameId, playerId)
const getFirstOwnedTile = (gameId: string, playerId: string): EncodedPiece|null => {
const idx = getFirstOwnedTileIdx(gameId, playerId)
return idx < 0 ? null : GAMES[gameId].puzzle.tiles[idx]
}
const getPieceDrawOffset = (gameId: string): number => {
const getTileDrawOffset = (gameId: string): number => {
return GAMES[gameId].puzzle.info.tileDrawOffset
}
const getPieceDrawSize = (gameId: string): number => {
const getTileDrawSize = (gameId: string): number => {
return GAMES[gameId].puzzle.info.tileDrawSize
}
const getPieceSize = (gameId: string): number => {
const getTileSize = (gameId: string): number => {
return GAMES[gameId].puzzle.info.tileSize
}
@ -348,12 +420,12 @@ const getMaxZIndex = (gameId: string): number => {
return GAMES[gameId].puzzle.data.maxZ
}
const getMaxZIndexByPieceIdxs = (gameId: string, pieceIdxs: Array<number>): number => {
const getMaxZIndexByTileIdxs = (gameId: string, tileIdxs: Array<number>): number => {
let maxZ = 0
for (const pieceIdx of pieceIdxs) {
const curZ = getPieceZIndex(gameId, pieceIdx)
if (curZ > maxZ) {
maxZ = curZ
for (let tileIdx of tileIdxs) {
let tileZIndex = getTileZIndex(gameId, tileIdx)
if (tileZIndex > maxZ) {
maxZ = tileZIndex
}
}
return maxZ
@ -362,7 +434,7 @@ const getMaxZIndexByPieceIdxs = (gameId: string, pieceIdxs: Array<number>): numb
function srcPosByTileIdx(gameId: string, tileIdx: number): Point {
const info = GAMES[gameId].puzzle.info
const c = Util.coordByPieceIdx(info, tileIdx)
const c = Util.coordByTileIdx(info, tileIdx)
const cx = c.x * info.tileSize
const cy = c.y * info.tileSize
@ -372,7 +444,7 @@ function srcPosByTileIdx(gameId: string, tileIdx: number): Point {
function getSurroundingTilesByIdx(gameId: string, tileIdx: number) {
const info = GAMES[gameId].puzzle.info
const c = Util.coordByPieceIdx(info, tileIdx)
const c = Util.coordByTileIdx(info, tileIdx)
return [
// top
@ -386,117 +458,109 @@ function getSurroundingTilesByIdx(gameId: string, tileIdx: number) {
]
}
const setPiecesZIndex = (gameId: string, tileIdxs: Array<number>, zIndex: number): void => {
for (const tilesIdx of tileIdxs) {
changePiece(gameId, tilesIdx, { z: zIndex })
const setTilesZIndex = (gameId: string, tileIdxs: Array<number>, zIndex: number): void => {
for (let tilesIdx of tileIdxs) {
changeTile(gameId, tilesIdx, { z: zIndex })
}
}
const moveTileDiff = (gameId: string, tileIdx: number, diff: Point): void => {
const oldPos = getPiecePos(gameId, tileIdx)
const oldPos = getTilePos(gameId, tileIdx)
const pos = Geometry.pointAdd(oldPos, diff)
changePiece(gameId, tileIdx, { pos })
changeTile(gameId, tileIdx, { pos })
}
const movePiecesDiff = (
const moveTilesDiff = (
gameId: string,
pieceIdxs: Array<number>,
tileIdxs: Array<number>,
diff: Point
): void => {
const drawSize = getPieceDrawSize(gameId)
const tileDrawSize = getTileDrawSize(gameId)
const bounds = getBounds(gameId)
const cappedDiff = diff
for (const pieceIdx of pieceIdxs) {
const t = getPiece(gameId, pieceIdx)
for (let tileIdx of tileIdxs) {
const t = getTile(gameId, tileIdx)
if (t.pos.x + diff.x < bounds.x) {
cappedDiff.x = Math.max(bounds.x - t.pos.x, cappedDiff.x)
} else if (t.pos.x + drawSize + diff.x > bounds.x + bounds.w) {
cappedDiff.x = Math.min(bounds.x + bounds.w - t.pos.x + drawSize, cappedDiff.x)
} else if (t.pos.x + tileDrawSize + diff.x > bounds.x + bounds.w) {
cappedDiff.x = Math.min(bounds.x + bounds.w - t.pos.x + tileDrawSize, cappedDiff.x)
}
if (t.pos.y + diff.y < bounds.y) {
cappedDiff.y = Math.max(bounds.y - t.pos.y, cappedDiff.y)
} else if (t.pos.y + drawSize + diff.y > bounds.y + bounds.h) {
cappedDiff.y = Math.min(bounds.y + bounds.h - t.pos.y + drawSize, cappedDiff.y)
} else if (t.pos.y + tileDrawSize + diff.y > bounds.y + bounds.h) {
cappedDiff.y = Math.min(bounds.y + bounds.h - t.pos.y + tileDrawSize, cappedDiff.y)
}
}
for (const pieceIdx of pieceIdxs) {
moveTileDiff(gameId, pieceIdx, cappedDiff)
for (let tileIdx of tileIdxs) {
moveTileDiff(gameId, tileIdx, cappedDiff)
}
}
const isFinishedPiece = (gameId: string, pieceIdx: number): boolean => {
return getPieceOwner(gameId, pieceIdx) === -1
}
const getPieceOwner = (gameId: string, pieceIdx: number): string|number => {
return getPiece(gameId, pieceIdx).owner
}
const finishPieces = (gameId: string, pieceIdxs: Array<number>): void => {
for (const pieceIdx of pieceIdxs) {
changePiece(gameId, pieceIdx, { owner: -1, z: 1 })
const finishTiles = (gameId: string, tileIdxs: Array<number>): void => {
for (let tileIdx of tileIdxs) {
changeTile(gameId, tileIdx, { owner: -1, z: 1 })
}
}
const setTilesOwner = (
gameId: string,
pieceIdxs: Array<number>,
tileIdxs: Array<number>,
owner: string|number
): void => {
for (const pieceIdx of pieceIdxs) {
changePiece(gameId, pieceIdx, { owner })
for (let tileIdx of tileIdxs) {
changeTile(gameId, tileIdx, { owner })
}
}
// get all grouped tiles for a tile
function getGroupedPieceIdxs(gameId: string, pieceIdx: number): number[] {
const pieces = GAMES[gameId].puzzle.tiles
const piece = Util.decodePiece(pieces[pieceIdx])
function getGroupedTileIdxs(gameId: string, tileIdx: number): number[] {
const tiles = GAMES[gameId].puzzle.tiles
const tile = Util.decodeTile(tiles[tileIdx])
const grouped = []
if (piece.group) {
for (const other of pieces) {
const otherPiece = Util.decodePiece(other)
if (otherPiece.group === piece.group) {
grouped.push(otherPiece.idx)
if (tile.group) {
for (let other of tiles) {
const otherTile = Util.decodeTile(other)
if (otherTile.group === tile.group) {
grouped.push(otherTile.idx)
}
}
} else {
grouped.push(piece.idx)
grouped.push(tile.idx)
}
return grouped
}
// Returns the index of the puzzle tile with the highest z index
// that is not finished yet and that matches the position
const freePieceIdxByPos = (gameId: string, pos: Point): number => {
const info = GAMES[gameId].puzzle.info
const pieces = GAMES[gameId].puzzle.tiles
const freeTileIdxByPos = (gameId: string, pos: Point): number => {
let info = GAMES[gameId].puzzle.info
let tiles = GAMES[gameId].puzzle.tiles
let maxZ = -1
let pieceIdx = -1
for (let idx = 0; idx < pieces.length; idx++) {
const piece = Util.decodePiece(pieces[idx])
if (piece.owner !== 0) {
let tileIdx = -1
for (let idx = 0; idx < tiles.length; idx++) {
const tile = Util.decodeTile(tiles[idx])
if (tile.owner !== 0) {
continue
}
const collisionRect: Rect = {
x: piece.pos.x,
y: piece.pos.y,
x: tile.pos.x,
y: tile.pos.y,
w: info.tileSize,
h: info.tileSize,
}
if (Geometry.pointInBounds(pos, collisionRect)) {
if (maxZ === -1 || piece.z > maxZ) {
maxZ = piece.z
pieceIdx = idx
if (maxZ === -1 || tile.z > maxZ) {
maxZ = tile.z
tileIdx = idx
}
}
}
return pieceIdx
return tileIdx
}
const getPlayerBgColor = (gameId: string, playerId: string): string|null => {
@ -525,8 +589,8 @@ const areGrouped = (
tileIdx1: number,
tileIdx2: number
): boolean => {
const g1 = getPieceGroup(gameId, tileIdx1)
const g2 = getPieceGroup(gameId, tileIdx2)
const g1 = getTileGroup(gameId, tileIdx1)
const g2 = getTileGroup(gameId, tileIdx2)
return !!(g1 && g1 === g2)
}
@ -557,52 +621,47 @@ const getPuzzleHeight = (gameId: string): number => {
function handleInput(
gameId: string,
playerId: string,
input: Input,
ts: Timestamp,
onSnap?: (playerId: string) => void
): Array<Change> {
input: any,
ts: number
): Array<Array<any>> {
const puzzle = GAMES[gameId].puzzle
const evtInfo = getEvtInfo(gameId, playerId)
const changes: Array<Change> = []
const changes = [] as Array<Array<any>>
const _dataChange = (): void => {
changes.push([Protocol.CHANGE_DATA, puzzle.data])
}
const _pieceChange = (pieceIdx: number): void => {
const _tileChange = (tileIdx: number): void => {
changes.push([
Protocol.CHANGE_TILE,
Util.encodePiece(getPiece(gameId, pieceIdx)),
Util.encodeTile(getTile(gameId, tileIdx)),
])
}
const _pieceChanges = (pieceIdxs: Array<number>): void => {
for (const pieceIdx of pieceIdxs) {
_pieceChange(pieceIdx)
const _tileChanges = (tileIdxs: Array<number>): void => {
for (const tileIdx of tileIdxs) {
_tileChange(tileIdx)
}
}
const _playerChange = (): void => {
const player = getPlayer(gameId, playerId)
if (!player) {
return
}
changes.push([
Protocol.CHANGE_PLAYER,
Util.encodePlayer(player),
Util.encodePlayer(getPlayer(gameId, playerId)),
])
}
// put both tiles (and their grouped tiles) in the same group
const groupTiles = (
gameId: string,
pieceIdx1: number,
pieceIdx2: number
tileIdx1: number,
tileIdx2: number
): void => {
const pieces = GAMES[gameId].puzzle.tiles
const group1 = getPieceGroup(gameId, pieceIdx1)
const group2 = getPieceGroup(gameId, pieceIdx2)
const tiles = GAMES[gameId].puzzle.tiles
const group1 = getTileGroup(gameId, tileIdx1)
const group2 = getTileGroup(gameId, tileIdx2)
let group
const searchGroups = []
@ -623,18 +682,18 @@ function handleInput(
group = getMaxGroup(gameId)
}
changePiece(gameId, pieceIdx1, { group })
_pieceChange(pieceIdx1)
changePiece(gameId, pieceIdx2, { group })
_pieceChange(pieceIdx2)
changeTile(gameId, tileIdx1, { group })
_tileChange(tileIdx1)
changeTile(gameId, tileIdx2, { group })
_tileChange(tileIdx2)
// TODO: strange
if (searchGroups.length > 0) {
for (const p of pieces) {
const piece = Util.decodePiece(p)
if (searchGroups.includes(piece.group)) {
changePiece(gameId, piece.idx, { group })
_pieceChange(piece.idx)
for (const t of tiles) {
const tile = Util.decodeTile(t)
if (searchGroups.includes(tile.group)) {
changeTile(gameId, tile.idx, { group })
_tileChange(tile.idx)
}
}
}
@ -653,16 +712,6 @@ function handleInput(
const name = `${input[1]}`.substr(0, 16)
changePlayer(gameId, playerId, { name, ts })
_playerChange()
} else if (type === Protocol.INPUT_EV_MOVE) {
const w = input[1]
const h = input[2]
const player = getPlayer(gameId, playerId)
if (player) {
const x = player.x - w
const y = player.y - h
changePlayer(gameId, playerId, { ts, x, y })
_playerChange()
}
} else if (type === Protocol.INPUT_EV_MOUSE_DOWN) {
const x = input[1]
const y = input[2]
@ -672,15 +721,15 @@ function handleInput(
_playerChange()
evtInfo._last_mouse_down = pos
const tileIdxAtPos = freePieceIdxByPos(gameId, pos)
const tileIdxAtPos = freeTileIdxByPos(gameId, pos)
if (tileIdxAtPos >= 0) {
const maxZ = getMaxZIndex(gameId) + 1
let maxZ = getMaxZIndex(gameId) + 1
changeData(gameId, { maxZ })
_dataChange()
const tileIdxs = getGroupedPieceIdxs(gameId, tileIdxAtPos)
setPiecesZIndex(gameId, tileIdxs, getMaxZIndex(gameId))
const tileIdxs = getGroupedTileIdxs(gameId, tileIdxAtPos)
setTilesZIndex(gameId, tileIdxs, getMaxZIndex(gameId))
setTilesOwner(gameId, tileIdxs, playerId)
_pieceChanges(tileIdxs)
_tileChanges(tileIdxs)
}
evtInfo._last_mouse = pos
@ -694,19 +743,19 @@ function handleInput(
changePlayer(gameId, playerId, {x, y, ts})
_playerChange()
} else {
const pieceIdx = getFirstOwnedPieceIdx(gameId, playerId)
if (pieceIdx >= 0) {
let tileIdx = getFirstOwnedTileIdx(gameId, playerId)
if (tileIdx >= 0) {
// player is moving a tile (and hand)
changePlayer(gameId, playerId, {x, y, ts})
_playerChange()
// check if pos is on the tile, otherwise dont move
// (mouse could be out of table, but tile stays on it)
const pieceIdxs = getGroupedPieceIdxs(gameId, pieceIdx)
const tileIdxs = getGroupedTileIdxs(gameId, tileIdx)
let anyOk = Geometry.pointInBounds(pos, getBounds(gameId))
&& Geometry.pointInBounds(evtInfo._last_mouse_down, getBounds(gameId))
for (const idx of pieceIdxs) {
const bounds = getPieceBounds(gameId, idx)
for (let idx of tileIdxs) {
const bounds = getTileBounds(gameId, idx)
if (Geometry.pointInBounds(pos, bounds)) {
anyOk = true
break
@ -717,9 +766,9 @@ function handleInput(
const diffY = y - evtInfo._last_mouse_down.y
const diff = { x: diffX, y: diffY }
movePiecesDiff(gameId, pieceIdxs, diff)
moveTilesDiff(gameId, tileIdxs, diff)
_pieceChanges(pieceIdxs)
_tileChanges(tileIdxs)
}
} else {
// player is just moving map, so no change in position!
@ -739,44 +788,26 @@ function handleInput(
evtInfo._last_mouse_down = null
const pieceIdx = getFirstOwnedPieceIdx(gameId, playerId)
if (pieceIdx >= 0) {
let tileIdx = getFirstOwnedTileIdx(gameId, playerId)
if (tileIdx >= 0) {
// drop the tile(s)
const pieceIdxs = getGroupedPieceIdxs(gameId, pieceIdx)
setTilesOwner(gameId, pieceIdxs, 0)
_pieceChanges(pieceIdxs)
let tileIdxs = getGroupedTileIdxs(gameId, tileIdx)
setTilesOwner(gameId, tileIdxs, 0)
_tileChanges(tileIdxs)
// Check if the tile was dropped near the final location
const tilePos = getPiecePos(gameId, pieceIdx)
const finalPos = getFinalPiecePos(gameId, pieceIdx)
let canSnapToFinal = false
if (getSnapMode(gameId) === SnapMode.REAL) {
// only can snap to final if any of the grouped pieces are
// corner pieces
for (const pieceIdxTmp of pieceIdxs) {
if (isCornerPiece(gameId, pieceIdxTmp)) {
canSnapToFinal = true
break
}
}
} else {
canSnapToFinal = true
}
if (
canSnapToFinal
&& Geometry.pointDistance(finalPos, tilePos) < puzzle.info.snapDistance
) {
const diff = Geometry.pointSub(finalPos, tilePos)
let tilePos = getTilePos(gameId, tileIdx)
let finalPos = getFinalTilePos(gameId, tileIdx)
if (Geometry.pointDistance(finalPos, tilePos) < puzzle.info.snapDistance) {
let diff = Geometry.pointSub(finalPos, tilePos)
// Snap the tile to the final destination
movePiecesDiff(gameId, pieceIdxs, diff)
finishPieces(gameId, pieceIdxs)
_pieceChanges(pieceIdxs)
moveTilesDiff(gameId, tileIdxs, diff)
finishTiles(gameId, tileIdxs)
_tileChanges(tileIdxs)
let points = getPlayerPoints(gameId, playerId)
if (getScoreMode(gameId) === ScoreMode.FINAL) {
points += pieceIdxs.length
points += tileIdxs.length
} else if (getScoreMode(gameId) === ScoreMode.ANY) {
points += 1
} else {
@ -787,13 +818,10 @@ function handleInput(
_playerChange()
// check if the puzzle is finished
if (getFinishedPiecesCount(gameId) === getPieceCount(gameId)) {
if (getFinishedTileCount(gameId) === getTileCount(gameId)) {
changeData(gameId, { finished: ts })
_dataChange()
}
if (onSnap) {
onSnap(playerId)
}
} else {
// Snap to other tiles
const check = (
@ -802,44 +830,40 @@ function handleInput(
otherTileIdx: number,
off: Array<number>
): boolean => {
const info = GAMES[gameId].puzzle.info
let info = GAMES[gameId].puzzle.info
if (otherTileIdx < 0) {
return false
}
if (areGrouped(gameId, tileIdx, otherTileIdx)) {
return false
}
const tilePos = getPiecePos(gameId, tileIdx)
const tilePos = getTilePos(gameId, tileIdx)
const dstPos = Geometry.pointAdd(
getPiecePos(gameId, otherTileIdx),
getTilePos(gameId, otherTileIdx),
{x: off[0] * info.tileSize, y: off[1] * info.tileSize}
)
if (Geometry.pointDistance(tilePos, dstPos) < info.snapDistance) {
const diff = Geometry.pointSub(dstPos, tilePos)
let pieceIdxs = getGroupedPieceIdxs(gameId, tileIdx)
movePiecesDiff(gameId, pieceIdxs, diff)
let diff = Geometry.pointSub(dstPos, tilePos)
let tileIdxs = getGroupedTileIdxs(gameId, tileIdx)
moveTilesDiff(gameId, tileIdxs, diff)
groupTiles(gameId, tileIdx, otherTileIdx)
pieceIdxs = getGroupedPieceIdxs(gameId, tileIdx)
if (isFinishedPiece(gameId, otherTileIdx)) {
finishPieces(gameId, pieceIdxs)
} else {
const zIndex = getMaxZIndexByPieceIdxs(gameId, pieceIdxs)
setPiecesZIndex(gameId, pieceIdxs, zIndex)
}
_pieceChanges(pieceIdxs)
tileIdxs = getGroupedTileIdxs(gameId, tileIdx)
const zIndex = getMaxZIndexByTileIdxs(gameId, tileIdxs)
setTilesZIndex(gameId, tileIdxs, zIndex)
_tileChanges(tileIdxs)
return true
}
return false
}
let snapped = false
for (const pieceIdxTmp of getGroupedPieceIdxs(gameId, pieceIdx)) {
const othersIdxs = getSurroundingTilesByIdx(gameId, pieceIdxTmp)
for (let tileIdxTmp of getGroupedTileIdxs(gameId, tileIdx)) {
let othersIdxs = getSurroundingTilesByIdx(gameId, tileIdxTmp)
if (
check(gameId, pieceIdxTmp, othersIdxs[0], [0, 1]) // top
|| check(gameId, pieceIdxTmp, othersIdxs[1], [-1, 0]) // right
|| check(gameId, pieceIdxTmp, othersIdxs[2], [0, -1]) // bottom
|| check(gameId, pieceIdxTmp, othersIdxs[3], [1, 0]) // left
check(gameId, tileIdxTmp, othersIdxs[0], [0, 1]) // top
|| check(gameId, tileIdxTmp, othersIdxs[1], [-1, 0]) // right
|| check(gameId, tileIdxTmp, othersIdxs[2], [0, -1]) // bottom
|| check(gameId, tileIdxTmp, othersIdxs[3], [1, 0]) // left
) {
snapped = true
break
@ -853,16 +877,6 @@ function handleInput(
changePlayer(gameId, playerId, { d, ts })
_playerChange()
}
if (snapped && getSnapMode(gameId) === SnapMode.REAL) {
if (getFinishedPiecesCount(gameId) === getPieceCount(gameId)) {
changeData(gameId, { finished: ts })
_dataChange()
}
}
if (snapped && onSnap) {
onSnap(playerId)
}
}
} else {
changePlayer(gameId, playerId, { d, ts })
@ -891,15 +905,17 @@ function handleInput(
}
export default {
__createPlayerObject,
setGame,
exists,
playerExists,
getActivePlayers,
getIdlePlayers,
addPlayer,
getFinishedPiecesCount,
getPieceCount,
getFinishedTileCount,
getTileCount,
getImageUrl,
setImageUrl,
get,
getAllGames,
getPlayerBgColor,
@ -909,7 +925,7 @@ export default {
getPlayerIdByIndex,
changePlayer,
setPlayer,
setPiece,
setTile,
setPuzzleData,
getTableWidth,
getTableHeight,
@ -917,11 +933,11 @@ export default {
getRng,
getPuzzleWidth,
getPuzzleHeight,
getPiecesSortedByZIndex,
getFirstOwnedPiece,
getPieceDrawOffset,
getPieceDrawSize,
getFinalPiecePos,
getTilesSortedByZIndex,
getFirstOwnedTile,
getTileDrawOffset,
getTileDrawSize,
getFinalTilePos,
getStartTs,
getFinishTs,
handleInput,

View file

@ -40,8 +40,10 @@ EV_SERVER_INIT: event sent to one client after that client
*/
const EV_SERVER_EVENT = 1
const EV_SERVER_INIT = 4
const EV_SERVER_INIT_REPLAY = 5
const EV_CLIENT_EVENT = 2
const EV_CLIENT_INIT = 3
const EV_CLIENT_INIT_REPLAY = 6
const LOG_HEADER = 1
const LOG_ADD_PLAYER = 2
@ -58,17 +60,6 @@ const INPUT_EV_PLAYER_COLOR = 7
const INPUT_EV_PLAYER_NAME = 8
const INPUT_EV_MOVE = 9
const INPUT_EV_TOGGLE_PREVIEW = 10
const INPUT_EV_TOGGLE_SOUNDS = 11
const INPUT_EV_REPLAY_TOGGLE_PAUSE = 12
const INPUT_EV_REPLAY_SPEED_UP = 13
const INPUT_EV_REPLAY_SPEED_DOWN = 14
const INPUT_EV_TOGGLE_PLAYER_NAMES = 15
const INPUT_EV_CENTER_FIT_PUZZLE = 16
const INPUT_EV_TOGGLE_FIXED_PIECES = 17
const INPUT_EV_TOGGLE_LOOSE_PIECES = 18
const CHANGE_DATA = 1
const CHANGE_TILE = 2
@ -77,8 +68,10 @@ const CHANGE_PLAYER = 3
export default {
EV_SERVER_EVENT,
EV_SERVER_INIT,
EV_SERVER_INIT_REPLAY,
EV_CLIENT_EVENT,
EV_CLIENT_INIT,
EV_CLIENT_INIT_REPLAY,
LOG_HEADER,
LOG_ADD_PLAYER,
@ -98,17 +91,6 @@ export default {
INPUT_EV_PLAYER_NAME,
INPUT_EV_TOGGLE_PREVIEW,
INPUT_EV_TOGGLE_SOUNDS,
INPUT_EV_REPLAY_TOGGLE_PAUSE,
INPUT_EV_REPLAY_SPEED_UP,
INPUT_EV_REPLAY_SPEED_DOWN,
INPUT_EV_TOGGLE_PLAYER_NAMES,
INPUT_EV_CENTER_FIT_PUZZLE,
INPUT_EV_TOGGLE_FIXED_PIECES,
INPUT_EV_TOGGLE_LOOSE_PIECES,
CHANGE_DATA,
CHANGE_TILE,

View file

@ -1,4 +1,4 @@
export interface RngSerialized {
interface RngSerialized {
rand_high: number,
rand_low: number,
}
@ -15,7 +15,7 @@ export class Rng {
random (min: number, max: number): number {
this.rand_high = ((this.rand_high << 16) + (this.rand_high >> 16) + this.rand_low) & 0xffffffff;
this.rand_low = (this.rand_low + this.rand_high) & 0xffffffff;
const n = (this.rand_high >>> 0) / 0xffffffff;
var n = (this.rand_high >>> 0) / 0xffffffff;
return (min + n * (max-min+1))|0;
}

View file

@ -1,255 +0,0 @@
import { Point } from "./Geometry"
import { Rng, RngSerialized } from "./Rng"
// @see https://stackoverflow.com/a/59906630/392905
type ArrayLengthMutationKeys = 'splice' | 'push' | 'pop' | 'shift' | 'unshift' | number
type ArrayItems<T extends Array<any>> = T extends Array<infer TItems> ? TItems : never
export type FixedLengthArray<T extends any[]> =
Pick<T, Exclude<keyof T, ArrayLengthMutationKeys>>
& { [Symbol.iterator]: () => IterableIterator< ArrayItems<T> > }
export type Timestamp = number
export type Input = any
export type Change = Array<any>
export type GameEvent = Array<any>
export type ServerEvent = Array<any>
export type ClientEvent = Array<any>
export type EncodedPlayer = FixedLengthArray<[
string,
number,
number,
0|1,
string|null,
string|null,
string|null,
number,
Timestamp,
]>
export type EncodedPiece = FixedLengthArray<[
number,
number,
number,
number,
string|number,
number,
]>
export type EncodedPieceShape = number
export type EncodedGame = FixedLengthArray<[
string,
string,
RngSerialized,
Puzzle,
Array<EncodedPlayer>,
Record<string, EvtInfo>,
ScoreMode,
ShapeMode,
SnapMode,
number|null,
]>
export interface ReplayData {
log: any[],
game: EncodedGame|null
}
export interface Tag {
id: number
slug: string
title: string
total: number
}
interface GameRng {
obj: Rng
type?: string
}
export interface Game {
id: string
creatorUserId: number|null
players: Array<EncodedPlayer>
puzzle: Puzzle
evtInfos: Record<string, EvtInfo>
scoreMode: ScoreMode
shapeMode: ShapeMode
snapMode: SnapMode
rng: GameRng
}
export interface Image {
id: number
filename: string
file: string
url: string
title: string
tags: Array<Tag>
created: number
}
export interface GameSettings {
tiles: number
image: ImageInfo
scoreMode: ScoreMode
shapeMode: ShapeMode
snapMode: SnapMode
}
export interface Puzzle {
tiles: Array<EncodedPiece>
data: PuzzleData
info: PuzzleInfo
}
export interface PuzzleData {
started: number
finished: number
maxGroup: number
maxZ: number
}
export interface PuzzleDataChange {
started?: number
finished?: number
maxGroup?: number
maxZ?: number
}
interface PuzzleTable {
width: number
height: number
}
enum PieceEdge {
Flat = 0,
Out = 1,
In = -1,
}
export interface PieceShape {
top: PieceEdge
bottom: PieceEdge
left: PieceEdge
right: PieceEdge
}
export interface Piece {
owner: string|number
idx: number
pos: Point
z: number
group: number
}
export interface PieceChange {
owner?: string|number
idx?: number
pos?: Point
z?: number
group?: number
}
export interface ImageInfo
{
id: number
uploaderUserId: number|null
filename: string
url: string
title: string
tags: Tag[]
created: Timestamp
width: number
height: number
}
export interface PuzzleInfo {
table: PuzzleTable
targetTiles: number
imageUrl?: string // deprecated, use image.url instead
image?: ImageInfo
width: number
height: number
tileSize: number
tileDrawSize: number
tileMarginWidth: number
tileDrawOffset: number
snapDistance: number
tiles: number
tilesX: number
tilesY: number
shapes: Array<EncodedPieceShape>
}
export interface Player {
id: string
x: number
y: number
d: 0|1
name: string|null
color: string|null
bgcolor: string|null
points: number
ts: Timestamp
}
export interface PlayerChange {
id?: string
x?: number
y?: number
d?: 0|1
name?: string|null
color?: string|null
bgcolor?: string|null
points?: number
ts?: Timestamp
}
export interface EvtInfo {
_last_mouse: Point|null
_last_mouse_down: Point|null
}
export enum ScoreMode {
FINAL = 0,
ANY = 1,
}
export enum ShapeMode {
NORMAL = 0,
ANY = 1,
FLAT = 2,
}
export enum SnapMode {
NORMAL = 0,
REAL = 1,
}
export const DefaultScoreMode = (v: any): ScoreMode => {
if (v === ScoreMode.FINAL || v === ScoreMode.ANY) {
return v
}
return ScoreMode.FINAL
}
export const DefaultShapeMode = (v: any): ShapeMode => {
if (v === ShapeMode.NORMAL || v === ShapeMode.ANY || v === ShapeMode.FLAT) {
return v
}
return ShapeMode.NORMAL
}
export const DefaultSnapMode = (v: any): SnapMode => {
if (v === SnapMode.NORMAL || v === SnapMode.REAL) {
return v
}
return SnapMode.NORMAL
}

View file

@ -1,29 +1,15 @@
import { PuzzleCreationInfo } from '../server/Puzzle'
import {
EncodedGame,
EncodedPiece,
EncodedPieceShape,
EncodedPlayer,
Game,
Piece,
PieceShape,
Player,
PuzzleInfo,
ScoreMode,
ShapeMode,
SnapMode
} from './Types'
import { EncodedPiece, EncodedPieceShape, EncodedPlayer, Piece, PieceShape, Player } from './GameCommon'
import { Point } from './Geometry'
import { Rng } from './Rng'
const slug = (str: string): string => {
const slug = (str: string) => {
let tmp = str.toLowerCase()
tmp = tmp.replace(/[^a-z0-9]+/g, '-')
tmp = tmp.replace(/^-|-$/, '')
return tmp
}
const pad = (x: number, pad: string): string => {
const pad = (x: any, pad: string) => {
const str = `${x}`
if (str.length >= pad.length) {
return str
@ -31,11 +17,8 @@ const pad = (x: number, pad: string): string => {
return pad.substr(0, pad.length - str.length) + str
}
type LogArgs = Array<any>
type LogFn = (...args: LogArgs) => void
export const logger = (...pre: string[]): { log: LogFn, error: LogFn, info: LogFn } => {
const log = (m: 'log'|'info'|'error') => (...args: LogArgs): void => {
export const logger = (...pre: Array<any>) => {
const log = (m: 'log'|'info'|'error') => (...args: Array<any>) => {
const d = new Date()
const hh = pad(d.getHours(), '00')
const mm = pad(d.getMinutes(), '00')
@ -50,9 +33,7 @@ export const logger = (...pre: string[]): { log: LogFn, error: LogFn, info: LogF
}
// get a unique id
export const uniqId = (): string => {
return Date.now().toString(36) + Math.random().toString(36).substring(2)
}
export const uniqId = () => Date.now().toString(36) + Math.random().toString(36).substring(2)
function encodeShape(data: PieceShape): EncodedPieceShape {
/* encoded in 1 byte:
@ -77,11 +58,11 @@ function decodeShape(data: EncodedPieceShape): PieceShape {
}
}
function encodePiece(data: Piece): EncodedPiece {
function encodeTile(data: Piece): EncodedPiece {
return [data.idx, data.pos.x, data.pos.y, data.z, data.owner, data.group]
}
function decodePiece(data: EncodedPiece): Piece {
function decodeTile(data: EncodedPiece): Piece {
return {
idx: data[0],
pos: {
@ -122,22 +103,25 @@ function decodePlayer(data: EncodedPlayer): Player {
}
}
function encodeGame(data: Game): EncodedGame {
function encodeGame(data: any): Array<any> {
if (Array.isArray(data)) {
return data
}
return [
data.id,
data.rng.type || '',
data.rng.type,
Rng.serialize(data.rng.obj),
data.puzzle,
data.players,
data.evtInfos,
data.scoreMode,
data.shapeMode,
data.snapMode,
data.creatorUserId,
]
}
function decodeGame(data: EncodedGame): Game {
function decodeGame(data: any) {
if (!Array.isArray(data)) {
return data
}
return {
id: data[0],
rng: {
@ -148,17 +132,14 @@ function decodeGame(data: EncodedGame): Game {
players: data[4],
evtInfos: data[5],
scoreMode: data[6],
shapeMode: data[7],
snapMode: data[8],
creatorUserId: data[9],
}
}
function coordByPieceIdx(info: PuzzleInfo|PuzzleCreationInfo, pieceIdx: number): Point {
function coordByTileIdx(info: any, tileIdx: number): Point {
const wTiles = info.width / info.tileSize
return {
x: pieceIdx % wTiles,
y: Math.floor(pieceIdx / wTiles),
x: tileIdx % wTiles,
y: Math.floor(tileIdx / wTiles),
}
}
@ -166,16 +147,16 @@ const hash = (str: string): number => {
let hash = 0
for (let i = 0; i < str.length; i++) {
const chr = str.charCodeAt(i);
let chr = str.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
}
function asQueryArgs(data: Record<string, any>): string {
function asQueryArgs(data: any) {
const q = []
for (const k in data) {
for (let k in data) {
const pair = [k, data[k]].map(encodeURIComponent)
q.push(pair.join('='))
}
@ -193,8 +174,8 @@ export default {
encodeShape,
decodeShape,
encodePiece,
decodePiece,
encodeTile,
decodeTile,
encodePlayer,
decodePlayer,
@ -202,7 +183,7 @@ export default {
encodeGame,
decodeGame,
coordByPieceIdx,
coordByTileIdx,
asQueryArgs,
}

View file

@ -0,0 +1,7 @@
CREATE TABLE game_log (
game_id TEXT,
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
entry TEXT NOT NULL
);

View file

@ -1,22 +0,0 @@
-- Add width/height to images table
CREATE TABLE images_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created TIMESTAMP NOT NULL,
filename TEXT NOT NULL UNIQUE,
filename_original TEXT NOT NULL,
title TEXT NOT NULL,
width INTEGER NOT NULL,
height INTEGER NOT NULL
);
INSERT INTO images_new
SELECT id, created, filename, filename_original, title, 0, 0
FROM images;
DROP TABLE images;
ALTER TABLE images_new RENAME TO images;

View file

@ -1,45 +0,0 @@
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created TIMESTAMP NOT NULL,
client_id TEXT NOT NULL,
client_secret TEXT NOT NULL
);
CREATE TABLE images_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
uploader_user_id INTEGER,
created TIMESTAMP NOT NULL,
filename TEXT NOT NULL UNIQUE,
filename_original TEXT NOT NULL,
title TEXT NOT NULL,
width INTEGER NOT NULL,
height INTEGER NOT NULL
);
CREATE TABLE image_x_category_new (
image_id INTEGER NOT NULL,
category_id INTEGER NOT NULL
);
INSERT INTO images_new
SELECT id, NULL, created, filename, filename_original, title, width, height
FROM images;
INSERT INTO image_x_category_new
SELECT image_id, category_id
FROM image_x_category;
PRAGMA foreign_keys = OFF;
DROP TABLE images;
DROP TABLE image_x_category;
ALTER TABLE images_new RENAME TO images;
ALTER TABLE image_x_category_new RENAME TO image_x_category;
PRAGMA foreign_keys = ON;

View file

@ -1,11 +0,0 @@
CREATE TABLE games (
id TEXT PRIMARY KEY,
creator_user_id INTEGER,
image_id INTEGER NOT NULL,
created TIMESTAMP NOT NULL,
finished TIMESTAMP NOT NULL,
data TEXT NOT NULL
);

View file

@ -1,7 +1,7 @@
<template>
<div id="app">
<ul class="nav" v-if="showNav">
<li><router-link class="btn" :to="{name: 'index'}">Games overview</router-link></li>
<li><router-link class="btn" :to="{name: 'index'}">Index</router-link></li>
<li><router-link class="btn" :to="{name: 'new-game'}">New game</router-link></li>
</ul>

View file

@ -11,12 +11,6 @@ export default function Camera () {
let y = 0
let curZoom = 1
const reset = () => {
x = 0
y = 0
curZoom = 1
}
const move = (byX: number, byY: number) => {
x += byX / curZoom
y += byY / curZoom
@ -122,32 +116,15 @@ export default function Camera () {
}
}
const viewportDimToWorld = (viewportDim: Dim): Dim => {
const { w, h } = viewportDimToWorldRaw(viewportDim)
return { w: Math.round(w), h: Math.round(h) }
}
const viewportDimToWorldRaw = (viewportDim: Dim): Dim => {
return {
w: viewportDim.w / curZoom,
h: viewportDim.h / curZoom,
}
}
return {
getCurrentZoom: () => curZoom,
reset,
move,
canZoom,
zoom,
setZoom,
worldToViewport,
worldToViewportRaw,
worldDimToViewport, // not used outside
worldDimToViewportRaw,
viewportToWorld,
viewportToWorldRaw, // not used outside
viewportDimToWorld,
viewportDimToWorldRaw,
}
}

View file

@ -1,9 +1,7 @@
"use strict"
import { ClientEvent, EncodedGame, GameEvent, ReplayData, ServerEvent } from '../common/Types'
import Util, { logger } from '../common/Util'
import { logger } from '../common/Util'
import Protocol from './../common/Protocol'
import xhr from './xhr'
const log = logger('Communication.js')
@ -17,41 +15,25 @@ const CONN_STATE_CONNECTING = 3 // connecting
const CONN_STATE_CLOSED = 4 // not connected (closed on purpose)
let ws: WebSocket
let changesCallback = (msg: Array<any>) => {}
let connectionStateChangeCallback = (state: number) => {}
let missedMessages: ServerEvent[] = []
let changesCallback = (msg: ServerEvent) => {
missedMessages.push(msg)
}
let missedStateChanges: Array<number> = []
let connectionStateChangeCallback = (state: number) => {
missedStateChanges.push(state)
}
function onServerChange(callback: (msg: ServerEvent) => void): void {
// TODO: change these to something like on(EVT, cb)
function onServerChange(callback: (msg: Array<any>) => void) {
changesCallback = callback
for (const missedMessage of missedMessages) {
changesCallback(missedMessage)
}
missedMessages = []
}
function onConnectionStateChange(callback: (state: number) => void): void {
function onConnectionStateChange(callback: (state: number) => void) {
connectionStateChangeCallback = callback
for (const missedStateChange of missedStateChanges) {
connectionStateChangeCallback(missedStateChange)
}
missedStateChanges = []
}
let connectionState = CONN_STATE_NOT_CONNECTED
const setConnectionState = (state: number): void => {
const setConnectionState = (state: number) => {
if (connectionState !== state) {
connectionState = state
connectionStateChangeCallback(state)
}
}
function send(message: ClientEvent): void {
function send(message: Array<any>): void {
if (connectionState === CONN_STATE_CONNECTED) {
try {
ws.send(JSON.stringify(message))
@ -61,25 +43,26 @@ function send(message: ClientEvent): void {
}
}
let clientSeq: number
let events: Record<number, GameEvent>
let events: Record<number, any>
function connect(
address: string,
gameId: string,
clientId: string
): Promise<EncodedGame> {
): Promise<any> {
clientSeq = 0
events = {}
setConnectionState(CONN_STATE_CONNECTING)
return new Promise(resolve => {
ws = new WebSocket(address, clientId + '|' + gameId)
ws.onopen = () => {
ws.onopen = (e) => {
setConnectionState(CONN_STATE_CONNECTED)
send([Protocol.EV_CLIENT_INIT])
}
ws.onmessage = (e: MessageEvent) => {
const msg: ServerEvent = JSON.parse(e.data)
ws.onmessage = (e) => {
const msg = JSON.parse(e.data)
const msgType = msg[0]
if (msgType === Protocol.EV_SERVER_INIT) {
const game = msg[1]
@ -98,12 +81,12 @@ function connect(
}
}
ws.onerror = () => {
ws.onerror = (e) => {
setConnectionState(CONN_STATE_DISCONNECTED)
throw `[ 2021-05-15 onerror ]`
}
ws.onclose = (e: CloseEvent) => {
ws.onclose = (e) => {
if (e.code === CODE_CUSTOM_DISCONNECT || e.code === CODE_GOING_AWAY) {
setConnectionState(CONN_STATE_CLOSED)
} else {
@ -113,14 +96,47 @@ function connect(
})
}
async function requestReplayData(
// TOOD: change replay stuff
function connectReplay(
address: string,
gameId: string,
offset: number
): Promise<ReplayData> {
const args = { gameId, offset }
const res = await xhr.get(`/api/replay-data${Util.asQueryArgs(args)}`, {})
const json: ReplayData = await res.json()
return json
clientId: string
): Promise<{ game: any, log: Array<any> }> {
clientSeq = 0
events = {}
setConnectionState(CONN_STATE_CONNECTING)
return new Promise(resolve => {
ws = new WebSocket(address, clientId + '|' + gameId)
ws.onopen = (e) => {
setConnectionState(CONN_STATE_CONNECTED)
send([Protocol.EV_CLIENT_INIT_REPLAY])
}
ws.onmessage = (e) => {
const msg = JSON.parse(e.data)
const msgType = msg[0]
if (msgType === Protocol.EV_SERVER_INIT_REPLAY) {
const game = msg[1]
const log = msg[2]
const replay: { game: any, log: Array<any> } = { game, log }
resolve(replay)
} else {
throw `[ 2021-05-09 invalid connectReplay msgType ${msgType} ]`
}
}
ws.onerror = (e) => {
setConnectionState(CONN_STATE_DISCONNECTED)
throw `[ 2021-05-15 onerror ]`
}
ws.onclose = (e) => {
if (e.code === CODE_CUSTOM_DISCONNECT || e.code === CODE_GOING_AWAY) {
setConnectionState(CONN_STATE_CLOSED)
} else {
setConnectionState(CONN_STATE_DISCONNECTED)
}
}
})
}
function disconnect(): void {
@ -131,7 +147,7 @@ function disconnect(): void {
events = {}
}
function sendClientEvent(evt: GameEvent): void {
function sendClientEvent(evt: any): void {
// when sending event, increase number of sent events
// and add the event locally
clientSeq++;
@ -141,7 +157,7 @@ function sendClientEvent(evt: GameEvent): void {
export default {
connect,
requestReplayData,
connectReplay,
disconnect,
sendClientEvent,
onServerChange,

View file

@ -7,12 +7,12 @@ const log = logger('Debug.js')
let _pt = 0
let _mindiff = 0
const checkpoint_start = (mindiff: number): void => {
const checkpoint_start = (mindiff: number) => {
_pt = performance.now()
_mindiff = mindiff
}
const checkpoint = (label: string): void => {
const checkpoint = (label: string) => {
const now = performance.now()
const diff = now - _pt
if (diff > _mindiff) {

View file

@ -1,177 +0,0 @@
import Protocol from "../common/Protocol"
import { GameEvent } from "../common/Types"
import { MODE_REPLAY } from "./game"
function EventAdapter (
canvas: HTMLCanvasElement,
window: any,
viewport: any,
MODE: string
) {
let events: Array<GameEvent> = []
let KEYS_ON = true
let LEFT = false
let RIGHT = false
let UP = false
let DOWN = false
let ZOOM_IN = false
let ZOOM_OUT = false
let SHIFT = false
const toWorldPoint = (x: number, y: number): [number, number] => {
const pos = viewport.viewportToWorld({x, y})
return [pos.x, pos.y]
}
const mousePos = (ev: MouseEvent) => toWorldPoint(ev.offsetX, ev.offsetY)
const canvasCenter = () => toWorldPoint(canvas.width / 2, canvas.height / 2)
const key = (state: boolean, ev: KeyboardEvent) => {
if (!KEYS_ON) {
return
}
if (ev.code === 'ShiftLeft' || ev.code === 'ShiftRight') {
SHIFT = state
} else if (ev.code === 'ArrowUp' || ev.code === 'KeyW') {
UP = state
} else if (ev.code === 'ArrowDown' || ev.code === 'KeyS') {
DOWN = state
} else if (ev.code === 'ArrowLeft' || ev.code === 'KeyA') {
LEFT = state
} else if (ev.code === 'ArrowRight' || ev.code === 'KeyD') {
RIGHT = state
} else if (ev.code === 'KeyQ') {
ZOOM_OUT = state
} else if (ev.code === 'KeyE') {
ZOOM_IN = state
}
}
let lastMouse: [number, number]|null = null
canvas.addEventListener('mousedown', (ev) => {
lastMouse = mousePos(ev)
if (ev.button === 0) {
addEvent([Protocol.INPUT_EV_MOUSE_DOWN, ...lastMouse])
}
})
canvas.addEventListener('mouseup', (ev) => {
lastMouse = mousePos(ev)
if (ev.button === 0) {
addEvent([Protocol.INPUT_EV_MOUSE_UP, ...lastMouse])
}
})
canvas.addEventListener('mousemove', (ev) => {
lastMouse = mousePos(ev)
addEvent([Protocol.INPUT_EV_MOUSE_MOVE, ...lastMouse])
})
canvas.addEventListener('wheel', (ev) => {
lastMouse = mousePos(ev)
if (viewport.canZoom(ev.deltaY < 0 ? 'in' : 'out')) {
const evt = ev.deltaY < 0
? Protocol.INPUT_EV_ZOOM_IN
: Protocol.INPUT_EV_ZOOM_OUT
addEvent([evt, ...lastMouse])
}
})
window.addEventListener('keydown', (ev: KeyboardEvent) => key(true, ev))
window.addEventListener('keyup', (ev: KeyboardEvent) => key(false, ev))
window.addEventListener('keypress', (ev: KeyboardEvent) => {
if (!KEYS_ON) {
return
}
if (ev.code === 'Space') {
addEvent([Protocol.INPUT_EV_TOGGLE_PREVIEW])
}
if (MODE === MODE_REPLAY) {
if (ev.code === 'KeyI') {
addEvent([Protocol.INPUT_EV_REPLAY_SPEED_UP])
}
if (ev.code === 'KeyO') {
addEvent([Protocol.INPUT_EV_REPLAY_SPEED_DOWN])
}
if (ev.code === 'KeyP') {
addEvent([Protocol.INPUT_EV_REPLAY_TOGGLE_PAUSE])
}
}
if (ev.code === 'KeyF') {
addEvent([Protocol.INPUT_EV_TOGGLE_FIXED_PIECES])
}
if (ev.code === 'KeyG') {
addEvent([Protocol.INPUT_EV_TOGGLE_LOOSE_PIECES])
}
if (ev.code === 'KeyM') {
addEvent([Protocol.INPUT_EV_TOGGLE_SOUNDS])
}
if (ev.code === 'KeyN') {
addEvent([Protocol.INPUT_EV_TOGGLE_PLAYER_NAMES])
}
if (ev.code === 'KeyC') {
addEvent([Protocol.INPUT_EV_CENTER_FIT_PUZZLE])
}
})
const addEvent = (event: GameEvent) => {
events.push(event)
}
const consumeAll = (): GameEvent[] => {
if (events.length === 0) {
return []
}
const all = events.slice()
events = []
return all
}
const createKeyEvents = (): void => {
const w = (LEFT ? 1 : 0) - (RIGHT ? 1 : 0)
const h = (UP ? 1 : 0) - (DOWN ? 1 : 0)
if (w !== 0 || h !== 0) {
const amount = (SHIFT ? 24 : 12) * Math.sqrt(viewport.getCurrentZoom())
const pos = viewport.viewportDimToWorld({w: w * amount, h: h * amount})
addEvent([Protocol.INPUT_EV_MOVE, pos.w, pos.h])
if (lastMouse) {
lastMouse[0] -= pos.w
lastMouse[1] -= pos.h
}
}
if (ZOOM_IN && ZOOM_OUT) {
// cancel each other out
} else if (ZOOM_IN) {
if (viewport.canZoom('in')) {
const target = lastMouse || canvasCenter()
addEvent([Protocol.INPUT_EV_ZOOM_IN, ...target])
}
} else if (ZOOM_OUT) {
if (viewport.canZoom('out')) {
const target = lastMouse || canvasCenter()
addEvent([Protocol.INPUT_EV_ZOOM_OUT, ...target])
}
}
}
const setHotkeys = (state: boolean) => {
KEYS_ON = state
}
return {
addEvent,
consumeAll,
createKeyEvents,
setHotkeys,
}
}
export default EventAdapter

View file

@ -1,6 +1,7 @@
"use strict"
import { Rng } from '../common/Rng'
import Util from '../common/Util'
let minVx = -10
let deltaVx = 20
@ -107,11 +108,11 @@ class Bomb {
}
class Particle {
px: number
py: number
px: any
py: any
vx: number
vy: number
color: string
color: any
duration: number
alive: boolean
radius: number
@ -170,7 +171,7 @@ class Controller {
})
}
setSpeedParams(): void {
setSpeedParams() {
let heightReached = 0
let vy = 0
@ -187,11 +188,11 @@ class Controller {
deltaVx = 2 * vx
}
resize(): void {
resize() {
this.setSpeedParams()
}
init(): void {
init() {
this.readyBombs = []
this.explodedBombs = []
this.particles = []
@ -201,7 +202,7 @@ class Controller {
}
}
update(): void {
update() {
if (Math.random() * 100 < percentChanceNewBomb) {
this.readyBombs.push(new Bomb(this.rng))
}
@ -249,7 +250,7 @@ class Controller {
this.particles = aliveParticles
}
render(): void {
render() {
this.ctx.beginPath()
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.1)' // Ghostly effect
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height)

View file

@ -3,22 +3,22 @@
import Geometry, { Rect } from '../common/Geometry'
import Graphics from './Graphics'
import Util, { logger } from './../common/Util'
import { Puzzle, PuzzleInfo, PieceShape, EncodedPiece } from './../common/Types'
import { Puzzle, PuzzleInfo, PieceShape } from './../common/GameCommon'
const log = logger('PuzzleGraphics.js')
async function createPuzzleTileBitmaps(
img: ImageBitmap,
pieces: EncodedPiece[],
tiles: Array<any>,
info: PuzzleInfo
): Promise<Array<ImageBitmap>> {
log.log('start createPuzzleTileBitmaps')
const tileSize = info.tileSize
const tileMarginWidth = info.tileMarginWidth
const tileDrawSize = info.tileDrawSize
const tileRatio = tileSize / 100.0
var tileSize = info.tileSize
var tileMarginWidth = info.tileMarginWidth
var tileDrawSize = info.tileDrawSize
var tileRatio = tileSize / 100.0
const curvyCoords = [
var curvyCoords = [
0, 0, 40, 15, 37, 5,
37, 5, 40, 0, 38, -5,
38, -5, 20, -20, 50, -20,
@ -27,7 +27,7 @@ async function createPuzzleTileBitmaps(
63, 5, 65, 15, 100, 0
];
const bitmaps: Array<ImageBitmap> = new Array(pieces.length)
const bitmaps: Array<ImageBitmap> = new Array(tiles.length)
const paths: Record<string, Path2D> = {}
function pathForShape(shape: PieceShape) {
@ -65,9 +65,9 @@ async function createPuzzleTileBitmaps(
}
if (shape.bottom !== 0) {
for (let i = 0; i < curvyCoords.length / 6; i++) {
const p1 = Geometry.pointSub(bottomRightEdge, { x: curvyCoords[i * 6 + 0] * tileRatio, y: shape.bottom * curvyCoords[i * 6 + 1] * tileRatio })
const p2 = Geometry.pointSub(bottomRightEdge, { x: curvyCoords[i * 6 + 2] * tileRatio, y: shape.bottom * curvyCoords[i * 6 + 3] * tileRatio })
const p3 = Geometry.pointSub(bottomRightEdge, { x: curvyCoords[i * 6 + 4] * tileRatio, y: shape.bottom * curvyCoords[i * 6 + 5] * tileRatio })
let p1 = Geometry.pointSub(bottomRightEdge, { x: curvyCoords[i * 6 + 0] * tileRatio, y: shape.bottom * curvyCoords[i * 6 + 1] * tileRatio })
let p2 = Geometry.pointSub(bottomRightEdge, { x: curvyCoords[i * 6 + 2] * tileRatio, y: shape.bottom * curvyCoords[i * 6 + 3] * tileRatio })
let p3 = Geometry.pointSub(bottomRightEdge, { x: curvyCoords[i * 6 + 4] * tileRatio, y: shape.bottom * curvyCoords[i * 6 + 5] * tileRatio })
path.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
}
} else {
@ -75,9 +75,9 @@ async function createPuzzleTileBitmaps(
}
if (shape.left !== 0) {
for (let i = 0; i < curvyCoords.length / 6; i++) {
const p1 = Geometry.pointSub(bottomLeftEdge, { x: -shape.left * curvyCoords[i * 6 + 1] * tileRatio, y: curvyCoords[i * 6 + 0] * tileRatio })
const p2 = Geometry.pointSub(bottomLeftEdge, { x: -shape.left * curvyCoords[i * 6 + 3] * tileRatio, y: curvyCoords[i * 6 + 2] * tileRatio })
const p3 = Geometry.pointSub(bottomLeftEdge, { x: -shape.left * curvyCoords[i * 6 + 5] * tileRatio, y: curvyCoords[i * 6 + 4] * tileRatio })
let p1 = Geometry.pointSub(bottomLeftEdge, { x: -shape.left * curvyCoords[i * 6 + 1] * tileRatio, y: curvyCoords[i * 6 + 0] * tileRatio })
let p2 = Geometry.pointSub(bottomLeftEdge, { x: -shape.left * curvyCoords[i * 6 + 3] * tileRatio, y: curvyCoords[i * 6 + 2] * tileRatio })
let p3 = Geometry.pointSub(bottomLeftEdge, { x: -shape.left * curvyCoords[i * 6 + 5] * tileRatio, y: curvyCoords[i * 6 + 4] * tileRatio })
path.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
}
} else {
@ -93,10 +93,10 @@ async function createPuzzleTileBitmaps(
const c2 = Graphics.createCanvas(tileDrawSize, tileDrawSize)
const ctx2 = c2.getContext('2d') as CanvasRenderingContext2D
for (const p of pieces) {
const piece = Util.decodePiece(p)
const srcRect = srcRectByIdx(info, piece.idx)
const path = pathForShape(Util.decodeShape(info.shapes[piece.idx]))
for (const t of tiles) {
const tile = Util.decodeTile(t)
const srcRect = srcRectByIdx(info, tile.idx)
const path = pathForShape(Util.decodeShape(info.shapes[tile.idx]))
ctx.clearRect(0, 0, tileDrawSize, tileDrawSize)
@ -195,7 +195,7 @@ async function createPuzzleTileBitmaps(
ctx2.restore()
ctx.drawImage(c2, 0, 0)
bitmaps[piece.idx] = await createImageBitmap(c)
bitmaps[tile.idx] = await createImageBitmap(c)
}
log.log('end createPuzzleTileBitmaps')
@ -203,7 +203,7 @@ async function createPuzzleTileBitmaps(
}
function srcRectByIdx(puzzleInfo: PuzzleInfo, idx: number): Rect {
const c = Util.coordByPieceIdx(puzzleInfo, idx)
const c = Util.coordByTileIdx(puzzleInfo, idx)
return {
x: c.x * puzzleInfo.tileSize,
y: c.y * puzzleInfo.tileSize,

Binary file not shown.

View file

@ -23,7 +23,7 @@
<!-- TODO: autocomplete tags -->
<td><label>Tags</label></td>
<td>
<tags-input v-model="tags" :autocompleteTags="autocompleteTags" />
<tags-input v-model="tags" />
</td>
</tr>
</table>
@ -38,7 +38,7 @@
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
import { Image, Tag } from '../../common/Types'
import { Image, Tag } from '../../common/GameCommon'
import ResponsiveImage from './ResponsiveImage.vue'
import TagsInput from './TagsInput.vue'
@ -54,9 +54,6 @@ export default defineComponent({
type: Object as PropType<Image>,
required: true,
},
autocompleteTags: {
type: Function,
},
},
emits: {
bgclick: null,
@ -96,17 +93,7 @@ export default defineComponent({
height: 90%;
width: 80%;
}
@media (max-width: 1400px) and (min-height: 720px),
(max-width: 1000px) {
.edit-image-dialog .overlay-content {
grid-template-columns: auto;
grid-template-rows: 1fr min-content min-content;
grid-template-areas:
"image"
"settings"
"buttons";
}
}
.edit-image-dialog .area-image {
grid-area: image;
margin: 20px;

View file

@ -7,7 +7,7 @@
{{time(game.started, game.finished)}}<br />
</span>
</router-link>
<router-link v-if="game.hasReplay" class="game-replay" :to="{ name: 'replay', params: { id: game.id } }">
<router-link v-if="false && game.hasReplay" class="game-replay" :to="{ name: 'replay', params: { id: game.id } }">
Watch replay
</router-link>
</div>

View file

@ -10,17 +10,8 @@
<tr><td>🔍+ Zoom in:</td><td><div><kbd>E</kbd>/🖱-Wheel</div></td></tr>
<tr><td>🔍- Zoom out:</td><td><div><kbd>Q</kbd>/🖱-Wheel</div></td></tr>
<tr><td>🖼 Toggle preview:</td><td><div><kbd>Space</kbd></div></td></tr>
<tr><td>🎯 Center puzzle in screen:</td><td><div><kbd>C</kbd></div></td></tr>
<tr><td>🧩 Toggle fixed pieces:</td><td><div><kbd>F</kbd></div></td></tr>
<tr><td>🧩 Toggle loose pieces:</td><td><div><kbd>G</kbd></div></td></tr>
<tr><td>👤 Toggle player names:</td><td><div><kbd>N</kbd></div></td></tr>
<tr><td>🔉 Toggle sounds:</td><td><div><kbd>M</kbd></div></td></tr>
<tr><td> Speed up (replay):</td><td><div><kbd>I</kbd></div></td></tr>
<tr><td> Speed down (replay):</td><td><div><kbd>O</kbd></div></td></tr>
<tr><td> Pause (replay):</td><td><div><kbd>P</kbd></div></td></tr>
</table>
</div>
</template>

View file

@ -5,7 +5,7 @@
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { Image } from '../../common/Types'
import { Image } from '../../common/GameCommon'
import ImageTeaser from './ImageTeaser.vue'

View file

@ -3,7 +3,7 @@
class="imageteaser"
:style="style"
@click="onClick">
<div class="btn edit" v-if="canEdit" @click.stop="onEditClick"></div>
<div class="btn edit" @click.stop="onEditClick"></div>
</div>
</template>
<script lang="ts">
@ -18,18 +18,12 @@ export default defineComponent({
},
},
computed: {
style(): object {
style (): object {
const url = this.image.url.replace('uploads/', 'uploads/r/') + '-150x100.webp'
return {
'backgroundImage': `url("${url}")`,
}
},
canEdit(): boolean {
if (!this.$me.id) {
return false
}
return this.$me.id === this.image.uploaderUserId
},
},
emits: {
click: null,

View file

@ -1,68 +0,0 @@
<template>
<div class="overlay transparent" @click="$emit('bgclick')">
<table class="overlay-content help" @click.stop="">
<tr>
<td colspan="2">Info about this puzzle</td>
</tr>
<tr>
<td>Image Title: </td>
<td>{{game.puzzle.info.image.title}}</td>
</tr>
<tr>
<td>Scoring: </td>
<td><span :title="snapMode[1]">{{scoreMode[0]}}</span></td>
</tr>
<tr>
<td>Shapes: </td>
<td><span :title="snapMode[1]">{{shapeMode[0]}}</span></td>
</tr>
<tr>
<td>Snapping: </td>
<td><span :title="snapMode[1]">{{snapMode[0]}}</span></td>
</tr>
</table>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
import { Game, ScoreMode, ShapeMode, SnapMode } from '../../common/Types'
export default defineComponent({
name: 'help-overlay',
emits: {
bgclick: null,
},
props: {
game: {
type: Object as PropType<Game>,
required: true,
},
},
computed: {
scoreMode () {
switch (this.game.scoreMode) {
case ScoreMode.ANY: return ['Any', 'Score when pieces are connected to each other or on final location']
case ScoreMode.FINAL:
default: return ['Final', 'Score when pieces are put to their final location']
}
},
shapeMode () {
switch (this.game.shapeMode) {
case ShapeMode.FLAT: return ['Flat', 'All pieces flat on all sides']
case ShapeMode.ANY: return ['Any', 'Flat pieces can occur anywhere']
case ShapeMode.NORMAL:
default:
return ['Normal', '']
}
},
snapMode () {
switch (this.game.snapMode) {
case SnapMode.REAL: return ['Real', 'Pieces snap only to corners, already snapped pieces and to each other']
case SnapMode.NORMAL:
default:
return ['Normal', 'Pieces snap to final destination and to each other']
}
},
},
})
</script>

View file

@ -6,10 +6,6 @@
<div class="has-image">
<responsive-image :src="image.url" :title="image.title" />
</div>
<div class="image-title" v-if="image.title || image.width || image.height">
<span class="image-title-title" v-if="image.title">"{{image.title}}"</span>
<span class="image-title-dim" v-if="image.width || image.height">({{image.width}} {{image.height}})</span>
</div>
</div>
<div class="area-settings">
@ -21,34 +17,9 @@
<tr>
<td><label>Scoring: </label></td>
<td>
<label><input type="radio" v-model="scoreMode" value="1" />
Any (Score when pieces are connected to each other or on final location)</label>
<label><input type="radio" v-model="scoreMode" value="1" /> Any (Score when pieces are connected to each other or on final location)</label>
<br />
<label><input type="radio" v-model="scoreMode" value="0" />
Final (Score when pieces are put to their final location)</label>
</td>
</tr>
<tr>
<td><label>Shapes: </label></td>
<td>
<label><input type="radio" v-model="shapeMode" value="0" />
Normal</label>
<br />
<label><input type="radio" v-model="shapeMode" value="1" />
Any (Flat pieces can occur anywhere)</label>
<br />
<label><input type="radio" v-model="shapeMode" value="2" />
Flat (All pieces flat on all sides)</label>
</td>
</tr>
<tr>
<td><label>Snapping: </label></td>
<td>
<label><input type="radio" v-model="snapMode" value="0" />
Normal (Pieces snap to final destination and to each other)</label>
<br />
<label><input type="radio" v-model="snapMode" value="1" />
Real (Pieces snap only to corners, already snapped pieces and to each other)</label>
<label><input type="radio" v-model="scoreMode" value="0" /> Final (Score when pieces are put to their final location)</label>
</td>
</tr>
</table>
@ -65,7 +36,7 @@
<script lang="ts">
import { defineComponent } from 'vue'
import { GameSettings, ScoreMode, ShapeMode, SnapMode } from './../../common/Types'
import { GameSettings, ScoreMode } from './../../common/GameCommon'
import ResponsiveImage from './ResponsiveImage.vue'
export default defineComponent({
@ -87,8 +58,6 @@ export default defineComponent({
return {
tiles: 1000,
scoreMode: ScoreMode.ANY,
shapeMode: ShapeMode.NORMAL,
snapMode: SnapMode.NORMAL,
}
},
methods: {
@ -97,8 +66,6 @@ export default defineComponent({
tiles: this.tilesInt,
image: this.image,
scoreMode: this.scoreModeInt,
shapeMode: this.shapeModeInt,
snapMode: this.snapModeInt,
} as GameSettings)
},
},
@ -117,12 +84,6 @@ export default defineComponent({
scoreModeInt (): number {
return parseInt(`${this.scoreMode}`, 10)
},
shapeModeInt (): number {
return parseInt(`${this.shapeMode}`, 10)
},
snapModeInt (): number {
return parseInt(`${this.snapMode}`, 10)
},
tilesInt (): number {
return parseInt(`${this.tiles}`, 10)
},
@ -145,26 +106,7 @@ export default defineComponent({
}
.new-game-dialog .area-image {
grid-area: image;
display: grid;
grid-template-rows: 1fr min-content;
grid-template-areas:
"image"
"image-title";
margin-right: 1em;
}
@media (max-width: 1400px) and (min-height: 720px),
(max-width: 1000px) {
.new-game-dialog .overlay-content {
grid-template-columns: auto;
grid-template-rows: 1fr min-content min-content;
grid-template-areas:
"image"
"settings"
"buttons";
}
.new-game-dialog .area-image {
margin-right: 0;
}
margin: 20px;
}
.new-game-dialog .area-settings {
grid-area: settings;
@ -182,29 +124,13 @@ export default defineComponent({
width: 100%;
}
.new-game-dialog .has-image {
box-sizing: border-box;
grid-area: image;
position: relative;
width: 100%;
height: 100%;
border: solid 1px;
}
.new-game-dialog .image-title {
grid-area: image-title;
text-align: center;
padding: .5em 0;
background: var(--main-color);
color: #262523;
}
.new-game-dialog .has-image .remove {
position: absolute;
top: .5em;
left: .5em;
}
.new-game-dialog .image-title > span { margin-right: .5em; }
.new-game-dialog .image-title > span:last-child { margin-right: 0; }
.image-title-dim { display: inline-block; white-space: no-wrap; }
</style>

View file

@ -7,21 +7,15 @@ gallery", if possible!
<div class="overlay new-image-dialog" @click="$emit('bgclick')">
<div class="overlay-content" @click.stop="">
<div
class="area-image"
:class="{'has-image': !!previewUrl, 'no-image': !previewUrl, droppable: droppable}"
@drop="onDrop"
@dragover="onDragover"
@dragleave="onDragleave">
<div class="area-image" :class="{'has-image': !!previewUrl, 'no-image': !previewUrl}">
<!-- TODO: ... -->
<div class="drop-target"></div>
<div v-if="previewUrl" class="has-image">
<span class="remove btn" @click="previewUrl=''">X</span>
<responsive-image :src="previewUrl" />
</div>
<div v-else>
<label class="upload">
<input type="file" style="display: none" @change="onFileSelect" accept="image/*" />
<input type="file" style="display: none" @change="preview" accept="image/*" />
<span class="btn">Upload File</span>
</label>
</div>
@ -42,57 +36,32 @@ gallery", if possible!
<!-- TODO: autocomplete tags -->
<td><label>Tags</label></td>
<td>
<tags-input v-model="tags" :autocompleteTags="autocompleteTags" />
<tags-input v-model="tags" />
</td>
</tr>
</table>
</div>
<div class="area-buttons">
<button class="btn"
:disabled="!canPostToGallery"
@click="postToGallery"
>
<template v-if="uploading === 'postToGallery'">Uploading ({{uploadProgressPercent}}%)</template>
<template v-else>🖼 Post to gallery</template>
</button>
<button class="btn"
:disabled="!canSetupGameClick"
@click="setupGameClick"
>
<template v-if="uploading === 'setupGame'">Uploading ({{uploadProgressPercent}}%)</template>
<template v-else>🧩 Post to gallery <br /> + set up game</template>
</button>
<button class="btn" :disabled="!canPostToGallery" @click="postToGallery">🖼 Post to gallery</button>
<button class="btn" :disabled="!canSetupGameClick" @click="setupGameClick">🧩 Post to gallery <br /> + set up game</button>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { logger } from '../../common/Util'
import ResponsiveImage from './ResponsiveImage.vue'
import TagsInput from './TagsInput.vue'
const log = logger('NewImageDialog.vue')
export default defineComponent({
name: 'new-image-dialog',
components: {
ResponsiveImage,
TagsInput,
},
props: {
autocompleteTags: {
type: Function,
},
uploadProgress: {
type: Number,
},
uploading: {
type: String,
},
},
emits: {
bgclick: null,
setupGameClick: null,
@ -104,47 +73,23 @@ export default defineComponent({
file: null as File|null,
title: '',
tags: [] as string[],
droppable: false,
}
},
computed: {
uploadProgressPercent (): number {
return this.uploadProgress ? Math.round(this.uploadProgress * 100) : 0
},
canPostToGallery (): boolean {
if (this.uploading) {
return false
}
return !!(this.previewUrl && this.file)
},
canSetupGameClick (): boolean {
if (this.uploading) {
return false
}
return !!(this.previewUrl && this.file)
},
},
methods: {
imageFromDragEvt (evt: DragEvent): DataTransferItem|null {
const items = evt.dataTransfer?.items
if (!items || items.length === 0) {
return null
}
const item = items[0]
if (!item.type.startsWith('image/')) {
return null
}
return item
},
onFileSelect (evt: Event) {
preview (evt: Event) {
const target = (evt.target as HTMLInputElement)
if (!target.files) return;
const file = target.files[0]
if (!file) return;
this.preview(file)
},
preview (file: File) {
const r = new FileReader()
r.readAsDataURL(file)
r.onload = (ev: any) => {
@ -166,34 +111,6 @@ export default defineComponent({
tags: this.tags,
})
},
onDrop (evt: DragEvent): boolean {
this.droppable = false
const img = this.imageFromDragEvt(evt)
if (!img) {
return false
}
const f = img.getAsFile()
if (!f) {
return false
}
this.file = f
this.preview(f)
evt.preventDefault()
return false
},
onDragover (evt: DragEvent): boolean {
const img = this.imageFromDragEvt(evt)
if (!img) {
return false
}
this.droppable = true
evt.preventDefault()
return false
},
onDragleave () {
log.info('onDragleave')
this.droppable = false
},
},
})
</script>
@ -210,35 +127,17 @@ export default defineComponent({
height: 90%;
width: 80%;
}
@media (max-width: 1400px) and (min-height: 720px),
(max-width: 1000px) {
.new-image-dialog .overlay-content {
grid-template-columns: auto;
grid-template-rows: 1fr min-content min-content;
grid-template-areas:
"image"
"settings"
"buttons";
}
.new-image-dialog .overlay-content .area-buttons .btn br {
display: none;
}
}
.new-image-dialog .area-image {
grid-area: image;
margin: .5em;
border: solid 6px transparent;
margin: 20px;
}
.new-image-dialog .area-image.no-image {
align-content: center;
display: grid;
text-align: center;
border: solid 6px;
position: relative;
}
.new-image-dialog .area-image.droppable {
border: dashed 6px;
position: relative;
}
.new-image-dialog .area-image .has-image {
position: relative;
@ -281,16 +180,4 @@ export default defineComponent({
top: 50%;
transform: translate(-50%,-50%);
}
.area-image .drop-target {
display: none;
}
.area-image.droppable .drop-target {
pointer-events: none;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 3;
}
</style>

View file

@ -13,28 +13,6 @@
<td><label>Name: </label></td>
<td><input type="text" maxLength="16" v-model="modelValue.name" /></td>
</tr>
<tr>
<td><label>Sounds: </label></td>
<td><input type="checkbox" v-model="modelValue.soundsEnabled" /></td>
</tr>
<tr>
<td><label>Sounds Volume: </label></td>
<td class="sound-volume">
<span @click="decreaseVolume">🔉</span>
<input
type="range"
min="0"
max="100"
:value="modelValue.soundsVolume"
@change="updateVolume"
/>
<span @click="increaseVolume">🔊</span>
</td>
</tr>
<tr>
<td><label>Show player names: </label></td>
<td><input type="checkbox" v-model="modelValue.showPlayerNames" /></td>
</tr>
</table>
</div>
</template>
@ -48,23 +26,7 @@ export default defineComponent({
'update:modelValue': null,
},
props: {
modelValue: {
type: Object,
required: true,
},
},
methods: {
updateVolume (ev: Event): void {
(this.modelValue as any).soundsVolume = (ev.target as HTMLInputElement).value
},
decreaseVolume (): void {
const vol = parseInt(this.modelValue.soundsVolume, 10) - 5
this.modelValue.soundsVolume = Math.max(0, vol)
},
increaseVolume (): void {
const vol = parseInt(this.modelValue.soundsVolume, 10) + 5
this.modelValue.soundsVolume = Math.min(100, vol)
},
modelValue: Object,
},
created () {
// TODO: ts type PlayerSettings
@ -74,7 +36,3 @@ export default defineComponent({
},
})
</script>
<style scoped>
.sound-volume span { cursor: pointer; user-select: none; }
.sound-volume input { vertical-align: middle; }
</style>

View file

@ -1,42 +1,19 @@
<template>
<div>
<input
ref="input"
class="input"
type="text"
v-model="input"
placeholder="Plants, People"
@change="onChange"
@keydown.enter="add"
@keyup="onKeyUp"
/>
<div v-if="autocomplete.values" class="autocomplete">
<ul>
<li
v-for="(val,idx) in autocomplete.values"
:key="idx"
:class="{active: idx===autocomplete.idx}"
@click="addVal(val)"
>{{val}}</li>
</ul>
</div>
<input class="input" type="text" v-model="input" placeholder="Plants, People" @keydown.enter="add" @keyup="onKeyUp" />
<span v-for="(tag,idx) in values" :key="idx" class="bit" @click="rm(tag)">{{tag}} </span>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
import { Tag } from '../../common/Types'
export default defineComponent({
name: 'tags-input',
props: {
modelValue: {
type: Array as PropType<string[]>,
type: Array as PropType<Array<string>>,
required: true,
},
autocompleteTags: {
type: Function,
},
},
emits: {
'update:modelValue': null,
@ -44,11 +21,7 @@ export default defineComponent({
data () {
return {
input: '',
values: [] as string[],
autocomplete: {
idx: -1,
values: [] as string[],
},
values: [] as Array<string>,
}
},
created () {
@ -56,39 +29,14 @@ export default defineComponent({
},
methods: {
onKeyUp (ev: KeyboardEvent) {
if (ev.code === 'ArrowDown' && this.autocomplete.values.length > 0) {
if (this.autocomplete.idx < this.autocomplete.values.length - 1) {
this.autocomplete.idx++
}
ev.stopPropagation()
return false
}
if (ev.code === 'ArrowUp' && this.autocomplete.values.length > 0) {
if (this.autocomplete.idx > 0) {
this.autocomplete.idx--
}
ev.stopPropagation()
return false
}
if (ev.key === ',') {
this.add()
ev.stopPropagation()
return false
}
if (this.input && this.autocompleteTags) {
this.autocomplete.values = this.autocompleteTags(
this.input,
this.values
)
this.autocomplete.idx = -1
} else {
this.autocomplete.values = []
this.autocomplete.idx = -1
}
},
addVal (value: string) {
const newval = value.replace(/,/g, '').trim()
add () {
const newval = this.input.replace(/,/g, '').trim()
if (!newval) {
return
}
@ -96,16 +44,7 @@ export default defineComponent({
this.values.push(newval)
}
this.input = ''
this.autocomplete.values = []
this.autocomplete.idx = -1
this.$emit('update:modelValue', this.values)
;(this.$refs.input as HTMLInputElement).focus()
},
add () {
const value = this.autocomplete.idx >= 0
? this.autocomplete.values[this.autocomplete.idx]
: this.input
this.addVal(value)
},
rm (val: string) {
this.values = this.values.filter(v => v !== val)
@ -118,31 +57,4 @@ export default defineComponent({
.input {
margin-bottom: .5em;
}
.autocomplete {
position: relative;
}
.autocomplete ul { list-style: none;
padding: 0;
margin: 0;
position: absolute;
left: 0;
right: 0;
background: #333230;
top: -.5em;
}
.autocomplete ul li {
position: relative;
padding: .5em .5em .5em 1.5em;
cursor: pointer;
}
.autocomplete ul li.active {
color: var(--link-hover-color);
background: var(--input-bg-color);
}
.autocomplete ul li.active:before {
content: '▶';
display: block;
position: absolute;
left: .5em;
}
</style>

View file

@ -6,7 +6,6 @@
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import xhr from '../xhr'
export default defineComponent({
name: 'upload',
@ -22,7 +21,8 @@ export default defineComponent({
if (!file) return;
const formData = new FormData();
formData.append('file', file, file.name);
const res = await xhr.post('/upload', {
const res = await fetch('/upload', {
method: 'post',
body: formData,
})
const j = await res.json()

View file

@ -1,44 +1,34 @@
"use strict"
import { GameLoopInstance, run } from './gameloop'
import {run} from './gameloop'
import Camera from './Camera'
import Graphics from './Graphics'
import Debug from './Debug'
import Communication from './Communication'
import Util, { logger } from './../common/Util'
import Util from './../common/Util'
import PuzzleGraphics from './PuzzleGraphics'
import Game from './../common/GameCommon'
import Game, { Player, Piece } from './../common/GameCommon'
import fireworksController from './Fireworks'
import Protocol from '../common/Protocol'
import Time from '../common/Time'
import settings from './settings'
import { SETTINGS } from './settings'
import { Dim, Point } from '../common/Geometry'
import {
FixedLengthArray,
Game as GameType,
Player,
Piece,
EncodedGame,
ReplayData,
Timestamp,
ServerEvent,
} from '../common/Types'
import EventAdapter from './EventAdapter'
declare global {
interface Window {
DEBUG?: boolean
}
}
const log = logger('game.ts')
// @see https://stackoverflow.com/a/59906630/392905
type ArrayLengthMutationKeys = 'splice' | 'push' | 'pop' | 'shift' | 'unshift' | number
type ArrayItems<T extends Array<any>> = T extends Array<infer TItems> ? TItems : never
type FixedLengthArray<T extends any[]> =
Pick<T, Exclude<keyof T, ArrayLengthMutationKeys>>
& { [Symbol.iterator]: () => IterableIterator< ArrayItems<T> > }
// @ts-ignore
const images = import.meta.globEager('./*.png')
// @ts-ignore
const sounds = import.meta.globEager('./*.mp3')
export const MODE_PLAY = 'play'
export const MODE_REPLAY = 'replay'
@ -46,7 +36,6 @@ let PIECE_VIEW_FIXED = true
let PIECE_VIEW_LOOSE = true
interface Hud {
setPuzzleCut: () => void
setActivePlayers: (v: Array<any>) => void
setIdlePlayers: (v: Array<any>) => void
setFinished: (v: boolean) => void
@ -55,24 +44,18 @@ interface Hud {
setPiecesTotal: (v: number) => void
setConnectionState: (v: number) => void
togglePreview: () => void
toggleSoundsEnabled: () => void
togglePlayerNames: () => void
setReplaySpeed?: (v: number) => void
setReplayPaused?: (v: boolean) => void
}
interface Replay {
final: boolean
log: Array<any> // current log entries
logPointer: number // pointer to current item in the log array
log: Array<any>
logIdx: number
speeds: Array<number>
speedIdx: number
paused: boolean
lastRealTs: number
lastGameTs: number
gameStartTs: number
skipNonActionPhases: boolean
//
dataOffset: number
}
const shouldDrawPiece = (piece: Piece) => {
@ -96,6 +79,139 @@ function addCanvasToDom(TARGET_EL: HTMLElement, canvas: HTMLCanvasElement) {
return canvas
}
function EventAdapter (canvas: HTMLCanvasElement, window: any, viewport: any) {
let events: Array<Array<any>> = []
let KEYS_ON = true
let LEFT = false
let RIGHT = false
let UP = false
let DOWN = false
let ZOOM_IN = false
let ZOOM_OUT = false
let SHIFT = false
const toWorldPoint = (x: number, y: number) => {
const pos = viewport.viewportToWorld({x, y})
return [pos.x, pos.y]
}
const mousePos = (ev: MouseEvent) => toWorldPoint(ev.offsetX, ev.offsetY)
const canvasCenter = () => toWorldPoint(canvas.width / 2, canvas.height / 2)
const key = (state: boolean, ev: KeyboardEvent) => {
if (!KEYS_ON) {
return
}
if (ev.key === 'Shift') {
SHIFT = state
} else if (ev.key === 'ArrowUp' || ev.key === 'w' || ev.key === 'W') {
UP = state
} else if (ev.key === 'ArrowDown' || ev.key === 's' || ev.key === 'S') {
DOWN = state
} else if (ev.key === 'ArrowLeft' || ev.key === 'a' || ev.key === 'A') {
LEFT = state
} else if (ev.key === 'ArrowRight' || ev.key === 'd' || ev.key === 'D') {
RIGHT = state
} else if (ev.key === 'q') {
ZOOM_OUT = state
} else if (ev.key === 'e') {
ZOOM_IN = state
}
}
canvas.addEventListener('mousedown', (ev) => {
if (ev.button === 0) {
addEvent([Protocol.INPUT_EV_MOUSE_DOWN, ...mousePos(ev)])
}
})
canvas.addEventListener('mouseup', (ev) => {
if (ev.button === 0) {
addEvent([Protocol.INPUT_EV_MOUSE_UP, ...mousePos(ev)])
}
})
canvas.addEventListener('mousemove', (ev) => {
addEvent([Protocol.INPUT_EV_MOUSE_MOVE, ...mousePos(ev)])
})
canvas.addEventListener('wheel', (ev) => {
if (viewport.canZoom(ev.deltaY < 0 ? 'in' : 'out')) {
const evt = ev.deltaY < 0
? Protocol.INPUT_EV_ZOOM_IN
: Protocol.INPUT_EV_ZOOM_OUT
addEvent([evt, ...mousePos(ev)])
}
})
window.addEventListener('keydown', (ev: KeyboardEvent) => key(true, ev))
window.addEventListener('keyup', (ev: KeyboardEvent) => key(false, ev))
window.addEventListener('keypress', (ev: KeyboardEvent) => {
if (!KEYS_ON) {
return
}
if (ev.key === ' ') {
addEvent([Protocol.INPUT_EV_TOGGLE_PREVIEW])
}
if (ev.key === 'F' || ev.key === 'f') {
PIECE_VIEW_FIXED = !PIECE_VIEW_FIXED
RERENDER = true
}
if (ev.key === 'G' || ev.key === 'g') {
PIECE_VIEW_LOOSE = !PIECE_VIEW_LOOSE
RERENDER = true
}
})
const addEvent = (event: Array<any>) => {
events.push(event)
}
const consumeAll = () => {
if (events.length === 0) {
return []
}
const all = events.slice()
events = []
return all
}
const createKeyEvents = () => {
const amount = SHIFT ? 20 : 10
const x = (LEFT ? amount : 0) - (RIGHT ? amount : 0)
const y = (UP ? amount : 0) - (DOWN ? amount : 0)
if (x !== 0 || y !== 0) {
addEvent([Protocol.INPUT_EV_MOVE, x, y])
}
if (ZOOM_IN && ZOOM_OUT) {
// cancel each other out
} else if (ZOOM_IN) {
if (viewport.canZoom('in')) {
addEvent([Protocol.INPUT_EV_ZOOM_IN, ...canvasCenter()])
}
} else if (ZOOM_OUT) {
if (viewport.canZoom('out')) {
addEvent([Protocol.INPUT_EV_ZOOM_OUT, ...canvasCenter()])
}
}
}
const setHotkeys = (state: boolean) => {
KEYS_ON = state
}
return {
addEvent,
consumeAll,
createKeyEvents,
setHotkeys,
}
}
export async function main(
gameId: string,
clientId: string,
@ -110,9 +226,6 @@ export async function main(
return MODE === MODE_REPLAY || player.id !== clientId
}
const click = sounds['./click.mp3'].default
const clickAudio = new Audio(click)
const cursorGrab = await Graphics.loadImageToBitmap(images['./grab.png'].default)
const cursorHand = await Graphics.loadImageToBitmap(images['./hand.png'].default)
const cursorGrabMask = await Graphics.loadImageToBitmap(images['./grab_mask.png'].default)
@ -147,63 +260,36 @@ export async function main(
// stuff only available in replay mode...
// TODO: refactor
const REPLAY: Replay = {
final: false,
log: [],
logPointer: 0,
speeds: [0.5, 1, 2, 5, 10, 20, 50, 100, 250, 500],
logIdx: 0,
speeds: [0.5, 1, 2, 5, 10, 20, 50],
speedIdx: 1,
paused: false,
lastRealTs: 0,
lastGameTs: 0,
gameStartTs: 0,
skipNonActionPhases: true,
dataOffset: 0,
}
Communication.onConnectionStateChange((state) => {
HUD.setConnectionState(state)
})
const queryNextReplayBatch = async (
gameId: string
): Promise<ReplayData> => {
const offset = REPLAY.dataOffset
REPLAY.dataOffset += 10000 // meh
const replay: ReplayData = await Communication.requestReplayData(
gameId,
offset
)
// cut log that was already handled
REPLAY.log = REPLAY.log.slice(REPLAY.logPointer)
REPLAY.logPointer = 0
REPLAY.log.push(...replay.log)
if (replay.log.length === 0) {
REPLAY.final = true
}
return replay
}
let TIME: () => number = () => 0
const connect = async () => {
if (MODE === MODE_PLAY) {
const game: EncodedGame = await Communication.connect(wsAddress, gameId, clientId)
const gameObject: GameType = Util.decodeGame(game)
const game = await Communication.connect(wsAddress, gameId, clientId)
const gameObject = Util.decodeGame(game)
Game.setGame(gameObject.id, gameObject)
TIME = () => Time.timestamp()
} else if (MODE === MODE_REPLAY) {
const replay: ReplayData = await queryNextReplayBatch(gameId)
if (!replay.game) {
throw '[ 2021-05-29 no game received ]'
}
const gameObject: GameType = Util.decodeGame(replay.game)
// TODO: change how replay connect is done...
const replay: {game: any, log: Array<any>} = await Communication.connectReplay(wsAddress, gameId, clientId)
const gameObject = Util.decodeGame(replay.game)
Game.setGame(gameObject.id, gameObject)
REPLAY.log = replay.log
REPLAY.lastRealTs = Time.timestamp()
REPLAY.gameStartTs = parseInt(replay.log[0][4], 10)
REPLAY.gameStartTs = parseInt(REPLAY.log[0][REPLAY.log[0].length - 2], 10)
REPLAY.lastGameTs = REPLAY.gameStartTs
TIME = () => REPLAY.lastGameTs
} else {
throw '[ 2020-12-22 MODE invalid, must be play|replay ]'
@ -215,8 +301,8 @@ export async function main(
await connect()
const PIECE_DRAW_OFFSET = Game.getPieceDrawOffset(gameId)
const PIECE_DRAW_SIZE = Game.getPieceDrawSize(gameId)
const TILE_DRAW_OFFSET = Game.getTileDrawOffset(gameId)
const TILE_DRAW_SIZE = Game.getTileDrawSize(gameId)
const PUZZLE_WIDTH = Game.getPuzzleWidth(gameId)
const PUZZLE_HEIGHT = Game.getPuzzleHeight(gameId)
const TABLE_WIDTH = Game.getTableWidth(gameId)
@ -231,8 +317,8 @@ export async function main(
h: PUZZLE_HEIGHT,
}
const PIECE_DIM = {
w: PIECE_DRAW_SIZE,
h: PIECE_DRAW_SIZE,
w: TILE_DRAW_SIZE,
h: TILE_DRAW_SIZE,
}
const bitmaps = await PuzzleGraphics.loadPuzzleBitmaps(Game.getPuzzle(gameId))
@ -242,40 +328,17 @@ export async function main(
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
canvas.classList.add('loaded')
HUD.setPuzzleCut()
// initialize some view data
// this global data will change according to input events
const viewport = Camera()
// center viewport
viewport.move(
-(TABLE_WIDTH - canvas.width) /2,
-(TABLE_HEIGHT - canvas.height) /2
)
const centerPuzzle = () => {
// center on the puzzle
viewport.reset()
viewport.move(
-(TABLE_WIDTH - canvas.width) /2,
-(TABLE_HEIGHT - canvas.height) /2
)
// zoom viewport to fit whole puzzle in
const x = viewport.worldDimToViewport(BOARD_DIM)
const border = 20
const targetW = canvas.width - (border * 2)
const targetH = canvas.height - (border * 2)
if (
(x.w > targetW || x.h > targetH)
|| (x.w < targetW && x.h < targetH)
) {
const zoom = Math.min(targetW / x.w, targetH / x.h)
viewport.setZoom(zoom, {
x: canvas.width / 2,
y: canvas.height / 2,
})
}
}
centerPuzzle()
const evts = EventAdapter(canvas, window, viewport, MODE)
const evts = EventAdapter(canvas, window, viewport)
const previewImageUrl = Game.getImageUrl(gameId)
@ -289,8 +352,8 @@ export async function main(
}
updateTimerElements()
HUD.setPiecesDone(Game.getFinishedPiecesCount(gameId))
HUD.setPiecesTotal(Game.getPieceCount(gameId))
HUD.setPiecesDone(Game.getFinishedTileCount(gameId))
HUD.setPiecesTotal(Game.getTileCount(gameId))
const ts = TIME()
HUD.setActivePlayers(Game.getActivePlayers(gameId, ts))
HUD.setIdlePlayers(Game.getIdlePlayers(gameId, ts))
@ -299,42 +362,20 @@ export async function main(
let finished = longFinished
const justFinished = () => finished && !longFinished
const playerSoundVolume = (): number => {
return settings.getInt(SETTINGS.SOUND_VOLUME, 100)
}
const playerSoundEnabled = (): boolean => {
return settings.getBool(SETTINGS.SOUND_ENABLED, false)
}
const showPlayerNames = (): boolean => {
return settings.getBool(SETTINGS.SHOW_PLAYER_NAMES, true)
}
const playClick = () => {
const vol = playerSoundVolume()
clickAudio.volume = vol / 100
clickAudio.play()
}
const playerBgColor = () => {
if (MODE === MODE_REPLAY) {
return settings.getStr(SETTINGS.COLOR_BACKGROUND, '#222222')
}
return Game.getPlayerBgColor(gameId, clientId)
|| settings.getStr(SETTINGS.COLOR_BACKGROUND, '#222222')
return (Game.getPlayerBgColor(gameId, clientId)
|| localStorage.getItem('bg_color')
|| '#222222')
}
const playerColor = () => {
if (MODE === MODE_REPLAY) {
return settings.getStr(SETTINGS.PLAYER_COLOR, '#ffffff')
}
return Game.getPlayerColor(gameId, clientId)
|| settings.getStr(SETTINGS.PLAYER_COLOR, '#ffffff')
return (Game.getPlayerColor(gameId, clientId)
|| localStorage.getItem('player_color')
|| '#ffffff')
}
const playerName = () => {
if (MODE === MODE_REPLAY) {
return settings.getStr(SETTINGS.PLAYER_NAME, 'anon')
}
return Game.getPlayerName(gameId, clientId)
|| settings.getStr(SETTINGS.PLAYER_NAME, 'anon')
return (Game.getPlayerName(gameId, clientId)
|| localStorage.getItem('player_name')
|| 'anon')
}
let cursorDown: string = ''
@ -378,35 +419,14 @@ export async function main(
doSetSpeedStatus()
}
const intervals: NodeJS.Timeout[] = []
let to: NodeJS.Timeout
const clearIntervals = () => {
intervals.forEach(inter => {
clearInterval(inter)
})
if (to) {
clearTimeout(to)
}
}
let gameLoopInstance: GameLoopInstance
const unload = () => {
clearIntervals()
if (gameLoopInstance) {
gameLoopInstance.stop()
}
}
if (MODE === MODE_PLAY) {
intervals.push(setInterval(() => {
updateTimerElements()
}, 1000))
setInterval(updateTimerElements, 1000)
} else if (MODE === MODE_REPLAY) {
doSetSpeedStatus()
}
if (MODE === MODE_PLAY) {
Communication.onServerChange((msg: ServerEvent) => {
Communication.onServerChange((msg) => {
const msgType = msg[0]
const evClientId = msg[1]
const evClientSeq = msg[2]
@ -421,8 +441,8 @@ export async function main(
}
} break;
case Protocol.CHANGE_TILE: {
const t = Util.decodePiece(changeData)
Game.setPiece(gameId, t.idx, t)
const t = Util.decodeTile(changeData)
Game.setTile(gameId, t.idx, t)
RERENDER = true
} break;
case Protocol.CHANGE_DATA: {
@ -434,91 +454,64 @@ export async function main(
finished = !! Game.getFinishTs(gameId)
})
} else if (MODE === MODE_REPLAY) {
const handleLogEntry = (logEntry: any[], ts: Timestamp) => {
const entry = logEntry
if (entry[0] === Protocol.LOG_ADD_PLAYER) {
const playerId = entry[1]
Game.addPlayer(gameId, playerId, ts)
return true
}
if (entry[0] === Protocol.LOG_UPDATE_PLAYER) {
const playerId = Game.getPlayerIdByIndex(gameId, entry[1])
if (!playerId) {
throw '[ 2021-05-17 player not found (update player) ]'
}
Game.addPlayer(gameId, playerId, ts)
return true
}
if (entry[0] === Protocol.LOG_HANDLE_INPUT) {
const playerId = Game.getPlayerIdByIndex(gameId, entry[1])
if (!playerId) {
throw '[ 2021-05-17 player not found (handle input) ]'
}
const input = entry[2]
Game.handleInput(gameId, playerId, input, ts)
return true
}
return false
}
let GAME_TS = REPLAY.lastGameTs
const next = async () => {
if (REPLAY.logPointer + 1 >= REPLAY.log.length) {
await queryNextReplayBatch(gameId)
}
// no external communication for replay mode,
// only the REPLAY.log is relevant
let inter = setInterval(() => {
const realTs = Time.timestamp()
if (REPLAY.paused) {
REPLAY.lastRealTs = realTs
to = setTimeout(next, 50)
return
}
const timePassedReal = realTs - REPLAY.lastRealTs
const timePassedGame = timePassedReal * REPLAY.speeds[REPLAY.speedIdx]
let maxGameTs = REPLAY.lastGameTs + timePassedGame
const maxGameTs = REPLAY.lastGameTs + timePassedGame
do {
if (REPLAY.paused) {
break
}
const nextIdx = REPLAY.logPointer + 1
const nextIdx = REPLAY.logIdx + 1
if (nextIdx >= REPLAY.log.length) {
clearInterval(inter)
break
}
const currLogEntry = REPLAY.log[REPLAY.logPointer]
const currTs: Timestamp = GAME_TS + currLogEntry[currLogEntry.length - 1]
const nextLogEntry = REPLAY.log[nextIdx]
const diffToNext = nextLogEntry[nextLogEntry.length - 1]
const nextTs: Timestamp = currTs + diffToNext
const logEntry = REPLAY.log[nextIdx]
const nextTs = REPLAY.gameStartTs + logEntry[logEntry.length - 1]
if (nextTs > maxGameTs) {
// next log entry is too far into the future
if (REPLAY.skipNonActionPhases && (maxGameTs + 500 * Time.MS < nextTs)) {
maxGameTs += diffToNext
}
break
}
GAME_TS = currTs
if (handleLogEntry(nextLogEntry, nextTs)) {
const entryWithTs = logEntry.slice()
if (entryWithTs[0] === Protocol.LOG_ADD_PLAYER) {
const playerId = entryWithTs[1]
Game.addPlayer(gameId, playerId, nextTs)
RERENDER = true
} else if (entryWithTs[0] === Protocol.LOG_UPDATE_PLAYER) {
const playerId = Game.getPlayerIdByIndex(gameId, entryWithTs[1])
if (!playerId) {
throw '[ 2021-05-17 player not found (update player) ]'
}
Game.addPlayer(gameId, playerId, nextTs)
RERENDER = true
} else if (entryWithTs[0] === Protocol.LOG_HANDLE_INPUT) {
const playerId = Game.getPlayerIdByIndex(gameId, entryWithTs[1])
if (!playerId) {
throw '[ 2021-05-17 player not found (handle input) ]'
}
const input = entryWithTs[2]
Game.handleInput(gameId, playerId, input, nextTs)
RERENDER = true
}
REPLAY.logPointer = nextIdx
REPLAY.logIdx = nextIdx
} while (true)
REPLAY.lastRealTs = realTs
REPLAY.lastGameTs = maxGameTs
updateTimerElements()
if (!REPLAY.final) {
to = setTimeout(next, 50)
}
}
next()
}, 50)
}
let _last_mouse_down: Point|null = null
const onUpdate = (): void => {
const onUpdate = () => {
// handle key downs once per onUpdate
// this will create Protocol.INPUT_EV_MOVE events if something
// relevant is pressed
@ -530,13 +523,12 @@ export async function main(
// -------------------------------------------------------------
const type = evt[0]
if (type === Protocol.INPUT_EV_MOVE) {
const w = evt[1]
const h = evt[2]
const dim = viewport.worldDimToViewport({w, h})
const diffX = evt[1]
const diffY = evt[2]
RERENDER = true
viewport.move(dim.w, dim.h)
viewport.move(diffX, diffY)
} else if (type === Protocol.INPUT_EV_MOUSE_MOVE) {
if (_last_mouse_down && !Game.getFirstOwnedPiece(gameId, clientId)) {
if (_last_mouse_down && !Game.getFirstOwnedTile(gameId, clientId)) {
// move the cam
const pos = { x: evt[1], y: evt[2] }
const mouse = viewport.worldToViewport(pos)
@ -566,34 +558,12 @@ export async function main(
viewport.zoom('out', viewport.worldToViewport(pos))
} else if (type === Protocol.INPUT_EV_TOGGLE_PREVIEW) {
HUD.togglePreview()
} else if (type === Protocol.INPUT_EV_TOGGLE_SOUNDS) {
HUD.toggleSoundsEnabled()
} else if (type === Protocol.INPUT_EV_TOGGLE_PLAYER_NAMES) {
HUD.togglePlayerNames()
} else if (type === Protocol.INPUT_EV_CENTER_FIT_PUZZLE) {
centerPuzzle()
} else if (type === Protocol.INPUT_EV_TOGGLE_FIXED_PIECES) {
PIECE_VIEW_FIXED = !PIECE_VIEW_FIXED
RERENDER = true
} else if (type === Protocol.INPUT_EV_TOGGLE_LOOSE_PIECES) {
PIECE_VIEW_LOOSE = !PIECE_VIEW_LOOSE
RERENDER = true
}
// LOCAL + SERVER CHANGES
// -------------------------------------------------------------
const ts = TIME()
const changes = Game.handleInput(
gameId,
clientId,
evt,
ts,
(playerId: string) => {
if (playerSoundEnabled()) {
playClick()
}
}
)
const changes = Game.handleInput(gameId, clientId, evt, ts)
if (changes.length > 0) {
RERENDER = true
}
@ -602,13 +572,7 @@ export async function main(
// LOCAL ONLY CHANGES
// -------------------------------------------------------------
const type = evt[0]
if (type === Protocol.INPUT_EV_REPLAY_TOGGLE_PAUSE) {
replayOnPauseToggle()
} else if (type === Protocol.INPUT_EV_REPLAY_SPEED_DOWN) {
replayOnSpeedDown()
} else if (type === Protocol.INPUT_EV_REPLAY_SPEED_UP) {
replayOnSpeedUp()
} else if (type === Protocol.INPUT_EV_MOVE) {
if (type === Protocol.INPUT_EV_MOVE) {
const diffX = evt[1]
const diffY = evt[2]
RERENDER = true
@ -625,15 +589,11 @@ export async function main(
_last_mouse_down = mouse
}
} else if (type === Protocol.INPUT_EV_PLAYER_COLOR) {
updatePlayerCursorColor(evt[1])
} else if (type === Protocol.INPUT_EV_MOUSE_DOWN) {
const pos = { x: evt[1], y: evt[2] }
_last_mouse_down = viewport.worldToViewport(pos)
updatePlayerCursorState(true)
} else if (type === Protocol.INPUT_EV_MOUSE_UP) {
_last_mouse_down = null
updatePlayerCursorState(false)
} else if (type === Protocol.INPUT_EV_ZOOM_IN) {
const pos = { x: evt[1], y: evt[2] }
RERENDER = true
@ -644,18 +604,6 @@ export async function main(
viewport.zoom('out', viewport.worldToViewport(pos))
} else if (type === Protocol.INPUT_EV_TOGGLE_PREVIEW) {
HUD.togglePreview()
} else if (type === Protocol.INPUT_EV_TOGGLE_SOUNDS) {
HUD.toggleSoundsEnabled()
} else if (type === Protocol.INPUT_EV_TOGGLE_PLAYER_NAMES) {
HUD.togglePlayerNames()
} else if (type === Protocol.INPUT_EV_CENTER_FIT_PUZZLE) {
centerPuzzle()
} else if (type === Protocol.INPUT_EV_TOGGLE_FIXED_PIECES) {
PIECE_VIEW_FIXED = !PIECE_VIEW_FIXED
RERENDER = true
} else if (type === Protocol.INPUT_EV_TOGGLE_LOOSE_PIECES) {
PIECE_VIEW_LOOSE = !PIECE_VIEW_LOOSE
RERENDER = true
}
}
}
@ -667,7 +615,7 @@ export async function main(
}
}
const onRender = async (): Promise<void> => {
const onRender = async () => {
if (!RERENDER) {
return
}
@ -700,7 +648,7 @@ export async function main(
// DRAW TILES
// ---------------------------------------------------------------
const tiles = Game.getPiecesSortedByZIndex(gameId)
const tiles = Game.getTilesSortedByZIndex(gameId)
if (window.DEBUG) Debug.checkpoint('get tiles done')
dim = viewport.worldDimToViewportRaw(PIECE_DIM)
@ -710,8 +658,8 @@ export async function main(
}
bmp = bitmaps[tile.idx]
pos = viewport.worldToViewportRaw({
x: PIECE_DRAW_OFFSET + tile.pos.x,
y: PIECE_DRAW_OFFSET + tile.pos.y,
x: TILE_DRAW_OFFSET + tile.pos.x,
y: TILE_DRAW_OFFSET + tile.pos.y,
})
ctx.drawImage(bmp,
0, 0, bmp.width, bmp.height,
@ -731,12 +679,10 @@ export async function main(
bmp = await getPlayerCursor(p)
pos = viewport.worldToViewport(p)
ctx.drawImage(bmp, pos.x - CURSOR_W_2, pos.y - CURSOR_H_2)
if (showPlayerNames()) {
// performance:
// not drawing text directly here, to have less ctx
// switches between drawImage and fillTxt
texts.push([`${p.name} (${p.points})`, pos.x, pos.y + CURSOR_H])
}
// performance:
// not drawing text directly here, to have less ctx
// switches between drawImage and fillTxt
texts.push([`${p.name} (${p.points})`, pos.x, pos.y + CURSOR_H])
}
}
@ -753,7 +699,7 @@ export async function main(
// ---------------------------------------------------------------
HUD.setActivePlayers(Game.getActivePlayers(gameId, ts))
HUD.setIdlePlayers(Game.getIdlePlayers(gameId, ts))
HUD.setPiecesDone(Game.getFinishedPiecesCount(gameId))
HUD.setPiecesDone(Game.getFinishedTileCount(gameId))
if (window.DEBUG) Debug.checkpoint('HUD done')
// ---------------------------------------------------------------
@ -764,7 +710,7 @@ export async function main(
RERENDER = false
}
gameLoopInstance = run({
run({
update: onUpdate,
render: onRender,
})
@ -774,27 +720,17 @@ export async function main(
evts.setHotkeys(state)
},
onBgChange: (value: string) => {
settings.setStr(SETTINGS.COLOR_BACKGROUND, value)
localStorage.setItem('bg_color', value)
evts.addEvent([Protocol.INPUT_EV_BG_COLOR, value])
},
onColorChange: (value: string) => {
settings.setStr(SETTINGS.PLAYER_COLOR, value)
localStorage.setItem('player_color', value)
evts.addEvent([Protocol.INPUT_EV_PLAYER_COLOR, value])
},
onNameChange: (value: string) => {
settings.setStr(SETTINGS.PLAYER_NAME, value)
localStorage.setItem('player_name', value)
evts.addEvent([Protocol.INPUT_EV_PLAYER_NAME, value])
},
onSoundsEnabledChange: (value: boolean) => {
settings.setBool(SETTINGS.SOUND_ENABLED, value)
},
onSoundsVolumeChange: (value: number) => {
settings.setInt(SETTINGS.SOUND_VOLUME, value)
playClick()
},
onShowPlayerNamesChange: (value: boolean) => {
settings.setBool(SETTINGS.SHOW_PLAYER_NAMES, value)
},
replayOnSpeedUp,
replayOnSpeedDown,
replayOnPauseToggle,
@ -803,13 +739,8 @@ export async function main(
background: playerBgColor(),
color: playerColor(),
name: playerName(),
soundsEnabled: playerSoundEnabled(),
soundsVolume: playerSoundVolume(),
showPlayerNames: showPlayerNames(),
},
game: Game.get(gameId),
disconnect: Communication.disconnect,
connect: connect,
unload: unload,
}
}

View file

@ -3,20 +3,10 @@
interface GameLoopOptions {
fps?: number
slow?: number
update: (step: number) => void
render: (passed: number) => void
update: (step: number) => any
render: (passed: number) => any
}
export interface GameLoopInstance {
stop: () => void
}
export const run = (options: GameLoopOptions): GameLoopInstance => {
let stopped = false
const stop = () => {
stopped = true
}
export const run = (options: GameLoopOptions) => {
const fps = options.fps || 60
const slow = options.slow || 1
const update = options.update
@ -38,15 +28,10 @@ export const run = (options: GameLoopOptions): GameLoopInstance => {
}
render(dt / slow)
last = now
if (!stopped) {
raf(frame)
}
raf(frame)
}
raf(frame)
return {
stop,
}
}
export default {

View file

@ -7,36 +7,19 @@ import NewGame from './views/NewGame.vue'
import Game from './views/Game.vue'
import Replay from './views/Replay.vue'
import Util from './../common/Util'
import settings from './settings'
import xhr from './xhr'
(async () => {
function initClientSecret() {
let SECRET = settings.getStr('SECRET', '')
if (!SECRET) {
SECRET = Util.uniqId()
settings.setStr('SECRET', SECRET)
}
return SECRET
}
function initClientId() {
let ID = settings.getStr('ID', '')
const res = await fetch(`/api/conf`)
const conf = await res.json()
function initme() {
let ID = localStorage.getItem('ID')
if (!ID) {
ID = Util.uniqId()
settings.setStr('ID', ID)
localStorage.setItem('ID', ID)
}
return ID
}
const clientId = initClientId()
const clientSecret = initClientSecret()
xhr.setClientId(clientId)
xhr.setClientSecret(clientSecret)
const meRes = await xhr.get(`/api/me`, {})
const me = await meRes.json()
const confRes = await xhr.get(`/api/conf`, {})
const conf = await confRes.json()
const router = VueRouter.createRouter({
history: VueRouter.createWebHashHistory(),
@ -56,9 +39,8 @@ import xhr from './xhr'
})
const app = Vue.createApp(App)
app.config.globalProperties.$me = me
app.config.globalProperties.$config = conf
app.config.globalProperties.$clientId = clientId
app.config.globalProperties.$clientId = initme()
app.use(router)
app.mount('#app')
})()

View file

@ -1,66 +0,0 @@
/**
* Player settings
*/
export const SETTINGS = {
SOUND_VOLUME: 'sound_volume',
SOUND_ENABLED: 'sound_enabled',
COLOR_BACKGROUND: 'bg_color',
PLAYER_COLOR: 'player_color',
PLAYER_NAME: 'player_name',
SHOW_PLAYER_NAMES: 'show_player_names',
}
const set = (setting: string, value: string): void => {
localStorage.setItem(setting, value)
}
const get = (setting: string): any => {
return localStorage.getItem(setting)
}
const setInt = (setting: string, val: number): void => {
set(setting, `${val}`)
}
const getInt = (setting: string, def: number): number => {
const value = get(setting)
if (value === null) {
return def
}
const vol = parseInt(value, 10)
return isNaN(vol) ? def : vol
}
const setBool = (setting: string, val: boolean): void => {
set(setting, val ? '1' : '0')
}
const getBool = (setting: string, def: boolean): boolean => {
const value = get(setting)
if (value === null) {
return def
}
return value === '1'
}
const setStr = (setting: string, val: string): void => {
set(setting, val)
}
const getStr = (setting: string, def: string): string => {
const value = get(setting)
if (value === null) {
return def
}
return value
}
export default {
setInt,
getInt,
setBool,
getBool,
setStr,
getStr,
}

View file

@ -127,7 +127,7 @@ input:focus {
}
.overlay {
position: fixed;
position: absolute;
top: 0;
left: 0;
right: 0;

View file

@ -2,15 +2,8 @@
<div id="game">
<settings-overlay v-show="overlay === 'settings'" @bgclick="toggle('settings', true)" v-model="g.player" />
<preview-overlay v-show="overlay === 'preview'" @bgclick="toggle('preview', false)" :img="g.previewImageUrl" />
<info-overlay v-if="g.game" v-show="overlay === 'info'" @bgclick="toggle('info', true)" :game="g.game" />
<help-overlay v-show="overlay === 'help'" @bgclick="toggle('help', true)" />
<div class="overlay" v-if="cuttingPuzzle">
<div class="overlay-content">
<div> Cutting puzzle, please wait... </div>
</div>
</div>
<connection-overlay
:connectionState="connectionState"
@reconnect="reconnect"
@ -28,8 +21,7 @@
<router-link class="opener" :to="{name: 'index'}" target="_blank">🧩 Puzzles</router-link>
<div class="opener" @click="toggle('preview', false)">🖼 Preview</div>
<div class="opener" @click="toggle('settings', true)">🛠 Settings</div>
<div class="opener" @click="toggle('info', true)"> Info</div>
<div class="opener" @click="toggle('help', true)"> Hotkeys</div>
<div class="opener" @click="toggle('help', true)"> Help</div>
</div>
</div>
@ -43,12 +35,11 @@ import Scores from './../components/Scores.vue'
import PuzzleStatus from './../components/PuzzleStatus.vue'
import SettingsOverlay from './../components/SettingsOverlay.vue'
import PreviewOverlay from './../components/PreviewOverlay.vue'
import InfoOverlay from './../components/InfoOverlay.vue'
import ConnectionOverlay from './../components/ConnectionOverlay.vue'
import HelpOverlay from './../components/HelpOverlay.vue'
import { main, MODE_PLAY } from './../game'
import { Game, Player } from '../../common/Types'
import { Player } from '../../common/GameCommon'
export default defineComponent({
name: 'game',
@ -57,7 +48,6 @@ export default defineComponent({
Scores,
SettingsOverlay,
PreviewOverlay,
InfoOverlay,
ConnectionOverlay,
HelpOverlay,
},
@ -74,29 +64,20 @@ export default defineComponent({
overlay: '',
connectionState: 0,
cuttingPuzzle: true,
g: {
player: {
background: '',
color: '',
name: '',
soundsEnabled: false,
soundsVolume: 100,
showPlayerNames: true,
},
game: null as Game|null,
previewImageUrl: '',
setHotkeys: (v: boolean) => {},
onBgChange: (v: string) => {},
onColorChange: (v: string) => {},
onNameChange: (v: string) => {},
onSoundsEnabledChange: (v: boolean) => {},
onSoundsVolumeChange: (v: number) => {},
onShowPlayerNamesChange: (v: boolean) => {},
connect: () => {},
disconnect: () => {},
unload: () => {},
connect: () => {},
},
}
},
@ -113,15 +94,6 @@ export default defineComponent({
this.$watch(() => this.g.player.name, (value: string) => {
this.g.onNameChange(value)
})
this.$watch(() => this.g.player.soundsEnabled, (value: boolean) => {
this.g.onSoundsEnabledChange(value)
})
this.$watch(() => this.g.player.soundsVolume, (value: number) => {
this.g.onSoundsVolumeChange(value)
})
this.$watch(() => this.g.player.showPlayerNames, (value: boolean) => {
this.g.onShowPlayerNamesChange(value)
})
this.g = await main(
`${this.$route.params.id}`,
// @ts-ignore
@ -131,22 +103,18 @@ export default defineComponent({
MODE_PLAY,
this.$el,
{
setPuzzleCut: () => { this.cuttingPuzzle = false },
setActivePlayers: (v: Array<Player>) => { this.activePlayers = v },
setIdlePlayers: (v: Array<Player>) => { this.idlePlayers = v },
setFinished: (v: boolean) => { this.finished = v },
setDuration: (v: number) => { this.duration = v },
setPiecesDone: (v: number) => { this.piecesDone = v },
setPiecesTotal: (v: number) => { this.piecesTotal = v },
togglePreview: () => { this.toggle('preview', false) },
setConnectionState: (v: number) => { this.connectionState = v },
toggleSoundsEnabled: () => { this.g.player.soundsEnabled = !this.g.player.soundsEnabled },
togglePlayerNames: () => { this.g.player.showPlayerNames = !this.g.player.showPlayerNames },
togglePreview: () => { this.toggle('preview', false) },
}
)
},
unmounted () {
this.g.unload()
this.g.disconnect()
},
methods: {

View file

@ -13,7 +13,6 @@
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import xhr from '../xhr'
import GameTeaser from './../components/GameTeaser.vue'
@ -28,7 +27,7 @@ export default defineComponent({
}
},
async created() {
const res = await xhr.get('/api/index-data', {})
const res = await fetch('/api/index-data')
const json = await res.json()
this.gamesRunning = json.gamesRunning
this.gamesFinished = json.gamesFinished

View file

@ -15,12 +15,7 @@ in jigsawpuzzles.io
<div>
<label v-if="tags.length > 0">
Tags:
<span
class="bit"
v-for="(t,idx) in relevantTags"
:key="idx"
@click="toggleTag(t)"
:class="{on: filters.tags.includes(t.slug)}">{{t.title}} ({{t.total}})</span>
<span class="bit" v-for="(t,idx) in tags" :key="idx" @click="toggleTag(t)" :class="{on: filters.tags.includes(t.slug)}">{{t.title}}</span>
<!-- <select v-model="filters.tags" @change="filtersChanged">
<option value="">All</option>
<option v-for="(c, idx) in tags" :key="idx" :value="c.slug">{{c.title}}</option>
@ -36,30 +31,10 @@ in jigsawpuzzles.io
</select>
</label>
</div>
<image-library
:images="images"
@imageClicked="onImageClicked"
@imageEditClicked="onImageEditClicked" />
<new-image-dialog
v-if="dialog==='new-image'"
:autocompleteTags="autocompleteTags"
@bgclick="dialog=''"
:uploadProgress="uploadProgress"
:uploading="uploading"
@postToGalleryClick="postToGalleryClick"
@setupGameClick="setupGameClick"
/>
<edit-image-dialog
v-if="dialog==='edit-image'"
:autocompleteTags="autocompleteTags"
@bgclick="dialog=''"
@saveClick="onSaveImageClick"
:image="image" />
<new-game-dialog
v-if="image && dialog==='new-game'"
@bgclick="dialog=''"
@newGame="onNewGame"
:image="image" />
<image-library :images="images" @imageClicked="onImageClicked" @imageEditClicked="onImageEditClicked" />
<new-image-dialog v-if="dialog==='new-image'" @bgclick="dialog=''" @postToGalleryClick="postToGalleryClick" @setupGameClick="setupGameClick" />
<edit-image-dialog v-if="dialog==='edit-image'" @bgclick="dialog=''" @saveClick="onSaveImageClick" :image="image" />
<new-game-dialog v-if="image && dialog==='new-game'" @bgclick="dialog=''" @newGame="onNewGame" :image="image" />
</div>
</template>
@ -70,9 +45,8 @@ import ImageLibrary from './../components/ImageLibrary.vue'
import NewImageDialog from './../components/NewImageDialog.vue'
import EditImageDialog from './../components/EditImageDialog.vue'
import NewGameDialog from './../components/NewGameDialog.vue'
import { GameSettings, Image, Tag } from '../../common/Types'
import { GameSettings, Image, Tag } from '../../common/GameCommon'
import Util from '../../common/Util'
import xhr from '../xhr'
export default defineComponent({
components: {
@ -88,7 +62,7 @@ export default defineComponent({
tags: [] as string[],
},
images: [],
tags: [] as Tag[],
tags: [],
image: {
id: 0,
@ -101,29 +75,12 @@ export default defineComponent({
} as Image,
dialog: '',
uploading: '',
uploadProgress: 0,
}
},
async created() {
await this.loadImages()
},
computed: {
relevantTags (): Tag[] {
return this.tags.filter((tag: Tag) => tag.total > 0)
},
},
methods: {
autocompleteTags (input: string, exclude: string[]): string[] {
return this.tags
.filter((tag: Tag) => {
return !exclude.includes(tag.title)
&& tag.title.toLowerCase().startsWith(input.toLowerCase())
})
.slice(0, 10)
.map((tag: Tag) => tag.title)
},
toggleTag (t: Tag) {
if (this.filters.tags.includes(t.slug)) {
this.filters.tags = this.filters.tags.filter(slug => slug !== t.slug)
@ -133,7 +90,7 @@ export default defineComponent({
this.filtersChanged()
},
async loadImages () {
const res = await xhr.get(`/api/newgame-data${Util.asQueryArgs(this.filters)}`, {})
const res = await fetch(`/api/newgame-data${Util.asQueryArgs(this.filters)}`)
const json = await res.json()
this.images = json.images
this.tags = json.tags
@ -150,22 +107,20 @@ export default defineComponent({
this.dialog = 'edit-image'
},
async uploadImage (data: any) {
this.uploadProgress = 0
const formData = new FormData();
formData.append('file', data.file, data.file.name);
formData.append('title', data.title)
formData.append('tags', data.tags)
const res = await xhr.post('/api/upload', {
const res = await fetch('/api/upload', {
method: 'post',
body: formData,
onUploadProgress: (evt: ProgressEvent<XMLHttpRequestEventTarget>): void => {
this.uploadProgress = evt.loaded / evt.total
},
})
this.uploadProgress = 1
return await res.json()
},
async saveImage (data: any) {
const res = await xhr.post('/api/save-image', {
const res = await fetch('/api/save-image', {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
@ -179,31 +134,24 @@ export default defineComponent({
return await res.json()
},
async onSaveImageClick(data: any) {
const res = await this.saveImage(data)
if (res.ok) {
this.dialog = ''
await this.loadImages()
} else {
alert(res.error)
}
await this.saveImage(data)
this.dialog = ''
await this.loadImages()
},
async postToGalleryClick(data: any) {
this.uploading = 'postToGallery'
await this.uploadImage(data)
this.uploading = ''
this.dialog = ''
await this.loadImages()
},
async setupGameClick (data: any) {
this.uploading = 'setupGame'
const image = await this.uploadImage(data)
this.uploading = ''
this.loadImages() // load images in background
this.image = image
this.dialog = 'new-game'
},
async onNewGame(gameSettings: GameSettings) {
const res = await xhr.post('/api/newgame', {
const res = await fetch('/newgame', {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'

View file

@ -2,15 +2,8 @@
<div id="replay">
<settings-overlay v-show="overlay === 'settings'" @bgclick="toggle('settings', true)" v-model="g.player" />
<preview-overlay v-show="overlay === 'preview'" @bgclick="toggle('preview', false)" :img="g.previewImageUrl" />
<info-overlay v-if="g.game" v-show="overlay === 'info'" @bgclick="toggle('info', true)" :game="g.game" />
<help-overlay v-show="overlay === 'help'" @bgclick="toggle('help', true)" />
<div class="overlay" v-if="cuttingPuzzle">
<div class="overlay-content">
<div> Cutting puzzle, please wait... </div>
</div>
</div>
<puzzle-status
:finished="finished"
:duration="duration"
@ -30,8 +23,7 @@
<router-link class="opener" :to="{name: 'index'}" target="_blank">🧩 Puzzles</router-link>
<div class="opener" @click="toggle('preview', false)">🖼 Preview</div>
<div class="opener" @click="toggle('settings', true)">🛠 Settings</div>
<div class="opener" @click="toggle('info', true)"> Info</div>
<div class="opener" @click="toggle('help', true)"> Hotkeys</div>
<div class="opener" @click="toggle('help', true)"> Help</div>
</div>
</div>
@ -45,11 +37,9 @@ import Scores from './../components/Scores.vue'
import PuzzleStatus from './../components/PuzzleStatus.vue'
import SettingsOverlay from './../components/SettingsOverlay.vue'
import PreviewOverlay from './../components/PreviewOverlay.vue'
import InfoOverlay from './../components/InfoOverlay.vue'
import HelpOverlay from './../components/HelpOverlay.vue'
import { main, MODE_REPLAY } from './../game'
import { Game, Player } from '../../common/Types'
export default defineComponent({
name: 'replay',
@ -58,13 +48,12 @@ export default defineComponent({
Scores,
SettingsOverlay,
PreviewOverlay,
InfoOverlay,
HelpOverlay,
},
data() {
return {
activePlayers: [] as Array<Player>,
idlePlayers: [] as Array<Player>,
activePlayers: [] as Array<any>,
idlePlayers: [] as Array<any>,
finished: false,
duration: 0,
@ -74,32 +63,22 @@ export default defineComponent({
overlay: '',
connectionState: 0,
cuttingPuzzle: true,
g: {
player: {
background: '',
color: '',
name: '',
soundsEnabled: false,
soundsVolume: 100,
showPlayerNames: true,
},
game: null as Game|null,
previewImageUrl: '',
setHotkeys: (v: boolean) => {},
onBgChange: (v: string) => {},
onColorChange: (v: string) => {},
onNameChange: (v: string) => {},
onSoundsEnabledChange: (v: boolean) => {},
onSoundsVolumeChange: (v: number) => {},
onShowPlayerNamesChange: (v: boolean) => {},
replayOnSpeedUp: () => {},
replayOnSpeedDown: () => {},
replayOnPauseToggle: () => {},
connect: () => {},
disconnect: () => {},
unload: () => {},
},
replay: {
@ -121,15 +100,6 @@ export default defineComponent({
this.$watch(() => this.g.player.name, (value: string) => {
this.g.onNameChange(value)
})
this.$watch(() => this.g.player.soundsEnabled, (value: boolean) => {
this.g.onSoundsEnabledChange(value)
})
this.$watch(() => this.g.player.soundsVolume, (value: number) => {
this.g.onSoundsVolumeChange(value)
})
this.$watch(() => this.g.player.showPlayerNames, (value: boolean) => {
this.g.onShowPlayerNamesChange(value)
})
this.g = await main(
`${this.$route.params.id}`,
// @ts-ignore
@ -139,9 +109,8 @@ export default defineComponent({
MODE_REPLAY,
this.$el,
{
setPuzzleCut: () => { this.cuttingPuzzle = false },
setActivePlayers: (v: Array<Player>) => { this.activePlayers = v },
setIdlePlayers: (v: Array<Player>) => { this.idlePlayers = v },
setActivePlayers: (v: Array<any>) => { this.activePlayers = v },
setIdlePlayers: (v: Array<any>) => { this.idlePlayers = v },
setFinished: (v: boolean) => { this.finished = v },
setDuration: (v: number) => { this.duration = v },
setPiecesDone: (v: number) => { this.piecesDone = v },
@ -150,13 +119,10 @@ export default defineComponent({
setConnectionState: (v: number) => { this.connectionState = v },
setReplaySpeed: (v: number) => { this.replay.speed = v },
setReplayPaused: (v: boolean) => { this.replay.paused = v },
toggleSoundsEnabled: () => { this.g.player.soundsEnabled = !this.g.player.soundsEnabled },
togglePlayerNames: () => { this.g.player.showPlayerNames = !this.g.player.showPlayerNames },
}
)
},
unmounted () {
this.g.unload()
this.g.disconnect()
},
methods: {

View file

@ -1,68 +0,0 @@
export interface Response {
status: number,
text: string,
json: () => Promise<any>,
}
export interface Options {
body: FormData|string,
headers?: any,
onUploadProgress?: (ev: ProgressEvent<XMLHttpRequestEventTarget>) => any,
}
let xhrClientId: string = ''
let xhrClientSecret: string = ''
const request = async (
method: string,
url: string,
options: Options
): Promise<Response> => {
return new Promise((resolve, reject) => {
const xhr = new window.XMLHttpRequest()
xhr.open(method, url, true)
xhr.withCredentials = true
for (const k in options.headers || {}) {
xhr.setRequestHeader(k, options.headers[k])
}
xhr.setRequestHeader('Client-Id', xhrClientId)
xhr.setRequestHeader('Client-Secret', xhrClientSecret)
xhr.addEventListener('load', function (ev: ProgressEvent<XMLHttpRequestEventTarget>
) {
resolve({
status: this.status,
text: this.responseText,
json: async () => JSON.parse(this.responseText),
})
})
xhr.addEventListener('error', function (ev: ProgressEvent<XMLHttpRequestEventTarget>) {
reject(new Error('xhr error'))
})
if (xhr.upload && options.onUploadProgress) {
xhr.upload.addEventListener('progress', function (ev: ProgressEvent<XMLHttpRequestEventTarget>) {
// typescript complains without this extra check
if (options.onUploadProgress) {
options.onUploadProgress(ev)
}
})
}
xhr.send(options.body || null)
})
}
export default {
request,
get: (url: string, options: any): Promise<Response> => {
return request('get', url, options)
},
post: (url: string, options: any): Promise<Response> => {
return request('post', url, options)
},
setClientId: (clientId: string): void => {
xhrClientId = clientId
},
setClientSecret: (clientSecret: string): void => {
xhrClientSecret = clientSecret
},
}

View file

@ -5,6 +5,10 @@ import { logger } from '../common/Util'
const log = logger('Db.ts')
// assume 32766 SQLITE_MAX_VARIABLE_NUMBER
// @see https://sqlite.org/limits.html
const SQLITE_MAX_VARIABLE_NUMBER = 32766
/**
* TODO: create a more specific type for OrderBy.
* It looks like this (example):
@ -13,12 +17,11 @@ const log = logger('Db.ts')
* {name: 1}, // then by name ascending
* ]
*/
type OrderBy = Array<any>
type Data = Record<string, any>
type WhereRaw = Record<string, any>
type Params = Array<any>
export type WhereRaw = Record<string, any>
export type OrderBy = Array<Record<string, 1|-1>>
interface Where {
sql: string
values: Array<any>
@ -89,7 +92,7 @@ class Db {
let prop = '$nin'
if (where[k][prop]) {
if (where[k][prop].length > 0) {
wheres.push(k + ' NOT IN (' + where[k][prop].map(() => '?') + ')')
wheres.push(k + ' NOT IN (' + where[k][prop].map((_: any) => '?') + ')')
values.push(...where[k][prop])
}
continue
@ -97,7 +100,7 @@ class Db {
prop = '$in'
if (where[k][prop]) {
if (where[k][prop].length > 0) {
wheres.push(k + ' IN (' + where[k][prop].map(() => '?') + ')')
wheres.push(k + ' IN (' + where[k][prop].map((_: any) => '?') + ')')
values.push(...where[k][prop])
}
continue
@ -187,15 +190,57 @@ class Db {
return this.get(table, check)[idcol] // get id manually
}
/**
* Inserts data into table and returns the last insert id
*/
insert (table: string, data: Data): Integer.IntLike {
const keys = Object.keys(data)
const values = keys.map(k => data[k])
const sql = 'INSERT INTO '+ table
+ ' (' + keys.join(',') + ')'
+ ' VALUES (' + keys.map(() => '?').join(',') + ')'
+ ' VALUES (' + keys.map(k => '?').join(',') + ')'
return this.run(sql, values).lastInsertRowid
}
/**
* Inserts multiple datas into table. Returns the total number
* of changes.
*/
insertMany (table: string, datas: Data[]): number {
if (datas.length === 0) {
return 0
}
const keys = Object.keys(datas[0])
const runChunk = (vars: string[], values: any[]) => {
const sql = `INSERT INTO ${table}
(${keys.join(',')})
VALUES ${vars.join(',')}`
return this.run(sql, values).changes
}
let len: number = 0
let vars: string[] = []
let values: any[] = []
let changes = 0
for (const data of datas) {
if (len + keys.length > SQLITE_MAX_VARIABLE_NUMBER) {
changes += runChunk(vars, values)
len = 0
vars = []
values = []
}
len += keys.length
vars.push('(' + keys.map(_ => '?').join(',') + ')')
values.push(...keys.map(k => data[k]))
}
if (len > 0) {
changes += runChunk(vars, values)
}
return changes
}
update (table: string, data: Data, whereRaw: WhereRaw = {}): void {
const keys = Object.keys(data)
if (keys.length === 0) {

View file

@ -1,88 +1,53 @@
import GameCommon from './../common/GameCommon'
import { Change, Game, Input, ScoreMode, ShapeMode, SnapMode,ImageInfo, Timestamp, GameSettings } from './../common/Types'
import Util, { logger } from './../common/Util'
import GameCommon, { ScoreMode } from './../common/GameCommon'
import Util from './../common/Util'
import { Rng } from './../common/Rng'
import GameLog from './GameLog'
import { createPuzzle } from './Puzzle'
import Protocol from './../common/Protocol'
import GameStorage from './GameStorage'
const log = logger('Game.ts')
async function createGameObject(
gameId: string,
targetTiles: number,
image: ImageInfo,
ts: Timestamp,
scoreMode: ScoreMode,
shapeMode: ShapeMode,
snapMode: SnapMode,
creatorUserId: number|null
): Promise<Game> {
image: { file: string, url: string },
ts: number,
scoreMode: ScoreMode
) {
const seed = Util.hash(gameId + ' ' + ts)
const rng = new Rng(seed)
return {
id: gameId,
creatorUserId,
rng: { type: 'Rng', obj: rng },
puzzle: await createPuzzle(rng, targetTiles, image, ts, shapeMode),
puzzle: await createPuzzle(rng, targetTiles, image, ts),
players: [],
evtInfos: {},
scoreMode,
shapeMode,
snapMode,
}
}
async function createNewGame(
gameSettings: GameSettings,
ts: Timestamp,
creatorUserId: number
): Promise<string> {
let gameId;
do {
gameId = Util.uniqId()
} while (GameCommon.exists(gameId))
async function createGame(
gameId: string,
targetTiles: number,
image: { file: string, url: string },
ts: number,
scoreMode: ScoreMode
): Promise<void> {
const gameObject = await createGameObject(gameId, targetTiles, image, ts, scoreMode)
const gameObject = await createGameObject(
gameId,
gameSettings.tiles,
gameSettings.image,
ts,
gameSettings.scoreMode,
gameSettings.shapeMode,
gameSettings.snapMode,
creatorUserId
)
GameLog.create(gameId, ts)
GameLog.log(
gameId,
Protocol.LOG_HEADER,
1,
gameSettings.tiles,
gameSettings.image,
ts,
gameSettings.scoreMode,
gameSettings.shapeMode,
gameSettings.snapMode,
gameObject.creatorUserId
)
GameLog.create(gameId)
GameLog.log(gameId, Protocol.LOG_HEADER, 1, targetTiles, image, ts, scoreMode)
GameCommon.setGame(gameObject.id, gameObject)
GameStorage.setDirty(gameId)
return gameId
}
function addPlayer(gameId: string, playerId: string, ts: Timestamp): void {
if (GameLog.shouldLog(GameCommon.getFinishTs(gameId), ts)) {
const idx = GameCommon.getPlayerIndexById(gameId, playerId)
if (idx === -1) {
GameLog.log(gameId, Protocol.LOG_ADD_PLAYER, playerId, ts)
} else {
GameLog.log(gameId, Protocol.LOG_UPDATE_PLAYER, idx, ts)
}
function addPlayer(gameId: string, playerId: string, ts: number): void {
const idx = GameCommon.getPlayerIndexById(gameId, playerId)
const diff = ts - GameCommon.getStartTs(gameId)
if (idx === -1) {
GameLog.log(gameId, Protocol.LOG_ADD_PLAYER, playerId, diff)
} else {
GameLog.log(gameId, Protocol.LOG_UPDATE_PLAYER, idx, diff)
}
GameCommon.addPlayer(gameId, playerId, ts)
@ -92,13 +57,12 @@ function addPlayer(gameId: string, playerId: string, ts: Timestamp): void {
function handleInput(
gameId: string,
playerId: string,
input: Input,
ts: Timestamp
): Array<Change> {
if (GameLog.shouldLog(GameCommon.getFinishTs(gameId), ts)) {
const idx = GameCommon.getPlayerIndexById(gameId, playerId)
GameLog.log(gameId, Protocol.LOG_HANDLE_INPUT, idx, input, ts)
}
input: any,
ts: number
): Array<Array<any>> {
const idx = GameCommon.getPlayerIndexById(gameId, playerId)
const diff = ts - GameCommon.getStartTs(gameId)
GameLog.log(gameId, Protocol.LOG_HANDLE_INPUT, idx, input, diff)
const ret = GameCommon.handleInput(gameId, playerId, input, ts)
GameStorage.setDirty(gameId)
@ -107,7 +71,17 @@ function handleInput(
export default {
createGameObject,
createNewGame,
createGame,
addPlayer,
handleInput,
getAllGames: GameCommon.getAllGames,
getActivePlayers: GameCommon.getActivePlayers,
getFinishedTileCount: GameCommon.getFinishedTileCount,
getImageUrl: GameCommon.getImageUrl,
getTileCount: GameCommon.getTileCount,
exists: GameCommon.exists,
playerExists: GameCommon.playerExists,
get: GameCommon.get,
getStartTs: GameCommon.getStartTs,
getFinishTs: GameCommon.getFinishTs,
}

View file

@ -1,106 +1,51 @@
import fs from 'fs'
import Protocol from '../common/Protocol'
import Time from '../common/Time'
import { DefaultScoreMode, DefaultShapeMode, DefaultSnapMode, Timestamp } from '../common/Types'
import { logger } from './../common/Util'
import { DATA_DIR } from './../server/Dirs'
const log = logger('GameLog.js')
const LINES_PER_LOG_FILE = 10000
const POST_GAME_LOG_DURATION = 5 * Time.MIN
const filename = (gameId: string) => `${DATA_DIR}/log_${gameId}.log`
const shouldLog = (finishTs: Timestamp, currentTs: Timestamp): boolean => {
// when not finished yet, always log
if (!finishTs) {
return true
}
// in finished games, log max POST_GAME_LOG_DURATION after
// the game finished, to record winning dance moves etc :P
const timeSinceGameEnd = currentTs - finishTs
return timeSinceGameEnd <= POST_GAME_LOG_DURATION
}
export const filename = (gameId: string, offset: number) => `${DATA_DIR}/log_${gameId}-${offset}.log`
export const idxname = (gameId: string) => `${DATA_DIR}/log_${gameId}.idx.log`
const create = (gameId: string, ts: Timestamp): void => {
const idxfile = idxname(gameId)
if (!fs.existsSync(idxfile)) {
fs.appendFileSync(idxfile, JSON.stringify({
gameId: gameId,
total: 0,
lastTs: ts,
currentFile: '',
perFile: LINES_PER_LOG_FILE,
}))
const create = (gameId: string) => {
const file = filename(gameId)
if (!fs.existsSync(file)) {
fs.appendFileSync(file, '')
}
}
const exists = (gameId: string): boolean => {
const idxfile = idxname(gameId)
return fs.existsSync(idxfile)
const exists = (gameId: string) => {
const file = filename(gameId)
return fs.existsSync(file)
}
const _log = (gameId: string, type: number, ...args: Array<any>): void => {
const idxfile = idxname(gameId)
if (!fs.existsSync(idxfile)) {
const _log = (gameId: string, ...args: Array<any>) => {
const file = filename(gameId)
if (!fs.existsSync(file)) {
return
}
const idxObj = JSON.parse(fs.readFileSync(idxfile, 'utf-8'))
if (idxObj.total % idxObj.perFile === 0) {
idxObj.currentFile = filename(gameId, idxObj.total)
}
const tsIdx = type === Protocol.LOG_HEADER ? 3 : (args.length - 1)
const ts: Timestamp = args[tsIdx]
if (type !== Protocol.LOG_HEADER) {
// for everything but header save the diff to last log entry
args[tsIdx] = ts - idxObj.lastTs
}
const line = JSON.stringify([type, ...args]).slice(1, -1)
fs.appendFileSync(idxObj.currentFile, line + "\n")
idxObj.total++
idxObj.lastTs = ts
fs.writeFileSync(idxfile, JSON.stringify(idxObj))
const str = JSON.stringify(args)
fs.appendFileSync(file, str + "\n")
}
const get = (
gameId: string,
offset: number = 0,
): any[] => {
const idxfile = idxname(gameId)
if (!fs.existsSync(idxfile)) {
return []
}
const file = filename(gameId, offset)
const get = (gameId: string) => {
const file = filename(gameId)
if (!fs.existsSync(file)) {
return []
}
const lines = fs.readFileSync(file, 'utf-8').split("\n")
const log = lines.filter(line => !!line).map(line => {
return JSON.parse(`[${line}]`)
return lines.filter((line: string) => !!line).map((line: string) => {
try {
return JSON.parse(line)
} catch (e) {
log.log(line)
log.log(e)
}
})
if (offset === 0 && log.length > 0) {
log[0][5] = DefaultScoreMode(log[0][5])
log[0][6] = DefaultShapeMode(log[0][6])
log[0][7] = DefaultSnapMode(log[0][7])
log[0][8] = log[0][8] || null // creatorUserId
}
return log
}
export default {
shouldLog,
create,
exists,
log: _log,
get,
filename,
idxname,
}

View file

@ -1,88 +1,21 @@
import fs from 'fs'
import GameCommon from './../common/GameCommon'
import { DefaultScoreMode, DefaultShapeMode, DefaultSnapMode, Game, Piece } from './../common/Types'
import GameCommon, { Piece, ScoreMode } from './../common/GameCommon'
import Util, { logger } from './../common/Util'
import { Rng } from './../common/Rng'
import { DATA_DIR } from './Dirs'
import Time from './../common/Time'
import Db from './Db'
const log = logger('GameStorage.js')
const dirtyGames: Record<string, boolean> = {}
const DIRTY_GAMES = {} as any
function setDirty(gameId: string): void {
dirtyGames[gameId] = true
DIRTY_GAMES[gameId] = true
}
function setClean(gameId: string): void {
delete dirtyGames[gameId]
}
function loadGamesFromDb(db: Db): void {
const gameRows = db.getMany('games')
for (const gameRow of gameRows) {
loadGameFromDb(db, gameRow.id)
}
delete DIRTY_GAMES[gameId]
}
function loadGameFromDb(db: Db, gameId: string): void {
const gameRow = db.get('games', {id: gameId})
let game
try {
game = JSON.parse(gameRow.data)
} catch {
log.log(`[ERR] unable to load game from db ${gameId}`);
}
if (typeof game.puzzle.data.started === 'undefined') {
game.puzzle.data.started = gameRow.created
}
if (typeof game.puzzle.data.finished === 'undefined') {
game.puzzle.data.finished = gameRow.finished
}
if (!Array.isArray(game.players)) {
game.players = Object.values(game.players)
}
const gameObject: Game = storeDataToGame(game, game.creator_user_id)
GameCommon.setGame(gameObject.id, gameObject)
}
function persistGamesToDb(db: Db): void {
for (const gameId of Object.keys(dirtyGames)) {
persistGameToDb(db, gameId)
}
}
function persistGameToDb(db: Db, gameId: string): void {
const game: Game|null = GameCommon.get(gameId)
if (!game) {
log.error(`[ERROR] unable to persist non existing game ${gameId}`)
return
}
if (game.id in dirtyGames) {
setClean(game.id)
}
db.upsert('games', {
id: game.id,
creator_user_id: game.creatorUserId,
image_id: game.puzzle.info.image?.id,
created: game.puzzle.data.started,
finished: game.puzzle.data.finished,
data: gameToStoreData(game)
}, {
id: game.id,
})
log.info(`[INFO] persisted game ${game.id}`)
}
/**
* @deprecated
*/
function loadGamesFromDisk(): void {
function loadGames(): void {
const files = fs.readdirSync(DATA_DIR)
for (const f of files) {
const m = f.match(/^([a-z0-9]+)\.json$/)
@ -90,14 +23,11 @@ function loadGamesFromDisk(): void {
continue
}
const gameId = m[1]
loadGameFromDisk(gameId)
loadGame(gameId)
}
}
/**
* @deprecated
*/
function loadGameFromDisk(gameId: string): void {
function loadGame(gameId: string): void {
const file = `${DATA_DIR}/${gameId}.json`
const contents = fs.readFileSync(file, 'utf-8')
let game
@ -111,36 +41,39 @@ function loadGameFromDisk(gameId: string): void {
}
if (typeof game.puzzle.data.finished === 'undefined') {
const unfinished = game.puzzle.tiles
.map(Util.decodePiece)
.map(Util.decodeTile)
.find((t: Piece) => t.owner !== -1)
game.puzzle.data.finished = unfinished ? 0 : Time.timestamp()
}
if (!Array.isArray(game.players)) {
game.players = Object.values(game.players)
}
const gameObject: Game = storeDataToGame(game, null)
const gameObject = {
id: game.id,
rng: {
type: game.rng ? game.rng.type : '_fake_',
obj: game.rng ? Rng.unserialize(game.rng.obj) : new Rng(0),
},
puzzle: game.puzzle,
players: game.players,
evtInfos: {},
scoreMode: game.scoreMode || ScoreMode.FINAL,
}
GameCommon.setGame(gameObject.id, gameObject)
}
function storeDataToGame(storeData: any, creatorUserId: number|null): Game {
return {
id: storeData.id,
creatorUserId,
rng: {
type: storeData.rng ? storeData.rng.type : '_fake_',
obj: storeData.rng ? Rng.unserialize(storeData.rng.obj) : new Rng(0),
},
puzzle: storeData.puzzle,
players: storeData.players,
evtInfos: {},
scoreMode: DefaultScoreMode(storeData.scoreMode),
shapeMode: DefaultShapeMode(storeData.shapeMode),
snapMode: DefaultSnapMode(storeData.snapMode),
function persistGames(): void {
for (const gameId of Object.keys(DIRTY_GAMES)) {
persistGame(gameId)
}
}
function gameToStoreData(game: Game): string {
return JSON.stringify({
function persistGame(gameId: string): void {
const game = GameCommon.get(gameId)
if (game.id in DIRTY_GAMES) {
setClean(game.id)
}
fs.writeFileSync(`${DATA_DIR}/${game.id}.json`, JSON.stringify({
id: game.id,
rng: {
type: game.rng.type,
@ -149,20 +82,14 @@ function gameToStoreData(game: Game): string {
puzzle: game.puzzle,
players: game.players,
scoreMode: game.scoreMode,
shapeMode: game.shapeMode,
snapMode: game.snapMode,
});
}))
log.info(`[INFO] persisted game ${game.id}`)
}
export default {
// disk functions are deprecated
loadGamesFromDisk,
loadGameFromDisk,
loadGamesFromDb,
loadGameFromDb,
persistGamesToDb,
persistGameToDb,
loadGames,
loadGame,
persistGames,
persistGame,
setDirty,
}

View file

@ -4,14 +4,10 @@ import exif from 'exif'
import sharp from 'sharp'
import {UPLOAD_DIR, UPLOAD_URL} from './Dirs'
import Db, { OrderBy, WhereRaw } from './Db'
import Db from './Db'
import { Dim } from '../common/Geometry'
import Util, { logger } from '../common/Util'
import { Tag, ImageInfo } from '../common/Types'
const log = logger('Images.ts')
const resizeImage = async (filename: string): Promise<void> => {
const resizeImage = async (filename: string) => {
if (!filename.toLowerCase().match(/\.(jpe?g|webp|png)$/)) {
return
}
@ -34,82 +30,55 @@ const resizeImage = async (filename: string): Promise<void> => {
[150, 100],
[375, 210],
]
for (const [w,h] of sizes) {
log.info(w, h, imagePath)
await sharpImg
.resize(w, h, { fit: 'contain' })
.toFile(`${imageOutPath}-${w}x${h}.webp`)
for (let [w,h] of sizes) {
console.log(w, h, imagePath)
await sharpImg.resize(w, h, { fit: 'contain' }).toFile(`${imageOutPath}-${w}x${h}.webp`)
}
}
async function getExifOrientation(imagePath: string): Promise<number> {
return new Promise((resolve) => {
new exif.ExifImage({ image: imagePath }, (error, exifData) => {
async function getExifOrientation(imagePath: string) {
return new Promise((resolve, reject) => {
new exif.ExifImage({ image: imagePath }, function (error, exifData) {
if (error) {
resolve(0)
} else {
resolve(exifData.image.Orientation || 0)
resolve(exifData.image.Orientation)
}
})
})
}
const getAllTags = (db: Db): Tag[] => {
const query = `
select c.id, c.slug, c.title, count(*) as total from categories c
inner join image_x_category ixc on c.id = ixc.category_id
inner join images i on i.id = ixc.image_id
group by c.id order by total desc;`
return db._getMany(query).map(row => ({
id: parseInt(row.id, 10) || 0,
slug: row.slug,
title: row.title,
total: parseInt(row.total, 10) || 0,
}))
}
const getTags = (db: Db, imageId: number): Tag[] => {
const getTags = (db: Db, imageId: number) => {
const query = `
select * from categories c
inner join image_x_category ixc on c.id = ixc.category_id
where ixc.image_id = ?`
return db._getMany(query, [imageId]).map(row => ({
id: parseInt(row.id, 10) || 0,
slug: row.slug,
title: row.title,
total: 0,
}))
return db._getMany(query, [imageId])
}
const imageFromDb = (db: Db, imageId: number): ImageInfo => {
const imageFromDb = (db: Db, imageId: number) => {
const i = db.get('images', { id: imageId })
return {
id: i.id,
uploaderUserId: i.uploader_user_id,
filename: i.filename,
file: `${UPLOAD_DIR}/${i.filename}`,
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
title: i.title,
tags: getTags(db, i.id),
tags: getTags(db, i.id) as any[],
created: i.created * 1000,
width: i.width,
height: i.height,
}
}
const allImagesFromDb = (
db: Db,
tagSlugs: string[],
orderBy: string
): ImageInfo[] => {
const orderByMap = {
const allImagesFromDb = (db: Db, tagSlugs: string[], sort: string) => {
const sortMap = {
alpha_asc: [{filename: 1}],
alpha_desc: [{filename: -1}],
date_asc: [{created: 1}],
date_desc: [{created: -1}],
} as Record<string, OrderBy>
} as Record<string, any>
// TODO: .... clean up
const wheresRaw: WhereRaw = {}
const wheresRaw: Record<string, any> = {}
if (tagSlugs.length > 0) {
const c = db.getMany('categories', {slug: {'$in': tagSlugs}})
if (!c) {
@ -127,52 +96,42 @@ inner join images i on i.id = ixc.image_id ${where.sql};
}
wheresRaw['id'] = {'$in': ids}
}
const images = db.getMany('images', wheresRaw, orderByMap[orderBy])
const images = db.getMany('images', wheresRaw, sortMap[sort])
return images.map(i => ({
id: i.id as number,
uploaderUserId: i.uploader_user_id,
filename: i.filename,
file: `${UPLOAD_DIR}/${i.filename}`,
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
title: i.title,
tags: getTags(db, i.id),
tags: getTags(db, i.id) as any[],
created: i.created * 1000,
width: i.width,
height: i.height,
}))
}
/**
* @deprecated old function, now database is used
*/
const allImagesFromDisk = (
tags: string[],
sort: string
): ImageInfo[] => {
const allImagesFromDisk = (tags: string[], sort: string) => {
let images = fs.readdirSync(UPLOAD_DIR)
.filter(f => f.toLowerCase().match(/\.(jpe?g|webp|png)$/))
.map(f => ({
id: 0,
uploaderUserId: null,
filename: f,
file: `${UPLOAD_DIR}/${f}`,
url: `${UPLOAD_URL}/${encodeURIComponent(f)}`,
title: f.replace(/\.[a-z]+$/, ''),
tags: [] as Tag[],
tags: [] as any[],
created: fs.statSync(`${UPLOAD_DIR}/${f}`).mtime.getTime(),
width: 0, // may have to fill when the function is used again
height: 0, // may have to fill when the function is used again
}))
switch (sort) {
case 'alpha_asc':
images = images.sort((a, b) => {
return a.filename > b.filename ? 1 : -1
return a.file > b.file ? 1 : -1
})
break;
case 'alpha_desc':
images = images.sort((a, b) => {
return a.filename < b.filename ? 1 : -1
return a.file < b.file ? 1 : -1
})
break;
@ -193,7 +152,7 @@ const allImagesFromDisk = (
}
async function getDimensions(imagePath: string): Promise<Dim> {
const dimensions = sizeOf(imagePath)
let dimensions = sizeOf(imagePath)
const orientation = await getExifOrientation(imagePath)
// when image is rotated to the left or right, switch width/height
// https://jdhao.github.io/2019/07/31/image_rotation_exif_info/
@ -209,26 +168,10 @@ async function getDimensions(imagePath: string): Promise<Dim> {
}
}
const setTags = (db: Db, imageId: number, tags: string[]): void => {
db.delete('image_x_category', { image_id: imageId })
tags.forEach((tag: string) => {
const slug = Util.slug(tag)
const id = db.upsert('categories', { slug, title: tag }, { slug }, 'id')
if (id) {
db.insert('image_x_category', {
image_id: imageId,
category_id: id,
})
}
})
}
export default {
allImagesFromDisk,
imageFromDb,
allImagesFromDb,
getAllTags,
resizeImage,
getDimensions,
setTags,
}

View file

@ -1,11 +1,10 @@
import Util from './../common/Util'
import { Rng } from './../common/Rng'
import Images from './Images'
import { EncodedPiece, EncodedPieceShape, PieceShape, Puzzle, ShapeMode, ImageInfo } from '../common/Types'
import { Dim, Point } from '../common/Geometry'
import { UPLOAD_DIR } from './Dirs'
import { EncodedPiece, EncodedPieceShape, PieceShape, Puzzle } from '../common/GameCommon'
import { Point } from '../common/Geometry'
export interface PuzzleCreationInfo {
interface PuzzleCreationInfo {
width: number
height: number
tileSize: number
@ -23,11 +22,10 @@ const TILE_SIZE = 64
async function createPuzzle(
rng: Rng,
targetTiles: number,
image: ImageInfo,
ts: number,
shapeMode: ShapeMode
image: { file: string, url: string },
ts: number
): Promise<Puzzle> {
const imagePath = `${UPLOAD_DIR}/${image.filename}`
const imagePath = image.file
const imageUrl = image.url
// determine puzzle information from the image dimensions
@ -35,18 +33,22 @@ async function createPuzzle(
if (!dim.w || !dim.h) {
throw `[ 2021-05-16 invalid dimension for path ${imagePath} ]`
}
const info: PuzzleCreationInfo = determinePuzzleInfo(dim, targetTiles)
const info: PuzzleCreationInfo = determinePuzzleInfo(
dim.w,
dim.h,
targetTiles
)
const rawPieces = new Array(info.tiles)
for (let i = 0; i < rawPieces.length; i++) {
rawPieces[i] = { idx: i }
let tiles = new Array(info.tiles)
for (let i = 0; i < tiles.length; i++) {
tiles[i] = { idx: i }
}
const shapes = determinePuzzleTileShapes(rng, info, shapeMode)
const shapes = determinePuzzleTileShapes(rng, info)
let positions: Point[] = new Array(info.tiles)
for (const piece of rawPieces) {
const coord = Util.coordByPieceIdx(info, piece.idx)
positions[piece.idx] = {
for (let tile of tiles) {
const coord = Util.coordByTileIdx(info, tile.idx)
positions[tile.idx] = {
// instead of info.tileSize, we use info.tileDrawSize
// to spread the tiles a bit
x: coord.x * info.tileSize * 1.5,
@ -58,7 +60,7 @@ async function createPuzzle(
const tableHeight = info.height * 3
const off = info.tileSize * 1.5
const last: Point = {
let last: Point = {
x: info.width - (1 * off),
y: info.height - (2 * off),
}
@ -68,7 +70,7 @@ async function createPuzzle(
let diffX = off
let diffY = 0
let index = 0
for (const pos of positions) {
for (let pos of positions) {
pos.x = last.x
pos.y = last.y
last.x+=diffX
@ -95,9 +97,9 @@ async function createPuzzle(
// then shuffle the positions
positions = rng.shuffle(positions)
const pieces: Array<EncodedPiece> = rawPieces.map(piece => {
return Util.encodePiece({
idx: piece.idx, // index of tile in the array
const pieces: Array<EncodedPiece> = tiles.map(tile => {
return Util.encodeTile({
idx: tile.idx, // index of tile in the array
group: 0, // if grouped with other tiles
z: 0, // z index of the tile
@ -110,7 +112,7 @@ async function createPuzzle(
// 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
pos: positions[piece.idx],
pos: positions[tile.idx],
})
})
@ -135,8 +137,7 @@ async function createPuzzle(
},
// information that was used to create the puzzle
targetTiles: targetTiles,
imageUrl, // todo: remove
image: image,
imageUrl,
width: info.width, // actual puzzle width (same as bitmap.width)
height: info.height, // actual puzzle height (same as bitmap.height)
@ -160,27 +161,16 @@ async function createPuzzle(
},
}
}
function determineTabs (shapeMode: ShapeMode): number[] {
switch(shapeMode) {
case ShapeMode.ANY:
return [-1, 0, 1]
case ShapeMode.FLAT:
return [0]
case ShapeMode.NORMAL:
default:
return [-1, 1]
}
}
function determinePuzzleTileShapes(
rng: Rng,
info: PuzzleCreationInfo,
shapeMode: ShapeMode
info: PuzzleCreationInfo
): Array<EncodedPieceShape> {
const tabs: number[] = determineTabs(shapeMode)
const tabs = [-1, 1]
const shapes: Array<PieceShape> = new Array(info.tiles)
for (let i = 0; i < info.tiles; i++) {
const coord = Util.coordByPieceIdx(info, i)
let coord = Util.coordByTileIdx(info, i)
shapes[i] = {
top: coord.y === 0 ? 0 : shapes[i - info.tilesX].bottom * -1,
right: coord.x === info.tilesX - 1 ? 0 : rng.choice(tabs),
@ -191,12 +181,9 @@ function determinePuzzleTileShapes(
return shapes.map(Util.encodeShape)
}
const determineTilesXY = (
dim: Dim,
targetTiles: number
): { tilesX: number, tilesY: number } => {
const w_ = dim.w < dim.h ? (dim.w * dim.h) : (dim.w * dim.w)
const h_ = dim.w < dim.h ? (dim.h * dim.h) : (dim.w * dim.h)
const determineTilesXY = (w: number, h: number, targetTiles: number) => {
const w_ = w < h ? (w * h) : (w * w)
const h_ = w < h ? (h * h) : (w * h)
let size = 0
let tiles = 0
do {
@ -211,10 +198,11 @@ const determineTilesXY = (
}
const determinePuzzleInfo = (
dim: Dim,
w: number,
h: number,
targetTiles: number
): PuzzleCreationInfo => {
const {tilesX, tilesY} = determineTilesXY(dim, targetTiles)
const {tilesX, tilesY} = determineTilesXY(w, h, targetTiles)
const tiles = tilesX * tilesY
const tileSize = TILE_SIZE
const width = tilesX * tileSize

View file

@ -1,36 +0,0 @@
import Time from '../common/Time'
import Db from './Db'
const TABLE = 'users'
const HEADER_CLIENT_ID = 'client-id'
const HEADER_CLIENT_SECRET = 'client-secret'
const getOrCreateUser = (db: Db, req: any): any => {
let user = getUser(db, req)
if (!user) {
db.insert(TABLE, {
'client_id': req.headers[HEADER_CLIENT_ID],
'client_secret': req.headers[HEADER_CLIENT_SECRET],
'created': Time.timestamp(),
})
user = getUser(db, req)
}
return user
}
const getUser = (db: Db, req: any): any => {
const user = db.get(TABLE, {
'client_id': req.headers[HEADER_CLIENT_ID],
'client_secret': req.headers[HEADER_CLIENT_SECRET],
})
if (user) {
user.id = parseInt(user.id, 10)
}
return user
}
export default {
getOrCreateUser,
getUser,
}

View file

@ -14,17 +14,17 @@ config = {
*/
class EvtBus {
private _on: Record<string, Function[]>
private _on: any
constructor() {
this._on = {}
this._on = {} as any
}
on (type: string, callback: Function): void {
on(type: string, callback: Function) {
this._on[type] = this._on[type] || []
this._on[type].push(callback)
}
dispatch (type: string, ...args: Array<any>): void {
dispatch(type: string, ...args: Array<any>) {
(this._on[type] || []).forEach((cb: Function) => {
cb(...args)
})
@ -43,20 +43,20 @@ class WebSocketServer {
this.evt = new EvtBus()
}
on (type: string, callback: Function): void {
on(type: string, callback: Function) {
this.evt.on(type, callback)
}
listen (): void {
listen() {
this._websocketserver = new WebSocket.Server(this.config)
this._websocketserver.on('connection', (socket: WebSocket, request: Request): void => {
this._websocketserver.on('connection', (socket: WebSocket, request: Request) => {
const pathname = new URL(this.config.connectstring).pathname
if (request.url.indexOf(pathname) !== 0) {
log.log('bad request url: ', request.url)
socket.close()
return
}
socket.on('message', (data: WebSocket.Data) => {
socket.on('message', (data: any) => {
log.log(`ws`, socket.protocol, data)
this.evt.dispatch('message', {socket, data})
})
@ -66,13 +66,13 @@ class WebSocketServer {
})
}
close (): void {
close() {
if (this._websocketserver) {
this._websocketserver.close()
}
}
notifyOne (data: any, socket: WebSocket): void {
notifyOne(data: any, socket: WebSocket) {
socket.send(JSON.stringify(data))
}
}

View file

@ -1,11 +1,11 @@
import WebSocketServer from './WebSocketServer'
import WebSocket from 'ws'
import express from 'express'
import compression from 'compression'
import multer from 'multer'
import Protocol from './../common/Protocol'
import Util, { logger } from './../common/Util'
import Game from './Game'
import bodyParser from 'body-parser'
import v8 from 'v8'
import fs from 'fs'
import GameLog from './GameLog'
@ -16,13 +16,11 @@ import {
DB_FILE,
DB_PATCHES_DIR,
PUBLIC_DIR,
UPLOAD_DIR,
UPLOAD_DIR
} from './Dirs'
import GameCommon from '../common/GameCommon'
import { ServerEvent, Game as GameType, GameSettings } from '../common/Types'
import { GameSettings, ScoreMode } from '../common/GameCommon'
import GameStorage from './GameStorage'
import Db from './Db'
import Users from './Users'
const db = new Db(DB_FILE, DB_PATCHES_DIR)
db.patch()
@ -48,8 +46,6 @@ const port = config.http.port
const hostname = config.http.hostname
const app = express()
app.use(compression())
const storage = multer.diskStorage({
destination: UPLOAD_DIR,
filename: function (req, file, cb) {
@ -58,76 +54,33 @@ const storage = multer.diskStorage({
})
const upload = multer({storage}).single('file');
app.get('/api/me', (req, res): void => {
let user = Users.getUser(db, req)
res.send({
id: user ? user.id : null,
created: user ? user.created : null,
})
})
app.get('/api/conf', (req, res): void => {
app.get('/api/conf', (req, res) => {
res.send({
WS_ADDRESS: config.ws.connectstring,
})
})
app.get('/api/replay-data', async (req, res): Promise<void> => {
const q: Record<string, any> = req.query
const offset = parseInt(q.offset, 10) || 0
if (offset < 0) {
res.status(400).send({ reason: 'bad offset' })
return
}
const size = parseInt(q.size, 10) || 10000
if (size < 0 || size > 10000) {
res.status(400).send({ reason: 'bad size' })
return
}
const gameId = q.gameId || ''
if (!GameLog.exists(q.gameId)) {
res.status(404).send({ reason: 'no log found' })
return
}
const log = GameLog.get(gameId, offset)
let game: GameType|null = null
if (offset === 0) {
// also need the game
game = await Game.createGameObject(
gameId,
log[0][2],
log[0][3], // must be ImageInfo
log[0][4],
log[0][5],
log[0][6],
log[0][7],
log[0][8], // creatorUserId
)
}
res.send({ log, game: game ? Util.encodeGame(game) : null })
})
app.get('/api/newgame-data', (req, res): void => {
const q: Record<string, any> = req.query
app.get('/api/newgame-data', (req, res) => {
const q = req.query as any
const tagSlugs: string[] = q.tags ? q.tags.split(',') : []
res.send({
images: Images.allImagesFromDb(db, tagSlugs, q.sort),
tags: Images.getAllTags(db),
tags: db.getMany('categories', {}, [{ title: 1 }]),
})
})
app.get('/api/index-data', (req, res): void => {
app.get('/api/index-data', (req, res) => {
const ts = Time.timestamp()
const games = [
...GameCommon.getAllGames().map((game: GameType) => ({
...Game.getAllGames().map((game: any) => ({
id: game.id,
hasReplay: GameLog.exists(game.id),
started: GameCommon.getStartTs(game.id),
finished: GameCommon.getFinishTs(game.id),
tilesFinished: GameCommon.getFinishedPiecesCount(game.id),
tilesTotal: GameCommon.getPieceCount(game.id),
players: GameCommon.getActivePlayers(game.id, ts).length,
imageUrl: GameCommon.getImageUrl(game.id),
started: Game.getStartTs(game.id),
finished: Game.getFinishTs(game.id),
tilesFinished: Game.getFinishedTileCount(game.id),
tilesTotal: Game.getTileCount(game.id),
players: Game.getActivePlayers(game.id, ts).length,
imageUrl: Game.getImageUrl(game.id),
})),
]
@ -143,77 +96,78 @@ interface SaveImageRequestData {
tags: string[]
}
app.post('/api/save-image', express.json(), (req, res): void => {
const user = Users.getUser(db, req)
if (!user || !user.id) {
res.status(403).send({ ok: false, error: 'forbidden' })
return
}
const setImageTags = (db: Db, imageId: number, tags: string[]) => {
tags.forEach((tag: string) => {
const slug = Util.slug(tag)
const id = db.upsert('categories', { slug, title: tag }, { slug }, 'id')
if (id) {
db.insert('image_x_category', {
image_id: imageId,
category_id: id,
})
}
})
}
app.post('/api/save-image', bodyParser.json(), (req, res) => {
const data = req.body as SaveImageRequestData
const image = db.get('images', {id: data.id})
if (parseInt(image.uploader_user_id, 10) !== user.id) {
res.status(403).send({ ok: false, error: 'forbidden' })
return
}
db.update('images', {
title: data.title,
}, {
id: data.id,
})
Images.setTags(db, data.id, data.tags || [])
db.delete('image_x_category', { image_id: data.id })
if (data.tags) {
setImageTags(db, data.id, data.tags)
}
res.send({ ok: true })
})
app.post('/api/upload', (req, res): void => {
upload(req, res, async (err: any): Promise<void> => {
app.post('/api/upload', (req, res) => {
upload(req, res, async (err: any) => {
if (err) {
log.log(err)
res.status(400).send("Something went wrong!")
return
res.status(400).send("Something went wrong!");
}
try {
await Images.resizeImage(req.file.filename)
} catch (err) {
log.log(err)
res.status(400).send("Something went wrong!")
return
res.status(400).send("Something went wrong!");
}
const user = Users.getOrCreateUser(db, req)
const dim = await Images.getDimensions(
`${UPLOAD_DIR}/${req.file.filename}`
)
const imageId = db.insert('images', {
uploader_user_id: user.id,
filename: req.file.filename,
filename_original: req.file.originalname,
title: req.body.title || '',
created: Time.timestamp(),
width: dim.w,
height: dim.h,
})
if (req.body.tags) {
const tags = req.body.tags.split(',').filter((tag: string) => !!tag)
Images.setTags(db, imageId as number, tags)
setImageTags(db, imageId as number, req.body.tags)
}
res.send(Images.imageFromDb(db, imageId as number))
})
})
app.post('/api/newgame', express.json(), async (req, res): Promise<void> => {
const user = Users.getOrCreateUser(db, req)
const gameId = await Game.createNewGame(
req.body as GameSettings,
Time.timestamp(),
user.id
)
app.post('/newgame', bodyParser.json(), async (req, res) => {
const gameSettings = req.body as GameSettings
log.log(gameSettings)
const gameId = Util.uniqId()
if (!Game.exists(gameId)) {
const ts = Time.timestamp()
await Game.createGame(
gameId,
gameSettings.tiles,
gameSettings.image,
ts,
gameSettings.scoreMode
)
}
res.send({ id: gameId })
})
@ -222,18 +176,17 @@ app.use('/', express.static(PUBLIC_DIR))
const wss = new WebSocketServer(config.ws);
const notify = (data: ServerEvent, sockets: Array<WebSocket>): void => {
for (const socket of sockets) {
const notify = (data: any, sockets: Array<WebSocket>) => {
// TODO: throttle?
for (let socket of sockets) {
wss.notifyOne(data, socket)
}
}
wss.on('close', async (
{socket} : { socket: WebSocket }
): Promise<void> => {
wss.on('close', async ({socket} : {socket: WebSocket}) => {
try {
const proto = socket.protocol.split('|')
// const clientId = proto[0]
const clientId = proto[0]
const gameId = proto[1]
GameSockets.removeSocket(gameId, socket)
} catch (e) {
@ -241,30 +194,40 @@ wss.on('close', async (
}
})
wss.on('message', async (
{socket, data} : { socket: WebSocket, data: WebSocket.Data }
): Promise<void> => {
wss.on('message', async ({socket, data} : { socket: WebSocket, data: any }) => {
try {
const proto = socket.protocol.split('|')
const clientId = proto[0]
const gameId = proto[1]
// TODO: maybe handle different types of data
// (but atm only string comes through)
const msg = JSON.parse(data as string)
const msg = JSON.parse(data)
const msgType = msg[0]
switch (msgType) {
case Protocol.EV_CLIENT_INIT_REPLAY: {
if (!GameLog.exists(gameId)) {
throw `[gamelog ${gameId} does not exist... ]`
}
const log = GameLog.get(gameId)
const game = await Game.createGameObject(
gameId,
log[0][2],
log[0][3],
log[0][4],
log[0][5] || ScoreMode.FINAL
)
notify(
[Protocol.EV_SERVER_INIT_REPLAY, Util.encodeGame(game), log],
[socket]
)
} break
case Protocol.EV_CLIENT_INIT: {
if (!GameCommon.exists(gameId)) {
if (!Game.exists(gameId)) {
throw `[game ${gameId} does not exist... ]`
}
const ts = Time.timestamp()
Game.addPlayer(gameId, clientId, ts)
GameSockets.addSocket(gameId, socket)
const game: GameType|null = GameCommon.get(gameId)
if (!game) {
throw `[game ${gameId} does not exist (anymore)... ]`
}
const game = Game.get(gameId)
notify(
[Protocol.EV_SERVER_INIT, Util.encodeGame(game)],
[socket]
@ -272,7 +235,7 @@ wss.on('message', async (
} break
case Protocol.EV_CLIENT_EVENT: {
if (!GameCommon.exists(gameId)) {
if (!Game.exists(gameId)) {
throw `[game ${gameId} does not exist... ]`
}
const clientSeq = msg[1]
@ -280,7 +243,7 @@ wss.on('message', async (
const ts = Time.timestamp()
let sendGame = false
if (!GameCommon.playerExists(gameId, clientId)) {
if (!Game.playerExists(gameId, clientId)) {
Game.addPlayer(gameId, clientId, ts)
sendGame = true
}
@ -289,10 +252,7 @@ wss.on('message', async (
sendGame = true
}
if (sendGame) {
const game: GameType|null = GameCommon.get(gameId)
if (!game) {
throw `[game ${gameId} does not exist (anymore)... ]`
}
const game = Game.get(gameId)
notify(
[Protocol.EV_SERVER_INIT, Util.encodeGame(game)],
[socket]
@ -311,7 +271,7 @@ wss.on('message', async (
}
})
GameStorage.loadGamesFromDb(db)
GameStorage.loadGames()
const server = app.listen(
port,
hostname,
@ -320,9 +280,9 @@ const server = app.listen(
wss.listen()
const memoryUsageHuman = (): void => {
const memoryUsageHuman = () => {
const totalHeapSize = v8.getHeapStatistics().total_available_size
const totalHeapSizeInGB = (totalHeapSize / 1024 / 1024 / 1024).toFixed(2)
let totalHeapSizeInGB = (totalHeapSize / 1024 / 1024 / 1024).toFixed(2)
log.log(`Total heap size (bytes) ${totalHeapSize}, (GB ~${totalHeapSizeInGB})`)
const used = process.memoryUsage().heapUsed / 1024 / 1024
@ -334,19 +294,19 @@ memoryUsageHuman()
// persist games in fixed interval
const persistInterval = setInterval(() => {
log.log('Persisting games...')
GameStorage.persistGamesToDb(db)
GameStorage.persistGames()
memoryUsageHuman()
}, config.persistence.interval)
const gracefulShutdown = (signal: string): void => {
const gracefulShutdown = (signal: any) => {
log.log(`${signal} received...`)
log.log('clearing persist interval...')
clearInterval(persistInterval)
log.log('persisting games...')
GameStorage.persistGamesToDb(db)
GameStorage.persistGames()
log.log('shutting down webserver...')
server.close()
@ -359,14 +319,14 @@ const gracefulShutdown = (signal: string): void => {
}
// used by nodemon
process.once('SIGUSR2', (): void => {
process.once('SIGUSR2', function () {
gracefulShutdown('SIGUSR2')
})
process.once('SIGINT', (): void => {
process.once('SIGINT', function (code) {
gracefulShutdown('SIGINT')
})
process.once('SIGTERM', (): void => {
process.once('SIGTERM', function (code) {
gracefulShutdown('SIGTERM')
})