File size: 15,945 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
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
#!/usr/bin/env python3
"""

Parity tests for IR <-> bytecode <-> readable decode.



This test suite ensures that:

1. Abilities compile to bytecode correctly

2. Semantic forms (IR) are built consistently from abilities

3. Bytecode can be decoded to human-readable form

4. All three representations (IR, bytecode, readable) are consistent



These tests catch layout drift early: if bytecode layout changes but

semantic form or decoder don't update, tests will fail immediately.



Layout versioning is verified: tests ensure version markers are present

and consistent across compiled output.

"""

import json
import os
import sys
from typing import Any, Dict, List, Tuple

# Add project root to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../.."))

from engine.models.ability import Ability
from engine.models.ability_ir import (
    BYTECODE_LAYOUT_NAME,
    BYTECODE_LAYOUT_VERSION,
    SEMANTIC_FORM_VERSION,
    AbilityIR,
)
from engine.models.bytecode_readable import (
    CONDITION_NAMES,
    COST_NAMES,
    OPCODE_NAMES,
    TRIGGER_NAMES,
    decode_bytecode,
    opcode_name,
    trigger_name,
)


class ParityTestResult:
    """Container for parity test outcomes."""

    def __init__(self, test_name: str):
        self.test_name = test_name
        self.passed = False
        self.errors: List[str] = []
        self.warnings: List[str] = []

    def add_error(self, msg: str):
        self.errors.append(msg)

    def add_warning(self, msg: str):
        self.warnings.append(msg)

    def set_passed(self):
        self.passed = True

    def summary(self) -> str:
        status = "[PASS]" if self.passed else "[FAIL]"
        lines = [f"{status} {self.test_name}"]
        if self.errors:
            for err in self.errors:
                lines.append(f"  ERROR: {err}")
        if self.warnings:
            for warn in self.warnings:
                lines.append(f"  WARNING: {warn}")
        return "\n".join(lines)


def test_ability_compilation(ability: Ability) -> ParityTestResult:
    """Test that an ability compiles to bytecode without errors."""
    result = ParityTestResult(f"Ability compilation: {ability.raw_text[:40]}")

    try:
        ability.compile()

        if not hasattr(ability, "bytecode") or ability.bytecode is None:
            result.add_error("Ability.compile() did not produce bytecode")
            return result

        if not isinstance(ability.bytecode, list):
            result.add_error(
                f"Bytecode is {type(ability.bytecode)}, expected list"
            )
            return result

        if len(ability.bytecode) == 0:
            result.add_error("Bytecode is empty")
            return result

        # Bytecode should be 5-word chunks
        if len(ability.bytecode) % 5 != 0:
            result.add_warning(
                f"Bytecode length {len(ability.bytecode)} is not multiple of 5"
            )

        result.set_passed()
    except Exception as e:
        result.add_error(f"Compilation raised exception: {e}")

    return result


def test_semantic_form_building(ability: Ability) -> ParityTestResult:
    """Test that semantic form (IR) builds successfully from ability."""
    result = ParityTestResult(f"Semantic form building: {ability.raw_text[:40]}")

    try:
        # Semantic form must be built after compilation
        if not hasattr(ability, "bytecode") or ability.bytecode is None:
            result.add_error("Cannot build semantic form without bytecode")
            return result

        semantic_form_dict = ability.build_semantic_form()

        if semantic_form_dict is None:
            result.add_error("build_semantic_form() returned None")
            return result

        if not isinstance(semantic_form_dict, dict):
            result.add_error(
                f"Semantic form is {type(semantic_form_dict)}, expected dict"
            )
            return result

        # Check required fields
        required_fields = [
            "semantic_version",
            "bytecode_layout_version",
            "bytecode_layout_name",
            "trigger",
            "effects",
            "conditions",
            "costs",
        ]
        for field in required_fields:
            if field not in semantic_form_dict:
                result.add_error(f"Semantic form missing field: {field}")
                return result

        # Verify version markers
        if semantic_form_dict["semantic_version"] != SEMANTIC_FORM_VERSION:
            result.add_error(
                f"semantic_version mismatch: "
                f"{semantic_form_dict['semantic_version']} != {SEMANTIC_FORM_VERSION}"
            )
            return result

        if semantic_form_dict["bytecode_layout_version"] != BYTECODE_LAYOUT_VERSION:
            result.add_error(
                f"bytecode_layout_version mismatch: "
                f"{semantic_form_dict['bytecode_layout_version']} != {BYTECODE_LAYOUT_VERSION}"
            )
            return result

        if semantic_form_dict["bytecode_layout_name"] != BYTECODE_LAYOUT_NAME:
            result.add_error(
                f"bytecode_layout_name mismatch: "
                f"{semantic_form_dict['bytecode_layout_name']} != {BYTECODE_LAYOUT_NAME}"
            )
            return result

        result.set_passed()
    except Exception as e:
        result.add_error(f"Semantic form building raised exception: {e}")

    return result


