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
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)
@@ -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)