mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-24 13:52:45 +00:00
Merge branch 'evan-nations'
This commit is contained in:
+1
-1
@@ -17,7 +17,7 @@ export default {
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
statements: 21.5,
|
||||
branches: 17.0,
|
||||
branches: 16.5,
|
||||
lines: 22.0,
|
||||
functions: 20.5,
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@ import { EventBus } from "../../../core/EventBus";
|
||||
import { AllPlayers } from "../../../core/game/Game";
|
||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
import { TerraNulliusImpl } from "../../../core/game/TerraNulliusImpl";
|
||||
import { emojiTable, flattenedEmojiTable } from "../../../core/Util";
|
||||
import { Emoji, emojiTable, flattenedEmojiTable } from "../../../core/Util";
|
||||
import { CloseViewEvent, ShowEmojiMenuEvent } from "../../InputHandler";
|
||||
import { SendEmojiIntentEvent } from "../../Transport";
|
||||
import { TransformHandler } from "../TransformHandler";
|
||||
@@ -42,7 +42,7 @@ export class EmojiTable extends LitElement {
|
||||
eventBus.emit(
|
||||
new SendEmojiIntentEvent(
|
||||
recipient,
|
||||
flattenedEmojiTable.indexOf(emoji),
|
||||
flattenedEmojiTable.indexOf(emoji as Emoji),
|
||||
),
|
||||
);
|
||||
this.hideTable();
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
} from "../../../core/game/Game";
|
||||
import { TileRef } from "../../../core/game/GameMap";
|
||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
import { flattenedEmojiTable } from "../../../core/Util";
|
||||
import { Emoji, flattenedEmojiTable } from "../../../core/Util";
|
||||
import { actionButton } from "../../components/ui/ActionButton";
|
||||
import "../../components/ui/Divider";
|
||||
import Countries from "../../data/countries.json";
|
||||
@@ -218,12 +218,15 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
this.eventBus.emit(
|
||||
new SendEmojiIntentEvent(
|
||||
AllPlayers,
|
||||
flattenedEmojiTable.indexOf(emoji),
|
||||
flattenedEmojiTable.indexOf(emoji as Emoji),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
this.eventBus.emit(
|
||||
new SendEmojiIntentEvent(other, flattenedEmojiTable.indexOf(emoji)),
|
||||
new SendEmojiIntentEvent(
|
||||
other,
|
||||
flattenedEmojiTable.indexOf(emoji as Emoji),
|
||||
),
|
||||
);
|
||||
}
|
||||
this.emojiTable.hideTable();
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Config } from "../../../core/configuration/Config";
|
||||
import { AllPlayers, PlayerActions, UnitType } from "../../../core/game/Game";
|
||||
import { TileRef } from "../../../core/game/GameMap";
|
||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
import { flattenedEmojiTable } from "../../../core/Util";
|
||||
import { Emoji, flattenedEmojiTable } from "../../../core/Util";
|
||||
import { renderNumber, translateText } from "../../Utils";
|
||||
import { BuildItemDisplay, BuildMenu, flattenedBuildTable } from "./BuildMenu";
|
||||
import { ChatIntegration } from "./ChatIntegration";
|
||||
@@ -271,7 +271,7 @@ const infoEmojiElement: MenuElement = {
|
||||
: params.selected;
|
||||
params.playerActionHandler.handleEmoji(
|
||||
targetPlayer!,
|
||||
flattenedEmojiTable.indexOf(emoji),
|
||||
flattenedEmojiTable.indexOf(emoji as Emoji),
|
||||
);
|
||||
params.emojiTable.hideTable();
|
||||
});
|
||||
|
||||
+5
-3
@@ -257,7 +257,7 @@ export function createRandomName(
|
||||
return randomName;
|
||||
}
|
||||
|
||||
export const emojiTable: string[][] = [
|
||||
export const emojiTable = [
|
||||
["😀", "😊", "🥰", "😇", "😎"],
|
||||
["😞", "🥺", "😭", "😱", "😡"],
|
||||
["😈", "🤡", "🖕", "🥱", "🤦♂️"],
|
||||
@@ -269,9 +269,11 @@ export const emojiTable: string[][] = [
|
||||
["⬅️", "🎯", "➡️", "🥈", "🥉"],
|
||||
["↙️", "⬇️", "↘️", "❤️", "💔"],
|
||||
["💰", "⚓", "⛵", "🏡", "🛡️"],
|
||||
];
|
||||
] as const;
|
||||
// 2d to 1d array
|
||||
export const flattenedEmojiTable: string[] = emojiTable.flat();
|
||||
export const flattenedEmojiTable = emojiTable.flat();
|
||||
|
||||
export type Emoji = (typeof flattenedEmojiTable)[number];
|
||||
|
||||
/**
|
||||
* JSON.stringify replacer function that converts bigint values to strings.
|
||||
|
||||
@@ -20,8 +20,8 @@ export class BotExecution implements Execution {
|
||||
this.random = new PseudoRandom(simpleHash(bot.id()));
|
||||
this.attackRate = this.random.nextInt(40, 80);
|
||||
this.attackTick = this.random.nextInt(0, this.attackRate);
|
||||
this.triggerRatio = this.random.nextInt(60, 90) / 100;
|
||||
this.reserveRatio = this.random.nextInt(20, 30) / 100;
|
||||
this.triggerRatio = this.random.nextInt(50, 60) / 100;
|
||||
this.reserveRatio = this.random.nextInt(30, 40) / 100;
|
||||
this.expandRatio = this.random.nextInt(10, 20) / 100;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
Cell,
|
||||
Difficulty,
|
||||
Execution,
|
||||
Game,
|
||||
Gold,
|
||||
@@ -14,17 +13,18 @@ import {
|
||||
Unit,
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import { euclDistFN, manhattanDistFN, TileRef } from "../game/GameMap";
|
||||
import { TileRef, euclDistFN, manhattanDistFN } from "../game/GameMap";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { GameID } from "../Schemas";
|
||||
import { calculateBoundingBox, flattenedEmojiTable, simpleHash } from "../Util";
|
||||
import { calculateBoundingBox, simpleHash } from "../Util";
|
||||
import { ConstructionExecution } from "./ConstructionExecution";
|
||||
import { EmojiExecution } from "./EmojiExecution";
|
||||
import { structureSpawnTileValue } from "./nation/structureSpawnTileValue";
|
||||
import { NukeExecution } from "./NukeExecution";
|
||||
import { SpawnExecution } from "./SpawnExecution";
|
||||
import { TransportShipExecution } from "./TransportShipExecution";
|
||||
import { closestTwoTiles } from "./Util";
|
||||
import { BotBehavior } from "./utils/BotBehavior";
|
||||
import { BotBehavior, EMOJI_HECKLE } from "./utils/BotBehavior";
|
||||
|
||||
export class FakeHumanExecution implements Execution {
|
||||
private active = true;
|
||||
@@ -39,10 +39,9 @@ export class FakeHumanExecution implements Execution {
|
||||
private reserveRatio: number;
|
||||
private expandRatio: number;
|
||||
|
||||
private lastEmojiSent = new Map<Player, Tick>();
|
||||
private lastNukeSent: [Tick, TileRef][] = [];
|
||||
private embargoMalusApplied = new Set<PlayerID>();
|
||||
private heckleEmoji: number[];
|
||||
private readonly lastEmojiSent = new Map<Player, Tick>();
|
||||
private readonly lastNukeSent: [Tick, TileRef][] = [];
|
||||
private readonly embargoMalusApplied = new Set<PlayerID>();
|
||||
|
||||
constructor(
|
||||
gameID: GameID,
|
||||
@@ -53,10 +52,9 @@ export class FakeHumanExecution implements Execution {
|
||||
);
|
||||
this.attackRate = this.random.nextInt(40, 80);
|
||||
this.attackTick = this.random.nextInt(0, this.attackRate);
|
||||
this.triggerRatio = this.random.nextInt(60, 90) / 100;
|
||||
this.reserveRatio = this.random.nextInt(30, 60) / 100;
|
||||
this.expandRatio = this.random.nextInt(15, 25) / 100;
|
||||
this.heckleEmoji = ["🤡", "😡"].map((e) => flattenedEmojiTable.indexOf(e));
|
||||
this.triggerRatio = this.random.nextInt(50, 60) / 100;
|
||||
this.reserveRatio = this.random.nextInt(30, 40) / 100;
|
||||
this.expandRatio = this.random.nextInt(10, 20) / 100;
|
||||
}
|
||||
|
||||
init(mg: Game) {
|
||||
@@ -224,23 +222,12 @@ export class FakeHumanExecution implements Execution {
|
||||
const toAlly = this.random.randElement(enemies);
|
||||
if (this.player.canSendAllianceRequest(toAlly)) {
|
||||
this.player.createAllianceRequest(toAlly);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 50-50 attack weakest player vs random player
|
||||
const toAttack = this.random.chance(2)
|
||||
? enemies[0]
|
||||
: this.random.randElement(enemies);
|
||||
|
||||
if (this.shouldAttack(toAttack)) {
|
||||
this.behavior.sendAttack(toAttack);
|
||||
return;
|
||||
}
|
||||
|
||||
this.behavior.forgetOldEnemies();
|
||||
this.behavior.assistAllies();
|
||||
const enemy = this.behavior.selectEnemy();
|
||||
const enemy = this.behavior.selectEnemy(enemies);
|
||||
if (!enemy) return;
|
||||
this.maybeSendEmoji(enemy);
|
||||
this.maybeSendNuke(enemy);
|
||||
@@ -251,53 +238,6 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
}
|
||||
|
||||
private shouldAttack(other: Player): boolean {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
|
||||
if (this.player.isOnSameTeam(other)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const shouldAttack = this.attackChance(other);
|
||||
|
||||
// Consider betrayal for allies
|
||||
if (shouldAttack && this.player.isAlliedWith(other)) {
|
||||
return this.maybeConsiderBetrayal(other);
|
||||
}
|
||||
|
||||
return shouldAttack;
|
||||
}
|
||||
|
||||
private attackChance(other: Player): boolean {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
|
||||
if (this.player.isAlliedWith(other)) {
|
||||
return this.shouldDiscourageAttack(other)
|
||||
? this.random.chance(200)
|
||||
: this.random.chance(50);
|
||||
} else {
|
||||
return this.shouldDiscourageAttack(other) ? this.random.chance(4) : true;
|
||||
}
|
||||
}
|
||||
|
||||
private shouldDiscourageAttack(other: Player) {
|
||||
if (other.isTraitor()) {
|
||||
return false;
|
||||
}
|
||||
const difficulty = this.mg.config().gameConfig().difficulty;
|
||||
if (
|
||||
difficulty === Difficulty.Hard ||
|
||||
difficulty === Difficulty.Impossible
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (other.type() !== PlayerType.Human) {
|
||||
return false;
|
||||
}
|
||||
// Only discourage attacks on Humans who are not traitors on easy or medium difficulty.
|
||||
return true;
|
||||
}
|
||||
|
||||
private maybeSendEmoji(enemy: Player) {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
if (enemy.type() !== PlayerType.Human) return;
|
||||
@@ -308,7 +248,7 @@ export class FakeHumanExecution implements Execution {
|
||||
new EmojiExecution(
|
||||
this.player,
|
||||
enemy.id(),
|
||||
this.random.randElement(this.heckleEmoji),
|
||||
this.random.randElement(EMOJI_HECKLE),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -460,6 +400,8 @@ export class FakeHumanExecution implements Execution {
|
||||
this.maybeSpawnStructure(UnitType.Port) ||
|
||||
this.maybeSpawnWarship() ||
|
||||
this.maybeSpawnStructure(UnitType.Factory) ||
|
||||
this.maybeSpawnStructure(UnitType.DefensePost) ||
|
||||
this.maybeSpawnStructure(UnitType.SAMLauncher) ||
|
||||
this.maybeSpawnStructure(UnitType.MissileSilo)
|
||||
);
|
||||
}
|
||||
@@ -486,7 +428,8 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
|
||||
private structureSpawnTile(type: UnitType): TileRef | null {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
if (this.mg === undefined) throw new Error("Not initialized");
|
||||
if (this.player === null) throw new Error("Not initialized");
|
||||
const tiles =
|
||||
type === UnitType.Port
|
||||
? Array.from(this.player.borderTiles()).filter((t) =>
|
||||
@@ -494,7 +437,7 @@ export class FakeHumanExecution implements Execution {
|
||||
)
|
||||
: Array.from(this.player.tiles());
|
||||
if (tiles.length === 0) return null;
|
||||
const valueFunction = this.structureSpawnTileValue(type);
|
||||
const valueFunction = structureSpawnTileValue(this.mg, this.player, type);
|
||||
let bestTile: TileRef | null = null;
|
||||
let bestValue = 0;
|
||||
const sampledTiles = this.arraySampler(tiles);
|
||||
@@ -524,69 +467,6 @@ export class FakeHumanExecution implements Execution {
|
||||
}
|
||||
}
|
||||
|
||||
private structureSpawnTileValue(type: UnitType): (tile: TileRef) => number {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
const borderTiles = this.player.borderTiles();
|
||||
const mg = this.mg;
|
||||
const otherUnits = this.player.units(type);
|
||||
// Prefer spacing structures out of atom bomb range
|
||||
const borderSpacing = this.mg
|
||||
.config()
|
||||
.nukeMagnitudes(UnitType.AtomBomb).outer;
|
||||
const structureSpacing = borderSpacing * 2;
|
||||
switch (type) {
|
||||
case UnitType.Port:
|
||||
return (tile) => {
|
||||
let w = 0;
|
||||
|
||||
// Prefer to be far away from other structures of the same type
|
||||
const otherTiles: Set<TileRef> = new Set(
|
||||
otherUnits.map((u) => u.tile()),
|
||||
);
|
||||
otherTiles.delete(tile);
|
||||
const closestOther = closestTwoTiles(mg, otherTiles, [tile]);
|
||||
if (closestOther !== null) {
|
||||
const d = mg.manhattanDist(closestOther.x, tile);
|
||||
w += Math.min(d, structureSpacing);
|
||||
}
|
||||
|
||||
return w;
|
||||
};
|
||||
case UnitType.City:
|
||||
case UnitType.Factory:
|
||||
case UnitType.MissileSilo:
|
||||
return (tile) => {
|
||||
let w = 0;
|
||||
|
||||
// Prefer higher elevations
|
||||
w += mg.magnitude(tile);
|
||||
|
||||
// Prefer to be away from the border
|
||||
const closestBorder = closestTwoTiles(mg, borderTiles, [tile]);
|
||||
if (closestBorder !== null) {
|
||||
const d = mg.manhattanDist(closestBorder.x, tile);
|
||||
w += Math.min(d, borderSpacing);
|
||||
}
|
||||
|
||||
// Prefer to be away from other structures of the same type
|
||||
const otherTiles: Set<TileRef> = new Set(
|
||||
otherUnits.map((u) => u.tile()),
|
||||
);
|
||||
otherTiles.delete(tile);
|
||||
const closestOther = closestTwoTiles(mg, otherTiles, [tile]);
|
||||
if (closestOther !== null) {
|
||||
const d = mg.manhattanDist(closestOther.x, tile);
|
||||
w += Math.min(d, structureSpacing);
|
||||
}
|
||||
|
||||
// TODO: Cities and factories should consider train range limits
|
||||
return w;
|
||||
};
|
||||
default:
|
||||
throw new Error(`Value function not implemented for ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
private maybeSpawnWarship(): boolean {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
if (!this.random.chance(50)) {
|
||||
|
||||
@@ -50,7 +50,8 @@ class SAMTargetingSystem {
|
||||
|
||||
private isInRange(tile: TileRef) {
|
||||
const samTile = this.sam.tile();
|
||||
const rangeSquared = this.mg.config().defaultSamRange() ** 2;
|
||||
const range = this.mg.config().defaultSamRange();
|
||||
const rangeSquared = range * range;
|
||||
return this.mg.euclideanDistSquared(samTile, tile) <= rangeSquared;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import { Game, Player, Relation, UnitType } from "../../game/Game";
|
||||
import { TileRef } from "../../game/GameMap";
|
||||
import { closestTwoTiles } from "../Util";
|
||||
|
||||
export function structureSpawnTileValue(
|
||||
mg: Game,
|
||||
player: Player,
|
||||
type: UnitType,
|
||||
): (tile: TileRef) => number {
|
||||
const borderTiles = player.borderTiles();
|
||||
const otherUnits = player.units(type);
|
||||
// Prefer spacing structures out of atom bomb range
|
||||
const borderSpacing = mg.config().nukeMagnitudes(UnitType.AtomBomb).outer;
|
||||
const structureSpacing = borderSpacing * 2;
|
||||
switch (type) {
|
||||
case UnitType.City:
|
||||
case UnitType.Factory:
|
||||
case UnitType.MissileSilo: {
|
||||
return (tile) => {
|
||||
let w = 0;
|
||||
|
||||
// Prefer higher elevations
|
||||
w += mg.magnitude(tile);
|
||||
|
||||
// Prefer to be away from the border
|
||||
const closestBorder = closestTwoTiles(mg, borderTiles, [tile]);
|
||||
if (closestBorder !== null) {
|
||||
const d = mg.manhattanDist(closestBorder.x, tile);
|
||||
w += Math.min(d, borderSpacing);
|
||||
}
|
||||
|
||||
// Prefer to be away from other structures of the same type
|
||||
const otherTiles: Set<TileRef> = new Set(
|
||||
otherUnits.map((u) => u.tile()),
|
||||
);
|
||||
otherTiles.delete(tile);
|
||||
const closestOther = closestTwoTiles(mg, otherTiles, [tile]);
|
||||
if (closestOther !== null) {
|
||||
const d = mg.manhattanDist(closestOther.x, tile);
|
||||
w += Math.min(d, structureSpacing);
|
||||
}
|
||||
|
||||
// TODO: Cities and factories should consider train range limits
|
||||
return w;
|
||||
};
|
||||
}
|
||||
case UnitType.Port: {
|
||||
return (tile) => {
|
||||
let w = 0;
|
||||
|
||||
// Prefer to be away from other structures of the same type
|
||||
const otherTiles: Set<TileRef> = new Set(
|
||||
otherUnits.map((u) => u.tile()),
|
||||
);
|
||||
otherTiles.delete(tile);
|
||||
const closestOther = closestTwoTiles(mg, otherTiles, [tile]);
|
||||
if (closestOther !== null) {
|
||||
const d = mg.manhattanDist(closestOther.x, tile);
|
||||
w += Math.min(d, structureSpacing);
|
||||
}
|
||||
|
||||
return w;
|
||||
};
|
||||
}
|
||||
case UnitType.DefensePost: {
|
||||
return (tile) => {
|
||||
let w = 0;
|
||||
|
||||
// Prefer higher elevations
|
||||
w += mg.magnitude(tile);
|
||||
|
||||
const closestBorder = closestTwoTiles(mg, borderTiles, [tile]);
|
||||
if (closestBorder !== null) {
|
||||
// Prefer to be borderSpacing tiles from the border
|
||||
const d = mg.manhattanDist(closestBorder.x, tile);
|
||||
w += Math.max(0, borderSpacing - Math.abs(borderSpacing - d));
|
||||
|
||||
// Prefer adjacent players who are hostile
|
||||
const neighbors: Set<Player> = new Set();
|
||||
for (const tile of mg.neighbors(closestBorder.x)) {
|
||||
if (!mg.isLand(tile)) continue;
|
||||
const id = mg.ownerID(tile);
|
||||
if (id === player.smallID()) continue;
|
||||
const neighbor = mg.playerBySmallID(id);
|
||||
if (!neighbor.isPlayer()) continue;
|
||||
neighbors.add(neighbor);
|
||||
}
|
||||
for (const neighbor of neighbors) {
|
||||
w +=
|
||||
borderSpacing * (Relation.Friendly - player.relation(neighbor));
|
||||
}
|
||||
}
|
||||
|
||||
// Prefer to be away from other structures of the same type
|
||||
const otherTiles: Set<TileRef> = new Set(
|
||||
otherUnits.map((u) => u.tile()),
|
||||
);
|
||||
otherTiles.delete(tile);
|
||||
const closestOther = closestTwoTiles(mg, otherTiles, [tile]);
|
||||
if (closestOther !== null) {
|
||||
const d = mg.manhattanDist(closestOther.x, tile);
|
||||
w += Math.min(d, structureSpacing);
|
||||
}
|
||||
|
||||
return w;
|
||||
};
|
||||
}
|
||||
case UnitType.SAMLauncher: {
|
||||
const protectTiles: Set<TileRef> = new Set();
|
||||
for (const unit of player.units()) {
|
||||
switch (unit.type()) {
|
||||
case UnitType.City:
|
||||
case UnitType.Factory:
|
||||
case UnitType.MissileSilo:
|
||||
case UnitType.Port:
|
||||
protectTiles.add(unit.tile());
|
||||
}
|
||||
}
|
||||
const range = mg.config().defaultSamRange();
|
||||
const rangeSquared = range * range;
|
||||
return (tile) => {
|
||||
let w = 0;
|
||||
|
||||
// Prefer higher elevations
|
||||
w += mg.magnitude(tile);
|
||||
|
||||
// Prefer to be away from the border
|
||||
const closestBorder = closestTwoTiles(mg, borderTiles, [tile]);
|
||||
if (closestBorder !== null) {
|
||||
const d = mg.manhattanDist(closestBorder.x, tile);
|
||||
w += Math.min(d, borderSpacing);
|
||||
}
|
||||
|
||||
// Prefer to be away from other structures of the same type
|
||||
const otherTiles: Set<TileRef> = new Set(
|
||||
otherUnits.map((u) => u.tile()),
|
||||
);
|
||||
otherTiles.delete(tile);
|
||||
const closestOther = closestTwoTiles(mg, otherTiles, [tile]);
|
||||
if (closestOther !== null) {
|
||||
const d = mg.manhattanDist(closestOther.x, tile);
|
||||
w += Math.min(d, structureSpacing);
|
||||
}
|
||||
|
||||
// Prefer to be in range of other structures
|
||||
for (const maybeProtected of protectTiles) {
|
||||
const distanceSquared = mg.euclideanDistSquared(tile, maybeProtected);
|
||||
if (distanceSquared > rangeSquared) continue;
|
||||
w += structureSpacing;
|
||||
}
|
||||
|
||||
return w;
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new Error(`Value function not implemented for ${type}`);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
AllianceRequest,
|
||||
Difficulty,
|
||||
Game,
|
||||
Player,
|
||||
PlayerType,
|
||||
@@ -13,11 +14,17 @@ import { AllianceExtensionExecution } from "../alliance/AllianceExtensionExecuti
|
||||
import { AttackExecution } from "../AttackExecution";
|
||||
import { EmojiExecution } from "../EmojiExecution";
|
||||
|
||||
const emojiId = (e: (typeof flattenedEmojiTable)[number]) =>
|
||||
flattenedEmojiTable.indexOf(e);
|
||||
const EMOJI_ASSIST_ACCEPT = (["👍", "⛵", "🤝", "🎯"] as const).map(emojiId);
|
||||
const EMOJI_RELATION_TOO_LOW = (["🥱", "🤦♂️"] as const).map(emojiId);
|
||||
const EMOJI_TARGET_ME = (["🥺", "💀"] as const).map(emojiId);
|
||||
const EMOJI_TARGET_ALLY = (["🕊️", "👎"] as const).map(emojiId);
|
||||
export const EMOJI_HECKLE = (["🤡", "😡"] as const).map(emojiId);
|
||||
|
||||
export class BotBehavior {
|
||||
private enemy: Player | null = null;
|
||||
private enemyUpdated: Tick;
|
||||
|
||||
private assistAcceptEmoji = flattenedEmojiTable.indexOf("👍");
|
||||
private enemyUpdated: Tick | undefined;
|
||||
|
||||
constructor(
|
||||
private random: PseudoRandom,
|
||||
@@ -65,23 +72,80 @@ export class BotBehavior {
|
||||
this.game.addExecution(new EmojiExecution(this.player, player.id(), emoji));
|
||||
}
|
||||
|
||||
private setNewEnemy(newEnemy: Player | null) {
|
||||
private setNewEnemy(newEnemy: Player | null, force = false) {
|
||||
if (newEnemy !== null && !force && !this.shouldAttack(newEnemy)) return;
|
||||
this.enemy = newEnemy;
|
||||
this.enemyUpdated = this.game.ticks();
|
||||
}
|
||||
|
||||
private shouldAttack(other: Player): boolean {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
if (this.player.isOnSameTeam(other)) {
|
||||
return false;
|
||||
}
|
||||
const shouldAttack = this.attackChance(other);
|
||||
if (shouldAttack && this.player.isAlliedWith(other)) {
|
||||
this.betray(other);
|
||||
return true;
|
||||
}
|
||||
return shouldAttack;
|
||||
}
|
||||
|
||||
private betray(target: Player): void {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
const alliance = this.player.allianceWith(target);
|
||||
if (!alliance) return;
|
||||
this.player.breakAlliance(alliance);
|
||||
}
|
||||
|
||||
private attackChance(other: Player): boolean {
|
||||
if (this.player === null) throw new Error("not initialized");
|
||||
|
||||
if (this.player.isAlliedWith(other)) {
|
||||
return this.shouldDiscourageAttack(other)
|
||||
? this.random.chance(200)
|
||||
: this.random.chance(50);
|
||||
} else {
|
||||
return this.shouldDiscourageAttack(other) ? this.random.chance(4) : true;
|
||||
}
|
||||
}
|
||||
|
||||
private shouldDiscourageAttack(other: Player) {
|
||||
if (other.isTraitor()) {
|
||||
return false;
|
||||
}
|
||||
const { difficulty } = this.game.config().gameConfig();
|
||||
if (
|
||||
difficulty === Difficulty.Hard ||
|
||||
difficulty === Difficulty.Impossible
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (other.type() !== PlayerType.Human) {
|
||||
return false;
|
||||
}
|
||||
// Only discourage attacks on Humans who are not traitors on easy or medium difficulty.
|
||||
return true;
|
||||
}
|
||||
|
||||
private clearEnemy() {
|
||||
this.enemy = null;
|
||||
}
|
||||
|
||||
forgetOldEnemies() {
|
||||
// Forget old enemies
|
||||
if (this.game.ticks() - this.enemyUpdated > 100) {
|
||||
if (this.game.ticks() - (this.enemyUpdated ?? 0) > 100) {
|
||||
this.clearEnemy();
|
||||
}
|
||||
}
|
||||
|
||||
private hasSufficientTroops(): boolean {
|
||||
private hasReserveRatioTroops(): boolean {
|
||||
const maxTroops = this.game.config().maxTroops(this.player);
|
||||
const ratio = this.player.troops() / maxTroops;
|
||||
return ratio >= this.reserveRatio;
|
||||
}
|
||||
|
||||
private hasTriggerRatioTroops(): boolean {
|
||||
const maxTroops = this.game.config().maxTroops(this.player);
|
||||
const ratio = this.player.troops() / maxTroops;
|
||||
return ratio >= this.triggerRatio;
|
||||
@@ -98,7 +162,7 @@ export class BotBehavior {
|
||||
largestAttacker = attack.attacker();
|
||||
}
|
||||
if (largestAttacker !== undefined) {
|
||||
this.setNewEnemy(largestAttacker);
|
||||
this.setNewEnemy(largestAttacker, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,34 +174,37 @@ export class BotBehavior {
|
||||
}
|
||||
|
||||
assistAllies() {
|
||||
outer: for (const ally of this.player.allies()) {
|
||||
for (const ally of this.player.allies()) {
|
||||
if (ally.targets().length === 0) continue;
|
||||
if (this.player.relation(ally) < Relation.Friendly) {
|
||||
// this.emoji(ally, "🤦");
|
||||
this.emoji(ally, this.random.randElement(EMOJI_RELATION_TOO_LOW));
|
||||
continue;
|
||||
}
|
||||
for (const target of ally.targets()) {
|
||||
if (target === this.player) {
|
||||
// this.emoji(ally, "💀");
|
||||
this.emoji(ally, this.random.randElement(EMOJI_TARGET_ME));
|
||||
continue;
|
||||
}
|
||||
if (this.player.isAlliedWith(target)) {
|
||||
// this.emoji(ally, "👎");
|
||||
this.emoji(ally, this.random.randElement(EMOJI_TARGET_ALLY));
|
||||
continue;
|
||||
}
|
||||
// All checks passed, assist them
|
||||
this.player.updateRelation(ally, -20);
|
||||
this.setNewEnemy(target);
|
||||
this.emoji(ally, this.assistAcceptEmoji);
|
||||
break outer;
|
||||
this.emoji(ally, this.random.randElement(EMOJI_ASSIST_ACCEPT));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectEnemy(): Player | null {
|
||||
selectEnemy(enemies: Player[]): Player | null {
|
||||
if (this.enemy === null) {
|
||||
// Save up troops until we reach the trigger ratio
|
||||
if (!this.hasSufficientTroops()) return null;
|
||||
// Save up troops until we reach the reserve ratio
|
||||
if (!this.hasReserveRatioTroops()) return null;
|
||||
|
||||
// Maybe save up troops until we reach the trigger ratio
|
||||
if (!this.hasTriggerRatioTroops() && !this.random.chance(10)) return null;
|
||||
|
||||
// Prefer neighboring bots
|
||||
const bots = this.player
|
||||
@@ -165,11 +232,13 @@ export class BotBehavior {
|
||||
|
||||
// Retaliate against incoming attacks
|
||||
if (this.enemy === null) {
|
||||
// Only after clearing bots
|
||||
this.checkIncomingAttacks();
|
||||
}
|
||||
|
||||
// Select the most hated player
|
||||
if (this.enemy === null) {
|
||||
if (this.enemy === null && this.random.chance(2)) {
|
||||
// 50% chance
|
||||
const mostHated = this.player.allRelationsSorted()[0];
|
||||
if (
|
||||
mostHated !== undefined &&
|
||||
@@ -178,6 +247,16 @@ export class BotBehavior {
|
||||
this.setNewEnemy(mostHated.player);
|
||||
}
|
||||
}
|
||||
|
||||
// Select the weakest player
|
||||
if (this.enemy === null && enemies.length > 0) {
|
||||
this.setNewEnemy(enemies[0]);
|
||||
}
|
||||
|
||||
// Select a random player
|
||||
if (this.enemy === null && enemies.length > 0) {
|
||||
this.setNewEnemy(this.random.randElement(enemies));
|
||||
}
|
||||
}
|
||||
|
||||
// Sanity check, don't attack our allies or teammates
|
||||
@@ -187,7 +266,7 @@ export class BotBehavior {
|
||||
selectRandomEnemy(): Player | TerraNullius | null {
|
||||
if (this.enemy === null) {
|
||||
// Save up troops until we reach the trigger ratio
|
||||
if (!this.hasSufficientTroops()) return null;
|
||||
if (!this.hasTriggerRatioTroops()) return null;
|
||||
|
||||
// Choose a new enemy randomly
|
||||
const neighbors = this.player.neighbors();
|
||||
|
||||
Reference in New Issue
Block a user