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
45 changes: 44 additions & 1 deletion C7/UIElements/NewGame/WorldSetup.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Godot;
using System;
using System.Linq;
using C7Engine;
using C7Engine.Lua;
using C7GameData;
Expand Down Expand Up @@ -64,13 +65,16 @@ 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;

private BarbarianActivity _barbarianActivity = BarbarianActivity.Roaming;

private WorldSize _worldSize = WorldSize.Generic();

private int GameSeed => int.Parse(seedInput.Text);
Expand Down Expand Up @@ -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<BarbarianActivity>().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();

Expand Down Expand Up @@ -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.");
Expand Down Expand Up @@ -363,6 +405,7 @@ private void CreateGame() {
climate = clim,
temperature = temp,
worldSize = _worldSize,
barbarianActivity = _barbarianActivity,
mapSeed = GameSeed,
};

Expand Down
22 changes: 21 additions & 1 deletion C7/UIElements/NewGame/world_setup.tscn
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
104 changes: 28 additions & 76 deletions C7Engine/AI/BarbarianAI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<BarbarianAI>();
public static class BarbarianAI {

public async Task PlayTurn(Player player, GameData gameData) {
private static ILogger log = Log.ForContext<TurnHandling>();

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));
}
}
}
102 changes: 102 additions & 0 deletions C7Engine/AI/BarbarianStrategy/BaseStrategy.cs
Original file line number Diff line number Diff line change
@@ -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?

/// <summary>
/// Observe - Orient - Decide - Act.
///
/// Note: This approach may or may not have anything to do with how Civ3 implements barbarian AI.
/// </summary>
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
}

/// <summary>
/// Wake the unit if a foreign unit or the borders of a civ are in sight.
Comment thread
ajhalme marked this conversation as resolved.
/// </summary>
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;
}

/// <summary>
/// "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."
/// </summary>
protected Task<Orientation> Orient(Player player, MapUnit unit) {
return Task.FromResult(new Orientation {
IsLastUnitInCamp = unit.location.hasBarbarianCamp && unit.location.unitsOnTile.Count == 1,
CombatIntel = CombatAI.MakeAiData(unit, player)
});
}

/// <summary>
/// "Decide: Formulate a plan or course of action based on the orientation."
/// </summary>
protected async Task<UnitAI> 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));
}

/// <summary>
/// "Act: Implement the decision, which creates new data and feeds back into the observation phase."
/// </summary>
protected async Task<UnitAI.Result> 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);
}
8 changes: 8 additions & 0 deletions C7Engine/AI/BarbarianStrategy/IBarbarianStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.Threading.Tasks;
using C7GameData;

namespace C7Engine;

internal interface IBarbarianStrategy {
Task PlayUnitTurn(Player player, MapUnit unit);
}
19 changes: 19 additions & 0 deletions C7Engine/AI/BarbarianStrategy/RagingStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using C7GameData;

namespace C7Engine;

/// <summary>
/// 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.
/// </summary>
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;
}
}
Loading