Ejemplo n.º 1
0
        public async Task <IActionResult> GetBacktimePlan()
        {
            var serverTime = CurrentServerTime;

            var invalidTribeIds = await(
                from tribe in CurrentSets.Ally
                join player in CurrentSets.Player on tribe.TribeId equals player.TribeId
                join user in CurrentSets.User on player.PlayerId equals user.PlayerId
                where user.Enabled
                select tribe.TribeId
                ).Distinct().ToListAsync();

            var invalidPlayerIds = await(
                from player in CurrentSets.Player
                join user in CurrentSets.User on player.PlayerId equals user.PlayerId into user
                where user.Any(u => u.Enabled) || (player.TribeId != null && invalidTribeIds.Contains(player.TribeId.Value))
                select player.PlayerId
                ).ToListAsync();

            var invalidVillageIds = await(
                from village in CurrentSets.Village
                where village.PlayerId != null && invalidPlayerIds.Contains(village.PlayerId.Value)
                select village.VillageId
                ).ToListAsync();

            var ownVillages = await(
                from village in CurrentSets.Village
                join currentVillage in CurrentSets.CurrentVillage.Include(cv => cv.ArmyAtHome) on village.VillageId equals currentVillage.VillageId
                where currentVillage.ArmyAtHome != null
                where village.PlayerId == CurrentPlayerId
                select new { village, currentVillage }
                ).ToDictionaryAsync(d => d.village, d => d.currentVillage);

            var possibleCommands = await(
                from command in CurrentSets.Command
                .Include(c => c.Army)
                .Include(c => c.SourceVillage)
                where command.ArmyId != null
                where command.ReturnsAt > serverTime
                where !invalidVillageIds.Contains(command.SourceVillageId)
                select command
                ).ToListAsync();

            var offensiveTypes  = new[] { JSON.TroopType.Axe, JSON.TroopType.Light, JSON.TroopType.Heavy, JSON.TroopType.Ram, JSON.TroopType.Catapult };
            var allInstructions = new ConcurrentDictionary <Scaffold.Command, List <Planning.CommandInstruction> >();

            var ownVillagesById = ownVillages.Keys.ToDictionary(v => v.VillageId, v => v);

            bool MeetsMinimumPopulation(Scaffold.Command command)
            {
                var army = (JSON.Army)command.Army;

                return(army != null && 2000 < Model.Native.ArmyStats.CalculateTotalPopulation(army, offensiveTypes));
            }

            var targetPlayerIdsTmp = new ConcurrentDictionary <long, byte>();

            //  Generate backtime plans
            try
            {
                var commands        = possibleCommands.Where(MeetsMinimumPopulation);
                var parallelOptions = new ParallelOptions {
                    MaxDegreeOfParallelism = Environment.ProcessorCount
                };
                var planningTask = Parallel.ForEach(commands, parallelOptions, (command) =>
                {
                    var planner = new Planning.CommandOptionsCalculator(CurrentWorldSettings);
                    planner.Requirements.Add(new Planning.Requirements.MaximumTravelTimeRequirement
                    {
                        MaximumTime = command.ReturnsAt.Value - serverTime
                    });

                    planner.Requirements.Add(Planning.Requirements.MinimumOffenseRequirement.FractionalNuke(0.05f).LimitTroopType(offensiveTypes));
                    planner.Requirements.Add(Planning.Requirements.WithoutTroopTypeRequirement.WithoutNobles);

                    var plan = planner.GenerateOptions(ownVillages, command.SourceVillage);
                    if (plan.Count > 0)
                    {
                        targetPlayerIdsTmp.TryAdd(command.SourcePlayerId, 0);
                        allInstructions.TryAdd(command, plan);
                    }
                });
            }
            catch (AggregateException e)
            {
                throw e.InnerException ?? e.InnerExceptions.First();
            }

            //  Find existing backtimes for commands that had plans generated
            var backtimedVillageIds         = allInstructions.Keys.Select(c => c.SourceVillageId).ToList();
            var commandsToBacktimedVillages = await(
                from command in CurrentSets.Command
                where backtimedVillageIds.Contains(command.TargetVillageId)
                where command.Army != null
                where command.LandsAt > serverTime
                select new { command.TargetVillageId, command.LandsAt, command.Army }
                ).ToListAsync();

            var troopsAtBacktimedVillages = await(
                from village in CurrentSets.CurrentVillage
                where backtimedVillageIds.Contains(village.VillageId)
                select new { village.VillageId, village.ArmyStationed }
                ).ToDictionaryAsync(v => v.VillageId, v => v.ArmyStationed);

            var battleSimulator             = new Features.Simulation.BattleSimulator();
            var existingBacktimesPerCommand = allInstructions.Keys.ToDictionary(c => c.CommandId, id => 0);

            foreach (var command in commandsToBacktimedVillages)
            {
                Scaffold.Command backtimedCommand = null;
                var commandsReturningToVillage    = allInstructions.Keys.Where(c => c.SourceVillageId == command.TargetVillageId);
                foreach (var returning in commandsReturningToVillage)
                {
                    if (command.LandsAt > returning.ReturnsAt && (command.LandsAt - returning.ReturnsAt.Value).TotalSeconds < 10)
                    {
                        backtimedCommand = returning;
                    }
                }

                if (backtimedCommand == null)
                {
                    continue;
                }

                var backtimedArmy      = (JSON.Army)backtimedCommand.Army;
                var battleResult       = battleSimulator.SimulateAttack(command.Army, backtimedArmy, 20, CurrentWorldSettings.ArchersEnabled);
                var originalPopulation = (float)Model.Native.ArmyStats.CalculateTotalPopulation(backtimedArmy, offensiveTypes);
                var newPopulation      = (float)Model.Native.ArmyStats.CalculateTotalPopulation(battleResult.DefendingArmy, offensiveTypes);

                var percentLost = 1 - newPopulation / originalPopulation;
                if (percentLost > 0.85f)
                {
                    existingBacktimesPerCommand[backtimedCommand.CommandId]++;
                }
            }

            var targetPlayerIds = targetPlayerIdsTmp.Keys.ToList();

            var playerInfoById = await(
                from player in CurrentSets.Player
                where targetPlayerIds.Contains(player.PlayerId)
                select new { player.PlayerId, player.TribeId, player.PlayerName }
                ).ToDictionaryAsync(i => i.PlayerId, i => i);

            var tribeIds      = playerInfoById.Values.Select(i => i.TribeId).Where(t => t != null).Distinct();
            var tribeInfoById = await(
                from tribe in CurrentSets.Ally
                where tribeIds.Contains(tribe.TribeId)
                select new { tribe.TribeId, tribe.Tag, tribe.TribeName }
                ).ToDictionaryAsync(i => i.TribeId, i => i);

            //  Get JSON version of instructions and info for all backtimeable nukes
            var result = allInstructions.Select(commandInstructions =>
            {
                (var command, var instructions) = commandInstructions.Tupled();

                var backtimeInfo  = new JSON.BacktimeInfo();
                var targetVillage = command.SourceVillage; // "Target" is the source of the command
                var targetPlayer  = playerInfoById[command.SourcePlayerId];
                var targetTribe   = targetPlayer.TribeId == null ? null : tribeInfoById[targetPlayer.TribeId.Value];
                var isStacked     = false;
                if (troopsAtBacktimedVillages[targetVillage.VillageId] != null)
                {
                    var stationedTroops = troopsAtBacktimedVillages[targetVillage.VillageId];
                    var defensePower    = Features.Simulation.BattleSimulator.TotalDefensePower(stationedTroops);
                    isStacked           = defensePower > 1000000;
                }

                //  Gather instructions and info for this command
                return(new JSON.BacktimeInfo
                {
                    //  General info
                    TravelingArmyPopulation = Model.Native.ArmyStats.CalculateTotalPopulation(command.Army, offensiveTypes),
                    TargetPlayerName = targetPlayer.PlayerName.UrlDecode(),
                    TargetTribeName = targetTribe?.TribeName?.UrlDecode(),
                    TargetTribeTag = targetTribe?.Tag?.UrlDecode(),
                    ExistingBacktimes = existingBacktimesPerCommand[command.CommandId],
                    IsStacked = isStacked,

                    //  Convert backtime instructions to JSON format
                    Instructions = instructions.Select(instruction =>
                    {
                        var sourceVillage = ownVillagesById[instruction.SendFrom];
                        var sourceVillageArmy = (JSON.Army)ownVillages[sourceVillage].ArmyAtHome;
                        var instructionArmy = sourceVillageArmy.BasedOn(instruction.TroopType);

                        return new JSON.BattlePlanCommand
                        {
                            LandsAt = command.ReturnsAt.Value,
                            LaunchAt = command.ReturnsAt.Value - instruction.TravelTime,
                            TravelTimeSeconds = (int)instruction.TravelTime.TotalSeconds,

                            TroopType = instruction.TroopType.ToString().ToLower(),
                            CommandPopulation = Model.Native.ArmyStats.CalculateTotalPopulation(instructionArmy),
                            CommandAttackPower = Features.Simulation.BattleSimulator.TotalAttackPower(instructionArmy),
                            CommandDefensePower = Features.Simulation.BattleSimulator.TotalDefensePower(instructionArmy),

                            SourceVillageId = instruction.SendFrom,
                            TargetVillageId = instruction.SendTo,

                            SourceVillageName = sourceVillage.VillageName.UrlDecode(),
                            TargetVillageName = targetVillage.VillageName.UrlDecode(),

                            SourceVillageX = sourceVillage.X.Value,
                            SourceVillageY = sourceVillage.Y.Value,

                            TargetVillageX = targetVillage.X.Value,
                            TargetVillageY = targetVillage.Y.Value,
                        };
                    }).ToList()
                });
            }).ToList();

            return(Ok(result));
        }
