switch to typescript

This commit is contained in:
Zutatensuppe 2021-05-17 00:27:47 +02:00
parent 031ca31c7e
commit 23559b1a3b
63 changed files with 7943 additions and 1397 deletions

2
.gitignore vendored
View file

@ -1,3 +1,3 @@
/node_modules
/config.js
/config.json
/data

View file

@ -1,14 +0,0 @@
export default {
http: {
hostname: '127.0.0.1',
port: 1337,
},
ws: {
hostname: '127.0.0.1',
port: 1338,
connectstring: `ws://localhost:1338/ws`,
},
persistence: {
interval: 30000,
},
}

14
config.example.json Normal file
View file

@ -0,0 +1,14 @@
{
"http": {
"hostname": "127.0.0.1",
"port": 1337
},
"ws": {
"hostname": "127.0.0.1",
"port": 1338,
"connectstring": "ws://localhost:1338/ws"
},
"persistence": {
"interval": 30000
}
}

7342
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -7,9 +7,28 @@
"image-size": "^0.9.3",
"multer": "^1.4.2",
"sharp": "^0.28.1",
"vue": "^3.0.11",
"vue-router": "^4.0.8",
"ws": "^7.3.1"
},
"devDependencies": {
"jest": "^26.6.3"
"@types/exif": "^0.6.2",
"@types/express": "^4.17.11",
"@types/multer": "^1.4.5",
"@types/sharp": "^0.28.1",
"@types/ws": "^7.4.4",
"@vitejs/plugin-vue": "^1.2.2",
"@vuedx/typescript-plugin-vue": "^0.6.3",
"jest": "^26.6.3",
"rollup": "^2.48.0",
"rollup-plugin-typescript2": "^0.30.0",
"rollup-plugin-vue": "^6.0.0-beta.10",
"ts-node": "^9.1.1",
"typescript": "^4.2.4",
"vite": "^2.3.2"
},
"scripts": {
"rollup": "rollup",
"vite": "vite"
}
}

View file

@ -1,22 +0,0 @@
"use strict"
export default {
name: 'image-teaser',
props: {
image: Object
},
template: `<div class="imageteaser" :style="style" @click="onClick"></div>`,
computed: {
style() {
const url = this.image.url.replace('uploads/', 'uploads/r/') + '-150x100.webp'
return {
'background-image': `url("${url}")`,
}
},
},
methods: {
onClick() {
this.$emit('click')
},
},
}

View file

@ -1,27 +0,0 @@
"use strict"
// ingame component
// shows the preview image
export default {
name: 'preview-overlay',
template: `
<div class="overlay" @click="$emit('bgclick')">
<div class="preview">
<div class="img" :style="previewStyle"></div>
</div>
</div>`,
props: {
img: String,
},
emits: {
bgclick: null,
},
computed: {
previewStyle () {
return {
backgroundImage: `url('${this.img}')`,
}
},
},
}

View file

@ -1,36 +0,0 @@
"use strict"
import Time from './../../common/Time.js'
// ingame component
// shows timer, tiles left, etc..
// maybe split it up later
export default {
name: 'puzzle-status',
template: `
<div class="timer">
<div>
🧩 {{piecesDone}}/{{piecesTotal}}
</div>
<div>
{{icon}} {{durationStr}}
</div>
<slot />
</div>
`,
props: {
finished: Boolean,
duration: Number,
piecesDone: Number,
piecesTotal: Number,
},
computed: {
icon () {
return this.finished ? '🏁' : '⏳'
},
durationStr () {
return Time.durationStr(this.duration)
},
}
}

View file

@ -1,38 +0,0 @@
"use strict"
// ingame component
// allows to change (player) settings
export default {
name: 'settings-overlay',
template: `
<div class="overlay transparent" @click="$emit('bgclick')">
<table class="overlay-content settings" @click.stop="">
<tr>
<td><label>Background: </label></td>
<td><input type="color" v-model="modelValue.background" /></td>
</tr>
<tr>
<td><label>Color: </label></td>
<td><input type="color" v-model="modelValue.color" /></td>
</tr>
<tr>
<td><label>Name: </label></td>
<td><input type="text" maxLength="16" v-model="modelValue.name" /></td>
</tr>
</table>
</div>
`,
emits: {
bgclick: null,
'update:modelValue': null,
},
props: {
modelValue: Object,
},
created () {
this.$watch('modelValue', val => {
this.$emit('update:modelValue', val)
}, { deep: true })
},
}

View file

@ -1,56 +0,0 @@
<html>
<head>
<link rel="stylesheet" href="/style.css" />
<script src="https://unpkg.com/vue@3.0.11"></script>
<script src="https://unpkg.com/vue-router@4.0.8"></script>
<title>🧩 jigsaw.hyottoko.club</title>
</head>
<body>
<div id="app"></div>
<script type="module">
import App from "/App.vue.js"
import Index from "/views/Index.vue.js"
import NewGame from "/views/NewGame.vue.js"
import Game from "/views/Game.vue.js"
import Replay from "/views/Replay.vue.js"
import Util from './../common/Util.js'
(async () => {
const res = await fetch(`/api/conf`)
const conf = await res.json()
function initme() {
let ID = localStorage.getItem('ID')
if (!ID) {
ID = Util.uniqId()
localStorage.setItem('ID', ID)
}
return ID
}
const router = VueRouter.createRouter({
history: VueRouter.createWebHashHistory(),
routes: [
{ name: 'index', path: '/', component: Index },
{ name: 'new-game', path: '/new-game', component: NewGame },
{ name: 'game', path: '/g/:id', component: Game },
{ name: 'replay', path: '/replay/:id', component: Replay },
],
})
router.beforeEach((to, from) => {
if (from.name !== undefined) {
document.documentElement.classList.remove(`view-${from.name}`)
}
document.documentElement.classList.add(`view-${to.name}`)
})
const app = Vue.createApp(App)
app.config.globalProperties.$config = conf
app.config.globalProperties.$clientId = initme()
app.use(router)
app.mount('#app')
})()
</script>
</body>
</html>

View file

@ -1,43 +0,0 @@
"use strict"
import Time from './../../common/Time.js'
import GameTeaser from './../components/GameTeaser.vue.js'
export default {
components: {
GameTeaser,
},
template: `
<div>
<h1>Running games</h1>
<div class="game-teaser-wrap" v-for="g in gamesRunning">
<game-teaser :game="g" />
</div>
<h1>Finished games</h1>
<div class="game-teaser-wrap" v-for="g in gamesFinished">
<game-teaser :game="g" />
</div>
</div>`,
data() {
return {
gamesRunning: [],
gamesFinished: [],
}
},
async created() {
const res = await fetch('/api/index-data')
const json = await res.json()
this.gamesRunning = json.gamesRunning
this.gamesFinished = json.gamesFinished
},
methods: {
time(start, end) {
const icon = end ? '🏁' : '⏳'
const from = start;
const to = end || Time.timestamp()
const timeDiffStr = Time.timeDiffStr(from, to)
return `${icon} ${timeDiffStr}`
},
}
}

13
rollup.server.config.js Normal file
View file

@ -0,0 +1,13 @@
// rollup.config.js
import typescript from 'rollup-plugin-typescript2';
export default {
input: 'src/server/main.ts',
output: {
dir: 'build/server',
format: 'es',
},
plugins: [typescript({
"tsconfig": "tsconfig.server.json"
})],
};

7
scripts/build Executable file
View file

@ -0,0 +1,7 @@
#!/bin/sh -ex
# server build
npm run rollup -- -c rollup.server.config.js
# frontend build
npm run vite -- build

View file

@ -1,5 +1,5 @@
#!/bin/sh
cd "$RUN_DIR/server"
cd "$RUN_DIR/build/server"
nodemon --max-old-space-size=64 index.js -e js
nodemon --max-old-space-size=64 main.js -c ../../config.json

View file

