feat: 토너먼트 자동 시작 핸들러 개선 및 관련 기능 추가

This commit is contained in:
2026-01-23 18:58:05 +00:00
parent 7a2b4c3911
commit 6e439f074d
5 changed files with 152 additions and 149 deletions
+1
View File
@@ -11,6 +11,7 @@ export * from './util/TestRNG.js';
export * from './util/TournamentRNG.js';
export * from './util/sha512.js';
export * from './util/parse.js';
export * from './tournament/autoStart.js';
export * from './turnDaemon/types.js';
export * from './realtime/keys.js';
export * from './realtime/types.js';
+113
View File
@@ -0,0 +1,113 @@
import { asRecord } from '../util/parse.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;
}
interface RedisClientLike {
get(key: string): Promise<string | null>;
set(key: string, value: string): Promise<unknown>;
}
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: () => RedisClientLike | null | undefined;
getWorldConfig: () => Record<string, unknown> | null | undefined;
getTickSeconds: () => number | null | undefined;
}): { onMonthChanged: (context: { currentYear: number; currentMonth: number }) => void } => {
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);
},
};
};