diff --git a/benchmark/src/main/scala/Benchmark.scala b/benchmark/src/main/scala/Benchmark.scala index 8314c94e4b..da559b45ef 100644 --- a/benchmark/src/main/scala/Benchmark.scala +++ b/benchmark/src/main/scala/Benchmark.scala @@ -61,7 +61,7 @@ class Benchmark extends SimpleScalaBenchmark { ps = ps, aiLevel = None, isWinner = None, - evts = Some("0s|1Msystem White creates the game|2Msystem Black joins the game|3r/ipkkf590ldrr"), + evts = "0s|1Msystem White creates the game|2Msystem Black joins the game", elo = Some(1280) ) val white = newDbPlayer("white", "ip ar jp bn kp cb lp dq mp ek np fb op gn pp hr") diff --git a/system/src/main/scala/Server.scala b/system/src/main/scala/Server.scala index 67350616e0..ac3fe171d0 100644 --- a/system/src/main/scala/Server.scala +++ b/system/src/main/scala/Server.scala @@ -16,13 +16,11 @@ final class Server(repo: GameRepo) { dest ← posAt(destString) toValid "Wrong dest " + destString promotion ← Role promotable promString toValid "Wrong promotion " + promString gameAndPlayer ← repo player fullId toValid "Wrong ID " + fullId - (game, player) = gameAndPlayer - chessGame = game.toChess + (g1, _) = gameAndPlayer + chessGame = g1.toChess newChessGameAndMove ← chessGame(orig, dest, promotion) (newChessGame, move) = newChessGameAndMove - g1 = game update newChessGame - eventStacks = game.eventStacks mapValues (_ withMove move optimize) - g2 = g1 withEventStacks eventStacks + g2 = g1.update(newChessGame, move) result ← unsafe { repo save g2 } } yield newChessGame.situation.destinations diff --git a/system/src/main/scala/model/DbGame.scala b/system/src/main/scala/model/DbGame.scala index e783d654c5..a6c985ea72 100644 --- a/system/src/main/scala/model/DbGame.scala +++ b/system/src/main/scala/model/DbGame.scala @@ -75,12 +75,13 @@ case class DbGame( ) } - def update(game: Game): DbGame = { + def update(game: Game, move: Move): DbGame = { val allPieces = (game.board.pieces map { case (pos, piece) ⇒ (pos, piece, false) }) ++ (game.deads map { case (pos, piece) ⇒ (pos, piece, true) }) + val events = Event fromMove move copy( pgn = game.pgnMoves, players = for { @@ -92,23 +93,18 @@ case class DbGame( if (dead) piece.role.forsyth.toUpper else piece.role.forsyth } - } mkString " " + } mkString " ", + evts = (player.eventStack withEvents { + events ::: List( + PossibleMovesEvent( + if (color == game.player) game.situation.destinations else Map.empty + ) + ) + }).optimize encode ), turns = game.turns ) } - - def eventStacks: Map[DbPlayer, EventStack] = players map { player => - (player, EventStack decode player.evts) - } toMap - - def withEventStacks(stacks: Map[DbPlayer, EventStack]): DbGame = copy( - players = players map { player ⇒ - stacks get player some { stack ⇒ - player.copy(evts = stack.encode) - } none player - } - ) } object DbGame { diff --git a/system/src/main/scala/model/DbPlayer.scala b/system/src/main/scala/model/DbPlayer.scala index fa2948e8cb..bc8a97d81d 100644 --- a/system/src/main/scala/model/DbPlayer.scala +++ b/system/src/main/scala/model/DbPlayer.scala @@ -10,5 +10,7 @@ case class DbPlayer( evts: String = "", elo: Option[Int]) { + def eventStack = EventStack decode evts + def isAi = aiLevel.isDefined } diff --git a/system/src/main/scala/model/Event.scala b/system/src/main/scala/model/Event.scala index 7bf134b686..eb7b45e3ba 100644 --- a/system/src/main/scala/model/Event.scala +++ b/system/src/main/scala/model/Event.scala @@ -5,16 +5,19 @@ import lila.chess._ import Piotr._ sealed trait Event { - def encode: Option[String] } +object Event { + def fromMove(move: Move): List[Event] = MoveEvent(move) :: List( + if (move.enpassant) move.capture map EnpassantEvent.apply else None, + move.promotion map { role ⇒ PromotionEvent(role, move.dest) } + ).flatten +} -trait EventDecoder { - +sealed trait EventDecoder { def decode(str: String): Option[Event] } object EventDecoder { - val all: Map[Char, EventDecoder] = Map( 's' -> StartEvent, 'p' -> PossibleMovesEvent, @@ -32,25 +35,20 @@ object EventDecoder { } case class StartEvent() extends Event { - def encode = Some("s") } object StartEvent extends EventDecoder { - def decode(str: String) = Some(StartEvent()) } case class MoveEvent(orig: Pos, dest: Pos, color: Color) extends Event { - def encode = for { o ← encodePos get orig d ← encodePos get dest } yield "m" + o + d + color.letter } object MoveEvent extends EventDecoder { - def apply(move: Move): MoveEvent = MoveEvent(move.orig, move.dest, move.piece.color) - def decode(str: String) = str.toList match { case List(o, d, c) ⇒ for { orig ← decodePos get o @@ -62,7 +60,6 @@ object MoveEvent extends EventDecoder { } case class PossibleMovesEvent(moves: Map[Pos, List[Pos]]) extends Event { - def encode = Some("p" + ((moves map { case (orig, dests) ⇒ for { o ← encodePos get orig @@ -71,7 +68,6 @@ case class PossibleMovesEvent(moves: Map[Pos, List[Pos]]) extends Event { }).flatten mkString ",")) } object PossibleMovesEvent extends EventDecoder { - def decode(str: String) = Some(PossibleMovesEvent( (str.split(",") map { line ⇒ line.toList match { @@ -87,13 +83,11 @@ object PossibleMovesEvent extends EventDecoder { } case class EnpassantEvent(killed: Pos) extends Event { - def encode = for { k ← encodePos get killed } yield "E" + k } object EnpassantEvent extends EventDecoder { - def decode(str: String) = for { k ← str.headOption killed ← decodePos get k @@ -101,7 +95,6 @@ object EnpassantEvent extends EventDecoder { } case class CastlingEvent(king: (Pos, Pos), rook: (Pos, Pos), color: Color) extends Event { - def encode = for { k1 ← encodePos get king._1 k2 ← encodePos get king._2 @@ -110,7 +103,6 @@ case class CastlingEvent(king: (Pos, Pos), rook: (Pos, Pos), color: Color) exten } yield "c" + k1 + k2 + r1 + r2 + color.letter } object CastlingEvent extends EventDecoder { - def decode(str: String) = str.toList match { case List(k1, k2, r1, r2, c) ⇒ for { king1 ← decodePos get k1 @@ -126,22 +118,18 @@ object CastlingEvent extends EventDecoder { } case class RedirectEvent(url: String) extends Event { - def encode = Some("r" + url) } object RedirectEvent extends EventDecoder { - def decode(str: String) = Some(RedirectEvent(str)) } case class PromotionEvent(role: PromotableRole, pos: Pos) extends Event { - def encode = for { p ← encodePos get pos } yield "P" + role.forsyth + p } object PromotionEvent extends EventDecoder { - def decode(str: String) = str.toList match { case List(r, p) ⇒ for { role ← Role promotable r @@ -152,13 +140,11 @@ object PromotionEvent extends EventDecoder { } case class CheckEvent(pos: Pos) extends Event { - def encode = for { p ← encodePos get pos } yield "C" + p } object CheckEvent extends EventDecoder { - def decode(str: String) = for { p ← str.headOption pos ← decodePos get p @@ -166,11 +152,9 @@ object CheckEvent extends EventDecoder { } case class MessageEvent(author: String, message: String) extends Event { - def encode = Some("M" + author + " " + message.replace("|", "(pipe)")) } object MessageEvent extends EventDecoder { - def decode(str: String) = str.split(' ').toList match { case author :: words ⇒ Some(MessageEvent( author, (words mkString " ").replace("(pipe)", "|") @@ -180,38 +164,30 @@ object MessageEvent extends EventDecoder { } case class EndEvent() extends Event { - def encode = Some("e") } object EndEvent extends EventDecoder { - def decode(str: String) = Some(EndEvent()) } case class ThreefoldEvent() extends Event { - def encode = Some("t") } object ThreefoldEvent extends EventDecoder { - def decode(str: String) = Some(ThreefoldEvent()) } case class ReloadTableEvent() extends Event { - def encode = Some("R") } object ReloadTableEvent extends EventDecoder { - def decode(str: String) = Some(ReloadTableEvent()) } case class MoretimeEvent(color: Color, seconds: Int) extends Event { - def encode = Some("T" + color.letter + seconds) } object MoretimeEvent extends EventDecoder { - def decode(str: String) = for { c ← str.headOption color ← Color(c) diff --git a/system/src/main/scala/model/EventStack.scala b/system/src/main/scala/model/EventStack.scala index 01f1b2bebc..9fb2246ef2 100644 --- a/system/src/main/scala/model/EventStack.scala +++ b/system/src/main/scala/model/EventStack.scala @@ -25,12 +25,6 @@ case class EventStack(events: Seq[(Int, Event)]) { def version: Int = events.lastOption map (_._1) getOrElse 0 - def withMove(move: Move): EventStack = withEvents(MoveEvent(move) :: { - move match { - case _ ⇒ Nil - } - }) - def withEvents(newEvents: List[Event]): EventStack = { def versionEvents(v: Int, events: List[Event]): List[(Int, Event)] = events match { diff --git a/system/src/test/scala/ChessToModelTest.scala b/system/src/test/scala/ChessToModelTest.scala index 9e49429bf2..7931d8bd43 100644 --- a/system/src/test/scala/ChessToModelTest.scala +++ b/system/src/test/scala/ChessToModelTest.scala @@ -13,7 +13,7 @@ class ChessToModelTest extends SystemTest { val dbGame = newDbGame val game = dbGame.toChess "identity" in { - val dbg2 = dbGame update game + val dbg2 = dbGame.update(game, anyMove) "white pieces" in { dbg2 playerByColor "white" map (_.ps) map sortPs must_== { dbGame playerByColor "white" map (_.ps) map sortPs @@ -49,7 +49,7 @@ R QK q H1 -> White.rook )) "identity" in { - val dbg2 = dbGame update game + val dbg2 = dbGame.update(game, anyMove) "white pieces" in { dbg2 playerByColor "white" map (_.ps) map sortPs must_== { dbGame playerByColor "white" map (_.ps) map sortPs @@ -62,7 +62,7 @@ R QK q } } "new pieces positions" in { - val dbg2 = newDbGame update game + val dbg2 = newDbGame.update(game, anyMove) "white pieces" in { dbg2 playerByColor "white" map (_.ps) map sortPs must_== { dbGame playerByColor "white" map (_.ps) map sortPs diff --git a/system/src/test/scala/EventStackTest.scala b/system/src/test/scala/EventStackTest.scala index 451448ed4f..4a9df1d702 100644 --- a/system/src/test/scala/EventStackTest.scala +++ b/system/src/test/scala/EventStackTest.scala @@ -73,24 +73,55 @@ class EventStackTest extends SystemTest { } } "apply move events" in { + def addMoves(eventStack: EventStack, moves: Move*) = moves.foldLeft(eventStack) { + case (stack, move) ⇒ stack withEvents (Event fromMove move) + } "start with no events" in { EventStack().events must beEmpty } - "add a move event" in { - val stack = EventStack() withMove newMove( + "move" in { + addMoves(EventStack(), newMove( piece = White.pawn, orig = D2, dest = D4 - ) - stack.events must_== Seq( + )).events must_== Seq( 1 -> MoveEvent(D2, D4, White) ) } - "add two move events" in { - val stack = EventStack() withMove newMove( - piece = White.pawn, orig = D2, dest = D4 - ) withMove newMove( - piece = Black.pawn, orig = D7, dest = D5 - ) - stack.events must_== Seq( + "capture" in { + addMoves(EventStack(), newMove( + piece = White.pawn, orig = D2, dest = E3, capture = Some(E3) + )).events must_== Seq( + 1 -> MoveEvent(D2, E3, White) + ) + } + "enpassant" in { + addMoves(EventStack(), newMove( + piece = White.pawn, orig = D5, dest = E6, capture = Some(E5), enpassant = true + )).events must_== Seq( + 1 -> MoveEvent(D5, E6, White), + 2 -> EnpassantEvent(E5) + ) + } + "promotion" in { + addMoves(EventStack(), newMove( + piece = White.pawn, orig = D7, dest = D8, promotion = Some(Rook) + )).events must_== Seq( + 1 -> MoveEvent(D7, D8, White), + 2 -> PromotionEvent(Rook, D8) + ) + } + "castling" in { + addMoves(EventStack(), newMove( + piece = White.king, orig = E1, dest = G1, castles = true + )).events must_== Seq( + 1 -> MoveEvent(E1, G1, White), + 2 -> CastlingEvent((E1, G1), (H1, F1), White) + ) + } + "two moves" in { + addMoves(EventStack(), + newMove(piece = White.pawn, orig = D2, dest = D4), + newMove(piece = Black.pawn, orig = D7, dest = D5) + ).events must_== Seq( 1 -> MoveEvent(D2, D4, White), 2 -> MoveEvent(D7, D5, Black) ) diff --git a/system/src/test/scala/Fixtures.scala b/system/src/test/scala/Fixtures.scala index 3b4dafd920..989b43bc10 100644 --- a/system/src/test/scala/Fixtures.scala +++ b/system/src/test/scala/Fixtures.scala @@ -3,6 +3,7 @@ package lila.system import scala.util.Random import lila.chess._ +import Pos._ import model._ import DbGame._ @@ -134,4 +135,6 @@ trait Fixtures { castles = castles, promotion = promotion, enpassant = enpassant) + + val anyMove = newMove(White.pawn, D2, D4) } diff --git a/system/src/test/scala/ServerTest.scala b/system/src/test/scala/ServerTest.scala index f132ba9fe4..2f111f7c66 100644 --- a/system/src/test/scala/ServerTest.scala +++ b/system/src/test/scala/ServerTest.scala @@ -54,24 +54,23 @@ RNBQKBNR val moves = List("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") - def play(game: DbGame) = for(m <- moves) yield move(game, m).get + def play(game: DbGame) = for (m ← moves) yield move(game, m).get "report success" in { val game = insert() sequenceValid(play(game)) must beSuccess } "be persisted" in { + val game = insert() + play(game) + val found = repo game game.id "update turns" in { - val game = insert() - play(game) - repo game game.id must beSome.like { + found must beSome.like { case g ⇒ g.turns must_== 27 } } "update board" in { - val game = insert() - play(game) - repo game game.id must beSome.like { + found must beSome.like { case g ⇒ addNewLines(g.toChess.board.visual) must_== """ kr nr p n ppp @@ -84,6 +83,15 @@ B p p """ } } + "event stacks" in { + val stack = found flatMap (_ playerByColor "white") map (_.eventStack) + "high version number" in { + stack must beSome.like { case s => s.version must be_>(20) } + } + "rotated" in { + stack must beSome.like { case s => s.events.size must_== 16 } + } + } } } } diff --git a/todo b/todo new file mode 100644 index 0000000000..e41e8dbc0c --- /dev/null +++ b/todo @@ -0,0 +1 @@ +ensure I can not play my opponent move