rustraight 0.5.2

A simple 2D game library for Rust, inspired by DXLib
// rustraight 統合検証プログラム。
//
// docs/機能辞典.md の全69項目を1つの連続したウィンドウセッションで先頭から順に確認する。
// 目視/耳/ログで人力判定が必要な項目(64件)はカテゴリごとの実演シーンを見ながら
// F1(合格) / F2(不合格) / F3(スキップ) / Esc(中断して終了) で判定する。
// 自動テスト可の項目(Color::rgb/rgba/定数、load_font、get_text_width。5件)は
// ウィンドウを開く前に単発で検証し、結果をログに記録する。
//
// 結果は 検証ログ/verify_all_<起動時unixエポック秒>.log に都度flushしながら記録する
// (docs/機能辞典.md 自体は書き換えない)。

#[path = "checklist.rs"] mod checklist;
#[path = "verify_log.rs"] mod verify_log;
#[path = "demo_cat01_init.rs"] mod demo_cat01_init;
#[path = "demo_cat02_sprites.rs"] mod demo_cat02_sprites;
#[path = "demo_cat03_shapes.rs"] mod demo_cat03_shapes;
#[path = "demo_cat04_blend_mask.rs"] mod demo_cat04_blend_mask;
#[path = "demo_cat05_subscreen.rs"] mod demo_cat05_subscreen;
#[path = "demo_cat06_keyboard.rs"] mod demo_cat06_keyboard;
#[path = "demo_cat07_mouse.rs"] mod demo_cat07_mouse;
#[path = "demo_cat08_gamepad.rs"] mod demo_cat08_gamepad;
#[path = "demo_cat09_sound.rs"] mod demo_cat09_sound;
#[path = "demo_cat10_text.rs"] mod demo_cat10_text;
#[path = "demo_cat11_overlay.rs"] mod demo_cat11_overlay;
#[path = "demo_cat12_debuglog.rs"] mod demo_cat12_debuglog;

use rustraight::prelude::*;

use checklist::{Category, ChecklistItem, VerifyMethod, CHECKLIST, RUN_ORDER};
use verify_log::{Verdict, VerifyLog};

pub(crate) const COMMON_W: i32 = 960;
pub(crate) const COMMON_H: i32 = 720;
const Y0: i32 = 110;

/// 検証素材/ 配下のファイルへの絶対パスを組み立てる(既存11 exampleと同じ規約)。
pub(crate) fn asset(rel: &str) -> String {
    format!("{}/検証素材/{}", env!("CARGO_MANIFEST_DIR"), rel)
}

/// 指定した検証素材/相対パス群のうち、実際に存在しないものの絶対パス一覧を返す。
pub(crate) fn missing_assets(rels: &[&str]) -> Vec<String> {
    rels.iter()
        .map(|r| asset(r))
        .filter(|p| !std::path::Path::new(p).exists())
        .collect()
}

enum CategoryState {
    Init(demo_cat01_init::State),
    Sprites(demo_cat02_sprites::State),
    Shapes,
    BlendMask(demo_cat04_blend_mask::State),
    Subscreen(demo_cat05_subscreen::State),
    Keyboard,
    Mouse(demo_cat07_mouse::State),
    Gamepad,
    Sound(demo_cat09_sound::State),
    Text(demo_cat10_text::State),
    Overlay(demo_cat11_overlay::State),
    DebugLog,
}

fn enter_category(cat: Category) -> Result<CategoryState, String> {
    // 前カテゴリでの set_window_size/set_screen_size の変更を引きずらないよう、
    // カテゴリ切替のたびに共通サイズへ正規化する(カテゴリ1自体もこの後すぐ操作で変更できる)。
    set_window_size(COMMON_W, COMMON_H);
    set_screen_size(COMMON_W, COMMON_H);

    match cat {
        Category::Init => demo_cat01_init::setup().map(CategoryState::Init),
        Category::Sprites => demo_cat02_sprites::setup().map(CategoryState::Sprites),
        Category::Shapes => demo_cat03_shapes::setup().map(|_| CategoryState::Shapes),
        Category::BlendMask => demo_cat04_blend_mask::setup().map(CategoryState::BlendMask),
        Category::Subscreen => demo_cat05_subscreen::setup().map(CategoryState::Subscreen),
        Category::Keyboard => demo_cat06_keyboard::setup().map(|_| CategoryState::Keyboard),
        Category::Mouse => demo_cat07_mouse::setup().map(CategoryState::Mouse),
        Category::Gamepad => demo_cat08_gamepad::setup().map(|_| CategoryState::Gamepad),
        Category::Sound => demo_cat09_sound::setup().map(CategoryState::Sound),
        Category::Text => demo_cat10_text::setup().map(CategoryState::Text),
        Category::Overlay => demo_cat11_overlay::setup().map(CategoryState::Overlay),
        Category::DebugLog => demo_cat12_debuglog::setup().map(|_| CategoryState::DebugLog),
    }
}

