Introduction: Tiny Game Console

About: I like to learn, like to make, like to share.

Tiny game consoles are awesome. You can add them to your keychain and carry it all the time with you. You will find some designs on the Internet but all of them have disadvantages. I found one nice design here but the position of the buttons is not perfect. From my experience, I think placing buttons just under the display can give more comfort on playing for such a tiny console instead of placing them on the bottom-most position of the console like the mentioned one. From that thinking, I made one placing the buttons in the right place. I hope you will enjoy it more. In the next few steps, I will show you how I made it. By following the steps, you can easily produce a replica of this console. All the supporting design files are added with the appropriate step.

Before starting your work watch the demo video below.

Step 1: Bill of Materials

Hardware Components

Tools

Step 2: Schematic Diagram

The main part of the console is an ATtiny85 Microcontroller and an OLED display.

The ATtiny85 is a microcontroller in a similar vein to the Arduino, but with much less IO pins, smaller memory and a smaller form factor. In fact, when we talk about the ATtiny85 we refer to the IC itself rather than the board. The ATtiny85 can be used as a bare chip on a breadboard, as long as you can supply the correct power for the device.

This is a high-performance, low-power Microchip 8-bit AVR RISC-based microcontroller combines 8KB ISP flash memory, 512B EEPROM, 512-Byte SRAM, 6 general-purpose I/O lines, 32 general purpose working registers, one 8-bit timer/counter with compare modes, one 8-bit high-speed timer/counter, USI, internal and external Interrupts, 4-channel 10-bit A/D converter, programmable watchdog timer with internal oscillator, three software selectable power saving modes, and debugWIRE for on-chip debugging. The device achieves a throughput of 20 MIPS at 20 MHz and operates between 2.7-5.5 volts.

So, ATtiny85 can be easily powered up from 3V coin cell. For getting started with ATtiny85 you can read this nice guide: https://www.instructables.com/id/How-to-Program-an-Attiny85-From-an-Arduino-Uno/

I2C OLED Display: The organic light-emitting diode (OLED) display that we’ll use for the console is the SSD1306 model: a monocolor, 0.96-inch display with 128×64 pixels. The OLED display doesn’t require any backlight, which results in a very nice contrast in dark environments. Additionally, its pixels consume energy only when they are on, so the OLED display consumes less power when compared with other displays.

The model we’re using here has only four pins and communicates with the Arduino using I2C communication protocol. There are models that come with an extra RESET pin. There are also other OLED displays that communicate using SPI communication.

Two tactile buttons is used for playing the game and a buzzer is used for generating sound. All the connections are shown in the figure. Connect them accordingly.

Step 3: Placing and Soldering

I took a 2 inch x 1.1 inch perf board for assembling ll the components. I cut it from a big PCB board. SMD package for ATtiny85 was used for the game console. You can not solder an SMD package directly in the PCB board. For doing that either you can use a DIP package or you can use an SMD to DIP converter PCB. I used a DIP converter PCB for this purpose. It is soldered at the bottom center of the PCB board. To button were soldered on two sides of the controller. To pull-down resistors were soldered from switch to ground and placed on the bellow of the switches.

Step 4: Soldering the Buzzer

The buzzer is for generating tone while playing the game. Due to space limitations, I will place it under the OLED display. One important thing is you must keep some gap between the buzzer and the OLED. Otherwise, it prevents from vibrate and producing sound.

A piezo electric element is a crystal or ceramic that deforms slightly when a voltage is applied to it. So if you supply an AC voltage at a few kilohertz, it deforms back and forth at the same speed as the AC signal, and produces an audible sound.
The same effect works in reverse. If you deform a piezo, it generates a voltage. This was the principle of “crystal” microphones and gramophone pickups, probably before your time. It’s also the principle of gas igniters - when you press the button a spring loaded hammer snaps against a piezo, and the sudden deformation generates thousands of volts which cause a spark. Quartz is a piezo crystal, and it exhibits both these effects. If you grind it very thin it can have a resonant frequency in the megahertz and be made part of an oscillator. An electronic driver applies a voltage that deforms it, then when the voltage is removed the crystal deforms back and overshoots, generating a voltage that is fed back to the oscillator and keeps it going at the resonant frequency. You probably have about six of these in your phone.

Step 5: Soldering OLED & Battery Holder

The OLED used for this project is I2C. An I2C OLED display has 4 pins to connect with the circuit. Two pins for data communication and the other two pins are for power supply. An OLED display was soldered to the IC through jumper wires. On the bottom side of the OLED display, the CR2032 coin cell holder was soldered. The game console will be powered from a coin cell battery.

Both ATtiny85 and OLED display is very power efficient. So, one coin cell can provide power for more than an hour for continuous playing.

After completing all the connection you should upload the sketch in the console. There are lots of games for this type of console are available on the internet. One such repository is here.

The code used for the demo is below:

/*  2015 / 2016 / 2017
 *  Frogger game by Andy Jackson - Twitter @andyhighnumber
 *  
 *  Special thanks to @senkunmusahi, who created the artwork bitmaps in the game using  https://www.riyas.org/2013/12/online-led-matrix-f...
 *  
 *  Inspired by  https://www.riyas.org/2013/12/online-led-matrix-f... and includes some code from the #AttinyArcade games on that site
 *  The code that does not fall under the licenses of sources listed below can be used non commercially with attribution.
 *  This software is supplied without warranty of any kind.
 *  
 *  Controls:
 *  On the standard AttinyArcade:
 *  LEFT and RIGHT buttons move the frog across 
 *  BOTH BOTTONS TOGETHER move the frog forwards
 *  
 *  HIGHLY RECOMMENDED:
 *  On custom hardware (see schematic in folder where you found this file) there is an additional button to move frog forward  
 *  
 *  Also, from standby....
 *  Press and hold left button to turn sound on and off
 *  Press and hold left button with the right button held to reset high score
 * 
 *  This sketch is using the screen control and font functions written by Neven Boyanov for the  https://www.riyas.org/2013/12/online-led-matrix-f... project
 *  Source code and font files available at:  https://www.riyas.org/2013/12/online-led-matrix-f...
 *  **Note that this highly size-optimised version requires modified library functions (which are in this source code file) 
 *  and a modified font header
 * 
 *  Sleep code is based on this blog post by Matthew Little:
 *   https://www.riyas.org/2013/12/online-led-matrix-f...
*/
#include <EEPROM.h>
#include "font6x8AJ2.h"
#include <avr/pgmspace.h>
#include <avr/sleep.h>
#include <avr/interrupt.h> // needed for the additional interrupt

