Slot machine on an Arduino Uno
Pull a real arcade lever to spin three OLED reels that stop one by one. Coins, a bet, win lights and a pay table with odds you can check. The whole build, an Arduino Uno and 9 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.
Teaching? Assign it to your class in one click.
Advanced Runs in your browser. Free, and no account needed.
Build it as the Slot Machine Kit: the parts in numbered bags and a booklet that checks every step. Free.
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Slot machine: three reels on three OLEDs, a real arcade lever, honest odds.
//
// Press COIN for five credits, set the bet with BET- and BET+, then pull the
// lever toward you (down) to spin. The reels stop one at a time, left to
// right, and a win lights the strip and pays bet x the table below.
//
// pin 2 the lever's DOWN switch, to GND: a pull spins
// pin 3 COIN button, to GND
// pin 4 BET+ button, to GND
// pin 5 BET- button, to GND
// pin 6 WS2812 strip DIN, eight win lights
// pin 7 passive piezo, its other leg on GND
// A0, A1 reel 1 OLED SDA and SCL
// A2, A3 reel 2 OLED SDA and SCL
// A4, A5 reel 3 OLED SDA and SCL
//
// Three OLEDs and only two addresses: a 128x64 module answers at 0x3C, or 0x3D
// with its jumper moved, so a third one cannot share a bus with the others.
// The usual fixes are an I2C multiplexer or one bus per screen. This sketch
// takes the second, because an Uno has the pins and I2C is easy to do by hand:
// each screen gets its own two wires and the same few lines of code.
//
// It also keeps no frame buffer. A 128x64 frame is 1 KB and the Uno has 2 KB,
// so three of them could never fit. Each reel is drawn straight onto the glass,
// one 8-pixel page at a time, worked out from the reel strip as it goes.
//
// THE ODDS. Every reel carries the same strip of sixteen stops:
//
// cherry x4, lemon x6, bell x3, bar x2, seven x1
//
// and each spin picks a stop on each reel at random, all sixteen equally
// likely, so there are 16 x 16 x 16 = 4096 outcomes and each is as likely as
// any other. The table pays, per credit bet:
//
// 7 7 7 100 1 way in 4096
// BAR BAR BAR 40 8 ways
// BELL x3 15 27 ways
// CHERRY x3 10 64 ways
// LEMON x3 5 216 ways
// CHERRY CHERRY 3 192 ways (first two reels, third anything else)
// CHERRY 1 768 ways (first reel only: your bet back)
//
// Add it up and the 4096 outcomes pay back 3889 credits for every 4096 bet:
// 94.9 percent. About one spin in three pays something, but most of those only
// hand the bet back, and in the long run the machine keeps about 5 credits of
// every 100. Every slot machine is built that way. This one tells you.
#include <Adafruit_NeoPixel.h>
#include <EEPROM.h>
#include <avr/pgmspace.h>
const int LEVER = 2;
const int COIN = 3;
const int BET_UP = 4;
const int BET_DOWN = 5;
const int LIGHTS_PIN = 6;
const int SPEAKER = 7;
const uint8_t OLED_ADDRESS = 0x3C;
const uint8_t REEL_SCL[3] = {A1, A3, A5};
const uint8_t REEL_SDA[3] = {A0, A2, A4};
const int CREDITS_PER_COIN = 5;
const int MAX_CREDITS = 995;
const int MAX_BET = 3;
// ---- The reels and the pay table ---------------------------------------
enum { CHERRY, LEMON, BELL, BAR, SEVEN };
const char *const NAMES[] = {"CHERRY", "LEMON", "BELL", "BAR", "SEVEN"};
const int STOPS = 16;
const uint8_t STRIP[STOPS] = {CHERRY, LEMON, BELL, LEMON, BAR, CHERRY, LEMON, BELL,
LEMON, CHERRY, SEVEN, LEMON, BAR, CHERRY, BELL, LEMON};
// What a line pays per credit bet, and what to call it.
int payFor(uint8_t a, uint8_t b, uint8_t c, const char **name) {
if (a == b && b == c) {
static const int TRIPLE[] = {10, 5, 15, 40, 100};
static const char *const LINE[] = {"3 CHERRIES", "3 LEMONS", "3 BELLS", "3 BARS", "JACKPOT 777"};
*name = LINE[a];
return TRIPLE[a];
}
if (a == CHERRY && b == CHERRY) {
*name = "2 CHERRIES";
return 3;
}
if (a == CHERRY) {
*name = "1 CHERRY";
return 1;
}
*name = "";
return 0;
}
// The five symbols, 32x32, in the panel's own order: four pages of 32 columns,
// each byte eight pixels stacked with bit 0 at the top.
const uint8_t ICONS[5][128] PROGMEM = {
{ // CHERRY
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80,
0xc0, 0xf0, 0xf8, 0xfc, 0xdc, 0xdc, 0xdc, 0xfc, 0xec, 0xec, 0x68, 0x70, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xe0, 0xf8, 0x3e, 0x0f,
0x03, 0x03, 0x1f, 0x7e, 0xf0, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xe0, 0xf0, 0xf8, 0x9c, 0x8e, 0x8e, 0xfe, 0xfe, 0xfe, 0xff, 0xff, 0xfc, 0xf8, 0xe0,
0x00, 0xc0, 0xe0, 0xf0, 0x3b, 0x1f, 0x1e, 0xfc, 0xfc, 0xfc, 0xfc, 0xf8, 0xf8, 0xf0, 0xc0, 0x00,
0x00, 0x00, 0x07, 0x1f, 0x3f, 0x3f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x3f, 0x3f, 0x1f, 0x07,
0x00, 0x0f, 0x3f, 0x7f, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0x7f, 0x3f, 0x0f, 0x00,
},
{ // LEMON
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x80, 0x80,
0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xc0, 0xf0, 0xf8, 0xfc, 0x7e, 0x1e, 0x8f, 0xc7, 0xe7, 0xf3, 0xf7, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xfe, 0xfc, 0xf8, 0xf0, 0xc0, 0x80, 0x00, 0x00,
0x00, 0x03, 0x07, 0x0f, 0x3f, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f,
0x9f, 0xff, 0xff, 0xff, 0xf3, 0xf3, 0xff, 0xff, 0xff, 0xff, 0x7f, 0x3f, 0x1f, 0x07, 0x03, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x03, 0x07, 0x07, 0x07, 0x07, 0x07,
0x07, 0x07, 0x07, 0x07, 0x07, 0x03, 0x03, 0x03, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
{ // BELL
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xc0, 0xe0, 0xe0, 0xf0, 0xfc, 0xfe,
0xfe, 0xfc, 0xf0, 0xe0, 0xe0, 0xc0, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xfc, 0xff, 0x3f, 0x01, 0xe1, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x80, 0xf0, 0xfe, 0xff, 0xff, 0xdf, 0xc0, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xf0, 0x80, 0x00, 0x00, 0x00,
0x00, 0x00, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x7f, 0xff, 0xff,
0xff, 0xff, 0x7f, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x00, 0x00,
},
{ // BAR
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xfe, 0xff, 0xff, 0x0f, 0x0f, 0x0f, 0x6f, 0x6f, 0x0f, 0x8f, 0xff, 0xff, 0xff, 0x7f, 0x0f, 0xcf,
0x8f, 0x0f, 0x7f, 0xff, 0xff, 0xff, 0x0f, 0x0f, 0xef, 0x6f, 0x0f, 0x0f, 0xff, 0xff, 0xfe, 0x00,
0x7f, 0xff, 0xff, 0xf0, 0xf0, 0xf2, 0xf7, 0xf6, 0xf0, 0xf0, 0xff, 0xff, 0xf1, 0xf0, 0xfc, 0xfd,
0xfd, 0xf8, 0xf0, 0xf3, 0xff, 0xff, 0xf0, 0xf0, 0xfe, 0xfe, 0xf8, 0xf1, 0xf3, 0xff, 0x7f, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
{ // SEVEN
0x00, 0x00, 0x00, 0x00, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc,
0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0xfc, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x81, 0xc1, 0xf1, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0x3f, 0x0f, 0x03, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xe0, 0xf8,
0xfe, 0xff, 0xff, 0xff, 0xff, 0x7f, 0x1f, 0x07, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x70, 0x7c, 0x7e, 0x7f, 0x7f, 0x7f,
0x7f, 0x7f, 0x1f, 0x07, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
};
// A 5x7 font for space to Z, one byte per column, bit 0 at the top.
const uint8_t FONT[] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5f, 0x00, 0x00, 0x00, 0x07, 0x00, 0x07, 0x00,
0x14, 0x7f, 0x14, 0x7f, 0x14, 0x24, 0x2a, 0x7f, 0x2a, 0x12, 0x23, 0x13, 0x08, 0x64, 0x62,
0x36, 0x49, 0x56, 0x20, 0x50, 0x00, 0x05, 0x03, 0x00, 0x00, 0x00, 0x1c, 0x22, 0x41, 0x00,
0x00, 0x41, 0x22, 0x1c, 0x00, 0x14, 0x08, 0x3e, 0x08, 0x14, 0x08, 0x08, 0x3e, 0x08, 0x08,
0x00, 0x50, 0x30, 0x00, 0x00, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x60, 0x60, 0x00, 0x00,
0x20, 0x10, 0x08, 0x04, 0x02, 0x3e, 0x51, 0x49, 0x45, 0x3e, 0x00, 0x42, 0x7f, 0x40, 0x00,
0x62, 0x51, 0x49, 0x49, 0x46, 0x22, 0x41, 0x49, 0x49, 0x36, 0x18, 0x14, 0x12, 0x7f, 0x10,
0x27, 0x45, 0x45, 0x45, 0x39, 0x3c, 0x4a, 0x49, 0x49, 0x30, 0x01, 0x71, 0x09, 0x05, 0x03,
0x36, 0x49, 0x49, 0x49, 0x36, 0x06, 0x49, 0x49, 0x29, 0x1e, 0x00, 0x36, 0x36, 0x00, 0x00,
0x00, 0x56, 0x36, 0x00, 0x00, 0x08, 0x14, 0x22, 0x41, 0x00, 0x14, 0x14, 0x14, 0x14, 0x14,
0x00, 0x41, 0x22, 0x14, 0x08, 0x02, 0x01, 0x51, 0x09, 0x06, 0x3e, 0x41, 0x5d, 0x55, 0x1e,
0x7e, 0x11, 0x11, 0x11, 0x7e, 0x7f, 0x49, 0x49, 0x49, 0x36, 0x3e, 0x41, 0x41, 0x41, 0x22,
0x7f, 0x41, 0x41, 0x22, 0x1c, 0x7f, 0x49, 0x49, 0x49, 0x41, 0x7f, 0x09, 0x09, 0x09, 0x01,
0x3e, 0x41, 0x49, 0x49, 0x7a, 0x7f, 0x08, 0x08, 0x08, 0x7f, 0x00, 0x41, 0x7f, 0x41, 0x00,
0x20, 0x40, 0x41, 0x3f, 0x01, 0x7f, 0x08, 0x14, 0x22, 0x41, 0x7f, 0x40, 0x40, 0x40, 0x40,
0x7f, 0x02, 0x04, 0x02, 0x7f, 0x7f, 0x04, 0x08, 0x10, 0x7f, 0x3e, 0x41, 0x41, 0x41, 0x3e,
0x7f, 0x09, 0x09, 0x09, 0x06, 0x3e, 0x41, 0x51, 0x61, 0x7e, 0x7f, 0x09, 0x19, 0x29, 0x46,
0x26, 0x49, 0x49, 0x49, 0x32, 0x01, 0x01, 0x7f, 0x01, 0x01, 0x3f, 0x40, 0x40, 0x40, 0x3f,
0x1f, 0x20, 0x40, 0x20, 0x1f, 0x3f, 0x40, 0x3c, 0x40, 0x3f, 0x63, 0x14, 0x08, 0x14, 0x63,
0x03, 0x04, 0x78, 0x04, 0x03, 0x61, 0x51, 0x49, 0x45, 0x43,
};
// ---- I2C by hand, one bus per screen -------------------------------------
// The module has its own pull-ups, so a line is "released" (an input, pulled
// high) or "pulled" (an output driving low). Never driven high: that is what
// makes it a bus.
//
// Drawing a screen takes a while, and a coin dropped in the middle of it must
// not be lost. So every byte sent also glances at the four buttons (pins 2 to 5
// are bits 2 to 5 of port D, one read for all four) and remembers any that were
// down. The next readInputs() counts it.
//
// A screen is thousands of line changes, so each line is kept as its port's
// direction register and bit: one write releases it or pulls it, where
// pinMode() would look the pin up every time. A0 to A5 are bits 0 to 5 of
// port C. The output latch is
// left at 0, so setting the direction bit pulls the line low.
struct Line {
volatile uint8_t *ddr;
uint8_t bit;
};
Line lineFor(uint8_t pin) {
if (pin >= A0) return {&DDRC, (uint8_t)(1 << (pin - A0))};
if (pin >= 8) return {&DDRB, (uint8_t)(1 << (pin - 8))};
return {&DDRD, (uint8_t)(1 << pin)};
}
Line scl, sda;
uint8_t sawDown = 0;
void release(const Line &line) { *line.ddr &= (uint8_t)~line.bit; }
void pull(const Line &line) { *line.ddr |= line.bit; }
void busStart(int reel) {
scl = lineFor(REEL_SCL[reel]);
sda = lineFor(REEL_SDA[reel]);
release(sda);
release(scl);
pull(sda); // START: SDA falls while SCL is high
pull(scl);
}
void busWrite(uint8_t value) {
for (uint8_t bit = 0x80; bit != 0; bit >>= 1) {
if (value & bit) release(sda);
else pull(sda);
release(scl);
pull(scl);
}
sawDown |= (uint8_t)~PIND & 0x3c; // any button held while we draw
release(sda); // the ninth clock, for the acknowledge
release(scl);
pull(scl);
}
void busStop() {
pull(sda);
release(scl);
release(sda); // STOP: SDA rises while SCL is high
}
void commands(int reel, const uint8_t *bytes, uint8_t count) {
busStart(reel);
busWrite(OLED_ADDRESS << 1);
busWrite(0x00); // what follows is commands
for (uint8_t i = 0; i < count; i++) busWrite(bytes[i]);
busStop();
}
// Point the panel at a window, then open a data transfer into it.
void window(int reel, uint8_t col0, uint8_t col1, uint8_t page0, uint8_t page1) {
const uint8_t w[] = {0x21, col0, col1, 0x22, page0, page1};
commands(reel, w, sizeof(w));
busStart(reel);
busWrite(OLED_ADDRESS << 1);
busWrite(0x40); // what follows is pixels
}
void oledBegin(int reel) {
static const uint8_t INIT[] = {
0xae, 0xd5, 0x80, 0xa8, 0x3f, 0xd3, 0x00, 0x40, 0x8d, 0x14,
0x20, 0x00, // horizontal addressing: a window fills left to right, page by page
0xa1, 0xc8, // the right way up on a real module
0xda, 0x12, 0x81, 0xcf, 0xd9, 0xf1, 0xdb, 0x40, 0xa4, 0xa6, 0xaf,
};
pinMode(REEL_SCL[reel], INPUT); // released, and the latch at 0 for pulling
pinMode(REEL_SDA[reel], INPUT);
commands(reel, INIT, sizeof(INIT));
window(reel, 0, 127, 0, 7);
for (int i = 0; i < 1024; i++) busWrite(0);
busStop();
}
// ---- Drawing --------------------------------------------------------------
// The reel window is the top six pages (48 rows); the icons are 32 columns wide
// in the middle of it. Stops are PITCH rows apart down the strip, so a stopped
// symbol sits in rows 8 to 39 with 8 rows of the next ones peeking above and below.
const int PITCH = 40;
const int STRIP_ROWS = STOPS * PITCH;
const uint8_t ICON_COL = 48;
// The scroll position of each reel: the strip row at the top of the window.
int reelPos[3];
int stoppedPos(int stop) { return (stop * PITCH - 4 + STRIP_ROWS) % STRIP_ROWS; }
void drawReel(int reel) {
window(reel, ICON_COL, ICON_COL + 31, 0, 5);
for (uint8_t page = 0; page < 6; page++) {
// Which symbol, and which of its rows, lands on each of this page's eight
// rows. Dividing is slow on an AVR, so it is done eight times a page here
// rather than 256 times in the loop below.
const uint8_t *iconRow[8];
uint8_t mask[8];
for (uint8_t bit = 0; bit < 8; bit++) {
int row = (reelPos[reel] + page * 8 + bit) % STRIP_ROWS;
int inIcon = row % PITCH - 4;
if (inIcon < 0 || inIcon >= 32) {
iconRow[bit] = nullptr;
continue;
}
iconRow[bit] = &ICONS[STRIP[row / PITCH]][(inIcon >> 3) * 32];
mask[bit] = (uint8_t)(1 << (inIcon & 7));
}
for (uint8_t col = 0; col < 32; col++) {
uint8_t out = 0;
for (uint8_t bit = 0; bit < 8; bit++) {
if (iconRow[bit] && (pgm_read_byte(iconRow[bit] + col) & mask[bit])) out |= 1 << bit;
}
busWrite(out);
}
}
busStop();
}
// Arrows either side of the middle row: the pay line.
void drawPayline(int reel) {
for (int side = 0; side < 2; side++) {
uint8_t col0 = side == 0 ? 38 : 84;
window(reel, col0, col0 + 5, 2, 3);
for (uint8_t page = 2; page < 4; page++) {
for (uint8_t i = 0; i < 6; i++) {
uint8_t depth = side == 0 ? 6 - i : i + 1; // the arrow points in
uint8_t out = 0;
for (uint8_t bit = 0; bit < 8; bit++) {
int row = page * 8 + bit;
int from = 24 - depth;
if (row >= from && row < 24 + depth - 1) out |= 1 << bit;
}
busWrite(out);
}
}
busStop();
}
}
// One line of text along the bottom two pages, at twice the font's size.
uint8_t stretch(uint8_t nibble) {
uint8_t out = 0;
for (uint8_t i = 0; i < 4; i++) {
if (nibble & (1 << i)) out |= 3 << (i * 2);
}
return out;
}
void drawText(int reel, const char *text) {
int len = strlen(text);
int width = len * 12 - 2;
int left = (128 - width) / 2;
window(reel, 0, 127, 6, 7);
for (uint8_t page = 0; page < 2; page++) {
for (int x = 0; x < 128; x++) {
int at = x - left;
uint8_t out = 0;
if (at >= 0 && at < width && at % 12 < 10) {
char ch = text[at / 12];
if (ch >= 'a' && ch <= 'z') ch -= 32;
if (ch >= ' ' && ch <= 'Z') {
uint8_t column = pgm_read_byte(&FONT[(ch - ' ') * 5 + (at % 12) / 2]);
out = stretch(page == 0 ? column & 0x0f : column >> 4);
}
}
busWrite(out);
}
}
busStop();
}
// ---- Sound ----------------------------------------------------------------
struct Note { uint16_t hz; uint16_t ms; };
const Note T_COIN[] = {{1319, 70}, {1760, 140}, {0, 0}};
const Note T_BET[] = {{900, 25}, {0, 0}};
const Note T_EMPTY[] = {{196, 120}, {147, 220}, {0, 0}};
const Note T_LOSE[] = {{330, 90}, {262, 160}, {0, 0}};
const Note T_SMALL[] = {{784, 80}, {1047, 160}, {0, 0}};
const Note T_WIN[] = {{523, 80}, {659, 80}, {784, 80}, {1047, 80}, {784, 80}, {1047, 240}, {0, 0}};
const Note T_JACKPOT[] = {{523, 90}, {659, 90}, {784, 90}, {1047, 180}, {784, 90}, {1047, 90},
{1319, 90}, {1568, 400}, {0, 0}};
const Note *tune = nullptr;
unsigned long noteEnds = 0;
void play(const Note *t) {
tune = t;
noteEnds = 0;
}
void serviceSound() {
if (tune == nullptr || millis() < noteEnds) return;
if (tune->ms == 0) {
noTone(SPEAKER);
tune = nullptr;
return;
}
tone(SPEAKER, tune->hz);
noteEnds = millis() + tune->ms;
tune++;
}
// ---- Buttons and the lever --------------------------------------------------
const uint8_t INPUTS[4] = {LEVER, COIN, BET_UP, BET_DOWN};
bool held[4];
bool pressed[4];
unsigned long lockedUntil[4];
void readInputs() {
unsigned long now = millis();
for (int i = 0; i < 4; i++) {
pressed[i] = false;
if (now < lockedUntil[i]) continue;
uint8_t bit = (uint8_t)(1 << INPUTS[i]);
bool down = digitalRead(INPUTS[i]) == LOW || (sawDown & bit);
sawDown &= (uint8_t)~bit;
if (down != held[i]) {
held[i] = down;
lockedUntil[i] = now + 10;
pressed[i] = down;
}
}
}
// ---- The machine --------------------------------------------------------
Adafruit_NeoPixel lights(8, LIGHTS_PIN, NEO_GRB + NEO_KHZ800);
enum State { IDLE, SPINNING, PAYING };
State state = IDLE;
unsigned long stateAt = 0;
int credits = 0;
int bet = 1;
int lastWin = 0;
int winPay = 0;
uint8_t target[3];
bool reelStopped[3];
const int STEP = 16; // rows the reel moves per frame while it spins
void saveCredits() {
EEPROM.update(0, 'S');
EEPROM.put(1, (int16_t)credits);
}
void loadCredits() {
if (EEPROM.read(0) == 'S') {
int16_t saved = 0;
EEPROM.get(1, saved);
if (saved >= 0 && saved <= MAX_CREDITS) credits = saved;
}
}
char line[12];
void showCredits() {
snprintf(line, sizeof(line), "CR %d", credits);
drawText(0, line);
}
void showBet() {
snprintf(line, sizeof(line), "BET %d", bet);
drawText(1, line);
}
void showStatus() {
if (state == IDLE && credits < bet) drawText(2, "COIN");
else if (state == IDLE && lastWin == 0) drawText(2, "PULL");
else if (state == SPINNING) drawText(2, "GOOD LUCK");
else {
snprintf(line, sizeof(line), "WIN %d", lastWin);
drawText(2, line);
}
}
void spin() {
credits -= bet;
saveCredits();
for (int r = 0; r < 3; r++) {
target[r] = random(STOPS);
reelStopped[r] = false;
}
lastWin = 0;
state = SPINNING;
stateAt = millis();
Serial.print("spin: bet ");
Serial.print(bet);
Serial.print(", credits ");
Serial.println(credits);
showCredits();
showStatus();
}
void settle() {
uint8_t a = STRIP[target[0]], b = STRIP[target[1]], c = STRIP[target[2]];
const char *name;
winPay = payFor(a, b, c, &name);
lastWin = winPay * bet;
credits += lastWin;
if (credits > MAX_CREDITS) credits = MAX_CREDITS;
saveCredits();
Serial.print("reels: ");
Serial.print(NAMES[a]);
Serial.print(" ");
Serial.print(NAMES[b]);
Serial.print(" ");
Serial.println(NAMES[c]);
if (lastWin > 0) {
Serial.print("win ");
Serial.print(lastWin);
Serial.print(" (");
Serial.print(name);
Serial.print(" pays ");
Serial.print(winPay);
Serial.print(" x bet ");
Serial.print(bet);
Serial.println(")");
play(winPay >= 40 ? T_JACKPOT : winPay >= 5 ? T_WIN : T_SMALL);
} else {
Serial.println("no win");
play(T_LOSE);
}
Serial.print("credits ");
Serial.println(credits);
state = PAYING;
stateAt = millis();
showCredits();
showStatus();
}
void spinning() {
unsigned long t = millis() - stateAt;
bool allStopped = true;
for (int r = 0; r < 3; r++) {
if (reelStopped[r]) continue;
allStopped = false;
int goal = stoppedPos(target[r]);
int left = (goal - reelPos[r] + STRIP_ROWS) % STRIP_ROWS;
// Each reel spins for a set time, the next one half a second longer, and
// then runs on until its stop comes round to the pay line.
if (t >= 900 + 500UL * r && left <= STEP) {
reelPos[r] = goal;
reelStopped[r] = true;
tone(SPEAKER, 140, 50); // the clunk of a reel dropping into place
} else {
reelPos[r] = (reelPos[r] + STEP) % STRIP_ROWS;
if (r == 0 || reelStopped[r - 1]) tone(SPEAKER, 2400, 3); // the tick
}
drawReel(r);
}
if (allStopped) settle();
}
// ---- Lights -------------------------------------------------------------
unsigned long lightsAt = 0;
void serviceLights() {
unsigned long now = millis();
if (now - lightsAt < 50) return;
lightsAt = now;
lights.clear();
if (state == SPINNING) {
int at = (now / 50) % 8;
lights.setPixelColor(at, 255, 255, 255);
lights.setPixelColor((at + 4) % 8, 255, 255, 255);
} else if (state == PAYING && lastWin > 0) {
bool on = (now / 120) % 2 == 0;
for (int i = 0; i < 8; i++) {
if (winPay >= 40) lights.setPixelColor(i, Adafruit_NeoPixel::ColorHSV((uint16_t)(now * 60 + i * 8192)));
else if (winPay >= 5) lights.setPixelColor(i, ((i + now / 80) % 3 == 0) ? 0xffc000 : 0x000000);
else if (on) lights.setPixelColor(i, 0x00ff40);
}
} else if (state == IDLE && credits < bet) {
// No credit: a slow red pulse that asks for a coin.
uint8_t level = (now / 400) % 2 ? 80 : 10;
for (int i = 0; i < 8; i++) lights.setPixelColor(i, level, 0, 0);
} else {
// Ready: a soft amber marquee.
for (int i = 0; i < 8; i++) {
bool lit = (i + now / 200) % 4 == 0;
lights.setPixelColor(i, lit ? 255 : 40, lit ? 140 : 20, 0);
}
}
lights.show();
}
void setup() {
for (int i = 0; i < 4; i++) pinMode(INPUTS[i], INPUT_PULLUP);
Serial.begin(115200);
lights.begin();
lights.setBrightness(90);
loadCredits();
for (int r = 0; r < 3; r++) {
oledBegin(r);
reelPos[r] = stoppedPos(r * 5);
drawPayline(r);
drawReel(r);
}
showCredits();
showBet();
showStatus();
Serial.print("Slot machine ready: ");
Serial.print(credits);
Serial.println(" credits. Press COIN, then pull the lever.");
}
void loop() {
readInputs();
if (state == IDLE) {
if (pressed[1]) {
// Each coin also seeds the random numbers: the chip has no true
// randomness, so it takes the microsecond a person's hand happened to land on.
randomSeed(micros());
credits += CREDITS_PER_COIN;
if (credits > MAX_CREDITS) credits = MAX_CREDITS;
saveCredits();
play(T_COIN);
Serial.print("coin: credits ");
Serial.println(credits);
showCredits();
showStatus();
}
if (pressed[2] || pressed[3]) {
bet += pressed[2] ? 1 : -1;
if (bet < 1) bet = 1;
if (bet > MAX_BET) bet = MAX_BET;
play(T_BET);
Serial.print("bet ");
Serial.println(bet);
showBet();
showStatus();
}
if (pressed[0]) {
if (credits >= bet) {
spin();
} else {
play(T_EMPTY);
Serial.println("not enough credits: press COIN");
}
}
} else if (state == SPINNING) {
spinning();
} else if (millis() - stateAt > 2500) {
state = IDLE;
showStatus();
} else if (pressed[0] && credits >= bet) {
spin(); // a quick player can pull again while the win lights run
}
serviceSound();
serviceLights();
}
Parts list
11 parts, plus the jumper wires. Every one is in the editor's parts bin.
How it is wired
14 connections, pin by pin, read from the circuit itself. Each line is one set of pins joined together, by a jumper wire or a breadboard strip.
- Arduino Uno R3 pin 7; Piezo speaker pin 1
- Arduino Uno R3 pin 6; Addressable RGB LEDs pin DIN
- Arduino Uno R3 pin 5; Arcade button (1) pin 1
- Arduino Uno R3 pin 4; Arcade button (2) pin 1
- Arduino Uno R3 pin 3; Arcade button (3) pin 1
- Arduino Uno R3 pin 2; Arcade joystick lever pin DOWN
- Arduino Uno R3 pin 5V; Addressable RGB LEDs pin VDD; OLED display, 128x64 (1) pin VCC; OLED display, 128x64 (2) pin VCC; OLED display, 128x64 (3) pin VCC
- Ground: Arduino Uno R3 pin GND; Piezo speaker pin 2; Addressable RGB LEDs pin VSS; Arcade button (1) pin 2; Arcade button (2) pin 2; Arcade button (3) pin 2; Arcade joystick lever pin GND; OLED display, 128x64 (1) pin GND; OLED display, 128x64 (2) pin GND; OLED display, 128x64 (3) pin GND
- Arduino Uno R3 pin A0; OLED display, 128x64 (1) pin SDA
- Arduino Uno R3 pin A1; OLED display, 128x64 (1) pin SCL
- Arduino Uno R3 pin A2; OLED display, 128x64 (2) pin SDA
- Arduino Uno R3 pin A3; OLED display, 128x64 (2) pin SCL
- Arduino Uno R3 pin A4; OLED display, 128x64 (3) pin SDA
- Arduino Uno R3 pin A5; OLED display, 128x64 (3) pin SCL
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.