Midterm Project: Tilt-Maze

The Tilt-Maze is an interactive electronic toy designed for young children who enjoy solving ball-in-a-maze puzzles. The electronic aspect of the toy allows the same toy to exhibit countless different mazes in various orientations, while also maintaining the ‘natural’ consequence of a ball falling due to gravity emulated by the device.

The appeal of ball-in-a-maze puzzles is largely due to the cognitive exercise associated with finding the solution to the maze and the inherent frustration that arises from trying to precisely control the ball’s motion through the use of gravity alone. Thus, the purpose of this project was to utilize technology to capitalize on each of the puzzle’s appeals. By utilizing LEDs to symbolize the position of the ‘ball’ and the maze ‘walls’, countless unique combinations of starting locations and maze configurations can be incorporated into a single product, greatly increasing the toy’s play-time. This was accomplished while also utilizing a form of tilt-motion controls that greatly resemble those of a real-life ball-in-a-maze puzzle. This was necessary to maintain an intuitive interaction between the user and the Tilt-Maze toy.

In accordance with my previous thoughts on the definition of meaningful interaction, I decided that any project I build should be simple and intuitive to use. Left alone without an instruction manual, any user, including small children, should soon be able to figure out the method of interaction on their own. This theme would dictate several of the design decisions to follow.

My idea of using tilt-motion controls to control the motion of the ‘ball’ was inspired by the following GIF, a component of this project:

Otherwise, the project bears little resemblance to my own. It consists of 16 back-lit buttons arranged in a 4×4 grid, and the various applications of the projects are related to audio/music. Although I was interested in the tilt-motion mechanism, I decided that, for the purposes of my project, an array of buttons was unnecessary and opposed the theme of my project. The array of lights would only act as a source of output, not input.

Having found my preferred method of interaction, I needed an application that would play to its strengths well. I was inspired in passing by a small child playing with a ball-in-a-maze puzzle. At this point, the idea of emulating the motion of the ball on a cube, each side consisting of an array of LEDs, came to mind. After considering the idea further. I figured that attempting to implement a six-sided toy would not be possible within the time-frame available. Instead, I would only build a 2D version as a proof of concept. Further research into the subject revealed the Perplexus, a similar sort of physical toy that instead took the shape of a sphere. While the purpose of the physical toy may be obvious to even a child, I did not think that an electronic version would be as intuitive, so I decided to not pursue the various complexities present within the Perplexus.

The design process began with simple sketches on paper of the desired result. I also noted down details pertaining to the materials I would need, the functions of the final product, and a few rough dimensions.

 

 

 

 

 

 

The first phase of the project involved constructing a working circuit that would allow the direct manipulation of an array of LEDs. After much hassle, I would eventually get my setup working for a square matrix, yellow in the video, but not for rectangular matrices. This handicap, as well as the frustration associated with deciphering the mess of wires, caused me to abandon the idea of using typical colored LEDs for my project.

 

 

 

 

 

To combat this problem, I was pointed in the direction of Adafruit Neopixel RGB LED strips. Such strips were much easier to handle programmatically and could produce much brighter light. Although this switch removed much of the programming complexity, it necessitated a large time contribution soldering. It was at this point that the dimensions of my final product was finalized and the lengths of the connecting wires was decided.

 

 

 

 

 

 

Three such matrices were constructed. One 4×4 matrix, one 3×4 matrix, and one 4×3 matrix were necessary to represent the position of the ‘ball’, the horizontal ‘walls’, and the vertical ‘walls’ respectively. The matrices were then overlayed and appropriately wired up to check functionality. At this point, four tilt sensors, one for each of the four directions of motion, were incorporated to produced the desired motion controls. At first, the product was placed upon a simple cardboard box for the purpose of user testing.

 

 

 

 

 

As final touches to functionality, I added a start button for the game and a buzzer to play a congratulatory tone upon beating a level. The user testing session offered many insights into the game functionality that I would incorporate into my final product.

The final step in this process was to construct a proper box to contain the Tilt-Maze within a single unit. The final design consisted of a box with internal divisions to separate the various LEDs and a bottom compartment to host the Arduino, tilt sensors, and various wired connections. This phase once again required accurate measurements for assembly to be successful.

