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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d03d2bd46ce0352ca4c5ad1f58d58fa1ae6a46abc0b817e09ae39d105e7a192d
4
- data.tar.gz: ccb6886006ec2242006efcbffb9d96035edcc14e6146633bc6155c6a3a8f03c8
3
+ metadata.gz: c06f3a9c0ab060ce1746fbd08687e44bdd3edc9d2bd7bc9f27991cfa56d34e96
4
+ data.tar.gz: 3fd982c69ed60580166016ad4824be96a67b5480d238311e347c7588e86ededd
5
5
  SHA512:
6
- metadata.gz: 52de2959fbee8ccabdd04d01fc1881b0ec1e58e39c58a9c45dd9d59ac1f4ef8a38f388dcda54419875c869f6f11cad916ab4c82041c66a2f8f5bad7103f22f80
7
- data.tar.gz: e0287cc443813d18bf5449da8dbf2c7383490ddc09bf0c1d1c179590d7d8d8b38e865ec16a4d41701e9b9b5d66121aaeda3f2f84eda2ea21f4d92de9a903938f
6
+ metadata.gz: 594855995d69d145ceb3294c3bfc571d681a18e5983e9130884bde9508feea77519764066ceb7fa6b843923dfebc728bfe8a5cf3f08058c9aacc5362f4e784c5
7
+ data.tar.gz: 6f2d79e5203433e39a8d0bcea6ca80442f8eac32e3eb3d72f04fbadd82490578836444f0c57ad93163d74c9bbbcbcec268b04c97a77d33a2152d191b73cdfba8
@@ -4,9 +4,7 @@ $(() => {
4
4
  // Muzzle rendering
5
5
  // ================
6
6
 
7
- console.log('Registering Muzzle...')
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.submission.registerContentSyncer(() => {
25
- Muzzle.prepareSubmission();
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 = (solutionJson, valid) => {
33
- console.log('submitting muzzle...')
34
- mumuki.submission.processSolution({solution: {content: solutionJson}});
35
- _onSubmit(solutionJson, valid);
33
+ Muzzle.onSubmit = (submission) => {
34
+ mumuki.submission.processSolution(submission);
35
+ _onSubmit(submission);
36
36
  }
37
37
 
38
38
  // ===========
@@ -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 this or a past session,
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.previousSolutionJson = null
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} solutionJson the solution, as a JSON
92
- * @param {boolean} valid whether this puzzle is valid or nor
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.onSubmit = (solutionJson, valid) => {};
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 = new headbreaker.Canvas(this.canvasId, this._canvasConfig(image));
197
+ const canvas = this._createCanvas(image);
144
198
  canvas.autogenerate({ horizontalPiecesCount: x, verticalPiecesCount: y });
145
- this._attachValidator(canvas);
199
+ this._attachBasicValidator(canvas);
200
+ this._configCanvas(canvas);
146
201
  canvas.onValid(() => {
147
202
  setTimeout(() => {
148
- if (canvas.puzzle.isValid) {
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 {any} configs
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(configs) {
162
- return Promise.reject("not implemented yet");
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
- * @param {PieceConfig[]} lefts
167
- * @param {PieceConfig[]} rights
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(lefts, rights, leftOdds, rightOdds) {
171
- /** @type {(Promise<Template>)[]} */
241
+ async match(leftUrls, rightUrls, leftOddUrls = [], rightOddUrls = []) {
242
+ /** @private @type {(Promise<Template>)[]} */
172
243
  const templatePromises = [];
173
- const last = lefts.length - 1;
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
- templatePromises.push(this._buildTemplate(lefts[i], `T-N-`));
176
- templatePromises.push(this._buildTemplate(rights[i], `N-S-`));
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 = new headbreaker.Canvas(this.canvasId, this._canvasConfig(null));
261
+ const canvas = this._createCanvas();
181
262
  templates.forEach(it => canvas.sketchPiece(it));
182
- this._attachValidator(canvas);
183
- this._configInitialCanvas(canvas);
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._configInitialCanvas(canvas);
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
- _attachValidator(canvas) {
298
+ _attachBasicValidator(canvas) {
209
299
  if (!this.expectedRefsAreOnlyDescriptive && this._expectedRefs) {
210
- canvas.attachValidator(
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
- _buildTemplate(config, structure) {
230
- return this._loadImage(config.imagePath).then((image) =>({
231
- structure: config.structure || structure,
232
- metadata: {image}
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
- _configInitialCanvas(canvas) {
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.previousSolutionJson) {
416
+ if (this.previousSolutionContent) {
276
417
  try {
277
- this.loadSolution(JSON.parse(this.previousSolutionJson));
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.prepareSubmission();
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 _solutionJson() {
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
- const Muzzle = new MuzzleCanvas();
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.aux = {};
316
- Muzzle.another = (id) => {
317
- const muzzle = new MuzzleCanvas(id);
318
- Muzzle.aux[id] = muzzle
319
- return muzzle;
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
  }