Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

You can build a playable Sokoban-style push-box puzzle with a classic 5 V Arduino Nano, a 128×64 I²C SSD1306 OLED, and four buttons. The key rule is that you can push a box but cannot pull it: a push is legal only when the square beyond the box is open. This guide covers the wiring, display setup, game logic, a compact working sketch, and the common faults that can stop the game from running.

It targets the classic Arduino Nano / Nano 3.x with an ATmega328P, not every board sold under the Nano name. Newer Nano-family boards can use different processors, voltages, and pin mappings; check the Arduino Nano family overview before adapting the wiring or code.

What you need

  • Classic Arduino Nano or a compatible ATmega328P Nano.
  • 128×64 I²C SSD1306 monochrome OLED.
  • Four momentary push buttons for up, down, left, and right.
  • Breadboard, jumper wires, and a data-capable USB Mini-B cable for the classic Nano.
  • Optional fifth button for reset, a buzzer, and an enclosure.

The classic Nano is a 16 MHz, 5 V board with 32 KB flash and 2 KB SRAM. Its small memory matters because a 128×64 monochrome framebuffer alone takes 1,024 bytes (128 × 64 ÷ 8), before the library, game state, and stack are counted. See the Arduino Nano documentation for board details. The framebuffer calculation is its raw pixel-buffer size, not a claim about total library RAM use.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

For a first build, Adafruit SSD1306 and Adafruit GFX are a straightforward display-library combination. A page-buffer library such as U8g2 can reduce RAM pressure, but changes the rendering approach; the sketch below uses Adafruit’s full-framebuffer API.

#1 Best Overall
LAFVIN Project Super Starter Kit for R3 Mega2560 Mega328 Nano with Tutorial Compatible with Arduino IDE
  • Perfect choice for beginners to learn, electronics and program.
  • This kit with tutorial user manual containing more than 20 lessons,code,Libraries, datasheets, and so on.
  • 100% Compatible with program.
  • Inlcude type motors and LCDs with servo motor, stepper motor and DC Motor; LCD 1602, LCD 4-bit 7-segment Display etc.
  • LCD 1602 module with pin header (not need to be soldered by yourself)

Wire the OLED and buttons

OLED connections

OLED pin Classic Nano
GND GND
VCC or VIN 5V only if that specific module is rated for 5 V
SDA A4
SCL A5
RST, if present Leave unconnected when using reset value -1, or wire to a free digital pin and configure it in the library

The Nano’s I²C pins are A4 (SDA) and A5 (SCL); Adafruit shows the same mapping for Uno/Nano-style ATmega328 boards in its 128×64 OLED wiring guide. Do not assume an unbranded OLED accepts 5 V: modules vary in regulator and level-shifting circuitry. Check its markings or manufacturer documentation first. Adafruit’s breakout overview describes specific 5 V-ready products, not every SSD1306 module.

Many I²C OLEDs respond at 0x3C or 0x3D, but the address depends on the module and its configuration. Do not treat either value as universal; scan for the address and set it in the sketch.

Rank #2
ELEGOO UNO R3 Project Super Starter Kit with PDF Tutorial for Beginners
  • TURN CODE INTO REAL-WORLD RESULTS — Follow 22+ guided lessons to make LEDs blink, read temperature and distance, move servo and stepper motors, control an LCD and respond to joystick or IR input; ideal for a family weekend build, homeschool unit, coding club or STEM classroom
  • MORE PROJECT VARIETY IN ONE ORGANIZED KIT — Includes the UNO R3 controller, LCD1602 with pre-soldered header, breadboard power module, ultrasonic and DHT11 sensors, joystick, IR receiver and remote, SG90 servo, stepper motor, relay, DC motor, fan blade, displays, LEDs, buttons, resistors and jumper wires
  • START WITHOUT SOLDERING — Plug-in modules, a solderless breadboard and the pre-soldered LCD help beginners focus on wiring, code and testing; the illustrated component list makes it easier to find each part and move from one lesson to the next
  • LEARN THE LOGIC, THEN CREATE YOUR OWN — Use Arduino IDE and the included example code to understand digital input and output, analog sensing, timing, motor control and display functions, then change thresholds, speeds and sequences for alarms, environmental monitors, reaction games and motion projects
  • CLEAR SETUP SUPPORT FOR FIRST-TIME BUILDERS — Download the latest tutorial and code, select the UNO board and correct computer port, check component polarity and breadboard rows, and keep power-module input at 9V or below; younger learners should work with an experienced adult

Button connections

Connect one side of each button to a digital input and the other side to GND. The code uses the Nano’s internal pull-ups, so external resistors are not needed:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Button Nano pin
Up D2
Down D3
Left D4
Right D5

With INPUT_PULLUP, an idle button reads HIGH and a pressed button reads LOW. A small debounce delay prevents one physical press from registering repeatedly; the sketch waits for release so one press makes one move.

Rank #3
ELEGOO Mega 2560 R3 Project The Most Complete Starter Kit with Tutorial
  • 35+ Guided Electronics Projects: Progress from LEDs and buttons to RFID access, real-time clocks, motion and distance sensing, environmental monitoring, motor control and interactive displays for STEM learning, coding clubs and maker projects
  • More I/O and Memory for Larger Builds: The MEGA 2560 R3 provides 54 digital I/O pins, including 15 PWM outputs, 16 analog inputs, 4 hardware serial ports and 256 KB flash for projects that combine more sensors, controls and displays
  • 200+ Components for Prototyping: Includes LCD1602, RC522 RFID, RTC, DHT11, HC-SR501 PIR, ultrasonic and water-level sensors, GY-521, MAX7219, keypad, joystick, rotary encoder, relay, SG90 servo, stepper motor, DC motor, breadboard and more
  • Learn, Modify and Create: Follow 35+ guided lessons with example code, then adjust sensor thresholds, timing, display text, motor behavior and control logic to turn structured exercises into access systems, monitors, alarms and interactive projects
  • Organized for Repeatable Learning: Pre-soldered modules, a solderless breadboard, storage case and small-parts box reduce setup time and keep sensors, LEDs, ICs, wires and other components easy to find between projects

Install and check the display library

  1. Install the current Arduino IDE from Arduino Software.
  2. Connect the Nano, then choose Tools → Board → Arduino AVR Boards → Arduino Nano and the correct port under Tools → Port.
  3. Start with Tools → Processor → ATmega328P. If upload fails on an older board, try ATmega328P (Old Bootloader); some third-party boards use ATmega168. Arduino explains these choices in its Nano processor-selection guide.
  4. Open Sketch → Include Library → Manage Libraries, search for Adafruit SSD1306, and install it along with Adafruit GFX Library. Adafruit documents installation and examples here.
  5. Before adding game code, open File → Examples → Adafruit SSD1306 → SSD1306_128x64_i2c, confirm the address and upload the example.

If the display is not detected, upload this scanner, open Serial Monitor at 115200 baud, and note the address it reports:

#include <Wire.h>

void setup() {
  Wire.begin();
  Serial.begin(115200);
  delay(1000);
  Serial.println("I2C scanner");
}

void loop() {
  byte count = 0;
  for (byte address = 1; address < 127; address++) {
    Wire.beginTransmission(address);
    byte error = Wire.endTransmission();
    if (error == 0) {
      Serial.print("Found 0x");
      if (address < 16) Serial.print('0');
      Serial.println(address, HEX);
      count++;
    }
  }
  if (count == 0) Serial.println("No I2C devices found");
  delay(3000);
}

Set SCREEN_ADDRESS in the game to the detected address. If the scanner finds nothing, check power, ground, SDA/SCL order, loose connections, and whether the module is actually I²C rather than SPI.

Rank #4
Smraza 298 Pieces Electronics Starter Kit with Breadboard for Arduino
  • Smraza Electronics Fun Kit - It has all consumable component are often used. Compatible with Arduino and Raspberry Pi, it can almost meet all your needs. Not included controller board.
  • A Breadboard and Power Supply Module -Include a good range of LEDs, resistors, buttons, capacitors, a few transistors and diodes.
  • With jumper wire and Male-female dupont wire to meet your project expetation.
  • All parts components are in a sturdy and nice storage box which can help you keep the components neat after using.
  • With Datasheet and Tutorial - We provide detailed instruction for you to begin your electronic projects, any questions, please contact our customer service.

Represent the board as terrain plus objects

A Sokoban board has static terrain (walls, floor, and target squares) and movable objects (player and boxes). Keeping them separate prevents a box or player from erasing the target beneath it. Each attempted move checks the next cell: a wall blocks movement; an empty walkable cell moves the player; a box can move only if the cell beyond it is walkable and unoccupied. Boxes cannot be pulled.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The example board is 16 cells wide and 7 high. At 8 pixels per cell, the board occupies 128×56 pixels, leaving the bottom 8 pixels for a move counter. The two boxes start directly below their respective targets, so the sample level is solvable by pushing each box upward. Its terrain is stored in flash with PROGMEM; the player and box positions are separate mutable data.

Best Value
Arduino Nano ESP32 with Headers [ABX00083] - ESP32-S3, USB-C, Wi-Fi, Bluetooth, HID Support, MicroPython Compatible for IoT & Embedded Projects
  • Powerful ESP32-S3 Microcontroller: The Arduino Nano ESP32 is powered by the ESP32-S3 chip, featuring a dual-core Xtensa 32-bit LX7 processor running at up to 240 MHz. This high-performance microcontroller offers excellent computational power for IoT, wireless communication, and advanced embedded applications like real-time data processing, voice recognition, and machine learning at the edge.
  • Comprehensive Wireless Connectivity: The board supports both Wi-Fi and Bluetooth 5.0, enabling seamless communication with other devices, networks, and cloud platforms. Whether you're building a smart home system, wearable tech, or remote sensors, the Nano ESP32 offers reliable and high-speed connectivity for wireless data transfer and control.
  • USB-C for Power and Programming: With the modern USB-C port, the Nano ESP32 ensures faster programming, better power delivery, and a more stable connection compared to traditional micro-USB boards. This makes it easier to work with, especially in development and prototyping stages.
  • HID Support for Advanced Applications: The board supports Human Interface Device (HID) profiles, making it ideal for projects that require integration with keyboards, mice, or other HID peripherals. This feature allows you to create custom input devices, virtual controllers, or even USB-based projects that interact directly with computers and other devices.
  • MicroPython Compatible: The Arduino Nano ESP32 is compatible with MicroPython, a streamlined version of Python designed for embedded systems. This makes the board perfect for rapid prototyping, educational projects, and developers who prefer Python over C/C++ for ease of use and faster development cycles.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Upload the game

Set SCREEN_ADDRESS to the scanner result before compiling. The sketch uses four active-low buttons, a 16×7 map, two boxes, and the Adafruit SSD1306/GFX libraries.

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <avr/pgmspace.h>

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C  // Change to the address found by the scanner

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

const byte LEVEL_WIDTH = 16;
const byte LEVEL_HEIGHT = 7;
const byte CELL = 8;
const byte BOX_COUNT = 2;
const byte BUTTON_UP = 2;
const byte BUTTON_DOWN = 3;
const byte BUTTON_LEFT = 4;
const byte BUTTON_RIGHT = 5;

enum Tile : byte { FLOOR, WALL, TARGET };
struct Position { int8_t x; int8_t y; };

// '#' is wall, '.' is target, and spaces are floor. Rows are 16 characters.
const char level[] PROGMEM =
  "################"
  "#              #"
  "#   .      .   #"
  "#              #"
  "#              #"
  "#              #"
  "################";

Position player = {8, 4};
Position boxes[BOX_COUNT] = {{4, 4}, {11, 4}};
const Position startPlayer = {8, 4};
const Position startBoxes[BOX_COUNT] = {{4, 4}, {11, 4}};
unsigned int moveCount = 0;
bool victoryShown = false;

bool inBounds(Position p) {
  return p.x >= 0 && p.x < LEVEL_WIDTH && p.y >= 0 && p.y < LEVEL_HEIGHT;
}

char mapAt(Position p) {
  if (!inBounds(p)) return '#';
  return (char)pgm_read_byte(&level[(byte)p.y * LEVEL_WIDTH + (byte)p.x]);
}

bool isWalkable(Position p) {
  return inBounds(p) && mapAt(p) != '#';
}

int findBox(Position p) {
  for (byte i = 0; i < BOX_COUNT; i++) {
    if (boxes[i].x == p.x && boxes[i].y == p.y) return i;
  }
  return -1;
}

bool solved() {
  for (byte i = 0; i < BOX_COUNT; i++) {
    if (mapAt(boxes[i]) != '.') return false;
  }
  return true;
}

bool tryMove(int8_t dx, int8_t dy) {
  Position next = {(int8_t)(player.x + dx), (int8_t)(player.y + dy)};
  if (!isWalkable(next)) return false;

  int boxIndex = findBox(next);
  if (boxIndex >= 0) {
    Position beyond = {(int8_t)(next.x + dx), (int8_t)(next.y + dy)};
    if (!isWalkable(beyond) || findBox(beyond) >= 0) return false;
    boxes[boxIndex] = beyond;
  }

  player = next;
  moveCount++;
  return true;
}

bool pressed(byte pin) {
  if (digitalRead(pin) != LOW) return false;
  delay(25);  // Simple switch debounce; use millis() for animation or sound projects.
  if (digitalRead(pin) != LOW) return false;
  while (digitalRead(pin) == LOW) delay(1);
  return true;
}

void drawGame() {
  display.clearDisplay();
  for (byte y = 0; y < LEVEL_HEIGHT; y++) {
    for (byte x = 0; x < LEVEL_WIDTH; x++) {
      Position p = {(int8_t)x, (int8_t)y};
      int px = x * CELL;
      int py = y * CELL;
      char tile = mapAt(p);

      if (tile == '#') {
        display.fillRect(px, py, CELL, CELL, SSD1306_WHITE);
      } else if (tile == '.') {
        display.drawCircle(px + 4, py + 4, 2, SSD1306_WHITE);
      }

      if (findBox(p) >= 0) {
        display.drawRect(px + 1, py + 1, 6, 6, SSD1306_WHITE);
        display.drawPixel(px + 3, py + 3, SSD1306_WHITE);
        display.drawPixel(px + 4, py + 4, SSD1306_WHITE);
      }
      if (player.x == p.x && player.y == p.y) {
        display.fillCircle(px + 4, py + 4, 3, SSD1306_WHITE);
      }
    }
  }

  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 57);
  display.print(F("Moves:"));
  display.print(moveCount);
  if (victoryShown) {
    display.setCursor(64, 57);
    display.print(F("SOLVED"));
  }
  display.display();
}

