初始化提交

This commit is contained in:
王立帮
2024-07-20 22:09:06 +08:00
commit c247dd07a6
6876 changed files with 2743096 additions and 0 deletions

View File

@@ -0,0 +1,99 @@
MD_MAX72XX::fontType_t _Fixed_5x3[] PROGMEM = {
'F', 1, 32, 127, 5,
2, 0, 0, // 32 - 'Space'
1, 23, // 33 - '!'
3, 3, 0, 3, // 34 - '"'
3, 31, 10, 31, // 35 - '#'
3, 22, 31, 13, // 36 - '$'
3, 9, 4, 18, // 37 - '%'
3, 10, 21, 26, // 38 - '&'
1, 3, // 39
2, 14, 17, // 40 - '('
2, 17, 14, // 41 - ')'
3, 10, 4, 10, // 42 - '*'
3, 4, 14, 4, // 43 - '+'
2, 16, 8, // 44 - ','
3, 4, 4, 4, // 45 - '-'
1, 16, // 46 - '.'
3, 8, 4, 2, // 47 - '/'
3, 31, 17, 31, // 48 - '0'
2, 0, 31, // 49 - '1'
3, 29, 21, 23, // 50 - '2'
3, 17, 21, 31, // 51 - '3'
3, 7, 4, 31, // 52 - '4'
3, 23, 21, 29, // 53 - '5'
3, 31, 21, 29, // 54 - '6'
3, 1, 1, 31, // 55 - '7'
3, 31, 21, 31, // 56 - '8'
3, 23, 21, 31, // 57 - '9'
1, 10, // 58 - ':'
2, 16, 10, // 59 - ';'
3, 4, 10, 17, // 60 - '<'
3, 10, 10, 10, // 61 - '='
3, 17, 10, 4, // 62 - '>'
3, 1, 21, 3, // 63 - '?'
3, 14, 21, 22, // 64 - '@'
3, 30, 5, 30, // 65 - 'A'
3, 31, 21, 10, // 66 - 'B'
3, 14, 17, 17, // 67 - 'C'
3, 31, 17, 14, // 68 - 'D'
3, 31, 21, 17, // 69 - 'E'
3, 31, 5, 1, // 70 - 'F'
3, 14, 17, 29, // 71 - 'G'
3, 31, 4, 31, // 72 - 'H'
3, 17, 31, 17, // 73 - 'I'
3, 8, 16, 15, // 74 - 'J'
3, 31, 4, 27, // 75 - 'K'
3, 31, 16, 16, // 76 - 'L'
3, 31, 2, 31, // 77 - 'M'
3, 31, 14, 31, // 78 - 'N'
3, 14, 17, 14, // 79 - 'O'
3, 31, 5, 2, // 80 - 'P'
3, 14, 25, 30, // 81 - 'Q'
3, 31, 5, 26, // 82 - 'R'
3, 18, 21, 9, // 83 - 'S'
3, 1, 31, 1, // 84 - 'T'
3, 15, 16, 15, // 85 - 'U'
3, 7, 24, 7, // 86 - 'V'
3, 15, 28, 15, // 87 - 'W'
3, 27, 4, 27, // 88 - 'X'
3, 3, 28, 3, // 89 - 'Y'
3, 25, 21, 19, // 90 - 'Z'
2, 31, 17, // 91 - '['
3, 2, 4, 8, // 92 - '\'
2, 17, 31, // 93 - ']'
3, 2, 1, 2, // 94 - '^'
3, 16, 16, 16, // 95 - '_'
2, 1, 2, // 96 - '`'
3, 12, 18, 28, // 97 - 'a'
3, 31, 18, 12, // 98 - 'b'
3, 12, 18, 18, // 99 - 'c'
3, 12, 18, 31, // 100 - 'd'
3, 12, 26, 20, // 101 - 'e'
3, 4, 31, 5, // 102 - 'f'
3, 20, 26, 12, // 103 - 'g'
3, 31, 2, 28, // 104 - 'h'
1, 29, // 105 - 'i'
2, 16, 13, // 106 - 'j'
3, 31, 8, 20, // 107 - 'k'
1, 31, // 108 - 'l'
3, 30, 6, 30, // 109 - 'm'
3, 30, 2, 28, // 110 - 'n'
3, 12, 18, 12, // 111 - 'o'
3, 30, 10, 4, // 112 - 'p'
3, 4, 10, 30, // 113 - 'q'
2, 30, 4, // 114 - 'r'
3, 20, 30, 10, // 115 - 's'
3, 4, 30, 4, // 116 - 't'
3, 14, 16, 30, // 117 - 'u'
3, 14, 16, 14, // 118 - 'v'
3, 14, 24, 14, // 119 - 'w'
3, 18, 12, 18, // 120 - 'x'
3, 22, 24, 14, // 121 - 'y'
3, 26, 30, 22, // 122 - 'z'
3, 4, 27, 17, // 123 - '{'
1, 27, // 124 - '|'
3, 17, 27, 4, // 125 - '}'
3, 6, 2, 3, // 126 - '~'
3, 31, 31, 31, // 127 - 'Full Block'
};

View File

@@ -0,0 +1,652 @@
// Implements the game of Bricks using MD_MAXPanel library
//
// Hardware used
// =============
// LEFT_PIN - bat left switch, INPUT_PULLUP
// RIGHT_PIN - bat right switch, INPUT_PULLUP
// UP_PIN - unused
// DOWN_PIN - unused
// SELECT_PIN - unused
// ENTER_PIN - unused
// BEEPER_PIN - piezo speaker
// CLK_PIN, DATA_PIN, CS_PIN - LED matrix display connections
//
// Libraries used
// ==============
// MD_MAX72XX available from https://github.com/MajicDesigns/MD_MAX72XX
//
// Rules of the Game
// =================
// Player must keep the ball in play by batting it away. Each 'brick' that
// is hit scores points. Once all the bricks are cleared the game continues
// with a new screen. Game end when the ball is let out of bounds.
#include <MD_MAXPanel.h>
#include "Font5x3.h"
#include "score.h"
#include "sound.h"
// Turn on debug statements to the serial output
#define DEBUG 0
#if DEBUG
#define PRINT(s, x) { Serial.print(F(s)); Serial.print(x); }
#define PRINTS(x) { Serial.print(F(x)); }
#define PRINTD(x) { Serial.print(x, DEC); }
#define PRINTXY(s,x,y) { Serial.print(F(s)); Serial.print(F("(")); Serial.print(x); Serial.print(F(",")); Serial.print(y); Serial.print(")"); }
#define PRINTSTATE(s) { Serial.print("\n++>"); Serial.print(s); }
#else
#define PRINT(s, x)
#define PRINTS(x)
#define PRINTD(x)
#define PRINTXY(s,x,y)
#define PRINTSTATE(s)
#endif
// Hardware pin definitions.
// All momentary on switches are initialized INPUT_PULLUP
const uint8_t UP_PIN = 2;
const uint8_t RIGHT_PIN = 3;
const uint8_t DOWN_PIN = 4;
const uint8_t LEFT_PIN = 5;
const uint8_t SELECT_PIN = 6;
const uint8_t ENTER_PIN = 7;
const uint8_t BEEPER_PIN = 9;
// Define the number of devices in the chain and the SPI hardware interface
// NOTE: These pin numbers will probably not work with your hardware and may
// need to be adapted
const MD_MAX72XX::moduleType_t HARDWARE_TYPE = MD_MAX72XX::FC16_HW;
const uint8_t X_DEVICES = 4;
const uint8_t Y_DEVICES = 5;
const uint8_t CLK_PIN = 12; // or SCK
const uint8_t DATA_PIN = 11; // or MOSI
const uint8_t CS_PIN = 10; // or SS
// SPI hardware interface
MD_MAXPanel mp = MD_MAXPanel(HARDWARE_TYPE, CS_PIN, X_DEVICES, Y_DEVICES);
// Arbitrary pins
// MD_MAXPanel mx = MD_MAXPanel(HARDWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, X_DEVICES, Y_DEVICES);
uint16_t FIELD_TOP, FIELD_RIGHT; // needs to be initialised in setup()
const uint16_t FIELD_LEFT = 1;
const uint8_t BRICK_SIZE_DEFAULT = 3;
const uint8_t BAT_SIZE_DEFAULT = 3; // must be an odd number
const uint8_t BAT_EDGE_OFFSET = 1;
const char TITLE_TEXT[] = "BRICKS";
const uint16_t SPLASH_DELAY =3000; // in milliseconds
const char GAME_TEXT[] = "GAME";
const char OVER_TEXT[] = "OVER";
const uint16_t GAME_OVER_DELAY = 3000; // in milliseconds
const uint8_t FONT_NUM_WIDTH = 3;
const uint8_t MAX_LIVES = 9;
const uint16_t MAX_SCORE = 999;
// A class to encapsulate the bricks bat
// Bat runs at the bottom of the display to keep the ball in play
class cBrickBat
{
private:
uint16_t _x, _y; // the position of the center of the bat
uint16_t _xmin, _xmax; // the max and min bat boundaries
int8_t _vel; // the velocity of the bat (+1 for moving up, -1 moving down)
uint8_t _size; // the size in pixels for the bat (odd number)
uint8_t _pinLeft; // the pin for the up switch
uint8_t _pinRight; // the pin for th down switch
uint16_t _batDelay; // the delay between possible moves of the bat in milliseconds
uint32_t _timeLastMove; // the millis() value for the last time we moved the bat
public:
enum hitType_t { NO_HIT, CORNER_HIT, FLAT_HIT };
void begin(uint16_t x, uint16_t y, uint16_t xmin, uint16_t xmax, uint8_t size, uint8_t pinL, uint8_t pinR)
{
_x = x;
_y = y;
_xmin = xmin;
_xmax = xmax;
_vel = 0;
_size = size;
_pinLeft = pinL;
_pinRight = pinR;
_batDelay = 40;
pinMode(_pinLeft, INPUT_PULLUP);
pinMode(_pinRight, INPUT_PULLUP);
PRINTXY("\nbat @", _x, _y);
PRINTXY(" limits", _xmin, _xmax);
}
uint16_t getX(void) { return (_x); }
uint16_t getY(void) { return (_y); }
int8_t getVelocity(void) { return(_vel); }
void draw(void) { mp.drawHLine(_y, _x - (_size / 2), _x + (_size / 2), true); }
void erase(void) { mp.drawHLine(_y, _x - (_size / 2), _x + (_size / 2), false); }
bool anyKey(void) { return(digitalRead(_pinLeft) == LOW || digitalRead(_pinRight) == LOW); }
hitType_t hit(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1)
{
int16_t dx = x1 - x0;
// if we are not in the same y plane as the bat there can be no hit
if (y1 != _y) return(NO_HIT);
// if the ball is at
// - the left of the bat and travelling left to right, or
// - the right of the bat and travelling right to left
// then it is a corner hit
if ((x1 == _x + (_size / 2) && dx < 0) || (x1 == _x - (_size / 2) && dx>0))
return(CORNER_HIT);
// the ball is between the left and right bat boundaries inclusive
// then it is a flat hit
if (x1 <= _x + (_size / 2) && x1 >= _x - (_size / 2))
return(FLAT_HIT);
// and in case we missed something, a default no hit
return(NO_HIT);
}
bool move(void)
{
bool moveLeft = (digitalRead(_pinLeft) == LOW);
bool moveRight = (digitalRead(_pinRight) == LOW);
// if this is the time for a move?
if (millis() - _timeLastMove <= _batDelay)
return(false);
_timeLastMove = millis();
mp.update(false);
if (moveRight)
{
PRINTS("\n-- BAT move Left");
erase();
_vel = 1;
_x++;
if (_x + (_size/2) > _xmax) _x--; // keep within top boundary
draw();
}
else if (moveLeft)
{
PRINTS("\n-- BAT move right");
erase();
_vel = -1;
_x--;
if (_x - (_size/2) < _xmin) _x++; // keep within bottom boundary
draw();
}
else
_vel = 0;
mp.update(true);
return((moveLeft || moveRight));
}
};
// A class to encapsulate the bricks ball
// Ball bounces off the bat, edges and bricks
class cBrickBall
{
private:
uint16_t _x, _y; // the position of the center of the ball
int8_t _dx, _dy; // the offsets for the x and y direction
uint16_t _xmin, _ymin; // minimum bounds for the ball
uint16_t _xmax, _ymax; // maximum bounds for the ball
uint32_t _timeLastMove; // last time the ball was moved
uint16_t _ballDelay; // the delay between ball moves in milliseconds
bool _run; // ball is running when true
public:
enum bounce_t { BOUNCE_NONE, BOUNCE_BACK, BOUNCE_TOP, BOUNCE_BOTTOM, BOUNCE_LEFT, BOUNCE_RIGHT };
void begin(uint16_t x, uint16_t y, uint16_t xmin, uint16_t ymin, uint16_t xmax, uint16_t ymax)
{
_dx = _dy = 1;
_timeLastMove = 0;
_ballDelay = 100;
_xmin = xmin;
_xmax = xmax;
_ymin = ymin;
_ymax = ymax;
_run = false;
reset(x, y);
}
uint16_t getX(void) { return (_x); }
uint16_t getY(void) { return (_y); }
uint16_t getNextX(void) { return (_x + _dx); }
uint16_t getNextY(void) { return (_y + _dy); }
uint16_t getDelay(void) { return (_ballDelay); }
void setDelay(uint16_t delay) { if (delay > 10) _ballDelay = delay; }
void start(void) { _run = true; }
void stop(void) { _run = false; }
void draw(void) { mp.setPoint(_x, _y, true); } // PRINTXY("\nball@", _x, _y); }
void erase(void) { mp.setPoint(_x, _y, false); }
void reset(uint16_t x, uint16_t y) { _x = x; _y = y; _dy = 1; }
bool move(void)
{
// if this is the time for a move?
if (_run && millis() - _timeLastMove <= _ballDelay)
return(false);
_timeLastMove = millis();
// do the animation
mp.update(false);
erase();
_x = _x + _dx;
_y = _y + _dy;
// ensure it always stays in bounds
if (_x < _xmin) { _x = _xmin; _dx = -_dx; }
if (_x > _xmax) { _x = _xmax; _dx = -_dx; }
if (_y < _ymin) { _y = _ymin; _dy = -_dy; }
if (_y > _ymax) { _y = _ymax; _dy = -_dy; }
// now update
draw();
mp.update(true);
return(true);
}
void bounce(bounce_t b)
{
switch (b)
{
case BOUNCE_TOP:
case BOUNCE_BOTTOM: _dy = -_dy; break;
case BOUNCE_LEFT:
case BOUNCE_RIGHT: _dx = -_dx; break;
case BOUNCE_BACK: _dx = -_dx; _dy = -_dy; break;
}
}
};
// A class to encapsulate the entire field of bricks
// All the bricks are contained in this object.
class cBrickField
{
private:
struct brick_t
{
uint16_t x, y; // leftmost coordinate for this brick
brick_t *next; // the next in the linked list
};
uint8_t _size; // size of the bricks
brick_t *_bricks; // start of the list
brick_t *_deleted; // the deleted list
void draw(brick_t *pb) { mp.drawHLine(pb->y, pb->x, pb->x + _size-1, true); }
void erase(brick_t *pb) { mp.drawHLine(pb->y, pb->x, pb->x + _size-1, false); }
void add(uint16_t x, uint16_t y)
// add a brick to the start of the list
{
brick_t *pb;
if (_deleted == nullptr)
pb = new brick_t;
else
{
// take from the front of the deleted list
pb = _deleted;
_deleted = _deleted->next;
}
pb->x = x;
pb->y = y;
pb->next = _bricks;
_bricks = pb;
return(pb);
}
void del(brick_t *pbPrev, brick_t *pb)
// delete the specified brick from the list
{
if (pbPrev == nullptr)
_bricks = pb->next;
else
pbPrev->next = pb->next;
// save to the front of the deleted list
pb->next = _deleted;
_deleted = pb;
}
void dumpList(void)
{
PRINTS("\nDUMP List ===");
for (brick_t *pb = _bricks; pb != nullptr; pb = pb->next)
{
PRINTXY("\n", pb->x, pb->y);
draw(pb);
}
PRINTS("\n===");
}
public:
enum bounce_t { BOUNCE_NONE, BOUNCE_BACK, BOUNCE_UP, BOUNCE_DOWN };
void begin(uint16_t xmin, uint16_t ymin, uint16_t xmax, uint16_t ymax, uint8_t size)
{
// work out how many bricks we can put in the space
const uint8_t gapX = 2;
const uint8_t gapY = 3;
uint8_t marginSide = 2;
const uint8_t marginTop = gapY;
const uint8_t numAcross = (xmax - xmin - marginSide - marginSide + size) / (gapX + size);
const uint8_t numDown = (ymax - ymin - marginTop) / gapY;
// now adjust the side margin to center the display as much as possible
marginSide = (1 + (xmax - xmin) - ((numAcross - 1)*gapX) - (numAcross*size))/2;
PRINT("\nBricks size=", size);
PRINTXY(" field = ", xmin, ymin);
PRINTXY(" ", xmax, ymax);
PRINT(" -> Across=", numAcross);
PRINT(" Down=", numDown);
PRINT(" Adj margin=", marginSide);
_size = size;
_bricks = _deleted = nullptr;
// create all the bricks
uint16_t x = xmin + marginSide;
for (uint8_t i = 0; i < numAcross; i++)
{
uint16_t y = ymax - marginTop;
PRINT("\nin column ", i);
for (uint8_t j = 0; j < numDown; j++)
{
add(x, y);
PRINTXY(" - ", x, y);
y -= gapY;
}
x += (gapX + size);
}
//dumpList(); // debug to verify the list is created properly
}
bool emptyField(void) { return(_bricks == nullptr); }
void drawField(void) { for (brick_t *pb = _bricks; pb != nullptr; pb = pb->next) draw(pb); }
void eraseField(void) { while (_bricks != nullptr) { erase(_bricks); del(nullptr, _bricks); } }
bounce_t checkHits(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2)
{
brick_t *pb = _bricks;
brick_t *pbPrev = nullptr;
int16_t dx = x2 - x1;
int16_t dy = y2 - y1;
bounce_t b = BOUNCE_NONE;
while (pb != nullptr)
{
if (y2 == pb->y) // at the same height as this brick
{
// check one of the corners with sideways approach
if ((x2 == pb->x && dx > 0) || (x2 == pb->x - _size - 1 && dx < 0))
{
PRINTS("\n-! BRICK hit on corner");
b = BOUNCE_BACK;
}
// check the whole flat surface with any approach
else if (x2 >= pb->x && x2 <= pb->x + _size - 1)
{
if (dy < 0)
{
b = BOUNCE_UP;
PRINTS("\n-! BRICK hit on top");
}
else
{
b = BOUNCE_DOWN;
PRINTS("\n-! BRICK hit on bottom");
}
}
// eliminate this brick
if (b != BOUNCE_NONE)
{
PRINTXY("\n-! Ball @", x1, y1);
PRINTXY(" next@", x2, y2);
PRINTXY(" brick ", pb->x, pb->y);
PRINTXY("-", pb->x+_size-1, pb->y);
PRINTS(" - deleting");
erase(pb);
del(pbPrev, pb);
break; // no point looping further - only one brick per hit
}
}
// advance the pointers
pbPrev = pb;
pb = pb->next;
}
return(b);
}
};
// main objects coordinated by the code logic
cScore lives, score;
cBrickBall ball;
cBrickBat bat;
cBrickField bricks;
cSound sound;
void setupField(void)
// Draw the playing field at the start of the game.
{
mp.clear();
mp.drawHLine(FIELD_TOP, FIELD_LEFT, FIELD_RIGHT);
mp.drawVLine(FIELD_LEFT, 0, FIELD_TOP);
mp.drawVLine(FIELD_RIGHT, 0, FIELD_TOP);
bat.draw();
lives.draw();
score.draw();
ball.draw();
}
void setup()
{
#if DEBUG
Serial.begin(57600);
#endif
PRINTS("\n[MD_MAXPanel_Bricks]");
mp.begin();
mp.setFont(_Fixed_5x3);
mp.setIntensity(4);
mp.setRotation(MD_MAXPanel::ROT_90);
// one time initialization
FIELD_TOP = mp.getYMax() - mp.getFontHeight() - 2;
FIELD_RIGHT = mp.getXMax() - 1;
bat.begin((FIELD_RIGHT - FIELD_LEFT) / 2, BAT_EDGE_OFFSET, FIELD_LEFT + 1, FIELD_RIGHT - 1, BAT_SIZE_DEFAULT, LEFT_PIN, RIGHT_PIN);
ball.begin(bat.getX(), bat.getY() + 1, FIELD_LEFT+1, 0, FIELD_RIGHT-1, FIELD_TOP-1);
lives.begin(&mp, FIELD_LEFT + 1, FIELD_TOP + 1 + mp.getFontHeight(), MAX_LIVES);
score.limit(MAX_SCORE); // set width() so we can used it below
score.begin(&mp, FIELD_RIGHT - (score.width() * (FONT_NUM_WIDTH + mp.getCharSpacing())) + mp.getCharSpacing(), FIELD_TOP + 1 + mp.getFontHeight(), MAX_SCORE);
sound.begin(BEEPER_PIN);
}
void loop(void)
{
static enum { S_SPLASH, S_INIT, S_WAIT_START, S_POINT_PLAY, S_BALL_OUT, S_BRICKS_EMPTY, S_POINT_RESET, S_GAME_OVER } runState = S_SPLASH;
switch (runState)
{
case S_SPLASH: // show splash screen at start
PRINTSTATE("SPLASH");
{
const uint16_t border = 2;
mp.clear();
mp.drawRectangle(border, border, mp.getXMax() - border, mp.getYMax() - border);
mp.drawLine(0, 0, border, border);
mp.drawLine(0, mp.getYMax(), border, mp.getYMax()-border);
mp.drawLine(mp.getXMax(), 0, mp.getXMax()-border, border);
mp.drawLine(mp.getXMax(), mp.getYMax(), mp.getXMax()-border, mp.getYMax()-border);
mp.drawText((mp.getXMax() - mp.getTextWidth(TITLE_TEXT)) / 2, (mp.getYMax() + mp.getFontHeight())/2 , TITLE_TEXT);
sound.splash();
delay(SPLASH_DELAY);
runState = S_INIT;
}
break;
case S_INIT: // initialize for a new game
PRINTSTATE("INIT");
ball.reset(bat.getX(), bat.getY() + 1);
lives.set(MAX_LIVES);
score.reset();
setupField();
bricks.begin(FIELD_LEFT + 1, FIELD_TOP / 3, FIELD_RIGHT - 1, FIELD_TOP - 1, BRICK_SIZE_DEFAULT);
bricks.drawField();
runState = S_WAIT_START;
PRINTSTATE("WAIT_START");
break;
case S_WAIT_START: // waiting for the start of a new game
if (bat.anyKey())
{
PRINTS("\n-- Starting Game");
sound.start();
ball.start();
runState = S_POINT_PLAY;
PRINTSTATE("POINT_PLAY");
}
break;
case S_POINT_PLAY: // playing a point
// handle the bat animation first
bat.move();
// now move the ball and check what this means
if (ball.move())
{
cBrickBat::hitType_t lastHit;
// check for top/bottom edge collisions
if (ball.getX() <= FIELD_LEFT + 1)
{
PRINTS("\n-- COLLISION left edge");
ball.bounce(cBrickBall::BOUNCE_LEFT);
sound.bounce();
}
else if (ball.getX() >= FIELD_RIGHT - 1)
{
PRINTS("\n-- COLLISION right edge");
ball.bounce(cBrickBall::BOUNCE_RIGHT);
sound.bounce();
}
if (ball.getY() >= FIELD_TOP - 1)
{
PRINTS("\n-- COLLISION top edge");
ball.bounce(cBrickBall::BOUNCE_TOP);
sound.bounce();
}
// check for bat collisions
if ((lastHit = bat.hit(ball.getX(), ball.getY(), ball.getNextX(), ball.getNextY())) != cBrickBat::NO_HIT)
{
PRINTS("\n-- COLLISION bat");
ball.bounce(lastHit == cBrickBat::CORNER_HIT ? cBrickBall::BOUNCE_BACK : cBrickBall::BOUNCE_BOTTOM);
sound.hit();
}
// check for out of bounds at the bottom
if (ball.getY() < BAT_EDGE_OFFSET)
{
PRINTS("\n-- OUT!");
runState = S_BALL_OUT;
}
// check for any hits to bricks
cBrickField::bounce_t b;
if ((b = bricks.checkHits(ball.getX(), ball.getY(), ball.getNextX(), ball.getNextY())) != cBrickField::BOUNCE_NONE)
{
score.increment();
sound.bounce();
// check if this is the end of all the bricks
if (bricks.emptyField())
{
PRINTS("\n== BRICKS empty");
runState = S_BRICKS_EMPTY;
}
else
{
// otherwise we need to bounce the ball in the right direction
switch (b)
{
case cBrickField::BOUNCE_UP: ball.bounce(cBrickBall::BOUNCE_BOTTOM); break;
case cBrickField::BOUNCE_DOWN: ball.bounce(cBrickBall::BOUNCE_TOP); break;
case cBrickField::BOUNCE_BACK: ball.bounce(cBrickBall::BOUNCE_BACK); break;
}
}
}
}
break;
case S_BRICKS_EMPTY: // handle the bricks being completed
PRINTSTATE("BRICKS_EMPTY");
ball.stop();
ball.erase();
lives.increment();
ball.setDelay(ball.getDelay() - 5);
sound.point();
bricks.begin(FIELD_LEFT + 1, FIELD_TOP / 3, FIELD_RIGHT - 1, FIELD_TOP - 1, BRICK_SIZE_DEFAULT);
bricks.drawField();
runState = S_POINT_RESET;
break;
case S_BALL_OUT: // handle the ball going out
PRINTSTATE("BALL_OUT");
ball.stop();
ball.erase();
lives.decrement();
sound.point();
if (lives.score() != 0)
runState = S_POINT_RESET;
else
runState = S_GAME_OVER;
break;
case S_POINT_RESET:
PRINTSTATE("POINT_RESET");
bat.draw();
ball.reset(bat.getX(), bat.getY() + 1);
ball.draw();
delay(500);
runState = S_WAIT_START;
PRINTSTATE("WAIT_START");
break;
case S_GAME_OVER:
PRINTSTATE("GAME_OVER");
bricks.eraseField();
mp.drawText((mp.getXMax() - mp.getTextWidth(GAME_TEXT)) / 2, FIELD_TOP / 2 + mp.getFontHeight() + 1, GAME_TEXT);
mp.drawText((mp.getXMax() - mp.getTextWidth(OVER_TEXT)) / 2, FIELD_TOP / 2 - 1, OVER_TEXT);
sound.over();
delay(GAME_OVER_DELAY);
runState = S_INIT;
break;
}
}

View File

@@ -0,0 +1,54 @@
#pragma once
#include <MD_MAXPanel.h>
// A class to encapsulate the score display
class cScore
{
private:
MD_MAXPanel *_mp; // the MAXPanel object for drawing the score
uint16_t _score; // the score
uint16_t _x, _y; // coordinate of top left for display
uint8_t _width; // number of digits wide
uint16_t _limit; // maximum value allowed
public:
void begin(MD_MAXPanel *mp, uint16_t x, uint16_t y, uint16_t maxScore) { _mp = mp; _x = x, _y = y; limit(maxScore); reset(); }
void reset(void) { erase(); _score = 0; draw(); }
void set(uint16_t s) { if (s <= _limit) { erase(); _score = s; draw(); } }
void increment(uint16_t inc = 1) { if (_score + inc <= _limit) { erase(); _score += inc; draw(); } }
void decrement(uint16_t dec = 1) { if (_score >= dec) { erase(); _score -= dec; draw(); } }
uint16_t score(void) { return(_score); }
void erase(void) { draw(false); }
uint16_t width(void) { return(_width); }
void limit(uint16_t m)
{
erase(); // width may change, so delete with the curret parameters
_limit = m;
// work out how many digits this is
_width = 0;
do
{
_width++;
m /= 10;
} while (m != 0);
}
void draw(bool state = true)
{
char sz[_width + 1];
uint16_t s = _score;
// PRINT("\n-- SCORE: ", _score);
sz[_width] = '\0';
for (int i = _width - 1; i >= 0; --i)
{
sz[i] = (s % 10) + '0';
s /= 10;
}
_mp->drawText(_x, _y, sz, MD_MAXPanel::ROT_0, state);
}
};

View File

@@ -0,0 +1,47 @@
#pragma once
// A class to encapsulate primitive sound effects
class cSound
{
private:
const uint16_t EOD = 0; // End Of Data marker
uint8_t _pinBeep; // the pin to use for beeping
// Sound data - frequency followed by duration in pairs.
// Data ends in End Of Data marker EOD.
const uint16_t soundSplash[1] PROGMEM = { EOD };
const uint16_t soundHit[3] PROGMEM = { 1000, 50, EOD };
const uint16_t soundBounce[3] PROGMEM = { 500, 50, EOD };
const uint16_t soundPoint[3] PROGMEM = { 150, 150, EOD };
const uint16_t soundStart[7] PROGMEM = { 250, 100, 500, 100, 1000, 100, EOD };
const uint16_t soundOver[7] PROGMEM = { 1000, 100, 500, 100, 250, 100, EOD };
void playSound(const uint16_t *table)
// Play sound table data. Data table must end in EOD marker.
{
uint8_t idx = 0;
//PRINTS("\nTone Data ");
while (table[idx] != EOD)
{
uint16_t t = table[idx++];
uint16_t d = table[idx++];
//PRINTXY("-", t, d);
tone(_pinBeep, t);
delay(d);
}
//PRINTS("-EOD");
noTone(_pinBeep); // be quiet now!
}
public:
void begin(uint8_t pinBeep) { _pinBeep = pinBeep; }
void splash(void) { playSound(soundSplash); }
void start(void) { playSound(soundStart); }
void hit(void) { playSound(soundHit); }
void bounce(void) { playSound(soundBounce); }
void point(void) { playSound(soundPoint); }
void over(void) { playSound(soundOver); }
};

View File

@@ -0,0 +1,98 @@
// Program to exercise the MD_MAXPanel library
//
// Displays a rotating 3D cube, shamelessly adapted from the
// Microview library example "MicroViewCube.ino"
//
// Libraries used
// ==============
// MD_MAX72XX available from https://github.com/MajicDesigns/MD_MAX72XX
//
#include <MD_MAXPanel.h>
#include <SPI.h>
// Define the number of devices we have in the chain and the hardware interface
// NOTE: These pin numbers will probably not work with your hardware and may
// need to be adapted
const MD_MAX72XX::moduleType_t HARDWARE_TYPE = MD_MAX72XX::FC16_HW;
const uint8_t X_DEVICES = 4;
const uint8_t Y_DEVICES = 5;
const uint8_t CLK_PIN = 13; // or SCK
const uint8_t DATA_PIN = 11; // or MOSI
const uint8_t CS_PIN = 10; // or SS
// SPI hardware interface
MD_MAXPanel mp = MD_MAXPanel(HARDWARE_TYPE, CS_PIN, X_DEVICES, Y_DEVICES);
// Arbitrary pins
// MD_MAXPanel mx = MD_MAXPanel(HARDWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, X_DEVICES, Y_DEVICES);
// We may wait a bit between updates of the display
const uint16_t ROTATION_DELAY = 0; // in milliseconds
float d = 3;
float px[] = { -d, d, d, -d, -d, d, d, -d };
float py[] = { -d, -d, d, d, -d, -d, d, d };
float pz[] = { -d, -d, -d, -d, d, d, d, d };
float p2x[] = {0,0,0,0,0,0,0,0};
float p2y[] = {0,0,0,0,0,0,0,0};
float r[] = {0,0,0};
void setup()
{
mp.begin();
mp.clear();
}
void loop()
{
drawCube();
delay(ROTATION_DELAY);
}
void drawCube()
{
const uint16_t SHAPE_SIZE = 40 * (min(mp.getXMax(), mp.getYMax()) / 3);
const uint16_t SCREEN_WIDTH = mp.getXMax();
const uint16_t SCREEN_HEIGHT = mp.getYMax();
r[0]=r[0]+PI/180.0; // Add a degree
r[1]=r[1]+PI/180.0; // Add a degree
r[2]=r[2]+PI/180.0; // Add a degree
if (r[0] >= 360.0*PI/180.0) r[0] = 0;
if (r[1] >= 360.0*PI/180.0) r[1] = 0;
if (r[2] >= 360.0*PI/180.0) r[2] = 0;
for (uint8_t i = 0; i < 8; i++)
{
float px2 = px[i];
float py2 = cos(r[0])*py[i] - sin(r[0])*pz[i];
float pz2 = sin(r[0])*py[i] + cos(r[0])*pz[i];
float px3 = cos(r[1])*px2 + sin(r[1])*pz2;
float py3 = py2;
float pz3 = -sin(r[1])*px2 + cos(r[1])*pz2;
float ax = cos(r[2])*px3 - sin(r[2])*py3;
float ay = sin(r[2])*px3 + cos(r[2])*py3;
float az = pz3-150;
p2x[i] = SCREEN_WIDTH/2+ax*SHAPE_SIZE/az;
p2y[i] = SCREEN_HEIGHT/2+ay*SHAPE_SIZE/az;
}
mp.clear();
mp.update(false);
for (uint8_t i = 0; i < 3; i++)
{
mp.drawLine(p2x[i],p2y[i],p2x[i+1],p2y[i+1]);
mp.drawLine(p2x[i+4],p2y[i+4],p2x[i+5],p2y[i+5]);
mp.drawLine(p2x[i],p2y[i],p2x[i+4],p2y[i+4]);
}
mp.drawLine(p2x[3],p2y[3],p2x[0],p2y[0]);
mp.drawLine(p2x[7],p2y[7],p2x[4],p2y[4]);
mp.drawLine(p2x[3],p2y[3],p2x[7],p2y[7]);
mp.update(true);
}

View File

@@ -0,0 +1,217 @@
// Implements Conway's Game of Life using MD_MAXPanel library
//
// Hardware used
// =============
// Momentary On push switch on SWITCH_PIN to start a new game. The digital I/O
// will be initialize INPUT_PULLUP
//
// Libraries used
// ==============
// MD_MAX72XX available from https://github.com/MajicDesigns/MD_MAX72XX
//
// Rules of the Game
// =================
// The universe of the Game of Life is an infinite, two-dimensional orthogonal
// grid of square cells, each of which is in one of two possible states,
// alive or dead (or populated and unpopulated). Every cell interacts with
// its eight neighbors, which are the cells that are horizontally, vertically,
// or diagonally adjacent. At each step in time, the following transitions occur:
//
// 1. Any live cell with fewer than two live neighbors dies, as if by under population.
// 2. Any live cell with two or three live neighbors lives on to the next generation.
// 3. Any live cell with more than three live neighbors dies, as if by overpopulation.
// 4. Any dead cell with exactly three live neighbors becomes a live cell, as if
// by reproduction.
//
// The initial pattern constitutes the seed of the system. The first generation is
// created by applying the above rules simultaneously to every cell in the seed;
// births and deaths occur simultaneously, and the discrete moment at which this
// happens is sometimes called a tick. Each generation is a pure function of the
// preceding one. The rules continue to be applied repeatedly to create further
// generations.
//
#include <MD_MAXPanel.h>
#include "randomseed.h"
// Turn on debug statements to the serial output
#define DEBUG 0
#if DEBUG
#define PRINT(s, x) { Serial.print(F(s)); Serial.print(x); }
#define PRINTS(x) { Serial.print(F(x)); }
#define PRINTD(x) { Serial.print(x, DEC); }
#define PRINTXY(s, x, y) { Serial.print(s); Serial.print(F("(")); Serial.print(x); Serial.print(F(",")); Serial.print(y); Serial.print(F(")")); }
#else
#define PRINT(s, x)
#define PRINTS(x)
#define PRINTD(x)
#define PRINTXY(s, x, y)
#endif
// Define the number of devices we have in the chain and the hardware interface
// NOTE: These pin numbers will probably not work with your hardware and may
// need to be adapted
const MD_MAX72XX::moduleType_t HARDWARE_TYPE = MD_MAX72XX::FC16_HW;
const uint8_t X_DEVICES = 4;
const uint8_t Y_DEVICES = 5;
const uint8_t CLK_PIN = 13; // or SCK
const uint8_t DATA_PIN = 11; // or MOSI
const uint8_t CS_PIN = 10; // or SS
// SPI hardware interface
MD_MAXPanel mp = MD_MAXPanel(HARDWARE_TYPE, CS_PIN, X_DEVICES, Y_DEVICES);
// Arbitrary pins
// MD_MAXPanel mx = MD_MAXPanel(HARWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, X_DEVICES, Y_DEVICES);
#define SWITCH_PIN 6
// We always wait a bit between updates of the display
#define TICK_TIME 150 // in milliseconds
void setup(void)
{
#if DEBUG
Serial.begin(57600);
#endif
PRINTS("\n[MD_MAXPanel Game of Life]");
pinMode(SWITCH_PIN, INPUT_PULLUP);
mp.begin();
mp.clear();
randomSeed(seedOut(31, RANDOM_SEED_PORT));
}
void loop(void)
{
static uint32_t timeLastRun = 0;
static uint8_t sameCount = 10;
static uint32_t lastCount = 0;
uint32_t count = countCells();
if (lastCount == count) sameCount++; else sameCount = 0;
if (digitalRead(SWITCH_PIN) == LOW || sameCount >= 10)
{
mp.clear(); // mark the end of the display ...
delay(1000); // ... with a minor pause!
firstGeneration();
sameCount = 0;
}
lastCount = count;
// Check if next generation time
if (millis() - timeLastRun >= TICK_TIME)
{
timeLastRun = millis();
nextGeneration();
}
}
uint32_t countCells(void)
{
uint32_t count = 0;
for (uint16_t x = 0; x <= mp.getXMax(); x++)
for (uint16_t y = 0; y <= mp.getYMax(); y++)
count += (mp.getPoint(x,y) ? 1:0);
return(count);
}
void firstGeneration(void)
// Create a 4-way symmetric random setup
{
mp.update(false);
PRINTS("\n-- FIRST Generation");
PRINTXY("\n-- Field size (1,1) - ", mp.getXMax() - 1, mp.getYMax() - 1);
mp.clear();
for (uint16_t x=1; x<(mp.getXMax()+1) / 2; x++)
for (uint16_t y = 1; y < (mp.getYMax()+1) / 2; y++)
{
bool b = (random(101) > 50);
mp.setPoint(x, y, b);
mp.setPoint(mp.getXMax() - x, y, b);
mp.setPoint(x, mp.getYMax() - y, b);
mp.setPoint(mp.getXMax() - x, mp.getYMax() - y, b);
}
mp.update(true);
}
void nextGeneration(void)
// Apply the rules
{
bool rowBuf[2][mp.getXMax()+2];
uint16_t count;
bool newCell;
PRINTS("\n-- NEW generation");
// clear out the row buffers
memset(rowBuf, 0, sizeof(rowBuf));
mp.update(false);
for (uint16_t y=mp.getYMax()-1; y>=1; y--)
{
// copy the current row to the buffer
for (uint16_t x=0; x<mp.getXMax(); x++)
rowBuf[1][x] = mp.getPoint(x, y);
// work out a 'new' current row
for (uint16_t x=1; x<mp.getXMax(); x++)
{
// count the number of neighbours
count = rowBuf[0][x-1] ? 1:0;
count += rowBuf[0][x] ? 1:0;
count += rowBuf[0][x+1]? 1:0;
count += rowBuf[1][x-1]? 1:0;
count += rowBuf[1][x+1]? 1:0;
count += mp.getPoint(x-1, y-1)? 1:0;
count += mp.getPoint(x, y-1) ? 1:0;
count += mp.getPoint(x+1, y-1)? 1:0;
PRINTXY("\n@", x, y);
PRINT(" count=", count);
PRINTS(" ->");
if (count < 2)
{
// A live cell with fewer than two neighbors dies.
newCell = false;
PRINTS("dies.");
}
else if ((count == 2 || count == 3) && mp.getPoint(x, y))
{
// A live cell with two or three neighbors lives on.
newCell = true;
PRINTS("stays.");
}
else if (count == 3 && !mp.getPoint(x, y))
{
// A dead cell with exactly three neighbors becomes live.
newCell = true;
PRINTS("born.");
}
else
{
// A live cell with more than three neighbors dies.
newCell = false;
PRINTS("dies.");
}
mp.setPoint(x, y, newCell);
}
// update the saved row buffers
for (uint16_t x=0; x<=mp.getXMax(); x++)
rowBuf[0][x] = rowBuf[1][x];
}
mp.update(true);
}

View File

@@ -0,0 +1,45 @@
#pragma once
// Random seed creation --------------------------
// Adapted from http://www.utopiamechanicus.com/article/arduino-better-random-numbers/
const uint8_t RANDOM_SEED_PORT = A3; // port read for random seed
uint16_t bitOut(uint8_t port)
{
static bool firstTime = true;
uint32_t prev = 0;
uint32_t bit1 = 0, bit0 = 0;
uint32_t x = 0, limit = 99;
if (firstTime)
{
firstTime = false;
prev = analogRead(port);
}
while (limit--)
{
x = analogRead(port);
bit1 = (prev != x ? 1 : 0);
prev = x;
x = analogRead(port);
bit0 = (prev != x ? 1 : 0);
prev = x;
if (bit1 != bit0)
break;
}
return(bit1);
}
uint32_t seedOut(uint16_t noOfBits, uint8_t port)
{
// return value with 'noOfBits' random bits set
uint32_t seed = 0;
for (int i = 0; i<noOfBits; ++i)
seed = (seed << 1) | bitOut(port);
return(seed);
}
//------------------------------------------------------------------------------

View File

@@ -0,0 +1,99 @@
MD_MAX72XX::fontType_t _Fixed_5x3[] PROGMEM = {
'F', 1, 32, 127, 5,
2, 0, 0, // 32 - 'Space'
1, 23, // 33 - '!'
3, 3, 0, 3, // 34 - '"'
3, 31, 10, 31, // 35 - '#'
3, 22, 31, 13, // 36 - '$'
3, 9, 4, 18, // 37 - '%'
3, 10, 21, 26, // 38 - '&'
1, 3, // 39
2, 14, 17, // 40 - '('
2, 17, 14, // 41 - ')'
3, 10, 4, 10, // 42 - '*'
3, 4, 14, 4, // 43 - '+'
2, 16, 8, // 44 - ','
3, 4, 4, 4, // 45 - '-'
1, 16, // 46 - '.'
3, 8, 4, 2, // 47 - '/'
3, 31, 17, 31, // 48 - '0'
2, 0, 31, // 49 - '1'
3, 29, 21, 23, // 50 - '2'
3, 17, 21, 31, // 51 - '3'
3, 7, 4, 31, // 52 - '4'
3, 23, 21, 29, // 53 - '5'
3, 31, 21, 29, // 54 - '6'
3, 1, 1, 31, // 55 - '7'
3, 31, 21, 31, // 56 - '8'
3, 23, 21, 31, // 57 - '9'
1, 10, // 58 - ':'
2, 16, 10, // 59 - ';'
3, 4, 10, 17, // 60 - '<'
3, 10, 10, 10, // 61 - '='
3, 17, 10, 4, // 62 - '>'
3, 1, 21, 3, // 63 - '?'
3, 14, 21, 22, // 64 - '@'
3, 30, 5, 30, // 65 - 'A'
3, 31, 21, 10, // 66 - 'B'
3, 14, 17, 17, // 67 - 'C'
3, 31, 17, 14, // 68 - 'D'
3, 31, 21, 17, // 69 - 'E'
3, 31, 5, 1, // 70 - 'F'
3, 14, 17, 29, // 71 - 'G'
3, 31, 4, 31, // 72 - 'H'
3, 17, 31, 17, // 73 - 'I'
3, 8, 16, 15, // 74 - 'J'
3, 31, 4, 27, // 75 - 'K'
3, 31, 16, 16, // 76 - 'L'
3, 31, 2, 31, // 77 - 'M'
3, 31, 14, 31, // 78 - 'N'
3, 14, 17, 14, // 79 - 'O'
3, 31, 5, 2, // 80 - 'P'
3, 14, 25, 30, // 81 - 'Q'
3, 31, 5, 26, // 82 - 'R'
3, 18, 21, 9, // 83 - 'S'
3, 1, 31, 1, // 84 - 'T'
3, 15, 16, 15, // 85 - 'U'
3, 7, 24, 7, // 86 - 'V'
3, 15, 28, 15, // 87 - 'W'
3, 27, 4, 27, // 88 - 'X'
3, 3, 28, 3, // 89 - 'Y'
3, 25, 21, 19, // 90 - 'Z'
2, 31, 17, // 91 - '['
3, 2, 4, 8, // 92 - '\'
2, 17, 31, // 93 - ']'
3, 2, 1, 2, // 94 - '^'
3, 16, 16, 16, // 95 - '_'
2, 1, 2, // 96 - '`'
3, 12, 18, 28, // 97 - 'a'
3, 31, 18, 12, // 98 - 'b'
3, 12, 18, 18, // 99 - 'c'
3, 12, 18, 31, // 100 - 'd'
3, 12, 26, 20, // 101 - 'e'
3, 4, 31, 5, // 102 - 'f'
3, 20, 26, 12, // 103 - 'g'
3, 31, 2, 28, // 104 - 'h'
1, 29, // 105 - 'i'
2, 16, 13, // 106 - 'j'
3, 31, 8, 20, // 107 - 'k'
1, 31, // 108 - 'l'
3, 30, 6, 30, // 109 - 'm'
3, 30, 2, 28, // 110 - 'n'
3, 12, 18, 12, // 111 - 'o'
3, 30, 10, 4, // 112 - 'p'
3, 4, 10, 30, // 113 - 'q'
2, 30, 4, // 114 - 'r'
3, 20, 30, 10, // 115 - 's'
3, 4, 30, 4, // 116 - 't'
3, 14, 16, 30, // 117 - 'u'
3, 14, 16, 14, // 118 - 'v'
3, 14, 24, 14, // 119 - 'w'
3, 18, 12, 18, // 120 - 'x'
3, 22, 24, 14, // 121 - 'y'
3, 26, 30, 22, // 122 - 'z'
3, 4, 27, 17, // 123 - '{'
1, 27, // 124 - '|'
3, 17, 27, 4, // 125 - '}'
3, 6, 2, 3, // 126 - '~'
3, 31, 31, 31, // 127 - 'Full Block'
};

View File

@@ -0,0 +1,824 @@
// Implements the game of Meteors using MD_MAXPanel library
//
// Hardware used
// =============
// LEFT_PIN - bat left switch, INPUT_PULLUP
// RIGHT_PIN - bat right switch, INPUT_PULLUP
// UP_PIN - unused
// DOWN_PIN - unused
// SELECT_PIN - shooting bullets switch, INPUT_PULLUP
// ENTER_PIN - unused
// BEEPER_PIN - piezo speaker
// CLK_PIN, DATA_PIN, CS_PIN - LED matrix display connections
//
// Libraries used
// ==============
// MD_MAX72XX available from https://github.com/MajicDesigns/MD_MAX72XX
//
// Rules of the Game
// =================
// The player controls a spaceship in an meteor field. The object of the
// game is to shoot and destroy meteors while not colliding with any meteor
// fragments. Meteors start as a large size and become smaller and move
// faster as they fragment when they are hit. Smaller, faster meteors
// score more points that large slow ones. The game ends when a meteor
// hits the spaceship, destroying it.
#include <MD_MAXPanel.h>
#include "Font5x3.h"
#include "score.h"
#include "sound.h"
#include "randomseed.h"
// Turn on debug statements to the serial output
#define DEBUG 0
#if DEBUG
#define PRINT(s, x) { Serial.print(F(s)); Serial.print(x); }
#define PRINTS(x) { Serial.print(F(x)); }
#define PRINTD(x) { Serial.print(x, DEC); }
#define PRINTXY(s,x,y) { Serial.print(F(s)); Serial.print(F("(")); Serial.print(x); Serial.print(F(",")); Serial.print(y); Serial.print(")"); }
#define PRINTSTATE(s) { Serial.print("\n++>"); Serial.print(s); }
#else
#define PRINT(s, x)
#define PRINTS(x)
#define PRINTD(x)
#define PRINTXY(s,x,y)
#define PRINTSTATE(s)
#endif
// Hardware pin definitions.
// All momentary on switches are initialized PULLUP
const uint8_t UP_PIN = 2;
const uint8_t RIGHT_PIN = 3;
const uint8_t DOWN_PIN = 4;
const uint8_t LEFT_PIN = 5;
const uint8_t SELECT_PIN = 6;
const uint8_t ENTER_PIN = 7;
const uint8_t BEEPER_PIN = 9;
// Define the number of devices in the chain and the SPI hardware interface
// NOTE: These pin numbers will probably not work with your hardware and may
// need to be adapted
const MD_MAX72XX::moduleType_t HARDWARE_TYPE = MD_MAX72XX::FC16_HW;
const uint8_t X_DEVICES = 4;
const uint8_t Y_DEVICES = 5;
const uint8_t CLK_PIN = 12; // or SCK
const uint8_t DATA_PIN = 11; // or MOSI
const uint8_t CS_PIN = 10; // or SS
// SPI hardware interface
MD_MAXPanel mp = MD_MAXPanel(HARDWARE_TYPE, CS_PIN, X_DEVICES, Y_DEVICES);
// Arbitrary pins
// MD_MAXPanel mx = MD_MAXPanel(HARDWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, X_DEVICES, Y_DEVICES);
uint16_t FIELD_TOP, FIELD_RIGHT; // needs to be initialized in setup()
const uint16_t FIELD_LEFT = 0;
const uint8_t BRICK_SIZE_DEFAULT = 3;
const uint8_t BAT_SIZE_DEFAULT = 3; // must be an odd number
const uint8_t BAT_EDGE_OFFSET = 1;
const char TITLE_TEXT[] = "METEOR";
const uint16_t SPLASH_DELAY = 3000; // in milliseconds
const char GAME_TEXT[] = "GAME";
const char OVER_TEXT[] = "OVER";
const uint16_t GAME_OVER_DELAY = 3000; // in milliseconds
const uint8_t FONT_NUM_WIDTH = 3;
const uint16_t MAX_SCORE = 999;
const uint16_t START_SCORE = 25;
const uint8_t ROID_BIG_SIZE = 4;
const uint8_t ROID_MID_SIZE = 2;
const uint8_t ROID_SML_SIZE = 1;
const uint8_t MAX_BULLETS = 5;
// A class to encapsulate the shooter
// Shooter is at the bottom of the display shooting bullets upwards
class cGun
{
private:
uint16_t _x, _y; // the position of the tip of the gun
uint16_t _xmin, _xmax; // the max and min gun boundaries
uint8_t _pinLeft; // the pin for the left switch
uint8_t _pinRight; // the pin for the right switch
uint16_t _moveDelay; // the delay between possible moves of the gun in milliseconds
uint32_t _timeLastMove; // the millis() value for the last time we moved the gun
public:
void begin(uint16_t x, uint16_t y, uint16_t xmin, uint16_t xmax, uint8_t pinL, uint8_t pinR)
{
_x = x;
_y = y;
_xmin = xmin;
_xmax = xmax;
_pinLeft = pinL;
_pinRight = pinR;
_moveDelay = 25;
pinMode(_pinLeft, INPUT_PULLUP);
pinMode(_pinRight, INPUT_PULLUP);
}
uint16_t getX(void) { return (_x); }
uint16_t getY(void) { return (_y); }
void draw(void) { mp.setPoint(_x, _y, true); mp.drawHLine(_y-1, _x-1, _x+1, true); }
void erase(void) { mp.setPoint(_x, _y, false); mp.drawHLine(_y-1, _x-1, _x+1, false); }
bool anyKey(void) { return(digitalRead(_pinLeft) == LOW || digitalRead(_pinRight) == LOW); }
bool move(void)
{
bool moveLeft = (digitalRead(_pinLeft) == LOW);
bool moveRight = (digitalRead(_pinRight) == LOW);
// if this is the time for a move?
if (millis() - _timeLastMove <= _moveDelay)
return(false);
_timeLastMove = millis();
mp.update(false);
if (moveRight)
{
//PRINTS("\n-- GUN move right");
erase();
_x++;
if (_x + 1 > _xmax) _x--; // keep within right boundary
draw();
}
else if (moveLeft)
{
//PRINTS("\n-- GUN move left");
erase();
_x--;
if (_x - 1 < _xmin) _x++; // keep within left boundary
draw();
}
mp.update(true);
return((moveLeft || moveRight));
}
bool checkHits(uint16_t x, uint16_t y)
{
bool b = false;
if (_y >= y)
b = (_x == x) || ((y == _y - 1) && (x == _x - 1 || x == _x + 1));
return(b);
}
};
// A class to encapsulate all the bullets
// Bullets are fired from the gun and move straight upwards
class cBullet
{
private:
typedef struct bullet_t
{
uint16_t x, y; // the position of the center of the bullet
uint32_t timeLastMove; // time bullet last moved
bullet_t *next; // next in the list
};
uint8_t _pinShoot; // the shooting switch pin
uint16_t _ymin, _ymax; // maximum bounds for the bullet
uint32_t _moveDelay; // delay between bullets moves in milliseconds
uint16_t _shootDelay; // the delay between shots in milliseconds
uint32_t _timeLastShoot; // the last time the gun was shot in milliseconds
bullet_t *_bullets; // the queue of bullets being fired
bullet_t *_deleted; // delete bullets list
bullet_t *_pScan; // for getFirst(), getNext()
uint8_t _count; // count the number of bullets
bullet_t *add(uint16_t x, uint16_t y)
{
bullet_t* pb = nullptr;
if (_count < MAX_BULLETS)
{
if (_deleted == nullptr)
{
pb = new bullet_t;
PRINTS(" new");
}
else
{
// take one from the front of the deleted queue
pb = _deleted;
_deleted = _deleted->next;
PRINTS(" recyle");
}
// save the data
if (pb != nullptr)
{
_count++;
pb->x = x;
pb->y = y;
pb->timeLastMove = 0;
pb->next = _bullets;
_bullets = pb;
}
}
return(pb);
}
void del(bullet_t *pbPrev, bullet_t * pb)
{
if (pbPrev == nullptr)
_bullets = pb->next;
else
pbPrev->next = pb->next;
_count--;
// save to the front of the deleted list
pb->next = _deleted;
_deleted = pb;
}
public:
void begin(uint16_t ymin, uint16_t ymax, uint8_t pinShoot)
{
_moveDelay = 50;
_shootDelay = _moveDelay * 3;
_ymin = ymin;
_ymax = ymax;
_deleted = _bullets = nullptr;
_pinShoot = pinShoot;
_count = 0;
pinMode(_pinShoot, INPUT_PULLUP);
}
bool getFirstXY(uint16_t &x, uint16_t &y)
{
_pScan = _bullets;
if (_pScan == nullptr)
return(false);
x = _pScan->x;
y = _pScan->y;
_pScan = _pScan->next;
return(true);
}
bool getNextXY(uint16_t &x, uint16_t &y)
{
if (_pScan == nullptr)
return(false);
x = _pScan->x;
y = _pScan->y;
_pScan = _pScan->next;
return(true);
}
void draw(bullet_t *pb) { if (pb != nullptr) mp.setPoint(pb->x, pb->y, true); } //PRINTXY("\nbullet @", pb->x, pb->y); }
void erase(bullet_t *pb) { if (pb != nullptr) mp.setPoint(pb->x, pb->y, false); }
void reset(void) { while (_bullets != nullptr) { erase(_bullets); del(nullptr, _bullets); }}
bool empty(void) { return (_bullets == nullptr); }
void kill(uint16_t x, uint16_t y)
// search for and kill the bullet at (x, y)
{
bullet_t *pb = _bullets;
bullet_t *pbPrev = nullptr;
while (pb!= nullptr)
{
if (pb->x == x && pb->y == y)
{
erase(pb);
del(pbPrev, pb);
break; // found it, no need to look further
}
// advance pointers
pbPrev = pb;
pb = pb->next;
}
}
void move(void)
{
bullet_t *pb = _bullets;
bullet_t *pbPrev = nullptr;
mp.update(false);
while (pb != nullptr)
{
// is this the time for a move?
if (millis() - pb->timeLastMove >= _moveDelay)
{
pb->timeLastMove = millis();
// do the animation
erase(pb);
pb->y++;
if (pb->y >= FIELD_TOP)
{
PRINTS("\n-- BULLET ending");
del(pbPrev, pb);
pb = pbPrev; // we have just deleted pb!
}
else
draw(pb);
}
// advance pointers
pbPrev = pb;
if (pb != nullptr) pb = pb->next;
}
mp.update(true);
}
bool shoot(uint16_t x, uint16_t y)
// shoot the next bullet if switch is pressed
{
bool b = false;
// if this is the time for a move?
if (millis() - _timeLastShoot <= _shootDelay)
return(b);
_timeLastShoot = millis();
// now check if the switch is pressed
if (digitalRead(_pinShoot) == LOW)
{
PRINTS("\n-- BULLET shoot");
b = (add(x, y) != nullptr);
}
return(b);
}
};
// A class to encapsulate all the meteors
class cMeteors
{
private:
typedef struct meteor_t
{
// NOTE - these are uint8_t to save RAM
uint8_t x, y; // lower tip coordinate for this meteor
uint8_t dx, dy; // speed in the x, y direction
uint8_t size; // size of the meteor (BIG, MID, SML)
uint32_t timeLastMove;// when this meteor last moved
meteor_t *next; // the next in the linked list
};
uint32_t _timeTick; // base time multiplied by size
uint32_t _timeLastBorn; // last time an meteor was born
uint32_t _timeGestation; // time between meteors being born
meteor_t *_meteors; // start of the list
meteor_t *_deleted; // the deleted list
meteor_t *_pScan; // for getFirst(), getNext()
void drawBig(meteor_t *pa, bool b)
{
mp.setPoint(pa->x, pa->y, b);
if ((pa->y + 1) < FIELD_TOP)
{
mp.drawHLine(pa->y + 1, pa->x - 1, pa->x + 1, b);
if ((pa->y + 2) < FIELD_TOP)
mp.setPoint(pa->x, pa->y + 2, b);
}
}
void drawMid(meteor_t *pa, bool b)
{
mp.setPoint(pa->x, pa->y, b);
if ((pa->y + 1) < FIELD_TOP)
{
mp.setPoint(pa->x, pa->y + 1, b);
}
}
void drawSml(meteor_t *pa, bool b)
{
mp.setPoint(pa->x, pa->y, b);
}
void draw(meteor_t *pa)
{
//PRINTXY("\nmeteor @", pa->x, pa->y);
if (pa == nullptr) return;
switch (pa->size)
{
case ROID_BIG_SIZE: drawBig(pa, true); break;
case ROID_MID_SIZE: drawMid(pa, true); break;
case ROID_SML_SIZE: drawSml(pa, true); break;
}
}
void erase(meteor_t *pa)
{
if (pa == nullptr) return;
switch (pa->size)
{
case ROID_BIG_SIZE: drawBig(pa, false); break;
case ROID_MID_SIZE: drawMid(pa, false); break;
case ROID_SML_SIZE: drawSml(pa, false); break;
}
}
meteor_t *add(uint16_t x, uint16_t y)
// add a brick to the start of the list
{
meteor_t *pa = nullptr;
if (_deleted == nullptr)
{
pa = new meteor_t;
PRINTS("new");
}
else
{
// take from the front of the deleted list
pa = _deleted;
_deleted = _deleted->next;
PRINTS("recyle");
}
if (pa != nullptr)
{
pa->x = x;
pa->y = y;
pa->dx = 0;
pa->dy = -1;
pa->size = ROID_BIG_SIZE;
pa->timeLastMove = 0;
pa->next = _meteors;
_meteors = pa;
}
return(pa);
}
void del(meteor_t *paPrev, meteor_t *pa)
// delete the specified brick from the list
{
if (paPrev == nullptr)
_meteors = pa->next;
else
paPrev->next = pa->next;
// save to the front of the deleted list
pa->next = _deleted;
_deleted = pa;
}
bool generate(void)
{
if (millis() - _timeLastBorn < _timeGestation)
return(false);;
_timeLastBorn = millis();
if (_timeGestation > 500) _timeGestation -= 10; // speed up the number born over the game duration
{
uint16_t x = FIELD_LEFT + 2 + random(FIELD_RIGHT - FIELD_LEFT - 4);
uint16_t y = FIELD_TOP - 1;
PRINTXY("\n-- ASTEROID create @", x, y);
add(x, y);
}
return(true);
}
public:
void begin()
{
_meteors = _deleted = nullptr;
_timeTick = 250;
_timeGestation = 5000;
}
bool emptyField(void) { return(_meteors == nullptr); }
void drawField(void) { for (meteor_t *pa = _meteors; pa != nullptr; pa = pa->next) draw(pa); }
void eraseField(void) { while (_meteors != nullptr) { erase(_meteors); del(nullptr, _meteors); } }
bool getFirstXY(uint16_t &x, uint16_t &y)
{
_pScan = _meteors;
if (_pScan == nullptr)
return(false);
x = _pScan->x;
y = _pScan->y;
_pScan = _pScan->next;
return(true);
}
bool getNextXY(uint16_t &x, uint16_t &y)
{
if (_pScan == nullptr)
return(false);
x = _pScan->x;
y = _pScan->y;
_pScan = _pScan->next;
return(true);
}
void move(void)
{
meteor_t *pa = _meteors;
meteor_t *paPrev = nullptr;
mp.update(false);
generate(); // create a new one if time to do so
while (pa != nullptr)
{
// is this the time for a move?
if (millis() - pa->timeLastMove >= _timeTick * pa->size)
{
pa->timeLastMove = millis();
// do the animation
erase(pa);
// y==0 neede to persist after the increment so the collision code can find it.
// Time to deletw it now before calculating moves.
if (pa->y == 0)
{
PRINTS("\n-- ASTEROID ending bottom");
del(paPrev, pa);
pa = paPrev; // we have just deleted pa!
}
else
{
pa->y += pa->dy;
pa->x += pa->dx;
if (pa->x <= FIELD_LEFT || pa->x >= FIELD_RIGHT)
{
PRINTS("\n-- ASTEROID ending side");
del(paPrev, pa);
pa = paPrev; // we have just deleted pa!
}
else
draw(pa);
}
}
// advance pointers
paPrev = pa;
if (pa != nullptr) pa = pa->next;
}
mp.update(true);
}
uint8_t checkHits(uint16_t x, uint16_t y)
// Returns the points for the meteor hit
{
meteor_t *pa = _meteors;
meteor_t *paPrev = nullptr;
meteor_t *paNew;
uint8_t points = 0;
bool notFound = true;
//PRINTXY("\n--- CHECKHITS for ", x, y);
while (pa != nullptr && notFound)
{
switch (pa->size)
{
case ROID_SML_SIZE: // front on point hit only
if (pa->x == x && pa->y == y)
{
//PRINTXY("\n-- ASTEROID SML hit @", pa->x, pa->y);
// smallest one just gets deleted
erase(pa);
del(paPrev, pa);
points = 4;
notFound = false;
}
break;
case ROID_MID_SIZE: // front on point hit only
if (pa->x == x && pa->y == y)
{
//PRINTXY("\n-- ASTEROID MID hit @", pa->x, pa->y);
// needs to be split
erase(pa);
paNew = add(pa->x, pa->y);
pa->dx = -1;
paNew->dx = 1;
pa->size = paNew->size = ROID_SML_SIZE;
draw(pa);
draw(paNew);
points = 2;
notFound = false;
}
break;
case ROID_BIG_SIZE: // front on point and either side hits
if ((pa->x == x && pa->y == y) ||
((pa->x - 1 == x || pa->x + 1 == x) && (pa->y + 1 == y)))
{
//PRINTXY("\n-- ASTEROID BIG hit @", pa->x, pa->y);
// needs to be split
erase(pa);
paNew = add(pa->x, pa->y);
pa->dx = -1;
paNew->dx = 1;
pa->size = paNew->size = ROID_MID_SIZE;
draw(pa);
draw(paNew);
points = 1;
notFound = false;
}
break;
default:
PRINT("\n--- CHECKHIT UNKNOWN size=", pa->size);
break;
}
// advance the pointers
paPrev = pa;
if (pa != nullptr) pa = pa->next;
}
return(points);
}
};
// main objects coordinated by the code logic
cScore score;
cGun gun;
cBullet bullets;
cMeteors meteors;
cSound sound;
void setupField(void)
// Draw the playing field at the start of the game.
{
mp.clear();
mp.drawHLine(FIELD_TOP, FIELD_LEFT, FIELD_RIGHT);
mp.drawVLine(FIELD_LEFT, 0, FIELD_TOP);
mp.drawVLine(FIELD_RIGHT, 0, FIELD_TOP);
gun.draw();
score.draw();
}
void setup()
{
#if DEBUG
Serial.begin(57600);
#endif
PRINTS("\n[MD_MAXPanel_Bricks]");
randomSeed(seedOut(31, RANDOM_SEED_PORT));
mp.begin();
mp.setFont(_Fixed_5x3);
mp.setIntensity(4);
//mp.setRotation(MD_MAXPanel::ROT_90);
// one time initialization
FIELD_TOP = mp.getYMax() - mp.getFontHeight() - 2;
FIELD_RIGHT = mp.getXMax();
gun.begin((FIELD_RIGHT - FIELD_LEFT) / 2, BAT_EDGE_OFFSET, FIELD_LEFT + 1, FIELD_RIGHT - 1, LEFT_PIN, RIGHT_PIN);
bullets.begin(1, FIELD_TOP-1, SELECT_PIN);
score.limit(MAX_SCORE); // set width() so we can use it below
score.begin(&mp, FIELD_RIGHT - (score.width() * (FONT_NUM_WIDTH + mp.getCharSpacing())) + mp.getCharSpacing(), FIELD_TOP + 1 + mp.getFontHeight(), MAX_SCORE);
sound.begin(BEEPER_PIN);
}
void loop(void)
{
static enum { S_SPLASH, S_INIT, S_WAIT_START, S_GAME_PLAY, S_GAME_OVER } runState = S_SPLASH;
switch (runState)
{
case S_SPLASH: // show splash screen at start
PRINTSTATE("SPLASH");
{
const uint16_t border = 2;
mp.clear();
mp.drawRectangle(border, border, mp.getXMax() - border, mp.getYMax() - border);
mp.drawLine(0, 0, border, border);
mp.drawLine(0, mp.getYMax(), border, mp.getYMax() - border);
mp.drawLine(mp.getXMax(), 0, mp.getXMax() - border, border);
mp.drawLine(mp.getXMax(), mp.getYMax(), mp.getXMax() - border, mp.getYMax() - border);
mp.drawText((mp.getXMax() - mp.getTextWidth(TITLE_TEXT)) / 2, (mp.getYMax() + mp.getFontHeight()) / 2, TITLE_TEXT);
sound.splash();
delay(SPLASH_DELAY);
runState = S_INIT;
}
break;
case S_INIT: // initialize for a new game
PRINTSTATE("INIT");
bullets.reset();
score.set(START_SCORE);
setupField();
meteors.begin();
runState = S_WAIT_START;
PRINTSTATE("WAIT_START");
break;
case S_WAIT_START: // waiting for the start of a new game
if (gun.anyKey())
{
PRINTS("\n-- Starting Game");
sound.start();
runState = S_GAME_PLAY;
PRINTSTATE("GAME_PLAY");
}
break;
case S_GAME_PLAY: // playing a point
// handle shooting first
if (score.score() != 0) // we have some bullets ...
if (bullets.shoot(gun.getX(), gun.getY() + 1)) // ... and we can shoot ...
{
score.decrement(); // ... then we have one bullet less
sound.hit();
}
// then run animations
gun.move();
meteors.move();
bullets.move();
// now check for bullet collisions
{
uint16_t x, y;
//PRINTS("\n--- CHECK bullets collisions");
if (bullets.getFirstXY(x, y))
{
do
{
uint8_t points = meteors.checkHits(x, y);
if (points != 0)
{
//PRINT(", hit=", points);
bullets.kill(x, y); // bullet is now dead
//PRINTS(", kill");
sound.bounce();
//PRINTS(", sound");
score.increment(points);
//PRINTS(", score");
}
} while (bullets.getNextXY(x, y));
}
// now check for gun collisions
//PRINTS("\n--- CHECK guns collisions");
if (meteors.getFirstXY(x, y))
{
do
{
if (gun.checkHits(x, y))
runState = S_GAME_OVER;
} while (meteors.getNextXY(x, y) && runState != S_GAME_OVER);
}
}
//PRINTS("\n--- CHECK collisions done");
// finally, have we reached the end of the line?
if (bullets.empty() && score.score() == 0)
runState = S_GAME_OVER;
break;
case S_GAME_OVER:
PRINTSTATE("GAME_OVER");
meteors.eraseField();
bullets.reset();
mp.drawText((mp.getXMax() - mp.getTextWidth(GAME_TEXT)) / 2, FIELD_TOP / 2 + mp.getFontHeight() + 1, GAME_TEXT);
mp.drawText((mp.getXMax() - mp.getTextWidth(OVER_TEXT)) / 2, FIELD_TOP / 2 - 1, OVER_TEXT);
sound.over();
delay(GAME_OVER_DELAY);
runState = S_INIT;
break;
}
}

View File

@@ -0,0 +1,45 @@
#pragma once
// Random seed creation --------------------------
// Adapted from http://www.utopiamechanicus.com/article/arduino-better-random-numbers/
const uint8_t RANDOM_SEED_PORT = A3; // port read for random seed
uint16_t bitOut(uint8_t port)
{
static bool firstTime = true;
uint32_t prev = 0;
uint32_t bit1 = 0, bit0 = 0;
uint32_t x = 0, limit = 99;
if (firstTime)
{
firstTime = false;
prev = analogRead(port);
}
while (limit--)
{
x = analogRead(port);
bit1 = (prev != x ? 1 : 0);
prev = x;
x = analogRead(port);
bit0 = (prev != x ? 1 : 0);
prev = x;
if (bit1 != bit0)
break;
}
return(bit1);
}
uint32_t seedOut(uint16_t noOfBits, uint8_t port)
{
// return value with 'noOfBits' random bits set
uint32_t seed = 0;
for (int i = 0; i<noOfBits; ++i)
seed = (seed << 1) | bitOut(port);
return(seed);
}
//------------------------------------------------------------------------------

View File

@@ -0,0 +1,54 @@
#pragma once
#include <MD_MAXPanel.h>
// A class to encapsulate the score display
class cScore
{
private:
MD_MAXPanel *_mp; // the MAXPanel object for drawing the score
uint16_t _score; // the score
uint16_t _x, _y; // coordinate of top left for display
uint8_t _width; // number of digits wide
uint16_t _limit; // maximum value allowed
public:
void begin(MD_MAXPanel *mp, uint16_t x, uint16_t y, uint16_t maxScore) { _mp = mp; _x = x, _y = y; limit(maxScore); reset(); }
void reset(void) { erase(); _score = 0; draw(); }
void set(uint16_t s) { if (s <= _limit) { erase(); _score = s; draw(); } }
void increment(uint16_t inc = 1) { if (_score + inc <= _limit) { erase(); _score += inc; draw(); } }
void decrement(uint16_t dec = 1) { if (_score >= dec) { erase(); _score -= dec; draw(); } }
uint16_t score(void) { return(_score); }
void erase(void) { draw(false); }
uint16_t width(void) { return(_width); }
void limit(uint16_t m)
{
erase(); // width may change, so delete with the curret parameters
_limit = m;
// work out how many digits this is
_width = 0;
do
{
_width++;
m /= 10;
} while (m != 0);
}
void draw(bool state = true)
{
char sz[_width + 1];
uint16_t s = _score;
// PRINT("\n-- SCORE: ", _score);
sz[_width] = '\0';
for (int i = _width - 1; i >= 0; --i)
{
sz[i] = (s % 10) + '0';
s /= 10;
}
_mp->drawText(_x, _y, sz, MD_MAXPanel::ROT_0, state);
}
};