Ejemplo n.º 2
0
        public async Task <IActionResult> GetSuggestedActions()
        {
            PreloadWorldData();
            PreloadTranslationData();

            var serverTime          = CurrentServerTime;
            var twoDaysAgo          = serverTime - TimeSpan.FromDays(2);
            var twoDaysAgoTimestamp = new DateTimeOffset(twoDaysAgo).ToUnixTimeSeconds();

            var ownVillageData = await(
                from village in CurrentSets.Village
                join currentVillage in CurrentSets.CurrentVillage
                on village.VillageId equals currentVillage.VillageId
                where village.PlayerId == CurrentPlayerId
                select new { X = village.X.Value, Y = village.Y.Value, village.VillageId, VillageName = village.VillageName.UrlDecode(), currentVillage.ArmyAtHome, currentVillage.ArmyStationed }
                ).ToListAsync();

            var vaultPlayerIds = await CurrentSets.ActiveUser.Select(u => u.PlayerId).ToListAsync();

            var ownVillageMap = new Features.Spatial.Quadtree(ownVillageData.Select(v => new Coordinate {
                X = v.X, Y = v.Y
            }));

            async Task <object> GetRecapSuggestions(CurrentContextDbSets CurrentSets)
            {
                var capturedVillages = await(
                    from conquer in CurrentSets.Conquer
                    join sourcePlayer in CurrentSets.ActiveUser on conquer.OldOwner equals sourcePlayer.PlayerId
                    join village in CurrentSets.Village on conquer.VillageId equals village.VillageId
                    where conquer.UnixTimestamp > twoDaysAgoTimestamp
                    where conquer.NewOwner == null || !vaultPlayerIds.Contains(conquer.NewOwner.Value)
                    where conquer.NewOwner == village.PlayerId
                    select new { X = village.X.Value, Y = village.Y.Value, VillageId = conquer.VillageId, village.VillageName, conquer.OldOwner, conquer.NewOwner, OccurredAt = DateTimeOffset.FromUnixTimeSeconds(conquer.UnixTimestamp).UtcDateTime }
                    ).ToListAsync();

                capturedVillages = capturedVillages
                                   .GroupBy(v => v.VillageId)
                                   .Select(g => g.OrderByDescending(v => v.OccurredAt).First())
                                   .ToList();

                var villageIds       = capturedVillages.Select(v => v.VillageId).ToList();
                var noblesToVillages = await(
                    from user in CurrentSets.ActiveUser
                    join command in CurrentSets.Command on user.PlayerId equals command.SourcePlayerId
                    where villageIds.Contains(command.TargetVillageId)
                    where command.ArmyId != null && command.Army.Snob > 0
                    where command.LandsAt > serverTime
                    select command
                    ).ToListAsync();

                var noblesToVillagesById = noblesToVillages.GroupBy(v => v.TargetVillageId).ToDictionary(g => g.Key, g => g.Count());

                var relevantPlayerIds = capturedVillages
                                        .Select(v => v.OldOwner.Value)
                                        .Concat(capturedVillages.Where(v => v.NewOwner != null).Select(v => v.NewOwner.Value))
                                        .Distinct()
                                        .ToList();

                var playerNamesById = await
                                      CurrentSets.Player.Where(p => relevantPlayerIds.Contains(p.PlayerId))
                                      .ToDictionaryAsync(p => p.PlayerId, p => p.PlayerName);

                var loyaltyCalculator = new Features.Simulation.LoyaltyCalculator(CurrentWorldSettings.GameSpeed);
                var possibleLoyalties = capturedVillages.ToDictionary(
                    v => v.VillageId,
                    v => loyaltyCalculator.PossibleLoyalty(25, serverTime - v.OccurredAt)
                    );

                var tlNONE = await TranslateAsync("NONE");

                return(capturedVillages
                       .Where(v => possibleLoyalties[v.VillageId] < 100)
                       .Where(v => noblesToVillagesById.GetValueOrDefault(v.VillageId, 0) * CurrentWorldSettings.NoblemanLoyaltyMax < possibleLoyalties[v.VillageId])
                       .Select(v => new
                {
                    v.OccurredAt,
                    v.X, v.Y, v.VillageId,
                    VillageName = v.VillageName.UrlDecode(),
                    OldOwnerId = v.OldOwner, NewOwnerId = v.NewOwner,
                    OldOwnerName = playerNamesById.GetValueOrDefault(v.OldOwner ?? -1, tlNONE).UrlDecode(),
                    NewOwnerName = playerNamesById.GetValueOrDefault(v.NewOwner ?? -1, tlNONE).UrlDecode(),
                    IsNearby = ownVillageMap.ContainsInRange(v.X, v.Y, 5),
                    Loyalty = possibleLoyalties[v.VillageId]
                })
                       .OrderBy(v => v.Loyalty)
                       .ToList());
            }

            async Task <object> GetSnipeSuggestions(CurrentContextDbSets CurrentSets)
            {
                var incomingNobles = await(
                    from user in CurrentSets.ActiveUser
                    join command in CurrentSets.Command on user.PlayerId equals command.TargetPlayerId
                    where command.IsAttack
                    where command.TroopType == "snob"
                    where command.LandsAt > serverTime
                    where !command.IsReturning
                    select new { command.TargetVillageId, command.LandsAt }
                    ).ToListAsync();

                var incomingTrains = incomingNobles
                                     .GroupBy(n => n.TargetVillageId)
                                     .ToDictionary(
                    g => g.Key,
                    g => g
                    .OrderBy(n => n.LandsAt)
                    .GroupWhile((prev, curr) => prev.LandsAt - curr.LandsAt < TimeSpan.FromSeconds(1))
                    .Select(t =>
                {
                    var train = t.ToList();
                    return(new { train.First().LandsAt, Range = train.Last().LandsAt - train.First().LandsAt, Train = train });
                })
                    .ToList()
                    );

                var targetVillageIds  = incomingTrains.Keys.ToList();
                var targetVillageInfo = await(
                    from village in CurrentSets.Village
                    join currentVillage in CurrentSets.CurrentVillage.Include(cv => cv.ArmyStationed) on village.VillageId equals currentVillage.VillageId
                    where targetVillageIds.Contains(village.VillageId)
                    select new { Village = village, CurrentVillage = currentVillage }
                    ).ToListAsync();

                var ownVillages = await(
                    from village in CurrentSets.Village
                    join currentVillage in CurrentSets.CurrentVillage.Include(cv => cv.ArmyAtHome) on village.VillageId equals currentVillage.VillageId
                    where village.PlayerId == CurrentPlayerId
                    where currentVillage.ArmyAtHomeId != null
                    select new { Village = village, CurrentVillage = currentVillage }
                    ).ToDictionaryAsync(d => d.Village, d => d.CurrentVillage);

                var planner         = new Features.Planning.CommandOptionsCalculator(CurrentWorldSettings);
                var timeRequirement = new MaximumTravelTimeRequirement();

                planner.Requirements.Add(new MinimumDefenseRequirement {
                    MinimumDefense = 10000
                }.LimitTroopType(ArmyStats.DefensiveTroopTypes));
                planner.Requirements.Add(timeRequirement);

                return(targetVillageInfo
                       // Don't bother sniping stacked villas
                       .Where(info =>
                {
                    var defPop = ArmyStats.CalculateTotalPopulation(info.CurrentVillage.ArmyStationed, ArmyStats.DefensiveTroopTypes);
                    var numDVsStationed = defPop / (float)Features.CommandClassification.Utils.FullArmyPopulation;
                    return numDVsStationed < 2;
                })
                       // Make a plan for each train
                       .SelectMany(info => incomingTrains[info.Village.VillageId].Select(train =>
                {
                    timeRequirement.MaximumTime = train.LandsAt - serverTime;
                    var plan = planner.GenerateOptions(ownVillages, info.Village).Take(100).ToList();
                    return new
                    {
                        info.Village,
                        Train = train,
                        Plan = plan
                    };
                }))
                       // Ignore plans without any instructions
                       .Where(info => info.Plan.Count > 0)
                       // For each train and target village, return a plan with info on the villa that needs to be sniped
                       .Select(info => new
                {
                    Plan = info.Plan,
                    Train = info.Train.Train,
                    LandsAt = info.Train.LandsAt,
                    TargetVillage = new {
                        Id = info.Village.VillageId,
                        Name = info.Village.VillageName.UrlDecode(),
                        X = info.Village.X.Value,
                        Y = info.Village.Y.Value
                    }
                })
                       .OrderBy(info => info.LandsAt)
                       .ToList());
            }

            async Task <object> GetStackSuggestions(CurrentContextDbSets CurrentSets)
            {
                var enemyVillages = await(
                    from enemy in CurrentSets.EnemyTribe
                    join player in CurrentSets.Player on enemy.EnemyTribeId equals player.TribeId
                    join village in CurrentSets.Village on player.PlayerId equals village.PlayerId
                    select new Coordinate {
                    X = village.X.Value, Y = village.Y.Value
                }
                    ).ToListAsync();

                var enemyMap            = new Features.Spatial.Quadtree(enemyVillages);
                var frontlineVillages   = ownVillageData.Where(v => enemyMap.ContainsInRange(v.X, v.Y, 4.0f)).ToList();
                var frontlineVillageIds = frontlineVillages.Select(v => v.VillageId).ToList();

                var attacksOnFrontline = await(
                    from command in CurrentSets.Command
                    where frontlineVillageIds.Contains(command.TargetVillageId)
                    where command.IsAttack
                    where command.LandsAt > serverTime
                    select new { command.SourceVillageId, command.TargetVillageId }
                    ).ToListAsync();

                var supportToFrontline = await(
                    from support in CurrentSets.Command
                    where frontlineVillageIds.Contains(support.TargetVillageId)
                    where support.LandsAt > serverTime
                    where support.ArmyId != null
                    select new { support.TargetVillageId, support.Army }
                    ).ToListAsync();

                var attackingVillageIds = attacksOnFrontline.Select(c => c.SourceVillageId).Distinct().ToList();
                var attackingVillages   = await CurrentSets
                                          .CurrentVillage
                                          .Where(v => attackingVillageIds.Contains(v.VillageId))
                                          .Select(v => new { v.VillageId, v.ArmyOwned, v.ArmyStationed, v.ArmyTraveling })
                                          .ToDictionaryAsync(
                    v => v.VillageId,
                    v => v
                    );

                var attackingVillagesWithNukes = attackingVillages
                                                 // Get possible troops from village
                                                 .Select(kvp => new { kvp.Key, Army = JSON.Army.Max(kvp.Value.ArmyOwned, kvp.Value.ArmyStationed, kvp.Value.ArmyTraveling) })
                                                 // Get population of offensive troops
                                                 .Select(kvp => new { kvp.Key, OffensivePop = ArmyStats.CalculateTotalPopulation(kvp.Army, ArmyStats.OffensiveTroopTypes.Except(new[] { JSON.TroopType.Heavy }).ToArray()) })
                                                 // Filter by full nukes
                                                 .Where(kvp => kvp.OffensivePop > 0.65f * Features.CommandClassification.Utils.FullArmyPopulation)
                                                 .Select(kvp => kvp.Key)
                                                 .ToList();

                var nukesSentPerVillage = frontlineVillageIds.ToDictionary(id => id, id => 0);

                foreach (var attack in attacksOnFrontline.Where(a => attackingVillagesWithNukes.Contains(a.SourceVillageId)))
                {
                    nukesSentPerVillage[attack.TargetVillageId]++;
                }

                var pendingSupportPerVillage = frontlineVillageIds.ToDictionary(id => id, id => new JSON.Army());

                foreach (var support in supportToFrontline)
                {
                    pendingSupportPerVillage[support.TargetVillageId] += support.Army;
                }

                var battleSimulator        = new Features.Simulation.BattleSimulator();
                var nukesEatablePerVillage = frontlineVillages.ToDictionary(
                    v => v.VillageId,
                    v => battleSimulator.EstimateRequiredNukes(v.ArmyStationed + pendingSupportPerVillage[v.VillageId], 20, CurrentWorldSettings.ArchersEnabled, 100).NukesRequired
                    );

                return(frontlineVillages
                       .Where(v => nukesSentPerVillage[v.VillageId] > 0 && nukesEatablePerVillage[v.VillageId] - nukesSentPerVillage[v.VillageId] < 2)
                       .Select(v => new
                {
                    v.VillageId,
                    v.VillageName,
                    v.X, v.Y,
                    SentNukes = nukesSentPerVillage[v.VillageId],
                    EatableNukes = nukesEatablePerVillage[v.VillageId]
                })
                       .OrderByDescending(v => v.SentNukes - v.EatableNukes)
                       .ThenBy(v => v.VillageId)
                       .ToList());
            }

            async Task <object> GetNobleTargetSuggestions(CurrentContextDbSets CurrentSets)
            {
                var villasWithNobles = await(
                    from village in CurrentSets.Village
                    join currentVillage in CurrentSets.CurrentVillage on village.VillageId equals currentVillage.VillageId
                    where currentVillage.ArmyOwned.Snob > 0
                    where village.PlayerId == CurrentPlayerId
                    select new Coordinate {
                    X = village.X.Value, Y = village.Y.Value
                }
                    ).ToListAsync();

                var enemyCurrentVillas = await(
                    from currentVillage in CurrentSets.CurrentVillage
                    join village in CurrentSets.Village on currentVillage.VillageId equals village.VillageId
                    where !CurrentSets.ActiveUser.Any(au => au.PlayerId == village.PlayerId)
                    select new { X = village.X.Value, Y = village.Y.Value, village.VillageId, village.Points, currentVillage.Loyalty, currentVillage.LoyaltyLastUpdated, currentVillage.ArmyStationed, village.PlayerId, VillageName = village.VillageName.UrlDecode(), PlayerName = village.Player.PlayerName.UrlDecode() }
                    ).ToListAsync();

                var villageMap = new Features.Spatial.Quadtree(villasWithNobles);

                var loyaltyCalculator = new Features.Simulation.LoyaltyCalculator(CurrentWorldSettings.GameSpeed);
                var possibleTargets   = enemyCurrentVillas
                                        .Where(v => villageMap.ContainsInRange(v.X, v.Y, 7.5f)) // Only consider enemy villas within 7.5 fields of any villa with nobles
                                        .Select(v =>
                {
                    var possibleLoyalty = v.Loyalty.HasValue
                            ? loyaltyCalculator.PossibleLoyalty(v.Loyalty.Value, serverTime - v.LoyaltyLastUpdated.Value)
                            : 100;

                    var stationedDVs = ArmyStats.CalculateTotalPopulation(v.ArmyStationed, ArmyStats.DefensiveTroopTypes) / (float)Features.CommandClassification.Utils.FullArmyPopulation;

                    // Select "confidence" in selecting the given target as a suggestion
                    // If < 0.75 DV stationed or loyalty under 50, 100% confident in the suggestion
                    var loyaltyConfidence = 1.0f - (possibleLoyalty - 50) / 50.0f;
                    var stackConfidence   = 1.0f - (stationedDVs - 0.75f) / 0.75f;

                    if (v.ArmyStationed?.LastUpdated != null)
                    {
                        var stackAge     = serverTime - v.ArmyStationed.LastUpdated.Value;
                        var ageFactor    = (TimeSpan.FromHours(48) - stackAge) / TimeSpan.FromHours(48);
                        stackConfidence *= Math.Max(0, (float)Math.Pow(Math.Abs(ageFactor), 0.5f) * Math.Sign(ageFactor));
                    }
                    else
                    {
                        stackConfidence = 0;
                    }

                    return(new
                    {
                        Loyalty = possibleLoyalty,
                        StationedDVs = stationedDVs,
                        DVsSeenAt = v.ArmyStationed?.LastUpdated,
                        Confidence = loyaltyConfidence + stackConfidence,
                        Village = v
                    });
                });

                var confidentTargets = possibleTargets.Where(t => t.Confidence > 1).ToList();

                return(confidentTargets
                       .Select(t => new
                {
                    t.Village.X,
                    t.Village.Y,
                    t.Village.VillageId,
                    t.Village.VillageName,
                    t.Village.PlayerId,
                    t.Village.PlayerName,
                    t.Village.Points,
                    t.Loyalty,
                    t.StationedDVs,
                    t.DVsSeenAt,
                    t.Confidence
                })
                       .OrderByDescending(t => t.Confidence)
                       .ThenBy(t => t.VillageId)
                       .ToList());
            }

            async Task <object> GetUselessStackSuggestions(CurrentContextDbSets CurrentSets)
            {
                var enemyVillages = await(
                    from enemy in CurrentSets.EnemyTribe
                    join player in CurrentSets.Player on enemy.EnemyTribeId equals player.TribeId
                    join village in CurrentSets.Village on player.PlayerId equals village.PlayerId
                    select new Coordinate {
                    X = village.X.Value, Y = village.Y.Value
                }
                    ).ToListAsync();

                var ownSupport = await(
                    from support in CurrentSets.CurrentVillageSupport
                    join targetVillage in CurrentSets.Village on support.TargetVillageId equals targetVillage.VillageId
                    join sourceVillage in CurrentSets.Village on support.SourceVillageId equals sourceVillage.VillageId
                    where targetVillage.PlayerId == null || !vaultPlayerIds.Contains(targetVillage.PlayerId.Value)
                    where sourceVillage.PlayerId == CurrentPlayerId
                    select new { support.SupportingArmy, targetVillage.VillageId }
                    ).ToListAsync();

                var notOwnVillageIds = ownSupport.Select(s => s.VillageId).Distinct().ToList();
                var notOwnVillages   = await(
                    from village in CurrentSets.Village
                    where notOwnVillageIds.Contains(village.VillageId)
                    select new { X = village.X.Value, Y = village.Y.Value, VillageName = village.VillageName.UrlDecode(), village.VillageId }
                    ).ToListAsync();

                var enemyMap = new Features.Spatial.Quadtree(enemyVillages);

                var backlineVillages = ownVillageData.Where(v => !enemyMap.ContainsInRange(new Coordinate {
                    X = v.X, Y = v.Y
                }, 10));

                var stackInfo = backlineVillages
                                // Select backline villages with over 3k pop of support
                                .Select(v => new { Village = new { v.X, v.Y, v.VillageName, v.VillageId }, Population = ArmyStats.CalculateTotalPopulation((JSON.Army)v.ArmyStationed - (JSON.Army)v.ArmyAtHome) })
                                .Where(v => v.Population > 3000)
                                .Select(v => new { v.Village.X, v.Village.Y, v.Village.VillageName, v.Village.VillageId, PopCount = v.Population })
                                // Include support to unknown villages
                                .Concat(notOwnVillages.Select(v => new { v.X, v.Y, v.VillageName, v.VillageId, PopCount = ownSupport.Where(s => s.VillageId == v.VillageId).Sum(s => ArmyStats.CalculateTotalPopulation(s.SupportingArmy)) }))
                                .ToList();

                var villageIds        = stackInfo.Select(i => i.VillageId).Distinct().ToList();
                var villageOwnersById = await(
                    from village in CurrentSets.Village
                    join player in CurrentSets.Player on village.PlayerId equals player.PlayerId
                    let tribe = (from tribe in CurrentSets.Ally
                                 where tribe.TribeId == player.TribeId
                                 select new { tribe.Tag, tribe.TribeId }).FirstOrDefault()
                                where villageIds.Contains(village.VillageId)
                                select new { village.VillageId, Player = player, Tribe = tribe }
                    ).ToDictionaryAsync(
                    v => v.VillageId,
                    v => new { v.Player.PlayerName, v.Player.PlayerId, TribeName = v.Tribe?.Tag, v.Tribe?.TribeId }
                    );

                return(stackInfo
                       .Select(info =>
                {
                    var owner = villageOwnersById.GetValueOrDefault(info.VillageId);
                    return new
                    {
                        info.VillageId,
                        info.VillageName,
                        info.PopCount,
                        info.X,
                        info.Y,
                        PlayerName = owner?.PlayerName == null ? null : owner.PlayerName.UrlDecode(),
                        owner?.PlayerId,
                        TribeName = owner?.TribeName == null ? null : owner.TribeName.UrlDecode(),
                        owner?.TribeId
                    };
                }));
            }

            (var recaps, var snipes, var stacks, var nobles, var uselessStacks) = await ManyTasks.Run(
                WithTemporarySets(GetRecapSuggestions),
                WithTemporarySets(GetSnipeSuggestions),
                WithTemporarySets(GetStackSuggestions),
                WithTemporarySets(GetNobleTargetSuggestions),
                WithTemporarySets(GetUselessStackSuggestions)
                );

            return(Ok(new
            {
                Recaps = recaps,
                Snipes = snipes,
                Stacks = stacks,
                NobleTargets = nobles,
                UselessStacks = uselessStacks
            }));
        }