John Bradnam
Published © GPL3+

Connect Four Game

Challenge yourself by trying to beat the computer in this rendition of the 1970's classic game of Connect 4 by Milton Bradley / Hasbro.

IntermediateFull instructions provided8 hours302
Connect Four Game

Things used in this project

Hardware components

Microchip ATtiny3224 Microprocessor
×1
74HC14 Hex Schmitt Inverter
SOIC variant
×1
Passive Components
6 x 10K 0805 Resistors, 1 x 470R 0805 Resistors, 4 x 0.1uf 0805 Ceramic Capacitors, 1 x 10uF 0805 Ceramic Capacitor, 1 x 1000uF/16V Electrolytic Capacitor
×1
Buzzer
Buzzer
×1
DC-DC Buck Step Down Module 3A Adjustable Voltage Regulator Power
×1
DC Power Supply Jack Socket Female Panel Mount Connector 5.5 x 2.1mm
×1
EC12 Rotary Encoder
20mm D Shaft
×1
8x8 WS2812B Matrix Display
×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

Schematic

PCB

Eagle files

Schematic & PCB in Eagle format

Code

ConnectFourV2.ino

Arduino
/*
  Connect 4
  Based on code by mit-mit
  (https://github.com/mit-mit-randomprojectlab/arduino_connect4)

  230530 John Bradnam (jbrad2089@gial.com)
    - Created Game for ATtiny3224 processor

  -------------------------------------------------------------------------
  Arduino IDE:
  --------------------------------------------------------------------------
  BOARD: ATtiny3224/1624/1614/1604/824/814/804/424/414/404/...
  Chip: ATtiny3224
  Clock Speed: 20MHz
  millis()/micros(): "TCA0 (default on 0-series)"
  Programmer: jtag2updi (megaTinyCore)

  ATTiny3224 Pins mapped to Ardunio Pins

              +--------+
          VCC + 1   14 + GND
  (SS)  0 PA4 + 2   13 + PA3 10 (SCK)
        1 PA5 + 3   12 + PA2 9  (MISO)
  (DAC) 2 PA6 + 4   11 + PA1 8  (MOSI)
        3 PA7 + 5   10 + PA0 11 (UPDI)
  (RXD) 4 PB3 + 6    9 + PB0 7  (SCL)
  (TXD) 5 PB2 + 7    8 + PB1 6  (SDA)
              +--------+

 **************************************************************************/

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

uint8_t current = 0;
uint8_t totalMoves = 0;
bool player = false;

struct LED {
  uint8_t row;
  uint8_t col;
};

LED winningLeds[4];

//---------------------------------------------------------------------
// Hardware setup
//---------------------------------------------------------------------

void setup()
{
  displaySetup();
  hardwareSetup();

  clearDisplay();
  drawString("CONNECT FOUR", COLOR_BLK);

  //Wait until switch pressed again or text scrolled away
  rotarySwitchPressed = false;
  while (!rotarySwitchPressed)
  {
    yield();
    if (scrollDelay == 0)
    {
      drawStringRepeat();
    }
  }
  rotarySwitchPressed = false;
  rotaryDirection = 0;

  newGame();
}

//---------------------------------------------------------------------
// Main progrm loop
//---------------------------------------------------------------------

void loop()
{
  if (rotaryDirection == -1 && current != 0)
  {
    beepDigit();
    displaySetStatus(current, COLOR_CLEAR);
    current--;
    displaySetStatus(current, COLOR_STATUS);
  }
  else if (rotaryDirection == 1 && current < (COLS - 1))
  {
    beepDigit();
    displaySetStatus(current, COLOR_CLEAR);
    current++;
    displaySetStatus(current, COLOR_STATUS);
  }
  rotaryDirection = 0;

  if (rotarySwitchPressed)
  {
    rotarySwitchPressed = false;

    uint32_t color = (player) ? COLOR_CPU : COLOR_USER;
    int row = animateDrop(current, color);
    if (row >= 0)
    {
      playValidTone();
      WIN_STATUS win = testForWin(row, current, color);
      if (win != WS_NONE)
      {
        if (win == WS_DRAW)
        {
          clearDisplay();
          drawString("IT IS A DRAW", COLOR_STATUS);
        }
        else
        {
          playWinSoundShort();
          animateFourInARow(color);
          clearDisplay();
          if (current == COLOR_USER)
          {
            drawString("YOU WIN", COLOR_USER);
          }
          else
          {
            drawString("YOU LOSE", COLOR_CPU);
          }
        }

        //wait for button release
        while (digitalRead(ENC_S) == LOW) ;
        //Wait until switch pressed again or text scrolled away
        rotarySwitchPressed = false;
        while (!rotarySwitchPressed)
        {
          yield();
          if (scrollDelay == 0)
          {
            drawStringRepeat();
          }
        }
        newGame();
        //wait for release
        while (digitalRead(ENC_S) == LOW);
        rotarySwitchPressed = false;
        rotaryDirection = 0;
      }
      else
      {
        player = !player;
        if (player)
        {
          int r = getCpuPlay(GB_CPU);
          if (r != -1)
          {
            displaySetStatus(current, COLOR_CLEAR);
            current = r;
            displaySetStatus(current, COLOR_STATUS);
            rotarySwitchPressed = true;
            rotaryDirection = 0;
          }
        }
      }
    }
    else
    {
      playInvalidTone();
    }

    //wait for release
    while (digitalRead(ENC_S) == LOW);
  }
}

