Introduction: STEM-Game Platform Lesson Plan - Device & Software Development Course

This is a complete beginner level course, emphasizing STEM elements, centered around software development for LED bar display games and mini apps running on an Arduino device.

In the implementing of the Arduino 'C' software, along with the building of the associated Micro-Controller based hardware, the maker will gain some valuable knowledge of computer Science, Arduino Technology, Electronics, Engineering, and Math along with logic and functional design.

Students (makers) get to quickly see real world consequences to their coding; not just numbers and text on a computer screen. Hands on interaction with solid feedback is one of the best learning vehicles.

By the end of the lessons, young people and neophytes should be able to create their own functionality including LED display games and mini applications.

If there are any words in this document that you are unsure of its definition, as used, please get it defined.

This Instructable is broken up into lessons for use in an education course format, while being just as well suited for individual self study and skill building.

In a formal class room setting you may want to parse out the material such that the students are not exposed to solutions prematurely. In the lessons, after some instructions, you'll often see a line with just “ . . . “ on it. This is where the student is to go about fulfilling the goals of the lesson.

There are 14 lessons here. If desired, more formal lessons can be formed from the sections given as optional or as a (b) portion of a lesson. If fewer lessons are wanted then these sections be glossed over to then resume with the next lesson. Some lessons, which take more time, could be broken up.

Supplies

In order to complete the lessons 1 & 2, you'll need these parts:

In order to complete the lessons beyond lesson #2, you'll need a mini STEM/Game Platform as built in the instructable: mini_STEM_Arduino LED Game Platform[ 2023 Update: Now you can order a PCB board and a 3D printed enclosure from the likes of PCBWay.com (very reasonable $) given Gerber & .stl files that you can obtain via this document along with detailed information: my public docs.google.com/document...]

To serve the STEM education goal, it is best that you (the student) builds one yourself. The associated required parts are listed therein.

Step 1: Lesson 1 - Starting Simple

We'll start with a simple combination of a Nano MCU module wired (via a prototyping solderless breadboard) to a button and one LED.
Put together these components as illustrated, and prepare to use it with upcoming software.

We will need some software programming in an Arduino 'sketch' project.
Refer to this Arduino development link: https://www.arduino.cc/en/Guide/ArduinoNano

First lets examine the basic minimum needed code sections of the software we'll be using.
The header section has overview information, configuration data, and global program data definitions as needed.

// ================================================================
// Program 1: Minimal Arduino 'C' Program
// using a Micro Controller, one LED and one Button
// ---------------- hardware dependent section ------------------
// constants won't change. They're used here to set pin numbers:
const int LED_OUT = 2; // pin attached to an LED, connects to arduino pin 2, “D2”
const int BTN_INPUT = 18; // the button input pin, connects to arduino pin 18, “A4”
const int PSUDOGND = 15; // used as Ground for the button

Next comes the 'setup' routine:

//=================================================================
// the setup routine runs once on power up or when you press reset:
void setup() {
pinMode(LED_OUT, OUTPUT);
digitalWrite(LED_OUT, HIGH); // start w/ LEDs off

pinMode(BTN_INPUT, INPUT); // button pins are inputs
digitalWrite(BTN_INPUT, HIGH); // enable internal pullup; button start in high position; pressed -> LOW
pinMode(PSUDOGND, OUTPUT); // provide Ground (-V) for the button
digitalWrite(PSUDOGND, LOW);
}

And then the 'loop' code:

//=================================================================
// the loop routine runs over and over again forever on
void loop() {
digitalWrite(LED_OUT, LOW);
delay(750); // allow the LEDs to Shine …
digitalWrite(LED_OUT, HIGH);
delay(750); // create an Off period
}

For study of elements of the software used herein, reference to:
https://www.arduino.cc/reference/en/

New elements used above:

C language elements                                       Arduino specific functions<br>--------------------                                      --------------------------
// a comment line pinMode()
variable type declaration eg: int btn_cnt; digitalWrite()
a function eg: void my_func() … delay()
function call eg: pinMode(); setup()
statement assigning a value to a variable: btn_cnt=8; loop()
code block, eg: { btn_cnt=8; delay(x); . . . }

As new C programming elements are used in the code an instructor should introduce them. In the case of self study please read about them enough to feel you could make use of them. Note: like with any other language, you don't have to know all about the C programming language to make use of it; just the elements and constructs you are going to use.

The above three sections of code make up a valid Arduino sketch. Put them into a file named “Lesson_1.ino” and that in a folder named “Lesson_1” which is located where your Arduino IDE looks for sketches. Alternately within the Arduino IDE use 'File – New', cut and paste in the sections of code above, then 'Save As …' with the filename “Lesson_1.ino”.

As needed refer to : https://www.arduino.cc/en/Guide/Environment

Compile and execute the sketch. It should download into your hardware device, and you see the LED flash.

Step 2: Lesson 1B - Let's Use the Button Input

Now let's use the button to stop the LED from flashing. We will do this with a 'while' statement. Who's format is:

    while (condition) statement;

To do this add the following line at the end of the 'loop' code.

    while (digitalRead(BTN_INPUT) == LOW)  ; 	// stay here while button pushed

There is no code needed to be executed within its statement's expression clause, while the condition is true. So following 'while( )' we have a do nothing statement, consisting only of the required “;”. This while statement does nothing except continuously check the condition until it is found to be false. Then it allows program execution to continue on its way.

Implement and test this.

. . .

Let's improve our 'loop' code so that the button can be detected when the LED is on or off and then pause that state. To do this we will add a local variable 'ledON', and a 'while(true)' block so as to stay in the scope of the 'loop' routine.

The reason for the added “while(true) {}“ block will be better explained in a later lesson.

Here is the new 'loop' routine:

void loop() {
bool ledON=LOW;

while(true) { // do this block of code forever
digitalWrite(LED_OUT, ledON);
ledON = ! ledON; // toggles the state
delay(750); // allow the LEDs to display its state

while (digitalRead(BTN_INPUT) == LOW) ; // stay here while button pushed
}
}

Try out this coding and verify that it behaves as we have intended.

Step 3: Lesson 2 - Now for a Little Game

We will now make what we have into a little game. Adding a variable for how long the LED stays on or off. Have this time decrease whenever the button is pressed and the LED is on, but have it reset if the button is pressed when the LED is off.

So the goal of this game could be to see how many times in a role you can make the LED flash faster.

Study all the new code, involving the new variables ('onMSecs' and 'btnPush') as well as the operating definition of the 'break' statement.

void loop() {	 // Lesson 2a
bool ledON=LOW;
bool btnPush;
int onMSecs;

onMSecs = 400;
while(true) {
ledON = ! ledON; // toggles the state
digitalWrite(LED_OUT, ledON);
delay(onMSecs); // allow the LEDs to display its state

btnPush = (digitalRead(BTN_INPUT) == LOW);
if (btnPush) {
if (ledON==LOW) onMSecs -= 40;
else break;
}

// wait for button not to be pushed
while(btnPush) btnPush=(digitalRead(BTN_INPUT) == LOW); // stay here while button pushed
}
}

