Skip to content

Commit b8c34e3

Browse files
fix: rank proc weapon candidates by slot role
Assisted-by: openai/gpt-5.4 on opencode Co-authored-by: chatgpt-codex-connector[bot] <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com>
1 parent 3819302 commit b8c34e3

File tree

2 files changed

+135
-0
lines changed

2 files changed

+135
-0
lines changed

src/proc_builder.cpp

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,104 @@ auto fact_soft_penalty( const proc::part_fact &fact ) -> int
164164
} );
165165
}
166166

167+
struct candidate_sort_entry {
168+
proc::part_ix ix = proc::invalid_part_ix;
169+
int score = 0;
170+
int uses = 0;
171+
int mass_g = 0;
172+
int volume_ml = 0;
173+
std::string id;
174+
};
175+
176+
auto weapon_role_score( const std::string &role, const proc::part_fact &fact ) -> int
177+
{
178+
const auto density = fact_density( fact );
179+
const auto bash_resist = fact_bash_resist( fact );
180+
const auto cut_resist = fact_cut_resist( fact );
181+
const auto chip_resist = fact_chip_resist( fact );
182+
const auto bullet_resist = fact_bullet_resist( fact );
183+
const auto reinforce_bonus = fact_reinforce_bonus( fact );
184+
const auto soft_penalty = fact_soft_penalty( fact );
185+
186+
if( role == "blade" ) {
187+
return density / 8 + cut_resist * 6 + chip_resist * 3 + bullet_resist * 2 +
188+
reinforce_bonus * 5 - soft_penalty * 4 + fact.mass_g / 120;
189+
}
190+
if( role == "guard" ) {
191+
return bash_resist * 5 + chip_resist * 3 + cut_resist * 2 + density / 10 +
192+
reinforce_bonus * 4 - soft_penalty * 3 + fact.mass_g / 180;
193+
}
194+
if( role == "handle" ) {
195+
return bash_resist * 3 + chip_resist * 2 + density / 14 + reinforce_bonus * 2 -
196+
soft_penalty * 2 + fact.mass_g / 220;
197+
}
198+
if( role == "grip" ) {
199+
return bash_resist * 3 + cut_resist * 2 + chip_resist * 2 + density / 12 +
200+
reinforce_bonus * 2 - soft_penalty * 3 + fact.mass_g / 260;
201+
}
202+
if( role == "reinforcement" ) {
203+
return reinforce_bonus * 8 + cut_resist * 3 + chip_resist * 3 + bash_resist * 2 +
204+
density / 10 - soft_penalty * 2 + fact.mass_g / 140;
205+
}
206+
return bash_resist * 2 + cut_resist * 2 + chip_resist * 2 + density / 16 +
207+
reinforce_bonus * 2 - soft_penalty * 2 + fact.mass_g / 200;
208+
}
209+
210+
auto build_candidate_sort_entry( const proc::slot_data &slot,
211+
const proc::part_fact &fact ) -> candidate_sort_entry
212+
{
213+
return candidate_sort_entry{
214+
.ix = fact.ix,
215+
.score = weapon_role_score( slot.role, fact ),
216+
.uses = fact.uses,
217+
.mass_g = fact.mass_g,
218+
.volume_ml = fact.volume_ml,
219+
.id = fact.id.str(),
220+
};
221+
}
222+
223+
auto sort_weapon_slot_candidates( const proc::slot_data &slot,
224+
const std::vector<proc::part_fact> &facts,
225+
std::vector<proc::part_ix> &slot_facts ) -> void
226+
{
227+
auto ranked = std::vector<candidate_sort_entry> {};
228+
ranked.reserve( slot_facts.size() );
229+
std::ranges::for_each( slot_facts, [&]( const proc::part_ix ix ) {
230+
const auto *fact = find_fact( facts, ix );
231+
if( fact == nullptr ) {
232+
return;
233+
}
234+
ranked.push_back( build_candidate_sort_entry( slot, *fact ) );
235+
} );
236+
237+
std::ranges::sort( ranked, []( const candidate_sort_entry & lhs,
238+
const candidate_sort_entry & rhs ) {
239+
if( lhs.score != rhs.score ) {
240+
return lhs.score > rhs.score;
241+
}
242+
if( lhs.uses != rhs.uses ) {
243+
return lhs.uses > rhs.uses;
244+
}
245+
if( lhs.mass_g != rhs.mass_g ) {
246+
return lhs.mass_g > rhs.mass_g;
247+
}
248+
if( lhs.volume_ml != rhs.volume_ml ) {
249+
return lhs.volume_ml > rhs.volume_ml;
250+
}
251+
if( lhs.id != rhs.id ) {
252+
return lhs.id < rhs.id;
253+
}
254+
return lhs.ix < rhs.ix;
255+
} );
256+
257+
slot_facts.clear();
258+
slot_facts.reserve( ranked.size() );
259+
std::ranges::transform( ranked, std::back_inserter( slot_facts ),
260+
[]( const candidate_sort_entry & entry ) {
261+
return entry.ix;
262+
} );
263+
}
264+
167265
auto role_count( const std::vector<proc::craft_pick> &picks, const proc::slot_id &slot ) -> int
168266
{
169267
return static_cast<int>( std::ranges::count_if( picks, [&]( const proc::craft_pick & pick ) {
@@ -556,6 +654,9 @@ auto proc::build_candidates( const schema &sch,
556654
slot_facts.push_back( fact.ix );
557655
}
558656
} );
657+
if( sch.id == proc::schema_id( "sword" ) || sch.cat == "weapon" ) {
658+
sort_weapon_slot_candidates( slot, facts, slot_facts );
659+
}
559660
ret.emplace( slot.id, std::move( slot_facts ) );
560661
} );
561662
return ret;

