#include <Streaming.h>
#include <Keypad.h>
#include <LiquidCrystal_I2C.h>
Print &cout {Serial};
constexpr uint8_t ROWS = 3;
constexpr uint8_t COLS = 3;
constexpr char buttons[ROWS][COLS] = {
{'0', '1', '2'},
{'3', '4', '5'},
{'6', '7', '8'}
};
uint8_t rowPins[ROWS] = {9, 8, 7};
uint8_t colPins[COLS] = {6, 5, 4};
constexpr uint8_t BOARD_COLS {3};
constexpr uint8_t BOARD_ROWS {BOARD_COLS};
constexpr uint8_t BOARD_SIZE {(BOARD_COLS + 2) * (BOARD_ROWS + 2)};
constexpr uint8_t BOARD_OFFSET {BOARD_COLS + 2};
// Index - Offsets to test whether three identical characters are in a row
// CHECK ROW, COL DIAG. RIGHT DIAG. LEFT
constexpr uint8_t DIRECTIONS[] {BOARD_OFFSET, 1, BOARD_OFFSET + 1, BOARD_OFFSET - 1};
constexpr char BD {'@'}; // Value: border of playing field
constexpr char FD {' '}; // Value: playing field
constexpr char BOARD_INIT[BOARD_SIZE] {BD, BD, BD, BD, BD, BD, FD, FD, FD, BD, BD, FD, FD,
FD, BD, BD, FD, FD, FD, BD, BD, BD, BD, BD, BD};
char board[BOARD_SIZE]; // Playground
LiquidCrystal_I2C lcd(0x27, 20, 4); // I2C_ADDR, LCD_COLUMNS, LCD_LINES
Print &lcdOut {lcd};
Keypad keypad = Keypad(makeKeymap(buttons), rowPins, colPins, ROWS, COLS);
//////////////////////////////////////////////////////////////////////////////
/// BDbrief Sets the playing field to its initial values
///
/// BDtparam (&brd)[N] The playing field as an array
//////////////////////////////////////////////////////////////////////////////
template <size_t N> void resetBoard(char (&brd)[N]) { memcpy(brd, BOARD_INIT, N); }
//////////////////////////////////////////////////////////////////////////////
/// @brief Display of the playing field (serial console)
///
/// @tparam BDtparam (&brd)[N] The playing field as an array
//////////////////////////////////////////////////////////////////////////////
template <size_t N> void showBoard(char (&brd)[N]) {
for (uint8_t i = BOARD_OFFSET; i < (N - BOARD_OFFSET); ++i) {
if (brd[i] != BD) {
cout << _FMT("[%,%,%]\n", brd[i], brd[i + 1], brd[i + 2]);
i += 4;
}
}
}
//////////////////////////////////////////////////////////////////////////////
/// @brief Display of the playing field (LCD/TFT)
///
/// @param disp Reference to display object
/// @tparam char (&brd)[N] The playing field as an array
//////////////////////////////////////////////////////////////////////////////
template <size_t N> void showBoard(LiquidCrystal_I2C &disp, char (&brd)[N]) {
Print &lcdOut {disp};
uint8_t row {0};
for (uint8_t i = BOARD_OFFSET; i < (N - BOARD_OFFSET); ++i) {
if (brd[i] != BD) {
disp.setCursor(6, row);
lcdOut << _FMT("[%,%,%]", brd[i], brd[i + 1], brd[i + 2]);
i += 4;
++row;
}
}
}
//////////////////////////////////////////////////////////////////////////////
/// @brief Show which player has the turn
///
/// @param disp Reference to display object
/// @param plChar Character of the player
/// @param plIdx Index represents the Playernumber - 1
//////////////////////////////////////////////////////////////////////////////
void showPlayer(LiquidCrystal_I2C &disp, const char plChar[], uint8_t plIdx) {
Print &lcdOut {disp};
disp.setCursor(0, 0);
lcdOut << "P:" << plIdx+1;
disp.setCursor(0, 1);
lcdOut << F("(") << plChar[plIdx] << F(")");
}
//////////////////////////////////////////////////////////////////////////////
/// @brief Show the winner
///
/// @param disp Reference to display object
/// @param plIdx Index represents the Playernumber - 1
//////////////////////////////////////////////////////////////////////////////
void showWinner(LiquidCrystal_I2C &disp, uint8_t plIdx) {
Print &lcdOut {disp};
disp.setCursor(1,3);
if (plIdx > 1) {
lcdOut << F("No one has won %-)");
} else {
lcdOut << F("Player: ") << plIdx + 1 << F(" has won!");
}
}
//////////////////////////////////////////////////////////////////////////////
/// @brief Deletes the fourth line of the display (status display)
///
/// @param disp Reference to display object
//////////////////////////////////////////////////////////////////////////////
void clearStatusRow(LiquidCrystal_I2C &disp) {
Print &lcdOut {disp};
disp.setCursor(0,3);
lcdOut << _PAD(20,' '); // clear fourth row
}
//////////////////////////////////////////////////////////////////////////////
/// @brief Conversion from the simple offset (buttons) in the array
/// to the array from the playing field
///
/// @param pos
/// @return uint8_t
//////////////////////////////////////////////////////////////////////////////
uint8_t calcBoardOffset(uint8_t pos) {
uint8_t col = pos % BOARD_COLS;
uint8_t row = pos / BOARD_ROWS;
return (BOARD_OFFSET + 1) + (row * BOARD_OFFSET) + col;
}
//////////////////////////////////////////////////////////////////////////////
/// @brief Implementation of a move. The player value is placed on the
/// corresponding field of the playing field (array).
///
/// @param brd Array representing the playing field
/// @param pos Position on the playing field (index 0 - 9)
/// @param value Player value (-1 = player 1 or 1 = player 2)
/// @return true Value was set
/// @return false Value was not set
//////////////////////////////////////////////////////////////////////////////
bool move(char brd[], uint8_t pos, const char value) {
bool isValueAssigned {false};
if (pos > (BOARD_COLS * BOARD_ROWS - 1)) {
cout << F("ERROR!\n"); // Index out of bounds
return isValueAssigned;
}
uint8_t idx = calcBoardOffset(pos);
// Only set player value if this field has not yet received one
if (brd[idx] == FD) {
brd[idx] = value;
isValueAssigned = true;
}
return isValueAssigned;
}
//////////////////////////////////////////////////////////////////////////////
/// @brief Check whether there are three identical, consecutive values
/// horizontally, vertically or diagonally on the playing field.
/// https://www.youtube.com/watch?v=e5lKoL9G0N4
///
/// @param brd Array representing the playing field
/// @param idx Index of the last player value entered on the playing field
/// @return true identical Values
/// @return false no identical values
//////////////////////////////////////////////////////////////////////////////
bool isRowComplete(char brd[], uint8_t idx) {
char compare = brd[idx];
uint8_t sum {1};
for (auto &dirIdx : DIRECTIONS) { // Get the index of the corresponding direction for comparison
uint8_t sIdx = idx + dirIdx;
sum = 1;
while (brd[sIdx] == compare) { // Compare to the right
sIdx += dirIdx;
sum += 1;
}
sIdx = idx - dirIdx;
while (brd[sIdx] == compare) { // Compare to the left
sIdx -= dirIdx;
sum += 1;
}
if (sum == 3) { return true; }
}
return false;
}
//////////////////////////////////////////////////////////////////////////////
/// @brief Check whether a button has been pressed
///
/// @tparam keypad Key matrix object
/// @return int8_t value (0-8) of the button pressed or -1 if none was pressed.
//////////////////////////////////////////////////////////////////////////////
int8_t checkButtons(Keypad &kp) {
char key = kp.getKey();
return (key) ? key-0x30 : -1;
}
void setup() {
Serial.begin(115200);
lcd.init();
lcd.backlight();
resetBoard(board);
// cout << F("Player: 1 (X) has the turn\n");
showPlayer(lcd,"X",0);
showBoard(lcd, board);
}
void loop() {
static bool restart {false};
static char player[2] {'X', 'O'};
static uint8_t pCounter[2] {0};
static uint8_t pIdx {0};
int8_t bVal = checkButtons(keypad);
if (bVal > (-1)) {
if (move(board, bVal, player[pIdx]) == true) {
pCounter[pIdx]++;
if (isRowComplete(board, calcBoardOffset(bVal))) { // Three in a row?
// cout << F("\nPlayer: ") << pIdx + 1 << F(" has won!\n");
showWinner(lcd,pIdx);
showBoard(lcd,board);
restart = true;
} else if (pCounter[0] + pCounter[1] == (BOARD_ROWS * BOARD_COLS)) { // no and no remaining fields
showWinner(lcd,2); // Index 2 (as marker) if no one has won.
// cout << F("\nNo one has won\n");
showBoard(lcd,board);
restart = true;
}
if (restart == true) {
pCounter[0] = 0;
pCounter[1] = 0;
resetBoard(board);
restart = false;
delay(5000);
clearStatusRow(lcd);
}
pIdx = !pIdx; // Swap Player
// cout << F("\nPlayer: ") << pIdx + 1 << F(" (") << player[pIdx] << F(") has the turn\n");
showBoard(lcd,board);
showPlayer(lcd,player,pIdx);
}
}
}