Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions C7Engine/AI/ChooseProducible.cs
Original file line number Diff line number Diff line change
Expand Up @@ -315,10 +315,10 @@ private static int NumberOfReachableOpenCitySpots(City city) {

// Note: GetScoredSettlerCandidates already excludes tiles with
// settlers moving towards them.
Dictionary<Tile, int> scoredLocations = SettlerLocationAI.GetScoredSettlerCandidates(city.location, city.owner);
List<KeyValuePair<Tile, int>> orderedScores = scoredLocations.OrderByDescending(t => t.Value).ToList();
Dictionary<Tile, float> scoredLocations = SettlerLocationAi.GetScoredSettlerCandidates(city.location, city.owner);
List<KeyValuePair<Tile, float>> orderedScores = scoredLocations.OrderByDescending(t => t.Value).ToList();

foreach ((Tile tile, int score) in orderedScores) {
foreach ((Tile tile, float score) in orderedScores) {
if (scoredLocations.ContainsKey(tile)) {
++result;

Expand Down
10 changes: 5 additions & 5 deletions C7Engine/AI/StrategicAI/UtilityCalculations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ namespace C7Engine.AI.StrategicAI {
/// </summary>
public class UtilityCalculations {

private static readonly int POSSIBLE_CITY_LOCATION_SCORE = 2; //how much weight to give to each possible city location
private static readonly int TILE_SCORE_DIVIDER = 10; //how much to divide each location's tile score by
private static readonly int PossibleCityLocationScore = 2; //how much weight to give to each possible city location
private static readonly int TileScoreDivider = 10; //how much to divide each location's tile score by

public static int CalculateAvailableLandScore(Player player) {
//Figure out if there's land to settle, and how much
Dictionary<Tile, int> possibleLocations = SettlerLocationAI.GetScoredSettlerCandidates(player.cities[0].location, player);
int score = possibleLocations.Count * POSSIBLE_CITY_LOCATION_SCORE;
Dictionary<Tile, float> possibleLocations = SettlerLocationAi.GetScoredSettlerCandidates(player.cities[0].location, player);
int score = possibleLocations.Count * PossibleCityLocationScore;
foreach (int i in possibleLocations.Values) {
score += i / TILE_SCORE_DIVIDER;
score += i / TileScoreDivider;
}
return score;
}
Expand Down
5 changes: 2 additions & 3 deletions C7Engine/AI/UnitAI/SettlerAI.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
using System;
using C7Engine.Pathing;
using C7GameData;
using C7GameData.AIData;
using Serilog;

namespace C7Engine {
public class SettlerAI : C7GameData.UnitAI {
public class SettlerAI : UnitAI {
private static ILogger log = Log.ForContext<SettlerAI>();
public SettlerAIData data;

Expand All @@ -17,7 +16,7 @@ public static SettlerAIData MakeAiData(MapUnit unit, Player player) {
settlerAiData.destination = unit.location;
log.Information("No cities yet! Set AI for unit to settler AI with destination of " + settlerAiData.destination);
} else {
settlerAiData.destination = SettlerLocationAI.findSettlerLocation(unit.location, player);
settlerAiData.destination = SettlerLocationAi.FindSettlerLocation(unit.location, player);
if (settlerAiData.destination == Tile.NONE) {
//This is possible if all tiles within 4 tiles of a city are either not land, or already claimed
//by another colonist. Longer-term, the AI shouldn't be building settlers if that is the case,
Expand Down
50 changes: 22 additions & 28 deletions C7Engine/AI/UnitAI/SettlerLocationAI.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using C7GameData;
using System.Linq;
using C7GameData.AIData;
using Serilog;
// ReSharper disable ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator
// ReSharper disable CheckNamespace

namespace C7Engine {
public class SettlerLocationAI {
private static ILogger log = Log.ForContext<SettlerLocationAI>();

//Eventually, there should be different weights based on whether the AI already
//has the resource or not (more important to secure ones that they don't have).
//But since we don't have trade networks yet, for now there's only one value.
static int STRATEGIC_RESOURCE_BONUS = 20;
static int LUXURY_RESOURCE_BONUS = 15;
public class SettlerLocationAi {
private static readonly ILogger Log = Serilog.Log.ForContext<SettlerLocationAi>();

//Figures out where to plant Settlers
public static Tile findSettlerLocation(Tile start, Player player) {
Dictionary<Tile, int> scores = GetScoredSettlerCandidates(start, player);
public static Tile FindSettlerLocation(Tile start, Player player) {
Dictionary<Tile, float> scores = GetScoredSettlerCandidates(start, player);
if (scores.Count == 0 || scores.Values.Max() <= 0) {
return Tile.NONE; //nowhere to settle
}
Expand All @@ -27,19 +21,19 @@ public static Tile findSettlerLocation(Tile start, Player player) {
return result;
}

public static Dictionary<Tile, int> GetScoredSettlerCandidates(Tile start, Player player) {
public static Dictionary<Tile, float> GetScoredSettlerCandidates(Tile start, Player player) {
List<MapUnit> playerUnits = player.units;
// TODO: handle settling other continents
IEnumerable<Tile> candidates = player.tileKnowledge.AllKnownTiles().Where(t => !IsInvalidCityLocation(t) && t.continent == start.continent);
Dictionary<Tile, int> scores = AssignTileScores(start, player, candidates, playerUnits.FindAll(u => u.unitType.name == "Settler"));
Dictionary<Tile, float> scores = AssignTileScores(start, player, candidates, playerUnits.FindAll(u => u.unitType.name == "Settler"));
return scores;
}

private static Dictionary<Tile, int> AssignTileScores(Tile startTile, Player player, IEnumerable<Tile> candidates, List<MapUnit> playerSettlers) {
Dictionary<Tile, int> scores = new Dictionary<Tile, int>();
private static Dictionary<Tile, float> AssignTileScores(Tile startTile, Player player, IEnumerable<Tile> candidates, List<MapUnit> playerSettlers) {
Dictionary<Tile, float> scores = new();
candidates = candidates.Where(t => !SettlerAlreadyMovingTowardsTile(t, playerSettlers) && t.IsAllowCities());
foreach (Tile t in candidates) {
int score = GetTileYieldScore(t, player);
float score = GetTileYieldScore(t, player);
//For simplicity's sake, I'm only going to look at immediate neighbors here, but
//a lot more things should be considered over time.
foreach (Tile nt in t.neighbors.Values) {
Expand All @@ -49,17 +43,17 @@ private static Dictionary<Tile, int> AssignTileScores(Tile startTile, Player pla

//Prefer hills for defense, and coast for boats and such.
if (t.baseTerrainType.Key == "hills") {
score += 10;
score += player.civilization.Adjustments.HillsBonus;
}
if (t.NeighborsWater()) {
score += 10;
score += player.civilization.Adjustments.WaterBonus;
}

//Lower scores if they are far away
int preDistanceScore = score;
float preDistanceScore = score;
int distance = startTile.distanceTo(t);
if (distance > 4) {
score -= distance * 2;
if (distance > player.civilization.Adjustments.DistancePenaltyRadius) {
score += player.civilization.Adjustments.DistancePenalty(distance);
}
if (distance > 8) {
score -= distance * 4;
Expand All @@ -75,15 +69,15 @@ private static Dictionary<Tile, int> AssignTileScores(Tile startTile, Player pla
return scores;
}

private static int GetTileYieldScore(Tile t, Player owner) {
int score = t.foodYield(owner).yield * 5;
score += t.productionYield(owner).yield * 3;
score += t.commerceYield(owner).yield * 2;
private static float GetTileYieldScore(Tile t, Player owner) {
float score = owner.civilization.Adjustments.FoodYieldBonus(t.foodYield(owner).yield);
score += owner.civilization.Adjustments.ProductionYieldBonus(t.productionYield(owner).yield);
score += owner.civilization.Adjustments.CommerceYieldBonus(t.commerceYield(owner).yield);
if (owner.KnowsAboutResource(t.Resource)) {
if (t.Resource.Category == ResourceCategory.STRATEGIC) {
score += STRATEGIC_RESOURCE_BONUS;
score += owner.civilization.Adjustments.StrategicResourceBonus;
} else if (t.Resource.Category == ResourceCategory.LUXURY) {
score += LUXURY_RESOURCE_BONUS;
score += owner.civilization.Adjustments.LuxuryResourceBonus;
}
}
return score;
Expand Down
15 changes: 15 additions & 0 deletions C7Engine/C7GameData/Civilization.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;

Expand Down Expand Up @@ -49,6 +50,20 @@ public Civilization(string name) {

public bool isBarbarian = false;

public class SettlerTileAdjustments {
public int DistancePenaltyRadius = 4;
public Func<float, float> CommerceYieldBonus = yield => yield * 2;
public Func<int, float> DistancePenalty = distance => distance * -2;
public Func<float, float> FoodYieldBonus = yield => yield * 5;
public float HillsBonus = 10;
public float LuxuryResourceBonus = 15;
public Func<float, float> ProductionYieldBonus = yield => yield * 3;
public float StrategicResourceBonus = 20;
public float WaterBonus = 10;
}

public SettlerTileAdjustments Adjustments = new();

[JsonIgnore]
public UnitPrototype uniqueUnit;

Expand Down
Loading