@windward/games 0.0.2 → 0.0.4

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.
Files changed (27) hide show
  1. package/components/content/blocks/crosswordPuzzle/Crossword.ts +475 -0
  2. package/components/content/blocks/crosswordPuzzle/CrosswordElements.ts +36 -0
  3. package/components/content/blocks/crosswordPuzzle/CrosswordPuzzle.vue +542 -0
  4. package/components/content/blocks/dragDrop/BucketGame.vue +4 -4
  5. package/components/content/blocks/dragDrop/SortingGame.vue +1 -1
  6. package/components/content/blocks/flashcards/FlashcardSlides.vue +2 -2
  7. package/components/content/blocks/matchingGame/MatchingGame.vue +2 -2
  8. package/components/content/blocks/multipleChoice/MultipleChoice.vue +1 -1
  9. package/components/content/blocks/multipleChoice/QuestionDialog.vue +2 -2
  10. package/components/content/blocks/quizshowGame/QuizShow.vue +2 -2
  11. package/components/content/blocks/slideshow/SlideShow.vue +2 -2
  12. package/components/content/blocks/wordJumble/WordJumble.vue +24 -12
  13. package/components/settings/CrosswordPuzzleSettingsManager.vue +274 -0
  14. package/components/settings/WordJumbleSettingsManager.vue +4 -1
  15. package/i18n/en-US/components/content/blocks/crossword.ts +7 -0
  16. package/i18n/en-US/components/content/blocks/index.ts +2 -0
  17. package/i18n/en-US/components/settings/crossword.ts +6 -0
  18. package/i18n/en-US/components/settings/index.ts +2 -0
  19. package/i18n/en-US/components/settings/word_jumble.ts +1 -1
  20. package/i18n/en-US/shared/content_blocks.ts +1 -0
  21. package/i18n/en-US/shared/settings.ts +1 -0
  22. package/package.json +1 -1
  23. package/plugin.js +22 -1
  24. package/test/blocks/crossword/CrosswordPuzzle.spec.js +49 -0
  25. package/test/settings/BucketGameManager.spec.js +1 -1
  26. package/test/settings/CrosswordPuzzleManager.spec.js +103 -0
  27. package/test/settings/WordJumbleManager.spec.js +2 -2