View File

@@ -0,0 +1,47 @@
#pragma once
// A class to encapsulate primitive sound effects
class cSound
{
private:
const uint16_t EOD = 0; // End Of Data marker
uint8_t _pinBeep; // the pin to use for beeping
// Sound data - frequency followed by duration in pairs.
// Data ends in End Of Data marker EOD.
const uint16_t soundSplash[1] PROGMEM = { EOD };
const uint16_t soundHit[3] PROGMEM = { 1000, 50, EOD };
const uint16_t soundBounce[3] PROGMEM = { 500, 50, EOD };
const uint16_t soundPoint[3] PROGMEM = { 150, 150, EOD };
const uint16_t soundStart[7] PROGMEM = { 250, 100, 500, 100, 1000, 100, EOD };
const uint16_t soundOver[7] PROGMEM = { 1000, 100, 500, 100, 250, 100, EOD };
void playSound(const uint16_t *table)
// Play sound table data. Data table must end in EOD marker.
{
uint8_t idx = 0;
//PRINTS("\nTone Data ");
while (table[idx] != EOD)
{
uint16_t t = table[idx++];
uint16_t d = table[idx++];
//PRINTXY("-", t, d);
tone(_pinBeep, t);
delay(d);
}
//PRINTS("-EOD");
noTone(_pinBeep); // be quiet now!
}
public:
void begin(uint8_t pinBeep) { _pinBeep = pinBeep; }
void splash(void) { playSound(soundSplash); }
void start(void) { playSound(soundStart); }
void hit(void) { playSound(soundHit); }
void bounce(void) { playSound(soundBounce); }
void point(void) { playSound(soundPoint); }
void over(void) { playSound(soundOver); }
};

View File

@@ -0,0 +1,99 @@
MD_MAX72XX::fontType_t _Fixed_5x3[] PROGMEM = {
'F', 1, 32, 127, 5,
2, 0, 0, // 32 - 'Space'
1, 23, // 33 - '!'
3, 3, 0, 3, // 34 - '"'
3, 31, 10, 31, // 35 - '#'
3, 22, 31, 13, // 36 - '$'
3, 9, 4, 18, // 37 - '%'
3, 10, 21, 26, // 38 - '&'
1, 3, // 39
2, 14, 17, // 40 - '('
2, 17, 14, // 41 - ')'
3, 10, 4, 10, // 42 - '*'
3, 4, 14, 4, // 43 - '+'
2, 16, 8, // 44 - ','
3, 4, 4, 4, // 45 - '-'
1, 16, // 46 - '.'
3, 8, 4, 2, // 47 - '/'
3, 31, 17, 31, // 48 - '0'
2, 0, 31, // 49 - '1'
3, 29, 21, 23, // 50 - '2'
3, 17, 21, 31, // 51 - '3'
3, 7, 4, 31, // 52 - '4'
3, 23, 21, 29, // 53 - '5'
3, 31, 21, 29, // 54 - '6'
3, 1, 1, 31, // 55 - '7'
3, 31, 21, 31, // 56 - '8'
3, 23, 21, 31, // 57 - '9'
1, 10, // 58 - ':'
2, 16, 10, // 59 - ';'
3, 4, 10, 17, // 60 - '<'
3, 10, 10, 10, // 61 - '='
3, 17, 10, 4, // 62 - '>'
3, 1, 21, 3, // 63 - '?'
3, 14, 21, 22, // 64 - '@'
3, 30, 5, 30, // 65 - 'A'
3, 31, 21, 10, // 66 - 'B'
3, 14, 17, 17, // 67 - 'C'
3, 31, 17, 14, // 68 - 'D'
3, 31, 21, 17, // 69 - 'E'
3, 31, 5, 1, // 70 - 'F'
3, 14, 17, 29, // 71 - 'G'
3, 31, 4, 31, // 72 - 'H'
3, 17, 31, 17, // 73 - 'I'
3, 8, 16, 15, // 74 - 'J'
3, 31, 4, 27, // 75 - 'K'
3, 31, 16, 16, // 76 - 'L'
3, 31, 2, 31, // 77 - 'M'
3, 31, 14, 31, // 78 - 'N'
3, 14, 17, 14, // 79 - 'O'
3, 31, 5, 2, // 80 - 'P'
3, 14, 25, 30, // 81 - 'Q'
3, 31, 5, 26, // 82 - 'R'
3, 18, 21, 9, // 83 - 'S'
3, 1, 31, 1, // 84 - 'T'
3, 15, 16, 15, // 85 - 'U'
3, 7, 24, 7, // 86 - 'V'
3, 15, 28, 15, // 87 - 'W'
3, 27, 4, 27, // 88 - 'X'
3, 3, 28, 3, // 89 - 'Y'
3, 25, 21, 19, // 90 - 'Z'
2, 31, 17, // 91 - '['
3, 2, 4, 8, // 92 - '\'
2, 17, 31, // 93 - ']'
3, 2, 1, 2, // 94 - '^'
3, 16, 16, 16, // 95 - '_'
2, 1, 2, // 96 - '`'
3, 12, 18, 28, // 97 - 'a'
3, 31, 18, 12, // 98 - 'b'
3, 12, 18, 18, // 99 - 'c'
3, 12, 18, 31, // 100 - 'd'
3, 12, 26, 20, // 101 - 'e'
3, 4, 31, 5, // 102 - 'f'
3, 20, 26, 12, // 103 - 'g'
3, 31, 2, 28, // 104 - 'h'
1, 29, // 105 - 'i'
2, 16, 13, // 106 - 'j'
3, 31, 8, 20, // 107 - 'k'
1, 31, // 108 - 'l'
3, 30, 6, 30, // 109 - 'm'
3, 30, 2, 28, // 110 - 'n'
3, 12, 18, 12, // 111 - 'o'
3, 30, 10, 4, // 112 - 'p'
3, 4, 10, 30, // 113 - 'q'
2, 30, 4, // 114 - 'r'
3, 20, 30, 10, // 115 - 's'
3, 4, 30, 4, // 116 - 't'
3, 14, 16, 30, // 117 - 'u'
3, 14, 16, 14, // 118 - 'v'
3, 14, 24, 14, // 119 - 'w'
3, 18, 12, 18, // 120 - 'x'
3, 22, 24, 14, // 121 - 'y'
3, 26, 30, 22, // 122 - 'z'
3, 4, 27, 17, // 123 - '{'
1, 27, // 124 - '|'
3, 17, 27, 4, // 125 - '}'
3, 6, 2, 3, // 126 - '~'
3, 31, 31, 31, // 127 - 'Full Block'
};

View File

@@ -0,0 +1,466 @@
// Implements the game of Pong using MD_MAXPanel library
//
// Hardware Used
// =============
// UP_PIN - left bat up switch, INPUT_PULLUP
// RIGHT_PIN - right bat up switch, INPUT_PULLUP
// RIGHT_PIN - left bat up switch, INPUT_PULLUP
// DOWN_PIN - right bat up switch, INPUT_PULLUP
// SELECT_PIN - unused
// ENTER_PIN - unused
// BEEPER_PIN - piezo speaker
// CLK_PIN, DATA_PIN, CS_PIN - LED matrix display connections
//
// Libraries used
// ==============
// MD_MAX72XX available from https://github.com/MajicDesigns/MD_MAX72XX
//
// Rules of the Game
// =================
// Simple game like table tennis where each player has to bat the ball to keep
// it in play. Points awarded when the ball goes out at the opponents side. First
// to reach MAX_SCORE is the winner.
#include <MD_MAXPanel.h>
#include "Font5x3.h"
#include "score.h"
#include "sound.h"
// Turn on debug statements to the serial output
#define DEBUG 0
#if DEBUG
#define PRINT(s, x) { Serial.print(F(s)); Serial.print(x); }
#define PRINTS(x) { Serial.print(F(x)); }
#define PRINTD(x) { Serial.print(x, DEC); }
#define PRINTXY(s, x, y) { Serial.print(s); Serial.print(F("(")); Serial.print(x); Serial.print(F(",")); Serial.print(y); Serial.print(")"); }
#else
#define PRINT(s, x)
#define PRINTS(x)
#define PRINTD(x)
#define PRINTXY(s, x, y)
#endif
// Hardware pin definitions.
// All momentary on switches are initialised INPUT_PULLUP
const uint8_t UP_PIN = 2;
const uint8_t RIGHT_PIN = 3;
const uint8_t DOWN_PIN = 4;
const uint8_t LEFT_PIN = 5;
const uint8_t SELECT_PIN = 6;
const uint8_t ENTER_PIN = 7;
const uint8_t BEEPER_PIN = 9;
// Define the number of devices in the chain and the SPI hardware interface
// NOTE: These pin numbers will probably not work with your hardware and may
// need to be adapted
const MD_MAX72XX::moduleType_t HARDWARE_TYPE = MD_MAX72XX::FC16_HW;
const uint8_t X_DEVICES = 4;
const uint8_t Y_DEVICES = 5;
const uint8_t CLK_PIN = 13; // or SCK
const uint8_t DATA_PIN = 11; // or MOSI
const uint8_t CS_PIN = 10; // or SS
// SPI hardware interface
MD_MAXPanel mp = MD_MAXPanel(HARDWARE_TYPE, CS_PIN, X_DEVICES, Y_DEVICES);
// Arbitrary pins
// MD_MAXPanel mx = MD_MAXPanel(HARDWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, X_DEVICES, Y_DEVICES);
uint16_t FIELD_TOP; // needs to be initialised in setup()
const uint16_t FIELD_BOTTOM = 0;
const uint8_t BAT_SIZE_DEFAULT = 3;
const uint8_t BAT_EDGE_OFFSET = 1;
const char TITLE_TEXT[] = "PONG";
const uint16_t SPLASH_DELAY = 5000; // in milliseconds
const char GAME_TEXT[] = "GAME";
const char OVER_TEXT[] = "OVER";
const uint16_t GAME_OVER_DELAY = 3000; // in milliseconds
const uint8_t FONT_NUM_WIDTH = 3;
const uint8_t MAX_SCORE = 11;
// A class to encapsulate the pong bat
// Bats are used either side of the display, moving up and down
class cPongBat
{
private:
uint16_t _x, _y; // the position of the center of the bat
uint16_t _ymin, _ymax; // the max and min bat boundaries
int8_t _vel; // the velocity of the bat (+1 for moving up, -1 moving down)
uint8_t _size; // the size in pixels for the bat (odd number)
uint8_t _pinUp; // the pin for the up switch
uint8_t _pinDown; // the pin for th down switch
uint16_t _batDelay; // the delay between possible moves of the bat in milliseconds
uint32_t _timeLastMove; // the millis() value for the last time we moved the bat
public:
enum hitType_t { NO_HIT, CORNER_HIT, FLAT_HIT };
void begin(uint16_t x, uint16_t y, uint16_t ymin, uint16_t ymax, uint8_t size, uint8_t pinU, uint8_t pinD)
{
_x = x;
_y = y;
_ymin = ymin;
_ymax = ymax;
_vel = 0;
_size = size;
_pinUp = pinU;
_pinDown = pinD;
_batDelay = 40;
pinMode(_pinUp, INPUT_PULLUP);
pinMode(_pinDown, INPUT_PULLUP);
}
uint16_t getX(void) { return (_x); }
uint16_t getY(void) { return (_y); }
int8_t getVelocity(void) { return(_vel); }
void draw(void) { mp.drawVLine(_x, _y - (_size / 2), _y + (_size / 2), true); }
void erase(void) { mp.drawVLine(_x, _y - (_size / 2), _y + (_size / 2), false); }
bool anyKey(void) { return((digitalRead(_pinUp) == LOW) || (digitalRead(_pinDown) == LOW)); }
hitType_t hit(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1)
{
int16_t dy = y1 - y0;
// if we are not in the same x plane as the bat there can be no hit
if (x1 != _x) return(NO_HIT);
// if the ball is at
// - the top of the bat and traveling down, or
// - the bottom of the bat and traveling up
// then it is a corner hit
if ((y1 == _y + (_size / 2) && dy < 0) || (y1 == _y - (_size / 2) && dy>0))
return(CORNER_HIT);
// the ball is between the top and bottom of the bat boundaries inclusive
// then it is a flat hit
if (y1 <= _y + (_size / 2) && y1 >= _y - (_size / 2))
return(FLAT_HIT);
// and in case we missed something, a default no hit
return(NO_HIT);
}
void move(void)
{
bool moveUp = (digitalRead(_pinUp) == LOW);
bool moveDown = (digitalRead(_pinDown) == LOW);
// if this is the time for a move?
if (millis() - _timeLastMove <= _batDelay)
return;
_timeLastMove = millis();
mp.update(false);
if (moveUp)
{
PRINTS("\n-- BAT move up");
erase();
_vel = 1;
_y++;
if (_y + (_size/2) > _ymax) _y--; // keep within top boundary
draw();
}
else if (moveDown)
{
PRINTS("\n-- BAT move down");
erase();
_vel = -1;
_y--;
if (_y - (_size/2) < _ymin) _y++; // keep within bottom boundary
draw();
}
else
_vel = 0;
mp.update(true);
}
};
// A class to encapsulate the pong ball
// Ball will bounce around. Bounces off the edges and bats.
class cPongBall
{
private:
uint16_t _x, _y; // the position of the center of the ball
int8_t _dx, _dy; // the offsets for the x and y direction
uint32_t _timeLastMove; // last time the ball was moved
uint16_t _ballDelay; // the delay between ball moves in milliseconds
bool _run; // ball is running when true
public:
enum bounce_t { BOUNCE_NONE, BOUNCE_BACK, BOUNCE_TOP, BOUNCE_BOTTOM, BOUNCE_LEFT, BOUNCE_RIGHT };
void begin(uint16_t x, uint16_t y)
{
reset(x, y);
_dx = _dy = 1;
_timeLastMove = 0;
_ballDelay = 100;
_run = false;
}
uint16_t getX(void) { return (_x); }
uint16_t getY(void) { return (_y); }
uint16_t getNextX(void) { return (_x + _dx); }
uint16_t getNextY(void) { return (_y + _dy); }
void start(void) { _run = true; }
void stop(void) { _run = false; }
void draw(void) { mp.setPoint(_x, _y, true); } //PRINTS("\nball@"); PRINTXY(_x, _y); }
void erase(void) { mp.setPoint(_x, _y, false); }
void reset(uint16_t x, uint16_t y) { _x = x; _y = y; }
bool move(void)
{
// if this is the time for a move?
if (_run && millis() - _timeLastMove <= _ballDelay)
return(false);
_timeLastMove = millis();
// do the animation
mp.update(false);
erase();
_x = _x + _dx;
_y = _y + _dy;
draw();
mp.update(true);
return(true);
}
void bounce(bounce_t b)
{
switch (b)
{
case BOUNCE_TOP:
case BOUNCE_BOTTOM: _dy = -_dy; break;
case BOUNCE_LEFT:
case BOUNCE_RIGHT: _dx = -_dx; break;
case BOUNCE_BACK: _dx = -_dx; _dy = -_dy; break;
}
}
};
// main objects coordinated by the code logic
cPongBat batL, batR;
cScore scoreL, scoreR;
cPongBall ball;
cSound sound;
void centerLine(void)
// Dotted line down the middle
{
bool bOn = true;
mp.update(false);
for (uint16_t y = FIELD_BOTTOM + 1; y < FIELD_TOP; y++)
{
mp.setPoint(mp.getXMax() / 2, y, bOn);
mp.setPoint((mp.getXMax() + 1) / 2, y, bOn);
bOn = !bOn;
}
mp.update(true);
}
void setupField(void)
// Draw the playing field at the start of the game.
{
mp.clear();
mp.drawHLine(FIELD_BOTTOM, 0, mp.getXMax());
mp.drawHLine(FIELD_TOP, 0, mp.getXMax());
centerLine();
batL.draw();
batR.draw();
scoreL.draw();
scoreR.draw();
ball.draw();
}
void setup()
{
#if DEBUG
Serial.begin(57600);
#endif
PRINTS("\n[MD_MAXPanel_Pong]");
mp.begin();
mp.setFont(_Fixed_5x3);
mp.setIntensity(4);
mp.setRotation(MD_MAXPanel::ROT_90);
sound.begin(BEEPER_PIN);
FIELD_TOP = mp.getYMax() - mp.getFontHeight() - 2;
}
void loop(void)
{
static enum { S_SPLASH, S_INIT, S_GAME_START, S_POINT_PLAY, S_POINT_END, S_WAIT_LSTART, S_WAIT_RSTART, S_GAME_OVER } runState = S_SPLASH;
switch (runState)
{
case S_SPLASH: // show splash screen at start
{
const uint16_t border = 2;
mp.clear();
mp.drawRectangle(border, border, mp.getXMax() - border, mp.getYMax() - border);
mp.drawLine(0, 0, border, border);
mp.drawLine(0, mp.getYMax(), border, mp.getYMax()-border);
mp.drawLine(mp.getXMax(), 0, mp.getXMax()-border, border);
mp.drawLine(mp.getXMax(), mp.getYMax(), mp.getXMax()-border, mp.getYMax()-border);
mp.drawText((mp.getXMax() - mp.getTextWidth(TITLE_TEXT)) / 2, (mp.getYMax() + mp.getFontHeight())/2 , TITLE_TEXT);
sound.splash();
delay(SPLASH_DELAY);
runState = S_INIT;
}
break;
case S_INIT: // initialise for a new game
batL.begin(BAT_EDGE_OFFSET, (FIELD_TOP - FIELD_BOTTOM) / 2, FIELD_BOTTOM + 1, FIELD_TOP - 1, BAT_SIZE_DEFAULT, UP_PIN, LEFT_PIN);
batR.begin(mp.getXMax() - BAT_EDGE_OFFSET, (FIELD_TOP - FIELD_BOTTOM) / 2, FIELD_BOTTOM + 1, FIELD_TOP - 1, BAT_SIZE_DEFAULT, RIGHT_PIN, DOWN_PIN);
scoreL.begin(&mp, BAT_EDGE_OFFSET, FIELD_TOP + 1 + mp.getFontHeight(), MAX_SCORE);
scoreR.limit(MAX_SCORE); // set width() used below
scoreR.begin(&mp, mp.getXMax() - (scoreR.width() * (FONT_NUM_WIDTH + mp.getCharSpacing())) + mp.getCharSpacing(), FIELD_TOP + 1 + mp.getFontHeight(), MAX_SCORE);
ball.begin((mp.getXMax() / 2) - BAT_EDGE_OFFSET - 1, (FIELD_TOP - FIELD_BOTTOM) / 3);
setupField();
runState = S_GAME_START;
break;
case S_GAME_START: // waiting for the start of a new game
if (batL.anyKey() || batR.anyKey())
{
PRINTS("\n-- Starting Game");
scoreL.reset();
scoreR.reset();
sound.start();
ball.start();
runState = S_POINT_PLAY;
}
break;
case S_POINT_PLAY: // playing a point
// handle the bat animation first
batL.move();
batR.move();
// now move the ball and check what this means
if (ball.move())
{
cPongBat::hitType_t lastHit;
// redraw the centerline if the ball is near it
if (ball.getX() >= (mp.getXMax() / 2) - 1 || ball.getX() >= (mp.getXMax() / 2) + 2)
{
centerLine();
ball.draw();
}
// check for top/bottom edge collisions
if (ball.getY() == FIELD_TOP - 1)
{
PRINTS("\n-- COLLISION top edge");
ball.bounce(cPongBall::BOUNCE_TOP);
sound.bounce();
}
else if (ball.getY() == FIELD_BOTTOM + 1)
{
PRINTS("\n-- COLLISION bottom edge");
ball.bounce(cPongBall::BOUNCE_BOTTOM);
sound.bounce();
}
// check for bat collisions
if ((lastHit = batL.hit(ball.getX(), ball.getY(), ball.getNextX(), ball.getNextY())) != cPongBat::NO_HIT)
{
PRINTS("\n-- COLLISION left bat");
ball.bounce(lastHit == cPongBat::CORNER_HIT ? cPongBall::BOUNCE_BACK: cPongBall::BOUNCE_LEFT);
sound.hit();
}
else if ((lastHit = batR.hit(ball.getX(), ball.getY(), ball.getNextX(), ball.getNextY())) != cPongBat::NO_HIT)
{
PRINTS("\n-- COLLISION right bat");
ball.bounce(lastHit == cPongBat::CORNER_HIT ? cPongBall::BOUNCE_BACK : cPongBall::BOUNCE_RIGHT);
sound.hit();
}
// check for out of bounds
if ((ball.getX() < BAT_EDGE_OFFSET) || (ball.getX() > mp.getXMax() - BAT_EDGE_OFFSET))
{
PRINTS("\n-- OUT!");
runState = S_POINT_END;
}
}
break;
case S_POINT_END: // handle the ball going out
ball.stop();
batL.draw();
batR.draw();
sound.point();
delay(500);
ball.erase();
if (ball.getX() < BAT_EDGE_OFFSET) // out on the left side
{
PRINTS("\n--- LEFT side");
ball.reset(BAT_EDGE_OFFSET + 1, batL.getY());
ball.bounce(cPongBall::BOUNCE_LEFT);
scoreR.increment();
if (scoreR.score() == MAX_SCORE)
runState = S_GAME_OVER;
else
runState = S_WAIT_LSTART;
}
else // out on the right side
{
PRINTS("\n--- RIGHT side");
ball.reset(mp.getXMax() - BAT_EDGE_OFFSET - 1, batR.getY());
ball.bounce(cPongBall::BOUNCE_RIGHT);
scoreL.increment();
if (scoreL.score() == MAX_SCORE)
runState = S_GAME_OVER;
else
runState = S_WAIT_RSTART;
}
ball.draw();
break;
case S_WAIT_LSTART: // waiting for left playter to restart the game
if (batL.anyKey())
{
ball.start();
runState = S_POINT_PLAY;
}
break;
case S_WAIT_RSTART: // waiting fo the right player to restrt the game
if (batR.anyKey())
{
ball.start();
runState = S_POINT_PLAY;
}
break;
case S_GAME_OVER:
mp.drawText((mp.getXMax() - mp.getTextWidth(GAME_TEXT))/2, (FIELD_TOP - FIELD_BOTTOM)/2 + mp.getFontHeight() + 1, GAME_TEXT);
mp.drawText((mp.getXMax() - mp.getTextWidth(OVER_TEXT)) / 2, (FIELD_TOP - FIELD_BOTTOM)/2 - 1, OVER_TEXT);
sound.over();
delay(GAME_OVER_DELAY);
runState = S_INIT;
break;
}
}

View File

@@ -0,0 +1,54 @@
#pragma once
#include <MD_MAXPanel.h>
// A class to encapsulate the score display
class cScore
{
private:
MD_MAXPanel *_mp; // the MAXPanel object for drawing the score
uint16_t _score; // the score
uint16_t _x, _y; // coordinate of top left for display
uint8_t _width; // number of digits wide
uint16_t _limit; // maximum value allowed
public:
void begin(MD_MAXPanel *mp, uint16_t x, uint16_t y, uint16_t maxScore) { _mp = mp; _x = x, _y = y; limit(maxScore); reset(); }
void reset(void) { erase(); _score = 0; draw(); }
void set(uint16_t s) { if (s <= _limit) { erase(); _score = s; draw(); } }
void increment(uint16_t inc = 1) { if (_score + inc <= _limit) { erase(); _score += inc; draw(); } }
void decrement(uint16_t dec = 1) { if (_score >= dec) { erase(); _score -= dec; draw(); } }
uint16_t score(void) { return(_score); }
void erase(void) { draw(false); }
uint16_t width(void) { return(_width); }
void limit(uint16_t m)
{
erase(); // width may change, so delete with the curret parameters
_limit = m;
// work out how many digits this is
_width = 0;
do
{
_width++;
m /= 10;
} while (m != 0);
}
void draw(bool state = true)
{
char sz[_width + 1];
uint16_t s = _score;
// PRINT("\n-- SCORE: ", _score);
sz[_width] = '\0';
for (int i = _width - 1; i >= 0; --i)
{
sz[i] = (s % 10) + '0';
s /= 10;
}
_mp->drawText(_x, _y, sz, MD_MAXPanel::ROT_0, state);
}
};

