mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-18 15:43:32 +00:00
Merge branch 'main' into give-territory
This commit is contained in:
@@ -21,7 +21,7 @@ let attackerSpawn: TileRef;
|
||||
|
||||
function sendBoat(target: TileRef, source: TileRef, troops: number) {
|
||||
game.addExecution(
|
||||
new TransportShipExecution(defender.id(), null, target, troops, source),
|
||||
new TransportShipExecution(defender, null, target, troops, source),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -64,7 +64,9 @@ describe("Attack", () => {
|
||||
attacker = game.player(attackerInfo.id);
|
||||
defender = game.player(defenderInfo.id);
|
||||
|
||||
game.addExecution(new AttackExecution(100, defender.id(), null));
|
||||
game.addExecution(
|
||||
new AttackExecution(100, defender, game.terraNullius().id()),
|
||||
);
|
||||
game.executeNextTick();
|
||||
while (defender.outgoingAttacks().length > 0) {
|
||||
game.executeNextTick();
|
||||
@@ -76,10 +78,10 @@ describe("Attack", () => {
|
||||
test("Nuke reduce attacking troop counts", async () => {
|
||||
// Not building exactly spawn to it's better protected from attacks (but still
|
||||
// on defender territory)
|
||||
constructionExecution(game, defender.id(), 1, 1, UnitType.MissileSilo);
|
||||
constructionExecution(game, defender, 1, 1, UnitType.MissileSilo);
|
||||
expect(defender.units(UnitType.MissileSilo)).toHaveLength(1);
|
||||
game.addExecution(new AttackExecution(100, attacker.id(), defender.id()));
|
||||
constructionExecution(game, defender.id(), 0, 15, UnitType.AtomBomb, 3);
|
||||
game.addExecution(new AttackExecution(100, attacker, defender.id()));
|
||||
constructionExecution(game, defender, 0, 15, UnitType.AtomBomb, 3);
|
||||
const nuke = defender.units(UnitType.AtomBomb)[0];
|
||||
expect(nuke.isActive()).toBe(true);
|
||||
|
||||
@@ -94,12 +96,12 @@ describe("Attack", () => {
|
||||
});
|
||||
|
||||
test("Nuke reduce attacking boat troop count", async () => {
|
||||
constructionExecution(game, defender.id(), 1, 1, UnitType.MissileSilo);
|
||||
constructionExecution(game, defender, 1, 1, UnitType.MissileSilo);
|
||||
expect(defender.units(UnitType.MissileSilo)).toHaveLength(1);
|
||||
|
||||
sendBoat(game.ref(15, 8), game.ref(10, 5), 100);
|
||||
|
||||
constructionExecution(game, defender.id(), 0, 15, UnitType.AtomBomb, 3);
|
||||
constructionExecution(game, defender, 0, 15, UnitType.AtomBomb, 3);
|
||||
const nuke = defender.units(UnitType.AtomBomb)[0];
|
||||
expect(nuke.isActive()).toBe(true);
|
||||
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import { BotBehavior } from "../src/core/execution/utils/BotBehavior";
|
||||
import {
|
||||
AllianceRequest,
|
||||
Game,
|
||||
Player,
|
||||
PlayerInfo,
|
||||
PlayerType,
|
||||
Tick,
|
||||
} from "../src/core/game/Game";
|
||||
import { PseudoRandom } from "../src/core/PseudoRandom";
|
||||
import { setup } from "./util/Setup";
|
||||
|
||||
let game: Game;
|
||||
let player: Player;
|
||||
let requestor: Player;
|
||||
let botBehavior: BotBehavior;
|
||||
|
||||
describe("BotBehavior.handleAllianceRequests", () => {
|
||||
beforeEach(async () => {
|
||||
game = await setup("BigPlains", { infiniteGold: true, instantBuild: true });
|
||||
|
||||
const playerInfo = new PlayerInfo(
|
||||
"us",
|
||||
"player_id",
|
||||
PlayerType.Bot,
|
||||
null,
|
||||
"player_id",
|
||||
);
|
||||
const requestorInfo = new PlayerInfo(
|
||||
"fr",
|
||||
"requestor_id",
|
||||
PlayerType.Human,
|
||||
null,
|
||||
"requestor_id",
|
||||
);
|
||||
|
||||
game.addPlayer(playerInfo);
|
||||
game.addPlayer(requestorInfo);
|
||||
|
||||
player = game.player("player_id");
|
||||
requestor = game.player("requestor_id");
|
||||
|
||||
const random = new PseudoRandom(42);
|
||||
|
||||
botBehavior = new BotBehavior(random, game, player, 0.5, 0.5);
|
||||
});
|
||||
|
||||
function setupAllianceRequest({
|
||||
isTraitor = false,
|
||||
relationDelta = 2,
|
||||
numTilesPlayer = 10,
|
||||
numTilesRequestor = 10,
|
||||
alliancesCount = 0,
|
||||
} = {}) {
|
||||
if (isTraitor) requestor.markTraitor();
|
||||
|
||||
player.updateRelation(requestor, relationDelta);
|
||||
requestor.updateRelation(player, relationDelta);
|
||||
|
||||
game.map().forEachTile((tile) => {
|
||||
if (game.map().isLand(tile)) {
|
||||
if (numTilesPlayer > 0) {
|
||||
player.conquer(tile);
|
||||
numTilesPlayer--;
|
||||
} else if (numTilesRequestor > 0) {
|
||||
requestor.conquer(tile);
|
||||
numTilesRequestor--;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
jest
|
||||
.spyOn(requestor, "alliances")
|
||||
.mockReturnValue(new Array(alliancesCount));
|
||||
|
||||
const mockRequest = {
|
||||
requestor: () => requestor,
|
||||
recipient: () => player,
|
||||
createdAt: () => 0 as unknown as Tick,
|
||||
accept: jest.fn(),
|
||||
reject: jest.fn(),
|
||||
} as unknown as AllianceRequest;
|
||||
|
||||
jest
|
||||
.spyOn(player, "incomingAllianceRequests")
|
||||
.mockReturnValue([mockRequest]);
|
||||
|
||||
return mockRequest;
|
||||
}
|
||||
|
||||
test("should accept alliance when all conditions are met", () => {
|
||||
const request = setupAllianceRequest({});
|
||||
|
||||
botBehavior.handleAllianceRequests();
|
||||
|
||||
expect(request.accept).toHaveBeenCalled();
|
||||
expect(request.reject).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should reject alliance if requestor is a traitor", () => {
|
||||
const request = setupAllianceRequest({ isTraitor: true });
|
||||
|
||||
botBehavior.handleAllianceRequests();
|
||||
|
||||
expect(request.accept).not.toHaveBeenCalled();
|
||||
expect(request.reject).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should reject alliance if relation is malicious", () => {
|
||||
const request = setupAllianceRequest({ relationDelta: -2 });
|
||||
|
||||
botBehavior.handleAllianceRequests();
|
||||
|
||||
expect(request.accept).not.toHaveBeenCalled();
|
||||
expect(request.reject).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should accept alliance if requestor is much larger (> 3 times size of recipient) and has too many alliances (>= 3)", () => {
|
||||
const request = setupAllianceRequest({
|
||||
numTilesRequestor: 40,
|
||||
alliancesCount: 4,
|
||||
});
|
||||
|
||||
botBehavior.handleAllianceRequests();
|
||||
|
||||
expect(request.accept).toHaveBeenCalled();
|
||||
expect(request.reject).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should accept alliance if requestor is much larger (> 3 times size of recipient) and does not have too many alliances (< 3)", () => {
|
||||
const request = setupAllianceRequest({
|
||||
numTilesRequestor: 40,
|
||||
alliancesCount: 2,
|
||||
});
|
||||
|
||||
botBehavior.handleAllianceRequests();
|
||||
|
||||
expect(request.accept).toHaveBeenCalled();
|
||||
expect(request.reject).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should reject alliance if requestor is acceptably small (<= 3 times size of recipient) and has too many alliances (>= 3)", () => {
|
||||
const request = setupAllianceRequest({ alliancesCount: 3 });
|
||||
|
||||
botBehavior.handleAllianceRequests();
|
||||
|
||||
expect(request.accept).not.toHaveBeenCalled();
|
||||
expect(request.reject).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -159,7 +159,7 @@ describe("Disconnected", () => {
|
||||
executeTicks(game, 1);
|
||||
expect(player1.isDisconnected()).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
test("Breaking alliance with disconnected player doesn't make you a traitor", () => {
|
||||
player1.createAllianceRequest(player2);
|
||||
player2
|
||||
|
||||
@@ -20,7 +20,7 @@ function attackerBuildsNuke(
|
||||
initialize = true,
|
||||
) {
|
||||
game.addExecution(
|
||||
new NukeExecution(UnitType.AtomBomb, attacker.id(), target, source),
|
||||
new NukeExecution(UnitType.AtomBomb, attacker, target, source),
|
||||
);
|
||||
if (initialize) {
|
||||
game.executeNextTick();
|
||||
@@ -50,7 +50,7 @@ describe("MissileSilo", () => {
|
||||
|
||||
attacker = game.player("attacker_id");
|
||||
|
||||
constructionExecution(game, attacker.id(), 1, 1, UnitType.MissileSilo);
|
||||
constructionExecution(game, attacker, 1, 1, UnitType.MissileSilo);
|
||||
});
|
||||
|
||||
test("missilesilo should launch nuke", async () => {
|
||||
|
||||
+58
-9
@@ -1,3 +1,4 @@
|
||||
import { NukeExecution } from "../src/core/execution/NukeExecution";
|
||||
import { SAMLauncherExecution } from "../src/core/execution/SAMLauncherExecution";
|
||||
import { SpawnExecution } from "../src/core/execution/SpawnExecution";
|
||||
import {
|
||||
@@ -13,10 +14,11 @@ import { constructionExecution, executeTicks } from "./util/utils";
|
||||
let game: Game;
|
||||
let attacker: Player;
|
||||
let defender: Player;
|
||||
let far_defender: Player;
|
||||
|
||||
describe("SAM", () => {
|
||||
beforeEach(async () => {
|
||||
game = await setup("Plains", { infiniteGold: true, instantBuild: true });
|
||||
game = await setup("BigPlains", { infiniteGold: true, instantBuild: true });
|
||||
const defender_info = new PlayerInfo(
|
||||
"us",
|
||||
"defender_id",
|
||||
@@ -24,6 +26,13 @@ describe("SAM", () => {
|
||||
null,
|
||||
"defender_id",
|
||||
);
|
||||
const far_defender_info = new PlayerInfo(
|
||||
"us",
|
||||
"far_defender_id",
|
||||
PlayerType.Human,
|
||||
null,
|
||||
"far_defender_id",
|
||||
);
|
||||
const attacker_info = new PlayerInfo(
|
||||
"fr",
|
||||
"attacker_id",
|
||||
@@ -32,10 +41,15 @@ describe("SAM", () => {
|
||||
"attacker_id",
|
||||
);
|
||||
game.addPlayer(defender_info);
|
||||
game.addPlayer(far_defender_info);
|
||||
game.addPlayer(attacker_info);
|
||||
|
||||
game.addExecution(
|
||||
new SpawnExecution(game.player(defender_info.id).info(), game.ref(1, 1)),
|
||||
new SpawnExecution(
|
||||
game.player(far_defender_info.id).info(),
|
||||
game.ref(199, 1),
|
||||
),
|
||||
new SpawnExecution(game.player(attacker_info.id).info(), game.ref(7, 7)),
|
||||
);
|
||||
|
||||
@@ -43,16 +57,19 @@ describe("SAM", () => {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
defender = game.player("defender_id");
|
||||
attacker = game.player("attacker_id");
|
||||
defender = game.player("defender_id");
|
||||
far_defender = game.player("far_defender_id");
|
||||
|
||||
constructionExecution(game, attacker.id(), 7, 7, UnitType.MissileSilo);
|
||||
constructionExecution(game, attacker, 7, 7, UnitType.MissileSilo);
|
||||
});
|
||||
|
||||
test("one sam should take down one nuke", async () => {
|
||||
const sam = defender.buildUnit(UnitType.SAMLauncher, game.ref(1, 1), {});
|
||||
game.addExecution(new SAMLauncherExecution(defender.id(), null, sam));
|
||||
attacker.buildUnit(UnitType.AtomBomb, game.ref(1, 1), {});
|
||||
game.addExecution(new SAMLauncherExecution(defender, null, sam));
|
||||
attacker.buildUnit(UnitType.AtomBomb, game.ref(1, 1), {
|
||||
targetTile: game.ref(2, 1),
|
||||
});
|
||||
|
||||
executeTicks(game, 3);
|
||||
|
||||
@@ -61,7 +78,7 @@ describe("SAM", () => {
|
||||
|
||||
test("sam should only get one nuke at a time", async () => {
|
||||
const sam = defender.buildUnit(UnitType.SAMLauncher, game.ref(1, 1), {});
|
||||
game.addExecution(new SAMLauncherExecution(defender.id(), null, sam));
|
||||
game.addExecution(new SAMLauncherExecution(defender, null, sam));
|
||||
attacker.buildUnit(UnitType.AtomBomb, game.ref(2, 1), {
|
||||
targetTile: game.ref(2, 1),
|
||||
});
|
||||
@@ -77,7 +94,7 @@ describe("SAM", () => {
|
||||
|
||||
test("sam should cooldown as long as configured", async () => {
|
||||
const sam = defender.buildUnit(UnitType.SAMLauncher, game.ref(1, 1), {});
|
||||
game.addExecution(new SAMLauncherExecution(defender.id(), null, sam));
|
||||
game.addExecution(new SAMLauncherExecution(defender, null, sam));
|
||||
expect(sam.isInCooldown()).toBeFalsy();
|
||||
const nuke = attacker.buildUnit(UnitType.AtomBomb, game.ref(1, 2), {
|
||||
targetTile: game.ref(1, 2),
|
||||
@@ -100,9 +117,9 @@ describe("SAM", () => {
|
||||
const sam1 = defender.buildUnit(UnitType.SAMLauncher, game.ref(1, 1), {
|
||||
cooldownDuration: 10,
|
||||
});
|
||||
game.addExecution(new SAMLauncherExecution(defender.id(), null, sam1));
|
||||
game.addExecution(new SAMLauncherExecution(defender, null, sam1));
|
||||
const sam2 = defender.buildUnit(UnitType.SAMLauncher, game.ref(1, 2), {});
|
||||
game.addExecution(new SAMLauncherExecution(defender.id(), null, sam2));
|
||||
game.addExecution(new SAMLauncherExecution(defender, null, sam2));
|
||||
const nuke = attacker.buildUnit(UnitType.AtomBomb, game.ref(2, 2), {
|
||||
targetTile: game.ref(2, 2),
|
||||
});
|
||||
@@ -112,4 +129,36 @@ describe("SAM", () => {
|
||||
expect(nuke.isActive()).toBeFalsy();
|
||||
expect([sam1, sam2].filter((s) => s.isInCooldown())).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("SAMs should target only nukes aimed at nearby targets", async () => {
|
||||
const targetDistance = 199;
|
||||
// Close SAM: should not intercept anything
|
||||
const sam1 = defender.buildUnit(UnitType.SAMLauncher, game.ref(1, 1), {});
|
||||
game.addExecution(new SAMLauncherExecution(defender, null, sam1));
|
||||
|
||||
// Far SAM: Should intercept the nuke. Use the far_defender so the SAM can be built
|
||||
const sam2 = far_defender.buildUnit(
|
||||
UnitType.SAMLauncher,
|
||||
game.ref(targetDistance, 1),
|
||||
{},
|
||||
);
|
||||
game.addExecution(new SAMLauncherExecution(far_defender, null, sam2));
|
||||
|
||||
const nukeExecution = new NukeExecution(
|
||||
UnitType.AtomBomb,
|
||||
attacker,
|
||||
game.ref(targetDistance, 1),
|
||||
null,
|
||||
);
|
||||
game.addExecution(nukeExecution);
|
||||
// Long distance nuke: compute the proper number of ticks
|
||||
const ticksToExecute = Math.ceil(
|
||||
targetDistance / game.config().defaultNukeSpeed(),
|
||||
);
|
||||
executeTicks(game, ticksToExecute);
|
||||
|
||||
expect(nukeExecution.isActive()).toBeFalsy();
|
||||
expect(sam1.isInCooldown()).toBeFalsy();
|
||||
expect(sam2.isInCooldown()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,21 @@ import { GameMapType } from "../../src/core/game/Game";
|
||||
import { GameID } from "../../src/core/Schemas";
|
||||
|
||||
export class TestServerConfig implements ServerConfig {
|
||||
cloudflareConfigDir(): string {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
domain(): string {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
subdomain(): string {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
cloudflareAccountId(): string {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
cloudflareApiToken(): string {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
jwtAudience(): string {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
+3
-3
@@ -4,18 +4,18 @@
|
||||
// If you also need execution use function below. Does not work with things not
|
||||
|
||||
import { ConstructionExecution } from "../../src/core/execution/ConstructionExecution";
|
||||
import { Game, PlayerID, UnitType } from "../../src/core/game/Game";
|
||||
import { Game, Player, UnitType } from "../../src/core/game/Game";
|
||||
|
||||
// built via UI (e.g.: trade ships)
|
||||
export function constructionExecution(
|
||||
game: Game,
|
||||
playerID: PlayerID,
|
||||
_owner: Player,
|
||||
x: number,
|
||||
y: number,
|
||||
unit: UnitType,
|
||||
ticks = 4,
|
||||
) {
|
||||
game.addExecution(new ConstructionExecution(playerID, game.ref(x, y), unit));
|
||||
game.addExecution(new ConstructionExecution(_owner, game.ref(x, y), unit));
|
||||
|
||||
// 4 ticks by default as it usually goes like this
|
||||
// Init of construction execution
|
||||
|
||||
Reference in New Issue
Block a user