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);