View File

@@ -0,0 +1,47 @@
#pragma once
// A class to encapsulate primitive sound effects
class cSound
{
private:
const uint16_t EOD = 0; // End Of Data marker
uint8_t _pinBeep; // the pin to use for beeping
// Sound data - frequency followed by duration in pairs.
// Data ends in End Of Data marker EOD.
const uint16_t soundSplash[1] PROGMEM = { EOD };
const uint16_t soundHit[3] PROGMEM = { 1000, 50, EOD };
const uint16_t soundBounce[3] PROGMEM = { 500, 50, EOD };
const uint16_t soundPoint[3] PROGMEM = { 150, 150, EOD };
const uint16_t soundStart[7] PROGMEM = { 250, 100, 500, 100, 1000, 100, EOD };
const uint16_t soundOver[7] PROGMEM = { 1000, 100, 500, 100, 250, 100, EOD };
void playSound(const uint16_t *table)
// Play sound table data. Data table must end in EOD marker.
{
uint8_t idx = 0;
//PRINTS("\nTone Data ");
while (table[idx] != EOD)
{
uint16_t t = table[idx++];
uint16_t d = table[idx++];
//PRINTXY("-", t, d);
tone(_pinBeep, t);
delay(d);
}
//PRINTS("-EOD");
noTone(_pinBeep); // be quiet now!
}
public:
void begin(uint8_t pinBeep) { _pinBeep = pinBeep; }
void splash(void) { playSound(soundSplash); }
void start(void) { playSound(soundStart); }
void hit(void) { playSound(soundHit); }
void bounce(void) { playSound(soundBounce); }
void point(void) { playSound(soundPoint); }
void over(void) { playSound(soundOver); }
};

View File

@@ -0,0 +1,99 @@
MD_MAX72XX::fontType_t _Fixed_5x3[] PROGMEM = {
'F', 1, 32, 127, 5,
2, 0, 0, // 32 - 'Space'
1, 23, // 33 - '!'
3, 3, 0, 3, // 34 - '"'
3, 31, 10, 31, // 35 - '#'
3, 22, 31, 13, // 36 - '$'
3, 9, 4, 18, // 37 - '%'
3, 10, 21, 26, // 38 - '&'
1, 3, // 39
2, 14, 17, // 40 - '('
2, 17, 14, // 41 - ')'
3, 10, 4, 10, // 42 - '*'
3, 4, 14, 4, // 43 - '+'
2, 16, 8, // 44 - ','
3, 4, 4, 4, // 45 - '-'
1, 16, // 46 - '.'
3, 8, 4, 2, // 47 - '/'
3, 31, 17, 31, // 48 - '0'
2, 0, 31, // 49 - '1'
3, 29, 21, 23, // 50 - '2'
3, 17, 21, 31, // 51 - '3'
3, 7, 4, 31, // 52 - '4'
3, 23, 21, 29, // 53 - '5'
3, 31, 21, 29, // 54 - '6'
3, 1, 1, 31, // 55 - '7'
3, 31, 21, 31, // 56 - '8'
3, 23, 21, 31, // 57 - '9'
1, 10, // 58 - ':'
2, 16, 10, // 59 - ';'
3, 4, 10, 17, // 60 - '<'
3, 10, 10, 10, // 61 - '='
3, 17, 10, 4, // 62 - '>'
3, 1, 21, 3, // 63 - '?'
3, 14, 21, 22, // 64 - '@'
3, 30, 5, 30, // 65 - 'A'
3, 31, 21, 10, // 66 - 'B'
3, 14, 17, 17, // 67 - 'C'
3, 31, 17, 14, // 68 - 'D'
3, 31, 21, 17, // 69 - 'E'
3, 31, 5, 1, // 70 - 'F'
3, 14, 17, 29, // 71 - 'G'
3, 31, 4, 31, // 72 - 'H'
3, 17, 31, 17, // 73 - 'I'
3, 8, 16, 15, // 74 - 'J'
3, 31, 4, 27, // 75 - 'K'
3, 31, 16, 16, // 76 - 'L'
3, 31, 2, 31, // 77 - 'M'
3, 31, 14, 31, // 78 - 'N'
3, 14, 17, 14, // 79 - 'O'
3, 31, 5, 2, // 80 - 'P'
3, 14, 25, 30, // 81 - 'Q'
3, 31, 5, 26, // 82 - 'R'
3, 18, 21, 9, // 83 - 'S'
3, 1, 31, 1, // 84 - 'T'
3, 15, 16, 15, // 85 - 'U'
3, 7, 24, 7, // 86 - 'V'
3, 15, 28, 15, // 87 - 'W'
3, 27, 4, 27, // 88 - 'X'
3, 3, 28, 3, // 89 - 'Y'
3, 25, 21, 19, // 90 - 'Z'
2, 31, 17, // 91 - '['
3, 2, 4, 8, // 92 - '\'
2, 17, 31, // 93 - ']'
3, 2, 1, 2, // 94 - '^'
3, 16, 16, 16, // 95 - '_'
2, 1, 2, // 96 - '`'
3, 12, 18, 28, // 97 - 'a'
3, 31, 18, 12, // 98 - 'b'
3, 12, 18, 18, // 99 - 'c'
3, 12, 18, 31, // 100 - 'd'
3, 12, 26, 20, // 101 - 'e'
3, 4, 31, 5, // 102 - 'f'
3, 20, 26, 12, // 103 - 'g'
3, 31, 2, 28, // 104 - 'h'
1, 29, // 105 - 'i'
2, 16, 13, // 106 - 'j'
3, 31, 8, 20, // 107 - 'k'
1, 31, // 108 - 'l'
3, 30, 6, 30, // 109 - 'm'
3, 30, 2, 28, // 110 - 'n'
3, 12, 18, 12, // 111 - 'o'
3, 30, 10, 4, // 112 - 'p'
3, 4, 10, 30, // 113 - 'q'
2, 30, 4, // 114 - 'r'
3, 20, 30, 10, // 115 - 's'
3, 4, 30, 4, // 116 - 't'
3, 14, 16, 30, // 117 - 'u'
3, 14, 16, 14, // 118 - 'v'
3, 14, 24, 14, // 119 - 'w'
3, 18, 12, 18, // 120 - 'x'
3, 22, 24, 14, // 121 - 'y'
3, 26, 30, 22, // 122 - 'z'
3, 4, 27, 17, // 123 - '{'
1, 27, // 124 - '|'
3, 17, 27, 4, // 125 - '}'
3, 6, 2, 3, // 126 - '~'
3, 31, 31, 31, // 127 - 'Full Block'
};

View File

@@ -0,0 +1,507 @@
// Implements the game of Snake using MD_MAXPanel library
//
// Hardware used
// =============
// LEFT_PIN - left move switch, INPUT_PULLUP
// UP_PIN - up move switch, INPUT_PULLUP
// DOWN_PIN - down move switch, INPUT_PULLUP
// RIGHT_PIN - right move switch, INPUT_PULLUP
// SELECT_PIN - unused
// ENTER_PIN - unused
// BEEPER_PIN - piezo speaker
// CLK_PIN, DATA_PIN, CS_PIN - LED matrix display connections
//
// Libraries used
// ==============
// MD_MAX72XX available from https://github.com/MajicDesigns/MD_MAX72XX
//
// Rules of the Game
// =================
// Guide the snake around the screen using the direction keys. Running into
// a pill will increase the length of the snake. Running into the sides or
// the snake will end the game.
#include <MD_MAXPanel.h>
#include "Font5x3.h"
#include "score.h"
#include "sound.h"
#include "randomseed.h"
// Turn on debug statements to the serial output
#define DEBUG 1
#if DEBUG
#define PRINT(s, x) { Serial.print(F(s)); Serial.print(x); }
#define PRINTS(x) { Serial.print(F(x)); }
#define PRINTD(x) { Serial.print(x, DEC); }
#define PRINTXY(s,x,y) { Serial.print(F(s)); Serial.print(F("(")); Serial.print(x); Serial.print(F(",")); Serial.print(y); Serial.print(")"); }
#define PRINTSTATE(s) { Serial.print("\n++>"); Serial.print(s); }
#else
#define PRINT(s, x)
#define PRINTS(x)
#define PRINTD(x)
#define PRINTXY(s,x,y)
#define PRINTSTATE(s)
#endif
// Hardware pin definitions.
// All momentary on switches are initialized PULLUP
const uint8_t UP_PIN = 2;
const uint8_t RIGHT_PIN = 3;
const uint8_t DOWN_PIN = 4;
const uint8_t LEFT_PIN = 5;
const uint8_t SELECT_PIN = 6;
const uint8_t ENTER_PIN = 7;
const uint8_t BEEPER_PIN = 9;
// Define the number of devices in the chain and the SPI hardware interface
// NOTE: These pin numbers will probably not work with your hardware and may
// need to be adapted
const MD_MAX72XX::moduleType_t HARDWARE_TYPE = MD_MAX72XX::FC16_HW;
const uint8_t X_DEVICES = 4;
const uint8_t Y_DEVICES = 5;
const uint8_t CLK_PIN = 12; // or SCK
const uint8_t DATA_PIN = 11; // or MOSI
const uint8_t CS_PIN = 10; // or SS
// SPI hardware interface
MD_MAXPanel mp = MD_MAXPanel(HARDWARE_TYPE, CS_PIN, X_DEVICES, Y_DEVICES);
// Arbitrary pins
// MD_MAXPanel mx = MD_MAXPanel(HARDWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, X_DEVICES, Y_DEVICES);
uint16_t FIELD_TOP, FIELD_RIGHT; // needs to be initialised in setup()
const uint16_t FIELD_LEFT = 0;
const uint16_t FIELD_BOTTOM = 0;
const uint8_t SNAKE_SIZE_DEFAULT = 2;
const char TITLE_TEXT[] = "SNAKE";
const uint16_t SPLASH_DELAY =3000; // in milliseconds
const char GAME_TEXT[] = "GAME";
const char OVER_TEXT[] = "OVER";
const uint16_t GAME_OVER_DELAY = 3000; // in milliseconds
const uint8_t MAX_LENGTH = 999;
const uint8_t FONT_NUM_WIDTH = 3;
const uint16_t MAX_SCORE = MAX_LENGTH;
const uint16_t MAX_FOOD = 99;
// A class to encapsulate the snake direction switches
// Can move up, down, left, right
class cMoveSW
{
private:
uint8_t _pinLeft;
uint8_t _pinRight;
uint8_t _pinUp;
uint8_t _pinDown;
public:
enum moveType_t { MOVE_NONE, MOVE_UP, MOVE_DOWN, MOVE_LEFT, MOVE_RIGHT };
void begin(uint8_t pinL, uint8_t pinR, uint8_t pinU, uint8_t pinD)
{
_pinLeft = pinL;
_pinRight = pinR;
_pinUp = pinU;
_pinDown = pinD;
pinMode(_pinLeft, INPUT_PULLUP);
pinMode(_pinRight, INPUT_PULLUP);
pinMode(_pinUp, INPUT_PULLUP);
pinMode(_pinDown, INPUT_PULLUP);
}
bool anyKey(void) { return (move() != MOVE_NONE);}
moveType_t move(void)
{
if (digitalRead(_pinLeft) == LOW)
return(MOVE_LEFT);
else if (digitalRead(_pinRight) == LOW)
return(MOVE_RIGHT);
else if (digitalRead(_pinUp) == LOW)
return(MOVE_UP);
else if (digitalRead(_pinDown) == LOW)
return(MOVE_DOWN);
return(MOVE_NONE);
}
};
// A class to encapsulate the food pill
class cPill
{
private:
uint16_t _x, _y; // pill coordinates
uint16_t _xmin, _xmax; // boundaries for the creation of the pill
uint16_t _ymin, _ymax;
uint8_t _value; // value of the pill
public:
void begin(uint16_t xmin, uint16_t ymin, uint16_t xmax, uint16_t ymax)
{
_value = 0;
_xmin = xmin;
_xmax = xmax;
_ymin = ymin;
_ymax = ymax;
PRINTXY("\n-- PILL boundaries ", _xmin, _ymin);
PRINTXY(" to ", _xmax, _ymax);
}
uint16_t getX(void) { return(_x); }
uint16_t getY(void) { return(_y); }
uint8_t getValue(void) { return(_value); }
void reset(void)
{
do // search for an unused space
{
_x = _xmin + (random(_xmax - _xmin + 1));
_y = _ymin + (random(_ymax - _ymin + 1));
//PRINTXY("\n--- PILL TEST ", _x, _y);
} while (mp.getPoint(_x, _y));
_value = random(10);
PRINTXY("\n-- PILL @", _x, _y);
PRINT(" worth ", _value);
mp.setPoint(_x, _y, true);
}
};
// A class to encapsulate the snake
class cSnake
{
private:
struct snakeNode_t
{
uint8_t x, y; // coordinate for this body segment. This is UINT*_T to save
// RAM, could overflow with larger displays
snakeNode_t *next; // the next in the linked list
};
snakeNode_t *_head; // the head segment of the snake
snakeNode_t *_tail; // the tail segment of the snake
snakeNode_t *_deleted; // deleted blocks list
int8_t _dx, _dy; // the movement offsets for the x and y direction
uint32_t _timeLastMove; // last time the snake was moved
uint16_t _moveDelay; // the delay between moves in milliseconds
bool _run; // snake is moving when true
cScore *_pFoodCount; // the food counter
void draw(uint16_t x, uint16_t y) { mp.setPoint(x, y, true); }
void erase(uint16_t x, uint16_t y) { mp.setPoint(x, y, false); }
snakeNode_t *add(uint16_t x, uint16_t y)
// add a body segment to the tail
// the list is stored from tail to head
{
snakeNode_t *psn;
if (_deleted == nullptr)
psn = new snakeNode_t;
else
{
// take one from the head of the deleted list
psn = _deleted;
_deleted = _deleted->next;
}
if (psn != nullptr)
{
psn->x = x;
psn->y = y;
if (_tail == nullptr)
{
psn->next = nullptr;
_head = _tail = psn; // the first segment
PRINTS("\n-- SNAKE added first element");
}
else
{
psn->next = _tail;
_tail = psn;
PRINTS("\n-- SNAKE added element");
}
}
return(psn);
}
void deleteAll()
// delete all the body segments in the snake
{
snakeNode_t *psn;
uint16_t count = 0;
while (_tail != nullptr)
{
// adjust the linked list by consuming from the tail
psn = _tail;
_tail = _tail->next;
// put the block in the deleted list
psn->next = _deleted;
_deleted = psn;
count++;
}
_head = nullptr;
PRINT("\n-- SNAKE nodes deleted: ", count);
}
public:
enum moveType_t { MOVE_UP, MOVE_DOWN, MOVE_LEFT, MOVE_RIGHT };
uint16_t getHeadX(void) { return (_head->x); }
uint16_t getHeadY(void) { return (_head->y); }
uint16_t getDelay(void) { return (_moveDelay); }
void setDelay(uint16_t delay) { if (delay > 10) _moveDelay = delay; }
void start(void) { _run = true; }
void stop(void) { _run = false; }
void setDirection(moveType_t d)
{
switch (d)
{
case MOVE_UP: _dx = 0; _dy = 1; break;
case MOVE_DOWN: _dx = 0; _dy = -1; break;
case MOVE_LEFT: _dx = -1; _dy = 0; break;
case MOVE_RIGHT: _dx = 1; _dy = 0; break;
}
}
void reset(uint16_t x, uint16_t y)
{
deleteAll();
for (uint8_t i = 0; i < SNAKE_SIZE_DEFAULT; i++)
{
snakeNode_t *psn = add(x - i, y);
if (psn != nullptr) draw(psn->x, psn->y);
}
setDirection(MOVE_RIGHT);
_pFoodCount->reset();
}
void begin(cScore *pFoodCount)
{
_dx = 1;
_dy = 0;
_timeLastMove = 0;
_moveDelay = 100;
_run = false;
_pFoodCount = pFoodCount;
_pFoodCount->reset();
_deleted = nullptr;
}
bool move(void)
// return true if something was hit
{
// if this is the time for a move?
if (_run && millis() - _timeLastMove <= _moveDelay)
return(false);
_timeLastMove = millis();
// do the animation
snakeNode_t *psn = _tail;
bool hitSomething = false;
// check if we would be colliding with the snake body
while (psn != nullptr && !hitSomething)
{
hitSomething = (psn->x == _head->x + _dx && psn->y == _head->y + _dy);
psn = psn->next;
}
if (hitSomething) PRINTS("\n-! COLLIDE snake");
if (!hitSomething)
{
psn = _tail; // reset back to the start of the data
// do we need to add one?
if (_pFoodCount->score() != 0)
{
add(_tail->x, _tail->y); // make a copy of the tail
_pFoodCount->decrement();
}
else // delete the tail on the display
erase(_tail->x, _tail->y);
while (psn != nullptr)
{
if (psn->next != nullptr) // main body segment
{
psn->x = psn->next->x;
psn->y = psn->next->y;
}
else // the head
{
psn->x += _dx;
psn->y += _dy;
// check if this is already used
hitSomething = (mp.getPoint(psn->x, psn->y));
draw(psn->x, psn->y);
}
psn = psn->next;
}
if (hitSomething) PRINTS("\n-! COLLIDE not snake");
}
return(hitSomething);
}
};
// main objects coordinated by the code logic
cScore score, food;
cPill pill;
cSnake snake;
cMoveSW moveSW;
cSound sound;
void setupField(void)
// Draw the playing field at the start of the game.
{
mp.clear();
mp.drawHLine(FIELD_TOP, FIELD_LEFT, FIELD_RIGHT);
mp.drawHLine(FIELD_BOTTOM, FIELD_LEFT, FIELD_RIGHT);
mp.drawVLine(FIELD_LEFT, 0, FIELD_TOP);
mp.drawVLine(FIELD_RIGHT, 0, FIELD_TOP);
score.draw();
food.draw();
}
void setup()
{
#if DEBUG
Serial.begin(57600);
#endif
PRINTS("\n[MD_MAXPanel_Snake]");
mp.begin();
mp.setFont(_Fixed_5x3);
mp.setIntensity(4);
mp.setRotation(MD_MAXPanel::ROT_90);
randomSeed(seedOut(31, RANDOM_SEED_PORT));
// one time initialization
FIELD_TOP = mp.getYMax() - mp.getFontHeight() - 2;
FIELD_RIGHT = mp.getXMax();
pill.begin(FIELD_LEFT + 1, FIELD_BOTTOM + 1, FIELD_RIGHT - 1, FIELD_TOP - 1);
food.begin(&mp, FIELD_LEFT + 1, FIELD_TOP + 1 + mp.getFontHeight(), MAX_FOOD);
sound.begin(BEEPER_PIN);
score.limit(MAX_SCORE); // set the width so we can use it below
score.begin(&mp, FIELD_RIGHT - ((score.width() * (FONT_NUM_WIDTH) + mp.getCharSpacing())) - mp.getCharSpacing(), FIELD_TOP + 1 + mp.getFontHeight(), MAX_SCORE);
moveSW.begin(LEFT_PIN, RIGHT_PIN, UP_PIN, DOWN_PIN);
snake.begin(&food);
}
bool doSwitches(void)
{
bool b = true;
switch (moveSW.move())
{
case cMoveSW::MOVE_UP: snake.setDirection(cSnake::MOVE_UP); break;
case cMoveSW::MOVE_DOWN: snake.setDirection(cSnake::MOVE_DOWN); break;
case cMoveSW::MOVE_LEFT: snake.setDirection(cSnake::MOVE_LEFT); break;
case cMoveSW::MOVE_RIGHT: snake.setDirection(cSnake::MOVE_RIGHT); break;
case cMoveSW::MOVE_NONE: b = false; break;
}
return(b);
}
void loop(void)
{
static enum { S_SPLASH, S_INIT, S_WAIT_START, S_POINT_PLAY, S_GAME_OVER } runState = S_SPLASH;
switch (runState)
{
case S_SPLASH: // show splash screen at start
PRINTSTATE("SPLASH");
{
const uint16_t border = 2;
mp.clear();
mp.drawRectangle(border, border, mp.getXMax() - border, mp.getYMax() - border);
mp.drawLine(0, 0, border, border);
mp.drawLine(0, mp.getYMax(), border, mp.getYMax()-border);
mp.drawLine(mp.getXMax(), 0, mp.getXMax()-border, border);
mp.drawLine(mp.getXMax(), mp.getYMax(), mp.getXMax()-border, mp.getYMax()-border);
mp.drawText((mp.getXMax() - mp.getTextWidth(TITLE_TEXT)) / 2, (mp.getYMax() + mp.getFontHeight())/2 , TITLE_TEXT);
sound.splash();
delay(SPLASH_DELAY);
runState = S_INIT;
}
break;
case S_INIT: // initialize for a new game
PRINTSTATE("INIT");
score.reset();
food.reset();
setupField();
snake.reset((FIELD_RIGHT - FIELD_LEFT) / 2, (FIELD_TOP - FIELD_BOTTOM) / 2);
pill.reset();
runState = S_WAIT_START;
PRINTSTATE("WAIT_START");
break;
case S_WAIT_START: // waiting for the start of a new game
if (doSwitches())
{
PRINTS("\n-- Starting Game");
sound.start();
snake.start();
runState = S_POINT_PLAY;
PRINTSTATE("POINT_PLAY");
}
break;
case S_POINT_PLAY: // playing a point
// handle the switches
doSwitches();
// move snake and check what this means
if (snake.move())
{
if (snake.getHeadX() == pill.getX() && snake.getHeadY() == pill.getY()) // have we hit the pill?
{
sound.hit();
score.increment(pill.getValue());
food.increment(pill.getValue());
pill.reset();
}
else // we have hit the wall or ourselves
{
snake.stop();
runState = S_GAME_OVER;
}
}
break;
case S_GAME_OVER:
PRINTSTATE("GAME_OVER");
mp.drawText((mp.getXMax() - mp.getTextWidth(GAME_TEXT)) / 2, FIELD_TOP / 2 + mp.getFontHeight() + 1, GAME_TEXT);
mp.drawText((mp.getXMax() - mp.getTextWidth(OVER_TEXT)) / 2, FIELD_TOP / 2 - 1, OVER_TEXT);
sound.over();
delay(GAME_OVER_DELAY);
runState = S_INIT;
break;
}
}

View File

@@ -0,0 +1,45 @@
#pragma once
// Random seed creation --------------------------
// Adapted from http://www.utopiamechanicus.com/article/arduino-better-random-numbers/
const uint8_t RANDOM_SEED_PORT = A3; // port read for random seed
uint16_t bitOut(uint8_t port)
{
static bool firstTime = true;
uint32_t prev = 0;
uint32_t bit1 = 0, bit0 = 0;
uint32_t x = 0, limit = 99;
if (firstTime)
{
firstTime = false;
prev = analogRead(port);
}
while (limit--)
{
x = analogRead(port);
bit1 = (prev != x ? 1 : 0);
prev = x;
x = analogRead(port);
bit0 = (prev != x ? 1 : 0);
prev = x;
if (bit1 != bit0)
break;
}
return(bit1);
}
uint32_t seedOut(uint16_t noOfBits, uint8_t port)
{
// return value with 'noOfBits' random bits set
uint32_t seed = 0;
for (int i = 0; i<noOfBits; ++i)
seed = (seed << 1) | bitOut(port);
return(seed);
}
//------------------------------------------------------------------------------

View File