@ -1,21 +1,111 @@
import Geometry from './Geometry.js'
import Protocol from './Protocol.js'
import Time from './Time.js'
import Util from './Util.js'
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, GameObject>
const GAMES = {}
// Map<gameId, Game>
const GAMES: Record<string, Game> = {}
function exists(gameId) {
function exists(gameId: string) {
return (!!GAMES[gameId]) || false
}
function __createPlayerObject(id, ts) {
function __createPlayerObject(id: string, ts: number): Player {
return {
id: id,
x: 0,
@ -29,11 +119,11 @@ function __createPlayerObject(id, ts) {
}
}
function setGame(gameId, game) {
function setGame(gameId: string, game: Game) {
GAMES[gameId] = game
}
function getPlayerIndexById(gameId, playerId) {
function getPlayerIndexById(gameId: string, playerId: string): number {
let i = 0;
for (let player of GAMES[gameId].players) {
if (Util.decodePlayer(player).id === playerId) {
@ -44,19 +134,19 @@ function getPlayerIndexById(gameId, playerId) {
return -1
}
function getPlayerIdByIndex(gameId, playerIndex) {
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, playerId) {
function getPlayer(gameId: string, playerId: string) {
let idx = getPlayerIndexById(gameId, playerId)
return Util.decodePlayer(GAMES[gameId].players[idx])
}
function setPlayer(gameId, playerId, player) {
function setPlayer(gameId: string, playerId: string, player: Player) {
let idx = getPlayerIndexById(gameId, playerId)
if (idx === -1) {
GAMES[gameId].players.push(Util.encodePlayer(player))
@ -65,30 +155,30 @@ function setPlayer(gameId, playerId, player) {
}
}
function setTile(gameId, tileIdx, tile) {
function setTile(gameId: string, tileIdx: number, tile: Piece) {
GAMES[gameId].puzzle.tiles[tileIdx] = Util.encodeTile(tile)
}
function setPuzzleData(gameId, data) {
function setPuzzleData(gameId: string, data: PuzzleData) {
GAMES[gameId].puzzle.data = data
}
function playerExists(gameId, playerId) {
function playerExists(gameId: string, playerId: string) {
const idx = getPlayerIndexById(gameId, playerId)
return idx !== -1
}
function getActivePlayers(gameId, ts) {
function getActivePlayers(gameId: string, ts: number) {
const minTs = ts - IDLE_TIMEOUT_SEC * Time.SEC
return getAllPlayers(gameId).filter(p => p.ts >= minTs)
return getAllPlayers(gameId).filter((p: Player) => p.ts >= minTs)
}
function getIdlePlayers(gameId, ts) {
function getIdlePlayers(gameId: string, ts: number) {
const minTs = ts - IDLE_TIMEOUT_SEC * Time.SEC
return getAllPlayers(gameId).filter(p => p.ts < minTs && p.points > 0)
return getAllPlayers(gameId).filter((p: Player) => p.ts < minTs && p.points > 0)
}
function addPlayer(gameId, playerId, ts) {
function addPlayer(gameId: string, playerId: string, ts: number) {
if (!playerExists(gameId, playerId)) {
setPlayer(gameId, playerId, __createPlayerObject(playerId, ts))
} else {
@ -96,7 +186,7 @@ function addPlayer(gameId, playerId, ts) {
}
}
function getEvtInfo(gameId, playerId) {
function getEvtInfo(gameId: string, playerId: string) {
if (playerId in GAMES[gameId].evtInfos) {
return GAMES[gameId].evtInfos[playerId]
}
@ -106,12 +196,12 @@ function getEvtInfo(gameId, playerId) {
}
}
function setEvtInfo(gameId, playerId, evtInfo) {
function setEvtInfo(gameId: string, playerId: string, evtInfo: EvtInfo) {
GAMES[gameId].evtInfos[playerId] = evtInfo
}
function getAllGames() {
return Object.values(GAMES).sort((a, b) => {
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
@ -121,37 +211,37 @@ function getAllGames() {
})
}
function getAllPlayers(gameId) {
function getAllPlayers(gameId: string) {
return GAMES[gameId]
? GAMES[gameId].players.map(Util.decodePlayer)
: []
}
function get(gameId) {
function get(gameId: string) {
return GAMES[gameId]
}
function getTileCount(gameId) {
function getTileCount(gameId: string) {
return GAMES[gameId].puzzle.tiles.length
}
function getImageUrl(gameId) {
function getImageUrl(gameId: string) {
return GAMES[gameId].puzzle.info.imageUrl
}
function setImageUrl(gameId, imageUrl) {
function setImageUrl(gameId: string, imageUrl: string) {
GAMES[gameId].puzzle.info.imageUrl = imageUrl
}
function getScoreMode(gameId) {
function getScoreMode(gameId: string) {
return GAMES[gameId].scoreMode || SCORE_MODE_FINAL
}
function isFinished(gameId) {
function isFinished(gameId: string) {
return getFinishedTileCount(gameId) === getTileCount(gameId)
}
function getFinishedTileCount(gameId) {
function getFinishedTileCount(gameId: string) {
let count = 0
for (let t of GAMES[gameId].puzzle.tiles) {
if (Util.decodeTile(t).owner === -1) {
@ -161,12 +251,12 @@ function getFinishedTileCount(gameId) {
return count
}
function getTilesSortedByZIndex(gameId) {
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, playerId, change) {
function changePlayer(gameId: string, playerId: string, change: any) {
const player = getPlayer(gameId, playerId)
for (let k of Object.keys(change)) {
player[k] = change[k]
@ -174,13 +264,14 @@ function changePlayer(gameId, playerId, change) {
setPlayer(gameId, playerId, player)
}
function changeData(gameId, change) {
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, tileIdx, change) {
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]
@ -188,16 +279,16 @@ function changeTile(gameId, tileIdx, change) {
}
}
const getTile = (gameId, tileIdx) => {
const getTile = (gameId: string, tileIdx: number) => {
return Util.decodeTile(GAMES[gameId].puzzle.tiles[tileIdx])
}
const getTileGroup = (gameId, tileIdx) => {
const getTileGroup = (gameId: string, tileIdx: number) => {
const tile = getTile(gameId, tileIdx)
return tile.group
}
const getFinalTilePos = (gameId, tileIdx) => {
const getFinalTilePos = (gameId: string, tileIdx: number) => {
const info = GAMES[gameId].puzzle.info
const boardPos = {
x: (info.table.width - info.width) / 2,
@ -207,13 +298,13 @@ const getFinalTilePos = (gameId, tileIdx) => {
return Geometry.pointAdd(boardPos, srcPos)
}
const getTilePos = (gameId, tileIdx) => {
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) => {
const getBounds = (gameId: string) => {
const tw = getTableWidth(gameId)
const th = getTableHeight(gameId)
@ -227,7 +318,7 @@ const getBounds = (gameId) => {
}
}
const getTileBounds = (gameId, tileIdx) => {
const getTileBounds = (gameId: string, tileIdx: number) => {
const s = getTileSize(gameId)
const tile = getTile(gameId, tileIdx)
return {
@ -238,55 +329,55 @@ const getTileBounds = (gameId, tileIdx) => {
}
}
const getTileZIndex = (gameId, tileIdx) => {
const getTileZIndex = (gameId: string, tileIdx: number) => {
const tile = getTile(gameId, tileIdx)
return tile.z
}
const getFirstOwnedTileIdx = (gameId, userId) => {
const getFirstOwnedTileIdx = (gameId: string, playerId: string) => {
for (let t of GAMES[gameId].puzzle.tiles) {
const tile = Util.decodeTile(t)
if (tile.owner === userId) {
if (tile.owner === playerId) {
return tile.idx
}
}
return -1
}
const getFirstOwnedTile = (gameId, userId) => {
const idx = getFirstOwnedTileIdx(gameId, userId)
const getFirstOwnedTile = (gameId: string, playerId: string) => {
const idx = getFirstOwnedTileIdx(gameId, playerId)
return idx < 0 ? null : GAMES[gameId].puzzle.tiles[idx]
}
const getTileDrawOffset = (gameId) => {
const getTileDrawOffset = (gameId: string) => {
return GAMES[gameId].puzzle.info.tileDrawOffset
}
const getTileDrawSize = (gameId) => {
const getTileDrawSize = (gameId: string) => {
return GAMES[gameId].puzzle.info.tileDrawSize
}
const getTileSize = (gameId) => {
const getTileSize = (gameId: string) => {
return GAMES[gameId].puzzle.info.tileSize
}
const getStartTs = (gameId) => {
const getStartTs = (gameId: string) => {
return GAMES[gameId].puzzle.data.started
}
const getFinishTs = (gameId) => {
const getFinishTs = (gameId: string) => {
return GAMES[gameId].puzzle.data.finished
}
const getMaxGroup = (gameId) => {
const getMaxGroup = (gameId: string) => {
return GAMES[gameId].puzzle.data.maxGroup
}
const getMaxZIndex = (gameId) => {
const getMaxZIndex = (gameId: string) => {
return GAMES[gameId].puzzle.data.maxZ
}
const getMaxZIndexByTileIdxs = (gameId, tileIdxs) => {
const getMaxZIndexByTileIdxs = (gameId: string, tileIdxs: Array<number>) => {
let maxZ = 0
for (let tileIdx of tileIdxs) {
let tileZIndex = getTileZIndex(gameId, tileIdx)
@ -297,7 +388,7 @@ const getMaxZIndexByTileIdxs = (gameId, tileIdxs) => {
return maxZ
}
function srcPosByTileIdx(gameId, tileIdx) {
function srcPosByTileIdx(gameId: string, tileIdx: number) {
const info = GAMES[gameId].puzzle.info
const c = Util.coordByTileIdx(info, tileIdx)
@ -307,7 +398,7 @@ function srcPosByTileIdx(gameId, tileIdx) {
return { x: cx, y: cy }
}
function getSurroundingTilesByIdx(gameId, tileIdx) {
function getSurroundingTilesByIdx(gameId: string, tileIdx: number) {
const info = GAMES[gameId].puzzle.info
const c = Util.coordByTileIdx(info, tileIdx)
@ -324,19 +415,19 @@ function getSurroundingTilesByIdx(gameId, tileIdx) {
]
}
const setTilesZIndex = (gameId, tileIdxs, zIndex) => {
const setTilesZIndex = (gameId: string, tileIdxs: Array<number>, zIndex: number) => {
for (let tilesIdx of tileIdxs) {
changeTile(gameId, tilesIdx, { z: zIndex })
}
}
const moveTileDiff = (gameId, tileIdx, diff) => {
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, tileIdxs, diff) => {
const moveTilesDiff = (gameId: string, tileIdxs: Array<number>, diff: Point) => {
const tileDrawSize = getTileDrawSize(gameId)
const bounds = getBounds(gameId)
const cappedDiff = diff
@ -360,20 +451,24 @@ const moveTilesDiff = (gameId, tileIdxs, diff) => {
}
}
const finishTiles = (gameId, tileIdxs) => {
const finishTiles = (gameId: string, tileIdxs: Array<number>) => {
for (let tileIdx of tileIdxs) {
changeTile(gameId, tileIdx, { owner: -1, z: 1 })
}
}
const setTilesOwner = (gameId, tileIdxs, owner) => {
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, tileIdx) {
function getGroupedTileIdxs(gameId: string, tileIdx: number) {
const tiles = GAMES[gameId].puzzle.tiles
const tile = Util.decodeTile(tiles[tileIdx])
@ -393,7 +488,7 @@ function getGroupedTileIdxs(gameId, tileIdx) {
// 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, pos) => {
const freeTileIdxByPos = (gameId: string, pos: Point) => {
let info = GAMES[gameId].puzzle.info
let tiles = GAMES[gameId].puzzle.tiles
@ -421,75 +516,75 @@ const freeTileIdxByPos = (gameId, pos) => {
return tileIdx
}
const getPlayerBgColor = (gameId, playerId) => {
const getPlayerBgColor = (gameId: string, playerId: string) => {
const p = getPlayer(gameId, playerId)
return p ? p.bgcolor : null
}
const getPlayerColor = (gameId, playerId) => {
const getPlayerColor = (gameId: string, playerId: string) => {
const p = getPlayer(gameId, playerId)
return p ? p.color : null
}
const getPlayerName = (gameId, playerId) => {
const getPlayerName = (gameId: string, playerId: string) => {
const p = getPlayer(gameId, playerId)
return p ? p.name : null
}
const getPlayerPoints = (gameId, playerId) => {
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, tileIdx1, tileIdx2) => {
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) => {
const getTableWidth = (gameId: string) => {
return GAMES[gameId].puzzle.info.table.width
}
const getTableHeight = (gameId) => {
const getTableHeight = (gameId: string) => {
return GAMES[gameId].puzzle.info.table.height
}
const getPuzzle = (gameId) => {
const getPuzzle = (gameId: string) => {
return GAMES[gameId].puzzle
}
const getRng = (gameId) => {
const getRng = (gameId: string): Rng => {
return GAMES[gameId].rng.obj
}
const getPuzzleWidth = (gameId) => {
const getPuzzleWidth = (gameId: string) => {
return GAMES[gameId].puzzle.info.width
}
const getPuzzleHeight = (gameId) => {
const getPuzzleHeight = (gameId: string) => {
return GAMES[gameId].puzzle.info.height
}
function handleInput(gameId, playerId, input, ts) {
function handleInput(gameId: string, playerId: string, input: any, ts: number) {
const puzzle = GAMES[gameId].puzzle
const evtInfo = getEvtInfo(gameId, playerId)
const changes = []
const changes = [] as Array<Array<any>>
const _dataChange = () => {
changes.push([Protocol.CHANGE_DATA, puzzle.data])
}
const _tileChange = (tileIdx) => {
const _tileChange = (tileIdx: number) => {
changes.push([
Protocol.CHANGE_TILE,
Util.encodeTile(getTile(gameId, tileIdx)),
])
}
const _tileChanges = (tileIdxs) => {
const _tileChanges = (tileIdxs: Array<number>) => {
for (const tileIdx of tileIdxs) {
_tileChange(tileIdx)
}
@ -503,7 +598,7 @@ function handleInput(gameId, playerId, input, ts) {
}
// put both tiles (and their grouped tiles) in the same group
const groupTiles = (gameId, tileIdx1, tileIdx2) => {
const groupTiles = (gameId: string, tileIdx1: number, tileIdx2: number) => {
const tiles = GAMES[gameId].puzzle.tiles
const group1 = getTileGroup(gameId, tileIdx1)
const group2 = getTileGroup(gameId, tileIdx2)
@ -669,7 +764,12 @@ function handleInput(gameId, playerId, input, ts) {
}
} else {
// Snap to other tiles
const check = (gameId, tileIdx, otherTileIdx, off) => {
const check = (
gameId: string,
tileIdx: number,
otherTileIdx: number,
off: Array<number>
) => {
let info = GAMES[gameId].puzzle.info
if (otherTileIdx < 0) {
return false

View file

@ -1,25 +1,37 @@
function pointSub(a, b) {
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, b) {
function pointAdd(a: Point, b: Point): Point {
return { x: a.x + b.x, y: a.y + b.y }
}
function pointDistance(a, b) {
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, rect) {
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) {
function rectCenter(rect: Rect): Point {
return {
x: rect.x + (rect.w / 2),
y: rect.y + (rect.h / 2),
@ -35,7 +47,7 @@ function rectCenter(rect) {
* @param number y
* @returns {x, y, w, h}
*/
function rectMoved(rect, x, y) {
function rectMoved(rect: Rect, x: number, y: number): Rect {
return {
x: rect.x + x,
y: rect.y + y,
@ -51,7 +63,7 @@ function rectMoved(rect, x, y) {
* @param {x, y, w, h} rectB
* @returns bool
*/
function rectsOverlap(rectA, rectB) {
function rectsOverlap(rectA: Rect, rectB: Rect): boolean {
return !(
rectB.x > (rectA.x + rectA.w)
|| rectA.x > (rectB.x + rectB.w)
@ -60,7 +72,7 @@ function rectsOverlap(rectA, rectB) {
)
}
function rectCenterDistance(rectA, rectB) {
function rectCenterDistance(rectA: Rect, rectB: Rect): number {
return pointDistance(rectCenter(rectA), rectCenter(rectB))
}

View file

@ -1,24 +1,32 @@
interface RngSerialized {
rand_high: number,
rand_low: number,
}
export class Rng {
constructor(seed) {
rand_high: number
rand_low: number
constructor(seed: number) {
this.rand_high = seed || 0xDEADC0DE
this.rand_low = seed ^ 0x49616E42
}
random (min, max) {
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) {
static serialize (rng: Rng): RngSerialized {
return {
rand_high: rng.rand_high,
rand_low: rng.rand_low
}
}
static unserialize (rngSerialized) {
static unserialize (rngSerialized: RngSerialized): Rng {
const rng = new Rng(0)
rng.rand_high = rngSerialized.rand_high
rng.rand_low = rngSerialized.rand_low

View file

@ -17,7 +17,7 @@ export const timestamp = () => {
)
}
export const durationStr = (duration) => {
export const durationStr = (duration: number) => {
const d = Math.floor(duration / DAY)
duration = duration % DAY
@ -32,7 +32,7 @@ export const durationStr = (duration) => {
return `${d}d ${h}h ${m}m ${s}s`
}
export const timeDiffStr = (from, to) => durationStr(to - from)
export const timeDiffStr = (from: number, to: number) => durationStr(to - from)
export default {
MS,

View file

@ -1,7 +1,7 @@
import { Rng } from './Rng.js'
import { Rng } from './Rng'
const pad = (x, pad) => {
const pad = (x: any, pad: string) => {
const str = `${x}`
if (str.length >= pad.length) {
return str
@ -9,8 +9,8 @@ const pad = (x, pad) => {
return pad.substr(0, pad.length - str.length) + str
}
export const logger = (...pre) => {
const log = (m) => (...args) => {
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')
@ -29,34 +29,21 @@ export const uniqId = () => Date.now().toString(36) + Math.random().toString(36)
// get a random int between min and max (inclusive)
export const randomInt = (
/** @type Rng */ rng,
min,
max
rng: Rng,
min: number,
max: number,
) => rng.random(min, max)
// get one random item from the given array
export const choice = (
/** @type Rng */ rng,
array
rng: Rng,
array: Array<any>
) => array[randomInt(rng, 0, array.length - 1)]
export const throttle = (fn, delay) => {
let canCall = true
return (...args) => {
if (canCall) {
fn.apply(null, args)
canCall = false
setTimeout(() => {
canCall = true
}, delay)
}
}
}
// return a shuffled (shallow) copy of the given array
export const shuffle = (
/** @type Rng */ rng,
array
rng: Rng,
array: Array<any>
) => {
const arr = array.slice()
for (let i = 0; i <= arr.length - 2; i++)
@ -69,7 +56,7 @@ export const shuffle = (
return arr
}
function encodeShape(data) {
function encodeShape(data: any): number {
if (typeof data === 'number') {
return data
}
@ -86,7 +73,7 @@ function encodeShape(data) {
| ((data.left + 1) << 6)
}
function decodeShape(data) {
function decodeShape(data: any) {
if (typeof data !== 'number') {
return data
}
@ -98,14 +85,14 @@ function decodeShape(data) {
}
}
function encodeTile(data) {
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) {
function decodeTile(data: any) {
if (!Array.isArray(data)) {
return data
}
@ -121,7 +108,7 @@ function decodeTile(data) {
}
}
function encodePlayer(data) {
function encodePlayer(data: any): Array<any> {
if (Array.isArray(data)) {
return data
}
@ -138,7 +125,7 @@ function encodePlayer(data) {
]
}
function decodePlayer(data) {
function decodePlayer(data: any) {
if (!Array.isArray(data)) {
return data
}
@ -155,7 +142,7 @@ function decodePlayer(data) {
}
}
function encodeGame(data) {
function encodeGame(data: any): Array<any> {
if (Array.isArray(data)) {
return data
}
@ -170,7 +157,7 @@ function encodeGame(data) {
]
}
function decodeGame(data) {
function decodeGame(data: any) {
if (!Array.isArray(data)) {
return data
}
@ -187,7 +174,7 @@ function decodeGame(data) {
}
}
function coordByTileIdx(info, tileIdx) {
function coordByTileIdx(info: any, tileIdx: number) {
const wTiles = info.width / info.tileSize
return {
x: tileIdx % wTiles,
@ -195,7 +182,7 @@ function coordByTileIdx(info, tileIdx) {
}
}
const hash = (str) => {
const hash = (str: string): number => {
let hash = 0
for (let i = 0; i < str.length; i++) {
@ -211,7 +198,6 @@ export default {
uniqId,
randomInt,
choice,
throttle,
shuffle,
encodeShape,

View file

@ -1,6 +1,4 @@
export default {
name: 'app',
template: `
<template>
<div id="app">
<ul class="nav" v-if="showNav">
<li><router-link class="btn" :to="{name: 'index'}">Index</router-link></li>
@ -8,11 +6,18 @@ export default {
</ul>
<router-view />
</div>`,
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'app',
computed: {
showNav () {
// TODO: add info wether to show nav to route props
return !['game', 'replay'].includes(this.$route.name)
return !['game', 'replay'].includes(String(this.$route.name))
},
},
}
})
</script>

View file

@ -1,31 +1,39 @@
"use strict"
const MIN_ZOOM = .1
const MAX_ZOOM = 6
const ZOOM_STEP = .05
type ZOOM_DIR = 'in'|'out'
interface Point {
x: number
y: number
}
interface Dim {
w: number
h: number
}
export default function Camera () {
let x = 0
let y = 0
let curZoom = 1
const move = (byX, byY) => {
const move = (byX: number, byY: number) => {
x += byX / curZoom
y += byY / curZoom
}
const calculateNewZoom = (inout) => {
const calculateNewZoom = (inout: ZOOM_DIR): number => {
const factor = inout === 'in' ? 1 : -1
const newzoom = curZoom + ZOOM_STEP * curZoom * factor
const capped = Math.min(Math.max(newzoom, MIN_ZOOM), MAX_ZOOM)
return capped
}
const canZoom = (inout) => {
const canZoom = (inout: ZOOM_DIR): boolean => {
return curZoom != calculateNewZoom(inout)
}
const setZoom = (newzoom, viewportCoordCenter) => {
const setZoom = (newzoom: number, viewportCoordCenter: Point): boolean => {
if (curZoom == newzoom) {
return false
}
@ -43,7 +51,7 @@ export default function Camera () {
* Zooms towards/away from the provided coordinate, if possible.
* If at max or min zoom respectively, no zooming is performed.
*/
const zoom = (inout, viewportCoordCenter) => {
const zoom = (inout: ZOOM_DIR, viewportCoordCenter: Point): boolean => {
return setZoom(calculateNewZoom(inout), viewportCoordCenter)
}
@ -52,7 +60,7 @@ export default function Camera () {
* coordinate in the world, rounded
* @param {x, y} viewportCoord
*/
const viewportToWorld = (viewportCoord) => {
const viewportToWorld = (viewportCoord: Point): Point => {
const { x, y } = viewportToWorldRaw(viewportCoord)
return { x: Math.round(x), y: Math.round(y) }
}
@ -62,7 +70,7 @@ export default function Camera () {
* coordinate in the world, not rounded
* @param {x, y} viewportCoord
*/
const viewportToWorldRaw = (viewportCoord) => {
const viewportToWorldRaw = (viewportCoord: Point): Point => {
return {
x: (viewportCoord.x / curZoom) - x,
y: (viewportCoord.y / curZoom) - y,
@ -74,7 +82,7 @@ export default function Camera () {
* coordinate in the viewport, rounded
* @param {x, y} worldCoord
*/
const worldToViewport = (worldCoord) => {
const worldToViewport = (worldCoord: Point): Point => {
const { x, y } = worldToViewportRaw(worldCoord)
return { x: Math.round(x), y: Math.round(y) }
}
@ -84,7 +92,7 @@ export default function Camera () {
* coordinate in the viewport, not rounded
* @param {x, y} worldCoord
*/
const worldToViewportRaw = (worldCoord) => {
const worldToViewportRaw = (worldCoord: Point): Point => {
return {
x: (worldCoord.x + x) * curZoom,
y: (worldCoord.y + y) * curZoom,
@ -96,7 +104,7 @@ export default function Camera () {
* one in the viewport, rounded
* @param {w, h} worldDim
*/
const worldDimToViewport = (worldDim) => {
const worldDimToViewport = (worldDim: Dim): Dim => {
const { w, h } = worldDimToViewportRaw(worldDim)
return { w: Math.round(w), h: Math.round(h) }
}
@ -107,7 +115,7 @@ export default function Camera () {
* one in the viewport, not rounded
* @param {w, h} worldDim
*/
const worldDimToViewportRaw = (worldDim) => {
const worldDimToViewportRaw = (worldDim: Dim): Dim => {
return {
w: worldDim.w * curZoom,
h: worldDim.h * curZoom,

View file

@ -1,7 +1,7 @@
"use strict"
import { logger } from '../common/Util.js'
import Protocol from './../common/Protocol.js'
import { logger } from '../common/Util'
import Protocol from './../common/Protocol'
const log = logger('Communication.js')
@ -14,27 +14,26 @@ const CONN_STATE_CONNECTED = 2 // connected
const CONN_STATE_CONNECTING = 3 // connecting
const CONN_STATE_CLOSED = 4 // not connected (closed on purpose)
/** @type WebSocket */
let ws
let changesCallback = () => {}
let connectionStateChangeCallback = () => {}
let ws: WebSocket
let changesCallback = (msg: Array<any>) => {}
let connectionStateChangeCallback = (state: number) => {}
// TODO: change these to something like on(EVT, cb)
function onServerChange(callback) {
function onServerChange(callback: (msg: Array<any>) => void) {
changesCallback = callback
}
function onConnectionStateChange(callback) {
function onConnectionStateChange(callback: (state: number) => void) {
connectionStateChangeCallback = callback
}
let connectionState = CONN_STATE_NOT_CONNECTED
const setConnectionState = (v) => {
if (connectionState !== v) {
connectionState = v
connectionStateChangeCallback(v)
const setConnectionState = (state: number) => {
if (connectionState !== state) {
connectionState = state
connectionStateChangeCallback(state)
}
}
function send(message) {
function send(message: Array<any>): void {
if (connectionState === CONN_STATE_CONNECTED) {
try {
ws.send(JSON.stringify(message))
@ -45,9 +44,14 @@ function send(message) {
}
let clientSeq
let events
function connect(address, gameId, clientId) {
let clientSeq: number
let events: Record<number, any>
function connect(
address: string,
gameId: string,
clientId: string
): Promise<any> {
clientSeq = 0
events = {}
setConnectionState(CONN_STATE_CONNECTING)
@ -55,7 +59,6 @@ function connect(address, gameId, clientId) {
ws = new WebSocket(address, clientId + '|' + gameId)
ws.onopen = (e) => {
setConnectionState(CONN_STATE_CONNECTED)
connectionStateChangeCallback()
send([Protocol.EV_CLIENT_INIT])
}
ws.onmessage = (e) => {
@ -94,7 +97,11 @@ function connect(address, gameId, clientId) {
}
// TOOD: change replay stuff
function connectReplay(address, gameId, clientId) {
function connectReplay(
address: string,
gameId: string,
clientId: string
): Promise<{ game: any, log: Array<any> }> {
clientSeq = 0
events = {}
setConnectionState(CONN_STATE_CONNECTING)
@ -110,7 +117,8 @@ function connectReplay(address, gameId, clientId) {
if (msgType === Protocol.EV_SERVER_INIT_REPLAY) {
const game = msg[1]
const log = msg[2]
resolve({game, log})
const replay: { game: any, log: Array<any> } = { game, log }
resolve(replay)
} else {
throw `[ 2021-05-09 invalid connectReplay msgType ${msgType} ]`
}
@ -131,7 +139,7 @@ function connectReplay(address, gameId, clientId) {
})
}
function disconnect() {
function disconnect(): void {
if (ws) {
ws.close(CODE_CUSTOM_DISCONNECT)
}
@ -139,7 +147,7 @@ function disconnect() {
events = {}
}
function sendClientEvent(evt) {
function sendClientEvent(evt: any): void {
// when sending event, increase number of sent events
// and add the event locally
clientSeq++;

View file

@ -1,18 +1,18 @@
"use strict"
import { logger } from '../common/Util.js'
import { logger } from '../common/Util'
const log = logger('Debug.js')
let _pt = 0
let _mindiff = 0
const checkpoint_start = (mindiff) => {
const checkpoint_start = (mindiff: number) => {
_pt = performance.now()
_mindiff = mindiff
}
const checkpoint = (label) => {
const checkpoint = (label: string) => {
const now = performance.now()
const diff = now - _pt
if (diff > _mindiff) {

View file

@ -1,7 +1,7 @@
"use strict"
import { Rng } from '../common/Rng.js'
import Util from '../common/Util.js'
import { Rng } from '../common/Rng'
import Util from '../common/Util'
let minVx = -10
let deltaVx = 20
@ -20,7 +20,7 @@ const explosionDividerFactor = 10
const nBombs = 1
const percentChanceNewBomb = 5
function color(/** @type Rng */ rng) {
function color(rng: Rng) {
const r = Util.randomInt(rng, 0, 255)
const g = Util.randomInt(rng, 0, 255)
const b = Util.randomInt(rng, 0, 255)
@ -29,7 +29,19 @@ function color(/** @type Rng */ rng) {
// A Bomb. Or firework.
class Bomb {
constructor(/** @type Rng */ rng) {
radius: number
previousRadius: number
explodingDuration: number
hasExploded: boolean
alive: boolean
color: string
px: number
py: number
vx: number
vy: number
duration: number
constructor(rng: Rng) {
this.radius = bombRadius
this.previousRadius = bombRadius
this.explodingDuration = explodingDuration
@ -46,7 +58,7 @@ class Bomb {
this.duration = 0
}
update(particlesVector) {
update(particlesVector?: Array<Particle>) {
if (this.hasExploded) {
const deltaRadius = explosionRadius - this.radius
this.previousRadius = this.radius
@ -60,15 +72,17 @@ class Bomb {
this.vx += 0
this.vy += gravity
if (this.vy >= 0) { // invertion point
if (particlesVector) {
this.explode(particlesVector)
}
}
this.px += this.vx
this.py += this.vy
}
}
draw(ctx) {
draw(ctx: CanvasRenderingContext2D) {
ctx.beginPath()
ctx.arc(this.px, this.py, this.previousRadius, 0, Math.PI * 2, false)
if (!this.hasExploded) {
@ -78,7 +92,7 @@ class Bomb {
}
}
explode(particlesVector) {
explode(particlesVector: Array<Particle>) {
this.hasExploded = true
const e = 3 + Math.floor(Math.random() * 3)
for (let j = 0; j < e; j++) {
@ -94,7 +108,15 @@ class Bomb {
}
class Particle {
constructor(parent, angle, speed) {
px: any
py: any
vx: number
vy: number
color: any
duration: number
alive: boolean
radius: number
constructor(parent: Bomb, angle: number, speed: number) {
this.px = parent.px
this.py = parent.py
this.vx = Math.cos(angle) * speed
@ -102,6 +124,7 @@ class Particle {
this.color = parent.color
this.duration = 40 + Math.floor(Math.random() * 20)
this.alive = true
this.radius = 0
}
update() {
this.vx += 0
@ -117,7 +140,7 @@ class Particle {
}
}
draw(ctx) {
draw(ctx: CanvasRenderingContext2D) {
ctx.beginPath()
ctx.arc(this.px, this.py, this.radius, 0, Math.PI * 2, false)
ctx.fillStyle = this.color
@ -127,12 +150,22 @@ class Particle {
}
class Controller {
constructor(canvas, /** @type Rng */ rng) {
canvas: HTMLCanvasElement
rng: Rng
ctx: CanvasRenderingContext2D
readyBombs: Array<Bomb>
explodedBombs: Array<Bomb>
particles: Array<Particle>
constructor(canvas: HTMLCanvasElement, rng: Rng) {
this.canvas = canvas
this.rng = rng
this.ctx = this.canvas.getContext('2d')
this.ctx = this.canvas.getContext('2d') as CanvasRenderingContext2D
this.resize()
this.readyBombs = []
this.explodedBombs = []
this.particles = []
window.addEventListener('resize', () => {
this.resize()
})
@ -177,6 +210,9 @@ class Controller {
const aliveBombs = []
while (this.explodedBombs.length > 0) {
const bomb = this.explodedBombs.shift()
if (!bomb) {
break;
}
bomb.update()
if (bomb.alive) {
aliveBombs.push(bomb)
@ -187,6 +223,9 @@ class Controller {
const notExplodedBombs = []
while (this.readyBombs.length > 0) {
const bomb = this.readyBombs.shift()
if (!bomb) {
break
}
bomb.update(this.particles)
if (bomb.hasExploded) {
this.explodedBombs.push(bomb)
@ -200,6 +239,9 @@ class Controller {
const aliveParticles = []
while (this.particles.length > 0) {
const particle = this.particles.shift()
if (!particle) {
break
}
particle.update()
if (particle.alive) {
aliveParticles.push(particle)

View file

@ -1,13 +1,13 @@
"use strict"
function createCanvas(width = 0, height = 0) {
function createCanvas(width:number = 0, height:number = 0): HTMLCanvasElement {
const c = document.createElement('canvas')
c.width = width
c.height = height
return c
}
async function loadImageToBitmap(imagePath) {
async function loadImageToBitmap(imagePath: string): Promise<ImageBitmap> {
return new Promise((resolve) => {
const img = new Image()
img.onload = () => {
@ -17,24 +17,16 @@ async function loadImageToBitmap(imagePath) {
})
}
async function resizeBitmap (bitmap, width, height) {
async function resizeBitmap (bitmap: ImageBitmap, width: number, height: number): Promise<ImageBitmap> {
const c = createCanvas(width, height)
const ctx = c.getContext('2d')
const ctx = c.getContext('2d') as CanvasRenderingContext2D
ctx.drawImage(bitmap, 0, 0, bitmap.width, bitmap.height, 0, 0, width, height)
return await createImageBitmap(c)
}
async function createBitmap(width, height, color) {
const c = createCanvas(width, height)
const ctx = c.getContext('2d')
ctx.fillStyle = color
ctx.fillRect(0, 0, width, height)
return await createImageBitmap(c)
}
async function colorize(image, mask, color) {
async function colorize(image: ImageBitmap, mask: ImageBitmap, color: string): Promise<ImageBitmap> {
const c = createCanvas(image.width, image.height)
const ctx = c.getContext('2d')
const ctx = c.getContext('2d') as CanvasRenderingContext2D
ctx.save()
ctx.drawImage(mask, 0, 0)
ctx.fillStyle = color
@ -49,7 +41,6 @@ async function colorize(image, mask, color) {
}
export default {
createBitmap,
createCanvas,
loadImageToBitmap,
resizeBitmap,

View file

@ -1,12 +1,13 @@
"use strict"
import Geometry from '../common/Geometry.js'
import Graphics from './Graphics.js'
import Util, { logger } from './../common/Util.js'
import Geometry from '../common/Geometry'
import Graphics from './Graphics'
import Util, { logger } from './../common/Util'
import { Puzzle, PuzzleInfo, PieceShape } from './../common/GameCommon'
const log = logger('PuzzleGraphics.js')
async function createPuzzleTileBitmaps(img, tiles, info) {
async function createPuzzleTileBitmaps(img: ImageBitmap, tiles: Array<any>, info: PuzzleInfo) {
log.log('start createPuzzleTileBitmaps')
var tileSize = info.tileSize
var tileMarginWidth = info.tileMarginWidth
@ -24,8 +25,8 @@ async function createPuzzleTileBitmaps(img, tiles, info) {
const bitmaps = new Array(tiles.length)
const paths = {}
function pathForShape(shape) {
const paths: Record<string, Path2D> = {}
function pathForShape(shape: PieceShape) {
const key = `${shape.top}${shape.right}${shape.left}${shape.bottom}`
if (paths[key]) {
return paths[key]
@ -83,10 +84,10 @@ async function createPuzzleTileBitmaps(img, tiles, info) {
}
const c = Graphics.createCanvas(tileDrawSize, tileDrawSize)
const ctx = c.getContext('2d')
const ctx = c.getContext('2d') as CanvasRenderingContext2D
const c2 = Graphics.createCanvas(tileDrawSize, tileDrawSize)
const ctx2 = c2.getContext('2d')
const ctx2 = c2.getContext('2d') as CanvasRenderingContext2D
for (let t of tiles) {
const tile = Util.decodeTile(t)
@ -197,7 +198,7 @@ async function createPuzzleTileBitmaps(img, tiles, info) {
return bitmaps
}
function srcRectByIdx(puzzleInfo, idx) {
function srcRectByIdx(puzzleInfo: PuzzleInfo, idx: number) {
const c = Util.coordByTileIdx(puzzleInfo, idx)
return {
x: c.x * puzzleInfo.tileSize,
@ -207,7 +208,7 @@ function srcRectByIdx(puzzleInfo, idx) {
}
}
async function loadPuzzleBitmaps(puzzle) {
async function loadPuzzleBitmaps(puzzle: Puzzle) {
// load bitmap, to determine the original size of the image
const bmp = await Graphics.loadImageToBitmap(puzzle.info.imageUrl)

View file

@ -1,10 +1,4 @@
"use strict"
import Communication from './../Communication.js'
export default {
name: 'connection-overlay',
template: `
<template>
<div class="overlay connection-lost" v-if="show">
<div class="overlay-content" v-if="lostConnection">
<div> LOST CONNECTION </div>
@ -13,7 +7,15 @@ export default {
<div class="overlay-content" v-if="connecting">
<div>Connecting...</div>
</div>
</div>`,
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import Communication from './../Communication'
export default defineComponent({
name: 'connection-overlay',
emits: {
reconnect: null,
},
@ -21,14 +23,15 @@ export default {
connectionState: Number,
},
computed: {
lostConnection () {
lostConnection (): boolean {
return this.connectionState === Communication.CONN_STATE_DISCONNECTED
},
connecting () {
connecting (): boolean {
return this.connectionState === Communication.CONN_STATE_CONNECTING
},
show () {
return this.lostConnection || this.connecting
show (): boolean {
return !!(this.lostConnection || this.connecting)
},
}
}
})
</script>

View file

@ -1,13 +1,4 @@
"use strict"
import Time from './../../common/Time.js'
export default {
name: 'game-teaser',
props: {
game: Object,
},
template: `
<template>
<div class="game-teaser" :style="style">
<router-link class="game-info" :to="{ name: 'game', params: { id: game.id } }">
<span class="game-info-text">
@ -19,9 +10,22 @@ export default {
<router-link v-if="false && game.hasReplay" class="game-replay" :to="{ name: 'replay', params: { id: game.id } }">
Watch replay
</router-link>
</div>`,
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import Time from './../../common/Time'
export default defineComponent({
name: 'game-teaser',
props: {
game: {
type: Object,
required: true,
},
},
computed: {
style() {
style (): object {
const url = this.game.imageUrl.replace('uploads/', 'uploads/r/') + '-375x210.webp'
return {
'background-image': `url("${url}")`,
@ -29,7 +33,7 @@ export default {
},
},
methods: {
time(start, end) {
time(start: number, end: number) {
const icon = end ? '🏁' : '⏳'
const from = start;
const to = end || Time.timestamp()
@ -37,4 +41,5 @@ export default {
return `${icon} ${timeDiffStr}`
},
},
}
})
</script>

View file

@ -1,11 +1,5 @@
"use strict"
// ingame component
// shows the help (key bindings)
export default {
name: 'help-overlay',
template: `<div class="overlay transparent" @click="$emit('bgclick')">
<template>
<div class="overlay transparent" @click="$emit('bgclick')">
<table class="overlay-content help" @click.stop="">
<tr><td> Move up:</td><td><div><kbd>W</kbd>/<kbd></kbd>/🖱</div></td></tr>
<tr><td> Move down:</td><td><div><kbd>S</kbd>/<kbd></kbd>/🖱</div></td></tr>
@ -19,8 +13,15 @@ export default {
<tr><td>🧩 Toggle fixed pieces:</td><td><div><kbd>F</kbd></div></td></tr>
<tr><td>🧩 Toggle loose pieces:</td><td><div><kbd>G</kbd></div></td></tr>
</table>
</div>`,
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'help-overlay',
emits: {
bgclick: null,
},
}
})
</script>

View file

@ -0,0 +1,29 @@
<template>
<div class="imageteaser" :style="style" @click="onClick"></div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'image-teaser',
props: {
image: {
type: Object,
required: true,
},
},
computed: {
style (): object {
const url = this.image.url.replace('uploads/', 'uploads/r/') + '-150x100.webp'
return {
'backgroundImage': `url("${url}")`,
}
},
},
methods: {
onClick() {
this.$emit('click')
},
},
})
</script>

View file

@ -1,16 +1,4 @@
"use strict"
import GameCommon from './../../common/GameCommon.js'
import Upload from './../components/Upload.vue.js'
import ImageTeaser from './../components/ImageTeaser.vue.js'
export default {
name: 'new-game-dialog',
components: {
Upload,
ImageTeaser,
},
template: `
<template>
<div>
<h1>New game</h1>
<table>
@ -49,9 +37,23 @@ export default {
<h1>Image lib</h1>
<div>
<image-teaser v-for="i in images" :image="i" @click="image = i" />
<image-teaser v-for="(i,idx) in images" :image="i" @click="image = i" :key="idx" />
</div>
</div>`,
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import GameCommon from './../../common/GameCommon'
import Upload from './../components/Upload.vue'
import ImageTeaser from './../components/ImageTeaser.vue'
export default defineComponent({
name: 'new-game-dialog',
components: {
Upload,
ImageTeaser,
},
props: {
images: Array,
},
@ -66,8 +68,9 @@ export default {
}
},
methods: {
mediaImgUploaded(j) {
this.image = j.image
// TODO: ts type UploadedImage
mediaImgUploaded(data: any) {
this.image = data.image
},
canStartNewGame () {
if (!this.tilesInt || !this.image || ![0, 1].includes(this.scoreModeInt)) {
@ -84,11 +87,12 @@ export default {
},
},
computed: {
scoreModeInt () {
return parseInt(this.scoreMode, 10)
scoreModeInt (): number {
return parseInt(`${this.scoreMode}`, 10)
},
tilesInt () {
return parseInt(this.tiles, 10)
tilesInt (): number {
return parseInt(`${this.tiles}`, 10)
},
},
}
})
</script>

View file

@ -0,0 +1,27 @@
<template>
<div class="overlay" @click="$emit('bgclick')">
<div class="preview">
<div class="img" :style="previewStyle"></div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'preview-overlay',
props: {
img: String,
},
emits: {
bgclick: null,
},
computed: {
previewStyle (): object {
return {
backgroundImage: `url('${this.img}')`,
}
},
},
})
</script>

View file

@ -0,0 +1,45 @@
<template>
<div class="timer">
<div>
🧩 {{piecesDone}}/{{piecesTotal}}
</div>
<div>
{{icon}} {{durationStr}}
</div>
<slot />
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import Time from './../../common/Time'
export default defineComponent({
name: 'puzzle-status',
props: {
finished: {
type: Boolean,
required: true,
},
duration: {
type: Number,
required: true,
},
piecesDone: {
type: Number,
required: true,
},
piecesTotal: {
type: Number,
required: true,
},
},
computed: {
icon (): string {
return this.finished ? '🏁' : '⏳'
},
durationStr (): string {
return Time.durationStr(this.duration)
},
}
})
</script>

View file

@ -1,11 +1,4 @@
"use strict"
// ingame component
// shows player scores
export default {
name: "scores",
template: `
<template>
<div class="scores">
<div>Scores</div>
<table>
@ -21,21 +14,33 @@ export default {
</tr>
</table>
</div>
`,
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: "scores",
props: {
activePlayers: {
type: Array,
required: true,
},
idlePlayers: {
type: Array,
required: true,
},
},
computed: {
actives () {
actives (): Array<any> {
// TODO: dont sort in place
this.activePlayers.sort((a, b) => b.points - a.points)
this.activePlayers.sort((a: any, b: any) => b.points - a.points)
return this.activePlayers
},
idles () {
idles (): Array<any> {
// TODO: dont sort in place
this.idlePlayers.sort((a, b) => b.points - a.points)
this.idlePlayers.sort((a: any, b: any) => b.points - a.points)
return this.idlePlayers
},
},
props: {
activePlayers: Array,
idlePlayers: Array,
},
}
})
</script>

View file

@ -0,0 +1,38 @@
<template>
<div class="overlay transparent" @click="$emit('bgclick')">
<table class="overlay-content settings" @click.stop="">
<tr>
<td><label>Background: </label></td>
<td><input type="color" v-model="modelValue.background" /></td>
</tr>
<tr>
<td><label>Color: </label></td>
<td><input type="color" v-model="modelValue.color" /></td>
</tr>
<tr>
<td><label>Name: </label></td>
<td><input type="text" maxLength="16" v-model="modelValue.name" /></td>
</tr>
</table>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'settings-overlay',
emits: {
bgclick: null,
'update:modelValue': null,
},
props: {
modelValue: Object,
},
created () {
// TODO: ts type PlayerSettings
this.$watch('modelValue', (val: any) => {
this.$emit('update:modelValue', val)
}, { deep: true })
},
})
</script>

View file

@ -1,20 +1,23 @@
"use strict"
<template>
<label>
<input type="file" style="display: none" @change="upload" :accept="accept" />
<span class="btn">{{label || 'Upload File'}}</span>
</label>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default {
export default defineComponent({
name: 'upload',
props: {
accept: String,
label: String,
},
template: `
<label>
<input type="file" style="display: none" @change="upload" :accept="accept" />
<span class="btn">{{label || 'Upload File'}}</span>
</label>
`,
methods: {
async upload(evt) {
const file = evt.target.files[0]
async upload(evt: Event) {
const target = (evt.target as HTMLInputElement)
if (!target.files) return;
const file = target.files[0]
if (!file) return;
const formData = new FormData();
formData.append('file', file, file.name);
@ -26,4 +29,5 @@ export default {
this.$emit('uploaded', j)
},
}
}
})
</script>

View file

@ -1,18 +1,32 @@
"use strict"
import {run} from './gameloop.js'
import Camera from './Camera.js'
import Graphics from './Graphics.js'
import Debug from './Debug.js'
import Communication from './Communication.js'
import Util, { logger } from './../common/Util.js'
import PuzzleGraphics from './PuzzleGraphics.js'
import Game from './../common/GameCommon.js'
import fireworksController from './Fireworks.js'
import Protocol from '../common/Protocol.js'
import Time from '../common/Time.js'
import {run} from './gameloop'
import Camera from './Camera'
import Graphics from './Graphics'
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 fireworksController from './Fireworks'
import Protocol from '../common/Protocol'
import Time from '../common/Time'
// const log = logger('game.js')
declare global {
interface Window {
DEBUG?: boolean
}
}
// @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')
export const MODE_PLAY = 'play'
export const MODE_REPLAY = 'replay'
@ -20,7 +34,34 @@ export const MODE_REPLAY = 'replay'
let PIECE_VIEW_FIXED = true
let PIECE_VIEW_LOOSE = true
const shouldDrawPiece = (piece) => {
interface Point {
x: number
y: number
}
interface Hud {
setActivePlayers: (v: Array<any>) => void
setIdlePlayers: (v: Array<any>) => void
setFinished: (v: boolean) => void
setDuration: (v: number) => void
setPiecesDone: (v: number) => void
setPiecesTotal: (v: number) => void
setConnectionState: (v: number) => void
togglePreview: () => void
setReplaySpeed?: (v: number) => void
setReplayPaused?: (v: boolean) => void
}
interface Replay {
log: Array<any>
logIdx: number
speeds: Array<number>
speedIdx: number
paused: boolean
lastRealTs: number
lastGameTs: number
gameStartTs: number
}
const shouldDrawPiece = (piece: Piece) => {
if (piece.owner === -1) {
return PIECE_VIEW_FIXED
}
@ -29,7 +70,7 @@ const shouldDrawPiece = (piece) => {
let RERENDER = true
function addCanvasToDom(TARGET_EL, canvas) {
function addCanvasToDom(TARGET_EL: HTMLElement, canvas: HTMLCanvasElement) {
canvas.width = window.innerWidth
canvas.height = window.innerHeight
TARGET_EL.appendChild(canvas)
@ -41,8 +82,8 @@ function addCanvasToDom(TARGET_EL, canvas) {
return canvas
}
function EventAdapter (canvas, window, viewport) {
let events = []
function EventAdapter (canvas: HTMLCanvasElement, window: any, viewport: any) {
let events: Array<Array<any>> = []
let KEYS_ON = true
@ -54,15 +95,15 @@ function EventAdapter (canvas, window, viewport) {
let ZOOM_OUT = false
let SHIFT = false
const toWorldPoint = (x, y) => {
const toWorldPoint = (x: number, y: number) => {
const pos = viewport.viewportToWorld({x, y})
return [pos.x, pos.y]
}
const mousePos = (ev) => toWorldPoint(ev.offsetX, ev.offsetY)
const mousePos = (ev: MouseEvent) => toWorldPoint(ev.offsetX, ev.offsetY)
const canvasCenter = () => toWorldPoint(canvas.width / 2, canvas.height / 2)
const key = (state, ev) => {
const key = (state: boolean, ev: KeyboardEvent) => {
if (!KEYS_ON) {
return
}
@ -108,10 +149,10 @@ function EventAdapter (canvas, window, viewport) {
}
})
window.addEventListener('keydown', (ev) => key(true, ev))
window.addEventListener('keyup', (ev) => key(false, ev))
window.addEventListener('keydown', (ev: KeyboardEvent) => key(true, ev))
window.addEventListener('keyup', (ev: KeyboardEvent) => key(false, ev))
window.addEventListener('keypress', (ev) => {
window.addEventListener('keypress', (ev: KeyboardEvent) => {
if (!KEYS_ON) {
return
}
@ -128,7 +169,7 @@ function EventAdapter (canvas, window, viewport) {
}
})
const addEvent = (event) => {
const addEvent = (event: Array<any>) => {
events.push(event)
}
@ -162,7 +203,7 @@ function EventAdapter (canvas, window, viewport) {
}
}
const setHotkeys = (state) => {
const setHotkeys = (state: boolean) => {
KEYS_ON = state
}
@ -175,23 +216,23 @@ function EventAdapter (canvas, window, viewport) {
}
export async function main(
gameId,
clientId,
wsAddress,
MODE,
TARGET_EL,
HUD
gameId: string,
clientId: string,
wsAddress: string,
MODE: string,
TARGET_EL: HTMLElement,
HUD: Hud
) {
if (typeof DEBUG === 'undefined') window.DEBUG = false
if (typeof window.DEBUG === 'undefined') window.DEBUG = false
const shouldDrawPlayerText = (player) => {
const shouldDrawPlayerText = (player: Player) => {
return MODE === MODE_REPLAY || player.id !== clientId
}
const cursorGrab = await Graphics.loadImageToBitmap('/grab.png')
const cursorHand = await Graphics.loadImageToBitmap('/hand.png')
const cursorGrabMask = await Graphics.loadImageToBitmap('/grab_mask.png')
const cursorHandMask = await Graphics.loadImageToBitmap('/hand_mask.png')
const cursorGrab = await Graphics.loadImageToBitmap(images['./grab.png'].default)
const cursorHand = await Graphics.loadImageToBitmap(images['./hand.png'].default)
const cursorGrabMask = await Graphics.loadImageToBitmap(images['./grab_mask.png'].default)
const cursorHandMask = await Graphics.loadImageToBitmap(images['./hand_mask.png'].default)
// all cursors must be of the same dimensions
const CURSOR_W = cursorGrab.width
@ -199,13 +240,17 @@ export async function main(
const CURSOR_H = cursorGrab.height
const CURSOR_H_2 = Math.round(CURSOR_H / 2)
const cursors = {}
const getPlayerCursor = async (p) => {
const cursors: Record<string, ImageBitmap> = {}
const getPlayerCursor = async (p: Player) => {
const key = p.color + ' ' + p.d
if (!cursors[key]) {
const cursor = p.d ? cursorGrab : cursorHand
if (p.color) {
const mask = p.d ? cursorGrabMask : cursorHandMask
cursors[key] = await Graphics.colorize(cursor, mask, p.color)
} else {
cursors[key] = cursor
}
}
return cursors[key]
}
@ -215,22 +260,22 @@ export async function main(
// stuff only available in replay mode...
// TODO: refactor
const REPLAY = {
log: null,
const REPLAY: Replay = {
log: [],
logIdx: 0,
speeds: [0.5, 1, 2, 5, 10, 20, 50],
speedIdx: 1,
paused: false,
lastRealTs: null,
lastGameTs: null,
gameStartTs: null,
lastRealTs: 0,
lastGameTs: 0,
gameStartTs: 0,
}
Communication.onConnectionStateChange((state) => {
HUD.setConnectionState(state)
})
let TIME
let TIME: () => number = () => 0
const connect = async () => {
if (MODE === MODE_PLAY) {
const game = await Communication.connect(wsAddress, gameId, clientId)
@ -239,12 +284,12 @@ export async function main(
TIME = () => Time.timestamp()
} else if (MODE === MODE_REPLAY) {
// TODO: change how replay connect is done...
const {game, log} = await Communication.connectReplay(wsAddress, gameId, clientId)
const gameObject = Util.decodeGame(game)
const replay: {game: any, log: Array<any>} = await Communication.connectReplay(wsAddress, gameId, clientId)
const gameObject = Util.decodeGame(replay.game)
Game.setGame(gameObject.id, gameObject)
REPLAY.log = log
REPLAY.log = replay.log
REPLAY.lastRealTs = Time.timestamp()
REPLAY.gameStartTs = REPLAY.log[0][REPLAY.log[0].length - 2]
REPLAY.gameStartTs = parseInt(REPLAY.log[0][REPLAY.log[0].length - 2], 10)
REPLAY.lastGameTs = REPLAY.gameStartTs
TIME = () => REPLAY.lastGameTs
} else {
@ -280,21 +325,21 @@ export async function main(
const bitmaps = await PuzzleGraphics.loadPuzzleBitmaps(Game.getPuzzle(gameId))
const fireworks = new fireworksController(canvas, Game.getRng(gameId))
fireworks.init(canvas)
fireworks.init()
const ctx = canvas.getContext('2d')
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
canvas.classList.add('loaded')
// initialize some view data
// this global data will change according to input events
const viewport = new Camera()
const viewport = Camera()
// center viewport
viewport.move(
-(TABLE_WIDTH - canvas.width) /2,
-(TABLE_HEIGHT - canvas.height) /2
)
const evts = new EventAdapter(canvas, window, viewport)
const evts = EventAdapter(canvas, window, viewport)
const previewImageUrl = Game.getImageUrl(gameId)
@ -335,9 +380,13 @@ export async function main(
}
const doSetSpeedStatus = () => {
if (HUD.setReplaySpeed) {
HUD.setReplaySpeed(REPLAY.speeds[REPLAY.speedIdx])
}
if (HUD.setReplayPaused) {
HUD.setReplayPaused(REPLAY.paused)
}
}
const replayOnSpeedUp = () => {
if (REPLAY.speedIdx + 1 < REPLAY.speeds.length) {
@ -419,17 +468,18 @@ export async function main(
}
const entryWithTs = logEntry.slice()
entryWithTs[entryWithTs.length - 1] = nextTs
if (entryWithTs[0] === Protocol.LOG_ADD_PLAYER) {
Game.addPlayer(gameId, ...entryWithTs.slice(1))
const playerId = entryWithTs[1]
Game.addPlayer(gameId, playerId, nextTs)
RERENDER = true
} else if (entryWithTs[0] === Protocol.LOG_UPDATE_PLAYER) {
const playerId = Game.getPlayerIdByIndex(gameId, entryWithTs[1])
Game.addPlayer(gameId, playerId, ...entryWithTs.slice(2))
Game.addPlayer(gameId, playerId, nextTs)
RERENDER = true
} else if (entryWithTs[0] === Protocol.LOG_HANDLE_INPUT) {
const playerId = Game.getPlayerIdByIndex(gameId, entryWithTs[1])
Game.handleInput(gameId, playerId, ...entryWithTs.slice(2))
const input = entryWithTs[2]
Game.handleInput(gameId, playerId, input, nextTs)
RERENDER = true
}
REPLAY.logIdx = nextIdx
@ -440,7 +490,7 @@ export async function main(
}, 50)
}
let _last_mouse_down = null
let _last_mouse_down: Point|null = null
const onUpdate = () => {
// handle key downs once per onUpdate
// this will create Protocol.INPUT_EV_MOVE events if something
@ -552,13 +602,13 @@ export async function main(
let dim
let bmp
if (DEBUG) Debug.checkpoint_start(0)
if (window.DEBUG) Debug.checkpoint_start(0)
// CLEAR CTX
// ---------------------------------------------------------------
ctx.fillStyle = playerBgColor()
ctx.fillRect(0, 0, canvas.width, canvas.height)
if (DEBUG) Debug.checkpoint('clear done')
if (window.DEBUG) Debug.checkpoint('clear done')
// ---------------------------------------------------------------
@ -568,14 +618,14 @@ export async function main(
dim = viewport.worldDimToViewportRaw(BOARD_DIM)
ctx.fillStyle = 'rgba(255, 255, 255, .3)'
ctx.fillRect(pos.x, pos.y, dim.w, dim.h)
if (DEBUG) Debug.checkpoint('board done')
if (window.DEBUG) Debug.checkpoint('board done')
// ---------------------------------------------------------------
// DRAW TILES
// ---------------------------------------------------------------
const tiles = Game.getTilesSortedByZIndex(gameId)
if (DEBUG) Debug.checkpoint('get tiles done')
if (window.DEBUG) Debug.checkpoint('get tiles done')
dim = viewport.worldDimToViewportRaw(PIECE_DIM)
for (const tile of tiles) {
@ -592,13 +642,13 @@ export async function main(
pos.x, pos.y, dim.w, dim.h
)
}
if (DEBUG) Debug.checkpoint('tiles done')
if (window.DEBUG) Debug.checkpoint('tiles done')
// ---------------------------------------------------------------
// DRAW PLAYERS
// ---------------------------------------------------------------
const texts = []
const texts: Array<FixedLengthArray<[string, number, number]>> = []
// Cursors
for (const p of Game.getActivePlayers(gameId, ts)) {
bmp = await getPlayerCursor(p)
@ -619,14 +669,14 @@ export async function main(
ctx.fillText(txt, x, y)
}
if (DEBUG) Debug.checkpoint('players done')
if (window.DEBUG) Debug.checkpoint('players done')
// propagate HUD changes
// ---------------------------------------------------------------
HUD.setActivePlayers(Game.getActivePlayers(gameId, ts))
HUD.setIdlePlayers(Game.getIdlePlayers(gameId, ts))
HUD.setPiecesDone(Game.getFinishedTileCount(gameId))
if (DEBUG) Debug.checkpoint('HUD done')
if (window.DEBUG) Debug.checkpoint('HUD done')
// ---------------------------------------------------------------
if (justFinished()) {
@ -642,18 +692,18 @@ export async function main(
})
return {
setHotkeys: (state) => {
setHotkeys: (state: boolean) => {
evts.setHotkeys(state)
},
onBgChange: (value) => {
onBgChange: (value: string) => {
localStorage.setItem('bg_color', value)
evts.addEvent([Protocol.INPUT_EV_BG_COLOR, value])
},
onColorChange: (value) => {
onColorChange: (value: string) => {
localStorage.setItem('player_color', value)
evts.addEvent([Protocol.INPUT_EV_PLAYER_COLOR, value])
},
onNameChange: (value) => {
onNameChange: (value: string) => {
localStorage.setItem('player_name', value)
evts.addEvent([Protocol.INPUT_EV_PLAYER_NAME, value])
},

View file

@ -1,6 +1,12 @@
"use strict"
export const run = options => {
interface GameLoopOptions {
fps?: number
slow?: number
update: (step: number) => any
render: (passed: number) => any
}
export const run = (options: GameLoopOptions) => {
const fps = options.fps || 60
const slow = options.slow || 1
const update = options.update

View file

Before

Width:  |  Height:  |  Size: 148 B

After

Width:  |  Height:  |  Size: 148 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 125 B

After

Width:  |  Height:  |  Size: 125 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 170 B

After

Width:  |  Height:  |  Size: 170 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 140 B

After

Width:  |  Height:  |  Size: 140 B

Before After
Before After

10
src/frontend/index.html Normal file
View file

@ -0,0 +1,10 @@
<html>
<head>
<link rel="stylesheet" href="/style.css" />
<title>🧩 jigsaw.hyottoko.club</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/main.ts"></script>
</body>
</html>

46
src/frontend/main.ts Normal file
View file

@ -0,0 +1,46 @@
import * as VueRouter from 'vue-router'
import * as Vue from 'vue'
import App from './App.vue'
import Index from './views/Index.vue'
import NewGame from './views/NewGame.vue'
import Game from './views/Game.vue'
import Replay from './views/Replay.vue'
import Util from './../common/Util'
(async () => {
const res = await fetch(`/api/conf`)
const conf = await res.json()
function initme() {
let ID = localStorage.getItem('ID')
if (!ID) {
ID = Util.uniqId()
localStorage.setItem('ID', ID)
}
return ID
}
const router = VueRouter.createRouter({
history: VueRouter.createWebHashHistory(),
routes: [
{ name: 'index', path: '/', component: Index },
{ name: 'new-game', path: '/new-game', component: NewGame },
{ name: 'game', path: '/g/:id', component: Game },
{ name: 'replay', path: '/replay/:id', component: Replay },
],
})
router.beforeEach((to, from) => {
if (from.name) {
document.documentElement.classList.remove(`view-${String(from.name)}`)
}
document.documentElement.classList.add(`view-${String(to.name)}`)
})
const app = Vue.createApp(App)
app.config.globalProperties.$config = conf
app.config.globalProperties.$clientId = initme()
app.use(router)
app.mount('#app')
})()

5
src/frontend/shims-vue.d.ts vendored Normal file
View file

@ -0,0 +1,5 @@
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

View file

@ -1,25 +1,5 @@
"use strict"
import Scores from './../components/Scores.vue.js'
import PuzzleStatus from './../components/PuzzleStatus.vue.js'
import SettingsOverlay from './../components/SettingsOverlay.vue.js'
import PreviewOverlay from './../components/PreviewOverlay.vue.js'
import ConnectionOverlay from './../components/ConnectionOverlay.vue.js'
import HelpOverlay from './../components/HelpOverlay.vue.js'
import { main, MODE_PLAY } from './../game.js'
export default {
name: 'game',
components: {
PuzzleStatus,
Scores,
SettingsOverlay,
PreviewOverlay,
ConnectionOverlay,
HelpOverlay,
},
template: `<div id="game">
<template>
<div id="game">
<settings-overlay v-show="overlay === 'settings'" @bgclick="toggle('settings', true)" v-model="g.player" />
<preview-overlay v-show="overlay === 'preview'" @bgclick="toggle('preview', false)" :img="g.previewImageUrl" />
<help-overlay v-show="overlay === 'help'" @bgclick="toggle('help', true)" />
@ -46,18 +26,42 @@ export default {
</div>
<scores :activePlayers="activePlayers" :idlePlayers="idlePlayers" />
</div>`,
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
import Scores from './../components/Scores.vue'
import PuzzleStatus from './../components/PuzzleStatus.vue'
import SettingsOverlay from './../components/SettingsOverlay.vue'
import PreviewOverlay from './../components/PreviewOverlay.vue'
import ConnectionOverlay from './../components/ConnectionOverlay.vue'
import HelpOverlay from './../components/HelpOverlay.vue'
import { main, MODE_PLAY } from './../game'
export default defineComponent({
name: 'game',
components: {
PuzzleStatus,
Scores,
SettingsOverlay,
PreviewOverlay,
ConnectionOverlay,
HelpOverlay,
},
data() {
return {
activePlayers: [],
idlePlayers: [],
// TODO: ts Array<Player> type
activePlayers: [] as PropType<Array<any>>,
idlePlayers: [] as PropType<Array<any>>,
finished: false,
duration: 0,
piecesDone: 0,
piecesTotal: 0,
overlay: null,
overlay: '',
connectionState: 0,
@ -68,10 +72,10 @@ export default {
name: '',
},
previewImageUrl: '',
setHotkeys: () => {},
onBgChange: () => {},
onColorChange: () => {},
onNameChange: () => {},
setHotkeys: (v: boolean) => {},
onBgChange: (v: string) => {},
onColorChange: (v: string) => {},
onNameChange: (v: string) => {},
disconnect: () => {},
connect: () => {},
},
@ -81,29 +85,31 @@ export default {
if (!this.$route.params.id) {
return
}
this.$watch(() => this.g.player.background, (value) => {
this.$watch(() => this.g.player.background, (value: string) => {
this.g.onBgChange(value)
})
this.$watch(() => this.g.player.color, (value) => {
this.$watch(() => this.g.player.color, (value: string) => {
this.g.onColorChange(value)
})
this.$watch(() => this.g.player.name, (value) => {
this.$watch(() => this.g.player.name, (value: string) => {
this.g.onNameChange(value)
})
this.g = await main(
this.$route.params.id,
`${this.$route.params.id}`,
// @ts-ignore
this.$clientId,
// @ts-ignore
this.$config.WS_ADDRESS,
MODE_PLAY,
this.$el,
{
setActivePlayers: (v) => { this.activePlayers = v },
setIdlePlayers: (v) => { this.idlePlayers = v },
setFinished: (v) => { this.finished = v },
setDuration: (v) => { this.duration = v },
setPiecesDone: (v) => { this.piecesDone = v },
setPiecesTotal: (v) => { this.piecesTotal = v },
setConnectionState: (v) => { this.connectionState = v },
setActivePlayers: (v: Array<any>) => { this.activePlayers = v },
setIdlePlayers: (v: Array<any>) => { this.idlePlayers = v },
setFinished: (v: boolean) => { this.finished = v },
setDuration: (v: number) => { this.duration = v },
setPiecesDone: (v: number) => { this.piecesDone = v },
setPiecesTotal: (v: number) => { this.piecesTotal = v },
setConnectionState: (v: number) => { this.connectionState = v },
togglePreview: () => { this.toggle('preview', false) },
}
)
@ -112,22 +118,23 @@ export default {
this.g.disconnect()
},
methods: {
reconnect() {
reconnect(): void {
this.g.connect()
},
toggle(overlay, affectsHotkeys) {
if (this.overlay === null) {
toggle(overlay: string, affectsHotkeys: boolean): void {
if (this.overlay === '') {
this.overlay = overlay
if (affectsHotkeys) {
this.g.setHotkeys(false)
}
} else {
// could check if overlay was the provided one
this.overlay = null
this.overlay = ''
if (affectsHotkeys) {
this.g.setHotkeys(true)
}
}
},
},
}
})
</script>

View file

@ -0,0 +1,36 @@
<template>
<div>
<h1>Running games</h1>
<div class="game-teaser-wrap" v-for="(g, idx) in gamesRunning" :key="idx">
<game-teaser :game="g" />
</div>
<h1>Finished games</h1>
<div class="game-teaser-wrap" v-for="(g, idx) in gamesFinished" :key="idx">
<game-teaser :game="g" />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import GameTeaser from './../components/GameTeaser.vue'
export default defineComponent({
components: {
GameTeaser,
},
data() {
return {
gamesRunning: [],
gamesFinished: [],
}
},
async created() {
const res = await fetch('/api/index-data')
const json = await res.json()
this.gamesRunning = json.gamesRunning
this.gamesFinished = json.gamesFinished
},
})
</script>

View file

@ -1,16 +1,19 @@
"use strict"
<template>
<div>
<new-game-dialog :images="images" @newGame="onNewGame" />
</div>
</template>
import NewGameDialog from './../components/NewGameDialog.vue.js'
<script lang="ts">
import { defineComponent } from 'vue'
export default {
// TODO: maybe move dialog back, now that this is a view on its own
import NewGameDialog from './../components/NewGameDialog.vue'
export default defineComponent({
components: {
NewGameDialog,
},
// TODO: maybe move dialog back, now that this is a view on its own
template: `
<div>
<new-game-dialog :images="images" @newGame="onNewGame" />
</div>`,
data() {
return {
images: [],
@ -22,7 +25,8 @@ export default {
this.images = json.images
},
methods: {
async onNewGame(gameSettings) {
// TODO: ts GameSettings type
async onNewGame(gameSettings: any) {
const res = await fetch('/newgame', {
method: 'post',
headers: {
@ -37,4 +41,5 @@ export default {
}
}
}
}
})
</script>

View file

@ -1,23 +1,5 @@
"use strict"
import Scores from './../components/Scores.vue.js'
import PuzzleStatus from './../components/PuzzleStatus.vue.js'
import SettingsOverlay from './../components/SettingsOverlay.vue.js'
import PreviewOverlay from './../components/PreviewOverlay.vue.js'
import HelpOverlay from './../components/HelpOverlay.vue.js'
import { main, MODE_REPLAY } from './../game.js'
export default {
name: 'replay',
components: {
PuzzleStatus,
Scores,
SettingsOverlay,
PreviewOverlay,
HelpOverlay,
},
template: `<div id="replay">
<template>
<div id="replay">
<settings-overlay v-show="overlay === 'settings'" @bgclick="toggle('settings', true)" v-model="g.player" />
<preview-overlay v-show="overlay === 'preview'" @bgclick="toggle('preview', false)" :img="g.previewImageUrl" />
<help-overlay v-show="overlay === 'help'" @bgclick="toggle('help', true)" />
@ -46,18 +28,41 @@ export default {
</div>
<scores :activePlayers="activePlayers" :idlePlayers="idlePlayers" />
</div>`,
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import Scores from './../components/Scores.vue'
import PuzzleStatus from './../components/PuzzleStatus.vue'
import SettingsOverlay from './../components/SettingsOverlay.vue'
import PreviewOverlay from './../components/PreviewOverlay.vue'
import HelpOverlay from './../components/HelpOverlay.vue'
import { main, MODE_REPLAY } from './../game'
export default defineComponent({
name: 'replay',
components: {
PuzzleStatus,
Scores,
SettingsOverlay,
PreviewOverlay,
HelpOverlay,
},
data() {
return {
activePlayers: [],
idlePlayers: [],
activePlayers: [] as Array<any>,
idlePlayers: [] as Array<any>,
finished: false,
duration: 0,
piecesDone: 0,
piecesTotal: 0,
overlay: null,
overlay: '',
connectionState: 0,
g: {
player: {
@ -66,10 +71,10 @@ export default {
name: '',
},
previewImageUrl: '',
setHotkeys: () => {},
onBgChange: () => {},
onColorChange: () => {},
onNameChange: () => {},
setHotkeys: (v: boolean) => {},
onBgChange: (v: string) => {},
onColorChange: (v: string) => {},
onNameChange: (v: string) => {},
replayOnSpeedUp: () => {},
replayOnSpeedDown: () => {},
replayOnPauseToggle: () => {},
@ -86,59 +91,62 @@ export default {
if (!this.$route.params.id) {
return
}
this.$watch(() => this.g.player.background, (value) => {
this.$watch(() => this.g.player.background, (value: string) => {
this.g.onBgChange(value)
})
this.$watch(() => this.g.player.color, (value) => {
this.$watch(() => this.g.player.color, (value: string) => {
this.g.onColorChange(value)
})
this.$watch(() => this.g.player.name, (value) => {
this.$watch(() => this.g.player.name, (value: string) => {
this.g.onNameChange(value)
})
this.g = await main(
this.$route.params.id,
`${this.$route.params.id}`,
// @ts-ignore
this.$clientId,
// @ts-ignore
this.$config.WS_ADDRESS,
MODE_REPLAY,
this.$el,
{
setActivePlayers: (v) => { this.activePlayers = v },
setIdlePlayers: (v) => { this.idlePlayers = v },
setFinished: (v) => { this.finished = v },
setDuration: (v) => { this.duration = v },
setPiecesDone: (v) => { this.piecesDone = v },
setPiecesTotal: (v) => { this.piecesTotal = v },
setActivePlayers: (v: Array<any>) => { this.activePlayers = v },
setIdlePlayers: (v: Array<any>) => { this.idlePlayers = v },
setFinished: (v: boolean) => { this.finished = v },
setDuration: (v: number) => { this.duration = v },
setPiecesDone: (v: number) => { this.piecesDone = v },
setPiecesTotal: (v: number) => { this.piecesTotal = v },
togglePreview: () => { this.toggle('preview', false) },
setConnectionState: (v) => { this.connectionState = v },
setReplaySpeed: (v) => { this.replay.speed = v },
setReplayPaused: (v) => { this.replay.paused = v },
setConnectionState: (v: number) => { this.connectionState = v },
setReplaySpeed: (v: number) => { this.replay.speed = v },
setReplayPaused: (v: boolean) => { this.replay.paused = v },
}
)
},
unmounted () {
this.g.disconnect()
},
computed: {
replayText () {
return 'Replay-Speed: ' +
(this.replay.speed + 'x') +
(this.replay.paused ? ' Paused' : '')
},
},
methods: {
toggle(overlay, affectsHotkeys) {
if (this.overlay === null) {
toggle(overlay: string, affectsHotkeys: boolean): void {
if (this.overlay === '') {
this.overlay = overlay
if (affectsHotkeys) {
this.g.setHotkeys(false)
}
} else {
// could check if overlay was the provided one
this.overlay = null
this.overlay = ''
if (affectsHotkeys) {
this.g.setHotkeys(true)
}
}
},
},
}
computed: {
replayText (): string {
return 'Replay-Speed: ' +
(this.replay.speed + 'x') +
(this.replay.paused ? ' Paused' : '')
},
},
})
</script>

View file

@ -4,10 +4,9 @@ import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const BASE_DIR = `${__dirname}/..`
const BASE_DIR = `${__dirname}/../..`
export const DATA_DIR = `${BASE_DIR}/data`
export const UPLOAD_DIR = `${BASE_DIR}/data/uploads`
export const UPLOAD_URL = `/uploads`
export const COMMON_DIR = `${BASE_DIR}/common/`
export const PUBLIC_DIR = `${BASE_DIR}/public/`
export const PUBLIC_DIR = `${BASE_DIR}/build/public/`

View file

@ -1,12 +1,12 @@
import GameCommon from './../common/GameCommon.js'
import Util from './../common/Util.js'
import { Rng } from '../common/Rng.js'
import GameLog from './GameLog.js'
import { createPuzzle } from './Puzzle.js'
import Protocol from '../common/Protocol.js'
import GameStorage from './GameStorage.js'
import GameCommon from './../common/GameCommon'
import Util from './../common/Util'
import { Rng } from '../common/Rng'
import GameLog from './GameLog'
import { createPuzzle } from './Puzzle'
import Protocol from '../common/Protocol'
import GameStorage from './GameStorage'
async function createGameObject(gameId, targetTiles, image, ts, scoreMode) {
async function createGameObject(gameId: string, targetTiles: number, image: { file: string, url: string }, ts: number, scoreMode: number) {
const seed = Util.hash(gameId + ' ' + ts)
const rng = new Rng(seed)
return {
@ -19,7 +19,7 @@ async function createGameObject(gameId, targetTiles, image, ts, scoreMode) {
}
}
async function createGame(gameId, targetTiles, image, ts, scoreMode) {
async function createGame(gameId: string, targetTiles: number, image: { file: string, url: string }, ts: number, scoreMode: number) {
const gameObject = await createGameObject(gameId, targetTiles, image, ts, scoreMode)
GameLog.create(gameId)
@ -29,7 +29,7 @@ async function createGame(gameId, targetTiles, image, ts, scoreMode) {
GameStorage.setDirty(gameId)
}
function addPlayer(gameId, playerId, ts) {
function addPlayer(gameId: string, playerId: string, ts: number) {
const idx = GameCommon.getPlayerIndexById(gameId, playerId)
const diff = ts - GameCommon.getStartTs(gameId)
if (idx === -1) {
@ -42,7 +42,7 @@ function addPlayer(gameId, playerId, ts) {
GameStorage.setDirty(gameId)
}
function handleInput(gameId, playerId, input, ts) {
function handleInput(gameId: string, playerId: string, input: any, ts: number) {
const idx = GameCommon.getPlayerIndexById(gameId, playerId)
const diff = ts - GameCommon.getStartTs(gameId)
GameLog.log(gameId, Protocol.LOG_HANDLE_INPUT, idx, input, diff)

View file

@ -4,21 +4,21 @@ import { DATA_DIR } from '../server/Dirs.js'
const log = logger('GameLog.js')
const filename = (gameId) => `${DATA_DIR}/log_${gameId}.log`
const filename = (gameId: string) => `${DATA_DIR}/log_${gameId}.log`
const create = (gameId) => {
const create = (gameId: string) => {
const file = filename(gameId)
if (!fs.existsSync(file)) {
fs.appendFileSync(file, '')
}
}
const exists = (gameId) => {
const exists = (gameId: string) => {
const file = filename(gameId)
return fs.existsSync(file)
}
const _log = (gameId, ...args) => {
const _log = (gameId: string, ...args: Array<any>) => {
const file = filename(gameId)
if (!fs.existsSync(file)) {
return
@ -27,13 +27,13 @@ const _log = (gameId, ...args) => {
fs.appendFileSync(file, str + "\n")
}
const get = (gameId) => {
const get = (gameId: string) => {
const file = filename(gameId)
if (!fs.existsSync(file)) {
return []
}
const lines = fs.readFileSync(file, 'utf-8').split("\n")
return lines.filter(line => !!line).map((line) => {
return lines.filter((line: string) => !!line).map((line: string) => {
try {
return JSON.parse(line)
} catch (e) {

View file

@ -1,27 +1,28 @@
import { logger } from '../common/Util.js'
import WebSocket from 'ws'
const log = logger('GameSocket.js')
// Map<gameId, Socket[]>
const SOCKETS = {}
const SOCKETS = {} as Record<string, Array<WebSocket>>
function socketExists(gameId, socket) {
function socketExists(gameId: string, socket: WebSocket) {
if (!(gameId in SOCKETS)) {
return false
}
return SOCKETS[gameId].includes(socket)
}
function removeSocket(gameId, socket) {
function removeSocket(gameId: string, socket: WebSocket) {
if (!(gameId in SOCKETS)) {
return
}
SOCKETS[gameId] = SOCKETS[gameId].filter(s => s !== socket)
SOCKETS[gameId] = SOCKETS[gameId].filter((s: WebSocket) => s !== socket)
log.log('removed socket: ', gameId, socket.protocol)
log.log('socket count: ', Object.keys(SOCKETS[gameId]).length)
}
function addSocket(gameId, socket) {
function addSocket(gameId: string, socket: WebSocket) {
if (!(gameId in SOCKETS)) {
SOCKETS[gameId] = []
}
@ -32,7 +33,7 @@ function addSocket(gameId, socket) {
}
}
function getSockets(gameId) {
function getSockets(gameId: string) {
if (!(gameId in SOCKETS)) {
return []
}

View file

@ -1,21 +1,21 @@
import fs from 'fs'
import GameCommon from './../common/GameCommon.js'
import Util, { logger } from './../common/Util.js'
import { Rng } from '../common/Rng.js'
import { DATA_DIR } from './Dirs.js'
import Time from '../common/Time.js'
import GameCommon from './../common/GameCommon'
import Util, { logger } from './../common/Util'
import { Rng } from '../common/Rng'
import { DATA_DIR } from './Dirs'
import Time from './../common/Time'
const log = logger('GameStorage.js')
const DIRTY_GAMES = {}
function setDirty(gameId) {
const DIRTY_GAMES = {} as any
function setDirty(gameId: string): void {
DIRTY_GAMES[gameId] = true
}
function setClean(gameId) {
function setClean(gameId: string): void {
delete DIRTY_GAMES[gameId]
}
function loadGames() {
function loadGames(): void {
const files = fs.readdirSync(DATA_DIR)
for (const f of files) {
const m = f.match(/^([a-z0-9]+)\.json$/)
@ -27,7 +27,7 @@ function loadGames() {
}
}
function loadGame(gameId) {
function loadGame(gameId: string): void {
const file = `${DATA_DIR}/${gameId}.json`
const contents = fs.readFileSync(file, 'utf-8')
let game
@ -40,7 +40,7 @@ function loadGame(gameId) {
game.puzzle.data.started = Math.round(fs.statSync(file).ctimeMs)
}
if (typeof game.puzzle.data.finished === 'undefined') {
let unfinished = game.puzzle.tiles.map(Util.decodeTile).find(t => t.owner !== -1)
let unfinished = game.puzzle.tiles.map(Util.decodeTile).find((t: any) => t.owner !== -1)
game.puzzle.data.finished = unfinished ? 0 : Time.timestamp()
}
if (!Array.isArray(game.players)) {
@ -50,7 +50,7 @@ function loadGame(gameId) {
id: game.id,
rng: {
type: game.rng ? game.rng.type : '_fake_',
obj: game.rng ? Rng.unserialize(game.rng.obj) : new Rng(),
obj: game.rng ? Rng.unserialize(game.rng.obj) : new Rng(0),
},
puzzle: game.puzzle,
players: game.players,
@ -66,7 +66,7 @@ function persistGames() {
}
}
function persistGame(gameId) {
function persistGame(gameId: string) {
const game = GameCommon.get(gameId)
if (game.id in DIRTY_GAMES) {
setClean(game.id)

View file

@ -2,9 +2,10 @@ import sizeOf from 'image-size'
import fs from 'fs'
import exif from 'exif'
import sharp from 'sharp'
import {UPLOAD_DIR, UPLOAD_URL} from './Dirs.js'
const resizeImage = async (filename) => {
const resizeImage = async (filename: string) => {
if (!filename.toLowerCase().match(/\.(jpe?g|webp|png)$/)) {
return
}
@ -33,7 +34,7 @@ const resizeImage = async (filename) => {
}
}
async function getExifOrientation(imagePath) {
async function getExifOrientation(imagePath: string) {
return new Promise((resolve, reject) => {
new exif.ExifImage({ image: imagePath }, function (error, exifData) {
if (error) {
@ -60,7 +61,7 @@ const allImages = () => {
return images
}
async function getDimensions(imagePath) {
async function getDimensions(imagePath: string) {
let dimensions = sizeOf(imagePath)
const orientation = await getExifOrientation(imagePath)
// when image is rotated to the left or right, switch width/height

View file

@ -1,22 +1,36 @@
import Util from '../common/Util.js'
import { Rng } from '../common/Rng.js'
import Util from '../common/Util'
import { Rng } from '../common/Rng'
import Images from './Images.js'
interface PuzzleInfo {
width: number
height: number
tileSize: number
tileMarginWidth: number
tileDrawSize: number
tiles: number
tilesX: number
tilesY: number
}
// cut size of each puzzle tile in the
// final resized version of the puzzle image
const TILE_SIZE = 64
async function createPuzzle(
/** @type Rng */ rng,
targetTiles,
image,
ts
rng: Rng,
targetTiles: number,
image: { file: string, url: string },
ts: number
) {
const imagePath = image.file
const imageUrl = image.url
// determine puzzle information from the image dimensions
const dim = await Images.getDimensions(imagePath)
if (!dim || !dim.width || !dim.height) {
throw `[ 2021-05-16 invalid dimension for path ${imagePath} ]`
}
const info = determinePuzzleInfo(dim.width, dim.height, targetTiles)
let tiles = new Array(info.tiles)
@ -143,8 +157,8 @@ async function createPuzzle(
}
function determinePuzzleTileShapes(
/** @type Rng */ rng,
info
rng: Rng,
info: PuzzleInfo
) {
const tabs = [-1, 1]
@ -161,7 +175,7 @@ function determinePuzzleTileShapes(
return shapes.map(Util.encodeShape)
}
const determineTilesXY = (w, h, targetTiles) => {
const determineTilesXY = (w: number, h: number, targetTiles: number) => {
const w_ = w < h ? (w * h) : (w * w)
const h_ = w < h ? (h * h) : (w * h)
let size = 0
@ -177,7 +191,7 @@ const determineTilesXY = (w, h, targetTiles) => {
}
}
const determinePuzzleInfo = (w, h, targetTiles) => {
const determinePuzzleInfo = (w: number, h: number, targetTiles: number) => {
const {tilesX, tilesY} = determineTilesXY(w, h, targetTiles)
const tiles = tilesX * tilesY
const tileSize = TILE_SIZE

View file

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

View file

@ -1,27 +1,41 @@
import WebSocketServer from './WebSocketServer.js'
import WebSocketServer from './WebSocketServer'
import WebSocket from 'ws'
import express from 'express'
import multer from 'multer'
import config from './../config.js'
import Protocol from './../common/Protocol.js'
import Util, { logger } from './../common/Util.js'
import Game from './Game.js'
import Protocol from './../common/Protocol'
import Util, { logger } from './../common/Util'
import Game from './Game'
import bodyParser from 'body-parser'
import v8 from 'v8'
import GameLog from './GameLog.js'
import GameSockets from './GameSockets.js'
import Time from '../common/Time.js'
import Images from './Images.js'
import fs from 'fs'
import GameLog from './GameLog'
import GameSockets from './GameSockets'
import Time from '../common/Time'
import Images from './Images'
import {
UPLOAD_DIR,
UPLOAD_URL,
COMMON_DIR,
PUBLIC_DIR,
} from './Dirs.js'
import GameCommon from '../common/GameCommon.js'
import GameStorage from './GameStorage.js'
} from './Dirs'
import GameCommon from '../common/GameCommon'
import GameStorage from './GameStorage'
const log = logger('index.js')
let configFile = ''
let last = ''
for (const val of process.argv) {
if (last === '-c') {
configFile = val
}
last = val
}
if (configFile === '') {
process.exit(2)
}
const config = JSON.parse(String(fs.readFileSync(configFile)))
const log = logger('main.js')
const port = config.http.port
const hostname = config.http.hostname
@ -50,7 +64,7 @@ app.get('/api/newgame-data', (req, res) => {
app.get('/api/index-data', (req, res) => {
const ts = Time.timestamp()
const games = [
...Game.getAllGames().map(game => ({
...Game.getAllGames().map((game: any) => ({
id: game.id,
hasReplay: GameLog.exists(game.id),
started: Game.getStartTs(game.id),
@ -69,7 +83,7 @@ app.get('/api/index-data', (req, res) => {
})
app.post('/upload', (req, res) => {
upload(req, res, async (err) => {
upload(req, res, async (err: any) => {
if (err) {
log.log(err)
res.status(400).send("Something went wrong!");
@ -107,20 +121,19 @@ app.post('/newgame', bodyParser.json(), async (req, res) => {
res.send({ id: gameId })
})
app.use('/common/', express.static(COMMON_DIR))
app.use('/uploads/', express.static(UPLOAD_DIR))
app.use('/', express.static(PUBLIC_DIR))
const wss = new WebSocketServer(config.ws);
const notify = (data, sockets) => {
const notify = (data: any, sockets: Array<WebSocket>) => {
// TODO: throttle?
for (let socket of sockets) {
wss.notifyOne(data, socket)
}
}
wss.on('close', async ({socket}) => {
wss.on('close', async ({socket} : {socket: WebSocket}) => {
try {
const proto = socket.protocol.split('|')
const clientId = proto[0]
@ -131,7 +144,7 @@ wss.on('close', async ({socket}) => {
}
})
wss.on('message', async ({socket, data}) => {
wss.on('message', async ({socket, data} : { socket: WebSocket, data: any }) => {
try {
const proto = socket.protocol.split('|')
const clientId = proto[0]
@ -236,7 +249,7 @@ const persistInterval = setInterval(() => {
memoryUsageHuman()
}, config.persistence.interval)
const gracefulShutdown = (signal) => {
const gracefulShutdown = (signal: any) => {
log.log(`${signal} received...`)
log.log('clearing persist interval...')

14
tsconfig.server.json Normal file
View file

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "es2020",
"module": "es2020",
"allowSyntheticDefaultImports": true,
"strict": true,
"moduleResolution": "node"
},
"include": [
"src/**/*.ts", "src/**/*.d.ts"
]
}

11
vite.config.js Normal file
View file

@ -0,0 +1,11 @@
import vite from 'vite'
import vue from '@vitejs/plugin-vue'
export default vite.defineConfig({
plugins: [ vue() ],
root: './src/frontend',
build: {
outDir: '../../build/public',
emptyOutDir: true,
},
})