110 lines
3.5 KiB
Scala
110 lines
3.5 KiB
Scala
package gambit
|
|
|
|
final case class PgnGame(
|
|
tags: List[(String, String)],
|
|
startFen: String,
|
|
moves: List[Int],
|
|
result: String
|
|
)
|
|
|
|
object Pgn:
|
|
private val resultTokens = Set("1-0", "0-1", "1/2-1/2", "*")
|
|
|
|
def write(
|
|
startFen: String,
|
|
moves: List[Int],
|
|
result: String,
|
|
tags: List[(String, String)] = Nil
|
|
): String =
|
|
val header = tagSection(startFen, result, tags)
|
|
val movetext = wrap(renderMoves(startFen, moves) :+ result)
|
|
s"$header\n$movetext\n"
|
|
|
|
private def tagSection(startFen: String, result: String, extra: List[(String, String)]): String =
|
|
val provided = extra.map(_._1).toSet
|
|
val roster = List(
|
|
"Event" -> "gambit self-play",
|
|
"Site" -> "local",
|
|
"Date" -> "????.??.??",
|
|
"Round" -> "-",
|
|
"White" -> "gambit",
|
|
"Black" -> "gambit",
|
|
"Result" -> result
|
|
).filterNot(pair => provided.contains(pair._1))
|
|
val setup =
|
|
if startFen == Fen.startPosition then Nil
|
|
else List("SetUp" -> "1", "FEN" -> startFen)
|
|
val all = extra ++ roster ++ setup
|
|
all.map((k, v) => s"""[$k "$v"]""").mkString("\n")
|
|
|
|
private def renderMoves(startFen: String, moves: List[Int]): List[String] =
|
|
val pos = Fen.parse(startFen)
|
|
val out = scala.collection.mutable.ListBuffer[String]()
|
|
var first = true
|
|
var index = 0
|
|
while index < moves.length do
|
|
val m = moves(index)
|
|
if pos.sideToMove == Color.White then out += s"${pos.fullmove}."
|
|
else if first then out += s"${pos.fullmove}..."
|
|
out += San.toSan(pos, m)
|
|
pos.make(m)
|
|
first = false
|
|
index += 1
|
|
out.toList
|
|
|
|
private def wrap(tokens: List[String], width: Int = 80): String =
|
|
val sb = new StringBuilder
|
|
var lineLen = 0
|
|
for token <- tokens do
|
|
val add = if lineLen == 0 then token.length else token.length + 1
|
|
if lineLen > 0 && lineLen + add > width then
|
|
sb.append('\n')
|
|
sb.append(token)
|
|
lineLen = token.length
|
|
else
|
|
if lineLen > 0 then sb.append(' ')
|
|
sb.append(token)
|
|
lineLen += add
|
|
sb.toString
|
|
|
|
def read(text: String): PgnGame =
|
|
val tags = parseTags(text)
|
|
val startFen = tags.find(_._1 == "FEN").map(_._2).getOrElse(Fen.startPosition)
|
|
val body = text.linesIterator.filterNot(_.trim.startsWith("[")).mkString(" ")
|
|
val cleaned = stripDecorations(body)
|
|
val tokens = cleaned.split("\\s+").filter(_.nonEmpty).toList
|
|
val pos = Fen.parse(startFen)
|
|
val moves = scala.collection.mutable.ListBuffer[Int]()
|
|
var result = "*"
|
|
for raw <- tokens do
|
|
val token = raw.replaceAll("^\\d+\\.+", "")
|
|
if token.isEmpty then ()
|
|
else if resultTokens.contains(token) then result = token
|
|
else
|
|
val m = San.parse(pos, token)
|
|
if m != 0 then
|
|
moves += m
|
|
pos.make(m)
|
|
PgnGame(tags, startFen, moves.toList, result)
|
|
|
|
private def parseTags(text: String): List[(String, String)] =
|
|
val pattern = """\[(\w+)\s+"([^"]*)"\]""".r
|
|
pattern.findAllMatchIn(text).map(m => (m.group(1), m.group(2))).toList
|
|
|
|
private def stripDecorations(body: String): String =
|
|
val noComments = removePaired(body, '{', '}')
|
|
val noVariations = removePaired(noComments, '(', ')')
|
|
noVariations.replaceAll("\\$\\d+", " ")
|
|
|
|
private def removePaired(s: String, open: Char, close: Char): String =
|
|
val sb = new StringBuilder
|
|
var depth = 0
|
|
var i = 0
|
|
while i < s.length do
|
|
val c = s.charAt(i)
|
|
if c == open then depth += 1
|
|
else if c == close then (if depth > 0 then depth -= 1)
|
|
else if depth == 0 then sb.append(c)
|
|
i += 1
|
|
sb.toString
|