The final product appropriately divided the different lights and included slots for the start button and the necessary wire to power the toy.

Although fully functional, I believe that my project serves best as a proof of concept. To truly be appealing to the targeted demographic. The final product ought to be a functional six-sided cube with smaller dimensions and a larger array of lights to allow for more complicated mazes. For the purpose of this project, only a few mazes and start-end position were hard-coded; however, the potential exists for countless mazes to be played on the device. I chose to hard-code maze designs to the time lag that may result from the computationally intensive maze generation algorithm. Finally, while the divisions are somewhat clear, there is still considerable bleeding of light into neighboring divisions which may confuse first-time users.

All in all, I believe my goal of building an intuitive interactive toy was achieved, but there is much room for improvement. The mess of wires that resulted from using ordinary LEDs was my fault and very frustrating, so the use of LED strips was justified. Due to prior familiarity with coding, I learned the most about the physical processes of soldering and laser cutting. If I were to further develop this project, the first goal would be to incorporate more maze designs or implement the generation algorithm. There is also room for improvement in the measurements of the divisions to prevent the bleeding of light. With that out of the way, the next major phase would be to construct larger, more tightly packed LED matrices for the construction of an entire cube. As it stands, I believe I was successful in producing a fully-functional prototype of a toy to serve as a proof of concept for future development.

//LEDStrip

#include <FastLED.h>
#include "ArrayConfig.h"
#include "pitches.h"

#define BUZZER_PIN 5
#define PLAYER_PIN  2
#define HORIZONTAL_PIN  3
#define VERTICAL_PIN  13
#define N_PIN 9
#define E_PIN 12
#define S_PIN 10
#define W_PIN 11
#define START_PIN 4
#define COLOR_ORDER GRB
#define CHIPSET     WS2811
#define BRIGHTNESS 255

bool start = false, prevButtonState = 0;
int xPosn, yPosn, NCount, ECount, SCount, WCount, MazeCount = 0;
int endPt = 0, endX = 3, endY = 0;

#define PLAYER_LEDS (PlayerMatrixWidth * PlayerMatrixHeight)
#define HORIZONTAL_LEDS (HorizontalMatrixWidth * HorizontalMatrixHeight)
#define VERTICAL_LEDS (VerticalMatrixWidth * VerticalMatrixHeight)
CRGB leds_plus_safety_pixel[ PLAYER_LEDS + 1];
CRGB leds_plus_safety_pixel1[ HORIZONTAL_LEDS + 1];
CRGB leds_plus_safety_pixel2[ VERTICAL_LEDS + 1];
CRGB* const Player( leds_plus_safety_pixel + 1);
CRGB* const Horizontal( leds_plus_safety_pixel1 + 1);
CRGB* const Vertical( leds_plus_safety_pixel2 + 1);

// Param for different pixel layouts
const bool    kMatrixSerpentineLayout = true;

uint16_t XY( uint8_t x, uint8_t y)
{
  uint16_t i;
  
  if( kMatrixSerpentineLayout == false) {
    i = (y * PlayerMatrixWidth) + x;
  }

  if( kMatrixSerpentineLayout == true) {
    if( y & 0x01) {
      // Odd rows run backwards
      uint8_t reverseX = (PlayerMatrixWidth - 1) - x;
      i = (y * PlayerMatrixWidth) + reverseX;
    } else {
      // Even rows run forwards
      i = (y * PlayerMatrixWidth) + x;
    }
  }
  
  return i;
}

uint16_t XYHor( uint8_t x, uint8_t y)
{
  uint16_t i;
  
  if( kMatrixSerpentineLayout == false) {
    i = (y * HorizontalMatrixWidth) + x;
  }

  if( kMatrixSerpentineLayout == true) {
    if( y & 0x01) {
      // Odd rows run backwards
      uint8_t reverseX = (HorizontalMatrixWidth - 1) - x;
      i = (y * HorizontalMatrixWidth) + reverseX;
    } else {
      // Even rows run forwards
      i = (y * HorizontalMatrixWidth) + x;
    }
  }
  
  return i;
}

uint16_t XYVer( uint8_t x, uint8_t y)
{
  uint16_t i;
  x = VerticalMatrixHeight - x;
  
  if( kMatrixSerpentineLayout == false) {
    i = (y * VerticalMatrixWidth) + x;
  }

  if( kMatrixSerpentineLayout == true) {
    if( y & 0x01) {
      // Odd rows run backwards
      uint8_t reverseX = (VerticalMatrixWidth - 1) - x;
      i = (y * VerticalMatrixWidth) + reverseX;
    } else {
      // Even rows run forwards
      i = (y * VerticalMatrixWidth) + x;
    }
  }
  
  return i;
}

void loop()
{
  if (start)
  {
    for( uint8_t x = 0; x < PlayerMatrixWidth; x++)
    {
      for( uint8_t y = 0; y < PlayerMatrixHeight; y++)
      {
        if (posn[x][y])
          Player[ XY( x, y) ] = CRGB::Blue;
        else if (x == endX && y == endY)
          Player[ XY( x, y) ] = CRGB::Green;
        else
          Player[ XY( x, y) ] = CRGB::Black;
      }
    }

    for( uint8_t x = 0; x < HorizontalMatrixWidth; x++)
    {
      for( uint8_t y = 0; y < HorizontalMatrixHeight; y++)
      {
        if (patternH[MazeCount][x][y])
          Horizontal[ XYHor( x, y) ] = CRGB::Red;
        else
          Horizontal[ XYHor( x, y) ] = CRGB::Black;
      }
    }
    
    for( uint8_t x = 0; x < VerticalMatrixWidth; x++)
    {
      for( uint8_t y = 0; y < VerticalMatrixHeight; y++)
      {
        if (patternV[MazeCount][x][y])
          Vertical[ XYVer( x, y) ] = CRGB::Red;
        else
          Vertical[ XYVer( x, y) ] = CRGB::Black;
      }
    }
    FastLED.show();

    if ((xPosn == endX && yPosn == endY) || (!digitalRead(START_PIN) && prevButtonState == 0))
    {
      for (int thisNote = 0; thisNote < 8; thisNote++)
      {
        int noteDuration = 1000 / noteDurations[thisNote];
        tone(BUZZER_PIN, melody[thisNote], noteDuration);
   
        int pauseBetweenNotes = noteDuration * 1.30;

        for( uint8_t x = 0; x < PlayerMatrixWidth; x++)
        for( uint8_t y = 0; y < PlayerMatrixHeight; y++)
            Player[ XY( x, y) ] = rainbow[thisNote];
        for( uint8_t x = 0; x < HorizontalMatrixWidth; x++)
          for( uint8_t y = 0; y < HorizontalMatrixHeight; y++)
            Horizontal[ XYHor( x, y) ] = rainbow[thisNote];
        for( uint8_t x = 0; x < VerticalMatrixWidth; x++)
          for( uint8_t y = 0; y < VerticalMatrixHeight; y++)
            Vertical[ XYVer( x, y) ] = rainbow[thisNote];
        FastLED.show();
      
        delay(pauseBetweenNotes);
        noTone(BUZZER_PIN);
      }
      
      endPt = (endPt + 1) % 3;
      if (endPt == 0)
      {
        endX = 3; endY = 0;
      }
      else if (endPt == 1)
      {
        endX = 3; endY = 3;
      }
      else
      {
        endX = 0; endY = 3;   
      }
      
      posn[xPosn][yPosn] = 0;
      xPosn = 0, yPosn = 0;
      posn[xPosn][yPosn] = 1;
      MazeCount = (MazeCount + 1) % mazeTotal;
    }

    if (yPosn > 0 && !digitalRead(N_PIN) && patternH[MazeCount][yPosn - 1][xPosn] == 0)
    {
      ECount, SCount, WCount = 0;
      NCount++;
      if (NCount == 20)
      {
        posn[xPosn][yPosn] = 0;
        yPosn--;
        posn[xPosn][yPosn] = 1;
        delay(100);
        NCount = 0;
      }
    }
    if (xPosn < 3 && !digitalRead(E_PIN) && patternV[MazeCount][yPosn][xPosn] == 0)
    {
      NCount, SCount, WCount = 0;
      ECount++;
      if (ECount == 20)
      {
        posn[xPosn][yPosn] = 0;
        xPosn++;
        posn[xPosn][yPosn] = 1;
        delay(100);
        ECount = 0;
      }
    }
    if (yPosn < 3 && digitalRead(S_PIN) && patternH[MazeCount][yPosn][xPosn] == 0)
    {
      NCount, ECount, WCount = 0;
      SCount++;
      if (SCount == 20)
      {
        posn[xPosn][yPosn] = 0;
        yPosn++;
        posn[xPosn][yPosn] = 1;
        delay(100);
        SCount = 0;
      }
    }
    if (xPosn > 0 && digitalRead(W_PIN) && patternV[MazeCount][yPosn][xPosn - 1] == 0)
    {
      NCount, ECount, SCount = 0;
      WCount++;
      if (WCount == 20)
      {
        posn[xPosn][yPosn] = 0;
        xPosn--;
        posn[xPosn][yPosn] = 1;
        delay(100);
        WCount = 0;
      }
    }

    if (digitalRead(START_PIN))
      prevButtonState = 0;
    else
      prevButtonState = 1;  
  }
  else if (!digitalRead(START_PIN))
  {
    start = true;
  }
}

void setup() 
{
  FastLED.addLeds<CHIPSET, PLAYER_PIN, COLOR_ORDER>(Player, PLAYER_LEDS).setCorrection(TypicalSMD5050);
  FastLED.addLeds<CHIPSET, HORIZONTAL_PIN, COLOR_ORDER>(Horizontal, HORIZONTAL_LEDS).setCorrection(TypicalSMD5050);
  FastLED.addLeds<CHIPSET, VERTICAL_PIN, COLOR_ORDER>(Vertical, VERTICAL_LEDS).setCorrection(TypicalSMD5050);
  FastLED.setBrightness( BRIGHTNESS );
  pinMode(BUZZER_PIN, OUTPUT);
  pinMode(START_PIN, INPUT_PULLUP);
  pinMode(N_PIN, INPUT_PULLUP);
  pinMode(E_PIN, INPUT_PULLUP);
  pinMode(S_PIN, INPUT_PULLUP);
  pinMode(W_PIN, INPUT_PULLUP);
  Serial.begin(9600);
  
    for( uint8_t x = 0; x < PlayerMatrixWidth; x++)
      for( uint8_t y = 0; y < PlayerMatrixHeight; y++)
          Player[ XY( x, y) ] = CRGB::Black;

    for( uint8_t x = 0; x < HorizontalMatrixWidth; x++)
      for( uint8_t y = 0; y < HorizontalMatrixHeight; y++)
          Horizontal[ XYHor( x, y) ] = CRGB::Black;
    
    for( uint8_t x = 0; x < VerticalMatrixWidth; x++)
      for( uint8_t y = 0; y < VerticalMatrixHeight; y++)
          Vertical[ XYVer( x, y) ] = CRGB::Black;
    FastLED.show();
}

//ArrayConfig.h

const uint8_t PlayerMatrixWidth = 4;
const uint8_t PlayerMatrixHeight = 4;
const uint8_t HorizontalMatrixWidth = 3;
const uint8_t HorizontalMatrixHeight = 4;
const uint8_t VerticalMatrixWidth = 4;
const uint8_t VerticalMatrixHeight = 3;
const uint8_t mazeTotal = 3;

CRGB rainbow[8] = {CRGB::Red, CRGB::Orange, CRGB::Yellow, CRGB::Green, CRGB::Blue, CRGB::Indigo, CRGB::Purple, CRGB::Black};

boolean posn[PlayerMatrixWidth][PlayerMatrixHeight] = {
  {1, 0, 0, 0},
  {0, 0, 0, 0},
  {0, 0, 0, 0},
  {0, 0, 0, 0},
};
boolean patternH[mazeTotal][HorizontalMatrixWidth][HorizontalMatrixHeight] = 
{ 
  {
  {0, 1, 1, 0},
  {1, 0, 1, 0},
  {1, 1, 0, 0},
  },
  
  {
  {0, 0, 0, 1},
  {0, 0, 1, 0},
  {0, 1, 0, 0},
  },
    
  {
  {1, 0, 1, 0},
  {1, 0, 1, 0},
  {1, 1, 0, 1},
  },
};
boolean patternV[mazeTotal][VerticalMatrixWidth][VerticalMatrixHeight] =
{ 
  {
  {1, 0, 0},
  {0, 0, 1},
  {0, 0, 0},
  {0, 0, 1},
  },

  {
  {1, 0, 1},
  {1, 1, 0},
  {1, 0, 1},
  {0, 0, 1},
  },
  
  {
  {0, 1, 1},
  {1, 0, 0},
  {1, 1, 0},
  {0, 0, 0},
  },
};

