Merge branch 'main' into patterned-territory

This commit is contained in:
Aotumuri
2025-05-17 16:19:00 +09:00
committed by GitHub
773 changed files with 15719 additions and 12321 deletions
+112 -127
View File
@@ -20,8 +20,9 @@ import {
Attack,
BuildableUnit,
Cell,
ColoredTeams,
Embargo,
EmojiMessage,
GameMode,
Gold,
MessageType,
MutableAlliance,
@@ -35,13 +36,12 @@ import {
TerraNullius,
Tick,
Unit,
UnitSpecificInfos,
UnitParams,
UnitType,
} from "./Game";
import { GameImpl } from "./GameImpl";
import { andFN, manhattanDistFN, TileRef } from "./GameMap";
import { AttackUpdate, GameUpdateType, PlayerUpdate } from "./GameUpdates";
import { TerraNulliusImpl } from "./TerraNulliusImpl";
import {
bestShoreDeploymentSource,
canBuildTransportShip,
@@ -73,15 +73,15 @@ export class PlayerImpl implements Player {
markedTraitorTick = -1;
private embargoes: Set<PlayerID> = new Set();
private embargoes = new Map<PlayerID, Embargo>();
public _borderTiles: Set<TileRef> = new Set();
public _units: UnitImpl[] = [];
public _units: Unit[] = [];
public _tiles: Set<TileRef> = new Set();
private _pattern: string;
private _flag: string;
private _pattern: string | undefined;
private _flag: string | undefined;
private _name: string;
private _displayName: string;
@@ -134,7 +134,7 @@ export class PlayerImpl implements Player {
name: this.name(),
displayName: this.displayName(),
id: this.id(),
team: this.team(),
team: this.team() ?? undefined,
smallID: this.smallID(),
playerType: this.type(),
isAlive: this.isAlive(),
@@ -145,7 +145,7 @@ export class PlayerImpl implements Player {
troops: this.troops(),
targetTroopRatio: this.targetTroopRatio(),
allies: this.alliances().map((a) => a.other(this).smallID()),
embargoes: this.embargoes,
embargoes: new Set([...this.embargoes.keys()].map((p) => p.toString())),
isTraitor: this.isTraitor(),
targets: this.targets().map((p) => p.smallID()),
outgoingEmojis: this.outgoingEmojis(),
@@ -179,11 +179,11 @@ export class PlayerImpl implements Player {
return this._smallID;
}
pattern(): string {
pattern(): string | undefined {
return this._pattern;
}
flag(): string {
flag(): string | undefined {
return this._flag;
}
@@ -194,7 +194,7 @@ export class PlayerImpl implements Player {
return this._displayName;
}
clientID(): ClientID {
clientID(): ClientID | null {
return this.playerInfo.clientID;
}
@@ -210,8 +210,8 @@ export class PlayerImpl implements Player {
return this.playerInfo.clan;
}
units(...types: UnitType[]): UnitImpl[] {
if (types.length == 0) {
units(...types: UnitType[]): Unit[] {
if (types.length === 0) {
return this._units;
}
const ts = new Set(types);
@@ -222,7 +222,7 @@ export class PlayerImpl implements Player {
const units = this.units(type);
units.push(
...this.units(UnitType.Construction).filter(
(u) => u.constructionType() == type,
(u) => u.constructionType() === type,
),
);
return units;
@@ -231,7 +231,7 @@ export class PlayerImpl implements Player {
sharesBorderWith(other: Player | TerraNullius): boolean {
for (const border of this._borderTiles) {
for (const neighbor of this.mg.map().neighbors(border)) {
if (this.mg.map().ownerID(neighbor) == other.smallID()) {
if (this.mg.map().ownerID(neighbor) === other.smallID()) {
return true;
}
}
@@ -256,10 +256,8 @@ export class PlayerImpl implements Player {
for (const neighbor of this.mg.map().neighbors(border)) {
if (this.mg.map().isLand(neighbor)) {
const owner = this.mg.map().ownerID(neighbor);
if (owner != this.smallID()) {
ns.add(
this.mg.playerBySmallID(owner) as PlayerImpl | TerraNulliusImpl,
);
if (owner !== this.smallID()) {
ns.add(this.mg.playerBySmallID(owner) as Player | TerraNullius);
}
}
}
@@ -277,7 +275,7 @@ export class PlayerImpl implements Player {
this.mg.conquer(this, tile);
}
orderRetreat(id: string) {
const attack = this._outgoingAttacks.filter((attack) => attack.id() == id);
const attack = this._outgoingAttacks.filter((attack) => attack.id() === id);
if (!attack || !attack[0]) {
consolex.warn(`Didn't find outgoing attack with id ${id}`);
return;
@@ -285,7 +283,7 @@ export class PlayerImpl implements Player {
attack[0].orderRetreat();
}
executeRetreat(id: string): void {
const attack = this._outgoingAttacks.filter((attack) => attack.id() == id);
const attack = this._outgoingAttacks.filter((attack) => attack.id() === id);
// Execution is delayed so it's not an error that the attack does not exist.
if (!attack || !attack[0]) {
return;
@@ -293,7 +291,7 @@ export class PlayerImpl implements Player {
attack[0].executeRetreat();
}
relinquish(tile: TileRef) {
if (this.mg.owner(tile) != this) {
if (this.mg.owner(tile) !== this) {
throw new Error(`Cannot relinquish tile not owned by this player`);
}
this.mg.relinquish(tile);
@@ -314,16 +312,16 @@ export class PlayerImpl implements Player {
}
incomingAllianceRequests(): AllianceRequest[] {
return this.mg.allianceRequests.filter((ar) => ar.recipient() == this);
return this.mg.allianceRequests.filter((ar) => ar.recipient() === this);
}
outgoingAllianceRequests(): AllianceRequest[] {
return this.mg.allianceRequests.filter((ar) => ar.requestor() == this);
return this.mg.allianceRequests.filter((ar) => ar.requestor() === this);
}
alliances(): MutableAlliance[] {
return this.mg.alliances_.filter(
(a) => a.requestor() == this || a.recipient() == this,
(a) => a.requestor() === this || a.recipient() === this,
);
}
@@ -332,23 +330,25 @@ export class PlayerImpl implements Player {
}
isAlliedWith(other: Player): boolean {
if (other == this) {
if (other === this) {
return false;
}
return this.allianceWith(other) != null;
return this.allianceWith(other) !== null;
}
allianceWith(other: Player): MutableAlliance | null {
if (other == this) {
if (other === this) {
return null;
}
return this.alliances().find(
(a) => a.recipient() == other || a.requestor() == other,
return (
this.alliances().find(
(a) => a.recipient() === other || a.requestor() === other,
) ?? null
);
}
canSendAllianceRequest(other: Player): boolean {
if (other == this) {
if (other === this) {
return false;
}
if (this.isFriendly(other)) {
@@ -356,20 +356,18 @@ export class PlayerImpl implements Player {
}
const hasPending =
this.incomingAllianceRequests().find((ar) => ar.requestor() == other) !=
null ||
this.outgoingAllianceRequests().find((ar) => ar.recipient() == other) !=
null;
this.incomingAllianceRequests().some((ar) => ar.requestor() === other) ||
this.outgoingAllianceRequests().some((ar) => ar.recipient() === other);
if (hasPending) {
return false;
}
const recent = this.pastOutgoingAllianceRequests
.filter((ar) => ar.recipient() == other)
.filter((ar) => ar.recipient() === other)
.sort((a, b) => b.createdAt() - a.createdAt());
if (recent.length == 0) {
if (recent.length === 0) {
return true;
}
@@ -393,7 +391,7 @@ export class PlayerImpl implements Player {
this.markedTraitorTick = this.mg.ticks();
}
createAllianceRequest(recipient: Player): AllianceRequest {
createAllianceRequest(recipient: Player): AllianceRequest | null {
if (this.isAlliedWith(recipient)) {
throw new Error(`cannot create alliance request, already allies`);
}
@@ -401,13 +399,11 @@ export class PlayerImpl implements Player {
}
relation(other: Player): Relation {
if (other == this) {
if (other === this) {
throw new Error(`cannot get relation with self: ${this}`);
}
if (this.relations.has(other)) {
return this.relationFromValue(this.relations.get(other));
}
return Relation.Neutral;
const relation = this.relations.get(other) ?? 0;
return this.relationFromValue(relation);
}
private relationFromValue(relationValue: number): Relation {
@@ -433,13 +429,10 @@ export class PlayerImpl implements Player {
}
updateRelation(other: Player, delta: number): void {
if (other == this) {
if (other === this) {
throw new Error(`cannot update relation with self: ${this}`);
}
let relation = 0;
if (this.relations.has(other)) {
relation = this.relations.get(other);
}
const relation = this.relations.get(other) ?? 0;
const newRelation = within(relation + delta, -100, 100);
this.relations.set(other, newRelation);
}
@@ -457,7 +450,7 @@ export class PlayerImpl implements Player {
}
canTarget(other: Player): boolean {
if (this == other) {
if (this === other) {
return false;
}
if (this.isFriendly(other)) {
@@ -476,12 +469,12 @@ export class PlayerImpl implements Player {
this.mg.target(this, other);
}
targets(): PlayerImpl[] {
targets(): Player[] {
return this.targets_
.filter(
(t) => this.mg.ticks() - t.tick < this.mg.config().targetDuration(),
)
.map((t) => t.target as PlayerImpl);
.map((t) => t.target);
}
transitiveTargets(): Player[] {
@@ -493,13 +486,13 @@ export class PlayerImpl implements Player {
}
sendEmoji(recipient: Player | typeof AllPlayers, emoji: string): void {
if (recipient == this) {
if (recipient === this) {
throw Error(`Cannot send emoji to oneself: ${this}`);
}
const msg: EmojiMessage = {
message: emoji,
senderID: this.smallID(),
recipientID: recipient == AllPlayers ? recipient : recipient.smallID(),
recipientID: recipient === AllPlayers ? recipient : recipient.smallID(),
createdAt: this.mg.ticks(),
};
this.outgoingEmojis_.push(msg);
@@ -518,9 +511,9 @@ export class PlayerImpl implements Player {
canSendEmoji(recipient: Player | typeof AllPlayers): boolean {
const recipientID =
recipient == AllPlayers ? AllPlayers : recipient.smallID();
recipient === AllPlayers ? AllPlayers : recipient.smallID();
const prevMsgs = this.outgoingEmojis_.filter(
(msg) => msg.recipientID == recipientID,
(msg) => msg.recipientID === recipientID,
);
for (const msg of prevMsgs) {
if (
@@ -534,18 +527,11 @@ export class PlayerImpl implements Player {
}
canDonate(recipient: Player): boolean {
if (
recipient.type() == PlayerType.Human &&
this.mg.config().gameConfig().gameMode == GameMode.FFA
) {
return false;
}
if (!this.isFriendly(recipient)) {
return false;
}
for (const donation of this.sentDonations) {
if (donation.recipient == recipient) {
if (donation.recipient === recipient) {
if (
this.mg.ticks() - donation.tick <
this.mg.config().donateCooldown()
@@ -593,21 +579,39 @@ export class PlayerImpl implements Player {
canTrade(other: Player): boolean {
const embargo =
other.hasEmbargoAgainst(this) || this.hasEmbargoAgainst(other);
return !embargo && other.id() != this.id();
return !embargo && other.id() !== this.id();
}
addEmbargo(other: PlayerID): void {
this.embargoes.add(other);
addEmbargo(other: PlayerID, isTemporary: boolean): void {
const embargo = this.embargoes.get(other);
if (embargo !== undefined && !embargo.isTemporary) return;
this.embargoes.set(other, {
createdAt: this.mg.ticks(),
isTemporary: isTemporary,
target: other,
});
}
getEmbargoes(): Embargo[] {
return [...this.embargoes.values()];
}
stopEmbargo(other: PlayerID): void {
this.embargoes.delete(other);
}
endTemporaryEmbargo(other: PlayerID): void {
const embargo = this.embargoes.get(other);
if (embargo !== undefined && !embargo.isTemporary) return;
this.stopEmbargo(other);
}
tradingPartners(): Player[] {
return this.mg
.players()
.filter((other) => other != this && this.canTrade(other));
.filter((other) => other !== this && this.canTrade(other));
}
team(): Team | null {
@@ -615,13 +619,16 @@ export class PlayerImpl implements Player {
}
isOnSameTeam(other: Player): boolean {
if (other == this) {
if (other === this) {
return false;
}
if (this.team() == null || other.team() == null) {
if (this.team() === null || other.team() === null) {
return false;
}
return this._team == other.team();
if (this.team() === ColoredTeams.Bot || other.team() === ColoredTeams.Bot) {
return false;
}
return this._team === other.team();
}
isFriendly(other: Player): boolean {
@@ -692,47 +699,35 @@ export class PlayerImpl implements Player {
}
captureUnit(unit: Unit): void {
if (unit.owner() == this) {
if (unit.owner() === this) {
throw new Error(`Cannot capture unit, ${this} already owns ${unit}`);
}
const prev = unit.owner();
(prev as PlayerImpl)._units = (prev as PlayerImpl)._units.filter(
(u) => u != unit,
);
(unit as UnitImpl)._owner = this;
this._units.push(unit as UnitImpl);
this.mg.addUpdate(unit.toUpdate());
this.mg.displayMessage(
`${unit.type()} captured by ${this.displayName()}`,
MessageType.ERROR,
prev.id(),
);
this.mg.displayMessage(
`Captured ${unit.type()} from ${prev.displayName()}`,
MessageType.SUCCESS,
this.id(),
);
unit.setOwner(this);
}
buildUnit(
type: UnitType,
troops: number,
buildUnit<T extends UnitType>(
type: T,
spawnTile: TileRef,
unitSpecificInfos: UnitSpecificInfos = {},
): UnitImpl {
params: UnitParams<T>,
): Unit {
if (this.mg.config().isUnitDisabled(type)) {
throw new Error(
`Attempted to build disabled unit ${type} at tile ${spawnTile} by player ${this.name()}`,
);
}
const cost = this.mg.unitInfo(type).cost(this);
const b = new UnitImpl(
type,
this.mg,
spawnTile,
troops,
this.mg.nextUnitID(),
this,
unitSpecificInfos,
params,
);
this._units.push(b);
this.removeGold(cost);
this.removeTroops(troops);
this.removeTroops("troops" in params ? (params.troops ?? 0) : 0);
this.mg.addUpdate(b.toUpdate());
this.mg.addUnit(b);
@@ -744,7 +739,9 @@ export class PlayerImpl implements Player {
return Object.values(UnitType).map((u) => {
return {
type: u,
canBuild: this.canBuild(u, tile, validTiles),
canBuild: this.mg.inSpawnPhase()
? false
: this.canBuild(u, tile, validTiles),
cost: this.mg.config().unitInfo(u).cost(this),
} as BuildableUnit;
});
@@ -755,19 +752,8 @@ export class PlayerImpl implements Player {
targetTile: TileRef,
validTiles: TileRef[] | null = null,
): TileRef | false {
// prevent the building of nukes and nuke related buildings
if (this.mg.config().disableNukes()) {
if (
unitType === UnitType.MissileSilo ||
unitType === UnitType.MIRV ||
unitType === UnitType.AtomBomb ||
unitType === UnitType.HydrogenBomb ||
unitType === UnitType.SAMLauncher ||
unitType === UnitType.SAMMissile ||
unitType === UnitType.MIRVWarhead
) {
return false;
}
if (this.mg.config().isUnitDisabled(unitType)) {
return false;
}
const cost = this.mg.unitInfo(unitType).cost(this);
@@ -816,25 +802,24 @@ export class PlayerImpl implements Player {
}
// only get missilesilos that are not on cooldown
const spawns = this.units(UnitType.MissileSilo)
.map((u) => u as Unit)
.filter((silo) => {
return !silo.isCooldown();
})
.sort(distSortUnit(this.mg, tile));
if (spawns.length == 0) {
if (spawns.length === 0) {
return false;
}
return spawns[0].tile();
}
portSpawn(tile: TileRef, validTiles: TileRef[]): TileRef | false {
portSpawn(tile: TileRef, validTiles: TileRef[] | null): TileRef | false {
const spawns = Array.from(
this.mg.bfs(
tile,
manhattanDistFN(tile, this.mg.config().radiusPortSpawn()),
),
)
.filter((t) => this.mg.owner(t) == this && this.mg.isOceanShore(t))
.filter((t) => this.mg.owner(t) === this && this.mg.isOceanShore(t))
.sort(
(a, b) =>
this.mg.manhattanDist(a, tile) - this.mg.manhattanDist(b, tile),
@@ -859,7 +844,7 @@ export class PlayerImpl implements Player {
this.mg.manhattanDist(a.tile(), tile) -
this.mg.manhattanDist(b.tile(), tile),
);
if (spawns.length == 0) {
if (spawns.length === 0) {
return false;
}
return spawns[0].tile();
@@ -870,14 +855,14 @@ export class PlayerImpl implements Player {
validTiles: TileRef[] | null = null,
): TileRef | false {
const tiles = validTiles ?? this.validStructureSpawnTiles(tile);
if (tiles.length == 0) {
if (tiles.length === 0) {
return false;
}
return tiles[0];
}
private validStructureSpawnTiles(tile: TileRef): TileRef[] {
if (this.mg.owner(tile) != this) {
if (this.mg.owner(tile) !== this) {
return [];
}
const searchRadius = 15;
@@ -892,7 +877,7 @@ export class PlayerImpl implements Player {
const nearbyTiles = this.mg.bfs(tile, (gm, t) => {
return (
this.mg.euclideanDistSquared(tile, t) < searchRadiusSquared &&
gm.ownerID(t) == this.smallID()
gm.ownerID(t) === this.smallID()
);
});
const validSet: Set<TileRef> = new Set(nearbyTiles);
@@ -917,9 +902,9 @@ export class PlayerImpl implements Player {
tradeShipSpawn(targetTile: TileRef): TileRef | false {
const spawns = this.units(UnitType.Port).filter(
(u) => u.tile() == targetTile,
(u) => u.tile() === targetTile,
);
if (spawns.length == 0) {
if (spawns.length === 0) {
return false;
}
return spawns[0].tile();
@@ -931,7 +916,7 @@ export class PlayerImpl implements Player {
hash(): number {
return (
simpleHash(this.id()) * (this.population() + this.numTilesOwned()) +
this._units.reduce((acc, unit) => acc + unit.hash(), 0)
this._units.reduce((acc, unit) => acc + (unit as UnitImpl).hash(), 0)
);
}
toString(): string {
@@ -958,7 +943,7 @@ export class PlayerImpl implements Player {
createAttack(
target: Player | TerraNullius,
troops: number,
sourceTile: TileRef,
sourceTile: TileRef | null,
): Attack {
const attack = new AttackImpl(
this._pseudo_random.nextID(),
@@ -990,7 +975,7 @@ export class PlayerImpl implements Player {
return false;
}
if (this.mg.owner(tile) == this) {
if (this.mg.owner(tile) === this) {
return false;
}
if (this.mg.hasOwner(tile)) {
@@ -1014,7 +999,7 @@ export class PlayerImpl implements Player {
),
)) {
for (const n of this.mg.neighbors(t)) {
if (this.mg.owner(n) == this) {
if (this.mg.owner(n) === this) {
return true;
}
}
@@ -1032,7 +1017,7 @@ export class PlayerImpl implements Player {
tradingPorts(port: Unit): Unit[] {
const ports = this.mg
.players()
.filter((p) => p != port.owner() && p.canTrade(port.owner()))
.filter((p) => p !== port.owner() && p.canTrade(port.owner()))
.flatMap((p) => p.units(UnitType.Port))
.sort((p1, p2) => {
return (
@@ -1053,7 +1038,7 @@ export class PlayerImpl implements Player {
// Make ally ports twice more likely by putting them again
this.mg
.players()
.filter((p) => p != port.owner() && p.canTrade(port.owner()))
.filter((p) => p !== port.owner() && p.canTrade(port.owner()))
.filter((p) => p.isAlliedWith(port.owner()))
.flatMap((p) => p.units(UnitType.Port))
.forEach((p) => ports.push(p));