add replay functionality

This commit is contained in:
Zutatensuppe 2020-12-22 22:35:09 +01:00
parent 4158aa0854
commit 083fc0463c
13 changed files with 452 additions and 125 deletions

View file

@ -7,7 +7,7 @@ function exists(gameId) {
return (!!GAMES[gameId]) || false return (!!GAMES[gameId]) || false
} }
function createGame(id, rng, puzzle, players, sockets, evtInfos) { function __createGameObject(id, rng, puzzle, players, sockets, evtInfos) {
return { return {
id: id, id: id,
rng: rng, rng: rng,
@ -18,7 +18,7 @@ function createGame(id, rng, puzzle, players, sockets, evtInfos) {
} }
} }
function createPlayer(id, ts) { function __createPlayerObject(id, ts) {
return { return {
id: id, id: id,
x: 0, x: 0,
@ -33,7 +33,7 @@ function createPlayer(id, ts) {
} }
function newGame({id, rng, puzzle, players, sockets, evtInfos}) { function newGame({id, rng, puzzle, players, sockets, evtInfos}) {
const game = createGame(id, rng, puzzle, players, sockets, evtInfos) const game = __createGameObject(id, rng, puzzle, players, sockets, evtInfos)
setGame(id, game) setGame(id, game)
return game return game
} }
@ -62,26 +62,23 @@ function playerExists(gameId, playerId) {
return !!GAMES[gameId].players[playerId] return !!GAMES[gameId].players[playerId]
} }
function getRelevantPlayers(gameId) { function getRelevantPlayers(gameId, ts) {
const ts = Util.timestamp()
const minTs = ts - 30000 const minTs = ts - 30000
return getAllPlayers(gameId).filter(player => { return getAllPlayers(gameId).filter(player => {
return player.ts >= minTs || player.points > 0 return player.ts >= minTs || player.points > 0
}) })
} }
function getActivePlayers(gameId) { function getActivePlayers(gameId, ts) {
const ts = Util.timestamp()
const minTs = ts - 30000 const minTs = ts - 30000
return getAllPlayers(gameId).filter(player => { return getAllPlayers(gameId).filter(player => {
return player.ts >= minTs return player.ts >= minTs
}) })
} }
function addPlayer(gameId, playerId) { function addPlayer(gameId, playerId, ts) {
const ts = Util.timestamp()
if (!GAMES[gameId].players[playerId]) { if (!GAMES[gameId].players[playerId]) {
setPlayer(gameId, playerId, createPlayer(playerId, ts)) setPlayer(gameId, playerId, __createPlayerObject(playerId, ts))
} else { } else {
changePlayer(gameId, playerId, { ts }) changePlayer(gameId, playerId, { ts })
} }
@ -368,19 +365,23 @@ const freeTileIdxByPos = (gameId, pos) => {
} }
const getPlayerBgColor = (gameId, playerId) => { const getPlayerBgColor = (gameId, playerId) => {
return getPlayer(gameId, playerId).bgcolor const p = getPlayer(gameId, playerId)
return p ? p.bgcolor : null
} }
const getPlayerColor = (gameId, playerId) => { const getPlayerColor = (gameId, playerId) => {
return getPlayer(gameId, playerId).color const p = getPlayer(gameId, playerId)
return p ? p.color : null
} }
const getPlayerName = (gameId, playerId) => { const getPlayerName = (gameId, playerId) => {
return getPlayer(gameId, playerId).name const p = getPlayer(gameId, playerId)
return p ? p.name : null
} }
const getPlayerPoints = (gameId, playerId) => { const getPlayerPoints = (gameId, playerId) => {
return getPlayer(gameId, playerId).points const p = getPlayer(gameId, playerId)
return p ? p.points : null
} }
// determine if two tiles are grouped together // determine if two tiles are grouped together
@ -398,6 +399,14 @@ const getTableHeight = (gameId) => {
return GAMES[gameId].puzzle.info.table.height return GAMES[gameId].puzzle.info.table.height
} }
const getPuzzle = (gameId) => {
return GAMES[gameId].puzzle
}
const getRng = (gameId) => {
return GAMES[gameId].rng.obj
}
const getPuzzleWidth = (gameId) => { const getPuzzleWidth = (gameId) => {
return GAMES[gameId].puzzle.info.width return GAMES[gameId].puzzle.info.width
} }
@ -406,7 +415,7 @@ const getPuzzleHeight = (gameId) => {
return GAMES[gameId].puzzle.info.height return GAMES[gameId].puzzle.info.height
} }
function handleInput(gameId, playerId, input) { function handleInput(gameId, playerId, input, ts) {
const puzzle = GAMES[gameId].puzzle const puzzle = GAMES[gameId].puzzle
let evtInfo = GAMES[gameId].evtInfos[playerId] let evtInfo = GAMES[gameId].evtInfos[playerId]
@ -472,8 +481,6 @@ function handleInput(gameId, playerId, input) {
} }
} }
const ts = Util.timestamp()
const type = input[0] const type = input[0]
if (type === 'bg_color') { if (type === 'bg_color') {
const bgcolor = input[1] const bgcolor = input[1]
@ -559,7 +566,7 @@ function handleInput(gameId, playerId, input) {
_tileChanges(tileIdxs) _tileChanges(tileIdxs)
// check if the puzzle is finished // check if the puzzle is finished
if (getFinishedTileCount(gameId) === getTileCount(gameId)) { if (getFinishedTileCount(gameId) === getTileCount(gameId)) {
changeData(gameId, { finished: Util.timestamp() }) changeData(gameId, { finished: ts })
_dataChange() _dataChange()
} }
} else { } else {
@ -614,6 +621,8 @@ function handleInput(gameId, playerId, input) {
} }
export default { export default {
__createGameObject,
__createPlayerObject,
newGame, newGame,
exists, exists,
playerExists, playerExists,
@ -638,6 +647,8 @@ export default {
setPuzzleData, setPuzzleData,
getTableWidth, getTableWidth,
getTableHeight, getTableHeight,
getPuzzle,
getRng,
getPuzzleWidth, getPuzzleWidth,
getPuzzleHeight, getPuzzleHeight,
getTilesSortedByZIndex, getTilesSortedByZIndex,

View file

@ -40,12 +40,16 @@ EV_SERVER_INIT: event sent to one client after that client
*/ */
const EV_SERVER_EVENT = 1 const EV_SERVER_EVENT = 1
const EV_SERVER_INIT = 4 const EV_SERVER_INIT = 4
const EV_SERVER_INIT_REPLAY = 5
const EV_CLIENT_EVENT = 2 const EV_CLIENT_EVENT = 2
const EV_CLIENT_INIT = 3 const EV_CLIENT_INIT = 3
const EV_CLIENT_INIT_REPLAY = 6
export default { export default {
EV_SERVER_EVENT, EV_SERVER_EVENT,
EV_SERVER_INIT, EV_SERVER_INIT,
EV_SERVER_INIT_REPLAY,
EV_CLIENT_EVENT, EV_CLIENT_EVENT,
EV_CLIENT_INIT, EV_CLIENT_INIT,
EV_CLIENT_INIT_REPLAY,
} }

View file

@ -152,7 +152,19 @@ function coordByTileIdx(info, tileIdx) {
} }
} }
const hash = (str) => {
let hash = 0
for (let i = 0; i < str.length; i++) {
let chr = str.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
}
export default { export default {
hash,
uniqId, uniqId,
randomInt, randomInt,
choice, choice,

View file

@ -42,6 +42,25 @@ function connect(gameId, clientId) {
}) })
} }
function connectReplay(gameId, clientId) {
clientSeq = 0
events = {}
conn = new WsClient(WS_ADDRESS, clientId + '|' + gameId)
return new Promise(r => {
conn.connect()
send([Protocol.EV_CLIENT_INIT_REPLAY])
conn.onSocket('message', async ({ data }) => {
const msg = JSON.parse(data)
const msgType = msg[0]
if (msgType === Protocol.EV_SERVER_INIT_REPLAY) {
const game = msg[1]
const log = msg[2]
r({game, log})
}
})
})
}
function sendClientEvent(mouse) { function sendClientEvent(mouse) {
// when sending event, increase number of sent events // when sending event, increase number of sent events
// and add the event locally // and add the event locally
@ -52,6 +71,7 @@ function sendClientEvent(mouse) {
export default { export default {
connect, connect,
connectReplay,
onServerChange, onServerChange,
sendClientEvent, sendClientEvent,
} }

View file

@ -4,6 +4,7 @@ export default {
newGame: GameCommon.newGame, newGame: GameCommon.newGame,
getRelevantPlayers: GameCommon.getRelevantPlayers, getRelevantPlayers: GameCommon.getRelevantPlayers,
getActivePlayers: GameCommon.getActivePlayers, getActivePlayers: GameCommon.getActivePlayers,
addPlayer: GameCommon.addPlayer,
handleInput: GameCommon.handleInput, handleInput: GameCommon.handleInput,
getPlayerBgColor: GameCommon.getPlayerBgColor, getPlayerBgColor: GameCommon.getPlayerBgColor,
getPlayerColor: GameCommon.getPlayerColor, getPlayerColor: GameCommon.getPlayerColor,
@ -15,6 +16,8 @@ export default {
setPuzzleData: GameCommon.setPuzzleData, setPuzzleData: GameCommon.setPuzzleData,
getTableWidth: GameCommon.getTableWidth, getTableWidth: GameCommon.getTableWidth,
getTableHeight: GameCommon.getTableHeight, getTableHeight: GameCommon.getTableHeight,
getPuzzle: GameCommon.getPuzzle,
getRng: GameCommon.getRng,
getPuzzleWidth: GameCommon.getPuzzleWidth, getPuzzleWidth: GameCommon.getPuzzleWidth,
getPuzzleHeight: GameCommon.getPuzzleHeight, getPuzzleHeight: GameCommon.getPuzzleHeight,
getTilesSortedByZIndex: GameCommon.getTilesSortedByZIndex, getTilesSortedByZIndex: GameCommon.getTilesSortedByZIndex,

View file

@ -12,11 +12,14 @@ import { Rng } from '../common/Rng.js'
if (typeof GAME_ID === 'undefined') throw '[ GAME_ID not set ]' if (typeof GAME_ID === 'undefined') throw '[ GAME_ID not set ]'
if (typeof WS_ADDRESS === 'undefined') throw '[ WS_ADDRESS not set ]' if (typeof WS_ADDRESS === 'undefined') throw '[ WS_ADDRESS not set ]'
if (typeof MODE === 'undefined') throw '[ MODE not set ]'
if (typeof DEBUG === 'undefined') window.DEBUG = false if (typeof DEBUG === 'undefined') window.DEBUG = false
let RERENDER = true let RERENDER = true
let TIME = () => Util.timestamp()
function addCanvasToDom(canvas) { function addCanvasToDom(canvas) {
canvas.width = window.innerWidth canvas.width = window.innerWidth
canvas.height = window.innerHeight canvas.height = window.innerHeight
@ -41,6 +44,13 @@ function addMenuToDom(gameId) {
return row return row
} }
function btn(txt) {
const btn = document.createElement('button')
btn.classList.add('btn')
btn.innerText = txt
return btn
}
function colorinput() { function colorinput() {
const input = document.createElement('input') const input = document.createElement('input')
input.type = 'color' input.type = 'color'
@ -143,10 +153,10 @@ function addMenuToDom(gameId) {
const scoresListEl = document.createElement('table') const scoresListEl = document.createElement('table')
const updateScores = () => { const updateScores = () => {
const ts = Util.timestamp() const ts = TIME()
const minTs = ts - 30000 const minTs = ts - 30000
const players = Game.getRelevantPlayers(gameId) const players = Game.getRelevantPlayers(gameId, ts)
const actives = players.filter(player => player.ts >= minTs) const actives = players.filter(player => player.ts >= minTs)
const nonActives = players.filter(player => player.ts < minTs) const nonActives = players.filter(player => player.ts < minTs)
@ -181,7 +191,7 @@ function addMenuToDom(gameId) {
const icon = ended ? '🏁' : '⏳' const icon = ended ? '🏁' : '⏳'
const from = started; const from = started;
const to = ended || Util.timestamp() const to = ended || TIME()
const MS = 1 const MS = 1
const SEC = MS * 1000 const SEC = MS * 1000
@ -208,12 +218,27 @@ function addMenuToDom(gameId) {
timerCountdownEl.innerText = timerStr() timerCountdownEl.innerText = timerStr()
setInterval(() => { setInterval(() => {
timerCountdownEl.innerText = timerStr() timerCountdownEl.innerText = timerStr()
}, 1000) }, 50) // needs to be small, so that it updates quick enough in replay
const timerEl = document.createElement('div') const timerEl = document.createElement('div')
timerEl.classList.add('timer') timerEl.classList.add('timer')
timerEl.appendChild(timerCountdownEl) timerEl.appendChild(timerCountdownEl)
let replayControl = null
if (MODE === 'replay') {
const replayControlEl = document.createElement('div')
const speedUp = btn('⏫')
const speedDown = btn('⏬')
const pause = btn('⏸️')
const speed = document.createElement('div')
replayControlEl.appendChild(speed)
replayControlEl.appendChild(speedUp)
replayControlEl.appendChild(speedDown)
replayControlEl.appendChild(pause)
timerEl.appendChild(replayControlEl)
replayControl = { speedUp, speedDown, pause, speed }
}
const scoresEl = document.createElement('div') const scoresEl = document.createElement('div')
scoresEl.classList.add('scores') scoresEl.classList.add('scores')
scoresEl.appendChild(scoresTitleEl) scoresEl.appendChild(scoresTitleEl)
@ -230,6 +255,7 @@ function addMenuToDom(gameId) {
playerColorPickerEl, playerColorPickerEl,
nameChangeEl, nameChangeEl,
updateScores, updateScores,
replayControl,
} }
} }
@ -324,23 +350,46 @@ async function main() {
return cursors[key] return cursors[key]
} }
// Create a canvas and attach adapters to it so we can work with it
const canvas = addCanvasToDom(Graphics.createCanvas())
// stuff only available in replay mode...
// TODO: refactor
let GAME_LOG
let GAME_LOG_IDX = 0
let REPLAY_SPEEDS = [0.5, 1, 2, 5, 10, 20, 50]
let REPLAY_SPEED_IDX = 1
let REPLAY_PAUSED = false
let lastRealTime = null
let lastGameTime = null
if (MODE === 'play') {
const game = await Communication.connect(gameId, CLIENT_ID) const game = await Communication.connect(gameId, CLIENT_ID)
game.rng.obj = Rng.unserialize(game.rng.obj) game.rng.obj = Rng.unserialize(game.rng.obj)
Game.newGame(game) Game.newGame(game)
} else if (MODE === 'replay') {
const {game, log} = await Communication.connectReplay(gameId, CLIENT_ID)
game.rng.obj = Rng.unserialize(game.rng.obj)
Game.newGame(game)
GAME_LOG = log
lastRealTime = Util.timestamp()
lastGameTime = GAME_LOG[0][GAME_LOG[0].length - 1]
TIME = () => lastGameTime
} else {
throw '[ 2020-12-22 MODE invalid, must be play|replay ]'
}
const bitmaps = await PuzzleGraphics.loadPuzzleBitmaps(game.puzzle) const bitmaps = await PuzzleGraphics.loadPuzzleBitmaps(Game.getPuzzle(gameId))
const {bgColorPickerEl, playerColorPickerEl, nameChangeEl, updateScores} = addMenuToDom(gameId) const {bgColorPickerEl, playerColorPickerEl, nameChangeEl, updateScores, replayControl} = addMenuToDom(gameId)
updateScores() updateScores()
// Create a dom and attach adapters to it so we can work with it
const canvas = addCanvasToDom(Graphics.createCanvas())
const longFinished = Game.getFinishTs(gameId) const longFinished = Game.getFinishTs(gameId)
let finished = longFinished ? true : false let finished = longFinished ? true : false
const justFinished = () => !!(finished && !longFinished) const justFinished = () => !!(finished && !longFinished)
const fireworks = new fireworksController(canvas, game.rng.obj) const fireworks = new fireworksController(canvas, Game.getRng(gameId))
fireworks.init(canvas) fireworks.init(canvas)
const ctx = canvas.getContext('2d') const ctx = canvas.getContext('2d')
@ -371,6 +420,7 @@ async function main() {
} }
const evts = new EventAdapter(canvas, viewport) const evts = new EventAdapter(canvas, viewport)
if (MODE === 'play') {
bgColorPickerEl.value = playerBgColor() bgColorPickerEl.value = playerBgColor()
evts.addEvent(['bg_color', bgColorPickerEl.value]) evts.addEvent(['bg_color', bgColorPickerEl.value])
bgColorPickerEl.addEventListener('change', () => { bgColorPickerEl.addEventListener('change', () => {
@ -389,7 +439,30 @@ async function main() {
localStorage.setItem('player_name', nameChangeEl.value) localStorage.setItem('player_name', nameChangeEl.value)
evts.addEvent(['player_name', nameChangeEl.value]) evts.addEvent(['player_name', nameChangeEl.value])
}) })
} else if (MODE === 'replay') {
let setSpeedStatus = () => {
replayControl.speed.innerText = 'Replay-Speed: ' + (REPLAY_SPEEDS[REPLAY_SPEED_IDX] + 'x') + (REPLAY_PAUSED ? ' Paused' : '')
}
setSpeedStatus()
replayControl.speedUp.addEventListener('click', () => {
if (REPLAY_SPEED_IDX + 1 < REPLAY_SPEEDS.length) {
REPLAY_SPEED_IDX++
setSpeedStatus()
}
})
replayControl.speedDown.addEventListener('click', () => {
if (REPLAY_SPEED_IDX >= 1) {
REPLAY_SPEED_IDX--
setSpeedStatus()
}
})
replayControl.pause.addEventListener('click', () => {
REPLAY_PAUSED = !REPLAY_PAUSED
setSpeedStatus()
})
}
if (MODE === 'play') {
Communication.onServerChange((msg) => { Communication.onServerChange((msg) => {
const msgType = msg[0] const msgType = msg[0]
const evClientId = msg[1] const evClientId = msg[1]
@ -417,11 +490,53 @@ async function main() {
} }
finished = Game.getFinishTs(gameId) finished = Game.getFinishTs(gameId)
}) })
} else if (MODE === 'replay') {
// no external communication for replay mode,
// only the GAME_LOG is relevant
let inter = setInterval(() => {
let realTime = Util.timestamp()
if (REPLAY_PAUSED) {
lastRealTime = realTime
return
}
let timePassedReal = realTime - lastRealTime
let timePassedGame = timePassedReal * REPLAY_SPEEDS[REPLAY_SPEED_IDX]
let maxGameTs = lastGameTime + timePassedGame
do {
if (REPLAY_PAUSED) {
break
}
let nextIdx = GAME_LOG_IDX + 1
if (nextIdx >= GAME_LOG.length) {
clearInterval(inter)
break
}
let logEntry = GAME_LOG[nextIdx]
let nextTs = logEntry[logEntry.length - 1]
if (nextTs > maxGameTs) {
break
}
if (logEntry[0] === 'addPlayer') {
Game.addPlayer(gameId, ...logEntry.slice(1))
RERENDER = true
} else if (logEntry[0] === 'handleInput') {
Game.handleInput(gameId, ...logEntry.slice(1))
RERENDER = true
}
GAME_LOG_IDX = nextIdx
} while (true)
lastRealTime = realTime
lastGameTime = maxGameTs
}, 50)
}
let _last_mouse_down = null let _last_mouse_down = null
const onUpdate = () => { const onUpdate = () => {
for (let evt of evts.consumeAll()) { for (let evt of evts.consumeAll()) {
if (MODE === 'play') {
// LOCAL ONLY CHANGES // LOCAL ONLY CHANGES
// ------------------------------------------------------------- // -------------------------------------------------------------
const type = evt[0] const type = evt[0]
@ -432,6 +547,7 @@ async function main() {
const mouse = viewport.worldToViewport(pos) const mouse = viewport.worldToViewport(pos)
const diffX = Math.round(mouse.x - _last_mouse_down.x) const diffX = Math.round(mouse.x - _last_mouse_down.x)
const diffY = Math.round(mouse.y - _last_mouse_down.y) const diffY = Math.round(mouse.y - _last_mouse_down.y)
RERENDER = true
viewport.move(diffX, diffY) viewport.move(diffX, diffY)
_last_mouse_down = mouse _last_mouse_down = mouse
@ -457,11 +573,41 @@ async function main() {
// LOCAL + SERVER CHANGES // LOCAL + SERVER CHANGES
// ------------------------------------------------------------- // -------------------------------------------------------------
const changes = Game.handleInput(GAME_ID, CLIENT_ID, evt) const ts = TIME()
const changes = Game.handleInput(GAME_ID, CLIENT_ID, evt, ts)
if (changes.length > 0) { if (changes.length > 0) {
RERENDER = true RERENDER = true
} }
Communication.sendClientEvent(evt) Communication.sendClientEvent(evt)
} else if (MODE === 'replay') {
// LOCAL ONLY CHANGES
// -------------------------------------------------------------
const type = evt[0]
if (type === 'move') {
if (_last_mouse_down) {
// move the cam
const pos = { x: evt[1], y: evt[2] }
const mouse = viewport.worldToViewport(pos)
const diffX = Math.round(mouse.x - _last_mouse_down.x)
const diffY = Math.round(mouse.y - _last_mouse_down.y)
RERENDER = true
viewport.move(diffX, diffY)
_last_mouse_down = mouse
}
} else if (type === 'down') {
const pos = { x: evt[1], y: evt[2] }
_last_mouse_down = viewport.worldToViewport(pos)
} else if (type === 'up') {
_last_mouse_down = null
} else if (type === 'zoomin') {
viewport.zoomIn()
RERENDER = true
} else if (type === 'zoomout') {
viewport.zoomOut()
RERENDER = true
}
}
} }
finished = Game.getFinishTs(gameId) finished = Game.getFinishTs(gameId)
@ -528,13 +674,15 @@ async function main() {
// DRAW PLAYERS // DRAW PLAYERS
// --------------------------------------------------------------- // ---------------------------------------------------------------
for (let player of Game.getActivePlayers(gameId)) { const ts = TIME()
for (let player of Game.getActivePlayers(gameId, ts)) {
const cursor = await getPlayerCursor(player) const cursor = await getPlayerCursor(player)
const pos = viewport.worldToViewport(player) const pos = viewport.worldToViewport(player)
ctx.drawImage(cursor, ctx.drawImage(cursor,
Math.round(pos.x - cursor.width/2), Math.round(pos.x - cursor.width/2),
Math.round(pos.y - cursor.height/2) Math.round(pos.y - cursor.height/2)
) )
if (MODE === 'play') {
if (player.id !== CLIENT_ID) { if (player.id !== CLIENT_ID) {
ctx.fillStyle = 'white' ctx.fillStyle = 'white'
ctx.font = '10px sans-serif' ctx.font = '10px sans-serif'
@ -544,6 +692,15 @@ async function main() {
Math.round(pos.y) + cursor.height Math.round(pos.y) + cursor.height
) )
} }
} else if (MODE === 'replay') {
ctx.fillStyle = 'white'
ctx.font = '10px sans-serif'
ctx.textAlign = 'center'
ctx.fillText(player.name + ' (' + player.points + ')',
Math.round(pos.x),
Math.round(pos.y) + cursor.height
)
}
} }
if (DEBUG) Debug.checkpoint('players done') if (DEBUG) Debug.checkpoint('players done')

View file

@ -22,6 +22,7 @@ a:hover { color: var(--link-hover-color); }
.scores { .scores {
position: absolute; position: absolute;
right: 0; right: 0;
top: 0;
background: var(--bg-color); background: var(--bg-color);
padding: 5px; padding: 5px;
@ -32,6 +33,7 @@ a:hover { color: var(--link-hover-color); }
.timer { .timer {
position: absolute; position: absolute;
left: 0; left: 0;
top: 0;
background: var(--bg-color); background: var(--bg-color);
padding: 5px; padding: 5px;
@ -41,6 +43,8 @@ a:hover { color: var(--link-hover-color); }
.menu { .menu {
position: absolute; position: absolute;
top: 0;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
background: var(--bg-color); background: var(--bg-color);
@ -192,3 +196,9 @@ input:focus {
background: var(--bg-color); background: var(--bg-color);
padding: 5px; padding: 5px;
} }
.game-replay {
position: absolute;
top: 0;
right: 0;
}

View file

@ -12,6 +12,7 @@
<body> <body>
<script>window.GAME_ID = '{{GAME_ID}}'</script> <script>window.GAME_ID = '{{GAME_ID}}'</script>
<script>window.WS_ADDRESS = '{{WS_ADDRESS}}'</script> <script>window.WS_ADDRESS = '{{WS_ADDRESS}}'</script>
<script>window.MODE = 'play'</script>
<script src="/game.js" type="module"></script> <script src="/game.js" type="module"></script>
</body> </body>
</html> </html>

View file

@ -0,0 +1,21 @@
<html>
<head>
<link rel="stylesheet" href="/style.css" />
<style type="text/css">
html,
body {
overflow: hidden;
}
canvas {
cursor: grab;
}
</style>
<title>🧩 jigsaw.hyottoko.club</title>
</head>
<body>
<script>window.GAME_ID = '{{GAME_ID}}'</script>
<script>window.WS_ADDRESS = '{{WS_ADDRESS}}'</script>
<script>window.MODE = 'replay'</script>
<script src="/game.js" type="module"></script>
</body>
</html>

View file

@ -1,8 +1,9 @@
import fs from 'fs' import fs from 'fs'
import { createPuzzle } from './Puzzle.js'
import GameCommon from './../common/GameCommon.js' import GameCommon from './../common/GameCommon.js'
import Util from './../common/Util.js' import Util from './../common/Util.js'
import { Rng } from '../common/Rng.js' import { Rng } from '../common/Rng.js'
import GameLog from './GameLog.js'
import { createPuzzle } from './Puzzle.js'
const DATA_DIR = './../data' const DATA_DIR = './../data'
@ -42,24 +43,44 @@ function loadAllGames() {
} }
const changedGames = {} const changedGames = {}
async function createGame(gameId, targetTiles, image) { async function createGameObject(gameId, targetTiles, image, ts) {
const rng = new Rng(gameId); const seed = Util.hash(gameId + ' ' + ts)
const rng = new Rng(seed)
return GameCommon.__createGameObject(
gameId,
{
type: 'Rng',
obj: rng,
},
await createPuzzle(rng, targetTiles, image, ts),
{},
[],
{}
)
}
async function createGame(gameId, targetTiles, image, ts) {
GameLog.log(gameId, 'createGame', targetTiles, image, ts)
const seed = Util.hash(gameId + ' ' + ts)
const rng = new Rng(seed)
GameCommon.newGame({ GameCommon.newGame({
id: gameId, id: gameId,
rng: { rng: {
type: 'Rng', type: 'Rng',
obj: rng, obj: rng,
}, },
puzzle: await createPuzzle(rng, targetTiles, image), puzzle: await createPuzzle(rng, targetTiles, image, ts),
players: {}, players: {},
sockets: [], sockets: [],
evtInfos: {}, evtInfos: {},
}) })
changedGames[gameId] = true changedGames[gameId] = true
} }
function addPlayer(gameId, playerId) { function addPlayer(gameId, playerId, ts) {
GameCommon.addPlayer(gameId, playerId) GameLog.log(gameId, 'addPlayer', playerId, ts)
GameCommon.addPlayer(gameId, playerId, ts)
changedGames[gameId] = true changedGames[gameId] = true
} }
@ -68,8 +89,10 @@ function addSocket(gameId, socket) {
changedGames[gameId] = true changedGames[gameId] = true
} }
function handleInput(gameId, playerId, input) { function handleInput(gameId, playerId, input, ts) {
const ret = GameCommon.handleInput(gameId, playerId, input) GameLog.log(gameId, 'handleInput', playerId, input, ts)
const ret = GameCommon.handleInput(gameId, playerId, input, ts)
changedGames[gameId] = true changedGames[gameId] = true
return ret return ret
} }
@ -93,6 +116,7 @@ function persistChangedGames() {
} }
export default { export default {
createGameObject,
loadAllGames, loadAllGames,
persistChangedGames, persistChangedGames,
createGame, createGame,

25
server/GameLog.js Normal file
View file

@ -0,0 +1,25 @@
import fs from 'fs'
const DATA_DIR = './../data'
const log = (gameId, ...args) => {
const str = JSON.stringify(args)
fs.appendFileSync(`${DATA_DIR}/log_${gameId}.log`, str + "\n")
}
const get = (gameId) => {
const all = fs.readFileSync(`${DATA_DIR}/log_${gameId}.log`, 'utf-8')
return all.split("\n").filter(line => !!line).map((line) => {
try {
return JSON.parse(line)
} catch (e) {
console.log(line)
console.log(e)
}
})
}
export default {
log,
get,
}

View file

@ -1,5 +1,5 @@
import sizeOf from 'image-size' import sizeOf from 'image-size'
import Util from './../common/Util.js' import Util from '../common/Util.js'
import exif from 'exif' import exif from 'exif'
import { Rng } from '../common/Rng.js' import { Rng } from '../common/Rng.js'
@ -38,7 +38,8 @@ async function getExifOrientation(imagePath) {
async function createPuzzle( async function createPuzzle(
/** @type Rng */ rng, /** @type Rng */ rng,
targetTiles, targetTiles,
image image,
ts
) { ) {
const imagePath = image.file const imagePath = image.file
const imageUrl = image.url const imageUrl = image.url
@ -135,7 +136,7 @@ async function createPuzzle(
// TODO: maybe calculate this each time? // TODO: maybe calculate this each time?
maxZ: 0, // max z of all pieces maxZ: 0, // max z of all pieces
maxGroup: 0, // max group of all pieces maxGroup: 0, // max group of all pieces
started: Util.timestamp(), // start timestamp started: ts, // start timestamp
finished: 0, // finish timestamp finished: 0, // finish timestamp
}, },
// static puzzle information. stays same for complete duration of // static puzzle information. stays same for complete duration of

View file

@ -11,6 +11,7 @@ import twing from 'twing'
import bodyParser from 'body-parser' import bodyParser from 'body-parser'
import v8 from 'v8' import v8 from 'v8'
import { Rng } from '../common/Rng.js' import { Rng } from '../common/Rng.js'
import GameLog from './GameLog.js'
const allImages = () => [ const allImages = () => [
...fs.readdirSync('./../data/uploads/').map(f => ({ ...fs.readdirSync('./../data/uploads/').map(f => ({
@ -50,6 +51,14 @@ app.use('/g/:gid', async (req, res, next) => {
WS_ADDRESS: config.ws.connectstring, WS_ADDRESS: config.ws.connectstring,
})) }))
}) })
app.use('/replay/:gid', async (req, res, next) => {
res.send(await render('replay.html.twig', {
GAME_ID: req.params.gid,
WS_ADDRESS: config.ws.connectstring,
}))
})
app.post('/upload', (req, res) => { app.post('/upload', (req, res) => {
upload(req, res, (err) => { upload(req, res, (err) => {
if (err) { if (err) {
@ -68,7 +77,8 @@ app.post('/newgame', bodyParser.json(), async (req, res) => {
console.log(req.body.tiles, req.body.image) console.log(req.body.tiles, req.body.image)
const gameId = Util.uniqId() const gameId = Util.uniqId()
if (!Game.exists(gameId)) { if (!Game.exists(gameId)) {
await Game.createGame(gameId, req.body.tiles, req.body.image) const ts = Util.timestamp()
await Game.createGame(gameId, req.body.tiles, req.body.image, ts)
} }
res.send({ url: `/g/${gameId}` }) res.send({ url: `/g/${gameId}` })
}) })
@ -77,6 +87,7 @@ app.use('/common/', express.static('./../common/'))
app.use('/uploads/', express.static('./../data/uploads/')) app.use('/uploads/', express.static('./../data/uploads/'))
app.use('/', async (req, res, next) => { app.use('/', async (req, res, next) => {
if (req.path === '/') { if (req.path === '/') {
const ts = Util.timestamp()
const games = [ const games = [
...Game.getAllGames().map(game => ({ ...Game.getAllGames().map(game => ({
id: game.id, id: game.id,
@ -84,7 +95,7 @@ app.use('/', async (req, res, next) => {
finished: Game.getFinishTs(game.id), finished: Game.getFinishTs(game.id),
tilesFinished: Game.getFinishedTileCount(game.id), tilesFinished: Game.getFinishedTileCount(game.id),
tilesTotal: Game.getTileCount(game.id), tilesTotal: Game.getTileCount(game.id),
players: Game.getActivePlayers(game.id).length, players: Game.getActivePlayers(game.id, ts).length,
imageUrl: Game.getImageUrl(game.id), imageUrl: Game.getImageUrl(game.id),
})), })),
] ]
@ -124,11 +135,36 @@ wss.on('message', async ({socket, data}) => {
const msg = JSON.parse(data) const msg = JSON.parse(data)
const msgType = msg[0] const msgType = msg[0]
switch (msgType) { switch (msgType) {
case Protocol.EV_CLIENT_INIT_REPLAY: {
const log = GameLog.get(gameId)
let game = await Game.createGameObject(
gameId,
log[0][1],
log[0][2],
log[0][3]
)
notify(
[Protocol.EV_SERVER_INIT_REPLAY, {
id: game.id,
rng: {
type: game.rng.type,
obj: Rng.serialize(game.rng.obj),
},
puzzle: game.puzzle,
players: game.players,
sockets: [],
evtInfos: game.evtInfos,
}, log],
[socket]
)
} break;
case Protocol.EV_CLIENT_INIT: { case Protocol.EV_CLIENT_INIT: {
if (!Game.exists(gameId)) { if (!Game.exists(gameId)) {
throw `[game ${gameId} does not exist... ]` throw `[game ${gameId} does not exist... ]`
} }
Game.addPlayer(gameId, clientId) const ts = Util.timestamp()
Game.addPlayer(gameId, clientId, ts)
Game.addSocket(gameId, socket) Game.addSocket(gameId, socket)
const game = Game.get(gameId) const game = Game.get(gameId)
notify( notify(
@ -150,7 +186,9 @@ wss.on('message', async ({socket, data}) => {
case Protocol.EV_CLIENT_EVENT: { case Protocol.EV_CLIENT_EVENT: {
const clientSeq = msg[1] const clientSeq = msg[1]
const clientEvtData = msg[2] const clientEvtData = msg[2]
Game.addPlayer(gameId, clientId) const ts = Util.timestamp()
Game.addPlayer(gameId, clientId, ts)
Game.addSocket(gameId, socket) Game.addSocket(gameId, socket)
const game = Game.get(gameId) const game = Game.get(gameId)
@ -164,7 +202,7 @@ wss.on('message', async ({socket, data}) => {
}], }],
[socket] [socket]
) )
const changes = Game.handleInput(gameId, clientId, clientEvtData) const changes = Game.handleInput(gameId, clientId, clientEvtData, ts)
notify( notify(
[Protocol.EV_SERVER_EVENT, clientId, clientSeq, changes], [Protocol.EV_SERVER_EVENT, clientId, clientSeq, changes],
Game.getSockets(gameId) Game.getSockets(gameId)