Skip to content

Getting Started

Provision the NNUE network, create the engine, and exchange UCI.

1. Provision the NNUE network

The NNUE network (v54-5478683c.nnue) is not embedded in the engine binary — it is loaded from disk at runtime. The engine returns nil on initialization if the file is missing or unreadable, so always ensure the net is present before creating the engine.

RecklessNetworkLoader.ensure(in:) is idempotent: a valid, present net is never re-downloaded (it verifies the complete SHA-256 on Apple, Linux, and Android, then returns immediately). Only a missing or invalid file triggers a download.

import Foundation
import SwiftReckless

let support = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
let netDir = support.appendingPathComponent("reckless-nets")

try await RecklessNetworkLoader().ensure(in: netDir) { progress in
    if let fraction = progress.fractionCompleted {
        print("Downloading net: \(Int(fraction * 100))%")
    } else {
        print("Downloading net: \(progress.bytesDownloaded) bytes")
    }
}

The progress closure is called only when a download is in progress. On a warm launch where the net is already present and valid, ensure returns immediately without calling the closure.

Progress fires once — not a live counter

The loader uses URLSession.downloadTask(with:completionHandler:), which reports progress inside the completion handler. The closure is called once, at download completion, with bytesDownloaded == totalBytes (or 0 when the server omitted Content-Length). It always prints a terminal 100%; it does not count up incrementally. Add a URLSessionDownloadDelegate if you need live streaming progress.

2. Create the engine

RecklessEngine.init(networkDirectory:) is failable — it returns nil if the NNUE net is absent from networkDirectory or if the Rust FFI layer could not start the engine thread.

guard let engine = RecklessEngine(networkDirectory: netDir) else {
    fatalError("engine failed to start — net missing or Rust FFI error")
}

One live engine per process

The Rust engine owns process-global state, so SwiftReckless rejects an overlapping engine with a nil initializer result. Pinned fork tag swiftreckless-v0.9.1 makes initialization and teardown restart-safe: after a clean shutdown(), a later engine lifetime may start normally.

3. Read output and send commands

engine.cancellationSafeOutput is a cancellation-safe channel of UCI lines, delivered in order, with no trailing newline. Breaking out of one for await loop leaves the channel reusable for later reads — which is exactly what the sequential steps below rely on. Output is buffered, so it is safe to send a command first and start reading afterwards.

engine.uci()   // sends "uci"; engine replies with id/option lines then "uciok"

for await line in engine.cancellationSafeOutput {
    print("engine>", line)
    if line == "uciok"   { engine.isReady() }
    if line == "readyok" { break }   // engine is ready; proceed below
}

output is single-consumer — pick one surface per engine

engine.output (an AsyncStream<String>) is a compatibility shim for the shared UCIEngine protocol. It is single-consumer and non-restartable: breaking out of its loop ends it permanently, and even a first access starts a forwarding consumer that competes with cancellationSafeOutput for lines. Use cancellationSafeOutput for stop-and-resume reads like the ones on this page, and never consume both surfaces on one engine.

Once readyok is received, set a position and start a search. The engine emits info lines with search progress, then a final bestmove line.

engine.setPosition(fen: "startpos")
engine.go(depth: 20)

for await line in engine.cancellationSafeOutput {
    if line.hasPrefix("bestmove ") {
        let move = line.split(separator: " ").dropFirst().first.map(String.init)
        print("Best move:", move ?? "none")
        break
    }
}

5. Tear down

engine.shutdown() // sends "quit", joins the Rust thread, frees state, finishes output
// deinit also calls shutdown() if explicit teardown was omitted.

Full minimal example

import Foundation
import SwiftReckless

@main struct MinimalReckless {
    static func main() async throws {
        let support = FileManager.default.urls(for: .applicationSupportDirectory,
                                               in: .userDomainMask)[0]
        let netDir = support.appendingPathComponent("reckless-nets")

        // 1. Ensure the NNUE net is present.
        try await RecklessNetworkLoader().ensure(in: netDir)

        // 2. Create the engine.
        guard let engine = RecklessEngine(networkDirectory: netDir) else {
            fatalError("engine init failed")
        }

        // 3. Handshake then search.
        engine.uci()
        for await line in engine.cancellationSafeOutput {
            if line == "uciok"   { engine.isReady() }
            if line == "readyok" {
                engine.setPosition(fen: "startpos")
                engine.go(depth: 18)
            }
            if line.hasPrefix("bestmove ") {
                print(line)
                engine.shutdown()
                break
            }
        }
    }
}

API summary

public final class RecklessEngine: @unchecked Sendable {
    public init?(networkDirectory: URL)
    /// Cancellation-safe, process-lifetime UCI output. Prefer this surface for
    /// any consumer that stops and restarts reads.
    public var cancellationSafeOutput: RecklessOutput { get }
    /// Compatibility shim for the shared `UCIEngine` protocol; single-consumer
    /// and non-restartable. Mutually exclusive with `cancellationSafeOutput` —
    /// pick one surface per engine.
    public var output: AsyncStream<String> { get }
    public func send(_ command: String)
    public func uci()
    public func isReady()
    public func newGame()
    public func quit()
    public func setPosition(fen: String = "startpos", moves: [String] = [])
    public func goInfinite()
    public func go(depth: Int)
    public func go(wtime: Int, btime: Int, winc: Int = 0, binc: Int = 0)
    public func stop()
}

See also