Step 4: Lesson 2b - Adding Some Sound

Now lets add some sounds to our activities. We will use it to give positive as well as negative feedback.

Add a passive piezoelectric sounder to the breadboard with its +V going to D13 and the other pin jumpered to GRD. Refer to photos.

First we need to know where the audio device is to be connected. Place the following line in the global data definition area:

const int AUDIO_OUT = 13;

Add code to initialize that Digital IO in the 'setup()' routine:

pinMode(AUDIO_OUT, OUTPUT);
digitalWrite(AUDIO_OUT, LOW);

In 'loop' we will expand our 'if (ledON==LOW) statement; else statement;' to use blocks of statements, adding in calls to sound sub-routine (functions), like this:

      if  (ledON==LOW) {
onMSecs -= 40;
beep(20);
} else {
onMSecs = 400;
boop(300);
}

(For greater comprehension and appreciation, the following support routines could be first coded inline.)

The following support functions are added to the end of the file. There is a new function used in the code 'delayMicroseconds(int)' you should look it up and read about this (see study reference link above).

// =========================== Support Functions ===================
void beep(int msecs){ // works OK with a Buzzer, best with a Speaker
for (int i=0; i<msecs; i++) { // loop for so many 'msecs'
digitalWrite ( AUDIO_OUT, HIGH);
delayMicroseconds(500);
digitalWrite ( AUDIO_OUT, LOW);
delayMicroseconds(500);
}
delay(msecs); // pause for the same # of 'msecs'
}

void boop(int msecs){ // produces a lower frequency tone then does 'beep()'
for (int i=0; i<(msecs/4); i++) { // loop for 'msecs' milliseconds
digitalWrite ( AUDIO_OUT, HIGH);
delay(2);
digitalWrite ( AUDIO_OUT, LOW);
delay(2);
}
delay(msecs/2); // pause for 1/2 as long
}

We can also add a line to the setup code, so that we know when the setup code has executed & that audio is working.

  beep(50);

You can now have fun playing this mini game. Try and see how many hits in a row you can get, and how fast you can get the LED to flash. When you 'hit' the LED ON flash you get a rewarding little beep; when you miss you are buzzed.

Here is the full code:

// ================================================================
// Program 2: Minimum Arduino 'C' Program
// Mini Flashing LED Game, using a Micro Controller, one LED and one Button
//

// ---------------- hardware dependent section ------------------
// constants won't change. They're used here to set pin numbers:
const int LED_OUT = 2; // pin attached to an LED
const int BTN_INPUT = 18; // the button input pin, goes to arduino pin 18, “A4”
const int PSUDOGND = 15; // used as Ground for the button
const int AUDIO_OUT = 13;

//=================================================================
// the setup routine runs once on power up or when you press reset:
void setup() {
pinMode(LED_OUT, OUTPUT);
digitalWrite(LED_OUT, HIGH); // start w/ LEDs off

pinMode(BTN_INPUT, INPUT); // button pins are inputs
digitalWrite(BTN_INPUT, HIGH); // enable internal pullup; buttons start in high position; pressed -> LOW
pinMode(PSUDOGND, OUTPUT); // provide Ground (-V) for the button
digitalWrite(PSUDOGND, LOW);

pinMode(AUDIO_OUT, OUTPUT);
digitalWrite(AUDIO_OUT, LOW);
beep(50);
}

//=================================================================
// the loop routine runs over and over again forever on
void loop() { // Lesson 2b
bool ledON=LOW;
bool btnPush;
int onMSecs;

onMSecs = 400;
while(true) {
ledON = ! ledON; // toggles the state
digitalWrite(LED_OUT, ledON);
delay(onMSecs); // allow the LEDs to display its state

btnPush = (digitalRead(BTN_INPUT) == LOW);
if (btnPush) {
if (ledON==LOW) {
onMSecs -= 40;
beep(20);
} else {
onMSecs = 400;
boop(400);
}
}

// wait for button not to be pushed
while(btnPush) btnPush=(digitalRead(BTN_INPUT) == LOW); // stay here while button pushed
}
}

// =========================== Support Functions ===================
void beep(int msecs){ // works for OK with Buzzers, best with Speakers
for (int i=0; i<msecs; i++) { // loop for so many 'msecs'
digitalWrite ( AUDIO_OUT, HIGH);
delayMicroseconds(500);
digitalWrite ( AUDIO_OUT, LOW);
delayMicroseconds(500);
}
delay(msecs); // pause for the same # of 'msecs'
}

void boop(int msecs){ // produces a lower frequency tone then does 'beep()'
for (int i=0; i<(msecs/4); i++) { // loop for 'msecs' milliseconds
digitalWrite ( AUDIO_OUT, HIGH);
delay(2);
digitalWrite ( AUDIO_OUT, LOW);
delay(2);
}
delay(msecs/2); // pause for 1/2 as long
}

Try this:

In setup() replace the beep(50); line with:

  beep(50);  beep(50);  beep(150);

Compile-download and run the code. Note what you hear.

