A port of Zachtronics' match-4 game HACK*MATCH to the TI-84 Plus CE
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}