/*
 * Gray_WorkJunkCarnival
 *
 * Code by: Gray Mack
 * License: Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0) 
 *          https://creativecommons.org/licenses/by-nc-sa/4.0/
 * Created: 03/04/2025
 * One Line Description: A little game system
 * Board: TBD
 * Select Board: TBD
 * Other Hardware: Button, LEDS, IR receiver
 * Detailed Description:
 *  A few mini games on a 3.3v device
 *  Use a jadak barcode scanner that has a trigger pin and serial output of the barcode text
 *  A proximity sensor to activate a barcode read
 *  Use an LCD screen
 *   if barcode starts with "A:" then it is a joke card, print the barcode text to the screen
 *   if barcode starts with "8:" then it is a magic 8 ball request, print the familiar random text
 *   if it is a known code, enter the shootem game
 *  the gun consists of 
 *   one cherry switch trigger, 
 *   one IR-remote decoder that outputs high when 40khz signal is present fitted with a shield tube,
 *   an output LED
 *  3 targets with each having: R, G, B LED, IR LED to send 40khz signal
 *  A small speaker to play tones
 *  1 Spare Button Input
 * 
 * Rev History:
 * 03/04/2025 initial code creation
 * 03/05/2025 updates 2
 * 03/14/2025 building circuits, testing hardware
 * 04/04/2025 integrating the results of pre-testing into the application.
 * 04/09/2025 integrating the latest results of pre-testing into the application.
 * 04/15/2025 game algorithm development
 * 05/12/2025 merge in test code forom Gray_Test_Esp32s3_1.ino
 * 05/19/2025 minor cleanup, first run bug fixes
 * //2025 ...
 */
