public Randomizer(IGameLayer gameLayer, ILoggerFactory loggerFactory, TurnStrategyBase strategy = null) { _gameLayer = gameLayer; _gameState = gameLayer.GetState(); _logger = loggerFactory.CreateLogger <Randomizer>(); if (strategy == null) { _strategy = TurnStrategyBase.Build(loggerFactory) .Append <BuildUtilityCloseToResidencesTurnStrategy>(c => { c.BuildingName = "Mall"; c.MaxNumberOfBuildings = 3; }) .Append <BuildUtilityCloseToResidencesTurnStrategy>(c => { c.BuildingName = "Park"; c.MaxNumberOfBuildings = 2; }) .Append <BuildBuildingWhenCloseToPopMaxTurnStrategy>() .Append <BuyUpgradeTurnStrategy>() .Append <MaintenanceWhenBuildingIsGettingDamagedTurnStrategy>() .Append <BuildWhenHasBuildingsUnderConstructionTurnStrategy>() .Append <AdjustBuildingTemperaturesTurnStrategy>() .Append <SingletonBuildingTurnStrategy>() .Compile(); } else { _strategy = strategy; } _logger.LogInformation($"Strategy: {Environment.NewLine + _strategy}"); }
public virtual bool TryExecuteStrategy(Randomizer randomizer, IGameLayer gameLayer, GameState state) { var result = _predicate == null || _predicate(state); if (result) { // Has no predicate or has meet the requirements result = TryExecuteTurn(randomizer, gameLayer, state); } var globalResult = result; if (!globalResult && _parent != null) { // Could not execute, continue with the parent globalResult = _parent.TryExecuteStrategy(randomizer, gameLayer, state); } // Make a callback that the strategy has succeeded if (_callback != null) { _callback?.Invoke(state, result, globalResult); } return(globalResult); }
protected override bool TryExecuteTurn(Randomizer randomizer, IGameLayer gameLayer, GameState state) { // Only build when has nothing else to do var action = randomizer.GetRandomAction(x => x != GameActions.StartBuild); if (action == GameActions.Wait) { action = randomizer.GetRandomAction(); if (action == GameActions.StartBuild) { var building = state.AvailableResidenceBuildings.Find(x => x.BuildingName == "Cabin"); if (building.Cost > state.Funds) { Logger.LogWarning("Wanted to build building, but cannot afford it"); return(false); } // Prioritize building Cabins var position = randomizer.GetRandomBuildablePosition(); if (position == null) { Logger.LogWarning("No valid positions to build building"); return(false); } gameLayer.StartBuild(position, building.BuildingName, state.GameId); return(true); } } return(false); }
protected override bool TryExecuteTurn(Randomizer randomizer, IGameLayer gameLayer, GameState state) { BlueprintUtilityBuilding building; var buildingName = BuildingName; if (!string.IsNullOrWhiteSpace(buildingName)) { building = state.AvailableUtilityBuildings.Find(x => x.BuildingName == buildingName); } else { var affordableBuildings = state.AvailableUtilityBuildings .Where(x => x.Cost <= state.Funds) .ToArray(); building = affordableBuildings.ElementAtOrDefault(randomizer.Random.Next(0, affordableBuildings.Length)); } if (building == null) { // No valid building return(false); } var buildings = state.GetBuiltBuildings().Where(x => x.BuildingName == building.BuildingName).ToArray(); if (buildings.Length >= MaxNumberOfBuildings) { // Don't build any more buildings return(false); } if (buildings.Any(x => x.BuildProgress < 100)) { // Already one in progress return(false); } if (building.Cost > state.Funds) { // Cannot afford it Logger.LogWarning("Wanted to build building, but cannot afford it"); return(false); } var position = randomizer.GetRandomBuildablePositionNearResidence(); if (position == null || !state.IsBuildablePosition(position)) { Logger.LogWarning("No valid positions to build utility building"); return(false); } gameLayer.StartBuild(position, building.BuildingName, state.GameId); return(true); }
public static void ExecuteAction(this IGameLayer gameLayer, GameActions action) { if (action == GameActions.Wait) { ExecuteAction(gameLayer, action, position: null); } else { throw new InvalidOperationException($"Only 'GameActions.Wait' can be invoked without a position"); } }
protected override bool TryExecuteTurn(Randomizer randomizer, IGameLayer gameLayer, GameState state) { var upgrades = state.AvailableUpgrades; if (!upgrades.Any()) { return(false); } var completedBuildings = state.GetCompletedBuildings().OfType <BuiltResidenceBuilding>().ToArray(); if (!completedBuildings.Any()) { return(false); } var allowedUpgrades = IncludedUpgrades != null ? upgrades.Where(x => IncludedUpgrades.Contains(x.Name)).ToArray() : upgrades.ToArray(); var affordedUpgrades = allowedUpgrades.Where(x => x.Cost < state.Funds).ToArray(); var upgrade = affordedUpgrades.ElementAtOrDefault(randomizer.Random.Next(0, affordedUpgrades.Length)); if (upgrade == null) { // No allowed upgrades return(false); } var applicableBuildings = completedBuildings.Where(x => !x.Effects.Contains(upgrade.Name)).ToArray(); var building = applicableBuildings.ElementAtOrDefault(randomizer.Random.Next(0, applicableBuildings.Length)); if (building == null) { // No applicable buildings for chosen upgrade return(false); } var resultingFunds = state.Funds - upgrade.Cost; if (resultingFunds < 30000) { // Always leave a good margin on the bank account return(false); } var position = building.Position; gameLayer.BuyUpgrade(position, upgrade.Name, state.GameId); return(true); }
protected override bool TryExecuteTurn(Randomizer randomizer, IGameLayer gameLayer, GameState state) { var currentBuildingCount = state.GetBuiltBuildings().Count(); if (currentBuildingCount > 0) { return(false); } var position = randomizer.GetRandomBuildablePosition(); if (position == null) { Logger.LogWarning("No valid positions to build building"); return(false); } BlueprintResidenceBuilding building; var buildingName = BuildingName; if (!string.IsNullOrWhiteSpace(buildingName)) { building = state.AvailableResidenceBuildings.Find(x => x.BuildingName == buildingName); } else { var affordableBuildings = state.AvailableResidenceBuildings .Where(x => x.Cost <= state.Funds) .ToArray(); building = affordableBuildings.ElementAtOrDefault(randomizer.Random.Next(0, affordableBuildings.Length)); } if (building == null) { // No valid building return(false); } if (building.Cost > state.Funds) { // Cannot afford it return(false); } gameLayer.StartBuild(position, building.BuildingName, state.GameId); return(true); }
protected override bool TryExecuteTurn(Randomizer randomizer, IGameLayer gameLayer, GameState state) { var buildingsUnderConstruction = state.GetBuildingsUnderConstruction().ToArray(); // Randomly choose building //var building = buildingsUnderConstruction.ElementAt(randomizer.Random.Next(0, buildingsUnderConstruction.Length)); // Choose the one closest to completion var building = buildingsUnderConstruction.OrderByDescending(x => x.BuildProgress).FirstOrDefault(); if (building != null) { var position = building.Position; gameLayer.ExecuteAction(GameActions.Build, position); building = state.GetCompletedBuildings().FirstOrDefault(x => x.Position.ToString() == position.ToString()); if (building is BuiltResidenceBuilding residence) { if (AdjustHeatOnConstructed) { var blueprint = state.AvailableResidenceBuildings.Find(x => x.BuildingName == building.BuildingName); var energy = blueprint.BaseEnergyNeed + (residence.Temperature - state.CurrentTemp) * blueprint.Emissivity / 1 - 0.5 - residence.CurrentPop * 0.04; if (IsBetween(energy, residence.RequestedEnergyIn - AllowedDiffMargin, residence.RequestedEnergyIn + AllowedDiffMargin)) { // current energy setting is sufficient } else { // adjust energy as the first action after completing building (will take one turn) gameLayer.AdjustEnergy(building.Position, energy, state.GameId); } } } else { } return(true); } return(false); }
protected override bool TryExecuteTurn(Randomizer randomizer, IGameLayer gameLayer, GameState state) { var damagedBuildings = state.ResidenceBuildings.Where(x => x.Health < ThresholdHealth) .OrderBy(x => x.Health) .ToArray(); if (!damagedBuildings.Any()) { return(false); } var affordedBuildings = damagedBuildings.Where(x => { var blueprint = state.AvailableResidenceBuildings.Single(p => p.BuildingName == x.BuildingName); var afforded = blueprint.MaintenanceCost < state.Funds; return(afforded); }).ToArray(); BuiltResidenceBuilding building; if (PrioritizeWeakest) { building = damagedBuildings.First(); if (!affordedBuildings.Contains(building)) { // Can't afford it, then skip hole strategy to save money for next time return(false); } } else { // Can afford maintenance on the weakest building building = affordedBuildings.FirstOrDefault(); if (building == null) { // Can't afford it, must wait until can afford return(false); } } var position = building.Position; gameLayer.Maintenance(position, state.GameId); return(true); }
public static void ExecuteAction(this IGameLayer gameLayer, GameActions action, Position position, object argument = null) { var state = gameLayer.GetState(); switch (action) { case GameActions.StartBuild: var buildingName = (string)argument ?? throw new ArgumentNullException(nameof(argument)); gameLayer.StartBuild(position, buildingName, state.GameId); break; case GameActions.Build: gameLayer.Build(position, state.GameId); break; case GameActions.Maintenance: gameLayer.Maintenance(position, state.GameId); break; case GameActions.BuyUpgrade: var upgradeName = (string)argument ?? throw new ArgumentNullException(nameof(argument)); gameLayer.BuyUpgrade(position, upgradeName, state.GameId); break; case GameActions.Wait: gameLayer.Wait(state.GameId); break; case GameActions.None: throw new NotSupportedException(); default: throw new NotImplementedException(); } }
public static IEnumerable <GameActions> GetPossibleActions(this IGameLayer gameLayer, bool includeWait = true) { var state = gameLayer.GetState(); // If has money availble for a building, then can start building var blueprints = state.GetBuildingBlueprints().ToArray(); if (blueprints.Any(x => x.Cost <= state.Funds)) { // Only build if will have plenty of money left... var fraction = state.Funds / blueprints.Average(x => x.Cost); if (fraction >= 2) { yield return(GameActions.StartBuild); } else { } } // If has non-completed buildings, then can build if (state.GetBuildingsUnderConstruction().Any(x => x.BuildProgress < 100)) { yield return(GameActions.Build); } // If has completed buildings and they are low on hp, then can do maintenance var damagedBuildings = state.ResidenceBuildings.Where(x => x.Health < 50).ToArray(); if (damagedBuildings.Any()) { var buildingTypes = damagedBuildings.Select(x => x.BuildingName).Distinct().ToArray(); var affordedMaintenance = state.AvailableResidenceBuildings .Where(x => buildingTypes.Contains(x.BuildingName)) .Where(x => x.MaintenanceCost < state.Funds) .ToArray(); if (affordedMaintenance.Any()) { yield return(GameActions.Maintenance); } } // If has completed buildings and they are low on hp, then can do maintenance if (state.GetCompletedBuildings().Any()) { if (state.AvailableUpgrades.Any(x => x.Cost < state.Funds)) { var fraction = state.Funds / state.AvailableUpgrades.OrderByDescending(x => x.Cost).First().Cost; if (fraction >= 6) { // todo: enable when logic is more stable //yield return GameActions.BuyUpgrade; } else { } } } if (includeWait) { yield return(GameActions.Wait); } }
protected abstract bool TryExecuteTurn(Randomizer randomizer, IGameLayer gameLayer, GameState state);
protected override bool TryExecuteTurn(Randomizer randomizer, IGameLayer gameLayer, GameState state) { var buildings = state.GetBuiltBuildings().OfType <BuiltResidenceBuilding>().ToArray(); if (buildings.Length >= MaxNumberOfResidences) { // Don't build any more buildings return(false); } var completedResidences = state.GetCompletedBuildings().OfType <BuiltResidenceBuilding>().ToArray(); // Only build when has nothing else to do var currentPop = completedResidences.Sum(x => x.CurrentPop); var currentPopMax = completedResidences .Join(state.AvailableResidenceBuildings, ok => ok.BuildingName, ik => ik.BuildingName, (rb, bp) => new { bp, rb }) .Sum(x => x.bp.MaxPop); var pendingPopMaxIncrease = state.GetBuildingsUnderConstruction().OfType <BuiltResidenceBuilding>() .Join(state.AvailableResidenceBuildings, ok => ok.BuildingName, ik => ik.BuildingName, (rb, bp) => new { bp, rb }) .Sum(x => x.bp.MaxPop); var currentPopPercentage = currentPopMax > 0 ? currentPop / (double)currentPopMax : 0; Logger.LogDebug($"Pop {currentPop}/{currentPopMax} = {currentPopPercentage:P2} (+ {pendingPopMaxIncrease})"); if (currentPopPercentage > PopulationPercentageThreshold || buildings.Length < 1) { var position = randomizer.GetRandomBuildablePosition(); if (position == null) { Logger.LogWarning("No valid positions to build building"); return(false); } BlueprintResidenceBuilding building; var buildingName = BuildingName; if (!string.IsNullOrWhiteSpace(buildingName)) { building = state.AvailableResidenceBuildings.Find(x => x.BuildingName == buildingName); } else { var affordableBuildings = state.AvailableResidenceBuildings .Where(x => x.Cost <= state.Funds) .ToArray(); building = affordableBuildings.ElementAtOrDefault(randomizer.Random.Next(0, affordableBuildings.Length)); } if (building == null) { // No valid building return(false); } if (building.Cost > state.Funds) { // Cannot afford it Logger.LogWarning("Wanted to build building, but cannot afford it"); return(false); } gameLayer.StartBuild(position, building.BuildingName, state.GameId); return(true); } return(false); }
protected override bool TryExecuteTurn(Randomizer randomizer, IGameLayer gameLayer, GameState state) { double?predictedTrend = null; var outdoorTemp = state.CurrentTemp; if (state.TemperatureHistory?.Count > 2) { // Predict outdoor temperature var prePreviousTemp = state.TemperatureHistory.Reverse().Skip(2).First().Value; var previousTemp = state.TemperatureHistory.Reverse().Skip(1).First().Value; var previousDiff = previousTemp - prePreviousTemp; var currentDiff = state.CurrentTemp - previousTemp; if (previousDiff > 0 && currentDiff > 0) { // Trend is "getting hotter" predictedTrend = (previousDiff + currentDiff) / 2; } else if (previousDiff < 0 && currentDiff < 0) { // Trend is "getting colder" predictedTrend = (previousDiff + currentDiff) / 2; } if (predictedTrend.HasValue) { // Add the trend twice to more quickly react to big temperature changes predictedTrend = predictedTrend.Value * 2; outdoorTemp = state.CurrentTemp + predictedTrend.Value; Logger.LogTrace($"Using prediction for OutdoorTemp: {outdoorTemp:N3}, CurrentTemp: {state.CurrentTemp:N1} (weighted trend: {predictedTrend:N3})"); } } var buildings = state.GetCompletedBuildings() .OfType <BuiltResidenceBuilding>() //.Where(x => x.Temperature < MinTemperature + AllowedTemperatureDiffMargin) .OrderBy(x => x.Temperature) .ToArray(); foreach (var building in buildings) { var blueprint = state.AvailableResidenceBuildings.Find(x => x.BuildingName == building.BuildingName); // Predict next temperature var newTemp = building.Temperature + (building.EffectiveEnergyIn - blueprint.BaseEnergyNeed) * _degreesPerExcessMwh + _degreesPerPop * building.CurrentPop - (building.Temperature - outdoorTemp) * blueprint.Emissivity; if (IsBetween(newTemp, TargetTemperature - AllowedTemperatureDiffMargin, TargetTemperature + AllowedTemperatureDiffMargin)) { // close enough to target temperature already... continue; } if (newTemp < MinTemperature) { // Is below minimum, fake that it is much colder than it is to make a faster recovery outdoorTemp -= Math.Abs(TargetTemperature - building.Temperature); } if (building.Temperature < MinTemperature) { // Is below minimum, fake that it is much colder than it is to make a faster recovery outdoorTemp -= Math.Abs(TargetTemperature - building.Temperature); } if (newTemp > MaxTemperature) { // Is above maximum, fake that it is much hotter than it is to make a faster recovery outdoorTemp += Math.Abs(TargetTemperature - building.Temperature); } if (building.Temperature > MaxTemperature) { // Is above maximum, fake that it is much hotter than it is to make a faster recovery outdoorTemp += Math.Abs(TargetTemperature - building.Temperature); } var energyOld = blueprint.BaseEnergyNeed + (building.Temperature - outdoorTemp) * blueprint.Emissivity / 1 + 0.5 - building.CurrentPop * 0.04; var energyNew = (TargetTemperature - building.Temperature + blueprint.BaseEnergyNeed * _degreesPerExcessMwh - _degreesPerPop * building.CurrentPop + building.Temperature * blueprint.Emissivity - outdoorTemp * blueprint.Emissivity) / _degreesPerExcessMwh; var energy = UseSecondaryAlgorithm ? energyNew : energyOld; if (predictedTrend.GetValueOrDefault() > 0) { // Trend is getting hotter // Then reduce energy to save heating resources and to cooldown appartment... energy *= 0.9; } else if (predictedTrend.GetValueOrDefault() < 0) { // Trend is getting colder // Then apply more energy to make sure has enough heating... energy *= 1.1; } if (energy < blueprint.BaseEnergyNeed) { Logger.LogWarning($"Wanted to set lower energy than BaseEnergyNeed, restoring to base: {blueprint.BaseEnergyNeed:N3} Mwh from {energy:N3} Mwh"); energy = blueprint.BaseEnergyNeed; } // Predict next temperature, if change energy var predictedNewTemp = building.Temperature + (energy - blueprint.BaseEnergyNeed) * _degreesPerExcessMwh + _degreesPerPop * building.CurrentPop - (building.Temperature - outdoorTemp) * blueprint.Emissivity; Logger.LogTrace($"{building.BuildingName} at {building.Position}"); Logger.LogTrace($"Current building temp: \t\t{building.Temperature:N3}"); Logger.LogTrace($"Next building temp: \t\t\t{newTemp:N3}"); Logger.LogTrace($"Predicted New Temp: \t\t\t{predictedNewTemp:N3}"); Logger.LogTrace($"Current building energy: \t{building.EffectiveEnergyIn}/{building.RequestedEnergyIn} Mwh"); Logger.LogTrace($"New requested energy: \t\t{energyOld:N3} Mwh"); Logger.LogTrace($"New requested energy v2: \t{energyNew:N3} Mwh"); if (newTemp < TargetTemperature) { // Colder than target if (energy < building.RequestedEnergyIn) { // Should not lower energy if already too cold continue; } else { } } else if (newTemp > TargetTemperature) { // Hotter than target if (energy > building.RequestedEnergyIn) { // Should not increase energy if already too hot continue; } else { } } if (IsBetween(energy, building.RequestedEnergyIn - AllowedDiffMargin, building.RequestedEnergyIn + AllowedDiffMargin)) { // minimal change, wait to update energy (to reduce calls and save money) continue; } if (state.Funds < _adjustCost) { Logger.LogWarning($"Wanted to apply energy '{energy:N1}' to building at {building.Position}, but has insufficient funds ({state.Funds})"); continue; } Logger.LogInformation($"Adjusting energy for {building.Position} from {building.RequestedEnergyIn} to {energy:N1} (TEMP:: current={building.Temperature:N3}, predicted={predictedNewTemp:N3})"); gameLayer.AdjustEnergy(building.Position, energy, state.GameId); return(true); } return(false); }