mumuki-puzzle-runner 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
}
|