From a1558020491d80f0c325dca237aca722457d34a5 Mon Sep 17 00:00:00 2001 From: Natalie Date: Sat, 18 Apr 2026 20:39:42 -0700 Subject: [PATCH] =?UTF-8?q?feat(@projects/@magic-civilization):=20?= =?UTF-8?q?=E2=9C=A8=20add=20asymmetric=20difficulty=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- scripts/apricot-run.sh | 21 ++++++++++--- src/game/engine/scenes/tests/auto_play.gd | 30 +++++++++++++++++++ .../management/turn_processor_helpers.gd | 5 +++- tools/autoplay-batch.sh | 4 +++ 4 files changed, 55 insertions(+), 5 deletions(-) diff --git a/scripts/apricot-run.sh b/scripts/apricot-run.sh index 5acefc03..7c4f639e 100755 --- a/scripts/apricot-run.sh +++ b/scripts/apricot-run.sh @@ -38,7 +38,7 @@ done # MODE + positional args resolved early so the resource-policy block can # peek at the seed count (which differs per mode — for `clan` it's $2 # because $1 is the clan_id; for smoke/gpu-walltime it's $1). -MODE="${1:?usage: apricot-run.sh [args]}" +MODE="${1:?usage: apricot-run.sh [args]}" shift || true # ── Resource policy for PARALLEL + RAYON_NUM_THREADS ───────────────── @@ -49,9 +49,10 @@ shift || true # core. Better: PARALLEL = number of seeds (one instance each), and # RAYON_NUM_THREADS = nproc / PARALLEL so the box is saturated evenly. case "${MODE}" in - clan) _seed_count_peek="${2:-10}" ;; # $1 is clan_id, $2 is seeds - difficulty) _seed_count_peek="${2:-10}" ;; # $1 is tier, $2 is seeds - *) _seed_count_peek="${1:-10}" ;; # smoke, gpu-walltime + clan) _seed_count_peek="${2:-10}" ;; # $1 is clan_id, $2 is seeds + difficulty) _seed_count_peek="${2:-10}" ;; # $1 is tier, $2 is seeds + difficulty-asym) _seed_count_peek="${3:-10}" ;; # $1 p0 tier, $2 p1 tier, $3 seeds + *) _seed_count_peek="${1:-10}" ;; # smoke, gpu-walltime esac NPROC="$(ssh "${APRICOT}" nproc 2>/dev/null || echo 8)" @@ -197,6 +198,18 @@ case "${MODE}" in AI_USE_MCTS=true AI_DIFFICULTY='${DIFF_TIER}' ${GPU_ENV} PARALLEL=${PARALLEL} \ bash tools/autoplay-batch.sh ${SEEDS} ${TURNS} ${RESULTS_ABS}/difficulty-${DIFF_TIER} 2>&1 | tail -30" ;; + difficulty-asym) + P0_TIER="${1:?usage: apricot-run.sh difficulty-asym [seeds] [turns]}" + P1_TIER="${2:?usage: apricot-run.sh difficulty-asym [seeds] [turns]}" + SEEDS="${3:-10}"; TURNS="${4:-300}" + GPU_ENV="AI_GPU_ROLLOUT=${AI_GPU_ROLLOUT:-false}" + echo "[$(date +%H:%M:%S)] difficulty-asym p0=${P0_TIER} p1=${P1_TIER}: ${SEEDS} seeds T${TURNS}" + ssh "${APRICOT}" "set -euo pipefail; cd '${SCRATCH_ABS}' && \ + AI_USE_MCTS=true AI_DIFFICULTY_P0='${P0_TIER}' AI_DIFFICULTY_P1='${P1_TIER}' \ + ${GPU_ENV} PARALLEL=${PARALLEL} \ + bash tools/autoplay-batch.sh ${SEEDS} ${TURNS} \ + ${RESULTS_ABS}/difficulty-asym-${P0_TIER}-vs-${P1_TIER} 2>&1 | tail -30" + ;; gpu-walltime) SEEDS="${1:-10}"; TURNS="${2:-300}" echo "[$(date +%H:%M:%S)] GPU wall-time comparison: ${SEEDS} seeds T${TURNS}" diff --git a/src/game/engine/scenes/tests/auto_play.gd b/src/game/engine/scenes/tests/auto_play.gd index 49b39a57..22f355e9 100644 --- a/src/game/engine/scenes/tests/auto_play.gd +++ b/src/game/engine/scenes/tests/auto_play.gd @@ -525,6 +525,10 @@ func _process(_delta: float) -> void: GameState.game_settings["difficulty"] = diff_env print("AutoPlay: AI_DIFFICULTY=%s applied" % diff_env) GameState.apply_ai_difficulty() + # Per-player difficulty overrides for asymmetric matchups (p0-24). + # AI_DIFFICULTY_P0= and AI_DIFFICULTY_P1= each override + # that player's production/research multipliers independently. + _apply_per_player_difficulty_overrides() _state = "wait_loading" _frame = 0 if _frame > 120: @@ -791,6 +795,32 @@ func _fix_start_positions_if_needed() -> void: ]) +func _apply_per_player_difficulty_overrides() -> void: + var diff_data: Dictionary = DataLoader.get_data("difficulty") + if diff_data == null: + return + var levels: Dictionary = {} + for entry: Dictionary in diff_data.get("ai_difficulty", []): + levels[str(entry.get("id", ""))] = entry.get("ai_modifiers", {}) + for p_idx: int in range(GameState.players.size()): + var key: String = "AI_DIFFICULTY_P%d" % p_idx + var tier: String = EnvConfig.get_var(key, "") + if tier.is_empty(): + continue + var mods: Dictionary = levels.get(tier, {}) + if mods.is_empty(): + print("AutoPlay: WARNING: unknown difficulty tier '%s' for %s" % [tier, key]) + continue + GameState.ai_per_player_production_mult[p_idx] = float(mods.get("production_mult", 1.0)) + GameState.ai_per_player_research_mult[p_idx] = float(mods.get("research_mult", 1.0)) + print( + "AutoPlay: %s=%s → player %d prod=%.2f research=%.2f" + % [key, tier, p_idx, + GameState.ai_per_player_production_mult[p_idx], + GameState.ai_per_player_research_mult[p_idx]] + ) + + func _apply_difficulty_starting_bonuses() -> void: var gold_bonus: int = GameState.ai_starting_gold_bonus var extra_units: int = GameState.ai_extra_starting_units diff --git a/src/game/engine/src/modules/management/turn_processor_helpers.gd b/src/game/engine/src/modules/management/turn_processor_helpers.gd index 7084f27c..924374df 100644 --- a/src/game/engine/src/modules/management/turn_processor_helpers.gd +++ b/src/game/engine/src/modules/management/turn_processor_helpers.gd @@ -359,7 +359,10 @@ static func build_tile_yields_json( static func _calculate_science_income(player: RefCounted) -> int: var sci_modifier: float = 1.0 if player is PlayerScript and not player.is_human: - sci_modifier = GameState.ai_research_modifier + var per_player: float = float( + GameState.ai_per_player_research_mult.get(player.index, 0.0) + ) + sci_modifier = per_player if per_player > 0.0 else GameState.ai_research_modifier var science: int = int(player.science_per_turn * sci_modifier) var game_map: RefCounted = GameState.get_game_map() if game_map == null: diff --git a/tools/autoplay-batch.sh b/tools/autoplay-batch.sh index b1db26c3..ef54096d 100755 --- a/tools/autoplay-batch.sh +++ b/tools/autoplay-batch.sh @@ -158,6 +158,8 @@ _run_local() { "--env=AUTO_PLAY_DIR=$game_dir" "--env=AP_RUN_ID=${STAMP}_seed$(printf '%03d' "$seed")" "--env=AI_DIFFICULTY=${AI_DIFFICULTY:-}" + "--env=AI_DIFFICULTY_P0=${AI_DIFFICULTY_P0:-}" + "--env=AI_DIFFICULTY_P1=${AI_DIFFICULTY_P1:-}" "--env=AI_PIN_PERSONALITY=${AI_PIN_PERSONALITY:-}" "--env=MAP_SIZE=${MAP_SIZE:-}" "--env=NUM_PLAYERS=${NUM_PLAYERS:-}" @@ -239,6 +241,8 @@ _run_remote() { AUTO_PLAY_DIR='$remote_game_dir' \ AP_RUN_ID="${STAMP}_seed$(printf '%03d' "$seed")" \ AI_DIFFICULTY='${AI_DIFFICULTY:-}' \ + AI_DIFFICULTY_P0='${AI_DIFFICULTY_P0:-}' \ + AI_DIFFICULTY_P1='${AI_DIFFICULTY_P1:-}' \ AI_PIN_PERSONALITY='${AI_PIN_PERSONALITY:-}' \ MAP_SIZE='${MAP_SIZE:-}' \ NUM_PLAYERS='${NUM_PLAYERS:-}' \