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 @@ -16,7 +16,7 @@ scala:
- 2.12.0
- 2.11.11

script: "sbt clean coverage mogCoreJVM/test"
script: "travis_wait 30 sbt clean coverage mogCoreJVM/test"

after_success: "sbt coverageReport coverageAggregate coveralls"

Expand Down
35 changes: 35 additions & 0 deletions js/src/test/scala/com/mogproject/mogami/bench/BenchmarkJS.scala
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ object BenchmarkJS extends scalajs.js.JSApp with Benchmark with TestData {
benchMateSolver(s4)
// benchMateSolver(s5)
// benchMateSolver(s6)
benchMateSolver(s7)
benchMateSolver(s8)
benchMateSolver(s9)
}
Expand Down Expand Up @@ -91,4 +92,38 @@ benchMateSolver
- avg: 5.287s, min: 4.402s, max: 6.252s

Some(List(3一飛, 1二玉, 3二飛成, 1三玉, 2四角, 1四玉, 1五銀, 2五玉, 3五竜, 1六玉, 2六竜))

[2017-06-28] Mac (2.9 GHz Intel Core i7): Incremental search

benchMateSolver

- avg: 1.985s, min: 1.713s, max: 2.391s

Some(List(5二金, 同玉, 5三金, 同玉, 5四と, 4二玉, 4三金, 3一玉, 3二金打))
benchMateSolver

- avg: 6.504s, min: 5.963s, max: 7.431s

Some(List(3一飛, 2一桂, 1二銀, 同玉, 2四桂, 1一玉, 1二香))
benchMateSolver

- avg: 54.624s, min: 53.085s, max: 57.543s

Some(List(2三角成, 3一香, 3三桂, 同銀引, 同馬, 2四飛, 3一飛成, 同銀, 1二銀, 同玉, 1三香, 2一玉, 1一香成))
benchMateSolver

- avg: 91.213s, min: 87.138s, max: 97.839s

Some(List(3二角, 同銀, 3一飛, 1二玉, 3二飛成, 1三玉, 2四角, 1四玉, 1五銀, 2五玉, 3五竜, 1六玉, 2六竜))
benchMateSolver

- avg: 0.090s, min: 0.081s, max: 0.108s

Some(List(2三角不成, 1一玉, 1二歩, 2二玉, 3二飛成))
benchMateSolver

- avg: 7.914s, min: 7.855s, max: 7.958s

Some(List(3一飛, 1二玉, 3二飛成, 1三玉, 2四角, 1四玉, 1五銀, 2五玉, 3五竜, 1六玉, 2六竜))

