mumuki-puzzle-runner 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/public/js/muzzle-editor.js +10 -10
- data/lib/public/js/muzzle.js +219 -61
- data/lib/public/vendor/headbreaker.d.ts +93 -41
- data/lib/public/vendor/headbreaker.js +1 -1
- data/lib/test_hook.rb +12 -6
- data/lib/version_hook.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c06f3a9c0ab060ce1746fbd08687e44bdd3edc9d2bd7bc9f27991cfa56d34e96
|
4
|
+
data.tar.gz: 3fd982c69ed60580166016ad4824be96a67b5480d238311e347c7588e86ededd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 594855995d69d145ceb3294c3bfc571d681a18e5983e9130884bde9508feea77519764066ceb7fa6b843923dfebc728bfe8a5cf3f08058c9aacc5362f4e784c5
|
7
|
+
data.tar.gz: 6f2d79e5203433e39a8d0bcea6ca80442f8eac32e3eb3d72f04fbadd82490578836444f0c57ad93163d74c9bbbcbcec268b04c97a77d33a2152d191b73cdfba8
|
@@ -4,9 +4,7 @@ $(() => {
|
|
4
4
|
// Muzzle rendering
|
5
5
|
// ================
|
6
6
|
|
7
|
-
|
8
|
-
const $customEditorValue = $('#mu-custom-editor-value');
|
9
|
-
Muzzle.previousSolutionJson = $customEditorValue.val();
|
7
|
+
Muzzle.previousSolutionContent = $('#mu-custom-editor-value').val();
|
10
8
|
|
11
9
|
$('#mu-puzzle-custom-editor').append(`
|
12
10
|
<div id="muzzle-canvas">
|
@@ -20,19 +18,21 @@ $(() => {
|
|
20
18
|
// Submission config
|
21
19
|
// =================
|
22
20
|
|
21
|
+
|
23
22
|
// Required to sync state before submitting
|
24
|
-
mumuki.
|
25
|
-
Muzzle.
|
26
|
-
$customEditorValue.val(Muzzle.previousSolutionJson);
|
23
|
+
mumuki.CustomEditor.addSource({
|
24
|
+
getContent() { return { name: "solution[content]", value: Muzzle.solutionContent }; }
|
27
25
|
});
|
26
|
+
mumuki.CustomEditor.addSource({
|
27
|
+
getContent() { return { name: "client_result[status]", value: Muzzle.clientResultStatus }; }
|
28
|
+
})
|
28
29
|
|
29
30
|
// Requiered to actually bind Muzzle's submit to
|
30
31
|
// mumuki's solution processing
|
31
32
|
const _onSubmit = Muzzle.onSubmit;
|
32
|
-
Muzzle.onSubmit = (
|
33
|
-
|
34
|
-
|
35
|
-
_onSubmit(solutionJson, valid);
|
33
|
+
Muzzle.onSubmit = (submission) => {
|
34
|
+
mumuki.submission.processSolution(submission);
|
35
|
+
_onSubmit(submission);
|
36
36
|
}
|
37
37
|
|
38
38
|
// ===========
|
data/lib/public/js/muzzle.js
CHANGED
@@ -32,19 +32,12 @@ class MuzzleCanvas {
|
|
32
32
|
* @type {Canvas}
|
33
33
|
**/
|
34
34
|
this._canvas = null;
|
35
|
-
this.baseConfig = {
|
36
|
-
width: 800,
|
37
|
-
height: 650,
|
38
|
-
pieceSize: 100,
|
39
|
-
proximity: 20,
|
40
|
-
borderFill: 10,
|
41
|
-
strokeWidth: 1.5,
|
42
|
-
lineSoftness: 0.18
|
43
|
-
};
|
44
35
|
|
45
36
|
/**
|
46
37
|
* The id of the HTML element that will contain the canvas
|
47
38
|
* Override it you are going to place in a non-standard way
|
39
|
+
*
|
40
|
+
* @type {string}
|
48
41
|
*/
|
49
42
|
this.canvasId = id;
|
50
43
|
|
@@ -61,9 +54,46 @@ class MuzzleCanvas {
|
|
61
54
|
* Wether expected refs shall be ignored by Muzzle.
|
62
55
|
*
|
63
56
|
* They will still be evaluated server-side.
|
57
|
+
*
|
58
|
+
* @type {boolean}
|
64
59
|
*/
|
65
60
|
this.expectedRefsAreOnlyDescriptive = false;
|
66
61
|
|
62
|
+
/**
|
63
|
+
* Width of canvas
|
64
|
+
*
|
65
|
+
* @type {number}
|
66
|
+
*/
|
67
|
+
this.canvasWidth = 800;
|
68
|
+
|
69
|
+
/**
|
70
|
+
* Height of canvas
|
71
|
+
*
|
72
|
+
* @type {number}
|
73
|
+
*/
|
74
|
+
this.canvasHeight = 800;
|
75
|
+
|
76
|
+
/**
|
77
|
+
* Size of fill. Set null for perfect-match
|
78
|
+
*
|
79
|
+
* @type {number}
|
80
|
+
*/
|
81
|
+
this.borderFill = null;
|
82
|
+
|
83
|
+
/**
|
84
|
+
* Piece size
|
85
|
+
*
|
86
|
+
* @type {number}
|
87
|
+
*/
|
88
|
+
this.pieceSize = 100;
|
89
|
+
|
90
|
+
/**
|
91
|
+
* * Whether image's width should be scaled to piece
|
92
|
+
*
|
93
|
+
* @type {boolean}
|
94
|
+
*/
|
95
|
+
this.scaleImageWidthToFit = true;
|
96
|
+
|
67
97
|
/**
|
68
98
|
* Callback that will be executed
|
69
99
|
* when muzzle has fully loaded and rendered its first
|
@@ -75,12 +105,12 @@ class MuzzleCanvas {
|
|
75
105
|
this.onReady = () => {};
|
76
106
|
|
77
107
|
/**
|
78
|
-
* The previous solution to the current puzzle in
|
108
|
+
* The previous solution to the current puzzle in a past session,
|
79
109
|
* if any
|
80
110
|
*
|
81
111
|
* @type {string}
|
82
112
|
*/
|
83
|
-
this.
|
113
|
+
this.previousSolutionContent = null
|
84
114
|
|
85
115
|
/**
|
86
116
|
* Callback to be executed when submitting puzzle.
|
@@ -88,15 +118,39 @@ class MuzzleCanvas {
|
|
88
118
|
* Does nothing by default but you can
|
89
119
|
* override it to perform additional actions
|
90
120
|
*
|
91
|
-
* @param {string}
|
92
|
-
|
121
|
+
* @param {{solution: {content: string}, client_result: {status: "passed" | "failed"}}} submission
|
122
|
+
*/
|
123
|
+
this.onSubmit = (submission) => {};
|
124
|
+
|
125
|
+
/**
|
126
|
+
* Callback that will be executed
|
127
|
+
* when muzzle's puzzle becomes valid
|
128
|
+
*
|
129
|
+
* It does nothing by default but you can override this
|
130
|
+
* property with any code you need the be called here
|
93
131
|
*/
|
94
|
-
this.
|
132
|
+
this.onValid = () => {};
|
133
|
+
}
|
134
|
+
|
135
|
+
/**
|
136
|
+
*/
|
137
|
+
get baseConfig() {
|
138
|
+
return {
|
139
|
+
width: this.canvasWidth,
|
140
|
+
height: this.canvasHeight,
|
141
|
+
pieceSize: this.pieceSize,
|
142
|
+
proximity: this.pieceSize / 5,
|
143
|
+
borderFill: this.borderFill === null ? this.pieceSize / 10 : this.borderFill,
|
144
|
+
strokeWidth: 1.5,
|
145
|
+
lineSoftness: 0.18
|
146
|
+
};
|
95
147
|
}
|
96
148
|
|
97
149
|
/**
|
98
150
|
* The currently active canvas, or null if
|
99
151
|
* it has not yet initialized
|
152
|
+
*
|
153
|
+
* @returns {Canvas}
|
100
154
|
*/
|
101
155
|
get canvas() {
|
102
156
|
return this._canvas;
|
@@ -140,47 +194,74 @@ class MuzzleCanvas {
|
|
140
194
|
const image = await this._loadImage(imagePath);
|
141
195
|
/** @type {Canvas} */
|
142
196
|
// @ts-ignore
|
143
|
-
const canvas =
|
197
|
+
const canvas = this._createCanvas(image);
|
144
198
|
canvas.autogenerate({ horizontalPiecesCount: x, verticalPiecesCount: y });
|
145
|
-
this.
|
199
|
+
this._attachBasicValidator(canvas);
|
200
|
+
this._configCanvas(canvas);
|
146
201
|
canvas.onValid(() => {
|
147
202
|
setTimeout(() => {
|
148
|
-
if (canvas.
|
149
|
-
this.submit()
|
203
|
+
if (canvas.valid) {
|
204
|
+
this.submit();
|
150
205
|
}
|
151
206
|
}, 1500);
|
152
207
|
});
|
153
|
-
this._configInitialCanvas(canvas);
|
154
208
|
return canvas;
|
155
209
|
}
|
156
210
|
|
157
211
|
/**
|
158
|
-
* @param {
|
212
|
+
* @param {number} x
|
213
|
+
* @param {number} y
|
214
|
+
* @param {string[]} [imagePaths]
|
159
215
|
* @returns {Promise<Canvas>} the promise of the built canvas
|
160
216
|
*/
|
161
|
-
multi(
|
162
|
-
|
217
|
+
async multi(x, y, imagePaths) {
|
218
|
+
const count = imagePaths.length;
|
219
|
+
const images = await Promise.all(imagePaths.map(imagePath => this._loadImage(imagePath)));
|
220
|
+
|
221
|
+
const canvas = this._createCanvas(null);
|
222
|
+
canvas.autogenerate({ horizontalPiecesCount: x, verticalPiecesCount: y * count });
|
223
|
+
|
224
|
+
// todo validate
|
225
|
+
// todo set images
|
226
|
+
|
227
|
+
this._configCanvas(canvas);
|
228
|
+
return canvas;
|
163
229
|
}
|
164
230
|
|
165
231
|
/**
|
166
|
-
*
|
167
|
-
*
|
232
|
+
* Craates a match puzzle, where left pieces are matched against right pieces,
|
233
|
+
* with optional odd left and right pieces that don't match
|
234
|
+
*
|
235
|
+
* @param {string[]} leftUrls
|
236
|
+
* @param {string[]} rightUrls must be of the same size of lefts
|
237
|
+
* @param {string[]} leftOddUrls
|
238
|
+
* @param {string[]} rightOddUrls
|
168
239
|
* @returns {Promise<Canvas>} the promise of the built canvas
|
169
240
|
*/
|
170
|
-
async match(
|
171
|
-
/** @type {(Promise<Template>)[]} */
|
241
|
+
async match(leftUrls, rightUrls, leftOddUrls = [], rightOddUrls = []) {
|
242
|
+
/** @private @type {(Promise<Template>)[]} */
|
172
243
|
const templatePromises = [];
|
173
|
-
const
|
244
|
+
const pushTemplate = (config, options) =>
|
245
|
+
templatePromises.push(this._createMatchTemplate(config, options));
|
246
|
+
|
247
|
+
const last = leftUrls.length - 1;
|
174
248
|
for (let i = 0; i <= last; i++) {
|
175
|
-
|
176
|
-
|
249
|
+
const leftId = `l${i}`;
|
250
|
+
const rightId = `r${i}`;
|
251
|
+
|
252
|
+
pushTemplate(leftUrls[i], {id: leftId, left: true, rightTargetId: rightId});
|
253
|
+
pushTemplate(rightUrls[i], {id: rightId});
|
177
254
|
}
|
255
|
+
|
256
|
+
leftOddUrls.forEach((it, i) => pushTemplate(it, {id: `lo${i}`, left: true, odd: true}));
|
257
|
+
rightOddUrls.forEach((it, i) => pushTemplate(it, {id: `ro${i}`, odd: true}));
|
258
|
+
|
178
259
|
const templates = await Promise.all(templatePromises);
|
179
260
|
/** @type {Canvas} */
|
180
|
-
const canvas =
|
261
|
+
const canvas = this._createCanvas();
|
181
262
|
templates.forEach(it => canvas.sketchPiece(it));
|
182
|
-
this.
|
183
|
-
this.
|
263
|
+
this._attachMatchValidator(canvas);
|
264
|
+
this._configCanvas(canvas);
|
184
265
|
return canvas;
|
185
266
|
}
|
186
267
|
|
@@ -189,10 +270,19 @@ class MuzzleCanvas {
|
|
189
270
|
* @returns {Promise<Canvas>} the promise of the built canvas
|
190
271
|
*/
|
191
272
|
custom(canvas) {
|
192
|
-
this.
|
273
|
+
this._configCanvas(canvas);
|
193
274
|
return Promise.resolve(canvas);
|
194
275
|
}
|
195
276
|
|
277
|
+
/**
|
278
|
+
* @private
|
279
|
+
* @param {HTMLImageElement} image
|
280
|
+
* @return {Canvas}
|
281
|
+
*/
|
282
|
+
_createCanvas(image = null) {
|
283
|
+
return new headbreaker.Canvas(this.canvasId, this._canvasConfig(image));
|
284
|
+
}
|
285
|
+
|
196
286
|
/**
|
197
287
|
* @private
|
198
288
|
* @param {HTMLImageElement} image
|
@@ -205,16 +295,26 @@ class MuzzleCanvas {
|
|
205
295
|
* @private
|
206
296
|
* @param {Canvas} canvas
|
207
297
|
*/
|
208
|
-
|
298
|
+
_attachBasicValidator(canvas) {
|
209
299
|
if (!this.expectedRefsAreOnlyDescriptive && this._expectedRefs) {
|
210
|
-
canvas.
|
211
|
-
new headbreaker.PuzzleValidator(
|
212
|
-
headbreaker.PuzzleValidator.relativeRefs(this._expectedRefs)));
|
300
|
+
canvas.attachRelativeRefsValidator(this._expectedRefs);
|
213
301
|
} else {
|
214
302
|
canvas.attachSolvedValidator();
|
215
303
|
}
|
216
304
|
}
|
217
305
|
|
306
|
+
/**
|
307
|
+
* @private
|
308
|
+
* @param {Canvas} canvas
|
309
|
+
*/
|
310
|
+
_attachMatchValidator(canvas) {
|
311
|
+
canvas.attachValidator(new headbreaker.PuzzleValidator(
|
312
|
+
puzzle => puzzle.pieces
|
313
|
+
.filter(it => !it.metadata.odd && it.metadata.left)
|
314
|
+
.every(it => it.rightConnection && it.rightConnection.id === it.metadata.rightTargetId)
|
315
|
+
));
|
316
|
+
}
|
317
|
+
|
218
318
|
/**
|
219
319
|
* @private
|
220
320
|
* @param {string} path
|
@@ -226,21 +326,59 @@ class MuzzleCanvas {
|
|
226
326
|
return new Promise((resolve, reject) => image.onload = () => resolve(image));
|
227
327
|
}
|
228
328
|
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
329
|
+
/**
|
330
|
+
* @private
|
331
|
+
* @param {string} imagePath
|
332
|
+
* @param {object} options
|
333
|
+
* @returns {Promise<object>}
|
334
|
+
*/
|
335
|
+
_createMatchTemplate(imagePath, {id, left = false, rightTargetId = null, odd = false}) {
|
336
|
+
const structure = left ? 'T-N-' : `N-S-`;
|
337
|
+
|
338
|
+
return this._loadImage(imagePath).then((image) => {
|
339
|
+
const scale = this._imageScale(image);
|
340
|
+
const offset = this.baseConfig.borderFill / scale;
|
341
|
+
return {
|
342
|
+
structure,
|
343
|
+
metadata: {
|
344
|
+
id,
|
345
|
+
left,
|
346
|
+
odd,
|
347
|
+
rightTargetId,
|
348
|
+
image: {
|
349
|
+
scale,
|
350
|
+
content: image,
|
351
|
+
offset: { x: offset, y: offset }
|
352
|
+
}
|
353
|
+
}
|
354
|
+
}
|
355
|
+
});
|
234
356
|
}
|
235
357
|
|
236
358
|
/**
|
359
|
+
* @private
|
360
|
+
* @param {HTMLImageElement} image
|
361
|
+
*/
|
362
|
+
_imageScale(image) {
|
363
|
+
return this.scaleImageWidthToFit ? this.pieceSize / image.width : 1;
|
364
|
+
}
|
365
|
+
|
366
|
+
/**
|
367
|
+
* @private
|
237
368
|
* @param {Canvas} canvas
|
238
369
|
*/
|
239
|
-
|
370
|
+
_configCanvas(canvas) {
|
240
371
|
this._canvas = canvas;
|
372
|
+
this._canvas.onValid(() => {
|
373
|
+
setTimeout(() => this.onValid(), 0);
|
374
|
+
});
|
241
375
|
this.ready();
|
242
376
|
}
|
243
377
|
|
378
|
+
/**
|
379
|
+
* Mark Muzzle as ready, loading previous solution
|
380
|
+
* and drawing the canvas
|
381
|
+
*/
|
244
382
|
ready() {
|
245
383
|
this.loadPreviousSolution();
|
246
384
|
this.draw();
|
@@ -262,6 +400,8 @@ class MuzzleCanvas {
|
|
262
400
|
}
|
263
401
|
|
264
402
|
/**
|
403
|
+
* Loads - but does not draw - a solution into the canvas.
|
404
|
+
*
|
265
405
|
* @param {Solution} solution
|
266
406
|
*/
|
267
407
|
loadSolution(solution) {
|
@@ -269,12 +409,13 @@ class MuzzleCanvas {
|
|
269
409
|
}
|
270
410
|
|
271
411
|
/**
|
272
|
-
* Loads the current canvas with the
|
412
|
+
* Loads - but does not draw - the current canvas with the previous solution, if available.
|
413
|
+
*
|
273
414
|
*/
|
274
415
|
loadPreviousSolution() {
|
275
|
-
if (this.
|
416
|
+
if (this.previousSolutionContent) {
|
276
417
|
try {
|
277
|
-
this.loadSolution(JSON.parse(this.
|
418
|
+
this.loadSolution(JSON.parse(this.previousSolutionContent));
|
278
419
|
} catch (e) {
|
279
420
|
console.warn("Ignoring unparseabe editor value");
|
280
421
|
}
|
@@ -283,11 +424,6 @@ class MuzzleCanvas {
|
|
283
424
|
}
|
284
425
|
}
|
285
426
|
|
286
|
-
prepareSubmission() {
|
287
|
-
this.canvas.puzzle.validate();
|
288
|
-
this.previousSolutionJson = this._solutionJson;
|
289
|
-
}
|
290
|
-
|
291
427
|
// ==========
|
292
428
|
// Submitting
|
293
429
|
// ==========
|
@@ -297,24 +433,46 @@ class MuzzleCanvas {
|
|
297
433
|
* validating it if necessary
|
298
434
|
*/
|
299
435
|
submit() {
|
300
|
-
this.
|
301
|
-
this.onSubmit(this._solutionJson, this.canvas.puzzle.valid);
|
436
|
+
this.onSubmit(this._prepareSubmission());
|
302
437
|
}
|
303
438
|
|
304
439
|
/**
|
305
440
|
* The current solution, expressed as a JSON string
|
306
441
|
*/
|
307
|
-
get
|
442
|
+
get solutionContent() {
|
308
443
|
return JSON.stringify(this.solution);
|
309
444
|
}
|
310
|
-
}
|
311
445
|
|
446
|
+
/**
|
447
|
+
* The solution validation status
|
448
|
+
*
|
449
|
+
* @returns {"passed" | "failed"}
|
450
|
+
*/
|
451
|
+
get clientResultStatus() {
|
452
|
+
return this.canvas.valid ? 'passed' : 'failed';
|
453
|
+
}
|
312
454
|
|
313
|
-
|
455
|
+
_prepareSubmission() {
|
456
|
+
return {
|
457
|
+
solution: {
|
458
|
+
content: this.solutionContent
|
459
|
+
},
|
460
|
+
client_result: {
|
461
|
+
status: this.clientResultStatus
|
462
|
+
}
|
463
|
+
};
|
464
|
+
}
|
465
|
+
|
466
|
+
}
|
314
467
|
|
315
|
-
Muzzle
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
468
|
+
const Muzzle = new class extends MuzzleCanvas {
|
469
|
+
constructor() {
|
470
|
+
super();
|
471
|
+
this.aux = {};
|
472
|
+
}
|
473
|
+
another = (id) => {
|
474
|
+
const muzzle = new MuzzleCanvas(id);
|
475
|
+
Muzzle.aux[id] = muzzle
|
476
|
+
return muzzle;
|
477
|
+
}
|
320
478
|
}
|