feat: 토너먼트 자동 시작 핸들러 추가 및 관련 로직 구현
This commit is contained in:
@@ -351,7 +351,6 @@ const sortByRanking = (entries: TournamentParticipantEntry[]): TournamentPartici
|
||||
};
|
||||
|
||||
const buildFinal16MatchesFromGroups = (
|
||||
state: TournamentState,
|
||||
participants: TournamentParticipantEntry[]
|
||||
): TournamentMatchEntry[] | null => {
|
||||
const groupOrder = [10, 14, 11, 15, 12, 16, 13, 17, 14, 10, 15, 11, 16, 12, 17, 13];
|
||||
@@ -935,7 +934,7 @@ const applyPreBattleStage = async (
|
||||
if (state.stage === 5) {
|
||||
const matches = await store.getMatches();
|
||||
if (matches.length === 0) {
|
||||
const fixedMatches = buildFinal16MatchesFromGroups(state, participants);
|
||||
const fixedMatches = buildFinal16MatchesFromGroups(participants);
|
||||
const participantIds = fixedMatches
|
||||
? fixedMatches.flatMap((entry) => [entry.attackerId, entry.defenderId])
|
||||
: pickFinalists(state, participants);
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { asRecord } from '@sammo-ts/common';
|
||||
import type { RedisConnector } from '@sammo-ts/infra';
|
||||
|
||||
import type { TurnCalendarHandler } from './inMemoryWorld.js';
|
||||
|
||||
interface TournamentState {
|
||||
stage: number;
|
||||
phase: number;
|
||||
type: number;
|
||||
auto: boolean;
|
||||
openYear: number;
|
||||
openMonth: number;
|
||||
termSeconds: number;
|
||||
nextAt: string;
|
||||
bettingId?: number;
|
||||
bettingCloseAt?: string;
|
||||
winnerId?: number;
|
||||
bettingSettled?: boolean;
|
||||
rewardSettled?: boolean;
|
||||
participantsLockedAt?: string;
|
||||
lastError?: string;
|
||||
lastErrorAt?: string;
|
||||
}
|
||||
|
||||
const safeJsonParse = <T>(raw: string | null): T | null => {
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(raw) as T;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const buildTournamentKeys = (profileName: string) => ({
|
||||
stateKey: `sammo:${profileName}:tournament:state`,
|
||||
participantsKey: `sammo:${profileName}:tournament:participants`,
|
||||
matchesKey: `sammo:${profileName}:tournament:matches`,
|
||||
bettingKey: `sammo:${profileName}:tournament:betting`,
|
||||
});
|
||||
|
||||
const resolveTermSeconds = (tickSeconds: number): number => {
|
||||
const turnMinutes = Math.max(1, Math.round(tickSeconds / 60));
|
||||
const clampedMinutes = Math.min(120, Math.max(5, turnMinutes));
|
||||
return clampedMinutes * 60;
|
||||
};
|
||||
|
||||
export const createTournamentAutoStartHandler = (options: {
|
||||
profileName: string;
|
||||
getRedisClient: () => RedisConnector['client'] | null | undefined;
|
||||
getWorldConfig: () => Record<string, unknown> | null | undefined;
|
||||
getTickSeconds: () => number | null | undefined;
|
||||
}): TurnCalendarHandler => {
|
||||
const profileName = options.profileName;
|
||||
const keys = buildTournamentKeys(profileName);
|
||||
const triggerAutoStart = async (currentYear: number, currentMonth: number): Promise<void> => {
|
||||
if (currentMonth % 2 !== 0) {
|
||||
return;
|
||||
}
|
||||
const config = asRecord(options.getWorldConfig() ?? {});
|
||||
if (config.tournamentTrig !== true) {
|
||||
return;
|
||||
}
|
||||
const redis = options.getRedisClient();
|
||||
if (!redis) {
|
||||
return;
|
||||
}
|
||||
|
||||
const state = safeJsonParse<TournamentState>(await redis.get(keys.stateKey));
|
||||
if (state && state.stage > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const tickSeconds = options.getTickSeconds() ?? 0;
|
||||
const termSeconds =
|
||||
state && Number.isFinite(state.termSeconds) && state.termSeconds > 0
|
||||
? state.termSeconds
|
||||
: resolveTermSeconds(tickSeconds);
|
||||
const nextState: TournamentState = {
|
||||
stage: 1,
|
||||
phase: 0,
|
||||
type: state && typeof state.type === 'number' ? state.type : 0,
|
||||
auto: true,
|
||||
openYear: currentYear,
|
||||
openMonth: currentMonth,
|
||||
termSeconds,
|
||||
nextAt: now,
|
||||
bettingId: undefined,
|
||||
bettingCloseAt: undefined,
|
||||
winnerId: undefined,
|
||||
bettingSettled: false,
|
||||
rewardSettled: false,
|
||||
participantsLockedAt: undefined,
|
||||
lastError: undefined,
|
||||
lastErrorAt: undefined,
|
||||
};
|
||||
|
||||
await redis.set(keys.participantsKey, '[]');
|
||||
await redis.set(keys.matchesKey, '[]');
|
||||
await redis.set(keys.bettingKey, '[]');
|
||||
await redis.set(keys.stateKey, JSON.stringify(nextState));
|
||||
};
|
||||
|
||||
return {
|
||||
onMonthChanged: (context) => {
|
||||
void triggerAutoStart(context.currentYear, context.currentMonth);
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -28,6 +28,7 @@ import { shouldUseAi } from './ai/generalAi.js';
|
||||
import { createUnificationHandler } from './unificationHandler.js';
|
||||
import { createAuctionFinalizer } from '../auction/finalizer.js';
|
||||
import { createTournamentRewardFinalizer } from '../tournament/finalizer.js';
|
||||
import { createTournamentAutoStartHandler } from './tournamentAutoStart.js';
|
||||
|
||||
export interface TurnDaemonRuntimeOptions {
|
||||
profile: string;
|
||||
@@ -105,6 +106,7 @@ export const createTurnDaemonRuntime = async (options: TurnDaemonRuntimeOptions)
|
||||
})
|
||||
: await loadTurnCommandProfile());
|
||||
let worldRef: InMemoryTurnWorld | null = null;
|
||||
let redisConnector: ReturnType<typeof createRedisConnector> | null = null;
|
||||
const nationTraits = await loadNationTraitModules([...NATION_TRAIT_KEYS], new NationTraitLoader());
|
||||
const nationTraitMap = new Map(nationTraits.map((module) => [module.key, module]));
|
||||
const unification = options.calendarHandler
|
||||
@@ -119,7 +121,17 @@ export const createTurnDaemonRuntime = async (options: TurnDaemonRuntimeOptions)
|
||||
scenarioConfig: snapshot.scenarioConfig,
|
||||
nationTraits: nationTraitMap,
|
||||
});
|
||||
const calendarHandler = composeCalendarHandlers(options.calendarHandler ?? unification?.handler, incomeHandler);
|
||||
const tournamentAutoStartHandler = createTournamentAutoStartHandler({
|
||||
profileName: options.profileName ?? options.profile,
|
||||
getRedisClient: () => redisConnector?.client,
|
||||
getWorldConfig: () => snapshot.worldConfig ?? null,
|
||||
getTickSeconds: () => worldRef?.getState().tickSeconds ?? null,
|
||||
});
|
||||
const calendarHandler = composeCalendarHandlers(
|
||||
options.calendarHandler ?? unification?.handler,
|
||||
incomeHandler,
|
||||
tournamentAutoStartHandler
|
||||
);
|
||||
const worldOptions: InMemoryTurnWorldOptions = {
|
||||
schedule,
|
||||
generalTurnHandler:
|
||||
@@ -175,7 +187,6 @@ export const createTurnDaemonRuntime = async (options: TurnDaemonRuntimeOptions)
|
||||
let auctionFinalizer: Awaited<ReturnType<typeof createAuctionFinalizer>> | null = null;
|
||||
let tournamentRewardFinalizer: Awaited<ReturnType<typeof createTournamentRewardFinalizer>> | null = null;
|
||||
let redisCommandStream: RedisTurnDaemonCommandStream | null = null;
|
||||
let redisConnector: ReturnType<typeof createRedisConnector> | null = null;
|
||||
let pauseGate: (() => Promise<boolean>) | undefined;
|
||||
let adminActionConsumer: Awaited<ReturnType<typeof createGatewayAdminActionConsumer>> | null = null;
|
||||
const gatewayGate = options.profileName
|
||||
|
||||
@@ -39,6 +39,7 @@ export interface TurnWorldSnapshot extends Omit<
|
||||
> {
|
||||
scenarioConfig: ScenarioConfig;
|
||||
scenarioMeta?: ScenarioMeta;
|
||||
worldConfig?: Record<string, unknown>;
|
||||
map: MapDefinition;
|
||||
unitSet?: UnitSetDefinition;
|
||||
diplomacy: TurnDiplomacy[];
|
||||
|
||||
@@ -258,6 +258,7 @@ export const loadTurnWorldFromDatabase = async (options: TurnWorldLoaderOptions)
|
||||
const diplomacy = diplomacyRows.map(mapDiplomacyRow);
|
||||
const troops = troopRows.map(mapTroopRow);
|
||||
|
||||
const worldConfig = asRecord(worldState.config);
|
||||
const scenarioConfig = mapScenarioConfig(worldState.config);
|
||||
const mapName = scenarioConfig.environment?.mapName ?? 'che';
|
||||
const map = await loadMapDefinitionByName(mapName, options.mapOptions);
|
||||
@@ -304,6 +305,7 @@ export const loadTurnWorldFromDatabase = async (options: TurnWorldLoaderOptions)
|
||||
snapshot: {
|
||||
scenarioConfig,
|
||||
...(scenarioMeta ? { scenarioMeta } : {}),
|
||||
worldConfig,
|
||||
map,
|
||||
unitSet,
|
||||
nations,
|
||||
|
||||
Reference in New Issue
Block a user