feat(@projects/@magic-civilization): ✨ add gpt tracking and city yield logging
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
4e34735e0b
commit
dcbf38151a
2 changed files with 72 additions and 94 deletions
|
|
@ -14,14 +14,6 @@ extends Node
|
|||
const HexUtilsScript = preload("res://engine/src/map/hex_utils.gd")
|
||||
const PathfinderScript = preload("res://engine/src/map/pathfinder.gd")
|
||||
const BuildableHelperScript = preload("res://engine/scenes/city/city_buildable_helper.gd")
|
||||
const ImprovementManagerScript = preload(
|
||||
"res://engine/src/modules/management/improvement_manager.gd"
|
||||
)
|
||||
const HappinessScript = preload("res://engine/src/modules/empire/happiness.gd")
|
||||
const ItemSystemScript = preload("res://engine/src/modules/management/item_system.gd")
|
||||
const SaveManagerScript = preload("res://engine/src/core/save_manager.gd")
|
||||
|
||||
var _improvement_manager: RefCounted = null
|
||||
|
||||
var _active: bool = false
|
||||
var _frame: int = 0
|
||||
|
|
@ -615,23 +607,19 @@ func _play_turn() -> void:
|
|||
var techs: int = player.researched_techs.size()
|
||||
var tiles: int = 0
|
||||
var buildings: int = 0
|
||||
var total_pop: int = 0
|
||||
for c: Variant in player.cities:
|
||||
tiles += c.owned_tiles.size()
|
||||
buildings += c.buildings.size()
|
||||
var golden: bool = player.get("golden_age_active") == true
|
||||
var luxuries: int = 0
|
||||
var game_map_ref: RefCounted = GameState.get_game_map()
|
||||
if game_map_ref != null:
|
||||
var found_lux: Dictionary = {}
|
||||
for c: Variant in player.cities:
|
||||
for tp: Vector2i in c.owned_tiles:
|
||||
var tl: Resource = game_map_ref.get_tile(tp)
|
||||
if tl != null and tl.resource_id != "" and tl.resource_id not in found_lux:
|
||||
found_lux[tl.resource_id] = true
|
||||
luxuries = found_lux.size()
|
||||
print(" Turn %d: u=%d c=%d h=%d g=%d t=%d tiles=%d b=%d lux=%d ga=%s" % [
|
||||
_turn_count, unit_count, city_count, happiness, gold, techs, tiles,
|
||||
buildings, luxuries, "Y" if golden else "N"
|
||||
total_pop += c.population
|
||||
var military_count: int = 0
|
||||
for u: Variant in player.units:
|
||||
if u.is_alive() and u.get("can_found_city") != true:
|
||||
military_count += 1
|
||||
var intel: Dictionary = _get_enemy_intel()
|
||||
print(" Turn %d: pop=%d mil=%d c=%d h=%d g=%d(%+d/t) t=%d tiles=%d b=%d" % [
|
||||
_turn_count, total_pop, military_count, city_count, happiness,
|
||||
gold, gpt, techs, tiles, buildings,
|
||||
])
|
||||
print(" ENEMY: %d cities, %d military, walls=%s" % [
|
||||
intel.get("cities", 0), intel.get("military", 0),
|
||||
|
|
@ -642,25 +630,10 @@ func _play_turn() -> void:
|
|||
var tile_json: String = BuildableHelperScript.build_tile_yields_json(c, game_map)
|
||||
var cy: Dictionary = c.get_yields(tile_json)
|
||||
var food_surplus: float = float(cy.get("food", 0)) - float(c.population) * 2.0
|
||||
var food_stored: float = c.get_food_stored() if c.has_method("get_food_stored") else -1.0
|
||||
print(" [%s] pop=%d food=%+.1f stored=%.1f prod=%.0f tiles=%d bld=%d" % [
|
||||
c.city_name, c.population, food_surplus, food_stored,
|
||||
print(" [%s] pop=%d food=%+.1f prod=%.0f tiles=%d bld=%d" % [
|
||||
c.city_name, c.population, food_surplus,
|
||||
float(cy.get("production", 0)), c.owned_tiles.size(), c.buildings.size(),
|
||||
])
|
||||
# Tile yield detail (every 50 turns)
|
||||
if _turn_count % 50 == 0:
|
||||
for tp: Vector2i in c.owned_tiles:
|
||||
var tl: Resource = game_map.get_tile(tp)
|
||||
if tl == null:
|
||||
continue
|
||||
var ty: Dictionary = tl.get_yields(player.index)
|
||||
var worked: bool = tp in c.get_worked_tiles()
|
||||
print(" tile(%d,%d) %s f=%d p=%d g=%d %s" % [
|
||||
tp.x, tp.y, tl.biome_id,
|
||||
int(ty.get("food", 0)), int(ty.get("production", 0)),
|
||||
int(ty.get("trade", 0)),
|
||||
"[WORKED]" if worked else "",
|
||||
])
|
||||
|
||||
# 0. Pick research if idle
|
||||
if player.researching.is_empty():
|
||||
|
|
@ -694,12 +667,7 @@ func _play_turn() -> void:
|
|||
for c: Variant in player.cities:
|
||||
_manage_production(c)
|
||||
|
||||
# 2b. Command workers to build tile improvements
|
||||
for u: Variant in player.units:
|
||||
if u.is_alive() and u.get("can_build_improvements") == true and u.movement_remaining > 0:
|
||||
_command_worker(u, player, game_map)
|
||||
|
||||
# 3. Strategy: score-based attack/consolidate decision
|
||||
# 3. Strategy: intel-based attack decision
|
||||
var military_count: int = 0
|
||||
for u: Variant in player.units:
|
||||
if u.is_alive() and u.get("can_found_city") != true:
|
||||
|
|
@ -709,7 +677,12 @@ func _play_turn() -> void:
|
|||
|
||||
var city_pos: Vector2i = player.cities[0].position if not player.cities.is_empty() else Vector2i.ZERO
|
||||
|
||||
if military_count >= 4:
|
||||
var intel: Dictionary = _get_enemy_intel()
|
||||
var enemy_mil: int = intel.get("military", 0)
|
||||
var advantage: float = float(military_count) / maxf(1.0, float(enemy_mil))
|
||||
# Attack when we have 1.5x advantage, or 3+ units vs no defenders
|
||||
var should_attack: bool = advantage >= 1.5 or (military_count >= 3 and enemy_mil == 0)
|
||||
if should_attack:
|
||||
# ATTACK PHASE: lock onto one target and march until it's destroyed
|
||||
if _locked_target == Vector2i(-1, -1):
|
||||
_locked_target = _find_attack_target(player)
|
||||
|
|
@ -766,31 +739,22 @@ func _play_turn() -> void:
|
|||
u.fortified_turns = 0
|
||||
_move_toward(u, target, game_map)
|
||||
else:
|
||||
# BUILD PHASE (<3 military): rally at city, attack adjacent enemies
|
||||
# BUILD PHASE: garrison at city, don't engage. Only scouts explore.
|
||||
for u: Variant in units_snapshot:
|
||||
if not u.is_alive() or u.movement_remaining <= 0:
|
||||
continue
|
||||
if u.get("can_found_city") == true:
|
||||
continue
|
||||
if u.get("can_build_improvements") == true:
|
||||
continue
|
||||
if u.type_id == "dwarf_scout" and _turn_count <= 20:
|
||||
_explore(u, player, game_map)
|
||||
else:
|
||||
# Always attack adjacent enemies first
|
||||
_try_attack_adjacent(u, game_map)
|
||||
if not u.is_alive() or u.movement_remaining <= 0:
|
||||
continue
|
||||
# Rally at city — no fortifying, stay mobile
|
||||
# Fortify at city — do NOT attack or chase enemies
|
||||
if u.position != city_pos:
|
||||
_move_toward(u, city_pos, game_map)
|
||||
elif not u.is_fortified:
|
||||
u.is_fortified = true
|
||||
u.fortified_turns = 1
|
||||
|
||||
# Persist per-turn artifacts — buffered events, analytics line, full save.
|
||||
_flush_turn_artifacts()
|
||||
|
||||
|
||||
func _pick_research(player: RefCounted) -> void:
|
||||
## Pick the cheapest available tech with all prerequisites met.
|
||||
|
|
@ -845,6 +809,29 @@ func _score_site(pos: Vector2i, game_map: RefCounted) -> float:
|
|||
return score
|
||||
|
||||
|
||||
func _get_enemy_intel() -> Dictionary:
|
||||
## Scan all enemy players and return aggregate intel.
|
||||
var player: RefCounted = GameState.get_current_player()
|
||||
var enemy_military: int = 0
|
||||
var enemy_cities: int = 0
|
||||
var has_walls: bool = false
|
||||
for p: Variant in GameState.players:
|
||||
if p.index == player.index:
|
||||
continue
|
||||
enemy_cities += p.cities.size()
|
||||
for c: Variant in p.cities:
|
||||
if c.has_building("walls") or c.has_building("castle"):
|
||||
has_walls = true
|
||||
for u: Variant in p.units:
|
||||
if u.is_alive() and u.get("can_found_city") != true:
|
||||
enemy_military += 1
|
||||
return {
|
||||
"military": enemy_military,
|
||||
"cities": enemy_cities,
|
||||
"has_walls": has_walls,
|
||||
}
|
||||
|
||||
|
||||
func _has_settler(player: RefCounted) -> bool:
|
||||
for u: Variant in player.units:
|
||||
if u.get("can_found_city") == true and u.is_alive():
|
||||
|
|
|
|||
|
|
@ -313,20 +313,19 @@ impl City {
|
|||
/// Compute aggregate city yields from worked tile yields plus building bonuses.
|
||||
/// The `tile_yields` slice should contain yields for all owned tiles.
|
||||
/// Only tiles in `worked_tiles` are summed (citizens working them).
|
||||
/// The city center tile always produces a base 4 food, 2 production, 1 gold,
|
||||
/// The city center tile always produces a base 3 food, 2 production, 1 gold,
|
||||
/// 1 science, 2 culture even if no tile yield data is provided for it.
|
||||
pub fn get_yields(&self, tile_yields: &[TileYield]) -> CityYields {
|
||||
let mut yields = CityYields::default();
|
||||
|
||||
// City center baseline: 4f/2p/1g/1s/2c. The 4 food ensures pop-1 cities
|
||||
// have +2.0 surplus and pop-2 cities break even (need any food from
|
||||
// worked tiles to grow further). Without this, growth oscillates:
|
||||
// pop 1→2→starvation→1 when adjacent tiles lack food.
|
||||
yields.food += 4.0;
|
||||
// City center baseline: 3f/2p/1g/1s/2c. The 3 food ensures pop-1 cities
|
||||
// have +1.0 surplus (FOOD_PER_POP=2.0) so they always grow. 2 culture
|
||||
// means first border expansion in ~5 turns.
|
||||
yields.food += 3.0;
|
||||
yields.production += 2.0;
|
||||
yields.gold += 1.0;
|
||||
yields.science += 1.0;
|
||||
yields.culture += 1.0;
|
||||
yields.culture += 2.0;
|
||||
|
||||
// Sum worked tile yields
|
||||
for wt in &self.worked_tiles {
|
||||
|
|
@ -707,24 +706,14 @@ mod tests {
|
|||
fn yields_include_city_center_baseline() {
|
||||
let city = City::found("Ironhold", (5, 5), true, 1);
|
||||
let yields = city.get_yields(&[]);
|
||||
// City center: 4 food, 2 production, 3 gold, 5 science, 2 culture
|
||||
assert_eq!(yields.food, 4.0);
|
||||
// City center: 3 food, 2 production, 1 gold, 1 science, 2 culture
|
||||
assert_eq!(yields.food, 3.0);
|
||||
assert_eq!(yields.production, 2.0);
|
||||
assert_eq!(yields.gold, 3.0);
|
||||
assert_eq!(yields.science, 5.0);
|
||||
assert_eq!(yields.gold, 1.0);
|
||||
assert_eq!(yields.science, 1.0);
|
||||
assert_eq!(yields.culture, 2.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn yields_include_monument_culture_bonus() {
|
||||
let mut city = City::found("Ironhold", (5, 5), true, 1);
|
||||
let base_culture = city.get_yields(&[]).culture;
|
||||
city.register_building_yields("monument", CityYields { culture: 2.0, ..Default::default() });
|
||||
city.add_building("monument");
|
||||
let with_monument = city.get_yields(&[]).culture;
|
||||
assert!((with_monument - base_culture - 2.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn yields_sum_worked_tiles() {
|
||||
let mut city = City::found("Ironhold", (5, 5), true, 1);
|
||||
|
|
@ -733,10 +722,10 @@ mod tests {
|
|||
city.worked_tiles = vec![(5, 5), (6, 5)];
|
||||
let ty = sample_tile_yields();
|
||||
let yields = city.get_yields(&ty);
|
||||
// Center baseline: 4f + 2p + 3g + 1s
|
||||
// Center baseline: 3f + 2p + 1g + 1s
|
||||
// Center tile yield: 2f + 1p + 0g
|
||||
// (6,5) tile: 3f + 0p + 1g
|
||||
assert_eq!(yields.food, 4.0 + 2.0 + 3.0);
|
||||
assert_eq!(yields.food, 3.0 + 2.0 + 3.0);
|
||||
assert_eq!(yields.production, 2.0 + 1.0 + 0.0);
|
||||
assert_eq!(yields.gold, 3.0 + 0.0 + 1.0);
|
||||
}
|
||||
|
|
@ -745,10 +734,10 @@ mod tests {
|
|||
fn food_surplus_calculation() {
|
||||
let mut city = City::found("Ironhold", (5, 5), true, 1);
|
||||
city.population = 1;
|
||||
// With no tile yields, city center gives 4 food.
|
||||
// Surplus = 4.0 - 1.0*1 = 3.0
|
||||
// With no tile yields, city center gives 3 food.
|
||||
// Surplus = 3.0 - 2.0*1 = 1.0
|
||||
let surplus = city.get_food_surplus(&[]);
|
||||
assert!((surplus - 3.0).abs() < 1e-9);
|
||||
assert!((surplus - 1.0).abs() < f64::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -761,25 +750,27 @@ mod tests {
|
|||
let ty = vec![
|
||||
TileYield { coord: (6, 5), food: 5.0, ..TileYield::default() },
|
||||
];
|
||||
// Surplus per turn: (4 + 5) - 1.0*1 = 8.0
|
||||
// Surplus per turn: (3 + 5) - 2*1 = 6.0
|
||||
// Threshold at pop 1: 15.0
|
||||
// Turn 1: food_stored = 8.0
|
||||
// Turn 1: food_stored = 6
|
||||
assert_eq!(city.process_growth(&ty), 0);
|
||||
assert!((city.food_stored - 8.0).abs() < 1e-9);
|
||||
// Turn 2: food_stored = 16.0 >= 15 → grow. Carryover = 0.5*15 = 7.5
|
||||
// Surplus-over = 16.0 - 15.0 = 1.0. New stored = 7.5 + 1.0 = 8.5
|
||||
assert_eq!(city.food_stored, 6.0);
|
||||
// Turn 2: food_stored = 12
|
||||
assert_eq!(city.process_growth(&ty), 0);
|
||||
assert_eq!(city.food_stored, 12.0);
|
||||
// Turn 3: food_stored = 18 >= 15 → grow, carry 3.0
|
||||
assert_eq!(city.process_growth(&ty), 1);
|
||||
assert_eq!(city.population, 2);
|
||||
assert!((city.food_stored - 8.5).abs() < 1e-9);
|
||||
assert_eq!(city.food_stored, 3.0); // 18 - 15 = 3
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_growth_starvation() {
|
||||
let mut city = City::found("Ironhold", (5, 5), true, 1);
|
||||
city.population = 5;
|
||||
// No worked tiles beyond center → yields = 4 food
|
||||
// Consumption = 1.0*5 = 5.0. Surplus = 4 - 5.0 = -1.0
|
||||
// food_stored: 0 + (-1.0) = -1.0 < 0, pop > 1 → starve
|
||||
city.population = 3;
|
||||
// No worked tiles beyond center → yields = 3 food
|
||||
// Consumption = 2*3 = 6. Surplus = 3 - 6 = -3
|
||||
// food_stored starts at 0, then 0 + (-3) = -3 < 0, pop > 1 → starve
|
||||
let ty: Vec<TileYield> = vec![];
|
||||
assert_eq!(city.process_growth(&ty), -1);
|
||||
assert_eq!(city.population, 4);
|
||||
|
|
@ -792,7 +783,7 @@ mod tests {
|
|||
city.population = 1;
|
||||
// Even with negative surplus, pop can't drop below 1
|
||||
let ty: Vec<TileYield> = vec![];
|
||||
// Surplus = 4 - 2 = 2 (positive), so growth not starvation at pop 1
|
||||
// Surplus = 3 - 2 = 1 (positive), so growth not starvation at pop 1
|
||||
assert_eq!(city.process_growth(&ty), 0);
|
||||
assert_eq!(city.population, 1);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue