diff --git a/.travis.yml b/.travis.yml index 48da262..077ea96 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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" diff --git a/js/src/test/scala/com/mogproject/mogami/bench/BenchmarkJS.scala b/js/src/test/scala/com/mogproject/mogami/bench/BenchmarkJS.scala index ead35a8..73bbef6 100644 --- a/js/src/test/scala/com/mogproject/mogami/bench/BenchmarkJS.scala +++ b/js/src/test/scala/com/mogproject/mogami/bench/BenchmarkJS.scala @@ -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) } @@ -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六竜)) + */ \ No newline at end of file diff --git a/shared/src/main/scala/com/mogproject/mogami/core/state/State.scala b/shared/src/main/scala/com/mogproject/mogami/core/state/State.scala index 0f94c14..81f1a1b 100644 --- a/shared/src/main/scala/com/mogproject/mogami/core/state/State.scala +++ b/shared/src/main/scala/com/mogproject/mogami/core/state/State.scala @@ -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 * diff --git a/shared/src/main/scala/com/mogproject/mogami/mate/MateSolver.scala b/shared/src/main/scala/com/mogproject/mogami/mate/MateSolver.scala index cbfca1e..078fc96 100644 --- a/shared/src/main/scala/com/mogproject/mogami/mate/MateSolver.scala +++ b/shared/src/main/scala/com/mogproject/mogami/mate/MateSolver.scala @@ -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. */ @@ -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 @@ -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] = { @@ -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) + } } diff --git a/shared/src/test/scala/com/mogproject/mogami/core/state/StateSpec.scala b/shared/src/test/scala/com/mogproject/mogami/core/state/StateSpec.scala index a5a3617..a98e770 100644 --- a/shared/src/test/scala/com/mogproject/mogami/core/state/StateSpec.scala +++ b/shared/src/test/scala/com/mogproject/mogami/core/state/StateSpec.scala @@ -1099,6 +1099,243 @@ class StateSpec extends FlatSpec with MustMatchers with GeneratorDrivenPropertyC State.HANDICAP_THREE_PAWNS.unusedPtypeCount mustBe Map(KING -> 0, ROOK -> 1, BISHOP -> 1, GOLD -> 2, SILVER -> 2, KNIGHT -> 2, LANCE -> 2, PAWN -> 6) } + "State#isUchifuzumePossible" must "return true if Uchifuzume is possible" in { + val states = Seq( + Seq( + "P1 * * * * * * * -FU-OU", + "P2 * * * * * * * * * ", + "P3 * * * * * * * * +TO", + "P4 * * * * * * * * * ", + "P5 * * * * * * * * * ", + "P6 * * * * * * * * * ", + "P7 * * * * * * * * * ", + "P8 * * * * * * * * * ", + "P9 * * * * * * * * * ", + "P+00FU", + "P-", + "+" + ), + Seq( + "P1 * * * * * * +TO * -OU", + "P2 * * * * * * * * * ", + "P3 * * * * * * * * +TO", + "P4 * * * * * * * * * ", + "P5 * * * * * * * * * ", + "P6 * * * * * * * * * ", + "P7 * * * * * * * * * ", + "P8 * * * * * * * * * ", + "P9 * * * * * * * * * ", + "P+00FU00FU00FU", + "P-", + "+" + ), + Seq( + "P1 * * * * * * * * * ", + "P2 * * * * * * * * * ", + "P3 * * * * * * * * * ", + "P4 * * * * * * * * * ", + "P5 * * * * * * * * * ", + "P6 * * * * * * * * * ", + "P7 * * * * * * -OU * * ", + "P8 * * * * * * * * * ", + "P9 * * * * * +KE+OU+KY * ", + "P+", + "P-00FU", + "-" + ), + Seq( + "P1 * * * * * * * * * ", + "P2 * * * * * * * * -KA", + "P3 * * * * * * * * * ", + "P4 * * * * * * * * * ", + "P5 * * * * * * * * * ", + "P6 * * * * * * * * * ", + "P7 * -OU * * * * * * * ", + "P8 * * * * * * * * * ", + "P9+OU * * * * * * * * ", + "P+", + "P-00FU", + "-" + ), + Seq( + "P1 * * * * * * * * * ", + "P2 * * * * * * * * * ", + "P3 * * * * +KA * * * * ", + "P4 * * * * -FU * * * * ", + "P5 * * * -KE-OU * * * * ", + "P6 * * * -KY * * +RY * * ", + "P7 * * * * * * * * * ", + "P8 * * * * * * * * * ", + "P9 * * * * * * * * * ", + "P+00FU", + "P-", + "+" + ), + Seq( + "P1 * * * * * * * * * ", + "P2 * * * * * * * * * ", + "P3 * * * * * * * * * ", + "P4 * * * * * -KI * * * ", + "P5 * * * * +KA * +KA * * ", + "P6 * * * * +HI+OU+HI * * ", + "P7 * * * * +FU+FU+FU * * ", + "P8 * * * * * -TO * * * ", + "P9 * * * * * * * * * ", + "P+00FU", + "P-00FU", + "-" + ) + ).map(s => State.parseCsaString(s.mkString("\n"))) + + states.map(_.isUchifuzumePossible) mustBe Seq.fill(states.length)(true) + } + it must "return false if Uchifuzume is impossible" in { + val states = Seq( + Seq( + "P1 * * * * * * * -FU-OU", + "P2 * * * * * * * * * ", + "P3 * * * * * * * * +TO", + "P4 * * * * * * * * * ", + "P5 * * * * * * * * * ", + "P6 * * * * * * * * * ", + "P7 * * * * * * * * * ", + "P8 * * * * * * * * * ", + "P9 * * * * * * * * * ", + "P+", + "P-", + "+" + ), + Seq( + "P1 * * * * * * * -FU-OU", + "P2 * * * * * * * * -FU", + "P3 * * * * * * * * +TO", + "P4 * * * * * * * * * ", + "P5 * * * * * * * * * ", + "P6 * * * * * * * * * ", + "P7 * * * * * * * * * ", + "P8 * * * * * * * * * ", + "P9 * * * * * * * * * ", + "P+00FU", + "P-", + "+" + ), + Seq( + "P1 * * * * * * * -KI-OU", + "P2 * * * * * * * * * ", + "P3 * * * * * * * * +TO", + "P4 * * * * * * * * * ", + "P5 * * * * * * * * * ", + "P6 * * * * * * * * * ", + "P7 * * * * * * * * * ", + "P8 * * * * * * * * * ", + "P9 * * * * * * * * * ", + "P+00FU", + "P-", + "+" + ), + Seq( + "P1 * * * * * * * -FU-OU", + "P2 * * * * * * * -KY * ", + "P3 * * * * * * * * +FU", + "P4 * * * * * * * * * ", + "P5 * * * * * * * * * ", + "P6 * * * * * * * * * ", + "P7 * * * * * * * * * ", + "P8 * * * * * * * * * ", + "P9 * * * * * * * * * ", + "P+00FU", + "P-", + "+" + ), + Seq( + "P1 * * * * * * * -FU-OU", + "P2 * * * * * * * -KY * ", + "P3 * * * * * * * +HI * ", + "P4 * * * * * * * * * ", + "P5 * * * * * * * * * ", + "P6 * * * * * * * * * ", + "P7 * * * * * * * * * ", + "P8 * * * * * * * * * ", + "P9 * * * * * * * * * ", + "P+00FU", + "P-", + "+" + ), + Seq( + "P1 * * * * * * * -FU-OU", + "P2 * * * * * * * * * ", + "P3 * * * * * * * * +TO", + "P4 * * * * * * * * * ", + "P5 * * * * * * * * * ", + "P6 * * * * * * * * * ", + "P7 * * * * * * * * * ", + "P8 * * * * * * * * * ", + "P9 * * * * * * * * * ", + "P+00FU", + "P-", + "-" + ), + Seq( + "P1 * * * * +OU * * * * ", + "P2 * * * -TO * -TO * * * ", + "P3 * * * * -UM * * * * ", + "P4 * * * * * * * * * ", + "P5 * * * * * * * * * ", + "P6 * * * * * * * * * ", + "P7 * * * * * * * * * ", + "P8 * * * * * * * * * ", + "P9 * * * * * * * * * ", + "P+00FU", + "P-00FU", + "-" + ), + Seq( + "P1 * * * * * * * * * ", + "P2 * * * * * * * * * ", + "P3 * * * * * * * * * ", + "P4 * * * * * -KI * * * ", + "P5 * * * * +KA * +KA * * ", + "P6 * * * * +HI+OU+HI * * ", + "P7 * * * * +FU+FU+FU * * ", + "P8 * * * * * -FU * * * ", + "P9 * * * * * * * * * ", + "P+00FU", + "P-00FU", + "-" + ), + Seq( + "P1 * * * * +OU * * * * ", + "P2 * * * -TO * -TO * * * ", + "P3 * * * * -UM * * * * ", + "P4 * * * * * * * * * ", + "P5 * * * * * * * * * ", + "P6 * * * * * * * * * ", + "P7 * * * +RY * +RY * * * ", + "P8 * * * * * * * * * ", + "P9 * * * * -OU * * * * ", + "P+00FU", + "P-00FU", + "+" + ), + Seq( + "P1 * * * * * * * * * ", + "P2 * * * * * * * * * ", + "P3 * * * * +KA * * * * ", + "P4 * * * * -FU * * * * ", + "P5 * * * -KE-OU * * * * ", + "P6 * * * -KY * +HI * * * ", + "P7 * * * * * * * * * ", + "P8 * * * * * * * * * ", + "P9 * * * * * * * * * ", + "P+00FU", + "P-", + "+" + ) + ).map(s => State.parseCsaString(s.mkString("\n"))) + + states.map(_.isUchifuzumePossible) mustBe Seq.fill(states.length)(false) + } + "State#createMoveFromNextState" must "create Move" in { val states = Seq( Seq( diff --git a/shared/src/test/scala/com/mogproject/mogami/mate/MateSolverSpec.scala b/shared/src/test/scala/com/mogproject/mogami/mate/MateSolverSpec.scala index d66ec26..4f9bc7f 100644 --- a/shared/src/test/scala/com/mogproject/mogami/mate/MateSolverSpec.scala +++ b/shared/src/test/scala/com/mogproject/mogami/mate/MateSolverSpec.scala @@ -8,18 +8,6 @@ import org.scalatest.{FlatSpec, MustMatchers} * */ class MateSolverSpec extends FlatSpec with MustMatchers with GeneratorDrivenPropertyChecks { - "MateSolver#removeParentNode" must "return proper lists" in { - MateSolver.removeParentNode(Nil) mustBe Nil - MateSolver.removeParentNode(List(List(1))) mustBe Nil - MateSolver.removeParentNode(List(List(2), List(1))) mustBe Nil - MateSolver.removeParentNode(List(List(2, 3), List(1))) mustBe Nil - MateSolver.removeParentNode(List(List(3), List(2), List(1))) mustBe Nil - MateSolver.removeParentNode(List(List(3, 4), List(2), List(1))) mustBe Nil - MateSolver.removeParentNode(List(List(3), List(2, 4), List(1))) mustBe List(List(4), List(1)) - MateSolver.removeParentNode(List(List(5, 6, 7), List(2, 3, 4), List(1))) mustBe List(List(3, 4), List(1)) - MateSolver.removeParentNode(List(List(6), List(5), List(3, 4), List(2), List(1))) mustBe List(List(4), List(2), List(1)) - } - "MateSolver#solve" must "return answers" in { val s = Seq( @@ -54,7 +42,7 @@ class MateSolverSpec extends FlatSpec with MustMatchers with GeneratorDrivenProp MateSolver.solve(s(0)) mustBe Some(Nil) MateSolver.solve(s(1)) mustBe Some(Nil) MateSolver.solve(s(2)) mustBe Some(Nil) - MateSolver.solve(s(3)) mustBe Some(Nil) + MateSolver.solve(s(3), timeLimitMillis = 4 * 60 * 1000) mustBe Some(Nil) } it must "return None when the solver requires more moves or time" in { val s = Seq( @@ -65,4 +53,13 @@ class MateSolverSpec extends FlatSpec with MustMatchers with GeneratorDrivenProp MateSolver.solve(s(0), maxDepth = 3) mustBe None MateSolver.solve(s(1), maxDepth = 3) mustBe None } + it must "return the longest answer" in { + val s = Seq( + "9/9/6B2/6p2/5n1l1/5p1k1/9/5S+B2/9 b RPr4g3s3n3l15p" + ).map(State.parseSfenString) + + MateSolver.solve(s(0), timeLimitMillis = 20 * 60 * 1000).map(_.map(_.toJapaneseNotationString)) mustBe Some(List( + "4四角成", "1五玉", "1六馬", "1四玉", "2五馬", "同玉", "2六飛", "1四玉", "1五歩", "同玉", "1六香" + )) + } }