Example #1
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()]);
        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);
                });