Determining the legality of a move in the game of checkers

I would like to know how to determine if a move in the game of checkers, also known as draughts, is legal or not.

Whenever I have tried to create this function, only one aspect has worked properly while many others have not.

Below is half of the script, which includes everything that is necessary.

function createCheckerboard(boardSize, squareSize, whiteColor, blackColor)
	-- Set default values for the function parameters
	boardSize = boardSize or 8
	squareSize = squareSize or 1
	whiteColor = whiteColor or BrickColor.new("White")
	blackColor = blackColor or BrickColor.new("Black")

	local squares = {}

	local folder = Instance.new("Folder", workspace)
	folder.Name = "CheckerBoard"

	-- Loop through the rows and columns of the checkerboard
	for row = 1, boardSize do
		for col = 1, boardSize do
			local square = Instance.new("Part", folder)

			square.Anchored = true
			square.Position = Vector3.new((row - 1) * squareSize - (boardSize / 2) * squareSize, 0.5, (col - 1) * squareSize - (boardSize / 2) * squareSize)
			square.Size = Vector3.new(squareSize, 0, squareSize)
			square.Material = Enum.Material.Wood
			square.Name = row..col

			if (row + col) % 2 == 0 then
				square.BrickColor = whiteColor
			else
				square.BrickColor = blackColor
				Instance.new("ClickDetector", square)
			end
			table.insert(squares, square)
		end
	end

	return squares;
end
function placeCheckersPiece(board, row, col, pieceModel)
	-- Find the square at the specified row and column
	local square = board[(row - 1) * 8 + col]

	-- Place the checkers piece on the square
	local piece = pieceModel:Clone()
	piece.Parent = square
	piece:MoveTo(square.Position + Vector3.new(0, 1, 0))
end

function isLegalMove(board, fromRow, fromCol, toRow, toCol, plr)
	-- Check if the move is within the bounds of the board
	if fromRow < 1 or fromRow > 8 or fromCol < 1 or fromCol > 8 or toRow < 1 or toRow > 8 or toCol < 1 or toCol > 8 then
		warn("Over the bounds")
		return false
	end

	return true
end


function moveCheckersPiece(board, fromRow, fromCol, toRow, toCol, plr)
	-- Find the square that the piece is currently on
	local fromSquare = board[(fromRow - 1) * 8 + fromCol]

	-- Find the square that the piece will be moved to
	local toSquare = board[(toRow - 1) * 8 + toCol]

	-- Get the checkers piece that is on the from square
	local piece = fromSquare:FindFirstChildWhichIsA("Model")
	-- Make sure there is a piece on the from square
	if piece then
		-- Check if the move is legal
		if isLegalMove(board, fromRow, fromCol, toRow, toCol, plr) then
			-- Move the piece to the to square
			if piece then
				piece:MoveTo(toSquare.Position + Vector3.new(0, 1, 0))
			end
		else
			-- Print an error message
			print("Illegal move!")
		end
	else
		-- Print an error message
		print("There is no piece on the from square!")
	end
end

Since the availability of moves depends on which pieces are in what places, it would be handy to be able to determine which pieces are in any spot given by it’s coordinates:

function getPieceAt(board, row, col)
    --return the piece that occupies the given spot
end

Then we can do arithmetic on a piece’s coordinates to get the diagonally adjacent spots one and two steps away:

function coordCandidateMoveSpots(coord, plr)
	return {
		--1 left 1 forward
		addCoords(fromCoord, coord(-1, plr.ForwardDir)),
		--2 left 2 forward
		addCoords(fromCoord, coord(-2, 2 * plr.ForwardDir)),
		--1 right 1 forward
		addCoords(fromCoord, coord(1, plr.ForwardDir)),
		--2 right 2 forward
		addCoords(fromCoord, coord(2, 2 * plr.ForwardDir))
	}
end

function coord(row, col)
	return {row=row, col=col}
end

function addCoords(coordA, coordB)
	return coord(coordA.row + coordB.row, coordA.col + coordB.col)
end

We can check if the given “to” coordinate is one of the candidates:

function eqCoords(coordA, coordB)
	return coordA.row == coordB.row and coordA.col == coordB.col
end

--Returns a function with it's first parameter fixed to a given value 
function curry(f, arg1)
	return function(...)
		return f(arg1, ...)
	end
end

--Returns true if f(v) is truthey for any v in t, otherwise returns false.
--I.e. checks if some condition holds for *any* value in a table 
function any(t, f)
	for _, v in pairs(t) do
		if f(v) then
			return true
		end
	end
	return false
end

    --In isLegalMove
    local toCoordIsACandidate = any( --Does any...
		candidateSpots, -- ... of the candidate spots ...
		curry(eqCoords, toCoord) -- ... equal toCoord?d
	)
	if not toCoordIsACandidate then
		return false
	end

Some conditions might make a candidate position invalid, e.g. being outside the board, but also if it already has a piece in it:

    --In isLegalMove
    --The move cannot place a piece on any other piece 
	if nil ~= getPieceAt(board, toRow, toCol) then
		return false
	end

and finally, if the to position is two diagonal moves away from the from position, then that’s invalid unless there’s an enemy piece in the position between those two:

    --In isLegalMove
    --If the move jumps over a spot, an opponent piece must be in that spot
	local dist = manhattanDistCoords(toCoord, fromCoord) 
	if dist == 4 then --4 orthogonal moves = 2 diagonal
		local dRow, dCol = toRow - fromRow, toCol - toCol
		local jumpedPiece = getPieceAt(board, fromRow + dRow/2, fromCol + dCol/2)
		if jumpedPiece and jumpedPiece.Owner ~= plr then
			return true
		else
			return false
		end
	elseif dist ~= 2 then  --2 orthogonal moves = 1 diagonal
		error() --In case I'm thinking wrong or something :)
	end

For most of this code I used some helper functions that let me treat a pair of numbers as a single coordinate object, because that’s more convenient. Of course the built in Vector2 class already has most of this functionality, in my own code I’d definitely just use that.

Hope this helps, ask away if you have any questions.

1 Like