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 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;
state = LOW;
timer.Start(toggleTimespan);
running = true;
}
void Stop()
{
running = false;
currentToggles = 0;
}
void Update()
{
if (running && timer.Expired())
{
currentToggles++;
if (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;
}
};
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);
digitalWrite(bluePin, LOW);
}
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;
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 Beeper
{
private:
byte pin;
public:
Beeper(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(A5, 50, 10, 11, 12);
const byte NUM_PLAYERS = 4;
// The players.
Player players[NUM_PLAYERS] =
{
// Button pin, debounce delay, LED pin.
Player(6, 50, 2),
Player(7, 50, 3),
Player(8, 50, 4),
Player(9, 50, 5)
};
// Beeper to prompt next action.
const byte SPEAKER_PIN = A3;
Beeper beeper(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 = 3;
const byte BLINKS_PER_ROUND[NUM_ROUNDS] = {3, 4, 6};
// These values are cumulative, i.e. not cards per round.
// const byte CARDS_PLAYED_AFTER_ROUND[NUM_ROUNDS] = { 18, 28, 52 }; // For game play.
const byte CARDS_PLAYED_AFTER_ROUND[NUM_ROUNDS] = {4, 8, 12}; // For testing.
const uint16_t DEALER_FREQUENCY = 1000;
const uint16_t PLAYER_FREQUENCY = 1500;
const uint16_t CARD_FREQUENCY = 500;
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)
{
beeper.On(500);
dealer.On(currentDealerIndex);
}
else
{
beeper.Off();
dealer.Off();
}
}
}
Prompter prompter(PrompterCallback);
void setup()
{
Serial.begin(115200);
// Initialise.
state = States::S00_WAIT_FOR_FIRST_DEALER;
dealer.Begin();
for (auto &player : players)
{
player.Begin();
}
beeper.Begin();
prompter.Start(250, 65000, 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 beeper for 500 ms.
beeper.On(DEALER_FREQUENCY); // Dealer Alert Deal Last Card Down. 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();
beeper.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(); // Turn off the RGB LED here.
beeper.Off();
beeper.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())
{
beeper.Off();
state = States::S04_WAIT_FOR_PLAYERS;
}
break;
case States::S04_WAIT_FOR_PLAYERS:
Player *currentPlayer = &players[currentPlayerIndex];
if (currentPlayer->button.Fall())
{
currentPlayer->led.Off(); // Turn off LED for current player.
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;
beeper.On(DEALER_FREQUENCY); // Prompt the dealer.
players[currentDealerIndex].led.On();
promptTimer.Start(500);
state = States::S01_PROMPT_DEALER;
}
else
{
currentPlayer->led.On(); // Light up LED for next player.
}
}
break;
default:
break;
}
}