Merge branch 'main' into team-names

This commit is contained in:
Evan
2026-03-05 20:50:47 -08:00
committed by GitHub
127 changed files with 5869 additions and 1658 deletions
+3 -3
View File
@@ -9,7 +9,7 @@ import {
GameMode,
GameType,
} from "../src/core/game/Game";
import { AnalyticsRecord } from "../src/core/Schemas";
import { AnalyticsRecord, GameConfig } from "../src/core/Schemas";
import {
GOLD_INDEX_STEAL,
GOLD_INDEX_TRADE,
@@ -19,7 +19,7 @@ import {
} from "../src/core/StatsSchemas";
describe("Ranking class", () => {
const mockConfig = {
const mockConfig: GameConfig = {
gameMap: GameMapType.Montreal,
difficulty: Difficulty.Medium,
donateGold: false,
@@ -27,7 +27,7 @@ describe("Ranking class", () => {
gameType: GameType.Public,
gameMode: GameMode.FFA,
gameMapSize: GameMapSize.Normal,
disableNations: true,
nations: "disabled",
bots: 0,
infiniteGold: false,
infiniteTroops: false,
+5 -4
View File
@@ -63,6 +63,7 @@ describe("AllianceBehavior.handleAllianceRequests", () => {
numTilesPlayer = 10,
numTilesRequestor = 10,
alliancesCount = 0,
createdAtTick = game.ticks() + 1,
} = {}) {
if (isTraitor) requestor.markTraitor();
@@ -86,7 +87,7 @@ describe("AllianceBehavior.handleAllianceRequests", () => {
const mockRequest = {
requestor: () => requestor,
recipient: () => player,
createdAt: () => 0 as unknown as Tick,
createdAt: () => createdAtTick as unknown as Tick,
accept: vi.fn(),
reject: vi.fn(),
} as unknown as AllianceRequest;
@@ -96,9 +97,9 @@ describe("AllianceBehavior.handleAllianceRequests", () => {
return mockRequest;
}
test("should reject alliance during spawn phase", () => {
vi.spyOn(game, "inSpawnPhase").mockReturnValue(true);
const request = setupAllianceRequest({});
test("should reject alliance created on first post-spawn tick", () => {
const cutoff = game.config().numSpawnPhaseTurns() + 1;
const request = setupAllianceRequest({ createdAtTick: cutoff });
allianceBehavior.handleAllianceRequests();
+4 -1
View File
@@ -29,9 +29,12 @@ describe("PlayerImpl", () => {
}
player = game.player("player_id");
player.addGold(BigInt(1000000));
other = game.player("other_id");
player.conquer(game.ref(0, 0));
other.conquer(game.ref(50, 50));
player.addGold(BigInt(1000000));
game.config().structureMinDist = () => 10;
});
+1 -1
View File
@@ -234,7 +234,7 @@ function extractDataI18nKeys(content: string): Set<string> {
function extractTranslationKeyLikeAttrs(content: string): Set<string> {
const keys = new Set<string>();
const keyLikeAttrRegex =
/\b(?:translationKey|labelKey|disabledKey|titleKey|ariaLabelKey|placeholderKey)\s*=\s*["']([^"']+)["']/g;
/\b(?:translationKey|labelKey|defaultLabelKey|disabledKey|titleKey|ariaLabelKey|placeholderKey)\s*=\s*["']([^"']+)["']/g;
let match: RegExpExecArray | null;
while ((match = keyLikeAttrRegex.exec(content)) !== null) {
keys.add(match[1]);
+134
View File
@@ -0,0 +1,134 @@
import { afterEach, describe, expect, it, vi } from "vitest";
type NavigatorOverride = {
userAgent: string;
userAgentData?: { platform?: string };
maxTouchPoints?: number;
};
const setInnerWidth = (value: number) => {
Object.defineProperty(window, "innerWidth", {
configurable: true,
writable: true,
value,
});
};
const loadPlatform = async ({
userAgent,
userAgentData,
maxTouchPoints,
}: NavigatorOverride) => {
vi.resetModules();
vi.stubGlobal("navigator", {
userAgent,
userAgentData,
maxTouchPoints,
});
const { Platform } = await import("../../src/client/Platform");
return Platform;
};
afterEach(() => {
vi.unstubAllGlobals();
vi.clearAllMocks();
});
describe("Platform", () => {
it("detects iOS before macOS for iPhone-like user agents", async () => {
const platform = await loadPlatform({
userAgent:
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1",
});
expect(platform.os).toBe("iOS");
expect(platform.isIOS).toBe(true);
expect(platform.isMac).toBe(false);
});
it("detects macOS for Macintosh user agents", async () => {
const platform = await loadPlatform({
userAgent:
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15",
});
expect(platform.os).toBe("macOS");
expect(platform.isMac).toBe(true);
expect(platform.isIOS).toBe(false);
});
it("detects iOS for iPad desktop-mode user agents with touch support", async () => {
const platform = await loadPlatform({
userAgent:
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15",
maxTouchPoints: 5,
});
expect(platform.os).toBe("iOS");
expect(platform.isIOS).toBe(true);
expect(platform.isMac).toBe(false);
});
it("uses userAgentData platform when available", async () => {
const platform = await loadPlatform({
userAgent:
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15",
userAgentData: { platform: "Android" },
});
expect(platform.os).toBe("Android");
expect(platform.isAndroid).toBe(true);
});
it("normalizes non-canonical userAgentData platform values", async () => {
const macPlatform = await loadPlatform({
userAgent: "Mozilla/5.0",
userAgentData: { platform: "Macintosh" },
});
expect(macPlatform.os).toBe("macOS");
expect(macPlatform.isMac).toBe(true);
const chromeOsPlatform = await loadPlatform({
userAgent: "Mozilla/5.0",
userAgentData: { platform: "Chrome OS" },
});
expect(chromeOsPlatform.os).toBe("Linux");
expect(chromeOsPlatform.isLinux).toBe(true);
const unknownPlatform = await loadPlatform({
userAgent: "Mozilla/5.0",
userAgentData: { platform: "PlayStation" },
});
expect(unknownPlatform.os).toBe("Unknown");
expect(unknownPlatform.isMac).toBe(false);
expect(unknownPlatform.isWindows).toBe(false);
expect(unknownPlatform.isIOS).toBe(false);
expect(unknownPlatform.isAndroid).toBe(false);
expect(unknownPlatform.isLinux).toBe(false);
});
it("reports viewport breakpoint helpers from window.innerWidth", async () => {
const platform = await loadPlatform({
userAgent:
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15",
});
setInnerWidth(767);
expect(platform.isMobileWidth).toBe(true);
expect(platform.isTabletWidth).toBe(false);
expect(platform.isDesktopWidth).toBe(false);
setInnerWidth(768);
expect(platform.isMobileWidth).toBe(false);
expect(platform.isTabletWidth).toBe(true);
expect(platform.isDesktopWidth).toBe(false);
setInnerWidth(1024);
expect(platform.isMobileWidth).toBe(false);
expect(platform.isTabletWidth).toBe(false);
expect(platform.isDesktopWidth).toBe(true);
});
});
+3 -2
View File
@@ -11,6 +11,7 @@ import {
import { createGame as createGameImpl } from "../../../src/core/game/GameImpl";
import { GameMapImpl } from "../../../src/core/game/GameMap";
import { UserSettings } from "../../../src/core/game/UserSettings";
import { GameConfig } from "../../../src/core/Schemas";
import { TestConfig } from "../../util/TestConfig";
import { TestServerConfig } from "../../util/TestServerConfig";
@@ -131,13 +132,13 @@ export function createGame(data: TestMapData): Game {
);
const serverConfig = new TestServerConfig();
const gameConfig = {
const gameConfig: GameConfig = {
gameMap: GameMapType.Asia,
gameMapSize: GameMapSize.Normal,
gameMode: GameMode.FFA,
gameType: GameType.Singleplayer,
difficulty: Difficulty.Medium,
disableNations: false,
nations: "default",
donateGold: false,
donateTroops: false,
bots: 0,
+1 -1
View File
@@ -263,7 +263,7 @@ export async function setupFromPath(
gameMode: GameMode.FFA,
gameType: GameType.Singleplayer,
difficulty: Difficulty.Medium,
disableNations: false,
nations: "default",
donateGold: false,
donateTroops: false,
bots: 0,
+109
View File
@@ -0,0 +1,109 @@
import Benchmark from "benchmark";
const STRUCTURE_COUNT = 50000;
const LOOKUP_COUNT = 50000;
const UPGRADE_LOOKUP_COUNT = 5000;
interface StructureRenderSample {
unitId: number;
ownerId: number;
level: number;
}
const rendersArray: StructureRenderSample[] = Array.from(
{ length: STRUCTURE_COUNT },
(_, index) => ({
unitId: index + 1,
ownerId: (index % 5) + 1,
level: (index % 4) + 1,
}),
);
const rendersMap = new Map<number, StructureRenderSample>();
for (const render of rendersArray) {
rendersMap.set(render.unitId, render);
}
const activeLookupIds = Array.from(
{ length: LOOKUP_COUNT },
(_, index) => ((index * 97) % STRUCTURE_COUNT) + 1,
);
const inactiveLookupIds = Array.from(
{ length: LOOKUP_COUNT },
(_, index) => ((index * 193) % STRUCTURE_COUNT) + 1,
);
const canUpgradeIds = Array.from(
{ length: UPGRADE_LOOKUP_COUNT },
(_, index) => ((index * 389) % STRUCTURE_COUNT) + 1,
);
const myOwnerId = 3;
const results: string[] = [];
new Benchmark.Suite()
.add("StructureIconsLayer BEFORE (array O(n) lookup/delete)", () => {
const localRenders = rendersArray.map((render) => ({ ...render }));
for (const unitId of activeLookupIds) {
const render = localRenders.find((entry) => entry.unitId === unitId);
if (render) {
render.level = render.level + 1;
}
}
for (const canUpgradeId of canUpgradeIds) {
const potentialUpgrade = localRenders.find(
(entry) => entry.unitId === canUpgradeId && entry.ownerId === myOwnerId,
);
if (potentialUpgrade) {
potentialUpgrade.level = potentialUpgrade.level + 1;
}
}
for (const unitId of inactiveLookupIds) {
const index = localRenders.findIndex((entry) => entry.unitId === unitId);
if (index !== -1) {
localRenders.splice(index, 1);
}
}
})
.add("StructureIconsLayer AFTER (unit-id map O(1) lookup/delete)", () => {
const localRenders = new Map<number, StructureRenderSample>();
for (const [unitId, render] of rendersMap) {
localRenders.set(unitId, { ...render });
}
for (const unitId of activeLookupIds) {
const render = localRenders.get(unitId);
if (render) {
render.level = render.level + 1;
}
}
for (const canUpgradeId of canUpgradeIds) {
const potentialUpgrade = localRenders.get(canUpgradeId);
if (potentialUpgrade && potentialUpgrade.ownerId === myOwnerId) {
potentialUpgrade.level = potentialUpgrade.level + 1;
}
}
for (const unitId of inactiveLookupIds) {
localRenders.delete(unitId);
}
})
.on("cycle", (event: Benchmark.Event) => {
results.push(String(event.target));
})
.on("complete", function () {
console.log("\n=== StructureIconsLayer Lookup Benchmark Results ===");
for (const result of results) {
console.log(result);
}
const fastest = this.filter("fastest").map("name");
console.log(`\nFastest implementation: ${fastest.join(", ")}`);
})
.run({ async: true });
+1 -1
View File
@@ -61,7 +61,7 @@ export async function setup(
gameMode: GameMode.FFA,
gameType: GameType.Singleplayer,
difficulty: Difficulty.Medium,
disableNations: false,
nations: "default",
donateGold: false,
donateTroops: false,
bots: 0,
+6 -1
View File
@@ -80,7 +80,12 @@ export class TestServerConfig implements ServerConfig {
throw new Error("Method not implemented.");
}
getRandomPublicGameModifiers(): PublicGameModifiers {
return { isCompact: false, isRandomSpawn: false, isCrowded: false };
return {
isCompact: false,
isRandomSpawn: false,
isCrowded: false,
isHardNations: false,
};
}
async supportsCompactMapForTeams(): Promise<boolean> {
throw new Error("Method not implemented.");