//-------------------------------------------
// Start a new game
//-------------------------------------------

void newGame()
{
  //Stop scrolling
  scrollDelay = 0;

  //Start a new board
  totalMoves = 0;
  player = false; //User starts
  clearDisplay();
  current = 0;
  displaySetStatus(current, COLOR_STATUS);
  displayRefresh();
}

//-------------------------------------------
// Test if 4 in a row made
//  row - 0 to 5 (0 = top, 5 = bottom)
//  col - 0 to 6 (0 = left, 6 = right)
//  color - COLOR_ constant
//  returns WIN_STATUS constant
//-------------------------------------------

WIN_STATUS testForWin(uint8_t row, uint8_t col, uint32_t color)
{
  WIN_STATUS result = WS_NONE;
  int8_t r, c, n;
  //Test vertical
  r = row;
  if (r > 0)
  {
    r--;
    while (r > 0 && displayGetPixel(r, col) == color)
    {
      r--;
    }
    r = (displayGetPixel(r, col) == color) ? r : r + 1;
  }
  n = 0;
  while (r < ROWS && n < 4 && displayGetPixel(r, col) == color)
  {
    winningLeds[n].row = r++;
    winningLeds[n].col = col;
    n++;
  }
  if (n < 4)
  {
    //Test horizontal
    c = col;
    if (c > 0)
    {
      c--;
      while (c > 0 && displayGetPixel(row, c) == color)
      {
        c--;
      }
      c = (displayGetPixel(row, c) == color) ? c : c + 1;
    }
    n = 0;
    while (c < COLS && n < 4 && displayGetPixel(row, c) == color)
    {
      winningLeds[n].row = row;
      winningLeds[n].col = c++;
      n++;
    }
    if (n < 4)
    {
      //Test diagonal top to bottom, left to right
      c = col;
      r = row;
      if (c > 0 && r > 0)
      {
        r--;
        c--;
        while (c > 0 && r > 0 && displayGetPixel(r, c) == color)
        {
          c--;
          r--;
        }
        if (displayGetPixel(r, c) != color)
        {
          c++;
          r++;
        }
      }
      n = 0;
      while (c < COLS && r < ROWS && n < 4 && displayGetPixel(r, c) == color)
      {
        winningLeds[n].row = r++;
        winningLeds[n].col = c++;
        n++;
      }
      if (n < 4)
      {
        //Test diagonal bottom to top, left to right
        c = col;
        r = row;
        if (c > 0 && r < (ROWS - 1))
        {
          r++;
          c--;
          while (c > 0 && r < (ROWS - 1) && displayGetPixel(r, c) == color)
          {
            r++;
            c--;
          }
          if (displayGetPixel(r, c) != color)
          {
            r--;
            c++;
          }
        }
        n = 0;
        while (c < COLS && r >= 0 && n < 4 && displayGetPixel(r, c) == color)
        {
          winningLeds[n].row = r--;
          winningLeds[n].col = c++;
          n++;
        }
      }
    }
  }
  if (n == 4)
  {
    result = WS_WIN;
  }
  else if (totalMoves == (ROWS * COLS))
  {
    result = WS_DRAW;
  }
  return result;
}

//-------------------------------------------
// Animate drop
//  col - 0 to 6 (0 = left, 6 = right)
//  color - COLOR_ constant
//  returns row for valid drop, -1 if full
//-------------------------------------------

int animateDrop(uint8_t col, uint32_t color)
{
  int result = -1;
  int8_t row = 0;
  uint32_t c = displayGetPixel(row, col);
  if (c == COLOR_BLK)
  {
    while (c == COLOR_BLK && row < ROWS)
    {
      result++;
      displaySetPixel(row, col, color);
      if (row != 0)
      {
        displaySetPixel(row - 1, col, COLOR_BLK);
      }
      row++;
      displayRefresh();
      delay(ANIMATE_DROP_DELAY);
      c = displayGetPixel(row, col);
    }
    totalMoves++;
  }
  return result;
}

//-------------------------------------------
// Flash the winning four tiles
//  color - COLOR_ constant
//-------------------------------------------

void animateFourInARow(uint32_t color)
{
  for (int i = 0; i < ANIMATE_WIN_COUNT; i++)
  {
    for (int n = 0; n < 4; n++)
    {
      displaySetPixel(winningLeds[n].row, winningLeds[n].col, ((i & 0x01) == 0) ? COLOR_BLU : color);
    }
    displayRefresh();
    delay(ANIMATE_WIN_DELAY);
  }
}

