Compare commits
3 Commits
alpha_stre
...
master
Author | SHA1 | Date |
---|---|---|
|
4144cdf067 | |
|
665689c550 | |
|
d11f76ac43 |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 30 KiB |
|
@ -1,99 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Test for Entity property setters - fixing "new style getargs format" error
|
|
||||||
|
|
||||||
Verifies that Entity position and sprite_number setters work correctly.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def test_entity_setters(timer_name):
|
|
||||||
"""Test that Entity property setters work correctly"""
|
|
||||||
import mcrfpy
|
|
||||||
|
|
||||||
print("Testing Entity property setters...")
|
|
||||||
|
|
||||||
# Create test scene and grid
|
|
||||||
mcrfpy.createScene("entity_test")
|
|
||||||
ui = mcrfpy.sceneUI("entity_test")
|
|
||||||
|
|
||||||
# Create grid with texture
|
|
||||||
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
|
|
||||||
grid = mcrfpy.Grid(10, 10, texture, (10, 10), (400, 400))
|
|
||||||
ui.append(grid)
|
|
||||||
|
|
||||||
# Create entity
|
|
||||||
initial_pos = mcrfpy.Vector(2.5, 3.5)
|
|
||||||
entity = mcrfpy.Entity(initial_pos, texture, 5, grid)
|
|
||||||
grid.entities.append(entity)
|
|
||||||
|
|
||||||
print(f"✓ Created entity at position {entity.pos}")
|
|
||||||
|
|
||||||
# Test position setter with Vector
|
|
||||||
new_pos = mcrfpy.Vector(4.0, 5.0)
|
|
||||||
try:
|
|
||||||
entity.pos = new_pos
|
|
||||||
assert entity.pos.x == 4.0, f"Expected x=4.0, got {entity.pos.x}"
|
|
||||||
assert entity.pos.y == 5.0, f"Expected y=5.0, got {entity.pos.y}"
|
|
||||||
print(f"✓ Position setter works with Vector: {entity.pos}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✗ Position setter failed: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
# Test position setter with tuple (should also work via PyVector::from_arg)
|
|
||||||
try:
|
|
||||||
entity.pos = (7.5, 8.5)
|
|
||||||
assert entity.pos.x == 7.5, f"Expected x=7.5, got {entity.pos.x}"
|
|
||||||
assert entity.pos.y == 8.5, f"Expected y=8.5, got {entity.pos.y}"
|
|
||||||
print(f"✓ Position setter works with tuple: {entity.pos}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✗ Position setter with tuple failed: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
# Test draw_pos setter (collision position)
|
|
||||||
try:
|
|
||||||
entity.draw_pos = mcrfpy.Vector(3, 4)
|
|
||||||
assert entity.draw_pos.x == 3, f"Expected x=3, got {entity.draw_pos.x}"
|
|
||||||
assert entity.draw_pos.y == 4, f"Expected y=4, got {entity.draw_pos.y}"
|
|
||||||
print(f"✓ Draw position setter works: {entity.draw_pos}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✗ Draw position setter failed: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
# Test sprite_number setter
|
|
||||||
try:
|
|
||||||
entity.sprite_number = 10
|
|
||||||
assert entity.sprite_number == 10, f"Expected sprite_number=10, got {entity.sprite_number}"
|
|
||||||
print(f"✓ Sprite number setter works: {entity.sprite_number}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✗ Sprite number setter failed: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
# Test invalid position setter (should raise TypeError)
|
|
||||||
try:
|
|
||||||
entity.pos = "invalid"
|
|
||||||
print("✗ Position setter should have raised TypeError for string")
|
|
||||||
assert False, "Should have raised TypeError"
|
|
||||||
except TypeError as e:
|
|
||||||
print(f"✓ Position setter correctly rejects invalid type: {e}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✗ Unexpected error: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
# Test invalid sprite number (should raise TypeError)
|
|
||||||
try:
|
|
||||||
entity.sprite_number = "invalid"
|
|
||||||
print("✗ Sprite number setter should have raised TypeError for string")
|
|
||||||
assert False, "Should have raised TypeError"
|
|
||||||
except TypeError as e:
|
|
||||||
print(f"✓ Sprite number setter correctly rejects invalid type: {e}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✗ Unexpected error: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
# Cleanup timer
|
|
||||||
mcrfpy.delTimer("test_timer")
|
|
||||||
|
|
||||||
print("\n✅ Entity property setters test PASSED - All setters work correctly")
|
|
||||||
|
|
||||||
# Execute the test after a short delay to ensure window is ready
|
|
||||||
import mcrfpy
|
|
||||||
mcrfpy.setTimer("test_timer", test_entity_setters, 100)
|
|
|
@ -1,61 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Simple test for Entity property setters
|
|
||||||
"""
|
|
||||||
|
|
||||||
def test_entity_setters(timer_name):
|
|
||||||
"""Test Entity property setters"""
|
|
||||||
import mcrfpy
|
|
||||||
import sys
|
|
||||||
|
|
||||||
print("Testing Entity property setters...")
|
|
||||||
|
|
||||||
# Create test scene and grid
|
|
||||||
mcrfpy.createScene("test")
|
|
||||||
ui = mcrfpy.sceneUI("test")
|
|
||||||
|
|
||||||
# Create grid with texture
|
|
||||||
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
|
|
||||||
grid = mcrfpy.Grid(10, 10, texture, (10, 10), (400, 400))
|
|
||||||
ui.append(grid)
|
|
||||||
|
|
||||||
# Create entity
|
|
||||||
entity = mcrfpy.Entity((2.5, 3.5), texture, 5, grid)
|
|
||||||
grid.entities.append(entity)
|
|
||||||
|
|
||||||
# Test 1: Initial position
|
|
||||||
print(f"Initial position: {entity.pos}")
|
|
||||||
print(f"Initial position x={entity.pos.x}, y={entity.pos.y}")
|
|
||||||
|
|
||||||
# Test 2: Set position with Vector
|
|
||||||
entity.pos = mcrfpy.Vector(4.0, 5.0)
|
|
||||||
print(f"After Vector setter: pos={entity.pos}, x={entity.pos.x}, y={entity.pos.y}")
|
|
||||||
|
|
||||||
# Test 3: Set position with tuple
|
|
||||||
entity.pos = (7.5, 8.5)
|
|
||||||
print(f"After tuple setter: pos={entity.pos}, x={entity.pos.x}, y={entity.pos.y}")
|
|
||||||
|
|
||||||
# Test 4: sprite_number
|
|
||||||
print(f"Initial sprite_number: {entity.sprite_number}")
|
|
||||||
entity.sprite_number = 10
|
|
||||||
print(f"After setter: sprite_number={entity.sprite_number}")
|
|
||||||
|
|
||||||
# Test 5: Invalid types
|
|
||||||
try:
|
|
||||||
entity.pos = "invalid"
|
|
||||||
print("ERROR: Should have raised TypeError")
|
|
||||||
except TypeError as e:
|
|
||||||
print(f"✓ Correctly rejected invalid position: {e}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
entity.sprite_number = "invalid"
|
|
||||||
print("ERROR: Should have raised TypeError")
|
|
||||||
except TypeError as e:
|
|
||||||
print(f"✓ Correctly rejected invalid sprite_number: {e}")
|
|
||||||
|
|
||||||
print("\n✅ Entity property setters test completed")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
# Execute the test after a short delay
|
|
||||||
import mcrfpy
|
|
||||||
mcrfpy.setTimer("test", test_entity_setters, 100)
|
|
Before Width: | Height: | Size: 31 KiB |
|
@ -1,105 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Test for Issue #27: EntityCollection.extend() method
|
|
||||||
|
|
||||||
Verifies that EntityCollection can extend with multiple entities at once.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def test_entity_extend(timer_name):
|
|
||||||
"""Test that EntityCollection.extend() method works correctly"""
|
|
||||||
import mcrfpy
|
|
||||||
import sys
|
|
||||||
|
|
||||||
print("Issue #27 test: EntityCollection.extend() method")
|
|
||||||
|
|
||||||
# Create test scene and grid
|
|
||||||
mcrfpy.createScene("test")
|
|
||||||
ui = mcrfpy.sceneUI("test")
|
|
||||||
|
|
||||||
# Create grid with texture
|
|
||||||
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
|
|
||||||
grid = mcrfpy.Grid(10, 10, texture, (10, 10), (400, 400))
|
|
||||||
ui.append(grid)
|
|
||||||
|
|
||||||
# Add some initial entities
|
|
||||||
entity1 = mcrfpy.Entity((1, 1), texture, 1, grid)
|
|
||||||
entity2 = mcrfpy.Entity((2, 2), texture, 2, grid)
|
|
||||||
grid.entities.append(entity1)
|
|
||||||
grid.entities.append(entity2)
|
|
||||||
|
|
||||||
print(f"✓ Initial entities: {len(grid.entities)}")
|
|
||||||
|
|
||||||
# Test 1: Extend with a list of entities
|
|
||||||
new_entities = [
|
|
||||||
mcrfpy.Entity((3, 3), texture, 3, grid),
|
|
||||||
mcrfpy.Entity((4, 4), texture, 4, grid),
|
|
||||||
mcrfpy.Entity((5, 5), texture, 5, grid)
|
|
||||||
]
|
|
||||||
|
|
||||||
try:
|
|
||||||
grid.entities.extend(new_entities)
|
|
||||||
assert len(grid.entities) == 5, f"Expected 5 entities, got {len(grid.entities)}"
|
|
||||||
print(f"✓ Extended with list: now {len(grid.entities)} entities")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✗ Failed to extend with list: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
# Test 2: Extend with a tuple
|
|
||||||
more_entities = (
|
|
||||||
mcrfpy.Entity((6, 6), texture, 6, grid),
|
|
||||||
mcrfpy.Entity((7, 7), texture, 7, grid)
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
grid.entities.extend(more_entities)
|
|
||||||
assert len(grid.entities) == 7, f"Expected 7 entities, got {len(grid.entities)}"
|
|
||||||
print(f"✓ Extended with tuple: now {len(grid.entities)} entities")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✗ Failed to extend with tuple: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
# Test 3: Extend with generator expression
|
|
||||||
try:
|
|
||||||
grid.entities.extend(mcrfpy.Entity((8, i), texture, 8+i, grid) for i in range(3))
|
|
||||||
assert len(grid.entities) == 10, f"Expected 10 entities, got {len(grid.entities)}"
|
|
||||||
print(f"✓ Extended with generator: now {len(grid.entities)} entities")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✗ Failed to extend with generator: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
# Test 4: Verify all entities have correct grid association
|
|
||||||
for i, entity in enumerate(grid.entities):
|
|
||||||
# Just checking that we can iterate and access them
|
|
||||||
assert entity.sprite_number >= 1, f"Entity {i} has invalid sprite number"
|
|
||||||
print("✓ All entities accessible and valid")
|
|
||||||
|
|
||||||
# Test 5: Invalid input - non-iterable
|
|
||||||
try:
|
|
||||||
grid.entities.extend(42)
|
|
||||||
print("✗ Should have raised TypeError for non-iterable")
|
|
||||||
except TypeError as e:
|
|
||||||
print(f"✓ Correctly rejected non-iterable: {e}")
|
|
||||||
|
|
||||||
# Test 6: Invalid input - iterable with non-Entity
|
|
||||||
try:
|
|
||||||
grid.entities.extend([entity1, "not an entity", entity2])
|
|
||||||
print("✗ Should have raised TypeError for non-Entity in iterable")
|
|
||||||
except TypeError as e:
|
|
||||||
print(f"✓ Correctly rejected non-Entity in iterable: {e}")
|
|
||||||
|
|
||||||
# Test 7: Empty iterable (should work)
|
|
||||||
initial_count = len(grid.entities)
|
|
||||||
try:
|
|
||||||
grid.entities.extend([])
|
|
||||||
assert len(grid.entities) == initial_count, "Empty extend changed count"
|
|
||||||
print("✓ Empty extend works correctly")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✗ Empty extend failed: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
print(f"\n✅ Issue #27 test PASSED - EntityCollection.extend() works correctly")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
# Execute the test after a short delay
|
|
||||||
import mcrfpy
|
|
||||||
mcrfpy.setTimer("test", test_entity_extend, 100)
|
|
|
@ -1,111 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Test for Issue #33: Sprite index validation
|
|
||||||
|
|
||||||
Verifies that Sprite and Entity objects validate sprite indices
|
|
||||||
against the texture's actual sprite count.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def test_sprite_index_validation(timer_name):
|
|
||||||
"""Test that sprite index validation works correctly"""
|
|
||||||
import mcrfpy
|
|
||||||
import sys
|
|
||||||
|
|
||||||
print("Issue #33 test: Sprite index validation")
|
|
||||||
|
|
||||||
# Create test scene
|
|
||||||
mcrfpy.createScene("test")
|
|
||||||
ui = mcrfpy.sceneUI("test")
|
|
||||||
|
|
||||||
# Create texture - kenney_ice.png is 11x12 sprites of 16x16 each
|
|
||||||
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
|
|
||||||
# Total sprites = 11 * 12 = 132 sprites (indices 0-131)
|
|
||||||
|
|
||||||
# Test 1: Create sprite with valid index
|
|
||||||
try:
|
|
||||||
sprite = mcrfpy.Sprite(100, 100, texture, 50) # Valid index
|
|
||||||
ui.append(sprite)
|
|
||||||
print(f"✓ Created sprite with valid index 50")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✗ Failed to create sprite with valid index: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
# Test 2: Set valid sprite index
|
|
||||||
try:
|
|
||||||
sprite.sprite_number = 100 # Still valid
|
|
||||||
assert sprite.sprite_number == 100
|
|
||||||
print(f"✓ Set sprite to valid index 100")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✗ Failed to set valid sprite index: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
# Test 3: Set maximum valid index
|
|
||||||
try:
|
|
||||||
sprite.sprite_number = 131 # Maximum valid index
|
|
||||||
assert sprite.sprite_number == 131
|
|
||||||
print(f"✓ Set sprite to maximum valid index 131")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✗ Failed to set maximum valid index: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
# Test 4: Invalid negative index
|
|
||||||
try:
|
|
||||||
sprite.sprite_number = -1
|
|
||||||
print("✗ Should have raised ValueError for negative index")
|
|
||||||
except ValueError as e:
|
|
||||||
print(f"✓ Correctly rejected negative index: {e}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✗ Wrong exception type for negative index: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
# Test 5: Invalid index too large
|
|
||||||
try:
|
|
||||||
sprite.sprite_number = 132 # One past the maximum
|
|
||||||
print("✗ Should have raised ValueError for index 132")
|
|
||||||
except ValueError as e:
|
|
||||||
print(f"✓ Correctly rejected out-of-bounds index: {e}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✗ Wrong exception type for out-of-bounds index: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
# Test 6: Very large invalid index
|
|
||||||
try:
|
|
||||||
sprite.sprite_number = 1000
|
|
||||||
print("✗ Should have raised ValueError for index 1000")
|
|
||||||
except ValueError as e:
|
|
||||||
print(f"✓ Correctly rejected large invalid index: {e}")
|
|
||||||
|
|
||||||
# Test 7: Entity sprite_number validation
|
|
||||||
grid = mcrfpy.Grid(10, 10, texture, (10, 10), (400, 400))
|
|
||||||
ui.append(grid)
|
|
||||||
|
|
||||||
entity = mcrfpy.Entity((5, 5), texture, 50, grid)
|
|
||||||
grid.entities.append(entity)
|
|
||||||
|
|
||||||
try:
|
|
||||||
entity.sprite_number = 200 # Out of bounds
|
|
||||||
print("✗ Entity should also validate sprite indices")
|
|
||||||
except ValueError as e:
|
|
||||||
print(f"✓ Entity also validates sprite indices: {e}")
|
|
||||||
except Exception as e:
|
|
||||||
# Entity might not have the same validation yet
|
|
||||||
print(f"Note: Entity validation not implemented yet: {e}")
|
|
||||||
|
|
||||||
# Test 8: Different texture sizes
|
|
||||||
# Create a smaller texture to test different bounds
|
|
||||||
small_texture = mcrfpy.Texture("assets/Sprite-0001.png", 32, 32)
|
|
||||||
small_sprite = mcrfpy.Sprite(200, 200, small_texture, 0)
|
|
||||||
|
|
||||||
# This texture might have fewer sprites, test accordingly
|
|
||||||
try:
|
|
||||||
small_sprite.sprite_number = 100 # Might be out of bounds
|
|
||||||
print("Note: Small texture accepted index 100")
|
|
||||||
except ValueError as e:
|
|
||||||
print(f"✓ Small texture has different bounds: {e}")
|
|
||||||
|
|
||||||
print(f"\n✅ Issue #33 test PASSED - Sprite index validation works correctly")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
# Execute the test after a short delay
|
|
||||||
import mcrfpy
|
|
||||||
mcrfpy.setTimer("test", test_sprite_index_validation, 100)
|
|
|
@ -1,101 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Test for Issue #73: Entity.index() method for removal
|
|
||||||
|
|
||||||
Verifies that Entity objects can report their index in the grid's entity collection.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def test_entity_index(timer_name):
|
|
||||||
"""Test that Entity.index() method works correctly"""
|
|
||||||
import mcrfpy
|
|
||||||
import sys
|
|
||||||
|
|
||||||
print("Issue #73 test: Entity.index() method")
|
|
||||||
|
|
||||||
# Create test scene and grid
|
|
||||||
mcrfpy.createScene("test")
|
|
||||||
ui = mcrfpy.sceneUI("test")
|
|
||||||
|
|
||||||
# Create grid with texture
|
|
||||||
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
|
|
||||||
grid = mcrfpy.Grid(10, 10, texture, (10, 10), (400, 400))
|
|
||||||
ui.append(grid)
|
|
||||||
|
|
||||||
# Create multiple entities
|
|
||||||
entities = []
|
|
||||||
for i in range(5):
|
|
||||||
entity = mcrfpy.Entity((i, i), texture, i, grid)
|
|
||||||
entities.append(entity)
|
|
||||||
grid.entities.append(entity)
|
|
||||||
|
|
||||||
print(f"✓ Created {len(entities)} entities")
|
|
||||||
|
|
||||||
# Test 1: Check each entity knows its index
|
|
||||||
for expected_idx, entity in enumerate(entities):
|
|
||||||
try:
|
|
||||||
actual_idx = entity.index()
|
|
||||||
assert actual_idx == expected_idx, f"Expected index {expected_idx}, got {actual_idx}"
|
|
||||||
print(f"✓ Entity {expected_idx} correctly reports index {actual_idx}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✗ Entity {expected_idx} index() failed: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
# Test 2: Remove entity using index
|
|
||||||
entity_to_remove = entities[2]
|
|
||||||
remove_idx = entity_to_remove.index()
|
|
||||||
grid.entities.remove(remove_idx)
|
|
||||||
print(f"✓ Removed entity at index {remove_idx}")
|
|
||||||
|
|
||||||
# Test 3: Verify indices updated after removal
|
|
||||||
for i, entity in enumerate(entities):
|
|
||||||
if i == 2:
|
|
||||||
# This entity was removed, should raise error
|
|
||||||
try:
|
|
||||||
idx = entity.index()
|
|
||||||
print(f"✗ Removed entity still reports index {idx}")
|
|
||||||
except ValueError as e:
|
|
||||||
print(f"✓ Removed entity correctly raises error: {e}")
|
|
||||||
elif i < 2:
|
|
||||||
# These entities should keep their indices
|
|
||||||
idx = entity.index()
|
|
||||||
assert idx == i, f"Entity before removal has wrong index: {idx}"
|
|
||||||
else:
|
|
||||||
# These entities should have shifted down by 1
|
|
||||||
idx = entity.index()
|
|
||||||
assert idx == i - 1, f"Entity after removal has wrong index: {idx}"
|
|
||||||
|
|
||||||
# Test 4: Entity without grid
|
|
||||||
orphan_entity = mcrfpy.Entity((0, 0), texture, 0, None)
|
|
||||||
try:
|
|
||||||
idx = orphan_entity.index()
|
|
||||||
print(f"✗ Orphan entity should raise error but returned {idx}")
|
|
||||||
except RuntimeError as e:
|
|
||||||
print(f"✓ Orphan entity correctly raises error: {e}")
|
|
||||||
|
|
||||||
# Test 5: Use index() in practical removal pattern
|
|
||||||
# Add some new entities
|
|
||||||
for i in range(3):
|
|
||||||
entity = mcrfpy.Entity((7+i, 7+i), texture, 10+i, grid)
|
|
||||||
grid.entities.append(entity)
|
|
||||||
|
|
||||||
# Remove entities with sprite_number > 10
|
|
||||||
removed_count = 0
|
|
||||||
i = 0
|
|
||||||
while i < len(grid.entities):
|
|
||||||
entity = grid.entities[i]
|
|
||||||
if entity.sprite_number > 10:
|
|
||||||
grid.entities.remove(entity.index())
|
|
||||||
removed_count += 1
|
|
||||||
# Don't increment i, as entities shifted down
|
|
||||||
else:
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
print(f"✓ Removed {removed_count} entities using index() in loop")
|
|
||||||
assert len(grid.entities) == 5, f"Expected 5 entities remaining, got {len(grid.entities)}"
|
|
||||||
|
|
||||||
print("\n✅ Issue #73 test PASSED - Entity.index() method works correctly")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
# Execute the test after a short delay
|
|
||||||
import mcrfpy
|
|
||||||
mcrfpy.setTimer("test", test_entity_index, 100)
|
|
|
@ -1,77 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Simple test for Issue #73: Entity.index() method
|
|
||||||
"""
|
|
||||||
|
|
||||||
def test_entity_index(timer_name):
|
|
||||||
"""Test that Entity.index() method works correctly"""
|
|
||||||
import mcrfpy
|
|
||||||
import sys
|
|
||||||
|
|
||||||
print("Testing Entity.index() method...")
|
|
||||||
|
|
||||||
# Create test scene and grid
|
|
||||||
mcrfpy.createScene("test")
|
|
||||||
ui = mcrfpy.sceneUI("test")
|
|
||||||
|
|
||||||
# Create grid with texture
|
|
||||||
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
|
|
||||||
grid = mcrfpy.Grid(10, 10, texture, (10, 10), (400, 400))
|
|
||||||
ui.append(grid)
|
|
||||||
|
|
||||||
# Clear any existing entities
|
|
||||||
while len(grid.entities) > 0:
|
|
||||||
grid.entities.remove(0)
|
|
||||||
|
|
||||||
# Create entities
|
|
||||||
entity1 = mcrfpy.Entity((1, 1), texture, 1, grid)
|
|
||||||
entity2 = mcrfpy.Entity((2, 2), texture, 2, grid)
|
|
||||||
entity3 = mcrfpy.Entity((3, 3), texture, 3, grid)
|
|
||||||
|
|
||||||
grid.entities.append(entity1)
|
|
||||||
grid.entities.append(entity2)
|
|
||||||
grid.entities.append(entity3)
|
|
||||||
|
|
||||||
print(f"Created {len(grid.entities)} entities")
|
|
||||||
|
|
||||||
# Test index() method
|
|
||||||
idx1 = entity1.index()
|
|
||||||
idx2 = entity2.index()
|
|
||||||
idx3 = entity3.index()
|
|
||||||
|
|
||||||
print(f"Entity 1 index: {idx1}")
|
|
||||||
print(f"Entity 2 index: {idx2}")
|
|
||||||
print(f"Entity 3 index: {idx3}")
|
|
||||||
|
|
||||||
assert idx1 == 0, f"Entity 1 should be at index 0, got {idx1}"
|
|
||||||
assert idx2 == 1, f"Entity 2 should be at index 1, got {idx2}"
|
|
||||||
assert idx3 == 2, f"Entity 3 should be at index 2, got {idx3}"
|
|
||||||
|
|
||||||
print("✓ All entities report correct indices")
|
|
||||||
|
|
||||||
# Test removal using index
|
|
||||||
remove_idx = entity2.index()
|
|
||||||
grid.entities.remove(remove_idx)
|
|
||||||
print(f"✓ Removed entity at index {remove_idx}")
|
|
||||||
|
|
||||||
# Check remaining entities
|
|
||||||
assert len(grid.entities) == 2
|
|
||||||
assert entity1.index() == 0
|
|
||||||
assert entity3.index() == 1 # Should have shifted down
|
|
||||||
|
|
||||||
print("✓ Indices updated correctly after removal")
|
|
||||||
|
|
||||||
# Test entity not in grid
|
|
||||||
orphan = mcrfpy.Entity((5, 5), texture, 5, None)
|
|
||||||
try:
|
|
||||||
idx = orphan.index()
|
|
||||||
print(f"✗ Orphan entity should raise error but returned {idx}")
|
|
||||||
except RuntimeError as e:
|
|
||||||
print(f"✓ Orphan entity correctly raises error")
|
|
||||||
|
|
||||||
print("\n✅ Entity.index() test PASSED")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
# Execute the test after a short delay
|
|
||||||
import mcrfpy
|
|
||||||
mcrfpy.setTimer("test", test_entity_index, 100)
|
|
|
@ -1,60 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Test for Issue #74: Add missing Grid.grid_y property
|
|
||||||
|
|
||||||
Verifies that Grid objects expose grid_x and grid_y properties correctly.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def test_grid_xy_properties(timer_name):
|
|
||||||
"""Test that Grid has grid_x and grid_y properties"""
|
|
||||||
import mcrfpy
|
|
||||||
|
|
||||||
# Test was run
|
|
||||||
print("Issue #74 test: Grid.grid_x and Grid.grid_y properties")
|
|
||||||
|
|
||||||
# Test with texture
|
|
||||||
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
|
|
||||||
grid = mcrfpy.Grid(20, 15, texture, (0, 0), (800, 600))
|
|
||||||
|
|
||||||
# Test grid_x property
|
|
||||||
assert hasattr(grid, 'grid_x'), "Grid should have grid_x property"
|
|
||||||
assert grid.grid_x == 20, f"Expected grid_x=20, got {grid.grid_x}"
|
|
||||||
print(f"✓ grid.grid_x = {grid.grid_x}")
|
|
||||||
|
|
||||||
# Test grid_y property
|
|
||||||
assert hasattr(grid, 'grid_y'), "Grid should have grid_y property"
|
|
||||||
assert grid.grid_y == 15, f"Expected grid_y=15, got {grid.grid_y}"
|
|
||||||
print(f"✓ grid.grid_y = {grid.grid_y}")
|
|
||||||
|
|
||||||
# Test grid_size still works
|
|
||||||
assert hasattr(grid, 'grid_size'), "Grid should still have grid_size property"
|
|
||||||
assert grid.grid_size == (20, 15), f"Expected grid_size=(20, 15), got {grid.grid_size}"
|
|
||||||
print(f"✓ grid.grid_size = {grid.grid_size}")
|
|
||||||
|
|
||||||
# Test without texture
|
|
||||||
grid2 = mcrfpy.Grid(30, 25, None, (10, 10), (480, 400))
|
|
||||||
assert grid2.grid_x == 30, f"Expected grid_x=30, got {grid2.grid_x}"
|
|
||||||
assert grid2.grid_y == 25, f"Expected grid_y=25, got {grid2.grid_y}"
|
|
||||||
assert grid2.grid_size == (30, 25), f"Expected grid_size=(30, 25), got {grid2.grid_size}"
|
|
||||||
print("✓ Grid without texture also has correct grid_x and grid_y")
|
|
||||||
|
|
||||||
# Test using in error message context (original issue)
|
|
||||||
try:
|
|
||||||
grid.at((-1, 0)) # Should raise error
|
|
||||||
except ValueError as e:
|
|
||||||
error_msg = str(e)
|
|
||||||
assert "Grid.grid_x" in error_msg, f"Error message should reference Grid.grid_x: {error_msg}"
|
|
||||||
print(f"✓ Error message correctly references Grid.grid_x: {error_msg}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
grid.at((0, -1)) # Should raise error
|
|
||||||
except ValueError as e:
|
|
||||||
error_msg = str(e)
|
|
||||||
assert "Grid.grid_y" in error_msg, f"Error message should reference Grid.grid_y: {error_msg}"
|
|
||||||
print(f"✓ Error message correctly references Grid.grid_y: {error_msg}")
|
|
||||||
|
|
||||||
print("\n✅ Issue #74 test PASSED - Grid.grid_x and Grid.grid_y properties work correctly")
|
|
||||||
|
|
||||||
# Execute the test after a short delay to ensure window is ready
|
|
||||||
import mcrfpy
|
|
||||||
mcrfpy.setTimer("test_timer", test_grid_xy_properties, 100)
|
|
Before Width: | Height: | Size: 31 KiB |
|
@ -1,87 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""Test that Issue #78 is fixed - Middle Mouse Click should NOT send 'C' keyboard event"""
|
|
||||||
import mcrfpy
|
|
||||||
from mcrfpy import automation
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# Track events
|
|
||||||
keyboard_events = []
|
|
||||||
click_events = []
|
|
||||||
|
|
||||||
def keyboard_handler(key):
|
|
||||||
"""Track keyboard events"""
|
|
||||||
keyboard_events.append(key)
|
|
||||||
print(f"Keyboard event received: '{key}'")
|
|
||||||
|
|
||||||
def click_handler(x, y, button):
|
|
||||||
"""Track click events"""
|
|
||||||
click_events.append((x, y, button))
|
|
||||||
print(f"Click event received: ({x}, {y}, button={button})")
|
|
||||||
|
|
||||||
def test_middle_click_fix(runtime):
|
|
||||||
"""Test that middle click no longer sends 'C' key event"""
|
|
||||||
print(f"\n=== Testing Issue #78 Fix (runtime: {runtime}) ===")
|
|
||||||
|
|
||||||
# Simulate middle click
|
|
||||||
print("\nSimulating middle click at (200, 200)...")
|
|
||||||
automation.middleClick(200, 200)
|
|
||||||
|
|
||||||
# Also test other clicks for comparison
|
|
||||||
print("Simulating left click at (100, 100)...")
|
|
||||||
automation.click(100, 100)
|
|
||||||
|
|
||||||
print("Simulating right click at (300, 300)...")
|
|
||||||
automation.rightClick(300, 300)
|
|
||||||
|
|
||||||
# Wait a moment for events to process
|
|
||||||
mcrfpy.setTimer("check_results", check_results, 500)
|
|
||||||
|
|
||||||
def check_results(runtime):
|
|
||||||
"""Check if the bug is fixed"""
|
|
||||||
print(f"\n=== Results ===")
|
|
||||||
print(f"Keyboard events received: {len(keyboard_events)}")
|
|
||||||
print(f"Click events received: {len(click_events)}")
|
|
||||||
|
|
||||||
# Check if 'C' was incorrectly triggered
|
|
||||||
if 'C' in keyboard_events or 'c' in keyboard_events:
|
|
||||||
print("\n✗ FAIL - Issue #78 still exists: Middle click triggered 'C' keyboard event!")
|
|
||||||
print(f"Keyboard events: {keyboard_events}")
|
|
||||||
else:
|
|
||||||
print("\n✓ PASS - Issue #78 is FIXED: No spurious 'C' keyboard event from middle click!")
|
|
||||||
|
|
||||||
# Take screenshot
|
|
||||||
filename = f"issue78_fixed_{int(runtime)}.png"
|
|
||||||
automation.screenshot(filename)
|
|
||||||
print(f"\nScreenshot saved: {filename}")
|
|
||||||
|
|
||||||
# Cleanup and exit
|
|
||||||
mcrfpy.delTimer("check_results")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
# Set up test scene
|
|
||||||
print("Setting up test scene...")
|
|
||||||
mcrfpy.createScene("issue78_test")
|
|
||||||
mcrfpy.setScene("issue78_test")
|
|
||||||
ui = mcrfpy.sceneUI("issue78_test")
|
|
||||||
|
|
||||||
# Register keyboard handler
|
|
||||||
mcrfpy.keypressScene(keyboard_handler)
|
|
||||||
|
|
||||||
# Create a clickable frame
|
|
||||||
frame = mcrfpy.Frame(50, 50, 400, 400,
|
|
||||||
fill_color=mcrfpy.Color(100, 150, 200),
|
|
||||||
outline_color=mcrfpy.Color(255, 255, 255),
|
|
||||||
outline=3.0)
|
|
||||||
frame.click = click_handler
|
|
||||||
ui.append(frame)
|
|
||||||
|
|
||||||
# Add label
|
|
||||||
caption = mcrfpy.Caption(mcrfpy.Vector(100, 100),
|
|
||||||
text="Issue #78 Test - Middle Click",
|
|
||||||
fill_color=mcrfpy.Color(255, 255, 255))
|
|
||||||
caption.size = 24
|
|
||||||
ui.append(caption)
|
|
||||||
|
|
||||||
# Schedule test
|
|
||||||
print("Scheduling test to run after render loop starts...")
|
|
||||||
mcrfpy.setTimer("test", test_middle_click_fix, 1000)
|
|
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 31 KiB |
|
@ -1,73 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Test for Sprite texture setter - fixing "error return without exception set"
|
|
||||||
"""
|
|
||||||
|
|
||||||
def test_sprite_texture_setter(timer_name):
|
|
||||||
"""Test that Sprite texture setter works correctly"""
|
|
||||||
import mcrfpy
|
|
||||||
import sys
|
|
||||||
|
|
||||||
print("Testing Sprite texture setter...")
|
|
||||||
|
|
||||||
# Create test scene
|
|
||||||
mcrfpy.createScene("test")
|
|
||||||
ui = mcrfpy.sceneUI("test")
|
|
||||||
|
|
||||||
# Create textures
|
|
||||||
texture1 = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
|
|
||||||
texture2 = mcrfpy.Texture("assets/kenney_lava.png", 16, 16)
|
|
||||||
|
|
||||||
# Create sprite with first texture
|
|
||||||
sprite = mcrfpy.Sprite(100, 100, texture1, 5)
|
|
||||||
ui.append(sprite)
|
|
||||||
|
|
||||||
# Test getting texture
|
|
||||||
try:
|
|
||||||
current_texture = sprite.texture
|
|
||||||
print(f"✓ Got texture: {current_texture}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✗ Failed to get texture: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
# Test setting new texture
|
|
||||||
try:
|
|
||||||
sprite.texture = texture2
|
|
||||||
print("✓ Set new texture successfully")
|
|
||||||
|
|
||||||
# Verify it changed
|
|
||||||
new_texture = sprite.texture
|
|
||||||
if new_texture != texture2:
|
|
||||||
print(f"✗ Texture didn't change properly")
|
|
||||||
else:
|
|
||||||
print("✓ Texture changed correctly")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✗ Failed to set texture: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
# Test invalid texture type
|
|
||||||
try:
|
|
||||||
sprite.texture = "invalid"
|
|
||||||
print("✗ Should have raised TypeError for invalid texture")
|
|
||||||
except TypeError as e:
|
|
||||||
print(f"✓ Correctly rejected invalid texture: {e}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✗ Wrong exception type: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
# Test None texture
|
|
||||||
try:
|
|
||||||
sprite.texture = None
|
|
||||||
print("✗ Should have raised TypeError for None texture")
|
|
||||||
except TypeError as e:
|
|
||||||
print(f"✓ Correctly rejected None texture: {e}")
|
|
||||||
|
|
||||||
# Test that sprite still renders correctly
|
|
||||||
print("✓ Sprite still renders with new texture")
|
|
||||||
|
|
||||||
print("\n✅ Sprite texture setter test PASSED")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
# Execute the test after a short delay
|
|
||||||
import mcrfpy
|
|
||||||
mcrfpy.setTimer("test", test_sprite_texture_setter, 100)
|
|
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 30 KiB |
|
@ -1,93 +0,0 @@
|
||||||
# Phase 1-3 Completion Summary
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
Successfully completed all tasks in Phases 1, 2, and 3 of the alpha_streamline_2 branch. This represents a major architectural improvement to McRogueFace's Python API, making it more consistent, safer, and feature-rich.
|
|
||||||
|
|
||||||
## Phase 1: Architecture Stabilization (Completed)
|
|
||||||
- ✅ #7 - Audited and fixed unsafe constructors across all UI classes
|
|
||||||
- ✅ #71 - Implemented _Drawable base class properties at C++ level
|
|
||||||
- ✅ #87 - Added visible property for show/hide functionality
|
|
||||||
- ✅ #88 - Added opacity property for transparency control
|
|
||||||
- ✅ #89 - Added get_bounds() method returning (x, y, width, height)
|
|
||||||
- ✅ #98 - Added move()/resize() methods for dynamic UI manipulation
|
|
||||||
|
|
||||||
## Phase 2: API Enhancements (Completed)
|
|
||||||
- ✅ #101 - Standardized default positions (all UI elements default to 0,0)
|
|
||||||
- ✅ #38 - Frame accepts children parameter in constructor
|
|
||||||
- ✅ #42 - All UI elements accept click handler in __init__
|
|
||||||
- ✅ #90 - Grid accepts size as tuple: Grid((20, 15))
|
|
||||||
- ✅ #19 - Sprite texture swapping via texture property
|
|
||||||
- ✅ #52 - Grid rendering skips out-of-bounds entities
|
|
||||||
|
|
||||||
## Phase 3: Game-Ready Features (Completed)
|
|
||||||
- ✅ #30 - Entity.die() method for proper cleanup
|
|
||||||
- ✅ #93 - Vector arithmetic operators (+, -, *, /, ==, bool, abs, neg)
|
|
||||||
- ✅ #94 - Color helper methods (from_hex, to_hex, lerp)
|
|
||||||
- ✅ #103 - Timer objects with pause/resume/cancel functionality
|
|
||||||
|
|
||||||
## Additional Improvements
|
|
||||||
- ✅ Standardized position arguments across all UI classes
|
|
||||||
- Created PyPositionHelper for consistent argument parsing
|
|
||||||
- All classes now accept: (x, y), pos=(x,y), x=x, y=y formats
|
|
||||||
- ✅ Fixed UTF-8 encoding configuration for Python output
|
|
||||||
- Configured PyConfig.stdio_encoding during initialization
|
|
||||||
- Resolved unicode character printing issues
|
|
||||||
|
|
||||||
## Technical Achievements
|
|
||||||
|
|
||||||
### Architecture
|
|
||||||
- Safe two-phase initialization for all Python objects
|
|
||||||
- Consistent constructor patterns across UI hierarchy
|
|
||||||
- Proper shared_ptr lifetime management
|
|
||||||
- Clean separation between C++ implementation and Python API
|
|
||||||
|
|
||||||
### API Consistency
|
|
||||||
- All UI elements follow same initialization patterns
|
|
||||||
- Position arguments work uniformly across all classes
|
|
||||||
- Properties accessible via standard Python attribute access
|
|
||||||
- Methods follow Python naming conventions
|
|
||||||
|
|
||||||
### Developer Experience
|
|
||||||
- Intuitive object construction with sensible defaults
|
|
||||||
- Flexible argument formats reduce boilerplate
|
|
||||||
- Clear error messages for invalid inputs
|
|
||||||
- Comprehensive test coverage for all features
|
|
||||||
|
|
||||||
## Impact on Game Development
|
|
||||||
|
|
||||||
### Before
|
|
||||||
```python
|
|
||||||
# Inconsistent, error-prone API
|
|
||||||
frame = mcrfpy.Frame()
|
|
||||||
frame.x = 100 # Had to set position after creation
|
|
||||||
frame.y = 50
|
|
||||||
caption = mcrfpy.Caption(mcrfpy.default_font, "Hello", 20, 20) # Different argument order
|
|
||||||
grid = mcrfpy.Grid(10, 10, 32, 32, 0, 0) # Confusing parameter order
|
|
||||||
```
|
|
||||||
|
|
||||||
### After
|
|
||||||
```python
|
|
||||||
# Clean, consistent API
|
|
||||||
frame = mcrfpy.Frame(x=100, y=50, children=[
|
|
||||||
mcrfpy.Caption("Hello", pos=(20, 20)),
|
|
||||||
mcrfpy.Sprite("icon.png", (10, 10))
|
|
||||||
])
|
|
||||||
grid = mcrfpy.Grid(size=(10, 10), pos=(0, 0))
|
|
||||||
|
|
||||||
# Advanced features
|
|
||||||
timer = mcrfpy.Timer("animation", update_frame, 16)
|
|
||||||
timer.pause() # Pause during menu
|
|
||||||
timer.resume() # Resume when gameplay continues
|
|
||||||
|
|
||||||
player.move(velocity * delta_time) # Vector math works naturally
|
|
||||||
ui_theme = mcrfpy.Color.from_hex("#2D3436")
|
|
||||||
```
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
With Phases 1-3 complete, the codebase is ready for:
|
|
||||||
- Phase 4: Event System & Animations (advanced interactivity)
|
|
||||||
- Phase 5: Scene Management (transitions, lifecycle)
|
|
||||||
- Phase 6: Audio System (procedural generation, effects)
|
|
||||||
- Phase 7: Optimization (sprite batching, profiling)
|
|
||||||
|
|
||||||
The foundation is now solid for building sophisticated roguelike games with McRogueFace.
|
|
|
@ -1,167 +0,0 @@
|
||||||
# RenderTexture Overhaul Design Document
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This document outlines the design for implementing RenderTexture support across all UIDrawable classes in McRogueFace. This is Issue #6 and represents a major architectural change to the rendering system.
|
|
||||||
|
|
||||||
## Goals
|
|
||||||
|
|
||||||
1. **Automatic Clipping**: Children rendered outside parent bounds should be clipped
|
|
||||||
2. **Off-screen Rendering**: Enable post-processing effects and complex compositing
|
|
||||||
3. **Performance**: Cache static content, only re-render when changed
|
|
||||||
4. **Backward Compatibility**: Existing code should continue to work
|
|
||||||
|
|
||||||
## Current State
|
|
||||||
|
|
||||||
### Classes Already Using RenderTexture:
|
|
||||||
- **UIGrid**: Uses a 1920x1080 RenderTexture for compositing grid view
|
|
||||||
- **SceneTransition**: Uses two 1024x768 RenderTextures for transitions
|
|
||||||
- **HeadlessRenderer**: Uses RenderTexture for headless mode
|
|
||||||
|
|
||||||
### Classes Using Direct Rendering:
|
|
||||||
- **UIFrame**: Renders box and children directly
|
|
||||||
- **UICaption**: Renders text directly
|
|
||||||
- **UISprite**: Renders sprite directly
|
|
||||||
|
|
||||||
## Design Decisions
|
|
||||||
|
|
||||||
### 1. Opt-in Architecture
|
|
||||||
|
|
||||||
Not all UIDrawables need RenderTextures. We'll use an opt-in approach:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
class UIDrawable {
|
|
||||||
protected:
|
|
||||||
// RenderTexture support (opt-in)
|
|
||||||
std::unique_ptr<sf::RenderTexture> render_texture;
|
|
||||||
sf::Sprite render_sprite;
|
|
||||||
bool use_render_texture = false;
|
|
||||||
bool render_dirty = true;
|
|
||||||
|
|
||||||
// Enable RenderTexture for this drawable
|
|
||||||
void enableRenderTexture(unsigned int width, unsigned int height);
|
|
||||||
void updateRenderTexture();
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. When to Use RenderTexture
|
|
||||||
|
|
||||||
RenderTextures will be enabled for:
|
|
||||||
1. **UIFrame with clipping enabled** (new property: `clip_children = true`)
|
|
||||||
2. **UIDrawables with effects** (future: shaders, blend modes)
|
|
||||||
3. **Complex composites** (many children that rarely change)
|
|
||||||
|
|
||||||
### 3. Render Flow
|
|
||||||
|
|
||||||
```
|
|
||||||
Standard Flow:
|
|
||||||
render() → render directly to target
|
|
||||||
|
|
||||||
RenderTexture Flow:
|
|
||||||
render() → if dirty → clear RT → render to RT → dirty = false
|
|
||||||
→ draw RT sprite to target
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Dirty Flag Management
|
|
||||||
|
|
||||||
Mark as dirty when:
|
|
||||||
- Properties change (position, size, color, etc.)
|
|
||||||
- Children added/removed
|
|
||||||
- Child marked as dirty (propagate up)
|
|
||||||
- Animation frame
|
|
||||||
|
|
||||||
### 5. Size Management
|
|
||||||
|
|
||||||
RenderTexture size options:
|
|
||||||
1. **Fixed Size**: Set at creation (current UIGrid approach)
|
|
||||||
2. **Dynamic Size**: Match bounds, recreate on resize
|
|
||||||
3. **Pooled Sizes**: Use standard sizes from pool
|
|
||||||
|
|
||||||
We'll use **Dynamic Size** with lazy creation.
|
|
||||||
|
|
||||||
## Implementation Plan
|
|
||||||
|
|
||||||
### Phase 1: Base Infrastructure (This PR)
|
|
||||||
1. Add RenderTexture members to UIDrawable
|
|
||||||
2. Add `enableRenderTexture()` method
|
|
||||||
3. Implement dirty flag system
|
|
||||||
4. Add `clip_children` property to UIFrame
|
|
||||||
|
|
||||||
### Phase 2: UIFrame Implementation
|
|
||||||
1. Update UIFrame::render() to use RenderTexture when clipping
|
|
||||||
2. Test with nested frames
|
|
||||||
3. Verify clipping works correctly
|
|
||||||
|
|
||||||
### Phase 3: Performance Optimization
|
|
||||||
1. Implement texture pooling
|
|
||||||
2. Add dirty flag propagation
|
|
||||||
3. Profile and optimize
|
|
||||||
|
|
||||||
### Phase 4: Extended Features
|
|
||||||
1. Blur/glow effects using RenderTexture
|
|
||||||
2. Viewport-based rendering (#8)
|
|
||||||
3. Screenshot improvements
|
|
||||||
|
|
||||||
## API Changes
|
|
||||||
|
|
||||||
### Python API:
|
|
||||||
```python
|
|
||||||
# Enable clipping on frames
|
|
||||||
frame.clip_children = True # New property
|
|
||||||
|
|
||||||
# Future: effects
|
|
||||||
frame.blur_amount = 5.0
|
|
||||||
sprite.glow_color = Color(255, 200, 100)
|
|
||||||
```
|
|
||||||
|
|
||||||
### C++ API:
|
|
||||||
```cpp
|
|
||||||
// Enable RenderTexture
|
|
||||||
frame->enableRenderTexture(width, height);
|
|
||||||
frame->setClipChildren(true);
|
|
||||||
|
|
||||||
// Mark dirty
|
|
||||||
frame->markDirty();
|
|
||||||
```
|
|
||||||
|
|
||||||
## Performance Considerations
|
|
||||||
|
|
||||||
1. **Memory**: Each RenderTexture uses GPU memory (width * height * 4 bytes)
|
|
||||||
2. **Creation Cost**: Creating RenderTextures is expensive, use pooling
|
|
||||||
3. **Clear Cost**: Clearing large RenderTextures each frame is costly
|
|
||||||
4. **Bandwidth**: Drawing to RenderTexture then to screen doubles bandwidth
|
|
||||||
|
|
||||||
## Migration Strategy
|
|
||||||
|
|
||||||
1. All existing code continues to work (direct rendering by default)
|
|
||||||
2. Gradually enable RenderTexture for specific use cases
|
|
||||||
3. Profile before/after to ensure performance gains
|
|
||||||
4. Document best practices
|
|
||||||
|
|
||||||
## Risks and Mitigation
|
|
||||||
|
|
||||||
| Risk | Mitigation |
|
|
||||||
|------|------------|
|
|
||||||
| Performance regression | Opt-in design, profile extensively |
|
|
||||||
| Memory usage increase | Texture pooling, size limits |
|
|
||||||
| Complexity increase | Clear documentation, examples |
|
|
||||||
| Integration issues | Extensive testing with SceneTransition |
|
|
||||||
|
|
||||||
## Success Criteria
|
|
||||||
|
|
||||||
1. ✓ Frames can clip children to bounds
|
|
||||||
2. ✓ No performance regression for direct rendering
|
|
||||||
3. ✓ Scene transitions continue to work
|
|
||||||
4. ✓ Memory usage is reasonable
|
|
||||||
5. ✓ API is intuitive and documented
|
|
||||||
|
|
||||||
## Future Extensions
|
|
||||||
|
|
||||||
1. **Shader Support** (#106): RenderTextures enable post-processing shaders
|
|
||||||
2. **Particle Systems** (#107): Render particles to texture for effects
|
|
||||||
3. **Caching**: Static UI elements cached in RenderTextures
|
|
||||||
4. **Resolution Independence**: RenderTextures for DPI scaling
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
This design provides a foundation for professional rendering capabilities while maintaining backward compatibility and performance. The opt-in approach allows gradual adoption and testing.
|
|
803
ROADMAP.md
|
@ -1,803 +0,0 @@
|
||||||
# McRogueFace - Development Roadmap
|
|
||||||
|
|
||||||
## 🚨 URGENT PRIORITIES - July 9, 2025 🚨
|
|
||||||
|
|
||||||
### IMMEDIATE ACTION REQUIRED (Next 48 Hours)
|
|
||||||
|
|
||||||
**CRITICAL DEADLINE**: RoguelikeDev Tutorial Event starts July 15 - Need to advertise by July 11!
|
|
||||||
|
|
||||||
#### 1. Tutorial Emergency Plan (2 DAYS)
|
|
||||||
- [ ] **Day 1 (July 9)**: Parts 1-2 (Setup, Moving @, Drawing Map, Entities)
|
|
||||||
- [ ] **Day 2 (July 10)**: Parts 3-4 (FOV, Combat/AI)
|
|
||||||
- [ ] **July 11**: Announce on r/roguelikedev with 4 completed parts
|
|
||||||
- [ ] **July 12-14**: Complete remaining 10 parts before event starts
|
|
||||||
|
|
||||||
#### 1b. Sizzle Reel Demo (URGENT)
|
|
||||||
- [ ] **Expand animation_sizzle_reel_working.py** with Grid/Entity demos:
|
|
||||||
- Grid scrolling and zooming animations
|
|
||||||
- Entity movement patterns (patrol, chase, flee)
|
|
||||||
- Particle effects using entity spawning
|
|
||||||
- Tile animation demonstrations
|
|
||||||
- Color cycling and transparency effects
|
|
||||||
- Mass entity choreography (100+ entities)
|
|
||||||
- Performance stress test with 1000+ entities
|
|
||||||
|
|
||||||
#### 2. TCOD Integration Sprint ✅ COMPLETE!
|
|
||||||
- [x] **UIGrid TCOD Integration** (8 hours) ✅ COMPLETED!
|
|
||||||
- ✅ Add TCODMap* to UIGrid constructor with proper lifecycle
|
|
||||||
- ✅ Implement complete Dijkstra pathfinding system
|
|
||||||
- ✅ Create mcrfpy.libtcod submodule with Python bindings
|
|
||||||
- ✅ Fix critical PyArg bug preventing Color object assignments
|
|
||||||
- ✅ Implement FOV with perspective rendering
|
|
||||||
- [ ] Add batch operations for NumPy-style access (deferred)
|
|
||||||
- [ ] Create CellView for ergonomic .at((x,y)) access (deferred)
|
|
||||||
- [x] **UIEntity Pathfinding** (4 hours) ✅ COMPLETED!
|
|
||||||
- ✅ Implement Dijkstra maps for multiple targets in UIGrid
|
|
||||||
- ✅ Add path_to(target) method using A* to UIEntity
|
|
||||||
- ✅ Cache paths in UIEntity for performance
|
|
||||||
|
|
||||||
#### 3. Performance Critical Path
|
|
||||||
- [ ] **Implement SpatialHash** for 10,000+ entities (2 hours)
|
|
||||||
- [ ] **Add dirty flag system** to UIGrid (1 hour)
|
|
||||||
- [ ] **Batch update context managers** (2 hours)
|
|
||||||
- [ ] **Memory pool for entities** (2 hours)
|
|
||||||
|
|
||||||
#### 4. Bug Fixing Pipeline
|
|
||||||
- [ ] Set up GitHub Issues automation
|
|
||||||
- [ ] Create test for each bug before fixing
|
|
||||||
- [ ] Track: Memory leaks, Segfaults, Python/C++ boundary errors
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 STRATEGIC ARCHITECTURE VISION
|
|
||||||
|
|
||||||
### Three-Layer Grid Architecture (From Compass Research)
|
|
||||||
Following successful roguelike patterns (Caves of Qud, Cogmind, DCSS):
|
|
||||||
|
|
||||||
1. **Visual Layer** (UIGridPoint) - Sprites, colors, animations
|
|
||||||
2. **World State Layer** (TCODMap) - Walkability, transparency, physics
|
|
||||||
3. **Entity Perspective Layer** (UIGridPointState) - Per-entity FOV, knowledge
|
|
||||||
|
|
||||||
### Performance Architecture (Critical for 1000x1000 maps)
|
|
||||||
- **Spatial Hashing** for entity queries (not quadtrees!)
|
|
||||||
- **Batch Operations** with context managers (10-100x speedup)
|
|
||||||
- **Memory Pooling** for entities and components
|
|
||||||
- **Dirty Flag System** to avoid unnecessary updates
|
|
||||||
- **Zero-Copy NumPy Integration** via buffer protocol
|
|
||||||
|
|
||||||
### Key Insight from Research
|
|
||||||
"Minimizing Python/C++ boundary crossings matters more than individual function complexity"
|
|
||||||
- Batch everything possible
|
|
||||||
- Use context managers for logical operations
|
|
||||||
- Expose arrays, not individual cells
|
|
||||||
- Profile and optimize hot paths only
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Project Status: 🎉 ALPHA 0.1 RELEASE! 🎉
|
|
||||||
|
|
||||||
**Current State**: Documentation system complete, TCOD integration urgent
|
|
||||||
**Latest Update**: Completed Phase 7 documentation infrastructure (2025-07-08)
|
|
||||||
**Branch**: alpha_streamline_2
|
|
||||||
**Open Issues**: ~46 remaining + URGENT TCOD/Tutorial work
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 TCOD Integration Implementation Details
|
|
||||||
|
|
||||||
### Phase 1: Core UIGrid Integration (Day 1 Morning)
|
|
||||||
```cpp
|
|
||||||
// UIGrid.h additions
|
|
||||||
class UIGrid : public UIDrawable {
|
|
||||||
private:
|
|
||||||
TCODMap* world_state; // Add TCOD map
|
|
||||||
std::unordered_map<int, UIGridPointState*> entity_perspectives;
|
|
||||||
bool batch_mode = false;
|
|
||||||
std::vector<CellUpdate> pending_updates;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 2: Python Bindings (Day 1 Afternoon)
|
|
||||||
```python
|
|
||||||
# New API surface
|
|
||||||
grid = mcrfpy.Grid(100, 100)
|
|
||||||
grid.compute_fov(player.x, player.y, radius=10) # Returns visible cells
|
|
||||||
grid.at((x, y)).walkable = False # Ergonomic access
|
|
||||||
with grid.batch_update(): # Context manager for performance
|
|
||||||
# All updates batched
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 3: Entity Integration (Day 2 Morning)
|
|
||||||
```python
|
|
||||||
# UIEntity additions
|
|
||||||
entity.path_to(target_x, target_y) # A* pathfinding
|
|
||||||
entity.flee_from(threat) # Dijkstra map
|
|
||||||
entity.can_see(other_entity) # FOV check
|
|
||||||
```
|
|
||||||
|
|
||||||
### Critical Success Factors:
|
|
||||||
1. **Batch everything** - Never update single cells in loops
|
|
||||||
2. **Lazy evaluation** - Only compute FOV for entities that need it
|
|
||||||
3. **Sparse storage** - Don't store full grids per entity
|
|
||||||
4. **Profile early** - Find the 20% of code taking 80% of time
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Recent Achievements
|
|
||||||
|
|
||||||
### 2025-07-10: Complete FOV, A* Pathfinding & GUI Text Widgets! 👁️🗺️⌨️
|
|
||||||
**Engine Feature Sprint - Major Capabilities Added**
|
|
||||||
- ✅ Complete FOV (Field of View) system with perspective rendering
|
|
||||||
- UIGrid.perspective property controls which entity's view to render
|
|
||||||
- Three-layer overlay system: unexplored (black), explored (dark), visible (normal)
|
|
||||||
- Per-entity visibility state tracking with UIGridPointState
|
|
||||||
- Perfect knowledge updates - only explored areas persist
|
|
||||||
- ✅ A* Pathfinding implementation
|
|
||||||
- Entity.path_to(x, y) method for direct pathfinding
|
|
||||||
- UIGrid compute_astar() and get_astar_path() methods
|
|
||||||
- Path caching in entities for performance
|
|
||||||
- Complete test suite comparing A* vs Dijkstra performance
|
|
||||||
- ✅ GUI Text Input Widget System
|
|
||||||
- Full-featured TextInputWidget class with cursor, selection, scrolling
|
|
||||||
- Improved widget with proper text rendering and multi-line support
|
|
||||||
- Example showcase demonstrating multiple input fields
|
|
||||||
- Foundation for in-game consoles, chat systems, and text entry
|
|
||||||
- ✅ Sizzle Reel Demos
|
|
||||||
- path_vision_sizzle_reel.py combines pathfinding with FOV
|
|
||||||
- Interactive visibility demos showing real-time FOV updates
|
|
||||||
- Performance demonstrations with multiple entities
|
|
||||||
|
|
||||||
### 2025-07-09: Dijkstra Pathfinding & Critical Bug Fix! 🗺️
|
|
||||||
**TCOD Integration Sprint - Major Progress**
|
|
||||||
- ✅ Complete Dijkstra pathfinding implementation in UIGrid
|
|
||||||
- compute_dijkstra(), get_dijkstra_distance(), get_dijkstra_path() methods
|
|
||||||
- Full TCODMap and TCODDijkstra integration with proper memory management
|
|
||||||
- Comprehensive test suite with both headless and interactive demos
|
|
||||||
- ✅ **CRITICAL FIX**: PyArg bug in UIGridPoint color setter
|
|
||||||
- Now supports both mcrfpy.Color objects and (r,g,b,a) tuples
|
|
||||||
- Eliminated mysterious "SystemError: new style getargs format" crashes
|
|
||||||
- Proper error handling and exception propagation
|
|
||||||
- ✅ mcrfpy.libtcod submodule with Python bindings
|
|
||||||
- dijkstra_compute(), dijkstra_get_distance(), dijkstra_get_path()
|
|
||||||
- line() function for corridor generation
|
|
||||||
- Foundation ready for FOV implementation
|
|
||||||
- ✅ Test consolidation: 6 broken demos → 2 clean, working versions
|
|
||||||
|
|
||||||
### 2025-07-08: PyArgHelpers Infrastructure Complete! 🔧
|
|
||||||
**Standardized Python API Argument Parsing**
|
|
||||||
- Unified position handling: (x, y) tuples or separate x, y args
|
|
||||||
- Consistent size parsing: (w, h) tuples or width, height args
|
|
||||||
- Grid-specific helpers for tile-based positioning
|
|
||||||
- Proper conflict detection between positional and keyword args
|
|
||||||
- All UI components migrated: Frame, Caption, Sprite, Grid, Entity
|
|
||||||
- Improved error messages: "Value must be a number (int or float)"
|
|
||||||
- Foundation for Phase 7 documentation efforts
|
|
||||||
|
|
||||||
### 2025-07-05: ALPHA 0.1 ACHIEVED! 🎊🍾
|
|
||||||
**All Alpha Blockers Resolved!**
|
|
||||||
- Z-order rendering with performance optimization (Issue #63)
|
|
||||||
- Python Sequence Protocol for collections (Issue #69)
|
|
||||||
- Comprehensive Animation System (Issue #59)
|
|
||||||
- Moved RenderTexture to Beta (not needed for Alpha)
|
|
||||||
- **McRogueFace is ready for Alpha release!**
|
|
||||||
|
|
||||||
### 2025-07-05: Z-order Rendering Complete! 🎉
|
|
||||||
**Issue #63 Resolved**: Consistent z-order rendering with performance optimization
|
|
||||||
- Dirty flag pattern prevents unnecessary per-frame sorting
|
|
||||||
- Lazy sorting for both Scene elements and Frame children
|
|
||||||
- Frame children now respect z_index (fixed inconsistency)
|
|
||||||
- Automatic dirty marking on z_index changes and collection modifications
|
|
||||||
- Performance: O(1) check for static scenes vs O(n log n) every frame
|
|
||||||
|
|
||||||
### 2025-07-05: Python Sequence Protocol Complete! 🎉
|
|
||||||
**Issue #69 Resolved**: Full sequence protocol implementation for collections
|
|
||||||
- Complete __setitem__, __delitem__, __contains__ support
|
|
||||||
- Slice operations with extended slice support (step != 1)
|
|
||||||
- Concatenation (+) and in-place concatenation (+=) with validation
|
|
||||||
- Negative indexing throughout, index() and count() methods
|
|
||||||
- Type safety: UICollection (Frame/Caption/Sprite/Grid), EntityCollection (Entity only)
|
|
||||||
- Default value support: None for texture/font parameters uses engine defaults
|
|
||||||
|
|
||||||
### 2025-07-05: Animation System Complete! 🎉
|
|
||||||
**Issue #59 Resolved**: Comprehensive animation system with 30+ easing functions
|
|
||||||
- Property-based animations for all UI classes (Frame, Caption, Sprite, Grid, Entity)
|
|
||||||
- Individual color component animation (r/g/b/a)
|
|
||||||
- Sprite sequence animation and text typewriter effects
|
|
||||||
- Pure C++ execution without Python callbacks
|
|
||||||
- Delta animation support for relative values
|
|
||||||
|
|
||||||
### 2025-01-03: Major Stability Update
|
|
||||||
**Major Cleanup**: Removed deprecated registerPyAction system (-180 lines)
|
|
||||||
**Bug Fixes**: 12 critical issues including Grid segfault, Issue #78 (middle click), Entity setters
|
|
||||||
**New Features**: Entity.index() (#73), EntityCollection.extend() (#27), Sprite validation (#33)
|
|
||||||
**Test Coverage**: Comprehensive test suite with timer callback pattern established
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 CURRENT WORK: Alpha Streamline 2 - Major Architecture Improvements
|
|
||||||
|
|
||||||
### Recent Completions:
|
|
||||||
- ✅ **Phase 1-4 Complete** - Foundation, API Polish, Entity Lifecycle, Visibility/Performance
|
|
||||||
- ✅ **Phase 5 Complete** - Window/Scene Architecture fully implemented!
|
|
||||||
- Window singleton with properties (#34)
|
|
||||||
- OOP Scene support with lifecycle methods (#61)
|
|
||||||
- Window resize events (#1)
|
|
||||||
- Scene transitions with animations (#105)
|
|
||||||
- ✅ **Phase 6 Complete** - Rendering Revolution achieved!
|
|
||||||
- Grid background colors (#50) ✅
|
|
||||||
- RenderTexture overhaul (#6) ✅
|
|
||||||
- UIFrame clipping support ✅
|
|
||||||
- Viewport-based rendering (#8) ✅
|
|
||||||
|
|
||||||
### Active Development:
|
|
||||||
- **Branch**: alpha_streamline_2
|
|
||||||
- **Current Phase**: Phase 7 - Documentation & Distribution
|
|
||||||
- **Achievement**: PyArgHelpers infrastructure complete - standardized Python API
|
|
||||||
- **Strategic Vision**: See STRATEGIC_VISION.md for platform roadmap
|
|
||||||
- **Latest**: All UI components now use consistent argument parsing patterns!
|
|
||||||
|
|
||||||
### 🏗️ Architectural Dependencies Map
|
|
||||||
|
|
||||||
```
|
|
||||||
Foundation Layer:
|
|
||||||
├── #71 Base Class (_Drawable)
|
|
||||||
│ ├── #10 Visibility System (needs AABB from base)
|
|
||||||
│ ├── #87 visible property
|
|
||||||
│ └── #88 opacity property
|
|
||||||
│
|
|
||||||
├── #7 Safe Constructors (affects all classes)
|
|
||||||
│ └── Blocks any new class creation until resolved
|
|
||||||
│
|
|
||||||
└── #30 Entity/Grid Integration (lifecycle management)
|
|
||||||
└── Enables reliable entity management
|
|
||||||
|
|
||||||
Window/Scene Layer:
|
|
||||||
├── #34 Window Object
|
|
||||||
│ ├── #61 Scene Object (depends on Window)
|
|
||||||
│ ├── #14 SFML Exposure (helps implement Window)
|
|
||||||
│ └── Future: Multi-window support
|
|
||||||
|
|
||||||
Rendering Layer:
|
|
||||||
└── #6 RenderTexture Overhaul
|
|
||||||
├── Enables clipping
|
|
||||||
├── Off-screen rendering
|
|
||||||
└── Post-processing effects
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚀 Alpha Streamline 2 - Comprehensive Phase Plan
|
|
||||||
|
|
||||||
### Phase 1: Foundation Stabilization (1-2 weeks)
|
|
||||||
**Goal**: Safe, predictable base for all future work
|
|
||||||
```
|
|
||||||
1. #7 - Audit and fix unsafe constructors (CRITICAL - do first!)
|
|
||||||
- Find all manually implemented no-arg constructors
|
|
||||||
- Verify map compatibility requirements
|
|
||||||
- Make pointer-safe or remove
|
|
||||||
|
|
||||||
2. #71 - _Drawable base class implementation
|
|
||||||
- Common properties: x, y, w, h, visible, opacity
|
|
||||||
- Virtual methods: get_bounds(), render()
|
|
||||||
- Proper Python inheritance setup
|
|
||||||
|
|
||||||
3. #87 - visible property
|
|
||||||
- Add to base class
|
|
||||||
- Update all render methods to check
|
|
||||||
|
|
||||||
4. #88 - opacity property (depends on #87)
|
|
||||||
- 0.0-1.0 float range
|
|
||||||
- Apply in render methods
|
|
||||||
|
|
||||||
5. #89 - get_bounds() method
|
|
||||||
- Virtual method returning (x, y, w, h)
|
|
||||||
- Override in each UI class
|
|
||||||
|
|
||||||
6. #98 - move()/resize() convenience methods
|
|
||||||
- move(dx, dy) - relative movement
|
|
||||||
- resize(w, h) - absolute sizing
|
|
||||||
```
|
|
||||||
*Rationale*: Can't build on unsafe foundations. Base class enables all UI improvements.
|
|
||||||
|
|
||||||
### Phase 2: Constructor & API Polish (1 week)
|
|
||||||
**Goal**: Pythonic, intuitive API
|
|
||||||
```
|
|
||||||
1. #101 - Standardize (0,0) defaults for all positions
|
|
||||||
2. #38 - Frame children parameter: Frame(children=[...])
|
|
||||||
3. #42 - Click handler in __init__: Button(click=callback)
|
|
||||||
4. #90 - Grid size tuple: Grid(grid_size=(10, 10))
|
|
||||||
5. #19 - Sprite texture swapping: sprite.texture = new_texture
|
|
||||||
6. #52 - Grid skip out-of-bounds entities (performance)
|
|
||||||
```
|
|
||||||
*Rationale*: Quick wins that make the API more pleasant before bigger changes.
|
|
||||||
|
|
||||||
### Phase 3: Entity Lifecycle Management (1 week)
|
|
||||||
**Goal**: Bulletproof entity/grid relationships
|
|
||||||
```
|
|
||||||
1. #30 - Entity.die() and grid association
|
|
||||||
- Grid.entities.append(e) sets e.grid = self
|
|
||||||
- Grid.entities.remove(e) sets e.grid = None
|
|
||||||
- Entity.die() calls self.grid.remove(self)
|
|
||||||
- Entity can only be in 0 or 1 grid
|
|
||||||
|
|
||||||
2. #93 - Vector arithmetic methods
|
|
||||||
- add, subtract, multiply, divide
|
|
||||||
- distance, normalize, dot product
|
|
||||||
|
|
||||||
3. #94 - Color helper methods
|
|
||||||
- from_hex("#FF0000"), to_hex()
|
|
||||||
- lerp(other_color, t) for interpolation
|
|
||||||
|
|
||||||
4. #103 - Timer objects
|
|
||||||
timer = mcrfpy.Timer("my_timer", callback, 1000)
|
|
||||||
timer.pause()
|
|
||||||
timer.resume()
|
|
||||||
timer.cancel()
|
|
||||||
```
|
|
||||||
*Rationale*: Games need reliable entity management. Timer objects enable entity AI.
|
|
||||||
|
|
||||||
### Phase 4: Visibility & Performance (1-2 weeks)
|
|
||||||
**Goal**: Only render/process what's needed
|
|
||||||
```
|
|
||||||
1. #10 - [UNSCHEDULED] Full visibility system with AABB
|
|
||||||
- Postponed: UIDrawables can exist in multiple collections
|
|
||||||
- Cannot reliably determine screen position due to multiple render contexts
|
|
||||||
- Needs architectural solution for parent-child relationships
|
|
||||||
|
|
||||||
2. #52 - Grid culling (COMPLETED in Phase 2)
|
|
||||||
|
|
||||||
3. #39/40/41 - Name system for finding elements
|
|
||||||
- name="button1" property on all UIDrawables
|
|
||||||
- only_one=True for unique names
|
|
||||||
- scene.find("button1") returns element
|
|
||||||
- collection.find("enemy*") returns list
|
|
||||||
|
|
||||||
4. #104 - Basic profiling/metrics
|
|
||||||
- Frame time tracking
|
|
||||||
- Draw call counting
|
|
||||||
- Python vs C++ time split
|
|
||||||
```
|
|
||||||
*Rationale*: Performance is feature. Finding elements by name is huge QoL.
|
|
||||||
|
|
||||||
### Phase 5: Window/Scene Architecture ✅ COMPLETE! (2025-07-06)
|
|
||||||
**Goal**: Modern, flexible architecture
|
|
||||||
```
|
|
||||||
1. ✅ #34 - Window object (singleton first)
|
|
||||||
window = mcrfpy.Window.get()
|
|
||||||
window.resolution = (1920, 1080)
|
|
||||||
window.fullscreen = True
|
|
||||||
window.vsync = True
|
|
||||||
|
|
||||||
2. ✅ #1 - Window resize events
|
|
||||||
scene.on_resize(self, width, height) callback implemented
|
|
||||||
|
|
||||||
3. ✅ #61 - Scene object (OOP scenes)
|
|
||||||
class MenuScene(mcrfpy.Scene):
|
|
||||||
def on_keypress(self, key, state):
|
|
||||||
# handle input
|
|
||||||
def on_enter(self):
|
|
||||||
# setup UI
|
|
||||||
def on_exit(self):
|
|
||||||
# cleanup
|
|
||||||
def update(self, dt):
|
|
||||||
# frame update
|
|
||||||
|
|
||||||
4. ✅ #14 - SFML exposure research
|
|
||||||
- Completed comprehensive analysis
|
|
||||||
- Recommendation: Direct integration as mcrfpy.sfml
|
|
||||||
- SFML 3.0 migration deferred to late 2025
|
|
||||||
|
|
||||||
5. ✅ #105 - Scene transitions
|
|
||||||
mcrfpy.setScene("menu", "fade", 1.0)
|
|
||||||
# Supports: fade, slide_left, slide_right, slide_up, slide_down
|
|
||||||
```
|
|
||||||
*Result*: Entire window/scene system modernized with OOP design!
|
|
||||||
|
|
||||||
### Phase 6: Rendering Revolution (3-4 weeks) ✅ COMPLETE!
|
|
||||||
**Goal**: Professional rendering capabilities
|
|
||||||
```
|
|
||||||
1. ✅ #50 - Grid background colors [COMPLETED]
|
|
||||||
grid.background_color = mcrfpy.Color(50, 50, 50)
|
|
||||||
- Added background_color property with animation support
|
|
||||||
- Default dark gray background (8, 8, 8, 255)
|
|
||||||
|
|
||||||
2. ✅ #6 - RenderTexture overhaul [COMPLETED]
|
|
||||||
✅ Base infrastructure in UIDrawable
|
|
||||||
✅ UIFrame clip_children property
|
|
||||||
✅ Dirty flag optimization system
|
|
||||||
✅ Nested clipping support
|
|
||||||
✅ UIGrid already has appropriate RenderTexture implementation
|
|
||||||
❌ UICaption/UISprite clipping not needed (no children)
|
|
||||||
|
|
||||||
3. ✅ #8 - Viewport-based rendering [COMPLETED]
|
|
||||||
- Fixed game resolution (window.game_resolution)
|
|
||||||
- Three scaling modes: "center", "stretch", "fit"
|
|
||||||
- Window to game coordinate transformation
|
|
||||||
- Mouse input properly scaled with windowToGameCoords()
|
|
||||||
- Python API fully integrated
|
|
||||||
- Tests: test_viewport_simple.py, test_viewport_visual.py, test_viewport_scaling.py
|
|
||||||
|
|
||||||
4. #106 - Shader support [DEFERRED TO POST-PHASE 7]
|
|
||||||
sprite.shader = mcrfpy.Shader.load("glow.frag")
|
|
||||||
frame.shader_params = {"intensity": 0.5}
|
|
||||||
|
|
||||||
5. #107 - Particle system [DEFERRED TO POST-PHASE 7]
|
|
||||||
emitter = mcrfpy.ParticleEmitter()
|
|
||||||
emitter.texture = spark_texture
|
|
||||||
emitter.emission_rate = 100
|
|
||||||
emitter.lifetime = (0.5, 2.0)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Phase 6 Achievement Summary**:
|
|
||||||
- Grid backgrounds (#50) ✅ - Customizable background colors with animation
|
|
||||||
- RenderTexture overhaul (#6) ✅ - UIFrame clipping with opt-in architecture
|
|
||||||
- Viewport rendering (#8) ✅ - Three scaling modes with coordinate transformation
|
|
||||||
- UIGrid already had optimal RenderTexture implementation for its use case
|
|
||||||
- UICaption/UISprite clipping unnecessary (no children to clip)
|
|
||||||
- Performance optimized with dirty flag system
|
|
||||||
- Backward compatibility preserved throughout
|
|
||||||
- Effects/Shader/Particle systems deferred for focused delivery
|
|
||||||
|
|
||||||
*Rationale*: This unlocks professional visual effects but is complex.
|
|
||||||
|
|
||||||
### Phase 7: Documentation & Distribution (1-2 weeks)
|
|
||||||
**Goal**: Ready for the world
|
|
||||||
```
|
|
||||||
1. ✅ #85 - Replace all "docstring" placeholders [COMPLETED 2025-07-08]
|
|
||||||
2. ✅ #86 - Add parameter documentation [COMPLETED 2025-07-08]
|
|
||||||
3. ✅ #108 - Generate .pyi type stubs for IDE support [COMPLETED 2025-07-08]
|
|
||||||
4. ❌ #70 - PyPI wheel preparation [CANCELLED - Architectural mismatch]
|
|
||||||
5. API reference generator tool
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📋 Critical Path & Parallel Tracks
|
|
||||||
|
|
||||||
### 🔴 **Critical Path** (Must do in order)
|
|
||||||
**Safe Constructors (#7)** → **Base Class (#71)** → **Visibility (#10)** → **Window (#34)** → **Scene (#61)**
|
|
||||||
|
|
||||||
### 🟡 **Parallel Tracks** (Can be done alongside critical path)
|
|
||||||
|
|
||||||
**Track A: Entity Systems**
|
|
||||||
- Entity/Grid integration (#30)
|
|
||||||
- Timer objects (#103)
|
|
||||||
- Vector/Color helpers (#93, #94)
|
|
||||||
|
|
||||||
**Track B: API Polish**
|
|
||||||
- Constructor improvements (#101, #38, #42, #90)
|
|
||||||
- Sprite texture swap (#19)
|
|
||||||
- Name/search system (#39/40/41)
|
|
||||||
|
|
||||||
**Track C: Performance**
|
|
||||||
- Grid culling (#52)
|
|
||||||
- Visibility culling (part of #10)
|
|
||||||
- Profiling tools (#104)
|
|
||||||
|
|
||||||
### 💎 **Quick Wins to Sprinkle Throughout**
|
|
||||||
1. Color helpers (#94) - 1 hour
|
|
||||||
2. Vector methods (#93) - 1 hour
|
|
||||||
3. Grid backgrounds (#50) - 30 minutes
|
|
||||||
4. Default positions (#101) - 30 minutes
|
|
||||||
|
|
||||||
### 🎯 **Recommended Execution Order**
|
|
||||||
|
|
||||||
**Week 1-2**: Foundation (Critical constructors + base class)
|
|
||||||
**Week 3**: Entity lifecycle + API polish
|
|
||||||
**Week 4**: Visibility system + performance
|
|
||||||
**Week 5-6**: Window/Scene architecture
|
|
||||||
**Week 7-9**: Rendering revolution (or defer to gamma)
|
|
||||||
**Week 10**: Documentation + release prep
|
|
||||||
|
|
||||||
### 🆕 **New Issues to Create/Track**
|
|
||||||
|
|
||||||
1. [x] **Timer Objects** - Pythonic timer management (#103) - *Completed Phase 3*
|
|
||||||
2. [ ] **Event System Enhancement** - Mouse enter/leave, drag, right-click
|
|
||||||
3. [ ] **Resource Manager** - Centralized asset loading
|
|
||||||
4. [ ] **Serialization System** - Save/load game state
|
|
||||||
5. [x] **Scene Transitions** - Fade, slide, custom effects (#105) - *Completed Phase 5*
|
|
||||||
6. [x] **Profiling Tools** - Performance metrics (#104) - *Completed Phase 4*
|
|
||||||
7. [ ] **Particle System** - Visual effects framework (#107)
|
|
||||||
8. [ ] **Shader Support** - Custom rendering effects (#106)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Phase 6 Implementation Strategy
|
|
||||||
|
|
||||||
### RenderTexture Overhaul (#6) - Technical Approach
|
|
||||||
|
|
||||||
**Current State**:
|
|
||||||
- UIGrid already uses RenderTexture for entity rendering
|
|
||||||
- Scene transitions use RenderTextures for smooth animations
|
|
||||||
- Direct rendering to window for Frame, Caption, Sprite
|
|
||||||
|
|
||||||
**Implementation Plan**:
|
|
||||||
1. **Base Infrastructure**:
|
|
||||||
- Add `sf::RenderTexture* target` to UIDrawable base
|
|
||||||
- Modify `render()` to check if target exists
|
|
||||||
- If target: render to texture, then draw texture to parent
|
|
||||||
- If no target: render directly (backward compatible)
|
|
||||||
|
|
||||||
2. **Clipping Support**:
|
|
||||||
- Frame enforces bounds on children via RenderTexture
|
|
||||||
- Children outside bounds are automatically clipped
|
|
||||||
- Nested frames create render texture hierarchy
|
|
||||||
|
|
||||||
3. **Performance Optimization**:
|
|
||||||
- Lazy RenderTexture creation (only when needed)
|
|
||||||
- Dirty flag system (only re-render when changed)
|
|
||||||
- Texture pooling for commonly used sizes
|
|
||||||
|
|
||||||
4. **Integration Points**:
|
|
||||||
- Scene transitions already working with RenderTextures
|
|
||||||
- UIGrid can be reference implementation
|
|
||||||
- Test with deeply nested UI structures
|
|
||||||
|
|
||||||
**Quick Wins Before Core Work**:
|
|
||||||
1. **Grid Background (#50)** - 30 min implementation
|
|
||||||
- Add `background_color` and `background_texture` properties
|
|
||||||
- Render before entities in UIGrid::render()
|
|
||||||
- Good warm-up before tackling RenderTexture
|
|
||||||
|
|
||||||
2. **Research Tasks**:
|
|
||||||
- Study UIGrid's current RenderTexture usage
|
|
||||||
- Profile scene transition performance
|
|
||||||
- Identify potential texture size limits
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 NEXT PHASE: Beta Features & Polish
|
|
||||||
|
|
||||||
### Alpha Complete! Moving to Beta Priorities:
|
|
||||||
1. ~~**#69** - Python Sequence Protocol for collections~~ - *Completed! (2025-07-05)*
|
|
||||||
2. ~~**#63** - Z-order rendering for UIDrawables~~ - *Completed! (2025-07-05)*
|
|
||||||
3. ~~**#59** - Animation system~~ - *Completed! (2025-07-05)*
|
|
||||||
4. **#6** - RenderTexture concept - *Extensive Overhaul*
|
|
||||||
5. ~~**#47** - New README.md for Alpha release~~ - *Completed*
|
|
||||||
- [x] **#78** - Middle Mouse Click sends "C" keyboard event - *Fixed*
|
|
||||||
- [x] **#77** - Fix error message copy/paste bug - *Fixed*
|
|
||||||
- [x] **#74** - Add missing `Grid.grid_y` property - *Fixed*
|
|
||||||
- [ ] **#37** - Fix Windows build module import from "scripts" directory - *Isolated Fix*
|
|
||||||
Issue #37 is **on hold** until we have a Windows build environment available. I actually suspect this is already fixed by the updates to the makefile, anyway.
|
|
||||||
- [x] **Entity Property Setters** - Fix "new style getargs format" error - *Fixed*
|
|
||||||
- [x] **Sprite Texture Setter** - Fix "error return without exception set" - *Fixed*
|
|
||||||
- [x] **keypressScene() Validation** - Add proper error handling - *Fixed*
|
|
||||||
|
|
||||||
### 🔄 Complete Iterator System
|
|
||||||
**Status**: Core iterators complete (#72 closed), Grid point iterators still pending
|
|
||||||
|
|
||||||
- [ ] **Grid Point Iterator Implementation** - Complete the remaining grid iteration work
|
|
||||||
- [x] **#73** - Add `entity.index()` method for collection removal - *Fixed*
|
|
||||||
- [x] **#69** ⚠️ **Alpha Blocker** - Refactor all collections to use Python Sequence Protocol - *Completed! (2025-07-05)*
|
|
||||||
|
|
||||||
**Dependencies**: Grid point iterators → #73 entity.index() → #69 Sequence Protocol overhaul
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🗂 ISSUE TRIAGE BY SYSTEM (78 Total Issues)
|
|
||||||
|
|
||||||
### 🎮 Core Engine Systems
|
|
||||||
|
|
||||||
#### Iterator/Collection System (2 issues)
|
|
||||||
- [x] **#73** - Entity index() method for removal - *Fixed*
|
|
||||||
- [x] **#69** ⚠️ **Alpha Blocker** - Sequence Protocol refactor - *Completed! (2025-07-05)*
|
|
||||||
|
|
||||||
#### Python/C++ Integration (7 issues)
|
|
||||||
- [x] **#76** - UIEntity derived type preservation in collections - *Multiple Integrations*
|
|
||||||
- [ ] **#71** - Drawable base class hierarchy - *Extensive Overhaul*
|
|
||||||
- [ ] **#70** - PyPI wheel distribution - *Extensive Overhaul*
|
|
||||||
- [~] **#32** - Executable behave like `python` command - *Extensive Overhaul* *(90% Complete: -h, -V, -c, -m, -i, script execution, sys.argv, --exec all implemented. Only stdin (-) support missing)*
|
|
||||||
- [ ] **#35** - TCOD as built-in module - *Extensive Overhaul*
|
|
||||||
- [~] **#14** - Expose SFML as built-in module - *Research Complete, Implementation Pending*
|
|
||||||
- [ ] **#46** - Subinterpreter threading tests - *Multiple Integrations*
|
|
||||||
|
|
||||||
#### UI/Rendering System (12 issues)
|
|
||||||
- [x] **#63** ⚠️ **Alpha Blocker** - Z-order for UIDrawables - *Multiple Integrations*
|
|
||||||
- [x] **#59** ⚠️ **Alpha Blocker** - Animation system - *Completed! (2025-07-05)*
|
|
||||||
- [ ] **#6** ⚠️ **Alpha Blocker** - RenderTexture for all UIDrawables - *Extensive Overhaul*
|
|
||||||
- [ ] **#10** - UIDrawable visibility/AABB system - *Extensive Overhaul*
|
|
||||||
- [ ] **#8** - UIGrid RenderTexture viewport sizing - *Multiple Integrations*
|
|
||||||
- [x] **#9** - UIGrid RenderTexture resize handling - *Multiple Integrations*
|
|
||||||
- [ ] **#52** - UIGrid skip out-of-bounds entities - *Isolated Fix*
|
|
||||||
- [ ] **#50** - UIGrid background color field - *Isolated Fix*
|
|
||||||
- [ ] **#19** - Sprite get/set texture methods - *Multiple Integrations*
|
|
||||||
- [ ] **#17** - Move UISprite position into sf::Sprite - *Isolated Fix*
|
|
||||||
- [x] **#33** - Sprite index validation against texture range - *Fixed*
|
|
||||||
|
|
||||||
#### Grid/Entity System (6 issues)
|
|
||||||
- [ ] **#30** - Entity/Grid association management (.die() method) - *Extensive Overhaul*
|
|
||||||
- [ ] **#16** - Grid strict mode for entity knowledge/visibility - *Extensive Overhaul*
|
|
||||||
- [ ] **#67** - Grid stitching for infinite worlds - *Extensive Overhaul*
|
|
||||||
- [ ] **#15** - UIGridPointState cleanup and standardization - *Multiple Integrations*
|
|
||||||
- [ ] **#20** - UIGrid get_grid_size standardization - *Multiple Integrations*
|
|
||||||
- [x] **#12** - GridPoint/GridPointState forbid direct init - *Isolated Fix*
|
|
||||||
|
|
||||||
#### Scene/Window Management (5 issues)
|
|
||||||
- [x] **#61** - Scene object encapsulating key callbacks - *Completed Phase 5*
|
|
||||||
- [x] **#34** - Window object for resolution/scaling - *Completed Phase 5*
|
|
||||||
- [ ] **#62** - Multiple windows support - *Extensive Overhaul*
|
|
||||||
- [ ] **#49** - Window resolution & viewport controls - *Multiple Integrations*
|
|
||||||
- [x] **#1** - Scene resize event handling - *Completed Phase 5*
|
|
||||||
|
|
||||||
### 🔧 Quality of Life Features
|
|
||||||
|
|
||||||
#### UI Enhancement Features (8 issues)
|
|
||||||
- [ ] **#39** - Name field on UIDrawables - *Multiple Integrations*
|
|
||||||
- [ ] **#40** - `only_one` arg for unique naming - *Multiple Integrations*
|
|
||||||
- [ ] **#41** - `.find(name)` method for collections - *Multiple Integrations*
|
|
||||||
- [ ] **#38** - `children` arg for Frame initialization - *Isolated Fix*
|
|
||||||
- [ ] **#42** - Click callback arg for UIDrawable init - *Isolated Fix*
|
|
||||||
- [x] **#27** - UIEntityCollection.extend() method - *Fixed*
|
|
||||||
- [ ] **#28** - UICollectionIter for scene ui iteration - *Isolated Fix*
|
|
||||||
- [ ] **#26** - UIEntityCollectionIter implementation - *Isolated Fix*
|
|
||||||
|
|
||||||
### 🧹 Refactoring & Cleanup
|
|
||||||
|
|
||||||
#### Code Cleanup (7 issues)
|
|
||||||
- [x] **#3** ⚠️ **Alpha Blocker** - Remove `McRFPy_API::player_input` - *Completed*
|
|
||||||
- [x] **#2** ⚠️ **Alpha Blocker** - Review `registerPyAction` necessity - *Completed*
|
|
||||||
- [ ] **#7** - Remove unsafe no-argument constructors - *Multiple Integrations*
|
|
||||||
- [ ] **#21** - PyUIGrid dealloc cleanup - *Isolated Fix*
|
|
||||||
- [ ] **#75** - REPL thread separation from SFML window - *Multiple Integrations*
|
|
||||||
|
|
||||||
### 📚 Demo & Documentation
|
|
||||||
|
|
||||||
#### Documentation (2 issues)
|
|
||||||
- [x] **#47** ⚠️ **Alpha Blocker** - Alpha release README.md - *Isolated Fix*
|
|
||||||
- [ ] **#48** - Dependency compilation documentation - *Isolated Fix*
|
|
||||||
|
|
||||||
#### Demo Projects (6 issues)
|
|
||||||
- [ ] **#54** - Jupyter notebook integration demo - *Multiple Integrations*
|
|
||||||
- [ ] **#55** - Hunt the Wumpus AI demo - *Multiple Integrations*
|
|
||||||
- [ ] **#53** - Web interface input demo - *Multiple Integrations* *(New automation API could help)*
|
|
||||||
- [ ] **#45** - Accessibility mode demos - *Multiple Integrations* *(New automation API could help test)*
|
|
||||||
- [ ] **#36** - Dear ImGui integration tests - *Extensive Overhaul*
|
|
||||||
- [ ] **#65** - Python Explorer scene (replaces uitest) - *Extensive Overhaul*
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎮 STRATEGIC DIRECTION
|
|
||||||
|
|
||||||
### Engine Philosophy Maintained
|
|
||||||
- **C++ First**: Performance-critical code stays in C++
|
|
||||||
- **Python Close Behind**: Rich scripting without frame-rate impact
|
|
||||||
- **Game-Ready**: Each improvement should benefit actual game development
|
|
||||||
|
|
||||||
### Architecture Goals
|
|
||||||
1. **Clean Inheritance**: Drawable → UI components, proper type preservation
|
|
||||||
2. **Collection Consistency**: Uniform iteration, indexing, and search patterns
|
|
||||||
3. **Resource Management**: RAII everywhere, proper lifecycle handling
|
|
||||||
4. **Multi-Platform**: Windows/Linux feature parity maintained
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 REFERENCES & CONTEXT
|
|
||||||
|
|
||||||
**Issue Dependencies** (Key Chains):
|
|
||||||
- Iterator System: Grid points → #73 → #69 (Alpha Blocker)
|
|
||||||
- UI Hierarchy: #71 → #63 (Alpha Blocker)
|
|
||||||
- Rendering: #6 (Alpha Blocker) → #8, #9 → #10
|
|
||||||
- Entity System: #30 → #16 → #67
|
|
||||||
- Window Management: #34 → #49, #61 → #62
|
|
||||||
|
|
||||||
**Commit References**:
|
|
||||||
- 167636c: Iterator improvements (UICollection/UIEntityCollection complete)
|
|
||||||
- Recent work: 7DRL 2025 completion, RPATH updates, console improvements
|
|
||||||
|
|
||||||
**Architecture Files**:
|
|
||||||
- Iterator patterns: src/UICollection.cpp, src/UIGrid.cpp
|
|
||||||
- Python integration: src/McRFPy_API.cpp, src/PyObjectUtils.h
|
|
||||||
- Game implementation: src/scripts/ (Crypt of Sokoban complete game)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔮 FUTURE VISION: Pure Python Extension Architecture
|
|
||||||
|
|
||||||
### Concept: McRogueFace as a Traditional Python Package
|
|
||||||
**Status**: Unscheduled - Long-term vision
|
|
||||||
**Complexity**: Major architectural overhaul
|
|
||||||
|
|
||||||
Instead of being a C++ application that embeds Python, McRogueFace could be redesigned as a pure Python extension module that can be installed via `pip install mcrogueface`.
|
|
||||||
|
|
||||||
### Technical Approach
|
|
||||||
1. **Separate Core Engine from Python Embedding**
|
|
||||||
- Extract SFML rendering, audio, and input into C++ extension modules
|
|
||||||
- Remove embedded CPython interpreter
|
|
||||||
- Use Python's C API to expose functionality
|
|
||||||
|
|
||||||
2. **Module Structure**
|
|
||||||
```
|
|
||||||
mcrfpy/
|
|
||||||
├── __init__.py # Pure Python coordinator
|
|
||||||
├── _core.so # C++ rendering/game loop extension
|
|
||||||
├── _sfml.so # SFML bindings
|
|
||||||
├── _audio.so # Audio system bindings
|
|
||||||
└── engine.py # Python game engine logic
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Inverted Control Flow**
|
|
||||||
- Python drives the main loop instead of C++
|
|
||||||
- C++ extensions handle performance-critical operations
|
|
||||||
- Python manages game logic, scenes, and entity systems
|
|
||||||
|
|
||||||
### Benefits
|
|
||||||
- **Standard Python Packaging**: `pip install mcrogueface`
|
|
||||||
- **Virtual Environment Support**: Works with venv, conda, poetry
|
|
||||||
- **Better IDE Integration**: Standard Python development workflow
|
|
||||||
- **Easier Testing**: Use pytest, standard Python testing tools
|
|
||||||
- **Cross-Python Compatibility**: Support multiple Python versions
|
|
||||||
- **Modular Architecture**: Users can import only what they need
|
|
||||||
|
|
||||||
### Challenges
|
|
||||||
- **Major Refactoring**: Complete restructure of codebase
|
|
||||||
- **Performance Considerations**: Python-driven main loop overhead
|
|
||||||
- **Build Complexity**: Multiple extension modules to compile
|
|
||||||
- **Platform Support**: Need wheels for many platform/Python combinations
|
|
||||||
- **API Stability**: Would need careful design to maintain compatibility
|
|
||||||
|
|
||||||
### Implementation Phases (If Pursued)
|
|
||||||
1. **Proof of Concept**: Simple SFML binding as Python extension
|
|
||||||
2. **Core Extraction**: Separate rendering from Python embedding
|
|
||||||
3. **Module Design**: Define clean API boundaries
|
|
||||||
4. **Incremental Migration**: Move systems one at a time
|
|
||||||
5. **Compatibility Layer**: Support existing games during transition
|
|
||||||
|
|
||||||
### Example Usage (Future Vision)
|
|
||||||
```python
|
|
||||||
import mcrfpy
|
|
||||||
from mcrfpy import Scene, Frame, Sprite, Grid
|
|
||||||
|
|
||||||
# Create game directly in Python
|
|
||||||
game = mcrfpy.Game(width=1024, height=768)
|
|
||||||
|
|
||||||
# Define scenes using Python classes
|
|
||||||
class MainMenu(Scene):
|
|
||||||
def on_enter(self):
|
|
||||||
self.ui.append(Frame(100, 100, 200, 50))
|
|
||||||
self.ui.append(Sprite("logo.png", x=400, y=100))
|
|
||||||
|
|
||||||
def on_keypress(self, key, pressed):
|
|
||||||
if key == "ENTER" and pressed:
|
|
||||||
self.game.set_scene("game")
|
|
||||||
|
|
||||||
# Run the game
|
|
||||||
game.add_scene("menu", MainMenu())
|
|
||||||
game.run()
|
|
||||||
```
|
|
||||||
|
|
||||||
This architecture would make McRogueFace a first-class Python citizen, following standard Python packaging conventions while maintaining high performance through C++ extensions.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 IMMEDIATE NEXT STEPS (Priority Order)
|
|
||||||
|
|
||||||
### Today (July 9) - EXECUTE NOW:
|
|
||||||
1. **Start Tutorial Part 1** - Basic setup and @ movement (2 hours)
|
|
||||||
2. **Implement UIGrid.at((x,y))** - CellView pattern (1 hour)
|
|
||||||
3. **Create Grid demo** for sizzle reel (1 hour)
|
|
||||||
4. **Fix any blocking bugs** discovered during tutorial writing
|
|
||||||
|
|
||||||
### Tomorrow (July 10) - CRITICAL PATH:
|
|
||||||
1. **Tutorial Parts 2-4** - Map drawing, entities, FOV, combat
|
|
||||||
2. **Implement compute_fov()** in UIGrid
|
|
||||||
3. **Add batch_update context manager**
|
|
||||||
4. **Expand sizzle reel** with entity choreography
|
|
||||||
|
|
||||||
### July 11 - ANNOUNCEMENT DAY:
|
|
||||||
1. **Polish 4 tutorial parts**
|
|
||||||
2. **Create announcement post** for r/roguelikedev
|
|
||||||
3. **Record sizzle reel video**
|
|
||||||
4. **Submit announcement** by end of day
|
|
||||||
|
|
||||||
### Architecture Decision Log:
|
|
||||||
- **DECIDED**: Use three-layer architecture (visual/world/perspective)
|
|
||||||
- **DECIDED**: Spatial hashing over quadtrees for entities
|
|
||||||
- **DECIDED**: Batch operations are mandatory, not optional
|
|
||||||
- **DECIDED**: TCOD integration as mcrfpy.libtcod submodule
|
|
||||||
- **DECIDED**: Tutorial must showcase McRogueFace strengths, not mimic TCOD
|
|
||||||
|
|
||||||
### Risk Mitigation:
|
|
||||||
- **If TCOD integration delays**: Use pure Python FOV for tutorial
|
|
||||||
- **If performance issues**: Focus on <100x100 maps for demos
|
|
||||||
- **If tutorial incomplete**: Ship with 4 solid parts + roadmap
|
|
||||||
- **If bugs block progress**: Document as "known issues" and continue
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Last Updated: 2025-07-09 (URGENT SPRINT MODE)*
|
|
||||||
*Next Review: July 11 after announcement*
|
|
|
@ -1,257 +0,0 @@
|
||||||
# SFML 3.0 Migration Research for McRogueFace
|
|
||||||
|
|
||||||
## Executive Summary
|
|
||||||
|
|
||||||
SFML 3.0 was released on December 21, 2024, marking the first major version in 12 years. While it offers significant improvements in type safety, modern C++ features, and API consistency, migrating McRogueFace would require substantial effort. Given our plans for `mcrfpy.sfml`, I recommend **deferring migration to SFML 3.0** until after implementing the initial `mcrfpy.sfml` module with SFML 2.6.1.
|
|
||||||
|
|
||||||
## SFML 3.0 Overview
|
|
||||||
|
|
||||||
### Release Highlights
|
|
||||||
- **Release Date**: December 21, 2024
|
|
||||||
- **Development**: 3 years, 1,100+ commits, 41 new contributors
|
|
||||||
- **Major Feature**: C++17 support (now required)
|
|
||||||
- **Audio Backend**: Replaced OpenAL with miniaudio
|
|
||||||
- **Test Coverage**: Expanded to 57%
|
|
||||||
- **New Features**: Scissor and stencil testing
|
|
||||||
|
|
||||||
### Key Breaking Changes
|
|
||||||
|
|
||||||
#### 1. C++ Standard Requirements
|
|
||||||
- **Minimum**: C++17 (was C++03)
|
|
||||||
- **Compilers**: MSVC 16 (VS 2019), GCC 9, Clang 9, AppleClang 12
|
|
||||||
|
|
||||||
#### 2. Event System Overhaul
|
|
||||||
```cpp
|
|
||||||
// SFML 2.x
|
|
||||||
sf::Event event;
|
|
||||||
while (window.pollEvent(event)) {
|
|
||||||
switch (event.type) {
|
|
||||||
case sf::Event::Closed:
|
|
||||||
window.close();
|
|
||||||
break;
|
|
||||||
case sf::Event::KeyPressed:
|
|
||||||
handleKey(event.key.code);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SFML 3.0
|
|
||||||
while (const std::optional event = window.pollEvent()) {
|
|
||||||
if (event->is<sf::Event::Closed>()) {
|
|
||||||
window.close();
|
|
||||||
}
|
|
||||||
else if (const auto* keyPressed = event->getIf<sf::Event::KeyPressed>()) {
|
|
||||||
handleKey(keyPressed->code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. Scoped Enumerations
|
|
||||||
```cpp
|
|
||||||
// SFML 2.x
|
|
||||||
sf::Keyboard::A
|
|
||||||
sf::Mouse::Left
|
|
||||||
|
|
||||||
// SFML 3.0
|
|
||||||
sf::Keyboard::Key::A
|
|
||||||
sf::Mouse::Button::Left
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 4. Resource Loading
|
|
||||||
```cpp
|
|
||||||
// SFML 2.x
|
|
||||||
sf::Texture texture;
|
|
||||||
if (!texture.loadFromFile("image.png")) {
|
|
||||||
// Handle error
|
|
||||||
}
|
|
||||||
|
|
||||||
// SFML 3.0
|
|
||||||
try {
|
|
||||||
sf::Texture texture("image.png");
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
// Handle error
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 5. Geometry Changes
|
|
||||||
```cpp
|
|
||||||
// SFML 2.x
|
|
||||||
sf::FloatRect rect(left, top, width, height);
|
|
||||||
|
|
||||||
// SFML 3.0
|
|
||||||
sf::FloatRect rect({left, top}, {width, height});
|
|
||||||
// Now uses position and size vectors
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 6. CMake Changes
|
|
||||||
```cmake
|
|
||||||
# SFML 2.x
|
|
||||||
find_package(SFML 2.6 COMPONENTS graphics window system audio REQUIRED)
|
|
||||||
target_link_libraries(app sfml-graphics sfml-window sfml-system sfml-audio)
|
|
||||||
|
|
||||||
# SFML 3.0
|
|
||||||
find_package(SFML 3.0 COMPONENTS Graphics Window System Audio REQUIRED)
|
|
||||||
target_link_libraries(app SFML::Graphics SFML::Window SFML::System SFML::Audio)
|
|
||||||
```
|
|
||||||
|
|
||||||
## McRogueFace SFML Usage Analysis
|
|
||||||
|
|
||||||
### Current Usage Statistics
|
|
||||||
- **SFML Version**: 2.6.1
|
|
||||||
- **Integration Level**: Moderate to Heavy
|
|
||||||
- **Affected Files**: ~40+ source files
|
|
||||||
|
|
||||||
### Major Areas Requiring Changes
|
|
||||||
|
|
||||||
#### 1. Event Handling (High Impact)
|
|
||||||
- **Files**: `GameEngine.cpp`, `PyScene.cpp`
|
|
||||||
- **Changes**: Complete rewrite of event loops
|
|
||||||
- **Effort**: High
|
|
||||||
|
|
||||||
#### 2. Enumerations (Medium Impact)
|
|
||||||
- **Files**: `ActionCode.h`, all input handling
|
|
||||||
- **Changes**: Update all keyboard/mouse enum references
|
|
||||||
- **Effort**: Medium (mostly find/replace)
|
|
||||||
|
|
||||||
#### 3. Resource Loading (Medium Impact)
|
|
||||||
- **Files**: `PyTexture.cpp`, `PyFont.cpp`, `McRFPy_API.cpp`
|
|
||||||
- **Changes**: Constructor-based loading with exception handling
|
|
||||||
- **Effort**: Medium
|
|
||||||
|
|
||||||
#### 4. Geometry (Low Impact)
|
|
||||||
- **Files**: Various UI classes
|
|
||||||
- **Changes**: Update Rect construction
|
|
||||||
- **Effort**: Low
|
|
||||||
|
|
||||||
#### 5. CMake Build System (Low Impact)
|
|
||||||
- **Files**: `CMakeLists.txt`
|
|
||||||
- **Changes**: Update find_package and target names
|
|
||||||
- **Effort**: Low
|
|
||||||
|
|
||||||
### Code Examples from McRogueFace
|
|
||||||
|
|
||||||
#### Current Event Loop (GameEngine.cpp)
|
|
||||||
```cpp
|
|
||||||
sf::Event event;
|
|
||||||
while (window && window->pollEvent(event)) {
|
|
||||||
processEvent(event);
|
|
||||||
if (event.type == sf::Event::Closed) {
|
|
||||||
running = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Current Key Mapping (ActionCode.h)
|
|
||||||
```cpp
|
|
||||||
{sf::Keyboard::Key::A, KEY_A},
|
|
||||||
{sf::Keyboard::Key::Left, KEY_LEFT},
|
|
||||||
{sf::Mouse::Left, MOUSEBUTTON_LEFT}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Impact on mcrfpy.sfml Module Plans
|
|
||||||
|
|
||||||
### Option 1: Implement with SFML 2.6.1 First (Recommended)
|
|
||||||
**Pros**:
|
|
||||||
- Faster initial implementation
|
|
||||||
- Stable, well-tested SFML version
|
|
||||||
- Can provide value immediately
|
|
||||||
- Migration can be done later
|
|
||||||
|
|
||||||
**Cons**:
|
|
||||||
- Will require migration work later
|
|
||||||
- API might need changes for SFML 3.0
|
|
||||||
|
|
||||||
### Option 2: Wait and Implement with SFML 3.0
|
|
||||||
**Pros**:
|
|
||||||
- Future-proof implementation
|
|
||||||
- Modern C++ features
|
|
||||||
- No migration needed later
|
|
||||||
|
|
||||||
**Cons**:
|
|
||||||
- Delays `mcrfpy.sfml` implementation
|
|
||||||
- SFML 3.0 is very new (potential bugs)
|
|
||||||
- Less documentation/examples available
|
|
||||||
|
|
||||||
### Option 3: Dual Support
|
|
||||||
**Pros**:
|
|
||||||
- Maximum flexibility
|
|
||||||
- Gradual migration path
|
|
||||||
|
|
||||||
**Cons**:
|
|
||||||
- Significant additional complexity
|
|
||||||
- Maintenance burden
|
|
||||||
- Conditional compilation complexity
|
|
||||||
|
|
||||||
## Migration Strategy Recommendation
|
|
||||||
|
|
||||||
### Phase 1: Current State (Now)
|
|
||||||
1. Continue with SFML 2.6.1
|
|
||||||
2. Implement `mcrfpy.sfml` module as planned
|
|
||||||
3. Design module API to minimize future breaking changes
|
|
||||||
|
|
||||||
### Phase 2: Preparation (3-6 months)
|
|
||||||
1. Monitor SFML 3.0 stability and adoption
|
|
||||||
2. Create migration branch for testing
|
|
||||||
3. Update development environment to C++17
|
|
||||||
|
|
||||||
### Phase 3: Migration (6-12 months)
|
|
||||||
1. Migrate McRogueFace core to SFML 3.0
|
|
||||||
2. Update `mcrfpy.sfml` to match
|
|
||||||
3. Provide migration guide for users
|
|
||||||
|
|
||||||
### Phase 4: Deprecation (12-18 months)
|
|
||||||
1. Deprecate SFML 2.6.1 support
|
|
||||||
2. Focus on SFML 3.0 features
|
|
||||||
|
|
||||||
## Specific Migration Tasks
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
- [ ] Update to C++17 compatible compiler
|
|
||||||
- [ ] Update CMake to 3.16+
|
|
||||||
- [ ] Review all SFML usage locations
|
|
||||||
|
|
||||||
### Core Changes
|
|
||||||
- [ ] Rewrite all event handling loops
|
|
||||||
- [ ] Update all enum references
|
|
||||||
- [ ] Convert resource loading to constructors
|
|
||||||
- [ ] Update geometry construction
|
|
||||||
- [ ] Update CMake configuration
|
|
||||||
|
|
||||||
### mcrfpy.sfml Considerations
|
|
||||||
- [ ] Design API to be version-agnostic where possible
|
|
||||||
- [ ] Use abstraction layer for version-specific code
|
|
||||||
- [ ] Document version requirements clearly
|
|
||||||
|
|
||||||
## Risk Assessment
|
|
||||||
|
|
||||||
### High Risk Areas
|
|
||||||
1. **Event System**: Complete paradigm shift
|
|
||||||
2. **Exception Handling**: New resource loading model
|
|
||||||
3. **Third-party Dependencies**: May not support SFML 3.0 yet
|
|
||||||
|
|
||||||
### Medium Risk Areas
|
|
||||||
1. **Performance**: New implementations may differ
|
|
||||||
2. **Platform Support**: New version may have issues
|
|
||||||
3. **Documentation**: Less community knowledge
|
|
||||||
|
|
||||||
### Low Risk Areas
|
|
||||||
1. **Basic Rendering**: Core concepts unchanged
|
|
||||||
2. **CMake**: Straightforward updates
|
|
||||||
3. **Enums**: Mechanical changes
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
While SFML 3.0 offers significant improvements, the migration effort is substantial. Given that:
|
|
||||||
|
|
||||||
1. SFML 3.0 is very new (released December 2024)
|
|
||||||
2. McRogueFace has heavy SFML integration
|
|
||||||
3. We plan to implement `mcrfpy.sfml` soon
|
|
||||||
4. The event system requires complete rewriting
|
|
||||||
|
|
||||||
**I recommend deferring SFML 3.0 migration** until after successfully implementing `mcrfpy.sfml` with SFML 2.6.1. This allows us to:
|
|
||||||
- Deliver value sooner with `mcrfpy.sfml`
|
|
||||||
- Learn from early adopters of SFML 3.0
|
|
||||||
- Design our module API with migration in mind
|
|
||||||
- Migrate when SFML 3.0 is more mature
|
|
||||||
|
|
||||||
The migration should be revisited in 6-12 months when SFML 3.0 has proven stability and wider adoption.
|
|
|
@ -1,200 +0,0 @@
|
||||||
# SFML Exposure Research (#14)
|
|
||||||
|
|
||||||
## Executive Summary
|
|
||||||
|
|
||||||
After thorough research, I recommend **Option 3: Direct Integration** - implementing our own `mcrfpy.sfml` module with API compatibility to existing python-sfml bindings. This approach gives us full control while maintaining familiarity for developers who have used python-sfml.
|
|
||||||
|
|
||||||
## Current State Analysis
|
|
||||||
|
|
||||||
### McRogueFace SFML Usage
|
|
||||||
|
|
||||||
**Version**: SFML 2.6.1 (confirmed in `modules/SFML/include/SFML/Config.hpp`)
|
|
||||||
|
|
||||||
**Integration Level**: Moderate to Heavy
|
|
||||||
- SFML types appear in most header files
|
|
||||||
- Core rendering depends on `sf::RenderTarget`
|
|
||||||
- Event system uses `sf::Event` directly
|
|
||||||
- Input mapping uses SFML enums
|
|
||||||
|
|
||||||
**SFML Modules Used**:
|
|
||||||
- Graphics (sprites, textures, fonts, shapes)
|
|
||||||
- Window (events, keyboard, mouse)
|
|
||||||
- System (vectors, time, clocks)
|
|
||||||
- Audio (sound effects, music)
|
|
||||||
|
|
||||||
**Already Exposed to Python**:
|
|
||||||
- `mcrfpy.Color` → `sf::Color`
|
|
||||||
- `mcrfpy.Vector` → `sf::Vector2f`
|
|
||||||
- `mcrfpy.Font` → `sf::Font`
|
|
||||||
- `mcrfpy.Texture` → `sf::Texture`
|
|
||||||
|
|
||||||
### Python-SFML Status
|
|
||||||
|
|
||||||
**Official python-sfml (pysfml)**:
|
|
||||||
- Last version: 2.3.2 (supports SFML 2.3.2)
|
|
||||||
- Last meaningful update: ~2019
|
|
||||||
- Not compatible with SFML 2.6.1
|
|
||||||
- Project appears abandoned (domain redirects elsewhere)
|
|
||||||
- GitHub repo has 43 forks but no active maintained fork
|
|
||||||
|
|
||||||
**Alternatives**:
|
|
||||||
- No other major Python SFML bindings found
|
|
||||||
- Most alternatives were archived by 2021
|
|
||||||
|
|
||||||
## Option Analysis
|
|
||||||
|
|
||||||
### Option 1: Use Existing python-sfml
|
|
||||||
**Pros**:
|
|
||||||
- No development work needed
|
|
||||||
- Established API
|
|
||||||
|
|
||||||
**Cons**:
|
|
||||||
- Incompatible with SFML 2.6.1
|
|
||||||
- Would require downgrading to SFML 2.3.2
|
|
||||||
- Abandoned project (security/bug risks)
|
|
||||||
- Installation issues reported
|
|
||||||
|
|
||||||
**Verdict**: Not viable due to version incompatibility and abandonment
|
|
||||||
|
|
||||||
### Option 2: Fork and Update python-sfml
|
|
||||||
**Pros**:
|
|
||||||
- Leverage existing codebase
|
|
||||||
- Maintain API compatibility
|
|
||||||
|
|
||||||
**Cons**:
|
|
||||||
- Significant work to update from 2.3.2 to 2.6.1
|
|
||||||
- Cython complexity
|
|
||||||
- Maintenance burden of external codebase
|
|
||||||
- Still requires users to pip install separately
|
|
||||||
|
|
||||||
**Verdict**: High effort with limited benefit
|
|
||||||
|
|
||||||
### Option 3: Direct Integration (Recommended)
|
|
||||||
**Pros**:
|
|
||||||
- Full control over implementation
|
|
||||||
- Tight integration with McRogueFace
|
|
||||||
- No external dependencies
|
|
||||||
- Can expose exactly what we need
|
|
||||||
- Built-in module (no pip install)
|
|
||||||
- Can maintain API compatibility with python-sfml
|
|
||||||
|
|
||||||
**Cons**:
|
|
||||||
- Development effort required
|
|
||||||
- Need to maintain bindings
|
|
||||||
|
|
||||||
**Verdict**: Best long-term solution
|
|
||||||
|
|
||||||
## Implementation Plan for Direct Integration
|
|
||||||
|
|
||||||
### 1. Module Structure
|
|
||||||
```python
|
|
||||||
# Built-in module: mcrfpy.sfml
|
|
||||||
import mcrfpy.sfml as sf
|
|
||||||
|
|
||||||
# Maintain compatibility with python-sfml API
|
|
||||||
window = sf.RenderWindow(sf.VideoMode(800, 600), "My Window")
|
|
||||||
sprite = sf.Sprite()
|
|
||||||
texture = sf.Texture()
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Priority Classes to Expose
|
|
||||||
|
|
||||||
**Phase 1 - Core Types** (Already partially done):
|
|
||||||
- [x] `sf::Vector2f`, `sf::Vector2i`
|
|
||||||
- [x] `sf::Color`
|
|
||||||
- [ ] `sf::Rect` (FloatRect, IntRect)
|
|
||||||
- [ ] `sf::VideoMode`
|
|
||||||
- [ ] `sf::Time`, `sf::Clock`
|
|
||||||
|
|
||||||
**Phase 2 - Graphics**:
|
|
||||||
- [x] `sf::Texture` (partial)
|
|
||||||
- [x] `sf::Font` (partial)
|
|
||||||
- [ ] `sf::Sprite` (full exposure)
|
|
||||||
- [ ] `sf::Text`
|
|
||||||
- [ ] `sf::Shape` hierarchy
|
|
||||||
- [ ] `sf::View`
|
|
||||||
- [ ] `sf::RenderWindow` (carefully managed)
|
|
||||||
|
|
||||||
**Phase 3 - Window/Input**:
|
|
||||||
- [ ] `sf::Event` and event types
|
|
||||||
- [ ] `sf::Keyboard` enums
|
|
||||||
- [ ] `sf::Mouse` enums
|
|
||||||
- [ ] `sf::Joystick`
|
|
||||||
|
|
||||||
**Phase 4 - Audio** (lower priority):
|
|
||||||
- [ ] `sf::SoundBuffer`
|
|
||||||
- [ ] `sf::Sound`
|
|
||||||
- [ ] `sf::Music`
|
|
||||||
|
|
||||||
### 3. Design Principles
|
|
||||||
|
|
||||||
1. **API Compatibility**: Match python-sfml's API where possible
|
|
||||||
2. **Memory Safety**: Use shared_ptr for resource management
|
|
||||||
3. **Thread Safety**: Consider GIL implications
|
|
||||||
4. **Integration**: Allow mixing with existing mcrfpy types
|
|
||||||
5. **Documentation**: Comprehensive docstrings
|
|
||||||
|
|
||||||
### 4. Technical Considerations
|
|
||||||
|
|
||||||
**Resource Sharing**:
|
|
||||||
- McRogueFace already manages SFML resources
|
|
||||||
- Need to share textures/fonts between mcrfpy and sfml modules
|
|
||||||
- Use the same underlying SFML objects
|
|
||||||
|
|
||||||
**Window Management**:
|
|
||||||
- McRogueFace owns the main window
|
|
||||||
- Expose read-only access or controlled modification
|
|
||||||
- Prevent users from closing/destroying the game window
|
|
||||||
|
|
||||||
**Event Handling**:
|
|
||||||
- Game engine processes events in main loop
|
|
||||||
- Need mechanism to expose events to Python safely
|
|
||||||
- Consider callback system or event queue
|
|
||||||
|
|
||||||
### 5. Implementation Phases
|
|
||||||
|
|
||||||
**Phase 1** (1-2 weeks):
|
|
||||||
- Create `mcrfpy.sfml` module structure
|
|
||||||
- Implement basic types (Vector, Color, Rect)
|
|
||||||
- Add comprehensive tests
|
|
||||||
|
|
||||||
**Phase 2** (2-3 weeks):
|
|
||||||
- Expose graphics classes
|
|
||||||
- Implement resource sharing with mcrfpy
|
|
||||||
- Create example scripts
|
|
||||||
|
|
||||||
**Phase 3** (2-3 weeks):
|
|
||||||
- Add window/input functionality
|
|
||||||
- Integrate with game event loop
|
|
||||||
- Performance optimization
|
|
||||||
|
|
||||||
**Phase 4** (1 week):
|
|
||||||
- Audio support
|
|
||||||
- Documentation
|
|
||||||
- PyPI packaging of mcrfpy.sfml separately
|
|
||||||
|
|
||||||
## Benefits of Direct Integration
|
|
||||||
|
|
||||||
1. **No Version Conflicts**: Always in sync with our SFML version
|
|
||||||
2. **Better Performance**: Direct C++ bindings without Cython overhead
|
|
||||||
3. **Selective Exposure**: Only expose what makes sense for game scripting
|
|
||||||
4. **Integrated Documentation**: Part of McRogueFace docs
|
|
||||||
5. **Future-Proof**: We control the implementation
|
|
||||||
|
|
||||||
## Migration Path for Users
|
|
||||||
|
|
||||||
Users familiar with python-sfml can easily migrate:
|
|
||||||
```python
|
|
||||||
# Old python-sfml code
|
|
||||||
import sfml as sf
|
|
||||||
|
|
||||||
# New McRogueFace code
|
|
||||||
import mcrfpy.sfml as sf
|
|
||||||
# Most code remains the same!
|
|
||||||
```
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
Direct integration as `mcrfpy.sfml` provides the best balance of control, compatibility, and user experience. While it requires development effort, it ensures long-term maintainability and tight integration with McRogueFace's architecture.
|
|
||||||
|
|
||||||
The abandoned state of python-sfml actually presents an opportunity: we can provide a modern, maintained SFML binding for Python as part of McRogueFace, potentially attracting users who need SFML 2.6+ support.
|
|
|
@ -1,226 +0,0 @@
|
||||||
# McRogueFace Strategic Vision: Beyond Alpha
|
|
||||||
|
|
||||||
## 🎯 Three Transformative Directions
|
|
||||||
|
|
||||||
### 1. **The Roguelike Operating System** 🖥️
|
|
||||||
|
|
||||||
Transform McRogueFace into a platform where games are apps:
|
|
||||||
|
|
||||||
#### Core Platform Features
|
|
||||||
- **Game Package Manager**: `mcrf install dungeon-crawler`
|
|
||||||
- **Hot-swappable Game Modules**: Switch between games without restarting
|
|
||||||
- **Shared Asset Library**: Common sprites, sounds, and UI components
|
|
||||||
- **Cross-Game Saves**: Universal character/inventory system
|
|
||||||
- **Multi-Game Sessions**: Run multiple roguelikes simultaneously in tabs
|
|
||||||
|
|
||||||
#### Technical Implementation
|
|
||||||
```python
|
|
||||||
# Future API Example
|
|
||||||
import mcrfpy.platform as platform
|
|
||||||
|
|
||||||
# Install and launch games
|
|
||||||
platform.install("nethack-remake")
|
|
||||||
platform.install("pixel-dungeon-port")
|
|
||||||
|
|
||||||
# Create multi-game session
|
|
||||||
session = platform.MultiGameSession()
|
|
||||||
session.add_tab("nethack-remake", save_file="warrior_lvl_15.sav")
|
|
||||||
session.add_tab("pixel-dungeon-port", new_game=True)
|
|
||||||
session.run()
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. **AI-Native Game Development** 🤖
|
|
||||||
|
|
||||||
Position McRogueFace as the first **AI-first roguelike engine**:
|
|
||||||
|
|
||||||
#### Integrated AI Features
|
|
||||||
- **GPT-Powered NPCs**: Dynamic dialogue and quest generation
|
|
||||||
- **Procedural Content via LLMs**: Describe a dungeon, AI generates it
|
|
||||||
- **AI Dungeon Master**: Adaptive difficulty and narrative
|
|
||||||
- **Code Assistant Integration**: Built-in AI helps write game logic
|
|
||||||
|
|
||||||
#### Revolutionary Possibilities
|
|
||||||
```python
|
|
||||||
# AI-Assisted Game Creation
|
|
||||||
from mcrfpy import ai_tools
|
|
||||||
|
|
||||||
# Natural language level design
|
|
||||||
dungeon = ai_tools.generate_dungeon("""
|
|
||||||
Create a haunted library with 3 floors.
|
|
||||||
First floor: Reading rooms with ghost librarians
|
|
||||||
Second floor: Restricted section with magical traps
|
|
||||||
Third floor: Ancient archive with boss encounter
|
|
||||||
""")
|
|
||||||
|
|
||||||
# AI-driven NPCs
|
|
||||||
npc = ai_tools.create_npc(
|
|
||||||
personality="Grumpy dwarf merchant who secretly loves poetry",
|
|
||||||
knowledge=["local rumors", "item prices", "hidden treasures"],
|
|
||||||
dynamic_dialogue=True
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. **Web-Native Multiplayer Platform** 🌐
|
|
||||||
|
|
||||||
Make McRogueFace the **Discord of Roguelikes**:
|
|
||||||
|
|
||||||
#### Multiplayer Revolution
|
|
||||||
- **Seamless Co-op**: Drop-in/drop-out multiplayer
|
|
||||||
- **Competitive Modes**: Racing, PvP arenas, daily challenges
|
|
||||||
- **Spectator System**: Watch and learn from others
|
|
||||||
- **Cloud Saves**: Play anywhere, sync everywhere
|
|
||||||
- **Social Features**: Guilds, tournaments, leaderboards
|
|
||||||
|
|
||||||
#### WebAssembly Future
|
|
||||||
```python
|
|
||||||
# Future Web API
|
|
||||||
import mcrfpy.web as web
|
|
||||||
|
|
||||||
# Host a game room
|
|
||||||
room = web.create_room("Epic Dungeon Run", max_players=4)
|
|
||||||
room.set_rules(friendly_fire=False, shared_loot=True)
|
|
||||||
room.open_to_public()
|
|
||||||
|
|
||||||
# Stream gameplay
|
|
||||||
stream = web.GameStream(room)
|
|
||||||
stream.to_twitch(channel="awesome_roguelike")
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🏗️ Architecture Evolution Roadmap
|
|
||||||
|
|
||||||
### Phase 1: Beta Foundation (3-4 months)
|
|
||||||
**Focus**: Stability and Polish
|
|
||||||
- Complete RenderTexture system (#6)
|
|
||||||
- Implement save/load system
|
|
||||||
- Add audio mixing and 3D sound
|
|
||||||
- Create plugin architecture
|
|
||||||
- **Deliverable**: Beta release with plugin support
|
|
||||||
|
|
||||||
### Phase 2: Platform Infrastructure (6-8 months)
|
|
||||||
**Focus**: Multi-game Support
|
|
||||||
- Game package format specification
|
|
||||||
- Resource sharing system
|
|
||||||
- Inter-game communication API
|
|
||||||
- Cloud save infrastructure
|
|
||||||
- **Deliverable**: McRogueFace Platform 1.0
|
|
||||||
|
|
||||||
### Phase 3: AI Integration (8-12 months)
|
|
||||||
**Focus**: AI-Native Features
|
|
||||||
- LLM integration framework
|
|
||||||
- Procedural content pipelines
|
|
||||||
- Natural language game scripting
|
|
||||||
- AI behavior trees
|
|
||||||
- **Deliverable**: McRogueFace AI Studio
|
|
||||||
|
|
||||||
### Phase 4: Web Deployment (12-18 months)
|
|
||||||
**Focus**: Browser-based Gaming
|
|
||||||
- WebAssembly compilation
|
|
||||||
- WebRTC multiplayer
|
|
||||||
- Cloud computation for AI
|
|
||||||
- Mobile touch controls
|
|
||||||
- **Deliverable**: play.mcrogueface.com
|
|
||||||
|
|
||||||
## 🎮 Killer App Ideas
|
|
||||||
|
|
||||||
### 1. **Roguelike Maker** (Like Mario Maker)
|
|
||||||
- Visual dungeon editor
|
|
||||||
- Share levels online
|
|
||||||
- Play-test with AI
|
|
||||||
- Community ratings
|
|
||||||
|
|
||||||
### 2. **The Infinite Dungeon**
|
|
||||||
- Persistent world all players explore
|
|
||||||
- Procedurally expands based on player actions
|
|
||||||
- AI Dungeon Master creates personalized quests
|
|
||||||
- Cross-platform play
|
|
||||||
|
|
||||||
### 3. **Roguelike Battle Royale**
|
|
||||||
- 100 players start in connected dungeons
|
|
||||||
- Dungeons collapse, forcing encounters
|
|
||||||
- Last adventurer standing wins
|
|
||||||
- AI-generated commentary
|
|
||||||
|
|
||||||
## 🛠️ Technical Innovations to Pursue
|
|
||||||
|
|
||||||
### 1. **Temporal Debugging**
|
|
||||||
- Rewind game state
|
|
||||||
- Fork timelines for "what-if" scenarios
|
|
||||||
- Visual debugging of entity histories
|
|
||||||
|
|
||||||
### 2. **Neural Tileset Generation**
|
|
||||||
- Train on existing tilesets
|
|
||||||
- Generate infinite variations
|
|
||||||
- Style transfer between games
|
|
||||||
|
|
||||||
### 3. **Quantum Roguelike Mechanics**
|
|
||||||
- Superposition states for entities
|
|
||||||
- Probability-based combat
|
|
||||||
- Observer-effect puzzles
|
|
||||||
|
|
||||||
## 🌍 Community Building Strategy
|
|
||||||
|
|
||||||
### 1. **Education First**
|
|
||||||
- University partnerships
|
|
||||||
- Free curriculum: "Learn Python with Roguelikes"
|
|
||||||
- Summer of Code participation
|
|
||||||
- Student game jams
|
|
||||||
|
|
||||||
### 2. **Open Core Model**
|
|
||||||
- Core engine: MIT licensed
|
|
||||||
- Premium platforms: Cloud, AI, multiplayer
|
|
||||||
- Revenue sharing for content creators
|
|
||||||
- Sponsored tournaments
|
|
||||||
|
|
||||||
### 3. **Developer Ecosystem**
|
|
||||||
- Comprehensive API documentation
|
|
||||||
- Example games and tutorials
|
|
||||||
- Asset marketplace
|
|
||||||
- GitHub integration for mods
|
|
||||||
|
|
||||||
## 🎯 Success Metrics
|
|
||||||
|
|
||||||
### Year 1 Goals
|
|
||||||
- 1,000+ games created on platform
|
|
||||||
- 10,000+ monthly active developers
|
|
||||||
- 3 AAA-quality showcase games
|
|
||||||
- University curriculum adoption
|
|
||||||
|
|
||||||
### Year 2 Goals
|
|
||||||
- 100,000+ monthly active players
|
|
||||||
- $1M in platform transactions
|
|
||||||
- Major game studio partnership
|
|
||||||
- Native VR support
|
|
||||||
|
|
||||||
### Year 3 Goals
|
|
||||||
- #1 roguelike development platform
|
|
||||||
- IPO or acquisition readiness
|
|
||||||
- 1M+ monthly active players
|
|
||||||
- Industry standard for roguelikes
|
|
||||||
|
|
||||||
## 🚀 Next Immediate Actions
|
|
||||||
|
|
||||||
1. **Finish Beta Polish**
|
|
||||||
- Merge alpha_streamline_2 → master
|
|
||||||
- Complete RenderTexture (#6)
|
|
||||||
- Implement basic save/load
|
|
||||||
|
|
||||||
2. **Build Community**
|
|
||||||
- Launch Discord server
|
|
||||||
- Create YouTube tutorials
|
|
||||||
- Host first game jam
|
|
||||||
|
|
||||||
3. **Prototype AI Features**
|
|
||||||
- Simple GPT integration
|
|
||||||
- Procedural room descriptions
|
|
||||||
- Dynamic NPC dialogue
|
|
||||||
|
|
||||||
4. **Plan Platform Architecture**
|
|
||||||
- Design plugin system
|
|
||||||
- Spec game package format
|
|
||||||
- Cloud infrastructure research
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*"McRogueFace: Not just an engine, but a universe of infinite dungeons."*
|
|
||||||
|
|
||||||
Remember: The best platforms create possibilities their creators never imagined. Build for the community you want to see, and they will create wonders.
|
|
16
_test.py
|
@ -1,16 +0,0 @@
|
||||||
import mcrfpy
|
|
||||||
|
|
||||||
# Create a new scene
|
|
||||||
mcrfpy.createScene("intro")
|
|
||||||
|
|
||||||
# Add a text caption
|
|
||||||
caption = mcrfpy.Caption((50, 50), "Welcome to McRogueFace!")
|
|
||||||
caption.size = 48
|
|
||||||
caption.fill_color = (255, 255, 255)
|
|
||||||
|
|
||||||
# Add to scene
|
|
||||||
mcrfpy.sceneUI("intro").append(caption)
|
|
||||||
|
|
||||||
# Switch to the scene
|
|
||||||
mcrfpy.setScene("intro")
|
|
||||||
|
|
|
@ -1,127 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
McRogueFace Automation API Example
|
|
||||||
|
|
||||||
This demonstrates how to use the automation API for testing game UIs.
|
|
||||||
The API is PyAutoGUI-compatible for easy migration of existing tests.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from mcrfpy import automation
|
|
||||||
import mcrfpy
|
|
||||||
import time
|
|
||||||
|
|
||||||
def automation_demo():
|
|
||||||
"""Demonstrate all automation API features"""
|
|
||||||
|
|
||||||
print("=== McRogueFace Automation API Demo ===\n")
|
|
||||||
|
|
||||||
# 1. Screen Information
|
|
||||||
print("1. Screen Information:")
|
|
||||||
screen_size = automation.size()
|
|
||||||
print(f" Screen size: {screen_size[0]}x{screen_size[1]}")
|
|
||||||
|
|
||||||
mouse_pos = automation.position()
|
|
||||||
print(f" Current mouse position: {mouse_pos}")
|
|
||||||
|
|
||||||
on_screen = automation.onScreen(100, 100)
|
|
||||||
print(f" Is (100, 100) on screen? {on_screen}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# 2. Mouse Movement
|
|
||||||
print("2. Mouse Movement:")
|
|
||||||
print(" Moving to center of screen...")
|
|
||||||
center_x, center_y = screen_size[0]//2, screen_size[1]//2
|
|
||||||
automation.moveTo(center_x, center_y, duration=0.5)
|
|
||||||
|
|
||||||
print(" Moving relative by (100, 100)...")
|
|
||||||
automation.moveRel(100, 100, duration=0.5)
|
|
||||||
print()
|
|
||||||
|
|
||||||
# 3. Mouse Clicks
|
|
||||||
print("3. Mouse Clicks:")
|
|
||||||
print(" Single click...")
|
|
||||||
automation.click()
|
|
||||||
time.sleep(0.2)
|
|
||||||
|
|
||||||
print(" Double click...")
|
|
||||||
automation.doubleClick()
|
|
||||||
time.sleep(0.2)
|
|
||||||
|
|
||||||
print(" Right click...")
|
|
||||||
automation.rightClick()
|
|
||||||
time.sleep(0.2)
|
|
||||||
|
|
||||||
print(" Triple click...")
|
|
||||||
automation.tripleClick()
|
|
||||||
print()
|
|
||||||
|
|
||||||
# 4. Keyboard Input
|
|
||||||
print("4. Keyboard Input:")
|
|
||||||
print(" Typing message...")
|
|
||||||
automation.typewrite("Hello from McRogueFace automation!", interval=0.05)
|
|
||||||
|
|
||||||
print(" Pressing Enter...")
|
|
||||||
automation.keyDown("enter")
|
|
||||||
automation.keyUp("enter")
|
|
||||||
|
|
||||||
print(" Hotkey Ctrl+A (select all)...")
|
|
||||||
automation.hotkey("ctrl", "a")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# 5. Drag Operations
|
|
||||||
print("5. Drag Operations:")
|
|
||||||
print(" Dragging from current position to (500, 500)...")
|
|
||||||
automation.dragTo(500, 500, duration=1.0)
|
|
||||||
|
|
||||||
print(" Dragging relative by (-100, -100)...")
|
|
||||||
automation.dragRel(-100, -100, duration=0.5)
|
|
||||||
print()
|
|
||||||
|
|
||||||
# 6. Scroll Operations
|
|
||||||
print("6. Scroll Operations:")
|
|
||||||
print(" Scrolling up 5 clicks...")
|
|
||||||
automation.scroll(5)
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
print(" Scrolling down 5 clicks...")
|
|
||||||
automation.scroll(-5)
|
|
||||||
print()
|
|
||||||
|
|
||||||
# 7. Screenshots
|
|
||||||
print("7. Screenshots:")
|
|
||||||
print(" Taking screenshot...")
|
|
||||||
success = automation.screenshot("automation_demo_screenshot.png")
|
|
||||||
print(f" Screenshot saved: {success}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
print("=== Demo Complete ===")
|
|
||||||
|
|
||||||
def create_test_ui():
|
|
||||||
"""Create a simple UI for testing automation"""
|
|
||||||
print("Creating test UI...")
|
|
||||||
|
|
||||||
# Create a test scene
|
|
||||||
mcrfpy.createScene("automation_test")
|
|
||||||
mcrfpy.setScene("automation_test")
|
|
||||||
|
|
||||||
# Add some UI elements
|
|
||||||
ui = mcrfpy.sceneUI("automation_test")
|
|
||||||
|
|
||||||
# Add a frame
|
|
||||||
frame = mcrfpy.Frame(50, 50, 300, 200)
|
|
||||||
ui.append(frame)
|
|
||||||
|
|
||||||
# Add a caption
|
|
||||||
caption = mcrfpy.Caption(60, 60, "Automation Test UI")
|
|
||||||
ui.append(caption)
|
|
||||||
|
|
||||||
print("Test UI created!")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# Create test UI first
|
|
||||||
create_test_ui()
|
|
||||||
|
|
||||||
# Run automation demo
|
|
||||||
automation_demo()
|
|
||||||
|
|
||||||
print("\nYou can now use the automation API to test your game!")
|
|
|
@ -1,336 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Examples of automation patterns using the proposed --exec flag
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
./mcrogueface game.py --exec automation_basic.py
|
|
||||||
./mcrogueface game.py --exec automation_stress.py --exec monitor.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
# ===== automation_basic.py =====
|
|
||||||
# Basic automation that runs alongside the game
|
|
||||||
|
|
||||||
import mcrfpy
|
|
||||||
from mcrfpy import automation
|
|
||||||
import time
|
|
||||||
|
|
||||||
class GameAutomation:
|
|
||||||
"""Automated testing that runs periodically"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.test_count = 0
|
|
||||||
self.test_results = []
|
|
||||||
|
|
||||||
def run_test_suite(self):
|
|
||||||
"""Called by timer - runs one test per invocation"""
|
|
||||||
test_name = f"test_{self.test_count}"
|
|
||||||
|
|
||||||
try:
|
|
||||||
if self.test_count == 0:
|
|
||||||
# Test main menu
|
|
||||||
self.test_main_menu()
|
|
||||||
elif self.test_count == 1:
|
|
||||||
# Test inventory
|
|
||||||
self.test_inventory()
|
|
||||||
elif self.test_count == 2:
|
|
||||||
# Test combat
|
|
||||||
self.test_combat()
|
|
||||||
else:
|
|
||||||
# All tests complete
|
|
||||||
self.report_results()
|
|
||||||
return
|
|
||||||
|
|
||||||
self.test_results.append((test_name, "PASS"))
|
|
||||||
except Exception as e:
|
|
||||||
self.test_results.append((test_name, f"FAIL: {e}"))
|
|
||||||
|
|
||||||
self.test_count += 1
|
|
||||||
|
|
||||||
def test_main_menu(self):
|
|
||||||
"""Test main menu interactions"""
|
|
||||||
automation.screenshot("test_main_menu_before.png")
|
|
||||||
automation.click(400, 300) # New Game button
|
|
||||||
time.sleep(0.5)
|
|
||||||
automation.screenshot("test_main_menu_after.png")
|
|
||||||
|
|
||||||
def test_inventory(self):
|
|
||||||
"""Test inventory system"""
|
|
||||||
automation.hotkey("i") # Open inventory
|
|
||||||
time.sleep(0.5)
|
|
||||||
automation.screenshot("test_inventory_open.png")
|
|
||||||
|
|
||||||
# Drag item
|
|
||||||
automation.moveTo(100, 200)
|
|
||||||
automation.dragTo(200, 200, duration=0.5)
|
|
||||||
|
|
||||||
automation.hotkey("i") # Close inventory
|
|
||||||
|
|
||||||
def test_combat(self):
|
|
||||||
"""Test combat system"""
|
|
||||||
# Move character
|
|
||||||
automation.keyDown("w")
|
|
||||||
time.sleep(0.5)
|
|
||||||
automation.keyUp("w")
|
|
||||||
|
|
||||||
# Attack
|
|
||||||
automation.click(500, 400)
|
|
||||||
automation.screenshot("test_combat.png")
|
|
||||||
|
|
||||||
def report_results(self):
|
|
||||||
"""Generate test report"""
|
|
||||||
print("\n=== Automation Test Results ===")
|
|
||||||
for test, result in self.test_results:
|
|
||||||
print(f"{test}: {result}")
|
|
||||||
print(f"Total: {len(self.test_results)} tests")
|
|
||||||
|
|
||||||
# Stop the timer
|
|
||||||
mcrfpy.delTimer("automation_suite")
|
|
||||||
|
|
||||||
# Create automation instance and register timer
|
|
||||||
auto = GameAutomation()
|
|
||||||
mcrfpy.setTimer("automation_suite", auto.run_test_suite, 2000) # Run every 2 seconds
|
|
||||||
|
|
||||||
print("Game automation started - tests will run every 2 seconds")
|
|
||||||
|
|
||||||
|
|
||||||
# ===== automation_stress.py =====
|
|
||||||
# Stress testing with random inputs
|
|
||||||
|
|
||||||
import mcrfpy
|
|
||||||
from mcrfpy import automation
|
|
||||||
import random
|
|
||||||
|
|
||||||
class StressTester:
|
|
||||||
"""Randomly interact with the game to find edge cases"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.action_count = 0
|
|
||||||
self.errors = []
|
|
||||||
|
|
||||||
def random_action(self):
|
|
||||||
"""Perform a random UI action"""
|
|
||||||
try:
|
|
||||||
action = random.choice([
|
|
||||||
self.random_click,
|
|
||||||
self.random_key,
|
|
||||||
self.random_drag,
|
|
||||||
self.random_hotkey
|
|
||||||
])
|
|
||||||
action()
|
|
||||||
self.action_count += 1
|
|
||||||
|
|
||||||
# Periodic screenshot
|
|
||||||
if self.action_count % 50 == 0:
|
|
||||||
automation.screenshot(f"stress_test_{self.action_count}.png")
|
|
||||||
print(f"Stress test: {self.action_count} actions performed")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.errors.append((self.action_count, str(e)))
|
|
||||||
|
|
||||||
def random_click(self):
|
|
||||||
x = random.randint(0, 1024)
|
|
||||||
y = random.randint(0, 768)
|
|
||||||
button = random.choice(["left", "right"])
|
|
||||||
automation.click(x, y, button=button)
|
|
||||||
|
|
||||||
def random_key(self):
|
|
||||||
key = random.choice([
|
|
||||||
"a", "b", "c", "d", "w", "s",
|
|
||||||
"space", "enter", "escape",
|
|
||||||
"1", "2", "3", "4", "5"
|
|
||||||
])
|
|
||||||
automation.keyDown(key)
|
|
||||||
automation.keyUp(key)
|
|
||||||
|
|
||||||
def random_drag(self):
|
|
||||||
x1 = random.randint(0, 1024)
|
|
||||||
y1 = random.randint(0, 768)
|
|
||||||
x2 = random.randint(0, 1024)
|
|
||||||
y2 = random.randint(0, 768)
|
|
||||||
automation.moveTo(x1, y1)
|
|
||||||
automation.dragTo(x2, y2, duration=0.2)
|
|
||||||
|
|
||||||
def random_hotkey(self):
|
|
||||||
modifier = random.choice(["ctrl", "alt", "shift"])
|
|
||||||
key = random.choice(["a", "s", "d", "f"])
|
|
||||||
automation.hotkey(modifier, key)
|
|
||||||
|
|
||||||
# Create stress tester and run frequently
|
|
||||||
stress = StressTester()
|
|
||||||
mcrfpy.setTimer("stress_test", stress.random_action, 100) # Every 100ms
|
|
||||||
|
|
||||||
print("Stress testing started - random actions every 100ms")
|
|
||||||
|
|
||||||
|
|
||||||
# ===== monitor.py =====
|
|
||||||
# Performance and state monitoring
|
|
||||||
|
|
||||||
import mcrfpy
|
|
||||||
from mcrfpy import automation
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
|
|
||||||
class PerformanceMonitor:
|
|
||||||
"""Monitor game performance and state"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.samples = []
|
|
||||||
self.start_time = time.time()
|
|
||||||
|
|
||||||
def collect_sample(self):
|
|
||||||
"""Collect performance data"""
|
|
||||||
sample = {
|
|
||||||
"timestamp": time.time() - self.start_time,
|
|
||||||
"fps": mcrfpy.getFPS() if hasattr(mcrfpy, 'getFPS') else 60,
|
|
||||||
"scene": mcrfpy.currentScene(),
|
|
||||||
"memory": self.estimate_memory_usage()
|
|
||||||
}
|
|
||||||
self.samples.append(sample)
|
|
||||||
|
|
||||||
# Log every 10 samples
|
|
||||||
if len(self.samples) % 10 == 0:
|
|
||||||
avg_fps = sum(s["fps"] for s in self.samples[-10:]) / 10
|
|
||||||
print(f"Average FPS (last 10 samples): {avg_fps:.1f}")
|
|
||||||
|
|
||||||
# Save data every 100 samples
|
|
||||||
if len(self.samples) % 100 == 0:
|
|
||||||
self.save_report()
|
|
||||||
|
|
||||||
def estimate_memory_usage(self):
|
|
||||||
"""Estimate memory usage based on scene complexity"""
|
|
||||||
# This is a placeholder - real implementation would use psutil
|
|
||||||
ui_count = len(mcrfpy.sceneUI(mcrfpy.currentScene()))
|
|
||||||
return ui_count * 1000 # Rough estimate in KB
|
|
||||||
|
|
||||||
def save_report(self):
|
|
||||||
"""Save performance report"""
|
|
||||||
with open("performance_report.json", "w") as f:
|
|
||||||
json.dump({
|
|
||||||
"samples": self.samples,
|
|
||||||
"summary": {
|
|
||||||
"total_samples": len(self.samples),
|
|
||||||
"duration": time.time() - self.start_time,
|
|
||||||
"avg_fps": sum(s["fps"] for s in self.samples) / len(self.samples)
|
|
||||||
}
|
|
||||||
}, f, indent=2)
|
|
||||||
print(f"Performance report saved ({len(self.samples)} samples)")
|
|
||||||
|
|
||||||
# Create monitor and start collecting
|
|
||||||
monitor = PerformanceMonitor()
|
|
||||||
mcrfpy.setTimer("performance_monitor", monitor.collect_sample, 1000) # Every second
|
|
||||||
|
|
||||||
print("Performance monitoring started - sampling every second")
|
|
||||||
|
|
||||||
|
|
||||||
# ===== automation_replay.py =====
|
|
||||||
# Record and replay user actions
|
|
||||||
|
|
||||||
import mcrfpy
|
|
||||||
from mcrfpy import automation
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
|
|
||||||
class ActionRecorder:
|
|
||||||
"""Record user actions for replay"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.recording = False
|
|
||||||
self.actions = []
|
|
||||||
self.start_time = None
|
|
||||||
|
|
||||||
def start_recording(self):
|
|
||||||
"""Start recording user actions"""
|
|
||||||
self.recording = True
|
|
||||||
self.actions = []
|
|
||||||
self.start_time = time.time()
|
|
||||||
print("Recording started - perform actions to record")
|
|
||||||
|
|
||||||
# Register callbacks for all input types
|
|
||||||
mcrfpy.registerPyAction("record_click", self.record_click)
|
|
||||||
mcrfpy.registerPyAction("record_key", self.record_key)
|
|
||||||
|
|
||||||
# Map all mouse buttons
|
|
||||||
for button in range(3):
|
|
||||||
mcrfpy.registerInputAction(8192 + button, "record_click")
|
|
||||||
|
|
||||||
# Map common keys
|
|
||||||
for key in range(256):
|
|
||||||
mcrfpy.registerInputAction(4096 + key, "record_key")
|
|
||||||
|
|
||||||
def record_click(self, action_type):
|
|
||||||
"""Record mouse click"""
|
|
||||||
if not self.recording or action_type != "start":
|
|
||||||
return
|
|
||||||
|
|
||||||
pos = automation.position()
|
|
||||||
self.actions.append({
|
|
||||||
"type": "click",
|
|
||||||
"time": time.time() - self.start_time,
|
|
||||||
"x": pos[0],
|
|
||||||
"y": pos[1]
|
|
||||||
})
|
|
||||||
|
|
||||||
def record_key(self, action_type):
|
|
||||||
"""Record key press"""
|
|
||||||
if not self.recording or action_type != "start":
|
|
||||||
return
|
|
||||||
|
|
||||||
# This is simplified - real implementation would decode the key
|
|
||||||
self.actions.append({
|
|
||||||
"type": "key",
|
|
||||||
"time": time.time() - self.start_time,
|
|
||||||
"key": "unknown"
|
|
||||||
})
|
|
||||||
|
|
||||||
def stop_recording(self):
|
|
||||||
"""Stop recording and save"""
|
|
||||||
self.recording = False
|
|
||||||
with open("recorded_actions.json", "w") as f:
|
|
||||||
json.dump(self.actions, f, indent=2)
|
|
||||||
print(f"Recording stopped - {len(self.actions)} actions saved")
|
|
||||||
|
|
||||||
def replay_actions(self):
|
|
||||||
"""Replay recorded actions"""
|
|
||||||
print("Replaying recorded actions...")
|
|
||||||
|
|
||||||
with open("recorded_actions.json", "r") as f:
|
|
||||||
actions = json.load(f)
|
|
||||||
|
|
||||||
start_time = time.time()
|
|
||||||
action_index = 0
|
|
||||||
|
|
||||||
def replay_next():
|
|
||||||
nonlocal action_index
|
|
||||||
if action_index >= len(actions):
|
|
||||||
print("Replay complete")
|
|
||||||
mcrfpy.delTimer("replay")
|
|
||||||
return
|
|
||||||
|
|
||||||
action = actions[action_index]
|
|
||||||
current_time = time.time() - start_time
|
|
||||||
|
|
||||||
# Wait until it's time for this action
|
|
||||||
if current_time >= action["time"]:
|
|
||||||
if action["type"] == "click":
|
|
||||||
automation.click(action["x"], action["y"])
|
|
||||||
elif action["type"] == "key":
|
|
||||||
automation.keyDown(action["key"])
|
|
||||||
automation.keyUp(action["key"])
|
|
||||||
|
|
||||||
action_index += 1
|
|
||||||
|
|
||||||
mcrfpy.setTimer("replay", replay_next, 10) # Check every 10ms
|
|
||||||
|
|
||||||
# Example usage - would be controlled by UI
|
|
||||||
recorder = ActionRecorder()
|
|
||||||
|
|
||||||
# To start recording:
|
|
||||||
# recorder.start_recording()
|
|
||||||
|
|
||||||
# To stop and save:
|
|
||||||
# recorder.stop_recording()
|
|
||||||
|
|
||||||
# To replay:
|
|
||||||
# recorder.replay_actions()
|
|
||||||
|
|
||||||
print("Action recorder ready - call recorder.start_recording() to begin")
|
|
Before Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 39 KiB |
|
@ -0,0 +1,36 @@
|
||||||
|
@echo off
|
||||||
|
REM Windows build script for McRogueFace
|
||||||
|
REM Run this over SSH without Visual Studio GUI
|
||||||
|
|
||||||
|
echo Building McRogueFace for Windows...
|
||||||
|
|
||||||
|
REM Clean previous build
|
||||||
|
if exist build_win rmdir /s /q build_win
|
||||||
|
mkdir build_win
|
||||||
|
cd build_win
|
||||||
|
|
||||||
|
REM Generate Visual Studio project files with CMake
|
||||||
|
REM Use -G to specify generator, -A for architecture
|
||||||
|
REM Visual Studio 2022 = "Visual Studio 17 2022"
|
||||||
|
REM Visual Studio 2019 = "Visual Studio 16 2019"
|
||||||
|
cmake -G "Visual Studio 17 2022" -A x64 ..
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo CMake configuration failed!
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Build using MSBuild (comes with Visual Studio)
|
||||||
|
REM You can also use cmake --build . --config Release
|
||||||
|
msbuild McRogueFace.sln /p:Configuration=Release /p:Platform=x64 /m
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo Build failed!
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo Build completed successfully!
|
||||||
|
echo Executable location: build_win\Release\mcrogueface.exe
|
||||||
|
|
||||||
|
REM Alternative: Using cmake to build (works with any generator)
|
||||||
|
REM cmake --build . --config Release --parallel
|
||||||
|
|
||||||
|
cd ..
|
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 31 KiB |
33
clean.sh
|
@ -1,33 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# Clean script for McRogueFace - removes build artifacts
|
|
||||||
|
|
||||||
# Colors for output
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
echo -e "${YELLOW}Cleaning McRogueFace build artifacts...${NC}"
|
|
||||||
|
|
||||||
# Remove build directory
|
|
||||||
if [ -d "build" ]; then
|
|
||||||
echo "Removing build directory..."
|
|
||||||
rm -rf build
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Remove CMake artifacts from project root
|
|
||||||
echo "Removing CMake artifacts from project root..."
|
|
||||||
rm -f CMakeCache.txt
|
|
||||||
rm -f cmake_install.cmake
|
|
||||||
rm -f Makefile
|
|
||||||
rm -rf CMakeFiles
|
|
||||||
|
|
||||||
# Remove compiled executable from project root
|
|
||||||
rm -f mcrogueface
|
|
||||||
|
|
||||||
# Remove any test artifacts
|
|
||||||
rm -f test_script.py
|
|
||||||
rm -rf test_venv
|
|
||||||
rm -f python3 # symlink
|
|
||||||
|
|
||||||
echo -e "${GREEN}Clean complete!${NC}"
|
|
|
@ -1,80 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""Compare the original and improved HTML documentation."""
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
def compare_docs():
|
|
||||||
"""Show key differences between the two HTML versions."""
|
|
||||||
|
|
||||||
print("HTML Documentation Improvements")
|
|
||||||
print("=" * 50)
|
|
||||||
|
|
||||||
# Read both files
|
|
||||||
original = Path("docs/api_reference.html")
|
|
||||||
improved = Path("docs/api_reference_improved.html")
|
|
||||||
|
|
||||||
if not original.exists() or not improved.exists():
|
|
||||||
print("Error: Documentation files not found")
|
|
||||||
return
|
|
||||||
|
|
||||||
with open(original, 'r') as f:
|
|
||||||
orig_content = f.read()
|
|
||||||
|
|
||||||
with open(improved, 'r') as f:
|
|
||||||
imp_content = f.read()
|
|
||||||
|
|
||||||
print("\n📊 File Size Comparison:")
|
|
||||||
print(f" Original: {len(orig_content):,} bytes")
|
|
||||||
print(f" Improved: {len(imp_content):,} bytes")
|
|
||||||
|
|
||||||
print("\n✅ Key Improvements:")
|
|
||||||
|
|
||||||
# Check newline handling
|
|
||||||
if '\\n' in orig_content and '\\n' not in imp_content:
|
|
||||||
print(" • Fixed literal \\n in documentation text")
|
|
||||||
|
|
||||||
# Check table of contents
|
|
||||||
if '[Classes](#classes)' in orig_content and '<a href="#classes">Classes</a>' in imp_content:
|
|
||||||
print(" • Converted markdown links to proper HTML anchors")
|
|
||||||
|
|
||||||
# Check headings
|
|
||||||
if '<h4>Args:</h4>' not in imp_content and '<strong>Arguments:</strong>' in imp_content:
|
|
||||||
print(" • Fixed Args/Attributes formatting (no longer H4 headings)")
|
|
||||||
|
|
||||||
# Check method descriptions
|
|
||||||
orig_count = orig_content.count('`Get bounding box')
|
|
||||||
imp_count = imp_content.count('get_bounds(...)')
|
|
||||||
if orig_count > imp_count:
|
|
||||||
print(f" • Reduced duplicate method descriptions ({orig_count} → {imp_count})")
|
|
||||||
|
|
||||||
# Check Entity inheritance
|
|
||||||
if 'Entity.*Inherits from: Drawable' not in imp_content:
|
|
||||||
print(" • Fixed Entity class (no longer shows incorrect inheritance)")
|
|
||||||
|
|
||||||
# Check styling
|
|
||||||
if '.container {' in imp_content and '.container {' not in orig_content:
|
|
||||||
print(" • Enhanced visual styling with better typography and layout")
|
|
||||||
|
|
||||||
# Check class documentation
|
|
||||||
if '<h4>Arguments:</h4>' in imp_content:
|
|
||||||
print(" • Added detailed constructor arguments for all classes")
|
|
||||||
|
|
||||||
# Check automation
|
|
||||||
if 'automation.click</code></h4>' in imp_content:
|
|
||||||
print(" • Improved automation module documentation formatting")
|
|
||||||
|
|
||||||
print("\n📋 Documentation Coverage:")
|
|
||||||
print(f" • Classes: {imp_content.count('class-section')} documented")
|
|
||||||
print(f" • Functions: {imp_content.count('function-section')} documented")
|
|
||||||
method_count = imp_content.count('<h5><code class="method">')
|
|
||||||
print(f" • Methods: {method_count} documented")
|
|
||||||
|
|
||||||
print("\n✨ Visual Enhancements:")
|
|
||||||
print(" • Professional color scheme with syntax highlighting")
|
|
||||||
print(" • Responsive layout with max-width container")
|
|
||||||
print(" • Clear visual hierarchy with styled headings")
|
|
||||||
print(" • Improved code block formatting")
|
|
||||||
print(" • Better spacing and typography")
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
compare_docs()
|
|
Before Width: | Height: | Size: 35 KiB |
|
@ -1,852 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html><head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>McRogueFace API Reference</title>
|
|
||||||
<style>
|
|
||||||
|
|
||||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
||||||
line-height: 1.6; color: #333; max-width: 900px; margin: 0 auto; padding: 20px; }
|
|
||||||
h1, h2, h3, h4, h5 { color: #2c3e50; margin-top: 24px; }
|
|
||||||
h1 { border-bottom: 2px solid #3498db; padding-bottom: 10px; }
|
|
||||||
h2 { border-bottom: 1px solid #ecf0f1; padding-bottom: 8px; }
|
|
||||||
code { background: #f4f4f4; padding: 2px 4px; border-radius: 3px; font-size: 90%; }
|
|
||||||
pre { background: #f4f4f4; padding: 12px; border-radius: 5px; overflow-x: auto; }
|
|
||||||
pre code { background: none; padding: 0; }
|
|
||||||
blockquote { border-left: 4px solid #3498db; margin: 0; padding-left: 16px; color: #7f8c8d; }
|
|
||||||
hr { border: none; border-top: 1px solid #ecf0f1; margin: 24px 0; }
|
|
||||||
a { color: #3498db; text-decoration: none; }
|
|
||||||
a:hover { text-decoration: underline; }
|
|
||||||
.property { color: #27ae60; }
|
|
||||||
.method { color: #2980b9; }
|
|
||||||
.class-name { color: #8e44ad; font-weight: bold; }
|
|
||||||
ul { padding-left: 24px; }
|
|
||||||
li { margin: 4px 0; }
|
|
||||||
|
|
||||||
</style>
|
|
||||||
</head><body>
|
|
||||||
<h1>McRogueFace API Reference</h1>
|
|
||||||
|
|
||||||
<em>Generated on 2025-07-08 10:11:22</em>
|
|
||||||
|
|
||||||
<h2>Overview</h2>
|
|
||||||
|
|
||||||
<p>McRogueFace Python API\n\nCore game engine interface for creating roguelike games with Python.\n\nThis module provides:\n- Scene management (createScene, setScene, currentScene)\n- UI components (Frame, Caption, Sprite, Grid)\n- Entity system for game objects\n- Audio playback (sound effects and music)\n- Timer system for scheduled events\n- Input handling\n- Performance metrics\n\nExample:\n import mcrfpy\n \n # Create a new scene\n mcrfpy.createScene('game')\n mcrfpy.setScene('game')\n \n # Add UI elements\n frame = mcrfpy.Frame(10, 10, 200, 100)\n caption = mcrfpy.Caption('Hello World', 50, 50)\n mcrfpy.sceneUI().extend([frame, caption])\n</p>
|
|
||||||
|
|
||||||
<h2>Table of Contents</h2>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>[Classes](#classes)</li>
|
|
||||||
<li>[Functions](#functions)</li>
|
|
||||||
<li>[Automation Module](#automation-module)</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h2>Classes</h2>
|
|
||||||
|
|
||||||
<h3>UI Components</h3>
|
|
||||||
|
|
||||||
<h3>class `Caption`</h3>
|
|
||||||
<em>Inherits from: Drawable</em>
|
|
||||||
|
|
||||||
<pre><code class="language-python">
|
|
||||||
Caption(text='', x=0, y=0, font=None, fill_color=None, outline_color=None, outline=0, click=None)
|
|
||||||
</code></pre>
|
|
||||||
|
|
||||||
<p>A text display UI element with customizable font and styling.</p>
|
|
||||||
|
|
||||||
<p>Args:</p>
|
|
||||||
<p>text (str): The text content to display. Default: ''</p>
|
|
||||||
<p>x (float): X position in pixels. Default: 0</p>
|
|
||||||
<p>y (float): Y position in pixels. Default: 0</p>
|
|
||||||
<p>font (Font): Font object for text rendering. Default: engine default font</p>
|
|
||||||
<p>fill_color (Color): Text fill color. Default: (255, 255, 255, 255)</p>
|
|
||||||
<p>outline_color (Color): Text outline color. Default: (0, 0, 0, 255)</p>
|
|
||||||
<p>outline (float): Text outline thickness. Default: 0</p>
|
|
||||||
<p>click (callable): Click event handler. Default: None</p>
|
|
||||||
|
|
||||||
<p>Attributes:</p>
|
|
||||||
<p>text (str): The displayed text content</p>
|
|
||||||
<p>x, y (float): Position in pixels</p>
|
|
||||||
<p>font (Font): Font used for rendering</p>
|
|
||||||
<p>fill_color, outline_color (Color): Text appearance</p>
|
|
||||||
<p>outline (float): Outline thickness</p>
|
|
||||||
<p>click (callable): Click event handler</p>
|
|
||||||
<p>visible (bool): Visibility state</p>
|
|
||||||
<p>z_index (int): Rendering order</p>
|
|
||||||
<p>w, h (float): Read-only computed size based on text and font</p>
|
|
||||||
|
|
||||||
<h4>Methods</h4>
|
|
||||||
|
|
||||||
<h5>`Get bounding box as (x, y, width, height)`</h5>
|
|
||||||
<p>Get bounding box as (x, y, width, height)</p>
|
|
||||||
|
|
||||||
<h5>`Move by relative offset (dx, dy)`</h5>
|
|
||||||
<p>Move by relative offset (dx, dy)</p>
|
|
||||||
|
|
||||||
<h5>`Resize to new dimensions (width, height)`</h5>
|
|
||||||
<p>Resize to new dimensions (width, height)</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>class `Entity`</h3>
|
|
||||||
<em>Inherits from: Drawable</em>
|
|
||||||
|
|
||||||
<p>UIEntity objects</p>
|
|
||||||
|
|
||||||
<h4>Methods</h4>
|
|
||||||
|
|
||||||
<h5>`at(...)`</h5>
|
|
||||||
|
|
||||||
<h5>`die(...)`</h5>
|
|
||||||
<p>Remove this entity from its grid</p>
|
|
||||||
|
|
||||||
<h5>`Get bounding box as (x, y, width, height)`</h5>
|
|
||||||
<p>Get bounding box as (x, y, width, height)</p>
|
|
||||||
|
|
||||||
<h5>`index(...)`</h5>
|
|
||||||
|
|
||||||
<h5>`Move by relative offset (dx, dy)`</h5>
|
|
||||||
<p>Move by relative offset (dx, dy)</p>
|
|
||||||
|
|
||||||
<h5>`Resize to new dimensions (width, height)`</h5>
|
|
||||||
<p>Resize to new dimensions (width, height)</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>class `Frame`</h3>
|
|
||||||
<em>Inherits from: Drawable</em>
|
|
||||||
|
|
||||||
<pre><code class="language-python">
|
|
||||||
Frame(x=0, y=0, w=0, h=0, fill_color=None, outline_color=None, outline=0, click=None, children=None)
|
|
||||||
</code></pre>
|
|
||||||
|
|
||||||
<p>A rectangular frame UI element that can contain other drawable elements.</p>
|
|
||||||
|
|
||||||
<p>Args:</p>
|
|
||||||
<p>x (float): X position in pixels. Default: 0</p>
|
|
||||||
<p>y (float): Y position in pixels. Default: 0</p>
|
|
||||||
<p>w (float): Width in pixels. Default: 0</p>
|
|
||||||
<p>h (float): Height in pixels. Default: 0</p>
|
|
||||||
<p>fill_color (Color): Background fill color. Default: (0, 0, 0, 128)</p>
|
|
||||||
<p>outline_color (Color): Border outline color. Default: (255, 255, 255, 255)</p>
|
|
||||||
<p>outline (float): Border outline thickness. Default: 0</p>
|
|
||||||
<p>click (callable): Click event handler. Default: None</p>
|
|
||||||
<p>children (list): Initial list of child drawable elements. Default: None</p>
|
|
||||||
|
|
||||||
<p>Attributes:</p>
|
|
||||||
<p>x, y (float): Position in pixels</p>
|
|
||||||
<p>w, h (float): Size in pixels</p>
|
|
||||||
<p>fill_color, outline_color (Color): Visual appearance</p>
|
|
||||||
<p>outline (float): Border thickness</p>
|
|
||||||
<p>click (callable): Click event handler</p>
|
|
||||||
<p>children (list): Collection of child drawable elements</p>
|
|
||||||
<p>visible (bool): Visibility state</p>
|
|
||||||
<p>z_index (int): Rendering order</p>
|
|
||||||
<p>clip_children (bool): Whether to clip children to frame bounds</p>
|
|
||||||
|
|
||||||
<h4>Methods</h4>
|
|
||||||
|
|
||||||
<h5>`Get bounding box as (x, y, width, height)`</h5>
|
|
||||||
<p>Get bounding box as (x, y, width, height)</p>
|
|
||||||
|
|
||||||
<h5>`Move by relative offset (dx, dy)`</h5>
|
|
||||||
<p>Move by relative offset (dx, dy)</p>
|
|
||||||
|
|
||||||
<h5>`Resize to new dimensions (width, height)`</h5>
|
|
||||||
<p>Resize to new dimensions (width, height)</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>class `Grid`</h3>
|
|
||||||
<em>Inherits from: Drawable</em>
|
|
||||||
|
|
||||||
<pre><code class="language-python">
|
|
||||||
Grid(x=0, y=0, grid_size=(20, 20), texture=None, tile_width=16, tile_height=16, scale=1.0, click=None)
|
|
||||||
</code></pre>
|
|
||||||
|
|
||||||
<p>A grid-based tilemap UI element for rendering tile-based levels and game worlds.</p>
|
|
||||||
|
|
||||||
<p>Args:</p>
|
|
||||||
<p>x (float): X position in pixels. Default: 0</p>
|
|
||||||
<p>y (float): Y position in pixels. Default: 0</p>
|
|
||||||
<p>grid_size (tuple): Grid dimensions as (width, height) in tiles. Default: (20, 20)</p>
|
|
||||||
<p>texture (Texture): Texture atlas containing tile sprites. Default: None</p>
|
|
||||||
<p>tile_width (int): Width of each tile in pixels. Default: 16</p>
|
|
||||||
<p>tile_height (int): Height of each tile in pixels. Default: 16</p>
|
|
||||||
<p>scale (float): Grid scaling factor. Default: 1.0</p>
|
|
||||||
<p>click (callable): Click event handler. Default: None</p>
|
|
||||||
|
|
||||||
<p>Attributes:</p>
|
|
||||||
<p>x, y (float): Position in pixels</p>
|
|
||||||
<p>grid_size (tuple): Grid dimensions (width, height) in tiles</p>
|
|
||||||
<p>tile_width, tile_height (int): Tile dimensions in pixels</p>
|
|
||||||
<p>texture (Texture): Tile texture atlas</p>
|
|
||||||
<p>scale (float): Scale multiplier</p>
|
|
||||||
<p>points (list): 2D array of GridPoint objects for tile data</p>
|
|
||||||
<p>entities (list): Collection of Entity objects in the grid</p>
|
|
||||||
<p>background_color (Color): Grid background color</p>
|
|
||||||
<p>click (callable): Click event handler</p>
|
|
||||||
<p>visible (bool): Visibility state</p>
|
|
||||||
<p>z_index (int): Rendering order</p>
|
|
||||||
|
|
||||||
<h4>Methods</h4>
|
|
||||||
|
|
||||||
<h5>`at(...)`</h5>
|
|
||||||
|
|
||||||
<h5>`Get bounding box as (x, y, width, height)`</h5>
|
|
||||||
<p>Get bounding box as (x, y, width, height)</p>
|
|
||||||
|
|
||||||
<h5>`Move by relative offset (dx, dy)`</h5>
|
|
||||||
<p>Move by relative offset (dx, dy)</p>
|
|
||||||
|
|
||||||
<h5>`Resize to new dimensions (width, height)`</h5>
|
|
||||||
<p>Resize to new dimensions (width, height)</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>class `Sprite`</h3>
|
|
||||||
<em>Inherits from: Drawable</em>
|
|
||||||
|
|
||||||
<pre><code class="language-python">
|
|
||||||
Sprite(x=0, y=0, texture=None, sprite_index=0, scale=1.0, click=None)
|
|
||||||
</code></pre>
|
|
||||||
|
|
||||||
<p>A sprite UI element that displays a texture or portion of a texture atlas.</p>
|
|
||||||
|
|
||||||
<p>Args:</p>
|
|
||||||
<p>x (float): X position in pixels. Default: 0</p>
|
|
||||||
<p>y (float): Y position in pixels. Default: 0</p>
|
|
||||||
<p>texture (Texture): Texture object to display. Default: None</p>
|
|
||||||
<p>sprite_index (int): Index into texture atlas (if applicable). Default: 0</p>
|
|
||||||
<p>scale (float): Sprite scaling factor. Default: 1.0</p>
|
|
||||||
<p>click (callable): Click event handler. Default: None</p>
|
|
||||||
|
|
||||||
<p>Attributes:</p>
|
|
||||||
<p>x, y (float): Position in pixels</p>
|
|
||||||
<p>texture (Texture): The texture being displayed</p>
|
|
||||||
<p>sprite_index (int): Current sprite index in texture atlas</p>
|
|
||||||
<p>scale (float): Scale multiplier</p>
|
|
||||||
<p>click (callable): Click event handler</p>
|
|
||||||
<p>visible (bool): Visibility state</p>
|
|
||||||
<p>z_index (int): Rendering order</p>
|
|
||||||
<p>w, h (float): Read-only computed size based on texture and scale</p>
|
|
||||||
|
|
||||||
<h4>Methods</h4>
|
|
||||||
|
|
||||||
<h5>`Get bounding box as (x, y, width, height)`</h5>
|
|
||||||
<p>Get bounding box as (x, y, width, height)</p>
|
|
||||||
|
|
||||||
<h5>`Move by relative offset (dx, dy)`</h5>
|
|
||||||
<p>Move by relative offset (dx, dy)</p>
|
|
||||||
|
|
||||||
<h5>`Resize to new dimensions (width, height)`</h5>
|
|
||||||
<p>Resize to new dimensions (width, height)</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>Collections</h3>
|
|
||||||
|
|
||||||
<h3>class `EntityCollection`</h3>
|
|
||||||
|
|
||||||
<p>Iterable, indexable collection of Entities</p>
|
|
||||||
|
|
||||||
<h4>Methods</h4>
|
|
||||||
|
|
||||||
<h5>`append(...)`</h5>
|
|
||||||
|
|
||||||
<h5>`count(...)`</h5>
|
|
||||||
|
|
||||||
<h5>`extend(...)`</h5>
|
|
||||||
|
|
||||||
<h5>`index(...)`</h5>
|
|
||||||
|
|
||||||
<h5>`remove(...)`</h5>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>class `UICollection`</h3>
|
|
||||||
|
|
||||||
<p>Iterable, indexable collection of UI objects</p>
|
|
||||||
|
|
||||||
<h4>Methods</h4>
|
|
||||||
|
|
||||||
<h5>`append(...)`</h5>
|
|
||||||
|
|
||||||
<h5>`count(...)`</h5>
|
|
||||||
|
|
||||||
<h5>`extend(...)`</h5>
|
|
||||||
|
|
||||||
<h5>`index(...)`</h5>
|
|
||||||
|
|
||||||
<h5>`remove(...)`</h5>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>class `UICollectionIter`</h3>
|
|
||||||
|
|
||||||
<p>Iterator for a collection of UI objects</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>class `UIEntityCollectionIter`</h3>
|
|
||||||
|
|
||||||
<p>Iterator for a collection of UI objects</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>System Types</h3>
|
|
||||||
|
|
||||||
<h3>class `Color`</h3>
|
|
||||||
|
|
||||||
<p>SFML Color Object</p>
|
|
||||||
|
|
||||||
<h4>Methods</h4>
|
|
||||||
|
|
||||||
<h5>`Create Color from hex string (e.g., '#FF0000' or 'FF0000')`</h5>
|
|
||||||
<p>Create Color from hex string (e.g., '#FF0000' or 'FF0000')</p>
|
|
||||||
|
|
||||||
<h5>`lerp(...)`</h5>
|
|
||||||
<p>Linearly interpolate between this color and another</p>
|
|
||||||
|
|
||||||
<h5>`to_hex(...)`</h5>
|
|
||||||
<p>Convert Color to hex string</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>class `Font`</h3>
|
|
||||||
|
|
||||||
<p>SFML Font Object</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>class `Texture`</h3>
|
|
||||||
|
|
||||||
<p>SFML Texture Object</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>class `Vector`</h3>
|
|
||||||
|
|
||||||
<p>SFML Vector Object</p>
|
|
||||||
|
|
||||||
<h4>Methods</h4>
|
|
||||||
|
|
||||||
<h5>`angle(...)`</h5>
|
|
||||||
|
|
||||||
<h5>`copy(...)`</h5>
|
|
||||||
|
|
||||||
<h5>`distance_to(...)`</h5>
|
|
||||||
<p>Return the distance to another vector</p>
|
|
||||||
|
|
||||||
<h5>`dot(...)`</h5>
|
|
||||||
|
|
||||||
<h5>`magnitude(...)`</h5>
|
|
||||||
<p>Return the length of the vector</p>
|
|
||||||
|
|
||||||
<h5>`magnitude_squared(...)`</h5>
|
|
||||||
<p>Return the squared length of the vector</p>
|
|
||||||
|
|
||||||
<h5>`normalize(...)`</h5>
|
|
||||||
<p>Return a unit vector in the same direction</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>Other Classes</h3>
|
|
||||||
|
|
||||||
<h3>class `Animation`</h3>
|
|
||||||
|
|
||||||
<p>Animation object for animating UI properties</p>
|
|
||||||
|
|
||||||
<h4>Methods</h4>
|
|
||||||
|
|
||||||
<h5>`get_current_value(...)`</h5>
|
|
||||||
<p>Get the current interpolated value</p>
|
|
||||||
|
|
||||||
<h5>`start(...)`</h5>
|
|
||||||
<p>Start the animation on a target UIDrawable</p>
|
|
||||||
|
|
||||||
<h5>`Update the animation by deltaTime (returns True if still running)`</h5>
|
|
||||||
<p>Update the animation by deltaTime (returns True if still running)</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>class `Drawable`</h3>
|
|
||||||
|
|
||||||
<p>Base class for all drawable UI elements</p>
|
|
||||||
|
|
||||||
<h4>Methods</h4>
|
|
||||||
|
|
||||||
<h5>`Get bounding box as (x, y, width, height)`</h5>
|
|
||||||
<p>Get bounding box as (x, y, width, height)</p>
|
|
||||||
|
|
||||||
<h5>`Move by relative offset (dx, dy)`</h5>
|
|
||||||
<p>Move by relative offset (dx, dy)</p>
|
|
||||||
|
|
||||||
<h5>`Resize to new dimensions (width, height)`</h5>
|
|
||||||
<p>Resize to new dimensions (width, height)</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>class `GridPoint`</h3>
|
|
||||||
|
|
||||||
<p>UIGridPoint object</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>class `GridPointState`</h3>
|
|
||||||
|
|
||||||
<p>UIGridPointState object</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>class `Scene`</h3>
|
|
||||||
|
|
||||||
<p>Base class for object-oriented scenes</p>
|
|
||||||
|
|
||||||
<h4>Methods</h4>
|
|
||||||
|
|
||||||
<h5>`activate(...)`</h5>
|
|
||||||
<p>Make this the active scene</p>
|
|
||||||
|
|
||||||
<h5>`get_ui(...)`</h5>
|
|
||||||
<p>Get the UI element collection for this scene</p>
|
|
||||||
|
|
||||||
<h5>`Register a keyboard handler function (alternative to overriding on_keypress)`</h5>
|
|
||||||
<p>Register a keyboard handler function (alternative to overriding on_keypress)</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>class `Timer`</h3>
|
|
||||||
|
|
||||||
<p>Timer object for scheduled callbacks</p>
|
|
||||||
|
|
||||||
<h4>Methods</h4>
|
|
||||||
|
|
||||||
<h5>`cancel(...)`</h5>
|
|
||||||
<p>Cancel the timer and remove it from the system</p>
|
|
||||||
|
|
||||||
<h5>`pause(...)`</h5>
|
|
||||||
<p>Pause the timer</p>
|
|
||||||
|
|
||||||
<h5>`restart(...)`</h5>
|
|
||||||
<p>Restart the timer from the current time</p>
|
|
||||||
|
|
||||||
<h5>`resume(...)`</h5>
|
|
||||||
<p>Resume a paused timer</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>class `Window`</h3>
|
|
||||||
|
|
||||||
<p>Window singleton for accessing and modifying the game window properties</p>
|
|
||||||
|
|
||||||
<h4>Methods</h4>
|
|
||||||
|
|
||||||
<h5>`center(...)`</h5>
|
|
||||||
<p>Center the window on the screen</p>
|
|
||||||
|
|
||||||
<h5>`get(...)`</h5>
|
|
||||||
<p>Get the Window singleton instance</p>
|
|
||||||
|
|
||||||
<h5>`screenshot(...)`</h5>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h2>Functions</h2>
|
|
||||||
|
|
||||||
<h3>Scene Management</h3>
|
|
||||||
|
|
||||||
<h3>`createScene(name: str)`</h3>
|
|
||||||
|
|
||||||
|
|
||||||
<p>Create a new empty scene.</p>
|
|
||||||
|
|
||||||
<em>*Args:*</em>
|
|
||||||
<p>name: Unique name for the new scene</p>
|
|
||||||
|
|
||||||
<em>*Raises:*</em>
|
|
||||||
<p>ValueError: If a scene with this name already exists</p>
|
|
||||||
|
|
||||||
<em>*Note:*</em>
|
|
||||||
<p>The scene is created but not made active. Use setScene() to switch to it.</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>`currentScene()`</h3>
|
|
||||||
|
|
||||||
|
|
||||||
<p>Get the name of the currently active scene.</p>
|
|
||||||
|
|
||||||
<em>*Returns:*</em>
|
|
||||||
<p>str: Name of the current scene</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>`keypressScene(handler: callable)`</h3>
|
|
||||||
|
|
||||||
|
|
||||||
<p>Set the keyboard event handler for the current scene.</p>
|
|
||||||
|
|
||||||
<em>*Args:*</em>
|
|
||||||
<p>handler: Callable that receives (key_name: str, is_pressed: bool)</p>
|
|
||||||
|
|
||||||
<em>*Example:*</em>
|
|
||||||
<p>def on_key(key, pressed):</p>
|
|
||||||
<p>if key == 'A' and pressed:</p>
|
|
||||||
<p>print('A key pressed')</p>
|
|
||||||
<p>mcrfpy.keypressScene(on_key)</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>`sceneUI(scene: str = None)`</h3>
|
|
||||||
|
|
||||||
|
|
||||||
<p>Get all UI elements for a scene.</p>
|
|
||||||
|
|
||||||
<em>*Args:*</em>
|
|
||||||
<p>scene: Scene name. If None, uses current scene</p>
|
|
||||||
|
|
||||||
<em>*Returns:*</em>
|
|
||||||
<p>list: All UI elements (Frame, Caption, Sprite, Grid) in the scene</p>
|
|
||||||
|
|
||||||
<em>*Raises:*</em>
|
|
||||||
<p>KeyError: If the specified scene doesn't exist</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>`setScene(scene: str, transition: str = None, duration: float = 0.0)`</h3>
|
|
||||||
|
|
||||||
|
|
||||||
<p>Switch to a different scene with optional transition effect.</p>
|
|
||||||
|
|
||||||
<em>*Args:*</em>
|
|
||||||
<p>scene: Name of the scene to switch to</p>
|
|
||||||
<p>transition: Transition type ('fade', 'slide_left', 'slide_right', 'slide_up', 'slide_down')</p>
|
|
||||||
<p>duration: Transition duration in seconds (default: 0.0 for instant)</p>
|
|
||||||
|
|
||||||
<em>*Raises:*</em>
|
|
||||||
<p>KeyError: If the scene doesn't exist</p>
|
|
||||||
<p>ValueError: If the transition type is invalid</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>Audio</h3>
|
|
||||||
|
|
||||||
<h3>`createSoundBuffer(filename: str)`</h3>
|
|
||||||
|
|
||||||
|
|
||||||
<p>Load a sound effect from a file and return its buffer ID.</p>
|
|
||||||
|
|
||||||
<em>*Args:*</em>
|
|
||||||
<p>filename: Path to the sound file (WAV, OGG, FLAC)</p>
|
|
||||||
|
|
||||||
<em>*Returns:*</em>
|
|
||||||
<p>int: Buffer ID for use with playSound()</p>
|
|
||||||
|
|
||||||
<em>*Raises:*</em>
|
|
||||||
<p>RuntimeError: If the file cannot be loaded</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>`getMusicVolume()`</h3>
|
|
||||||
|
|
||||||
|
|
||||||
<p>Get the current music volume level.</p>
|
|
||||||
|
|
||||||
<em>*Returns:*</em>
|
|
||||||
<p>int: Current volume (0-100)</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>`getSoundVolume()`</h3>
|
|
||||||
|
|
||||||
|
|
||||||
<p>Get the current sound effects volume level.</p>
|
|
||||||
|
|
||||||
<em>*Returns:*</em>
|
|
||||||
<p>int: Current volume (0-100)</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>`loadMusic(filename: str)`</h3>
|
|
||||||
|
|
||||||
|
|
||||||
<p>Load and immediately play background music from a file.</p>
|
|
||||||
|
|
||||||
<em>*Args:*</em>
|
|
||||||
<p>filename: Path to the music file (WAV, OGG, FLAC)</p>
|
|
||||||
|
|
||||||
<em>*Note:*</em>
|
|
||||||
<p>Only one music track can play at a time. Loading new music stops the current track.</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>`playSound(buffer_id: int)`</h3>
|
|
||||||
|
|
||||||
|
|
||||||
<p>Play a sound effect using a previously loaded buffer.</p>
|
|
||||||
|
|
||||||
<em>*Args:*</em>
|
|
||||||
<p>buffer_id: Sound buffer ID returned by createSoundBuffer()</p>
|
|
||||||
|
|
||||||
<em>*Raises:*</em>
|
|
||||||
<p>RuntimeError: If the buffer ID is invalid</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>`setMusicVolume(volume: int)`</h3>
|
|
||||||
|
|
||||||
|
|
||||||
<p>Set the global music volume.</p>
|
|
||||||
|
|
||||||
<em>*Args:*</em>
|
|
||||||
<p>volume: Volume level from 0 (silent) to 100 (full volume)</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>`setSoundVolume(volume: int)`</h3>
|
|
||||||
|
|
||||||
|
|
||||||
<p>Set the global sound effects volume.</p>
|
|
||||||
|
|
||||||
<em>*Args:*</em>
|
|
||||||
<p>volume: Volume level from 0 (silent) to 100 (full volume)</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>UI Utilities</h3>
|
|
||||||
|
|
||||||
<h3>`find(name: str, scene: str = None)`</h3>
|
|
||||||
|
|
||||||
|
|
||||||
<p>Find the first UI element with the specified name.</p>
|
|
||||||
|
|
||||||
<em>*Args:*</em>
|
|
||||||
<p>name: Exact name to search for</p>
|
|
||||||
<p>scene: Scene to search in (default: current scene)</p>
|
|
||||||
|
|
||||||
<em>*Returns:*</em>
|
|
||||||
<p>Frame, Caption, Sprite, Grid, or Entity if found; None otherwise</p>
|
|
||||||
|
|
||||||
<em>*Note:*</em>
|
|
||||||
<p>Searches scene UI elements and entities within grids.</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>`findAll(pattern: str, scene: str = None)`</h3>
|
|
||||||
|
|
||||||
|
|
||||||
<p>Find all UI elements matching a name pattern.</p>
|
|
||||||
|
|
||||||
<em>*Args:*</em>
|
|
||||||
<p>pattern: Name pattern with optional wildcards (* matches any characters)</p>
|
|
||||||
<p>scene: Scene to search in (default: current scene)</p>
|
|
||||||
|
|
||||||
<em>*Returns:*</em>
|
|
||||||
<p>list: All matching UI elements and entities</p>
|
|
||||||
|
|
||||||
<em>*Example:*</em>
|
|
||||||
<p>findAll('enemy*') # Find all elements starting with 'enemy'</p>
|
|
||||||
<p>findAll('*_button') # Find all elements ending with '_button'</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>System</h3>
|
|
||||||
|
|
||||||
<h3>`delTimer(name: str)`</h3>
|
|
||||||
|
|
||||||
|
|
||||||
<p>Stop and remove a timer.</p>
|
|
||||||
|
|
||||||
<em>*Args:*</em>
|
|
||||||
<p>name: Timer identifier to remove</p>
|
|
||||||
|
|
||||||
<em>*Note:*</em>
|
|
||||||
<p>No error is raised if the timer doesn't exist.</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>`exit()`</h3>
|
|
||||||
|
|
||||||
|
|
||||||
<p>Cleanly shut down the game engine and exit the application.</p>
|
|
||||||
|
|
||||||
<em>*Note:*</em>
|
|
||||||
<p>This immediately closes the window and terminates the program.</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>`getMetrics()`</h3>
|
|
||||||
|
|
||||||
|
|
||||||
<p>Get current performance metrics.</p>
|
|
||||||
|
|
||||||
<em>*Returns:*</em>
|
|
||||||
<p>dict: Performance data with keys:</p>
|
|
||||||
<ul>
|
|
||||||
<li>frame_time: Last frame duration in seconds</li>
|
|
||||||
<li>avg_frame_time: Average frame time</li>
|
|
||||||
<li>fps: Frames per second</li>
|
|
||||||
<li>draw_calls: Number of draw calls</li>
|
|
||||||
<li>ui_elements: Total UI element count</li>
|
|
||||||
<li>visible_elements: Visible element count</li>
|
|
||||||
<li>current_frame: Frame counter</li>
|
|
||||||
<li>runtime: Total runtime in seconds</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>`setScale(multiplier: float)`</h3>
|
|
||||||
|
|
||||||
|
|
||||||
<p>Scale the game window size.</p>
|
|
||||||
|
|
||||||
<em>*Args:*</em>
|
|
||||||
<p>multiplier: Scale factor (e.g., 2.0 for double size)</p>
|
|
||||||
|
|
||||||
<em>*Note:*</em>
|
|
||||||
<p>The internal resolution remains 1024x768, but the window is scaled.</p>
|
|
||||||
<p>This is deprecated - use Window.resolution instead.</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>`setTimer(name: str, handler: callable, interval: int)`</h3>
|
|
||||||
|
|
||||||
|
|
||||||
<p>Create or update a recurring timer.</p>
|
|
||||||
|
|
||||||
<em>*Args:*</em>
|
|
||||||
<p>name: Unique identifier for the timer</p>
|
|
||||||
<p>handler: Function called with (runtime: float) parameter</p>
|
|
||||||
<p>interval: Time between calls in milliseconds</p>
|
|
||||||
|
|
||||||
<em>*Note:*</em>
|
|
||||||
<p>If a timer with this name exists, it will be replaced.</p>
|
|
||||||
<p>The handler receives the total runtime in seconds as its argument.</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h2>Automation Module</h2>
|
|
||||||
|
|
||||||
<p>The <code>mcrfpy.automation</code> module provides testing and automation capabilities.</p>
|
|
||||||
|
|
||||||
<h3>`automation.click(x=None, y=None, clicks=1, interval=0.0, button='left') - Click at position`</h3>
|
|
||||||
|
|
||||||
<p>click(x=None, y=None, clicks=1, interval=0.0, button='left') - Click at position</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>`automation.doubleClick(x=None, y=None) - Double click at position`</h3>
|
|
||||||
|
|
||||||
<p>doubleClick(x=None, y=None) - Double click at position</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>`automation.dragRel(xOffset, yOffset, duration=0.0, button='left') - Drag mouse relative to current position`</h3>
|
|
||||||
|
|
||||||
<p>dragRel(xOffset, yOffset, duration=0.0, button='left') - Drag mouse relative to current position</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>`automation.dragTo(x, y, duration=0.0, button='left') - Drag mouse to position`</h3>
|
|
||||||
|
|
||||||
<p>dragTo(x, y, duration=0.0, button='left') - Drag mouse to position</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>`automation.hotkey(*keys) - Press a hotkey combination (e.g., hotkey('ctrl', 'c'))`</h3>
|
|
||||||
|
|
||||||
<p>hotkey(*keys) - Press a hotkey combination (e.g., hotkey('ctrl', 'c'))</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>`automation.keyDown(key) - Press and hold a key`</h3>
|
|
||||||
|
|
||||||
<p>keyDown(key) - Press and hold a key</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>`automation.keyUp(key) - Release a key`</h3>
|
|
||||||
|
|
||||||
<p>keyUp(key) - Release a key</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>`automation.middleClick(x=None, y=None) - Middle click at position`</h3>
|
|
||||||
|
|
||||||
<p>middleClick(x=None, y=None) - Middle click at position</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>`automation.mouseDown(x=None, y=None, button='left') - Press mouse button`</h3>
|
|
||||||
|
|
||||||
<p>mouseDown(x=None, y=None, button='left') - Press mouse button</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>`automation.mouseUp(x=None, y=None, button='left') - Release mouse button`</h3>
|
|
||||||
|
|
||||||
<p>mouseUp(x=None, y=None, button='left') - Release mouse button</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>`automation.moveRel(xOffset, yOffset, duration=0.0) - Move mouse relative to current position`</h3>
|
|
||||||
|
|
||||||
<p>moveRel(xOffset, yOffset, duration=0.0) - Move mouse relative to current position</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>`automation.moveTo(x, y, duration=0.0) - Move mouse to absolute position`</h3>
|
|
||||||
|
|
||||||
<p>moveTo(x, y, duration=0.0) - Move mouse to absolute position</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>`automation.onScreen(x, y) - Check if coordinates are within screen bounds`</h3>
|
|
||||||
|
|
||||||
<p>onScreen(x, y) - Check if coordinates are within screen bounds</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>`automation.position() - Get current mouse position as (x, y) tuple`</h3>
|
|
||||||
|
|
||||||
<p>position() - Get current mouse position as (x, y) tuple</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>`automation.rightClick(x=None, y=None) - Right click at position`</h3>
|
|
||||||
|
|
||||||
<p>rightClick(x=None, y=None) - Right click at position</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>`automation.screenshot(filename) - Save a screenshot to the specified file`</h3>
|
|
||||||
|
|
||||||
<p>screenshot(filename) - Save a screenshot to the specified file</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>`automation.scroll(clicks, x=None, y=None) - Scroll wheel at position`</h3>
|
|
||||||
|
|
||||||
<p>scroll(clicks, x=None, y=None) - Scroll wheel at position</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>`automation.size() - Get screen size as (width, height) tuple`</h3>
|
|
||||||
|
|
||||||
<p>size() - Get screen size as (width, height) tuple</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>`automation.tripleClick(x=None, y=None) - Triple click at position`</h3>
|
|
||||||
|
|
||||||
<p>tripleClick(x=None, y=None) - Triple click at position</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>`automation.typewrite(message, interval=0.0) - Type text with optional interval between keystrokes`</h3>
|
|
||||||
|
|
||||||
<p>typewrite(message, interval=0.0) - Type text with optional interval between keystrokes</p>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
</body></html>
|
|
|
@ -1,342 +0,0 @@
|
||||||
/**
|
|
||||||
* Example implementation demonstrating the proposed visibility tracking system
|
|
||||||
* This shows how UIGridPoint, UIGridPointState, and libtcod maps work together
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <memory>
|
|
||||||
#include <unordered_map>
|
|
||||||
#include <vector>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
// Forward declarations
|
|
||||||
class UIGrid;
|
|
||||||
class UIEntity;
|
|
||||||
class TCODMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UIGridPoint - The "ground truth" of a grid cell
|
|
||||||
* This represents the actual state of the world
|
|
||||||
*/
|
|
||||||
class UIGridPoint {
|
|
||||||
public:
|
|
||||||
// Core properties
|
|
||||||
bool walkable = true; // Can entities move through this cell?
|
|
||||||
bool transparent = true; // Does this cell block line of sight?
|
|
||||||
int tilesprite = 0; // What tile to render
|
|
||||||
|
|
||||||
// Visual properties
|
|
||||||
sf::Color color;
|
|
||||||
sf::Color color_overlay;
|
|
||||||
|
|
||||||
// Grid position
|
|
||||||
int grid_x, grid_y;
|
|
||||||
UIGrid* parent_grid;
|
|
||||||
|
|
||||||
// When these change, sync with TCOD map
|
|
||||||
void setWalkable(bool value) {
|
|
||||||
walkable = value;
|
|
||||||
if (parent_grid) syncTCODMapCell();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setTransparent(bool value) {
|
|
||||||
transparent = value;
|
|
||||||
if (parent_grid) syncTCODMapCell();
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
void syncTCODMapCell(); // Update TCOD map when properties change
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UIGridPointState - What an entity knows about a grid cell
|
|
||||||
* Each entity maintains one of these for each cell it has encountered
|
|
||||||
*/
|
|
||||||
class UIGridPointState {
|
|
||||||
public:
|
|
||||||
// Visibility state
|
|
||||||
bool visible = false; // Currently in entity's FOV?
|
|
||||||
bool discovered = false; // Has entity ever seen this cell?
|
|
||||||
|
|
||||||
// When the entity last saw this cell (for fog of war effects)
|
|
||||||
int last_seen_turn = -1;
|
|
||||||
|
|
||||||
// What the entity remembers about this cell
|
|
||||||
// (may be outdated if cell changed after entity saw it)
|
|
||||||
bool remembered_walkable = true;
|
|
||||||
bool remembered_transparent = true;
|
|
||||||
int remembered_tilesprite = 0;
|
|
||||||
|
|
||||||
// Update remembered state from actual grid point
|
|
||||||
void updateFromTruth(const UIGridPoint& truth, int current_turn) {
|
|
||||||
if (visible) {
|
|
||||||
discovered = true;
|
|
||||||
last_seen_turn = current_turn;
|
|
||||||
remembered_walkable = truth.walkable;
|
|
||||||
remembered_transparent = truth.transparent;
|
|
||||||
remembered_tilesprite = truth.tilesprite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* EntityGridKnowledge - Manages an entity's knowledge across multiple grids
|
|
||||||
* This allows entities to remember explored areas even when changing levels
|
|
||||||
*/
|
|
||||||
class EntityGridKnowledge {
|
|
||||||
private:
|
|
||||||
// Map from grid ID to the entity's knowledge of that grid
|
|
||||||
std::unordered_map<std::string, std::vector<UIGridPointState>> grid_knowledge;
|
|
||||||
|
|
||||||
public:
|
|
||||||
// Get or create knowledge vector for a specific grid
|
|
||||||
std::vector<UIGridPointState>& getGridKnowledge(const std::string& grid_id, int grid_size) {
|
|
||||||
auto& knowledge = grid_knowledge[grid_id];
|
|
||||||
if (knowledge.empty()) {
|
|
||||||
knowledge.resize(grid_size);
|
|
||||||
}
|
|
||||||
return knowledge;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if entity has visited this grid before
|
|
||||||
bool hasGridKnowledge(const std::string& grid_id) const {
|
|
||||||
return grid_knowledge.find(grid_id) != grid_knowledge.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear knowledge of a specific grid (e.g., for memory-wiping effects)
|
|
||||||
void forgetGrid(const std::string& grid_id) {
|
|
||||||
grid_knowledge.erase(grid_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get total number of grids this entity knows about
|
|
||||||
size_t getKnownGridCount() const {
|
|
||||||
return grid_knowledge.size();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enhanced UIEntity with visibility tracking
|
|
||||||
*/
|
|
||||||
class UIEntity {
|
|
||||||
private:
|
|
||||||
// Entity properties
|
|
||||||
float x, y; // Position
|
|
||||||
UIGrid* current_grid; // Current grid entity is on
|
|
||||||
EntityGridKnowledge knowledge; // Multi-grid knowledge storage
|
|
||||||
int sight_radius = 10; // How far entity can see
|
|
||||||
bool omniscient = false; // Does entity know everything?
|
|
||||||
|
|
||||||
public:
|
|
||||||
// Update entity's FOV and visibility knowledge
|
|
||||||
void updateFOV(int radius = -1) {
|
|
||||||
if (!current_grid) return;
|
|
||||||
if (radius < 0) radius = sight_radius;
|
|
||||||
|
|
||||||
// Get entity's knowledge of current grid
|
|
||||||
auto& grid_knowledge = knowledge.getGridKnowledge(
|
|
||||||
current_grid->getGridId(),
|
|
||||||
current_grid->getGridSize()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Reset visibility for all cells
|
|
||||||
for (auto& cell_knowledge : grid_knowledge) {
|
|
||||||
cell_knowledge.visible = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (omniscient) {
|
|
||||||
// Omniscient entities see everything
|
|
||||||
for (int i = 0; i < grid_knowledge.size(); i++) {
|
|
||||||
grid_knowledge[i].visible = true;
|
|
||||||
grid_knowledge[i].discovered = true;
|
|
||||||
grid_knowledge[i].updateFromTruth(
|
|
||||||
current_grid->getPointAt(i),
|
|
||||||
current_grid->getCurrentTurn()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Normal FOV calculation using TCOD
|
|
||||||
current_grid->computeFOVForEntity(this, (int)x, (int)y, radius);
|
|
||||||
|
|
||||||
// Update visibility states based on TCOD FOV results
|
|
||||||
for (int gy = 0; gy < current_grid->getHeight(); gy++) {
|
|
||||||
for (int gx = 0; gx < current_grid->getWidth(); gx++) {
|
|
||||||
int idx = gy * current_grid->getWidth() + gx;
|
|
||||||
|
|
||||||
if (current_grid->isCellInFOV(gx, gy)) {
|
|
||||||
grid_knowledge[idx].visible = true;
|
|
||||||
grid_knowledge[idx].updateFromTruth(
|
|
||||||
current_grid->getPointAt(idx),
|
|
||||||
current_grid->getCurrentTurn()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if entity can see a specific position
|
|
||||||
bool canSeePosition(int gx, int gy) const {
|
|
||||||
if (!current_grid) return false;
|
|
||||||
|
|
||||||
auto& grid_knowledge = const_cast<EntityGridKnowledge&>(knowledge).getGridKnowledge(
|
|
||||||
current_grid->getGridId(),
|
|
||||||
current_grid->getGridSize()
|
|
||||||
);
|
|
||||||
|
|
||||||
int idx = gy * current_grid->getWidth() + gx;
|
|
||||||
return idx >= 0 && idx < grid_knowledge.size() && grid_knowledge[idx].visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if entity has ever discovered a position
|
|
||||||
bool hasDiscoveredPosition(int gx, int gy) const {
|
|
||||||
if (!current_grid) return false;
|
|
||||||
|
|
||||||
auto& grid_knowledge = const_cast<EntityGridKnowledge&>(knowledge).getGridKnowledge(
|
|
||||||
current_grid->getGridId(),
|
|
||||||
current_grid->getGridSize()
|
|
||||||
);
|
|
||||||
|
|
||||||
int idx = gy * current_grid->getWidth() + gx;
|
|
||||||
return idx >= 0 && idx < grid_knowledge.size() && grid_knowledge[idx].discovered;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find path using only discovered/remembered terrain
|
|
||||||
std::vector<std::pair<int, int>> findKnownPath(int dest_x, int dest_y) {
|
|
||||||
if (!current_grid) return {};
|
|
||||||
|
|
||||||
// Create a TCOD map based on entity's knowledge
|
|
||||||
auto knowledge_map = current_grid->createKnowledgeMapForEntity(this);
|
|
||||||
|
|
||||||
// Use A* on the knowledge map
|
|
||||||
auto path = knowledge_map->computePath((int)x, (int)y, dest_x, dest_y);
|
|
||||||
|
|
||||||
delete knowledge_map;
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move to a new grid, preserving knowledge of the old one
|
|
||||||
void moveToGrid(UIGrid* new_grid) {
|
|
||||||
if (current_grid) {
|
|
||||||
// Knowledge is automatically preserved in the knowledge map
|
|
||||||
current_grid->removeEntity(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
current_grid = new_grid;
|
|
||||||
if (new_grid) {
|
|
||||||
new_grid->addEntity(this);
|
|
||||||
// If we've been here before, we still remember it
|
|
||||||
updateFOV();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Example use cases
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Use Case 1: Player exploring a dungeon
|
|
||||||
void playerExploration() {
|
|
||||||
auto player = std::make_shared<UIEntity>();
|
|
||||||
auto dungeon_level1 = std::make_shared<UIGrid>("dungeon_level_1", 50, 50);
|
|
||||||
|
|
||||||
// Player starts with no knowledge
|
|
||||||
player->moveToGrid(dungeon_level1.get());
|
|
||||||
player->updateFOV(10); // Can see 10 tiles in each direction
|
|
||||||
|
|
||||||
// Only render what player can see
|
|
||||||
dungeon_level1->renderWithEntityPerspective(player.get());
|
|
||||||
|
|
||||||
// Player tries to path to unexplored area
|
|
||||||
auto path = player->findKnownPath(45, 45);
|
|
||||||
if (path.empty()) {
|
|
||||||
// "You haven't explored that area yet!"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use Case 2: Entity with perfect knowledge
|
|
||||||
void omniscientEntity() {
|
|
||||||
auto guardian = std::make_shared<UIEntity>();
|
|
||||||
guardian->setOmniscient(true); // Knows everything about any grid it enters
|
|
||||||
|
|
||||||
auto temple = std::make_shared<UIGrid>("temple", 30, 30);
|
|
||||||
guardian->moveToGrid(temple.get());
|
|
||||||
|
|
||||||
// Guardian immediately knows entire layout
|
|
||||||
auto path = guardian->findKnownPath(29, 29); // Can path anywhere
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use Case 3: Entity returning to previously explored area
|
|
||||||
void returningToArea() {
|
|
||||||
auto scout = std::make_shared<UIEntity>();
|
|
||||||
auto forest = std::make_shared<UIGrid>("forest", 40, 40);
|
|
||||||
auto cave = std::make_shared<UIGrid>("cave", 20, 20);
|
|
||||||
|
|
||||||
// Scout explores forest
|
|
||||||
scout->moveToGrid(forest.get());
|
|
||||||
scout->updateFOV(15);
|
|
||||||
// ... scout moves around, discovering ~50% of forest ...
|
|
||||||
|
|
||||||
// Scout enters cave
|
|
||||||
scout->moveToGrid(cave.get());
|
|
||||||
scout->updateFOV(8); // Darker in cave, reduced vision
|
|
||||||
|
|
||||||
// Later, scout returns to forest
|
|
||||||
scout->moveToGrid(forest.get());
|
|
||||||
// Scout still remembers the areas previously explored!
|
|
||||||
// Can immediately path through known areas
|
|
||||||
auto path = scout->findKnownPath(10, 10); // Works if area was explored before
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use Case 4: Fog of war - remembered vs current state
|
|
||||||
void fogOfWar() {
|
|
||||||
auto player = std::make_shared<UIEntity>();
|
|
||||||
auto dungeon = std::make_shared<UIGrid>("dungeon", 50, 50);
|
|
||||||
|
|
||||||
player->moveToGrid(dungeon.get());
|
|
||||||
player->setPosition(25, 25);
|
|
||||||
player->updateFOV(10);
|
|
||||||
|
|
||||||
// Player sees a door at (30, 25) - it's open
|
|
||||||
auto& door_point = dungeon->at(30, 25);
|
|
||||||
door_point.walkable = true;
|
|
||||||
door_point.transparent = true;
|
|
||||||
|
|
||||||
// Player moves away
|
|
||||||
player->setPosition(10, 10);
|
|
||||||
player->updateFOV(10);
|
|
||||||
|
|
||||||
// While player is gone, door closes
|
|
||||||
door_point.walkable = false;
|
|
||||||
door_point.transparent = false;
|
|
||||||
|
|
||||||
// Player's memory still thinks door is open
|
|
||||||
auto& player_knowledge = player->getKnowledgeAt(30, 25);
|
|
||||||
// player_knowledge.remembered_walkable is still true!
|
|
||||||
|
|
||||||
// Player tries to path through the door based on memory
|
|
||||||
auto path = player->findKnownPath(35, 25);
|
|
||||||
// Path planning succeeds based on remembered state
|
|
||||||
|
|
||||||
// But when player gets close enough to see it again...
|
|
||||||
player->setPosition(25, 25);
|
|
||||||
player->updateFOV(10);
|
|
||||||
// Knowledge updates - door is actually closed!
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Proper use of each component:
|
|
||||||
*
|
|
||||||
* UIGridPoint:
|
|
||||||
* - Stores the actual, current state of the world
|
|
||||||
* - Used by the game logic to determine what really happens
|
|
||||||
* - Syncs with TCOD map for consistent pathfinding/FOV
|
|
||||||
*
|
|
||||||
* UIGridPointState:
|
|
||||||
* - Stores what an entity believes/remembers about a cell
|
|
||||||
* - May be outdated if world changed since last seen
|
|
||||||
* - Used for rendering fog of war and entity decision-making
|
|
||||||
*
|
|
||||||
* TCOD Map:
|
|
||||||
* - Provides efficient FOV and pathfinding algorithms
|
|
||||||
* - Can be created from either ground truth or entity knowledge
|
|
||||||
* - Multiple maps can exist (one for truth, one per entity for knowledge-based pathfinding)
|
|
||||||
*/
|
|
|
@ -1,63 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Example automation script using --exec flag
|
|
||||||
Usage: ./mcrogueface game.py --exec example_automation.py
|
|
||||||
"""
|
|
||||||
import mcrfpy
|
|
||||||
from mcrfpy import automation
|
|
||||||
|
|
||||||
class GameAutomation:
|
|
||||||
def __init__(self):
|
|
||||||
self.frame_count = 0
|
|
||||||
self.test_phase = 0
|
|
||||||
print("Automation: Initialized")
|
|
||||||
|
|
||||||
def periodic_test(self):
|
|
||||||
"""Called every second to perform automation tasks"""
|
|
||||||
self.frame_count = mcrfpy.getFrame()
|
|
||||||
|
|
||||||
print(f"Automation: Running test at frame {self.frame_count}")
|
|
||||||
|
|
||||||
# Take periodic screenshots
|
|
||||||
if self.test_phase % 5 == 0:
|
|
||||||
filename = f"automation_screenshot_{self.test_phase}.png"
|
|
||||||
automation.screenshot(filename)
|
|
||||||
print(f"Automation: Saved {filename}")
|
|
||||||
|
|
||||||
# Simulate user input based on current scene
|
|
||||||
scene = mcrfpy.currentScene()
|
|
||||||
print(f"Automation: Current scene is '{scene}'")
|
|
||||||
|
|
||||||
if scene == "main_menu" and self.test_phase < 5:
|
|
||||||
# Click start button
|
|
||||||
automation.click(512, 400)
|
|
||||||
print("Automation: Clicked start button")
|
|
||||||
elif scene == "game":
|
|
||||||
# Perform game actions
|
|
||||||
if self.test_phase % 3 == 0:
|
|
||||||
automation.hotkey("i") # Toggle inventory
|
|
||||||
print("Automation: Toggled inventory")
|
|
||||||
else:
|
|
||||||
# Random movement
|
|
||||||
import random
|
|
||||||
key = random.choice(["w", "a", "s", "d"])
|
|
||||||
automation.keyDown(key)
|
|
||||||
automation.keyUp(key)
|
|
||||||
print(f"Automation: Pressed '{key}' key")
|
|
||||||
|
|
||||||
self.test_phase += 1
|
|
||||||
|
|
||||||
# Stop after 20 tests
|
|
||||||
if self.test_phase >= 20:
|
|
||||||
print("Automation: Test suite complete")
|
|
||||||
mcrfpy.delTimer("automation_test")
|
|
||||||
# Could also call mcrfpy.quit() to exit the game
|
|
||||||
|
|
||||||
# Create automation instance
|
|
||||||
automation_instance = GameAutomation()
|
|
||||||
|
|
||||||
# Register periodic timer
|
|
||||||
mcrfpy.setTimer("automation_test", automation_instance.periodic_test, 1000)
|
|
||||||
|
|
||||||
print("Automation: Script loaded - tests will run every second")
|
|
||||||
print("Automation: The game and automation share the same Python environment")
|
|
|
@ -1,53 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Example configuration script that sets up shared state for other scripts
|
|
||||||
Usage: ./mcrogueface --exec example_config.py --exec example_automation.py game.py
|
|
||||||
"""
|
|
||||||
import mcrfpy
|
|
||||||
|
|
||||||
# Create a shared configuration namespace
|
|
||||||
class AutomationConfig:
|
|
||||||
# Test settings
|
|
||||||
test_enabled = True
|
|
||||||
screenshot_interval = 5 # Take screenshot every N tests
|
|
||||||
max_test_count = 50
|
|
||||||
test_delay_ms = 1000
|
|
||||||
|
|
||||||
# Monitoring settings
|
|
||||||
monitor_enabled = True
|
|
||||||
monitor_interval_ms = 500
|
|
||||||
report_delay_seconds = 30
|
|
||||||
|
|
||||||
# Game-specific settings
|
|
||||||
start_button_pos = (512, 400)
|
|
||||||
inventory_key = "i"
|
|
||||||
movement_keys = ["w", "a", "s", "d"]
|
|
||||||
|
|
||||||
# Shared state
|
|
||||||
test_results = []
|
|
||||||
performance_data = []
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def log_result(cls, test_name, success, details=""):
|
|
||||||
"""Log a test result"""
|
|
||||||
cls.test_results.append({
|
|
||||||
"test": test_name,
|
|
||||||
"success": success,
|
|
||||||
"details": details,
|
|
||||||
"frame": mcrfpy.getFrame()
|
|
||||||
})
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_summary(cls):
|
|
||||||
"""Get test summary"""
|
|
||||||
total = len(cls.test_results)
|
|
||||||
passed = sum(1 for r in cls.test_results if r["success"])
|
|
||||||
return f"Tests: {passed}/{total} passed"
|
|
||||||
|
|
||||||
# Attach config to mcrfpy module so other scripts can access it
|
|
||||||
mcrfpy.automation_config = AutomationConfig
|
|
||||||
|
|
||||||
print("Config: Automation configuration loaded")
|
|
||||||
print(f"Config: Test delay = {AutomationConfig.test_delay_ms}ms")
|
|
||||||
print(f"Config: Max tests = {AutomationConfig.max_test_count}")
|
|
||||||
print("Config: Other scripts can access config via mcrfpy.automation_config")
|
|
|
@ -1,69 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Example monitoring script that works alongside automation
|
|
||||||
Usage: ./mcrogueface game.py --exec example_automation.py --exec example_monitoring.py
|
|
||||||
"""
|
|
||||||
import mcrfpy
|
|
||||||
import time
|
|
||||||
|
|
||||||
class PerformanceMonitor:
|
|
||||||
def __init__(self):
|
|
||||||
self.start_time = time.time()
|
|
||||||
self.frame_samples = []
|
|
||||||
self.scene_changes = []
|
|
||||||
self.last_scene = None
|
|
||||||
print("Monitor: Performance monitoring initialized")
|
|
||||||
|
|
||||||
def collect_metrics(self):
|
|
||||||
"""Collect performance and state metrics"""
|
|
||||||
current_frame = mcrfpy.getFrame()
|
|
||||||
current_time = time.time() - self.start_time
|
|
||||||
current_scene = mcrfpy.currentScene()
|
|
||||||
|
|
||||||
# Track frame rate
|
|
||||||
if len(self.frame_samples) > 0:
|
|
||||||
last_frame, last_time = self.frame_samples[-1]
|
|
||||||
fps = (current_frame - last_frame) / (current_time - last_time)
|
|
||||||
print(f"Monitor: FPS = {fps:.1f}")
|
|
||||||
|
|
||||||
self.frame_samples.append((current_frame, current_time))
|
|
||||||
|
|
||||||
# Track scene changes
|
|
||||||
if current_scene != self.last_scene:
|
|
||||||
print(f"Monitor: Scene changed from '{self.last_scene}' to '{current_scene}'")
|
|
||||||
self.scene_changes.append((current_time, self.last_scene, current_scene))
|
|
||||||
self.last_scene = current_scene
|
|
||||||
|
|
||||||
# Keep only last 100 samples
|
|
||||||
if len(self.frame_samples) > 100:
|
|
||||||
self.frame_samples = self.frame_samples[-100:]
|
|
||||||
|
|
||||||
def generate_report(self):
|
|
||||||
"""Generate a summary report"""
|
|
||||||
if len(self.frame_samples) < 2:
|
|
||||||
return
|
|
||||||
|
|
||||||
total_frames = self.frame_samples[-1][0] - self.frame_samples[0][0]
|
|
||||||
total_time = self.frame_samples[-1][1] - self.frame_samples[0][1]
|
|
||||||
avg_fps = total_frames / total_time
|
|
||||||
|
|
||||||
print("\n=== Performance Report ===")
|
|
||||||
print(f"Monitor: Total time: {total_time:.1f} seconds")
|
|
||||||
print(f"Monitor: Total frames: {total_frames}")
|
|
||||||
print(f"Monitor: Average FPS: {avg_fps:.1f}")
|
|
||||||
print(f"Monitor: Scene changes: {len(self.scene_changes)}")
|
|
||||||
|
|
||||||
# Stop monitoring
|
|
||||||
mcrfpy.delTimer("performance_monitor")
|
|
||||||
|
|
||||||
# Create monitor instance
|
|
||||||
monitor = PerformanceMonitor()
|
|
||||||
|
|
||||||
# Register monitoring timer (runs every 500ms)
|
|
||||||
mcrfpy.setTimer("performance_monitor", monitor.collect_metrics, 500)
|
|
||||||
|
|
||||||
# Register report generation (runs after 30 seconds)
|
|
||||||
mcrfpy.setTimer("performance_report", monitor.generate_report, 30000)
|
|
||||||
|
|
||||||
print("Monitor: Script loaded - collecting metrics every 500ms")
|
|
||||||
print("Monitor: Will generate report after 30 seconds")
|
|
|
@ -1,189 +0,0 @@
|
||||||
// Example implementation of --exec flag for McRogueFace
|
|
||||||
// This shows the minimal changes needed to support multiple script execution
|
|
||||||
|
|
||||||
// === In McRogueFaceConfig.h ===
|
|
||||||
struct McRogueFaceConfig {
|
|
||||||
// ... existing fields ...
|
|
||||||
|
|
||||||
// Scripts to execute after main script (McRogueFace style)
|
|
||||||
std::vector<std::filesystem::path> exec_scripts;
|
|
||||||
};
|
|
||||||
|
|
||||||
// === In CommandLineParser.cpp ===
|
|
||||||
CommandLineParser::ParseResult CommandLineParser::parse(McRogueFaceConfig& config) {
|
|
||||||
// ... existing parsing code ...
|
|
||||||
|
|
||||||
for (int i = 1; i < argc; i++) {
|
|
||||||
std::string arg = argv[i];
|
|
||||||
|
|
||||||
// ... existing flag handling ...
|
|
||||||
|
|
||||||
else if (arg == "--exec") {
|
|
||||||
// Add script to exec list
|
|
||||||
if (i + 1 < argc) {
|
|
||||||
config.exec_scripts.push_back(argv[++i]);
|
|
||||||
} else {
|
|
||||||
std::cerr << "Error: --exec requires a script path\n";
|
|
||||||
return {true, 1};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// === In GameEngine.cpp ===
|
|
||||||
GameEngine::GameEngine(const McRogueFaceConfig& cfg) : config(cfg) {
|
|
||||||
// ... existing initialization ...
|
|
||||||
|
|
||||||
// Only load game.py if no custom script/command/module is specified
|
|
||||||
bool should_load_game = config.script_path.empty() &&
|
|
||||||
config.python_command.empty() &&
|
|
||||||
config.python_module.empty() &&
|
|
||||||
!config.interactive_mode &&
|
|
||||||
!config.python_mode &&
|
|
||||||
config.exec_scripts.empty(); // Add this check
|
|
||||||
|
|
||||||
if (should_load_game) {
|
|
||||||
if (!Py_IsInitialized()) {
|
|
||||||
McRFPy_API::api_init();
|
|
||||||
}
|
|
||||||
McRFPy_API::executePyString("import mcrfpy");
|
|
||||||
McRFPy_API::executeScript("scripts/game.py");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute any --exec scripts
|
|
||||||
for (const auto& exec_script : config.exec_scripts) {
|
|
||||||
std::cout << "Executing script: " << exec_script << std::endl;
|
|
||||||
McRFPy_API::executeScript(exec_script.string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Usage Examples ===
|
|
||||||
|
|
||||||
// Example 1: Run game with automation
|
|
||||||
// ./mcrogueface game.py --exec automation.py
|
|
||||||
|
|
||||||
// Example 2: Run game with multiple automation scripts
|
|
||||||
// ./mcrogueface game.py --exec test_suite.py --exec monitor.py --exec logger.py
|
|
||||||
|
|
||||||
// Example 3: Run only automation (no game)
|
|
||||||
// ./mcrogueface --exec standalone_test.py
|
|
||||||
|
|
||||||
// Example 4: Headless automation
|
|
||||||
// ./mcrogueface --headless game.py --exec automation.py
|
|
||||||
|
|
||||||
// === Python Script Example (automation.py) ===
|
|
||||||
/*
|
|
||||||
import mcrfpy
|
|
||||||
from mcrfpy import automation
|
|
||||||
|
|
||||||
def periodic_test():
|
|
||||||
"""Run automated tests every 5 seconds"""
|
|
||||||
# Take screenshot
|
|
||||||
automation.screenshot(f"test_{mcrfpy.getFrame()}.png")
|
|
||||||
|
|
||||||
# Check game state
|
|
||||||
scene = mcrfpy.currentScene()
|
|
||||||
if scene == "main_menu":
|
|
||||||
# Click start button
|
|
||||||
automation.click(400, 300)
|
|
||||||
elif scene == "game":
|
|
||||||
# Perform game tests
|
|
||||||
automation.hotkey("i") # Open inventory
|
|
||||||
|
|
||||||
print(f"Test completed at frame {mcrfpy.getFrame()}")
|
|
||||||
|
|
||||||
# Register timer for periodic testing
|
|
||||||
mcrfpy.setTimer("automation_test", periodic_test, 5000)
|
|
||||||
|
|
||||||
print("Automation script loaded - tests will run every 5 seconds")
|
|
||||||
|
|
||||||
# Script returns here - giving control back to C++
|
|
||||||
*/
|
|
||||||
|
|
||||||
// === Advanced Example: Event-Driven Automation ===
|
|
||||||
/*
|
|
||||||
# automation_advanced.py
|
|
||||||
|
|
||||||
import mcrfpy
|
|
||||||
from mcrfpy import automation
|
|
||||||
import json
|
|
||||||
|
|
||||||
class AutomationFramework:
|
|
||||||
def __init__(self):
|
|
||||||
self.test_queue = []
|
|
||||||
self.results = []
|
|
||||||
self.load_test_suite()
|
|
||||||
|
|
||||||
def load_test_suite(self):
|
|
||||||
"""Load test definitions from JSON"""
|
|
||||||
with open("test_suite.json") as f:
|
|
||||||
self.test_queue = json.load(f)["tests"]
|
|
||||||
|
|
||||||
def run_next_test(self):
|
|
||||||
"""Execute next test in queue"""
|
|
||||||
if not self.test_queue:
|
|
||||||
self.finish_testing()
|
|
||||||
return
|
|
||||||
|
|
||||||
test = self.test_queue.pop(0)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if test["type"] == "click":
|
|
||||||
automation.click(test["x"], test["y"])
|
|
||||||
elif test["type"] == "key":
|
|
||||||
automation.keyDown(test["key"])
|
|
||||||
automation.keyUp(test["key"])
|
|
||||||
elif test["type"] == "screenshot":
|
|
||||||
automation.screenshot(test["filename"])
|
|
||||||
elif test["type"] == "wait":
|
|
||||||
# Re-queue this test for later
|
|
||||||
self.test_queue.insert(0, test)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.results.append({"test": test, "status": "pass"})
|
|
||||||
except Exception as e:
|
|
||||||
self.results.append({"test": test, "status": "fail", "error": str(e)})
|
|
||||||
|
|
||||||
def finish_testing(self):
|
|
||||||
"""Save test results and cleanup"""
|
|
||||||
with open("test_results.json", "w") as f:
|
|
||||||
json.dump(self.results, f, indent=2)
|
|
||||||
print(f"Testing complete: {len(self.results)} tests executed")
|
|
||||||
mcrfpy.delTimer("automation_framework")
|
|
||||||
|
|
||||||
# Create and start automation
|
|
||||||
framework = AutomationFramework()
|
|
||||||
mcrfpy.setTimer("automation_framework", framework.run_next_test, 100)
|
|
||||||
*/
|
|
||||||
|
|
||||||
// === Thread Safety Considerations ===
|
|
||||||
|
|
||||||
// The --exec approach requires NO thread safety changes because:
|
|
||||||
// 1. All scripts run in the same Python interpreter
|
|
||||||
// 2. Scripts execute sequentially during initialization
|
|
||||||
// 3. After initialization, only callbacks run (timer/input based)
|
|
||||||
// 4. C++ maintains control of the render loop
|
|
||||||
|
|
||||||
// This is the "honor system" - scripts must:
|
|
||||||
// - Set up their callbacks/timers
|
|
||||||
// - Return control to C++
|
|
||||||
// - Not block or run infinite loops
|
|
||||||
// - Use timers for periodic tasks
|
|
||||||
|
|
||||||
// === Future Extensions ===
|
|
||||||
|
|
||||||
// 1. Script communication via shared Python modules
|
|
||||||
// game.py:
|
|
||||||
// import mcrfpy
|
|
||||||
// mcrfpy.game_state = {"level": 1, "score": 0}
|
|
||||||
//
|
|
||||||
// automation.py:
|
|
||||||
// import mcrfpy
|
|
||||||
// if mcrfpy.game_state["level"] == 1:
|
|
||||||
// # Test level 1 specific features
|
|
||||||
|
|
||||||
// 2. Priority-based script execution
|
|
||||||
// ./mcrogueface game.py --exec-priority high:critical.py --exec-priority low:logging.py
|
|
||||||
|
|
||||||
// 3. Conditional execution
|
|
||||||
// ./mcrogueface game.py --exec-if-scene menu:menu_test.py --exec-if-scene game:game_test.py
|
|
Before Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 31 KiB |
|
@ -1,268 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""Generate .pyi type stub files for McRogueFace Python API.
|
|
||||||
|
|
||||||
This script introspects the mcrfpy module and generates type stubs
|
|
||||||
for better IDE support and type checking.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import inspect
|
|
||||||
import types
|
|
||||||
from typing import Dict, List, Set, Any
|
|
||||||
|
|
||||||
# Add the build directory to path to import mcrfpy
|
|
||||||
sys.path.insert(0, './build')
|
|
||||||
|
|
||||||
try:
|
|
||||||
import mcrfpy
|
|
||||||
except ImportError:
|
|
||||||
print("Error: Could not import mcrfpy. Make sure to run this from the project root after building.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
def parse_docstring_signature(doc: str) -> tuple[str, str]:
|
|
||||||
"""Extract signature and description from docstring."""
|
|
||||||
if not doc:
|
|
||||||
return "", ""
|
|
||||||
|
|
||||||
lines = doc.strip().split('\n')
|
|
||||||
if lines:
|
|
||||||
# First line often contains the signature
|
|
||||||
first_line = lines[0]
|
|
||||||
if '(' in first_line and ')' in first_line:
|
|
||||||
# Extract just the part after the function name
|
|
||||||
start = first_line.find('(')
|
|
||||||
end = first_line.rfind(')') + 1
|
|
||||||
if start != -1 and end != 0:
|
|
||||||
sig = first_line[start:end]
|
|
||||||
# Get return type if present
|
|
||||||
if '->' in first_line:
|
|
||||||
ret_start = first_line.find('->')
|
|
||||||
ret_type = first_line[ret_start:].strip()
|
|
||||||
return sig, ret_type
|
|
||||||
return sig, ""
|
|
||||||
return "", ""
|
|
||||||
|
|
||||||
def get_type_hint(obj_type: type) -> str:
|
|
||||||
"""Convert Python type to type hint string."""
|
|
||||||
if obj_type == int:
|
|
||||||
return "int"
|
|
||||||
elif obj_type == float:
|
|
||||||
return "float"
|
|
||||||
elif obj_type == str:
|
|
||||||
return "str"
|
|
||||||
elif obj_type == bool:
|
|
||||||
return "bool"
|
|
||||||
elif obj_type == list:
|
|
||||||
return "List[Any]"
|
|
||||||
elif obj_type == dict:
|
|
||||||
return "Dict[Any, Any]"
|
|
||||||
elif obj_type == tuple:
|
|
||||||
return "Tuple[Any, ...]"
|
|
||||||
elif obj_type == type(None):
|
|
||||||
return "None"
|
|
||||||
else:
|
|
||||||
return "Any"
|
|
||||||
|
|
||||||
def generate_class_stub(class_name: str, cls: type) -> List[str]:
|
|
||||||
"""Generate stub for a class."""
|
|
||||||
lines = []
|
|
||||||
|
|
||||||
# Get class docstring
|
|
||||||
if cls.__doc__:
|
|
||||||
doc_lines = cls.__doc__.strip().split('\n')
|
|
||||||
# Use only the first paragraph for the stub
|
|
||||||
lines.append(f'class {class_name}:')
|
|
||||||
lines.append(f' """{doc_lines[0]}"""')
|
|
||||||
else:
|
|
||||||
lines.append(f'class {class_name}:')
|
|
||||||
|
|
||||||
# Check for __init__ method
|
|
||||||
if hasattr(cls, '__init__'):
|
|
||||||
init_doc = cls.__init__.__doc__ or cls.__doc__
|
|
||||||
if init_doc:
|
|
||||||
sig, ret = parse_docstring_signature(init_doc)
|
|
||||||
if sig:
|
|
||||||
lines.append(f' def __init__(self{sig[1:-1]}) -> None: ...')
|
|
||||||
else:
|
|
||||||
lines.append(f' def __init__(self, *args, **kwargs) -> None: ...')
|
|
||||||
else:
|
|
||||||
lines.append(f' def __init__(self, *args, **kwargs) -> None: ...')
|
|
||||||
|
|
||||||
# Get properties and methods
|
|
||||||
properties = []
|
|
||||||
methods = []
|
|
||||||
|
|
||||||
for attr_name in dir(cls):
|
|
||||||
if attr_name.startswith('_') and not attr_name.startswith('__'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
attr = getattr(cls, attr_name)
|
|
||||||
|
|
||||||
if isinstance(attr, property):
|
|
||||||
properties.append((attr_name, attr))
|
|
||||||
elif callable(attr) and not attr_name.startswith('__'):
|
|
||||||
methods.append((attr_name, attr))
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Add properties
|
|
||||||
if properties:
|
|
||||||
lines.append('')
|
|
||||||
for prop_name, prop in properties:
|
|
||||||
# Try to determine property type from docstring
|
|
||||||
if prop.fget and prop.fget.__doc__:
|
|
||||||
lines.append(f' @property')
|
|
||||||
lines.append(f' def {prop_name}(self) -> Any: ...')
|
|
||||||
if prop.fset:
|
|
||||||
lines.append(f' @{prop_name}.setter')
|
|
||||||
lines.append(f' def {prop_name}(self, value: Any) -> None: ...')
|
|
||||||
else:
|
|
||||||
lines.append(f' {prop_name}: Any')
|
|
||||||
|
|
||||||
# Add methods
|
|
||||||
if methods:
|
|
||||||
lines.append('')
|
|
||||||
for method_name, method in methods:
|
|
||||||
if method.__doc__:
|
|
||||||
sig, ret = parse_docstring_signature(method.__doc__)
|
|
||||||
if sig and ret:
|
|
||||||
lines.append(f' def {method_name}(self{sig[1:-1]}) {ret}: ...')
|
|
||||||
elif sig:
|
|
||||||
lines.append(f' def {method_name}(self{sig[1:-1]}) -> Any: ...')
|
|
||||||
else:
|
|
||||||
lines.append(f' def {method_name}(self, *args, **kwargs) -> Any: ...')
|
|
||||||
else:
|
|
||||||
lines.append(f' def {method_name}(self, *args, **kwargs) -> Any: ...')
|
|
||||||
|
|
||||||
lines.append('')
|
|
||||||
return lines
|
|
||||||
|
|
||||||
def generate_function_stub(func_name: str, func: Any) -> str:
|
|
||||||
"""Generate stub for a function."""
|
|
||||||
if func.__doc__:
|
|
||||||
sig, ret = parse_docstring_signature(func.__doc__)
|
|
||||||
if sig and ret:
|
|
||||||
return f'def {func_name}{sig} {ret}: ...'
|
|
||||||
elif sig:
|
|
||||||
return f'def {func_name}{sig} -> Any: ...'
|
|
||||||
|
|
||||||
return f'def {func_name}(*args, **kwargs) -> Any: ...'
|
|
||||||
|
|
||||||
def generate_stubs():
|
|
||||||
"""Generate the main mcrfpy.pyi file."""
|
|
||||||
lines = [
|
|
||||||
'"""Type stubs for McRogueFace Python API.',
|
|
||||||
'',
|
|
||||||
'Auto-generated - do not edit directly.',
|
|
||||||
'"""',
|
|
||||||
'',
|
|
||||||
'from typing import Any, List, Dict, Tuple, Optional, Callable, Union',
|
|
||||||
'',
|
|
||||||
'# Module documentation',
|
|
||||||
]
|
|
||||||
|
|
||||||
# Add module docstring as comment
|
|
||||||
if mcrfpy.__doc__:
|
|
||||||
for line in mcrfpy.__doc__.strip().split('\n')[:3]:
|
|
||||||
lines.append(f'# {line}')
|
|
||||||
|
|
||||||
lines.extend(['', '# Classes', ''])
|
|
||||||
|
|
||||||
# Collect all classes
|
|
||||||
classes = []
|
|
||||||
functions = []
|
|
||||||
constants = []
|
|
||||||
|
|
||||||
for name in sorted(dir(mcrfpy)):
|
|
||||||
if name.startswith('_'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
obj = getattr(mcrfpy, name)
|
|
||||||
|
|
||||||
if isinstance(obj, type):
|
|
||||||
classes.append((name, obj))
|
|
||||||
elif callable(obj):
|
|
||||||
functions.append((name, obj))
|
|
||||||
elif not inspect.ismodule(obj):
|
|
||||||
constants.append((name, obj))
|
|
||||||
|
|
||||||
# Generate class stubs
|
|
||||||
for class_name, cls in classes:
|
|
||||||
lines.extend(generate_class_stub(class_name, cls))
|
|
||||||
|
|
||||||
# Generate function stubs
|
|
||||||
if functions:
|
|
||||||
lines.extend(['# Functions', ''])
|
|
||||||
for func_name, func in functions:
|
|
||||||
lines.append(generate_function_stub(func_name, func))
|
|
||||||
lines.append('')
|
|
||||||
|
|
||||||
# Generate constants
|
|
||||||
if constants:
|
|
||||||
lines.extend(['# Constants', ''])
|
|
||||||
for const_name, const in constants:
|
|
||||||
const_type = get_type_hint(type(const))
|
|
||||||
lines.append(f'{const_name}: {const_type}')
|
|
||||||
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
def generate_automation_stubs():
|
|
||||||
"""Generate stubs for the automation submodule."""
|
|
||||||
if not hasattr(mcrfpy, 'automation'):
|
|
||||||
return None
|
|
||||||
|
|
||||||
automation = mcrfpy.automation
|
|
||||||
|
|
||||||
lines = [
|
|
||||||
'"""Type stubs for McRogueFace automation API."""',
|
|
||||||
'',
|
|
||||||
'from typing import Optional, Tuple',
|
|
||||||
'',
|
|
||||||
]
|
|
||||||
|
|
||||||
# Get all automation functions
|
|
||||||
for name in sorted(dir(automation)):
|
|
||||||
if name.startswith('_'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
obj = getattr(automation, name)
|
|
||||||
if callable(obj):
|
|
||||||
lines.append(generate_function_stub(name, obj))
|
|
||||||
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Main entry point."""
|
|
||||||
print("Generating type stubs for McRogueFace...")
|
|
||||||
|
|
||||||
# Generate main module stubs
|
|
||||||
stubs = generate_stubs()
|
|
||||||
|
|
||||||
# Create stubs directory
|
|
||||||
os.makedirs('stubs', exist_ok=True)
|
|
||||||
|
|
||||||
# Write main module stubs
|
|
||||||
with open('stubs/mcrfpy.pyi', 'w') as f:
|
|
||||||
f.write(stubs)
|
|
||||||
print("Generated stubs/mcrfpy.pyi")
|
|
||||||
|
|
||||||
# Generate automation module stubs if available
|
|
||||||
automation_stubs = generate_automation_stubs()
|
|
||||||
if automation_stubs:
|
|
||||||
os.makedirs('stubs/mcrfpy', exist_ok=True)
|
|
||||||
with open('stubs/mcrfpy/__init__.pyi', 'w') as f:
|
|
||||||
f.write(stubs)
|
|
||||||
with open('stubs/mcrfpy/automation.pyi', 'w') as f:
|
|
||||||
f.write(automation_stubs)
|
|
||||||
print("Generated stubs/mcrfpy/automation.pyi")
|
|
||||||
|
|
||||||
print("\nType stubs generated successfully!")
|
|
||||||
print("\nTo use in your IDE:")
|
|
||||||
print("1. Add the 'stubs' directory to your PYTHONPATH")
|
|
||||||
print("2. Or configure your IDE to look for stubs in the 'stubs' directory")
|
|
||||||
print("3. Most IDEs will automatically detect .pyi files")
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
102
gitea_issues.py
|
@ -1,102 +0,0 @@
|
||||||
import json
|
|
||||||
from time import time
|
|
||||||
#with open("/home/john/issues.json", "r") as f:
|
|
||||||
# data = json.loads(f.read())
|
|
||||||
#with open("/home/john/issues2.json", "r") as f:
|
|
||||||
# data.extend(json.loads(f.read()))
|
|
||||||
|
|
||||||
print("Fetching issues...", end='')
|
|
||||||
start = time()
|
|
||||||
from gitea import Gitea, Repository, Issue
|
|
||||||
g = Gitea("https://gamedev.ffwf.net/gitea", token_text="3b450f66e21d62c22bb9fa1c8b975049a5d0c38d")
|
|
||||||
repo = Repository.request(g, "john", "McRogueFace")
|
|
||||||
issues = repo.get_issues()
|
|
||||||
dur = time() - start
|
|
||||||
print(f"({dur:.1f}s)")
|
|
||||||
print("Gitea Version: " + g.get_version())
|
|
||||||
print("API-Token belongs to user: " + g.get_user().username)
|
|
||||||
|
|
||||||
data = [
|
|
||||||
{
|
|
||||||
"labels": i.labels,
|
|
||||||
"body": i.body,
|
|
||||||
"number": i.number,
|
|
||||||
}
|
|
||||||
for i in issues
|
|
||||||
]
|
|
||||||
|
|
||||||
input()
|
|
||||||
|
|
||||||
def front_number(txt):
|
|
||||||
if not txt[0].isdigit(): return None
|
|
||||||
number = ""
|
|
||||||
for c in txt:
|
|
||||||
if not c.isdigit():
|
|
||||||
break
|
|
||||||
number += c
|
|
||||||
return int(number)
|
|
||||||
|
|
||||||
def split_any(txt, splitters):
|
|
||||||
tokens = []
|
|
||||||
txt = [txt]
|
|
||||||
for s in splitters:
|
|
||||||
for t in txt:
|
|
||||||
tokens.extend(t.split(s))
|
|
||||||
txt = tokens
|
|
||||||
tokens = []
|
|
||||||
return txt
|
|
||||||
|
|
||||||
def find_refs(txt):
|
|
||||||
tokens = [tok for tok in split_any(txt, ' ,;\t\r\n') if tok.startswith('#')]
|
|
||||||
return [front_number(tok[1:]) for tok in tokens]
|
|
||||||
|
|
||||||
from collections import defaultdict
|
|
||||||
issue_relations = defaultdict(list)
|
|
||||||
|
|
||||||
nodes = set()
|
|
||||||
|
|
||||||
for issue in data:
|
|
||||||
#refs = issue['body'].split('#')[1::2]
|
|
||||||
|
|
||||||
#refs = [front_number(r) for r in refs if front_number(r) is not None]
|
|
||||||
refs = find_refs(issue['body'])
|
|
||||||
print(issue['number'], ':', refs)
|
|
||||||
issue_relations[issue['number']].extend(refs)
|
|
||||||
nodes.add(issue['number'])
|
|
||||||
for r in refs:
|
|
||||||
nodes.add(r)
|
|
||||||
issue_relations[r].append(issue['number'])
|
|
||||||
|
|
||||||
|
|
||||||
# Find issue labels
|
|
||||||
issue_labels = {}
|
|
||||||
for d in data:
|
|
||||||
labels = [l['name'] for l in d['labels']]
|
|
||||||
#print(d['number'], labels)
|
|
||||||
issue_labels[d['number']] = labels
|
|
||||||
|
|
||||||
import networkx as nx
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
|
|
||||||
relations = nx.Graph()
|
|
||||||
|
|
||||||
for k in issue_relations:
|
|
||||||
relations.add_node(k)
|
|
||||||
for r in issue_relations[k]:
|
|
||||||
relations.add_edge(k, r)
|
|
||||||
relations.add_edge(r, k)
|
|
||||||
|
|
||||||
#nx.draw_networkx(relations)
|
|
||||||
|
|
||||||
pos = nx.spring_layout(relations)
|
|
||||||
nx.draw_networkx_nodes(relations, pos,
|
|
||||||
nodelist = [n for n in issue_labels if 'Alpha Release Requirement' in issue_labels[n]],
|
|
||||||
node_color="tab:red")
|
|
||||||
nx.draw_networkx_nodes(relations, pos,
|
|
||||||
nodelist = [n for n in issue_labels if 'Alpha Release Requirement' not in issue_labels[n]],
|
|
||||||
node_color="tab:blue")
|
|
||||||
nx.draw_networkx_edges(relations, pos,
|
|
||||||
edgelist = relations.edges()
|
|
||||||
)
|
|
||||||
nx.draw_networkx_labels(relations, pos, {i: str(i) for i in relations.nodes()})
|
|
||||||
plt.show()
|
|
|
@ -0,0 +1,253 @@
|
||||||
|
# Part 0 - Setting Up McRogueFace
|
||||||
|
|
||||||
|
Welcome to the McRogueFace Roguelike Tutorial! This tutorial will teach you how to create a complete roguelike game using the McRogueFace game engine. Unlike traditional Python libraries, McRogueFace is a complete, portable game engine that includes everything you need to make and distribute games.
|
||||||
|
|
||||||
|
## What is McRogueFace?
|
||||||
|
|
||||||
|
McRogueFace is a high-performance game engine with Python scripting support. Think of it like Unity or Godot, but specifically designed for roguelikes and 2D games. It includes:
|
||||||
|
|
||||||
|
- A complete Python 3.12 runtime (no installation needed!)
|
||||||
|
- High-performance C++ rendering and entity management
|
||||||
|
- Built-in UI components and scene management
|
||||||
|
- Integrated audio system
|
||||||
|
- Professional sprite-based graphics
|
||||||
|
- Easy distribution - your players don't need Python installed!
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before starting this tutorial, you should:
|
||||||
|
|
||||||
|
- Have basic Python knowledge (variables, functions, classes)
|
||||||
|
- Be comfortable editing text files
|
||||||
|
- Have a text editor (VS Code, Sublime Text, Notepad++, etc.)
|
||||||
|
|
||||||
|
That's it! Unlike other roguelike tutorials, you don't need Python installed - McRogueFace includes everything.
|
||||||
|
|
||||||
|
## Getting McRogueFace
|
||||||
|
|
||||||
|
### Step 1: Download the Engine
|
||||||
|
|
||||||
|
1. Visit the McRogueFace releases page
|
||||||
|
2. Download the version for your operating system:
|
||||||
|
- `McRogueFace-Windows.zip` for Windows
|
||||||
|
- `McRogueFace-MacOS.zip` for macOS
|
||||||
|
- `McRogueFace-Linux.zip` for Linux
|
||||||
|
|
||||||
|
### Step 2: Extract the Archive
|
||||||
|
|
||||||
|
Extract the downloaded archive to a folder where you want to develop your game. You should see this structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
McRogueFace/
|
||||||
|
├── mcrogueface (or mcrogueface.exe on Windows)
|
||||||
|
├── scripts/
|
||||||
|
│ └── game.py
|
||||||
|
├── assets/
|
||||||
|
│ ├── sprites/
|
||||||
|
│ ├── fonts/
|
||||||
|
│ └── audio/
|
||||||
|
└── lib/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Run the Engine
|
||||||
|
|
||||||
|
Run the McRogueFace executable:
|
||||||
|
|
||||||
|
- **Windows**: Double-click `mcrogueface.exe`
|
||||||
|
- **Mac/Linux**: Open a terminal in the folder and run `./mcrogueface`
|
||||||
|
|
||||||
|
You should see a window open with the default McRogueFace demo. This shows the engine is working correctly!
|
||||||
|
|
||||||
|
## Your First McRogueFace Script
|
||||||
|
|
||||||
|
Let's modify the engine to display "Hello Roguelike!" instead of the default demo.
|
||||||
|
|
||||||
|
### Step 1: Open game.py
|
||||||
|
|
||||||
|
Open `scripts/game.py` in your text editor. You'll see the default demo code. Replace it entirely with:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import mcrfpy
|
||||||
|
|
||||||
|
# Create a new scene called "hello"
|
||||||
|
mcrfpy.createScene("hello")
|
||||||
|
|
||||||
|
# Switch to our new scene
|
||||||
|
mcrfpy.setScene("hello")
|
||||||
|
|
||||||
|
# Get the UI container for our scene
|
||||||
|
ui = mcrfpy.sceneUI("hello")
|
||||||
|
|
||||||
|
# Create a text caption
|
||||||
|
caption = mcrfpy.Caption("Hello Roguelike!", 400, 300)
|
||||||
|
caption.font_size = 32
|
||||||
|
caption.fill_color = mcrfpy.Color(255, 255, 255) # White text
|
||||||
|
|
||||||
|
# Add the caption to our scene
|
||||||
|
ui.append(caption)
|
||||||
|
|
||||||
|
# Create a smaller instruction caption
|
||||||
|
instruction = mcrfpy.Caption("Press ESC to exit", 400, 350)
|
||||||
|
instruction.font_size = 16
|
||||||
|
instruction.fill_color = mcrfpy.Color(200, 200, 200) # Light gray
|
||||||
|
ui.append(instruction)
|
||||||
|
|
||||||
|
# Set up a simple key handler
|
||||||
|
def handle_keys(key, state):
|
||||||
|
if state == "start" and key == "Escape":
|
||||||
|
mcrfpy.setScene(None) # This exits the game
|
||||||
|
|
||||||
|
mcrfpy.keypressScene(handle_keys)
|
||||||
|
|
||||||
|
print("Hello Roguelike is running!")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Save and Run
|
||||||
|
|
||||||
|
1. Save the file
|
||||||
|
2. If McRogueFace is still running, it will automatically reload!
|
||||||
|
3. If not, run the engine again
|
||||||
|
|
||||||
|
You should now see "Hello Roguelike!" displayed in the window.
|
||||||
|
|
||||||
|
### Step 3: Understanding the Code
|
||||||
|
|
||||||
|
Let's break down what we just wrote:
|
||||||
|
|
||||||
|
1. **Import mcrfpy**: This is McRogueFace's Python API
|
||||||
|
2. **Create a scene**: Scenes are like game states (menu, gameplay, inventory, etc.)
|
||||||
|
3. **UI elements**: We create Caption objects for text display
|
||||||
|
4. **Colors**: McRogueFace uses RGB colors (0-255 for each component)
|
||||||
|
5. **Input handling**: We set up a callback for keyboard input
|
||||||
|
6. **Scene switching**: Setting the scene to None exits the game
|
||||||
|
|
||||||
|
## Key Differences from Pure Python Development
|
||||||
|
|
||||||
|
### The Game Loop
|
||||||
|
|
||||||
|
Unlike typical Python scripts, McRogueFace runs your code inside its game loop:
|
||||||
|
|
||||||
|
1. The engine starts and loads `scripts/game.py`
|
||||||
|
2. Your script sets up scenes, UI elements, and callbacks
|
||||||
|
3. The engine runs at 60 FPS, handling rendering and input
|
||||||
|
4. Your callbacks are triggered by game events
|
||||||
|
|
||||||
|
### Hot Reloading
|
||||||
|
|
||||||
|
McRogueFace can reload your scripts while running! Just save your changes and the engine will reload automatically. This makes development incredibly fast.
|
||||||
|
|
||||||
|
### Asset Pipeline
|
||||||
|
|
||||||
|
McRogueFace includes a complete asset system:
|
||||||
|
|
||||||
|
- **Sprites**: Place images in `assets/sprites/`
|
||||||
|
- **Fonts**: TrueType fonts in `assets/fonts/`
|
||||||
|
- **Audio**: Sound effects and music in `assets/audio/`
|
||||||
|
|
||||||
|
We'll explore these in later lessons.
|
||||||
|
|
||||||
|
## Testing Your Setup
|
||||||
|
|
||||||
|
Let's create a more interactive test to ensure everything is working properly:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import mcrfpy
|
||||||
|
|
||||||
|
# Create our test scene
|
||||||
|
mcrfpy.createScene("test")
|
||||||
|
mcrfpy.setScene("test")
|
||||||
|
ui = mcrfpy.sceneUI("test")
|
||||||
|
|
||||||
|
# Create a background frame
|
||||||
|
background = mcrfpy.Frame(0, 0, 1024, 768)
|
||||||
|
background.fill_color = mcrfpy.Color(20, 20, 30) # Dark blue-gray
|
||||||
|
ui.append(background)
|
||||||
|
|
||||||
|
# Title text
|
||||||
|
title = mcrfpy.Caption("McRogueFace Setup Test", 512, 100)
|
||||||
|
title.font_size = 36
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 100) # Yellow
|
||||||
|
ui.append(title)
|
||||||
|
|
||||||
|
# Status text that will update
|
||||||
|
status_text = mcrfpy.Caption("Press any key to test input...", 512, 300)
|
||||||
|
status_text.font_size = 20
|
||||||
|
status_text.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
|
ui.append(status_text)
|
||||||
|
|
||||||
|
# Instructions
|
||||||
|
instructions = [
|
||||||
|
"Arrow Keys: Test movement input",
|
||||||
|
"Space: Test action input",
|
||||||
|
"Mouse Click: Test mouse input",
|
||||||
|
"ESC: Exit"
|
||||||
|
]
|
||||||
|
|
||||||
|
y_offset = 400
|
||||||
|
for instruction in instructions:
|
||||||
|
inst_caption = mcrfpy.Caption(instruction, 512, y_offset)
|
||||||
|
inst_caption.font_size = 16
|
||||||
|
inst_caption.fill_color = mcrfpy.Color(150, 150, 150)
|
||||||
|
ui.append(inst_caption)
|
||||||
|
y_offset += 30
|
||||||
|
|
||||||
|
# Input handler
|
||||||
|
def handle_input(key, state):
|
||||||
|
if state != "start":
|
||||||
|
return
|
||||||
|
|
||||||
|
if key == "Escape":
|
||||||
|
mcrfpy.setScene(None)
|
||||||
|
else:
|
||||||
|
status_text.text = f"You pressed: {key}"
|
||||||
|
status_text.fill_color = mcrfpy.Color(100, 255, 100) # Green
|
||||||
|
|
||||||
|
# Set up input handling
|
||||||
|
mcrfpy.keypressScene(handle_input)
|
||||||
|
|
||||||
|
print("Setup test is running! Try pressing different keys.")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Engine Won't Start
|
||||||
|
|
||||||
|
- **Windows**: Make sure you extracted all files, not just the .exe
|
||||||
|
- **Mac**: You may need to right-click and select "Open" the first time
|
||||||
|
- **Linux**: Make sure the file is executable: `chmod +x mcrogueface`
|
||||||
|
|
||||||
|
### Scripts Not Loading
|
||||||
|
|
||||||
|
- Ensure your script is named exactly `game.py` in the `scripts/` folder
|
||||||
|
- Check the console output for Python errors
|
||||||
|
- Make sure you're using Python 3 syntax
|
||||||
|
|
||||||
|
### Performance Issues
|
||||||
|
|
||||||
|
- McRogueFace should run smoothly at 60 FPS
|
||||||
|
- If not, check if your graphics drivers are updated
|
||||||
|
- The engine shows FPS in the window title
|
||||||
|
|
||||||
|
## What's Next?
|
||||||
|
|
||||||
|
Congratulations! You now have McRogueFace set up and running. You've learned:
|
||||||
|
|
||||||
|
- How to download and run the McRogueFace engine
|
||||||
|
- The basic structure of a McRogueFace project
|
||||||
|
- How to create scenes and UI elements
|
||||||
|
- How to handle keyboard input
|
||||||
|
- The development workflow with hot reloading
|
||||||
|
|
||||||
|
In Part 1, we'll create our player character and implement movement. We'll explore McRogueFace's entity system and learn how to create a game world.
|
||||||
|
|
||||||
|
## Why McRogueFace?
|
||||||
|
|
||||||
|
Before we continue, let's highlight why McRogueFace is excellent for roguelike development:
|
||||||
|
|
||||||
|
1. **No Installation Hassles**: Your players just download and run - no Python needed!
|
||||||
|
2. **Professional Performance**: C++ engine core means smooth gameplay even with hundreds of entities
|
||||||
|
3. **Built-in Features**: UI, audio, scenes, and animations are already there
|
||||||
|
4. **Easy Distribution**: Just zip your game folder and share it
|
||||||
|
5. **Rapid Development**: Hot reloading and Python scripting for quick iteration
|
||||||
|
|
||||||
|
Ready to make a roguelike? Let's continue to Part 1!
|
|
@ -0,0 +1,457 @@
|
||||||
|
# Part 1 - Drawing the '@' Symbol and Moving It Around
|
||||||
|
|
||||||
|
In Part 0, we set up McRogueFace and created a simple "Hello Roguelike" scene. Now it's time to create the foundation of our game: a player character that can move around the screen.
|
||||||
|
|
||||||
|
In traditional roguelikes, the player is represented by the '@' symbol. We'll honor that tradition while taking advantage of McRogueFace's powerful sprite-based rendering system.
|
||||||
|
|
||||||
|
## Understanding McRogueFace's Architecture
|
||||||
|
|
||||||
|
Before we dive into code, let's understand two key concepts in McRogueFace:
|
||||||
|
|
||||||
|
### Grid - The Game World
|
||||||
|
|
||||||
|
A `Grid` represents your game world. It's a 2D array of tiles where each tile can be:
|
||||||
|
- **Walkable or not** (for collision detection)
|
||||||
|
- **Transparent or not** (for field of view, which we'll cover later)
|
||||||
|
- **Have a visual appearance** (sprite index and color)
|
||||||
|
|
||||||
|
Think of the Grid as the dungeon floor, walls, and other static elements.
|
||||||
|
|
||||||
|
### Entity - Things That Move
|
||||||
|
|
||||||
|
An `Entity` represents anything that can move around on the Grid:
|
||||||
|
- The player character
|
||||||
|
- Monsters
|
||||||
|
- Items (if you want them to be thrown or moved)
|
||||||
|
- Projectiles
|
||||||
|
|
||||||
|
Entities exist "on top of" the Grid and automatically handle smooth movement animation between tiles.
|
||||||
|
|
||||||
|
## Creating Our Game World
|
||||||
|
|
||||||
|
Let's start by creating a simple room for our player to move around in. Create a new `game.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import mcrfpy
|
||||||
|
|
||||||
|
# Define some constants for our tile types
|
||||||
|
FLOOR_TILE = 0
|
||||||
|
WALL_TILE = 1
|
||||||
|
PLAYER_SPRITE = 2
|
||||||
|
|
||||||
|
# Window configuration
|
||||||
|
mcrfpy.createScene("game")
|
||||||
|
mcrfpy.setScene("game")
|
||||||
|
|
||||||
|
# Configure window properties
|
||||||
|
window = mcrfpy.Window.get()
|
||||||
|
window.title = "McRogueFace Roguelike - Part 1"
|
||||||
|
|
||||||
|
# Get the UI container for our scene
|
||||||
|
ui = mcrfpy.sceneUI("game")
|
||||||
|
|
||||||
|
# Create a dark background
|
||||||
|
background = mcrfpy.Frame(0, 0, 1024, 768)
|
||||||
|
background.fill_color = mcrfpy.Color(0, 0, 0)
|
||||||
|
ui.append(background)
|
||||||
|
```
|
||||||
|
|
||||||
|
Now we need to set up our tileset. For this tutorial, we'll use ASCII-style sprites. McRogueFace comes with a built-in ASCII tileset:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Load the ASCII tileset
|
||||||
|
# This tileset has characters mapped to sprite indices
|
||||||
|
# For example: @ = 64, # = 35, . = 46
|
||||||
|
tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
||||||
|
|
||||||
|
# Create the game grid
|
||||||
|
# 50x30 tiles is a good size for a roguelike
|
||||||
|
GRID_WIDTH = 50
|
||||||
|
GRID_HEIGHT = 30
|
||||||
|
|
||||||
|
grid = mcrfpy.Grid(grid_x=GRID_WIDTH, grid_y=GRID_HEIGHT, texture=tileset)
|
||||||
|
grid.position = (100, 100) # Position on screen
|
||||||
|
grid.size = (800, 480) # Size in pixels
|
||||||
|
|
||||||
|
# Add the grid to our UI
|
||||||
|
ui.append(grid)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Initializing the Game World
|
||||||
|
|
||||||
|
Now let's fill our grid with a simple room:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def create_room():
|
||||||
|
"""Create a room with walls around the edges"""
|
||||||
|
# Fill everything with floor tiles first
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
cell = grid.at(x, y)
|
||||||
|
cell.walkable = True
|
||||||
|
cell.transparent = True
|
||||||
|
cell.sprite_index = 46 # '.' character
|
||||||
|
cell.color = mcrfpy.Color(50, 50, 50) # Dark gray floor
|
||||||
|
|
||||||
|
# Create walls around the edges
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
# Top wall
|
||||||
|
cell = grid.at(x, 0)
|
||||||
|
cell.walkable = False
|
||||||
|
cell.transparent = False
|
||||||
|
cell.sprite_index = 35 # '#' character
|
||||||
|
cell.color = mcrfpy.Color(100, 100, 100) # Gray walls
|
||||||
|
|
||||||
|
# Bottom wall
|
||||||
|
cell = grid.at(x, GRID_HEIGHT - 1)
|
||||||
|
cell.walkable = False
|
||||||
|
cell.transparent = False
|
||||||
|
cell.sprite_index = 35 # '#' character
|
||||||
|
cell.color = mcrfpy.Color(100, 100, 100)
|
||||||
|
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
# Left wall
|
||||||
|
cell = grid.at(0, y)
|
||||||
|
cell.walkable = False
|
||||||
|
cell.transparent = False
|
||||||
|
cell.sprite_index = 35 # '#' character
|
||||||
|
cell.color = mcrfpy.Color(100, 100, 100)
|
||||||
|
|
||||||
|
# Right wall
|
||||||
|
cell = grid.at(GRID_WIDTH - 1, y)
|
||||||
|
cell.walkable = False
|
||||||
|
cell.transparent = False
|
||||||
|
cell.sprite_index = 35 # '#' character
|
||||||
|
cell.color = mcrfpy.Color(100, 100, 100)
|
||||||
|
|
||||||
|
# Create the room
|
||||||
|
create_room()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Creating the Player
|
||||||
|
|
||||||
|
Now let's add our player character:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Create the player entity
|
||||||
|
player = mcrfpy.Entity(x=GRID_WIDTH // 2, y=GRID_HEIGHT // 2, grid=grid)
|
||||||
|
player.sprite_index = 64 # '@' character
|
||||||
|
player.color = mcrfpy.Color(255, 255, 255) # White
|
||||||
|
|
||||||
|
# The entity is automatically added to the grid when we pass grid= parameter
|
||||||
|
# This is equivalent to: grid.entities.append(player)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Handling Input
|
||||||
|
|
||||||
|
McRogueFace uses a callback system for input. For a turn-based roguelike, we only care about key presses, not releases:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def handle_input(key, state):
|
||||||
|
"""Handle keyboard input for player movement"""
|
||||||
|
# Only process key presses, not releases
|
||||||
|
if state != "start":
|
||||||
|
return
|
||||||
|
|
||||||
|
# Movement deltas
|
||||||
|
dx, dy = 0, 0
|
||||||
|
|
||||||
|
# Arrow keys
|
||||||
|
if key == "Up":
|
||||||
|
dy = -1
|
||||||
|
elif key == "Down":
|
||||||
|
dy = 1
|
||||||
|
elif key == "Left":
|
||||||
|
dx = -1
|
||||||
|
elif key == "Right":
|
||||||
|
dx = 1
|
||||||
|
|
||||||
|
# Numpad movement (for true roguelike feel!)
|
||||||
|
elif key == "Num7": # Northwest
|
||||||
|
dx, dy = -1, -1
|
||||||
|
elif key == "Num8": # North
|
||||||
|
dy = -1
|
||||||
|
elif key == "Num9": # Northeast
|
||||||
|
dx, dy = 1, -1
|
||||||
|
elif key == "Num4": # West
|
||||||
|
dx = -1
|
||||||
|
elif key == "Num6": # East
|
||||||
|
dx = 1
|
||||||
|
elif key == "Num1": # Southwest
|
||||||
|
dx, dy = -1, 1
|
||||||
|
elif key == "Num2": # South
|
||||||
|
dy = 1
|
||||||
|
elif key == "Num3": # Southeast
|
||||||
|
dx, dy = 1, 1
|
||||||
|
|
||||||
|
# Escape to quit
|
||||||
|
elif key == "Escape":
|
||||||
|
mcrfpy.setScene(None)
|
||||||
|
return
|
||||||
|
|
||||||
|
# If there's movement, try to move the player
|
||||||
|
if dx != 0 or dy != 0:
|
||||||
|
move_player(dx, dy)
|
||||||
|
|
||||||
|
# Register the input handler
|
||||||
|
mcrfpy.keypressScene(handle_input)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementing Movement with Collision Detection
|
||||||
|
|
||||||
|
Now let's implement the movement function with proper collision detection:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def move_player(dx, dy):
|
||||||
|
"""Move the player if the destination is walkable"""
|
||||||
|
# Calculate new position
|
||||||
|
new_x = player.x + dx
|
||||||
|
new_y = player.y + dy
|
||||||
|
|
||||||
|
# Check bounds
|
||||||
|
if new_x < 0 or new_x >= GRID_WIDTH or new_y < 0 or new_y >= GRID_HEIGHT:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if the destination is walkable
|
||||||
|
destination = grid.at(new_x, new_y)
|
||||||
|
if destination.walkable:
|
||||||
|
# Move the player
|
||||||
|
player.x = new_x
|
||||||
|
player.y = new_y
|
||||||
|
# The entity will automatically animate to the new position!
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding Visual Polish
|
||||||
|
|
||||||
|
Let's add some UI elements to make our game look more polished:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Add a title
|
||||||
|
title = mcrfpy.Caption("McRogueFace Roguelike", 512, 30)
|
||||||
|
title.font_size = 24
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 100) # Yellow
|
||||||
|
ui.append(title)
|
||||||
|
|
||||||
|
# Add instructions
|
||||||
|
instructions = mcrfpy.Caption("Arrow Keys or Numpad to move, ESC to quit", 512, 60)
|
||||||
|
instructions.font_size = 16
|
||||||
|
instructions.fill_color = mcrfpy.Color(200, 200, 200) # Light gray
|
||||||
|
ui.append(instructions)
|
||||||
|
|
||||||
|
# Add a status line at the bottom
|
||||||
|
status = mcrfpy.Caption("@ You", 100, 600)
|
||||||
|
status.font_size = 18
|
||||||
|
status.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
ui.append(status)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complete Code
|
||||||
|
|
||||||
|
Here's the complete `game.py` for Part 1:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import mcrfpy
|
||||||
|
|
||||||
|
# Window configuration
|
||||||
|
mcrfpy.createScene("game")
|
||||||
|
mcrfpy.setScene("game")
|
||||||
|
|
||||||
|
window = mcrfpy.Window.get()
|
||||||
|
window.title = "McRogueFace Roguelike - Part 1"
|
||||||
|
|
||||||
|
# Get the UI container for our scene
|
||||||
|
ui = mcrfpy.sceneUI("game")
|
||||||
|
|
||||||
|
# Create a dark background
|
||||||
|
background = mcrfpy.Frame(0, 0, 1024, 768)
|
||||||
|
background.fill_color = mcrfpy.Color(0, 0, 0)
|
||||||
|
ui.append(background)
|
||||||
|
|
||||||
|
# Load the ASCII tileset
|
||||||
|
tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
||||||
|
|
||||||
|
# Create the game grid
|
||||||
|
GRID_WIDTH = 50
|
||||||
|
GRID_HEIGHT = 30
|
||||||
|
|
||||||
|
grid = mcrfpy.Grid(grid_x=GRID_WIDTH, grid_y=GRID_HEIGHT, texture=tileset)
|
||||||
|
grid.position = (100, 100)
|
||||||
|
grid.size = (800, 480)
|
||||||
|
ui.append(grid)
|
||||||
|
|
||||||
|
def create_room():
|
||||||
|
"""Create a room with walls around the edges"""
|
||||||
|
# Fill everything with floor tiles first
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
cell = grid.at(x, y)
|
||||||
|
cell.walkable = True
|
||||||
|
cell.transparent = True
|
||||||
|
cell.sprite_index = 46 # '.' character
|
||||||
|
cell.color = mcrfpy.Color(50, 50, 50) # Dark gray floor
|
||||||
|
|
||||||
|
# Create walls around the edges
|
||||||
|
for x in range(GRID_WIDTH):
|
||||||
|
# Top wall
|
||||||
|
cell = grid.at(x, 0)
|
||||||
|
cell.walkable = False
|
||||||
|
cell.transparent = False
|
||||||
|
cell.sprite_index = 35 # '#' character
|
||||||
|
cell.color = mcrfpy.Color(100, 100, 100) # Gray walls
|
||||||
|
|
||||||
|
# Bottom wall
|
||||||
|
cell = grid.at(x, GRID_HEIGHT - 1)
|
||||||
|
cell.walkable = False
|
||||||
|
cell.transparent = False
|
||||||
|
cell.sprite_index = 35 # '#' character
|
||||||
|
cell.color = mcrfpy.Color(100, 100, 100)
|
||||||
|
|
||||||
|
for y in range(GRID_HEIGHT):
|
||||||
|
# Left wall
|
||||||
|
cell = grid.at(0, y)
|
||||||
|
cell.walkable = False
|
||||||
|
cell.transparent = False
|
||||||
|
cell.sprite_index = 35 # '#' character
|
||||||
|
cell.color = mcrfpy.Color(100, 100, 100)
|
||||||
|
|
||||||
|
# Right wall
|
||||||
|
cell = grid.at(GRID_WIDTH - 1, y)
|
||||||
|
cell.walkable = False
|
||||||
|
cell.transparent = False
|
||||||
|
cell.sprite_index = 35 # '#' character
|
||||||
|
cell.color = mcrfpy.Color(100, 100, 100)
|
||||||
|
|
||||||
|
# Create the room
|
||||||
|
create_room()
|
||||||
|
|
||||||
|
# Create the player entity
|
||||||
|
player = mcrfpy.Entity(x=GRID_WIDTH // 2, y=GRID_HEIGHT // 2, grid=grid)
|
||||||
|
player.sprite_index = 64 # '@' character
|
||||||
|
player.color = mcrfpy.Color(255, 255, 255) # White
|
||||||
|
|
||||||
|
def move_player(dx, dy):
|
||||||
|
"""Move the player if the destination is walkable"""
|
||||||
|
# Calculate new position
|
||||||
|
new_x = player.x + dx
|
||||||
|
new_y = player.y + dy
|
||||||
|
|
||||||
|
# Check bounds
|
||||||
|
if new_x < 0 or new_x >= GRID_WIDTH or new_y < 0 or new_y >= GRID_HEIGHT:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if the destination is walkable
|
||||||
|
destination = grid.at(new_x, new_y)
|
||||||
|
if destination.walkable:
|
||||||
|
# Move the player
|
||||||
|
player.x = new_x
|
||||||
|
player.y = new_y
|
||||||
|
|
||||||
|
def handle_input(key, state):
|
||||||
|
"""Handle keyboard input for player movement"""
|
||||||
|
# Only process key presses, not releases
|
||||||
|
if state != "start":
|
||||||
|
return
|
||||||
|
|
||||||
|
# Movement deltas
|
||||||
|
dx, dy = 0, 0
|
||||||
|
|
||||||
|
# Arrow keys
|
||||||
|
if key == "Up":
|
||||||
|
dy = -1
|
||||||
|
elif key == "Down":
|
||||||
|
dy = 1
|
||||||
|
elif key == "Left":
|
||||||
|
dx = -1
|
||||||
|
elif key == "Right":
|
||||||
|
dx = 1
|
||||||
|
|
||||||
|
# Numpad movement (for true roguelike feel!)
|
||||||
|
elif key == "Num7": # Northwest
|
||||||
|
dx, dy = -1, -1
|
||||||
|
elif key == "Num8": # North
|
||||||
|
dy = -1
|
||||||
|
elif key == "Num9": # Northeast
|
||||||
|
dx, dy = 1, -1
|
||||||
|
elif key == "Num4": # West
|
||||||
|
dx = -1
|
||||||
|
elif key == "Num6": # East
|
||||||
|
dx = 1
|
||||||
|
elif key == "Num1": # Southwest
|
||||||
|
dx, dy = -1, 1
|
||||||
|
elif key == "Num2": # South
|
||||||
|
dy = 1
|
||||||
|
elif key == "Num3": # Southeast
|
||||||
|
dx, dy = 1, 1
|
||||||
|
|
||||||
|
# Escape to quit
|
||||||
|
elif key == "Escape":
|
||||||
|
mcrfpy.setScene(None)
|
||||||
|
return
|
||||||
|
|
||||||
|
# If there's movement, try to move the player
|
||||||
|
if dx != 0 or dy != 0:
|
||||||
|
move_player(dx, dy)
|
||||||
|
|
||||||
|
# Register the input handler
|
||||||
|
mcrfpy.keypressScene(handle_input)
|
||||||
|
|
||||||
|
# Add UI elements
|
||||||
|
title = mcrfpy.Caption("McRogueFace Roguelike", 512, 30)
|
||||||
|
title.font_size = 24
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 100)
|
||||||
|
ui.append(title)
|
||||||
|
|
||||||
|
instructions = mcrfpy.Caption("Arrow Keys or Numpad to move, ESC to quit", 512, 60)
|
||||||
|
instructions.font_size = 16
|
||||||
|
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
|
ui.append(instructions)
|
||||||
|
|
||||||
|
status = mcrfpy.Caption("@ You", 100, 600)
|
||||||
|
status.font_size = 18
|
||||||
|
status.fill_color = mcrfpy.Color(255, 255, 255)
|
||||||
|
ui.append(status)
|
||||||
|
|
||||||
|
print("Part 1: The @ symbol moves!")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Understanding What We've Built
|
||||||
|
|
||||||
|
Let's review the key concepts we've implemented:
|
||||||
|
|
||||||
|
1. **Grid-Entity Architecture**: The Grid represents our static world (floors and walls), while the Entity (player) moves on top of it.
|
||||||
|
|
||||||
|
2. **Collision Detection**: By checking the `walkable` property of grid cells, we prevent the player from walking through walls.
|
||||||
|
|
||||||
|
3. **Turn-Based Input**: By only responding to key presses (not releases), we've created true turn-based movement.
|
||||||
|
|
||||||
|
4. **Visual Feedback**: The Entity system automatically animates movement between tiles, giving smooth visual feedback.
|
||||||
|
|
||||||
|
## Exercises
|
||||||
|
|
||||||
|
Try these modifications to deepen your understanding:
|
||||||
|
|
||||||
|
1. **Add More Rooms**: Create multiple rooms connected by corridors
|
||||||
|
2. **Different Tile Types**: Add doors (walkable but different appearance)
|
||||||
|
3. **Sprint Movement**: Hold Shift to move multiple tiles at once
|
||||||
|
4. **Mouse Support**: Click a tile to pathfind to it (we'll cover pathfinding properly later)
|
||||||
|
|
||||||
|
## ASCII Sprite Reference
|
||||||
|
|
||||||
|
Here are some useful ASCII character indices for the default tileset:
|
||||||
|
- @ (player): 64
|
||||||
|
- # (wall): 35
|
||||||
|
- . (floor): 46
|
||||||
|
- + (door): 43
|
||||||
|
- ~ (water): 126
|
||||||
|
- % (item): 37
|
||||||
|
- ! (potion): 33
|
||||||
|
|
||||||
|
## What's Next?
|
||||||
|
|
||||||
|
In Part 2, we'll expand our world with:
|
||||||
|
- A proper Entity system for managing multiple objects
|
||||||
|
- NPCs that can also move around
|
||||||
|
- A more interesting map layout
|
||||||
|
- The beginning of our game architecture
|
||||||
|
|
||||||
|
The foundation is set - you have a player character that can move around a world with collision detection. This is the core of any roguelike game!
|
|
@ -0,0 +1,562 @@
|
||||||
|
# Part 2 - The Generic Entity, the Render Functions, and the Map
|
||||||
|
|
||||||
|
In Part 1, we created a player character that could move around a simple room. Now it's time to build a proper architecture for our roguelike. We'll create a flexible entity system, a proper map structure, and organize our code for future expansion.
|
||||||
|
|
||||||
|
## Understanding Game Architecture
|
||||||
|
|
||||||
|
Before diving into code, let's understand the architecture we're building:
|
||||||
|
|
||||||
|
1. **Entities**: Anything that can exist in the game world (player, monsters, items)
|
||||||
|
2. **Game Map**: The dungeon structure with tiles that can be walls or floors
|
||||||
|
3. **Game Engine**: Coordinates everything - entities, map, input, and rendering
|
||||||
|
|
||||||
|
In McRogueFace, we'll adapt these concepts to work with the engine's scene-based architecture.
|
||||||
|
|
||||||
|
## Creating a Flexible Entity System
|
||||||
|
|
||||||
|
While McRogueFace provides a built-in `Entity` class, we'll create a wrapper to add game-specific functionality:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class GameObject:
|
||||||
|
"""Base class for all game objects (player, monsters, items)"""
|
||||||
|
|
||||||
|
def __init__(self, x, y, sprite_index, color, name, blocks=False):
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.sprite_index = sprite_index
|
||||||
|
self.color = color
|
||||||
|
self.name = name
|
||||||
|
self.blocks = blocks # Does this entity block movement?
|
||||||
|
self._entity = None # The McRogueFace entity
|
||||||
|
self.grid = None # Reference to the grid
|
||||||
|
|
||||||
|
def attach_to_grid(self, grid):
|
||||||
|
"""Attach this game object to a McRogueFace grid"""
|
||||||
|
self.grid = grid
|
||||||
|
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
||||||
|
self._entity.sprite_index = self.sprite_index
|
||||||
|
self._entity.color = self.color
|
||||||
|
|
||||||
|
def move(self, dx, dy):
|
||||||
|
"""Move by the given amount if possible"""
|
||||||
|
if not self.grid:
|
||||||
|
return
|
||||||
|
|
||||||
|
new_x = self.x + dx
|
||||||
|
new_y = self.y + dy
|
||||||
|
|
||||||
|
# Update our position
|
||||||
|
self.x = new_x
|
||||||
|
self.y = new_y
|
||||||
|
|
||||||
|
# Update the visual entity
|
||||||
|
if self._entity:
|
||||||
|
self._entity.x = new_x
|
||||||
|
self._entity.y = new_y
|
||||||
|
|
||||||
|
def destroy(self):
|
||||||
|
"""Remove this entity from the game"""
|
||||||
|
if self._entity and self.grid:
|
||||||
|
# Find and remove from grid's entity list
|
||||||
|
for i, entity in enumerate(self.grid.entities):
|
||||||
|
if entity == self._entity:
|
||||||
|
del self.grid.entities[i]
|
||||||
|
break
|
||||||
|
self._entity = None
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building the Game Map
|
||||||
|
|
||||||
|
Let's create a proper map class that manages our dungeon:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class GameMap:
|
||||||
|
"""Manages the game world"""
|
||||||
|
|
||||||
|
def __init__(self, width, height):
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.grid = None
|
||||||
|
self.entities = [] # List of GameObjects
|
||||||
|
|
||||||
|
def create_grid(self, tileset):
|
||||||
|
"""Create the McRogueFace grid"""
|
||||||
|
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
||||||
|
self.grid.position = (100, 100)
|
||||||
|
self.grid.size = (800, 480)
|
||||||
|
|
||||||
|
# Initialize all tiles as walls
|
||||||
|
self.fill_with_walls()
|
||||||
|
|
||||||
|
return self.grid
|
||||||
|
|
||||||
|
def fill_with_walls(self):
|
||||||
|
"""Fill the entire map with wall tiles"""
|
||||||
|
for y in range(self.height):
|
||||||
|
for x in range(self.width):
|
||||||
|
self.set_tile(x, y, walkable=False, transparent=False,
|
||||||
|
sprite_index=35, color=(100, 100, 100))
|
||||||
|
|
||||||
|
def set_tile(self, x, y, walkable, transparent, sprite_index, color):
|
||||||
|
"""Set properties for a specific tile"""
|
||||||
|
if 0 <= x < self.width and 0 <= y < self.height:
|
||||||
|
cell = self.grid.at(x, y)
|
||||||
|
cell.walkable = walkable
|
||||||
|
cell.transparent = transparent
|
||||||
|
cell.sprite_index = sprite_index
|
||||||
|
cell.color = mcrfpy.Color(*color)
|
||||||
|
|
||||||
|
def create_room(self, x1, y1, x2, y2):
|
||||||
|
"""Carve out a room in the map"""
|
||||||
|
# Make sure coordinates are in the right order
|
||||||
|
x1, x2 = min(x1, x2), max(x1, x2)
|
||||||
|
y1, y2 = min(y1, y2), max(y1, y2)
|
||||||
|
|
||||||
|
# Carve out floor tiles
|
||||||
|
for y in range(y1, y2 + 1):
|
||||||
|
for x in range(x1, x2 + 1):
|
||||||
|
self.set_tile(x, y, walkable=True, transparent=True,
|
||||||
|
sprite_index=46, color=(50, 50, 50))
|
||||||
|
|
||||||
|
def create_tunnel_h(self, x1, x2, y):
|
||||||
|
"""Create a horizontal tunnel"""
|
||||||
|
for x in range(min(x1, x2), max(x1, x2) + 1):
|
||||||
|
self.set_tile(x, y, walkable=True, transparent=True,
|
||||||
|
sprite_index=46, color=(50, 50, 50))
|
||||||
|
|
||||||
|
def create_tunnel_v(self, y1, y2, x):
|
||||||
|
"""Create a vertical tunnel"""
|
||||||
|
for y in range(min(y1, y2), max(y1, y2) + 1):
|
||||||
|
self.set_tile(x, y, walkable=True, transparent=True,
|
||||||
|
sprite_index=46, color=(50, 50, 50))
|
||||||
|
|
||||||
|
def is_blocked(self, x, y):
|
||||||
|
"""Check if a tile blocks movement"""
|
||||||
|
# Check map boundaries
|
||||||
|
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check if tile is walkable
|
||||||
|
if not self.grid.at(x, y).walkable:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check if any blocking entity is at this position
|
||||||
|
for entity in self.entities:
|
||||||
|
if entity.blocks and entity.x == x and entity.y == y:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def add_entity(self, entity):
|
||||||
|
"""Add a GameObject to the map"""
|
||||||
|
self.entities.append(entity)
|
||||||
|
entity.attach_to_grid(self.grid)
|
||||||
|
|
||||||
|
def get_blocking_entity_at(self, x, y):
|
||||||
|
"""Return any blocking entity at the given position"""
|
||||||
|
for entity in self.entities:
|
||||||
|
if entity.blocks and entity.x == x and entity.y == y:
|
||||||
|
return entity
|
||||||
|
return None
|
||||||
|
```
|
||||||
|
|
||||||
|
## Creating the Game Engine
|
||||||
|
|
||||||
|
Now let's build our game engine to tie everything together:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Engine:
|
||||||
|
"""Main game engine that manages game state"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.game_map = None
|
||||||
|
self.player = None
|
||||||
|
self.entities = []
|
||||||
|
|
||||||
|
# Create the game scene
|
||||||
|
mcrfpy.createScene("game")
|
||||||
|
mcrfpy.setScene("game")
|
||||||
|
|
||||||
|
# Configure window
|
||||||
|
window = mcrfpy.Window.get()
|
||||||
|
window.title = "McRogueFace Roguelike - Part 2"
|
||||||
|
|
||||||
|
# Get UI container
|
||||||
|
self.ui = mcrfpy.sceneUI("game")
|
||||||
|
|
||||||
|
# Add background
|
||||||
|
background = mcrfpy.Frame(0, 0, 1024, 768)
|
||||||
|
background.fill_color = mcrfpy.Color(0, 0, 0)
|
||||||
|
self.ui.append(background)
|
||||||
|
|
||||||
|
# Load tileset
|
||||||
|
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
||||||
|
|
||||||
|
# Create the game world
|
||||||
|
self.setup_game()
|
||||||
|
|
||||||
|
# Setup input handling
|
||||||
|
self.setup_input()
|
||||||
|
|
||||||
|
# Add UI elements
|
||||||
|
self.setup_ui()
|
||||||
|
|
||||||
|
def setup_game(self):
|
||||||
|
"""Initialize the game world"""
|
||||||
|
# Create the map
|
||||||
|
self.game_map = GameMap(50, 30)
|
||||||
|
grid = self.game_map.create_grid(self.tileset)
|
||||||
|
self.ui.append(grid)
|
||||||
|
|
||||||
|
# Create some rooms
|
||||||
|
self.game_map.create_room(10, 10, 20, 20)
|
||||||
|
self.game_map.create_room(30, 15, 40, 25)
|
||||||
|
self.game_map.create_room(15, 22, 25, 28)
|
||||||
|
|
||||||
|
# Connect rooms with tunnels
|
||||||
|
self.game_map.create_tunnel_h(20, 30, 15)
|
||||||
|
self.game_map.create_tunnel_v(20, 22, 20)
|
||||||
|
|
||||||
|
# Create player
|
||||||
|
self.player = GameObject(15, 15, 64, (255, 255, 255), "Player", blocks=True)
|
||||||
|
self.game_map.add_entity(self.player)
|
||||||
|
|
||||||
|
# Create an NPC
|
||||||
|
npc = GameObject(35, 20, 64, (255, 255, 0), "NPC", blocks=True)
|
||||||
|
self.game_map.add_entity(npc)
|
||||||
|
self.entities.append(npc)
|
||||||
|
|
||||||
|
# Create some items (non-blocking)
|
||||||
|
potion = GameObject(12, 12, 33, (255, 0, 255), "Potion", blocks=False)
|
||||||
|
self.game_map.add_entity(potion)
|
||||||
|
self.entities.append(potion)
|
||||||
|
|
||||||
|
def handle_movement(self, dx, dy):
|
||||||
|
"""Handle player movement"""
|
||||||
|
new_x = self.player.x + dx
|
||||||
|
new_y = self.player.y + dy
|
||||||
|
|
||||||
|
# Check if movement is blocked
|
||||||
|
if not self.game_map.is_blocked(new_x, new_y):
|
||||||
|
self.player.move(dx, dy)
|
||||||
|
else:
|
||||||
|
# Check if we bumped into an entity
|
||||||
|
target = self.game_map.get_blocking_entity_at(new_x, new_y)
|
||||||
|
if target:
|
||||||
|
print(f"You bump into the {target.name}!")
|
||||||
|
|
||||||
|
def setup_input(self):
|
||||||
|
"""Setup keyboard input handling"""
|
||||||
|
def handle_keys(key, state):
|
||||||
|
if state != "start":
|
||||||
|
return
|
||||||
|
|
||||||
|
# Movement keys
|
||||||
|
movement = {
|
||||||
|
"Up": (0, -1),
|
||||||
|
"Down": (0, 1),
|
||||||
|
"Left": (-1, 0),
|
||||||
|
"Right": (1, 0),
|
||||||
|
"Num7": (-1, -1),
|
||||||
|
"Num8": (0, -1),
|
||||||
|
"Num9": (1, -1),
|
||||||
|
"Num4": (-1, 0),
|
||||||
|
"Num6": (1, 0),
|
||||||
|
"Num1": (-1, 1),
|
||||||
|
"Num2": (0, 1),
|
||||||
|
"Num3": (1, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
if key in movement:
|
||||||
|
dx, dy = movement[key]
|
||||||
|
self.handle_movement(dx, dy)
|
||||||
|
elif key == "Escape":
|
||||||
|
mcrfpy.setScene(None)
|
||||||
|
|
||||||
|
mcrfpy.keypressScene(handle_keys)
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
"""Setup UI elements"""
|
||||||
|
# Title
|
||||||
|
title = mcrfpy.Caption("McRogueFace Roguelike - Part 2", 512, 30)
|
||||||
|
title.font_size = 24
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 100)
|
||||||
|
self.ui.append(title)
|
||||||
|
|
||||||
|
# Instructions
|
||||||
|
instructions = mcrfpy.Caption("Explore the dungeon! ESC to quit", 512, 60)
|
||||||
|
instructions.font_size = 16
|
||||||
|
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
|
self.ui.append(instructions)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Putting It All Together
|
||||||
|
|
||||||
|
Here's the complete `game.py` file:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import mcrfpy
|
||||||
|
|
||||||
|
class GameObject:
|
||||||
|
"""Base class for all game objects (player, monsters, items)"""
|
||||||
|
|
||||||
|
def __init__(self, x, y, sprite_index, color, name, blocks=False):
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.sprite_index = sprite_index
|
||||||
|
self.color = color
|
||||||
|
self.name = name
|
||||||
|
self.blocks = blocks
|
||||||
|
self._entity = None
|
||||||
|
self.grid = None
|
||||||
|
|
||||||
|
def attach_to_grid(self, grid):
|
||||||
|
"""Attach this game object to a McRogueFace grid"""
|
||||||
|
self.grid = grid
|
||||||
|
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
||||||
|
self._entity.sprite_index = self.sprite_index
|
||||||
|
self._entity.color = mcrfpy.Color(*self.color)
|
||||||
|
|
||||||
|
def move(self, dx, dy):
|
||||||
|
"""Move by the given amount if possible"""
|
||||||
|
if not self.grid:
|
||||||
|
return
|
||||||
|
|
||||||
|
new_x = self.x + dx
|
||||||
|
new_y = self.y + dy
|
||||||
|
|
||||||
|
self.x = new_x
|
||||||
|
self.y = new_y
|
||||||
|
|
||||||
|
if self._entity:
|
||||||
|
self._entity.x = new_x
|
||||||
|
self._entity.y = new_y
|
||||||
|
|
||||||
|
class GameMap:
|
||||||
|
"""Manages the game world"""
|
||||||
|
|
||||||
|
def __init__(self, width, height):
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.grid = None
|
||||||
|
self.entities = []
|
||||||
|
|
||||||
|
def create_grid(self, tileset):
|
||||||
|
"""Create the McRogueFace grid"""
|
||||||
|
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
||||||
|
self.grid.position = (100, 100)
|
||||||
|
self.grid.size = (800, 480)
|
||||||
|
self.fill_with_walls()
|
||||||
|
return self.grid
|
||||||
|
|
||||||
|
def fill_with_walls(self):
|
||||||
|
"""Fill the entire map with wall tiles"""
|
||||||
|
for y in range(self.height):
|
||||||
|
for x in range(self.width):
|
||||||
|
self.set_tile(x, y, walkable=False, transparent=False,
|
||||||
|
sprite_index=35, color=(100, 100, 100))
|
||||||
|
|
||||||
|
def set_tile(self, x, y, walkable, transparent, sprite_index, color):
|
||||||
|
"""Set properties for a specific tile"""
|
||||||
|
if 0 <= x < self.width and 0 <= y < self.height:
|
||||||
|
cell = self.grid.at(x, y)
|
||||||
|
cell.walkable = walkable
|
||||||
|
cell.transparent = transparent
|
||||||
|
cell.sprite_index = sprite_index
|
||||||
|
cell.color = mcrfpy.Color(*color)
|
||||||
|
|
||||||
|
def create_room(self, x1, y1, x2, y2):
|
||||||
|
"""Carve out a room in the map"""
|
||||||
|
x1, x2 = min(x1, x2), max(x1, x2)
|
||||||
|
y1, y2 = min(y1, y2), max(y1, y2)
|
||||||
|
|
||||||
|
for y in range(y1, y2 + 1):
|
||||||
|
for x in range(x1, x2 + 1):
|
||||||
|
self.set_tile(x, y, walkable=True, transparent=True,
|
||||||
|
sprite_index=46, color=(50, 50, 50))
|
||||||
|
|
||||||
|
def create_tunnel_h(self, x1, x2, y):
|
||||||
|
"""Create a horizontal tunnel"""
|
||||||
|
for x in range(min(x1, x2), max(x1, x2) + 1):
|
||||||
|
self.set_tile(x, y, walkable=True, transparent=True,
|
||||||
|
sprite_index=46, color=(50, 50, 50))
|
||||||
|
|
||||||
|
def create_tunnel_v(self, y1, y2, x):
|
||||||
|
"""Create a vertical tunnel"""
|
||||||
|
for y in range(min(y1, y2), max(y1, y2) + 1):
|
||||||
|
self.set_tile(x, y, walkable=True, transparent=True,
|
||||||
|
sprite_index=46, color=(50, 50, 50))
|
||||||
|
|
||||||
|
def is_blocked(self, x, y):
|
||||||
|
"""Check if a tile blocks movement"""
|
||||||
|
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if not self.grid.at(x, y).walkable:
|
||||||
|
return True
|
||||||
|
|
||||||
|
for entity in self.entities:
|
||||||
|
if entity.blocks and entity.x == x and entity.y == y:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def add_entity(self, entity):
|
||||||
|
"""Add a GameObject to the map"""
|
||||||
|
self.entities.append(entity)
|
||||||
|
entity.attach_to_grid(self.grid)
|
||||||
|
|
||||||
|
def get_blocking_entity_at(self, x, y):
|
||||||
|
"""Return any blocking entity at the given position"""
|
||||||
|
for entity in self.entities:
|
||||||
|
if entity.blocks and entity.x == x and entity.y == y:
|
||||||
|
return entity
|
||||||
|
return None
|
||||||
|
|
||||||
|
class Engine:
|
||||||
|
"""Main game engine that manages game state"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.game_map = None
|
||||||
|
self.player = None
|
||||||
|
self.entities = []
|
||||||
|
|
||||||
|
mcrfpy.createScene("game")
|
||||||
|
mcrfpy.setScene("game")
|
||||||
|
|
||||||
|
window = mcrfpy.Window.get()
|
||||||
|
window.title = "McRogueFace Roguelike - Part 2"
|
||||||
|
|
||||||
|
self.ui = mcrfpy.sceneUI("game")
|
||||||
|
|
||||||
|
background = mcrfpy.Frame(0, 0, 1024, 768)
|
||||||
|
background.fill_color = mcrfpy.Color(0, 0, 0)
|
||||||
|
self.ui.append(background)
|
||||||
|
|
||||||
|
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
||||||
|
|
||||||
|
self.setup_game()
|
||||||
|
self.setup_input()
|
||||||
|
self.setup_ui()
|
||||||
|
|
||||||
|
def setup_game(self):
|
||||||
|
"""Initialize the game world"""
|
||||||
|
self.game_map = GameMap(50, 30)
|
||||||
|
grid = self.game_map.create_grid(self.tileset)
|
||||||
|
self.ui.append(grid)
|
||||||
|
|
||||||
|
self.game_map.create_room(10, 10, 20, 20)
|
||||||
|
self.game_map.create_room(30, 15, 40, 25)
|
||||||
|
self.game_map.create_room(15, 22, 25, 28)
|
||||||
|
|
||||||
|
self.game_map.create_tunnel_h(20, 30, 15)
|
||||||
|
self.game_map.create_tunnel_v(20, 22, 20)
|
||||||
|
|
||||||
|
self.player = GameObject(15, 15, 64, (255, 255, 255), "Player", blocks=True)
|
||||||
|
self.game_map.add_entity(self.player)
|
||||||
|
|
||||||
|
npc = GameObject(35, 20, 64, (255, 255, 0), "NPC", blocks=True)
|
||||||
|
self.game_map.add_entity(npc)
|
||||||
|
self.entities.append(npc)
|
||||||
|
|
||||||
|
potion = GameObject(12, 12, 33, (255, 0, 255), "Potion", blocks=False)
|
||||||
|
self.game_map.add_entity(potion)
|
||||||
|
self.entities.append(potion)
|
||||||
|
|
||||||
|
def handle_movement(self, dx, dy):
|
||||||
|
"""Handle player movement"""
|
||||||
|
new_x = self.player.x + dx
|
||||||
|
new_y = self.player.y + dy
|
||||||
|
|
||||||
|
if not self.game_map.is_blocked(new_x, new_y):
|
||||||
|
self.player.move(dx, dy)
|
||||||
|
else:
|
||||||
|
target = self.game_map.get_blocking_entity_at(new_x, new_y)
|
||||||
|
if target:
|
||||||
|
print(f"You bump into the {target.name}!")
|
||||||
|
|
||||||
|
def setup_input(self):
|
||||||
|
"""Setup keyboard input handling"""
|
||||||
|
def handle_keys(key, state):
|
||||||
|
if state != "start":
|
||||||
|
return
|
||||||
|
|
||||||
|
movement = {
|
||||||
|
"Up": (0, -1), "Down": (0, 1),
|
||||||
|
"Left": (-1, 0), "Right": (1, 0),
|
||||||
|
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
|
||||||
|
"Num4": (-1, 0), "Num6": (1, 0),
|
||||||
|
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
if key in movement:
|
||||||
|
dx, dy = movement[key]
|
||||||
|
self.handle_movement(dx, dy)
|
||||||
|
elif key == "Escape":
|
||||||
|
mcrfpy.setScene(None)
|
||||||
|
|
||||||
|
mcrfpy.keypressScene(handle_keys)
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
"""Setup UI elements"""
|
||||||
|
title = mcrfpy.Caption("McRogueFace Roguelike - Part 2", 512, 30)
|
||||||
|
title.font_size = 24
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 100)
|
||||||
|
self.ui.append(title)
|
||||||
|
|
||||||
|
instructions = mcrfpy.Caption("Explore the dungeon! ESC to quit", 512, 60)
|
||||||
|
instructions.font_size = 16
|
||||||
|
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
|
self.ui.append(instructions)
|
||||||
|
|
||||||
|
# Create and run the game
|
||||||
|
engine = Engine()
|
||||||
|
print("Part 2: Entities and Maps!")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Understanding the Architecture
|
||||||
|
|
||||||
|
### GameObject Class
|
||||||
|
Our `GameObject` class wraps McRogueFace's `Entity` and adds:
|
||||||
|
- Game logic properties (name, blocking)
|
||||||
|
- Position tracking independent of the visual entity
|
||||||
|
- Easy attachment/detachment from grids
|
||||||
|
|
||||||
|
### GameMap Class
|
||||||
|
The `GameMap` manages:
|
||||||
|
- The McRogueFace `Grid` for visual representation
|
||||||
|
- A list of all entities in the map
|
||||||
|
- Collision detection including entity blocking
|
||||||
|
- Map generation utilities (rooms, tunnels)
|
||||||
|
|
||||||
|
### Engine Class
|
||||||
|
The `Engine` coordinates everything:
|
||||||
|
- Scene and UI setup
|
||||||
|
- Game state management
|
||||||
|
- Input handling
|
||||||
|
- Entity-map interactions
|
||||||
|
|
||||||
|
## Key Improvements from Part 1
|
||||||
|
|
||||||
|
1. **Proper Entity Management**: Multiple entities can exist and interact
|
||||||
|
2. **Blocking Entities**: Some entities block movement, others don't
|
||||||
|
3. **Map Generation**: Tools for creating rooms and tunnels
|
||||||
|
4. **Collision System**: Checks both tiles and entities
|
||||||
|
5. **Organized Code**: Clear separation of concerns
|
||||||
|
|
||||||
|
## Exercises
|
||||||
|
|
||||||
|
1. **Add More Entity Types**: Create different sprites for monsters, items, and NPCs
|
||||||
|
2. **Entity Interactions**: Make items disappear when walked over
|
||||||
|
3. **Random Map Generation**: Place rooms and tunnels randomly
|
||||||
|
4. **Entity Properties**: Add health, damage, or other attributes to GameObjects
|
||||||
|
|
||||||
|
## What's Next?
|
||||||
|
|
||||||
|
In Part 3, we'll implement proper dungeon generation with:
|
||||||
|
- Procedurally generated rooms
|
||||||
|
- Smart tunnel routing
|
||||||
|
- Entity spawning
|
||||||
|
- The beginning of a real roguelike dungeon!
|
||||||
|
|
||||||
|
We now have a solid foundation with proper entity management and map structure. This architecture will serve us well as we add more complex features to our roguelike!
|
|
@ -0,0 +1,548 @@
|
||||||
|
# Part 3 - Generating a Dungeon
|
||||||
|
|
||||||
|
In Parts 1 and 2, we created a player that could move around and interact with a hand-crafted dungeon. Now it's time to generate dungeons procedurally - a core feature of any roguelike game!
|
||||||
|
|
||||||
|
## The Plan
|
||||||
|
|
||||||
|
We'll create a dungeon generator that:
|
||||||
|
1. Places rectangular rooms randomly
|
||||||
|
2. Ensures rooms don't overlap
|
||||||
|
3. Connects rooms with tunnels
|
||||||
|
4. Places the player in the first room
|
||||||
|
|
||||||
|
This is a classic approach used by many roguelikes, and it creates interesting, playable dungeons.
|
||||||
|
|
||||||
|
## Creating a Room Class
|
||||||
|
|
||||||
|
First, let's create a class to represent rectangular rooms:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class RectangularRoom:
|
||||||
|
"""A rectangular room with its position and size"""
|
||||||
|
|
||||||
|
def __init__(self, x, y, width, height):
|
||||||
|
self.x1 = x
|
||||||
|
self.y1 = y
|
||||||
|
self.x2 = x + width
|
||||||
|
self.y2 = y + height
|
||||||
|
|
||||||
|
@property
|
||||||
|
def center(self):
|
||||||
|
"""Return the center coordinates of the room"""
|
||||||
|
center_x = (self.x1 + self.x2) // 2
|
||||||
|
center_y = (self.y1 + self.y2) // 2
|
||||||
|
return center_x, center_y
|
||||||
|
|
||||||
|
@property
|
||||||
|
def inner(self):
|
||||||
|
"""Return the inner area of the room as a tuple of slices
|
||||||
|
|
||||||
|
This property returns the area inside the walls.
|
||||||
|
We'll add 1 to min coordinates and subtract 1 from max coordinates.
|
||||||
|
"""
|
||||||
|
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
|
||||||
|
|
||||||
|
def intersects(self, other):
|
||||||
|
"""Return True if this room overlaps with another RectangularRoom"""
|
||||||
|
return (
|
||||||
|
self.x1 <= other.x2
|
||||||
|
and self.x2 >= other.x1
|
||||||
|
and self.y1 <= other.y2
|
||||||
|
and self.y2 >= other.y1
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementing Tunnel Generation
|
||||||
|
|
||||||
|
Since McRogueFace doesn't include line-drawing algorithms, let's implement simple L-shaped tunnels:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def tunnel_between(start, end):
|
||||||
|
"""Return an L-shaped tunnel between two points"""
|
||||||
|
x1, y1 = start
|
||||||
|
x2, y2 = end
|
||||||
|
|
||||||
|
# Randomly decide whether to go horizontal first or vertical first
|
||||||
|
if random.random() < 0.5:
|
||||||
|
# Horizontal, then vertical
|
||||||
|
corner_x = x2
|
||||||
|
corner_y = y1
|
||||||
|
else:
|
||||||
|
# Vertical, then horizontal
|
||||||
|
corner_x = x1
|
||||||
|
corner_y = y2
|
||||||
|
|
||||||
|
# Generate the coordinates
|
||||||
|
# First line: from start to corner
|
||||||
|
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
|
||||||
|
yield x, y1
|
||||||
|
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
|
||||||
|
yield corner_x, y
|
||||||
|
|
||||||
|
# Second line: from corner to end
|
||||||
|
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
|
||||||
|
yield x, corner_y
|
||||||
|
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
|
||||||
|
yield x2, y
|
||||||
|
```
|
||||||
|
|
||||||
|
## The Dungeon Generator
|
||||||
|
|
||||||
|
Now let's update our GameMap class to generate dungeons:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import random
|
||||||
|
|
||||||
|
class GameMap:
|
||||||
|
"""Manages the game world"""
|
||||||
|
|
||||||
|
def __init__(self, width, height):
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.grid = None
|
||||||
|
self.entities = []
|
||||||
|
self.rooms = [] # Keep track of rooms for game logic
|
||||||
|
|
||||||
|
def generate_dungeon(
|
||||||
|
self,
|
||||||
|
max_rooms,
|
||||||
|
room_min_size,
|
||||||
|
room_max_size,
|
||||||
|
player
|
||||||
|
):
|
||||||
|
"""Generate a new dungeon map"""
|
||||||
|
# Start with everything as walls
|
||||||
|
self.fill_with_walls()
|
||||||
|
|
||||||
|
for r in range(max_rooms):
|
||||||
|
# Random width and height
|
||||||
|
room_width = random.randint(room_min_size, room_max_size)
|
||||||
|
room_height = random.randint(room_min_size, room_max_size)
|
||||||
|
|
||||||
|
# Random position without going out of bounds
|
||||||
|
x = random.randint(0, self.width - room_width - 1)
|
||||||
|
y = random.randint(0, self.height - room_height - 1)
|
||||||
|
|
||||||
|
# Create the room
|
||||||
|
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||||
|
|
||||||
|
# Check if it intersects with any existing room
|
||||||
|
if any(new_room.intersects(other_room) for other_room in self.rooms):
|
||||||
|
continue # This room intersects, so go to the next attempt
|
||||||
|
|
||||||
|
# If we get here, it's a valid room
|
||||||
|
|
||||||
|
# Carve out this room
|
||||||
|
self.carve_room(new_room)
|
||||||
|
|
||||||
|
# Place the player in the center of the first room
|
||||||
|
if len(self.rooms) == 0:
|
||||||
|
player.x, player.y = new_room.center
|
||||||
|
if player._entity:
|
||||||
|
player._entity.x, player._entity.y = new_room.center
|
||||||
|
else:
|
||||||
|
# All rooms after the first:
|
||||||
|
# Tunnel between this room and the previous one
|
||||||
|
self.carve_tunnel(self.rooms[-1].center, new_room.center)
|
||||||
|
|
||||||
|
# Finally, append the new room to the list
|
||||||
|
self.rooms.append(new_room)
|
||||||
|
|
||||||
|
def carve_room(self, room):
|
||||||
|
"""Carve out a room"""
|
||||||
|
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
|
||||||
|
|
||||||
|
for y in range(inner_y1, inner_y2):
|
||||||
|
for x in range(inner_x1, inner_x2):
|
||||||
|
self.set_tile(x, y, walkable=True, transparent=True,
|
||||||
|
sprite_index=46, color=(50, 50, 50))
|
||||||
|
|
||||||
|
def carve_tunnel(self, start, end):
|
||||||
|
"""Carve a tunnel between two points"""
|
||||||
|
for x, y in tunnel_between(start, end):
|
||||||
|
self.set_tile(x, y, walkable=True, transparent=True,
|
||||||
|
sprite_index=46, color=(30, 30, 40)) # Slightly different color for tunnels
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complete Code
|
||||||
|
|
||||||
|
Here's the complete `game.py` with procedural dungeon generation:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import mcrfpy
|
||||||
|
import random
|
||||||
|
|
||||||
|
class GameObject:
|
||||||
|
"""Base class for all game objects"""
|
||||||
|
def __init__(self, x, y, sprite_index, color, name, blocks=False):
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.sprite_index = sprite_index
|
||||||
|
self.color = color
|
||||||
|
self.name = name
|
||||||
|
self.blocks = blocks
|
||||||
|
self._entity = None
|
||||||
|
self.grid = None
|
||||||
|
|
||||||
|
def attach_to_grid(self, grid):
|
||||||
|
"""Attach this game object to a McRogueFace grid"""
|
||||||
|
self.grid = grid
|
||||||
|
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
||||||
|
self._entity.sprite_index = self.sprite_index
|
||||||
|
self._entity.color = mcrfpy.Color(*self.color)
|
||||||
|
|
||||||
|
def move(self, dx, dy):
|
||||||
|
"""Move by the given amount"""
|
||||||
|
if not self.grid:
|
||||||
|
return
|
||||||
|
self.x += dx
|
||||||
|
self.y += dy
|
||||||
|
if self._entity:
|
||||||
|
self._entity.x = self.x
|
||||||
|
self._entity.y = self.y
|
||||||
|
|
||||||
|
class RectangularRoom:
|
||||||
|
"""A rectangular room with its position and size"""
|
||||||
|
|
||||||
|
def __init__(self, x, y, width, height):
|
||||||
|
self.x1 = x
|
||||||
|
self.y1 = y
|
||||||
|
self.x2 = x + width
|
||||||
|
self.y2 = y + height
|
||||||
|
|
||||||
|
@property
|
||||||
|
def center(self):
|
||||||
|
"""Return the center coordinates of the room"""
|
||||||
|
center_x = (self.x1 + self.x2) // 2
|
||||||
|
center_y = (self.y1 + self.y2) // 2
|
||||||
|
return center_x, center_y
|
||||||
|
|
||||||
|
@property
|
||||||
|
def inner(self):
|
||||||
|
"""Return the inner area of the room"""
|
||||||
|
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
|
||||||
|
|
||||||
|
def intersects(self, other):
|
||||||
|
"""Return True if this room overlaps with another"""
|
||||||
|
return (
|
||||||
|
self.x1 <= other.x2
|
||||||
|
and self.x2 >= other.x1
|
||||||
|
and self.y1 <= other.y2
|
||||||
|
and self.y2 >= other.y1
|
||||||
|
)
|
||||||
|
|
||||||
|
def tunnel_between(start, end):
|
||||||
|
"""Return an L-shaped tunnel between two points"""
|
||||||
|
x1, y1 = start
|
||||||
|
x2, y2 = end
|
||||||
|
|
||||||
|
if random.random() < 0.5:
|
||||||
|
corner_x = x2
|
||||||
|
corner_y = y1
|
||||||
|
else:
|
||||||
|
corner_x = x1
|
||||||
|
corner_y = y2
|
||||||
|
|
||||||
|
# Generate the coordinates
|
||||||
|
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
|
||||||
|
yield x, y1
|
||||||
|
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
|
||||||
|
yield corner_x, y
|
||||||
|
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
|
||||||
|
yield x, corner_y
|
||||||
|
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
|
||||||
|
yield x2, y
|
||||||
|
|
||||||
|
class GameMap:
|
||||||
|
"""Manages the game world"""
|
||||||
|
|
||||||
|
def __init__(self, width, height):
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.grid = None
|
||||||
|
self.entities = []
|
||||||
|
self.rooms = []
|
||||||
|
|
||||||
|
def create_grid(self, tileset):
|
||||||
|
"""Create the McRogueFace grid"""
|
||||||
|
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
||||||
|
self.grid.position = (100, 100)
|
||||||
|
self.grid.size = (800, 480)
|
||||||
|
return self.grid
|
||||||
|
|
||||||
|
def fill_with_walls(self):
|
||||||
|
"""Fill the entire map with wall tiles"""
|
||||||
|
for y in range(self.height):
|
||||||
|
for x in range(self.width):
|
||||||
|
self.set_tile(x, y, walkable=False, transparent=False,
|
||||||
|
sprite_index=35, color=(100, 100, 100))
|
||||||
|
|
||||||
|
def set_tile(self, x, y, walkable, transparent, sprite_index, color):
|
||||||
|
"""Set properties for a specific tile"""
|
||||||
|
if 0 <= x < self.width and 0 <= y < self.height:
|
||||||
|
cell = self.grid.at(x, y)
|
||||||
|
cell.walkable = walkable
|
||||||
|
cell.transparent = transparent
|
||||||
|
cell.sprite_index = sprite_index
|
||||||
|
cell.color = mcrfpy.Color(*color)
|
||||||
|
|
||||||
|
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player):
|
||||||
|
"""Generate a new dungeon map"""
|
||||||
|
self.fill_with_walls()
|
||||||
|
|
||||||
|
for r in range(max_rooms):
|
||||||
|
room_width = random.randint(room_min_size, room_max_size)
|
||||||
|
room_height = random.randint(room_min_size, room_max_size)
|
||||||
|
|
||||||
|
x = random.randint(0, self.width - room_width - 1)
|
||||||
|
y = random.randint(0, self.height - room_height - 1)
|
||||||
|
|
||||||
|
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||||
|
|
||||||
|
if any(new_room.intersects(other_room) for other_room in self.rooms):
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.carve_room(new_room)
|
||||||
|
|
||||||
|
if len(self.rooms) == 0:
|
||||||
|
player.x, player.y = new_room.center
|
||||||
|
if player._entity:
|
||||||
|
player._entity.x, player._entity.y = new_room.center
|
||||||
|
else:
|
||||||
|
self.carve_tunnel(self.rooms[-1].center, new_room.center)
|
||||||
|
|
||||||
|
self.rooms.append(new_room)
|
||||||
|
|
||||||
|
def carve_room(self, room):
|
||||||
|
"""Carve out a room"""
|
||||||
|
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
|
||||||
|
|
||||||
|
for y in range(inner_y1, inner_y2):
|
||||||
|
for x in range(inner_x1, inner_x2):
|
||||||
|
self.set_tile(x, y, walkable=True, transparent=True,
|
||||||
|
sprite_index=46, color=(50, 50, 50))
|
||||||
|
|
||||||
|
def carve_tunnel(self, start, end):
|
||||||
|
"""Carve a tunnel between two points"""
|
||||||
|
for x, y in tunnel_between(start, end):
|
||||||
|
self.set_tile(x, y, walkable=True, transparent=True,
|
||||||
|
sprite_index=46, color=(30, 30, 40))
|
||||||
|
|
||||||
|
def is_blocked(self, x, y):
|
||||||
|
"""Check if a tile blocks movement"""
|
||||||
|
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
||||||
|
return True
|
||||||
|
if not self.grid.at(x, y).walkable:
|
||||||
|
return True
|
||||||
|
for entity in self.entities:
|
||||||
|
if entity.blocks and entity.x == x and entity.y == y:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def add_entity(self, entity):
|
||||||
|
"""Add a GameObject to the map"""
|
||||||
|
self.entities.append(entity)
|
||||||
|
entity.attach_to_grid(self.grid)
|
||||||
|
|
||||||
|
class Engine:
|
||||||
|
"""Main game engine"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.game_map = None
|
||||||
|
self.player = None
|
||||||
|
self.entities = []
|
||||||
|
|
||||||
|
mcrfpy.createScene("game")
|
||||||
|
mcrfpy.setScene("game")
|
||||||
|
|
||||||
|
window = mcrfpy.Window.get()
|
||||||
|
window.title = "McRogueFace Roguelike - Part 3"
|
||||||
|
|
||||||
|
self.ui = mcrfpy.sceneUI("game")
|
||||||
|
|
||||||
|
background = mcrfpy.Frame(0, 0, 1024, 768)
|
||||||
|
background.fill_color = mcrfpy.Color(0, 0, 0)
|
||||||
|
self.ui.append(background)
|
||||||
|
|
||||||
|
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
||||||
|
|
||||||
|
self.setup_game()
|
||||||
|
self.setup_input()
|
||||||
|
self.setup_ui()
|
||||||
|
|
||||||
|
def setup_game(self):
|
||||||
|
"""Initialize the game world"""
|
||||||
|
self.game_map = GameMap(80, 45)
|
||||||
|
grid = self.game_map.create_grid(self.tileset)
|
||||||
|
self.ui.append(grid)
|
||||||
|
|
||||||
|
# Create player (before dungeon generation)
|
||||||
|
self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True)
|
||||||
|
|
||||||
|
# Generate the dungeon
|
||||||
|
self.game_map.generate_dungeon(
|
||||||
|
max_rooms=30,
|
||||||
|
room_min_size=6,
|
||||||
|
room_max_size=10,
|
||||||
|
player=self.player
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add player to map
|
||||||
|
self.game_map.add_entity(self.player)
|
||||||
|
|
||||||
|
# Add some monsters in random rooms
|
||||||
|
for i in range(5):
|
||||||
|
if i < len(self.game_map.rooms) - 1: # Don't spawn in first room
|
||||||
|
room = self.game_map.rooms[i + 1]
|
||||||
|
x, y = room.center
|
||||||
|
|
||||||
|
# Create an orc
|
||||||
|
orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
|
||||||
|
self.game_map.add_entity(orc)
|
||||||
|
self.entities.append(orc)
|
||||||
|
|
||||||
|
def handle_movement(self, dx, dy):
|
||||||
|
"""Handle player movement"""
|
||||||
|
new_x = self.player.x + dx
|
||||||
|
new_y = self.player.y + dy
|
||||||
|
|
||||||
|
if not self.game_map.is_blocked(new_x, new_y):
|
||||||
|
self.player.move(dx, dy)
|
||||||
|
|
||||||
|
def setup_input(self):
|
||||||
|
"""Setup keyboard input handling"""
|
||||||
|
def handle_keys(key, state):
|
||||||
|
if state != "start":
|
||||||
|
return
|
||||||
|
|
||||||
|
movement = {
|
||||||
|
"Up": (0, -1), "Down": (0, 1),
|
||||||
|
"Left": (-1, 0), "Right": (1, 0),
|
||||||
|
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
|
||||||
|
"Num4": (-1, 0), "Num6": (1, 0),
|
||||||
|
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
if key in movement:
|
||||||
|
dx, dy = movement[key]
|
||||||
|
self.handle_movement(dx, dy)
|
||||||
|
elif key == "Escape":
|
||||||
|
mcrfpy.setScene(None)
|
||||||
|
elif key == "Space":
|
||||||
|
# Regenerate the dungeon
|
||||||
|
self.regenerate_dungeon()
|
||||||
|
|
||||||
|
mcrfpy.keypressScene(handle_keys)
|
||||||
|
|
||||||
|
def regenerate_dungeon(self):
|
||||||
|
"""Generate a new dungeon"""
|
||||||
|
# Clear existing entities
|
||||||
|
self.game_map.entities.clear()
|
||||||
|
self.game_map.rooms.clear()
|
||||||
|
self.entities.clear()
|
||||||
|
|
||||||
|
# Clear the entity list in the grid
|
||||||
|
if self.game_map.grid:
|
||||||
|
self.game_map.grid.entities.clear()
|
||||||
|
|
||||||
|
# Regenerate
|
||||||
|
self.game_map.generate_dungeon(
|
||||||
|
max_rooms=30,
|
||||||
|
room_min_size=6,
|
||||||
|
room_max_size=10,
|
||||||
|
player=self.player
|
||||||
|
)
|
||||||
|
|
||||||
|
# Re-add player
|
||||||
|
self.game_map.add_entity(self.player)
|
||||||
|
|
||||||
|
# Add new monsters
|
||||||
|
for i in range(5):
|
||||||
|
if i < len(self.game_map.rooms) - 1:
|
||||||
|
room = self.game_map.rooms[i + 1]
|
||||||
|
x, y = room.center
|
||||||
|
orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
|
||||||
|
self.game_map.add_entity(orc)
|
||||||
|
self.entities.append(orc)
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
"""Setup UI elements"""
|
||||||
|
title = mcrfpy.Caption("Procedural Dungeon Generation", 512, 30)
|
||||||
|
title.font_size = 24
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 100)
|
||||||
|
self.ui.append(title)
|
||||||
|
|
||||||
|
instructions = mcrfpy.Caption("Arrow keys to move, SPACE to regenerate, ESC to quit", 512, 60)
|
||||||
|
instructions.font_size = 16
|
||||||
|
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
|
self.ui.append(instructions)
|
||||||
|
|
||||||
|
# Create and run the game
|
||||||
|
engine = Engine()
|
||||||
|
print("Part 3: Procedural Dungeon Generation!")
|
||||||
|
print("Press SPACE to generate a new dungeon")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Understanding the Algorithm
|
||||||
|
|
||||||
|
Our dungeon generation algorithm is simple but effective:
|
||||||
|
|
||||||
|
1. **Start with solid walls** - The entire map begins filled with wall tiles
|
||||||
|
2. **Try to place rooms** - Generate random rooms and check for overlaps
|
||||||
|
3. **Connect with tunnels** - Each new room connects to the previous one
|
||||||
|
4. **Place entities** - The player starts in the first room, monsters in others
|
||||||
|
|
||||||
|
### Room Placement
|
||||||
|
|
||||||
|
The algorithm attempts to place `max_rooms` rooms, but may place fewer if many attempts result in overlapping rooms. This is called "rejection sampling" - we generate random rooms and reject ones that don't fit.
|
||||||
|
|
||||||
|
### Tunnel Design
|
||||||
|
|
||||||
|
Our L-shaped tunnels are simple but effective. They either go:
|
||||||
|
- Horizontal first, then vertical
|
||||||
|
- Vertical first, then horizontal
|
||||||
|
|
||||||
|
This creates variety while ensuring all rooms are connected.
|
||||||
|
|
||||||
|
## Experimenting with Parameters
|
||||||
|
|
||||||
|
Try adjusting these parameters to create different dungeon styles:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Sparse dungeon with large rooms
|
||||||
|
self.game_map.generate_dungeon(
|
||||||
|
max_rooms=10,
|
||||||
|
room_min_size=10,
|
||||||
|
room_max_size=15,
|
||||||
|
player=self.player
|
||||||
|
)
|
||||||
|
|
||||||
|
# Dense dungeon with small rooms
|
||||||
|
self.game_map.generate_dungeon(
|
||||||
|
max_rooms=50,
|
||||||
|
room_min_size=4,
|
||||||
|
room_max_size=6,
|
||||||
|
player=self.player
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Visual Enhancements
|
||||||
|
|
||||||
|
Notice how we gave tunnels a slightly different color:
|
||||||
|
- Rooms: `color=(50, 50, 50)` - Medium gray
|
||||||
|
- Tunnels: `color=(30, 30, 40)` - Darker with blue tint
|
||||||
|
|
||||||
|
This subtle difference helps players understand the dungeon layout.
|
||||||
|
|
||||||
|
## Exercises
|
||||||
|
|
||||||
|
1. **Different Room Shapes**: Create circular or cross-shaped rooms
|
||||||
|
2. **Better Tunnel Routing**: Implement A* pathfinding for more natural tunnels
|
||||||
|
3. **Room Types**: Create special rooms (treasure rooms, trap rooms)
|
||||||
|
4. **Dungeon Themes**: Use different tile sets and colors for different dungeon levels
|
||||||
|
|
||||||
|
## What's Next?
|
||||||
|
|
||||||
|
In Part 4, we'll implement Field of View (FOV) so the player can only see parts of the dungeon they've explored. This will add mystery and atmosphere to our procedurally generated dungeons!
|
||||||
|
|
||||||
|
Our dungeon generator is now creating unique, playable levels every time. The foundation of a true roguelike is taking shape!
|
|
@ -0,0 +1,520 @@
|
||||||
|
# Part 4 - Field of View
|
||||||
|
|
||||||
|
One of the defining features of roguelikes is exploration and discovery. In Part 3, we could see the entire dungeon at once. Now we'll implement Field of View (FOV) so players can only see what their character can actually see, adding mystery and tactical depth to our game.
|
||||||
|
|
||||||
|
## Understanding Field of View
|
||||||
|
|
||||||
|
Field of View creates three distinct visibility states for each tile:
|
||||||
|
|
||||||
|
1. **Visible**: Currently in the player's line of sight
|
||||||
|
2. **Explored**: Previously seen but not currently visible
|
||||||
|
3. **Unexplored**: Never seen (completely hidden)
|
||||||
|
|
||||||
|
This creates the classic "fog of war" effect where you remember the layout of areas you've explored, but can't see current enemy positions unless they're in your view.
|
||||||
|
|
||||||
|
## McRogueFace's FOV System
|
||||||
|
|
||||||
|
Good news! McRogueFace includes built-in FOV support through its C++ engine. We just need to enable and configure it. The engine uses an efficient shadowcasting algorithm that provides smooth, realistic line-of-sight calculations.
|
||||||
|
|
||||||
|
Let's update our code to use FOV:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class GameObject:
|
||||||
|
"""Base class for all game objects"""
|
||||||
|
def __init__(self, x, y, sprite_index, color, name, blocks=False):
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.sprite_index = sprite_index
|
||||||
|
self.color = color
|
||||||
|
self.name = name
|
||||||
|
self.blocks = blocks
|
||||||
|
self._entity = None
|
||||||
|
self.grid = None
|
||||||
|
|
||||||
|
def attach_to_grid(self, grid):
|
||||||
|
"""Attach this game object to a McRogueFace grid"""
|
||||||
|
self.grid = grid
|
||||||
|
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
||||||
|
self._entity.sprite_index = self.sprite_index
|
||||||
|
self._entity.color = mcrfpy.Color(*self.color)
|
||||||
|
|
||||||
|
def move(self, dx, dy):
|
||||||
|
"""Move by the given amount"""
|
||||||
|
if not self.grid:
|
||||||
|
return
|
||||||
|
self.x += dx
|
||||||
|
self.y += dy
|
||||||
|
if self._entity:
|
||||||
|
self._entity.x = self.x
|
||||||
|
self._entity.y = self.y
|
||||||
|
# Update FOV when player moves
|
||||||
|
if self.name == "Player":
|
||||||
|
self.update_fov()
|
||||||
|
|
||||||
|
def update_fov(self):
|
||||||
|
"""Update field of view from this entity's position"""
|
||||||
|
if self._entity and self.grid:
|
||||||
|
self._entity.update_fov(radius=8)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuring Visibility Rendering
|
||||||
|
|
||||||
|
McRogueFace automatically handles the rendering of visible/explored/unexplored tiles. We need to set up our grid to use perspective-based rendering:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class GameMap:
|
||||||
|
"""Manages the game world"""
|
||||||
|
|
||||||
|
def create_grid(self, tileset):
|
||||||
|
"""Create the McRogueFace grid"""
|
||||||
|
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
||||||
|
self.grid.position = (100, 100)
|
||||||
|
self.grid.size = (800, 480)
|
||||||
|
|
||||||
|
# Enable perspective rendering (0 = first entity = player)
|
||||||
|
self.grid.perspective = 0
|
||||||
|
|
||||||
|
return self.grid
|
||||||
|
```
|
||||||
|
|
||||||
|
## Visual Appearance Configuration
|
||||||
|
|
||||||
|
Let's define how our tiles look in different visibility states:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Color configurations for visibility states
|
||||||
|
COLORS_VISIBLE = {
|
||||||
|
'wall': (100, 100, 100), # Light gray
|
||||||
|
'floor': (50, 50, 50), # Dark gray
|
||||||
|
'tunnel': (30, 30, 40), # Dark blue-gray
|
||||||
|
}
|
||||||
|
|
||||||
|
COLORS_EXPLORED = {
|
||||||
|
'wall': (50, 50, 70), # Darker, bluish
|
||||||
|
'floor': (20, 20, 30), # Very dark
|
||||||
|
'tunnel': (15, 15, 25), # Almost black
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update the tile-setting methods to store the tile type
|
||||||
|
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
|
||||||
|
"""Set properties for a specific tile"""
|
||||||
|
if 0 <= x < self.width and 0 <= y < self.height:
|
||||||
|
cell = self.grid.at(x, y)
|
||||||
|
cell.walkable = walkable
|
||||||
|
cell.transparent = transparent
|
||||||
|
cell.sprite_index = sprite_index
|
||||||
|
# Store both visible and explored colors
|
||||||
|
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
|
||||||
|
# The engine will automatically darken explored tiles
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complete Implementation
|
||||||
|
|
||||||
|
Here's the complete updated `game.py` with FOV:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import mcrfpy
|
||||||
|
import random
|
||||||
|
|
||||||
|
# Color configurations for visibility
|
||||||
|
COLORS_VISIBLE = {
|
||||||
|
'wall': (100, 100, 100),
|
||||||
|
'floor': (50, 50, 50),
|
||||||
|
'tunnel': (30, 30, 40),
|
||||||
|
}
|
||||||
|
|
||||||
|
class GameObject:
|
||||||
|
"""Base class for all game objects"""
|
||||||
|
def __init__(self, x, y, sprite_index, color, name, blocks=False):
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.sprite_index = sprite_index
|
||||||
|
self.color = color
|
||||||
|
self.name = name
|
||||||
|
self.blocks = blocks
|
||||||
|
self._entity = None
|
||||||
|
self.grid = None
|
||||||
|
|
||||||
|
def attach_to_grid(self, grid):
|
||||||
|
"""Attach this game object to a McRogueFace grid"""
|
||||||
|
self.grid = grid
|
||||||
|
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
||||||
|
self._entity.sprite_index = self.sprite_index
|
||||||
|
self._entity.color = mcrfpy.Color(*self.color)
|
||||||
|
|
||||||
|
def move(self, dx, dy):
|
||||||
|
"""Move by the given amount"""
|
||||||
|
if not self.grid:
|
||||||
|
return
|
||||||
|
self.x += dx
|
||||||
|
self.y += dy
|
||||||
|
if self._entity:
|
||||||
|
self._entity.x = self.x
|
||||||
|
self._entity.y = self.y
|
||||||
|
# Update FOV when player moves
|
||||||
|
if self.name == "Player":
|
||||||
|
self.update_fov()
|
||||||
|
|
||||||
|
def update_fov(self):
|
||||||
|
"""Update field of view from this entity's position"""
|
||||||
|
if self._entity and self.grid:
|
||||||
|
self._entity.update_fov(radius=8)
|
||||||
|
|
||||||
|
class RectangularRoom:
|
||||||
|
"""A rectangular room with its position and size"""
|
||||||
|
|
||||||
|
def __init__(self, x, y, width, height):
|
||||||
|
self.x1 = x
|
||||||
|
self.y1 = y
|
||||||
|
self.x2 = x + width
|
||||||
|
self.y2 = y + height
|
||||||
|
|
||||||
|
@property
|
||||||
|
def center(self):
|
||||||
|
center_x = (self.x1 + self.x2) // 2
|
||||||
|
center_y = (self.y1 + self.y2) // 2
|
||||||
|
return center_x, center_y
|
||||||
|
|
||||||
|
@property
|
||||||
|
def inner(self):
|
||||||
|
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
|
||||||
|
|
||||||
|
def intersects(self, other):
|
||||||
|
return (
|
||||||
|
self.x1 <= other.x2
|
||||||
|
and self.x2 >= other.x1
|
||||||
|
and self.y1 <= other.y2
|
||||||
|
and self.y2 >= other.y1
|
||||||
|
)
|
||||||
|
|
||||||
|
def tunnel_between(start, end):
|
||||||
|
"""Return an L-shaped tunnel between two points"""
|
||||||
|
x1, y1 = start
|
||||||
|
x2, y2 = end
|
||||||
|
|
||||||
|
if random.random() < 0.5:
|
||||||
|
corner_x = x2
|
||||||
|
corner_y = y1
|
||||||
|
else:
|
||||||
|
corner_x = x1
|
||||||
|
corner_y = y2
|
||||||
|
|
||||||
|
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
|
||||||
|
yield x, y1
|
||||||
|
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
|
||||||
|
yield corner_x, y
|
||||||
|
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
|
||||||
|
yield x, corner_y
|
||||||
|
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
|
||||||
|
yield x2, y
|
||||||
|
|
||||||
|
class GameMap:
|
||||||
|
"""Manages the game world"""
|
||||||
|
|
||||||
|
def __init__(self, width, height):
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.grid = None
|
||||||
|
self.entities = []
|
||||||
|
self.rooms = []
|
||||||
|
|
||||||
|
def create_grid(self, tileset):
|
||||||
|
"""Create the McRogueFace grid"""
|
||||||
|
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
||||||
|
self.grid.position = (100, 100)
|
||||||
|
self.grid.size = (800, 480)
|
||||||
|
|
||||||
|
# Enable perspective rendering (0 = first entity = player)
|
||||||
|
self.grid.perspective = 0
|
||||||
|
|
||||||
|
return self.grid
|
||||||
|
|
||||||
|
def fill_with_walls(self):
|
||||||
|
"""Fill the entire map with wall tiles"""
|
||||||
|
for y in range(self.height):
|
||||||
|
for x in range(self.width):
|
||||||
|
self.set_tile(x, y, walkable=False, transparent=False,
|
||||||
|
sprite_index=35, tile_type='wall')
|
||||||
|
|
||||||
|
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
|
||||||
|
"""Set properties for a specific tile"""
|
||||||
|
if 0 <= x < self.width and 0 <= y < self.height:
|
||||||
|
cell = self.grid.at(x, y)
|
||||||
|
cell.walkable = walkable
|
||||||
|
cell.transparent = transparent
|
||||||
|
cell.sprite_index = sprite_index
|
||||||
|
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
|
||||||
|
|
||||||
|
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player):
|
||||||
|
"""Generate a new dungeon map"""
|
||||||
|
self.fill_with_walls()
|
||||||
|
|
||||||
|
for r in range(max_rooms):
|
||||||
|
room_width = random.randint(room_min_size, room_max_size)
|
||||||
|
room_height = random.randint(room_min_size, room_max_size)
|
||||||
|
|
||||||
|
x = random.randint(0, self.width - room_width - 1)
|
||||||
|
y = random.randint(0, self.height - room_height - 1)
|
||||||
|
|
||||||
|
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||||
|
|
||||||
|
if any(new_room.intersects(other_room) for other_room in self.rooms):
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.carve_room(new_room)
|
||||||
|
|
||||||
|
if len(self.rooms) == 0:
|
||||||
|
player.x, player.y = new_room.center
|
||||||
|
if player._entity:
|
||||||
|
player._entity.x, player._entity.y = new_room.center
|
||||||
|
else:
|
||||||
|
self.carve_tunnel(self.rooms[-1].center, new_room.center)
|
||||||
|
|
||||||
|
self.rooms.append(new_room)
|
||||||
|
|
||||||
|
def carve_room(self, room):
|
||||||
|
"""Carve out a room"""
|
||||||
|
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
|
||||||
|
|
||||||
|
for y in range(inner_y1, inner_y2):
|
||||||
|
for x in range(inner_x1, inner_x2):
|
||||||
|
self.set_tile(x, y, walkable=True, transparent=True,
|
||||||
|
sprite_index=46, tile_type='floor')
|
||||||
|
|
||||||
|
def carve_tunnel(self, start, end):
|
||||||
|
"""Carve a tunnel between two points"""
|
||||||
|
for x, y in tunnel_between(start, end):
|
||||||
|
self.set_tile(x, y, walkable=True, transparent=True,
|
||||||
|
sprite_index=46, tile_type='tunnel')
|
||||||
|
|
||||||
|
def is_blocked(self, x, y):
|
||||||
|
"""Check if a tile blocks movement"""
|
||||||
|
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
||||||
|
return True
|
||||||
|
if not self.grid.at(x, y).walkable:
|
||||||
|
return True
|
||||||
|
for entity in self.entities:
|
||||||
|
if entity.blocks and entity.x == x and entity.y == y:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def add_entity(self, entity):
|
||||||
|
"""Add a GameObject to the map"""
|
||||||
|
self.entities.append(entity)
|
||||||
|
entity.attach_to_grid(self.grid)
|
||||||
|
|
||||||
|
class Engine:
|
||||||
|
"""Main game engine"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.game_map = None
|
||||||
|
self.player = None
|
||||||
|
self.entities = []
|
||||||
|
self.fov_radius = 8
|
||||||
|
|
||||||
|
mcrfpy.createScene("game")
|
||||||
|
mcrfpy.setScene("game")
|
||||||
|
|
||||||
|
window = mcrfpy.Window.get()
|
||||||
|
window.title = "McRogueFace Roguelike - Part 4"
|
||||||
|
|
||||||
|
self.ui = mcrfpy.sceneUI("game")
|
||||||
|
|
||||||
|
background = mcrfpy.Frame(0, 0, 1024, 768)
|
||||||
|
background.fill_color = mcrfpy.Color(0, 0, 0)
|
||||||
|
self.ui.append(background)
|
||||||
|
|
||||||
|
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
||||||
|
|
||||||
|
self.setup_game()
|
||||||
|
self.setup_input()
|
||||||
|
self.setup_ui()
|
||||||
|
|
||||||
|
def setup_game(self):
|
||||||
|
"""Initialize the game world"""
|
||||||
|
self.game_map = GameMap(80, 45)
|
||||||
|
grid = self.game_map.create_grid(self.tileset)
|
||||||
|
self.ui.append(grid)
|
||||||
|
|
||||||
|
# Create player
|
||||||
|
self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True)
|
||||||
|
|
||||||
|
# Generate the dungeon
|
||||||
|
self.game_map.generate_dungeon(
|
||||||
|
max_rooms=30,
|
||||||
|
room_min_size=6,
|
||||||
|
room_max_size=10,
|
||||||
|
player=self.player
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add player to map
|
||||||
|
self.game_map.add_entity(self.player)
|
||||||
|
|
||||||
|
# Add monsters in random rooms
|
||||||
|
for i in range(10):
|
||||||
|
if i < len(self.game_map.rooms) - 1:
|
||||||
|
room = self.game_map.rooms[i + 1]
|
||||||
|
x, y = room.center
|
||||||
|
|
||||||
|
# Randomly offset from center
|
||||||
|
x += random.randint(-2, 2)
|
||||||
|
y += random.randint(-2, 2)
|
||||||
|
|
||||||
|
# Make sure position is walkable
|
||||||
|
if self.game_map.grid.at(x, y).walkable:
|
||||||
|
if i % 2 == 0:
|
||||||
|
# Create an orc
|
||||||
|
orc = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
|
||||||
|
self.game_map.add_entity(orc)
|
||||||
|
self.entities.append(orc)
|
||||||
|
else:
|
||||||
|
# Create a troll
|
||||||
|
troll = GameObject(x, y, 84, (0, 127, 0), "Troll", blocks=True)
|
||||||
|
self.game_map.add_entity(troll)
|
||||||
|
self.entities.append(troll)
|
||||||
|
|
||||||
|
# Initial FOV calculation
|
||||||
|
self.player.update_fov()
|
||||||
|
|
||||||
|
def handle_movement(self, dx, dy):
|
||||||
|
"""Handle player movement"""
|
||||||
|
new_x = self.player.x + dx
|
||||||
|
new_y = self.player.y + dy
|
||||||
|
|
||||||
|
if not self.game_map.is_blocked(new_x, new_y):
|
||||||
|
self.player.move(dx, dy)
|
||||||
|
|
||||||
|
def setup_input(self):
|
||||||
|
"""Setup keyboard input handling"""
|
||||||
|
def handle_keys(key, state):
|
||||||
|
if state != "start":
|
||||||
|
return
|
||||||
|
|
||||||
|
movement = {
|
||||||
|
"Up": (0, -1), "Down": (0, 1),
|
||||||
|
"Left": (-1, 0), "Right": (1, 0),
|
||||||
|
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
|
||||||
|
"Num4": (-1, 0), "Num6": (1, 0),
|
||||||
|
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
if key in movement:
|
||||||
|
dx, dy = movement[key]
|
||||||
|
self.handle_movement(dx, dy)
|
||||||
|
elif key == "Escape":
|
||||||
|
mcrfpy.setScene(None)
|
||||||
|
elif key == "v":
|
||||||
|
# Toggle FOV on/off
|
||||||
|
if self.game_map.grid.perspective == 0:
|
||||||
|
self.game_map.grid.perspective = -1 # Omniscient
|
||||||
|
print("FOV disabled - omniscient view")
|
||||||
|
else:
|
||||||
|
self.game_map.grid.perspective = 0 # Player perspective
|
||||||
|
print("FOV enabled - player perspective")
|
||||||
|
elif key == "Plus" or key == "Equals":
|
||||||
|
# Increase FOV radius
|
||||||
|
self.fov_radius = min(self.fov_radius + 1, 20)
|
||||||
|
self.player._entity.update_fov(radius=self.fov_radius)
|
||||||
|
print(f"FOV radius: {self.fov_radius}")
|
||||||
|
elif key == "Minus":
|
||||||
|
# Decrease FOV radius
|
||||||
|
self.fov_radius = max(self.fov_radius - 1, 3)
|
||||||
|
self.player._entity.update_fov(radius=self.fov_radius)
|
||||||
|
print(f"FOV radius: {self.fov_radius}")
|
||||||
|
|
||||||
|
mcrfpy.keypressScene(handle_keys)
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
"""Setup UI elements"""
|
||||||
|
title = mcrfpy.Caption("Field of View", 512, 30)
|
||||||
|
title.font_size = 24
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 100)
|
||||||
|
self.ui.append(title)
|
||||||
|
|
||||||
|
instructions = mcrfpy.Caption("Arrow keys to move | V to toggle FOV | +/- to adjust radius | ESC to quit", 512, 60)
|
||||||
|
instructions.font_size = 16
|
||||||
|
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
|
self.ui.append(instructions)
|
||||||
|
|
||||||
|
# FOV indicator
|
||||||
|
self.fov_text = mcrfpy.Caption(f"FOV Radius: {self.fov_radius}", 900, 100)
|
||||||
|
self.fov_text.font_size = 14
|
||||||
|
self.fov_text.fill_color = mcrfpy.Color(150, 200, 255)
|
||||||
|
self.ui.append(self.fov_text)
|
||||||
|
|
||||||
|
# Create and run the game
|
||||||
|
engine = Engine()
|
||||||
|
print("Part 4: Field of View!")
|
||||||
|
print("Press V to toggle FOV on/off")
|
||||||
|
print("Press +/- to adjust FOV radius")
|
||||||
|
```
|
||||||
|
|
||||||
|
## How FOV Works
|
||||||
|
|
||||||
|
McRogueFace's built-in FOV system uses a shadowcasting algorithm that:
|
||||||
|
|
||||||
|
1. **Casts rays** from the player's position to tiles within the radius
|
||||||
|
2. **Checks transparency** along each ray path
|
||||||
|
3. **Marks tiles as visible** if the ray reaches them unobstructed
|
||||||
|
4. **Remembers explored tiles** automatically
|
||||||
|
|
||||||
|
The engine handles all the complex calculations in C++ for optimal performance.
|
||||||
|
|
||||||
|
## Visibility States in Detail
|
||||||
|
|
||||||
|
### Visible Tiles
|
||||||
|
- Currently in the player's line of sight
|
||||||
|
- Rendered at full brightness
|
||||||
|
- Show current entity positions
|
||||||
|
|
||||||
|
### Explored Tiles
|
||||||
|
- Previously seen but not currently visible
|
||||||
|
- Rendered darker/muted
|
||||||
|
- Show remembered terrain but not entities
|
||||||
|
|
||||||
|
### Unexplored Tiles
|
||||||
|
- Never been in the player's FOV
|
||||||
|
- Rendered as black/invisible
|
||||||
|
- Complete mystery to the player
|
||||||
|
|
||||||
|
## FOV Parameters
|
||||||
|
|
||||||
|
You can customize FOV behavior:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Basic FOV update
|
||||||
|
entity.update_fov(radius=8)
|
||||||
|
|
||||||
|
# The grid's perspective property controls rendering:
|
||||||
|
grid.perspective = 0 # Use first entity's FOV (player)
|
||||||
|
grid.perspective = 1 # Use second entity's FOV
|
||||||
|
grid.perspective = -1 # Omniscient (no FOV, see everything)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
McRogueFace's C++ FOV implementation is highly optimized:
|
||||||
|
- Uses efficient shadowcasting algorithm
|
||||||
|
- Only recalculates when needed
|
||||||
|
- Handles large maps smoothly
|
||||||
|
- Automatically culls entities outside FOV
|
||||||
|
|
||||||
|
## Visual Polish
|
||||||
|
|
||||||
|
The engine automatically handles visual transitions:
|
||||||
|
- Smooth color changes between visibility states
|
||||||
|
- Entities fade in/out of view
|
||||||
|
- Explored areas remain visible but dimmed
|
||||||
|
|
||||||
|
## Exercises
|
||||||
|
|
||||||
|
1. **Variable Vision**: Give different entities different FOV radii
|
||||||
|
2. **Light Sources**: Create torches that expand local FOV
|
||||||
|
3. **Blind Spots**: Add pillars that create interesting shadows
|
||||||
|
4. **X-Ray Vision**: Temporary power-up to see through walls
|
||||||
|
|
||||||
|
## What's Next?
|
||||||
|
|
||||||
|
In Part 5, we'll place enemies throughout the dungeon and implement basic interactions. With FOV in place, enemies will appear and disappear as you explore, creating tension and surprise!
|
||||||
|
|
||||||
|
Field of View transforms our dungeon from a tactical puzzle into a mysterious world to explore. The fog of war adds atmosphere and gameplay depth that's essential to the roguelike experience.
|
|
@ -0,0 +1,570 @@
|
||||||
|
# Part 5 - Placing Enemies and Kicking Them (Harmlessly)
|
||||||
|
|
||||||
|
Now that we have Field of View working, it's time to populate our dungeon with enemies! In this part, we'll:
|
||||||
|
- Place enemies randomly in rooms
|
||||||
|
- Implement entity-to-entity collision detection
|
||||||
|
- Create basic interactions (bumping into enemies)
|
||||||
|
- Set the stage for combat in Part 6
|
||||||
|
|
||||||
|
## Enemy Spawning System
|
||||||
|
|
||||||
|
First, let's create a system to spawn enemies in our dungeon rooms. We'll avoid placing them in the first room (where the player starts) to give players a safe starting area.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def spawn_enemies_in_room(room, game_map, max_enemies=2):
|
||||||
|
"""Spawn between 0 and max_enemies in a room"""
|
||||||
|
import random
|
||||||
|
|
||||||
|
number_of_enemies = random.randint(0, max_enemies)
|
||||||
|
|
||||||
|
for i in range(number_of_enemies):
|
||||||
|
# Try to find a valid position
|
||||||
|
attempts = 10
|
||||||
|
while attempts > 0:
|
||||||
|
# Random position within room bounds
|
||||||
|
x = random.randint(room.x1 + 1, room.x2 - 1)
|
||||||
|
y = random.randint(room.y1 + 1, room.y2 - 1)
|
||||||
|
|
||||||
|
# Check if position is valid
|
||||||
|
if not game_map.is_blocked(x, y):
|
||||||
|
# 80% chance for orc, 20% for troll
|
||||||
|
if random.random() < 0.8:
|
||||||
|
enemy = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
|
||||||
|
else:
|
||||||
|
enemy = GameObject(x, y, 84, (0, 127, 0), "Troll", blocks=True)
|
||||||
|
|
||||||
|
game_map.add_entity(enemy)
|
||||||
|
break
|
||||||
|
|
||||||
|
attempts -= 1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Enhanced Collision Detection
|
||||||
|
|
||||||
|
We need to improve our collision detection to check for entities, not just walls:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class GameMap:
|
||||||
|
"""Manages the game world"""
|
||||||
|
|
||||||
|
def get_blocking_entity_at(self, x, y):
|
||||||
|
"""Return any blocking entity at the given position"""
|
||||||
|
for entity in self.entities:
|
||||||
|
if entity.blocks and entity.x == x and entity.y == y:
|
||||||
|
return entity
|
||||||
|
return None
|
||||||
|
|
||||||
|
def is_blocked(self, x, y):
|
||||||
|
"""Check if a tile blocks movement"""
|
||||||
|
# Check boundaries
|
||||||
|
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check walls
|
||||||
|
if not self.grid.at(x, y).walkable:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check entities
|
||||||
|
if self.get_blocking_entity_at(x, y):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
```
|
||||||
|
|
||||||
|
## Action System Introduction
|
||||||
|
|
||||||
|
Let's create a simple action system to handle different types of interactions:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Action:
|
||||||
|
"""Base class for all actions"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class MovementAction(Action):
|
||||||
|
"""Action for moving an entity"""
|
||||||
|
def __init__(self, dx, dy):
|
||||||
|
self.dx = dx
|
||||||
|
self.dy = dy
|
||||||
|
|
||||||
|
class BumpAction(Action):
|
||||||
|
"""Action for bumping into something"""
|
||||||
|
def __init__(self, dx, dy, target=None):
|
||||||
|
self.dx = dx
|
||||||
|
self.dy = dy
|
||||||
|
self.target = target
|
||||||
|
|
||||||
|
class WaitAction(Action):
|
||||||
|
"""Action for waiting/skipping turn"""
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
## Handling Player Actions
|
||||||
|
|
||||||
|
Now let's update our movement handling to support bumping into enemies:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def handle_player_turn(self, action):
|
||||||
|
"""Process the player's action"""
|
||||||
|
if isinstance(action, MovementAction):
|
||||||
|
dest_x = self.player.x + action.dx
|
||||||
|
dest_y = self.player.y + action.dy
|
||||||
|
|
||||||
|
# Check what's at the destination
|
||||||
|
target = self.game_map.get_blocking_entity_at(dest_x, dest_y)
|
||||||
|
|
||||||
|
if target:
|
||||||
|
# We bumped into something!
|
||||||
|
print(f"You kick the {target.name} in the shins, much to its annoyance!")
|
||||||
|
elif not self.game_map.is_blocked(dest_x, dest_y):
|
||||||
|
# Move the player
|
||||||
|
self.player.move(action.dx, action.dy)
|
||||||
|
# Update message
|
||||||
|
self.status_text.text = "Exploring the dungeon..."
|
||||||
|
else:
|
||||||
|
# Bumped into a wall
|
||||||
|
self.status_text.text = "Ouch! You bump into a wall."
|
||||||
|
|
||||||
|
elif isinstance(action, WaitAction):
|
||||||
|
self.status_text.text = "You wait..."
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complete Updated Code
|
||||||
|
|
||||||
|
Here's the complete `game.py` with enemy placement and interactions:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import mcrfpy
|
||||||
|
import random
|
||||||
|
|
||||||
|
# Color configurations
|
||||||
|
COLORS_VISIBLE = {
|
||||||
|
'wall': (100, 100, 100),
|
||||||
|
'floor': (50, 50, 50),
|
||||||
|
'tunnel': (30, 30, 40),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Actions
|
||||||
|
class Action:
|
||||||
|
"""Base class for all actions"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class MovementAction(Action):
|
||||||
|
"""Action for moving an entity"""
|
||||||
|
def __init__(self, dx, dy):
|
||||||
|
self.dx = dx
|
||||||
|
self.dy = dy
|
||||||
|
|
||||||
|
class WaitAction(Action):
|
||||||
|
"""Action for waiting/skipping turn"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class GameObject:
|
||||||
|
"""Base class for all game objects"""
|
||||||
|
def __init__(self, x, y, sprite_index, color, name, blocks=False):
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.sprite_index = sprite_index
|
||||||
|
self.color = color
|
||||||
|
self.name = name
|
||||||
|
self.blocks = blocks
|
||||||
|
self._entity = None
|
||||||
|
self.grid = None
|
||||||
|
|
||||||
|
def attach_to_grid(self, grid):
|
||||||
|
"""Attach this game object to a McRogueFace grid"""
|
||||||
|
self.grid = grid
|
||||||
|
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
||||||
|
self._entity.sprite_index = self.sprite_index
|
||||||
|
self._entity.color = mcrfpy.Color(*self.color)
|
||||||
|
|
||||||
|
def move(self, dx, dy):
|
||||||
|
"""Move by the given amount"""
|
||||||
|
if not self.grid:
|
||||||
|
return
|
||||||
|
self.x += dx
|
||||||
|
self.y += dy
|
||||||
|
if self._entity:
|
||||||
|
self._entity.x = self.x
|
||||||
|
self._entity.y = self.y
|
||||||
|
# Update FOV when player moves
|
||||||
|
if self.name == "Player":
|
||||||
|
self.update_fov()
|
||||||
|
|
||||||
|
def update_fov(self):
|
||||||
|
"""Update field of view from this entity's position"""
|
||||||
|
if self._entity and self.grid:
|
||||||
|
self._entity.update_fov(radius=8)
|
||||||
|
|
||||||
|
class RectangularRoom:
|
||||||
|
"""A rectangular room with its position and size"""
|
||||||
|
|
||||||
|
def __init__(self, x, y, width, height):
|
||||||
|
self.x1 = x
|
||||||
|
self.y1 = y
|
||||||
|
self.x2 = x + width
|
||||||
|
self.y2 = y + height
|
||||||
|
|
||||||
|
@property
|
||||||
|
def center(self):
|
||||||
|
center_x = (self.x1 + self.x2) // 2
|
||||||
|
center_y = (self.y1 + self.y2) // 2
|
||||||
|
return center_x, center_y
|
||||||
|
|
||||||
|
@property
|
||||||
|
def inner(self):
|
||||||
|
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
|
||||||
|
|
||||||
|
def intersects(self, other):
|
||||||
|
return (
|
||||||
|
self.x1 <= other.x2
|
||||||
|
and self.x2 >= other.x1
|
||||||
|
and self.y1 <= other.y2
|
||||||
|
and self.y2 >= other.y1
|
||||||
|
)
|
||||||
|
|
||||||
|
def tunnel_between(start, end):
|
||||||
|
"""Return an L-shaped tunnel between two points"""
|
||||||
|
x1, y1 = start
|
||||||
|
x2, y2 = end
|
||||||
|
|
||||||
|
if random.random() < 0.5:
|
||||||
|
corner_x = x2
|
||||||
|
corner_y = y1
|
||||||
|
else:
|
||||||
|
corner_x = x1
|
||||||
|
corner_y = y2
|
||||||
|
|
||||||
|
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
|
||||||
|
yield x, y1
|
||||||
|
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
|
||||||
|
yield corner_x, y
|
||||||
|
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
|
||||||
|
yield x, corner_y
|
||||||
|
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
|
||||||
|
yield x2, y
|
||||||
|
|
||||||
|
def spawn_enemies_in_room(room, game_map, max_enemies=2):
|
||||||
|
"""Spawn between 0 and max_enemies in a room"""
|
||||||
|
number_of_enemies = random.randint(0, max_enemies)
|
||||||
|
|
||||||
|
enemies_spawned = []
|
||||||
|
|
||||||
|
for i in range(number_of_enemies):
|
||||||
|
# Try to find a valid position
|
||||||
|
attempts = 10
|
||||||
|
while attempts > 0:
|
||||||
|
# Random position within room bounds
|
||||||
|
x = random.randint(room.x1 + 1, room.x2 - 1)
|
||||||
|
y = random.randint(room.y1 + 1, room.y2 - 1)
|
||||||
|
|
||||||
|
# Check if position is valid
|
||||||
|
if not game_map.is_blocked(x, y):
|
||||||
|
# 80% chance for orc, 20% for troll
|
||||||
|
if random.random() < 0.8:
|
||||||
|
enemy = GameObject(x, y, 111, (63, 127, 63), "Orc", blocks=True)
|
||||||
|
else:
|
||||||
|
enemy = GameObject(x, y, 84, (0, 127, 0), "Troll", blocks=True)
|
||||||
|
|
||||||
|
game_map.add_entity(enemy)
|
||||||
|
enemies_spawned.append(enemy)
|
||||||
|
break
|
||||||
|
|
||||||
|
attempts -= 1
|
||||||
|
|
||||||
|
return enemies_spawned
|
||||||
|
|
||||||
|
class GameMap:
|
||||||
|
"""Manages the game world"""
|
||||||
|
|
||||||
|
def __init__(self, width, height):
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.grid = None
|
||||||
|
self.entities = []
|
||||||
|
self.rooms = []
|
||||||
|
|
||||||
|
def create_grid(self, tileset):
|
||||||
|
"""Create the McRogueFace grid"""
|
||||||
|
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
||||||
|
self.grid.position = (100, 100)
|
||||||
|
self.grid.size = (800, 480)
|
||||||
|
|
||||||
|
# Enable perspective rendering
|
||||||
|
self.grid.perspective = 0
|
||||||
|
|
||||||
|
return self.grid
|
||||||
|
|
||||||
|
def fill_with_walls(self):
|
||||||
|
"""Fill the entire map with wall tiles"""
|
||||||
|
for y in range(self.height):
|
||||||
|
for x in range(self.width):
|
||||||
|
self.set_tile(x, y, walkable=False, transparent=False,
|
||||||
|
sprite_index=35, tile_type='wall')
|
||||||
|
|
||||||
|
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
|
||||||
|
"""Set properties for a specific tile"""
|
||||||
|
if 0 <= x < self.width and 0 <= y < self.height:
|
||||||
|
cell = self.grid.at(x, y)
|
||||||
|
cell.walkable = walkable
|
||||||
|
cell.transparent = transparent
|
||||||
|
cell.sprite_index = sprite_index
|
||||||
|
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
|
||||||
|
|
||||||
|
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player, max_enemies_per_room):
|
||||||
|
"""Generate a new dungeon map"""
|
||||||
|
self.fill_with_walls()
|
||||||
|
|
||||||
|
for r in range(max_rooms):
|
||||||
|
room_width = random.randint(room_min_size, room_max_size)
|
||||||
|
room_height = random.randint(room_min_size, room_max_size)
|
||||||
|
|
||||||
|
x = random.randint(0, self.width - room_width - 1)
|
||||||
|
y = random.randint(0, self.height - room_height - 1)
|
||||||
|
|
||||||
|
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||||
|
|
||||||
|
if any(new_room.intersects(other_room) for other_room in self.rooms):
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.carve_room(new_room)
|
||||||
|
|
||||||
|
if len(self.rooms) == 0:
|
||||||
|
# First room - place player
|
||||||
|
player.x, player.y = new_room.center
|
||||||
|
if player._entity:
|
||||||
|
player._entity.x, player._entity.y = new_room.center
|
||||||
|
else:
|
||||||
|
# All other rooms - add tunnel and enemies
|
||||||
|
self.carve_tunnel(self.rooms[-1].center, new_room.center)
|
||||||
|
spawn_enemies_in_room(new_room, self, max_enemies_per_room)
|
||||||
|
|
||||||
|
self.rooms.append(new_room)
|
||||||
|
|
||||||
|
def carve_room(self, room):
|
||||||
|
"""Carve out a room"""
|
||||||
|
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
|
||||||
|
|
||||||
|
for y in range(inner_y1, inner_y2):
|
||||||
|
for x in range(inner_x1, inner_x2):
|
||||||
|
self.set_tile(x, y, walkable=True, transparent=True,
|
||||||
|
sprite_index=46, tile_type='floor')
|
||||||
|
|
||||||
|
def carve_tunnel(self, start, end):
|
||||||
|
"""Carve a tunnel between two points"""
|
||||||
|
for x, y in tunnel_between(start, end):
|
||||||
|
self.set_tile(x, y, walkable=True, transparent=True,
|
||||||
|
sprite_index=46, tile_type='tunnel')
|
||||||
|
|
||||||
|
def get_blocking_entity_at(self, x, y):
|
||||||
|
"""Return any blocking entity at the given position"""
|
||||||
|
for entity in self.entities:
|
||||||
|
if entity.blocks and entity.x == x and entity.y == y:
|
||||||
|
return entity
|
||||||
|
return None
|
||||||
|
|
||||||
|
def is_blocked(self, x, y):
|
||||||
|
"""Check if a tile blocks movement"""
|
||||||
|
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if not self.grid.at(x, y).walkable:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if self.get_blocking_entity_at(x, y):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def add_entity(self, entity):
|
||||||
|
"""Add a GameObject to the map"""
|
||||||
|
self.entities.append(entity)
|
||||||
|
entity.attach_to_grid(self.grid)
|
||||||
|
|
||||||
|
class Engine:
|
||||||
|
"""Main game engine"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.game_map = None
|
||||||
|
self.player = None
|
||||||
|
self.entities = []
|
||||||
|
|
||||||
|
mcrfpy.createScene("game")
|
||||||
|
mcrfpy.setScene("game")
|
||||||
|
|
||||||
|
window = mcrfpy.Window.get()
|
||||||
|
window.title = "McRogueFace Roguelike - Part 5"
|
||||||
|
|
||||||
|
self.ui = mcrfpy.sceneUI("game")
|
||||||
|
|
||||||
|
background = mcrfpy.Frame(0, 0, 1024, 768)
|
||||||
|
background.fill_color = mcrfpy.Color(0, 0, 0)
|
||||||
|
self.ui.append(background)
|
||||||
|
|
||||||
|
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
||||||
|
|
||||||
|
self.setup_game()
|
||||||
|
self.setup_input()
|
||||||
|
self.setup_ui()
|
||||||
|
|
||||||
|
def setup_game(self):
|
||||||
|
"""Initialize the game world"""
|
||||||
|
self.game_map = GameMap(80, 45)
|
||||||
|
grid = self.game_map.create_grid(self.tileset)
|
||||||
|
self.ui.append(grid)
|
||||||
|
|
||||||
|
# Create player
|
||||||
|
self.player = GameObject(0, 0, 64, (255, 255, 255), "Player", blocks=True)
|
||||||
|
|
||||||
|
# Generate the dungeon
|
||||||
|
self.game_map.generate_dungeon(
|
||||||
|
max_rooms=30,
|
||||||
|
room_min_size=6,
|
||||||
|
room_max_size=10,
|
||||||
|
player=self.player,
|
||||||
|
max_enemies_per_room=2
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add player to map
|
||||||
|
self.game_map.add_entity(self.player)
|
||||||
|
|
||||||
|
# Store reference to all entities
|
||||||
|
self.entities = [e for e in self.game_map.entities if e != self.player]
|
||||||
|
|
||||||
|
# Initial FOV calculation
|
||||||
|
self.player.update_fov()
|
||||||
|
|
||||||
|
def handle_player_turn(self, action):
|
||||||
|
"""Process the player's action"""
|
||||||
|
if isinstance(action, MovementAction):
|
||||||
|
dest_x = self.player.x + action.dx
|
||||||
|
dest_y = self.player.y + action.dy
|
||||||
|
|
||||||
|
# Check what's at the destination
|
||||||
|
target = self.game_map.get_blocking_entity_at(dest_x, dest_y)
|
||||||
|
|
||||||
|
if target:
|
||||||
|
# We bumped into something!
|
||||||
|
print(f"You kick the {target.name} in the shins, much to its annoyance!")
|
||||||
|
self.status_text.text = f"You kick the {target.name}!"
|
||||||
|
elif not self.game_map.is_blocked(dest_x, dest_y):
|
||||||
|
# Move the player
|
||||||
|
self.player.move(action.dx, action.dy)
|
||||||
|
self.status_text.text = ""
|
||||||
|
else:
|
||||||
|
# Bumped into a wall
|
||||||
|
self.status_text.text = "Blocked!"
|
||||||
|
|
||||||
|
elif isinstance(action, WaitAction):
|
||||||
|
self.status_text.text = "You wait..."
|
||||||
|
|
||||||
|
def setup_input(self):
|
||||||
|
"""Setup keyboard input handling"""
|
||||||
|
def handle_keys(key, state):
|
||||||
|
if state != "start":
|
||||||
|
return
|
||||||
|
|
||||||
|
action = None
|
||||||
|
|
||||||
|
# Movement keys
|
||||||
|
movement = {
|
||||||
|
"Up": (0, -1), "Down": (0, 1),
|
||||||
|
"Left": (-1, 0), "Right": (1, 0),
|
||||||
|
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
|
||||||
|
"Num4": (-1, 0), "Num5": (0, 0), "Num6": (1, 0),
|
||||||
|
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
if key in movement:
|
||||||
|
dx, dy = movement[key]
|
||||||
|
if dx == 0 and dy == 0:
|
||||||
|
action = WaitAction()
|
||||||
|
else:
|
||||||
|
action = MovementAction(dx, dy)
|
||||||
|
elif key == "Period":
|
||||||
|
action = WaitAction()
|
||||||
|
elif key == "Escape":
|
||||||
|
mcrfpy.setScene(None)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Process the action
|
||||||
|
if action:
|
||||||
|
self.handle_player_turn(action)
|
||||||
|
|
||||||
|
mcrfpy.keypressScene(handle_keys)
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
"""Setup UI elements"""
|
||||||
|
title = mcrfpy.Caption("Placing Enemies", 512, 30)
|
||||||
|
title.font_size = 24
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 100)
|
||||||
|
self.ui.append(title)
|
||||||
|
|
||||||
|
instructions = mcrfpy.Caption("Arrow keys to move | . to wait | Bump into enemies! | ESC to quit", 512, 60)
|
||||||
|
instructions.font_size = 16
|
||||||
|
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
|
self.ui.append(instructions)
|
||||||
|
|
||||||
|
# Status text
|
||||||
|
self.status_text = mcrfpy.Caption("", 512, 600)
|
||||||
|
self.status_text.font_size = 18
|
||||||
|
self.status_text.fill_color = mcrfpy.Color(255, 200, 200)
|
||||||
|
self.ui.append(self.status_text)
|
||||||
|
|
||||||
|
# Entity count
|
||||||
|
entity_count = len(self.entities)
|
||||||
|
count_text = mcrfpy.Caption(f"Enemies: {entity_count}", 900, 100)
|
||||||
|
count_text.font_size = 14
|
||||||
|
count_text.fill_color = mcrfpy.Color(150, 150, 255)
|
||||||
|
self.ui.append(count_text)
|
||||||
|
|
||||||
|
# Create and run the game
|
||||||
|
engine = Engine()
|
||||||
|
print("Part 5: Placing Enemies!")
|
||||||
|
print("Try bumping into enemies - combat coming in Part 6!")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Understanding Entity Interactions
|
||||||
|
|
||||||
|
### Collision Detection
|
||||||
|
Our system now checks three things when the player tries to move:
|
||||||
|
1. **Map boundaries** - Can't move outside the map
|
||||||
|
2. **Wall tiles** - Can't walk through walls
|
||||||
|
3. **Blocking entities** - Can't walk through enemies
|
||||||
|
|
||||||
|
### The Action System
|
||||||
|
We've introduced a simple action system that will grow in Part 6:
|
||||||
|
- `Action` - Base class for all actions
|
||||||
|
- `MovementAction` - Represents attempted movement
|
||||||
|
- `WaitAction` - Skip a turn (important for turn-based games)
|
||||||
|
|
||||||
|
### Entity Spawning
|
||||||
|
Enemies are placed randomly in rooms with these rules:
|
||||||
|
- Never in the first room (player's starting room)
|
||||||
|
- Random number between 0 and max per room
|
||||||
|
- 80% orcs, 20% trolls
|
||||||
|
- Must be placed on walkable, unoccupied tiles
|
||||||
|
|
||||||
|
## Visual Feedback
|
||||||
|
|
||||||
|
With FOV enabled, enemies will appear and disappear as you explore:
|
||||||
|
- Enemies in sight are fully visible
|
||||||
|
- Enemies in explored but dark areas are hidden
|
||||||
|
- Creates tension and surprise encounters
|
||||||
|
|
||||||
|
## Exercises
|
||||||
|
|
||||||
|
1. **More Enemy Types**: Add different sprites and names (goblins, skeletons)
|
||||||
|
2. **Enemy Density**: Adjust spawn rates based on dungeon depth
|
||||||
|
3. **Special Rooms**: Create rooms with guaranteed enemies or treasures
|
||||||
|
4. **Better Feedback**: Add sound effects or visual effects for bumping
|
||||||
|
|
||||||
|
## What's Next?
|
||||||
|
|
||||||
|
In Part 6, we'll transform those harmless kicks into a real combat system! We'll add:
|
||||||
|
- Health points for all entities
|
||||||
|
- Damage calculations
|
||||||
|
- Death and corpses
|
||||||
|
- Combat messages
|
||||||
|
- The beginning of a real roguelike!
|
||||||
|
|
||||||
|
Right now our enemies are just obstacles. Soon they'll fight back!
|
|
@ -0,0 +1,743 @@
|
||||||
|
# Part 6 - Doing (and Taking) Some Damage
|
||||||
|
|
||||||
|
It's time to turn our harmless kicks into real combat! In this part, we'll implement:
|
||||||
|
- Health points for all entities
|
||||||
|
- A damage calculation system
|
||||||
|
- Death and corpse mechanics
|
||||||
|
- Combat feedback messages
|
||||||
|
- The foundation of tactical roguelike combat
|
||||||
|
|
||||||
|
## Adding Combat Stats
|
||||||
|
|
||||||
|
First, let's enhance our GameObject class with combat capabilities:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class GameObject:
|
||||||
|
"""Base class for all game objects"""
|
||||||
|
def __init__(self, x, y, sprite_index, color, name,
|
||||||
|
blocks=False, hp=0, defense=0, power=0):
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.sprite_index = sprite_index
|
||||||
|
self.color = color
|
||||||
|
self.name = name
|
||||||
|
self.blocks = blocks
|
||||||
|
self._entity = None
|
||||||
|
self.grid = None
|
||||||
|
|
||||||
|
# Combat stats
|
||||||
|
self.max_hp = hp
|
||||||
|
self.hp = hp
|
||||||
|
self.defense = defense
|
||||||
|
self.power = power
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_alive(self):
|
||||||
|
"""Returns True if this entity can act"""
|
||||||
|
return self.hp > 0
|
||||||
|
|
||||||
|
def take_damage(self, amount):
|
||||||
|
"""Apply damage to this entity"""
|
||||||
|
damage = amount - self.defense
|
||||||
|
if damage > 0:
|
||||||
|
self.hp -= damage
|
||||||
|
|
||||||
|
# Check for death
|
||||||
|
if self.hp <= 0 and self.hp + damage > 0:
|
||||||
|
self.die()
|
||||||
|
|
||||||
|
return damage
|
||||||
|
|
||||||
|
def die(self):
|
||||||
|
"""Handle entity death"""
|
||||||
|
if self.name == "Player":
|
||||||
|
# Player death is special - we'll handle it differently
|
||||||
|
self.sprite_index = 64 # Stay as @ but change color
|
||||||
|
self.color = (127, 0, 0) # Dark red
|
||||||
|
if self._entity:
|
||||||
|
self._entity.color = mcrfpy.Color(127, 0, 0)
|
||||||
|
print("You have died!")
|
||||||
|
else:
|
||||||
|
# Enemy death
|
||||||
|
self.sprite_index = 37 # % character for corpse
|
||||||
|
self.color = (127, 0, 0) # Dark red
|
||||||
|
self.blocks = False # Corpses don't block
|
||||||
|
self.name = f"remains of {self.name}"
|
||||||
|
|
||||||
|
if self._entity:
|
||||||
|
self._entity.sprite_index = 37
|
||||||
|
self._entity.color = mcrfpy.Color(127, 0, 0)
|
||||||
|
```
|
||||||
|
|
||||||
|
## The Combat System
|
||||||
|
|
||||||
|
Now let's implement actual combat when entities bump into each other:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MeleeAction(Action):
|
||||||
|
"""Action for melee attacks"""
|
||||||
|
def __init__(self, attacker, target):
|
||||||
|
self.attacker = attacker
|
||||||
|
self.target = target
|
||||||
|
|
||||||
|
def perform(self):
|
||||||
|
"""Execute the attack"""
|
||||||
|
if not self.target.is_alive:
|
||||||
|
return # Can't attack the dead
|
||||||
|
|
||||||
|
damage = self.attacker.power - self.target.defense
|
||||||
|
|
||||||
|
if damage > 0:
|
||||||
|
attack_desc = f"{self.attacker.name} attacks {self.target.name} for {damage} damage!"
|
||||||
|
self.target.take_damage(damage)
|
||||||
|
else:
|
||||||
|
attack_desc = f"{self.attacker.name} attacks {self.target.name} but does no damage."
|
||||||
|
|
||||||
|
return attack_desc
|
||||||
|
```
|
||||||
|
|
||||||
|
## Entity Factories
|
||||||
|
|
||||||
|
Let's create factory functions for consistent entity creation:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def create_player(x, y):
|
||||||
|
"""Create the player entity"""
|
||||||
|
return GameObject(
|
||||||
|
x=x, y=y,
|
||||||
|
sprite_index=64, # @
|
||||||
|
color=(255, 255, 255),
|
||||||
|
name="Player",
|
||||||
|
blocks=True,
|
||||||
|
hp=30,
|
||||||
|
defense=2,
|
||||||
|
power=5
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_orc(x, y):
|
||||||
|
"""Create an orc enemy"""
|
||||||
|
return GameObject(
|
||||||
|
x=x, y=y,
|
||||||
|
sprite_index=111, # o
|
||||||
|
color=(63, 127, 63),
|
||||||
|
name="Orc",
|
||||||
|
blocks=True,
|
||||||
|
hp=10,
|
||||||
|
defense=0,
|
||||||
|
power=3
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_troll(x, y):
|
||||||
|
"""Create a troll enemy"""
|
||||||
|
return GameObject(
|
||||||
|
x=x, y=y,
|
||||||
|
sprite_index=84, # T
|
||||||
|
color=(0, 127, 0),
|
||||||
|
name="Troll",
|
||||||
|
blocks=True,
|
||||||
|
hp=16,
|
||||||
|
defense=1,
|
||||||
|
power=4
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## The Message Log
|
||||||
|
|
||||||
|
Combat needs feedback! Let's create a simple message log:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MessageLog:
|
||||||
|
"""Manages game messages"""
|
||||||
|
def __init__(self, max_messages=5):
|
||||||
|
self.messages = []
|
||||||
|
self.max_messages = max_messages
|
||||||
|
|
||||||
|
def add_message(self, text, color=(255, 255, 255)):
|
||||||
|
"""Add a message to the log"""
|
||||||
|
self.messages.append((text, color))
|
||||||
|
# Keep only recent messages
|
||||||
|
if len(self.messages) > self.max_messages:
|
||||||
|
self.messages.pop(0)
|
||||||
|
|
||||||
|
def render(self, ui, x, y, line_height=20):
|
||||||
|
"""Render messages to the UI"""
|
||||||
|
for i, (text, color) in enumerate(self.messages):
|
||||||
|
caption = mcrfpy.Caption(text, x, y + i * line_height)
|
||||||
|
caption.font_size = 14
|
||||||
|
caption.fill_color = mcrfpy.Color(*color)
|
||||||
|
ui.append(caption)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complete Implementation
|
||||||
|
|
||||||
|
Here's the complete `game.py` with combat:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import mcrfpy
|
||||||
|
import random
|
||||||
|
|
||||||
|
# Color configurations
|
||||||
|
COLORS_VISIBLE = {
|
||||||
|
'wall': (100, 100, 100),
|
||||||
|
'floor': (50, 50, 50),
|
||||||
|
'tunnel': (30, 30, 40),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Message colors
|
||||||
|
COLOR_PLAYER_ATK = (230, 230, 230)
|
||||||
|
COLOR_ENEMY_ATK = (255, 200, 200)
|
||||||
|
COLOR_PLAYER_DIE = (255, 100, 100)
|
||||||
|
COLOR_ENEMY_DIE = (255, 165, 0)
|
||||||
|
|
||||||
|
# Actions
|
||||||
|
class Action:
|
||||||
|
"""Base class for all actions"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class MovementAction(Action):
|
||||||
|
"""Action for moving an entity"""
|
||||||
|
def __init__(self, dx, dy):
|
||||||
|
self.dx = dx
|
||||||
|
self.dy = dy
|
||||||
|
|
||||||
|
class MeleeAction(Action):
|
||||||
|
"""Action for melee attacks"""
|
||||||
|
def __init__(self, attacker, target):
|
||||||
|
self.attacker = attacker
|
||||||
|
self.target = target
|
||||||
|
|
||||||
|
def perform(self):
|
||||||
|
"""Execute the attack"""
|
||||||
|
if not self.target.is_alive:
|
||||||
|
return None
|
||||||
|
|
||||||
|
damage = self.attacker.power - self.target.defense
|
||||||
|
|
||||||
|
if damage > 0:
|
||||||
|
attack_desc = f"{self.attacker.name} attacks {self.target.name} for {damage} damage!"
|
||||||
|
self.target.take_damage(damage)
|
||||||
|
|
||||||
|
# Choose color based on attacker
|
||||||
|
if self.attacker.name == "Player":
|
||||||
|
color = COLOR_PLAYER_ATK
|
||||||
|
else:
|
||||||
|
color = COLOR_ENEMY_ATK
|
||||||
|
|
||||||
|
return attack_desc, color
|
||||||
|
else:
|
||||||
|
attack_desc = f"{self.attacker.name} attacks {self.target.name} but does no damage."
|
||||||
|
return attack_desc, (150, 150, 150)
|
||||||
|
|
||||||
|
class WaitAction(Action):
|
||||||
|
"""Action for waiting/skipping turn"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class GameObject:
|
||||||
|
"""Base class for all game objects"""
|
||||||
|
def __init__(self, x, y, sprite_index, color, name,
|
||||||
|
blocks=False, hp=0, defense=0, power=0):
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.sprite_index = sprite_index
|
||||||
|
self.color = color
|
||||||
|
self.name = name
|
||||||
|
self.blocks = blocks
|
||||||
|
self._entity = None
|
||||||
|
self.grid = None
|
||||||
|
|
||||||
|
# Combat stats
|
||||||
|
self.max_hp = hp
|
||||||
|
self.hp = hp
|
||||||
|
self.defense = defense
|
||||||
|
self.power = power
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_alive(self):
|
||||||
|
"""Returns True if this entity can act"""
|
||||||
|
return self.hp > 0
|
||||||
|
|
||||||
|
def attach_to_grid(self, grid):
|
||||||
|
"""Attach this game object to a McRogueFace grid"""
|
||||||
|
self.grid = grid
|
||||||
|
self._entity = mcrfpy.Entity(x=self.x, y=self.y, grid=grid)
|
||||||
|
self._entity.sprite_index = self.sprite_index
|
||||||
|
self._entity.color = mcrfpy.Color(*self.color)
|
||||||
|
|
||||||
|
def move(self, dx, dy):
|
||||||
|
"""Move by the given amount"""
|
||||||
|
if not self.grid:
|
||||||
|
return
|
||||||
|
self.x += dx
|
||||||
|
self.y += dy
|
||||||
|
if self._entity:
|
||||||
|
self._entity.x = self.x
|
||||||
|
self._entity.y = self.y
|
||||||
|
# Update FOV when player moves
|
||||||
|
if self.name == "Player":
|
||||||
|
self.update_fov()
|
||||||
|
|
||||||
|
def update_fov(self):
|
||||||
|
"""Update field of view from this entity's position"""
|
||||||
|
if self._entity and self.grid:
|
||||||
|
self._entity.update_fov(radius=8)
|
||||||
|
|
||||||
|
def take_damage(self, amount):
|
||||||
|
"""Apply damage to this entity"""
|
||||||
|
self.hp -= amount
|
||||||
|
|
||||||
|
# Check for death
|
||||||
|
if self.hp <= 0:
|
||||||
|
self.die()
|
||||||
|
|
||||||
|
def die(self):
|
||||||
|
"""Handle entity death"""
|
||||||
|
if self.name == "Player":
|
||||||
|
# Player death
|
||||||
|
self.sprite_index = 64 # Stay as @
|
||||||
|
self.color = (127, 0, 0) # Dark red
|
||||||
|
if self._entity:
|
||||||
|
self._entity.color = mcrfpy.Color(127, 0, 0)
|
||||||
|
else:
|
||||||
|
# Enemy death
|
||||||
|
self.sprite_index = 37 # % character for corpse
|
||||||
|
self.color = (127, 0, 0) # Dark red
|
||||||
|
self.blocks = False # Corpses don't block
|
||||||
|
self.name = f"remains of {self.name}"
|
||||||
|
|
||||||
|
if self._entity:
|
||||||
|
self._entity.sprite_index = 37
|
||||||
|
self._entity.color = mcrfpy.Color(127, 0, 0)
|
||||||
|
|
||||||
|
# Entity factories
|
||||||
|
def create_player(x, y):
|
||||||
|
"""Create the player entity"""
|
||||||
|
return GameObject(
|
||||||
|
x=x, y=y,
|
||||||
|
sprite_index=64, # @
|
||||||
|
color=(255, 255, 255),
|
||||||
|
name="Player",
|
||||||
|
blocks=True,
|
||||||
|
hp=30,
|
||||||
|
defense=2,
|
||||||
|
power=5
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_orc(x, y):
|
||||||
|
"""Create an orc enemy"""
|
||||||
|
return GameObject(
|
||||||
|
x=x, y=y,
|
||||||
|
sprite_index=111, # o
|
||||||
|
color=(63, 127, 63),
|
||||||
|
name="Orc",
|
||||||
|
blocks=True,
|
||||||
|
hp=10,
|
||||||
|
defense=0,
|
||||||
|
power=3
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_troll(x, y):
|
||||||
|
"""Create a troll enemy"""
|
||||||
|
return GameObject(
|
||||||
|
x=x, y=y,
|
||||||
|
sprite_index=84, # T
|
||||||
|
color=(0, 127, 0),
|
||||||
|
name="Troll",
|
||||||
|
blocks=True,
|
||||||
|
hp=16,
|
||||||
|
defense=1,
|
||||||
|
power=4
|
||||||
|
)
|
||||||
|
|
||||||
|
class RectangularRoom:
|
||||||
|
"""A rectangular room with its position and size"""
|
||||||
|
|
||||||
|
def __init__(self, x, y, width, height):
|
||||||
|
self.x1 = x
|
||||||
|
self.y1 = y
|
||||||
|
self.x2 = x + width
|
||||||
|
self.y2 = y + height
|
||||||
|
|
||||||
|
@property
|
||||||
|
def center(self):
|
||||||
|
center_x = (self.x1 + self.x2) // 2
|
||||||
|
center_y = (self.y1 + self.y2) // 2
|
||||||
|
return center_x, center_y
|
||||||
|
|
||||||
|
@property
|
||||||
|
def inner(self):
|
||||||
|
return self.x1 + 1, self.y1 + 1, self.x2 - 1, self.y2 - 1
|
||||||
|
|
||||||
|
def intersects(self, other):
|
||||||
|
return (
|
||||||
|
self.x1 <= other.x2
|
||||||
|
and self.x2 >= other.x1
|
||||||
|
and self.y1 <= other.y2
|
||||||
|
and self.y2 >= other.y1
|
||||||
|
)
|
||||||
|
|
||||||
|
def tunnel_between(start, end):
|
||||||
|
"""Return an L-shaped tunnel between two points"""
|
||||||
|
x1, y1 = start
|
||||||
|
x2, y2 = end
|
||||||
|
|
||||||
|
if random.random() < 0.5:
|
||||||
|
corner_x = x2
|
||||||
|
corner_y = y1
|
||||||
|
else:
|
||||||
|
corner_x = x1
|
||||||
|
corner_y = y2
|
||||||
|
|
||||||
|
for x in range(min(x1, corner_x), max(x1, corner_x) + 1):
|
||||||
|
yield x, y1
|
||||||
|
for y in range(min(y1, corner_y), max(y1, corner_y) + 1):
|
||||||
|
yield corner_x, y
|
||||||
|
for x in range(min(corner_x, x2), max(corner_x, x2) + 1):
|
||||||
|
yield x, corner_y
|
||||||
|
for y in range(min(corner_y, y2), max(corner_y, y2) + 1):
|
||||||
|
yield x2, y
|
||||||
|
|
||||||
|
def spawn_enemies_in_room(room, game_map, max_enemies=2):
|
||||||
|
"""Spawn between 0 and max_enemies in a room"""
|
||||||
|
number_of_enemies = random.randint(0, max_enemies)
|
||||||
|
|
||||||
|
enemies_spawned = []
|
||||||
|
|
||||||
|
for i in range(number_of_enemies):
|
||||||
|
attempts = 10
|
||||||
|
while attempts > 0:
|
||||||
|
x = random.randint(room.x1 + 1, room.x2 - 1)
|
||||||
|
y = random.randint(room.y1 + 1, room.y2 - 1)
|
||||||
|
|
||||||
|
if not game_map.is_blocked(x, y):
|
||||||
|
# 80% chance for orc, 20% for troll
|
||||||
|
if random.random() < 0.8:
|
||||||
|
enemy = create_orc(x, y)
|
||||||
|
else:
|
||||||
|
enemy = create_troll(x, y)
|
||||||
|
|
||||||
|
game_map.add_entity(enemy)
|
||||||
|
enemies_spawned.append(enemy)
|
||||||
|
break
|
||||||
|
|
||||||
|
attempts -= 1
|
||||||
|
|
||||||
|
return enemies_spawned
|
||||||
|
|
||||||
|
class GameMap:
|
||||||
|
"""Manages the game world"""
|
||||||
|
|
||||||
|
def __init__(self, width, height):
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.grid = None
|
||||||
|
self.entities = []
|
||||||
|
self.rooms = []
|
||||||
|
|
||||||
|
def create_grid(self, tileset):
|
||||||
|
"""Create the McRogueFace grid"""
|
||||||
|
self.grid = mcrfpy.Grid(grid_x=self.width, grid_y=self.height, texture=tileset)
|
||||||
|
self.grid.position = (100, 100)
|
||||||
|
self.grid.size = (800, 480)
|
||||||
|
|
||||||
|
# Enable perspective rendering
|
||||||
|
self.grid.perspective = 0
|
||||||
|
|
||||||
|
return self.grid
|
||||||
|
|
||||||
|
def fill_with_walls(self):
|
||||||
|
"""Fill the entire map with wall tiles"""
|
||||||
|
for y in range(self.height):
|
||||||
|
for x in range(self.width):
|
||||||
|
self.set_tile(x, y, walkable=False, transparent=False,
|
||||||
|
sprite_index=35, tile_type='wall')
|
||||||
|
|
||||||
|
def set_tile(self, x, y, walkable, transparent, sprite_index, tile_type):
|
||||||
|
"""Set properties for a specific tile"""
|
||||||
|
if 0 <= x < self.width and 0 <= y < self.height:
|
||||||
|
cell = self.grid.at(x, y)
|
||||||
|
cell.walkable = walkable
|
||||||
|
cell.transparent = transparent
|
||||||
|
cell.sprite_index = sprite_index
|
||||||
|
cell.color = mcrfpy.Color(*COLORS_VISIBLE[tile_type])
|
||||||
|
|
||||||
|
def generate_dungeon(self, max_rooms, room_min_size, room_max_size, player, max_enemies_per_room):
|
||||||
|
"""Generate a new dungeon map"""
|
||||||
|
self.fill_with_walls()
|
||||||
|
|
||||||
|
for r in range(max_rooms):
|
||||||
|
room_width = random.randint(room_min_size, room_max_size)
|
||||||
|
room_height = random.randint(room_min_size, room_max_size)
|
||||||
|
|
||||||
|
x = random.randint(0, self.width - room_width - 1)
|
||||||
|
y = random.randint(0, self.height - room_height - 1)
|
||||||
|
|
||||||
|
new_room = RectangularRoom(x, y, room_width, room_height)
|
||||||
|
|
||||||
|
if any(new_room.intersects(other_room) for other_room in self.rooms):
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.carve_room(new_room)
|
||||||
|
|
||||||
|
if len(self.rooms) == 0:
|
||||||
|
# First room - place player
|
||||||
|
player.x, player.y = new_room.center
|
||||||
|
if player._entity:
|
||||||
|
player._entity.x, player._entity.y = new_room.center
|
||||||
|
else:
|
||||||
|
# All other rooms - add tunnel and enemies
|
||||||
|
self.carve_tunnel(self.rooms[-1].center, new_room.center)
|
||||||
|
spawn_enemies_in_room(new_room, self, max_enemies_per_room)
|
||||||
|
|
||||||
|
self.rooms.append(new_room)
|
||||||
|
|
||||||
|
def carve_room(self, room):
|
||||||
|
"""Carve out a room"""
|
||||||
|
inner_x1, inner_y1, inner_x2, inner_y2 = room.inner
|
||||||
|
|
||||||
|
for y in range(inner_y1, inner_y2):
|
||||||
|
for x in range(inner_x1, inner_x2):
|
||||||
|
self.set_tile(x, y, walkable=True, transparent=True,
|
||||||
|
sprite_index=46, tile_type='floor')
|
||||||
|
|
||||||
|
def carve_tunnel(self, start, end):
|
||||||
|
"""Carve a tunnel between two points"""
|
||||||
|
for x, y in tunnel_between(start, end):
|
||||||
|
self.set_tile(x, y, walkable=True, transparent=True,
|
||||||
|
sprite_index=46, tile_type='tunnel')
|
||||||
|
|
||||||
|
def get_blocking_entity_at(self, x, y):
|
||||||
|
"""Return any blocking entity at the given position"""
|
||||||
|
for entity in self.entities:
|
||||||
|
if entity.blocks and entity.x == x and entity.y == y:
|
||||||
|
return entity
|
||||||
|
return None
|
||||||
|
|
||||||
|
def is_blocked(self, x, y):
|
||||||
|
"""Check if a tile blocks movement"""
|
||||||
|
if x < 0 or x >= self.width or y < 0 or y >= self.height:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if not self.grid.at(x, y).walkable:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if self.get_blocking_entity_at(x, y):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def add_entity(self, entity):
|
||||||
|
"""Add a GameObject to the map"""
|
||||||
|
self.entities.append(entity)
|
||||||
|
entity.attach_to_grid(self.grid)
|
||||||
|
|
||||||
|
class Engine:
|
||||||
|
"""Main game engine"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.game_map = None
|
||||||
|
self.player = None
|
||||||
|
self.entities = []
|
||||||
|
self.messages = [] # Simple message log
|
||||||
|
self.max_messages = 5
|
||||||
|
|
||||||
|
mcrfpy.createScene("game")
|
||||||
|
mcrfpy.setScene("game")
|
||||||
|
|
||||||
|
window = mcrfpy.Window.get()
|
||||||
|
window.title = "McRogueFace Roguelike - Part 6"
|
||||||
|
|
||||||
|
self.ui = mcrfpy.sceneUI("game")
|
||||||
|
|
||||||
|
background = mcrfpy.Frame(0, 0, 1024, 768)
|
||||||
|
background.fill_color = mcrfpy.Color(0, 0, 0)
|
||||||
|
self.ui.append(background)
|
||||||
|
|
||||||
|
self.tileset = mcrfpy.Texture("assets/sprites/ascii_tileset.png", 16, 16)
|
||||||
|
|
||||||
|
self.setup_game()
|
||||||
|
self.setup_input()
|
||||||
|
self.setup_ui()
|
||||||
|
|
||||||
|
def add_message(self, text, color=(255, 255, 255)):
|
||||||
|
"""Add a message to the log"""
|
||||||
|
self.messages.append((text, color))
|
||||||
|
if len(self.messages) > self.max_messages:
|
||||||
|
self.messages.pop(0)
|
||||||
|
self.update_message_display()
|
||||||
|
|
||||||
|
def update_message_display(self):
|
||||||
|
"""Update the message display"""
|
||||||
|
# Clear old messages
|
||||||
|
for caption in self.message_captions:
|
||||||
|
# Remove from UI (McRogueFace doesn't have remove, so we hide it)
|
||||||
|
caption.text = ""
|
||||||
|
|
||||||
|
# Display current messages
|
||||||
|
for i, (text, color) in enumerate(self.messages):
|
||||||
|
if i < len(self.message_captions):
|
||||||
|
self.message_captions[i].text = text
|
||||||
|
self.message_captions[i].fill_color = mcrfpy.Color(*color)
|
||||||
|
|
||||||
|
def setup_game(self):
|
||||||
|
"""Initialize the game world"""
|
||||||
|
self.game_map = GameMap(80, 45)
|
||||||
|
grid = self.game_map.create_grid(self.tileset)
|
||||||
|
self.ui.append(grid)
|
||||||
|
|
||||||
|
# Create player
|
||||||
|
self.player = create_player(0, 0)
|
||||||
|
|
||||||
|
# Generate the dungeon
|
||||||
|
self.game_map.generate_dungeon(
|
||||||
|
max_rooms=30,
|
||||||
|
room_min_size=6,
|
||||||
|
room_max_size=10,
|
||||||
|
player=self.player,
|
||||||
|
max_enemies_per_room=2
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add player to map
|
||||||
|
self.game_map.add_entity(self.player)
|
||||||
|
|
||||||
|
# Store reference to all entities
|
||||||
|
self.entities = [e for e in self.game_map.entities if e != self.player]
|
||||||
|
|
||||||
|
# Initial FOV calculation
|
||||||
|
self.player.update_fov()
|
||||||
|
|
||||||
|
# Welcome message
|
||||||
|
self.add_message("Welcome to the dungeon!", (100, 100, 255))
|
||||||
|
|
||||||
|
def handle_player_turn(self, action):
|
||||||
|
"""Process the player's action"""
|
||||||
|
if not self.player.is_alive:
|
||||||
|
return
|
||||||
|
|
||||||
|
if isinstance(action, MovementAction):
|
||||||
|
dest_x = self.player.x + action.dx
|
||||||
|
dest_y = self.player.y + action.dy
|
||||||
|
|
||||||
|
# Check what's at the destination
|
||||||
|
target = self.game_map.get_blocking_entity_at(dest_x, dest_y)
|
||||||
|
|
||||||
|
if target:
|
||||||
|
# Attack!
|
||||||
|
attack = MeleeAction(self.player, target)
|
||||||
|
result = attack.perform()
|
||||||
|
if result:
|
||||||
|
text, color = result
|
||||||
|
self.add_message(text, color)
|
||||||
|
|
||||||
|
# Check if target died
|
||||||
|
if not target.is_alive:
|
||||||
|
death_msg = f"The {target.name.replace('remains of ', '')} is dead!"
|
||||||
|
self.add_message(death_msg, COLOR_ENEMY_DIE)
|
||||||
|
|
||||||
|
elif not self.game_map.is_blocked(dest_x, dest_y):
|
||||||
|
# Move the player
|
||||||
|
self.player.move(action.dx, action.dy)
|
||||||
|
|
||||||
|
elif isinstance(action, WaitAction):
|
||||||
|
pass # Do nothing
|
||||||
|
|
||||||
|
# Enemy turns
|
||||||
|
self.handle_enemy_turns()
|
||||||
|
|
||||||
|
def handle_enemy_turns(self):
|
||||||
|
"""Let all enemies take their turn"""
|
||||||
|
for entity in self.entities:
|
||||||
|
if entity.is_alive:
|
||||||
|
# Simple AI: if player is adjacent, attack. Otherwise, do nothing.
|
||||||
|
dx = entity.x - self.player.x
|
||||||
|
dy = entity.y - self.player.y
|
||||||
|
distance = abs(dx) + abs(dy)
|
||||||
|
|
||||||
|
if distance == 1: # Adjacent to player
|
||||||
|
attack = MeleeAction(entity, self.player)
|
||||||
|
result = attack.perform()
|
||||||
|
if result:
|
||||||
|
text, color = result
|
||||||
|
self.add_message(text, color)
|
||||||
|
|
||||||
|
# Check if player died
|
||||||
|
if not self.player.is_alive:
|
||||||
|
self.add_message("You have died!", COLOR_PLAYER_DIE)
|
||||||
|
|
||||||
|
def setup_input(self):
|
||||||
|
"""Setup keyboard input handling"""
|
||||||
|
def handle_keys(key, state):
|
||||||
|
if state != "start":
|
||||||
|
return
|
||||||
|
|
||||||
|
action = None
|
||||||
|
|
||||||
|
# Movement keys
|
||||||
|
movement = {
|
||||||
|
"Up": (0, -1), "Down": (0, 1),
|
||||||
|
"Left": (-1, 0), "Right": (1, 0),
|
||||||
|
"Num7": (-1, -1), "Num8": (0, -1), "Num9": (1, -1),
|
||||||
|
"Num4": (-1, 0), "Num5": (0, 0), "Num6": (1, 0),
|
||||||
|
"Num1": (-1, 1), "Num2": (0, 1), "Num3": (1, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
if key in movement:
|
||||||
|
dx, dy = movement[key]
|
||||||
|
if dx == 0 and dy == 0:
|
||||||
|
action = WaitAction()
|
||||||
|
else:
|
||||||
|
action = MovementAction(dx, dy)
|
||||||
|
elif key == "Period":
|
||||||
|
action = WaitAction()
|
||||||
|
elif key == "Escape":
|
||||||
|
mcrfpy.setScene(None)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Process the action
|
||||||
|
if action:
|
||||||
|
self.handle_player_turn(action)
|
||||||
|
|
||||||
|
mcrfpy.keypressScene(handle_keys)
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
"""Setup UI elements"""
|
||||||
|
title = mcrfpy.Caption("Combat System", 512, 30)
|
||||||
|
title.font_size = 24
|
||||||
|
title.fill_color = mcrfpy.Color(255, 255, 100)
|
||||||
|
self.ui.append(title)
|
||||||
|
|
||||||
|
instructions = mcrfpy.Caption("Attack enemies by bumping into them!", 512, 60)
|
||||||
|
instructions.font_size = 16
|
||||||
|
instructions.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
|
self.ui.append(instructions)
|
||||||
|
|
||||||
|
# Player stats
|
||||||
|
self.hp_text = mcrfpy.Caption(f"HP: {self.player.hp}/{self.player.max_hp}", 50, 100)
|
||||||
|
self.hp_text.font_size = 18
|
||||||
|
self.hp_text.fill_color = mcrfpy.Color(255, 100, 100)
|
||||||
|
self.ui.append(self.hp_text)
|
||||||
|
|
||||||
|
# Message log
|
||||||
|
self.message_captions = []
|
||||||
|
for i in range(self.max_messages):
|
||||||
|
caption = mcrfpy.Caption("", 50, 620 + i * 20)
|
||||||
|
caption.font_size = 14
|
||||||
|
caption.fill_color = mcrfpy.Color(200, 200, 200)
|
||||||
|
self.ui.append(caption)
|
||||||
|
self.message_captions.append(caption)
|
||||||
|
|
||||||
|
# Timer to update HP display
|
||||||
|
def update_stats(dt):
|
||||||
|
self.hp_text.text = f"HP: {self.player.hp}/{self.player.max_hp}"
|
||||||
|
if self.player.hp <= 0:
|
||||||
|
self.hp_text.fill_color = mcrfpy.Color(127, 0, 0)
|
||||||
|
elif self.player.hp < self.player.max_hp // 3:
|
||||||
|
self.hp_text.fill_color = mcrfpy.Color(255, 100, 100)
|
||||||
|
else:
|
||||||
|
self.hp_text.fill_color = mcrfpy.Color(0, 255, 0)
|
||||||
|
|
||||||
|
mcrfpy.setTimer("update_stats", update_stats, 100)
|
||||||
|
|
||||||
|
# Create and run the game
|
||||||
|
engine = Engine()
|
||||||
|
print("Part 6: Combat System!")
|
||||||
|
print("Attack enemies to defeat them, but watch your HP!")
|
|
@ -1,449 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en" style="color-scheme: dark;"><head>
|
|
||||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
|
||||||
<title>
|
|
||||||
Part 0 - Setting Up · Roguelike Tutorials
|
|
||||||
</title>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta name="color-scheme" content="light dark">
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<meta name="description" content="Prior knowledge Link to heading This tutorial assumes some basic familiarity with programming in general, and with Python. If you’ve never used Python before, this tutorial could be a little confusing. There are many free resources online about learning programming and Python (too many to list here), and I’d recommend learning about objects and functions in Python at the very least before attempting to read this tutorial.
|
|
||||||
… Of course, there are those who have ignored this advice and done well with this tutorial anyway, so feel free to ignore that last paragraph if you’re feeling bold!">
|
|
||||||
<meta name="keywords" content="">
|
|
||||||
|
|
||||||
<meta name="twitter:card" content="summary">
|
|
||||||
<meta name="twitter:title" content="Part 0 - Setting Up">
|
|
||||||
<meta name="twitter:description" content="Prior knowledge Link to heading This tutorial assumes some basic familiarity with programming in general, and with Python. If you’ve never used Python before, this tutorial could be a little confusing. There are many free resources online about learning programming and Python (too many to list here), and I’d recommend learning about objects and functions in Python at the very least before attempting to read this tutorial.
|
|
||||||
… Of course, there are those who have ignored this advice and done well with this tutorial anyway, so feel free to ignore that last paragraph if you’re feeling bold!">
|
|
||||||
|
|
||||||
<meta property="og:title" content="Part 0 - Setting Up">
|
|
||||||
<meta property="og:description" content="Prior knowledge Link to heading This tutorial assumes some basic familiarity with programming in general, and with Python. If you’ve never used Python before, this tutorial could be a little confusing. There are many free resources online about learning programming and Python (too many to list here), and I’d recommend learning about objects and functions in Python at the very least before attempting to read this tutorial.
|
|
||||||
… Of course, there are those who have ignored this advice and done well with this tutorial anyway, so feel free to ignore that last paragraph if you’re feeling bold!">
|
|
||||||
<meta property="og:type" content="article">
|
|
||||||
<meta property="og:url" content="https://rogueliketutorials.com/tutorials/tcod/v2/part-0/"><meta property="article:section" content="tutorials">
|
|
||||||
<meta property="article:published_time" content="2020-06-14T11:25:36-07:00">
|
|
||||||
<meta property="article:modified_time" content="2020-06-14T11:25:36-07:00">
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<link rel="canonical" href="https://rogueliketutorials.com/tutorials/tcod/v2/part-0/">
|
|
||||||
|
|
||||||
|
|
||||||
<link rel="preload" href="https://rogueliketutorials.com/fonts/forkawesome-webfont.woff2?v=1.2.0" as="font" type="font/woff2" crossorigin="">
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="Part%200%20-%20Setting%20Up%20%C2%B7%20Roguelike%20Tutorials_files/coder.min.c4d7e93a158eda5a65b3df343745d2092a0a1e2170feeec909.css" integrity="sha256-xNfpOhWO2lpls980N0XSCSoKHiFw/u7JCbiolEOQPGo=" crossorigin="anonymous" media="screen">
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="Part%200%20-%20Setting%20Up%20%C2%B7%20Roguelike%20Tutorials_files/coder-dark.min.78b5fe3864945faf5207fb8fe3ab2320d49c3365def0e.css" integrity="sha256-eLX+OGSUX69SB/uP46sjINScM2Xe8OiKwd8N2txUoDw=" crossorigin="anonymous" media="screen">
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="Part%200%20-%20Setting%20Up%20%C2%B7%20Roguelike%20Tutorials_files/style.min.9d3eb202952dddb888856ff12c83bc88de866c596286bfb4c1.css" integrity="sha256-nT6yApUt3biIhW/xLIO8iN6GbFlihr+0wfjmvq2a42Y=" crossorigin="anonymous" media="screen">
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<link rel="icon" type="image/png" href="https://rogueliketutorials.com/images/favicon-32x32.png" sizes="32x32">
|
|
||||||
<link rel="icon" type="image/png" href="https://rogueliketutorials.com/images/favicon-16x16.png" sizes="16x16">
|
|
||||||
|
|
||||||
<link rel="apple-touch-icon" href="https://rogueliketutorials.com/images/apple-touch-icon.png">
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="https://rogueliketutorials.com/images/apple-touch-icon.png">
|
|
||||||
|
|
||||||
<link rel="manifest" href="https://rogueliketutorials.com/site.webmanifest">
|
|
||||||
<link rel="mask-icon" href="https://rogueliketutorials.com/images/safari-pinned-tab.svg" color="#5bbad5">
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<meta name="generator" content="Hugo 0.110.0">
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<style>:is([id*='google_ads_iframe'],[id*='taboola-'],.taboolaHeight,.taboola-placeholder,#top-ad,#credential_picker_container,#credentials-picker-container,#credential_picker_iframe,[id*='google-one-tap-iframe'],#google-one-tap-popup-container,.google-one-tap__module,.google-one-tap-modal-div,#amp_floatingAdDiv,#ez-content-blocker-container) {display:none!important;min-height:0!important;height:0!important;}</style></head>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<body class="colorscheme-dark vsc-initialized">
|
|
||||||
|
|
||||||
<div class="float-container">
|
|
||||||
<a id="dark-mode-toggle" class="colorscheme-toggle">
|
|
||||||
<i class="fa fa-adjust fa-fw" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<main class="wrapper">
|
|
||||||
<nav class="navigation">
|
|
||||||
<section class="container">
|
|
||||||
<a class="navigation-title" href="https://rogueliketutorials.com/">
|
|
||||||
Roguelike Tutorials
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<input type="checkbox" id="menu-toggle">
|
|
||||||
<label class="menu-button float-right" for="menu-toggle">
|
|
||||||
<i class="fa fa-bars fa-fw" aria-hidden="true"></i>
|
|
||||||
</label>
|
|
||||||
<ul class="navigation-list">
|
|
||||||
|
|
||||||
|
|
||||||
<li class="navigation-item">
|
|
||||||
<a class="navigation-link" href="https://rogueliketutorials.com/">Home</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="navigation-item">
|
|
||||||
<a class="navigation-link" href="https://rogueliketutorials.com/tutorials/tcod/v2/">TCOD Tutorial (2020)</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="navigation-item">
|
|
||||||
<a class="navigation-link" href="https://rogueliketutorials.com/tutorials/tcod/2019/">TCOD Tutorial (2019)</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="navigation-item">
|
|
||||||
<a class="navigation-link" href="https://rogueliketutorials.com/about/">About</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
</section>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="content">
|
|
||||||
|
|
||||||
<section class="container page">
|
|
||||||
<article>
|
|
||||||
<header>
|
|
||||||
<h1 class="title">
|
|
||||||
<a class="title-link" href="https://rogueliketutorials.com/tutorials/tcod/v2/part-0/">
|
|
||||||
Part 0 - Setting Up
|
|
||||||
</a>
|
|
||||||
</h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<h4 id="prior-knowledge">
|
|
||||||
Prior knowledge
|
|
||||||
<a class="heading-link" href="#prior-knowledge">
|
|
||||||
<i class="fa fa-link" aria-hidden="true" title="Link to heading"></i>
|
|
||||||
<span class="sr-only">Link to heading</span>
|
|
||||||
</a>
|
|
||||||
</h4>
|
|
||||||
<p>This tutorial assumes some basic familiarity with programming in
|
|
||||||
general, and with Python. If you’ve never used Python before, this
|
|
||||||
tutorial could be a little confusing. There are many free resources
|
|
||||||
online about learning programming and Python (too many to list here),
|
|
||||||
and I’d recommend learning about objects and functions in Python at the
|
|
||||||
very least before attempting to read this tutorial.</p>
|
|
||||||
<p>… Of course, there are those who have ignored this advice and done
|
|
||||||
well with this tutorial anyway, so feel free to ignore that last
|
|
||||||
paragraph if you’re feeling bold!</p>
|
|
||||||
<h4 id="installation">
|
|
||||||
Installation
|
|
||||||
<a class="heading-link" href="#installation">
|
|
||||||
<i class="fa fa-link" aria-hidden="true" title="Link to heading"></i>
|
|
||||||
<span class="sr-only">Link to heading</span>
|
|
||||||
</a>
|
|
||||||
</h4>
|
|
||||||
<p>To do this tutorial, you’ll need Python version 3.7 or higher. The
|
|
||||||
latest version of Python is recommended (currently 3.8 as of June
|
|
||||||
2020). <strong>Note: Python 2 is not compatible.</strong></p>
|
|
||||||
<p><a href="https://www.python.org/downloads/">Download Python here</a>.</p>
|
|
||||||
<p>You’ll also want the latest version of the TCOD library, which is what
|
|
||||||
this tutorial is based on.</p>
|
|
||||||
<p><a href="https://python-tcod.readthedocs.io/en/latest/installation.html">Installation instructions for TCOD can be found
|
|
||||||
here.</a></p>
|
|
||||||
<p>While you can certainly install TCOD and complete this tutorial without
|
|
||||||
it, I’d highly recommend using a virtual environment. <a href="https://docs.python.org/3/library/venv.html">Documentation on
|
|
||||||
how to do that can be found
|
|
||||||
here.</a></p>
|
|
||||||
<p>Additionally, if you are going to use a virtual environment, you may want to take the time to set up a <code>requirements.txt</code>
|
|
||||||
file. This will allow you to track your project dependencies if you add
|
|
||||||
any in the future, and more easily install them if you need to (for
|
|
||||||
example, if you pull from a remote git repository).</p>
|
|
||||||
<p>You can set up your <code>requirements.txt</code> file in the same directory that you plan on working in for the project. Create the file <code>requirements.txt</code> and put the following in it:</p>
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>tcod>=11.13
|
|
||||||
</span></span><span style="display:flex;"><span>numpy>=1.18</span></span></code></pre></div>
|
|
||||||
<p>Once that’s done, with your virtual environment activated, type the following command:</p>
|
|
||||||
<p><code>pip install -r requirements.txt</code></p>
|
|
||||||
<p>This should install the TCOD library, along with its dependency, numpy.</p>
|
|
||||||
<p>Depending on your computer, you might also need to install SDL2.
|
|
||||||
Check the instructions for installing it based on your operating system.
|
|
||||||
For example, Ubuntu can install it with the following command:</p>
|
|
||||||
<p><code>sudo apt-get install libsdl2-dev</code></p>
|
|
||||||
<h4 id="editors">
|
|
||||||
Editors
|
|
||||||
<a class="heading-link" href="#editors">
|
|
||||||
<i class="fa fa-link" aria-hidden="true" title="Link to heading"></i>
|
|
||||||
<span class="sr-only">Link to heading</span>
|
|
||||||
</a>
|
|
||||||
</h4>
|
|
||||||
<p>Any text editor can work for writing Python. You could even use Notepad
|
|
||||||
if you really wanted to. Personally, I’m a fan of
|
|
||||||
<a href="https://www.jetbrains.com/pycharm/">Pycharm</a> and <a href="https://code.visualstudio.com/">Visual Studio
|
|
||||||
Code</a>. Whatever you choose, I strongly
|
|
||||||
recommend something that can help catch Python syntax errors at the very
|
|
||||||
least. I’ve been working with Python for over five years, and I still
|
|
||||||
make these types of mistakes all the time!</p>
|
|
||||||
<h4 id="making-sure-python-works">
|
|
||||||
Making sure Python works
|
|
||||||
<a class="heading-link" href="#making-sure-python-works">
|
|
||||||
<i class="fa fa-link" aria-hidden="true" title="Link to heading"></i>
|
|
||||||
<span class="sr-only">Link to heading</span>
|
|
||||||
</a>
|
|
||||||
</h4>
|
|
||||||
<p>To verify that your installation of both Python 3 and TCOD are working,
|
|
||||||
create a new file (in whatever directory you plan on using for the
|
|
||||||
tutorial) called <code>main.py</code>, and enter the following text into it:</p>
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span><span style="color:#75715e">#!/usr/bin/env python3</span>
|
|
||||||
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> tcod
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">main</span>():
|
|
||||||
</span></span><span style="display:flex;"><span> print(<span style="color:#e6db74">"Hello World!"</span>)
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> __name__ <span style="color:#f92672">==</span> <span style="color:#e6db74">"__main__"</span>:
|
|
||||||
</span></span><span style="display:flex;"><span> main()
|
|
||||||
</span></span></code></pre></div><p>Run the file in your terminal (or alternatively in your editor, if
|
|
||||||
possible):</p>
|
|
||||||
<p><code>python main.py</code></p>
|
|
||||||
<p>If you’re not using <code>virtualenv</code>, the command will probably look like
|
|
||||||
this:</p>
|
|
||||||
<p><code>python3 main.py</code></p>
|
|
||||||
<p>You should see “Hello World!” printed out to the terminal. If you
|
|
||||||
receive an error, there is probably an issue with either your Python or
|
|
||||||
TCOD installation.</p>
|
|
||||||
<h3 id="downloading-the-image-file">
|
|
||||||
Downloading the Image File
|
|
||||||
<a class="heading-link" href="#downloading-the-image-file">
|
|
||||||
<i class="fa fa-link" aria-hidden="true" title="Link to heading"></i>
|
|
||||||
<span class="sr-only">Link to heading</span>
|
|
||||||
</a>
|
|
||||||
</h3>
|
|
||||||
<p>For this tutorial, we’ll need an image file. The default one is provided below.</p>
|
|
||||||
<p><img src="Part%200%20-%20Setting%20Up%20%C2%B7%20Roguelike%20Tutorials_files/dejavu10x10_gs_tc.png" alt="Font File"></p>
|
|
||||||
<p>Right click the image and save it to the same directory that you’re planning on
|
|
||||||
placing your code in. If the above image is not displaying for some reason,
|
|
||||||
it is also <a href="https://raw.githubusercontent.com/TStand90/tcod_tutorial_v2/1667c8995fb0d0fd6df98bd84c0be46cb8b78dac/dejavu10x10_gs_tc.png">available for download here.</a></p>
|
|
||||||
<h3 id="about-this-site">
|
|
||||||
About this site
|
|
||||||
<a class="heading-link" href="#about-this-site">
|
|
||||||
<i class="fa fa-link" aria-hidden="true" title="Link to heading"></i>
|
|
||||||
<span class="sr-only">Link to heading</span>
|
|
||||||
</a>
|
|
||||||
</h3>
|
|
||||||
<p>Code snippets in this website are presented in a way that tries to convey
|
|
||||||
exactly what the user should be adding to a file at what time. When a user
|
|
||||||
is expected to create a file from scratch and enter code into it, it will
|
|
||||||
be represented with standard Python code highlighting, like so:</p>
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Fighter</span>:
|
|
||||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> __init__(self, hp, defense, power):
|
|
||||||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>max_hp <span style="color:#f92672">=</span> hp
|
|
||||||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>hp <span style="color:#f92672">=</span> hp
|
|
||||||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>defense <span style="color:#f92672">=</span> defense
|
|
||||||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>power <span style="color:#f92672">=</span> power</span></span></code></pre></div>
|
|
||||||
<p>*<em>Taken from part 6</em>.</p>
|
|
||||||
<p>Most of the time, you’ll be editing a file and code that already exists.
|
|
||||||
In such cases, the code will be displayed like this:</p>
|
|
||||||
<div>
|
|
||||||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
|
||||||
Diff
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
|
||||||
Original
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="data-pane active" data-pane="diff">
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>class Entity:
|
|
||||||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def __init__(self, x, y, char, color, name, blocks=False):
|
|
||||||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ def __init__(self, x, y, char, color, name, blocks=False, fighter=None, ai=None):
|
|
||||||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> self.x = x
|
|
||||||
</span></span><span style="display:flex;"><span> self.y = y
|
|
||||||
</span></span><span style="display:flex;"><span> self.char = char
|
|
||||||
</span></span><span style="display:flex;"><span> self.color = color
|
|
||||||
</span></span><span style="display:flex;"><span> self.name = name
|
|
||||||
</span></span><span style="display:flex;"><span> self.blocks = blocks
|
|
||||||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.fighter = fighter
|
|
||||||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.ai = ai
|
|
||||||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+
|
|
||||||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if self.fighter:
|
|
||||||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.fighter.owner = self
|
|
||||||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+
|
|
||||||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if self.ai:
|
|
||||||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.ai.owner = self
|
|
||||||
</span></span></span></code></pre></div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="data-pane" data-pane="original">
|
|
||||||
|
|
||||||
<pre>class Entity:
|
|
||||||
<span class="crossed-out-text">def __init__(self, x, y, char, color, name, blocks=False):</span>
|
|
||||||
<span class="new-text">def __init__(self, x, y, char, color, name, blocks=False, fighter=None, ai=None):</span>
|
|
||||||
self.x = x
|
|
||||||
self.y = y
|
|
||||||
self.char = char
|
|
||||||
self.color = color
|
|
||||||
self.name = name
|
|
||||||
self.blocks = blocks
|
|
||||||
<span class="new-text">self.fighter = fighter
|
|
||||||
self.ai = ai
|
|
||||||
|
|
||||||
if self.fighter:
|
|
||||||
self.fighter.owner = self
|
|
||||||
|
|
||||||
if self.ai:
|
|
||||||
self.ai.owner = self</span></pre>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>*<em>Also taken from part 6.</em></p>
|
|
||||||
<p>Clicking a button above the code section changes the “style” for not just that code block,
|
|
||||||
but the entire website. You can switch between these styles at any time.</p>
|
|
||||||
<p>In the case of the example above, you would remove the old <code>__init__</code> definition, replacing
|
|
||||||
it with the new one. Then, you’d add the necessary lines at the bottom. Both styles convey
|
|
||||||
the same idea.</p>
|
|
||||||
<p>But what’s the difference? The “Diff” style shows the code as you might find it when doing
|
|
||||||
a Git diff comparison (hence the name). It shows plusses and minuses on the side to denote
|
|
||||||
whether you should be adding or subtracting a line from a file. The “Original” style shows
|
|
||||||
the same thing, but it crosses out the lines to remove and does not have plusses nor minuses.</p>
|
|
||||||
<p>The benefit of the “Diff” style is that it doesn’t rely on color to denote what to add, making
|
|
||||||
it more accessible all around. The drawback is that it’s impossible to accurately display the
|
|
||||||
proper indentation in some instances. The plusses and minuses take up one space, so in a code
|
|
||||||
section like this one, be sure not to leave the space for the plus in your code (there should
|
|
||||||
be no spaces before “from”):</p>
|
|
||||||
<div>
|
|
||||||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
|
||||||
Diff
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
|
||||||
Original
|
|
||||||
</button>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="data-pane active" data-pane="diff">
|
|
||||||
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>import tcod
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+from input_handlers import handle_keys
|
|
||||||
</span></span></span></code></pre></div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="data-pane" data-pane="original">
|
|
||||||
|
|
||||||
<pre>import tcod
|
|
||||||
|
|
||||||
<span class="new-text">from input_handlers import handle_keys</span></pre>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>The “Original” style omits the + and - symbols and doesn’t have the indentation issue,
|
|
||||||
making it a bit easier to copy and paste code sections.</p>
|
|
||||||
<p>Which style you use is a matter of personal preference. The actual code of the tutorial
|
|
||||||
remains the same.</p>
|
|
||||||
<h3 id="getting-help">
|
|
||||||
Getting help
|
|
||||||
<a class="heading-link" href="#getting-help">
|
|
||||||
<i class="fa fa-link" aria-hidden="true" title="Link to heading"></i>
|
|
||||||
<span class="sr-only">Link to heading</span>
|
|
||||||
</a>
|
|
||||||
</h3>
|
|
||||||
<p>Be sure to check out the <a href="https://www.reddit.com/r/roguelikedev">Roguelike Development
|
|
||||||
Subreddit</a> for help. There’s a
|
|
||||||
link there to the Discord channel as well.</p>
|
|
||||||
<hr>
|
|
||||||
<h3 id="ready-to-go">
|
|
||||||
Ready to go?
|
|
||||||
<a class="heading-link" href="#ready-to-go">
|
|
||||||
<i class="fa fa-link" aria-hidden="true" title="Link to heading"></i>
|
|
||||||
<span class="sr-only">Link to heading</span>
|
|
||||||
</a>
|
|
||||||
</h3>
|
|
||||||
<p>Once you’re set up and ready to go, you can proceed to <a href="https://rogueliketutorials.com/tutorials/tcod/v2/part-1">Part
|
|
||||||
1</a>.</p>
|
|
||||||
|
|
||||||
</article>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer class="footer">
|
|
||||||
<section class="container">
|
|
||||||
©
|
|
||||||
|
|
||||||
2023
|
|
||||||
|
|
||||||
·
|
|
||||||
|
|
||||||
Powered by <a href="https://gohugo.io/">Hugo</a> & <a href="https://github.com/luizdepra/hugo-coder/">Coder</a>.
|
|
||||||
|
|
||||||
</section>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<script src="Part%200%20-%20Setting%20Up%20%C2%B7%20Roguelike%20Tutorials_files/coder.min.236049395dc3682fb2719640872958e12f1f24067bb09c327b2.js" integrity="sha256-I2BJOV3DaC+ycZZAhylY4S8fJAZ7sJwyeyM+YpDH7aw="></script>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<script src="Part%200%20-%20Setting%20Up%20%C2%B7%20Roguelike%20Tutorials_files/codetabs.min.cc52451e7f25e50f64c1c893826f606d58410d742c214dce.js" integrity="sha256-zFJFHn8l5Q9kwciTgm9gbVhBDXQsIU3OI/tEfJlh8rA="></script>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</body></html>
|
|
|
@ -1 +0,0 @@
|
||||||
const body=document.body,darkModeToggle=document.getElementById("dark-mode-toggle"),darkModeMediaQuery=window.matchMedia("(prefers-color-scheme: dark)");localStorage.getItem("colorscheme")?setTheme(localStorage.getItem("colorscheme")):setTheme(body.classList.contains("colorscheme-light")||body.classList.contains("colorscheme-dark")?body.classList.contains("colorscheme-dark")?"dark":"light":darkModeMediaQuery.matches?"dark":"light"),darkModeToggle&&darkModeToggle.addEventListener("click",()=>{let e=body.classList.contains("colorscheme-dark")?"light":"dark";setTheme(e),rememberTheme(e)}),darkModeMediaQuery.addListener(e=>{setTheme(e.matches?"dark":"light")}),document.addEventListener("DOMContentLoaded",function(){let e=document.querySelector(".preload-transitions");e.classList.remove("preload-transitions")});function setTheme(e){body.classList.remove("colorscheme-auto");let n=e==="dark"?"light":"dark";body.classList.remove("colorscheme-"+n),body.classList.add("colorscheme-"+e),document.documentElement.style["color-scheme"]=e;function t(e){return new Promise(t=>{if(document.querySelector(e))return t(document.querySelector(e));const n=new MutationObserver(s=>{document.querySelector(e)&&(t(document.querySelector(e)),n.disconnect())});n.observe(document.body,{childList:!0,subtree:!0})})}if(e==="dark"){const e={type:"set-theme",theme:"github-dark"};t(".utterances-frame").then(t=>{t.contentWindow.postMessage(e,"https://utteranc.es")})}else{const e={type:"set-theme",theme:"github-light"};t(".utterances-frame").then(t=>{t.contentWindow.postMessage(e,"https://utteranc.es")})}}function rememberTheme(e){localStorage.setItem("colorscheme",e)}
|
|
|
@ -1 +0,0 @@
|
||||||
var allTabs=document.querySelectorAll("[data-toggle-tab]"),allPanes=document.querySelectorAll("[data-pane]");function toggleTabs(e){if(e.target){e.preventDefault();var n,s,o=e.currentTarget,t=o.getAttribute("data-toggle-tab")}else t=e;window.localStorage&&window.localStorage.setItem("configLangPref",t),n=document.querySelectorAll("[data-toggle-tab='"+t+"']"),s=document.querySelectorAll("[data-pane='"+t+"']");for(let e=0;e<allTabs.length;e++)allTabs[e].classList.remove("active"),allPanes[e].classList.remove("active");for(let e=0;e<n.length;e++)n[e].classList.add("active"),s[e].classList.add("active")}for(let e=0;e<allTabs.length;e++)allTabs[e].addEventListener("click",toggleTabs);window.localStorage.getItem("configLangPref")&&toggleTabs(window.localStorage.getItem("configLangPref"))
|
|
Before Width: | Height: | Size: 8.2 KiB |
|
@ -1 +0,0 @@
|
||||||
pre{border:1px solid #000;padding:15px;background-color:#272822;color:#f8f8f2;background-color:#272822}.language-diff,.language-py3{background-color:#272822!important}body.colorscheme-dart code{background-color:#272822!important}.crossed-out-text{color:#f92672;text-decoration:line-through}.new-text{color:#a6e22e}.data-pane{display:none}.data-pane.active{display:inline}
|
|
|
@ -1,704 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en" style="color-scheme: dark;"><head>
|
|
||||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
|
||||||
<title>
|
|
||||||
Part 1 - Drawing the '@' symbol and moving it around · Roguelike Tutorials
|
|
||||||
</title>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta name="color-scheme" content="light dark">
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<meta name="description" content="Welcome to part 1 of this tutorial! This series will help you create your very first roguelike game, written in Python!
|
|
||||||
This tutorial is largely based off the one found on Roguebasin. Many of the design decisions were mainly to keep this tutorial in lockstep with that one (at least in terms of chapter composition and general direction). This tutorial would not have been possible without the guidance of those who wrote that tutorial, along with all the wonderful contributors to tcod and python-tcod over the years.">
|
|
||||||
<meta name="keywords" content="">
|
|
||||||
|
|
||||||
<meta name="twitter:card" content="summary">
|
|
||||||
<meta name="twitter:title" content="Part 1 - Drawing the '@' symbol and moving it around">
|
|
||||||
<meta name="twitter:description" content="Welcome to part 1 of this tutorial! This series will help you create your very first roguelike game, written in Python!
|
|
||||||
This tutorial is largely based off the one found on Roguebasin. Many of the design decisions were mainly to keep this tutorial in lockstep with that one (at least in terms of chapter composition and general direction). This tutorial would not have been possible without the guidance of those who wrote that tutorial, along with all the wonderful contributors to tcod and python-tcod over the years.">
|
|
||||||
|
|
||||||
<meta property="og:title" content="Part 1 - Drawing the '@' symbol and moving it around">
|
|
||||||
<meta property="og:description" content="Welcome to part 1 of this tutorial! This series will help you create your very first roguelike game, written in Python!
|
|
||||||
This tutorial is largely based off the one found on Roguebasin. Many of the design decisions were mainly to keep this tutorial in lockstep with that one (at least in terms of chapter composition and general direction). This tutorial would not have been possible without the guidance of those who wrote that tutorial, along with all the wonderful contributors to tcod and python-tcod over the years.">
|
|
||||||
<meta property="og:type" content="article">
|
|
||||||
<meta property="og:url" content="https://rogueliketutorials.com/tutorials/tcod/v2/part-1/"><meta property="article:section" content="tutorials">
|
|
||||||
<meta property="article:published_time" content="2020-06-14T11:35:26-07:00">
|
|
||||||
<meta property="article:modified_time" content="2020-06-14T11:35:26-07:00">
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<link rel="canonical" href="https://rogueliketutorials.com/tutorials/tcod/v2/part-1/">
|
|
||||||
|
|
||||||
|
|
||||||
<link rel="preload" href="https://rogueliketutorials.com/fonts/forkawesome-webfont.woff2?v=1.2.0" as="font" type="font/woff2" crossorigin="">
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="Part%201%20-%20Drawing%20the%20'@'%20symbol%20and%20moving%20it%20around%20%C2%B7%20Roguelike%20Tutorials_files/coder.min.c4d7e93a158eda5a65b3df343745d2092a0a1e2170feeec909.css" integrity="sha256-xNfpOhWO2lpls980N0XSCSoKHiFw/u7JCbiolEOQPGo=" crossorigin="anonymous" media="screen">
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="Part%201%20-%20Drawing%20the%20'@'%20symbol%20and%20moving%20it%20around%20%C2%B7%20Roguelike%20Tutorials_files/coder-dark.min.78b5fe3864945faf5207fb8fe3ab2320d49c3365def0e.css" integrity="sha256-eLX+OGSUX69SB/uP46sjINScM2Xe8OiKwd8N2txUoDw=" crossorigin="anonymous" media="screen">
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="Part%201%20-%20Drawing%20the%20'@'%20symbol%20and%20moving%20it%20around%20%C2%B7%20Roguelike%20Tutorials_files/style.min.9d3eb202952dddb888856ff12c83bc88de866c596286bfb4c1.css" integrity="sha256-nT6yApUt3biIhW/xLIO8iN6GbFlihr+0wfjmvq2a42Y=" crossorigin="anonymous" media="screen">
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<link rel="icon" type="image/png" href="https://rogueliketutorials.com/images/favicon-32x32.png" sizes="32x32">
|
|
||||||
<link rel="icon" type="image/png" href="https://rogueliketutorials.com/images/favicon-16x16.png" sizes="16x16">
|
|
||||||
|
|
||||||
<link rel="apple-touch-icon" href="https://rogueliketutorials.com/images/apple-touch-icon.png">
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="https://rogueliketutorials.com/images/apple-touch-icon.png">
|
|
||||||
|
|
||||||
<link rel="manifest" href="https://rogueliketutorials.com/site.webmanifest">
|
|
||||||
<link rel="mask-icon" href="https://rogueliketutorials.com/images/safari-pinned-tab.svg" color="#5bbad5">
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<meta name="generator" content="Hugo 0.110.0">
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<style>:is([id*='google_ads_iframe'],[id*='taboola-'],.taboolaHeight,.taboola-placeholder,#top-ad,#credential_picker_container,#credentials-picker-container,#credential_picker_iframe,[id*='google-one-tap-iframe'],#google-one-tap-popup-container,.google-one-tap__module,.google-one-tap-modal-div,#amp_floatingAdDiv,#ez-content-blocker-container) {display:none!important;min-height:0!important;height:0!important;}</style></head>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<body class="colorscheme-dark vsc-initialized">
|
|
||||||
|
|
||||||
<div class="float-container">
|
|
||||||
<a id="dark-mode-toggle" class="colorscheme-toggle">
|
|
||||||
<i class="fa fa-adjust fa-fw" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<main class="wrapper">
|
|
||||||
<nav class="navigation">
|
|
||||||
<section class="container">
|
|
||||||
<a class="navigation-title" href="https://rogueliketutorials.com/">
|
|
||||||
Roguelike Tutorials
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<input type="checkbox" id="menu-toggle">
|
|
||||||
<label class="menu-button float-right" for="menu-toggle">
|
|
||||||
<i class="fa fa-bars fa-fw" aria-hidden="true"></i>
|
|
||||||
</label>
|
|
||||||
<ul class="navigation-list">
|
|
||||||
|
|
||||||
|
|
||||||
<li class="navigation-item">
|
|
||||||
<a class="navigation-link" href="https://rogueliketutorials.com/">Home</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="navigation-item">
|
|
||||||
<a class="navigation-link" href="https://rogueliketutorials.com/tutorials/tcod/v2/">TCOD Tutorial (2020)</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="navigation-item">
|
|
||||||
<a class="navigation-link" href="https://rogueliketutorials.com/tutorials/tcod/2019/">TCOD Tutorial (2019)</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="navigation-item">
|
|
||||||
<a class="navigation-link" href="https://rogueliketutorials.com/about/">About</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
</section>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="content">
|
|
||||||
|
|
||||||
<section class="container page">
|
|
||||||
<article>
|
|
||||||
<header>
|
|
||||||
<h1 class="title">
|
|
||||||
<a class="title-link" href="https://rogueliketutorials.com/tutorials/tcod/v2/part-1/">
|
|
||||||
Part 1 - Drawing the '@' symbol and moving it around
|
|
||||||
</a>
|
|
||||||
</h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<p>Welcome to part 1 of this tutorial! This series will help you create your very first roguelike game, written in Python!</p>
|
|
||||||
<p>This tutorial is largely based off the <a href="http://www.roguebasin.com/index.php?title=Complete_Roguelike_Tutorial,_using_python%2Blibtcod">one found on Roguebasin</a>.
|
|
||||||
Many of the design decisions were mainly to keep this tutorial in
|
|
||||||
lockstep
|
|
||||||
with that one (at least in terms of chapter composition and general
|
|
||||||
direction). This tutorial would not have been possible without the
|
|
||||||
guidance of those who wrote that tutorial, along with all the wonderful
|
|
||||||
contributors to tcod and python-tcod over the years.</p>
|
|
||||||
<p>This part assumes that you have either checked <a href="https://rogueliketutorials.com/tutorials/tcod/part-0">Part 0</a>
|
|
||||||
and are already set up and ready to go. If not, be sure to check that
|
|
||||||
page, and make sure that you’ve got Python and TCOD installed, and a
|
|
||||||
file called <code>main.py</code> created in the directory that you want to work in.</p>
|
|
||||||
<p>Assuming that you’ve done all that, let’s get started. Modify (or create, if you haven’t already) the file <code>main.py</code> to look like this:</p>
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span><span style="color:#75715e">#!/usr/bin/env python3</span>
|
|
||||||
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> tcod
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">main</span>():
|
|
||||||
</span></span><span style="display:flex;"><span> print(<span style="color:#e6db74">"Hello World!"</span>)
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> __name__ <span style="color:#f92672">==</span> <span style="color:#e6db74">"__main__"</span>:
|
|
||||||
</span></span><span style="display:flex;"><span> main()</span></span></code></pre></div>
|
|
||||||
<p>You can run the program like any other Python program, but for those who are brand new, you do that by typing <code>python main.py</code> in the terminal. If you have both Python 2 and 3 installed on your machine, you might have to use <code>python3 main.py</code> to run (it depends on your default python, and whether you’re using a virtualenv or not).</p>
|
|
||||||
<p>Alternatively, because of the first line, <code>#!usr/bin/env python</code>, you can run the program by typing <code>./main.py</code>,
|
|
||||||
assuming you’ve either activated your virtual environment, or installed
|
|
||||||
tcod on your base Python installation. This line is called a “shebang”.</p>
|
|
||||||
<p>Okay, not the most exciting program in the world, I admit, but we’ve
|
|
||||||
already got our first major difference from the other tutorial. Namely,
|
|
||||||
this funky looking thing here:</p>
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span><span style="color:#66d9ef">if</span> __name__ <span style="color:#f92672">==</span> <span style="color:#e6db74">"__main__"</span>:
|
|
||||||
</span></span><span style="display:flex;"><span> main()</span></span></code></pre></div>
|
|
||||||
<p>So what does that do? Basically, we’re saying that we’re only going
|
|
||||||
to run the “main” function when we explicitly run the script, using <code>python main.py</code>. It’s not super important that you understand this now, but if you want a more detailed explanation, <a href="https://stackoverflow.com/a/419185">this answer on Stack Overflow</a> gives a pretty good overview.</p>
|
|
||||||
<p>Confirm that the above program runs (if not, there’s probably an
|
|
||||||
issue with your tcod setup). Once that’s done, we can move on to bigger
|
|
||||||
and better things. The first major step to creating any roguelike is
|
|
||||||
getting an ‘@’ character on the screen and moving, so let’s get started
|
|
||||||
with that.</p>
|
|
||||||
<p>Modify <code>main.py</code> to look like this:</p>
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span><span style="color:#75715e">#!/usr/bin/env python3</span>
|
|
||||||
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> tcod
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">main</span>() <span style="color:#f92672">-></span> <span style="color:#66d9ef">None</span>:
|
|
||||||
</span></span><span style="display:flex;"><span> screen_width <span style="color:#f92672">=</span> <span style="color:#ae81ff">80</span>
|
|
||||||
</span></span><span style="display:flex;"><span> screen_height <span style="color:#f92672">=</span> <span style="color:#ae81ff">50</span>
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span> tileset <span style="color:#f92672">=</span> tcod<span style="color:#f92672">.</span>tileset<span style="color:#f92672">.</span>load_tilesheet(
|
|
||||||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"dejavu10x10_gs_tc.png"</span>, <span style="color:#ae81ff">32</span>, <span style="color:#ae81ff">8</span>, tcod<span style="color:#f92672">.</span>tileset<span style="color:#f92672">.</span>CHARMAP_TCOD
|
|
||||||
</span></span><span style="display:flex;"><span> )
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">with</span> tcod<span style="color:#f92672">.</span>context<span style="color:#f92672">.</span>new_terminal(
|
|
||||||
</span></span><span style="display:flex;"><span> screen_width,
|
|
||||||
</span></span><span style="display:flex;"><span> screen_height,
|
|
||||||
</span></span><span style="display:flex;"><span> tileset<span style="color:#f92672">=</span>tileset,
|
|
||||||
</span></span><span style="display:flex;"><span> title<span style="color:#f92672">=</span><span style="color:#e6db74">"Yet Another Roguelike Tutorial"</span>,
|
|
||||||
</span></span><span style="display:flex;"><span> vsync<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
|
|
||||||
</span></span><span style="display:flex;"><span> ) <span style="color:#66d9ef">as</span> context:
|
|
||||||
</span></span><span style="display:flex;"><span> root_console <span style="color:#f92672">=</span> tcod<span style="color:#f92672">.</span>Console(screen_width, screen_height, order<span style="color:#f92672">=</span><span style="color:#e6db74">"F"</span>)
|
|
||||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">while</span> <span style="color:#66d9ef">True</span>:
|
|
||||||
</span></span><span style="display:flex;"><span> root_console<span style="color:#f92672">.</span>print(x<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>, y<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>, string<span style="color:#f92672">=</span><span style="color:#e6db74">"@"</span>)
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span> context<span style="color:#f92672">.</span>present(root_console)
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> event <span style="color:#f92672">in</span> tcod<span style="color:#f92672">.</span>event<span style="color:#f92672">.</span>wait():
|
|
||||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> event<span style="color:#f92672">.</span>type <span style="color:#f92672">==</span> <span style="color:#e6db74">"QUIT"</span>:
|
|
||||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">raise</span> <span style="color:#a6e22e">SystemExit</span>()
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> __name__ <span style="color:#f92672">==</span> <span style="color:#e6db74">"__main__"</span>:
|
|
||||||
</span></span><span style="display:flex;"><span> main()</span></span></code></pre></div>
|
|
||||||
<p>Run <code>main.py</code> again, and you should see an ‘@’ symbol on
|
|
||||||
the screen. Once you’ve fully soaked in the glory on the screen in front
|
|
||||||
of you, you can click the “X” in the top-left corner of the program to
|
|
||||||
close it.</p>
|
|
||||||
<p>There’s a lot going on here, so let’s break it down line by line.</p>
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> screen_width <span style="color:#f92672">=</span> <span style="color:#ae81ff">80</span>
|
|
||||||
</span></span><span style="display:flex;"><span> screen_height <span style="color:#f92672">=</span> <span style="color:#ae81ff">50</span></span></span></code></pre></div>
|
|
||||||
<p>This is simple enough. We’re defining some variables for the screen size.</p>
|
|
||||||
<p>Eventually, we’ll load these values from a JSON file rather than hard
|
|
||||||
coding them in the source, but we won’t worry about that until we have
|
|
||||||
some more variables like this.</p>
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> tileset <span style="color:#f92672">=</span> tcod<span style="color:#f92672">.</span>tileset<span style="color:#f92672">.</span>load_tilesheet(
|
|
||||||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"dejavu10x10_gs_tc.png"</span>, <span style="color:#ae81ff">32</span>, <span style="color:#ae81ff">8</span>, tcod<span style="color:#f92672">.</span>tileset<span style="color:#f92672">.</span>CHARMAP_TCOD
|
|
||||||
</span></span><span style="display:flex;"><span> )</span></span></code></pre></div>
|
|
||||||
<p>Here, we’re telling tcod which font to use. The <code>"dejavu10x10_gs_tc.png"</code> bit is the actual file we’re reading from (this should exist in your project folder).</p>
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> <span style="color:#66d9ef">with</span> tcod<span style="color:#f92672">.</span>context<span style="color:#f92672">.</span>new_terminal(
|
|
||||||
</span></span><span style="display:flex;"><span> screen_width,
|
|
||||||
</span></span><span style="display:flex;"><span> screen_height,
|
|
||||||
</span></span><span style="display:flex;"><span> tileset<span style="color:#f92672">=</span>tileset
|
|
||||||
</span></span><span style="display:flex;"><span> title<span style="color:#f92672">=</span><span style="color:#e6db74">"Yet Another Roguelike Tutorial"</span>,
|
|
||||||
</span></span><span style="display:flex;"><span> vsync<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
|
|
||||||
</span></span><span style="display:flex;"><span> ) <span style="color:#66d9ef">as</span> context:</span></span></code></pre></div>
|
|
||||||
<p>This part is what actually creates the screen. We’re giving it the <code>screen_width</code> and <code>screen_height</code>
|
|
||||||
values from before (80 and 50, respectively), along with a title
|
|
||||||
(change this if you’ve already got your game’s name figured out). <code>tileset</code> uses the tileset we defined earlier. and <code>vsync</code> will either enable or disable vsync, which shouldn’t matter too much in our case.</p>
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> root_console <span style="color:#f92672">=</span> tcod<span style="color:#f92672">.</span>Console(screen_width, screen_height, order<span style="color:#f92672">=</span><span style="color:#e6db74">"F"</span>)</span></span></code></pre></div>
|
|
||||||
<p>This creates our “console” which is what we’ll be drawing to. We also
|
|
||||||
set this console’s width and height to the same as our new terminal.
|
|
||||||
The “order” argument affects the order of our x and y variables in numpy
|
|
||||||
(an underlying library that tcod uses). By default, numpy accesses 2D
|
|
||||||
arrays in [y, x] order, which is fairly unintuitive. By setting <code>order="F"</code>, we can change this to be [x, y] instead. This will make more sense once we start drawing the map.</p>
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> <span style="color:#66d9ef">while</span> <span style="color:#66d9ef">True</span>:</span></span></code></pre></div>
|
|
||||||
<p>This is what’s called our ‘game loop’. Basically, this is a loop that
|
|
||||||
won’t ever end, until we close the screen. Every game has some sort of
|
|
||||||
game loop or another.</p>
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> root_console<span style="color:#f92672">.</span>print(x<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>, y<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>, string<span style="color:#f92672">=</span><span style="color:#e6db74">"@"</span>)</span></span></code></pre></div>
|
|
||||||
<p>This line is what tells the program to actually put the “@” symbol on the screen in its proper place. We’re telling the <code>root_console</code> we created to <code>print</code> the “@” symbol at the given x and y coordinates. Try changing the x and y values and see what happens, if you feel so inclined.</p>
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> context<span style="color:#f92672">.</span>present(root_console)</span></span></code></pre></div>
|
|
||||||
<p>Without this line, nothing would actually print out on the screen. This is because <code>context.present</code> is what actually updates the screen with what we’ve told it to display so far.</p>
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> event <span style="color:#f92672">in</span> tcod<span style="color:#f92672">.</span>event<span style="color:#f92672">.</span>wait():
|
|
||||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> event<span style="color:#f92672">.</span>type <span style="color:#f92672">==</span> <span style="color:#e6db74">"QUIT"</span>:
|
|
||||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">raise</span> <span style="color:#a6e22e">SystemExit</span>()</span></span></code></pre></div>
|
|
||||||
<p>This part gives us a way to gracefully exit (i.e. not crashing) the program by hitting the <code>X</code> button in the console’s window. The line <code>for event in tcod.event.wait()</code>
|
|
||||||
will wait for some sort of input from the user (mouse clicks, keyboard
|
|
||||||
strokes, etc.) and loop through each event that happened. <code>SystemExit()</code> tells Python to quit the current running program.</p>
|
|
||||||
<p>Alright, our “@” symbol is successfully displayed on the screen, but
|
|
||||||
we can’t rest just yet. We still need to get it moving around!</p>
|
|
||||||
<p>We need to keep track of the player’s position at all times. Since
|
|
||||||
this is a 2D game, we can express this in two data points: the <code>x</code> and <code>y</code> coordinates. Let’s create two variables, <code>player_x</code> and <code>player_y</code>, to keep track of this.</p>
|
|
||||||
<div>
|
|
||||||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
|
||||||
Diff
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
|
||||||
Original
|
|
||||||
</button>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="data-pane active" data-pane="diff">
|
|
||||||
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span> ...
|
|
||||||
</span></span><span style="display:flex;"><span> screen_height = 50
|
|
||||||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+
|
|
||||||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ player_x = int(screen_width / 2)
|
|
||||||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ player_y = int(screen_height / 2)
|
|
||||||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+
|
|
||||||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> tileset = tcod.tileset.load_tilesheet(
|
|
||||||
</span></span><span style="display:flex;"><span> "dejavu10x10_gs_tc.png", 32, 8, tcod.tileset.CHARMAP_TCOD
|
|
||||||
</span></span><span style="display:flex;"><span> )
|
|
||||||
</span></span><span style="display:flex;"><span> ...
|
|
||||||
</span></span></code></pre></div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="data-pane" data-pane="original">
|
|
||||||
|
|
||||||
<pre> ...
|
|
||||||
screen_height = 50
|
|
||||||
<span class="new-text">
|
|
||||||
player_x = int(screen_width / 2)
|
|
||||||
player_y = int(screen_height / 2)
|
|
||||||
</span>
|
|
||||||
tileset = tcod.tileset.load_tilesheet(
|
|
||||||
"dejavu10x10_gs_tc.png", 32, 8, tcod.tileset.CHARMAP_TCOD
|
|
||||||
)
|
|
||||||
...</pre>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p><em>Note: Ellipses denote omitted parts of the code. I’ll include
|
|
||||||
lines around the code to be inserted so that you’ll know exactly where
|
|
||||||
to put new pieces of code, but I won’t be showing the entire file every
|
|
||||||
time. The green lines denote code that you should be adding.</em></p>
|
|
||||||
<p>We’re placing the player right in the middle of the screen. What’s with the <code>int()</code>
|
|
||||||
function though? Well, Python 3 doesn’t automatically
|
|
||||||
truncate division like Python 2 does, so we have to cast the division
|
|
||||||
result (a float) to an integer. If we don’t, tcod will give an error.</p>
|
|
||||||
<p><em>Note: It’s been pointed out that you could divide with <code>//</code> instead of <code>/</code>
|
|
||||||
and achieve the same effect. This is true, except in cases where, for
|
|
||||||
whatever reason, one of the numbers given is a decimal. For example, <code>screen_width // 2.0</code> will give an error. That shouldn’t happen in this case, but wrapping the function in <code>int()</code> gives us certainty that this won’t ever happen.</em></p>
|
|
||||||
<p>We also have to modify the command to put the ‘@’ symbol to use these new coordinates.</p>
|
|
||||||
<div>
|
|
||||||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
|
||||||
Diff
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
|
||||||
Original
|
|
||||||
</button>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="data-pane active" data-pane="diff">
|
|
||||||
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span> ...
|
|
||||||
</span></span><span style="display:flex;"><span> while True:
|
|
||||||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- root_console.print(x=1, y=1, string="@")
|
|
||||||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ root_console.print(x=player_x, y=player_y, string="@")
|
|
||||||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
|
||||||
</span></span><span style="display:flex;"><span> context.present(root_console)
|
|
||||||
</span></span><span style="display:flex;"><span> ...
|
|
||||||
</span></span></code></pre></div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="data-pane" data-pane="original">
|
|
||||||
|
|
||||||
<pre> ...
|
|
||||||
while True:
|
|
||||||
<span class="crossed-out-text">root_console.print(x=1, y=1, string="@")</span>
|
|
||||||
<span class="new-text">root_console.print(x=player_x, y=player_y, string="@")</span>
|
|
||||||
|
|
||||||
context.present(root_console)
|
|
||||||
...</pre>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p><em>Note: The red lines denote code that has been removed.</em></p>
|
|
||||||
<p>Run the code now and you should see the ‘@’ in the center of the screen. Let’s take care of moving it around now.</p>
|
|
||||||
<p>So, how do we actually capture the user’s input? TCOD makes this
|
|
||||||
pretty easy, and in fact, we’re already doing it. This line takes care
|
|
||||||
of it for us:</p>
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> event <span style="color:#f92672">in</span> tcod<span style="color:#f92672">.</span>event<span style="color:#f92672">.</span>wait():</span></span></code></pre></div>
|
|
||||||
<p>It gets the “events”, which we can then process. Events range from
|
|
||||||
mouse movements to keyboard strokes. Let’s start by getting some basic
|
|
||||||
keyboard commands and processing them, and based on what we get, we’ll
|
|
||||||
move our little “@” symbol around.</p>
|
|
||||||
<p>We <em>could</em> identify which key is being pressed right here in <code>main.py</code>,
|
|
||||||
but this is a good opportunity to break our project up a little bit.
|
|
||||||
Sooner or later, we’re going to have quite a few potential keyboard
|
|
||||||
commands, so putting them all in <code>main.py</code> would make the file longer than it needs to be. Maybe we should import what we need into <code>main.py</code> rather than writing it all there.</p>
|
|
||||||
<p>To handle the keyboard inputs and the actions associated with them, let’s actually create <em>two</em>
|
|
||||||
new files. One will hold the different types of “actions” our rogue can
|
|
||||||
perform, and the other will bridge the gap between the keys we press
|
|
||||||
and those actions.</p>
|
|
||||||
<p>Create two new Python files in your project’s directory, one called <code>input_handlers.py</code>, and the other called <code>actions.py</code>. Let’s fill out <code>actions.py</code> first:</p>
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Action</span>:
|
|
||||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">pass</span>
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">EscapeAction</span>(Action):
|
|
||||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">pass</span>
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">MovementAction</span>(Action):
|
|
||||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> __init__(self, dx: int, dy: int):
|
|
||||||
</span></span><span style="display:flex;"><span> super()<span style="color:#f92672">.</span>__init__()
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>dx <span style="color:#f92672">=</span> dx
|
|
||||||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>dy <span style="color:#f92672">=</span> dy</span></span></code></pre></div>
|
|
||||||
<p>We define three classes: <code>Action</code>, <code>EscapeAction</code>, and <code>MovementAction</code>. <code>EscapeAction</code> and <code>MovementAction</code> are subclasses of <code>Action</code>.</p>
|
|
||||||
<p>So what’s the plan for these classes? Basically, whenever we have an “action”, we’ll use one of the subclasses of <code>Action</code> to describe it. We’ll be able to detect which subclass we’re using, and respond accordingly. In this case, <code>EscapeAction</code> will be when we hit the <code>Esc</code> key (to exit the game), and <code>MovementAction</code> will be used to describe our player moving around.</p>
|
|
||||||
<p>There might be instances where we need to know more than just the “type” of action, like in the case of <code>MovementAction</code>. There, we need to know not only that we’re trying to move, but in which direction. Therefore, we can pass the <code>dx</code> and <code>dy</code> arguments to <code>MovementAction</code>, which will tell us where the player is trying to move to. Other <code>Action</code> subclasses might contain additional data as well, and others might just be subclasses with nothing else in them, like <code>EscapeAction</code>.</p>
|
|
||||||
<p>That’s all we need to do in <code>actions.py</code> right now. Let’s fill out <code>input_handlers.py</code>, which will use the <code>Action</code> class and subclasses we just created:</p>
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span><span style="color:#f92672">from</span> typing <span style="color:#f92672">import</span> Optional
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> tcod.event
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> actions <span style="color:#f92672">import</span> Action, EscapeAction, MovementAction
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">EventHandler</span>(tcod<span style="color:#f92672">.</span>event<span style="color:#f92672">.</span>EventDispatch[Action]):
|
|
||||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">ev_quit</span>(self, event: tcod<span style="color:#f92672">.</span>event<span style="color:#f92672">.</span>Quit) <span style="color:#f92672">-></span> Optional[Action]:
|
|
||||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">raise</span> <span style="color:#a6e22e">SystemExit</span>()
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">ev_keydown</span>(self, event: tcod<span style="color:#f92672">.</span>event<span style="color:#f92672">.</span>KeyDown) <span style="color:#f92672">-></span> Optional[Action]:
|
|
||||||
</span></span><span style="display:flex;"><span> action: Optional[Action] <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span> key <span style="color:#f92672">=</span> event<span style="color:#f92672">.</span>sym
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> key <span style="color:#f92672">==</span> tcod<span style="color:#f92672">.</span>event<span style="color:#f92672">.</span>K_UP:
|
|
||||||
</span></span><span style="display:flex;"><span> action <span style="color:#f92672">=</span> MovementAction(dx<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>, dy<span style="color:#f92672">=-</span><span style="color:#ae81ff">1</span>)
|
|
||||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">elif</span> key <span style="color:#f92672">==</span> tcod<span style="color:#f92672">.</span>event<span style="color:#f92672">.</span>K_DOWN:
|
|
||||||
</span></span><span style="display:flex;"><span> action <span style="color:#f92672">=</span> MovementAction(dx<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>, dy<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>)
|
|
||||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">elif</span> key <span style="color:#f92672">==</span> tcod<span style="color:#f92672">.</span>event<span style="color:#f92672">.</span>K_LEFT:
|
|
||||||
</span></span><span style="display:flex;"><span> action <span style="color:#f92672">=</span> MovementAction(dx<span style="color:#f92672">=-</span><span style="color:#ae81ff">1</span>, dy<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>)
|
|
||||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">elif</span> key <span style="color:#f92672">==</span> tcod<span style="color:#f92672">.</span>event<span style="color:#f92672">.</span>K_RIGHT:
|
|
||||||
</span></span><span style="display:flex;"><span> action <span style="color:#f92672">=</span> MovementAction(dx<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>, dy<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>)
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">elif</span> key <span style="color:#f92672">==</span> tcod<span style="color:#f92672">.</span>event<span style="color:#f92672">.</span>K_ESCAPE:
|
|
||||||
</span></span><span style="display:flex;"><span> action <span style="color:#f92672">=</span> EscapeAction()
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># No valid key was pressed</span>
|
|
||||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> action</span></span></code></pre></div>
|
|
||||||
<p>Let’s go over what we’ve added.</p>
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span><span style="color:#f92672">from</span> typing <span style="color:#f92672">import</span> Optional</span></span></code></pre></div>
|
|
||||||
<p>This is part of Python’s type hinting system (which you don’t have to include in your project). <code>Optional</code> denotes something that could be set to <code>None</code>.</p>
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span><span style="color:#f92672">import</span> tcod.event
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> actions <span style="color:#f92672">import</span> Action, EscapeAction, MovementAction</span></span></code></pre></div>
|
|
||||||
<p>We’re importing <code>tcod.event</code> so that we can use tcod’s event system. We don’t need to import <code>tcod</code>, as we only need the contents of <code>event</code>.</p>
|
|
||||||
<p>The next line imports the <code>Action</code> class and its subclasses that we just created.</p>
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">EventHandler</span>(tcod<span style="color:#f92672">.</span>event<span style="color:#f92672">.</span>EventDispatch[Action]):</span></span></code></pre></div>
|
|
||||||
<p>We’re creating a class called <code>EventHandler</code>, which is a subclass of tcod’s <code>EventDispatch</code> class. <code>EventDispatch</code>
|
|
||||||
is a class that allows us to send an event to its proper method based
|
|
||||||
on what type of event it is. Let’s take a look at the methods we’re
|
|
||||||
creating for <code>EventHandler</code> to see a few examples of this.</p>
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">ev_quit</span>(self, event: tcod<span style="color:#f92672">.</span>event<span style="color:#f92672">.</span>Quit) <span style="color:#f92672">-></span> Optional[Action]:
|
|
||||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">raise</span> <span style="color:#a6e22e">SystemExit</span>()</span></span></code></pre></div>
|
|
||||||
<p>Here’s an example of us using a method of <code>EventDispatch</code>: <code>ev_quit</code> is a method defined in <code>EventDispatch</code>, which we’re overriding in <code>EventHandler</code>. <code>ev_quit</code>
|
|
||||||
is called when we receive a “quit” event, which happens when we click
|
|
||||||
the “X” in the window of the program. In that case, we want to quit the
|
|
||||||
program, so we raise <code>SystemExit()</code> to do so.</p>
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">ev_keydown</span>(self, event: tcod<span style="color:#f92672">.</span>event<span style="color:#f92672">.</span>KeyDown) <span style="color:#f92672">-></span> Optional[Action]:</span></span></code></pre></div>
|
|
||||||
<p>This method will receive key press events, and return either an <code>Action</code> subclass, or <code>None</code>, if no valid key was pressed.</p>
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> action: Optional[Action] <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span> key <span style="color:#f92672">=</span> event<span style="color:#f92672">.</span>sym</span></span></code></pre></div>
|
|
||||||
<p><code>action</code> is the variable that will hold whatever subclass of <code>Action</code> we end up assigning it to. If no valid key press is found, it will remain set to <code>None</code>. We’ll return it either way.</p>
|
|
||||||
<p><code>key</code> holds the actual key we pressed. It doesn’t contain additional information about modifiers like <code>Shift</code> or <code>Alt</code>, just the actual key that was pressed. That’s all we need right now.</p>
|
|
||||||
<p>From there, we go down a list of possible keys pressed. For example:</p>
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> key <span style="color:#f92672">==</span> tcod<span style="color:#f92672">.</span>event<span style="color:#f92672">.</span>K_UP:
|
|
||||||
</span></span><span style="display:flex;"><span> action <span style="color:#f92672">=</span> MovementAction(dx<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>, dy<span style="color:#f92672">=-</span><span style="color:#ae81ff">1</span>)</span></span></code></pre></div>
|
|
||||||
<p>In this case, the user pressed the up-arrow key, so we’re creating a <code>MovementAction</code>. Notice that here (and in all the other cases of <code>MovementAction</code>) we provide <code>dx</code> and <code>dy</code>. These describe which direction our character will move in.</p>
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> <span style="color:#66d9ef">elif</span> key <span style="color:#f92672">==</span> tcod<span style="color:#f92672">.</span>event<span style="color:#f92672">.</span>K_ESCAPE:
|
|
||||||
</span></span><span style="display:flex;"><span> action <span style="color:#f92672">=</span> EscapeAction()</span></span></code></pre></div>
|
|
||||||
<p>If the user pressed the “Escape” key, we return <code>EscapeAction</code>. We’ll use this to exit the game for now, though in the future, <code>EscapeAction</code> can be used to do things like exit menus.</p>
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> action</span></span></code></pre></div>
|
|
||||||
<p>Whether <code>action</code> is assigned to an <code>Action</code> subclass or <code>None</code>, we return it.</p>
|
|
||||||
<p>Let’s put our new actions and input handlers to use in <code>main.py</code>. Edit <code>main.py</code> like this:</p>
|
|
||||||
<div>
|
|
||||||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
|
||||||
Diff
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
|
||||||
Original
|
|
||||||
</button>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="data-pane active" data-pane="diff">
|
|
||||||
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>#!/usr/bin/env python3
|
|
||||||
</span></span><span style="display:flex;"><span>import tcod
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+from actions import EscapeAction, MovementAction
|
|
||||||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+from input_handlers import EventHandler
|
|
||||||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span>def main() -> None:
|
|
||||||
</span></span><span style="display:flex;"><span> screen_width = 80
|
|
||||||
</span></span><span style="display:flex;"><span> screen_height = 50
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span> player_x = int(screen_width / 2)
|
|
||||||
</span></span><span style="display:flex;"><span> player_y = int(screen_height / 2)
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span> tileset = tcod.tileset.load_tilesheet(
|
|
||||||
</span></span><span style="display:flex;"><span> "dejavu10x10_gs_tc.png", 32, 8, tcod.tileset.CHARMAP_TCOD
|
|
||||||
</span></span><span style="display:flex;"><span> )
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ event_handler = EventHandler()
|
|
||||||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
|
||||||
</span></span><span style="display:flex;"><span> with tcod.context.new_terminal(
|
|
||||||
</span></span><span style="display:flex;"><span> ...
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span> ...
|
|
||||||
</span></span><span style="display:flex;"><span> for event in tcod.event.wait():
|
|
||||||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- if event.type == "QUIT":
|
|
||||||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- raise SystemExit()
|
|
||||||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
|
|
||||||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ action = event_handler.dispatch(event)
|
|
||||||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
|
||||||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if action is None:
|
|
||||||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ continue
|
|
||||||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
|
||||||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if isinstance(action, MovementAction):
|
|
||||||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ player_x += action.dx
|
|
||||||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ player_y += action.dy
|
|
||||||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
|
||||||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ elif isinstance(action, EscapeAction):
|
|
||||||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ raise SystemExit()
|
|
||||||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span>if __name__ == "__main__":
|
|
||||||
</span></span><span style="display:flex;"><span> main()
|
|
||||||
</span></span></code></pre></div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="data-pane" data-pane="original">
|
|
||||||
|
|
||||||
<pre>#!/usr/bin/env python3
|
|
||||||
import tcod
|
|
||||||
|
|
||||||
<span class="new-text">from actions import EscapeAction, MovementAction
|
|
||||||
from input_handlers import EventHandler</span>
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
screen_width = 80
|
|
||||||
screen_height = 50
|
|
||||||
|
|
||||||
player_x = int(screen_width / 2)
|
|
||||||
player_y = int(screen_height / 2)
|
|
||||||
|
|
||||||
tileset = tcod.tileset.load_tilesheet(
|
|
||||||
"dejavu10x10_gs_tc.png", 32, 8, tcod.tileset.CHARMAP_TCOD
|
|
||||||
)
|
|
||||||
|
|
||||||
<span class="new-text">event_handler = EventHandler()</span>
|
|
||||||
|
|
||||||
with tcod.context.new_terminal(
|
|
||||||
...
|
|
||||||
|
|
||||||
...
|
|
||||||
for event in tcod.event.wait():
|
|
||||||
<span class="crossed-out-text">if event.type == "QUIT":</span>
|
|
||||||
<span class="crossed-out-text">raise SystemExit()</span>
|
|
||||||
<span class="new-text">
|
|
||||||
action = event_handler.dispatch(event)
|
|
||||||
|
|
||||||
if action is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if isinstance(action, MovementAction):
|
|
||||||
player_x += action.dx
|
|
||||||
player_y += action.dy
|
|
||||||
|
|
||||||
elif isinstance(action, EscapeAction):
|
|
||||||
raise SystemExit()</span>
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()</pre>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>Let’s break down the new additions a bit.</p>
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span><span style="color:#f92672">from</span> actions <span style="color:#f92672">import</span> EscapeAction, MovementAction
|
|
||||||
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> input_handlers <span style="color:#f92672">import</span> EventHandler</span></span></code></pre></div>
|
|
||||||
<p>We’re importing the <code>EscapeAction</code> and <code>MovementAction</code> from <code>actions</code>, and <code>EventHandler</code> from <code>input_handlers</code>. This allows us to use the functions we wrote in those files in our <code>main</code> file.</p>
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> event_handler <span style="color:#f92672">=</span> EventHandler()</span></span></code></pre></div>
|
|
||||||
<p><code>event_handler</code> is an instance of our <code>EventHandler</code> class. We’ll use it to receive events and process them.</p>
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> action <span style="color:#f92672">=</span> event_handler<span style="color:#f92672">.</span>dispatch(event)</span></span></code></pre></div>
|
|
||||||
<p>We send the <code>event</code> to our <code>event_handler</code>’s “dispatch” method, which sends the event to its proper place. In this case, a keyboard event will be sent to the <code>ev_keydown</code> method we wrote. The <code>Action</code> returned from that method is assigned to our local <code>action</code> variable.</p>
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> action <span style="color:#f92672">is</span> <span style="color:#66d9ef">None</span>:
|
|
||||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">continue</span></span></span></code></pre></div>
|
|
||||||
<p>This is pretty straightforward: If <code>action</code> is <code>None</code>
|
|
||||||
(that is, no key was pressed, or the key pressed isn’t recognized),
|
|
||||||
then we skip over the rest the loop. There’s no need to go any further,
|
|
||||||
since the lines below are going to handle the valid key presses.</p>
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> isinstance(action, MovementAction):
|
|
||||||
</span></span><span style="display:flex;"><span> player_x <span style="color:#f92672">+=</span> action<span style="color:#f92672">.</span>dx
|
|
||||||
</span></span><span style="display:flex;"><span> player_y <span style="color:#f92672">+=</span> action<span style="color:#f92672">.</span>dy</span></span></code></pre></div>
|
|
||||||
<p>Now we arrive at the interesting part. If the <code>action</code> is an instance of the class <code>MovementAction</code>, we need to move our “@” symbol. We grab the <code>dx</code> and <code>dy</code> values we gave to <code>MovementAction</code> earlier, which will move the “@” symbol in which direction we want it to move. <code>dx</code> and <code>dy</code>, as of now, will only ever be -1, 0, or 1. Regardless of what the value is, we add <code>dx</code> and <code>dy</code> to <code>player_x</code> and <code>player_y</code>, respectively. Because the console is using <code>player_x</code> and <code>player_y</code> to draw where our “@” symbol is, modifying these two variables will cause the symbol to move.</p>
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> <span style="color:#66d9ef">elif</span> isinstance(action, EscapeAction):
|
|
||||||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">raise</span> <span style="color:#a6e22e">SystemExit</span>()</span></span></code></pre></div>
|
|
||||||
<p><code>raise SystemExit()</code> should look familiar: it’s how we’re quitting out of the program. So basically, if the user hits the <code>Esc</code> key, our program should exit.</p>
|
|
||||||
<p>With all that done, let’s run the program and see what happens!</p>
|
|
||||||
<p>Indeed, our “@” symbol does move, but… it’s perhaps not what was expected.</p>
|
|
||||||
<p><img src="Part%201%20-%20Drawing%20the%20'@'%20symbol%20and%20moving%20it%20around%20%C2%B7%20Roguelike%20Tutorials_files/snake_the_roguelike.png" alt="Snake the Roguelike?" title="Snake the Roguelike?"></p>
|
|
||||||
<p>Unless you’re making a roguelike version of “Snake” (and who knows,
|
|
||||||
maybe you are), we need to fix the “@” symbol being left behind wherever
|
|
||||||
we move. So why is this happening in the first place?</p>
|
|
||||||
<p>Turns out, we need to “clear” the console after we’ve drawn it, or
|
|
||||||
we’ll get these leftovers when we draw symbols in their new places.
|
|
||||||
Luckily, this is as easy as adding one line:</p>
|
|
||||||
<div>
|
|
||||||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
|
||||||
Diff
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
|
||||||
Original
|
|
||||||
</button>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="data-pane active" data-pane="diff">
|
|
||||||
|
|
||||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span> ...
|
|
||||||
</span></span><span style="display:flex;"><span> while True:
|
|
||||||
</span></span><span style="display:flex;"><span> root_console.print(x=player_x, y=player_y, string="@")
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span> context.present(root_console)
|
|
||||||
</span></span><span style="display:flex;"><span>
|
|
||||||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ root_console.clear()
|
|
||||||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
|
||||||
</span></span><span style="display:flex;"><span> for event in tcod.event.wait():
|
|
||||||
</span></span><span style="display:flex;"><span> ...
|
|
||||||
</span></span></code></pre></div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="data-pane" data-pane="original">
|
|
||||||
|
|
||||||
<pre> ...
|
|
||||||
while True:
|
|
||||||
root_console.print(x=player_x, y=player_y, string="@")
|
|
||||||
|
|
||||||
context.present(root_console)
|
|
||||||
|
|
||||||
<span class="new-text">root_console.clear()</span>
|
|
||||||
|
|
||||||
for event in tcod.event.wait():
|
|
||||||
...</pre>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>That’s it! Run the project now, and the “@” symbol will move around, without leaving traces of itself behind.</p>
|
|
||||||
<p>That wraps up part one of this tutorial! If you’re using git or some
|
|
||||||
other form of version control (and I recommend you do), commit your
|
|
||||||
changes now.</p>
|
|
||||||
<p>If you want to see the code so far in its entirety, <a href="https://github.com/TStand90/tcod_tutorial_v2/tree/2020/part-1">click
|
|
||||||
here</a>.</p>
|
|
||||||
<p><a href="https://rogueliketutorials.com/tutorials/tcod/v2/part-2">Click here to move on to the next part of this
|
|
||||||
tutorial.</a></p>
|
|
||||||
|
|
||||||
</article>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer class="footer">
|
|
||||||
<section class="container">
|
|
||||||
©
|
|
||||||
|
|
||||||
2023
|
|
||||||
|
|
||||||
·
|
|
||||||
|
|
||||||
Powered by <a href="https://gohugo.io/">Hugo</a> & <a href="https://github.com/luizdepra/hugo-coder/">Coder</a>.
|
|
||||||
|
|
||||||
</section>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<script src="Part%201%20-%20Drawing%20the%20'@'%20symbol%20and%20moving%20it%20around%20%C2%B7%20Roguelike%20Tutorials_files/coder.min.236049395dc3682fb2719640872958e12f1f24067bb09c327b2.js" integrity="sha256-I2BJOV3DaC+ycZZAhylY4S8fJAZ7sJwyeyM+YpDH7aw="></script>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<script src="Part%201%20-%20Drawing%20the%20'@'%20symbol%20and%20moving%20it%20around%20%C2%B7%20Roguelike%20Tutorials_files/codetabs.min.cc52451e7f25e50f64c1c893826f606d58410d742c214dce.js" integrity="sha256-zFJFHn8l5Q9kwciTgm9gbVhBDXQsIU3OI/tEfJlh8rA="></script>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</body></html>
|
|
|
@ -1 +0,0 @@
|
||||||
const body=document.body,darkModeToggle=document.getElementById("dark-mode-toggle"),darkModeMediaQuery=window.matchMedia("(prefers-color-scheme: dark)");localStorage.getItem("colorscheme")?setTheme(localStorage.getItem("colorscheme")):setTheme(body.classList.contains("colorscheme-light")||body.classList.contains("colorscheme-dark")?body.classList.contains("colorscheme-dark")?"dark":"light":darkModeMediaQuery.matches?"dark":"light"),darkModeToggle&&darkModeToggle.addEventListener("click",()=>{let e=body.classList.contains("colorscheme-dark")?"light":"dark";setTheme(e),rememberTheme(e)}),darkModeMediaQuery.addListener(e=>{setTheme(e.matches?"dark":"light")}),document.addEventListener("DOMContentLoaded",function(){let e=document.querySelector(".preload-transitions");e.classList.remove("preload-transitions")});function setTheme(e){body.classList.remove("colorscheme-auto");let n=e==="dark"?"light":"dark";body.classList.remove("colorscheme-"+n),body.classList.add("colorscheme-"+e),document.documentElement.style["color-scheme"]=e;function t(e){return new Promise(t=>{if(document.querySelector(e))return t(document.querySelector(e));const n=new MutationObserver(s=>{document.querySelector(e)&&(t(document.querySelector(e)),n.disconnect())});n.observe(document.body,{childList:!0,subtree:!0})})}if(e==="dark"){const e={type:"set-theme",theme:"github-dark"};t(".utterances-frame").then(t=>{t.contentWindow.postMessage(e,"https://utteranc.es")})}else{const e={type:"set-theme",theme:"github-light"};t(".utterances-frame").then(t=>{t.contentWindow.postMessage(e,"https://utteranc.es")})}}function rememberTheme(e){localStorage.setItem("colorscheme",e)}
|
|
|
@ -1 +0,0 @@
|
||||||
var allTabs=document.querySelectorAll("[data-toggle-tab]"),allPanes=document.querySelectorAll("[data-pane]");function toggleTabs(e){if(e.target){e.preventDefault();var n,s,o=e.currentTarget,t=o.getAttribute("data-toggle-tab")}else t=e;window.localStorage&&window.localStorage.setItem("configLangPref",t),n=document.querySelectorAll("[data-toggle-tab='"+t+"']"),s=document.querySelectorAll("[data-pane='"+t+"']");for(let e=0;e<allTabs.length;e++)allTabs[e].classList.remove("active"),allPanes[e].classList.remove("active");for(let e=0;e<n.length;e++)n[e].classList.add("active"),s[e].classList.add("active")}for(let e=0;e<allTabs.length;e++)allTabs[e].addEventListener("click",toggleTabs);window.localStorage.getItem("configLangPref")&&toggleTabs(window.localStorage.getItem("configLangPref"))
|
|
Before Width: | Height: | Size: 8.8 KiB |
|
@ -1 +0,0 @@
|
||||||
pre{border:1px solid #000;padding:15px;background-color:#272822;color:#f8f8f2;background-color:#272822}.language-diff,.language-py3{background-color:#272822!important}body.colorscheme-dart code{background-color:#272822!important}.crossed-out-text{color:#f92672;text-decoration:line-through}.new-text{color:#a6e22e}.data-pane{display:none}.data-pane.active{display:inline}
|
|
|
@ -1 +0,0 @@
|
||||||
const body=document.body,darkModeToggle=document.getElementById("dark-mode-toggle"),darkModeMediaQuery=window.matchMedia("(prefers-color-scheme: dark)");localStorage.getItem("colorscheme")?setTheme(localStorage.getItem("colorscheme")):setTheme(body.classList.contains("colorscheme-light")||body.classList.contains("colorscheme-dark")?body.classList.contains("colorscheme-dark")?"dark":"light":darkModeMediaQuery.matches?"dark":"light"),darkModeToggle&&darkModeToggle.addEventListener("click",()=>{let e=body.classList.contains("colorscheme-dark")?"light":"dark";setTheme(e),rememberTheme(e)}),darkModeMediaQuery.addListener(e=>{setTheme(e.matches?"dark":"light")}),document.addEventListener("DOMContentLoaded",function(){let e=document.querySelector(".preload-transitions");e.classList.remove("preload-transitions")});function setTheme(e){body.classList.remove("colorscheme-auto");let n=e==="dark"?"light":"dark";body.classList.remove("colorscheme-"+n),body.classList.add("colorscheme-"+e),document.documentElement.style["color-scheme"]=e;function t(e){return new Promise(t=>{if(document.querySelector(e))return t(document.querySelector(e));const n=new MutationObserver(s=>{document.querySelector(e)&&(t(document.querySelector(e)),n.disconnect())});n.observe(document.body,{childList:!0,subtree:!0})})}if(e==="dark"){const e={type:"set-theme",theme:"github-dark"};t(".utterances-frame").then(t=>{t.contentWindow.postMessage(e,"https://utteranc.es")})}else{const e={type:"set-theme",theme:"github-light"};t(".utterances-frame").then(t=>{t.contentWindow.postMessage(e,"https://utteranc.es")})}}function rememberTheme(e){localStorage.setItem("colorscheme",e)}
|
|
|
@ -1 +0,0 @@
|
||||||
var allTabs=document.querySelectorAll("[data-toggle-tab]"),allPanes=document.querySelectorAll("[data-pane]");function toggleTabs(e){if(e.target){e.preventDefault();var n,s,o=e.currentTarget,t=o.getAttribute("data-toggle-tab")}else t=e;window.localStorage&&window.localStorage.setItem("configLangPref",t),n=document.querySelectorAll("[data-toggle-tab='"+t+"']"),s=document.querySelectorAll("[data-pane='"+t+"']");for(let e=0;e<allTabs.length;e++)allTabs[e].classList.remove("active"),allPanes[e].classList.remove("active");for(let e=0;e<n.length;e++)n[e].classList.add("active"),s[e].classList.add("active")}for(let e=0;e<allTabs.length;e++)allTabs[e].addEventListener("click",toggleTabs);window.localStorage.getItem("configLangPref")&&toggleTabs(window.localStorage.getItem("configLangPref"))
|
|
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 29 KiB |
|
@ -1 +0,0 @@
|
||||||
pre{border:1px solid #000;padding:15px;background-color:#272822;color:#f8f8f2;background-color:#272822}.language-diff,.language-py3{background-color:#272822!important}body.colorscheme-dart code{background-color:#272822!important}.crossed-out-text{color:#f92672;text-decoration:line-through}.new-text{color:#a6e22e}.data-pane{display:none}.data-pane.active{display:inline}
|
|
|
@ -1 +0,0 @@
|
||||||
const body=document.body,darkModeToggle=document.getElementById("dark-mode-toggle"),darkModeMediaQuery=window.matchMedia("(prefers-color-scheme: dark)");localStorage.getItem("colorscheme")?setTheme(localStorage.getItem("colorscheme")):setTheme(body.classList.contains("colorscheme-light")||body.classList.contains("colorscheme-dark")?body.classList.contains("colorscheme-dark")?"dark":"light":darkModeMediaQuery.matches?"dark":"light"),darkModeToggle&&darkModeToggle.addEventListener("click",()=>{let e=body.classList.contains("colorscheme-dark")?"light":"dark";setTheme(e),rememberTheme(e)}),darkModeMediaQuery.addListener(e=>{setTheme(e.matches?"dark":"light")}),document.addEventListener("DOMContentLoaded",function(){let e=document.querySelector(".preload-transitions");e.classList.remove("preload-transitions")});function setTheme(e){body.classList.remove("colorscheme-auto");let n=e==="dark"?"light":"dark";body.classList.remove("colorscheme-"+n),body.classList.add("colorscheme-"+e),document.documentElement.style["color-scheme"]=e;function t(e){return new Promise(t=>{if(document.querySelector(e))return t(document.querySelector(e));const n=new MutationObserver(s=>{document.querySelector(e)&&(t(document.querySelector(e)),n.disconnect())});n.observe(document.body,{childList:!0,subtree:!0})})}if(e==="dark"){const e={type:"set-theme",theme:"github-dark"};t(".utterances-frame").then(t=>{t.contentWindow.postMessage(e,"https://utteranc.es")})}else{const e={type:"set-theme",theme:"github-light"};t(".utterances-frame").then(t=>{t.contentWindow.postMessage(e,"https://utteranc.es")})}}function rememberTheme(e){localStorage.setItem("colorscheme",e)}
|
|
|
@ -1 +0,0 @@
|
||||||
var allTabs=document.querySelectorAll("[data-toggle-tab]"),allPanes=document.querySelectorAll("[data-pane]");function toggleTabs(e){if(e.target){e.preventDefault();var n,s,o=e.currentTarget,t=o.getAttribute("data-toggle-tab")}else t=e;window.localStorage&&window.localStorage.setItem("configLangPref",t),n=document.querySelectorAll("[data-toggle-tab='"+t+"']"),s=document.querySelectorAll("[data-pane='"+t+"']");for(let e=0;e<allTabs.length;e++)allTabs[e].classList.remove("active"),allPanes[e].classList.remove("active");for(let e=0;e<n.length;e++)n[e].classList.add("active"),s[e].classList.add("active")}for(let e=0;e<allTabs.length;e++)allTabs[e].addEventListener("click",toggleTabs);window.localStorage.getItem("configLangPref")&&toggleTabs(window.localStorage.getItem("configLangPref"))
|
|
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 21 KiB |