def test_bytecode_decodable(

    bytecode: List[int], ability_desc: str

) -> ParityTestResult:
    """Test that bytecode can be decoded to readable form."""
    result = ParityTestResult(f"Bytecode decodable: {ability_desc[:40]}")

    try:
        readable = decode_bytecode(bytecode)

        if readable is None or readable == "":
            result.add_error("Bytecode decoding returned empty/None")
            return result

        if "LEGEND" not in readable:
            result.add_warning("Decoded bytecode missing legend section")

        result.set_passed()
    except Exception as e:
        result.add_error(f"Bytecode decoding raised exception: {e}")

    return result


def test_bytecode_chunk_structure(

    bytecode: List[int], ability_desc: str

) -> ParityTestResult:
    """Test that bytecode chunks are valid 5-word structures."""
    result = ParityTestResult(f"Bytecode structure valid: {ability_desc[:40]}")

    try:
        # All chunks should be 5 words
        if len(bytecode) % 5 != 0:
            result.add_error(
                f"Bytecode length {len(bytecode)} not multiple of 5"
            )
            return result

        # Break into 5-word chunks
        for i in range(0, len(bytecode), 5):
            chunk = bytecode[i : i + 5]
            if len(chunk) != 5:
                result.add_error(
                    f"Chunk at offset {i} has length {len(chunk)}, "
                    f"expected 5"
                )
                return result

            op, val, attr_low, attr_high, slot = chunk

            # Opcode should be decodable
            if op not in OPCODE_NAMES and op < 1000:
                # Some opcodes may legitimately not be in OPCODE_NAMES if dynamic
                if op >= 1000:
                    # Negated opcode
                    base_op = op - 1000
                    if base_op not in OPCODE_NAMES:
                        result.add_warning(
                            f"Chunk {i//5}: opcode {op} not found in "
                            f"OPCODE_NAMES"
                        )

        result.set_passed()
    except Exception as e:
        result.add_error(f"Bytecode structure validation raised exception: {e}")

    return result


def test_naming_consistency() -> ParityTestResult:
    """Test that naming dicts are consistent and have no conflicts."""
    result = ParityTestResult("Naming consistency")

    try:
        # Check for duplicate keys (shouldn't happen, but catch edge cases)
        all_keys = set()
        for key in OPCODE_NAMES.keys():
            if key in all_keys:
                result.add_error(f"Duplicate key in OPCODE_NAMES: {key}")
                return result
            all_keys.add(key)

        # Check that trigger names are populated
        if not TRIGGER_NAMES:
            result.add_error("TRIGGER_NAMES is empty")
            return result

        # Check that at least some conditions exist
        if not CONDITION_NAMES:
            result.add_error("CONDITION_NAMES is empty")
            return result

        # Test opcode_name() function works
        for key in list(OPCODE_NAMES.keys())[:5]:  # Test first 5
            name = opcode_name(key)
            if name is None or name == "":
                result.add_error(f"opcode_name({key}) returned empty/None")
                return result

        # Test trigger_name() function works
        for key in list(TRIGGER_NAMES.keys())[:5]:  # Test first 5
            name = trigger_name(key)
            if name is None or name == "":
                result.add_error(f"trigger_name({key}) returned empty/None")
                return result

        result.set_passed()
    except Exception as e:
        result.add_error(f"Naming consistency check raised exception: {e}")

    return result


