Chess Engine generates 500000 moves per second

I don’t really expect much, the code is working perfectly and also did some perft test with different positions and got the same results from the chessprogramming wiki.

This is actually my third engine the last two where using 2d array which was slower and also had a lot of problems with the perft result not getting the same result and currently I am using 1d array meaning its just array[64], I’m not really sure how I would increase the speed of move generation.

I created a plugin for the perft test since roblox does not allow multiple recrution. I am currently looking for ideas of how I would increase the speed of the move generation so the alpha beta(nega max) pruning would be able to reach deeper depth. Feel free testing the game out if you found anything please let me know
Chess.rbxl (72.8 KB)

Code
local Move = require(game.ReplicatedStorage.ModuleScripts.PieceMove)
local insert = table.insert
local remove = table.remove
local find = table.find

local floor = math.floor
local abs = math.abs
local min = math.min

local ChessEngine = {}

function ChessEngine:GameState(fen : string)
	local self = {
		FenString = fen,
		
		Checkmate = false,
		Stalemate = false,
		InCheck = false,
		Draw = false,
		
		Piece = {
			King = "K",
			Pawn = "P",
			Knight = "N",
			Bishop = "B",
			Rook = "R",
			Queen = "Q",
			
			White = "w",
			Black = "b"
		},
		
		Pins = {},
		Board = {},
		Checks = {},
		MoveLog = {},
		CastleRightLog = {},
		NumSquareToEdge = {},
		PawnSquareInEdge = {},
		KnightSquareInEdge = {},
		EnpassantPossibleLog = {},
		PromotionTypes = {"Q", "R", "B", "N"},
		DirectionOffsets = {-1, 1, 8, -8, 7, -7, 9, -9},
		KnightMoves = {15, 6, -10, -17, -15, -6, 10, 17},
	}
	
	function self.Initialize()
		self.ImportFen()
		self.PrecomputedMoveData()
		self.PrecomputedPawnMoveData()
		self.PrecumputedKnightMoveData()
		
		self.PieceMoveFunctions = {
			P = self.GetPawnMoves,
			K = self.GetKingMoves,
			N = self.GetKnightMoves,
			B = self.GetSlidingMoves,
			R = self.GetSlidingMoves,
			Q = self.GetSlidingMoves
		}
	end
	
	function self.ImportFen()
		local pieceTypeFromSymbol = {
			k = self.Piece.King,
			p = self.Piece.Pawn,
			n = self.Piece.Knight,
			b = self.Piece.Bishop,
			r = self.Piece.Rook,
			q = self.Piece.Queen
		}
		
		local fen = self.FenString:split(" ")
		
		local fenBoard = fen[1]:split("")
		local fenTurn = fen[2]
		local fenCastling = fen[3]:split("")
		local fenEnpassant = fen[4]:split("")
		local fen50Rule = tonumber(fen[5])
		
		local file, rank = 1, 7
		local tempEnpassant = {}
		local tempCastleRight = {false, false, false, false}
		local ranksToRows = {0, 1, 2, 3, 4, 5, 6, 7}
		local filesToCols = {
			a = 1,
			b = 2,
			c = 3,
			d = 4,
			e = 5,
			f = 6,
			g = 7,
			h = 8
		}
		
		for _ = 1, 64 do
			insert(self.Board, "--")
		end
		
		for _, char in fenBoard do
			if char ~= "/" then
				local isNumber = tonumber(char)
				
				if isNumber then
					file += isNumber
				else
					local pieceType = pieceTypeFromSymbol[char:lower()]
					local pieceColor = char:upper() == char and self.Piece.White or self.Piece.Black
					local square = rank * 8 + file
					
					if char == "K" then
						self.WhiteKingLocation = square
					elseif char == "k" then
						self.BlackKingLocation = square
					end
					
					self.Board[square] = pieceColor..pieceType
					file += 1
				end
			else
				file = 1
				rank -= 1
			end
		end
		
		if fenTurn == "w" then
			self.WhiteToMove = true
		elseif fenTurn == "b" then
			self.WhiteToMove = false
		end
		
		for _, char in fenCastling do
			if char == "K" then
				tempCastleRight[1] = true
			elseif char == "Q" then
				tempCastleRight[2] = true
			elseif char == "k" then
				tempCastleRight[3] = true
			elseif char == "q" then
				tempCastleRight[4] = true
			end
		end
		
		if fenEnpassant[1] ~= "-" then
			self.EnpassantPossible = ranksToRows[tonumber(fenEnpassant[2])] * 8 + filesToCols[fenEnpassant[1]]
		else
			self.EnpassantPossible = 0
		end
		
		self.CurrentCastleRights = ChessEngine:CastleRights(
			tempCastleRight[1], tempCastleRight[2],
			tempCastleRight[3], tempCastleRight[4]
		)
		self.CastleRightLog.append(self.CurrentCastleRights)
		self.EnpassantPossibleLog.append(self.EnpassantPossible)
	end
	
	function self.FlipTheBoard()
		local board = {}
		
		for i = 1, floor(#self.Board / 2) do
			local j = #self.Board - i + 1
			
			board[i], board[j] = self.Board[j], self.Board[i]
		end
		
		return board
	end
	
	function self.FlipValidMoves()
		local moves = self.GetValidMoves()
		
		for _, move in moves do
			move.SqStart = abs(move.SqStart - 65)
			move.SqTarget = abs(move.SqTarget - 65)
		end
		
		return moves
	end
	
	function self.MakeMove(move : any)
		if move.SqStart == move.SqTarget then return end
		
		self.Board[move.SqStart] = "--"
		self.Board[move.SqTarget] = move.PieceMoved
		self.WhiteToMove = not self.WhiteToMove
		
		if move.PieceMoved == "wK" then
			self.WhiteKingLocation = move.SqTarget
		elseif move.PieceMoved == "bK" then
			self.BlackKingLocation = move.SqTarget
		end
		
		if move.PieceMoved:split("")[2] == "P" and abs(move.SqStart - move.SqTarget) == 16 then
			self.EnpassantPossible = floor((move.SqStart + move.SqTarget) / 2)
		else
			self.EnpassantPossible = 0
		end
		
		if move.IsEnpassantMove then
			self.Board[move.EnpassantTarget] = "--"
		elseif move.IsPawnPromotion then
			self.Board[move.SqTarget] = move.PieceMoved:split("")[1]..move.PromotionType
		elseif move.IsCastleMove then
			self.Board[move.RookNewSquare] = self.Board[move.RookCurrentSquare]
			self.Board[move.RookCurrentSquare] = "--"
		end
		
		self.UpdateCastleRights(move)
		self.MoveLog.append(move)
		self.CastleRightLog.append(self.CurrentCastleRights)
		self.EnpassantPossibleLog.append(self.EnpassantPossible)
	end
	
	function self.UndoMove()
		if #self.MoveLog ~= 0 then
			local move = self.MoveLog.pop()
			
			self.Board[move.SqStart] = move.PieceMoved
			self.Board[move.SqTarget] = move.PieceCaptured
			self.WhiteToMove = not self.WhiteToMove
			self.Checkmate = false
			self.Stalemate = false
			self.Draw = false
			
			if move.PieceMoved == "wK" then
				self.WhiteKingLocation = move.SqStart
			elseif move.PieceMoved == "bK" then
				self.BlackKingLocation = move.SqStart
			end
			
			if move.IsEnpassantMove then
				self.Board[move.EnpassantTarget] = move.PieceCaptured
				self.Board[move.SqTarget] = "--"
			elseif move.IsCastleMove then
				self.Board[move.RookCurrentSquare] = self.Board[move.RookNewSquare]
				self.Board[move.RookNewSquare] = "--"
			end
			
			self.CurrentCastleRights = self.CastleRightLog.pop()
			self.EnpassantPossible = self.EnpassantPossibleLog.pop()
		end
	end
	
	function self.MoveLog.pop() : any
		return remove(self.MoveLog, #self.MoveLog)
	end
	
	function self.EnpassantPossibleLog.pop() : any
		remove(self.EnpassantPossibleLog, #self.EnpassantPossibleLog)
		
		return self.EnpassantPossibleLog[#self.EnpassantPossibleLog]
	end
	
	function self.CastleRightLog.pop() : any
		remove(self.CastleRightLog, #self.CastleRightLog)
		local castleRight = self.CastleRightLog[#self.CastleRightLog]
		
		castleRight = ChessEngine:CastleRights(
			castleRight.wK, castleRight.wQ,
			castleRight.bK, castleRight.bQ
		)
		
		return castleRight
	end
	
	function self.MoveLog.append(move : any)
		insert(self.MoveLog, move)
	end
	
	function self.EnpassantPossibleLog.append(enpassantPossible : number)
		insert(self.EnpassantPossibleLog, enpassantPossible)
	end
	
	function self.CastleRightLog.append(castleRight : any)
		insert(self.CastleRightLog, ChessEngine:CastleRights(
			castleRight.wK, castleRight.wQ,
			castleRight.bK, castleRight.bQ
		))
	end
	
	function self.UpdateCastleRights(move : any)
		local pieceMoved = move.PieceMoved
		
		if pieceMoved == "wK" then
			self.CurrentCastleRights.wK = false
			self.CurrentCastleRights.wQ = false
		elseif pieceMoved == "bK" then
			self.CurrentCastleRights.bK = false
			self.CurrentCastleRights.bQ = false
		elseif pieceMoved == "wR" then
			if move.SqStart == 8 then
				self.CurrentCastleRights.wK = false
			elseif move.SqStart == 1 then
				self.CurrentCastleRights.wQ = false
			end
		elseif pieceMoved == "bR" then
			if move.SqStart == 64 then
				self.CurrentCastleRights.bK = false
			elseif move.SqStart == 57 then
				self.CurrentCastleRights.bQ = false
			end
		end
	end
	
	function self.GetValidMoves(debugging : boolean) : any
		local moves = {}
		local sqStart = self.WhiteToMove and self.WhiteKingLocation or self.BlackKingLocation
		self.InCheck, self.Pins, self.Checks = self.CheckForPossiblePins()
		
		if self.InCheck then
			if #self.Checks == 1 then
				moves = self.GetAllPossibleMoves()
				
				local check = self.Checks[1]
				local checkSquare = check[1]
				local pieceChecking = self.Board[checkSquare]
				local piece = pieceChecking:split("")
				local validSquares = {}
				
				if piece[2] == "N" then
					validSquares = {checkSquare}
				else
					for i = 1, 8 do
						local validSquare = sqStart + check[2] * i
						insert(validSquares, validSquare)
						
						if validSquare == checkSquare then
							break
						end
					end
				end
				
				for i = #moves, 1, -1 do
					local move = moves[i]
					
					if move.PieceMoved:split("")[2] ~= "K" then
						if not find(validSquares, move.SqTarget) then
							if move.IsEnpassantMove then
								local moveAmount = self.WhiteToMove and -8 or 8
								
								if move.SqTarget + moveAmount ~= checkSquare then
									remove(moves, i)
								end
							else
								remove(moves, i)
							end
						end
					end
				end
			else
				self.GetKingMoves(sqStart, "K", moves)
			end
		else
			moves = self.GetAllPossibleMoves()
		end
		
		if #moves == 0 then
			if self.InCheck then
				self.Checkmate = true
				--print("checkmate")
			else
				self.Stalemate = true
				--print("stalemate")
			end
		else
			if not debugging then
				for _, move in moves do
					self.MakeMove(move)
					local nextMoves = self.GetValidMoves(true)
					
					if #nextMoves == 0 then
						if self.Checkmate then
							self.Checkmate = false
							move.Checkmate = true
						elseif self.Stalemate then
							self.Checkmate = false
							move.Stalemate = true
						end
					elseif self.InCheck then
						self.InCheck = false
						move.Check = true
					end
					
					self.UndoMove(move)
				end
				
				self.Checkmate = false
				self.Stalemate = false
			end
		end
		
		return moves
	end
	
	function self.CheckForPossiblePins() : any
		local pins, checks = {}, {}
		local inCheck = false
		local sqStart, ally, enemy
		
		if self.WhiteToMove then
			sqStart = self.WhiteKingLocation
			enemy = "b"
			ally = "w"
		else
			sqStart = self.BlackKingLocation
			enemy = "w"
			ally = "b"
		end
		
		for directionIndex, direction in self.DirectionOffsets do
			if self.NumSquareToEdge[sqStart][directionIndex] == 0 then continue end
			local possiblePin = {}
			
			for n = 1, self.NumSquareToEdge[sqStart][directionIndex] do
				local sqTarget = sqStart + self.DirectionOffsets[directionIndex] * n
				local pieceOnTargetSquare = self.Board[sqTarget]
				local endPiece = pieceOnTargetSquare:split("")
				
				if endPiece[1] == ally and endPiece[2] ~= "K" then
					if #possiblePin == 0 then
						possiblePin = {sqTarget, direction}
					else 
						break
					end
				elseif endPiece[1] == enemy then
					local Type = endPiece[2]
					
					if (Type == "R" and (directionIndex >= 1 and directionIndex <= 4)) or
						(Type == "B" and (directionIndex >= 5 and directionIndex <= 8)) or
						(Type == "P" and n == 1 and
						((ally == "w" and (directionIndex == 5 or directionIndex == 7)) or
						((ally == "b" and (directionIndex == 6 or directionIndex == 8))))) or
						(Type == "Q") or (Type == "K" and n == 1) then
						
						if #possiblePin == 0 then
							inCheck = true
							insert(checks, {sqTarget, direction})
							break
						else
							insert(pins, possiblePin)
						end
					else
						break
					end
				end
			end
		end
		
		for directionIndex, moveOffset in self.KnightMoves do
			if self.KnightSquareInEdge[sqStart][directionIndex] then
				local sqTarget = sqStart + moveOffset
				local pieceOnTargetSquare = self.Board[sqTarget]
				local endPiece = pieceOnTargetSquare:split("")
				
				if endPiece[1] == enemy and endPiece[2] == "N" then
					inCheck = true
					insert(checks, {sqTarget, moveOffset})
				end
			end
		end
		
		return inCheck, pins, checks
	end
	
	function self.CheckSquareUnderAttack(sqStart : number, ally : string) : boolean
		local enemy = ally == "w" and "b" or "w"
		
		for directionIndex, direction in self.DirectionOffsets do
			if self.NumSquareToEdge[sqStart][directionIndex] == 0 then continue end
			local possiblePin = {}
			
			for n = 1, self.NumSquareToEdge[sqStart][directionIndex] do
				local sqTarget = sqStart + self.DirectionOffsets[directionIndex] * n
				local pieceOnTargetSquare = self.Board[sqTarget]
				local endPiece = pieceOnTargetSquare:split("")
				
				if endPiece[1] == ally then
					break
				elseif endPiece[1] == enemy then
					local Type = endPiece[2]
					
					if (Type == "R" and (directionIndex >= 1 and directionIndex <= 4)) or
						(Type == "B" and (directionIndex >= 5 and directionIndex <= 8)) or
						(Type == "P" and n == 1 and
						((ally == "w" and (directionIndex == 5 or directionIndex == 7)) or
						((ally == "b" and (directionIndex == 6 or directionIndex == 8))))) or
						(Type == "Q") or (Type == "K" and n == 1) then
						
						return true
					else
						break
					end
				end
			end
		end
		
		for directionIndex, moveOffset in self.KnightMoves do
			if self.KnightSquareInEdge[sqStart][directionIndex] then
				local sqTarget = sqStart + moveOffset
				local pieceOnTargetSquare = self.Board[sqTarget]
				local endPiece = pieceOnTargetSquare:split("")
				
				if endPiece[1] == enemy and endPiece[2] == "N" then
					return true
				end
			end
		end
		
		return false
	end
	
	function self.GetAllPossibleMoves() : any
		local moves = {}
		
		for i, piece in self.Board do
			local piece = piece:split("")
			
			if (self.WhiteToMove and piece[1] == "w") or (not self.WhiteToMove and piece[1] == "b") then
				self.PieceMoveFunctions[piece[2]](i, piece[2], moves)
			end
		end
		
		return moves
	end
	
	function self.PrecomputedMoveData()
		for file = 0, 7 do
			for rank = 0, 7 do
				local numNorth = 7 - rank
				local numSouth = rank
				local numWest = file
				local numEast = 7 - file
				
				local squareIndex = rank * 8 + (file + 1)
				
				self.NumSquareToEdge[squareIndex] = {
					numWest,
					numEast,
					numNorth,
					numSouth,
					min(numNorth, numWest),
					min(numSouth, numEast),
					min(numNorth, numEast),
					min(numSouth, numWest)
				}
			end
		end
	end
	
	function self.PrecomputedPawnMoveData()
		for row = 0, 7 do
			for col = 1, 8 do
				local squareIndex = row * 8 + col
				
				if col == 1 then
					self.PawnSquareInEdge[squareIndex] = {true, false}
				elseif col == 8 then
					self.PawnSquareInEdge[squareIndex] = {false, true}
				else
					self.PawnSquareInEdge[squareIndex] = {false, false}
				end
			end
		end
	end
	
	function self.PrecumputedKnightMoveData()
		for row = 0, 7 do
			for col = 1, 8 do
				local squareIndex = row * 8 + col
				
				local north1 = row + 1
				local south1 = row - 1
				local north2 = row + 2
				local south2 = row - 2
				local west1 = col - 1
				local east1 = col + 1
				local west2 = col - 2
				local east2 = col + 2
				
				north1 = (north1 >= 0 and north1 <= 7)
				south1 = (south1 >= 0 and south1 <= 7)
				north2 = (north2 >= 0 and north2 <= 7)
				south2 = (south2 >= 0 and south2 <= 7)
				west1 = (west1 >= 1 and west1 <= 8)
				east1 = (east1 >= 1 and east1 <= 8)
				west2 = (west2 >= 1 and west2 <= 8)
				east2 = (east2 >= 1 and east2 <= 8)
				
				self.KnightSquareInEdge[squareIndex] = {
					(north2 and west1),
					(north1 and west2),
					(south1 and west2),
					(south2 and west1),
					(south2 and east1),
					(south1 and east2),
					(north1 and east2),
					(north2 and east1)
				}
				
			end
		end
	end
	
	function self.GetSlidingMoves(sqStart : number, piece : string, moves : any)
		local startDirIndex = piece == "B" and 5 or 1
		local endDirIndex = piece == "R" and 4 or 8
		local piecePinned = false
		local pinDirection = 0
		local enemy, ally
		
		if self.WhiteToMove then
			ally = "w"
			enemy = "b"
		else
			ally = "b"
			enemy = "w"
		end
		
		for i, pin in self.Pins do
			if pin[1] == sqStart then
				piecePinned = true
				pinDirection = pin[2]
				remove(self.Pins, i)
				
				break
			end
		end
		
		for directionIndex = startDirIndex, endDirIndex do
			if self.NumSquareToEdge[sqStart][directionIndex] == 0 then continue end
			
			for n = 1, self.NumSquareToEdge[sqStart][directionIndex] do
				if not piecePinned or pinDirection == self.DirectionOffsets[directionIndex] or pinDirection == -self.DirectionOffsets[directionIndex] then
					local sqTarget = sqStart + self.DirectionOffsets[directionIndex] * n
					local pieceOnTargetSquare = self.Board[sqTarget]
					
					if pieceOnTargetSquare:split("")[1] == ally then
						break
					end
					
					insert(moves, Move:Add(sqStart, sqTarget, self.Board))
					
					if pieceOnTargetSquare:split("")[1] == enemy then
						break
					end
				end
			end
		end
	end
	
	function self.GetKingMoves(sqStart : number, piece : string, moves : any)
		local enemy, ally
		
		if self.WhiteToMove then
			enemy = "b"
			ally = "w"
		else
			enemy = "w"
			ally = "b"
		end
		
		for directionIndex = 1, 8 do
			if not (self.NumSquareToEdge[sqStart][directionIndex] == 0) then
				local sqTarget = sqStart + self.DirectionOffsets[directionIndex]
				local pieceOnTargetSquare = self.Board[sqTarget]
				
				if pieceOnTargetSquare:split("")[1] == ally then
					continue
				end
				
				if ally == "w" then
					self.WhiteKingLocation = sqTarget
				else
					self.BlackKingLocation = sqTarget
				end
				
				local inCheck, pins, checks = self.CheckForPossiblePins()
				
				if not inCheck then
					insert(moves, Move:Add(sqStart, sqTarget, self.Board))
				end
				
				if ally == "w" then
					self.WhiteKingLocation = sqStart
				else
					self.BlackKingLocation = sqStart
				end
			end
		end
		
		self.GetCastleMoves(sqStart, ally, moves)
	end
	
	function self.GetCastleMoves(sqStart : number, ally : string, moves : any)
		local inCheck = self.CheckSquareUnderAttack(sqStart, ally)
		
		if inCheck then return end
		
		if (self.WhiteToMove and self.CurrentCastleRights.wK) or (not self.WhiteToMove and self.CurrentCastleRights.bK) then
			self.GetKingSideCaslteMove(sqStart, ally, moves)
		end
		
		if (self.WhiteToMove and self.CurrentCastleRights.wQ) or (not self.WhiteToMove and self.CurrentCastleRights.bQ) then
			self.GetQueenSideCaslteMove(sqStart, ally, moves)
		end
	end
	
	function self.GetKingSideCaslteMove(sqStart : number, ally : string, moves : any)
		if self.Board[sqStart + 1] == "--" and self.Board[sqStart + 2] == "--" and self.Board[sqStart + 3]:split("")[1] == ally then
			if not self.CheckSquareUnderAttack(sqStart + 1, ally) and not self.CheckSquareUnderAttack(sqStart + 2, ally) then
				local move = Move:Add(sqStart, sqStart + 2, self.Board, true)
				move.RookCurrentSquare = sqStart + 3
				move.RookNewSquare = sqStart + 1
				
				insert(moves, move)
			end
		end
	end
	
	function self.GetQueenSideCaslteMove(sqStart : number, ally : string, moves : any)
		if self.Board[sqStart - 1] == "--" and self.Board[sqStart - 2] == "--" and self.Board[sqStart - 3] == "--" and self.Board[sqStart - 4]:split("")[1] == ally then
			if not self.CheckSquareUnderAttack(sqStart - 1, ally) and not self.CheckSquareUnderAttack(sqStart - 2, ally) then
				local move = Move:Add(sqStart, sqStart - 2, self.Board, true)
				move.RookCurrentSquare = sqStart - 4
				move.RookNewSquare = sqStart - 1
				
				insert(moves, move)
			end
		end
	end
	
	function self.GetKnightMoves(sqStart : number, piece : string, moves : any)
		local piecePinned = false
		local enemy, ally
		
		if self.WhiteToMove then
			ally = "w"
			enemy = "b"
		else
			ally = "b"
			enemy = "w"
		end
		
		for i, pin in self.Pins do
			if pin[1] == sqStart then
				piecePinned = true
				remove(self.Pins, i)
				break
			end
		end
		
		for directionIndex, moveOffset in self.KnightMoves do
			if self.KnightSquareInEdge[sqStart][directionIndex] then
				if not piecePinned then
					local sqTarget = sqStart + moveOffset
					local pieceOnTargetSquare = self.Board[sqTarget]
					
					if pieceOnTargetSquare:split("")[1] == ally then
						continue
					end
					
					insert(moves, Move:Add(sqStart, sqTarget, self.Board))
				end
			end
		end
	end
	
	function self.GetPawnMoves(sqStart : number, piece : string, moves : any)
		local kingMinSquare, kingMaxSquare, kingLocation, kingInRowOfEnpassant, canDoublePush, moveAmount, enemy, ally
		local piecePinned = false
		local pinDirection = 0
		
		if self.WhiteToMove then
			ally = "w"
			enemy = "b"
			moveAmount = 8
			kingMinSquare = 33
			kingMaxSquare = 40
			kingLocation = self.WhiteKingLocation
			canDoublePush = (sqStart > 8 and sqStart <= 16)
			kingInRowOfEnpassant = (kingLocation >= kingMinSquare and kingLocation <= kingMaxSquare)
		else
			ally = "b"
			enemy = "w"
			moveAmount = -8
			kingMinSquare = 25
			kingMaxSquare = 32
			kingLocation = self.BlackKingLocation
			canDoublePush = (sqStart > 48 and sqStart <= 56)
			kingInRowOfEnpassant = (kingLocation >= kingMinSquare and kingLocation <= kingMaxSquare)
		end
		
		local singlePush = sqStart + moveAmount
		local doublePush = sqStart + 2 * moveAmount
		
		for i, pin in self.Pins do
			if pin[1] == sqStart then
				piecePinned = true
				pinDirection = pin[2]
				remove(self.Pins, i)
				break
			end
		end
		
		if self.Board[singlePush] == "--" then
			if not piecePinned or sqStart - pinDirection == singlePush or sqStart + pinDirection == singlePush then
				if (self.WhiteToMove and (singlePush > 55 and singlePush <= 64)) or (not self.WhiteToMove and (singlePush > 0 and singlePush <= 8)) then
					for i, piece in self.PromotionTypes do
						local move = Move:Add(sqStart, singlePush, self.Board)
						move.IsPawnPromotion = true
						move.PromotionType = piece
						
						insert(moves, move)
					end
				else
					insert(moves, Move:Add(sqStart, singlePush, self.Board))
					
					if canDoublePush and self.Board[doublePush] == "--" then
						insert(moves, Move:Add(sqStart, doublePush, self.Board))
					end
				end
			end
		end
		
		for directionIndex = 1, 2 do
			local attackDirection = self.DirectionOffsets[directionIndex]
			local sqTarget = sqStart + moveAmount + attackDirection
			
			if (attackDirection == -1 and self.PawnSquareInEdge[sqStart][1]) or (attackDirection == 1 and self.PawnSquareInEdge[sqStart][2]) then continue end
			
			if not piecePinned or sqStart + pinDirection == sqTarget then
				if self.Board[sqTarget]:split("")[1] == enemy then
					if (self.WhiteToMove and (sqTarget > 55 and sqTarget <= 64)) or (not self.WhiteToMove and (sqTarget > 0 and sqTarget <= 8)) then
						for i, piece in self.PromotionTypes do
							local move = Move:Add(sqStart, sqTarget, self.Board)
							move.IsPawnPromotion = true
							move.PromotionType = piece
							
							insert(moves, move)
						end
					else
						insert(moves, Move:Add(sqStart, sqTarget, self.Board))
					end
				end
				
				if self.EnpassantPossible == sqTarget and self.Board[self.EnpassantPossible - moveAmount]:split("")[1] == enemy then
					local attackingPiece, blockingPiece = false, false
					local inRange = {}
					local outRange = {}
					
					if kingInRowOfEnpassant then
						if attackDirection == 1 then
							if kingLocation < sqStart then
								inRange = {kingLocation, sqStart - 1, 1}
								outRange = {sqStart + 2, kingMaxSquare, 1}
							else
								inRange = {kingLocation, sqStart + 2, -1}
								outRange = {sqStart - 1, kingMinSquare, -1}
							end
						else
							if kingLocation < sqStart then
								inRange = {kingLocation, sqStart - 2, 1}
								outRange = {sqStart + 1, kingMaxSquare, 1}
							else
								inRange = {kingLocation, sqStart + 1, -1}
								outRange = {sqStart - 2, kingMinSquare, -1}
							end
						end
						
						for i = inRange[1], inRange[2], inRange[3] do
							local piece = self.Board[i]
							
							if piece ~= "--" and piece ~= ally.."K" then
								blockingPiece = true
							end
						end
						
						for i = outRange[1], outRange[2], outRange[3] do
							local piece = self.Board[i]:split("")
							
							if piece[1] == enemy and (piece[2] == "Q" or piece[2] == "R") then
								attackingPiece = true
								break
							elseif piece[1] ~= "-" then
								blockingPiece = true
								break
							end
						end
					end
					
					if not attackingPiece or blockingPiece then
						local move = Move:Add(sqStart, sqTarget, self.Board, true)
						move.EnpassantTarget = self.EnpassantPossible - moveAmount
						
						insert(moves, move)
					end
				end
			end
		end
	end
	
	self.Initialize()
	return self
end

function ChessEngine:CastleRights(wK : boolean, wQ : boolean, bK : boolean, bQ : boolean)
	local self = {
		wK = wK,
		wQ = wQ,
		bK = bK,
		bQ = bQ
	}
	
	return self
end

return ChessEngine
6 Likes

Absolutely amazing that you made this, lol the number of if statements are making my head hurt. This is so interesting though! Thank you!

3 Likes

Really cool! Although, I recommend you to not “localize” libraries as luau already optimizes these kind of stuff. . more info can be found here

3 Likes

While global access for library functions can be optimized in a similar way, this optimization breaks down when the global table is using sandboxing through metatables, and even when globals aren’t sandboxed, math.max still requires two table accesses.

It’s always possible to “localize” the global accesses by using local max = math.max , but this is cumbersome - in practice it’s easy to forget to apply this optimization. To avoid relying on programmers remembering to do this, Luau implements a special optimization called “imports”, where most global chains such as math.max are resolved when the script is loaded instead of when the script is executed.

Thanks, I always thought that localizing libraries improves the speed of not needing to access two tables, because there was a document that I read that localizing global function improves the speed, looking back the document was actually from 2008 so it was outdated.

2 Likes

So I have notice that the engine have become slower, even just a few thousand moves took a second I might change the title to 10000 moves per second. After a few days I made another engine which is now my forth but this time instead of comparing strings which holds a lot of bytes, I just compared them with 0 and 1 which holds 0 for 0000 bytes and 1 for 0001 bytes in other words its just 0 and 1 yeah…; anyway this time it’s like 10-15x faster; comparison with the depth of 5 6 and 7:

Fen: 8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1

Old Engine
e2e3: 45326
g2g3: 14747
a5a6: 59028
e2e4: 36889
g2g4: 53895
b4b1: 69665
b4b2: 48498
b4b3: 59719
b4a4: 45591
b4c4: 63781
b4d4: 59574
b4e4: 54192
b4f4: 10776
a5a4: 52943
Depth: 5	Nodes: 674624	Time: 69995ms or 69.995s

-- Didn't bother generating depth 6 and 7
-- it was already more than 10 minutes so I just end task.
Current Engine
e2e3: 45326
e2e4: 36889
g2g3: 14747
g2g4: 53895
a5a6: 59028
a5a4: 52943
b4a4: 45591
b4c4: 63781
b4d4: 59574
b4e4: 54192
b4f4: 10776
b4b3: 59719
b4b2: 48498
b4b1: 69665
Depth: 5	Nodes: 674624	Time: 4917ms or 4.917s

e2e3: 745505
e2e4: 597519
g2g3: 271220
g2g4: 892781
a5a6: 968724
a5a4: 868162
b4a4: 745667
b4c4: 1027199
b4d4: 957108
b4e4: 860971
b4f4: 174919
b4b3: 941129
b4b2: 818501
b4b1: 1160678
Depth: 6	Nodes: 11030083	Time: 82472ms or 82.472s or 1.374m

e2e3: 11427551
e2e4: 8853383
g2g3: 4190119
g2g4: 13629805
a5a6: 16022983
a5a4: 14139786
b4a4: 11996400
b4c4: 17400108
b4d4: 15996777
b4e4: 14187097
b4f4: 3069955
b4b3: 15482610
b4b2: 12755330
b4b1: 19481757
Depth: 7	Nodes: 178633661	Time: 1299410ms or 1299.410s or 21.656m
Stock Fish at perft 7
e2e3: 11427551
g2g3: 4190119
a5a6: 16022983
e2e4: 8853383
g2g4: 13629805
b4b1: 19481757
b4b2: 12755330
b4b3: 15482610
b4a4: 11996400
b4c4: 17400108
b4d4: 15996777
b4e4: 14187097
b4f4: 3069955
a5a4: 14139786
Depth: 7	Nodes: 178633661	Time: 1832ms or 1.832s

I can’t really show the code because I separated the methods/functions with a different module, if I add those up probably the lines would go up to 2000-3000 lines.

The engine can still be optimized even further but I’ll continue that another time; compared to other engines out there like stock fish which can generate like 130 million moves per seconds with an ello of 3750; my engine can just be tossed aside; because I’m using lua/roblox studio which is slower than C, or other programming language; I chose to do this on lua, because I already know a lot about lua.

1 Like