// Uncomment this #define to make the logs smaller (/thinner)
//#define SMALLLOGS

// Make click delay an even number - it gets halved and then used in an integer comparison
#define CLICKDELAY 120 

// The basline speed - higher number is slower
#define MOVEBASE 1000 

#define DIGITAL_WRITE_HIGH(PORT) PORTB |= (1 << PORT)
#define DIGITAL_WRITE_LOW(PORT) PORTB &= ~(1 << PORT)

// Routines to set and clear bits (used in the sleep code)
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))

// Defines for OLED output
#define SSD1306XLED_H
#define SSD1306_SCL   PORTB4  // SCL, Pin 4 on SSD1306 Board - for webbogles board
#define SSD1306_SDA   PORTB3  // SDA, Pin 3 on SSD1306 Board - for webbogles board
#define SSD1306_SA    0x78  // Slave address

// Function prototypes

// Drawing functions - adapted from those at  https://www.riyas.org/2013/12/online-led-matrix-f...
void ssd1306_init(void);
void ssd1306_xfer_start(void);
void ssd1306_xfer_stop(void);
void ssd1306_send_byte(uint8_t byte);
void ssd1306_send_command(uint8_t command);
void ssd1306_send_data_start(void);
void ssd1306_send_data_stop(void);
void ssd1306_setpos(uint8_t x, uint8_t y);
void ssd1306_fillscreen(uint8_t fill_Data);
void ssd1306_char_f6x8(uint8_t x, uint8_t y, const char ch[]);
void ssd1306_draw_bmp(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, uint8_t bitmap[]);

// Custom draw functions - allow for extra functionality like inverse display
void sendBlock(byte, bool);
void sendByte(byte, bool);

// Other generic functions for games (both originated in code from webboggles.com and the sleep code is by Matthew Little - see above)
void beep(int,int);
void system_sleep(void);
void doNumber (int,int,int);

// Game functions
void playFrogger(void);
void levelUp(int);
void moveBlocks(void);
void initScreen(void);
void drawDocks(void);
void drawLives(void);
void displayTitle(void);
void resetDock(byte);
void checkCollision(void);

// Global variables - yes I know all these global vars is a lazy way to code but it makes it easier to prevent stack overflows when you're working with 512 bytes! 
// Most of these are initialised in the main game function (playFrogger())
int watchDog;             // Counts drawing cycles so I can shut the game down if there's inactivity - battery saver!
boolean stopAnimate;      // this is set to 1 when a collision is detected
int lives;                // Lives in the game - this can go negative to end the game, which is why it's a signed variable  
bool frogDocks[5];        // Tracks which frog docks are full (at the top of the screen)
bool flipFlop;            // Used in routines that flip-flop between two states (left and right)
bool flipFlopShift;       // Same as previous one
byte frogColumn;          // Column location of frog (there are 16 altogether)
byte frogRow;             // Row locaiton of frog (there are 8, but 0 is the frog docks at the top and 7 is the start row)
byte frogLeftLimit;       // Left limit of frog travel on start row (changes as digits in score increases)
byte frogRightLimit;      // Right limit of frog travel on start row (changes as lives decrease as there's then more space)
byte level;               // Level - starts at 1
byte blockShiftL;         // Number of pixels to shift the left-going rows by
byte blockShiftR;         // Number of pixels to shift the right-going rows by
int interimStep;          // Used as timer for incremental movements
int moveDelay;            // How long to wait until the next movement of logs etc - changes as levels increase to make the game go faster
int dockedFrogs;          // How many frogs are in the docks at the top
unsigned long clickBase;  // Timer for debounce
boolean clickLock;        // For debounce routine
int score;                // Obvious I hope
int topScore;             // High score
boolean newHigh;          // Is there a new high score?
boolean mute = 0;         // Mute the speaker
byte grid[6][16];         // Grid for items like logs, crocs, cars and lorries
byte frogMode;            // Represents the frog direction 
bool moveForward=0;       // Captures when the 'forward' button is pressed
bool moveLeft=0;          // Captures when the 'left' button is pressed
bool moveRight=0;         // Captures when the 'right' button is pressed

