rng class..

This commit is contained in:
Zutatensuppe 2020-12-21 18:34:57 +01:00
parent 3ff375dbb4
commit cbc2b88f47
12 changed files with 4762 additions and 25 deletions

View file

@ -7,9 +7,10 @@ function exists(gameId) {
return (!!GAMES[gameId]) || false return (!!GAMES[gameId]) || false
} }
function createGame(id, puzzle, players, sockets, evtInfos) { function createGame(id, rng, puzzle, players, sockets, evtInfos) {
return { return {
id: id, id: id,
rng: rng,
puzzle: puzzle, puzzle: puzzle,
players: players, players: players,
sockets: sockets, sockets: sockets,
@ -31,8 +32,8 @@ function createPlayer(id, ts) {
} }
} }
function newGame({id, puzzle, players, sockets, evtInfos}) { function newGame({id, rng, puzzle, players, sockets, evtInfos}) {
const game = createGame(id, puzzle, players, sockets, evtInfos) const game = createGame(id, rng, puzzle, players, sockets, evtInfos)
setGame(id, game) setGame(id, game)
return game return game
} }

27
common/Rng.js Normal file
View file

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

View file

@ -1,11 +1,20 @@
import { Rng } from './Rng.js'
// get a unique id // get a unique id
export const uniqId = () => Date.now().toString(36) + Math.random().toString(36).substring(2) export const uniqId = () => Date.now().toString(36) + Math.random().toString(36).substring(2)
// get a random int between min and max (inclusive) // get a random int between min and max (inclusive)
export const randomInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min export const randomInt = (
/** @type Rng */ rng,
min,
max
) => rng.random(min, max)
// get one random item from the given array // get one random item from the given array
export const choice = (array) => array[randomInt(0, array.length - 1)] export const choice = (
/** @type Rng */ rng,
array
) => array[randomInt(rng, 0, array.length - 1)]
export const throttle = (fn, delay) => { export const throttle = (fn, delay) => {
let canCall = true let canCall = true
@ -21,11 +30,14 @@ export const throttle = (fn, delay) => {
} }
// return a shuffled (shallow) copy of the given array // return a shuffled (shallow) copy of the given array
export const shuffle = (array) => { export const shuffle = (
/** @type Rng */ rng,
array
) => {
let arr = array.slice() let arr = array.slice()
for (let i = 0; i <= arr.length - 2; i++) for (let i = 0; i <= arr.length - 2; i++)
{ {
const j = randomInt(i, arr.length -1); const j = randomInt(rng, i, arr.length -1);
const tmp = arr[i]; const tmp = arr[i];
arr[i] = arr[j]; arr[i] = arr[j];
arr[j] = tmp; arr[j] = tmp;

View file

@ -1,4 +1,5 @@
import Util from './../common/Util.js' import { Rng } from '../common/Rng.js'
import Util from '../common/Util.js'
let minVx = -10 let minVx = -10
let deltaVx = 20 let deltaVx = 20
@ -17,22 +18,22 @@ const explosionDividerFactor = 10
const nBombs = 1 const nBombs = 1
const percentChanceNewBomb = 5 const percentChanceNewBomb = 5
function color() { function color(/** @type Rng */ rng) {
const r = Util.randomInt(0, 255) const r = Util.randomInt(rng, 0, 255)
const g = Util.randomInt(0, 255) const g = Util.randomInt(rng, 0, 255)
const b = Util.randomInt(0, 255) const b = Util.randomInt(rng, 0, 255)
return 'rgba(' + r + ',' + g + ',' + b + ', 0.8)' return 'rgba(' + r + ',' + g + ',' + b + ', 0.8)'
} }
// A Bomb. Or firework. // A Bomb. Or firework.
class Bomb { class Bomb {
constructor() { constructor(/** @type Rng */ rng) {
this.radius = bombRadius this.radius = bombRadius
this.previousRadius = bombRadius this.previousRadius = bombRadius
this.explodingDuration = explodingDuration this.explodingDuration = explodingDuration
this.hasExploded = false this.hasExploded = false
this.alive = true this.alive = true
this.color = color() this.color = color(rng)
this.px = (window.innerWidth / 4) + (Math.random() * window.innerWidth / 2) this.px = (window.innerWidth / 4) + (Math.random() * window.innerWidth / 2)
this.py = window.innerHeight this.py = window.innerHeight
@ -124,8 +125,9 @@ class Particle {
} }
class Controller { class Controller {
constructor(canvas) { constructor(canvas, /** @type Rng */ rng) {
this.canvas = canvas this.canvas = canvas
this.rng = rng
this.ctx = this.canvas.getContext('2d') this.ctx = this.canvas.getContext('2d')
this.resize() this.resize()
@ -161,13 +163,13 @@ class Controller {
this.particles = [] this.particles = []
for (let i = 0; i < nBombs; i++) { for (let i = 0; i < nBombs; i++) {
this.readyBombs.push(new Bomb()) this.readyBombs.push(new Bomb(this.rng))
} }
} }
update() { update() {
if (Math.random() * 100 < percentChanceNewBomb) { if (Math.random() * 100 < percentChanceNewBomb) {
this.readyBombs.push(new Bomb()) this.readyBombs.push(new Bomb(this.rng))
} }
const aliveBombs = [] const aliveBombs = []

View file

@ -8,6 +8,7 @@ import Util from './../common/Util.js'
import PuzzleGraphics from './PuzzleGraphics.js' import PuzzleGraphics from './PuzzleGraphics.js'
import Game from './Game.js' import Game from './Game.js'
import fireworksController from './Fireworks.js' import fireworksController from './Fireworks.js'
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 ]'
@ -324,6 +325,7 @@ async function main() {
} }
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.newGame(game) Game.newGame(game)
const bitmaps = await PuzzleGraphics.loadPuzzleBitmaps(game.puzzle) const bitmaps = await PuzzleGraphics.loadPuzzleBitmaps(game.puzzle)
@ -338,7 +340,7 @@ async function main() {
let finished = longFinished ? true : false let finished = longFinished ? true : false
const justFinished = () => !!(finished && !longFinished) const justFinished = () => !!(finished && !longFinished)
const fireworks = new fireworksController(canvas) const fireworks = new fireworksController(canvas, game.rng.obj)
fireworks.init(canvas) fireworks.init(canvas)
const ctx = canvas.getContext('2d') const ctx = canvas.getContext('2d')

4636
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -8,5 +8,8 @@
"multer": "^1.4.2", "multer": "^1.4.2",
"twing": "^5.0.2", "twing": "^5.0.2",
"ws": "^7.3.1" "ws": "^7.3.1"
},
"devDependencies": {
"jest": "^26.6.3"
} }
} }

3
scripts/tests Executable file
View file

@ -0,0 +1,3 @@
#!/bin/sh
node --experimental-vm-modules node_modules/.bin/jest

View file

@ -2,6 +2,7 @@ import fs from 'fs'
import { createPuzzle } from './Puzzle.js' 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'
const DATA_DIR = './../data' const DATA_DIR = './../data'
@ -28,6 +29,10 @@ function loadAllGames() {
} }
GameCommon.newGame({ GameCommon.newGame({
id: game.id, id: game.id,
rng: {
type: game.rng ? game.rng.type : '_fake_',
obj: game.rng ? Rng.unserialize(game.rng.obj) : new Rng(),
},
puzzle: game.puzzle, puzzle: game.puzzle,
players: game.players, players: game.players,
sockets: [], sockets: [],
@ -38,9 +43,14 @@ function loadAllGames() {
const changedGames = {} const changedGames = {}
async function createGame(gameId, targetTiles, image) { async function createGame(gameId, targetTiles, image) {
const rng = new Rng(gameId);
GameCommon.newGame({ GameCommon.newGame({
id: gameId, id: gameId,
puzzle: await createPuzzle(targetTiles, image), rng: {
type: 'Rng',
obj: rng,
},
puzzle: await createPuzzle(rng, targetTiles, image),
players: {}, players: {},
sockets: [], sockets: [],
evtInfos: {}, evtInfos: {},
@ -70,6 +80,10 @@ function persistChangedGames() {
delete changedGames[game.id] delete changedGames[game.id]
fs.writeFileSync(`${DATA_DIR}/${game.id}.json`, JSON.stringify({ fs.writeFileSync(`${DATA_DIR}/${game.id}.json`, JSON.stringify({
id: game.id, id: game.id,
rng: {
type: game.rng.type,
obj: Rng.serialize(game.rng.obj),
},
puzzle: game.puzzle, puzzle: game.puzzle,
players: game.players, players: game.players,
})) }))

View file

@ -1,6 +1,7 @@
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'
// cut size of each puzzle tile in the // cut size of each puzzle tile in the
// final resized version of the puzzle image // final resized version of the puzzle image
@ -34,7 +35,11 @@ async function getExifOrientation(imagePath) {
}) })
} }
async function createPuzzle(targetTiles, image) { async function createPuzzle(
/** @type Rng */ rng,
targetTiles,
image
) {
const imagePath = image.file const imagePath = image.file
const imageUrl = image.url const imageUrl = image.url
@ -48,7 +53,7 @@ async function createPuzzle(targetTiles, image) {
for (let i = 0; i < tiles.length; i++) { for (let i = 0; i < tiles.length; i++) {
tiles[i] = { idx: i } tiles[i] = { idx: i }
} }
const shapes = determinePuzzleTileShapes(info) const shapes = determinePuzzleTileShapes(rng, info)
let positions = new Array(info.tiles) let positions = new Array(info.tiles)
for (let tile of tiles) { for (let tile of tiles) {
@ -100,7 +105,7 @@ async function createPuzzle(targetTiles, image) {
} }
// then shuffle the positions // then shuffle the positions
positions = Util.shuffle(positions) positions = Util.shuffle(rng, positions)
tiles = tiles.map(tile => { tiles = tiles.map(tile => {
return Util.encodeTile({ return Util.encodeTile({
@ -167,7 +172,10 @@ async function createPuzzle(targetTiles, image) {
} }
} }
function determinePuzzleTileShapes(info) { function determinePuzzleTileShapes(
/** @type Rng */ rng,
info
) {
const tabs = [-1, 1] const tabs = [-1, 1]
const shapes = new Array(info.tiles) const shapes = new Array(info.tiles)
@ -175,9 +183,9 @@ function determinePuzzleTileShapes(info) {
let coord = Util.coordByTileIdx(info, i) let coord = Util.coordByTileIdx(info, i)
shapes[i] = { shapes[i] = {
top: coord.y === 0 ? 0 : shapes[i - info.tilesX].bottom * -1, top: coord.y === 0 ? 0 : shapes[i - info.tilesX].bottom * -1,
right: coord.x === info.tilesX - 1 ? 0 : Util.choice(tabs), right: coord.x === info.tilesX - 1 ? 0 : Util.choice(rng, tabs),
left: coord.x === 0 ? 0 : shapes[i - 1].right * -1, left: coord.x === 0 ? 0 : shapes[i - 1].right * -1,
bottom: coord.y === info.tilesY - 1 ? 0 : Util.choice(tabs), bottom: coord.y === info.tilesY - 1 ? 0 : Util.choice(rng, tabs),
} }
} }
return shapes.map(Util.encodeShape) return shapes.map(Util.encodeShape)

View file

@ -10,6 +10,7 @@ import Game from './Game.js'
import twing from 'twing' 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'
const allImages = () => [ const allImages = () => [
...fs.readdirSync('./../data/uploads/').map(f => ({ ...fs.readdirSync('./../data/uploads/').map(f => ({
@ -130,9 +131,14 @@ wss.on('message', async ({socket, data}) => {
Game.addPlayer(gameId, clientId) Game.addPlayer(gameId, clientId)
Game.addSocket(gameId, socket) Game.addSocket(gameId, socket)
const game = Game.get(gameId) const game = Game.get(gameId)
console.log(gameId, game)
notify( notify(
[Protocol.EV_SERVER_INIT, { [Protocol.EV_SERVER_INIT, {
id: game.id, id: game.id,
rng: {
type: game.rng.type,
obj: Rng.serialize(game.rng.obj),
},
puzzle: game.puzzle, puzzle: game.puzzle,
players: game.players, players: game.players,
sockets: [], sockets: [],

23
tests/Rng.test.js Normal file
View file

@ -0,0 +1,23 @@
import { Rng } from '../common/Rng'
test('random should give same results', () => {
const rng = new Rng(1337)
const rng2 = new Rng(1337)
for (let i = 0; i < 100; i++) {
expect(rng.random()).toBe(rng2.random())
}
})
test('should be serializable/deserializable', () => {
const rng = new Rng(1337)
// do some randoms, so that it doesnt start at 'after init'
for (let i = 0; i < 100; i++) {
rng.random()
}
const rng2 = Rng.unserialize(Rng.serialize(rng))
for (let i = 0; i < 100; i++) {
expect(rng.random()).toBe(rng2.random())
}
})