Game.h

C Header File
/*
Game play
Author: John Bradnam (jbrad2089@gmail.com)
Purpose: AI for Connect Four
Based on code from mit-mit
(https://github.com/mit-mit-randomprojectlab/arduino_connect4)
*/
#pragma once

#include "Display.h"

#define COLOR_CPU COLOR_GRN
#define COLOR_USER COLOR_RED
#define COLOR_STATUS COLOR_BLU
#define COLOR_CLEAR COLOR_BLK

enum WIN_STATUS { WS_NONE, WS_DRAW, WS_WIN };

#define GB_COLS 8
#define GB_ROWS 6
#define PIECE(r,c) ((r << 3) + c)
enum GAME_BOARD { GB_EMPTY, GB_USER, GB_CPU };
GAME_BOARD board[GB_ROWS * GB_COLS];

#define NUM_MCTS_RUNS 500
#define SIMULATIONS 7
long simWins[SIMULATIONS];
long simLosses[SIMULATIONS];

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

int8_t getCpuPlay(GAME_BOARD player);
uint8_t getCurrentDisplay(GAME_BOARD gb[]);
void transferToDisplay(GAME_BOARD gb[]);
WIN_STATUS testForWin(GAME_BOARD gb[], uint8_t row, uint8_t col, GAME_BOARD player, uint8_t count);
int addPieceToColumn(GAME_BOARD gb[], uint8_t col, GAME_BOARD player);

//-------------------------------------------------------------------------
// CPU Play
//-------------------------------------------------------------------------

int8_t getCpuPlay(GAME_BOARD player) 
{
  int pieceCount;
  int simMoves;
  GAME_BOARD simPlayer;
  GAME_BOARD simOpponent;
  int8_t simRow;
  int8_t simCol;
  WIN_STATUS simWinStatus;
  long simScore, simBestScore;
  uint8_t simBestCol;

  simOpponent = (player == GB_CPU) ? GB_USER : GB_CPU;

  //Place current board into memory
  pieceCount = getCurrentDisplay(board);

  //Look for a CPU win
  for (int c = 0; c < COLS; c++) 
  {
    //Add a temporary piece to the board
    simRow = addPieceToColumn(board, c, GB_CPU);
    if (simRow != -1)
    {
      pieceCount++;
      simWinStatus = testForWin(board, simRow, c, GB_CPU, pieceCount);

      //Remove this piece from board
      board[PIECE(simRow, c)] = GB_EMPTY;
      pieceCount--;

      //Test if we won
      if (simWinStatus == WS_WIN)
      {
        return c;     //Return winning column
      }
    }
  }

  //Look for a square that stops a player win
  for (int c = 0; c < COLS; c++) 
  {
    //First look for the players winning move
    simRow = addPieceToColumn(board, c, GB_USER);
    if (simRow != -1)
    {
      pieceCount++;
      simWinStatus = testForWin(board, simRow, c, GB_USER, pieceCount);

      //Remove this piece from board
      board[PIECE(simRow,c)] = GB_EMPTY;
      pieceCount--;
      
      //Test if player won
      if (simWinStatus == WS_WIN)
      {
        return c;     //Return blocking column
      }
    }
  }

  //clear simulation tables
  for (int i = 0; i < SIMULATIONS; i++) 
  {
    simWins[i] = 0;
    simLosses[i] = 0;
  }

  simCol = 0;
  for (int i = 0; i < NUM_MCTS_RUNS; i++)
  {
    pieceCount = getCurrentDisplay(board);
    simPlayer = GB_CPU;
    // Make players first move
    simRow = addPieceToColumn(board, simCol, simPlayer);
    while (simRow == -1) 
    {
      simCol++;
      if (simCol == COLS) 
      {
        simCol = 0;
      }
      simRow = addPieceToColumn(board, simCol, simPlayer);
    }
    pieceCount++;
    if (testForWin(board, simRow, simCol, simPlayer, pieceCount) == WS_DRAW)
    {
      continue; //if draw, try next random move
    }
    
    // Loop through remaining moves until result
    simMoves = (ROWS * COLS) - pieceCount;
    while (true) 
    {
      // Swap  players
      simPlayer = (simPlayer == GB_CPU) ? GB_USER : GB_CPU;
      
      int8_t ranCol = random(0, COLS);
      int8_t ranRow = addPieceToColumn(board, ranCol, simPlayer);
      while (ranRow == -1) 
      {
        ranCol++;
        if (ranCol == COLS) 
        {
          ranCol = 0;
        }
        ranRow = addPieceToColumn(board, ranCol, simPlayer);
      }
      pieceCount++;
      simWinStatus = testForWin(board, ranRow, ranCol, simPlayer, pieceCount);
      if (simWinStatus == WS_WIN && simPlayer == player)
      { 
        // game resulted in win - record victory against this first played column
        simWins[simCol] += simMoves;
        // move to next random sample        
        break;
      }
      else if (simWinStatus == WS_WIN && simPlayer == simOpponent) 
      {
        // game resulted in loss - record defeat against this first played column
        simLosses[simCol] += simMoves;
        // move to next random sample
        break;
      }
      else if (simWinStatus == WS_DRAW) 
      { 
        // resulted in draw, move onto next sim
        break;
      }
      //Keep populating board
      simMoves--;
    }  //wend
    
    // Cycle through first-played cols
    simCol++;
    if (simCol == COLS) 
    {
      simCol = 0;
    }
  }
  
  // evaluate win/loss stats to determine best move
  simBestCol = 0;
  simBestScore = -49l * NUM_MCTS_RUNS;
  for (int c = 0; c < COLS; c++) 
  {
    simScore = simWins[c] - simLosses[c];
    if (simScore > simBestScore && displayGetPixel(0,c) == COLOR_CLEAR)
    {
      simBestCol = c;
      simBestScore = simScore;
    }
  }

  return simBestCol;
}

//-------------------------------------------------------------------------
// Copy current display to game array
//  Game board is a array of 48 bytes 6 row of 8 columns where coloum 8 is not used
//  gb - pointer to game board
//  returns count of pieces
//-------------------------------------------------------------------------

uint8_t getCurrentDisplay(GAME_BOARD gb[])
{
  uint32_t col;
  uint8_t count = 0;
  for (int r = 0; r < ROWS; r++)
  {
    for (int c = 0; c < COLS; c++)
    {
      col = displayGetPixel(((ROWS - 1) - r), c);
      gb[PIECE(r,c)] = (col == COLOR_USER) ? GB_USER : (col == COLOR_CPU) ? GB_CPU : GB_EMPTY;
      if (col != COLOR_CLEAR)
      {
        count++;
      }
    }
  }
  return count;
}

//-------------------------------------------------------------------------
// Copy current display to game array
//  Game board is a array of 48 bytes 6 row of 8 columns where coloum 8 is not used
//  gb - pointer to game board
//  returns count of pieces
//-------------------------------------------------------------------------

void transferToDisplay(GAME_BOARD gb[])
{
  GAME_BOARD player;
  for (int r = 0; r < ROWS; r++)
  {
    for (int c = 0; c < COLS; c++)
    {
      player = gb[PIECE(((ROWS - 1) - r),c)];
      displaySetPixel(r, c, (player == GB_USER) ? COLOR_USER : (player == GB_CPU) ? COLOR_CPU : COLOR_CLEAR);
    }
  }
}

//-------------------------------------------------------------------------
// Add a game piece to the board
//  gb - pointer to game board
//  col - column to add piece to (0 to 6)
//  player - GAME_BOARD constant
//  returns - row number where piece is added or -1 
//-------------------------------------------------------------------------

int addPieceToColumn(GAME_BOARD gb[], uint8_t col, GAME_BOARD player)
{
  int r = 0;
  while (r < ROWS && gb[PIECE(r, col)] != GB_EMPTY)
  {
      r++;
  };
  if (r < ROWS)
  {
      gb[PIECE(r, col)] = player;
      return r;
  }
  return -1;
}

//-------------------------------------------
// Test if 4 in a row made
//  gb - pointer to game board
//  row - 0 to 5 (0 = top, 5 = bottom) - piece dropped
//  col - 0 to 6 (0 = left, 6 = right) - piece dropped
//  player - GAME_BOARD constant
//  count - current number of moves
//  returns WIN_STATUS constant
//-------------------------------------------

WIN_STATUS testForWin(GAME_BOARD gb[], uint8_t row, uint8_t col, GAME_BOARD player, uint8_t count)
{
  WIN_STATUS result = WS_NONE;
  int8_t r, c, n;
  //Test vertical
  r = row;
  if (r > 0)
  {
    r--;
    while (r > 0 && gb[PIECE(r,col)] == player)
    {
      r--;
    }
    r = (gb[PIECE(r,col)] == player) ? r : r + 1;
  }
  n = 0;
  while (r < ROWS && n < 4 && gb[PIECE(r,col)] == player)
  {
    r++;
    n++;
  }
  if (n < 4)
  {
    //Test horizontal
    c = col;
    if (c > 0)
    {
      c--;
      while (c > 0 && gb[PIECE(row,c)] == player)
      {
        c--;
      }
      c = (gb[PIECE(row,c)] == player) ? c : c + 1;
    }
    n = 0;
    while (c < COLS && n < 4 && gb[PIECE(row,c)] == player)
    {
      c++;
      n++;
    }
    if (n < 4)
    {
      //Test diagonal top to bottom, left to right
      c = col;
      r = row;
      if (c > 0 && r > 0)
      {
        r--;
        c--;
        while (c > 0 && r > 0 && gb[PIECE(r,c)] == player)
        {
          c--;
          r--;
        }
        if (gb[PIECE(r,c)] != player)
        {
          c++;
          r++;
        }
      }
      n = 0;
      while (c < COLS && r < ROWS && n < 4 && gb[PIECE(r,c)] == player)
      {
        r++;
        c++;
        n++;
      }
      if (n < 4)
      {
        //Test diagonal bottom to top, left to right
        c = col;
        r = row;
        if (c > 0 && r < (ROWS - 1))
        {
          r++;
          c--;
          while (c > 0 && r < (ROWS - 1) && gb[PIECE(r,c)] == player)
          {
            r++;
            c--;
          }
          if (gb[PIECE(r,c)] != player)
          {
            r--;
            c++;
          }
        }
        n = 0;
        while (c < COLS && r >= 0 && n < 4 && gb[PIECE(r,c)] == player)
        {
          r--;
          c++;
          n++;
        }
      }
    }
  }

  if (n == 4)
  {
    result = WS_WIN;
  }
  else if (count == (ROWS * COLS))
  {
    result = WS_DRAW;
  }
  return result;
}

