1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339
|
/*
* Atom-4 AI player
* Implementation file
*
* $Id: ai.cc,v 1.23 2003/04/10 15:39:54 hsteoh Exp hsteoh $
*/
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include "ai.h"
#include "color4.h"
#include "exception.h"
/*
* AI behaviour settings
*/
#define DEFAULT_SEARCH_DEPTH 2
// Heuristics for nodes at maxdepth=0
#define COLOR_CHANGE_WEIGHT 1.0
#define PCREATED_WEIGHT 1.7
#define PDESTROYED_WEIGHT 1.5
#define WINNING_WEIGHT 1000.0
#ifdef DEBUG_AI
void atom4ai::reopen_log() {
fclose(logfile); // reopen log file per round
logfile=fopen("/tmp/atom4ai.log","w");
if (!logfile) throw exception("atom4ai: failed to open logfile\n");
}
#endif //DEBUG_AI
#ifdef PROFILE_AI
#include <sys/gmon.h>
extern "C" {
extern void _start(void), etext(void);
}
#endif //PROFILE_AI
void atom4ai::childmon::read_ready(eventloop *loop, int fd) {
int x,y;
int count;
int status; // child status
if (read(fd, &x, sizeof(x))==-1)
throw exception("Error while reading from AI subprocess");
if (read(fd, &y, sizeof(y))==-1)
throw exception("Error while reading from AI subprocess");
// Remove handler from event loop
loop->unregister_handler(eventloop::READER, fd);
close(fd);
waitpid(ai->child, &status, 0); // wait for child to terminate
ai->child = -1;
if (!ai->move(ai->playernum, x,y)) {
throw exception("@Unable to move to (%d,%d) as requested by AI subprocess",
x,y);
}
}
void atom4ai::childmon::write_ready(eventloop *loop, int fd) {
throw exception("atom4ai::childmon registered as a write handler??\n");
}
void atom4ai::find_legal_moves(board4 &board, elist<atom4ai::move_t> &moves) {
move_t m;
// Scan for all possible legal moves
moves.clear();
for (m.y=0; m.y<board.height(); m.y++) {
for (m.x=0; m.x<board.width(); m.x++) {
if (board.check_legal(m.x, m.y)) {
moves.append(m);
}
} // endfor(x)
} // endfor(y)
}
float atom4ai::score_opponent_turn(board4 &board, color4 nexttile, float min,
float max, hdelta h, int maxdepth) {
elist<move_t> moves;
elistiter<move_t> it;
// Find opponent's best response to our move
find_legal_moves(board, moves);
for (it=moves.headp(); it; it++) {
#ifdef DEBUG_AI
fprintf(logfile, " (%d) Considering: (%d,%d), min=%4.3f max=%4.3f <<\n",
maxdepth, (*it).x, (*it).y, min, max);
#endif //DEBUG_AI
float score = score_move(board, nexttile, *it, min, max, h, maxdepth);
if (score > min) min = score;
#ifdef DEBUG_AI
fprintf(logfile, " >>(%d) move(%d,%d): %4.3f\n", maxdepth, (*it).x,
(*it).y, score);
#endif //DEBUG_AI
// Alpha prune
if (score > max) {
#ifdef DEBUG_AI
fprintf(logfile, " [(%d) PRUNED: %4.3f > (%4.3f)]\n", maxdepth, score,
max);
#endif //DEBUG_AI
return 1e9; // anything > max will do, actually.
}
}
return min;
}
void atom4ai::update_heuristic(board4 &board, color4 our_prop,
elist<boardchange> &undo, hdelta &h) {
elistiter<boardchange> it;
color4 enemy_prop = our_prop.complement();
for (it=undo.headp(); it; it++, h.changes++) {
color4 newcell = board.getcell((*it).x, (*it).y);
color4 oldcell = (*it).cell;
if (newcell==our_prop) h.pcreated++;
if (oldcell==enemy_prop) h.pdestroyed++;
}
}
float atom4ai::calc_heuristic(hdelta h) {
return COLOR_CHANGE_WEIGHT*h.changes +
PCREATED_WEIGHT*h.pcreated +
PDESTROYED_WEIGHT*h.pdestroyed;
}
float atom4ai::score_move(board4 &board, color4 tile, move_t m, float min,
float max, hdelta h, int maxdepth) {
color4 our_prop = tile.propagator();
elist<boardchange> splash, unsplash;
float score;
int win;
// Compute changes caused by move
board.compute_splash(m.x, m.y, tile, splash);
board.applychanges(splash, &unsplash);
update_heuristic(board, our_prop, unsplash, h);
if (board.check_win(splash, our_prop, WIN_ROW)) {
// Adjust based on real, hard data :-)
score = WINNING_WEIGHT + calc_heuristic(h);
} else if (maxdepth >0) {
// Swap and negate min and max: if opponent is able to make a move that
// scores higher than current (-min), we won't make that choice.
score = -score_opponent_turn(board, nextcolor(tile), -1e9, -min, -h,
maxdepth-1);
} else {
score = calc_heuristic(h); // exhausted search depth
}
board.applychanges(unsplash, NULL); // restore original board
return score;
}
atom4ai::move_t atom4ai::pick_random(elist<atom4ai::move_t> &list) {
int p; // 1/probability
elistiter<move_t> it;
move_t pick;
for (p=1, it=list.headp(); it; p++, it++) {
if (rand()%p == 0) {
pick = *it;
}
}
return pick;
}
void atom4ai::pick_best_move(int fd) {
elist<move_t> moves;
elist<move_t> bestmoves; // in case >1 moves have equal score
elistiter<move_t> it;
float min=-1e9, max=+1e9;
hdelta h = { 0, 0, 0 }; // heuristic function accumulator
find_legal_moves(*get_board(), moves);
// compute score for each move
for (it=moves.headp(); it; it++) {
move_t m = *it;
#ifdef DEBUG_AI
fprintf(logfile, "AI: considering move (%d,%d), max depth=%d {{\n",
m.x, m.y, search_depth);
#endif //DEBUG_AI
float score = score_move(*get_board(), current_tile(), m, min, max, h,
search_depth);
#ifdef DEBUG_AI
fprintf(logfile, "}} move (%d,%d): %.3f\n", m.x, m.y, score);
#endif //DEBUG_AI
// Update list of good moves
if (score > min) {
min = score;
bestmoves.clear(); // prev moves are worse than this one
bestmoves.append(m); // this move is now the best
} else if (score==min) {
bestmoves.append(m); // more than one best move so far
}
}
// randomly pick a move from the list of highest-scored moves
if (bestmoves.num_elem() < 1)
throw exception("atom4ai: unable to find valid move?");
move_t m = pick_random(bestmoves);
#ifdef DEBUG_AI
fprintf(logfile, "AI: moving (%d,%d) --> %.3f\n", m.x, m.y, min);
#endif //DEBUG_AI
// Send move back to parent process
write(fd, &m.x, sizeof(m.x));
write(fd, &m.y, sizeof(m.y));
#ifdef PROFILE_AI
_mcleanup();
#endif //PROFILE_AI
}
// TESTING: spawn subprocess to do the computation, so that the event loop
// can continue processing events.
void atom4ai::make_move() {
int pipefd[2];
if (current_player()!=playernum || child != -1)
return; // not our turn
if (pipe(pipefd)==-1)
throw exception("Unable to create communications channel with AI "
"subprocess");
switch(child=fork()) {
case -1:
throw exception("Unable to create AI subprocess\n");
case 0: // in child
close(pipefd[0]); // close read fd
#ifdef PROFILE_AI
monstartup((u_long)&_start, (u_long)&etext);
#endif //PROFILE_AI
pick_best_move(pipefd[1]);
exit(0); // never return
default: // in parent
close(pipefd[1]); // close write fd
childpipe = pipefd[0]; // remember read fd
loop->register_handler(eventloop::READER, childpipe, &babysitter);
break;
}
}
void atom4ai::notify_move(int player, elist<boardchange> &chg) {
atom4::notify_move(player, chg); // forward notification first
make_move(); // if it's now our turn, make a move
}
void atom4ai::notify_clear() {
atom4::notify_clear(); // forward notification
make_move(); // move if it's our turn
}
atom4ai::atom4ai(eventloop *eloop, unsigned int width, unsigned int height,
int which_player) :
atom4local(width,height), playernum(which_player),
search_depth(DEFAULT_SEARCH_DEPTH),
loop(eloop), babysitter(this) {
#ifdef DEBUG_AI
logfile=fopen("/tmp/atom4ai.log","w");
if (!logfile) throw exception("atom4ai: failed to open logfile\n");
#endif //DEBUG_AI
child = -1; // indicate not waiting for process
// Make first move if we're starting
make_move();
}
atom4ai::~atom4ai() {
if (child != -1) {
loop->unregister_handler(eventloop::READER, childpipe);
// TODO: kill child process
}
#ifdef DEBUG_AI
fclose(logfile);
#endif //DEBUG_AI
}
void atom4ai::reset() {
// NOTE: may want to insert resetting code here if we keep state between
// turns
atom4local::reset();
#ifdef DEBUG_AI
reopen_log();
#endif //DEBUG_AI
make_move(); // move if it's our turn after reset
}
atom4::mode_t atom4ai::game_mode() {
return atom4::PEER;
}
int atom4ai::local_playernum() {
// FIXME: can be more general
return playernum==1 ? 2 : 1;
}
int atom4ai::is_local_turn() {
return current_player()!=playernum && !round_over();
}
int atom4ai::newround() {
atom4local::newround();
#ifdef DEBUG_AI
reopen_log();
#endif //DEBUG_AI
make_move(); // move if it's our turn after reset
}
|