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
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jdk: oraclejdk8

scala: 2.12.0

script: "sbt clean coverage test"
script: "travis_wait sbt clean coverage test"

after_success: "sbt coverageReport coveralls"

Expand Down
32 changes: 27 additions & 5 deletions src/main/scala/com/mogproject/mogami/core/Game.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.mogproject.mogami.core

import scala.annotation.tailrec
import com.mogproject.mogami._
import com.mogproject.mogami.core.io._
import com.mogproject.mogami.util.Implicits._

Expand All @@ -24,14 +25,32 @@ case class Game(initialState: State = State.HIRATE,

lazy val hashCodes: Seq[Int] = history.map(_.hashCode())

lazy val status: GameStatus = currentState.isMated.fold(Mated, Playing)
lazy val status: GameStatus = {
if (currentState.isMated) {
if (lastMove.exists(m => m.isDrop && m.oldPtype == PAWN))
Illegal // uchifuzume
else
Mated
} else if (isPerpetualCheck) {
Illegal // perpetual check
} else if (isRepetition) {
Drawn // Sennichite
} else {
Playing
}
}

/**
* Get the latest state.
*/
def currentState: State = history.last

def makeMove(move: ExtendedMove): Option[Game] = currentState.isValidMove(move).option(this.copy(moves = moves :+ move))
def lastMove: Option[ExtendedMove] = moves.lastOption

def turn: Player = currentState.turn

def makeMove(move: ExtendedMove): Option[Game] =
(status == Playing && currentState.isValidMove(move)).option(this.copy(moves = moves :+ move))

def makeMove(move: Move): Option[Game] = ExtendedMove.fromMove(move, currentState).flatMap(makeMove)

Expand All @@ -45,7 +64,10 @@ case class Game(initialState: State = State.HIRATE,
*
* @return true if the latest move is the repetition
*/
def isRepetition: Boolean = hashCodes.count(_ == currentState.hashCode()) >= 4
def isRepetition: Boolean = hashCodes.drop(1).count(_ == currentState.hashCode()) >= 4

def isPerpetualCheck: Boolean = currentState.isChecked &&
(history.drop(1).reverse.takeWhile(s => s.turn == !turn || s.isChecked).count(_.hashCode() == currentState.hashCode()) >= 4)
}

object Game extends CsaFactory[Game] with SfenFactory[Game] {
Expand All @@ -69,15 +91,15 @@ object Game extends CsaFactory[Game] with SfenFactory[Game] {
for {
st <- State.parseSfenString(tokens.take(3).mkString(" ")) if tokens.length >= 4
offset <- Try(tokens(3).toInt).toOption
gi = GameInfo() // initialize without information
gi = GameInfo() // initialize without information
moves = tokens.drop(4).flatMap(ss => Move.parseSfenString(ss)) if moves.length == tokens.length - 4
game <- moves.foldLeft(Some(Game(st, Seq.empty, gi, offset)): Option[Game])((g, m) => g.flatMap(_.makeMove(m)))
} yield game
}

object GameStatus extends Enumeration {
type GameStatus = Value
val Playing, Mated = Value
val Playing, Mated, Illegal, Drawn = Value
}

}
Expand Down
4 changes: 0 additions & 4 deletions src/main/scala/com/mogproject/mogami/core/Square.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,13 @@ case class Square(index: Int) extends CsaLike with SfenLike {

def isHand: Boolean = index < 0

// todo: maybe deprecated
/**
* Distance from the player's farthest rank.
*/
def closeness(player: Player): Int = isHand.fold(0, (player == WHITE).when[Int](10 - _)(rank))

// todo: Deprecated
def isPromotionZone(player: Player): Boolean = closeness(player) <= 3

// todo: Deprecated
def isLegalZone(piece: Piece): Boolean = (piece.ptype match {
case PAWN | LANCE => 2
case KNIGHT => 3
Expand All @@ -59,7 +56,6 @@ case class Square(index: Int) extends CsaLike with SfenLike {
}
}

// todo: Deprecated
def getDisplacement(player: Player, to: Square): Displacement = {
(math.abs(to.file - file), to.closeness(player) - closeness(player)) match {
case (0, 0) => Displacement(NoRelation, 0)
Expand Down
49 changes: 12 additions & 37 deletions src/main/scala/com/mogproject/mogami/core/State.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import com.mogproject.mogami.util.Implicits._
*/
case class State(turn: Player, board: BoardType, hand: HandType) extends CsaLike with SfenLike {

require(checkCapacity, "the number of pieces must be within the capacity")
require(hand.keySet == State.EMPTY_HANDS.keySet, "hand pieces must be in-hand type")
require(!board.keySet.contains(HAND), "all board pieces must have on-board squares")
require(board.forall{ case (s, p) => s.isLegalZone(p) }, "all board pieces must be placed in their legal zones")
require(!getKing(!turn).exists(getAttackBB(turn).get), "player must not be able to attack the opponent's king")

import com.mogproject.mogami.core.State.PromotionFlag.{PromotionFlag, CannotPromote, CanPromote, MustPromote}

override def toCsaString: String = {
Expand Down Expand Up @@ -196,33 +202,7 @@ case class State(turn: Player, board: BoardType, hand: HandType) extends CsaLike
* @param move move to test
* @return true if the move is legal
*/
def isValidMove(move: ExtendedMove): Boolean = {
verifyPlayer(move) &&
move.isDrop.fold(verifyHandMove(move), verifyBoardMove(move)) &&
verifyKing(board - move.from + (move.to -> Piece(turn, PPAWN)))
}

private[this] def verifyPlayer(move: ExtendedMove): Boolean = move.player == turn

// sub methods for hand move
private[this] def verifyHandMove(move: ExtendedMove): Boolean =
hand.get(move.newPiece).exists(_ > 0) && board.get(move.to).isEmpty && verifyNifu(move)

private[this] def verifyNifu(move: ExtendedMove): Boolean =
move.newPtype != PAWN || !(1 to 9).map(Square(move.to.file, _)).exists(s => board.get(s).contains(Piece(turn, PAWN)))

// sub methods for board move
private[this] def verifyBoardMove(move: ExtendedMove): Boolean =
board.get(move.to).map(_.ptype) == move.captured && State.canAttack(board, move.from, move.to)

// test if king is safe
private[this] def verifyKing(newBoard: Map[Square, Piece]): Boolean = {
State.getKingSquare(turn, newBoard).forall { k =>
newBoard.forall { case (s, p) =>
p.owner == turn || !State.canAttack(newBoard, s, k)
}
}
}
def isValidMove(move: ExtendedMove): Boolean = legalMoves.contains(move.copy(elapsedTime = None))

/** *
* Check if the state is mated.
Expand Down Expand Up @@ -251,6 +231,10 @@ case class State(turn: Player, board: BoardType, hand: HandType) extends CsaLike

def checkCapacity: Boolean = getPieceCount.filterKeys(_.ptype == KING).forall(_._2 <= 1) && getUnusedPtypeCount.values.forall(_ >= 0)

def canAttack(from: Square, to: Square): Boolean = {
require(from != HAND, "from must not be in hand")
attackBBOnBoard(turn).get(from).exists(_.get(to))
}
}

object State extends CsaStateReader with SfenStateReader {
Expand All @@ -266,16 +250,7 @@ object State extends CsaStateReader with SfenStateReader {
val EMPTY_HANDS: HandType = (for (t <- Player.constructor; pt <- Ptype.inHand) yield Piece(t, pt) -> 0).toMap

val empty = State(BLACK, Map.empty, EMPTY_HANDS)
val capacity: Map[Ptype, Int] = Map(PAWN -> 18, LANCE -> 4, KNIGHT -> 4, SILVER -> 4, GOLD -> 4, BISHOP -> 2, ROOK -> 2, KING -> 2)

// TODO: use BitBoard or deprecated
def canAttack(board: Map[Square, Piece], from: Square, to: Square): Boolean = {
(for {
p <- board.get(from)
if p.ptype.canMoveTo(from.getDisplacement(p.owner, to)) // check capability
if from.getBetweenBB(to).toSet.intersect(board.keySet).isEmpty // check blocking pieces
} yield {}).isDefined
}
lazy val capacity: Map[Ptype, Int] = Map(PAWN -> 18, LANCE -> 4, KNIGHT -> 4, SILVER -> 4, GOLD -> 4, BISHOP -> 2, ROOK -> 2, KING -> 2)

/**
* Get the square where the turn-to-move player's king.
Expand Down
101 changes: 57 additions & 44 deletions src/main/scala/com/mogproject/mogami/core/io/CsaStateReader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,114 +4,127 @@ import com.mogproject.mogami._
import com.mogproject.mogami.util.MapUtil

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

/**
* Reads CSA-formatted state
*/
trait CsaStateReader extends CsaFactory[State] {
type PtypeMap = Map[Ptype, Int]
type Result = (BoardType, HandType, PtypeMap)

protected[io] def parseInitExpression(s: String): Option[State] = {
/**
* parse init expression
*
* @param s string expression
* @return parsed result (board and hand) or None (if failed)
*/
protected[io] def parseInitExpression(s: String): Option[Result] = {
@tailrec
def f(sofar: Option[State], ls: List[String]): Option[State] = (sofar, ls) match {
case (Some(st), x :: xs) if x.length == 4 =>
def f(sofar: Option[Result], ls: List[String]): Option[Result] = (sofar, ls) match {
case (Some((b, h, rest)), x :: xs) if x.length == 4 =>
val next = for {
pos <- Square.parseCsaString(x.substring(0, 2))
pt <- Ptype.parseCsaString(x.substring(2, 4))
piece <- st.board.get(pos) if pt == piece.ptype
piece <- b.get(pos)
if pt == piece.ptype
} yield {
State(st.turn, st.board - pos, st.hand)
(b - pos, h, MapUtil.incrementMap(rest, pt))
}
f(next, xs)
case (_, Nil) => sofar
case _ => None
}

if (s.startsWith("PI")) {
f(Some(State.HIRATE), s.substring(2).grouped(4).toList)
f(Some(State.HIRATE.board, State.HIRATE.hand, State.capacity.mapValues(_ => 0)), s.substring(2).grouped(4).toList)
} else {
None
}
}

/**
* parse bundle expression
* @param lines string expression
* @return parsed state or None (if failed)
*
* @param lines string expression
* @return parsed result (board and hand) or None (if failed)
* @see http://www2.computer-shogi.org/protocol/record_v22.html
* "先後の区別が"+""-"以外のとき、駒がないとする。"
*/
protected[io] def parseBundleExpression(lines: List[String]): Option[State] = {
protected[io] def parseBundleExpression(result: Option[Result], lines: List[String]): Option[Result] = {
@tailrec
def f(sofar: Option[State], ls: List[String], rank: Int): Option[State] = (sofar, ls, rank) match {
case (Some(st), x :: xs, _) if x.length == 2 + 3 * 9 && x.startsWith(s"P${rank}") =>
def f(sofar: Option[Result], ls: List[String], rank: Int): Option[Result] = (sofar, ls, rank) match {
case (Some(_), x :: xs, _) if x.length == 2 + 3 * 9 && x.startsWith(s"P${rank}") =>
f(parseOneLine(sofar, x.drop(2).grouped(3).toList, 9, rank), xs, rank + 1)
case (_, Nil, 10) => sofar
case _ => None
}

@tailrec
def parseOneLine(sofar: Option[State], ls: List[String], file: Int, rank: Int): Option[State] = (sofar, ls, file) match {
case (Some(st), x :: xs, _) if !List("+", "-").contains(x.take(1)) => // no piece
def parseOneLine(sofar: Option[Result], ls: List[String], file: Int, rank: Int): Option[Result] = (sofar, ls, file) match {
case (Some(_), x :: xs, _) if !List("+", "-").contains(x.take(1)) => // no piece
parseOneLine(sofar, xs, file - 1, rank)
case (Some(st), x :: xs, _) => // add one piece
case (Some((b, h, rest)), x :: xs, _) => // add one piece
val pos = Square(file, rank)
val p = Piece.parseCsaString(x)
if (p.isDefined)
parseOneLine(Some(State(st.turn, st.board + (pos -> p.get), st.hand)), xs, file - 1, rank)
else
val pt = p.map(_.ptype.demoted)
if (p.isDefined && rest.getOrElse(pt.get, 0) >= 1) {
parseOneLine(Some((b.updated(pos, p.get), h, MapUtil.decrementMap(rest, pt.get))), xs, file - 1, rank)
} else {
None // invalid piece string
}
case (_, Nil, 0) => sofar
case _ => None
}

f(Some(State.empty), lines, 1).filter(_.checkCapacity)
f(result, lines, 1)
}

protected[io] def parseSingleExpression(state: State, s: String): Option[(State, Boolean)] = {
protected[io] def parseSingleExpression(result: Option[Result], s: String): Option[(Result, Boolean)] = {
@tailrec
def f(sofar: Option[(State, Boolean)], ls: List[String], t: Player): Option[(State, Boolean)] = (sofar, ls) match {
case (Some((st, b)), "00AL" :: Nil) =>
val rest = st.getUnusedPtypeCount.filter(_._1 != Ptype.KING)
val newHands = MapUtil.mergeMaps(st.hand, rest.map { case (k, v) => Piece(t, k) -> v })(_ + _, 0)
Some((State(st.turn, st.board, newHands), true))
case (Some((st, b)), x :: xs) if x.length == 4 =>
def f(sofar: Option[(Result, Boolean)], ls: List[String], t: Player) : Option[(Result, Boolean)] = (sofar, ls) match {
case (Some(((b, h, rest), _)), "00AL" :: Nil) =>
val newHand = MapUtil.mergeMaps(h, rest.collect { case (k, v) if k != KING => Piece(t, k) -> v })(_ + _, 0)
f(Some(((b, newHand, State.capacity.mapValues(_ => 0)), true)), Nil, t)
case (Some(((b, h, rest), used)), x :: xs) if x.length == 4 =>
val next = for {
pos <- Square.parseCsaString(x.substring(0, 2))
pt <- Ptype.parseCsaString(x.substring(2, 4))
if !st.board.contains(pos)
if pos != Square.HAND || Ptype.inHand.contains(pt)
if !b.contains(pos) // the square must be unused
if pos != HAND || Ptype.inHand.contains(pt) // check the piece type
if rest.getOrElse(pt.demoted, 0) >= 1 // check the number of piece types
} yield {
val p = Piece(t, pt)
val newRest = MapUtil.decrementMap(rest, pt.demoted)
pos match {
case Square.HAND => (State(st.turn, st.board, st.hand.updated(p, st.hand.getOrElse(p, 0) + 1)), false)
case _ => (State(st.turn, st.board.updated(pos, p), st.hand), false)
case HAND => ((b, MapUtil.incrementMap(h, p), newRest), used)
case _ => ((b.updated(pos, p), h, newRest), used)
}
}
f(next, xs, t)
case (Some((st, b)), Nil) => if (st.checkCapacity) sofar else None
case (_, Nil) => sofar
case _ => None
}

if (s.startsWith("P+") || s.startsWith("P-")) {
Player.parseCsaString(s.substring(1, 2)) flatMap { t => f(Some((state, false)), s.drop(2).grouped(4).toList, t) }
} else {
None
}
for {
t <- Player.parseCsaString(s.slice(1, 2)) if s.startsWith("P")
r <- result
x <- f(Some(r, false), s.drop(2).grouped(4).toList, t)
} yield x
}

def parseCsaString(s: String): Option[State] = {
@tailrec
def f(ss: List[String], sofar: Option[State], usedInit: Boolean, usedAll: Boolean): Option[State] = {
def f(ss: List[String], sofar: Option[Result], usedInit: Boolean, usedAll: Boolean): Option[State] = {
(ss, sofar, usedInit, usedAll) match {
case (x :: Nil, Some(st), _, _) => // Turn to move should be written in the last line
Player.parseCsaString(x) map { t => State(t, st.board, st.hand) }
case (x :: xs, Some(st), false, false) if x.startsWith("PI") => // Initialize with Hirate and handicap
case (x :: Nil, Some((b, h, _)), _, _) => // Turn to move should be written in the last line
Player.parseCsaString(x) flatMap { t => Try(State(t, b, h)).toOption }
case (x :: xs, Some(_), false, false) if x.startsWith("PI") => // Initialize with Hirate and handicap
f(xs, parseInitExpression(x), usedInit = true, usedAll = false)
case (x :: xs, Some(st), false, false) if x.startsWith("P1") =>
f(ss.drop(9), parseBundleExpression(ss.take(9)), usedInit = true, usedAll = false)
case (x :: xs, Some(st), _, false) if x.startsWith("P+") || x.startsWith("P-") =>
parseSingleExpression(st, x) match {
case (x :: xs, Some(_), false, false) if x.startsWith("P1") =>
f(ss.drop(9), parseBundleExpression(sofar, ss.take(9)), usedInit = true, usedAll = false)
case (x :: xs, Some(_), _, false) if x.startsWith("P+") || x.startsWith("P-") =>
parseSingleExpression(sofar, x) match {
case Some((t, u)) => f(xs, Some(t), usedInit, u)
case _ => None
}
Expand All @@ -120,7 +133,7 @@ trait CsaStateReader extends CsaFactory[State] {
}
}

f(s.split('\n').toList, Some(State.empty), usedInit = false, usedAll = false)
f(s.split('\n').toList, Some((Map.empty, State.EMPTY_HANDS, State.capacity)), usedInit = false, usedAll = false)
}

}
2 changes: 1 addition & 1 deletion src/test/scala/com/mogproject/mogami/core/GameGen.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ object GameGen {
def games: Gen[Game] = for {
gameInfo <- GameInfoGen.infos
state <- StateGen.statesWithFullPieces
n <- Gen.choose(0, 100)
n <- Gen.choose(0, 50)
} yield {
val moves = movesStream(state).take(n)
Game(state, moves, gameInfo)
Expand Down
Loading