mumuki-puzzle-runner 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
}
|