Retro Arcade Gaming Console
on Raspberry Pi Pico
Turn a Raspberry Pi Pico into a pocket arcade machine running Snake, Pong, Space Shooter, and Breakout — all on a 128×64 OLED display with real button controls, 8-bit sound effects, and MicroPython. Simulate everything in Wokwi first.
Hardware Components
All components are available in Wokwi — no physical hardware needed to get started. Internal pull-ups are used via MicroPython configuration.
Circuit Connections
All buttons use Pin.PULL_UP internally — no external resistors required.
4 Classic Arcade Games
- Grid logic
- Self-collision detection
- Random food generation
- Direction control
- Ball physics
- Paddle collision
- Score increment
- Velocity inversion
- Projectile management
- Enemy spawning
- AABB hit detection
- Life system
- Brick grid management
- Ball reflection physics
- Win-state detection
- Score scaling
Code Architecture
The project uses a clean state machine design with object-oriented game classes, running at 20 FPS (50ms sleep per frame).
State: menu
A button
State: playing
to menu
SnakeGame
PongGame
SpaceShooter
BreakoutGame
GameMenu
button_pressed()
beep()
game.update()
game.draw()
oled.show()
"double"
"game_over"
diagram.json
Paste this into Wokwi's diagram.json tab to auto-place and wire all components.
{
"version": 1,
"author": "Retro Gaming Console",
"editor": "wokwi",
"parts": [
{ "type": "wokwi-pi-pico", "id": "pico", "top": 0, "left": 0, "attrs": {} },
{
"type": "wokwi-ssd1306", "id": "oled1",
"top": -137.3, "left": 213.1,
"attrs": { "i2cAddress": "0x3C" }
},
{
"type": "wokwi-pushbutton", "id": "btn_up",
"top": -120, "left": -115.2,
"attrs": { "color": "blue", "label": "UP" }
},
{
"type": "wokwi-pushbutton", "id": "btn_down",
"top": -60, "left": -115.2,
"attrs": { "color": "blue", "label": "DOWN" }
},
{
"type": "wokwi-pushbutton", "id": "btn_left",
"top": -90, "left": -153.6,
"attrs": { "color": "blue", "label": "LEFT" }
},
{
"type": "wokwi-pushbutton", "id": "btn_right",
"top": -90, "left": -76.8,
"attrs": { "color": "blue", "label": "RIGHT" }
},
{
"type": "wokwi-pushbutton", "id": "btn_a",
"top": -90, "left": 57.6,
"attrs": { "color": "red", "label": "A" }
},
{
"type": "wokwi-pushbutton", "id": "btn_b",
"top": -90, "left": 124.8,
"attrs": { "color": "green", "label": "B" }
},
{
"type": "wokwi-buzzer", "id": "buzzer1",
"top": 120, "left": 100,
"attrs": { "volume": "0.6" }
}
],
"connections": [
[ "pico:GP0", "oled1:SDA", "green", [ "v0" ] ],
[ "pico:GP1", "oled1:SCL", "blue", [ "v0" ] ],
[ "pico:3V3(OUT)", "oled1:VCC", "red", [ "v0" ] ],
[ "pico:GND.8", "oled1:GND", "black", [ "v0" ] ],
[ "pico:GP10", "btn_up:1.l", "blue", [ "v0" ] ],
[ "btn_up:2.l", "pico:GND.1", "black", [ "v0" ] ],
[ "pico:GP11", "btn_down:1.l", "blue", [ "v0" ] ],
[ "btn_down:2.l", "pico:GND.2", "black", [ "v0" ] ],
[ "pico:GP12", "btn_left:1.l", "blue", [ "v0" ] ],
[ "btn_left:2.l", "pico:GND.3", "black", [ "v0" ] ],
[ "pico:GP13", "btn_right:1.l", "blue", [ "v0" ] ],
[ "btn_right:2.l","pico:GND.4", "black", [ "v0" ] ],
[ "pico:GP14", "btn_a:1.l", "red", [ "v0" ] ],
[ "btn_a:2.l", "pico:GND.5", "black", [ "v0" ] ],
[ "pico:GP15", "btn_b:1.l", "green", [ "v0" ] ],
[ "btn_b:2.l", "pico:GND.6", "black", [ "v0" ] ],
[ "pico:GP16", "buzzer1:1", "magenta", [ "v0" ] ],
[ "buzzer1:2", "pico:GND.7", "black", [ "v0" ] ]
],
"dependencies": {}
}
MicroPython Code (main.py)
Save this as main.py on your Pico, or paste into Wokwi's code editor.
from machine import Pin, I2C import time import framebuf import random # ── SSD1306 OLED Driver (128×64) ────────────────────────────── class SSD1306_I2C: def __init__(self, width, height, i2c, addr=0x3C): self.i2c = i2c; self.addr = addr self.width = width; self.height = height self.pages = height // 8 self.buffer = bytearray(self.pages * width) self.framebuf = framebuf.FrameBuffer( self.buffer, width, height, framebuf.MONO_VLSB) self.init_display() def init_display(self): for cmd in ( 0xAE, 0xD5, 0x80, 0xA8, 0x3F, 0xD3, 0x00, 0x40, 0x8D, 0x14, 0x20, 0x00, 0xA1, 0xC8, 0xDA, 0x12, 0x81, 0xCF, 0xD9, 0xF1, 0xDB, 0x40, 0xA4, 0xA6, 0xAF ): self.write_cmd(cmd) self.fill(0); self.show() def write_cmd(self, cmd): self.i2c.writeto(self.addr, bytearray([0x00, cmd])) def write_data(self, buf): self.i2c.writeto(self.addr, b'\x40' + buf) def show(self): for page in range(self.pages): self.write_cmd(0xB0 + page) self.write_cmd(0x00); self.write_cmd(0x10) self.write_data(self.buffer[page*self.width:(page+1)*self.width]) def fill(self, c): self.framebuf.fill(c) def pixel(self,x,y,c): self.framebuf.pixel(x,y,c) def text(self,s,x,y,c=1):self.framebuf.text(s,x,y,c) def rect(self,x,y,w,h,c): self.framebuf.rect(x,y,w,h,c) def fill_rect(self,x,y,w,h,c): self.framebuf.fill_rect(x,y,w,h,c) def line(self,x1,y1,x2,y2,c): self.framebuf.line(x1,y1,x2,y2,c) # ── Hardware Init ───────────────────────────────────────────── i2c = I2C(0, scl=Pin(1), sda=Pin(0), freq=400000) oled = SSD1306_I2C(128, 64, i2c) btn_up = Pin(10, Pin.IN, Pin.PULL_UP) btn_down = Pin(11, Pin.IN, Pin.PULL_UP) btn_left = Pin(12, Pin.IN, Pin.PULL_UP) btn_right = Pin(13, Pin.IN, Pin.PULL_UP) btn_a = Pin(14, Pin.IN, Pin.PULL_UP) btn_b = Pin(15, Pin.IN, Pin.PULL_UP) buzzer = Pin(16, Pin.OUT) # ── Sound Effects ───────────────────────────────────────────── def beep(duration=0.05, pattern="single"): if pattern == "single": buzzer.on(); time.sleep(duration); buzzer.off() elif pattern == "double": for _ in range(2): buzzer.on(); time.sleep(0.05) buzzer.off(); time.sleep(0.05) elif pattern == "game_over": for f in [0.1, 0.1, 0.15]: buzzer.on(); time.sleep(f) buzzer.off(); time.sleep(0.05) # ── Button Debouncing ───────────────────────────────────────── last_press = {"up":0, "down":0, "left":0, "right":0, "a":0, "b":0} debounce_time = 150 # ms def button_pressed(name): now = time.ticks_ms() if time.ticks_diff(now, last_press[name]) > debounce_time: last_press[name] = now; return True return False def read_buttons(): return { "up": btn_up.value() == 0, "down": btn_down.value() == 0, "left": btn_left.value() == 0, "right": btn_right.value() == 0, "a": btn_a.value() == 0, "b": btn_b.value() == 0, } # ── SNAKE ───────────────────────────────────────────────────── class SnakeGame: def __init__(self): self.reset() def reset(self): self.snake = [[64,32],[60,32],[56,32]] self.direction = [4,0] self.food = [random.randint(0,30)*4, random.randint(0,14)*4] self.score = 0; self.game_over = False def update(self, buttons): if self.game_over: return dx, dy = self.direction if buttons["up"] and dy == 0: self.direction = [0,-4] elif buttons["down"] and dy == 0: self.direction = [0,4] elif buttons["left"] and dx == 0: self.direction = [-4,0] elif buttons["right"] and dx == 0: self.direction = [4,0] head = [self.snake[0][0]+self.direction[0], self.snake[0][1]+self.direction[1]] if head[0] < 0 or head[0] >= 128 or head[1] < 0 or head[1] >= 64: self.game_over = True; beep(0.1, "game_over"); return if head in self.snake: self.game_over = True; beep(0.1, "game_over"); return self.snake.insert(0, head) if head == self.food: self.score += 10; beep(0.05, "double") self.food = [random.randint(0,30)*4, random.randint(0,14)*4] else: self.snake.pop() def draw(self): oled.fill(0) for s in self.snake: oled.fill_rect(s[0],s[1],4,4,1) oled.fill_rect(self.food[0],self.food[1],4,4,1) oled.text(f"Score:{self.score}",0,0) if self.game_over: oled.fill_rect(20,25,88,20,0); oled.rect(20,25,88,20,1) oled.text("GAME OVER",30,30) oled.show() # ── PONG ────────────────────────────────────────────────────── class PongGame: def __init__(self): self.reset() def reset(self): self.paddle_y = 24; self.ball_x = 64; self.ball_y = 32 self.ball_dx = 2; self.ball_dy = 1 self.score = 0; self.game_over = False def update(self, buttons): if self.game_over: return if buttons["up"] and self.paddle_y > 0: self.paddle_y -= 3 if buttons["down"] and self.paddle_y < 48: self.paddle_y += 3 self.ball_x += self.ball_dx; self.ball_y += self.ball_dy if self.ball_y <= 0 or self.ball_y >= 62: self.ball_dy *= -1; beep(0.03) if self.ball_x <= 8 and self.paddle_y <= self.ball_y <= self.paddle_y+16: self.ball_dx *= -1; self.score += 1; beep(0.05) if self.ball_x < 0: self.game_over = True; beep(0.1, "game_over") if self.ball_x >= 126: self.ball_dx *= -1 def draw(self): oled.fill(0) oled.fill_rect(2,self.paddle_y,4,16,1) oled.fill_rect(self.ball_x,self.ball_y,2,2,1) oled.text(f"Score:{self.score}",40,0) if self.game_over: oled.fill_rect(20,25,88,20,0); oled.rect(20,25,88,20,1) oled.text("GAME OVER",30,30) oled.show() # ── SPACE SHOOTER ───────────────────────────────────────────── class SpaceShooter: def __init__(self): self.reset() def reset(self): self.player_x = 60; self.bullets = []; self.score = 0 self.enemies = [[random.randint(0,120), random.randint(-30,-10)] for _ in range(3)] self.lives = 3; self.game_over = False def update(self, buttons): if self.game_over: return if buttons["left"] and self.player_x > 0: self.player_x -= 3 if buttons["right"] and self.player_x < 120: self.player_x += 3 if buttons["a"] and button_pressed("a"): self.bullets.append([self.player_x+3, 50]); beep(0.03) for b in self.bullets[:]: b[1] -= 4 if b[1] < 0: self.bullets.remove(b) for e in self.enemies: e[1] += 1 if e[1] > 64: e[0] = random.randint(0,120); e[1] = random.randint(-30,-10) self.lives -= 1; beep(0.1) if self.lives <= 0: self.game_over = True; beep(0.1, "game_over") for b in self.bullets[:]: for e in self.enemies[:]: if abs(b[0]-e[0]) < 6 and abs(b[1]-e[1]) < 6: self.bullets.remove(b) e[0] = random.randint(0,120); e[1] = random.randint(-30,-10) self.score += 10; beep(0.05, "double"); break def draw(self): oled.fill(0) oled.fill_rect(self.player_x,54,8,6,1); oled.pixel(self.player_x+4,52,1) for b in self.bullets: oled.fill_rect(b[0],b[1],2,4,1) for e in self.enemies: oled.fill_rect(e[0],e[1],6,6,1) oled.text(f"S:{self.score}",0,0); oled.text(f"L:{self.lives}",100,0) if self.game_over: oled.fill_rect(20,25,88,20,0); oled.rect(20,25,88,20,1) oled.text("GAME OVER",30,30) oled.show() # ── BREAKOUT ────────────────────────────────────────────────── class BreakoutGame: def __init__(self): self.reset() def reset(self): self.paddle_x = 52; self.ball_x = 64; self.ball_y = 50 self.ball_dx = 2; self.ball_dy = -2 self.bricks = [[x*16, y*6+10] for x in range(8) for y in range(4)] self.score = 0; self.game_over = False; self.won = False def update(self, buttons): if self.game_over or self.won: return if buttons["left"] and self.paddle_x > 0: self.paddle_x -= 4 if buttons["right"] and self.paddle_x < 104: self.paddle_x += 4 self.ball_x += self.ball_dx; self.ball_y += self.ball_dy if self.ball_x <= 0 or self.ball_x >= 126: self.ball_dx *= -1; beep(0.03) if self.ball_y <= 0: self.ball_dy *= -1; beep(0.03) if (self.paddle_x <= self.ball_x <= self.paddle_x+24 and 58 <= self.ball_y <= 60): self.ball_dy *= -1; beep(0.05) if self.ball_y > 64: self.game_over = True; beep(0.1, "game_over") for brick in self.bricks[:]: if (brick[0] <= self.ball_x <= brick[0]+14 and brick[1] <= self.ball_y <= brick[1]+4): self.bricks.remove(brick); self.ball_dy *= -1 self.score += 5; beep(0.04) if not self.bricks: self.won = True; beep(0.1, "double") break def draw(self): oled.fill(0) oled.fill_rect(self.paddle_x,58,24,4,1) oled.fill_rect(self.ball_x,self.ball_y,2,2,1) for b in self.bricks: oled.rect(b[0],b[1],14,4,1) oled.text(f"Score:{self.score}",0,0) if self.game_over: oled.fill_rect(20,25,88,20,0); oled.rect(20,25,88,20,1) oled.text("GAME OVER",30,30) elif self.won: oled.fill_rect(20,25,88,20,0); oled.rect(20,25,88,20,1) oled.text("YOU WIN!",35,30) oled.show() # ── GAME MENU ───────────────────────────────────────────────── class GameMenu: def __init__(self): self.games = ["Snake", "Pong", "Shooter", "Breakout"] self.selected = 0 def update(self, buttons): if buttons["up"] and button_pressed("up"): self.selected = (self.selected - 1) % len(self.games); beep(0.03) if buttons["down"] and button_pressed("down"): self.selected = (self.selected + 1) % len(self.games); beep(0.03) if buttons["a"] and button_pressed("a"): beep(0.05, "double"); return self.selected return None def draw(self): oled.fill(0); oled.rect(0,0,128,64,1) oled.text("RETRO ARCADE",20,5); oled.line(0,15,128,15,1) for i, game in enumerate(self.games): y = 22 + i * 10 oled.text(f"{'>' if i==self.selected else ' '}{game}", 15, y) oled.show() # ── MAIN LOOP ───────────────────────────────────────────────── print("=" * 40); print("🎮 RETRO GAMING CONSOLE"); print("=" * 40) print("\nD-Pad: UP/DOWN/LEFT/RIGHT | A=Select | B=Back\n") oled.fill(0) oled.text("RETRO ARCADE",25,20); oled.text("Loading...",30,35) oled.show(); time.sleep(1) menu = GameMenu(); current_game = None; game_mode = "menu" try: while True: buttons = read_buttons() if game_mode == "menu": selected = menu.update(buttons); menu.draw() if selected is not None: current_game = [SnakeGame, PongGame, SpaceShooter, BreakoutGame][selected]() game_mode = "playing" print(f"🎮 Starting {menu.games[selected]}...") elif game_mode == "playing": current_game.update(buttons); current_game.draw() if buttons["b"] and button_pressed("b"): game_mode = "menu"; beep(0.05) print("📋 Back to menu...") time.sleep(0.05) # 20 FPS except KeyboardInterrupt: print("\n🛑 Shutting down...") oled.fill(0); oled.text("GAME OVER",35,28); oled.show() buzzer.off()
What You'll Learn
Run the Simulation
Copy into Wokwi's diagram tab to auto-wire all components.
Copy the full code into Wokwi's code editor.
Press the green start button — the OLED boot animation appears.
UP/DOWN to select a game, A to launch it.
Use the 6 virtual buttons. B returns to the main menu.
Comments
Post a Comment