https://en.wikipedia.org/w/index.php?action=history&feed=atom&title=Module%3APgn Module:Pgn - Revision history 2025-06-01T01:11:50Z Revision history for this page on the wiki MediaWiki 1.45.0-wmf.3 https://en.wikipedia.org/w/index.php?title=Module:Pgn&diff=853186768&oldid=prev קיפודנחש: import from hewiki 2018-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 &quot;nowiki&quot; 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 &quot;pgn&quot; program.<br /> <br /> main object: &quot;board&quot;: 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., &quot;pP&quot; 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 (&quot;black king&quot;, &quot;white rook&quot; 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 &quot;meat&quot; is the &quot;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 &quot;yes&quot;. <br /> there should be only one yes (some execptions to be handled). execute the move. <br /> <br /> <br /> <br /> ]]<br /> <br /> local BLACK = &quot;black&quot;<br /> local WHITE = &quot;white&quot;<br /> <br /> local PAWN = &quot;P&quot;<br /> local ROOK = &quot;R&quot;<br /> local KNIGHT = &quot;N&quot;<br /> local BISHOP = &quot;B&quot;<br /> local QUEEN = &quot;Q&quot;<br /> local KING = &quot;K&quot;<br /> <br /> local KINGSIDE = 7<br /> local QUEENSIDE = 12<br /> <br /> local DEFAULT_BOARD = &#039;rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR&#039;<br /> <br /> local bit32 = bit32 or require(&#039;bit32&#039;)<br /> <br /> --[[ following lines require when running locally - uncomment.<br /> mw = mw or {<br /> ustring = string,<br /> text = {<br /> [&#039;split&#039;] = local function(s, pattern)<br /> local res = {}<br /> while true do<br /> local start, finish = s:find(pattern)<br /> if finish and finish &gt; 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 /> [&#039;trim&#039;] = local function(t)<br /> t = type(t) == &#039;string&#039; and t:gsub(&#039;^%s+&#039;, &#039;&#039;)<br /> t = t:gsub(&#039;%s+$&#039;, &#039;&#039;)<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) == &#039;&#039;<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(&#039;a&#039;)<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 &lt; 0 and -1 <br /> or a &gt; 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&#039;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(&#039;call to roadIsClear with identical indices&#039;, 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(&#039;sent two indices to roadIsClear which are not same row, col, or diagonal: &#039;, 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 = &#039;&#039;<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 &#039;1&#039;)<br /> end<br /> if row &gt; 0 then res = res .. &#039;/&#039; end<br /> end<br /> return mw.ustring.gsub(res, &#039;1+&#039;, 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(&#039;could not find a piece that can execute &#039; .. notation) <br /> end<br /> -- we have more than one candidate. this means that all but one of them can&#039;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(&#039;too many (%d, expected 1) pieces can execute %s at board %s&#039;, #candidates, notation, generateFen(board)))<br /> end<br /> <br /> local function move(board, notation, color)<br /> local endGame = {[&#039;1-0&#039;]=true, [&#039;0-1&#039;]=true, [&#039;1/2-1/2&#039;]=true, [&#039;*&#039;]=true}<br /> <br /> local cleanNotation = mw.ustring.gsub(notation, &#039;[!?+# ]&#039;, &#039;&#039;)<br /> <br /> if cleanNotation == &#039;O-O&#039; then<br /> return doCastle(board, color, KINGSIDE)<br /> end<br /> if cleanNotation == &#039;O-O-O&#039; then<br /> return doCastle(board, color, QUEENSIDE)<br /> end<br /> if endGame[cleanNotation] then<br /> return board, true<br /> end<br /> <br /> local pattern = &#039;([RNBKQ]?)([a-h]?)([1-8]?)(x?)([a-h])([1-8])(=?[RNBKQ]?)&#039;<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( &quot;/&quot; .. fen, &quot;/%w+&quot; ) 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( &quot;%w&quot; ) do<br /> if piece:match( &quot;%d&quot; ) 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 &#039;&#039;, &#039;%[([^%]]*)%]&#039;) do<br /> key, val = item:match(&#039;([^&quot;]+)&quot;([^&quot;]*)&quot;&#039;)<br /> if key and val then<br /> res[mw.text.trim(key)] = mw.text.trim(val) -- add mw.text.trim()<br /> else<br /> error(&#039;strange item detected: &#039; .. item .. #items) -- error later<br /> end<br /> end<br /> return res<br /> end<br /> <br /> local function analyzePgn(pgn)<br /> local grossMeta = pgn:match(&#039;%[(.*)%]&#039;) -- first open to to last bracket <br /> pgn = string.gsub(pgn, &#039;%[(.*)%]&#039;, &#039;&#039;)<br /> local steps = mw.text.split(pgn, &#039;%s*%d+%.%s*&#039;)<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, &#039;%s+&#039;)<br /> for _, sstep in ipairs(ssteps) do <br /> if sstep and not mw.ustring.match(sstep, &#039;^%s*$&#039;) 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> קיפודנחש