Skip to content

Commit 1b44273

Browse files
committed
Fix leftover troop removal on attack and add tests
1 parent d479c3e commit 1b44273

File tree

4 files changed

+312
-4
lines changed

4 files changed

+312
-4
lines changed

src/core/configuration/DefaultConfig.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -670,7 +670,7 @@ export class DefaultConfig implements Config {
670670

671671
if (attacker.isPlayer() && defender.isPlayer()) {
672672
if (defender.isDisconnected() && attacker.isOnSameTeam(defender)) {
673-
// No troop loss if defender is disconnected.
673+
// No troop loss if defender is disconnected and on same team
674674
mag = 0;
675675
}
676676
if (

src/core/execution/AttackExecution.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,13 @@ export class AttackExecution implements Execution {
105105
this.startTroops ??= this.mg
106106
.config()
107107
.attackAmount(this._owner, this.target);
108-
if (this.removeTroops) {
108+
if (
109+
this.removeTroops &&
110+
this.target.isPlayer() &&
111+
this.target.isDisconnected() === false &&
112+
this._owner.isOnSameTeam(this.target)
113+
) {
114+
// No troop loss if defender is disconnected and on same team
109115
this.startTroops = Math.min(this._owner.troops(), this.startTroops);
110116
this._owner.removeTroops(this.startTroops);
111117
}

src/core/execution/TransportShipExecution.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ export class TransportShipExecution implements Execution {
184184
this.attacker = this.boat.owner();
185185
this.originalOwner = this.boat.owner(); // for when this owner disconnects too
186186

187-
// Ensure retreat source is valid for the new owner, may be same port
187+
// Ensure retreat source is valid for the new owner, may be same tile
188188
const newSrc = this.attacker.canBuild(UnitType.TransportShip, this.dst);
189189
if (newSrc === false) {
190190
this.src = null;

tests/Disconnected.test.ts

Lines changed: 303 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,24 @@
1+
import { AttackExecution } from "../src/core/execution/AttackExecution";
12
import { MarkDisconnectedExecution } from "../src/core/execution/MarkDisconnectedExecution";
23
import { SpawnExecution } from "../src/core/execution/SpawnExecution";
3-
import { Game, Player, PlayerInfo, PlayerType } from "../src/core/game/Game";
4+
import { TransportShipExecution } from "../src/core/execution/TransportShipExecution";
5+
import { WarshipExecution } from "../src/core/execution/WarshipExecution";
6+
import {
7+
Game,
8+
GameMode,
9+
Player,
10+
PlayerInfo,
11+
PlayerType,
12+
UnitType,
13+
} from "../src/core/game/Game";
14+
import { toInt } from "../src/core/Util";
415
import { setup } from "./util/Setup";
516
import { executeTicks } from "./util/utils";
617

718
let game: Game;
819
let player1: Player;
920
let player2: Player;
21+
let enemy: Player;
1022

1123
describe("Disconnected", () => {
1224
beforeEach(async () => {
@@ -158,4 +170,294 @@ describe("Disconnected", () => {
158170
expect(player1.isDisconnected()).toBe(true);
159171
});
160172
});
173+
174+
describe("Disconnected team member interactions", () => {
175+
const coastX = 7;
176+
177+
beforeEach(async () => {
178+
const player1Info = new PlayerInfo(
179+
"[CLAN]Player1",
180+
PlayerType.Human,
181+
null,
182+
"player_1_id",
183+
);
184+
const player2Info = new PlayerInfo(
185+
"[CLAN]Player2",
186+
PlayerType.Human,
187+
null,
188+
"player_2_id",
189+
);
190+
191+
game = await setup(
192+
"half_land_half_ocean",
193+
{
194+
infiniteGold: true,
195+
instantBuild: true,
196+
gameMode: GameMode.Team,
197+
playerTeams: 2, // ignore player2 "kicked" console warn
198+
},
199+
[player1Info, player2Info],
200+
);
201+
202+
game.addExecution(
203+
new SpawnExecution(player1Info, game.map().ref(coastX - 2, 1)),
204+
new SpawnExecution(player2Info, game.map().ref(coastX - 2, 4)),
205+
);
206+
207+
while (game.inSpawnPhase()) {
208+
game.executeNextTick();
209+
}
210+
211+
player1 = game.player(player1Info.id);
212+
player2 = game.player(player2Info.id);
213+
player2.markDisconnected(false);
214+
215+
expect(player1.team()).not.toBeNull();
216+
expect(player2.team()).not.toBeNull();
217+
expect(player1.isOnSameTeam(player2)).toBe(true);
218+
});
219+
220+
test("Team Warships should not attack disconnected team mate ships", () => {
221+
const warship = player1.buildUnit(
222+
UnitType.Warship,
223+
game.map().ref(coastX + 1, 10),
224+
{
225+
patrolTile: game.map().ref(coastX + 1, 10),
226+
},
227+
);
228+
game.addExecution(new WarshipExecution(warship));
229+
230+
const transportShip = player2.buildUnit(
231+
UnitType.TransportShip,
232+
game.map().ref(coastX + 1, 11),
233+
{
234+
troops: 100,
235+
},
236+
);
237+
238+
player2.markDisconnected(true);
239+
executeTicks(game, 10);
240+
241+
expect(warship.targetUnit()).toBe(undefined);
242+
expect(transportShip.isActive()).toBe(true);
243+
expect(transportShip.owner()).toBe(player2);
244+
});
245+
246+
test("Disconnected player Warship should not attack team members' ships", () => {
247+
const warship = player2.buildUnit(
248+
UnitType.Warship,
249+
game.map().ref(coastX + 1, 5),
250+
{
251+
patrolTile: game.map().ref(coastX + 1, 10),
252+
},
253+
);
254+
game.addExecution(new WarshipExecution(warship));
255+
256+
const transportShip = player1.buildUnit(
257+
UnitType.TransportShip,
258+
game.map().ref(coastX + 1, 6),
259+
{
260+
troops: 100,
261+
},
262+
);
263+
264+
player2.markDisconnected(true);
265+
executeTicks(game, 10);
266+
267+
expect(warship.targetUnit()).toBe(undefined);
268+
expect(transportShip.isActive()).toBe(true);
269+
expect(transportShip.owner()).toBe(player1);
270+
});
271+
272+
test("Player can attack disconnected team mate without troop loss", () => {
273+
player2.conquer(game.map().ref(coastX - 2, 2));
274+
player2.conquer(game.map().ref(coastX - 2, 3));
275+
player2.markDisconnected(true);
276+
277+
const attackTroops = 1000;
278+
player1.addTroops(attackTroops);
279+
280+
const troopIncPerTick = game.config().troopIncreaseRate(player1);
281+
const expectedTroopGrowth = toInt(troopIncPerTick * 1);
282+
const expectedFinalTroops = Number(
283+
toInt(player1.troops()) + expectedTroopGrowth,
284+
);
285+
286+
game.addExecution(
287+
new AttackExecution(attackTroops, player1, player2.id(), null),
288+
);
289+
290+
executeTicks(game, 2); // first tick is the initial tick, in 2nd troop growth happens
291+
expect(player1.troops()).toBe(expectedFinalTroops);
292+
});
293+
294+
test("Conqueror gets conquered disconnected team member's transport- and warships", () => {
295+
const warship = player2.buildUnit(
296+
UnitType.Warship,
297+
game.map().ref(coastX + 1, 1),
298+
{
299+
patrolTile: game.map().ref(coastX + 1, 1),
300+
},
301+
);
302+
const transportShip = player2.buildUnit(
303+
UnitType.TransportShip,
304+
game.map().ref(coastX + 1, 3),
305+
{
306+
troops: 100,
307+
},
308+
);
309+
310+
player2.conquer(game.map().ref(coastX - 2, 1));
311+
player2.markDisconnected(true);
312+
313+
game.addExecution(new AttackExecution(1000, player1, player2.id(), null));
314+
315+
executeTicks(game, 10);
316+
317+
expect(player2.isAlive()).toBe(false);
318+
expect(warship.owner()).toBe(player1);
319+
expect(transportShip.owner()).toBe(player1);
320+
});
321+
322+
test("Captured transport ship landing attack should be in name of new owner", () => {
323+
player2.conquer(game.map().ref(coastX, 1));
324+
player2.conquer(game.map().ref(coastX - 1, 1));
325+
player2.conquer(game.map().ref(coastX, 2));
326+
327+
const enemyShoreTile = game.map().ref(coastX, 15);
328+
329+
game.addExecution(
330+
new TransportShipExecution(
331+
player2,
332+
null,
333+
enemyShoreTile,
334+
100,
335+
game.map().ref(coastX, 1),
336+
player2,
337+
),
338+
);
339+
340+
executeTicks(game, 1);
341+
342+
expect(player2.isAlive()).toBe(true);
343+
const transportShip = player2.units(UnitType.TransportShip)[0];
344+
expect(player2.units(UnitType.TransportShip).length).toBe(1);
345+
346+
player2.markDisconnected(true);
347+
game.addExecution(new AttackExecution(1000, player1, player2.id(), null));
348+
349+
executeTicks(game, 10);
350+
351+
expect(player2.isAlive()).toBe(false);
352+
expect(transportShip.owner()).toBe(player1);
353+
354+
executeTicks(game, 30);
355+
356+
// Verify ship landed and tile ownership transferred to new ship owner
357+
expect(game.owner(enemyShoreTile)).toBe(player1);
358+
});
359+
360+
test("Captured transport ship should retreat to owner's shore tile", () => {
361+
player1.conquer(game.map().ref(coastX, 4));
362+
player2.conquer(game.map().ref(coastX, 1));
363+
364+
const enemyShoreTile = game.map().ref(coastX, 8);
365+
366+
game.addExecution(
367+
new TransportShipExecution(
368+
player2,
369+
null,
370+
enemyShoreTile,
371+
100,
372+
game.map().ref(coastX, 1),
373+
player2,
374+
),
375+
);
376+
executeTicks(game, 1);
377+
378+
const transportShip = player2.units(UnitType.TransportShip)[0];
379+
expect(player2.units(UnitType.TransportShip).length).toBe(1);
380+
381+
expect(transportShip.targetTile()).toBe(enemyShoreTile);
382+
383+
player2.markDisconnected(true);
384+
game.addExecution(new AttackExecution(1000, player1, player2.id(), null));
385+
executeTicks(game, 10);
386+
387+
expect(player2.isAlive()).toBe(false);
388+
expect(transportShip.owner()).toBe(player1);
389+
390+
transportShip.orderBoatRetreat();
391+
executeTicks(game, 2);
392+
393+
expect(transportShip.targetTile()).not.toBe(enemyShoreTile);
394+
expect(game.owner(transportShip.targetTile()!)).toBe(player1);
395+
});
396+
397+
test("Retreating transport ship is deleted if new owner has no shore tiles", () => {
398+
player2.conquer(game.map().ref(coastX, 1));
399+
player2.conquer(game.map().ref(coastX - 6, 2));
400+
player1.conquer(game.map().ref(coastX - 6, 3));
401+
402+
const enemyShoreTile = game.map().ref(coastX, 15);
403+
404+
const boatTroops = 100;
405+
game.addExecution(
406+
new TransportShipExecution(
407+
player2,
408+
null,
409+
enemyShoreTile,
410+
boatTroops,
411+
game.map().ref(coastX, 1),
412+
player2,
413+
),
414+
);
415+
executeTicks(game, 1);
416+
417+
const transportShip = player2.units(UnitType.TransportShip)[0];
418+
expect(player2.units(UnitType.TransportShip).length).toBe(1);
419+
420+
player2.markDisconnected(true);
421+
game.addExecution(new AttackExecution(1000, player1, player2.id(), null));
422+
executeTicks(game, 2);
423+
424+
expect(player2.isAlive()).toBe(false);
425+
expect(transportShip.owner()).toBe(player1);
426+
427+
// Make sure player1 has no shore tiles for the ship to retreat to anymore
428+
const enemyInfo = new PlayerInfo(
429+
"Enemy",
430+
PlayerType.Human,
431+
null,
432+
"enemy_id",
433+
);
434+
enemy = game.addPlayer(enemyInfo);
435+
436+
const shoreTiles = Array.from(player1.borderTiles()).filter((t) =>
437+
game.isShore(t),
438+
);
439+
shoreTiles.forEach((tile) => {
440+
enemy.conquer(tile);
441+
});
442+
443+
expect(
444+
Array.from(player1.borderTiles()).filter((t) => game.isShore(t)).length,
445+
).toBe(0);
446+
447+
executeTicks(game, 1);
448+
449+
const troopIncPerTick = game.config().troopIncreaseRate(player1);
450+
const expectedTroopGrowth = toInt(troopIncPerTick * 1);
451+
const expectedFinalTroops = Number(
452+
toInt(player1.troops()) + expectedTroopGrowth,
453+
);
454+
455+
transportShip.orderBoatRetreat();
456+
executeTicks(game, 1);
457+
458+
expect(transportShip.isActive()).toBe(false);
459+
// Also test if boat troops were returned to player1 as new ship owner
460+
expect(player1.troops()).toBe(expectedFinalTroops + boatTroops);
461+
});
462+
});
161463
});

0 commit comments

Comments
 (0)