Hardware components | ||||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
Software apps and online services | ||||||
| ||||||
Hand tools and fabrication machines | ||||||
| ||||||
|
A popular game back in the 1970's was Connect 4 by Milton Bradley / Hasbro.
It was a simple game played by two players. Each player takes turns to put their colored tile into one of the seven columns. The object of the game is to be the first player to have four of your colored tiles in a row either vertically, horizontally or diagonally.
This build emulates the game except that the other player is now a micro-controller.
DemonstrationDesignThe game is designed around a 8x8 WS2812B LED Matrix panel. Since the Connect Four "game board" is made up of 7 columns by 6 rows, one of the extra rows is used to highlight the column you want to "drop" your tile into. A 3D printed bezel hides the unused LEDs.
Column selection is made using a rotary encoder. Pressing the rotary encoder will "drop" your colored tile in the selected column.
The software uses the Monte Carlo Tree Search algorithm to determine its move. This provides a good challenge for the player.
The "brains" is a ATtiny3224 microprocessor. The rotary encoder contacts are hardware debounced using a simple RC network and Schmitt trigger.
Power is feed through a DC-DC Buck Step Down Module 3A Adjustable Voltage Regulator Power set to 5.0V.
3D printing was done using a 0.2mm layer height. Supports touching the build plate should be used on "Four - Holder.stl".
The case incorporates the rotary switch holder and knob designed by smily77 for their Alarm Clock - Simplicity project.
All the electronics are mounted on a PCB. As the ATtiny3224 only comes in a SMD package, SMD devices have been used for the resistors and small capacitors. The Eagle files have been included should you wish to get the board commercially made or you can make it yourself. I used the Toner method to make mine.
Start by adding the SMD components. I find it easier to use solder paste rather than use solder from a reel when soldering SMD components. I used my SMD Hot Plate to reflow the solder paste.
Use right-angle male pin headers for the connections for the rotary encoder and LED Matrix. (This is optional - you can solder the wires directly onto the board if you don't want to use headers).
Use a right-angle male for the UPDI programmer connection. (see programming the microprocessor further on in this text).
Use 4 x straight male single pin headers to solder the Buck Regulator to PCB.
The 1000uF/16V capacitor can be laid on its side if it is too high to fit in the case.
Add the buzzer. If your buzzer has a smaller hole spacing than what the board was designed for, you may have to drill a closer 1 mm hole on the ground side.
Mount the 20mm D-shaft EC12 rotary encoder on to "Four - Holder.stl".
Next solder three wires to the LED Matrix panel and sit it in "Four - Holder.stl". Ensure the matrix panel is orientated so that the wiring comes out in the top-left corner.
Use super glue to glue on "Four - Bezel.stl" to "Four - Holder.stl".
Wire up the LED Matrix and Rotary encoder to the PCB.
Screw on the PCB using 4 x 4mm M2 screws.
The ATtiny3224 is part of the new breed of ATtiny microprocessors. Unlike the earlier series such as the ATtiny85, the new breed use the RESET pin to program the CPU. To program it you need a UPDI programmer. I made one using a Arduino Nano. You can find complete build instructions at Create Your Own UPDI Programmer. It also contains the instructions for adding the megaTinyCore boards to your IDE.
Once the board has been installed in the IDE, select it from the Tools menu.
Select the ATtiny3224 board in your IDE.
Select BOARD: ATtiny3224/1624/1614/1604/824/814/804/424/414/404/...
Select Chip: ATtiny3224
Select millis()/micros(): "TCA0 (default on 0-series)"
Select Programmer: jtag2updi (megaTinyCore)
Open the sketch and upload it to the ATtiny3224.
Wire up the DC power socket and close the case.
As a game it provides a really good opponent and it is hard to beat. I had built Alarm Clock - Simplicity around 6 years ago and really liked the rotary encoder design. However when incorporated in this project, I found it doesn't really add anything to the game play that three buttons could of done just as well if not better. The case could of been half as high if buttons were used instead. Still it was a interesting and satisfying build.
/*
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 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;
}
/*
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;
}
/*
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
};
/*
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);
}
Comments