private void UpdateLabels() { string unfairText = ""; // if (options.GetNum("veryunfairweight") > 0.5) unfairText = " and very unfair"; // else if (options.GetNum("unfairweight") > 0.5) unfairText = " and unfair"; string loc; if (options.GetNum("allitemdifficulty") > 0.86) { loc = $"Much better rewards for difficult and late{unfairText} locations."; } else if (options.GetNum("allitemdifficulty") > 0.55) { loc = $"Better rewards for difficult and late{unfairText} locations."; } else if (options.GetNum("allitemdifficulty") > 0.3) { loc = $"Slightly better rewards for difficult and late{unfairText} locations."; } else if (options.GetNum("allitemdifficulty") > 0.001) { loc = "Most locations for items are equally likely. Often results in a lot of early memories and prayer beads."; } else { loc = "All locations for items are equally likely. Often results in a lot of early memories and prayer beads."; } string chain = ""; if (!options["norandom"]) { if (options.GetNum("keyitemchainweight") <= 3) { chain = "Key items will usually be easy to find and not require much side content."; } else if (options.GetNum("keyitemchainweight") <= 6) { chain = "Key items will usually be in different areas and depend on each other."; } // else if (options.GetNum("keyitemchainweight") <= 10) chain = "Key items will usually form long chains across different areas."; else { chain = "Key items will usually be in different areas and form interesting chains."; } } difficultyL.Text = $"{loc}\r\n{chain}"; difficultyAmtL.Text = $"{options.Difficulty}%"; }
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()]);