// Bitmaps created by @senkunmusahi using  https://www.riyas.org/2013/12/online-led-matrix-f...
static const byte  bitmaps[15][8] PROGMEM = {
// Frogs
  {0x83, 0xDC, 0x7A, 0x3F, 0x3F, 0x7A, 0xDC, 0x83},
  {0x99, 0xBD, 0xDB, 0x7E, 0x7E, 0x3C, 0xE7, 0x81},
  {0x81, 0xE7, 0x3C, 0x7E, 0x7E, 0xDB, 0xBD, 0x99},

#ifdef SMALLLOGS
// Small logs
  {0x1C, 0x22, 0x41, 0x55, 0x55, 0x51, 0x43, 0x61},
  {0x69, 0x6B, 0x43, 0x61, 0x45, 0x45, 0x61, 0x65},
  {0x45, 0x55, 0x41, 0x5D, 0x63, 0x5D, 0x22, 0x1C},
#else
// Bigger logs
  {0x3C, 0x7E, 0xD7, 0xB5, 0xAD, 0xBF, 0xFF, 0xED}, 
  {0xAD, 0xAD, 0xFF, 0xB7, 0xF5, 0xBF, 0xB7, 0xAD}, 
  {0xED, 0xBD, 0xC3, 0xBD, 0xA5, 0xBD, 0x42, 0x3C},  
#endif

// Trucks
  {0x00, 0x7F, 0x41, 0x55, 0x55, 0x55, 0x55, 0x55},
  {0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55}, 
  {0x41, 0x7F, 0x22, 0x7F, 0x7F, 0x63, 0x22, 0x1C},

// Crocs
  {0x41, 0x63, 0x46, 0x6E, 0x7C, 0x7E, 0x7A, 0x3E},
  {0xBC, 0xFE, 0x7E, 0x3E, 0xBE, 0xBE, 0xFC, 0x7C},  
  {0x78, 0x38, 0x38, 0x38, 0x70, 0x60, 0x60, 0x40},

// Cars
  {0x00, 0x1C, 0x22, 0x63, 0x7F, 0x7F, 0x22, 0x22},
  {0x22, 0x3E, 0x3E, 0x7F, 0x63, 0x63, 0x22, 0x1C},
  {0x22, 0x3E, 0x3E, 0x7F, 0x63, 0x63, 0x22, 0x1C}
  };

// Opening artwork created by @senkunmusahi using  https://www.riyas.org/2013/12/online-led-matrix-f...
static const byte titleBmp[] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xC0, 0xF0, 0x7C, 0x06, 0x73, 0x59, 0x43,
0x06, 0x3C, 0x38, 0x30, 0x30, 0x38, 0x3E, 0x26, 0x7B, 0x59, 0x43, 0x06, 0x7C, 0xF0, 0xC0, 0x80,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0xFF,
0xCF, 0x01, 0x00, 0x00, 0x30, 0x60, 0xE0, 0xC0, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0xC0,
0xC0, 0x60, 0x30, 0x00, 0x00, 0x00, 0x01, 0xCF, 0xFE, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x7C, 0xFE, 0x86, 0x0E, 0x0E, 0x1C, 0x18, 0x31, 0x7F, 0xFE, 0xFC, 0x1C, 0x18, 0x38, 0x38, 0x38,
0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x38, 0x38, 0x38, 0x38, 0x18, 0x1C, 0xFC, 0xFE, 0x7F,
0x39, 0x18, 0x1C, 0x0E, 0x0E, 0xC6, 0xFE, 0x3C, 0x00, 0x01, 0x07, 0x0E, 0x1C, 0x38, 0x70, 0xC0,
0xC0, 0x80, 0x03, 0x07, 0xFC, 0xF8, 0x00, 0x00, 0xF0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xE0, 0xF0,
0x00, 0x00, 0xF8, 0xFC, 0x0F, 0x03, 0x80, 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x03, 0x01, 0x00,
0x04, 0x06, 0x0F, 0x0F, 0x06, 0x06, 0x03, 0x03, 0x03, 0x63, 0x73, 0x33, 0x3B, 0xFF, 0xFF, 0x7F,
0x3F, 0x38, 0xF0, 0xC0, 0x00, 0xF0, 0xF8, 0x3F, 0x7F, 0xFF, 0xFF, 0x3B, 0x33, 0x63, 0x63, 0x03,
0x03, 0x03, 0x06, 0x06, 0x0F, 0x0F, 0x06, 0x00, 
};

// Interrupt handlers
ISR(PCINT0_vect){ // PB0 pin button interrupt           
  if (clickLock == 0) {
    moveLeft = 1;
    clickLock = 1;
    clickBase = millis();
  }
}

void playerIncFrogger(){ // PB2 pin button interrupt
  if (clickLock == 0) {
    moveRight = 1;
    clickLock = 1;
    clickBase = millis();
  }
}

void displayTitle(void) {
  int incr = 0;
  for(int lxn = 2; lxn < 7; lxn++) {
    ssd1306_setpos(85,lxn); 
    ssd1306_send_data_start();
    for(int lxn2 = 0; lxn2 < 40; lxn2++) {
      ssd1306_send_byte(pgm_read_byte(&titleBmp[incr]));      
      incr++;
    }
    ssd1306_send_data_stop();                    
  }
}

// Arduino stuff - setup
void setup() {
  DDRB = 0b00000010;    // set PB1 as output (for the speaker)
  PCMSK = 0b00000001; // pin change mask: listen to portb bit 1
  GIMSK |= 0b00100000;  // enable PCINT interrupt 
  sei();          // enable all interrupts
}