Display.h

C Header File
/*
Low Level Display Routines
Author: John Bradnam (jbrad2089@gmail.com)
Purpose: RGB Matrix display routines
*/
#pragma once

#include "Hardware.h"
#include "Font.h"
#include <tinyNeoPixel.h>

#define NUMLEDS 64
tinyNeoPixel leds = tinyNeoPixel(NUMLEDS, LEDS, NEO_GRB);

#define ANIMATE_DROP_DELAY 100
#define ANIMATE_WIN_DELAY 250
#define ANIMATE_WIN_COUNT 15

#define ROWS 6
#define COLS 7
#define COLSEL 57
#define PIXEL(r,c) (((c & 0x07) + 1) + ((r & 0x07) << 3))

#define COLOR_RED 0x003F0000
#define COLOR_GRN 0x00003F00
#define COLOR_BLU 0x0000003F
#define COLOR_BLK 0x00000000

#define SCROLL_SPEED 80               //Speed at which text is scrolled
String scrollText;                    //Used to store scrolling text
volatile int8_t scrollDelay;          //Used to store scroll delay
volatile uint8_t scrollCharPos;       //Current character position in text being scrolled
volatile int8_t scrollCharCol;        //Next column in character to display
volatile int8_t scrollOffScreen;      //Extra columns required to scroll last character off screen
volatile uint32_t scrollColor;        //String color
volatile uint8_t scrollAngle;         //Color angle

//-------------------------------------------------------------------------
//Forward references
void displaySetup();
void clearDisplay();
void displayRefresh();
void drawString(String s, uint32_t color);
void drawStringRepeat();
void scrollTextLeft();
void drawCharacter(int8_t x, char ch, uint32_t color);
void displaySetStatus(uint8_t c, uint32_t color);
uint32_t displayGetPixel(uint8_t row, uint8_t col);
void displaySetPixel(uint8_t row, uint8_t col, uint32_t color);
uint32_t Wheel(byte a, uint8_t brightness);

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

void displaySetup()
{
  leds.begin();
  leds.clear();
  leds.show();

  //Set up display refresh timer
  //CLK_PER = 3.3MHz (303nS)
  TCB1.CCMP = 49152;   //Refresh value for display (67Hz)
  TCB1.INTCTRL = TCB_CAPT_bm;
  TCB1.CTRLA = TCB_ENABLE_bm;
}

//-------------------------------------------------------------------------
//Timer B Interrupt handler interrupt each mS - output segments
ISR(TCB1_INT_vect)
{
  //Handle scrolling of text
  if (scrollDelay != 0)
  {
    scrollDelay--;
    if (scrollDelay == 0)
    {
      scrollDelay = SCROLL_SPEED;
      scrollTextLeft();
    }
  }

  //Clear interrupt flag
  TCB1.INTFLAGS |= TCB_CAPT_bm; //clear the interrupt flag(to reset TCB1.CNT)
}

//-------------------------------------------
// Clear all pixels on display
//-------------------------------------------

void clearDisplay()
{
  leds.clear();
}

//---------------------------------------------------------------
//Transfers the working buffer to the display buffer
void displayRefresh()
{
  leds.show();
}

//---------------------------------------------------------------
//Draw string
// s = String to display
// col = color to show
void drawString(String s, uint32_t color)
{
  if (scrollDelay == 0)
  {
    scrollText = s;
    scrollColor = color;
    scrollCharPos = 0;
    scrollCharCol = 0;
    scrollOffScreen = 0;
    scrollAngle = 0;
    scrollDelay = SCROLL_SPEED;     //Starts scrolling
  }
}

//---------------------------------------------------------------
//Repeat last string
void drawStringRepeat()
{
  if (scrollDelay == 0)
  {
    scrollCharPos = 0;
    scrollCharCol = 0;
    scrollOffScreen = 0;
    scrollDelay = SCROLL_SPEED;     //Starts scrolling
  }
}

//---------------------------------------------------------------
//Scroll text left
void scrollTextLeft()
{
  uint8_t bits;
  uint8_t mask;

  //Scroll screen buffer left
  for (int8_t c = 0; c < COLS; c++)
  {
    for (int8_t r = 0; r < ROWS; r++)
    {
      displaySetPixel(r, c, (c == (COLS-1)) ? COLOR_BLK : displayGetPixel(r, c + 1));
    }
  }

  //Fifth character column is blank for letter spacing
  if (scrollOffScreen == 0 && scrollCharCol < 4)
  {
    char ch = scrollText[scrollCharPos];
    if (ch >= 48 && ch <= 90)
    {
      //Get bits in the next column and output to buffer
      bits = pgm_read_byte(&font4x6[ch-48][scrollCharCol]);
      mask = 0x20;
      for(int8_t r = 0; r < ROWS; r++)
      {
        if (bits & mask)
        {
          displaySetPixel((ROWS-1) - r, (COLS-1), (scrollColor != 0) ? scrollColor : Wheel(scrollAngle, 64));
        }
        mask = mask >> 1;
      }
    }
  }

  if (scrollOffScreen > 0)
  {
    scrollOffScreen--;
    if (scrollOffScreen == 0)
    {
      //Stop scrolling
      scrollDelay = 0;
    }
  }
  else
  {
    scrollCharCol++;
    if (scrollCharCol == 5)
    {
      scrollCharCol = 0;
      scrollCharPos++;
      if (scrollCharPos == scrollText.length())
      {
        //All text has been outputted, just wait until it is scrolled of the screen
        scrollOffScreen = 8;
      }
    }
  }
  displayRefresh();
  scrollAngle = (scrollAngle + 4) & 0xFF;
}

//---------------------------------------------------------------
//Draw character
// x = column (0-7) 0 being farest left hand and 7 being farest right
// ch = ASCII character
// col = color to show
void drawCharacter(int8_t x, char ch, uint32_t color)
{
  uint8_t bits;
  uint8_t mask;
  if (ch >= 48 && ch <= 90)
  {
    ch = ch - 48;
    for(int c = 0; c < 6; c++)
    {
      int8_t pos = c + x;
      if (pos >= 0 && pos < COLS)
      {
        if (c == 5)
        {
          //Blank column for character spacing
          for(int r = 0; r < ROWS; r++)
          {
            leds.setPixelColor(PIXEL(((ROWS-1) - r), pos), COLOR_BLK);
          }
        }
        else
        {
          bits = pgm_read_byte(&font4x6[(int)ch][c]);
          mask = 0x20;
          for(int r = 0; r < ROWS; r++)
          {
            if (bits & mask)
            {
              leds.setPixelColor(PIXEL(((ROWS-1) - r), pos), color);
            }
            mask = mask >> 1;
          }
        }
      }
    }
  }
}

//-------------------------------------------
// Display Status LED
//  col - 0 to 7 (0 = left, 7 = right)
//  color - leds.Color(R, G, B)
//-------------------------------------------

void displaySetStatus(uint8_t c, uint32_t color)
{
  leds.setPixelColor(COLSEL + c, color);
  leds.show();
}

//-------------------------------------------
// Get a pixel on the matrix
//  row - 0 to 7 (0 = top, 7 = bottom)
//  col - 0 to 7 (0 = left, 7 = right)
//  color - leds.Color(R, G, B)
//-------------------------------------------

uint32_t displayGetPixel(uint8_t row, uint8_t col)
{
  return leds.getPixelColor(PIXEL(row, col));
}

//-------------------------------------------
// Set a pixel on the matrix
//  row - 0 to 7 (0 = top, 7 = bottom)
//  col - 0 to 7 (0 = left, 7 = right)
//  color - leds.Color(R, G, B)
//  Note: leds.show() needs to be executed to display the pixel
//-------------------------------------------

void displaySetPixel(uint8_t row, uint8_t col, uint32_t color)
{
  leds.setPixelColor(PIXEL(row, col), color);
}

//-------------------------------------------------------------------------
// Input a value 0 to 255 to get a color value.
// The colours are a transition r - g - b - back to r.
//  a = angle (0 to 255)
//  brightness - (0 to 255)
//-------------------------------------------------------------------------

uint32_t Wheel(byte a, uint8_t brightness) 
{
  uint32_t pos = a;
  uint32_t r, g, b;
  pos = 255 - pos;
  if(pos < 85) 
  {
    r = 255 - pos * 3;
    g = 0;
    b = pos * 3;
  }
  else if(pos < 170) 
  {
    pos -= 85;
    r = 0;
    g = pos * 3;
    b = 255 - pos * 3;
  }
  else
  {
    pos -= 170;
    r = pos * 3;
    g = 255 - pos * 3;
    b = 0;
  }
  if (brightness != 0)
  {
    r = (r * brightness) >> 8;
    g = (g * brightness) >> 8;
    b = (b * brightness) >> 8;
  }
  return (r << 16) | (g << 8) | b;
}

Font.h

C Header File
/*
4x6 font
Author: John Bradnam (jbrad2089@gmail.com)
Purpose: Font for scrolling text
*/
#pragma once

//Font
#define SPACE 16
const uint8_t font4x6 [43][4] PROGMEM = {
/*  
 *   00011110
 *   00100101
 *   00101001
 *   00011110
 */
  {0x1E, 0x25, 0x29, 0x1E}, // 0 
/*  
 *   00000000
 *   00100010
 *   00111111
 *   00100000
 */
  {0x00, 0x22, 0x3F, 0x20}, // 1
/*
 *   00110001
 *   00101101
 *   00101101
 *   00100010
 */
  {0x31, 0x2D, 0x2D, 0x22}, // 2 
/*  
 *   00100001
 *   00101001
 *   00101001
 *   00010110
 */
  {0x21, 0x29, 0x29, 0x16}, // 3 
/*  
 *   00011100
 *   00010010
 *   00111111
 *   00010000
 */
  {0x1C, 0x12, 0x3F, 0x10}, // 4 
/*  
 *   00101111 
 *   00101101
 *   00101101
 *   00010001
 */
  {0x2F, 0x2D, 0x2D, 0x11}, // 5 
/*  
 *   00011110
 *   00101001
 *   00101001
 *   00010000
 */
  {0x1E, 0x29, 0x29, 0x10}, // 6 
/*  
 *   00000001
 *   00111001
 *   00000101
 *   00000011
 */
  {0x01, 0x39, 0x05, 0x03}, // 7 
/*  
 *   00010010
 *   00101101
 *   00101101
 *   00010010
 */
  {0x12, 0x2D, 0x2D, 0x12}, // 8 
/*  
 *   00000010
 *   00100101
 *   00100101
 *   00011110
 */
  {0x02, 0x25, 0x25, 0x1E}, // 9 
/*  
 *   00000000
 *   00010100
 *   00000000
 *   00000000
 */
  {0x00, 0x14, 0x00, 0x00}, // : 
/*  
 *   00000000
 *   00110100
 *   00000000
 *   00000000
 */
  {0x00, 0x34, 0x00, 0x00}, // ; 
/*  
 *   00001000
 *   00010100
 *   00100010
 *   00000000
 */
  {0x08, 0x14, 0x22, 0x00}, // < 
/*  
 *   00001010
 *   00001010
 *   00001010
 *   00001010
 */
  {0x0A, 0x0A, 0x0A, 0x0A}, // = 
/*  
 *   00000000
 *   00100010
 *   00010100
 *   00001000
 */
  {0x00, 0x22, 0x14, 0x08}, // > 
/*  
 *   00000001
 *   00101101
 *   00001101
 *   00000010
 */
  {0x01, 0x2D, 0x0D, 0x02}, // ? 
  {0x00, 0x00, 0x00, 0x00}, // Space
/*
 *   00111110
 *   00010001
 *   00010001
 *   00111110
 */
  {0x3E, 0x11, 0x11, 0x3E}, // A 
/*  
 *   00111111
 *   00101101
 *   00101101
 *   00010010
 */
  {0x3F, 0x2D, 0x2D, 0x12}, // B 
/*  
 *   00011110
 *   00100001
 *   00100001
 *   00010010
 */
  {0x1E, 0x21, 0x21, 0x12}, // C 
/*  
 *   00111111
 *   00100001
 *   00100001
 *   00011110
 */
  {0x3F, 0x21, 0x21, 0x1E}, // D 
/*  
 *   00111111
 *   00101101
 *   00101101
 *   00101101
 */
  {0x3F, 0x2D, 0x2D, 0x2D}, // E
/*
 *   00111111
 *   00001101
 *   00001101
 *   00001101
 */
  {0x3F, 0x0D, 0x0D, 0x0D}, // F
/* 
 *   00011110
 *   00100001
 *   00101001
 *   00111001
 */
  {0x1E, 0x21, 0x29, 0x39}, // G 
/* 
 *   00111111
 *   00001100
 *   00001100
 *   00111111
 */
  {0x3F, 0x0C, 0x0C, 0x3F}, // H 
/* 
 *   00100001
 *   00111111
 *   00100001
 *   00000000
 */
  {0x21, 0x3F, 0x21, 0x00}, // I 
/* 
 *   00010000
 *   00100000
 *   00100001
 *   00011111
 */
  {0x10, 0x20, 0x21, 0x1F}, // J 
/* 
 *   00111111
 *   00001100
 *   00010010
 *   00100001
 */
  {0x3F, 0x0C, 0x12, 0x21}, // K 
/* 
 *   00111111
 *   00100000
 *   00100000
 *   00100000
 */
  {0x3F, 0x20, 0x20, 0x20}, // L 
/* 
 *   00111111
 *   00001110
 *   00001110
 *   00111111
 */
  {0x3F, 0x0E, 0x0E, 0x3F}, // M 
/* 
 *   00111111
 *   00011000
 *   00000110
 *   00111111
 */
  {0x3F, 0x18, 0x06, 0x3F}, // N 
/* 
 *   00011110
 *   00100001
 *   00100001
 *   00011110
 */
  {0x1E, 0x21, 0x21, 0x1E}, // O 
/* 
 *   00111111
 *   00001001
 *   00001001
 *   00000110
 */
  {0x3F, 0x09, 0x09, 0x06}, // P 
/* 
 *   00001110
 *   00010001
 *   00010001
 *   00101110
 */
  {0x0E, 0x11, 0x11, 0x2E}, // Q 
/* 
 *   00111111
 *   00010001
 *   00010001
 *   00101110
 */
  {0x3F, 0x11, 0x11, 0x2E}, // R 
/* 
 *   00100010
 *   00101101
 *   00101101
 *   00010001
 */
  {0x22, 0x2D, 0x2D, 0x11}, // S 
/* 
 *   00000001
 *   00111111
 *   00000001
 *   00000000
 */
  {0x01, 0x3F, 0x01, 0x00}, // T 
/* 
 *   00011111
 *   00100000
 *   00100000
 *   00011111
 */
  {0x1F, 0x20, 0x20, 0x1F}, // U 
/* 
 *   00011111
 *   00100000
 *   00011000
 *   00000111
 */
  {0x1F, 0x20, 0x18, 0x07}, // V
/* 
 *   00111111
 *   00011100
 *   00011100
 *   00111111
 */
  {0x3F, 0x1C, 0x1C, 0x3F}, // W 
/* 
 *   00110011
 *   00001100
 *   00001100
 *   00110011
 */
  {0x33, 0x0C, 0x0C, 0x33}, // X 
/* 
 *   00000011
 *   00100100
 *   00100100
 *   00011111
 */
  {0x03, 0x24, 0x24, 0x1F}, // Y 
/* 
 *   00110001
 *   00101001
 *   00100101
 *   00100011
 */
  {0x31, 0x29, 0x25, 0x23}  // Z 
};

