test(scenes): Add test scene (improvement_proof.tscn) with validation scripts (improvement_proof.gd) and UID file for improvement-proof logic testing

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-04-07 17:50:33 -07:00
parent 3297d82f88
commit 9cb59bea5d
3 changed files with 440 additions and 0 deletions

View file

@ -0,0 +1,433 @@
extends Node2D
## Phase 10 Improvement Proof Scene.
## Proves: Engineer can build Farm on grassland, improvement completes after
## build_turns, tile yields update with +1 food from Farm improvement.
## Self-capturing: renders two panels, saves screenshot, and quits.
const MapGeneratorScript: GDScript = preload("res://engine/src/generation/map_generator.gd")
const HexUtilsScript: GDScript = preload("res://engine/src/map/hex_utils.gd")
const CityScript: GDScript = preload("res://engine/src/entities/city.gd")
const PlayerScript: GDScript = preload("res://engine/src/entities/player.gd")
const UnitScript: GDScript = preload("res://engine/src/entities/unit.gd")
const ImprovementScript: GDScript = preload("res://engine/src/entities/improvement.gd")
const ImprovementManagerScript: GDScript = preload(
"res://engine/src/modules/management/improvement_manager.gd"
)
const CELL_W: int = 11
const CELL_H: int = 8
const MARGIN: Vector2i = Vector2i(10, 40)
const OUTPUT_DIR: String = "user://screenshots"
const WATER_BIOMES: Dictionary = {
"ocean": true, "deep_ocean": true, "coast": true, "inland_sea": true, "lake": true,
}
const TERRAIN_COLORS: Dictionary = {
"ocean": Color(0.05, 0.10, 0.35),
"deep_ocean": Color(0.02, 0.05, 0.25),
"coast": Color(0.25, 0.45, 0.75),
"lake": Color(0.15, 0.65, 0.75),
"inland_sea": Color(0.10, 0.30, 0.60),
"grassland": Color(0.30, 0.65, 0.20),
"plains": Color(0.60, 0.70, 0.25),
"forest": Color(0.10, 0.40, 0.10),
"jungle": Color(0.20, 0.70, 0.15),
"boreal_forest": Color(0.15, 0.40, 0.35),
"enchanted_forest": Color(0.30, 0.55, 0.60),
"desert": Color(0.85, 0.75, 0.40),
"tundra": Color(0.70, 0.75, 0.72),
"snow": Color(0.92, 0.94, 0.96),
"ice": Color(0.80, 0.88, 0.95),
"mountains": Color(0.45, 0.42, 0.40),
"hills": Color(0.55, 0.45, 0.30),
"swamp": Color(0.30, 0.35, 0.15),
"volcano": Color(0.75, 0.15, 0.10),
"land": Color(0.50, 0.50, 0.30),
}
var _game_map: RefCounted = null
var _player: RefCounted = null
var _engineer: RefCounted = null
var _imp_manager: RefCounted = null
var _engineer_pos: Vector2i = Vector2i.ZERO
var _farm_tile_biome: String = ""
var _yields_before: Dictionary = {}
var _yields_after: Dictionary = {}
var _buildable_list: Array[Dictionary] = []
var _build_turns: int = 0
var _improvement_applied: bool = false
var _captured: bool = false
var _screenshot_name: String = "phase10_proof"
func _ready() -> void:
RenderingServer.set_default_clear_color(Color(0.06, 0.05, 0.04))
get_viewport().size = Vector2i(1920, 1080)
DisplayServer.window_set_size(Vector2i(1920, 1080))
var env_name: String = OS.get_environment("SCREENSHOT_NAME")
if not env_name.is_empty():
_screenshot_name = env_name
await get_tree().process_frame
_generate_map()
_setup_game()
_run_improvement_cycle()
queue_redraw()
for _i: int in range(12):
await get_tree().process_frame
_capture_and_quit()
func _generate_map() -> void:
print("=== Phase 10 Improvement Proof ===")
var settings: Dictionary = {
"map_size": "duel",
"map_type": "continents",
"seed": 42,
"num_players": 2,
"map_wrap": "cylinder",
}
var gen: RefCounted = MapGeneratorScript.new()
_game_map = gen.generate(settings)
print("Map: %dx%d, duel, seed 42" % [_game_map.width, _game_map.height])
func _setup_game() -> void:
_player = PlayerScript.new()
_player.index = 0
_player.player_name = "Dwarf King"
_player.race_id = "dwarf"
_player.gold = 50
# Find a grassland tile for the Engineer (Farm requires grassland)
var center: Vector2i = HexUtilsScript.offset_to_axial(
Vector2i(_game_map.width / 2, _game_map.height / 2)
)
_engineer_pos = _find_terrain_tile(center, "grassland", 20)
# Set tile ownership to player so improvement is valid
var tile: Resource = _game_map.get_tile(_engineer_pos) as Resource
if tile != null:
tile.owner = 0
_farm_tile_biome = tile.biome_id
# Create Engineer unit
_engineer = UnitScript.new()
_engineer.id = "eng_0_1"
_engineer.type_id = "dwarf_engineer"
_engineer.owner = 0
_engineer.name = "Engineer"
_engineer.position = _engineer_pos
_engineer.can_build_improvements = true
_engineer.movement_remaining = 2
_engineer.max_hp = 10
_engineer.hp = 10
_engineer.unit_type = "civilian"
_player.units = [_engineer]
# Create ImprovementManager
_imp_manager = ImprovementManagerScript.new()
# Record yields before improvement
_yields_before = _get_tile_yields(_engineer_pos)
# Get buildable list for display
_buildable_list = _imp_manager.get_buildable_improvements(
_engineer, _game_map, _player
)
print("Engineer at %s on %s" % [str(_engineer_pos), _farm_tile_biome])
print("Buildable improvements: %d" % _buildable_list.size())
for entry: Dictionary in _buildable_list:
print(" - %s (%d turns)" % [entry.get("name", ""), entry.get("build_turns", 0)])
func _run_improvement_cycle() -> void:
## Start building a Farm and simulate turns until completion.
var started: bool = _imp_manager.start_improvement(_engineer, "farm", _player)
if not started:
push_error("Phase10Proof: Failed to start Farm improvement")
return
_build_turns = ImprovementScript.get_build_time("farm")
print("Farm started — %d turns to complete" % _build_turns)
# Simulate turns by decrementing turns_remaining on pending improvements
for turn: int in range(_build_turns):
for i: int in range(_player.pending_improvements.size()):
var pending: Dictionary = _player.pending_improvements[i] as Dictionary
pending["turns_remaining"] = pending.get("turns_remaining", 1) - 1
if pending["turns_remaining"] <= 0:
var tile_pos: Vector2i = Vector2i(
pending.get("x", 0), pending.get("y", 0)
)
var imp_type: String = pending.get("type", "")
EventBus.improvement_completed.emit(tile_pos, imp_type)
_improvement_applied = true
print("Turn %d: Farm completed at %s" % [turn + 1, str(tile_pos)])
# Remove completed
var remaining: Array = []
for i: int in range(_player.pending_improvements.size()):
var pending: Dictionary = _player.pending_improvements[i] as Dictionary
if pending.get("turns_remaining", 0) > 0:
remaining.append(pending)
_player.pending_improvements = remaining
# Record yields after improvement
_yields_after = _get_tile_yields(_engineer_pos)
var food_before: int = _yields_before.get("food", 0)
var food_after: int = _yields_after.get("food", 0)
print("Tile yields: food %d%d (delta +%d)" % [
food_before, food_after, food_after - food_before
])
func _get_tile_yields(pos: Vector2i) -> Dictionary:
var tile: Resource = _game_map.get_tile(pos) as Resource
if tile == null:
return {}
return tile.get_yields()
func _find_terrain_tile(near: Vector2i, biome: String, max_radius: int) -> Vector2i:
## Find the nearest tile with the specified biome.
var center_tile: Resource = _game_map.tiles.get(near)
if center_tile != null and center_tile.biome_id == biome:
return near
for r: int in range(1, max_radius + 1):
for pos: Vector2i in HexUtilsScript.hex_ring(near, r):
var tile: Resource = _game_map.tiles.get(pos)
if tile != null and tile.biome_id == biome:
return pos
# Fallback: any land tile
for r: int in range(1, max_radius + 1):
for pos: Vector2i in HexUtilsScript.hex_ring(near, r):
var tile: Resource = _game_map.tiles.get(pos)
if tile != null and not WATER_BIOMES.has(tile.biome_id):
return pos
return near
func _draw() -> void:
if _game_map == null:
return
var font: Font = ThemeDB.fallback_font
var map_w: float = _game_map.width * CELL_W
var p1_x: float = MARGIN.x
var p2_x: float = p1_x + map_w + 20
# Header
draw_string(font, Vector2(MARGIN.x, 18),
"Phase 10 Proof — Tile Improvements: Engineer Build + Farm Completion",
HORIZONTAL_ALIGNMENT_LEFT, -1, 13, Color.WHITE)
# Panel labels
draw_string(font, Vector2(p1_x, MARGIN.y - 6),
"Panel 1: Engineer on Map + Build Menu",
HORIZONTAL_ALIGNMENT_LEFT, -1, 10, Color(0.8, 0.6, 0.2))
draw_string(font, Vector2(p2_x, MARGIN.y - 6),
"Panel 2: Farm Completed — Yields Updated",
HORIZONTAL_ALIGNMENT_LEFT, -1, 10, Color(0.4, 0.9, 0.4))
_draw_map_panel(p1_x)
_draw_results_panel(p2_x, font)
# Footer
var footer_y: float = MARGIN.y + (_game_map.height + 4) * CELL_H + 20
var status: String = "Farm completed" if _improvement_applied else "Farm in progress"
var food_delta: int = _yields_after.get("food", 0) - _yields_before.get("food", 0)
draw_string(font, Vector2(MARGIN.x, footer_y),
"%s — tile food yield %s%d" % [
status,
"+" if food_delta >= 0 else "",
food_delta,
],
HORIZONTAL_ALIGNMENT_LEFT, -1, 12, Color(0.3, 0.9, 0.3))
func _draw_map_panel(px: float) -> void:
var font: Font = ThemeDB.fallback_font
# Draw terrain grid
for tile_ref: Resource in _game_map.tiles.values():
var pos: Vector2i = tile_ref.position
var offset: Vector2i = HexUtilsScript.axial_to_offset(pos)
var x: float = px + offset.x * CELL_W
var y: float = MARGIN.y + offset.y * CELL_H
if offset.x & 1:
y += CELL_H * 0.5
var base: Color = TERRAIN_COLORS.get(tile_ref.biome_id, Color(0.5, 0.0, 0.5))
draw_rect(Rect2(x, y, CELL_W - 1, CELL_H - 1), base)
# Engineer marker: cyan square
var eng_off: Vector2i = HexUtilsScript.axial_to_offset(_engineer_pos)
var ex: float = px + eng_off.x * CELL_W
var ey: float = MARGIN.y + eng_off.y * CELL_H
if eng_off.x & 1:
ey += CELL_H * 0.5
draw_rect(Rect2(ex + 1, ey + 1, CELL_W - 2, CELL_H - 2), Color(0.2, 0.9, 0.9))
# Label
draw_string(font, Vector2(ex - 4, ey + CELL_H + 8),
"Engineer", HORIZONTAL_ALIGNMENT_LEFT, -1, 8, Color(0.2, 0.9, 0.9))
# Farm highlight after completion
if _improvement_applied:
draw_rect(Rect2(ex - 1, ey - 1, CELL_W + 1, CELL_H + 1), Color(0.3, 0.9, 0.3, 0.5), false, 2.0)
# Build menu popup simulation (right of map)
var menu_x: float = px + _game_map.width * CELL_W + 8
var menu_y: float = MARGIN.y + 20
draw_string(font, Vector2(menu_x, menu_y),
"Build Improvement:", HORIZONTAL_ALIGNMENT_LEFT, -1, 10, Color(0.9, 0.8, 0.3))
menu_y += 14
# Draw buildable list
for entry: Dictionary in _buildable_list:
var imp_name: String = entry.get("name", "")
var turns: int = entry.get("build_turns", 0)
var is_farm: bool = entry.get("id", "") == "farm"
var row_color: Color = Color(0.3, 1.0, 0.3) if is_farm else Color(0.7, 0.7, 0.7)
var prefix: String = "" if is_farm else " "
draw_string(font, Vector2(menu_x, menu_y),
"%s%s (%d turns)" % [prefix, imp_name, turns],
HORIZONTAL_ALIGNMENT_LEFT, -1, 9, row_color)
menu_y += 12
# Legend
var legend_y: float = MARGIN.y + (_game_map.height + 2) * CELL_H + 2
draw_rect(Rect2(px, legend_y, 8, 8), Color(0.2, 0.9, 0.9))
draw_string(font, Vector2(px + 11, legend_y + 8),
"engineer", HORIZONTAL_ALIGNMENT_LEFT, -1, 8, Color.WHITE)
draw_rect(Rect2(px + 70, legend_y, 8, 8), Color(0.3, 0.9, 0.3, 0.5))
draw_string(font, Vector2(px + 81, legend_y + 8),
"farm built", HORIZONTAL_ALIGNMENT_LEFT, -1, 8, Color.WHITE)
func _draw_results_panel(px: float, font: Font) -> void:
var y: float = MARGIN.y
draw_string(font, Vector2(px, y),
"Tile: %s at %s" % [_farm_tile_biome, str(_engineer_pos)],
HORIZONTAL_ALIGNMENT_LEFT, -1, 11, Color(0.8, 0.7, 0.5))
y += 16
draw_string(font, Vector2(px, y),
"Improvement: Farm (%d turns to build)" % _build_turns,
HORIZONTAL_ALIGNMENT_LEFT, -1, 11, Color(0.9, 0.8, 0.3))
y += 16
draw_string(font, Vector2(px, y),
"Status: %s" % ("Completed" if _improvement_applied else "In Progress"),
HORIZONTAL_ALIGNMENT_LEFT, -1, 11,
Color(0.3, 1.0, 0.3) if _improvement_applied else Color(1.0, 0.7, 0.2))
y += 20
# Divider
draw_rect(Rect2(px, y, 400, 1), Color(0.35, 0.30, 0.22))
y += 12
# Yields comparison
draw_string(font, Vector2(px, y),
"YIELDS COMPARISON", HORIZONTAL_ALIGNMENT_LEFT, -1, 10, Color(0.75, 0.70, 0.55))
y += 16
var yield_keys: Array = [
["Food", "food", Color(0.3, 0.9, 0.3)],
["Production", "production", Color(0.9, 0.6, 0.2)],
["Trade", "trade", Color(0.95, 0.85, 0.15)],
["Culture", "culture", Color(0.75, 0.35, 0.95)],
]
draw_string(font, Vector2(px, y),
" Before After Delta",
HORIZONTAL_ALIGNMENT_LEFT, -1, 9, Color(0.6, 0.6, 0.6))
y += 14
for row: Array in yield_keys:
var label: String = row[0] as String
var key: String = row[1] as String
var color: Color = row[2] as Color
var before: int = _yields_before.get(key, 0)
var after: int = _yields_after.get(key, 0)
var delta: int = after - before
var delta_str: String = "+%d" % delta if delta > 0 else str(delta)
var delta_color: Color = Color(0.3, 1.0, 0.3) if delta > 0 else color
draw_string(font, Vector2(px, y),
"%-12s %4d %4d %s" % [label, before, after, delta_str],
HORIZONTAL_ALIGNMENT_LEFT, -1, 10, color)
if delta > 0:
draw_string(font, Vector2(px + 310, y),
delta_str, HORIZONTAL_ALIGNMENT_LEFT, -1, 10, delta_color)
y += 14
y += 12
# Improvement data summary
draw_string(font, Vector2(px, y),
"Improvement data from JSON:",
HORIZONTAL_ALIGNMENT_LEFT, -1, 9, Color(0.6, 0.6, 0.6))
y += 14
var farm_yields: Dictionary = ImprovementScript.get_yield_bonus("farm")
draw_string(font, Vector2(px, y),
"Farm yields: food +%d, prod +%d, gold +%d" % [
farm_yields.get("food", 0),
farm_yields.get("production", 0),
farm_yields.get("gold", 0),
],
HORIZONTAL_ALIGNMENT_LEFT, -1, 10, Color(0.7, 0.9, 0.7))
y += 14
var farm_terrain: String = "grassland, plains, enchanted_forest"
draw_string(font, Vector2(px, y),
"Valid terrain: %s" % farm_terrain,
HORIZONTAL_ALIGNMENT_LEFT, -1, 10, Color(0.7, 0.9, 0.7))
y += 20
# Tile state verification
var tile: Resource = _game_map.get_tile(_engineer_pos) as Resource
var tile_imp: String = tile.improvement if tile != null else "(null)"
draw_string(font, Vector2(px, y),
"Tile.improvement = \"%s\"" % tile_imp,
HORIZONTAL_ALIGNMENT_LEFT, -1, 10,
Color(0.3, 1.0, 0.3) if tile_imp == "farm" else Color(1.0, 0.4, 0.4))
func _capture_and_quit() -> void:
if _captured:
return
_captured = true
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(OUTPUT_DIR))
var image: Image = get_viewport().get_texture().get_image()
if image == null:
push_error("Phase10Proof: Failed to get viewport image")
get_tree().quit(1)
return
var timestamp: String = (
Time.get_datetime_string_from_system().replace(":", "-").replace("T", "_")
)
var rel_path: String = "%s/%s_%s.png" % [OUTPUT_DIR, _screenshot_name, timestamp]
var abs_path: String = ProjectSettings.globalize_path(rel_path)
var err: Error = image.save_png(abs_path)
if err == OK:
print("SCREENSHOT_PATH:%s" % abs_path)
print("Screenshot: %dx%d saved to %s" % [
image.get_width(), image.get_height(), abs_path
])
else:
push_error("Phase10Proof: Save failed: %s" % error_string(err))
get_tree().quit()

View file

@ -0,0 +1 @@
uid://bp0lcyalm1ah1

View file

@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://imp10proof1"]
[ext_resource type="Script" path="res://engine/scenes/tests/improvement_proof.gd" id="1_script"]
[node name="ImprovementProof" type="Node2D"]
script = ExtResource("1_script")