Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 23 additions & 23 deletions shared/src/main/scala/com/mogproject/mogami/core/Game.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.mogproject.mogami.core

import com.mogproject.mogami._
import com.mogproject.mogami.core.io._
import com.mogproject.mogami.core.move.MoveBuilder
import com.mogproject.mogami.core.move.{Move => _, MoveBuilderCsa => _, _}
import com.mogproject.mogami.util.Implicits._

import scala.annotation.tailrec
Expand All @@ -12,11 +12,12 @@ import scala.util.Try
* Game
*/
case class Game(initialState: State = State.HIRATE,
moves: Vector[move.Move] = Vector.empty,
moves: Vector[Move] = Vector.empty,
gameInfo: GameInfo = GameInfo(),
movesOffset: Int = 0,
finalAction: Option[SpecialMove] = None,
givenHistory: Option[Vector[State]] = None
) extends CsaLike with SfenLike with KifGameWriter {
) extends CsaGameWriter with SfenLike with KifGameWriter {

require(history.length == moves.length + 1, "all moves must be valid")

Expand All @@ -25,7 +26,11 @@ case class Game(initialState: State = State.HIRATE,
override def equals(obj: scala.Any): Boolean = obj match {
case that: Game =>
// ignore givenHistory
initialState == that.initialState && moves == that.moves && gameInfo == that.gameInfo && movesOffset == that.movesOffset
initialState == that.initialState &&
moves == that.moves &&
gameInfo == that.gameInfo &&
movesOffset == that.movesOffset &&
finalAction == that.finalAction
case _ => false
}

Expand All @@ -47,7 +52,12 @@ case class Game(initialState: State = State.HIRATE,
} else if (isRepetition) {
Drawn // Sennichite
} else {
Playing
finalAction match {
case Some(IllegalMove(_)) => IllegallyMoved
case Some(Resign(_)) => Resigned
case Some(TimeUp(_)) => TimedUp
case _ => Playing
}
}
}

Expand All @@ -66,9 +76,6 @@ case class Game(initialState: State = State.HIRATE,

def makeMove(move: MoveBuilder): Option[Game] = move.toMove(currentState).flatMap(makeMove)

override def toCsaString: String =
(gameInfo :: initialState :: moves.toList) map (_.toCsaString) filter (!_.isEmpty) mkString "\n"

override def toSfenString: String = (initialState.toSfenString :: movesOffset.toString :: moves.map(_.toSfenString).toList).mkString(" ")

/**
Expand All @@ -82,20 +89,7 @@ case class Game(initialState: State = State.HIRATE,
(history.drop(1).reverse.takeWhile(s => s.turn == !turn || s.isChecked).count(_.hashCode() == currentState.hashCode()) >= 4)
}

object Game extends CsaFactory[Game] with SfenFactory[Game] with KifGameReader {
override def parseCsaString(s: String): Option[Game] = {
def isStateText(t: String) = t.startsWith("P") || t == "+" || t == "-"

for {
xs <- Some(s.split("[;\n]").filter(s => !s.startsWith("'"))) // ignore comment lines
(a, ys) = xs.span(!isStateText(_))
(b, c) = ys.span(isStateText)
gi <- GameInfo.parseCsaString(a)
st <- State.parseCsaString(b)
moves = c.flatMap(s => move.MoveBuilderCsa.parseCsaString(s)) if moves.length == c.length
game <- moves.foldLeft(Some(Game(st, Vector.empty, gi)): Option[Game])((g, m) => g.flatMap(_.makeMove(m)))
} yield game
}
object Game extends CsaGameReader with SfenFactory[Game] with KifGameReader {

override def parseSfenString(s: String): Option[Game] = {
val tokens = s.split(" ")
Expand Down Expand Up @@ -123,6 +117,11 @@ object Game extends CsaFactory[Game] with SfenFactory[Game] with KifGameReader {

case object Drawn extends GameStatus

case object TimedUp extends GameStatus

case object IllegallyMoved extends GameStatus

case object Resigned extends GameStatus
}

}
Expand Down Expand Up @@ -151,7 +150,8 @@ case class GameInfo(tags: Map[Symbol, String] = Map()) extends CsaLike {
example:

#KIF version=2.0 encoding=UTF-8
開始日時:2017/03/13
開始日時:2017/03/13 ??:??
終了日時:2017/03/13 ??:??
場所:81Dojo (ver.2016/03/20)
持ち時間:15分+60秒
手合割:平手
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.mogproject.mogami.core.io

import com.mogproject.mogami.core.move._
import com.mogproject.mogami.core.{Game, GameInfo, State}

import scala.annotation.tailrec

/**
*/
trait CsaGameIO {

}

/**
* Writes Csa-formatted game
*/
trait CsaGameWriter extends CsaGameIO with CsaLike {
def initialState: State

def moves: Vector[Move]

def gameInfo: GameInfo

def finalAction: Option[SpecialMove]

override def toCsaString: String =
(gameInfo :: initialState :: (moves ++ finalAction).toList) map (_.toCsaString) filter (!_.isEmpty) mkString "\n"

}

/**
* Reads Csa-formatted game
*/
trait CsaGameReader extends CsaGameIO with CsaFactory[Game] {

private def isStateText(t: String): Boolean = t.startsWith("P") || t == "+" || t == "-"

private def isValidLine(s: String): Boolean = s.nonEmpty && !s.startsWith("'") && !s.startsWith("%CHUDAN")

private def concatMoveLines(lines: List[String]): List[String] = {
@tailrec
def f(ss: List[String], latest: String, sofar: List[String]): List[String] = (ss, latest.isEmpty) match {
case (x :: xs, true) => f(xs, x, sofar)
case (x :: xs, false) if x.startsWith("T") => f(xs, "", s"${latest},${x}" :: sofar)
case (x :: xs, false) => f(xs, x, latest :: sofar)
case (Nil, true) => sofar.reverse
case (Nil, false) => (latest :: sofar).reverse
}

f(lines, "", Nil)
}

// todo: refactor w/ KifGameIO
@tailrec
final protected[io] def parseMovesCsa(chunks: List[String], pending: Option[Move], sofar: Option[Game]): Option[Game] = (chunks, pending, sofar) match {
case (x :: Nil, None, Some(g)) if x.startsWith("%") => // ends with a special move
MoveBuilderCsa.parseTime(x) match {
case Some((ss, tm)) =>
(ss match {
case Resign.csaKeyword => Some(Resign(tm))
case TimeUp.csaKeyword => Some(TimeUp(tm))
case _ => None // unknown command
}).map(sm => g.copy(finalAction = Some(sm)))
case None => None // format error
}
case (IllegalMove.csaKeyword :: Nil, Some(mv), Some(g)) => // ends with an explicit illegal move
Some(g.copy(finalAction = Some(IllegalMove(mv))))
case (Nil, Some(mv), Some(g)) => // ends with implicit illegal move
Some(g.copy(finalAction = Some(IllegalMove(mv))))
case (Nil, None, Some(g)) => sofar // ends without errors
case (x :: xs, None, Some(g)) => MoveBuilderCsa.parseCsaString(x) match {
case Some(bldr) => bldr.toMove(g.currentState, isStrict = false) match {
case Some(mv) => mv.verify.flatMap(g.makeMove) match {
case Some(gg) => parseMovesCsa(xs, None, Some(gg)) // legal move
case None => parseMovesCsa(xs, Some(mv), sofar) // illegal move
}
case None => None // failed to create Move
}
case None => None // failed to parse Move string
}
case _ => None
}

override def parseCsaString(s: String): Option[Game] = {
for {
xs <- Some(s.split("[,\n]").filter(isValidLine))
(a, ys) = xs.span(!isStateText(_))
(b, c) = ys.span(isStateText)
gi <- GameInfo.parseCsaString(a)
st <- State.parseCsaString(b)
chunks = concatMoveLines(c.toList)
game <- parseMovesCsa(chunks, None, Some(Game(st, Vector.empty, gi)))
} yield game
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ trait CsaLike {

def toCsaString: String

protected def timeToCsaString(time: Option[Int]): String = time.map(",T" + _.toString).getOrElse("")
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package com.mogproject.mogami.core.io

import com.mogproject.mogami.core.move.{Move, MoveBuilderKif}
import com.mogproject.mogami.core.move._
import com.mogproject.mogami.core.{Game, GameInfo, State, StateConstant}

import scala.annotation.tailrec
import scala.util.Try

/**
* Common interface for Kif format
*/
trait KifGameInterface {
trait KifGameIO {
protected val presetStates: Map[String, State] = Map(
"平手" -> StateConstant.HIRATE,
"香落ち" -> StateConstant.HANDICAP_LANCE,
Expand All @@ -32,7 +35,7 @@ trait KifGameInterface {
/**
* Writes Kif-formatted game
*/
trait KifGameWriter extends KifGameInterface with KifLike {
trait KifGameWriter extends KifGameIO with KifLike {
def initialState: State

def moves: Vector[Move]
Expand All @@ -41,6 +44,8 @@ trait KifGameWriter extends KifGameInterface with KifLike {

def movesOffset: Int

def finalAction: Option[SpecialMove]

override def toKifString: String = {
// todo: add 上手/下手
val blackName = s"先手:${gameInfo.tags.getOrElse('blackName, "")}"
Expand All @@ -53,8 +58,9 @@ trait KifGameWriter extends KifGameInterface with KifLike {
(whiteName +: ss.take(13)) ++ (blackName +: ss.drop(13))
}

val body = "手数----指手----消費時間--" +: moves.zipWithIndex.map { case (m, n) =>
f"${n + movesOffset + 1}%4d ${m.toKifString}"
val ms = moves.map(_.toKifString) ++ finalAction.toList.flatMap(_.toKifString.split('\n'))
val body = "手数----指手----消費時間--" +: ms.zipWithIndex.map { case (m, n) =>
f"${n + movesOffset + 1}%4d ${m}"
}

(header ++ body).mkString("\n")
Expand All @@ -64,7 +70,54 @@ trait KifGameWriter extends KifGameInterface with KifLike {
/**
* Reads Kif-formatted game
*/
trait KifGameReader extends KifGameInterface with KifFactory[Game] {
trait KifGameReader extends KifGameIO with KifFactory[Game] {

private def isNormalMove(s: String): Boolean = s.headOption.exists(c => c == '同' || '1' <= c && c <= '9')

private def isValidLine(s: String): Boolean = s.nonEmpty && !s.startsWith("*") && !s.startsWith("#")

private def createChunks(xs: Seq[String]): Seq[String] = xs.flatMap { s => {
val chunks = s.trim.split(" ", 2)
if (chunks.length < 2 || Try(chunks(0).toInt).isFailure)
Seq.empty
else
Seq(chunks(1))
}}

@tailrec
final protected[io] def parseMovesKif(chunks: List[String], pending: Option[Move], sofar: Option[Game]): Option[Game] = (chunks, pending, sofar) match {
case (x :: Nil, None, Some(g)) if !isNormalMove(x) => // ends with a special move
MoveBuilderKif.parseTime(x) match {
case Some((ss, tm)) =>
(ss match {
case Resign.kifKeyword => Some(Resign(tm))
case TimeUp.kifKeyword => Some(TimeUp(tm))
case _ => None // unknown command
}).map(sm => g.copy(finalAction = Some(sm)))
case None => None // format error
}
case (x :: Nil, Some(mv), Some(g)) if x.startsWith(IllegalMove.kifKeyword) => // ends with an explicit illegal move
Some(g.copy(finalAction = Some(IllegalMove(mv))))
case (Nil, Some(mv), Some(g)) => // ends with implicit illegal move
Some(g.copy(finalAction = Some(IllegalMove(mv))))
case (Nil, None, Some(g)) => sofar // ends without errors
case (x :: xs, None, Some(g)) => MoveBuilderKif.parseKifString(x) match {
case Some(bldr) => bldr.toMove(g.currentState, isStrict = false) match {
case Some(mv) => mv.verify.flatMap(g.makeMove) match {
case Some(gg) => parseMovesKif(xs, None, Some(gg)) // read the next line
case None => parseMovesKif(xs, Some(mv), sofar)
}
case None =>
None // failed to create Move
}
case None =>
None // failed to parse Move string
}
case _ =>
None
}


override def parseKifString(s: String): Option[Game] = {
def getPresetState(ls: Seq[String]): Option[State] =
ls.withFilter(_.startsWith("手合割:")).flatMap(ss => presetStates.get(ss.drop(4))).headOption
Expand All @@ -78,12 +131,12 @@ trait KifGameReader extends KifGameInterface with KifFactory[Game] {
}.mkString("\n"))

for {
xs <- Some(s.split('\n').filter(s => !s.startsWith("*") && !s.startsWith("#"))) // ignore comment lines
xs <- Some(s.split('\n').filter(isValidLine))
(header, body) = xs.span(!_.startsWith("手数"))
gi <- GameInfo.parseKifString(header.mkString("\n"))
st <- (getPresetState(header) #:: getDefinedState(header) #:: Stream.empty).flatten.headOption
moves = body.drop(1).flatMap(s => MoveBuilderKif.parseKifString(s.trim.split(" ", 2).drop(1).mkString))
game <- moves.foldLeft[Option[Game]](Some(Game(st, Vector.empty, gi)))((g, m) => g.flatMap(_.makeMove(m)))
chunks = createChunks(body.drop(1))
game <- parseMovesKif(chunks.toList, None, Some(Game(st, Vector.empty, gi)))
} yield game
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ trait KifLike {

def toKifString: String

protected def timeToKifString(time: Option[Int]): String = time.map(t => f" (${t / 60}%02d:${t % 60}%02d/)").getOrElse("")
}
Loading