feat: 토너먼트 관련 기능 추가 및 베팅 처리 로직 구현
This commit is contained in:
@@ -16,6 +16,7 @@ import { turnDaemonRouter } from './router/turnDaemon/index.js';
|
|||||||
import { turnsRouter } from './router/turns/index.js';
|
import { turnsRouter } from './router/turns/index.js';
|
||||||
import { worldRouter } from './router/world/index.js';
|
import { worldRouter } from './router/world/index.js';
|
||||||
import { auctionRouter } from './router/auction/index.js';
|
import { auctionRouter } from './router/auction/index.js';
|
||||||
|
import { tournamentRouter } from './router/tournament/index.js';
|
||||||
|
|
||||||
export const appRouter = router({
|
export const appRouter = router({
|
||||||
health: healthRouter,
|
health: healthRouter,
|
||||||
@@ -34,6 +35,7 @@ export const appRouter = router({
|
|||||||
npc: npcRouter,
|
npc: npcRouter,
|
||||||
turnDaemon: turnDaemonRouter,
|
turnDaemon: turnDaemonRouter,
|
||||||
auction: auctionRouter,
|
auction: auctionRouter,
|
||||||
|
tournament: tournamentRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type AppRouter = typeof appRouter;
|
export type AppRouter = typeof appRouter;
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import { TRPCError } from '@trpc/server';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import type { TournamentType } from '@sammo-ts/logic';
|
||||||
|
|
||||||
|
import { TournamentStore } from '../../tournament/store.js';
|
||||||
|
import { buildTournamentKeys } from '../../tournament/keys.js';
|
||||||
|
import { authedProcedure, procedure, router } from '../../trpc.js';
|
||||||
|
|
||||||
|
const hasAdminRole = (roles: string[], profileName: string): boolean => {
|
||||||
|
if (roles.includes('superuser') || roles.includes('admin') || roles.includes('admin.superuser')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return roles.some((role) => role === 'admin.tournament' || role === `admin.tournament:${profileName}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const adminProcedure = authedProcedure.use(({ ctx, next }) => {
|
||||||
|
const roles = ctx.auth?.user.roles ?? [];
|
||||||
|
if (!hasAdminRole(roles, ctx.profile.name)) {
|
||||||
|
throw new TRPCError({ code: 'FORBIDDEN', message: 'Admin permission is required.' });
|
||||||
|
}
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
|
const zTournamentState = z.object({
|
||||||
|
stage: z.number().int().min(0),
|
||||||
|
phase: z.number().int().min(0),
|
||||||
|
type: z.number().int().min(0).max(3),
|
||||||
|
auto: z.boolean(),
|
||||||
|
openYear: z.number().int(),
|
||||||
|
openMonth: z.number().int(),
|
||||||
|
termSeconds: z.number().int().positive(),
|
||||||
|
nextAt: z.string().min(1),
|
||||||
|
bettingId: z.number().int().optional(),
|
||||||
|
bettingCloseAt: z.string().optional(),
|
||||||
|
winnerId: z.number().int().optional(),
|
||||||
|
bettingSettled: z.boolean().optional(),
|
||||||
|
lastError: z.string().optional(),
|
||||||
|
lastErrorAt: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const zParticipant = z.object({
|
||||||
|
id: z.number().int().positive(),
|
||||||
|
name: z.string().min(1),
|
||||||
|
leadership: z.number().int().min(0),
|
||||||
|
strength: z.number().int().min(0),
|
||||||
|
intel: z.number().int().min(0),
|
||||||
|
level: z.number().int().min(0),
|
||||||
|
});
|
||||||
|
|
||||||
|
const zMatch = z.object({
|
||||||
|
id: z.number().int().positive(),
|
||||||
|
stage: z.number().int().min(0),
|
||||||
|
roundIndex: z.number().int().min(0),
|
||||||
|
attackerId: z.number().int().positive(),
|
||||||
|
defenderId: z.number().int().positive(),
|
||||||
|
winnerId: z.number().int().positive().optional(),
|
||||||
|
log: z.array(z.string()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const zBetEntry = z.object({
|
||||||
|
generalId: z.number().int().positive(),
|
||||||
|
targetId: z.number().int().positive(),
|
||||||
|
amount: z.number().int().positive(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const tournamentRouter = router({
|
||||||
|
getState: procedure.query(async ({ ctx }) => {
|
||||||
|
const store = new TournamentStore(ctx.redis, buildTournamentKeys(ctx.profile.name));
|
||||||
|
return store.getState();
|
||||||
|
}),
|
||||||
|
getSnapshot: procedure.query(async ({ ctx }) => {
|
||||||
|
const store = new TournamentStore(ctx.redis, buildTournamentKeys(ctx.profile.name));
|
||||||
|
const [state, participants, matches, bets] = await Promise.all([
|
||||||
|
store.getState(),
|
||||||
|
store.getParticipants(),
|
||||||
|
store.getMatches(),
|
||||||
|
store.getBettingEntries(),
|
||||||
|
]);
|
||||||
|
return { state, participants, matches, bets };
|
||||||
|
}),
|
||||||
|
setState: adminProcedure.input(zTournamentState).mutation(async ({ ctx, input }) => {
|
||||||
|
const store = new TournamentStore(ctx.redis, buildTournamentKeys(ctx.profile.name));
|
||||||
|
await store.setState({
|
||||||
|
...input,
|
||||||
|
type: input.type as TournamentType,
|
||||||
|
});
|
||||||
|
return { ok: true };
|
||||||
|
}),
|
||||||
|
patchState: adminProcedure.input(zTournamentState.partial()).mutation(async ({ ctx, input }) => {
|
||||||
|
const store = new TournamentStore(ctx.redis, buildTournamentKeys(ctx.profile.name));
|
||||||
|
const current = await store.getState();
|
||||||
|
if (!current) {
|
||||||
|
throw new TRPCError({ code: 'NOT_FOUND', message: 'Tournament state not found.' });
|
||||||
|
}
|
||||||
|
const next = {
|
||||||
|
...current,
|
||||||
|
...input,
|
||||||
|
type: (input.type !== undefined ? input.type : current.type) as TournamentType,
|
||||||
|
};
|
||||||
|
await store.setState(next);
|
||||||
|
return { ok: true };
|
||||||
|
}),
|
||||||
|
setParticipants: adminProcedure.input(z.array(zParticipant)).mutation(async ({ ctx, input }) => {
|
||||||
|
const store = new TournamentStore(ctx.redis, buildTournamentKeys(ctx.profile.name));
|
||||||
|
await store.setParticipants(input);
|
||||||
|
return { ok: true, count: input.length };
|
||||||
|
}),
|
||||||
|
setMatches: adminProcedure.input(z.array(zMatch)).mutation(async ({ ctx, input }) => {
|
||||||
|
const store = new TournamentStore(ctx.redis, buildTournamentKeys(ctx.profile.name));
|
||||||
|
await store.setMatches(input);
|
||||||
|
return { ok: true, count: input.length };
|
||||||
|
}),
|
||||||
|
setBettingEntries: adminProcedure.input(z.array(zBetEntry)).mutation(async ({ ctx, input }) => {
|
||||||
|
const store = new TournamentStore(ctx.redis, buildTournamentKeys(ctx.profile.name));
|
||||||
|
await store.setBettingEntries(input);
|
||||||
|
return { ok: true, count: input.length };
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -2,10 +2,12 @@ export interface TournamentKeys {
|
|||||||
stateKey: string;
|
stateKey: string;
|
||||||
participantsKey: string;
|
participantsKey: string;
|
||||||
matchesKey: string;
|
matchesKey: string;
|
||||||
|
bettingKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const buildTournamentKeys = (profileName: string): TournamentKeys => ({
|
export const buildTournamentKeys = (profileName: string): TournamentKeys => ({
|
||||||
stateKey: `sammo:${profileName}:tournament:state`,
|
stateKey: `sammo:${profileName}:tournament:state`,
|
||||||
participantsKey: `sammo:${profileName}:tournament:participants`,
|
participantsKey: `sammo:${profileName}:tournament:participants`,
|
||||||
matchesKey: `sammo:${profileName}:tournament:matches`,
|
matchesKey: `sammo:${profileName}:tournament:matches`,
|
||||||
|
bettingKey: `sammo:${profileName}:tournament:betting`,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { TournamentKeys } from './keys.js';
|
import type { TournamentKeys } from './keys.js';
|
||||||
import type { TournamentMatchEntry, TournamentParticipantEntry, TournamentState } from './types.js';
|
import type { TournamentBetEntry, TournamentMatchEntry, TournamentParticipantEntry, TournamentState } from './types.js';
|
||||||
|
|
||||||
interface RedisClientLike {
|
interface RedisClientLike {
|
||||||
get(key: string): Promise<string | null>;
|
get(key: string): Promise<string | null>;
|
||||||
@@ -43,4 +43,12 @@ export class TournamentStore {
|
|||||||
async setMatches(matches: TournamentMatchEntry[]): Promise<void> {
|
async setMatches(matches: TournamentMatchEntry[]): Promise<void> {
|
||||||
await this.redis.set(this.keys.matchesKey, JSON.stringify(matches));
|
await this.redis.set(this.keys.matchesKey, JSON.stringify(matches));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getBettingEntries(): Promise<TournamentBetEntry[]> {
|
||||||
|
return safeJsonParse<TournamentBetEntry[]>(await this.redis.get(this.keys.bettingKey)) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async setBettingEntries(entries: TournamentBetEntry[]): Promise<void> {
|
||||||
|
await this.redis.set(this.keys.bettingKey, JSON.stringify(entries));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export interface TournamentState {
|
|||||||
bettingId?: number;
|
bettingId?: number;
|
||||||
bettingCloseAt?: string;
|
bettingCloseAt?: string;
|
||||||
winnerId?: number;
|
winnerId?: number;
|
||||||
|
bettingSettled?: boolean;
|
||||||
lastError?: string;
|
lastError?: string;
|
||||||
lastErrorAt?: string;
|
lastErrorAt?: string;
|
||||||
}
|
}
|
||||||
@@ -34,3 +35,9 @@ export interface TournamentMatchEntry {
|
|||||||
winnerId?: number;
|
winnerId?: number;
|
||||||
log?: string[];
|
log?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TournamentBetEntry {
|
||||||
|
generalId: number;
|
||||||
|
targetId: number;
|
||||||
|
amount: number;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import {
|
|||||||
} from '@sammo-ts/infra';
|
} from '@sammo-ts/infra';
|
||||||
|
|
||||||
import { resolveGameApiConfigFromEnv } from '../config.js';
|
import { resolveGameApiConfigFromEnv } from '../config.js';
|
||||||
|
import { RedisTurnDaemonTransport } from '../daemon/redisTransport.js';
|
||||||
|
import { buildTurnDaemonStreamKeys } from '../daemon/streamKeys.js';
|
||||||
import { buildTournamentKeys } from './keys.js';
|
import { buildTournamentKeys } from './keys.js';
|
||||||
import { TournamentStore } from './store.js';
|
import { TournamentStore } from './store.js';
|
||||||
import type { TournamentMatchEntry, TournamentState } from './types.js';
|
import type { TournamentBetEntry, TournamentMatchEntry, TournamentState } from './types.js';
|
||||||
|
|
||||||
const sleepMs = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
|
const sleepMs = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
@@ -244,6 +246,28 @@ const applyBattle = async (
|
|||||||
return nextState;
|
return nextState;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const buildBettingPayouts = (
|
||||||
|
winnerId: number,
|
||||||
|
entries: TournamentBetEntry[]
|
||||||
|
): { payouts: Array<{ generalId: number; amount: number }>; total: number; refundAll: boolean } => {
|
||||||
|
const total = entries.reduce((sum, entry) => sum + entry.amount, 0);
|
||||||
|
if (total <= 0) {
|
||||||
|
return { payouts: [], total: 0, refundAll: false };
|
||||||
|
}
|
||||||
|
const winners = entries.filter((entry) => entry.targetId === winnerId);
|
||||||
|
const winnersTotal = winners.reduce((sum, entry) => sum + entry.amount, 0);
|
||||||
|
if (winnersTotal <= 0) {
|
||||||
|
const refunds = entries.map((entry) => ({ generalId: entry.generalId, amount: entry.amount }));
|
||||||
|
return { payouts: refunds, total, refundAll: true };
|
||||||
|
}
|
||||||
|
const ratio = total / winnersTotal;
|
||||||
|
const payouts = winners.map((entry) => ({
|
||||||
|
generalId: entry.generalId,
|
||||||
|
amount: Math.round(entry.amount * ratio),
|
||||||
|
}));
|
||||||
|
return { payouts, total, refundAll: false };
|
||||||
|
};
|
||||||
|
|
||||||
const applyPreBattleStage = async (
|
const applyPreBattleStage = async (
|
||||||
store: TournamentStore,
|
store: TournamentStore,
|
||||||
state: TournamentState,
|
state: TournamentState,
|
||||||
@@ -362,6 +386,10 @@ export const runTournamentWorker = async (): Promise<void> => {
|
|||||||
await redis.connect();
|
await redis.connect();
|
||||||
|
|
||||||
const store = new TournamentStore(redis.client, buildTournamentKeys(config.profileName));
|
const store = new TournamentStore(redis.client, buildTournamentKeys(config.profileName));
|
||||||
|
const daemonTransport = new RedisTurnDaemonTransport(redis.client, {
|
||||||
|
keys: buildTurnDaemonStreamKeys(config.profileName),
|
||||||
|
requestTimeoutMs: config.daemonRequestTimeoutMs,
|
||||||
|
});
|
||||||
|
|
||||||
const handleExit = async () => {
|
const handleExit = async () => {
|
||||||
await redis.disconnect();
|
await redis.disconnect();
|
||||||
@@ -387,10 +415,46 @@ export const runTournamentWorker = async (): Promise<void> => {
|
|||||||
try {
|
try {
|
||||||
const worldState = await postgres.prisma.worldState.findFirst();
|
const worldState = await postgres.prisma.worldState.findFirst();
|
||||||
const baseSeed = (worldState?.meta as Record<string, unknown> | null)?.hiddenSeed ?? 'tournament';
|
const baseSeed = (worldState?.meta as Record<string, unknown> | null)?.hiddenSeed ?? 'tournament';
|
||||||
|
let nextState = state;
|
||||||
if (isBattleStage(state.stage)) {
|
if (isBattleStage(state.stage)) {
|
||||||
await applyBattle(store, state, String(baseSeed));
|
nextState = await applyBattle(store, state, String(baseSeed));
|
||||||
} else if (isPreBattleStage(state.stage)) {
|
} else if (isPreBattleStage(state.stage)) {
|
||||||
await applyPreBattleStage(store, state, String(baseSeed));
|
nextState = await applyPreBattleStage(store, state, String(baseSeed));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
nextState.stage === 0 &&
|
||||||
|
nextState.winnerId &&
|
||||||
|
nextState.bettingId &&
|
||||||
|
!nextState.bettingSettled
|
||||||
|
) {
|
||||||
|
const bettingEntries = await store.getBettingEntries();
|
||||||
|
if (bettingEntries.length > 0) {
|
||||||
|
const payoutInfo = buildBettingPayouts(nextState.winnerId, bettingEntries);
|
||||||
|
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',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const settledState: TournamentState = {
|
||||||
|
...nextState,
|
||||||
|
bettingSettled: true,
|
||||||
|
};
|
||||||
|
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';
|
||||||
|
|||||||
@@ -233,6 +233,29 @@ const normalizeCommand = (envelope: TurnDaemonCommandEnvelope): TurnDaemonComman
|
|||||||
refunds,
|
refunds,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case 'tournamentBettingPayout': {
|
||||||
|
if (!Array.isArray(command.payouts)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const payouts = command.payouts
|
||||||
|
.filter((entry) =>
|
||||||
|
entry && typeof entry.generalId === 'number' && typeof entry.amount === 'number'
|
||||||
|
)
|
||||||
|
.map((entry) => ({
|
||||||
|
generalId: entry.generalId,
|
||||||
|
amount: entry.amount,
|
||||||
|
}));
|
||||||
|
if (payouts.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: 'tournamentBettingPayout',
|
||||||
|
requestId: envelope.requestId,
|
||||||
|
bettingId: typeof command.bettingId === 'number' ? command.bettingId : undefined,
|
||||||
|
reason: typeof command.reason === 'string' ? command.reason : undefined,
|
||||||
|
payouts,
|
||||||
|
};
|
||||||
|
}
|
||||||
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 };
|
||||||
|
|||||||
@@ -499,6 +499,54 @@ async function handleTournamentRefund(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleTournamentBettingPayout(
|
||||||
|
ctx: CommandHandlerContext,
|
||||||
|
command: Extract<TurnDaemonCommand, { type: 'tournamentBettingPayout' }>
|
||||||
|
): Promise<TurnDaemonCommandResult> {
|
||||||
|
const { world, hooks } = ctx;
|
||||||
|
if (!command.payouts || command.payouts.length === 0) {
|
||||||
|
return {
|
||||||
|
type: 'tournamentBettingPayout',
|
||||||
|
ok: false,
|
||||||
|
bettingId: command.bettingId,
|
||||||
|
reason: '정산 대상이 없습니다.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let processed = 0;
|
||||||
|
let missing = 0;
|
||||||
|
let totalPayout = 0;
|
||||||
|
|
||||||
|
for (const payout of command.payouts) {
|
||||||
|
if (!payout || typeof payout.generalId !== 'number' || typeof payout.amount !== 'number') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (payout.amount <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const general = world.getGeneralById(payout.generalId);
|
||||||
|
if (!general) {
|
||||||
|
missing += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
world.updateGeneral(payout.generalId, {
|
||||||
|
gold: general.gold + payout.amount,
|
||||||
|
});
|
||||||
|
processed += 1;
|
||||||
|
totalPayout += payout.amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
await flushWorld(world, hooks);
|
||||||
|
return {
|
||||||
|
type: 'tournamentBettingPayout',
|
||||||
|
ok: true,
|
||||||
|
bettingId: command.bettingId,
|
||||||
|
processed,
|
||||||
|
missing,
|
||||||
|
totalPayout,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const createTurnDaemonCommandHandler = (options: {
|
export const createTurnDaemonCommandHandler = (options: {
|
||||||
world: InMemoryTurnWorld;
|
world: InMemoryTurnWorld;
|
||||||
hooks?: TurnDaemonHooks;
|
hooks?: TurnDaemonHooks;
|
||||||
@@ -535,6 +583,8 @@ export const createTurnDaemonCommandHandler = (options: {
|
|||||||
return handleAppoint(ctx, command);
|
return handleAppoint(ctx, command);
|
||||||
case 'tournamentRefund':
|
case 'tournamentRefund':
|
||||||
return handleTournamentRefund(ctx, command);
|
return handleTournamentRefund(ctx, command);
|
||||||
|
case 'tournamentBettingPayout':
|
||||||
|
return handleTournamentBettingPayout(ctx, command);
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,17 +85,27 @@ export type TurnDaemonCommand =
|
|||||||
destGeneralId: number;
|
destGeneralId: number;
|
||||||
destCityId: number;
|
destCityId: number;
|
||||||
officerLevel: number;
|
officerLevel: number;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'tournamentRefund';
|
type: 'tournamentRefund';
|
||||||
requestId?: string;
|
requestId?: string;
|
||||||
bettingId?: number;
|
bettingId?: number;
|
||||||
reason?: string;
|
reason?: string;
|
||||||
refunds: Array<{
|
refunds: Array<{
|
||||||
generalId: number;
|
generalId: number;
|
||||||
amount: number;
|
amount: number;
|
||||||
}>;
|
}>;
|
||||||
};
|
}
|
||||||
|
| {
|
||||||
|
type: 'tournamentBettingPayout';
|
||||||
|
requestId?: string;
|
||||||
|
bettingId?: number;
|
||||||
|
reason?: string;
|
||||||
|
payouts: Array<{
|
||||||
|
generalId: number;
|
||||||
|
amount: number;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
export type TurnDaemonCommandResult =
|
export type TurnDaemonCommandResult =
|
||||||
| {
|
| {
|
||||||
@@ -156,7 +166,21 @@ export type TurnDaemonCommandResult =
|
|||||||
ok: false;
|
ok: false;
|
||||||
bettingId?: number;
|
bettingId?: number;
|
||||||
reason: string;
|
reason: string;
|
||||||
};
|
}
|
||||||
|
| {
|
||||||
|
type: 'tournamentBettingPayout';
|
||||||
|
ok: true;
|
||||||
|
bettingId?: number;
|
||||||
|
processed: number;
|
||||||
|
missing: number;
|
||||||
|
totalPayout: number;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'tournamentBettingPayout';
|
||||||
|
ok: false;
|
||||||
|
bettingId?: 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