mirror of
https://github.com/luau-lang/luau.git
synced 2024-12-13 21:40:43 +00:00
2e6fdd90a0
Some checks are pending
benchmark / callgrind (map[branch:main name:luau-lang/benchmark-data], ubuntu-22.04) (push) Waiting to run
build / macos (push) Waiting to run
build / macos-arm (push) Waiting to run
build / ubuntu (push) Waiting to run
build / windows (Win32) (push) Waiting to run
build / windows (x64) (push) Waiting to run
build / coverage (push) Waiting to run
build / web (push) Waiting to run
release / macos (push) Waiting to run
release / ubuntu (push) Waiting to run
release / windows (push) Waiting to run
release / web (push) Waiting to run
## New Solver * Type functions should be able to signal whether or not irreducibility is due to an error * Do not generate extra expansion constraint for uninvoked user-defined type functions * Print in a user-defined type function reports as an error instead of logging to stdout * Many e-graphs bugfixes and performance improvements * Many general bugfixes and improvements to the new solver as a whole * Fixed issue with used-defined type functions not being able to call each other * Infer types of globals under new type solver ## Fragment Autocomplete * Miscellaneous fixes to make interop with the old solver better ## Runtime * Support disabling specific built-in functions from being fast-called or constant-evaluated (Closes #1538) * New compiler option `disabledBuiltins` accepts a list of library function names like "tonumber" or "math.cos" * Added constant folding for vector arithmetic * Added constant propagation and type inference for vector globals (Fixes #1511) * New compiler option `librariesWithKnownMembers` accepts a list of libraries for members of which a request for constant value and/or type will be made * `libraryMemberTypeCb` callback is called to get the type of a global, return one of the `LuauBytecodeType` values. 'boolean', 'number', 'string' and 'vector' type are supported. * `libraryMemberConstantCb` callback is called to setup the constant value of a global. To set a value, C API `luau_set_compile_constant_*` or C++ API `setCompileConstant*` functions should be used. --- Co-authored-by: Aaron Weiss <aaronweiss@roblox.com> Co-authored-by: Andy Friesen <afriesen@roblox.com> Co-authored-by: Aviral Goel <agoel@roblox.com> Co-authored-by: Daniel Angel <danielangel@roblox.com> Co-authored-by: Jonathan Kelaty <jkelaty@roblox.com> Co-authored-by: Hunter Goldstein <hgoldstein@roblox.com> Co-authored-by: Varun Saini <vsaini@roblox.com> Co-authored-by: Vighnesh Vijay <vvijay@roblox.com> Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com> --------- Co-authored-by: Aaron Weiss <aaronweiss@roblox.com> Co-authored-by: Alexander McCord <amccord@roblox.com> Co-authored-by: Andy Friesen <afriesen@roblox.com> Co-authored-by: Aviral Goel <agoel@roblox.com> Co-authored-by: David Cope <dcope@roblox.com> Co-authored-by: Lily Brown <lbrown@roblox.com> Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com> Co-authored-by: Junseo Yoo <jyoo@roblox.com> Co-authored-by: Hunter Goldstein <hgoldstein@roblox.com> Co-authored-by: Varun Saini <61795485+vrn-sn@users.noreply.github.com> Co-authored-by: Alexander Youngblood <ayoungblood@roblox.com> Co-authored-by: Varun Saini <vsaini@roblox.com> Co-authored-by: Andrew Miranti <amiranti@roblox.com> Co-authored-by: Shiqi Ai <sai@roblox.com> Co-authored-by: Yohoo Lin <yohoo@roblox.com> Co-authored-by: Daniel Angel <danielangel@roblox.com> Co-authored-by: Jonathan Kelaty <jkelaty@roblox.com>
229 lines
10 KiB
Lua
229 lines
10 KiB
Lua
local function prequire(name) local success, result = pcall(require, name); return success and result end
|
|
local bench = script and require(script.Parent.bench_support) or prequire("bench_support") or require("../bench_support")
|
|
|
|
function test()
|
|
|
|
-- https://github.com/stefandd/Tic4
|
|
local negaMax = {maxdepth = 4, minsearchpos = 0, numsearchpos = 0}
|
|
negaMax.__index = negaMax
|
|
|
|
function negaMax:evaluate(board, depth)
|
|
--[[
|
|
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.
|
|
|
|
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.
|
|
|
|
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.
|
|
--]]
|
|
print ("This function needs to be implemented!")
|
|
end
|
|
|
|
function negaMax:move_candidates(board, side_to_move)
|
|
print ("This function needs to be implemented!")
|
|
end
|
|
|
|
function negaMax:make_move(board, side_to_move, move)
|
|
print ("This function needs to be implemented!")
|
|
end
|
|
|
|
function negaMax:negaMax(board, side_to_move, depth, alpha, beta) -- side_to_move: e.g. 1 is blue, -1 is read
|
|
--
|
|
-- init vars for root call
|
|
--
|
|
if not depth then -- root call
|
|
depth = 0
|
|
alpha = -math.huge
|
|
beta = math.huge
|
|
self.numsearchpos = 0 -- reset call counter
|
|
end
|
|
--
|
|
-- test if the node is terminal (i.e. full board or win)
|
|
--
|
|
local best_move = -1
|
|
local score, is_term_node = self:evaluate(board, depth)
|
|
-- we abort the recursion if this is a terminal node, or if one of the search abort conditions are met
|
|
--
|
|
if is_term_node or depth == self.maxdepth then
|
|
return side_to_move*score, best_move, is_term_node
|
|
end
|
|
--
|
|
-- if not terminal node, eval child nodes
|
|
--
|
|
local moves = self:move_candidates(board, side_to_move)
|
|
score = -math.huge
|
|
|
|
for _, analyzed_move in pairs(moves) do -- iterate over all boards
|
|
self.numsearchpos = self.numsearchpos + 1
|
|
local b = self:make_move(board, side_to_move, analyzed_move)
|
|
local move_score, _, _ = -self:negaMax(b, -side_to_move, depth+1, -beta, -alpha)
|
|
if move_score > score then
|
|
score = move_score
|
|
best_move = analyzed_move
|
|
end
|
|
-- disable alpha-beta pruning
|
|
--
|
|
alpha = math.max(alpha, score)
|
|
if alpha >= beta then
|
|
break
|
|
end
|
|
--
|
|
end
|
|
if depth == 0 then
|
|
--
|
|
-- exit for root call (depth == 0)
|
|
--
|
|
-- debug stuff
|
|
--print(string.format("---- Negamax: node info, depth: %d, side: %d, score: %d, best move: %d", depth, side_to_move, score, best_move))
|
|
--print_board(board)
|
|
--print(string.format("----"))
|
|
print("Analyzed positions: " .. self.numsearchpos)
|
|
end
|
|
return score, best_move, game_over
|
|
end
|
|
|
|
local empty_board = {0,0,0,0,
|
|
0,0,0,0,
|
|
0,0,0,0,
|
|
0,0,0,0} -- 16 empty positions
|
|
|
|
----------- helper methods
|
|
|
|
function copy_board(board)
|
|
local copy = {}
|
|
for i = 1, #board do
|
|
copy[i] = board[i]
|
|
end
|
|
return copy
|
|
end
|
|
|
|
function print_board(board)
|
|
gboard = {}
|
|
for i = 1, #board do
|
|
if board[i] == 0 then gboard[i] = '.'
|
|
elseif board[i] == 1 then gboard[i] = 'x'
|
|
else gboard[i] = 'o'
|
|
end
|
|
end
|
|
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)))
|
|
end
|
|
|
|
function is_board_full(board)
|
|
for i = 1, #board do
|
|
if board[i] == 0 then
|
|
return false
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
----------- implement negaMax methods
|
|
|
|
negaMax.index_quadruplets = {
|
|
{1,2,3,4}, {5,6,7,8}, {9,10,11,12}, -- rows
|
|
{13,14,15,16}, {1,5,9,13}, {2,6,10,14}, -- cols
|
|
{3,7,11,15}, {4,8,12,16}, {1,6,11,16}, {4,7,10,13}, -- diags
|
|
{1,2,5,6}, {2,3,6,7}, {3,4,7,8}, -- squares
|
|
{5,6,9,10}, {6,7,10,11}, {7,8,11,12},
|
|
{9,10,13,14}, {10,11,14,15}, {11,12,15,16}
|
|
}
|
|
|
|
function negaMax:evaluate(board, depth) -- return format is score, is_terminal_position
|
|
--[[
|
|
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.
|
|
|
|
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.
|
|
|
|
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.
|
|
--]]
|
|
local player_plus_score, player_minus_score = 0, 0
|
|
local game_won = false
|
|
for _, curr_qdr in pairs(negaMax.index_quadruplets) do -- iterate over all index quadruplets
|
|
-- count the empty positions and positions occupied by the side whos move it is
|
|
local player_plus_fields, player_minus_fields, empties = 0, 0, 0
|
|
for _, index in next, curr_qdr do -- iterate over all indices
|
|
if board[index] == 0 then
|
|
empties = empties + 1
|
|
elseif board[index] == 1 then
|
|
player_plus_fields = player_plus_fields + 1
|
|
elseif board[index] == -1 then
|
|
player_minus_fields = player_minus_fields + 1
|
|
end
|
|
end
|
|
-- evaluate the quadruplets score by looking at empty vs occupied positions
|
|
if empties == 3 then
|
|
if player_plus_fields == 1 then
|
|
player_plus_score = player_plus_score + 3
|
|
elseif player_minus_fields == 1 then
|
|
player_minus_score = player_minus_score + 3
|
|
end
|
|
elseif empties == 2 then
|
|
if player_plus_fields == 2 then
|
|
player_plus_score = player_plus_score + 13
|
|
elseif player_minus_fields == 2 then
|
|
player_minus_score = player_minus_score + 13
|
|
end
|
|
elseif empties == 1 then
|
|
if player_plus_fields == 3 then
|
|
player_plus_score = player_plus_score + 31
|
|
elseif player_minus_fields == 3 then
|
|
player_minus_score = player_minus_score + 31
|
|
end
|
|
elseif empties == 0 then
|
|
-- check for winning situations
|
|
if player_plus_fields == 4 then
|
|
player_plus_score = 999-depth
|
|
player_minus_score = 0
|
|
game_won = true
|
|
break
|
|
elseif player_minus_fields == 4 then
|
|
-- this should not happen if there is a proper terminal node detection!
|
|
player_plus_score = 0
|
|
player_minus_score = 999-depth
|
|
game_won = true
|
|
break
|
|
end
|
|
end
|
|
end
|
|
-- return format is score, is_terminal_position
|
|
if not game_won and is_board_full(board) then
|
|
return 0, true -- DRAW
|
|
else
|
|
return (player_plus_score - player_minus_score), game_won -- >0 is good for player 1 [+], <0 means good for the other player (player 2 [-]))
|
|
end
|
|
end
|
|
|
|
function negaMax:move_candidates(board, side_to_move)
|
|
local moves = {}
|
|
for i = 1, #board do
|
|
if board[i] == 0 then -- empty?
|
|
moves[#moves + 1] = i -- save move that was made
|
|
end
|
|
end
|
|
return moves
|
|
end
|
|
|
|
function negaMax:make_move(board, side_to_move, move)
|
|
local copy = copy_board(board)
|
|
copy[move] = side_to_move
|
|
return copy
|
|
end
|
|
|
|
local human_player = 1
|
|
local AI_player = -human_player
|
|
local game_board = copy_board(empty_board)
|
|
local curr_move = -1
|
|
local curr_player = human_player -- human player goes first
|
|
local score = 0
|
|
local stop_loop = false
|
|
local game_over = false
|
|
|
|
negaMax.maxdepth = 5
|
|
|
|
local t0 = os.clock()
|
|
score, curr_move = negaMax:negaMax(game_board, curr_player)
|
|
local t1 = os.clock()
|
|
|
|
return t1-t0
|
|
end
|
|
|
|
bench.runCode(test, "tictactoe")
|