TicTacToe avec IA en C
J'ai récemment commencé à apprendre le C, même si j'ai une certaine expérience dans d'autres langues. J'ai récemment écrit une IA TicTacToe en utilisant l'algorithme Minimax en C.Je voudrais savoir comment je pourrais écrire mieux C.
#include <stdio.h>
#define X -1
#define O -2
#define MAX_SIZE 50 //This is the Max size of the Input Buffer
int turn = O;
int board[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
int variations = 0; //To keep track of no. of variations the ai has seen
void copyBoard(int from[], int to[]){
for(int i = 0; i < 9; i++){
to[i] = from[i];
}
}
//gets line WITHOUT \n
int getl(char s[], int lim){
int c, i;
for(i = 0; i < lim-1 && (c = getchar()) != EOF && c != '\n'; i++)
s[i] = c;
s[i] = '\0';
return i;
}
//Converts char[] to int
int bufferToNum(char buffer[]){
int n = 0;
for(int i = 0; buffer[i] != '\0'; i++){
n = 10 * n + buffer[i] - '0';
}
return n;
}
//converts the board numbers to char to display
char boardToChar(int i){
int a = board[i];
if (a == X){
return 'X';
} else if (a == O){
return 'O';
} else {
return a + '0';
}
}
//prints board
void printBoard(){
printf("=============\n| %c | %c | %c |\n-------------\n| %c | %c | %c |\n-------------\n| %c | %c | %c |\n=============\n", boardToChar(0), boardToChar(1), boardToChar(2), boardToChar(3), boardToChar(4), boardToChar(5), boardToChar(6), boardToChar(7), boardToChar(8));
}
//alternates turn
void alternateTurn(){
if (turn == O){
turn = X;
} else if (turn == X){
turn = O;
}
}
//returns 1 if draw, return 0 if not a draw
int drawCheck(int l_board[]){
for(int i = 0; i < 9; i++){
if (l_board[i] == i+1){
return 0;
}
}
return 1;
}
//returns X if X won and O if O one and 0 if nobody winning
int winCheck(int l_board[]){
//Rows
for (int i = 0; i < 3; i++){
if (l_board[3*i] == l_board[3*i + 1] && l_board[3*i + 1] == l_board[3*i + 2]){
return l_board[3*i];
}
}
//Columns
for (int j = 0; j < 3; j++){
if (l_board[j] == l_board[3 + j] && l_board[3 + j] == l_board[6 + j]){
return l_board[j];
}
}
//Diagonal Top Left to Bottom Right
if (l_board[0] == l_board[4] && l_board[0] == l_board[8]){
return l_board[0];
}
//Diagonal Top Right to bottom Left
if (l_board[2] == l_board[4] && l_board[2] == l_board[6]){
return l_board[2];
}
return 0;
}
//1 if nothing is ther and 0 if something was already ther
int putInBoard(int l_board[], int pos, int newVal){
if (l_board[pos] == pos+1){
l_board[pos] = newVal;
return 1;
} else
{
return 0;
}
}
//X if X win, O if O win, 0 if draw, 1 if nothing
int gameState(int l_board[]){
int wc = winCheck(l_board);
if (wc == X){
return X;
} else if(wc == O){
return O;
} else {
if (drawCheck(l_board)){
return 0;
}
}
return 1;
}
void legalMoves(int l_board[], int output[]){
for(int i = 0; i < 9; i++){
if (l_board[i] == i+1){
output[i] = 1;
} else {
output[i] = 0;
}
}
}
int max(int a, int b){
return a>b ? a : b;
}
int min(int a, int b){
return a<b ? a : b;
}
//X is ai
int minimax(int l_board[], int depth, int maximising){
int gs = gameState(l_board);
variations++;
if (gs == X){
return 10;
} else if (gs == O){
return -10;
} else if (gs == 0){
return 0;
}
if (depth == 0){
return 0;
}
if (maximising){
//Its AI's Turn so it has to maximise
int val = -100;
int legalMovesArr[9];
legalMoves(l_board, legalMovesArr);
for (int i = 0; i < 9; i++){
if (legalMovesArr[i]){
int tempBoard[9];
copyBoard(l_board, tempBoard);
putInBoard(tempBoard, i, X);
val = max(minimax(tempBoard, depth-1, 0), val);
}
}
return val;
} else {
int val = 100;
int legalMovesArr[9];
legalMoves(l_board, legalMovesArr);
for (int i = 0; i < 9; i++){
if (legalMovesArr[i]){
int tempBoard[9];
copyBoard(l_board, tempBoard);
putInBoard(tempBoard, i, O);
val = min(minimax(tempBoard, depth-1, 1), val);
}
}
return val;
}
}
int ai(int l_board[], int depth){
int legalMovesArr[9];
legalMoves(board, legalMovesArr);
int val = -100;
int best_move = 0;
for (int i = 0; i < 9; i++){
if (legalMovesArr[i]){
int tempBoard[9];
copyBoard(l_board, tempBoard);
putInBoard(tempBoard, i, X);
int temp = minimax(tempBoard, depth-1, 0);
if (val <= temp){
val = temp;
best_move = i;
}
}
}
return best_move;
}
int main(){
printBoard();
int gameOn = 0;
char buffer[MAX_SIZE];
while(!gameOn){
if (turn == O){
printf("%c's turn: ", turn == X ? 'X' : 'O');
getl(buffer, MAX_SIZE);
int num = bufferToNum(buffer);
while (num <= 0 || num > 9){
printf("Please enter an integer between 1 and 9: ");
getl(buffer, MAX_SIZE);
num = bufferToNum(buffer);
}
if (putInBoard(board, num-1, turn)){
;
} else {
while(!putInBoard(board, num-1, turn)){
printf("Something already exists, Please enter a new number: ");
getl(buffer, MAX_SIZE);
num = bufferToNum(buffer);
}
}
} else {
putInBoard(board, ai(board, 8), X);
printf("Calculated %d variations\n", variations);
variations = 0;
}
printBoard();
alternateTurn();
int gs = gameState(board);
if (gs == X){
printf("X won!");
return 0;
} else if (gs == O){
printf("O won!");
return 0;
} else if (gs == 0){
printf("Draw!");
return 0;
}
}
return 0;
}
Réponses
Observations générales
Je vois déjà beaucoup de bonnes pratiques de programmation suivies ici, continuez votre bon travail.
La plupart des fonctions sont petites et suivent le principe de responsabilité unique.
Utilisez les fonctions de la bibliothèque lorsque vous le pouvez
La fonction getl()
peut être implémentée à l'aide de 2 fonctions standard de la bibliothèque C, fgets et strrchr .
La fgets()
fonction entre une ligne entière de caractères à la fois, plutôt que d'utiliser la saisie de caractères, et strrchr()
vous permettra de trouver le \n
caractère et de le remplacer.
Code DRY
Il existe un principe de programmation appelé le principe de ne pas se répéter, parfois appelé code DRY. Si vous vous retrouvez à répéter le même code plusieurs fois, il est préférable de l'encapsuler dans une fonction. S'il est possible de parcourir le code en boucle, cela peut également réduire la répétition.
La répétition se produit dans la fonction int minimax(int l_board[], int depth, int maximising)
:
if (maximising) {
//Its AI's Turn so it has to maximise
int val = -100;
int legalMovesArr[9];
legalMoves(l_board, legalMovesArr);
for (int i = 0; i < 9; i++) {
if (legalMovesArr[i]) {
int tempBoard[9];
copyBoard(l_board, tempBoard);
putInBoard(tempBoard, i, X);
val = max(minimax(tempBoard, depth - 1, 0), val);
}
}
return val;
}
else {
int val = 100;
int legalMovesArr[9];
legalMoves(l_board, legalMovesArr);
for (int i = 0; i < 9; i++) {
if (legalMovesArr[i]) {
int tempBoard[9];
copyBoard(l_board, tempBoard);
putInBoard(tempBoard, i, O);
val = min(minimax(tempBoard, depth - 1, 1), val);
}
}
Ce code répétitif pourrait être sa propre fonction.
Le code répétitif rend également la fonction trop complexe.
Complexité
La complexité de la fonction a minimax()
été mentionnée ci-dessus, la fonction main()
est également trop complexe (en fait trop). Au fur et à mesure que la taille des programmes augmente, l'utilisation de main()
devrait être limitée à l'appel des fonctions qui analysent la ligne de commande, à l'appel des fonctions qui sont configurées pour le traitement, à l'appel des fonctions qui exécutent la fonction souhaitée du programme et à l'appel des fonctions à nettoyer après la partie principale de le programme.
Il existe également un principe de programmation appelé principe de responsabilité unique qui s'applique ici. Le principe de responsabilité unique stipule:
que chaque module, classe ou fonction devrait avoir la responsabilité d'une seule partie de la fonctionnalité fournie par le logiciel, et que cette responsabilité devrait être entièrement encapsulée par ce module, cette classe ou cette fonction.
La while (!gameOn)
boucle entière doit être dans sa propre fonction.
Évitez les variables globales
Il est très difficile de lire, d'écrire, de déboguer et de maintenir des programmes qui utilisent des variables globales. Les variables globales peuvent être modifiées par n'importe quelle fonction du programme et nécessitent donc que chaque fonction soit examinée avant d'apporter des modifications au code. En C et C ++, les variables globales ont un impact sur l'espace de noms et peuvent provoquer des erreurs de liaison si elles sont définies dans plusieurs fichiers. Les réponses à cette question stackoverflow fournissent une explication plus complète.
Mettre à jour la réponse pour commenter
void game_loops()
{
if (turn == O) {
printf("%c's turn: ", turn == X ? 'X' : 'O');
char buffer[MAX_SIZE];
getl(buffer, MAX_SIZE);
int num = bufferToNum(buffer);
while (num <= 0 || num > 9) {
printf("Please enter an integer between 1 and 9: ");
getl(buffer, MAX_SIZE);
num = bufferToNum(buffer);
}
if (putInBoard(board, num - 1, turn)) {
;
}
else {
while (!putInBoard(board, num - 1, turn)) {
printf("Something already exists, Please enter a new number: ");
getl(buffer, MAX_SIZE);
num = bufferToNum(buffer);
}
}
}
else {
putInBoard(board, ai(board, 8), X);
printf("Calculated %d variations\n", variations);
variations = 0;
}
printBoard();
alternateTurn();
}
int main() {
printBoard();
int gs = 1;
while (gs == 1) {
game_loops();
gs = gameState(board);
}
switch (gs)
{
case X:
printf("X won!");
break;
case O:
printf("O won!");
break;
default:
printf("Draw!");
break;
}
return 0;
}
Petit examen
Brisez cette longue ligne de code.
//prints board
void printBoard(){
printf("=============\n| %c | %c | %c |\n-------------\n| %c | %c | %c |\n-------------\n| %c | %c | %c |\n=============\n", boardToChar(0), boardToChar(1), boardToChar(2), boardToChar(3), boardToChar(4), boardToChar(5), boardToChar(6), boardToChar(7), boardToChar(8));
}
Peut-être comme
//prints board
void printBoard() {
printf("=============\n"
"| %c | %c | %c |\n"
"-------------\n"
"| %c | %c | %c |\n"
"-------------\n"
"| %c | %c | %c |\n"
"=============\n", //
boardToChar(0), boardToChar(1), boardToChar(2), //
boardToChar(3), boardToChar(4), boardToChar(5), //
boardToChar(6), boardToChar(7), boardToChar(8));
}
Pour moi, la //
fin de 3 lignes empêche le formatage automatique de ces lignes en 1 ou 2 lignes.