forked from giuocob/OoT-Bingo
-
Notifications
You must be signed in to change notification settings - Fork 1
/
generator.js
787 lines (660 loc) · 26.9 KB
/
generator.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
//NOTICE: As of version 6, this script will only generate cards correctly for Ocarina of Time bingo
//and as such should be saved alongside the regular bingo script.
// 100 is way higher than would ever be allowed, so use it
// as a signal to get out
var TOO_MUCH_SYNERGY = 100;
// the number of squares in a row on the board
// might change in the future if we want to support arbitrarily sized boards
var SQUARES_PER_ROW = 5;
/*
* Profile Format:
*
* defaultMinimumSynergy: the minimum synergy allowed in any one row
* defaultMaximumSynergy: the maximum synergy allowed in any one row
* defaultMaximumIndividualSynergy: the maximum synergy allowed between a pair of goals
* defaultMaximumSpill: the maximum allowed spill up in difficulty when choosing a goal
* defaultInitialOffset: the initial deviation from the desired time to allow when choosing a goal
* defaultMaximumOffset: the maximum allowed deviation from the desired time when choosing a goal
* baselineTime: the base amount of time that is factored in to account for starting / common setup
* timePerDifficulty: the ratio between time and difficulty
*/
var DEFAULT_PROFILE = {
defaultMinimumSynergy: -3,
defaultMaximumSynergy: 7,
defaultMaximumIndividualSynergy: 3.75,
defaultMaximumSpill: 2,
defaultInitialOffset: 1,
defaultMaximumOffset: 2,
baselineTime: 27.75,
timePerDifficulty: 0.75
};
var NORMAL_PROFILE = {
defaultMinimumSynergy: DEFAULT_PROFILE.defaultMinimumSynergy,
defaultMaximumSynergy: DEFAULT_PROFILE.defaultMaximumSynergy,
defaultMaximumIndividualSynergy: DEFAULT_PROFILE.defaultMaximumIndividualSynergy,
defaultMaximumSpill: DEFAULT_PROFILE.defaultMaximumSpill,
defaultInitialOffset: DEFAULT_PROFILE.defaultInitialOffset,
defaultMaximumOffset: DEFAULT_PROFILE.defaultMaximumOffset,
baselineTime: DEFAULT_PROFILE.baselineTime,
timePerDifficulty: DEFAULT_PROFILE.timePerDifficulty
};
var SHORT_PROFILE = {
defaultMinimumSynergy: DEFAULT_PROFILE.defaultMinimumSynergy,
defaultMaximumSynergy: 3,
defaultMaximumIndividualSynergy: DEFAULT_PROFILE.defaultMaximumIndividualSynergy,
defaultMaximumSpill: DEFAULT_PROFILE.defaultMaximumSpill,
defaultInitialOffset: DEFAULT_PROFILE.defaultInitialOffset,
defaultMaximumOffset: DEFAULT_PROFILE.defaultMaximumOffset,
baselineTime: 12,
timePerDifficulty: 0.5
};
var BLACKOUT_PROFILE = {
defaultMinimumSynergy: -10,
defaultMaximumSynergy: 10,
defaultMaximumIndividualSynergy: 4.5,
defaultMaximumSpill: 4,
defaultInitialOffset: 2,
defaultMaximumOffset: 6,
baselineTime: DEFAULT_PROFILE.baselineTime,
timePerDifficulty: DEFAULT_PROFILE.timePerDifficulty
};
var SHORTBLACKOUT_PROFILE = {
defaultMinimumSynergy: -4,
defaultMaximumSynergy: 4,
defaultMaximumIndividualSynergy: DEFAULT_PROFILE.defaultMaximumIndividualSynergy,
defaultMaximumSpill: DEFAULT_PROFILE.defaultMaximumSpill,
defaultInitialOffset: 2,
defaultMaximumOffset: 6,
baselineTime: 12,
timePerDifficulty: 0.5
};
Array.prototype.sortNumerically = function() {
return this.sort(function(a, b) {
return a - b;
});
};
Array.prototype.shuffled = function() {
var toShuffle = this.slice();
for (var i = 0; i < toShuffle.length; i++) {
var randElement = Math.floor(Math.random() * (i + 1));
var temp = toShuffle[i];
toShuffle[i] = toShuffle[randElement];
toShuffle[randElement] = temp;
}
return toShuffle;
};
function hasDuplicateStrings(array) {
var seen = {};
for (var i = 0; i < array.length; i++) {
var el = array[i];
if (el in seen) {
return true;
}
seen[el] = true;
}
return false;
};
//giuocob 16-8-12: lineCheckList[] has been replaced to allow for removal of all-child rows
//Note: the INDICES_PER_ROW relation is simply the inverse of the ROWS_PER_INDEX relation
var INDICES_PER_ROW = {
"row1": [1, 2, 3, 4, 5],
"row2": [6, 7, 8, 9, 10],
"row3": [11, 12, 13, 14, 15],
"row4": [16, 17, 18, 19, 20],
"row5": [21, 22, 23, 24, 25],
"col1": [1, 6, 11, 16, 21],
"col2": [2, 7, 12, 17, 22],
"col3": [3, 8, 13, 18, 23],
"col4": [4, 9, 14, 19, 24],
"col5": [5, 10, 15, 20, 25],
"tlbr": [1, 7, 13, 19, 25],
"bltr": [5, 9, 13, 17, 21]
};
//Given an object that maps keys to flat arrays, invert said object
function invertObject(obj) {
var ret = {};
Object.keys(obj).forEach(function (key) {
obj[key].forEach(function (item) {
if (!ret[item]) ret[item] = [];
ret[item].push(key);
});
});
return ret;
}
// a mapping from board slot to the rows that it's a part of
// for example, ROWS_PER_INDEX[1] returns ["row1", "col1", "tlbr"]
var ROWS_PER_INDEX = invertObject(INDICES_PER_ROW);
var BingoGenerator = function(bingoList, options) {
if (!options) {
options = {};
}
this.language = options.lang || 'name';
this.mode = options.mode || 'normal';
this.seed = options.seed || Math.ceil(999999 * Math.random()).toString();
if (bingoList.info && bingoList.info.combined === 'true') {
if (bingoList[this.mode]) {
bingoList = bingoList[this.mode];
}
else if (bingoList["normal"]) {
bingoList = bingoList["normal"];
}
else {
console.log("bingoList doesn't contain a valid sub goal list for mode: \"" + this.mode + "\"");
}
}
this.goalsByDifficulty = bingoList;
this.rowtypeTimeSave = bingoList.rowtypes;
this.synergyFilters = bingoList.synfilters || {};
// assemble a list of all goals sorted by the goals' times
this.goalsList = [];
for (var i = 1; i <= 25; i++) {
this.goalsList = this.goalsList.concat(bingoList[i]);
}
this.goalsList.sort(function(a, b) {
var timeDiff = a.time - b.time;
if (timeDiff !== 0) {
return timeDiff;
}
if (a.id > b.id) {
return 1;
}
else if (a.id < b.id) {
return -1;
}
else {
return 0;
}
});
this.goalsByName = {};
for (var i = 0; i < this.goalsList.length; i++) {
var goal = this.goalsList[i];
this.goalsByName[goal.name] = goal;
}
this.profile = NORMAL_PROFILE;
if (this.mode === 'short') {
this.profile = SHORT_PROFILE;
}
else if (this.mode === 'blackout') {
this.profile = BLACKOUT_PROFILE;
}
this.baselineTime = options.baselineTime || this.profile.baselineTime;
this.timePerDifficulty = options.timePerDifficulty || this.profile.timePerDifficulty;
this.minimumSynergy = options.minimumSynergy || this.profile.defaultMinimumSynergy;
this.maximumSynergy = options.maximumSynergy || this.profile.defaultMaximumSynergy;
this.maximumIndividualSynergy = options.maximumIndividualSynergy || this.profile.defaultMaximumIndividualSynergy;
this.maximumSpill = options.maximumSpill || this.profile.defaultMaximumSpill;
this.initialOffset = options.initialOffset || this.profile.defaultInitialOffset;
this.maximumOffset = options.maximumOffset || this.profile.defaultMaximumOffset;
Math.seedrandom(this.seed);
};
//Main entry point
BingoGenerator.prototype.makeCard = function() {
// set up the bingo board by filling in the difficulties based on a magic square
this.bingoBoard = this.generateMagicSquare();
// fill in the goals of the board in a random order
var populationOrder = this.generatePopulationOrder();
for (var i = 1; i <= 25; i++) {
var nextPosition = populationOrder[i];
var result = this.chooseGoalForPosition(nextPosition);
if (result.goal) {
// copy the goal data into the square
this.bingoBoard[nextPosition].types = result.goal.types;
this.bingoBoard[nextPosition].subtypes = result.goal.subtypes;
this.bingoBoard[nextPosition].rowtypes = result.goal.rowtypes;
this.bingoBoard[nextPosition].name = result.goal[this.language] || result.goal.name;
this.bingoBoard[nextPosition].id = result.goal.id;
this.bingoBoard[nextPosition].time = result.goal.time;
this.bingoBoard[nextPosition].goal = result.goal;
// also copy the synergy
this.bingoBoard[nextPosition].synergy = result.synergy;
}
else {
return false;
}
}
return this.bingoBoard;
};
/**
* Generate an initial magic square of difficulties based on the random seed
* @returns {Array}
*/
BingoGenerator.prototype.generateMagicSquare = function() {
var magicSquare = [];
for (var i = 1; i <= 25; i++) {
var difficulty = this.difficulty(i);
magicSquare[i] = {
difficulty: difficulty,
desiredTime: difficulty * this.timePerDifficulty
};
}
return magicSquare;
};
function weightedShuffle(arr) {
return arr.map(el => ({
el,
sortVal: (el.weight || 0) + Math.random() + Math.random() + Math.random() + Math.random() - 2
}))
.sort(({
sortVal: sv1
}, {
sortVal: sv2
}) => sv2 - sv1)
.map(({
el
}) => el);
}
/**
* Given a position on the board, chooses a goal that can be placed in that position without
* blowing our synergy budget.
* @param position the position on the board that we want to find a goal for
* @returns {goal, synergy} or false
*/
BingoGenerator.prototype.chooseGoalForPosition = function(position) {
var desiredDifficulty = this.bingoBoard[position].difficulty;
var desiredTime = desiredDifficulty * this.timePerDifficulty;
// scan through the acceptable difficulty ranges
for (var offset = this.initialOffset; offset <= this.maximumOffset; offset++) {
var minTime = desiredTime - offset;
var maxTime = desiredTime + offset;
var goalsAtTime = this.getGoalsInTimeRange(minTime, maxTime);
goalsAtTime = weightedShuffle(goalsAtTime);
// scan through each goal at this difficulty level
for (var j = 0; j < goalsAtTime.length; j++) {
var goal = goalsAtTime[j];
// don't allow duplicates of goals
if (this.hasGoalOnBoard(goal)) {
continue;
}
// in blackout mode, don't allow goals that conflict with each other, e.g. 8 hearts and 9 hearts,
// even if they are in different rows
if (this.mode === 'blackout') {
if (this.hasConflictsOnBoard(goal)) {
continue;
}
}
var synergies = this.checkLine(position, goal);
if (this.maximumSynergy >= synergies.maxSynergy && synergies.minSynergy >= this.minimumSynergy) {
return {goal: goal, synergy: synergies.maxSynergy};
}
}
}
return false;
};
/**
* Generate a semi-random order to populate the bingo board goals in
* @returns {Array}
*/
BingoGenerator.prototype.generatePopulationOrder = function() {
//giuocob 19-2-13: this.bingoBoard is no longer populated left to right:
//It is now populated mostly randomly, with high difficult goals and
//goals on the diagonals out in front
//Populate center first
var populationOrder = [];
populationOrder[1] = 13;
//Next populate diagonals
var diagonals = [1, 7, 19, 25, 5, 9, 17, 21].shuffled();
populationOrder = populationOrder.concat(diagonals);
//Finally add the rest of the squares
var nondiagonals = [2, 3, 4, 6, 8, 10, 11, 12, 14, 15, 16, 18, 20, 22, 23, 24].shuffled();
populationOrder = populationOrder.concat(nondiagonals);
//Lastly, find location of difficulty 23,24,25 elements and put them out front
for (var k = 23; k <= 25; k++) {
var currentSquare = this.getDifficultyIndex(k);
if (currentSquare === 0) continue;
for (var i = 1; i < 25; i++) {
if (populationOrder[i] == currentSquare) {
populationOrder.splice(i, 1);
break;
}
}
populationOrder.splice(1, 0, currentSquare);
}
return populationOrder;
};
// uses a magic square to calculate the intended difficulty of a location on the bingo board
BingoGenerator.prototype.difficulty = function(i) {
// To create the magic square we need 2 random orderings of the numbers 0, 1, 2, 3, 4.
// The following creates those orderings and calls them Table5 and Table1
var Num3 = this.seed % 1000; // Table5 will use the ones, tens, and hundreds digits.
var Rem8 = Num3 % 8;
var Rem4 = Math.floor(Rem8 / 2);
var Rem2 = Rem8 % 2;
var Rem5 = Num3 % 5;
var Rem3 = Num3 % 3; // Note that Rem2, Rem3, Rem4, and Rem5 are mathematically independent.
var RemT = Math.floor(Num3 / 120); // This is between 0 and 8
// The idea is to begin with an array containing a single number, 0.
// Each number 1 through 4 is added in a random spot in the array's current size.
// The result - the numbers 0 to 4 are in the array in a random (and uniform) order.
var Table5 = [0];
Table5.splice(Rem2, 0, 1);
Table5.splice(Rem3, 0, 2);
Table5.splice(Rem4, 0, 3);
Table5.splice(Rem5, 0, 4);
Num3 = Math.floor(this.seed / 1000); // Table1 will use the next 3 digits.
Num3 = Num3 % 1000;
Rem8 = Num3 % 8;
Rem4 = Math.floor(Rem8 / 2);
Rem2 = Rem8 % 2;
Rem5 = Num3 % 5;
Rem3 = Num3 % 3;
RemT = RemT * 8 + Math.floor(Num3 / 120); // This is between 0 and 64.
var Table1 = [0];
Table1.splice(Rem2, 0, 1);
Table1.splice(Rem3, 0, 2);
Table1.splice(Rem4, 0, 3);
Table1.splice(Rem5, 0, 4);
i--;
RemT = RemT % 5; // Between 0 and 4, fairly uniformly.
x = (i + RemT) % 5; // RemT is horizontal shift to put any diagonal on the main diagonal.
y = Math.floor(i / 5);
// The Tables are set into a single magic square template
// Some are the same up to some rotation, reflection, or row permutation.
// However, all genuinely different magic squares can arise in this fashion.
var e5 = Table5[(x + 3 * y) % 5];
var e1 = Table1[(3 * x + y) % 5];
// Table5 controls the 5* part and Table1 controls the 1* part.
value = 5 * e5 + e1;
if (this.mode == "long") {
value = Math.floor((value + 25) / 2);
}
value++;
return value;
};
//Get a uniformly shuffled array of all the goals of a given difficulty tier
BingoGenerator.prototype.getShuffledGoals = function(difficulty) {
return this.goalsByDifficulty[difficulty].shuffled();
};
//Given a difficulty as an argument, find the square that contains that difficulty
BingoGenerator.prototype.getDifficultyIndex = function(difficulty) {
for (var i = 1; i <= 25; i++) {
if (this.bingoBoard[i].difficulty == difficulty) {
return i;
}
}
return 0;
};
/**
* Returns all of the goals in the goalsList that have a time value in the range [minTime, maxTime]
* (the range is inclusive on both ends).
* Does not take into account any synergy between goals.
* @param minTime the minimum acceptable time, inclusive
* @param maxTime the maximum acceptable time, inclusive
* @returns {Array.<T>} sorted array of goals within the range of times
*/
BingoGenerator.prototype.getGoalsInTimeRange = function(minTime, maxTime) {
// if linear scan ends up being too slow, we can optimize this by finding the min using binary search
// and bailing out early after the first goal exceeds maxTime
return this.goalsList.filter(function(goal) {
return minTime <= goal.time && goal.time <= maxTime;
});
};
/**
* Returns true if the given goal has already been placed on the board.
* Does so by checking against the ids of goals already on the board. Therefore relies on
* different goals having different id fields.
* @param goal the goal to check for
* @returns {boolean} true if the goal is on the board, false otherwise
*/
BingoGenerator.prototype.hasGoalOnBoard = function(goal) {
for (var i = 1; i <= 25; i++) {
if (this.bingoBoard[i].id === goal.id) {
return true;
}
}
return false;
};
/**
* Returns true if there are goals that conflict with the given goal anywhere on the board.
* This is intended for helping to generate better blackout cards.
* @param goal the goal to check for conflicts with (not already on the board)
* @returns {boolean} true if the board contains goals that conflict with the given goal
*/
BingoGenerator.prototype.hasConflictsOnBoard = function(goal) {
for (var i = 1; i <= 25; i++) {
var square = this.bingoBoard[i];
if (square.goal) {
var squares = [goal, square.goal];
var synergy = this.evaluateSquares(squares);
if (synergy >= TOO_MUCH_SYNERGY) {
return true;
}
}
}
return false;
};
/**
* Return the squares in the given row *EXCEPT* the square at the given position.
*
* for example, getOtherSquares("row1", 4) would return the squares at positions [1, 2, 3, 5],
* but not the square at position 4.
*
* @param row the row on the board to pull squares from
* @param position the position to ignore
* @returns {*|Array}
*/
BingoGenerator.prototype.getOtherSquares = function(row, position) {
var rowIndices = INDICES_PER_ROW[row].filter(function(index) {
return index != position;
});
var board = this;
return rowIndices.map(function(index) {
return board.bingoBoard[index];
});
};
/**
* Given a position on the board and a potential goal, determines the maximum amount of synergy that
* any row containing the position would have
* @param position the position on the bingo board
* @param potentialGoal the goal that we're considering adding to the position
* @returns {number} the maximum synergy that the goal would have at that position
*/
BingoGenerator.prototype.checkLine = function(position, potentialGoal) {
var rows = ROWS_PER_INDEX[position];
var maxSynergy = 0;
var minSynergy = TOO_MUCH_SYNERGY;
for (var rowIndex = 0; rowIndex < rows.length; rowIndex++) {
var row = rows[rowIndex];
// include the desired difficulty along with the goal to make the "potential square"
// because we use desired time now for calculating "difficulty" synergy
var potentialSquare = JSON.parse(JSON.stringify(potentialGoal));
potentialSquare.desiredTime = this.bingoBoard[position].desiredTime;
// get the list of other squares in the row and append the potential one
var potentialRow = this.getOtherSquares(row, position);
potentialRow.push(potentialSquare);
var effectiveRowSynergy = this.evaluateSquares(potentialRow);
maxSynergy = Math.max(maxSynergy, effectiveRowSynergy);
minSynergy = Math.min(minSynergy, effectiveRowSynergy);
}
return {
minSynergy: minSynergy,
maxSynergy: maxSynergy
};
};
/**
* Given a row, calculates the effective synergy between the squares in the row.
* @param row the string name of the row to check
* @returns {number}
*/
BingoGenerator.prototype.evaluateRow = function(row) {
return this.evaluateSquares(this.getOtherSquares(row));
};
BingoGenerator.prototype.getEffectiveTypeSynergiesForRow = function(row) {
var synergiesForSquares = this.calculateSynergiesForSquares(this.getOtherSquares(row));
var effectiveTypeSynergies = this.calculateEffectiveTypeSynergies(this.calculateCombinedTypeSynergies(synergiesForSquares));
var rowtypeSynergies = this.filterRowtypeSynergies(synergiesForSquares);
return [effectiveTypeSynergies, rowtypeSynergies];
};
/**
* Given an array of squares, calculates the effective synergy between the squares.
* This is determined using the type and subtype information of the goals in each square.
* @param squares
*/
BingoGenerator.prototype.evaluateSquares = function(squares) {
// bail out if there are duplicate goals
// NOTE: keep this in addition to the duplicate checking from chooseGoalForPosition
// because this still detects cases from hardcoded boards for analysis
var ids = squares.map(function(el) { return el.id; }).filter(function(el) { return el; });
if (hasDuplicateStrings(ids)) {
return TOO_MUCH_SYNERGY;
}
var synergiesForSquares = this.calculateSynergiesForSquares(squares);
return this.calculateEffectiveSynergyForSquares(synergiesForSquares);
};
// aggregates type synergy data from the squares in a row for later use
BingoGenerator.prototype.calculateSynergiesForSquares = function(squares) {
// a map of type -> list of type synergy values
var typeSynergies = {};
// a map of subtype -> list of subtype synergy values
var subtypeSynergies = {};
// a map of rowtype -> list of rowtype synergy values
var rowtypeSynergies = {};
// list of differences between desiredTime and actual time
var timeDifferences = [];
for (var m = 0; m < squares.length; m++) {
var square = squares[m];
this.mergeTypeSynergies(typeSynergies, square.types);
this.mergeTypeSynergies(subtypeSynergies, square.subtypes);
this.mergeTypeSynergies(rowtypeSynergies, square.rowtypes);
// can't add a time difference for squares that are empty (since it's undefined)
if (square.time !== undefined && square.desiredTime !== undefined) {
timeDifferences.push(square.desiredTime - square.time);
}
}
return {
typeSynergies: typeSynergies,
subtypeSynergies: subtypeSynergies,
rowtypeSynergies: rowtypeSynergies,
goals: squares,
timeDifferences: timeDifferences
};
};
// helper method for implementing calculateSynergiesForSquares
BingoGenerator.prototype.mergeTypeSynergies = function(typeSynergies, newTypeSynergies) {
for (var type in newTypeSynergies) {
if (!typeSynergies[type]) {
typeSynergies[type] = [];
}
typeSynergies[type].push(newTypeSynergies[type]);
}
};
BingoGenerator.prototype.calculateCombinedTypeSynergies = function(synergiesForSquares) {
var typeSynergies = synergiesForSquares.typeSynergies;
var subtypeSynergies = synergiesForSquares.subtypeSynergies;
var combinedTypeSynergies = {};
// Check each subtype found to see if there is a matching type somewhere in the row
// If so, add the subtype to the grand list
for (var type in typeSynergies) {
if (type in subtypeSynergies) {
combinedTypeSynergies[type] = typeSynergies[type].concat(subtypeSynergies[type]);
}
else {
combinedTypeSynergies[type] = typeSynergies[type];
}
}
return combinedTypeSynergies;
};
/**
* Filters rowtypeSynergies to only include entries that are present in every square of the board
* @param synergiesForSquares
*/
BingoGenerator.prototype.filterRowtypeSynergies = function(synergiesForSquares) {
var rowtypeSynergies = {};
for (var rowtype in synergiesForSquares.rowtypeSynergies) {
var rowtypeSynergy = synergiesForSquares.rowtypeSynergies[rowtype];
// don't count it yet until we've filled up the entire row
if (rowtypeSynergy.length < SQUARES_PER_ROW) {
continue;
}
var rowtypeCost = 0;
for (var i = 0; i < rowtypeSynergy.length; i++) {
rowtypeCost += rowtypeSynergy[i];
}
var rowTypeThreshold = this.rowtypeTimeSave[rowtype];
// "regular" row type synergy
if (rowTypeThreshold > 0 && rowTypeThreshold > rowtypeCost) {
rowtypeSynergies[rowtype] = rowTypeThreshold - rowtypeCost;
}
// "reverse" row type synergy
else if (rowTypeThreshold < 0 && rowTypeThreshold > rowtypeCost) {
rowtypeSynergies[rowtype] = rowtypeCost - rowTypeThreshold;
}
}
return rowtypeSynergies;
};
BingoGenerator.prototype.calculateEffectiveTypeSynergies = function(typeSynergies) {
var effectiveTypeSynergies = {};
for (var type in typeSynergies) {
var synergies = typeSynergies[type];
var effectiveSynergies = this.filterSynergyValuesForType(type, synergies);
if (effectiveSynergies.length > 0) {
effectiveTypeSynergies[type] = effectiveSynergies;
}
}
return effectiveTypeSynergies;
};
BingoGenerator.prototype.filterSynergyValuesForType = function(type, synergies) {
synergies.sortNumerically();
var filter = this.synergyFilters[type] || "";
if (/^min/.test(filter)) {
var count = Number(filter.split(" ")[1]);
return synergies.slice(0, count);
}
else if (/^max/.test(filter)) {
var count = Number(filter.split(" ")[1]);
synergies.reverse();
return synergies.slice(0, count);
}
else {
return synergies.slice(0, -1);
}
};
// given aggregated type synergies for the row, calculates the effective synergy for that row
BingoGenerator.prototype.calculateEffectiveSynergyForSquares = function(synergiesForSquares) {
var typeSynergies = this.calculateCombinedTypeSynergies(synergiesForSquares);
var rowtypeSynergies = this.filterRowtypeSynergies(synergiesForSquares);
// Assess final row synergy by removing the largest element from each type and adding the rest
var effectiveTypeSynergies = this.calculateEffectiveTypeSynergies(typeSynergies);
// total synergy in the row
var rowSynergy = 0;
// by this point we've already filtered out the highest value synergy in
// calculateEffectiveTypeSynergies(), so just sum the synergies and return the value
for (var type in effectiveTypeSynergies) {
var synergies = effectiveTypeSynergies[type];
for (var i = 0; i < synergies.length; i++) {
if (synergies[i] > this.maximumIndividualSynergy) {
return TOO_MUCH_SYNERGY;
}
rowSynergy += synergies[i];
}
}
// we've already prefiltered/calculated these values, so just add them up
// see filterRowtypeSynergies for details
for (var rowtype in rowtypeSynergies) {
rowSynergy += rowtypeSynergies[rowtype];
}
var timeDifferences = synergiesForSquares.timeDifferences;
// here's where we factor in expected time vs desired time:
for (var i = 0; i < timeDifferences.length; i++) {
// this is desiredTime - actualTime, so a positive value means a goal that is faster than desired
var timeDifference = timeDifferences[i];
rowSynergy += timeDifference;
}
return rowSynergy;
};
// preserve this function name for compatibility with existing code
ootBingoGenerator = function (bingoList, opts) {
var bingoGenerator = new BingoGenerator(bingoList, opts);
// repeatedly attempt to generate a card until it succeeds, bailing out after 10 fails
var card = false;
var iterations = 0;
while (!card && iterations < 100) {
card = bingoGenerator.makeCard();
iterations++;
}
card["meta"] = {iterations: iterations};
if (!card) {
console.log();
console.log(iterations);
}
return card;
};