mumuki-puzzle-runner 0.0.1 → 0.4.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 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;