You haven’t, in fact you’ve ended up pre-empting some of my suggestions slightly :P
uint8_t
is technically the same as unsigned char
on the Arduboy, but they have slightly different meanings semantically.
The uint8_t
type isn’t present when compiling for processors that can’t handle an 8 bit value (it’s extremely rare, but such processors do exist).
I prefer to use uint8_t
because it signals that “this code is expecting to use an 8-bit type and might not work for other types”.
If you use unsigned char
, I’d take it to mean “this type expects something at least 8 bits wide, but will work with larger types too”, because the definition of unsigned char
as per the C++ standard is ‘at least 8 bits’.
I assumed you were either planning to add one or intentially left it out for simplicity.
I wrote most of this before your recent edits, so if you’ve already made some of these changes then don’t worry.
Don’t be afraid to question some of my suggestions if you aren’t sure why I’ve suggested or think there’s an alternative that will work just as well or even better.
I usually have a reason for doing something a particular way that isn’t always immediately obvious,
but sometimes I just do things out of preference.
My fifth bit of advice is to use ‘scoped enumerations’ (a.k.a. enum class
es).
Instead of having an unspoken rule that 0 is ‘none’, 1 is ‘cross’ and 2 is ‘nought’, you can make that rule explicit in your code.
enum class Cell : uint8_t
{
None,
Cross,
Nought,
};
(The : uint8_t
bit forces it to be 8 bits in size.)
Then you can define your board as:
Cell board[3][3] =
{
{ Cell::None, Cell::None, Cell::None, },
{ Cell::None, Cell::None, Cell::None, },
{ Cell::None, Cell::None, Cell::None, },
};
Which means instead of doing if(board[selectx][selecty] == 0)
you then do if(board[selectx][selecty] == Cell::None)
and instead of doing board[selectx][selecty] = player
you’d do something like: board[selectx][selecty] = (player == 1) ? Cell::Cross : Cell::Nought
.
And then you could take it a step further and combine that with @drummyfish’s code to not only detect a win, but detect who won:
Cell calculateWinner(void)
{
// Check rows
for (uint8_t i = 0; i < 3; ++i)
{
Cell cell = board[0][i];
if((cell != Cell::None) && (cell == board[1][i]) && (cell == board[2][i]))
return cell;
}
// Check columns
for (uint8_t i = 0; i < 3; ++i)
{
Cell cell = board[i][0];
if((cell != Cell::None) && (cell == board[i][1]) && (cell == board[i][2]))
return cell;
}
Cell cell = board[1][1];
if(cell != Cell::None)
{
// Check diagonal 1
if((cell == board[0][0]) && (cell == board[2][2]))
return cell;
// Check diagonal 2
if((cell == board[2][0]) && (cell == board[0][2]))
return cell;
}
return Cell::None;
}
Note that I’ve also cached the values so the code doesn’t have to do keep doing the same array lookup,
and I’ve taken the transitivity a step further by caching the middle cell for evaluating the diagonals.
Using this you can do:
Cell winner = calculateWinner();
// By passing the winner to 'win', you can alter the win message
if(winner != Cell::None)
win(winner);
And my final bit of advice - avoid delay
like the plague.
Instead, prefer to structure your code like a state machine.
The simplest way to do that is an enum class
and a switch statement, e.g. have:
enum class GameState : uint8_t
{
Reset,
Play,
Win,
Tie,
};
GameState currentState; = GameState::Reset;
void changeState(GameState state)
{
currentState = state;
}
And then in loop
you can:
void loop(void)
{
if(!arduboy.nextFrame())
return;
arduboy.pollButtons();
arduboy.clear();
switch(currentState)
{
case GameState::Reset:
updateResetState();
break;
case GameState::Play:
updatePlayState();
break;
case GameState::Win:
updateWinState();
break;
case GameState::Tie:
updateTieState();
break;
}
arduboy.display();
}
Incorporating these suggesntions should mean that your whole code ends up looking roughly like this:
#include <Arduboy2.h>
uint8_t selectx = 1;
uint8_t selecty = 1;
uint8_t player = 1;
uint8_t turn = 1;
enum class GameState : uint8_t
{
Reset,
Play,
Win,
Tie,
};
GameState currentState; = GameState::Reset;
void changeState(GameState state)
{
currentState = state;
}
Cell winner;
void win(Cell winnerCell)
{
winner = winnerCell;
changeState(GameState::Win);
}
void tie(void)
{
changeState(GameState::Tie);
}
void loop(void)
{
if(!arduboy.nextFrame())
return;
arduboy.pollButtons();
arduboy.clear();
switch(currentState)
{
case GameState::Reset:
updateResetState();
break;
case GameState::Play:
updatePlayState();
break;
case GameState::Win:
updateWinState();
break;
case GameState::Tie:
updateTieState();
break;
}
arduboy.display();
}
void clearBoard(void)
{
for(uint8_t x = 0; x < 3; ++x)
for(uint8_t y = 0; y < 3; ++y)
board[x][y] = Cell::None;
}
void drawGrid(void)
{
arduboy.fillRect(56, 8, 1, 48, WHITE);
arduboy.fillRect(72, 8, 1, 48, WHITE);
arduboy.fillRect(40, 24, 48, 1, WHITE);
arduboy.fillRect(40, 40, 48, 1, WHITE);
}
void drawBoard(void)
{
for(uint8_t x = 0; x < 3; ++x)
for(uint8_t y = 0; y < 3; ++y)
{
const char * sprite = nullptr;
if(board[x][y] == Cell::Cross)
sprite = crossSprite;
else if(board[x][y] == Cell::Nought)
sprite = noughtSprite;
arduboy.drawBitmap(40 + (x * 16), 16 + (y * 16), sprite, 16, 16);
}
}
void updateResetState(void)
{
if(arduboy.justPressed(A_BUTTON))
changeState(GameState::Play);
selectx = 1;
selecty = 1;
player = 1;
turn = 1;
winner = Cell::None;
clearBoard();
drawGrid();
arduboy.print(F("Press A to play"));
}
void updateTieState(void)
{
if(arduboy.justPressed(A_BUTTON))
changeState(GameState::Reset);
arduboy.setCursor(48, 57);
arduboy.print(F("Tie!!!"));
}
void updateWinState(void)
{
if(arduboy.justPressed(A_BUTTON))
changeState(GameState::Reset);
arduboy.setCursor(36, 57);
arduboy.print((winner == Cell::Cross) ? F("Crosses") : F("Noughts"));
arduboy.print(F(" Wins!!!"));
}
void updatePlayState(void)
{
if(arduboy.justPressed(LEFT_BUTTON))
if(selectx > 0)
--selectx;
else
selectx = 2;
if(arduboy.justPressed(RIGHT_BUTTON))
if(selectx < 2)
++selectx;
else
selectx = 0;
if(arduboy.justPressed(UP_BUTTON))
if(selecty > 0)
--selecty;
else
selecty = 2;
if(arduboy.justPressed(DOWN_BUTTON))
if(selecty < 2)
++selecty;
else
selecty = 0;
arduboy.print(F("Player "));
arduboy.print(static_cast<int>(player));
if(arduboy.justPressed(A_BUTTON))
if(board[selectx][selecty] == Cell::None)
{
board[selectx][selecty] = ((player == 1) ? Cell::Cross : Cell::Nought);
++turn;
player = ((player == 1) ? 2 : 1);
}
drawBoard();
drawGrid();
Cell winnerCell = calculateWinner();
Cell winner = calculateWinner();
if(winner != Cell::None)
win(winner);
if(turn > 9)
tie();
}
Cell calculateWinner(void)
{
// Check rows
for (unsigned char i = 0; i < 3; ++i)
{
Cell cell = board[0][i];
if((cell != Cell::None) && (cell == board[1][i]) && (cell == board[2][i]))
return cell;
}
// Check columns
for (unsigned char i = 0; i < 3; ++i)
{
Cell cell = board[i][0];
if((cell != Cell::None) && (cell == board[i][1]) && (cell == board[i][2]))
return cell;
}
Cell cell = board[1][1];
if(cell != Cell::None)
{
// Check diagonal 1
if((cell == board[0][0]) && (cell == board[2][2]))
return cell;
// Check diagonal 2
if((cell == board[2][0]) && (cell == board[0][2]))
return cell;
}
return Cell::None;
}