Main source of inspiration
TICO, an XO robot
My project, named Ludus, is an innovative twist on the classic game of Tic-Tac-Toe. It uses joystick modules and an 8×8 NeoPixel RGB LED matrix to create an interactive experience where the game is played against another player or against a smart, automated system. The project transforms a simple game into an engaging, high-tech experience to make decisions and move game pieces. I’m deeply passionate about this project because it combines my interests in robotics and games, allowing me to apply my skills in programming, electronics, and robotics in a fun and practical way.
Main source of inspiration is of a previous experience working as STEM instructor my students wanted to build a Tic Tac Toe game using Arduino using buttons and RGB LEDs as shown in the video attached. So not only inspired by similar projects that leverage basic AI and robotics but also by the fact that we can replace the huge number of buttons and LEDs to create interactive experiences that elevates traditional gameplay. For further context, you can find similar projects on platforms like Instructables, where enthusiasts have built robots to play Tic-Tac-Toe using basic electronics and Arduino code.
LaserCAD
I used a combination of Fusion 360 and LaserCAD to craft Ludus in a cabinet/arcade shape.
Software:
Fusion 360 for its robust design capabilities, which allowed me to assemble detailed and precise bodies of the holder. Moreover, creating all the components of my design and join them together enabled me to have a better vision of how it would look like after the assembly.
GrabCAD: A website from which electronic components were added to my project on Fusion360.
LaserCAD: used to enable precise cutting and speed cutting , which is essential for creating detailed and accurate designs the game logo.
Convertio: A website used to convert my chosen decoration pictures of JPEG or PNG extensions to .DXF
1. Design Process in Fusion 360
I started a new design by applying rule number #1 by saving my file and creating a new component for the arcade sides.
Using the Rectangle tool, I drew one of dimensions (200*60) mm
I used the Line tool to drew a line of (160 mm) on which the Neopixel and OLED will be mounted. Using the Line I extended the right side of the rectangle to close the shape of the arcade side.
Using the Rectangle Tool (R) to draw tabs of dimensions (20*3) mm to fit in the base and the back sides.
Using the Rectangle tool, I drew to rectangles of shown dimensions to form the T-slot. I've sketched two T-slots for both base and back sides.
Using the Circle and Offset tools to form the shown hinge to have an easy access to the electronic kit inside the prototype.
Using the Mirror tool to duplicate that face to form the other side but deleting one of the tabs of the base and forming the Arduino openings for mounting purposes as shown.
Extruding the sketch with Extrude tool with 3mm thickness then aligning them
Creating a new component for the Neopixel/OLED face, building a new sketch on the tabs of the side parts after manually aligning them and using Project tool to fit the shapes together.
Using the Rectangle tool to sketch different opening on the Neopixel/OLED face for Neopixel (69*69) mm, OLED (27*19) mm and two openings of dimensions (10*10) mm for Neopixel pins. Finally extruding the shape 3 mm as well.
Assembling those 3 parts together with the tool Joint to make sure that the projections are suitable.
Creating a new component for Joystick modules, building a new sketch on the tabs of the side parts and using Project tool to fit the shapes together.
Using the Rectangle, circle tools to sketch different opening for the Joysticks of shown dimensions and creating for circles for the Joystick module screws for mounting. I then extruded the body 3 mm.
Assembling the Joystick body with the sides with the tool Joint to make sure that the projections are fitting.
Creating a new component for Switch face.
Using the Rectangle tool to sketch a the switch face of shown dimensions and Circle tool for the switch opening. I then extruded the body 3 mm. A T-slot was sketched to attach the face to the base and duplicated with Mirror tool.
Assembling the Switch body with the sides with the tool Joint to make sure that the projections are fitting.
Creating a new component for the Arcade ceiling.
Using the Rectangle tool to sketch the part and Project tool to build all the slots to fit in the side parts tabs. I then extruded the body 3 mm.
Assembling the Ceiling body with the sides & Neopixel/OLED parts with the tool Joint to make sure that the projections are fitting.
Creating a new component for the back rigid part and called it Back1.
Using the Rectangle tool to sketch the part and Project tool to build all the slots to fit in the side parts tabs. I also used the Circle tool to project the T-slot openings of the side parts and then extruded the body 3 mm.
Assembling the Rigid Back body with the sides & Ceiling bodies with the tool Joint to make sure that the projections are fitting.
Creating a new component for the back hinge.
Using the Rectangle tool to sketch the part and Project tool to build all the slots to fit in the side parts tabs. Projecting on the tab but making the slot 10 mm larger to make sure it fits.
Building the hinge as follows; Using number of rectangles with Rectangle tool of shown dimensions and only extruding the ones which will shape the hinge of 3 mm.
Assembling the Back hinge body with the sides bodies with the tool Joint to make sure that the projections are fitting.
Using the Orbit tool and flipping the whole assembled Arcade body to build the base upon the Tabs and T-slots using the Project tool.
Using the Circle tool to build openings for T-slots & Arduino screws of shown dimensions.
Assembling the Base body with the sides & Switch bodies with the tool Joint to make sure that the projections are fitting.
Using GrabCAD to download all the electronic components.
Importing components by using Show Data Panel >> New Project >> New Folder; and I name the folder under mounting components as shown.
Using the Joint tool to assemble all the electronic components, screws and nuts to make sure that my measurments were suitable.
I have built a new component for the Neopixel/OLED body to cover their shape by crating a sketch on the Tabs and decorate this body.
Designing Ludus logo on Canva, importing the picture as JPEG and using Convertio to convert it into a DXF file. This DXF file was imported on RDWorks.
Crafting the sides with tabs, slots and the hinge
Extruding the sides
Neopixel/OLED face and body
Joystick sketch and body
Switch sketch and body
Arcade ceiling sketch and body
Back rigid part sketch and body
Back hinge part sketch and body
Assembled prototype without the base
Cool pictures of the hinge
Sketching the base
The Arduino openings on the base
Importing components
Mounting components
Hello, Ludus!
LaserCAD
RDWorks
First trial of fabrication
First trial of fabrication process
Successful fabrication trial
Spraying at San3a
Machines:
Laser Cutter:
I fabricated my project twice because of the size of the plywood sheet in Dokki (2.7 mm). The machine in Creativa Giza I used is "Malky ML64 CO2 Laser Cutter.". In FabLab the machine is El Malky ML149 CO2 Laser Cutter.
Materials:
All the sides of Ludus were cut on a Plywood sheet.
Ludus is assembled using Tabs, screws and nuts (M3), utilising the T-slot technique.
Software:
LaserCAD: used to enable precise cutting and speed cutting , which is essential for creating detailed and accurate designs the game logo.
RDWorks.
Importing to Laser Cutter Software:
The design was imported into the ElMalky CO2 Laser Cutter’s software by using LaserCAD & RDWorks and downloading the design into the machine files.
Placement: The design was positioned on the virtual material sheet within the software to optimise the material usage and ensure correct alignment.
Setting Parameters:
The material chosen was 3 mm plywood.
Laser parameters like power, speed, and modes as shown above
1. Cutting Process
Material Preparation:
The plywood sheet was placed on the laser cutter's bed, and the machine was checked to ensure proper focus and alignment.
The sheet was leveled, and the origin was set to match the starting point of the design using Box & Origin buttons on the machine.
Cutting Execution:
The laser cutter was started, following the paths defined in the design file.
The cutting process was monitored to ensure precision and to avoid any interruptions such as material shifting or incomplete cuts.
2. Post-Processing
Cleaning: Any burn marks, residue from the cutting process or even the tabs were gently sanded off with a sanding paper. A damp cloth was used to wipe off any dust or debris.
Spraying: I sprayed the side, switch, joystick and back parts with black spray paint to have a better appearance and more arcade-like.
Assembling: The arcade parts were assembled with tabs, screws and nuts.
Circuit simulation on TinkerCAD
The electronic circuit for the Ludus integrates input components (joysticks), processing (Arduino), and action components (Neopixel LED matrix, OLED display, and buzzer) to create an interactive smart system.
Input Components:
Two Joystick Modules (XY + Push Button)
Used for player movement across the grid.
Each player has a dedicated joystick to control their cursor.
The X-axis moves the cursor left/right, and the Y-axis moves it up/down.
The button press places an "X" or "O" on the board.
Processing Unit:
Arduino Uno
Reads input from the joysticks.
Updates the display and Neopixel matrix accordingly.
Checks for winning conditions and triggers animations when a player wins.
Action Components:
8×8 Neopixel LED Matrix
Displays the 3×3 Tic-Tac-Toe grid in green.
Highlights selected positions for Player 1 (Blue - "X") and Player 2 (Red - "O").
Flashes the winning pattern when a player wins.
OLED Display (SSD1306 128×64)
Shows messages like "Winner: Player X".
Buzzer
Sounds when a player wins to celebrate.
Small breadboard
Used to assemble the electronic components.
Player Moves Cursor → Joystick sends an analog signal (X/Y values) to Arduino.
Arduino Processes Movement → Converts joystick input into valid moves (left/right/up/down).
Cursor Updates on Neopixel Matrix → Highlights the selected position in yellow.
Player Presses Button to Select Move → Arduino checks if the position is empty.
Game Board Updates → Neopixel changes color to Blue (X) or Red (O).
Win Check → If three marks align, Neopixel blinks the winning line, and the OLED displays the winner.
Buzzer Plays Celebration Sound.
Arduino IDE → Writing & uploading the game code.
Adafruit Neopixel Library → Controlling the LED matrix.
Adafruit GFX & SSD1306 Library → Managing OLED display output.
TinkerCAD → Designing and simulating the circuit before implementation. I used alternatives for simulating:
Joystick modules (Motor encoder)
OLED ( LCD I2C)
Arduino UNO
Small Breadboard
OLED 0.96 inch
Joystick module
Neopixel 8*8
Buzzer
Jumper wires
Ludus is powered by a 7.4V DC power supply (battery).
A DC power step-down module (Buck Converter) is used to regulate the voltage.
Using the potentiometer on the step-down module, the output was adjusted to 5V.
An Avometer (Multimeter) was used to precisely measure and confirm the 5V output.
Why 5V?
Arduino Uno & OLED Display operate at 5V.
NeoPixel LED Matrix also requires 5V stable power for proper functionality.
Joysticks work efficiently within the 5V range, avoiding damage from higher voltages.
The whole circuit consumption is 0.2A (measured using the Avometer)
Tools & Components Used for Power Adjustment:
On/Off switch
7.4V DC Power Adapter
DC-DC Step-Down Module (Buck Converter)
Avometer (Multimeter)
Potentiometer (on Step-Down Module)
Power Flow & Integration:
When the On/Off switch is pressed
7.4V DC Input → Enters the DC Step-Down Converter.
Voltage Adjusted to 5V → Confirmed using the Avometer.
5V Output Distribution:
Powers the Arduino Uno.
Provides 5V to the OLED Display.
Supplies stable 5V to the NeoPixel LED Matrix and Joystick Modules.
Power source: 7.4V Battery
DC-DC Step-down module
On/Off switch
Joystick inspiration
Neopixel (8*8) inspiration
OLED inspiration
Well, at the beginning I started by searching for previous projects that explain the idea of each component on its own (attached above). Testing every component output and required libraries to function properly, I started my Ludus code with the following:
#include <Adafruit_NeoPixel.h>: controls NeoPixel RGB LED matrix allowing setting individual LED colors, brightness, and animation.
#include <Wire.h>: enables data transfer between the Arduino and OLED.
#include <Adafruit_GFX.h>: for text writing on OLED.
#include <Adafruit_SSD1306.h>: for allowing functions like display.print() in our code.
PIN 6: Connects the LED matrix to pin 6.
MATRIX_WIDTH 8, MATRIX_HEIGHT 8: Defines an 8x8 grid.
BRIGHTNESS 40: Sets LED brightness.
TIME_LIMIT 60000: Each player has 1 minute to move.
JOYSTICK1_X A0, JOYSTICK1_Y A1, JOYSTICK1_BTN 2: Player 1’s joystick.
JOYSTICK2_X A2, JOYSTICK2_Y A3, JOYSTICK2_BTN 3: Player 2’s joystick.
BUZZER 4: Controls sound alerts.
SCREEN_WIDTH 128, SCREEN_HEIGHT 64: Defines OLED resolution.
OLED_RESET -1: No reset pin needed.
Adafruit_SSD1306 display(...): Creates an OLED display object with a 128x64 resolution for showing Ludus messages.
Adafruit_NeoPixel pixels(...): Defines an 8x8 LED matrix, connected to pin 6, using NeoPixel color format.
int grid[3][3]: A 3x3 array storing the game board (0 = empty, 1 = Player 1, 2 = Player 2).
int cursorX = 0, cursorY = 0: The cursor starts at the top-left corner.
int currentPlayer = 1: Player 1 starts first.
unsigned long turnStartTime: Tracks when a player’s turn starts.
unsigned long lastMoveTime = 0: Helps with joystick debounce.
const int cursorMoveDelay = 300: Limits cursor movement speed (one move per 300ms).
Serial.begin(9600); Starts the serial monitor for debugging and testing joystick movements.
pinMode(JOYSTICK1_BTN, INPUT_PULLUP);
pinMode(JOYSTICK2_BTN, INPUT_PULLUP);
They both set joystick buttons as input with pull-up resistors.
pinMode(BUZZER, OUTPUT); declaring the buzzer as output for sound alerts.
pixels.begin();Initializes the NeoPixel LED matrix for displaying the game board.
pixels.setBrightness(BRIGHTNESS);Sets the LED brightness for a better user interface and power consumption.
drawGrid();Calls a function to draw the initial game grid on the NeoPixel.
updateDisplay();Updates the NeoPixel to show any initial game elements.
display.begin(SSD1306_SWITCHCAPVCC, 0x3C); Initializes the OLED screen with the correct address (0x3C).
display.clearDisplay(); Clears any previous content on the screen.
turnStartTime = millis(); Records the starting time for tracking player turns.
drawGrid() function creates the Tic-Tac-Toe game grid on an 8×8 NeoPixel matrix.
pixels.clear(); resets all LEDs.
The first for loop sets green pixels ((0,255,0)) at specific x-positions (every 3rd column).
The second for loop sets green pixels at y-positions (every 3rd row).
pixels.show(); applies the changes to the LED matrix.
The updateDisplay() function refreshes the NeoPixel matrix by:
Clearing & Redrawing the Grid.
Displaying Player Moves using blue (X) and red (O) in 2×2 pixel blocks.
Highlighting the Cursor in yellow.
Updating the Matrix with pixels.show();.
The getJoystickMove() function reads joystick input and determines movement:
Delays movement using millis() to prevent rapid jumps.
Reads joystick X & Y values with analogRead().
Prints values to the Serial Monitor for debugging.
This code determines joystick movement based on analog readings:
Resets movement (moveX = 0, moveY = 0).
Checks X-axis: Moves left if < 400, right if > 600.
Checks Y-axis: Moves down if > 600, up if < 400.
Prints movement direction for debugging.
Updates last move time to control speed.
This function blinks the winning player's color on the NeoPixel grid:
Loops 5 times to create a blinking effect.
Clears the display and shows an empty grid.
Pauses for 300ms, then lights up all 3×3 game positions with the winner’s color.
Displays the update, waits another 300ms, and repeats.
This function determines the winner’s color:
Declares winnerColor.
If Player 1 wins, it sets the color to blue.
If Player 2 wins, it sets the color to red.
This color is later used to blink the winning pattern on the NeoPixel grid
This code effectively announces the winner both visually (on the OLED screen and the Neopixel matrix) and audibly (with a buzzer sound).
blinkWinner(winnerColor);
Calls a function blinkWinner(), passing winnerColor as an argument. This makes the NeoPixel blinks in the color representing the winner.
display.clearDisplay();
Clears the OLED display before showing new content.
display.setTextSize(2);
Sets the text size to 2 for better readability.
display.setTextColor(SSD1306_WHITE);
Sets the text color to white.
display.setCursor(5, 20);
Sets the cursor position at (5,20) pixels on the OLED screen.
display.print("Player ");
Prints "Player " on the display.
display.print(currentPlayer);
Prints the value of currentPlayer, indicating which player won.
display.println(" Wins!");
Prints " Wins!" and moves to the next line.
display.display();
Updates the OLED screen to show the new text.
tone(BUZZER, 1000, 1000);
Generates a 1000 Hz sound on the buzzer for 1 second.
delay(2000);
Pauses execution for 2 seconds to allow the message to be displayed.
noTone(BUZZER);
Stops the buzzer sound.
turnStartTime = millis();
Stores the current time (in milliseconds) to track the turn duration.
currentPlayer = 1;
Resets the game to start with Player 1.
cursorX = 0; cursorY = 0;
Resets the cursor position to the top-left corner.
memset(grid, 0, sizeof(grid));
Clears the game grid by setting all its elements to 0.
updateDisplay();
Refreshes the display to reflect the reset state.
This function checkWinner() checks for a winner in Ludus.
Row Check: Loops through each row to see if all three cells contain the same non-zero value (indicating a win).
Column Check: Loops through each column to check for a vertical win.
Diagonal Check: Checks both main diagonals for a win.
Return Values:
Returns the winning player's number if a match is found.
Returns 0 if there's no winner yet.
This loop() function manages the gameplay logic for a Tic-Tac-Toe game using joysticks.
Turn Timeout Check:
If the current turn exceeds TIME_LIMIT, the turn is forfeited, and the opponent wins (declareWinner() is called).
Joystick Input Handling:
Determines which joystick to use based on currentPlayer.
Calls getJoystickMove() to get movement values (moveX, moveY).
Updates the cursor position within the 3x3 grid using constrain().
Refreshes the display (updateDisplay()).
Player Move Handling:
Checks if the correct joystick button is pressed.
Ensures the selected grid cell is empty before placing a move.
Calls checkWinner() to determine if the move resulted in a win.
Switches to the next player (currentPlayer = 3 - currentPlayer).
Resets turnStartTime for the new turn.
For visual learners like me here's a diagram to explain the code logic/structure and summarise the whole idea.
Start & Setup:
Initializes NeoPixel display, OLED screen, buzzer, and joystick inputs.
Sets up the game grid and cursor position.
Game Loop:
Turn Timeout Check: If a player takes too long, the turn is forfeited.
Joystick Input Handling: Reads joystick movement and updates the cursor position.
Move Validation: Checks if the selected grid cell is empty before placing a move.
Winner Check: If a player wins, the winner is displayed, and the game resets.
Switch Players: If no winner, turn switches to the next player, and the loop repeats.
To integrate Ludus’ modules, Iused tabs, screws and nuts to securely assemble the physical components, ensuring stability and alignment. The electronic modules were connected using jumper wires and secured in place within the structure.
Arduino Mounting
Assembling the Side parts & hinge.
Joysitck Face Mounting & Assembly
NeoPixel & OLED Face Mounting & Assembly
Ludus final look.
Testing confirmed smooth joystick movement, accurate NeoPixel representation, and correct turn-based logic execution.
As a perfectionist it took me some time to start sketching my design. But my friend, Menna Bissar helped me breaking this thinking trap and starting my design right away.
I also asked for feedback about the hinge I planned to build as an easy access in the back of my arcade design, but I asked both Mohanad Mohsen and Yasen Elfeky for their feedback and they guided me with different ideas like leaving a space for the user's finger whenever they want to adjust any of the electronic components.
A huge thank you to Abdelrahman Oraby and Mahmoud Walid for their constant support. I asked Abdelrahman to help me figure out the idea of the DC-DC Stepdown module and he guided me through what to with it, Yasen helped me with its connection. Abdelrahman helped with the Neopixel pins as my design required to have the pins in a specific angle.
Fixing the NeoPixel pins
While developing Ludus, I faced several challenges, especially in code writing and integration.
The first challenge was that I need to have an easy-access to the circuit and electronic components, my friend "Menna Bissar" suggested building a hinge in my prototype. I approached Mohanad and he helped me forming this hinge with the mentioned dimensions in the design section.
My second challenge was the fact that I need a stable wire-free source of power and I used a battery and a DC-DC stepdown module.
Thirdly, While developing Ludus, I faced a major issue which was that the cursor on the NeoPixel was moving diagonally only.
I asked Ahmed Sami what could be the problem, he told me to check the Joystick movement directions code. Although I tried multiple times of changing the XY values, it didn't work out. While discussing this issue with my friend Menna Bissar she suggested that the problem could be within the NeoPixel code. After some research, I found that the problem was the incorrect definition of the NeoPixel grid for the Tic-Tac-Toe game. The game logic relied on a 3x3 grid, but I had to display it on an 8x8 NeoPixel matrix. Initially, I miscalculated the mapping between the game grid and the NeoPixel indices, which led to incorrectly lit LEDs, misplaced cursor movements, and a distorted game board.
Easy access through the hinge
Cursor moving diagonally only
Debugging the joystick
The NeoPixel matrix uses a single array index to represent LED positions, while the game grid follows a row-column format. My initial approach to lighting up the LEDs was:
However, this caused the LEDs to light up in the wrong places because the game's 3x3 logic did not properly align with the 8x8 LED matrix.
To correctly display the Tic-Tac-Toe grid, I had to map each 3x3 game cell to a block of four LEDs within the 8x8 matrix. Instead of lighting up a single pixel, I used a block of four LEDs per game cell:
This ensured that each game cell correctly occupied a 3x3 space on the NeoPixel matrix. The cursor logic also had to be adjusted to move in steps of 3 instead of 1:
While assembling the project using screws & nuts I found that the screws I have are either short or long enough to leave the screws shank & head and they were not fully stable. I made used of a classic trick which is rolling a nut within every screw before using it in assembling wooden part as shown in pictures.
While assembling the Arduino side, I found that the tabs are slightly shifted of their slots. Using a Coarse sandpaper, I managed to remove those imperfections and assemble the tabs with the slots.
Play with AI mode:
Integrating an AI opponent into Ludus would elevate the game by allowing players to challenge a computer-controlled player instead of just another human.
The game will be displayed to the user on a NeoPixel LED matrix and based on the user input, on an OLED screen a (You win!) or (Ludus wins!)
Difficulty Levels:
Implementing different AI strategies (Easy = random moves, Hard = Minimax).