feat(adventurer-oracle): item-set objectives (hold N of a set of item ids) - #33
feat(adventurer-oracle): item-set objectives (hold N of a set of item ids)#33starknetdev wants to merge 1 commit into
Conversation
Adds a new objective kind so a single objective_id can mean "hold N of the
item ids {A,B,C}", usable by every consumer (the entry validator, Budokan, the
beast rewards) without ANDing several ids.
- interface: three `ItemSet*` metrics (HeldAnywhere/Equipped/InBag) + a new
`create_item_set_objective(name, description, config, items: Span<u8>)`
entrypoint and `get_objective_items` view. For set metrics `config.target` is
the required count compared via `config.comparator` ("hold all 3" = AtLeast 3;
"hold any of" = AtLeast 1).
- oracle_lib: pure `is_item_set_metric` / `set_item_held` / `count_items_held` /
`evaluate_item_set`; `evaluate`/`scalar_value` now explicitly reject set
metrics so they can never silently take the scalar path.
- oracle: stores the item id set alongside the objective (shared id space with
`create_objective`); `completed_objective` routes set metrics to
`evaluate_item_set`. `create_objective` rejects set metrics; the set creator
rejects scalar metrics, empty sets, and zero item ids.
9 new snforge tests (28 total green). scarb build + fmt --check clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GPT Code ReviewReview execution failed (exit code 1). The Codex CLI log was not posted because it can contain the full review prompt. |
Claude Code ReviewNow I have full context. Let me analyze the key findings:
Let me check the most impactful: the duplicate-id issue is the strongest correctness finding. Let me confirm there's no dedup guard. The duplicate concern is real but owner-controlled config. Let me weigh severity. The comparator flexibility ( Let me also verify the Now let me write up findings. Findings: [MEDIUM] Duplicate item ids inflate the held count — Let me double check whether there are other reviewers might flag. The Let me finalize. Based on my review of the scoped diff: [MEDIUM] packages/adventurer_oracle/src/oracle_lib.cairo:146-154 - for i in 0..items.len() {
let id = *items.at(i);
assert(id != 0, Errors::ZERO_ITEM_ID);
let mut j = 0;
while j != i {
assert(*items.at(j) != id, Errors::DUPLICATE_ITEM_ID);
j += 1;
}
}Alternatively, if duplicates are intended to be tolerated, dedup them (or count distinct held ids) and document/test the chosen semantics. [LOW] packages/adventurer_oracle/src/oracle.cairo:258-268,327-336 - |
There was a problem hiding this comment.
Code Review
This pull request introduces item-set objectives to the adventurer oracle, allowing verification of whether an adventurer holds a specific set of items (equipped, in their bag, or anywhere). It adds new metrics, contract methods to register and retrieve these sets, and corresponding library evaluation logic and tests. The review feedback focuses on improving robustness and code quality by recommending: 1) validating that registered item sets do not contain duplicate item IDs (along with adding a corresponding error constant and unit test) to prevent logical exploits; 2) refactoring read_objective_items to return Array<u8> so that get_objective_items can reuse it, eliminating duplicate storage-reading loops; and 3) correcting a comment in oracle_lib.cairo to accurately state that duplicate IDs in items are counted multiple times.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| pub const SET_METRIC_NEEDS_ITEMS: felt252 = 'Oracle: use item_set create'; | ||
| pub const NOT_SET_METRIC: felt252 = 'Oracle: not an item-set metric'; | ||
| pub const EMPTY_ITEM_SET: felt252 = 'Oracle: item set is empty'; | ||
| pub const ZERO_ITEM_ID: felt252 = 'Oracle: item id is zero'; |
There was a problem hiding this comment.
Add a new error constant DUPLICATE_ITEM_ID to represent the failure when duplicate item IDs are registered in an item-set objective.
pub const SET_METRIC_NEEDS_ITEMS: felt252 = 'Oracle: use item_set create';
pub const NOT_SET_METRIC: felt252 = 'Oracle: not an item-set metric';
pub const EMPTY_ITEM_SET: felt252 = 'Oracle: item set is empty';
pub const ZERO_ITEM_ID: felt252 = 'Oracle: item id is zero';
pub const DUPLICATE_ITEM_ID: felt252 = 'Oracle: duplicate item id';
| for item_id in items { | ||
| assert(*item_id != 0, Errors::ZERO_ITEM_ID); | ||
| } |
There was a problem hiding this comment.
Add validation to ensure that the registered item set does not contain duplicate item IDs. This prevents logical errors and potential exploits where an adventurer holding a single item can satisfy an objective requiring multiple distinct items.
let mut i: u32 = 0;
while i < items.len() {
let item_id = *items.at(i);
assert(item_id != 0, Errors::ZERO_ITEM_ID);
let mut j: u32 = i + 1;
while j < items.len() {
assert(item_id != *items.at(j), Errors::DUPLICATE_ITEM_ID);
j += 1;
};
i += 1;
};
| if oracle_lib::is_item_set_metric(config.metric) { | ||
| let items = self.read_objective_items(objective_id); | ||
| oracle_lib::evaluate_item_set(adventurer, bag, items, config) | ||
| } else { | ||
| oracle_lib::evaluate(adventurer, bag, config) | ||
| } |
There was a problem hiding this comment.
Since read_objective_items now returns Array<u8>, call .span() on the returned array when passing it to evaluate_item_set.
if oracle_lib::is_item_set_metric(config.metric) {
let items = self.read_objective_items(objective_id);
oracle_lib::evaluate_item_set(adventurer, bag, items.span(), config)
} else {
oracle_lib::evaluate(adventurer, bag, config)
}
| fn get_objective_items(self: @ContractState, objective_id: u32) -> Array<u8> { | ||
| assert(self.objective_exists(objective_id), Errors::OBJECTIVE_MISSING); | ||
| let len = self.objective_item_len.read(objective_id); | ||
| let mut items: Array<u8> = array![]; | ||
| let mut index: u32 = 0; | ||
| while index != len { | ||
| items.append(self.objective_items.read((objective_id, index))); | ||
| index += 1; | ||
| } | ||
| items | ||
| } |
There was a problem hiding this comment.
Simplify get_objective_items by reusing the refactored read_objective_items function, removing the duplicate storage reading loop.
fn get_objective_items(self: @ContractState, objective_id: u32) -> Array<u8> {
assert(self.objective_exists(objective_id), Errors::OBJECTIVE_MISSING);
self.read_objective_items(objective_id)
}
| fn read_objective_items(self: @ContractState, objective_id: u32) -> Span<u8> { | ||
| let len = self.objective_item_len.read(objective_id); | ||
| let mut items: Array<u8> = array![]; | ||
| let mut index: u32 = 0; | ||
| while index != len { | ||
| items.append(self.objective_items.read((objective_id, index))); | ||
| index += 1; | ||
| } | ||
| items.span() | ||
| } |
There was a problem hiding this comment.
Refactor read_objective_items to return Array<u8> instead of Span<u8>. This allows us to reuse this function in get_objective_items and eliminate the duplicated loop.
/// Read the stored item id set for an objective as an array.
fn read_objective_items(self: @ContractState, objective_id: u32) -> Array<u8> {
let len = self.objective_item_len.read(objective_id);
let mut items: Array<u8> = array![];
let mut index: u32 = 0;
while index != len {
items.append(self.objective_items.read((objective_id, index)));
index += 1;
}
items
}
| } | ||
|
|
||
| /// Number of items from `items` the adventurer holds under the set metric's predicate. | ||
| /// Duplicate ids in `items` are counted once each (the caller controls the list). |
| config(Metric::ItemSetHeldAnywhere, Comparator::AtLeast, 1, 0), | ||
| array![1_u8, 0, 3].span(), | ||
| ); | ||
| } |
There was a problem hiding this comment.
Add a unit test to verify that create_item_set_objective correctly panics when duplicate item IDs are supplied.
}
#[test]
#[should_panic(expected: 'Oracle: duplicate item id')]
fn test_create_item_set_rejects_duplicate_id() {
let (_mock, oracle) = setup();
start_cheat_caller_address(oracle.contract_address, owner_addr());
oracle
.create_item_set_objective(
"n",
"d",
config(Metric::ItemSetHeldAnywhere, Comparator::AtLeast, 1, 0),
array![1_u8, 2, 1].span(),
);
}
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…+ optional item set
Collapse create_objective + create_item_set_objective into ONE endpoint and
generalize an objective from a single check to a CONJUNCTION:
create_objective(name, description, settings_id,
conditions: Array<Condition>, // scalars/single-item, all ANDed
items: Option<Array<u8>>) // "own ALL these" (held anywhere)
An objective is complete iff settings match AND every condition passes AND (when
present) the adventurer holds every item in the set. This makes:
- a simple goal ("reach level 10") one condition,
- a composite goal ("score + gold + holds an item") several conditions,
- "own this whole set of items" the objective's `items` list (as an Option),
all through a single entrypoint — no separate item-set metrics or endpoint.
- interface: `Condition { metric, comparator, target, aux }` (settings_id moves to
the objective); new getters get_objective_settings_id / _conditions / _items.
Drops the ItemSet* metrics.
- oracle_lib: `evaluate` -> `evaluate_condition`; add `holds_all_items` (empty set
vacuously true, zero id never held).
- oracle: per-objective conditions + item-set storage; completed_objective ANDs
them; objectives_details renders one row per condition + an item count.
Supersedes the item-set-metric approach (PR #33). 25 snforge tests (composite,
item-set, items-only, empty-objective + zero-id guards). scarb build + fmt clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Superseded by the composite-conditions refactor now in #31 (commit ca080f0). Per the design discussion, objectives are unified into a single |
Summary
Stacks on the adventurer oracle (#31) to add a new objective kind: "hold N of these item ids", per the product ask. A single
objective_idcan now represent an item-set goal, so every consumer (the entry validator, Budokan gating, the beast-mode rewards) can check it with onecompleted_objectivecall instead of ANDing several per-item objectives.What's added
ItemSet*metrics —ItemSetHeldAnywhere,ItemSetEquipped,ItemSetInBag— plus a dedicatedcreate_item_set_objective(name, description, config, items: Span<u8>)entrypoint and aget_objective_itemsview. For a set metric,config.targetis the required count, compared viaconfig.comparator: "hold all 3 of {A,B,C}" =AtLeast 3; "hold at least one of {A,B,C}" =AtLeast 1.is_item_set_metric,set_item_held(zero ids never count — empty slots share id 0),count_items_held,evaluate_item_set.evaluate/scalar_valuenow explicitly reject set metrics so they can never silently fall through to the scalar path.create_objective);completed_objectiveroutes set metrics toevaluate_item_set. Guardrails:create_objectiverejects set metrics; the set creator rejects scalar metrics, empty sets, and zero item ids.Tests
9 new snforge tests (count-per-variant, zero-id handling, hold-all vs hold-any end to end, all four guardrail panics, shared id space) — 28 total green.
scarb build+scarb fmt --checkclean.Notes
feat/adventurer-oracle(stacked on the in-review Phase 1 PR) to keep review unified; retarget to main if feat: adventurer oracle — general objective checker for Death Mountain adventurers #31 lands first.objective_idit already delegates.🤖 Generated with Claude Code