forked from asalga/Horadrix
-
Notifications
You must be signed in to change notification settings - Fork 0
/
BoardModel.pde
632 lines (528 loc) · 18.1 KB
/
BoardModel.pde
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
/*
This class is responsible for keeping the integrity of the board data intact.
*/
public class BoardModel{
// Quality with MATCH_ to avoid conflicts with P5 constants
private final int MATCH_LEFT = -1;
private final int MATCH_RIGHT = 1;
private final int MATCH_UP = -1;
private final int MATCH_DOWN = 1;
private Token[][] board;
// Tokens that have been remove from the board, but still need to be rendered for their death animation.
private ArrayList<Token> dyingTokens;
private int numGemsAllowedAtOnce;
private int numGemsOnBoard;
/*
Instead of the board knowing how to draw itself, it
instead provides an iterator for the screen to render the tokens.
*/
public class Iterator{
private int count;
public Iterator(){
count = -1;
}
public boolean next(){
count++;
return count != BOARD_COLS * BOARD_ROWS;
}
public Token item(){
int r = count / BOARD_COLS;
int c = count % BOARD_COLS;
// required type conversion for pjs
return board[Utils.floatToInt(r)][Utils.floatToInt(c)];
//count / BOARD_COLS][count % BOARD_COLS];
}
}
public Iterator getIterator(){
return new Iterator();
}
/*
*/
public BoardModel(){
//screenGameplay = s;
numGemsOnBoard = 0;
numGemsAllowedAtOnce = 0;
board = new Token[BOARD_ROWS][BOARD_COLS];
dyingTokens = new ArrayList<Token>();
}
/*
We are dropping the tokens, so we need to start from the bottom and work out way
up to fill in all the gaps.
First we find a destination for a token to move to. Any cell with an empty cell or that
has a dying token will serve as a destination for another token to move to.
The source token must be above the destination, so we can begin counting above the dest.
*/
private void dropTokens(){
for(int c = 0; c < BOARD_COLS; c++){
boolean needsDrop = false;
int dst = BOARD_ROWS;
int src;
while(dst >= 2){
dst--;
if(board[dst][c].getType() == Token.TYPE_NULL || board[dst][c].isDying() ){
needsDrop = true;
break;
}
}
// Don't subtract 1 because we do that already in the next line
src = dst;
while(src >= 1){
src--;
if(board[src][c].getType() != Token.TYPE_NULL || board[src][c].isDying() ){
break;
}
}
while(src >= 0){
// move the first token
if(needsDrop){
Token tokenToMove = board[src][c];
tokenToMove.fallTo(dst, c);
}
do{
src--;
}while(src >= 1 && board[src][c].getType() == Token.TYPE_NULL);
dst--;
}
}
}
/*
Several columns may be dropping down tokens, but once a column
is finished dropping its gems, it should immediately fill up the holes.
Note this does not change whether the token has a gem or not.
*/
private void fillInvisibleSectionOfColumn(int c){
for(int r = 0; r < START_ROW_INDEX; r++){
if(board[r][c].getType() == Token.TYPE_NULL){
Token t = new Token();
t.setType(getRandomTokenType());
t.setRowColumn(r, c);
board[r][c] = t;
}
}
}
/*
Get a reference to a token at a particular cell
*/
public Token getToken(int r, int c){
assertTest(r > -1 && r < BOARD_ROWS, "OOB error in getToken");
assertTest(c > -1 && c < BOARD_COLS, "OOB error in getToken");
return board[r][c];
}
/*
*/
public void setToken(int r, int c, Token t){
assertTest(r > -1 && r < BOARD_ROWS, "OOB error in getToken");
assertTest(c > -1 && c < BOARD_COLS, "OOB error in getToken");
board[r][c] = t;
}
/*
* Find any 3 matches either going vertically or horizontally and
* remove them from the board. Replace the cells in the board will NULL tokens.
*
* @param {startIndex}
* @param {endIndex}
* @returns the number of tokens this method found that need to be removed.
*/
private int markTokensForRemoval(int startIndex, int endIndex){
int numTokensMarked = 0;
// Mark the matched horizontal gems
// TODO: Add extra column buffer at end of board to fix matches counter?
for(int r = startIndex; r <= endIndex; r++){
// start with the first token in the first column as the thing we want to match against.
int tokenTypeToMatchAgainst = board[r][0].getType();
// start of matched row
int markerIndex = 0;
int matches = 1;
for(int c = 1; c < BOARD_COLS; c++){
// Found a match, keep going...
if(board[r][c].matchesWith(tokenTypeToMatchAgainst) && board[r][c].canBeMatched()){
matches++;
}
// We bank on finding a different gem. Once that happens, we can see if
// we found enough of the previous gems. Didn't find 3 matches, start over.
else if( (board[r][c].matchesWith(tokenTypeToMatchAgainst) == false) && matches < 3){
matches = 1;
markerIndex = c;
tokenTypeToMatchAgainst = board[r][c].getType();
}
// We need to also do it at the end of the board
// Did we reach the end of the board?
else if( (board[r][c].matchesWith(tokenTypeToMatchAgainst) == false && matches >= 3) || (c == BOARD_COLS - 1 && matches >= 3)){
for(int gemC = markerIndex; gemC < markerIndex + matches; gemC++){
//board[r][gemC].kill();
board[r][gemC].markForDeath();
numTokensMarked++;
}
matches = 1;
markerIndex = c;
tokenTypeToMatchAgainst = board[r][c].getType();
}
}
// Each iteration of checking we just increment matches, so the removal code doesn't get a chance to execute
// if there is a line touching the border of the game board.
if(matches >= 3){
for(int gemC = markerIndex; gemC < markerIndex + matches; gemC++){
board[r][gemC].markForDeath();
numTokensMarked++;
}
}
}
//
// Now do the columns...
//
// Add extra column buffer at end of board to fix matches counter?
for(int c = 0; c < BOARD_COLS; c++){
int tokenTypeToMatchAgainst = board[startIndex][c].getType();
int markerIndex = startIndex;
int matches = 1;
for(int r = startIndex + 1; r <= endIndex; r++){
if(board[r][c].matchesWith(tokenTypeToMatchAgainst) && board[r][c].canBeMatched()){
matches++;
}
// We bank on finding a different gem. Once that happens, we can see if
// we found enough of the previous gems.
else if(board[r][c].matchesWith(tokenTypeToMatchAgainst) == false && matches < 3){
matches = 1;
markerIndex = r;
tokenTypeToMatchAgainst = board[r][c].getType();
}
// Either we found a non-match after at least finding a match 3, or the last match was at the end of the column.
else if( (board[r][c].matchesWith(tokenTypeToMatchAgainst) == false && matches >= 3) || (r == endIndex && matches >= 3)){
for(int gemR = markerIndex; gemR < markerIndex + matches; gemR++){
board[gemR][c].markForDeath();
numTokensMarked++;
}
matches = 1;
markerIndex = r;
tokenTypeToMatchAgainst = board[r][c].getType();
}
}
if(matches >= 3){
for(int gemR = markerIndex; gemR < markerIndex + matches; gemR++){
board[gemR][c].markForDeath();
numTokensMarked++;
}
}
}
return numTokensMarked;
}
/*
Speed: O(n)
Returns true as soon as it finds a valid swap/move.
Checks to see if the user can make a valid match anywhere in the visible part of the board.
In case there are no valid swap/moves left, the board needs to be reset.
*/
/*private boolean validSwapExists(){
// First check any potential matches in the horizontal
for(int r = START_ROW_INDEX; r < BOARD_ROWS; r++){
for(int c = 0; c < BOARD_COLS - 1; c++){
Token t1 = board[r][c];
Token t2 = board[r][c + 1];
swapTokens(t1, t2);
int matches = getNumCosecutiveMatches(t1, t2);
swapTokens(t1, t2);
if(matches >= 3){
return true;
}
}
}
// Check any potential matches in the vertical
for(int c = 0; c < BOARD_COLS; c++){
for(int r = START_ROW_INDEX; r < BOARD_ROWS - 1; r++){
Token t1 = board[r][c];
Token t2 = board[r + 1][c];
swapTokens(t1, t2);
int matches = getNumCosecutiveMatches(t1, t2);
swapTokens(t1, t2);
if(matches >= 3){
return true;
}
}
}
return false;
}*/
/*
*/
/*public void swapTokens(Token token1, Token token2){
int token1Row = token1.getRow();
int token1Col = token1.getColumn();
int token2Row = token2.getRow();
int token2Col = token2.getColumn();
// Swap on the board and in the tokens
board[token1Row][token1Col] = token2;
board[token2Row][token2Col] = token1;
token2.swap(token1);
}*/
/*
We can only match up until the visible part of the board
returns the number of matching types excluding this one.
*/
public int numMatchesUpDown(Token token, int direction){
int row = token.getRow();
int matchesFound = 0;
int type = token.getType();
int tokenColumn = token.getColumn();
while(row >= START_ROW_INDEX && row < BOARD_ROWS && board[row][tokenColumn].matchesWith(type)){
matchesFound++;
row += direction;
}
return matchesFound -1;
}
/*
@returns {bool} true if any of the tokens on the board are moving
*/
public boolean hasMovement(){
for(int r = 0; r < BOARD_ROWS; r++){
for(int c = 0; c < BOARD_COLS; c++){
if(board[r][c].isMoving()){
return true;
}
}
}
return false;
}
/*
* Return how many tokens match this one on its left or right side
* Does not include the count of the token itself.
*/
public int numMatchesSideways(Token token, int direction){
int currColumn = token.getColumn();
int tokenRow = token.getRow();
int matchesFound = 0;
int type = token.getType();
// Watch for going out of bounds
while(currColumn >= 0 && currColumn < BOARD_COLS && board[tokenRow][currColumn].matchesWith(type)){
matchesFound++;
currColumn += direction;
}
// matchesFound included the token we started with to
// keep the code in this funciton short, but we have to
// only return the number of matched tokens excluding it.
return matchesFound - 1;
}
/**
* Tokens that are considrered too far to swap include ones that
* are across from each other diagonally or have 1 token between them.
*/
public boolean isCloseEnoughForSwap(Token t1, Token t2){
// !!!
return abs(t1.getRow() - t2.getRow()) + abs(t1.getColumn() - t2.getColumn()) == 1;
}
/*
A swap of two gems is only valid if it results in a row or column of 3 or more
gems of the same type getting lined up.
*/
private int getNumCosecutiveMatches(Token t1, Token t2){
// When the player selects a token on the other side of the board,
// we still call wasValidSwap, which checks here if the tokens are too far apart to match.
if(isCloseEnoughForSwap(t1, t2) == false){
return 0;
}
int matches = numMatchesSideways(t1, MATCH_LEFT) + numMatchesSideways(t1, MATCH_RIGHT);
if(matches >= 2){
return matches + 1;
}
matches = numMatchesSideways(t2, MATCH_LEFT) + numMatchesSideways(t2, MATCH_RIGHT);
if(matches >= 2){
return matches + 1;
}
matches = numMatchesUpDown(t1, MATCH_UP) + numMatchesUpDown(t1, MATCH_DOWN);
if(matches >= 2){
return matches + 1;
}
matches = numMatchesUpDown(t2, MATCH_UP) + numMatchesUpDown(t2, MATCH_DOWN);
return matches + 1;
}
/*
*/
public void update(float td){
int numTokensArrivedAtDest = 0;
// Update all tokens on board. This includes the falling tokens
for(int r = BOARD_ROWS - 1; r >= 0 ; r--){
for(int c = 0; c < BOARD_COLS; c++){
Token t = board[r][c];
t.update(td);
if(t.isFalling() && t.arrivedAtDest()){
t.dropIntoCell();
setToken(t.getRow(), t.getColumn(), t);
// If the token was actually falling and not swapping, we need to
// put a null token in its OLD location
// If the token hasn't been overwritten yet
// if(getToken(r, c) == this){
createNullToken(r, c);
numTokensArrivedAtDest++;
// If the top token arrived at its destination, it means we can safely fill up tokens above it.
if(t.getFillCellMarker()){
fillInvisibleSectionOfColumn(t.getColumn());
setFillMarker(t.getColumn());
}
}
}
}
if(numTokensArrivedAtDest > 0){
markTokensForRemoval(START_ROW_INDEX, BOARD_ROWS-1);
if(removeMarkedTokens(true) > 2){
soundManager.playMatchSound();
}
dropTokens();
}
}
/*
Find any null tokens on the board and replace them with a random token.
This is used whenever we need to create a board that has no matches. A board
is generated, matches are removed, then empty cells are replaced by calling this method.
@return {int} Num holes/cells filled.
*/
private int fillHolesForRows(int startIndex, int endIndex){
int numFilled = 0;
for(int r = startIndex; r < endIndex; r++){
for(int c = 0; c < BOARD_COLS; c++){
if(board[r][c].getType() == Token.TYPE_NULL){
board[r][c].setType(getRandomTokenType());
numFilled++;
}
}
}
return numFilled;
}
/*
Don't start with 0, since that's null
*/
public int getRandomTokenType(){
return Utils.getRandomInt(1, numTokenTypesOnBoard);
}
/*
Stupidly fill the board with random tokens first.
*/
private void fillBoardWithRandomTokens(){
for(int r = 0; r < BOARD_ROWS; r++){
for(int c = 0; c < BOARD_COLS; c++){
Token token = new Token();
token.setType(getRandomTokenType());
token.setRowColumn(r, c);
board[r][c] = token;
}
}
}
/*
*/
public ArrayList<Token> getDyingTokens(){
return dyingTokens;
}
/*
This can only be done if nothing is moving or animating to make
sure the board stays in a proper state.
*/
public void generateNewBoardWithDyingAnimation(boolean playDyingAnimation){
fillBoardWithRandomTokens();
// Kill all the tokens on the visible part of the board
for(int c = 0; c < BOARD_COLS; c++){
for(int r = START_ROW_INDEX; r < BOARD_ROWS; r++){
// Set score to zero so once they die, the score total isn't changed.
board[r][c].setScore(0);
board[r][c].kill();
if(playDyingAnimation){
dyingTokens.add(board[r][c]);
}
createNullToken(r,c);
}
}
// The invisible part of the board will drop down, so we need to
// remove all immediate matches so there are no matches as soon as it falls.
while(markTokensForRemoval(0, 7) > 0){
removeMarkedTokens(false);
fillHolesForRows(0, BOARD_ROWS/2);
}
// We don't want any gems to appear on the board on init, just based on design
removeAllGemsFromBoard();
// TODO: comment !!!
setFillMarkers();
dropTokens();
}
/**
*/
private void setFillMarker(int c){
board[0][c].setFillCellMarker(true);
}
/**
*/
private void setFillMarkers(){
for(int c = 0; c < BOARD_COLS; c++){
board[0][c].setFillCellMarker(true);
}
}
/*
Move the tokens that have been marked for deletion from
the board to the dying tokens list.
@param {doDyingAnimation}
@returns {int} The number of tokens removed from the board.
*/
private int removeMarkedTokens(boolean doDyingAnimation){
int numRemoved = 0;
// Now delete everything marked for deletion.
for(int r = 0; r < BOARD_ROWS; r++){
for(int c = 0; c < BOARD_COLS; c++){
Token tokenToDestroy = board[r][c];
// Don't need to check if already in the list because we're removing it from
// the board, so it can never be placed from the board into the dying token list more than once.
if(tokenToDestroy.isMarkedForDeath()){
tokenToDestroy.kill();
numRemoved++;
// On setup we use this method, but we don't actually want to play the animation.
if(doDyingAnimation){
dyingTokens.add(tokenToDestroy);
}
// Replace the token we removed with a null Token
createNullToken(r, c);
}
// !!! TODO: check
board[r][c].setSelect(false);
}
}
return numRemoved;
}
/*
*/
public void setNumGemsAllowedAtOnce(int num){
numGemsAllowedAtOnce = num;
}
public void reduceGemCount(int i){
numGemsOnBoard -= i;
}
public int getNumGems(){
return numGemsOnBoard;
}
public void ensureGemCount(){
while(numGemsOnBoard < numGemsAllowedAtOnce){
// We can only add the gem to the part of the board the user doesn't see.
// Don't forget getRandom int is inclusive.
int r = Utils.getRandomInt(0, START_ROW_INDEX - 1);
int c = Utils.getRandomInt(0, BOARD_COLS - 1);
Token token = getToken(r, c);
if(token.hasGem() == false && token.isMoving() == false){
token.setHasGem(true);
numGemsOnBoard++;
}
}
}
/**
*/
private void createNullToken(int r, int c){
Token nullToken = new Token();
nullToken.setType(Token.TYPE_NULL);
nullToken.setRowColumn(r, c);
board[r][c] = nullToken;
}
/*
In some cases (like the start of a level) we need to make sure
there are no gems on the visible part of the board.
*/
private void removeAllGemsFromBoard(){
numGemsOnBoard = 0;
for(int c = 0; c < BOARD_COLS; c++){
for(int r = 0; r < BOARD_ROWS; r++){
board[r][c].setHasGem(false);
}
}
}
}