From 76ef1fdb65778fb98b486e035deb9c0904b6b51e Mon Sep 17 00:00:00 2001 From: Antti Halme Date: Fri, 27 Feb 2026 01:16:46 +0000 Subject: [PATCH 1/7] Add barbarian activity buttons --- C7/UIElements/NewGame/WorldSetup.cs | 44 +++++++++++++++++++++++++- C7/UIElements/NewGame/world_setup.tscn | 22 ++++++++++++- C7Engine/MapGenerator.cs | 9 ++++++ 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/C7/UIElements/NewGame/WorldSetup.cs b/C7/UIElements/NewGame/WorldSetup.cs index 26105c3f..010a3dc7 100644 --- a/C7/UIElements/NewGame/WorldSetup.cs +++ b/C7/UIElements/NewGame/WorldSetup.cs @@ -1,5 +1,6 @@ using Godot; using System; +using System.Linq; using C7Engine; using C7Engine.Lua; using C7GameData; @@ -64,12 +65,14 @@ public partial class WorldSetup : Control { [Export] LineEdit seedInput; [Export] VBoxContainer worldSizeButtonsContainer; + [Export] VBoxContainer barbActivityButtonsContainer; WorldCharacteristics.Landform landform = WorldCharacteristics.Landform.Pangaea; WorldCharacteristics.OceanCoverage ocean = WorldCharacteristics.OceanCoverage.Percent_70; WorldCharacteristics.Age age = WorldCharacteristics.Age.Billion_4; WorldCharacteristics.Temperature temp = WorldCharacteristics.Temperature.Temperate; WorldCharacteristics.Climate clim = WorldCharacteristics.Climate.Normal; + WorldCharacteristics.BarbarianActivity _barbarianActivity = WorldCharacteristics.BarbarianActivity.Roaming; private WorldSize _worldSize = WorldSize.Generic(); @@ -270,9 +273,47 @@ public override void _Ready() { _saveGame = GameModeLoader.Load(GamePaths.GameModesDir, GamePaths.GameMode); + InitBarbarianActivityOptions(); InitMapSizes(); } + private void InitBarbarianActivityOptions() { + var barbRandom = new Random(); + var barbDefault = WorldCharacteristics.BarbarianActivity.Roaming; + var barbOptions = Enum.GetValues().OrderBy(x => x).ToList(); + + var barbActivityButtonGroup = new ButtonGroup() { ResourceName = "BarbActivityButtonGroup" }; + var randomSizeButton = new Civ3MenuButton + { + Text = "Random", + textPosition = Civ3MenuButton.TextPosition.TextRightOfIcon, + FontSize = 0, + ButtonGroup = barbActivityButtonGroup, + ToggleMode = true + }; + randomSizeButton.Pressed += () => { + _barbarianActivity = barbOptions[barbRandom.Next(barbOptions.Count)]; + }; + + // Dynamically create a new button for each barbarian activity option + foreach (var ba in barbOptions) { + var barbActivityButton = new Civ3MenuButton + { + Text = ba.ToString("G"), + textPosition = Civ3MenuButton.TextPosition.TextRightOfIcon, + FontSize = 0, + ButtonGroup = barbActivityButtonGroup, + ToggleMode = true, + ButtonPressed = ba == barbDefault + }; + barbActivityButton.Pressed += () => _barbarianActivity = ba; + barbActivityButtonsContainer.AddChild(barbActivityButton); + } + + // Append random as last in the list + barbActivityButtonsContainer.AddChild(randomSizeButton); + } + private void InitMapSizes() { var sizeRandom = new Random(); @@ -311,7 +352,7 @@ private void InitMapSizes() { _worldSize = ws; } - // Move random as last in the list and drop default map option and + // Append random as last in the list worldSizeButtonsContainer.AddChild(randomSizeButton); } catch (Exception ex) { log.Warning(ex, "Failed to load map sizes from game mode."); @@ -363,6 +404,7 @@ private void CreateGame() { climate = clim, temperature = temp, worldSize = _worldSize, + barbarianActivity = _barbarianActivity, mapSeed = GameSeed, }; diff --git a/C7/UIElements/NewGame/world_setup.tscn b/C7/UIElements/NewGame/world_setup.tscn index d6d38741..77c258ac 100644 --- a/C7/UIElements/NewGame/world_setup.tscn +++ b/C7/UIElements/NewGame/world_setup.tscn @@ -36,7 +36,7 @@ keycode = 4194305 [sub_resource type="Shortcut" id="Shortcut_7oyjg"] events = [SubResource("InputEventKey_klcu1")] -[node name="Control" type="CenterContainer" node_paths=PackedStringArray("background", "pangaeaLabel", "continentsLabel", "archipelagoLabel", "pangaea60", "pangaea70", "pangaea80", "continents60", "continents70", "continents80", "archipelago60", "archipelago70", "archipelago80", "pangaea60Large", "pangaea70Large", "pangaea80Large", "continents60Large", "continents70Large", "continents80Large", "archipelago60Large", "archipelago70Large", "archipelago80Large", "arid", "normal", "wet", "cool", "temperate", "warm", "billion3", "billion4", "billion5", "aridLarge", "normalLarge", "wetLarge", "coolLarge", "temperateLarge", "warmLarge", "billion3Large", "billion4Large", "billion5Large", "confirm", "cancel", "seedInput", "worldSizeButtonsContainer")] +[node name="Control" type="CenterContainer" node_paths=PackedStringArray("background", "pangaeaLabel", "continentsLabel", "archipelagoLabel", "pangaea60", "pangaea70", "pangaea80", "continents60", "continents70", "continents80", "archipelago60", "archipelago70", "archipelago80", "pangaea60Large", "pangaea70Large", "pangaea80Large", "continents60Large", "continents70Large", "continents80Large", "archipelago60Large", "archipelago70Large", "archipelago80Large", "arid", "normal", "wet", "cool", "temperate", "warm", "billion3", "billion4", "billion5", "aridLarge", "normalLarge", "wetLarge", "coolLarge", "temperateLarge", "warmLarge", "billion3Large", "billion4Large", "billion5Large", "confirm", "cancel", "seedInput", "worldSizeButtonsContainer", "barbActivityButtonsContainer")] anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 @@ -87,6 +87,7 @@ confirm = NodePath("Background/Confirm") cancel = NodePath("Background/Cancel") seedInput = NodePath("Background/SeedInput") worldSizeButtonsContainer = NodePath("Background/WorldSizeButtonsScroller/WorldSizeButtonsContainer") +barbActivityButtonsContainer = NodePath("Background/BarbActivityScroller/BarbActivityButtonsContainer") [node name="Background" type="TextureRect" parent="."] layout_mode = 2 @@ -102,6 +103,15 @@ theme_override_font_sizes/font_size = 16 text = "WORLD SIZE" horizontal_alignment = 1 +[node name="Label" type="Label" parent="Background/Label"] +offset_left = 623.0 +offset_top = -1.0 +offset_right = 789.0 +offset_bottom = 22.0 +theme_override_font_sizes/font_size = 16 +text = "BARBARIANS" +horizontal_alignment = 1 + [node name="Label2" type="Label" parent="Background"] layout_mode = 0 offset_left = 114.0 @@ -648,3 +658,13 @@ offset_bottom = 256.0 [node name="WorldSizeButtonsContainer" type="VBoxContainer" parent="Background/WorldSizeButtonsScroller"] layout_mode = 2 + +[node name="BarbActivityScroller" type="ScrollContainer" parent="Background"] +layout_mode = 0 +offset_left = 743.0 +offset_top = 113.0 +offset_right = 901.0 +offset_bottom = 257.0 + +[node name="BarbActivityButtonsContainer" type="VBoxContainer" parent="Background/BarbActivityScroller"] +layout_mode = 2 diff --git a/C7Engine/MapGenerator.cs b/C7Engine/MapGenerator.cs index b4726e70..03558253 100644 --- a/C7Engine/MapGenerator.cs +++ b/C7Engine/MapGenerator.cs @@ -44,6 +44,15 @@ public enum Age { } public Age age; + public enum BarbarianActivity { + None = -1, + Sedentary = 0, + Roaming = 1, + Restless = 2, + Raging = 3 + } + public BarbarianActivity barbarianActivity; + public int mapSeed = -1; public WorldSize worldSize; public List terrainTypes; From de718b3e751fce3a37b39c6108aac9b2652e5171 Mon Sep 17 00:00:00 2001 From: Antti Halme Date: Sat, 28 Feb 2026 22:33:00 +0000 Subject: [PATCH 2/7] Add BarbarianActivity save state --- C7/UIElements/NewGame/WorldSetup.cs | 7 ++++--- C7Engine/C7GameData/BarbarianActivity.cs | 9 +++++++++ C7Engine/C7GameData/BarbarianInfo.cs | 2 ++ C7Engine/C7GameData/ImportCiv3.cs | 8 ++++++++ C7Engine/MapGenerator.cs | 9 ++------- EngineTests/GameData/SaveTest.cs | 1 + 6 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 C7Engine/C7GameData/BarbarianActivity.cs diff --git a/C7/UIElements/NewGame/WorldSetup.cs b/C7/UIElements/NewGame/WorldSetup.cs index 010a3dc7..633d6953 100644 --- a/C7/UIElements/NewGame/WorldSetup.cs +++ b/C7/UIElements/NewGame/WorldSetup.cs @@ -72,7 +72,8 @@ public partial class WorldSetup : Control { WorldCharacteristics.Age age = WorldCharacteristics.Age.Billion_4; WorldCharacteristics.Temperature temp = WorldCharacteristics.Temperature.Temperate; WorldCharacteristics.Climate clim = WorldCharacteristics.Climate.Normal; - WorldCharacteristics.BarbarianActivity _barbarianActivity = WorldCharacteristics.BarbarianActivity.Roaming; + + private BarbarianActivity _barbarianActivity = BarbarianActivity.Roaming; private WorldSize _worldSize = WorldSize.Generic(); @@ -279,8 +280,8 @@ public override void _Ready() { private void InitBarbarianActivityOptions() { var barbRandom = new Random(); - var barbDefault = WorldCharacteristics.BarbarianActivity.Roaming; - var barbOptions = Enum.GetValues().OrderBy(x => x).ToList(); + var barbDefault = BarbarianActivity.Roaming; + var barbOptions = Enum.GetValues().OrderBy(x => x).ToList(); var barbActivityButtonGroup = new ButtonGroup() { ResourceName = "BarbActivityButtonGroup" }; var randomSizeButton = new Civ3MenuButton diff --git a/C7Engine/C7GameData/BarbarianActivity.cs b/C7Engine/C7GameData/BarbarianActivity.cs new file mode 100644 index 00000000..d3e71ff2 --- /dev/null +++ b/C7Engine/C7GameData/BarbarianActivity.cs @@ -0,0 +1,9 @@ +namespace C7GameData; + +public enum BarbarianActivity { + None = -1, + Sedentary = 0, + Roaming = 1, + Restless = 2, + Raging = 3 +} \ No newline at end of file diff --git a/C7Engine/C7GameData/BarbarianInfo.cs b/C7Engine/C7GameData/BarbarianInfo.cs index 65aa5df0..7c7ab7a6 100644 --- a/C7Engine/C7GameData/BarbarianInfo.cs +++ b/C7Engine/C7GameData/BarbarianInfo.cs @@ -17,5 +17,7 @@ public class BarbarianInfo { public int defaultHitpoints = 2; public int maxHitpoints = 2; + + public BarbarianActivity barbarianActivity = BarbarianActivity.Roaming; } } diff --git a/C7Engine/C7GameData/ImportCiv3.cs b/C7Engine/C7GameData/ImportCiv3.cs index dd37bd06..042c4bbf 100644 --- a/C7Engine/C7GameData/ImportCiv3.cs +++ b/C7Engine/C7GameData/ImportCiv3.cs @@ -103,6 +103,8 @@ private SaveGame importSav(string savePath, string defaultBicPath, Func 0) { diff --git a/C7Engine/MapGenerator.cs b/C7Engine/MapGenerator.cs index 03558253..386ea0e1 100644 --- a/C7Engine/MapGenerator.cs +++ b/C7Engine/MapGenerator.cs @@ -44,13 +44,6 @@ public enum Age { } public Age age; - public enum BarbarianActivity { - None = -1, - Sedentary = 0, - Roaming = 1, - Restless = 2, - Raging = 3 - } public BarbarianActivity barbarianActivity; public int mapSeed = -1; @@ -71,6 +64,8 @@ public WorldCharacteristics(SaveGame save) { maxRankOfWorkableTiles = save.Rules.MaxRankOfWorkableTiles; maxRankOfBarbarianCampTiles = save.Rules.MaxRankOfBarbarianCampTiles; + + barbarianActivity = save.BarbarianInfo.barbarianActivity; } } diff --git a/EngineTests/GameData/SaveTest.cs b/EngineTests/GameData/SaveTest.cs index b84b845e..689874f1 100644 --- a/EngineTests/GameData/SaveTest.cs +++ b/EngineTests/GameData/SaveTest.cs @@ -77,6 +77,7 @@ private static SaveGame LoadSave(GameModeConfig gameModeConfig) { age = WorldCharacteristics.Age.Billion_4, climate = WorldCharacteristics.Climate.Normal, temperature = WorldCharacteristics.Temperature.Temperate, + barbarianActivity = BarbarianActivity.Roaming, worldSize = worldSize, mapSeed = TestSeed, }; From cd513f493180c3d6bf1ab23c6268f4a54cb20706 Mon Sep 17 00:00:00 2001 From: Antti Halme Date: Sun, 1 Mar 2026 00:55:15 +0000 Subject: [PATCH 3/7] Barbarian AI strategies --- C7Engine/AI/BarbarianAI.cs | 104 +++++------------- C7Engine/AI/BarbarianStrategy/BaseStrategy.cs | 96 ++++++++++++++++ .../BarbarianStrategy/IBarbarianStrategy.cs | 8 ++ .../AI/BarbarianStrategy/RagingStrategy.cs | 17 +++ .../AI/BarbarianStrategy/RestlessStrategy.cs | 17 +++ .../AI/BarbarianStrategy/RoamingStrategy.cs | 18 +++ .../AI/BarbarianStrategy/SedentaryStrategy.cs | 17 +++ C7Engine/AI/PlayerAI.cs | 4 +- C7Engine/C7GameData/BarbarianActivity.cs | 2 +- C7Engine/C7GameData/ImportCiv3.cs | 4 +- C7Engine/EntryPoints/TurnHandling.cs | 7 +- C7Engine/GameSetup.cs | 3 +- 12 files changed, 211 insertions(+), 86 deletions(-) create mode 100644 C7Engine/AI/BarbarianStrategy/BaseStrategy.cs create mode 100644 C7Engine/AI/BarbarianStrategy/IBarbarianStrategy.cs create mode 100644 C7Engine/AI/BarbarianStrategy/RagingStrategy.cs create mode 100644 C7Engine/AI/BarbarianStrategy/RestlessStrategy.cs create mode 100644 C7Engine/AI/BarbarianStrategy/RoamingStrategy.cs create mode 100644 C7Engine/AI/BarbarianStrategy/SedentaryStrategy.cs diff --git a/C7Engine/AI/BarbarianAI.cs b/C7Engine/AI/BarbarianAI.cs index bcc237ab..f88682bd 100644 --- a/C7Engine/AI/BarbarianAI.cs +++ b/C7Engine/AI/BarbarianAI.cs @@ -12,95 +12,47 @@ namespace C7Engine { using System; using System.Threading.Tasks; - public class BarbarianAI { + // TODO: The AI state (plans, strategy, ..) should be stored somewhere in game state. + // For now, we have a stateless random AI. - private ILogger log = Log.ForContext(); + public static class BarbarianAI { - public async Task PlayTurn(Player player, GameData gameData) { + private static ILogger log = Log.ForContext(); + + public static async Task PlayTurn(Player player, GameData gameData) { if (!player.isBarbarians) { - throw new System.Exception("Barbarian AI can only play barbarian players"); + throw new Exception("Barbarian AI can only play barbarian players"); } - foreach (MapUnit unit in player.units.ToArray()) { - // Make the barbarians wake up if they see a unit or a civ's - // borders. This will happen each turn, so eventually the barb - // should muster the courage to attack. - foreach (Tile t in unit.location.neighbors.Values) { - if (t.unitsOnTile.Count > 0 && t.unitsOnTile[0].owner != player) { - unit.wake(); - break; - } - if (t.OwningPlayer() != null) { - unit.wake(); - break; - } - } - - // Don't waste time recalculating behaviors for fortified units. - if (unit.isFortified) { - continue; - } - - // For each unit, if there's already an AI task assigned, it will attempt to complete its goal. - // It may fail due to conditions having changed since that goal was assigned; in that case it will - // get a new task to try to complete. - // - // Cap our attempts at 2 to avoid getting stuck in bad situations. - for (int attempt = 0; attempt < 2; ++attempt) { - if (unit.currentAI == null) { - unit.currentAI = GetAIForUnit(unit, player); - } - - // If the unit is still the process of doing its plan, allow - // it to continue next turn. - UnitAI.Result result = await unit.currentAI.PlayTurn(player, unit); - if (result == UnitAI.Result.InProgress) { - break; - } - - if (result == UnitAI.Result.Error) { - unit.currentAI = null; - break; - } + if (gameData.barbarianInfo.barbarianActivity == BarbarianActivity.None) + return; - if (unit.hitPointsRemaining <= 0 || unit.isFortified) { - unit.currentAI = null; - break; - } + var strategy = SelectStrategy(gameData.barbarianInfo.barbarianActivity); - // Otherwise we need a new plan for next turn. Pick it now - // to avoid things like new units being preferred for - // exploration instead of units already far away from home - // for exploration. - unit.currentAI = GetAIForUnit(unit, player); - } + // TODO: Band units into tribes, decide at the tribe level --> work together + foreach (MapUnit unit in player.units.ToArray()) { + await strategy.PlayUnitTurn(player, unit); player.tileKnowledge.AddTilesToKnown(unit.location); } } - public static UnitAI GetAIForUnit(MapUnit unit, Player player) { - // Barbarians should always defend their camp if it is unguarded. - if (unit.location.hasBarbarianCamp && unit.location.unitsOnTile.Count == 1) { - return new DefenderAI(DefenderAI.MakeAiDataForDefendInPlace(unit, player)); - } - - // If the barbarian can fight, it should. - CombatAIData maybeCombat = CombatAI.MakeAiData(unit, player); - if (maybeCombat != null) { - return new CombatAI(maybeCombat); + private static IBarbarianStrategy SelectStrategy(BarbarianActivity barbarianActivity) { + switch (barbarianActivity) { + case BarbarianActivity.None: + throw new Exception("Cannot select an AI strategy for BarbarianActivity 'None'."); + case BarbarianActivity.Sedentary: + return new SedentaryStrategy(); + case BarbarianActivity.Roaming: + return new RoamingStrategy(); + case BarbarianActivity.Restless: + return new RestlessStrategy(); + case BarbarianActivity.Raging: + return new RagingStrategy(); + default: + log.Warning("Unknown BarbarianActivity. Defaulting to Sedentary."); + return new SedentaryStrategy(); } - - // Give barbarians a chance to explore if they can't fight. - if (GameData.rng.Next(100) < 30) { - ExplorerAIData? maybeAiData = ExplorerAI.MaybeMakeAiData(unit, player); - if (maybeAiData != null) { - return new ExplorerAI(maybeAiData); - } - } - - // Otherwise just sit tight. - return new DefenderAI(DefenderAI.MakeAiDataForDefendInPlace(unit, player)); } } } diff --git a/C7Engine/AI/BarbarianStrategy/BaseStrategy.cs b/C7Engine/AI/BarbarianStrategy/BaseStrategy.cs new file mode 100644 index 00000000..a4f40ced --- /dev/null +++ b/C7Engine/AI/BarbarianStrategy/BaseStrategy.cs @@ -0,0 +1,96 @@ +using System.Threading.Tasks; +using C7Engine.AI.UnitAI; +using C7GameData; +using C7GameData.AIData; + +namespace C7Engine; + +internal abstract class BaseStrategy : IBarbarianStrategy { + /// + /// Observe - Orient - Decide - Act + /// + public async Task PlayUnitTurn(Player player, MapUnit unit) { + // "Observe: Collect data and information from the environment through senses and feedback." + + // Wake up the unit if there's a reason to do so + if (ShouldWake(player, unit)) + unit.wake(); + + // Skip units that didn't wake up + if (unit.isFortified) + return; + + var orientation = await Orient(player, unit); + var plan = await Decide(player, unit, orientation); + var result = await Act(player, unit, plan); + + // TODO: store result + } + + /// + /// Wake the unit if a foreign unit or the borders of a civ are in sight. + /// + private static bool ShouldWake(Player player, MapUnit unit) { + foreach (Tile t in unit.location.neighbors.Values) { + if (t.unitsOnTile.Count > 0 && t.unitsOnTile[0].owner != player) + return true; + + if (t.OwningPlayer() != null) + return true; + } + + return false; + } + + /// + /// "Orient: Analyze and synthesize data to form a mental perspective, considering experience, + /// culture, and new information. This is considered the most important phase of the OODA loop." + /// + protected Task Orient(Player player, MapUnit unit) { + return Task.FromResult(new Orientation { + IsLastUnitInCamp = unit.location.hasBarbarianCamp && unit.location.unitsOnTile.Count == 1, + CombatIntel = CombatAI.MakeAiData(unit, player) + }); + } + + /// + /// "Decide: Formulate a plan or course of action based on the orientation." + /// + protected async Task Decide(Player player, MapUnit unit, Orientation orientation) { + // Barbarians defend their camp if it is unguarded. + if (orientation.IsLastUnitInCamp) + return new DefenderAI(DefenderAI.MakeAiDataForDefendInPlace(unit, player)); + + // Decide whether to engage enemy units + if (orientation.CanEngage() && DecideToEngage(player, unit, orientation)) + return new CombatAI(orientation.CombatIntel); + + // Decide whether to explore + if (DecideToExplore(player, unit, orientation)) { + var maybeAiData = ExplorerAI.MaybeMakeAiData(unit, player); + if (maybeAiData != null) + return new ExplorerAI(maybeAiData); + } + + // Defend otherwise + return new DefenderAI(DefenderAI.MakeAiDataForDefendInPlace(unit, player)); + } + + /// + /// "Act: Implement the decision, which creates new data and feeds back into the observation phase." + /// + protected async Task Act(Player player, MapUnit unit, UnitAI plan) { + return await plan.PlayTurn(player, unit); + } + + internal class Orientation { + public bool IsLastUnitInCamp { get; set; } + public CombatAIData CombatIntel { get; set; } + + public bool CanEngage() => CombatIntel != null; + } + + protected abstract bool DecideToEngage(Player player, MapUnit unit, Orientation orientation); + + protected abstract bool DecideToExplore(Player player, MapUnit unit, Orientation orientation); +} diff --git a/C7Engine/AI/BarbarianStrategy/IBarbarianStrategy.cs b/C7Engine/AI/BarbarianStrategy/IBarbarianStrategy.cs new file mode 100644 index 00000000..9737e11a --- /dev/null +++ b/C7Engine/AI/BarbarianStrategy/IBarbarianStrategy.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; +using C7GameData; + +namespace C7Engine; + +internal interface IBarbarianStrategy { + Task PlayUnitTurn(Player player, MapUnit unit); +} diff --git a/C7Engine/AI/BarbarianStrategy/RagingStrategy.cs b/C7Engine/AI/BarbarianStrategy/RagingStrategy.cs new file mode 100644 index 00000000..deda507f --- /dev/null +++ b/C7Engine/AI/BarbarianStrategy/RagingStrategy.cs @@ -0,0 +1,17 @@ +using C7GameData; + +namespace C7Engine; + +/// +/// Civ3 Manual - Raging: +/// You asked for it! The world is full of barbarians,and they appear in large numbers. +/// +internal class RagingStrategy : BaseStrategy { + protected override bool DecideToEngage(Player player, MapUnit unit, Orientation orientation) { + return true; + } + + protected override bool DecideToExplore(Player player, MapUnit unit, Orientation orientation) { + return GameData.rng.Next(100) < 50; + } +} diff --git a/C7Engine/AI/BarbarianStrategy/RestlessStrategy.cs b/C7Engine/AI/BarbarianStrategy/RestlessStrategy.cs new file mode 100644 index 00000000..423d2160 --- /dev/null +++ b/C7Engine/AI/BarbarianStrategy/RestlessStrategy.cs @@ -0,0 +1,17 @@ +using C7GameData; + +namespace C7Engine; + +/// +/// Civ3 Manual - Restless: +/// Barbarians appear in moderate up to significant numbers, at shorter intervals than at lower levels. +/// +internal class RestlessStrategy : BaseStrategy { + protected override bool DecideToEngage(Player player, MapUnit unit, Orientation orientation) { + return GameData.rng.Next(100) < 75; + } + + protected override bool DecideToExplore(Player player, MapUnit unit, Orientation orientation) { + return GameData.rng.Next(100) < 75; + } +} diff --git a/C7Engine/AI/BarbarianStrategy/RoamingStrategy.cs b/C7Engine/AI/BarbarianStrategy/RoamingStrategy.cs new file mode 100644 index 00000000..e4f206b9 --- /dev/null +++ b/C7Engine/AI/BarbarianStrategy/RoamingStrategy.cs @@ -0,0 +1,18 @@ +using C7GameData; + +namespace C7Engine; + +/// +/// Civ3 Manual - Roaming: +/// Barbarian settlements occasionally appear, but less frequently and in smaller numbers +/// than at higher levels.This is the standard level of barbarian activity. +/// +internal class RoamingStrategy : BaseStrategy { + protected override bool DecideToEngage(Player player, MapUnit unit, Orientation orientation) { + return GameData.rng.Next(100) < 50; + } + + protected override bool DecideToExplore(Player player, MapUnit unit, Orientation orientation) { + return GameData.rng.Next(100) < 75; + } +} diff --git a/C7Engine/AI/BarbarianStrategy/SedentaryStrategy.cs b/C7Engine/AI/BarbarianStrategy/SedentaryStrategy.cs new file mode 100644 index 00000000..10b9548c --- /dev/null +++ b/C7Engine/AI/BarbarianStrategy/SedentaryStrategy.cs @@ -0,0 +1,17 @@ +using C7GameData; + +namespace C7Engine; + +/// +/// Civ3 Manual - Sedentary: +/// Barbarians are restricted to their encampments. The surrounding terrain is free of their mischief. +/// +internal class SedentaryStrategy : BaseStrategy { + protected override bool DecideToEngage(Player player, MapUnit unit, Orientation orientation) { + return false; // TODO: attack units next to camp? + } + + protected override bool DecideToExplore(Player player, MapUnit unit, Orientation orientation) { + return false; + } +} diff --git a/C7Engine/AI/PlayerAI.cs b/C7Engine/AI/PlayerAI.cs index 9564b348..c36c31b2 100644 --- a/C7Engine/AI/PlayerAI.cs +++ b/C7Engine/AI/PlayerAI.cs @@ -20,10 +20,12 @@ public class PlayerAI { public static readonly int MAX_LAND_EXPLORERS = 10; public static readonly int MAX_WATER_EXPLORERS = 4; - public static async Task PlayTurn(Player player, Random rng, List techs) { + public static async Task PlayTurn(Player player, GameData gameData) { if (player.isHuman || player.isBarbarians) { return; } + List techs = gameData.techs; + Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); log.Information("-> Begin " + player.civilization.cityNames[0] + " turn"); diff --git a/C7Engine/C7GameData/BarbarianActivity.cs b/C7Engine/C7GameData/BarbarianActivity.cs index d3e71ff2..126a98fa 100644 --- a/C7Engine/C7GameData/BarbarianActivity.cs +++ b/C7Engine/C7GameData/BarbarianActivity.cs @@ -6,4 +6,4 @@ public enum BarbarianActivity { Roaming = 1, Restless = 2, Raging = 3 -} \ No newline at end of file +} diff --git a/C7Engine/C7GameData/ImportCiv3.cs b/C7Engine/C7GameData/ImportCiv3.cs index 042c4bbf..9bc1e879 100644 --- a/C7Engine/C7GameData/ImportCiv3.cs +++ b/C7Engine/C7GameData/ImportCiv3.cs @@ -103,7 +103,7 @@ private SaveGame importSav(string savePath, string defaultBicPath, Func PlayPlayerTurns(GameData gameData, bool firstTur } if (player.isBarbarians) { - //Call the barbarian AI - //TODO: The AIs should be stored somewhere on the game state as some of them will store state (plans, - //strategy, etc.) For now, we only have a random AI, so that will be in a future commit - await new BarbarianAI().PlayTurn(player, gameData); + await BarbarianAI.PlayTurn(player, gameData); player.hasPlayedThisTurn = true; } else if (!player.isHuman) { - await PlayerAI.PlayTurn(player, GameData.rng, gameData.techs); + await PlayerAI.PlayTurn(player, gameData); player.hasPlayedThisTurn = true; } else if (player.id != EngineStorage.uiControllerID) { player.hasPlayedThisTurn = true; diff --git a/C7Engine/GameSetup.cs b/C7Engine/GameSetup.cs index 28ce6479..e5753f2d 100644 --- a/C7Engine/GameSetup.cs +++ b/C7Engine/GameSetup.cs @@ -42,7 +42,8 @@ private void PopulatePlayers(SaveGame save) { // Add barbarian AddPlayer(save, save.Civilizations.Find(c => c.isBarbarian), isHuman: false); - + save.BarbarianInfo.barbarianActivity = worldCharacteristics.barbarianActivity; + // Add the human player. AddPlayer(save, this.playerCivilization, isHuman: true); From aa23c99bf65df3f724005317ea75dc5696b5288b Mon Sep 17 00:00:00 2001 From: Antti Halme Date: Sun, 1 Mar 2026 12:18:37 +0000 Subject: [PATCH 4/7] Apply barb activity to unit spawn --- C7Engine/EntryPoints/BarbarianInteractions.cs | 34 +++++++++++++++++-- C7Engine/GameSetup.cs | 2 +- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/C7Engine/EntryPoints/BarbarianInteractions.cs b/C7Engine/EntryPoints/BarbarianInteractions.cs index c46ffeea..2c5d4140 100644 --- a/C7Engine/EntryPoints/BarbarianInteractions.cs +++ b/C7Engine/EntryPoints/BarbarianInteractions.cs @@ -9,10 +9,17 @@ namespace C7Engine; public class BarbarianInteractions { public static int SpawnBarbarians(GameData gameData) { Player barbPlayer = gameData.players.Find(player => player.isBarbarians); + var activity = gameData.barbarianInfo.barbarianActivity; - // A random 5% of camps will spawn a unit each turn. Shuffle the - // camps to make this random. - int barbariansToSpawn = (int)Math.Ceiling(GameData.rng.Next(gameData.map.barbarianCamps.Count) / 20.0); + if (activity == BarbarianActivity.None) + return 0; + + // A random number of camps will spawn a unit each turn. + var spawnRate = DetermineSpawnRate(activity); + var spawnMeasure = spawnRate * GameData.rng.Next(gameData.map.barbarianCamps.Count); + int barbariansToSpawn = (int)Math.Ceiling(spawnMeasure); + + // Make the spawn locations random by shuffling a list of camp indexes List tileIndicies = Enumerable.Range(0, gameData.map.barbarianCamps.Count).ToList(); GameData.rng.Shuffle(CollectionsMarshal.AsSpan(tileIndicies)); @@ -33,6 +40,27 @@ public static int SpawnBarbarians(GameData gameData) { return barbariansToSpawn; } + /// + /// + /// + private static float DetermineSpawnRate(BarbarianActivity activity) { + switch (activity) { + case BarbarianActivity.None: + return 0; + case BarbarianActivity.Sedentary: + return 0.03f; + case BarbarianActivity.Roaming: + return 0.05f; + case BarbarianActivity.Restless: + return 0.08f; + case BarbarianActivity.Raging: + return 0.12f; + default: + throw new ArgumentOutOfRangeException(nameof(activity), activity, null); + } + + } + public static UnitPrototype SelectBarbarianUnitType(BarbarianInfo barbInfo, Tile tile) { // Coastal camps have a 20% chance of spawning a sea unit if (tile.NeighborsWater() && GameData.rng.Next(100) < 20) { diff --git a/C7Engine/GameSetup.cs b/C7Engine/GameSetup.cs index e5753f2d..18008629 100644 --- a/C7Engine/GameSetup.cs +++ b/C7Engine/GameSetup.cs @@ -43,7 +43,7 @@ private void PopulatePlayers(SaveGame save) { // Add barbarian AddPlayer(save, save.Civilizations.Find(c => c.isBarbarian), isHuman: false); save.BarbarianInfo.barbarianActivity = worldCharacteristics.barbarianActivity; - + // Add the human player. AddPlayer(save, this.playerCivilization, isHuman: true); From 4bdbff51c61d7acc25c4f349db012d2d1cfc0552 Mon Sep 17 00:00:00 2001 From: Antti Halme Date: Wed, 4 Mar 2026 00:09:20 +0000 Subject: [PATCH 5/7] Make use of GetTilesVisibleToUnit on barb waking --- C7Engine/AI/BarbarianStrategy/BaseStrategy.cs | 6 ++++-- C7Engine/C7GameData/AIData/TileKnowledge.cs | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/C7Engine/AI/BarbarianStrategy/BaseStrategy.cs b/C7Engine/AI/BarbarianStrategy/BaseStrategy.cs index a4f40ced..dd135396 100644 --- a/C7Engine/AI/BarbarianStrategy/BaseStrategy.cs +++ b/C7Engine/AI/BarbarianStrategy/BaseStrategy.cs @@ -30,8 +30,10 @@ public async Task PlayUnitTurn(Player player, MapUnit unit) { /// /// Wake the unit if a foreign unit or the borders of a civ are in sight. /// - private static bool ShouldWake(Player player, MapUnit unit) { - foreach (Tile t in unit.location.neighbors.Values) { + private static bool ShouldWake(Player player, MapUnit unit) + { + var tiles = player.tileKnowledge.GetTilesVisibleToUnit(unit.location); + foreach (Tile t in tiles) { if (t.unitsOnTile.Count > 0 && t.unitsOnTile[0].owner != player) return true; diff --git a/C7Engine/C7GameData/AIData/TileKnowledge.cs b/C7Engine/C7GameData/AIData/TileKnowledge.cs index 47abc1c4..d80381c8 100644 --- a/C7Engine/C7GameData/AIData/TileKnowledge.cs +++ b/C7Engine/C7GameData/AIData/TileKnowledge.cs @@ -60,7 +60,8 @@ public void AddTilesToKnown(Tile unitLocation, bool recomputeActiveTiles = true) } } - private List GetTilesVisibleToUnit(Tile unitLocation) { + public List GetTilesVisibleToUnit(Tile unitLocation) { + // TODO: Make visibility configurable in game rules // Space for current tile, 8 inner ring tiles, 12 outer ring tiles List result = new(21); result.Add(unitLocation); From f373dc54dcd2ce18a517b129bc6a265971038a45 Mon Sep 17 00:00:00 2001 From: Antti Halme Date: Wed, 4 Mar 2026 00:42:43 +0000 Subject: [PATCH 6/7] Add comments to clarify barb AI implementation isn't true to civ3 --- C7Engine/AI/BarbarianStrategy/BaseStrategy.cs | 9 +++++++-- C7Engine/AI/BarbarianStrategy/RagingStrategy.cs | 2 ++ C7Engine/AI/BarbarianStrategy/RestlessStrategy.cs | 2 ++ C7Engine/AI/BarbarianStrategy/RoamingStrategy.cs | 2 ++ C7Engine/AI/BarbarianStrategy/SedentaryStrategy.cs | 2 ++ C7Engine/EntryPoints/BarbarianInteractions.cs | 4 +++- 6 files changed, 18 insertions(+), 3 deletions(-) diff --git a/C7Engine/AI/BarbarianStrategy/BaseStrategy.cs b/C7Engine/AI/BarbarianStrategy/BaseStrategy.cs index dd135396..9131aee6 100644 --- a/C7Engine/AI/BarbarianStrategy/BaseStrategy.cs +++ b/C7Engine/AI/BarbarianStrategy/BaseStrategy.cs @@ -6,8 +6,13 @@ namespace C7Engine; internal abstract class BaseStrategy : IBarbarianStrategy { + // TODO: Determine how barbarian AI is implemented in Civ3 + // TODO: What are the key parameters influencing barbarian activity levels in Civ3? + /// - /// Observe - Orient - Decide - Act + /// Observe - Orient - Decide - Act. + /// + /// Note: This approach may or may not have anything to do with how Civ3 implements barbarian AI. /// public async Task PlayUnitTurn(Player player, MapUnit unit) { // "Observe: Collect data and information from the environment through senses and feedback." @@ -91,7 +96,7 @@ internal class Orientation { public bool CanEngage() => CombatIntel != null; } - + protected abstract bool DecideToEngage(Player player, MapUnit unit, Orientation orientation); protected abstract bool DecideToExplore(Player player, MapUnit unit, Orientation orientation); diff --git a/C7Engine/AI/BarbarianStrategy/RagingStrategy.cs b/C7Engine/AI/BarbarianStrategy/RagingStrategy.cs index deda507f..5930d2d6 100644 --- a/C7Engine/AI/BarbarianStrategy/RagingStrategy.cs +++ b/C7Engine/AI/BarbarianStrategy/RagingStrategy.cs @@ -5,6 +5,8 @@ namespace C7Engine; /// /// Civ3 Manual - Raging: /// You asked for it! The world is full of barbarians,and they appear in large numbers. +/// +/// Note: Implementation is not based on known Civ3 AI logic. /// internal class RagingStrategy : BaseStrategy { protected override bool DecideToEngage(Player player, MapUnit unit, Orientation orientation) { diff --git a/C7Engine/AI/BarbarianStrategy/RestlessStrategy.cs b/C7Engine/AI/BarbarianStrategy/RestlessStrategy.cs index 423d2160..5d54c46e 100644 --- a/C7Engine/AI/BarbarianStrategy/RestlessStrategy.cs +++ b/C7Engine/AI/BarbarianStrategy/RestlessStrategy.cs @@ -5,6 +5,8 @@ namespace C7Engine; /// /// Civ3 Manual - Restless: /// Barbarians appear in moderate up to significant numbers, at shorter intervals than at lower levels. +/// +/// Note: Implementation is not based on known Civ3 AI logic. /// internal class RestlessStrategy : BaseStrategy { protected override bool DecideToEngage(Player player, MapUnit unit, Orientation orientation) { diff --git a/C7Engine/AI/BarbarianStrategy/RoamingStrategy.cs b/C7Engine/AI/BarbarianStrategy/RoamingStrategy.cs index e4f206b9..f569e9cb 100644 --- a/C7Engine/AI/BarbarianStrategy/RoamingStrategy.cs +++ b/C7Engine/AI/BarbarianStrategy/RoamingStrategy.cs @@ -6,6 +6,8 @@ namespace C7Engine; /// Civ3 Manual - Roaming: /// Barbarian settlements occasionally appear, but less frequently and in smaller numbers /// than at higher levels.This is the standard level of barbarian activity. +/// +/// Note: Implementation is not based on known Civ3 AI logic. /// internal class RoamingStrategy : BaseStrategy { protected override bool DecideToEngage(Player player, MapUnit unit, Orientation orientation) { diff --git a/C7Engine/AI/BarbarianStrategy/SedentaryStrategy.cs b/C7Engine/AI/BarbarianStrategy/SedentaryStrategy.cs index 10b9548c..dd93edba 100644 --- a/C7Engine/AI/BarbarianStrategy/SedentaryStrategy.cs +++ b/C7Engine/AI/BarbarianStrategy/SedentaryStrategy.cs @@ -5,6 +5,8 @@ namespace C7Engine; /// /// Civ3 Manual - Sedentary: /// Barbarians are restricted to their encampments. The surrounding terrain is free of their mischief. +/// +/// Note: Implementation is not based on known Civ3 AI logic. /// internal class SedentaryStrategy : BaseStrategy { protected override bool DecideToEngage(Player player, MapUnit unit, Orientation orientation) { diff --git a/C7Engine/EntryPoints/BarbarianInteractions.cs b/C7Engine/EntryPoints/BarbarianInteractions.cs index 2c5d4140..d96d36fb 100644 --- a/C7Engine/EntryPoints/BarbarianInteractions.cs +++ b/C7Engine/EntryPoints/BarbarianInteractions.cs @@ -41,7 +41,9 @@ public static int SpawnBarbarians(GameData gameData) { } /// - /// + /// Apply barbarian activity level to barbarian unit spawn rate. Currently NOT based on Civ3 values. + /// TODO: Make configurable + /// TODO: Determine what these values are in Civ3 (could be a constant across all) /// private static float DetermineSpawnRate(BarbarianActivity activity) { switch (activity) { From 4df8a6d06c8b5e367371dfc72f3014737671d31f Mon Sep 17 00:00:00 2001 From: Antti Halme Date: Wed, 4 Mar 2026 00:43:12 +0000 Subject: [PATCH 7/7] Add DeriveTotalPossibleBarbCamps --- C7Engine/AI/BarbarianStrategy/BaseStrategy.cs | 7 ++--- C7Engine/MapGenerator.cs | 28 +++++++++++++++++-- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/C7Engine/AI/BarbarianStrategy/BaseStrategy.cs b/C7Engine/AI/BarbarianStrategy/BaseStrategy.cs index 9131aee6..9f7c42dc 100644 --- a/C7Engine/AI/BarbarianStrategy/BaseStrategy.cs +++ b/C7Engine/AI/BarbarianStrategy/BaseStrategy.cs @@ -8,7 +8,7 @@ namespace C7Engine; internal abstract class BaseStrategy : IBarbarianStrategy { // TODO: Determine how barbarian AI is implemented in Civ3 // TODO: What are the key parameters influencing barbarian activity levels in Civ3? - + /// /// Observe - Orient - Decide - Act. /// @@ -35,8 +35,7 @@ public async Task PlayUnitTurn(Player player, MapUnit unit) { /// /// Wake the unit if a foreign unit or the borders of a civ are in sight. /// - private static bool ShouldWake(Player player, MapUnit unit) - { + private static bool ShouldWake(Player player, MapUnit unit) { var tiles = player.tileKnowledge.GetTilesVisibleToUnit(unit.location); foreach (Tile t in tiles) { if (t.unitsOnTile.Count > 0 && t.unitsOnTile[0].owner != player) @@ -96,7 +95,7 @@ internal class Orientation { public bool CanEngage() => CombatIntel != null; } - + protected abstract bool DecideToEngage(Player player, MapUnit unit, Orientation orientation); protected abstract bool DecideToExplore(Player player, MapUnit unit, Orientation orientation); diff --git a/C7Engine/MapGenerator.cs b/C7Engine/MapGenerator.cs index 386ea0e1..421d6bbb 100644 --- a/C7Engine/MapGenerator.cs +++ b/C7Engine/MapGenerator.cs @@ -1310,8 +1310,8 @@ private static void AddBarbarianCamps(WorldCharacteristics wc, GameMap m) { ++landTiles; } } - int totalPossibleBarbCamps = landTiles / 100; - // TODO: Update this based on barbarian activity. + + int totalPossibleBarbCamps = DeriveTotalPossibleBarbCamps(wc, landTiles); int numCamps = 0; for (int i = 0; i < tileIndicies.Count && numCamps < totalPossibleBarbCamps; ++i) { @@ -1324,6 +1324,30 @@ private static void AddBarbarianCamps(WorldCharacteristics wc, GameMap m) { } } + /// + /// Apply barbarian activity level to barbarian camp spawn rate. Currently NOT based on Civ3 values. + /// TODO: Make configurable + /// TODO: Determine what these values are in Civ3 + /// + private static int DeriveTotalPossibleBarbCamps(WorldCharacteristics wc, int landTiles) { + var totalCampsBaseline = landTiles / 100; + switch (wc.barbarianActivity) { + case BarbarianActivity.None: + return 0; + case BarbarianActivity.Sedentary: + return totalCampsBaseline; + case BarbarianActivity.Roaming: + return totalCampsBaseline; + case BarbarianActivity.Restless: + return (int)Math.Round(totalCampsBaseline * 1.25); // extra 25% + case BarbarianActivity.Raging: + return (int)Math.Round(totalCampsBaseline * 1.50); // extra 50% + default: + log.Warning("Unknown Barbarian Activity at barb camps derivation."); + return totalCampsBaseline; + } + } + private static bool IsValidForBarbarianCamp(WorldCharacteristics wc, GameMap m, Tile t) { // No barbarian camps on water, volcanos, or mountains. if (!t.IsLand() || t == Tile.NONE || t.overlayTerrainType.Key == "volcano" || t.overlayTerrainType.Key == "mountains") {