John Bradnam
Published © GPL3+

2048 Puzzle

An addictive puzzle where the objective is to merge tiles to create a tile whose value is 2048.

AdvancedFull instructions provided20 hours209
2048 Puzzle

Things used in this project

Hardware components

Microchip ATtiny1614 CPU
×1
MAX7219 ic
×8
0.28in 4-Digit 7-Segment CC display
×16
PTS 645 Series Switch
C&K Switches PTS 645 Series Switch
9mm shaft
×7
Pushbutton Switch, Push-Pull
Pushbutton Switch, Push-Pull
8mmx8mm on/off switch with button top
×1
Buzzer
Buzzer
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)
Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Custom parts and enclosures

STL Files

Files for 3D printing

Schematics

PCB - Display (2 off)

Eagle Files

Schematic and PCB in Eagle format

Schematic - CPU

PCB - CPU

Schematic - Display (8 off)

Code

_2048_V2.ino

C/C++
/*============================================================================================================
  2048 Game
  Description (Soure wikipedia)
  2048 is a game created by Gabriele Cirulli. It is played on a plain 44 grid, with numbered tiles that slide 
  when a player moves them using the four arrow keys. Every turn, a new tile randomly appears in an empty spot
  on the board with a value of either 2 or 4. Tiles slide as far as possible in the chosen direction until they 
  are stopped by either another tile or the edge of the grid. If two tiles of the same number collide while 
  moving, they will merge into a tile with the total value of the two tiles that collided. The resulting tile 
  cannot merge with another tile again in the same move.

  If a move causes three consecutive tiles of the same value to slide together, only the two tiles farthest 
  along the direction of motion will combine. If all four spaces in a row or column are filled with tiles of 
  the same value, a move parallel to that row/column will combine the first two and last two. The user's score 
  starts at zero, and is increased whenever two tiles combine, by the value of the new tile.

  The game is won when a tile with a value of 2048 appears on the board. When the player has no legal moves 
  (there are no empty spaces and no adjacent tiles with the same value), the game ends.

  ------------------------------------------------------------------------------------------------------------\
  CPU:  ATtiny1614
  Display: 8 x MAX7219 controlling 4 rows by 4 columns of 4 Digit displays
  Code: jbrad2089@gmail.com
  
  BOARD: ATtiny1614/1604/814/804/414/404/214/204
  Chip: ATtiny1614
  Clock Speed: 20MHz
  Programmer: jtag2updi (megaTinyCore)
==============================================================================================================*/

#include "Game.h"
#include "Display.h"
#include "Switches.h"
#include "Memory.h"
#include "Sound.h"

#define RANDOM 1 //PA5

//---------------------------  Pin definitions -------------------------------

void setup()
{
  //Read in data from EEPROM;
  ee::read();
  
  //Setup screen
  srn::setup();
  srn::clear();     //Clear the primary buffer
  srn::refresh();   //Transfer to display buffer

  //Setup switches
  sw::setup();

  //Setup sound
  play::setup();

  //Setup random generator
  pinMode(RANDOM, INPUT);
  randomSeed(analogRead(RANDOM));

  //clear the board and variables
  startGame(false);

  //Show splash screen
  splashState = true;
  splashTimeOut = 0;
}

void loop()
{

  gameLoop();
  if (sw::pressed() == sw::BA)
  {
    //Start game
    splashState = false;  //turn off splash screen
    startGame(true);
    sw::waitForKeyUp();
  }
  else if (sw::pressed() == sw::BB)
  {
    //Equivlant to power on
    srn::clear();     //Clear the primary buffer
    srn::refresh();   //Transfer to display buffer
    //clear the board and variables
    startGame(false);
    //Show splash screen
    splashState = true;
    splashTimeOut = 0;
    sw::waitForKeyUp();
  }
  else if (sw::pressed() == sw::BC)
  {
    //I seem to have a faulty MAX7219 chip that sometimes go blank
    //This resets all the MAX7219 chips which seems to address the problem
    //The board is restored and the game can continue.
    srn::reset();
    transferBoard();
    sw::waitForKeyUp();
  }
}

2048.zip

Assembly x86
Playable Windows version.
No preview (download only).

Game.h

C/C++
/*
Namespace: game
Author: John Bradnam (jbrad2089@gmail.com)
Purpose: 2048 game logic
*/
#pragma once

#include "Display.h"
#include "Switches.h"
#include "Memory.h"
#include "Sound.h"

#define ROW(i) (i >> 2)
#define COL(i) (i & 0x03)
#define INDEX(r,c) ((r << 2) | c)

#define NO_FREE_SQUARES 16
#define GRID_SIZE 4
#define GRID_TOTAL (GRID_SIZE * GRID_SIZE)
uint16_t grid[GRID_TOTAL];

uint32_t score;
bool gameOver = true;
uint16_t lastJoin;
uint8_t nextCell;

#define FLASH_TIMEOUT 200;
unsigned long flashTimeOut = 0;
bool flashState = false;

#define SCORE_TIMEOUT 2000;
unsigned long scoreTimeOut = 0;
bool scoreState = false;

#define SPLASH_TIMEOUT 100;
unsigned long splashTimeOut = 0;
bool splashState = false;
uint8_t splashRow = 0;
uint8_t splashCol = 0;
int8_t splashDir = 1;

//-------------------------------------------------------------------------
//Forward references
void startGame(bool addTiles);
void evaluateGame();
uint8_t getRandomFreeSquare();
void transferBoard();
void flashNewCell();
void flashScore();
void animateSplashScreen();
bool isLeft();
void moveLeft();
void removeEmptyCellsShiftLeft(int r, int s);
bool isRight();
void moveRight();
void removeEmptyCellsShiftRight(int r, int s);
bool isUp();
void moveUp();
void removeEmptyCellsShiftUp(int r, int s);
bool isDown();
void moveDown();
void removeEmptyCellsShiftDown(int r, int s);

//-------------------------------------------------------------------------

/// <summary>
/// Initialise the game
/// </summary>
/// <param name="addTiles">true to add two starting tiles</param>
void startGame(bool addTiles)
{
  for (int i=0; i<GRID_TOTAL; i++) 
  { 
    grid[i] = 0;      //Clear board
  }

  flashTimeOut = 0;
  flashState = false;
  
  score = 0;
  lastJoin = 0;
  nextCell = NO_FREE_SQUARES;
  gameOver = !addTiles;

  if (addTiles)
  {
    //Set two random squares with a value of either 2 or 4
    grid[getRandomFreeSquare()] = (random(2) << 1) + 2;
    grid[getRandomFreeSquare()] = (random(2) << 1) + 2;
  }

  //Update screen
  transferBoard();
}

/// <summary>
/// Loop while game is in play
/// </summary>
void gameLoop()
{
  if (!gameOver)
  {
    sw::BUTTONS b = sw::pressed();
    if (b != sw::NONE)
    {
      switch (b)
      {
        case sw::LF:
          if (isLeft())
          {
              play::buttonPressed();
              moveLeft();
              evaluateGame();
          }
          else
          {
              play::invalidResponse();
          }
          break;

        case sw::RG:
          if (isRight())
          {
              play::buttonPressed();
              moveRight();
              evaluateGame();
          }
          else
          {
              play::invalidResponse();
          }
          break;

        case sw::UP:
          if (isUp())
          {
              play::buttonPressed();
              moveUp();
              evaluateGame();
          }
          else
          {
              play::invalidResponse();
          }
          break;

        case sw::DN:
          if (isDown())
          {
              play::buttonPressed();
              moveDown();
              evaluateGame();
          }
          else
          {
              play::invalidResponse();
          }
          break;
      }
      sw::waitForKeyUp();
    }
    flashNewCell();
  }
  else if (splashState)
  {
    animateSplashScreen();
  }
  else
  {
    flashScore();
  }
}

/// <summary>
/// Test for win or loss
/// </summary>
void evaluateGame()
{
    if (!gameOver)
    {
        //labScore.Text = String.Format("{0}", score);
        if (lastJoin == 2048)
        {
            nextCell = NO_FREE_SQUARES;
            gameOver = true;
            ee::data.wins++;
            if (score > ee::data.high)
            {
              ee::data.high = score;
            }
            ee::write();
            play::winSound();    
        }
        else
        {
            //Generate a new random piece
            nextCell = getRandomFreeSquare();
            if (nextCell != NO_FREE_SQUARES)
            {
                grid[nextCell] = (random(2) << 1) + 2;
                transferBoard();      //force update just incase there is no valid moves left
                if (!isLeft() && !isRight() && !isUp() && !isDown())
                {
                    nextCell = NO_FREE_SQUARES;
                    gameOver = true;
                    if (score > ee::data.high)
                    {
                      ee::data.high = score;
                      ee::write();
                    }
                    play::loseSound();    
                }
            }
            else
            {
                nextCell = NO_FREE_SQUARES;
                gameOver = true;
                if (score > ee::data.high)
                {
                  ee::data.high = score;
                  ee::write();
                }
                play::loseSound();    
            }
        }

        //Update screen
        transferBoard();
    }
}

/// <summary>
/// Return the next available random square 
/// </summary>
/// <returns>returns 0 to 15 otherwise NO_FREE_SQUARES if no free squares</returns>
uint8_t getRandomFreeSquare()
{
  uint8_t freeCount = 0;
  for (int i=0; i<GRID_TOTAL; i++) 
  { 
    if (grid[i] == 0)
    {
      freeCount = freeCount + 1;
    }
  }
  if (freeCount != 0)
  {
    //Get ordinal value of random free square
    freeCount = random(freeCount);  //0 to freeCount
    for (int i=0; i<GRID_TOTAL; i++) 
    { 
      if (grid[i] == 0)
      {
        if (freeCount == 0)
        {
          return i;
        }
        else
        {
          freeCount--;
        }
      }
    }
  }
  return NO_FREE_SQUARES;
}

/// <summary>
/// Transfer board to the display 
/// </summary>
void transferBoard()
{
  for (int i=0; i<GRID_TOTAL; i++) 
  {
    srn::displayNumber(ROW(i), COL(i), grid[i], true, true);     
  }
  srn::refresh();
  
  flashState = true;
  flashTimeOut = millis() + FLASH_TIMEOUT;
}

/// <summary>
/// Flash the newest cell
/// </summary>
void flashNewCell()
{
  if (nextCell != NO_FREE_SQUARES && millis() > flashTimeOut)
  {
    srn::displayNumber(ROW(nextCell), COL(nextCell), grid[nextCell], true, flashState);     
    srn::refresh();
    flashState = !flashState;
    flashTimeOut = millis() + FLASH_TIMEOUT;
  }
}

/// <summary>
/// Switch between score and board
/// </summary>
void flashScore()
{
  if (score != 0 && millis() > scoreTimeOut)
  {
    if (scoreState)
    {
      srn::clear();
      srn::displayString(0, 1, "RESULTS");
      srn::displayString(1, 0, "   SCORE");
      srn::displayNumberLong(1, 2, score);     
      srn::displayString(2, 0, "    HIGH");
      srn::displayNumberLong(2, 2, ee::data.high);     
      srn::displayString(3, 0, "TOT 2048");
      srn::displayNumber(3, 3, ee::data.wins, false, true);
      srn::refresh();
    }
    else
    {
      transferBoard();
    }
    scoreState = !scoreState;
    scoreTimeOut = millis() + SCORE_TIMEOUT;
  }
}

/// <summary>
/// Show splash screen animation
/// </summary>
void animateSplashScreen()
{
  if (splashState && millis() > splashTimeOut)
  {
    srn::displayNumber(splashRow, splashCol, 0, true, false);
    if (splashDir == 1)
    {
      if (splashCol != 3)
      {
        splashCol++;
      }
      else
      {
        splashDir = GRID_SIZE;
        splashRow += 1;
      }
    }
    else if (splashDir == -1)
    {
      if (splashCol != 0)
      {
        splashCol--;
      }
      else
      {
        splashDir = -GRID_SIZE;
        splashRow -= 1;
      }
    }
    else if (splashDir == GRID_SIZE)
    {
      if (splashRow != 3)
      {
        splashRow++;
      }
      else
      {
        splashDir = -1;
        splashCol--;
      }
    }
    else if (splashDir == -GRID_SIZE)
    {
      if (splashRow != 0)
      {
        splashRow--;
      }
      else
      {
        splashDir = 1;
        splashCol++;
      }
    }
    srn::displayNumber(splashRow, splashCol, 2048, true, true);
    srn::displayString(1, 1, " 2  ");
    srn::displayString(1, 2, "  0 ");
    srn::displayString(2, 1, " 4  ");
    srn::displayString(2, 2, "  8 ");
    srn::refresh();
    splashTimeOut = millis() + SPLASH_TIMEOUT;
  }
}

//==========================================================================
// LEFT routines
//==========================================================================

/// <summary>
/// Test if there is a valid left move 
/// </summary>
/// <returns>true if one or more left moves available</returns>
bool isLeft()
{
    int i;
    for (int r = 0; r < GRID_SIZE; r++)
    {
        bool leading = true;
        for (int c = (GRID_SIZE - 1); c >= 0; c--)
        {
            i = INDEX(r, c);
            if (grid[i] != 0 || !leading)
            {
                leading = false;
                if (grid[i] == 0 || (c > 0 && grid[i] == grid[i - 1]))
                {
                    //Cell is empty or the cell to the left is the same value
                    return true;
                }
            }
        }
    }
    return false;
}

/// <summary>
/// Moves the tiles left
/// </summary>
void moveLeft()
{
    int i;
    for (int r = 0; r < GRID_SIZE; r++)
    {
        removeEmptyCellsShiftLeft(r, 0);

        //Merge any cells that match
        for (int c = 0; c < (GRID_SIZE - 1); c++)
        {
            i = INDEX(r, c);
            if (grid[i] != 0 && grid[i] == grid[i + 1])
            {
                //Cell to the right matches so merge cell
                grid[i] += grid[i + 1];
                //Add combined total to score
                lastJoin = grid[i];
                score += lastJoin;
                //Erase cell to the right
                grid[i + 1] = 0;
                //Because we just erased a cell, we need to shift
                //any to the right to fill the hole
                removeEmptyCellsShiftLeft(r, c + 1);
            }
        }
    }
}

/// <summary>
/// Remove any empty cells to the right and shift non-empty cells left
/// </summary>
/// <param name="r">row to operate on</param>
/// <param name="s">starting column</param>
void removeEmptyCellsShiftLeft(int r, int s)
{
    int i;
    int j;
    int e;

    //Remove spaces and shift cells left
    for (int c = s; c < (GRID_SIZE - 1); c++)
    {
        i = INDEX(r, c);
        if (grid[i] == 0)
        {
            j = i;
            e = 0;
            while (grid[j] == 0 && COL(j) < (GRID_SIZE - 1))
            {
                grid[j] = grid[j + 1];
                e += grid[j];
                grid[j + 1] = 0;
                j++;
            }
            if (grid[i] == 0 && e != 0)
            {
                c--;  //still empty
            }
        }
    }
}

//==========================================================================
// RIGHT routines
//==========================================================================

/// <summary>
/// Test if there is a valid right move 
/// </summary>
/// <returns>true if one or more right moves available</returns>
bool isRight()
{
    int i;
    for (int r = 0; r < GRID_SIZE; r++)
    {
        bool leading = true;
        for (int c = 0; c < GRID_SIZE; c++)
        {
            i = INDEX(r, c);
            if (grid[i] != 0 || !leading)
            {
                leading = false;
                if (grid[i] == 0 || (c < (GRID_SIZE - 1) && grid[i] == grid[i + 1]))
                {
                    //Cell is empty or the cell to the right is the same value
                    return true;
                }
            }
        }
    }
    return false;
}

/// <summary>
/// Moves the tiles right
/// </summary>
void moveRight()
{
    int i;
    for (int r = 0; r < GRID_SIZE; r++)
    {
        removeEmptyCellsShiftRight(r, 0);

        //Merge any cells that match
        for (int c = (GRID_SIZE - 1); c > 0; c--)
        {
            i = INDEX(r, c);
            if (grid[i] != 0 && grid[i] == grid[i - 1])
            {
                //Cell to the right matches so merge cell
                grid[i] += grid[i - 1];
                //Add combined total to score
                lastJoin = grid[i];
                score += lastJoin;
                //Erase cell to the right
                grid[i - 1] = 0;
                //Because we just erased a cell, we need to shift
                //any to the left to fill the hole
                removeEmptyCellsShiftRight(r, c - 1);
            }
        }
    }
}

/// <summary>
/// Remove any empty cells to the right and shift non-empty cells left
/// </summary>
/// <param name="r">row to operate on</param>
/// <param name="s">starting column</param>
void removeEmptyCellsShiftRight(int r, int s)
{
    int i;
    int j;
    int e;

    //Remove spaces and shift cells right
    for (int c = (GRID_SIZE - 1); c > 0; c--)
    {
        i = INDEX(r, c);
        if (grid[i] == 0)
        {
            j = i;
            e = 0;
            while (grid[j] == 0 && COL(j) > 0)
            {
                grid[j] = grid[j - 1];
                e += grid[j];
                grid[j - 1] = 0;
                j--;
            }
            if (grid[i] == 0 && e != 0)
            {
                c++;  //still empty
            }
        }
    }
}

//==========================================================================
// UP routines
//==========================================================================

/// <summary>
/// Test if there is a valid up move 
/// </summary>
/// <returns>true if one or more up moves available</returns>
bool isUp()
{
    int i;
    for (int c = 0; c < GRID_SIZE; c++)
    {
        bool leading = true;
        for (int r = (GRID_SIZE - 1); r >= 0; r--)
        {
            i = INDEX(r, c);
            if (grid[i] != 0 || !leading)
            {
                leading = false;
                if (grid[i] == 0 || (r > 0 && grid[i] == grid[i - GRID_SIZE]))
                {
                    //Cell is empty or the cell above is the same value
                    return true;
                }
            }
        }
    }
    return false;
}

/// <summary>
/// Moves the tiles up
/// </summary>
void moveUp()
{
    int i;
    for (int c = 0; c < GRID_SIZE; c++)
    {
        removeEmptyCellsShiftUp(0, c);

        //Merge any cells that match
        for (int r = 0; r < (GRID_SIZE - 1); r++)
        {
            i = INDEX(r, c);
            if (grid[i] != 0 && grid[i] == grid[i + GRID_SIZE])
            {
                //Cell below matches so merge cell
                grid[i] += grid[i + GRID_SIZE];
                //Add combined total to score
                lastJoin = grid[i];
                score += lastJoin;
                //Erase cell below
                grid[i + GRID_SIZE] = 0;
                //Because we just erased a cell, we need to shift
                //any to the up to fill the hole
                removeEmptyCellsShiftUp(r + 1, c);
            }
        }
    }
}