// ----[ configuration  ]------------------------------------
#define CONSOLE_BAUD 115200
#define JADAK_BAUD 19200
#define STARTUP_MESSAGE "Gray_WorkJunkCarnival v1.0"
#define NUM_LEDS 1
#define NUM_TARGETS 3
// ----[ included libraries ]------------------------------------
#include <Arduino.h>
#include <Bounce2.h> // https://github.com/thomasfredericks/Bounce2 v2.71
#include <FastLED.h> // By Daniel Garcia v3.9.14
#include "Adafruit_VL53L0X.h" // v1.2.2
#include <LiquidCrystal_I2C.h> // https://github.com/johnrickman/LiquidCrystal_I2C v1.1.2 By Frank de Brabander
// ----[ pin definitions ]------------------------------------
// 3.3v
// 3.3v
// RST
const uint8_t PIN_RED_LED_PINS[NUM_TARGETS] =   {  4,  9, 12 };   // target flashes red when missed
const uint8_t PIN_GREEN_LED_PINS[NUM_TARGETS] = {  5, 10, 13 };   // target flashes green when hit
const uint8_t PIN_BLUE_LED_PINS[NUM_TARGETS] =  {  6, 11, 14 };   // target lights blue when active
const uint8_t PIN_BARCODE_TAKE_READING = 7;
const uint8_t PIN_RESERVED_U0RTS = 15;
const uint8_t PIN_RESERVED_U0CTS = 16;
const uint8_t PIN_UART1_TXD = 17;
const uint8_t PIN_LCD_UART = PIN_UART1_TXD;
const uint8_t PIN_UART1_RXD = 18;
const uint8_t PIN_JADAK_UART = PIN_UART1_RXD;
const uint8_t PIN_PROXIMITY = 8;
const uint8_t PIN_RESERVED_JTAG = 3;
const uint8_t PIN_RESERVED_LOG = 46;
// 9,10,11,12,13,14 see above
// 5v
// GND
// GND
const uint8_t PIN_GUN_TRIGGER = 1;
const uint8_t PIN_AVAILABLE1 = 2;
const uint8_t PIN_IR_LED_PINS[NUM_TARGETS] =    { 40, 41, 42 };
const uint8_t PIN_MUZZLE_FLASH = 39;
const uint8_t PIN_SPEAKER = 38;
const uint8_t PIN_I2C_SCL = 37;
const uint8_t PIN_I2C_SDA = 36;
const uint8_t PIN_AVAILABLE2 = 35;
const uint8_t PIN_RESERVED_BOOT = 0;
const uint8_t PIN_RESERVED_VSPI = 45;
#ifndef PIN_RGB_LED
const uint8_t PIN_RGB_LED = 48;
#endif
const uint8_t PIN_IR_GUN_SENSOR = 47;
const uint8_t PIN_SPARE_BUTTON = 21;
const uint8_t PIN_RESERVED_USB_DP = 20;
const uint8_t PIN_RESERVED_USB_DM = 19;
// GND
// GND
// ----[ constants ]------------------------------------
const int8_t NO_TARGET = -1; // hit values are 0,1,2
const uint16_t IR_FREQUENCY = 56000;
const uint16_t SPEAKER_FREQUENCY = 440;
const uint16_t TURN_OFF_IR_LED = 0;
const uint16_t TURN_OFF_SPEAKER = 0;
//auto//const int LEDC_CHANNEL = 0;  // LEDC channel (0-15) to run the tone command for 40khz square wave to IR LED
const int PWM_RESOLUTION = 2; // (1-16 bits)
const uint8_t DISTANCE_SENSOR_ADDRESS = 0x50;
const int DISTANCE_MINIMUM_MM = 90;
const int DISTANCE_TO_TRIGGER_BARCODE_MM = 225;
const int DISTANCE_OUT_OF_RANGE = 0;
const uint8_t LED_ON = HIGH;
const uint8_t LED_OFF = LOW;
const uint8_t PROXIMITY_SENSED = LOW;
const uint8_t PROXIMITY_ABSENT = HIGH;
const uint8_t IR_TARGET_HIT = LOW;
const uint8_t IR_TARGET_MISSED = HIGH;
const uint8_t TRIGGER_RELEASED = LOW;
const uint8_t TRIGGER_PULLED = HIGH;
const uint8_t REQUEST_BARCODE_SCAN = HIGH;
const uint8_t RELEASE_BARCODE_SCAN = LOW;
enum LedSetting : uint8_t { TurnOn, TurnOff, TurnOffAfterBeingOnForAWhile };
const uint8_t LCD_ROWS = 2;
const uint8_t LCE_COLS = 16;
enum TargetActionType : uint8_t { TargetLightLive, TargetLightHit, TargetLightMiss, TargetLightDeactivated };
// ----[ game constants ]------------------------------------
#define TARGET_GAME_BARCODE "01HS0215S0010000081"
#define MAGIC8_BARCODE "Magic8"
#define ANSWER_PREFIX_BARCODE "A:"
#define TESTMODE_BARCODE "TestMode"
enum SystemStateEnum : uint8_t {
   Attract, 
   Hunting, TargetHit, TargetMiss, GameOver, TestMode
};
enum SoundType : uint8_t { 
   NoSound, 
   TargetLiveSound, TargetHitSound, TargetMissSound, 
   ContinueSound 
};
const uint16_t GAME_MAX_SHOTS = 30;
const uint32_t GAME_MAX_TIME_MS = 30000;
const uint32_t JADAK_TRIGGER_TIME_MS = 10;
const uint32_t BARCODE_WAITING_TIME_MS = 45000;
const uint32_t DISPLAY_MESSAGE_TIME_MS = 30000;
const uint32_t BACKLIGHT_OFF_TIME_MS = 30000;
const uint32_t HIT_MISS_INDICATOR_TIME_MS = 200;
const uint32_t GAMEOVER_ANNOUNCE_TIME_MS = 3000;
const uint32_t TARGET_EXPIRE_TIME_MS = 1700;
const uint32_t MUZZLE_FLASH_OFF_TIME_MS = 250;
const int MAX_BARCODE_SIZE = 30;
//#define IsGameRunning() (GameStartTime != 0)
const uint8_t NO_ACTIVE_TARGET = -1;
#define MAGIC_8 "Magic 8:"
const uint8_t Magic8Responses = 20; // 10 affirmative, 5 non comittal, 5 negative
const char *Magic8Messages[Magic8Responses] = {
// ----------------
  "It is certain",
  "It is so",            //"It is decidedly so",
  "Without a doubt",
  "Yes definitely",
  "Rely on it",          //"You may rely on it",
  "As I see it, yes",
// ----------------
  "Most likely",
  "Outlook good",
  "Yes",
  "Signs point yes",  //"Signs point to yes",
// ----------------
  "Hazy, try again",  //"Reply hazy, try again",
  "Ask again later",
  "I wont tell you",  //"Better not tell you now",
  "Unpredictable",  // "Cannot predict now", 
  "Ask again",      //"Concentrate and ask again",
// ----------------
  "Perhaps not",  //"Dont count on it",
  "My reply is no",
  "Sources say no",  //"My sources say no",
  "Outlook: no", // "Outlook not so good",
  "Very doubtful"
};
#define LCD_INTRO            "Luminex Carnival"
#define LCD_ANSWER           "Answer:         "
#define LCD_SCAN_THE_8_BALL  "Scan the 8 ball "
#define LCD_FOR_AN_ANSWER    "for an answer   "
#define LCD_MAGIC_8_ANSWERS  "Magic 8 answers:"
#define LCD_STUN_THE_ROBOTS  "Stun the Robots"
#define LCD_OUT_OF_SHOTS     "Out of Shots!   "
#define LCD_OUT_OF_TIME      "Out of time!    "
#define LCD_TGT___SHOTS_     "Tgt:  Shots:    "
#define LCD_TIME___SCORE_    "Time:   Score:  "
#define LCD_HELLO_GUYS       "      Hello Guys"
#define LCD_VL53L0_BOOT_FAIL "VL53L0 boot fail"
// ----[ predeclarations ]------------------------------------
void setup();
void loop();
void EnterAttractMode();
void AttractMode();
void StartBarcodeRead();
void AnswerJoke(String answer);
void ProximityTo8ballMessage();
void AnswerMagic8();
void PrintBarcode(String barcode);
void StartTargetGame();
void ConsiderNewTarget();
bool CheckForTrigger();
bool CheckEndGame();
void EndTargetGame();
void ShowGameStatsLcd();
void ShowGameStatsConsole();
void StartTestMode();
void RunTestMode();
void PlaySound(SoundType soundEffect);
int8_t CheckForTargetHit();
uint32_t GetNextTargetPopTimeMs();
void PixelHitToRgb(uint8_t whichTarget);
int GetRangingMm(bool print = false);
void SetTargetColor(int8_t target, CRGB color);
void SetSingleTargetLight(int8_t target, TargetActionType action);
void DistanceTestLoop();
void SetBacklight(LedSetting val);
void SetMuzzleFlash(LedSetting val);
// ----[ helper classes ]-------------------------------------------
// ----[ global variables ]------------------------------------
SystemStateEnum SystemState;
bool BacklightOn = false;
bool MuzzleFlashOn = false;
// Target game variables
//bool GameRunning = false;  // ?
int8_t GameScore;
int8_t GameShots;
int8_t GameTimeRemain;
int8_t CurrentTarget = NO_TARGET;
uint32_t GameStartTimeMs = 0; // changes to millis() when the target game starts and when attract mode starts (to potentially track time since user interaction)
uint32_t GameNextTargetPopTimeMs;
uint32_t LastActionTime = 0;  // changes to millis() when 1 enter attract mode, 2 trigger pulled while Hunting, 3 game over reached, 
                                                       // 4 answer joke, 5 proximity to 8ball, 6 answer 8ball, 7 print barcode, 
                                                       // 8 start target game, 9 end target game, 10 start test mode, 11 state changes from TargetHit to Hunting so next pop time will be accurate
                              // used to know when to finish target hit light,target miss light,game over message, 
                              //          and when to pop new target, when to turn off lcd light
