Files
OpenFrontIO/src/core/execution/TribeExecution.ts
T
PGray be9ea14fe9 refactor: rename Bot to Tribe in internal execution code (#3372)
## Description

Follows up on #3290 which renamed the user-facing "Bots" to "Tribes".
This renames the internal implementation to match:

- `BotExecution` → `TribeExecution`
- `BotSpawner` → `TribeSpawner`
- `BotNames` → `TribeNames` (`BOT_NAME_*` → `TRIBE_NAME_*`)

All callers updated accordingly. `PlayerType.Bot` and `ColoredTeams.Bot`
are intentionally left unchanged as they are serialised wire-format
values.

Closes #3335

## Please complete the following:
- [x] My changes do not break existing functionality
- [x] I have tested my changes

## Please put your Discord username so you can be contacted if a bug or
regression is found:
Just reply here

---------

Co-authored-by: PGray <PGrayCS@users.noreply.github.com>
2026-03-09 21:31:18 -07:00

129 lines
3.7 KiB
TypeScript

import { Execution, Game, Player, Structures } from "../game/Game";
import { PseudoRandom } from "../PseudoRandom";
import { simpleHash } from "../Util";
import { AllianceExtensionExecution } from "./alliance/AllianceExtensionExecution";
import { DeleteUnitExecution } from "./DeleteUnitExecution";
import { AiAttackBehavior } from "./utils/AiAttackBehavior";
export class TribeExecution implements Execution {
private active = true;
private random: PseudoRandom;
private mg: Game;
private neighborsTerraNullius = true;
private attackBehavior: AiAttackBehavior | null = null;
private attackRate: number;
private attackTick: number;
private triggerRatio: number;
private reserveRatio: number;
private expandRatio: number;
constructor(private tribe: Player) {
this.random = new PseudoRandom(simpleHash(tribe.id()));
this.attackRate = this.random.nextInt(40, 80);
this.attackTick = this.random.nextInt(0, this.attackRate);
this.triggerRatio = this.random.nextInt(50, 60) / 100;
this.reserveRatio = this.random.nextInt(30, 40) / 100;
this.expandRatio = this.random.nextInt(10, 20) / 100;
}
activeDuringSpawnPhase(): boolean {
return false;
}
init(mg: Game) {
this.mg = mg;
}
tick(ticks: number) {
if (ticks % this.attackRate !== this.attackTick) return;
if (!this.tribe.isAlive()) {
//removeOnDeath is called from tribe's PlayerExecution
this.active = false;
return;
}
if (this.attackBehavior === null) {
this.attackBehavior = new AiAttackBehavior(
this.random,
this.mg,
this.tribe,
this.triggerRatio,
this.reserveRatio,
this.expandRatio,
);
// Send an attack on the first tick
this.attackBehavior.sendAttack(this.mg.terraNullius());
return;
}
this.acceptAllAllianceRequests();
this.deleteAllStructures();
this.maybeAttack();
}
private acceptAllAllianceRequests() {
// Accept all alliance requests
for (const req of this.tribe.incomingAllianceRequests()) {
req.accept();
}
// Accept all alliance extension requests
for (const alliance of this.tribe.alliances()) {
// Alliance expiration tracked by Events Panel, only human ally can click Request to Renew
// Skip if no expiration yet/ ally didn't request extension yet / tribe already agreed to extend
if (!alliance.onlyOneAgreedToExtend()) continue;
const human = alliance.other(this.tribe);
this.mg.addExecution(
new AllianceExtensionExecution(this.tribe, human.id()),
);
}
}
private deleteAllStructures() {
for (const unit of this.tribe.units()) {
if (Structures.has(unit.type()) && this.tribe.canDeleteUnit()) {
this.mg.addExecution(new DeleteUnitExecution(this.tribe, unit.id()));
}
}
}
private maybeAttack() {
if (this.attackBehavior === null) {
throw new Error("not initialized");
}
const toAttack = this.attackBehavior.getNeighborTraitorToAttack();
if (toAttack !== null) {
const odds = this.tribe.isFriendly(toAttack) ? 6 : 3;
if (this.random.chance(odds)) {
// Check and break alliance before attacking if needed
const alliance = this.tribe.allianceWith(toAttack);
if (alliance !== null) {
this.tribe.breakAlliance(alliance);
}
this.attackBehavior.sendAttack(toAttack);
return;
}
}
if (this.neighborsTerraNullius) {
if (this.tribe.neighbors().some((n) => !n.isPlayer())) {
this.attackBehavior.sendAttack(this.mg.terraNullius());
return;
}
this.neighborsTerraNullius = false;
}
this.attackBehavior.attackRandomTarget();
}
isActive(): boolean {
return this.active;
}
}