All Downloads are FREE. Search and download functionalities are using the official Maven repository.

format.csa.CsaModel.scala Maven / Gradle / Ivy

There is a newer version: 12.1.1
Show newest version
package shogi
package format
package csa

import cats.syntax.option._

import shogi.variant.Standard
import shogi.format.usi.Usi

final case class Csa(
    tags: Tags,
    moves: List[NotationMove],
    initial: Initial = Initial.empty
) extends Notation {

  def withMoves(moves: List[NotationMove]) =
    copy(moves = moves)

  def withTags(tags: Tags) =
    copy(tags = tags)

  private def renderMainline(moveline: List[NotationMove], turn: Color): String =
    moveline
      .foldLeft[(List[String], Color)]((Nil, turn)) { case ((acc, curTurn), cur) =>
        (Csa.renderNotationMove(cur, curTurn.some) :: acc, !curTurn)
      }
      ._1
      .reverse mkString "\n"

  def render: String = {
    val initStr =
      if (initial.comments.nonEmpty)
        initial.comments.map(Csa.fixComment _).mkString("")
      else ""
    val header = Csa renderHeader tags
    val setup =
      (tags.sfen | Standard.initialSfen).toSituation(Standard).fold("")(Csa renderSituation _)
    val startColor: Color = tags.sfen.flatMap(_.color) | Sente
    val movesStr          = renderMainline(moves, startColor)
    List[String](
      header,
      setup,
      initStr,
      movesStr
    ).filter(_.nonEmpty).mkString("\n")
  }.trim

  override def toString = render
}

object Csa {

  def renderNotationMove(cur: NotationMove, turn: Option[Color]) = {
    val csaMove     = renderCsaMove(cur.usiWithRole, turn)
    val timeStr     = clockString(cur) | ""
    val commentsStr = cur.comments.map { text => s"\n'${fixComment(text)}" }.mkString("")
    val resultStr   = cur.result.fold("")(t => s"\n$t")
    s"$csaMove$timeStr$commentsStr$resultStr"
  }

  def renderCsaMove(usiWithRole: Usi.WithRole, turn: Option[Color]) =
    usiWithRole.usi match {
      case Usi.Drop(role, pos) =>
        s"${turn.fold("")(_.fold("+", "-"))}00${CsaUtils.makeCsaPos(pos)}${CsaUtils.toCsa(role) | ""}"
      case Usi.Move(orig, dest, prom, _) => {
        val finalRole = Standard.promote(usiWithRole.role).filter(_ => prom) | usiWithRole.role
        s"${turn.fold("")(_.fold("+", "-"))}${CsaUtils.makeCsaPos(orig)}${CsaUtils
            .makeCsaPos(dest)}${CsaUtils.toCsa(finalRole) | ""}"
      }
    }

  def renderHeader(tags: Tags): String =
    csaHeaderTags
      .map { ct =>
        if (ct == Tag.Sente || ct == Tag.Gote) {
          tags(ct.name).fold("")(tagValue =>
            if (isValidTagValue(tagValue))
              s"N${ct.csaName}${tagValue.replace(",", ";")}"
            else
              ""
          )
        } else {
          tags(ct.name).fold("")(tagValue => {
            if (isValidTagValue(tagValue)) s"$$${ct.csaName}:${tagValue.replace(",", ";")}"
            else ""
          })
        }
      }
      .filter(_.nonEmpty)
      .mkString("\n")

  // we want only ascii tags
  private def isValidTagValue(str: String): Boolean =
    str.nonEmpty && str != "?" && str.forall(c => c >= 32 && c < 127)

  def renderSituation(sit: Situation): String = {
    val csaBoard = new scala.collection.mutable.StringBuilder(256)
    for (y <- 0 to 8) {
      csaBoard append ("P" + (y + 1))
      for (x <- 8 to 0 by -1) {
        sit.board(x, y).flatMap(CsaUtils toCsa _) match {
          case None => csaBoard append " * "
          case Some(csa) =>
            csaBoard append s"$csa"
        }
      }
      if (y < 8) csaBoard append '\n'
    }
    List[String](
      csaBoard.toString,
      renderHand(sit.hands(Sente), "P+"),
      renderHand(sit.hands(Gote), "P-"),
      if (sit.color.gote) "-" else "+"
    ).filter(_.nonEmpty).mkString("\n")
  }

  private def renderHand(hand: Hand, prefix: String): String = {
    if (hand.isEmpty) ""
    else
      Standard.handRoles
        .map { r =>
          val cnt = hand(r)
          s"00${CsaUtils.toCsa(r) | ""}".repeat(math.min(cnt, 81))
        }
        .filter(_.nonEmpty)
        .mkString(prefix, "", "")
  }

  def createTerminationMove(
      status: Status,
      winnerTurn: Boolean,
      winnerColor: Option[Color]
  ): Option[String] = {
    import Status._
    status match {
      case Aborted | NoStart                                 => "%CHUDAN".some
      case Timeout | Outoftime                               => "%TIME_UP".some
      case Resign if !winnerTurn                             => "%TORYO".some
      case PerpetualCheck if winnerColor.contains(Sente)     => "%-ILLEGAL_ACTION".some
      case PerpetualCheck                                    => "%+ILLEGAL_ACTION".some
      case Mate if winnerTurn && winnerColor.contains(Sente) => "%-ILLEGAL_ACTION".some // pawn checkmate
      case Mate if winnerTurn                                => "%+ILLEGAL_ACTION".some // pawn checkmate
      case Mate | Stalemate                                  => "%TSUMI".some
      case Draw                                              => "%SENNICHITE".some
      case Impasse27                                         => "%KACHI".some
      case _                                                 => None
    }
  }

  // tags we render in header
  private val csaHeaderTags = List[TagType](
    Tag.Sente,
    Tag.Gote,
    Tag.Event,
    Tag.Site,
    Tag.Start,
    Tag.End,
    Tag.TimeControl,
    Tag.SenteTeam,
    Tag.GoteTeam,
    Tag.Opening
  )

  private def clockString(cur: NotationMove): Option[String] =
    cur.secondsSpent.map(spent => s",T$spent")

  private val noDoubleLineBreakRegex = "(\r?\n){2,}".r

  private def fixComment(txt: String) =
    noDoubleLineBreakRegex.replaceAllIn(txt, "\n").replace("\n", "\n'")

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy