diff --git a/Epd.scala b/Epd.scala new file mode 100644 index 0000000..37372c6 --- /dev/null +++ b/Epd.scala @@ -0,0 +1,80 @@ +package gambit + +final case class EpdPosition( + fen: String, + bestMoves: List[String], + avoidMoves: List[String], + id: String, + operations: Map[String, String] +) + +final case class EpdResult( + position: EpdPosition, + solved: Boolean, + playedSan: String, + depth: Int, + nodes: Long, + timeMs: Long +) + +object Epd: + def parse(line: String): EpdPosition = + val trimmed = line.trim + val fields = trimmed.split("\\s+") + val board = fields.take(4).mkString(" ") + val rest = trimmed.split("\\s+", 5) + val opsText = if rest.length >= 5 then rest(4) else "" + val operations = parseOperations(opsText) + val fen = s"$board ${operations.getOrElse("hmvc", "0")} ${operations.getOrElse("fmvn", "1")}" + EpdPosition( + fen = fen, + bestMoves = tokenizeMoves(operations.getOrElse("bm", "")), + avoidMoves = tokenizeMoves(operations.getOrElse("am", "")), + id = operations.getOrElse("id", "").replace("\"", ""), + operations = operations + ) + + private def tokenizeMoves(operand: String): List[String] = + operand.split("\\s+").filter(_.nonEmpty).toList + + private def parseOperations(text: String): Map[String, String] = + val out = scala.collection.mutable.LinkedHashMap[String, String]() + for chunk <- text.split(";") do + val piece = chunk.trim + if piece.nonEmpty then + val space = piece.indexWhere(_.isWhitespace) + if space < 0 then out(piece) = "" + else out(piece.take(space)) = piece.drop(space).trim + out.toMap + + def solve(position: EpdPosition, timeMs: Long, maxDepth: Int = 64): EpdResult = + val pos = Fen.parse(position.fen) + val good = resolve(pos, position.bestMoves) + val bad = resolve(pos, position.avoidMoves) + val searcher = new Searcher + val result = searcher.search(pos, maxDepth, timeMs) + val best = result.bestMove + val solved = + if position.bestMoves.nonEmpty then good.contains(best) + else if position.avoidMoves.nonEmpty then !bad.contains(best) + else false + val playedSan = if best != 0 then San.toSan(pos, best) else "(none)" + EpdResult(position, solved, playedSan, result.depth, result.nodes, timeMs) + + private def resolve(pos: Position, sans: List[String]): Set[Int] = + sans.map(San.parse(pos, _)).filter(_ != 0).toSet + + def runSuite(lines: Seq[String], timeMs: Long, maxDepth: Int = 64): List[EpdResult] = + lines.filter(_.trim.nonEmpty).map(line => solve(parse(line), timeMs, maxDepth)).toList + + def report(results: List[EpdResult]): String = + val solved = results.count(_.solved) + val sb = new StringBuilder + for r <- results do + val flag = if r.solved then "ok " else "MISS" + val label = if r.position.id.nonEmpty then r.position.id else r.position.fen + val target = if r.position.bestMoves.nonEmpty then s"bm ${r.position.bestMoves.mkString(" ")}" + else s"am ${r.position.avoidMoves.mkString(" ")}" + sb.append(f"[$flag] $label%-12s played ${r.playedSan}%-8s ($target) depth ${r.depth} nodes ${r.nodes}\n") + sb.append(s"solved ${solved}/${results.length}\n") + sb.toString diff --git a/WinAtChess.scala b/WinAtChess.scala new file mode 100644 index 0000000..6419eac --- /dev/null +++ b/WinAtChess.scala @@ -0,0 +1,25 @@ +package gambit + +object WinAtChess: + val positions: List[String] = List( + "2rr3k/pp3pp1/1nnqbN1p/3pN3/2pP4/2P3Q1/PPB4P/R4RK1 w - - bm Qg6; id \"WAC.001\";", + "8/7p/5k2/5p2/p1p2P2/Pr1pPK2/1P1R3P/8 b - - bm Rxb2; id \"WAC.002\";", + "5rk1/1ppb3p/p1pb4/6q1/3P1p1r/2P1R2P/PP1BQ1P1/5RKN w - - bm Rg3; id \"WAC.003\";", + "r1bq2rk/pp3pbp/2p1p1pQ/7P/3P4/2PB1N2/PP3PP1/R3K2R w - - bm Qxh7+; id \"WAC.004\";", + "5k2/6pp/p1qN4/1p1p4/3P4/2PKP2Q/PP3r2/3R4 b - - bm Qc4+; id \"WAC.005\";", + "rnbqkb1r/pppp1ppp/8/4P3/6n1/7P/PPPNPPP1/R1BQKBNR b KQkq - bm Ne3; id \"WAC.007\";", + "3q1rk1/p4pp1/2pb3p/3p4/6Pr/1PNQ4/P1PB1PP1/4RRK1 b - - bm Bh2+; id \"WAC.009\";", + "2br2k1/2q3rn/p2NppQp/2p1P3/Pp5R/4P3/1P3PPP/3R2K1 w - - bm Rxh7; id \"WAC.010\";", + "r1b1kb1r/3q1ppp/pBp1pn2/8/Np3P2/5B2/PPP3PP/R2Q1RK1 w kq - bm Bxc6; id \"WAC.011\";", + "4k1r1/2p3r1/1pR1p3/3pP2p/3P2qP/P4N2/1PQ4P/5R1K b - - bm Qxf3+; id \"WAC.014\";", + "5rk1/pp4p1/2n1p2p/2Npq3/2p5/6P1/P3P1BP/R4Q1K w - - bm Qxf8+; id \"WAC.015\";", + "r2rb1k1/pp1q1p1p/2n1p1p1/2bp4/5P2/PP1BPR1Q/1BPN2PP/R5K1 w - - bm Qxh7+; id \"WAC.016\";", + "r1b2rk1/2q1b1pp/p2ppn2/1p6/3QP3/1BN1B3/PPP3PP/R4RK1 w - - bm Nd5; id \"WAC.019\";", + "6k1/6p1/p7/3Pn3/5p2/1p3P2/1P3qrP/2Q2RRK b - - bm Qxf1+; id \"WAC.032\";", + "2rr2k1/1p4bp/p1q1p1p1/4Pp1n/2PB4/1PN3P1/P3Q2P/2RR2K1 w - - bm Qxh5; id \"WAC.034\";", + "2r5/2rk2pp/1pn1pb2/pN1p4/P2P4/1N2B3/nPR1KPPP/3R4 b - - bm Nxd4+; id \"WAC.036\";", + "1r6/p2p1pk1/2pq2p1/8/8/6RN/6P1/6K1 w - - bm Rxg6+; id \"WAC.RXG\";", + "r1bqk2r/ppp2ppp/2n5/2bp4/8/2N2N2/PPPP1PPP/R1BQ1RK1 b kq - bm d4; id \"WAC.EXTRA1\";", + "8/8/8/8/8/6k1/6p1/6K1 b - - bm g1=Q+; id \"WAC.PROMO\";", + "6k1/5ppp/8/8/8/8/8/R6K w - - bm Ra8; id \"WAC.M1\";" + )