*/
20 changes: 20 additions & 0 deletions shared/src/main/scala/com/mogproject/mogami/core/state/State.scala
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,26 @@ case class State(turn: Player = BLACK,
*/
def hasHand(h: Hand): Boolean = hand.get(h).exists(_ > 0)

/**
* Check if Uchifuzume can happen at the next move.
*
* @return true if Uchifuzume can happen
* @note condition:
* - Turn's player can drop a pawn in hand on the front square of the opponent's king
* - The opponent's king cannot move
* - The opponent's pieces do not protect the opponent's king's front square
*/
def isUchifuzumePossible: Boolean = getKing(!turn).exists { opponentKing =>
val kingsFrontRank = opponentKing.rank + turn.isBlack.fold(1, -1)
(1 <= kingsFrontRank && kingsFrontRank <= 9) && {
val kingsFront = Square(opponentKing.file, kingsFrontRank)

attackBBInHand.get(Hand(turn, PAWN)).exists(_.get(kingsFront)) &&
(attackBBOnBoard(!turn)(opponentKing) & ~getAttackBB(turn) & (~occupancy(!turn))).isEmpty &&
attackBBOnBoard(!turn).forall { case (sq, bb) => sq == opponentKing || !bb.get(kingsFront) }
}
}

/**
* Create a Move instance from the next state
*
Expand Down
213 changes: 105 additions & 108 deletions shared/src/main/scala/com/mogproject/mogami/mate/MateSolver.scala
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package com.mogproject.mogami.mate

import com.mogproject.mogami._
import com.mogproject.mogami.util.Implicits._
import com.mogproject.mogami.core.state.ThreadUnsafeStateCache

import scala.annotation.tailrec

/**
* Solve a mate problem.
*/
Expand All @@ -13,32 +12,44 @@ object MateSolver {

// flags and statistics
private[this] var timeout: Boolean = false
private[this] var numComputedNodes: Int = 0
private[this] var timeoutCounter: Int = 0
private[this] var timeLimit: Long = 0

def solve(state: State, lastMoveTo: Option[Square] = None, maxDepth: Int = 29, timeLimitMillis: Long = 15000): Option[Seq[Move]] = {
/**
* Solve a mate.
*
* @param state state
* @param lastMoveTo last move to
* @param minDepth minimum depth
* @param maxDepth maximum depth
* @param timeLimitMillis time limit
* @return Some(List(mv1, mv2, ...)): Found a solution.
* Some(Nil): No solutions.
* None: Time or depth limit exceeded
*/
def solve(state: State, lastMoveTo: Option[Square] = None, minDepth: Int = 3, maxDepth: Int = 29, timeLimitMillis: Long = 15000): Option[Seq[Move]] = {
// initialization
numComputedNodes = 0
timeoutCounter = 0
timeLimit = System.currentTimeMillis() + timeLimitMillis
timeout = false

solveImpl(state, lastMoveTo, maxDepth, timeLimitMillis)
solveImpl(state, lastMoveTo, minDepth, maxDepth, timeLimitMillis)
}

private[this] def solveImpl(state: State, lastMoveTo: Option[Square], maxDepth: Int, timeLimitMillis: Long): Option[Seq[Move]] = {
3 to maxDepth by 2 map { n =>
depthFirstSearch(state, n) match {
case Some(result) =>
val states = result.flatMap(mateSolverStateCache.get)
type T = (State, Option[Square], Option[Move])
val moves = states.scanLeft[T, List[T]]((state, lastMoveTo, None)) { case ((st, lmt, mv), nst) =>
val m = st.createMoveFromNextState(nst, lmt)
(nst, m.map(_.to), m)
private[this] def solveImpl(state: State, lastMoveTo: Option[Square], minDepth: Int, maxDepth: Int, timeLimitMillis: Long): Option[Seq[Move]] = {
minDepth to maxDepth by 2 map { depth =>
searchAttack(state, depth - 1) match {
case Some(xs) if xs.nonEmpty =>
// Found a solution
val moves = (lastMoveTo :: xs.init.map(mv => Some(mv.to))).zip(xs).map {
case (Some(to), mv) if mv.to == to => mv.copy(isSameSquare = true)
case (_, mv) => mv
}
return if (moves.isEmpty) Some(Seq.empty) else Some(moves.tail.map(_._3.get))
case None => // continue searching
return Some(moves)
case Some(Nil) =>
// No solutions
return Some(Nil)
case None => // No conclusion
}
}
None // depth limit exceeded
Expand All @@ -49,98 +60,13 @@ object MateSolver {
private[this] def refreshStateCache(keep: => Set[StateHash]): Unit =
if ((timeoutCounter & 2047) == 0 && mateSolverStateCache.numKeys > 100000) mateSolverStateCache.refresh(keep)

protected[mate] def removeParentNode(xss: List[List[StateHash]]): List[List[StateHash]] = if (xss.isEmpty) Nil else removeLeaf(xss.tail)

@tailrec
protected[mate] def removeVerified(xss: List[List[StateHash]]): List[List[StateHash]] =
if (xss.isDefinedAt(1) && xss(1).length == 1) removeVerified(xss.drop(2)) else removeParentNode(xss)

protected[mate] def removeVerifiedThis(xss: List[List[StateHash]]): List[List[StateHash]] =
if (xss.headOption.exists(_.length == 1)) removeVerified(xss.tail) else removeLeaf(xss)

@tailrec
final protected[mate] def removeLeaf(xss: List[List[StateHash]]): List[List[StateHash]] =
if (xss.isEmpty)
Nil
else if (xss.head.isEmpty || xss.head.tail.isEmpty)
removeLeaf(xss.tail)
else
xss.head.tail :: xss.tail

def depthFirstSearch(initialState: State, maxDepth: Int): Option[List[StateHash]] = {

@tailrec
def f(sofar: List[List[StateHash]], solution: List[StateHash], isUnProven: Boolean): Option[List[StateHash]] = {
timeoutCounter += 1

if (timeout || checkTimeout()) {
timeout = true // necessary for Javascript
None
} else {
refreshStateCache(sofar.flatten.toSet ++ solution)

val depth = sofar.length

if (sofar.isEmpty) {
if (solution.isEmpty) if (isUnProven) None else Some(Nil) else Some(solution.reverse.tail)
} else {
// get the current state
mateSolverStateCache.apply(sofar.head.head) match {
case st if depth % 2 == 1 =>
//
// attacker's turn
//
if (depth > maxDepth) {
f(removeVerified(sofar), Nil, isUnProven = true)
} else {
val checkMoves = st.legalMoves(None).filter(_.isCheck)

findImmediateCheckmate(st, checkMoves) match {
case Some(s) => // found an immediate checkmate
// println(s"im: ${depth + 1}")
// take a longer solution
f(removeVerifiedThis(sofar), if (solution.length <= depth) mateSolverStateCache.set(s) :: sofar.map(_.head) else solution, isUnProven)
case None =>
if (checkMoves.isEmpty) {
f(removeVerified(sofar), Nil, isUnProven = isUnProven) // no solution
} else {
// println(s"at ${depth}: " + sortMoves(checkMoves).map(_.toJapaneseNotationString))
// numComputedNodes += checkMoves.length
f(sortMoves(checkMoves).toList.map(mv => makeMove(st, mv)) :: sofar, solution, isUnProven)
}
}
}

case st =>
//
// defender's turn
//
val legalMoves = st.legalMoves(None)

if (legalMoves.isEmpty) {
if (mateSolverStateCache.get(sofar.tail.head.head).get.createMoveFromNextState(st).get.isPawnDrop) {
f(removeLeaf(sofar), Nil, isUnProven) // Uchifuzume
} else {
// found a solution
// println(s"fd: ${depth}")
f(removeVerified(sofar), if (solution.length < depth) sofar.map(_.head) else solution, isUnProven)
}
} else {
// println(s"df ${depth}: " + sortMoves(legalMoves).map(_.toJapaneseNotationString))
// numComputedNodes += legalMoves.length
f(sortMoves(legalMoves).toList.map(mv => makeMove(st, mv)) :: sofar, solution, isUnProven)
}
}
}
}
def findImmediateCheckmate(state: State, checkMoves: Seq[Move]): Option[Move] = {
// val mvs = checkMoves.filter(mv => !mv.isPawnDrop)
// if (mvs.isEmpty) None else mvs.view.map(mv => mateSolverStateCache.get(makeMove(state, mv)).get).find(_.isMated)
for (mv <- checkMoves) {
if (mateSolverStateCache.get(makeMove(state, mv)).get.isMated) return Some(mv)
}

f(List(List(mateSolverStateCache.set(initialState))), Nil, isUnProven = false)
}

def findImmediateCheckmate(state: State, checkMoves: Seq[Move]): Option[State] = {
val mvs = checkMoves.filter(mv => !mv.isPawnDrop)
if (mvs.isEmpty) None else mvs.view.map(mv => mateSolverStateCache.get(makeMove(state, mv)).get).find(_.isMated)
None
}

private[this] def sortMoves(moves: Seq[Move]): Seq[Move] = {
Expand All @@ -154,4 +80,75 @@ object MateSolver {
val fromCache = StateHash.getNextStateHash(state, move)
if (mateSolverStateCache.hasKey(fromCache)) fromCache else mateSolverStateCache.set(state.makeMove(move).get)
}

/**
* Recursive functions
*/
final private[this] def searchAttack(state: State, depth: Int): Option[List[Move]] = {
timeoutCounter += 1

if (timeout || checkTimeout()) {
timeout = true // necessary for Javascript
None
} else {
refreshStateCache(Set.empty)

val checkMoves = state.legalMoves(None).filter(_.isCheck)
val candidates = state.isUchifuzumePossible.fold(checkMoves.filterNot(_.isPawnDrop), checkMoves)

if (candidates.isEmpty) {
// No attack moves
Some(Nil)
} else {
findImmediateCheckmate(state, candidates) match {
case Some(mv) =>
// Found an immediate checkmate
Some(List(mv))
case None if depth > 0 =>
// Continue search.
var sofar: Option[List[Move]] = Some(Nil)
for (mv <- sortMoves(candidates)) {
searchDefence(mateSolverStateCache.get(makeMove(state, mv)).get, depth - 1) match {
case Some(Nil) => // No valid moves
case Some(xs) if xs.nonEmpty => return Some(mv :: xs) // Found a solution
case None => sofar = None // No conclusion
}
}
sofar
case _ =>
// Reaches the max depth
None
}
}
}
}

final private[this] def searchDefence(state: State, depth: Int): Option[List[Move]] = {
val legalMoves = state.legalMoves(None)
var candidateLength = 0
var candidate = List.empty[Move]

for (mv <- sortMoves(legalMoves)) {
// incremental search
var found = false
for (d <- 0 until depth by 2 if !found) {
searchAttack(mateSolverStateCache.get(makeMove(state, mv)).get, d) match {
case None => // Reached the max depth
case Some(xs) if xs.nonEmpty =>
// Found a solution
val len = xs.length
if (len > candidateLength) {
candidateLength = len
candidate = mv :: xs
}
found = true
case Some(Nil) =>
// No solution
return Some(Nil)
}
}
if (!found) return None // Reached the max depth
}
Some(candidate)
}
}
Loading