diff --git a/Makefile b/Makefile index a016647..e607452 100644 --- a/Makefile +++ b/Makefile @@ -12,5 +12,8 @@ console: clean: ${SBT} clean -.PHONY: build test console clean +bench: clean + ${SBT} mogCoreJVM/test:run mogCoreJS/test:run + +.PHONY: build test console clean bench diff --git a/js/src/test/scala/com/mogproject/mogami/bench/BenchmarkJS.scala b/js/src/test/scala/com/mogproject/mogami/bench/BenchmarkJS.scala new file mode 100644 index 0000000..f027100 --- /dev/null +++ b/js/src/test/scala/com/mogproject/mogami/bench/BenchmarkJS.scala @@ -0,0 +1,27 @@ +package com.mogproject.mogami.bench + +import com.mogproject.mogami.core.BitBoard +import com.mogproject.mogami.core.Ptype.{PROOK, ROOK} +import com.mogproject.mogami.core.PieceConstant._ +import com.mogproject.mogami.core.SquareConstant._ + + +/** + * Benchmarks for Scala.js + */ +object BenchmarkJS extends scalajs.js.JSApp with Benchmark with TestData { + def main(): Unit = { + benchAttack(BP, None, BitBoard.empty, BitBoard.empty) + + benchAttack(BP, Some(P55), BitBoard.empty, BitBoard.empty) + + benchAttack(BPR, Some(P11), BitBoard.empty, BitBoard.empty) + benchAttack(BPR, Some(P11), BitBoard.full, BitBoard.full) + + benchAttack(BPB, Some(P55), BitBoard.empty, BitBoard.empty) + benchAttack(BPB, Some(P55), BitBoard.full, BitBoard.full) + + benchGameLoading(recordSfen01) + benchGameLoading(recordSfen02) + } +} diff --git a/shared/src/main/scala/com/mogproject/mogami/core/Game.scala b/shared/src/main/scala/com/mogproject/mogami/core/Game.scala index cfa773f..1f51674 100644 --- a/shared/src/main/scala/com/mogproject/mogami/core/Game.scala +++ b/shared/src/main/scala/com/mogproject/mogami/core/Game.scala @@ -13,15 +13,25 @@ import scala.util.Try case class Game(initialState: State = State.HIRATE, moves: Seq[Move] = Seq.empty, gameInfo: GameInfo = GameInfo(), - movesOffset: Int = 0 + movesOffset: Int = 0, + givenHistory: Option[Seq[State]] = None ) extends CsaLike with SfenLike { require(history.length == moves.length + 1, "all moves must be valid") import com.mogproject.mogami.core.Game.GameStatus._ + override def equals(obj: scala.Any): Boolean = obj match { + case that: Game => + // ignore givenHistory + initialState == that.initialState && moves == that.moves && gameInfo == that.gameInfo && movesOffset == that.movesOffset + case _ => false + } + /** history of states */ - lazy val history: Seq[State] = moves.scanLeft(Some(initialState): Option[State])((s, m) => s.flatMap(_.makeMove(m))).flatten + lazy val history: Seq[State] = { + givenHistory.getOrElse(moves.scanLeft(Some(initialState): Option[State])((s, m) => s.flatMap(_.makeMove(m))).flatten) + } lazy val hashCodes: Seq[Int] = history.map(_.hashCode()) @@ -49,8 +59,9 @@ case class Game(initialState: State = State.HIRATE, def turn: Player = currentState.turn - def makeMove(move: Move): Option[Game] = - (status == Playing && currentState.isValidMove(move)).option(this.copy(moves = moves :+ move)) + def makeMove(move: Move): Option[Game] = { + (status == Playing && currentState.isValidMove(move)).option(this.copy(moves = moves :+ move, givenHistory = currentState.makeMove(move).map(history :+ _))) + } def makeMove(move: MoveBuilder): Option[Game] = move.toMove(currentState).flatMap(makeMove) diff --git a/shared/src/main/scala/com/mogproject/mogami/core/Move.scala b/shared/src/main/scala/com/mogproject/mogami/core/Move.scala index 2112469..8c7a73b 100644 --- a/shared/src/main/scala/com/mogproject/mogami/core/Move.scala +++ b/shared/src/main/scala/com/mogproject/mogami/core/Move.scala @@ -173,6 +173,8 @@ case class Move(player: Player, def capturedPiece: Option[Piece] = captured.map(Piece(!player, _)) + def moveFrom: MoveFrom = from.map(Left.apply).getOrElse(Right(Hand(player, oldPtype))) + override def toCsaString: String = from.map(fr => MoveBuilderCsaBoard(player, fr, to, newPtype, elapsedTime)) .getOrElse(MoveBuilderCsaHand(player, to, newPtype, elapsedTime)).toCsaString diff --git a/shared/src/main/scala/com/mogproject/mogami/core/State.scala b/shared/src/main/scala/com/mogproject/mogami/core/State.scala index c035ccb..d27677d 100644 --- a/shared/src/main/scala/com/mogproject/mogami/core/State.scala +++ b/shared/src/main/scala/com/mogproject/mogami/core/State.scala @@ -177,7 +177,12 @@ case class State(turn: Player = BLACK, board: BoardType = Map.empty, hand: HandT m.mapValues(_ & ~occupancy(turn)).filter(_._2.nonEmpty) } - // todo: consider converting to Set + /** + * Get all legal moves. + * + * @note This method can be relatively expensive. + * @return list of legal moves + */ def legalMoves: Seq[Move] = ( for { (from, bb) <- legalMovesBB @@ -186,14 +191,6 @@ case class State(turn: Player = BLACK, board: BoardType = Map.empty, hand: HandT mv <- from.fold(MoveBuilderSfenBoard(_, to, promote), p => MoveBuilderSfenHand(p.ptype, to)).toMove(this) } yield mv).toSeq - /** - * Check if the move is legal. - * - * @param move move to test - * @return true if the move is legal - */ - def isValidMove(move: Move): Boolean = legalMoves.contains(move.copy(elapsedTime = None)) - /** * * Check if the state is mated. * @@ -250,6 +247,17 @@ case class State(turn: Player = BLACK, board: BoardType = Map.empty, hand: HandT case None => List() } + /** + * Check if the move is legal. + * + * @param move move to test + * @return true if the move is legal + */ + def isValidMove(move: Move): Boolean = { + val mf = move.moveFrom + canAttack(mf, move.to) && getPromotionList(mf, move.to).contains(move.promote) + } + /** * Check if the in-hand piece is non-empty. */ @@ -281,14 +289,6 @@ object State extends CsaStateReader with SfenStateReader { val empty = State(BLACK, Map.empty, EMPTY_HANDS) 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. - * - * @return None if the king is not on board - */ - def getKingSquare(player: Player, board: BoardType): Option[Square] = - board.view.filter { case (s, p) => p == Piece(player, KING) }.map(_._1).headOption - // constant states val HIRATE = State(BLACK, Map( Square(1, 1) -> Piece(WHITE, LANCE), diff --git a/shared/src/main/scala/com/mogproject/mogami/util/BenchmarkUtil.scala b/shared/src/main/scala/com/mogproject/mogami/util/BenchmarkUtil.scala new file mode 100644 index 0000000..c67169c --- /dev/null +++ b/shared/src/main/scala/com/mogproject/mogami/util/BenchmarkUtil.scala @@ -0,0 +1,12 @@ +package com.mogproject.mogami.util + +object BenchmarkUtil { + def withTime[A](label: String)(thunk: => A): A = { + val start = System.currentTimeMillis() + val ret = thunk + val end = System.currentTimeMillis() + println(s"${label}: ${(end - start) / 1000.0}s") + ret + } + +} diff --git a/shared/src/test/scala/com/mogproject/mogami/bench/Benchmark.scala b/shared/src/test/scala/com/mogproject/mogami/bench/Benchmark.scala new file mode 100644 index 0000000..0d509ff --- /dev/null +++ b/shared/src/test/scala/com/mogproject/mogami/bench/Benchmark.scala @@ -0,0 +1,57 @@ +package com.mogproject.mogami.bench + +import com.mogproject.mogami._ + +import scala.io.Source + +/** + * Shared benchmark utility + */ +case class BenchResult(result: Seq[Double]) { + def average: Double = if (result.isEmpty) 0.0 else result.sum / result.length / 1000 + + def maxValue: Double = result.max / 1000 + + def minValue: Double = result.min / 1000 + + def print(): Unit = { + println(f"\n- avg: ${average}%.3fs, min: ${minValue}s, max: ${maxValue}s\n") + } +} + +trait Benchmark { + + val benchmarkCount = 3 + val attackRepeat = 10000 + + private[this] def withBenchmark(thunk: => Unit): BenchResult = { + val ret = (1 to benchmarkCount).map { n => + val start = System.currentTimeMillis() + thunk + (System.currentTimeMillis() - start).toDouble + } + BenchResult(ret) + } + + def benchGameLoading(sfen: String): Unit = { + println(s"benchGameLoading: sfen=${sfen}") + + withBenchmark { + val g = Game.parseSfenString(sfen) + assert(g.isDefined) + }.print() + } + + def benchAttack(piece: Piece, from: Option[Square], allOcc: BitBoard, myPawnOcc: BitBoard): Unit = { + println(s"benchAttack: repeat=${attackRepeat}, piece=${piece}, from=${from}, allOcc=${allOcc.toOctalString}, myPawnOcc=${myPawnOcc.toOctalString}") + + withBenchmark { + var i = 0 + while (i < attackRepeat) { + Attack.get(piece, from, allOcc, myPawnOcc) + i += 1 + } + }.print() + } + +} diff --git a/shared/src/test/scala/com/mogproject/mogami/bench/BenchmarkJVM.scala b/shared/src/test/scala/com/mogproject/mogami/bench/BenchmarkJVM.scala new file mode 100644 index 0000000..79fc72e --- /dev/null +++ b/shared/src/test/scala/com/mogproject/mogami/bench/BenchmarkJVM.scala @@ -0,0 +1,13 @@ +package com.mogproject.mogami.bench + +/** + * Benchmarks for Scala JVM + */ +object BenchmarkJVM extends Benchmark with TestData { + + def main(args: Array[String]): Unit = { + benchGameLoading(recordSfen01) + benchGameLoading(recordSfen02) + } + +} diff --git a/shared/src/test/scala/com/mogproject/mogami/bench/TestData.scala b/shared/src/test/scala/com/mogproject/mogami/bench/TestData.scala new file mode 100644 index 0000000..a424445 --- /dev/null +++ b/shared/src/test/scala/com/mogproject/mogami/bench/TestData.scala @@ -0,0 +1,13 @@ +package com.mogproject.mogami.bench + +trait TestData { + val recordSfen01: String = "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b - 0" + val recordSfen02: String = Seq( + "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b - 0", + " 7g7f 3c3d 2g2f 4a3b 6i7h 8c8d 2f2e 2b8h+ 7i8h 3a2b", + " 3i3h 2b3c 3g3f 7a7b 5i6h 6c6d 3h3g 8d8e 3g4f 8e8f", + " 8g8f 8b8f 2e2d 2c2d 8i7g 8f8b 3f3e 3d3e 4f3e 7c7d", + " 3e2d 3c2d 2h2d P*2c 2d2f 7d7e P*8c 7b8c B*6c B*7d", + " 6c1h+ 7d4g+ 4i5h 4g1d" + ).mkString +}