fix(@projects/@magic-civilization): 🐛 update clan personality research divergence fix
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
c6dce9a597
commit
36c839268a
9 changed files with 152 additions and 31 deletions
|
|
@ -5,7 +5,7 @@ priority: p0
|
|||
status: partial
|
||||
scope: game1
|
||||
owner: warcouncil
|
||||
updated_at: 2026-04-19
|
||||
updated_at: 2026-04-25
|
||||
evidence:
|
||||
- public/games/age-of-dwarves/data/ai_personalities.json
|
||||
- .local/iter/apricot-20260418_08*/ # 5-clan re-runs on p0-25-instrumented binary
|
||||
|
|
@ -94,11 +94,28 @@ Note: ablated TTV drops (not rises) because most games hit T300 stalemate when t
|
|||
|
||||
**Victory-balance gate**: all 5 clans win ≥6/9–9/10 in their pinned matchup — PASSED (every clan dominant when pinned).
|
||||
|
||||
**Era-divergence gate**: ≥1 era delta between production/expansion-divergent pairs — NOT MET. Production-axis mean (ironhold+deepforge) = 3.0; trade-axis mean (goldvein+runesmith) = 3.0; delta = 0. Root cause updated (post-pacing fix): games now run to T192 median (p0-01 evidence), but clan personalities do not yet drive distinct research sequences — all clans research the same techs in roughly the same priority, so `tier_peak` converges regardless of personality. Fixing this requires either personality-specific research weights or a research-axis differentiation outside warcouncil scope.
|
||||
**Era-divergence gate**: ≥1 era delta between production/expansion-divergent pairs — NOT MET (as of 2026-04-19). Root cause confirmed: `auto_play.gd::_pick_research` was hardcoded military-priority with no personality input. Fix landed 2026-04-25 — see "Post-reframe evidence v3" below.
|
||||
|
||||
## Post-reframe evidence v3 (2026-04-25, research personality wiring)
|
||||
|
||||
**Root cause fix**: `auto_play.gd::_pick_research` previously applied a flat `×2` for `pillar == "military"` with no per-clan variation, so all five clans converged on the same research order. Two code paths updated:
|
||||
|
||||
1. `src/game/engine/scenes/tests/auto_play.gd::_pick_research` — now reads `DataLoader.get_ai_personality(clan_id)` per player, normalises the 6 raw axes (1–10 → [0,1]) via the new `_norm_axis` static helper, and computes a per-pillar multiplier (range 1.0–2.0) for all six actual pillar names in `techs/*.json` (`military`, `metallurgy`, `agriculture`, `civics`, `scholarship`, `ecology`). The hardcoded `×2 military` is gone.
|
||||
|
||||
2. `src/simulator/crates/mc-ai/src/evaluator.rs::score_tech` — corrected stale pillar names (`engineering`, `warfare`, `growth`, `commerce`, `trade`, `construction`, `production` — none of which exist in the actual data) to the real pillar set, and switched from blended `StrategicWeights::economy` / `aggression` to per-axis weights read from `AiPlayerState::strategic_axes`. Build: `cargo build -p mc-ai --lib --locked` clean; `cargo test -p mc-ai --lib --locked` 184/184 pass. `pick_tech` is not yet wired to GDExtension (no caller outside tests) — wiring is tracked in p0-26.
|
||||
|
||||
**Expected research differentiation per clan (pillar → axis mapping):**
|
||||
- `blackhammer` (aggression=9): `military` multiplier ≈ 2.0 → rushes `war`, `tracking`, `combined_arms` etc.
|
||||
- `ironhold` (production=9): `metallurgy` multiplier ≈ 2.0 → prioritises `steelworking`, `runelore`, `high_smithing`
|
||||
- `deepforge` (production=8): `metallurgy` multiplier ≈ 1.78 + `ecology` blend → tall-empire smithing + land techs
|
||||
- `goldvein` (wealth=9, trade=9): `civics` multiplier ≈ 1.7, `scholarship` multiplier ≈ 1.5 → income/knowledge techs
|
||||
- `runesmith` (balanced): all multipliers ≈ 1.3–1.5 → adaptive order based on game state
|
||||
|
||||
Status: code landed; batch validation pending. Next step: re-run 5-clan batch under p0-25 instrumentation to measure `tier_peak` divergence.
|
||||
|
||||
## Remaining to reach done
|
||||
|
||||
1. **Waiting on p0-01 balance tune** — era-divergence gate cannot be evaluated until games routinely reach tier 6+. After p0-01 lands its pacing fix, re-run the 5-clan batch and cite `tier_peak_med` delta between ironhold/deepforge (low production) and goldvein/runesmith (high production) pairs.
|
||||
1. **5-clan batch re-run** under p0-25 instrumentation (tier_peak available); demonstrate ≥10% tier_peak delta between contrasting clan pairs (goldvein vs ironhold; runesmith vs blackhammer). Run: `ssh apricot tools/matchup-grid.sh` or `tools/huge-map-5clan.sh` with `AI_PIN_PERSONALITY=<id>` per slot.
|
||||
2. **6-axis ablation re-run** on the tuned binary with `tier_peak_med` deltas for expansion/production/grudge_persistence. The pre-reframe ablation (2026-04-17) already confirmed all 6 axes live under the legacy metric; this is confirmation under the reframed gate.
|
||||
|
||||
## Depends on
|
||||
|
|
|
|||
|
|
@ -2,13 +2,19 @@
|
|||
id: p1-22
|
||||
title: MCTS per-decision wall-clock budget — bound per-turn cost on huge maps
|
||||
priority: p1
|
||||
status: missing
|
||||
status: partial
|
||||
scope: game1
|
||||
owner: warcouncil
|
||||
updated_at: 2026-04-25
|
||||
evidence:
|
||||
- .local/iter/huge-map-5clan-20260425_115416/ (3/10 victories at PARALLEL=2 SAFETY_TIMEOUT=3600s; perf-bound seeds time out)
|
||||
- src/simulator/crates/mc-ai/src/mcts_tree.rs (no current wall-clock or iteration cap per decision)
|
||||
- src/simulator/crates/mc-ai/src/mcts_tree.rs:301 (simulate_parallel budget_ms: Option<u64> — breaks collection loop when elapsed >= budget)
|
||||
- src/simulator/crates/mc-ai/src/mcts_tree.rs:382 (iterate_gpu_batched budget_ms: Option<u64> — breaks batch-collection loop when elapsed >= budget)
|
||||
- src/simulator/crates/mc-ai/src/mcts_tree.rs:641 (simulate_parallel_respects_wall_clock_budget test — 1_000_000 rollout target exits in <500ms with budget_ms=Some(50), >0 visits confirmed)
|
||||
- src/simulator/api-gdext/src/ai.rs:36 (GdMcTreeController.budget_ms field, default 0=unbounded)
|
||||
- src/simulator/api-gdext/src/ai.rs:111 (set_budget_ms(ms: i64) #[func], plumbed into both choose_action and choose_action_with_stats)
|
||||
- src/game/engine/src/modules/ai/ai_turn_bridge.gd:71 (reads MCTS_DECISION_BUDGET_MS env, calls ctrl.set_budget_ms, logs when active)
|
||||
- tools/huge-map-5clan.sh:44 (: "${MCTS_DECISION_BUDGET_MS:=2000}" default; line ~99 MCTS_DECISION_BUDGET_MS=2000 passed to autoplay-batch.sh)
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
|
@ -19,8 +25,8 @@ This is engineering work, not test calibration: the AI is ALWAYS faster when it
|
|||
|
||||
## Acceptance
|
||||
|
||||
- ❌ `mc-ai` exposes a per-decision wall-clock budget (e.g. `MCTS_DECISION_BUDGET_MS=2000`) that caps the iteration loop in `mcts_tree::iterate*` once `now() - start >= budget_ms`. Default off; opt-in via env var.
|
||||
- ❌ `huge-map-5clan.sh` sets `MCTS_DECISION_BUDGET_MS=2000` so each AI decision is bounded — predictable per-turn cost regardless of state complexity.
|
||||
- ✓ `mc-ai` exposes a per-decision wall-clock budget (e.g. `MCTS_DECISION_BUDGET_MS=2000`) that caps the iteration loop in `mcts_tree::simulate_parallel` and `mcts_tree::iterate_gpu_batched` once `now() - start >= budget_ms`. Default off (0 = unbounded); opt-in via env var. Evidence: `src/simulator/crates/mc-ai/src/mcts_tree.rs:301,382`; unit test `simulate_parallel_respects_wall_clock_budget` passes at line 641.
|
||||
- ✓ `huge-map-5clan.sh` sets `MCTS_DECISION_BUDGET_MS=2000` so each AI decision is bounded — predictable per-turn cost regardless of state complexity. Evidence: `tools/huge-map-5clan.sh` lines 44 and ~99.
|
||||
- ❌ Re-run `huge-map-5clan` 10-seed batch with the budget — verify ≥5/10 victories and ≥2 distinct winners. With `~5s/turn × 5 players × 500 turns = 12500s = 3.5hr` per game at PARALLEL=2 and 3600s safety_timeout, all seeds should reach T500 (≥9/10 victories expected).
|
||||
- ❌ p0-22's `ultimate_stress: PASS` gate (now followup-tracked here) flips ✓ once this lands.
|
||||
|
||||
|
|
|
|||
|
|
@ -1181,12 +1181,48 @@ func _play_turn() -> void:
|
|||
|
||||
|
||||
func _pick_research(player: RefCounted) -> void:
|
||||
## Score available techs: base = 1000/cost; military pillar ×2;
|
||||
## Score available techs: base = 1000/cost; per-pillar personality multiplier;
|
||||
## unlocks tier≥4 unit adds ×3; prerequisite of high-value tech adds ×1.5.
|
||||
## The prereq-chain boost ensures steelworking (→combined_arms→ironwarden)
|
||||
## gets researched ~50 turns earlier than cheapest-first ordering.
|
||||
## Personality axes drive per-pillar multipliers so clan research orders diverge:
|
||||
## military → aggression (blackhammer rushes military techs)
|
||||
## metallurgy → production (ironhold/deepforge prioritise smithing)
|
||||
## agriculture → expansion (blackhammer/runesmith expand aggressively)
|
||||
## civics → wealth + trade_willingness (goldvein)
|
||||
## scholarship → wealth + production blend (goldvein science income)
|
||||
## ecology → expansion × 0.5 + production × 0.5 (deepforge tall-empire)
|
||||
##
|
||||
## Research scoring belongs in mc-ai::ScoringEvaluator::pick_tech (Rail-1).
|
||||
## This test-harness path reads axes inline; wiring through GdAiController
|
||||
## requires the tactical bridge to emit research actions (tracked in p0-26).
|
||||
var all_techs: Array = DataLoader.get_all_techs()
|
||||
|
||||
# Load clan personality axes (1..=10). Defaults to 5 (neutral) if clan
|
||||
# is unset so vanilla scoring degrades gracefully to neutral multipliers.
|
||||
var clan_id: String = str(player.get("clan_id") if player.get("clan_id") != null else "")
|
||||
var axes: Dictionary = {}
|
||||
if not clan_id.is_empty():
|
||||
var personality: Dictionary = DataLoader.get_ai_personality(clan_id)
|
||||
if personality != null and not personality.is_empty():
|
||||
axes = personality.get("strategic_axes", {})
|
||||
|
||||
# Normalise raw 1..=10 axis values to [0, 1] (neutral 5 → 0.44).
|
||||
var agg: float = _norm_axis(axes, "aggression")
|
||||
var prod: float = _norm_axis(axes, "production")
|
||||
var wlth: float = _norm_axis(axes, "wealth")
|
||||
var trd: float = _norm_axis(axes, "trade_willingness")
|
||||
var exp: float = _norm_axis(axes, "expansion")
|
||||
|
||||
# Per-pillar multiplier derived from clan axes (range 1.0..=2.0).
|
||||
# Base of 1.0 ensures clans with low axes still research every pillar.
|
||||
var pillar_mult: Dictionary = {
|
||||
"military": 1.0 + agg,
|
||||
"metallurgy": 1.0 + prod,
|
||||
"agriculture": 1.0 + exp * 0.8,
|
||||
"civics": 1.0 + (wlth + trd) / 2.0 * 0.7,
|
||||
"scholarship": 1.0 + (wlth + prod) / 2.0 * 0.6,
|
||||
"ecology": 1.0 + (exp + prod) / 2.0 * 0.5,
|
||||
}
|
||||
|
||||
# Pass 1: compute raw score for every tech (ignoring availability).
|
||||
var raw_score: Dictionary = {}
|
||||
for tech: Dictionary in all_techs:
|
||||
|
|
@ -1195,8 +1231,9 @@ func _pick_research(player: RefCounted) -> void:
|
|||
continue
|
||||
var cost: int = maxi(int(tech.get("cost", 1)), 1)
|
||||
var sc: float = 1000.0 / float(cost)
|
||||
if str(tech.get("pillar", "")) == "military":
|
||||
sc *= 2.0
|
||||
# Apply personality-driven pillar multiplier (replaces hardcoded x2 for military).
|
||||
var pillar: String = str(tech.get("pillar", ""))
|
||||
sc *= float(pillar_mult.get(pillar, 1.0))
|
||||
for uid: Variant in tech.get("unlocks", {}).get("units", []):
|
||||
var udata: Dictionary = DataLoader.get_unit(str(uid))
|
||||
if int(udata.get("tier", 1)) >= 4:
|
||||
|
|
@ -1204,7 +1241,7 @@ func _pick_research(player: RefCounted) -> void:
|
|||
break
|
||||
raw_score[tid] = sc
|
||||
|
||||
# Pass 2: prerequisites of any tech scoring ≥ 20 get a 1.5× boost.
|
||||
# Pass 2: prerequisites of any tech scoring >= 20 get a 1.5x boost.
|
||||
var prereq_mult: Dictionary = {}
|
||||
for tech: Dictionary in all_techs:
|
||||
var tid: String = str(tech.get("id", ""))
|
||||
|
|
@ -1240,6 +1277,14 @@ func _pick_research(player: RefCounted) -> void:
|
|||
print(" Researching: %s (score %.1f)" % [best_id, best_score])
|
||||
|
||||
|
||||
static func _norm_axis(axes: Dictionary, key: String) -> float:
|
||||
## Normalise a 1..=10 raw personality axis value to [0, 1].
|
||||
## Returns 0.44 for neutral (5), 0.0 for minimum (1), 1.0 for maximum (10).
|
||||
## Missing keys default to 5 (neutral).
|
||||
var raw: float = float(axes.get(key, 5))
|
||||
return (clampf(raw, 1.0, 10.0) - 1.0) / 9.0
|
||||
|
||||
|
||||
func _score_site(pos: Vector2i, game_map: RefCounted) -> float:
|
||||
## Score a hex as a city site. Food*2 + production*1.5 + resources.
|
||||
var tile: Resource = game_map.get_tile(pos)
|
||||
|
|
|
|||
|
|
@ -70,6 +70,12 @@ static func _apply_mcts_strategic_override(player: RefCounted) -> void:
|
|||
ctrl.set_rollout_depth(MCTS_ROLLOUT_DEPTH)
|
||||
ctrl.set_gpu_enabled(OS.get_environment("AI_GPU_ROLLOUT") in ["1", "true", "TRUE", "True"])
|
||||
ctrl.set_priors_enabled(OS.get_environment("AI_MCTS_PRIORS") in ["1", "true", "TRUE", "True"])
|
||||
var budget_ms_env: String = OS.get_environment("MCTS_DECISION_BUDGET_MS")
|
||||
if not budget_ms_env.is_empty() and budget_ms_env.is_valid_int():
|
||||
var budget_ms_val: int = int(budget_ms_env)
|
||||
if budget_ms_val > 0:
|
||||
ctrl.set_budget_ms(budget_ms_val)
|
||||
print("AiTurnBridge: MCTS_DECISION_BUDGET_MS=%d ms active (p1-22)" % budget_ms_val)
|
||||
var data_dir: String = ProjectSettings.globalize_path(
|
||||
"res://public/games/age-of-dwarves/data"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -753,19 +753,46 @@ fn score_tech(
|
|||
score += tech.unlocks_buildings.len() as f32 * 0.2;
|
||||
score += tech.unlocks_units.len() as f32 * 0.15;
|
||||
|
||||
// Strategic fit by pillar
|
||||
// Extract raw per-axis weights from state (1..=10 scale, default 5 = neutral).
|
||||
// These may come from the clan personality JSON (1–10) or race axes (-10..+10).
|
||||
// Normalise both to [0, 1] by clamping to [1, 10] then dividing by 9.
|
||||
let norm_axis = |key: &str| -> f32 {
|
||||
let raw = *state.strategic_axes.get(key).unwrap_or(&5) as f32;
|
||||
(raw.clamp(1.0, 10.0) - 1.0) / 9.0
|
||||
};
|
||||
let aggression = norm_axis("aggression");
|
||||
let production_axis = norm_axis("production");
|
||||
let wealth_axis = norm_axis("wealth");
|
||||
let trade_axis = norm_axis("trade_willingness");
|
||||
let expansion = norm_axis("expansion");
|
||||
|
||||
// Strategic fit by actual pillar names in techs/*.json.
|
||||
//
|
||||
// Mapping:
|
||||
// military → aggression (blackhammer: agg=9 → ~0.89)
|
||||
// metallurgy → production (ironhold: prod=9 → ~0.89; deepforge: prod=8 → ~0.78)
|
||||
// agriculture → expansion (blackhammer: exp=6; runesmith: exp=6)
|
||||
// civics → wealth + trade_willingness blend (goldvein)
|
||||
// scholarship → wealth (goldvein) + production (ironhold/deepforge)
|
||||
// ecology → expansion × 0.5 + production × 0.5 (deepforge tall-empire)
|
||||
score += match tech.pillar.as_str() {
|
||||
"metallurgy" | "engineering" => strategic.economy * 0.6,
|
||||
"military" | "warfare" => strategic.aggression * 0.6,
|
||||
"agriculture" | "growth" => strategic.economy * 0.5,
|
||||
"commerce" | "trade" => strategic.economy * 0.4,
|
||||
"construction" | "production" => strategic.economy * 0.5,
|
||||
_ => 0.2,
|
||||
"military" => aggression * 0.7,
|
||||
"metallurgy" => production_axis * 0.7,
|
||||
"agriculture" => expansion * 0.5,
|
||||
"civics" => (wealth_axis + trade_axis) / 2.0 * 0.6,
|
||||
"scholarship" => (wealth_axis + production_axis) / 2.0 * 0.5,
|
||||
"ecology" => (expansion + production_axis) / 2.0 * 0.4,
|
||||
_ => 0.2,
|
||||
};
|
||||
|
||||
// Economic emergency: commerce techs urgent when broke
|
||||
if state.gold < 0 && matches!(tech.pillar.as_str(), "commerce" | "trade") {
|
||||
score += 0.5;
|
||||
// Economic emergency: civics/scholarship give commerce-adjacent income boost
|
||||
if state.gold < 0 && matches!(tech.pillar.as_str(), "civics" | "scholarship") {
|
||||
score += 0.4;
|
||||
}
|
||||
|
||||
// Aggression bonus for military techs when threat is high
|
||||
if state.threat_level > 0.5 && tech.pillar == "military" {
|
||||
score += strategic.aggression * 0.3;
|
||||
}
|
||||
|
||||
score
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ fn iterate_gpu_batched_exercises_gpu_path_when_adapter_available() {
|
|||
let mut rolled_out = 0_usize;
|
||||
let mut batch_idx: u64 = 0;
|
||||
while rolled_out < TOTAL_ROLLOUTS {
|
||||
let n = tree.iterate_gpu_batched(BATCH_SIZE, 1000 + batch_idx);
|
||||
let n = tree.iterate_gpu_batched(BATCH_SIZE, 1000 + batch_idx, None);
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
|
|
@ -143,7 +143,7 @@ fn iterate_gpu_batched_cpu_fallback_without_context() {
|
|||
let mut rolled_out = 0_usize;
|
||||
let mut batch_idx: u64 = 0;
|
||||
while rolled_out < TOTAL_ROLLOUTS {
|
||||
let n = tree.iterate_gpu_batched(BATCH_SIZE, 2000 + batch_idx);
|
||||
let n = tree.iterate_gpu_batched(BATCH_SIZE, 2000 + batch_idx, None);
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
|
|
@ -179,8 +179,8 @@ fn iterate_gpu_batched_is_seed_deterministic() {
|
|||
let mut tree_b = Tree::new(root_b).with_gpu_context(ctx);
|
||||
|
||||
for i in 0..2 {
|
||||
tree_a.iterate_gpu_batched(BATCH_SIZE, 7000 + i as u64);
|
||||
tree_b.iterate_gpu_batched(BATCH_SIZE, 7000 + i as u64);
|
||||
tree_a.iterate_gpu_batched(BATCH_SIZE, 7000 + i as u64, None);
|
||||
tree_b.iterate_gpu_batched(BATCH_SIZE, 7000 + i as u64, None);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
|
|
@ -200,7 +200,7 @@ fn iterate_gpu_batched_is_seed_deterministic() {
|
|||
fn iterate_gpu_batched_zero_batch_is_noop() {
|
||||
let ctx = GpuContext::shared();
|
||||
let mut tree = Tree::new(make_root_state()).with_gpu_context(ctx);
|
||||
let n = tree.iterate_gpu_batched(0, 42);
|
||||
let n = tree.iterate_gpu_batched(0, 42, None);
|
||||
assert_eq!(n, 0);
|
||||
assert_eq!(tree.root().visits, 0);
|
||||
assert_eq!(tree.gpu_batch_count, 0);
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ fn terminal_state_has_no_legal_actions() {
|
|||
#[test]
|
||||
fn simulate_parallel_visits_match_n_rollouts() {
|
||||
let mut tree = Tree::new(ToyState { depth: 3, branching: 4 });
|
||||
tree.simulate_parallel(200, 42, Tree::<ToyState>::default_rollout);
|
||||
tree.simulate_parallel(200, 42, Tree::<ToyState>::default_rollout, None);
|
||||
// All 200 rollouts must be backpropagated: root visits == 200.
|
||||
assert_eq!(tree.root().visits, 200);
|
||||
// Stubbed rollout returns 0.5 → wins == 0.5 * visits.
|
||||
|
|
@ -91,7 +91,7 @@ fn simulate_parallel_visits_match_n_rollouts() {
|
|||
fn simulate_parallel_deterministic_across_runs() {
|
||||
let make = || {
|
||||
let mut tree = Tree::new(ToyState { depth: 3, branching: 4 });
|
||||
tree.simulate_parallel(500, 99, Tree::<ToyState>::default_rollout);
|
||||
tree.simulate_parallel(500, 99, Tree::<ToyState>::default_rollout, None);
|
||||
(tree.root().visits, tree.nodes.len())
|
||||
};
|
||||
let (v1, n1) = make();
|
||||
|
|
@ -118,7 +118,7 @@ fn parallel_faster_than_serial_for_large_n() {
|
|||
let parallel_ms = {
|
||||
let mut tree = Tree::new(ToyState { depth: 3, branching: 4 });
|
||||
let t = Instant::now();
|
||||
tree.simulate_parallel(N, 1, Tree::<ToyState>::default_rollout);
|
||||
tree.simulate_parallel(N, 1, Tree::<ToyState>::default_rollout, None);
|
||||
t.elapsed().as_millis()
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -213,6 +213,21 @@ if $godot_cmd --headless --path "$export_game_dir" "$flag" "$preset_name" "$out_
|
|||
if [ -e "$out_path" ]; then
|
||||
size="$(du -h "$out_path" 2>/dev/null | cut -f1)"
|
||||
echo -e "${GREEN} ✓ Exported $artifact ($size)${NC}"
|
||||
# ── Relocate GDExtension .so into the addon subdir for Linux ───
|
||||
# Godot's Linux export drops `lib*.so` next to the binary, but the
|
||||
# .gdextension references `res://engine/addons/magic_civ_physics/...`
|
||||
# which the runtime resolves to that exact relative path next to the
|
||||
# executable. Without the move, the library is never loaded and
|
||||
# Gd* classes never register.
|
||||
if [ "$platform" = "linux" ]; then
|
||||
so_root="$out_dir/libmagic_civ_physics.x86_64.so"
|
||||
so_addon_dir="$out_dir/engine/addons/magic_civ_physics"
|
||||
if [ -f "$so_root" ]; then
|
||||
mkdir -p "$so_addon_dir"
|
||||
mv "$so_root" "$so_addon_dir/"
|
||||
echo -e "${DIM} · relocated .so → engine/addons/magic_civ_physics/${NC}"
|
||||
fi
|
||||
fi
|
||||
cleanup_staging
|
||||
exit 0
|
||||
else
|
||||
|
|
|
|||
|
|
@ -41,6 +41,10 @@ DIM='\033[2m'; NC='\033[0m'
|
|||
# (128×80, 12-player) is stretch-goal; switch to
|
||||
# MAP_SIZE=huge once POD's MAX_PLAYERS=4 limit is
|
||||
# lifted and the game supports >8 AI slots.
|
||||
# p1-22: bound MCTS per-decision wall-clock cost. 2000 ms caps each AI
|
||||
# decision so slow seeds finish in ~5s/turn × 5 players × 500 turns ≈ 3.5 hr
|
||||
# per game — well within the 3600s safety timeout.
|
||||
: "${MCTS_DECISION_BUDGET_MS:=2000}"
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
|
|
@ -90,6 +94,7 @@ MARKER="$PARENT/completion.marker"
|
|||
MAP_SIZE="$MAP_SIZE" \
|
||||
NUM_PLAYERS="$NUM_PLAYERS" \
|
||||
PARALLEL="$PARALLEL" \
|
||||
MCTS_DECISION_BUDGET_MS=2000 \
|
||||
AI_USE_MCTS=true \
|
||||
AI_PIN_PERSONALITY_P0=ironhold \
|
||||
AI_PIN_PERSONALITY_P1=blackhammer \
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue