Skip to content

Commit 6c8c036

Browse files
authored
Merge pull request #903 from ajhalme/barbarians-ui
Add barbarian activity levels
2 parents 86b04f8 + 4df8a6d commit 6c8c036

19 files changed

Lines changed: 371 additions & 90 deletions

C7/UIElements/NewGame/WorldSetup.cs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Godot;
22
using System;
3+
using System.Linq;
34
using C7Engine;
45
using C7Engine.Lua;
56
using C7GameData;
@@ -64,13 +65,16 @@ public partial class WorldSetup : Control {
6465
[Export] LineEdit seedInput;
6566

6667
[Export] VBoxContainer worldSizeButtonsContainer;
68+
[Export] VBoxContainer barbActivityButtonsContainer;
6769

6870
WorldCharacteristics.Landform landform = WorldCharacteristics.Landform.Pangaea;
6971
WorldCharacteristics.OceanCoverage ocean = WorldCharacteristics.OceanCoverage.Percent_70;
7072
WorldCharacteristics.Age age = WorldCharacteristics.Age.Billion_4;
7173
WorldCharacteristics.Temperature temp = WorldCharacteristics.Temperature.Temperate;
7274
WorldCharacteristics.Climate clim = WorldCharacteristics.Climate.Normal;
7375

76+
private BarbarianActivity _barbarianActivity = BarbarianActivity.Roaming;
77+
7478
private WorldSize _worldSize = WorldSize.Generic();
7579

7680
private int GameSeed => int.Parse(seedInput.Text);
@@ -270,9 +274,47 @@ public override void _Ready() {
270274

271275
_saveGame = GameModeLoader.Load(GamePaths.GameModesDir, GamePaths.GameMode);
272276

277+
InitBarbarianActivityOptions();
273278
InitMapSizes();
274279
}
275280

281+
private void InitBarbarianActivityOptions() {
282+
var barbRandom = new Random();
283+
var barbDefault = BarbarianActivity.Roaming;
284+
var barbOptions = Enum.GetValues<BarbarianActivity>().OrderBy(x => x).ToList();
285+
286+
var barbActivityButtonGroup = new ButtonGroup() { ResourceName = "BarbActivityButtonGroup" };
287+
var randomSizeButton = new Civ3MenuButton
288+
{
289+
Text = "Random",
290+
textPosition = Civ3MenuButton.TextPosition.TextRightOfIcon,
291+
FontSize = 0,
292+
ButtonGroup = barbActivityButtonGroup,
293+
ToggleMode = true
294+
};
295+
randomSizeButton.Pressed += () => {
296+
_barbarianActivity = barbOptions[barbRandom.Next(barbOptions.Count)];
297+
};
298+
299+
// Dynamically create a new button for each barbarian activity option
300+
foreach (var ba in barbOptions) {
301+
var barbActivityButton = new Civ3MenuButton
302+
{
303+
Text = ba.ToString("G"),
304+
textPosition = Civ3MenuButton.TextPosition.TextRightOfIcon,
305+
FontSize = 0,
306+
ButtonGroup = barbActivityButtonGroup,
307+
ToggleMode = true,
308+
ButtonPressed = ba == barbDefault
309+
};
310+
barbActivityButton.Pressed += () => _barbarianActivity = ba;
311+
barbActivityButtonsContainer.AddChild(barbActivityButton);
312+
}
313+
314+
// Append random as last in the list
315+
barbActivityButtonsContainer.AddChild(randomSizeButton);
316+
}
317+
276318
private void InitMapSizes() {
277319
var sizeRandom = new Random();
278320

@@ -311,7 +353,7 @@ private void InitMapSizes() {
311353
_worldSize = ws;
312354
}
313355

314-
// Move random as last in the list and drop default map option and
356+
// Append random as last in the list
315357
worldSizeButtonsContainer.AddChild(randomSizeButton);
316358
} catch (Exception ex) {
317359
log.Warning(ex, "Failed to load map sizes from game mode.");
@@ -363,6 +405,7 @@ private void CreateGame() {
363405
climate = clim,
364406
temperature = temp,
365407
worldSize = _worldSize,
408+
barbarianActivity = _barbarianActivity,
366409
mapSeed = GameSeed,
367410
};
368411

C7/UIElements/NewGame/world_setup.tscn

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ keycode = 4194305
3636
[sub_resource type="Shortcut" id="Shortcut_7oyjg"]
3737
events = [SubResource("InputEventKey_klcu1")]
3838

39-
[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")]
39+
[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")]
4040
anchors_preset = 15
4141
anchor_right = 1.0
4242
anchor_bottom = 1.0
@@ -87,6 +87,7 @@ confirm = NodePath("Background/Confirm")
8787
cancel = NodePath("Background/Cancel")
8888
seedInput = NodePath("Background/SeedInput")
8989
worldSizeButtonsContainer = NodePath("Background/WorldSizeButtonsScroller/WorldSizeButtonsContainer")
90+
barbActivityButtonsContainer = NodePath("Background/BarbActivityScroller/BarbActivityButtonsContainer")
9091

9192
[node name="Background" type="TextureRect" parent="."]
9293
layout_mode = 2
@@ -102,6 +103,15 @@ theme_override_font_sizes/font_size = 16
102103
text = "WORLD SIZE"
103104
horizontal_alignment = 1
104105

106+
[node name="Label" type="Label" parent="Background/Label"]
107+
offset_left = 623.0
108+
offset_top = -1.0
109+
offset_right = 789.0
110+
offset_bottom = 22.0
111+
theme_override_font_sizes/font_size = 16
112+
text = "BARBARIANS"
113+
horizontal_alignment = 1
114+
105115
[node name="Label2" type="Label" parent="Background"]
106116
layout_mode = 0
107117
offset_left = 114.0
@@ -648,3 +658,13 @@ offset_bottom = 256.0
648658

649659
[node name="WorldSizeButtonsContainer" type="VBoxContainer" parent="Background/WorldSizeButtonsScroller"]
650660
layout_mode = 2
661+
662+
[node name="BarbActivityScroller" type="ScrollContainer" parent="Background"]
663+
layout_mode = 0
664+
offset_left = 743.0
665+
offset_top = 113.0
666+
offset_right = 901.0
667+
offset_bottom = 257.0
668+
669+
[node name="BarbActivityButtonsContainer" type="VBoxContainer" parent="Background/BarbActivityScroller"]
670+
layout_mode = 2

C7Engine/AI/BarbarianAI.cs

Lines changed: 28 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -12,95 +12,47 @@ namespace C7Engine {
1212
using System;
1313
using System.Threading.Tasks;
1414

15-
public class BarbarianAI {
15+
// TODO: The AI state (plans, strategy, ..) should be stored somewhere in game state.
16+
// For now, we have a stateless random AI.
1617

17-
private ILogger log = Log.ForContext<BarbarianAI>();
18+
public static class BarbarianAI {
1819

19-
public async Task PlayTurn(Player player, GameData gameData) {
20+
private static ILogger log = Log.ForContext<TurnHandling>();
21+
22+
public static async Task PlayTurn(Player player, GameData gameData) {
2023
if (!player.isBarbarians) {
21-
throw new System.Exception("Barbarian AI can only play barbarian players");
24+
throw new Exception("Barbarian AI can only play barbarian players");
2225
}
2326

24-
foreach (MapUnit unit in player.units.ToArray()) {
25-
// Make the barbarians wake up if they see a unit or a civ's
26-
// borders. This will happen each turn, so eventually the barb
27-
// should muster the courage to attack.
28-
foreach (Tile t in unit.location.neighbors.Values) {
29-
if (t.unitsOnTile.Count > 0 && t.unitsOnTile[0].owner != player) {
30-
unit.wake();
31-
break;
32-
}
33-
if (t.OwningPlayer() != null) {
34-
unit.wake();
35-
break;
36-
}
37-
}
38-
39-
// Don't waste time recalculating behaviors for fortified units.
40-
if (unit.isFortified) {
41-
continue;
42-
}
43-
44-
// For each unit, if there's already an AI task assigned, it will attempt to complete its goal.
45-
// It may fail due to conditions having changed since that goal was assigned; in that case it will
46-
// get a new task to try to complete.
47-
//
48-
// Cap our attempts at 2 to avoid getting stuck in bad situations.
49-
for (int attempt = 0; attempt < 2; ++attempt) {
50-
if (unit.currentAI == null) {
51-
unit.currentAI = GetAIForUnit(unit, player);
52-
}
53-
54-
// If the unit is still the process of doing its plan, allow
55-
// it to continue next turn.
56-
UnitAI.Result result = await unit.currentAI.PlayTurn(player, unit);
57-
if (result == UnitAI.Result.InProgress) {
58-
break;
59-
}
60-
61-
if (result == UnitAI.Result.Error) {
62-
unit.currentAI = null;
63-
break;
64-
}
27+
if (gameData.barbarianInfo.barbarianActivity == BarbarianActivity.None)
28+
return;
6529

66-
if (unit.hitPointsRemaining <= 0 || unit.isFortified) {
67-
unit.currentAI = null;
68-
break;
69-
}
30+
var strategy = SelectStrategy(gameData.barbarianInfo.barbarianActivity);
7031

71-
// Otherwise we need a new plan for next turn. Pick it now
72-
// to avoid things like new units being preferred for
73-
// exploration instead of units already far away from home
74-
// for exploration.
75-
unit.currentAI = GetAIForUnit(unit, player);
76-
}
32+
// TODO: Band units into tribes, decide at the tribe level --> work together
7733

34+
foreach (MapUnit unit in player.units.ToArray()) {
35+
await strategy.PlayUnitTurn(player, unit);
7836
player.tileKnowledge.AddTilesToKnown(unit.location);
7937
}
8038
}
8139

82-
public static UnitAI GetAIForUnit(MapUnit unit, Player player) {
83-
// Barbarians should always defend their camp if it is unguarded.
84-
if (unit.location.hasBarbarianCamp && unit.location.unitsOnTile.Count == 1) {
85-
return new DefenderAI(DefenderAI.MakeAiDataForDefendInPlace(unit, player));
86-
}
87-
88-
// If the barbarian can fight, it should.
89-
CombatAIData maybeCombat = CombatAI.MakeAiData(unit, player);
90-
if (maybeCombat != null) {
91-
return new CombatAI(maybeCombat);
40+
private static IBarbarianStrategy SelectStrategy(BarbarianActivity barbarianActivity) {
41+
switch (barbarianActivity) {
42+
case BarbarianActivity.None:
43+
throw new Exception("Cannot select an AI strategy for BarbarianActivity 'None'.");
44+
case BarbarianActivity.Sedentary:
45+
return new SedentaryStrategy();
46+
case BarbarianActivity.Roaming:
47+
return new RoamingStrategy();
48+
case BarbarianActivity.Restless:
49+
return new RestlessStrategy();
50+
case BarbarianActivity.Raging:
51+
return new RagingStrategy();
52+
default:
53+
log.Warning("Unknown BarbarianActivity. Defaulting to Sedentary.");
54+
return new SedentaryStrategy();
9255
}
93-
94-
// Give barbarians a chance to explore if they can't fight.
95-
if (GameData.rng.Next(100) < 30) {
96-
ExplorerAIData? maybeAiData = ExplorerAI.MaybeMakeAiData(unit, player);
97-
if (maybeAiData != null) {
98-
return new ExplorerAI(maybeAiData);
99-
}
100-
}
101-
102-
// Otherwise just sit tight.
103-
return new DefenderAI(DefenderAI.MakeAiDataForDefendInPlace(unit, player));
10456
}
10557
}
10658
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
using System.Threading.Tasks;
2+
using C7Engine.AI.UnitAI;
3+
using C7GameData;
4+
using C7GameData.AIData;
5+
6+
namespace C7Engine;
7+
8+
internal abstract class BaseStrategy : IBarbarianStrategy {
9+
// TODO: Determine how barbarian AI is implemented in Civ3
10+
// TODO: What are the key parameters influencing barbarian activity levels in Civ3?
11+
12+
/// <summary>
13+
/// Observe - Orient - Decide - Act.
14+
///
15+
/// Note: This approach may or may not have anything to do with how Civ3 implements barbarian AI.
16+
/// </summary>
17+
public async Task PlayUnitTurn(Player player, MapUnit unit) {
18+
// "Observe: Collect data and information from the environment through senses and feedback."
19+
20+
// Wake up the unit if there's a reason to do so
21+
if (ShouldWake(player, unit))
22+
unit.wake();
23+
24+
// Skip units that didn't wake up
25+
if (unit.isFortified)
26+
return;
27+
28+
var orientation = await Orient(player, unit);
29+
var plan = await Decide(player, unit, orientation);
30+
var result = await Act(player, unit, plan);
31+
32+
// TODO: store result
33+
}
34+
35+
/// <summary>
36+
/// Wake the unit if a foreign unit or the borders of a civ are in sight.
37+
/// </summary>
38+
private static bool ShouldWake(Player player, MapUnit unit) {
39+
var tiles = player.tileKnowledge.GetTilesVisibleToUnit(unit.location);
40+
foreach (Tile t in tiles) {
41+
if (t.unitsOnTile.Count > 0 && t.unitsOnTile[0].owner != player)
42+
return true;
43+
44+
if (t.OwningPlayer() != null)
45+
return true;
46+
}
47+
48+
return false;
49+
}
50+
51+
/// <summary>
52+
/// "Orient: Analyze and synthesize data to form a mental perspective, considering experience,
53+
/// culture, and new information. This is considered the most important phase of the OODA loop."
54+
/// </summary>
55+
protected Task<Orientation> Orient(Player player, MapUnit unit) {
56+
return Task.FromResult(new Orientation {
57+
IsLastUnitInCamp = unit.location.hasBarbarianCamp && unit.location.unitsOnTile.Count == 1,
58+
CombatIntel = CombatAI.MakeAiData(unit, player)
59+
});
60+
}
61+
62+
/// <summary>
63+
/// "Decide: Formulate a plan or course of action based on the orientation."
64+
/// </summary>
65+
protected async Task<UnitAI> Decide(Player player, MapUnit unit, Orientation orientation) {
66+
// Barbarians defend their camp if it is unguarded.
67+
if (orientation.IsLastUnitInCamp)
68+
return new DefenderAI(DefenderAI.MakeAiDataForDefendInPlace(unit, player));
69+
70+
// Decide whether to engage enemy units
71+
if (orientation.CanEngage() && DecideToEngage(player, unit, orientation))
72+
return new CombatAI(orientation.CombatIntel);
73+
74+
// Decide whether to explore
75+
if (DecideToExplore(player, unit, orientation)) {
76+
var maybeAiData = ExplorerAI.MaybeMakeAiData(unit, player);
77+
if (maybeAiData != null)
78+
return new ExplorerAI(maybeAiData);
79+
}
80+
81+
// Defend otherwise
82+
return new DefenderAI(DefenderAI.MakeAiDataForDefendInPlace(unit, player));
83+
}
84+
85+
/// <summary>
86+
/// "Act: Implement the decision, which creates new data and feeds back into the observation phase."
87+
/// </summary>
88+
protected async Task<UnitAI.Result> Act(Player player, MapUnit unit, UnitAI plan) {
89+
return await plan.PlayTurn(player, unit);
90+
}
91+
92+
internal class Orientation {
93+
public bool IsLastUnitInCamp { get; set; }
94+
public CombatAIData CombatIntel { get; set; }
95+
96+
public bool CanEngage() => CombatIntel != null;
97+
}
98+
99+
protected abstract bool DecideToEngage(Player player, MapUnit unit, Orientation orientation);
100+
101+
protected abstract bool DecideToExplore(Player player, MapUnit unit, Orientation orientation);
102+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using System.Threading.Tasks;
2+
using C7GameData;
3+
4+
namespace C7Engine;
5+
6+
internal interface IBarbarianStrategy {
7+
Task PlayUnitTurn(Player player, MapUnit unit);
8+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using C7GameData;
2+
3+
namespace C7Engine;
4+
5+
/// <summary>
6+
/// Civ3 Manual - Raging:
7+
/// You asked for it! The world is full of barbarians,and they appear in large numbers.
8+
///
9+
/// Note: Implementation is not based on known Civ3 AI logic.
10+
/// </summary>
11+
internal class RagingStrategy : BaseStrategy {
12+
protected override bool DecideToEngage(Player player, MapUnit unit, Orientation orientation) {
13+
return true;
14+
}
15+
16+
protected override bool DecideToExplore(Player player, MapUnit unit, Orientation orientation) {
17+
return GameData.rng.Next(100) < 50;
18+
}
19+
}

0 commit comments

Comments
 (0)