예제 #1
0
        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}%";
        }
예제 #2
0
        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()]);