@@ -0,0 +1,54 @@
#pragma once
#include <MD_MAXPanel.h>
// A class to encapsulate the score display
class cScore
{
private:
MD_MAXPanel *_mp; // the MAXPanel object for drawing the score
uint16_t _score; // the score
uint16_t _x, _y; // coordinate of top left for display
uint8_t _width; // number of digits wide
uint16_t _limit; // maximum value allowed
public:
void begin(MD_MAXPanel *mp, uint16_t x, uint16_t y, uint16_t maxScore) { _mp = mp; _x = x, _y = y; limit(maxScore); reset(); }
void reset(void) { erase(); _score = 0; draw(); }
void set(uint16_t s) { if (s <= _limit) { erase(); _score = s; draw(); } }
void increment(uint16_t inc = 1) { if (_score + inc <= _limit) { erase(); _score += inc; draw(); } }
void decrement(uint16_t dec = 1) { if (_score >= dec) { erase(); _score -= dec; draw(); } }
uint16_t score(void) { return(_score); }
void erase(void) { draw(false); }
uint16_t width(void) { return(_width); }
void limit(uint16_t m)
{
erase(); // width may change, so delete with the curret parameters
_limit = m;
// work out how many digits this is
_width = 0;
do
{
_width++;
m /= 10;
} while (m != 0);
}
void draw(bool state = true)
{
char sz[_width + 1];
uint16_t s = _score;
// PRINT("\n-- SCORE: ", _score);
sz[_width] = '\0';
for (int i = _width - 1; i >= 0; --i)
{
sz[i] = (s % 10) + '0';
s /= 10;
}
_mp->drawText(_x, _y, sz, MD_MAXPanel::ROT_0, state);
}
};

View File

@@ -0,0 +1,47 @@
#pragma once
// A class to encapsulate primitive sound effects
class cSound
{
private:
const uint16_t EOD = 0; // End Of Data marker
uint8_t _pinBeep; // the pin to use for beeping
// Sound data - frequency followed by duration in pairs.
// Data ends in End Of Data marker EOD.
const uint16_t soundSplash[1] PROGMEM = { EOD };
const uint16_t soundHit[3] PROGMEM = { 1000, 50, EOD };
const uint16_t soundBounce[3] PROGMEM = { 500, 50, EOD };
const uint16_t soundPoint[3] PROGMEM = { 150, 150, EOD };
const uint16_t soundStart[7] PROGMEM = { 250, 100, 500, 100, 1000, 100, EOD };
const uint16_t soundOver[7] PROGMEM = { 1000, 100, 500, 100, 250, 100, EOD };
void playSound(const uint16_t *table)
// Play sound table data. Data table must end in EOD marker.
{
uint8_t idx = 0;
//PRINTS("\nTone Data ");
while (table[idx] != EOD)
{
uint16_t t = table[idx++];
uint16_t d = table[idx++];
//PRINTXY("-", t, d);
tone(_pinBeep, t);
delay(d);
}
//PRINTS("-EOD");
noTone(_pinBeep); // be quiet now!
}
public:
void begin(uint8_t pinBeep) { _pinBeep = pinBeep; }
void splash(void) { playSound(soundSplash); }
void start(void) { playSound(soundStart); }
void hit(void) { playSound(soundHit); }
void bounce(void) { playSound(soundBounce); }
void point(void) { playSound(soundPoint); }
void over(void) { playSound(soundOver); }
};

View File

@@ -0,0 +1,99 @@
MD_MAX72XX::fontType_t _Fixed_5x3[] PROGMEM = {
'F', 1, 32, 127, 5,
2, 0, 0, // 32 - 'Space'
1, 23, // 33 - '!'
3, 3, 0, 3, // 34 - '"'
3, 31, 10, 31, // 35 - '#'
3, 22, 31, 13, // 36 - '$'
3, 9, 4, 18, // 37 - '%'
3, 10, 21, 26, // 38 - '&'
1, 3, // 39
2, 14, 17, // 40 - '('
2, 17, 14, // 41 - ')'
3, 10, 4, 10, // 42 - '*'
3, 4, 14, 4, // 43 - '+'
2, 16, 8, // 44 - ','
3, 4, 4, 4, // 45 - '-'
1, 16, // 46 - '.'
3, 8, 4, 2, // 47 - '/'
3, 31, 17, 31, // 48 - '0'
2, 0, 31, // 49 - '1'
3, 29, 21, 23, // 50 - '2'
3, 17, 21, 31, // 51 - '3'
3, 7, 4, 31, // 52 - '4'
3, 23, 21, 29, // 53 - '5'
3, 31, 21, 29, // 54 - '6'
3, 1, 1, 31, // 55 - '7'
3, 31, 21, 31, // 56 - '8'
3, 23, 21, 31, // 57 - '9'
1, 10, // 58 - ':'
2, 16, 10, // 59 - ';'
3, 4, 10, 17, // 60 - '<'
3, 10, 10, 10, // 61 - '='
3, 17, 10, 4, // 62 - '>'
3, 1, 21, 3, // 63 - '?'
3, 14, 21, 22, // 64 - '@'
3, 30, 5, 30, // 65 - 'A'
3, 31, 21, 10, // 66 - 'B'
3, 14, 17, 17, // 67 - 'C'
3, 31, 17, 14, // 68 - 'D'
3, 31, 21, 17, // 69 - 'E'
3, 31, 5, 1, // 70 - 'F'
3, 14, 17, 29, // 71 - 'G'
3, 31, 4, 31, // 72 - 'H'
3, 17, 31, 17, // 73 - 'I'
3, 8, 16, 15, // 74 - 'J'
3, 31, 4, 27, // 75 - 'K'
3, 31, 16, 16, // 76 - 'L'
3, 31, 2, 31, // 77 - 'M'
3, 31, 14, 31, // 78 - 'N'
3, 14, 17, 14, // 79 - 'O'
3, 31, 5, 2, // 80 - 'P'
3, 14, 25, 30, // 81 - 'Q'
3, 31, 5, 26, // 82 - 'R'
3, 18, 21, 9, // 83 - 'S'
3, 1, 31, 1, // 84 - 'T'
3, 15, 16, 15, // 85 - 'U'
3, 7, 24, 7, // 86 - 'V'
3, 15, 28, 15, // 87 - 'W'
3, 27, 4, 27, // 88 - 'X'
3, 3, 28, 3, // 89 - 'Y'
3, 25, 21, 19, // 90 - 'Z'
2, 31, 17, // 91 - '['
3, 2, 4, 8, // 92 - '\'
2, 17, 31, // 93 - ']'
3, 2, 1, 2, // 94 - '^'
3, 16, 16, 16, // 95 - '_'
2, 1, 2, // 96 - '`'
3, 12, 18, 28, // 97 - 'a'
3, 31, 18, 12, // 98 - 'b'
3, 12, 18, 18, // 99 - 'c'
3, 12, 18, 31, // 100 - 'd'
3, 12, 26, 20, // 101 - 'e'
3, 4, 31, 5, // 102 - 'f'
3, 20, 26, 12, // 103 - 'g'
3, 31, 2, 28, // 104 - 'h'
1, 29, // 105 - 'i'
2, 16, 13, // 106 - 'j'
3, 31, 8, 20, // 107 - 'k'
1, 31, // 108 - 'l'
3, 30, 6, 30, // 109 - 'm'
3, 30, 2, 28, // 110 - 'n'
3, 12, 18, 12, // 111 - 'o'
3, 30, 10, 4, // 112 - 'p'
3, 4, 10, 30, // 113 - 'q'
2, 30, 4, // 114 - 'r'
3, 20, 30, 10, // 115 - 's'
3, 4, 30, 4, // 116 - 't'
3, 14, 16, 30, // 117 - 'u'
3, 14, 16, 14, // 118 - 'v'
3, 14, 24, 14, // 119 - 'w'
3, 18, 12, 18, // 120 - 'x'
3, 22, 24, 14, // 121 - 'y'
3, 26, 30, 22, // 122 - 'z'
3, 4, 27, 17, // 123 - '{'
1, 27, // 124 - '|'
3, 17, 27, 4, // 125 - '}'
3, 6, 2, 3, // 126 - '~'
3, 31, 31, 31, // 127 - 'Full Block'
};

View File

@@ -0,0 +1,427 @@
// Implements the game of TTT using MD_MAXPanel library
// Adapted from the LCD example in the MD_TTT library
//
// Hardware used
// =============
// LEFT_PIN - unused
// UP_PIN - unused
// DOWN_PIN - unused
// RIGHT_PIN - unused
// SELECT_PIN - switch moves from one selection to another,INPUT_PULLUP
// ENTER_PIN - accepts the current selection, INPUT_PULLUP
// BEEPER_PIN - piezo speaker
// CLK_PIN, DATA_PIN, CS_PIN - LED matrix display connections
//
// Libraries used
// ==============
// MD_MAX72XX available from https://github.com/MajicDesigns/MD_MAX72XX
// MD_TTT available from https://github.com/MajicDesigns/MD_TTT
//
// Rules of the Game
// =================
// Tic-tac-toe (also known as noughts and crosses or Xs and Os) is a
// paper-and-pencil game for two players, X and O, who take turns marking
// the spaces in a 3×3 grid. The player who succeeds in placing three
// of their marks in a horizontal, vertical, or diagonal row wins the
// game. This version has the computer as one of the players.
//
#include <MD_TTT.h>
#include <MD_MAXPanel.h>
#include "Font5x3.h"
#include "MD_MAXPanel_TTT_Types.h"
// Turn on debug statements to the serial output
#define DEBUG 0
#if DEBUG
#define PRINT(s, x) { Serial.print(F(s)); Serial.print(x); }
#define PRINTS(x) { Serial.print(F(x)); }
#define PRINTD(x) { Serial.print(x, DEC); }
#define PRINTXY(s, x, y) { Serial.print(s); Serial.print(F("(")); Serial.print(x); Serial.print(F(",")); Serial.print(y); Serial.print(")"); }
#define PRINTSTATE(s) { Serial.print("\n++>"); Serial.print(s); }
#else
#define PRINT(s, x)
#define PRINTS(x)
#define PRINTD(x)
#define PRINTXY(s, x, y)
#define PRINTSTATE(s)
#endif
// function prototype
void tttCallback(uint8_t position, int8_t player);
// User switches for gameplay
const uint8_t UP_PIN = 2;
const uint8_t RIGHT_PIN = 3;
const uint8_t DOWN_PIN = 4;
const uint8_t LEFT_PIN = 5;
const uint8_t SELECT_PIN = 6;
const uint8_t ENTER_PIN = 7;
const uint8_t BEEPER_PIN = 9;
// Define the number of devices in the chain and the SPI hardware interface
// NOTE: These pin numbers will probably not work with your hardware and may
// need to be adapted
const MD_MAX72XX::moduleType_t HARDWARE_TYPE = MD_MAX72XX::FC16_HW;
const uint8_t X_DEVICES = 4;
const uint8_t Y_DEVICES = 5;
const uint8_t CLK_PIN = 13; // or SCK
const uint8_t DATA_PIN = 11; // or MOSI
const uint8_t CS_PIN = 10; // or SS
// Miscellaneous defines
const uint16_t FLASH_DELAY = 500; // milliseconds
const uint8_t FLASH_REPEAT = 5; // number of flashes
const char TITLE_TEXT[] = "TTT";
const uint16_t SPLASH_DELAY = 3000; // in milliseconds
// Coordinates for a board position
boardCoord_t movePos[TTT_BOARD_SIZE] = { 0 };
// Handling for switch states (User Input)
swState_t swAccept = { ENTER_PIN, false, 0 };
swState_t swSelect = { SELECT_PIN, false, 0 };
// Main objects used defined here
MD_TTT TTT(tttCallback);
MD_MAXPanel mp = MD_MAXPanel(HARDWARE_TYPE, CS_PIN, X_DEVICES, Y_DEVICES); // SPI hardware interface
// MD_MAXPanel mx = MD_MAXPanel(HARDWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, X_DEVICES, Y_DEVICES); // Arbitrary pins
int8_t curPlayer = TTT_P2;
bool inGamePlay = false;
// variables and constants used for the display
uint16_t USER_MESG, GRID_BOTTOM, GRID_RIGHT, GRID_TOP;
uint8_t squareSize, offsetX, offsetY;
void calcGridSize(void)
{
uint16_t dx = (GRID_RIGHT - 2) / 3;
uint16_t dy = (GRID_TOP - GRID_BOTTOM - 2) / 3;
squareSize = min(dx, dy);
offsetX = ((GRID_RIGHT - 2) - (squareSize * 3)) / 2; // offset from vert edge
offsetY = ((GRID_TOP - GRID_BOTTOM - 2) - (squareSize * 3)) / 2; // offset from horiz edge
GRID_BOTTOM = GRID_TOP - offsetY - (3 * squareSize) - 2;
PRINT("\nsquareSize = ", squareSize);
PRINT(", offsetX = ", offsetX);
PRINT(", offsetY = ", offsetY);
// set up the board square coordinates
for (uint8_t j = 0; j < TTT_BOARD_SIZE / 3; j++)
for (uint8_t i = 0; i < TTT_BOARD_SIZE / 3; i++)
{
uint8_t idx = (j * 3) + i;
movePos[idx].x = offsetX + (i*squareSize) + i; // last element is for lines
movePos[idx].y = GRID_TOP - offsetY - (j*squareSize) - j; // last element is for lines
movePos[idx].size = squareSize-1;
PRINT("\nmovePos[", idx);
PRINTXY("] - ", movePos[idx].x, movePos[idx].y);
PRINT(" size=", movePos[idx].size);
}
}
void displayGrid(void)
{
mp.update(false);
mp.clear();
mp.drawVLine(offsetX + squareSize, GRID_BOTTOM + offsetY, GRID_TOP - offsetY);
mp.drawVLine(offsetX + (2*squareSize) + 1, GRID_BOTTOM + offsetY, GRID_TOP - offsetY);
mp.drawHLine(GRID_TOP - offsetY - squareSize, offsetX, GRID_RIGHT - offsetX);
mp.drawHLine(GRID_TOP - offsetY - (2*squareSize) - 1, offsetX, GRID_RIGHT - offsetX);
mp.update(true);
}
void highlightCell(uint8_t cell, bool b = true)
{
mp.drawRectangle(movePos[cell].x, movePos[cell].y,
movePos[cell].x + movePos[cell].size, movePos[cell].y - movePos[cell].size, b);
}
bool detectSwitch(swState_t *ss)
// detects the HIGH to LOW transition of a switch
// returns true if a transition has occurred
// only check if a period of time has expired to debounce
{
boolean b = false;
if ((millis() - ss->lastCheckTime) > 50)
{
bool curState = (digitalRead(ss->pin) == LOW);
ss->lastCheckTime = millis();
b = (curState && !ss->lastState);
ss->lastState = curState;
}
return(b);
}
void userMessage(char *psz)
{
mp.clear(1, 0, mp.getXMax(), USER_MESG);
mp.drawText(1, USER_MESG, psz, MD_MAXPanel::ROT_0, true);
}
uint8_t getMove(void)
// get the next move from the player
// there may not be a move there so we need to split the
// function into a prompting and then checking phase
// return 0xff of no move entered
{
static enum { UI_START, UI_HILIGHT, UI_SELECT, UI_NEXT_HILIGHT, UI_ACCEPT } promptMode = UI_START;
static uint8_t curPosUI = 0;
uint8_t m = 0xff;
switch (promptMode)
{
case UI_START: // print the message
PRINTS("\n- START");
userMessage("You move");
promptMode = UI_HILIGHT;
break;
case UI_HILIGHT: // find the first empty cell and highlight it
PRINTS("\n- HILIGHT");
//highlightCell(curPosUI, false); // unhighlight current selection
for (curPosUI = 0; curPosUI<TTT_BOARD_SIZE; curPosUI++)
{
PRINT(" ", curPosUI);
if (TTT.getBoardPosition(curPosUI) == TTT_P0)
{
PRINTS(": FOUND");
highlightCell(curPosUI); // highlight new selection
break;
}
}
promptMode = UI_SELECT;
break;
case UI_SELECT: // highlight the cell we are on and handle switches
if (detectSwitch(&swSelect))
promptMode = UI_NEXT_HILIGHT;
else if (detectSwitch(&swAccept))
promptMode = UI_ACCEPT;
break;
case UI_NEXT_HILIGHT: // user selected next cell, find it and highlight it
PRINTS("\n- NEXT HILIGHT");
if (curPosUI < TTT_BOARD_SIZE)
highlightCell(curPosUI, false); // unhighlight current selection
for (curPosUI=curPosUI+1; curPosUI<TTT_BOARD_SIZE; curPosUI++)
{
PRINT(" ", curPosUI);
if (TTT.getBoardPosition(curPosUI) == TTT_P0)
{
PRINTS(": FOUND");
highlightCell(curPosUI); // highlight new selection
break;
}
}
if (curPosUI == TTT_BOARD_SIZE) // oops - none there
{
PRINTS(": NOT FOUND");
promptMode = UI_HILIGHT; // start again
}
else
promptMode = UI_SELECT; // wait for a switch again
break;
case UI_ACCEPT: // we have a selection, return the appropriate move
PRINTS("\n- ACCEPT");
highlightCell(curPosUI, false); // unhighlight current selection
m = curPosUI;
promptMode = UI_START; // set up for next time
break;
default: // catch all - reset
promptMode = UI_START;
break;
}
return(m);
}
void tttCallback(uint8_t position, int8_t player)
// update the board position with the player token
{
displayPosition(position, player);
}
void displayPosition(uint8_t pos, int8_t player)
{
// update the position on the grid
switch (player)
{
case TTT_P0: // clear the space
mp.clear(movePos[pos].x, movePos[pos].y,
movePos[pos].x+movePos[pos].size, movePos[pos].y-movePos[pos].size);
break;
case TTT_P1: // draw an X
mp.drawLine(movePos[pos].x+1, movePos[pos].y-1,
movePos[pos].x-1+movePos[pos].size, movePos[pos].y+1-movePos[pos].size);
mp.drawLine(movePos[pos].x+1, movePos[pos].y+1-movePos[pos].size,
movePos[pos].x-1+movePos[pos].size, movePos[pos].y-1);
break;
case TTT_P2: // draw an O
mp.drawCircle(movePos[pos].x+(movePos[pos].size / 2), movePos[pos].y-(movePos[pos].size / 2),
(movePos[pos].size / 2) - 1);
break;
}
}
void flashLine(uint8_t line)
// note this is blocking as it uses delay();
{
uint8_t l[3];
// work out the cells for this line
switch (line)
{
case TTT_WL_D1: l[0]=0; l[1]=4; l[2]=8; break;
case TTT_WL_D2: l[0]=2; l[1]=4; l[2]=6; break;
case TTT_WL_H1: l[0]=0; l[1]=1; l[2]=2; break;
case TTT_WL_H2: l[0]=3; l[1]=4; l[2]=5; break;
case TTT_WL_H3: l[0]=6; l[1]=7; l[2]=8; break;
case TTT_WL_V1: l[0]=0; l[1]=3; l[2]=6; break;
case TTT_WL_V2: l[0]=1; l[1]=4; l[2]=7; break;
case TTT_WL_V3: l[0]=2; l[1]=5; l[2]=8; break;
}
// turn them off and on a number of times (flash!)
for (uint8_t i=0; i<FLASH_REPEAT; i++)
{
mp.update(false);
for (uint8_t j=0; j<ARRAY_SIZE(l); j++)
displayPosition(l[j], TTT_P0);
mp.update(true);
delay(FLASH_DELAY);
mp.update(false);
for (uint8_t j=0; j<ARRAY_SIZE(l); j++)
displayPosition(l[j], TTT.getBoardPosition(l[j]));
mp.update(true);
delay(FLASH_DELAY);
}
}
void setup()
{
#if DEBUG
Serial.begin(57600);
#endif
PRINTS("\n[MD_MAXPanel_TTT]");
mp.begin();
mp.setFont(_Fixed_5x3);
mp.setIntensity(4);
//mp.setRotation(MD_MAXPanel::ROT_90);
// initialize switch pins for input
pinMode(swAccept.pin, INPUT_PULLUP);
pinMode(swSelect.pin, INPUT_PULLUP);
// set up global constants
USER_MESG = mp.getFontHeight() + 1;
GRID_BOTTOM = USER_MESG + 1;
GRID_RIGHT = mp.getXMax();
GRID_TOP = mp.getYMax();
// initialize display
calcGridSize();
TTT.setAutoPlayer(curPlayer);
}
void loop(void)
{
static enum { S_SPLASH, S_START, S_GET_MOVE, S_CHECK_END } curState = S_SPLASH; // current state
switch (curState)
{
case S_SPLASH: // show splash screen at start
{
const uint16_t border = 2;
PRINTSTATE("S_SPLASH");
mp.clear();
mp.drawRectangle(border, border, mp.getXMax() - border, mp.getYMax() - border);
mp.drawLine(0, 0, border, border);
mp.drawLine(0, mp.getYMax(), border, mp.getYMax() - border);
mp.drawLine(mp.getXMax(), 0, mp.getXMax() - border, border);
mp.drawLine(mp.getXMax(), mp.getYMax(), mp.getXMax() - border, mp.getYMax() - border);
mp.drawText((mp.getXMax() - mp.getTextWidth(TITLE_TEXT)) / 2, (mp.getYMax() + mp.getFontHeight()) / 2, TITLE_TEXT);
delay(SPLASH_DELAY);
curState = S_START;
}
break;
case S_START: // initialize for a new game
PRINTSTATE("S_START");
displayGrid();
inGamePlay = TTT.start();
curState = S_GET_MOVE;
break;
case S_GET_MOVE: // get and make player move - this section is non-blocking
{
uint8_t m = 0;
if (TTT.getAutoPlayer() != curPlayer)
m = getMove();
if (m != 0xff)
{
TTT.doMove(m, curPlayer);
curState = S_CHECK_END;
}
}
break;
case S_CHECK_END: // switch players and check if this is the end of the game
PRINTSTATE("S_CHECK_END");
if (TTT.isGameOver())
{
inGamePlay = false;
if (TTT.getGameWinner() == TTT_P0)
{
userMessage("A draw");
delay(FLASH_REPEAT * 2 * FLASH_DELAY); // yes this blocks - so does flashLine()
}
else if (TTT.getGameWinner() == TTT.getAutoPlayer())
userMessage("I win!");
else
userMessage("You win!");
if (TTT.getGameWinner() != TTT_P0) // not a draw
flashLine(TTT.getWinLine());
curState = S_START; // restart
}
else
curState = S_GET_MOVE; // get or make next move
// switch turns for players
curPlayer = (curPlayer == TTT_P1 ? TTT_P2 : TTT_P1);
break;
default:
PRINTSTATE("DEFAULT!");
curState = S_START;
break;
}
}

View File

@@ -0,0 +1,15 @@
// Coordinates for the top left corner of the board cells on the display and size (square)
typedef struct
{
uint16_t x, y;
uint8_t size;
} boardCoord_t;
// Switch status
typedef struct
{
uint8_t pin;
bool lastState;
uint32_t lastCheckTime;
} swState_t;

View File