Hardware.h

C Header File
/*
Low Level HardwareRoutines
Author: John Bradnam (jbrad2089@gmail.com)
Purpose: Sound & rotary encoder support
*/
#pragma once

#define LEDS 2    //PA6
#define SPEAKER 3 //PA7
#define ENC_A 8   //PA1
#define ENC_B 9   //PA2
#define ENC_S 10  //PA3

#define BUZZER_PORT PORTA.OUT
#define BUZZER_MASK PIN7_bm

int8_t volatile rotaryDirection = 0;
bool volatile lastRotA = false;
bool volatile rotarySwitchPressed = false;

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

void hardwareSetup();
void rotaryInterrupt();
void switchInterrupt();
void beepDigit(); 
void playValidTone();
void playInvalidTone();
void playWinSoundShort();

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

void hardwareSetup()
{
  pinMode(SPEAKER, OUTPUT);
  //Interrupt handers for rotary encoder
  pinMode(ENC_A, INPUT_PULLUP);
  pinMode(ENC_B, INPUT_PULLUP);
  pinMode(ENC_S, INPUT);
  attachInterrupt(ENC_A, rotaryInterrupt, CHANGE);
  attachInterrupt(ENC_S, switchInterrupt, CHANGE);
}

//---------------------------------------------------------------------
// Interrupt Handler: Rotary encoder has moved
//---------------------------------------------------------------------

void rotaryInterrupt()
{
  if (!digitalRead(ENC_A) && lastRotA)
  {
    rotaryDirection = (digitalRead(ENC_B)) ? 1 : -1;
  }
  lastRotA = digitalRead(ENC_A);
}

//---------------------------------------------------------------------
// Interrupt Handler: Rotary encoder shaft was pressed
//---------------------------------------------------------------------

void switchInterrupt()
{
  //Record when pressed
  rotarySwitchPressed = (digitalRead(ENC_S) == LOW);
}

//------------------------------------------------------------------
// Turn on and off buzzer quickly
//------------------------------------------------------------------

void beepDigit() 
{
  BUZZER_PORT |= BUZZER_MASK;   // turn on buzzer
  delay(5);
  BUZZER_PORT &= ~BUZZER_MASK;  // turn off the buzzer
}

//------------------------------------------------------------------
// Tone to play on a valid move
//------------------------------------------------------------------

void playValidTone()
{
  TimerFreeTone(SPEAKER, 300, 150); 
}

//------------------------------------------------------------------
// Tone to play on a invalid move
//------------------------------------------------------------------

void playInvalidTone()
{
  TimerFreeTone(SPEAKER, 50, 150); 
}


//------------------------------------------------------------------
// Play the winning sound
//------------------------------------------------------------------

void playWinSoundShort()
{
  //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);
}

Credits

John Bradnam

John Bradnam

141 projects • 167 followers
Thanks to mit-mit and smily77.

Comments