fn render_category(state: &mut CategoryState) {
    match state {
        CategoryState::Init(s) => demo_cat01_init::render(s),
        CategoryState::Sprites(s) => demo_cat02_sprites::render(s),
        CategoryState::Shapes => demo_cat03_shapes::render(),
        CategoryState::BlendMask(s) => demo_cat04_blend_mask::render(s),
        CategoryState::Subscreen(s) => demo_cat05_subscreen::render(s),
        CategoryState::Keyboard => demo_cat06_keyboard::render(),
        CategoryState::Mouse(s) => demo_cat07_mouse::render(s),
        CategoryState::Gamepad => demo_cat08_gamepad::render(),
        CategoryState::Sound(s) => demo_cat09_sound::render(s),
        CategoryState::Text(s) => demo_cat10_text::render(s),
        CategoryState::Overlay(s) => demo_cat11_overlay::render(s),
        CategoryState::DebugLog => demo_cat12_debuglog::render(),
    }
}

/// ウィンドウ/GPUを必要としない項目(Color系・フォント系)を init() 前に単発で検証する。
fn run_auto_checks(log: &mut VerifyLog) {
    for item in CHECKLIST.iter().filter(|i| i.method == VerifyMethod::Auto) {
        let (ok, reason) = auto_check(item);
        log.record(item, if ok { Verdict::Pass } else { Verdict::Fail }, &reason);
    }
}

fn auto_check(item: &ChecklistItem) -> (bool, String) {
    match item.id {
        27 => {
            // Color::rgb(r, g, b) -> Color
            let c = Color::rgb(10, 20, 30);
            let ok = c.r == 10 && c.g == 20 && c.b == 30 && c.a == 255;
            (ok, format!("r={} g={} b={} a={}", c.r, c.g, c.b, c.a))
        }
        28 => {
            // Color::rgba(r, g, b, a) -> Color
            let c = Color::rgba(10, 20, 30, 40);
            let ok = c.r == 10 && c.g == 20 && c.b == 30 && c.a == 40;
            (ok, format!("r={} g={} b={} a={}", c.r, c.g, c.b, c.a))
        }
        29 => {
            // 定義済み定数
            let ok = Color::WHITE.r == 255 && Color::WHITE.g == 255 && Color::WHITE.b == 255 && Color::WHITE.a == 255
                && Color::BLACK.r == 0 && Color::BLACK.g == 0 && Color::BLACK.b == 0
                && Color::RED.r == 255 && Color::RED.g == 0 && Color::RED.b == 0
                && Color::GREEN.r == 0 && Color::GREEN.g == 255 && Color::GREEN.b == 0
                && Color::BLUE.r == 0 && Color::BLUE.g == 0 && Color::BLUE.b == 255
                && Color::YELLOW.r == 255 && Color::YELLOW.g == 255 && Color::YELLOW.b == 0
                && Color::CYAN.r == 0 && Color::CYAN.g == 255 && Color::CYAN.b == 255
                && Color::MAGENTA.r == 255 && Color::MAGENTA.g == 0 && Color::MAGENTA.b == 255;
            (ok, String::new())
        }
        55 => {
            // load_font(path, size) -> u32 (成功/失敗の両方を検証)
            let missing = missing_assets(&["fonts/Mamelon-3.5-Hi-Regular.otf"]);
            if !missing.is_empty() {
                return (false, format!("アセット不足のため検証不可: {}", missing.join(", ")));
            }
            let ok_handle = load_font(&asset("fonts/Mamelon-3.5-Hi-Regular.otf"), 20);
            let bad_handle = load_font("no_such_font_for_verify_all.ttf", 20);
            let ok = ok_handle != 0 && bad_handle == 0;
            (ok, format!("ok_handle={ok_handle} bad_handle={bad_handle}"))
        }
        56 => {
            // get_text_width(text, font_id) -> i32
            let missing = missing_assets(&["fonts/Mamelon-3.5-Hi-Regular.otf"]);
            if !missing.is_empty() {
                return (false, format!("アセット不足のため検証不可: {}", missing.join(", ")));
            }
            let font = load_font(&asset("fonts/Mamelon-3.5-Hi-Regular.otf"), 20);
            let w_short = get_text_width("A", font);
            let w_long = get_text_width("AAAA", font);
            let ok = w_short > 0 && w_long > w_short;
            (ok, format!("w_short={w_short} w_long={w_long}"))
        }
        _ => (false, "未対応の自動チェック項目".to_string()),
    }
}

fn draw_hud(item: &ChecklistItem, item_idx: usize, cat_total: usize, global_done: usize, global_total: usize) {
    draw_rectangle(MAIN_SCREEN, 0, 0, COMMON_W, Y0, Color::rgb(30, 30, 45), true);
    draw_text(
        MAIN_SCREEN, 10, 6,
        format!(
            "[{}/{}] 全体 {}/{}  カテゴリ: {}",
            item_idx + 1, cat_total, global_done, global_total, item.category.name_ja()
        ),
        Color::CYAN,
    );
    draw_text(MAIN_SCREEN, 10, 30, item.api, Color::YELLOW);
    draw_text(MAIN_SCREEN, 10, 54, item.description, Color::WHITE);
    draw_text(MAIN_SCREEN, 10, 84, "F1:合格(PASS)  F2:不合格(FAIL)  F3:スキップ(SKIP)  Esc:中断して終了", Color::GREEN);
}

fn draw_done_screen(log: &VerifyLog) {
    let (pass, fail, skip, notrun) = log.summary_counts();
    draw_text(MAIN_SCREEN, 20, 40, "検証完了", Color::CYAN);
    draw_text(MAIN_SCREEN, 20, 80, format!("PASS={pass}  FAIL={fail}  SKIP={skip}  (未実施={notrun})"), Color::WHITE);
    draw_text(MAIN_SCREEN, 20, 120, format!("詳細ログ: {}", log.path()), Color::YELLOW);
    draw_text(MAIN_SCREEN, 20, 150, "ウィンドウを閉じるとこのプログラムは終了します。", Color::YELLOW);
}

enum Phase {
    EnterCategory,
    Judging,
    Done,
}

struct Runner {
    order_idx: usize,
    items: Vec<&'static ChecklistItem>,
    item_idx: usize,
    phase: Phase,
    state: Option<CategoryState>,
    global_done: usize,
    global_total: usize,
}

fn main() {
    let mut log = VerifyLog::new();
    run_auto_checks(&mut log);

    init(WindowConfig {
        title: String::from("rustraight verify_all"),
        width: COMMON_W,
        height: COMMON_H,
        screen_width: COMMON_W,
        screen_height: COMMON_H,
        overlay_enabled: true,
        ..Default::default()
    });

    let mut runner = Runner {
        order_idx: 0,
        items: Vec::new(),
        item_idx: 0,
        phase: Phase::EnterCategory,
        state: None,
        global_done: 0,
        global_total: checklist::manual_total(),
    };

    while advance_frame() {
        draw_fill(MAIN_SCREEN, Color::rgb(15, 15, 20));

        match runner.phase {
            Phase::EnterCategory => {
                if runner.order_idx >= RUN_ORDER.len() {
                    runner.phase = Phase::Done;
                } else {
                    let cat = RUN_ORDER[runner.order_idx];
                    let items = checklist::manual_items_for(cat);
                    match enter_category(cat) {
                        Ok(state) => {
                            runner.state = Some(state);
                            runner.items = items;
                            runner.item_idx = 0;
                            if runner.items.is_empty() {
                                runner.order_idx += 1;
                            } else {
                                runner.phase = Phase::Judging;
                            }
                        }
                        Err(reason) => {
                            for item in &items {
                                log.record(item, Verdict::Skip, &reason);
                                runner.global_done += 1;
                            }
                            runner.order_idx += 1;
                        }
                    }
                }
            }
            Phase::Judging => {
                if let Some(state) = runner.state.as_mut() {
                    render_category(state);
                }

                let item = runner.items[runner.item_idx];
                draw_hud(item, runner.item_idx, runner.items.len(), runner.global_done, runner.global_total);

                if is_just_pressed(KeyCode::F1) {
                    log.record(item, Verdict::Pass, "");
                    runner.item_idx += 1;
                    runner.global_done += 1;
                } else if is_just_pressed(KeyCode::F2) {
                    log.record(item, Verdict::Fail, "");
                    runner.item_idx += 1;
                    runner.global_done += 1;
                } else if is_just_pressed(KeyCode::F3) {
                    log.record(item, Verdict::Skip, "手動スキップ");
                    runner.item_idx += 1;
                    runner.global_done += 1;
                }

                if runner.item_idx >= runner.items.len() {
                    runner.state = None;
                    runner.order_idx += 1;
                    runner.phase = Phase::EnterCategory;
                }
            }
            Phase::Done => {
                draw_done_screen(&log);
            }
        }

        if is_just_pressed(KeyCode::Escape) {
            runner.phase = Phase::Done;
        }
    }

    // ウィンドウが閉じられて終了する場合(Doneに到達済み/Escで中断/×ボタンでの強制終了、いずれも含む)、
    // 未記録の項目を一括でNOTRUNとして記録してからサマリーを書き出す。
    log.sweep_notrun_all();
    log.write_summary();
    println!("検証終了。詳細は {} を参照してください。", log.path());
}