@@ -0,0 +1,99 @@
MD_MAX72XX::fontType_t _Fixed_5x3[] PROGMEM = {
'F', 1, 32, 127, 5,
2, 0, 0, // 32 - 'Space'
1, 23, // 33 - '!'
3, 3, 0, 3, // 34 - '"'
3, 31, 10, 31, // 35 - '#'
3, 22, 31, 13, // 36 - '$'
3, 9, 4, 18, // 37 - '%'
3, 10, 21, 26, // 38 - '&'
1, 3, // 39
2, 14, 17, // 40 - '('
2, 17, 14, // 41 - ')'
3, 10, 4, 10, // 42 - '*'
3, 4, 14, 4, // 43 - '+'
2, 16, 8, // 44 - ','
3, 4, 4, 4, // 45 - '-'
1, 16, // 46 - '.'
3, 8, 4, 2, // 47 - '/'
3, 31, 17, 31, // 48 - '0'
2, 0, 31, // 49 - '1'
3, 29, 21, 23, // 50 - '2'
3, 17, 21, 31, // 51 - '3'
3, 7, 4, 31, // 52 - '4'
3, 23, 21, 29, // 53 - '5'
3, 31, 21, 29, // 54 - '6'
3, 1, 1, 31, // 55 - '7'
3, 31, 21, 31, // 56 - '8'
3, 23, 21, 31, // 57 - '9'
1, 10, // 58 - ':'
2, 16, 10, // 59 - ';'
3, 4, 10, 17, // 60 - '<'
3, 10, 10, 10, // 61 - '='
3, 17, 10, 4, // 62 - '>'
3, 1, 21, 3, // 63 - '?'
3, 14, 21, 22, // 64 - '@'
3, 30, 5, 30, // 65 - 'A'
3, 31, 21, 10, // 66 - 'B'
3, 14, 17, 17, // 67 - 'C'
3, 31, 17, 14, // 68 - 'D'
3, 31, 21, 17, // 69 - 'E'
3, 31, 5, 1, // 70 - 'F'
3, 14, 17, 29, // 71 - 'G'
3, 31, 4, 31, // 72 - 'H'
3, 17, 31, 17, // 73 - 'I'
3, 8, 16, 15, // 74 - 'J'
3, 31, 4, 27, // 75 - 'K'
3, 31, 16, 16, // 76 - 'L'
3, 31, 2, 31, // 77 - 'M'
3, 31, 14, 31, // 78 - 'N'
3, 14, 17, 14, // 79 - 'O'
3, 31, 5, 2, // 80 - 'P'
3, 14, 25, 30, // 81 - 'Q'
3, 31, 5, 26, // 82 - 'R'
3, 18, 21, 9, // 83 - 'S'
3, 1, 31, 1, // 84 - 'T'
3, 15, 16, 15, // 85 - 'U'
3, 7, 24, 7, // 86 - 'V'
3, 15, 28, 15, // 87 - 'W'
3, 27, 4, 27, // 88 - 'X'
3, 3, 28, 3, // 89 - 'Y'
3, 25, 21, 19, // 90 - 'Z'
2, 31, 17, // 91 - '['
3, 2, 4, 8, // 92 - '\'
2, 17, 31, // 93 - ']'
3, 2, 1, 2, // 94 - '^'
3, 16, 16, 16, // 95 - '_'
2, 1, 2, // 96 - '`'
3, 12, 18, 28, // 97 - 'a'
3, 31, 18, 12, // 98 - 'b'
3, 12, 18, 18, // 99 - 'c'
3, 12, 18, 31, // 100 - 'd'
3, 12, 26, 20, // 101 - 'e'
3, 4, 31, 5, // 102 - 'f'
3, 20, 26, 12, // 103 - 'g'
3, 31, 2, 28, // 104 - 'h'
1, 29, // 105 - 'i'
2, 16, 13, // 106 - 'j'
3, 31, 8, 20, // 107 - 'k'
1, 31, // 108 - 'l'
3, 30, 6, 30, // 109 - 'm'
3, 30, 2, 28, // 110 - 'n'
3, 12, 18, 12, // 111 - 'o'
3, 30, 10, 4, // 112 - 'p'
3, 4, 10, 30, // 113 - 'q'
2, 30, 4, // 114 - 'r'
3, 20, 30, 10, // 115 - 's'
3, 4, 30, 4, // 116 - 't'
3, 14, 16, 30, // 117 - 'u'
3, 14, 16, 14, // 118 - 'v'
3, 14, 24, 14, // 119 - 'w'
3, 18, 12, 18, // 120 - 'x'
3, 22, 24, 14, // 121 - 'y'
3, 26, 30, 22, // 122 - 'z'
3, 4, 27, 17, // 123 - '{'
1, 27, // 124 - '|'
3, 17, 27, 4, // 125 - '}'
3, 6, 2, 3, // 126 - '~'
3, 31, 31, 31, // 127 - 'Full Block'
};

View File

@@ -0,0 +1,442 @@
// Program to exercise the MD_MAXPanel library
//
// Uses most of the functions in the library
//
// Libraries used
// ==============
// MD_MAX72XX available from https://github.com/MajicDesigns/MD_MAX72XX
//
#include <MD_MAXPanel.h>
#include "Font5x3.h"
// Turn on debug statements to the serial output
#define DEBUG 1
#if DEBUG
#define PRINT(s, x) { Serial.print(F(s)); Serial.print(x); }
#define PRINTS(x) Serial.print(F(x))
#define PRINTD(x) Serial.print(x, DEC)
#else
#define PRINT(s, x)
#define PRINTS(x)
#define PRINTD(x)
#endif
// Define the number of devices we have in the chain and the hardware interface
// NOTE: These pin numbers will probably not work with your hardware and may
// need to be adapted
const MD_MAX72XX::moduleType_t HARDWARE_TYPE = MD_MAX72XX::FC16_HW;
const uint8_t X_DEVICES = 4;
const uint8_t Y_DEVICES = 5;
const uint8_t CLK_PIN = 13; // or SCK
const uint8_t DATA_PIN = 11; // or MOSI
const uint8_t CS_PIN = 10; // or SS
// SPI hardware interface
MD_MAXPanel mp = MD_MAXPanel(HARDWARE_TYPE, CS_PIN, X_DEVICES, Y_DEVICES);
// Arbitrary pins
// MD_MAXPanel mx = MD_MAXPanel(HARDWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, X_DEVICES, Y_DEVICES);
// We always wait a bit between updates of the display
#define DELAYTIME 100 // in milliseconds
void zeroPointer(void)
// Demonstrates the use of setPoint and
// show where the zero point is in the display
{
PRINTS("\nZero point highlight");
mp.clear();
for (uint8_t i=0; i<=max(mp.getXMax(), mp.getYMax()); i++)
{
mp.setPoint(i, i, true);
mp.setPoint(0, i, true);
mp.setPoint(i, 0, true);
delay(DELAYTIME/mp.getXMax());
}
delay(DELAYTIME*6);
}
void showUp(void)
// Triangle pointing to the of the display as currently rotated
{
PRINTS("\nTop of display");
char sz[] = "UP";
uint8_t w, h;
mp.setFont(_Fixed_5x3);
w = mp.getTextWidth(sz);
h = 5;
mp.clear();
mp.drawTriangle(0, mp.getYMax() / 2, (mp.getXMax() + 1) / 2, mp.getYMax(), mp.getXMax(), mp.getYMax() / 2, true);
mp.drawText((mp.getXMax() - w) / 2, (mp.getYMax() / 2) + 2 + h, sz);
mp.setFont(nullptr);
delay(DELAYTIME * 10);
}
void brightness(void)
// Demonstrate the use of setBrightness()
// Striped display so as not to overload power supply
{
const uint8_t SKIP_COUNT = 3;
uint8_t c = 0;
PRINTS("\nBrigtness");
mp.clear();
// set up diagonal dots on the display to avoid using too
// much power in big displays
for (uint16_t i = 0; i <= mp.getXMax(); i++)
{
uint8_t ctr = c;
c++;
if (c > SKIP_COUNT) c = 0;
for (uint16_t j = 0; j <= mp.getYMax(); j++)
{
mp.setPoint(i, j, (ctr == 0));
ctr++;
if (ctr > SKIP_COUNT) ctr = 0;
}
}
// now show the brightness
for (uint8_t i = 0; i < MAX_INTENSITY; i++)
{
mp.setIntensity(i);
PRINT(" ", i);
delay(DELAYTIME * 5);
}
// reset to a sensible value
mp.setIntensity(7);
}
void lines(void)
// Demonstrate the use of drawLine().
// Stepped fan out lines from each corner
{
PRINTS("\nLines");
const uint8_t stepSize = 4;
mp.clear();
for (uint16_t x = 0; x<=mp.getXMax(); x += stepSize)
{
mp.drawLine(0, 0, x, mp.getYMax(), true);
delay(DELAYTIME);
mp.clear();
}
for (uint16_t x = 0; x<=mp.getXMax(); x += stepSize)
{
mp.drawLine(0, mp.getYMax(), x, 0, true);
delay(DELAYTIME);
mp.clear();
}
for (uint16_t x = 0; x<=mp.getXMax(); x += stepSize)
{
mp.drawLine(mp.getXMax(), mp.getYMax(), mp.getXMax() - x, 0, true);
delay(DELAYTIME);
mp.clear();
}
for (uint16_t x = 0; x<=mp.getXMax(); x += stepSize)
{
mp.drawLine(mp.getXMax(), 0, mp.getXMax() - x, mp.getYMax(), true);
delay(DELAYTIME);
mp.clear();
}
}
void hLines(void)
// Demonstrate the use of drawHLine()
// Draw a sawtooth across the display (depends on aspect ratio)
{
PRINTS("\nHorizontal Lines");
mp.clear();
for (uint8_t y = 0; y < mp.getYMax(); y++)
{
mp.drawHLine(y, 0, y % mp.getXMax(), true);
delay(DELAYTIME/4);
}
}
void vLines(void)
// Demonstrate the use of drawVLine()
// Stepped increment lines from zero point
{
PRINTS("\nVertical Lines");
mp.clear();
for (uint16_t x = 0; x < mp.getXMax(); x++)
{
mp.drawVLine(x, 0, x % mp.getYMax(), true);
delay(DELAYTIME/4);
}
}
void rectanglesFill(void)
// Demonstrate the use of drawFillRectangle()
// Various sized rectangles spanning the entire display
{
PRINTS("\nRectangles Fill");
mp.clear();
for (uint8_t stepSize = 2; stepSize < 6; stepSize++)
{
for (uint16_t i = 0; i < min(mp.getXMax(), mp.getYMax()) / 2; i += stepSize)
{
mp.drawFillRectangle(i, i, mp.getXMax() - i, mp.getYMax() - i, true);
delay(2 * DELAYTIME);
mp.drawFillRectangle(i, i, mp.getXMax() - i, mp.getYMax() - i, false);
delay(2 * DELAYTIME);
}
}
}
void rectangles(void)
// Demonstrate the use of drawRectangle()
// Nested rectangles spanning the entire display
{
PRINTS("\nRectangles");
mp.clear();
for (uint8_t stepSize = 2; stepSize < 6; stepSize++)
{
for (uint16_t i = 0; i < min(mp.getXMax(), mp.getYMax()) / 2; i += stepSize)
{
mp.drawRectangle(i, i, mp.getXMax() - i, mp.getYMax() - i, true);
delay(2 * DELAYTIME);
}
for (uint16_t i = 0; i < min(mp.getXMax(), mp.getYMax()) / 2; i += stepSize)
{
mp.drawRectangle(i, i, mp.getXMax() - i, mp.getYMax() - i, false);
delay(2 * DELAYTIME);
}
}
}
void quadrilaterals(void)
// Demonstrate the use of drawQuadrilateral()
// Rubber band quadrilaterals anchored to the display edge
{
const uint8_t numQuadri = min(mp.getXMax(), mp.getYMax())/2;
const uint8_t stepSizeX = mp.getXMax() / numQuadri;
const uint8_t stepSizeY = mp.getYMax() / numQuadri;
const uint8_t offset = 2;
PRINTS("\nQuadrilaterals");
mp.clear();
for (uint8_t j = 0; j < 4; j++)
{
for (uint16_t i = 0; i < numQuadri; i++)
{
uint16_t x = stepSizeX * i;
uint16_t y = stepSizeY * i;
mp.drawQuadrilateral(x+offset, 0+offset, 0+offset, mp.getYMax() - y - offset, mp.getXMax() - x - offset, mp.getYMax() - offset, mp.getXMax() - offset, y + offset, true);
delay(DELAYTIME/2);
mp.drawQuadrilateral(x + offset, 0 + offset, 0 + offset, mp.getYMax() - y - offset, mp.getXMax() - x - offset, mp.getYMax() - offset, mp.getXMax() - offset, y + offset, false);
}
}
}
void trianglesFill(void)
// Demonstrate the use of drawFillTriangle()
// Random triangles.
{
const uint8_t NUM_TRIANGLE = 25;
const uint8_t NUM_VERTEX = 3;
uint16_t x[NUM_VERTEX], y[NUM_VERTEX];
PRINTS("\nTriangles Fill");
mp.clear();
for (uint8_t j = 0; j < NUM_TRIANGLE; j++)
{
for (uint16_t i = 0; i < NUM_VERTEX; i++)
{
x[i] = random(mp.getXMax() + 1);
y[i] = random(mp.getYMax() + 1);
}
mp.drawFillTriangle(x[0], y[0], x[1], y[1], x[2], y[2], true);
delay(2 * DELAYTIME);
mp.drawFillTriangle(x[0], y[0], x[1], y[1], x[2], y[2], false);
delay(2 * DELAYTIME);
}
}
void triangles(void)
// Demonstrate the use of drawTriangle()
// Random triangles.
{
const uint8_t NUM_TRIANGLE = 25;
const uint8_t NUM_VERTEX = 3;
uint16_t x[NUM_VERTEX], y[NUM_VERTEX];
PRINTS("\nTriangles");
mp.clear();
for (uint8_t j = 0; j < NUM_TRIANGLE; j++)
{
for (uint16_t i = 0; i < NUM_VERTEX; i++)
{
x[i] = random(mp.getXMax() + 1);
y[i] = random(mp.getYMax() + 1);
}
mp.drawTriangle(x[0], y[0], x[1], y[1], x[2], y[2], true);
delay(2 * DELAYTIME);
mp.drawTriangle(x[0], y[0], x[1], y[1], x[2], y[2], false);
delay(2 * DELAYTIME);
}
}
void circlesFill(void)
// Demonstrate the use of drawFillCircle()
// Nested circles spanning the entire display
{
PRINTS("\nCircles Fill");
mp.clear();
for (uint8_t stepSize = 2; stepSize < 6; stepSize++)
{
for (uint16_t i = stepSize; i < min(mp.getXMax(), mp.getYMax()) / 2; i += stepSize)
{
mp.drawFillCircle(mp.getXMax()/2, mp.getYMax()/2, i, true);
delay(2 * DELAYTIME);
mp.drawFillCircle(mp.getXMax() / 2, mp.getYMax() / 2, i, false);
delay(2 * DELAYTIME);
}
}
}
void circles(void)
// Demonstrate the use of drawCircle()
// Nested circles spanning the entire display
{
PRINTS("\nCircles");
mp.clear();
for (uint8_t stepSize = 2; stepSize < 6; stepSize++)
{
for (uint16_t i = stepSize; i < min(mp.getXMax(), mp.getYMax()) / 2; i += stepSize)
{
mp.drawCircle(mp.getXMax() / 2, mp.getYMax() / 2, i, true);
delay(2 * DELAYTIME);
}
for (uint16_t i = stepSize; i < min(mp.getXMax(), mp.getYMax()) / 2; i += stepSize)
{
mp.drawCircle(mp.getXMax() / 2, mp.getYMax() / 2, i, false);
delay(2 * DELAYTIME);
}
}
}
void bounce(void)
// Animation of a bouncing ball
{
const uint16_t minX = 0;
const uint16_t maxX = mp.getXMax();
const uint16_t minY = 0;
const uint16_t maxY = mp.getYMax();
uint16_t nCounter = 0;
uint16_t y = random(maxY/2) + maxY/2, x = random(maxX/2) + maxX/2;
int8_t dy = 1, dx = 1; // delta row and column
PRINTS("\nBouncing ball");
mp.clear();
while (nCounter++ < 200)
{
mp.setPoint(x, y, false);
x += dx;
y += dy;
mp.setPoint(x, y, true);
delay(DELAYTIME/2);
if ((x == minX) || (x == maxX))
dx = -dx;
if ((y == minY) || (y == maxY))
dy = -dy;
}
}
void text(MD_MAX72XX::fontType_t *fontData)
// Demonstrate the use of drawText()
// Display text in different orientations and fonts
{
PRINTS("\nText");
mp.setFont(fontData);
PRINT(" - Font height: ", mp.getFontHeight());
mp.clear();
mp.drawText(0, mp.getYMax(), "R_0", MD_MAXPanel::ROT_0);
delay(5 * DELAYTIME);
mp.clear();
mp.drawText(0, 0, "R_90", MD_MAXPanel::ROT_90);
delay(5 * DELAYTIME);
mp.clear();
mp.drawText(mp.getXMax(), 0, "R_180", MD_MAXPanel::ROT_180);
delay(5 * DELAYTIME);
mp.clear();
mp.drawText(mp.getXMax(), mp.getYMax(), "R_270", MD_MAXPanel::ROT_270);
delay(5 * DELAYTIME);
}
void setup(void)
{
mp.begin();
#if DEBUG
Serial.begin(57600);
#endif
PRINTS("\n[MD_MAXPanel Test & Demo]");
}
void loop(void)
{
zeroPointer();
showUp();
brightness();
lines();
hLines();
vLines();
rectangles();
rectanglesFill();
circles();
circlesFill();
triangles();
trianglesFill();
quadrilaterals();
bounce();
text(_Fixed_5x3);
text(nullptr);
// rotate the display and do it all again
mp.setRotation(mp.getRotation() == MD_MAXPanel::ROT_0 ? MD_MAXPanel::ROT_90 : MD_MAXPanel::ROT_0);
}

View File

@@ -0,0 +1,99 @@
MD_MAX72XX::fontType_t _Fixed_5x3[] PROGMEM = {
'F', 1, 32, 127, 5,
2, 0, 0, // 32 - 'Space'
1, 23, // 33 - '!'
3, 3, 0, 3, // 34 - '"'
3, 31, 10, 31, // 35 - '#'
3, 22, 31, 13, // 36 - '$'
3, 9, 4, 18, // 37 - '%'
3, 10, 21, 26, // 38 - '&'
1, 3, // 39
2, 14, 17, // 40 - '('
2, 17, 14, // 41 - ')'
3, 10, 4, 10, // 42 - '*'
3, 4, 14, 4, // 43 - '+'
2, 16, 8, // 44 - ','
3, 4, 4, 4, // 45 - '-'
1, 16, // 46 - '.'
3, 8, 4, 2, // 47 - '/'
3, 31, 17, 31, // 48 - '0'
2, 0, 31, // 49 - '1'
3, 29, 21, 23, // 50 - '2'
3, 17, 21, 31, // 51 - '3'
3, 7, 4, 31, // 52 - '4'
3, 23, 21, 29, // 53 - '5'
3, 31, 21, 29, // 54 - '6'
3, 1, 1, 31, // 55 - '7'
3, 31, 21, 31, // 56 - '8'
3, 23, 21, 31, // 57 - '9'
1, 10, // 58 - ':'
2, 16, 10, // 59 - ';'
3, 4, 10, 17, // 60 - '<'
3, 10, 10, 10, // 61 - '='
3, 17, 10, 4, // 62 - '>'
3, 1, 21, 3, // 63 - '?'
3, 14, 21, 22, // 64 - '@'
3, 30, 5, 30, // 65 - 'A'
3, 31, 21, 10, // 66 - 'B'
3, 14, 17, 17, // 67 - 'C'
3, 31, 17, 14, // 68 - 'D'
3, 31, 21, 17, // 69 - 'E'
3, 31, 5, 1, // 70 - 'F'
3, 14, 17, 29, // 71 - 'G'
3, 31, 4, 31, // 72 - 'H'
3, 17, 31, 17, // 73 - 'I'
3, 8, 16, 15, // 74 - 'J'
3, 31, 4, 27, // 75 - 'K'
3, 31, 16, 16, // 76 - 'L'
3, 31, 2, 31, // 77 - 'M'
3, 31, 14, 31, // 78 - 'N'
3, 14, 17, 14, // 79 - 'O'
3, 31, 5, 2, // 80 - 'P'
3, 14, 25, 30, // 81 - 'Q'
3, 31, 5, 26, // 82 - 'R'
3, 18, 21, 9, // 83 - 'S'
3, 1, 31, 1, // 84 - 'T'
3, 15, 16, 15, // 85 - 'U'
3, 7, 24, 7, // 86 - 'V'
3, 15, 28, 15, // 87 - 'W'
3, 27, 4, 27, // 88 - 'X'
3, 3, 28, 3, // 89 - 'Y'
3, 25, 21, 19, // 90 - 'Z'
2, 31, 17, // 91 - '['
3, 2, 4, 8, // 92 - '\'
2, 17, 31, // 93 - ']'
3, 2, 1, 2, // 94 - '^'
3, 16, 16, 16, // 95 - '_'
2, 1, 2, // 96 - '`'
3, 12, 18, 28, // 97 - 'a'
3, 31, 18, 12, // 98 - 'b'
3, 12, 18, 18, // 99 - 'c'
3, 12, 18, 31, // 100 - 'd'
3, 12, 26, 20, // 101 - 'e'
3, 4, 31, 5, // 102 - 'f'
3, 20, 26, 12, // 103 - 'g'
3, 31, 2, 28, // 104 - 'h'
1, 29, // 105 - 'i'
2, 16, 13, // 106 - 'j'
3, 31, 8, 20, // 107 - 'k'
1, 31, // 108 - 'l'
3, 30, 6, 30, // 109 - 'm'
3, 30, 2, 28, // 110 - 'n'
3, 12, 18, 12, // 111 - 'o'
3, 30, 10, 4, // 112 - 'p'
3, 4, 10, 30, // 113 - 'q'
2, 30, 4, // 114 - 'r'
3, 20, 30, 10, // 115 - 's'
3, 4, 30, 4, // 116 - 't'
3, 14, 16, 30, // 117 - 'u'
3, 14, 16, 14, // 118 - 'v'
3, 14, 24, 14, // 119 - 'w'
3, 18, 12, 18, // 120 - 'x'
3, 22, 24, 14, // 121 - 'y'
3, 26, 30, 22, // 122 - 'z'
3, 4, 27, 17, // 123 - '{'
1, 27, // 124 - '|'
3, 17, 27, 4, // 125 - '}'
3, 6, 2, 3, // 126 - '~'
3, 31, 31, 31, // 127 - 'Full Block'
};

View File

