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 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
  }