mumuki-puzzle-runner 0.0.1
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 +7 -0
- data/lib/assets_server.rb +11 -0
- data/lib/metadata_hook.rb +35 -0
- data/lib/public/css/muzzle-editor.css +4 -0
- data/lib/public/css/muzzle.css +0 -0
- data/lib/public/js/muzzle-editor.js +65 -0
- data/lib/public/js/muzzle.js +320 -0
- data/lib/public/vendor/headbreaker.d.ts +802 -0
- data/lib/public/vendor/headbreaker.js +2 -0
- data/lib/public/vendor/headbreaker.js.map +1 -0
- data/lib/puzzle_runner.rb +10 -0
- data/lib/test_hook.rb +28 -0
- data/lib/version_hook.rb +3 -0
- metadata +138 -0
checksums.yaml
ADDED
@@ -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
|
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
|
+
}
|