harness.pyΒΆ

Part of Tanks of Freedom.

  1"""Scripted runtime harness: drives the world headless and verifies state.
  2
  3Run:
  4    uv run python examples/ports/tanks_of_freedom/harness.py
  5
  6Validates:
  7- The iso tile map builds with the expected mode and bounds.
  8- The BFS pathfinder returns a viable path between two passable cells.
  9- The AP flood fill only reports in-bounds cells.
 10- Combat resolution actually deals damage.
 11- Turn manager alternates blue / red and counts full rounds.
 12- The world boots with all starting units / buildings.
 13"""
 14
 15from __future__ import annotations
 16
 17import sys
 18from pathlib import Path
 19
 20_PORT_DIR = Path(__file__).parent
 21if str(_PORT_DIR) not in sys.path:
 22    sys.path.insert(0, str(_PORT_DIR))
 23
 24from main import TanksRoot  # noqa: E402
 25from nodes.combat import resolve_attack  # noqa: E402
 26from nodes.data import (  # noqa: E402
 27    PLAYER_BLUE,
 28    PLAYER_RED,
 29    STARTING_BUILDINGS,
 30    STARTING_UNITS,
 31)
 32from nodes.pathfinder import find_path, reachable_cells  # noqa: E402
 33from nodes.tile_map import TankTileMap  # noqa: E402
 34from nodes.turn import TurnManager  # noqa: E402
 35
 36from simvx.core.testing import SceneRunner  # noqa: E402
 37
 38
 39def main() -> int:
 40    print("--- Tanks of Freedom port harness ---")
 41
 42    # 1) Tile map terrain layout
 43    tm = TankTileMap()
 44    assert tm.mode == "isometric"
 45    assert tm.in_bounds(0, 0)
 46    assert not tm.in_bounds(99, 99)
 47    print(f"OK  tile map ready in mode={tm.mode!r} cell_size={tm.cell_size}")
 48
 49    # 2) Pathfinder finds a route between two passable cells
 50    def passable(x, y):
 51        return tm.is_passable(x, y, is_air=False)
 52
 53    def unblocked(x, y):
 54        return False
 55
 56    path = find_path((1, 10), (10, 1), passable=passable, blocked=unblocked)
 57    assert path, "no path blue-HQ -> red-HQ"
 58    assert path[0] == (1, 10) and path[-1] == (10, 1)
 59    print(f"OK  ground path length={len(path)}")
 60
 61    # 3) Reachable flood works with AP=4
 62    reach = reachable_cells((2, 10), 4, passable=passable, blocked=unblocked)
 63    assert (2, 10) in reach, "start cell missing from reachable set"
 64    assert all(0 <= c[0] < 12 and 0 <= c[1] < 12 for c in reach)
 65    print(f"OK  reachable from (2,10) AP=4 -> {len(reach)} cells")
 66
 67    # 4) Combat resolves with damage applied
 68    class _Stub:
 69        def __init__(self, type_, hp, max_hp):
 70            self.type = type_
 71            self.life = hp
 72            self.max_life = max_hp
 73
 74    a = _Stub(1, 15, 15)  # tank
 75    b = _Stub(0, 10, 10)  # soldier
 76    pre = b.life
 77    resolve_attack(a, b)
 78    assert b.life < pre
 79    print(f"OK  combat: tank vs soldier, soldier hp {pre}->{b.life}")
 80
 81    # 5) Turn manager alternates and refreshes turn counter every two passes
 82    tm_state = TurnManager()
 83    assert tm_state.current == PLAYER_BLUE
 84    tm_state.end_turn()
 85    assert tm_state.current == PLAYER_RED
 86    tm_state.end_turn()
 87    assert tm_state.current == PLAYER_BLUE
 88    assert tm_state.turn_number == 2
 89    print("OK  turn manager alternates blue->red->blue, turn_number 2")
 90
 91    # 6) End-to-end scene boot via SceneRunner
 92    runner = SceneRunner()
 93    runner.load(TanksRoot(autostart=True))
 94    runner.advance_frames(60)
 95    root = runner.root
 96    world = root.children[0]
 97    assert len(world.units) == len(STARTING_UNITS)
 98    assert len(world.buildings) == len(STARTING_BUILDINGS)
 99    print(f"OK  world boots: {len(world.units)} units, {len(world.buildings)} buildings")
100
101    print("--- ALL CHECKS PASSED ---")
102    return 0
103
104
105if __name__ == "__main__":
106    sys.exit(main())