feat(@projects/@magic-civilization): add asymmetric difficulty mode

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-04-18 20:39:42 -07:00
parent 5c0e39fffe
commit a155802049
4 changed files with 55 additions and 5 deletions

View file

@ -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 <smoke|clan|difficulty|gpu-walltime> [args]}"
MODE="${1:?usage: apricot-run.sh <smoke|clan|difficulty|difficulty-asym|gpu-walltime> [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 <p0-tier> <p1-tier> [seeds] [turns]}"
P1_TIER="${2:?usage: apricot-run.sh difficulty-asym <p0-tier> <p1-tier> [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}"

View file

@ -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=<tier> and AI_DIFFICULTY_P1=<tier> 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

View file

@ -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:

View file

@ -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:-}' \