feat(battle-sim): implement battle simulation logic and metrics tracking
- Added WarBattleMetrics interface to track attacker and defender skills during battles. - Enhanced WarUnit class with method to log activated skills. - Introduced environment setup for battle simulation, including unit set loading and configuration resolution. - Developed in-memory and Redis transport layers for handling battle simulation requests and results. - Created types and interfaces for battle simulation payloads and responses. - Implemented battle simulation processing logic, including logging and skill tracking. - Added tests for battle simulation processor to ensure functionality and correctness.
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
import type { BattleSimJobPayload, BattleSimResultPayload, BattleSimTransportResponse } from './types.js';
|
||||
import type { BattleSimQueueKeys } from './keys.js';
|
||||
|
||||
type RedisBlPopResult =
|
||||
| { key: string; element: string }
|
||||
| [string, string]
|
||||
| null;
|
||||
|
||||
interface RedisClientLike {
|
||||
rPush(key: string, value: string): Promise<number>;
|
||||
blPop(key: string, timeout: number): Promise<RedisBlPopResult>;
|
||||
set(key: string, value: string, options?: { EX?: number }): Promise<string | null>;
|
||||
get(key: string): Promise<string | null>;
|
||||
expire(key: string, seconds: number): Promise<number>;
|
||||
}
|
||||
|
||||
export interface RedisBattleSimTransportOptions {
|
||||
keys: BattleSimQueueKeys;
|
||||
requestTimeoutMs: number;
|
||||
resultTtlSeconds: number;
|
||||
}
|
||||
|
||||
const toTimeoutSeconds = (timeoutMs: number): number =>
|
||||
Math.max(1, Math.ceil(timeoutMs / 1000));
|
||||
|
||||
const parseBlPopValue = (result: RedisBlPopResult): string | null => {
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
if (Array.isArray(result)) {
|
||||
return result[1] ?? null;
|
||||
}
|
||||
return result.element ?? null;
|
||||
};
|
||||
|
||||
export class RedisBattleSimTransport {
|
||||
private readonly client: RedisClientLike;
|
||||
private readonly keys: BattleSimQueueKeys;
|
||||
private readonly requestTimeoutMs: number;
|
||||
private readonly resultTtlSeconds: number;
|
||||
|
||||
constructor(client: RedisClientLike, options: RedisBattleSimTransportOptions) {
|
||||
this.client = client;
|
||||
this.keys = options.keys;
|
||||
this.requestTimeoutMs = options.requestTimeoutMs;
|
||||
this.resultTtlSeconds = options.resultTtlSeconds;
|
||||
}
|
||||
|
||||
private buildResultKey(jobId: string): string {
|
||||
return `${this.keys.resultKeyPrefix}${jobId}`;
|
||||
}
|
||||
|
||||
private buildNotifyKey(jobId: string): string {
|
||||
return `${this.keys.notifyKeyPrefix}${jobId}`;
|
||||
}
|
||||
|
||||
private async readResult(jobId: string): Promise<BattleSimResultPayload | null> {
|
||||
const raw = await this.client.get(this.buildResultKey(jobId));
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(raw) as BattleSimResultPayload;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async waitForResult(jobId: string, timeoutMs: number): Promise<BattleSimResultPayload | null> {
|
||||
const existing = await this.readResult(jobId);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const notifyKey = this.buildNotifyKey(jobId);
|
||||
const timeoutSec = toTimeoutSeconds(timeoutMs);
|
||||
const signal = await this.client.blPop(notifyKey, timeoutSec);
|
||||
if (!parseBlPopValue(signal)) {
|
||||
return null;
|
||||
}
|
||||
return this.readResult(jobId);
|
||||
}
|
||||
|
||||
public async simulate(payload: BattleSimJobPayload): Promise<BattleSimTransportResponse> {
|
||||
const jobId = crypto.randomUUID();
|
||||
const job = {
|
||||
jobId,
|
||||
requestedAt: new Date().toISOString(),
|
||||
payload,
|
||||
};
|
||||
await this.client.rPush(this.keys.queueKey, JSON.stringify(job));
|
||||
|
||||
const result = await this.waitForResult(jobId, this.requestTimeoutMs);
|
||||
if (result) {
|
||||
return { status: 'completed', jobId, payload: result };
|
||||
}
|
||||
return { status: 'queued', jobId };
|
||||
}
|
||||
|
||||
public async getSimulationResult(jobId: string): Promise<BattleSimResultPayload | null> {
|
||||
return this.readResult(jobId);
|
||||
}
|
||||
|
||||
public async pushResult(jobId: string, payload: BattleSimResultPayload): Promise<void> {
|
||||
const resultKey = this.buildResultKey(jobId);
|
||||
const notifyKey = this.buildNotifyKey(jobId);
|
||||
await this.client.set(resultKey, JSON.stringify(payload), {
|
||||
EX: this.resultTtlSeconds,
|
||||
});
|
||||
await this.client.rPush(notifyKey, 'ready');
|
||||
await this.client.expire(notifyKey, this.resultTtlSeconds);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user