Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fab176b
Sweep Inferno state curriculum
valtterivalo May 26, 2026
609aab9
Add Inferno fast step-out forecast mode
valtterivalo May 26, 2026
1af094f
Add Inferno read-only step-out forecast
valtterivalo May 26, 2026
4463880
Sweep Inferno forecast modes
valtterivalo May 26, 2026
915a3ab
Restore Inferno spark landing observations
valtterivalo May 26, 2026
1497b6d
Broaden Inferno forecast mode sweep
valtterivalo May 26, 2026
c9ddd23
Prune Inferno sweep search space
valtterivalo May 26, 2026
ae56373
Update Inferno default from sweep best
valtterivalo May 26, 2026
9b547a6
Add Inferno curriculum supply variation
valtterivalo May 27, 2026
d889d8a
Add Inferno offensive prayer shaping
valtterivalo May 27, 2026
e2422f9
Shape Inferno offensive prayer reward by damage
valtterivalo May 27, 2026
f35c6a7
Record Inferno player damage on fire tick
valtterivalo May 27, 2026
894ca0b
Log Inferno idle diagnostics
valtterivalo May 27, 2026
88c2896
Keep Inferno log metrics within dict cap
valtterivalo May 28, 2026
7c39877
Remove unused Inferno log variable
valtterivalo May 28, 2026
6041ded
Reserve Inferno log slot for env count
valtterivalo May 28, 2026
74dce7e
Tune Inferno sweep space
valtterivalo May 28, 2026
3fee699
Fix Inferno eval control hint
valtterivalo May 28, 2026
b59e830
Show Inferno wave in eval HUD
valtterivalo May 28, 2026
f52dead
Fix minimap compass render
valtterivalo May 28, 2026
112243e
Fix Inferno lab pause and restore
valtterivalo May 28, 2026
449b62a
Simplify Inferno eval glue
valtterivalo May 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 118 additions & 108 deletions config/ocean/osrs_inferno.ini

Large diffs are not rendered by default.

848 changes: 832 additions & 16 deletions ocean/osrs/encounters/inferno/encounter_inferno_forecast.inc

Large diffs are not rendered by default.

66 changes: 54 additions & 12 deletions ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,31 @@ static InfTargetArea inf_npc_current_target_area(const InfernoState* s, const In
};
}

/* check if NPC at index i has LOS to its current target */
static int inf_npc_has_los_direct(InfernoState* s, int i) {
InfNPC* npc = &s->npcs[i];
static int inf_npc_has_los_to_area(
const InfernoState* s,
const InfNPC* npc,
int target_x,
int target_y,
int target_size
) {
const InfNPCStats* stats = &INF_NPC_STATS[npc->type];
InfTargetArea target = inf_npc_current_target_area(s, npc);
return entity_has_line_of_sight(s->los_blockers, s->los_blocker_count,
npc->x, npc->y, npc->size,
target.x, target.y, target.size,
stats->attack_range);
npc->x, npc->y, npc->size,
target_x, target_y, target_size,
stats->attack_range);
}

static int inf_npc_has_los_to_tile(
const InfernoState* s, const InfNPC* npc, int target_x, int target_y
) {
return inf_npc_has_los_to_area(s, npc, target_x, target_y, 1);
}

/* check if NPC at index i has LOS to its current target */
static int inf_npc_has_los_direct(const InfernoState* s, int i) {
const InfNPC* npc = &s->npcs[i];
InfTargetArea target = inf_npc_current_target_area(s, npc);
return inf_npc_has_los_to_area(s, npc, target.x, target.y, target.size);
}

