Skip to content

Usage Examples

The following examples use only the public Swift API (RecklessEngine and RecklessNetworkLoader). Each standalone snippet includes its required imports.

Use cancellationSafeOutput for sequential reads

output is a single-consumer AsyncStream — breaking out of its loop ends it permanently, so a later for await gets immediate EOF. Use cancellationSafeOutput for sequential/restartable reads (as every example below does); never consume both surfaces on one engine. output is only for generic UCIEngine consumers that retain one long-lived subscription for the engine's whole life.

Setup: provision the net, create the engine, and handshake

import Foundation
import SwiftReckless

func makeEngine() async throws -> RecklessEngine {
    let support = FileManager.default.urls(for: .applicationSupportDirectory,
                                           in: .userDomainMask)[0]
    let netDir = support.appendingPathComponent("reckless-nets")

    // 1. Ensure the NNUE net is present. Idempotent — fast no-op if already valid.
    // Note: the progress closure fires once at download completion (terminal
    // byte count), not incrementally. fractionCompleted is 1.0 on success.
    try await RecklessNetworkLoader().ensure(in: netDir) { p in
        if let f = p.fractionCompleted {
            print("Downloading net: \(Int(f * 100))%")
        }
    }

    // 2. Create the engine.
    guard let engine = RecklessEngine(networkDirectory: netDir) else {
        throw NSError(domain: "SwiftReckless", code: 1,
                      userInfo: [NSLocalizedDescriptionKey: "engine init failed"])
    }

    // 3. Handshake: uci → uciok → isready → readyok.
    // cancellationSafeOutput survives the `break` below; `output` would not.
    engine.uci()
    for await line in engine.cancellationSafeOutput {
        if line == "uciok"   { engine.isReady() }
        if line == "readyok" { break }
    }
    return engine
}

Best move at fixed depth

func bestMove(for fen: String, depth: Int, engine: RecklessEngine) async -> String? {
    engine.setPosition(fen: fen)
    engine.go(depth: depth)
    for await line in engine.cancellationSafeOutput {
        if line.hasPrefix("bestmove ") {
            return line.split(separator: " ").dropFirst().first.map(String.init)
        }
    }
    return nil
}

// Usage:
let engine = try await makeEngine()
let move = await bestMove(
    for: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
    depth: 20,
    engine: engine
)
print("Best move:", move ?? "none")

Stream a live evaluation bar

Parse info lines to drive a centipawn evaluation bar or depth readout:

struct Eval {
    var depth = 0
    var scoreCp: Int?
    var mateIn: Int?
    var pv: [String] = []
}

func parseInfo(_ line: String) -> Eval? {
    guard line.hasPrefix("info "), line.contains(" pv ") else { return nil }
    var eval = Eval()
    let tokens = line.split(separator: " ").map(String.init)
    var i = 0
    while i < tokens.count {
        switch tokens[i] {
        case "depth": eval.depth = Int(tokens[i + 1]) ?? 0; i += 2
        case "score":
            if tokens[i + 1] == "cp"   { eval.scoreCp = Int(tokens[i + 2]) }
            if tokens[i + 1] == "mate" { eval.mateIn  = Int(tokens[i + 2]) }
            i += 3
        case "pv":
            eval.pv = Array(tokens[(i + 1)...]); i = tokens.count
        default: i += 1
        }
    }
    return eval
}

engine.setPosition(fen: "startpos", moves: ["e2e4", "e7e5"])
engine.go(depth: 24)
for await line in engine.cancellationSafeOutput {
    if let eval = parseInfo(line) {
        let score = eval.scoreCp.map { "\($0)cp" } ?? "mate \(eval.mateIn ?? 0)"
        print("depth \(eval.depth)  score \(score)  pv \(eval.pv.prefix(5).joined(separator: " "))")
    }
    if line.hasPrefix("bestmove ") { break }
}

Infinite analysis with stop

engine.setPosition(fen: "startpos")
engine.goInfinite()

// Let it run for a few seconds, then stop.
try await Task.sleep(for: .seconds(5))
engine.stop()

// Collect the bestmove after stop.
for await line in engine.cancellationSafeOutput {
    if line.hasPrefix("bestmove ") {
        print("Stopped at:", line)
        break
    }
}

Time-managed search (clock game)

// White has 2 minutes, Black has 1 minute 55 seconds, 2s increment each.
engine.setPosition(fen: "startpos", moves: ["e2e4", "e7e5", "g1f3"])
engine.go(wtime: 120_000, btime: 115_000, winc: 2_000, binc: 2_000)
for await line in engine.cancellationSafeOutput {
    if line.hasPrefix("bestmove ") {
        print(line)
        break
    }
}

Top-3 candidates (MultiPV)

engine.send("setoption name MultiPV value 3")
engine.setPosition(fen: "startpos")
engine.go(depth: 18)

var candidates: [Int: String] = [:]   // multipv index → first pv move

for await line in engine.cancellationSafeOutput {
    if line.hasPrefix("info "),
       let idxRange = line.range(of: "multipv ") {
        let idxStr = line[idxRange.upperBound...].prefix { $0.isNumber }
        let idx = Int(idxStr) ?? 0
        if let pvRange = line.range(of: " pv ") {
            candidates[idx] = line[pvRange.upperBound...]
                .split(separator: " ").first.map(String.init)
        }
    }
    if line.hasPrefix("bestmove ") { break }
}

print(candidates)   // e.g. [1: "e2e4", 2: "d2d4", 3: "g1f3"]

Using setPosition with a move list

// Start position + first three moves of the Italian Game.
engine.setPosition(fen: "startpos", moves: ["e2e4", "e7e5", "g1f3", "b8c6", "f1c4"])
engine.go(depth: 22)

New game between searches

// Reset hash tables between independent positions.
engine.newGame()
engine.setPosition(fen: "startpos")
engine.go(depth: 20)

Tear down

engine.shutdown() // joins the Rust thread, frees state, and finishes output
// Releasing the reference also triggers deinit-based teardown if omitted.

Only one engine may be live at a time. After shutdown() returns, a new RecklessEngine may be created in the same process; the pinned fork supports restartable sequential lifetimes.