From 76a1f1487c79268bb43116029d851150c7a8efc3 Mon Sep 17 00:00:00 2001
From: 1brucben <1benjbruce@gmail.com>
Date: Fri, 18 Apr 2025 18:18:38 +0200
Subject: [PATCH] added defensive posture options with experimental effect on
defense
---
src/client/Transport.ts | 14 +++++
src/client/graphics/layers/ControlPanel.ts | 58 ++++++++++++++++++-
src/core/Schemas.ts | 12 +++-
src/core/configuration/DefaultConfig.ts | 36 +++++++++---
src/core/execution/AttackExecution.ts | 6 ++
src/core/execution/ExecutionManager.ts | 4 ++
.../execution/SetDefensivePostureExecution.ts | 41 +++++++++++++
src/core/game/Game.ts | 2 +
src/core/game/PlayerImpl.ts | 8 +++
9 files changed, 171 insertions(+), 10 deletions(-)
create mode 100644 src/core/execution/SetDefensivePostureExecution.ts
diff --git a/src/client/Transport.ts b/src/client/Transport.ts
index d3c570a9b..247686e05 100644
--- a/src/client/Transport.ts
+++ b/src/client/Transport.ts
@@ -98,6 +98,9 @@ export class SendDonateGoldIntentEvent implements GameEvent {
public readonly gold: number | null,
) {}
}
+export class SendSetDefensivePostureEvent {
+ constructor(public readonly posture: "retreat" | "balanced" | "hold") {}
+}
export class SendDonateTroopsIntentEvent implements GameEvent {
constructor(
@@ -213,6 +216,9 @@ export class Transport {
this.eventBus.on(MoveWarshipIntentEvent, (e) => {
this.onMoveWarshipEvent(e);
});
+ this.eventBus.on(SendSetDefensivePostureEvent, (e) =>
+ this.onSendSetDefensivePostureEvent(e),
+ );
}
private startPing() {
@@ -552,6 +558,14 @@ export class Transport {
});
}
+ private onSendSetDefensivePostureEvent(event: SendSetDefensivePostureEvent) {
+ this.sendIntent({
+ type: "setDefensivePosture",
+ clientID: this.lobbyConfig.clientID,
+ posture: event.posture,
+ });
+ }
+
private sendIntent(intent: Intent) {
if (this.isLocal || this.socket.readyState === WebSocket.OPEN) {
const msg = ClientIntentMessageSchema.parse({
diff --git a/src/client/graphics/layers/ControlPanel.ts b/src/client/graphics/layers/ControlPanel.ts
index fac9fb6b6..9f9188392 100644
--- a/src/client/graphics/layers/ControlPanel.ts
+++ b/src/client/graphics/layers/ControlPanel.ts
@@ -4,7 +4,10 @@ import { EventBus } from "../../../core/EventBus";
import { GameView } from "../../../core/game/GameView";
import { ClientID } from "../../../core/Schemas";
import { AttackRatioEvent } from "../../InputHandler";
-import { SendSetTargetTroopRatioEvent } from "../../Transport";
+import {
+ SendSetDefensivePostureEvent,
+ SendSetTargetTroopRatioEvent,
+} from "../../Transport";
import { renderNumber, renderTroops } from "../../Utils";
import { UIState } from "../UIState";
import { Layer } from "./Layer";
@@ -22,6 +25,9 @@ export class ControlPanel extends LitElement implements Layer {
@state()
private targetTroopRatio = 0.95;
+ @state()
+ private defensivePosture: "retreat" | "balanced" | "hold" = "balanced";
+
@state()
private currentTroopRatio = 0.95;
@@ -65,6 +71,8 @@ export class ControlPanel extends LitElement implements Layer {
this.targetTroopRatio = Number(
localStorage.getItem("settings.troopRatio") ?? "0.95",
);
+ this.defensivePosture =
+ (localStorage.getItem("settings.defensivePosture") as any) ?? "balanced";
this.init_ = true;
this.uiState.attackRatio = this.attackRatio;
this.currentTroopRatio = this.targetTroopRatio;
@@ -135,6 +143,16 @@ export class ControlPanel extends LitElement implements Layer {
this.uiState.attackRatio = newRatio;
}
+ private onPostureChange(e: Event) {
+ const raw = (e.target as HTMLInputElement).value;
+
+ if (raw === "retreat" || raw === "balanced" || raw === "hold") {
+ const value = raw as "retreat" | "balanced" | "hold";
+ this.eventBus.emit(new SendSetDefensivePostureEvent(value));
+ } else {
+ console.warn(`Unexpected posture value: ${raw}`);
+ }
+ }
renderLayer(context: CanvasRenderingContext2D) {
// Render any necessary canvas elements
}
@@ -297,6 +315,44 @@ export class ControlPanel extends LitElement implements Layer {
/>
+
`;
}
diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts
index 7fc8e63a5..c9cd90dc2 100644
--- a/src/core/Schemas.ts
+++ b/src/core/Schemas.ts
@@ -28,7 +28,8 @@ export type Intent =
| TargetTroopRatioIntent
| BuildUnitIntent
| EmbargoIntent
- | MoveWarshipIntent;
+ | MoveWarshipIntent
+ | SetDefensivePostureIntent;
export type AttackIntent = z.infer;
export type CancelAttackIntent = z.infer;
@@ -52,6 +53,9 @@ export type MoveWarshipIntent = z.infer;
export type Turn = z.infer;
export type GameConfig = z.infer;
+export type SetDefensivePostureIntent = z.infer<
+ typeof SetDefensivePostureIntentSchema
+>;
export type ClientMessage =
| ClientSendWinnerMessage
@@ -268,6 +272,11 @@ export const MoveWarshipIntentSchema = BaseIntentSchema.extend({
tile: z.number(),
});
+export const SetDefensivePostureIntentSchema = BaseIntentSchema.extend({
+ type: z.literal("setDefensivePosture"),
+ posture: z.enum(["retreat", "balanced", "hold"]),
+});
+
const IntentSchema = z.union([
AttackIntentSchema,
CancelAttackIntentSchema,
@@ -284,6 +293,7 @@ const IntentSchema = z.union([
BuildUnitIntentSchema,
EmbargoIntentSchema,
MoveWarshipIntentSchema,
+ SetDefensivePostureIntentSchema,
]);
export const TurnSchema = z.object({
diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts
index e3d87ddbe..d57a74ed3 100644
--- a/src/core/configuration/DefaultConfig.ts
+++ b/src/core/configuration/DefaultConfig.ts
@@ -462,15 +462,35 @@ export class DefaultConfig implements Config {
}
if (defender.isPlayer()) {
- const defenderdensity = defender.troops() / defender.numTilesOwned();
- if (attacker.type() == PlayerType.Human) {
- console.log(
- "speed:",
- defenderdensity *
- Math.max(defender.troops() / attackTroops, 0.3) ** 0.5 *
- speed,
- );
+ let postureExponent = 1;
+ if (defender.isPlayer()) {
+ const posture = defender.defensivePosture?.() ?? "balanced";
+ switch (posture) {
+ case "retreat":
+ postureExponent = 1.4;
+ break;
+ case "balanced":
+ postureExponent = 1.0;
+ break;
+ case "hold":
+ postureExponent = 0.6;
+ break;
+ }
+ if (defender.isPlayer() && defender.type() === PlayerType.Human) {
+ console.log("Defensive posture:", posture);
+ console.log("Exponent used for defender density:", postureExponent);
+ }
}
+ const defenderdensity =
+ defender.troops() / Math.pow(defender.numTilesOwned(), postureExponent);
+ // if (attacker.type() == PlayerType.Human) {
+ // console.log(
+ // "speed:",
+ // defenderdensity *
+ // Math.max(defender.troops() / attackTroops, 0.3) ** 0.5 *
+ // speed,
+ // );
+ // }
return {
attackerTroopLoss:
mag * 10 +
diff --git a/src/core/execution/AttackExecution.ts b/src/core/execution/AttackExecution.ts
index ae77969bb..6163a464c 100644
--- a/src/core/execution/AttackExecution.ts
+++ b/src/core/execution/AttackExecution.ts
@@ -254,6 +254,12 @@ export class AttackExecution implements Execution {
continue;
}
this.addNeighbors(tileToConquer);
+ const posture: "retreat" | "balanced" | "hold" = "balanced";
+ // if (this.target.isPlayer()) {
+ // posture = (this.target as Player).defensivePosture?.() ?? "balanced";
+ // console.log("Defender posture:", posture);
+ // }
+
const { attackerTroopLoss, defenderTroopLoss, tilesPerTickUsed } = this.mg
.config()
.attackLogic(
diff --git a/src/core/execution/ExecutionManager.ts b/src/core/execution/ExecutionManager.ts
index 968455cc1..4bba72b65 100644
--- a/src/core/execution/ExecutionManager.ts
+++ b/src/core/execution/ExecutionManager.ts
@@ -16,6 +16,7 @@ import { FakeHumanExecution } from "./FakeHumanExecution";
import { MoveWarshipExecution } from "./MoveWarshipExecution";
import { NoOpExecution } from "./NoOpExecution";
import { RetreatExecution } from "./RetreatExecution";
+import { SetDefensivePostureExecution } from "./SetDefensivePostureExecution";
import { SetTargetTroopRatioExecution } from "./SetTargetTroopRatioExecution";
import { SpawnExecution } from "./SpawnExecution";
import { TargetPlayerExecution } from "./TargetPlayerExecution";
@@ -103,6 +104,9 @@ export class Executor {
this.mg.ref(intent.x, intent.y),
intent.unit,
);
+ case "setDefensivePosture":
+ return new SetDefensivePostureExecution(playerID, intent.posture);
+
default:
throw new Error(`intent type ${intent} not found`);
}
diff --git a/src/core/execution/SetDefensivePostureExecution.ts b/src/core/execution/SetDefensivePostureExecution.ts
new file mode 100644
index 000000000..476d2eea5
--- /dev/null
+++ b/src/core/execution/SetDefensivePostureExecution.ts
@@ -0,0 +1,41 @@
+import { Execution, Game, PlayerID } from "../game/Game";
+
+export class SetDefensivePostureExecution implements Execution {
+ constructor(
+ private playerID: PlayerID,
+ private posture: "retreat" | "balanced" | "hold",
+ ) {}
+
+ init(mg: Game, ticks: number): void {
+ const player = mg.player(this.playerID);
+ if (!player) {
+ console.warn(
+ `SetDefensivePostureExecution: player ${this.playerID} not found`,
+ );
+ return;
+ }
+
+ if (!player.setDefensivePosture) {
+ console.warn(
+ `SetDefensivePostureExecution: setDefensivePosture not defined on player`,
+ );
+ return;
+ }
+
+ player.setDefensivePosture(this.posture);
+ }
+ tick(_ticks: number): void {
+ // No-op: nothing happens over time
+ }
+ activeDuringSpawnPhase(): boolean {
+ return false;
+ }
+
+ isActive(): boolean {
+ return false; // It's a one-time effect
+ }
+
+ owner(): null {
+ return null;
+ }
+}
diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts
index d0fa0caf2..5391c692a 100644
--- a/src/core/game/Game.ts
+++ b/src/core/game/Game.ts
@@ -333,6 +333,8 @@ export interface Player {
workers(): number;
troops(): number;
targetTroopRatio(): number;
+ defensivePosture(): "retreat" | "balanced" | "hold";
+ setDefensivePosture(p: "retreat" | "balanced" | "hold"): void;
addGold(toAdd: Gold): void;
removeGold(toRemove: Gold): void;
addWorkers(toAdd: number): void;
diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts
index ba647f77e..58046a4dc 100644
--- a/src/core/game/PlayerImpl.ts
+++ b/src/core/game/PlayerImpl.ts
@@ -67,6 +67,8 @@ export class PlayerImpl implements Player {
// 0 to 100
private _targetTroopRatio: bigint;
+ private _defensivePosture: "retreat" | "balanced" | "hold" = "balanced";
+
isTraitor_ = false;
private embargoes: Set = new Set();
@@ -645,6 +647,12 @@ export class PlayerImpl implements Player {
}
this._targetTroopRatio = toInt(target * 100);
}
+ public defensivePosture(): "retreat" | "balanced" | "hold" {
+ return this._defensivePosture;
+ }
+ public setDefensivePosture(p: "retreat" | "balanced" | "hold") {
+ this._defensivePosture = p;
+ }
troops(): number {
return Number(this._troops);