/* * This file is part of the OpenKropki project. * * Copyright (C) 2014-2022 Mateusz Viste * * MIT License * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #include <stdio.h> /* printf(), puts() */ #include <stdlib.h> /* rand(), srand() */ #include <string.h> /* memset */ #include <time.h> /* time(), clock_gettime() */ #include <SDL2/SDL_thread.h> #include "kropcore.h" #include "okp.h" #include "ai.h" /* include self for control */ struct minimax_t { int x, y; int score; }; struct threaddata_t { struct minimax_t *minimax; struct kropki_game *game; int ai_player; int singlerun; int *interesting_box; }; static int distance(int x1, int y1, int x2, int y2) { int xdist, ydist; if (x1 > x2) { xdist = x1 - x2; } else { xdist = x2 - x1; } if (y1 > y2) { ydist = y1 - y2; } else { ydist = y2 - y1; } return(xdist + ydist); } /* there are some specific situations when a simple hard-wired logic can * detect things NOT to do - this function provides a VETO against such * well-known bad situations. */ static int is_veto_situation(int ai_player, struct kropki_game *game, int x, int y) { int i, t, a, b, c, d; /* do not put your soldier between three oponents */ i = 0; /* i will count the number of opponents around */ t = 0; /* t is the number of my soldiers */ a = kropki_getpos(game, x, y - 1) & 3; /* use the (slower) getpos version */ b = kropki_getpos(game, x, y + 1) & 3; /* that performs boundary checks */ c = kropki_getpos(game, x - 1, y) & 3; d = kropki_getpos(game, x + 1, y) & 3; if (a == 3 - ai_player) i++; if (b == 3 - ai_player) i++; if (c == 3 - ai_player) i++; if (d == 3 - ai_player) i++; if (a == ai_player) t++; if (b == ai_player) t++; if (c == ai_player) t++; if (d == ai_player) t++; if ((i > 2) && (t == 0)) { /* printf("%d:%d is a VETO!\n", x, y); */ return(1); } return(0); } static int ai_minimax(int ai_player, struct kropki_game *game, struct minimax_t *minmax, int recur, const int *interesting_box) { int bestscore = 0, bestmovex = -1, bestmovey = -1; int x, y, deltascore = 0, orgdelta; struct kropki_game *simgame; orgdelta = game->score[ai_player - 1] - game->score[(3 - ai_player) - 1]; for (y = interesting_box[1]; y <= interesting_box[3]; y++) { for (x = interesting_box[0]; x <= interesting_box[2]; x++) { int vetoflag = 0; /* if field not empty -> skip it */ if (KROPKI_GETPOS(game, x, y) != 0) continue; /* do the actual simulation (without wall building: useless, faster) */ simgame = kropki_copygame(game, kropki_NO_WALLS); if (simgame->nextplayer + 1 != ai_player) kropki_pass(simgame, 3 - ai_player); /* opponent always passes his moves */ kropki_setpos(simgame, x, y, ai_player, kropki_NO_WALLS); deltascore = simgame->score[ai_player - 1] - simgame->score[(3 - ai_player) - 1]; deltascore -= orgdelta; vetoflag = is_veto_situation(3 - ai_player, game, x, y); /* check veto positions */ if (recur < 2) { /* stop at 3 recursions (0, 1, 2) */ deltascore += ai_minimax(ai_player, simgame, minmax, recur + 1, interesting_box); } if ((deltascore > bestscore) || (bestmovex < 0)) { /* printf("best move for ai %d found at [%d,%d] -> %d pts\n", ai_player, x, y, deltascore); */ bestmovex = x; bestmovey = y; bestscore = deltascore; } else if ((deltascore == bestscore) && (vetoflag == 0)) { /* if same score, see if closer, unless VETOed */ int distbest, distthis; distbest = distance(bestmovex, bestmovey, game->lastmovex, game->lastmovey); distthis = distance(x, y, game->lastmovex, game->lastmovey); if ((distthis < distbest) || ((distthis == distbest) && ((rand() & 7) == 0))) {/* for equivalent moves, choose randomly whether to follow it or not */ /* printf("[%d,%d] is closer to [%d,%d]\n", x, y, game->lastmovex, game->lastmovey); */ bestmovex = x; bestmovey = y; } } kropki_freegame(simgame); } } minmax->x = bestmovex; minmax->y = bestmovey; minmax->score = bestscore; return(bestscore); } static int ai_minimax_thread(void *param) { struct threaddata_t *p = param; int recursion = 0; if (p->singlerun != 0) recursion = 99; ai_minimax(p->ai_player, p->game, p->minimax, recursion, p->interesting_box); return(0); } /* find a box around last move with at least minpos available positions */ static void computeinterestingbox(int *box, const struct kropki_game *game, int minpos) { int i, x, y, count; /* if no move yet, then interesting box is somewhere at the center of the field */ if (game->lastplayer == 0) { box[0] = game->field_width / 2 - 3; box[1] = game->field_height / 2 - 2; box[2] = game->field_width / 2 + 3; box[3] = game->field_height / 2 + 2; return; } /* */ for (i = 2; i < (game->field_width + game->field_height); i++) { count = 0; box[0] = game->lastmovex - i; box[1] = game->lastmovey - i; box[2] = game->lastmovex + i; box[3] = game->lastmovey + i; if (box[0] < 0) box[0] = 0; if (box[1] < 0) box[1] = 0; if (box[2] >= game->field_width) box[2] = game->field_width - 1; if (box[3] >= game->field_height) box[3] = game->field_height - 1; /* count the amount of available fields in this box */ for (y = box[1]; y <= box[3]; y++) { for (x = box[0]; x <= box[2]; x++) { if (KROPKI_GETPOS(game, x, y) == 0) count++; } } /* printf("interesting box [%d,%d]-[%d,%d] yields %d avail. fields\n", box[0], box[1], box[2], box[3], count); */ if (count >= minpos) break; } } static void ai_run(int ai_player, struct kropki_game *game, int *movex, int *movey) { SDL_Thread* threadid[2]; struct threaddata_t threadparam[2]; struct minimax_t mini, maxi; struct timespec starttime, stoptime; unsigned long mstime; int playsafe; int i; int interesting_box[4]; /* x1,y1,x2,y2 */ memset(&mini, 0, sizeof(mini)); memset(&maxi, 0, sizeof(maxi)); clock_gettime(CLOCK_REALTIME, &starttime); /* CLOCK_REALTIME is the only clock guaranteed to be implemented */ /* runs minmax 2 times: first time with only a single recursion and analyzing a broader field: if there are any immediate wins or dangers, no need to analyze any further */ for (i = 0; i < 2; i++) { /* find the 'interesting' box of places (at least 100 positions around last move for first iteration, and 20 positions for further iterations) */ computeinterestingbox(interesting_box, game, 100 - (i * 80)); /* prepare and run 1st thread */ threadparam[0].minimax = &maxi; threadparam[0].game = game; threadparam[0].singlerun = 1 - i; threadparam[0].ai_player = ai_player; threadparam[0].interesting_box = interesting_box; threadid[0] = SDL_CreateThread(ai_minimax_thread, "max", &threadparam[0]); /* prepare and run 2nd thread */ threadparam[1].minimax = &mini; threadparam[1].game = game; threadparam[1].singlerun = 1 - i; threadparam[1].ai_player = 3 - ai_player; threadparam[1].interesting_box = interesting_box; threadid[1] = SDL_CreateThread(ai_minimax_thread, "min", &threadparam[1]); /* wait for the parallell threads to terminate */ SDL_WaitThread(threadid[0], NULL); SDL_WaitThread(threadid[1], NULL); if ((mini.score != 0) || (maxi.score != 0)) break; } clock_gettime(CLOCK_REALTIME, &stoptime); mstime = (unsigned long)((stoptime.tv_sec - starttime.tv_sec) * 1000); mstime += (unsigned long)((stoptime.tv_nsec - starttime.tv_nsec) / 1000 / 1000); printf("max: [%d,%d] -> %d pts\nmin: [%d,%d] -> %d pts\ncomputation time: %lu ms\n\n", mini.x, mini.y, mini.score, maxi.x, maxi.y, maxi.score, mstime); /* decide whether to play defensively or offensively */ if (i == 0) { /* if there is an immediate win, go for it */ if (maxi.score > 0) { printf("immediate win detected! [%d,%d]\n", maxi.x, maxi.y); playsafe = 0; } else { printf("immediate danger detected! [%d,%d]\n", maxi.x, maxi.y); playsafe = 1; } } else if (maxi.score > mini.score) { playsafe = 0; } else { /* if I'm afraid to loose points, check the bold move anyway */ struct kropki_game *simgame; struct minimax_t newmini; memset(&newmini, 0, sizeof(newmini)); playsafe = 1; if (maxi.score > 0) { /* simulate the bold move */ simgame = kropki_copygame(game, kropki_NO_WALLS); ai_minimax(3 - ai_player, simgame, &newmini, 0, interesting_box); /* recheck min now - is it still bad? */ if (newmini.score <= maxi.score) { printf("it appears that playing boldly might be rewarding!\n\n"); playsafe = 0; } kropki_freegame(simgame); } } /* make the move */ if (playsafe != 0) { *movex = mini.x; *movey = mini.y; } else { *movex = maxi.x; *movey = maxi.y; } } void ai_dumbo(const char *okp, int *movex, int *movey) { static int rand_init = 0; struct kropki_game *game; int ai_player = 2; /* pre-init move coordinates to 'failed' */ *movex = -1; *movey = -1; /* init the random generator, if not inited yet */ if (rand_init == 0) { rand_init = 1; srand((unsigned int)time(NULL)); } /* convert OKP into a game struct */ game = kropki_okp2struct(okp, NULL, NULL, 0); if (game == NULL) { puts("kropki_okp2struct() FAIL!"); return; } /* verify that I can make any move at all */ if (kropki_findfirstfreefield(game, NULL, NULL) != 0) return; /* execute the AI now */ ai_run(ai_player, game, movex, movey); }