From 7ede90028770bf930da91fd3d18c0ca57bbbd8f1 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Wed, 29 Feb 2012 01:16:16 +0100 Subject: [PATCH] Improve engine performances --- benchmark/src/main/scala/Benchmark.scala | 26 +++++++++++--- chess/src/main/scala/Actor.scala | 2 +- chess/src/main/scala/Board.scala | 4 --- chess/src/main/scala/Game.scala | 23 ++++-------- chess/src/main/scala/Move.scala | 8 ----- chess/src/main/scala/Pos.scala | 16 ++++++--- chess/src/main/scala/Situation.scala | 6 ++-- chess/src/main/scala/format/PgnDump.scala | 36 ++++++++----------- chess/src/test/scala/BoardTest.scala | 2 +- chess/src/test/scala/LilaTest.scala | 15 ++++++++ chess/src/test/scala/PlayOneMoveTest.scala | 12 +++++++ chess/src/test/scala/format/PgnDumpTest.scala | 14 ++++---- project/Build.scala | 4 +-- 13 files changed, 95 insertions(+), 73 deletions(-) create mode 100644 chess/src/test/scala/PlayOneMoveTest.scala diff --git a/benchmark/src/main/scala/Benchmark.scala b/benchmark/src/main/scala/Benchmark.scala index 0dc9a3a52a..ea86bd0397 100644 --- a/benchmark/src/main/scala/Benchmark.scala +++ b/benchmark/src/main/scala/Benchmark.scala @@ -2,24 +2,40 @@ package lila.benchmark import annotation.tailrec import com.google.caliper.Param +import ornicar.scalalib.OrnicarValidation -import lila.chess.Game +import lila.chess.{ Game, Pos } import lila.chess.Pos._ // a caliper benchmark is a class that extends com.google.caliper.Benchmark // the SimpleScalaBenchmark trait does it and also adds some convenience functionality -class Benchmark extends SimpleScalaBenchmark { +class Benchmark extends SimpleScalaBenchmark with OrnicarValidation { @Param(Array("100")) val length: Int = 0 def timeImmortal(reps: Int) = repeat(reps) { - val s = Game().playMoves(E2 -> E4, D7 -> D5, E4 -> D5, D8 -> D5, B1 -> C3, D5 -> A5, D2 -> D4, C7 -> C6, G1 -> F3, C8 -> G4, C1 -> F4, E7 -> E6, H2 -> H3, G4 -> F3, D1 -> F3, F8 -> B4, F1 -> E2, B8 -> D7, A2 -> A3, E8 -> C8, A3 -> B4, A5 -> A1, E1 -> D2, A1 -> H1, F3 -> C6, B7 -> C6, E2 -> A6) + val s = playMoves(E2 -> E4, D7 -> D5, E4 -> D5, D8 -> D5, B1 -> C3, D5 -> A5, D2 -> D4, C7 -> C6, G1 -> F3, C8 -> G4, C1 -> F4, E7 -> E6, H2 -> H3, G4 -> F3, D1 -> F3, F8 -> B4, F1 -> E2, B8 -> D7, A2 -> A3, E8 -> C8, A3 -> B4, A5 -> A1, E1 -> D2, A1 -> H1, F3 -> C6, B7 -> C6, E2 -> A6) if (s.isFailure) throw new Exception("success") "haha" } - def timeDeepBlue(reps: Int) = repeat(reps) { - Game().playMoves(E2 -> E4, C7 -> C5, C2 -> C3, D7 -> D5, E4 -> D5, D8 -> D5, D2 -> D4, G8 -> F6, G1 -> F3, C8 -> G4, F1 -> E2, E7 -> E6, H2 -> H3, G4 -> H5, E1 -> G1, B8 -> C6, C1 -> E3, C5 -> D4, C3 -> D4, F8 -> B4) + //def timeDeepBlue(reps: Int) = repeat(reps) { + //playMoves(E2 -> E4, C7 -> C5, C2 -> C3, D7 -> D5, E4 -> D5, D8 -> D5, D2 -> D4, G8 -> F6, G1 -> F3, C8 -> G4, F1 -> E2, E7 -> E6, H2 -> H3, G4 -> H5, E1 -> G1, B8 -> C6, C1 -> E3, C5 -> D4, C3 -> D4, F8 -> B4) + //} + + def playMoves(moves: (Pos, Pos)*): Valid[Game] = { + val game = moves.foldLeft(success(Game()): Valid[Game]) { + (vg, move) ⇒ { + vg flatMap { g ⇒ + // will be called to pass as playable squares to the client + g.situation.destinations + g.playMove(move._1, move._2) + } + } + } + if (game.isFailure) throw new RuntimeException("Failure!") + game } + } diff --git a/chess/src/main/scala/Actor.scala b/chess/src/main/scala/Actor.scala index babead8455..c7c8e6315d 100644 --- a/chess/src/main/scala/Actor.scala +++ b/chess/src/main/scala/Actor.scala @@ -168,5 +168,5 @@ case class Actor(piece: Piece, pos: Pos, board: Board) { private def history = board.history private def friends = board occupation color private def enemies = board occupation !color - private val pawnDir: Direction = if (color == White) _.up else _.down + private lazy val pawnDir: Direction = if (color == White) _.up else _.down } diff --git a/chess/src/main/scala/Board.scala b/chess/src/main/scala/Board.scala index 54fdf8219d..66b316b8a3 100644 --- a/chess/src/main/scala/Board.scala +++ b/chess/src/main/scala/Board.scala @@ -43,8 +43,6 @@ case class Board(pieces: Map[Pos, Piece], history: History) { if (pieces contains at) None else Some(copy(pieces = pieces + ((at, piece)))) - def takeValid(at: Pos): Valid[Board] = take(at) toSuccess ("No piece at " + at + " to take") - def take(at: Pos): Option[Board] = pieces get at map { piece ⇒ copy(pieces = pieces - at) } @@ -87,8 +85,6 @@ case class Board(pieces: Map[Pos, Piece], history: History) { def updateHistory(f: History ⇒ History) = copy(history = f(history)) - def as(c: Color) = Situation(this, c) - def count(p: Piece) = pieces.values count (_ == p) def visual = Visual >> this diff --git a/chess/src/main/scala/Game.scala b/chess/src/main/scala/Game.scala index a54288e5de..6e98895bff 100644 --- a/chess/src/main/scala/Game.scala +++ b/chess/src/main/scala/Game.scala @@ -7,26 +7,15 @@ case class Game( player: Color, reversedPgnMoves: List[String] = Nil) { - def this() = this(Board.empty, White) - - def playMoves(moves: (Pos, Pos)*): Valid[Game] = - moves.foldLeft(success(this): Valid[Game]) { (sit, move) ⇒ - sit flatMap { s ⇒ s.playMove(move._1, move._2) } - } - def playMove(from: Pos, to: Pos, promotion: PromotableRole = Queen): Valid[Game] = for { move ← situation.move(from, to, promotion) - } yield copy( - board = move.after, - player = !player, - reversedPgnMoves = (PgnDump move move) :: reversedPgnMoves - ) + } yield { + val newGame = copy(board = move.after, player = !player) + val pgnMove = PgnDump.move(situation, move, newGame.situation) + newGame.copy(reversedPgnMoves = pgnMove :: reversedPgnMoves) + } - val players = List(White, Black) - - def situation = board as player - - def as(c: Color) = copy(player = c) + lazy val situation = Situation(board, player) def pgnMoves = reversedPgnMoves.reverse } diff --git a/chess/src/main/scala/Move.scala b/chess/src/main/scala/Move.scala index f12babef1e..49ce4b391f 100644 --- a/chess/src/main/scala/Move.scala +++ b/chess/src/main/scala/Move.scala @@ -13,14 +13,6 @@ case class Move( def withHistory(h: History) = copy(after = after withHistory h) - def situation = before as piece.color - - // does this move check the opponent? - def checks: Boolean = (after as !color).check - - // does this move checkmate the opponent? - def checkMates: Boolean = (after as !color).checkMate - // does this move capture an opponent piece? def captures = capture.isDefined diff --git a/chess/src/main/scala/Pos.scala b/chess/src/main/scala/Pos.scala index 161d0800cd..f5944efb0e 100644 --- a/chess/src/main/scala/Pos.scala +++ b/chess/src/main/scala/Pos.scala @@ -41,12 +41,9 @@ sealed case class Pos private (x: Int, y: Int) { object Pos { - private val bounds: Set[Int] = (1 to 8) toSet + def makePos(x: Int, y: Int): Option[Pos] = allCoords get (x, y) - def makePos(x: Int, y: Int): Option[Pos] = - if (bounds(x) && bounds(y)) Some(Pos(x, y)) else None - - def unsafe(x: Int, y: Int): Pos = Pos(x, y) + def unsafe(x: Int, y: Int): Pos = allCoords((x, y)) def xToString(x: Int) = (96 + x).toChar.toString @@ -117,5 +114,14 @@ object Pos { val allKeys: Map[String, Pos] = Map("a1" -> A1, "a2" -> A2, "a3" -> A3, "a4" -> A4, "a5" -> A5, "a6" -> A6, "a7" -> A7, "a8" -> A8, "b1" -> B1, "b2" -> B2, "b3" -> B3, "b4" -> B4, "b5" -> B5, "b6" -> B6, "b7" -> B7, "b8" -> B8, "c1" -> C1, "c2" -> C2, "c3" -> C3, "c4" -> C4, "c5" -> C5, "c6" -> C6, "c7" -> C7, "c8" -> C8, "d1" -> D1, "d2" -> D2, "d3" -> D3, "d4" -> D4, "d5" -> D5, "d6" -> D6, "d7" -> D7, "d8" -> D8, "e1" -> E1, "e2" -> E2, "e3" -> E3, "e4" -> E4, "e5" -> E5, "e6" -> E6, "e7" -> E7, "e8" -> E8, "f1" -> F1, "f2" -> F2, "f3" -> F3, "f4" -> F4, "f5" -> F5, "f6" -> F6, "f7" -> F7, "f8" -> F8, "g1" -> G1, "g2" -> G2, "g3" -> G3, "g4" -> G4, "g5" -> G5, "g6" -> G6, "g7" -> G7, "g8" -> G8, "h1" -> H1, "h2" -> H2, "h3" -> H3, "h4" -> H4, "h5" -> H5, "h6" -> H6, "h7" -> H7, "h8" -> H8) + val allCoords: Map[(Int, Int), Pos] = { + for { + x ← 1 to 8 + xString = xToString(x) + y ← 1 to 8 + key = xString + y.toString + } yield (x, y) -> allKeys(key) + } toMap + def all = allKeys.values } diff --git a/chess/src/main/scala/Situation.scala b/chess/src/main/scala/Situation.scala index bf9604902d..9414d5eb5a 100644 --- a/chess/src/main/scala/Situation.scala +++ b/chess/src/main/scala/Situation.scala @@ -8,6 +8,8 @@ case class Situation(board: Board, color: Color) { case actor if actor.moves.nonEmpty ⇒ actor.pos -> actor.moves } toMap + lazy val destinations: Map[Pos, List[Pos]] = moves mapValues { ms => ms map (_.dest) } + lazy val check: Boolean = board kingPosOf color map { king ⇒ board actorsOf !color exists (_ threatens king) } getOrElse false @@ -33,9 +35,7 @@ case class Situation(board: Board, color: Color) { b3 ← b2.place(color - promotion, to) } yield m.copy(after = b3, promotion = Some(promotion)) - } toSuccess "Invalid move %s->%s".format(from, to).wrapNel - - def as(c: Color) = copy(color = c) + } toSuccess "Invalid move %s %s".format(from, to).wrapNel } object Situation { diff --git a/chess/src/main/scala/format/PgnDump.scala b/chess/src/main/scala/format/PgnDump.scala index 0410ed1580..e5215305e9 100644 --- a/chess/src/main/scala/format/PgnDump.scala +++ b/chess/src/main/scala/format/PgnDump.scala @@ -3,27 +3,21 @@ package format object PgnDump { - def move(m: Move): String = { - import m._ - - def disambiguate = { - val candidates = situation.actors filter { a ⇒ - a.pos != orig && a.piece.role == piece.role && (a.destinations contains dest) - } - if (candidates.isEmpty) "" - else if (candidates exists (_.pos ?| orig)) orig.file + orig.rank else orig.file - } - def capturing = if (captures) "x" else "" - def checking = if (checkMates) "#" else if (checks) "+" else "" - + def move(situation: Situation, data: Move, nextSituation: Situation): String = { + import data._ ((promotion, piece.role) match { - case _ if castle ⇒ if (orig ?> dest) "O-O-O" else "O-O" - case _ if enpassant ⇒ orig.file + 'x' + dest.rank - case (Some(promotion), _) ⇒ dest.key + promotion.pgn - case (_, Pawn) if captures ⇒ orig.file + 'x' + dest.key - case (_, Pawn) ⇒ dest.key - case (_, role) ⇒ role.pgn + disambiguate + capturing + dest.key - case _ ⇒ "?" - }) + checking + case _ if castle ⇒ if (orig ?> dest) "O-O-O" else "O-O" + case _ if enpassant ⇒ orig.file + 'x' + dest.rank + case (Some(promotion), _) ⇒ dest.key + promotion.pgn + case (_, Pawn) ⇒ (if (captures) orig.file + "x" else "") + dest.key + case (_, role) ⇒ role.pgn + { + val candidates = situation.actors filter { a ⇒ + a.piece.role == piece.role && a.pos != orig && (a.destinations contains dest) + } + if (candidates.isEmpty) "" + else if (candidates exists (_.pos ?| orig)) orig.file + orig.rank else orig.file + } + (if (captures) "x" else "") + dest.key + case _ ⇒ "?" + }) + (if (nextSituation.check) if (nextSituation.checkMate) "#" else "+" else "") } } diff --git a/chess/src/test/scala/BoardTest.scala b/chess/src/test/scala/BoardTest.scala index d92cdc0045..59b68813b2 100644 --- a/chess/src/test/scala/BoardTest.scala +++ b/chess/src/test/scala/BoardTest.scala @@ -23,7 +23,7 @@ class BoardTest extends LilaTest { } "allow a piece to be taken" in { - board takeValid A1 must beSuccess.like { + board take A1 must beSome.like { case b ⇒ b(A1) must beNone } } diff --git a/chess/src/test/scala/LilaTest.scala b/chess/src/test/scala/LilaTest.scala index fedc90cd7d..d679ddd274 100644 --- a/chess/src/test/scala/LilaTest.scala +++ b/chess/src/test/scala/LilaTest.scala @@ -12,6 +12,21 @@ trait LilaTest implicit def stringToBoard(str: String): Board = Visual << str + implicit def stringToSituationBuilder(str: String) = new { + + def as(color: Color): Situation = Situation(Visual << str, color) + } + + implicit def richGame(game: Game) = new { + + def as(color: Color): Game = game.copy(player = color) + + def playMoves(moves: (Pos, Pos)*): Valid[Game] = + moves.foldLeft(success(game): Valid[Game]) { (vg, move) ⇒ + vg flatMap { g ⇒ g.playMove(move._1, move._2) } + } + } + def bePoss(poss: Pos*): Matcher[Option[Iterable[Pos]]] = beSome.like { case p ⇒ sortPoss(p.toList) must_== sortPoss(poss.toList) } diff --git a/chess/src/test/scala/PlayOneMoveTest.scala b/chess/src/test/scala/PlayOneMoveTest.scala new file mode 100644 index 0000000000..10e1f5ffa9 --- /dev/null +++ b/chess/src/test/scala/PlayOneMoveTest.scala @@ -0,0 +1,12 @@ +package lila.chess + +import Pos._ + +class PlayOneMoveTest extends LilaTest { + + "playing a move" should { + "only process things once" in { + Game().playMoves(E2 -> E4) must beSuccess + } + } +} diff --git a/chess/src/test/scala/format/PgnDumpTest.scala b/chess/src/test/scala/format/PgnDumpTest.scala index 0cef123911..d856d10a0d 100644 --- a/chess/src/test/scala/format/PgnDumpTest.scala +++ b/chess/src/test/scala/format/PgnDumpTest.scala @@ -5,17 +5,19 @@ import Pos._ class PgnDumpTest extends LilaTest { - "complete game dump" should { - "only moves" in { + val gioachineGreco = Game().playMoves(D2 -> D4, D7 -> D5, C2 -> C4, D5 -> C4, E2 -> E3, B7 -> B5, A2 -> A4, C7 -> C6, A4 -> B5, C6 -> B5, D1 -> F3) + + val peruvianImmortal = Game().playMoves(E2 -> E4, D7 -> D5, E4 -> D5, D8 -> D5, B1 -> C3, D5 -> A5, D2 -> D4, C7 -> C6, G1 -> F3, C8 -> G4, C1 -> F4, E7 -> E6, H2 -> H3, G4 -> F3, D1 -> F3, F8 -> B4, F1 -> E2, B8 -> D7, A2 -> A3, E8 -> C8, A3 -> B4, A5 -> A1, E1 -> D2, A1 -> H1, F3 -> C6, B7 -> C6, E2 -> A6) + + "dump a game to pgn" should { + "move list" in { "Gioachine Greco" in { - val game = Game().playMoves(D2 -> D4, D7 -> D5, C2 -> C4, D5 -> C4, E2 -> E3, B7 -> B5, A2 -> A4, C7 -> C6, A4 -> B5, C6 -> B5, D1 -> F3) - game map (_.pgnMoves) must beSuccess.like { + gioachineGreco map (_.pgnMoves) must beSuccess.like { case ms ⇒ ms must_== ("d4 d5 c4 dxc4 e3 b5 a4 c6 axb5 cxb5 Qf3" split ' ').toSeq } } "Peruvian Immortal" in { - val game = Game().playMoves(E2 -> E4, D7 -> D5, E4 -> D5, D8 -> D5, B1 -> C3, D5 -> A5, D2 -> D4, C7 -> C6, G1 -> F3, C8 -> G4, C1 -> F4, E7 -> E6, H2 -> H3, G4 -> F3, D1 -> F3, F8 -> B4, F1 -> E2, B8 -> D7, A2 -> A3, E8 -> C8, A3 -> B4, A5 -> A1, E1 -> D2, A1 -> H1, F3 -> C6, B7 -> C6, E2 -> A6) - game map (_.pgnMoves) must beSuccess.like { + peruvianImmortal map (_.pgnMoves) must beSuccess.like { case ms ⇒ ms must_== ("e4 d5 exd5 Qxd5 Nc3 Qa5 d4 c6 Nf3 Bg4 Bf4 e6 h3 Bxf3 Qxf3 Bb4 Be2 Nd7 a3 O-O-O axb4 Qxa1+ Kd2 Qxh1 Qxc6+ bxc6 Ba6#" split ' ').toSeq } } diff --git a/project/Build.scala b/project/Build.scala index b002c92088..7aeb95f467 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -42,8 +42,8 @@ object ApplicationBuild extends Build with Resolvers with Dependencies { lazy val benchmark = Project("benchmark", file("benchmark"), settings = Project.defaultSettings).settings( fork in run := true, - libraryDependencies := Seq(instrumenter, gson), - resolvers := Seq(codahale, sonatype), + libraryDependencies := Seq(scalalib, instrumenter, gson), + resolvers := Seq(iliaz, codahale, sonatype), shellPrompt := ShellPrompt.buildShellPrompt, scalacOptions := Seq("-deprecation", "-unchecked"), // we need to add the runtime classpath as a "-cp" argument