https://en.wikipedia.org/w/index.php?action=history&feed=atom&title=Module%3APgnModule:Pgn - Revision history2025-06-01T01:11:50ZRevision history for this page on the wikiMediaWiki 1.45.0-wmf.3https://en.wikipedia.org/w/index.php?title=Module:Pgn&diff=853186768&oldid=prevקיפודנחש: import from hewiki2018-08-03T00:57:17Z<p>import from hewiki</p>
<p><b>New page</b></p><div>--[[<br />
the purpose of this module is to provide pgn analysis local functionality<br />
main local function, called pgn2fen:<br />
input: either algebraic notation or full pgn of a single game<br />
output: <br />
* 1 table of positions (using FEN notation), one per each move of the game<br />
* 1 lua table with pgn metadata (if present)<br />
<br />
<br />
purpose:<br />
using this local , we can create utility local functions to be used by templates.<br />
the utility local function will work something like so:<br />
it receives (in addition to the pgn, of course) list of moves and captions, and some wikicode in "nowiki" tag.<br />
per each move, it will replace the token FEN with the fen of the move, and the token COMMENT with the comment (if any) of the move.<br />
it will then parse the wikicode, return all the parser results concataneted.<br />
others may fund other ways to use it.<br />
<br />
the logic:<br />
the analysis part copies freely from the javascipt "pgn" program.<br />
<br />
main object: "board": 0-based table(one dimensional array) of 64 squares (0-63), <br />
each square is either empty or contains the letter of the charToFile, e.g., "pP" is pawn.<br />
<br />
utility local functions<br />
index to row/col<br />
row/col to index<br />
disambig(file, row): if file is number, return it, otherwise return rowtoindex().<br />
create(fen): returns ready board<br />
generateFen(board) - selbverständlich<br />
<br />
pieceAt(coords): returns the piece at row/col<br />
findPieces(piece): returns list of all squares containing specific piece ("black king", "white rook" etc).<br />
roadIsClear(start/end row/column): start and end _must_ be on the same row, same column, or a diagonal. will error if not.<br />
returns true if all the squares between start and end are clear.<br />
canMove(source, dest, capture): boolean (capture is usually reduntant, except for en passant)<br />
promote(coordinate, designation, color)<br />
move(color, algebraic notation): finds out which piece should move, error if no piece or more than one piece found,<br />
and execute the move.<br />
<br />
rawPgnAnalysis(input)<br />
gets a pgn or algebraic notation, returns a table withthe metadata, and a second table with the algebraic notation individual moves<br />
<br />
main:<br />
-- metadata, notations := rawPgnAnalysis(input)<br />
-- result := empty table<br />
-- startFen := metadata.fen || default; results += startFen<br />
-- board := create(startFen)<br />
-- loop through notations <br />
----- pass board, color and notation, get modified board<br />
----- results += generateFen()<br />
-- return result<br />
<br />
the "meat" is the "canMove. however, as it turns out, it is not that difficult.<br />
the only complexity is with pawns, both because they are asymmetrical, and irregular. brute force (as elegantly as possible)<br />
<br />
other pieces are a breeze. color does not matter. calc da := abs(delta raw), db := abs(delta column)<br />
piece | rule<br />
Knight: da * db - 2 = 0<br />
Rook: da * db = 0<br />
Bishop: da - db = 0<br />
King db | db = 1 (bitwise or)<br />
Queen da * db * (da - db) = 0<br />
<br />
<br />
move:<br />
find out which piece. find all of them on the board. ask each if it can execute the move, and count "yes". <br />
there should be only one yes (some execptions to be handled). execute the move. <br />
<br />
<br />
<br />
]]<br />
<br />
local BLACK = "black"<br />
local WHITE = "white"<br />
<br />
local PAWN = "P"<br />
local ROOK = "R"<br />
local KNIGHT = "N"<br />
local BISHOP = "B"<br />
local QUEEN = "Q"<br />
local KING = "K"<br />
<br />
local KINGSIDE = 7<br />
local QUEENSIDE = 12<br />
<br />
local DEFAULT_BOARD = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR'<br />
<br />
local bit32 = bit32 or require('bit32')<br />
<br />
--[[ following lines require when running locally - uncomment.<br />
mw = mw or {<br />
ustring = string,<br />
text = {<br />
['split'] = local function(s, pattern)<br />
local res = {}<br />
while true do<br />
local start, finish = s:find(pattern)<br />
if finish and finish > 1 then<br />
local frag = s:sub(1, start - 1)<br />
table.insert(res, frag)<br />
s = s:sub(finish + 1) <br />
else<br />
break<br />
end<br />
end<br />
if #s then table.insert(res, s) end<br />
return res<br />
end,<br />
['trim'] = local function(t)<br />
t = type(t) == 'string' and t:gsub('^%s+', '')<br />
t = t:gsub('%s+$', '')<br />
return t<br />
end<br />
}<br />
}<br />
]]<br />
<br />
-- in lua 5.3, unpack is not a first class citizen anymore, but - assign table.unpack<br />
local unpack = unpack or table.unpack<br />
<br />
local function apply(f, ...)<br />
res = {}<br />
targ = {...}<br />
for ind = 1, #targ do<br />
res[ind] = f(targ[ind])<br />
end<br />
return unpack(res)<br />
end<br />
<br />
local function empty(s)<br />
return not s or mw.text.trim(s) == ''<br />
end<br />
<br />
local function falseIfEmpty(s)<br />
return not empty(s) and s<br />
end<br />
<br />
local function charToFile(ch)<br />
return falseIfEmpty(ch) and string.byte(ch) - string.byte('a')<br />
end<br />
<br />
local function charToRow(ch)<br />
return falseIfEmpty(ch) and tonumber(ch) - 1<br />
end<br />
<br />
local function indexToCoords(index)<br />
return index % 8, math.floor(index / 8)<br />
end<br />
<br />
local function coordsToIndex(file, row) <br />
return row * 8 + file<br />
end<br />
<br />
local function charToPiece(letter)<br />
local piece = mw.ustring.upper(letter)<br />
return piece, piece == letter and WHITE or BLACK<br />
end<br />
<br />
local function pieceToChar(piece, color)<br />
return color == WHITE and piece or mw.ustring.lower(piece)<br />
end<br />
<br />
local function ambigToIndex(file, row)<br />
if row == nil then return file end<br />
return coordsToIndex(file, row)<br />
end<br />
<br />
local function enPasantRow(color)<br />
return color == WHITE and 5 or 2<br />
end<br />
<br />
<br />
<br />
local function sign(a)<br />
return a < 0 and -1 <br />
or a > 0 and 1 <br />
or 0<br />
end<br />
<br />
local function pieceAt(board, fileOrInd, row) -- called with 2 params, fileOrInd is the index, otherwise it's the file.<br />
local letter = board[ambigToIndex(fileOrInd, row)] <br />
if not letter then return end<br />
return charToPiece(letter)<br />
end<br />
<br />
local function findPieces(board, piece, color)<br />
local result = {}<br />
local lookFor = pieceToChar(piece, color)<br />
for index = 0, 63 do<br />
local letter = board[index]<br />
if letter == lookFor then table.insert(result, index) end<br />
end<br />
return result<br />
end<br />
<br />
local function roadIsClear(board, ind1, ind2)<br />
if ind1 == ind2 then error('call to roadIsClear with identical indices', ind1) end<br />
local file1, row1 = indexToCoords(ind1)<br />
local file2, row2 = indexToCoords(ind2) <br />
if (file1 - file2) * (row1 - row2) * (math.abs(row1 - row2) - math.abs(file1 - file2)) ~= 0 then<br />
error('sent two indices to roadIsClear which are not same row, col, or diagonal: ', ind1, ind2)<br />
end<br />
local hdelta = sign(file2 - file1)<br />
local vdelta = sign(row2 - row1)<br />
local row, file = row1 + vdelta, file1 + hdelta<br />
while row ~= row2 or file ~= file2 do<br />
if pieceAt(board, file, row) then return false end<br />
row = row + vdelta<br />
file = file + hdelta<br />
end<br />
return true<br />
end<br />
<br />
local function pawnCanMove(board, color, startFile, startRow, file, row, capture)<br />
local hor, ver = file - startFile, row - startRow<br />
local absVer = math.abs(ver)<br />
if capture then<br />
local ok = hor * hor == 1 and (<br />
color == WHITE and ver == 1 or<br />
color == BLACK and ver == - 1<br />
)<br />
<br />
local enpassant = ok and<br />
row == enPasantRow(color) and <br />
pieceAt(board, file, row) == nil<br />
return ok, enpassant<br />
else <br />
if hor ~= 0 then return false end<br />
end<br />
if absVer == 2 then<br />
if not roadIsClear(board, coordsToIndex(startFile, startRow), coordsToIndex(file, row)) then return false end<br />
return color == WHITE and startRow == 1 and ver == 2 or<br />
color == BLACK and startRow == 6 and ver == -2<br />
end<br />
return color == WHITE and ver == 1 or color == BLACK and ver == -1<br />
end<br />
<br />
local function canMove(board, start, dest, capture, verbose)<br />
local startFile, startRow = indexToCoords(start) <br />
local file, row = indexToCoords(dest)<br />
local piece, color = pieceAt(board, startFile, startRow)<br />
if piece == PAWN then return pawnCanMove(board, color, startFile, startRow, file, row, capture) end<br />
local dx, dy = math.abs(startFile - file), math.abs(startRow - row)<br />
return piece == KNIGHT and dx * dy == 2<br />
or piece == KING and bit32.bor(dx, dy) == 1 <br />
or (<br />
piece == ROOK and dx * dy == 0 <br />
or piece == BISHOP and dx == dy <br />
or piece == QUEEN and dx * dy * (dx - dy) == 0<br />
) and roadIsClear(board, start, dest, verbose)<br />
end<br />
<br />
local function exposed(board, color) -- only test for queen, rook, bishop.<br />
local king = findPieces(board, KING, color)[1]<br />
for ind = 1, 63 do<br />
local letter = board[ind]<br />
if letter then<br />
local _, pcolor = charToPiece(letter)<br />
if pcolor ~= color and canMove(board, ind, king, true) then<br />
return true <br />
end<br />
end<br />
end<br />
end<br />
<br />
local function clone(orig)<br />
local res = {}<br />
for k, v in pairs(orig) do res[k] = v end<br />
return res<br />
end<br />
<br />
local function place(board, piece, color, file, row) -- in case of chess960, we have to search<br />
board[ambigToIndex(file, row)] = pieceToChar(piece, color)<br />
return board<br />
end<br />
<br />
local function clear(board, file, row)<br />
board[ambigToIndex(file, row)] = nil<br />
return board<br />
end<br />
<br />
local function doCastle(board, color, side)<br />
local row = color == WHITE and 0 or 7<br />
local startFile, step = 0, 1<br />
local kingDestFile, rookDestFile = 2, 3<br />
local king = findPieces(board, KING, color)[1]<br />
local rook<br />
if side == KINGSIDE then<br />
startFile, step = 7, -1<br />
kingDestFile, rookDestFile = 6, 5<br />
end<br />
for file = startFile, 7 - startFile, step do<br />
local piece = pieceAt(board, file, row)<br />
if piece == ROOK then<br />
rook = coordsToIndex(file, row)<br />
break<br />
end<br />
end<br />
board = clear(board, king)<br />
board = clear(board, rook)<br />
board = place(board, KING, color, kingDestFile, row) <br />
board = place(board, ROOK, color, rookDestFile, row)<br />
return board<br />
end<br />
<br />
local function doEnPassant(board, pawn, file, row)<br />
local _, color = pieceAt(board, pawn)<br />
board = clear(board, pawn)<br />
board = place(board, PAWN, color, file, row)<br />
if row == 5 then board = clear(board, file, 4) end<br />
if row == 2 then board = clear(board, file, 3) end<br />
return board<br />
end<br />
<br />
local function generateFen(board)<br />
local res = ''<br />
local offset = 0<br />
for row = 7, 0, -1 do<br />
for file = 0, 7 do<br />
piece = board[coordsToIndex(file, row)]<br />
res = res .. (piece or '1')<br />
end<br />
if row > 0 then res = res .. '/' end<br />
end<br />
return mw.ustring.gsub(res, '1+', function( s ) return #s end )<br />
end<br />
<br />
local function findCandidate(board, piece, color, oldFile, oldRow, file, row, capture, notation)<br />
local enpassant = {}<br />
local candidates, newCands = findPieces(board, piece, color), {} -- all black pawns or white kings etc.<br />
if oldFile or oldRow then <br />
local newCands = {}<br />
for _, cand in ipairs(candidates) do<br />
local file, row = indexToCoords(cand)<br />
if file == oldFile then table.insert(newCands, cand) end<br />
if row == oldRow then table.insert(newCands, cand) end<br />
end<br />
candidates, newCands = newCands, {}<br />
end<br />
local dest = coordsToIndex(file, row)<br />
for _, candidate in ipairs(candidates) do<br />
local can<br />
can, enpassant[candidate] = canMove(board, candidate, dest, capture)<br />
if can then table.insert(newCands, candidate) end<br />
end<br />
<br />
candidates, newCands = newCands, {}<br />
if #candidates == 1 then return candidates[1], enpassant[candidates[1]] end<br />
if #candidates == 0 then <br />
error('could not find a piece that can execute ' .. notation) <br />
end<br />
-- 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 <br />
-- test for it by creating a new board with this candidate removed, and see if the king became exposed<br />
for _, candidate in ipairs(candidates) do <br />
local cloneBoard = clone(board) -- first, clone the board<br />
cloneBoard = clear(cloneBoard, candidate) -- now, remove the piece<br />
if not exposed(cloneBoard, color) then table.insert(newCands, candidate) end<br />
end<br />
candidates, newCands = newCands, {}<br />
if #candidates == 1 then return candidates[1] end<br />
error(mw.ustring.format('too many (%d, expected 1) pieces can execute %s at board %s', #candidates, notation, generateFen(board)))<br />
end<br />
<br />
local function move(board, notation, color)<br />
local endGame = {['1-0']=true, ['0-1']=true, ['1/2-1/2']=true, ['*']=true}<br />
<br />
local cleanNotation = mw.ustring.gsub(notation, '[!?+# ]', '')<br />
<br />
if cleanNotation == 'O-O' then<br />
return doCastle(board, color, KINGSIDE)<br />
end<br />
if cleanNotation == 'O-O-O' then<br />
return doCastle(board, color, QUEENSIDE)<br />
end<br />
if endGame[cleanNotation] then<br />
return board, true<br />
end<br />
<br />
local pattern = '([RNBKQ]?)([a-h]?)([1-8]?)(x?)([a-h])([1-8])(=?[RNBKQ]?)'<br />
local _, _, piece, oldFile, oldRow, isCapture, file, row, promotion = mw.ustring.find(cleanNotation, pattern)<br />
oldFile, file = apply(charToFile, oldFile, file) <br />
oldRow, row = apply(charToRow, oldRow, row)<br />
piece = falseIfEmpty(piece) or PAWN<br />
promotion = falseIfEmpty(promotion)<br />
isCapture = falseIfEmpty(isCapture)<br />
local candidate, enpassant = findCandidate(board, piece, color, oldFile, oldRow, file, row, isCapture, notation) -- findCandidates should panic if # != 1<br />
if enpassant then<br />
return doEnPassant(board, candidate, file, row)<br />
end<br />
board[coordsToIndex(file, row)] = promotion and pieceToChar(promotion:sub(-1), color) or board[candidate]<br />
board = clear(board, candidate)<br />
return board<br />
end<br />
<br />
local function create( fen )<br />
-- converts FEN notation to 64 entry array of positions. copied from enwiki Module:Chessboard (in some distant past i prolly wrote it)<br />
local res = {}<br />
local row = 8<br />
-- Loop over rows, which are delimited by /<br />
for srow in string.gmatch( "/" .. fen, "/%w+" ) do<br />
srow = srow:sub(2)<br />
row = row - 1<br />
local ind = row * 8<br />
-- Loop over all letters and numbers in the row<br />
for piece in srow:gmatch( "%w" ) do<br />
if piece:match( "%d" ) then -- if a digit<br />
ind = ind + piece<br />
else -- not a digit<br />
res[ind] = piece<br />
ind = ind + 1<br />
end<br />
end<br />
end<br />
return res<br />
end<br />
<br />
local function processMeta(grossMeta) <br />
res = {}<br />
-- process grossMEta here<br />
for item in mw.ustring.gmatch(grossMeta or '', '%[([^%]]*)%]') do<br />
key, val = item:match('([^"]+)"([^"]*)"')<br />
if key and val then<br />
res[mw.text.trim(key)] = mw.text.trim(val) -- add mw.text.trim()<br />
else<br />
error('strange item detected: ' .. item .. #items) -- error later<br />
end<br />
end<br />
return res<br />
end<br />
<br />
local function analyzePgn(pgn)<br />
local grossMeta = pgn:match('%[(.*)%]') -- first open to to last bracket <br />
pgn = string.gsub(pgn, '%[(.*)%]', '')<br />
local steps = mw.text.split(pgn, '%s*%d+%.%s*')<br />
local moves = {}<br />
for _, step in ipairs(steps) do<br />
if mw.ustring.len(mw.text.trim(step)) then<br />
ssteps = mw.text.split(step, '%s+')<br />
for _, sstep in ipairs(ssteps) do <br />
if sstep and not mw.ustring.match(sstep, '^%s*$') then table.insert(moves, sstep) end<br />
end<br />
end<br />
end<br />
return processMeta(grossMeta), moves<br />
end<br />
<br />
local function pgn2fen(pgn)<br />
local metadata, notationList = analyzePgn(pgn)<br />
local fen = metadata.fen or DEFAULT_BOARD<br />
local board = create(fen)<br />
local res = {fen}<br />
local colors = {BLACK, WHITE} <br />
for step, notation in ipairs(notationList) do<br />
local color = colors[step % 2 + 1]<br />
board = move(board, notation, color)<br />
local fen = generateFen(board)<br />
table.insert(res, fen)<br />
end<br />
return res, metadata<br />
end<br />
<br />
return {<br />
pgn2fen = pgn2fen,<br />
main = function(pgn) <br />
local res, metadata = pgn2fen(pgn) <br />
return metadata, res <br />
end,<br />
}</div>קיפודנחש