public Assignment AssignItems(Random random, RandomizerOptions options, Preset preset) { List <string> itemOrder = new List <string>(items); // Right now, assign key items in a random order, with endgame items last. // We will get more devious runs from assigning later items later, may be worth looking into, especially for game with clear phases like Sekiro has. Shuffle(random, itemOrder); bool isEndgame(string i) => i.StartsWith("cinder") || i == "secretpassagekey"; itemOrder = itemOrder.OrderBy(i => isEndgame(i) ? 1 : 0).ToList(); // In race mode, always put shinobiprosthetic and younglordsbellcharm first, since there is only one ashinaoutskirts_template spot for key item placement logic if (ann.RaceModeItems.Count > 0) { itemOrder = itemOrder.OrderBy(i => i == "shinobiprosthetic" || i == "younglordsbellcharm" ? 0 : 1).ToList(); } Assignment ret = new Assignment(); // Find which events are required to access other things, and do not give them as much weight as dead ends with placement biases foreach (Node node in nodes.Values) { if (node.Req != null) { ret.RequiredEvents.UnionWith(node.Req.FreeVars().Where(v => ann.Events.ContainsKey(v))); } } // First assign ashes for key item placement. Other quest assignments can happen later. // TODO: Make ashes its own system, rather than abusing quest system. if (ann.ItemGroups.ContainsKey("ashes")) { List <string> ashesOrder = new List <string>(areas); ashesOrder = WeightedShuffle(random, ashesOrder, loc => Math.Min(nodes[loc].KeyCount - nodes[loc].Counts[UniqueCategory.KEY_SHOP], 3)); int ashesIndex = 0; foreach (KeyValuePair <LocationScope, SlotAnnotation> entry in ann.Slots) { LocationScope scope = entry.Key; SlotAnnotation slot = entry.Value; HashSet <string> tags = slot.GetTags(); // Unique item unlocks other unique items unconditionally - can add a location for key item. Mainly for ashes. if (slot.QuestReqs != null && !slot.HasAnyTags(ann.NoKeyTags) && scope.UniqueId > 0 && slot.ItemReqs.Count == 1) { ItemKey key = slot.ItemReqs[0]; if (ret.Assign.ContainsKey(key)) { throw new Exception($"Multiple assignments for {slot.QuestReqs}"); } string selected = ashesOrder[ashesIndex++]; if (explain) { Console.WriteLine($"Assigning key quest item {slot.QuestReqs} to {selected}"); } int ashesCount = data.Location(scope).Count; nodes[selected].AddShopCapacity(false, ashesCount); nodes[selected].AddItem(false, false); ret.Assign[key] = new HashSet <string> { selected }; if (ann.AreaEvents.TryGetValue(selected, out List <string> events)) { ret.Assign[key].UnionWith(events); } } } } if (explain) { int raceModeCount = 0; foreach (string area in areas) { int rmode = nodes[area].Count(false, true); if (rmode > 0) { Console.WriteLine($"RACEMODE {area}: {rmode}"); raceModeCount += rmode; } } Console.WriteLine($"TOTAL: {raceModeCount}"); } Dictionary <string, string> forcemap = new Dictionary <string, string>(); if (options["norandom"]) { foreach (string item in itemOrder) { ItemKey itemKey = ann.Items[item]; // Some hardcoding for Path of the Dragon which is not an actual item if (item == "pathofthedragon") { forcemap[item] = "highwall_garden"; } else { ItemLocation loc = data.Data[itemKey].Locations.Values.First(); SlotAnnotation sn = ann.Slot(loc.LocScope); forcemap[item] = sn.Area; } } } else if (preset?.Items != null) { foreach (KeyValuePair <string, string> entry in preset.Items) { forcemap[entry.Key] = entry.Value; } } // Assign key items bool debugChoices = false; float scaling = options.GetNum("keyitemchainweight"); Dictionary <string, Expr> reqs = CollapseReqs(); foreach (string item in itemOrder) { List <string> allowedAreas = areas.Where(a => !reqs[a].Needs(item)).ToList(); if (debugChoices) { Console.WriteLine($"\n> {item} not allowed in areas: {string.Join(",", areas.Where(a => !allowedAreas.Contains(a)))}"); } bool redundant = allowedAreas.Count == areas.Count; HashSet <string> neededForEvent = itemEvents.TryGetValue(item, out HashSet <string> ev) ? ev : null; allowedAreas.RemoveAll(a => ann.Areas[a].Until != null && (neededForEvent == null || !neededForEvent.Contains(ann.Areas[a].Until))); Dictionary <string, float> specialScaling = new Dictionary <string, float>(); if (item == "smalllothricbanner") { specialScaling = allowedAreas.Where(area => ann.Areas[area].BoringKeyItem).ToDictionary(area => area, _ => 1 / scaling); } // A bit hacky, but prevent Small Lothric Banner from being so prevalent in Firelink Shrine Dictionary <string, float> weights = allowedAreas.ToDictionary(area => area, area => Weight(area) * (specialScaling.TryGetValue(area, out float sp) ? sp : 1)); if (debugChoices) { Console.WriteLine($"> Choices for {item}: " + string.Join(", ", allowedAreas.OrderBy(a => weights[a]).Select(a => $"{a} {weights[a]}"))); } string selected = WeightedChoice(random, allowedAreas, a => weights[a]); if (forcemap.TryGetValue(item, out string forced)) { if (explain && !allowedAreas.Contains(forced)) { Console.WriteLine($"Key item {item} put in non-random location {forced} which isn't normally allowed by logic"); } selected = forced; } AddItem(item, selected, forced != null); ItemKey itemKey = ann.Items[item]; ret.Priority.Add(itemKey); ret.Assign[itemKey] = new HashSet <string> { selected }; // Areas should include events there. Except for bell charm being dropped by chained ogre, if that option is enabled // todo: check this works okay with racemode key items, and nothing else randomized. if (!(item == "younglordsbellcharm" && options["earlyhirata"])) { if (ann.AreaEvents.TryGetValue(selected, out List <string> events)) { ret.Assign[itemKey].UnionWith(events); } } if (explain || debugChoices) { Console.WriteLine($"Adding {item} to {string.Join(",", ret.Assign[itemKey])}"); } // Update weights reqs = CollapseReqs(); // If item was not really needed, don't update weighhts if (redundant) { if (explain) { Console.WriteLine($"{item} is redundant to another key item (does not uniquely make new areas available)"); } continue; } // Heuristic which forms chains and spreads items across areas // Reduce weight for this area, and increase weight for areas which depend on the item AdjustWeight(selected, 1 / scaling); HashSet <string> addedAreas = new HashSet <string>(); foreach (string area in areas) { if (addedAreas.Contains(combinedWeights[area].First())) { continue; } if (reqs[area].Needs(item)) { AdjustWeight(area, scaling); addedAreas.Add(combinedWeights[area].First()); } } } // The last placed item has the highest priority ret.Priority.Reverse(); // Now that all key items have been assigned, determine which areas are blocked by other areas. // This is used to determine lateness within the game (by # of items encountered up to that point). HashSet <string> getIncludedAreas(string name, List <string> path) { path = path.Concat(new[] { name }).ToList(); if (!nodes.TryGetValue(name, out Node node)) { throw new Exception($"Bad options: no way to access area \"{name}\""); } if (ret.IncludedAreas.ContainsKey(name)) { if (ret.IncludedAreas[name] == null) { throw new Exception($"Loop from {name} to {node.Req} - path {string.Join(",", path)}"); } return(ret.IncludedAreas[name]); } ret.IncludedAreas[name] = null; HashSet <string> result = new HashSet <string>(); // Add all nodes for now, but remove items later if (areas.Contains(name) || ann.Events.ContainsKey(name) || ann.Items.ContainsKey(name)) { result.Add(name); } foreach (string free in node.Req.FreeVars()) { if (!(loops.ContainsKey(name) && loops[name].Contains(free))) { result.UnionWith(getIncludedAreas(free, path)); } } ret.IncludedAreas[name] = result; return(result); }; foreach (Node node in nodes.Values) { getIncludedAreas(node.Name, new List <string>()); // Redefine weights for quest selection node.Weight = 1; if (areas.Contains(node.Name)) { node.CumKeyCount = ret.IncludedAreas[node.Name].Where(n => nodes[n].Counts != null).Select(n => nodes[n].Count(true, true)).Sum(); if (explain && false) { Console.WriteLine($"Quest area {node.Name}: {node.Count(true, true)}/{node.CumKeyCount}: {string.Join(",", ret.IncludedAreas[node.Name])}"); } } } // Find out which items are required to access the ending, while items are still included in the graph. // For hints later. // Also do this in DS3 after deciding where to add hints. if (ret.IncludedAreas.TryGetValue("ashinareservoir_end", out HashSet <string> requiredForEnd)) { List <ItemKey> requiredItems = requiredForEnd.Where(t => ann.Items.ContainsKey(t)).Select(t => ann.Items[t]).ToList(); ret.NotRequiredKeyItems.UnionWith(ret.Priority.Except(requiredItems)); } // The above DFS adds both items and areas together, so remove the items. foreach (string key in ret.IncludedAreas.Keys.ToList()) { if (ann.Items.ContainsKey(key)) { ret.IncludedAreas.Remove(key); } else { ret.IncludedAreas[key].RemoveWhere(v => ann.Items.ContainsKey(v)); } } foreach (string area in unusedAreas) { ret.IncludedAreas[area] = new HashSet <string>(); } // Make an area order List <string> areaOrder = areas.OrderBy(a => nodes[a].CumKeyCount).ToList(); Dictionary <string, int> areaIndex = Enumerable.Range(0, areaOrder.Count()).ToDictionary(i => areaOrder[i], i => i); string latestArea(IEnumerable <string> ns) { return(areaOrder[ns.Select(n => areaIndex.TryGetValue(n, out int i) ? i : throw new Exception($"No order for area {n}")).DefaultIfEmpty().Max()]);
public void RandomizeTrees(Random random, Permutation permutation, SkillSplitter.Assignment split) { // >= 700: prosthetics // < 400: skills before mushin GameEditor editor = game.Editor; PARAM param = game.Params["SkillParam"]; // Orderings for skills which completely supersede each other. (For prosthetics, just use their natural id ordering) Dictionary <int, int> skillOrderings = new Dictionary <int, int> { [110] = 111, // Nightjar slash [210] = 211, // Ichimonji [310] = 311, // Praying Strikes }; Dictionary <int, ItemKey> texts = new Dictionary <int, ItemKey> { [0] = game.ItemForName("Shinobi Esoteric Text"), [1] = game.ItemForName("Prosthetic Esoteric Text"), [2] = game.ItemForName("Ashina Esoteric Text"), [3] = game.ItemForName("Senpou Esoteric Text"), // [4] = game.ItemForName("Mushin Esoteric Text"), }; SortedDictionary <ItemKey, string> names = game.Names(); string descName(int desc) { return(names[new ItemKey(ItemType.WEAPON, desc)]); } Dictionary <int, SkillData> allData = new Dictionary <int, SkillData>(); Dictionary <int, SkillSlot> allSlots = new Dictionary <int, SkillSlot>(); Dictionary <ItemKey, SkillData> skillItems = new Dictionary <ItemKey, SkillData>(); List <SkillData> skills = new List <SkillData>(); List <SkillSlot> skillSlots = new List <SkillSlot>(); List <SkillData> prosthetics = new List <SkillData>(); List <SkillSlot> prostheticSlots = new List <SkillSlot>(); bool explain = false; foreach (PARAM.Row r in param.Rows) { SkillData data = new SkillData { ID = (int)r.ID, Item = (int)r["SkilLDescriptionId"].Value, Equip = (int)r["Unk1"].Value, Flag = (int)r["EventFlagId"].Value, Placeholder = (int)r["Unk5"].Value, SpEffects = new[] { (int)r["Unk2"].Value, (int)r["Unk3"].Value }, EmblemChange = (byte)r["Unk10"].Value != 0, }; data.Key = new ItemKey(ItemType.WEAPON, data.Item); skillItems[data.Key] = data; SkillSlot slot = new SkillSlot { ID = (int)r.ID, Col = (short)r["MenuDisplayPositionIndexXZ"].Value, Row = (short)r["MenuDisplayPositionIndexY"].Value, Text = data.ID < 400 && texts.TryGetValue((byte)r["Unk7"].Value, out ItemKey text) ? text : null, }; if (explain) { Console.WriteLine($"{r.ID}: {data.Item}, {data.Equip}, {descName(data.Item)}"); } if (data.ID < 400) { skills.Add(data); skillSlots.Add(slot); } else if (data.ID >= 700) { prosthetics.Add(data); prostheticSlots.Add(slot); } allData[data.ID] = data; allSlots[slot.ID] = slot; } void applyData(PARAM.Row r, SkillData data) { r["SkilLDescriptionId"].Value = data.Item; r["EventFlagId"].Value = data.Flag; r["Unk1"].Value = data.Equip; r["Unk2"].Value = data.SpEffects[0]; r["Unk3"].Value = data.SpEffects[0]; r["Unk5"].Value = data.Placeholder; r["Unk10"].Value = (byte)(data.EmblemChange ? 1 : 0); } Shuffle(random, skills); Shuffle(random, skillSlots); Shuffle(random, prosthetics); Shuffle(random, prostheticSlots); // Skills rando if (split == null) { Dictionary <ItemKey, string> textWeight = new Dictionary <ItemKey, string>(); Dictionary <ItemKey, string> textLocations = texts.Values.ToDictionary(t => t, t => { SlotKey target = permutation.GetFiniteTargetKey(t); textWeight[t] = permutation.GetLogOrder(target); SlotAnnotation sn = ann.Slot(data.Location(target).LocScope); if (explain) { Console.WriteLine($"{game.Name(t)} in {sn.Area} - {sn.Text}. Lateness {(permutation.ItemLateness.TryGetValue(t, out double val) ? val : -1)}"); } return(sn.Area); });