/// <summary>
/// Remove any empty cells below and shift non-empty cells up
/// </summary>
/// <param name="s">starting row</param>
/// <param name="c">column to operate on</param>
void removeEmptyCellsShiftUp(int s, int c)
{
    int i;
    int j;
    int e;

    //Remove spaces and shift cells up
    for (int r = 0; r < (GRID_SIZE - 1); r++)
    {
        i = INDEX(r, c);
        if (grid[i] == 0)
        {
            j = i;
            e = 0;
            while (grid[j] == 0 && ROW(j) < (GRID_SIZE - 1))
            {
                grid[j] = grid[j + GRID_SIZE];
                e += grid[j];
                grid[j + GRID_SIZE] = 0;
                j += GRID_SIZE;
            }
            if (grid[i] == 0 && e != 0)
            {
                r--;  //still empty
            }
        }
    }
}

//==========================================================================
// DOWN routines
//==========================================================================

/// <summary>
/// Test if there is a valid down move 
/// </summary>
/// <returns>true if one or more down moves available</returns>
bool isDown()
{
    int i;
    for (int c = 0; c < GRID_SIZE; c++)
    {
        bool leading = true;
        for (int r = 0; r < GRID_SIZE; r++)
        {
            i = INDEX(r, c);
            if (grid[i] != 0 || !leading)
            {
                leading = false;
                if (grid[i] == 0 || (r < (GRID_SIZE - 1) && grid[i] == grid[i + GRID_SIZE]))
                {
                    //Cell is empty or the cell below is the same value
                    return true;
                }
            }
        }
    }
    return false;
}

/// <summary>
/// Moves the tiles down
/// </summary>
void moveDown()
{
    int i;
    for (int c = 0; c < GRID_SIZE; c++)
    {
        removeEmptyCellsShiftDown(0, c);

        //Merge any cells that match
        for (int r = (GRID_SIZE - 1); r > 0; r--)
        {
            i = INDEX(r, c);
            if (grid[i] != 0 && grid[i] == grid[i - GRID_SIZE])
            {
                //Cell above matches so merge cell
                grid[i] += grid[i - GRID_SIZE];
                //Add combined total to score
                lastJoin = grid[i];
                score += lastJoin;
                //Erase cell above
                grid[i - GRID_SIZE] = 0;
                //Because we just erased a cell, we need to shift
                //any to the down to fill the hole
                removeEmptyCellsShiftDown(r - 1, c);
            }
        }
    }
}

/// <summary>
/// Remove any empty cells to the above and shift non-empty cells down
/// </summary>
/// <param name="s">starting row</param>
/// <param name="c">column to operate on</param>
void removeEmptyCellsShiftDown(int s, int c)
{
    int i;
    int j;
    int e;

    //Remove spaces and shift cells down
    for (int r = (GRID_SIZE - 1); r > 0; r--)
    {
        i = INDEX(r, c);
        if (grid[i] == 0)
        {
            j = i;
            e = 0;
            while (grid[j] == 0 && ROW(j) > 0)
            {
                grid[j] = grid[j - GRID_SIZE];
                e += grid[j];
                grid[j - GRID_SIZE] = 0;
                j -= GRID_SIZE;
            }
            if (grid[i] == 0 && e != 0)
            {
                r++;  //still empty
            }
        }
    }
}

Display.h

C/C++
/*
Namespace: srn
Author: John Bradnam (jbrad2089@gmail.com)
Purpose: Display routines
*/
#pragma once

#include <MD_MAX72xx.h>
#include <SPI.h>

namespace srn
{
  
//---------------------------  Pin definitions -------------------------------

#define LED_CLK   10  // PA3 or SCK
#define LED_DATA  8   // PA1 or MOSI
#define LED_CS    0   // PA4 or SS

//---------------------------  MAX7219 mappping ------------------------------

#define HARDWARE_TYPE MD_MAX72XX::GENERIC_HW
#define MAX_DEVICES 8

// SPI hardware interface
MD_MAX72XX mx = MD_MAX72XX(HARDWARE_TYPE, LED_CS, MAX_DEVICES);

//ASCII Character Set
//Numbers 0 - 9
//Letters A - Z
//Standard Segment order _ a b c d e f g
//2048 Segment order c b d e f g _ a
uint8_t ascii[] = {
    B11111001, B11000000, B01110101, B11100101, B11001100, B10101101, B10111101, B11000001, B11111101, B11101101, B00000000, B00000000, B00000000, B00000100, B00000000, B00000000, 
    B00000000, B11011101, B10111100, B00111001, B11110100, B00111101, B00011101, B11101101, B11011100, B11000000, B11110000, B00000000, B00111000, B00000000, B10010100, B10110100, 
    B01011101, B00000000, B00010100, B10101101, B00111100, B10110000, B00000000, B00000000, B00000000, B11101100, B00000000, B10110001, B01010100, B11100001, B00000000, B00100000
};

//Digits table maps logical MAX7219 digit order to physical digit wiring on PCB
uint8_t digits[] = {0, 6, 5, 1, 4, 2, 3, 7};

//Devices table are the offsets to convert the vertical display layout per MAX7219 to a horziontal layout
int8_t devices[] = {0, 4, 8, 12, -12, -8, -4, 0};


//-------------------------------------------------------------------------
//Forward references
void setup();
void reset();
void refresh();
void clear();
void displayString(uint8_t r, uint8_t c, String s);
void displayNumber(uint8_t r, uint8_t c, uint16_t v, bool blankWhenZero, bool on);
void displayNumberLong(uint8_t r, uint8_t c, uint32_t v);
void setLogicalColumn(uint16_t c, uint8_t v);

//-------------------------------------------------------------------------
//Initialise Hardware

void setup()
{
  reset();
}

//-------------------------------------------------------------------------
//Initialise the MAX7219 chips

void reset()
{
  mx.begin();
  mx.control(MD_MAX72XX::INTENSITY, 2); //0..MAX_INTENSITY
  mx.update(MD_MAX72XX::OFF) ;          //No Auto updating
}

//-----------------------------------------------------------------------------------------------
//Refresh display

void clear()
{
  mx.clear(0, mx.getDeviceCount()-1);
}

//-----------------------------------------------------------------------------------------------
//Refresh display

void refresh()
{
  mx.update();
}

//-----------------------------------------------------------------------------------------------
// Write string of characters
// r = row (0 to 3)
// c = column (0 to 3)
// v = 0 to 9999
void displayString(uint8_t r, uint8_t c, String s)
{
  uint8_t p = (r << 4) + (c << 2);
  for (int i = 0; i < s.length(); i++)
  {
    byte c = (byte)s.charAt(i);
    if (c == 0x2D)
    {
      c = 0x3C;
    }
    if (c < 0x30 || c > 0x5F)
    {
      c = 0x3F;   //Used as a space character
    }
    c = c - 0x30;
    setLogicalColumn(p++, ascii[c]);
  }
}

//-----------------------------------------------------------------------------------------------
//Displays number in array
// r = row (0 to 3)
// c = column (0 to 3)
// v = 0 to 9999
// blankWhenZero (true = zero not shown)
// on (true to show value)

void displayNumber(uint8_t r, uint8_t c, uint16_t v, bool blankWhenZero, bool on)
{
  uint8_t p = (r << 4) + (c << 2) + 3;
  for (int i = 0; i < 4; i++)
  {
    if (on && (v != 0 || (i == 0 && !blankWhenZero)))
    {
      setLogicalColumn(p, ascii[v % 10]);
      v = v / 10;
    }
    else
    {
      setLogicalColumn(p, 0);  //Space character
    }
    p--;
  }
}

//-----------------------------------------------------------------------------------------------
//Displays number in array
// r = row (0 to 3)
// c = column (0 to 3) (Points to most significant column - number continues in column to the right) 
// v = 0 to 9999

void displayNumberLong(uint8_t r, uint8_t c, uint32_t v)
{
  //display in right matrix
  uint8_t p = (r << 4) + ((c + 1) << 2) + 3;
  for (int i = 0; i < 4; i++)
  {
    if (v != 0 || i == 0)
    {
      setLogicalColumn(p, ascii[v % 10]);
      v = v / 10;
    }
    else
    {
      setLogicalColumn(p, 0);  //Space character
    }
    p--;
  }
  //continue in left matrix
  p = (r << 4) + (c << 2) + 3;
  for (int i = 0; i < 4; i++)
  {
    if (v != 0)
    {
      setLogicalColumn(p, ascii[v % 10]);
      v = v / 10;
    }
    else
    {
      setLogicalColumn(p, 0);  //Space character
    }
    p--;
  }
}

//-----------------------------------------------------------------------------------------------
//Displays segment bit mask v using logical column value (0 being top-left, 63 being bottom-right) 
//to the physical device and digit
// c = 0 to 63, v = segs

void setLogicalColumn(uint16_t c, uint8_t v)
{
  //Logical column and row layout 
  //[00 01 02 03] [04 05 06 07] [08 09 10 11] [12 13 14 15]
  //[16 17 18 19] [20 21 22 23] [24 25 26 27] [28 29 30 31]
  //[32 33 34 35] [36 37 38 39] [40 41 42 43] [44 45 46 47]
  //[48 49 50 51] [52 53 54 55] [56 57 58 59] [60 61 62 63]
  
  //[00 01 02 03] [04 05 06 07] [08 09 0A 0B] [0C 0D 0E 0F]
  //[10 11 12 13] [14 15 16 17] [18 19 1A 1B] [1C 1D 1E 1F]
  //[20 21 22 23] [24 25 26 27] [28 29 2A 2B] [2C 2D 2E 2F]
  //[30 31 32 33] [34 35 36 37] [38 39 3A 1B] [3C 3D 3E 3F]

  //Phyisc column and row layout
  //[00 01 02 03] [08 09 0A 0B] [10 11 12 13] [18 19 1A 1B] 
  //[04 05 06 07] [0C 0D 0E 0F] [14 15 16 17] [1C 1D 1E 1F]
  //[20 21 22 23] [28 29 2A 2B] [30 31 32 33] [38 39 3A 1B] 
  //[24 25 26 27] [2C 2D 2E 2F] [34 35 36 37] [3C 3D 3E 3F]
  //Convert to horziontal layout
  uint8_t p = c + devices[(c & 0x1F) >> 2];
  
  //Fix up digit mappings
  //uint8_t digits[] = {0, 6, 5, 1, 4, 2, 3, 7};
  uint8_t tmp = ((p & 0xF8) ^ 0x20) | digits[p & 07];

  //Display digit on MAX7219 array
  mx.setColumn(tmp, v);
}

} //namespace

Memory.h

C/C++
/*
Namespace: ee
Author: John Bradnam (jbrad2089@gmail.com)
Purpose: EEPROM routines
*/
#pragma once

//Uncomment to reset EEPROM data
//#define RESET_EEPROM

#ifdef __AVR_ATtiny1614__
#include <avr/eeprom.h>
#else
#include <EEPROM.h>
#endif

namespace ee
{

//EEPROM handling
#define EEPROM_ADDRESS 0
#define EEPROM_MAGIC 0x0BAD0DAD
typedef struct {
  uint32_t magic;
  uint32_t high;
  uint16_t wins;
} EEPROM_DATA;

volatile EEPROM_DATA data;       //Current EEPROM settings

//-----------------------------------------------------------------------------------
// Forward references

void write();
void read();

//---------------------------------------------------------------
//Write the EepromData structure to EEPROM
void write()
{
  //This function uses EEPROM.update() to perform the write, so does not rewrites the value if it didn't change.
  #ifdef __AVR_ATtiny1614__
    eeprom_update_block (( void *) &data , ( const void *) EEPROM_ADDRESS, sizeof(data));  
  #else
    EEPROM.put(EEPROM_ADDRESS,data);
  #endif
}

//---------------------------------------------------------------
//Read the EepromData structure from EEPROM, initialise if necessary
void read()
{
  //Eprom
  #ifdef __AVR_ATtiny1614__
    eeprom_read_block (( void *) &data , ( const void *) EEPROM_ADDRESS, sizeof(data));  
  #else
    EEPROM.get(EEPROM_ADDRESS,EepromData);
  #endif
  #ifndef RESET_EEPROM
  if (data.magic != EEPROM_MAGIC)
  #endif
  {
    data.magic = EEPROM_MAGIC;
    data.high = 0;
    data.wins = 0;
    write();
  }
}

} //namespace

Sound.h

C/C++
/*
Namespace: play
Author: John Bradnam (jbrad2089@gmail.com)
Purpose: Sound routines
*/
#pragma once

#include <TimerFreeTone.h>  // https://bitbucket.org/teckel12/arduino-timer-free-tone/wiki/Home

namespace play
{

#define SPEAKER   2   // PA6

#define BUZZER_PORT PORTA
#define BUZZER_PIN  PIN6_bm

  
//-------------------------------------------------------------------------
//Forward references
void setup();
void buttonPressed();
void invalidResponse();
void noteDecay(int start); 
void noteAttack(int start); 
void winSound();
void loseSound();
void beep();

//-------------------------------------------------------------------------
//Initialise Hardware
void setup()
{
  pinMode(SPEAKER,OUTPUT);
}

//------------------------------------------------------------------
//Play button pressed sound
void buttonPressed()
{
  TimerFreeTone(SPEAKER, 440, 150); 
}

//------------------------------------------------------------------
//Play bad move sound
void invalidResponse()
{
  TimerFreeTone(SPEAKER, 50, 150); 
}

//-----------------------------------------------------------------------------------
//Play a decaying sound
void noteDecay(int start) 
{
  #define DECAY_NOTE 100                // Minimum delta time.
  
  for (int note = start; note >= DECAY_NOTE; note -= 10)
  {                       
    TimerFreeTone(SPEAKER, note, 100);
  }
}

//-----------------------------------------------------------------------------------
//Play a attack sound
void noteAttack(int start) 
{
  #define DECAY_NOTE 100                // Minimum delta time.
  
  for (int note = DECAY_NOTE; note <= start; note += 10)
  {                       
    TimerFreeTone(SPEAKER, note, 100);
  }
}

//------------------------------------------------------------------
//Play a high note as a sign you lost
void winSound()
{
  //TimerFreeTone(SPEAKER,880,300);
  TimerFreeTone(SPEAKER,880,100); //A5
  TimerFreeTone(SPEAKER,988,100); //B5
  TimerFreeTone(SPEAKER,523,100); //C5
  TimerFreeTone(SPEAKER,988,100); //B5
  TimerFreeTone(SPEAKER,523,100); //C5
  TimerFreeTone(SPEAKER,587,100); //D5
  TimerFreeTone(SPEAKER,523,100); //C5
  TimerFreeTone(SPEAKER,587,100); //D5
  TimerFreeTone(SPEAKER,659,100); //E5
  TimerFreeTone(SPEAKER,587,100); //D5
  TimerFreeTone(SPEAKER,659,100); //E5
  TimerFreeTone(SPEAKER,659,100); //E5
  delay(250);
}

//------------------------------------------------------------------------------------------------------------------
//Play wah wah wah wahwahwahwahwahwah
void loseSound()
{
  delay(400);
  //wah wah wah wahwahwahwahwahwah
  for(double wah=0; wah<4; wah+=6.541)
  {
    TimerFreeTone(SPEAKER, 440+wah, 50);
  }
  TimerFreeTone(SPEAKER, 466.164, 100);
  delay(80);
  for(double wah=0; wah<5; wah+=4.939)
  {
    TimerFreeTone(SPEAKER, 415.305+wah, 50);
  }
  TimerFreeTone(SPEAKER, 440.000, 100);
  delay(80);
  for(double wah=0; wah<5; wah+=4.662)
  {
    TimerFreeTone(SPEAKER, 391.995+wah, 50);
  }
  TimerFreeTone(SPEAKER, 415.305, 100);
  delay(80);
  for(int j=0; j<7; j++)
  {
    TimerFreeTone(SPEAKER, 391.995, 70);
    TimerFreeTone(SPEAKER, 415.305, 70);
  }
  delay(400);
}

//------------------------------------------------------------------------------------------------------------------
//Turn on and off buzzer quickly
void beep() 
{                                     // Beep and flash LED green unless STATE_AUTO
  BUZZER_PORT.OUTSET = BUZZER_PIN;   // turn on buzzer
  delay(5);
  BUZZER_PORT.OUTCLR = BUZZER_PIN;  // turn off the buzzer
}

} //namespace

Switches.h

C/C++
/*
Namespace: sw
Author: John Bradnam (jbrad2089@gmail.com)
Purpose: Switch routines
*/
#pragma once

#include "Sound.h"

namespace sw
{

#define SWITCHES  7   // PB0

#define DEBOUNCE_DELAY 30
enum BUTTONS { NONE, BA, BB, BC, UP, LF, DN, RG };

typedef struct
{
  int low;
  int high;
} RANGE;

//This table maps the analog pins and voltages to the actual keys
//LF=733, RG=924, UP=617, DN=827, BA=0, BB=344, BC=515
RANGE ranges[8] = {
  { 1000, 1024 }, //NONE
  { 0, 99 },      //BA
  { 100, 419 },   //BB
  { 420, 559 },   //BC
  { 560, 649 },   //UP
  { 650, 779 },   //LF
  { 780, 869 },   //DN
  { 870, 999 }    //RG
};
  
//-------------------------------------------------------------------------
//Forward references
void setup();
bool state(BUTTONS b);
BUTTONS pressed();

//-------------------------------------------------------------------------
//Initialise Hardware

void setup()
{
  pinMode(SWITCHES,INPUT);
}

//-------------------------------------------------------------------
// Return the state of the given button
//  b - BUTTONS constant to test
//  returns TRUE if key is pressed, otherwise FALSE
bool state(BUTTONS b)
{
  int16_t v = analogRead(SWITCHES);
  return (v >= ranges[(int)b].low && v <= ranges[(int)b].high);
}

//-------------------------------------------------------------------
// Return the button being pressed. Wait until button is debounced and released
//  returns BUTTONS constant of key being pressed (NONE if no key pressed)
BUTTONS pressed()
{
  for (BUTTONS b = BA; b <= RG; b = (BUTTONS)((int)b + 1))
  {
    if (state(b))
    {
      delay(DEBOUNCE_DELAY);
      if (state(b))
      {
        return b;
      }
    }
  }
  return NONE;
}

//-------------------------------------------------------------------
// Wait until any button pressed is released
void waitForKeyUp()
{
  while (analogRead(SWITCHES) < 1000) ;
}

} //namespace

Credits

John Bradnam
155 projects • 202 followers

Comments