BSP: add Binary Space Partitioning for procedural dungeon generation
Implements #202, #203, #204, #205; partially implements #206:
- BSP class: core tree structure with bounds, split_once, split_recursive, clear
- BSPNode class: lightweight node reference with bounds, level, is_leaf,
split_horizontal, split_position; navigation via left/right/parent/sibling;
contains() and center() methods
- Traversal enum: PRE_ORDER, IN_ORDER, POST_ORDER, LEVEL_ORDER, INVERTED_LEVEL_ORDER
- BSP iteration: leaves() for leaf nodes only, traverse(order) for all nodes
- BSP query: find(pos) returns deepest node containing position
- BSP.to_heightmap(): converts BSP to HeightMap with select, shrink, value options
Note: #206's BSPMap subclass deferred - to_heightmap returns plain HeightMap.
The HeightMap already has all necessary operations (inverse, threshold, etc.)
for procedural generation workflows.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:02:54 -05:00
|
|
|
"""Unit tests for BSP (Binary Space Partitioning) procedural generation.
|
|
|
|
|
|
|
|
|
|
Tests for issues #202-#206:
|
|
|
|
|
- #202: BSP core class with splitting
|
|
|
|
|
- #203: BSPNode lightweight node reference
|
|
|
|
|
- #204: BSP iteration (leaves, traverse) with Traversal enum
|
|
|
|
|
- #205: BSP query methods (find)
|
|
|
|
|
- #206: BSP.to_heightmap (returns HeightMap; BSPMap subclass deferred)
|
|
|
|
|
"""
|
|
|
|
|
import sys
|
|
|
|
|
import mcrfpy
|
|
|
|
|
|
|
|
|
|
def test_bsp_construction():
|
BSP: add safety features and API improvements (closes #202, #203, #204, #205, #206)
Safety improvements:
- Generation counter detects stale BSPNode references after clear()/split_recursive()
- GRID_MAX validation prevents oversized BSP trees
- Depth parameter capped at 16 to prevent resource exhaustion
- Iterator checks generation to detect invalidation during mutation
API improvements:
- Changed constructor from bounds=((x,y),(w,h)) to pos=(x,y), size=(w,h)
- Added pos and size properties alongside bounds
- BSPNode __eq__ compares underlying pointers for identity
- BSP __iter__ as shorthand for leaves()
- BSP __len__ returns leaf count
Tests:
- Added tests for stale node detection, GRID_MAX validation, depth cap
- Added tests for __len__, __iter__, and BSPNode equality
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:59:31 -05:00
|
|
|
"""Test BSP construction with pos/size."""
|
BSP: add Binary Space Partitioning for procedural dungeon generation
Implements #202, #203, #204, #205; partially implements #206:
- BSP class: core tree structure with bounds, split_once, split_recursive, clear
- BSPNode class: lightweight node reference with bounds, level, is_leaf,
split_horizontal, split_position; navigation via left/right/parent/sibling;
contains() and center() methods
- Traversal enum: PRE_ORDER, IN_ORDER, POST_ORDER, LEVEL_ORDER, INVERTED_LEVEL_ORDER
- BSP iteration: leaves() for leaf nodes only, traverse(order) for all nodes
- BSP query: find(pos) returns deepest node containing position
- BSP.to_heightmap(): converts BSP to HeightMap with select, shrink, value options
Note: #206's BSPMap subclass deferred - to_heightmap returns plain HeightMap.
The HeightMap already has all necessary operations (inverse, threshold, etc.)
for procedural generation workflows.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:02:54 -05:00
|
|
|
print("Testing BSP construction...")
|
|
|
|
|
|
BSP: add safety features and API improvements (closes #202, #203, #204, #205, #206)
Safety improvements:
- Generation counter detects stale BSPNode references after clear()/split_recursive()
- GRID_MAX validation prevents oversized BSP trees
- Depth parameter capped at 16 to prevent resource exhaustion
- Iterator checks generation to detect invalidation during mutation
API improvements:
- Changed constructor from bounds=((x,y),(w,h)) to pos=(x,y), size=(w,h)
- Added pos and size properties alongside bounds
- BSPNode __eq__ compares underlying pointers for identity
- BSP __iter__ as shorthand for leaves()
- BSP __len__ returns leaf count
Tests:
- Added tests for stale node detection, GRID_MAX validation, depth cap
- Added tests for __len__, __iter__, and BSPNode equality
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:59:31 -05:00
|
|
|
# Basic construction with pos/size kwargs
|
|
|
|
|
bsp = mcrfpy.BSP(pos=(0, 0), size=(100, 80))
|
BSP: add Binary Space Partitioning for procedural dungeon generation
Implements #202, #203, #204, #205; partially implements #206:
- BSP class: core tree structure with bounds, split_once, split_recursive, clear
- BSPNode class: lightweight node reference with bounds, level, is_leaf,
split_horizontal, split_position; navigation via left/right/parent/sibling;
contains() and center() methods
- Traversal enum: PRE_ORDER, IN_ORDER, POST_ORDER, LEVEL_ORDER, INVERTED_LEVEL_ORDER
- BSP iteration: leaves() for leaf nodes only, traverse(order) for all nodes
- BSP query: find(pos) returns deepest node containing position
- BSP.to_heightmap(): converts BSP to HeightMap with select, shrink, value options
Note: #206's BSPMap subclass deferred - to_heightmap returns plain HeightMap.
The HeightMap already has all necessary operations (inverse, threshold, etc.)
for procedural generation workflows.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:02:54 -05:00
|
|
|
assert bsp is not None, "BSP should be created"
|
|
|
|
|
|
BSP: add safety features and API improvements (closes #202, #203, #204, #205, #206)
Safety improvements:
- Generation counter detects stale BSPNode references after clear()/split_recursive()
- GRID_MAX validation prevents oversized BSP trees
- Depth parameter capped at 16 to prevent resource exhaustion
- Iterator checks generation to detect invalidation during mutation
API improvements:
- Changed constructor from bounds=((x,y),(w,h)) to pos=(x,y), size=(w,h)
- Added pos and size properties alongside bounds
- BSPNode __eq__ compares underlying pointers for identity
- BSP __iter__ as shorthand for leaves()
- BSP __len__ returns leaf count
Tests:
- Added tests for stale node detection, GRID_MAX validation, depth cap
- Added tests for __len__, __iter__, and BSPNode equality
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:59:31 -05:00
|
|
|
# Check pos and size properties
|
|
|
|
|
assert bsp.pos == (0, 0), f"pos should be (0, 0), got {bsp.pos}"
|
|
|
|
|
assert bsp.size == (100, 80), f"size should be (100, 80), got {bsp.size}"
|
|
|
|
|
|
|
|
|
|
# Check bounds property (combines pos and size)
|
BSP: add Binary Space Partitioning for procedural dungeon generation
Implements #202, #203, #204, #205; partially implements #206:
- BSP class: core tree structure with bounds, split_once, split_recursive, clear
- BSPNode class: lightweight node reference with bounds, level, is_leaf,
split_horizontal, split_position; navigation via left/right/parent/sibling;
contains() and center() methods
- Traversal enum: PRE_ORDER, IN_ORDER, POST_ORDER, LEVEL_ORDER, INVERTED_LEVEL_ORDER
- BSP iteration: leaves() for leaf nodes only, traverse(order) for all nodes
- BSP query: find(pos) returns deepest node containing position
- BSP.to_heightmap(): converts BSP to HeightMap with select, shrink, value options
Note: #206's BSPMap subclass deferred - to_heightmap returns plain HeightMap.
The HeightMap already has all necessary operations (inverse, threshold, etc.)
for procedural generation workflows.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:02:54 -05:00
|
|
|
bounds = bsp.bounds
|
|
|
|
|
assert bounds == ((0, 0), (100, 80)), f"Bounds should be ((0, 0), (100, 80)), got {bounds}"
|
|
|
|
|
|
|
|
|
|
# Check root property
|
|
|
|
|
root = bsp.root
|
|
|
|
|
assert root is not None, "Root should not be None"
|
|
|
|
|
assert root.bounds == ((0, 0), (100, 80)), f"Root bounds mismatch"
|
|
|
|
|
|
|
|
|
|
# Construction with offset
|
BSP: add safety features and API improvements (closes #202, #203, #204, #205, #206)
Safety improvements:
- Generation counter detects stale BSPNode references after clear()/split_recursive()
- GRID_MAX validation prevents oversized BSP trees
- Depth parameter capped at 16 to prevent resource exhaustion
- Iterator checks generation to detect invalidation during mutation
API improvements:
- Changed constructor from bounds=((x,y),(w,h)) to pos=(x,y), size=(w,h)
- Added pos and size properties alongside bounds
- BSPNode __eq__ compares underlying pointers for identity
- BSP __iter__ as shorthand for leaves()
- BSP __len__ returns leaf count
Tests:
- Added tests for stale node detection, GRID_MAX validation, depth cap
- Added tests for __len__, __iter__, and BSPNode equality
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:59:31 -05:00
|
|
|
bsp2 = mcrfpy.BSP(pos=(10, 20), size=(50, 40))
|
BSP: add Binary Space Partitioning for procedural dungeon generation
Implements #202, #203, #204, #205; partially implements #206:
- BSP class: core tree structure with bounds, split_once, split_recursive, clear
- BSPNode class: lightweight node reference with bounds, level, is_leaf,
split_horizontal, split_position; navigation via left/right/parent/sibling;
contains() and center() methods
- Traversal enum: PRE_ORDER, IN_ORDER, POST_ORDER, LEVEL_ORDER, INVERTED_LEVEL_ORDER
- BSP iteration: leaves() for leaf nodes only, traverse(order) for all nodes
- BSP query: find(pos) returns deepest node containing position
- BSP.to_heightmap(): converts BSP to HeightMap with select, shrink, value options
Note: #206's BSPMap subclass deferred - to_heightmap returns plain HeightMap.
The HeightMap already has all necessary operations (inverse, threshold, etc.)
for procedural generation workflows.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:02:54 -05:00
|
|
|
assert bsp2.bounds == ((10, 20), (50, 40)), "Offset bounds not preserved"
|
|
|
|
|
|
|
|
|
|
print(" BSP construction: PASS")
|
|
|
|
|
|
|
|
|
|
def test_bsp_split_once():
|
|
|
|
|
"""Test single split operation (#202)."""
|
|
|
|
|
print("Testing BSP split_once...")
|
|
|
|
|
|
BSP: add safety features and API improvements (closes #202, #203, #204, #205, #206)
Safety improvements:
- Generation counter detects stale BSPNode references after clear()/split_recursive()
- GRID_MAX validation prevents oversized BSP trees
- Depth parameter capped at 16 to prevent resource exhaustion
- Iterator checks generation to detect invalidation during mutation
API improvements:
- Changed constructor from bounds=((x,y),(w,h)) to pos=(x,y), size=(w,h)
- Added pos and size properties alongside bounds
- BSPNode __eq__ compares underlying pointers for identity
- BSP __iter__ as shorthand for leaves()
- BSP __len__ returns leaf count
Tests:
- Added tests for stale node detection, GRID_MAX validation, depth cap
- Added tests for __len__, __iter__, and BSPNode equality
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:59:31 -05:00
|
|
|
bsp = mcrfpy.BSP(pos=(0, 0), size=(100, 80))
|
BSP: add Binary Space Partitioning for procedural dungeon generation
Implements #202, #203, #204, #205; partially implements #206:
- BSP class: core tree structure with bounds, split_once, split_recursive, clear
- BSPNode class: lightweight node reference with bounds, level, is_leaf,
split_horizontal, split_position; navigation via left/right/parent/sibling;
contains() and center() methods
- Traversal enum: PRE_ORDER, IN_ORDER, POST_ORDER, LEVEL_ORDER, INVERTED_LEVEL_ORDER
- BSP iteration: leaves() for leaf nodes only, traverse(order) for all nodes
- BSP query: find(pos) returns deepest node containing position
- BSP.to_heightmap(): converts BSP to HeightMap with select, shrink, value options
Note: #206's BSPMap subclass deferred - to_heightmap returns plain HeightMap.
The HeightMap already has all necessary operations (inverse, threshold, etc.)
for procedural generation workflows.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:02:54 -05:00
|
|
|
|
|
|
|
|
# Before split, root should be a leaf
|
|
|
|
|
assert bsp.root.is_leaf, "Root should be leaf before split"
|
|
|
|
|
|
|
|
|
|
# Horizontal split at y=40
|
|
|
|
|
result = bsp.split_once(horizontal=True, position=40)
|
|
|
|
|
assert result is bsp, "split_once should return self for chaining"
|
|
|
|
|
|
|
|
|
|
# After split, root should not be a leaf
|
|
|
|
|
root = bsp.root
|
|
|
|
|
assert not root.is_leaf, "Root should not be leaf after split"
|
|
|
|
|
assert root.split_horizontal == True, "Split should be horizontal"
|
|
|
|
|
assert root.split_position == 40, "Split position should be 40"
|
|
|
|
|
|
|
|
|
|
# Check children exist
|
|
|
|
|
left = root.left
|
|
|
|
|
right = root.right
|
|
|
|
|
assert left is not None, "Left child should exist"
|
|
|
|
|
assert right is not None, "Right child should exist"
|
|
|
|
|
assert left.is_leaf, "Left child should be leaf"
|
|
|
|
|
assert right.is_leaf, "Right child should be leaf"
|
|
|
|
|
|
|
|
|
|
print(" BSP split_once: PASS")
|
|
|
|
|
|
|
|
|
|
def test_bsp_split_recursive():
|
|
|
|
|
"""Test recursive splitting (#202)."""
|
|
|
|
|
print("Testing BSP split_recursive...")
|
|
|
|
|
|
BSP: add safety features and API improvements (closes #202, #203, #204, #205, #206)
Safety improvements:
- Generation counter detects stale BSPNode references after clear()/split_recursive()
- GRID_MAX validation prevents oversized BSP trees
- Depth parameter capped at 16 to prevent resource exhaustion
- Iterator checks generation to detect invalidation during mutation
API improvements:
- Changed constructor from bounds=((x,y),(w,h)) to pos=(x,y), size=(w,h)
- Added pos and size properties alongside bounds
- BSPNode __eq__ compares underlying pointers for identity
- BSP __iter__ as shorthand for leaves()
- BSP __len__ returns leaf count
Tests:
- Added tests for stale node detection, GRID_MAX validation, depth cap
- Added tests for __len__, __iter__, and BSPNode equality
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:59:31 -05:00
|
|
|
bsp = mcrfpy.BSP(pos=(0, 0), size=(80, 60))
|
BSP: add Binary Space Partitioning for procedural dungeon generation
Implements #202, #203, #204, #205; partially implements #206:
- BSP class: core tree structure with bounds, split_once, split_recursive, clear
- BSPNode class: lightweight node reference with bounds, level, is_leaf,
split_horizontal, split_position; navigation via left/right/parent/sibling;
contains() and center() methods
- Traversal enum: PRE_ORDER, IN_ORDER, POST_ORDER, LEVEL_ORDER, INVERTED_LEVEL_ORDER
- BSP iteration: leaves() for leaf nodes only, traverse(order) for all nodes
- BSP query: find(pos) returns deepest node containing position
- BSP.to_heightmap(): converts BSP to HeightMap with select, shrink, value options
Note: #206's BSPMap subclass deferred - to_heightmap returns plain HeightMap.
The HeightMap already has all necessary operations (inverse, threshold, etc.)
for procedural generation workflows.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:02:54 -05:00
|
|
|
|
|
|
|
|
# Recursive split with seed for reproducibility
|
|
|
|
|
result = bsp.split_recursive(depth=3, min_size=(8, 8), max_ratio=1.5, seed=42)
|
|
|
|
|
assert result is bsp, "split_recursive should return self"
|
|
|
|
|
|
|
|
|
|
# Count leaves
|
|
|
|
|
leaves = list(bsp.leaves())
|
|
|
|
|
assert len(leaves) > 1, f"Should have multiple leaves, got {len(leaves)}"
|
|
|
|
|
assert len(leaves) <= 8, f"Should have at most 2^3=8 leaves, got {len(leaves)}"
|
|
|
|
|
|
|
|
|
|
# All leaves should be within bounds
|
|
|
|
|
for leaf in leaves:
|
|
|
|
|
x, y = leaf.bounds[0]
|
|
|
|
|
w, h = leaf.bounds[1]
|
|
|
|
|
assert x >= 0 and y >= 0, f"Leaf position out of bounds: {leaf.bounds}"
|
|
|
|
|
assert x + w <= 80 and y + h <= 60, f"Leaf extends beyond bounds: {leaf.bounds}"
|
|
|
|
|
assert w >= 8 and h >= 8, f"Leaf smaller than min_size: {leaf.bounds}"
|
|
|
|
|
|
|
|
|
|
print(" BSP split_recursive: PASS")
|
|
|
|
|
|
|
|
|
|
def test_bsp_clear():
|
|
|
|
|
"""Test clear operation (#202)."""
|
|
|
|
|
print("Testing BSP clear...")
|
|
|
|
|
|
BSP: add safety features and API improvements (closes #202, #203, #204, #205, #206)
Safety improvements:
- Generation counter detects stale BSPNode references after clear()/split_recursive()
- GRID_MAX validation prevents oversized BSP trees
- Depth parameter capped at 16 to prevent resource exhaustion
- Iterator checks generation to detect invalidation during mutation
API improvements:
- Changed constructor from bounds=((x,y),(w,h)) to pos=(x,y), size=(w,h)
- Added pos and size properties alongside bounds
- BSPNode __eq__ compares underlying pointers for identity
- BSP __iter__ as shorthand for leaves()
- BSP __len__ returns leaf count
Tests:
- Added tests for stale node detection, GRID_MAX validation, depth cap
- Added tests for __len__, __iter__, and BSPNode equality
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:59:31 -05:00
|
|
|
bsp = mcrfpy.BSP(pos=(0, 0), size=(100, 80))
|
BSP: add Binary Space Partitioning for procedural dungeon generation
Implements #202, #203, #204, #205; partially implements #206:
- BSP class: core tree structure with bounds, split_once, split_recursive, clear
- BSPNode class: lightweight node reference with bounds, level, is_leaf,
split_horizontal, split_position; navigation via left/right/parent/sibling;
contains() and center() methods
- Traversal enum: PRE_ORDER, IN_ORDER, POST_ORDER, LEVEL_ORDER, INVERTED_LEVEL_ORDER
- BSP iteration: leaves() for leaf nodes only, traverse(order) for all nodes
- BSP query: find(pos) returns deepest node containing position
- BSP.to_heightmap(): converts BSP to HeightMap with select, shrink, value options
Note: #206's BSPMap subclass deferred - to_heightmap returns plain HeightMap.
The HeightMap already has all necessary operations (inverse, threshold, etc.)
for procedural generation workflows.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:02:54 -05:00
|
|
|
bsp.split_recursive(depth=4, min_size=(8, 8), seed=42)
|
|
|
|
|
|
|
|
|
|
# Should have multiple leaves
|
|
|
|
|
leaves_before = len(list(bsp.leaves()))
|
|
|
|
|
assert leaves_before > 1, "Should have multiple leaves after split"
|
|
|
|
|
|
|
|
|
|
# Clear
|
|
|
|
|
result = bsp.clear()
|
|
|
|
|
assert result is bsp, "clear should return self"
|
|
|
|
|
|
|
|
|
|
# Should be back to single leaf
|
|
|
|
|
leaves_after = list(bsp.leaves())
|
|
|
|
|
assert len(leaves_after) == 1, f"Should have 1 leaf after clear, got {len(leaves_after)}"
|
|
|
|
|
|
|
|
|
|
# Root should be a leaf with original bounds
|
|
|
|
|
assert bsp.root.is_leaf, "Root should be leaf after clear"
|
|
|
|
|
assert bsp.bounds == ((0, 0), (100, 80)), "Bounds should be restored"
|
|
|
|
|
|
|
|
|
|
print(" BSP clear: PASS")
|
|
|
|
|
|
|
|
|
|
def test_bspnode_properties():
|
|
|
|
|
"""Test BSPNode properties (#203)."""
|
|
|
|
|
print("Testing BSPNode properties...")
|
|
|
|
|
|
BSP: add safety features and API improvements (closes #202, #203, #204, #205, #206)
Safety improvements:
- Generation counter detects stale BSPNode references after clear()/split_recursive()
- GRID_MAX validation prevents oversized BSP trees
- Depth parameter capped at 16 to prevent resource exhaustion
- Iterator checks generation to detect invalidation during mutation
API improvements:
- Changed constructor from bounds=((x,y),(w,h)) to pos=(x,y), size=(w,h)
- Added pos and size properties alongside bounds
- BSPNode __eq__ compares underlying pointers for identity
- BSP __iter__ as shorthand for leaves()
- BSP __len__ returns leaf count
Tests:
- Added tests for stale node detection, GRID_MAX validation, depth cap
- Added tests for __len__, __iter__, and BSPNode equality
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:59:31 -05:00
|
|
|
bsp = mcrfpy.BSP(pos=(0, 0), size=(100, 80))
|
BSP: add Binary Space Partitioning for procedural dungeon generation
Implements #202, #203, #204, #205; partially implements #206:
- BSP class: core tree structure with bounds, split_once, split_recursive, clear
- BSPNode class: lightweight node reference with bounds, level, is_leaf,
split_horizontal, split_position; navigation via left/right/parent/sibling;
contains() and center() methods
- Traversal enum: PRE_ORDER, IN_ORDER, POST_ORDER, LEVEL_ORDER, INVERTED_LEVEL_ORDER
- BSP iteration: leaves() for leaf nodes only, traverse(order) for all nodes
- BSP query: find(pos) returns deepest node containing position
- BSP.to_heightmap(): converts BSP to HeightMap with select, shrink, value options
Note: #206's BSPMap subclass deferred - to_heightmap returns plain HeightMap.
The HeightMap already has all necessary operations (inverse, threshold, etc.)
for procedural generation workflows.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:02:54 -05:00
|
|
|
bsp.split_recursive(depth=3, min_size=(8, 8), seed=42)
|
|
|
|
|
|
|
|
|
|
root = bsp.root
|
|
|
|
|
|
|
|
|
|
# Root properties
|
|
|
|
|
assert root.level == 0, f"Root level should be 0, got {root.level}"
|
|
|
|
|
assert root.parent is None, "Root parent should be None"
|
|
|
|
|
assert not root.is_leaf, "Split root should not be leaf"
|
|
|
|
|
|
|
|
|
|
# Split properties (not leaf)
|
|
|
|
|
assert root.split_horizontal is not None, "Split horizontal should be bool for non-leaf"
|
|
|
|
|
assert root.split_position is not None, "Split position should be int for non-leaf"
|
|
|
|
|
|
|
|
|
|
# Navigate to a leaf
|
|
|
|
|
current = root
|
|
|
|
|
while not current.is_leaf:
|
|
|
|
|
current = current.left
|
|
|
|
|
|
|
|
|
|
# Leaf properties
|
|
|
|
|
assert current.is_leaf, "Should be a leaf"
|
|
|
|
|
assert current.split_horizontal is None, "Leaf split_horizontal should be None"
|
|
|
|
|
assert current.split_position is None, "Leaf split_position should be None"
|
|
|
|
|
assert current.level > 0, "Leaf level should be > 0"
|
|
|
|
|
|
|
|
|
|
# Test center method
|
|
|
|
|
bounds = current.bounds
|
|
|
|
|
cx, cy = current.center()
|
|
|
|
|
expected_cx = bounds[0][0] + bounds[1][0] // 2
|
|
|
|
|
expected_cy = bounds[0][1] + bounds[1][1] // 2
|
|
|
|
|
assert cx == expected_cx, f"Center x mismatch: {cx} != {expected_cx}"
|
|
|
|
|
assert cy == expected_cy, f"Center y mismatch: {cy} != {expected_cy}"
|
|
|
|
|
|
|
|
|
|
print(" BSPNode properties: PASS")
|
|
|
|
|
|
|
|
|
|
def test_bspnode_navigation():
|
|
|
|
|
"""Test BSPNode navigation (#203)."""
|
|
|
|
|
print("Testing BSPNode navigation...")
|
|
|
|
|
|
BSP: add safety features and API improvements (closes #202, #203, #204, #205, #206)
Safety improvements:
- Generation counter detects stale BSPNode references after clear()/split_recursive()
- GRID_MAX validation prevents oversized BSP trees
- Depth parameter capped at 16 to prevent resource exhaustion
- Iterator checks generation to detect invalidation during mutation
API improvements:
- Changed constructor from bounds=((x,y),(w,h)) to pos=(x,y), size=(w,h)
- Added pos and size properties alongside bounds
- BSPNode __eq__ compares underlying pointers for identity
- BSP __iter__ as shorthand for leaves()
- BSP __len__ returns leaf count
Tests:
- Added tests for stale node detection, GRID_MAX validation, depth cap
- Added tests for __len__, __iter__, and BSPNode equality
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:59:31 -05:00
|
|
|
bsp = mcrfpy.BSP(pos=(0, 0), size=(100, 80))
|
BSP: add Binary Space Partitioning for procedural dungeon generation
Implements #202, #203, #204, #205; partially implements #206:
- BSP class: core tree structure with bounds, split_once, split_recursive, clear
- BSPNode class: lightweight node reference with bounds, level, is_leaf,
split_horizontal, split_position; navigation via left/right/parent/sibling;
contains() and center() methods
- Traversal enum: PRE_ORDER, IN_ORDER, POST_ORDER, LEVEL_ORDER, INVERTED_LEVEL_ORDER
- BSP iteration: leaves() for leaf nodes only, traverse(order) for all nodes
- BSP query: find(pos) returns deepest node containing position
- BSP.to_heightmap(): converts BSP to HeightMap with select, shrink, value options
Note: #206's BSPMap subclass deferred - to_heightmap returns plain HeightMap.
The HeightMap already has all necessary operations (inverse, threshold, etc.)
for procedural generation workflows.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:02:54 -05:00
|
|
|
bsp.split_once(horizontal=True, position=40)
|
|
|
|
|
|
|
|
|
|
root = bsp.root
|
|
|
|
|
left = root.left
|
|
|
|
|
right = root.right
|
|
|
|
|
|
|
|
|
|
# Parent navigation
|
|
|
|
|
assert left.parent is not None, "Left parent should exist"
|
|
|
|
|
assert left.parent.bounds == root.bounds, "Left parent should be root"
|
|
|
|
|
|
|
|
|
|
# Sibling navigation
|
|
|
|
|
assert left.sibling is not None, "Left sibling should exist"
|
|
|
|
|
assert left.sibling.bounds == right.bounds, "Left sibling should be right"
|
|
|
|
|
assert right.sibling is not None, "Right sibling should exist"
|
|
|
|
|
assert right.sibling.bounds == left.bounds, "Right sibling should be left"
|
|
|
|
|
|
|
|
|
|
# Root has no parent or sibling
|
|
|
|
|
assert root.parent is None, "Root parent should be None"
|
|
|
|
|
assert root.sibling is None, "Root sibling should be None"
|
|
|
|
|
|
|
|
|
|
print(" BSPNode navigation: PASS")
|
|
|
|
|
|
|
|
|
|
def test_bspnode_contains():
|
|
|
|
|
"""Test BSPNode contains method (#203)."""
|
|
|
|
|
print("Testing BSPNode contains...")
|
|
|
|
|
|
BSP: add safety features and API improvements (closes #202, #203, #204, #205, #206)
Safety improvements:
- Generation counter detects stale BSPNode references after clear()/split_recursive()
- GRID_MAX validation prevents oversized BSP trees
- Depth parameter capped at 16 to prevent resource exhaustion
- Iterator checks generation to detect invalidation during mutation
API improvements:
- Changed constructor from bounds=((x,y),(w,h)) to pos=(x,y), size=(w,h)
- Added pos and size properties alongside bounds
- BSPNode __eq__ compares underlying pointers for identity
- BSP __iter__ as shorthand for leaves()
- BSP __len__ returns leaf count
Tests:
- Added tests for stale node detection, GRID_MAX validation, depth cap
- Added tests for __len__, __iter__, and BSPNode equality
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:59:31 -05:00
|
|
|
bsp = mcrfpy.BSP(pos=(10, 20), size=(50, 40)) # x: 10-60, y: 20-60
|
BSP: add Binary Space Partitioning for procedural dungeon generation
Implements #202, #203, #204, #205; partially implements #206:
- BSP class: core tree structure with bounds, split_once, split_recursive, clear
- BSPNode class: lightweight node reference with bounds, level, is_leaf,
split_horizontal, split_position; navigation via left/right/parent/sibling;
contains() and center() methods
- Traversal enum: PRE_ORDER, IN_ORDER, POST_ORDER, LEVEL_ORDER, INVERTED_LEVEL_ORDER
- BSP iteration: leaves() for leaf nodes only, traverse(order) for all nodes
- BSP query: find(pos) returns deepest node containing position
- BSP.to_heightmap(): converts BSP to HeightMap with select, shrink, value options
Note: #206's BSPMap subclass deferred - to_heightmap returns plain HeightMap.
The HeightMap already has all necessary operations (inverse, threshold, etc.)
for procedural generation workflows.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:02:54 -05:00
|
|
|
|
|
|
|
|
root = bsp.root
|
|
|
|
|
|
|
|
|
|
# Inside
|
|
|
|
|
assert root.contains((30, 40)), "Center should be inside"
|
|
|
|
|
assert root.contains((10, 20)), "Top-left corner should be inside"
|
|
|
|
|
assert root.contains((59, 59)), "Near bottom-right should be inside"
|
|
|
|
|
|
|
|
|
|
# Outside
|
|
|
|
|
assert not root.contains((5, 40)), "Left of bounds should be outside"
|
|
|
|
|
assert not root.contains((65, 40)), "Right of bounds should be outside"
|
|
|
|
|
assert not root.contains((30, 15)), "Above bounds should be outside"
|
|
|
|
|
assert not root.contains((30, 65)), "Below bounds should be outside"
|
|
|
|
|
|
|
|
|
|
print(" BSPNode contains: PASS")
|
|
|
|
|
|
|
|
|
|
def test_bsp_leaves_iteration():
|
|
|
|
|
"""Test leaves iteration (#204)."""
|
|
|
|
|
print("Testing BSP leaves iteration...")
|
|
|
|
|
|
BSP: add safety features and API improvements (closes #202, #203, #204, #205, #206)
Safety improvements:
- Generation counter detects stale BSPNode references after clear()/split_recursive()
- GRID_MAX validation prevents oversized BSP trees
- Depth parameter capped at 16 to prevent resource exhaustion
- Iterator checks generation to detect invalidation during mutation
API improvements:
- Changed constructor from bounds=((x,y),(w,h)) to pos=(x,y), size=(w,h)
- Added pos and size properties alongside bounds
- BSPNode __eq__ compares underlying pointers for identity
- BSP __iter__ as shorthand for leaves()
- BSP __len__ returns leaf count
Tests:
- Added tests for stale node detection, GRID_MAX validation, depth cap
- Added tests for __len__, __iter__, and BSPNode equality
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:59:31 -05:00
|
|
|
bsp = mcrfpy.BSP(pos=(0, 0), size=(80, 60))
|
BSP: add Binary Space Partitioning for procedural dungeon generation
Implements #202, #203, #204, #205; partially implements #206:
- BSP class: core tree structure with bounds, split_once, split_recursive, clear
- BSPNode class: lightweight node reference with bounds, level, is_leaf,
split_horizontal, split_position; navigation via left/right/parent/sibling;
contains() and center() methods
- Traversal enum: PRE_ORDER, IN_ORDER, POST_ORDER, LEVEL_ORDER, INVERTED_LEVEL_ORDER
- BSP iteration: leaves() for leaf nodes only, traverse(order) for all nodes
- BSP query: find(pos) returns deepest node containing position
- BSP.to_heightmap(): converts BSP to HeightMap with select, shrink, value options
Note: #206's BSPMap subclass deferred - to_heightmap returns plain HeightMap.
The HeightMap already has all necessary operations (inverse, threshold, etc.)
for procedural generation workflows.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:02:54 -05:00
|
|
|
bsp.split_recursive(depth=3, min_size=(8, 8), seed=42)
|
|
|
|
|
|
|
|
|
|
# Iterate leaves
|
|
|
|
|
leaves = list(bsp.leaves())
|
|
|
|
|
assert len(leaves) > 0, "Should have at least one leaf"
|
|
|
|
|
|
|
|
|
|
# All should be leaves
|
|
|
|
|
for leaf in leaves:
|
|
|
|
|
assert leaf.is_leaf, f"Should be leaf: {leaf}"
|
|
|
|
|
|
|
|
|
|
# Can convert to list multiple times
|
|
|
|
|
leaves2 = list(bsp.leaves())
|
|
|
|
|
assert len(leaves) == len(leaves2), "Multiple iterations should yield same count"
|
|
|
|
|
|
|
|
|
|
print(" BSP leaves iteration: PASS")
|
|
|
|
|
|
|
|
|
|
def test_bsp_traverse():
|
|
|
|
|
"""Test traverse with different orders (#204)."""
|
|
|
|
|
print("Testing BSP traverse...")
|
|
|
|
|
|
BSP: add safety features and API improvements (closes #202, #203, #204, #205, #206)
Safety improvements:
- Generation counter detects stale BSPNode references after clear()/split_recursive()
- GRID_MAX validation prevents oversized BSP trees
- Depth parameter capped at 16 to prevent resource exhaustion
- Iterator checks generation to detect invalidation during mutation
API improvements:
- Changed constructor from bounds=((x,y),(w,h)) to pos=(x,y), size=(w,h)
- Added pos and size properties alongside bounds
- BSPNode __eq__ compares underlying pointers for identity
- BSP __iter__ as shorthand for leaves()
- BSP __len__ returns leaf count
Tests:
- Added tests for stale node detection, GRID_MAX validation, depth cap
- Added tests for __len__, __iter__, and BSPNode equality
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:59:31 -05:00
|
|
|
bsp = mcrfpy.BSP(pos=(0, 0), size=(80, 60))
|
BSP: add Binary Space Partitioning for procedural dungeon generation
Implements #202, #203, #204, #205; partially implements #206:
- BSP class: core tree structure with bounds, split_once, split_recursive, clear
- BSPNode class: lightweight node reference with bounds, level, is_leaf,
split_horizontal, split_position; navigation via left/right/parent/sibling;
contains() and center() methods
- Traversal enum: PRE_ORDER, IN_ORDER, POST_ORDER, LEVEL_ORDER, INVERTED_LEVEL_ORDER
- BSP iteration: leaves() for leaf nodes only, traverse(order) for all nodes
- BSP query: find(pos) returns deepest node containing position
- BSP.to_heightmap(): converts BSP to HeightMap with select, shrink, value options
Note: #206's BSPMap subclass deferred - to_heightmap returns plain HeightMap.
The HeightMap already has all necessary operations (inverse, threshold, etc.)
for procedural generation workflows.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:02:54 -05:00
|
|
|
bsp.split_recursive(depth=2, min_size=(8, 8), seed=42)
|
|
|
|
|
|
|
|
|
|
# Test all traversal orders
|
|
|
|
|
orders = [
|
|
|
|
|
mcrfpy.Traversal.PRE_ORDER,
|
|
|
|
|
mcrfpy.Traversal.IN_ORDER,
|
|
|
|
|
mcrfpy.Traversal.POST_ORDER,
|
|
|
|
|
mcrfpy.Traversal.LEVEL_ORDER,
|
|
|
|
|
mcrfpy.Traversal.INVERTED_LEVEL_ORDER,
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
for order in orders:
|
|
|
|
|
nodes = list(bsp.traverse(order=order))
|
|
|
|
|
assert len(nodes) > 0, f"Should have nodes for {order}"
|
|
|
|
|
# All traversals should visit same number of nodes
|
|
|
|
|
assert len(nodes) == len(list(bsp.traverse())), f"Node count mismatch for {order}"
|
|
|
|
|
|
|
|
|
|
# Default should be LEVEL_ORDER
|
|
|
|
|
default_nodes = list(bsp.traverse())
|
|
|
|
|
level_nodes = list(bsp.traverse(mcrfpy.Traversal.LEVEL_ORDER))
|
|
|
|
|
assert len(default_nodes) == len(level_nodes), "Default should match LEVEL_ORDER"
|
|
|
|
|
|
|
|
|
|
# PRE_ORDER: root first
|
|
|
|
|
pre_nodes = list(bsp.traverse(mcrfpy.Traversal.PRE_ORDER))
|
|
|
|
|
assert pre_nodes[0].bounds == bsp.root.bounds, "PRE_ORDER should start with root"
|
|
|
|
|
|
|
|
|
|
# POST_ORDER: root last
|
|
|
|
|
post_nodes = list(bsp.traverse(mcrfpy.Traversal.POST_ORDER))
|
|
|
|
|
assert post_nodes[-1].bounds == bsp.root.bounds, "POST_ORDER should end with root"
|
|
|
|
|
|
|
|
|
|
print(" BSP traverse: PASS")
|
|
|
|
|
|
|
|
|
|
def test_bsp_find():
|
|
|
|
|
"""Test find method (#205)."""
|
|
|
|
|
print("Testing BSP find...")
|
|
|
|
|
|
BSP: add safety features and API improvements (closes #202, #203, #204, #205, #206)
Safety improvements:
- Generation counter detects stale BSPNode references after clear()/split_recursive()
- GRID_MAX validation prevents oversized BSP trees
- Depth parameter capped at 16 to prevent resource exhaustion
- Iterator checks generation to detect invalidation during mutation
API improvements:
- Changed constructor from bounds=((x,y),(w,h)) to pos=(x,y), size=(w,h)
- Added pos and size properties alongside bounds
- BSPNode __eq__ compares underlying pointers for identity
- BSP __iter__ as shorthand for leaves()
- BSP __len__ returns leaf count
Tests:
- Added tests for stale node detection, GRID_MAX validation, depth cap
- Added tests for __len__, __iter__, and BSPNode equality
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:59:31 -05:00
|
|
|
bsp = mcrfpy.BSP(pos=(0, 0), size=(80, 60))
|
BSP: add Binary Space Partitioning for procedural dungeon generation
Implements #202, #203, #204, #205; partially implements #206:
- BSP class: core tree structure with bounds, split_once, split_recursive, clear
- BSPNode class: lightweight node reference with bounds, level, is_leaf,
split_horizontal, split_position; navigation via left/right/parent/sibling;
contains() and center() methods
- Traversal enum: PRE_ORDER, IN_ORDER, POST_ORDER, LEVEL_ORDER, INVERTED_LEVEL_ORDER
- BSP iteration: leaves() for leaf nodes only, traverse(order) for all nodes
- BSP query: find(pos) returns deepest node containing position
- BSP.to_heightmap(): converts BSP to HeightMap with select, shrink, value options
Note: #206's BSPMap subclass deferred - to_heightmap returns plain HeightMap.
The HeightMap already has all necessary operations (inverse, threshold, etc.)
for procedural generation workflows.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:02:54 -05:00
|
|
|
bsp.split_recursive(depth=3, min_size=(8, 8), seed=42)
|
|
|
|
|
|
|
|
|
|
# Find a point inside bounds
|
|
|
|
|
node = bsp.find((40, 30))
|
|
|
|
|
assert node is not None, "Should find node for point inside"
|
|
|
|
|
assert node.is_leaf, "Found node should be a leaf (deepest)"
|
|
|
|
|
assert node.contains((40, 30)), "Found node should contain the point"
|
|
|
|
|
|
|
|
|
|
# Find at corner
|
|
|
|
|
corner_node = bsp.find((0, 0))
|
|
|
|
|
assert corner_node is not None, "Should find node at corner"
|
|
|
|
|
assert corner_node.contains((0, 0)), "Corner node should contain (0,0)"
|
|
|
|
|
|
|
|
|
|
# Find outside bounds
|
|
|
|
|
outside = bsp.find((100, 100))
|
|
|
|
|
assert outside is None, "Should return None for point outside"
|
|
|
|
|
|
|
|
|
|
print(" BSP find: PASS")
|
|
|
|
|
|
|
|
|
|
def test_bsp_to_heightmap():
|
|
|
|
|
"""Test to_heightmap conversion (#206)."""
|
|
|
|
|
print("Testing BSP to_heightmap...")
|
|
|
|
|
|
BSP: add safety features and API improvements (closes #202, #203, #204, #205, #206)
Safety improvements:
- Generation counter detects stale BSPNode references after clear()/split_recursive()
- GRID_MAX validation prevents oversized BSP trees
- Depth parameter capped at 16 to prevent resource exhaustion
- Iterator checks generation to detect invalidation during mutation
API improvements:
- Changed constructor from bounds=((x,y),(w,h)) to pos=(x,y), size=(w,h)
- Added pos and size properties alongside bounds
- BSPNode __eq__ compares underlying pointers for identity
- BSP __iter__ as shorthand for leaves()
- BSP __len__ returns leaf count
Tests:
- Added tests for stale node detection, GRID_MAX validation, depth cap
- Added tests for __len__, __iter__, and BSPNode equality
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:59:31 -05:00
|
|
|
bsp = mcrfpy.BSP(pos=(0, 0), size=(50, 40))
|
BSP: add Binary Space Partitioning for procedural dungeon generation
Implements #202, #203, #204, #205; partially implements #206:
- BSP class: core tree structure with bounds, split_once, split_recursive, clear
- BSPNode class: lightweight node reference with bounds, level, is_leaf,
split_horizontal, split_position; navigation via left/right/parent/sibling;
contains() and center() methods
- Traversal enum: PRE_ORDER, IN_ORDER, POST_ORDER, LEVEL_ORDER, INVERTED_LEVEL_ORDER
- BSP iteration: leaves() for leaf nodes only, traverse(order) for all nodes
- BSP query: find(pos) returns deepest node containing position
- BSP.to_heightmap(): converts BSP to HeightMap with select, shrink, value options
Note: #206's BSPMap subclass deferred - to_heightmap returns plain HeightMap.
The HeightMap already has all necessary operations (inverse, threshold, etc.)
for procedural generation workflows.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:02:54 -05:00
|
|
|
bsp.split_recursive(depth=2, min_size=(8, 8), seed=42)
|
|
|
|
|
|
|
|
|
|
# Basic conversion
|
|
|
|
|
hmap = bsp.to_heightmap()
|
|
|
|
|
assert hmap is not None, "Should create heightmap"
|
|
|
|
|
assert hmap.size == (50, 40), f"Size should match bounds, got {hmap.size}"
|
|
|
|
|
|
|
|
|
|
# Check that leaves are filled
|
|
|
|
|
leaves = list(bsp.leaves())
|
|
|
|
|
for leaf in leaves:
|
|
|
|
|
lx, ly = leaf.bounds[0]
|
|
|
|
|
val = hmap[lx, ly]
|
|
|
|
|
assert val == 1.0, f"Leaf interior should be 1.0, got {val}"
|
|
|
|
|
|
|
|
|
|
# Test with shrink
|
|
|
|
|
hmap_shrink = bsp.to_heightmap(shrink=2)
|
|
|
|
|
assert hmap_shrink is not None, "Should create with shrink"
|
|
|
|
|
|
|
|
|
|
# Test with select='internal'
|
|
|
|
|
hmap_internal = bsp.to_heightmap(select='internal')
|
|
|
|
|
assert hmap_internal is not None, "Should create with select=internal"
|
|
|
|
|
|
|
|
|
|
# Test with select='all'
|
|
|
|
|
hmap_all = bsp.to_heightmap(select='all')
|
|
|
|
|
assert hmap_all is not None, "Should create with select=all"
|
|
|
|
|
|
|
|
|
|
# Test with custom size
|
|
|
|
|
hmap_sized = bsp.to_heightmap(size=(100, 80))
|
|
|
|
|
assert hmap_sized.size == (100, 80), f"Custom size should work, got {hmap_sized.size}"
|
|
|
|
|
|
|
|
|
|
# Test with custom value
|
|
|
|
|
hmap_val = bsp.to_heightmap(value=0.5)
|
|
|
|
|
leaves = list(bsp.leaves())
|
|
|
|
|
leaf = leaves[0]
|
|
|
|
|
lx, ly = leaf.bounds[0]
|
|
|
|
|
val = hmap_val[lx, ly]
|
|
|
|
|
assert val == 0.5, f"Custom value should be 0.5, got {val}"
|
|
|
|
|
|
|
|
|
|
print(" BSP to_heightmap: PASS")
|
|
|
|
|
|
|
|
|
|
def test_traversal_enum():
|
|
|
|
|
"""Test Traversal enum (#204)."""
|
|
|
|
|
print("Testing Traversal enum...")
|
|
|
|
|
|
|
|
|
|
# Check enum exists
|
|
|
|
|
assert hasattr(mcrfpy, 'Traversal'), "Traversal enum should exist"
|
|
|
|
|
|
|
|
|
|
# Check all members
|
|
|
|
|
assert mcrfpy.Traversal.PRE_ORDER.value == 0
|
|
|
|
|
assert mcrfpy.Traversal.IN_ORDER.value == 1
|
|
|
|
|
assert mcrfpy.Traversal.POST_ORDER.value == 2
|
|
|
|
|
assert mcrfpy.Traversal.LEVEL_ORDER.value == 3
|
|
|
|
|
assert mcrfpy.Traversal.INVERTED_LEVEL_ORDER.value == 4
|
|
|
|
|
|
|
|
|
|
print(" Traversal enum: PASS")
|
|
|
|
|
|
|
|
|
|
def test_bsp_chaining():
|
|
|
|
|
"""Test method chaining."""
|
|
|
|
|
print("Testing BSP method chaining...")
|
|
|
|
|
|
BSP: add safety features and API improvements (closes #202, #203, #204, #205, #206)
Safety improvements:
- Generation counter detects stale BSPNode references after clear()/split_recursive()
- GRID_MAX validation prevents oversized BSP trees
- Depth parameter capped at 16 to prevent resource exhaustion
- Iterator checks generation to detect invalidation during mutation
API improvements:
- Changed constructor from bounds=((x,y),(w,h)) to pos=(x,y), size=(w,h)
- Added pos and size properties alongside bounds
- BSPNode __eq__ compares underlying pointers for identity
- BSP __iter__ as shorthand for leaves()
- BSP __len__ returns leaf count
Tests:
- Added tests for stale node detection, GRID_MAX validation, depth cap
- Added tests for __len__, __iter__, and BSPNode equality
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:59:31 -05:00
|
|
|
bsp = mcrfpy.BSP(pos=(0, 0), size=(80, 60))
|
BSP: add Binary Space Partitioning for procedural dungeon generation
Implements #202, #203, #204, #205; partially implements #206:
- BSP class: core tree structure with bounds, split_once, split_recursive, clear
- BSPNode class: lightweight node reference with bounds, level, is_leaf,
split_horizontal, split_position; navigation via left/right/parent/sibling;
contains() and center() methods
- Traversal enum: PRE_ORDER, IN_ORDER, POST_ORDER, LEVEL_ORDER, INVERTED_LEVEL_ORDER
- BSP iteration: leaves() for leaf nodes only, traverse(order) for all nodes
- BSP query: find(pos) returns deepest node containing position
- BSP.to_heightmap(): converts BSP to HeightMap with select, shrink, value options
Note: #206's BSPMap subclass deferred - to_heightmap returns plain HeightMap.
The HeightMap already has all necessary operations (inverse, threshold, etc.)
for procedural generation workflows.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:02:54 -05:00
|
|
|
|
|
|
|
|
# Chain multiple operations
|
|
|
|
|
result = bsp.split_recursive(depth=2, min_size=(8, 8), seed=42).clear().split_once(True, 30)
|
|
|
|
|
assert result is bsp, "Chaining should return self"
|
|
|
|
|
|
|
|
|
|
print(" BSP chaining: PASS")
|
|
|
|
|
|
|
|
|
|
def test_bsp_repr():
|
|
|
|
|
"""Test repr output."""
|
|
|
|
|
print("Testing BSP repr...")
|
|
|
|
|
|
BSP: add safety features and API improvements (closes #202, #203, #204, #205, #206)
Safety improvements:
- Generation counter detects stale BSPNode references after clear()/split_recursive()
- GRID_MAX validation prevents oversized BSP trees
- Depth parameter capped at 16 to prevent resource exhaustion
- Iterator checks generation to detect invalidation during mutation
API improvements:
- Changed constructor from bounds=((x,y),(w,h)) to pos=(x,y), size=(w,h)
- Added pos and size properties alongside bounds
- BSPNode __eq__ compares underlying pointers for identity
- BSP __iter__ as shorthand for leaves()
- BSP __len__ returns leaf count
Tests:
- Added tests for stale node detection, GRID_MAX validation, depth cap
- Added tests for __len__, __iter__, and BSPNode equality
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:59:31 -05:00
|
|
|
bsp = mcrfpy.BSP(pos=(0, 0), size=(80, 60))
|
BSP: add Binary Space Partitioning for procedural dungeon generation
Implements #202, #203, #204, #205; partially implements #206:
- BSP class: core tree structure with bounds, split_once, split_recursive, clear
- BSPNode class: lightweight node reference with bounds, level, is_leaf,
split_horizontal, split_position; navigation via left/right/parent/sibling;
contains() and center() methods
- Traversal enum: PRE_ORDER, IN_ORDER, POST_ORDER, LEVEL_ORDER, INVERTED_LEVEL_ORDER
- BSP iteration: leaves() for leaf nodes only, traverse(order) for all nodes
- BSP query: find(pos) returns deepest node containing position
- BSP.to_heightmap(): converts BSP to HeightMap with select, shrink, value options
Note: #206's BSPMap subclass deferred - to_heightmap returns plain HeightMap.
The HeightMap already has all necessary operations (inverse, threshold, etc.)
for procedural generation workflows.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:02:54 -05:00
|
|
|
repr_str = repr(bsp)
|
|
|
|
|
assert "BSP" in repr_str, f"repr should contain BSP: {repr_str}"
|
|
|
|
|
assert "80" in repr_str and "60" in repr_str, f"repr should contain size: {repr_str}"
|
|
|
|
|
|
|
|
|
|
bsp.split_recursive(depth=2, min_size=(8, 8), seed=42)
|
|
|
|
|
repr_str2 = repr(bsp)
|
|
|
|
|
assert "leaves" in repr_str2, f"repr should mention leaves: {repr_str2}"
|
|
|
|
|
|
|
|
|
|
# BSPNode repr
|
|
|
|
|
node_repr = repr(bsp.root)
|
|
|
|
|
assert "BSPNode" in node_repr, f"BSPNode repr: {node_repr}"
|
|
|
|
|
|
|
|
|
|
print(" BSP repr: PASS")
|
|
|
|
|
|
BSP: add safety features and API improvements (closes #202, #203, #204, #205, #206)
Safety improvements:
- Generation counter detects stale BSPNode references after clear()/split_recursive()
- GRID_MAX validation prevents oversized BSP trees
- Depth parameter capped at 16 to prevent resource exhaustion
- Iterator checks generation to detect invalidation during mutation
API improvements:
- Changed constructor from bounds=((x,y),(w,h)) to pos=(x,y), size=(w,h)
- Added pos and size properties alongside bounds
- BSPNode __eq__ compares underlying pointers for identity
- BSP __iter__ as shorthand for leaves()
- BSP __len__ returns leaf count
Tests:
- Added tests for stale node detection, GRID_MAX validation, depth cap
- Added tests for __len__, __iter__, and BSPNode equality
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:59:31 -05:00
|
|
|
def test_bsp_stale_node_detection():
|
|
|
|
|
"""Test that stale nodes are detected after clear()/split_recursive() (#review)."""
|
|
|
|
|
print("Testing BSP stale node detection...")
|
|
|
|
|
|
|
|
|
|
bsp = mcrfpy.BSP(pos=(0, 0), size=(80, 60))
|
|
|
|
|
bsp.split_recursive(depth=2, min_size=(8, 8), seed=42)
|
|
|
|
|
|
|
|
|
|
# Save reference to a node
|
|
|
|
|
old_root = bsp.root
|
|
|
|
|
old_leaf = list(bsp.leaves())[0]
|
|
|
|
|
|
|
|
|
|
# Clear invalidates all nodes
|
|
|
|
|
bsp.clear()
|
|
|
|
|
|
|
|
|
|
# Accessing stale node should raise RuntimeError
|
|
|
|
|
try:
|
|
|
|
|
_ = old_root.bounds
|
|
|
|
|
assert False, "Accessing stale root bounds should raise RuntimeError"
|
|
|
|
|
except RuntimeError as e:
|
|
|
|
|
assert "stale" in str(e).lower() or "invalid" in str(e).lower(), \
|
|
|
|
|
f"Error should mention staleness: {e}"
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
_ = old_leaf.is_leaf
|
|
|
|
|
assert False, "Accessing stale leaf should raise RuntimeError"
|
|
|
|
|
except RuntimeError as e:
|
|
|
|
|
pass # Expected
|
|
|
|
|
|
|
|
|
|
# split_recursive also invalidates (rebuilds tree)
|
|
|
|
|
bsp2 = mcrfpy.BSP(pos=(0, 0), size=(80, 60))
|
|
|
|
|
bsp2.split_recursive(depth=2, min_size=(8, 8), seed=42)
|
|
|
|
|
saved_node = bsp2.root
|
|
|
|
|
bsp2.split_recursive(depth=3, min_size=(8, 8), seed=99)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
_ = saved_node.bounds
|
|
|
|
|
assert False, "Accessing node after split_recursive should raise RuntimeError"
|
|
|
|
|
except RuntimeError:
|
|
|
|
|
pass # Expected
|
|
|
|
|
|
|
|
|
|
print(" BSP stale node detection: PASS")
|
|
|
|
|
|
|
|
|
|
def test_bsp_grid_max_validation():
|
|
|
|
|
"""Test GRID_MAX validation (#review)."""
|
|
|
|
|
print("Testing BSP GRID_MAX validation...")
|
|
|
|
|
|
|
|
|
|
# Should succeed with valid size
|
|
|
|
|
bsp = mcrfpy.BSP(pos=(0, 0), size=(1000, 1000))
|
|
|
|
|
assert bsp is not None
|
|
|
|
|
|
|
|
|
|
# Should fail with size exceeding GRID_MAX (8192)
|
|
|
|
|
try:
|
|
|
|
|
bsp_too_big = mcrfpy.BSP(pos=(0, 0), size=(10000, 100))
|
|
|
|
|
assert False, "Should raise ValueError for size > GRID_MAX"
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
assert "8192" in str(e) or "exceed" in str(e).lower(), f"Error should mention limit: {e}"
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
bsp_too_big = mcrfpy.BSP(pos=(0, 0), size=(100, 10000))
|
|
|
|
|
assert False, "Should raise ValueError for height > GRID_MAX"
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
pass # Expected
|
|
|
|
|
|
|
|
|
|
print(" BSP GRID_MAX validation: PASS")
|
|
|
|
|
|
|
|
|
|
def test_bsp_depth_cap():
|
|
|
|
|
"""Test depth is capped at 16 (#review)."""
|
|
|
|
|
print("Testing BSP depth cap...")
|
|
|
|
|
|
|
|
|
|
bsp = mcrfpy.BSP(pos=(0, 0), size=(1000, 1000))
|
|
|
|
|
|
|
|
|
|
# Should cap depth at 16 or raise ValueError
|
|
|
|
|
try:
|
|
|
|
|
bsp.split_recursive(depth=20, min_size=(1, 1), seed=42)
|
|
|
|
|
# If it succeeds, verify reasonable number of leaves (not 2^20)
|
|
|
|
|
leaves = list(bsp.leaves())
|
|
|
|
|
assert len(leaves) <= 2**16, f"Too many leaves: {len(leaves)}"
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
# It's also acceptable to reject excessive depth
|
|
|
|
|
assert "16" in str(e) or "depth" in str(e).lower(), f"Error should mention depth limit: {e}"
|
|
|
|
|
|
|
|
|
|
print(" BSP depth cap: PASS")
|
|
|
|
|
|
|
|
|
|
def test_bsp_len():
|
|
|
|
|
"""Test __len__ returns leaf count (#review)."""
|
|
|
|
|
print("Testing BSP __len__...")
|
|
|
|
|
|
|
|
|
|
bsp = mcrfpy.BSP(pos=(0, 0), size=(80, 60))
|
|
|
|
|
assert len(bsp) == 1, f"Unsplit BSP should have 1 leaf, got {len(bsp)}"
|
|
|
|
|
|
|
|
|
|
bsp.split_once(horizontal=True, position=30)
|
|
|
|
|
assert len(bsp) == 2, f"After one split should have 2 leaves, got {len(bsp)}"
|
|
|
|
|
|
|
|
|
|
bsp.clear()
|
|
|
|
|
bsp.split_recursive(depth=3, min_size=(8, 8), seed=42)
|
|
|
|
|
expected = len(list(bsp.leaves()))
|
|
|
|
|
assert len(bsp) == expected, f"len() mismatch: {len(bsp)} != {expected}"
|
|
|
|
|
|
|
|
|
|
print(" BSP __len__: PASS")
|
|
|
|
|
|
|
|
|
|
def test_bsp_iter():
|
|
|
|
|
"""Test __iter__ as shorthand for leaves() (#review)."""
|
|
|
|
|
print("Testing BSP __iter__...")
|
|
|
|
|
|
|
|
|
|
bsp = mcrfpy.BSP(pos=(0, 0), size=(80, 60))
|
|
|
|
|
bsp.split_recursive(depth=2, min_size=(8, 8), seed=42)
|
|
|
|
|
|
|
|
|
|
# Direct iteration should yield same results as leaves()
|
|
|
|
|
iter_list = list(bsp)
|
|
|
|
|
leaves_list = list(bsp.leaves())
|
|
|
|
|
|
|
|
|
|
assert len(iter_list) == len(leaves_list), \
|
|
|
|
|
f"Iterator count mismatch: {len(iter_list)} != {len(leaves_list)}"
|
|
|
|
|
|
|
|
|
|
# All items should be leaves
|
|
|
|
|
for node in bsp:
|
|
|
|
|
assert node.is_leaf, f"Iterator should yield leaves: {node}"
|
|
|
|
|
|
|
|
|
|
# Can iterate multiple times
|
|
|
|
|
count1 = sum(1 for _ in bsp)
|
|
|
|
|
count2 = sum(1 for _ in bsp)
|
|
|
|
|
assert count1 == count2, "Should be able to iterate multiple times"
|
|
|
|
|
|
|
|
|
|
print(" BSP __iter__: PASS")
|
|
|
|
|
|
|
|
|
|
def test_bspnode_equality():
|
|
|
|
|
"""Test BSPNode __eq__ comparison (#review)."""
|
|
|
|
|
print("Testing BSPNode equality...")
|
|
|
|
|
|
|
|
|
|
bsp = mcrfpy.BSP(pos=(0, 0), size=(80, 60))
|
|
|
|
|
bsp.split_recursive(depth=2, min_size=(8, 8), seed=42)
|
|
|
|
|
|
|
|
|
|
# Same node should be equal
|
|
|
|
|
root1 = bsp.root
|
|
|
|
|
root2 = bsp.root
|
|
|
|
|
assert root1 == root2, "Same node should be equal"
|
|
|
|
|
|
|
|
|
|
# Different nodes should not be equal
|
|
|
|
|
leaves = list(bsp.leaves())
|
|
|
|
|
assert len(leaves) >= 2, "Need at least 2 leaves for comparison"
|
|
|
|
|
assert leaves[0] != leaves[1], "Different nodes should not be equal"
|
|
|
|
|
|
|
|
|
|
# Parent vs child should not be equal
|
|
|
|
|
root = bsp.root
|
|
|
|
|
left = root.left
|
|
|
|
|
assert root != left, "Parent and child should not be equal"
|
|
|
|
|
|
|
|
|
|
# Not equal to non-BSPNode
|
|
|
|
|
assert not (root == "not a node"), "BSPNode should not equal string"
|
|
|
|
|
assert not (root == 42), "BSPNode should not equal int"
|
|
|
|
|
|
|
|
|
|
print(" BSPNode equality: PASS")
|
|
|
|
|
|
BSP: add Binary Space Partitioning for procedural dungeon generation
Implements #202, #203, #204, #205; partially implements #206:
- BSP class: core tree structure with bounds, split_once, split_recursive, clear
- BSPNode class: lightweight node reference with bounds, level, is_leaf,
split_horizontal, split_position; navigation via left/right/parent/sibling;
contains() and center() methods
- Traversal enum: PRE_ORDER, IN_ORDER, POST_ORDER, LEVEL_ORDER, INVERTED_LEVEL_ORDER
- BSP iteration: leaves() for leaf nodes only, traverse(order) for all nodes
- BSP query: find(pos) returns deepest node containing position
- BSP.to_heightmap(): converts BSP to HeightMap with select, shrink, value options
Note: #206's BSPMap subclass deferred - to_heightmap returns plain HeightMap.
The HeightMap already has all necessary operations (inverse, threshold, etc.)
for procedural generation workflows.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:02:54 -05:00
|
|
|
def run_all_tests():
|
|
|
|
|
"""Run all BSP tests."""
|
|
|
|
|
print("\n=== BSP Unit Tests ===\n")
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
test_bsp_construction()
|
|
|
|
|
test_bsp_split_once()
|
|
|
|
|
test_bsp_split_recursive()
|
|
|
|
|
test_bsp_clear()
|
|
|
|
|
test_bspnode_properties()
|
|
|
|
|
test_bspnode_navigation()
|
|
|
|
|
test_bspnode_contains()
|
|
|
|
|
test_bsp_leaves_iteration()
|
|
|
|
|
test_bsp_traverse()
|
|
|
|
|
test_bsp_find()
|
|
|
|
|
test_bsp_to_heightmap()
|
|
|
|
|
test_traversal_enum()
|
|
|
|
|
test_bsp_chaining()
|
|
|
|
|
test_bsp_repr()
|
BSP: add safety features and API improvements (closes #202, #203, #204, #205, #206)
Safety improvements:
- Generation counter detects stale BSPNode references after clear()/split_recursive()
- GRID_MAX validation prevents oversized BSP trees
- Depth parameter capped at 16 to prevent resource exhaustion
- Iterator checks generation to detect invalidation during mutation
API improvements:
- Changed constructor from bounds=((x,y),(w,h)) to pos=(x,y), size=(w,h)
- Added pos and size properties alongside bounds
- BSPNode __eq__ compares underlying pointers for identity
- BSP __iter__ as shorthand for leaves()
- BSP __len__ returns leaf count
Tests:
- Added tests for stale node detection, GRID_MAX validation, depth cap
- Added tests for __len__, __iter__, and BSPNode equality
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:59:31 -05:00
|
|
|
test_bsp_stale_node_detection()
|
|
|
|
|
test_bsp_grid_max_validation()
|
|
|
|
|
test_bsp_depth_cap()
|
|
|
|
|
test_bsp_len()
|
|
|
|
|
test_bsp_iter()
|
|
|
|
|
test_bspnode_equality()
|
BSP: add Binary Space Partitioning for procedural dungeon generation
Implements #202, #203, #204, #205; partially implements #206:
- BSP class: core tree structure with bounds, split_once, split_recursive, clear
- BSPNode class: lightweight node reference with bounds, level, is_leaf,
split_horizontal, split_position; navigation via left/right/parent/sibling;
contains() and center() methods
- Traversal enum: PRE_ORDER, IN_ORDER, POST_ORDER, LEVEL_ORDER, INVERTED_LEVEL_ORDER
- BSP iteration: leaves() for leaf nodes only, traverse(order) for all nodes
- BSP query: find(pos) returns deepest node containing position
- BSP.to_heightmap(): converts BSP to HeightMap with select, shrink, value options
Note: #206's BSPMap subclass deferred - to_heightmap returns plain HeightMap.
The HeightMap already has all necessary operations (inverse, threshold, etc.)
for procedural generation workflows.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:02:54 -05:00
|
|
|
|
|
|
|
|
print("\n=== ALL BSP TESTS PASSED ===\n")
|
|
|
|
|
sys.exit(0)
|
|
|
|
|
except AssertionError as e:
|
|
|
|
|
print(f"\nTEST FAILED: {e}")
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"\nUNEXPECTED ERROR: {type(e).__name__}: {e}")
|
|
|
|
|
import traceback
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
run_all_tests()
|