tests/proc_builder_test.cpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "catch/catch.hpp"
22

33
#include <fstream>
4+
#include <iterator>
45
#include <ranges>
56
#include <sstream>
67
#include <string>
@@ -839,6 +840,39 @@ TEST_CASE( "proc_builder_supports_sword_reinforcements_and_legacy_like_names",
839840
CHECK( crude.fast.mass_g > plain.fast.mass_g );
840841
}
841842

843+
TEST_CASE( "proc_builder_ranks_sword_candidates_by_role_quality", "[proc][builder][weapon]" )
844+
{
845+
const auto sch = load_schema_from_file( "data/json/proc/sword.json", "sword" );
846+
847+
const auto wood_blade = proc::normalize_part_fact( item( "stick_long" ), { .ix = 1 } );
848+
const auto steel_blade = proc::normalize_part_fact( item( "steel_chunk" ), { .ix = 2 } );
849+
const auto bone_blade = proc::normalize_part_fact( item( "bone" ), { .ix = 3 } );
850+
const auto leather_grip = proc::normalize_part_fact( item( "leather" ), { .ix = 4 } );
851+
const auto rag_grip = proc::normalize_part_fact( item( "rag" ), { .ix = 5 } );
852+
853+
const auto state = proc::build_state( sch, {
854+
wood_blade,
855+
steel_blade,
856+
bone_blade,
857+
leather_grip,
858+
rag_grip,
859+
} );
860+
861+
const auto position_in = []( const std::vector<proc::part_ix> &values,
862+
const proc::part_ix target ) {
863+
const auto iter = std::ranges::find( values, target );
864+
REQUIRE( iter != values.end() );
865+
return static_cast<int>( std::distance( values.begin(), iter ) );
866+
};
867+
868+
const auto &blade_candidates = state.cand.at( proc::slot_id( "blade" ) );
869+
CHECK( position_in( blade_candidates, 2 ) < position_in( blade_candidates, 3 ) );
870+
CHECK( position_in( blade_candidates, 3 ) < position_in( blade_candidates, 1 ) );
871+
872+
const auto &grip_candidates = state.cand.at( proc::slot_id( "grip" ) );
873+
CHECK( position_in( grip_candidates, 4 ) < position_in( grip_candidates, 5 ) );
874+
}
875+
842876
TEST_CASE( "proc_recipe_search_matches_builder_name_and_role", "[proc][builder][recipe]" )
843877
{
844878
proc::reset();

0 commit comments

Comments
 (0)