ai-hex 0.1.0

A small Hex rules and Monte Carlo tree search engine
Documentation
use ai_hex::Engine;

fn snapshot(engine: &Engine) -> serde_json::Value {
    serde_json::from_str(&engine.snapshot_json()).unwrap()
}

#[test]
fn handshake_identifies_the_engine() {
    assert_eq!(
        Engine::new().command("gai"),
        ["id name ai-hex", "id game hex", "gaiok"]
    );
}

#[test]
fn position_round_trips_swap_state() {
    let mut engine = Engine::new();
    assert!(
        engine
            .command("position size 15 swap on moves h8 swap g8")
            .is_empty()
    );
    let state = snapshot(&engine);
    assert_eq!(state["size"], 15);
    assert_eq!(state["seatToMove"], "two");
    assert_eq!(state["colorToMove"], "R");
    assert_eq!(state["seatColors"], serde_json::json!(["B", "R"]));
    assert_eq!(state["history"], serde_json::json!(["h8", "swap", "g8"]));
}

#[test]
fn mcts_does_not_apply_its_answer() {
    let mut engine = Engine::new();
    engine.command("newgame size 9 swap on");
    let response = engine.command(
        "mcts simulations 25 softtime 0 exploration 0.2 strategy uct-rave rave 750 rollout save-bridge knowledge 32 connections on seed 3",
    );
    assert!(response[0].starts_with("bestmove "));
    let state = snapshot(&engine);
    assert_eq!(state["history"], serde_json::json!([]));
    assert_eq!(state["decision"]["simulations"], 25);
    assert_eq!(state["decision"]["algorithm"], "uct");
    assert_eq!(state["decision"]["strategy"], "uct-rave");
    assert_eq!(state["decision"]["raveEquivalence"], 750.0);
    assert_eq!(state["decision"]["rolloutPolicy"], "save-bridge");
    assert_eq!(state["decision"]["knowledgeThreshold"], 32);
    assert_eq!(state["decision"]["virtualConnectionsEnabled"], true);
    assert!(state["decision"]["bridgeReplies"].is_number());
    assert!(state["decision"]["prunedMoves"].is_number());
    assert!(state["decision"]["virtualConnections"].is_number());
    assert!(state["decision"]["semiConnections"].is_number());
    assert!(state["decision"]["provenNodes"].is_number());
    assert!(
        state["decision"]["provenWinner"].is_null()
            || state["decision"]["provenWinner"].is_string()
    );
    assert!(state["decision"]["rootMoves"][0]["raveVisits"].is_number());
    assert!(
        state["decision"]["rootMoves"][0]["provenWinner"].is_null()
            || state["decision"]["rootMoves"][0]["provenWinner"].is_string()
    );
}

#[test]
fn malformed_commands_return_errors() {
    let mut engine = Engine::new();
    assert!(engine.command("newgame size 8")[0].contains("error"));
    assert!(engine.command("position size 9 moves z1")[0].contains("error"));
    assert!(engine.command("mcts simulations 0")[0].contains("error"));
    assert!(engine.command("mcts softtime 2001")[0].contains("error"));
    assert!(engine.command("mcts strategy rave")[0].contains("error"));
    assert!(engine.command("mcts rave 0")[0].contains("error"));
    assert!(engine.command("mcts rollout bridge")[0].contains("error"));
    assert!(engine.command("mcts knowledge nope")[0].contains("error"));
    assert!(engine.command("mcts connections maybe")[0].contains("error"));
}