// Arduino stuff - loop
void loop() { 
  ssd1306_init();
  ssd1306_fillscreen(0x00);

  // The lower case character set is seriously compromised because I've had to truncate the ASCII table
  // to release space for executable code.
  // There is no z in the table as this isn't used anywhere in the text here and most of the 
  // symbols are also missing for the same reason (see my hacked version of font6x8.h - font6x8AJ.h for more detail)
  ssd1306_char_f6x8(0, 2, "F R O G G E R");
  ssd1306_char_f6x8(0, 4, "andy jackson"); // see comments above !

  ssd1306_setpos(0,1); 
  for (int incr = 0; incr < 80; incr++) {
    ssd1306_send_data_start();
    ssd1306_send_byte(B00111000);
    ssd1306_send_data_stop();                    
  }
  ssd1306_setpos(0,3); 
  for (int incr = 0; incr < 80; incr++) {
    ssd1306_send_data_start();
    ssd1306_send_byte(B00011100);
    ssd1306_send_data_stop();                    
  }

  displayTitle();

  ssd1306_char_f6x8(0, 6, "inspired by");
  ssd1306_char_f6x8(0, 7, "webboggles.com");
  delay(1500);
  ssd1306_char_f6x8(0, 6, "artwork by ");
  ssd1306_char_f6x8(0, 7, "zsenkunmusashi");  // see comments above - f has been replaced by @ in the ASCII table

  long startT = millis();
  long nowT =0;
  boolean sChange = 0;

  while(digitalRead(0) == HIGH) {
    nowT = millis();
    if (nowT - startT > 2000) {
      sChange = 1;     
      if (digitalRead(2) == HIGH) {
        EEPROM.write(0,0);
        EEPROM.write(1,0);
        ssd1306_char_f6x8(8, 0, "-HIGH SCORE RESET-");  
      } else if (mute == 0) { mute = 1; ssd1306_char_f6x8(32, 0, "-- MUTE --"); } else { mute = 0; ssd1306_char_f6x8(31, 0, "- SOUND ON -");  }    
      break;
    }
    if (sChange == 1) break;
  }  
  while(digitalRead(0) == HIGH);

  if (sChange == 0) {
    delay(2000);

    ssd1306_init();
    ssd1306_fillscreen(0x00);

    playFrogger(); 

    topScore = EEPROM.read(0);
    topScore = topScore << 8;
    topScore = topScore |  EEPROM.read(1);

    newHigh = 0;
    if (score > topScore) { 
      topScore = score;
      EEPROM.write(1,score & 0xFF); 
      EEPROM.write(0,(score>>8) & 0xFF); 
      newHigh = 1;
      }

    ssd1306_fillscreen(0x00);
    ssd1306_char_f6x8(11, 1, "----------------");
    ssd1306_char_f6x8(11, 2, "G A M E  O V E R");
    ssd1306_char_f6x8(11, 3, "----------------");
    ssd1306_char_f6x8(37, 5, "SCORE:");
    doNumber(75, 5, score);
    if (!newHigh) {
      ssd1306_char_f6x8(21, 7, "HIGH SCORE:");
      doNumber(88, 7, topScore);
    }
    delay(1000);
    if (newHigh) {
      ssd1306_fillscreen(0x00);
      ssd1306_char_f6x8(10, 1, "----------------");
      ssd1306_char_f6x8(10, 3, " NEW HIGH SCORE ");
      ssd1306_char_f6x8(10, 7, "----------------");
      doNumber(50,5,topScore);
      for (int i = 700; i>200; i = i - 50){
      beep(30,i);
      }
      delay(1200);    
    } 
  }
  system_sleep();
}

void doNumber (int x, int y, int value) {
    char temp[10] = {0,0,0,0,0,0,0,0,0,0};
    itoa(value,temp,10);
    ssd1306_char_f6x8(x, y, temp);
}

void ssd1306_init(void){
  DDRB |= (1 << SSD1306_SDA); // Set port as output
  DDRB |= (1 << SSD1306_SCL); // Set port as output

  ssd1306_send_command(0xAE); // display off
  ssd1306_send_command(0x00); // Set Memory Addressing Mode
  ssd1306_send_command(0x10); // 00,Horizontal Addressing Mode;01,Vertical Addressing Mode;10,Page Addressing Mode (RESET);11,Invalid
  ssd1306_send_command(0x40); // Set Page Start Address for Page Addressing Mode,0-7
  ssd1306_send_command(0x81); // Set COM Output Scan Direction
  ssd1306_send_command(0xCF); // ---set low rowumn address
  ssd1306_send_command(0xA1); // ---set high rowumn address
  ssd1306_send_command(0xC8); // --set start line address
  ssd1306_send_command(0xA6); // --set contrast control register
  ssd1306_send_command(0xA8);
  ssd1306_send_command(0x3F); // --set segment re-map 0 to 127
  ssd1306_send_command(0xD3); // --set normal display
  ssd1306_send_command(0x00); // --set multiplex ratio(1 to 64)
  ssd1306_send_command(0xD5); // 
  ssd1306_send_command(0x80); // 0xa4,Output follows RAM content;0xa5,Output ignores RAM content
  ssd1306_send_command(0xD9); // -set display offset
  ssd1306_send_command(0xF1); // -not offset
  ssd1306_send_command(0xDA); // --set display clock divide ratio/oscillator frequency
  ssd1306_send_command(0x12); // --set divide ratio
  ssd1306_send_command(0xDB); // --set pre-charge period
  ssd1306_send_command(0x40); // 
  ssd1306_send_command(0x20); // --set com pins hardware configuration
  ssd1306_send_command(0x02);
  ssd1306_send_command(0x8D); // --set vcomh
  ssd1306_send_command(0x14); // 0x20,0.77xVcc
  ssd1306_send_command(0xA4); // --set DC-DC enable
  ssd1306_send_command(0xA6); // 
  ssd1306_send_command(0xAF); // --turn on oled panel 
}

void ssd1306_xfer_start(void){
  DIGITAL_WRITE_HIGH(SSD1306_SCL);  // Set to HIGH
  DIGITAL_WRITE_HIGH(SSD1306_SDA);  // Set to HIGH
  DIGITAL_WRITE_LOW(SSD1306_SDA);   // Set to LOW
  DIGITAL_WRITE_LOW(SSD1306_SCL);   // Set to LOW
}

void ssd1306_xfer_stop(void){
  DIGITAL_WRITE_LOW(SSD1306_SCL);   // Set to LOW
  DIGITAL_WRITE_LOW(SSD1306_SDA);   // Set to LOW
  DIGITAL_WRITE_HIGH(SSD1306_SCL);  // Set to HIGH
  DIGITAL_WRITE_HIGH(SSD1306_SDA);  // Set to HIGH
}