def test_compiled_json_structure(compiled_json_path: str) -> List[ParityTestResult]:
    """Test that compiled JSON file has proper version markers."""
    results = []
    result = ParityTestResult(f"Compiled JSON structure: {compiled_json_path}")

    try:
        if not os.path.exists(compiled_json_path):
            result.add_error(f"Compiled JSON not found: {compiled_json_path}")
            results.append(result)
            return results

        with open(compiled_json_path, "r", encoding="utf-8") as f:
            data = json.load(f)

        if "meta" not in data:
            result.add_error("Compiled JSON missing 'meta' section")
            results.append(result)
            return results

        meta = data["meta"]
        required_meta_fields = [
            "bytecode_layout_version",
            "bytecode_layout_name",
            "semantic_form_version",
            "semantic_form_enabled",
        ]
        for field in required_meta_fields:
            if field not in meta:
                result.add_error(f"Meta section missing field: {field}")
                results.append(result)
                return results

        # Verify meta values match constants
        if meta["bytecode_layout_version"] != BYTECODE_LAYOUT_VERSION:
            result.add_error(
                f"Meta bytecode_layout_version {meta['bytecode_layout_version']} "
                f"!= {BYTECODE_LAYOUT_VERSION}"
            )
            results.append(result)
            return results

        if meta["semantic_form_version"] != SEMANTIC_FORM_VERSION:
            result.add_error(
                f"Meta semantic_form_version {meta['semantic_form_version']} "
                f"!= {SEMANTIC_FORM_VERSION}"
            )
            results.append(result)
            return results

        result.set_passed()
        results.append(result)

        # Now check individual card entries
        card_check_result = ParityTestResult("Compiled JSON card entries have version markers")
        
        # Check for cards in member_db, live_db, energy_db
        card_sources = [
            ("member_db", data.get("member_db", {})),
            ("live_db", data.get("live_db", {})),
            ("energy_db", data.get("energy_db", {})),
        ]
        
        card_count_checked = 0
        for source_name, source_data in card_sources:
            if card_count_checked >= 10:
                break

            for card_no, card_data in source_data.items():
                if card_count_checked >= 10:
                    break

                abilities = card_data.get("abilities", [])
                
                for ability in abilities:
                    if "semantic_form" in ability:
                        sf = ability["semantic_form"]
                        if "semantic_version" not in sf:
                            card_check_result.add_error(
                                f"{source_name}[{card_no}] ability missing "
                                f"semantic_version in semantic_form"
                            )
                            break
                        if "bytecode_layout_version" not in sf:
                            card_check_result.add_error(
                                f"{source_name}[{card_no}] ability missing "
                                f"bytecode_layout_version in semantic_form"
                            )
                            break

                card_count_checked += 1

        if not card_check_result.errors:
            card_check_result.set_passed()
        results.append(card_check_result)

    except Exception as e:
        result.add_error(f"JSON structure check raised exception: {e}")
        results.append(result)

    return results


def run_parity_tests(compiled_json_path: str = None) -> Tuple[int, int]:
    """

    Run all parity tests and report results.

    

    Returns: (passed_count, failed_count)

    """
    results = []

    print("\n" + "=" * 70)
    print("PARITY TESTS: IR <-> Bytecode <-> Readable Decode")
    print("=" * 70)

    # Test 1: Naming consistency
    print("\n[1] Testing naming consistency...")
    results.append(test_naming_consistency())

    # Test 2: Load a few sample abilities to test
    print("[2] Testing with sample abilities...")
    try:
        # This is a simplified test - in production you'd load from compiled data
        # For now, we just test the framework
        sample_abilities = [
            # Example ability (simplified for testing)
            {
                "trigger": "ON_PLAY",
                "costs": [],
                "conditions": [],
                "effects": [],
                "instructions": [],
                "raw_text": "Test ability",
            }
        ]

        # Note: This is a placeholder. In real use, you'd load actual abilities
        # from the compiled JSON or from parsing
        print("  (Skipping real ability tests - use compiled JSON to validate)")

    except Exception as e:
        print(f"  ERROR loading sample abilities: {e}")

    # Test 3: Compiled JSON structure
    if compiled_json_path is None:
        # Try default locations
        possible_paths = [
            "data/cards_compiled.json",
            "engine/data/cards_compiled.json",
        ]
        for path in possible_paths:
            if os.path.exists(path):
                compiled_json_path = path
                break

    if compiled_json_path and os.path.exists(compiled_json_path):
        print(f"[3] Testing compiled JSON structure ({compiled_json_path})...")
        results.extend(test_compiled_json_structure(compiled_json_path))
    else:
        print("[3] Skipping compiled JSON tests (file not found)")

    # Print results
    print("\n" + "=" * 70)
    print("RESULTS")
    print("=" * 70)

    passed = 0
    failed = 0
    for result in results:
        print(result.summary())
        if result.passed:
            passed += 1
        else:
            failed += 1

    print("\n" + "=" * 70)
    print(f"Summary: {passed} passed, {failed} failed")
    print("=" * 70 + "\n")

    return passed, failed


if __name__ == "__main__":
    passed, failed = run_parity_tests()
    sys.exit(0 if failed == 0 else 1)