feat(official-bots): activate opening book in expert bot (native-safe)
Build & Test (NowChessSystems) TeamCity build finished
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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user