diff --git a/.gitignore b/.gitignore index 2e52e28f73077f0ea9ddeaf996aae920822411d5..8235c0eb972aebe41497932c5846a5bb870dce8c 100644 --- a/.gitignore +++ b/.gitignore @@ -78,4 +78,9 @@ dkms.conf # End of https://www.toptal.com/developers/gitignore/api/c,visualstudiocode # Custom gitignore for project -puissance \ No newline at end of file +puissance4 +testbed/2players/test* +testbed/rand_ai/test* +testbed/smart_ai/test* +cmake-build-debug +.idea \ No newline at end of file diff --git a/README.md b/README.md index 29e3b2e423012d5d775b814db7e32132b8450671..26496aa32f4225bcb284c4beb5e1f742c2fb8bd3 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Use this command to compile the project. Use this command to clean the project. ### Run the tests -> `make test` +> `make tests` > > `./test` diff --git a/main.c b/main.c index 9c3e10981ec45baeb100dbd489387b373660c22e..801fe257e881c2b2581a5305d219429edd1bb61e 100644 --- a/main.c +++ b/main.c @@ -7,12 +7,62 @@ #include <stdlib.h> #include "puissance.h" -int main() { +void print_help() { + printf("Usage: puissance4 <mode> <row> <col>\n"); + printf(" mode specifies the mode:\n"); + printf(" 1 = single player game (random),\n"); + printf(" 2 = single player game (AI),\n"); + printf(" 3 = two players game\n"); + printf(" row specifies the number of rows (>= 4)\n"); + printf(" col specifies the number of columns (>= 4)\n"); +} + +void play_game(puissance game) { + int selected_col_index = -1; + + while(game.state == ONGOING) { + // First player action (always a human) + if (game.current_player == PLAYER_ONE || game.mode == TWO_PLAYERS) { + printf("\nColumn number? (starts at 1):"); + do { + scanf("%d", &selected_col_index); + selected_col_index -= 1; + } while (manual_play(&game, selected_col_index) == false); + } + // Second player action (can be the computer) + else { + if (game.mode == RAND_AI) { + random_play(&game); + } else { + smart_play(&game); + }; + } + print_game(game); + } +} + +int main(int argc, char *argv[]) { + srand(0); puissance game; - game_init(&game, RAND_AI, DEFAULT_ROW, DEFAULT_COL); + // Get game arguments + if (argc != 4) { + print_help(); + return EXIT_FAILURE; + } + GameMode mode = atoi(argv[1]) - 1; + int rows = atoi(argv[2]); + int cols = atoi(argv[3]); + + // Initialize the game + game_init(&game, mode, rows, cols); + printf("Board size is %dx%d (rows x col)", rows, cols); + print_game(game); + + // Start playing + play_game(game); + // Free the memory and exit the program game_destroy(&game); - return EXIT_SUCCESS; } diff --git a/makefile b/makefile index 86ccf0e709171408d7bd94df087555e53e3db3da..2375bfbbfc1e65fe9d48d16cf47466c2cb960250 100644 --- a/makefile +++ b/makefile @@ -1,12 +1,15 @@ LIB=-lm CC=gcc -Wall -Wextra -g -puissance: puissance.o main.o +puissance4: puissance.o main.o $(CC) $^ -fsanitize=address -fsanitize=leak -o $@ $(LIB) puissance.o: puissance.c puissance.h $(CC) -c $< $(LIB) main.o: main.c $(CC) -c $< $(LIB) +tests: puissance4 + $(MAKE) -C testbed clean: - rm -f *.o puissance test \ No newline at end of file + rm -f *.o puissance4 test + $(MAKE) -C testbed clean \ No newline at end of file diff --git a/puissance.c b/puissance.c index f926d3fad94ef34a226f5210574d18d72dc06035..4df082382b7bb4c259d796c920319b2c8da169c4 100644 --- a/puissance.c +++ b/puissance.c @@ -5,45 +5,416 @@ #include "puissance.h" #include <stdio.h> +#include <stdlib.h> +#include <string.h> void game_init(puissance *p, GameMode mode, int row, int col) { p->mode = mode; + p->state = ONGOING; p->current_player = PLAYER_ONE; p->row = row; p->col = col; + // Allocate memory for the data p->data = malloc(col * sizeof(int*)); for (int i = 0; i < col; i++) { p->data[i] = malloc(row * sizeof(int*)); } + // Set a default value for the data + for (int r = 0; r < row; r++) { + for (int c = 0; c < col; c++) { + p->data[r][c] = EMPTY_CELL_VALUE; + } + } +} + +puissance game_copy(puissance *p) { + puissance copy; + game_init(©, p->mode, p->row, p->col); + copy.state = p->state; + copy.current_player = p->current_player; + + for (int r = 0; r < copy.row; r++) { + for (int c = 0; c < copy.col; c++) { + copy.data[r][c] = p->data[r][c]; + } + } + + return copy; +} + +void print_top(char *display, int col) { + // Print top + strcat(display, "\n┌"); + for (int i = 1; i <= col * 2 - 1; i++) { + if (i % 2 == 1) { + strcat(display, "─"); + } else { + strcat(display, "┬"); + } + } + strcat(display, "┐\n"); +} + +void print_bot(char *display, int col) { + // Print bot + strcat(display, "└"); + for (int i = 1; i <= col * 2 - 1; i++) { + if (i % 2 == 1) { + strcat(display, "─"); + } else { + strcat(display, "┴"); + } + } + strcat(display, "┘\n"); + + // Print col numbers + for (int i = 1; i <= col; i++) { + char i_has_string[256]; + sprintf(i_has_string, "%d", i); + strcat(display, " "); + strcat(display, i_has_string); + } +} + +void print_row_separator(char *display, int col) { + strcat(display, "├"); + for (int i = 1; i <= col * 2 - 1; i++) { + if (i % 2 == 1) { + strcat(display, "─"); + } else { + strcat(display, "┼"); + } + } + strcat(display, "┤\n"); } void print_game(puissance p) { + char display[1024]; + // Ensure that the display string is empty before doing anything + display[0] = '\0'; + + // Print the top of the game + print_top(display, p.col); + + // Print the rest of the game (except bottom) + for (int row_index = 0; row_index < p.row; row_index++) { + for (int col_index = 0; col_index < p.col; col_index++) { + // Print each row + strcat(display, "│"); + if (p.data[row_index][col_index] == PLAYER_ONE) { + strcat(display, PLAYER_ONE_STRING); + } + else if (p.data[row_index][col_index] == PLAYER_TWO) { + strcat(display, PLAYER_TWO_STRING); + } + else { + strcat(display, " "); + } + + // Close the row before going to the next line + if (col_index == p.col - 1) { + strcat(display, "│\n"); + } + } + // Print the row separator (except when we are at the bottom) + if (row_index != p.row - 1) { + print_row_separator(display, p.col); + } + } + + // Print the bottom then display the game + print_bot(display, p.col); + printf("%s", display); + // Clear the string to avoid SIGABRT + display[0] = '\0'; + + if (p.state != ONGOING) { + display_game_result(p); + } } -void verify_game(puissance p) { +GameResult get_winning_player(puissance *p) { + // Return the player who have won. + if (p->current_player == PLAYER_ONE) { + p->state = PLAYER_ONE_WIN; + } else { + p->state = PLAYER_TWO_WIN; + } + return p->state; +} + +GameResult vertical_game_check(puissance *p, int last_col_index_played) { + if (p->state != ONGOING) { + return p->state; + } + + bool four_aligned = false; + // Get the row index + int last_row_index_played = get_available_row_index(p, last_col_index_played) + 1; + + // Verify if we have enough vertical space. + if (last_row_index_played + NB_VERIFICATION_FOR_WIN < p->row) { + int last_played_value = p->data[last_row_index_played][last_col_index_played]; + four_aligned = true; + + // Verify if the aligned value are the same + for (int i = 1; i <= NB_VERIFICATION_FOR_WIN; i++) { + if (last_played_value != p->data[last_row_index_played + i][last_col_index_played]) { + four_aligned = false; + break; + } + } + + if (four_aligned) { + return get_winning_player(p); + } + } + + return ONGOING; +} + +GameResult horizontal_game_check(puissance *p, int last_col_index_played) { + if (p->state != ONGOING) { + return p->state; + } + bool four_aligned = false; + // Get the row index + int last_row_index_played = get_available_row_index(p, last_col_index_played) + 1; + int last_played_value = p->data[last_row_index_played][last_col_index_played]; + + int same_aligned = 0; + for (int i = 0; i < p->col; i++) { + if (last_played_value == p->data[last_row_index_played][i]) { + same_aligned++; + } else { + same_aligned = 0; + } + + if (same_aligned == NB_SAME_VALUE_ALIGNED_FOR_WIN) { + four_aligned = true; + break; + } + } + + if (four_aligned) { + return get_winning_player(p); + } + + return ONGOING; +} + +bool diagonal_parse(puissance *p, int x_r, int y_c, int starting_row, int starting_col) { + bool four_aligned = true; + int last_played_value = p->data[starting_row][starting_col]; + + // Search the end position of the diagonal + do { + int x = -x_r + starting_row; + int y = -y_c + starting_col; + + if (x > 0 && x < p->row && y > 0 && y < p->col && p->data[x][y] == last_played_value) { + starting_row = x; + starting_col = y; + } else { + break; + } + } while(true); + + // Verify if we have the same value aligned enough times + for (int i = 1; i <= NB_VERIFICATION_FOR_WIN; i++) { + int r = x_r * i + starting_row; + int c = y_c * i + starting_col; + + if (r < 0 || r >= p->row || c < 0 || c >= p->col) { + return false; + } + + if (p->data[r][c] != last_played_value) { + four_aligned = false; + break; + } + } + + return four_aligned; +} + +GameResult diagonal_game_check(puissance *p, int last_col_index_played) { + if (p->state != ONGOING) { + return p->state; + } + + // Get the row index + int last_row_index_played = get_available_row_index(p, last_col_index_played) + 1; + + // down right + if (diagonal_parse(p, 1, 1, last_row_index_played, last_col_index_played)) { + return get_winning_player(p); + } + // down left + if (diagonal_parse(p, 1, -1, last_row_index_played, last_col_index_played)) { + return get_winning_player(p); + } + // up left + if (diagonal_parse(p, -1, -1, last_row_index_played, last_col_index_played)) { + return get_winning_player(p); + } + // up right + if (diagonal_parse(p, -1, 1, last_row_index_played, last_col_index_played)) { + return get_winning_player(p); + } + + return ONGOING; +} + +GameResult verify_space_remaining(puissance *p) { + if (p->state != ONGOING) { + return p->state; + } + bool space_available = false; + int top_row_index = 0; + + // Verify that at least one cell at the top row is empty. + for (int i = 0; i < p->col; i++) { + if (p->data[top_row_index][i] == EMPTY_CELL_VALUE) { + space_available = true; + break; + } + } + // If no cells at top is empty, then the game can't continue, so it's a draw. + if (space_available == false) { + p->state = DRAW; + } + return p->state; +} + +GameResult verify_game(puissance *p, int last_col_index_played) { + // Vertical check + vertical_game_check(p, last_col_index_played); + // Horizontal check + horizontal_game_check(p, last_col_index_played); + // Diagonal check + diagonal_game_check(p, last_col_index_played); + // Verify remaining space + verify_space_remaining(p); + + return p->state; +} + +GameResult display_game_result(puissance p) { + if (p.state == PLAYER_ONE_WIN) { + printf("\nPlayer one won!\n"); + } + if (p.state == PLAYER_TWO_WIN) { + if (p.mode == TWO_PLAYERS) { + printf("\nPlayer two won!\n"); + } else { + printf("\nComputer won!\n"); + } + } + if (p.state == DRAW) { + printf("\nIt is a draw.\n"); + } + + return p.state; +} + +int get_available_row_index(puissance *p, int selected_col_index) { + // Verify the index selected + if (selected_col_index >= p->col || selected_col_index < 0) { + return EMPTY_CELL_VALUE; // Bad index + } + + for (int i = p->row - 1; i >= 0; i--) { + if (p->data[i][selected_col_index] == EMPTY_CELL_VALUE) { + return i; + } + } + return -1; // No remaining row } bool manual_play(puissance *p, int selected_col_index) { - bool valid_action = true; + // Get the available row index and verify that the column is not full. + int row_index = get_available_row_index(p, selected_col_index); + if (row_index < 0) { + return false; + } + + // Play + p->data[row_index][selected_col_index] = p->current_player; + + // Verify the game + GameResult result = verify_game(p, selected_col_index); - return valid_action; + // Switch the current player + if (result == ONGOING) { + if (p->current_player == PLAYER_ONE) { + p->current_player = PLAYER_TWO; + } else { + p->current_player = PLAYER_ONE; + } + } + + // Valid the action + return true; } bool random_play(puissance *p) { - bool valid_action = true; + int max = p->col - 1; + int min = 0; + int random_index = -1; + do { + random_index = rand() % (max - min + 1) + min; + } while(manual_play(p, random_index) == false); + + return true; +} + +bool search_optimal_action(puissance *p, bool simulate_player_one) { + bool move_validated = false; + + // Search the optimal action by simulating each possible action + // Not the must memory efficient but this method is easily implemented and should work without problems + for (int i = 0; i < p->col; i++) { + puissance copy = game_copy(p); + if (simulate_player_one) { + copy.current_player = PLAYER_ONE; + } - return valid_action; + if (manual_play(©, i)) { + if (copy.state != ONGOING) { + move_validated = manual_play(p, i); + } + } + + game_destroy(©); + if (move_validated) { + return move_validated; + } + } + return move_validated; } bool smart_play(puissance *p) { - bool valid_action = true; + // Search for a wining action + if (search_optimal_action(p, false)) { + return true; + } - return valid_action; + // Try to prevent the user to win + if (search_optimal_action(p, true)) { + return true; + } + + // If nothing has been done, do something random + return random_play(p); } void game_destroy(puissance *p) { + for (int i = 0; i < p->col; i++) { + free(p->data[i]); + } free(p->data); p = NULL; } diff --git a/puissance.h b/puissance.h index fdfec3c8fc70e9bccd94dd47debdbb808742a246..1e866bd0707390dedb191316d7507db31cee926f 100644 --- a/puissance.h +++ b/puissance.h @@ -13,6 +13,11 @@ #define COL_MIN 4 #define DEFAULT_ROW 6 #define DEFAULT_COL 7 +#define PLAYER_ONE_STRING "X" +#define PLAYER_TWO_STRING "O" +#define EMPTY_CELL_VALUE -2 +#define NB_SAME_VALUE_ALIGNED_FOR_WIN 4 +#define NB_VERIFICATION_FOR_WIN (NB_SAME_VALUE_ALIGNED_FOR_WIN - 1) typedef enum { RAND_AI, @@ -34,6 +39,7 @@ typedef enum { typedef struct _puissance { GameMode mode; + GameResult state; Player current_player; int row; int col; @@ -41,11 +47,16 @@ typedef struct _puissance { } puissance; void game_init(puissance *p, GameMode mode, int row, int col); +puissance game_copy(puissance *p); void print_game(puissance p); -void verify_game(puissance p); +GameResult verify_game(puissance *p, int last_col_index_played); +GameResult display_game_result(puissance p); +int get_available_row_index(puissance *p, int selected_col_index); bool manual_play(puissance *p, int selected_col_index); bool random_play(puissance *p); bool smart_play(puissance *p); void game_destroy(puissance *p); +// TODO: add get top of col + #endif \ No newline at end of file diff --git a/skeleton_for_students/Makefile b/skeleton_for_students/Makefile deleted file mode 100644 index 57c7cd62f3d1b3b3db4cf8b9d50106740c620218..0000000000000000000000000000000000000000 --- a/skeleton_for_students/Makefile +++ /dev/null @@ -1,9 +0,0 @@ -puissance4: - @echo "first rule which must create the puissance4 executable" - -clean: - @echo "this rule must clean everything up (including candidate files in testbed)" - $(MAKE) -C testbed clean - -tests: puissance4 - $(MAKE) -C testbed \ No newline at end of file diff --git a/skeleton_for_students/puissance4 b/skeleton_for_students/puissance4 deleted file mode 100755 index c73ed624297549082e6abff83ff60652fc3c34c8..0000000000000000000000000000000000000000 Binary files a/skeleton_for_students/puissance4 and /dev/null differ diff --git a/skeleton_for_students/testbed/2players/Makefile b/testbed/2players/Makefile similarity index 100% rename from skeleton_for_students/testbed/2players/Makefile rename to testbed/2players/Makefile diff --git a/skeleton_for_students/testbed/2players/test1.in b/testbed/2players/test1.in similarity index 100% rename from skeleton_for_students/testbed/2players/test1.in rename to testbed/2players/test1.in diff --git a/skeleton_for_students/testbed/2players/test1.ref b/testbed/2players/test1.ref similarity index 100% rename from skeleton_for_students/testbed/2players/test1.ref rename to testbed/2players/test1.ref diff --git a/skeleton_for_students/testbed/2players/test2.in b/testbed/2players/test2.in similarity index 100% rename from skeleton_for_students/testbed/2players/test2.in rename to testbed/2players/test2.in diff --git a/skeleton_for_students/testbed/2players/test2.ref b/testbed/2players/test2.ref similarity index 100% rename from skeleton_for_students/testbed/2players/test2.ref rename to testbed/2players/test2.ref diff --git a/skeleton_for_students/testbed/2players/test3.in b/testbed/2players/test3.in similarity index 100% rename from skeleton_for_students/testbed/2players/test3.in rename to testbed/2players/test3.in diff --git a/skeleton_for_students/testbed/2players/test3.ref b/testbed/2players/test3.ref similarity index 100% rename from skeleton_for_students/testbed/2players/test3.ref rename to testbed/2players/test3.ref diff --git a/skeleton_for_students/testbed/2players/test4.in b/testbed/2players/test4.in similarity index 100% rename from skeleton_for_students/testbed/2players/test4.in rename to testbed/2players/test4.in diff --git a/skeleton_for_students/testbed/2players/test4.ref b/testbed/2players/test4.ref similarity index 100% rename from skeleton_for_students/testbed/2players/test4.ref rename to testbed/2players/test4.ref diff --git a/skeleton_for_students/testbed/2players/test5.in b/testbed/2players/test5.in similarity index 100% rename from skeleton_for_students/testbed/2players/test5.in rename to testbed/2players/test5.in diff --git a/skeleton_for_students/testbed/2players/test5.ref b/testbed/2players/test5.ref similarity index 100% rename from skeleton_for_students/testbed/2players/test5.ref rename to testbed/2players/test5.ref diff --git a/skeleton_for_students/testbed/Makefile b/testbed/Makefile similarity index 100% rename from skeleton_for_students/testbed/Makefile rename to testbed/Makefile diff --git a/skeleton_for_students/testbed/common.mk b/testbed/common.mk similarity index 100% rename from skeleton_for_students/testbed/common.mk rename to testbed/common.mk diff --git a/skeleton_for_students/testbed/rand_ai/Makefile b/testbed/rand_ai/Makefile similarity index 100% rename from skeleton_for_students/testbed/rand_ai/Makefile rename to testbed/rand_ai/Makefile diff --git a/skeleton_for_students/testbed/rand_ai/test1.in b/testbed/rand_ai/test1.in similarity index 100% rename from skeleton_for_students/testbed/rand_ai/test1.in rename to testbed/rand_ai/test1.in diff --git a/skeleton_for_students/testbed/rand_ai/test1.ref b/testbed/rand_ai/test1.ref similarity index 100% rename from skeleton_for_students/testbed/rand_ai/test1.ref rename to testbed/rand_ai/test1.ref diff --git a/skeleton_for_students/testbed/rand_ai/test2.in b/testbed/rand_ai/test2.in similarity index 100% rename from skeleton_for_students/testbed/rand_ai/test2.in rename to testbed/rand_ai/test2.in diff --git a/skeleton_for_students/testbed/rand_ai/test2.ref b/testbed/rand_ai/test2.ref similarity index 100% rename from skeleton_for_students/testbed/rand_ai/test2.ref rename to testbed/rand_ai/test2.ref diff --git a/skeleton_for_students/testbed/rand_ai/test3.in b/testbed/rand_ai/test3.in similarity index 100% rename from skeleton_for_students/testbed/rand_ai/test3.in rename to testbed/rand_ai/test3.in diff --git a/skeleton_for_students/testbed/rand_ai/test3.ref b/testbed/rand_ai/test3.ref similarity index 100% rename from skeleton_for_students/testbed/rand_ai/test3.ref rename to testbed/rand_ai/test3.ref diff --git a/skeleton_for_students/testbed/rand_ai/test4.in b/testbed/rand_ai/test4.in similarity index 100% rename from skeleton_for_students/testbed/rand_ai/test4.in rename to testbed/rand_ai/test4.in diff --git a/skeleton_for_students/testbed/rand_ai/test4.ref b/testbed/rand_ai/test4.ref similarity index 100% rename from skeleton_for_students/testbed/rand_ai/test4.ref rename to testbed/rand_ai/test4.ref diff --git a/skeleton_for_students/testbed/smart_ai/Makefile b/testbed/smart_ai/Makefile similarity index 100% rename from skeleton_for_students/testbed/smart_ai/Makefile rename to testbed/smart_ai/Makefile diff --git a/skeleton_for_students/testbed/smart_ai/test1.in b/testbed/smart_ai/test1.in similarity index 100% rename from skeleton_for_students/testbed/smart_ai/test1.in rename to testbed/smart_ai/test1.in diff --git a/skeleton_for_students/testbed/smart_ai/test1.ref b/testbed/smart_ai/test1.ref similarity index 100% rename from skeleton_for_students/testbed/smart_ai/test1.ref rename to testbed/smart_ai/test1.ref diff --git a/skeleton_for_students/testbed/smart_ai/test2.in b/testbed/smart_ai/test2.in similarity index 100% rename from skeleton_for_students/testbed/smart_ai/test2.in rename to testbed/smart_ai/test2.in diff --git a/skeleton_for_students/testbed/smart_ai/test2.ref b/testbed/smart_ai/test2.ref similarity index 100% rename from skeleton_for_students/testbed/smart_ai/test2.ref rename to testbed/smart_ai/test2.ref diff --git a/skeleton_for_students/testbed/smart_ai/test3.in b/testbed/smart_ai/test3.in similarity index 100% rename from skeleton_for_students/testbed/smart_ai/test3.in rename to testbed/smart_ai/test3.in diff --git a/skeleton_for_students/testbed/smart_ai/test3.ref b/testbed/smart_ai/test3.ref similarity index 100% rename from skeleton_for_students/testbed/smart_ai/test3.ref rename to testbed/smart_ai/test3.ref diff --git a/skeleton_for_students/testbed/smart_ai/test4.in b/testbed/smart_ai/test4.in similarity index 100% rename from skeleton_for_students/testbed/smart_ai/test4.in rename to testbed/smart_ai/test4.in diff --git a/skeleton_for_students/testbed/smart_ai/test4.ref b/testbed/smart_ai/test4.ref similarity index 100% rename from skeleton_for_students/testbed/smart_ai/test4.ref rename to testbed/smart_ai/test4.ref diff --git a/skeleton_for_students/testbed/smart_ai/test5.in b/testbed/smart_ai/test5.in similarity index 100% rename from skeleton_for_students/testbed/smart_ai/test5.in rename to testbed/smart_ai/test5.in diff --git a/skeleton_for_students/testbed/smart_ai/test5.ref b/testbed/smart_ai/test5.ref similarity index 100% rename from skeleton_for_students/testbed/smart_ai/test5.ref rename to testbed/smart_ai/test5.ref