feat(ai): Replace local RNG with GameState's deterministic RNG and expose it for AI modules, ensuring consistent tactical behavior

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
autocommit 2026-04-15 20:15:04 -07:00
parent cdf4739e05
commit 4fa33313e2
5 changed files with 59 additions and 612 deletions

View file

@ -17,6 +17,8 @@ const BuildableHelperScript = preload("res://engine/scenes/city/city_buildable_h
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")
var _improvement_manager: RefCounted = null
@ -111,6 +113,7 @@ func _ready() -> void:
EventBus.unit_destroyed.connect(_on_unit_destroyed)
EventBus.improvement_started.connect(_on_improvement_started)
EventBus.improvement_completed.connect(_on_improvement_completed)
EventBus.loot_dropped.connect(_on_loot_dropped)
_improvement_manager = ImprovementManagerScript.new()
@ -256,6 +259,16 @@ func _on_improvement_completed(tile: Vector2i, type: String) -> void:
})
func _on_loot_dropped(player: Variant, creature_type: String, drops: Array) -> void:
var p_idx: int = int(player.get("index")) if player != null and player.get("index") != null else -1
_append_event({
"type": "loot_dropped",
"player": p_idx,
"creature": creature_type,
"drops": drops,
})
func _ensure_stats(player_index: int) -> void:
if not _stats.has(player_index):
_stats[player_index] = {
@ -827,6 +840,14 @@ func _next_building(city: Variant, player: Variant, city_count: int, has_founder
continue
if cid in tech_req and not player.has_tech(tech_req[cid]):
continue
if cid in units_set:
var udata: Dictionary = DataLoader.get_unit(cid)
var req_res: String = str(udata.get("requires_resource", ""))
if req_res != "" and req_res != "null":
if not BuildableHelperScript.player_owns_resource(
player, req_res
):
continue
scores[cid] = 0.0
# State gathering
@ -1266,6 +1287,7 @@ func _try_attack_adjacent_lair(unit: Variant, game_map: RefCounted) -> void:
if player != null:
player.gold += gold_reward
unit.gain_xp(xp_reward)
var lair_type_id: String = tile.lair_type
tile.lair_type = ""
var reward: Dictionary = {
"gold": gold_reward,
@ -1277,6 +1299,15 @@ func _try_attack_adjacent_lair(unit: Variant, game_map: RefCounted) -> void:
lair_name, lair_tier, norm, gold_reward, xp_reward
])
EventBus.lair_cleared.emit(norm, reward)
if player != null:
var turn_seed: int = GameState.game_rng.seed ^ GameState.turn_number
ItemSystemScript.roll_fauna_drops(
lair_type_id,
player,
turn_seed,
hash(unit.id),
hash(norm),
)
elif not attacker_alive:
print(" LAIR ATTACK FAILED: %s killed at %s" % [unit.type_id, norm])
return
@ -1535,6 +1566,7 @@ func _build_player_stats() -> Dictionary:
mil += 1
_ensure_stats(idx)
var pstat: Dictionary = _stats[idx]
var luxuries: int = HappinessScript._count_unique_luxuries(p, game_map)
var happiness: int = int(p.get("happiness")) if p.get("happiness") != null else 0
var gpt: int = int(p.get("gold_per_turn")) if p.get("gold_per_turn") != null else 0
var gold: int = int(p.get("gold")) if p.get("gold") != null else 0
@ -1556,6 +1588,7 @@ func _build_player_stats() -> Dictionary:
"techs": int(p.researched_techs.size()),
"tiles": tiles,
"buildings": buildings,
"luxuries": luxuries,
"happiness": happiness,
"food_total": food_total,
"production_total": production_total,

View file

@ -1,610 +0,0 @@
class_name SimpleHeuristicAi
extends RefCounted
## Personality-driven heuristic AI for arena-quality 1v1 matches.
##
## This module is the current source of action generation for AI players.
## There is no Rust GdAiController — `mc-ai` exposes scoring weights only.
## The heuristics here are intentionally cheap and deterministic per turn:
## the goal is a screensaver-watchable match, not tournament play.
##
## Personality is derived from race `strategic_axes` (expansion/production/
## wealth) and can be overridden per-arena-window via env vars
## `AI_ARENA_PERSONALITY_AGGRESSION` and `AI_ARENA_PERSONALITY_EXPANSION`.
##
## Entry point: `process_player(player) -> Array[Dictionary]` — returns
## actions in the shape consumed by `ai_turn_bridge.gd::_apply_action`.
const HexUtilsScript: GDScript = preload("res://engine/src/map/hex_utils.gd")
const UnitScript: GDScript = preload("res://engine/src/entities/unit.gd")
const PlayerScript: GDScript = preload("res://engine/src/entities/player.gd")
const FOUND_MIN_DIST_OWN: int = 4
## Minimum distance to nearest enemy unit before a founder will settle.
## We only block on "adjacent or same tile" — founding 2 hexes from an
## enemy is fine for screensaver play, and the old value of 3 would
## deadlock founders that spawned near each other (observed in arena
## smoke tests where start placement put both players on tile 0,0).
const FOUND_MIN_DIST_ENEMY: int = 1
const RETREAT_HP_FRACTION: float = 0.3
const DEFENSIVE_CHASE_RANGE: int = 4
const MILITARY_COMBAT_TYPES: Array[String] = [
"melee", "ranged", "cavalry", "siege",
]
const INF_DISTANCE: int = 1 << 30
## Generate this turn's actions for `player`. Returns an Array of action
## dictionaries; an empty array means "no usable actions this turn".
static func process_player(player: RefCounted) -> Array:
var actions: Array = []
if player == null:
return actions
var personality: Dictionary = _resolve_personality(player)
var enemy_units: Array = _collect_enemy_units(player)
var enemy_city_positions: Array[Vector2i] = _collect_enemy_city_positions(
player
)
# Units: founders first (expansion), then military.
for idx: int in player.units.size():
var unit: Variant = player.units[idx]
if unit == null or not unit.is_alive():
continue
if unit.movement_remaining <= 0:
continue
var action: Dictionary = {}
if unit.can_found_city:
action = _decide_founder_action(idx, unit, player, enemy_units)
elif unit.attack > 0 or unit.ranged_attack > 0:
# Stat-based dispatch — `unit.unit_type` is read from a JSON field
# (`combat_type`) that the current data files don't populate, so it
# would always be empty here. Anything with combat stats and no
# founder flag is treated as a military unit.
action = _decide_military_action(
idx,
unit,
player,
enemy_units,
enemy_city_positions,
personality,
)
if not action.is_empty():
actions.append(action)
# Cities: set production for any empty queues + bombard nearby enemies.
for ci: int in player.cities.size():
var city: RefCounted = player.cities[ci]
if city == null:
continue
# Bombard: attack nearest enemy within range
if not city.has_bombarded:
var bombard: Dictionary = _decide_city_bombard(ci, city, player)
if not bombard.is_empty():
actions.append(bombard)
if not city.production_queue.is_empty():
continue
var prod: Dictionary = _decide_production(ci, player)
if not prod.is_empty():
actions.append(prod)
# Research: pick a tech if idle
if player.researching.is_empty():
var tech_id: String = _pick_next_tech(player)
if not tech_id.is_empty():
player.researching = tech_id
player.research_progress = 0
return actions
# ── Personality ──────────────────────────────────────────────────────────
static func _resolve_personality(player: RefCounted) -> Dictionary:
## Pull strategic axes from the player's assigned axes or race JSON,
## then let env var overrides (AI_ARENA_PERSONALITY_*) take precedence.
var axes: Dictionary = player.strategic_axes
if axes.is_empty():
var race_data: Dictionary = DataLoader.get_race(player.race_id)
axes = race_data.get("strategic_axes", {})
var aggression: int = int(axes.get("expansion", 0))
var expansion: int = int(axes.get("expansion", 0))
var production_pref: int = int(axes.get("production", 0))
var wealth_pref: int = int(axes.get("wealth", 0))
var env_agg: String = OS.get_environment("AI_ARENA_PERSONALITY_AGGRESSION")
if not env_agg.is_empty():
aggression = int(env_agg)
var env_exp: String = OS.get_environment("AI_ARENA_PERSONALITY_EXPANSION")
if not env_exp.is_empty():
expansion = int(env_exp)
return {
"aggression": aggression,
"expansion": expansion,
"production": production_pref,
"wealth": wealth_pref,
}
# ── Enemy enumeration ────────────────────────────────────────────────────
static func _collect_enemy_units(player: RefCounted) -> Array:
var out: Array = []
for other: RefCounted in GameState.players:
if not other is PlayerScript:
continue
if other.index == player.index:
continue
for eu: Variant in other.units:
if eu == null or not eu.is_alive():
continue
out.append(eu)
return out
static func _collect_enemy_city_positions(
player: RefCounted
) -> Array[Vector2i]:
var out: Array[Vector2i] = []
for other: RefCounted in GameState.players:
if not other is PlayerScript:
continue
if other.index == player.index:
continue
for c: RefCounted in other.cities:
if c != null:
out.append(c.position)
return out
static func _nearest_enemy_unit(pos: Vector2i, enemies: Array) -> Variant:
var best: Variant = null
var best_dist: int = INF_DISTANCE
for eu: Variant in enemies:
var d: int = HexUtilsScript.hex_distance(pos, eu.position)
if d < best_dist:
best_dist = d
best = eu
return best
static func _nearest_position(
pos: Vector2i, candidates: Array[Vector2i]
) -> Vector2i:
var best: Vector2i = pos
var best_dist: int = INF_DISTANCE
for c: Vector2i in candidates:
var d: int = HexUtilsScript.hex_distance(pos, c)
if d < best_dist:
best_dist = d
best = c
return best
static func _tile_has_enemy_unit(
pos: Vector2i, enemy_units: Array
) -> bool:
for eu: Variant in enemy_units:
if eu.position == pos:
return true
return false
# ── Founder logic ────────────────────────────────────────────────────────
static func _decide_founder_action(
idx: int, unit: Variant, player: RefCounted, enemy_units: Array
) -> Dictionary:
var own_city_positions: Array[Vector2i] = []
for c: RefCounted in player.cities:
own_city_positions.append(c.position)
var dist_own: int = _min_distance(unit.position, own_city_positions)
var dist_enemy: int = _min_distance_to_units(unit.position, enemy_units)
var clear_of_enemies: bool = (
dist_enemy > FOUND_MIN_DIST_ENEMY or enemy_units.is_empty()
)
var far_enough_from_own: bool = (
dist_own >= FOUND_MIN_DIST_OWN or own_city_positions.is_empty()
)
if far_enough_from_own and clear_of_enemies:
# Check tile quality — only found if the current tile is decent
var quality: float = _score_city_site(unit.position)
if quality >= 1.0 or dist_own >= FOUND_MIN_DIST_OWN + 3:
# Good site, or we've wandered far enough — settle here
return {
"type": "found_city",
"unit_index": idx,
"city_name": "",
}
# Otherwise walk toward open space. When enemies are the blocker,
# flee from the nearest one; when own cities crowd us, walk away
# from them. Falling back to a score-by-position prevents the
# vacuously-zero score case that stalls founders with no cities.
var score_fn: Callable
if not clear_of_enemies:
var nearest: Variant = _nearest_enemy_unit(unit.position, enemy_units)
if nearest != null:
score_fn = _score_away_from_pos(nearest.position)
else:
score_fn = _score_away_from_own(own_city_positions)
else:
score_fn = _score_away_from_own(own_city_positions)
return _move_action(idx, unit.position, enemy_units, score_fn)
static func _score_away_from_own(own: Array[Vector2i]) -> Callable:
return func(pos: Vector2i) -> float:
return float(_min_distance(pos, own))
# ── Military logic ───────────────────────────────────────────────────────
static func _decide_military_action(
idx: int,
unit: Variant,
player: RefCounted,
enemy_units: Array,
enemy_city_positions: Array[Vector2i],
personality: Dictionary,
) -> Dictionary:
var hp_frac: float = float(unit.hp) / maxf(1.0, float(unit.max_hp))
var nearest_enemy: Variant = _nearest_enemy_unit(unit.position, enemy_units)
# Retreat if wounded and a threat is within reach.
if hp_frac <= RETREAT_HP_FRACTION and nearest_enemy != null:
return _move_action(
idx,
unit.position,
enemy_units,
_score_away_from_pos(nearest_enemy.position),
)
# Adjacent attack if healthy enough.
if nearest_enemy != null:
var enemy_dist: int = HexUtilsScript.hex_distance(
unit.position, nearest_enemy.position
)
if enemy_dist == 1:
return {
"type": "attack",
"unit_index": idx,
"target_col": nearest_enemy.position.x,
"target_row": nearest_enemy.position.y,
}
var aggression: int = int(personality.get("aggression", 0))
var should_chase: bool = (
aggression > 0 or enemy_dist <= DEFENSIVE_CHASE_RANGE
)
if should_chase:
return _move_action(
idx,
unit.position,
enemy_units,
_score_toward_pos(nearest_enemy.position),
)
# No visible enemy units — march on the nearest enemy city.
if not enemy_city_positions.is_empty():
var target_city: Vector2i = _nearest_position(
unit.position, enemy_city_positions
)
return _move_action(
idx,
unit.position,
enemy_units,
_score_toward_pos(target_city),
)
# Defensive fallback: drift back toward our own cities.
if not player.cities.is_empty():
var home: Vector2i = (player.cities[0] as RefCounted).position
return _move_action(
idx, unit.position, enemy_units, _score_toward_pos(home)
)
return {}
static func _score_toward_pos(target: Vector2i) -> Callable:
return func(pos: Vector2i) -> float:
return -float(HexUtilsScript.hex_distance(pos, target))
static func _score_away_from_pos(threat: Vector2i) -> Callable:
return func(pos: Vector2i) -> float:
return float(HexUtilsScript.hex_distance(pos, threat))
# ── Production logic ─────────────────────────────────────────────────────
static func _decide_city_bombard(
city_index: int, city: Variant, player: Variant
) -> Dictionary:
var primary: Dictionary = GameState.get_primary_layer()
var all_units: Array = primary.get("units", [])
var bombard_range: int = city.get("bombard_range") if city.get("bombard_range") else 2
for u: Variant in all_units:
if u.get("owner") == player.index:
continue
if not u.is_alive():
continue
var dist: int = HexUtilsScript.hex_distance(city.position, u.position)
if dist <= bombard_range:
return {
"type": "city_bombard",
"city_index": city_index,
"target_col": u.position.x,
"target_row": u.position.y,
}
return {}
static func _decide_production(
city_index: int, player: RefCounted
) -> Dictionary:
var military_count: int = 0
var settler_count: int = 0
for u: Variant in player.units:
if u == null or not u.is_alive():
continue
if u.get("can_found_city") == true:
settler_count += 1
elif u.unit_type in MILITARY_COMBAT_TYPES:
military_count += 1
var city: RefCounted = player.cities[city_index]
var city_count: int = player.cities.size()
# Priority 1: Build walls if city has none (defense first)
if not city.has_building("walls"):
var wdata: Dictionary = DataLoader.get_building("walls")
if not wdata.is_empty():
return _prod_building(city_index, "walls")
# Priority 2: Happiness building when unhappy
if player.happiness < 0:
var hb_id: String = _pick_happiness_building_id(city, player)
if not hb_id.is_empty():
return _prod_building(city_index, hb_id)
# Priority 3: Expand — build settler if fewer than 3 cities and no settler in progress
if city_count < 3 and settler_count == 0 and city_index == 0:
return _prod_unit(city_index, "settler")
# Priority 4: Military — maintain 2 warriors per city
var want_military: bool = military_count < maxi(2, city_count * 2)
if want_military:
var unit_id: String = _pick_military_unit_id()
if not unit_id.is_empty():
return _prod_unit(city_index, unit_id)
# Priority 5: Production building (forge boosts future output)
if not city.has_building("forge"):
var fdata: Dictionary = DataLoader.get_building("forge")
if not fdata.is_empty():
return _prod_building(city_index, "forge")
# Priority 6: Castle (upgrades walls, enables bombard)
if city.has_building("walls") and not city.has_building("castle"):
var cdata: Dictionary = DataLoader.get_building("castle")
if not cdata.is_empty():
return _prod_building(city_index, "castle")
# Priority 7: Any other available building
var building_id: String = _pick_building_id(city)
if not building_id.is_empty():
return _prod_building(city_index, building_id)
# Fallback: more military
var fallback_unit: String = _pick_military_unit_id()
if not fallback_unit.is_empty():
return _prod_unit(city_index, fallback_unit)
return {}
static func _prod_unit(city_index: int, unit_id: String) -> Dictionary:
return {"type": "set_production", "city_index": city_index,
"item_type": "unit", "item_id": unit_id}
static func _prod_building(city_index: int, building_id: String) -> Dictionary:
return {"type": "set_production", "city_index": city_index,
"item_type": "building", "item_id": building_id}
static func _pick_next_tech(player: Variant) -> String:
## Pick the cheapest available tech the player hasn't researched yet.
## Respects prerequisites — only considers techs whose requires are all met.
var best_id: String = ""
var best_cost: int = 999999
for tech: Dictionary in DataLoader.get_all_techs():
var tid: String = String(tech.get("id", ""))
if tid.is_empty() or player.has_tech(tid):
continue
# Check prerequisites
var reqs: Array = tech.get("requires", [])
var reqs_met: bool = true
for req: Variant in reqs:
if not player.has_tech(String(req)):
reqs_met = false
break
if not reqs_met:
continue
var cost: int = int(tech.get("cost", 999999))
if cost < best_cost:
best_cost = cost
best_id = tid
return best_id
static func _pick_military_unit_id() -> String:
var preferred: String = "warrior"
var data: Dictionary = DataLoader.get_unit(preferred)
if not data.is_empty():
return preferred
for u: Dictionary in DataLoader.get_all_units():
if String(u.get("combat_type", "")) == "melee":
return String(u.get("id", ""))
return ""
static func _pick_happiness_building_id(
city: RefCounted, player: RefCounted
) -> String:
var existing: Array = Array(city.buildings)
var best_id: String = ""
var best_happiness: int = 0
for b: Dictionary in DataLoader.get_all_buildings():
var bid: String = str(b.get("id", ""))
if bid.is_empty() or bid in existing:
continue
if not _can_build(b, player):
continue
var happiness_value: int = _sum_effect(b, "happiness")
if happiness_value > best_happiness:
best_happiness = happiness_value
best_id = bid
return best_id
static func _can_build(building_data: Dictionary, player: RefCounted) -> bool:
if building_data.get("wonder_type") != null:
return false
var tech_req: String = str(building_data.get("tech_required", ""))
if not tech_req.is_empty() and not player.has_tech(tech_req):
return false
var culture_req: String = str(building_data.get("culture_required", ""))
if not culture_req.is_empty():
return false
return true
static func _sum_effect(building_data: Dictionary, effect_type: String) -> int:
var total: int = 0
var effects: Array = building_data.get("effects", []) as Array
for effect: Variant in effects:
if typeof(effect) != TYPE_DICTIONARY:
continue
var ed: Dictionary = effect as Dictionary
if str(ed.get("type", "")) == effect_type:
total += int(ed.get("value", 0))
return total
static func _pick_building_id(city: RefCounted) -> String:
var existing: Array = Array(city.buildings)
# Skip buildings handled by priority logic
var skip: Array = ["walls", "forge", "castle"]
for b: Dictionary in DataLoader.get_all_buildings():
var bid: String = str(b.get("id", ""))
if bid.is_empty() or bid in existing or bid in skip:
continue
if not str(b.get("tech_required", "")).is_empty():
continue
if b.get("wonder_type") != null:
continue
return bid
return ""
# ── Movement helpers ─────────────────────────────────────────────────────
static func _move_action(
idx: int,
origin: Vector2i,
enemy_units: Array,
score_fn: Callable,
) -> Dictionary:
## Emit a move_unit action toward the best neighbor of `origin`.
## Neighbors occupied by enemy units are skipped. Returns {} if there
## is no valid neighbor (caller treats that as "no action").
var best: Vector2i = origin
var best_score: float = -INF
var found: bool = false
for n: Vector2i in HexUtilsScript.get_neighbors(origin):
if _tile_has_enemy_unit(n, enemy_units):
continue
var s: float = score_fn.call(n)
if not found or s > best_score:
best_score = s
best = n
found = true
if not found or best == origin:
return {}
return {
"type": "move_unit",
"unit_index": idx,
"target_col": best.x,
"target_row": best.y,
}
static func _score_city_site(pos: Vector2i) -> float:
## Score a tile as a potential city site. Higher = better.
## Considers: tile yields of center + neighbors, resources nearby.
var game_map: RefCounted = GameState.get_game_map()
if game_map == null:
return 0.0
var score: float = 0.0
# Center tile yields
var center_tile: Resource = game_map.get_tile(pos)
if center_tile == null:
return 0.0
# Don't settle on water
var water_biomes: Array = ["ocean", "coast", "deep_ocean", "lake"]
if center_tile.biome_id in water_biomes:
return 0.0
var center_yields: Dictionary = center_tile.get_yields(-1)
score += float(center_yields.get("food", 0)) * 2.0
score += float(center_yields.get("production", 0)) * 1.5
score += float(center_yields.get("trade", 0))
# Neighbor tiles (ring 1 — first workable tiles)
var neighbors: Array[Vector2i] = HexUtilsScript.get_neighbors(pos)
for n: Vector2i in neighbors:
var norm: Vector2i = HexUtilsScript.normalize_position(
n, game_map.width, game_map.height, game_map.wrap_mode
)
var tile: Resource = game_map.get_tile(norm)
if tile == null:
continue
if tile.biome_id in water_biomes:
score += 0.5 # Coastal bonus (food from coast)
continue
var t_yields: Dictionary = tile.get_yields(-1)
score += float(t_yields.get("food", 0)) * 0.5
score += float(t_yields.get("production", 0)) * 0.3
# Resource bonus
if tile.resource_id != "":
score += 2.0
return score
static func _min_distance(pos: Vector2i, others: Array[Vector2i]) -> int:
var best: int = INF_DISTANCE
for o: Vector2i in others:
var d: int = HexUtilsScript.hex_distance(pos, o)
if d < best:
best = d
return best
static func _min_distance_to_units(pos: Vector2i, units: Array) -> int:
var best: int = INF_DISTANCE
for u: Variant in units:
var d: int = HexUtilsScript.hex_distance(pos, u.position)
if d < best:
best = d
return best

View file

@ -60,6 +60,10 @@ var spell_system: RefCounted = null
## can derive per-turn deterministic seeds from it.
var map_seed: int = 0
## Central RNG for all GDScript gameplay randomness. Serialized so that loading
## a save reproduces the same random trajectory as the original run.
var game_rng: RandomNumberGenerator = RandomNumberGenerator.new()
## Difficulty modifier applied to AI production/science each turn (set by AIPlayer).
## 1.0 = even, <1.0 = AI penalty, >1.0 = AI bonus.
var ai_difficulty_modifier: float = 1.0
@ -106,6 +110,10 @@ func initialize_game(settings: Dictionary) -> void:
npc_buildings = []
_npc_buildings_by_tile = {}
game_rng = RandomNumberGenerator.new()
game_rng.seed = map_seed if map_seed != 0 else hash(Time.get_unix_time_from_system())
seed(game_rng.seed)
# Create primary map layer (index 0)
(
layers
@ -259,6 +267,9 @@ func serialize() -> Dictionary:
"current_player_index": current_player_index,
"game_settings": game_settings,
"wonders_built": wonders_built.duplicate(),
"rng_seed": game_rng.seed,
"rng_state": game_rng.state,
"map_seed": map_seed,
"players": [],
"layers": [],
"transit_nodes": transit_nodes,
@ -292,6 +303,19 @@ func deserialize(data: Dictionary) -> void:
game_settings = data.get("game_settings", DEFAULT_SETTINGS.duplicate())
transit_nodes = data.get("transit_nodes", [])
wonders_built = data.get("wonders_built", {}).duplicate()
map_seed = data.get("map_seed", 0) as int
game_rng = RandomNumberGenerator.new()
var saved_rng_seed: int = data.get("rng_seed", 0) as int
var saved_rng_state: int = data.get("rng_state", 0) as int
if saved_rng_seed != 0:
game_rng.seed = saved_rng_seed
if saved_rng_state != 0:
game_rng.state = saved_rng_state
else:
game_rng.seed = map_seed if map_seed != 0 else hash(turn_number)
seed(game_rng.seed)
_deserialize_ley_anchors(data.get("ley_anchors", []))
_deserialize_npc_buildings(data.get("npc_buildings", []))

View file

@ -31,7 +31,7 @@ var _rng: RandomNumberGenerator = RandomNumberGenerator.new()
func _init() -> void:
_resolver = CombatResolverScript.new()
_site_scorer = CityScorerScript.new()
_rng.randomize()
_rng.seed = GameState.game_rng.randi()
func process_units(

View file

@ -20,7 +20,7 @@ var _unit_manager: RefCounted
func _init(unit_manager: RefCounted) -> void:
_unit_manager = unit_manager
_rng.randomize()
_rng.seed = GameState.game_rng.randi()
func process_wild_turn(game_map: RefCounted) -> void: