mumuki-puzzle-runner 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d03d2bd46ce0352ca4c5ad1f58d58fa1ae6a46abc0b817e09ae39d105e7a192d
4
+ data.tar.gz: ccb6886006ec2242006efcbffb9d96035edcc14e6146633bc6155c6a3a8f03c8
5
+ SHA512:
6
+ metadata.gz: 52de2959fbee8ccabdd04d01fc1881b0ec1e58e39c58a9c45dd9d59ac1f4ef8a38f388dcda54419875c869f6f11cad916ab4c82041c66a2f8f5bad7103f22f80
7
+ data.tar.gz: e0287cc443813d18bf5449da8dbf2c7383490ddc09bf0c1d1c179590d7d8d8b38e865ec16a4d41701e9b9b5d66121aaeda3f2f84eda2ea21f4d92de9a903938f
@@ -0,0 +1,11 @@
1
+ class Mumukit::Server::App < Sinatra::Base
2
+ include Mumukit::Server::WithAssets
3
+
4
+ get_local_asset 'headbreaker.js', 'lib/public/vendor/headbreaker.js', 'application/javascript'
5
+
6
+ get_local_asset 'muzzle.js', 'lib/public/js/muzzle.js', 'application/javascript'
7
+ get_local_asset 'muzzle.css', 'lib/public/css/muzzle.css', 'text/css'
8
+
9
+ get_local_asset 'muzzle-editor.js', 'lib/public/js/muzzle-editor.js', 'application/javascript'
10
+ get_local_asset 'muzzle-editor.css', 'lib/public/css/muzzle-editor.css', 'text/css'
11
+ end
@@ -0,0 +1,35 @@
1
+ class PuzzleMetadataHook < Mumukit::Hook
2
+ def metadata
3
+ {
4
+ language: {
5
+ name: 'muzzle',
6
+ version: '1.0.0',
7
+ extension: 'js',
8
+ ace_mode: 'javascript'
9
+ },
10
+ test_framework: {
11
+ name: 'muzzle',
12
+ version: '1.0.0',
13
+ test_extension: 'js'
14
+ },
15
+ layout_assets_urls: {
16
+ js: [
17
+ 'assets/headbreaker.js',
18
+ 'assets/muzzle.js'
19
+ ],
20
+ css: [
21
+ 'assets/muzzle.css'
22
+ ]
23
+ },
24
+ editor_assets_urls: {
25
+ js: [
26
+ 'assets/muzzle-editor.js'
27
+ ],
28
+ css: [
29
+ 'assets/muzzle-editor.css'
30
+ ],
31
+ shows_loading_content: true
32
+ }
33
+ }
34
+ end
35
+ end
@@ -0,0 +1,4 @@
1
+ .mu-kids-state-image img {
2
+ width: 90%;
3
+ padding: 30px;
4
+ }
File without changes
@@ -0,0 +1,65 @@
1
+ // @ts-nocheck
2
+ $(() => {
3
+ // ================
4
+ // Muzzle rendering
5
+ // ================
6
+
7
+ console.log('Registering Muzzle...')
8
+ const $customEditorValue = $('#mu-custom-editor-value');
9
+ Muzzle.previousSolutionJson = $customEditorValue.val();
10
+
11
+ $('#mu-puzzle-custom-editor').append(`
12
+ <div id="muzzle-canvas">
13
+ </div>
14
+ <script>
15
+ ${$('#mu-custom-editor-test').val()}
16
+ </script>
17
+ `);
18
+
19
+ // =================
20
+ // Submission config
21
+ // =================
22
+
23
+ // Required to sync state before submitting
24
+ mumuki.submission.registerContentSyncer(() => {
25
+ Muzzle.prepareSubmission();
26
+ $customEditorValue.val(Muzzle.previousSolutionJson);
27
+ });
28
+
29
+ // Requiered to actually bind Muzzle's submit to
30
+ // 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
+ }
37
+
38
+ // ===========
39
+ // Kids config
40
+ // ===========
41
+
42
+ // Required to make scaler work
43
+ mumuki.kids.registerStateScaler(($state, fullMargin, preferredWidth, preferredHeight) => {
44
+ // nothing
45
+ // no state scaling needed
46
+ });
47
+ mumuki.kids.registerBlocksAreaScaler(($blocks) => {
48
+ // nothing
49
+ // no blocks scaling needed
50
+ });
51
+
52
+ // ==============
53
+ // Assets loading
54
+ // ==============
55
+
56
+ const _onReady = Muzzle.onReady;
57
+ Muzzle.onReady = () => {
58
+ mumuki.assetsLoadedFor('editor');
59
+ // although layout assets
60
+ // are actually loaded before this script, puzzle runner is not aimed
61
+ // to be used without a custom editor
62
+ mumuki.assetsLoadedFor('layout');
63
+ _onReady();
64
+ };
65
+ });
@@ -0,0 +1,320 @@
1
+ /**
2
+ * @typedef {object} PieceConfig
3
+ * @property {string} imagePath
4
+ * @property {string} structure
5
+ */
6
+
7
+ /**
8
+ * @typedef {number[]} Point
9
+ */
10
+
11
+ /**
12
+ * @typedef {object} Solution
13
+ * @property {Point[]} positions list of points
14
+ */
15
+
16
+
17
+
18
+ /**
19
+ * Facade for referencing and creating a global puzzle canvas,
20
+ * handling solutions persistence and submitting them
21
+ */
22
+ class MuzzleCanvas {
23
+
24
+ // =============
25
+ // Global canvas
26
+ // =============
27
+
28
+ constructor(id = 'muzzle-canvas') {
29
+
30
+ /**
31
+ * @private
32
+ * @type {Canvas}
33
+ **/
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
+
45
+ /**
46
+ * The id of the HTML element that will contain the canvas
47
+ * Override it you are going to place in a non-standard way
48
+ */
49
+ this.canvasId = id;
50
+
51
+ /**
52
+ * An optional list of refs that, if set, will be used to validate
53
+ * this puzzle both on client and server side
54
+ *
55
+ * @private
56
+ * @type {Point[]}
57
+ * */
58
+ this._expectedRefs = null;
59
+
60
+ /**
61
+ * Wether expected refs shall be ignored by Muzzle.
62
+ *
63
+ * They will still be evaluated server-side.
64
+ */
65
+ this.expectedRefsAreOnlyDescriptive = false;
66
+
67
+ /**
68
+ * Callback that will be executed
69
+ * when muzzle has fully loaded and rendered its first
70
+ * canvas.
71
+ *
72
+ * It does nothing by default but you can override this
73
+ * property with any code you need the be called here
74
+ */
75
+ this.onReady = () => {};
76
+
77
+ /**
78
+ * The previous solution to the current puzzle in this or a past session,
79
+ * if any
80
+ *
81
+ * @type {string}
82
+ */
83
+ this.previousSolutionJson = null
84
+
85
+ /**
86
+ * Callback to be executed when submitting puzzle.
87
+ *
88
+ * Does nothing by default but you can
89
+ * override it to perform additional actions
90
+ *
91
+ * @param {string} solutionJson the solution, as a JSON
92
+ * @param {boolean} valid whether this puzzle is valid or nor
93
+ */
94
+ this.onSubmit = (solutionJson, valid) => {};
95
+ }
96
+
97
+ /**
98
+ * The currently active canvas, or null if
99
+ * it has not yet initialized
100
+ */
101
+ get canvas() {
102
+ return this._canvas;
103
+ }
104
+
105
+ /**
106
+ * Draws the - previusly built - current canvas.
107
+ *
108
+ * Prefer {@code this.currentCanvas.redraw()} when performing
109
+ * small updates to the pieces.
110
+ */
111
+ draw() {
112
+ this.canvas.draw();
113
+ }
114
+
115
+ // ========
116
+ // Building
117
+ // ========
118
+
119
+ /**
120
+ * @param {Point[]} refs
121
+ */
122
+ expect(refs) {
123
+ this._expectedRefs = refs;
124
+ }
125
+
126
+ /**
127
+ * Creates a basic puzzle canvas with a rectangular shape
128
+ * and a background image, that is automatically
129
+ * submitted when solved
130
+ *
131
+ * @param {number} x the number of horizontal pieces
132
+ * @param {number} y the number of vertical pieces
133
+ * @param {string} imagePath
134
+ * @returns {Promise<Canvas>} the promise of the built canvas
135
+ */
136
+ async basic(x, y, imagePath) {
137
+ /**
138
+ * @todo take all container size
139
+ **/
140
+ const image = await this._loadImage(imagePath);
141
+ /** @type {Canvas} */
142
+ // @ts-ignore
143
+ const canvas = new headbreaker.Canvas(this.canvasId, this._canvasConfig(image));
144
+ canvas.autogenerate({ horizontalPiecesCount: x, verticalPiecesCount: y });
145
+ this._attachValidator(canvas);
146
+ canvas.onValid(() => {
147
+ setTimeout(() => {
148
+ if (canvas.puzzle.isValid) {
149
+ this.submit()
150
+ }
151
+ }, 1500);
152
+ });
153
+ this._configInitialCanvas(canvas);
154
+ return canvas;
155
+ }
156
+
157
+ /**
158
+ * @param {any} configs
159
+ * @returns {Promise<Canvas>} the promise of the built canvas
160
+ */
161
+ multi(configs) {
162
+ return Promise.reject("not implemented yet");
163
+ }
164
+
165
+ /**
166
+ * @param {PieceConfig[]} lefts
167
+ * @param {PieceConfig[]} rights
168
+ * @returns {Promise<Canvas>} the promise of the built canvas
169
+ */
170
+ async match(lefts, rights, leftOdds, rightOdds) {
171
+ /** @type {(Promise<Template>)[]} */
172
+ const templatePromises = [];
173
+ const last = lefts.length - 1;
174
+ 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-`));
177
+ }
178
+ const templates = await Promise.all(templatePromises);
179
+ /** @type {Canvas} */
180
+ const canvas = new headbreaker.Canvas(this.canvasId, this._canvasConfig(null));
181
+ templates.forEach(it => canvas.sketchPiece(it));
182
+ this._attachValidator(canvas);
183
+ this._configInitialCanvas(canvas);
184
+ return canvas;
185
+ }
186
+
187
+ /**
188
+ * @param {Canvas} canvas
189
+ * @returns {Promise<Canvas>} the promise of the built canvas
190
+ */
191
+ custom(canvas) {
192
+ this._configInitialCanvas(canvas);
193
+ return Promise.resolve(canvas);
194
+ }
195
+
196
+ /**
197
+ * @private
198
+ * @param {HTMLImageElement} image
199
+ */
200
+ _canvasConfig(image) {
201
+ return Object.assign({ image }, this.baseConfig);
202
+ }
203
+
204
+ /**
205
+ * @private
206
+ * @param {Canvas} canvas
207
+ */
208
+ _attachValidator(canvas) {
209
+ if (!this.expectedRefsAreOnlyDescriptive && this._expectedRefs) {
210
+ canvas.attachValidator(
211
+ new headbreaker.PuzzleValidator(
212
+ headbreaker.PuzzleValidator.relativeRefs(this._expectedRefs)));
213
+ } else {
214
+ canvas.attachSolvedValidator();
215
+ }
216
+ }
217
+
218
+ /**
219
+ * @private
220
+ * @param {string} path
221
+ * @returns {Promise<HTMLImageElement>}
222
+ */
223
+ _loadImage(path) {
224
+ const image = new Image();
225
+ image.src = path;
226
+ return new Promise((resolve, reject) => image.onload = () => resolve(image));
227
+ }
228
+
229
+ _buildTemplate(config, structure) {
230
+ return this._loadImage(config.imagePath).then((image) =>({
231
+ structure: config.structure || structure,
232
+ metadata: {image}
233
+ }));
234
+ }
235
+
236
+ /**
237
+ * @param {Canvas} canvas
238
+ */
239
+ _configInitialCanvas(canvas) {
240
+ this._canvas = canvas;
241
+ this.ready();
242
+ }
243
+
244
+ ready() {
245
+ this.loadPreviousSolution();
246
+ this.draw();
247
+ this.onReady();
248
+ }
249
+
250
+ // ===========
251
+ // Persistence
252
+ // ===========
253
+
254
+ /**
255
+ * The state of the current puzzle
256
+ * expressed as a Solution object
257
+ *
258
+ * @returns {Solution}
259
+ */
260
+ get solution() {
261
+ return { positions: this.canvas.puzzle.points }
262
+ }
263
+
264
+ /**
265
+ * @param {Solution} solution
266
+ */
267
+ loadSolution(solution) {
268
+ this.canvas.puzzle.relocateTo(solution.positions);
269
+ }
270
+
271
+ /**
272
+ * Loads the current canvas with the
273
+ */
274
+ loadPreviousSolution() {
275
+ if (this.previousSolutionJson) {
276
+ try {
277
+ this.loadSolution(JSON.parse(this.previousSolutionJson));
278
+ } catch (e) {
279
+ console.warn("Ignoring unparseabe editor value");
280
+ }
281
+ } else {
282
+ this.canvas.shuffle(0.8);
283
+ }
284
+ }
285
+
286
+ prepareSubmission() {
287
+ this.canvas.puzzle.validate();
288
+ this.previousSolutionJson = this._solutionJson;
289
+ }
290
+
291
+ // ==========
292
+ // Submitting
293
+ // ==========
294
+
295
+ /**
296
+ * Submits the puzzle to the bridge,
297
+ * validating it if necessary
298
+ */
299
+ submit() {
300
+ this.prepareSubmission();
301
+ this.onSubmit(this._solutionJson, this.canvas.puzzle.valid);
302
+ }
303
+
304
+ /**
305
+ * The current solution, expressed as a JSON string
306
+ */
307
+ get _solutionJson() {
308
+ return JSON.stringify(this.solution);
309
+ }
310
+ }
311
+
312
+
313
+ const Muzzle = new MuzzleCanvas();
314
+
315
+ Muzzle.aux = {};
316
+ Muzzle.another = (id) => {
317
+ const muzzle = new MuzzleCanvas(id);
318
+ Muzzle.aux[id] = muzzle
319
+ return muzzle;
320
+ }