@windward/games 0.0.3 → 0.0.5

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 (34) hide show
  1. package/components/content/DatableEditor.vue +0 -3
  2. package/components/content/blocks/crosswordPuzzle/Crossword.ts +553 -0
  3. package/components/content/blocks/crosswordPuzzle/CrosswordClues.vue +91 -0
  4. package/components/content/blocks/crosswordPuzzle/CrosswordElements.ts +38 -0
  5. package/components/content/blocks/crosswordPuzzle/CrosswordPuzzle.vue +673 -0
  6. package/components/content/blocks/multipleChoice/MultipleChoice.vue +187 -127
  7. package/components/content/blocks/multipleChoice/QuestionDialog.vue +37 -13
  8. package/components/content/blocks/sevenStrikes/SevenStikes.vue +368 -0
  9. package/components/content/blocks/sevenStrikes/keyboard.vue +71 -0
  10. package/components/content/blocks/wordJumble/Jumble.vue +2 -10
  11. package/components/content/blocks/wordJumble/WordJumble.vue +22 -10
  12. package/components/settings/CrosswordPuzzleSettingsManager.vue +271 -0
  13. package/components/settings/MultipleChoiceSettingsManager.vue +21 -7
  14. package/components/settings/SevenStrikesSettingsManager.vue +288 -0
  15. package/components/settings/WordJumbleSettingsManager.vue +4 -1
  16. package/i18n/en-US/components/content/blocks/crossword.ts +22 -0
  17. package/i18n/en-US/components/content/blocks/index.ts +4 -0
  18. package/i18n/en-US/components/content/blocks/multiple_choice.ts +2 -4
  19. package/i18n/en-US/components/content/blocks/seven_strikes.ts +6 -0
  20. package/i18n/en-US/components/settings/crossword.ts +7 -0
  21. package/i18n/en-US/components/settings/index.ts +4 -0
  22. package/i18n/en-US/components/settings/multiple_choice.ts +1 -1
  23. package/i18n/en-US/components/settings/seven_strikes.ts +8 -0
  24. package/i18n/en-US/components/settings/word_jumble.ts +1 -1
  25. package/i18n/en-US/shared/content_blocks.ts +2 -0
  26. package/i18n/en-US/shared/settings.ts +2 -0
  27. package/package.json +2 -1
  28. package/plugin.js +43 -1
  29. package/test/blocks/crossword/CrosswordPuzzle.spec.js +49 -0
  30. package/test/blocks/sevenStrikes/sevenStrikes.spec.js +24 -0
  31. package/test/settings/BucketGameManager.spec.js +1 -1
  32. package/test/settings/CrosswordPuzzleManager.spec.js +103 -0
  33. package/test/settings/SevenStrikesManager.spec.js +53 -0
  34. package/test/settings/WordJumbleManager.spec.js +2 -2
@@ -0,0 +1,673 @@
1
+ <template>
2
+ <v-container>
3
+ <div>
4
+ <h2>{{ block.metadata.config.title }}</h2>
5
+ <p>{{ block.metadata.config.instructions }}</p>
6
+ </div>
7
+ <v-alert v-if="wordMap.length === 0" type="info" class="text-center">
8
+ <p>
9
+ {{
10
+ $t(
11
+ 'plugin.games.components.content.blocks.crossword.initial_setup'
12
+ )
13
+ }}
14
+ </p>
15
+ </v-alert>
16
+ <v-alert
17
+ v-if="wordMap.length > 0 && error"
18
+ type="error"
19
+ class="text-center"
20
+ >
21
+ <p>
22
+ {{
23
+ $t(
24
+ 'plugin.games.components.content.blocks.crossword.validation_error'
25
+ )
26
+ }}
27
+ </p>
28
+ <p>{{ error }}</p>
29
+ </v-alert>
30
+ <div
31
+ v-if="!error"
32
+ class="crossword-container"
33
+ ref="crossword-container"
34
+ >
35
+ <div v-if="grid">
36
+ <v-row>
37
+ <v-col cols="12" class="crossword-board-container">
38
+ <v-container :class="containerClass">
39
+ <!-- Rows in grid -->
40
+ <template v-for="(row, rowIndex) in grid">
41
+ <!-- Cell in row -->
42
+ <template v-for="(item, cellIndex) in row">
43
+ <div
44
+ :key="`${id}-cell-parent-${rowIndex}-${cellIndex}`"
45
+ :index="`${id}-cell-parent-${rowIndex}-${cellIndex}`"
46
+ :class="
47
+ cellClass(
48
+ item,
49
+ form[rowIndex][cellIndex],
50
+ isHighlighted(item)
51
+ )
52
+ "
53
+ >
54
+ <div
55
+ v-if="getPositionNumber(item)"
56
+ class="crossword-board__item--number"
57
+ >
58
+ {{ getPositionNumber(item) }}
59
+ </div>
60
+ <input
61
+ v-if="item"
62
+ v-model="form[rowIndex][cellIndex]"
63
+ :id="index(rowIndex, cellIndex)"
64
+ type="text"
65
+ minlength="1"
66
+ maxlength="1"
67
+ :pattern="pattern(item.char)"
68
+ required="required"
69
+ />
70
+ </div>
71
+ </template>
72
+ <!-- /Cell in row -->
73
+ </template>
74
+ </v-container>
75
+ </v-col>
76
+ <!-- Clues -->
77
+ <v-col cols="12" class="crossword-clues">
78
+ <CrosswordClues
79
+ :down="downWords"
80
+ :across="acrossWords"
81
+ @click="onClickClue"
82
+ ></CrosswordClues>
83
+ </v-col>
84
+ </v-row>
85
+ <v-row class="d-flex justify-center">
86
+ <v-col cols="12" class="d-flex justify-center">
87
+ <v-btn
88
+ color="primary"
89
+ class="ma-3"
90
+ @click="onSetUpData"
91
+ >
92
+ {{
93
+ $t(
94
+ 'plugin.games.components.content.blocks.crossword.play_again'
95
+ )
96
+ }}
97
+ </v-btn>
98
+ </v-col>
99
+ </v-row>
100
+ </div>
101
+ </div>
102
+ </v-container>
103
+ </template>
104
+
105
+ <script>
106
+ import { Crossword } from './Crossword'
107
+ import BaseContentBlock from '~/components/Content/Blocks/BaseContentBlock'
108
+ import _ from 'lodash'
109
+ import Crypto from '~/helpers/Crypto'
110
+ import CrosswordClues from './CrosswordClues.vue'
111
+
112
+ export default {
113
+ name: 'CrossWordPuzzle',
114
+ extends: BaseContentBlock,
115
+ components: { CrosswordClues },
116
+ props: {},
117
+ beforeMount() {
118
+ this.id = Crypto.id()
119
+ if (_.isEmpty(this.block)) {
120
+ this.block = {}
121
+ }
122
+ if (_.isEmpty(this.block.metadata)) {
123
+ this.block.metadata = {}
124
+ }
125
+ if (_.isEmpty(this.block.metadata.config)) {
126
+ this.block.metadata.config = {}
127
+ }
128
+ if (_.isEmpty(this.block.metadata.config.title)) {
129
+ this.block.metadata.config.title = ''
130
+ }
131
+ if (_.isEmpty(this.block.metadata.config.instructions)) {
132
+ this.block.metadata.config.instructions = ''
133
+ }
134
+ },
135
+ data() {
136
+ return {
137
+ id: '',
138
+ highlightIndex: -1,
139
+ acrossWords: [],
140
+ downWords: [],
141
+ error: '',
142
+ matrix: null,
143
+ grid: null,
144
+ form: null,
145
+ }
146
+ },
147
+ computed: {
148
+ wordMap() {
149
+ const map = _.cloneDeep(
150
+ _.get(this.block, 'metadata.config.words', [])
151
+ )
152
+
153
+ // Toss out crossword items that are missing the word or clue
154
+ return map.filter((v) => {
155
+ return !_.isEmpty(v.word) && !_.isEmpty(v.clue)
156
+ })
157
+ },
158
+ containerClass() {
159
+ let classValue = ''
160
+ let crosswordSize = 'crossword--sm'
161
+
162
+ // Apply the column size
163
+ classValue +=
164
+ 'crossword-board crossword-col-' +
165
+ _.get(this.block, 'metadata.columns', 12) / 3
166
+
167
+ // If we're still showing sample data, figure out the size on that
168
+ // We do this instead of hardcoding (since it changes based on locale)
169
+ if (!_.isEmpty(this.wordMap)) {
170
+ // if render is true then see what grid size should be based on longest word
171
+ this.wordMap.forEach((element) => {
172
+ if (element.word.length > 13) {
173
+ crosswordSize = 'crossword--lg'
174
+ }
175
+ })
176
+ }
177
+ classValue += ' ' + crosswordSize
178
+ return classValue
179
+ },
180
+ },
181
+ watch: {
182
+ render(newVal) {
183
+ if (newVal === true) {
184
+ // on render reset up grid and look for word changes
185
+ this.onSetUpData()
186
+ }
187
+ },
188
+ block: {
189
+ deep: true,
190
+ handler() {
191
+ // on render reset up grid and look for word changes
192
+ this.onSetUpData()
193
+ },
194
+ },
195
+ },
196
+ mounted() {
197
+ this.onSetUpData()
198
+ },
199
+ methods: {
200
+ onClickClue(word) {
201
+ if (this.highlightIndex !== word.index) {
202
+ this.highlightIndex = word.index
203
+ } else {
204
+ this.highlightIndex = -1
205
+ }
206
+ },
207
+ isHighlighted(cell) {
208
+ const acrossIndex = _.get(cell, 'across.index', false)
209
+ const downIndex = _.get(cell, 'down.index', false)
210
+
211
+ if (acrossIndex !== false) {
212
+ return this.highlightIndex === acrossIndex
213
+ } else if (downIndex !== false) {
214
+ return this.highlightIndex === downIndex
215
+ } else {
216
+ return false
217
+ }
218
+ },
219
+ cellClass(cell, formValue, isHighlighted) {
220
+ let cellClass = 'crossword-board__item crossword-board__item--blank'
221
+ if (cell) {
222
+ cellClass = 'crossword-board__item'
223
+
224
+ if (this.equalsLetters(formValue, cell.char)) {
225
+ cellClass += ' crossword-board__item--valid'
226
+ }
227
+ if (isHighlighted) {
228
+ cellClass += ' crossword-board__item--highlighted'
229
+ }
230
+ }
231
+
232
+ return cellClass
233
+ },
234
+ getPositionNumber(cell) {
235
+ const isAcross = _.get(cell, 'across.isStartOfWord', false)
236
+ const isDown = _.get(cell, 'down.isStartOfWord', false)
237
+
238
+ if (isAcross) {
239
+ return cell.across.position
240
+ } else if (isDown) {
241
+ return cell.down.position
242
+ } else {
243
+ return 0
244
+ }
245
+ },
246
+ onSetUpData(generate = true) {
247
+ this.error = ''
248
+ this.highlightIndex = -1
249
+
250
+ if (_.isEmpty(this.wordMap)) {
251
+ this.error = this.$t(
252
+ 'plugin.games.components.content.blocks.crossword.error.min_words'
253
+ )
254
+ } else {
255
+ // checks if there are any words that don't share letters with any others
256
+ const letterCheck = this.onCheckWordsShareLetters(this.wordMap)
257
+
258
+ if (this.wordMap.length < 2) {
259
+ this.error = this.$t(
260
+ 'plugin.games.components.content.blocks.crossword.error.min_words'
261
+ )
262
+ } else if (letterCheck.length > 0) {
263
+ // if a word doesn't share letters load with dummy data and alert the user
264
+ let words = letterCheck.toString()
265
+ this.error = this.$t(
266
+ 'plugin.games.components.content.blocks.crossword.error.words_no_shared_letters',
267
+ [words]
268
+ )
269
+ } else if (!this.validateWordMaxLength(this.wordMap)) {
270
+ this.error = this.$t(
271
+ 'plugin.games.components.content.blocks.crossword.error.word_length_max_limit'
272
+ )
273
+ } else if (!this.validateWordMinLength(this.wordMap)) {
274
+ this.error = this.$t(
275
+ 'plugin.games.components.content.blocks.crossword.error.word_length_min_limit'
276
+ )
277
+ }
278
+ }
279
+
280
+ // this is true if render is toggled back to true
281
+ if (generate && this.error === '') {
282
+ this.generateGrid(this.wordMap)
283
+ }
284
+ },
285
+ validateWordMaxLength(wordMap) {
286
+ let isValid = true
287
+ // inputs are not allowed over 20
288
+ // this was added to check if imports have more than 20 characters
289
+ wordMap.forEach((element) => {
290
+ if (_.isEmpty(element) || _.isEmpty(element.word)) {
291
+ isValid = false
292
+ return
293
+ }
294
+ if (element.word.length > 20) {
295
+ isValid = false
296
+ }
297
+ })
298
+ return isValid
299
+ },
300
+ validateWordMinLength(wordMap) {
301
+ let isValid = true
302
+ // inputs are not allowed over 20
303
+ // this was added to check if imports have more than 20 characters
304
+ wordMap.forEach((element) => {
305
+ if (_.isEmpty(element) || _.isEmpty(element.word)) {
306
+ isValid = false
307
+ return
308
+ }
309
+ if (element.word.length < 2) {
310
+ isValid = false
311
+ }
312
+ })
313
+ return isValid
314
+ },
315
+ onCheckWordsShareLetters(wordMap) {
316
+ let arrayOfWordsLetters = []
317
+ let wordsThatDontShareLetters = []
318
+
319
+ // splits words into an array of letters that is in an array
320
+ wordMap.forEach((element) => {
321
+ if (_.isEmpty(element) || _.isEmpty(element.word)) {
322
+ return
323
+ }
324
+ element.word = element.word.toUpperCase()
325
+ const letterArray = element.word.split('')
326
+ arrayOfWordsLetters.push(letterArray)
327
+ })
328
+
329
+ for (let i = 0; i < arrayOfWordsLetters.length; i++) {
330
+ let shareLetters = false
331
+ let theyShareLetters = false
332
+ // loop over individual letters to check if any words share the letters
333
+ arrayOfWordsLetters[i].forEach((letter) => {
334
+ wordMap.forEach((element) => {
335
+ const elementIndex = wordMap.indexOf(element)
336
+ // prevents from checking the letters against the word they came from
337
+ if (elementIndex !== i) {
338
+ // checks if the word from the block being looped over contains the letter
339
+ theyShareLetters = element.word.includes(letter)
340
+ // if it does contain the letter shareLetters is true and will not push
341
+ //the word into the array for words that don't share any letters
342
+ if (theyShareLetters) {
343
+ shareLetters = true
344
+ }
345
+ }
346
+ })
347
+ })
348
+ if (!shareLetters) {
349
+ // this holds all words that don't share any letters with other words
350
+ wordsThatDontShareLetters.push(wordMap[i].word)
351
+ }
352
+ }
353
+ return wordsThatDontShareLetters
354
+ },
355
+ index(rowIndex, cellIndex) {
356
+ let id = `${this.id}-cell-${rowIndex + 1}-${cellIndex + 1}`
357
+ return id
358
+ },
359
+
360
+ pattern(value) {
361
+ if (value) {
362
+ let pattern = `^[${value.toLowerCase()}${value.toUpperCase()}]{1}$`
363
+ return pattern
364
+ }
365
+ },
366
+
367
+ equalsLetters(letter1, letter2) {
368
+ if (letter1 === null || typeof letter1 === 'undefined') {
369
+ letter1 = ''
370
+ }
371
+ if (letter2 === null || typeof letter2 === 'undefined') {
372
+ letter2 = ''
373
+ }
374
+ return (
375
+ letter1.toUpperCase() === letter2.toUpperCase() ||
376
+ letter1.toLowerCase() === letter2.toLowerCase()
377
+ )
378
+ },
379
+
380
+ shuffle(array) {
381
+ let currentIndex = array.length,
382
+ randomIndex
383
+
384
+ // While there remain elements to shuffle.
385
+ while (currentIndex !== 0) {
386
+ // Pick a remaining element.
387
+ randomIndex = Math.floor(Math.random() * currentIndex)
388
+ currentIndex--
389
+
390
+ // And swap it with the current element.
391
+ ;[array[currentIndex], array[randomIndex]] = [
392
+ array[randomIndex],
393
+ array[currentIndex],
394
+ ]
395
+ }
396
+
397
+ return array
398
+ },
399
+ generateGrid(wordMap) {
400
+ let MATRIX_LENGTH = 13
401
+ wordMap.forEach((e) => {
402
+ if (e.word.length > 13) {
403
+ MATRIX_LENGTH = 20
404
+ }
405
+ })
406
+
407
+ let grid = null
408
+ let cw = null
409
+ let attempts = 0
410
+ let correctSize = false
411
+
412
+ //while (!grid || grid.length > MATRIX_LENGTH) {
413
+ // // Create crossword object with the words and clues
414
+
415
+ // create the crossword grid (try to make it have a 1:1 width to height ratio in 10 tries)
416
+ // Sometimes the crossword will generate a 21x21 matrix even though we defined a 20x20.
417
+ while (attempts < 10 && !correctSize) {
418
+ // Re-randomize the words
419
+ const randomized = this.shuffle(wordMap)
420
+ const words = randomized.map((w) => w.word)
421
+ const clues = randomized.map((w) => w.clue)
422
+
423
+ cw = new Crossword(words, clues)
424
+
425
+ // We set 10 tries so we don't try calculating the grid forever
426
+ grid = cw.getSquareGrid(10)
427
+
428
+ // Verify the size of the grid
429
+ correctSize =
430
+ grid.length <= MATRIX_LENGTH &&
431
+ grid[0].length <= MATRIX_LENGTH
432
+
433
+ attempts++
434
+ }
435
+
436
+ if (grid.length > MATRIX_LENGTH) {
437
+ this.error = this.$t(
438
+ 'plugin.games.components.content.blocks.crossword.error.could_not_generate'
439
+ )
440
+ return false
441
+ }
442
+
443
+ if (_.isEmpty(grid)) {
444
+ this.error = this.$t(
445
+ 'plugin.games.components.content.blocks.crossword.error.unknown'
446
+ )
447
+
448
+ return false
449
+ }
450
+
451
+ this.form = [...Array(MATRIX_LENGTH)].map(() =>
452
+ Array(MATRIX_LENGTH).fill('')
453
+ )
454
+
455
+ // fill empty grid spots in
456
+ // add rows first
457
+ while (grid.length < MATRIX_LENGTH) {
458
+ grid.push([])
459
+ }
460
+ // add columns
461
+ for (let row = 0; row < grid.length; row++) {
462
+ while (grid[row].length < MATRIX_LENGTH) {
463
+ grid[row].push(null)
464
+ }
465
+ }
466
+
467
+ // Generate the legend now that everything's filled
468
+ const legend = cw.getLegend(grid)
469
+
470
+ this.acrossWords = legend.across
471
+ this.downWords = legend.down
472
+
473
+ if (!_.isEmpty(this.grid)) {
474
+ this.grid.splice(0)
475
+ }
476
+
477
+ this.grid = grid
478
+ },
479
+ },
480
+ }
481
+ </script>
482
+
483
+ <style lang="scss" scoped>
484
+ .crossword-board__item--valid {
485
+ background: var(--v-success-base);
486
+ color: #fff;
487
+ }
488
+
489
+ .crossword-board-container {
490
+ position: relative;
491
+ padding-top: 5px;
492
+ overflow: auto;
493
+ }
494
+
495
+ .crossword-board {
496
+ position: relative;
497
+ background: transparent;
498
+ display: grid;
499
+ list-style-type: none;
500
+ padding: 0;
501
+ box-sizing: content-box !important;
502
+ border: 1px solid #000;
503
+ }
504
+ .crossword-board.crossword--sm {
505
+ grid-template: repeat(13, 7.69231%) / repeat(13, 7.69231%);
506
+ }
507
+ .crossword-board.crossword--lg {
508
+ grid-template: repeat(20, 5%) / repeat(20, 5%);
509
+ }
510
+ .crossword-col-4 {
511
+ width: 800px;
512
+ height: 800px;
513
+ }
514
+ .crossword-col-3 {
515
+ width: 550px;
516
+ height: 550px;
517
+ }
518
+ .crossword-col-2 {
519
+ width: 450px;
520
+ height: 450px;
521
+ }
522
+ .crossword-col-1 {
523
+ width: 220px;
524
+ height: 220px;
525
+ }
526
+ .crossword-board__item {
527
+ border: 1px solid #333;
528
+ position: relative;
529
+ text-align: center;
530
+ font-size: 20px;
531
+ font-weight: bold;
532
+ }
533
+
534
+ .crossword-board__item > input {
535
+ width: 100%;
536
+ height: 100%;
537
+ text-align: center;
538
+ text-transform: uppercase;
539
+ }
540
+ .crossword-board.crossword--sm .crossword-board__item--number {
541
+ font-size: 75%;
542
+ line-height: 100%;
543
+ }
544
+ .crossword-board.crossword--lg .crossword-board__item--number {
545
+ font-size: 50%;
546
+ line-height: 100%;
547
+ }
548
+
549
+ .crossword-board__item:not(.crossword-board__item--valid) {
550
+ background-color: #fff;
551
+
552
+ &.crossword-board__item--highlighted {
553
+ animation: animated-highlight 2s linear infinite;
554
+ @keyframes animated-highlight {
555
+ 0% {
556
+ filter: brightness(120%);
557
+ background-color: var(--v-secondary-lighten1);
558
+ }
559
+
560
+ 50% {
561
+ filter: brightness(100%);
562
+ background-color: var(--v-secondary-lighten1);
563
+ }
564
+
565
+ 100% {
566
+ filter: brightness(120%);
567
+ background-color: var(--v-secondary-lighten1);
568
+ }
569
+ }
570
+ }
571
+ }
572
+
573
+ .crossword-board__item:active:not(.crossword-board__item--valid),
574
+ .crossword-board__item:focus:not(.crossword-board__item--valid) {
575
+ background: var(--v-error-base);
576
+ border: 1px solid #333;
577
+ }
578
+
579
+ .crossword-board__item--blank {
580
+ background-color: #111 !important;
581
+ border: 1px solid #333;
582
+ }
583
+
584
+ .crossword-clues {
585
+ background: var(--v-content-background-base);
586
+ position: sticky;
587
+ bottom: 100px;
588
+ border-radius: 5px;
589
+ }
590
+ .crossword-board__item--number {
591
+ position: relative;
592
+ height: 0;
593
+ width: 0;
594
+ color: #000;
595
+ user-select: none;
596
+ pointer-events: none;
597
+ }
598
+
599
+ @media (max-width: 1570px) {
600
+ .crossword-col-4 {
601
+ width: 500px;
602
+ height: 500px;
603
+ }
604
+ .crossword-board-container {
605
+ box-sizing: content-box !important;
606
+ }
607
+ }
608
+ @media (max-width: 1480px) {
609
+ .crossword-col-4 {
610
+ width: 400px;
611
+ height: 400px;
612
+ }
613
+ .crossword-board-container {
614
+ box-sizing: content-box !important;
615
+ }
616
+ }
617
+ @media (max-width: 1360px) {
618
+ .crossword-col-4 {
619
+ width: 350px;
620
+ height: 350px;
621
+ }
622
+ .crossword-board-container {
623
+ box-sizing: content-box !important;
624
+ }
625
+ }
626
+ @media (max-width: 1300px) {
627
+ .crossword-col-4 {
628
+ width: 300px;
629
+ height: 300px;
630
+ }
631
+ .crossword-board-container {
632
+ box-sizing: content-box !important;
633
+ }
634
+ }
635
+ @media (max-width: 1263px) {
636
+ .crossword-col-4 {
637
+ width: 550px;
638
+ height: 550px;
639
+ }
640
+ .crossword-board-container {
641
+ box-sizing: content-box !important;
642
+ }
643
+ }
644
+ @media (max-width: 823px) {
645
+ .crossword-col-4 {
646
+ width: 400px;
647
+ height: 400px;
648
+ }
649
+ .crossword-board-container {
650
+ box-sizing: content-box !important;
651
+ }
652
+ }
653
+ @media (max-width: 631px) {
654
+ .crossword-col-4 {
655
+ width: 300px;
656
+ height: 300px;
657
+ }
658
+ .crossword-board-container {
659
+ box-sizing: content-box !important;
660
+ }
661
+ }
662
+ @media (max-width: 565px) {
663
+ .crossword-col-4 {
664
+ width: 300px;
665
+ height: 300px;
666
+ }
667
+ .crossword-board-container {
668
+ box-sizing: content-box !important;
669
+ padding-top: 12px;
670
+ padding-left: 12px;
671
+ }
672
+ }
673
+ </style>