mumuki-puzzle-runner 0.0.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d03d2bd46ce0352ca4c5ad1f58d58fa1ae6a46abc0b817e09ae39d105e7a192d
4
- data.tar.gz: ccb6886006ec2242006efcbffb9d96035edcc14e6146633bc6155c6a3a8f03c8
3
+ metadata.gz: 5da4e03efe5edbeb4e6eadfaf79f5d37e4f5bea50127a26e91c5f6d2cedb2438
4
+ data.tar.gz: f0eeb8f873165d98ecfcc04854aa4fb781d4753fc4350bcfed90fd24c184744b
5
5
  SHA512:
6
- metadata.gz: 52de2959fbee8ccabdd04d01fc1881b0ec1e58e39c58a9c45dd9d59ac1f4ef8a38f388dcda54419875c869f6f11cad916ab4c82041c66a2f8f5bad7103f22f80
7
- data.tar.gz: e0287cc443813d18bf5449da8dbf2c7383490ddc09bf0c1d1c179590d7d8d8b38e865ec16a4d41701e9b9b5d66121aaeda3f2f84eda2ea21f4d92de9a903938f
6
+ metadata.gz: c98b0e5f9fe633c8fb0587b3c948ce0a6d6d79b80619befc28bc4c583578be596b1d8d2d9f61fddbb3241a320424b602af417fca96827c632a9537342d2a62e9
7
+ data.tar.gz: f40b2c76b102651f25ca76eda5b82d24f6cb037e92efcbd68299d634ec8a4f8a22477f4639893b780f6e8f44e790761189c43c12a925a5421f8e33416b3273af
@@ -3,14 +3,15 @@ class PuzzleMetadataHook < Mumukit::Hook
3
3
  {
4
4
  language: {
5
5
  name: 'muzzle',
6
- version: '1.0.0',
6
+ version: PuzzleVersionHook::VERSION,
7
7
  extension: 'js',
8
8
  ace_mode: 'javascript'
9
9
  },
10
10
  test_framework: {
11
11
  name: 'muzzle',
12
- version: '1.0.0',
13
- test_extension: 'js'
12
+ version: PuzzleVersionHook::VERSION,
13
+ test_extension: 'js',
14
+ template: "// see more examples at https://github.com/mumuki/mumuki-puzzle-runner\nMuzzle.basic(3, 2, 'https://flbulgarelli.github.io/headbreaker/static/berni.jpg');"
14
15
  },
15
16
  layout_assets_urls: {
16
17
  js: [
@@ -1,4 +1,13 @@
1
1
  .mu-kids-state-image img {
2
- width: 90%;
2
+ height: 100%;
3
+ width: auto;
3
4
  padding: 30px;
4
5
  }
6
+
7
+ .mu-kids-state.mu-state-initial {
8
+ width: 100%;
9
+ }
10
+
11
+ .mu-kids-exercise-workspace.muzzle-simple .mu-kids-submit-button {
12
+ display: none;
13
+ }
@@ -1,12 +1,18 @@
1
1
  // @ts-nocheck
2
2
  $(() => {
3
+ function register(event, callback) {
4
+ const _event = Muzzle[event];
5
+ Muzzle[event] = (...args) => {
6
+ callback(...args);
7
+ _event(...args);
8
+ }
9
+ }
10
+
3
11
  // ================
4
12
  // Muzzle rendering
5
13
  // ================
6
14
 
7
- console.log('Registering Muzzle...')
8
- const $customEditorValue = $('#mu-custom-editor-value');
9
- Muzzle.previousSolutionJson = $customEditorValue.val();
15
+ Muzzle.previousSolutionContent = $('#mu-custom-editor-value').val();
10
16
 
11
17
  $('#mu-puzzle-custom-editor').append(`
12
18
  <div id="muzzle-canvas">
@@ -20,46 +26,55 @@ $(() => {
20
26
  // Submission config
21
27
  // =================
22
28
 
29
+
23
30
  // Required to sync state before submitting
24
- mumuki.submission.registerContentSyncer(() => {
25
- Muzzle.prepareSubmission();
26
- $customEditorValue.val(Muzzle.previousSolutionJson);
31
+ mumuki.CustomEditor.addSource({
32
+ getContent() { return { name: "solution[content]", value: Muzzle.solutionContent }; }
27
33
  });
34
+ mumuki.CustomEditor.addSource({
35
+ getContent() { return { name: "client_result[status]", value: Muzzle.clientResultStatus }; }
36
+ })
28
37
 
29
38
  // Requiered to actually bind Muzzle's submit to
30
39
  // mumuki's solution processing
31
- const _onSubmit = Muzzle.onSubmit;
32
- Muzzle.onSubmit = (solutionJson, valid) => {
33
- console.log('submitting muzzle...')
34
- mumuki.submission.processSolution({solution: {content: solutionJson}});
35
- _onSubmit(solutionJson, valid);
36
- }
40
+ register('onSubmit', (submission) => {
41
+ mumuki.submission.processSolution(submission);
42
+ });
37
43
 
38
44
  // ===========
39
45
  // Kids config
40
46
  // ===========
41
47
 
42
- // Required to make scaler work
43
48
  mumuki.kids.registerStateScaler(($state, fullMargin, preferredWidth, preferredHeight) => {
44
- // nothing
45
- // no state scaling needed
49
+ // no manul image scalling. Leave it to css
46
50
  });
51
+
47
52
  mumuki.kids.registerBlocksAreaScaler(($blocks) => {
48
- // nothing
49
- // no blocks scaling needed
53
+ const maxHeight = $('.mu-kids-exercise').height() - $('.mu-kids-exercise-description').height();
54
+ Muzzle.scale($blocks.width(), Math.min($blocks.height(), maxHeight));
55
+ });
56
+
57
+ Muzzle.manualScale = true;
58
+
59
+ // ====================
60
+ // Submit button hiding
61
+ // ====================
62
+
63
+ register('onReady', () => {
64
+ if (Muzzle.simple) {
65
+ $('.mu-kids-exercise-workspace').addClass('muzzle-simple');
66
+ }
50
67
  });
51
68
 
52
69
  // ==============
53
70
  // Assets loading
54
71
  // ==============
55
72
 
56
- const _onReady = Muzzle.onReady;
57
- Muzzle.onReady = () => {
73
+ register('onReady', () => {
58
74
  mumuki.assetsLoadedFor('editor');
59
75
  // although layout assets
60
76
  // are actually loaded before this script, puzzle runner is not aimed
61
77
  // to be used without a custom editor
62
78
  mumuki.assetsLoadedFor('layout');
63
- _onReady();
64
- };
79
+ });
65
80
  });
@@ -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,72 @@ 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 = 600;
68
+
69
+ /**
70
+ * Height of canvas
71
+ *
72
+ * @type {number}
73
+ */
74
+ this.canvasHeight = 600;
75
+
76
+ /**
77
+ * Wether canvas shoud **not** be resized.
78
+ * Default is `false`
79
+ *
80
+ * @type {boolean}
81
+ */
82
+ this.fixedDimensions = false;
83
+
84
+ /**
85
+ * Size of fill. Set null for perfect-match
86
+ *
87
+ * @type {number}
88
+ */
89
+ this.borderFill = null;
90
+
91
+ /**
92
+ * Canvas line width
93
+ *
94
+ * @type {number}
95
+ */
96
+ this.strokeWidth = 1.5;
97
+
98
+ /**
99
+ * Piece size
100
+ *
101
+ * @type {number}
102
+ */
103
+ this.pieceSize = 100;
104
+
105
+ /**
106
+ * The x:y aspect ratio of the piece. Set null for automatic
107
+ * aspectRatio
108
+ *
109
+ * @type {number}
110
+ */
111
+ this.aspectRatio = null;
112
+
113
+ /**
114
+ * If the images should be adjusted vertically instead of horizontally
115
+ * to puzzle dimensions. `false` by default
116
+ *
117
+ * @type {boolean}
118
+ */
119
+ this.fitImagesVertically = false;
120
+
121
+ this.manualScale = false;
122
+
67
123
  /**
68
124
  * Callback that will be executed
69
125
  * when muzzle has fully loaded and rendered its first
@@ -75,12 +131,22 @@ class MuzzleCanvas {
75
131
  this.onReady = () => {};
76
132
 
77
133
  /**
78
- * The previous solution to the current puzzle in this or a past session,
134
+ * The previous solution to the current puzzle in a past session,
79
135
  * if any
80
136
  *
81
137
  * @type {string}
82
138
  */
83
- this.previousSolutionJson = null
139
+ this.previousSolutionContent = null;
140
+
141
+ /**
142
+ * Whether the current puzzle can be solved in very few tries.
143
+ *
144
+ * Set null for automatic configuration of this property. Basic puzzles will be considered
145
+ * basic and match puzzles will be considered non-basic.
146
+ *
147
+ * @type {boolean}
148
+ */
149
+ this.simple = null;
84
150
 
85
151
  /**
86
152
  * Callback to be executed when submitting puzzle.
@@ -88,15 +154,49 @@ class MuzzleCanvas {
88
154
  * Does nothing by default but you can
89
155
  * override it to perform additional actions
90
156
  *
91
- * @param {string} solutionJson the solution, as a JSON
92
- * @param {boolean} valid whether this puzzle is valid or nor
157
+ * @param {{solution: {content: string}, client_result: {status: "passed" | "failed"}}} submission
158
+ */
159
+ this.onSubmit = (submission) => {};
160
+
161
+ /**
162
+ * Callback that will be executed
163
+ * when muzzle's puzzle becomes valid
164
+ *
165
+ * It does nothing by default but you can override this
166
+ * property with any code you need the be called here
93
167
  */
94
- this.onSubmit = (solutionJson, valid) => {};
168
+ this.onValid = () => {};
169
+ }
170
+
171
+ /**
172
+ */
173
+ get baseConfig() {
174
+ const aspectRatio = this.aspectRatio || 1;
175
+ const pieceSize = headbreaker.vector(this.pieceSize / aspectRatio, this.pieceSize);
176
+ return {
177
+ width: this.canvasWidth,
178
+ height: this.canvasHeight,
179
+ pieceSize: pieceSize,
180
+ proximity: Math.min(pieceSize.x, pieceSize.y) / 5,
181
+ borderFill: this.borderFill === null ? headbreaker.Vector.divide(pieceSize, 10) : this.borderFill,
182
+ strokeWidth: this.strokeWidth,
183
+ lineSoftness: 0.18
184
+ };
185
+ }
186
+
187
+ /**
188
+ * @type {Axis}
189
+ */
190
+ get imageAdjustmentAxis() {
191
+ console.log(this.fitImagesVertically)
192
+ return this.fitImagesVertically ? headbreaker.Vertical : headbreaker.Horizontal;
95
193
  }
96
194
 
97
195
  /**
98
196
  * The currently active canvas, or null if
99
197
  * it has not yet initialized
198
+ *
199
+ * @returns {Canvas}
100
200
  */
101
201
  get canvas() {
102
202
  return this._canvas;
@@ -134,53 +234,93 @@ class MuzzleCanvas {
134
234
  * @returns {Promise<Canvas>} the promise of the built canvas
135
235
  */
136
236
  async basic(x, y, imagePath) {
237
+ if (!this.aspectRatio) {
238
+ this.aspectRatio = x / y;
239
+ }
240
+
241
+ if (this.simple === null) {
242
+ this.simple = true;
243
+ }
244
+
137
245
  /**
138
246
  * @todo take all container size
139
247
  **/
140
248
  const image = await this._loadImage(imagePath);
141
249
  /** @type {Canvas} */
142
250
  // @ts-ignore
143
- const canvas = new headbreaker.Canvas(this.canvasId, this._canvasConfig(image));
251
+ const canvas = this._createCanvas({ image: image });
252
+ canvas.adjustImagesToPuzzle(this.imageAdjustmentAxis);
144
253
  canvas.autogenerate({ horizontalPiecesCount: x, verticalPiecesCount: y });
145
- this._attachValidator(canvas);
254
+ this._attachBasicValidator(canvas);
255
+ canvas.shuffleGrid(0.8);
256
+ this._configCanvas(canvas);
146
257
  canvas.onValid(() => {
147
258
  setTimeout(() => {
148
- if (canvas.puzzle.isValid) {
149
- this.submit()
259
+ if (canvas.valid) {
260
+ this.submit();
150
261
  }
151
262
  }, 1500);
152
263
  });
153
- this._configInitialCanvas(canvas);
154
264
  return canvas;
155
265
  }
156
266
 
157
267
  /**
158
- * @param {any} configs
268
+ * @param {number} x
269
+ * @param {number} y
270
+ * @param {string[]} [imagePaths]
159
271
  * @returns {Promise<Canvas>} the promise of the built canvas
160
272
  */
161
- multi(configs) {
162
- return Promise.reject("not implemented yet");
273
+ async multi(x, y, imagePaths) {
274
+ const count = imagePaths.length;
275
+ const images = await Promise.all(imagePaths.map(imagePath => this._loadImage(imagePath)));
276
+
277
+ const canvas = this._createCanvas();
278
+ canvas.autogenerate({ horizontalPiecesCount: x, verticalPiecesCount: y * count });
279
+
280
+ // todo validate
281
+ // todo set images
282
+
283
+ this._configCanvas(canvas);
284
+ return canvas;
163
285
  }
164
286
 
165
287
  /**
166
- * @param {PieceConfig[]} lefts
167
- * @param {PieceConfig[]} rights
288
+ * Craates a match puzzle, where left pieces are matched against right pieces,
289
+ * with optional odd left and right pieces that don't match
290
+ *
291
+ * @param {string[]} leftUrls
292
+ * @param {string[]} rightUrls must be of the same size of lefts
293
+ * @param {string[]} leftOddUrls
294
+ * @param {string[]} rightOddUrls
168
295
  * @returns {Promise<Canvas>} the promise of the built canvas
169
296
  */
170
- async match(lefts, rights, leftOdds, rightOdds) {
171
- /** @type {(Promise<Template>)[]} */
297
+ async match(leftUrls, rightUrls, leftOddUrls = [], rightOddUrls = []) {
298
+ /** @private @type {(Promise<Template>)[]} */
172
299
  const templatePromises = [];
173
- const last = lefts.length - 1;
300
+ const pushTemplate = (config, options) =>
301
+ templatePromises.push(this._createMatchTemplate(config, options));
302
+
303
+ const last = leftUrls.length - 1;
174
304
  for (let i = 0; i <= last; i++) {
175
- templatePromises.push(this._buildTemplate(lefts[i], `T-N-`));
176
- templatePromises.push(this._buildTemplate(rights[i], `N-S-`));
305
+ const leftId = `l${i}`;
306
+ const rightId = `r${i}`;
307
+
308
+ pushTemplate(leftUrls[i], {id: leftId, targetPosition: { x: this.pieceSize, y: this.pieceSize * (i + 1) }, left: true, rightTargetId: rightId});
309
+ pushTemplate(rightUrls[i], {id: rightId, targetPosition: { x: 2 * this.pieceSize, y: this.pieceSize * (i + 1) }});
177
310
  }
311
+
312
+ leftOddUrls.forEach((it, i) => pushTemplate(it, {id: `lo${i}`, left: true, odd: true, targetPosition: { x: this.pieceSize, y: this.pieceSize * (i + leftUrls.length) }, }));
313
+ rightOddUrls.forEach((it, i) => pushTemplate(it, {id: `ro${i}`, odd: true, targetPosition: { x: 2 * this.pieceSize, y: this.pieceSize * (i + rightUrls.length) },}));
314
+
315
+ // + Math.max(leftOddUrls.length, rightOddUrls.length)
178
316
  const templates = await Promise.all(templatePromises);
179
317
  /** @type {Canvas} */
180
- const canvas = new headbreaker.Canvas(this.canvasId, this._canvasConfig(null));
318
+ const canvas = this._createCanvas({ maxPiecesCount: {x: 2, y: leftUrls.length} });
319
+ canvas.adjustImagesToPiece(this.imageAdjustmentAxis);
181
320
  templates.forEach(it => canvas.sketchPiece(it));
182
- this._attachValidator(canvas);
183
- this._configInitialCanvas(canvas);
321
+ canvas.shuffleColumns(0.8);
322
+ this._attachMatchValidator(canvas);
323
+ this._configCanvas(canvas);
184
324
  return canvas;
185
325
  }
186
326
 
@@ -189,32 +329,43 @@ class MuzzleCanvas {
189
329
  * @returns {Promise<Canvas>} the promise of the built canvas
190
330
  */
191
331
  custom(canvas) {
192
- this._configInitialCanvas(canvas);
332
+ this._configCanvas(canvas);
193
333
  return Promise.resolve(canvas);
194
334
  }
195
335
 
196
336
  /**
197
337
  * @private
198
- * @param {HTMLImageElement} image
338
+ * @param {any} config
339
+ * @return {Canvas}
199
340
  */
200
- _canvasConfig(image) {
201
- return Object.assign({ image }, this.baseConfig);
341
+ _createCanvas(config = {}) {
342
+ return new headbreaker.Canvas(this.canvasId, Object.assign(config, this.baseConfig));
202
343
  }
203
344
 
204
345
  /**
205
346
  * @private
206
347
  * @param {Canvas} canvas
207
348
  */
208
- _attachValidator(canvas) {
349
+ _attachBasicValidator(canvas) {
209
350
  if (!this.expectedRefsAreOnlyDescriptive && this._expectedRefs) {
210
- canvas.attachValidator(
211
- new headbreaker.PuzzleValidator(
212
- headbreaker.PuzzleValidator.relativeRefs(this._expectedRefs)));
351
+ canvas.attachRelativeRefsValidator(this._expectedRefs);
213
352
  } else {
214
353
  canvas.attachSolvedValidator();
215
354
  }
216
355
  }
217
356
 
357
+ /**
358
+ * @private
359
+ * @param {Canvas} canvas
360
+ */
361
+ _attachMatchValidator(canvas) {
362
+ canvas.attachValidator(new headbreaker.PuzzleValidator(
363
+ puzzle => puzzle.pieces
364
+ .filter(it => !it.metadata.odd && it.metadata.left)
365
+ .every(it => it.rightConnection && it.rightConnection.id === it.metadata.rightTargetId)
366
+ ));
367
+ }
368
+
218
369
  /**
219
370
  * @private
220
371
  * @param {string} path
@@ -226,23 +377,95 @@ class MuzzleCanvas {
226
377
  return new Promise((resolve, reject) => image.onload = () => resolve(image));
227
378
  }
228
379
 
229
- _buildTemplate(config, structure) {
230
- return this._loadImage(config.imagePath).then((image) =>({
231
- structure: config.structure || structure,
232
- metadata: {image}
233
- }));
380
+ /**
381
+ * @private
382
+ * @param {string} imagePath
383
+ * @param {object} options
384
+ * @returns {Promise<object>}
385
+ */
386
+ _createMatchTemplate(imagePath, {id, left = false, targetPosition = null, rightTargetId = null, odd = false}) {
387
+ const structure = left ? 'T-N-' : `N-S-`;
388
+
389
+ return this._loadImage(imagePath).then((image) => {
390
+ return {
391
+ structure,
392
+ metadata: { id, left, odd, rightTargetId, image, targetPosition }
393
+ }
394
+ });
234
395
  }
235
396
 
236
397
  /**
398
+ * @private
237
399
  * @param {Canvas} canvas
238
400
  */
239
- _configInitialCanvas(canvas) {
401
+ _configCanvas(canvas) {
240
402
  this._canvas = canvas;
403
+ this._canvas.onValid(() => {
404
+ setTimeout(() => this.onValid(), 0);
405
+ });
406
+ this._setUpScaler();
241
407
  this.ready();
242
408
  }
243
409
 
410
+ _setUpScaler() {
411
+ if (this.manualScale) return;
412
+
413
+ ['resize', 'load'].forEach((event) => {
414
+ window.addEventListener(event, () => {
415
+ var container = document.getElementById(this.canvasId);
416
+ this.scale(container.offsetWidth, container.scrollHeight);
417
+ });
418
+ });
419
+ }
420
+
421
+ scale(width, height) {
422
+ if (this.fixedDimensions || !this.canvas) return;
423
+ const factor = this.optimalScaleFactor(width, height);
424
+ this.canvas.resize(width, height);
425
+ this.canvas.scale(factor);
426
+ this.canvas.redraw();
427
+ this.focus();
428
+ }
429
+
430
+ focus() {
431
+ const stage = this.canvas['__konvaLayer__'].getStage();
432
+
433
+ const area = headbreaker.Vector.divide(headbreaker.vector(stage.width(), stage.height()), stage.scaleX());
434
+ const realDiameter = (() => {
435
+ const [xs, ys] = this.coordinates;
436
+
437
+ const minX = Math.min(...xs);
438
+ const minY = Math.min(...ys);
439
+
440
+ const maxX = Math.max(...xs);
441
+ const maxY = Math.max(...ys);
442
+
443
+ return headbreaker.Vector.plus(headbreaker.vector(maxX - minX, maxY - minY), this.canvas.puzzle.pieceDiameter);
444
+ })();
445
+ const diff = headbreaker.Vector.minus(area, realDiameter);
446
+ const semi = headbreaker.Vector.divide(diff, -2);
447
+
448
+ stage.setOffset(semi);
449
+ stage.draw();
450
+ }
451
+
452
+ get coordinates() {
453
+ const points = this.canvas.puzzle.points;
454
+ return [points.map(([x, _y]) => x), points.map(([_x, y]) => y)];
455
+ }
456
+
457
+ optimalScaleFactor(width, height) {
458
+ const factors = headbreaker.Vector.divide(headbreaker.vector(width, height), this.canvas.puzzleDiameter);
459
+ return Math.min(factors.x, factors.y) / 1.75;
460
+ }
461
+
462
+ /**
463
+ * Mark Muzzle as ready, loading previous solution
464
+ * and drawing the canvas
465
+ */
244
466
  ready() {
245
467
  this.loadPreviousSolution();
468
+ this.resetCoordinates();
246
469
  this.draw();
247
470
  this.onReady();
248
471
  }
@@ -262,30 +485,34 @@ class MuzzleCanvas {
262
485
  }
263
486
 
264
487
  /**
488
+ * Loads - but does not draw - a solution into the canvas.
489
+ *
265
490
  * @param {Solution} solution
266
491
  */
267
492
  loadSolution(solution) {
268
493
  this.canvas.puzzle.relocateTo(solution.positions);
494
+ this.canvas.puzzle.autoconnect();
269
495
  }
270
496
 
271
497
  /**
272
- * Loads the current canvas with the
498
+ * Loads - but does not draw - the current canvas with the previous solution, if available.
499
+ *
273
500
  */
274
501
  loadPreviousSolution() {
275
- if (this.previousSolutionJson) {
502
+ if (this.previousSolutionContent) {
276
503
  try {
277
- this.loadSolution(JSON.parse(this.previousSolutionJson));
504
+ this.loadSolution(JSON.parse(this.previousSolutionContent));
278
505
  } catch (e) {
279
506
  console.warn("Ignoring unparseabe editor value");
280
507
  }
281
- } else {
282
- this.canvas.shuffle(0.8);
283
508
  }
284
509
  }
285
510
 
286
- prepareSubmission() {
287
- this.canvas.puzzle.validate();
288
- this.previousSolutionJson = this._solutionJson;
511
+ resetCoordinates() {
512
+ const [xs, ys] = this.coordinates;
513
+ const minX = Math.min(...xs);
514
+ const minY = Math.min(...ys);
515
+ this.canvas.puzzle.translate(-minX, -minY);
289
516
  }
290
517
 
291
518
  // ==========
@@ -297,24 +524,56 @@ class MuzzleCanvas {
297
524
  * validating it if necessary
298
525
  */
299
526
  submit() {
300
- this.prepareSubmission();
301
- this.onSubmit(this._solutionJson, this.canvas.puzzle.valid);
527
+ this.onSubmit(this._prepareSubmission());
302
528
  }
303
529
 
304
530
  /**
305
531
  * The current solution, expressed as a JSON string
306
532
  */
307
- get _solutionJson() {
533
+ get solutionContent() {
308
534
  return JSON.stringify(this.solution);
309
535
  }
310
- }
311
536
 
537
+ /**
538
+ * The solution validation status
539
+ *
540
+ * @returns {"passed" | "failed"}
541
+ */
542
+ get clientResultStatus() {
543
+ return this.canvas.valid ? 'passed' : 'failed';
544
+ }
312
545
 
313
- const Muzzle = new MuzzleCanvas();
546
+ _prepareSubmission() {
547
+ return {
548
+ solution: {
549
+ content: this.solutionContent
550
+ },
551
+ client_result: {
552
+ status: this.clientResultStatus
553
+ }
554
+ };
555
+ }
314
556
 
315
- Muzzle.aux = {};
316
- Muzzle.another = (id) => {
317
- const muzzle = new MuzzleCanvas(id);
318
- Muzzle.aux[id] = muzzle
319
- return muzzle;
320
557
  }
558
+
559
+ const Muzzle = new class extends MuzzleCanvas {
560
+ constructor() {
561
+ super();
562
+ this.aux = {};
563
+ }
564
+
565
+ /**
566
+ * Creates a suplementary canvas at the element
567
+ * of the given id
568
+ *
569
+ * @param {string} id
570
+ * @returns {MuzzleCanvas}
571
+ */
572
+ another(id) {
573
+ const muzzle = new MuzzleCanvas(id);
574
+ Muzzle.aux[id] = muzzle
575
+ return muzzle;
576
+ }
577
+ }
578
+
579
+ window['Muzzle'] = Muzzle;