/// <summary> /// <para>Calculates and returns which resources can meet meet the logical criteria /// for refilling by farming this farm cycle.</para> /// <para>This includes resources that only meet those criteria after redistributing /// the drop rate from other resources that were farmed to full.</para> /// </summary> /// <param name="model">A model that can be used to obtain data about the current game configuration.</param> /// <param name="costingResources">A dictionary of resources that have a cost associated with executing a cycle. /// The resource is the key and the cost per cycle is the value.</param> /// <returns></returns> private IEnumerable <ConsumableResourceEnum> ComputeFarmableResources(SuperMetroidModel model, IDictionary <ConsumableResourceEnum, int> costingResources) { ISet <ConsumableResourceEnum> farmableResources = new HashSet <ConsumableResourceEnum>(); IEnumerable <ConsumableResourceEnum> newFarmableResources; // Figure out which resources meet the threshold for farmability // We'll keep looping as long as we find farmable resources, // to discover resources that go above the threshold when redistributing drop rates do { // Determine effective drop rates for this iteration EnemyDrops effectiveDropRates = FarmCycle.RoomEnemy.Enemy.GetEffectiveDropRates(model, farmableResources); // Look for resources that meet the logical farming threshold, but didn't in the previous loops newFarmableResources = Enum.GetValues(typeof(ConsumableResourceEnum)) .Cast <ConsumableResourceEnum>() .Except(farmableResources) .Where(resource => { return(CalculateResourceVariationPerSecond(model, resource, effectiveDropRates, costingResources) >= model.LogicalOptions.SpawnerFarmingOptions.MinimumRatesPerSecond[resource]); }) // Actualize this to calculate it only once .ToArray(); farmableResources.UnionWith(newFarmableResources); } // Stop iterating if the last loop revealed no new farmable resource while (newFarmableResources.Any()); return(farmableResources); }
public bool IsResourceAvailable(SuperMetroidModel model, ConsumableResourceEnum resource, int quantity) { if (quantity == 0) { return(true); } // If resource tracking is enabled, look at current resource amounts if (model.LogicalOptions.ResourceTrackingEnabled) { // The other resources can be fully spent, but for energy we don't want to go below 1 if (resource == ConsumableResourceEnum.ENERGY) { return(GetCurrentAmount(resource) > quantity); } else { return(GetCurrentAmount(resource) >= quantity); } } // If resource tracking is not enabled, use max resource amounts instead of current amounts else { // The other resources can be fully spent, but for energy we don't want to go below 1 if (resource == ConsumableResourceEnum.ENERGY) { return(GetMaxAmount(resource) > quantity); } else { return(GetMaxAmount(resource) >= quantity); } } }
public override ExecutionResult Execute(SuperMetroidModel model, InGameState inGameState, int times = 1, bool usePreviousRoom = false) { IEnumerable <int> visitedNodeIds = inGameState.GetVisitedNodeIds(usePreviousRoom); // If the node at which we entered is not allowed, this is not fulfilled. if (!NodeIds.Contains(visitedNodeIds.First())) { return(null); } // If we have visited a node to avoid, this is not fulfilled. if (NodeIdsToAvoid.Intersect(visitedNodeIds).Any()) { return(null); } // If we were supposed to stay put but have visited more than the starting node, this is not fulfilled if (MustStayPut && visitedNodeIds.Count() > 1) { return(null); } // If we have destroyed an obstacle that needed to be preserved, this is not fulfilled if (ObstaclesIdsToAvoid.Intersect(inGameState.GetDestroyedObstacleIds(usePreviousRoom)).Any()) { return(null); } // We've avoided all pitfalls. This ResetRoom is fulfilled. Clone the InGameState to fulfill method contract return(new ExecutionResult(inGameState.Clone())); }
/// <summary> /// <para>Calculates how much of the provided resource is gained or lost (on average) by executing this farm cycle once, /// given the provided effective drop rates and resource costs per cycle</para> /// <para>If the resource is also spent while executing a cycle, the calculation includes /// reducing the drop rate by the safety margin in the logical options.</para> /// </summary> /// <param name="model">A model that can be used to obtain data about the current game configuration.</param> /// <param name="resource">The resource to check</param> /// <param name="effectiveDropRates">The effective drop rates, after accounting for unneeded drops.</param> /// <param name="costingResources">A dictionary of resources to their cost per cycle.</param> /// <returns></returns> private decimal CalculateResourceVariationPerCycle(SuperMetroidModel model, ConsumableResourceEnum resource, EnemyDrops effectiveDropRates, IDictionary <ConsumableResourceEnum, int> costingResources) { // If this farm cycle has to spend some of this resource, do some adaptations: // - Reduce the expected drop rate by the logical safety margin // - Include the cost when looking at the amount obtained from drops decimal dropRateMultiplier; int costPerCycle; if (costingResources.TryGetValue(resource, out costPerCycle)) { dropRateMultiplier = (100 - model.LogicalOptions.SpawnerFarmingOptions.SafetyMarginPercent) / 100; } else { costPerCycle = 0; dropRateMultiplier = 1M; } decimal netGainPerCycle = resource.GetRelatedDrops() .Select(drop => dropRateMultiplier * model.Rules.ConvertDropRateToPercent(effectiveDropRates.GetDropRate(drop)) * FarmCycle.RoomEnemy.Quantity * model.Rules.GetDropResourceCount(drop)) .Sum() - costPerCycle; return(netGainPerCycle); }
public void Initialize(SuperMetroidModel model) { SuperMetroidModel = model; List <Action> postInitializationCallbacks = new List <Action>(); foreach (RoomNode node in Nodes.Values) { postInitializationCallbacks.AddRange(node.Initialize(model, this)); } foreach (RoomObstacle obstacle in Obstacles.Values) { postInitializationCallbacks.AddRange(obstacle.Initialize(model, this)); } foreach (Link link in Links) { postInitializationCallbacks.AddRange(link.Initialize(model, this)); } foreach (RoomEnemy enemy in EnemiesSequence) { postInitializationCallbacks.AddRange(enemy.Initialize(model, this)); } // If we received any callbacks to execute after the room is done, execute them now postInitializationCallbacks.ForEach(action => action.Invoke()); }
public IEnumerable <string> InitializeReferencedLogicalElementProperties(SuperMetroidModel model) { List <string> unhandled = new List <string>(); foreach (RoomNode node in Nodes.Values) { unhandled.AddRange(node.InitializeReferencedLogicalElementProperties(model, this)); } foreach (Link link in Links) { unhandled.AddRange(link.InitializeReferencedLogicalElementProperties(model, this)); } foreach (RoomEnemy enemy in EnemiesSequence) { unhandled.AddRange(enemy.InitializeReferencedLogicalElementProperties(model, this)); } foreach (RoomObstacle obstacle in Obstacles.Values) { unhandled.AddRange(obstacle.InitializeReferencedLogicalElementProperties(model, this)); } return(unhandled.Distinct()); }
public override int CalculateDamage(SuperMetroidModel model, InGameState inGameState, int times = 1, bool usePreviousRoom = false) { int currentRegularEnergy = inGameState.GetCurrentAmount(Items.RechargeableResourceEnum.RegularEnergy); // Don't take damage if we've already reached the threshold return(Math.Max(0, currentRegularEnergy - Value)); }
public ExecutionResult Execute(SuperMetroidModel model, InGameState inGameState, int times = 1, bool usePreviousRoom = false) { bool enemyInitiallyFull = EnemyWithHealth.Health == EnemyWithHealth.Enemy.Hp; EnemyWithHealth.Enemy.WeaponSusceptibilities.TryGetValue(Weapon.Name, out WeaponSusceptibility susceptibility); if (susceptibility == null) { return(null); } int numberOfShots = susceptibility.NumberOfHits(EnemyWithHealth.Health); ExecutionResult result = Weapon.ShotRequires.Execute(model, inGameState, times: times * numberOfShots, usePreviousRoom: usePreviousRoom); if (result != null) { // Record the kill // If the enemy was full, then the prior splash attack (if any) didn't affect it. Ignore it. if (enemyInitiallyFull) { result.AddKilledEnemy(EnemyWithHealth.Enemy, Weapon, numberOfShots); } // If the enemy was not full, the splash attack contributed to its death else { result.AddKilledEnemy(EnemyWithHealth.Enemy, new [] { (PriorSplashWeapon, PriorSplashShots), (Weapon, numberOfShots) });
/// <summary> /// <para>Calculates how much of the provided resource is gained or lost (on average) over 60 frames of farming, /// given the provided effective drop rates and resource costs per cycle</para> /// <para>If the resource is also spent while executing a cycle, the calculation includes /// reducing the drop rate by the safety margin in the logical options.</para> /// </summary> /// <param name="model">A model that can be used to obtain data about the current game configuration.</param> /// <param name="resource">The resource to check</param> /// <param name="effectiveDropRates">The effective drop rates, after accounting for unneeded drops.</param> /// <param name="costingResources">A dictionary of resources to their cost per cycle.</param> /// <returns></returns> private decimal CalculateResourceVariationPerSecond(SuperMetroidModel model, ConsumableResourceEnum resource, EnemyDrops effectiveDropRates, IDictionary <ConsumableResourceEnum, int> costingResources) { decimal variationPerCycle = CalculateResourceVariationPerCycle(model, resource, effectiveDropRates, costingResources); return(variationPerCycle / FarmCycle.CycleFrames * 60); }
// STITCHME It might be valuable to eventually have InGameState be able to say which nodes are reachable? // STITCHME It could be nice to keep track of all canResets in the room and evaluate them as you move around? // Another option would be to have something in an initialization phase that converts canResets into just names, // and adds information on nodes and strats that they invalidate the canReset. // We'll see when we get to the step of reducing logical elements *shrug* /// <summary> /// Creates a new InGameState /// </summary> /// <param name="model">A SuperMetroidModel. Its rooms must have both been set and initialized. /// Its items and game flags must also have been set.</param> /// <param name="itemContainer">The result of reading the items.json file.</param> public InGameState(SuperMetroidModel model, ItemContainer itemContainer) { IEnumerable <ResourceCapacity> startingResources = itemContainer.StartingResources; Inventory = new ItemInventory(startingResources); Resources = new ResourceCount(); // Start the player at full foreach (ResourceCapacity capacity in startingResources) { Resources.ApplyAmountIncrease(capacity.Resource, capacity.MaxAmount); } // Initialize starting game flags foreach (string gameFlagName in itemContainer.StartingGameFlagNames) { ApplyAddGameFlag(model.GameFlags[gameFlagName]); } // Initialize starting items foreach (string itemName in itemContainer.StartingItemNames) { ApplyAddItem(model.Items[itemName]); } RoomNode startingNode = model.Rooms[itemContainer.StartingRoomName].Nodes[itemContainer.StartingNodeId]; InRoomState = new InRoomState(startingNode); }
public ExecutionResult Execute(SuperMetroidModel model, InGameState inGameState, int times = 1, bool usePreviousRoom = false) { // There may be up to 2 requirements. This StratObstacle may have some, and the RoomObstacle may also have some general requirements that apply to any strat. // Start with the RoomObstacle's requirements ExecutionResult result = StratObstacle.Obstacle.Requires.Execute(model, inGameState, times: times, usePreviousRoom: usePreviousRoom); // If we couldn't execute the RoomObstacle's requirements, give up if (result == null) { return(null); } // Add this specific StratObstacle's requirements result = result.AndThen(StratObstacle.Requires, model, times: times, usePreviousRoom: usePreviousRoom); // If that failed, give up if (result == null) { return(null); } // We have succeeded, but we must update the ExecutionResult and its InGameState to reflect any destroyed obstacles result.ApplyDestroyedObstacles(new[] { StratObstacle.Obstacle }.Concat(StratObstacle.AdditionalObstacles), usePreviousRoom); return(result); }
/// <summary> /// Consumes the provided quantity of the provided consumable resource. When consuming energy, regular energy is used up first (down to 1) /// then reserves are used. /// </summary> /// <param name="model">A model that can be used to obtain data about the current game configuration.</param> /// <param name="resource">The resource to consume</param> /// <param name="quantity">The amount to consume</param> public void ApplyConsumeResource(SuperMetroidModel model, ConsumableResourceEnum resource, int quantity) { // Don't bother with current resource count if resource tracking is disabled if (model.LogicalOptions.ResourceTrackingEnabled) { Resources.ApplyAmountReduction(resource, quantity); } }
public IEnumerable <string> InitializeReferencedLogicalElementProperties(SuperMetroidModel model, Room room) { List <string> unhandled = new List <string>(); unhandled.AddRange(Requires.InitializeReferencedLogicalElementProperties(model, room)); return(unhandled.Distinct()); }
public override AbstractNavigationAction Reverse(SuperMetroidModel model) { InteractWithNodeAction reverseAction = new InteractWithNodeAction($"Undo action '{this.IntentDescription}'"); TransferDataToReverseAbstractAction(reverseAction); return(reverseAction); }
/// <summary> /// Sets current value for the provided resource to the current maximum /// </summary> /// <param name="model">A model that can be used to obtain data about the current game configuration.</param> /// <param name="resource">The resource to refill</param> public void ApplyRefillResource(SuperMetroidModel model, RechargeableResourceEnum resource) { // Don't bother with current resource count if resource tracking is disabled if (model.LogicalOptions.ResourceTrackingEnabled) { Resources.ApplyAmount(resource, Inventory.GetMaxAmount(resource)); } }
public override AbstractNavigationAction Reverse(SuperMetroidModel model) { MoveToNodeAction reverseAction = new MoveToNodeAction($"Undo action '{this.IntentDescription}'"); TransferDataToReverseAbstractAction(reverseAction); reverseAction.StratUsed = StratUsed; return(reverseAction); }
public IEnumerable <Action> Initialize(SuperMetroidModel model, Room room) { // Initialize LeadsToNode if (LeadsToNodeId != null) { LeadsToNode = room.Nodes[(int)LeadsToNodeId]; } return(Enumerable.Empty <Action>()); }
public IEnumerable <Action> Initialize(SuperMetroidModel model, Room room) { // Initialize Obstacle Obstacle = room.Obstacles[ObstacleId]; // Initialize AdditionalObstacles AdditionalObstacles = AdditionalObstacleIds.Select(id => room.Obstacles[id]); return(Enumerable.Empty <Action>()); }
/// <summary> /// Creates and returns an ExecutionResult based on the provided in-game state, with the provided resources refilled. /// </summary> /// <param name="model">A model that can be used to obtain data about the current game configuration.</param> /// <param name="inGameState">The in-game state to use for execution. This will NOT be altered by this method.</param> /// <param name="resourcesToRefill">The resources that should be refilled.</param> /// <returns></returns> private ExecutionResult ExecuteRefill(SuperMetroidModel model, InGameState inGameState, IEnumerable <ConsumableResourceEnum> resourcesToRefill) { InGameState resultingState = inGameState.Clone(); foreach (ConsumableResourceEnum resource in resourcesToRefill) { resultingState.ApplyRefillResource(model, resource); } return(new ExecutionResult(resultingState)); }
public AbstractNavigationAction(string intent, SuperMetroidModel model, InGameState initialInGameState, ExecutionResult executionResult) : this(intent) { Succeeded = true; // Initialize position change if (initialInGameState.GetCurrentNode() != executionResult.ResultingState.GetCurrentNode()) { PositionChange = (initialInGameState.GetCurrentNode(), executionResult.ResultingState.GetCurrentNode()); } // Initialize gained and lost items ItemInventory gainedInventory = executionResult.ResultingState.GetInventoryExceptWith(initialInGameState); ItemsGained = gainedInventory; // Cannot lose items, just create an empty inventory ItemsLost = new ItemInventory(model.StartConditions.BaseResourceMaximums); // Initialize enabled and disabled items ItemsDisabledNames = executionResult.ResultingState.GetDisabledItemNames().Except(initialInGameState.GetDisabledItemNames()); ItemsEnabledNames = initialInGameState.GetDisabledItemNames().Except(executionResult.ResultingState.GetDisabledItemNames()); // Initialize flags gained GameFlagsGained = GameFlagsGained.Concat(executionResult.ResultingState.GetActiveGameFlagsExceptWith(initialInGameState).Values); // Initialize locks opened LocksOpened = LocksOpened.Concat(executionResult.ResultingState.GetOpenedNodeLocksExceptWith(initialInGameState).Values); // Initialize item locations taken ItemLocationsTaken = ItemLocationsTaken.Concat(executionResult.ResultingState.GetTakenItemLocationsExceptWith(initialInGameState).Values); // Initialize resources before and after ResourcesBefore = initialInGameState.GetCurrentResources(); ResourcesAfter = executionResult.ResultingState.GetCurrentResources(); // Initialize destroyed obstacles, but that's only relevant if we didn't change rooms if (executionResult.ResultingState.GetCurrentRoom() == initialInGameState.GetCurrentRoom()) { ObstaclesDestroyed = ObstaclesDestroyed.Concat( executionResult.ResultingState.GetDestroyedObstacleIds() .Except(initialInGameState.GetDestroyedObstacleIds()) .Select(obstacleId => executionResult.ResultingState.GetCurrentRoom().Obstacles[obstacleId]) ); } // Transfer information data from the ExecutionResult. // No need to copy since they are IEnumerable and not supposed to be mutated. RunwaysUsed = executionResult.RunwaysUsed; CanLeaveChargedExecuted = executionResult.CanLeaveChargedExecuted; OpenedLocks = executionResult.OpenedLocks; BypassedLocks = executionResult.BypassedLocks; KilledEnemies = executionResult.KilledEnemies; // Since the set of items is mutable, do not transfer the instance ItemsInvolved.UnionWith(executionResult.ItemsInvolved); DamageReducingItemsInvolved.UnionWith(executionResult.DamageReducingItemsInvolved); }
/// <summary> /// Sets current value for the provided consumable resource to the current maximum. /// This is almost the same as refilling a rechargeable resource, /// except both types of energy are grouped together. /// </summary> /// <param name="model">A model that can be used to obtain data about the current game configuration.</param> /// <param name="resource">The resource to refill</param> public void ApplyRefillResource(SuperMetroidModel model, ConsumableResourceEnum resource) { // Don't bother with current resource count if resource tracking is disabled if (model.LogicalOptions.ResourceTrackingEnabled) { foreach (RechargeableResourceEnum rechargeableResource in resource.ToRechargeableResources()) { ApplyRefillResource(model, rechargeableResource); } } }
/// <summary> /// Goes through all logical elements within this LogicalRequirements (and all LogicalRequirements within any of them), /// attempting to initialize any property that is an object referenced by another property(which is its identifier). /// </summary> /// <param name="model">A SuperMetroidModel that contains global data</param> /// <param name="room">The room in which this LogicalRequirements is, or null if it's not in a room</param> /// <returns>A sequence of strings describing references that could not be initialized properly.</returns> public IEnumerable <string> InitializeReferencedLogicalElementProperties(SuperMetroidModel model, Room room) { List <string> unhandled = new List <string>(); foreach (AbstractLogicalElement logicalElement in LogicalElements) { unhandled.AddRange(logicalElement.InitializeReferencedLogicalElementProperties(model, room)); } return(unhandled); }
public IEnumerable <string> InitializeReferencedLogicalElementProperties(SuperMetroidModel model, Room room, RoomNode node) { List <string> unhandled = new List <string>(); foreach (Strat strat in Strats) { unhandled.AddRange(strat.InitializeReferencedLogicalElementProperties(model, room)); } return(unhandled.Distinct()); }
// Ok so how do I want this to work? We'll have operations that will have results. // What are things you can do: // - Follow a link // - Interact with current node // - Remember that interacting with a door means you change rooms // - Needs some kind of support for a remote exit. // - In the case of door locks, you could absolutely attempt to open the lock without interacting with it // - Farm enemies // - Optionally you could provide a door to use to reset the room and keep farming (mandatory if the enemies don't respawn) // - Optionally you could indicate specific enemies you want to farm // - Optionally you could indicate a weapon to use (only worth doing if no effective free weapon available) // Anything else? // - There could be some more complex operations like "go to THIS node", but then that can lead to evaluating a lot of things to find the cheapest option. // Still, I'm sure it could be worth. // // Failure to do an action would leave the state unchanged and return something to indicate failure. // // Some kind of system to customize/override the items at in-game locations could be good, to navigate an actual randomized seed and grab items. // Though this one might be out of scope for GameNavigator. It should probably happen upstream. /// <summary> /// Constructor that initializes a GameNavigator with the provided initial state. /// </summary> /// <param name="model">A model that can be used to obtain data about the current game configuration.</param> /// <param name="initialState">The starting inGameState for this navigator.</param> /// <param name="maxPreviousStatesSize">The maximum number of previous states that this navigator should keep in memory.</param> /// <param name="options">Optional game navigation options. If left null, default options will be used.</param> public GameNavigator(SuperMetroidModel model, InGameState initialState, int maxPreviousStatesSize, GameNavigatorOptions options = null) { GameModel = model; CurrentInGameState = initialState; MaxPreviousStatesSize = maxPreviousStatesSize; if (options == null) { options = new GameNavigatorOptions(); } Options = options; }
public IEnumerable <Action> Initialize(SuperMetroidModel model, Room room) { List <Action> postRoomInitializeCallbacks = new List <Action>(); foreach (LinkTo linkTo in To) { postRoomInitializeCallbacks.AddRange(linkTo.Initialize(model, room)); } return(postRoomInitializeCallbacks); }
/// <summary> /// Creates and returns an instance of EnemyDrops reprenting this enemy's effective drop rates, /// given that the provided resources are full. /// </summary> /// <param name="model">A model that can be used to obtain data about the current game configuration.</param> /// <param name="fullResources">An enumeration of consumable resources that are considered full /// (and hence no longer cause their corresponding enemy drop to happen).</param> /// <returns></returns> public EnemyDrops GetEffectiveDropRates(SuperMetroidModel model, IEnumerable <ConsumableResourceEnum> fullResources) { if (fullResources.Any()) { return(model.Rules.CalculateEffectiveDropRates(Drops, model.Rules.GetUnneededDrops(fullResources))); } else { return(Drops.Clone()); } }
public IEnumerable <string> InitializeReferencedLogicalElementProperties(SuperMetroidModel model, Room room) { List <string> unhandled = new List <string>(); foreach (LinkTo linkTo in To) { unhandled.AddRange(linkTo.InitializeReferencedLogicalElementProperties(model, room)); } return(unhandled.Distinct()); }
public ExecutionResult Execute(SuperMetroidModel model, InGameState inGameState, int times = 1, bool usePreviousRoom = false) { // The bypass attempt fails if there's no way to bypass if (StratObstacle.Bypass == null) { return(null); } else { return(StratObstacle.Bypass.Execute(model, inGameState, times: times, usePreviousRoom: usePreviousRoom)); } }
public void Initialize(SuperMetroidModel model, Room room, RoomNode node) { // Eliminate disabled strats Strats = Strats.WhereEnabled(model); foreach (Strat strat in Strats) { strat.Initialize(model, room); } Node = node; }
public override ExecutionResult Execute(SuperMetroidModel model, InGameState inGameState, int times = 1, bool usePreviousRoom = false) { if (inGameState.HasGameFlag(GameFlag)) { // Clone the In-game state to fulfill method contract return(new ExecutionResult(inGameState.Clone())); } else { return(null); } }