diff --git a/C7/UIElements/NewGame/WorldSetup.cs b/C7/UIElements/NewGame/WorldSetup.cs index 26105c3f..633d6953 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,6 +65,7 @@ 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; @@ -71,6 +73,8 @@ public partial class WorldSetup : Control { WorldCharacteristics.Temperature temp = WorldCharacteristics.Temperature.Temperate; WorldCharacteristics.Climate clim = WorldCharacteristics.Climate.Normal; + private BarbarianActivity _barbarianActivity = BarbarianActivity.Roaming; + private WorldSize _worldSize = WorldSize.Generic(); private int GameSeed => int.Parse(seedInput.Text); @@ -270,9 +274,47 @@ public override void _Ready() { _saveGame = GameModeLoader.Load(GamePaths.GameModesDir, GamePaths.GameMode); + InitBarbarianActivityOptions(); InitMapSizes(); } + private void InitBarbarianActivityOptions() { + var barbRandom = new Random(); + var barbDefault = 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 +353,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 +405,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/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..9f7c42dc --- /dev/null +++ b/C7Engine/AI/BarbarianStrategy/BaseStrategy.cs @@ -0,0 +1,102 @@ +using System.Threading.Tasks; +using C7Engine.AI.UnitAI; +using C7GameData; +using C7GameData.AIData; + +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. + /// + /// 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." + + // 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) { + var tiles = player.tileKnowledge.GetTilesVisibleToUnit(unit.location); + foreach (Tile t in tiles) { + 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..5930d2d6 --- /dev/null +++ b/C7Engine/AI/BarbarianStrategy/RagingStrategy.cs @@ -0,0 +1,19 @@ +using C7GameData; + +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) { + 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..5d54c46e --- /dev/null +++ b/C7Engine/AI/BarbarianStrategy/RestlessStrategy.cs @@ -0,0 +1,19 @@ +using C7GameData; + +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) { + 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..f569e9cb --- /dev/null +++ b/C7Engine/AI/BarbarianStrategy/RoamingStrategy.cs @@ -0,0 +1,20 @@ +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. +/// +/// Note: Implementation is not based on known Civ3 AI logic. +/// +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..dd93edba --- /dev/null +++ b/C7Engine/AI/BarbarianStrategy/SedentaryStrategy.cs @@ -0,0 +1,19 @@ +using C7GameData; + +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) { + 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/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); diff --git a/C7Engine/C7GameData/BarbarianActivity.cs b/C7Engine/C7GameData/BarbarianActivity.cs new file mode 100644 index 00000000..126a98fa --- /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 +} 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..9bc1e879 100644 --- a/C7Engine/C7GameData/ImportCiv3.cs +++ b/C7Engine/C7GameData/ImportCiv3.cs @@ -104,6 +104,8 @@ private SaveGame importSav(string savePath, string defaultBicPath, Func 0) { save.Map.wrapHorizontally = biq.Wmap[0].XWrapping; diff --git a/C7Engine/EntryPoints/BarbarianInteractions.cs b/C7Engine/EntryPoints/BarbarianInteractions.cs index c46ffeea..d96d36fb 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,29 @@ public static int SpawnBarbarians(GameData gameData) { return barbariansToSpawn; } + /// + /// 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) { + 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/EntryPoints/TurnHandling.cs b/C7Engine/EntryPoints/TurnHandling.cs index 96157dd6..2c98b8a2 100644 --- a/C7Engine/EntryPoints/TurnHandling.cs +++ b/C7Engine/EntryPoints/TurnHandling.cs @@ -86,13 +86,10 @@ private static async Task 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..18008629 100644 --- a/C7Engine/GameSetup.cs +++ b/C7Engine/GameSetup.cs @@ -42,6 +42,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); diff --git a/C7Engine/MapGenerator.cs b/C7Engine/MapGenerator.cs index b4726e70..421d6bbb 100644 --- a/C7Engine/MapGenerator.cs +++ b/C7Engine/MapGenerator.cs @@ -44,6 +44,8 @@ public enum Age { } public Age age; + public BarbarianActivity barbarianActivity; + public int mapSeed = -1; public WorldSize worldSize; public List terrainTypes; @@ -62,6 +64,8 @@ public WorldCharacteristics(SaveGame save) { maxRankOfWorkableTiles = save.Rules.MaxRankOfWorkableTiles; maxRankOfBarbarianCampTiles = save.Rules.MaxRankOfBarbarianCampTiles; + + barbarianActivity = save.BarbarianInfo.barbarianActivity; } } @@ -1306,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) { @@ -1320,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") { 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, };