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)