Bounce2::Button GunTrigger = Bounce2::Button();
Bounce2::Button ProximityToMagic8Ball = Bounce2::Button();
CRGB leds[NUM_LEDS];
Adafruit_VL53L0X DistanceSensor = Adafruit_VL53L0X();
int objectDistanceFromScannerMM = DISTANCE_OUT_OF_RANGE;
LiquidCrystal_I2C Lcd(0x27,20,4);  // set the LCD address to 0x27 for a 16 chars and 2 line display
// ----[ code ]------------------------------------
void setup()
{
   Serial.begin(CONSOLE_BAUD);
   delay(500);
   Serial.println(STARTUP_MESSAGE);
   FastLED.addLeds<NEOPIXEL, PIN_RGB_LED>(leds, NUM_LEDS);  // GRB ordering is assumed
   FastLED.setBrightness(64);
   Serial1.begin(JADAK_BAUD, SERIAL_8N1, PIN_UART1_RXD, PIN_UART1_TXD); // Initialize Serial1 for communication with RX1 JADAK barcode scanner
   delay(500);
   for (int i = 0; i < 3; i++)
   {
      pinMode(PIN_BLUE_LED_PINS[i], OUTPUT);
      pinMode(PIN_RED_LED_PINS[i], OUTPUT);
      pinMode(PIN_GREEN_LED_PINS[i], OUTPUT);
      pinMode(PIN_IR_LED_PINS[i], OUTPUT);
   }
   pinMode(PIN_IR_GUN_SENSOR, INPUT);
   pinMode(PIN_PROXIMITY, INPUT);
   pinMode(PIN_BARCODE_TAKE_READING, OUTPUT);
   pinMode(PIN_MUZZLE_FLASH, OUTPUT);
   GunTrigger.attach(PIN_GUN_TRIGGER, INPUT_PULLUP);
   GunTrigger.interval(5); 
   GunTrigger.setPressedState(TRIGGER_PULLED);
   ProximityToMagic8Ball.attach(PIN_PROXIMITY, INPUT);
   ProximityToMagic8Ball.interval(50);
   ProximityToMagic8Ball.setPressedState(PROXIMITY_SENSED);
   Wire.setPins(PIN_I2C_SDA, PIN_I2C_SCL);
   Wire.begin();
   Lcd.init();
   SetBacklight(LedSetting::TurnOn);
   Lcd.setCursor(0,0);
   Lcd.print(LCD_HELLO_GUYS);
   if(!DistanceSensor.begin(DISTANCE_SENSOR_ADDRESS, true, &Wire, Adafruit_VL53L0X::VL53L0X_SENSE_HIGH_SPEED))
   {
      Lcd.setCursor(0,0);
      Lcd.print(LCD_VL53L0_BOOT_FAIL);
   }
   // Configure the LEDC peripheral for tone() capability
   ledcAttach(PIN_IR_LED_PINS[0], IR_FREQUENCY, PWM_RESOLUTION);
   ledcWriteTone(PIN_IR_LED_PINS[0], TURN_OFF_IR_LED);
   ledcAttach(PIN_IR_LED_PINS[1], IR_FREQUENCY, PWM_RESOLUTION);
   ledcWriteTone(PIN_IR_LED_PINS[1], TURN_OFF_IR_LED);
   ledcAttach(PIN_IR_LED_PINS[2], IR_FREQUENCY, PWM_RESOLUTION);
   ledcWriteTone(PIN_IR_LED_PINS[2], TURN_OFF_IR_LED);
   ledcAttach(PIN_SPEAKER, IR_FREQUENCY, PWM_RESOLUTION);
   ledcWriteTone(PIN_SPEAKER, TURN_OFF_SPEAKER);
   // lcd test
   for (int b=0; b<=100; b+=5)
   {
     Lcd.setCursor(0,1);
     Lcd.print(millis());
     Serial.println(millis());
     delay(100);
   }
   Serial.println("setup complete");
   Lcd.clear();
   EnterAttractMode();
   
}
void loop() 
{
   switch(SystemState)
   {
      case SystemStateEnum::Attract:
         AttractMode();
         break;
      case SystemStateEnum::Hunting:
         if(CheckEndGame())
         {
            EndTargetGame();
            SystemState = SystemStateEnum::GameOver;
         }
         ConsiderNewTarget();
         if(CheckForTrigger())
         {
            Serial.print("Bang! ");
            SetMuzzleFlash(LedSetting::TurnOn);
            LastActionTime = millis();
            int16_t targetHit = CheckForTargetHit();
            if(targetHit != NO_TARGET && targetHit == CurrentTarget)
            {
               Serial.println("Target hit!");
               GameScore++;
               SetSingleTargetLight(CurrentTarget, TargetActionType::TargetLightHit);
               PlaySound(SoundType::TargetHitSound);
               SystemState = SystemStateEnum::TargetHit;
            }
            else if(CurrentTarget != NO_TARGET)
            {
               Serial.println("Target miss");
               PlaySound(SoundType::TargetMissSound);
               SetSingleTargetLight(CurrentTarget, TargetActionType::TargetLightMiss);
               SystemState = SystemStateEnum::TargetMiss;
            }
            else // trigger fired but no current targets
            {
               Serial.println("Wasted shot");
               PlaySound(SoundType::TargetMissSound);
               //SystemState stays SystemStateEnum::Hunting, another shot is quickly possible
            }
         }
         SetMuzzleFlash(LedSetting::TurnOffAfterBeingOnForAWhile);
         break;
      case SystemStateEnum::TargetHit:
         // stage to pop a new target soon
         if (millis() - LastActionTime >= HIT_MISS_INDICATOR_TIME_MS)
         {
            SetSingleTargetLight(CurrentTarget, TargetActionType::TargetLightDeactivated);
            //digitalWrite(PIN_MUZZLE_FLASH, LED_OFF);
            PixelHitToRgb(NO_ACTIVE_TARGET);
            CurrentTarget = NO_ACTIVE_TARGET;
            GameNextTargetPopTimeMs = GetNextTargetPopTimeMs();
            LastActionTime = millis(); // so GameNextTargetPopTimeMs does not include HIT_MISS_INDICATOR_TIME_MS
            SystemState = SystemStateEnum::Hunting;
         }
         SetMuzzleFlash(LedSetting::TurnOffAfterBeingOnForAWhile);
         break;
      case SystemStateEnum::TargetMiss:
         // previous target is still active
         if (millis() - LastActionTime >= HIT_MISS_INDICATOR_TIME_MS)
         {
            SetSingleTargetLight(CurrentTarget, TargetActionType::TargetLightLive);
            //digitalWrite(PIN_MUZZLE_FLASH, LED_OFF);
            PixelHitToRgb(NO_ACTIVE_TARGET);
            SystemState = SystemStateEnum::Hunting;
         }
         SetMuzzleFlash(LedSetting::TurnOffAfterBeingOnForAWhile);
         break;
      case SystemStateEnum::GameOver:
         // clear the score and go back to attract mode
         if(millis() - LastActionTime >= GAMEOVER_ANNOUNCE_TIME_MS)
         {
            //SetSingleTargetLight(CurrentTarget, TargetActionType::TargetLightDeactivated);
            //Serial.println("Game over.");
            //Lcd.setCursor(0,0);
            //Lcd.print("-[ Game Over ]- ");
            //LastActionTime = millis();
            EnterAttractMode(); //SystemState = SystemStateEnum::Attract;
         }
         break;
      case SystemStateEnum::TestMode:
         RunTestMode();
         break;
   }
   PlaySound(SoundType::ContinueSound);
}
// ====================================================
// ----[ function ]------------------------------------
void EnterAttractMode()
{
   Serial.println("EnterAttractMode");
   SetBacklight(LedSetting::TurnOn);
   Lcd.clear();
   Lcd.setCursor(0,0);
   Lcd.print(LCD_INTRO);
   GameStartTimeMs = millis(); // attract start time
   LastActionTime = millis();
   SystemState = SystemStateEnum::Attract;
}
// ----[ function ]------------------------------------
void AttractMode()
{
   // check for barcode results
   if(Serial1.available())
   {
      String barcodeValue = Serial1.readString();
      if(strncmp(barcodeValue.c_str(), TARGET_GAME_BARCODE, MAX_BARCODE_SIZE) == 0)
      {
         StartTargetGame();
      }
      else if(strncmp(barcodeValue.c_str(), MAGIC8_BARCODE, MAX_BARCODE_SIZE) == 0)
      {
         AnswerMagic8();
      }
      else if(strncmp(barcodeValue.c_str(), ANSWER_PREFIX_BARCODE, 2) == 0)
      {
         AnswerJoke(barcodeValue);
      }
      else if(strncmp(barcodeValue.c_str(), TESTMODE_BARCODE, MAX_BARCODE_SIZE) == 0)
      {
         StartTestMode();
      }
      else
      {
         PrintBarcode(barcodeValue);
      }
   }
  
   // check distance sensor if ShouldReadBarcode
   EVERY_N_MILLISECONDS(100) {
      objectDistanceFromScannerMM = GetRangingMm();
   }
   static bool previousObjectDetected = false;
   bool objectDetected = objectDistanceFromScannerMM > DISTANCE_MINIMUM_MM && 
                         objectDistanceFromScannerMM < DISTANCE_TO_TRIGGER_BARCODE_MM;
   if(objectDetected && !previousObjectDetected)
   {
      StartBarcodeRead();
   }
   previousObjectDetected = objectDetected;
   // check for hand proximity to 8ball
   ProximityToMagic8Ball.update();
   if(ProximityToMagic8Ball.pressed())
   {
      ProximityTo8ballMessage();
   }
//   static bool lastProximityTo8ball = false;
//   bool proximityTo8ball = digitalRead(PIN_PROXIMITY) == PROXIMITY_SENSED;
//   if(proximityTo8ball && !lastProximityTo8ball )
//   {
//      ProximityTo8ballMessage();
//   }
//   lastProximityTo8ball = proximityTo8ball;
   SetBacklight(LedSetting::TurnOffAfterBeingOnForAWhile);
}
// ----[ function ]------------------------------------
void StartBarcodeRead()
{
   Serial.println("Reading for barcode");
   // Send a pulse to jadak camera 
   digitalWrite(PIN_BARCODE_TAKE_READING, REQUEST_BARCODE_SCAN);
   delayMicroseconds(10);
   digitalWrite(PIN_BARCODE_TAKE_READING, RELEASE_BARCODE_SCAN);
}
// ----[ function ]------------------------------------
void AnswerJoke(String answer)
{
   Serial.print("Joke: ");
   Serial.println(answer);
   SetBacklight(LedSetting::TurnOn);
   Lcd.clear();
   Lcd.setCursor(0,0);
   Lcd.print(LCD_ANSWER);
   Lcd.setCursor(0,1);
   Lcd.print(answer.substring(2)); // skip the "A:""
   LastActionTime = millis();
}
// ----[ function ]------------------------------------
void ProximityTo8ballMessage()
{
   Serial.println("Proximity to 8 ball detected");
   SetBacklight(LedSetting::TurnOn);
   Lcd.clear();
   Lcd.setCursor(0,0);
   Lcd.print(LCD_SCAN_THE_8_BALL);
   Lcd.setCursor(0,1);
   Lcd.print(LCD_FOR_AN_ANSWER);
   LastActionTime = millis();
}
// ----[ function ]------------------------------------
void AnswerMagic8()
{
   Serial.println("Magic 8 Ball Says:");
   SetBacklight(LedSetting::TurnOn);
   Lcd.clear();
   Lcd.setCursor(0,0);
   Lcd.print(LCD_MAGIC_8_ANSWERS);
   int yourLuck = random(Magic8Responses);
   Serial.println(Magic8Messages[yourLuck]);
   Lcd.setCursor(0,1);
   Lcd.print(Magic8Messages[yourLuck]);
   LastActionTime = millis();
}
// ----[ function ]------------------------------------
void PrintBarcode(String barcode)
{
   Serial.print("barcode seen: ");
   Serial.println(barcode);
   SetBacklight(LedSetting::TurnOn);
   Lcd.clear();
   Lcd.setCursor(0,0);
   Lcd.print(barcode);
   LastActionTime = millis();
}
// ====================================================
// ----[ function ]------------------------------------
void StartTargetGame()
{
   Serial.println("Starting Target Game:");
   SetBacklight(LedSetting::TurnOn);
   Lcd.clear();
   Lcd.setCursor(0,0);
   Lcd.print(LCD_STUN_THE_ROBOTS);
//   lastStateChangeTime = millis();
   CurrentTarget = NO_ACTIVE_TARGET;
   GameNextTargetPopTimeMs = GetNextTargetPopTimeMs();
   GameScore = 0;
   GameShots = 30;
   //IsGameRunning = true;
   delay(500);
   Serial.println("Game started!");
   SystemState = SystemStateEnum::Hunting;
   GameStartTimeMs = millis();
   LastActionTime = millis();
}
// ----[ function ]------------------------------------
void ConsiderNewTarget()
{
  static uint32_t targetPopTimeMs = 0;
  bool timeForTarget = (CurrentTarget == NO_ACTIVE_TARGET) && (millis() - LastActionTime > GameNextTargetPopTimeMs);
  bool timeFortargetToExpire = (CurrentTarget != NO_ACTIVE_TARGET) && (millis() - targetPopTimeMs > TARGET_EXPIRE_TIME_MS);
  if( ! timeForTarget && ! timeFortargetToExpire) return;
  
  // pop the new target
  CurrentTarget = random(NUM_TARGETS);
  targetPopTimeMs = millis();
  Serial.print("Target "); Serial.println(CurrentTarget);
  SetSingleTargetLight(CurrentTarget, TargetActionType::TargetLightLive);
  PlaySound(SoundType::TargetLiveSound);
}
// ----[ function ]------------------------------------
bool CheckForTrigger()
{
   GunTrigger.update();
   if(GunTrigger.pressed() && GameShots > 0)
   {
      GameShots--;
      return true;
   }
   return false;
}
// ----[ function ]------------------------------------ 
// the game ends when out of shots or game time remain <= 0
bool CheckEndGame()
{
   if(GameShots <= 0)
   {
      ShowGameStatsLcd();
      ShowGameStatsConsole();
      Serial.println(LCD_OUT_OF_SHOTS);
      Lcd.setCursor(0,0);
      Lcd.print(LCD_OUT_OF_SHOTS);
      //delay(500);
      //GameStartTimeMs = 0;
      return true;
   }
   static int16_t lastTimeRemainSec = 0;
   GameTimeRemain = int16_t((GAME_MAX_TIME_MS - (millis() - GameStartTimeMs)) / 1000);
   if(GameTimeRemain != lastTimeRemainSec)
   {  // every second, do countdown to screen with score
      ShowGameStatsLcd();
      ShowGameStatsConsole();
      lastTimeRemainSec = GameTimeRemain;
   }
   if(GameTimeRemain <= 0)
   {
      Serial.println(LCD_OUT_OF_TIME);
      Lcd.setCursor(0,0);
      Lcd.print(LCD_OUT_OF_TIME);
      //delay(500);
      //GameStartTimeMs = 0;
      return true;
   }
   return false;
}
// ----[ function ]------------------------------------
void EndTargetGame()
{
   Serial.println("Game Ended!");
   SetSingleTargetLight(CurrentTarget, TargetActionType::TargetLightDeactivated);
   SetMuzzleFlash(LedSetting::TurnOff);
   PlaySound(SoundType::NoSound);
   LastActionTime = millis();
}
// ----[ function ]------------------------------------ 
void ShowGameStatsLcd()
{
   int row = 0;
   Lcd.setCursor(0,row);
   Lcd.print(LCD_TGT___SHOTS_);
   if(CurrentTarget != NO_TARGET)
   {
      Lcd.setCursor(4,row);
      Lcd.print(CurrentTarget);
   }
   Lcd.setCursor(12,row);
   Lcd.print(GameShots);
   row = 1;
   Lcd.setCursor(0,row);
   Lcd.print(LCD_TIME___SCORE_);
   if(GameTimeRemain > 0)
   {
    Lcd.setCursor(5,row);
    Lcd.print(GameTimeRemain);
   }
   Lcd.setCursor(14,row);
   Lcd.print(GameScore);
}
// ----[ function ]------------------------------------
void ShowGameStatsConsole()
{
   Serial.print("Time:");
   if(GameTimeRemain <= 0) { Serial.print("--"); } 
   else 
   {
      if(GameTimeRemain < 10) { Serial.print(" "); }
      Serial.print(GameTimeRemain);
   }
   Serial.print(" Score:"); 
   if(GameScore < 10) Serial.print(" ");
   Serial.print(GameScore);
   Serial.print(" Shots:"); 
   Serial.print(GameShots);
   Serial.println();
}
// ----[ function ]------------------------------------
void StartTestMode()
{
   Serial.print("TestMode");
   SetBacklight(LedSetting::TurnOn);
   Lcd.clear();
   Lcd.setCursor(0,0);
   Lcd.print("TestMode");
   SystemState = SystemStateEnum::TestMode;
   LastActionTime = millis();
}
// ----[ function ]------------------------------------
void RunTestMode()
{
   static bool blinkToggler = false;
   static int counter = 0;
   int8_t targetHit = NO_TARGET;
   EVERY_N_MILLISECONDS(500)
   {
      blinkToggler = !blinkToggler;
      if(blinkToggler)
      {
         ledcWriteTone(PIN_SPEAKER, SPEAKER_FREQUENCY);
      }
      else
      {
         ledcWriteTone(PIN_SPEAKER, TURN_OFF_SPEAKER);
      }
   }
   GunTrigger.update();
   if(GunTrigger.pressed())
   {
      //digitalWrite(PIN_MUZZLE_FLASH, HIGH);   
      Serial.println("Bang!");
      targetHit = CheckForTargetHit();
      PixelHitToRgb(targetHit);
      SetSingleTargetLight(targetHit, TargetActionType::TargetLightHit);
      //digitalWrite(PIN_MUZZLE_FLASH, LOW);
   }
   // continuous reading code to test target response
   digitalWrite(PIN_MUZZLE_FLASH, HIGH);   
   targetHit = CheckForTargetHit();
   PixelHitToRgb(targetHit);
   digitalWrite(PIN_MUZZLE_FLASH, LOW);
   EVERY_N_MILLISECONDS(100)
   {
      objectDistanceFromScannerMM = GetRangingMm();
   }
   static bool previousObjectDetected = false;
   bool objectDetected = objectDistanceFromScannerMM > DISTANCE_MINIMUM_MM && 
                         objectDistanceFromScannerMM < DISTANCE_TO_TRIGGER_BARCODE_MM;
   if(objectDetected && !previousObjectDetected)
   {
      digitalWrite(PIN_BARCODE_TAKE_READING, REQUEST_BARCODE_SCAN);
      delayMicroseconds(10);
      digitalWrite(PIN_BARCODE_TAKE_READING, RELEASE_BARCODE_SCAN);
   }
   previousObjectDetected = objectDetected;
   ProximityToMagic8Ball.update();
   if(ProximityToMagic8Ball.pressed())
   {
      Serial.println("Proximity to 8 ball!");
   }
   bool log = objectDetected || GunTrigger.pressed();
   EVERY_N_MILLISECONDS(1000)
   {
      log = true;
   }
   if(log) {
      if(digitalRead(PIN_PROXIMITY) == PROXIMITY_SENSED) Serial.print("   Prox"); else Serial.print(" noProx");
      if(objectDetected) Serial.print("   Obj"); else Serial.print(" noObj");
      if(targetHit > 0) { Serial.print("   IrHit"); Serial.print(targetHit); Serial.print(" "); } else Serial.print("   IrMiss ");
      Serial.print(" bang="); Serial.print(GunTrigger.isPressed());
      Serial.print(" counter="); Serial.print(counter);
      Serial.print(" dist="); Serial.println(objectDistanceFromScannerMM);
   }
   if(Serial1.available())
   {
      Serial.print("jadack says=");
      Serial.println(Serial1.readString());
   }
   EVERY_N_MILLISECONDS(1000)
   {
      Lcd.setCursor(0,0);
      Lcd.print(counter++);
   }
}
// ----[ function ]------------------------------------
void PlaySound(SoundType soundEffect)
{
   switch(soundEffect)
   {
      case SoundType::TargetLiveSound:
      break;
      case SoundType::TargetHitSound:
         //ledcWriteTone(PIN_SPEAKER, 600);
      break;
      case SoundType::TargetMissSound:
        //ledcWriteTone(PIN_SPEAKER, 60);
      break;
      case SoundType::ContinueSound:
      break;
      case SoundType::NoSound:
         ledcWriteTone(PIN_SPEAKER, TURN_OFF_SPEAKER);
      break;
   }
}
// ----[ function ]------------------------------------
// ----[ function ]------------------------------------
// ----[ function ]------------------------------------
// ----[ function ]------------------------------------
int8_t CheckForTargetHit()
{
  uint8_t target = NO_TARGET;
      
  //ledcWriteTone(PIN_IR_LED_PINS[0], IR_FREQUENCY);
  ledcChangeFrequency(PIN_IR_LED_PINS[0], IR_FREQUENCY, 2);
  ledcWrite(PIN_IR_LED_PINS[0], 1);
  delayMicroseconds(600);
  if(digitalRead(PIN_IR_GUN_SENSOR) == IR_TARGET_HIT) target=0;
  ledcWriteTone(PIN_IR_LED_PINS[0], TURN_OFF_IR_LED);
  delayMicroseconds(200);
  //ledcWriteTone(PIN_IR_LED_PINS[1], IR_FREQUENCY);
  ledcChangeFrequency(PIN_IR_LED_PINS[1], IR_FREQUENCY, 2);
  ledcWrite(PIN_IR_LED_PINS[1], 1);
  delayMicroseconds(600);
  if(digitalRead(PIN_IR_GUN_SENSOR) == IR_TARGET_HIT) target=1;
  ledcWriteTone(PIN_IR_LED_PINS[1], TURN_OFF_IR_LED);
  delayMicroseconds(200);
  //ledcWriteTone(PIN_IR_LED_PINS[2], IR_FREQUENCY);
  ledcChangeFrequency(PIN_IR_LED_PINS[2], IR_FREQUENCY, 2);
  ledcWrite(PIN_IR_LED_PINS[2], 1);
  delayMicroseconds(600);
  if(digitalRead(PIN_IR_GUN_SENSOR) == IR_TARGET_HIT) target=2;
  ledcWriteTone(PIN_IR_LED_PINS[2], TURN_OFF_IR_LED);
  return target; 
}
// ----[ function ]------------------------------------
uint32_t GetNextTargetPopTimeMs()
{
   return random(1000, 2000);
}
// ----[ function ]------------------------------------
void PixelHitToRgb(uint8_t whichTarget)
{
  switch(whichTarget) {
    case 0:  leds[0] = CRGB::Red;   break;
    case 1:  leds[0] = CRGB::Green; break;
    case 2:  leds[0] = CRGB::Blue;  break;
    default: leds[0] = CRGB::Black; break;
  }
      FastLED.show();
}
// ----[ function ]------------------------------------
int GetRangingMm(bool print)
{
  VL53L0X_RangingMeasurementData_t measure;
    
  DistanceSensor.getSingleRangingMeasurement(&measure, false); // pass in 'true' to get debug data printout!
  if (measure.RangeStatus != 4) {  // phase failures have incorrect data
    if(print) { Serial.print("Distance (mm): "); Serial.println(measure.RangeMilliMeter); }
    return measure.RangeMilliMeter;
  } else {
    if(print) { Serial.println(" out of range "); }
    return DISTANCE_OUT_OF_RANGE;
  }
}
// ----[ function ]------------------------------------
void SetTargetColor(int8_t target, CRGB color)
{
   if(target <= NO_TARGET || target >= NUM_TARGETS) return;
   //Serial.print("TSC"); Serial.println(target);
   if(color == CRGB::Red) {
      digitalWrite(PIN_RED_LED_PINS[target], HIGH);
      digitalWrite(PIN_GREEN_LED_PINS[target], LOW);
      digitalWrite(PIN_BLUE_LED_PINS[target], LOW);
   } else if(color == CRGB::Green) {
      digitalWrite(PIN_RED_LED_PINS[target], LOW);
      digitalWrite(PIN_GREEN_LED_PINS[target], HIGH);
      digitalWrite(PIN_BLUE_LED_PINS[target], LOW);
   } else if(color == CRGB::Blue) {
      digitalWrite(PIN_RED_LED_PINS[target], LOW);
      digitalWrite(PIN_GREEN_LED_PINS[target], LOW);
      digitalWrite(PIN_BLUE_LED_PINS[target], HIGH);
   } else {
      digitalWrite(PIN_RED_LED_PINS[target], LOW);
      digitalWrite(PIN_GREEN_LED_PINS[target], LOW);
      digitalWrite(PIN_BLUE_LED_PINS[target], LOW);
   }   
}
// ----[ function ]------------------------------------
void SetSingleTargetLight(int8_t target, TargetActionType action)
{ // passing target NO_TARGET will Black all
   //Serial.print("Target:"); Serial.print(target); Serial.print(" Action:"); Serial.println(action);
   if(target != 0) SetTargetColor(0, CRGB::Black);
   if(target != 1) SetTargetColor(1, CRGB::Black);
   if(target != 2) SetTargetColor(2, CRGB::Black);
   switch(action) {
      case TargetActionType::TargetLightLive:        SetTargetColor(target, CRGB::Blue);  break;
      case TargetActionType::TargetLightHit:         SetTargetColor(target, CRGB::Green); break;
      case TargetActionType::TargetLightMiss:        SetTargetColor(target, CRGB::Red);   break;
      case TargetActionType::TargetLightDeactivated: SetTargetColor(target, CRGB::Black); break;
   }
}
// ----[ function ]------------------------------------
void DistanceTestLoop()
{
   while(true) {
      GetRangingMm(true);
      delay(200);
   }
}
// ----[ function ]------------------------------------
void SetBacklight(LedSetting val)
{
   if(val ==  LedSetting::TurnOn)
   {
      Lcd.backlight(); BacklightOn = true;
   }
   else if(val == TurnOff)
   {
      Lcd.noBacklight(); BacklightOn = false;
   }
   else // val == TurnOffAfterBeingOnForAWhile  (check for inactivity)
   {
      if(BacklightOn && (millis() - LastActionTime > BACKLIGHT_OFF_TIME_MS))
      {
         Lcd.noBacklight(); BacklightOn = false;
      }
   }
}
// ----[ function ]------------------------------------
void SetMuzzleFlash(LedSetting val)
{
   if(val ==  LedSetting::TurnOn)
   {
      digitalWrite(PIN_MUZZLE_FLASH, LED_ON); MuzzleFlashOn = true;
   }
   else if(val == TurnOff)
   {
      digitalWrite(PIN_MUZZLE_FLASH, LED_OFF); MuzzleFlashOn = false;
   }
   else // val == TurnOffAfterBeingOnForAWhile  (check for inactivity)
   {
      if(MuzzleFlashOn && (millis() - LastActionTime > MUZZLE_FLASH_OFF_TIME_MS))
      {
         digitalWrite(PIN_MUZZLE_FLASH, LED_OFF); MuzzleFlashOn = false;
      }
   }
}
  /* Meanwhile, A Different project
  https://sensorium.github.io/Mozzi/learn/output/
  https://community.m5stack.com/topic/3334/mozzi-sound-synthesis-library-for-arduino-how-to-make-it-work-on-the-core2
  
// before including Mozzi.h, configure external audio output mode:
#include "MozziConfigValues.h"                        // for named option values
#define MOZZI_AUDIO_MODE MOZZI_OUTPUT_I2S_DAC
#include <Mozzi.h>
void audioOutput(const AudioOutput f) {
  // put here code to output the sample encapsulated by the structure f:
  // This holds either one (mono) or two (stereo) channels, which can be
  // obtained using f.l() and f.r().
  // Each contains a zero-centered integer value scaled to MOZZI_AUDIO_BITS resolution
  // e.g.:
  myDAC.write(f.l() + MOZZI_AUDIO_BIAS, f.r() + MOZZI_AUDIO_BIAS);
}*/Proximity
Trigger