public override ExecutionResult Execute(SuperMetroidModel model, InGameState inGameState, int times = 1, bool usePreviousRoom = false) { // If no in-room path is specified, then player is expected to have entered at fromNode and not moved IEnumerable <int> requiredInRoomPath = (InRoomPath == null || !InRoomPath.Any()) ? new[] { FromNodeId } : InRoomPath; // Find all runways from the previous room that can be retroactively attempted and are long enough. // We're calculating runway length to account for open ends, but using 0 for tilesSavedWithStutter because no charging is involved. IEnumerable <Runway> retroactiveRunways = inGameState.GetRetroactiveRunways(requiredInRoomPath, usePreviousRoom) .Where(r => model.Rules.CalculateEffectiveRunwayLength(r, tilesSavedWithStutter: 0) >= UsedTiles); (_, var executionResult) = model.ExecuteBest(retroactiveRunways.Select(runway => runway.AsExecutable(comingIn: false)), inGameState, times: times, usePreviousRoom: usePreviousRoom); return(executionResult); // Note that there are no concerns here about unlocking the previous door, because unlocking a door to use it cannot be done retroactively. // It has to have already been done in order to use the door in the first place. }
public override ExecutionResult Execute(SuperMetroidModel model, InGameState inGameState, int times = 1, bool usePreviousRoom = false) { var mustShinespark = ShinesparkFrames > 0; var energyNeededForShinespark = model.Rules.CalculateEnergyNeededForShinespark(ShinesparkFrames, times: times); var shinesparkEnergyCost = model.Rules.CalculateShinesparkDamage(inGameState, ShinesparkFrames, times: times); // Not calling IsResourceAvailable() because Samus only needs to have that much energy, not necessarily spend all of it Predicate <InGameState> hasEnergyForShinespark = state => state.GetCurrentResources().GetAmount(ConsumableResourceEnum.ENERGY) >= energyNeededForShinespark; Action <ExecutionResult> consumeShinesparkEnergy = result => result.ResultingState.ApplyConsumeResource(model, ConsumableResourceEnum.ENERGY, shinesparkEnergyCost); // Check simple preconditions before looking at anything if (!inGameState.HasSpeedBooster() || (mustShinespark && !model.CanShinespark())) { return(null); } ExecutionResult bestOverallResult = null; // So we have to see if we can use an in-room runway (not coming in). // And we have to see if we can use an adjacent runway by itself. // And we have to see if we can use a canLeavecharged. // And we have to see if we can combine an adjacent runway with a an in-room runway (coming in). // And after that we have to still be able to execute the shinespark. // We'll start with looking at in-room runways (not coming in). // For all in-room runways we are able to use while still doing the shinespark after, // figure out the resulting state, effective length, and the overall best resulting state var(usableInRoomRunwayEvaluations, bestInRoomResult) = EvaluateRunways(model, inGameState, FromNode.Runways, times, usePreviousRoom, hasEnergyForShinespark, runwaysReversible: true); // If using this in-room runway cost nothing, spend the shinespark and return if (model.CompareInGameStates(inGameState, bestInRoomResult?.ResultingState) == 0) { consumeShinesparkEnergy(bestInRoomResult); bestInRoomResult.AddItemsInvolved(new Item[] { model.Items[SuperMetroidModel.SPEED_BOOSTER_NAME] }); return(bestInRoomResult); } // If the best in-room runway we found is an improvement over the previous best solution, replace it if (model.CompareInGameStates(bestInRoomResult?.ResultingState, bestOverallResult?.ResultingState) > 0) { bestOverallResult = bestInRoomResult; } // Next Step: all adjacent runways with their resulting state. // If no in-room path is specified, then player is expected to have entered at fromNode and not moved var requiredInRoomPath = (InRoomPath == null || !InRoomPath.Any()) ? new[] { FromNodeId } : InRoomPath; // For all adjacent runways that can be used retroactively while still doing the shinespark after, // figure out the resulting state, effective length, and the overall best resulting state var(usableAdjacentRunwayEvaluations, bestAdjacentRunwayResult) = EvaluateRunways(model, inGameState, inGameState.GetRetroactiveRunways(requiredInRoomPath, usePreviousRoom: true), times, usePreviousRoom, hasEnergyForShinespark, runwaysReversible: false); // If using this adjacent runway cost nothing, spend the shinespark and return if (model.CompareInGameStates(inGameState, bestAdjacentRunwayResult?.ResultingState) == 0) { consumeShinesparkEnergy(bestAdjacentRunwayResult); bestAdjacentRunwayResult.AddItemsInvolved(new Item[] { model.Items[SuperMetroidModel.SPEED_BOOSTER_NAME] }); return(bestAdjacentRunwayResult); } // If the best adjacent runway we found is an improvement over the previous best solution, replace it if (model.CompareInGameStates(bestAdjacentRunwayResult?.ResultingState, bestOverallResult?.ResultingState) > 0) { bestOverallResult = bestAdjacentRunwayResult; } // Next step: Find the best retroactive CanLeaveCharged that has enough frames // remaining and leaves Samus with enough energy for the shinespark var usableCanLeaveChargeds = inGameState.GetRetroactiveCanLeaveChargeds(model, requiredInRoomPath, usePreviousRoom: usePreviousRoom) .Where(clc => clc.FramesRemaining >= FramesRemaining); (_, ExecutionResult bestLeaveChargedResult) = model.ExecuteBest(usableCanLeaveChargeds, inGameState, times: times, usePreviousRoom: usePreviousRoom, hasEnergyForShinespark); // If using this CanLeaveCharged cost nothing, spend the shinespark and return if (model.CompareInGameStates(inGameState, bestLeaveChargedResult?.ResultingState) == 0) { consumeShinesparkEnergy(bestLeaveChargedResult); bestLeaveChargedResult.AddItemsInvolved(new Item[] { model.Items[SuperMetroidModel.SPEED_BOOSTER_NAME] }); return(bestLeaveChargedResult); } // If the best CanLeaveCharged we found is an improvement over the previous best solution, replace it if (model.CompareInGameStates(bestLeaveChargedResult?.ResultingState, bestOverallResult?.ResultingState) > 0) { bestOverallResult = bestLeaveChargedResult; } // Next step: Find the best combination of adjacent and in-room runway // We can re-use the results from evaluating the adjacent runways that we calculated, but not // the in-room ones (because executions, not results, must be applied on top of each other). // Iterate over usable adjacent runways that actually offer a gain over the number of // tiles lost by the room transation, and match each of those against each in-room // runway that is usable coming in and combines for a long enough runway foreach (var(_, currentAdjacentRunwayResult, currentLength) in usableAdjacentRunwayEvaluations.Where(runway => runway.length > model.Rules.RoomTransitionTilesLost)) { var requiredInRoomLength = model.LogicalOptions.TilesToShineCharge + model.Rules.RoomTransitionTilesLost - currentLength; // Determine which runways we may attempt to use. Limit to the ones we evaluated // earlier because there's no point re-evaluating those we couldn't execute then, // but we'll re-attempt to use the runways using the resulting state of the current // adjacent runway. // We'll also ignore in-room runways that are not long enough to combine with our // current adjacent runway. var adequateRunways = from r in usableInRoomRunwayEvaluations where r.length >= requiredInRoomLength select r.runway; var(_, bestCombinationResult) = model.ExecuteBest(adequateRunways.Select(runway => runway.AsExecutable(comingIn: true)), currentAdjacentRunwayResult.ResultingState, times: times, usePreviousRoom: usePreviousRoom, hasEnergyForShinespark); // If the best combination we found is free, spend energy for the shinespark and return it. // Make sure to apply the in-room runway result on top of the adjacent runway result. if (model.CompareInGameStates(inGameState, bestCombinationResult?.ResultingState) == 0) { consumeShinesparkEnergy(bestCombinationResult); bestCombinationResult.AddItemsInvolved(new Item[] { model.Items[SuperMetroidModel.SPEED_BOOSTER_NAME] }); return(currentAdjacentRunwayResult.Clone().ApplySubsequentResult(bestCombinationResult)); } // If the best combination we found is free, spend energy for the shinespark and return it. // Make sure to apply the in-room runway result on top of the adjacent runway result. if (model.CompareInGameStates(bestCombinationResult?.ResultingState, bestOverallResult?.ResultingState) > 0) { consumeShinesparkEnergy(bestCombinationResult); bestOverallResult = currentAdjacentRunwayResult.Clone().ApplySubsequentResult(bestCombinationResult); } } // If we have found no solution at all that we can execute and that leaves us with // enough energy for the shinespark, we cannot do this if (bestOverallResult == null) { return(null); } // Apply shinespark on the best solution we've found and return it else { consumeShinesparkEnergy(bestOverallResult); bestOverallResult.AddItemsInvolved(new Item[] { model.Items[SuperMetroidModel.SPEED_BOOSTER_NAME] }); return(bestOverallResult); } }