Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

139 - Smarter Barbarians #396

Open
wants to merge 6 commits into
base: Development
Choose a base branch
from
Open

Conversation

QuintillusCFC
Copy link
Member

Closes #139

This is a set of changes to make the barbarians more dangerous. More changes can be added to make them more dangerous, and may be as part of this PR, or later.

It also organizes them into tribes, with the idea of various tribes having different priorities.

So far the changes center around making them more opportunistic:

  • If a barbarian unit starts its turn next to an undefended non-combat unit, goodbye undefended unit
  • If a barbarian unit starts its turn next to a non-barbarian unit it thinks it has a chance of beating, it will attack

Corollary, the barbarians aren't the best at figuring out if they think they have a chance of winning yet. I added a very simple risk calculator that takes into account defensive bonuses, but not the round-based nature of combat, meaning the barbarians will be more optimistic than they should be.

Long-term, I envision adding a more accurate combat odds calculator, and perhaps a multi-unit combat calculator (do my five units have a chance of taking that city?). Which combat odds calculator the barbarians (and AI) use could vary based on difficulty level.

…e based from now on.

The behavior is still the same (aside from grouping barbarian actions by tribe), but this sets the groundwork for creating tribe-specific behavior.
…her to attack nearby units, and will only do so if they calculate the odds to be at least 1/3 in their favor.

Their odds calculation is not yet the most sophisticated in the world (well, I guess it is in the C7 world at this time, but you know what I mean), but this does mean they won't take on hopelessly long odds.

They will still occasionally make hopeless attacks on units when they randomly choose to do so from exploring...
…e destroyed, so barbarians don't keep spawning from them.

Also increase barbarian spawn rate a bit to make it more fun.
@QuintillusCFC QuintillusCFC marked this pull request as ready for review April 5, 2023 04:16
foreach (BarbarianTribe tribe in ((BarbarianPlayer)(player)).getTribes()) {
if (tribe.GetUnits().Contains(unit)) {
tribe.RemoveUnit(unit);
goto endDisband;
Copy link
Member

Choose a reason for hiding this comment

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

Yikes, I didn't know C# even supported goto. Can't this just be a return?

Copy link
Member Author

Choose a reason for hiding this comment

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

You know, it actually can in this case.

I wound up with goto as an alternative to Java's ability to break out of an outer for loop, e.g.:

outer: foreach (someVar in outerCollection): {
    //find corresponding innerCollection
    foreach (innerVar in innerCollection): {
        if (innerVar.shouldBreakLoop()) {
            break outer;  //Only supported in Java, not C#
        }
    }
}
//do some additional tasks

This is the idiom that I don't know how to C#-ize, and which leads to my uses of goto. In this case there are no additional tasks, so return works, but in most of the other goto uses I've added, there are additional tasks once the outer loop is broken.

What is the C# way of doing this? I suppose I could add an additional variable to set and check at the end of the outer loop, and if it's true, break out of it? But I'm not sure that's actually any cleaner than goto.

Copy link
Member Author

Choose a reason for hiding this comment

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

This has now been refactored and is part of BarbarianAI and is a break.

{
if (player.units.Contains(unit)) {
player.units.Remove(unit);
if (!player.isBarbarians) {
Copy link
Member

Choose a reason for hiding this comment

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

More broadly though, a MapUnit should know which player it belongs to if it doesn't already, and Player should be able to handle disbanding its own unit (with BarbarianPlayer providing an override) rather than having to do this search here.

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree. I will see if I can find a better way to implement this.

(It's also cool to see comments on my pull request!)

Copy link
Member Author

Choose a reason for hiding this comment

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

This has now been refactored as suggested. Surprisingly we only had 7 virtual functions previously.

BarbarianPlayer barbarians = (BarbarianPlayer)EngineStorage.gameData.players.Find(player => player.isBarbarians);
foreach (BarbarianTribe tribe in barbarians.getTribes()) {
if (tribe.GetCamps().Contains(tile)) {
tribe.RemoveCamp(tile);
Copy link
Member

Choose a reason for hiding this comment

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

eventually, when the components are in place... this kind of thing should be event based

C7Engine/AI/BarbarianAI.cs Show resolved Hide resolved
C7Engine/AI/BarbarianAI.cs Outdated Show resolved Hide resolved
double attackerStrength = attacker.unitType.attack * StrengthBonus.ListToMultiplier(attackBonuses),
defenderStrength = defender.unitType.defense * StrengthBonus.ListToMultiplier(defenseBonuses);

return attackerStrength / (attackerStrength + defenderStrength);
Copy link
Member

Choose a reason for hiding this comment

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

This seems like per-hit odds, isn't it?

foreach (Tile tile in validTiles) {
if (tile.unitsOnTile.Exists(mapUnit => UndefendedUnit(mapUnit))) {
bool alive = unit.move(unit.location.directionTo(tile));
// TODO: Restructure so we can avoid gotos.
Copy link
Member

Choose a reason for hiding this comment

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

Right, these are making me nervous. To me this suggests breaking up the nested loops into functions that return a boolean or nullable object and check the return value to decide the next step

}

Tile newLocation = validTiles[GameData.rng.Next(validTiles.Count)];
//Because it chooses a semi-cardinal direction at random, not accounting for map, it could get none
Copy link
Member

Choose a reason for hiding this comment

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

Do the neighbor functions not account for map edges?

foreach (Tile tile in tribe.GetCamps()) {
//7% chance of a new barbarian. Probably should scale based on barbarian activity.
int result = GameData.rng.Next(100);
log.Verbose("Random barb result = " + result);
Copy link
Member

Choose a reason for hiding this comment

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

If this is happening at each camp maybe include the location

Comment on lines +91 to +117
MapUnit newUnit = new MapUnit();
newUnit.location = tile;
newUnit.owner = barbPlayer;
newUnit.unitType = gameData.barbarianInfo.basicBarbarian;
newUnit.experienceLevelKey = gameData.defaultExperienceLevelKey;
newUnit.experienceLevel = gameData.defaultExperienceLevel;
newUnit.hitPointsRemaining = 3;
newUnit.isFortified = true; //todo: hack for unit selection

tile.unitsOnTile.Add(newUnit);
gameData.mapUnits.Add(newUnit);
tribe.AddUnit(newUnit);
log.Debug("New barbarian added at " + tile);
} else if (tile.NeighborsWater() && result < 7) {
MapUnit newUnit = new MapUnit();
newUnit.location = tile;
newUnit.owner = barbPlayer;
newUnit.unitType = gameData.barbarianInfo.barbarianSeaUnit;
newUnit.experienceLevelKey = gameData.defaultExperienceLevelKey;
newUnit.experienceLevel = gameData.defaultExperienceLevel;
newUnit.hitPointsRemaining = 3;
newUnit.isFortified = true; //todo: hack for unit selection

tile.unitsOnTile.Add(newUnit);
gameData.mapUnits.Add(newUnit);
tribe.AddUnit(newUnit);
log.Debug("New barbarian galley added at " + tile);
Copy link
Member

Choose a reason for hiding this comment

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

sounds like a call for factory methods!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Barbarians Try to Attack Your Units and Destroy Your Roads and Cities
2 participants