switch to typescript
This commit is contained in:
parent
031ca31c7e
commit
23559b1a3b
63 changed files with 7943 additions and 1397 deletions
886
src/common/GameCommon.ts
Normal file
886
src/common/GameCommon.ts
Normal file
|
|
@ -0,0 +1,886 @@
|
|||
import Geometry from './Geometry'
|
||||
import Protocol from './Protocol'
|
||||
import { Rng } from './Rng'
|
||||
import Time from './Time'
|
||||
import Util from './Util'
|
||||
|
||||
interface EncodedPlayer extends Array<any> {}
|
||||
interface EncodedPiece extends Array<any> {}
|
||||
|
||||
interface Point {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
interface GameRng {
|
||||
obj: Rng
|
||||
type?: string
|
||||
}
|
||||
|
||||
interface Game {
|
||||
id: string
|
||||
players: Array<EncodedPlayer>
|
||||
puzzle: Puzzle
|
||||
evtInfos: Record<string, EvtInfo>
|
||||
scoreMode?: number
|
||||
rng: GameRng
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
export interface PieceShape {
|
||||
top: 0|1|-1
|
||||
bottom: 0|1|-1
|
||||
left: 0|1|-1
|
||||
right: 0|1|-1
|
||||
}
|
||||
|
||||
export interface Piece {
|
||||
owner: string|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
|
||||
|
||||
// TODO: ts type Array<PieceShape>
|
||||
shapes: Array<any>
|
||||
}
|
||||
|
||||
export interface Player {
|
||||
id: string
|
||||
x: number
|
||||
y: number
|
||||
d: 0|1
|
||||
name: string|null
|
||||
color: string|null
|
||||
bgcolor: string|null
|
||||
points: number
|
||||
ts: number
|
||||
}
|
||||
|
||||
interface EvtInfo {
|
||||
_last_mouse: Point|null
|
||||
_last_mouse_down: Point|null
|
||||
}
|
||||
|
||||
const SCORE_MODE_FINAL = 0
|
||||
const SCORE_MODE_ANY = 1
|
||||
|
||||
const IDLE_TIMEOUT_SEC = 30
|
||||
|
||||
// Map<gameId, Game>
|
||||
const GAMES: Record<string, Game> = {}
|
||||
|
||||
function exists(gameId: string) {
|
||||
return (!!GAMES[gameId]) || false
|
||||
}
|
||||
|
||||
function __createPlayerObject(id: string, ts: number): Player {
|
||||
return {
|
||||
id: id,
|
||||
x: 0,
|
||||
y: 0,
|
||||
d: 0, // mouse down
|
||||
name: null, // 'anon'
|
||||
color: null, // '#ffffff'
|
||||
bgcolor: null, // '#222222'
|
||||
points: 0,
|
||||
ts: ts,
|
||||
}
|
||||
}
|
||||
|
||||
function setGame(gameId: string, game: Game) {
|
||||
GAMES[gameId] = game
|
||||
}
|
||||
|
||||
function getPlayerIndexById(gameId: string, playerId: string): number {
|
||||
let i = 0;
|
||||
for (let player of GAMES[gameId].players) {
|
||||
if (Util.decodePlayer(player).id === playerId) {
|
||||
return i
|
||||
}
|
||||
i++
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
function getPlayerIdByIndex(gameId: string, playerIndex: number) {
|
||||
if (GAMES[gameId].players.length > playerIndex) {
|
||||
return Util.decodePlayer(GAMES[gameId].players[playerIndex]).id
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function getPlayer(gameId: string, playerId: string) {
|
||||
let idx = getPlayerIndexById(gameId, playerId)
|
||||
return Util.decodePlayer(GAMES[gameId].players[idx])
|
||||
}
|
||||
|
||||
function setPlayer(gameId: string, playerId: string, player: Player) {
|
||||
let idx = getPlayerIndexById(gameId, playerId)
|
||||
if (idx === -1) {
|
||||
GAMES[gameId].players.push(Util.encodePlayer(player))
|
||||
} else {
|
||||
GAMES[gameId].players[idx] = Util.encodePlayer(player)
|
||||
}
|
||||
}
|
||||
|
||||
function setTile(gameId: string, tileIdx: number, tile: Piece) {
|
||||
GAMES[gameId].puzzle.tiles[tileIdx] = Util.encodeTile(tile)
|
||||
}
|
||||
|
||||
function setPuzzleData(gameId: string, data: PuzzleData) {
|
||||
GAMES[gameId].puzzle.data = data
|
||||
}
|
||||
|
||||
function playerExists(gameId: string, playerId: string) {
|
||||
const idx = getPlayerIndexById(gameId, playerId)
|
||||
return idx !== -1
|
||||
}
|
||||
|
||||
function getActivePlayers(gameId: string, ts: number) {
|
||||
const minTs = ts - IDLE_TIMEOUT_SEC * Time.SEC
|
||||
return getAllPlayers(gameId).filter((p: Player) => p.ts >= minTs)
|
||||
}
|
||||
|
||||
function getIdlePlayers(gameId: string, ts: number) {
|
||||
const minTs = ts - IDLE_TIMEOUT_SEC * Time.SEC
|
||||
return getAllPlayers(gameId).filter((p: Player) => p.ts < minTs && p.points > 0)
|
||||
}
|
||||
|
||||
function addPlayer(gameId: string, playerId: string, ts: number) {
|
||||
if (!playerExists(gameId, playerId)) {
|
||||
setPlayer(gameId, playerId, __createPlayerObject(playerId, ts))
|
||||
} else {
|
||||
changePlayer(gameId, playerId, { ts })
|
||||
}
|
||||
}
|
||||
|
||||
function getEvtInfo(gameId: string, playerId: string) {
|
||||
if (playerId in GAMES[gameId].evtInfos) {
|
||||
return GAMES[gameId].evtInfos[playerId]
|
||||
}
|
||||
return {
|
||||
_last_mouse: null,
|
||||
_last_mouse_down: null,
|
||||
}
|
||||
}
|
||||
|
||||
function setEvtInfo(gameId: string, playerId: string, evtInfo: EvtInfo) {
|
||||
GAMES[gameId].evtInfos[playerId] = evtInfo
|
||||
}
|
||||
|
||||
function getAllGames(): Array<Game> {
|
||||
return Object.values(GAMES).sort((a: Game, b: Game) => {
|
||||
// when both have same finished state, sort by started
|
||||
if (isFinished(a.id) === isFinished(b.id)) {
|
||||
return b.puzzle.data.started - a.puzzle.data.started
|
||||
}
|
||||
// otherwise, sort: unfinished, finished
|
||||
return isFinished(a.id) ? 1 : -1
|
||||
})
|
||||
}
|
||||
|
||||
function getAllPlayers(gameId: string) {
|
||||
return GAMES[gameId]
|
||||
? GAMES[gameId].players.map(Util.decodePlayer)
|
||||
: []
|
||||
}
|
||||
|
||||
function get(gameId: string) {
|
||||
return GAMES[gameId]
|
||||
}
|
||||
|
||||
function getTileCount(gameId: string) {
|
||||
return GAMES[gameId].puzzle.tiles.length
|
||||
}
|
||||
|
||||
function getImageUrl(gameId: string) {
|
||||
return GAMES[gameId].puzzle.info.imageUrl
|
||||
}
|
||||
|
||||
function setImageUrl(gameId: string, imageUrl: string) {
|
||||
GAMES[gameId].puzzle.info.imageUrl = imageUrl
|
||||
}
|
||||
|
||||
function getScoreMode(gameId: string) {
|
||||
return GAMES[gameId].scoreMode || SCORE_MODE_FINAL
|
||||
}
|
||||
|
||||
function isFinished(gameId: string) {
|
||||
return getFinishedTileCount(gameId) === getTileCount(gameId)
|
||||
}
|
||||
|
||||
function getFinishedTileCount(gameId: string) {
|
||||
let count = 0
|
||||
for (let t of GAMES[gameId].puzzle.tiles) {
|
||||
if (Util.decodeTile(t).owner === -1) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
function getTilesSortedByZIndex(gameId: string) {
|
||||
const tiles = GAMES[gameId].puzzle.tiles.map(Util.decodeTile)
|
||||
return tiles.sort((t1, t2) => t1.z - t2.z)
|
||||
}
|
||||
|
||||
function changePlayer(gameId: string, playerId: string, change: any) {
|
||||
const player = getPlayer(gameId, playerId)
|
||||
for (let k of Object.keys(change)) {
|
||||
player[k] = change[k]
|
||||
}
|
||||
setPlayer(gameId, playerId, player)
|
||||
}
|
||||
|
||||
function changeData(gameId: string, change: any) {
|
||||
for (let k of Object.keys(change)) {
|
||||
// @ts-ignore
|
||||
GAMES[gameId].puzzle.data[k] = change[k]
|
||||
}
|
||||
}
|
||||
|
||||
function changeTile(gameId: string, tileIdx: number, change: any) {
|
||||
for (let k of Object.keys(change)) {
|
||||
const tile = Util.decodeTile(GAMES[gameId].puzzle.tiles[tileIdx])
|
||||
tile[k] = change[k]
|
||||
GAMES[gameId].puzzle.tiles[tileIdx] = Util.encodeTile(tile)
|
||||
}
|
||||
}
|
||||
|
||||
const getTile = (gameId: string, tileIdx: number) => {
|
||||
return Util.decodeTile(GAMES[gameId].puzzle.tiles[tileIdx])
|
||||
}
|
||||
|
||||
const getTileGroup = (gameId: string, tileIdx: number) => {
|
||||
const tile = getTile(gameId, tileIdx)
|
||||
return tile.group
|
||||
}
|
||||
|
||||
const getFinalTilePos = (gameId: string, tileIdx: number) => {
|
||||
const info = GAMES[gameId].puzzle.info
|
||||
const boardPos = {
|
||||
x: (info.table.width - info.width) / 2,
|
||||
y: (info.table.height - info.height) / 2
|
||||
}
|
||||
const srcPos = srcPosByTileIdx(gameId, tileIdx)
|
||||
return Geometry.pointAdd(boardPos, srcPos)
|
||||
}
|
||||
|
||||
const getTilePos = (gameId: string, tileIdx: number) => {
|
||||
const tile = getTile(gameId, tileIdx)
|
||||
return tile.pos
|
||||
}
|
||||
|
||||
// todo: instead, just make the table bigger and use that :)
|
||||
const getBounds = (gameId: string) => {
|
||||
const tw = getTableWidth(gameId)
|
||||
const th = getTableHeight(gameId)
|
||||
|
||||
const overX = Math.round(tw / 4)
|
||||
const overY = Math.round(th / 4)
|
||||
return {
|
||||
x: 0 - overX,
|
||||
y: 0 - overY,
|
||||
w: tw + 2 * overX,
|
||||
h: th + 2 * overY,
|
||||
}
|
||||
}
|
||||
|
||||
const getTileBounds = (gameId: string, tileIdx: number) => {
|
||||
const s = getTileSize(gameId)
|
||||
const tile = getTile(gameId, tileIdx)
|
||||
return {
|
||||
x: tile.pos.x,
|
||||
y: tile.pos.y,
|
||||
w: s,
|
||||
h: s,
|
||||
}
|
||||
}
|
||||
|
||||
const getTileZIndex = (gameId: string, tileIdx: number) => {
|
||||
const tile = getTile(gameId, tileIdx)
|
||||
return tile.z
|
||||
}
|
||||
|
||||
const getFirstOwnedTileIdx = (gameId: string, playerId: string) => {
|
||||
for (let t of GAMES[gameId].puzzle.tiles) {
|
||||
const tile = Util.decodeTile(t)
|
||||
if (tile.owner === playerId) {
|
||||
return tile.idx
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
const getFirstOwnedTile = (gameId: string, playerId: string) => {
|
||||
const idx = getFirstOwnedTileIdx(gameId, playerId)
|
||||
return idx < 0 ? null : GAMES[gameId].puzzle.tiles[idx]
|
||||
}
|
||||
|
||||
const getTileDrawOffset = (gameId: string) => {
|
||||
return GAMES[gameId].puzzle.info.tileDrawOffset
|
||||
}
|
||||
|
||||
const getTileDrawSize = (gameId: string) => {
|
||||
return GAMES[gameId].puzzle.info.tileDrawSize
|
||||
}
|
||||
|
||||
const getTileSize = (gameId: string) => {
|
||||
return GAMES[gameId].puzzle.info.tileSize
|
||||
}
|
||||
|
||||
const getStartTs = (gameId: string) => {
|
||||
return GAMES[gameId].puzzle.data.started
|
||||
}
|
||||
|
||||
const getFinishTs = (gameId: string) => {
|
||||
return GAMES[gameId].puzzle.data.finished
|
||||
}
|
||||
|
||||
const getMaxGroup = (gameId: string) => {
|
||||
return GAMES[gameId].puzzle.data.maxGroup
|
||||
}
|
||||
|
||||
const getMaxZIndex = (gameId: string) => {
|
||||
return GAMES[gameId].puzzle.data.maxZ
|
||||
}
|
||||
|
||||
const getMaxZIndexByTileIdxs = (gameId: string, tileIdxs: Array<number>) => {
|
||||
let maxZ = 0
|
||||
for (let tileIdx of tileIdxs) {
|
||||
let tileZIndex = getTileZIndex(gameId, tileIdx)
|
||||
if (tileZIndex > maxZ) {
|
||||
maxZ = tileZIndex
|
||||
}
|
||||
}
|
||||
return maxZ
|
||||
}
|
||||
|
||||
function srcPosByTileIdx(gameId: string, tileIdx: number) {
|
||||
const info = GAMES[gameId].puzzle.info
|
||||
|
||||
const c = Util.coordByTileIdx(info, tileIdx)
|
||||
const cx = c.x * info.tileSize
|
||||
const cy = c.y * info.tileSize
|
||||
|
||||
return { x: cx, y: cy }
|
||||
}
|
||||
|
||||
function getSurroundingTilesByIdx(gameId: string, tileIdx: number) {
|
||||
const info = GAMES[gameId].puzzle.info
|
||||
|
||||
const c = Util.coordByTileIdx(info, tileIdx)
|
||||
|
||||
return [
|
||||
// top
|
||||
(c.y > 0) ? (tileIdx - info.tilesX) : -1,
|
||||
// right
|
||||
(c.x < info.tilesX - 1) ? (tileIdx + 1) : -1,
|
||||
// bottom
|
||||
(c.y < info.tilesY - 1) ? (tileIdx + info.tilesX) : -1,
|
||||
// left
|
||||
(c.x > 0) ? (tileIdx - 1) : -1,
|
||||
]
|
||||
}
|
||||
|
||||
const setTilesZIndex = (gameId: string, tileIdxs: Array<number>, zIndex: number) => {
|
||||
for (let tilesIdx of tileIdxs) {
|
||||
changeTile(gameId, tilesIdx, { z: zIndex })
|
||||
}
|
||||
}
|
||||
|
||||
const moveTileDiff = (gameId: string, tileIdx: number, diff: Point) => {
|
||||
const oldPos = getTilePos(gameId, tileIdx)
|
||||
const pos = Geometry.pointAdd(oldPos, diff)
|
||||
changeTile(gameId, tileIdx, { pos })
|
||||
}
|
||||
|
||||
const moveTilesDiff = (gameId: string, tileIdxs: Array<number>, diff: Point) => {
|
||||
const tileDrawSize = getTileDrawSize(gameId)
|
||||
const bounds = getBounds(gameId)
|
||||
const cappedDiff = diff
|
||||
|
||||
for (let tileIdx of tileIdxs) {
|
||||
const t = getTile(gameId, tileIdx)
|
||||
if (t.pos.x + diff.x < bounds.x) {
|
||||
cappedDiff.x = Math.max(bounds.x - t.pos.x, cappedDiff.x)
|
||||
} else if (t.pos.x + tileDrawSize + diff.x > bounds.x + bounds.w) {
|
||||
cappedDiff.x = Math.min(bounds.x + bounds.w - t.pos.x + tileDrawSize, cappedDiff.x)
|
||||
}
|
||||
if (t.pos.y + diff.y < bounds.y) {
|
||||
cappedDiff.y = Math.max(bounds.y - t.pos.y, cappedDiff.y)
|
||||
} else if (t.pos.y + tileDrawSize + diff.y > bounds.y + bounds.h) {
|
||||
cappedDiff.y = Math.min(bounds.y + bounds.h - t.pos.y + tileDrawSize, cappedDiff.y)
|
||||
}
|
||||
}
|
||||
|
||||
for (let tileIdx of tileIdxs) {
|
||||
moveTileDiff(gameId, tileIdx, cappedDiff)
|
||||
}
|
||||
}
|
||||
|
||||
const finishTiles = (gameId: string, tileIdxs: Array<number>) => {
|
||||
for (let tileIdx of tileIdxs) {
|
||||
changeTile(gameId, tileIdx, { owner: -1, z: 1 })
|
||||
}
|
||||
}
|
||||
|
||||
const setTilesOwner = (
|
||||
gameId: string,
|
||||
tileIdxs: Array<number>,
|
||||
owner: string|number
|
||||
) => {
|
||||
for (let tileIdx of tileIdxs) {
|
||||
changeTile(gameId, tileIdx, { owner })
|
||||
}
|
||||
}
|
||||
|
||||
// get all grouped tiles for a tile
|
||||
function getGroupedTileIdxs(gameId: string, tileIdx: number) {
|
||||
const tiles = GAMES[gameId].puzzle.tiles
|
||||
const tile = Util.decodeTile(tiles[tileIdx])
|
||||
|
||||
const grouped = []
|
||||
if (tile.group) {
|
||||
for (let other of tiles) {
|
||||
const otherTile = Util.decodeTile(other)
|
||||
if (otherTile.group === tile.group) {
|
||||
grouped.push(otherTile.idx)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
grouped.push(tile.idx)
|
||||
}
|
||||
return grouped
|
||||
}
|
||||
|
||||
// Returns the index of the puzzle tile with the highest z index
|
||||
// that is not finished yet and that matches the position
|
||||
const freeTileIdxByPos = (gameId: string, pos: Point) => {
|
||||
let info = GAMES[gameId].puzzle.info
|
||||
let tiles = 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) {
|
||||
continue
|
||||
}
|
||||
|
||||
const collisionRect = {
|
||||
x: tile.pos.x,
|
||||
y: tile.pos.y,
|
||||
w: info.tileSize,
|
||||
h: info.tileSize,
|
||||
}
|
||||
if (Geometry.pointInBounds(pos, collisionRect)) {
|
||||
if (maxZ === -1 || tile.z > maxZ) {
|
||||
maxZ = tile.z
|
||||
tileIdx = idx
|
||||
}
|
||||
}
|
||||
}
|
||||
return tileIdx
|
||||
}
|
||||
|
||||
const getPlayerBgColor = (gameId: string, playerId: string) => {
|
||||
const p = getPlayer(gameId, playerId)
|
||||
return p ? p.bgcolor : null
|
||||
}
|
||||
|
||||
const getPlayerColor = (gameId: string, playerId: string) => {
|
||||
const p = getPlayer(gameId, playerId)
|
||||
return p ? p.color : null
|
||||
}
|
||||
|
||||
const getPlayerName = (gameId: string, playerId: string) => {
|
||||
const p = getPlayer(gameId, playerId)
|
||||
return p ? p.name : null
|
||||
}
|
||||
|
||||
const getPlayerPoints = (gameId: string, playerId: string) => {
|
||||
const p = getPlayer(gameId, playerId)
|
||||
return p ? p.points : null
|
||||
}
|
||||
|
||||
// determine if two tiles are grouped together
|
||||
const areGrouped = (gameId: string, tileIdx1: number, tileIdx2: number) => {
|
||||
const g1 = getTileGroup(gameId, tileIdx1)
|
||||
const g2 = getTileGroup(gameId, tileIdx2)
|
||||
return g1 && g1 === g2
|
||||
}
|
||||
|
||||
const getTableWidth = (gameId: string) => {
|
||||
return GAMES[gameId].puzzle.info.table.width
|
||||
}
|
||||
|
||||
const getTableHeight = (gameId: string) => {
|
||||
return GAMES[gameId].puzzle.info.table.height
|
||||
}
|
||||
|
||||
const getPuzzle = (gameId: string) => {
|
||||
return GAMES[gameId].puzzle
|
||||
}
|
||||
|
||||
const getRng = (gameId: string): Rng => {
|
||||
return GAMES[gameId].rng.obj
|
||||
}
|
||||
|
||||
const getPuzzleWidth = (gameId: string) => {
|
||||
return GAMES[gameId].puzzle.info.width
|
||||
}
|
||||
|
||||
const getPuzzleHeight = (gameId: string) => {
|
||||
return GAMES[gameId].puzzle.info.height
|
||||
}
|
||||
|
||||
function handleInput(gameId: string, playerId: string, input: any, ts: number) {
|
||||
const puzzle = GAMES[gameId].puzzle
|
||||
const evtInfo = getEvtInfo(gameId, playerId)
|
||||
|
||||
const changes = [] as Array<Array<any>>
|
||||
|
||||
const _dataChange = () => {
|
||||
changes.push([Protocol.CHANGE_DATA, puzzle.data])
|
||||
}
|
||||
|
||||
const _tileChange = (tileIdx: number) => {
|
||||
changes.push([
|
||||
Protocol.CHANGE_TILE,
|
||||
Util.encodeTile(getTile(gameId, tileIdx)),
|
||||
])
|
||||
}
|
||||
|
||||
const _tileChanges = (tileIdxs: Array<number>) => {
|
||||
for (const tileIdx of tileIdxs) {
|
||||
_tileChange(tileIdx)
|
||||
}
|
||||
}
|
||||
|
||||
const _playerChange = () => {
|
||||
changes.push([
|
||||
Protocol.CHANGE_PLAYER,
|
||||
Util.encodePlayer(getPlayer(gameId, playerId)),
|
||||
])
|
||||
}
|
||||
|
||||
// put both tiles (and their grouped tiles) in the same group
|
||||
const groupTiles = (gameId: string, tileIdx1: number, tileIdx2: number) => {
|
||||
const tiles = GAMES[gameId].puzzle.tiles
|
||||
const group1 = getTileGroup(gameId, tileIdx1)
|
||||
const group2 = getTileGroup(gameId, tileIdx2)
|
||||
|
||||
let group
|
||||
const searchGroups = []
|
||||
if (group1) {
|
||||
searchGroups.push(group1)
|
||||
}
|
||||
if (group2) {
|
||||
searchGroups.push(group2)
|
||||
}
|
||||
if (group1) {
|
||||
group = group1
|
||||
} else if (group2) {
|
||||
group = group2
|
||||
} else {
|
||||
const maxGroup = getMaxGroup(gameId) + 1
|
||||
changeData(gameId, { maxGroup })
|
||||
_dataChange()
|
||||
group = getMaxGroup(gameId)
|
||||
}
|
||||
|
||||
changeTile(gameId, tileIdx1, { group })
|
||||
_tileChange(tileIdx1)
|
||||
changeTile(gameId, tileIdx2, { group })
|
||||
_tileChange(tileIdx2)
|
||||
|
||||
// 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 type = input[0]
|
||||
if (type === Protocol.INPUT_EV_BG_COLOR) {
|
||||
const bgcolor = input[1]
|
||||
changePlayer(gameId, playerId, { bgcolor, ts })
|
||||
_playerChange()
|
||||
} else if (type === Protocol.INPUT_EV_PLAYER_COLOR) {
|
||||
const color = input[1]
|
||||
changePlayer(gameId, playerId, { color, ts })
|
||||
_playerChange()
|
||||
} else if (type === Protocol.INPUT_EV_PLAYER_NAME) {
|
||||
const name = `${input[1]}`.substr(0, 16)
|
||||
changePlayer(gameId, playerId, { name, ts })
|
||||
_playerChange()
|
||||
} else if (type === Protocol.INPUT_EV_MOUSE_DOWN) {
|
||||
const x = input[1]
|
||||
const y = input[2]
|
||||
const pos = {x, y}
|
||||
|
||||
changePlayer(gameId, playerId, { d: 1, ts })
|
||||
_playerChange()
|
||||
evtInfo._last_mouse_down = pos
|
||||
|
||||
const tileIdxAtPos = freeTileIdxByPos(gameId, pos)
|
||||
if (tileIdxAtPos >= 0) {
|
||||
let maxZ = getMaxZIndex(gameId) + 1
|
||||
changeData(gameId, { maxZ })
|
||||
_dataChange()
|
||||
const tileIdxs = getGroupedTileIdxs(gameId, tileIdxAtPos)
|
||||
setTilesZIndex(gameId, tileIdxs, getMaxZIndex(gameId))
|
||||
setTilesOwner(gameId, tileIdxs, playerId)
|
||||
_tileChanges(tileIdxs)
|
||||
}
|
||||
evtInfo._last_mouse = pos
|
||||
|
||||
} else if (type === Protocol.INPUT_EV_MOUSE_MOVE) {
|
||||
const x = input[1]
|
||||
const y = input[2]
|
||||
const pos = {x, y}
|
||||
|
||||
if (evtInfo._last_mouse_down === null) {
|
||||
// player is just moving the hand
|
||||
changePlayer(gameId, playerId, {x, y, ts})
|
||||
_playerChange()
|
||||
} else {
|
||||
let tileIdx = getFirstOwnedTileIdx(gameId, playerId)
|
||||
if (tileIdx >= 0) {
|
||||
// player is moving a tile (and hand)
|
||||
changePlayer(gameId, playerId, {x, y, ts})
|
||||
_playerChange()
|
||||
|
||||
// check if pos is on the tile, otherwise dont move
|
||||
// (mouse could be out of table, but tile stays on it)
|
||||
const tileIdxs = getGroupedTileIdxs(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)
|
||||
if (Geometry.pointInBounds(pos, bounds)) {
|
||||
anyOk = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (anyOk) {
|
||||
const diffX = x - evtInfo._last_mouse_down.x
|
||||
const diffY = y - evtInfo._last_mouse_down.y
|
||||
|
||||
const diff = { x: diffX, y: diffY }
|
||||
moveTilesDiff(gameId, tileIdxs, diff)
|
||||
|
||||
_tileChanges(tileIdxs)
|
||||
}
|
||||
} else {
|
||||
// player is just moving map, so no change in position!
|
||||
changePlayer(gameId, playerId, {ts})
|
||||
_playerChange()
|
||||
}
|
||||
|
||||
evtInfo._last_mouse_down = pos
|
||||
}
|
||||
evtInfo._last_mouse = pos
|
||||
|
||||
} else if (type === Protocol.INPUT_EV_MOUSE_UP) {
|
||||
const x = input[1]
|
||||
const y = input[2]
|
||||
const pos = {x, y}
|
||||
const d = 0
|
||||
|
||||
evtInfo._last_mouse_down = null
|
||||
|
||||
let tileIdx = getFirstOwnedTileIdx(gameId, playerId)
|
||||
if (tileIdx >= 0) {
|
||||
// drop the tile(s)
|
||||
let tileIdxs = getGroupedTileIdxs(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)
|
||||
if (Geometry.pointDistance(finalPos, tilePos) < puzzle.info.snapDistance) {
|
||||
let diff = Geometry.pointSub(finalPos, tilePos)
|
||||
// Snap the tile to the final destination
|
||||
moveTilesDiff(gameId, tileIdxs, diff)
|
||||
finishTiles(gameId, tileIdxs)
|
||||
_tileChanges(tileIdxs)
|
||||
|
||||
let points = getPlayerPoints(gameId, playerId)
|
||||
if (getScoreMode(gameId) === SCORE_MODE_FINAL) {
|
||||
points += tileIdxs.length
|
||||
} else if (getScoreMode(gameId) === SCORE_MODE_ANY) {
|
||||
points += 1
|
||||
} else {
|
||||
// no score mode... should never occur, because there is a
|
||||
// fallback to SCORE_MODE_FINAL in getScoreMode function
|
||||
}
|
||||
changePlayer(gameId, playerId, { d, ts, points })
|
||||
_playerChange()
|
||||
|
||||
// check if the puzzle is finished
|
||||
if (getFinishedTileCount(gameId) === getTileCount(gameId)) {
|
||||
changeData(gameId, { finished: ts })
|
||||
_dataChange()
|
||||
}
|
||||
} else {
|
||||
// Snap to other tiles
|
||||
const check = (
|
||||
gameId: string,
|
||||
tileIdx: number,
|
||||
otherTileIdx: number,
|
||||
off: Array<number>
|
||||
) => {
|
||||
let info = GAMES[gameId].puzzle.info
|
||||
if (otherTileIdx < 0) {
|
||||
return false
|
||||
}
|
||||
if (areGrouped(gameId, tileIdx, otherTileIdx)) {
|
||||
return false
|
||||
}
|
||||
const tilePos = getTilePos(gameId, tileIdx)
|
||||
const dstPos = Geometry.pointAdd(
|
||||
getTilePos(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)
|
||||
moveTilesDiff(gameId, tileIdxs, diff)
|
||||
groupTiles(gameId, tileIdx, otherTileIdx)
|
||||
tileIdxs = getGroupedTileIdxs(gameId, tileIdx)
|
||||
const zIndex = getMaxZIndexByTileIdxs(gameId, tileIdxs)
|
||||
setTilesZIndex(gameId, tileIdxs, zIndex)
|
||||
_tileChanges(tileIdxs)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
let snapped = false
|
||||
for (let tileIdxTmp of getGroupedTileIdxs(gameId, tileIdx)) {
|
||||
let othersIdxs = getSurroundingTilesByIdx(gameId, tileIdxTmp)
|
||||
if (
|
||||
check(gameId, tileIdxTmp, othersIdxs[0], [0, 1]) // top
|
||||
|| check(gameId, tileIdxTmp, othersIdxs[1], [-1, 0]) // right
|
||||
|| check(gameId, tileIdxTmp, othersIdxs[2], [0, -1]) // bottom
|
||||
|| check(gameId, tileIdxTmp, othersIdxs[3], [1, 0]) // left
|
||||
) {
|
||||
snapped = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (snapped && getScoreMode(gameId) === SCORE_MODE_ANY) {
|
||||
const points = getPlayerPoints(gameId, playerId) + 1
|
||||
changePlayer(gameId, playerId, { d, ts, points })
|
||||
_playerChange()
|
||||
} else {
|
||||
changePlayer(gameId, playerId, { d, ts })
|
||||
_playerChange()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
changePlayer(gameId, playerId, { d, ts })
|
||||
_playerChange()
|
||||
}
|
||||
evtInfo._last_mouse = pos
|
||||
} else if (type === Protocol.INPUT_EV_ZOOM_IN) {
|
||||
const x = input[1]
|
||||
const y = input[2]
|
||||
changePlayer(gameId, playerId, { x, y, ts })
|
||||
_playerChange()
|
||||
evtInfo._last_mouse = { x, y }
|
||||
} else if (type === Protocol.INPUT_EV_ZOOM_OUT) {
|
||||
const x = input[1]
|
||||
const y = input[2]
|
||||
changePlayer(gameId, playerId, { x, y, ts })
|
||||
_playerChange()
|
||||
evtInfo._last_mouse = { x, y }
|
||||
} else {
|
||||
changePlayer(gameId, playerId, { ts })
|
||||
_playerChange()
|
||||
}
|
||||
|
||||
setEvtInfo(gameId, playerId, evtInfo)
|
||||
return changes
|
||||
}
|
||||
|
||||
export default {
|
||||
__createPlayerObject,
|
||||
setGame,
|
||||
exists,
|
||||
playerExists,
|
||||
getActivePlayers,
|
||||
getIdlePlayers,
|
||||
addPlayer,
|
||||
getFinishedTileCount,
|
||||
getTileCount,
|
||||
getImageUrl,
|
||||
setImageUrl,
|
||||
get,
|
||||
getAllGames,
|
||||
getPlayerBgColor,
|
||||
getPlayerColor,
|
||||
getPlayerName,
|
||||
getPlayerIndexById,
|
||||
getPlayerIdByIndex,
|
||||
changePlayer,
|
||||
setPlayer,
|
||||
setTile,
|
||||
setPuzzleData,
|
||||
getTableWidth,
|
||||
getTableHeight,
|
||||
getPuzzle,
|
||||
getRng,
|
||||
getPuzzleWidth,
|
||||
getPuzzleHeight,
|
||||
getTilesSortedByZIndex,
|
||||
getFirstOwnedTile,
|
||||
getTileDrawOffset,
|
||||
getTileDrawSize,
|
||||
getFinalTilePos,
|
||||
getStartTs,
|
||||
getFinishTs,
|
||||
handleInput,
|
||||
SCORE_MODE_FINAL,
|
||||
SCORE_MODE_ANY,
|
||||
}
|
||||
88
src/common/Geometry.ts
Normal file
88
src/common/Geometry.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
interface Point {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
interface Rect {
|
||||
x: number
|
||||
y: number
|
||||
w: number
|
||||
h: number
|
||||
}
|
||||
|
||||
function pointSub(a: Point, b: Point): Point {
|
||||
return { x: a.x - b.x, y: a.y - b.y }
|
||||
}
|
||||
|
||||
function pointAdd(a: Point, b: Point): Point {
|
||||
return { x: a.x + b.x, y: a.y + b.y }
|
||||
}
|
||||
|
||||
function pointDistance(a: Point, b: Point): number {
|
||||
const diffX = a.x - b.x
|
||||
const diffY = a.y - b.y
|
||||
return Math.sqrt(diffX * diffX + diffY * diffY)
|
||||
}
|
||||
|
||||
function pointInBounds(pt: Point, rect: Rect): boolean {
|
||||
return pt.x >= rect.x
|
||||
&& pt.x <= rect.x + rect.w
|
||||
&& pt.y >= rect.y
|
||||
&& pt.y <= rect.y + rect.h
|
||||
}
|
||||
|
||||
function rectCenter(rect: Rect): Point {
|
||||
return {
|
||||
x: rect.x + (rect.w / 2),
|
||||
y: rect.y + (rect.h / 2),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a rectangle with same dimensions as the given one, but
|
||||
* location (x/y) moved by x and y.
|
||||
*
|
||||
* @param {x, y, w,, h} rect
|
||||
* @param number x
|
||||
* @param number y
|
||||
* @returns {x, y, w, h}
|
||||
*/
|
||||
function rectMoved(rect: Rect, x: number, y: number): Rect {
|
||||
return {
|
||||
x: rect.x + x,
|
||||
y: rect.y + y,
|
||||
w: rect.w,
|
||||
h: rect.h,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the rectangles overlap, including their borders.
|
||||
*
|
||||
* @param {x, y, w, h} rectA
|
||||
* @param {x, y, w, h} rectB
|
||||
* @returns bool
|
||||
*/
|
||||
function rectsOverlap(rectA: Rect, rectB: Rect): boolean {
|
||||
return !(
|
||||
rectB.x > (rectA.x + rectA.w)
|
||||
|| rectA.x > (rectB.x + rectB.w)
|
||||
|| rectB.y > (rectA.y + rectA.h)
|
||||
|| rectA.y > (rectB.y + rectB.h)
|
||||
)
|
||||
}
|
||||
|
||||
function rectCenterDistance(rectA: Rect, rectB: Rect): number {
|
||||
return pointDistance(rectCenter(rectA), rectCenter(rectB))
|
||||
}
|
||||
|
||||
export default {
|
||||
pointSub,
|
||||
pointAdd,
|
||||
pointDistance,
|
||||
pointInBounds,
|
||||
rectCenter,
|
||||
rectMoved,
|
||||
rectCenterDistance,
|
||||
rectsOverlap,
|
||||
}
|
||||
98
src/common/Protocol.ts
Normal file
98
src/common/Protocol.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
SERVER_CLIENT_MESSAGE_PROTOCOL
|
||||
NOTE: clients always send game id and their id
|
||||
when creating sockets (via socket.protocol), so
|
||||
this doesn't need to be set in each message data
|
||||
|
||||
NOTE: first value in the array is always the type of event/message
|
||||
when describing them below, the value each has is used
|
||||
instead of writing EVENT_TYPE or something ismilar
|
||||
|
||||
|
||||
EV_CLIENT_EVENT: event triggered by clients and sent to server
|
||||
[
|
||||
EV_CLIENT_EVENT, // constant value, type of event
|
||||
CLIENT_SEQ, // sequence number sent by client.
|
||||
EV_DATA, // (eg. mouse input info)
|
||||
]
|
||||
|
||||
EV_SERVER_EVENT: event sent to clients after recieving a client
|
||||
event and processing it
|
||||
[
|
||||
EV_SERVER_EVENT, // constant value, type of event
|
||||
CLIENT_ID, // user who sent the client event
|
||||
CLIENT_SEQ, // sequence number of the client event msg
|
||||
CHANGES_TRIGGERED_BY_CLIENT_EVENT,
|
||||
]
|
||||
|
||||
EV_CLIENT_INIT: event sent by client to enter a game
|
||||
[
|
||||
EV_CLIENT_INIT, // constant value, type of event
|
||||
]
|
||||
|
||||
EV_SERVER_INIT: event sent to one client after that client
|
||||
connects to a game
|
||||
[
|
||||
EV_SERVER_INIT, // constant value, type of event
|
||||
GAME, // complete game instance required by
|
||||
// client to build client side of the game
|
||||
]
|
||||
*/
|
||||
const EV_SERVER_EVENT = 1
|
||||
const EV_SERVER_INIT = 4
|
||||
const EV_SERVER_INIT_REPLAY = 5
|
||||
const EV_CLIENT_EVENT = 2
|
||||
const EV_CLIENT_INIT = 3
|
||||
const EV_CLIENT_INIT_REPLAY = 6
|
||||
|
||||
const LOG_HEADER = 1
|
||||
const LOG_ADD_PLAYER = 2
|
||||
const LOG_UPDATE_PLAYER = 4
|
||||
const LOG_HANDLE_INPUT = 3
|
||||
|
||||
const INPUT_EV_MOUSE_DOWN = 1
|
||||
const INPUT_EV_MOUSE_UP = 2
|
||||
const INPUT_EV_MOUSE_MOVE = 3
|
||||
const INPUT_EV_ZOOM_IN = 4
|
||||
const INPUT_EV_ZOOM_OUT = 5
|
||||
const INPUT_EV_BG_COLOR = 6
|
||||
const INPUT_EV_PLAYER_COLOR = 7
|
||||
const INPUT_EV_PLAYER_NAME = 8
|
||||
const INPUT_EV_MOVE = 9
|
||||
const INPUT_EV_TOGGLE_PREVIEW = 10
|
||||
|
||||
const CHANGE_DATA = 1
|
||||
const CHANGE_TILE = 2
|
||||
const CHANGE_PLAYER = 3
|
||||
|
||||
export default {
|
||||
EV_SERVER_EVENT,
|
||||
EV_SERVER_INIT,
|
||||
EV_SERVER_INIT_REPLAY,
|
||||
EV_CLIENT_EVENT,
|
||||
EV_CLIENT_INIT,
|
||||
EV_CLIENT_INIT_REPLAY,
|
||||
|
||||
LOG_HEADER,
|
||||
LOG_ADD_PLAYER,
|
||||
LOG_UPDATE_PLAYER,
|
||||
LOG_HANDLE_INPUT,
|
||||
|
||||
INPUT_EV_MOVE, // move by x/y
|
||||
|
||||
INPUT_EV_MOUSE_DOWN,
|
||||
INPUT_EV_MOUSE_UP,
|
||||
INPUT_EV_MOUSE_MOVE,
|
||||
|
||||
INPUT_EV_ZOOM_IN,
|
||||
INPUT_EV_ZOOM_OUT,
|
||||
INPUT_EV_BG_COLOR,
|
||||
INPUT_EV_PLAYER_COLOR,
|
||||
INPUT_EV_PLAYER_NAME,
|
||||
|
||||
INPUT_EV_TOGGLE_PREVIEW,
|
||||
|
||||
CHANGE_DATA,
|
||||
CHANGE_TILE,
|
||||
CHANGE_PLAYER,
|
||||
}
|
||||
35
src/common/Rng.ts
Normal file
35
src/common/Rng.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
interface RngSerialized {
|
||||
rand_high: number,
|
||||
rand_low: number,
|
||||
}
|
||||
|
||||
export class Rng {
|
||||
rand_high: number
|
||||
rand_low: number
|
||||
|
||||
constructor(seed: number) {
|
||||
this.rand_high = seed || 0xDEADC0DE
|
||||
this.rand_low = seed ^ 0x49616E42
|
||||
}
|
||||
|
||||
random (min: number, max: number) {
|
||||
this.rand_high = ((this.rand_high << 16) + (this.rand_high >> 16) + this.rand_low) & 0xffffffff;
|
||||
this.rand_low = (this.rand_low + this.rand_high) & 0xffffffff;
|
||||
var n = (this.rand_high >>> 0) / 0xffffffff;
|
||||
return (min + n * (max-min+1))|0;
|
||||
}
|
||||
|
||||
static serialize (rng: Rng): RngSerialized {
|
||||
return {
|
||||
rand_high: rng.rand_high,
|
||||
rand_low: rng.rand_low
|
||||
}
|
||||
}
|
||||
|
||||
static unserialize (rngSerialized: RngSerialized): Rng {
|
||||
const rng = new Rng(0)
|
||||
rng.rand_high = rngSerialized.rand_high
|
||||
rng.rand_low = rngSerialized.rand_low
|
||||
return rng
|
||||
}
|
||||
}
|
||||
47
src/common/Time.ts
Normal file
47
src/common/Time.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
const MS = 1
|
||||
const SEC = MS * 1000
|
||||
const MIN = SEC * 60
|
||||
const HOUR = MIN * 60
|
||||
const DAY = HOUR * 24
|
||||
|
||||
export const timestamp = () => {
|
||||
const d = new Date();
|
||||
return Date.UTC(
|
||||
d.getUTCFullYear(),
|
||||
d.getUTCMonth(),
|
||||
d.getUTCDate(),
|
||||
d.getUTCHours(),
|
||||
d.getUTCMinutes(),
|
||||
d.getUTCSeconds(),
|
||||
d.getUTCMilliseconds(),
|
||||
)
|
||||
}
|
||||
|
||||
export const durationStr = (duration: number) => {
|
||||
const d = Math.floor(duration / DAY)
|
||||
duration = duration % DAY
|
||||
|
||||
const h = Math.floor(duration / HOUR)
|
||||
duration = duration % HOUR
|
||||
|
||||
const m = Math.floor(duration / MIN)
|
||||
duration = duration % MIN
|
||||
|
||||
const s = Math.floor(duration / SEC)
|
||||
|
||||
return `${d}d ${h}h ${m}m ${s}s`
|
||||
}
|
||||
|
||||
export const timeDiffStr = (from: number, to: number) => durationStr(to - from)
|
||||
|
||||
export default {
|
||||
MS,
|
||||
SEC,
|
||||
MIN,
|
||||
HOUR,
|
||||
DAY,
|
||||
|
||||
timestamp,
|
||||
timeDiffStr,
|
||||
durationStr,
|
||||
}
|
||||
216
src/common/Util.ts
Normal file
216
src/common/Util.ts
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
import { Rng } from './Rng'
|
||||
|
||||
|
||||
const pad = (x: any, pad: string) => {
|
||||
const str = `${x}`
|
||||
if (str.length >= pad.length) {
|
||||
return str
|
||||
}
|
||||
return pad.substr(0, pad.length - str.length) + str
|
||||
}
|
||||
|
||||
export const logger = (...pre: Array<any>) => {
|
||||
const log = (m: 'log'|'info'|'error') => (...args: Array<any>) => {
|
||||
const d = new Date()
|
||||
const hh = pad(d.getHours(), '00')
|
||||
const mm = pad(d.getMinutes(), '00')
|
||||
const ss = pad(d.getSeconds(), '00')
|
||||
console[m](`${hh}:${mm}:${ss}`, ...pre, ...args)
|
||||
}
|
||||
return {
|
||||
log: log('log'),
|
||||
error: log('error'),
|
||||
info: log('info'),
|
||||
}
|
||||
}
|
||||
|
||||
// get a unique id
|
||||
export const uniqId = () => Date.now().toString(36) + Math.random().toString(36).substring(2)
|
||||
|
||||
// get a random int between min and max (inclusive)
|
||||
export const randomInt = (
|
||||
rng: Rng,
|
||||
min: number,
|
||||
max: number,
|
||||
) => rng.random(min, max)
|
||||
|
||||
// get one random item from the given array
|
||||
export const choice = (
|
||||
rng: Rng,
|
||||
array: Array<any>
|
||||
) => array[randomInt(rng, 0, array.length - 1)]
|
||||
|
||||
// return a shuffled (shallow) copy of the given array
|
||||
export const shuffle = (
|
||||
rng: Rng,
|
||||
array: Array<any>
|
||||
) => {
|
||||
const arr = array.slice()
|
||||
for (let i = 0; i <= arr.length - 2; i++)
|
||||
{
|
||||
const j = randomInt(rng, i, arr.length -1);
|
||||
const tmp = arr[i];
|
||||
arr[i] = arr[j];
|
||||
arr[j] = tmp;
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
function encodeShape(data: any): number {
|
||||
if (typeof data === 'number') {
|
||||
return data
|
||||
}
|
||||
/* encoded in 1 byte:
|
||||
00000000
|
||||
^^ top
|
||||
^^ right
|
||||
^^ bottom
|
||||
^^ left
|
||||
*/
|
||||
return ((data.top + 1) << 0)
|
||||
| ((data.right + 1) << 2)
|
||||
| ((data.bottom + 1) << 4)
|
||||
| ((data.left + 1) << 6)
|
||||
}
|
||||
|
||||
function decodeShape(data: any) {
|
||||
if (typeof data !== 'number') {
|
||||
return data
|
||||
}
|
||||
return {
|
||||
top: (data >> 0 & 0b11) - 1,
|
||||
right: (data >> 2 & 0b11) - 1,
|
||||
bottom: (data >> 4 & 0b11) - 1,
|
||||
left: (data >> 6 & 0b11) - 1,
|
||||
}
|
||||
}
|
||||
|
||||
function encodeTile(data: any): Array<any> {
|
||||
if (Array.isArray(data)) {
|
||||
return data
|
||||
}
|
||||
return [data.idx, data.pos.x, data.pos.y, data.z, data.owner, data.group]
|
||||
}
|
||||
|
||||
function decodeTile(data: any) {
|
||||
if (!Array.isArray(data)) {
|
||||
return data
|
||||
}
|
||||
return {
|
||||
idx: data[0],
|
||||
pos: {
|
||||
x: data[1],
|
||||
y: data[2],
|
||||
},
|
||||
z: data[3],
|
||||
owner: data[4],
|
||||
group: data[5],
|
||||
}
|
||||
}
|
||||
|
||||
function encodePlayer(data: any): Array<any> {
|
||||
if (Array.isArray(data)) {
|
||||
return data
|
||||
}
|
||||
return [
|
||||
data.id,
|
||||
data.x,
|
||||
data.y,
|
||||
data.d,
|
||||
data.name,
|
||||
data.color,
|
||||
data.bgcolor,
|
||||
data.points,
|
||||
data.ts,
|
||||
]
|
||||
}
|
||||
|
||||
function decodePlayer(data: any) {
|
||||
if (!Array.isArray(data)) {
|
||||
return data
|
||||
}
|
||||
return {
|
||||
id: data[0],
|
||||
x: data[1],
|
||||
y: data[2],
|
||||
d: data[3], // mouse down
|
||||
name: data[4],
|
||||
color: data[5],
|
||||
bgcolor: data[6],
|
||||
points: data[7],
|
||||
ts: data[8],
|
||||
}
|
||||
}
|
||||
|
||||
function encodeGame(data: any): Array<any> {
|
||||
if (Array.isArray(data)) {
|
||||
return data
|
||||
}
|
||||
return [
|
||||
data.id,
|
||||
data.rng.type,
|
||||
Rng.serialize(data.rng.obj),
|
||||
data.puzzle,
|
||||
data.players,
|
||||
data.evtInfos,
|
||||
data.scoreMode,
|
||||
]
|
||||
}
|
||||
|
||||
function decodeGame(data: any) {
|
||||
if (!Array.isArray(data)) {
|
||||
return data
|
||||
}
|
||||
return {
|
||||
id: data[0],
|
||||
rng: {
|
||||
type: data[1],
|
||||
obj: Rng.unserialize(data[2]),
|
||||
},
|
||||
puzzle: data[3],
|
||||
players: data[4],
|
||||
evtInfos: data[5],
|
||||
scoreMode: data[6],
|
||||
}
|
||||
}
|
||||
|
||||
function coordByTileIdx(info: any, tileIdx: number) {
|
||||
const wTiles = info.width / info.tileSize
|
||||
return {
|
||||
x: tileIdx % wTiles,
|
||||
y: Math.floor(tileIdx / wTiles),
|
||||
}
|
||||
}
|
||||
|
||||
const hash = (str: string): number => {
|
||||
let hash = 0
|
||||
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
let chr = str.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + chr;
|
||||
hash |= 0; // Convert to 32bit integer
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
export default {
|
||||
hash,
|
||||
uniqId,
|
||||
randomInt,
|
||||
choice,
|
||||
shuffle,
|
||||
|
||||
encodeShape,
|
||||
decodeShape,
|
||||
|
||||
encodeTile,
|
||||
decodeTile,
|
||||
|
||||
encodePlayer,
|
||||
decodePlayer,
|
||||
|
||||
encodeGame,
|
||||
decodeGame,
|
||||
|
||||
coordByTileIdx,
|
||||
}
|
||||
3
src/common/package.json
Normal file
3
src/common/package.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"type": "module"
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue