-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathTinyMinez.ino
550 lines (468 loc) · 15.6 KB
/
TinyMinez.ino
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
// >>>>> T-I-N-Y M-I-N-E-Z v1.0 for ATTINY85 GPLv3 <<<<
// Tinyjoypad rev2 compatible
// Programmer: Sven B 2021
// Contact EMAIL: [email protected]
// Tiny Minez v1.0 is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// The code works at 16MHZ internal
// and uses ssd1306xled Library for SSD1306 oled display 128x64.
//
// To stuff all code and data into the 8192 bytes of the ATtiny85
// the ATTinyCore (v1.5.2) by Spence Konde is recommended.
// The core is available at github: [https://github.com/SpenceKonde/ATTinyCore], just add the
// following board manager to the Arduino IDE: [http://drazzy.com/package_drazzy.com_index.json]
// Please enable LTO (link time optimization) and disable 'millis()' and
// 'micros()'.
// enable serial screenshot
//#define _ENABLE_SERIAL_SCREENSHOT_
// perform a serial screenshot if this condition is true:
//#define _SERIAL_SCREENSHOT_TRIGGER_CONDITION_ ( isRightPressed() )
#include <Arduino.h>
#include <util/delay.h>
#if !defined(__AVR_ATtiny85__)
#include "SerialHexTools.h"
#endif
#include "spritebank.h"
#include "tinyJoypadUtils.h"
#include "textUtils.h"
#include "soundFX.h"
#include "RLEdecompression.h"
#include "TinyMinezGame.h"
#include "Selection.h"
const uint8_t KEY_DELAY = 100;
const uint8_t FLAG_DELAY = 100;
// the mines per board for the 4 difficulties
const uint8_t mineDifficulty[] PROGMEM = { 5, 10, 15, 20 };
// the game object containing all logic and data
Game game;
// the difficulty selection
Selection selection( checked, unchecked, 16, 6, 0x01 );
// it's difficult to spot the cursor, so let it flash (frame time is ~50ms)
const uint8_t CursorMaxFlashCount = 24;
// flash if count is greater or equal the threshold
const uint8_t cursorFlashThreshold = CursorMaxFlashCount / 2;
// increase this counter on every display;
uint8_t cursorFlashCount = 0;
/*--------------------------------------------------------*/
void setup()
{
// initialize the pins (and serial port if present)
InitTinyJoypad();
// perform display initialization
InitDisplay();
}
/*--------------------------------------------------------*/
void loop()
{
game.setStatus( Status::intro );
uint8_t count = 0;
// game main loop
while ( true )
{
// always incement seed
game.incrementSeed();
switch ( game.getStatus() )
{
/////////////////////////////
// intro screen
case Status::intro:
case Status::rules:
{
// alternate between title screen and rules
game.setStatus( ( count++ < 128 ) ? Status::intro : Status::rules );
// display intro screen
Tiny_Flip( false );
// check if button pressed
if ( isFirePressed() )
{
game.setStatus( Status::difficultySelection );
// increment seed while waiting - this way we get a good enough random seed
while ( isFirePressed() ) { game.incrementSeed(); }
}
break;
}
/////////////////////////////
// difficulty selection
case Status::difficultySelection:
{
// reset title flip count
count = 0;
// force the first redraw
bool userAction = true;
do
{
// check for user actions
if ( isUpPressed() )
{
selection.previousSelection();
// user action detected
userAction = true;
}
if ( isDownPressed() )
{
selection.nextSelection();
// user action detected
userAction = true;
}
if ( userAction )
{
// play a sound
blip5();
// display new selection
Tiny_Flip( false );
// wait until the button is released
waitUntilButtonsReleased( KEY_DELAY );
// action processed
userAction = false;
}
// help the RNG
game.incrementSeed();
} while( !isFirePressed() );
// increment seed while waiting - this way we get a good enough random seed
while ( isFirePressed() ) { game.incrementSeed(); }
// prepare the game
game.setStatus( Status::prepareGame );
break;
}
/////////////////////////////
// prepare new game
case Status::prepareGame:
{
// acknowledge the pressed button
blip5();
// hide the selected number of mines
uint8_t numberOfMines = pgm_read_byte( mineDifficulty + selection.getSelection() );
// create a new level depending on the difficulty (TODO)
game.createLevel( numberOfMines );
// dump the level to serial
game.serialPrintLevel();
// start the game
game.setStatus( Status::playGame );
break;
}
/////////////////////////////
// play the game
case Status::playGame:
{
bool playerAction = false;
while ( ( game.getStatus() != Status::gameWon ) && ( game.getStatus() != Status::boom ) )
{
// increase random seed
game.incrementSeed();
// get current cursor position
uint8_t cursorX = game.getCursorX();
uint8_t cursorY = game.getCursorY();
// any buttons pressed?
if ( isLeftPressed() && ( cursorX > 0 ) )
{
cursorX--;
// wait a moment
playerAction = true;
}
if ( isRightPressed() && ( cursorX < game.getLevelWidth() - 1 ) )
{
cursorX++;
// wait a moment
playerAction = true;
}
if ( isUpPressed() && ( cursorY > 0 ) )
{
cursorY--;
// wait a moment
playerAction = true;
}
if ( isDownPressed() && ( cursorY < game.getLevelHeight() - 1 ) )
{
cursorY++;
// wait a moment
playerAction = true;
}
if ( isFirePressed() )
{
uint8_t count = 0;
// let's check how long fire is pressed
do
{
// wait until the button is released
_delay_ms( FLAG_DELAY );
// count this!
if ( count < 255 ) { count++; }
// wait unit the button is released
} while ( isFirePressed() );
// was fire pressed longer than 3 rounds(~400ms)?
if ( count > 3 )
{
// set or remove a flag symbol
game.toggleFlag( cursorX, cursorY );
}
else
{
// uncover this cell and all adjacent cells (if this cell is empty)
if ( !game.uncoverCells( cursorX, cursorY ) )
{
// are all non mine fields uncovered?
if ( game.isWon() )
{
// game won!
game.setStatus( Status::gameWon );
}
}
else
{
// something bad did happen...
game.setStatus( Status::boom );
}
}
// wait a moment
playerAction = true;
}
else if ( playerAction )
{
// play a sound
blip2();
}
// update cursor flash count
cursorFlashCount++;
if ( cursorFlashCount >= CursorMaxFlashCount ) { cursorFlashCount = 0; }
// set cursor to the new position
game.setCursorPosition( cursorX, cursorY );
// draw board
Tiny_Flip( false );
// only delay if there were any changes (important for getting a good seed)
if ( playerAction )
{
// reset cursor to visible
cursorFlashCount = 0;
// no forced update required
playerAction = false;
// wait a moment
_delay_ms( KEY_DELAY );
}
}
break;
}
/////////////////////////////
// show game over screen (aka BOOM screen)
case Status::boom:
{
// play some sound
failingSound();
// display ***BOOM*** screen and flash
for ( uint8_t flash = 0; flash < 10; flash++ ) { Tiny_Flip( flash == 0 ); _delay_ms( 100 ); }
// uncover all mines
game.uncoverCells( BOMB );
game.setStatus( Status::gameOver );
while ( !isFirePressed() )
{
// show the board with all tiles uncoverted
Tiny_Flip( true );
// update cursor flash count
cursorFlashCount++;
if ( cursorFlashCount >= CursorMaxFlashCount ) { cursorFlashCount = 0; }
}
// wait until fire is released
while ( isFirePressed() );
_delay_ms( KEY_DELAY );
// acknowledge the button
blip5();
// return to the title screen
game.setStatus( Status::intro );
break;
}
/////////////////////////////
// all mines found, congratulations!
case Status::gameWon:
{
// display game won screen
Tiny_Flip( false );
// play a tune
successSound();
// wait for button
waitForFireButtonPressedAndReleased();
// acknowledge the button
blip5();
// switch to intro screen
game.setStatus( Status::intro );
break;
}
} // switch
} // while ( true )
}
/*--------------------------------------------------------*/
void Tiny_Flip( bool invert )
{
Status gameStatus = game.getStatus();
// prepare text buffer for statistics (only displayed during the game)
clearTextBuffer();
uint8_t *textBuffer = getTextBuffer();
// prepare statistics when game is running
convertValueToDigits( game.getFlaggedTilesCount(), textBuffer + 1 + 1 * 4 );
convertValueToDigits( game.getHiddenTilesCount(), textBuffer + 1 + 4 * 4 );
convertValueToDigits( game.getClicksCount(), textBuffer + 1 + 7 * 4 );
// optional bitmap buffer pointer
uint8_t *compressedBitmap;
// only invert cursor if flash count is less than threshold
uint8_t cursor = ( cursorFlashCount < cursorFlashThreshold ) ? 0xff : 0x00;
// there are 8 rows of 8 pixels each
for ( uint8_t y = 0; y < 8; y++)
{
TinyFlip_PrepareDisplayRow( y );
switch ( gameStatus )
{
///////////////////////////
// display a compressed bitmap
case Status::intro:
case Status::rules:
case Status::difficultySelection:
case Status::prepareGame:
case Status::boom:
case Status::gameWon:
{
// select bitmap
if ( y == 0 )
{
switch( gameStatus )
{
case Status::intro:
case Status::prepareGame:
compressedBitmap = TitleScreen; break;
case Status::rules:
compressedBitmap = Rules; break;
case Status::difficultySelection:
compressedBitmap = difficultySelection; break;
case Status::boom:
compressedBitmap = BOOM; break;
case Status::gameWon:
compressedBitmap = ( selection.getSelection() < 3 ) ? game_won : AWESOME; break;
}
}
// display the full line
compressedBitmap = displayBitmapRow( y, compressedBitmap, invert );
break;
}
///////////////////////////
// display the board, if the game is over,
// the board will be inverted
case Status::playGame:
case Status::gameOver:
{
// invert image?
uint8_t invertValue = invert ? 0xff : 0x00;
// the first 96 columns are used to display the dungeon
for ( uint8_t x = 0; x < 96; x++ )
{
uint8_t spriteColumn = x & 0x07;
uint8_t cellValue = game.getCellValue( x >> 3, y );
uint8_t pixels = getSpriteData( cellValue, spriteColumn );
// invert the tile with the cursor above it
if ( cellValue & 0x80 ) { pixels ^= cursor; }
// invert anyway?
pixels ^= invertValue;
TinyFlip_SendPixels( pixels );
} // for x
// display the dashboard here
for ( uint8_t x = 0; x < 32; x++)
{
uint8_t pixels = pgm_read_byte( dashBoard + x + y * 32 )
| displayText( x, y );
TinyFlip_SendPixels( pixels );
}
break;
}
///////////////////////////
// this should never happen
default:
{
#if !defined(__AVR_ATtiny85__)
Serial.println( F("*** Tiny_Flip() : default branch hit - did you forget something? ***") );
while( 1 );
break;
#endif
}
} // switch
TinyFlip_FinishDisplayRow();
} // for y
// display the whole screen at once
TinyFlip_DisplayBuffer();
#ifdef _ENABLE_SERIAL_SCREENSHOT_
if ( _SERIAL_SCREENSHOT_TRIGGER_CONDITION_ )
{
// print a screenshot to the serial interface
TinyFlip_SerialScreenshot();
}
#endif
}
/*--------------------------------------------------------*/
// Displays the row of the given bitmap and inverts it if required
// The bitmap is expected to be RLE encoded. The function returns
// the pointer of the next compressed chunk of image data.
uint8_t* displayBitmapRow( const uint8_t y, const uint8_t *bitmap, const bool invert )
{
uint8_t xorValue = ( invert ? 0xff : 0x00 );
// overlay is only required during difficulty selection
Selection *overlay = nullptr;
if ( game.getStatus() == Status::difficultySelection )
{
overlay = &selection;
}
// we will repurpose the text buffer to save valuable RAM
uint8_t *chunkBuffer = getTextBuffer();
// uncompress chunk and save next address
uint8_t *render = pgm_RLEdecompress( bitmap, chunkBuffer, 128 );
// display the full line
for ( uint8_t x = 0; x < 128; x++ )
{
uint8_t pixels = ( *chunkBuffer++ ) ^ xorValue;
if ( overlay != nullptr )
{
pixels |= selection.getOverlayPixels( x, y );
}
TinyFlip_SendPixels( pixels );
} // for x
// return the current decompression pointer
return( render );
}
/*--------------------------------------------------------*/
uint8_t getSpriteData( uint8_t cellValue, uint8_t spriteColumn )
{
// remove cursor
cellValue &= DATA_MASK;
if ( cellValue & FLAG )
{
// a flag was planted here!
return( pgm_read_byte( flag8x8 + spriteColumn ) );
}
if ( cellValue & HIDDEN )
{
// this cellValue is still covered
return( pgm_read_byte( tile8x8 + spriteColumn ) );
}
if ( cellValue == BOMB )
{
// a bomb!
return( pgm_read_byte( bomb8x8 + spriteColumn ) );
}
if ( ( cellValue >=1 ) && ( cellValue <= 9 ) )
{
// draw according digit
return( pgm_read_byte( digits + cellValue * 8 + spriteColumn ) );
}
// obviously empty ;)
return( pgm_read_byte( empty8x8 + spriteColumn ) );
}
/*--------------------------------------------------------*/
void waitForFireButtonPressedAndReleased()
{
// wait until fire is pressed
while ( !isFirePressed() ) { game.incrementSeed(); }
// wait until fire is released
while ( isFirePressed() ) { game.incrementSeed(); }
}