$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(); ?> Nine-board Tic Tac Toe

Nine-board Tic Tac Toe

Strength

 



 

Rules

Nine-board tic-tac-toe is a tic tac toe variant where nine ordinary, 3-by-3 tic-tac-toe boards make up a 3-by-3 array. Players take turns making marks (human 'X', computer 'O') in the ordinary tic-tac-toe boards. The first mark can appear anywhere. Each subsequent mark gets made in any un-marked spot in the ordinary board that corresponds to the spot where the previous mark got made. The usual 3-in-a-row arrangement of marks in any of the 9 ordinary boards wins.

Design

Nine-board tic-tac-toe is a distributed system: the user interface, written in JavaScript, runs entirely in your browser. Move calculation, written in PHP, runs in the web server.

This program uses a stock alpha-beta minimax algorithm. It does calculate the utility function incrementally, rather than doing the entire calculation at each terminal node in the game tree.

Download and Install

This version uses Sajax version 0.13 for communication between the user interface and the move calculation. Download sajax-0.13.zip, unzip it, and put files sajax.php, json2.parse.js, json2.stringify.js, json_parse.js, json_parse_state.js, json_stringify.js, and sajax.js in a directory under the web server's DocumentRoot. Download the PHP file, rename it to gen9.php, and move it to the same directory used for the Sajax files above.

Configuration

As currently configured, the program does a 4-level deep search of the game tree for the best move. Changing the value of $DEPTH_LIMIT in the PHP code to a greater value will make it search deeper, and take longer to decide. A value of 4 will handily defeat humans who have never played Nine-board before, and most who have.