public static readonly List <int> UsedTreasureIndices = Enumerable.Range(0, 256).Except(UnusedTreasureIndices).ToList(); // This maps a compacted list back to the game's array, skipping the unused slots. public List <IRewardSource> ShuffleTreasures(MT19337 rng, ITreasureShuffleFlags flags, IncentiveData incentivesData, ItemShopSlot caravanItemLocation, Dictionary <MapLocation, List <MapChange> > mapLocationRequirements) { if (flags.NPCItems) { EnableBridgeShipCanalAnywhere(); EnableNPCsGiveAnyItem(); // This extends Vampire's routine to set a flag for Sarda, but it also clobers Sarda's routine if (!flags.EarlyRod) { Put(0x393E1, Blob.FromHex("207F90A51160")); } } var treasureBlob = Get(TreasureOffset, TreasureSize * TreasureCount); var treasurePool = UsedTreasureIndices.Select(x => (Item)treasureBlob[x]) .Concat(ItemLists.AllNonTreasureChestItems).ToList(); if (flags.OrbHunt) { treasurePool = treasurePool.Select(OrbHuntTreasureSelector).ToList(); System.Diagnostics.Debug.Assert(treasurePool.Count(item => item == Item.Shard) == TotalOrbsToInsert); } var placedItems = ItemPlacement.PlaceSaneItems(rng, flags, incentivesData, treasurePool, caravanItemLocation, mapLocationRequirements); // Output the results tothe ROM foreach (var item in placedItems.Where(x => !x.IsUnused && x.Address < 0x80000 && !incentivesData.ForcedItemPlacements.Any(y => y.Address == x.Address))) { //Debug.WriteLine(item.SpoilerText); item.Put(this); } return(placedItems); }
public static bool CheckSanity(List <IRewardSource> treasurePlacements, ITreasureShuffleFlags flags, Dictionary <MapLocation, List <MapChange> > mapLocationRequirements) { const int maxIterations = 20; var currentIteration = 0; var currentAccess = AccessRequirement.None; var currentMapChanges = MapChange.None; var allMapLocations = Enum.GetValues(typeof(MapLocation)) .Cast <MapLocation>().ToList(); Func <IEnumerable <MapLocation> > currentMapLocations = () => allMapLocations .Where(x => mapLocationRequirements[x] .Any(y => currentMapChanges.HasFlag(y))); Func <IEnumerable <IRewardSource> > currentItemLocations = () => treasurePlacements .Where(x => { var locations = currentMapLocations().ToList(); return(locations.Contains(x.MapLocation) && currentAccess.HasFlag(x.AccessRequirement) && (!(x is MapObject) || locations.Contains(((MapObject)x).SecondLocation))); }); var winTheGameAccess = ItemLocations.ChaosReward.AccessRequirement; var winTheGameLocation = ItemLocations.ChaosReward.MapLocation; var accessibleLocationCount = currentItemLocations().Count(); var requiredAccess = winTheGameAccess; if (!flags.EarlyOrdeals) { requiredAccess |= AccessRequirement.Crown; } var requiredMapChanges = MapChange.None; if (flags.MapTitansTrove) { requiredMapChanges |= MapChange.TitanFed; } while (!currentAccess.HasFlag(requiredAccess) || !currentMapChanges.HasFlag(requiredMapChanges) || !currentMapLocations().Contains(winTheGameLocation)) { if (currentIteration > maxIterations) { throw new InvalidOperationException($"Sanity Check hit max iterations: {currentIteration}"); } currentIteration++; var currentItems = currentItemLocations().Select(x => x.Item).ToList(); if (!currentAccess.HasFlag(AccessRequirement.Key) && currentItems.Contains(Item.Key)) { currentAccess |= AccessRequirement.Key; } if (!currentMapChanges.HasFlag(MapChange.Bridge) && currentItems.Contains(Item.Bridge) && currentMapLocations().Contains(MapLocation.BridgeLocation)) { currentMapChanges |= MapChange.Bridge; } if (!currentAccess.HasFlag(AccessRequirement.Crown) && currentItems.Contains(Item.Crown)) { currentAccess |= AccessRequirement.Crown; } if (!currentAccess.HasFlag(AccessRequirement.Crystal) && currentItems.Contains(Item.Crystal)) { currentAccess |= AccessRequirement.Crystal; } if (!currentAccess.HasFlag(AccessRequirement.Herb) && currentItems.Contains(Item.Herb)) { currentAccess |= AccessRequirement.Herb; } if (!currentMapChanges.HasFlag(MapChange.Canoe) && currentItems.Contains(Item.Canoe)) { currentMapChanges |= MapChange.Canoe; } if (!currentMapChanges.HasFlag(MapChange.Ship) && currentItems.Contains(Item.Ship) && currentMapLocations().Contains(MapLocation.ShipLocation)) { currentMapChanges |= MapChange.Ship; } if (!currentAccess.HasFlag(AccessRequirement.Tnt) && currentItems.Contains(Item.Tnt)) { currentAccess |= AccessRequirement.Tnt; } if (!currentAccess.HasFlag(AccessRequirement.Adamant) && currentItems.Contains(Item.Adamant)) { currentAccess |= AccessRequirement.Adamant; } if (!currentMapChanges.HasFlag(MapChange.Canal) && currentItems.Contains(Item.Canal) && currentMapChanges.HasFlag(MapChange.Ship)) { currentMapChanges |= MapChange.Canal; } if (!currentMapChanges.HasFlag(MapChange.TitanFed) && currentItems.Contains(Item.Ruby) && currentMapLocations().Contains(MapLocation.TitansTunnelEast)) { currentMapChanges |= MapChange.TitanFed; } if (!currentAccess.HasFlag(AccessRequirement.Rod) && currentItems.Contains(Item.Rod)) { currentAccess |= AccessRequirement.Rod; } if (!currentAccess.HasFlag(AccessRequirement.Slab) && currentItems.Contains(Item.Slab)) { currentAccess |= AccessRequirement.Slab; } if (!currentMapChanges.HasFlag(MapChange.Airship) && (currentItems.Contains(Item.Floater)) && // || currentItems.Contains(Item.Airship)) && currentMapLocations().Contains(MapLocation.AirshipLocation)) { currentMapChanges |= MapChange.Airship; } if (!currentAccess.HasFlag(AccessRequirement.Bottle) && currentItems.Contains(Item.Bottle)) { currentAccess |= AccessRequirement.Bottle; } if (!currentAccess.HasFlag(AccessRequirement.Oxyale) && currentItems.Contains(Item.Oxyale)) { currentAccess |= AccessRequirement.Oxyale; } if (!currentMapChanges.HasFlag(MapChange.Chime) && currentItems.Contains(Item.Chime) && currentMapChanges.HasFlag(MapChange.Airship)) { currentMapChanges |= MapChange.Chime; } if (!currentAccess.HasFlag(AccessRequirement.Cube) && currentItems.Contains(Item.Cube)) { currentAccess |= AccessRequirement.Cube; } if (!currentAccess.HasFlag(AccessRequirement.BlackOrb) && ItemLists.AllOrbs.All(y => currentItems.Contains(y))) { currentAccess |= AccessRequirement.BlackOrb; } if (!currentAccess.HasFlag(AccessRequirement.Lute) && currentItems.Contains(Item.Lute)) { currentAccess |= AccessRequirement.Lute; } var newCount = currentItemLocations().Count(); if (newCount <= accessibleLocationCount) { return(false); } accessibleLocationCount = newCount; } return(true); }
public static List <IRewardSource> PlaceSaneItems(MT19337 rng, ITreasureShuffleFlags flags, IncentiveData incentivesData, List <Item> allTreasures, ItemShopSlot caravanItemLocation, Dictionary <MapLocation, List <MapChange> > mapLocationRequirements) { long sanityCounter = 0; List <IRewardSource> placedItems; var incentiveLocationPool = incentivesData.IncentiveLocations.ToList(); var incentivePool = incentivesData.IncentiveItems.ToList(); var forcedItems = incentivesData.ForcedItemPlacements.ToList(); var bridgeLocations = incentivesData.BridgeLocations.ToList(); var shipLocations = incentivesData.ShipLocations.ToList(); var itemLocationPool = incentivesData.AllValidItemLocations.ToList(); var unincentivizedQuestItems = ItemLists.AllQuestItems .Where(x => !incentivePool.Contains(x) && x != Item.Ship && x != Item.Bridge && x != Item.Bottle && !forcedItems.Any(y => y.Item == x)).ToList(); var treasurePool = allTreasures.ToList(); treasurePool.Remove(Item.Bridge); treasurePool.Remove(Item.Ship); foreach (var incentive in incentivePool) { treasurePool.Remove(incentive); } foreach (var placement in forcedItems) { treasurePool.Remove(placement.Item); } foreach (var questItem in unincentivizedQuestItems) { treasurePool.Remove(questItem); } while (treasurePool.Remove(Item.Shard)) { unincentivizedQuestItems.Add(Item.Shard); } do { sanityCounter++; if (sanityCounter > 10000) { throw new InvalidOperationException("Invalid flag set"); } // 1. (Re)Initialize lists inside of loop placedItems = forcedItems.ToList(); var incentives = incentivePool.ToList(); incentives.Shuffle(rng); if (flags.NPCItems) { // 2. Place caravan item first because among incentive locations it has the smallest set of possible items if (!placedItems.Any(x => x.Address == ItemLocations.CaravanItemShop1.Address)) { var itemPick = Item.Bottle; var validShopIncentives = incentives .Where(x => x > Item.None && x <= Item.Soft) .ToList(); if (validShopIncentives.Any()) { itemPick = validShopIncentives.PickRandom(rng); incentives.Remove(itemPick); } else if (placedItems.Any(x => x.Item == Item.Bottle)) { itemPick = ItemLists.AllConsumables.ToList().PickRandom(rng); } else { treasurePool.Remove(Item.Bottle); } placedItems.Add(new ItemShopSlot(caravanItemLocation, itemPick)); } // 3. Place Bridge and Ship next since the valid location lists are so small IRewardSource bridgePlacement = bridgeLocations.PickRandom(rng); placedItems.Add(NewItemPlacement(bridgePlacement, Item.Bridge)); var shipPlacement = shipLocations .Where(x => x.Address != bridgePlacement.Address) .ToList().PickRandom(rng); placedItems.Add(NewItemPlacement(shipPlacement, Item.Ship)); } // 4. Then place all incentive locations that don't have special logic foreach (var incentiveLocation in incentiveLocationPool.Where(x => !placedItems.Any(y => y.Address == x.Address))) { if (!incentives.Any()) { break; } placedItems.Add(NewItemPlacement(incentiveLocation, incentives.SpliceRandom(rng))); } // 5. Then place remanining incentive items and unincentivized quest items in any other chest before ToFR var leftoverItems = incentives.Concat(unincentivizedQuestItems).ToList(); leftoverItems.Shuffle(rng); var leftoverItemLocations = itemLocationPool .Where(x => !ItemLocations.ToFR.Any(y => y.Address == x.Address) && !x.IsUnused && !placedItems.Any(y => y.Address == x.Address)) .ToList(); foreach (var leftoverItem in leftoverItems) { placedItems.Add(NewItemPlacement(leftoverItemLocations.SpliceRandom(rng), leftoverItem)); } // 6. Check sanity and loop if needed } while (!CheckSanity(placedItems, flags, mapLocationRequirements)); // 7. Place all remaining unincentivized treasures or incentivized non-quest items that weren't placed var i = 0; itemLocationPool = itemLocationPool .Where(x => !x.IsUnused && !placedItems.Any(y => y.Address == x.Address)) .ToList(); foreach (var placedItem in placedItems) { incentivePool.Remove(placedItem.Item); } foreach (var unplacedIncentive in incentivePool) { treasurePool.Add(unplacedIncentive); } treasurePool.Shuffle(rng); foreach (var remainingTreasure in itemLocationPool) { placedItems.Add(NewItemPlacement(remainingTreasure, treasurePool[i])); i++; } //Debug.WriteLine($"Sanity Check Fails: {sanityCounter}"); return(placedItems); }
public static void RunStats(MT19337 rng, ITreasureShuffleFlags flags, IncentiveData incentivesData) { var forcedItems = ItemLocations.AllOtherItemLocations.ToList(); if (!flags.NPCItems) { forcedItems = ItemLocations.AllNonTreasureItemLocations.ToList(); } var placedItems = new List <IRewardSource>(); var treasurePool = ItemLocations.AllTreasures.Where(x => !x.IsUnused).Select(x => x.Item).ToList(); var requirementChecks = ItemLists.AllQuestItems.ToDictionary(x => x, x => 0); requirementChecks[Item.Ribbon] = 0; var requirementsToCheck = new List <Item> { Item.Crown, Item.Crystal, Item.Herb, Item.Tnt, Item.Adamant, Item.Slab, Item.Ruby, Item.Bottle, Item.Canal }; const int maxIterations = 10000; var forcedIceCount = 0; var itemPlacementStats = ItemLists.AllQuestItems.ToDictionary(x => x, x => new List <int>()); itemPlacementStats[Item.Ribbon] = new List <int>(); var itemPlacementZones = ItemLists.AllQuestItems.ToDictionary(x => x, x => new List <string>()); itemPlacementZones[Item.Ribbon] = new List <string>(); var mapLocationRequirements = ItemLocations.MapLocationRequirements.ToDictionary(x => x.Key, x => x.Value.ToList()); long iterations = 0; while (iterations < maxIterations) { iterations++; // When Enemy Status Shuffle is turned on Coneria is reduced to 11% chance with other shops spliting the remaining 89% var shopTownSelected = ItemShops.PickRandom(rng); var itemShopItem = new ItemShopSlot(ItemLocations.CaravanItemShop1.Address, $"{Enum.GetName(typeof(MapLocation), shopTownSelected)}Shop", shopTownSelected, Item.Bottle); placedItems = ItemPlacement.PlaceSaneItems(rng, flags, incentivesData, treasurePool, itemShopItem, mapLocationRequirements); var outputIndexes = placedItems.ToLookup(x => x.Item, x => x); foreach (Item item in itemPlacementStats.Keys) { itemPlacementStats[item].AddRange(outputIndexes[item].Select(x => x.Address).ToList()); } var outputZones = placedItems .ToLookup(x => x.Item, x => Enum.GetName(typeof(MapLocation), x.MapLocation)); foreach (Item item in itemPlacementZones.Keys) { if (!outputZones[item].Any()) { continue; } itemPlacementZones[item].AddRange(outputZones[item]); } var matoyaShip = placedItems.Any(x => x.Address == ItemLocations.Matoya.Address && x.Item == Item.Ship); var crystalIceCave = placedItems.Any(x => x.Item == Item.Crystal && x.Address >= ItemLocations.IceCave1.Address && x.Address <= ItemLocations.IceCaveMajor.Address); var keyIceCave = placedItems.Any(x => x.Item == Item.Key && x.Address >= ItemLocations.IceCave1.Address && x.Address <= ItemLocations.IceCaveMajor.Address); var keyLockedCrystal = placedItems.Any(x => x.Item == Item.Crystal && x.AccessRequirement.HasFlag(AccessRequirement.Key)); var keyLockedShip = placedItems.Any(x => x.Item == Item.Ship && x.AccessRequirement.HasFlag(AccessRequirement.Key)); if ((keyLockedShip && keyIceCave) || (matoyaShip && crystalIceCave) || (matoyaShip && keyLockedCrystal && keyIceCave)) { forcedIceCount++; } foreach (Item item in requirementsToCheck) { if (!ItemPlacement.CheckSanity(placedItems.Where(x => x.Item != item).ToList(), flags, mapLocationRequirements)) { requirementChecks[item]++; } } } if (iterations > 10) { Debug.WriteLine(PrintStats(maxIterations, itemPlacementStats, itemPlacementZones, requirementChecks)); Debug.WriteLine($"Forced Early Ice Cave for Ship: {forcedIceCount} out of {maxIterations}"); } }