mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-24 04:58:01 +00:00
Nations rarely sent boats, now they do! (#2161)
## Description: ### Nations rarely sent random boats. Now they are sending twice as many. It feels right now, not too many and not too few random boats. To make sure that small island nations with, for example, 10k troops don't repeatedly spam boats into an nation with 1.5M troops (that makes no sense), they no longer send boats to opponents which have more than twice the amount of troops. | my optimizations - 27s into the game - `8 Boats active` | openfront.io live - 27s into the game - `only 2 Boats active` | | :---: | :---: | | <img width="2560" height="1068" alt="27s into the game - 8 Boats active" src="https://github.com/user-attachments/assets/258e3d69-d86b-4c68-94ec-ade04c6c18b3" />|<img width="2560" height="1068" alt="27s into the game - 2 Boats active" src="https://github.com/user-attachments/assets/343603d6-e95d-402c-a660-cc54f715d148" />| | my optimizations - 1m20s into the game - `much more boats` | openfront.io live - 1m20s into the game - `only 4 boats active` | | :---: | :---: | | <img width="2560" height="1068" alt="1m20s into the game - much more boats" src="https://github.com/user-attachments/assets/bbe35603-8db4-4d82-af45-c77b8ef0dcbf" /> | <img width="2560" height="1068" alt="1m20s into the game - 4 boats" src="https://github.com/user-attachments/assets/f698794d-f94f-49fb-a09a-1a89b292456f" /> | ### There was a bug in the random boat sending. It did not check if the target is the player himself. That caused console warnings and a reduced amount of boat-sending. ### The Hiding-Strategy on small islands on the impossible difficulty is now harder! Because the random-boat-sending-method preferred large landmasses instead of small islands (it randomly selects a valid tile), human-players could easily hide on them, play a warship-infestation-strat and nearly NEVER get boat-attacked by nations. I implemented that the random boat functionality now searches for bots and untaken tiles before searching for nations / humans. That way, they will try to take some of these tiny islands before the human-player does. Also its cool to see nations playing on the entire available land-tiles instead of just the bigger landmasses. Fixes #1916 (Please check this issue for screenshots and more info) | my optimizations - 6m53s into the game - `only 5 islands not taken!` | openfront.io live - 6m53s into the game - `19 islands not taken!` | | :---: | :---: | | <img width="1078" height="876" alt="6m53s into the game - 5 islands not taken!" src="https://github.com/user-attachments/assets/6c103ca1-7d4c-4d0d-948f-3313839302a7" /> | <img width="1102" height="852" alt="6m53s into the game - 19 islands not taken!" src="https://github.com/user-attachments/assets/01cc512d-71c9-47aa-b4b1-f7cd5123a782" /> | | my optimizations - 6m53s into the game - `all islands taken!` | openfront.io live - 6m53s into the game - `8 islands not taken!` | | :---: | :---: | | <img width="2154" height="489" alt="6m53s into the game - 0 islands not taken!" src="https://github.com/user-attachments/assets/fc8000b2-de28-4d47-859d-0cc3c6d28ac3" /> | <img width="2181" height="317" alt="6m53s into the game - 8 islands not taken!" src="https://github.com/user-attachments/assets/093243de-f6dc-41ee-ab5e-7e485fe27646" />| ### Nations now boat-attack other nations more often! If there are two nations on two very large landmasses, which are divided by water, they nearly NEVER attacked each other. Most attack-functionality relies on the enemies sharing a border. If they don't have one, the only possible attack-mechanism is the random-boat-sending. But for very large landmasses (=> very large number of coastal tiles) it can take a long, long time before it randomly selects an enemy-tile. With the bug I described above ("did not check if the target is the player himself") it took even longer. And on the world-map, the nations have to go over iceland, this is also very unlikely. So I implemented the method selectNearestIslandEnemy, which specifically doesn't cares about borders. It makes sure that a nation always has someone to attack. This method only gets used as a fallback, and only if a nation has no borders with anybody. Fixes #1916 (Please check this issue for screenshots and more info) | my optimizations - 29m41s into the game - `China won and is in the process of killing the last enemy` | openfront.io live - NEARLY A HOUR into the game - `UK finally won. There are still 14 players on the map...` | | :---: | :---: | | <img width="2560" height="1068" alt="localhost_9000_" src="https://github.com/user-attachments/assets/a8107b88-19c8-4df7-a3fd-76d3d3e05d8e" /> | <img width="2560" height="1068" alt="openfront io_ (1)" src="https://github.com/user-attachments/assets/0d80e503-d5ed-41d4-a103-33dffc302001" /> | ## 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:
+6
-2
@@ -148,9 +148,13 @@ export function calculateBoundingBoxCenter(
|
||||
borderTiles: ReadonlySet<TileRef>,
|
||||
): Cell {
|
||||
const { min, max } = calculateBoundingBox(gm, borderTiles);
|
||||
return boundingBoxCenter({ min, max });
|
||||
}
|
||||
|
||||
export function boundingBoxCenter(box: { min: Cell; max: Cell }): Cell {
|
||||
return new Cell(
|
||||
min.x + Math.floor((max.x - min.x) / 2),
|
||||
min.y + Math.floor((max.y - min.y) / 2),
|
||||
box.min.x + Math.floor((box.max.x - box.min.x) / 2),
|
||||
box.min.y + Math.floor((box.max.y - box.min.y) / 2),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -197,43 +197,44 @@ export class FakeHumanExecution implements Execution {
|
||||
this.mg.isLand(t) && this.mg.ownerID(t) !== this.player?.smallID(),
|
||||
);
|
||||
|
||||
let borderingEnemies: Player[] = [];
|
||||
if (enemyborder.length === 0) {
|
||||
if (this.random.chance(10)) {
|
||||
if (this.random.chance(5)) {
|
||||
this.sendBoatRandomly();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this.random.chance(20)) {
|
||||
this.sendBoatRandomly();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (this.random.chance(10)) {
|
||||
this.sendBoatRandomly();
|
||||
return;
|
||||
}
|
||||
|
||||
const borderPlayers = enemyborder.map((t) =>
|
||||
this.mg.playerBySmallID(this.mg.ownerID(t)),
|
||||
);
|
||||
if (borderPlayers.some((o) => !o.isPlayer())) {
|
||||
this.behavior.sendAttack(this.mg.terraNullius());
|
||||
return;
|
||||
}
|
||||
const borderPlayers = enemyborder.map((t) =>
|
||||
this.mg.playerBySmallID(this.mg.ownerID(t)),
|
||||
);
|
||||
if (borderPlayers.some((o) => !o.isPlayer())) {
|
||||
this.behavior.sendAttack(this.mg.terraNullius());
|
||||
return;
|
||||
}
|
||||
|
||||
const enemies = borderPlayers
|
||||
.filter((o) => o.isPlayer())
|
||||
.sort((a, b) => a.troops() - b.troops());
|
||||
borderingEnemies = borderPlayers
|
||||
.filter((o) => o.isPlayer())
|
||||
.sort((a, b) => a.troops() - b.troops());
|
||||
|
||||
// 5% chance to send a random alliance request
|
||||
if (this.random.chance(20)) {
|
||||
const toAlly = this.random.randElement(enemies);
|
||||
if (this.player.canSendAllianceRequest(toAlly)) {
|
||||
this.mg.addExecution(
|
||||
new AllianceRequestExecution(this.player, toAlly.id()),
|
||||
);
|
||||
// 5% chance to send a random alliance request
|
||||
if (this.random.chance(20)) {
|
||||
const toAlly = this.random.randElement(borderingEnemies);
|
||||
if (this.player.canSendAllianceRequest(toAlly)) {
|
||||
this.mg.addExecution(
|
||||
new AllianceRequestExecution(this.player, toAlly.id()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.behavior.forgetOldEnemies();
|
||||
this.behavior.assistAllies();
|
||||
|
||||
const enemy = this.behavior.selectEnemy(enemies);
|
||||
const enemy = this.behavior.selectEnemy(borderingEnemies);
|
||||
if (!enemy) return;
|
||||
this.maybeSendEmoji(enemy);
|
||||
this.maybeSendNuke(enemy);
|
||||
@@ -592,9 +593,14 @@ export class FakeHumanExecution implements Execution {
|
||||
|
||||
const src = this.random.randElement(oceanShore);
|
||||
|
||||
const dst = this.randomBoatTarget(src, 150);
|
||||
// First look for high-interest targets (unowned or bot-owned). Mainly relevant for earlygame
|
||||
let dst = this.randomBoatTarget(src, 150, true);
|
||||
if (dst === null) {
|
||||
return;
|
||||
// None found? Then look for players
|
||||
dst = this.randomBoatTarget(src, 150, false);
|
||||
if (dst === null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.mg.addExecution(
|
||||
@@ -634,7 +640,11 @@ export class FakeHumanExecution implements Execution {
|
||||
return null;
|
||||
}
|
||||
|
||||
private randomBoatTarget(tile: TileRef, dist: number): TileRef | null {
|
||||
private randomBoatTarget(
|
||||
tile: TileRef,
|
||||
dist: number,
|
||||
highInterestOnly: boolean = false,
|
||||
): TileRef | null {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
const x = this.mg.x(tile);
|
||||
const y = this.mg.y(tile);
|
||||
@@ -649,11 +659,23 @@ export class FakeHumanExecution implements Execution {
|
||||
continue;
|
||||
}
|
||||
const owner = this.mg.owner(randTile);
|
||||
if (!owner.isPlayer()) {
|
||||
return randTile;
|
||||
if (owner === this.player) {
|
||||
continue;
|
||||
}
|
||||
if (!owner.isFriendly(this.player)) {
|
||||
return randTile;
|
||||
// Don't spam boats into players that are more than twice as large as us
|
||||
if (owner.isPlayer() && owner.troops() > this.player.troops() * 2) {
|
||||
continue;
|
||||
}
|
||||
// High-interest targeting: prioritize unowned tiles or tiles owned by bots
|
||||
if (highInterestOnly) {
|
||||
if (!owner.isPlayer() || owner.type() === PlayerType.Bot) {
|
||||
return randTile;
|
||||
}
|
||||
} else {
|
||||
// Normal targeting: return unowned tiles or tiles owned by non-friendly players
|
||||
if (!owner.isPlayer() || !owner.isFriendly(this.player)) {
|
||||
return randTile;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -9,7 +9,11 @@ import {
|
||||
Tick,
|
||||
} from "../../game/Game";
|
||||
import { PseudoRandom } from "../../PseudoRandom";
|
||||
import { flattenedEmojiTable } from "../../Util";
|
||||
import {
|
||||
boundingBoxCenter,
|
||||
calculateBoundingBoxCenter,
|
||||
flattenedEmojiTable,
|
||||
} from "../../Util";
|
||||
import { AllianceExtensionExecution } from "../alliance/AllianceExtensionExecution";
|
||||
import { AttackExecution } from "../AttackExecution";
|
||||
import { EmojiExecution } from "../EmojiExecution";
|
||||
@@ -198,7 +202,7 @@ export class BotBehavior {
|
||||
}
|
||||
}
|
||||
|
||||
selectEnemy(enemies: Player[]): Player | null {
|
||||
selectEnemy(borderingEnemies: Player[]): Player | null {
|
||||
if (this.enemy === null) {
|
||||
// Save up troops until we reach the reserve ratio
|
||||
if (!this.hasReserveRatioTroops()) return null;
|
||||
@@ -249,13 +253,18 @@ export class BotBehavior {
|
||||
}
|
||||
|
||||
// Select the weakest player
|
||||
if (this.enemy === null && enemies.length > 0) {
|
||||
this.setNewEnemy(enemies[0]);
|
||||
if (this.enemy === null && borderingEnemies.length > 0) {
|
||||
this.setNewEnemy(borderingEnemies[0]);
|
||||
}
|
||||
|
||||
// Select a random player
|
||||
if (this.enemy === null && enemies.length > 0) {
|
||||
this.setNewEnemy(this.random.randElement(enemies));
|
||||
if (this.enemy === null && borderingEnemies.length > 0) {
|
||||
this.setNewEnemy(this.random.randElement(borderingEnemies));
|
||||
}
|
||||
|
||||
// If we don't have bordering enemies, we are on an island. Attack someone on an island next to us
|
||||
if (this.enemy === null && borderingEnemies.length === 0) {
|
||||
this.selectNearestIslandEnemy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,6 +272,64 @@ export class BotBehavior {
|
||||
return this.enemySanityCheck();
|
||||
}
|
||||
|
||||
getPlayerCenter(player: Player) {
|
||||
if (player.largestClusterBoundingBox) {
|
||||
return boundingBoxCenter(player.largestClusterBoundingBox);
|
||||
}
|
||||
return calculateBoundingBoxCenter(this.game, player.borderTiles());
|
||||
}
|
||||
|
||||
selectNearestIslandEnemy() {
|
||||
const myBorder = this.player.borderTiles();
|
||||
if (myBorder.size === 0) return;
|
||||
|
||||
const filteredPlayers = this.game.players().filter((p) => {
|
||||
if (p === this.player) return false;
|
||||
if (!p.isAlive()) return false;
|
||||
if (p.borderTiles().size === 0) return false;
|
||||
if (this.player.isFriendly(p)) return false;
|
||||
// Don't spam boats into players more than 2x our troops
|
||||
return p.troops() <= this.player.troops() * 2;
|
||||
});
|
||||
|
||||
if (filteredPlayers.length > 0) {
|
||||
const playerCenter = this.getPlayerCenter(this.player);
|
||||
|
||||
const sortedPlayers = filteredPlayers
|
||||
.map((filteredPlayer) => {
|
||||
const filteredPlayerCenter = this.getPlayerCenter(filteredPlayer);
|
||||
|
||||
const playerCenterTile = this.game.ref(
|
||||
playerCenter.x,
|
||||
playerCenter.y,
|
||||
);
|
||||
const filteredPlayerCenterTile = this.game.ref(
|
||||
filteredPlayerCenter.x,
|
||||
filteredPlayerCenter.y,
|
||||
);
|
||||
|
||||
const distance = this.game.manhattanDist(
|
||||
playerCenterTile,
|
||||
filteredPlayerCenterTile,
|
||||
);
|
||||
return { player: filteredPlayer, distance };
|
||||
})
|
||||
.sort((a, b) => a.distance - b.distance); // Sort by distance (ascending)
|
||||
|
||||
// Select the nearest or second-nearest enemy (So our boat doesn't always run into the same warship, if there is one)
|
||||
let selectedEnemy: Player | null;
|
||||
if (sortedPlayers.length > 1 && this.random.chance(2)) {
|
||||
selectedEnemy = sortedPlayers[1].player;
|
||||
} else {
|
||||
selectedEnemy = sortedPlayers[0].player;
|
||||
}
|
||||
|
||||
if (selectedEnemy !== null) {
|
||||
this.setNewEnemy(selectedEnemy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectRandomEnemy(): Player | TerraNullius | null {
|
||||
if (this.enemy === null) {
|
||||
// Save up troops until we reach the trigger ratio
|
||||
|
||||
Reference in New Issue
Block a user