/* cached LOS check — lazy: computes on first access per tick, caches for reuse.
Expand Down Expand Up @@ -350,8 +366,8 @@ static inline int inf_cardinal_contact_with_npc(int px, int py, int nx, int ny,
return dx + dy == 1;
}

static inline int inf_melee_fallback_possible(
const InfernoState* s, const InfNPC* npc, const InfNPCStats* stats,
static inline int inf_melee_fallback_possible_at_tile(
int player_x, int player_y, const InfNPC* npc, const InfNPCStats* stats,
int planned_style, int dist
) {
if (!stats->can_melee || planned_style == ATTACK_STYLE_MELEE || dist != 1)
Expand All @@ -364,22 +380,39 @@ static inline int inf_melee_fallback_possible(
case INF_NPC_BLOB:
case INF_NPC_JAD:
return inf_cardinal_contact_with_npc(
s->player.x, s->player.y, npc->x, npc->y, npc->size);
player_x, player_y, npc->x, npc->y, npc->size);
default:
return 0;
}
}

static inline int inf_attack_style_options_mask(
static inline int inf_melee_fallback_possible(
const InfernoState* s, const InfNPC* npc, const InfNPCStats* stats,
int planned_style, int dist
) {
return inf_melee_fallback_possible_at_tile(
s->player.x, s->player.y, npc, stats, planned_style, dist);
}

static inline int inf_attack_style_options_mask_at_tile(
int player_x, int player_y, const InfNPC* npc, const InfNPCStats* stats,
int planned_style, int dist
) {
int mask = inf_attack_style_mask_bit(planned_style);
if (inf_melee_fallback_possible(s, npc, stats, planned_style, dist))
if (inf_melee_fallback_possible_at_tile(
player_x, player_y, npc, stats, planned_style, dist))
mask |= INF_STYLE_MASK_MELEE;
return mask;
}

static inline int inf_attack_style_options_mask(
const InfernoState* s, const InfNPC* npc, const InfNPCStats* stats,
int planned_style, int dist
) {
return inf_attack_style_options_mask_at_tile(
s->player.x, s->player.y, npc, stats, planned_style, dist);
}

static inline int inf_attack_style_from_mask(int style_mask) {
if (style_mask == INF_STYLE_MASK_MELEE) return ATTACK_STYLE_MELEE;
if (style_mask == INF_STYLE_MASK_RANGED) return ATTACK_STYLE_RANGED;
Expand Down Expand Up @@ -597,14 +630,23 @@ static InfConfig inf_default_config(void) {
.loadout_profile_mode = INF_LOADOUT_PROFILE_MODE_MAX_ONLY,
.budget_loadout_fraction = 0.0f,
.damage_reward_coeff = 0.0f,
.offensive_prayer_reward_coeff = 0.0f,
.shield_penalty_coeff = 0.0f,
.tag_reward_coeff = 0.0f,
.late_start_supply_profile_scale = 1.0f,
.curriculum_agent = 0,
.curriculum_supply_jitter_mode = INF_CURRICULUM_SUPPLY_MODE_OFF,
.curriculum_supply_shared_jitter = 0.0f,
.curriculum_supply_brew_jitter = 0.0f,
.curriculum_supply_restore_jitter = 0.0f,
.curriculum_no_brew_mode = INF_CURRICULUM_SUPPLY_MODE_OFF,
.curriculum_no_brew_frac = 0.0f,
.supply_milestone_brew_reward_coeff = 0.0f,
.supply_milestone_restore_reward_coeff = 0.0f,
.death_penalty_coeff = 0.0f,
.terminal_penalty_enabled = 0,
.step_out_forecast_obs_enabled = 1,
.step_out_forecast_obs_mode = INF_STEP_OUT_FORECAST_MODE_EXACT_ROLLOUT,
.phase_900_bonus = 0.0f,
.phase_600_bonus = 0.0f,
.phase_300_bonus = 0.0f,
Expand Down
2 changes: 2 additions & 0 deletions ocean/osrs/encounters/inferno/encounter_inferno_lab.inc
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ static void inf_lab_clear_transient(InfernoState* s) {
s->player_pending_hit_count = 0;
memset(s->pending_sparks, 0, sizeof(s->pending_sparks));
memset(s->npc_target_hits, 0, sizeof(s->npc_target_hits));
s->offensive_prayer_correct_damage_roll_this_tick = 0.0f;
s->offensive_prayer_correct_this_tick = 0;
s->player_attacked_this_tick = 0;
s->player_attack_npc_idx = -1;
s->player_attack_dmg = 0;
Expand Down
46 changes: 46 additions & 0 deletions ocean/osrs/encounters/inferno/encounter_inferno_model.inc
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,29 @@ enum {
INF_JOSEPH_REWARD_MODE_ON = 1,
};

enum {
INF_STEP_OUT_FORECAST_MODE_OFF = 0,
INF_STEP_OUT_FORECAST_MODE_EXACT_ROLLOUT = 1,
INF_STEP_OUT_FORECAST_MODE_FAST_STATIC_TILE = 2,
INF_STEP_OUT_FORECAST_MODE_FAST_READONLY_MOVE = 3,
};

enum {
INF_CURRICULUM_SUPPLY_MODE_OFF = 0,
INF_CURRICULUM_SUPPLY_MODE_ALL = 1,
INF_CURRICULUM_SUPPLY_MODE_ZUK = 2,
INF_CURRICULUM_SUPPLY_MODE_PRE_ZUK = 3,
};

typedef enum {
INF_IDLE_PHASE_SET = 0,
INF_IDLE_PHASE_JAD = 1,
INF_IDLE_PHASE_ZUK_PRE_JAD = 2,
INF_IDLE_PHASE_ZUK_JAD = 3,
INF_IDLE_PHASE_ZUK_HEALERS = 4,
INF_IDLE_PHASE_ZUK_POST_HEALERS = 5,
} InfIdleDiagnosticPhase;

typedef struct {
int brew_doses;
int restore_doses;
Expand All @@ -631,14 +654,23 @@ typedef struct {
InfLoadoutProfileMode loadout_profile_mode;
float budget_loadout_fraction;
float damage_reward_coeff;
float offensive_prayer_reward_coeff;
float shield_penalty_coeff;
float tag_reward_coeff;
float late_start_supply_profile_scale;
int curriculum_agent;
int curriculum_supply_jitter_mode;
float curriculum_supply_shared_jitter;
float curriculum_supply_brew_jitter;
float curriculum_supply_restore_jitter;
int curriculum_no_brew_mode;
float curriculum_no_brew_frac;
float supply_milestone_brew_reward_coeff;
float supply_milestone_restore_reward_coeff;
float death_penalty_coeff;
int terminal_penalty_enabled;
int step_out_forecast_obs_enabled;
int step_out_forecast_obs_mode;
float phase_900_bonus;
float phase_600_bonus;
float phase_300_bonus;
Expand Down Expand Up @@ -752,6 +784,8 @@ typedef struct {
float damage_received_this_tick;
float hp_restored_this_tick;
float hp_restored_zuk_this_tick;
float offensive_prayer_correct_damage_roll_this_tick;
int offensive_prayer_correct_this_tick;
int prayer_correct_this_tick; /* count of NPC attacks blocked by prayer this tick */
int wave_completed_this_tick;
int pillar_lost_this_tick; /* -1 = none, 0-2 = which pillar was destroyed */
Expand All @@ -775,6 +809,10 @@ typedef struct {
int total_prayer_correct; /* times prayer blocked an NPC attack */
int total_npc_attacks; /* total NPC attacks on player (for prayer_correct_rate) */
int total_unavoidable_off; /* off-prayer hits where a different style was correctly prayed */
int total_offensive_prayer_attacks;
int total_offensive_prayer_correct;
int offensive_prayer_attacks_by_style[4];
int offensive_prayer_correct_by_style[4];
int off_prayer_hits_this_tick;
/* per-tick tracking for multi-style analysis */
int tick_styles_fired; /* bitmask of styles that fired this tick (bit0=mel,1=rng,2=mag) */
Expand Down Expand Up @@ -805,6 +843,14 @@ typedef struct {
int last_hit_by_type; /* NPC type that last dealt damage to player (-1=none) */
int killed_by_type[INF_NUM_NPC_TYPES]; /* count of deaths caused by each NPC type */
int total_idle_ticks; /* cumulative ticks of ticks_without_action > 0 */
int total_attack_ready_no_attack_ticks;
int total_target_available_no_attack_ticks;
int total_safe_attack_opportunity_missed_ticks;
int total_progressless_ticks;
int attack_ready_no_attack_ticks_by_phase[OSRS_INFERNO_IDLE_PHASE_COUNT];
int target_available_no_attack_ticks_by_phase[OSRS_INFERNO_IDLE_PHASE_COUNT];
int safe_attack_opportunity_missed_ticks_by_phase[OSRS_INFERNO_IDLE_PHASE_COUNT];
int progressless_ticks_by_phase[OSRS_INFERNO_IDLE_PHASE_COUNT];
int total_brews_used; /* brew doses consumed this episode */
int total_blood_healed; /* HP healed via blood barrage this episode */
int total_npc_kills; /* NPCs killed this episode */
Expand Down
127 changes: 40 additions & 87 deletions ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc
Original file line number Diff line number Diff line change
Expand Up @@ -43,82 +43,25 @@ static int inf_npc_is_phantom_barrage_targetable_now(
inf_player_can_phantom_barrage_npc(s, ctx, npc_idx);
}

typedef struct {
int active;
int src_x;
int src_y;
int earliest_ticks_remaining;
int total_damage;
} InfSparkObsBucket;

static int inf_spark_bucket_obs_less(
static int inf_pending_spark_obs_less(
const InfernoState* s,
const InfSparkObsBucket* a,
const InfSparkObsBucket* b
const InfPendingSpark* a,
int ai,
const InfPendingSpark* b,
int bi
) {
if (a->earliest_ticks_remaining != b->earliest_ticks_remaining)
return a->earliest_ticks_remaining < b->earliest_ticks_remaining;
int adx = abs(a->src_x - s->player.x);
int ady = abs(a->src_y - s->player.y);
int bdx = abs(b->src_x - s->player.x);
int bdy = abs(b->src_y - s->player.y);
if (a->ticks_remaining != b->ticks_remaining)
return a->ticks_remaining < b->ticks_remaining;
int adx = abs(a->x - s->player.x);
int ady = abs(a->y - s->player.y);
int bdx = abs(b->x - s->player.x);
int bdy = abs(b->y - s->player.y);
int ad = adx > ady ? adx : ady;
int bd = bdx > bdy ? bdx : bdy;
if (ad != bd) return ad < bd;
if (a->src_x != b->src_x) return a->src_x < b->src_x;
return a->src_y < b->src_y;
}

static int inf_build_spark_obs_buckets(
const InfernoState* s,
InfSparkObsBucket* buckets,
int capacity
) {
int count = 0;
for (int i = 0; i < INF_MAX_PENDING_SPARKS; i++) {
const InfPendingSpark* spark = &s->pending_sparks[i];
if (!spark->active) continue;

int bucket_idx = -1;
for (int b = 0; b < count; b++) {
if (buckets[b].src_x == spark->src_x &&
buckets[b].src_y == spark->src_y) {
bucket_idx = b;
break;
}
}

if (bucket_idx < 0) {
if (count >= capacity) {
fprintf(stderr, "BUG: spark obs bucket overflow\n");
abort();
}
bucket_idx = count++;
buckets[bucket_idx] = (InfSparkObsBucket){
.active = 1,
.src_x = spark->src_x,
.src_y = spark->src_y,
.earliest_ticks_remaining = spark->ticks_remaining,
.total_damage = 0,
};
}

if (spark->ticks_remaining < buckets[bucket_idx].earliest_ticks_remaining)
buckets[bucket_idx].earliest_ticks_remaining = spark->ticks_remaining;
buckets[bucket_idx].total_damage += spark->damage;
}

for (int i = 0; i < count; i++) {
for (int j = i + 1; j < count; j++) {
if (inf_spark_bucket_obs_less(s, &buckets[j], &buckets[i])) {
InfSparkObsBucket tmp = buckets[i];
buckets[i] = buckets[j];
buckets[j] = tmp;
}
}
}

return count;
if (a->x != b->x) return a->x < b->x;
if (a->y != b->y) return a->y < b->y;
return ai < bi;
}

static void inf_refresh_current_obs_slots_ctx(InfernoState* s, const InfernoContext* ctx) {
Expand Down Expand Up @@ -495,9 +438,9 @@ static void inf_write_obs_ctx(
}

{
if (ctx->config.step_out_forecast_obs_enabled) {
if (ctx->config.step_out_forecast_obs_mode != INF_STEP_OUT_FORECAST_MODE_OFF) {
InfStepOutForecast forecast;
inf_build_step_out_forecast_ctx(s, ctx, &forecast);
inf_build_step_out_forecast_mode_ctx(s, ctx, &forecast);
for (int action_idx = 0; action_idx < ENCOUNTER_MOVE_ACTIONS; action_idx++) {
const InfStepOutForecastAction* action = &forecast.actions[action_idx];
int first_attack_tick = 0;
Expand Down Expand Up @@ -560,22 +503,32 @@ static void inf_write_obs_ctx(
INF_PROFILE_MARK(INF_PROF_OBS_PENDING_HITS);
#endif

{
InfSparkObsBucket buckets[INF_MAX_PENDING_SPARKS];
memset(buckets, 0, sizeof(buckets));
int bucket_count = inf_build_spark_obs_buckets(s, buckets, INF_MAX_PENDING_SPARKS);
for (int slot = 0; slot < INF_SPARK_OBS_SLOTS; slot++) {
if (slot < bucket_count) {
const InfSparkObsBucket* bucket = &buckets[slot];
obs[i++] = 1.0f;
obs[i++] = (float)(bucket->src_x - px) / (float)INF_ARENA_WIDTH;
obs[i++] = (float)(bucket->src_y - py) / (float)INF_ARENA_HEIGHT;
obs[i++] = (float)bucket->earliest_ticks_remaining / 10.0f;
obs[i++] = (float)bucket->total_damage / 10.0f;
} else {
for (int j = 0; j < INF_FEATURES_PER_SPARK; j++) obs[i++] = 0.0f;
int used_sparks[INF_MAX_PENDING_SPARKS] = {0};
for (int slot = 0; slot < INF_SPARK_OBS_SLOTS; slot++) {
int best = -1;
for (int sp = 0; sp < INF_MAX_PENDING_SPARKS; sp++) {
if (used_sparks[sp] || !s->pending_sparks[sp].active) continue;
if (best < 0 ||
inf_pending_spark_obs_less(
s, &s->pending_sparks[sp], sp,
&s->pending_sparks[best], best)) {
best = sp;
}
}

if (best >= 0) {
const InfPendingSpark* spark = &s->pending_sparks[best];
used_sparks[best] = 1;
obs[i++] = 1.0f;
obs[i++] = (float)(spark->x - px) / (float)INF_ARENA_WIDTH;
obs[i++] = (float)(spark->y - py) / (float)INF_ARENA_HEIGHT;
obs[i++] = (float)(spark->src_x - px) / (float)INF_ARENA_WIDTH;
obs[i++] = (float)(spark->src_y - py) / (float)INF_ARENA_HEIGHT;
obs[i++] = (float)spark->ticks_remaining / 10.0f;
obs[i++] = (float)spark->damage / 10.0f;
} else {
for (int j = 0; j < INF_FEATURES_PER_SPARK; j++) obs[i++] = 0.0f;
}
}
#ifdef INF_PROFILE_ENABLED
INF_PROFILE_MARK(INF_PROF_OBS_SPARKS);
Expand Down
Loading
Loading