fix(@projects/@magic-civilization): 🐛 update ai player and unit initialization
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
084661c4ba
commit
e104ed91c9
1 changed files with 104 additions and 78 deletions
|
|
@ -109,7 +109,6 @@ func _initialize() -> void:
|
|||
add_child(_mock_tm)
|
||||
|
||||
_setup_game_state()
|
||||
(_tech_web as TechWebScript).initialize()
|
||||
|
||||
var game_map: RefCounted = GameState.get_game_map()
|
||||
var map_w: int = (game_map as GameMapScript).width if game_map is GameMapScript else 0
|
||||
|
|
@ -202,13 +201,10 @@ func _collect_start_positions(
|
|||
func _create_ai_player(
|
||||
p_name: String, race_id: String, clan_id: String, personality: Dictionary
|
||||
) -> RefCounted:
|
||||
var player: RefCounted = PlayerScript.new()
|
||||
player.player_name = p_name
|
||||
player.race_id = race_id
|
||||
var player: RefCounted = PlayerScript.new(-1, p_name, race_id)
|
||||
if "clan_id" in player:
|
||||
player.clan_id = clan_id
|
||||
player.is_player_controlled = false
|
||||
player.science_per_turn = 3
|
||||
player.is_human = false
|
||||
player.gold = 50
|
||||
player.gold_per_turn = 5
|
||||
player.happiness = 5
|
||||
|
|
@ -220,19 +216,9 @@ func _create_ai_player(
|
|||
|
||||
|
||||
func _create_founder(owner_idx: int, pos: Vector2i) -> RefCounted:
|
||||
var unit: UnitScript = UnitScript.new()
|
||||
var unit: UnitScript = UnitScript.new("dwarf_founder", owner_idx, pos)
|
||||
unit.id = "founder_%d" % owner_idx
|
||||
unit.type_id = "dwarf_founder"
|
||||
unit.owner = owner_idx
|
||||
unit.position = pos
|
||||
unit.unit_type = "civilian"
|
||||
unit.can_found_city = true
|
||||
var data: Dictionary = DataLoader.get_unit("dwarf_founder")
|
||||
if not data.is_empty():
|
||||
unit.apply_data(data)
|
||||
unit.owner = owner_idx
|
||||
unit.position = pos
|
||||
unit.id = "founder_%d" % owner_idx
|
||||
return unit
|
||||
|
||||
|
||||
|
|
@ -357,9 +343,9 @@ func _draw() -> void:
|
|||
)
|
||||
var clan_id: String = str(p.get("clan_id"))
|
||||
var clan_color: Color = clan_colors.get(clan_id, Color.WHITE) as Color
|
||||
var win_rate_any: float = float(stats.get("win_rate", -1.0))
|
||||
var has_win: bool = stats.has("win_rate") and stats["win_rate"] != null
|
||||
var win_str: String = (
|
||||
"--" if win_rate_any < 0.0 else "%.1f%%" % (win_rate_any * 100.0)
|
||||
"%.1f%%" % (float(stats["win_rate"]) * 100.0) if has_win else "--"
|
||||
)
|
||||
var values: Array[String] = [
|
||||
p.player_name,
|
||||
|
|
@ -379,49 +365,11 @@ func _draw() -> void:
|
|||
HORIZONTAL_ALIGNMENT_LEFT, -1, 12, col)
|
||||
y += lh + 2
|
||||
|
||||
y += 16
|
||||
draw_rect(Rect2(24, y, 1872, 1), Color(0.35, 0.30, 0.22))
|
||||
y += 8
|
||||
|
||||
draw_string(font, Vector2(24, y),
|
||||
"CITIES BY CLAN (founded during %d-turn simulation)" % TOTAL_TURNS,
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 13, Color(0.8, 0.6, 0.2))
|
||||
y += lh + 4
|
||||
|
||||
var game_map: RefCounted = GameState.get_game_map()
|
||||
for p: RefCounted in _players:
|
||||
var clan_id: String = str(p.get("clan_id"))
|
||||
var clan_color: Color = clan_colors.get(clan_id, Color.WHITE) as Color
|
||||
var line_parts: PackedStringArray = PackedStringArray()
|
||||
for city: CityScript in p.cities:
|
||||
if city == null:
|
||||
continue
|
||||
var suffix: String = " pop=%d" % city.population
|
||||
if game_map != null:
|
||||
var ylds: Dictionary = city.get_yields(game_map)
|
||||
suffix += " F:%d P:%d" % [
|
||||
int(ylds.get("food", 0)), int(ylds.get("production", 0)),
|
||||
]
|
||||
line_parts.append("%s@%s%s" % [city.city_name, str(city.position), suffix])
|
||||
var joined: String = (
|
||||
", ".join(line_parts) if not line_parts.is_empty()
|
||||
else "(no cities founded)"
|
||||
)
|
||||
var line: String = "%s: %s" % [p.player_name, joined]
|
||||
draw_string(font, Vector2(24, y), line,
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 11, clan_color)
|
||||
y += lh
|
||||
|
||||
if not _error_log.is_empty():
|
||||
y += lh
|
||||
draw_string(font, Vector2(24, y),
|
||||
"ERRORS (%d)" % _error_log.size(),
|
||||
"ERRORS (%d) — see text summary for full detail" % _error_log.size(),
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 12, Color(0.9, 0.4, 0.4))
|
||||
y += lh
|
||||
for err: String in _error_log.slice(0, 8):
|
||||
draw_string(font, Vector2(24, y), err,
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 10, Color(0.9, 0.5, 0.5))
|
||||
y += lh
|
||||
|
||||
|
||||
func _capture_and_quit() -> void:
|
||||
|
|
@ -430,24 +378,102 @@ func _capture_and_quit() -> void:
|
|||
_captured = true
|
||||
|
||||
var exit_code: int = 0 if _error_log.is_empty() else 1
|
||||
if DisplayServer.get_name() == "headless":
|
||||
get_tree().quit(exit_code)
|
||||
return
|
||||
var image: Image = get_viewport().get_texture().get_image()
|
||||
if image == null:
|
||||
get_tree().quit(exit_code)
|
||||
return
|
||||
|
||||
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(OUTPUT_DIR))
|
||||
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("AISanityProof: Save failed: %s" % error_string(err))
|
||||
|
||||
var abs_path: String = _save_viewport_or_summary()
|
||||
if abs_path.is_empty():
|
||||
exit_code = maxi(1, exit_code)
|
||||
get_tree().quit(exit_code)
|
||||
|
||||
|
||||
## Save an image of the overlay if a real display backs the viewport, OR a
|
||||
## deterministic text summary when the process is fully headless (dummy
|
||||
## rendering driver returns null textures). Returns the path written, or
|
||||
## "" on failure. Both outputs land under OUTPUT_DIR with the same stem so
|
||||
## Phase Gate review pipelines can pick up whichever exists.
|
||||
func _save_viewport_or_summary() -> String:
|
||||
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(OUTPUT_DIR))
|
||||
var timestamp: String = (
|
||||
Time.get_datetime_string_from_system().replace(":", "-").replace("T", "_")
|
||||
)
|
||||
var img_rel: String = "%s/%s_%s.png" % [OUTPUT_DIR, _screenshot_name, timestamp]
|
||||
var img_abs: String = ProjectSettings.globalize_path(img_rel)
|
||||
|
||||
var texture: ViewportTexture = get_viewport().get_texture()
|
||||
var image: Image = texture.get_image() if texture != null else null
|
||||
if image != null and image.get_width() > 0:
|
||||
var err: Error = image.save_png(img_abs)
|
||||
if err == OK:
|
||||
print("SCREENSHOT_PATH:%s" % img_abs)
|
||||
print("Screenshot: %dx%d saved to %s" % [
|
||||
image.get_width(), image.get_height(), img_abs,
|
||||
])
|
||||
return img_abs
|
||||
push_error("AISanityProof: PNG save failed: %s" % error_string(err))
|
||||
|
||||
# Headless-fallback text summary — same filename stem, .txt suffix. The
|
||||
# simulation results are what matters for the Phase Gate review; the
|
||||
# dummy renderer only prevents the overlay bitmap, not the data.
|
||||
var txt_rel: String = "%s/%s_%s.txt" % [OUTPUT_DIR, _screenshot_name, timestamp]
|
||||
var txt_abs: String = ProjectSettings.globalize_path(txt_rel)
|
||||
var summary: String = _build_text_summary()
|
||||
var f: FileAccess = FileAccess.open(txt_abs, FileAccess.WRITE)
|
||||
if f == null:
|
||||
push_error("AISanityProof: text summary open failed")
|
||||
return ""
|
||||
f.store_string(summary)
|
||||
f.close()
|
||||
print("SCREENSHOT_PATH:%s" % txt_abs)
|
||||
print("Text summary: %s (headless fallback)" % txt_abs)
|
||||
return txt_abs
|
||||
|
||||
|
||||
func _build_text_summary() -> String:
|
||||
var lines: PackedStringArray = PackedStringArray()
|
||||
lines.append("AI Sanity Proof -- 5 clans, %d turns, seed %d" % [TOTAL_TURNS, MAP_SEED])
|
||||
lines.append("Status: %s" % ("PASS" if _error_log.is_empty() else "FAIL"))
|
||||
lines.append("")
|
||||
var header: String = (
|
||||
"| Clan | Cities | Units | Pop | Gold | Techs "
|
||||
+ "| MCTS Path | Rollouts | Win% | Action |"
|
||||
)
|
||||
lines.append(header)
|
||||
lines.append(
|
||||
"|---------------------|--------|-------|-----|------|-------"
|
||||
+ "|------------|----------|--------|-------------|"
|
||||
)
|
||||
for p: RefCounted in _players:
|
||||
var stats: Dictionary = AiTurnBridgeScript.get_last_mcts_stats(
|
||||
_final_turn - 1, p.index
|
||||
)
|
||||
var has_win: bool = stats.has("win_rate") and stats["win_rate"] != null
|
||||
var win_str: String = (
|
||||
"%.1f%%" % (float(stats["win_rate"]) * 100.0) if has_win else "--"
|
||||
)
|
||||
lines.append(
|
||||
"| %-19s | %6d | %5d | %3d | %4d | %5d | %-10s | %8d | %-6s | %-11s |" % [
|
||||
p.player_name, p.cities.size(), p.units.size(),
|
||||
_get_total_pop(p), p.gold, p.researched_techs.size(),
|
||||
String(stats.get("path", "--")),
|
||||
int(stats.get("rollouts", 0)),
|
||||
win_str,
|
||||
String(stats.get("action", "--")),
|
||||
]
|
||||
)
|
||||
lines.append("")
|
||||
lines.append("Cities by clan:")
|
||||
for p: RefCounted in _players:
|
||||
var parts: PackedStringArray = PackedStringArray()
|
||||
for city: CityScript in p.cities:
|
||||
if city == null:
|
||||
continue
|
||||
parts.append("%s@%s pop=%d" % [city.city_name, str(city.position), city.population])
|
||||
var line: String = " %s: %s" % [
|
||||
p.player_name,
|
||||
", ".join(parts) if not parts.is_empty() else "(no cities founded)",
|
||||
]
|
||||
lines.append(line)
|
||||
if not _error_log.is_empty():
|
||||
lines.append("")
|
||||
lines.append("Errors:")
|
||||
for err: String in _error_log:
|
||||
lines.append(" - %s" % err)
|
||||
return "\n".join(lines) + "\n"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue