feat(official-bots): activate opening book in expert bot (native-safe)
Build & Test (NowChessSystems) TeamCity build finished

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 <noreply@anthropic.com>
This commit is contained in:
Janis Eccarius
2026-06-23 23:17:22 +02:00
parent 80e1cc258b
commit 260db25803
3 changed files with 50 additions and 34 deletions
@@ -1,17 +1,20 @@
package de.nowchess.bot package de.nowchess.bot
import de.nowchess.bot.bots.{ClassicalBot, HybridBot} import de.nowchess.bot.bots.{ClassicalBot, HybridBot}
import de.nowchess.bot.util.PolyglotBook
import jakarta.enterprise.context.ApplicationScoped import jakarta.enterprise.context.ApplicationScoped
import org.jboss.logging.Logger import org.jboss.logging.Logger
object BotController: object BotController:
private val log = Logger.getLogger(classOf[BotController]) private val log = Logger.getLogger(classOf[BotController])
private val openingBook = PolyglotBook.fromResource("/opening_book.bin")
private val bots: Map[String, Bot] = Map( private val bots: Map[String, Bot] = Map(
"easy" -> ClassicalBot(BotDifficulty.Easy), "easy" -> ClassicalBot(BotDifficulty.Easy),
"medium" -> ClassicalBot(BotDifficulty.Medium), "medium" -> ClassicalBot(BotDifficulty.Medium),
"hard" -> ClassicalBot(BotDifficulty.Hard), "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) def getBot(name: String): Option[Bot] = bots.get(name.toLowerCase)
@@ -4,7 +4,7 @@ import de.nowchess.api.board.*
import de.nowchess.api.game.GameContext import de.nowchess.api.game.GameContext
import de.nowchess.api.move.{Move, MoveType, PromotionPiece} 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.collection.mutable
import scala.util.Random import scala.util.Random
@@ -16,24 +16,11 @@ import scala.util.Random
* - weight: 2 bytes (Short) — move weight (higher = preferred) * - weight: 2 bytes (Short) — move weight (higher = preferred)
* - learn: 4 bytes (Int) — learning data (unused) * - learn: 4 bytes (Int) — learning data (unused)
*/ */
final class PolyglotBook(path: String): final class PolyglotBook private (entries: Map[Long, Vector[BookEntry]]):
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
/** Probe the book for a move in the given position. Returns a weighted random move, or None if not in book. */ /** 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] = def probe(context: GameContext): Option[Move] =
val hash = PolyglotHash.hash(context) val hash = PolyglotHash.hash(context)
println(f"0x$hash%016X")
entries.get(hash).flatMap { bookEntries => entries.get(hash).flatMap { bookEntries =>
if bookEntries.isEmpty then None if bookEntries.isEmpty then None
else else
@@ -41,24 +28,6 @@ final class PolyglotBook(path: String):
decodeMove(entry.move, context) 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]. /** Decode a packed Polyglot move short into an Option[Move].
* *
* Bit layout of the move Short: * Bit layout of the move Short:
@@ -134,4 +103,48 @@ final class PolyglotBook(path: String):
select(pick, 0) 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) private case class BookEntry(key: Long, move: Short, weight: Int)