Module:Sandbox/SD0001/Chess3
-- Forked from [[Module:Sandbox/Bawolff/Chessboard]]
----------------------------------------------------
local Calculator = require('Module:Sandbox/SD0001/Calculator')
local p = {}
local cfg, nrows, ncols
cfg = {}
-- From Module:Chessboard/Chess
function cfg.dims()
return 8, 8
end
function cfg.letters()
return { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h' }
end
function cfg.image_board(size)
return string.format('[[File:Chessboard480.svg|%dx%dpx|link=|class=notpageimage]]', 8 * size, 8 * size)
end
function cfg.image_square(pc, row, col, size)
local colornames = { l = 'white', d = 'black', u = 'unknown color' }
local piecenames = {
p = 'pawn',
r = 'rook',
n = 'knight',
b = 'bishop',
q = 'queen',
k = 'king',
a = 'archbishop',
c = 'chancellor',
z = 'champion',
w = 'wizard',
t = 'fool',
M = 'mann',
h = 'upside-down pawn',
m = 'upside-down rook',
B = 'upside-down bishop',
N = 'upside-down knight',
f = 'upside-down king',
g = 'upside-down queen',
e = 'elephant',
s = 'boat',
G = 'giraffe',
U = 'unicorn',
Z = 'zebra'
}
local symnames = {
xx = 'black cross',
ox = 'white cross',
xo = 'black circle',
oo = 'white circle',
ul = 'up-left arrow',
ua = 'up arrow',
ur = 'up-right arrow',
la = 'left arrow',
ra = 'right arrow',
dl = 'down-left arrow',
da = 'down arrow',
dr = 'down-right arrow',
lr = 'left-right arrow',
ud = 'up-down arrow',
db = 'up-right and down-left arrow',
dw = 'up-left and down-right arrow',
x0 = 'zero',
x1 = 'one',
x2 = 'two',
x3 = 'three',
x4 = 'four',
x5 = 'five',
x6 = 'six',
x7 = 'seven',
x8 = 'eight',
x9 = 'nine'
}
local colchar = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h' }
local color = mw.ustring.gsub(pc, '^.*(%w)(%w).*$', '%2') or ''
local piece = mw.ustring.gsub(pc, '^.*(%w)(%w).*$', '%1') or ''
--local alt = colchar[col] .. row .. ' '
local alt = ''
if colornames[color] and piecenames[piece] then
alt = alt .. colornames[color] .. ' ' .. piecenames[piece]
else
alt = alt .. (symnames[piece .. color] or piece .. ' ' .. color)
end
return string.format('[[File:Chess %s%st45.svg|%dx%dpx|alt=%s|%s|link=|class=notpageimage|top]]', piece, color, size, size, alt, alt)
end
-- End Module:Chessboard/Chess
-- From Module:Pgn
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 = 7
local 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.unpack
local 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) == ''
end
local function falseIfEmpty(s)
return not empty(s) and s
end
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) - 1
end
local function indexToCoords(index)
return index % 8, math.floor(index / 8)
end
local function coordsToIndex(file, row)
return row * 8 + file
end
local function charToPiece(letter)
local piece = mw.ustring.upper(letter)
return piece, piece == letter and WHITE or BLACK
end
local function pieceToChar(piece, color)
return color == WHITE and piece or mw.ustring.lower(piece)
end
local function ambigToIndex(file, row)
if row == nil then
return file
end
return coordsToIndex(file, row)
end
local function enPasantRow(color)
return color == WHITE and 5 or 2
end
local function sign(a)
return a < 0 and -1
or a > 0 and 1
or 0
end
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 == lookFor then
table.insert(result, index)
end
end
return result
end
local function roadIsClear(board, ind1, ind2)
if ind1 == ind2 then
error('call to roadIsClear with identical indices', ind1)
end
local file1, row1 = indexToCoords(ind1)
local file2, row2 = indexToCoords(ind2)
if (file1 - file2) * (row1 - row2) * (math.abs(row1 - row2) - math.abs(file1 - file2)) ~= 0 then
error('sent two indices to roadIsClear which are not same row, col, or diagonal: ', ind1, ind2)
end
local hdelta = sign(file2 - file1)
local vdelta = sign(row2 - row1)
local row, file = row1 + vdelta, file1 + hdelta
while row ~= row2 or file ~= file2 do
if pieceAt(board, file, row) then
return false
end
row = row + vdelta
file = file + hdelta
end
return true
end
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 == 1 and (
color == WHITE and ver == 1 or
color == BLACK and ver == -1
)
local enpassant = ok and
row == enPasantRow(color) and
pieceAt(board, file, row) == nil
return ok, enpassant
else
if hor ~= 0 then
return false
end
end
if absVer == 2 then
if not roadIsClear(board, coordsToIndex(startFile, startRow), coordsToIndex(file, row)) then
return false
end
return color == WHITE and startRow == 1 and ver == 2 or
color == BLACK and startRow == 6 and ver == -2
end
return color == WHITE and ver == 1 or color == BLACK and ver == -1
end
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 == PAWN then
return pawnCanMove(board, color, startFile, startRow, file, row, capture)
end
local dx, dy = math.abs(startFile - file), math.abs(startRow - row)
return piece == KNIGHT and dx * dy == 2
or piece == KING and bit32.bor(dx, dy) == 1
or (
piece == ROOK and dx * dy == 0
or piece == BISHOP and dx == dy
or piece == QUEEN and dx * dy * (dx - dy) == 0
) and roadIsClear(board, start, dest, verbose)
end
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
end
end
local function clone(orig)
local res = {}
for k, v in pairs(orig) do
res[k] = v
end
return res
end
local function place(board, piece, color, file, row)
-- in case of chess960, we have to search
board[ambigToIndex(file, row)] = pieceToChar(piece, color)
return board
end
local function clear(board, file, row)
board[ambigToIndex(file, row)] = nil
return board
end
local function doCastle(board, color, side)
local row = color == WHITE and 0 or 7
local startFile, step = 0, 1
local kingDestFile, rookDestFile = 2, 3
local king = findPieces(board, KING, color)[1]
local rook
if side == 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 == ROOK then
rook = coordsToIndex(file, row)
break
end
end
board = clear(board, king)
board = clear(board, rook)
board = place(board, KING, color, kingDestFile, row)
board = place(board, ROOK, color, rookDestFile, row)
return board
end
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 == 5 then
board = clear(board, file, 4)
end
if row == 2 then
board = clear(board, file, 3)
end
return board
end
local function generateFen(board)
local res = ''
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 == oldFile then
table.insert(newCands, cand)
end
if row == 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 == 1 then
return candidates[1], enpassant[candidates[1]]
end
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 == 1 then
return candidates[1]
end
error(mw.ustring.format('too many (%d, expected 1) pieces can execute %s at board %s', #candidates, notation, generateFen(board)))
end
local function move(board, notation, color)
local endGame = { ['1-0'] = true, ['0-1'] = true, ['1/2-1/2'] = true, ['*'] = true }
local cleanNotation = mw.ustring.gsub(notation, '[!?+# ]', '')
if cleanNotation == 'O-O' then
return doCastle(board, color, KINGSIDE)
end
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 board
end
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 res
end
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 res
end
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), moves
end
local function pgn2fen(pgn)
local metadata, notationList = analyzePgn(pgn)
local fen = metadata.fen or DEFAULT_BOARD
local board = create(fen)
local res = { fen }
local colors = { BLACK, WHITE }
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, metadata
end
-- End Module:Pgn
-- start Module:Chessboard
local function innerboard(definedPieces, pieceInfo, size, rev)
pattern = cfg.pattern or '%w%w'
local root = mw.html.create('div')
root:addClass('chess-pieces notheme')
:css('position', 'relative')
:wikitext(cfg.image_board(size))
for piece in pairs(definedPieces) do
if piece:match(pattern) then
local img = cfg.image_square(piece:match(pattern), row, col, size)
local curPos = pieceInfo[1][piece] or { -1, -1 }
root:tag('div')
:css('top', 'calc(var(--calculator-top_piece' .. piece .. ',' .. curPos[1] .. ')*1px)')
:css('left', 'calc(var(--calculator-left_piece' .. piece .. ',' .. curPos[2] .. ')*1px)')
:css('z-index', 'min(var(--calculator-top_piece' .. piece .. ',' .. curPos[1] .. '),0)')
:wikitext(img)
end
end
return tostring(root)
end
local function getPositions(args, size, rev)
pattern = cfg.pattern or '%w%w'
local pieceInfo = {}
local definedPieces = {}
for moveIndex, move in ipairs(args) do
pieceInfo[moveIndex] = {}
local layout = convertFenToArgs(move)
for trow = 1, nrows do
local row = rev and trow or (1 + nrows - trow)
for tcol = 1, ncols do
local col = rev and (1 + ncols - tcol) or tcol
local piece = layout[ncols * (nrows - row) + col + 2] or ''
local pieceName = piece:match(pattern)
if pieceName then
-- Maximum number of pieces of the same type and color is 10
-- (eg. 2 rooks + 8 pawns each promoted to a rook)
local pieceIdx = 1
for i = 1, 10 do
if not pieceInfo[moveIndex][pieceName .. i] then
pieceIdx = i
break
end
end
definedPieces[pieceName .. pieceIdx] = true
pieceInfo[moveIndex][pieceName .. pieceIdx] = { (trow - 1) * size, (tcol - 1) * size }
end
end
end
end
return definedPieces, pieceInfo
end
function chessboard(definedPieces, pieceInfo, size, rev, letters, numbers, header, footer, align, clear)
function letters_row(rev, num_lt, num_rt)
local letters = cfg.letters()
local root = mw.html.create('')
if num_lt then
root:tag('td')
end
for k = 1, ncols do
root:tag('td')
:css('height', '18px')
:css('width', size .. 'px')
:wikitext(rev and letters[1 + ncols - k] or letters[k])
end
if num_rt then
root:tag('td')
end
return tostring(root)
end
local letters_top = letters:match('both') or letters:match('top')
local letters_bottom = letters:match('both') or letters:match('bottom')
local numbers_left = numbers:match('both') or numbers:match('left')
local numbers_right = numbers:match('both') or numbers:match('right')
local width = ncols * size + 2
if (numbers_left) then
width = width + 18
end
if (numbers_right) then
width = width + 18
end
local root = mw.html.create('div')
:addClass('chessboard')
:addClass('calculator-container')
:addClass('thumb')
:addClass('noviewer')
:addClass(align)
if (header and header ~= '') then
root:tag('div')
:addClass('center')
:css('line-height', '130%')
:css('margin', '0 auto')
:css('max-width', (width + ncols) .. 'px')
:wikitext(header)
end
local div = root:tag('div')
:addClass('thumbinner')
:css('width', width .. 'px')
local b = div:tag('table')
:attr('cellpadding', '0')
:attr('cellspacing', '0')
:addClass('calculator-field')
:attr('data-calculator-type', 'passthru')
:attr('data-calculator-formula', 'rotate')
if (letters_top) then
b:tag('tr')
:wikitext(letters_row(rev, numbers_left, numbers_right))
end
local tablerow = b:tag('tr')
if (numbers_left) then
tablerow:tag('td')
:css('width', '18px')
:css('height', size .. 'px')
:wikitext(rev and 1 or nrows)
end
local td = tablerow:tag('td')
:attr('colspan', ncols)
:attr('rowspan', nrows)
:addClass('pieces')
:wikitext(innerboard(definedPieces, pieceInfo, size, rev))
if (numbers_right) then
tablerow:tag('td')
:css('width', '18px')
:css('height', size .. 'px')
:wikitext(rev and 1 or nrows)
end
if (numbers_left or numbers_right) then
for trow = 2, nrows do
local idx = rev and trow or (1 + nrows - trow)
tablerow = b:tag('tr')
if (numbers_left) then
tablerow:tag('td')
:css('height', size .. 'px')
:wikitext(idx)
end
if (numbers_right) then
tablerow:tag('td')
:css('height', size .. 'px')
:wikitext(idx)
end
end
end
if (letters_bottom) then
b:tag('tr')
:wikitext(letters_row(rev, numbers_left, numbers_right))
end
if footer and mw.text.trim(footer) ~= '' then
div:tag('div')
:addClass('thumbcaption')
:wikitext(footer)
end
return tostring(root) ..
mw.getCurrentFrame():extensionTag('templatestyles', '', { src = 'Module:Chessboard/styles.css' })
end
function convertFenToArgs(fen)
-- converts FEN notation to 64 entry array of positions, offset by 2
local res = { ' ', ' ' }
-- Loop over rows, which are delimited by /
for srow in string.gmatch("/" .. fen, "/%w+") do
-- Loop over all letters and numbers in the row
for piece in srow:gmatch("%w") do
if piece:match("%d") then
-- if a digit
for k = 1, piece do
table.insert(res, ' ')
end
else
-- not a digit
local color = piece:match('%u') and 'l' or 'd'
piece = piece:lower()
table.insert(res, piece .. color)
end
end
end
return res
end
local function getPieceMoves(definedPieces, pieceInfo)
local calc = Calculator:new { scoped = false }
for piece in pairs(definedPieces) do
local curTop = -1
local curLeft = -1
local switchTop = 'switch(move,'
local switchLeft = 'switch(move,'
for moveNumber, move in ipairs(pieceInfo) do
if (move[piece] and move[piece][1] or -1) ~= curTop then
switchTop = switchTop .. (moveNumber - 1) .. ',' .. curTop .. ','
curTop = (move[piece] and move[piece][1] or -1)
end
if (move[piece] and move[piece][2] or -1) ~= curLeft then
switchLeft = switchLeft .. (moveNumber - 1) .. ',' .. curLeft .. ','
curLeft = (move[piece] and move[piece][2] or -1)
end
end
switchTop = switchTop .. curTop .. ')'
switchLeft = switchLeft .. curLeft .. ')'
calc:hidden {
id = 'top_piece' .. piece,
formula = switchTop
}
calc:hidden {
id = 'left_piece' .. piece,
formula = switchLeft
}
end
return tostring(calc)
end
function p.board(frame)
local args = frame.args
local pargs = frame:getParent().args
nrows, ncols = cfg.dims()
local size = args.size or pargs.size or '26'
local reverse = (args.reverse or pargs.reverse or ''):lower() == "true"
local letters = (args.letters or pargs.letters or 'both'):lower()
local numbers = (args.numbers or pargs.numbers or 'both'):lower()
local header = args[2] or pargs[2] or ''
local footer = args[nrows * ncols + 3] or pargs[nrows * ncols + 3] or ''
local align = (args[1] or pargs[1] or 'tright'):lower()
local clear = args.clear or pargs.clear or (align:match('tright') and 'right') or 'none'
local pgn = args.pgn or pargs.pgn
local moves, metadata
local definedPieces, pieceInfo
size = mw.ustring.match(size, '[%d]+') or '26' -- remove px from size
assert(pgn, "pgn argument required")
moves, metadata = pgn2fen(pgn)
definedPieces, pieceInfo = getPositions(moves, size, reverse)
align = args.align or pargs.align or 'tright'
clear = args.clear or pargs.clear or (align:match('tright') and 'right') or 'none'
header = args.header or pargs.header or ''
footer = args.footer or pargs.footer or ''
if args.animate or pargs.animate then
align = align .. ' animate'
end
local controls = Calculator:new { scoped = false, noJSFallback = '' }
controls:button { text = '|←', ['for'] = 'move', type = 'default', formula = 1 }
controls:button { text = '←', ['for'] = 'move', type = 'default', formula = 'max(1, move-1)' }
controls:button { text = '☯', ['for'] = 'rotate', type = 'default', formula = '(rotate+1)%2' }
controls:button { text = '→', ['for'] = 'move', type = 'default', formula = 'min('.. #moves..', move+1)' }
controls:button { text = '→|', ['for'] = 'move', type = 'default', formula = #moves }
controls:hidden { id = 'move', default = 1 }
controls:hidden { id = 'rotate', default = 0 }
header = header .. tostring(controls)
footer = footer
.. frame:extensionTag('templatestyles', '', { src = 'Module:Sandbox/SD0001/Chess3/animations.css' })
.. getPieceMoves(definedPieces, pieceInfo)
return chessboard(definedPieces, pieceInfo, size, reverse, letters, numbers, header, footer, align, clear)
end
return p
Content Disclaimer
Informasi ini disarikan dari Wikipedia dan disajikan kembali untuk tujuan edukasi. Konten tersedia di bawah lisensi CC BY-SA 3.0. Kami tidak bertanggung jawab atas ketidakakuratan data yang bersumber dari kontribusi publik tersebut.
- The information displayed on this website is sourced in part or in whole from Wikipedia and has been adapted for the purpose of restating it. We strive to provide accurate and relevant information, however:
- There is no guarantee of absolute accuracy. Wikipedia is an open, collaborative project that can be edited by anyone, so information is subject to change.
- It is not intended to constitute professional advice. The content displayed is for informational and educational purposes only. For important decisions (e.g., medical, legal, or financial), please consult a professional.
- Content copyright. Wikipedia is licensed under the Creative Commons Attribution-ShareAlike License (CC BY-SA). This means that content may be reused with appropriate attribution and shared under a similar license.
- Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.