Skip to content

Commit

Permalink
make easy and medium bots dumber
Browse files Browse the repository at this point in the history
  • Loading branch information
Sesu8642 committed Oct 17, 2022
1 parent dbb72a5 commit 7f0daa5
Showing 1 changed file with 60 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -47,25 +47,42 @@ public class BotAi {

/** Possible intelligence levels for the AI. */
public enum Intelligence {
DUMB(0.5F, 0.6F, false, Integer.MAX_VALUE, Integer.MAX_VALUE),
MEDIUM(1F, 0.8F, false, Integer.MAX_VALUE, Integer.MAX_VALUE), SMART(1F, 1F, true, 25, 20);
DUMB(0.5F, 0.6F, false, Integer.MAX_VALUE, Integer.MAX_VALUE, false, false),
MEDIUM(0.8F, 0.8F, false, Integer.MAX_VALUE, Integer.MAX_VALUE, false, true),
SMART(1F, 1F, true, 25, 20, true, true);

/** Chance that the bot will even try to conquer anything in a given turn. */
public final float chanceToConquerPerTurn;

/** Chance to remove each blocking map object. */
public final float chanceToRemoveEachBlockingObject;

/**
* Whether to defend smartly: i.e. considering how many tiles are protected. If
* false, the defense score of every tile is 0. This means that (basically)
* random tiles near the border are protected.
*/
private boolean smartDefending;

/**
* Whether to attack smartly: prefer enemy kingdom tiles over unconnected ones,
* destroy castles etc. If false, the offense score of every tile is 0. This
* means that (basically) random tiles are conquered.
*/
private boolean smartAttacking;

/**
* Minimum defense tile score to be worth protecting with a castle. Will be
* protected with a unit if a castle is too expensive. Use a very high value to
* disable protecting with castles.
* disable protecting with castles. If {@link #smartDefending} is false, any
* value above 0 will disable it as well.
*/
public final int protectWithCastleScoreTreshold;

/**
* Minimum defense tile score to be worth protecting with a unit. Use a very
* high value to disable protecting with units as a first choice.
* high value to disable protecting with units as a first choice. If
* {@link #smartDefending} is false, any value above 0 will disable it as well.
*/
public final int protectWithUnitScoreTreshold;

Expand All @@ -77,12 +94,14 @@ public enum Intelligence {

private Intelligence(float chanceToConquerPerTurn, float chanceToRemoveEachBlockingObject,
boolean reconsidersWhichTilesToProtect, int protectWithCastleScoreTreshold,
int protectWithUnitScoreTreshold) {
int protectWithUnitScoreTreshold, boolean smartProtectionPlacement, boolean smartAttacking) {
this.chanceToConquerPerTurn = chanceToConquerPerTurn;
this.chanceToRemoveEachBlockingObject = chanceToRemoveEachBlockingObject;
this.reconsidersWhichTilesToProtect = reconsidersWhichTilesToProtect;
this.smartDefending = smartProtectionPlacement;
this.protectWithCastleScoreTreshold = protectWithCastleScoreTreshold;
this.protectWithUnitScoreTreshold = protectWithUnitScoreTreshold;
this.smartAttacking = smartAttacking;
}

}
Expand Down Expand Up @@ -165,14 +184,14 @@ private GameState doKingdomMove(GameState gameState, Kingdom kingdom, Intelligen
removeBlockingObjects(gameState, pickedUpUnits, intelligence.chanceToRemoveEachBlockingObject, random);
defendMostImportantTiles(gameState, intelligence, pickedUpUnits, placedCastleTiles);
if (random.nextFloat() <= intelligence.chanceToConquerPerTurn) {
conquerAsMuchAsPossible(gameState, pickedUpUnits);
conquerAsMuchAsPossible(gameState, intelligence, pickedUpUnits);
}
if (intelligence.reconsidersWhichTilesToProtect) {
sellCastles(gameState.getActiveKingdom(), placedCastleTiles);
pickUpAllAvailableUnits(gameState.getActiveKingdom(), pickedUpUnits);
defendMostImportantTiles(gameState, intelligence, pickedUpUnits, placedCastleTiles);
}
protectWithLeftoverUnits(gameState, pickedUpUnits);
protectWithLeftoverUnits(gameState, intelligence, pickedUpUnits);

delayForPreview(gameState);
return gameState;
Expand Down Expand Up @@ -228,7 +247,8 @@ private void defendMostImportantTiles(GameState gameState, Intelligence intellig
Set<HexTile> placedCastleTiles) {
Gdx.app.debug(TAG, "defending most important tiles");
Set<HexTile> interestingProtectionTiles = getInterestingProtectionTiles(gameState);
TileScoreInfo bestProtectionCandidate = getBestDefenseTileScore(gameState, interestingProtectionTiles);
TileScoreInfo bestProtectionCandidate = getBestDefenseTileScore(gameState, intelligence,
interestingProtectionTiles);
while (bestProtectionCandidate.score >= intelligence.protectWithCastleScoreTreshold) {
// if enough money buy castle
if (InputValidationHelper.checkBuyObject(gameState, gameState.getActivePlayer(), Castle.COST)) {
Expand All @@ -247,7 +267,7 @@ private void defendMostImportantTiles(GameState gameState, Intelligence intellig
} else {
break;
}
bestProtectionCandidate = getBestDefenseTileScore(gameState, interestingProtectionTiles);
bestProtectionCandidate = getBestDefenseTileScore(gameState, intelligence, interestingProtectionTiles);
}
while (bestProtectionCandidate.score >= intelligence.protectWithUnitScoreTreshold) {
if (pickedUpUnits.ofType(UnitTypes.PEASANT) > 0 || acquireUnit(gameState, gameState.getActiveKingdom(),
Expand All @@ -259,11 +279,11 @@ private void defendMostImportantTiles(GameState gameState, Intelligence intellig
} else {
break;
}
bestProtectionCandidate = getBestDefenseTileScore(gameState, interestingProtectionTiles);
bestProtectionCandidate = getBestDefenseTileScore(gameState, intelligence, interestingProtectionTiles);
}
}

private void conquerAsMuchAsPossible(GameState gameState, PickedUpUnits pickedUpUnits) {
private void conquerAsMuchAsPossible(GameState gameState, Intelligence intelligence, PickedUpUnits pickedUpUnits) {
Gdx.app.debug(TAG, "conquering as much as possible");
boolean unableToConquerAnyMore = false;
whileloop: while (!unableToConquerAnyMore) {
Expand All @@ -277,8 +297,8 @@ private void conquerAsMuchAsPossible(GameState gameState, PickedUpUnits pickedUp
// determine how "valuable" the tiles are for conquering
Set<OffenseTileScoreInfo> offenseTileScoreInfoSet = Collections
.newSetFromMap(new ConcurrentHashMap<BotAi.OffenseTileScoreInfo, Boolean>());
possibleConquerTiles.parallelStream().forEach(
conquerTile -> offenseTileScoreInfoSet.add(getOffenseTileScoreInfo(gameState, conquerTile)));
possibleConquerTiles.parallelStream().forEach(conquerTile -> offenseTileScoreInfoSet
.add(getOffenseTileScoreInfo(gameState, intelligence, conquerTile)));
List<OffenseTileScoreInfo> offenseTileScoreInfos = new ArrayList<>(offenseTileScoreInfoSet);
offenseTileScoreInfos.sort((OffenseTileScoreInfo o1, OffenseTileScoreInfo o2) -> {
int result = Integer.compare(o2.score, o1.score);
Expand Down Expand Up @@ -443,10 +463,11 @@ private void buyUnitDirectly(Kingdom kingdom, PickedUpUnits pickedUpUnits, UnitT
pickedUpUnits.addUnit(unitType);
}

private void protectWithLeftoverUnits(GameState gameState, PickedUpUnits pickedUpUnits) {
private void protectWithLeftoverUnits(GameState gameState, Intelligence intelligence, PickedUpUnits pickedUpUnits) {
Gdx.app.debug(TAG, "protecting the kingdom with leftover units");
Set<HexTile> interestingProtectionTiles = getInterestingProtectionTiles(gameState);
TileScoreInfo bestDefenseTileScore = getBestDefenseTileScore(gameState, interestingProtectionTiles);
TileScoreInfo bestDefenseTileScore = getBestDefenseTileScore(gameState, intelligence,
interestingProtectionTiles);
while (bestDefenseTileScore.score >= 0) {
if (pickedUpUnits.getTotalNoOfUnits() == 0) {
break;
Expand All @@ -463,7 +484,7 @@ private void protectWithLeftoverUnits(GameState gameState, PickedUpUnits pickedU
break;
}
}
bestDefenseTileScore = getBestDefenseTileScore(gameState, interestingProtectionTiles);
bestDefenseTileScore = getBestDefenseTileScore(gameState, intelligence, interestingProtectionTiles);
}
placeLeftOverUnitsSomeWhere(gameState, pickedUpUnits);
}
Expand Down Expand Up @@ -519,10 +540,11 @@ private Set<HexTile> getInterestingProtectionTiles(GameState gameState) {
return interestingPlacementTiles;
}

private TileScoreInfo getBestDefenseTileScore(GameState gameState, Set<HexTile> interestingProtectionTiles) {
private TileScoreInfo getBestDefenseTileScore(GameState gameState, Intelligence intelligence,
Set<HexTile> interestingProtectionTiles) {
Set<TileScoreInfo> results = Collections.newSetFromMap(new ConcurrentHashMap<BotAi.TileScoreInfo, Boolean>());
interestingProtectionTiles.parallelStream()
.forEach(tile -> results.add(new TileScoreInfo(tile, getTileDefenseScore(gameState, tile))));
interestingProtectionTiles.parallelStream().forEach(
tile -> results.add(new TileScoreInfo(tile, getTileDefenseScore(gameState, intelligence, tile))));
return results.stream().max((TileScoreInfo t1, TileScoreInfo t2) -> {
int result = Integer.compare(t1.score, t2.score);
// if the score is the same, use the coordinates to eliminate randomness
Expand All @@ -533,7 +555,18 @@ private TileScoreInfo getBestDefenseTileScore(GameState gameState, Set<HexTile>
}).orElse(new TileScoreInfo(null, -1));
}

private int getTileDefenseScore(GameState gameState, HexTile tile) {
/**
* Calculates a score for a given tile. The score expresses how good the tile is
* as a candidate to place a unit or castle on for defensive purposes. Occupied
* tiles get a score of -1. Depending of the intelligence level, all other tiles
* may get a score of 0. The highest possible value should be 60 (I think).
*
* @param gameStategame state to work with
* @param intelligence intelligence level of the bot player
* @param tile tiles to calculate the score of
* @return defense score
*/
private int getTileDefenseScore(GameState gameState, Intelligence intelligence, HexTile tile) {
if (tile.getContent() != null) {
// already occupied
return -1;
Expand Down Expand Up @@ -582,10 +615,13 @@ private int getTileDefenseScore(GameState gameState, HexTile tile) {
// border
score += tileIsProtected ? 1 : 5;
}
if (!intelligence.smartDefending) {
return 0;
}
return score;
}

private OffenseTileScoreInfo getOffenseTileScoreInfo(GameState gameState, HexTile tile) {
private OffenseTileScoreInfo getOffenseTileScoreInfo(GameState gameState, Intelligence intelligence, HexTile tile) {
int score;
int requiredStrength = 1;
if (tile.getKingdom() == null) {
Expand Down Expand Up @@ -622,6 +658,9 @@ private OffenseTileScoreInfo getOffenseTileScoreInfo(GameState gameState, HexTil
}
}
}
if (!intelligence.smartAttacking) {
score = 0;
}
return new OffenseTileScoreInfo(tile, score, requiredStrength);
}

Expand Down

2 comments on commit 7f0daa5

@d-albrecht
Copy link

Choose a reason for hiding this comment

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

Okay, again, not play-tested yet, but some immediate thoughts now that I invested a bit more time in checking the diff:

  • If you use smartAttacking and smartDefending as shortcuts for the evaluation of tiles, then move this check to the top of each method. No need to do all the checks and computation if afterwards you say "screw it" and manually overwrite the returned value anyway (at least for the defensive score).
  • If you assume that this AI could still be too smart, then there really isn't much to add here to handicap them even more. This already basically neuters both new AI levels to a point that they can't lose any more IQ points before rampaging uncontrollably.
  • That said, I'm not sure if this actually made them dumber. Let me elaborate: I usually play against the medium AI currently. With knowledge of the code, I know that the AI will (most of the time) use up some units to destroy trees and gravestones. Hence, I can (to some extend) exploit the AI by tricking it into acting according to their programming. If I conquer one tile and leave a tree tile in this opponent's kingdom (instead of conquering the tree tile), one unit will be bound to clear the tree. That said, your new AI levels will in fact rampage and will consider everything randomly. I'm not sure that new players will be able to make heads or tails off of this. In contrast, a predictable AI is always - in my experience - better to learn a game.
  • I know that this will cause some changes as well, but the better approach could be to tweak the scoring rules based on the AI level. For example, a dumber AI could target capitals with a score of 20 instead of 50. Putting more focus on barons this way, if you kept unit scoring the same in this example. (This is just an example, you would have to find suitable parameters yourself.) Aka, you would have to put some (or all) of these scoring parameters into the enum.
  • Or - although this would again jeopardize the idea of a predictable AI a bit - you could add some noise/uncertainty to each score. More or less simulating an inexperienced player who isn't able to estimate the value of each tile correctly yet. For example, by adding some random number between 0 and n to each score, the basic idea of a targeted approach would be kept but the AI would appear to act dumber/inexperienced/unsure overall. This n could be part of the Intelligence enum: smart had no noise, medium had some noise, dumb had more noise, but ideally all AI levels with n < 30 (approximately) to keep them at least somewhat focused.

@Sesu8642
Copy link
Owner Author

Choose a reason for hiding this comment

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

If you use smartAttacking and smartDefending as shortcuts for the evaluation of tiles, then move this check to the top of each method. No need to do all the checks and computation if afterwards you say "screw it" and manually overwrite the returned value anyway (at least for the defensive score).

Good point. I tried a lot of things at the end of the methods where the score is calulated but what I finally settled with could be just as well at the beginning.

Please sign in to comment.