A port of Zachtronics' match-4 game HACK*MATCH to the TI-84 Plus CE
at main 683 lines 16 kB view raw
1#include <sys/rtc.h> 2#include <ti/screen.h> 3#include <fileioc.h> 4#include <graphx.h> 5#include <keypadc.h> 6#include <stdlib.h> 7#include <stdio.h> 8#include <math.h> 9#include <time.h> 10 11#include "gfx/gfx.h" 12#include "drawing.h" 13#include "variables.h" 14 15#include <debug.h> 16 17bool gameOver; 18 19unsigned int score; 20unsigned int highScore; 21 22unsigned char files[NUM_COLS][MAX_ROWS]; 23unsigned char exaCol; 24bool isHoldingFile; 25unsigned char heldFile; 26 27unsigned char gridMoveOffset; 28clock_t nextMoveTime; 29 30unsigned char prevRight, prevLeft, prev2nd, prevAlpha; 31 32bool matched; 33unsigned char starMatches; 34clock_t clearTime; 35unsigned int nextValue; 36 37bool getTargetedFile(unsigned char *output) // each of these returns false on failure 38{ 39 for (unsigned char row = MAX_ROWS - 1; row < MAX_ROWS; row--) 40 { 41 if (files[exaCol][row] == FILE_EMPTY) continue; 42 if (files[exaCol][row] & 0x80) return false; // no moving matched files! 43 44 *output = row; 45 return true; 46 } 47 48 return false; 49} 50 51bool getTargetedSpace(unsigned char *output) 52{ 53 for (unsigned char row = 0; row < MAX_ROWS; row++) 54 { 55 if (files[exaCol][row] != FILE_EMPTY) continue; 56 57 *output = row; 58 return true; 59 } 60 61 return false; 62} 63 64bool grab() // all of these return true if something changed 65{ 66 unsigned char targetedRow; 67 if (!getTargetedFile(&targetedRow)) return false; // column is empty 68 69 const unsigned char fileValue = files[exaCol][targetedRow]; 70 71 clock_t refundTimer = clock(); 72 73 // these animations are always drawn unbuffered. YEAH! 74#ifndef NO_BUFFER 75 gfx_SetDrawScreen(); 76#endif 77 for (unsigned char row = targetedRow; row < MAX_ROWS - 1; row++) 78 { 79 clock_t animationTimer = clock(); 80 81 files[exaCol][row] = FILE_EMPTY; 82 files[exaCol][row + 1] = fileValue; 83 drawCol(exaCol); 84 85 while (clock() - animationTimer < MOVE_ANIMATION_FRAME_TIME); 86 } 87 88 files[exaCol][MAX_ROWS - 1] = FILE_EMPTY; 89 isHoldingFile = true; 90 heldFile = fileValue; 91#ifndef NO_BUFFER 92 gfx_SetDrawBuffer(); 93#endif 94 95 nextMoveTime += clock() - refundTimer; 96 97 return true; 98} 99 100bool drop() 101{ 102 unsigned char targetedRow; 103 if (!getTargetedSpace(&targetedRow)) return false; 104 105 clock_t refundTimer = clock(); 106 clock_t animationTimer = clock(); 107 108#ifndef NO_BUFFER 109 gfx_SetDrawScreen(); 110#endif 111 const unsigned char fileValue = heldFile; 112 isHoldingFile = false; 113 files[exaCol][MAX_ROWS - 1] = fileValue; 114 drawCol(exaCol); 115 116 for (unsigned char row = MAX_ROWS - 1; row > targetedRow; row--) 117 { 118 files[exaCol][row] = FILE_EMPTY; 119 files[exaCol][row - 1] = fileValue; 120 121 while (clock() - animationTimer < MOVE_ANIMATION_FRAME_TIME); 122 123 drawCol(exaCol); 124 125 animationTimer = clock(); 126 } 127#ifndef NO_BUFFER 128 gfx_SetDrawBuffer(); 129#endif 130 131 nextMoveTime += clock() - refundTimer; 132 133 return true; 134} 135 136bool swap() 137{ 138 unsigned char topRow; 139 if (!getTargetedFile(&topRow)) return false; 140 if (topRow == 0) return false; // nothing under the top file to swap 141 if (files[exaCol][topRow - 1] & 0x80) return false; // no moving matched files! 142 143 const unsigned char swapFile = files[exaCol][topRow]; 144 145 files[exaCol][topRow] = files[exaCol][topRow - 1]; 146 files[exaCol][topRow - 1] = swapFile; 147 return true; 148} 149 150bool doInput() 151{ 152 kb_Scan(); 153 154 const unsigned char oldExaCol = exaCol; 155 156 if (kb_IsDown(kb_KeyRight) && !prevRight && exaCol < NUM_COLS - 1) exaCol++; 157 if (kb_IsDown(kb_KeyLeft) && !prevLeft && exaCol > 0) exaCol--; 158 159 bool changedExaLook = false; 160 161 // the three main operations 162 if (kb_IsDown(kb_Key2nd) && !prev2nd) 163 { 164 if (isHoldingFile) changedExaLook = drop(); 165 else changedExaLook = grab(); 166 } 167 if (kb_IsDown(kb_KeyAlpha) && !prevAlpha) swap(); 168 169 prevLeft = kb_IsDown(kb_KeyLeft); 170 prevRight = kb_IsDown(kb_KeyRight); 171 prev2nd = kb_IsDown(kb_Key2nd); 172 prevAlpha = kb_IsDown(kb_KeyAlpha); 173 174 if (exaCol != oldExaCol || changedExaLook) // redraw the exa 175 { 176 const unsigned char oldExaX = EXA_HOFFSET + GRID_SIZE * oldExaCol; 177 gfx_Sprite(behindExa, oldExaX, EXA_VOFFSET); 178 179 const unsigned char exaX = EXA_HOFFSET + GRID_SIZE * exaCol; 180 gfx_GetSprite(behindExa, exaX, EXA_VOFFSET); 181 182 drawExa(); 183 } 184 185 return !kb_On; 186} 187 188void findMatchRegion 189(const unsigned char row, const unsigned char col, const unsigned char target, unsigned char *count) 190{ 191 files[col][row] |= 0xc0; // mark as visited 192 193 // visited tiles won't == target 194 if (col > 0 && (files[col - 1][row] & 0x7f) == target) 195 { 196 findMatchRegion(row, col - 1, target, count); 197 } 198 if (col < NUM_COLS - 1 && (files[col + 1][row] & 0x7f) == target) 199 { 200 findMatchRegion(row, col + 1, target, count); 201 } 202 if (row > 0 && (files[col][row - 1] & 0x7f) == target) 203 { 204 findMatchRegion(row - 1, col, target, count); 205 } 206 if (row < MAX_ROWS - 1 && (files[col][row + 1] & 0x7f) == target) 207 { 208 findMatchRegion(row + 1, col, target, count); 209 } 210 211 (*count)++; 212} 213 214void findMatchRegionClean 215(const unsigned char row, const unsigned char col, const unsigned char target, unsigned char *count) 216{ 217 findMatchRegion(row, col, target, count); 218 219 for (unsigned char cleaningRow = 0; cleaningRow < MAX_ROWS; cleaningRow++) 220 { 221 for (unsigned char cleaningCol = 0; cleaningCol < NUM_COLS; cleaningCol++) 222 { 223 if (files[cleaningCol][cleaningRow] & 0x40) files[cleaningCol][cleaningRow] &= 0x3f; 224 } 225 } 226} 227 228void getNextMoveTime() 229{ 230 if (score < ROW_INTERVAL_SCALE_END) 231 { 232 nextMoveTime = clock() + MAX_NEW_ROW_INTERVAL / 4; 233 234 if (score > ROW_INTERVAL_SCALE_START) 235 { 236 nextMoveTime -= (score - ROW_INTERVAL_SCALE_START) * ROW_INTERVAL_SCALE_FACTOR / 4; 237 } 238 } 239 else 240 { 241 nextMoveTime = clock() + MIN_NEW_ROW_INTERVAL / 4; 242 } 243} 244 245void addNewRow() 246{ 247 // check for game over 248 for (unsigned char col = 0; col < NUM_COLS; col++) 249 { 250 if (files[col][MAX_ROWS - 1] == FILE_EMPTY) continue; 251 252 // if we make it here, there was something in the lowest row 253 gameOver = true; 254 return; 255 } 256 257 unsigned char swapRow[NUM_COLS]; 258 for (unsigned char col = 0; col < NUM_COLS; col++) 259 { 260 // save the old rows to move 261 swapRow[col] = files[col][0]; 262 263 // overwrite with zeroes so it doesn't mess with checks while making a new row later 264 files[col][0] = FILE_EMPTY; 265 } 266 267 // move all the blocks down 268 for (unsigned char row = 1; row < MAX_ROWS; row++) 269 { 270 for (unsigned char col = 0; col < NUM_COLS; col++) 271 { 272 unsigned char swapFile = files[col][row]; 273 files[col][row] = swapRow[col]; 274 swapRow[col] = swapFile; 275 } 276 } 277 278 // make a new first row, regenerating each block until it doesn't make a set 279 for (unsigned char col = 0; col < NUM_COLS; col++) 280 { 281 while (true) 282 { 283 files[col][0] = rand() % NUM_BLOCK_COLORS + 1; 284 if (rand() % STAR_CHANCE == 0) files[col][0] |= 0x08; // set it to be a star 285 286 unsigned char numInGroup = 0; 287 findMatchRegionClean(0, col, files[col][0], &numInGroup); 288 289 if ((files[col][0] & 0x08) && numInGroup >= 2) continue; 290 if (numInGroup >= AMOUNT_FILES_TO_MATCH) continue; 291 292 break; 293 } 294 } 295 296 // move all the blocks back up so it's not too jarring 297 gridMoveOffset += GRID_SIZE; 298} 299 300void collapseGrid() 301{ 302 // since this is a top-to-bottom search, we can do all the collapsing in one pass 303 304 for (unsigned char col = 0; col < NUM_COLS; col++) 305 { 306 for (unsigned char row = 0; row < MAX_ROWS; row++) 307 { 308 // searching for empty files 309 if (files[col][row] != FILE_EMPTY) continue; 310 311 // search for the next file down (if any) 312 for (unsigned char targetRow = row + 1; targetRow < MAX_ROWS; targetRow++) 313 { 314 if (files[col][targetRow] == FILE_EMPTY) continue; 315 316 // move the file up 317 files[col][row] = files[col][targetRow]; 318 files[col][targetRow] = FILE_EMPTY; 319 break; 320 } 321 } 322 } 323} 324 325void setClearTime() 326{ 327 if (matched) return; 328 329 matched = true; 330 clearTime = clock() + MATCH_TIME; 331} 332 333void clearMarks(const bool toMark) 334{ 335 for (unsigned char checkRow = 0; checkRow < MAX_ROWS; checkRow++) 336 { 337 for (unsigned char checkCol = 0; checkCol < NUM_COLS; checkCol++) 338 { 339 if (toMark) files[checkCol][checkRow] &= 0xbf; 340 else if (files[checkCol][checkRow] & 0x40) files[checkCol][checkRow] &= 0x3f; 341 } 342 } 343} 344 345void checkForMatch() 346{ 347 // check for sets to pop and score 348 for (unsigned char row = MAX_ROWS - 1; row < MAX_ROWS; row--) 349 { 350 for (unsigned char col = 0; col < NUM_COLS; col++) 351 { 352 if (files[col][row] == FILE_EMPTY) continue; 353 if (files[col][row] & 0x80) continue; 354 355 if (files[col][row] & 0x08) 356 { 357 // it's a star... does it have a match? 358 unsigned char numMatchingStars = 0; 359 findMatchRegion(row, col, files[col][row], &numMatchingStars); 360 if (numMatchingStars < 2) 361 { 362 // if not, we don't care about it, bc it can't form a set 363 clearMarks(false); 364 } 365 else 366 { 367 starMatches += numMatchingStars; 368 369 // if we get here, it does have a match! 370 const unsigned char target = files[col][row] & 0x07; 371 for (unsigned char starRow = 0; starRow < MAX_ROWS; starRow++) 372 { 373 for (unsigned char starCol = 0; starCol < NUM_COLS; starCol++) 374 { 375 if (files[starCol][starRow] & 0x40) files[starCol][starRow] &= 0xbf; 376 if (files[starCol][starRow] != target) continue; 377 378 files[starCol][starRow] |= 0x80; 379 starMatches++; // keep track so we don't award points for them later 380 } 381 } 382 383 setClearTime(); 384 } 385 } 386 else 387 { 388 // find all contiguous matching blocks and their count 389 unsigned char numMatchingBlocks = 0; 390 findMatchRegion(row, col, files[col][row], &numMatchingBlocks); 391 392 // unmark or cement the matching region 393 const bool toMark = numMatchingBlocks >= AMOUNT_FILES_TO_MATCH; 394 clearMarks(toMark); 395 if (toMark) setClearTime(); 396 } 397 } 398 } 399} 400 401void scoreGrid() 402{ 403 animateClear(); 404 405 // remove the matching region and count how many blocks 406 unsigned char numMatchingBlocks = 0; 407 for (unsigned char checkRow = 0; checkRow < MAX_ROWS; checkRow++) 408 { 409 for (unsigned char checkCol = 0; checkCol < NUM_COLS; checkCol++) 410 { 411 if (files[checkCol][checkRow] & 0x80) 412 { 413 files[checkCol][checkRow] = FILE_EMPTY; 414 numMatchingBlocks++; 415 } 416 } 417 } 418 419 // calculate the score for the matching region 420 unsigned char atBase = 0; 421 if (nextValue == BASE_BLOCK_VALUE) atBase = AMOUNT_FILES_TO_MATCH; 422 numMatchingBlocks -= starMatches; 423 while (numMatchingBlocks > 0) 424 { 425 if (atBase > 0) (atBase)--; 426 else nextValue += CHAIN_BLOCK_BONUS; 427 428 score += nextValue; 429 numMatchingBlocks--; 430 431 dbg_printf("added block with value %u\n", nextValue); 432 } 433 434 dbg_printf("total score: %u\n", score); 435 436 // tidy up 437 matched = false; 438 starMatches = 0; 439 collapseGrid(); 440} 441 442void updateGrid() 443{ 444 clock_t refundTimer = clock(); 445 446 // remove the matched blocks and collapse the grid if it's time 447 if (matched && clock() > clearTime) 448 { 449 scoreGrid(); 450 } 451 452 // check to see if there are any matches now 453 checkForMatch(); 454 455 // reset the score if not 456 if (!matched) 457 { 458 nextValue = BASE_BLOCK_VALUE; 459 460 // ...and move the grid down if it's time 461 if (clock() > nextMoveTime) 462 { 463 gridMoveOffset -= GRID_SIZE / GRID_MOVE_STEPS; 464 if (gridMoveOffset == 0) addNewRow(); 465 getNextMoveTime(); 466 } 467 } 468 469 // if there is a match, don't move the grid down 470 else 471 { 472 nextMoveTime += clock() - refundTimer; 473 } 474} 475 476void startGame() 477{ 478 // restore the palette 479 gfx_SetPalette(global_palette, 32 + sizeof_grid_palette, 0); 480 481 // draw the background 482 gfx_FillScreen(COLOR_BLACK); 483 gfx_SetColor(COLOR_METAL); 484 gfx_FillRectangle_NoClip(BG_HOFFSET, BG_VOFFSET, background_width, background_height); 485 gfx_SetColor(COLOR_BLACK); 486 gfx_FillRectangle_NoClip(BLK_RECT_1_X, BLK_RECT_1_Y, BLK_RECT_1_W, BLK_RECT_1_H); 487 gfx_FillRectangle_NoClip(BLK_RECT_2_X, BLK_RECT_2_Y, BLK_RECT_2_W, BLK_RECT_2_H); 488 gfx_FillRectangle_NoClip(BLK_RECT_3_X, BLK_RECT_3_Y, BLK_RECT_3_W, BLK_RECT_3_H); 489 gfx_RLETSprite_NoClip(background, BG_HOFFSET, BG_VOFFSET); 490 gfx_SetColor(COLOR_DOT); 491 for (unsigned int x = DOTS_HOFFSET; x <= DOTS_HOFFSET + 2 * NUM_DOTS; x += 2) 492 { 493 gfx_SetPixel(x, DOTS_VOFFSET); 494 } 495 496 // draw the high score 497 drawNumber(HIGH_SCORE_HOFFSET, HIGH_SCORE_VOFFSET, highScore); 498 499 score = 0; 500 gameOver = 0; 501 deathStage = 0; // reset animation from last game 502 matched = false; 503 nextValue = BASE_BLOCK_VALUE; 504 505 srand(rtc_Time()); 506 507 // clear the grid from last game 508 for (unsigned char row = 0; row < MAX_ROWS; row++) 509 { 510 for (unsigned char col = 0; col < NUM_COLS; col++) 511 { 512 files[col][row] = FILE_EMPTY; 513 } 514 } 515 516 // populate the initial grid 517 for (unsigned char i = 0; i <= NUM_INITIAL_ROWS; i++) 518 { 519 addNewRow(); 520 } 521 522 gridMoveOffset = GRID_SIZE; 523 524 // set and draw initial exa position 525 exaCol = EXA_START_COL; 526 gfx_GetSprite(behindExa, EXA_HOFFSET + EXA_START_COL * GRID_SIZE, EXA_VOFFSET); 527 gfx_TransparentSprite_NoClip(exa_empty, EXA_HOFFSET + EXA_START_COL * GRID_SIZE, EXA_VOFFSET); 528 529 nextMoveTime = clock() + MIN_NEW_ROW_INTERVAL; 530} 531 532bool endGame() 533{ 534 if (score > highScore) 535 { 536 const unsigned char saveVar = ti_Open(SAVE_VAR_NAME, "w"); 537 ti_Write(&score, 3, 1, saveVar); 538 ti_Close(saveVar); 539 highScore = score; 540 } 541 542 if (gameOver) 543 { 544 isHoldingFile = false; 545 animateDeath(); 546 547 // wait for input to either exit or restart 548 while (true) 549 { 550 deathPeriodic(); 551 552 kb_Scan(); 553 554 if (kb_IsDown(kb_KeyClear)) return false; 555 if (kb_IsDown(kb_Key2nd)) return true; 556 } 557 } 558 559 return false; 560} 561 562unsigned char init() 563{ 564 kb_EnableOnLatch(); 565 kb_ClearOnLatch(); 566 567 // initialize the sprites 568 if(!HKMCHGFX_init()) return 1; 569 570 fileSprites[0] = 0; 571 fileSprites[1] = file_red; 572 fileSprites[2] = file_yellow; 573 fileSprites[3] = file_cyan; 574 fileSprites[4] = file_blue; 575 fileSprites[5] = file_purple; 576 fileSprites[6] = 0; 577 fileSprites[7] = 0; 578 fileSprites[8] = 0; 579 fileSprites[9] = star_red; 580 fileSprites[10] = star_yellow; 581 fileSprites[11] = star_cyan; 582 fileSprites[12] = star_blue; 583 fileSprites[13] = star_purple; 584 585 fileMatchSprites[0] = 0; 586 fileMatchSprites[1] = file_match_red; 587 fileMatchSprites[2] = file_match_yellow; 588 fileMatchSprites[3] = file_match_cyan; 589 fileMatchSprites[4] = file_match_blue; 590 fileMatchSprites[5] = file_match_purple; 591 592 digitSprites[0] = digit_0; 593 digitSprites[1] = digit_1; 594 digitSprites[2] = digit_2; 595 digitSprites[3] = digit_3; 596 digitSprites[4] = digit_4; 597 digitSprites[5] = digit_5; 598 digitSprites[6] = digit_6; 599 digitSprites[7] = digit_7; 600 digitSprites[8] = digit_8; 601 digitSprites[9] = digit_9; 602 603 deathSprites[0] = 0; 604 deathSprites[1] = exa_dying_1; 605 deathSprites[2] = exa_dying_2; 606 607 // basic graphics display settings 608 gfx_Begin(); 609#ifdef NO_BUFFER 610 gfx_SetDrawScreen(); 611#else 612 gfx_SetDrawBuffer(); 613#endif 614 615 // set the clip area so that the grid can be offset 616 gfx_SetClipRegion(0, CLIP_VOFFSET, GFX_LCD_WIDTH, GFX_LCD_HEIGHT); 617 618 // prepare the sketchy combination palette 619 for (unsigned char i = 0; i < sizeof_fixed_palette; i++) global_palette[i] = fixed_palette[i]; 620 for (unsigned char i = 0; i < sizeof_grid_palette; i++) global_palette[i + 32] = grid_palette[i]; 621 gfx_SetPalette(global_palette, 32 + sizeof_grid_palette, 0); 622 gfx_SetTransparentColor(16); 623 624 // find the high score 625 unsigned char saveVar = ti_Open(SAVE_VAR_NAME, "r"); 626 if (saveVar == 0) 627 { 628 // need to make a new file 629 ti_Close(saveVar); 630 saveVar = ti_Open(SAVE_VAR_NAME, "w"); 631 highScore = 0; 632 ti_Write(&highScore, 3, 1, saveVar); 633 } 634 else 635 { 636 ti_Read(&highScore, 3, 1, saveVar); 637 } 638 ti_Close(saveVar); 639 640 // initialize sprite storage 641 behindExa->width = exa_empty_width; 642 behindExa->height = exa_empty_height; 643 644 return 0; 645} 646 647int main(void) 648{ 649 if (init()) 650 { 651 os_ClrHome(); 652 os_PutStrFull("Missing gfx var!"); 653 654 while (!kb_AnyKey()); 655 656 return 0; 657 } 658 659 do 660 { 661 if (titleScreen()) break; 662 663 // wait for keys to be reset 664 while (kb_AnyKey()); 665 666 startGame(); 667 668 while (doInput()) 669 { 670 updateGrid(); 671 drawFrame(); 672 673 if (gameOver) break; 674 } 675 } 676 while (endGame()); 677 678 gfx_End(); 679 680 kb_ClearOnLatch(); 681 682 return 0; 683}