Update to Stockfish 16.1 and Lc0 0.30 (#12)

### Engine Upgrades
* Updated to Stockfish 16.1 (see chesskit-app/Stockfish#3).
* Now requires two `.nnue` files to be bundled with the app, see
CHANGEGLOG.
* Updated to Lc0 0.30 (see chesskit-app/Lc0#5).

### Command Execution Updates
* Replaced `UCI::execute_command` with native `stdin` calls.
* This will allow for much faster updates in the future since the engine
code has not been modified.

### Other Changes
* `Engine.start()` now requires a completion handler to be provided.
  * `completion` is called when engine initialization is complete.

resolves #11
This commit is contained in:
Paolo Di Lorenzo
2024-04-22 14:34:21 -04:00
committed by GitHub
25 changed files with 318 additions and 270 deletions
+15
View File
@@ -0,0 +1,15 @@
name: checks
on:
push:
branches: master
pull_request:
branches: master
jobs:
check:
uses: chesskit-app/workflows/.github/workflows/check-swift-package.yaml@master
secrets: inherit
with:
test_bundle: ChessKitEnginePackageTests
@@ -1,14 +0,0 @@
name: ChessKitEngine Tests
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
test:
uses: chesskit-app/workflows/.github/workflows/test-swift-package.yml@master
secrets: inherit
with:
test_bundle: ChessKitEnginePackageTests
+3
View File
@@ -0,0 +1,3 @@
retain_public: true
targets:
- ChessKitEngine
+25 -2
View File
@@ -1,3 +1,26 @@
# [unreleased]
#### Engine Upgrades
* Update to [*Stockfish 16.1*](https://stockfishchess.org/blog/2024/stockfish-16-1/).
* ⚠️ Stockfish now requires `EvalFile` and `EvalFileSmall` options to be set after launch, with a path to the `*.nnue` files provided.
* Currently `chesskit-engine` assumes [`nn-baff1ede1f90.nnue`](https://tests.stockfishchess.org/nns?network_name=baff1ede1f90&user=), [`nn-b1a57edbea57.nnue`](https://tests.stockfishchess.org/nns?network_name=b1a57edbea57&user=) are available in your app's `Bundle.main`.
* Click the file names in the previous line to access the download pages.
* Any other files can be added via `.setoption(id:value:)` engine commands.
* Update to [*LeelaChessZero 0.30*](https://github.com/LeelaChessZero/lc0/releases/tag/v0.30.0).
* ⚠️ Lc0 requires `WeightsFile` options to be set after launch, with a path to a neural network file provided.
* Currently `chesskit-engine` assumes `192x15_network` is available in your app's `Bundle.main`.
* Network files can be downloaded from [lczero.org](https://lczero.org/play/bestnets/).
* Any other files can be added via `.setoption(id:value:)` engine commands.
* Currently there are some performance issues using `lc0` in an app; this is being investigated but any contributions (via PRs or issues) are appreciated.
#### Improvements
* `Engine.start()` now takes a `completion` handler.
* This is called once the engine has finished initializing.
* Engine commands (i.e. setting options or requesting evaluations) should not be sent until this completion handler is called.
* `EngineMessenger` now sends commands to the engines via `stdin`, see [Issue #11](https://github.com/chesskit-app/chesskit-engine/issues/11).
* This will allow for much simpler upgrades to existing engines, as well as the inclusion of new engines in the future.
* Special thanks [@dehlen](https://github.com/dehlen).
# ChessKitEngine 0.3.0
Released Wednesday, March 27, 2024.
@@ -32,7 +55,7 @@ Released Wednesday, April 26, 2023.
#### New Features
* Add [`LeelaChessZero (lc0)` engine](https://lczero.org)
* Currently comes bundled with a neural network weights file `192x15_network`
#### Improvements
* `Engine` initializer no longer has a default `engineType` (previously `.stockfish`)
* Type must be specified using `Engine(type: <engine type>)`
@@ -63,7 +86,7 @@ Released Friday, April 14, 2023.
Released Friday, April 14, 2023.
* Fix build issue related to missing `ChessKitEngine_Cxx` target
# ChessKitEngine 0.1.0
Released Friday, April 14, 2023.
+2 -4
View File
@@ -40,8 +40,7 @@ let package = Package(
name: "ChessKitEngineTests",
dependencies: ["ChessKitEngine"],
resources: [
.copy("EngineTests/Resources/192x15_network"),
.copy("EngineTests/Resources/nn-1337b1adec5b.nnue")
.copy("EngineTests/Resources/192x15_network")
]
)
],
@@ -70,7 +69,6 @@ package.targets.first { $0.name == "ChessKitEngineCore" }?.exclude = [
"Engines/lc0/subprojects/eigen-3.4.0/test",
"Engines/lc0/subprojects/eigen-3.4.0/unsupported",
"Engines/lc0/third_party",
"Engines/lc0/src/main.cc",
"Engines/lc0/src/utils/filesystem.win32.cc",
"Engines/lc0/src/chess/board_test.cc",
"Engines/lc0/src/chess/position_test.cc",
@@ -85,9 +83,9 @@ package.targets.first { $0.name == "ChessKitEngineCore" }?.exclude = [
"Engines/lc0/src/trainingdata/",
"Engines/lc0/src/neural/cuda/",
"Engines/lc0/src/neural/dx/",
"Engines/lc0/src/neural/metal/",
"Engines/lc0/src/neural/onednn/",
"Engines/lc0/src/neural/onnx/",
"Engines/lc0/src/neural/opencl/",
"Engines/lc0/src/neural/metal/",
"Engines/lc0/src/neural/network_tf_cc.cc"
]
+51 -24
View File
@@ -1,23 +1,31 @@
# ♟️🤖 ChessKitEngine
[![ChessKitEngine Tests](https://github.com/chesskit-app/chesskit-engine/actions/workflows/test-chesskit-engine.yml/badge.svg)](https://github.com/chesskit-app/chesskit-engine/actions/workflows/test-chesskit-engine.yml) [![codecov](https://codecov.io/github/chesskit-app/chesskit-engine/branch/master/graph/badge.svg?token=TDS6QOD25U)](https://codecov.io/gh/chesskit-app/chesskit-engine)
[![checks](https://github.com/chesskit-app/chesskit-engine/actions/workflows/checks.yaml/badge.svg)](https://github.com/chesskit-app/chesskit-engine/actions/workflows/checks.yaml) [![codecov](https://codecov.io/github/chesskit-app/chesskit-engine/branch/master/graph/badge.svg?token=TDS6QOD25U)](https://codecov.io/gh/chesskit-app/chesskit-engine)
A Swift package for the following chess engines:
[<img src="https://stockfishchess.org/images/logo/icon_512x512.png" width="50" />](https://stockfishchess.org) [<img src="https://lczero.org/images/logo.svg" width="50" />](https://lczero.org)
<table>
<tr>
<td valign="center">
<a href="https://stockfishchess.org"><img src="https://stockfishchess.org/images/logo/icon_512x512.png" width="50" /></a>
</td>
<td valign="center">
<a href="https://lczero.org"><img src="https://lczero.org/images/logo.svg" width="50" /></a>
</td>
</tr>
</table>
`ChessKitEngine` implements the [Universal Chess Interface protocol](https://backscattering.de/chess/uci/2006-04.txt) for communication between [chess engines](https://en.wikipedia.org/wiki/Chess_engine) and user interfaces built with Swift.
`chesskit-engine` implements the [Universal Chess Interface protocol](https://backscattering.de/chess/uci/2006-04.txt) for communication between [chess engines](https://en.wikipedia.org/wiki/Chess_engine) and user interfaces built with Swift.
For a related Swift package that manages chess logic, see [chesskit-swift](https://github.com/chesskit-app/chesskit-swift).
## Usage
* Add a package dependency to your Xcode project or Swift Package:
``` swift
.package(url: "https://github.com/chesskit-app/chesskit-engine", from: "0.2.0")
```
1. Add `chesskit-engine` as a dependency
* In an [app built in Xcode](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app),
* or [as a dependency to another Swift Package](https://www.swift.org/documentation/package-manager/#importing-dependencies).
* Next you can import `ChessKitEngine` to use it in your Swift code:
2. Next, import `ChessKitEngine` to use it in Swift code:
``` swift
import ChessKitEngine
@@ -25,6 +33,8 @@ import ChessKitEngine
```
⚠️ Be sure to check the [Neural Networks](https://github.com/chesskit-app/chesskit-engine/tree/master?tab=readme-ov-file#neural-networks) section below for important setup details.
## Features
* Initialize an engine and set response handler
@@ -32,13 +42,15 @@ import ChessKitEngine
// create Stockfish engine
let engine = Engine(type: .stockfish)
// set response handler
// set response handler, called when engine issues responses
engine.receiveResponse = { response in
print(response)
}
// start listening for engine responses
engine.start()
engine.start {
// engine is ready to go!
}
```
* Send [UCI protocol](https://backscattering.de/chess/uci/2006-04.txt) commands
@@ -95,26 +107,41 @@ engine.loggingEnabled = true
// verbose while analyzing positions and returning evaluations.
```
* Enable engine neural networks
* Copy the relevant file in the `Resources` directory of this repo to your app's bundle, then use the engine-specific commands to provide them to the engine (where `fileURL` is a `String` of the URL of the file).
* These must be called in the order shown.
* For `Stockfish 15.1` (`nn-1337b1adec5b.nnue`):
``` swift
engine.send(command: .setoption(id: "EvalFile", value: fileURL))
engine.send(command: .setoption(id: "Use NNUE", value: "true"))
```
* For `LeelaChessZero 0.29` (`192x15_network`):
``` swift
engine.send(command: .setoption(id: "WeightsFile", value: fileURL))
```
## Neural Networks
Both `Stockfish 16.1` and `LeelaChessZero 0.30` require neural network files to be provided to the engine for computation.
In order to keep the package size small and allow for the greatest level of flexibility, these neural network files are **not** bundled with the package. Therefore they must be added to the app (either in the bundle or manually by a user) and then provided to the engine at runtime.
They can be provided to the engine using the `.setoption(id:value:)` UCI commands included in `chesskit-engine`.
For example:
``` swift
// Stockfish
engine.send(command: .setoption(id: "EvalFile", value: fileURL))
engine.send(command: .setoption(id: "EvalFileSmall", value: smallFileURL))
// Lc0
engine.send(command: .setoption(id: "WeightsFile", value: fileURL))
```
The following details the recommended files for each engine and where to obtain them.
#### Stockfish
* `"EvalFile"`: `nn-b1a57edbea57.nnue` ([download here](https://tests.stockfishchess.org/nns?network_name=b1a57edbea57&user=))
* `"EvalFileSmall"`: `nn-baff1ede1f90.nnue` ([download here](https://tests.stockfishchess.org/nns?network_name=baff1ede1f90&user=))
* Other files from https://tests.stockfishchess.org can be used if desired.
#### LeelaChessZero
⚠️ There are currently some performance issues with lc0 in `chesskit-engine` ([PR's are welcome!](https://github.com/chesskit-app/chesskit-engine/compare)).
* `"WeightsFile"`: `192x15_network` ([download here](https://github.com/chesskit-app/chesskit-engine/tree/0f11891b3c053e12d04c2e9c9d294c4404b006c3/Tests/ChessKitEngineTests/EngineTests/Resources))
* Other files can be obtained [here](https://lczero.org/play/bestnets/) or [here](https://training.lczero.org/networks/).
## Supported Engines
The following engines are currently supported:
| | Engine | Version | License | Options Reference |
| :---: | --- | :---: | :---: | :---: |
| <img src="https://stockfishchess.org/images/logo/icon_512x512.png" width="25" /> | [Stockfish](https://stockfishchess.org) | [15.1](https://github.com/official-stockfish/Stockfish/tree/sf_15.1) | [GPL v3](https://github.com/official-stockfish/Stockfish/blob/sf_15.1/Copying.txt) | [🔗](https://github.com/official-stockfish/Stockfish/tree/sf_15.1#the-uci-protocol-and-available-options)
| <img src="https://lczero.org/images/logo.svg" width="25" /> | [lc0](https://lczero.org) | [0.29](https://github.com/LeelaChessZero/lc0/tree/v0.29.0) | [GPL v3](https://github.com/LeelaChessZero/lc0/blob/v0.29.0/COPYING) | [🔗](https://github.com/LeelaChessZero/lc0/wiki/Lc0-options)
| <img src="https://stockfishchess.org/images/logo/icon_512x512.png" width="25" /> | [Stockfish](https://stockfishchess.org) | [16.1](https://github.com/official-stockfish/Stockfish/tree/sf_16.1) | [GPL v3](https://github.com/official-stockfish/Stockfish/blob/sf_16.1/Copying.txt) | [🔗](https://github.com/official-stockfish/Stockfish/wiki/UCI-&-Commands#setoption)
| <img src="https://lczero.org/images/logo.svg" width="25" /> | [lc0](https://lczero.org) | [0.30](https://github.com/LeelaChessZero/lc0/tree/v0.30.0) | [GPL v3](https://github.com/LeelaChessZero/lc0/blob/v0.30.0/COPYING) | [🔗](https://github.com/LeelaChessZero/lc0/wiki/Lc0-options)
## Author
Binary file not shown.
Binary file not shown.
+81 -52
View File
@@ -6,20 +6,20 @@
import ChessKitEngineCore
public class Engine {
/// The type of the engine.
private let type: EngineType
/// Messenger used to communicate with engine.
private let messenger: EngineMessenger
/// Whether logging should be enabled.
///
/// If set to `true`, engine commands and responses
/// will be logged to the console. The default value is
/// `false`.
public var loggingEnabled = false
/// Whether the engine is currently running.
///
/// - To start the engine, call `start()`.
@@ -39,57 +39,69 @@ public class Engine {
///
public init(type: EngineType) {
self.type = type
messenger = EngineMessenger(engineType: type.objc)
messenger.responseHandler = { [weak self] response in
guard let self else { return }
DispatchQueue.main.async {
if let parsedResponse = EngineResponse(rawValue: response) {
self.log(parsedResponse.rawValue)
self.receiveResponse(parsedResponse)
} else if !response.isEmpty {
self.log(response)
}
}
}
}
deinit {
stop()
}
/// Starts the engine.
///
/// - parameter coreCount: The number of processor cores to use for engine
/// calculation. The default value is `nil` which uses the number of
/// cores available on the device.
/// - parameter multipv: The number of lines the engine should return,
/// sent via the `"MultiPV"` UCI option.
/// - parameter completion: The completion handler that is called when
/// the engine setup is complete. You must wait for this to be called
/// before sending further commands to the engine.
///
/// This must be called before sending any commands
/// with `send(command:)`.
public func start(coreCount: Int? = nil, multipv: Int = 1) {
public func start(coreCount: Int? = nil, multipv: Int = 1, completion: @escaping () -> Void) {
messenger.responseHandler = { [weak self] response in
guard let self else { return }
DispatchQueue.main.async {
guard let parsed = EngineResponse(rawValue: response) else {
if !response.isEmpty {
self.log(response)
}
return
}
self.log(parsed.rawValue)
guard self.isRunning else {
// engine setup loop
// <uci> <uciok> <isready> <readok> complete
switch parsed {
case .uciok:
self.send(command: .isready)
case .readyok:
self.isRunning = true
self.performInitialSetup(
coreCount: coreCount ?? ProcessInfo.processInfo.processorCount,
multipv: multipv
)
completion()
default:
break
}
return
}
self.receiveResponse(parsed)
}
}
messenger.start()
isRunning = true
// set UCI mode
// start engine setup loop
send(command: .uci)
// configure engine-specific options
type.setupCommands.forEach(send)
// configure common engine options
send(command: .setoption(
id: "Threads",
value: "\(max((coreCount ?? ProcessInfo.processInfo.processorCount) - 1, 1))"
))
send(command: .setoption(id: "MultiPV", value: "\(multipv)"))
// start analyzing
send(command: .isready)
send(command: .ucinewgame)
}
/// Stops the engine.
///
/// Call this to stop all engine calculation and clean up.
@@ -97,14 +109,15 @@ public class Engine {
/// sending any more commands with `send(command:)`.
public func stop() {
guard isRunning else { return }
send(command: .stop)
send(command: .quit)
messenger.stop()
isRunning = false
initialSetupComplete = false
}
/// Sends a command to the engine.
///
/// - parameter command: The command to send.
@@ -113,17 +126,17 @@ public class Engine {
/// validity. While the engine is processing commands or
/// thinking, any responses will be returned via `receiveResponse`.
public func send(command: EngineCommand) {
guard isRunning else {
guard isRunning || [.uci, .isready].contains(command) else {
log("Engine is not running, call start() first.")
return
}
queue.sync {
self.log(command.rawValue)
self.messenger.sendCommand(command.rawValue)
log(command.rawValue)
messenger.sendCommand(command.rawValue)
}
}
/// Closure that is called when engine responses are received.
///
/// - parameter response: The response received from the engine.
@@ -133,17 +146,33 @@ public class Engine {
public var receiveResponse: (_ response: EngineResponse) -> Void = {
_ in
}
}
// MARK: - Private
// MARK: - Private
extension Engine {
/// Logs `message` if `loggingEnabled` is `true`.
private func log(_ message: String) {
if loggingEnabled {
Logging.print(message)
}
}
private var initialSetupComplete = false
/// Sets initial engine options.
private func performInitialSetup(coreCount: Int, multipv: Int) {
guard !initialSetupComplete else { return }
// configure engine-specific options
type.setupCommands.forEach(send)
// configure common engine options
send(command: .setoption(
id: "Threads",
value: "\(max(coreCount - 1, 1))"
))
send(command: .setoption(id: "MultiPV", value: "\(multipv)"))
initialSetupComplete = true
}
}
@@ -6,7 +6,7 @@
/// Possible engine commands based on the
/// [Universal Chess Interface (UCI)](https://backscattering.de/chess/uci/2006-04.txt).
///
public enum EngineCommand {
public enum EngineCommand: Equatable {
/// `"debug [ on | off ]"`
///
@@ -7,37 +7,37 @@
/// [Universal Chess Interface (UCI)](https://backscattering.de/chess/uci/2006-04.txt).
///
public enum EngineResponse {
/// `"id name <x>"`, `"id author <x>"`
///
/// See [UCI protocol documentation](https://backscattering.de/chess/uci/2006-04.txt)
/// for more information.
case id(ID)
/// `"uciok"`
///
/// See [UCI protocol documentation](https://backscattering.de/chess/uci/2006-04.txt)
/// for more information.
case uciok
/// `"readyok"`
///
/// See [UCI protocol documentation](https://backscattering.de/chess/uci/2006-04.txt)
/// for more information.
case readyok
/// `"bestmove <move1> [ ponder <move2> ]"`
///
/// See [UCI protocol documentation](https://backscattering.de/chess/uci/2006-04.txt)
/// for more information.
case bestmove(move: String, ponder: String?)
/// `"info"`
///
/// See [UCI protocol documentation](https://backscattering.de/chess/uci/2006-04.txt)
/// for more information.
case info(Info)
}
extension EngineResponse: Equatable {}
@@ -45,35 +45,35 @@ extension EngineResponse: Equatable {}
extension EngineResponse: RawRepresentable {
public init?(rawValue: String) {
let parsed = EngineResponseParser.parse(response: rawValue)
guard let parsed else {
return nil
}
self = parsed
}
public var rawValue: String {
switch self {
case let .id(id):
switch id {
case let .name(name):
return "<id> <name> \(name)"
"<id> <name> \(name)"
case let .author(author):
return "<id> <author> \(author)"
"<id> <author> \(author)"
}
case .uciok:
return "<uciok>"
"<uciok>"
case .readyok:
return "<readyok>"
"<readyok>"
case let .bestmove(move, ponder):
if let ponder {
return "<bestmove> \(move) <ponder> \(ponder)"
"<bestmove> \(move) <ponder> \(ponder)"
} else {
return "<bestmove> \(move)"
"<bestmove> \(move)"
}
case let .info(info):
return "<info>\(info)"
"<info>\(info)"
}
}
}
@@ -6,7 +6,7 @@
// MARK: - Info
public extension EngineResponse {
struct Info {
public var depth: Int?
public var seldepth: Int?
@@ -25,7 +25,7 @@ public extension EngineResponse {
public var string: String?
public var refutation: [String]?
public var currline: CurrLine?
/// Possible arguments for the `<info>` command.
enum Argument: String, CaseIterable {
case depth
@@ -45,7 +45,7 @@ public extension EngineResponse {
case string
case refutation
case currline
enum ArgType {
/// Single token follows the argument.
case single
@@ -59,7 +59,7 @@ public extension EngineResponse {
/// The argument is of type `(Int, [String])`
case currentLine
}
var type: ArgType {
switch self {
case .depth: return .single
@@ -82,18 +82,13 @@ public extension EngineResponse {
}
}
}
subscript(arg: Argument) -> Any? {
get { self[arg.rawValue] }
set { self[arg.rawValue] = newValue }
}
/// Allows subscript access to `Info` properties
/// to allow for easier `String` parsing.
subscript(arg: String) -> Any? {
get {
guard let arg = Argument(rawValue: arg) else { return nil }
switch arg {
case .depth: return depth
case .seldepth: return seldepth
@@ -116,7 +111,7 @@ public extension EngineResponse {
}
set {
guard let arg = Argument(rawValue: arg) else { return }
switch arg {
case .depth: depth = newValue as? Int
case .seldepth: seldepth = newValue as? Int
@@ -139,7 +134,7 @@ public extension EngineResponse {
}
}
}
}
extension EngineResponse.Info: Equatable {}
@@ -147,75 +142,75 @@ extension EngineResponse.Info: Equatable {}
extension EngineResponse.Info: CustomStringConvertible {
public var description: String {
var result = ""
if let depth = depth {
result += " <depth> \(depth)"
}
if let seldepth = seldepth {
result += " <seldepth> \(seldepth)"
}
if let time = time {
result += " <time> \(time)"
}
if let nodes = nodes {
result += " <nodes> \(nodes)"
}
if let pv = pv, !pv.isEmpty {
result += " <pv> \(pv.joined(separator: " "))"
}
if let multipv = multipv {
result += " <multipv> \(multipv)"
}
if let score = score {
result += " <score>\(score)"
}
if let currmove = currmove {
result += " <currmove> \(currmove)"
}
if let currmovenumber = currmovenumber {
result += " <currmovenumber> \(currmovenumber)"
}
if let hashfull = hashfull {
result += " <hashfull> \(hashfull)"
}
if let nps = nps {
result += " <nps> \(nps)"
}
if let tbhits = tbhits {
result += " <tbhits> \(tbhits)"
}
if let sbhits = sbhits {
result += " <sbhits> \(sbhits)"
}
if let cpuload = cpuload {
result += " <cpuload> \(cpuload)"
}
if let string = string {
result += " <string> \(string)"
}
if let refutation = refutation {
result += " <refutation> \(refutation.joined(separator: " "))"
}
if let currline = currline {
result += " <currline>\(currline)"
}
return result
}
}
@@ -234,7 +229,7 @@ public extension EngineResponse.Info {
public var lowerbound: Bool?
/// The score is just an upper bound.
public var upperbound: Bool?
/// Allows subscript access to `Score` properties
/// to allow for easier `String` parsing.
subscript(member: String) -> Any? {
@@ -258,29 +253,29 @@ public extension EngineResponse.Info {
}
}
}
}
extension EngineResponse.Info.Score: CustomStringConvertible {
public var description: String {
var result = ""
if let cp = cp {
result += " <cp> \(cp)"
}
if let mate = mate {
result += " <mate> \(mate)"
}
if let lowerbound = lowerbound, lowerbound {
result += " <lowerbound>"
}
if let upperbound = upperbound, upperbound {
result += " <upperbound>"
}
return result
}
}
@@ -299,15 +294,15 @@ extension EngineResponse.Info {
extension EngineResponse.Info.CurrLine: CustomStringConvertible {
public var description: String {
var result = ""
if let cpunr = cpunr {
result += " \(cpunr)"
}
if !moves.isEmpty {
result += " \(moves.joined(separator: " "))"
}
return result
}
}
+27 -17
View File
@@ -7,10 +7,10 @@ import ChessKitEngineCore
/// Possible engines available in `ChessKitEngine`.
public enum EngineType: Int {
case stockfish
case lc0
/// Internal mapping from Swift to Obj-C type.
var objc: EngineType_objc {
switch self {
@@ -18,7 +18,7 @@ public enum EngineType: Int {
case .lc0: return .lc0
}
}
/// The user-readable name of the engine.
public var name: String {
switch self {
@@ -26,46 +26,56 @@ public enum EngineType: Int {
case .lc0: return "LeelaChessZero (Lc0)"
}
}
/// The current version of the given engine.
public var version: String {
switch self {
case .stockfish: return "15.1"
case .lc0: return "0.29"
case .stockfish: return "16.1"
case .lc0: return "0.30"
}
}
/// Engine-specific options to configure at initialization.
var setupCommands: [EngineCommand] {
switch self {
case .stockfish:
[
.setoption(id: "Use NNUE", value: "false"),
.setoption(id: "UCI_AnalyseMode", value: "true")
]
let fileOptions = [
"EvalFile": "nn-b1a57edbea57",
"EvalFileSmall": "nn-baff1ede1f90"
].compactMapValues {
Bundle.main.url(forResource: $0, withExtension: "nnue")?.path()
}
return fileOptions.map(EngineCommand.setoption)
case .lc0:
[]
let fileOptions = [
"WeightsFile": "192x15_network"
].compactMapValues {
Bundle.main.url(forResource: $0, withExtension: nil)?.path()
}
return fileOptions.map(EngineCommand.setoption)
}
}
}
// MARK: - CaseIterable
extension EngineType: CaseIterable {
}
// MARK: - Equatable
extension EngineType: Equatable {
}
// MARK: - Identifiable
extension EngineType: Identifiable {
public var id: Self { self }
}
@@ -8,9 +8,12 @@
@implementation EngineMessenger : NSObject
dispatch_queue_t _queue;
Engine *_engine;
NSPipe *_pipe;
NSPipe *_readPipe;
NSPipe *_writePipe;
NSFileHandle *_pipeReadHandle;
NSFileHandle *_pipeWriteHandle;
/// Initializes a new `EngineMessenger` with default engine `Stockfish`.
- (id)init {
@@ -19,7 +22,7 @@ NSFileHandle *_pipeReadHandle;
- (id)initWithEngineType: (EngineType_objc) type {
self = [super init];
if (self) {
switch (type) {
case EngineTypeStockfish:
@@ -29,10 +32,8 @@ NSFileHandle *_pipeReadHandle;
_engine = new Lc0Engine();
break;
}
_engine->initialize();
}
return self;
}
@@ -42,42 +43,62 @@ NSFileHandle *_pipeReadHandle;
}
- (void)start {
_pipe = [NSPipe pipe];
_pipeReadHandle = [_pipe fileHandleForReading];
dup2([[_pipe fileHandleForWriting] fileDescriptor], fileno(stdout));
// set up read pipe
_readPipe = [NSPipe pipe];
_pipeReadHandle = [_readPipe fileHandleForReading];
dup2([[_readPipe fileHandleForWriting] fileDescriptor], fileno(stdout));
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(readStdout:)
name:NSFileHandleReadCompletionNotification
object:_pipeReadHandle
];
[_pipeReadHandle readInBackgroundAndNotify];
// set up write pipe
_writePipe = [NSPipe pipe];
_pipeWriteHandle = [_writePipe fileHandleForWriting];
dup2([[_writePipe fileHandleForReading] fileDescriptor], fileno(stdin));
// create command dispatch queue and start engine
_queue = dispatch_queue_create("ck-message-queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(_queue, ^{
_engine->initialize();
});
}
- (void)stop {
[_pipeReadHandle closeFile];
_pipe = NULL;
[_pipeWriteHandle closeFile];
_readPipe = NULL;
_pipeReadHandle = NULL;
_writePipe = NULL;
_pipeWriteHandle = NULL;
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)sendCommand: (NSString*) command {
_engine->send_command(std::string([command UTF8String]));
dispatch_sync(_queue, ^{
const char *cmd = [[command stringByAppendingString:@"\n"] UTF8String];
write([_pipeWriteHandle fileDescriptor], cmd, strlen(cmd));
});
}
# pragma mark Private
- (void)readStdout: (NSNotification*) notification {
[_pipeReadHandle readInBackgroundAndNotify];
NSData *data = [[notification userInfo] objectForKey:NSFileHandleNotificationDataItem];
NSArray<NSString *> *output = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] componentsSeparatedByString:@"\n"];
[output enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[self responseHandler](obj);
}];
@@ -27,14 +27,6 @@ public:
/// Any required deinitialization and cleanup should
/// be performed here
virtual void deinitialize() {};
/// Sends a command to the engine.
/// - parameter cmd: The UCI command to send to the engine.
/// See https://backscattering.de/chess/uci/2006-04.txt
/// for valid commands.
///
/// The output from the engine will appear in `stdout`.
virtual void send_command(const std::string &cmd) {};
};
#endif /* engine_h */
@@ -3,31 +3,15 @@
// ChessKitEngine
//
#import "lc0+engine.h"
#include "lc0+engine.h"
#import "../lc0/src/chess/board.h"
#import "../lc0/src/engine.h"
using namespace lczero;
EngineLoop loop;
#include "../lc0/src/_main.h"
void Lc0Engine::initialize() {
InitializeMagicBitboards();
loop.Initialize();
loop.RunLoop();
const char* argv[] = { "uci" };
_main(sizeof(argv) / sizeof(argv[0]), argv);
}
void Lc0Engine::deinitialize() {
}
void Lc0Engine::send_command(const std::string &cmd) {
auto command = loop.ParseCommand(cmd);
try {
loop.DispatchCommand(command.first, command.second);
} catch(std::exception& e) {
// ignore unsupported commands
}
}
@@ -6,15 +6,14 @@
#ifndef lc0_engine_h
#define lc0_engine_h
#import "engine.h"
#import <string>
#include "engine.h"
#include <string>
/// LeelaChessZero (Lc0) implementation of `Engine`.
class Lc0Engine: public Engine {
public:
void initialize();
void deinitialize();
void send_command(const std::string &cmd);
};
#endif /* lc0_engine_h */
@@ -3,38 +3,18 @@
// ChessKitEngine
//
#import "stockfish+engine.h"
#import "../Stockfish/src/bitboard.h"
#import "../Stockfish/src/endgame.h"
#import "../Stockfish/src/evaluate.h"
#import "../Stockfish/src/position.h"
#import "../Stockfish/src/psqt.h"
#import "../Stockfish/src/search.h"
#import "../Stockfish/src/thread.h"
#import "../Stockfish/src/uci.h"
#import "../Stockfish/src/types.h"
#include "stockfish+engine.h"
#include "../Stockfish/src/_main.h"
#include "../Stockfish/src/thread.h"
using namespace Stockfish;
void StockfishEngine::initialize() {
UCI::init(Options);
Tune::init();
PSQT::init();
Bitboards::init();
Position::init();
Bitbases::init();
Endgames::init();
Threads.set(size_t(Stockfish::Options["Threads"]));
Search::clear(); // After threads are up
Eval::NNUE::init();
char empty[] = "";
char* argv[] = { empty };
_main(1, argv);
}
void StockfishEngine::deinitialize() {
Threads.clear();
Threads.end();
}
void StockfishEngine::send_command(const std::string &cmd) {
UCI::execute_command(cmd);
ThreadPool().end();
}
@@ -6,15 +6,14 @@
#ifndef stockfish_engine_h
#define stockfish_engine_h
#import "engine.h"
#import <string>
#include "engine.h"
#include <string>
/// Stockfish implementation of `Engine`.
class StockfishEngine: public Engine {
public:
void initialize();
void deinitialize();
void send_command(const std::string &cmd);
};
#endif /* stockfish_engine_h */
@@ -46,7 +46,6 @@ class BaseEngineTests: XCTestCase {
super.setUp()
engine = Engine(type: engineType)
engine.start()
}
override func tearDown() {
@@ -57,16 +56,17 @@ class BaseEngineTests: XCTestCase {
func testEngineSetup() {
let expectation = XCTestExpectation()
engine.start { [self] in
engine.send(command: .isready)
}
engine.receiveResponse = {
if $0 == .uciok || $0 == .readyok {
if $0 == .readyok {
expectation.fulfill()
}
}
engine.send(command: .uci)
engine.send(command: .isready)
wait(for: [expectation], timeout: 5)
}
@@ -11,12 +11,6 @@ final class Lc0Tests: BaseEngineTests {
override func setUp() {
engineType = .lc0
super.setUp()
let weightsFileURL = Bundle.module
.path(forResource: "192x15_network", ofType: nil)!
.replacingOccurrences(of: "file://", with: "")
engine.send(command: .setoption(id: "WeightsFile", value: weightsFileURL))
}
}
@@ -11,13 +11,6 @@ final class StockfishTests: BaseEngineTests {
override func setUp() {
engineType = .stockfish
super.setUp()
let evalFileURL = Bundle.module
.path(forResource: "nn-1337b1adec5b", ofType: "nnue")!
.replacingOccurrences(of: "file://", with: "")
engine.send(command: .setoption(id: "Eval File", value: evalFileURL))
engine.send(command: .setoption(id: "Use NNUE", value: "true"))
}
}