$x) { $CORNERS[$x] = 1; } // Indexes into a given 88-place board for middle-squares // of the 9 sub-boards. // Pre-calculation of this array allows easy utility value calcs. $MIDDLE_INDEXES = array( 4, 14, 24, 34, 44, 54, 64, 74, 84 ); // Array, indexable by board-slot-index, value 1 // if the index is a middle square in a sub-board, // 0 if it's not a middle square in a sub-board. $MIDDLES = array_pad(array(), 89, 0); foreach ($MIDDLE_INDEXES as $idx => $x) { $MIDDLES[$x] = 1; } // A single move's static value. Sets $wn to 1 if it // finds a win for anyone. Gives smaller, possibly negative, // values for 'X' (-1) player, gives larger, possibly positive, // values for 'O' (1) player. // The move has a (subboard, slot) representation. Both have // numeric values 0 thru 8 inclusive. function utility($board, $subboard, $slot, $depth, &$wn) { global $DEPTH_LIMIT, $VERY_LARGE, $MIDDLES, $CORNERS; global $WINNING_TRIADS_BY_SLOT; $wn = 0; $score = 0; $winning_triads = $WINNING_TRIADS_BY_SLOT[$slot]; foreach ($winning_triads as $mv => $triad) { $triad_sum = $board[$triad[0] + $subboard] + $board[$triad[1] + $subboard] + $board[$triad[2] + $subboard]; switch ($triad_sum) { case 3: case -3: $wn = 1; $score = $board[$triad[0]+$subboard] * ($VERY_LARGE - $depth); break; case 2: $score += 3000; // Two in a triad, next move wins for maximizer break; case -2: $score -= 3000; // Two in a triad, next move wins for minimizer break; // 1, -1: could be a single mark, or could be a block. // $triad_sum contains -1 if -1 got blocked, 1 if 1 got blocked. // Bitwise-and lets us distinguish between a single mark, and // a two-X, one-O type of position. case 1: case -1: if (($board[$triad[0] + $subboard] & $board[$triad[1] + $subboard] & $board[$triad[2] + $subboard]) != 0) { $score -= $triad_sum * 1000; // blocks a win } break; case 0: // One of each mark, since player marked $slot // Give a point just for monkeying with the other guy. $score += $board[$subboard + $slot]; break; default: error_log("bad value calc in utility(): " . $triad_sum); break; } if ($wn) break; } if (!$wn) { // Give it a general preference for middles and // slightly lesser preference for corners, but only // in non-winning positions. $move = $subboard + $slot; $bonus = 7*$MIDDLES[$move] + 2*$CORNERS[$move]; $score += $board[$move]*$bonus; } return $score; } // Alpha-Beta minimaxing. // Slightly non-standard, as it calculates the static utility incrementally. // Beginning at the top ply in your_move(), utility() gets called on each // preceding move, and added to the overall utility value ($score_so_far // parameter. This practice speeds up the frequent calls to utility() in // terminal nodes of the game tree, as it only has to calculate the delta // utility for the move in question. function alphabeta( $board, $last_slot, $player, $next_player, $alpha, $beta, $depth, $score_so_far, $last_move_won ) { global $DEPTH_LIMIT; // Static evaluation. You could actually move this adjacent to the // actual calls to utility(), but that would mess up the readability. if ($last_move_won || $depth >= $DEPTH_LIMIT) return $score_so_far; // Find value of all the moves available to $player. // $subboard - "slot" of the last move becomes the // subboard in which this move must take place. $subboard = $last_slot * 10; if ($player == 1) { // Maximizing player, 'O', the computer for ($slot = 0; $slot < 9; ++$slot) { $move = $subboard + $slot; if ($board[$move] == 0) { $board[$move] = $player; $wn = 0; $move_score = utility($board, $subboard, $slot, $depth, $wn); $val = alphabeta($board, $slot, $next_player, $player, $alpha, $beta, $depth + 1, $score_so_far + $move_score, $wn); $board[$move] = 0; if ($val > $alpha) $alpha = $val; if ($beta <= $alpha) break; } } return $alpha; } else { // Minimizing player, 'X', the human for ($slot = 0; $slot < 9; ++$slot) { $move = $subboard + $slot; if ($board[$move] == 0) { $board[$move] = $player; $wn = 0; $move_score = utility($board, $subboard, $slot, $depth, $wn); $val = alphabeta($board, $slot, $next_player, $player, $alpha, $beta, $depth + 1, $score_so_far + $move_score, $wn); $board[$move] = 0; if ($val < $beta) $beta = $val; if ($beta <= $alpha) break; } } return $beta; } } // Called on server side by sajax. On client side, looks like a JavaScript // function call to x_your_move() function your_move($board, $jssubboard) { global $VERY_LARGE, $DEPTH_LIMIT; error_log("enter your_move([" . $board . "], " . $jssubboard . ")"); // Find out if we're given an empty board. If so, make the first move. $empty_board = 1; for ($i = 0; $empty_board && $i < 9; ++$i) { $sb = 10 * $i; for ($slot = 0; $empty_board && $slot < 9; ++$slot) { $move = $sb + $slot; if ($board[$move]) $empty_board = 0; } } if ($empty_board) { // JavaScript passed an empty board, indicating computer moves first. // Choice of first move doesn't seem to confer a huge advantage. // In lieu of a "book" just pick a random postition. while ((($r = rand(0, 88))%10) == 9) ; error_log("Taking random first move $r"); return $r; } // Not the first move. Pick the best move remaining. Again, a "book" // would have some value for maybe 2 moves into the game. $my_moves = null; $best_val = -$VERY_LARGE - $DEPTH_LIMIT; $subboard = $jssubboard * 10; for ($slot = 0; $slot < 9; ++$slot) { $move = $subboard + $slot; if ($board[$move] == 0) { error_log("Evaluating move " . $move . ". best value " . $best_val); $board[$move] = 1; $wn = 0; $mv_score = utility($board, $subboard, $slot, 0, $wn); $val = alphabeta($board, $slot, -1, 1, -2000000, 2000000, 1, $mv_score, $wn); error_log("Move " . $move . ". has value " . $val); $board[$move] = 0; if ($val > $best_val) { error_log("Best move so far: " . $move . ", value ". $val); $best_val = $val; $my_moves = array($move); } else if ($val == $best_val) { error_log("Equally good move: " . $move . ". value " . $val); array_push($my_moves, $move); } } } if (count($my_moves) == 0) error_log("Problem: found no acceptable moves."); // We've kept all the moves that end up with $best_val. Pick one // of them to make the game seem less deterministic. $rv = $my_moves[array_rand($my_moves)]; error_log("choosing " . $rv . ", value " . $best_val); return $rv; } $sajax_request_type = "GET"; sajax_export("your_move"); sajax_handle_client_request(); ?>
Strength