Module:Peg solitaire sequence
-- Module:Peg solitaire sequence
--
-- Renders multi-frame peg-solitaire galleries from either:
-- (1) a compact move list, or
-- (2) explicit board1=..., board2=..., ... parameters.
--
-- Depends on Template:Peg solitaire diagram already existing.
local p = {}
local function trim(s)
if s == nil then
return nil
end
s = mw.text.trim(tostring(s))
if s == '' then
return nil
end
return s
end
local function yesno(v, default)
v = trim(v)
if v == nil then
return default
end
v = mw.ustring.lower(v)
if v == '1' or v == 'yes' or v == 'y' or v == 'true' then
return true
elseif v == '0' or v == 'no' or v == 'n' or v == 'false' then
return false
end
return default
end
local function mergedArgs(frame)
local args = {}
if frame:getParent() then
for k, v in pairs(frame:getParent().args) do
if trim(v) ~= nil then
args[k] = v
end
end
end
for k, v in pairs(frame.args) do
if trim(v) ~= nil then
args[k] = v
end
end
return args
end
local PRESETS = {
english = table.concat({
'--...--',
'--...--',
'.......',
'...o...',
'.......',
'--...--',
'--...--',
}, '/'),
english_full = table.concat({
'--...--',
'--...--',
'.......',
'.......',
'.......',
'--...--',
'--...--',
}, '/'),
european = table.concat({
'--...--',
'-.....-',
'.......',
'...o...',
'.......',
'-.....-',
'--...--',
}, '/'),
european_full = table.concat({
'--...--',
'-.....-',
'.......',
'.......',
'.......',
'-.....-',
'--...--',
}, '/'),
triangle15 = table.concat({
'----.----',
'---..---',
'--...--',
'-..o.-',
'.....',
}, '/'),
triangle15_full = table.concat({
'----.----',
'---..---',
'--...--',
'-....-',
'.....',
}, '/'),
}
local function buildEnglishMap()
local m = {}
local rows = {
{ nil, nil, 'a', 'b', 'c', nil, nil },
{ nil, nil, 'd', 'e', 'f', nil, nil },
{ 'g', 'h', 'i', 'j', 'k', 'l', 'm' },
{ 'n', 'o', 'p', 'x', 'P', 'O', 'N' },
{ 'M', 'L', 'K', 'J', 'I', 'H', 'G' },
{ nil, nil, 'F', 'E', 'D', nil, nil },
{ nil, nil, 'C', 'B', 'A', nil, nil },
}
for r = 1, #rows do
for c = 1, #rows[r] do
local v = rows[r][c]
if v then
m[v] = { r = r, c = c }
end
end
end
return m
end
local function buildEuropeanMap()
local m = {}
local rows = {
{ nil, nil, 'a', 'b', 'c', nil, nil },
{ nil, 'y', 'd', 'e', 'f', 'z', nil },
{ 'g', 'h', 'i', 'j', 'k', 'l', 'm' },
{ 'n', 'o', 'p', 'x', 'P', 'O', 'N' },
{ 'M', 'L', 'K', 'J', 'I', 'H', 'G' },
{ nil, 'Z', 'F', 'E', 'D', 'Y', nil },
{ nil, nil, 'C', 'B', 'A', nil, nil },
}
for r = 1, #rows do
for c = 1, #rows[r] do
local v = rows[r][c]
if v then
m[v] = { r = r, c = c }
end
end
end
return m
end
local function buildTriangle15NumberMap()
local m = {}
local n = 1
-- These coordinates must match the actual parsed columns of:
-- ----.----
-- ---..---
-- --...--
-- -....-
-- .....
--
-- So the playable cells are:
-- row 1: col 5
-- row 2: col 4,5
-- row 3: col 3,4,5
-- row 4: col 2,3,4,5
-- row 5: col 1,2,3,4,5
for r = 1, 5 do
local startCol = 6 - r
for i = 0, r - 1 do
m[tostring(n)] = { r = r, c = startCol + i }
n = n + 1
end
end
return m
end
local NAMED_MAPS = {
english = buildEnglishMap(),
english_full = buildEnglishMap(),
european = buildEuropeanMap(),
european_full = buildEuropeanMap(),
triangle15 = buildTriangle15NumberMap(),
triangle15_full = buildTriangle15NumberMap(),
}
local function splitRows(raw)
raw = raw:gsub('\r\n', '\n'):gsub('\r', '\n')
local rows = {}
if raw:find('/') then
for row in mw.text.gsplit(raw, '/', true) do
table.insert(rows, row)
end
else
for row in mw.text.gsplit(raw, '\n', false) do
if trim(row) ~= nil then
table.insert(rows, row)
end
end
end
return rows
end
local function parseBoard(raw)
local rows = splitRows(raw)
local cells = {}
local maxCols = 0
for r, row in ipairs(rows) do
cells[r] = {}
for ch in mw.ustring.gmatch(row, '.') do
if ch ~= ' ' and ch ~= '\t' and ch ~= '|' and ch ~= ',' then
if ch == '.' or ch == '·' or ch == '1' then
table.insert(cells[r], 'peg')
elseif ch == 'o' or ch == '0' then
table.insert(cells[r], 'hole')
else
table.insert(cells[r], 'void')
end
end
end
if #cells[r] > maxCols then
maxCols = #cells[r]
end
end
for r = 1, #cells do
while #cells[r] < maxCols do
table.insert(cells[r], 'void')
end
end
return {
rows = #cells,
cols = maxCols,
cells = cells,
}
end
local function cloneBoard(board)
local out = {
rows = board.rows,
cols = board.cols,
cells = {},
}
for r = 1, board.rows do
out.cells[r] = {}
for c = 1, board.cols do
out.cells[r][c] = board.cells[r][c]
end
end
return out
end
local function boardToRaw(board)
local rows = {}
for r = 1, board.rows do
local t = {}
for c = 1, board.cols do
local s = board.cells[r][c]
if s == 'peg' then
table.insert(t, '.')
elseif s == 'hole' then
table.insert(t, 'o')
else
table.insert(t, '-')
end
end
table.insert(rows, table.concat(t))
end
return table.concat(rows, '/')
end
local function coord0(r, c)
return tostring(r - 1) .. ':' .. tostring(c - 1)
end
local function splitList(s)
local out = {}
s = trim(s)
if not s then
return out
end
s = s:gsub('\n', ';')
for item in mw.text.gsplit(s, ';', true) do
item = trim(item)
if item then
table.insert(out, item)
end
end
return out
end
local function applyStateList(board, list, state, namedMap)
for _, token in ipairs(splitList(list)) do
local r, c
local named = namedMap[token]
if named then
r, c = named.r, named.c
else
local r0, c0 = token:match('^(%-?%d+)%s*:%s*(%-?%d+)$')
if r0 and c0 then
r, c = tonumber(r0) + 1, tonumber(c0) + 1
end
end
if r and c and board.cells[r] and board.cells[r][c] and board.cells[r][c] ~= 'void' then
board.cells[r][c] = state
end
end
end
local function parseOneCoord(token, namedMap)
token = trim(token)
if not token then
return nil, nil
end
local named = namedMap[token]
if named then
return named.r, named.c
end
local r0, c0 = token:match('^(%-?%d+)%s*:%s*(%-?%d+)$')
if r0 and c0 then
return tonumber(r0) + 1, tonumber(c0) + 1
end
local r1, c1 = token:match('^[Rr](%d+)[Cc](%d+)$')
if r1 and c1 then
return tonumber(r1), tonumber(c1)
end
local letters, digits = token:match('^([A-Za-z]+)(%d+)$')
if letters and digits then
local col = 0
letters = mw.ustring.upper(letters)
for ch in mw.ustring.gmatch(letters, '.') do
col = col * 26 + (mw.ustring.byte(ch) - 64)
end
return tonumber(digits), col
end
local r2, c2 = token:match('^(%d+)%s*,%s*(%d+)$')
if r2 and c2 then
return tonumber(r2), tonumber(c2)
end
return nil, nil
end
local function getGeometry(preset)
if preset and preset:match('^triangle15') then
return 'triangle'
end
return 'orthogonal'
end
local function midpointOrthogonal(r1, c1, r2, c2)
if r1 == r2 and math.abs(c2 - c1) == 2 then
return r1, (c1 + c2) / 2
end
if c1 == c2 and math.abs(r2 - r1) == 2 then
return (r1 + r2) / 2, c1
end
return nil, nil
end
local function midpointTriangle(r1, c1, r2, c2)
local dr = r2 - r1
local dc = c2 - c1
-- horizontal
if dr == 0 and math.abs(dc) == 2 then
return r1, (c1 + c2) / 2
end
-- down-right / up-left in rendered coordinates
if dr == 2 and dc == 0 then
return r1 + 1, c1
end
if dr == -2 and dc == 0 then
return r1 - 1, c1
end
-- down-left / up-right in rendered coordinates
if dr == 2 and dc == -2 then
return r1 + 1, c1 - 1
end
if dr == -2 and dc == 2 then
return r1 - 1, c1 + 1
end
return nil, nil
end
local function midpointFor(geometry, r1, c1, r2, c2)
if geometry == 'triangle' then
return midpointTriangle(r1, c1, r2, c2)
end
return midpointOrthogonal(r1, c1, r2, c2)
end
local function parseMoveChain(token, namedMap)
local out = {}
for part in mw.text.gsplit(token, '-', true) do
part = trim(part)
if part then
local r, c = parseOneCoord(part, namedMap)
if not r or not c then
error('Unrecognized coordinate in move token: ' .. token)
end
table.insert(out, { r = r, c = c, text = part })
end
end
if #out < 2 then
error('Move token must contain at least two positions: ' .. token)
end
return out
end
local function removeCoordFromList(list, coord)
local out = {}
for _, v in ipairs(list) do
if v ~= coord then
table.insert(out, v)
end
end
return out
end
local function applyMoveChain(board, chain, geometry)
local out = cloneBoard(board)
local marks = {
from = {},
over = {},
to = nil,
}
for i = 1, #chain - 1 do
local s = chain[i]
local d = chain[i + 1]
local mr, mc = midpointFor(geometry, s.r, s.c, d.r, d.c)
if not mr or not mc then
error(string.format('Illegal jump geometry from %s to %s', s.text, d.text))
end
if not out.cells[s.r] or not out.cells[s.r][s.c] or out.cells[s.r][s.c] ~= 'peg' then
error('Source is not a peg at ' .. s.text)
end
if not out.cells[mr] or not out.cells[mr][mc] or out.cells[mr][mc] ~= 'peg' then
error('Jumped cell is not a peg between ' .. s.text .. ' and ' .. d.text)
end
if not out.cells[d.r] or not out.cells[d.r][d.c] or out.cells[d.r][d.c] ~= 'hole' then
error('Destination is not a hole at ' .. d.text)
end
out.cells[s.r][s.c] = 'hole'
out.cells[mr][mc] = 'hole'
out.cells[d.r][d.c] = 'peg'
table.insert(marks.from, coord0(s.r, s.c))
table.insert(marks.over, coord0(mr, mc))
marks.to = coord0(d.r, d.c)
end
marks.from = removeCoordFromList(marks.from, marks.to)
marks.over = removeCoordFromList(marks.over, marks.to)
return out, marks
end
local function expandDiagram(frame, args)
return frame:expandTemplate{
title = 'Peg solitaire diagram',
args = args
}
end
local function parseChunkedMoves(raw)
local chunks = {}
raw = raw:gsub('\r\n', '\n'):gsub('\r', '\n')
for chunkText in mw.text.gsplit(raw, '/', true) do
local chunk = {}
for token in chunkText:gmatch('[^,%s]+') do
token = trim(token)
if token then
table.insert(chunk, token)
end
end
if #chunk > 0 then
table.insert(chunks, chunk)
end
end
return chunks
end
local function renderMoveMode(frame, args)
local preset = trim(args.preset) or 'english'
local namedMap = NAMED_MAPS[preset] or {}
local geometry = getGeometry(preset)
local rawBoard = trim(args.board) or PRESETS[preset]
if not rawBoard then
error('Unknown preset: ' .. tostring(preset))
end
local board = parseBoard(rawBoard)
applyStateList(board, args.vacancy or args.empty, 'hole', namedMap)
applyStateList(board, args.fill, 'peg', namedMap)
local chunks = parseChunkedMoves(assert(trim(args.moves), 'Missing moves= parameter'))
local root = mw.html.create('div')
:addClass('pegsol-seq-root')
local includeStart = yesno(args.include_start, true)
local startLabel = trim(args.start_label)
local showLabels = yesno(args.show_move_labels, true)
local firstChunk = true
for _, chunk in ipairs(chunks) do
local row = root:tag('div')
:addClass('pegsol-seq-row')
if firstChunk and includeStart then
row:wikitext(expandDiagram(frame, {
board = boardToRaw(board),
label = startLabel,
cell = trim(args.cell),
gap = trim(args.gap),
layout = geometry == 'triangle' and 'triangle' or nil,
}))
end
for _, token in ipairs(chunk) do
local chain = parseMoveChain(token, namedMap)
local nextBoard, marks = applyMoveChain(board, chain, geometry)
row:wikitext(expandDiagram(frame, {
board = boardToRaw(nextBoard),
label = showLabels and token or nil,
from = table.concat(marks.from, ';'),
over = table.concat(marks.over, ';'),
to = marks.to,
cell = trim(args.cell),
gap = trim(args.gap),
layout = geometry == 'triangle' and 'triangle' or nil,
}))
board = nextBoard
end
firstChunk = false
end
if trim(args.caption) and not yesno(args._suppresscaption, false) then
root:tag('div')
:addClass('pegsol-seq-caption')
:wikitext(args.caption)
end
return tostring(root)
end
local function parseBreakSet(s)
local out = {}
s = trim(s)
if not s then
return out
end
for token in s:gmatch('[^,%s;]+') do
local n = tonumber(token)
if n then
out[n] = true
end
end
return out
end
local function renderExplicitMode(frame, args)
local root = mw.html.create('div')
:addClass('pegsol-seq-root')
local breaks = parseBreakSet(args.breaks)
local row = root:tag('div')
:addClass('pegsol-seq-row')
local i = 1
while true do
local boardArg = trim(args['board' .. tostring(i)])
if not boardArg then
break
end
if i > 1 and breaks[i - 1] then
row = root:tag('div')
:addClass('pegsol-seq-row')
end
row:wikitext(expandDiagram(frame, {
board = boardArg,
label = trim(args['label' .. tostring(i)]),
from = trim(args['from' .. tostring(i)]),
over = trim(args['over' .. tostring(i)]),
to = trim(args['to' .. tostring(i)]),
cell = trim(args.cell),
gap = trim(args.gap),
layout = trim(args['layout' .. tostring(i)]) or trim(args.layout),
}))
i = i + 1
end
if trim(args.caption) and not yesno(args._suppresscaption, false) then
root:tag('div')
:addClass('pegsol-seq-caption')
:wikitext(args.caption)
end
return tostring(root)
end
function p.main(frame)
local ok, result = pcall(function()
local args = mergedArgs(frame)
if trim(args.moves) then
return renderMoveMode(frame, args)
end
if trim(args.board1) then
return renderExplicitMode(frame, args)
end
error('Supply either moves=... or board1=..., board2=..., ...')
end)
if ok then
return result
end
return tostring(
mw.html.create('strong')
:addClass('error')
:wikitext('Peg solitaire sequence error: ' .. tostring(result))
)
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.