TicTacToe พร้อม AI ใน C
ฉันเพิ่งเริ่มเรียนภาษา C แม้ว่าฉันจะมีประสบการณ์ในภาษาอื่นบ้าง ฉันเพิ่งเขียน TicTacToe AI โดยใช้ Minimax Algorithm ใน C ฉันอยากรู้ว่าฉันจะเขียน 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;
}
คำตอบ
ข้อสังเกตทั่วไป
ฉันเห็นแนวทางปฏิบัติในการเขียนโปรแกรมที่ดีมากมายตามมาที่นี่แล้วติดตามผลงานที่ดีต่อไป
หน้าที่ส่วนใหญ่มีขนาดเล็กและเป็นไปตามหลักการความรับผิดชอบเดียว
ใช้ฟังก์ชันไลบรารีเมื่อทำได้
ฟังก์ชั่นgetl()
ที่สามารถดำเนินการโดยใช้ 2 มาตรฐานการทำงานห้องสมุด C, fgetsและstrrchr
fgets()
ฟังก์ชั่นปัจจัยการผลิตสายทั้งหมดของตัวละครที่เวลามากกว่าการใช้ป้อนตัวอักษรและstrrchr()
จะช่วยให้คุณพบว่า\n
ตัวอักษรและแทนที่
รหัสแห้ง
มีหลักการเขียนโปรแกรมที่เรียกว่าDon't Repeat Yourself Principleบางครั้งเรียกว่า DRY code หากคุณพบว่าตัวเองใช้รหัสเดิมซ้ำหลายครั้งควรห่อหุ้มไว้ในฟังก์ชันจะดีกว่า หากเป็นไปได้ที่จะวนซ้ำโค้ดที่สามารถลดการทำซ้ำได้เช่นกัน
การทำซ้ำเกิดขึ้นในฟังก์ชัน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);
}
}
รหัสซ้ำนี้อาจเป็นฟังก์ชันของตัวเอง
รหัสซ้ำยังทำให้ฟังก์ชันซับซ้อนเกินไป
ความซับซ้อน
ความซับซ้อนของฟังก์ชั่นminimax()
ดังกล่าวข้างต้นฟังก์ชั่นmain()
นั้นซับซ้อนเกินไป (ทำมากเกินไป) เนื่องจากโปรแกรมมีขนาดใหญ่ขึ้นการใช้งานmain()
ควร จำกัด เฉพาะฟังก์ชันการโทรที่แยกวิเคราะห์บรรทัดคำสั่งการเรียกฟังก์ชันที่ตั้งค่าสำหรับการประมวลผลการเรียกฟังก์ชันที่เรียกใช้ฟังก์ชันที่ต้องการของโปรแกรมและการเรียกฟังก์ชันเพื่อล้างข้อมูลหลังจากส่วนหลักของ โปรแกรม.
นอกจากนี้ยังมีหลักการเขียนโปรแกรมที่เรียกว่าหลักการความรับผิดชอบเดียวที่ใช้ที่นี่ เดี่ยวรับผิดชอบหลักการฯ :
ทุกโมดูลคลาสหรือฟังก์ชันควรมีความรับผิดชอบในส่วนเดียวของฟังก์ชันที่ซอฟต์แวร์จัดหาให้และความรับผิดชอบนั้นควรถูกห่อหุ้มไว้ทั้งหมดโดยโมดูลคลาสหรือฟังก์ชันนั้น
while (!gameOn)
ลูปทั้งหมดควรอยู่ในฟังก์ชันของตัวเอง
หลีกเลี่ยงตัวแปรร่วม
เป็นเรื่องยากมากที่จะอ่านเขียนแก้จุดบกพร่องและบำรุงรักษาโปรแกรมที่ใช้ตัวแปรส่วนกลาง ตัวแปรส่วนกลางสามารถแก้ไขได้โดยฟังก์ชันใด ๆ ภายในโปรแกรมดังนั้นจึงต้องมีการตรวจสอบแต่ละฟังก์ชันก่อนทำการเปลี่ยนแปลงในโค้ด ในตัวแปรโกลบอล C และ C ++ ส่งผลกระทบต่อเนมสเปซและอาจทำให้เกิดข้อผิดพลาดในการเชื่อมโยงหากกำหนดไว้ในหลายไฟล์ คำตอบในคำถาม StackOverflow นี้ให้คำอธิบายฟูลเลอร์
อัปเดตคำตอบเพื่อแสดงความคิดเห็น
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;
}
รีวิวเล็ก ๆ
แบ่งรหัสบรรทัดยาวนั้น
//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));
}
บางทีอาจจะเป็น
//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));
}
สำหรับฉัน//
เมื่อสิ้นสุด 3 บรรทัดจะป้องกันไม่ให้จัดรูปแบบอัตโนมัติของบรรทัดเหล่านั้นเป็น 1 หรือ 2 บรรทัด