Skip to content

Commit 98f2c1d

Browse files
authored
Merge pull request #18 from Xwilarg/webworker
2 parents d26e9dc + 8e87576 commit 98f2c1d

File tree

7 files changed

+190
-97
lines changed

7 files changed

+190
-97
lines changed

.eslintrc.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
],
2727
"rules": {
2828
"react/react-in-jsx-scope": "off", // handled by @vitejs/plugin-react
29-
"react/prop-types": "off" // todo: readdress this after moving all react files to typescript
29+
"react/prop-types": "off", // todo: readdress this after moving all react files to typescript
30+
"import/no-unresolved": ["error", { "ignore": ["\\?worker$"] }] // ignore vite worker import
3031
}
3132
}

src/App.jsx

+93-29
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { Client as BgioClient } from "boardgame.io/react";
2-
import { Game } from "./Game";
2+
import { Game, generatePuzzleBoard } from "./Game";
33
import { BoardWrapper } from "./components/BoardWrapper";
44
import { parseUrl } from "./Parsing";
55
import { Footer } from "./components/Footer";
6-
import { useState } from "react";
6+
import { useCallback, useEffect, useMemo, useState } from "react";
7+
import PuzzleGenWorker from "./PuzzleGenWorker?worker";
78

89
const wrapBoardWithReload = ({ reload, board: RawBoard }) => {
910
const Board = (props) => {
@@ -14,35 +15,98 @@ const wrapBoardWithReload = ({ reload, board: RawBoard }) => {
1415
};
1516

1617
export const App = () => {
17-
const { seed, setupData } = parseUrl();
18-
const [game, setGame] = useState({ ...Game(setupData), seed });
18+
const setupData = useMemo(() => parseUrl(), []);
19+
const [game, setGame] = useState(null);
20+
const [worker, setWorker] = useState();
1921

20-
const reload = () => {
21-
setGame({ ...Game(setupData), seed });
22-
};
22+
const setupGame = useCallback(() => {
23+
console.log(
24+
`Loading game: ${
25+
setupData.gamemode === "c" ? "classic" : "puzzle"
26+
} gamemode${
27+
setupData.seed != null ? ` with a seed of "${setupData.seed}"` : ""
28+
}, ${setupData.count} piece${setupData.count > 1 ? "s" : ""}, ${
29+
setupData.size
30+
}x${setupData.size} grid, piece${
31+
Object.keys(setupData.pieces).length > 1 ? "s" : ""
32+
} allowed: ${Object.keys(setupData.pieces)
33+
.map((x) => `${x} (x${setupData.pieces[x]})`)
34+
.join(", ")}`
35+
);
2336

24-
const Client = BgioClient({
25-
game,
26-
board: wrapBoardWithReload({ reload, board: BoardWrapper }),
27-
numPlayers: 1,
28-
debug: {
29-
collapseOnLoad: true,
30-
},
31-
});
32-
33-
console.log(
34-
`Loading game: ${
35-
setupData.gamemode === "c" ? "classic" : "puzzle"
36-
} gamemode${seed != null ? ` with a seed of "${seed}"` : ""}, ${
37-
setupData.count
38-
} piece${setupData.count > 1 ? "s" : ""}, ${setupData.size}x${
39-
setupData.size
40-
} grid, piece${
41-
Object.keys(setupData.pieces).length > 1 ? "s" : ""
42-
} allowed: ${Object.keys(setupData.pieces)
43-
.map((x) => `${x} (x${setupData.pieces[x]})`)
44-
.join(", ")}`
45-
);
37+
if (setupData.gamemode === "p") {
38+
if (worker) {
39+
worker.postMessage(setupData);
40+
} else {
41+
const { cells, knownCells, error } = generatePuzzleBoard(
42+
setupData.seed,
43+
setupData.pieces,
44+
setupData.size,
45+
setupData.count,
46+
setupData.difficulty
47+
);
48+
49+
if (error) {
50+
console.error(error);
51+
} else {
52+
setGame({ ...Game({ ...setupData, cells, knownCells }) });
53+
}
54+
}
55+
} else {
56+
setGame({ ...Game(setupData) });
57+
}
58+
}, [worker, setGame, setupData]);
59+
60+
// Setup web worker
61+
useEffect(() => {
62+
// Firefox does not allow module workers, but PuzzleGenWorker is compiled
63+
// to a non-module type in production - so don't allow worker in dev with Firefox
64+
const isFirefox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
65+
const isWorkerAvailable =
66+
process.env.NODE_ENV === "production" || !isFirefox;
67+
68+
let w;
69+
70+
if (isWorkerAvailable) {
71+
w = new PuzzleGenWorker();
72+
w.onmessage = (e) => {
73+
if (typeof e.data === "string") {
74+
console.error(e.data);
75+
} else {
76+
const { cells, knownCells } = e.data;
77+
setGame(Game({ ...setupData, cells, knownCells }));
78+
}
79+
};
80+
setWorker(w);
81+
} else {
82+
setWorker(null);
83+
}
84+
85+
return () => {
86+
if (w) {
87+
w.terminate();
88+
setWorker(undefined);
89+
}
90+
};
91+
}, [setGame, setupData]);
92+
93+
// Wait for worker creation to start game
94+
useEffect(() => {
95+
if (worker !== undefined) {
96+
setupGame();
97+
}
98+
}, [worker, setupGame]);
99+
100+
const Client = game
101+
? BgioClient({
102+
game,
103+
board: wrapBoardWithReload({ reload: setupGame, board: BoardWrapper }),
104+
numPlayers: 1,
105+
debug: {
106+
collapseOnLoad: true,
107+
},
108+
})
109+
: () => <div>Generating Board...</div>;
46110

47111
return (
48112
<div>

src/Game.js

+34-45
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { INVALID_MOVE } from "boardgame.io/dist/cjs/core.js";
1+
import { INVALID_MOVE } from "boardgame.io/core";
2+
import { Random } from "./Random";
23

34
function isValid(data, size, x, y) {
45
return (
@@ -190,9 +191,9 @@ export function generateBoard(random, id, pieces, size, count) {
190191
let data = Array(size * size).fill(0);
191192
let i = count;
192193
while (i > 0) {
193-
const rand = Math.floor(random.Number() * (size * size));
194+
const rand = Math.floor(random.next() * (size * size));
194195
if (rand !== id && Number.isInteger(data[rand])) {
195-
const value = Math.floor(random.Number() * Object.keys(piecesMdf).length);
196+
const value = Math.floor(random.next() * Object.keys(piecesMdf).length);
196197
let piece = Object.keys(piecesMdf)[value];
197198

198199
if (piecesMdf[piece] === 0) {
@@ -231,7 +232,8 @@ function validateBoard(data, discovered, pieces, size) {
231232
}
232233

233234
let str = "";
234-
for (let piece of Object.keys(pieces)) { // Check all pieces
235+
for (let piece of Object.keys(pieces)) {
236+
// Check all pieces
235237
// List of all moves for the current piece
236238
let moves = parseNotation(
237239
pieceMovesCheck[piece],
@@ -279,10 +281,12 @@ function validateBoard(data, discovered, pieces, size) {
279281
};
280282
}
281283

282-
function generatePuzzleBoard(random, pieces, size, count, difficulty) {
284+
export function generatePuzzleBoard(seed, pieces, size, count, difficulty) {
283285
let data;
284286
let discovered;
285-
let hasError = false;
287+
let error;
288+
289+
const random = new Random(seed);
286290

287291
let c = 0;
288292
const maxIt = 300;
@@ -306,7 +310,7 @@ function generatePuzzleBoard(random, pieces, size, count, difficulty) {
306310
}
307311
}
308312
if (possibilities.length > 0) {
309-
let randPos = Math.floor(random.Number() * possibilities.length);
313+
let randPos = Math.floor(random.next() * possibilities.length);
310314
discovered[possibilities[randPos]] = true;
311315
} else {
312316
giveup = true; // Algorithm failed with this generation, we give up
@@ -348,7 +352,7 @@ function generatePuzzleBoard(random, pieces, size, count, difficulty) {
348352
}
349353
}
350354
for (let i = emptyCasesAfter; i > difficulty; i--) {
351-
const rand = Math.floor(random.Number() * possibleTarget.length);
355+
const rand = Math.floor(random.next() * possibleTarget.length);
352356
discovered[possibleTarget[rand]] = true;
353357
possibleTarget.splice(rand, 1).indexOf(rand);
354358
}
@@ -363,11 +367,25 @@ function generatePuzzleBoard(random, pieces, size, count, difficulty) {
363367
}
364368
}
365369

370+
let knownCells;
366371
if (c === maxIt) {
367-
hasError = true;
372+
error = "Failed to generate puzzle";
373+
} else {
374+
knownCells = Array(size * size).fill(false);
375+
for (let i in discovered) {
376+
if (discovered[i]) {
377+
knownCells[i] = true;
378+
}
379+
}
368380
}
369381

370-
return { data, discovered, hasError };
382+
return { cells: data, knownCells, error };
383+
}
384+
385+
function generateClassicBoard(G, id) {
386+
const random = new Random(G.seed);
387+
G.cells = fillPositions(generateBoard(random, id, G.pieces, G.size, G.count));
388+
G.knownCells = Array(G.size * G.size).fill(false);
371389
}
372390

373391
function isWinCondition(G, id) {
@@ -391,40 +409,14 @@ function isWinCondition(G, id) {
391409
export const Game = (setupData) => ({
392410
setup: () => ({
393411
...setupData,
394-
knownCells: null,
395-
cells: null,
412+
knownCells: setupData.knownCells ?? null,
413+
cells: setupData.cells ?? null,
396414
}),
397415

398416
moves: {
399-
generatePuzzleBoard: ({ G, random }) => {
400-
// using destructured values from G caused very slow load times?
401-
const { pieces, size, count, difficulty } = setupData;
402-
403-
const { data, discovered, hasError } = generatePuzzleBoard(
404-
random,
405-
pieces,
406-
size,
407-
count,
408-
difficulty
409-
);
410-
if (!hasError) {
411-
G.cells = data;
412-
G.knownCells = Array(size * size).fill(false);
413-
414-
for (let i in discovered) {
415-
if (discovered[i]) {
416-
G.knownCells[i] = true;
417-
}
418-
}
419-
}
420-
},
421-
422-
discoverPiece: ({ G, events, random }, id) => {
417+
discoverPiece: ({ G, events }, id) => {
423418
if (G.cells === null) {
424-
G.cells = fillPositions(
425-
generateBoard(random, id, G.pieces, G.size, G.count)
426-
);
427-
G.knownCells = Array(G.size * G.size).fill(false);
419+
generateClassicBoard(G, id);
428420
}
429421

430422
if (G.knownCells[id] !== false || G.gamemode === "p") {
@@ -438,12 +430,9 @@ export const Game = (setupData) => ({
438430
}
439431
},
440432

441-
placeHint: ({ G, events, random }, id, action) => {
433+
placeHint: ({ G, events }, id, action) => {
442434
if (G.cells === null) {
443-
G.cells = fillPositions(
444-
generateBoard(random, id, G.pieces, G.size, G.count)
445-
);
446-
G.knownCells = Array(G.size * G.size).fill(false);
435+
generateClassicBoard(G, id);
447436
}
448437

449438
if (G.knownCells[id] === true) {

src/Parsing.js

+6-8
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ function findGetParameter(parameterName) {
1717

1818
export function parseUrl() {
1919
// Setting everything from URL parameter
20-
const seed = findGetParameter("r") ?? undefined;
20+
const seed = findGetParameter("r");
2121
let pieces = findGetParameter("p");
2222
let size = findGetParameter("s");
2323
let count = findGetParameter("c");
@@ -135,12 +135,10 @@ export function parseUrl() {
135135

136136
return {
137137
seed,
138-
setupData: {
139-
pieces: availablePieces,
140-
size,
141-
count,
142-
gamemode,
143-
difficulty,
144-
},
138+
pieces: availablePieces,
139+
size,
140+
count,
141+
gamemode,
142+
difficulty,
145143
};
146144
}

src/PuzzleGenWorker.js

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { generatePuzzleBoard } from "./Game";
2+
3+
onmessage = (e) => {
4+
const { seed, pieces, size, count, difficulty } = e.data;
5+
6+
const { cells, knownCells, error } = generatePuzzleBoard(
7+
seed,
8+
pieces,
9+
size,
10+
count,
11+
difficulty
12+
);
13+
14+
if (error) {
15+
postMessage(error);
16+
} else {
17+
postMessage({ cells, knownCells });
18+
}
19+
};

src/Random.js

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
export class Random
2+
{
3+
constructor(seed) {
4+
function getHashCode(value) { // https://stackoverflow.com/a/7616484
5+
var hash = 0,
6+
i, chr;
7+
if (value.length === 0) return hash;
8+
for (i = 0; i < value.length; i++) {
9+
chr = value.charCodeAt(i);
10+
hash = ((hash << 5) - hash) + chr;
11+
hash |= 0; // Convert to 32bit integer
12+
}
13+
return hash;
14+
}
15+
16+
if (!seed) {
17+
this.seed = Date.now();
18+
} else {
19+
this.seed = getHashCode(seed);
20+
}
21+
}
22+
23+
setSeed(seed) {
24+
this.seed = seed;
25+
}
26+
27+
reset() {
28+
this.seed = Date.now();
29+
}
30+
31+
next() {
32+
var x = Math.sin(this.seed++) * 10000;
33+
return x - Math.floor(x);
34+
}
35+
}

0 commit comments

Comments
 (0)