feat(@projects/@magic-civilization): ✨ update difficulty modifiers and thresholds
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
c81744b01e
commit
4c8dbbe29c
7 changed files with 91 additions and 21 deletions
|
|
@ -6,12 +6,13 @@
|
||||||
"name": "Easy",
|
"name": "Easy",
|
||||||
"description": "AI receives penalties. Ideal for learning the game.",
|
"description": "AI receives penalties. Ideal for learning the game.",
|
||||||
"ai_modifiers": {
|
"ai_modifiers": {
|
||||||
"production_mult": 0.80,
|
"production_mult": 0.70,
|
||||||
"research_mult": 0.90,
|
"research_mult": 0.80,
|
||||||
"gold_mult": 1.0,
|
"gold_mult": 1.0,
|
||||||
"combat_bonus": 0,
|
"combat_bonus": 0,
|
||||||
"extra_starting_units": 0,
|
"extra_starting_units": 0,
|
||||||
"starting_gold_bonus": 0
|
"starting_gold_bonus": 0,
|
||||||
|
"difficulty_threshold_mult": 0.85
|
||||||
},
|
},
|
||||||
"player_modifiers": {
|
"player_modifiers": {
|
||||||
"production_mult": 1.0,
|
"production_mult": 1.0,
|
||||||
|
|
@ -29,7 +30,8 @@
|
||||||
"gold_mult": 1.0,
|
"gold_mult": 1.0,
|
||||||
"combat_bonus": 0,
|
"combat_bonus": 0,
|
||||||
"extra_starting_units": 0,
|
"extra_starting_units": 0,
|
||||||
"starting_gold_bonus": 0
|
"starting_gold_bonus": 0,
|
||||||
|
"difficulty_threshold_mult": 1.00
|
||||||
},
|
},
|
||||||
"player_modifiers": {
|
"player_modifiers": {
|
||||||
"production_mult": 1.0,
|
"production_mult": 1.0,
|
||||||
|
|
@ -40,14 +42,15 @@
|
||||||
"level": 3,
|
"level": 3,
|
||||||
"id": "hard",
|
"id": "hard",
|
||||||
"name": "Hard",
|
"name": "Hard",
|
||||||
"description": "AI receives minor bonuses. A fair challenge for experienced players.",
|
"description": "AI receives significant bonuses. A fair challenge for experienced players.",
|
||||||
"ai_modifiers": {
|
"ai_modifiers": {
|
||||||
"production_mult": 1.10,
|
"production_mult": 1.30,
|
||||||
"research_mult": 1.0,
|
"research_mult": 1.20,
|
||||||
"gold_mult": 1.0,
|
"gold_mult": 1.0,
|
||||||
"combat_bonus": 0,
|
"combat_bonus": 0,
|
||||||
"extra_starting_units": 0,
|
"extra_starting_units": 0,
|
||||||
"starting_gold_bonus": 50
|
"starting_gold_bonus": 75,
|
||||||
|
"difficulty_threshold_mult": 1.15
|
||||||
},
|
},
|
||||||
"player_modifiers": {
|
"player_modifiers": {
|
||||||
"production_mult": 1.0,
|
"production_mult": 1.0,
|
||||||
|
|
@ -60,13 +63,14 @@
|
||||||
"name": "Insane",
|
"name": "Insane",
|
||||||
"description": "AI receives major bonuses and a free starting warrior. A serious challenge.",
|
"description": "AI receives major bonuses and a free starting warrior. A serious challenge.",
|
||||||
"ai_modifiers": {
|
"ai_modifiers": {
|
||||||
"production_mult": 1.20,
|
"production_mult": 1.50,
|
||||||
"research_mult": 1.10,
|
"research_mult": 1.40,
|
||||||
"gold_mult": 1.0,
|
"gold_mult": 1.0,
|
||||||
"combat_bonus": 0,
|
"combat_bonus": 0,
|
||||||
"extra_starting_units": 1,
|
"extra_starting_units": 1,
|
||||||
"extra_unit_id": "warrior",
|
"extra_unit_id": "warrior",
|
||||||
"starting_gold_bonus": 0
|
"starting_gold_bonus": 150,
|
||||||
|
"difficulty_threshold_mult": 1.25
|
||||||
},
|
},
|
||||||
"player_modifiers": {
|
"player_modifiers": {
|
||||||
"production_mult": 1.0,
|
"production_mult": 1.0,
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ done
|
||||||
# MODE + positional args resolved early so the resource-policy block can
|
# 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
|
# 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).
|
# because $1 is the clan_id; for smoke/gpu-walltime it's $1).
|
||||||
MODE="${1:?usage: apricot-run.sh <smoke|clan|gpu-walltime> [args]}"
|
MODE="${1:?usage: apricot-run.sh <smoke|clan|difficulty|gpu-walltime> [args]}"
|
||||||
shift || true
|
shift || true
|
||||||
|
|
||||||
# ── Resource policy for PARALLEL + RAYON_NUM_THREADS ─────────────────
|
# ── Resource policy for PARALLEL + RAYON_NUM_THREADS ─────────────────
|
||||||
|
|
@ -49,8 +49,9 @@ shift || true
|
||||||
# core. Better: PARALLEL = number of seeds (one instance each), and
|
# core. Better: PARALLEL = number of seeds (one instance each), and
|
||||||
# RAYON_NUM_THREADS = nproc / PARALLEL so the box is saturated evenly.
|
# RAYON_NUM_THREADS = nproc / PARALLEL so the box is saturated evenly.
|
||||||
case "${MODE}" in
|
case "${MODE}" in
|
||||||
clan) _seed_count_peek="${2:-10}" ;; # $1 is clan_id, $2 is seeds
|
clan) _seed_count_peek="${2:-10}" ;; # $1 is clan_id, $2 is seeds
|
||||||
*) _seed_count_peek="${1:-10}" ;; # smoke, gpu-walltime
|
difficulty) _seed_count_peek="${2:-10}" ;; # $1 is tier, $2 is seeds
|
||||||
|
*) _seed_count_peek="${1:-10}" ;; # smoke, gpu-walltime
|
||||||
esac
|
esac
|
||||||
|
|
||||||
NPROC="$(ssh "${APRICOT}" nproc 2>/dev/null || echo 8)"
|
NPROC="$(ssh "${APRICOT}" nproc 2>/dev/null || echo 8)"
|
||||||
|
|
@ -187,6 +188,15 @@ case "${MODE}" in
|
||||||
AI_USE_MCTS=true AI_PIN_PERSONALITY='${CLAN}' ${GPU_ENV} PARALLEL=${PARALLEL} \
|
AI_USE_MCTS=true AI_PIN_PERSONALITY='${CLAN}' ${GPU_ENV} PARALLEL=${PARALLEL} \
|
||||||
bash tools/autoplay-batch.sh ${SEEDS} ${TURNS} ${RESULTS_ABS}/clan-${CLAN} 2>&1 | tail -30"
|
bash tools/autoplay-batch.sh ${SEEDS} ${TURNS} ${RESULTS_ABS}/clan-${CLAN} 2>&1 | tail -30"
|
||||||
;;
|
;;
|
||||||
|
difficulty)
|
||||||
|
DIFF_TIER="${1:?usage: apricot-run.sh difficulty <easy|normal|hard|insane> [seeds] [turns]}"
|
||||||
|
SEEDS="${2:-10}"; TURNS="${3:-300}"
|
||||||
|
GPU_ENV="AI_GPU_ROLLOUT=${AI_GPU_ROLLOUT:-false}"
|
||||||
|
echo "[$(date +%H:%M:%S)] difficulty=${DIFF_TIER} batch: ${SEEDS} seeds T${TURNS} PARALLEL=${PARALLEL} ${GPU_ENV}"
|
||||||
|
ssh "${APRICOT}" "set -euo pipefail; cd '${SCRATCH_ABS}' && \
|
||||||
|
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"
|
||||||
|
;;
|
||||||
gpu-walltime)
|
gpu-walltime)
|
||||||
SEEDS="${1:-10}"; TURNS="${2:-300}"
|
SEEDS="${1:-10}"; TURNS="${2:-300}"
|
||||||
echo "[$(date +%H:%M:%S)] GPU wall-time comparison: ${SEEDS} seeds T${TURNS}"
|
echo "[$(date +%H:%M:%S)] GPU wall-time comparison: ${SEEDS} seeds T${TURNS}"
|
||||||
|
|
|
||||||
|
|
@ -568,6 +568,7 @@ func _process(_delta: float) -> void:
|
||||||
_turn_count = GameState.turn_number
|
_turn_count = GameState.turn_number
|
||||||
else:
|
else:
|
||||||
_fix_start_positions_if_needed()
|
_fix_start_positions_if_needed()
|
||||||
|
_apply_difficulty_starting_bonuses()
|
||||||
# Test-only scaffold — forces scout near a low-tier lair to exercise
|
# Test-only scaffold — forces scout near a low-tier lair to exercise
|
||||||
# loot-drop path in a 100-turn smoke. Biases normal batches; keep gated.
|
# loot-drop path in a 100-turn smoke. Biases normal batches; keep gated.
|
||||||
if EnvConfig.get_bool("AUTO_PLAY_TEST_LOOT_SCAFFOLD"):
|
if EnvConfig.get_bool("AUTO_PLAY_TEST_LOOT_SCAFFOLD"):
|
||||||
|
|
@ -790,6 +791,28 @@ func _fix_start_positions_if_needed() -> void:
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_difficulty_starting_bonuses() -> void:
|
||||||
|
var gold_bonus: int = GameState.ai_starting_gold_bonus
|
||||||
|
var extra_units: int = GameState.ai_extra_starting_units
|
||||||
|
var extra_unit_id: String = GameState.ai_extra_unit_id
|
||||||
|
if gold_bonus == 0 and extra_units == 0:
|
||||||
|
return
|
||||||
|
var unit_manager: Node = get_node_or_null("/root/UnitManager")
|
||||||
|
for p: Variant in GameState.players:
|
||||||
|
if p is PlayerScript and not p.is_human:
|
||||||
|
if gold_bonus > 0:
|
||||||
|
p.gold += gold_bonus
|
||||||
|
if extra_units > 0 and unit_manager != null:
|
||||||
|
for city: Variant in p.cities:
|
||||||
|
for _i: int in range(extra_units):
|
||||||
|
unit_manager.create_unit(extra_unit_id, p.index, city.position, p)
|
||||||
|
if gold_bonus > 0 or extra_units > 0:
|
||||||
|
print(
|
||||||
|
"AutoPlay: difficulty bonuses applied — gold+%d extra_units=%d (%s)"
|
||||||
|
% [gold_bonus, extra_units, extra_unit_id]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ── Player Actions ───────────────────────────────────────────────────
|
# ── Player Actions ───────────────────────────────────────────────────
|
||||||
|
|
||||||
func _play_turn() -> void:
|
func _play_turn() -> void:
|
||||||
|
|
|
||||||
|
|
@ -70,10 +70,23 @@ var map_seed: int = 0
|
||||||
## a save reproduces the same random trajectory as the original run.
|
## a save reproduces the same random trajectory as the original run.
|
||||||
var game_rng: RandomNumberGenerator = RandomNumberGenerator.new()
|
var game_rng: RandomNumberGenerator = RandomNumberGenerator.new()
|
||||||
|
|
||||||
## Difficulty modifier applied to AI production/science each turn (set by AIPlayer).
|
## Difficulty modifier applied to AI production each turn.
|
||||||
## 1.0 = even, <1.0 = AI penalty, >1.0 = AI bonus.
|
## 1.0 = even, <1.0 = AI penalty, >1.0 = AI bonus.
|
||||||
var ai_difficulty_modifier: float = 1.0
|
var ai_difficulty_modifier: float = 1.0
|
||||||
|
|
||||||
|
## Difficulty modifier applied to AI research (science) each turn.
|
||||||
|
## Separate from production so Easy can penalise production more than research.
|
||||||
|
var ai_research_modifier: float = 1.0
|
||||||
|
|
||||||
|
## Gold added to every AI player at game start for the current difficulty tier.
|
||||||
|
var ai_starting_gold_bonus: int = 0
|
||||||
|
|
||||||
|
## Extra warrior-class units spawned per AI city at game start.
|
||||||
|
var ai_extra_starting_units: int = 0
|
||||||
|
|
||||||
|
## ID of the extra starting unit (e.g. "warrior").
|
||||||
|
var ai_extra_unit_id: String = "warrior"
|
||||||
|
|
||||||
## Diplomatic relations between players.
|
## Diplomatic relations between players.
|
||||||
## Key: "min_idx_max_idx", value: "neutral" | "war" | "peace" | "alliance".
|
## Key: "min_idx_max_idx", value: "neutral" | "war" | "peace" | "alliance".
|
||||||
var diplomacy: Dictionary = {}
|
var diplomacy: Dictionary = {}
|
||||||
|
|
@ -201,8 +214,8 @@ func get_era_count() -> int:
|
||||||
|
|
||||||
|
|
||||||
func apply_ai_difficulty() -> void:
|
func apply_ai_difficulty() -> void:
|
||||||
## Read game_settings["difficulty"] (id) and set ai_difficulty_modifier from
|
## Read game_settings["difficulty"] (id) and populate all ai_difficulty_* fields
|
||||||
## difficulty.json ai_modifiers.production_mult. Called after setup finishes.
|
## from difficulty.json ai_modifiers. Called after setup finishes.
|
||||||
var diff_id: String = str(game_settings.get("difficulty", "normal"))
|
var diff_id: String = str(game_settings.get("difficulty", "normal"))
|
||||||
var diff_data: Dictionary = DataLoader.get_data("difficulty") as Dictionary
|
var diff_data: Dictionary = DataLoader.get_data("difficulty") as Dictionary
|
||||||
if diff_data == null or diff_data.is_empty():
|
if diff_data == null or diff_data.is_empty():
|
||||||
|
|
@ -212,10 +225,20 @@ func apply_ai_difficulty() -> void:
|
||||||
if entry.get("id", "") == diff_id:
|
if entry.get("id", "") == diff_id:
|
||||||
var mods: Dictionary = entry.get("ai_modifiers", {}) as Dictionary
|
var mods: Dictionary = entry.get("ai_modifiers", {}) as Dictionary
|
||||||
ai_difficulty_modifier = float(mods.get("production_mult", 1.0))
|
ai_difficulty_modifier = float(mods.get("production_mult", 1.0))
|
||||||
|
ai_research_modifier = float(mods.get("research_mult", 1.0))
|
||||||
|
ai_starting_gold_bonus = int(mods.get("starting_gold_bonus", 0))
|
||||||
|
ai_extra_starting_units = int(mods.get("extra_starting_units", 0))
|
||||||
|
ai_extra_unit_id = str(mods.get("extra_unit_id", "warrior"))
|
||||||
print(
|
print(
|
||||||
(
|
(
|
||||||
"GameState: difficulty=%s ai_modifier=%.2f"
|
"GameState: difficulty=%s prod=%.2f research=%.2f gold_bonus=%d extra_units=%d"
|
||||||
% [diff_id, ai_difficulty_modifier]
|
% [
|
||||||
|
diff_id,
|
||||||
|
ai_difficulty_modifier,
|
||||||
|
ai_research_modifier,
|
||||||
|
ai_starting_gold_bonus,
|
||||||
|
ai_extra_starting_units
|
||||||
|
]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,7 @@ func _process_research(player: RefCounted) -> void: # Player
|
||||||
# Apply difficulty modifier for AI players
|
# Apply difficulty modifier for AI players
|
||||||
var sci_modifier: float = 1.0
|
var sci_modifier: float = 1.0
|
||||||
if player is PlayerScript and not player.is_human:
|
if player is PlayerScript and not player.is_human:
|
||||||
sci_modifier = GameState.ai_difficulty_modifier
|
sci_modifier = GameState.ai_research_modifier
|
||||||
|
|
||||||
player.research_progress += int(player.science_per_turn * sci_modifier)
|
player.research_progress += int(player.science_per_turn * sci_modifier)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -356,7 +356,7 @@ static func build_tile_yields_json(
|
||||||
static func _calculate_science_income(player: RefCounted) -> int:
|
static func _calculate_science_income(player: RefCounted) -> int:
|
||||||
var sci_modifier: float = 1.0
|
var sci_modifier: float = 1.0
|
||||||
if player is PlayerScript and not player.is_human:
|
if player is PlayerScript and not player.is_human:
|
||||||
sci_modifier = GameState.ai_difficulty_modifier
|
sci_modifier = GameState.ai_research_modifier
|
||||||
var science: int = int(player.science_per_turn * sci_modifier)
|
var science: int = int(player.science_per_turn * sci_modifier)
|
||||||
var game_map: RefCounted = GameState.get_game_map()
|
var game_map: RefCounted = GameState.get_game_map()
|
||||||
if game_map == null:
|
if game_map == null:
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,16 @@ pub struct TacticalState {
|
||||||
/// unlocks (p0-39). Empty vec falls back to tier-1 `warrior` only.
|
/// unlocks (p0-39). Empty vec falls back to tier-1 `warrior` only.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub unit_catalog: Vec<TacticalUnitSpec>,
|
pub unit_catalog: Vec<TacticalUnitSpec>,
|
||||||
|
/// Multiplicative scalar applied on top of all personality-axis-derived
|
||||||
|
/// thresholds (p0-24). Easy < 1.0 (overcommits), Hard > 1.0 (waits for
|
||||||
|
/// real superiority). Defaults to 1.0 (normal / unset). Populated from
|
||||||
|
/// `difficulty.json::ai_modifiers.difficulty_threshold_mult` by the bridge.
|
||||||
|
#[serde(default = "default_threshold_mult")]
|
||||||
|
pub difficulty_threshold_mult: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_threshold_mult() -> f32 {
|
||||||
|
1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hex map with row-major tile storage.
|
/// Hex map with row-major tile storage.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue