--| default; results += startFen-- board := create(startFen)-- loop through notations ----- pass board, color and notation, get modified board----- results += generateFen-- return result
the "meat" is the "canMove. however, as it turns out, it is not that difficult.the only complexity is with pawns, both because they are asymmetrical, and irregular. brute force (as elegantly as possible)
other pieces are a breeze. color does not matter. calc da := abs(delta raw), db := abs(delta column)piece | ruleKnight: da * db - 2 = 0Rook: da * db = 0Bishop: da - db = 0King db | db = 1 (bitwise or)Queen da * db * (da - db) = 0
move:find out which piece. find all of them on the board. ask each if it can execute the move, and count "yes". there should be only one yes (some execptions to be handled). execute the move.
local BLACK = "black"local WHITE = "white"
local PAWN = "P"local ROOK = "R"local KNIGHT = "N"local BISHOP = "B"local QUEEN = "Q"local KING = "K"
local KINGSIDE = 7local QUEENSIDE = 12
local DEFAULT_BOARD = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR'
local bit32 = bit32 or require('bit32')
--
-- in lua 5.3, unpack is not a first class citizen anymore, but - assign table.unpacklocal unpack = unpack or table.unpack local function apply(f, ...) res = targ = for ind = 1, #targ do res[ind] = f(targ[ind]) end return unpack(res)end
local function empty(s) return not s or mw.text.trim(s)
local function falseIfEmpty(s) return not empty(s) and send
local function charToFile(ch) return falseIfEmpty(ch) and string.byte(ch) - string.byte('a')end
local function charToRow(ch) return falseIfEmpty(ch) and tonumber(ch) - 1end
local function indexToCoords(index) return index % 8, math.floor(index / 8)end
local function coordsToIndex(file, row) return row * 8 + fileend
local function charToPiece(letter) local piece = mw.ustring.upper(letter) return piece, piece
local function pieceToChar(piece, color) return color
local function ambigToIndex(file, row) if row
local function enPasantRow(color) return color
local function sign(a) return a < 0 and -1 or a > 0 and 1 or 0end
local function pieceAt(board, fileOrInd, row) -- called with 2 params, fileOrInd is the index, otherwise it's the file. local letter = board[ambigToIndex(fileOrInd, row)] if not letter then return end return charToPiece(letter)end
local function findPieces(board, piece, color) local result = local lookFor = pieceToChar(piece, color) for index = 0, 63 do local letter = board[index] if letter
local function roadIsClear(board, ind1, ind2) if ind1
local function pawnCanMove(board, color, startFile, startRow, file, row, capture) local hor, ver = file - startFile, row - startRow local absVer = math.abs(ver) if capture then local ok = hor * hor
WHITE and ver
BLACK and ver
enPasantRow(color) and pieceAt(board, file, row)
2 then if not roadIsClear(board, coordsToIndex(startFile, startRow), coordsToIndex(file, row)) then return false end return color
1 and ver
BLACK and startRow
-2 end return color
1 or color
-1end
local function canMove(board, start, dest, capture, verbose) local startFile, startRow = indexToCoords(start) local file, row = indexToCoords(dest) local piece, color = pieceAt(board, startFile, startRow) if piece
KNIGHT and dx * dy
KING and bit32.bor(dx, dy)
ROOK and dx * dy
BISHOP and dx
QUEEN and dx * dy * (dx - dy)
local function exposed(board, color) -- only test for queen, rook, bishop. local king = findPieces(board, KING, color)[1] for ind = 1, 63 do local letter = board[ind] if letter then local _, pcolor = charToPiece(letter) if pcolor ~= color and canMove(board, ind, king, true) then return true end end endend
local function clone(orig) local res = for k, v in pairs(orig) do res[k] = v end return resend
local function place(board, piece, color, file, row) -- in case of chess960, we have to search board[ambigToIndex(file, row)] = pieceToChar(piece, color) return boardend
local function clear(board, file, row) board[ambigToIndex(file, row)] = nil return boardend
local function doCastle(board, color, side) local row = color
KINGSIDE then startFile, step = 7, -1 kingDestFile, rookDestFile = 6, 5 end for file = startFile, 7 - startFile, step do local piece = pieceAt(board, file, row) if piece
local function doEnPassant(board, pawn, file, row) local _, color = pieceAt(board, pawn) board = clear(board, pawn) board = place(board, PAWN, color, file, row) if row
2 then board = clear(board, file, 3) end return boardend
local function generateFen(board) local res = local offset = 0 for row = 7, 0, -1 do for file = 0, 7 do piece = board[coordsToIndex(file, row)] res = res .. (piece or '1') end if row > 0 then res = res .. '/' end end return mw.ustring.gsub(res, '1+', function(s) return #s end)end
local function findCandidate(board, piece, color, oldFile, oldRow, file, row, capture, notation) local enpassant = local candidates, newCands = findPieces(board, piece, color), -- all black pawns or white kings etc. if oldFile or oldRow then local newCands = for _, cand in ipairs(candidates) do local file, row = indexToCoords(cand) if file
oldRow then table.insert(newCands, cand) end end candidates, newCands = newCands, end local dest = coordsToIndex(file, row) for _, candidate in ipairs(candidates) do local can can, enpassant[candidate] = canMove(board, candidate, dest, capture) if can then table.insert(newCands, candidate) end end
candidates, newCands = newCands, if #candidates
0 then error('could not find a piece that can execute ' .. notation) end -- we have more than one candidate. this means that all but one of them can't really move, b/c it will expose the king -- test for it by creating a new board with this candidate removed, and see if the king became exposed for _, candidate in ipairs(candidates) do local cloneBoard = clone(board) -- first, clone the board cloneBoard = clear(cloneBoard, candidate) -- now, remove the piece if not exposed(cloneBoard, color) then table.insert(newCands, candidate) end end candidates, newCands = newCands, if #candidates
local function move(board, notation, color) local endGame =
local cleanNotation = mw.ustring.gsub(notation, '[!?+# ]', )
if cleanNotation
'O-O-O' then return doCastle(board, color, QUEENSIDE) end if endGame[cleanNotation] then return board, true end
local pattern = '([RNBKQ]?)([a-h]?)([1-8]?)(x?)([a-h])([1-8])(=?[RNBKQ]?)' local _, _, piece, oldFile, oldRow, isCapture, file, row, promotion = mw.ustring.find(cleanNotation, pattern) oldFile, file = apply(charToFile, oldFile, file) oldRow, row = apply(charToRow, oldRow, row) piece = falseIfEmpty(piece) or PAWN promotion = falseIfEmpty(promotion) isCapture = falseIfEmpty(isCapture) local candidate, enpassant = findCandidate(board, piece, color, oldFile, oldRow, file, row, isCapture, notation) -- findCandidates should panic if # != 1 if enpassant then return doEnPassant(board, candidate, file, row) end board[coordsToIndex(file, row)] = promotion and pieceToChar(promotion:sub(-1), color) or board[candidate] board = clear(board, candidate) return boardend
local function create(fen) -- converts FEN notation to 64 entry array of positions. copied from enwiki Module:Chessboard (in some distant past i prolly wrote it) local res = local row = 8 -- Loop over rows, which are delimited by / for srow in string.gmatch("/" .. fen, "/%w+") do srow = srow:sub(2) row = row - 1 local ind = row * 8 -- Loop over all letters and numbers in the row for piece in srow:gmatch("%w") do if piece:match("%d") then -- if a digit ind = ind + piece else -- not a digit res[ind] = piece ind = ind + 1 end end end return resend
local function processMeta(grossMeta) res = -- process grossMEta here for item in mw.ustring.gmatch(grossMeta or , '%[([^%]]*)%]') do key, val = item:match('([^"]+)"([^"]*)"') if key and val then res[mw.text.trim(key)] = mw.text.trim(val) -- add mw.text.trim else error('strange item detected: ' .. item .. #items) -- error later end end return resend
local function analyzePgn(pgn) local grossMeta = pgn:match('%[(.*)%]') -- first open to to last bracket pgn = string.gsub(pgn, '%[(.*)%]', ) local steps = mw.text.split(pgn, '%s*%d+%.%s*') local moves = for _, step in ipairs(steps) do if mw.ustring.len(mw.text.trim(step)) then ssteps = mw.text.split(step, '%s+') for _, sstep in ipairs(ssteps) do if sstep and not mw.ustring.match(sstep, '^%s*$') then table.insert(moves, sstep) end end end end return processMeta(grossMeta), movesend
local function pgn2fen(pgn) local metadata, notationList = analyzePgn(pgn) local fen = metadata.fen or DEFAULT_BOARD local board = create(fen) local res = local colors = for step, notation in ipairs(notationList) do local color = colors[step % 2 + 1] board = move(board, notation, color) local fen = generateFen(board) table.insert(res, fen) end return res, metadataend
return