//Pitches.h

/*************************************************
 * Public Constants
 *************************************************/

#define NOTE_B0  31
#define NOTE_C1  33
#define NOTE_CS1 35
#define NOTE_D1  37
#define NOTE_DS1 39
#define NOTE_E1  41
#define NOTE_F1  44
#define NOTE_FS1 46
#define NOTE_G1  49
#define NOTE_GS1 52
#define NOTE_A1  55
#define NOTE_AS1 58
#define NOTE_B1  62
#define NOTE_C2  65
#define NOTE_CS2 69
#define NOTE_D2  73
#define NOTE_DS2 78
#define NOTE_E2  82
#define NOTE_F2  87
#define NOTE_FS2 93
#define NOTE_G2  98
#define NOTE_GS2 104
#define NOTE_A2  110
#define NOTE_AS2 117
#define NOTE_B2  123
#define NOTE_C3  131
#define NOTE_CS3 139
#define NOTE_D3  147
#define NOTE_DS3 156
#define NOTE_E3  165
#define NOTE_F3  175
#define NOTE_FS3 185
#define NOTE_G3  196
#define NOTE_GS3 208
#define NOTE_A3  220
#define NOTE_AS3 233
#define NOTE_B3  247
#define NOTE_C4  262
#define NOTE_CS4 277
#define NOTE_D4  294
#define NOTE_DS4 311
#define NOTE_E4  330
#define NOTE_F4  349
#define NOTE_FS4 370
#define NOTE_G4  392
#define NOTE_GS4 415
#define NOTE_A4  440
#define NOTE_AS4 466
#define NOTE_B4  494
#define NOTE_C5  523
#define NOTE_CS5 554
#define NOTE_D5  587
#define NOTE_DS5 622
#define NOTE_E5  659
#define NOTE_F5  698
#define NOTE_FS5 740
#define NOTE_G5  784
#define NOTE_GS5 831
#define NOTE_A5  880
#define NOTE_AS5 932
#define NOTE_B5  988
#define NOTE_C6  1047
#define NOTE_CS6 1109
#define NOTE_D6  1175
#define NOTE_DS6 1245
#define NOTE_E6  1319
#define NOTE_F6  1397
#define NOTE_FS6 1480
#define NOTE_G6  1568
#define NOTE_GS6 1661
#define NOTE_A6  1760
#define NOTE_AS6 1865
#define NOTE_B6  1976
#define NOTE_C7  2093
#define NOTE_CS7 2217
#define NOTE_D7  2349
#define NOTE_DS7 2489
#define NOTE_E7  2637
#define NOTE_F7  2794
#define NOTE_FS7 2960
#define NOTE_G7  3136
#define NOTE_GS7 3322
#define NOTE_A7  3520
#define NOTE_AS7 3729
#define NOTE_B7  3951
#define NOTE_C8  4186
#define NOTE_CS8 4435
#define NOTE_D8  4699
#define NOTE_DS8 4978

// notes in the melody:
int melody[] = {
  NOTE_C4, NOTE_G3, NOTE_G3, NOTE_A3, NOTE_G3, 0, NOTE_B3, NOTE_C4
};

// note durations: 4 = quarter note, 8 = eighth note, etc.:
int noteDurations[] = {
  4, 8, 8, 4, 4, 4, 4, 4
};

One thought on “Midterm Project: Tilt-Maze

  1. great process Ahmad.
    I’m just a little bit curious about the feedback you received in the user testing session.
    I know you worked alone and you worked hard, I wonder if you considered at some point to add sound to the falling “ball” besides the congratulatory tone. That might be helpful too.

Leave a Reply