Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 22 additions & 8 deletions src/client/ClientGameRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -943,7 +943,8 @@ export class ClientGameRunner {
this.eventBus.emit(
new SendAttackIntentEvent(
this.gameView.owner(tile).id(),
this.myPlayer!.troops() * this.renderer.uiState.attackRatio,
Math.floor(100 * this.renderer.uiState.attackRatio),
this.myPlayer!.troops(),
),
);
} else if (this.canAutoBoat(actions.buildableUnits, tile)) {
Expand Down Expand Up @@ -1102,7 +1103,8 @@ export class ClientGameRunner {
this.eventBus.emit(
new SendAttackIntentEvent(
this.gameView.owner(tile).id(),
this.myPlayer!.troops() * this.renderer.uiState.attackRatio,
Math.floor(100 * this.renderer.uiState.attackRatio),
this.myPlayer!.troops(),
),
);
}
Expand Down Expand Up @@ -1136,12 +1138,23 @@ export class ClientGameRunner {
mostRecentAttack.attackerID,
) as PlayerView;
if (!attacker) return;

const counterTroops = Math.min(
mostRecentAttack.troops,
this.renderer.uiState.attackRatio * this.myPlayer.troops(),
this.eventBus.emit(
new SendAttackIntentEvent(
attacker.id(),
Math.floor(
// Ensures the attackRatio is between 1% and the required percentage to defend fully.
Math.max(
100 *
Math.min(
this.renderer.uiState.attackRatio,
mostRecentAttack.troops / this.myPlayer.troops(),
),
1,
),
),
this.myPlayer.troops(),
),
);
this.eventBus.emit(new SendAttackIntentEvent(attacker.id(), counterTroops));
}

private doRequestAllianceUnderCursor(): void {
Expand Down Expand Up @@ -1226,7 +1239,8 @@ export class ClientGameRunner {
this.eventBus.emit(
new SendBoatAttackIntentEvent(
tile,
this.myPlayer.troops() * this.renderer.uiState.attackRatio,
this.renderer.uiState.attackRatio,
this.myPlayer.troops(),
),
);
}
Expand Down
13 changes: 9 additions & 4 deletions src/client/Transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,17 @@ export class SendSpawnIntentEvent implements GameEvent {
export class SendAttackIntentEvent implements GameEvent {
constructor(
public readonly targetID: PlayerID | null,
public readonly troops: number,
public readonly troopRatio: number,
public readonly troopCount: number,
public readonly maxTroopCount: number | null = null,
) {}
}

export class SendBoatAttackIntentEvent implements GameEvent {
constructor(
public readonly dst: TileRef,
public readonly troops: number,
public readonly troopRatio: number,
public readonly troopCount: number,
) {}
}

Expand Down Expand Up @@ -484,14 +487,16 @@ export class Transport {
this.sendIntent({
type: "attack",
targetID: event.targetID,
troops: event.troops,
troopRatio: event.troopRatio,
troopCount: event.troopCount,
});
}

private onSendBoatAttackIntent(event: SendBoatAttackIntentEvent) {
this.sendIntent({
type: "boat",
troops: event.troops,
troopRatio: event.troopRatio,
troopCount: event.troopCount,
dst: event.dst,
});
}
Expand Down
21 changes: 16 additions & 5 deletions src/client/hud/layers/AttacksDisplay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,12 +200,23 @@ export class AttacksDisplay extends LitElement implements Controller {

const myPlayer = this.game.myPlayer();
if (!myPlayer) return;

const counterTroops = Math.min(
attack.troops,
this.uiState.attackRatio * myPlayer.troops(),
this.eventBus.emit(
new SendAttackIntentEvent(
attacker.id(),
Math.floor(
// Ensures the attackRatio is between 1% and the required percentage to defend fully.
Math.max(
100 *
Math.min(
this.uiState.attackRatio,
attack.troops / myPlayer.troops(),
),
1,
),
),
myPlayer.troops(),
),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
);
this.eventBus.emit(new SendAttackIntentEvent(attacker.id(), counterTroops));
}

private renderIncomingAttacks() {
Expand Down
6 changes: 4 additions & 2 deletions src/client/hud/layers/PlayerActionHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export class PlayerActionHandler {
this.eventBus.emit(
new SendAttackIntentEvent(
targetId,
this.uiState.attackRatio * player.troops(),
Math.floor(100 * this.uiState.attackRatio),
player.troops(),
),
);
}
Expand All @@ -36,7 +37,8 @@ export class PlayerActionHandler {
this.eventBus.emit(
new SendBoatAttackIntentEvent(
targetTile,
this.uiState.attackRatio * player.troops(),
Math.floor(100 * this.uiState.attackRatio),
player.troops(),
),
);
}
Expand Down
6 changes: 4 additions & 2 deletions src/core/Schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,8 @@ export const AllianceExtensionIntentSchema = z.object({
export const AttackIntentSchema = z.object({
type: z.literal("attack"),
targetID: ID.nullable(),
troops: z.number().nonnegative().nullable(),
troopCount: z.number().int().nonnegative(),
troopRatio: z.number().int().gt(0).max(100),
});

export const SpawnIntentSchema = z.object({
Expand All @@ -369,7 +370,8 @@ export const SpawnIntentSchema = z.object({

export const BoatAttackIntentSchema = z.object({
type: z.literal("boat"),
troops: z.number().nonnegative(),
troopCount: z.number().int().nonnegative(),
troopRatio: z.number().int().gt(0).max(100),
dst: z.number(),
});

Expand Down
72 changes: 68 additions & 4 deletions src/core/execution/ExecutionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,67 @@ export class Executor {
this.random = new PseudoRandom(simpleHash(gameID) + 1);
}

private computeRatio(
counterTroopRatio: number,
remainingTroopRatio: number,
totalRatioUsage: number,
): number {
const factor = 100 ** (counterTroopRatio - 1);
return Math.floor(
(100 * (100 * factor - remainingTroopRatio)) / factor / totalRatioUsage,
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

createExecs(turn: Turn): Execution[] {
return turn.intents.map((i) => this.createExec(i));
// In the rare case a client sends multiple troopRatio-orders,
// we need to "merge" their orders instead of executing them in parallel.
// (two 60% attacks should be one 84% attack, not one 120% attack)
// But, they may be of different types/on different targets
// (hence we do two (84/120)*60% = 42% attacks).
const counterTroopRatio_perClientID = new Map<ClientID, number>();
const remainingTroopRatio_perClientID = new Map<ClientID, number>();
const totalRatioUsage_perClientID = new Map<ClientID, number>();
for (const intent of turn.intents) {
switch (intent.type) {
case "boat":
case "attack": {
counterTroopRatio_perClientID.set(
intent.clientID,
(counterTroopRatio_perClientID.get(intent.clientID) ?? 0) + 1,
);
remainingTroopRatio_perClientID.set(
intent.clientID,
(remainingTroopRatio_perClientID.get(intent.clientID) ?? 1) *
(100 - intent.troopRatio),
);
totalRatioUsage_perClientID.set(
intent.clientID,
(totalRatioUsage_perClientID.get(intent.clientID) ?? 0) +
intent.troopRatio,
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
}

return turn.intents.map((intent) => {
switch (intent.type) {
case "boat":
case "attack":
return this.createExec(
intent,
this.computeRatio(
counterTroopRatio_perClientID.get(intent.clientID)!,
remainingTroopRatio_perClientID.get(intent.clientID)!,
totalRatioUsage_perClientID.get(intent.clientID)!,
),
);
default:
return this.createExec(intent, undefined);
}
});
}

createExec(intent: StampedIntent): Execution {
createExec(intent: StampedIntent, troopRatioFactor = 100): Execution {
const player = this.mg.playerByClientID(intent.clientID);
if (!player) {
console.warn(`player with clientID ${intent.clientID} not found`);
Expand All @@ -57,7 +113,9 @@ export class Executor {
switch (intent.type) {
case "attack": {
return new AttackExecution(
intent.troops,
Math.floor(
(troopRatioFactor * intent.troopRatio * intent.troopCount) / 10000,
),
player,
intent.targetID,
null,
Expand All @@ -72,7 +130,13 @@ export class Executor {
case "spawn":
return new SpawnExecution(this.gameID, player.info(), intent.tile);
case "boat":
return new TransportShipExecution(player, intent.dst, intent.troops);
return new TransportShipExecution(
player,
intent.dst,
Math.floor(
(troopRatioFactor * intent.troopRatio * intent.troopCount) / 10000,
),
);
case "allianceRequest":
return new AllianceRequestExecution(player, intent.recipient);
case "allianceReject":
Expand Down
12 changes: 6 additions & 6 deletions tests/Attack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,6 @@ let defender: Player;
let defenderSpawn: TileRef;
let attackerSpawn: TileRef;

function sendBoat(target: TileRef, troops: number) {
game.addExecution(new TransportShipExecution(defender, target, troops));
}

const immunityPhaseTicks = 10;
function waitForImmunityToEnd() {
for (let i = 0; i < immunityPhaseTicks + 1; i++) {
Expand Down Expand Up @@ -110,7 +106,9 @@ describe("Attack", () => {
constructionExecution(game, defender, 1, 1, UnitType.MissileSilo);
expect(defender.units(UnitType.MissileSilo)).toHaveLength(1);

sendBoat(game.ref(15, 8), 100);
game.addExecution(
new TransportShipExecution(defender, game.ref(15, 8), 100),
);

constructionExecution(game, defender, 0, 15, UnitType.AtomBomb, 3);
const nuke = defender.units(UnitType.AtomBomb)[0];
Expand All @@ -129,7 +127,9 @@ describe("Attack", () => {
const player_start_troops = defender.troops();
const boat_troops = player_start_troops * 0.5;

sendBoat(game.ref(15, 8), boat_troops);
game.addExecution(
new TransportShipExecution(defender, game.ref(15, 8), boat_troops),
);

game.executeNextTick();

Expand Down
Loading
Loading