Merge main into v0.25.x

This commit is contained in:
Scott Anderson
2025-07-25 18:56:22 -04:00
66 changed files with 1446 additions and 564 deletions
+1 -3
View File
@@ -76,9 +76,7 @@ export class ColorAllocator {
case ColoredTeams.Bot:
return botColor;
default:
return this.availableColors[
simpleHash(team) % this.availableColors.length
];
return this.assignColor(team);
}
}
}
+11 -4
View File
@@ -1,6 +1,5 @@
import { Colord } from "colord";
import { JWK } from "jose";
import { GameConfig, GameID, TeamCountConfig } from "../Schemas";
import {
Difficulty,
Game,
@@ -18,6 +17,8 @@ import {
import { GameMap, TileRef } from "../game/GameMap";
import { PlayerView } from "../game/GameView";
import { UserSettings } from "../game/UserSettings";
import { GameConfig, GameID, TeamCountConfig } from "../Schemas";
import { NukeType } from "../StatsSchemas";
export enum GameEnv {
Dev,
@@ -48,8 +49,7 @@ export interface ServerConfig {
r2AccessKey(): string;
r2SecretKey(): string;
otelEndpoint(): string;
otelUsername(): string;
otelPassword(): string;
otelAuthHeader(): string;
otelEnabled(): boolean;
jwtAudience(): string;
jwtIssuer(): string;
@@ -153,10 +153,17 @@ export interface Config {
traitorDefenseDebuff(): number;
traitorDuration(): number;
nukeMagnitudes(unitType: UnitType): NukeMagnitude;
// Number of tiles destroyed to break an alliance
nukeAllianceBreakThreshold(): number;
defaultNukeSpeed(): number;
defaultNukeTargetableRange(): number;
defaultSamRange(): number;
nukeDeathFactor(humans: number, tilesOwned: number): number;
nukeDeathFactor(
nukeType: NukeType,
humans: number,
tilesOwned: number,
maxPop: number,
): number;
structureMinDist(): number;
isReplay(): boolean;
allianceExtensionPromptOffset(): number;
+68 -87
View File
@@ -23,6 +23,7 @@ import { TileRef } from "../game/GameMap";
import { PlayerView } from "../game/GameView";
import { UserSettings } from "../game/UserSettings";
import { GameConfig, GameID, TeamCountConfig } from "../Schemas";
import { NukeType } from "../StatsSchemas";
import { assertNever, simpleHash, within } from "../Util";
import { Config, GameEnv, NukeMagnitude, ServerConfig, Theme } from "./Config";
import { PastelTheme } from "./PastelTheme";
@@ -119,19 +120,16 @@ export abstract class DefaultServerConfig implements ServerConfig {
}
otelEnabled(): boolean {
return (
this.env() !== GameEnv.Dev &&
Boolean(this.otelEndpoint()) &&
Boolean(this.otelUsername()) &&
Boolean(this.otelPassword())
Boolean(this.otelAuthHeader())
);
}
otelEndpoint(): string {
return process.env.OTEL_ENDPOINT ?? "";
return process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "";
}
otelUsername(): string {
return process.env.OTEL_USERNAME ?? "";
}
otelPassword(): string {
return process.env.OTEL_PASSWORD ?? "";
otelAuthHeader(): string {
return process.env.OTEL_AUTH_HEADER ?? "";
}
gitCommit(): string {
return process.env.GIT_COMMIT ?? "";
@@ -236,7 +234,7 @@ export class DefaultConfig implements Config {
return 0.5;
}
traitorSpeedDebuff(): number {
return 0.6;
return 0.8;
}
traitorDuration(): number {
return 30 * 10; // 30 seconds
@@ -342,9 +340,9 @@ export class DefaultConfig implements Config {
}
tradeShipGold(dist: number, numPorts: number): Gold {
const baseGold = Math.floor(50000 + 130 * dist);
const basePortBonus = 0.2;
const diminishingFactor = 0.95;
const baseGold = Math.floor(50000 + 100 * dist);
const basePortBonus = 0.25;
const diminishingFactor = 0.9;
let totalMultiplier = 1;
for (let i = 0; i < numPorts; i++) {
@@ -361,7 +359,7 @@ export class DefaultConfig implements Config {
}
if (numTradeShips <= 150) {
const additional = numTradeShips - 20;
return Math.pow(additional, 0.8) + 5;
return Math.floor(Math.pow(additional, 0.85) + 5);
}
return 1_000_000;
}
@@ -375,15 +373,9 @@ export class DefaultConfig implements Config {
};
case UnitType.Warship:
return {
cost: (p: Player) =>
p.type() === PlayerType.Human && this.infiniteGold()
? 0n
: BigInt(
Math.min(
1_000_000,
(p.unitsOwned(UnitType.Warship) + 1) * 250_000,
),
),
cost: this.costWrapper(UnitType.Warship, (numUnits: number) =>
Math.min(1_000_000, (numUnits + 1) * 250_000),
),
territoryBound: false,
maxHealth: 1000,
};
@@ -400,15 +392,9 @@ export class DefaultConfig implements Config {
};
case UnitType.Port:
return {
cost: (p: Player) =>
p.type() === PlayerType.Human && this.infiniteGold()
? 0n
: BigInt(
Math.min(
1_000_000,
Math.pow(2, p.unitsConstructed(UnitType.Port)) * 125_000,
),
),
cost: this.costWrapper(UnitType.Port, (numUnits: number) =>
Math.min(1_000_000, Math.pow(2, numUnits) * 125_000),
),
territoryBound: true,
constructionDuration: this.instantBuild() ? 0 : 2 * 10,
upgradable: true,
@@ -416,26 +402,17 @@ export class DefaultConfig implements Config {
};
case UnitType.AtomBomb:
return {
cost: (p: Player) =>
p.type() === PlayerType.Human && this.infiniteGold()
? 0n
: 750_000n,
cost: this.costWrapper(UnitType.AtomBomb, () => 750_000),
territoryBound: false,
};
case UnitType.HydrogenBomb:
return {
cost: (p: Player) =>
p.type() === PlayerType.Human && this.infiniteGold()
? 0n
: 5_000_000n,
cost: this.costWrapper(UnitType.HydrogenBomb, () => 5_000_000),
territoryBound: false,
};
case UnitType.MIRV:
return {
cost: (p: Player) =>
p.type() === PlayerType.Human && this.infiniteGold()
? 0n
: 25_000_000n,
cost: this.costWrapper(UnitType.MIRV, () => 35_000_000),
territoryBound: false,
};
case UnitType.MIRVWarhead:
@@ -450,54 +427,33 @@ export class DefaultConfig implements Config {
};
case UnitType.MissileSilo:
return {
cost: (p: Player) =>
p.type() === PlayerType.Human && this.infiniteGold()
? 0n
: 1_000_000n,
cost: this.costWrapper(UnitType.MissileSilo, () => 1_000_000),
territoryBound: true,
constructionDuration: this.instantBuild() ? 0 : 10 * 10,
upgradable: true,
};
case UnitType.DefensePost:
return {
cost: (p: Player) =>
p.type() === PlayerType.Human && this.infiniteGold()
? 0n
: BigInt(
Math.min(
250_000,
(p.unitsConstructed(UnitType.DefensePost) + 1) * 50_000,
),
),
cost: this.costWrapper(UnitType.DefensePost, (numUnits: number) =>
Math.min(250_000, (numUnits + 1) * 50_000),
),
territoryBound: true,
constructionDuration: this.instantBuild() ? 0 : 5 * 10,
};
case UnitType.SAMLauncher:
return {
cost: (p: Player) =>
p.type() === PlayerType.Human && this.infiniteGold()
? 0n
: BigInt(
Math.min(
3_000_000,
(p.unitsConstructed(UnitType.SAMLauncher) + 1) * 1_500_000,
),
),
cost: this.costWrapper(UnitType.SAMLauncher, (numUnits: number) =>
Math.min(3_000_000, (numUnits + 1) * 1_500_000),
),
territoryBound: true,
constructionDuration: this.instantBuild() ? 0 : 30 * 10,
upgradable: true,
};
case UnitType.City:
return {
cost: (p: Player) =>
p.type() === PlayerType.Human && this.infiniteGold()
? 0n
: BigInt(
Math.min(
1_000_000,
Math.pow(2, p.unitsConstructed(UnitType.City)) * 125_000,
),
),
cost: this.costWrapper(UnitType.City, (numUnits: number) =>
Math.min(1_000_000, Math.pow(2, numUnits) * 125_000),
),
territoryBound: true,
constructionDuration: this.instantBuild() ? 0 : 2 * 10,
upgradable: true,
@@ -505,15 +461,9 @@ export class DefaultConfig implements Config {
};
case UnitType.Factory:
return {
cost: (p: Player) =>
p.type() === PlayerType.Human && this.infiniteGold()
? 0n
: BigInt(
Math.min(
1_000_000,
Math.pow(2, p.unitsConstructed(UnitType.Factory)) * 125_000,
),
),
cost: this.costWrapper(UnitType.Factory, (numUnits: number) =>
Math.min(1_000_000, Math.pow(2, numUnits) * 125_000),
),
territoryBound: true,
constructionDuration: this.instantBuild() ? 0 : 2 * 10,
canBuildTrainStation: true,
@@ -535,6 +485,20 @@ export class DefaultConfig implements Config {
assertNever(type);
}
}
private costWrapper(
type: UnitType,
costFn: (units: number) => number,
): (p: Player) => bigint {
return (p: Player) => {
if (p.type() === PlayerType.Human && this.infiniteGold()) {
return 0n;
}
const numUnits = Math.min(p.unitsOwned(type), p.unitsConstructed(type));
return BigInt(costFn(numUnits));
};
}
defaultDonationAmount(sender: Player): number {
return Math.floor(sender.troops() / 3);
}
@@ -557,7 +521,7 @@ export class DefaultConfig implements Config {
return 30 * 10;
}
allianceDuration(): Tick {
return 600 * 10; // 10 minutes.
return 300 * 10; // 5 minutes.
}
temporaryEmbargoDuration(): Tick {
return 300 * 10; // 5 minutes.
@@ -788,7 +752,7 @@ export class DefaultConfig implements Config {
toAdd *= ratio;
if (player.type() === PlayerType.Bot) {
toAdd *= 0.7;
toAdd *= 0.6;
}
if (player.type() === PlayerType.FakeHuman) {
@@ -842,6 +806,10 @@ export class DefaultConfig implements Config {
throw new Error(`Unknown nuke type: ${unitType}`);
}
nukeAllianceBreakThreshold(): number {
return 100;
}
defaultNukeSpeed(): number {
return 6;
}
@@ -855,8 +823,21 @@ export class DefaultConfig implements Config {
}
// Humans can be population, soldiers attacking, soldiers in boat etc.
nukeDeathFactor(humans: number, tilesOwned: number): number {
return (5 * humans) / Math.max(1, tilesOwned);
nukeDeathFactor(
nukeType: NukeType,
humans: number,
tilesOwned: number,
maxPop: number,
): number {
if (nukeType !== UnitType.MIRVWarhead) {
return (5 * humans) / Math.max(1, tilesOwned);
}
const targetPop = 0.05 * maxPop;
const excessPop = Math.max(0, humans - targetPop);
const scalingFactor = 20000;
return (scalingFactor * excessPop * excessPop) / (maxPop * maxPop);
}
structureMinDist(): number {