void ssd1306_send_byte(uint8_t byte){
  uint8_t i;
  for(i=0; i<8; i++)
  {
    if((byte << i) & 0x80)
      DIGITAL_WRITE_HIGH(SSD1306_SDA);
    else
      DIGITAL_WRITE_LOW(SSD1306_SDA);
    
    DIGITAL_WRITE_HIGH(SSD1306_SCL);
    DIGITAL_WRITE_LOW(SSD1306_SCL);
  }
  DIGITAL_WRITE_HIGH(SSD1306_SDA);
  DIGITAL_WRITE_HIGH(SSD1306_SCL);
  DIGITAL_WRITE_LOW(SSD1306_SCL);
}

void ssd1306_send_command(uint8_t command){
  ssd1306_xfer_start();
  ssd1306_send_byte(SSD1306_SA);  // Slave address, SA0=0
  ssd1306_send_byte(0x00);  // write command
  ssd1306_send_byte(command);
  ssd1306_xfer_stop();
}

void ssd1306_send_data_start(void){
  ssd1306_xfer_start();
  ssd1306_send_byte(SSD1306_SA);
  ssd1306_send_byte(0x40);  //write data
}

void ssd1306_send_data_stop(void){
  ssd1306_xfer_stop();
}

void ssd1306_setpos(uint8_t x, uint8_t y)
{
  if (y>7) return;
  ssd1306_xfer_start();
  ssd1306_send_byte(SSD1306_SA);  //Slave address,SA0=0
  ssd1306_send_byte(0x00);  //write command

  ssd1306_send_byte(0xb0+y);
  ssd1306_send_byte(((x&0xf0)>>4)|0x10); // |0x10
  ssd1306_send_byte((x&0x0f)|0x01); // |0x01

  ssd1306_xfer_stop();
}

void ssd1306_fillscreen(uint8_t fill_Data){
  uint8_t m,n;
  for(m=0;m<8;m++)
  {
    ssd1306_send_command(0xb0+m); //page0-page1
    ssd1306_send_command(0x00);   //low rowumn start address
    ssd1306_send_command(0x10);   //high rowumn start address
    ssd1306_send_data_start();
    for(n=0;n<128;n++)
    {
      ssd1306_send_byte(fill_Data);
    }
    ssd1306_send_data_stop();
  }
}

void ssd1306_char_f6x8(uint8_t x, uint8_t y, const char ch[]){
  uint8_t c,i,j=0;
  while(ch[j] != '\0')
  {
    c = ch[j] - 32;
    if (c >0) c = c - 12;
    if (c >15) c = c - 6;
    if (c>40) c=c-9;
    if(x>126)
    {
      x=0;
      y++;
    }
    ssd1306_setpos(x,y);
    ssd1306_send_data_start();
    for(i=0;i<6;i++)
    {
      ssd1306_send_byte(pgm_read_byte(&ssd1306xled_font6x8[c*6+i]));
    }
    ssd1306_send_data_stop();
    x += 6;
    j++;
  }
}

void system_sleep(void) {
  ssd1306_fillscreen(0x00);
  ssd1306_send_command(0xAE);
  cbi(ADCSRA,ADEN);                    // switch analog to digital converter off
  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 
  sbi(ADCSRA,ADEN);                    // switch analog to digital converter on
  ssd1306_send_command(0xAF);
}

void beep(int bCount,int bDelay){
  if (mute) return;
  for (int i = 0; i<=bCount; i++){digitalWrite(1,HIGH);for(int i2=0; i2<bDelay; i2++){__asm__("nop\n\t");}digitalWrite(1,LOW);for(int i2=0; i2<bDelay; i2++){__asm__("nop\n\t");}}
}


/* ------------------------
 *  Frogger main game code
 */
void playFrogger(){
  stopAnimate = 0;
  score = 0;
  moveDelay = MOVEBASE;
  level = 1;
  frogColumn = 8;
  frogRow = 7;
  clickLock = 0;
  frogMode = 1;
  interimStep =0;
  blockShiftL = 0;
  blockShiftR = 0;
  flipFlop = 1;
  flipFlopShift = 1;
  dockedFrogs = 0;
  lives = 2;
  frogRightLimit = 12;
  watchDog = 1; // we use this to see if there's been movement - it's only ever zero when the frog has just moved!

  attachInterrupt(0,playerIncFrogger,CHANGE);

  initScreen();
  resetDock(0);

  drawFrog(frogMode,0);
  drawGameScreen(frogMode);
  drawLives();
  drawDocks();
  
  doNumber(0,7,score);
  
  while (lives >= 0) {
    interimStep++;

    if (watchDog >= 500) lives = -1; // Stop the game if nothing's happening - maybe triggered in someone's pocket so this is to save battery!
    
    // Calculate left limit of frog movement so it doesn't hit the score
    frogLeftLimit = 1;
    if ((score / 10) % 10 != 0) frogLeftLimit++; 
    if ((score / 100) % 10 != 0) frogLeftLimit++;
    if ((score / 1000) % 10 != 0) frogLeftLimit++;

    // Move stuff along if it's time to
    if (interimStep > moveDelay/8) {
      watchDog++;
      blockShiftL++;
      if (flipFlopShift == 1) flipFlopShift = 0; else flipFlopShift = 1;
      if (flipFlopShift == 1) blockShiftR++;
      if (blockShiftL == 7) {
        moveBlocks();
        blockShiftL = 0;
      }
      if (blockShiftR == 7) {
        blockShiftR = 0;
      }
      interimStep = 0;
      checkCollision();
      if (stopAnimate == 0) {
        drawGameScreen(frogMode);
        drawFrog(frogMode,0);
      }
    }

    // Handle input from 'jump' button (the other two buttons are captured in the interrupt routines)
    if (analogRead(0) < 940 && clickLock == 0) {
      moveForward = 1;
      watchDog = 0;   // reset the watchdog so the game doesn't end!
      clickLock = 1;
      clickBase = millis();
    }

    // Handle moving left
    if(moveLeft == 1 && millis() > clickBase + CLICKDELAY/2) {
      watchDog = 0;   // reset the watchdog so the game doesn't end!
      moveLeft = 0;
      if (digitalRead(2) == HIGH) moveForward = 1; else {
        drawFrog(0,0);  // delete the frog
        // move the frog, checking it isn't jumping off the edge of the screen
        if ((frogRow == 7 && frogColumn > frogLeftLimit) || (frogRow < 7 && frogColumn > 0)) {
          frogColumn --;
        } else if (frogRow < 7) stopAnimate = 1;
        frogMode = 2; // pointing left
      }
    }

    // Handle moving right
    if(moveRight == 1 && millis() > clickBase + CLICKDELAY/2){
      watchDog = 0;   // reset the watchdog so the game doesn't end!
      moveRight = 0;
      if (digitalRead(0) == HIGH) moveForward = 1; else {
        drawFrog(0,0); // delete the frog
        // move the frog, checking it isn't jumping off the edge of the screen
        if ((frogRow == 7 && frogColumn < frogRightLimit) || (frogRow < 7 && frogColumn < 14)) {
          frogColumn ++;
        } else if (frogRow < 7) stopAnimate = 1;
        frogMode = 3; // pointing right    
      }
    }

    // Handle 'move forward' button press
    if (moveForward == 1) {
      moveForward = 0;
  
      score+= level;          // increment the score for every move
      doNumber(0,7,score);    // display new score
      drawFrog(0,0);          // delete the frog
  
      if (frogRow > 1) {
        frogRow--; 
        // Correct for the skew in frog position created by the blockShift scrolling parameter
        if (frogRow == 3 && blockShiftL < 4) frogColumn--;
        if (frogRow == 2 && blockShiftR + blockShiftL < 5) frogColumn++;
        if (frogRow == 1 && blockShiftR + blockShiftL < 5) frogColumn--;
                
      } else {
        // frog is at the docks!
        if (blockShiftL < 4 && frogColumn <15) frogColumn++;  // account for skew due to block shifting
        byte dockPos = (byte)floor(frogColumn/3);
        if (frogDocks[dockPos] == 0 ) {
          dockedFrogs++;          
          frogDocks[dockPos] = 1;                             // assign this dock as filled
          frogRow = 7;                                        // reposition the frog at the start
          frogColumn = 8;
          for (int i = 1000; i>200; i = i - 100){             // make sound
          beep(10,i);
          drawDocks();                                        // redraw the docks
          }
        } else stopAnimate = 1;
      }
      frogMode = 1;             // mode 1 = forwards position

      // check if all docks are full - if so, then level up!
      if (dockedFrogs >= 5) {
        level++;
        levelUp(level);
        if (moveDelay > 99) moveDelay -=100; // make the game speed up 
        initScreen();                        // reinitalise the position of game items
        resetDock(0);                        // reinitliase the dock
        dockedFrogs = 0;              
        drawDocks();                         // display the (now empty) docks
        drawLives();                         // display the lives
        doNumber(0,7,score);                 // display the score
      }      
    }

    // The frog has moved 
    if (watchDog == 0 && stopAnimate == 0) {
      watchDog = 1;               // set to something other than zero so this routine doesn't run again
      // redraw the frog
      drawFrog(frogMode,0);
      // redraw the screen
      drawGameScreen(frogMode);
      // make jump sound
      beep(30,400);
      beep(30,300);
      beep(30,200);
    }
    
    checkCollision();
    
    if (clickLock == 1 && millis() > clickBase + CLICKDELAY && digitalRead(2)==0 && digitalRead(0)==0 && analogRead(0) > 940) clickLock = 0; // normal debounce

    // check to see if the frog has been killed
    if (stopAnimate != 0) {
      // redraw the screen
      drawGameScreen(frogMode);
      // animation for frog death
      drawFrog(0,1);
      for (int i = 0; i<250; i = i+ 50){  
        beep(50,i);
      }
      drawFrog(frogMode,1);    
      for (int i = 250; i<500; i = i+ 50){  
        beep(50,i);
      }
      drawFrog(0,1);
      for (int i = 500; i<750; i = i+ 50){  
        beep(50,i);
      }
      drawFrog(frogMode,1);
      for (int i = 750; i<1000; i = i+ 50){  
        beep(50,i);
      }
      delay(600);
      lives--;          // increment the score for every move
      frogRightLimit++; // there's one less frog drawn on right so you can move a bit further across (if you really want to!)
      stopAnimate = 0;  // reset parameter
      drawLives();      // display number of lives left
      frogColumn = 8;   // reinitalise frog location
      frogRow = 7;
      }
  }  // Big while loop (main game loop) goes until lives is negative
} 

void checkCollision(void) {
  if (frogRow > 0 && frogRow < 4 && grid[frogRow-1][frogColumn] == 0) stopAnimate = 1; // the frog has fallen in the river
  if (frogRow > 0 && frogRow < 4 && grid[frogRow-1][frogColumn] > 9) stopAnimate = 1; // the frog has stepped on a croc
  if ((frogRow < 7 && frogRow > 3) && (grid[frogRow-1][frogColumn] != 0 || grid[frogRow-1][frogColumn-1] != 0)) stopAnimate = 1; // the frog has been hit by a vehicle
}

