ai-connect4 0.1.1

A compact Connect Four rules and alpha-beta search engine
Documentation
use ai_connect4::Engine;

#[test]
fn handshake_position_search_and_snapshot_share_one_session() {
    let mut engine = Engine::new();
    assert_eq!(engine.command("gai").last().unwrap(), "gaiok");
    assert!(engine.command("position startpos moves d c d e").is_empty());
    assert!(
        engine
            .command("setoption name Algorithm value ordered")
            .is_empty()
    );
    let search = engine.command("go depth 6");
    assert!(search[0].starts_with("info depth 6 score eval "));
    assert!(search[0].contains(" nodes "));
    assert!(search[0].contains(" cutoffs "));
    assert!(search[1].starts_with("bestmove "));

    let state = engine.command("state");
    assert!(state[0].starts_with("state {\"game\":\"connect4\""));
    assert!(state[0].contains("\"history\":[\"d\",\"c\",\"d\",\"e\"]"));
    assert!(state[0].contains("\"algorithm\":\"ordered\""));
    assert!(state[0].contains("\"rootBranches\""));

    let iterative = engine.command("go iterative depth 9 nodes 20000");
    assert!(iterative[0].starts_with("info depth "));
    assert!(iterative[0].contains(" nodes 20000 "));
    assert!(iterative[0].contains(" iterations "));

    assert!(engine.command("position startpos moves d d e e").is_empty());
    assert_eq!(
        engine.command("oracle"),
        ["oracle source gamesolver-tutorial notation 4455 score 18 outcome win"]
    );
    assert!(engine.command("state")[0].contains("\"oracle\":{\"status\":\"hit\""));
}

#[test]
fn browser_snapshot_omits_developer_diagnostics() {
    let mut engine = Engine::new();
    assert!(engine.command("position startpos moves d c d e").is_empty());
    assert!(
        engine
            .command("setoption name Algorithm value ordered")
            .is_empty()
    );
    let search = engine.command("go depth 6");
    assert!(search[1].starts_with("bestmove "));

    let snapshot = engine.snapshot_json();
    assert!(snapshot.starts_with("{\"columns\":["));
    assert!(snapshot.contains("\"history\":[\"d\",\"c\",\"d\",\"e\"]"));
    assert!(snapshot.contains("\"analysis\":{\"bestMove\":"));
    for diagnostic in [
        "\"game\"",
        "\"algorithm\"",
        "\"oracle\"",
        "\"depth\"",
        "\"score\"",
        "\"stats\"",
        "\"rootBranches\"",
    ] {
        assert!(
            !snapshot.contains(diagnostic),
            "browser snapshot contains {diagnostic}"
        );
    }
    assert!(snapshot.len() < 1_000);

    let state = engine.command("state");
    assert!(state[0].starts_with("state {\"game\":\"connect4\""));
    assert!(state[0].contains("\"algorithm\":\"ordered\""));
    assert!(state[0].contains("\"depth\":6"));
    assert!(state[0].contains("\"score\""));
    assert!(state[0].contains("\"stats\""));
    assert!(state[0].contains("\"rootBranches\""));
}

#[test]
fn malformed_commands_return_structured_errors_without_panicking() {
    let mut engine = Engine::new();
    for command in [
        "",
        "wat",
        "position",
        "position startpos wat",
        "position startpos moves z",
        "setoption",
        "setoption name Algorithm value magic",
        "go",
        "go depth 0",
        "go depth 43",
        "go nodes 100",
        "go depth seven",
        "go depth 7 depth 8",
        "go depth 8 nodes 0",
        "oracle extra",
        "perft",
        "perft many",
        "perft 13",
    ] {
        let response = engine.command(command);
        assert!(
            response
                .first()
                .is_some_and(|line| line.starts_with("error code ")),
            "{command:?}: {response:?}"
        );
    }
}