File size: 7,302 Bytes
463f868
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
// use crate::test_helpers::{Action, TestUtils, create_test_db, create_test_state, p_state};
use crate::core::logic::*;
// use crate::core::models::*;
// use crate::core::enums::*;
use serde_json::Value;
use std::collections::HashSet;

const DB_JSON: &str = include_str!("../../data/cards_compiled.json");
const METADATA_JSON: &str = include_str!("../../data/metadata.json");
const GENERATED_CONSTANTS_RS: &str = include_str!("core/generated_constants.rs");
const GENERATED_ENUMS_RS: &str = include_str!("core/enums.rs");

#[test]
fn test_database_integrity() {
    let card_db =
        CardDatabase::from_json(DB_JSON).expect("Failed to parse production CardDatabase");

    let mut member_ids = HashSet::new();
    let mut card_nos = HashSet::new();

    for (id, member) in &card_db.members {
        assert!(member_ids.insert(id), "Duplicate Member ID found: {}", id);
        assert!(!member.card_no.is_empty(), "Empty CardNo for ID: {}", id);
        card_nos.insert(member.card_no.clone());
    }

    let mut live_ids = HashSet::new();
    for (id, live) in &card_db.lives {
        assert!(live_ids.insert(id), "Duplicate Live ID found: {}", id);
        assert!(!live.card_no.is_empty(), "Empty CardNo for ID: {}", id);
        card_nos.insert(live.card_no.clone());
    }

    println!(
        "Database Integrity Check Passed: {} members, {} lives",
        card_db.members.len(),
        card_db.lives.len()
    );
}

#[test]
fn test_bytecode_sanity_all_cards() {
    let card_db =
        CardDatabase::from_json(DB_JSON).expect("Failed to parse production CardDatabase");
    let mut total_abilities = 0;
    let mut opcodes_seen = HashSet::new();

    for member in card_db.members.values() {
        for (idx, ab) in member.abilities.iter().enumerate() {
            total_abilities += 1;
            verify_ability_bytecode(&member.card_no, idx, ab, &mut opcodes_seen);
        }
    }

    for live in card_db.lives.values() {
        for (idx, ab) in live.abilities.iter().enumerate() {
            total_abilities += 1;
            verify_ability_bytecode(&live.card_no, idx, ab, &mut opcodes_seen);
        }
    }

    println!("Bytecode Sanity Passed for {} abilities.", total_abilities);
    println!(
        "Unique Opcodes Found in Production: {:?}",
        opcodes_seen.len()
    );
}

fn verify_ability_bytecode(card_no: &str, ab_idx: usize, ab: &Ability, opcodes: &mut HashSet<i32>) {
    if ab.bytecode.is_empty() {
        return;
    }

    // Rule 1: Bytecode block must contain O_RETURN (10)
    assert!(
        ab.bytecode.contains(&O_RETURN),
        "Ability {} [{}] does not contain O_RETURN",
        card_no,
        ab_idx
    );

    // Rule 2: Bytecode length should generally be a multiple of 5 (5-word extended format)
    // Note: Some jump targets or complex opcodes might vary, but O_RETURN is always at the end.
    // In our compiler, almost all are 5-word aligned.
    assert!(
        ab.bytecode.len() % 5 == 0,
        "Ability {} [{}] bytecode length {} is not multiple of 5",
        card_no,
        ab_idx,
        ab.bytecode.len()
    );

    for chunk in ab.bytecode.chunks(5) {
        if !chunk.is_empty() {
            opcodes.insert(chunk[0]);
        }
    }
}

#[test]
fn test_dry_run_all_cards() {
    let card_db =
        CardDatabase::from_json(DB_JSON).expect("Failed to parse production CardDatabase");
    let mut state = GameState::default();
    // Basic setup for dry run
    state.players[0].player_id = 0;
    state.players[1].player_id = 1;

    for member in card_db.members.values() {
        for (idx, ab) in member.abilities.iter().enumerate() {
            if ab.bytecode.is_empty() || ab.trigger == TriggerType::Constant {
                continue;
            }

            let ctx = AbilityContext {
                player_id: 0,
                area_idx: 0,
                source_card_id: member.card_id,
                ability_index: idx.try_into().expect("ability_index out of i16 range"),
                ..Default::default()
            };

            // Dry run execution (this should NOT panic)
            // We use a clone of state to keep it clean
            let mut test_state = state.clone();
            test_state.resolve_bytecode_cref(&card_db, &ab.bytecode, &ctx);
        }
    }
}

#[test]
fn test_generated_metadata_stays_in_sync() {
    let metadata: Value = serde_json::from_str(METADATA_JSON).expect("Failed to parse metadata.json");

    assert_generated_constants_match(&metadata, "opcodes", "O_");
    assert_generated_constants_match(&metadata, "action_bases", "ACTION_BASE_");
    assert_generated_constants_match(&metadata, "conditions", "C_");
    assert_generated_constants_match(&metadata, "costs", "COST_");

    assert_generated_enum_match(&metadata, "triggers", enum_variant_name, GENERATED_ENUMS_RS);
    assert_generated_enum_match(&metadata, "targets", target_variant_name, GENERATED_ENUMS_RS);
    assert_generated_enum_match(&metadata, "phases", enum_variant_name, GENERATED_ENUMS_RS);
}

fn assert_generated_constants_match(metadata: &Value, section: &str, prefix: &str) {
    for (key, value) in metadata_section(metadata, section) {
        let expected = value.as_i64().unwrap_or_else(|| panic!("{section}.{key} is not numeric"));
        let expected_line = format!("pub const {prefix}{key}: i32 = {expected};");
        assert!(
            GENERATED_CONSTANTS_RS
                .lines()
                .any(|line| line.trim_start().starts_with(&expected_line)),
            "generated_constants.rs is out of sync for {section}.{key}={expected}"
        );
    }
}

fn assert_generated_enum_match(
    metadata: &Value,
    section: &str,
    variant_name: fn(&str) -> String,
    source: &str,
) {
    for (key, value) in metadata_section(metadata, section) {
        let expected = value.as_i64().unwrap_or_else(|| panic!("{section}.{key} is not numeric"));
        let variant = variant_name(key);
        let expected_line = format!("{variant} = {expected},");
        assert!(
            source
                .lines()
                .any(|line| line.trim_start().starts_with(&expected_line)),
            "enums.rs is out of sync for {section}.{key}={expected}"
        );
    }
}

fn metadata_section<'a>(metadata: &'a Value, section: &str) -> &'a serde_json::Map<String, Value> {
    metadata[section]
        .as_object()
        .unwrap_or_else(|| panic!("metadata section {section} is missing or not an object"))
}

fn enum_variant_name(raw: &str) -> String {
    if raw == "NONE" {
        return "None".to_string();
    }

    raw.to_ascii_lowercase()
        .split('_')
        .filter(|part| !part.is_empty())
        .map(|part| {
            let mut chars = part.chars();
            match chars.next() {
                Some(first) => first.to_ascii_uppercase().to_string() + chars.as_str(),
                None => String::new(),
            }
        })
        .collect::<String>()
}

fn target_variant_name(raw: &str) -> String {
    if raw == "SELF" {
        return "Self_".to_string();
    }
    enum_variant_name(raw)
}