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) });
public ExecutionResult Execute(SuperMetroidModel model, InGameState inGameState, int times = 1, bool usePreviousRoom = false) { ExecutionResult result = null; // We'll need to track the health of individual enemies, so create an EnemyWithHealth for each IEnumerable <EnemyWithHealth> enemiesWithHealth = EnemyGroup.Select(e => new EnemyWithHealth(e)); // If using a splash weapon, spend the ammo then apply the damage if (SplashWeapon != null && SplashWeapon.HitsGroup) { result = SplashWeapon.ShotRequires.Execute(model, inGameState, times: times * SplashShots, usePreviousRoom: usePreviousRoom); // If we can't spend the ammo, fail immediately if (result == null) { return(null); } // Apply the splash attack to each enemy enemiesWithHealth = enemiesWithHealth .Select(e => { e.Enemy.WeaponSusceptibilities.TryGetValue(SplashWeapon.Name, out WeaponSusceptibility susceptibility); // If the splash weapon hurts the enemy, apply damage if (susceptibility != null) { int damage = susceptibility.DamagePerShot * SplashShots; e.Health -= damage; } // Return the new state of the enemy. if (e.IsAlive()) { return(e); } // If the enemy is dead, record the kill and return null else { // No matter how many shots we dealt to the group, record how many shots it took to actually kill this enemy. result.AddKilledEnemy(e.Enemy, SplashWeapon, susceptibility.Shots); return(null); } }) // Remove dead enemies .Where(e => e != null); } // No else: If no splashWeapon is provided (or it's somehow not a splash weapon), skip that step and only do the individual kill // Iterate over each remaining enemy, killing it with the cheapest non-splash weapon foreach (EnemyWithHealth currentEnemy in enemiesWithHealth) { var(_, killResult) = model.ExecuteBest(NonSplashWeapons.Select(weapon => currentEnemy.ToExecutable(weapon, SplashWeapon, SplashShots)), result?.ResultingState ?? inGameState, times: times, usePreviousRoom: usePreviousRoom); // If we can't kill one of the enemies, give up if (killResult == null) { return(null); } // Update the sequential ExecutionResult result = result == null ? killResult : result.ApplySubsequentResult(killResult); } return(result); }
public override ExecutionResult Execute(SuperMetroidModel model, InGameState inGameState, int times = 1, bool usePreviousRoom = false) { // Create an ExecutionResult immediately so we can record free kills in it ExecutionResult result = new ExecutionResult(inGameState.Clone()); // Filter the list of valid weapons, to keep only those we can actually use right now IEnumerable <Weapon> usableWeapons = ValidWeapons.Where(w => w.UseRequires.Execute(model, inGameState, times: times, usePreviousRoom: usePreviousRoom) != null); // Find all usable weapons that are free to use. That's all weapons without an ammo cost, plus all weapons whose ammo is farmable in this EnemyKill // Technically if a weapon were to exist with a shot cost that requires something other than ammo (something like energy or ammo drain?), // this wouldn't work. Should that be a worry? IEnumerable <Weapon> freeWeapons = usableWeapons.Where(w => !w.ShotRequires.LogicalElements.Where(le => le is Ammo ammo && !FarmableAmmo.Contains(ammo.AmmoType)).Any()); // Remove all enemies that can be killed by free weapons IEnumerable <IEnumerable <Enemy> > nonFreeGroups = GroupedEnemies .RemoveEnemies(e => { // Look for a free usable weapon this enemy is susceptible to. var firstWeaponSusceptibility = e.WeaponSusceptibilities.Values .Where(ws => freeWeapons.Contains(ws.Weapon, ObjectReferenceEqualityComparer <Weapon> .Default)) .FirstOrDefault(); // If we found a weapon, record a kill and return true (to remove the enemy) if (firstWeaponSusceptibility != null) { result.AddKilledEnemy(e, firstWeaponSusceptibility.Weapon, firstWeaponSusceptibility.Shots); return(true); } // If we didn't find a weapon, return false (to retain the enemy) else { return(false); } }); // If there are no enemies left, we are done! if (!nonFreeGroups.Any()) { return(result); } // The remaining enemies require ammo IEnumerable <Weapon> nonFreeWeapons = usableWeapons.Except(freeWeapons, ObjectReferenceEqualityComparer <Weapon> .Default); IEnumerable <Weapon> nonFreeSplashWeapons = nonFreeWeapons.Where(w => w.HitsGroup); IEnumerable <Weapon> nonFreeIndividualWeapons = nonFreeWeapons.Where(w => !w.HitsGroup); // Iterate over each group, killing it and updating the resulting state. // We'll test many scenarios, each with 0 to 1 splash weapon and a fixed number of splash weapon shots (after which enemies are killed with single-target weapons). // We will not test multiple combinations of splash weapons. foreach (IEnumerable <Enemy> currentEnemyGroup in nonFreeGroups) { // Build a list of combinations of splash weapons and splash shots (including one entry for no splash weapon at all) IEnumerable <(Weapon splashWeapon, int splashShots)> splashCombinations = nonFreeSplashWeapons.SelectMany(w => // Figure out what different shot counts for this weapon will lead to different numbers of casualties currentEnemyGroup .Select(e => e.WeaponSusceptibilities.TryGetValue(w.Name, out WeaponSusceptibility susceptibility) ? susceptibility.Shots : 0) .Where(shots => shots > 0) // Convert each different number of shot into a combination of this weapon and the number of shots .Select(shots => (splashWeapon: w, splashShots: shots)) ) // Add the one entry for not using a splash weapon at all .Append((splashWeapon: null, splashShots: 0)); // Evaluate all combinations and apply the cheapest to our current resulting state (_, ExecutionResult killResult) = model.ExecuteBest(splashCombinations.Select(combination => new EnemyGroupAmmoExecutable(currentEnemyGroup, nonFreeIndividualWeapons, combination.splashWeapon, combination.splashShots)), result.ResultingState, times: times, usePreviousRoom: usePreviousRoom); // If we failed to kill an enemy group, we can't kill all enemies if (killResult == null) { return(null); } // Update the sequential ExecutionResult result = result.ApplySubsequentResult(killResult); } return(result); }