added defensive posture options with experimental effect on defense

This commit is contained in:
1brucben
2025-04-18 18:18:38 +02:00
parent 97fa91769a
commit 76a1f1487c
9 changed files with 171 additions and 10 deletions
+14
View File
@@ -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({
+57 -1
View File
@@ -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 {
/>
</div>
</div>
<div class="text-white mt-4">
<label class="block font-bold mb-1">Defensive Posture</label>
<div class="flex gap-2">
<label class="flex items-center gap-1 cursor-pointer">
<input
type="radio"
name="posture"
value="retreat"
.checked=${this.defensivePosture === "retreat"}
@change=${this.onPostureChange}
class="form-radio text-blue-500"
/>
Retreat
</label>
<label class="flex items-center gap-1 cursor-pointer">
<input
type="radio"
name="posture"
value="balanced"
.checked=${this.defensivePosture === "balanced"}
@change=${this.onPostureChange}
class="form-radio text-blue-500"
/>
Balanced
</label>
<label class="flex items-center gap-1 cursor-pointer">
<input
type="radio"
name="posture"
value="hold"
.checked=${this.defensivePosture === "hold"}
@change=${this.onPostureChange}
class="form-radio text-blue-500"
/>
Hold
</label>
</div>
</div>
</div>
`;
}
+11 -1
View File
@@ -28,7 +28,8 @@ export type Intent =
| TargetTroopRatioIntent
| BuildUnitIntent
| EmbargoIntent
| MoveWarshipIntent;
| MoveWarshipIntent
| SetDefensivePostureIntent;
export type AttackIntent = z.infer<typeof AttackIntentSchema>;
export type CancelAttackIntent = z.infer<typeof CancelAttackIntentSchema>;
@@ -52,6 +53,9 @@ export type MoveWarshipIntent = z.infer<typeof MoveWarshipIntentSchema>;
export type Turn = z.infer<typeof TurnSchema>;
export type GameConfig = z.infer<typeof GameConfigSchema>;
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({
+28 -8
View File
@@ -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 +
+6
View File
@@ -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(
+4
View File
@@ -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`);
}
@@ -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;
}
}
+2
View File
@@ -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;
+8
View File
@@ -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<PlayerID> = 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);