/**************************************************************************
Space Impact Game
Author: John Bradnam (jbrad2089@gmail.com)
Modified code from Space Impact LCD game by MOHD SOHAIL
(https://www.youtube.com/channel/UCaXI2PcsTlH5g0et67kdD6g)
(https://www.hackster.io/mohammadsohail0008/space-impact-lcd-game-ce5c74)
2021-07-21
- Create program for ATtiny1614
- Replaced joystick with X-Pad
- Made fire and movement processing interrupt driven
- Made LCD backlight and power enabled via code
- Added processor sleep mode for battery powered systems
- Added more sounds
- Added EEPROM functions to store high score
--------------------------------------------------------------------------
Arduino IDE:
--------------------------------------------------------------------------
BOARD: ATtiny1614/1604/814/804/414/404/214/204
Chip: ATtiny1614
Clock Speed: 8MHz (if using external power) or 1MHz (if using a battery)
millis()/micros() Timer: "TCD0 (1-series only, default there)
Programmer: jtag2updi (megaTinyCore)
ATtiny1614 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 <avr/pgmspace.h>
#include <avr/sleep.h>
#include <avr/power.h>
#include <LiquidCrystal.h>
#include <TimerFreeTone.h> // https://bitbucket.org/teckel12/arduino-timer-free-tone/wiki/Home
#include <EEPROM.h>
#include "button.h"
//LCD Screen
#define LCD_RS 0 //PA4
#define LCD_EN 1 //PA5
#define LCD_D4 2 //PA6
#define LCD_D5 3 //PA7
#define LCD_D6 4 //PB3
#define LCD_D7 5 //PB2
//Switches
#define SW_FIRE 6 //PB1
#define SW_XPAD 10 //PA3
//Other
#define SPEAKER 7 //PB0
#define LIGHT 8 //PA1
#define POWER 9 //PA2
//Initialize the LCD
LiquidCrystal lcd(LCD_RS, LCD_EN, LCD_D4, LCD_D5, LCD_D6, LCD_D7);
//Buttons
enum buttonEnum { SW_NONE, SW_LEFT, SW_RIGHT, SW_DOWN, SW_UP };
Button* leftButton;
Button* rightButton;
Button* downButton;
Button* upButton;
Button* fireButton;
//EEPROM handling
#define EEPROM_ADDRESS 0
#define EEPROM_MAGIC 0x0BAD0DAD
typedef struct {
uint32_t magic;
uint32_t highScore;
} EEPROM_DATA;
EEPROM_DATA EepromData; //Current EEPROM settings
//Logical screen
volatile uint8_t area[4][15] =
{
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
};
#define CUSTOM_CHARACTERS 8
const byte cc[CUSTOM_CHARACTERS][8] =
{
{B11100, B01111, B11100, B00000, B00000, B00000, B00000, B00000},
{B00000, B00000, B00000, B00000, B11100, B01111, B11100, B00000},
{B11100, B01111, B11100, B00000, B11100, B10100, B11100, B00000},
{B11100, B10100, B11100, B00000, B11100, B01111, B11100, B00000},
{B11100, B10100, B11100, B00000, B11100, B10100, B11100, B00000},
{B00000, B00000, B00000, B00000, B00100, B10010, B01000, B00000},
{B00100, B10010, B01000, B00000, B11100, B10100, B11100, B00000},
{B11100, B10100, B11100, B00000, B00100, B10010, B01000, B00000}
};
#define SLEEP_TIMEOUT 15000
unsigned long sleepTimeOut;
#if (F_CPU == 1000000L)
//A 1MHz clock uses less battery power when running
#define MAX_GAME_DELAY 50
#define MIN_GAME_DELAY 10
#define STEP_GAME_DELAY 5
#else
//Assume 8MHz if running via external power
#define MAX_GAME_DELAY 200
#define MIN_GAME_DELAY 50
#define STEP_GAME_DELAY 5
#endif
volatile uint8_t fireLoad = 0;
volatile uint8_t fireConsumption = 0;
volatile bool xPadButtonDown = false;
volatile bool gameover = true;
//-------------------------------------------------------------------------
// Initialise Hardware
void setup()
{
pinMode(POWER, OUTPUT);
digitalWrite(POWER, HIGH);
pinMode(LIGHT, OUTPUT);
digitalWrite(LIGHT, HIGH);
pinMode(SPEAKER, OUTPUT);
//Initialise buttons
leftButton = new Button(SW_LEFT, SW_XPAD, 250, 399, false); //22K/10K = 320
rightButton = new Button(SW_RIGHT, SW_XPAD, 520, 700, false); //22K/30K = 590
downButton = new Button(SW_DOWN, SW_XPAD, 400, 519, false); //22K/20K = 487
upButton = new Button(SW_UP, SW_XPAD, 0, 100, false); //GND = 0
fireButton = new Button(SW_FIRE);
attachInterrupt(SW_FIRE, fireButtonInterrupt, CHANGE); //Used to wake up processor and to fire bullet
//Set up background player control
TCB1.CCMP = 10000;
TCB1.INTCTRL = TCB_CAPT_bm;
TCB1.CTRLA = TCB_ENABLE_bm;
//Get last high score
readEepromData();
// set up the LCD's number of columns and rows:
lcd.begin(16, 2);
//Define custom characters
for (int i = 0; i < CUSTOM_CHARACTERS; i++)
{
lcd.createChar(i, &cc[i][0]);
}
lcd.home();
}
//--------------------------------------------------------------------
// Handle pin change interrupt when SW_FIRE is pressed
void fireButtonInterrupt()
{
if (!gameover && fireButton->State() == LOW && fireLoad >= fireConsumption) {
fireLoad -= fireConsumption;
for (uint8_t y=0; y<4; y++)
{
for (uint8_t x=0; x<14; x++)
{
if (area[y][x]==1) // spaceship
{
area[y][x+1] += 4;
}
}
}
}
}
//-------------------------------------------------------------------------
//Timer B Interrupt handler interrupt each mS - output segments
ISR(TCB1_INT_vect)
{
if (xPadButtonDown)
{
xPadButtonDown = (leftButton->State() == HIGH || rightButton->State() == HIGH || upButton->State() == HIGH || downButton->State() == HIGH);
}
else if (!gameover)
{
bool doBreak = false;
for (uint8_t y=0; y<4; y++)
{
for (uint8_t x=0; x<15; x++)
{
if (area[y][x]==1)
{
doBreak = true;
if (leftButton->State() == HIGH && x > 0)
{
area[y][x] = 0;
area[y][x-1] += 1;
xPadButtonDown = true;
}
else if (rightButton->State() == HIGH && x < 14)
{
if (area[y][x+1]!=4)
{
area[y][x] = 0;
area[y][x+1] += 1;
xPadButtonDown = true;
}
}
else if (upButton->State() == HIGH && y > 0)
{
area[y][x] = 0;
area[y-1][x] += 1;
xPadButtonDown = true;
}
else if (downButton->State() == HIGH && y < 3)
{
area[y][x] = 0;
area[y+1][x] += 1;
xPadButtonDown = true;
}
}
if (doBreak)
{
break;
}
}
if (doBreak)
{
break;
}
}
}
//Clear interrupt flag
TCB1.INTFLAGS |= TCB_CAPT_bm; //clear the interrupt flag(to reset TCB1.CNT)
}
//-------------------------------------------------------------------------
// Main program loop
void loop()
{
displayInitialScreens();
sleepTimeOut = millis() + SLEEP_TIMEOUT;
while(!fireButton->Pressed())
{
if (millis() > sleepTimeOut)
{
Sleep();
displayInitialScreens();
sleepTimeOut = millis() + SLEEP_TIMEOUT;
while (fireButton->State() == LOW); //Wait until fire button is released
}
};
//Setup the game
lcd.clear();
delay(500);
playStartRound();
//Clear playing field
for (uint8_t y=0; y<4; y++)
{
for (uint8_t x=0; x<15; x++)
{
area[y][x] = 0;
}
}
area[0][0] = 1; //Put ship in top left corner
uint8_t sleep = MAX_GAME_DELAY;
uint8_t junkRisk = 10;
fireLoad = 0;
fireConsumption = 9;
unsigned long score = 0;
uint8_t life = 3;
unsigned long count = 0;
lcd.setCursor(0,0);
lcd.print(life);
lcd.setCursor(0,1);
lcd.print(fireLoad);
draw();
gameover = false;
while (!gameover)
{
for (uint8_t y=0; y<4; y++)
{
for (uint8_t x=15; x>0; x--)
{
if (area[y][x-1]==4)
{
area[y][x-1] = 0;
if (x<15)
{
area[y][x] += 4;
}
}
}
}
if (fireLoad<9)
{
fireLoad++;
lcd.setCursor(0,1);
lcd.print(fireLoad);
}
draw();
delay(sleep);
for (uint8_t y=0; y<4; y++)
{
for (uint8_t x=0; x<15; x++)
{
if (area[y][x]==2)
{
area[y][x] = 0;
if (x>0)
{
area[y][x-1] += 2;
}
}
}
}
for (uint8_t y=0; y<4; y++) {
if (random(100) < junkRisk) {
area[y][14] += 2;
}
}
draw();
delay(sleep);
for (uint8_t y=0; y<4; y++)
{
for (uint8_t x=0; x<15; x++)
{
if (area[y][x]==3) // collided ship
{
area[y][x] = 1;
blinkShip();
life--;
lcd.setCursor(0, 0);
lcd.print(life);
if(life==0)
{
gameover = true;
lcd.clear();
lcd.setCursor(6, 0);
lcd.print("GAME");
lcd.setCursor(6, 1);
lcd.print("OVER");
playLoseSound();
for (uint8_t y=0; y<2; y++)
{
for (uint8_t x=0; x<16; x++)
{
lcd.setCursor(x, y);
lcd.print(char(1));
delay(100);
lcd.setCursor(x, y);
lcd.print(" ");
}
}
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("SCORE:");
lcd.setCursor(7, 0);
lcd.print(score);
lcd.setCursor(0, 1);
lcd.print("BEST:");
lcd.setCursor(7, 1);
lcd.print(EepromData.highScore);
if (score > EepromData.highScore)
{
EepromData.highScore = score;
writeEepromData();
playWinSound();
}
delay(1000);
for (uint8_t y=0; y<2; y++)
{
for (uint8_t x=0; x<16; x++)
{
lcd.setCursor(x, y);
lcd.print(char(1));
delay(100);
lcd.setCursor(x, y);
lcd.print(" ");
}
}
delay(1000);
}
}
else if (area[y][x]>4)
{
for (uint8_t i=0; i<10; i++)
{
digitalWrite(SPEAKER,HIGH);
delay(3);
digitalWrite(SPEAKER,LOW);
delay(3);
}
score+=10;
area[y][x] -= 6;
}
}
}
score++;
count++;
if (count % 100 == 0)
{
sleep = max(MIN_GAME_DELAY, sleep - STEP_GAME_DELAY);
junkRisk +=3;
fireConsumption--;
}
}
}
//-------------------------------------------------------------------------
// Transfer the logical screen to the physical screen
void draw()
{
for (uint8_t y=0; y<4; y+=2)
{
for (uint8_t x=0; x<15; x++)
{
lcd.setCursor(x+1, y/2);
if (area[y][x]==1)
{
if (area[y+1][x]==0)
{
lcd.print(char(0));
}
else if (area[y+1][x]==2 || area[y+1][x]==6 || area[y+1][x]==10) // down obstacle
{
lcd.print(char(2));
}
} else if (area[y][x]==2 || area[y][x]==6 || area[y][x]==10)
{
if (area[y+1][x]==0)
{
lcd.write(0b11011111); //upper Small box
}
else if (area[y+1][x]==1)
{
lcd.print(char(3));
}
else if (area[y+1][x]==2)
{
lcd.print(char(4));
}
else if (area[y+1][x]==4 || area[y+1][x]==6 || area[y+1][x]==10)
{
lcd.print(char(7));
}
}
else if (area[y][x]==4)
{
if (area[y+1][x]==0)
{
lcd.write(0b11011110); //bullet
}
else if (area[y+1][x]==2 || area[y+1][x]==6 || area[y+1][x]==10)
{
lcd.print(char(6)); //bullet + junk
}
}
else if (area[y][x]==0) // above nothing
{
if (area[y+1][x]==0) // nothing below
{
lcd.print(" ");
}
else if (area[y+1][x]==1) // below ship
{
lcd.print(char(1));
}
else if (area[y+1][x]==2 || area[y+1][x]==6 || area[y+1][x]==10)
{
lcd.write(0b10100001); //lower Small box
}
else if (area[y+1][x]==4)
{
lcd.print(char(5));
}
} else {
lcd.print(" ");
}
}
}
}
//-------------------------------------------------------------------------
// Flash the ship
void blinkShip()
{
for (uint8_t y=0; y<4; y++)
{
for (uint8_t x=0; x<15; x++)
{
if (area[y][x]==1) //Found ship
{
for (uint8_t i=0; i<3; i++)
{
lcd.setCursor(x+1, y>1);
if (y==0 || y==2)
{
if (area[y+1][x]==0)
{
lcd.print(" ");
}
else
{
lcd.write(0b10100001);
}
}
else
{
if (area[y-1][x]==0) {
lcd.print(" ");
}
else
{
lcd.write(0b11011111);
}
}
for (uint8_t i=0; i<10; i++)
{
digitalWrite(SPEAKER,HIGH);
delay(25);
digitalWrite(SPEAKER,LOW);
delay(5);
}
lcd.setCursor(x+1, y>1);
if (y==0 || y==2)
{
if (area[y+1][x]==0)
{
lcd.print(char(0));
}
else
{
lcd.print(char(2));
}
}
else
{
if (area[y-1][x]==0)
{
lcd.print(char(1));
}
else
{
lcd.print(char(3));
}
}
for (uint8_t i=0; i<10; i++)
{
digitalWrite(SPEAKER,HIGH);
delay(25);
digitalWrite(SPEAKER,LOW);
delay(5);
}
}
}
}
}
}
//--------------------------------------------------------------------
// Display Opening animation, instructions and "Press button to start" message
void displayInitialScreens()
{
lcd.clear();
for (uint8_t y=0; y<2; y++)
{
for (uint8_t x=0; x<16; x++)
{
lcd.setCursor(x, y);
lcd.print(char(1));
delay(100);
lcd.setCursor(x, y);
if (y==0)
{
lcd.print(" SPACE "[x]);
}
else
{
lcd.print(" IMPACT "[x]);
}
}
}
delay(1000);
lcd.clear();
lcd.setCursor(0,0);
lcd.print("3 -> LIFE POINTS");
lcd.setCursor(0,1);
lcd.print("9 -> WEAPON LOAD");
delay(3000);
lcd.clear();
lcd.setCursor(0,0);
lcd.print(" PRESS BUTTON ");
lcd.setCursor(0,1);
lcd.print(" TO START ");
}
//--------------------------------------------------------------------
// Set all LCD pins LOW or HIGH
void setLcdPins(int state)
{
static char Outputs[] = {LCD_RS, LCD_EN, LCD_D4, LCD_D5, LCD_D6, LCD_D7};
for (int i=0; i<6; i++)
{
pinMode(Outputs[i], state);
}
}
//--------------------------------------------------------------------
// Put the processor to sleep
void Sleep()
{
TCB1.CTRLA = TCB1.CTRLA & ~TCB_ENABLE_bm; //Switch off background timer
setLcdPins(INPUT);
digitalWrite(LIGHT, LOW);
digitalWrite(POWER, LOW);
set_sleep_mode(SLEEP_MODE_PWR_DOWN); // sleep mode is set here
sleep_enable();
sleep_mode(); // System actually sleeps here
sleep_disable(); // System continues execution here when watchdog timed out
// Continue after sleep
pinMode(POWER, OUTPUT);
digitalWrite(POWER, HIGH);
pinMode(LIGHT, OUTPUT);
digitalWrite(LIGHT, HIGH);
setLcdPins(OUTPUT);
lcd.begin(16, 2);
TCB1.CTRLA = TCB1.CTRLA | TCB_ENABLE_bm; //Switch on background timer
}
//-----------------------------------------------------------------------------------
//Play the sound for the start of a round
void playStartRound()
{
#define MAX_NOTE 4978 // Maximum high tone in hertz. Used for siren.
#define MIN_NOTE 31 // Minimum low tone in hertz. Used for siren.
for (int note = MIN_NOTE; note <= MAX_NOTE; note += 5)
{
TimerFreeTone(SPEAKER, note, 1);
}
}
//------------------------------------------------------------------
//Play a high note as a sign you lost
void playWinSound()
{
//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 playLoseSound()
{
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);
}
//---------------------------------------------------------------
//Write the EepromData structure to EEPROM
void writeEepromData()
{
//This function uses EEPROM.update() to perform the write, so does not rewrites the value if it didn't change.
EEPROM.put(EEPROM_ADDRESS,EepromData);
}
//---------------------------------------------------------------
//Read the EepromData structure from EEPROM, initialise if necessary
void readEepromData()
{
//Eprom
EEPROM.get(EEPROM_ADDRESS,EepromData);
if (EepromData.magic != EEPROM_MAGIC)
{
EepromData.magic = EEPROM_MAGIC;
EepromData.highScore =0;
writeEepromData();
}
}
Comments