Build Your Own Memory Game With Netduino

About the project

Learn how to build a memory game with an OLED display and a 4x4 push button keypad using Netduino.Foundation and DisplayGraphics Library.

Project info

Difficulty: Easy

Platforms: MicrosoftNetduino

Estimated time: 1 hour

License: MIT license (MIT)

Items used in this project

Hardware components

Push-button Power Switch Breakout Push-button Power Switch Breakout x 1
Tiny Breadboard Tiny Breadboard x 1
Netduino 3 Ethernet Netduino 3 Ethernet x 1
Netduino 3 Wifi Netduino 3 Wifi x 1
Netduino 3 Netduino 3 x 1
Silicone Elastomer 4x4 Button Keypad Silicone Elastomer 4x4 Button Keypad x 1

Software apps and online services

Microsoft Visual Studio 2015 Microsoft Visual Studio 2015

Story

What you'll be building in this project is a memory style game, where each button on a 4 x 4 keypad will correspond to an alphabetic character going from 'A' to 'H', and pressing each button will reveal the character associated to it in the OLED display. You win the game when you find all the pair of characters on the keypad. You'll create the logic using Netduino.Foundation.

Pressing a button to see the letter associated

The 4x4 Keypad is a matrix of Push Buttons, where four pins corresponds to four columns and the other four corresponds to the four rows. The way to determine which button is pressed is by setting the column output pins HIGH one at a time at high speed, and when pressing a button and the corresponding column is HIGH, you can detect which row of the matrix completes the circuit and it will emit a signal to the row input pins. You can see a small video tutorial here.

For displaying the UI on the 128 x 32 I2C OLED display, you can connect to it using Netduino.Foundation, and for displaying texts and shapes you will use DisplayGraphics, an extended Library of Netduino.Foundation.

Netduino.Foundation is a platform for quickly and easily build connected things using the.NET MicroFramework on Netduino. Created by Wilderness Labs, it's completely open source and maintained by the Netduino community.

If you're new in Netduino development, I suggest you go to the Getting started with Netduino project to properly set up your development environment.

Step 1 - Assemble the circuit

For this project, wire up your breadboard and Netduino as shown in the Fritzing diagram:

Circuit diagram of the OLED display with the 16 button keypad

Step 2 - Create a Netduino project

Create a Netduino project in Visual Studio 2015 for Windows or the latest Visual Studio for Mac; name the project MemoryGame.

Step 3 - Add the Netduino.Foundation NuGet Package

Right-click on your MemoryGame project and click Manage Nuget Packages. In the Browse tab, search for Netduino.Foundation; it should be the first search result. Click the Install button.

Adding Netduino.Foundation NuGet Package

You'll use three (2) additional nuget packages. Search for Netduino.Foundation.SSD1306 and Netduino.Foundation.GraphicsLibrary and add each one to your project.

macOS

Alt-click on your MemoryGame project in the Solution Explorer, and click Add => Add Nuget Package to open the NuGet Package window. Search for the Netduino.Foundation package and click Add Package to add it to your project.

Adding Netduino.Foundation NuGet package

You'll use three (2) additional nuget packages. Search for Netduino.Foundation.SSD1306 and Netduino.Foundation.GraphicsLibrary and add each one to your project.

Step 4 - Write the code for the MemoryGame Project

Add App Class

For this project, we implement a common App software pattern that manages all the peripherals and main logic.

Add a new App class to your project, and paste the following code:

using Microsoft.SPOT.Hardware;
using System.Threading;
using Netduino.Foundation.Displays;
using System;
namespace MemoryGame
{
   public class App
   {
       protected SSD1306 display;
       protected GraphicsLibrary graphics;
       protected int currentColumn;
       protected InputPort[] rowPorts = new InputPort[4];
       protected OutputPort[] columnPorts = new OutputPort[4];
       protected char[] options;
       protected bool[] optionsSolved;
       protected char[] optionsPossible;
       protected int option1, option2;
       public App()
       {
           options = new char[16];
           optionsSolved = new bool[16];
           optionsPossible = new char[8] { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H' };
           option1 = option2 = -1;
           InitializePeripherals();
       }
       protected void InitializePeripherals()
       {
           display = new SSD1306(0x3c, 400, SSD1306.DisplayType.OLED128x32);
           graphics = new GraphicsLibrary(display);
           rowPorts[0] = new InputPort(SecretLabs.NETMF.Hardware.Netduino.Pins.GPIO_PIN_D0, true, Port.ResistorMode.PullDown);
           rowPorts[1] = new InputPort(SecretLabs.NETMF.Hardware.Netduino.Pins.GPIO_PIN_D1, true, Port.ResistorMode.PullDown);
           rowPorts[2] = new InputPort(SecretLabs.NETMF.Hardware.Netduino.Pins.GPIO_PIN_D2, true, Port.ResistorMode.PullDown);
           rowPorts[3] = new InputPort(SecretLabs.NETMF.Hardware.Netduino.Pins.GPIO_PIN_D3, true, Port.ResistorMode.PullDown);
           columnPorts[0] = new OutputPort(SecretLabs.NETMF.Hardware.Netduino.Pins.GPIO_PIN_D4, false);
           columnPorts[1] = new OutputPort(SecretLabs.NETMF.Hardware.Netduino.Pins.GPIO_PIN_D5, false);
           columnPorts[2] = new OutputPort(SecretLabs.NETMF.Hardware.Netduino.Pins.GPIO_PIN_D6, false);
           columnPorts[3] = new OutputPort(SecretLabs.NETMF.Hardware.Netduino.Pins.GPIO_PIN_D7, false);
           currentColumn = 0;
       }
       protected bool IsLevelComplete()
       {
           bool isComplete = true;
           for(int i = 0; i < 16; i++)
           {
               if (!optionsSolved[i])
               {
                   isComplete = false;
                   break;
               }
           }
           return isComplete;
       }
       protected void LoadMemoryBoard()
       {
           for (int i = 0; i < 16; i++)
               options[i] = ' ';
           for (int i = 0; i < 8; i++)
           {
               PlaceCharacter(i);
               PlaceCharacter(i);
           }
           // Uncomment to print all board values
           // for (int i = 0; i < 16; i++)
           //    Debug.Print((i+1).ToString() + " " + options[i].ToString() + " ");
       }
       protected void PlaceCharacter(int i)
       {
           var r = new Random();
           bool isPlaced = false;
           while (!isPlaced)
           {
               int index = r.Next(16);
               if (options[index] == ' ')
               {
                   options[index] = optionsPossible[i];
                   isPlaced = true;
               }
           }
       }
       protected void StartGameAnimation()
       {
           DisplayText("MEMORY GAME", 20);
           Thread.Sleep(1000);
           DisplayText("Ready?", 40);
           Thread.Sleep(1000);
           DisplayText("Start!", 40);
           Thread.Sleep(1000);
           DisplayText("Select Button");
       }
       protected void GameLoop()
       {
           Thread thread = new Thread(()=> 
           {
               int lastButton = -1;
               while (true)
               {
                   Thread.Sleep(50);
                   int currentButton = -1;
                   switch (currentColumn)
                   {
                       case 0:
                           columnPorts[0].Write(true);
                           columnPorts[1].Write(false);
                           columnPorts[2].Write(false);
                           columnPorts[3].Write(false);
                           if (rowPorts[0].Read()) currentButton = 13;
                           if (rowPorts[1].Read()) currentButton = 9;
                           if (rowPorts[2].Read()) currentButton = 5;
                           if (rowPorts[3].Read()) currentButton = 1;
                           break;
                       case 1:
                           columnPorts[0].Write(false);
                           columnPorts[1].Write(true);
                           columnPorts[2].Write(false);
                           columnPorts[3].Write(false);
                           if (rowPorts[0].Read()) currentButton = 14;
                           if (rowPorts[1].Read()) currentButton = 10;
                           if (rowPorts[2].Read()) currentButton = 6;
                           if (rowPorts[3].Read()) currentButton = 2;
                           break;
                       case 2:
                           columnPorts[0].Write(false);
                           columnPorts[1].Write(false);
                           columnPorts[2].Write(true);
                           columnPorts[3].Write(false);
                           if (rowPorts[0].Read()) currentButton = 15;
                           if (rowPorts[1].Read()) currentButton = 11;
                           if (rowPorts[2].Read()) currentButton = 7;
                           if (rowPorts[3].Read()) currentButton = 3;
                           break;
                       case 3:
                           columnPorts[0].Write(false);
                           columnPorts[1].Write(false);
                           columnPorts[2].Write(false);
                           columnPorts[3].Write(true);
                           if (rowPorts[0].Read()) currentButton = 16;
                           if (rowPorts[1].Read()) currentButton = 12;
                           if (rowPorts[2].Read()) currentButton = 8;
                           if (rowPorts[3].Read()) currentButton = 4;
                           break;
                   }
                   currentColumn = (currentColumn == 3)? 0 : currentColumn + 1;
                   if(currentButton != lastButton)
                   {
                       if (currentButton != -1)
                       {
                           if (optionsSolved[currentButton - 1])
                           {
                               DisplayText("Button "+ options[currentButton - 1] + " Found", 8);
                               Thread.Sleep(1000);
                           }
                           else
                           {
                               if (option1 == -1)
                                   option1 = currentButton - 1;
                               else
                                   option2 = currentButton - 1;
                               DisplayText("Button = " + options[currentButton - 1], 24);
                               Thread.Sleep(1000);
                               if (option2 != -1 && option1 != option2)
                               {
                                   if (options[option1] == options[option2])
                                   {
                                       DisplayText(options[option1] + " == " + options[option2], 40);
                                       optionsSolved[option1] = optionsSolved[option2] = true;
                                   }
                                   else
                                       DisplayText(options[option1] + " != " + options[option2], 40);
                                   Thread.Sleep(1000);
                                   option1 = option2 = -1;
                               }
                           }
                       }
                       else
                       {
                           if (IsLevelComplete())
                           {
                               DisplayText("You Win!", 32);
                               Thread.Sleep(1000);
                               LoadMemoryBoard();
                               StartGameAnimation();
                           }
                           else
                           {
                               DisplayText("Select Button");
                           }
                       }
                   }
                   lastButton = currentButton;
               }
           });
           thread.Start();
       }
       protected void DisplayText(string text, int x = 12)
       {
           graphics.Clear(true);
           graphics.DrawRectangle(0, 0, 128, 32);           
           graphics.CurrentFont = new Font8x12();
           graphics.DrawText(x, 12, text);
           graphics.Show();
       }
       public void Run()
       {
           LoadMemoryBoard();
           StartGameAnimation();
           GameLoop();
       }
   }
}

There are several things happening in this class, so lets brake it down:

Initialization

In the App constructor, we're initializing an array of 16 characters options, which will basically be the board that associate each character to the keypad. The InitializePeripherals() method is used to group all the hardware initialization part.

Notice we're initializing the OLED display, specifying the resolution, the speed and the address, and right after, we pass in the display object to GraphicsLibrary, so we can easily draw text and shapes onto it.

LoadMemoryBoard() method is called after initializing the app, and what it does is iterate through the optionsPossible char array, and on each iteration calls PlaceCharacter() method twice, so that same letter is placed twice in the board randomly.

Output

What the StartGameAnimation() method do is call DisplayText() method which uses the GraphicsLibrary to first clear the entire screen, calls DrawRectangle() to draw a non-filled rectangle around the entire screen, and calls DrawText() to display text at a 8x12 font size with the specified x and y coordinates.

GameLoop

In the GameLoop() method is the logic to capture button presses on the Keypad. currentColumn iterates from 0 to 3, and on each iteration, it powers the corresponding column, and also reads all 4 rows, and when one of them reads a true value, depending on the row, we can determine which button is pressed. This happens every 50ms. Too fast could lead to several presses, too slow could not detect any button presses unless you push and hold.

We also save lastButton value at the end of each cycle, so we can compare a change with the currentButton, and update the display when theres a change of the button states.

In each iteration, we also call IsLevelComplete() to check if all the option values are found to display a You Win message and start the game all over again.

Program Class

Finally, create a new App class object and invoke the Run method. Your code should look like this:

using System.Threading;
namespace MemoryGame
{
   public class Program
   {
       public static void Main()
       {
           App app = new App();
           app.Run();
           Thread.Sleep(Timeout.Infinite);
       }
   }
}

Step 5 - Run the project

Click the run button in Visual Studio to see your memory game in action! Press a button and see the character that corresponds to it and try to look for its pair.

Memory Game running

Check out Netduino.Foundation!

This project is only the tip of the iceberg in terms of the extensive exciting things you can do with Netduino.Foundation.

  • It comes with a Huge Peripheral Driver Library with drivers for the most common sensors and peripherals available in the market.
  • All the peripheral drivers are simplified with built-in functionality, exposed by a clean, modern API.
  • This project is backed by a growing community that is constantly working on building cool connected things and always excited to help new-comers and discuss new projects.

References

Schematics, diagrams and documents

MemoryGame Circuit

Code

MemoryGame Complete project

Credits

Photo of Wilderness Labs

Wilderness Labs

Creators of Meadow. Makers of Netduino. We power connected things.

   

Leave your feedback...