I've started to write a small Tic-Tac-Toe game. I am pretty sure everyone knows how this game works so I don't have to explain that part. I've decided to represent the grid using 2 bitsets which allows for an easy grid evaluation. I just want to hear some thoughts on this code regarding style and my solution in general. Keep in mind that this isn't a finished game yet, it's just the logic component which works as expected if you manually do all the stuff in main.
#include <algorithm>
#include <array>
#include <bitset>
#include <exception>
#include <iostream>
#include <limits>
#include <map>
#include <utility>
using Turn = std::bitset<1u>; // std::bitset for convenience
struct Grid
{
using PlayerBitset = std::bitset<9u>;
Turn turn;
PlayerBitset x;
PlayerBitset o;
};
enum class GridState
: std::uint16_t
{
VictoryX = 0x0,
VictoryO = 0x1,
Draw = 0x2,
Unspecified = 0x3
};
using PlayerBitmask = std::bitset<9u>;
constexpr std::array<PlayerBitmask, 8u> victoryMasks{ 0x007, 0x038, 0x049, 0x054, 0x092, 0x111, 0x124, 0x1c0 };
GridState CheckGrid(const Grid& grid) noexcept
{
if(grid.turn[0u]) {
for(auto& mask : victoryMasks) {
if((mask & grid.x) == mask) {
return GridState::VictoryX;
}
}
}
else {
for(auto& mask : victoryMasks) {
if((mask & grid.o) == mask) {
return GridState::VictoryO;
}
}
}
if(grid.x.count() + grid.o.count() >= 9u) {
return GridState::Draw;
}
else {
return GridState::Unspecified;
}
}
using Move = std::uint16_t;
using MoveSet = std::bitset<9u>;
MoveSet GetPossibleMoves(const Grid& grid) noexcept
{
return ~(grid.x ^ grid.o);
}
void MakeMove(const Move move, Grid& grid)
{
if(GetPossibleMoves(grid).test(move)) { // Throws std::out_of_range if move does not correspond to a valid position within the bitset
if(grid.turn[0u]) {
grid.x[move] = true;
}
else {
grid.o[move] = true;
}
grid.turn.flip();
}
else {
throw std::invalid_argument("Invalid move.");
}
}
std::int16_t AlphaBeta(const Grid& grid, std::int16_t alpha, std::int16_t beta, std::uint16_t depth) noexcept
{
switch(CheckGrid(grid)) {
case GridState::VictoryX :
{
return depth - 10u;
}
case GridState::VictoryO :
{
return 10u - depth;
}
case GridState::Draw :
{
return 0u;
}
case GridState::Unspecified :
{
auto moveSet{ GetPossibleMoves(grid) };
if(!grid.turn[0u]) {
for(auto i{ 0u }; i < moveSet.size(); ++i) {
if(moveSet[i]) {
Grid g{ grid };
MakeMove(i, g);
auto utility{ AlphaBeta(g, alpha, beta, (depth + 1u)) };
if(utility > alpha) {
alpha = utility;
}
if(alpha >= beta) {
return alpha;
}
}
}
return alpha;
} // if
else {
for(auto i{ 0u }; i < moveSet.size(); ++i) {
if(moveSet[i]) {
Grid g{ grid };
MakeMove(i, g);
auto utility{ AlphaBeta(g, alpha, beta, (depth + 1u)) };
if(utility < beta) {
beta = utility;
}
if(alpha >= beta) {
return beta;
}
}
}
return beta;
} // else
}
} // switch
}
Move GetBestMove(const Grid& grid) noexcept
{
auto moveSet{ GetPossibleMoves(grid) };
std::multimap<std::int16_t, Move> results;
for(auto i{ 0u }; i < moveSet.size(); ++i) {
if(moveSet[i]) {
Grid g{ grid };
MakeMove(i, g);
results.emplace(AlphaBeta(g, std::numeric_limits<std::int16_t>::min(), std::numeric_limits<std::int16_t>::max(), 0u), i);
}
}
return std::max_element(results.cbegin(), results.cend())->second;
}