Skip to content
Merged
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
17 changes: 8 additions & 9 deletions C7Engine/AI/StrategicAI/ExpansionPriority.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public override void CalculateWeightAndMetadata(Player player) {
if (player.cities.Count < 2) {
this.calculatedWeight = 1000;
} else {
int score = UtilityCalculations.CalculateAvailableLandScore(player);
float score = UtilityCalculations.CalculateAvailableLandScore(player);
score = ApplyEarlyGameMultiplier(score);
score = ApplyNationTraitMultiplier(score, player);

Expand Down Expand Up @@ -58,18 +58,17 @@ public override string ToString() {
return "ExpansionPriority";
}

private int ApplyEarlyGameMultiplier(int score) {
private float ApplyEarlyGameMultiplier(float score) {
//If it's early game, multiply this score.
//TODO: We haven't implemented the part for "how many turns does the game have?" yet. So this is hard-coded.
int gameTurn = EngineStorage.gameData.turn;
int percentOfGameFinished = (gameTurn * 100) / TEMP_GAME_LENGTH;
if (percentOfGameFinished < EARLY_GAME_CUTOFF) {
score = score * (EARLY_GAME_CUTOFF - percentOfGameFinished) / 5;
}
return score;
var gameTurn = EngineStorage.gameData.turn;
var percentOfGameFinished = (gameTurn * 100) / TEMP_GAME_LENGTH;
var isEarlyGame = percentOfGameFinished < EARLY_GAME_CUTOFF;
var multiplier = isEarlyGame ? (EARLY_GAME_CUTOFF - percentOfGameFinished) / 5.0f : 1.0f;
return score * multiplier;
}

private int ApplyNationTraitMultiplier(int score, Player player) {
private float ApplyNationTraitMultiplier(float score, Player player) {
// TODO: The "Expansionist" trait should give a higher priority to this strategic priority.
return score;
}
Expand Down
15 changes: 7 additions & 8 deletions C7Engine/AI/StrategicAI/UtilityCalculations.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@

using System.Collections.Generic;
using System.Linq;
using C7GameData;

namespace C7Engine.AI.StrategicAI {
Expand All @@ -11,16 +12,14 @@ namespace C7Engine.AI.StrategicAI {
public class UtilityCalculations {

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
private static readonly float TileScoreDivider = 10f; //how much to divide each location's tile score by

public static int CalculateAvailableLandScore(Player player) {
public static float CalculateAvailableLandScore(Player player) {
//Figure out if there's land to settle, and how much
Dictionary<Tile, float> possibleLocations = SettlerLocationAI.GetScoredSettlerCandidates(player.cities[0].location, player);
int score = possibleLocations.Count * PossibleCityLocationScore;
foreach (int i in possibleLocations.Values) {
score += i / TileScoreDivider;
}
return score;
var possibleLocations = SettlerLocationAI.GetScoredSettlerCandidates(player.cities[0].location, player);
var availableLand = possibleLocations.Count * PossibleCityLocationScore;
var settlementQuality = possibleLocations.Values.Sum(i => i / TileScoreDivider);
return settlementQuality + availableLand;
}
}
}
2 changes: 1 addition & 1 deletion C7Engine/AI/StrategicAI/WarPriority.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public override void CalculateWeightAndMetadata(Player player) {
}
}

bool outOfLandToExpandTo = UtilityCalculations.CalculateAvailableLandScore(player) == 0;
bool outOfLandToExpandTo = UtilityCalculations.CalculateAvailableLandScore(player) < 1;

// Don't go to war if there's still land we should be expanding to.
if (!outOfLandToExpandTo) {
Expand Down
39 changes: 30 additions & 9 deletions C7Engine/AI/UnitAI/SettlerLocationAI.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using C7GameData;
using System.Collections.Generic;
using System.Linq;
Expand Down Expand Up @@ -27,15 +28,27 @@ public static Dictionary<Tile, float> GetScoredSettlerCandidates(Tile start, Pla

private static Dictionary<Tile, float> AssignTileScores(Tile startTile, Player player, IEnumerable<Tile> candidates, List<MapUnit> playerSettlers) {
Dictionary<Tile, float> scores = new();
var memo = new Dictionary<string, float>();

candidates = candidates.Where(t => !SettlerAlreadyMovingTowardsTile(t, playerSettlers) && t.IsAllowCities());

foreach (Tile t in candidates) {
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) {
score += GetTileYieldScore(nt, player);
float score = GetTileYieldScore(t, player, memo);

// Consider all tiles within the BFC for total score.
// Score contribution decreases linearly with distance, by 1/R with each step:
// e.g., with four ranks of workable tiles, R=4:
// city | 100% | 75% | 50% | 25% | 0% | 0% | ..
var maxRank = player.rules.MaxRankOfWorkableTiles;
foreach (Tile workable in t.GetTilesWithinRankDistance(maxRank)) {
if (workable == Tile.NONE)
continue;
var rank = t.rankDistanceTo(workable);
if (rank <= 0)
continue;
var adjustment = Math.Max(0, (maxRank - rank + 1f) / maxRank);
score += GetTileYieldScore(workable, player, memo) * adjustment;
}
//TODO #802: Also look at the next ring out, with lower weights.

//Prefer hills for defense, and coast for boats and such.
if (t.baseTerrainType.Key == "hills") {
Expand All @@ -45,6 +58,9 @@ private static Dictionary<Tile, float> AssignTileScores(Tile startTile, Player p
score += player.civilization.Adjustments.WaterBonus;
}

// Let defensibility play a role
score += (float)t.baseTerrainType.defenseBonus.amount * 20.0f;

//Lower scores if they are far away
float preDistanceScore = score;
int distance = startTile.distanceTo(t);
Expand All @@ -63,7 +79,11 @@ private static Dictionary<Tile, float> AssignTileScores(Tile startTile, Player p
return scores;
}

private static float GetTileYieldScore(Tile t, Player owner) {
private static float GetTileYieldScore(Tile t, Player owner, Dictionary<string, float> memo) {
var key = $"Tile_{t.XCoordinate}_{t.YCoordinate}";
if (memo.TryGetValue(key, out var value))
return value;

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;
Expand All @@ -74,13 +94,14 @@ private static float GetTileYieldScore(Tile t, Player owner) {
score += owner.civilization.Adjustments.LuxuryResourceBonus;
}
}

memo[key] = score;
return score;
}

private static bool IsInvalidCityLocation(Tile tile) {
if (tile.HasCity) {
if (tile == Tile.NONE || tile.HasCity)
return true;
}
foreach (Tile neighbor in tile.neighbors.Values) {
if (neighbor.HasCity) {
return true;
Expand Down
9 changes: 7 additions & 2 deletions EngineTests/AI/UnitAI/SettlerLocationTest.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using System;
using C7Engine;
using C7GameData;
using C7GameData.AIData;
using C7GameData.Save;
using EngineTests.Utils;
using System.Collections.Generic;
using System.Linq;
using Xunit;

namespace EngineTests.AI.UnitAI {
Expand Down Expand Up @@ -83,20 +85,23 @@ private void CloseOverFar() {

// a single plains tile surrounded by desert
// not a good settlement spot, but our settler is already on this tile
InitilizeStartTile(MakePlainsTile(), new TileLocation(50, 50));
InitilizeStartTile(MakePlainsTile(), new TileLocation(25, 25));
Tile close = startTile;
map.AddRange(SurroundTile(close, MakeDesertTileWithDefaultYield));

// a hill tile surrounded by flood plains
// high settlement score from yield but very far away
InitilizeStartTile(MakeHillTile(), new TileLocation(200, 50));
InitilizeStartTile(MakeHillTile(), new TileLocation(200, 25));
Tile far = startTile;
map.AddRange(SurroundTile(far, MakeFloodPlainTileWithDefaultYield));

InitPartialGameMap(250, 50, map);

Player player = MakeTestPlayer(map);
Tile chosenTile = SettlerLocationAI.FindSettlerLocation(close, player);
Assert.Equal(close, chosenTile);
}

[Fact]
private void NotAlreadyBeingSettled() {
// just one hill tile
Expand Down
24 changes: 24 additions & 0 deletions EngineTests/Utils/MapBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,17 @@ protected Player MakeTestPlayer(List<Tile> knownTiles) {
Player player = MakePlayer(true);
player.civilization = new Civilization();
player.government = new Government();
player.rules = MakeTestRules();
foreach (Tile tile in knownTiles) { player.tileKnowledge.knownTiles.Add(tile); }
return player;
}

protected static Rules MakeTestRules() {
return new Rules() {
MaxRankOfWorkableTiles = 2
};
}

private TileDirection[] directions = {
TileDirection.NORTH,
TileDirection.NORTHEAST,
Expand Down Expand Up @@ -207,4 +214,21 @@ protected Tile MakeOceanTile() {
overlayTerrainType = new() { Key = "ocean", movementCost = 1 }
};
}

/// <summary>
/// Given game map dimensions in tiles and an incomplete set of tiles, fill the
/// game map with NONE tiles, and then overlay the known tiles.
/// </summary>
/// <param name="tilesWidth"></param>
/// <param name="tilesHeight"></param>
/// <param name="tiles"></param>
protected void InitPartialGameMap(int tilesWidth, int tilesHeight, List<Tile> tiles) {
gameMap.numTilesWide = tilesWidth;
gameMap.numTilesTall = tilesHeight;
gameMap.tiles = Enumerable.Range(0, tilesWidth * tilesHeight).Select(x => Tile.NONE).ToList();
foreach (var tile in tiles) {
var idx = gameMap.tileCoordsToIndex(tile.XCoordinate, tile.YCoordinate);
gameMap.tiles[idx] = tile;
}
}
}
Loading