// Initialise all the moving objects on the game screen
void initScreen(void) {
  int initCounter[6] = {3,2,4,2,2,3};       // the length of the objects on each row - doesn't change
  int gapCounter[6] = {-2,-3,-4,-4,-3,-5};  // the gaps between objects - change with levels to make it harder as you go thru the game
  int counter[6];                           // used to hold the gap data
  byte stepMode = 0;                        // which component of the object are we drawing (they all have three - a start a middle and an end)
  byte stepShift = 0;                       // offset to shift up to the different objects in the array
  byte crocStartColumn = 0;                 // column at which to stop drawing crocs - is zero at start hence no crocs!

  
  // Adjust difficulty by changing gaps between objects according to level
  if (level == 1) {
    gapCounter[5] = -14;           // easiset setting, for start of game
  }
  if (level < 3) {
    gapCounter[4] = -6;            // make it easier for levels less than 3 by increasing the gap in the cars on this row
  }
  if (level < 4) {
    gapCounter[3] = -7;
  }
  if (level > 4) {
    for (byte incr = 1; incr < 3; incr++) {
      gapCounter[incr]--;         // increase the gaps between the logs for levels over 4  
    }
  }
  if (level > 7) {                // set smaller gaps between cars for levels over 7
    gapCounter[3] = -4; 
    gapCounter[4] = -2;
    gapCounter[5] = -3;
  }
  if (level > 2) crocStartColumn = 5; // one croc appears at level 3 and above
  if (level > 6) crocStartColumn = 9; // two croc appear at level 7 and above
  
  // Initialise the counters
  for (byte incr = 0; incr < 6;incr++) counter[incr] = initCounter[incr];  

  // Initialise array with zeros
  for (byte col = 0; col < 16; col++) {
    for (byte row = 0; row < 6; row++) {
      grid[row][col] = 0;
    }
  }
       
  stepMode = 0;
  // Initialise array with obstacles
  for (byte row = 0; row < 6; row++) {
    for (byte col = 0; col < 15; col++) {         
      if (counter[row] > 0) {
        if (14-row > counter[row]) {
          if (counter[row] == 1) if (stepMode == 1) stepMode = 2; // the next space is blank and we are drawing the middle - draw the end! 
          if (row > 2) stepShift = 3; else stepShift = 0;         // shift up to the trucks in the array
          if (row == 4) stepShift = 9;                            // shift up to the cars in the array - also theres no middle
          
          if (row > 0) {
            grid[row][col] = 4+stepMode+stepShift;                // if you are on any row but the first - draw whatever is appropriate from the bitmaps
          } else if (col >= crocStartColumn) {                    
            grid[row][col] = 4+stepMode+stepShift;                // if you're on row zero (top row of logs) and you are above where crocs should be drawm, draw logs ...
          } else grid[row][col] = 10+stepMode;                    // .. otherwise draw crocs
          if (stepMode == 0) stepMode = 1;                        // we've drawn the left side now switch to central sections
          if (stepMode == 2) stepMode = 0;                        // we've drawn the end, now reset
        }
      } 
      counter[row]--;                                             // decrement the counter
      if (counter[row] <= gapCounter[row]) {
        counter[row] = initCounter[row];                          // if we have gone negative enough to account for the gaps - reset the counter and start again
      }
    }
  }
}

// Display the frog
void drawFrog(byte mode, bool frogDead) {
  if (frogRow > 6 || frogRow < 1 || frogDead == 1) {           // don't draw the frog when it's on the road or on logs - because they are moving, that's handled in the main drawing routine below- exception is when you are animating frog death
    if (frogRow == 1 || frogRow == 3) {                        // these allow for the blocks being shifted when animating the frog death on rows with logs
      ssd1306_setpos(frogColumn*8 + 7 - blockShiftL,frogRow);
    } else if (frogRow == 2) {
      ssd1306_setpos(frogColumn*8 + blockShiftR,frogRow);      
    } else {
      ssd1306_setpos(frogColumn*8,frogRow); 
    }
    ssd1306_send_data_start();
    sendBlock(mode,0);                   // draw the frog - mode is direction
    ssd1306_send_data_stop();                    
  }    
}

// Display the frog and all the moving items on the screen
void drawGameScreen(byte mode) {
  bool inverse = 0;
  
  // Draw objects going left
  for (byte row = 0; row < 6; row+=2) {
    if (row >=0 && row < 3) inverse = 1; else inverse = 0;                              // draw everything (except the frog) in inverse video on the river rows (0,1,2)
    ssd1306_setpos(0,row+1);                                                            // +1 because row 0 here is actually row 1 on the screen
    ssd1306_send_data_start();
    for (byte incr = 0; incr < 7-blockShiftL; incr++) if (grid[row][15] == 0) {         // cover the tiny bit to the far left of the screen up to wherever the main blocks will be drawn (depends on how far they are shifted)
      sendByte(0,inverse);                                                              // draw an empty 8-bit line if there's nothing wrapping around
    } else {
      sendByte(pgm_read_byte(&bitmaps[grid[row][15]-1][1+blockShiftL+incr]), inverse);  // pick the correct bit of whatever is wrapping from the right of the screen
    }
    for (byte col = 0; col < 15; col++) {         
      if (frogRow == row+1 && frogColumn == col && frogRow < 4 && frogRow > 0) {
        sendBlock(mode,0);                                                                          // if we are in a location with the frog, and it's on the logs, draw it - never invert it (hence zero as second parameter here)
      } else if (stopAnimate == 0 && frogRow == row+1 && frogColumn == col + 1 && frogRow > 3 && frogRow < 7) {         // frog is amongst the cars and needs drawing
        for (byte incr = 0; incr < blockShiftL; incr++) sendByte(0,0);                              // draw the blank space up to the frog
        sendBlock(mode,0);                                                                          // draw frog
        for (byte incr = 0; incr < 7-blockShiftL; incr++) sendByte(0,0);                            // draw the blank space after the frog
        col++;                                                                                      // we've now drawn two columns so increment
      } else {
        sendBlock(grid[row][col],inverse);                                                          // draw the correct object for this space - it's not a frog ;)
      }
    }
    // fill in the bit to the right of the main blocks 
    for (byte incr = 0; incr < blockShiftL; incr++) if (grid[row][15] == 0) sendByte(0,inverse); else sendByte(pgm_read_byte(&bitmaps[grid[row][15]-1][incr]),inverse);

    ssd1306_send_data_stop();
  }
  if (frogColumn == 0) drawFrog(mode,1); // this covers the exceptional case where the frog is in the far left colum, in which case the normal routine can't draw it when it's on the road
  
  // Draw objects going right - see comments above, works in basically the same way
  for (byte row = 1; row < 6; row+=2) {
    if (row > 0 && row < 3) inverse = 1; else inverse = 0;
    ssd1306_setpos(0,row+1);
    ssd1306_send_data_start();
    for (byte incr = 0; incr < blockShiftR; incr++) if (grid[row][15] == 0) sendByte(0, inverse); else sendByte(pgm_read_byte(&bitmaps[grid[row][15]-1][incr+(8-blockShiftR)]),inverse);
    for (byte col = 0; col < 15; col++) {         
      if (frogRow == row+1 && frogColumn == col && frogRow < 4 && frogRow > 0) {
        sendBlock(mode,0);    
      } else if (stopAnimate == 0 && frogRow == row+1 && frogColumn == col + 1 && frogRow > 3 && frogRow < 7) {  
        for (byte incr = 0; incr < 7-blockShiftR; incr++) sendByte(0,0);
        sendBlock(mode,0); // draw frog
        for (byte incr = 0; incr < blockShiftR; incr++) sendByte(0,0);
        col++;        
      } else {
        sendBlock(grid[row][col],inverse);        
      }
    }
    for (byte incr = 0; incr < 7-blockShiftR; incr++) if (grid[row][15] == 0) sendByte(0,inverse); else sendByte(pgm_read_byte(&bitmaps[grid[row][15]-1][incr]),inverse);
    ssd1306_send_data_stop();
  }
  if (frogColumn == 0) drawFrog(mode,1);
}

// Send one byte to the screen
void sendByte(byte fill, bool inverse) {
  if (inverse == 0) ssd1306_send_byte(fill); else ssd1306_send_byte(~fill);
}

// Send one block of 8 bytes to the screen - inverse means inverse video, for the river section
void sendBlock(byte fill, bool inverse){
  for (int incr = 0; incr < 8; incr++) {
    if (fill > 0) {
      if (inverse == 0) ssd1306_send_byte(pgm_read_byte(&bitmaps[fill-1][incr])); else ssd1306_send_byte(~pgm_read_byte(&bitmaps[fill-1][incr]));  
    } else if (inverse ==0) ssd1306_send_byte(0); else ssd1306_send_byte(0xFF); 
  }
}

// Draw the frog lives (in the right hand corner)
void drawLives(void) {
  byte tempRow = frogColumn;
  byte tempCol = frogRow;
  frogRow = 7;

  for (int incr = 2; incr > 0; incr--) {
    frogColumn = 15-incr;
    drawFrog(0,1);
  }

  for (int incr = lives; incr > 0; incr--) {
    frogColumn = 15-incr;
    drawFrog(1,1);
  }
  frogRow = tempCol;
  frogColumn = tempRow;    
}

// Draw the docks for the frog to land in at top of screen
void drawDocks(void) {
  byte drawPos = 3;
  for (byte incr = 0; incr < 5; incr++) {
    ssd1306_setpos(drawPos,0);
    ssd1306_send_data_start();
    ssd1306_send_byte(B11111111);
    ssd1306_send_byte(B00000001);
    ssd1306_send_byte(B00000001);
    if (frogDocks[incr] == 1) sendBlock(1,0); else for(byte lxn = 0; lxn < 8; lxn++) ssd1306_send_byte(B00000001);
    ssd1306_send_byte(B00000001);
    ssd1306_send_byte(B00000001);
    ssd1306_send_byte(B11111111);
    ssd1306_send_data_stop();
    drawPos+= 24;
    }    
}

// Set all the frog docks to a single value
void resetDock(byte value) {   for (byte incr = 0; incr < 5;incr++) frogDocks[incr] = value; }

// Handle what happens at the end of a level
void levelUp(int number) {  
  // Flash the frog docks
  delay(200);
  for (byte incr = 0; incr < 5; incr ++) {
    resetDock(0);
    drawDocks();
    for (int i = 800; i>200; i = i - 200){
    beep(20,i);
    }
    resetDock(1);
    drawDocks();
    for (int i = 800; i>200; i = i - 200){
    beep(20,i);
    }
  }  
  delay(500);
  ssd1306_fillscreen(0x00);
  ssd1306_char_f6x8(35, 1, "---------");
  ssd1306_char_f6x8(35, 3, " LEVEL ");
  ssd1306_char_f6x8(35, 5, "---------");
  doNumber(77,3,number);
  delay(1500);    
  ssd1306_fillscreen(0x00);
}

// Move all the items on the game screen (wrapping at the ends) and check for frog dropping off the end of the screen
void moveBlocks(void) {
  int direct = 0;

  if (flipFlop == 1) flipFlop = 0; else flipFlop = 1;
  
  for (byte row = 0; row < 6; row++) {
    // Move the frog along and check to see whether it's gone off the screen, in which case it dies
    if (frogRow < 4 && frogRow > 0) { 
      if (frogRow == row + 1) {
        if (direct == 1 && flipFlop == 1) {
          if (frogColumn >= 14) stopAnimate = 1; else frogColumn++; 
        } else if (direct == 0) {
          if (frogColumn < 1) stopAnimate = 1; else frogColumn--;
        }
      }  
    }
    if (direct == 0) { // move left
      byte temp = grid[row][0];
      for (byte col = 0; col < 15; col++) {         
        grid[row][col] = grid[row][col+1];
      }
      grid[row][15] = temp; // wrap around
      direct = 1;
    } else { // move right
      if (flipFlop == 1) {
        byte temp = grid[row][15];
        for (byte col = 15; col > 0; col--) {         
          grid[row][col] = grid[row][col-1];
          }
        grid[row][0] = temp; // wrap around
        }
      direct = 0;  
      }
    }
  }
Tiny Speed Challenge

Participated in the
Tiny Speed Challenge