approaches/fair-expectimax/rust-engine/src/bin/gate_trajectory.rsRust248 lines · 8.3 KB
// Trajectory equivalence gate: replays the columns recorded by the C++
// reference trace emitter (cpp/trace.cpp) through the Rust engine and
// compares everything observable, move by move: board, next disc, score,
// score delta, the full wave list, level, moves remaining, board-clear and
// level-advance flags, and the terminal flag.
//
// The columns come from the trace, so the gate isolates engine semantics
// from policy semantics.  Any nonzero mismatch count fails.
//
//   gate-trajectory --trace <path-to-c++-trace>

use drop7_rs::board::Board;
use drop7_rs::engine::{
    initial_ts_state, play_headless_move, play_ts_move, FullWaveSink, State,
};
use drop7_rs::rng::Mulberry32;

use std::env;
use std::fs;

struct MoveRecord {
    moves_played: i32,
    column: usize,
    score_delta: i64,
    board: String,
    next_disc: u8,
    score: i64,
    level: i32,
    moves_remaining: i32,
    game_over: bool,
    cleared: bool,
    advanced: bool,
    waves: String,
}

fn parse_move(line: &str) -> MoveRecord {
    // m <played> col <c> sd <delta> b <board> next <d> score <s> level <l>
    // mr <r> over <0|1> cleared <0|1> advanced <0|1> waves d:c:r:p ...
    let tokens: Vec<&str> = line.split_whitespace().collect();
    let mut record = MoveRecord {
        moves_played: 0,
        column: 0,
        score_delta: 0,
        board: String::new(),
        next_disc: 0,
        score: 0,
        level: 0,
        moves_remaining: 0,
        game_over: false,
        cleared: false,
        advanced: false,
        waves: String::new(),
    };
    let mut i = 0;
    while i < tokens.len() {
        match tokens[i] {
            "m" => {
                record.moves_played = tokens[i + 1].parse().unwrap();
                i += 2;
            }
            "col" => {
                record.column = tokens[i + 1].parse().unwrap();
                i += 2;
            }
            "sd" => {
                record.score_delta = tokens[i + 1].parse().unwrap();
                i += 2;
            }
            "b" => {
                record.board = tokens[i + 1].to_string();
                i += 2;
            }
            "next" => {
                record.next_disc = tokens[i + 1].parse().unwrap();
                i += 2;
            }
            "score" => {
                record.score = tokens[i + 1].parse().unwrap();
                i += 2;
            }
            "level" => {
                record.level = tokens[i + 1].parse().unwrap();
                i += 2;
            }
            "mr" => {
                record.moves_remaining = tokens[i + 1].parse().unwrap();
                i += 2;
            }
            "over" => {
                record.game_over = tokens[i + 1] == "1";
                i += 2;
            }
            "cleared" => {
                record.cleared = tokens[i + 1] == "1";
                i += 2;
            }
            "advanced" => {
                record.advanced = tokens[i + 1] == "1";
                i += 2;
            }
            "waves" => {
                record.waves = tokens[i + 1..].join(" ");
                break;
            }
            _ => panic!("unknown token {}", tokens[i]),
        }
    }
    record
}

fn main() {
    let mut trace_path = String::new();
    let mut driver = String::from("headless");
    let args: Vec<String> = env::args().collect();
    let mut i = 1;
    while i + 1 < args.len() {
        match args[i].as_str() {
            "--trace" => trace_path = args[i + 1].clone(),
            "--driver" => driver = args[i + 1].clone(),
            _ => {}
        }
        i += 2;
    }
    if trace_path.is_empty() {
        eprintln!("usage: gate-trajectory --trace <trace file> [--driver headless|ts]");
        std::process::exit(2);
    }
    let text = fs::read_to_string(&trace_path).expect("read trace");

    let mut games = 0u64;
    let mut moves = 0u64;
    let mut waves_total = 0u64;
    let mut mismatches = 0u64;
    let mut first_failure = String::new();

    let mut state = State {
        board: Board::initial(),
        next_disc: 0,
        score: 0,
        level: 1,
        moves_remaining: 5,
        moves_played: 0,
        game_over: false,
    };
    let mut seed = 0u32;
    let mut sink = FullWaveSink::new();
    let mut ts_random = Mulberry32::new(0);

    for line in text.lines() {
        if let Some(rest) = line.strip_prefix("game 0x") {
            // game <seed> next <d>
            let tokens: Vec<&str> = rest.split_whitespace().collect();
            seed = u32::from_str_radix(tokens[0], 16).unwrap();
            let next: u8 = tokens[2].parse().unwrap();
            if driver == "ts" {
                ts_random = Mulberry32::new(seed);
                state = initial_ts_state(&mut ts_random);
            } else {
                state = State::initial_headless(seed);
            }
            if state.next_disc != next {
                mismatches += 1;
                if first_failure.is_empty() {
                    first_failure = format!("seed {seed:#x}: initial next disc differs");
                }
            }
            games += 1;
        } else if line.starts_with("m ") {
            let record = parse_move(line);
            sink.clear();
            let result = if driver == "ts" {
                play_ts_move(&mut state, &mut ts_random, record.column, &mut sink)
            } else {
                play_headless_move(&mut state, seed, record.column, &mut sink)
            };
            moves += 1;
            waves_total += sink.count as u64;
            let rust_waves: Vec<String> = sink.waves[..sink.count]
                .iter()
                .map(|w| format!("{}:{}:{}:{}", w.depth, w.cleared, w.revealed, w.points))
                .collect();
            let identical = result.is_some()
                && state.moves_played == record.moves_played
                && result.unwrap().score_delta == record.score_delta
                && state.board.serialize() == record.board
                && state.next_disc == record.next_disc
                && state.score == record.score
                && state.level == record.level
                && state.moves_remaining == record.moves_remaining
                && state.game_over == record.game_over
                && result.unwrap().cleared_board == record.cleared
                && result.unwrap().level_advanced == record.advanced
                && rust_waves.join(" ") == record.waves;
            if !identical {
                mismatches += 1;
                if first_failure.is_empty() {
                    first_failure = format!(
                        "seed {seed:#x} move {}\n  rust: b {} next {} score {} mr {} waves {}\n  ref:  b {} next {} score {} mr {} waves {}",
                        record.moves_played,
                        state.board.serialize(),
                        state.next_disc,
                        state.score,
                        state.moves_remaining,
                        rust_waves.join(" "),
                        record.board,
                        record.next_disc,
                        record.score,
                        record.moves_remaining,
                        record.waves,
                    );
                }
            }
        } else if line.starts_with("end ") {
            // end score <s> moves <m> over <0|1>
            let tokens: Vec<&str> = line.split_whitespace().collect();
            let end_score: i64 = tokens[2].parse().unwrap();
            let end_moves: i32 = tokens[4].parse().unwrap();
            let end_over = tokens[6] == "1";
            if state.score != end_score
                || state.moves_played != end_moves
                || state.game_over != end_over
            {
                mismatches += 1;
                if first_failure.is_empty() {
                    first_failure = format!("seed {seed:#x}: terminal state differs");
                }
            }
        }
    }

    println!("games compared: {games}");
    println!("moves compared: {moves}");
    println!("waves compared: {waves_total}");
    println!("mismatches: {mismatches}");
    if mismatches > 0 {
        println!("first failure: {first_failure}");
    }
    let ok = mismatches == 0 && games > 0;
    println!(
        "{}",
        if ok {
            "TRAJECTORY GATE PASSED"
        } else {
            "TRAJECTORY GATE FAILED"
        }
    );
    std::process::exit(if ok { 0 } else { 1 });
}