void resetLevel() {
  player = startPlayer;
  for (byte i = 0; i < BOX_COUNT; i++) boxes[i] = startBoxes[i];
  moveCount = 0;
  victoryShown = false;
  drawGame();
}

void setup() {
  pinMode(BUTTON_UP, INPUT_PULLUP);
  pinMode(BUTTON_DOWN, INPUT_PULLUP);
  pinMode(BUTTON_LEFT, INPUT_PULLUP);
  pinMode(BUTTON_RIGHT, INPUT_PULLUP);

  if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    while (true) { }  // Check wiring, address, and display type if initialization fails.
  }
  drawGame();
}

void loop() {
  if (victoryShown) return;

  int8_t dx = 0;
  int8_t dy = 0;
  if (pressed(BUTTON_UP)) dy = -1;
  else if (pressed(BUTTON_DOWN)) dy = 1;
  else if (pressed(BUTTON_LEFT)) dx = -1;
  else if (pressed(BUTTON_RIGHT)) dx = 1;

  if ((dx != 0 || dy != 0) && tryMove(dx, dy)) {
    if (solved()) victoryShown = true;
    drawGame();
  }
}

For this fixed level, the sketch has one player and two boxes and contains two targets; the equal counts are deliberate. Victory is checked by confirming that every box occupies a target. If you change the level, preserve the board dimensions and make sure the number of targets matches the boxes. A visually valid map is not necessarily solvable: check that each box can be pushed to a target and avoid trapping one in a non-target corner.

Change the board or add levels

Every row in the sample has 16 characters, and there are seven rows. The outer wall keeps the player and boxes inside; the movement routine also checks bounds, so a missing wall cannot make an out-of-range map read. The board uses # for wall, . for target, and spaces for floor. Player and box coordinates are initialized separately.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

For another level, update the terrain string, starting player position, box count, and starting box positions together. Validate each level for exactly one player, equal box and target counts, valid characters, and a path to a solution. If you store a larger collection of maps in flash, AVR PROGMEM data must be read with functions such as pgm_read_byte(); it cannot always be treated as an ordinary RAM string.

The map symbols for walls and targets are drawn in white on black. Boxes use an outline and player a filled circle, so they remain distinguishable on a monochrome screen; a target remains visible beneath either object. Larger cells improve legibility but limit board size, while smaller cells permit more of a level at the cost of harder-to-read symbols.

Troubleshoot the common failures

  • Upload fails with programmer-not-responding or avrdude: stk500_recv(): recheck the port and board selection, close Serial Monitor, try the old-bootloader processor setting, disconnect external wiring during upload, and use a known data-capable USB cable.
  • Compiler cannot find Adafruit_SSD1306.h: install Adafruit SSD1306 and its Adafruit GFX dependency through Library Manager, then restart the IDE if needed.
  • OLED stays blank or initialization fails: run the scanner, set the detected address, verify A4/A5 and ground, and confirm the module’s voltage requirements and geometry.
  • Only part of the display appears: the physical module may not be 128×64, or its controller may be SH1106 rather than SSD1306. Check the module documentation and use a matching library constructor.
  • Buttons make extra moves: confirm each button connects its pin to GND, not 5V, and that the input uses INPUT_PULLUP. Increase the debounce interval modestly if the switches bounce heavily.
  • Random resets or corrupted graphics: check power and breadboard connections, remove high-current accessories while diagnosing, avoid large dynamic allocations and extensive String use, and remember that the full display buffer already uses 1,024 bytes.

Extensions that fit the project

  • Reset: add a fifth active-low button and call resetLevel() on a press.
  • Multiple levels: store each map and its dimensions and box count, then load its player and box starting positions on reset.
  • Undo: save the previous player and box coordinates before each accepted move; keep history bounded to protect SRAM.
  • Joystick: use analog thresholds, a calibrated dead zone, and a deliberate repeat rate. A held joystick should not unintentionally generate rapid moves.
  • Sound or animation: replace the blocking debounce with non-blocking timing based on millis().
  • Memory-conscious display: consider U8g2 page-buffer rendering if the game grows. Arduino’s U8g2 library listing describes SSD1306 support and page-buffer options; it has a different API from Adafruit SSD1306.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.