From 94f1e595eef3a8e8446206bbb0dd211f8ca8e6b3 Mon Sep 17 00:00:00 2001 From: aquanight Date: Sat, 24 Aug 2024 23:59:10 -0600 Subject: [PATCH 1/3] Adds ability to restrict location of items to Vanilla locations - i.e. map-only randomization --- rust/maprando-game/src/lib.rs | 4 + rust/maprando-web/src/logic_helper.rs | 1 + rust/maprando-web/src/web/randomize.rs | 9 + .../maprando-web/src/web/randomize/helpers.rs | 3 + .../item_location_restriction.html | 28 ++ .../templates/generate/item_progression.html | 22 + .../templates/generate/scripts.html | 6 +- .../seed/item_progression_details.html | 4 + rust/maprando/src/bin/debug.rs | 5 +- rust/maprando/src/bin/maprando-cli.rs | 39 +- rust/maprando/src/bin/maprando-test.rs | 8 +- rust/maprando/src/randomize.rs | 455 +++++++++++++++--- 12 files changed, 492 insertions(+), 92 deletions(-) create mode 100644 rust/maprando-web/templates/generate/help/progression/item_location_restriction.html diff --git a/rust/maprando-game/src/lib.rs b/rust/maprando-game/src/lib.rs index 6c999326c4..f4f59268e5 100644 --- a/rust/maprando-game/src/lib.rs +++ b/rust/maprando-game/src/lib.rs @@ -1044,6 +1044,7 @@ pub struct GameData { pub vertex_isv: IndexedVec, pub grey_lock_map: HashMap<(RoomId, NodeId), JsonValue>, pub item_locations: Vec<(RoomId, NodeId)>, + pub vanilla_items: HashMap<(RoomId, NodeId), Item>, pub item_vertex_ids: Vec>, pub flag_ids: Vec, pub flag_vertex_ids: Vec>, @@ -3816,7 +3817,10 @@ impl GameData { for (&(room_id, node_id), node_json) in &self.node_json_map { if node_json["nodeType"] == "item" { + let item_type = &node_json["nodeItem"]; + let item: Item = item_type.as_str().expect("Missing?").parse().unwrap(); self.item_locations.push((room_id, node_id)); + self.vanilla_items.insert((room_id, node_id), item); } if node_json.has_key("utility") { if node_json["utility"].members().any(|x| x == "save") { diff --git a/rust/maprando-web/src/logic_helper.rs b/rust/maprando-web/src/logic_helper.rs index 481fc20138..349b9d84cf 100644 --- a/rust/maprando-web/src/logic_helper.rs +++ b/rust/maprando-web/src/logic_helper.rs @@ -333,6 +333,7 @@ fn get_difficulty_config(preset: &PresetData) -> DifficultyConfig { speed_ball_tiles: preset.preset.speed_ball_tiles as f32, shinecharge_leniency_frames: preset.preset.shinecharge_leniency_frames as Capacity, progression_rate: maprando::randomize::ProgressionRate::Fast, + item_location_restriction: maprando::randomize::ItemLocationRestriction::None, random_tank: true, spazer_before_plasma: true, stop_item_placement_early: false, diff --git a/rust/maprando-web/src/web/randomize.rs b/rust/maprando-web/src/web/randomize.rs index 2a8a32d68e..309ac4914c 100644 --- a/rust/maprando-web/src/web/randomize.rs +++ b/rust/maprando-web/src/web/randomize.rs @@ -100,6 +100,7 @@ struct RandomizeRequest { progression_rate: Text, item_placement_style: Text, item_priority_strength: Text, + item_location_restriction: Text, random_tank: Text, spazer_before_plasma: Text, stop_item_placement_early: Text, @@ -301,6 +302,14 @@ async fn randomize( req.progression_rate.0.as_str() ), }, + item_location_restriction: match req.item_location_restriction.0.as_str() { + "None" => maprando::randomize::ItemLocationRestriction::None, + "Vanilla" => maprando::randomize::ItemLocationRestriction::Vanilla, + _ => panic!( + "Unrecognized item location restriction {}", + req.item_location_restriction.0.as_str() + ), + }, item_priority_strength: match req.item_priority_strength.0.as_str() { "Moderate" => ItemPriorityStrength::Moderate, "Heavy" => ItemPriorityStrength::Heavy, diff --git a/rust/maprando-web/src/web/randomize/helpers.rs b/rust/maprando-web/src/web/randomize/helpers.rs index 8972f5b2a9..3fd1b50593 100644 --- a/rust/maprando-web/src/web/randomize/helpers.rs +++ b/rust/maprando-web/src/web/randomize/helpers.rs @@ -31,6 +31,7 @@ pub struct SeedHeaderTemplate<'a> { semi_filler_items: Vec, early_filler_items: Vec, item_placement_style: String, + item_location_restriction: String, difficulty: &'a DifficultyConfig, _notable_strats: Vec, quality_of_life_preset: String, @@ -258,6 +259,7 @@ pub fn get_difficulty_tiers( preset.shinecharge_leniency_frames as Capacity, ), progression_rate: difficulty.progression_rate, + item_location_restriction: difficulty.item_location_restriction, random_tank: difficulty.random_tank, spazer_before_plasma: difficulty.spazer_before_plasma, stop_item_placement_early: difficulty.stop_item_placement_early, @@ -518,6 +520,7 @@ pub fn render_seed( .map(|x| format!("{:?}", x)) .collect(), item_placement_style: format!("{:?}", seed_data.difficulty.item_placement_style), + item_location_restriction: format!("{:?}", seed_data.difficulty.item_location_restriction), difficulty: &seed_data.difficulty, _notable_strats: notable_strats, quality_of_life_preset: seed_data diff --git a/rust/maprando-web/templates/generate/help/progression/item_location_restriction.html b/rust/maprando-web/templates/generate/help/progression/item_location_restriction.html new file mode 100644 index 0000000000..9a76f40eda --- /dev/null +++ b/rust/maprando-web/templates/generate/help/progression/item_location_restriction.html @@ -0,0 +1,28 @@ + + + + \ No newline at end of file diff --git a/rust/maprando-web/templates/generate/item_progression.html b/rust/maprando-web/templates/generate/item_progression.html index 794682f0af..9f467c5f8c 100644 --- a/rust/maprando-web/templates/generate/item_progression.html +++ b/rust/maprando-web/templates/generate/item_progression.html @@ -60,6 +60,28 @@ +
+
+ {% include "help/progression/item_location_restriction.html" %} + +
+
+ {% for name in ["None", "Vanilla"] %} + + + {% endfor %} +
+
+
{% include "help/progression/random_tank.html" %} diff --git a/rust/maprando-web/templates/generate/scripts.html b/rust/maprando-web/templates/generate/scripts.html index 2cfaf6ec63..05ce74d977 100644 --- a/rust/maprando-web/templates/generate/scripts.html +++ b/rust/maprando-web/templates/generate/scripts.html @@ -251,6 +251,7 @@ function itemProgressionPresetChanged() { if (document.getElementById("itemProgressionPresetNormal").checked) { document.getElementById("progressionRateFast").checked = true; + document.getElementById("itemLocationRestrictionNone").checked = true; document.getElementById("itemPlacementStyleNeutral").checked = true; document.getElementById("itemPriorityStrengthModerate").checked = true; document.getElementById("randomTankYes").checked = true; @@ -282,6 +283,7 @@ } if (document.getElementById("itemProgressionPresetTricky").checked) { document.getElementById("progressionRateUniform").checked = true; + document.getElementById("itemLocationRestrictionNone").checked = true; document.getElementById("itemPlacementStyleForced").checked = true; document.getElementById("itemPriorityStrengthModerate").checked = true; document.getElementById("randomTankYes").checked = true; @@ -312,6 +314,7 @@ } if (document.getElementById("itemProgressionPresetChallenge").checked) { document.getElementById("progressionRateSlow").checked = true; + document.getElementById("itemLocationRestrictionNone").checked = true; document.getElementById("itemPlacementStyleForced").checked = true; document.getElementById("itemPriorityStrengthModerate").checked = true; document.getElementById("randomTankYes").checked = true; @@ -337,6 +340,7 @@ } if (document.getElementById("itemProgressionPresetDesolate").checked) { document.getElementById("progressionRateSlow").checked = true; + document.getElementById("itemLocationRestrictionNone").checked = true; document.getElementById("itemPlacementStyleForced").checked = true; document.getElementById("itemPriorityStrengthModerate").checked = true; document.getElementById("randomTankYes").checked = true; @@ -367,7 +371,7 @@ document.getElementById("itemProgressionPresetNormal").checked = false; document.getElementById("itemProgressionPresetTricky").checked = false; document.getElementById("itemProgressionPresetChallenge").checked = false; - document.getElementById("itemProgressionPresetDesolate").checked = false; + document.getElementById("itemProgressionPresetDesolate").checked = false; } function processStartingItemsPreset() { if (document.getElementById("startingItemsPresetNone").checked) { diff --git a/rust/maprando-web/templates/seed/item_progression_details.html b/rust/maprando-web/templates/seed/item_progression_details.html index ae17984d81..f2d503306d 100644 --- a/rust/maprando-web/templates/seed/item_progression_details.html +++ b/rust/maprando-web/templates/seed/item_progression_details.html @@ -6,6 +6,10 @@
Item placement:
{{+ item_placement_style }}
+
+
Item location restriction:
+
{{+ item_location_restriction }}
+
Item priority strength:
{% let difficulty = difficulty %}{{+ format!("{:?}", difficulty.item_priority_strength) }}
diff --git a/rust/maprando/src/bin/debug.rs b/rust/maprando/src/bin/debug.rs index 4fd8eeb323..c5ff1064b8 100644 --- a/rust/maprando/src/bin/debug.rs +++ b/rust/maprando/src/bin/debug.rs @@ -2,8 +2,8 @@ use anyhow::Result; use hashbrown::HashMap; use maprando::{ randomize::{ - AreaAssignment, DifficultyConfig, DoorsMode, ItemDotChange, ItemMarkers, - ItemPlacementStyle, ItemPriorityGroup, ItemPriorityStrength, MapStationReveal, + AreaAssignment, DifficultyConfig, DoorsMode, ItemDotChange, ItemLocationRestriction, + ItemMarkers, ItemPlacementStyle, ItemPriorityGroup, ItemPriorityStrength, MapStationReveal, MapsRevealed, MotherBrainFight, ProgressionRate, SaveAnimals, WallJump, }, traverse::{apply_requirement, LockedDoorData}, @@ -87,6 +87,7 @@ fn run_scenario( speed_ball_tiles: 24.0, shinecharge_leniency_frames: 15, progression_rate: ProgressionRate::Uniform, + item_location_restriction: ItemLocationRestriction::None, random_tank: true, spazer_before_plasma: true, stop_item_placement_early: false, diff --git a/rust/maprando/src/bin/maprando-cli.rs b/rust/maprando/src/bin/maprando-cli.rs index 563cac280a..de9977f1c5 100644 --- a/rust/maprando/src/bin/maprando-cli.rs +++ b/rust/maprando/src/bin/maprando-cli.rs @@ -6,9 +6,9 @@ use maprando::customize::{customize_rom, ControllerConfig, CustomizeSettings, Mu use maprando::patch::ips_write::create_ips_patch; use maprando::patch::Rom; use maprando::randomize::{ - randomize_doors, AreaAssignment, DoorsMode, ItemDotChange, ItemMarkers, ItemPlacementStyle, - ItemPriorityGroup, ItemPriorityStrength, MapStationReveal, MotherBrainFight, ProgressionRate, - Randomization, Randomizer, SaveAnimals, StartLocationMode, + randomize_doors, AreaAssignment, DoorsMode, ItemDotChange, ItemLocationRestriction, + ItemMarkers, ItemPlacementStyle, ItemPriorityGroup, ItemPriorityStrength, MapStationReveal, + MotherBrainFight, ProgressionRate, Randomization, Randomizer, SaveAnimals, StartLocationMode, }; use maprando::spoiler_map; use maprando::{patch::make_rom, randomize::DifficultyConfig}; @@ -54,30 +54,17 @@ fn get_randomization(args: &Args, game_data: &GameData) -> Result name: None, tech: game_data.tech_isv.keys.clone(), notable_strats: vec![], - shine_charge_tiles: 16.0, - heated_shine_charge_tiles: 16.0, - speed_ball_tiles: 24.0, - shinecharge_leniency_frames: 15, + shine_charge_tiles: 13.0, + heated_shine_charge_tiles: 14.0, + speed_ball_tiles: 15.0, + shinecharge_leniency_frames: 0, progression_rate: ProgressionRate::Fast, + item_location_restriction: ItemLocationRestriction::Vanilla, random_tank: true, spazer_before_plasma: true, stop_item_placement_early: false, item_pool: vec![], - starting_items: vec![ - (Item::Gravity, 1), - (Item::Varia, 1), - (Item::Morph, 1), - (Item::Missile, 1), - (Item::Super, 1), - (Item::PowerBomb, 1), - (Item::SpeedBooster, 1), - (Item::SpaceJump, 1), - (Item::ScrewAttack, 1), - (Item::HiJump, 1), - (Item::Grapple, 1), - (Item::ETank, 1), - (Item::ReserveTank, 1), - ], + starting_items: vec![], semi_filler_items: vec![], filler_items: vec![Item::Missile], early_filler_items: vec![], @@ -104,7 +91,7 @@ fn get_randomization(args: &Args, game_data: &GameData) -> Result }, ], resource_multiplier: 1.0, - escape_timer_multiplier: 3.0, + escape_timer_multiplier: 1.0, gate_glitch_leniency: 0, door_stuck_leniency: 0, phantoon_proficiency: 1.0, @@ -113,9 +100,9 @@ fn get_randomization(args: &Args, game_data: &GameData) -> Result botwoon_proficiency: 1.0, mother_brain_proficiency: 1.0, supers_double: true, - mother_brain_fight: MotherBrainFight::Skip, - escape_enemies_cleared: true, - escape_refill: true, + mother_brain_fight: MotherBrainFight::Vanilla, + escape_enemies_cleared: false, + escape_refill: false, escape_movement_items: true, mark_map_stations: true, room_outline_revealed: true, diff --git a/rust/maprando/src/bin/maprando-test.rs b/rust/maprando/src/bin/maprando-test.rs index 8968618bda..693f507947 100644 --- a/rust/maprando/src/bin/maprando-test.rs +++ b/rust/maprando/src/bin/maprando-test.rs @@ -8,9 +8,10 @@ use maprando::patch::ips_write::create_ips_patch; use maprando::patch::Rom; use maprando::preset::Preset; use maprando::randomize::{ - randomize_doors, randomize_map_areas, AreaAssignment, DoorsMode, ItemDotChange, ItemMarkers, - ItemPlacementStyle, ItemPriorityGroup, ItemPriorityStrength, MotherBrainFight, Objective, - ProgressionRate, Randomization, Randomizer, SaveAnimals, StartLocationMode, + randomize_doors, randomize_map_areas, AreaAssignment, DoorsMode, ItemDotChange, + ItemLocationRestriction, ItemMarkers, ItemPlacementStyle, ItemPriorityGroup, + ItemPriorityStrength, MotherBrainFight, Objective, ProgressionRate, Randomization, Randomizer, + SaveAnimals, StartLocationMode, }; use maprando::spoiler_map; use maprando::{patch::make_rom, randomize::DifficultyConfig}; @@ -63,6 +64,7 @@ fn create_difficulty_from_preset(preset: &Preset) -> DifficultyConfig { mother_brain_proficiency: preset.mother_brain_proficiency, // Progression options, Normal preset progression_rate: ProgressionRate::Fast, + item_location_restriction: ItemLocationRestriction::None, random_tank: true, spazer_before_plasma: true, stop_item_placement_early: false, diff --git a/rust/maprando/src/randomize.rs b/rust/maprando/src/randomize.rs index c832c995fd..f3ce7bc6be 100644 --- a/rust/maprando/src/randomize.rs +++ b/rust/maprando/src/randomize.rs @@ -47,6 +47,12 @@ pub enum ItemPlacementStyle { Forced, } +#[derive(Clone, Copy, Serialize, Deserialize, Debug, PartialEq)] +pub enum ItemLocationRestriction { + None, + Vanilla, +} + #[derive(Clone, Copy, Serialize, Deserialize, Debug, PartialEq)] pub enum ItemPriorityStrength { Moderate, @@ -236,6 +242,7 @@ pub struct DifficultyConfig { pub item_pool: Vec<(Item, usize)>, pub starting_items: Vec<(Item, usize)>, pub item_placement_style: ItemPlacementStyle, + pub item_location_restriction: ItemLocationRestriction, pub item_priority_strength: ItemPriorityStrength, pub item_priorities: Vec, pub semi_filler_items: Vec, @@ -3039,13 +3046,14 @@ impl<'r> Randomizer<'r> { state: &RandomizationState, num_key_items_to_select: usize, attempt_num: usize, + item_precedence: &Vec, ) -> Option> { if num_key_items_to_select >= 1 { let mut unplaced_items: Vec = vec![]; let mut placed_items: Vec = vec![]; let mut additional_items: Vec = vec![]; - for &item in &state.item_precedence { + for &item in item_precedence { if state.items_remaining[item as usize] > 0 || (self.difficulty_tiers[0].stop_item_placement_early && item == Item::Nothing) { @@ -3079,6 +3087,15 @@ impl<'r> Randomizer<'r> { remaining_items.extend(placed_items); remaining_items.extend(additional_items); + // For the case where no key items are available to select: + if remaining_items.len() < 1 { + if attempt_num > 0 { + return None; + } else { + return Some(vec![]); + } + } + if attempt_num > 0 && num_key_items_to_select - 1 + attempt_num >= cnt_different_items_remaining { @@ -3183,6 +3200,7 @@ impl<'r> Randomizer<'r> { other_locations: &[ItemLocationId], key_items_to_place: &[Item], other_items_to_place: &[Item], + partial_placement: bool, ) { info!( "[attempt {attempt_num_rando}] Placing {:?}, {:?}", @@ -3256,7 +3274,7 @@ impl<'r> Randomizer<'r> { let mut all_items_to_place: Vec = Vec::new(); all_items_to_place.extend(key_items_to_place); all_items_to_place.extend(other_items_to_place); - assert!(all_locations.len() == all_items_to_place.len()); + assert!(partial_placement || all_locations.len() == all_items_to_place.len()); for (&loc, &item) in iter::zip(&all_locations, &all_items_to_place) { new_state.item_location_state[loc].placed_item = Some(item); } @@ -3284,14 +3302,35 @@ impl<'r> Randomizer<'r> { "[attempt {attempt_num_rando}] Finishing with {:?}", remaining_items ); - let mut idx = 0; - for item_loc_state in &mut state.item_location_state { - if item_loc_state.placed_item.is_none() { - item_loc_state.placed_item = Some(remaining_items[idx]); - idx += 1; + if self.difficulty_tiers[0].item_location_restriction + == ItemLocationRestriction::Vanilla + { + remaining_items.retain(|&i| i != Item::Nothing); + while let Some(loc) = state + .item_location_state + .iter() + .position(|ils| ils.placed_item.is_none()) + { + let can_place = + self.game_data.vanilla_items[&self.game_data.item_locations[loc]]; + if let Some(idx) = remaining_items.iter().position(|&i| i == can_place) { + remaining_items.remove(idx); + state.item_location_state[loc].placed_item = Some(can_place); + } else { + state.item_location_state[loc].placed_item = Some(Item::Nothing); + } } + assert!(remaining_items.len() == 0); + } else { + let mut idx = 0; + for item_loc_state in &mut state.item_location_state { + if item_loc_state.placed_item.is_none() { + item_loc_state.placed_item = Some(remaining_items[idx]); + idx += 1; + } + } + assert!(idx == remaining_items.len()); } - assert!(idx == remaining_items.len()); } } @@ -3406,7 +3445,12 @@ impl<'r> Randomizer<'r> { let mut attempt_num = 0; let mut selected_key_items = self - .select_key_items(&new_state_filler, num_key_items_to_select, attempt_num) + .select_key_items( + &new_state_filler, + num_key_items_to_select, + attempt_num, + &state.item_precedence, + ) .unwrap(); loop { @@ -3432,9 +3476,12 @@ impl<'r> Randomizer<'r> { return (selection, new_state); } - if let Some(new_selected_key_items) = - self.select_key_items(&new_state_filler, num_key_items_to_select, attempt_num) - { + if let Some(new_selected_key_items) = self.select_key_items( + &new_state_filler, + num_key_items_to_select, + attempt_num, + &state.item_precedence, + ) { selected_key_items = new_selected_key_items; } else { info!("[attempt {attempt_num_rando}] Exhausted key item placement attempts"); @@ -3467,6 +3514,254 @@ impl<'r> Randomizer<'r> { } } + fn vanilla_classify_locations( + &self, + state: &RandomizationState, + location_set: &Vec, + ) -> (Vec, Vec) { + let mut filler_locations: Vec = Vec::new(); + let mut key_locations: Vec = Vec::new(); + + for &loc in location_set { + let (room, node) = self.game_data.item_locations[loc]; + let item = self.game_data.vanilla_items[&(room, node)]; + if self.difficulty_tiers[0].early_filler_items.contains(&item) + || self.difficulty_tiers[0].filler_items.contains(&item) + || (self.difficulty_tiers[0].semi_filler_items.contains(&item) + && state.items_remaining[item as usize] + < self.initial_items_remaining[item as usize]) + { + filler_locations.push(loc); + } else { + key_locations.push(loc); + } + } + return (filler_locations, key_locations); + } + + fn multi_attempt_select_and_place_vanilla( + &self, + attempt_num_rando: usize, + state: &RandomizationState, + placed_uncollected_bireachable_items: &mut Vec, + placed_uncollected_bireachable_loc: &mut Vec, + unplaced_bireachable: &mut Vec, + unplaced_oneway_reachable: &mut Vec, + rng: &mut R, + ) -> (SelectItemsOutput, RandomizationState) { + let (mut max_key_items_to_select, _) = self.determine_item_split( + state, + unplaced_bireachable.len(), + unplaced_oneway_reachable.len(), + ); + info!("[attempt {attempt_num_rando}] Beginning vanilla placement attempt"); + // Now we have to divide the list of bireachable/oneway reachable locations + // based on whether or not a location is filler or progression. + let (filler_bireachable, mut key_bireachable) = + self.vanilla_classify_locations(&state, &unplaced_bireachable); + let (filler_oneway, _) = + self.vanilla_classify_locations(&state, &unplaced_oneway_reachable); + // key_oneway won't be used for now. + // Now get the set of filler items that we might want to place on this step: + let mut selected_filler = + self.select_filler_items(&state, filler_bireachable.len(), filler_oneway.len(), rng); + let mut placed_filler: Vec = Vec::new(); + + if max_key_items_to_select > key_bireachable.len() { + max_key_items_to_select = key_bireachable.len(); + } + + let mut new_state_filler: RandomizationState = RandomizationState { + step_num: state.step_num, + start_location: state.start_location.clone(), + hub_location: state.hub_location.clone(), + item_precedence: state.item_precedence.clone(), + item_location_state: state.item_location_state.clone(), + flag_location_state: state.flag_location_state.clone(), + save_location_state: state.save_location_state.clone(), + door_state: state.door_state.clone(), + items_remaining: state.items_remaining.clone(), + global_state: state.global_state.clone(), + debug_data: None, + previous_debug_data: None, + key_visited_vertices: HashSet::new(), + }; + + // What we're going to place so far. + //let item_loc_placement : Vec<(ItemLocationId, Item)> = Vec::new(); + for &loc in filler_bireachable.iter().chain(filler_oneway.iter()) { + // What item belongs here: + let mut item = self.game_data.vanilla_items[&self.game_data.item_locations[loc]]; + if let Some(select_index) = selected_filler + .iter() + .position(|&i| i == item || i == Item::Nothing) + { + item = selected_filler[select_index]; + // We can fill this spot. + new_state_filler.item_location_state[loc].placed_item = Some(item); + selected_filler.remove(select_index); + placed_filler.push(item); + // We check if items_remaining is positive, only because with "Stop item placement early" there + // could be extra (unplanned) Nothing items placed. + if new_state_filler.items_remaining[item as usize] > 0 { + new_state_filler.items_remaining[item as usize] -= 1; + } + } else { + // We're out of Nothings and the item to be put here hasn't been selected yet. DO NOT FILL. This is so that reachable locations with Supers can get + // filled with Supers once logical Supers are placed. + } + } + + new_state_filler.previous_debug_data = state.debug_data.clone(); + new_state_filler.key_visited_vertices = state.key_visited_vertices.clone(); + + // Mark the newly collected items that were placed on earlier steps: + for &loc in placed_uncollected_bireachable_loc.iter() { + new_state_filler.item_location_state[loc].collected = true; + } + + // And now the key item(s). ONLY bireachable key items will be placed. The one-way + // list is ignored. + let mut attempt_num = 0; + + // Preprocess the item precedence to only include items that can be reached right now. + let placeable_key_items: Vec = key_bireachable + .iter() + .map(|&loc| self.game_data.vanilla_items[&self.game_data.item_locations[loc]]) + .collect(); + let item_precedence: Vec = state + .item_precedence + .iter() + .filter(|&i| placeable_key_items.contains(i)) + .map(|&i| i) + .collect(); + + let mut selected_key_items = self + .select_key_items( + &state, + max_key_items_to_select, + attempt_num, + &item_precedence, + ) + .unwrap(); + loop { + // Remove from selected_key_items all key items that we can't reach. + selected_key_items.retain(|&i| placeable_key_items.contains(&i)); + + let mut new_state: RandomizationState = new_state_filler.clone(); + for &item in &selected_key_items { + if new_state.items_remaining[item as usize] > 0 { + new_state.items_remaining[item as usize] -= 1; + } + } + + if self.provides_progression( + &state, + &mut new_state, + &selected_key_items, + &placed_filler, + &placed_uncollected_bireachable_items, + unplaced_bireachable.len(), + ) { + // Success: we can now commit the placement. + for &item in &selected_key_items { + if item == Item::Nothing { + continue; + } // Handle Nothings later. + // The locations this item can be placed: + let where_to_place: Vec = key_bireachable + .iter() + .map(|&loc| loc) + .filter(|&loc| { + self.game_data.vanilla_items[&self.game_data.item_locations[loc]] + == item + }) + .collect(); + if where_to_place.len() < 1 { + continue; + } // It can't be placed? Why? + self.place_items( + attempt_num_rando, + &state, + &mut new_state, + &where_to_place, + &[], + &[item], + &[], + true, + ); + // Remove the locations we just filled. + key_bireachable + .retain(|&loc| new_state.item_location_state[loc].placed_item.is_none()); + } + // Now for the Nothing(s) that were selected - we'll basically just stuff them randomly. Note that sticking a Nothing in a unique item's location + // effectively removes that item from randomization. + key_bireachable.shuffle(rng); + let nothings_to_place = selected_key_items + .iter() + .filter(|&i| *i == Item::Nothing) + .count(); + for _ in 0..nothings_to_place { + // Pick a key location we didn't fill yet to stick a Nothing into. + if let Some(loc) = key_bireachable.pop() { + new_state.item_location_state[loc].placed_item = Some(Item::Nothing); + } + } + let selection = SelectItemsOutput { + key_items: selected_key_items, + other_items: placed_filler, + }; + return (selection, new_state); + } + + if let Some(new_selected_key_items) = self.select_key_items( + &state, + max_key_items_to_select, + attempt_num, + &item_precedence, + ) { + selected_key_items = new_selected_key_items; + } else { + info!("[attempt {attempt_num_rando}] Exhausted key item placement attempts"); + if self.difficulty_tiers[0].stop_item_placement_early { + for x in &mut selected_key_items { + *x = Item::Nothing; + } + new_state = new_state_filler; + for &item in &selected_key_items { + if new_state.items_remaining[item as usize] > 0 { + new_state.items_remaining[item as usize] -= 1; + } + } + unplaced_bireachable.shuffle(rng); + let nothings_to_place = selected_key_items + .iter() + .filter(|&i| *i == Item::Nothing) + .count(); + for _ in 0..nothings_to_place { + if let Some(loc) = unplaced_bireachable.pop() { + new_state.item_location_state[loc].placed_item = Some(Item::Nothing); + } + } + let _ = self.provides_progression( + &state, + &mut new_state, + &selected_key_items, + &placed_filler, + &placed_uncollected_bireachable_items, + unplaced_bireachable.len(), + ); + } + let selection = SelectItemsOutput { + key_items: selected_key_items, + other_items: placed_filler, + }; + return (selection, new_state); + } + attempt_num += 1; + } + } + fn step( &self, attempt_num_rando: usize, @@ -3582,58 +3877,98 @@ impl<'r> Randomizer<'r> { } unplaced_bireachable.shuffle(rng); unplaced_oneway_reachable.shuffle(rng); - let (selection, mut new_state) = self.multi_attempt_select_items( - attempt_num_rando, - &state, - &placed_uncollected_bireachable_items, - unplaced_bireachable.len(), - unplaced_oneway_reachable.len(), - rng, - ); - new_state.previous_debug_data = state.debug_data.clone(); - new_state.key_visited_vertices = state.key_visited_vertices.clone(); - // Mark the newly collected items that were placed on earlier steps: - for &loc in &placed_uncollected_bireachable_loc { - new_state.item_location_state[loc].collected = true; - } + if self.difficulty_tiers[0].item_location_restriction == ItemLocationRestriction::Vanilla { + let (_, mut new_state) = self.multi_attempt_select_and_place_vanilla( + attempt_num_rando, + &state, + &mut placed_uncollected_bireachable_items, + &mut placed_uncollected_bireachable_loc, + &mut unplaced_bireachable, + &mut unplaced_oneway_reachable, + rng, + ); - // Place the new items: - // We place items in all newly reachable locations (bireachable as - // well as one-way-reachable locations). One-way-reachable locations are filled only - // with filler items, to reduce the possibility of them being usable to break from the - // intended logical sequence. - self.place_items( - attempt_num_rando, - &state, - &mut new_state, - &unplaced_bireachable, - &unplaced_oneway_reachable, - &selection.key_items, - &selection.other_items, - ); + // Mark the newly placed bireachable items as collected: + for &loc in &unplaced_bireachable { + // Skip if an item wasn't placed here. + if new_state.item_location_state[loc].placed_item.is_none() { + continue; + } + new_state.item_location_state[loc].collected = true; + } - // Mark the newly placed bireachable items as collected: - for &loc in &unplaced_bireachable { - new_state.item_location_state[loc].collected = true; - } + let spoiler_summary = self.get_spoiler_summary( + &orig_global_state, + state, + &new_state, + spoiler_flag_summaries, + spoiler_door_summaries, + ); + let spoiler_details = self.get_spoiler_details( + &orig_global_state, + state, + &new_state, + spoiler_flag_details, + spoiler_door_details, + ); + *state = new_state; + (spoiler_summary, spoiler_details, false) + } else { + let (selection, mut new_state) = self.multi_attempt_select_items( + attempt_num_rando, + &state, + &placed_uncollected_bireachable_items, + unplaced_bireachable.len(), + unplaced_oneway_reachable.len(), + rng, + ); + new_state.previous_debug_data = state.debug_data.clone(); + new_state.key_visited_vertices = state.key_visited_vertices.clone(); - let spoiler_summary = self.get_spoiler_summary( - &orig_global_state, - state, - &new_state, - spoiler_flag_summaries, - spoiler_door_summaries, - ); - let spoiler_details = self.get_spoiler_details( - &orig_global_state, - state, - &new_state, - spoiler_flag_details, - spoiler_door_details, - ); - *state = new_state; - (spoiler_summary, spoiler_details, false) + // Mark the newly collected items that were placed on earlier steps: + for &loc in &placed_uncollected_bireachable_loc { + new_state.item_location_state[loc].collected = true; + } + + // Place the new items: + // We place items in all newly reachable locations (bireachable as + // well as one-way-reachable locations). One-way-reachable locations are filled only + // with filler items, to reduce the possibility of them being usable to break from the + // intended logical sequence. + self.place_items( + attempt_num_rando, + &state, + &mut new_state, + &unplaced_bireachable, + &unplaced_oneway_reachable, + &selection.key_items, + &selection.other_items, + false, + ); + + // Mark the newly placed bireachable items as collected: + for &loc in &unplaced_bireachable { + new_state.item_location_state[loc].collected = true; + } + + let spoiler_summary = self.get_spoiler_summary( + &orig_global_state, + state, + &new_state, + spoiler_flag_summaries, + spoiler_door_summaries, + ); + let spoiler_details = self.get_spoiler_details( + &orig_global_state, + state, + &new_state, + spoiler_flag_details, + spoiler_door_details, + ); + *state = new_state; + (spoiler_summary, spoiler_details, false) + } } fn get_seed_name(&self, seed: usize) -> String { From e5597eb97f527eae3905eadb0529c3d4e15ed502 Mon Sep 17 00:00:00 2001 From: aquanight Date: Sun, 25 Aug 2024 09:29:14 -0600 Subject: [PATCH 2/3] Improve filler placement logic on Vanilla mode --- rust/maprando/src/randomize.rs | 47 +++++++++++++++++----------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/rust/maprando/src/randomize.rs b/rust/maprando/src/randomize.rs index f3ce7bc6be..ffd4e93943 100644 --- a/rust/maprando/src/randomize.rs +++ b/rust/maprando/src/randomize.rs @@ -3557,14 +3557,16 @@ impl<'r> Randomizer<'r> { info!("[attempt {attempt_num_rando}] Beginning vanilla placement attempt"); // Now we have to divide the list of bireachable/oneway reachable locations // based on whether or not a location is filler or progression. - let (filler_bireachable, mut key_bireachable) = + let (mut filler_bireachable, mut key_bireachable) = self.vanilla_classify_locations(&state, &unplaced_bireachable); - let (filler_oneway, _) = + let (mut filler_oneway, _) = self.vanilla_classify_locations(&state, &unplaced_oneway_reachable); // key_oneway won't be used for now. // Now get the set of filler items that we might want to place on this step: - let mut selected_filler = + let selected_filler = self.select_filler_items(&state, filler_bireachable.len(), filler_oneway.len(), rng); + // The only thing we actually care about from selected_filler is whether we place Nothing or place an item. + // After all, a location is only in the filler set if its item is also in filler. let mut placed_filler: Vec = Vec::new(); if max_key_items_to_select > key_bireachable.len() { @@ -3587,28 +3589,25 @@ impl<'r> Randomizer<'r> { key_visited_vertices: HashSet::new(), }; - // What we're going to place so far. - //let item_loc_placement : Vec<(ItemLocationId, Item)> = Vec::new(); - for &loc in filler_bireachable.iter().chain(filler_oneway.iter()) { - // What item belongs here: - let mut item = self.game_data.vanilla_items[&self.game_data.item_locations[loc]]; - if let Some(select_index) = selected_filler - .iter() - .position(|&i| i == item || i == Item::Nothing) - { - item = selected_filler[select_index]; - // We can fill this spot. - new_state_filler.item_location_state[loc].placed_item = Some(item); - selected_filler.remove(select_index); - placed_filler.push(item); - // We check if items_remaining is positive, only because with "Stop item placement early" there - // could be extra (unplanned) Nothing items placed. - if new_state_filler.items_remaining[item as usize] > 0 { - new_state_filler.items_remaining[item as usize] -= 1; - } + for (idx, &loc) in filler_bireachable + .iter() + .chain(filler_oneway.iter()) + .enumerate() + { + let selected_item = selected_filler[idx]; + // If Nothing was selected, place Nothing. Otherwise place the item that belongs here. + let item = if selected_item == Item::Nothing { + Item::Nothing } else { - // We're out of Nothings and the item to be put here hasn't been selected yet. DO NOT FILL. This is so that reachable locations with Supers can get - // filled with Supers once logical Supers are placed. + self.game_data.vanilla_items[&self.game_data.item_locations[loc]] + }; + // We can fill this spot. + new_state_filler.item_location_state[loc].placed_item = Some(item); + placed_filler.push(item); + // We check if items_remaining is positive, only because with "Stop item placement early" there + // could be extra (unplanned) Nothing items placed. + if new_state_filler.items_remaining[item as usize] > 0 { + new_state_filler.items_remaining[item as usize] -= 1; } } From 4686489155efc9dd9e19553980faeedd30659c39 Mon Sep 17 00:00:00 2001 From: aquanight Date: Sun, 25 Aug 2024 09:46:37 -0600 Subject: [PATCH 3/3] Minor text changes --- .../help/progression/item_location_restriction.html | 2 +- rust/maprando/src/randomize.rs | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/rust/maprando-web/templates/generate/help/progression/item_location_restriction.html b/rust/maprando-web/templates/generate/help/progression/item_location_restriction.html index 9a76f40eda..20ad211b9b 100644 --- a/rust/maprando-web/templates/generate/help/progression/item_location_restriction.html +++ b/rust/maprando-web/templates/generate/help/progression/item_location_restriction.html @@ -7,7 +7,7 @@