add linting, do more type hinting

This commit is contained in:
Zutatensuppe 2021-05-29 17:58:05 +02:00
parent 46f3fc7480
commit d4f02c10df
29 changed files with 3353 additions and 1354 deletions

3
.eslintignore Normal file
View file

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

17
.eslintrc.cjs Normal file
View file

@ -0,0 +1,17 @@
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

View file

@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>🧩 jigsaw.hyottoko.club</title> <title>🧩 jigsaw.hyottoko.club</title>
<script type="module" crossorigin src="/assets/index.643c957c.js"></script> <script type="module" crossorigin src="/assets/index.ae0a2617.js"></script>
<link rel="modulepreload" href="/assets/vendor.b622ee49.js"> <link rel="modulepreload" href="/assets/vendor.b622ee49.js">
<link rel="stylesheet" href="/assets/index.f7304069.css"> <link rel="stylesheet" href="/assets/index.f7304069.css">
</head> </head>

File diff suppressed because it is too large Load diff

1845
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

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

View file

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

5
scripts/lint Executable file
View file

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

View file

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

View file

@ -1,172 +1,31 @@
import Geometry, { Point, Rect } from './Geometry' import Geometry, { Point, Rect } from './Geometry'
import Protocol from './Protocol' import Protocol from './Protocol'
import { Rng, RngSerialized } from './Rng' import { Rng } from './Rng'
import Time from './Time' import Time from './Time'
import { FixedLengthArray } from './Types' import {
import Util from './Util' Change,
EncodedPiece,
export type Timestamp = number EvtInfo,
Game,
export type EncodedPlayer = FixedLengthArray<[ Input,
string, Piece,
number, PieceChange,
number, Player,
0|1, PlayerChange,
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, Puzzle,
Array<EncodedPlayer>, PuzzleData,
Record<string, EvtInfo>, PuzzleDataChange,
ScoreMode, ScoreMode,
]> Timestamp
} from './Types'
export interface ReplayData { import Util from './Util'
log: any[],
game: EncodedGame|null
}
export interface Tag {
id: number
slug: string
title: string
}
interface GameRng {
obj: Rng
type?: string
}
export 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: Timestamp
}
interface EvtInfo {
_last_mouse: Point|null
_last_mouse_down: Point|null
}
export enum ScoreMode {
FINAL = 0,
ANY = 1,
}
const IDLE_TIMEOUT_SEC = 30 const IDLE_TIMEOUT_SEC = 30
// Map<gameId, Game> // Map<gameId, Game>
const GAMES: Record<string, Game> = {} const GAMES: Record<string, Game> = {}
function exists(gameId: string) { function exists(gameId: string): boolean {
return (!!GAMES[gameId]) || false return (!!GAMES[gameId]) || false
} }
@ -190,7 +49,7 @@ function setGame(gameId: string, game: Game): void {
function getPlayerIndexById(gameId: string, playerId: string): number { function getPlayerIndexById(gameId: string, playerId: string): number {
let i = 0; let i = 0;
for (let player of GAMES[gameId].players) { for (const player of GAMES[gameId].players) {
if (Util.decodePlayer(player).id === playerId) { if (Util.decodePlayer(player).id === playerId) {
return i return i
} }
@ -293,8 +152,8 @@ function getAllPlayers(gameId: string): Array<Player> {
: [] : []
} }
function get(gameId: string) { function get(gameId: string): Game|null {
return GAMES[gameId] return GAMES[gameId] || null
} }
function getPieceCount(gameId: string): number { function getPieceCount(gameId: string): number {
@ -319,7 +178,7 @@ function isFinished(gameId: string): boolean {
function getFinishedPiecesCount(gameId: string): number { function getFinishedPiecesCount(gameId: string): number {
let count = 0 let count = 0
for (let t of GAMES[gameId].puzzle.tiles) { for (const t of GAMES[gameId].puzzle.tiles) {
if (Util.decodePiece(t).owner === -1) { if (Util.decodePiece(t).owner === -1) {
count++ count++
} }
@ -335,29 +194,33 @@ function getPiecesSortedByZIndex(gameId: string): Piece[] {
function changePlayer( function changePlayer(
gameId: string, gameId: string,
playerId: string, playerId: string,
change: any change: PlayerChange
): void { ): void {
const player = getPlayer(gameId, playerId) const player = getPlayer(gameId, playerId)
if (player === null) { if (player === null) {
return return
} }
for (let k of Object.keys(change)) { for (const k of Object.keys(change)) {
// @ts-ignore // @ts-ignore
player[k] = change[k] player[k] = change[k]
} }
setPlayer(gameId, playerId, player) setPlayer(gameId, playerId, player)
} }
function changeData(gameId: string, change: any): void { function changeData(gameId: string, change: PuzzleDataChange): void {
for (let k of Object.keys(change)) { for (const k of Object.keys(change)) {
// @ts-ignore // @ts-ignore
GAMES[gameId].puzzle.data[k] = change[k] GAMES[gameId].puzzle.data[k] = change[k]
} }
} }
function changeTile(gameId: string, pieceIdx: number, change: any): void { function changePiece(
for (let k of Object.keys(change)) { gameId: string,
pieceIdx: number,
change: PieceChange
): void {
for (const k of Object.keys(change)) {
const piece = Util.decodePiece(GAMES[gameId].puzzle.tiles[pieceIdx]) const piece = Util.decodePiece(GAMES[gameId].puzzle.tiles[pieceIdx])
// @ts-ignore // @ts-ignore
piece[k] = change[k] piece[k] = change[k]
@ -415,13 +278,12 @@ const getPieceBounds = (gameId: string, tileIdx: number): Rect => {
} }
} }
const getTileZIndex = (gameId: string, tileIdx: number): number => { const getPieceZIndex = (gameId: string, pieceIdx: number): number => {
const tile = getPiece(gameId, tileIdx) return getPiece(gameId, pieceIdx).z
return tile.z
} }
const getFirstOwnedPieceIdx = (gameId: string, playerId: string): number => { const getFirstOwnedPieceIdx = (gameId: string, playerId: string): number => {
for (let t of GAMES[gameId].puzzle.tiles) { for (const t of GAMES[gameId].puzzle.tiles) {
const tile = Util.decodePiece(t) const tile = Util.decodePiece(t)
if (tile.owner === playerId) { if (tile.owner === playerId) {
return tile.idx return tile.idx
@ -430,7 +292,10 @@ const getFirstOwnedPieceIdx = (gameId: string, playerId: string): number => {
return -1 return -1
} }
const getFirstOwnedPiece = (gameId: string, playerId: string): EncodedPiece|null => { const getFirstOwnedPiece = (
gameId: string,
playerId: string
): EncodedPiece|null => {
const idx = getFirstOwnedPieceIdx(gameId, playerId) const idx = getFirstOwnedPieceIdx(gameId, playerId)
return idx < 0 ? null : GAMES[gameId].puzzle.tiles[idx] return idx < 0 ? null : GAMES[gameId].puzzle.tiles[idx]
} }
@ -463,12 +328,12 @@ const getMaxZIndex = (gameId: string): number => {
return GAMES[gameId].puzzle.data.maxZ return GAMES[gameId].puzzle.data.maxZ
} }
const getMaxZIndexByTileIdxs = (gameId: string, tileIdxs: Array<number>): number => { const getMaxZIndexByPieceIdxs = (gameId: string, pieceIdxs: Array<number>): number => {
let maxZ = 0 let maxZ = 0
for (let tileIdx of tileIdxs) { for (const pieceIdx of pieceIdxs) {
let tileZIndex = getTileZIndex(gameId, tileIdx) const curZ = getPieceZIndex(gameId, pieceIdx)
if (tileZIndex > maxZ) { if (curZ > maxZ) {
maxZ = tileZIndex maxZ = curZ
} }
} }
return maxZ return maxZ
@ -477,7 +342,7 @@ const getMaxZIndexByTileIdxs = (gameId: string, tileIdxs: Array<number>): number
function srcPosByTileIdx(gameId: string, tileIdx: number): Point { function srcPosByTileIdx(gameId: string, tileIdx: number): Point {
const info = GAMES[gameId].puzzle.info const info = GAMES[gameId].puzzle.info
const c = Util.coordByTileIdx(info, tileIdx) const c = Util.coordByPieceIdx(info, tileIdx)
const cx = c.x * info.tileSize const cx = c.x * info.tileSize
const cy = c.y * info.tileSize const cy = c.y * info.tileSize
@ -487,7 +352,7 @@ function srcPosByTileIdx(gameId: string, tileIdx: number): Point {
function getSurroundingTilesByIdx(gameId: string, tileIdx: number) { function getSurroundingTilesByIdx(gameId: string, tileIdx: number) {
const info = GAMES[gameId].puzzle.info const info = GAMES[gameId].puzzle.info
const c = Util.coordByTileIdx(info, tileIdx) const c = Util.coordByPieceIdx(info, tileIdx)
return [ return [
// top // top
@ -501,29 +366,29 @@ function getSurroundingTilesByIdx(gameId: string, tileIdx: number) {
] ]
} }
const setTilesZIndex = (gameId: string, tileIdxs: Array<number>, zIndex: number): void => { const setPiecesZIndex = (gameId: string, tileIdxs: Array<number>, zIndex: number): void => {
for (let tilesIdx of tileIdxs) { for (const tilesIdx of tileIdxs) {
changeTile(gameId, tilesIdx, { z: zIndex }) changePiece(gameId, tilesIdx, { z: zIndex })
} }
} }
const moveTileDiff = (gameId: string, tileIdx: number, diff: Point): void => { const moveTileDiff = (gameId: string, tileIdx: number, diff: Point): void => {
const oldPos = getPiecePos(gameId, tileIdx) const oldPos = getPiecePos(gameId, tileIdx)
const pos = Geometry.pointAdd(oldPos, diff) const pos = Geometry.pointAdd(oldPos, diff)
changeTile(gameId, tileIdx, { pos }) changePiece(gameId, tileIdx, { pos })
} }
const moveTilesDiff = ( const movePiecesDiff = (
gameId: string, gameId: string,
tileIdxs: Array<number>, pieceIdxs: Array<number>,
diff: Point diff: Point
): void => { ): void => {
const drawSize = getPieceDrawSize(gameId) const drawSize = getPieceDrawSize(gameId)
const bounds = getBounds(gameId) const bounds = getBounds(gameId)
const cappedDiff = diff const cappedDiff = diff
for (let tileIdx of tileIdxs) { for (const pieceIdx of pieceIdxs) {
const t = getPiece(gameId, tileIdx) const t = getPiece(gameId, pieceIdx)
if (t.pos.x + diff.x < bounds.x) { if (t.pos.x + diff.x < bounds.x) {
cappedDiff.x = Math.max(bounds.x - t.pos.x, cappedDiff.x) cappedDiff.x = Math.max(bounds.x - t.pos.x, cappedDiff.x)
} else if (t.pos.x + drawSize + diff.x > bounds.x + bounds.w) { } else if (t.pos.x + drawSize + diff.x > bounds.x + bounds.w) {
@ -536,24 +401,24 @@ const moveTilesDiff = (
} }
} }
for (let tileIdx of tileIdxs) { for (const pieceIdx of pieceIdxs) {
moveTileDiff(gameId, tileIdx, cappedDiff) moveTileDiff(gameId, pieceIdx, cappedDiff)
} }
} }
const finishTiles = (gameId: string, tileIdxs: Array<number>): void => { const finishPieces = (gameId: string, pieceIdxs: Array<number>): void => {
for (let tileIdx of tileIdxs) { for (const pieceIdx of pieceIdxs) {
changeTile(gameId, tileIdx, { owner: -1, z: 1 }) changePiece(gameId, pieceIdx, { owner: -1, z: 1 })
} }
} }
const setTilesOwner = ( const setTilesOwner = (
gameId: string, gameId: string,
tileIdxs: Array<number>, pieceIdxs: Array<number>,
owner: string|number owner: string|number
): void => { ): void => {
for (let tileIdx of tileIdxs) { for (const pieceIdx of pieceIdxs) {
changeTile(gameId, tileIdx, { owner }) changePiece(gameId, pieceIdx, { owner })
} }
} }
@ -564,7 +429,7 @@ function getGroupedPieceIdxs(gameId: string, pieceIdx: number): number[] {
const grouped = [] const grouped = []
if (piece.group) { if (piece.group) {
for (let other of pieces) { for (const other of pieces) {
const otherPiece = Util.decodePiece(other) const otherPiece = Util.decodePiece(other)
if (otherPiece.group === piece.group) { if (otherPiece.group === piece.group) {
grouped.push(otherPiece.idx) grouped.push(otherPiece.idx)
@ -579,8 +444,8 @@ function getGroupedPieceIdxs(gameId: string, pieceIdx: number): number[] {
// Returns the index of the puzzle tile with the highest z index // Returns the index of the puzzle tile with the highest z index
// that is not finished yet and that matches the position // that is not finished yet and that matches the position
const freePieceIdxByPos = (gameId: string, pos: Point): number => { const freePieceIdxByPos = (gameId: string, pos: Point): number => {
let info = GAMES[gameId].puzzle.info const info = GAMES[gameId].puzzle.info
let pieces = GAMES[gameId].puzzle.tiles const pieces = GAMES[gameId].puzzle.tiles
let maxZ = -1 let maxZ = -1
let pieceIdx = -1 let pieceIdx = -1
@ -664,28 +529,28 @@ const getPuzzleHeight = (gameId: string): number => {
function handleInput( function handleInput(
gameId: string, gameId: string,
playerId: string, playerId: string,
input: any, input: Input,
ts: number ts: Timestamp
): Array<Array<any>> { ): Array<Change> {
const puzzle = GAMES[gameId].puzzle const puzzle = GAMES[gameId].puzzle
const evtInfo = getEvtInfo(gameId, playerId) const evtInfo = getEvtInfo(gameId, playerId)
const changes = [] as Array<Array<any>> const changes: Array<Change> = []
const _dataChange = (): void => { const _dataChange = (): void => {
changes.push([Protocol.CHANGE_DATA, puzzle.data]) changes.push([Protocol.CHANGE_DATA, puzzle.data])
} }
const _tileChange = (tileIdx: number): void => { const _pieceChange = (pieceIdx: number): void => {
changes.push([ changes.push([
Protocol.CHANGE_TILE, Protocol.CHANGE_TILE,
Util.encodePiece(getPiece(gameId, tileIdx)), Util.encodePiece(getPiece(gameId, pieceIdx)),
]) ])
} }
const _tileChanges = (tileIdxs: Array<number>): void => { const _pieceChanges = (pieceIdxs: Array<number>): void => {
for (const tileIdx of tileIdxs) { for (const pieceIdx of pieceIdxs) {
_tileChange(tileIdx) _pieceChange(pieceIdx)
} }
} }
@ -703,12 +568,12 @@ function handleInput(
// put both tiles (and their grouped tiles) in the same group // put both tiles (and their grouped tiles) in the same group
const groupTiles = ( const groupTiles = (
gameId: string, gameId: string,
tileIdx1: number, pieceIdx1: number,
tileIdx2: number pieceIdx2: number
): void => { ): void => {
const tiles = GAMES[gameId].puzzle.tiles const pieces = GAMES[gameId].puzzle.tiles
const group1 = getPieceGroup(gameId, tileIdx1) const group1 = getPieceGroup(gameId, pieceIdx1)
const group2 = getPieceGroup(gameId, tileIdx2) const group2 = getPieceGroup(gameId, pieceIdx2)
let group let group
const searchGroups = [] const searchGroups = []
@ -729,18 +594,18 @@ function handleInput(
group = getMaxGroup(gameId) group = getMaxGroup(gameId)
} }
changeTile(gameId, tileIdx1, { group }) changePiece(gameId, pieceIdx1, { group })
_tileChange(tileIdx1) _pieceChange(pieceIdx1)
changeTile(gameId, tileIdx2, { group }) changePiece(gameId, pieceIdx2, { group })
_tileChange(tileIdx2) _pieceChange(pieceIdx2)
// TODO: strange // TODO: strange
if (searchGroups.length > 0) { if (searchGroups.length > 0) {
for (const t of tiles) { for (const p of pieces) {
const piece = Util.decodePiece(t) const piece = Util.decodePiece(p)
if (searchGroups.includes(piece.group)) { if (searchGroups.includes(piece.group)) {
changeTile(gameId, piece.idx, { group }) changePiece(gameId, piece.idx, { group })
_tileChange(piece.idx) _pieceChange(piece.idx)
} }
} }
} }
@ -770,13 +635,13 @@ function handleInput(
const tileIdxAtPos = freePieceIdxByPos(gameId, pos) const tileIdxAtPos = freePieceIdxByPos(gameId, pos)
if (tileIdxAtPos >= 0) { if (tileIdxAtPos >= 0) {
let maxZ = getMaxZIndex(gameId) + 1 const maxZ = getMaxZIndex(gameId) + 1
changeData(gameId, { maxZ }) changeData(gameId, { maxZ })
_dataChange() _dataChange()
const tileIdxs = getGroupedPieceIdxs(gameId, tileIdxAtPos) const tileIdxs = getGroupedPieceIdxs(gameId, tileIdxAtPos)
setTilesZIndex(gameId, tileIdxs, getMaxZIndex(gameId)) setPiecesZIndex(gameId, tileIdxs, getMaxZIndex(gameId))
setTilesOwner(gameId, tileIdxs, playerId) setTilesOwner(gameId, tileIdxs, playerId)
_tileChanges(tileIdxs) _pieceChanges(tileIdxs)
} }
evtInfo._last_mouse = pos evtInfo._last_mouse = pos
@ -790,18 +655,18 @@ function handleInput(
changePlayer(gameId, playerId, {x, y, ts}) changePlayer(gameId, playerId, {x, y, ts})
_playerChange() _playerChange()
} else { } else {
let tileIdx = getFirstOwnedPieceIdx(gameId, playerId) const pieceIdx = getFirstOwnedPieceIdx(gameId, playerId)
if (tileIdx >= 0) { if (pieceIdx >= 0) {
// player is moving a tile (and hand) // player is moving a tile (and hand)
changePlayer(gameId, playerId, {x, y, ts}) changePlayer(gameId, playerId, {x, y, ts})
_playerChange() _playerChange()
// check if pos is on the tile, otherwise dont move // check if pos is on the tile, otherwise dont move
// (mouse could be out of table, but tile stays on it) // (mouse could be out of table, but tile stays on it)
const tileIdxs = getGroupedPieceIdxs(gameId, tileIdx) const pieceIdxs = getGroupedPieceIdxs(gameId, pieceIdx)
let anyOk = Geometry.pointInBounds(pos, getBounds(gameId)) let anyOk = Geometry.pointInBounds(pos, getBounds(gameId))
&& Geometry.pointInBounds(evtInfo._last_mouse_down, getBounds(gameId)) && Geometry.pointInBounds(evtInfo._last_mouse_down, getBounds(gameId))
for (let idx of tileIdxs) { for (const idx of pieceIdxs) {
const bounds = getPieceBounds(gameId, idx) const bounds = getPieceBounds(gameId, idx)
if (Geometry.pointInBounds(pos, bounds)) { if (Geometry.pointInBounds(pos, bounds)) {
anyOk = true anyOk = true
@ -813,9 +678,9 @@ function handleInput(
const diffY = y - evtInfo._last_mouse_down.y const diffY = y - evtInfo._last_mouse_down.y
const diff = { x: diffX, y: diffY } const diff = { x: diffX, y: diffY }
moveTilesDiff(gameId, tileIdxs, diff) movePiecesDiff(gameId, pieceIdxs, diff)
_tileChanges(tileIdxs) _pieceChanges(pieceIdxs)
} }
} else { } else {
// player is just moving map, so no change in position! // player is just moving map, so no change in position!
@ -835,26 +700,26 @@ function handleInput(
evtInfo._last_mouse_down = null evtInfo._last_mouse_down = null
let tileIdx = getFirstOwnedPieceIdx(gameId, playerId) const pieceIdx = getFirstOwnedPieceIdx(gameId, playerId)
if (tileIdx >= 0) { if (pieceIdx >= 0) {
// drop the tile(s) // drop the tile(s)
let tileIdxs = getGroupedPieceIdxs(gameId, tileIdx) const pieceIdxs = getGroupedPieceIdxs(gameId, pieceIdx)
setTilesOwner(gameId, tileIdxs, 0) setTilesOwner(gameId, pieceIdxs, 0)
_tileChanges(tileIdxs) _pieceChanges(pieceIdxs)
// Check if the tile was dropped near the final location // Check if the tile was dropped near the final location
let tilePos = getPiecePos(gameId, tileIdx) const tilePos = getPiecePos(gameId, pieceIdx)
let finalPos = getFinalPiecePos(gameId, tileIdx) const finalPos = getFinalPiecePos(gameId, pieceIdx)
if (Geometry.pointDistance(finalPos, tilePos) < puzzle.info.snapDistance) { if (Geometry.pointDistance(finalPos, tilePos) < puzzle.info.snapDistance) {
let diff = Geometry.pointSub(finalPos, tilePos) const diff = Geometry.pointSub(finalPos, tilePos)
// Snap the tile to the final destination // Snap the tile to the final destination
moveTilesDiff(gameId, tileIdxs, diff) movePiecesDiff(gameId, pieceIdxs, diff)
finishTiles(gameId, tileIdxs) finishPieces(gameId, pieceIdxs)
_tileChanges(tileIdxs) _pieceChanges(pieceIdxs)
let points = getPlayerPoints(gameId, playerId) let points = getPlayerPoints(gameId, playerId)
if (getScoreMode(gameId) === ScoreMode.FINAL) { if (getScoreMode(gameId) === ScoreMode.FINAL) {
points += tileIdxs.length points += pieceIdxs.length
} else if (getScoreMode(gameId) === ScoreMode.ANY) { } else if (getScoreMode(gameId) === ScoreMode.ANY) {
points += 1 points += 1
} else { } else {
@ -877,7 +742,7 @@ function handleInput(
otherTileIdx: number, otherTileIdx: number,
off: Array<number> off: Array<number>
): boolean => { ): boolean => {
let info = GAMES[gameId].puzzle.info const info = GAMES[gameId].puzzle.info
if (otherTileIdx < 0) { if (otherTileIdx < 0) {
return false return false
} }
@ -890,27 +755,27 @@ function handleInput(
{x: off[0] * info.tileSize, y: off[1] * info.tileSize} {x: off[0] * info.tileSize, y: off[1] * info.tileSize}
) )
if (Geometry.pointDistance(tilePos, dstPos) < info.snapDistance) { if (Geometry.pointDistance(tilePos, dstPos) < info.snapDistance) {
let diff = Geometry.pointSub(dstPos, tilePos) const diff = Geometry.pointSub(dstPos, tilePos)
let tileIdxs = getGroupedPieceIdxs(gameId, tileIdx) let pieceIdxs = getGroupedPieceIdxs(gameId, tileIdx)
moveTilesDiff(gameId, tileIdxs, diff) movePiecesDiff(gameId, pieceIdxs, diff)
groupTiles(gameId, tileIdx, otherTileIdx) groupTiles(gameId, tileIdx, otherTileIdx)
tileIdxs = getGroupedPieceIdxs(gameId, tileIdx) pieceIdxs = getGroupedPieceIdxs(gameId, tileIdx)
const zIndex = getMaxZIndexByTileIdxs(gameId, tileIdxs) const zIndex = getMaxZIndexByPieceIdxs(gameId, pieceIdxs)
setTilesZIndex(gameId, tileIdxs, zIndex) setPiecesZIndex(gameId, pieceIdxs, zIndex)
_tileChanges(tileIdxs) _pieceChanges(pieceIdxs)
return true return true
} }
return false return false
} }
let snapped = false let snapped = false
for (let tileIdxTmp of getGroupedPieceIdxs(gameId, tileIdx)) { for (const pieceIdxTmp of getGroupedPieceIdxs(gameId, pieceIdx)) {
let othersIdxs = getSurroundingTilesByIdx(gameId, tileIdxTmp) const othersIdxs = getSurroundingTilesByIdx(gameId, pieceIdxTmp)
if ( if (
check(gameId, tileIdxTmp, othersIdxs[0], [0, 1]) // top check(gameId, pieceIdxTmp, othersIdxs[0], [0, 1]) // top
|| check(gameId, tileIdxTmp, othersIdxs[1], [-1, 0]) // right || check(gameId, pieceIdxTmp, othersIdxs[1], [-1, 0]) // right
|| check(gameId, tileIdxTmp, othersIdxs[2], [0, -1]) // bottom || check(gameId, pieceIdxTmp, othersIdxs[2], [0, -1]) // bottom
|| check(gameId, tileIdxTmp, othersIdxs[3], [1, 0]) // left || check(gameId, pieceIdxTmp, othersIdxs[3], [1, 0]) // left
) { ) {
snapped = true snapped = true
break break

View file

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

View file

@ -1,6 +1,199 @@
import { Point } from "./Geometry"
import { Rng, RngSerialized } from "./Rng"
// @see https://stackoverflow.com/a/59906630/392905 // @see https://stackoverflow.com/a/59906630/392905
type ArrayLengthMutationKeys = 'splice' | 'push' | 'pop' | 'shift' | 'unshift' | number type ArrayLengthMutationKeys = 'splice' | 'push' | 'pop' | 'shift' | 'unshift' | number
type ArrayItems<T extends Array<any>> = T extends Array<infer TItems> ? TItems : never type ArrayItems<T extends Array<any>> = T extends Array<infer TItems> ? TItems : never
export type FixedLengthArray<T extends any[]> = export type FixedLengthArray<T extends any[]> =
Pick<T, Exclude<keyof T, ArrayLengthMutationKeys>> Pick<T, Exclude<keyof T, ArrayLengthMutationKeys>>
& { [Symbol.iterator]: () => IterableIterator< ArrayItems<T> > } & { [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 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,
]>
export interface ReplayData {
log: any[],
game: EncodedGame|null
}
export interface Tag {
id: number
slug: string
title: string
}
interface GameRng {
obj: Rng
type?: string
}
export 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
}
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 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: 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,
}

View file

@ -1,3 +1,4 @@
import { PuzzleCreationInfo } from '../server/Puzzle'
import { import {
EncodedGame, EncodedGame,
EncodedPiece, EncodedPiece,
@ -7,19 +8,20 @@ import {
Piece, Piece,
PieceShape, PieceShape,
Player, Player,
PuzzleInfo,
ScoreMode ScoreMode
} from './GameCommon' } from './Types'
import { Point } from './Geometry' import { Point } from './Geometry'
import { Rng } from './Rng' import { Rng } from './Rng'
const slug = (str: string) => { const slug = (str: string): string => {
let tmp = str.toLowerCase() let tmp = str.toLowerCase()
tmp = tmp.replace(/[^a-z0-9]+/g, '-') tmp = tmp.replace(/[^a-z0-9]+/g, '-')
tmp = tmp.replace(/^-|-$/, '') tmp = tmp.replace(/^-|-$/, '')
return tmp return tmp
} }
const pad = (x: any, pad: string) => { const pad = (x: number, pad: string): string => {
const str = `${x}` const str = `${x}`
if (str.length >= pad.length) { if (str.length >= pad.length) {
return str return str
@ -43,7 +45,9 @@ export const logger = (...pre: Array<any>) => {
} }
// get a unique id // get a unique id
export const uniqId = () => Date.now().toString(36) + Math.random().toString(36).substring(2) export const uniqId = (): string => {
return Date.now().toString(36) + Math.random().toString(36).substring(2)
}
function encodeShape(data: PieceShape): EncodedPieceShape { function encodeShape(data: PieceShape): EncodedPieceShape {
/* encoded in 1 byte: /* encoded in 1 byte:
@ -139,11 +143,11 @@ function decodeGame(data: EncodedGame): Game {
} }
} }
function coordByTileIdx(info: any, tileIdx: number): Point { function coordByPieceIdx(info: PuzzleInfo|PuzzleCreationInfo, pieceIdx: number): Point {
const wTiles = info.width / info.tileSize const wTiles = info.width / info.tileSize
return { return {
x: tileIdx % wTiles, x: pieceIdx % wTiles,
y: Math.floor(tileIdx / wTiles), y: Math.floor(pieceIdx / wTiles),
} }
} }
@ -151,16 +155,16 @@ const hash = (str: string): number => {
let hash = 0 let hash = 0
for (let i = 0; i < str.length; i++) { for (let i = 0; i < str.length; i++) {
let chr = str.charCodeAt(i); const chr = str.charCodeAt(i);
hash = ((hash << 5) - hash) + chr; hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer hash |= 0; // Convert to 32bit integer
} }
return hash; return hash;
} }
function asQueryArgs(data: any) { function asQueryArgs(data: Record<string, any>): string {
const q = [] const q = []
for (let k in data) { for (const k in data) {
const pair = [k, data[k]].map(encodeURIComponent) const pair = [k, data[k]].map(encodeURIComponent)
q.push(pair.join('=')) q.push(pair.join('='))
} }
@ -187,7 +191,7 @@ export default {
encodeGame, encodeGame,
decodeGame, decodeGame,
coordByTileIdx, coordByPieceIdx,
asQueryArgs, asQueryArgs,
} }

View file

@ -1,6 +1,6 @@
"use strict" "use strict"
import { EncodedGame, ReplayData } from '../common/GameCommon' import { ClientEvent, EncodedGame, GameEvent, ReplayData } from '../common/Types'
import Util, { logger } from '../common/Util' import Util, { logger } from '../common/Util'
import Protocol from './../common/Protocol' import Protocol from './../common/Protocol'
@ -16,25 +16,29 @@ const CONN_STATE_CONNECTING = 3 // connecting
const CONN_STATE_CLOSED = 4 // not connected (closed on purpose) const CONN_STATE_CLOSED = 4 // not connected (closed on purpose)
let ws: WebSocket let ws: WebSocket
let changesCallback = (msg: Array<any>) => {} let changesCallback = (msg: Array<any>) => {
let connectionStateChangeCallback = (state: number) => {} // empty
}
let connectionStateChangeCallback = (state: number) => {
// empty
}
// TODO: change these to something like on(EVT, cb) // TODO: change these to something like on(EVT, cb)
function onServerChange(callback: (msg: Array<any>) => void) { function onServerChange(callback: (msg: Array<any>) => void): void {
changesCallback = callback changesCallback = callback
} }
function onConnectionStateChange(callback: (state: number) => void) { function onConnectionStateChange(callback: (state: number) => void): void {
connectionStateChangeCallback = callback connectionStateChangeCallback = callback
} }
let connectionState = CONN_STATE_NOT_CONNECTED let connectionState = CONN_STATE_NOT_CONNECTED
const setConnectionState = (state: number) => { const setConnectionState = (state: number): void => {
if (connectionState !== state) { if (connectionState !== state) {
connectionState = state connectionState = state
connectionStateChangeCallback(state) connectionStateChangeCallback(state)
} }
} }
function send(message: Array<any>): void { function send(message: ClientEvent): void {
if (connectionState === CONN_STATE_CONNECTED) { if (connectionState === CONN_STATE_CONNECTED) {
try { try {
ws.send(JSON.stringify(message)) ws.send(JSON.stringify(message))
@ -46,7 +50,7 @@ function send(message: Array<any>): void {
let clientSeq: number let clientSeq: number
let events: Record<number, any> let events: Record<number, GameEvent>
function connect( function connect(
address: string, address: string,
@ -58,7 +62,7 @@ function connect(
setConnectionState(CONN_STATE_CONNECTING) setConnectionState(CONN_STATE_CONNECTING)
return new Promise(resolve => { return new Promise(resolve => {
ws = new WebSocket(address, clientId + '|' + gameId) ws = new WebSocket(address, clientId + '|' + gameId)
ws.onopen = (e) => { ws.onopen = () => {
setConnectionState(CONN_STATE_CONNECTED) setConnectionState(CONN_STATE_CONNECTED)
send([Protocol.EV_CLIENT_INIT]) send([Protocol.EV_CLIENT_INIT])
} }
@ -82,7 +86,7 @@ function connect(
} }
} }
ws.onerror = (e) => { ws.onerror = () => {
setConnectionState(CONN_STATE_DISCONNECTED) setConnectionState(CONN_STATE_DISCONNECTED)
throw `[ 2021-05-15 onerror ]` throw `[ 2021-05-15 onerror ]`
} }
@ -116,7 +120,7 @@ function disconnect(): void {
events = {} events = {}
} }
function sendClientEvent(evt: any): void { function sendClientEvent(evt: GameEvent): void {
// when sending event, increase number of sent events // when sending event, increase number of sent events
// and add the event locally // and add the event locally
clientSeq++; clientSeq++;

View file

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

View file

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

View file

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

View file

@ -36,7 +36,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue' import { defineComponent } from 'vue'
import { GameSettings, ScoreMode } from './../../common/GameCommon' import { GameSettings, ScoreMode } from './../../common/Types'
import ResponsiveImage from './ResponsiveImage.vue' import ResponsiveImage from './ResponsiveImage.vue'
export default defineComponent({ export default defineComponent({

View file

@ -7,13 +7,21 @@ import Debug from './Debug'
import Communication from './Communication' import Communication from './Communication'
import Util from './../common/Util' import Util from './../common/Util'
import PuzzleGraphics from './PuzzleGraphics' import PuzzleGraphics from './PuzzleGraphics'
import Game, { Game as GameType, Player, Piece, EncodedGame, ReplayData, Timestamp } from './../common/GameCommon' import Game from './../common/GameCommon'
import fireworksController from './Fireworks' import fireworksController from './Fireworks'
import Protocol from '../common/Protocol' import Protocol from '../common/Protocol'
import Time from '../common/Time' import Time from '../common/Time'
import { Dim, Point } from '../common/Geometry' import { Dim, Point } from '../common/Geometry'
import { FixedLengthArray } from '../common/Types' import {
FixedLengthArray,
Game as GameType,
Player,
Piece,
EncodedGame,
ReplayData,
Timestamp,
GameEvent,
} from '../common/Types'
declare global { declare global {
interface Window { interface Window {
DEBUG?: boolean DEBUG?: boolean
@ -79,7 +87,7 @@ function addCanvasToDom(TARGET_EL: HTMLElement, canvas: HTMLCanvasElement) {
} }
function EventAdapter (canvas: HTMLCanvasElement, window: any, viewport: any) { function EventAdapter (canvas: HTMLCanvasElement, window: any, viewport: any) {
let events: Array<Array<any>> = [] let events: Array<GameEvent> = []
let KEYS_ON = true let KEYS_ON = true
@ -165,11 +173,11 @@ function EventAdapter (canvas: HTMLCanvasElement, window: any, viewport: any) {
} }
}) })
const addEvent = (event: Array<any>) => { const addEvent = (event: GameEvent) => {
events.push(event) events.push(event)
} }
const consumeAll = () => { const consumeAll = (): GameEvent[] => {
if (events.length === 0) { if (events.length === 0) {
return [] return []
} }
@ -491,7 +499,7 @@ export async function main(
} else if (MODE === MODE_REPLAY) { } else if (MODE === MODE_REPLAY) {
// no external communication for replay mode, // no external communication for replay mode,
// only the REPLAY.log is relevant // only the REPLAY.log is relevant
let inter = setInterval(() => { const inter = setInterval(() => {
const realTs = Time.timestamp() const realTs = Time.timestamp()
if (REPLAY.requesting) { if (REPLAY.requesting) {
REPLAY.lastRealTs = realTs REPLAY.lastRealTs = realTs
@ -559,7 +567,7 @@ export async function main(
} }
let _last_mouse_down: Point|null = null let _last_mouse_down: Point|null = null
const onUpdate = () => { const onUpdate = (): void => {
// handle key downs once per onUpdate // handle key downs once per onUpdate
// this will create Protocol.INPUT_EV_MOVE events if something // this will create Protocol.INPUT_EV_MOVE events if something
// relevant is pressed // relevant is pressed
@ -663,7 +671,7 @@ export async function main(
} }
} }
const onRender = async () => { const onRender = async (): Promise<void> => {
if (!RERENDER) { if (!RERENDER) {
return return
} }

View file

@ -3,10 +3,10 @@
interface GameLoopOptions { interface GameLoopOptions {
fps?: number fps?: number
slow?: number slow?: number
update: (step: number) => any update: (step: number) => void
render: (passed: number) => any render: (passed: number) => void
} }
export const run = (options: GameLoopOptions) => { export const run = (options: GameLoopOptions): void => {
const fps = options.fps || 60 const fps = options.fps || 60
const slow = options.slow || 1 const slow = options.slow || 1
const update = options.update const update = options.update

View file

@ -13,11 +13,12 @@ const log = logger('Db.ts')
* {name: 1}, // then by name ascending * {name: 1}, // then by name ascending
* ] * ]
*/ */
type OrderBy = Array<any>
type Data = Record<string, any> type Data = Record<string, any>
type WhereRaw = Record<string, any>
type Params = Array<any> type Params = Array<any>
export type WhereRaw = Record<string, any>
export type OrderBy = Array<Record<string, 1|-1>>
interface Where { interface Where {
sql: string sql: string
values: Array<any> values: Array<any>
@ -88,7 +89,7 @@ class Db {
let prop = '$nin' let prop = '$nin'
if (where[k][prop]) { if (where[k][prop]) {
if (where[k][prop].length > 0) { if (where[k][prop].length > 0) {
wheres.push(k + ' NOT IN (' + where[k][prop].map((_: any) => '?') + ')') wheres.push(k + ' NOT IN (' + where[k][prop].map(() => '?') + ')')
values.push(...where[k][prop]) values.push(...where[k][prop])
} }
continue continue
@ -96,7 +97,7 @@ class Db {
prop = '$in' prop = '$in'
if (where[k][prop]) { if (where[k][prop]) {
if (where[k][prop].length > 0) { if (where[k][prop].length > 0) {
wheres.push(k + ' IN (' + where[k][prop].map((_: any) => '?') + ')') wheres.push(k + ' IN (' + where[k][prop].map(() => '?') + ')')
values.push(...where[k][prop]) values.push(...where[k][prop])
} }
continue continue
@ -191,7 +192,7 @@ class Db {
const values = keys.map(k => data[k]) const values = keys.map(k => data[k])
const sql = 'INSERT INTO '+ table const sql = 'INSERT INTO '+ table
+ ' (' + keys.join(',') + ')' + ' (' + keys.join(',') + ')'
+ ' VALUES (' + keys.map(k => '?').join(',') + ')' + ' VALUES (' + keys.map(() => '?').join(',') + ')'
return this.run(sql, values).lastInsertRowid return this.run(sql, values).lastInsertRowid
} }

View file

@ -1,4 +1,5 @@
import GameCommon, { Game, ScoreMode, Timestamp } from './../common/GameCommon' import GameCommon from './../common/GameCommon'
import { Change, Game, Input, ScoreMode, Timestamp } from './../common/Types'
import Util from './../common/Util' import Util from './../common/Util'
import { Rng } from './../common/Rng' import { Rng } from './../common/Rng'
import GameLog from './GameLog' import GameLog from './GameLog'
@ -10,7 +11,7 @@ async function createGameObject(
gameId: string, gameId: string,
targetTiles: number, targetTiles: number,
image: PuzzleCreationImageInfo, image: PuzzleCreationImageInfo,
ts: number, ts: Timestamp,
scoreMode: ScoreMode scoreMode: ScoreMode
): Promise<Game> { ): Promise<Game> {
const seed = Util.hash(gameId + ' ' + ts) const seed = Util.hash(gameId + ' ' + ts)
@ -29,7 +30,7 @@ async function createGame(
gameId: string, gameId: string,
targetTiles: number, targetTiles: number,
image: PuzzleCreationImageInfo, image: PuzzleCreationImageInfo,
ts: number, ts: Timestamp,
scoreMode: ScoreMode scoreMode: ScoreMode
): Promise<void> { ): Promise<void> {
const gameObject = await createGameObject( const gameObject = await createGameObject(
@ -63,9 +64,9 @@ function addPlayer(gameId: string, playerId: string, ts: Timestamp): void {
function handleInput( function handleInput(
gameId: string, gameId: string,
playerId: string, playerId: string,
input: any, input: Input,
ts: number ts: Timestamp
): Array<Array<any>> { ): Array<Change> {
const idx = GameCommon.getPlayerIndexById(gameId, playerId) const idx = GameCommon.getPlayerIndexById(gameId, playerId)
const diff = ts - GameCommon.getStartTs(gameId) const diff = ts - GameCommon.getStartTs(gameId)
GameLog.log(gameId, Protocol.LOG_HANDLE_INPUT, idx, input, diff) GameLog.log(gameId, Protocol.LOG_HANDLE_INPUT, idx, input, diff)

View file

@ -1,5 +1,6 @@
import fs from 'fs' import fs from 'fs'
import GameCommon, { Piece, ScoreMode } from './../common/GameCommon' import GameCommon from './../common/GameCommon'
import { Piece, ScoreMode } from './../common/Types'
import Util, { logger } from './../common/Util' import Util, { logger } from './../common/Util'
import { Rng } from './../common/Rng' import { Rng } from './../common/Rng'
import { DATA_DIR } from './Dirs' import { DATA_DIR } from './Dirs'
@ -7,12 +8,12 @@ import Time from './../common/Time'
const log = logger('GameStorage.js') const log = logger('GameStorage.js')
const DIRTY_GAMES = {} as any const dirtyGames: Record<string, boolean> = {}
function setDirty(gameId: string): void { function setDirty(gameId: string): void {
DIRTY_GAMES[gameId] = true dirtyGames[gameId] = true
} }
function setClean(gameId: string): void { function setClean(gameId: string): void {
delete DIRTY_GAMES[gameId] delete dirtyGames[gameId]
} }
function loadGames(): void { function loadGames(): void {
@ -63,14 +64,19 @@ function loadGame(gameId: string): void {
} }
function persistGames(): void { function persistGames(): void {
for (const gameId of Object.keys(DIRTY_GAMES)) { for (const gameId of Object.keys(dirtyGames)) {
persistGame(gameId) persistGame(gameId)
} }
} }
function persistGame(gameId: string): void { function persistGame(gameId: string): void {
const game = GameCommon.get(gameId) const game = GameCommon.get(gameId)
if (game.id in DIRTY_GAMES) { if (!game) {
log.error(`[ERROR] unable to persist non existing game ${gameId}`)
return
}
if (game.id in dirtyGames) {
setClean(game.id) setClean(game.id)
} }
fs.writeFileSync(`${DATA_DIR}/${game.id}.json`, JSON.stringify({ fs.writeFileSync(`${DATA_DIR}/${game.id}.json`, JSON.stringify({

View file

@ -4,10 +4,32 @@ import exif from 'exif'
import sharp from 'sharp' import sharp from 'sharp'
import {UPLOAD_DIR, UPLOAD_URL} from './Dirs' import {UPLOAD_DIR, UPLOAD_URL} from './Dirs'
import Db from './Db' import Db, { OrderBy, WhereRaw } from './Db'
import { Dim } from '../common/Geometry' import { Dim } from '../common/Geometry'
import { logger } from '../common/Util'
import { Timestamp } from '../common/Types'
const resizeImage = async (filename: string) => { const log = logger('Images.ts')
interface Tag
{
id: number
slug: string
title: string
}
interface ImageInfo
{
id: number
filename: string
file: string
url: string
title: string
tags: Tag[]
created: Timestamp,
}
const resizeImage = async (filename: string): Promise<void> => {
if (!filename.toLowerCase().match(/\.(jpe?g|webp|png)$/)) { if (!filename.toLowerCase().match(/\.(jpe?g|webp|png)$/)) {
return return
} }
@ -30,33 +52,39 @@ const resizeImage = async (filename: string) => {
[150, 100], [150, 100],
[375, 210], [375, 210],
] ]
for (let [w,h] of sizes) { for (const [w,h] of sizes) {
console.log(w, h, imagePath) log.info(w, h, imagePath)
await sharpImg.resize(w, h, { fit: 'contain' }).toFile(`${imageOutPath}-${w}x${h}.webp`) await sharpImg
.resize(w, h, { fit: 'contain' })
.toFile(`${imageOutPath}-${w}x${h}.webp`)
} }
} }
async function getExifOrientation(imagePath: string) { async function getExifOrientation(imagePath: string): Promise<number> {
return new Promise((resolve, reject) => { return new Promise((resolve) => {
new exif.ExifImage({ image: imagePath }, function (error, exifData) { new exif.ExifImage({ image: imagePath }, (error, exifData) => {
if (error) { if (error) {
resolve(0) resolve(0)
} else { } else {
resolve(exifData.image.Orientation) resolve(exifData.image.Orientation || 0)
} }
}) })
}) })
} }
const getTags = (db: Db, imageId: number) => { const getTags = (db: Db, imageId: number): Tag[] => {
const query = ` const query = `
select * from categories c select * from categories c
inner join image_x_category ixc on c.id = ixc.category_id inner join image_x_category ixc on c.id = ixc.category_id
where ixc.image_id = ?` where ixc.image_id = ?`
return db._getMany(query, [imageId]) return db._getMany(query, [imageId]).map(row => ({
id: parseInt(row.number, 10) || 0,
slug: row.slug,
title: row.tittle,
}))
} }
const imageFromDb = (db: Db, imageId: number) => { const imageFromDb = (db: Db, imageId: number): ImageInfo => {
const i = db.get('images', { id: imageId }) const i = db.get('images', { id: imageId })
return { return {
id: i.id, id: i.id,
@ -64,21 +92,25 @@ const imageFromDb = (db: Db, imageId: number) => {
file: `${UPLOAD_DIR}/${i.filename}`, file: `${UPLOAD_DIR}/${i.filename}`,
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`, url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
title: i.title, title: i.title,
tags: getTags(db, i.id) as any[], tags: getTags(db, i.id),
created: i.created * 1000, created: i.created * 1000,
} }
} }
const allImagesFromDb = (db: Db, tagSlugs: string[], sort: string) => { const allImagesFromDb = (
const sortMap = { db: Db,
tagSlugs: string[],
orderBy: string
): ImageInfo[] => {
const orderByMap = {
alpha_asc: [{filename: 1}], alpha_asc: [{filename: 1}],
alpha_desc: [{filename: -1}], alpha_desc: [{filename: -1}],
date_asc: [{created: 1}], date_asc: [{created: 1}],
date_desc: [{created: -1}], date_desc: [{created: -1}],
} as Record<string, any> } as Record<string, OrderBy>
// TODO: .... clean up // TODO: .... clean up
const wheresRaw: Record<string, any> = {} const wheresRaw: WhereRaw = {}
if (tagSlugs.length > 0) { if (tagSlugs.length > 0) {
const c = db.getMany('categories', {slug: {'$in': tagSlugs}}) const c = db.getMany('categories', {slug: {'$in': tagSlugs}})
if (!c) { if (!c) {
@ -96,7 +128,7 @@ inner join images i on i.id = ixc.image_id ${where.sql};
} }
wheresRaw['id'] = {'$in': ids} wheresRaw['id'] = {'$in': ids}
} }
const images = db.getMany('images', wheresRaw, sortMap[sort]) const images = db.getMany('images', wheresRaw, orderByMap[orderBy])
return images.map(i => ({ return images.map(i => ({
id: i.id as number, id: i.id as number,
@ -104,12 +136,15 @@ inner join images i on i.id = ixc.image_id ${where.sql};
file: `${UPLOAD_DIR}/${i.filename}`, file: `${UPLOAD_DIR}/${i.filename}`,
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`, url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
title: i.title, title: i.title,
tags: getTags(db, i.id) as any[], tags: getTags(db, i.id),
created: i.created * 1000, created: i.created * 1000,
})) }))
} }
const allImagesFromDisk = (tags: string[], sort: string) => { const allImagesFromDisk = (
tags: string[],
sort: string
): ImageInfo[] => {
let images = fs.readdirSync(UPLOAD_DIR) let images = fs.readdirSync(UPLOAD_DIR)
.filter(f => f.toLowerCase().match(/\.(jpe?g|webp|png)$/)) .filter(f => f.toLowerCase().match(/\.(jpe?g|webp|png)$/))
.map(f => ({ .map(f => ({
@ -118,7 +153,7 @@ const allImagesFromDisk = (tags: string[], sort: string) => {
file: `${UPLOAD_DIR}/${f}`, file: `${UPLOAD_DIR}/${f}`,
url: `${UPLOAD_URL}/${encodeURIComponent(f)}`, url: `${UPLOAD_URL}/${encodeURIComponent(f)}`,
title: f.replace(/\.[a-z]+$/, ''), title: f.replace(/\.[a-z]+$/, ''),
tags: [] as any[], tags: [] as Tag[],
created: fs.statSync(`${UPLOAD_DIR}/${f}`).mtime.getTime(), created: fs.statSync(`${UPLOAD_DIR}/${f}`).mtime.getTime(),
})) }))
@ -152,7 +187,7 @@ const allImagesFromDisk = (tags: string[], sort: string) => {
} }
async function getDimensions(imagePath: string): Promise<Dim> { async function getDimensions(imagePath: string): Promise<Dim> {
let dimensions = sizeOf(imagePath) const dimensions = sizeOf(imagePath)
const orientation = await getExifOrientation(imagePath) const orientation = await getExifOrientation(imagePath)
// when image is rotated to the left or right, switch width/height // when image is rotated to the left or right, switch width/height
// https://jdhao.github.io/2019/07/31/image_rotation_exif_info/ // https://jdhao.github.io/2019/07/31/image_rotation_exif_info/

View file

@ -1,7 +1,7 @@
import Util from './../common/Util' import Util from './../common/Util'
import { Rng } from './../common/Rng' import { Rng } from './../common/Rng'
import Images from './Images' import Images from './Images'
import { EncodedPiece, EncodedPieceShape, PieceShape, Puzzle } from '../common/GameCommon' import { EncodedPiece, EncodedPieceShape, PieceShape, Puzzle } from '../common/Types'
import { Dim, Point } from '../common/Geometry' import { Dim, Point } from '../common/Geometry'
export interface PuzzleCreationImageInfo { export interface PuzzleCreationImageInfo {
@ -9,7 +9,7 @@ export interface PuzzleCreationImageInfo {
url: string url: string
} }
interface PuzzleCreationInfo { export interface PuzzleCreationInfo {
width: number width: number
height: number height: number
tileSize: number tileSize: number
@ -40,16 +40,16 @@ async function createPuzzle(
} }
const info: PuzzleCreationInfo = determinePuzzleInfo(dim, targetTiles) const info: PuzzleCreationInfo = determinePuzzleInfo(dim, targetTiles)
let tiles = new Array(info.tiles) const rawPieces = new Array(info.tiles)
for (let i = 0; i < tiles.length; i++) { for (let i = 0; i < rawPieces.length; i++) {
tiles[i] = { idx: i } rawPieces[i] = { idx: i }
} }
const shapes = determinePuzzleTileShapes(rng, info) const shapes = determinePuzzleTileShapes(rng, info)
let positions: Point[] = new Array(info.tiles) let positions: Point[] = new Array(info.tiles)
for (let tile of tiles) { for (const piece of rawPieces) {
const coord = Util.coordByTileIdx(info, tile.idx) const coord = Util.coordByPieceIdx(info, piece.idx)
positions[tile.idx] = { positions[piece.idx] = {
// instead of info.tileSize, we use info.tileDrawSize // instead of info.tileSize, we use info.tileDrawSize
// to spread the tiles a bit // to spread the tiles a bit
x: coord.x * info.tileSize * 1.5, x: coord.x * info.tileSize * 1.5,
@ -61,7 +61,7 @@ async function createPuzzle(
const tableHeight = info.height * 3 const tableHeight = info.height * 3
const off = info.tileSize * 1.5 const off = info.tileSize * 1.5
let last: Point = { const last: Point = {
x: info.width - (1 * off), x: info.width - (1 * off),
y: info.height - (2 * off), y: info.height - (2 * off),
} }
@ -71,7 +71,7 @@ async function createPuzzle(
let diffX = off let diffX = off
let diffY = 0 let diffY = 0
let index = 0 let index = 0
for (let pos of positions) { for (const pos of positions) {
pos.x = last.x pos.x = last.x
pos.y = last.y pos.y = last.y
last.x+=diffX last.x+=diffX
@ -98,9 +98,9 @@ async function createPuzzle(
// then shuffle the positions // then shuffle the positions
positions = rng.shuffle(positions) positions = rng.shuffle(positions)
const pieces: Array<EncodedPiece> = tiles.map(tile => { const pieces: Array<EncodedPiece> = rawPieces.map(piece => {
return Util.encodePiece({ return Util.encodePiece({
idx: tile.idx, // index of tile in the array idx: piece.idx, // index of tile in the array
group: 0, // if grouped with other tiles group: 0, // if grouped with other tiles
z: 0, // z index of the tile z: 0, // z index of the tile
@ -113,7 +113,7 @@ async function createPuzzle(
// physical current position of the tile (x/y in pixels) // physical current position of the tile (x/y in pixels)
// this position is the initial position only and is the // this position is the initial position only and is the
// value that changes when moving a tile // value that changes when moving a tile
pos: positions[tile.idx], pos: positions[piece.idx],
}) })
}) })
@ -171,7 +171,7 @@ function determinePuzzleTileShapes(
const shapes: Array<PieceShape> = new Array(info.tiles) const shapes: Array<PieceShape> = new Array(info.tiles)
for (let i = 0; i < info.tiles; i++) { for (let i = 0; i < info.tiles; i++) {
let coord = Util.coordByTileIdx(info, i) const coord = Util.coordByPieceIdx(info, i)
shapes[i] = { shapes[i] = {
top: coord.y === 0 ? 0 : shapes[i - info.tilesX].bottom * -1, top: coord.y === 0 ? 0 : shapes[i - info.tilesX].bottom * -1,
right: coord.x === info.tilesX - 1 ? 0 : rng.choice(tabs), right: coord.x === info.tilesX - 1 ? 0 : rng.choice(tabs),

View file

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

View file

@ -6,7 +6,6 @@ import multer from 'multer'
import Protocol from './../common/Protocol' import Protocol from './../common/Protocol'
import Util, { logger } from './../common/Util' import Util, { logger } from './../common/Util'
import Game from './Game' import Game from './Game'
import bodyParser from 'body-parser'
import v8 from 'v8' import v8 from 'v8'
import fs from 'fs' import fs from 'fs'
import GameLog from './GameLog' import GameLog from './GameLog'
@ -19,7 +18,8 @@ import {
PUBLIC_DIR, PUBLIC_DIR,
UPLOAD_DIR, UPLOAD_DIR,
} from './Dirs' } from './Dirs'
import GameCommon, { Game as GameType, GameSettings, ScoreMode } from '../common/GameCommon' import GameCommon from '../common/GameCommon'
import { Game as GameType, GameSettings, ScoreMode } from '../common/Types'
import GameStorage from './GameStorage' import GameStorage from './GameStorage'
import Db from './Db' import Db from './Db'
@ -144,7 +144,7 @@ const setImageTags = (db: Db, imageId: number, tags: string[]) => {
}) })
} }
app.post('/api/save-image', bodyParser.json(), (req, res) => { app.post('/api/save-image', express.json(), (req, res) => {
const data = req.body as SaveImageRequestData const data = req.body as SaveImageRequestData
db.update('images', { db.update('images', {
title: data.title, title: data.title,
@ -189,7 +189,7 @@ app.post('/api/upload', (req, res) => {
}) })
}) })
app.post('/newgame', bodyParser.json(), async (req, res) => { app.post('/newgame', express.json(), async (req, res) => {
const gameSettings = req.body as GameSettings const gameSettings = req.body as GameSettings
log.log(gameSettings) log.log(gameSettings)
const gameId = Util.uniqId() const gameId = Util.uniqId()
@ -212,8 +212,7 @@ app.use('/', express.static(PUBLIC_DIR))
const wss = new WebSocketServer(config.ws); const wss = new WebSocketServer(config.ws);
const notify = (data: any, sockets: Array<WebSocket>) => { const notify = (data: any, sockets: Array<WebSocket>) => {
// TODO: throttle? for (const socket of sockets) {
for (let socket of sockets) {
wss.notifyOne(data, socket) wss.notifyOne(data, socket)
} }
} }
@ -221,7 +220,7 @@ const notify = (data: any, sockets: Array<WebSocket>) => {
wss.on('close', async ({socket} : {socket: WebSocket}) => { wss.on('close', async ({socket} : {socket: WebSocket}) => {
try { try {
const proto = socket.protocol.split('|') const proto = socket.protocol.split('|')
const clientId = proto[0] // const clientId = proto[0]
const gameId = proto[1] const gameId = proto[1]
GameSockets.removeSocket(gameId, socket) GameSockets.removeSocket(gameId, socket)
} catch (e) { } catch (e) {
@ -244,7 +243,11 @@ wss.on('message', async ({socket, data} : { socket: WebSocket, data: any }) => {
const ts = Time.timestamp() const ts = Time.timestamp()
Game.addPlayer(gameId, clientId, ts) Game.addPlayer(gameId, clientId, ts)
GameSockets.addSocket(gameId, socket) GameSockets.addSocket(gameId, socket)
const game: GameType = GameCommon.get(gameId)
const game: GameType|null = GameCommon.get(gameId)
if (!game) {
throw `[game ${gameId} does not exist (anymore)... ]`
}
notify( notify(
[Protocol.EV_SERVER_INIT, Util.encodeGame(game)], [Protocol.EV_SERVER_INIT, Util.encodeGame(game)],
[socket] [socket]
@ -269,7 +272,10 @@ wss.on('message', async ({socket, data} : { socket: WebSocket, data: any }) => {
sendGame = true sendGame = true
} }
if (sendGame) { if (sendGame) {
const game: GameType = GameCommon.get(gameId) const game: GameType|null = GameCommon.get(gameId)
if (!game) {
throw `[game ${gameId} does not exist (anymore)... ]`
}
notify( notify(
[Protocol.EV_SERVER_INIT, Util.encodeGame(game)], [Protocol.EV_SERVER_INIT, Util.encodeGame(game)],
[socket] [socket]
@ -299,7 +305,7 @@ wss.listen()
const memoryUsageHuman = () => { const memoryUsageHuman = () => {
const totalHeapSize = v8.getHeapStatistics().total_available_size const totalHeapSize = v8.getHeapStatistics().total_available_size
let totalHeapSizeInGB = (totalHeapSize / 1024 / 1024 / 1024).toFixed(2) const totalHeapSizeInGB = (totalHeapSize / 1024 / 1024 / 1024).toFixed(2)
log.log(`Total heap size (bytes) ${totalHeapSize}, (GB ~${totalHeapSizeInGB})`) log.log(`Total heap size (bytes) ${totalHeapSize}, (GB ~${totalHeapSizeInGB})`)
const used = process.memoryUsage().heapUsed / 1024 / 1024 const used = process.memoryUsage().heapUsed / 1024 / 1024
@ -340,10 +346,10 @@ process.once('SIGUSR2', function () {
gracefulShutdown('SIGUSR2') gracefulShutdown('SIGUSR2')
}) })
process.once('SIGINT', function (code) { process.once('SIGINT', function () {
gracefulShutdown('SIGINT') gracefulShutdown('SIGINT')
}) })
process.once('SIGTERM', function (code) { process.once('SIGTERM', function () {
gracefulShutdown('SIGTERM') gracefulShutdown('SIGTERM')
}) })