local function prequire(name) local success, result = pcall(require, name); return success and result end1local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")23function test()45-- https://github.com/stefandd/Tic46local negaMax = {maxdepth = 4, minsearchpos = 0, numsearchpos = 0}7negaMax.__index = negaMax89function negaMax:evaluate(board, depth)10--[[11What can be confusing is how the heuristic value of the current node is calculated. In this implementation, this value is always calculated from the point of view of player A, whose color value is one. In other words, higher heuristic values always represent situations more favorable for player A. This is the same behavior as the normal minimax algorithm. The heuristic value is not necessarily the same as a node's return value due to value negation by negamax and the color parameter. The negamax node's return value is a heuristic score from the point of view of the node's current player.1213Negamax scores match minimax scores for nodes where player A is about to play, and where player A is the maximizing player in the minimax equivalent. Negamax always searches for the maximum value for all its nodes. Hence for player B nodes, the minimax score is a negation of its negamax score. Player B is the minimizing player in the minimax equivalent.1415Variations in negamax implementations may omit the color parameter. In this case, the heuristic evaluation function must return values from the point of view of the node's current player.16--]]17print ("This function needs to be implemented!")18end1920function negaMax:move_candidates(board, side_to_move)21print ("This function needs to be implemented!")22end2324function negaMax:make_move(board, side_to_move, move)25print ("This function needs to be implemented!")26end2728function negaMax:negaMax(board, side_to_move, depth, alpha, beta) -- side_to_move: e.g. 1 is blue, -1 is read29--30-- init vars for root call31--32if not depth then -- root call33depth = 034alpha = -math.huge35beta = math.huge36self.numsearchpos = 0 -- reset call counter37end38--39-- test if the node is terminal (i.e. full board or win)40--41local best_move = -142local score, is_term_node = self:evaluate(board, depth)43-- we abort the recursion if this is a terminal node, or if one of the search abort conditions are met44--45if is_term_node or depth == self.maxdepth then46return side_to_move*score, best_move, is_term_node47end48--49-- if not terminal node, eval child nodes50--51local moves = self:move_candidates(board, side_to_move)52score = -math.huge5354for _, analyzed_move in pairs(moves) do -- iterate over all boards55self.numsearchpos = self.numsearchpos + 156local b = self:make_move(board, side_to_move, analyzed_move)57local move_score, _, _ = -self:negaMax(b, -side_to_move, depth+1, -beta, -alpha)58if move_score > score then59score = move_score60best_move = analyzed_move61end62-- disable alpha-beta pruning63--64alpha = math.max(alpha, score)65if alpha >= beta then66break67end68--69end70if depth == 0 then71--72-- exit for root call (depth == 0)73--74-- debug stuff75--print(string.format("---- Negamax: node info, depth: %d, side: %d, score: %d, best move: %d", depth, side_to_move, score, best_move))76--print_board(board)77--print(string.format("----"))78print("Analyzed positions: " .. self.numsearchpos)79end80return score, best_move, game_over81end8283local empty_board = {0,0,0,0,840,0,0,0,850,0,0,0,860,0,0,0} -- 16 empty positions8788----------- helper methods8990function copy_board(board)91local copy = {}92for i = 1, #board do93copy[i] = board[i]94end95return copy96end9798function print_board(board)99gboard = {}100for i = 1, #board do101if board[i] == 0 then gboard[i] = '.'102elseif board[i] == 1 then gboard[i] = 'x'103else gboard[i] = 'o'104end105end106print(string.format("\n%s %s %s %s\n%s %s %s %s\n%s %s %s %s\n%s %s %s %s\n", unpack(gboard)))107end108109function is_board_full(board)110for i = 1, #board do111if board[i] == 0 then112return false113end114end115return true116end117118----------- implement negaMax methods119120negaMax.index_quadruplets = {121{1,2,3,4}, {5,6,7,8}, {9,10,11,12}, -- rows122{13,14,15,16}, {1,5,9,13}, {2,6,10,14}, -- cols123{3,7,11,15}, {4,8,12,16}, {1,6,11,16}, {4,7,10,13}, -- diags124{1,2,5,6}, {2,3,6,7}, {3,4,7,8}, -- squares125{5,6,9,10}, {6,7,10,11}, {7,8,11,12},126{9,10,13,14}, {10,11,14,15}, {11,12,15,16}127}128129function negaMax:evaluate(board, depth) -- return format is score, is_terminal_position130--[[131What can be confusing is how the heuristic value of the current node is calculated. In this implementation, this value is always calculated from the point of view of player A, whose color value is one. In other words, higher heuristic values always represent situations more favorable for player A. This is the same behavior as the normal minimax algorithm. The heuristic value is not necessarily the same as a node's return value due to value negation by negamax and the color parameter. The negamax node's return value is a heuristic score from the point of view of the node's current player.132133Negamax scores match minimax scores for nodes where player A is about to play, and where player A is the maximizing player in the minimax equivalent. Negamax always searches for the maximum value for all its nodes. Hence for player B nodes, the minimax score is a negation of its negamax score. Player B is the minimizing player in the minimax equivalent.134135Variations in negamax implementations may omit the color parameter. In this case, the heuristic evaluation function must return values from the point of view of the node's current player.136--]]137local player_plus_score, player_minus_score = 0, 0138local game_won = false139for _, curr_qdr in pairs(negaMax.index_quadruplets) do -- iterate over all index quadruplets140-- count the empty positions and positions occupied by the side whos move it is141local player_plus_fields, player_minus_fields, empties = 0, 0, 0142for _, index in next, curr_qdr do -- iterate over all indices143if board[index] == 0 then144empties = empties + 1145elseif board[index] == 1 then146player_plus_fields = player_plus_fields + 1147elseif board[index] == -1 then148player_minus_fields = player_minus_fields + 1149end150end151-- evaluate the quadruplets score by looking at empty vs occupied positions152if empties == 3 then153if player_plus_fields == 1 then154player_plus_score = player_plus_score + 3155elseif player_minus_fields == 1 then156player_minus_score = player_minus_score + 3157end158elseif empties == 2 then159if player_plus_fields == 2 then160player_plus_score = player_plus_score + 13161elseif player_minus_fields == 2 then162player_minus_score = player_minus_score + 13163end164elseif empties == 1 then165if player_plus_fields == 3 then166player_plus_score = player_plus_score + 31167elseif player_minus_fields == 3 then168player_minus_score = player_minus_score + 31169end170elseif empties == 0 then171-- check for winning situations172if player_plus_fields == 4 then173player_plus_score = 999-depth174player_minus_score = 0175game_won = true176break177elseif player_minus_fields == 4 then178-- this should not happen if there is a proper terminal node detection!179player_plus_score = 0180player_minus_score = 999-depth181game_won = true182break183end184end185end186-- return format is score, is_terminal_position187if not game_won and is_board_full(board) then188return 0, true -- DRAW189else190return (player_plus_score - player_minus_score), game_won -- >0 is good for player 1 [+], <0 means good for the other player (player 2 [-]))191end192end193194function negaMax:move_candidates(board, side_to_move)195local moves = {}196for i = 1, #board do197if board[i] == 0 then -- empty?198moves[#moves + 1] = i -- save move that was made199end200end201return moves202end203204function negaMax:make_move(board, side_to_move, move)205local copy = copy_board(board)206copy[move] = side_to_move207return copy208end209210local human_player = 1211local AI_player = -human_player212local game_board = copy_board(empty_board)213local curr_move = -1214local curr_player = human_player -- human player goes first215local score = 0216local stop_loop = false217local game_over = false218219negaMax.maxdepth = 5220221local t0 = os.clock()222score, curr_move = negaMax:negaMax(game_board, curr_player)223local t1 = os.clock()224225return t1-t0226end227228bench.runCode(test, "tictactoe")229230231