Skip to content

Improve settler location AI#913

Merged
ajhalme merged 5 commits intoC7-Game:Developmentfrom
ajhalme:improve-settler-location-ai
Mar 18, 2026
Merged

Improve settler location AI#913
ajhalme merged 5 commits intoC7-Game:Developmentfrom
ajhalme:improve-settler-location-ai

Conversation

@ajhalme
Copy link
Contributor

@ajhalme ajhalme commented Mar 8, 2026

Basically just salvaging some ideas from #430 to improve the SettlerLocationAI with tile improvement metrics.

@ajhalme ajhalme changed the title Improve settler location ai Improve settler location AI Mar 8, 2026
@ajhalme ajhalme force-pushed the improve-settler-location-ai branch from 94a689c to 0935557 Compare March 8, 2026 01:36
@ajhalme ajhalme marked this pull request as draft March 8, 2026 01:42
@ajhalme ajhalme force-pushed the improve-settler-location-ai branch 3 times, most recently from 087cd95 to a1c3ce3 Compare March 9, 2026 00:34
@ajhalme ajhalme force-pushed the improve-settler-location-ai branch from a1c3ce3 to 3bf9a5a Compare March 9, 2026 00:36
@ajhalme
Copy link
Contributor Author

ajhalme commented Mar 9, 2026

The more I read the SettlerLocationAI code, the less it seems like #430 has to add here.

Only thing that isn't really here is the potential food/production yield from a tile, but that's influenced by game rules on terrain improvement and player characteristics, etc. - an expensive calculation for not that much gain.

As such, I'm reducing this mainly to adding scaled contributions from the BFC outer ring to the existing tile scoring logic, as called for in #802.

Something like a quadratic penalty from distance could be an improvement over the current logic.

I believe this now closes #802. I don't think #430 has anything more to add either.

@ajhalme ajhalme marked this pull request as ready for review March 9, 2026 00:48
Copy link
Contributor

@stavrosfa stavrosfa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I only have one comment, as I wasn't really involved in any of this, so I can't really provide any usefull insight.


private List<Tile> _outerRing;

public List<Tile> GetBFCOuterRing() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I have some reservations regarding this.

The BFC is not necessarily up to 2 tiles, as we have made this configurable in the rules

public int MaxRankOfWorkableTiles;

So I think it would be preferable if we added another method like

public List<Tile> GetTilesWithinRankDistance(int rank) {

excluding anything but the rank we want, to get the tiles

Unless this method is doing something extra which I am not immediatly getting.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced GetBFCOuterRing(..) with GetTilesWithinRankDistance(..). Made the distance-based score adjustment linear so tiles up to maximum workable rank have stepwise diminishing influence over the target tile, with tiles beyond the max rank contributing nothing.

Turned out to be a bit of a hassle, as the implementation of the GetTilesWithinRankDistance makes some assumptions about the tile init that the existing test setup doesn't cover. I patched the tests with some extra setup to make the function work.

Also spotted an opportunity to do a bit of memoization to speed up the calculation: once scored, a tile's score can easily be reused when scoring neighboring tiles.

Copy link
Contributor

@stavrosfa stavrosfa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks good to me. I just have a question, feel free to merge

// 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 = EngineStorage.gameData?.rules?.MaxRankOfWorkableTiles ?? 2;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the null checks here? Did you come by an instance where any of these were null? If so I think we might have a bigger problem

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the EngineStorage part in, as that was how I saw MaxRankOfWorkableTiles being used. Using this global state is problematic for a a number of reasons, but the null stuff here is just so the test suite can run the function.

There's an easy fix, though, as the Player object carries the game's rules. I expanded the test suite to generate the missing context and the tests are now happy. There's no longer any dependency on EngineStorage.

Indeed, to make testing of all kinds easier across the codebase, I feel that the use of EngineStorage should be removed where it isn't absolutely essential.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I saw that the Player carries the rules the other day, this seems super weird to me, but anyway.

Regarding the EngineStorage stuff, we could theoretically do the following:

GameData gameData = new GameData();
EngineStorage.InitializeGameDataForTests(gameData); <- this is an existing method in EngineStorage (and is I guess just a fancy way of doing EngineStorage.gameData = gameData; )

add the rules like you 've done here

I don't know what happens after a test has finished, I haven't really thought about it untill now, but at the very end we could also do this for some cleanup

EngineStorage.gameData = null;

That's all in case we want to keep the previous format, though I don't know how strongly you feel against EngineStorage.
If you want to elaborate on that, I 'd love to hear your thoughts and also how it can be improved.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I think it's reasonable to have the rules in the Player object. The Game object is of course the natural home for the rules, but if the player is the main argument passed around to both engine functions and map/presentation related things, having the rules there is handy. Just like here in the test case. The player could know what game is being played, and then make use of game->rules, but the current setup is fine. I can even imagine player-specific rules or behaviours, where rules really are more naturally a property of the player.

I agree that something like what you are suggesting could work for test harnesses, but I would still refrain from using the EngineStorage global state as much as possible. It's much nicer to have static functions where all the inputs are defined in the arguments.

That said, games are one of those software domains where relying on global state sometimes is the least bad option available. I'm a big fan of C#'s interfaces and various dependency injection patterns, etc., but we make do with what's available.

@ajhalme ajhalme merged commit d9dea6f into C7-Game:Development Mar 18, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants