type hints galore!

This commit is contained in:
Zutatensuppe 2021-05-29 15:36:03 +02:00
parent 7b1f270587
commit 46f3fc7480
17 changed files with 700 additions and 667 deletions

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">
<title>🧩 jigsaw.hyottoko.club</title>
<script type="module" crossorigin src="/assets/index.8f906b9e.js"></script>
<script type="module" crossorigin src="/assets/index.643c957c.js"></script>
<link rel="modulepreload" href="/assets/vendor.b622ee49.js">
<link rel="stylesheet" href="/assets/index.f7304069.css">
</head>

File diff suppressed because it is too large Load diff

View file

@ -7,22 +7,22 @@ const log = logger('fix_tiles.js')
function fix_tiles(gameId) {
GameStorage.loadGame(gameId)
let changed = false
const tiles = GameCommon.getTilesSortedByZIndex(gameId)
const tiles = GameCommon.getPiecesSortedByZIndex(gameId)
for (let tile of tiles) {
if (tile.owner === -1) {
const p = GameCommon.getFinalTilePos(gameId, tile.idx)
const p = GameCommon.getFinalPiecePos(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.setTile(gameId, tile.idx, tile)
GameCommon.setPiece(gameId, tile.idx, tile)
changed = true
}
} else if (tile.owner !== 0) {
tile.owner = 0
log.log('unowning tile', tile.idx)
GameCommon.setTile(gameId, tile.idx, tile)
GameCommon.setPiece(gameId, tile.idx, tile)
changed = true
}
}

View file

@ -1,14 +1,50 @@
import Geometry, { Point, Rect } from './Geometry'
import Protocol from './Protocol'
import { Rng } from './Rng'
import { Rng, RngSerialized } from './Rng'
import Time from './Time'
import { FixedLengthArray } from './Types'
import Util from './Util'
export type Timestamp = number
export type EncodedPlayer = Array<any>
export type EncodedPiece = 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
@ -20,7 +56,7 @@ interface GameRng {
type?: string
}
interface Game {
export interface Game {
id: string
players: Array<EncodedPlayer>
puzzle: Puzzle
@ -112,7 +148,7 @@ export interface Player {
color: string|null
bgcolor: string|null
points: number
ts: number
ts: Timestamp
}
interface EvtInfo {
@ -134,7 +170,7 @@ function exists(gameId: string) {
return (!!GAMES[gameId]) || false
}
function __createPlayerObject(id: string, ts: number): Player {
function __createPlayerObject(id: string, ts: Timestamp): Player {
return {
id: id,
x: 0,
@ -191,8 +227,8 @@ function setPlayer(
}
}
function setTile(gameId: string, tileIdx: number, tile: Piece): void {
GAMES[gameId].puzzle.tiles[tileIdx] = Util.encodeTile(tile)
function setPiece(gameId: string, pieceIdx: number, piece: Piece): void {
GAMES[gameId].puzzle.tiles[pieceIdx] = Util.encodePiece(piece)
}
function setPuzzleData(gameId: string, data: PuzzleData): void {
@ -214,7 +250,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: number): void {
function addPlayer(gameId: string, playerId: string, ts: Timestamp): void {
if (!playerExists(gameId, playerId)) {
setPlayer(gameId, playerId, __createPlayerObject(playerId, ts))
} else {
@ -261,7 +297,7 @@ function get(gameId: string) {
return GAMES[gameId]
}
function getTileCount(gameId: string): number {
function getPieceCount(gameId: string): number {
return GAMES[gameId].puzzle.tiles.length
}
@ -278,22 +314,22 @@ function getScoreMode(gameId: string): ScoreMode {
}
function isFinished(gameId: string): boolean {
return getFinishedTileCount(gameId) === getTileCount(gameId)
return getFinishedPiecesCount(gameId) === getPieceCount(gameId)
}
function getFinishedTileCount(gameId: string): number {
function getFinishedPiecesCount(gameId: string): number {
let count = 0
for (let t of GAMES[gameId].puzzle.tiles) {
if (Util.decodeTile(t).owner === -1) {
if (Util.decodePiece(t).owner === -1) {
count++
}
}
return count
}
function getTilesSortedByZIndex(gameId: string): Piece[] {
const tiles = GAMES[gameId].puzzle.tiles.map(Util.decodeTile)
return tiles.sort((t1, t2) => t1.z - t2.z)
function getPiecesSortedByZIndex(gameId: string): Piece[] {
const pieces = GAMES[gameId].puzzle.tiles.map(Util.decodePiece)
return pieces.sort((t1, t2) => t1.z - t2.z)
}
function changePlayer(
@ -320,25 +356,25 @@ function changeData(gameId: string, change: any): void {
}
}
function changeTile(gameId: string, tileIdx: number, change: any): void {
function changeTile(gameId: string, pieceIdx: number, change: any): void {
for (let k of Object.keys(change)) {
const tile = Util.decodeTile(GAMES[gameId].puzzle.tiles[tileIdx])
const piece = Util.decodePiece(GAMES[gameId].puzzle.tiles[pieceIdx])
// @ts-ignore
tile[k] = change[k]
GAMES[gameId].puzzle.tiles[tileIdx] = Util.encodeTile(tile)
piece[k] = change[k]
GAMES[gameId].puzzle.tiles[pieceIdx] = Util.encodePiece(piece)
}
}
const getTile = (gameId: string, tileIdx: number): Piece => {
return Util.decodeTile(GAMES[gameId].puzzle.tiles[tileIdx])
const getPiece = (gameId: string, pieceIdx: number): Piece => {
return Util.decodePiece(GAMES[gameId].puzzle.tiles[pieceIdx])
}
const getTileGroup = (gameId: string, tileIdx: number): number => {
const tile = getTile(gameId, tileIdx)
const getPieceGroup = (gameId: string, tileIdx: number): number => {
const tile = getPiece(gameId, tileIdx)
return tile.group
}
const getFinalTilePos = (gameId: string, tileIdx: number): Point => {
const getFinalPiecePos = (gameId: string, tileIdx: number): Point => {
const info = GAMES[gameId].puzzle.info
const boardPos = {
x: (info.table.width - info.width) / 2,
@ -348,8 +384,8 @@ const getFinalTilePos = (gameId: string, tileIdx: number): Point => {
return Geometry.pointAdd(boardPos, srcPos)
}
const getTilePos = (gameId: string, tileIdx: number): Point => {
const tile = getTile(gameId, tileIdx)
const getPiecePos = (gameId: string, tileIdx: number): Point => {
const tile = getPiece(gameId, tileIdx)
return tile.pos
}
@ -368,9 +404,9 @@ const getBounds = (gameId: string): Rect => {
}
}
const getTileBounds = (gameId: string, tileIdx: number): Rect => {
const s = getTileSize(gameId)
const tile = getTile(gameId, tileIdx)
const getPieceBounds = (gameId: string, tileIdx: number): Rect => {
const s = getPieceSize(gameId)
const tile = getPiece(gameId, tileIdx)
return {
x: tile.pos.x,
y: tile.pos.y,
@ -380,13 +416,13 @@ const getTileBounds = (gameId: string, tileIdx: number): Rect => {
}
const getTileZIndex = (gameId: string, tileIdx: number): number => {
const tile = getTile(gameId, tileIdx)
const tile = getPiece(gameId, tileIdx)
return tile.z
}
const getFirstOwnedTileIdx = (gameId: string, playerId: string): number => {
const getFirstOwnedPieceIdx = (gameId: string, playerId: string): number => {
for (let t of GAMES[gameId].puzzle.tiles) {
const tile = Util.decodeTile(t)
const tile = Util.decodePiece(t)
if (tile.owner === playerId) {
return tile.idx
}
@ -394,20 +430,20 @@ const getFirstOwnedTileIdx = (gameId: string, playerId: string): number => {
return -1
}
const getFirstOwnedTile = (gameId: string, playerId: string): EncodedPiece|null => {
const idx = getFirstOwnedTileIdx(gameId, playerId)
const getFirstOwnedPiece = (gameId: string, playerId: string): EncodedPiece|null => {
const idx = getFirstOwnedPieceIdx(gameId, playerId)
return idx < 0 ? null : GAMES[gameId].puzzle.tiles[idx]
}
const getTileDrawOffset = (gameId: string): number => {
const getPieceDrawOffset = (gameId: string): number => {
return GAMES[gameId].puzzle.info.tileDrawOffset
}
const getTileDrawSize = (gameId: string): number => {
const getPieceDrawSize = (gameId: string): number => {
return GAMES[gameId].puzzle.info.tileDrawSize
}
const getTileSize = (gameId: string): number => {
const getPieceSize = (gameId: string): number => {
return GAMES[gameId].puzzle.info.tileSize
}
@ -472,7 +508,7 @@ const setTilesZIndex = (gameId: string, tileIdxs: Array<number>, zIndex: number)
}
const moveTileDiff = (gameId: string, tileIdx: number, diff: Point): void => {
const oldPos = getTilePos(gameId, tileIdx)
const oldPos = getPiecePos(gameId, tileIdx)
const pos = Geometry.pointAdd(oldPos, diff)
changeTile(gameId, tileIdx, { pos })
}
@ -482,21 +518,21 @@ const moveTilesDiff = (
tileIdxs: Array<number>,
diff: Point
): void => {
const tileDrawSize = getTileDrawSize(gameId)
const drawSize = getPieceDrawSize(gameId)
const bounds = getBounds(gameId)
const cappedDiff = diff
for (let tileIdx of tileIdxs) {
const t = getTile(gameId, tileIdx)
const t = getPiece(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 + tileDrawSize + diff.x > bounds.x + bounds.w) {
cappedDiff.x = Math.min(bounds.x + bounds.w - t.pos.x + tileDrawSize, 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)
}
if (t.pos.y + diff.y < bounds.y) {
cappedDiff.y = Math.max(bounds.y - t.pos.y, 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)
} 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)
}
}
@ -522,52 +558,52 @@ const setTilesOwner = (
}
// get all grouped tiles for a tile
function getGroupedTileIdxs(gameId: string, tileIdx: number): number[] {
const tiles = GAMES[gameId].puzzle.tiles
const tile = Util.decodeTile(tiles[tileIdx])
function getGroupedPieceIdxs(gameId: string, pieceIdx: number): number[] {
const pieces = GAMES[gameId].puzzle.tiles
const piece = Util.decodePiece(pieces[pieceIdx])
const grouped = []
if (tile.group) {
for (let other of tiles) {
const otherTile = Util.decodeTile(other)
if (otherTile.group === tile.group) {
grouped.push(otherTile.idx)
if (piece.group) {
for (let other of pieces) {
const otherPiece = Util.decodePiece(other)
if (otherPiece.group === piece.group) {
grouped.push(otherPiece.idx)
}
}
} else {
grouped.push(tile.idx)
grouped.push(piece.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 freeTileIdxByPos = (gameId: string, pos: Point): number => {
const freePieceIdxByPos = (gameId: string, pos: Point): number => {
let info = GAMES[gameId].puzzle.info
let tiles = GAMES[gameId].puzzle.tiles
let pieces = GAMES[gameId].puzzle.tiles
let maxZ = -1
let tileIdx = -1
for (let idx = 0; idx < tiles.length; idx++) {
const tile = Util.decodeTile(tiles[idx])
if (tile.owner !== 0) {
let pieceIdx = -1
for (let idx = 0; idx < pieces.length; idx++) {
const piece = Util.decodePiece(pieces[idx])
if (piece.owner !== 0) {
continue
}
const collisionRect: Rect = {
x: tile.pos.x,
y: tile.pos.y,
x: piece.pos.x,
y: piece.pos.y,
w: info.tileSize,
h: info.tileSize,
}
if (Geometry.pointInBounds(pos, collisionRect)) {
if (maxZ === -1 || tile.z > maxZ) {
maxZ = tile.z
tileIdx = idx
if (maxZ === -1 || piece.z > maxZ) {
maxZ = piece.z
pieceIdx = idx
}
}
}
return tileIdx
return pieceIdx
}
const getPlayerBgColor = (gameId: string, playerId: string): string|null => {
@ -596,8 +632,8 @@ const areGrouped = (
tileIdx1: number,
tileIdx2: number
): boolean => {
const g1 = getTileGroup(gameId, tileIdx1)
const g2 = getTileGroup(gameId, tileIdx2)
const g1 = getPieceGroup(gameId, tileIdx1)
const g2 = getPieceGroup(gameId, tileIdx2)
return !!(g1 && g1 === g2)
}
@ -643,7 +679,7 @@ function handleInput(
const _tileChange = (tileIdx: number): void => {
changes.push([
Protocol.CHANGE_TILE,
Util.encodeTile(getTile(gameId, tileIdx)),
Util.encodePiece(getPiece(gameId, tileIdx)),
])
}
@ -671,8 +707,8 @@ function handleInput(
tileIdx2: number
): void => {
const tiles = GAMES[gameId].puzzle.tiles
const group1 = getTileGroup(gameId, tileIdx1)
const group2 = getTileGroup(gameId, tileIdx2)
const group1 = getPieceGroup(gameId, tileIdx1)
const group2 = getPieceGroup(gameId, tileIdx2)
let group
const searchGroups = []
@ -701,10 +737,10 @@ function handleInput(
// TODO: strange
if (searchGroups.length > 0) {
for (const t of tiles) {
const tile = Util.decodeTile(t)
if (searchGroups.includes(tile.group)) {
changeTile(gameId, tile.idx, { group })
_tileChange(tile.idx)
const piece = Util.decodePiece(t)
if (searchGroups.includes(piece.group)) {
changeTile(gameId, piece.idx, { group })
_tileChange(piece.idx)
}
}
}
@ -732,12 +768,12 @@ function handleInput(
_playerChange()
evtInfo._last_mouse_down = pos
const tileIdxAtPos = freeTileIdxByPos(gameId, pos)
const tileIdxAtPos = freePieceIdxByPos(gameId, pos)
if (tileIdxAtPos >= 0) {
let maxZ = getMaxZIndex(gameId) + 1
changeData(gameId, { maxZ })
_dataChange()
const tileIdxs = getGroupedTileIdxs(gameId, tileIdxAtPos)
const tileIdxs = getGroupedPieceIdxs(gameId, tileIdxAtPos)
setTilesZIndex(gameId, tileIdxs, getMaxZIndex(gameId))
setTilesOwner(gameId, tileIdxs, playerId)
_tileChanges(tileIdxs)
@ -754,7 +790,7 @@ function handleInput(
changePlayer(gameId, playerId, {x, y, ts})
_playerChange()
} else {
let tileIdx = getFirstOwnedTileIdx(gameId, playerId)
let tileIdx = getFirstOwnedPieceIdx(gameId, playerId)
if (tileIdx >= 0) {
// player is moving a tile (and hand)
changePlayer(gameId, playerId, {x, y, ts})
@ -762,11 +798,11 @@ function handleInput(
// check if pos is on the tile, otherwise dont move
// (mouse could be out of table, but tile stays on it)
const tileIdxs = getGroupedTileIdxs(gameId, tileIdx)
const tileIdxs = getGroupedPieceIdxs(gameId, tileIdx)
let anyOk = Geometry.pointInBounds(pos, getBounds(gameId))
&& Geometry.pointInBounds(evtInfo._last_mouse_down, getBounds(gameId))
for (let idx of tileIdxs) {
const bounds = getTileBounds(gameId, idx)
const bounds = getPieceBounds(gameId, idx)
if (Geometry.pointInBounds(pos, bounds)) {
anyOk = true
break
@ -799,16 +835,16 @@ function handleInput(
evtInfo._last_mouse_down = null
let tileIdx = getFirstOwnedTileIdx(gameId, playerId)
let tileIdx = getFirstOwnedPieceIdx(gameId, playerId)
if (tileIdx >= 0) {
// drop the tile(s)
let tileIdxs = getGroupedTileIdxs(gameId, tileIdx)
let tileIdxs = getGroupedPieceIdxs(gameId, tileIdx)
setTilesOwner(gameId, tileIdxs, 0)
_tileChanges(tileIdxs)
// Check if the tile was dropped near the final location
let tilePos = getTilePos(gameId, tileIdx)
let finalPos = getFinalTilePos(gameId, tileIdx)
let tilePos = getPiecePos(gameId, tileIdx)
let finalPos = getFinalPiecePos(gameId, tileIdx)
if (Geometry.pointDistance(finalPos, tilePos) < puzzle.info.snapDistance) {
let diff = Geometry.pointSub(finalPos, tilePos)
// Snap the tile to the final destination
@ -829,7 +865,7 @@ function handleInput(
_playerChange()
// check if the puzzle is finished
if (getFinishedTileCount(gameId) === getTileCount(gameId)) {
if (getFinishedPiecesCount(gameId) === getPieceCount(gameId)) {
changeData(gameId, { finished: ts })
_dataChange()
}
@ -848,17 +884,17 @@ function handleInput(
if (areGrouped(gameId, tileIdx, otherTileIdx)) {
return false
}
const tilePos = getTilePos(gameId, tileIdx)
const tilePos = getPiecePos(gameId, tileIdx)
const dstPos = Geometry.pointAdd(
getTilePos(gameId, otherTileIdx),
getPiecePos(gameId, otherTileIdx),
{x: off[0] * info.tileSize, y: off[1] * info.tileSize}
)
if (Geometry.pointDistance(tilePos, dstPos) < info.snapDistance) {
let diff = Geometry.pointSub(dstPos, tilePos)
let tileIdxs = getGroupedTileIdxs(gameId, tileIdx)
let tileIdxs = getGroupedPieceIdxs(gameId, tileIdx)
moveTilesDiff(gameId, tileIdxs, diff)
groupTiles(gameId, tileIdx, otherTileIdx)
tileIdxs = getGroupedTileIdxs(gameId, tileIdx)
tileIdxs = getGroupedPieceIdxs(gameId, tileIdx)
const zIndex = getMaxZIndexByTileIdxs(gameId, tileIdxs)
setTilesZIndex(gameId, tileIdxs, zIndex)
_tileChanges(tileIdxs)
@ -868,7 +904,7 @@ function handleInput(
}
let snapped = false
for (let tileIdxTmp of getGroupedTileIdxs(gameId, tileIdx)) {
for (let tileIdxTmp of getGroupedPieceIdxs(gameId, tileIdx)) {
let othersIdxs = getSurroundingTilesByIdx(gameId, tileIdxTmp)
if (
check(gameId, tileIdxTmp, othersIdxs[0], [0, 1]) // top
@ -916,15 +952,14 @@ function handleInput(
}
export default {
__createPlayerObject,
setGame,
exists,
playerExists,
getActivePlayers,
getIdlePlayers,
addPlayer,
getFinishedTileCount,
getTileCount,
getFinishedPiecesCount,
getPieceCount,
getImageUrl,
setImageUrl,
get,
@ -936,7 +971,7 @@ export default {
getPlayerIdByIndex,
changePlayer,
setPlayer,
setTile,
setPiece,
setPuzzleData,
getTableWidth,
getTableHeight,
@ -944,11 +979,11 @@ export default {
getRng,
getPuzzleWidth,
getPuzzleHeight,
getTilesSortedByZIndex,
getFirstOwnedTile,
getTileDrawOffset,
getTileDrawSize,
getFinalTilePos,
getPiecesSortedByZIndex,
getFirstOwnedPiece,
getPieceDrawOffset,
getPieceDrawSize,
getFinalPiecePos,
getStartTs,
getFinishTs,
handleInput,

View file

@ -1,4 +1,4 @@
interface RngSerialized {
export interface RngSerialized {
rand_high: number,
rand_low: number,
}

6
src/common/Types.ts Normal file
View file

@ -0,0 +1,6 @@
// @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> > }

View file

@ -1,4 +1,14 @@
import { EncodedPiece, EncodedPieceShape, EncodedPlayer, Piece, PieceShape, Player } from './GameCommon'
import {
EncodedGame,
EncodedPiece,
EncodedPieceShape,
EncodedPlayer,
Game,
Piece,
PieceShape,
Player,
ScoreMode
} from './GameCommon'
import { Point } from './Geometry'
import { Rng } from './Rng'
@ -58,11 +68,11 @@ function decodeShape(data: EncodedPieceShape): PieceShape {
}
}
function encodeTile(data: Piece): EncodedPiece {
function encodePiece(data: Piece): EncodedPiece {
return [data.idx, data.pos.x, data.pos.y, data.z, data.owner, data.group]
}
function decodeTile(data: EncodedPiece): Piece {
function decodePiece(data: EncodedPiece): Piece {
return {
idx: data[0],
pos: {
@ -103,25 +113,19 @@ function decodePlayer(data: EncodedPlayer): Player {
}
}
function encodeGame(data: any): Array<any> {
if (Array.isArray(data)) {
return data
}
function encodeGame(data: Game): EncodedGame {
return [
data.id,
data.rng.type,
data.rng.type || '',
Rng.serialize(data.rng.obj),
data.puzzle,
data.players,
data.evtInfos,
data.scoreMode,
data.scoreMode || ScoreMode.FINAL,
]
}
function decodeGame(data: any) {
if (!Array.isArray(data)) {
return data
}
function decodeGame(data: EncodedGame): Game {
return {
id: data[0],
rng: {
@ -174,8 +178,8 @@ export default {
encodeShape,
decodeShape,
encodeTile,
decodeTile,
encodePiece,
decodePiece,
encodePlayer,
decodePlayer,

View file

@ -1,6 +1,7 @@
"use strict"
import { logger } from '../common/Util'
import { EncodedGame, ReplayData } from '../common/GameCommon'
import Util, { logger } from '../common/Util'
import Protocol from './../common/Protocol'
const log = logger('Communication.js')
@ -51,7 +52,7 @@ function connect(
address: string,
gameId: string,
clientId: string
): Promise<any> {
): Promise<EncodedGame> {
clientSeq = 0
events = {}
setConnectionState(CONN_STATE_CONNECTING)
@ -100,9 +101,10 @@ async function requestReplayData(
gameId: string,
offset: number,
size: number
): Promise<{ log: Array<any>, game: any }> {
const res = await fetch(`/api/replay-data?gameId=${gameId}&offset=${offset}&size=${size}`)
const json: { log: Array<any>, game: any } = await res.json()
): Promise<ReplayData> {
const args = { gameId, offset, size }
const res = await fetch(`/api/replay-data${Util.asQueryArgs(args)}`)
const json: ReplayData = await res.json()
return json
}

View file

@ -94,7 +94,7 @@ async function createPuzzleTileBitmaps(
const ctx2 = c2.getContext('2d') as CanvasRenderingContext2D
for (const t of tiles) {
const tile = Util.decodeTile(t)
const tile = Util.decodePiece(t)
const srcRect = srcRectByIdx(info, tile.idx)
const path = pathForShape(Util.decodeShape(info.shapes[tile.idx]))

View file

@ -7,11 +7,12 @@ import Debug from './Debug'
import Communication from './Communication'
import Util from './../common/Util'
import PuzzleGraphics from './PuzzleGraphics'
import Game, { Player, Piece } from './../common/GameCommon'
import Game, { Game as GameType, Player, Piece, EncodedGame, ReplayData, Timestamp } from './../common/GameCommon'
import fireworksController from './Fireworks'
import Protocol from '../common/Protocol'
import Time from '../common/Time'
import { Dim, Point } from '../common/Geometry'
import { FixedLengthArray } from '../common/Types'
declare global {
interface Window {
@ -19,13 +20,6 @@ declare global {
}
}
// @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')
@ -50,15 +44,17 @@ interface Hud {
interface Replay {
final: boolean
requesting: boolean
log: Array<any>
logPointer: number,
logIdx: number
log: Array<any> // current log entries
logPointer: number // pointer to current item in the log array
speeds: Array<number>
speedIdx: number
paused: boolean
lastRealTs: number
lastGameTs: number
gameStartTs: number
//
dataOffset: number
dataSize: number
}
const shouldDrawPiece = (piece: Piece) => {
@ -267,51 +263,61 @@ export async function main(
requesting: true,
log: [],
logPointer: 0,
logIdx: 0,
speeds: [0.5, 1, 2, 5, 10, 20, 50, 100, 250, 500],
speedIdx: 1,
paused: false,
lastRealTs: 0,
lastGameTs: 0,
gameStartTs: 0,
dataOffset: 0,
dataSize: 10000,
}
Communication.onConnectionStateChange((state) => {
HUD.setConnectionState(state)
})
const queryNextReplayBatch = async (
gameId: string
): Promise<ReplayData> => {
REPLAY.requesting = true
const replay: ReplayData = await Communication.requestReplayData(
gameId,
REPLAY.dataOffset,
REPLAY.dataSize
)
REPLAY.dataOffset += REPLAY.dataSize
REPLAY.requesting = false
return replay
}
const getNextReplayBatch = async (
gameId: string,
offset: number,
size: number
gameId: string
) => {
const replay: {
game: any,
log: Array<any>
} = await Communication.requestReplayData(gameId, offset, size)
const replay: ReplayData = await queryNextReplayBatch(gameId)
// 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 < 10000) {
if (replay.log.length < REPLAY.dataSize) {
REPLAY.final = true
}
REPLAY.requesting = false
}
let TIME: () => number = () => 0
const connect = async () => {
if (MODE === MODE_PLAY) {
const game = await Communication.connect(wsAddress, gameId, clientId)
const gameObject = Util.decodeGame(game)
const game: EncodedGame = await Communication.connect(wsAddress, gameId, clientId)
const gameObject: GameType = Util.decodeGame(game)
Game.setGame(gameObject.id, gameObject)
TIME = () => Time.timestamp()
} else if (MODE === MODE_REPLAY) {
const replay: {
game: any,
log: Array<any>
} = await Communication.requestReplayData(gameId, 0, 10000)
const gameObject = Util.decodeGame(replay.game)
const replay: ReplayData = await queryNextReplayBatch(gameId)
if (!replay.game) {
throw '[ 2021-05-29 no game received ]'
}
const gameObject: GameType = Util.decodeGame(replay.game)
Game.setGame(gameObject.id, gameObject)
REPLAY.requesting = false
REPLAY.log = replay.log
@ -329,8 +335,8 @@ export async function main(
await connect()
const TILE_DRAW_OFFSET = Game.getTileDrawOffset(gameId)
const TILE_DRAW_SIZE = Game.getTileDrawSize(gameId)
const PIECE_DRAW_OFFSET = Game.getPieceDrawOffset(gameId)
const PIECE_DRAW_SIZE = Game.getPieceDrawSize(gameId)
const PUZZLE_WIDTH = Game.getPuzzleWidth(gameId)
const PUZZLE_HEIGHT = Game.getPuzzleHeight(gameId)
const TABLE_WIDTH = Game.getTableWidth(gameId)
@ -345,8 +351,8 @@ export async function main(
h: PUZZLE_HEIGHT,
}
const PIECE_DIM = {
w: TILE_DRAW_SIZE,
h: TILE_DRAW_SIZE,
w: PIECE_DRAW_SIZE,
h: PIECE_DRAW_SIZE,
}
const bitmaps = await PuzzleGraphics.loadPuzzleBitmaps(Game.getPuzzle(gameId))
@ -380,8 +386,8 @@ export async function main(
}
updateTimerElements()
HUD.setPiecesDone(Game.getFinishedTileCount(gameId))
HUD.setPiecesTotal(Game.getTileCount(gameId))
HUD.setPiecesDone(Game.getFinishedPiecesCount(gameId))
HUD.setPiecesTotal(Game.getPieceCount(gameId))
const ts = TIME()
HUD.setActivePlayers(Game.getActivePlayers(gameId, ts))
HUD.setIdlePlayers(Game.getIdlePlayers(gameId, ts))
@ -470,8 +476,8 @@ export async function main(
}
} break;
case Protocol.CHANGE_TILE: {
const t = Util.decodeTile(changeData)
Game.setTile(gameId, t.idx, t)
const t = Util.decodePiece(changeData)
Game.setPiece(gameId, t.idx, t)
RERENDER = true
} break;
case Protocol.CHANGE_DATA: {
@ -494,8 +500,7 @@ export async function main(
if (REPLAY.logPointer + 1 >= REPLAY.log.length) {
REPLAY.lastRealTs = realTs
REPLAY.requesting = true
getNextReplayBatch(gameId, REPLAY.logIdx, 10000)
getNextReplayBatch(gameId)
return
}
@ -519,7 +524,7 @@ export async function main(
}
const logEntry = REPLAY.log[nextIdx]
const nextTs = REPLAY.gameStartTs + logEntry[logEntry.length - 1]
const nextTs: Timestamp = REPLAY.gameStartTs + logEntry[logEntry.length - 1]
if (nextTs > maxGameTs) {
break
}
@ -546,7 +551,6 @@ export async function main(
RERENDER = true
}
REPLAY.logPointer = nextIdx
REPLAY.logIdx++
} while (true)
REPLAY.lastRealTs = realTs
REPLAY.lastGameTs = maxGameTs
@ -572,7 +576,7 @@ export async function main(
RERENDER = true
viewport.move(diffX, diffY)
} else if (type === Protocol.INPUT_EV_MOUSE_MOVE) {
if (_last_mouse_down && !Game.getFirstOwnedTile(gameId, clientId)) {
if (_last_mouse_down && !Game.getFirstOwnedPiece(gameId, clientId)) {
// move the cam
const pos = { x: evt[1], y: evt[2] }
const mouse = viewport.worldToViewport(pos)
@ -692,7 +696,7 @@ export async function main(
// DRAW TILES
// ---------------------------------------------------------------
const tiles = Game.getTilesSortedByZIndex(gameId)
const tiles = Game.getPiecesSortedByZIndex(gameId)
if (window.DEBUG) Debug.checkpoint('get tiles done')
dim = viewport.worldDimToViewportRaw(PIECE_DIM)
@ -702,8 +706,8 @@ export async function main(
}
bmp = bitmaps[tile.idx]
pos = viewport.worldToViewportRaw({
x: TILE_DRAW_OFFSET + tile.pos.x,
y: TILE_DRAW_OFFSET + tile.pos.y,
x: PIECE_DRAW_OFFSET + tile.pos.x,
y: PIECE_DRAW_OFFSET + tile.pos.y,
})
ctx.drawImage(bmp,
0, 0, bmp.width, bmp.height,
@ -743,7 +747,7 @@ export async function main(
// ---------------------------------------------------------------
HUD.setActivePlayers(Game.getActivePlayers(gameId, ts))
HUD.setIdlePlayers(Game.getIdlePlayers(gameId, ts))
HUD.setPiecesDone(Game.getFinishedTileCount(gameId))
HUD.setPiecesDone(Game.getFinishedPiecesCount(gameId))
if (window.DEBUG) Debug.checkpoint('HUD done')
// ---------------------------------------------------------------

View file

@ -1,18 +1,18 @@
import GameCommon, { ScoreMode } from './../common/GameCommon'
import GameCommon, { Game, ScoreMode, Timestamp } from './../common/GameCommon'
import Util from './../common/Util'
import { Rng } from './../common/Rng'
import GameLog from './GameLog'
import { createPuzzle } from './Puzzle'
import { createPuzzle, PuzzleCreationImageInfo } from './Puzzle'
import Protocol from './../common/Protocol'
import GameStorage from './GameStorage'
async function createGameObject(
gameId: string,
targetTiles: number,
image: { file: string, url: string },
image: PuzzleCreationImageInfo,
ts: number,
scoreMode: ScoreMode
) {
): Promise<Game> {
const seed = Util.hash(gameId + ' ' + ts)
const rng = new Rng(seed)
return {
@ -28,11 +28,17 @@ async function createGameObject(
async function createGame(
gameId: string,
targetTiles: number,
image: { file: string, url: string },
image: PuzzleCreationImageInfo,
ts: number,
scoreMode: ScoreMode
): Promise<void> {
const gameObject = await createGameObject(gameId, targetTiles, image, ts, scoreMode)
const gameObject = await createGameObject(
gameId,
targetTiles,
image,
ts,
scoreMode
)
GameLog.create(gameId)
GameLog.log(gameId, Protocol.LOG_HEADER, 1, targetTiles, image, ts, scoreMode)
@ -41,7 +47,7 @@ async function createGame(
GameStorage.setDirty(gameId)
}
function addPlayer(gameId: string, playerId: string, ts: number): void {
function addPlayer(gameId: string, playerId: string, ts: Timestamp): void {
const idx = GameCommon.getPlayerIndexById(gameId, playerId)
const diff = ts - GameCommon.getStartTs(gameId)
if (idx === -1) {
@ -74,14 +80,4 @@ export default {
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

@ -8,19 +8,19 @@ const log = logger('GameLog.js')
const filename = (gameId: string) => `${DATA_DIR}/log_${gameId}.log`
const create = (gameId: string) => {
const create = (gameId: string): void => {
const file = filename(gameId)
if (!fs.existsSync(file)) {
fs.appendFileSync(file, '')
}
}
const exists = (gameId: string) => {
const exists = (gameId: string): boolean => {
const file = filename(gameId)
return fs.existsSync(file)
}
const _log = (gameId: string, ...args: Array<any>) => {
const _log = (gameId: string, ...args: Array<any>): void => {
const file = filename(gameId)
if (!fs.existsSync(file)) {
return

View file

@ -41,7 +41,7 @@ function loadGame(gameId: string): void {
}
if (typeof game.puzzle.data.finished === 'undefined') {
const unfinished = game.puzzle.tiles
.map(Util.decodeTile)
.map(Util.decodePiece)
.find((t: Piece) => t.owner !== -1)
game.puzzle.data.finished = unfinished ? 0 : Time.timestamp()
}

View file

@ -2,7 +2,12 @@ import Util from './../common/Util'
import { Rng } from './../common/Rng'
import Images from './Images'
import { EncodedPiece, EncodedPieceShape, PieceShape, Puzzle } from '../common/GameCommon'
import { Point } from '../common/Geometry'
import { Dim, Point } from '../common/Geometry'
export interface PuzzleCreationImageInfo {
file: string
url: string
}
interface PuzzleCreationInfo {
width: number
@ -22,7 +27,7 @@ const TILE_SIZE = 64
async function createPuzzle(
rng: Rng,
targetTiles: number,
image: { file: string, url: string },
image: PuzzleCreationImageInfo,
ts: number
): Promise<Puzzle> {
const imagePath = image.file
@ -33,11 +38,7 @@ async function createPuzzle(
if (!dim.w || !dim.h) {
throw `[ 2021-05-16 invalid dimension for path ${imagePath} ]`
}
const info: PuzzleCreationInfo = determinePuzzleInfo(
dim.w,
dim.h,
targetTiles
)
const info: PuzzleCreationInfo = determinePuzzleInfo(dim, targetTiles)
let tiles = new Array(info.tiles)
for (let i = 0; i < tiles.length; i++) {
@ -98,7 +99,7 @@ async function createPuzzle(
positions = rng.shuffle(positions)
const pieces: Array<EncodedPiece> = tiles.map(tile => {
return Util.encodeTile({
return Util.encodePiece({
idx: tile.idx, // index of tile in the array
group: 0, // if grouped with other tiles
z: 0, // z index of the tile
@ -181,9 +182,12 @@ function determinePuzzleTileShapes(
return shapes.map(Util.encodeShape)
}
const determineTilesXY = (w: number, h: number, targetTiles: number) => {
const w_ = w < h ? (w * h) : (w * w)
const h_ = w < h ? (h * h) : (w * h)
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)
let size = 0
let tiles = 0
do {
@ -198,11 +202,10 @@ const determineTilesXY = (w: number, h: number, targetTiles: number) => {
}
const determinePuzzleInfo = (
w: number,
h: number,
dim: Dim,
targetTiles: number
): PuzzleCreationInfo => {
const {tilesX, tilesY} = determineTilesXY(w, h, targetTiles)
const {tilesX, tilesY} = determineTilesXY(dim, targetTiles)
const tiles = tilesX * tilesY
const tileSize = TILE_SIZE
const width = tilesX * tileSize

View file

@ -19,7 +19,7 @@ import {
PUBLIC_DIR,
UPLOAD_DIR,
} from './Dirs'
import { GameSettings, ScoreMode } from '../common/GameCommon'
import GameCommon, { Game as GameType, GameSettings, ScoreMode } from '../common/GameCommon'
import GameStorage from './GameStorage'
import Db from './Db'
@ -81,7 +81,7 @@ app.get('/api/replay-data', async (req, res) => {
return
}
const log = await GameLog.get(gameId, offset, size)
let game = null
let game: GameType|null = null
if (offset === 0) {
// also need the game
game = await Game.createGameObject(
@ -107,15 +107,15 @@ app.get('/api/newgame-data', (req, res) => {
app.get('/api/index-data', (req, res) => {
const ts = Time.timestamp()
const games = [
...Game.getAllGames().map((game: any) => ({
...GameCommon.getAllGames().map((game: any) => ({
id: game.id,
hasReplay: GameLog.exists(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),
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),
})),
]
@ -193,7 +193,7 @@ 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)) {
if (!GameCommon.exists(gameId)) {
const ts = Time.timestamp()
await Game.createGame(
gameId,
@ -238,13 +238,13 @@ wss.on('message', async ({socket, data} : { socket: WebSocket, data: any }) => {
const msgType = msg[0]
switch (msgType) {
case Protocol.EV_CLIENT_INIT: {
if (!Game.exists(gameId)) {
if (!GameCommon.exists(gameId)) {
throw `[game ${gameId} does not exist... ]`
}
const ts = Time.timestamp()
Game.addPlayer(gameId, clientId, ts)
GameSockets.addSocket(gameId, socket)
const game = Game.get(gameId)
const game: GameType = GameCommon.get(gameId)
notify(
[Protocol.EV_SERVER_INIT, Util.encodeGame(game)],
[socket]
@ -252,7 +252,7 @@ wss.on('message', async ({socket, data} : { socket: WebSocket, data: any }) => {
} break
case Protocol.EV_CLIENT_EVENT: {
if (!Game.exists(gameId)) {
if (!GameCommon.exists(gameId)) {
throw `[game ${gameId} does not exist... ]`
}
const clientSeq = msg[1]
@ -260,7 +260,7 @@ wss.on('message', async ({socket, data} : { socket: WebSocket, data: any }) => {
const ts = Time.timestamp()
let sendGame = false
if (!Game.playerExists(gameId, clientId)) {
if (!GameCommon.playerExists(gameId, clientId)) {
Game.addPlayer(gameId, clientId, ts)
sendGame = true
}
@ -269,7 +269,7 @@ wss.on('message', async ({socket, data} : { socket: WebSocket, data: any }) => {
sendGame = true
}
if (sendGame) {
const game = Game.get(gameId)
const game: GameType = GameCommon.get(gameId)
notify(
[Protocol.EV_SERVER_INIT, Util.encodeGame(game)],
[socket]