class Timer
{
private:
uint32_t timestamp;
uint32_t timespan;
public:
Timer()
{
}
void Start(uint32_t timespan)
{
this->timespan = timespan;
timestamp = millis();
}
bool Expired() const
{
return (millis() - timestamp) >= timespan;
}
};
// Prompter to control blinking and beeping.
class Prompter
{
private:
Timer timer;
uint32_t toggleTimespan;
uint32_t pauseTimespan;
uint32_t numToggles;
uint32_t currentToggles;
bool indefinitely;
bool running;
byte state;
public:
Prompter(void (*function)(byte))
{
Callback = function;
}
void (*Callback)(byte);
void Start(uint32_t toggleTimespan, uint16_t numBlinks, uint32_t pauseTimespan)
{
this->toggleTimespan = toggleTimespan;
numToggles = 2 * numBlinks;
currentToggles = 0;
this->pauseTimespan = pauseTimespan;
indefinitely = false;
state = LOW;
timer.Start(toggleTimespan);
running = true;
}
void StartIndefinitely(uint32_t toggleTimespan)
{
this->toggleTimespan = toggleTimespan;
indefinitely = true;
state = LOW;
timer.Start(toggleTimespan);
running = true;
}
void Stop()
{
running = false;
currentToggles = 0;
}
void Update()
{
if (running && timer.Expired())
{
currentToggles++;
if (!indefinitely && currentToggles > numToggles)
{
state = LOW;
timer.Start(pauseTimespan);
currentToggles = 0;
}
else
{
state = state == HIGH ? LOW : HIGH;
timer.Start(toggleTimespan);
}
if (Callback != nullptr)
{
Callback(state);
}
}
}
byte State() const
{
return state;
}
bool IsRunning() const
{
return running;
}
};
class Debouncer
{
private:
Timer timer;
uint32_t debounceDelay;
byte pin;
byte state;
bool fall;
public:
Debouncer(byte pin, uint32_t debounceDelay)
{
this->pin = pin;
this->debounceDelay = debounceDelay;
}
void Begin()
{
pinMode(pin, INPUT_PULLUP);
state = digitalRead(pin);
}
byte State() const
{
return state;
}
bool Fall() const
{
return fall;
}
void Update()
{
const bool newState = digitalRead(pin);
// Hysteresis:
// If there is no change, reset the debounce timer.
// Else, compare the time difference with the debounce delay.
if (newState == state)
{
timer.Start(debounceDelay);
}
else if (timer.Expired())
{
// Successfully debounced, so reset the debounce timer and update the state.
fall = !newState && state;
state = newState;
timer.Start(debounceDelay);
return;
}
fall = false;
}
};
class Led
{
private:
byte pin;
public:
Led(byte pin)
{
this->pin = pin;
}
void Begin()
{
pinMode(pin, OUTPUT);
}
void On()
{
digitalWrite(pin, HIGH);
}
void Off()
{
digitalWrite(pin, LOW);
}
void Set(byte state)
{
digitalWrite(pin, state);
}
};
class RgbLed
{
private:
byte redPin;
byte greenPin;
byte bluePin;
public:
RgbLed(byte redPin, byte greenPin, byte bluePin)
{
this->redPin = redPin;
this->greenPin = greenPin;
this->bluePin = bluePin;
}
void Begin()
{
pinMode(redPin, OUTPUT);
pinMode(greenPin, OUTPUT);
pinMode(bluePin, OUTPUT);
}
void RedOn()
{
digitalWrite(redPin, HIGH);
}
void GreenOn()
{
digitalWrite(greenPin, HIGH);
}
void BlueOn()
{
digitalWrite(bluePin, HIGH);
}
void YellowOn()
{
digitalWrite(redPin, HIGH);
//digitalWrite(greenPin, HIGH);
analogWrite(greenPin, 128); // Output a PWM signal where the mark:space ratio is proportional to the analogue level. Range is 0 to 255.
digitalWrite(bluePin, LOW);
}
void WhiteOn()
{
digitalWrite(redPin, HIGH);
digitalWrite(greenPin, HIGH);
digitalWrite(bluePin, HIGH);
}
void VioletOn()
{
digitalWrite(redPin, HIGH);
digitalWrite(bluePin, HIGH);
}
void Off()
{
digitalWrite(redPin, LOW);
digitalWrite(greenPin, LOW);
digitalWrite(bluePin, LOW);
}
};
class Dealer
{
public:
Debouncer button;
RgbLed rgbLed;
Dealer(byte buttonPin, uint32_t buttonDebounceDelay, byte redPin, byte greenPin, byte bluePin) : button(buttonPin, buttonDebounceDelay), rgbLed(redPin, greenPin, bluePin)
{
}
void Begin()
{
button.Begin();
rgbLed.Begin();
}
void On(byte dealerIndex)
{
rgbLed.Off();
switch (dealerIndex)
{
case 0:
rgbLed.RedOn();
break;
case 1:
rgbLed.BlueOn();
break;
case 2:
rgbLed.YellowOn();
break;
case 3:
rgbLed.GreenOn();
break;
case 4:
rgbLed.WhiteOn();
break;
case 5:
rgbLed.VioletOn();
break;
default:
break;
}
}
void Off()
{
rgbLed.Off();
}
};
class Player
{
public:
Debouncer button;
Led led;
Player(byte buttonPin, uint32_t buttonDebounceDelay, byte ledPin) : button(buttonPin, buttonDebounceDelay), led(ledPin)
{
}
void Begin()
{
button.Begin();
led.Begin();
}
};
class Speaker
{
private:
byte pin;
public:
Speaker(byte pin)
{
this->pin = pin;
}
void Begin()
{
pinMode(pin, OUTPUT);
}
void On(uint16_t frequency)
{
tone(pin, frequency);
}
void Off()
{
noTone(pin);
}
};
// The Dealer.
// Button pin, debounce delay, red LED pin, green LED pin, blue LED pin.
Dealer dealer(12, 50, 9, 10, 11);
const byte NUM_PLAYERS = 6;
// The players.
Player players[NUM_PLAYERS] =
{
// Button pin, debounce delay, LED pin.
Player(A0, 50, 2),
Player(A1, 50, 3),
Player(A2, 50, 4),
Player(A3, 50, 5),
Player(A4, 50, 6),
Player(A5, 50, 7)
};
// Speaker to prompt next action.
const byte SPEAKER_PIN = 8;
Speaker speaker(SPEAKER_PIN);
// State machine states.
enum class States : byte
{
S00_WAIT_FOR_FIRST_DEALER,
S01_PROMPT_DEALER,
S02_WAIT_FOR_DEALER,
S03_PROMPT_NEXT_PLAYER,
S04_WAIT_FOR_PLAYERS,
};
States state;
byte currentDealerIndex;
byte currentPlayerIndex;
byte currentRoundIndex;
byte cardsPlayed;
const byte NUM_ROUNDS = 4;
const byte BLINKS_PER_ROUND[NUM_ROUNDS] = {3, 4, 5, 6};
// These values are cumulative, i.e. not cards per round.
// const byte CARDS_PLAYED_AFTER_ROUND[NUM_ROUNDS] = { 18, 42, 72, 108 }; // For game play.
const byte CARDS_PLAYED_AFTER_ROUND[NUM_ROUNDS] = {6, 12, 18, 24}; // For testing.
const uint16_t DEALER_FREQUENCY = 1000;
const uint16_t PLAYER_FREQUENCY = 1500;
const uint16_t CARD_FREQUENCY = 500;
//const uint32_t PLAYER_TIMER = 15 * 1000; // For game play.
const uint32_t PLAYER_TIMER = 5 * 1000; // For testing.
void PrompterCallback(byte prompterState)
{
switch (state)
{
case States::S00_WAIT_FOR_FIRST_DEALER:
for (auto &player : players)
{
player.led.Set(prompterState);
}
break;
case States::S02_WAIT_FOR_DEALER:
if (prompterState == HIGH)
{
speaker.On(CARD_FREQUENCY);
dealer.On(currentDealerIndex);
}
else
{
speaker.Off();
dealer.Off();
}
break;
case States::S04_WAIT_FOR_PLAYERS:
players[currentPlayerIndex].led.Set(prompterState);
break;
}
}
Prompter prompter(PrompterCallback);
void setup()
{
Serial.begin(115200);
// Initialise.
state = States::S00_WAIT_FOR_FIRST_DEALER;
dealer.Begin();
for (auto &player : players)
{
player.Begin();
}
speaker.Begin();
// Blink all player LEDs until one player presses their button.
prompter.StartIndefinitely(250);
Serial.println("Waiting for the first dealer.");
}
Timer promptTimer;
void loop()
{
// Update button states and blinking states.
dealer.button.Update();
for (auto &player : players)
{
player.button.Update();
}
prompter.Update();
// State machine.
switch (state)
{
case States::S00_WAIT_FOR_FIRST_DEALER:
for (byte i = 0; i < NUM_PLAYERS; i++)
{
if (players[i].button.Fall())
{
currentDealerIndex = i;
Serial.print("Found the first dealer: Player ");
Serial.println(currentDealerIndex + 1);
currentPlayerIndex = (currentDealerIndex + 1) % NUM_PLAYERS;
prompter.Stop();
for (auto &player : players)
{
player.led.Off();
}
players[currentDealerIndex].led.On();
// Beep the speaker for 500 ms.
speaker.On(DEALER_FREQUENCY); // Prompt the first dealer. You can adjust the frequency (Hz) and duration (ms).
promptTimer.Start(500);
state = States::S01_PROMPT_DEALER;
break;
}
}
break;
case States::S01_PROMPT_DEALER:
if (promptTimer.Expired())
{
dealer.rgbLed.Off();
players[currentDealerIndex].led.Off();
speaker.Off();
prompter.Start(250, BLINKS_PER_ROUND[currentRoundIndex], 500); // Blink and beep to prompt the dealer.
state = States::S02_WAIT_FOR_DEALER;
}
break;
case States::S02_WAIT_FOR_DEALER:
if (dealer.button.Fall())
{
currentPlayerIndex = (currentDealerIndex + 1) % NUM_PLAYERS;
prompter.Stop();
dealer.Off();
speaker.Off();
speaker.On(PLAYER_FREQUENCY);
players[currentPlayerIndex].led.On(); // Light up LED for next player.
promptTimer.Start(500);
Serial.print("Round ");
Serial.println(currentRoundIndex + 1);
state = States::S03_PROMPT_NEXT_PLAYER;
}
break;
case States::S03_PROMPT_NEXT_PLAYER:
if (promptTimer.Expired())
{
speaker.Off();
promptTimer.Start(PLAYER_TIMER);
Serial.println("Started player timer.");
state = States::S04_WAIT_FOR_PLAYERS;
}
break;
case States::S04_WAIT_FOR_PLAYERS:
if (!prompter.IsRunning() && promptTimer.Expired())
{
Serial.println("Player timer expired.");
prompter.StartIndefinitely(250);
}
Player *currentPlayer = &players[currentPlayerIndex];
if (currentPlayer->button.Fall())
{
prompter.Stop();
currentPlayer->led.Off(); // Turn off LED for current player.
speaker.Off();
currentPlayerIndex = ++currentPlayerIndex % NUM_PLAYERS; // Move to next player.
currentPlayer = &players[currentPlayerIndex];
cardsPlayed++;
Serial.print("cardsPlayed=");
Serial.println(cardsPlayed);
if (cardsPlayed >= CARDS_PLAYED_AFTER_ROUND[currentRoundIndex])
{
Serial.print("End of round ");
Serial.println(currentRoundIndex + 1);
currentRoundIndex++;
if (currentRoundIndex >= NUM_ROUNDS)
{
currentDealerIndex = ++currentDealerIndex % NUM_PLAYERS;
currentRoundIndex = 0;
cardsPlayed = 0;
}
currentRoundIndex = currentRoundIndex % NUM_ROUNDS;
speaker.On(DEALER_FREQUENCY); // Prompt the dealer.
players[currentDealerIndex].led.On();
promptTimer.Start(500);
state = States::S01_PROMPT_DEALER;
}
else
{
speaker.On(PLAYER_FREQUENCY);
currentPlayer->led.On(); // Light up LED for next player.
promptTimer.Start(500);
state = States::S03_PROMPT_NEXT_PLAYER;
}
break;
}
default:
break;
}
}