//retrieve all accessible RewardSources(with no item in them) from the SanityChecker private List <IRewardSource> GetAllAccessibleRewardSources(IEnumerable <IRewardSource> preBlackOrbLocationPool, List <IRewardSource> placedItems, Item item1 = Item.None, Item item2 = Item.None) { var placedItems2 = placedItems.Select(i => logicSources.TryGetValue(i.Address, out var l) ? new SCLogicRewardSource { Requirements = l.Requirements, RewardSource = i } : null).Where(l => l != null).ToList(); SCRequirements requirements = freeRequirements; if (item1 != Item.None) { requirements = requirements.AddItem(item1); } if (item2 != Item.None) { requirements = requirements.AddItem(item2); } List <SCLogicRewardSource> toRemove = new List <SCLogicRewardSource>(placedItems2.Count); var maxCount = placedItems2.Count; for (int i = 0; i < maxCount; i++) { foreach (var r in placedItems2.Where(l => l.Requirements.IsAccessible(requirements))) { toRemove.Add(r); requirements = requirements.AddItem(r.RewardSource.Item); } foreach (var r in toRemove) { placedItems2.Remove(r); } if (toRemove.Count == 0) { break; } if (placedItems2.Count == 0) { break; } toRemove.Clear(); } return(preBlackOrbLocationPool.Where(i => logicSources.TryGetValue(i.Address, out var l) && l.Requirements.IsAccessible(requirements)).Where(x => !placedItems.Any(y => y.Address == x.Address)).ToList()); }
public static SCRequirements AddItem(this SCRequirements left, Item right) { switch (right) { case Item.Adamant: return(left | SCRequirements.Adamant); case Item.Bottle: return(left | SCRequirements.Bottle); case Item.Bridge: return(left | SCRequirements.Bridge); case Item.Canal: return(left | SCRequirements.Canal); case Item.Canoe: return(left | SCRequirements.Canoe); case Item.Chime: return(left | SCRequirements.Chime); case Item.Crown: return(left | SCRequirements.Crown); case Item.Crystal: return(left | SCRequirements.Crystal); case Item.Cube: return(left | SCRequirements.Cube); case Item.Floater: return(left | SCRequirements.Floater); case Item.Herb: return(left | SCRequirements.Herb); case Item.Key: return(left | SCRequirements.Key); case Item.Lute: return(left | SCRequirements.Lute); case Item.Oxyale: return(left | SCRequirements.Oxyale); case Item.Rod: return(left | SCRequirements.Rod); case Item.Ruby: return(left | SCRequirements.Ruby); case Item.Ship: return(left | SCRequirements.Ship); case Item.Slab: return(left | SCRequirements.Slab); case Item.Tnt: return(left | SCRequirements.Tnt); default: return(left); } }
public SCLogicArea(short areaId, SCOwArea area, SCRequirements flags) { AreaId = areaId; Area = area; Requirements = new SCRequirementsSet(flags); }
public static bool IsEqual(this SCRequirements left, SCRequirements right) { return(left == right); }
public static bool IsOrthogonalTo(this SCRequirements left, SCRequirements right) { return(!left.IsSupersetOf(right) && !right.IsSupersetOf(left)); }
public static bool IsSubsetOf(this SCRequirements left, SCRequirements right) { return((right & left) == left); }
public static bool IsStrictSupersetOf(this SCRequirements left, SCRequirements right) { return((right & left) == right && left != right); }
private List <string> GetRule(SCRequirements l) { var list = new List <string>(); if (l.HasFlag(SCRequirements.Lute)) { list.Add(Item.Lute.ToString()); } if (l.HasFlag(SCRequirements.Crown)) { list.Add(Item.Crown.ToString()); } if (l.HasFlag(SCRequirements.Key)) { list.Add(Item.Key.ToString()); } if (l.HasFlag(SCRequirements.Ruby)) { list.Add(Item.Ruby.ToString()); } if (l.HasFlag(SCRequirements.Rod)) { list.Add(Item.Rod.ToString()); } if (l.HasFlag(SCRequirements.Chime)) { list.Add(Item.Chime.ToString()); } if (l.HasFlag(SCRequirements.Cube)) { list.Add(Item.Cube.ToString()); } if (l.HasFlag(SCRequirements.Oxyale)) { list.Add(Item.Oxyale.ToString()); } if (l.HasFlag(SCRequirements.Tnt)) { list.Add(Item.Tnt.ToString()); } if (l.HasFlag(SCRequirements.Canoe)) { list.Add(Item.Canoe.ToString()); } if (l.HasFlag(SCRequirements.Floater)) { list.Add(Item.Floater.ToString()); } if (l.HasFlag(SCRequirements.Bridge)) { list.Add(Item.Bridge.ToString()); } if (l.HasFlag(SCRequirements.Canal)) { list.Add(Item.Canal.ToString()); } if (l.HasFlag(SCRequirements.Bottle)) { list.Add(Item.Bottle.ToString()); } if (l.HasFlag(SCRequirements.Crystal)) { list.Add(Item.Crystal.ToString()); } if (l.HasFlag(SCRequirements.Herb)) { list.Add(Item.Herb.ToString()); } if (l.HasFlag(SCRequirements.Adamant)) { list.Add(Item.Adamant.ToString()); } if (l.HasFlag(SCRequirements.Slab)) { list.Add(Item.Slab.ToString()); } if (l.HasFlag(SCRequirements.Ship)) { list.Add(Item.Ship.ToString()); } return(list); }
protected override ItemPlacementResult DoSanePlacement(MT19337 rng, ItemPlacementContext ctx) { exceptionWeights = _flags.AllowUnsafePlacement || (_flags.Entrances ?? false) ? UnsafeExceptionWeights : SafeExceptionWeights; exceptionMinCycles = _flags.AllowUnsafePlacement || (_flags.Entrances ?? false) ? UnsafeExceptioMinCycles : SafeExceptioMinCycles; minAccessibility = _flags.AllowUnsafePlacement || (_flags.Entrances ?? false) ? UnsafeMinAccessibility : SafeMinAccessibility; _sanityCounter = 0; var incentiveLocationPool = new HashSet <IRewardSource>(_incentivesData.IncentiveLocations, new RewardSourceEqualityComparer()); var preBlackOrbLocationPool = _incentivesData.AllValidPreBlackOrbItemLocations.ToList(); var preBlackOrbUnincentivizedLocationPool = preBlackOrbLocationPool.Where(x => !incentiveLocationPool.Any(y => y.Address == x.Address)).ToList(); if ((bool)_flags.LooseExcludePlacedDungeons) { preBlackOrbUnincentivizedLocationPool = IncentivizedDungeons(preBlackOrbUnincentivizedLocationPool); } var unincentivizedLocationPool = new HashSet <IRewardSource>(preBlackOrbUnincentivizedLocationPool, new RewardSourceEqualityComparer()); var allRewardSources = preBlackOrbLocationPool.Append(new ItemShopSlot(_caravanItemLocation, Item.None)).ToList(); List <IRewardSource> placedItems = null; List <Item> treasurePool = null; freeRequirements = BuildFreeRequirements(); bool placementFailed; do { ((SanityCheckerV2)_checker).Shiplocations.SetShipLocation(255); BuildLogic(allRewardSources); placementFailed = false; var balancedPicker = new RewardSourcePicker(0.5, _flags.LooseItemsNpcBalance ? 7.0 : 1.0, _checker); _sanityCounter++; if (_sanityCounter > 2) { throw new InsaneException("Item Placement could not meet incentivization requirements!"); } placedItems = ctx.Forced.ToList(); var incentives = new HashSet <Item>(ctx.Incentivized); var nonincentives = ctx.Unincentivized.ToList(); var shards = ctx.Shards.ToList(); treasurePool = ctx.AllTreasures.ToList(); var state = PlacementState.Normal; while (incentives.Count() > incentiveLocationPool.Count()) { nonincentives.Add(incentives.SpliceRandom(rng)); } if (((bool)_flags.NPCItems) || ((bool)_flags.NPCFetchItems)) { HashSet <Item> allPlacements = new HashSet <Item>(nonincentives.Concat(incentives)); HashSet <Item> allKeyItems = new HashSet <Item>(MapChangeItems.Concat(FetchQuestItems).Concat(GatingItems).Intersect(allPlacements)); if ((bool)_flags.IsFloaterRemoved) { allKeyItems.Remove(Item.Floater); } //The sanity checker currently doesn't allow tracking which shops are available //It could be easily added, but a little randomnes can't hurt(or so I'm thinking) //So it places the vendoritem upfront. var itemShopItem = SelectVendorItem(incentives.ToList(), nonincentives, treasurePool, incentiveLocationPool, rng); placedItems.Add(new ItemShopSlot(_caravanItemLocation, itemShopItem)); allPlacements.Remove(itemShopItem); allKeyItems.Remove(itemShopItem); if (_flags.Archipelago && allKeyItems.Contains(Item.Bridge)) { var accessibleSources = GetAllAccessibleRewardSources(preBlackOrbUnincentivizedLocationPool, placedItems); var rewardSource = balancedPicker.Pick(accessibleSources, _flags.LooseItemsForwardPlacement, _flags.LooseItemsSpreadPlacement, false, rng); placedItems.Add(NewItemPlacement(rewardSource, Item.Bridge)); allPlacements.Remove(Item.Bridge); allKeyItems.Remove(Item.Bridge); } //Since it can waste cycles, this number needs to be high enough to be able to place all incentives and all nonincentives and still have some leeway //When it's done it breaks anyway //When it reached enough cycles so no cycle based restricitons apply and can place neither an incentive nor a nonincentive it also breaks for (int cycle = 1; cycle <= 100; cycle++) { var accessibleSources = GetAllAccessibleRewardSources(preBlackOrbLocationPool, placedItems); //look how many rewardsources of each type are available var incSourceCount = accessibleSources.Where(s => incentiveLocationPool.Contains(s)).Count(); var nonSourceCount = accessibleSources.Count - incSourceCount; List <(Item item, int incCount, int nonCount)> candidates = new List <(Item item, int incCount, int nonCount)>(); //go through each KI, place it in the testRewardSource, run the sanity checker and see how many RewardSources are available afterwards. foreach (var item in allKeyItems) { var accessibleSources2 = GetAllAccessibleRewardSources(preBlackOrbLocationPool, placedItems, item); var incCount = accessibleSources2.Where(s => incentiveLocationPool.Contains(s)).Count(); var nonCount = accessibleSources2.Count - incCount - nonSourceCount; incCount -= incSourceCount; //if the placed item is an incentive, it will take up an incentive location when it will be placed //and vice versa if (incentives.Contains(item)) { incCount--; } if (!incentives.Contains(item)) { nonCount--; } candidates.Add((item, incCount, nonCount)); } //Since it can happen, that neither the ship, nor the canal by themselves open up something, but the combination does, //here the combination is checked //it grabs another Rewardsource, places Ship and Canal, runs the SanityChecker and calculates the results if (allKeyItems.Contains(Item.Ship) && allKeyItems.Contains(Item.Canal)) { var accessibleSources2 = GetAllAccessibleRewardSources(preBlackOrbLocationPool, placedItems, Item.Ship, Item.Canal); var incCount = accessibleSources2.Where(s => incentiveLocationPool.Contains(s)).Count(); var nonCount = accessibleSources2.Count - incCount - nonSourceCount; incCount -= incSourceCount; if (incentives.Contains(Item.Ship)) { incCount--; } if (!incentives.Contains(Item.Ship)) { nonCount--; } if (incentives.Contains(Item.Canal)) { incCount--; } if (!incentives.Contains(Item.Canal)) { nonCount--; } //it gets the ship and canal candidate from the list, later it's put back in var shipCandidate = candidates.FirstOrDefault(c => c.item == Item.Ship); var canalCandidate = candidates.FirstOrDefault(c => c.item == Item.Canal); candidates.Remove(shipCandidate); candidates.Remove(canalCandidate); //half of the opened up incentive locations and nonincentive locations hould go to either item(rounded down) //In order to continue the seed, Ship and Canal must open up 2 incentive locations, since they may take up 2 //it's not an exact estimate, since it doesn'T differentiate between them beeing incentive or nonincentive items incCount /= 2; nonCount /= 2; if (shipCandidate.incCount < incCount && canalCandidate.incCount < incCount) { shipCandidate.incCount = incCount; canalCandidate.incCount = incCount; } if (shipCandidate.nonCount < nonCount && canalCandidate.nonCount < nonCount) { shipCandidate.nonCount = nonCount; canalCandidate.nonCount = nonCount; } candidates.Add(shipCandidate); candidates.Add(canalCandidate); } var incKeyItemCount = incentives.Where(i => allKeyItems.Contains(i)).Count(); var nonKeyItemCount = nonincentives.Where(i => allKeyItems.Contains(i)).Count(); List <(float weight, Item item)> weightedCandidates; var incItemCount = allPlacements.Where(i => incentives.Contains(i)).Count(); //here it randomly decides whether to place and incentive or a non incentive //the chance is based on the number of incentive and nonincentive items //If however it couldn't place an incentive item in the last cycle, it'll always try a nonincentive this time //and vice versa if (state == PlacementState.Incentive || state == PlacementState.Normal && rng.Between(1, allPlacements.Count) <= incItemCount || _flags.LaterLoose && state == PlacementState.Normal && cycle <= 5) { //Filter out nonincentive items candidates = candidates.Where(c => incentives.Contains(c.item)).ToList(); var nonKeyCandidates = allPlacements.Where(i => !allKeyItems.Contains(i) && incentives.Contains(i)); if (incSourceCount <= 1 || incSourceCount <= CriticalIncentiveSourceCount && allKeyItems.Contains(Item.Ship) && allKeyItems.Contains(Item.Canal)) { //we are in incentive critical mode //the function takes weights for different categories of items(depending of how they're expected they change the amount of incentive locations available) //bin0 doesn't open up a new location(it therefore reduces the incentive location count by one) //bin1 opens up 1 location and therefore keeps the count the same //bin2 opens up 2 locations and therefore increases the amount by 1 //bin3 opens up more than two locations. It has two parameters here, more on that further down //Anyway, we can't afford to reduce the incentive location count here(bin0) //We also don't neccesarily want to keep it the same, but that's okish(bin1) //we want to increase the amount(bin2, bin3) weightedCandidates = AssignWeights(candidates, 0.0f, 0.5f, 1.0f, 1.0f, 0.5f, cycle, accessibleSources.Count); } else if (incSourceCount <= SafeIncentiveSourceCount) { //We have 1 incentive location to squander, not a lot, but sufficient //We don't want to reduce it or keep it the same, but it would still be ok(bin0, bin1) //We'd like to increase it a little(bin2) //but not too much(bin3) weightedCandidates = AssignWeights(candidates, 0.5f, 0.5f, 1.0f, 0.5f, 0.25f, cycle, accessibleSources.Count); //it's also ok to place a NonKeyItem, wa have an incentive location to squander after all //but don't make it too likely weightedCandidates.AddRange(nonKeyCandidates.Select(i => (GetItemWeight(i, 0.5f, cycle, accessibleSources.Count), i))); } else { //We have at least 2 incentive locations to squander(considered a lot) //We like reducing it by one(for variety) //We like keeping it the same(for variety) //We also like to increase it slightly(for variety) //But we don't like to increase it a lot, but then again it's not a problem, so make it less likely weightedCandidates = AssignWeights(candidates, 1.0f, 1.0f, 1.0f, 0.25f, 0.25f, cycle, accessibleSources.Count); //Also we like placing NoneKeyItems weightedCandidates.AddRange(nonKeyCandidates.Select(i => (GetItemWeight(i, 1.0f, cycle, accessibleSources.Count), i))); } //If we didn't find any candidates, but we have enough locations to place all remaining items, then we just do that //Otherwise the algorithm would refuse to give up the last 2 incentive locations if (weightedCandidates.Sum(i => i.weight) == 0 && incItemCount <= incSourceCount) { weightedCandidates.AddRange(allPlacements.Where(i => incentives.Contains(i)).Select(i => (1.0f, i))); } //If we don't have any candidates and not enough incentive locations to place all, we can't place an incentive in this cycle if (weightedCandidates.Sum(i => i.weight) == 0) { //If it's at least cycle 15 and therefore no cycle based restricctions apply //And in the last cycle we tried to place a NonIncentive //we had all KI options available, but couldn't place any of them, so we're doomed if (cycle >= 15 && state != PlacementState.Normal) { placementFailed = true; break; } //since we couldn't place an incentive, we won't be able to do so next cycle(same results), so we don't try to do that next cycle //This heavily speeds up the situations when there are very few loose(low chance of trying to place a loose), but a loose is required to go on. state = PlacementState.NonIncentice; continue; } } else { //same as above, but for nonincentive items. //The main difference is that the RewardSource thresholds are way higher //While it's not a safety concern, cou generally want a lot more chest's available than neccesary to be in the confort zone candidates = candidates.Where(c => !incentives.Contains(c.item)).ToList(); var nonKeyCandidates = allPlacements.Where(i => !allKeyItems.Contains(i) && !incentives.Contains(i)); if (nonSourceCount <= CriticalNonIncentiveSourceCount) { weightedCandidates = AssignWeights2(candidates, 0.0f, 0.5f, 1.0f, 1.0f, 0.5f, cycle, accessibleSources.Count); } else if (nonSourceCount <= SafeNonIncentiveSourceCount) { weightedCandidates = AssignWeights2(candidates, 0.5f, 0.5f, 1.0f, 0.5f, 0.25f, cycle, accessibleSources.Count); weightedCandidates.AddRange(nonKeyCandidates.Select(i => (GetItemWeight(i, 0.5f, cycle, accessibleSources.Count), i))); } else { weightedCandidates = AssignWeights2(candidates, 1.0f, 1.0f, 1.0f, 0.25f, 0.25f, cycle, accessibleSources.Count); weightedCandidates.AddRange(nonKeyCandidates.Select(i => (GetItemWeight(i, 1.0f, cycle, accessibleSources.Count), i))); } if (weightedCandidates.Sum(i => i.weight) == 0) { if (cycle >= 15 && state != PlacementState.Normal) { placementFailed = true; break; } state = PlacementState.Incentive; continue; } } //The above weighting strategy works with this mindset: //If it's an all incentive seed, we just need to manage the incentive locations //If it's an all loose seed, we just need to manage the nonincentive locations //If it's something in between, we switch between those two ideas. //It works, because a nonincentive item goes into a nonincentive chest and can't negatively impact the incentive locations. //and vice versa. //There is a flaw in this simplistic approach. When placing a nonincentive, it could additionally weigh in if it's desired for incentive placment. //But that's a thing for another time to think through //remove the candidates with weight 0(should be unneccesary, but anyway) weightedCandidates = weightedCandidates.Where(w => w.weight > 0).ToList(); //shuffle the list(should be totally unneccesary, but it makes me more comfortable) weightedCandidates.Shuffle(rng); //randomly select an item from the weighted list Item nextPlacment = Item.None; float sum = weightedCandidates.Sum(i => i.weight); var r = rng.Next() / (float)uint.MaxValue * sum; sum = 0.0f; foreach (var s in weightedCandidates) { sum += s.weight; if (r <= sum) { nextPlacment = s.item; break; } } //if we didn't find one, just select a random one(this shouldn't happen, but safe is safe) if (nextPlacment == Item.None) { nextPlacment = weightedCandidates.PickRandom(rng).item; } //place the item(same code as in GuidedPlacement to have compatibility with other flags) if (incentives.Contains(nextPlacment)) { var rewardSources = accessibleSources.Where(s => incentiveLocationPool.Contains(s)).ToList(); //can't happen per definition. We already know, that there is a location available for the item(but in case anything unexpectedly goes wrong sure) if (rewardSources.Count == 0) { continue; } var rewardSource = balancedPicker.Pick(rewardSources, false, _flags.LooseItemsSpreadPlacement, false, rng); placedItems.Add(NewItemPlacement(rewardSource, nextPlacment)); } else { var rewardSources = accessibleSources.Where(s => unincentivizedLocationPool.Contains(s)).ToList(); if (rewardSources.Count == 0) { continue; } var rewardSource = balancedPicker.Pick(rewardSources, _flags.LooseItemsForwardPlacement, _flags.LooseItemsSpreadPlacement, false, rng); placedItems.Add(NewItemPlacement(rewardSource, nextPlacment)); } //remove the item from the lists of items to place allPlacements.Remove(nextPlacment); allKeyItems.Remove(nextPlacment); if (nextPlacment == Item.Ship) { BuildLogic(allRewardSources); } //we placed an item so we should randomly select incentive/nonincentive next cycle state = PlacementState.Normal; //we're finished if (allPlacements.Count == 0) { break; } } //we couldn't place all items or encountered a placement problem if (allPlacements.Count > 0 || placementFailed) { continue; } } //place the shards(almost same code as in GuidedPlacement) if (!placementFailed && shards.Count > 0) { var leftoverItemLocations = GetAllAccessibleRewardSources(preBlackOrbLocationPool, placedItems); foreach (var shard in shards) { if (!leftoverItemLocations.Any()) { placementFailed = true; break; } placedItems.Add(NewItemPlacement(leftoverItemLocations.SpliceRandom(rng), shard)); } } //finally check the placement(if we arrive here, it's safe, but let's do it anyway in case something goes wrong) } while (placementFailed || !_checker.CheckSanity(placedItems, null, _flags).Complete); return(new ItemPlacementResult { PlacedItems = placedItems, RemainingTreasures = treasurePool }); }