add replay functionality
This commit is contained in:
parent
4158aa0854
commit
083fc0463c
13 changed files with 452 additions and 125 deletions
|
|
@ -7,7 +7,7 @@ function exists(gameId) {
|
|||
return (!!GAMES[gameId]) || false
|
||||
}
|
||||
|
||||
function createGame(id, rng, puzzle, players, sockets, evtInfos) {
|
||||
function __createGameObject(id, rng, puzzle, players, sockets, evtInfos) {
|
||||
return {
|
||||
id: id,
|
||||
rng: rng,
|
||||
|
|
@ -18,7 +18,7 @@ function createGame(id, rng, puzzle, players, sockets, evtInfos) {
|
|||
}
|
||||
}
|
||||
|
||||
function createPlayer(id, ts) {
|
||||
function __createPlayerObject(id, ts) {
|
||||
return {
|
||||
id: id,
|
||||
x: 0,
|
||||
|
|
@ -33,7 +33,7 @@ function createPlayer(id, ts) {
|
|||
}
|
||||
|
||||
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)
|
||||
return game
|
||||
}
|
||||
|
|
@ -62,26 +62,23 @@ function playerExists(gameId, playerId) {
|
|||
return !!GAMES[gameId].players[playerId]
|
||||
}
|
||||
|
||||
function getRelevantPlayers(gameId) {
|
||||
const ts = Util.timestamp()
|
||||
function getRelevantPlayers(gameId, ts) {
|
||||
const minTs = ts - 30000
|
||||
return getAllPlayers(gameId).filter(player => {
|
||||
return player.ts >= minTs || player.points > 0
|
||||
})
|
||||
}
|
||||
|
||||
function getActivePlayers(gameId) {
|
||||
const ts = Util.timestamp()
|
||||
function getActivePlayers(gameId, ts) {
|
||||
const minTs = ts - 30000
|
||||
return getAllPlayers(gameId).filter(player => {
|
||||
return player.ts >= minTs
|
||||
})
|
||||
}
|
||||
|
||||
function addPlayer(gameId, playerId) {
|
||||
const ts = Util.timestamp()
|
||||
function addPlayer(gameId, playerId, ts) {
|
||||
if (!GAMES[gameId].players[playerId]) {
|
||||
setPlayer(gameId, playerId, createPlayer(playerId, ts))
|
||||
setPlayer(gameId, playerId, __createPlayerObject(playerId, ts))
|
||||
} else {
|
||||
changePlayer(gameId, playerId, { ts })
|
||||
}
|
||||
|
|
@ -368,19 +365,23 @@ const freeTileIdxByPos = (gameId, pos) => {
|
|||
}
|
||||
|
||||
const getPlayerBgColor = (gameId, playerId) => {
|
||||
return getPlayer(gameId, playerId).bgcolor
|
||||
const p = getPlayer(gameId, playerId)
|
||||
return p ? p.bgcolor : null
|
||||
}
|
||||
|
||||
const getPlayerColor = (gameId, playerId) => {
|
||||
return getPlayer(gameId, playerId).color
|
||||
const p = getPlayer(gameId, playerId)
|
||||
return p ? p.color : null
|
||||
}
|
||||
|
||||
const getPlayerName = (gameId, playerId) => {
|
||||
return getPlayer(gameId, playerId).name
|
||||
const p = getPlayer(gameId, playerId)
|
||||
return p ? p.name : null
|
||||
}
|
||||
|
||||
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
|
||||
|
|
@ -398,6 +399,14 @@ const getTableHeight = (gameId) => {
|
|||
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) => {
|
||||
return GAMES[gameId].puzzle.info.width
|
||||
}
|
||||
|
|
@ -406,7 +415,7 @@ const getPuzzleHeight = (gameId) => {
|
|||
return GAMES[gameId].puzzle.info.height
|
||||
}
|
||||
|
||||
function handleInput(gameId, playerId, input) {
|
||||
function handleInput(gameId, playerId, input, ts) {
|
||||
const puzzle = GAMES[gameId].puzzle
|
||||
let evtInfo = GAMES[gameId].evtInfos[playerId]
|
||||
|
||||
|
|
@ -472,8 +481,6 @@ function handleInput(gameId, playerId, input) {
|
|||
}
|
||||
}
|
||||
|
||||
const ts = Util.timestamp()
|
||||
|
||||
const type = input[0]
|
||||
if (type === 'bg_color') {
|
||||
const bgcolor = input[1]
|
||||
|
|
@ -559,7 +566,7 @@ function handleInput(gameId, playerId, input) {
|
|||
_tileChanges(tileIdxs)
|
||||
// check if the puzzle is finished
|
||||
if (getFinishedTileCount(gameId) === getTileCount(gameId)) {
|
||||
changeData(gameId, { finished: Util.timestamp() })
|
||||
changeData(gameId, { finished: ts })
|
||||
_dataChange()
|
||||
}
|
||||
} else {
|
||||
|
|
@ -614,6 +621,8 @@ function handleInput(gameId, playerId, input) {
|
|||
}
|
||||
|
||||
export default {
|
||||
__createGameObject,
|
||||
__createPlayerObject,
|
||||
newGame,
|
||||
exists,
|
||||
playerExists,
|
||||
|
|
@ -638,6 +647,8 @@ export default {
|
|||
setPuzzleData,
|
||||
getTableWidth,
|
||||
getTableHeight,
|
||||
getPuzzle,
|
||||
getRng,
|
||||
getPuzzleWidth,
|
||||
getPuzzleHeight,
|
||||
getTilesSortedByZIndex,
|
||||
|
|
|
|||
|
|
@ -40,12 +40,16 @@ EV_SERVER_INIT: event sent to one client after that client
|
|||
*/
|
||||
const EV_SERVER_EVENT = 1
|
||||
const EV_SERVER_INIT = 4
|
||||
const EV_SERVER_INIT_REPLAY = 5
|
||||
const EV_CLIENT_EVENT = 2
|
||||
const EV_CLIENT_INIT = 3
|
||||
const EV_CLIENT_INIT_REPLAY = 6
|
||||
|
||||
export default {
|
||||
EV_SERVER_EVENT,
|
||||
EV_SERVER_INIT,
|
||||
EV_SERVER_INIT_REPLAY,
|
||||
EV_CLIENT_EVENT,
|
||||
EV_CLIENT_INIT,
|
||||
EV_CLIENT_INIT_REPLAY,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
hash,
|
||||
uniqId,
|
||||
randomInt,
|
||||
choice,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
// when sending event, increase number of sent events
|
||||
// and add the event locally
|
||||
|
|
@ -52,6 +71,7 @@ function sendClientEvent(mouse) {
|
|||
|
||||
export default {
|
||||
connect,
|
||||
connectReplay,
|
||||
onServerChange,
|
||||
sendClientEvent,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ export default {
|
|||
newGame: GameCommon.newGame,
|
||||
getRelevantPlayers: GameCommon.getRelevantPlayers,
|
||||
getActivePlayers: GameCommon.getActivePlayers,
|
||||
addPlayer: GameCommon.addPlayer,
|
||||
handleInput: GameCommon.handleInput,
|
||||
getPlayerBgColor: GameCommon.getPlayerBgColor,
|
||||
getPlayerColor: GameCommon.getPlayerColor,
|
||||
|
|
@ -15,6 +16,8 @@ export default {
|
|||
setPuzzleData: GameCommon.setPuzzleData,
|
||||
getTableWidth: GameCommon.getTableWidth,
|
||||
getTableHeight: GameCommon.getTableHeight,
|
||||
getPuzzle: GameCommon.getPuzzle,
|
||||
getRng: GameCommon.getRng,
|
||||
getPuzzleWidth: GameCommon.getPuzzleWidth,
|
||||
getPuzzleHeight: GameCommon.getPuzzleHeight,
|
||||
getTilesSortedByZIndex: GameCommon.getTilesSortedByZIndex,
|
||||
|
|
|
|||
183
game/game.js
183
game/game.js
|
|
@ -12,11 +12,14 @@ import { Rng } from '../common/Rng.js'
|
|||
|
||||
if (typeof GAME_ID === 'undefined') throw '[ GAME_ID 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
|
||||
|
||||
let RERENDER = true
|
||||
|
||||
let TIME = () => Util.timestamp()
|
||||
|
||||
function addCanvasToDom(canvas) {
|
||||
canvas.width = window.innerWidth
|
||||
canvas.height = window.innerHeight
|
||||
|
|
@ -41,6 +44,13 @@ function addMenuToDom(gameId) {
|
|||
return row
|
||||
}
|
||||
|
||||
function btn(txt) {
|
||||
const btn = document.createElement('button')
|
||||
btn.classList.add('btn')
|
||||
btn.innerText = txt
|
||||
return btn
|
||||
}
|
||||
|
||||
function colorinput() {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'color'
|
||||
|
|
@ -143,10 +153,10 @@ function addMenuToDom(gameId) {
|
|||
|
||||
const scoresListEl = document.createElement('table')
|
||||
const updateScores = () => {
|
||||
const ts = Util.timestamp()
|
||||
const ts = TIME()
|
||||
const minTs = ts - 30000
|
||||
|
||||
const players = Game.getRelevantPlayers(gameId)
|
||||
const players = Game.getRelevantPlayers(gameId, ts)
|
||||
const actives = 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 from = started;
|
||||
const to = ended || Util.timestamp()
|
||||
const to = ended || TIME()
|
||||
|
||||
const MS = 1
|
||||
const SEC = MS * 1000
|
||||
|
|
@ -208,12 +218,27 @@ function addMenuToDom(gameId) {
|
|||
timerCountdownEl.innerText = timerStr()
|
||||
setInterval(() => {
|
||||
timerCountdownEl.innerText = timerStr()
|
||||
}, 1000)
|
||||
}, 50) // needs to be small, so that it updates quick enough in replay
|
||||
|
||||
const timerEl = document.createElement('div')
|
||||
timerEl.classList.add('timer')
|
||||
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')
|
||||
scoresEl.classList.add('scores')
|
||||
scoresEl.appendChild(scoresTitleEl)
|
||||
|
|
@ -230,6 +255,7 @@ function addMenuToDom(gameId) {
|
|||
playerColorPickerEl,
|
||||
nameChangeEl,
|
||||
updateScores,
|
||||
replayControl,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -324,23 +350,46 @@ async function main() {
|
|||
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)
|
||||
game.rng.obj = Rng.unserialize(game.rng.obj)
|
||||
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()
|
||||
|
||||
// Create a dom and attach adapters to it so we can work with it
|
||||
const canvas = addCanvasToDom(Graphics.createCanvas())
|
||||
|
||||
const longFinished = Game.getFinishTs(gameId)
|
||||
let finished = longFinished ? true : false
|
||||
const justFinished = () => !!(finished && !longFinished)
|
||||
|
||||
const fireworks = new fireworksController(canvas, game.rng.obj)
|
||||
const fireworks = new fireworksController(canvas, Game.getRng(gameId))
|
||||
fireworks.init(canvas)
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
|
@ -371,6 +420,7 @@ async function main() {
|
|||
}
|
||||
|
||||
const evts = new EventAdapter(canvas, viewport)
|
||||
if (MODE === 'play') {
|
||||
bgColorPickerEl.value = playerBgColor()
|
||||
evts.addEvent(['bg_color', bgColorPickerEl.value])
|
||||
bgColorPickerEl.addEventListener('change', () => {
|
||||
|
|
@ -389,7 +439,30 @@ async function main() {
|
|||
localStorage.setItem('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) => {
|
||||
const msgType = msg[0]
|
||||
const evClientId = msg[1]
|
||||
|
|
@ -417,11 +490,53 @@ async function main() {
|
|||
}
|
||||
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
|
||||
const onUpdate = () => {
|
||||
for (let evt of evts.consumeAll()) {
|
||||
|
||||
if (MODE === 'play') {
|
||||
// LOCAL ONLY CHANGES
|
||||
// -------------------------------------------------------------
|
||||
const type = evt[0]
|
||||
|
|
@ -432,6 +547,7 @@ async function main() {
|
|||
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
|
||||
|
|
@ -457,11 +573,41 @@ async function main() {
|
|||
|
||||
// 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) {
|
||||
RERENDER = true
|
||||
}
|
||||
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)
|
||||
|
|
@ -528,13 +674,15 @@ async function main() {
|
|||
|
||||
// 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 pos = viewport.worldToViewport(player)
|
||||
ctx.drawImage(cursor,
|
||||
Math.round(pos.x - cursor.width/2),
|
||||
Math.round(pos.y - cursor.height/2)
|
||||
)
|
||||
if (MODE === 'play') {
|
||||
if (player.id !== CLIENT_ID) {
|
||||
ctx.fillStyle = 'white'
|
||||
ctx.font = '10px sans-serif'
|
||||
|
|
@ -544,6 +692,15 @@ async function main() {
|
|||
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')
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ a:hover { color: var(--link-hover-color); }
|
|||
.scores {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
|
||||
background: var(--bg-color);
|
||||
padding: 5px;
|
||||
|
|
@ -32,6 +33,7 @@ a:hover { color: var(--link-hover-color); }
|
|||
.timer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
|
||||
background: var(--bg-color);
|
||||
padding: 5px;
|
||||
|
|
@ -41,6 +43,8 @@ a:hover { color: var(--link-hover-color); }
|
|||
|
||||
.menu {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--bg-color);
|
||||
|
|
@ -192,3 +196,9 @@ input:focus {
|
|||
background: var(--bg-color);
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.game-replay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
<body>
|
||||
<script>window.GAME_ID = '{{GAME_ID}}'</script>
|
||||
<script>window.WS_ADDRESS = '{{WS_ADDRESS}}'</script>
|
||||
<script>window.MODE = 'play'</script>
|
||||
<script src="/game.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
21
game/templates/replay.html.twig
Normal file
21
game/templates/replay.html.twig
Normal 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>
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
import fs from 'fs'
|
||||
import { createPuzzle } from './Puzzle.js'
|
||||
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'
|
||||
|
||||
const DATA_DIR = './../data'
|
||||
|
||||
|
|
@ -42,24 +43,44 @@ function loadAllGames() {
|
|||
}
|
||||
|
||||
const changedGames = {}
|
||||
async function createGame(gameId, targetTiles, image) {
|
||||
const rng = new Rng(gameId);
|
||||
async function createGameObject(gameId, targetTiles, image, ts) {
|
||||
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({
|
||||
id: gameId,
|
||||
rng: {
|
||||
type: 'Rng',
|
||||
obj: rng,
|
||||
},
|
||||
puzzle: await createPuzzle(rng, targetTiles, image),
|
||||
puzzle: await createPuzzle(rng, targetTiles, image, ts),
|
||||
players: {},
|
||||
sockets: [],
|
||||
evtInfos: {},
|
||||
})
|
||||
|
||||
changedGames[gameId] = true
|
||||
}
|
||||
|
||||
function addPlayer(gameId, playerId) {
|
||||
GameCommon.addPlayer(gameId, playerId)
|
||||
function addPlayer(gameId, playerId, ts) {
|
||||
GameLog.log(gameId, 'addPlayer', playerId, ts)
|
||||
GameCommon.addPlayer(gameId, playerId, ts)
|
||||
changedGames[gameId] = true
|
||||
}
|
||||
|
||||
|
|
@ -68,8 +89,10 @@ function addSocket(gameId, socket) {
|
|||
changedGames[gameId] = true
|
||||
}
|
||||
|
||||
function handleInput(gameId, playerId, input) {
|
||||
const ret = GameCommon.handleInput(gameId, playerId, input)
|
||||
function handleInput(gameId, playerId, input, ts) {
|
||||
GameLog.log(gameId, 'handleInput', playerId, input, ts)
|
||||
|
||||
const ret = GameCommon.handleInput(gameId, playerId, input, ts)
|
||||
changedGames[gameId] = true
|
||||
return ret
|
||||
}
|
||||
|
|
@ -93,6 +116,7 @@ function persistChangedGames() {
|
|||
}
|
||||
|
||||
export default {
|
||||
createGameObject,
|
||||
loadAllGames,
|
||||
persistChangedGames,
|
||||
createGame,
|
||||
|
|
|
|||
25
server/GameLog.js
Normal file
25
server/GameLog.js
Normal 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,
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import sizeOf from 'image-size'
|
||||
import Util from './../common/Util.js'
|
||||
import Util from '../common/Util.js'
|
||||
import exif from 'exif'
|
||||
import { Rng } from '../common/Rng.js'
|
||||
|
||||
|
|
@ -38,7 +38,8 @@ async function getExifOrientation(imagePath) {
|
|||
async function createPuzzle(
|
||||
/** @type Rng */ rng,
|
||||
targetTiles,
|
||||
image
|
||||
image,
|
||||
ts
|
||||
) {
|
||||
const imagePath = image.file
|
||||
const imageUrl = image.url
|
||||
|
|
@ -135,7 +136,7 @@ async function createPuzzle(
|
|||
// TODO: maybe calculate this each time?
|
||||
maxZ: 0, // max z of all pieces
|
||||
maxGroup: 0, // max group of all pieces
|
||||
started: Util.timestamp(), // start timestamp
|
||||
started: ts, // start timestamp
|
||||
finished: 0, // finish timestamp
|
||||
},
|
||||
// static puzzle information. stays same for complete duration of
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import twing from 'twing'
|
|||
import bodyParser from 'body-parser'
|
||||
import v8 from 'v8'
|
||||
import { Rng } from '../common/Rng.js'
|
||||
import GameLog from './GameLog.js'
|
||||
|
||||
const allImages = () => [
|
||||
...fs.readdirSync('./../data/uploads/').map(f => ({
|
||||
|
|
@ -50,6 +51,14 @@ app.use('/g/:gid', async (req, res, next) => {
|
|||
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) => {
|
||||
upload(req, res, (err) => {
|
||||
if (err) {
|
||||
|
|
@ -68,7 +77,8 @@ app.post('/newgame', bodyParser.json(), async (req, res) => {
|
|||
console.log(req.body.tiles, req.body.image)
|
||||
const gameId = Util.uniqId()
|
||||
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}` })
|
||||
})
|
||||
|
|
@ -77,6 +87,7 @@ app.use('/common/', express.static('./../common/'))
|
|||
app.use('/uploads/', express.static('./../data/uploads/'))
|
||||
app.use('/', async (req, res, next) => {
|
||||
if (req.path === '/') {
|
||||
const ts = Util.timestamp()
|
||||
const games = [
|
||||
...Game.getAllGames().map(game => ({
|
||||
id: game.id,
|
||||
|
|
@ -84,7 +95,7 @@ app.use('/', async (req, res, next) => {
|
|||
finished: Game.getFinishTs(game.id),
|
||||
tilesFinished: Game.getFinishedTileCount(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),
|
||||
})),
|
||||
]
|
||||
|
|
@ -124,11 +135,36 @@ wss.on('message', async ({socket, data}) => {
|
|||
const msg = JSON.parse(data)
|
||||
const msgType = msg[0]
|
||||
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: {
|
||||
if (!Game.exists(gameId)) {
|
||||
throw `[game ${gameId} does not exist... ]`
|
||||
}
|
||||
Game.addPlayer(gameId, clientId)
|
||||
const ts = Util.timestamp()
|
||||
Game.addPlayer(gameId, clientId, ts)
|
||||
Game.addSocket(gameId, socket)
|
||||
const game = Game.get(gameId)
|
||||
notify(
|
||||
|
|
@ -150,7 +186,9 @@ wss.on('message', async ({socket, data}) => {
|
|||
case Protocol.EV_CLIENT_EVENT: {
|
||||
const clientSeq = msg[1]
|
||||
const clientEvtData = msg[2]
|
||||
Game.addPlayer(gameId, clientId)
|
||||
const ts = Util.timestamp()
|
||||
|
||||
Game.addPlayer(gameId, clientId, ts)
|
||||
Game.addSocket(gameId, socket)
|
||||
|
||||
const game = Game.get(gameId)
|
||||
|
|
@ -164,7 +202,7 @@ wss.on('message', async ({socket, data}) => {
|
|||
}],
|
||||
[socket]
|
||||
)
|
||||
const changes = Game.handleInput(gameId, clientId, clientEvtData)
|
||||
const changes = Game.handleInput(gameId, clientId, clientEvtData, ts)
|
||||
notify(
|
||||
[Protocol.EV_SERVER_EVENT, clientId, clientSeq, changes],
|
||||
Game.getSockets(gameId)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue