Color Sequence Memory Game

by EHMD28 in Circuits > Arduino

129 Views, 0 Favorites, 0 Comments

Color Sequence Memory Game

Design Final Project

This project recreates the Simon memory game using an Arduino, push buttons, and LEDs. At the start of each round, the LEDs display a color sequence. The length of the color sequence starts at one, and it increases by one each round. After the sequence is displayed, the user then must push the buttons in the same order that the lights flashed. If the player succeeds, they move on to the next round. If they fail, the game will end and their score will scroll on the seven segment display.

Supplies

Components

  1. 1 Arduino Uno R3
  2. 1 common cathode 7-segment display
  3. 1 BCD to 7-segment display decoder
  4. 2 hex inverter chips
  5. 4 differently colored LEDs
  6. 4 push-buttons
  7. 6 1kΩ resistors

Software

  1. Arduino IDE

Connect Power and Ground to Arduino

Smooth Waasa.png

The entire project is powered from the Arduino. Connect the positive power rails of the breadboard to the 5V pin of the Arduino and connect the negative power rail to any of the ground pins of the Arduino. Then, add jumper wires between the top and bottom power rails.


Note: For each part of the circuit, I'm going to add an image of the circuit made using TinkerCAD. There is also a schematic at the end if you are more comfortable reading that. Also, you should not add/remove circuit components while the circuit is being powered (this can damage the components). Always double-check your wiring before turning the Arduino on.

Add Buttons to Breadboard

Smooth Waasa (1).png

For each of the four push-buttons, one terminal should be connected to digital pins 2 - 5 of the Arduino, and the other terminal should be connected to the ground pin of the Arduino. The reason to skip over digital pins 0 (RX) and 1 (TX) are because they are used for uploading code to the Arduino. Having them connected would interfere with uploading, so it's easier to just leave them unconnected. In order from pin 2 to 5, the buttons are red, blue, yellow, and green.

Add LEDs to Breadboard

Smooth Waasa (2).png

Add the four LEDs to the breadboard in the same order of the push-buttons. The cathode of each LED should be connected to digital pins 5 through 9, and the anode should be connected to ground using a 1 kΩ resistor.

Add 7-Segment Decoder

Smooth Waasa (3).png

Connect the power and ground pins (top left and bottom right respectively) of the 7-segment decoder to the power/ground rails. You also need to set the configuration pins of the seven segment decoder. For the real life circuit, I used an HD74LS48 chip, but any BCD to seven-segment display decoder should do. TinkerCAD only has a CD4511 decoder chip, which has a slightly different pinout. The lamp test pin (used to turn on all segments) should be held at HIGH (so it's disabled). The blanking input pin should be held at HIGH so it's disabled. The CD4511 also has a latch enable pin (for displaying the same value regardless of inputs) should be held at LOW.

Add Seven Segment Display

Smooth Waasa (4).png

The FND500 seven-segment display is a common-cathode display. This means that the common pins at the middle-top and middle-button should be connected to LOW. When an input is HIGH, the corresponding segment turns on, and when an input is LOW, the corresponding segment is off. Connect each output from the decoder to the corresponding input of the the display. The pinouts are pretty confusing, so make sure you connect everything correctly. Since the decimal point is never used, connected to ground with a wire. The common cathode pins should be connected to ground with two 1 kΩ resistors. After that, connect the four input pins of the decoder to digital pins 10 - 13 in order of least-significant to most-significant bit.


Note: If you experience inconsistent brightness problems, try adding a separate resistor for each pin and just using wires for the common cathode.

Downloads

Label Pins in Code

In Arduino IDE, create a new sketch. Above the setup function, add the following code. This code is used for mapping each digital pin to its corresponding function. The enum /* name */ : uint8_t syntax means that each value in the enum is an unsigned, 8-bit integer. Throughout this project, I will use uint8_t over unsigned int because uint8_t takes up less memory, and the range of an int isn't necessary for this project.


Note: Code formatting on Instructables doesn't work well. I spent an hour or so trying different methods, but none of them worked reliably. Look at the code.ino file in the Github repository if you are ever confused.


namespace Buttons {
/* The value of the enum variant corresponds to the pin. PinType 0 (RX) and
pin 1 (TX) should not be connected to anything, because it interferes with
uploading code. */
enum PinType : uint8_t {
RED = 2,
BLUE = 3,
YELLOW = 4,
GREEN = 5,
};
}

namespace Leds {
// The value of the enum variant corresponds to the pin.
enum PinType : uint8_t {
RED = 6,
BLUE = 7,
YELLOW = 8,
GREEN = 9
};
}

namespace Display {
enum PinType : uint8_t {
// Least significant bit
BIT_ONE = 10,
BIT_TWO = 11,
BIT_THREE = 12,
// Most significant bit
BIT_FOUR = 13
};
}


It will also be helpful to have a set of constants for representing specific delays.

namespace Delays {
enum : uint16_t {
LONG = 1000,
MEDIUM_LONG = 750,
MEDIUM = 500,
SHORT = 250,
};
};

Set Up Each Pin

Configure each pin using the builtin pinMode() function. For the button pins, they should be configured as INPUT_PULLUP. When a pin is not connected to power or ground, it is called a "floating pin." Trying to read from a floating pin will result in random behavior from surrounding electrical noise. INPUT_PULLUP pins fix this by configuring the Arduino's internal pull-up resistors. Because of the pull-up resistors, floating inputs will be treated as HIGH, which is why the buttons connect to ground (LOW). The LED and display pins should be configured as OUTPUT. See Digital Pins | Arduino Documentation for more details.

void setup() {
/* Button pins should be configured as INPUT_PULLUP. A HIGH value indicates
the button isn't being pushed, and a LOW value indicates that it is being
pushed. */
pinMode(Buttons::RED, INPUT_PULLUP);
pinMode(Buttons::BLUE, INPUT_PULLUP);
pinMode(Buttons::YELLOW, INPUT_PULLUP);
pinMode(Buttons::GREEN, INPUT_PULLUP);
/* LED pins should be configured as OUTPUT. The circuit should be designed
so that each LED only draws up to 20 mA of current. */
pinMode(Leds::RED, OUTPUT);
pinMode(Leds::BLUE, OUTPUT);
pinMode(Leds::YELLOW, OUTPUT);
pinMode(Leds::GREEN, OUTPUT);
/* Display pins should be configured as outputs, since they are used to
drive the seven segment display. */
pinMode(Display::BIT_ONE, OUTPUT);
pinMode(Display::BIT_TWO, OUTPUT);
pinMode(Display::BIT_THREE, OUTPUT);
pinMode(Display::BIT_FOUR, OUTPUT);
}

Add Color Type

It is useful to have a way of representing colors agnostic of the button and LED pins. At the top of the file, add this enum.


namespace Colors {
enum ColorType : uint8_t {
RED,
BLUE,
YELLOW,
GREEN
};
}

Add Game State

Now, add a struct for representing the game state. The struct will have three fields: a boolean representing if the player has lost, the sequence of colors, and the current round (which corresponds the length of the sequence). Also create a instance of the Gamestate struct called game_state and a function for initializing that instance.

struct GameState {
/* The longest a sequence can be is 256 colors, because that is the largest
array which can be indexed using an unsigned, 8-bit integer like `round`. */
Colors::ColorType sequence[UINT8_MAX + 1];
/* Represents the current length of the sequence, starting from 1. 0
represents that no colors have been initialized. */
uint8_t round;
/* Represents if the player has made a mistake yet. Currently, the game
doesn't have a win state. */
bool has_lost;
};

Add Game Logic

The logic for the game is fairly simple. In the loop() function, add the following code.

void loop() {
if (!game_state.has_lost) {
start_round();
/* Delay between the end of one round and the start of the next. */
delay(Delays::LONG);
} else {
display_loss();
}
}




During each round, a random color is generated and added to the sequence. In order to generate a new random sequence each time the user plays, you need to call randomSeed() in the setup function. One good way to seed the random number generation is by calling analogRead() on an unconnected analog pin.

void generate_next_color() {
/* Generates a random color from RED until the last color (GREEN). The plus
one is because random is exclusive. */
uint8_t index = random(Colors::RED, Colors::GREEN + 1);
Colors::ColorType color = Colors::colors[index];
game_state.sequence[game_state.round] = color;
game_state.round++;
}

void setup() {
/* Seeding the random number generation is necessary to have different
results each time. Calling analogRead on an unconnected pin gives a
relatively random seed. */
randomSeed(analogRead(A0));
initalize_game_state();

// vvvv other code vvvv
}


After a new color is generated and added to the sequence, the sequence is displayed to the user using the LEDs.

void display_sequence() {
for (uint8_t i = 0; i < game_state.round; i++) {
Leds::PinType pin = led_pin_of(game_state.sequence[i]);
digitalWrite(pin, HIGH);
delay(Delays::MEDIUM_LONG);
digitalWrite(pin, LOW);
delay(Delays::SHORT);
}
}


For each color in the sequence, the user needs to push a button. The button can be pressed or held and it will still count as one button press. In the the Buttons namespace, add an array containing all of the buttons and an int containing the number of buttons. The "constexpr" keyword is used because it can be optimized more than a simple const.

namespace Buttons {
// ^^^^ Pins enum ^^^^

constexpr Buttons::PinType buttons[] = {
Buttons::RED,
Buttons::BLUE,
Buttons::YELLOW,
Buttons::GREEN
};
constexpr uint8_t num_buttons = sizeof(buttons) / sizeof(buttons[0]);
}

bool button_is_pressed(Buttons::PinType pin) {
return digitalRead(pin) == LOW;
}

Colors::ColorType get_button_input() {
Buttons::PinType button_pin = (Buttons::PinType) 0;
Leds::PinType led_pin;
while (button_pin == 0) {
/* Check to see if any of the buttons are being pressed */
for (uint8_t i = 0; i < Buttons::num_buttons; i++) {
if (button_is_pressed(Buttons::buttons[i])) {
button_pin = Buttons::buttons[i];
led_pin = button_to_led(button_pin);
digitalWrite(led_pin, HIGH);
break;
}
}
}
/* When a button is held, it should only count as one press. */
while (button_is_pressed(button_pin)) {
/* This delay is the prevent the Arduino from executing this polling
loop more times than is necessary. It can be removed if necessary. */
delay(Delays::SHORT);
}
digitalWrite(led_pin, LOW);
return button_to_color(button_pin);
}


Finally, put all of this logic into a start_round() function.

void start_round() {
generate_next_color();
display_sequence();
for (uint8_t i = 0; i < game_state.round; i++) {
Colors::ColorType color = get_button_input();
if (color != game_state.sequence[i]) {
game_state.has_lost = true;
break;
}
}
}

Add Loss Display

When the player loses, all of the LEDs should turn on, then the player's score will be displayed using the seven-segment display. The display can only show one digit at a time, so the score has to be displayed digit by digit with a pause at the end. In the Display namespace, add a function for converting a binary digit to a the corresponding output pins.

namespace Display {
// ^^^^ Pins enum ^^^^

constexpr Display::PinType pins[] = {
Display::BIT_ONE,
Display::BIT_TWO,
Display::BIT_THREE,
Display::BIT_FOUR
};
constexpr uint8_t num_pins = sizeof(pins) / sizeof(pins[0]);

void display_digit(uint8_t digit) {
for (uint8_t i = 0; i < num_pins; i++) {
uint8_t bit = bitRead(digit, i);
if (bit == 1) {
digitalWrite(pins[i], HIGH);
} else {
digitalWrite(pins[i], LOW);
}
}
}
}


Then, add a function for displaying each digit of the number. The last digit of the number can be found using the modulo (remainder) operator. To display the digits in order, it's necessary to "shift" the number to the right by dividing by a power of 10.

namespace Display {
// ^^^^ display_digits() ^^^^

void display_score(uint8_t score) {
/* Can't take the logarithm of 0. */
if (score == 0) display_digit(0);
/* This method of counting the number of digits in a number works
because, for any 2 digit number n, 10 <= n < 100. Taking the logarithm
base 10 of each expression shows that 1 <= log(n) < 2. This reasoning
can be generalized for any number of digits. Normally, you would need to
floor the value of the logarithm, but C++ automatically ignores decimal
places when converting from a decimal to an integer. */
const uint8_t num_digits = log10(score) + 1;
for (uint8_t i = 0; i < num_digits; i++) {
/* A number modulus 10 extracts the least significant digit from the
number. To start with the most significant digit, divide 10^n where
n is 1 less than the number of digits left. For example, 123 divided
by 100 becomes 1.23 (truncated to 1), 1 % 10 is 1. For the second
digit, 123 becomes 12.3 (12) % 10 = 2. So on and so forth. */
uint8_t truncated_num = floor(score / pow(10, num_digits - i - 1));
uint8_t digit = truncated_num % 10;
display_digit(digit);
delay(Delays::MEDIUM);
}
}
}


Now, add the display_loss function above setup()

void display_loss() {
Leds::all_on();
/* The current round is the one the player failed on, so their score is 1
less than that. */
Display::display_score(game_state.round - 1);
delay(Delays::MEDIUM);
Display::reset();
delay(Delays::LONG * 2);
}

Add Startup Sequence

To test that the LEDs are all working and to give the player time to prepare, there is a startup sequence. All the LEDs will turn on in ascending order, then they will alternate between being on and off.

void display_start() {
/* Light up in ascending order. */
digitalWrite(Leds::RED, HIGH);
delay(Delays::MEDIUM);
digitalWrite(Leds::BLUE, HIGH);
delay(Delays::MEDIUM);
digitalWrite(Leds::YELLOW, HIGH);
delay(Delays::MEDIUM);
digitalWrite(Leds::GREEN, HIGH);
delay(Delays::MEDIUM);
/* Light up in alternating order. */
Leds::reset();
digitalWrite(Leds::RED, HIGH);
digitalWrite(Leds::YELLOW, HIGH);
delay(Delays::MEDIUM);
Leds::reset();
digitalWrite(Leds::BLUE, HIGH);
digitalWrite(Leds::GREEN, HIGH);
delay(Delays::MEDIUM);
}


And that's it. The game is now fully functional. You can find the full source code in the Github repository.