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
+1 -1
View File
@@ -15,7 +15,7 @@ let game: Game;
let attacker: Player;
function attackerBuildsNuke(
source: TileRef,
source: TileRef | null,
target: TileRef,
initialize = true,
) {
+20 -10
View File
@@ -52,9 +52,9 @@ describe("SAM", () => {
});
test("one sam should take down one nuke", async () => {
const sam = defender.buildUnit(UnitType.SAMLauncher, 0, game.ref(1, 1));
const sam = defender.buildUnit(UnitType.SAMLauncher, game.ref(1, 1), {});
game.addExecution(new SAMLauncherExecution(defender.id(), null, sam));
attacker.buildUnit(UnitType.AtomBomb, 0, game.ref(1, 1));
attacker.buildUnit(UnitType.AtomBomb, game.ref(1, 1), {});
executeTicks(game, 3);
@@ -62,10 +62,14 @@ describe("SAM", () => {
});
test("sam should only get one nuke at a time", async () => {
const sam = defender.buildUnit(UnitType.SAMLauncher, 0, game.ref(1, 1));
const sam = defender.buildUnit(UnitType.SAMLauncher, game.ref(1, 1), {});
game.addExecution(new SAMLauncherExecution(defender.id(), null, sam));
attacker.buildUnit(UnitType.AtomBomb, 0, game.ref(2, 1));
attacker.buildUnit(UnitType.AtomBomb, 0, game.ref(1, 2));
attacker.buildUnit(UnitType.AtomBomb, game.ref(2, 1), {
detonationDst: game.ref(2, 1),
});
attacker.buildUnit(UnitType.AtomBomb, game.ref(1, 2), {
detonationDst: game.ref(1, 2),
});
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(2);
executeTicks(game, 3);
@@ -74,10 +78,12 @@ describe("SAM", () => {
});
test("sam should cooldown as long as configured", async () => {
const sam = defender.buildUnit(UnitType.SAMLauncher, 0, game.ref(1, 1));
const sam = defender.buildUnit(UnitType.SAMLauncher, game.ref(1, 1), {});
game.addExecution(new SAMLauncherExecution(defender.id(), null, sam));
expect(sam.isCooldown()).toBeFalsy();
const nuke = attacker.buildUnit(UnitType.AtomBomb, 0, game.ref(1, 2));
const nuke = attacker.buildUnit(UnitType.AtomBomb, game.ref(1, 2), {
detonationDst: game.ref(1, 2),
});
executeTicks(game, 3);
@@ -93,11 +99,15 @@ describe("SAM", () => {
});
test("two sams should not target twice same nuke", async () => {
const sam1 = defender.buildUnit(UnitType.SAMLauncher, 0, game.ref(1, 1));
const sam1 = defender.buildUnit(UnitType.SAMLauncher, game.ref(1, 1), {
cooldownDuration: 10,
});
game.addExecution(new SAMLauncherExecution(defender.id(), null, sam1));
const sam2 = defender.buildUnit(UnitType.SAMLauncher, 0, game.ref(1, 2));
const sam2 = defender.buildUnit(UnitType.SAMLauncher, game.ref(1, 2), {});
game.addExecution(new SAMLauncherExecution(defender.id(), null, sam2));
const nuke = attacker.buildUnit(UnitType.AtomBomb, 0, game.ref(2, 2));
const nuke = attacker.buildUnit(UnitType.AtomBomb, game.ref(2, 2), {
detonationDst: game.ref(2, 2),
});
executeTicks(game, 3);
+42
View File
@@ -0,0 +1,42 @@
import {
ColoredTeams,
Game,
GameMode,
PlayerType,
} from "../src/core/game/Game";
import { playerInfo, setup } from "./util/Setup";
let game: Game;
describe("Teams", () => {
test("bots are on the same team, but can attack each other", async () => {
game = await setup("Plains", { gameMode: GameMode.Team, playerTeams: 2 });
const bot1 = game.addPlayer(playerInfo("bot1", PlayerType.Bot));
const bot2 = game.addPlayer(playerInfo("bot2", PlayerType.Bot));
// Both bots should be on the same team
expect(bot1.team()).toBe(ColoredTeams.Bot);
expect(bot2.team()).toBe(ColoredTeams.Bot);
// But they should be allowed to attack each other.
expect(bot1.isOnSameTeam(bot2)).toBe(false);
});
test("humans spawn on different teams", async () => {
game = await setup(
"Plains",
{
gameMode: GameMode.Team,
playerTeams: 2,
},
[
playerInfo("human1", PlayerType.Human),
playerInfo("human2", PlayerType.Human),
],
);
expect(game.player("human1").isOnSameTeam(game.player("human2"))).toBe(
false,
);
});
});
+46 -46
View File
@@ -1,7 +1,7 @@
import { PlayerInfo, PlayerType, Team } from "../src/core/game/Game";
import { ColoredTeams, PlayerInfo, PlayerType } from "../src/core/game/Game";
import { assignTeams } from "../src/core/game/TeamAssignment";
const teams = [Team.Red, Team.Blue];
const teams = [ColoredTeams.Red, ColoredTeams.Blue];
describe("assignTeams", () => {
const createPlayer = (id: string, clan?: string): PlayerInfo => {
@@ -27,10 +27,10 @@ describe("assignTeams", () => {
const result = assignTeams(players, teams);
// Check that players are assigned alternately
expect(result.get(players[0])).toEqual(Team.Red);
expect(result.get(players[1])).toEqual(Team.Blue);
expect(result.get(players[2])).toEqual(Team.Red);
expect(result.get(players[3])).toEqual(Team.Blue);
expect(result.get(players[0])).toEqual(ColoredTeams.Red);
expect(result.get(players[1])).toEqual(ColoredTeams.Blue);
expect(result.get(players[2])).toEqual(ColoredTeams.Red);
expect(result.get(players[3])).toEqual(ColoredTeams.Blue);
});
it("should keep clan members together on the same team", () => {
@@ -44,10 +44,10 @@ describe("assignTeams", () => {
const result = assignTeams(players, teams);
// Check that clan members are on the same team
expect(result.get(players[0])).toEqual(Team.Red);
expect(result.get(players[1])).toEqual(Team.Red);
expect(result.get(players[2])).toEqual(Team.Blue);
expect(result.get(players[3])).toEqual(Team.Blue);
expect(result.get(players[0])).toEqual(ColoredTeams.Red);
expect(result.get(players[1])).toEqual(ColoredTeams.Red);
expect(result.get(players[2])).toEqual(ColoredTeams.Blue);
expect(result.get(players[3])).toEqual(ColoredTeams.Blue);
});
it("should handle mixed clan and non-clan players", () => {
@@ -61,10 +61,10 @@ describe("assignTeams", () => {
const result = assignTeams(players, teams);
// Check that clan members are together and non-clan players balance teams
expect(result.get(players[0])).toEqual(Team.Red);
expect(result.get(players[1])).toEqual(Team.Red);
expect(result.get(players[2])).toEqual(Team.Blue);
expect(result.get(players[3])).toEqual(Team.Blue);
expect(result.get(players[0])).toEqual(ColoredTeams.Red);
expect(result.get(players[1])).toEqual(ColoredTeams.Red);
expect(result.get(players[2])).toEqual(ColoredTeams.Blue);
expect(result.get(players[3])).toEqual(ColoredTeams.Blue);
});
it("should kick players when teams are full", () => {
@@ -80,14 +80,14 @@ describe("assignTeams", () => {
const result = assignTeams(players, teams);
// Check that players are kicked when teams are full
expect(result.get(players[0])).toEqual(Team.Red);
expect(result.get(players[1])).toEqual(Team.Red);
expect(result.get(players[2])).toEqual(Team.Red);
expect(result.get(players[0])).toEqual(ColoredTeams.Red);
expect(result.get(players[1])).toEqual(ColoredTeams.Red);
expect(result.get(players[2])).toEqual(ColoredTeams.Red);
expect(result.get(players[3])).toEqual("kicked");
expect(result.get(players[4])).toEqual(Team.Blue);
expect(result.get(players[5])).toEqual(Team.Blue);
expect(result.get(players[4])).toEqual(ColoredTeams.Blue);
expect(result.get(players[5])).toEqual(ColoredTeams.Blue);
});
it("should handle empty player list", () => {
@@ -98,7 +98,7 @@ describe("assignTeams", () => {
it("should handle single player", () => {
const players = [createPlayer("1")];
const result = assignTeams(players, teams);
expect(result.get(players[0])).toEqual(Team.Red);
expect(result.get(players[0])).toEqual(ColoredTeams.Red);
});
it("should handle multiple clans with different sizes", () => {
@@ -114,12 +114,12 @@ describe("assignTeams", () => {
const result = assignTeams(players, teams);
// Check that larger clans are assigned first
expect(result.get(players[0])).toEqual(Team.Red);
expect(result.get(players[1])).toEqual(Team.Red);
expect(result.get(players[2])).toEqual(Team.Red);
expect(result.get(players[3])).toEqual(Team.Blue);
expect(result.get(players[4])).toEqual(Team.Blue);
expect(result.get(players[5])).toEqual(Team.Blue);
expect(result.get(players[0])).toEqual(ColoredTeams.Red);
expect(result.get(players[1])).toEqual(ColoredTeams.Red);
expect(result.get(players[2])).toEqual(ColoredTeams.Red);
expect(result.get(players[3])).toEqual(ColoredTeams.Blue);
expect(result.get(players[4])).toEqual(ColoredTeams.Blue);
expect(result.get(players[5])).toEqual(ColoredTeams.Blue);
});
it("should distribute players among a larger number of teams", () => {
@@ -141,28 +141,28 @@ describe("assignTeams", () => {
];
const result = assignTeams(players, [
Team.Red,
Team.Blue,
Team.Teal,
Team.Purple,
Team.Yellow,
Team.Orange,
Team.Green,
ColoredTeams.Red,
ColoredTeams.Blue,
ColoredTeams.Yellow,
ColoredTeams.Green,
ColoredTeams.Purple,
ColoredTeams.Orange,
ColoredTeams.Teal,
]);
expect(result.get(players[0])).toEqual(Team.Red);
expect(result.get(players[1])).toEqual(Team.Red);
expect(result.get(players[0])).toEqual(ColoredTeams.Red);
expect(result.get(players[1])).toEqual(ColoredTeams.Red);
expect(result.get(players[2])).toEqual("kicked");
expect(result.get(players[3])).toEqual(Team.Blue);
expect(result.get(players[4])).toEqual(Team.Blue);
expect(result.get(players[5])).toEqual(Team.Teal);
expect(result.get(players[6])).toEqual(Team.Purple);
expect(result.get(players[7])).toEqual(Team.Yellow);
expect(result.get(players[8])).toEqual(Team.Orange);
expect(result.get(players[9])).toEqual(Team.Green);
expect(result.get(players[10])).toEqual(Team.Teal);
expect(result.get(players[11])).toEqual(Team.Purple);
expect(result.get(players[12])).toEqual(Team.Yellow);
expect(result.get(players[13])).toEqual(Team.Orange);
expect(result.get(players[3])).toEqual(ColoredTeams.Blue);
expect(result.get(players[4])).toEqual(ColoredTeams.Blue);
expect(result.get(players[5])).toEqual(ColoredTeams.Yellow);
expect(result.get(players[6])).toEqual(ColoredTeams.Green);
expect(result.get(players[7])).toEqual(ColoredTeams.Purple);
expect(result.get(players[8])).toEqual(ColoredTeams.Orange);
expect(result.get(players[9])).toEqual(ColoredTeams.Teal);
expect(result.get(players[10])).toEqual(ColoredTeams.Yellow);
expect(result.get(players[11])).toEqual(ColoredTeams.Green);
expect(result.get(players[12])).toEqual(ColoredTeams.Purple);
expect(result.get(players[13])).toEqual(ColoredTeams.Orange);
});
});
+139
View File
@@ -0,0 +1,139 @@
import { PlayerInfo, PlayerType, UnitType } from "../src/core/game/Game";
import { UnitGrid } from "../src/core/game/UnitGrid";
import { setup } from "./util/Setup";
async function checkRange(
mapName: string,
unitPosX: number,
rangeCheck: number,
range: number,
) {
const game = await setup(mapName, { infiniteGold: true, instantBuild: true });
const grid = new UnitGrid(game.map());
const player = game.addPlayer(
new PlayerInfo("us", "test_player", PlayerType.Human, null, "test_id"),
);
const unitTile = game.map().ref(unitPosX, 0);
grid.addUnit(player.buildUnit(UnitType.DefensePost, unitTile, {}));
const tileToCheck = game.map().ref(rangeCheck, 0);
return grid.hasUnitNearby(
tileToCheck,
range,
UnitType.DefensePost,
"test_id",
);
}
async function nearbyUnits(
mapName: string,
unitPosX: number,
rangeCheck: number,
range: number,
unitTypes: UnitType[],
) {
const game = await setup(mapName, { infiniteGold: true, instantBuild: true });
const grid = new UnitGrid(game.map());
const player = game.addPlayer(
new PlayerInfo("us", "test_player", PlayerType.Human, null, "test_id"),
);
const unitTile = game.map().ref(unitPosX, 0);
for (const unitType of unitTypes) {
grid.addUnit(player.buildUnit(unitType, unitTile, {}));
}
const tileToCheck = game.map().ref(rangeCheck, 0);
return grid.nearbyUnits(tileToCheck, range, unitTypes);
}
describe("Unit Grid range tests", () => {
const hasUnitCases = [
["Plains", 0, 10, 0, true], // Same spot
["Plains", 0, 10, 10, true], // Exactly on the range
["Plains", 0, 10, 11, false], // Exactly 1px outside
["BigPlains", 0, 198, 42, true], // Inside huge range
["BigPlains", 0, 198, 199, false], // Exactly 1px outside huge range
];
describe("Is unit in range", () => {
test.each(hasUnitCases)(
"on %p map, look if unit at position %p with a range of %p is in range of %p position, returns %p",
async (
mapName: string,
unitPosX: number,
range: number,
rangeCheck: number,
expectedResult: boolean,
) => {
const result = await checkRange(mapName, unitPosX, rangeCheck, range);
expect(result).toBe(expectedResult);
},
);
});
const unitsInRangeCases = [
["Plains", 0, 10, 0, [UnitType.Warship], 1], // Same spot
["Plains", 0, 10, 0, [UnitType.City, UnitType.Port], 2], // 2 in range
["Plains", 0, 10, 0, [], 0], // no unit
["Plains", 0, 10, 10, [UnitType.City], 1], // Exactly on the range
["Plains", 0, 10, 11, [UnitType.DefensePost], 0], // 1px outside
["BigPlains", 0, 198, 42, [UnitType.TradeShip], 1], // Inside huge range
["BigPlains", 0, 198, 199, [UnitType.TransportShip], 0], // 1px outside
];
describe("Retrieve all units in range", () => {
test.each(unitsInRangeCases)(
"on %p map, look if unit at position %p with a range of %p is in range of %p position, returns %p",
async (
mapName: string,
unitPosX: number,
range: number,
rangeCheck: number,
units: UnitType[],
expectedResult: number,
) => {
const result = await nearbyUnits(
mapName,
unitPosX,
rangeCheck,
range,
units,
);
expect(result.length).toBe(expectedResult);
},
);
test("Wrong unit type in range", async () => {
const game = await setup("Plains", {
infiniteGold: true,
instantBuild: true,
});
const grid = new UnitGrid(game.map());
const player = game.addPlayer(
new PlayerInfo("us", "test_player", PlayerType.Human, null, "test_id"),
);
const unitTile = game.map().ref(0, 0);
grid.addUnit(player.buildUnit(UnitType.City, unitTile, {}));
const tileToCheck = game.map().ref(0, 0);
expect(grid.nearbyUnits(tileToCheck, 10, [UnitType.Port])).toHaveLength(
0,
);
});
test("One inside, one outside of range", async () => {
const game = await setup("Plains", {
infiniteGold: true,
instantBuild: true,
});
const grid = new UnitGrid(game.map());
const player = game.addPlayer(
new PlayerInfo("us", "test_player", PlayerType.Human, null, "test_id"),
);
const unitType = UnitType.City;
const unitTile = game.map().ref(0, 0);
grid.addUnit(player.buildUnit(unitType, unitTile, {}));
const outsideTile = game.map().ref(99, 0);
grid.addUnit(player.buildUnit(unitType, outsideTile, {}));
const tileToCheck = game.map().ref(0, 0);
expect(grid.nearbyUnits(tileToCheck, 10, [unitType])).toHaveLength(1);
});
});
});
+21 -4
View File
@@ -60,12 +60,16 @@ describe("Warship", () => {
test("Warship heals only if player has port", async () => {
const maxHealth = game.config().unitInfo(UnitType.Warship).maxHealth;
if (typeof maxHealth !== "number") {
expect(typeof maxHealth).toBe("number");
throw new Error("unreachable");
}
const port = player1.buildUnit(UnitType.Port, 0, game.ref(coastX, 10));
const port = player1.buildUnit(UnitType.Port, game.ref(coastX, 10), {});
const warship = player1.buildUnit(
UnitType.Warship,
0,
game.ref(coastX + 1, 10),
{},
);
game.executeNextTick();
@@ -88,13 +92,22 @@ describe("Warship", () => {
// Warship need one more tick (for warship exec to actually build warship)
game.executeNextTick();
expect(player1.units(UnitType.Warship)).toHaveLength(1);
expect(player1.units(UnitType.Port)).toHaveLength(1);
const dstPort = player2.buildUnit(
UnitType.Port,
game.ref(coastX + 2, 10),
{},
);
// Cannot buildExec with trade ship as it's not buildable (but
// we can obviously directly add it to the player)
const tradeShip = player2.buildUnit(
UnitType.TradeShip,
0,
game.ref(coastX + 1, 7),
{
dstPort,
},
);
expect(tradeShip.owner().id()).toBe(player2.id());
@@ -110,13 +123,17 @@ describe("Warship", () => {
constructionExecution(game, player1.id(), coastX + 1, 10, UnitType.Warship);
expect(player1.units(UnitType.Warship)).toHaveLength(1);
const [dstPort] = player1.units(UnitType.Port);
player1.units(UnitType.Port)[0].delete();
// Cannot buildExec with trade ship as it's not buildable (but
// we can obviously directly add it to the player)
const tradeShip = player2.buildUnit(
UnitType.TradeShip,
0,
game.ref(coastX + 1, 11),
{
dstPort,
},
);
expect(tradeShip.owner().id()).toBe(player2.id());
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+28 -7
View File
@@ -1,6 +1,14 @@
import fs from "fs/promises";
import path from "path";
import { Difficulty, GameType } from "../../src/core/game/Game";
import {
Difficulty,
Game,
GameMapType,
GameMode,
GameType,
PlayerInfo,
PlayerType,
} from "../../src/core/game/Game";
import { createGame } from "../../src/core/game/GameImpl";
import { genTerrainFromBin } from "../../src/core/game/TerrainMapLoader";
import { UserSettings } from "../../src/core/game/UserSettings";
@@ -9,7 +17,11 @@ import { generateMap } from "../../src/scripts/TerrainMapGenerator";
import { TestConfig } from "./TestConfig";
import { TestServerConfig } from "./TestServerConfig";
export async function setup(mapName: string, _gameConfig: GameConfig = {}) {
export async function setup(
mapName: string,
_gameConfig: Partial<GameConfig> = {},
humans: PlayerInfo[] = [],
): Promise<Game> {
// Load the specified map
const mapPath = path.join(__dirname, "..", "testdata", `${mapName}.png`);
const imageBuffer = await fs.readFile(mapPath);
@@ -18,12 +30,12 @@ export async function setup(mapName: string, _gameConfig: GameConfig = {}) {
const miniGameMap = await genTerrainFromBin(
String.fromCharCode.apply(null, miniMap),
);
const nationMap = { nations: [] };
// Configure the game
const serverConfig = new TestServerConfig();
const gameConfig = {
gameMap: null,
const gameConfig: GameConfig = {
gameMap: GameMapType.Asia,
gameMode: GameMode.FFA,
gameType: GameType.Singleplayer,
difficulty: Difficulty.Medium,
disableNPCs: false,
@@ -33,8 +45,17 @@ export async function setup(mapName: string, _gameConfig: GameConfig = {}) {
instantBuild: false,
..._gameConfig,
};
const config = new TestConfig(serverConfig, gameConfig, new UserSettings());
const config = new TestConfig(
serverConfig,
gameConfig,
new UserSettings(),
false,
);
// Create and return the game
return createGame([], gameMap, miniGameMap, nationMap, config); // TODO: !!!
return createGame(humans, [], gameMap, miniGameMap, config);
}
export function playerInfo(name: string, type: PlayerType): PlayerInfo {
return new PlayerInfo("fr", name, type, null, name);
}
+22 -3
View File
@@ -1,8 +1,30 @@
import { JWK } from "jose";
import { GameEnv, ServerConfig } from "../../src/core/configuration/Config";
import { GameMapType } from "../../src/core/game/Game";
import { GameID } from "../../src/core/Schemas";
export class TestServerConfig implements ServerConfig {
jwtAudience(): string {
throw new Error("Method not implemented.");
}
jwtIssuer(): string {
throw new Error("Method not implemented.");
}
jwkPublicKey(): Promise<JWK> {
throw new Error("Method not implemented.");
}
otelEnabled(): boolean {
throw new Error("Method not implemented.");
}
otelEndpoint(): string {
throw new Error("Method not implemented.");
}
otelUsername(): string {
throw new Error("Method not implemented.");
}
otelPassword(): string {
throw new Error("Method not implemented.");
}
region(): string {
return "test";
}
@@ -15,9 +37,6 @@ export class TestServerConfig implements ServerConfig {
lobbyMaxPlayers(map: GameMapType): number {
throw new Error("Method not implemented.");
}
discordRedirectURI(): string {
throw new Error("Method not implemented.");
}
numWorkers(): number {
throw new Error("Method not implemented.");
}