mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-02 14:18:21 +00:00
Yet another nation improvement PR 🤖 (#2841)
## Description: ### `NationAllianceBehavior` - `isAlliancePartnerSimilarlyStrong()` now also checks for `numTilesOwned`, should make it a bit easier to get alliances. The troop calculation now also uses the players `outgoingAttacks` to make it feel less random. OF-Discord-Humans complained. - Rebalanced `checkAlreadyEnoughAlliances()` a bit ### `NationNukeBehavior` - Don't save up for MIRV if they are disabled - Don't try to throw atom bombs / hydros if they are disabled - Hydro-Nations are allowed to throw atom bombs if they are under heavy attack - Rebalance `isUnderHeavyAttack()` a bit - Increased perceived cost ### `NationEmojiBehavior` - Fix multiple nations congratulated the winner instead of one - Don't brag with our crown if the game is already over ### `DonateGoldExecution` & `DonateTroopExecution` - Added `canSendEmoji` checks ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory - [X] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: FloPinguin
This commit is contained in:
@@ -62,7 +62,10 @@ export class DonateGoldExecution implements Execution {
|
||||
}
|
||||
|
||||
// Only AI nations auto-respond with emojis, human players should not
|
||||
if (this.recipient.type() === PlayerType.Nation) {
|
||||
if (
|
||||
this.recipient.type() === PlayerType.Nation &&
|
||||
this.recipient.canSendEmoji(this.sender)
|
||||
) {
|
||||
// Select emoji based on donation value
|
||||
const emoji =
|
||||
relationUpdate >= 50
|
||||
|
||||
@@ -62,7 +62,10 @@ export class DonateTroopsExecution implements Execution {
|
||||
}
|
||||
|
||||
// Only AI nations auto-respond with emojis, human players should not
|
||||
if (this.recipient.type() === PlayerType.Nation) {
|
||||
if (
|
||||
this.recipient.type() === PlayerType.Nation &&
|
||||
this.recipient.canSendEmoji(this.sender)
|
||||
) {
|
||||
this.mg.addExecution(
|
||||
new EmojiExecution(
|
||||
this.recipient,
|
||||
|
||||
@@ -258,10 +258,10 @@ export class NationAllianceBehavior {
|
||||
case Difficulty.Easy:
|
||||
return false; // On easy we never think we have enough alliances
|
||||
case Difficulty.Medium:
|
||||
return this.player.alliances().length >= this.random.nextInt(5, 8);
|
||||
return this.player.alliances().length >= this.random.nextInt(4, 6);
|
||||
case Difficulty.Hard:
|
||||
case Difficulty.Impossible: {
|
||||
// On hard and impossible we try to not ally with all our neighbors (If we have 3+ neighbors)
|
||||
// On hard and impossible we try to not ally with all our neighbors (If we have 2+ neighbors)
|
||||
const borderingPlayers = this.player
|
||||
.neighbors()
|
||||
.filter(
|
||||
@@ -271,15 +271,15 @@ export class NationAllianceBehavior {
|
||||
(o) => this.player?.isFriendly(o) === true,
|
||||
);
|
||||
if (
|
||||
borderingPlayers.length >= 3 &&
|
||||
borderingPlayers.length >= 2 &&
|
||||
borderingPlayers.includes(otherPlayer)
|
||||
) {
|
||||
return borderingPlayers.length <= borderingFriends.length + 1;
|
||||
}
|
||||
if (difficulty === Difficulty.Hard) {
|
||||
return this.player.alliances().length >= this.random.nextInt(3, 6);
|
||||
return this.player.alliances().length >= this.random.nextInt(3, 5);
|
||||
}
|
||||
return this.player.alliances().length >= this.random.nextInt(2, 5);
|
||||
return this.player.alliances().length >= this.random.nextInt(2, 4);
|
||||
}
|
||||
default:
|
||||
assertNever(difficulty);
|
||||
@@ -310,30 +310,43 @@ export class NationAllianceBehavior {
|
||||
// It would make a lot of sense to use nextFloat here, but "there's a chance floats can cause desyncs"
|
||||
private isAlliancePartnerSimilarlyStrong(otherPlayer: Player): boolean {
|
||||
const { difficulty } = this.game.config().gameConfig();
|
||||
switch (difficulty) {
|
||||
case Difficulty.Easy:
|
||||
return (
|
||||
otherPlayer.troops() >
|
||||
this.player.troops() * (this.random.nextInt(60, 70) / 100)
|
||||
);
|
||||
case Difficulty.Medium:
|
||||
return (
|
||||
otherPlayer.troops() >
|
||||
this.player.troops() * (this.random.nextInt(70, 80) / 100)
|
||||
);
|
||||
case Difficulty.Hard:
|
||||
return (
|
||||
otherPlayer.troops() >
|
||||
this.player.troops() * (this.random.nextInt(75, 85) / 100)
|
||||
);
|
||||
case Difficulty.Impossible:
|
||||
return (
|
||||
otherPlayer.troops() >
|
||||
this.player.troops() * (this.random.nextInt(80, 90) / 100)
|
||||
);
|
||||
default:
|
||||
assertNever(difficulty);
|
||||
}
|
||||
const troopPercentRangeByDifficulty = {
|
||||
[Difficulty.Easy]: [60, 70],
|
||||
[Difficulty.Medium]: [70, 80],
|
||||
[Difficulty.Hard]: [75, 85],
|
||||
[Difficulty.Impossible]: [80, 90],
|
||||
} as const;
|
||||
const tilePercentRangeByDifficulty = {
|
||||
[Difficulty.Easy]: [70, 80],
|
||||
[Difficulty.Medium]: [80, 90],
|
||||
[Difficulty.Hard]: [85, 95],
|
||||
[Difficulty.Impossible]: [90, 100],
|
||||
} as const;
|
||||
|
||||
const troopRange = troopPercentRangeByDifficulty[difficulty];
|
||||
const tileRange = tilePercentRangeByDifficulty[difficulty];
|
||||
|
||||
const playerOutgoingTroops = this.player
|
||||
.outgoingAttacks()
|
||||
.reduce((sum, attack) => sum + attack.troops(), 0);
|
||||
const otherOutgoingTroops = otherPlayer
|
||||
.outgoingAttacks()
|
||||
.reduce((sum, attack) => sum + attack.troops(), 0);
|
||||
const playerTotalTroops = this.player.troops() + playerOutgoingTroops;
|
||||
const otherTotalTroops = otherPlayer.troops() + otherOutgoingTroops;
|
||||
|
||||
const troopThreshold =
|
||||
playerTotalTroops *
|
||||
(this.random.nextInt(troopRange[0], troopRange[1]) / 100);
|
||||
const tileThreshold =
|
||||
this.player.numTilesOwned() *
|
||||
(this.random.nextInt(tileRange[0], tileRange[1]) / 100);
|
||||
|
||||
const hasComparableTroops = otherTotalTroops > troopThreshold;
|
||||
const hasComparableTiles =
|
||||
otherPlayer.numTilesOwned() > tileThreshold &&
|
||||
otherTotalTroops > playerTotalTroops * 0.5;
|
||||
return hasComparableTroops || hasComparableTiles;
|
||||
}
|
||||
|
||||
maybeBetray(otherPlayer: Player, borderingPlayerCount: number): boolean {
|
||||
|
||||
@@ -46,7 +46,7 @@ export const EMOJI_DONATION_TOO_SMALL = (["❓", "🥱"] as const).map(emojiId);
|
||||
|
||||
export class NationEmojiBehavior {
|
||||
private readonly lastEmojiSent = new Map<Player, Tick>();
|
||||
private hasSentWinnerClap = false;
|
||||
private gameOver = false;
|
||||
|
||||
constructor(
|
||||
private random: PseudoRandom,
|
||||
@@ -107,7 +107,7 @@ export class NationEmojiBehavior {
|
||||
|
||||
// Check if game is over - send congratulations
|
||||
private congratulateWinner(): void {
|
||||
if (this.hasSentWinnerClap) return;
|
||||
if (this.gameOver) return;
|
||||
|
||||
const percentToWin = this.game.config().percentageTilesOwnedToWin();
|
||||
const numTilesWithoutFallout =
|
||||
@@ -136,10 +136,11 @@ export class NationEmojiBehavior {
|
||||
const winningPercent = (winningTiles / numTilesWithoutFallout) * 100;
|
||||
if (winningPercent < percentToWin) return;
|
||||
|
||||
this.gameOver = true;
|
||||
|
||||
// Don't congratulate if it's our own team
|
||||
if (winningTeam === this.player.team()) return;
|
||||
|
||||
this.hasSentWinnerClap = true;
|
||||
this.sendEmoji(AllPlayers, EMOJI_CONGRATULATE);
|
||||
} else {
|
||||
// FFA game: The largest nation congratulates if a human player won
|
||||
@@ -156,6 +157,8 @@ export class NationEmojiBehavior {
|
||||
(firstPlace.numTilesOwned() / numTilesWithoutFallout) * 100;
|
||||
if (firstPlacePercent < percentToWin) return;
|
||||
|
||||
this.gameOver = true;
|
||||
|
||||
// Only send if first place is a human
|
||||
if (firstPlace.type() !== PlayerType.Human) return;
|
||||
|
||||
@@ -166,13 +169,13 @@ export class NationEmojiBehavior {
|
||||
.sort((a, b) => b.numTilesOwned() - a.numTilesOwned())[0];
|
||||
if (largestNation !== this.player) return;
|
||||
|
||||
this.hasSentWinnerClap = true;
|
||||
this.sendEmoji(firstPlace, EMOJI_CONGRATULATE);
|
||||
}
|
||||
}
|
||||
|
||||
// Brag with our crown
|
||||
private brag(): void {
|
||||
if (this.gameOver) return;
|
||||
if (!this.random.chance(300)) return;
|
||||
|
||||
const sorted = this.game
|
||||
|
||||
@@ -60,9 +60,16 @@ export class NationNukeBehavior {
|
||||
const hydroCost = this.getPerceivedNukeCost(UnitType.HydrogenBomb);
|
||||
const atomCost = this.getPerceivedNukeCost(UnitType.AtomBomb);
|
||||
let nukeType: UnitType;
|
||||
if (this.player.gold() >= hydroCost) {
|
||||
if (
|
||||
!this.game.config().isUnitDisabled(UnitType.HydrogenBomb) &&
|
||||
this.player.gold() >= hydroCost
|
||||
) {
|
||||
nukeType = UnitType.HydrogenBomb;
|
||||
} else if (!this.isHydroNation && this.player.gold() >= atomCost) {
|
||||
} else if (
|
||||
!this.game.config().isUnitDisabled(UnitType.AtomBomb) &&
|
||||
(!this.isHydroNation || this.isUnderHeavyAttack()) &&
|
||||
this.player.gold() >= atomCost
|
||||
) {
|
||||
nukeType = UnitType.AtomBomb;
|
||||
} else {
|
||||
return;
|
||||
@@ -342,6 +349,11 @@ export class NationNukeBehavior {
|
||||
|
||||
// Simulate saving up for a MIRV
|
||||
private getPerceivedNukeCost(type: UnitType): Gold {
|
||||
// If MIRVs are disabled, return the actual cost
|
||||
if (this.game.config().isUnitDisabled(UnitType.MIRV)) {
|
||||
return this.cost(type);
|
||||
}
|
||||
|
||||
// Return the actual cost in team games (saving up for a MIRV is not relevant, the game will be finished before that)
|
||||
// or if we already have enough gold to buy both a MIRV and a hydro
|
||||
if (
|
||||
@@ -352,7 +364,7 @@ export class NationNukeBehavior {
|
||||
return this.cost(type);
|
||||
}
|
||||
|
||||
// On Hard & Impossible, ignore perceived cost when under heavy attack (2x troops)
|
||||
// On Hard & Impossible, ignore perceived cost when under heavy attack
|
||||
// The nation is probably going to get destroyed soon, so go all-in on nukes
|
||||
const difficulty = this.game.config().gameConfig().difficulty;
|
||||
if (
|
||||
@@ -380,8 +392,7 @@ export class NationNukeBehavior {
|
||||
|
||||
const myTroops = this.player.troops();
|
||||
|
||||
// Consider it a heavy attack if total incoming attacks have 2x our troops
|
||||
return totalIncomingTroops >= myTroops * 2;
|
||||
return totalIncomingTroops >= myTroops;
|
||||
}
|
||||
|
||||
private removeOldNukeEvents() {
|
||||
@@ -654,13 +665,13 @@ export class NationNukeBehavior {
|
||||
this.recentlySentNukes.push([tick, tile, nukeType]);
|
||||
if (nukeType === UnitType.AtomBomb) {
|
||||
this.atomBombsLaunched++;
|
||||
// Increase perceived cost by 25% each time to simulate saving up for a MIRV (higher than hydro to make atom bombs less attractive for the lategame)
|
||||
this.atomBombPerceivedCost = (this.atomBombPerceivedCost * 125n) / 100n;
|
||||
// Increase perceived cost by 35% each time to simulate saving up for a MIRV (higher than hydro to make atom bombs less attractive for the lategame)
|
||||
this.atomBombPerceivedCost = (this.atomBombPerceivedCost * 135n) / 100n;
|
||||
} else if (nukeType === UnitType.HydrogenBomb) {
|
||||
this.hydrogenBombsLaunched++;
|
||||
// Increase perceived cost by 15% each time to simulate saving up for a MIRV
|
||||
// Increase perceived cost by 25% each time to simulate saving up for a MIRV
|
||||
this.hydrogenBombPerceivedCost =
|
||||
(this.hydrogenBombPerceivedCost * 115n) / 100n;
|
||||
(this.hydrogenBombPerceivedCost * 125n) / 100n;
|
||||
}
|
||||
this.game.addExecution(new NukeExecution(nukeType, this.player, tile));
|
||||
this.emojiBehavior.maybeSendEmoji(targetPlayer, EMOJI_NUKE);
|
||||
|
||||
Reference in New Issue
Block a user