feat: 토너먼트 보상 처리 기능 추가 및 관련 타입 정의
This commit is contained in:
@@ -36,6 +36,7 @@ const zTournamentState = z.object({
|
|||||||
bettingCloseAt: z.string().optional(),
|
bettingCloseAt: z.string().optional(),
|
||||||
winnerId: z.number().int().optional(),
|
winnerId: z.number().int().optional(),
|
||||||
bettingSettled: z.boolean().optional(),
|
bettingSettled: z.boolean().optional(),
|
||||||
|
rewardSettled: z.boolean().optional(),
|
||||||
lastError: z.string().optional(),
|
lastError: z.string().optional(),
|
||||||
lastErrorAt: z.string().optional(),
|
lastErrorAt: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export interface TournamentState {
|
|||||||
bettingCloseAt?: string;
|
bettingCloseAt?: string;
|
||||||
winnerId?: number;
|
winnerId?: number;
|
||||||
bettingSettled?: boolean;
|
bettingSettled?: boolean;
|
||||||
|
rewardSettled?: boolean;
|
||||||
lastError?: string;
|
lastError?: string;
|
||||||
lastErrorAt?: string;
|
lastErrorAt?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -268,6 +268,42 @@ const buildBettingPayouts = (
|
|||||||
return { payouts, total, refundAll: false };
|
return { payouts, total, refundAll: false };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const buildTournamentRewardPayload = (
|
||||||
|
matches: TournamentMatchEntry[]
|
||||||
|
): { top16: number[]; top8: number[]; top4: number[]; winnerId: number; runnerUpId: number } => {
|
||||||
|
const top16 = new Set<number>();
|
||||||
|
const top8 = new Set<number>();
|
||||||
|
const top4 = new Set<number>();
|
||||||
|
|
||||||
|
for (const match of matches) {
|
||||||
|
if (match.stage === 7) {
|
||||||
|
top16.add(match.attackerId);
|
||||||
|
top16.add(match.defenderId);
|
||||||
|
if (typeof match.winnerId === 'number') {
|
||||||
|
top8.add(match.winnerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (match.stage === 8 && typeof match.winnerId === 'number') {
|
||||||
|
top4.add(match.winnerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalMatch = matches.find((match) => match.stage === 10 && typeof match.winnerId === 'number');
|
||||||
|
if (!finalMatch || typeof finalMatch.winnerId !== 'number') {
|
||||||
|
throw new Error('결승전 결과를 찾을 수 없습니다.');
|
||||||
|
}
|
||||||
|
const winnerId = finalMatch.winnerId;
|
||||||
|
const runnerUpId = finalMatch.attackerId === winnerId ? finalMatch.defenderId : finalMatch.attackerId;
|
||||||
|
|
||||||
|
return {
|
||||||
|
top16: Array.from(top16),
|
||||||
|
top8: Array.from(top8),
|
||||||
|
top4: Array.from(top4),
|
||||||
|
winnerId,
|
||||||
|
runnerUpId,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const applyPreBattleStage = async (
|
const applyPreBattleStage = async (
|
||||||
store: TournamentStore,
|
store: TournamentStore,
|
||||||
state: TournamentState,
|
state: TournamentState,
|
||||||
@@ -481,39 +517,59 @@ export const runTournamentWorker = async (): Promise<void> => {
|
|||||||
nextState = await applyPreBattleStage(store, state, String(baseSeed));
|
nextState = await applyPreBattleStage(store, state, String(baseSeed));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (nextState.stage === 0 && nextState.winnerId) {
|
||||||
nextState.stage === 0 &&
|
let settledState: TournamentState | null = null;
|
||||||
nextState.winnerId &&
|
|
||||||
nextState.bettingId &&
|
if (!nextState.rewardSettled) {
|
||||||
!nextState.bettingSettled
|
const matches = await store.getMatches();
|
||||||
) {
|
const rewardPayload = buildTournamentRewardPayload(matches);
|
||||||
const bettingEntries = await store.getBettingEntries();
|
await daemonTransport.sendCommand({
|
||||||
if (bettingEntries.length > 0) {
|
type: 'tournamentReward',
|
||||||
const payoutInfo = buildBettingPayouts(nextState.winnerId, bettingEntries);
|
tournamentType: nextState.type,
|
||||||
if (payoutInfo.payouts.length > 0) {
|
winnerId: rewardPayload.winnerId,
|
||||||
if (payoutInfo.refundAll) {
|
runnerUpId: rewardPayload.runnerUpId,
|
||||||
await daemonTransport.sendCommand({
|
top16: rewardPayload.top16,
|
||||||
type: 'tournamentRefund',
|
top8: rewardPayload.top8,
|
||||||
bettingId: nextState.bettingId,
|
top4: rewardPayload.top4,
|
||||||
refunds: payoutInfo.payouts,
|
});
|
||||||
reason: 'no_winner',
|
settledState = {
|
||||||
});
|
...(settledState ?? nextState),
|
||||||
} else {
|
rewardSettled: true,
|
||||||
await daemonTransport.sendCommand({
|
};
|
||||||
type: 'tournamentBettingPayout',
|
|
||||||
bettingId: nextState.bettingId,
|
|
||||||
payouts: payoutInfo.payouts,
|
|
||||||
reason: 'winner_payout',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const settledState: TournamentState = {
|
if (nextState.bettingId && !nextState.bettingSettled) {
|
||||||
...nextState,
|
const bettingEntries = await store.getBettingEntries();
|
||||||
bettingSettled: true,
|
if (bettingEntries.length > 0) {
|
||||||
};
|
const payoutInfo = buildBettingPayouts(nextState.winnerId, bettingEntries);
|
||||||
await store.setState(settledState);
|
if (payoutInfo.payouts.length > 0) {
|
||||||
|
if (payoutInfo.refundAll) {
|
||||||
|
await daemonTransport.sendCommand({
|
||||||
|
type: 'tournamentRefund',
|
||||||
|
bettingId: nextState.bettingId,
|
||||||
|
refunds: payoutInfo.payouts,
|
||||||
|
reason: 'no_winner',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await daemonTransport.sendCommand({
|
||||||
|
type: 'tournamentBettingPayout',
|
||||||
|
bettingId: nextState.bettingId,
|
||||||
|
payouts: payoutInfo.payouts,
|
||||||
|
reason: 'winner_payout',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
settledState = {
|
||||||
|
...(settledState ?? nextState),
|
||||||
|
bettingSettled: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settledState) {
|
||||||
|
await store.setState(settledState);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
|||||||
@@ -256,6 +256,32 @@ const normalizeCommand = (envelope: TurnDaemonCommandEnvelope): TurnDaemonComman
|
|||||||
payouts,
|
payouts,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case 'tournamentReward': {
|
||||||
|
if (typeof command.winnerId !== 'number' || typeof command.runnerUpId !== 'number') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!Array.isArray(command.top16) || !Array.isArray(command.top8) || !Array.isArray(command.top4)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const normalizeIds = (list: unknown[]): number[] =>
|
||||||
|
list.filter((entry): entry is number => typeof entry === 'number' && Number.isFinite(entry));
|
||||||
|
const top16 = normalizeIds(command.top16);
|
||||||
|
const top8 = normalizeIds(command.top8);
|
||||||
|
const top4 = normalizeIds(command.top4);
|
||||||
|
if (top16.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: 'tournamentReward',
|
||||||
|
requestId: envelope.requestId,
|
||||||
|
tournamentType: typeof command.tournamentType === 'number' ? command.tournamentType : 0,
|
||||||
|
winnerId: command.winnerId,
|
||||||
|
runnerUpId: command.runnerUpId,
|
||||||
|
top16,
|
||||||
|
top8,
|
||||||
|
top4,
|
||||||
|
};
|
||||||
|
}
|
||||||
case 'getStatus': {
|
case 'getStatus': {
|
||||||
const requestId = typeof command.requestId === 'string' ? command.requestId : envelope.requestId;
|
const requestId = typeof command.requestId === 'string' ? command.requestId : envelope.requestId;
|
||||||
return { type: 'getStatus', requestId };
|
return { type: 'getStatus', requestId };
|
||||||
|
|||||||
@@ -0,0 +1,229 @@
|
|||||||
|
import { JosaUtil, asRecord } from '@sammo-ts/common';
|
||||||
|
import { createGamePostgresConnector } from '@sammo-ts/infra';
|
||||||
|
import { ActionLogger, LogFormat, type TournamentType } from '@sammo-ts/logic';
|
||||||
|
|
||||||
|
import type { TurnDaemonCommand, TurnDaemonCommandResult, TurnDaemonHooks } from '../lifecycle/types.js';
|
||||||
|
import type { InMemoryTurnWorld } from '../turn/inMemoryWorld.js';
|
||||||
|
|
||||||
|
export interface TournamentRewardFinalizer {
|
||||||
|
finalize(command: Extract<TurnDaemonCommand, { type: 'tournamentReward' }>): Promise<TurnDaemonCommandResult>;
|
||||||
|
close(): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveTournamentLabel = (type: TournamentType): string => {
|
||||||
|
switch (type) {
|
||||||
|
case 1:
|
||||||
|
return '통솔전';
|
||||||
|
case 2:
|
||||||
|
return '일기토';
|
||||||
|
case 3:
|
||||||
|
return '설전';
|
||||||
|
case 0:
|
||||||
|
default:
|
||||||
|
return '전력전';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveNumber = (source: Record<string, unknown>, keys: string[], fallback: number): number => {
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = source[key];
|
||||||
|
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pushLogs = (world: InMemoryTurnWorld, logs: ReturnType<ActionLogger['flush']>): void => {
|
||||||
|
if (logs.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const entry of logs) {
|
||||||
|
world.pushLog(entry);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const flushWorld = async (world: InMemoryTurnWorld, hooks?: TurnDaemonHooks): Promise<void> => {
|
||||||
|
if (!hooks?.flushChanges) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const state = world.getState();
|
||||||
|
await hooks.flushChanges({
|
||||||
|
lastTurnTime: state.lastTurnTime.toISOString(),
|
||||||
|
processedGenerals: 0,
|
||||||
|
processedTurns: 0,
|
||||||
|
durationMs: 0,
|
||||||
|
partial: false,
|
||||||
|
checkpoint: world.getCheckpoint(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createTournamentRewardFinalizer = async (options: {
|
||||||
|
databaseUrl: string;
|
||||||
|
world: InMemoryTurnWorld;
|
||||||
|
hooks?: TurnDaemonHooks;
|
||||||
|
}): Promise<TournamentRewardFinalizer> => {
|
||||||
|
const connector = createGamePostgresConnector({ url: options.databaseUrl });
|
||||||
|
await connector.connect();
|
||||||
|
const prisma = connector.prisma;
|
||||||
|
|
||||||
|
const finalize = async (
|
||||||
|
command: Extract<TurnDaemonCommand, { type: 'tournamentReward' }>
|
||||||
|
): Promise<TurnDaemonCommandResult> => {
|
||||||
|
const { world, hooks } = options;
|
||||||
|
const { winnerId, runnerUpId } = command;
|
||||||
|
const rewardMap = new Map<
|
||||||
|
number,
|
||||||
|
{ gold: number; exp: number; label: string; inheritPoint: number }
|
||||||
|
>();
|
||||||
|
|
||||||
|
const applyTier = (
|
||||||
|
ids: number[],
|
||||||
|
tier: { gold: number; exp: number; label: string; inheritPoint: number }
|
||||||
|
): void => {
|
||||||
|
for (const id of new Set(ids)) {
|
||||||
|
const current = rewardMap.get(id) ?? { gold: 0, exp: 0, label: tier.label, inheritPoint: 0 };
|
||||||
|
rewardMap.set(id, {
|
||||||
|
gold: current.gold + tier.gold,
|
||||||
|
exp: current.exp + tier.exp,
|
||||||
|
label: tier.label,
|
||||||
|
inheritPoint: tier.inheritPoint > 0 ? tier.inheritPoint : current.inheritPoint,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const constValues = asRecord(world.getScenarioConfig().const ?? {});
|
||||||
|
const develCost = resolveNumber(constValues, ['develCost', 'develcost', 'develrate'], 0);
|
||||||
|
|
||||||
|
applyTier(command.top16, { gold: develCost, exp: 25, label: '16강 진출', inheritPoint: 10 });
|
||||||
|
applyTier(command.top8, { gold: develCost * 2, exp: 50, label: '8강 진출', inheritPoint: 0 });
|
||||||
|
applyTier(command.top4, { gold: develCost * 3, exp: 50, label: '4강 진출', inheritPoint: 10 });
|
||||||
|
applyTier([runnerUpId], { gold: develCost * 6, exp: 100, label: '준우승', inheritPoint: 50 });
|
||||||
|
applyTier([winnerId], { gold: develCost * 8, exp: 200, label: '우승', inheritPoint: 100 });
|
||||||
|
|
||||||
|
if (rewardMap.size === 0) {
|
||||||
|
return {
|
||||||
|
type: 'tournamentReward',
|
||||||
|
ok: false,
|
||||||
|
winnerId,
|
||||||
|
runnerUpId,
|
||||||
|
reason: '보상 대상이 없습니다.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const nameMap = new Map<number, string>();
|
||||||
|
const generals = await prisma.general.findMany({
|
||||||
|
where: { id: { in: Array.from(rewardMap.keys()) } },
|
||||||
|
select: { id: true, userId: true, name: true },
|
||||||
|
});
|
||||||
|
const userMap = new Map<number, string>();
|
||||||
|
for (const general of generals) {
|
||||||
|
nameMap.set(general.id, general.name);
|
||||||
|
if (general.userId) {
|
||||||
|
userMap.set(general.id, general.userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tournamentLabel = resolveTournamentLabel(command.tournamentType as TournamentType);
|
||||||
|
const logs: ReturnType<ActionLogger['flush']> = [];
|
||||||
|
let rewarded = 0;
|
||||||
|
let missing = 0;
|
||||||
|
let totalGold = 0;
|
||||||
|
let totalExp = 0;
|
||||||
|
|
||||||
|
for (const [generalId, reward] of rewardMap) {
|
||||||
|
const general = world.getGeneralById(generalId);
|
||||||
|
if (!general) {
|
||||||
|
missing += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
world.updateGeneral(generalId, {
|
||||||
|
gold: general.gold + reward.gold,
|
||||||
|
experience: general.experience + reward.exp,
|
||||||
|
});
|
||||||
|
totalGold += reward.gold;
|
||||||
|
totalExp += reward.exp;
|
||||||
|
rewarded += 1;
|
||||||
|
|
||||||
|
const rewardText = reward.gold.toLocaleString('ko-KR');
|
||||||
|
const logger = new ActionLogger({ generalId, nationId: general.nationId });
|
||||||
|
logger.pushGeneralActionLog(
|
||||||
|
`<C>${tournamentLabel}</> 대회의 ${reward.label}로 <C>${rewardText}</>의 <S>상금</>, 약간의 <S>명성</> 획득!`,
|
||||||
|
LogFormat.PLAIN
|
||||||
|
);
|
||||||
|
logs.push(...logger.flush());
|
||||||
|
}
|
||||||
|
|
||||||
|
const winnerName = nameMap.get(winnerId);
|
||||||
|
const runnerUpName = nameMap.get(runnerUpId);
|
||||||
|
const winnerReward = rewardMap.get(winnerId)?.gold ?? 0;
|
||||||
|
const runnerUpReward = rewardMap.get(runnerUpId)?.gold ?? 0;
|
||||||
|
if (winnerName) {
|
||||||
|
const winnerLogger = new ActionLogger({ generalId: winnerId });
|
||||||
|
winnerLogger.pushGeneralHistoryLog(`<C>${tournamentLabel}</> 대회에서 우승`);
|
||||||
|
logs.push(...winnerLogger.flush());
|
||||||
|
}
|
||||||
|
if (runnerUpName) {
|
||||||
|
const runnerLogger = new ActionLogger({ generalId: runnerUpId });
|
||||||
|
runnerLogger.pushGeneralHistoryLog(`<C>${tournamentLabel}</> 대회에서 준우승`);
|
||||||
|
logs.push(...runnerLogger.flush());
|
||||||
|
}
|
||||||
|
if (winnerName && runnerUpName) {
|
||||||
|
const globalLogger = new ActionLogger();
|
||||||
|
const josaWinner = JosaUtil.pick(winnerName, '이');
|
||||||
|
const josaRunner = JosaUtil.pick(runnerUpName, '이');
|
||||||
|
const winnerRewardText = winnerReward.toLocaleString('ko-KR');
|
||||||
|
const runnerRewardText = runnerUpReward.toLocaleString('ko-KR');
|
||||||
|
globalLogger.pushGlobalHistoryLog(
|
||||||
|
`<B><b>【대회】</b></><C>${tournamentLabel}</> 대회에서 <Y>${winnerName}</>${josaWinner} <C>우승</>, <Y>${runnerUpName}</>${josaRunner} <C>준우승</>을 차지하여 천하에 이름을 떨칩니다!`,
|
||||||
|
LogFormat.YEAR_MONTH
|
||||||
|
);
|
||||||
|
globalLogger.pushGlobalHistoryLog(
|
||||||
|
`<B><b>【대회】</b></><C>${tournamentLabel}</> 대회의 <S>우승자</>에게는 <C>${winnerRewardText}</>, <S>준우승자</>에겐 <C>${runnerRewardText}</>의 <S>상금</>과 약간의 <S>명성</>이 주어집니다!`,
|
||||||
|
LogFormat.YEAR_MONTH
|
||||||
|
);
|
||||||
|
logs.push(...globalLogger.flush());
|
||||||
|
}
|
||||||
|
|
||||||
|
pushLogs(world, logs);
|
||||||
|
|
||||||
|
const pointUpdates = Array.from(rewardMap.entries())
|
||||||
|
.filter(([, reward]) => reward.inheritPoint > 0)
|
||||||
|
.map(([generalId, reward]) => ({
|
||||||
|
generalId,
|
||||||
|
userId: userMap.get(generalId),
|
||||||
|
value: reward.inheritPoint,
|
||||||
|
}))
|
||||||
|
.filter((entry) => !!entry.userId);
|
||||||
|
|
||||||
|
for (const entry of pointUpdates) {
|
||||||
|
await prisma.inheritancePoint.upsert({
|
||||||
|
where: {
|
||||||
|
userId_key: { userId: entry.userId!, key: 'tournament' },
|
||||||
|
},
|
||||||
|
update: { value: { increment: entry.value } },
|
||||||
|
create: { userId: entry.userId!, key: 'tournament', value: entry.value },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await flushWorld(world, hooks);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'tournamentReward',
|
||||||
|
ok: true,
|
||||||
|
winnerId,
|
||||||
|
runnerUpId,
|
||||||
|
rewarded,
|
||||||
|
missing,
|
||||||
|
totalGold,
|
||||||
|
totalExp,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
finalize,
|
||||||
|
close: async () => {
|
||||||
|
await connector.disconnect();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -27,6 +27,7 @@ import { loadTurnWorldFromDatabase } from './worldLoader.js';
|
|||||||
import { shouldUseAi } from './ai/generalAi.js';
|
import { shouldUseAi } from './ai/generalAi.js';
|
||||||
import { createUnificationHandler } from './unificationHandler.js';
|
import { createUnificationHandler } from './unificationHandler.js';
|
||||||
import { createAuctionFinalizer } from '../auction/finalizer.js';
|
import { createAuctionFinalizer } from '../auction/finalizer.js';
|
||||||
|
import { createTournamentRewardFinalizer } from '../tournament/finalizer.js';
|
||||||
|
|
||||||
export interface TurnDaemonRuntimeOptions {
|
export interface TurnDaemonRuntimeOptions {
|
||||||
profile: string;
|
profile: string;
|
||||||
@@ -172,6 +173,7 @@ export const createTurnDaemonRuntime = async (options: TurnDaemonRuntimeOptions)
|
|||||||
let publishRealtimeEvent: ((event: RealtimeEvent) => Promise<void>) | null = null;
|
let publishRealtimeEvent: ((event: RealtimeEvent) => Promise<void>) | null = null;
|
||||||
let close = async () => {};
|
let close = async () => {};
|
||||||
let auctionFinalizer: Awaited<ReturnType<typeof createAuctionFinalizer>> | null = null;
|
let auctionFinalizer: Awaited<ReturnType<typeof createAuctionFinalizer>> | null = null;
|
||||||
|
let tournamentRewardFinalizer: Awaited<ReturnType<typeof createTournamentRewardFinalizer>> | null = null;
|
||||||
let redisCommandStream: RedisTurnDaemonCommandStream | null = null;
|
let redisCommandStream: RedisTurnDaemonCommandStream | null = null;
|
||||||
let redisConnector: ReturnType<typeof createRedisConnector> | null = null;
|
let redisConnector: ReturnType<typeof createRedisConnector> | null = null;
|
||||||
let pauseGate: (() => Promise<boolean>) | undefined;
|
let pauseGate: (() => Promise<boolean>) | undefined;
|
||||||
@@ -196,6 +198,11 @@ export const createTurnDaemonRuntime = async (options: TurnDaemonRuntimeOptions)
|
|||||||
world,
|
world,
|
||||||
hooks: dbHooks.hooks,
|
hooks: dbHooks.hooks,
|
||||||
});
|
});
|
||||||
|
tournamentRewardFinalizer = await createTournamentRewardFinalizer({
|
||||||
|
databaseUrl: options.databaseUrl,
|
||||||
|
world,
|
||||||
|
hooks: dbHooks.hooks,
|
||||||
|
});
|
||||||
hooks = {
|
hooks = {
|
||||||
...dbHooks.hooks,
|
...dbHooks.hooks,
|
||||||
onRunError: async (error) => {
|
onRunError: async (error) => {
|
||||||
@@ -207,6 +214,9 @@ export const createTurnDaemonRuntime = async (options: TurnDaemonRuntimeOptions)
|
|||||||
if (auctionFinalizer) {
|
if (auctionFinalizer) {
|
||||||
await auctionFinalizer.close();
|
await auctionFinalizer.close();
|
||||||
}
|
}
|
||||||
|
if (tournamentRewardFinalizer) {
|
||||||
|
await tournamentRewardFinalizer.close();
|
||||||
|
}
|
||||||
await dbHooks.close();
|
await dbHooks.close();
|
||||||
if (reservedTurnStoreHandle) {
|
if (reservedTurnStoreHandle) {
|
||||||
await reservedTurnStoreHandle.close();
|
await reservedTurnStoreHandle.close();
|
||||||
@@ -292,6 +302,7 @@ export const createTurnDaemonRuntime = async (options: TurnDaemonRuntimeOptions)
|
|||||||
world,
|
world,
|
||||||
hooks,
|
hooks,
|
||||||
auctionFinalizer: auctionFinalizer ?? undefined,
|
auctionFinalizer: auctionFinalizer ?? undefined,
|
||||||
|
tournamentRewardFinalizer: tournamentRewardFinalizer ?? undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultBudget: TurnRunBudget = options.defaultBudget ?? {
|
const defaultBudget: TurnRunBudget = options.defaultBudget ?? {
|
||||||
|
|||||||
@@ -30,12 +30,17 @@ interface CommandHandlerContext {
|
|||||||
world: InMemoryTurnWorld;
|
world: InMemoryTurnWorld;
|
||||||
hooks?: TurnDaemonHooks;
|
hooks?: TurnDaemonHooks;
|
||||||
auctionFinalizer?: AuctionFinalizer;
|
auctionFinalizer?: AuctionFinalizer;
|
||||||
|
tournamentRewardFinalizer?: TournamentRewardFinalizer;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AuctionFinalizer {
|
interface AuctionFinalizer {
|
||||||
finalize(auctionId: number): Promise<TurnDaemonCommandResult>;
|
finalize(auctionId: number): Promise<TurnDaemonCommandResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TournamentRewardFinalizer {
|
||||||
|
finalize(command: Extract<TurnDaemonCommand, { type: 'tournamentReward' }>): Promise<TurnDaemonCommandResult>;
|
||||||
|
}
|
||||||
|
|
||||||
async function handleTroopJoin(
|
async function handleTroopJoin(
|
||||||
ctx: CommandHandlerContext,
|
ctx: CommandHandlerContext,
|
||||||
command: Extract<TurnDaemonCommand, { type: 'troopJoin' }>
|
command: Extract<TurnDaemonCommand, { type: 'troopJoin' }>
|
||||||
@@ -547,12 +552,34 @@ async function handleTournamentBettingPayout(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleTournamentReward(
|
||||||
|
ctx: CommandHandlerContext,
|
||||||
|
command: Extract<TurnDaemonCommand, { type: 'tournamentReward' }>
|
||||||
|
): Promise<TurnDaemonCommandResult> {
|
||||||
|
if (!ctx.tournamentRewardFinalizer) {
|
||||||
|
return {
|
||||||
|
type: 'tournamentReward',
|
||||||
|
ok: false,
|
||||||
|
winnerId: command.winnerId,
|
||||||
|
runnerUpId: command.runnerUpId,
|
||||||
|
reason: '보상 처리기가 준비되지 않았습니다.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return ctx.tournamentRewardFinalizer.finalize(command);
|
||||||
|
}
|
||||||
|
|
||||||
export const createTurnDaemonCommandHandler = (options: {
|
export const createTurnDaemonCommandHandler = (options: {
|
||||||
world: InMemoryTurnWorld;
|
world: InMemoryTurnWorld;
|
||||||
hooks?: TurnDaemonHooks;
|
hooks?: TurnDaemonHooks;
|
||||||
auctionFinalizer?: AuctionFinalizer;
|
auctionFinalizer?: AuctionFinalizer;
|
||||||
|
tournamentRewardFinalizer?: TournamentRewardFinalizer;
|
||||||
}): TurnDaemonCommandHandler => {
|
}): TurnDaemonCommandHandler => {
|
||||||
const ctx = { world: options.world, hooks: options.hooks, auctionFinalizer: options.auctionFinalizer };
|
const ctx = {
|
||||||
|
world: options.world,
|
||||||
|
hooks: options.hooks,
|
||||||
|
auctionFinalizer: options.auctionFinalizer,
|
||||||
|
tournamentRewardFinalizer: options.tournamentRewardFinalizer,
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handle: async (command): Promise<TurnDaemonCommandResult | null> => {
|
handle: async (command): Promise<TurnDaemonCommandResult | null> => {
|
||||||
@@ -585,6 +612,8 @@ export const createTurnDaemonCommandHandler = (options: {
|
|||||||
return handleTournamentRefund(ctx, command);
|
return handleTournamentRefund(ctx, command);
|
||||||
case 'tournamentBettingPayout':
|
case 'tournamentBettingPayout':
|
||||||
return handleTournamentBettingPayout(ctx, command);
|
return handleTournamentBettingPayout(ctx, command);
|
||||||
|
case 'tournamentReward':
|
||||||
|
return handleTournamentReward(ctx, command);
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,6 +105,16 @@ export type TurnDaemonCommand =
|
|||||||
generalId: number;
|
generalId: number;
|
||||||
amount: number;
|
amount: number;
|
||||||
}>;
|
}>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'tournamentReward';
|
||||||
|
requestId?: string;
|
||||||
|
tournamentType: number;
|
||||||
|
winnerId: number;
|
||||||
|
runnerUpId: number;
|
||||||
|
top16: number[];
|
||||||
|
top8: number[];
|
||||||
|
top4: number[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TurnDaemonCommandResult =
|
export type TurnDaemonCommandResult =
|
||||||
@@ -166,21 +176,38 @@ export type TurnDaemonCommandResult =
|
|||||||
ok: false;
|
ok: false;
|
||||||
bettingId?: number;
|
bettingId?: number;
|
||||||
reason: string;
|
reason: string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'tournamentBettingPayout';
|
type: 'tournamentBettingPayout';
|
||||||
ok: true;
|
ok: true;
|
||||||
bettingId?: number;
|
bettingId?: number;
|
||||||
processed: number;
|
processed: number;
|
||||||
missing: number;
|
missing: number;
|
||||||
totalPayout: number;
|
totalPayout: number;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'tournamentBettingPayout';
|
type: 'tournamentBettingPayout';
|
||||||
ok: false;
|
ok: false;
|
||||||
bettingId?: number;
|
bettingId?: number;
|
||||||
reason: string;
|
reason: string;
|
||||||
};
|
}
|
||||||
|
| {
|
||||||
|
type: 'tournamentReward';
|
||||||
|
ok: true;
|
||||||
|
winnerId: number;
|
||||||
|
runnerUpId: number;
|
||||||
|
rewarded: number;
|
||||||
|
missing: number;
|
||||||
|
totalGold: number;
|
||||||
|
totalExp: number;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'tournamentReward';
|
||||||
|
ok: false;
|
||||||
|
winnerId: number;
|
||||||
|
runnerUpId: number;
|
||||||
|
reason: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TurnDaemonEvent =
|
export type TurnDaemonEvent =
|
||||||
| { type: 'status'; requestId?: string; status: TurnDaemonStatus }
|
| { type: 'status'; requestId?: string; status: TurnDaemonStatus }
|
||||||
|
|||||||
Reference in New Issue
Block a user