Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Roblox
GitHub Repository: Roblox/luau
Path: blob/master/bench/tests/tictactoe.lua
2725 views
1
local function prequire(name) local success, result = pcall(require, name); return success and result end
2
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
3
4
function test()
5
6
-- https://github.com/stefandd/Tic4
7
local negaMax = {maxdepth = 4, minsearchpos = 0, numsearchpos = 0}
8
negaMax.__index = negaMax
9
10
function negaMax:evaluate(board, depth)
11
--[[
12
What 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.
13
14
Negamax 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.
15
16
Variations 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.
17
--]]
18
print ("This function needs to be implemented!")
19
end
20
21
function negaMax:move_candidates(board, side_to_move)
22
print ("This function needs to be implemented!")
23
end
24
25
function negaMax:make_move(board, side_to_move, move)
26
print ("This function needs to be implemented!")
27
end
28
29
function negaMax:negaMax(board, side_to_move, depth, alpha, beta) -- side_to_move: e.g. 1 is blue, -1 is read
30
--
31
-- init vars for root call
32
--
33
if not depth then -- root call
34
depth = 0
35
alpha = -math.huge
36
beta = math.huge
37
self.numsearchpos = 0 -- reset call counter
38
end
39
--
40
-- test if the node is terminal (i.e. full board or win)
41
--
42
local best_move = -1
43
local score, is_term_node = self:evaluate(board, depth)
44
-- we abort the recursion if this is a terminal node, or if one of the search abort conditions are met
45
--
46
if is_term_node or depth == self.maxdepth then
47
return side_to_move*score, best_move, is_term_node
48
end
49
--
50
-- if not terminal node, eval child nodes
51
--
52
local moves = self:move_candidates(board, side_to_move)
53
score = -math.huge
54
55
for _, analyzed_move in pairs(moves) do -- iterate over all boards
56
self.numsearchpos = self.numsearchpos + 1
57
local b = self:make_move(board, side_to_move, analyzed_move)
58
local move_score, _, _ = -self:negaMax(b, -side_to_move, depth+1, -beta, -alpha)
59
if move_score > score then
60
score = move_score
61
best_move = analyzed_move
62
end
63
-- disable alpha-beta pruning
64
--
65
alpha = math.max(alpha, score)
66
if alpha >= beta then
67
break
68
end
69
--
70
end
71
if depth == 0 then
72
--
73
-- exit for root call (depth == 0)
74
--
75
-- debug stuff
76
--print(string.format("---- Negamax: node info, depth: %d, side: %d, score: %d, best move: %d", depth, side_to_move, score, best_move))
77
--print_board(board)
78
--print(string.format("----"))
79
print("Analyzed positions: " .. self.numsearchpos)
80
end
81
return score, best_move, game_over
82
end
83
84
local empty_board = {0,0,0,0,
85
0,0,0,0,
86
0,0,0,0,
87
0,0,0,0} -- 16 empty positions
88
89
----------- helper methods
90
91
function copy_board(board)
92
local copy = {}
93
for i = 1, #board do
94
copy[i] = board[i]
95
end
96
return copy
97
end
98
99
function print_board(board)
100
gboard = {}
101
for i = 1, #board do
102
if board[i] == 0 then gboard[i] = '.'
103
elseif board[i] == 1 then gboard[i] = 'x'
104
else gboard[i] = 'o'
105
end
106
end
107
print(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)))
108
end
109
110
function is_board_full(board)
111
for i = 1, #board do
112
if board[i] == 0 then
113
return false
114
end
115
end
116
return true
117
end
118
119
----------- implement negaMax methods
120
121
negaMax.index_quadruplets = {
122
{1,2,3,4}, {5,6,7,8}, {9,10,11,12}, -- rows
123
{13,14,15,16}, {1,5,9,13}, {2,6,10,14}, -- cols
124
{3,7,11,15}, {4,8,12,16}, {1,6,11,16}, {4,7,10,13}, -- diags
125
{1,2,5,6}, {2,3,6,7}, {3,4,7,8}, -- squares
126
{5,6,9,10}, {6,7,10,11}, {7,8,11,12},
127
{9,10,13,14}, {10,11,14,15}, {11,12,15,16}
128
}
129
130
function negaMax:evaluate(board, depth) -- return format is score, is_terminal_position
131
--[[
132
What 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.
133
134
Negamax 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.
135
136
Variations 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.
137
--]]
138
local player_plus_score, player_minus_score = 0, 0
139
local game_won = false
140
for _, curr_qdr in pairs(negaMax.index_quadruplets) do -- iterate over all index quadruplets
141
-- count the empty positions and positions occupied by the side whos move it is
142
local player_plus_fields, player_minus_fields, empties = 0, 0, 0
143
for _, index in next, curr_qdr do -- iterate over all indices
144
if board[index] == 0 then
145
empties = empties + 1
146
elseif board[index] == 1 then
147
player_plus_fields = player_plus_fields + 1
148
elseif board[index] == -1 then
149
player_minus_fields = player_minus_fields + 1
150
end
151
end
152
-- evaluate the quadruplets score by looking at empty vs occupied positions
153
if empties == 3 then
154
if player_plus_fields == 1 then
155
player_plus_score = player_plus_score + 3
156
elseif player_minus_fields == 1 then
157
player_minus_score = player_minus_score + 3
158
end
159
elseif empties == 2 then
160
if player_plus_fields == 2 then
161
player_plus_score = player_plus_score + 13
162
elseif player_minus_fields == 2 then
163
player_minus_score = player_minus_score + 13
164
end
165
elseif empties == 1 then
166
if player_plus_fields == 3 then
167
player_plus_score = player_plus_score + 31
168
elseif player_minus_fields == 3 then
169
player_minus_score = player_minus_score + 31
170
end
171
elseif empties == 0 then
172
-- check for winning situations
173
if player_plus_fields == 4 then
174
player_plus_score = 999-depth
175
player_minus_score = 0
176
game_won = true
177
break
178
elseif player_minus_fields == 4 then
179
-- this should not happen if there is a proper terminal node detection!
180
player_plus_score = 0
181
player_minus_score = 999-depth
182
game_won = true
183
break
184
end
185
end
186
end
187
-- return format is score, is_terminal_position
188
if not game_won and is_board_full(board) then
189
return 0, true -- DRAW
190
else
191
return (player_plus_score - player_minus_score), game_won -- >0 is good for player 1 [+], <0 means good for the other player (player 2 [-]))
192
end
193
end
194
195
function negaMax:move_candidates(board, side_to_move)
196
local moves = {}
197
for i = 1, #board do
198
if board[i] == 0 then -- empty?
199
moves[#moves + 1] = i -- save move that was made
200
end
201
end
202
return moves
203
end
204
205
function negaMax:make_move(board, side_to_move, move)
206
local copy = copy_board(board)
207
copy[move] = side_to_move
208
return copy
209
end
210
211
local human_player = 1
212
local AI_player = -human_player
213
local game_board = copy_board(empty_board)
214
local curr_move = -1
215
local curr_player = human_player -- human player goes first
216
local score = 0
217
local stop_loop = false
218
local game_over = false
219
220
negaMax.maxdepth = 5
221
222
local t0 = os.clock()
223
score, curr_move = negaMax:negaMax(game_board, curr_player)
224
local t1 = os.clock()
225
226
return t1-t0
227
end
228
229
bench.runCode(test, "tictactoe")
230
231