Comment out (with // or /* … */) the last line inside the support function beep().

  // delay(msecs);          // pause for the same # of 'msecs' 

Compile-download and run the code. Note what you hear. To listen to it again, unplug and re-plug in the micro USB cable or push the 'reset' button on the micro-controller board. BTW: Ignore any burst audio produced just as the micro-controller gets power or is reset; which is prior to the code execution.

When you have commented out the last line of code inside the beep() function, and retested how the code behaves,

what's the difference and why ?

Undo the changes just made for testing above; so that the individual calls to beep() can be discerned.

Let's now get some exposure to what might happen when things are not as needed or wanted.

Comment out one line at a time of the beep() code; starting with the “// void beep(...” first line.

Then note what happens during re-compiling. If red error text is outputted, briefly study it over, trying to correlate it to your edit. Reinstate one line before commenting out the next.

If compilation is successful and it downloads, what's the difference in the behavior of our mini game ?

Step 5: Lesson 3 - Build a Mini STEM Platform Device

Build (or obtain) your mini STEM hardware platform. See how to put together this hardware platform in the Instructable:

Mini-STEM-LED-Game Platform

Let's make a modified version of the “Speed Flasher” code from lesson 2, which will work on this “Educational Arduino Game/Activity hardware Platform”.

Change out the header/configuration code to this:

// ================================================================
// Program Lesson 3:
// using a Micro Controller, 8 LED 4 color bar and 4 Button module
//

// ---------------- hardware dependent section ------------------
// constants won't change, unless the hardware changes
const int AUDIO_OUT = 13;
const int AUDIO_NEG = 12; // acts as a psuedo ground & audio enable/disable

// ---------- 8 LED configuration data
const int nleds=8;
const int lites[] = {9, 8, 7, 6, 5, 4, 3, 2}; //the LED pin assignments
#define ON_STATE 0
const int PSUDOVCC = 10; // supplies +V to 8 LED module

// ---------- button config. data
const int button[] = {16, 17, 18, 19}; //The four button input pins
#define PRESSED_STATE 0
const int PSUDOGND = 15; // used by 4 Key module

//=================================
// Temporary #defines, so we can test the old code with the new hardware and its configuration data
#define LED_OUT lites[7]
#define BTN_INPUT button[2]

The setup for the new hardware platform should look something like this:

void setup() {
// ------------ setup LED pins as outputs
for (int x = 0; x < nleds; x++)
{
pinMode(lites[x], OUTPUT);
digitalWrite(lites[x], (!ON_STATE)); // start w/ LEDs off
}
// pinMode(PSUDOVCC, OUTPUT); // provide +v for LED bar module
// digitalWrite(PSUDOVCC, HIGH);
pinMode(PSUDOVCC, INPUT); // let it be externally provided (via the red wire)

// ------------ setup the four button module
for (int x = 0; x < 4; x++)
{
pinMode(button[x], INPUT); // button pins are inputs
digitalWrite(button[x], HIGH); // enable internal pullup; buttons start in high position; logic reversed
}
pinMode(PSUDOGND, OUTPUT); // provide Ground (-V) for the button module
digitalWrite(PSUDOGND, LOW);

// ------------ setup & init test for the Audio
pinMode(AUDIO_OUT, OUTPUT);
digitalWrite(AUDIO_OUT, LOW);
pinMode(AUDIO_NEG, OUTPUT);
digitalWrite(AUDIO_NEG, LOW);
beep(50); boop(75);
}

For proper behavior, one line in the loop() code needs to be changed:

was: if (ledON==LOW) {

needs to be: if (ledON==ON_STATE) {

Given the above code change, the “Speed Flasher” single LED-Button game should still work on the hardware setup used in lesson 2, and work on this new hardware; utilizing the third button from the left and the last LEDs on the right. Try it.

Also notice that on reset, activity on D13 (which BTW lights the on-board LED) will no longer result in audio due to not yet having a return -V (ground) path.

Now create a code loop to test/verify that the buttons all work.

Have each button light up a unique LED while it is pressed, else be off.

See if you can accomplish this without further instruction.

. . .

Here is my code for this activity:

void loop() { // Lesson_3a reflect button states on associated LEDs
bool ledON;

while(true) {
// reflect button states on assosiated LEDs
if (digitalRead(button[0])==PRESSED_STATE) digitalWrite(lites[0], ON_STATE);
if (digitalRead(button[1])==PRESSED_STATE) digitalWrite(lites[1], ON_STATE);
if (digitalRead(button[2])==PRESSED_STATE) digitalWrite(lites[2], ON_STATE);
if (digitalRead(button[3])==PRESSED_STATE) digitalWrite(lites[3], ON_STATE);

delay(1); // allow the LEDs time to shine

// reset the LEDs to off
for (int i=0; i<4; i++) {
digitalWrite(lites[i], !ON_STATE);
}

ledON = !ledON;
digitalWrite(lites[nleds-1], ledON); // alternate On/Off the last LED
}
}

This code will reflect any combination of buttons 1-4 displayed on LEDs 1-4 respectively.

Try it.

The last LED will be lit ½ the time but will appear to us to be on, just dimmer than full on.

I like to always have some visual &/or audio presented (at least occasionally) when there is no user interaction. This is so that a device is not mistaken for being turned off. If such a device is left and forgotten it could eventually discharge and possibly damage its battery or attached power-pack.

To add some sense of 'waiting...' we can make this, otherwise boring, LED Flash.

To do this add, within the loop() code, the variable declaration:

int tic=0;

And replace:

 ledON = ! ledON;

with:

    if (tic++ > 750) {
tic = 0;
ledON = ! ledON;
}

The last LED will now flash about every 1.5 seconds.

Step 6: Lesson 4 - Make a Blip Scan Across the Display

Create code to have a blip scan across the LEDs. Say, about every one to two second.

. . .

Hope you had some success.

Here is one set of code to do this:

void loop() { // Lesson 4, scanning the LEDs
for(int blip=0; blip<nleds; blip++) {
digitalWrite(lites[blip], ON_STATE);
delay(150); // flash the LED (blip) for a bit
digitalWrite(lites[blip], !ON_STATE); // turn last lit LED off
}
}

Try it.

Make it so that we get a beep if button 1 is pressed while the fifth LED is lit.
Add, just before the delay(150) these lines of code:

    // if button-1 is press when the 5th LED is on give a beep
if (digitalRead(button[0])==PRESSED_STATE && blip==4) beep(50);

Test and evaluate the resulting behavior.

. . .

So, did you notice that you can simply hold down Btn1 and when the 5th LED is lit you'll get a beep ?
To fix this add, after loop() … , these declarations:

  byte btnState, lastState;
bool btnPressed;

and change the line of code testing the button & LED state to:

    // if button-1 was just pressed when the 5th LED is on, give a beep
btnState=digitalRead(button[0]);
btnPressed = (btnState!=lastState) && (btnState==PRESSED_STATE);
lastState=btnState;
if (btnPressed && blip==4) beep(50);

Test and evaluate the resulting behavior.

. . .

Notice that it sounds a beep if the button is pushed as it goes to Led5 not if pushed while LED5 is lit.
To fix this move the delay(150) line to just after the line that turns an LED on.
OK now verify that it works as desired.
This is what the loop() code should look like now:

void Lesson4a2_loop() { // Lesson 4 scan LEDs (also if Btn1 pressed && LED 5 lit ... Beep
int btnState, lastState;
bool btnPressed;

for(int blip=0; blip<nleds; blip++) {
digitalWrite(lites[blip], ON_STATE);
delay(150); // flash the LED (blip) for a bit

// if button-1 was just pressed when the 5th LED is on, give a beep
// if (digitalRead(button[0])==PRESSED_STATE && blip==4) beep(50);
btnState=digitalRead(button[0]);
btnPressed = btnState!=lastState && btnState==PRESSED_STATE;
lastState=btnState;
if (btnPressed && blip==4) beep(50);

digitalWrite(lites[blip], !ON_STATE); // turn last lit LED off
}
}

Next, make the needed changes to speed up the scan similar to how we had the single LED flash faster and faster.
To do this (within the loop() code),

add declaration: int onMSecs=200;
change the delay(150) to delay(onMSecs)
and replace:

	if (btnPressed && blip==4)  beep(50);

with:

    if (btnPressed) {
if (blip==4) {
onMSecs -= 20;
beep(20);
} else {
onMSecs = 200;
boop(400);
}
}

Try and see if it works as expected.

. . .

You will likely notice that it does not seem to reliably speed up. This is due to the fact that when the special function 'loop' reruns it is not assured that it will put its variable in the same location as previously, so 'onMsecs' does not reliably maintain the value changes made to it. In our earlier single LED "Speed Flasher" the main code was inside a while(true) forever loop so the “loop()” function was never rerun.

To fix this you can change the declaration of onMsecs to: static int onMSecs=200;
Have 'static' explained to you or read about it and perhaps the subject of scope in general.

Your code should now look like this:

void loop() {
byte btnState, lastState;
bool btnPressed;
static int onMSecs=200;

for(int blip=0; blip<nleds; blip++) {
digitalWrite(lites[blip], ON_STATE);
delay(onMSecs); // flash the LED (blip) for a bit

// if button-1 is press when the 5th LED is on give a beep
btnState=digitalRead(button[0]);
btnPressed = btnState!=lastState && btnState==PRESSED_STATE;
lastState=btnState;
if (btnPressed) {
if (blip==4) {
onMSecs -= 20;
beep(20);
} else {
onMSecs = 200;
boop(400);
}
}

digitalWrite(lites[blip], !ON_STATE); // turn last lit LED off
}
}

A problem with using 'static' in the 'loop()' function is you may miss that one of your variable needed it, further the more complex your code is the more likely this may occur. For this reason and others, I like to use 'loop()' as a simple top level activity controller and a separate (non special) function for activities and games wherein they have a forever while loop as needed.

Given we name our little activity “Speed_Scan”, we get something like this:

void loop() {
Speed_Scan();
}

void Speed_Scan() {
the declarations (without 'static') that were in the 'loop()' function.
while(true) {
The code we had in the 'loop()' function.
}
}

The use of 'static' is no longer needed because we will never leave the scope (by exiting and rerunning) of the Speed_Scan() function. The local variables will remain in one location and maintain their values.

I went ahead and modified my “Speed_Scan” to be more of a challenging game with a goal and reward.
Please note that it is important to give the user of your software feedback to their actions and accomplishments. Here it is. Study the changes I made and what their associated feature difference is.

. . .

void Speed_Scan() {
int btnState, lastState;
bool btnPressed;
int onMSecs, stRate=165;
int blip, hits=0, goal=10;

blip=0;
onMSecs=stRate;
while(true) {
digitalWrite(lites[blip], ON_STATE);
delay(onMSecs); // flash the LED (blip) for a bit

// allow use of left or right most button
btnState=(digitalRead(button[0])==PRESSED_STATE || digitalRead(button[3])==PRESSED_STATE);
btnPressed = btnState!=lastState && btnState;
lastState=btnState;

if (btnPressed && (blip==4 || blip==5)) {
beep(100);
hits++;
if (hits==goal) {
beep(100);delay(50); beep(100);delay(50); beep(250); // you did it
hits=0; // start over
delay(1000);
}
}

// added block for support of a missed penalty
if (btnPressed && !(blip==4 || blip==5)) {
delay(300);
hits--; // slow down update rate
}

onMSecs = stRate - (15*hits); // more hits he get faster the blip scans
digitalWrite(lites[blip], !ON_STATE); // turn last lit LED off
blip = (blip+1) % nleds; // advance to next LED
}
}

Step 7: Lesson 4B - Adding in Randomity

For gaming, the random() function is quite valuable. Given x=random(10,30); x will be assigned a number =>10 and <30.

Try this: Create a function which challenges one's quick reaction and gives feedback on how well they did.
Make use of the random function to make the time of the event they need to react to unpredictable.

. . .

As a fine example, you might have coded something like this:

void TagIt() {    // Respond by hitting button-1 as soon as LED lights up
while(true) {
delay( random(1000,4000) ); // wait for 1 - 4 seconds
digitalWrite(LED_OUT, ON_STATE);
delay(200); // give em little time to react. (adjust to your liking)
if (digitalRead(BTN_INPUT)==PRESSED_STATE) boop(1000); // success
digitalWrite(LED_OUT, !ON_STATE);
}
}

Remember that BTN_INPUT is defined as button[2] the third button. I recommend now using the first button on the left, by redefining it with:

#define BTN_INPUT button[0]

Here is a more involved example, which uses more of the I/O, more than one random() statement, and involves additional Math. Study the coding and its related behavior.

void SeeIt_TagIt() {    // attempt to hit associated button when an LED lights up
int time_given = 350; // within given time (adjust to your liking)
byte n;

while(true) {
delay(random(1000,4000)); // wait for 1 - 4 seconds
n = random(4); // pick an LED
digitalWrite(lites[2*n], ON_STATE);
delay(time_given);
if (digitalRead(button[n])==PRESSED_STATE) {
beep(100); boop(400); // ta Da!
while(digitalRead(button[n])==PRESSED_STATE) ; // stay until released
}
digitalWrite(lites[2*n], !ON_STATE);
}
}

Challenge: Given a requirement that the same LED should not be selected twice in a row, update SeeIt_TagIt to adhere to that requirement.

. . .

When programming professionally you are usually given requires and often tasked with accomplishing those requirements with minimal CPU execution time. For this challenge this could be done along with some well tuned mathematics.
Replacing:

n = random(4);    // pick an LED

with:

n = (random(1,4) + n) % 4;    // pick an LED, other than the last one picked

Optionally, study and try out this more complex function, which gives a measure of the reaction time:

//----------------  React_Now - Swat Quickly as LEDs light up
void React_Now() { // attempt to hit the button when the first LED lights up
// 1st LED is lit for 100 msecs, additional LEDs lite every 20ms
// challenge: if one pushes & holds before the first LED get wrong win; how to fix ?
bool btn = !PRESSED_STATE;
int min_Period = 100;
byte btnInput;
byte LEDptr;

btnInput = button[0]; // use the first button on the left of the four
while(true) {

// ensure that the button is not already pressed
while (btn==PRESSED_STATE) btn=digitalRead(btnInput);

// ensure all LEDs are off
for (int LEDptr=0; LEDptr<8; LEDptr++) digitalWrite(lites[LEDptr], !ON_STATE);

delay(random(2000,5000)); // wait for 2 - 5 seconds
digitalWrite(lites[0], ON_STATE);
beep(10); // takes 2x msecs
delay(min_Period-20-20); // account for the 20ms beep time and 20ms in for loop

for (LEDptr=0; LEDptr<8; LEDptr++) {
digitalWrite(lites[LEDptr], ON_STATE);
delay(20);
btn = digitalRead(btnInput);
if (btn==PRESSED_STATE) { // they pressed the button
delay(1000);
break;
}
}
if (LEDptr==8) beep(100);
}
}

Study the coding of this function; get defined, any new to you, types of elements (operators, key words, functions, structures etc.); until you have a working understanding of it all.

Step 8: Lesson 5 - Development of Hardware I/O Support Routines

It is a very good idea for your code be logically divided into smaller sub-functions. This modularization makes the code easier to test, improve, more reusable (portable), and easier to understand. One specific case of this is especially so; that is having hardware dependent code taken out of your top level function code. This way if there ever is a change in the hardware it should not effect your application code.

I have written some hardware support routines. One for the button module “scanBtns()” and two for the LED bar display “clearDisp()” and “refreshDisp()”. We already have a “beep()” function for audio.

In related application code you'll need to call “scanBtns()” just prior to making decisions based on button activity. The hardware support routine (aka: hardware driver) maintains several global variables, and #defines, which you can then utilize in the control and decision making processes of your application code. Here they are:

// ------------------------------------------------------------
// 4-Button module support

bool btn1 = false; // reflects the state of button #1
bool btn2 = false;
bool btn3 = false;
bool btn4 = false;
bool Btn=false; // reflects Buttons collectively
bool ESC = false; // a pseudo button, true if btn4 is held down for 2 seconds (given ~1ms update freq.)

bool btn1Changed = false; // true if btn1 just changed
bool btn2Changed = false;
bool btn3Changed = false;
bool btn4Changed = false;
bool BtnChanged = false; // true if any button changes

#define btn1Pressed (btn1Changed && btn1)
#define btn2Pressed (btn2Changed && btn2)
#define btn3Pressed (btn3Changed && btn3)
#define btn4Pressed (btn4Changed && btn4)
#define BtnPressed (BtnChanged && Btn)

The LED display support code uses the following global variable for interfacing with it:

// ------------------------------------------------------------
// 8-LED display bar support
unsigned int msCnt; // pseudo MSecs, incremented every time 'refreshDisp()' is called
// NOTE! in the arrays below, elements [1]-[8] relate to LEDs 1-8 respectively. ([0] & [9] are for overflow protection)
bool dim[10]; // set true which LEDs you want to be dim (~10% on)
bool lit[8+2]; // " lit (~33% on)
bool brt[8+2]; // " bright (100%)
int brightOne; // sets indicated (1-8) LED Bright. Useful as a cursor or sprite
int litLED; // " Lit “

The “refreshDisp()” code provides means to maintain LED brightness levels by way of regularly switching them On/Off at a given proportion. It is done directly by this code instead of using controlled Analog out levels or the PWM (Pulse Width Modulation) control of the MCU, mainly because not all the 8 control lines that are connected to our 8 LEDs are Analog Output lines nor DIO lines which support PWM control.

In order to perform as intended “refreshDisp()” needs to be executed approximately every 1 millisecond, independent of the implementation of your higher level functionality. This could (maybe should, as it does have some advantages) be accomplished utilizing an ISR (Interrupt Service Routine). Their implementations are highly hardware dependent, hard to debug, and generally esoteric. So we are going to side step this advanced option by doing the following:

Within the loop of your application code “refreshDisp()” needs to be called fairly regularly (~ every 1+ millisecond). This can generally be done by having a delay(1); line right after a call to refreshDisp(). Realizing that all of the remaining of your code within your loop likely takes a 1/10 ms or so to execute.

Using the below appShell.ino write some lines of code, inside of “myApp()” which utilizes these buttons and LED facilities.

. . .

I expect that you have created some interesting sets of code.

Here is one simple, and hopefully instructive, example set of instructions:

    if (btn1Pressed) {boop(75); lit[1]=true;}
if (btn2Pressed) {beep(25); lit[2]=true;}
if (btn3Pressed) {beep(150); lit[3]=true;}
if (btn4Pressed) {clearDisp();}

Here is another example coding that uses a little more of the features provided by the hardware support routines:

    if (BtnPressed) {
clearDisp();
for (int i=1; i<=nleds; i++) { // set all LEDs to a given level
if (btn1Pressed) dim[i]=true;
if (btn2Pressed) lit[i]=true;
if (btn3Pressed) brt[i]=true;
}
}
//if (btn4Pressed) {clearDisp();} // this line is NOT needed

// produce sound while btn1-3 is held. btn3 should produce a raspy buzzer like sound.
while (Btn) {
if (btn1 || btn3) boop(3);
if (btn2 || btn3) beep(3);
scanBtns();
//refreshDisp();
}

Try each of these blocks of codes, in place of the line: “// code to implement your function/activity/game of choice“ and observe their behavior.

You should see how they demonstrate some of the capabilities of the button & LED support functions. I find that I can do whatever I think of, and am satisfied, with the behavior of these HW support functions. Having used them quite a bit.

But I had to remove the delay() calls at the end of beep() and boop() to get a smooth note while holding a button down, in my last code example. That's now OK, but in setup() I have, what is for practical purposes, a lie: beep(); beep(); beep(); I expect and want to hear three “BEEP”s. Instead I get one long “b e e p”. So reinstate the commented out delay() calls at the end of the beep() and boop() function code. Now at start up you should hear three “beep”s. Right ?

Oh what now? Using this last code set, again no longer makes smooth continuous boop... or beep... while the button 1 or 2 is held down. So I am a bit two faced, sometimes I want beep()s to be individually distinct and at other times I want the beep() sounds to melt together. Using a default parameter, in the call to these functions, can accomplish this. Here is what you do for beep(), you'll need to do similarly for the boop() function and its use.

void beep(int duration, int delayms=0){  // works for both Buzzers & Speakers
pinMode(BEEPPIN, OUTPUT);
for (int i=0; i<duration; i++) { // wait for a delayms ms
digitalWrite ( BEEPPIN, HIGH);
delayMicroseconds(500);
digitalWrite ( BEEPPIN, LOW);
delayMicroseconds(500);
}
delay(delayms); // pause for delayms millisecs
}

Add these lines at the file top just after the beginning comment block:

// ----------------  Function prototypes (pre definitions, to help the compiler manage)
void beep(int duration, int delayms=0);
void boop(int msecs, int delayms=0);

and change the beep-beep-beep line in setup to:

  beep(50,30); beep(50,30); beep(150);  // announce "it is alive"

When a parameter is assigned a value in the defining parameter list, that value is used if it is not given in a call to the function.
Now retest to observe the change in behavior. You'll note that during startup and my app code I get the behavior I want from beep(); calling it with and without a delay pause time, as appropriate, as well as during button presses.

Step 9: Lesson 5B - Refreshing the Display While Making Sounds

Now Lets get the LED display refreshed while making sounds.

Reinstate the comment out line: //refreshDisp();
which is in the block of code, entitled: // produce sound while btn1-3 is held

On retesting you'll see that the LEDs light even during audio being played; yet while some sounds are produced the display flickers.
The problem comes from the sound code using delay() for timing (and not meanwhile refreshing the display) to get the sounds we want. This same situation is likely to also come up in some game/application code. So we need a delay function that refreshes while waiting. Let's use this function:

// -----------------------
void refreshWait(int msec) {
while (msec>0) {
refreshDisp();
delay(1);
msec--;
}
}

Change the delay() calls to refreshWait() calls in the beep() and boop() functions.
And add this line to the end of the for loop code in the beep() function:

refreshDisp();

Now the sounds and display update should work very well and as expected, with minimal flicker.

Your code file should now look like HW_Support_Demo.ino, download it and compare as needed.

Step 10: Lesson 6 - Use the Support Capability to Dim LEDs

Rework “Speed_Scan()” , from lesson 4, so that it has no digitalRead()s or digitalWrite()s within it and it relies on instead the hardware support functions developed in lesson 5.

. . .

Debug as needed to get it working, as before.

Now add some new feature(s) to the new “Speed_Scan”.
Use the capability to dim a number of LEDs to reflect the progress in speeding up the scan.
Add the following line where appropriate:

    for (int i=1; i<=hits; i++) dim[i]=true;

and this line as and where needed:

      clearDisp();

Also replace the use of the likes of “lit[blip] =” with the use of the globally supported “litLED”.
Retest to ensure you retained the functionality as before.

. . .

See: Speed_Scan_ver4()
BTW in this revised version you can use any button not just btn1, due to the use of “Btn”.

Step 11: Lesson 6B - Utilizing Hardware Support Functionality

Rework “SeeIt_TagIt()”, from lesson 4, so that it has no digitalRead()s or digitalWrite()s within it and relies on instead the hardware support functions developed in lesson 5.

. . .

Debug as needed.

You may have found that replacing “digitalRead(button[n])” with something from the new hardware support functions was not straight forward. You could of ended up with something like this:
replacing :

  if (digitalRead(button[n])==PRESSED_STATE) {

with:

  if ((btn1 && n==0) || (btn2 && n==1) || (btn3 && n==2) || (btn4 && n==3) ) {

This coding is involved and takes several CPU instructions to execute. It is not intuitive on first examination, thus prone to mistakes in debugging or upgrades. So I changed this hardware support block of code:

  b1 = ! digitalRead(button[0]);
b2 = ! digitalRead(button[1]);
b3 = ! digitalRead(button[2]);
b4 = ! digitalRead(button[3]);

to this:

  b1 = btn[1] = !digitalRead(button[0]);
b2 = btn[2] = !digitalRead(button[1]);
b3 = btn[3] = !digitalRead(button[2]);
b4 = btn[4] = !digitalRead(button[3]);

and add this global declaration:

bool btn[5];      // array of btn1-4 states,  btn[0] is not used

thus I was able to use this simple if test in the application code:

    if (btn[n+1]) {

Optionally rework “React_Now()”, as you just did for SeeIt_TagIt.
Also make use of “dim[]”, “litLED” and possibly even “brightOne”.
Then have it respond to either btn1 or btn4, so left and right handed users are equally served.

. . .

See: SeeIt_TagIt_ver2() and React_Now_ver2() in Lesson_6.ino

Step 12: Lesson 7 - Verifying Your Hardware Implementation

To verify that a hardware design and implementation is sound, test software is usually generated.
We have done a little of this, but it was not thorough. Adding textual output is a great improvement for testing and debugging.

The Arduino IDE (Inter-active Development Environment) application provides a Serial Monitor window for text output; and a set of serial support functions which are of the form “Serial.function()” We will use “Serial.print()” & “.println()” Refer to https://www.arduino.cc/reference/en/language/func...
Especially looking over the 'Example Code' section.

Add to our init() …

  // initialize serial communication at 9600 bits per second:
Serial.begin(9600);
Serial.print(“ Arduino Educational Mini Game Platform”);
Serial.println(" - Lesson #7");

The output of these “Serial.” commands will be seen in the Serial Monitor window. Once you have downloaded your sketch to the connected project platform, you can bring it up, in the Arduino IDE via menu item “Tools” - “Serial Monitor”. Your sketch will run and you'll see the results of your print statements. If you have problems with the Serial Monitor please review:

https://create.arduino.cc/projecthub/glowascii/ser...

Here is a function for printing out button states for our proposed purposes.

void btnReporting() {
// ================================= Button - LED exercise & Reporting
beep(100);
clearDisp();
while (Btn) scanBtns(); // wait for button release
while (1) {
refreshWait(1);
scanBtns();
// ------------------- button reporting
if (BtnChanged) {
Serial.println(" ");
if (btn1) Serial.print("[Btn1] "); else Serial.print(" ");
if (btn2) Serial.print("[Btn2] "); else Serial.print(" ");
if (btn3) Serial.print("[Btn3] "); else Serial.print(" ");
if (btn4) Serial.print("[Btn4] "); else Serial.print(" ");

// ------------------ LED reflection of button state
lit[1] = lit[2] = btn1; // set Grn LEDs
lit[3] = lit[4] = btn2; // set Blu LEDs
lit[5] = lit[6] = btn3; // set Yel LEDs
lit[7] = lit[8] = btn4; // set Red LEDs
brightOne=0;

beep(15); // make a key press click, for up&down
}
}
}

Use this function to test you hardware and our support function scanBtns().
Did you notice that all combinations are not detected properly? Can you see what's needed to fix it?
Make btnState global and add this (after the print for btn4) to the reporting:

          Serial.print(btnState);

Notice that, as coded, in scanBtns(), 'currState' will only represent one button at a time.

To fix this we need to redefine what 'currState' is set equate to. I also created a new global variable 'btnNum' to represent this 'one' button accounting. So I replaced:

currState = (b1)? 1 : ((b2)? 2 : ((b3)? 3 : ((b4)? 4 : 0)));  // logical button # (1-4)

with:

  btnNum = (b1)? 1 : ((b2)? 2 : ((b3)? 3 : ((b4)? 4 : 0)));  // logical button # (1-4) (multiples not  supported)
currState = int(b1) + 2*int(b2) + 4*int(b3) + 8*int(b4); // a combined button states

With these changes our button test should fully work as expected. Check it out. Lesson_7.ino has all the above code integrated in.

Step 13: Lesson 8 - Menu for Function Selection

Propose a Menu operation for selecting one of our four functions

	HW_Support_Demo();
Speed_Scan(); // latest ver
React_Now();
btnReporting();

Implement your idea, getting help from others as needed.
Add a print notification as to which functionality has been selected. . . .

For an example of a fairly simple method to select a different function for each of the 4 buttons. See my implementation in Lesson_8.ino

Ensure that you understand everything so far; if not get what you don't understand cleared up.

Step 14: Lesson 8B - Developing an Escape Strategy

With our software project as it stands so far ...
You need to cycle power, reload, or reset to then select a different function.

See if you can have each functional activity have a way to exit back out to the top level menu when-ever button-4 is held down for more than a couple of seconds. BTW: checkout millis();

. . .

I propose we use an Escape ! “ESC” to return to the top level menu. In the provided Lesson_8.ino code there is a new global variable “ESC” which is maintained in scanBtns() with:

  if (btn4) ESC = (millis()-lastChg)>2000;
else ESC = false;
. . .
lastChg = millis();

The intended usual case will be for functions to exit when 'ESC' is discovered to be set. With eg:

  if (ESC) return;

Expand btnReporting() to report on ESC, giving textual & visual indication of its state, with:

    if (ESC && brightOne==0 && btn4 ) Serial.print(" [ESC] ");    
brightOne = (ESC)? 8 : 0;
if (ESC && btn1) break; // only leave Btn Reporting if btn1 is also held down.

Placed at the end of the 'while' forever loop.

Notice that here, in btnReporting(), we need a special case test to escape, as reporting on 'ESC' itself is desired.

First copy the code for btnReporting() and scanBtns() from Lesson_8.ino into Lesson_8b.ino (or your version) and then ...
Expand other functions to support a means to escape back to the menu, by appropriately adding the likes of:

    if (ESC) return;

. . .

Test that all your escape code works as desired. You will likely find that the responsiveness from within each function is not the same, due to their operational characteristics. You will hear a distinctive buzz and the screen will be cleared when the escape is executed. When you let go of the button you'll be back at the main menu.

Step 15: Lesson 9 - Modularizing With an Include Library

C libraries are groups of pre-made functions placed in a separate file; which is made part of your compiled sketch by means of an #include statement.

For an introduction to the use of an included lib file, take a look at this Arduino library example: https://www.arduino.cc/en/Tutorial/ToneMelody

We are going to create an include (library) file which contains all the hardware support functions appropriate for our mini STEM platform. This can then be maintained separate from any specific activity implementation (think sketch), and reused across many. Note, this file will be composed of C functions and not a C++ object, like many others are.

Make a first cut attempt at a library of the functions that support this mini Arduino Game Platform. The file name is arbitrary, but could be “my_lib.h”.

Move the hardware support data structure declarations and functions from the code contained in Lesson_8b.ino to this .h file. Also within the .h file create a function to preform the hardware operations which are currently in the setup() function of the Lesson_8b.ino (or your related code) file.

Have your project code in your .ino file use the #include command to load the created hardware support file, and call the new hardware setup function where appropriate.

Then insure that the new sketch containing the two files compiles and works as before.

. . .

See Lesson_9.ino & Mini_STEM_Platform.h for sketch using the new library include file. There should only be minor differences in this and your implementations.

Step 16: Lesson 10 - Creating a New Mini App

Let's create a mini app inspired by the “Magic-8-Ball” toy.

We could start with this code. Which flashes LED #1 while waiting for a button push. Upon a push a random response is given.

// One of Four responses:   Red: NO   YEL: Maybe   Grn: YES    Blue: Try again
void Magic_8_ball() {
while (1) {
clearDisp();
while(Btn) scanBtns(); // wait for previous button press to be released
while(!Btn) {
scanBtns();
litLED=(msCnt/1000)%2; // toggles first LED once per second
refreshWait(1);
}

litLED = 2*random(1,5)+1; // select and show one of 4 possible outcomes
refreshWait(3000);
}
}

Try out this minimal app (replacing btnReporting). If there is any line of the code you don't recognize its purpose, change or remove it and observe how the behavior changes, until it is clear to you.

Make the following improvements, to make it more like the toy:

- Wait while the button is pushed with the intention that the user ponders their query while doing so.
- Pause a little before revealing the answer.
- Lit the LEDs of a given color all together, to appear more definitive.

To do this, replace:

    litLED = 2*random(1,5)+1; // select and show one of 4 possible

with:

    while(Btn) scanBtns();         // wait while button is being pressed
delay(300 + random(900)); // make them wait a little more as if divining the answer
showColor(random(1,5)); // select and show one of 4 colors as the possible answer

In support of the last item I added a local sub-routine “showColor()”

// ================== Local Support Functions =====================
void showColor(byte color) { // 1-4: light LED color set 1-4, 5: All
clearDisp();
if (color==0) return;
if (color==1 || color==5) lit[1] = lit[2] = true; // LEDs in set 1
if (color==2 || color==5) lit[3] = lit[4] = true; // LEDs in set 2
if (color==3 || color==5) lit[5] = lit[6] = true; // LEDs in set 3
if (color==4 || color==5) lit[7] = lit[8] = true; // LEDs in set 4
}

Check out the new features of this mini app.

. . .

While the user is holding the button down have potential answers apparently float by. With that in mind, I'll have one set of LEDs at a time light up, out of order, with an off state interspersed.

Replace:

    while(Btn) scanBtns();         // wait while button pressed 

with:

    while(Btn) {         // flash potential answers while button is being pressed
scanBtns();
if (ESC || btn4) return;
if (cnt != (msCnt/400)%8) { // flash LEDs (1-3-2-4)
clearDisp();
cnt = (msCnt/400) %8;
// determine next color set to light or not to light
if ((cnt%2)==0) color=0;
else if (cnt>3) color=cnt-3;
else color=cnt;
showColor(color);
}
refreshWait(1);
}

plus these declarations: int cnt; byte color; placed at the top of function.
Work through this new code and its math, and correlate it to the resulting behavior.

. . .

The clause (msCnt/400)%8) steps through values of 0-7 changing every 400 msecs.
The “if ((cnt%2)==0) color=0;” will make every other 400ms period have LEDs off.
When the 'cnt' is odd and >3 it will be = 5 or 7 and color= 2 or 4.
When 'cnt' is odd but <=3 it, and 'color' will be = 1 or 3.

This will make for a sort of floating like pattern of the potential answers. Have you ever seen the responses float by the window on a Magic-8-ball?

This may be a slick use of mathematics to get the behavior we want. But, there are other ways to accomplish the same, with other pros and cons.
With few possibilities to handle (only 8) it may be better and clearer to

replace the . . . if (cnt>3) color=cnt-3;
with . . . if (cnt==5 || cnt==7) color=cnt-3;
Or with . . . if (cnt==5) color=2; else if (cnt==7) color=4;

Another option is data driven vs code driven logic. In cases where the code gets too complicated to easily follow and know what to expect from it, using a data table may be a good option. I think this is a good choice here.

Replace all the code in bold in the above block , after “// determine“ with this one line:

  color = ccycle[cnt];

plus a local array declaration:

  static byte ccycle[] = {0,1,0,3,0,2,0,4};

Now you can readily see the effect being created. Further, this implementation tends not to be prone to bugs. Even when a data array could be used, the trade offs of code vs data array driven are not always in favor of the use of data over code. In particular when the data set would have to be very large or complex when a smaller amount of code would do the job and potentially be easier to understand the significance of the process.

There are three versions of the development of Magic_8_ball for review in Lesson_10.ino. They can be selected as activity 1-3.

Step 17: Lesson 11 - Using Random & Real World Influence

Now let's make the Magic_8_ball() function even more like the toy by making the outcome more truly random based, while also being directly influenced by the user.

The nature of “random()”
We are using 'random(1,5 )' to get a “random” number of 1 thru 4.The first parameter is the lower limit inclusive while the second is the upper limit exclusive. Try out our “Magic_8_ball”, as coded above so far, noting the pseudo randomness of the given results. Now from a restart, note a half dozen or so responses. Repeat this a few times.

. . .

The results you get, beginning after a restart, are always the same. This is because 'random()' returns a sequence of numbers which taken as a set are statistically of random nature (pseudo random). But from any given starting point, after a restart or 'randomseed()', the results of random() will be predetermined and repeatable. This can be very useful for code testing, but generally not good for creating game play.

Add into 'setup()' a line to seed the random function. For the parameter use any whole number, of your choice. For instance: randomseed(1234);

Testing the sequence of results as before; you'll find the sequence is different from before, but it does repeat each time after a restart. For any given seed value you get different results but they are repeatable.

To get more true unpredictable randomness, real world events or analog values can be used directly or (generally easier and better) in conjunction with 'randomseed()'.

Add Real World influence.
Having an analog input with a high degree of resolution of something like outside air temperature (much better yet would be cosmic RF noise) to use to seed the random function would give you a virtually new set of random numbers from calls to the random() function. What is more available and easy but not as variable is the analog value from an open analog input. And some recommend doing this, in the setup() code:

randomSeed(analogRead(0));

This is definitely better but realistically only provides a dozen or so seed values.
Using micros() for a seed seems like a great idea, but its value is initialized on startup and the code in setup() all takes a repeatable amount of time. Hence you'll get a repeatable value from micros().
I recommend adding the time it takes for the analog input to vary over a given range. Like This:

  // wait an amount of pseudo random time due to HW input noise (Entropy), then randomize
aVal = val = analogRead(A0);
while (abs(aVal-val)<=20) {val=analogRead(A0);} // expects analog noise (A0 needs to be open)
randomSeed(micros()/4); // micros() resolution is ~ 4us

Try this out, placing the above code near the end of you setup. Be sure to declare 'aVal' and 'val' as integers. Now the initial set of responses should not repeat (<0.1%)

Add User influence.
When you use an actual physical Magic-8-ball the “prophetic” answer is directly dependent on the user, through their motion and the point in time at which they turn it over. To add this functional characteristic we can base the answer on the milli-seconds that they hold down the button while stating or contemplating their query. Conceptually we do this: (as needed see code for details implementation)

ans = (millisecs the button was pressed) %4;

Optional Exercise:
Check out the operation of the executable code for sketch “Lesson_11.ino”. Don't look at the code yet.

. . .

Note that applet #1, which is my expanded implementation of Magic_8_ball(), additionally supports unique behaviors when button 2 or 3 are used instead of button 1. The response also stays until you press a button for another query. Use them sufficiently to understand their nature. Then try your hand at writing code to exhibit the same behavior. For a much greater challenge do the same for applet #2 (re btn2); then refer to my Magic_8_ball_v2() function. I hope you find success at your endeavors.

A group of programmers (let alone students) are likely to do this several different ways. Once you are done take a look at how I accomplished the behaviors for btn2 & bnt3. I did create a utility called “setLEDs()” in support of btn#2's functionality, as I envision this may be useful elsewhere.

Step 18: Lesson 12 - Express Your Own Creativity With Your New Capabilities

Replace the use of the function “btnReporting();” with an activity of your own devising.

Document your code, variable descriptions, functional characteristics etc., for other's benefit and yourself at a later date.

. . .

Step 19: Lesson 13 - Expanding the Top Level Menu

What would we do with the menu if we want to support and select from more than 4 activities?

Devise and implement a top level Menu scheme to handle this.

. . .

Go over what I have done. Work with it as needed to fully understand the operations of this top level menu. Note the use of the switch statement.

void loop() { // Top Menu level, for selecting 1 of N activities
byte func, maxFunc;

func=1;
while(1) {
clearDisp();
while(Btn) {buZZ(1); scanBtns();} // wait for no button

maxFunc=6; // currently there are 6 activities/games supported
// indicate selectable functions 1-6
dim[1]=dim[2]=dim[3]=dim[4]=dim[5]=dim[6]=true;
brightOne=func;
refreshDisp();

while(Btn) {scanBtns(); refreshDisp();} // wait here for button release
while (! btn1) {
refreshDisp();
delay(1);
scanBtns();
if (btn2Pressed) brightOne = (brightOne==1)? maxFunc : brightOne-1; // -- move left
if (btn3Pressed) brightOne = (brightOne%maxFunc) + 1; // ++ moves right
if (btn4Pressed) brightOne = (brightOne%maxFunc) + 1; // allow most play with only 2 buttons (1&4)
}

func = brightOne;
clearDisp();
while(Btn) scanBtns(); // wait for button release

Serial.print(" FUNC: "); Serial.println(func);
switch (func) {
case 1:
HW_Support_Demo(); // Demonstrates LED display and audio support functionality
break;
case 2:
Speed_Scan_ver4(); // hit it on the right spot to speed things up to a winning max
break;
case 3:
React_Now_ver2(); // Test your Reaction time
break;
case 4:
SeeIt_TagIt(); // Capture and Release fleeting blips
// btnReporting(); // exercises the buttons and LEDs, and reports via Serial
// Or replace this with your own previously devised activity
break;
case 5:
pingPong(); // Two person game of Ping-Pong
break;
case 6:
Magic_8_ball(); // Give responses using PRNG along with Human Heuristics
break;
default:
buZZ(1000);
}

}
}

In, the attached file, Lesson_13.ino, you'll find the above loop “Menu” code, and two functions I included, along with the original 4, to fill out a suit of 6 activities. Here is a brief description of them:

pingPong() Supports two person play. To hit the ball back and forth, the appropriate paddle must hit (button pressed, btn1/btn4) it when it is in the end position (last LED). The first side to miss loses, and the lights on the winning side are flashed. The winner of a rally is always the server for the next. After a rally, the number of times the ball was hit back & forth is reflected on the display. By the way, one or more external trigger button controllers can be made and used by connecting them to D16 &/or D19 and Ground; for reference see https://youtu.be/EZTxLNJKRbw

Magic_8_ball() Uses analog IN with random(). The Magic-8 ball's response (Red: NO YEL: Maybe Grn: YES Blue: Try again) is directly determined by the user, via the amount of time they hold down the button when they ask their question. Albeit not consciously deliberate. Use btn2 for a 1/6 out come ~ like a dice, useful for games. Use btn3 for a 1 out of 8 result.

Step 20: Lesson 14 - Final Lesson Exercise

Check out the games that are part of “Menu_16Games” on Instructables. Download the game description documentation, read it over and Pick a game. Download the .ino project code. Rework that game function to use our just created library include file. The support functions it did use may already be similar but not identical, also the games were designed with a 12 LED bar display in mind, and not one with 8 LEDs. So modifications are likely needed.

Include the game in our collection of activities, expanding the Top level Menu accordingly. Test, debug and revise the code to function as desired.

Optional Extra Credit
Create your own game (or other applet/activity) using this mini STEM platform (HW + Lib.).

Please share and post (or Email me) what you make. Also do the same if you come up with an improvement on any of my games or functions.

There may be inconsistencies within the lesson plans, as it was developed over a long time. I'll update the text if I find any, or have any reported to me.

Further Study

Other areas of study, found in similar code projects:

EEPROM PushIt(); see: http://instructables.com/id/Single-Line-LED-Displ... Step 6 and files: Menu_16Games.odt & Menu_16Games.ino

Interrupts processing: Psyche_Ometers() see Step 10: The Software 'Code' of http://instructables.com/id/Single-Line-LED-Displ...

Last minute Notes:

Some of the files have had an extra extension (of .txt) added to their file names, due to site upload errors "internal server error"; at the time this instrucable was being created.

This same STEM platform, has now been used for the basis of a smart Morse Code Trainer. See:
https://www.instructables.com/id/Morse-Code-Trainer/

STEM Contest

Runner Up in the
STEM Contest