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
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ object Game extends CsaFactory[Game] with SfenFactory[Game] {
} yield game
}

object GameStatus extends Enumeration {
object GameStatus {

sealed trait GameStatus

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

import com.mogproject.mogami._
import com.mogproject.mogami.core.Movement._
import com.mogproject.mogami.core.io._
import com.mogproject.mogami.util.Implicits._

Expand All @@ -26,6 +27,56 @@ trait MoveBuilder {
}
}

/**
* Get set of squares from which the same ptype can move to the same destinatiton.
*/
protected def getAmbiguousPieces(state: State, from: Option[Square], to: Square, oldPtype: Ptype): Set[Square] = {
state.board
.filter(_._2 == Piece(state.turn, oldPtype))
.keySet.filter(fr => !from.contains(fr) && state.attackBBOnBoard(state.turn)(fr).get(to))
}

protected def getRelation(player: Player, from: Square, to: Square): (Int, Int) = {
val r = from.rank.compare(to.rank)
val f = from.file.compare(to.file)
player.isBlack.fold((r, f), (-r, -f))
}

protected def getMovement(state: State, from: Option[Square], to: Square, oldPtype: Ptype): Option[Movement] = {
val ambs = getAmbiguousPieces(state, from, to, oldPtype)

from match {
case _ if ambs.isEmpty => None
case None => Some(Dropped)
case Some(fr) =>
val rel = getRelation(state.turn, fr, to)
val relations = ambs.map(getRelation(state.turn, _, to))
if (!relations.exists(_._1 == rel._1)) rel._1 match {
case 0 => Some(Horizontally)
case 1 => Some(Upward)
case -1 => Some(Downward)
case _ => throw new RuntimeException(s"Unexpected movement: state=${state}, from=${from}, to=${to}, oldPtype=${oldPtype}")
} else if (!relations.exists(_._2 == rel._2)) rel._2 match {
case 0 if !List(BISHOP, ROOK).contains(oldPtype.demoted) => Some(Vertical)
case 0 if relations.head._2 == -1 => Some(Rightwards) // relations must contain one another
case 0 if relations.head._2 == 1 => Some(Leftwards) // relations must contain one another
case 1 => Some(Rightwards)
case -1 => Some(Leftwards)
case _ => throw new RuntimeException(s"Unexpected movement: state=${state}, from=${from}, to=${to}, oldPtype=${oldPtype}")
} else rel match {
case (-1, -1) => Some(LeftDownward)
case (-1, 1) => Some(RightDownward)
case (0, -1) => Some(LeftHorizontally)
case (0, 1) => Some(RightHorizontally)
case (1, -1) => Some(LeftUpward)
case (1, 1) => Some(RightUpward)
case (_, 0) => Some(Vertical)
case _ => throw new RuntimeException(s"Unexpected movement: state=${state}, from=${from}, to=${to}, oldPtype=${oldPtype}")
}
}

}

def toMove(state: State): Option[Move]
}

Expand Down Expand Up @@ -63,9 +114,11 @@ case class MoveBuilderCsaBoard(player: Player, from: Square, to: Square, newPtyp
for {
oldPiece <- state.board.get(from)
promote = oldPiece.ptype != newPtype
isSame = state.lastMoveTo.contains(to)
isCheck = isCheckMove(state, Some(from), to, newPtype)
movement = getMovement(state, Some(from), to, oldPiece.ptype)
captured = state.board.get(to).map(_.ptype)
mv <- Try(Move(player, Some(from), to, newPtype, promote, captured, isCheck, elapsedTime)).toOption
mv <- Try(Move(player, Some(from), to, newPtype, promote, isSame, movement, captured, isCheck, elapsedTime)).toOption
if player == state.turn
} yield mv
}
Expand All @@ -75,8 +128,9 @@ case class MoveBuilderCsaHand(player: Player, to: Square, ptype: Ptype, elapsedT

override def toMove(state: State): Option[Move] = {
val isCheck = isCheckMove(state, None, to, ptype)
val movement = getMovement(state, None, to, ptype)
for {
mv <- Try(Move(player, None, to, ptype, promote = false, captured = None, isCheck, elapsedTime)).toOption
mv <- Try(Move(player, None, to, ptype, promote = false, isSameSquare = false, movement, captured = None, isCheck, elapsedTime)).toOption
if player == state.turn
} yield mv
}
Expand Down Expand Up @@ -124,9 +178,11 @@ case class MoveBuilderSfenBoard(from: Square, to: Square, promote: Boolean) exte
for {
oldPiece <- state.board.get(from)
newPtype = promote.fold(oldPiece.ptype.promoted, oldPiece.ptype)
isSame = state.lastMoveTo.contains(to)
isCheck = isCheckMove(state, Some(from), to, newPtype)
movement = getMovement(state, Some(from), to, oldPiece.ptype)
captured = state.board.get(to).map(_.ptype)
mv <- Try(Move(state.turn, Some(from), to, newPtype, promote, captured, isCheck, None)).toOption
mv <- Try(Move(state.turn, Some(from), to, newPtype, promote, isSame, movement, captured, isCheck, None)).toOption
} yield mv
}

Expand All @@ -135,8 +191,9 @@ case class MoveBuilderSfenHand(ptype: Ptype, to: Square) extends MoveBuilderSfen

override def toMove(state: State): Option[Move] = {
val isCheck = isCheckMove(state, None, to, ptype)
val movement = getMovement(state, None, to, ptype)
for {
mv <- Try(Move(state.turn, None, to, ptype, promote = false, None, isCheck, None)).toOption
mv <- Try(Move(state.turn, None, to, ptype, promote = false, isSameSquare = false, movement, None, isCheck, None)).toOption
} yield mv
}
}
Expand All @@ -149,17 +206,20 @@ case class Move(player: Player,
to: Square,
newPtype: Ptype,
promote: Boolean,
isSameSquare: Boolean, // true when `to` is same as the last move's `to`
movement: Option[Movement], // None = no ambiguity
captured: Option[Ptype],
isCheck: Boolean,
elapsedTime: Option[Int] = None
) extends CsaLike with SfenLike {
) extends CsaLike with SfenLike with KifLike {
require(!isDrop || !promote, "promote must be false when dropping")
require(!isDrop || captured.isEmpty, "captured must be None when dropping")
require(from.exists(_.isPromotionZone(player)) || to.isPromotionZone(player) || !promote, "either from or to must be in the promotion zone")
require(from.map(_.getDisplacement(player, to)).forall(oldPtype.canMoveTo), "move must be within the capability")
require(to.isLegalZone(newPiece), "to must be legal for the new piece")
require(elapsedTime.forall(_ >= 0), "elapsedTime must be positive or zero")
require(!captured.contains(KING), "king cannot be captured")
require(oldPtype != PAWN || movement.isEmpty, "pawn cannot be ambiguous")

def oldPtype: Ptype = if (promote) newPtype.demoted else newPtype

Expand All @@ -175,11 +235,47 @@ case class Move(player: Player,

def moveFrom: MoveFrom = from.map(Left.apply).getOrElse(Right(Hand(player, oldPtype)))

def couldPromote: Boolean = oldPtype.canPromote && from.exists(_.isPromotionZone(player) || to.isPromotionZone(player))

override def toCsaString: String =
from.map(fr => MoveBuilderCsaBoard(player, fr, to, newPtype, elapsedTime))
.getOrElse(MoveBuilderCsaHand(player, to, newPtype, elapsedTime)).toCsaString

override def toSfenString: String =
from.map(fr => MoveBuilderSfenBoard(fr, to, promote)).getOrElse(MoveBuilderSfenHand(newPtype, to)).toSfenString

override def toKifString: String =
isSameSquare.fold("同", to.toKifString) + oldPtype.toKifString + movement.map(_.kifString).getOrElse("") + promote.fold("成", couldPromote.fold("不成", ""))
}

object Movement {

abstract sealed class Movement(val kifString: String)

case object Dropped extends Movement("打")

case object Downward extends Movement("引")

case object Horizontally extends Movement("寄")

case object Upward extends Movement("上")

case object Leftwards extends Movement("右")

case object Rightwards extends Movement("左")

case object Vertical extends Movement("直")

case object LeftDownward extends Movement("右引")

case object LeftHorizontally extends Movement("右寄")

case object LeftUpward extends Movement("右上")

case object RightDownward extends Movement("左引")

case object RightHorizontally extends Movement("左寄")

case object RightUpward extends Movement("左上")

}
11 changes: 9 additions & 2 deletions shared/src/main/scala/com/mogproject/mogami/core/Ptype.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import com.mogproject.mogami.core.io._
/**
* Piece type
*/
sealed abstract class Ptype(val id: Int) extends CsaLike {
sealed abstract class Ptype(val id: Int) extends CsaLike with KifLike {
lazy val sortId: Int = Ptype.sortIdTable(id)

override def toCsaString: String = Ptype.csaTable(id)

override def toKifString: String = Ptype.kifTable(id)

final def isBasic: Boolean = 8 <= id

final def isPromoted: Boolean = id < 8
Expand Down Expand Up @@ -50,7 +52,7 @@ sealed abstract class Ptype(val id: Int) extends CsaLike {
def canMoveTo(displacement: Displacement): Boolean = displacement.distance <= capability(displacement.direction.id)
}

object Ptype extends CsaTableFactory[Ptype] {
object Ptype extends CsaTableFactory[Ptype] with KifTableFactory[Ptype] {
implicit def ordering[A <: Ptype]: Ordering[A] = Ordering.by(_.sortId)

val sortIdTable: Seq[Int] = Seq(
Expand All @@ -63,6 +65,11 @@ object Ptype extends CsaTableFactory[Ptype] {
"OU", "KI", "FU", "KY", "KE", "GI", "KA", "HI"
)

val kifTable: Seq[String] = Seq(
"", "", "と", "成香", "成桂", "成銀", "馬", "竜",
"玉", "金", "歩", "香", "桂", "銀", "角", "飛"
)

val englishSimpleNames: Seq[String] = Seq(
"", "", "+P", "+L", "+N", "+S", "+B", "+R",
"K", "G", "P", "L", "N", "S", "B", "R"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import com.mogproject.mogami.util.Implicits._
/**
* Square -- each cell on the board
*/
case class Square(index: Int) extends CsaLike with SfenLike {
case class Square(index: Int) extends CsaLike with SfenLike with KifLike {
require(0 <= index && index < 81)

import com.mogproject.mogami.core.Direction._
Expand All @@ -24,6 +24,8 @@ case class Square(index: Int) extends CsaLike with SfenLike {

override def toSfenString = s"${file}${rankToChar}"

override def toKifString: String = s"${"123456789".charAt(file - 1)}${"一二三四五六七八九".charAt(rank - 1)}"

/**
* Distance from the player's farthest rank.
*/
Expand Down
8 changes: 6 additions & 2 deletions shared/src/main/scala/com/mogproject/mogami/core/State.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import scala.util.Try
/**
* State class
*/
case class State(turn: Player = BLACK, board: BoardType = Map.empty, hand: HandType = State.EMPTY_HANDS) extends CsaLike with SfenLike {
case class State(turn: Player = BLACK,
board: BoardType = Map.empty,
hand: HandType = State.EMPTY_HANDS,
lastMoveTo: Option[Square] = None
) 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")
Expand Down Expand Up @@ -208,7 +212,7 @@ case class State(turn: Player = BLACK, board: BoardType = Map.empty, hand: HandT
val releaseBoard: BoardType => BoardType = move.from.when(sq => b => b - sq)
val releaseHand: HandType => HandType = move.isDrop.when(MapUtil.decrementMap(_, Hand(move.newPiece)))
val obtainHand: HandType => HandType = move.capturedPiece.when(p => h => MapUtil.incrementMap(h, Hand(!p.demoted)))
State(!turn, releaseBoard(board) + (move.to -> move.newPiece), (releaseHand andThen obtainHand) (hand))
State(!turn, releaseBoard(board) + (move.to -> move.newPiece), (releaseHand andThen obtainHand) (hand), Some(move.to))
}

def getPieceCount: Map[Piece, Int] = MapUtil.mergeMaps(board.groupBy(_._2).mapValues(_.size), hand.map { case (k, v) => k.toPiece -> v })(_ + _, 0)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.mogproject.mogami.core.io

/**
*
*/
trait KifFactory[T <: KifLike] {

def parseKifString(s: String): Option[T]

}
10 changes: 10 additions & 0 deletions shared/src/main/scala/com/mogproject/mogami/core/io/KifLike.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.mogproject.mogami.core.io

/**
*
*/
trait KifLike {

def toKifString: String

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.mogproject.mogami.core.io

/**
*
*/
trait KifTableFactory[T <: KifLike] extends KifFactory[T] {

val kifTable: Seq[String]

private[this] lazy val tableFactory = BaseTableFactory[T](kifTable)

override def parseKifString(s: String): Option[T] = tableFactory.get(s)(apply)

def apply(id: Int): T

}
28 changes: 14 additions & 14 deletions shared/src/test/scala/com/mogproject/mogami/core/GameSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class GameSpec extends FlatSpec with MustMatchers with GeneratorDrivenPropertyCh
val dataForTest = Seq(
Game(stateEmpty, Vector(), GameInfo()),
Game(stateHirate, Vector(
Move(BLACK, Some(P77), P76, PAWN, false, None, false)
Move(BLACK, Some(P77), P76, PAWN, false, false, None, None, false)
), GameInfo(
Map(
'formatVersion -> "2.2",
Expand All @@ -38,13 +38,13 @@ class GameSpec extends FlatSpec with MustMatchers with GeneratorDrivenPropertyCh
'opening -> "YAGURA"
))),
Game(stateHirate, Vector(
Move(BLACK, Some(P77), P76, PAWN, false, None, false, Some(50)),
Move(WHITE, Some(P33), P34, PAWN, false, None, false, Some(1)),
Move(BLACK, Some(P88), P22, PBISHOP, true, Some(BISHOP), false, Some(12)),
Move(WHITE, Some(P31), P22, SILVER, false, Some(PBISHOP), false, Some(100)),
Move(BLACK, None, P33, BISHOP, false, None, true, Some(10)),
Move(WHITE, Some(P51), P62, KING, false, None, false, Some(2)),
Move(BLACK, Some(P33), P55, PBISHOP, true, None, false, Some(3))
Move(BLACK, Some(P77), P76, PAWN, false, false, None, None, false, Some(50)),
Move(WHITE, Some(P33), P34, PAWN, false, false, None, None, false, Some(1)),
Move(BLACK, Some(P88), P22, PBISHOP, true, false, None, Some(BISHOP), false, Some(12)),
Move(WHITE, Some(P31), P22, SILVER, false, true, None, Some(PBISHOP), false, Some(100)),
Move(BLACK, None, P33, BISHOP, false, false, None, None, true, Some(10)),
Move(WHITE, Some(P51), P62, KING, false, false, None, None, false, Some(2)),
Move(BLACK, Some(P33), P55, PBISHOP, true, false, None, None, false, Some(3))
), GameInfo(
Map('formatVersion -> "", 'blackName -> "B", 'whiteName -> "W", 'event -> "", 'site -> "",
'startTime -> "", 'endTime -> "", 'timeLimit -> "", 'opening -> "")
Expand Down Expand Up @@ -115,8 +115,8 @@ class GameSpec extends FlatSpec with MustMatchers with GeneratorDrivenPropertyCh
Game.parseCsaString("-") must be(Some(Game(stateEmptyInv, Vector(), GameInfo())))

Game.parseCsaString("PI\n-\n-5152OU,T2345\n+5958OU") must be(Some(Game(stateHirateInv, Vector(
Move(WHITE, Some(P51), P52, KING, false, None, false, Some(2345)),
Move(BLACK, Some(P59), P58, KING, false, None, false)
Move(WHITE, Some(P51), P52, KING, false, false, None, None, false, Some(2345)),
Move(BLACK, Some(P59), P58, KING, false, false, None, None, false)
), GameInfo())))
Game.parseCsaString("N-yyy\n" +
"P1-KY-KE-GI-KI-OU-KI-GI-KE-KY\n" +
Expand All @@ -131,8 +131,8 @@ class GameSpec extends FlatSpec with MustMatchers with GeneratorDrivenPropertyCh
"P+\n" +
"P-\n" +
"-\n-5152OU,T2345\n+5958OU") must be(Some(Game(stateHirateInv, Vector(
Move(WHITE, Some(P51), P52, KING, false, None, false, Some(2345)),
Move(BLACK, Some(P59), P58, KING, false, None, false)
Move(WHITE, Some(P51), P52, KING, false, false, None, None, false, Some(2345)),
Move(BLACK, Some(P59), P58, KING, false, false, None, None, false)
), GameInfo(Map('whiteName -> "yyy")))))
Game.parseCsaString("V2.2\nN+x\nN-y\n$OPENING:AIGAKARI\n+") must be(Some(Game(stateEmpty, Vector(), GameInfo(
Map('formatVersion -> "2.2", 'blackName -> "x", 'whiteName -> "y", 'opening -> "AIGAKARI")))))
Expand All @@ -143,8 +143,8 @@ class GameSpec extends FlatSpec with MustMatchers with GeneratorDrivenPropertyCh
Game.parseCsaString("V2.2\n$EVENT:event name\nN-white name\nPI\n-\n-5152OU,T2345\n+5958OU") must be(Some(Game(
stateHirateInv,
Vector(
Move(WHITE, Some(P51), P52, KING, false, None, false, Some(2345)),
Move(BLACK, Some(P59), P58, KING, false, None, false)
Move(WHITE, Some(P51), P52, KING, false, false, None, None, false, Some(2345)),
Move(BLACK, Some(P59), P58, KING, false, false, None, None, false)
),
GameInfo(Map('formatVersion -> "2.2", 'event -> "event name", 'whiteName -> "white name")))))
}
Expand Down
Loading