@@ -0,0 +1,475 @@
1
+ import {
2
+ CrosswordCell,
3
+ CrosswordCellNode,
4
+ WordElement,
5
+ } from './CrosswordElements'
6
+ class Crossword {
7
+ public GRID_COLS: any = 50
8
+ public GRID_ROWS: any = 50
9
+ // This is an index of the positions of the char in the crossword (so we know where we can potentially place words)
10
+ // example {"a" : [{'row' : 10, 'col' : 5}, {'row' : 62, 'col' :17}], {'row' : 54, 'col' : 12}], "b" : [{'row' : 3, 'col' : 13}]}
11
+ // where the two item arrays are the row and column of where the letter occurs
12
+ public char_index!: {}
13
+ // these words are the words that can't be placed on the crossword
14
+ public bad_words!: any
15
+ public words_in!: any
16
+ public clues_in!: any
17
+ public grid: any = []
18
+ public word_elements: any = []
19
+ constructor(words_in, clues_in) {
20
+ this.words_in = words_in
21
+ this.clues_in = clues_in
22
+ // constructor
23
+ if (this.words_in.length < 2)
24
+ throw 'A crossword must have at least 2 words'
25
+ if (this.words_in.length != clues_in.length)
26
+ throw 'The number of words must equal the number of clues'
27
+ // build the grid;
28
+ this.grid = new Array(this.GRID_ROWS)
29
+ for (var i = 0; i < this.GRID_ROWS; i++) {
30
+ this.grid[i] = new Array(this.GRID_COLS)
31
+ }
32
+
33
+ // build the element list (need to keep track of indexes in the originial input arrays)
34
+ for (let i = 0; i < words_in.length; i++) {
35
+ this.word_elements.push(new WordElement(words_in[i], i))
36
+ }
37
+
38
+ // I got this sorting idea from http://stackoverflow.com/questions/943113/algorithm-to-generate-a-crossword/1021800#1021800
39
+ // seems to work well
40
+ this.word_elements.sort(function (a, b) {
41
+ return b.word.length - a.word.length
42
+ })
43
+ }
44
+
45
+ // returns the crossword grid that has the ratio closest to 1 or null if it can't build one
46
+ public getSquareGrid(max_tries) {
47
+ let best_grid: any = null
48
+ var best_ratio = 0
49
+ for (var i = 0; i < max_tries; i++) {
50
+ var a_grid = this.getGrid(1)
51
+ if (a_grid == null) {
52
+ continue
53
+ }
54
+ var ratio =
55
+ (Math.min(a_grid.length, a_grid[0].length) * 1.0) /
56
+ Math.max(a_grid.length, a_grid[0].length)
57
+ if (ratio > best_ratio) {
58
+ best_grid = a_grid
59
+ best_ratio = ratio
60
+ }
61
+
62
+ if (best_ratio == 1) break
63
+ }
64
+ return best_grid
65
+ }
66
+
67
+ // returns an abitrary grid, or null if it can't build one
68
+ public getGrid(max_tries) {
69
+ for (var tries = 0; tries < max_tries; tries++) {
70
+ this.clear() // always start with a fresh grid and char_index
71
+ // place the first word in the middle of the grid
72
+ var start_dir = this.randomDirection()
73
+ var r = Math.floor(this.grid.length / 2)
74
+ var c = Math.floor(this.grid[0].length / 2)
75
+
76
+ var word_element = this.word_elements[0]
77
+ if (start_dir == 'across') {
78
+ c -= Math.floor(word_element.word.length / 2)
79
+ } else {
80
+ r -= Math.floor(word_element.word.length / 2)
81
+ }
82
+
83
+ if (
84
+ this.canPlaceWordAt(word_element.word, r, c, start_dir) !==
85
+ false
86
+ ) {
87
+ this.placeWordAt(
88
+ word_element.word,
89
+ word_element.index,
90
+ r,
91
+ c,
92
+ start_dir
93
+ )
94
+ } else {
95
+ this.bad_words = [word_element]
96
+ return null
97
+ }
98
+
99
+ // start with a group containing all the words (except the first)
100
+ // as we go, we try to place each word in the group onto the grid
101
+ // if the word can't go on the grid, we add that word to the next group
102
+ var groups: any = []
103
+ let word_has_been_added_to_grid
104
+ groups.push(this.word_elements.slice(1))
105
+ for (var g = 0; g < groups.length; g++) {
106
+ word_has_been_added_to_grid = false
107
+ // try to add all the words in this group to the grid
108
+ for (var i = 0; i < groups[g].length; i++) {
109
+ word_element = groups[g][i]
110
+ var best_position = this.findPositionForWord(
111
+ word_element.word
112
+ )
113
+ if (!best_position) {
114
+ // make the new group (if needed)
115
+ if (groups.length - 1 == g) groups.push([])
116
+ // place the word in the next group
117
+ groups[g + 1].push(word_element)
118
+ } else {
119
+ ;(r = best_position['row']), (c = best_position['col'])
120
+ var dir = best_position['direction']
121
+ this.placeWordAt(
122
+ word_element.word,
123
+ word_element.index,
124
+ r,
125
+ c,
126
+ dir
127
+ )
128
+ word_has_been_added_to_grid = true
129
+ }
130
+ }
131
+ // if we haven't made any progress, there is no point in going on to the next group
132
+ if (!word_has_been_added_to_grid) break
133
+ }
134
+ // no need to try again
135
+ if (word_has_been_added_to_grid) return this.minimizeGrid()
136
+ }
137
+
138
+ this.bad_words = groups[groups.length - 1]
139
+ return null
140
+ }
141
+
142
+ // returns the list of WordElements that can't fit on the crossword
143
+ public getBadWords() {
144
+ return this.bad_words
145
+ }
146
+
147
+ // get two arrays ("across" and "down") that contain objects describing the
148
+ // topological position of the word (e.g. 1 is the first word starting from
149
+ // the top left, going to the bottom right), the index of the word (in the
150
+ // original input list), the clue, and the word itself
151
+ public getLegend(grid) {
152
+ var groups = { across: [], down: [] }
153
+ var position = 1
154
+ for (var r = 0; r < grid.length; r++) {
155
+ for (var c = 0; c < grid[r].length; c++) {
156
+ var cell = grid[r][c]
157
+ var increment_position = false
158
+ // check across and down
159
+ for (var k in groups) {
160
+ // does a word start here? (make sure the cell isn't null, first)
161
+ if (cell && cell[k] && cell[k]['is_start_of_word']) {
162
+ var index = cell[k]['index']
163
+ groups[k].push({
164
+ position: position,
165
+ index: index,
166
+ clue: this.clues_in[index],
167
+ word: this.words_in[index],
168
+ })
169
+ increment_position = true
170
+ }
171
+ }
172
+
173
+ if (increment_position) position++
174
+ }
175
+ }
176
+ return groups
177
+ }
178
+
179
+ // move the grid onto the smallest grid that will fit it
180
+ public minimizeGrid() {
181
+ // find bounds
182
+ var r_min = this.GRID_ROWS - 1,
183
+ r_max = 0,
184
+ c_min = this.GRID_COLS - 1,
185
+ c_max = 0
186
+ for (var r = 0; r < this.GRID_ROWS; r++) {
187
+ for (var c = 0; c < this.GRID_COLS; c++) {
188
+ var cell = this.grid[r][c]
189
+ if (cell != null) {
190
+ if (r < r_min) r_min = r
191
+ if (r > r_max) r_max = r
192
+ if (c < c_min) c_min = c
193
+ if (c > c_max) c_max = c
194
+ }
195
+ }
196
+ }
197
+ // initialize new grid
198
+ var rows = r_max - r_min + 1
199
+ var cols = c_max - c_min + 1
200
+ var new_grid = new Array(rows)
201
+ for (let r = 0; r < rows; r++) {
202
+ for (let c = 0; c < cols; c++) {
203
+ new_grid[r] = new Array(cols)
204
+ }
205
+ }
206
+
207
+ // copy the grid onto the smaller grid
208
+ for (let r = r_min, r2 = 0; r2 < rows; r++, r2++) {
209
+ for (let c = c_min, c2 = 0; c2 < cols; c++, c2++) {
210
+ new_grid[r2][c2] = this.grid[r][c]
211
+ }
212
+ }
213
+
214
+ return new_grid
215
+ }
216
+
217
+ // helper for placeWordAt();
218
+ public addCellToGrid(
219
+ word,
220
+ index_of_word_in_input_list,
221
+ index_of_char,
222
+ r,
223
+ c,
224
+ direction
225
+ ) {
226
+ var char = word.charAt(index_of_char)
227
+ if (this.grid[r][c] == null) {
228
+ this.grid[r][c] = new CrosswordCell(char)
229
+
230
+ // init the char_index for that character if needed
231
+ if (!this.char_index[char]) this.char_index[char] = []
232
+
233
+ // add to index
234
+ this.char_index[char].push({ row: r, col: c })
235
+ }
236
+
237
+ var is_start_of_word = index_of_char == 0
238
+ this.grid[r][c][direction] = new CrosswordCellNode(
239
+ is_start_of_word,
240
+ index_of_word_in_input_list
241
+ )
242
+ }
243
+
244
+ // place the word at the row and col indicated (the first char goes there)
245
+ // the next chars go to the right (across) or below (down), depending on the direction
246
+ public placeWordAt(word, index_of_word_in_input_list, row, col, direction) {
247
+ if (direction == 'across') {
248
+ for (let c = col, i = 0; c < col + word.length; c++, i++) {
249
+ this.addCellToGrid(
250
+ word,
251
+ index_of_word_in_input_list,
252
+ i,
253
+ row,
254
+ c,
255
+ direction
256
+ )
257
+ }
258
+ } else if (direction == 'down') {
259
+ for (let r = row, i = 0; r < row + word.length; r++, i++) {
260
+ this.addCellToGrid(
261
+ word,
262
+ index_of_word_in_input_list,
263
+ i,
264
+ r,
265
+ col,
266
+ direction
267
+ )
268
+ }
269
+ } else {
270
+ throw 'Invalid Direction'
271
+ }
272
+ }
273
+
274
+ // you can only place a char where the space is blank, or when the same
275
+ // character exists there already
276
+ // returns false, if you can't place the char
277
+ // 0 if you can place the char, but there is no intersection
278
+ // 1 if you can place the char, and there is an intersection
279
+ public canPlaceCharAt(char, row, col) {
280
+ // no intersection
281
+ if (this.grid[row][col] == null) return 0
282
+ // intersection!
283
+ if (this.grid[row][col]['char'] == char) return 1
284
+
285
+ return false
286
+ }
287
+
288
+ // determines if you can place a word at the row, column in the direction
289
+ public canPlaceWordAt(word, row, col, direction): any {
290
+ // out of bounds
291
+ if (
292
+ row < 0 ||
293
+ row >= this.grid.length ||
294
+ col < 0 ||
295
+ col >= this.grid[row].length
296
+ )
297
+ return false
298
+
299
+ var intersections
300
+
301
+ if (direction == 'across') {
302
+ // out of bounds (word too long)
303
+ if (col + word.length > this.grid[row].length) return false
304
+ // can't have a word directly to the left
305
+ if (col - 1 >= 0 && this.grid[row][col - 1] != null) return false
306
+ // can't have word directly to the right
307
+ if (
308
+ col + word.length < this.grid[row].length &&
309
+ this.grid[row][col + word.length] != null
310
+ )
311
+ return false
312
+
313
+ // check the row above to make sure there isn't another word
314
+ // running parallel. It is ok if there is a character above, only if
315
+ // the character below it intersects with the current word
316
+ for (
317
+ let r = row - 1, c = col, i = 0;
318
+ r >= 0 && c < col + word.length;
319
+ c++, i++
320
+ ) {
321
+ let is_empty = this.grid[r][c] == null
322
+ let is_intersection =
323
+ this.grid[row][c] != null &&
324
+ this.grid[row][c]['char'] == word.charAt(i)
325
+ let can_place_here = is_empty || is_intersection
326
+ if (!can_place_here) return false
327
+ }
328
+
329
+ // same deal as above, we just search in the row below the word
330
+ for (
331
+ let r = row + 1, c = col, i = 0;
332
+ r < this.grid.length && c < col + word.length;
333
+ c++, i++
334
+ ) {
335
+ let is_empty = this.grid[r][c] == null
336
+ let is_intersection =
337
+ this.grid[row][c] != null &&
338
+ this.grid[row][c]['char'] == word.charAt(i)
339
+ let can_place_here = is_empty || is_intersection
340
+ if (!can_place_here) return false
341
+ }
342
+
343
+ // check to make sure we aren't overlapping a char (that doesn't match)
344
+ // and get the count of intersections
345
+ intersections = 0
346
+ for (let c = col, i = 0; c < col + word.length; c++, i++) {
347
+ let result = this.canPlaceCharAt(word.charAt(i), row, c)
348
+ if (result === false) return false
349
+ intersections += result
350
+ }
351
+ } else if (direction == 'down') {
352
+ // out of bounds
353
+ if (row + word.length > this.grid.length) return false
354
+ // can't have a word directly above
355
+ if (row - 1 >= 0 && this.grid[row - 1][col] != null) return false
356
+ // can't have a word directly below
357
+ if (
358
+ row + word.length < this.grid.length &&
359
+ this.grid[row + word.length][col] != null
360
+ )
361
+ return false
362
+
363
+ // check the column to the left to make sure there isn't another
364
+ // word running parallel. It is ok if there is a character to the
365
+ // left, only if the character to the right intersects with the
366
+ // current word
367
+ for (
368
+ let c = col - 1, r = row, i = 0;
369
+ c >= 0 && r < row + word.length;
370
+ r++, i++
371
+ ) {
372
+ let is_empty = this.grid[r][c] == null
373
+ let is_intersection =
374
+ this.grid[r][col] != null &&
375
+ this.grid[r][col]['char'] == word.charAt(i)
376
+ let can_place_here = is_empty || is_intersection
377
+ if (!can_place_here) return false
378
+ }
379
+
380
+ // same deal, but look at the column to the right
381
+ for (
382
+ let c = col + 1, r = row, i = 0;
383
+ r < row + word.length && c < this.grid[r].length;
384
+ r++, i++
385
+ ) {
386
+ let is_empty = this.grid[r][c] == null
387
+ let is_intersection =
388
+ this.grid[r][col] != null &&
389
+ this.grid[r][col]['char'] == word.charAt(i)
390
+ let can_place_here = is_empty || is_intersection
391
+ if (!can_place_here) return false
392
+ }
393
+
394
+ // check to make sure we aren't overlapping a char (that doesn't match)
395
+ // and get the count of intersections
396
+ intersections = 0
397
+ for (let r = row, i = 0; r < row + word.length; r++, i++) {
398
+ let result = this.canPlaceCharAt(word.charAt(i, 1), r, col)
399
+ if (result === false) return false
400
+ intersections += result
401
+ }
402
+ } else {
403
+ throw 'Invalid Direction'
404
+ }
405
+ return intersections
406
+ }
407
+
408
+ public randomDirection() {
409
+ return Math.floor(Math.random() * 2) ? 'across' : 'down'
410
+ }
411
+
412
+ public findPositionForWord(word) {
413
+ // check the char_index for every letter, and see if we can put it there in a direction
414
+ let bests: object[] = []
415
+ for (var i = 0; i < word.length; i++) {
416
+ var possible_locations_on_grid = this.char_index[word.charAt(i)]
417
+ if (!possible_locations_on_grid) continue
418
+ for (var j = 0; j < possible_locations_on_grid.length; j++) {
419
+ var point = possible_locations_on_grid[j]
420
+ var r = point['row']
421
+ var c = point['col']
422
+ // the c - i, and r - i here compensate for the offset of character in the word
423
+ var intersections_across = this.canPlaceWordAt(
424
+ word,
425
+ r,
426
+ c - i,
427
+ 'across'
428
+ )
429
+ var intersections_down = this.canPlaceWordAt(
430
+ word,
431
+ r - i,
432
+ c,
433
+ 'down'
434
+ )
435
+
436
+ if (intersections_across !== false) {
437
+ var objectAcross = {
438
+ intersections: intersections_across,
439
+ row: r,
440
+ col: c - i,
441
+ direction: 'across',
442
+ }
443
+ bests.push(objectAcross)
444
+ }
445
+ if (intersections_down !== false) {
446
+ var objectDown = {
447
+ intersections: intersections_down,
448
+ row: r - i,
449
+ col: c,
450
+ direction: 'down',
451
+ }
452
+ bests.push(objectDown)
453
+ }
454
+ }
455
+ }
456
+
457
+ if (bests.length == 0) return false
458
+
459
+ // find a good random position
460
+ var best = bests[Math.floor(Math.random() * bests.length)]
461
+
462
+ return best
463
+ }
464
+
465
+ public clear() {
466
+ for (var r = 0; r < this.grid.length; r++) {
467
+ for (var c = 0; c < this.grid[r].length; c++) {
468
+ this.grid[r][c] = null
469
+ }
470
+ }
471
+ this.char_index = {}
472
+ }
473
+ }
474
+
475
+ export { Crossword }
@@ -0,0 +1,36 @@
1
+ class CrosswordCell {
2
+ public char!: any
3
+ public across!: any
4
+ public down!: any
5
+ constructor(letter) {
6
+ this.char = letter // the actual letter for the cell on the crossword
7
+ // If a word hits this cell going in the "across" direction, this will be a CrosswordCellNode
8
+ this.across = null
9
+ // If a word hits this cell going in the "down" direction, this will be a CrosswordCellNode
10
+ this.down = null
11
+ }
12
+ }
13
+
14
+ // You can tell if the Node is the start of a word (which is needed if you want to number the cells)
15
+ // and what word and clue it corresponds to (using the index)
16
+ class CrosswordCellNode {
17
+ public is_start_of_word!: any
18
+ public index!: any
19
+ constructor(is_start_of_word, index) {
20
+ this.is_start_of_word = is_start_of_word
21
+ this.index = index // use to map this node to its word or clue
22
+ }
23
+
24
+ // public functionName
25
+ }
26
+
27
+ class WordElement {
28
+ public word!: any
29
+ public index!: any
30
+ constructor(word, index) {
31
+ this.word = word // the actual word
32
+ this.index = index // use to map this node to its word or clue
33
+ }
34
+ }
35
+
36
+ export { CrosswordCell, CrosswordCellNode, WordElement }