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"White")
	blackColor = blackColor or"Black")

	local squares = {}

	local folder ="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 ="Part", folder)

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

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

	return squares;
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 +, 1, 0))

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

	return true

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 +, 1, 0))
			-- Print an error message
			print("Illegal move!")
		-- Print an error message
		print("There is no piece on the from square!")

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

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))

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

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

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

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

--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
	return false

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

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

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
			return false
	elseif dist ~= 2 then  --2 orthogonal moves = 1 diagonal
		error() --In case I'm thinking wrong or something :)

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