-
Notifications
You must be signed in to change notification settings - Fork 14
/
rules.js
435 lines (398 loc) · 12.8 KB
/
rules.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
/* Copyright (c) 2017 MIT 6.813/6.831 course staff, all rights reserved.
* Redistribution of original or derived work requires permission of course staff.
*/
/*
*
* This class implements the rules of Candy Crush.
*
*/
var Rules = function(board)
{
// Set during setup, to avoid scoring.
var scoring = false;
////////////////////////////////////////////////
// PUBLIC METHODS
//
// You will likely call these methods in index.html
//
/*
*
* Prepares a new game with no groups of three adjacent same-color candies.
* Any time there is a group of adjacent same-color candies, re-rolls them.
* Sets the score to zero such that the player does not get points for
* crushes that occure by luck initially.
*
*/
this.prepareNewGame = function()
{
scoring = false;
while (true)
{
this.populateBoard()
var crushable = this.getCandyCrushes();
if (crushable.length == 0) break;
this.removeCrushes(crushable);
}
scoring = true;
}
/*
*
* Returns true if flipping fromCandy with the candy in the direction
* specified (['up', 'down', 'left', 'right']) is valid
* (according to the rules), else returns false.
*
*/
this.isMoveTypeValid = function(fromCandy, direction)
{
return this.numberCandiesCrushedByMove(fromCandy, direction) > 0;
}
/*
*
* Returns a list of ALL candy crushes on the board. A candy crush is
* a list of three or more candies in a single row or column that have
* the same color. Each crush is provided as a list of the candies being
* crushed, resulting in a list of lists. The output of this method should
* be passed directly into this.removeCrushes to remove candy crushes.
*
*/
this.getCandyCrushes = function(swap) {
// Implemented with a (not fully optimized) Tarjan's union-find algorithm.
// Implementation of the classic union-find algorithm (unoptimized).
// Allows any string keys to be unioned into a set of disjoint sets.
// https://en.wikipedia.org/wiki/Disjoint-set_data_structure
var unioned = {};
var sizes = {};
var row, col;
// Finds the set representative for the set that this key is a member of.
function find(key)
{
var parent = unioned[key];
if (parent == null) return key;
parent = find(parent);
unioned[key] = parent; // Path compression
return parent;
}
// The size of the set represented by 'found'; assume 1 if not stored.
function size(found)
{
return sizes[found] || 1;
}
// Ennsures that the two keys are in the same set, joining if needed.
function union(key1, key2)
{
var p1 = find(key1), p2 = find(key2);
if (p1 == p2) return p1;
// Do not bother implementing union by rank. This is pretty fast too.
// n.b., http://stackoverflow.com/a/2326676/265298
unioned[p2] = p1;
sizes[p1] = size(p1) + size(p2);
delete sizes[p2];
}
// Get strips of length 3.
var vert = this.findColorStrips(true, swap);
var horiz = this.findColorStrips(false, swap);
var sets = vert.concat(horiz);
// Execute union of all the strips, possibly joining
// horizontal and vertical strips that intersect.
for (var j = 0; j < sets.length; j++)
{
var set = sets[j];
for (var k = 1; k < set.length; k++)
{
union(set[0].id, set[k].id)
}
}
// Pass 2: list out resulting sets of minSize or larger.
var results = {}
for (row = 0; row < board.boardSize; row++)
{
for (col = 0; col < board.boardSize; col++)
{
var candy = board.getCandyAt(row, col);
if (candy)
{
var p = find(candy.id);
if (size(p) >= 3)
{
if (!(p in results)) results[p] = [];
results[p].push(candy);
}
}
}
}
// Pass 3: Return results as a list of list of candies.
var list = [];
for (var key in results)
{
list.push(results[key]);
}
return list;
}
/*
*
* Deletes all the candies in setOfSetsOfCrushes (which can be
* generated by getCandyCrushes or by getCandiesToCrushGivenMove.)
* Does not shift candies down at all. Updates the score accordingly.
*
*/
this.removeCrushes = function(setOfSetsOfCrushes)
{
for (var j = 0; j < setOfSetsOfCrushes.length; j++)
{
var set = setOfSetsOfCrushes[j];
for (var k = 0; k < set.length; k++)
{
if (scoring) board.incrementScore(set[k], set[k].row, set[k].col);
board.remove(set[k]);
}
}
}
/*
*
* Moves candies down as far as there are empty spaces. Issues calls to
* board.moveTo, which generate "move" events to listen for. If there
* are holes created by moving the candies down, populates the holes
* with random candies, and issues "add" events for these candies.
*
*/
this.moveCandiesDown = function()
{
// Collapse each column
for (var col = 0; col < board.boardSize; col++)
{
var emptyRow = null;
// In each column, scan for the bottom most empty row
for (var emptyRow = board.boardSize - 1; emptyRow >= 0; emptyRow--)
{
if (board.getCandyAt(emptyRow, col) == null)
{
break;
}
}
// Then shift any nonempty rows up
for (var row = emptyRow - 1; row >= 0; row--)
{
var candy = board.getCandyAt(row, col);
if (candy != null)
{
board.moveTo(candy, emptyRow, col);
emptyRow--;
}
}
for (var spawnRow = -1; emptyRow >= 0; emptyRow--, spawnRow--)
{
// We report spawnRow as the (negative) position where
// the candy "would have" started to fall into place.
board.addRandomCandy(emptyRow, col, spawnRow, col);
}
}
}
/*
*
* If there is a valid move, returns an object with two properties:
* candy: a Candy that can be moved
* direction: the direction that it can be moved.
* If there are no valid moves, returns null. The move is selected
* randomly from the available moves, favoring moves with smaller crushes.
*
*/
this.getRandomValidMove = function()
{
var directions = ['up', 'down', 'left', 'right'];
var validMovesThreeCrush = [];
var validMovesMoreThanThreeCrush = [];
// For each cell in the board, check to see if moving it in
// any of the four directions would result in a crush
// if so, add it to the appropriate list (validMoves_threeCrush for
// crushes of size 3, validMoves_moreThanThreeCrush for crushes
// larger than 3)
for (var row = 0; row < board.boardSize; row++)
{
for (var col = 0; col < board.boardSize; col++)
{
var fromCandy = board.getCandyAt(row,col);
if (!fromCandy) continue;
for (i = 0; i < 4; i++)
{
var direction = directions[i];
var numCandiesCrushed =
this.numberCandiesCrushedByMove(fromCandy, direction);
if (numCandiesCrushed == 3)
{
validMovesThreeCrush.push({candy: fromCandy, direction: direction});
}
else if (numCandiesCrushed > 3)
{
validMovesMoreThanThreeCrush.push(
{candy: fromCandy, direction: direction});
}
}
}
}
// if there are three-crushes possible, prioritize these
var searchArray = validMovesThreeCrush.length ? validMovesThreeCrush :
validMovesMoreThanThreeCrush;
// If there are no valid moves, return null.
if (searchArray.length == 0) return null;
// select a random crush from among the crushes found
return searchArray[Math.floor(Math.random() * searchArray.length)];
}
////////////////////////////////////////////////
// USEFUL FOR DEBUGGING
//
//
/*
*
* Specify a board configuration by passing in a boardSpec. The format
* of boardSpec is a list of strings, one sequence for each row. In each
* string, there must be boardSize characters, where each character should
* be the first letter of the color for that square. For example, a boardSpec
* that specifies an 8x8 board with alternating columns of red and orange would have
* a boardSpec of:
* ['rorororo',
* 'rorororo',
* 'rorororo',
* 'rorororo',
* 'rorororo',
* 'rorororo',
* 'rorororo',
* 'rorororo']
*
*/
this.createSpecifiedBoard = function(boardSpec) {
color_dict = {'r':'red', 'o':'orange', 'y':'yellow', 'g':'green','b':'blue','p':'purple'}
var numChars=0;
boardSpec.map(function (i) { return numChars+=i.length });
if (boardSpec.length != board.boardSize || numChars != Math.pow(board.boardSize,2)){
console.warn("boardSpec must be of dimensions boardSize x boardSize to populate board");
return;
}
for (var col = 0; col < board.boardSize; col++)
{
for (var row = 0; row < board.boardSize; row++)
{
if (board.getCandyAt(row, col) == null)
{
var color = color_dict[boardSpec[row].charAt(col)];
board.addCandy(color, row, col);
}
}
}
}
////////////////////////////////////////////////
// Private methods
//
// You likely do NOT need to call these methods
//
/*
* Helper method to rules.prepareNewGame
* Called when a new game is created. Fills all the empty positions on
* the board with random-colored candies.
*
*/
this.populateBoard = function()
{
for (var col = 0; col < board.boardSize; col++)
{
for (var row = 0; row < board.boardSize; row++)
{
// Check the empty candy position (hole), fill with new candy
if (board.getCandyAt(row, col) == null)
{
board.addRandomCandy(row, col);
}
}
}
}
/*
*
* Helper method for rules.isMoveTypeValid
* Returns the number of candies that would be crushed if the candy
* provided by fromCandy were to be flipped in the direction
* specified (['up', 'down', 'left', 'right'])
*
* If this move is not valid (based on the game rules), then 0 is returned
*
*/
this.numberCandiesCrushedByMove = function(fromCandy, direction)
{
return this.getCandiesToCrushGivenMove(fromCandy, direction).length;
}
/*
*
* Helper method for rules.numberCandiesCrushedByMove
* Returns a list of candies that would be "crushed" (i.e. removed) if
* fromCandy were to be moved in the direction specified by direction (['up',
* 'down', 'left', 'right'])
* If move would result in no crushed candies, an empty list is returned.
*
*/
this.getCandiesToCrushGivenMove = function(fromCandy, direction)
{
var toCandy = board.getCandyInDirection(fromCandy, direction);
if (!toCandy || toCandy.color == fromCandy.color)
{
return [];
}
var swap = [fromCandy, toCandy];
var crushable = this.getCandyCrushes(swap);
// Only return crushable groups that involve the swapped candies.
// If the board has incompletely-resolved crushes, there can be
// many crushable candies that are not touching the swapped ones.
var connected = crushable.filter(function(set)
{
for (var k = 0; k < swap.length; k++)
{
if (set.indexOf(swap[k]) >= 0) return true;
}
return false;
});
return [].concat.apply([], connected); //flatten nested lists
}
/*
*
* Helper Method for rules.getCandyCrushes
* Returns a set of sets of all the same-color candy strips of length
* at least 3 on the board. If 'vertical' is set to true, looks only for
* vertical strips; otherwise only horizontal ones. If the 'swap' array
* is passed, then every even-indexed candy in the array is considered
* swapped with every odd-indexed candy in the array.
*
*/
this.findColorStrips = function(vertical, swap) {
var getAt = function(x, y)
{
// Retrieve the candy at a row and column (depending on vertical)
var result = vertical ? board.getCandyAt(y, x) : board.getCandyAt(x, y);
if (swap)
{
// If the result candy is in the 'swap' array, then swap the
// result with its adjacent pair.
var index = swap.indexOf(result);
if (index >= 0) return swap[index ^ 1];
}
return result;
};
var result = [];
for (var j = 0; j < board.boardSize; j++)
{
for (var h, k = 0; k < board.boardSize; k = h)
{
// Scan for rows of same-colored candy starting at k
var firstCandy = getAt(j, k);
h = k + 1;
if (!firstCandy) continue;
var candies = [firstCandy];
for (; h < board.boardSize; h++)
{
var lastCandy = getAt(j, h);
if (!lastCandy || lastCandy.color != firstCandy.color) break;
candies.push(lastCandy);
}
// If there are at least 3 candies in a row, remember the set.
if (candies.length >= 3) result.push(candies);
}
}
return result;
}
}