Project updated to beta 6.2 (released on March 5th, 2022)
In this project, we'll build a game inspired by the popular logic game Simon using a Meadow F7 Micro, LEDs of different colors, push buttons and a piezo speaker, and write the code using Meadow.Foundation. Everything you need to build this project is included in the Wilderness Labs Meadow F7 w/Hack Kit Pro.
Push buttons (or momentary tactile buttons) are physical buttons that are used to complete a circuit when pressed and break the circuit when released. Push buttons come is a wide range of sizes and configurations, we'll use a common type with four (4) leads that are designed to fit standard prototype boards. When the button is pressed, all four leads are connected. You can read more about push buttons here.
Piezospeakers (or piezoelectric speaker) is a loudspeaker that uses the piezoelectric effect for generating sound. The initial mechanical motion is created by applying a voltage to a piezoelectric material, and this motion is typically converted into audible sound using diaphragms and resonators.
Meadow.Foundation is a platform for quickly and easily building connected things using.NET on Meadow. Created by Wilderness Labs, it's completely open source and maintained by the Wilderness Labs community.
If you're new working with Meadow, I suggest you go to the Getting Started w/ Meadow by Controlling the Onboard RGB LED project to properly set up your development environment.
Step 1 - Assemble the CircuitCreate a new Meadow Application project in Visual Studio 2019 for Windows or macOS; since our game is a variation of the game Simon, let's call it Simon.
Step 3 - Write the Code for the Simon ProjectAdd SimonGameClass
First class we're going to add to the project contains the main logic of the Simon game. Add a new class to your project named SimonGame and add the following code:
using System;
namespace Simon
{
public enum GameState
{
Start,
NextLevel,
Win,
GameOver,
}
public class SimonEventArgs : EventArgs
{
public GameState GameState { get; set; }
public SimonEventArgs(GameState state)
{
GameState = state;
}
}
public class SimonGame
{
static int MAX_LEVELS = 25;
static int NUM_BUTTONS = 4;
public delegate void GameStateChangedDelegate(object sender, SimonEventArgs e);
public event GameStateChangedDelegate OnGameStateChanged = delegate { };
public int Level { get; set; }
int currentStep;
int[] Steps = new int[MAX_LEVELS];
Random rand = new Random((int)DateTime.Now.Ticks);
public void Reset()
{
OnGameStateChanged(this, new SimonEventArgs(GameState.Start));
Level = 1;
currentStep = 0;
NextLevel();
}
public int[] GetStepsForLevel()
{
var steps = new int[Level];
for (int i = 0; i < Level; i++)
steps[i] = Steps[i];
return steps;
}
public void EnterStep(int step)
{
if (Steps[currentStep] == step)
{
currentStep++;
}
else
{
OnGameStateChanged(this, new SimonEventArgs(GameState.GameOver));
Reset();
}
if (currentStep == Level)
{
NextLevel();
}
}
void NextLevel()
{
currentStep = 0;
Level++;
if (Level >= MAX_LEVELS)
{
OnGameStateChanged(this, new SimonEventArgs(GameState.Win));
Reset();
return;
}
var level = string.Empty;
for (int i = 0; i < Level; i++)
{
Steps[i] = rand.Next(NUM_BUTTONS);
level += Steps[i] + ", ";
}
OnGameStateChanged(this, new SimonEventArgs(GameState.NextLevel));
}
}
}
Notice that the SimonGame class implements the following methods:
- Reset - Starts the game from the first level
- int[] GetStepsForLevel - Returns a series of steps of a certain level
- EnterStep - Verifies that the user's button pressed is the correct option
- NextLevel - Used to generate a new sequence of steps for a new level
It also contains GameStates and SimonEventArgs for when triggering an event when the game changes to any state. More on that next.
MeadowApp Class
For the main MeadowApp class, copy the following code below:
public class MeadowApp : App<F7Micro, MeadowApp>
{
int ANIMATION_DELAY = 200;
float[] notes = new float[] { 261.63f, 329.63f, 392, 523.25f };
Led[] leds = new Led[4];
PushButton[] pushButtons = new PushButton[4];
PiezoSpeaker speaker;
bool isAnimating = false;
SimonGame game = new SimonGame();
public MeadowApp()
{
var led = new RgbLed(Device, Device.Pins.OnboardLedRed, Device.Pins.OnboardLedGreen, Device.Pins.OnboardLedBlue);
led.SetColor(RgbLed.Colors.Red);
leds[0] = new Led(Device.CreateDigitalOutputPort(Device.Pins.D10));
leds[1] = new Led(Device.CreateDigitalOutputPort(Device.Pins.D09));
leds[2] = new Led(Device.CreateDigitalOutputPort(Device.Pins.D08));
leds[3] = new Led(Device.CreateDigitalOutputPort(Device.Pins.D07));
pushButtons[0] = new PushButton(Device.CreateDigitalInputPort(Device.Pins.MISO));
pushButtons[0].Clicked += ButtonRedClicked;
pushButtons[1] = new PushButton(Device.CreateDigitalInputPort(Device.Pins.D02));
pushButtons[1].Clicked += ButtonGreenClicked;
pushButtons[2] = new PushButton(Device.CreateDigitalInputPort(Device.Pins.D03));
pushButtons[2].Clicked += ButtonBlueClicked;
pushButtons[3] = new PushButton(Device.CreateDigitalInputPort(Device.Pins.D04));
pushButtons[3].Clicked += ButtonYellowClicked;
speaker = new PiezoSpeaker(Device.CreatePwmPort(Device.Pins.D11));
Console.WriteLine("Welcome to Simon");
SetAllLEDs(true);
game.OnGameStateChanged += OnGameStateChanged;
game.Reset();
led.SetColor(RgbLed.Colors.Green);
}
void ButtonRedClicked(object sender, EventArgs e)
{
OnButton(0);
}
void ButtonGreenClicked(object sender, EventArgs e)
{
OnButton(1);
}
void ButtonBlueClicked(object sender, EventArgs e)
{
OnButton(2);
}
void ButtonYellowClicked(object sender, EventArgs e)
{
OnButton(3);
}
void OnButton(int buttonIndex)
{
Console.WriteLine("Button tapped: " + buttonIndex);
if (isAnimating == false)
{
TurnOnLED(buttonIndex);
game.EnterStep(buttonIndex);
}
}
void OnGameStateChanged(object sender, SimonEventArgs e)
{
var th = new Thread(() =>
{
switch (e.GameState)
{
case GameState.Start:
break;
case GameState.NextLevel:
ShowStartAnimation();
ShowNextLevelAnimation(game.Level);
ShowSequenceAnimation(game.Level);
break;
case GameState.GameOver:
ShowGameOverAnimation();
game.Reset();
break;
case GameState.Win:
ShowGameWonAnimation();
break;
}
});
th.Start();
}
void TurnOnLED(int index, int duration = 400)
{
leds[index].IsOn = true;
speaker.PlayTone(notes[index], duration);
leds[index].IsOn = false;
}
void SetAllLEDs(bool isOn)
{
leds[0].IsOn = isOn;
leds[1].IsOn = isOn;
leds[2].IsOn = isOn;
leds[3].IsOn = isOn;
}
void ShowStartAnimation()
{
if (isAnimating)
return;
isAnimating = true;
SetAllLEDs(false);
for (int i = 0; i < 4; i++)
{
leds[i].IsOn = true;
Thread.Sleep(ANIMATION_DELAY);
}
for (int i = 0; i < 4; i++)
{
leds[3 - i].IsOn = false;
Thread.Sleep(ANIMATION_DELAY);
}
isAnimating = false;
}
void ShowNextLevelAnimation(int level)
{
if (isAnimating)
return;
isAnimating = true;
SetAllLEDs(false);
for (int i = 0; i < level; i++)
{
Thread.Sleep(ANIMATION_DELAY);
SetAllLEDs(true);
Thread.Sleep(ANIMATION_DELAY * 3);
SetAllLEDs(false);
}
isAnimating = false;
}
void ShowSequenceAnimation(int level)
{
if (isAnimating)
return;
isAnimating = true;
var steps = game.GetStepsForLevel();
SetAllLEDs(false);
for (int i = 0; i < level; i++)
{
Thread.Sleep(200);
TurnOnLED(steps[i], 400);
}
isAnimating = false;
}
void ShowGameOverAnimation()
{
if (isAnimating)
return;
isAnimating = true;
speaker.PlayTone(123.47f, 750);
for (int i = 0; i < 20; i++)
{
SetAllLEDs(false);
Thread.Sleep(50);
SetAllLEDs(true);
Thread.Sleep(50);
}
isAnimating = false;
}
void ShowGameWonAnimation()
{
ShowStartAnimation();
ShowStartAnimation();
ShowStartAnimation();
ShowStartAnimation();
}
}
There are several things happening in this class. The most important thing to note here is that Simon works as a state machine, meaning that when starting a game, passing to a next level, winning or losing the game are all game states and when changing states, an event is triggered and OnGameStateChanged event handler will run the appropriate routine depending on the state of the game.
Input - Push Buttons
The only way the user interacts with the Simon game is through four push buttons. In the code, these push buttons are declared on the top, named buttons as an array of four items, and in the MeadowApp's Constructor, we instantiate each PushButton and after we declare the handlers for each one, which all of them invoke the OnButton method that ultimately checks if the button pressed is the correct step of the game's sequence.
OutPut - LEDs and PiezoSpeaker
The peripherals used for output in this project are the LEDs and the PiezoSpeaker. Using Meadow.Fundation we can simply instanciate four(4) LED and PiezoSpeaker object in the InitializerPeripherals method, where we specify in which pins these components are connected to the Netduino.
Animations
To give the user feedback on which state the game is in, we wrote the following animation methods:
- void ShowStartAnimation()
- void ShowNextLevelAnimation(int level)
- void ShowSequenceAnimation(int level)
- void ShowGameOverAnimation()
- void ShowGameWonAnimation()
These animation methods are explicit scripts of turning LEDs on and off in a certain sequence to express different states of the game.
Program Class
using Meadow;
using System.Threading;
namespace Simon
{
class Program
{
static IApp app;
public static void Main(string[] args)
{
if (args.Length > 0 && args[0] == "--exitOnDebug") return;
// instantiate and run new meadow app
app = new MeadowApp();
Thread.Sleep(Timeout.Infinite);
}
}
}
Step 5 - Run the ProjectClick the run button in Visual Studio to see your Simon game in action! Notice the start animation, and all the LEDs flashing a certain number of times to indicate in which level you currently are!
This project is only the tip of the iceberg in terms of the extensive exciting things you can do with Meadow.Foundation.
- It comes with a huge peripheral driver library with drivers for the most common sensors and peripherals.
- The peripheral drivers encapsulate the core logic and expose a simple, clean, modern API.
- This project is backed by a growing community that is constantly working on building cool connected things and are always excited to help new-comers and discuss new projects.
Comments