feat: integrate NNUE bot and add Python training pipeline with weight export functionality

This commit is contained in:
2026-04-07 23:33:20 +02:00
parent 6a9ac55b31
commit b25be99dcf
29 changed files with 338 additions and 2538 deletions
Binary file not shown.
@@ -1,4 +1,4 @@
package de.nowchess.bot.bots.nnue
package de.nowchess.bot.bots
import de.nowchess.api.game.GameContext
import de.nowchess.api.move.Move
@@ -2,15 +2,58 @@ package de.nowchess.bot.bots.nnue
import de.nowchess.api.board.{Board, Color, File, PieceType, Rank, Square}
import de.nowchess.api.game.GameContext
import java.nio.ByteBuffer
import java.nio.ByteOrder
class NNUE:
private val l1Weights = NNUEWeights.l1_weights
private val l1Bias = NNUEWeights.l1_bias
private val l2Weights = NNUEWeights.l2_weights
private val l2Bias = NNUEWeights.l2_bias
private val l3Weights = NNUEWeights.l3_weights
private val l3Bias = NNUEWeights.l3_bias
private val (l1Weights, l1Bias, l2Weights, l2Bias, l3Weights, l3Bias) = loadWeights()
private def loadWeights(): (Array[Float], Array[Float], Array[Float], Array[Float], Array[Float], Array[Float]) =
val stream = getClass.getResourceAsStream("/nnue_weights.bin")
if stream == null then
throw RuntimeException("NNUE weights file not found in resources")
try
val bytes = stream.readAllBytes()
val buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN)
// Read and verify magic number
val magic = buffer.getInt()
if magic != 0x4555_4e4e then // "NNUE" in little-endian
throw RuntimeException(s"Invalid magic number: 0x${magic.toHexString}")
// Read version
val version = buffer.getInt()
if version != 1 then
throw RuntimeException(s"Unsupported weight version: $version")
// Read all weight tensors in order
val l1w = readTensor(buffer)
val l1b = readTensor(buffer)
val l2w = readTensor(buffer)
val l2b = readTensor(buffer)
val l3w = readTensor(buffer)
val l3b = readTensor(buffer)
(l1w, l1b, l2w, l2b, l3w, l3b)
finally stream.close()
private def readTensor(buffer: ByteBuffer): Array[Float] =
// Read shape
val shapeLen = buffer.getInt()
val shape = Array.ofDim[Int](shapeLen)
for i <- 0 until shapeLen do
shape(i) = buffer.getInt()
// Calculate total elements
val totalElements = shape.product
// Read float data
val floats = Array.ofDim[Float](totalElements)
for i <- 0 until totalElements do
floats(i) = buffer.getFloat()
floats
// Pre-allocated buffers for inference
private val features = new Array[Float](768)
@@ -19,7 +62,7 @@ class NNUE:
/** Convert a position to 768-dimensional binary feature vector.
* 12 piece types (white pawn to black king) × 64 squares from white's perspective. */
def positionToFeatures(board: Board, sideToMove: Color): Array[Float] =
private def positionToFeatures(board: Board, sideToMove: Color): Array[Float] =
// Zero out features array
java.util.Arrays.fill(features, 0f)
@@ -1,39 +0,0 @@
package de.nowchess.bot.bots.nnue
object NNUEWeights:
// PLACEHOLDER: This file is generated by export_weights.py
// Run: python3 modules/bot/python/run_pipeline.sh to generate actual weights
// Layer 1: Input(768) -> Hidden(256)
val l1_weights = Array(
0f
)
// Shape: [256, 768]
val l1_bias = Array(
0f
)
// Shape: [256]
// Layer 2: Hidden(256) -> Hidden(32)
val l2_weights = Array(
0f
)
// Shape: [32, 256]
val l2_bias = Array(
0f
)
// Shape: [32]
// Layer 3: Hidden(32) -> Output(1)
val l3_weights = Array(
0f
)
// Shape: [1, 32]
val l3_bias = Array(
0f
)
// Shape: [1]