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
3 changes: 2 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ lazy val root = (project in file("."))
import com.mogproject.mogami.core.PieceConstant._
import com.mogproject.mogami.core.Square.HAND
import com.mogproject.mogami.core.State.PromotionFlag._
"""
""",
parallelExecution in Test := false
)
8 changes: 4 additions & 4 deletions src/main/scala/com/mogproject/mogami/core/BitBoard.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.mogproject.mogami.core

import com.mogproject.mogami.util.BitOperation
import com.mogproject.mogami.util.BooleanOps.Implicits._
import com.mogproject.mogami.util.Implicits._


class BitBoard(val lo: Long, val hi: Long) {
Expand Down Expand Up @@ -43,7 +43,7 @@ class BitBoard(val lo: Long, val hi: Long) {

def isEmpty: Boolean = (lo | hi) == 0L

def isDefined: Boolean = !isEmpty
def nonEmpty: Boolean = !isEmpty

def count: Int = BitOperation.pop(lo) + BitOperation.pop(hi)

Expand All @@ -54,7 +54,7 @@ class BitBoard(val lo: Long, val hi: Long) {
private[this] def operate(f: (Long, Long) => Long)(that: BitBoard) = BitBoard(f(lo, that.lo), f(hi, that.hi))

private[this] def setProcess(index: Int)(f: (Long, Long) => Long) = {
require(0 <= index && index < 81)
require(0 <= index && index < 81, s"Invalid index: ${index}")
if (index < 54) {
BitBoard(f(lo, 1L << index), hi)
} else {
Expand Down Expand Up @@ -284,7 +284,7 @@ object BitBoard {

def ident(sq: Square): BitBoard = ident(sq.index)

def promotion(player: Player): BitBoard = BitBoard(0x7ffffffL, 0L).flipByPlayer(player)
def promotionZone(player: Player): BitBoard = BitBoard(0x7ffffffL, 0L).flipByPlayer(player)

/**
* Make bitboard sequence from long-width string lines
Expand Down
4 changes: 2 additions & 2 deletions src/main/scala/com/mogproject/mogami/core/Game.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.mogproject.mogami.core

import scala.annotation.tailrec
import com.mogproject.mogami.core.io.{CsaFactory, CsaLike, SfenFactory, SfenLike}
import com.mogproject.mogami.util.BooleanOps.Implicits._
import com.mogproject.mogami.core.io._
import com.mogproject.mogami.util.Implicits._

import scala.util.Try

Expand Down
22 changes: 18 additions & 4 deletions src/main/scala/com/mogproject/mogami/core/Move.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.mogproject.mogami.core

import com.mogproject.mogami.core.io.{CsaFactory, CsaLike, SfenFactory, SfenLike}
import com.mogproject.mogami._
import com.mogproject.mogami.core.io._
import com.mogproject.mogami.util.Implicits._

import scala.util.Try
import scala.util.matching.Regex
Expand Down Expand Up @@ -85,7 +87,7 @@ case class ExtendedMove(player: Player,
promote: Boolean,
captured: Option[Ptype],
isCheck: Boolean
) extends CsaLike with SfenLike {
) extends CsaLike with SfenLike {
require(!isDrop || !promote, "promote must be false when dropping")
require(!isDrop || captured.isEmpty, "captured must be None when dropping")
require(from.isPromotionZone(player) || to.isPromotionZone(player) || !promote, "either from or to must be in the promotion zone")
Expand Down Expand Up @@ -123,14 +125,26 @@ object ExtendedMove {
* @return completed information
*/
def fromMove(move: Move, state: State): Option[ExtendedMove] = {
// todo: implement isCheck
for {
oldPiece <- if (move.from.isHand) Some(Piece(state.turn, move.newPtype.get)) else state.board.get(move.from)
oldPtype = oldPiece.ptype
newPtype = move.newPtype.getOrElse(if (move.promote.get) oldPtype.promoted else oldPtype)
promote = move.promote.getOrElse(oldPtype != newPtype)
mv <- Try(ExtendedMove(state.turn, move.from, move.to, newPtype, promote, state.board.get(move.to).map(_.ptype), false)).toOption
isCheck = isCheckMove(Piece(state.turn, newPtype), move.from, move.to, state)
mv <- Try(ExtendedMove(state.turn, move.from, move.to, newPtype, promote, state.board.get(move.to).map(_.ptype), isCheck)).toOption
} yield mv
}

def isCheckMove(newPiece: Piece, from: Square, to: Square, state: State): Boolean = {
val pl = state.turn
val newOccAll = (!from.isHand).when[BitBoard](_.reset(from))(state.occupancy.set(to))
val newOccTurn = (!from.isHand).when[BitBoard](_.reset(from))(state.occupancy(pl).set(to))
val king = state.getKing(!pl)

king.exists { k =>
val pieces = (to, newPiece) +: state.getRangedPieces(pl)
// `to` must not be in hand, and therefore Nifu does not matter
pieces.exists { case (s, p) => Attack.get(p, s, newOccAll, newOccTurn, BitBoard.empty).get(k) }
}
}
}
6 changes: 6 additions & 0 deletions src/main/scala/com/mogproject/mogami/core/Piece.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ case class Piece(owner: Player, ptype: Ptype) extends CsaLike with SfenLike {
def promoted: Piece = Piece(owner, ptype.promoted)

def demoted: Piece = Piece(owner, ptype.demoted)

def isPromoted: Boolean = ptype.isPromoted

def isRanged: Boolean = ptype.isRanged

def canPromote: Boolean = ptype.canPromote
}

object Piece extends CsaTableFactory[Piece] with SfenTableFactory[Piece] {
Expand Down
4 changes: 3 additions & 1 deletion src/main/scala/com/mogproject/mogami/core/Ptype.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.mogproject.mogami.core

import com.mogproject.mogami.core.io.{CsaLike, CsaTableFactory}
import com.mogproject.mogami.core.io._

/**
* Piece type
Expand All @@ -16,6 +16,8 @@ sealed abstract class Ptype(val id: Int) extends CsaLike {

final def canPromote: Boolean = 10 <= id

final def isRanged: Boolean = id == 11 || (id & 7) >= 6

final def demoted: Ptype = Ptype(id | 8)

final def promoted: Ptype = if (canPromote) Ptype(id - 8) else this
Expand Down
27 changes: 14 additions & 13 deletions src/main/scala/com/mogproject/mogami/core/Square.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package com.mogproject.mogami.core
import com.mogproject.mogami.core.Player.WHITE

import scala.util.matching.Regex
import com.mogproject.mogami.core.Ptype._
import com.mogproject.mogami.core.io.{CsaFactory, CsaLike, SfenFactory, SfenLike}
import com.mogproject.mogami.util.BooleanOps.Implicits._
import com.mogproject.mogami._
import com.mogproject.mogami.core.io._
import com.mogproject.mogami.util.Implicits._

/**
* Square -- each cell on the board (and in hand)
Expand All @@ -26,39 +26,40 @@ 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
case _ => 1
}) <= closeness(piece.owner)

def getInnerSquares(to: Square): Seq[Square] = {
def getBetweenBB(to: Square): BitBoard = {
if (isHand || to.isHand) {
Seq.empty
BitBoard.empty
} else {
val f = to.file - file
val r = to.rank - rank
val distance = (math.abs(f), math.abs(r)) match {
case (0, 0) => None
case (0, y) => Some(y)
case (x, 0) => Some(x)
case (x, y) if x == y => Some(x)
case _ => None
case (0, y) => y // including the case when y == 0
case (x, 0) => x
case (x, y) if x == y => x
case _ => 0
}

(for {
d <- distance
} yield (1 until d).map(n => Square(file + f / d * n, rank + r / d * n))).getOrElse(Seq.empty)
(1 until distance).foldLeft(BitBoard.empty)((b, n) => b.set(Square(file + f / distance * n, rank + r / distance * n)))
}
}

// 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
140 changes: 130 additions & 10 deletions src/main/scala/com/mogproject/mogami/core/State.scala
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
package com.mogproject.mogami.core

import com.mogproject.mogami.core.Player.{BLACK, WHITE}
import com.mogproject.mogami.core.Ptype._
import com.mogproject.mogami._
import com.mogproject.mogami.core.io._
import com.mogproject.mogami.util.MapUtil
import com.mogproject.mogami.util.BooleanOps.Implicits._
import com.mogproject.mogami.util.OptionOps.Implicits._
import com.mogproject.mogami.core.State.{BoardType, HandType}
import com.mogproject.mogami.util.Implicits._

/**
* State class
Expand Down Expand Up @@ -59,6 +56,131 @@ case class State(turn: Player, board: BoardType, hand: HandType) extends CsaLike
}
}

/**
* Occupancy bitboards
*/
private[this] def aggregateSquares(boardMap: BoardType): BitBoard = boardMap.keys.view.map(BitBoard.ident).fold(BitBoard.empty)(_ | _)

private[this] lazy val occupancyAll: BitBoard = aggregateSquares(board)

private[this] lazy val occupancyByOwner: Map[Player, BitBoard] = board.groupBy(_._2.owner).mapValues(aggregateSquares)

private[this] lazy val occupancyByPiece: Map[Piece, BitBoard] = board.groupBy(_._2).mapValues(aggregateSquares)

def occupancy: BitBoard = occupancyAll

def occupancy(player: Player): BitBoard = occupancyByOwner.getOrElse(player, BitBoard.empty)

def occupancy(piece: Piece): BitBoard = occupancyByPiece.getOrElse(piece, BitBoard.empty)

def getSquares(piece: Piece): Set[Square] = occupancy(piece).toSet

def getKing(player: Player): Option[Square] = occupancy(Piece(player, KING)).toList.headOption

lazy val turnsKing: Option[Square] = getKing(turn)

def getRangedPieces(player: Player): Seq[(Square, Piece)] = board.filter { case (s, p) => p.owner == player && p.isRanged }.toSeq

/**
* Attack bitboards
*/
lazy val attackBBOnBoard: Map[Player, Map[Square, BitBoard]] = {
val m = (for ((sq, piece) <- board) yield {
(piece.owner, sq) -> Attack.get(piece, sq, occupancy, occupancy(piece.owner), occupancy(Piece(piece.owner, PAWN)))
}).filter(_._2.nonEmpty).groupBy(_._1._1)

m.mapValues {
_.map { case ((p, s), b) => s -> b }
}
}

lazy val attackBBInHand: Map[(Square, Piece), BitBoard] = for {
(piece, num) <- hand if piece.owner == turn && num > 0
} yield {
(HAND, piece) -> Attack.get(piece, HAND, occupancy, occupancy(turn), occupancy(Piece(turn, PAWN)))
}

def getAttackBB(player: Player): BitBoard = attackBBOnBoard(player).values.fold(BitBoard.empty)(_ | _)

/**
* Get the positions of pieces that are attacking the turn playrer's king
*
* @return set of squares
*/
lazy val attackers: Set[Square] = turnsKing.map(k => attackBBOnBoard(!turn).filter(_._2.get(k)).keys.toSet).getOrElse(Set.empty)

/**
* Get the guard pieces, which protect the turn player's king from ranged attack.
*
* @return set of squares and guarding area bitboards
*/
lazy val guards: Map[Square, BitBoard] = {
for {
(s, p) <- board if p.owner == !turn && p.isRanged
k <- getKing(turn)
bt = s.getBetweenBB(k) if Attack.getRangedAttack(p, s, BitBoard.empty).get(k)
g = bt & occupancy if g.count == 1
} yield {
g.toList.head -> bt
}
}

/**
* Check if the player is checked.
*/
lazy val isChecked: Boolean = turnsKing.exists(getAttackBB(!turn).get)

def getNonSuicidalMovesOnBoard: Map[Square, BitBoard] = (for ((sq, bb) <- attackBBOnBoard(turn)) yield {
if (board(sq).ptype == KING)
sq -> (bb & ~getAttackBB(!turn))
else if (guards.keySet.contains(sq))
sq -> (bb & guards(sq))
else
sq -> bb
}).filter(_._2.nonEmpty)

def generateLegalMovesOnBoard(m: Map[Square, BitBoard]): Map[(Square, Piece), BitBoard] = for {
(s, bb) <- m
(p, b) <- Attack.getSeq(board(s), s, bb)
} yield {
(s, p) -> b
}

def getEscapeMoves: Map[(Square, Piece), BitBoard] = {
require(turnsKing.isDefined)
val king = turnsKing.get

// king's move
val kingEscape = Map((king, Piece(turn, KING)) -> (attackBBOnBoard(turn)(king) & ~getAttackBB(!turn)))

val attacker = if (attackers.size == 1) attackers.headOption else None

// capture the attacker
val captureAttacker = for ((sq, bb) <- attackBBOnBoard(turn); atk <- attacker) yield sq -> (bb & BitBoard.ident(atk))

// drop a piece between king and the attacker
val dropBetween = for (((sq, p), bb) <- attackBBInHand; atk <- attacker) yield (sq, p) -> (bb & king.getBetweenBB(atk))

(kingEscape ++ generateLegalMovesOnBoard(captureAttacker) ++ dropBetween).filter(_._2.nonEmpty)
}

/**
* All legal moves in the bitboard description
*
* @return map of the square from which piece moves, new piece, and attack bitboard
*/
lazy val legalMovesBB: Map[(Square, Piece), BitBoard] =
if (isChecked)
getEscapeMoves
else
generateLegalMovesOnBoard(getNonSuicidalMovesOnBoard) ++ attackBBInHand

def legalMoves: Seq[ExtendedMove] = (for {
((from, p), bb) <- legalMovesBB
to <- bb.toList
mv <- ExtendedMove.fromMove(Move(from, to, None, Some(p.ptype), None), this) // todo: improve? State#generateExtendedMove ?
} yield mv).toSeq

/**
* Check if the move is legal.
*
Expand Down Expand Up @@ -93,14 +215,12 @@ case class State(turn: Player, board: BoardType, hand: HandType) extends CsaLike
}
}

def legalMoves: Seq[ExtendedMove] = ???

/** *
* Check if the state is mated.
*
* @return true if mated
*/
def isMated: Boolean = ??? // todo
def isMated: Boolean = legalMovesBB.isEmpty

/**
* Make one move.
Expand Down Expand Up @@ -139,12 +259,12 @@ object State extends CsaStateReader with SfenStateReader {
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
// 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.getInnerSquares(to).toSet.intersect(board.keySet).isEmpty // check blocking pieces
if from.getBetweenBB(to).toSet.intersect(board.keySet).isEmpty // check blocking pieces
} yield {}).isDefined
}

Expand Down
Loading