/*
Rev0:
The whole idea behind this program is to test to see if gun was pointed accurately at a target when the trigger was pulled using the Nintendo Duck Hunt technique.
When the trigger is pulled, the target LED is turned off and a measurement of the ambient light the gun sees is taken.
A margin is added to the ambient light to create a threshold, the margin is 10% of the ambient light.
Then the target light is turned on and another light measurement is taken.
If the light measured when the target light is on, then the result is a hit, otherwise it is a miss.
For multiple targets, ambient measurements are made between testing each target
Rev2:
This version has the target LED lit before trigger is pulled like it would be once a valid target is lit up.
This changes the delay timing requred for the LED to dim enough so that it is not continuing to dim when the target light measurement is made, which result in it being lower than the threshold and always a miss.
Rev3:
Only allow one shot per trigger pull, provide the result as soon as the trigger is pulled, but do not reset until the trigger is released
Rev4:
Removes the increasing margin for a static margin since this was preventing triggering off a reflection, which is how I plan to trigger them in the final design.
Maybe using a longer ambient delay would have worked as well, I may have to fiddle with these when we get to the final design.
Rev5:
Created arrays for targets and eliminate individual test and ambient states. Ambient measurements are only taken for valid targets.
Rev6:
1: Created a scoring scheme. 1 point per hit of a valid villain per level so 1 point per hit villain on level 1, 2 points per hit villain on level 2, etc.
2: Created a high score that is saved and retrieved from EEPROM
3: Created the GAMEOVER state. If out of misses, then the game is over. A miss is a villain that was never hit before its valid duration expires. If you hit a bystander, the game is immediately over.
4: Created a misses counter, but have not yet created the definitino and enforcement of a miss.
Rev7:
1: Created random times for targets to be displayed. The durations start at 10 seconds for level one and are cut by 1/4 for each level and the min/max allow for +/- 20%.
Meaning level 1 is 10 +/- 2 seconds, level 2 is 7.5 seconds +/- 1.5 seconds, level 3 is 5.625 +/- 1.125, etc.
2: A random draw based on the duration for targets displayed will also be used to set a timer for when the next valid target decision will be made.
This randomizes when the targets become valid, doesn't increase the pace of targets displayed if they are shot quickly, and can display the proper number of targets per duration (by dividing the number of targets based on the duration they are valid).
3: Inizialize the random seed generator using an anolog read of an unused pin will vary the pattern from one startup to the next.
4: Created a 1ms interrupt timer to create a timer that is used for all time based decisions
5: Create LEXTLEVEL state that decrements the amount of time between targets and the duration of those targets
6: Increment to the NEXTLEVEL every 30 seconds, the number of targets per 30 seconds is not limited
Rev8:
1: Fixed timer issue, when I created the counter in Rev7, it messed up the default timer used for functions such as "delay()"
Rev9:
1: Rearramged pins 11, 12, and 13 to allow for connecting an SD card to play .wav files
2: Added an SD card reader and the TMRpcm library to play wav files
3: Added sounds for gunshots, hitting a villain, hitting a bystander, game over, and new game
Rev10:
1: Dead end branch where I removed the TMRpcm library to troubleshoot the code bogging down and sometimes freezing, that didn't make any improvement.
Rev11:
1: Fixed the NEXTLEVEL counter logic so it now can get beyond level 2.
2: Fixed bug where you could get 2 simultaneous villains after killing them both in level 2 and beyond
Rev12:
1: Changed the duration until the next target after a valid target is killed
Rev13:
1: Transitioned from individual output pins to shift registers
2: Reassigned pins as required to switch to an Arduino Mega 2560
Things to do in future versions:
- Create an display to show the current score and the high score
*/
#include <EEPROM.h>
#include <SD.h> // need to include the SD library
#define SD_ChipSelectPin 53 // using digital pin 4 on arduino nano 328, can use other pins
#include <TMRpcm.h> // also need to include this library...
#include <SPI.h>
//
enum states {
IDLE,
TEST,
RESULTS,
RESET,
NEXTLEVEL,
GAMEOVER,
NEWGAME
};
TMRpcm tmrpcm; // create an object for use in this sketch
// constants won't change
//const int SuccessLedPin = 10; // LED pin
const int ldrPin = A0; // LDR pin
const int buttonPin = 2; // Pushbutton pin
const int dataPin = 11; // Pin connected to DS of 74HC595 (Blue)
const int latchPin = 8; // Pin connected to ST_CP of 74HC595 (Green)
const int clockPin = 12; // Pin connected to SH_CP of 74HC595 (Yellow)
const int AudioPin = 46; // Pin connected to the audio output jack
const int numLedChannels = 1; // Number of 8 LED channel relays
const int margin = 50; // Adjust this as needed for accuracy. 50 seems to work well testing directly against bulbs, may need 5 for reflections
char numTargets = 8;
bool TargetVillain[16] = {true, true, true, true, true, true, true, true, false, false, false, false, false, false, false, false};
byte ledArray[2];
//const char TargetLedPin[16] = {8, 7, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10}; // UPDATE THESE!!! These are the pins associated with the outputs that control the relays
bool TargetValid[16] = {false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false}; // is the target a valid target, you can't hit valid targets, either villains or bystanders
bool TargetStatus[16] = {false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false}; // was the target hit or not?
unsigned long TargetDuration[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; // length of time the target will be valid
int ldrAmbient[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; // this is the count associated with the voltage of the Light Resistant Diode
// variables will change
char ButtonState = 0; // variable for reading the pushbutton status
char LastButtonState = 0; // variable for the previous reading of the pushbutton
bool FirstPass = false; // This allows only 1 shot per button press
bool hit = false; // If any of the targets were hit, show a hit
bool fail = false; // If any of the bystanders were hit, show a fail regardless of if a valid target was also hit
int score = 0; // Keeps track of the total score
int highScore; // High score that is retrieved from memory and stored each time a score is greater that the high Score
signed char misses = 10; // Allow 10 misses before the game is over
int ldrTrigger = 0; // reading of the LDR at trigger pull
char level = 1; // level of the game controls the difficulty
char nextTarget = 0;
unsigned int BaseDuration = 10; // Length of nominal time the target will be displayed for level 1
unsigned int RandomDuration = 0;
unsigned long counter = 0; // This will be a continuously increasing counter for the duration of the game
unsigned long temp1 = 0;
unsigned long temp2 = 0;
unsigned long temp3 = 0;
unsigned long temp4 = 0;
unsigned long nextSerial = 0; // For printing data to the serial monitor periodically
states state = NEWGAME;
void setup() {
tmrpcm.speakerPin = AudioPin; // 5,6,11 or 46 on Mega, 9 on Uno, Nano, etc
pinMode(AudioPin,OUTPUT); // Pin pairs: 9,10 Mega: 5-2,6-7,11-12,46-45
pinMode(latchPin, OUTPUT);
pinMode(clockPin, OUTPUT);
pinMode(dataPin, OUTPUT);
highScore = EEPROM.read(0); // Retrieve previous high score into memory
randomSeed(analogRead(1)); // Initialize the random seed generator by reading the #1 analog pin, which is unconnected to randomize draws between power ups.
Serial.begin(115200); // Start the serial connection
if (!SD.begin(SD_ChipSelectPin)) { // See if the card is present and can be initialized:
Serial.println("SD fail");
return; // don't do anything more if not
} else {
Serial.println("SD ok");
}
// for (char target=0; target<numTargets; target++) { // Set each of the TargetLedPins to OUTPUT
// pinMode(TargetLedPin[target], OUTPUT);
// digitalWrite(TargetLedPin[target], HIGH); // Blink LEDs to verify pins are wired correctly
// }
// delay(1000);
// for (char target=0; target<numTargets; target++) {
// digitalWrite(TargetLedPin[target], LOW); // Turn all LEDs back off after verifying they are wired correctly
// }
// pinMode(SuccessLedPin, OUTPUT);
pinMode(ldrPin, INPUT);
pinMode(buttonPin, INPUT);
TCCR2A=(1<<WGM21); // Set the CTC mode
OCR2A=0xF9; // Value for ORC0A for 1ms
TIMSK2|=(1<<OCIE2A); // Set the interrupt request
sei(); // Enable interrupt
TCCR2B|=(1<<CS22); // Set the prescale 1/64 clock // CS02 = 0, CS01 = 1, CS00 = 1
//TCCR2B|=(1<<CS20);
}
void loop() {
LastButtonState = ButtonState;
ButtonState = digitalRead(buttonPin);
// for (char target=0; target<numTargets; target++) {
// if (TargetValid[target]) {
// digitalWrite(TargetLedPin[target], HIGH);
// } else {
// digitalWrite(TargetLedPin[target], LOW);
// }
// }
updateLEDs();
switch (state) {
case IDLE:
if ((ButtonState == HIGH) && (LastButtonState == LOW)) {
FirstPass = 1;
state = TEST;
}
break;
case TEST:
tmrpcm.play((char *)"GS.WAV");
ldrTrigger = analogRead(ldrPin); // Measure current total light, this is what the gun sees at trigger pull and only measured once
for (char target=0; target<numTargets; target++) { // Turn off each of the valid targets once at a time to measure the ambient light
if (TargetValid[target]) { // For each valid target...
//digitalWrite(TargetLedPin[target], LOW); // Turn off the target LED to be able to make an ambient light measurement
TargetValid[target] = false;
updateLEDs();
delay(20); // Keep the LED off for 20ms to give them time to dim and the LDR to respond
ldrAmbient[target] = analogRead(ldrPin); // Measure current total light
//digitalWrite(TargetLedPin[target], HIGH); // Turn the target back on
TargetValid[target] = true;
updateLEDs();
if (ldrTrigger > ldrAmbient[target] + margin) { // If the light at trigger pull was greater than the ambient light with the target off plus a margin...
TargetStatus[target] = true; // Then it was a hit on this target, regardless of it was a villain or bystander
}
//Serial.print("ldrAmbient"); Serial.print((uint8_t)target); Serial.print(": "); Serial.println(ldrAmbient[target]);
//Serial.print("TargetStatus"); Serial.print((uint8_t)target); Serial.print(": "); Serial.println((uint8_t)TargetStatus[target]);
}
}
//Serial.print("ldrTrigger: "); Serial.println(ldrTrigger);
state = RESULTS;
break;
case RESULTS:
if (FirstPass) {
for (char target=0; target<numTargets; target++) { // For each target
if (TargetValid[target]) { // For each valid target
//hit = hit | TargetStatus[target];
if (TargetStatus[target] & TargetVillain[target]) { // If you hit a target and it is a villain
tmrpcm.play((char *)"PT.WAV");
TargetValid[target] = false; // The villain is dead and therefore no longer valid after you hit it
Serial.print("counter: "); Serial.println(counter);
Serial.print("current target duration: "); Serial.println(TargetDuration[target]);
// temp1 = min(750, BaseDuration1000);
// temp2 = random(temp1, 3000);
// temp3 = counter+temp2;
// temp4 = min(TargetDuration[target], temp3);
// Serial.print("temp1: "); Serial.println(temp1);
// Serial.print("temp2: "); Serial.println(temp2);
// Serial.print("temp3: "); Serial.println(temp3);
// Serial.print("temp4: "); Serial.println(temp4);
// TargetDuration[target] = temp4; // Shorten the duration of the targets that were hit so it doesn't take so long between targets
TargetDuration[target] = min(TargetDuration[target], counter+random(min(750, BaseDuration1000), 3000)); // Shorten the duration of the targets that were hit so it doesn't take so long between targets
Serial.print("new target duration: "); Serial.println(TargetDuration[target]);
score = score + level; // 1 point per target per level --> 1 point per target for level 1, 2 points per target for level 2, etc.
if (score > highScore) { // If the current score is higher than the high score
highScore = score; // Set the high score to the current score
EEPROM.write(0,highScore); // Write the new high score to EEPROM
}
} else if (TargetStatus[target]) { // If you didn't hit a villain, but you still hit something, then you hit a bystander
tmrpcm.play((char *)"DL.WAV");
fail = true; // You hit a bystander so you fail
//hit = false; // Do not count a hit against a bystander
misses = 0; // Set misses to zero, which will end the game
}
//Serial.print("Target: "); Serial.print((uint8_t)target);
//Serial.print(", TargetValid: "); Serial.print((uint8_t)TargetValid[target]);
//Serial.print(", TargetStatus: "); Serial.print((uint8_t)TargetStatus[target]);
//Serial.print(", TargetVillain: "); Serial.print((uint8_t)TargetVillain[target]);
//Serial.print(", Fail: "); Serial.println((uint8_t)fail);
}
FirstPass = 0;
}
Serial.print("Score: "); Serial.print(score); Serial.print(", High Score: "); Serial.println(highScore);
}
state = RESET;
break;
case RESET:
memset(TargetStatus, false, sizeof(TargetStatus));
memset(ldrAmbient, 0, sizeof(ldrAmbient));
//hit = false;
fail = false;
if ((ButtonState == LOW) && (LastButtonState == HIGH)) {
state = IDLE;
}
break;
case NEXTLEVEL:
level++; // Increase the level
BaseDuration = BaseDuration * 0.75; // Decrease the time targets are valid and the time between targets
if (level<=numTargets) { // Only add another target if there are additional targets still available
nextTarget = (char)random(0,numTargets); // Randomly choose a candidate for a new valid target
while(TargetDuration[nextTarget]!=0) { // Continue randomly choosing candidates for the new valid target until you find one that is currently not valid
nextTarget = (char)random(0,numTargets);
}
TargetValid[nextTarget] = true; // Set the new random valid target
RandomDuration = random(1000*BaseDuration*.8,1000*BaseDuration*1.2); // Calculate the duration of this target
TargetDuration[nextTarget] = counter + RandomDuration; // Set the duration for this target
}
state = IDLE;
break;
case GAMEOVER:
tmrpcm.play((char *)"LS.WAV");
while (tmrpcm.isPlaying()) {}
memset(TargetValid, false, sizeof(TargetValid)); // Reset TargetValid to all false
memset(TargetDuration, 0, sizeof(TargetDuration)); // Reset TargetDuration to all 0's
misses = 10; // Reset misses
score = 0; // Reset score to 0
level = 1; // Reset to level 1
// Flash "Game Over"
// Flash score
// Flash "New Game"
// Reset LCD
state = NEWGAME; // set this to NEWGAME until a new game button is created
break;
case NEWGAME:
tmrpcm.play((char *)"NG.WAV");
while (tmrpcm.isPlaying()) {}
counter = 0; // Reset the timer, I want to reset it here rather that GAMEOVER in case it hasn't been play for a while.
BaseDuration = 10; // Length of nominal time the target will be displayed for level 1
nextSerial = 0;
nextTarget = (char)random(0,numTargets); // Randomly choose a candidate for the next valid target
while(TargetDuration[nextTarget]!=0) { // Continue randomly choosing candidates for the next valid target until you find one that is currently not valid
nextTarget = (char)random(0,numTargets);
}
TargetValid[nextTarget] = true; // Set the next random valid target
RandomDuration = random(1000*BaseDuration*.8,1000*BaseDuration*1.2); // Calculate the duration of this target
TargetDuration[nextTarget] = counter + RandomDuration; // Set the duration for this target
state = IDLE;
break;
} // switch(state)
// Check to see if any of the villains have expired without being hit
for (char target=0; target<numTargets; target++) { // Loop through all numTargets targets
if (TargetDuration[target]>0) { // For each valid target...
if (counter > TargetDuration[target]) { // If the target's duration has expired
if (TargetVillain[target] & TargetValid[target]) { // If the valid target was a villain
misses--; // Count this as a miss
tmrpcm.play((char )"DL.WAV");
}
TargetValid[target] = false;
TargetDuration[target] = 0;
nextTarget = (char)random(0,numTargets); // Randomly choose a candidate for a new valid target
while(TargetDuration[nextTarget]!=0) { // Continue randomly choosing candidates for the new valid target until you find one that is currently not valid
nextTarget = (char)random(0,numTargets);
}
TargetValid[nextTarget] = true; // Set the new random valid target
RandomDuration = random(1000BaseDuration*.8,1000BaseDuration1.2); // Calculate the duration of this target
TargetDuration[nextTarget] = counter + RandomDuration; // Set the duration for this target
}
}
}
// See if the level timer has expired
if (counter>(unsigned long)level100030) {
if (state==IDLE) {
state = NEXTLEVEL;
}
}
// See if game is over
if (misses <= 0) {
if (state==IDLE) {
state = GAMEOVER;
}
}
// if (counter>nextSerial) {
// Serial.print("Counter: "); Serial.println(counter);
// Serial.print("Level: "); Serial.println((uint8_t)level);
// Serial.print("Misses: "); Serial.println((uint8_t)misses);
// Serial.print("TargetValid: ");
// for (char target=0; target<numTargets; target++) {
// Serial.print((uint8_t)TargetValid[target]); Serial.print(",");
// }
// Serial.println("");
// Serial.print("TargetDuration: ");
// for (char target=0; target<numTargets; target++) {
// Serial.print(TargetDuration[target]); Serial.print(",");
// }
// Serial.println("");
// nextSerial = counter + 1000;
// }
} // void loop()
void updateLEDs() {
ledArray[0] = (TargetValid[7] << 7) | (TargetValid[6] << 6) | (TargetValid[5] << 5) | (TargetValid[4] <<4) | (TargetValid[3] << 3) | (TargetValid[2] << 2) | (TargetValid[1] << 1) | TargetValid[0];
ledArray[1] = (TargetValid[15] << 7) | (TargetValid[14] << 6) | (TargetValid[13] << 5) | (TargetValid[12] <<4) | (TargetValid[11] << 3) | (TargetValid[10] << 2) | (TargetValid[9] << 1) | TargetValid[8];
// Ensure clockPin is low prior to transmitting to clock on rising edges
digitalWrite(clockPin, LOW);
// Set the latchPin low while transmitting
digitalWrite(latchPin, LOW);
// Loop through each byte in the LED channel array
for (int j=0; j < numLedChannels; j++) {
// Shift out the bytes of the array from least to greatest and the bits in the byte from least to greatest
shiftOut(dataPin, clockPin, LSBFIRST, ledArray[j]);
}
// set the latchPin high when done transmitting
digitalWrite(latchPin, HIGH);
return;
}
ISR(TIMER2_COMPA_vect) {
counter++;
}