This machine learning Tic Tac Toe game project done in my first year trimester 1 of my university days.
Language Used:
C
Software Used: raylib
Role: Logistic Regression ML AI
As part of my Programming Methodology module in year 1, our group embarked on our journey to develop a tic tac toe game.
To make the AI stand out (and to grasp more marks), we decided to include a Machine Learning AI as part of the difficulty levels in the singleplayer mode of this game.
The first challenge I had encountered was selecting the correct model to use for the AI.
After some research, I chose the Logistic Regression model to ensure that the AI was sufficiently easy to beat.
(220/575 * 100 ≈ 38%)

The next issue was handling the data and transforming it into something readable by the learning model.

Tic Tac Toe Dataset
I did some digging around and found the idea of converting the data into an integer array, representing the current board’s gamestate.These gamestates could then be assigned to a value to influence the model’s next decision as “the next best choice”.

After which, I started to train the model to generate its weights so that the Logistic Model could be a medium leveled opponent for the player.

After training the model, a test was done to see the accuracy of the trained weights.

Finally I concluded the AI by writing its behaviour when it is playing its turn so that the rest of the team could merge this AI more easily into the main game:
// The file that communicates with the game system on the next move the AI is going to make.
#include <stdio.h>
#include "PrepareData/prepare_data.h"
#include "TrainModel/train_model.h"
#include "training_settings.h"
#pragma region === Defitinition & Declaration ===
// Determines if this ML AI is initialised or not. 1 = true, 0 = false
static int _isInitialised = 0;
// Initializes the machine learning ai values and model. Will retrain model if model is missing.
void Init_MachineLearning_AI();
// Pass in current gamestate and returns chosen tile via pointers
void Decide_Next_Move(int gridSize, int currentBoard[gridSize][gridSize], int *x, int *y);
#pragma endregion
void Init_MachineLearning_AI()
{
// Machine learning ai already initialised, return code flow
if (_isInitialised == 1)
return;
_isInitialised = 1;
// Initialise data prep
// we will use the symbol from the settings ('O' in this case)
Init_Data_Prep(SETTINGS_NUM_OF_DATALINES, SETTINGS_DATAFILE_PATH,SETTINGS_SYMBOL_THAT_AI_USE);
// Initialise Model Trainer and check for any missing values (1 means nothing wrong)
if (Init_Model_Trainer(SETTINGS_NUM_OF_DATALINES, GAMESTATE_AS_INT_SIZE, SETTINGS_LEARNING_RATE, SETTINGS_ITERATIONS, SETTINGS_TRAINED_MODEL_FILEPATH) == 1)
return;
//--- Values missing from Model Trainer ---
TicTacData *trainingData_pointer = Read_Data(); // Grab data from data file
FlatTicTacData flatData = Flatten_TicTac_Data(trainingData_pointer, SETTINGS_NUM_OF_DATALINES); // Make data usable for training model
Retrain_Model(flatData.allGameStateAsInt_pointer, flatData.allIsPositive_pointer); // Retrain model
free(trainingData_pointer); // Free memory just incase
}
void Decide_Next_Move(int gridSize, int currentBoard[gridSize][gridSize], int *x, int *y)
{
int row, col, i;
int bestMoveToMake = -1;
int originalSetOfThree[3] = {0, 0, 0};
double bestProbability = -1, tempProbability;
int flatCurrBoardData[GAMESTATE_AS_INT_SIZE] = {}; // holds the current board data in model trainable form
#pragma region Flatten currentBoard Data
// Using the knowledge that 0 = blank, 1 = cross and 2 = circle,
// we can convert this data into the data needed for the model prediction
for (row = 0; row < gridSize; row += 1)
{
for (col = 0; col < gridSize; col += 1)
{
// printf("Grid[%d][%d] = %d\n", row, col, currentBoard[row][col]); //debug
i = (row * gridSize + col) * 3; // To get the correct element index in the flattened array data
switch (currentBoard[row][col])
{
case 0: // when we see currentBoard[row][col] = 0 (blank), we assign a value of 0,0,1 to our flattened data array
flatCurrBoardData[i] = 0;
flatCurrBoardData[i + 1] = 0;
flatCurrBoardData[i + 2] = 1;
break;
case 1:
// when we see currentBoard[row][col] = 1 (cross), we assign a value of 1,0,0 to our flattened data array
flatCurrBoardData[i] = 1;
flatCurrBoardData[i + 1] = 0;
flatCurrBoardData[i + 2] = 0;
break;
case 2:
// when we see currentBoard[row][col] = 2 (circle), we assign a value of 0,1,0 to our flattened data array
flatCurrBoardData[i] = 0;
flatCurrBoardData[i + 1] = 1;
flatCurrBoardData[i + 2] = 0;
break;
default:
printf("Unrecognised case: %d !\n", currentBoard[row][col]);
break;
}
}
}
#pragma endregion
#pragma region Deciding the Best Move to Make
// Loop thru all sets of 3 in the current gamestate
for (i = 0; i < GAMESTATE_AS_INT_SIZE; i += 3)
{
// Since i increments by 3 every loop, i will be the index of the first set of 3
// Check if current cell is blank (representation of blank is 001)
if (flatCurrBoardData[i] != 0 || flatCurrBoardData[i + 1] != 0 || flatCurrBoardData[i + 2] != 1)
{
continue;
}
// Record the discovered blank cell
originalSetOfThree[0] = flatCurrBoardData[i];
originalSetOfThree[1] = flatCurrBoardData[i + 1];
originalSetOfThree[2] = flatCurrBoardData[i + 2];
// --- Create a scenario where the current blank cell in the loop is chosen ---
// Change the set of 3 starting from 'i' into the symbol which the AI uses which is O, which is represented by 0,1,0
flatCurrBoardData[i] = 0;
flatCurrBoardData[i + 1] = 1;
flatCurrBoardData[i + 2] = 0;
// --- Predict using the new gamestate to see if it is the best chance of winning ---
// Call Predict() and compare the probability returned if it is higher than the current one
tempProbability = Predict(flatCurrBoardData);
// if the new prediction probability of winning is better than the previous one,
if (tempProbability > bestProbability)
{
// set that as the best probability to win
bestProbability = tempProbability;
// Record the current best gamestate to move
bestMoveToMake = i;
}
// Revert the board to its original scenario
flatCurrBoardData[i] = originalSetOfThree[0];
flatCurrBoardData[i + 1] = originalSetOfThree[1];
flatCurrBoardData[i + 2] = originalSetOfThree[2];
}
#pragma endregion
// reverse the equation i = (row * gridSize + col) * 3 to find the cell number
bestMoveToMake /= 3;
*y = bestMoveToMake % gridSize; // col index = remainder of cell index / gridSize (in this case gridSize = 3)
*x = (bestMoveToMake - *y) / gridSize; // row index = (cell index - remainder) gridSize
}
As this is my first time handling a machine learning task, the machine learning model may or may not be as accurate as the graph above had predicted.
However, this had led me to learn a great deal (althought I know it is only a small fraction) of what data scientists go through.
As such I am very much grateful to have a team dedicated to making this game together with me. To play or see the game code, click here