From 260db25803ec55ce99e55782791eabdc190dfed4 Mon Sep 17 00:00:00 2001 From: Janis Eccarius Date: Tue, 23 Jun 2026 23:17:22 +0200 Subject: [PATCH] feat(official-bots): activate opening book in expert bot (native-safe) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Load the Polyglot opening book as a classpath resource and wire it into the expert HybridBot. Previously the bot supported Option[PolyglotBook] but BotController passed None, so the book was never used. PolyglotBook.fromResource reads via getResourceAsStream so the book is embedded in the GraalVM native image instead of read from the filesystem (FileInputStream) — no file needs mounting into the pod. The filesystem apply(path) factory is kept for tests. Moved codekiddy.bin into resources as opening_book.bin. Dropped the per-probe debug println. NCS-43 Co-Authored-By: Claude Opus 4.8 --- .../main/resources/opening_book.bin} | Bin .../scala/de/nowchess/bot/BotController.scala | 5 +- .../de/nowchess/bot/util/PolyglotBook.scala | 79 ++++++++++-------- 3 files changed, 50 insertions(+), 34 deletions(-) rename modules/official-bots/{codekiddy.bin => src/main/resources/opening_book.bin} (100%) diff --git a/modules/official-bots/codekiddy.bin b/modules/official-bots/src/main/resources/opening_book.bin similarity index 100% rename from modules/official-bots/codekiddy.bin rename to modules/official-bots/src/main/resources/opening_book.bin diff --git a/modules/official-bots/src/main/scala/de/nowchess/bot/BotController.scala b/modules/official-bots/src/main/scala/de/nowchess/bot/BotController.scala index e8df5b6..cb9ba36 100644 --- a/modules/official-bots/src/main/scala/de/nowchess/bot/BotController.scala +++ b/modules/official-bots/src/main/scala/de/nowchess/bot/BotController.scala @@ -1,17 +1,20 @@ package de.nowchess.bot import de.nowchess.bot.bots.{ClassicalBot, HybridBot} +import de.nowchess.bot.util.PolyglotBook import jakarta.enterprise.context.ApplicationScoped import org.jboss.logging.Logger object BotController: private val log = Logger.getLogger(classOf[BotController]) + private val openingBook = PolyglotBook.fromResource("/opening_book.bin") + private val bots: Map[String, Bot] = Map( "easy" -> ClassicalBot(BotDifficulty.Easy), "medium" -> ClassicalBot(BotDifficulty.Medium), "hard" -> ClassicalBot(BotDifficulty.Hard), - "expert" -> HybridBot(BotDifficulty.Expert, vetoReporter = log.debug(_)), + "expert" -> HybridBot(BotDifficulty.Expert, vetoReporter = log.debug(_), book = Some(openingBook)), ) def getBot(name: String): Option[Bot] = bots.get(name.toLowerCase) diff --git a/modules/official-bots/src/main/scala/de/nowchess/bot/util/PolyglotBook.scala b/modules/official-bots/src/main/scala/de/nowchess/bot/util/PolyglotBook.scala index 582ff94..9cc530e 100644 --- a/modules/official-bots/src/main/scala/de/nowchess/bot/util/PolyglotBook.scala +++ b/modules/official-bots/src/main/scala/de/nowchess/bot/util/PolyglotBook.scala @@ -4,7 +4,7 @@ import de.nowchess.api.board.* import de.nowchess.api.game.GameContext import de.nowchess.api.move.{Move, MoveType, PromotionPiece} -import java.io.{DataInputStream, FileInputStream} +import java.io.{DataInputStream, FileInputStream, InputStream} import scala.collection.mutable import scala.util.Random @@ -16,24 +16,11 @@ import scala.util.Random * - weight: 2 bytes (Short) — move weight (higher = preferred) * - learn: 4 bytes (Int) — learning data (unused) */ -final class PolyglotBook(path: String): - - private val entries: Map[Long, Vector[BookEntry]] = - try { - val r = loadBookFile(path) - println(s"Book loaded successfully. ${r.size} entries found.") - r - } catch - case e: Exception => - println(s"Error loading book: $e") - // Gracefully fail: return empty map if book cannot be loaded - // This allows the bot to work even if the book file is missing - scala.collection.immutable.Map.empty +final class PolyglotBook private (entries: Map[Long, Vector[BookEntry]]): /** Probe the book for a move in the given position. Returns a weighted random move, or None if not in book. */ def probe(context: GameContext): Option[Move] = val hash = PolyglotHash.hash(context) - println(f"0x$hash%016X") entries.get(hash).flatMap { bookEntries => if bookEntries.isEmpty then None else @@ -41,24 +28,6 @@ final class PolyglotBook(path: String): decodeMove(entry.move, context) } - private def loadBookFile(path: String): Map[Long, Vector[BookEntry]] = - val input = DataInputStream(FileInputStream(path)) - try - val result = mutable.Map[Long, Vector[BookEntry]]() - while input.available() > 0 do - val key = input.readLong() - val move = input.readShort() - val weight = input.readShort() - input.readInt() // learning data (unused) - - val entry = BookEntry(key, move, weight) - result.updateWith(key) { - case Some(entries) => Some(entries :+ entry) - case None => Some(Vector(entry)) - } - result.toMap - finally input.close() - /** Decode a packed Polyglot move short into an Option[Move]. * * Bit layout of the move Short: @@ -134,4 +103,48 @@ final class PolyglotBook(path: String): select(pick, 0) +object PolyglotBook: + + /** Load a book from a filesystem path. Fails gracefully to an empty book. */ + def apply(path: String): PolyglotBook = + safeLoad(s"file $path")(FileInputStream(path)) + + /** Load a book from a classpath resource (native-image safe: the resource is embedded in the binary, so no file must + * be mounted into the pod). + */ + def fromResource(name: String): PolyglotBook = + Option(getClass.getResourceAsStream(name)) match + case Some(stream) => safeLoad(s"resource $name")(stream) + case None => + println(s"Error loading book: resource $name not found on classpath") + new PolyglotBook(Map.empty) + + private def safeLoad(source: String)(stream: => InputStream): PolyglotBook = + try + val entries = parse(stream) + println(s"Book loaded successfully from $source. ${entries.size} entries found.") + new PolyglotBook(entries) + catch + case e: Exception => + println(s"Error loading book from $source: $e") + new PolyglotBook(Map.empty) + + private def parse(stream: InputStream): Map[Long, Vector[BookEntry]] = + val input = DataInputStream(stream) + try + val result = mutable.Map[Long, Vector[BookEntry]]() + while input.available() > 0 do + val key = input.readLong() + val move = input.readShort() + val weight = input.readShort() + input.readInt() // learning data (unused) + + val entry = BookEntry(key, move, weight) + result.updateWith(key) { + case Some(entries) => Some(entries :+ entry) + case None => Some(Vector(entry)) + } + result.toMap + finally input.close() + private case class BookEntry(key: Long, move: Short, weight: Int)