@@ -0,0 +1,694 @@
// Implements the game of Tetris using MD_MAXPanel library
// Tips for coding this taken from https://www.youtube.com/watch?v=8OK8_tHeCIA
//
// Hardware used
// =============
//
// LEFT_PIN - move left switch, INPUT_PULLUP
// RIGHT_PIN - move right switch, INPUT_PULLUP
// UP_PIN - unused
// DOWN_PIN - drop the piece switch, INPUT_PULLUP
// SELECT_PIN - rotate the piece switch, INPUT_PULLUP
// ENTER_PIN - unused
// BEEPER_PIN - piezo speaker
// CLK_PIN, DATA_PIN, CS_PIN - LED matrix display connections
//
// Libraries used
// ==============
// MD_MAX72XX available from https://github.com/MajicDesigns/MD_MAX72XX
//
// Rules of the Game
// =================
// Game pieces are shaped like tetrominoes, geometric shapes composed of four
// square blocks each. A random sequence of tetrominoes fall down the playing
// field (a rectangular vertical shaft, called the "well" or "matrix"). The
// objective of the game is to manipulate these tetrominoes, by moving each one
// sideways (if the player feels the need) and rotating it by 90 degree units,
// with the aim of creating a horizontal line of ten units without gaps. When
// such a line is created, it gets destroyed, and any block above the deleted
// line will fall. When a certain number of lines are cleared, the game enters
// a new level. As the game progresses, each level causes the tetrominoes to fall
// faster, and the game ends when the stack of tetrominoes reaches the top of
// the playing field and no new tetrominos are able to enter.
#include <MD_MAXPanel.h>
#include "Font5x3.h"
#include "score.h"
#include "sound.h"
#include "randomseed.h"
// Turn on debug statements to the serial output
#define DEBUG 0
#if DEBUG
#define PRINT(s, x) { Serial.print(F(s)); Serial.print(x); }
#define PRINTS(x) { Serial.print(F(x)); }
#define PRINTD(x) { Serial.print(x, DEC); }
#define PRINTXY(s,x,y) { Serial.print(F(s)); Serial.print(F("(")); Serial.print(x); Serial.print(F(",")); Serial.print(y); Serial.print(")"); }
#define PRINTSTATE(s) { Serial.print("\n++>"); Serial.print(s); }
#else
#define PRINT(s, x)
#define PRINTS(x)
#define PRINTD(x)
#define PRINTXY(s,x,y)
#define PRINTSTATE(s)
#endif
// Hardware pin definitions.
// All momentary on switches are initialized PULLUP
const uint8_t UP_PIN = 2;
const uint8_t RIGHT_PIN = 3;
const uint8_t DOWN_PIN = 4;
const uint8_t LEFT_PIN = 5;
const uint8_t SELECT_PIN = 6;
const uint8_t ENTER_PIN = 7;
const uint8_t BEEPER_PIN = 9;
// Define the number of devices in the chain and the SPI hardware interface
// NOTE: These pin numbers will probably not work with your hardware and may
// need to be adapted
const MD_MAX72XX::moduleType_t HARDWARE_TYPE = MD_MAX72XX::FC16_HW;
const uint8_t X_DEVICES = 4;
const uint8_t Y_DEVICES = 5;
const uint8_t CLK_PIN = 12; // or SCK
const uint8_t DATA_PIN = 11; // or MOSI
const uint8_t CS_PIN = 10; // or SS
// SPI hardware interface
MD_MAXPanel mp = MD_MAXPanel(HARDWARE_TYPE, CS_PIN, X_DEVICES, Y_DEVICES);
// Arbitrary pins
// MD_MAXPanel mx = MD_MAXPanel(HARDWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, X_DEVICES, Y_DEVICES);
const uint16_t FIELD_WIDTH = 10; // playable area width inside 'bucket'
const uint16_t FIELD_HEIGHT = 21; // playable area depth inside 'bucket'
const uint16_t FIELD_BOTTOM = 3; // y coord bottom wall
const uint16_t FIELD_LEFT = 5; // x coord left wall
const uint16_t FIELD_TOP = FIELD_BOTTOM + FIELD_HEIGHT; // y coord top opening of the 'bucket'
const uint16_t FIELD_RIGHT = FIELD_LEFT + FIELD_WIDTH + 1; // x coord right wall
const char TITLE_TEXT[] = "TETRIS";
const uint16_t SPLASH_DELAY = 3000; // in milliseconds
const char GAME_TEXT[] = "GAME";
const char OVER_TEXT[] = "OVER";
const uint16_t GAME_OVER_DELAY = 3000; // in milliseconds
const uint8_t FONT_NUM_WIDTH = 3;
const uint16_t MAX_SCORE = 60000;
const uint16_t PIECE_SCORE = 5;
const uint16_t LINE_SCORE = 20;
#define ARRAY_SIZE(a) (sizeof(a)/sizeof(a[0]))
// A class to encapsulate the snake direction switches
// Can move up, down, rotate, drop
class cMoveSW
{
private:
uint8_t _pinLeft;
uint8_t _pinRight;
uint8_t _pinRotate;
uint8_t _pinDrop;
uint16_t _timeDelay; // the delay between switch detection
uint32_t _timeLastCheck; // the millis() value for the last time we checked
public:
enum moveType_t { MOVE_NONE, MOVE_LEFT, MOVE_RIGHT, MOVE_DROP, MOVE_ROTATE };
void begin(uint8_t pinL, uint8_t pinR, uint8_t pinRot, uint8_t pinD)
{
_timeDelay = 100;
_pinLeft = pinL;
_pinRight = pinR;
_pinRotate = pinRot;
_pinDrop = pinD;
_timeLastCheck = 0;
pinMode(_pinLeft, INPUT_PULLUP);
pinMode(_pinRight, INPUT_PULLUP);
pinMode(_pinRotate, INPUT_PULLUP);
pinMode(_pinDrop, INPUT_PULLUP);
}
bool anyKey(void) { return (move() != MOVE_NONE);}
moveType_t move(void)
{
if (millis() - _timeLastCheck < _timeDelay)
return(MOVE_NONE);
_timeLastCheck = millis();
if (digitalRead(_pinLeft) == LOW)
return(MOVE_LEFT);
else if (digitalRead(_pinRight) == LOW)
return(MOVE_RIGHT);
else if (digitalRead(_pinRotate) == LOW)
return(MOVE_ROTATE);
else if (digitalRead(_pinDrop) == LOW)
return(MOVE_DROP);
return(MOVE_NONE);
}
};
// A class to encapsulate the Tetris field
class cTetris
{
private:
const uint8_t OMINO_SIZE = 4; // 4x4 field flattened out into 16 bits
const uint8_t MAX_ROTATE = 4; // maximum number of rotations
uint16_t _tetromino[7]; // tetronimoes, values initialized in the constructor
bool _field[FIELD_WIDTH][FIELD_HEIGHT];
uint32_t _timeLastMove; // last time the snake was moved
uint16_t _moveDelay; // the delay between moves in milliseconds
bool _run; // game is playing when true
int8_t _pieceCount; // number of pieces completed
uint8_t _curOmino; // current tetronimo
uint8_t _nxtOmino; // the next tetronimo
uint8_t _curRotation; // current rotated orientation
uint16_t _x, _y; // _field coordinates for the piece
cScore *_pScore; // for keeping score
cSound *_pSound; // for making noise
void dumpOmino(uint8_t omino, uint8_t rot)
// for debugging only
{
PRINT("\n -- DUMPOMINO ", omino); PRINT(" R:", rot);
for (int8_t j = 0; j < OMINO_SIZE; j++)
{
PRINT("\n L", j); PRINTS(": ");
for (int8_t i = 0; i < OMINO_SIZE; i++)
{
// Get index into piece
int8_t idx = rotate(i, j, rot);
// draw the point
if ((_tetromino[omino] >> idx) & 1) { PRINTS(" 1") }
else { PRINTS(" 0") }
}
}
}
void showOmino(uint8_t omino, uint8_t rot, int16_t x, int16_t y, bool b)
// show the tetronimo on the actual display
// x and y need to be display coordinates
{
//PRINT("\n-- SHOW ", omino);
//PRINTXY(" @", x, y);
//PRINT(" b=", b);
mp.update(false);
//dumpOmino(omino, rot);
for (int8_t j = 0; j < OMINO_SIZE; j++)
for (int8_t i = 0; i < OMINO_SIZE; i++)
{
// Get index into piece
int8_t idx = rotate(i, j, rot);
// draw the point
//PRINTXY(" ", FIELD_LEFT + x + i, FIELD_TOP - y - j);
//PRINT(" i", idx);
//PRINT(" v", (_tetromino[omino] >> idx) & 1);
if ((_tetromino[omino] >> idx) & 1)
mp.setPoint(x + i, y - j, b);
}
mp.update(true);
}
void drawNxtOmino(void) { showOmino(_nxtOmino, 0, FIELD_RIGHT + OMINO_SIZE, FIELD_TOP - (2*OMINO_SIZE), true); }
void eraseNxtOmino(void) { showOmino(_nxtOmino, 0, FIELD_RIGHT + OMINO_SIZE, FIELD_TOP - (2*OMINO_SIZE), false); }
void drawOmino(uint8_t omino, uint8_t rot, int16_t x, int16_t y) { showOmino(omino, rot, FIELD_LEFT + x + 1, FIELD_TOP - y, true); }
void eraseOmino(uint8_t omino, uint8_t rot, int16_t x, int16_t y) { showOmino(omino, rot, FIELD_LEFT + x + 1, FIELD_TOP - y, false); }
void omino2Field(uint8_t omino, uint8_t rot, int16_t x, int16_t y)
// copy the tetronimo to the field, leaving the rest untouched
{
for (int8_t j = 0; j < OMINO_SIZE; j++)
{
for (int8_t i = 0; i < OMINO_SIZE; i++)
{
// Get index into piece
int8_t idx = rotate(i, j, rot);
// set in field if set in tetronimo
if ((_tetromino[omino] >> idx) & 1)
_field[x + i][y + j] = true;
}
}
}
void displayField(void)
// display the field, mindful of offsets in displayable field
{
mp.update(false);
for (uint8_t j = 0; j < FIELD_HEIGHT; j++)
for (uint8_t i = 0; i < FIELD_WIDTH; i++)
mp.setPoint(FIELD_LEFT+i+1, FIELD_TOP-j, _field[i][j]);
mp.update(true);
}
void clearField(void)
// clear the playing field
{
memset(_field, 0, sizeof(bool)*FIELD_HEIGHT*FIELD_WIDTH);
displayField();
}
uint8_t rotate(uint8_t x, uint8_t y, uint8_t r)
// return the linear index for from the x, y coords and the
// current rotation
{
uint8_t idx = 0;
switch (r) // Rotation effect
{
case 0: // 0 degrees // 0 1 2 3
idx = y * 4 + x; // 4 5 6 7
break; // 8 9 10 11
//12 13 14 15
case 1: // 90 degrees //12 8 4 0
idx = 12 + y - (x * 4); //13 9 5 1
break; //14 10 6 2
//15 11 7 3
case 2: // 180 degrees //15 14 13 12
idx = 15 - (y * 4) - x; //11 10 9 8
break; // 7 6 5 4
// 3 2 1 0
case 3: // 270 degrees // 3 7 11 15
idx = 3 - y + (x * 4); // 2 6 10 14
break; // 1 5 9 13
} // 0 4 8 12
return(idx);
}
bool checkPieceFit(uint8_t omino, uint8_t rot, int16_t x, int16_t y)
// does the piece fit in the _field array?
{
/*
PRINTXY("\n-- CHKFIT @", x, y);
PRINT(" T:", omino);
PRINT(" R:", rot);
PRINT(" H:", FIELD_HEIGHT);
PRINT(" W:", FIELD_WIDTH);
*/
for (int8_t j = 0; j < OMINO_SIZE; j++)
{
for (int8_t i = 0; i < OMINO_SIZE; i++)
{
// Get index into piece
int8_t idx = rotate(i, j, rot);
bool bCell = (_tetromino[omino] >> idx) & 1;
if (bCell) // we have an occupied cell in the tetromino
{
// PRINTXY("\n", i, j); PRINTXY("=>", x + i, y + j); PRINT(" i:", idx); PRINT(" cell: ", bCell);
// check it is in bounds
if (x + i < 0 || x + i >= FIELD_WIDTH)
{
//PRINTS(" fail x");
return(false); // out of side bounds
}
else if (y + j < 0 || y + j >= FIELD_HEIGHT)
{
//PRINTS(" fail y");
return(false); // out of bounds
}
else if (_field[x + i][y + j]) // in bounds - do a collision check
{
//PRINTS(" fail field");
return(false); // occupied field cell
}
}
}
}
//PRINTS(" pass");
return(true);
}
public:
enum moveType_t { M_LEFT, M_RIGHT, M_DROP, M_ROTATE };
cTetris(void)
{
// straight block
// 0010
// 0010
// 0010
// 0010
_tetromino[0] = 0x2222;
// T block
// 0010
// 0110
// 0010
// 0000
_tetromino[1] = 0x2620;
// square block
// 0000
// 0110
// 0110
// 0000
_tetromino[2] = 0x0660;
// normal Z block
// 0010
// 0110
// 0100
// 0000
_tetromino[3] = 0x2640;
// reversed Z block
// 0100
// 0110
// 0010
// 0000
_tetromino[4] = 0x4620;
// normal L block
// 0100
// 0100
// 0110
_tetromino[5] = 0x4460;
// reversed L block
// 0010
// 0010
// 0110
_tetromino[6] = 0x2260;
}
uint16_t getDelay(void) { return (_moveDelay); }
void setDelay(uint16_t delay) { if (delay > 10) _moveDelay = delay; }
void start(void) { _run = true; }
void stop(void) { _run = false; }
void begin(cScore *pScore, cSound *pSound)
{
_timeLastMove = 0;
_moveDelay = 1000;
_pieceCount = 0;
_run = false;
// set up the next tetronimo
_curOmino = 0;
_nxtOmino = random(ARRAY_SIZE(_tetromino));
_curRotation = 0;
// save and reset the score
_pScore = pScore;
_pScore->reset();
// save the sound object
_pSound = pSound;
// clear and display the field
clearField();
}
bool nextOmino(void)
{
_x = (FIELD_WIDTH - OMINO_SIZE) / 2;
_y = 0;
_curRotation = 0;
_curOmino = _nxtOmino;
if (!checkPieceFit(_curOmino, _curRotation, _x, _y))
return(false); // can't fit it it, pass the message back!
drawOmino(_curOmino, _curRotation, _x, _y);
_pSound->hit();
// work out and display the next omino
eraseNxtOmino();
_nxtOmino = random(ARRAY_SIZE(_tetromino));
drawNxtOmino();
return(true);
}
bool move(moveType_t moveType)
// user defined action to do something
{
bool b = false;
eraseOmino(_curOmino, _curRotation, _x, _y);
switch (moveType)
{
case M_LEFT:
if (b = checkPieceFit(_curOmino, _curRotation, _x - 1, _y))
_x--;
break;
case M_RIGHT:
if (b = checkPieceFit(_curOmino, _curRotation, _x + 1, _y))
_x++;
break;
case M_ROTATE:
if (b = checkPieceFit(_curOmino, (_curRotation + 1) % MAX_ROTATE, _x, _y))
_curRotation = (_curRotation + 1) % MAX_ROTATE;
break;
case M_DROP:
if (b = checkPieceFit(_curOmino, _curRotation, _x, _y + 1))
_y++;
break;
}
drawOmino(_curOmino, _curRotation, _x, _y);
return(b);
}
bool run(void)
// return false if the game has ended
{
bool bReturn = true;
// if this is the time for a move?
if (_run && millis() - _timeLastMove <= _moveDelay)
return(true);
_timeLastMove = millis();
// check if the piece can be moved down
if (checkPieceFit(_curOmino, _curRotation, _x, _y + 1))
{
// yes - do it and animate the display
eraseOmino(_curOmino, _curRotation, _x, _y);
_y++;
drawOmino(_curOmino, _curRotation, _x, _y);
// now adjust the seed if we have reached the threshold
if (_pieceCount >= 10)
{
// cap how fast we make it!
if (_moveDelay >= 200) _moveDelay -= 50;
_pieceCount = 0;
}
}
else
{
_pieceCount++; // just landed another one
// insert the current piece in the field, show it and update the score
omino2Field(_curOmino, _curRotation, _x, _y);
displayField();
_pScore->increment(PIECE_SCORE);
// Check for lines to delete
// There can only be lines in the span of the last block, so
// check the 4 lines it takes up
uint16_t lines = 0;
for (int16_t y = _y + OMINO_SIZE; (y >= _y) && (y >= 0); y--)
{
int16_t count = 0;
for (int16_t x = 0; x < FIELD_WIDTH; x++)
count += (_field[x][y] ? 1 : 0);
if (count == FIELD_WIDTH)
{
lines++; // keep count of lines completed
// delete the line - blank it out, pause for effect,
// make a sound then collapse the lines!
for (int16_t x = 0; x < FIELD_WIDTH; x++)
_field[x][y] = false;
displayField();
delay(200);
_pSound->bounce();
for (int16_t j = y; j >= 1; j--)
for (int16_t x = 0; x < FIELD_WIDTH; x++)
_field[x][j] = _field[x][j-1];
displayField();
// roll back the index as we have just changed the lines
y++;
}
}
// update the score if deleted lines
if (lines != 0) _pScore->increment((1 << lines)*LINE_SCORE);
// create the next omino and return status
bReturn = nextOmino();
}
return(bReturn);
}
};
// main objects coordinated by the code logic
cScore score;
cMoveSW moveSW;
cSound sound;
cTetris tetris;
void setupField(void)
// Draw the playing field at the start of the game.
{
mp.clear();
mp.drawHLine(FIELD_BOTTOM, FIELD_LEFT, FIELD_RIGHT);
mp.drawVLine(FIELD_LEFT, FIELD_TOP, FIELD_BOTTOM);
mp.drawVLine(FIELD_RIGHT, FIELD_TOP, FIELD_BOTTOM);
score.draw();
}
void setup()
{
#if DEBUG
Serial.begin(57600);
#endif
PRINTS("\n[MD_MAXPanel_Tetris]");
mp.begin();
mp.setFont(_Fixed_5x3);
mp.setIntensity(4);
// mp.setRotation(MD_MAXPanel::ROT_90);
sound.begin(BEEPER_PIN);
score.limit(MAX_SCORE); // so we can use width() below
score.begin(&mp, mp.getXMax() - (score.width() * (FONT_NUM_WIDTH + mp.getCharSpacing())) + mp.getCharSpacing(), mp.getYMax() - 1, MAX_SCORE);
moveSW.begin(LEFT_PIN, RIGHT_PIN, SELECT_PIN, DOWN_PIN);
randomSeed(seedOut(31, RANDOM_SEED_PORT));
}
bool handleUI(void)
{
bool b = true;
switch (moveSW.move())
{
case cMoveSW::MOVE_ROTATE:tetris.move(cTetris::M_ROTATE); break;
case cMoveSW::MOVE_DROP: tetris.move(cTetris::M_DROP); break;
case cMoveSW::MOVE_LEFT: tetris.move(cTetris::M_LEFT); break;
case cMoveSW::MOVE_RIGHT: tetris.move(cTetris::M_RIGHT); break;
case cMoveSW::MOVE_NONE: b = false; break;
}
return(b);
}
void loop(void)
{
static enum { S_SPLASH, S_INIT, S_WAIT_START, S_PLAY, S_GAME_OVER } runState = S_SPLASH;
switch (runState)
{
case S_SPLASH: // show splash screen at start
PRINTSTATE("SPLASH");
{
const uint16_t border = 2;
mp.clear();
mp.drawRectangle(border, border, mp.getXMax() - border, mp.getYMax() - border);
mp.drawLine(0, 0, border, border);
mp.drawLine(0, mp.getYMax(), border, mp.getYMax()-border);
mp.drawLine(mp.getXMax(), 0, mp.getXMax()-border, border);
mp.drawLine(mp.getXMax(), mp.getYMax(), mp.getXMax()-border, mp.getYMax()-border);
mp.drawText((mp.getXMax() - mp.getTextWidth(TITLE_TEXT)) / 2, (mp.getYMax() + mp.getFontHeight())/2 , TITLE_TEXT);
sound.splash();
delay(SPLASH_DELAY);
runState = S_INIT;
}
break;
case S_INIT: // initialize for a new game
PRINTSTATE("INIT");
setupField();
tetris.begin(&score, &sound);
runState = S_WAIT_START;
PRINTSTATE("WAIT_START");
break;
case S_WAIT_START: // waiting for the start of a new game
if (moveSW.anyKey())
{
PRINTS("\n-- Starting Game");
sound.start();
tetris.start();
if (tetris.nextOmino())
{
runState = S_PLAY;
PRINTSTATE("PLAY");
}
else
runState = S_GAME_OVER;
}
break;
case S_PLAY: // playing a point
// handle the switches
handleUI();
// move and finish the game if false
if (!tetris.run())
{
tetris.stop();
runState = S_GAME_OVER;
}
break;
case S_GAME_OVER:
{
uint16_t w, x, y;
PRINTSTATE("GAME_OVER");
w = mp.getTextWidth(GAME_TEXT);
x = ((mp.getXMax() - w) / 2) - 1;
y = (mp.getYMax() / 2) + mp.getFontHeight() + 2;
mp.clear(x, y, x + w + 2, y - mp.getFontHeight() - 2);
mp.drawText(x + 1, y - 1, GAME_TEXT);
w = mp.getTextWidth(OVER_TEXT);
x = ((mp.getXMax() - w) / 2) - 1;
y = (mp.getYMax() / 2) - 1 + 2;
mp.clear(x, y, x + w + 2, y - mp.getFontHeight() - 1);
mp.drawText(x + 1, y - 1, OVER_TEXT);
sound.over();
delay(GAME_OVER_DELAY);
runState = S_INIT;
}
break;
}
}

View File

@@ -0,0 +1,45 @@
#pragma once
// Random seed creation --------------------------
// Adapted from http://www.utopiamechanicus.com/article/arduino-better-random-numbers/
const uint8_t RANDOM_SEED_PORT = A3; // port read for random seed
uint16_t bitOut(uint8_t port)
{
static bool firstTime = true;
uint32_t prev = 0;
uint32_t bit1 = 0, bit0 = 0;
uint32_t x = 0, limit = 99;
if (firstTime)
{
firstTime = false;
prev = analogRead(port);
}
while (limit--)
{
x = analogRead(port);
bit1 = (prev != x ? 1 : 0);
prev = x;
x = analogRead(port);
bit0 = (prev != x ? 1 : 0);
prev = x;
if (bit1 != bit0)
break;
}
return(bit1);
}
uint32_t seedOut(uint16_t noOfBits, uint8_t port)
{
// return value with 'noOfBits' random bits set
uint32_t seed = 0;
for (int i = 0; i<noOfBits; ++i)
seed = (seed << 1) | bitOut(port);
return(seed);
}
//------------------------------------------------------------------------------

View File

@@ -0,0 +1,54 @@
#pragma once
#include <MD_MAXPanel.h>
// A class to encapsulate the score display
class cScore
{
private:
MD_MAXPanel *_mp; // the MAXPanel object for drawing the score
uint16_t _score; // the score
uint16_t _x, _y; // coordinate of top left for display
uint8_t _width; // number of digits wide
uint16_t _limit; // maximum value allowed
public:
void begin(MD_MAXPanel *mp, uint16_t x, uint16_t y, uint16_t maxScore) { _mp = mp; _x = x, _y = y; limit(maxScore); reset(); }
void reset(void) { erase(); _score = 0; draw(); }
void set(uint16_t s) { if (s <= _limit) { erase(); _score = s; draw(); } }
void increment(uint16_t inc = 1) { if (_score + inc <= _limit) { erase(); _score += inc; draw(); } }
void decrement(uint16_t dec = 1) { if (_score >= dec) { erase(); _score -= dec; draw(); } }
uint16_t score(void) { return(_score); }
void erase(void) { draw(false); }
uint16_t width(void) { return(_width); }
void limit(uint16_t m)
{
erase(); // width may change, so delete with the curret parameters
_limit = m;
// work out how many digits this is
_width = 0;
do
{
_width++;
m /= 10;
} while (m != 0);
}
void draw(bool state = true)
{
char sz[_width + 1];
uint16_t s = _score;
// PRINT("\n-- SCORE: ", _score);
sz[_width] = '\0';
for (int i = _width - 1; i >= 0; --i)
{
sz[i] = (s % 10) + '0';
s /= 10;
}
_mp->drawText(_x, _y, sz, MD_MAXPanel::ROT_0, state);
}
};

View File

@@ -0,0 +1,47 @@
#pragma once
// A class to encapsulate primitive sound effects
class cSound
{
private:
const uint16_t EOD = 0; // End Of Data marker
uint8_t _pinBeep; // the pin to use for beeping
// Sound data - frequency followed by duration in pairs.
// Data ends in End Of Data marker EOD.
const uint16_t soundSplash[1] PROGMEM = { EOD };
const uint16_t soundHit[3] PROGMEM = { 1000, 50, EOD };
const uint16_t soundBounce[3] PROGMEM = { 500, 50, EOD };
const uint16_t soundPoint[3] PROGMEM = { 150, 150, EOD };
const uint16_t soundStart[7] PROGMEM = { 250, 100, 500, 100, 1000, 100, EOD };
const uint16_t soundOver[7] PROGMEM = { 1000, 100, 500, 100, 250, 100, EOD };
void playSound(const uint16_t *table)
// Play sound table data. Data table must end in EOD marker.
{
uint8_t idx = 0;
//PRINTS("\nTone Data ");
while (table[idx] != EOD)
{
uint16_t t = table[idx++];
uint16_t d = table[idx++];
//PRINTXY("-", t, d);
tone(_pinBeep, t);
delay(d);
}
//PRINTS("-EOD");
noTone(_pinBeep); // be quiet now!
}
public:
void begin(uint8_t pinBeep) { _pinBeep = pinBeep; }
void splash(void) { playSound(soundSplash); }
void start(void) { playSound(soundStart); }
void hit(void) { playSound(soundHit); }
void bounce(void) { playSound(soundBounce); }
void point(void) { playSound(soundPoint); }
void over(void) { playSound(soundOver); }
};