This commit is contained in:
React User
2026-05-05 20:01:28 +00:00
parent a09b33e158
commit 04b3463b19
24 changed files with 594 additions and 37 deletions
+80 -14
View File
@@ -4,13 +4,62 @@ import type { SupabaseClient } from '@supabase/supabase-js';
import { Room, Settings } from './types';
import { createSupabase, supabaseConfigured } from './auth_bridge';
/** Per player on the match: deducted when a winner is recorded. */
/** Fallback when `public.settings` has no `entry_fee` row or value is invalid. */
export const MATCH_ENTRY_FEE_RC = 21;
/** `prize_pool = entry_fee * 2`, `server_fee = ceil(prize_pool / 10)`, returns `prize_pool - server_fee`. */
export function computeRcPrizeFromEntryFee(entryFee: number): number {
const SETTINGS_KEY_ENTRY_FEE = 'entry_fee';
const SETTINGS_KEY_BET_FEE = 'bet_fee';
/** Fallback when `public.settings` has no `bet_fee` row or value is invalid (percent of prize pool taken as server fee). */
export const DEFAULT_BET_FEE_PERCENT = 10;
/** RC entry fee for normal matchmaking (not rematches): `public.settings` row `key = entry_fee`, `value` integer string. */
export async function getDefaultMatchEntryFeeRc(settings: Settings): Promise<number> {
if (!supabaseConfigured(settings)) return MATCH_ENTRY_FEE_RC;
const supabase = createSupabase(settings);
const { data, error } = await supabase
.from('settings')
.select('value')
.eq('key', SETTINGS_KEY_ENTRY_FEE)
.maybeSingle();
if (error || !data || typeof (data as { value?: unknown }).value !== 'string') {
return MATCH_ENTRY_FEE_RC;
}
const n = parseInt(String((data as { value: string }).value).trim(), 10);
if (!Number.isInteger(n) || n < 0) return MATCH_ENTRY_FEE_RC;
return n;
}
/** Server fee percent of the prize pool (`entry_fee * 2`): `public.settings` row `key = bet_fee`, `value` integer 0100. */
export async function getBetFeePercentFromSettings(settings: Settings): Promise<number> {
if (!supabaseConfigured(settings)) return DEFAULT_BET_FEE_PERCENT;
const supabase = createSupabase(settings);
const { data, error } = await supabase
.from('settings')
.select('value')
.eq('key', SETTINGS_KEY_BET_FEE)
.maybeSingle();
if (error || !data || typeof (data as { value?: unknown }).value !== 'string') {
return DEFAULT_BET_FEE_PERCENT;
}
const n = parseInt(String((data as { value: string }).value).trim(), 10);
if (!Number.isInteger(n) || n < 0 || n > 100) return DEFAULT_BET_FEE_PERCENT;
return n;
}
export type InsertMatchForNewRoomResult = {
matchId: number | null;
/** Same RC fee stored on `matches.entry_fee` and returned for in-memory rooms. */
entry_fee: number;
};
/**
* `prize_pool = entry_fee * 2`, `server_fee = ceil(prize_pool * betFeePercent / 100)`, returns `prize_pool - server_fee`.
* `betFeePercent` comes from `public.settings` key `bet_fee` (default {@link DEFAULT_BET_FEE_PERCENT}).
*/
export function computeRcPrizeFromEntryFee(entryFee: number, betFeePercent: number = DEFAULT_BET_FEE_PERCENT): number {
const prizePool = entryFee * 2;
const serverFee = Math.ceil(prizePool / 10);
const serverFee = Math.ceil((prizePool * betFeePercent) / 100);
return prizePool - serverFee;
}
/** Added to each player's CC when a winner is recorded (default if `matches.prize_cc` is null). */
@@ -28,17 +77,23 @@ function coerceId(id: unknown): number | null {
}
/** Insert a match row when a new room is created. First player in the room = red (if provided). */
export async function insertMatchForNewRoom(settings: Settings, userRedId?: number): Promise<number | null> {
if (!supabaseConfigured(settings)) return null;
export async function insertMatchForNewRoom(
settings: Settings,
userRedId?: number
): Promise<InsertMatchForNewRoomResult> {
const entry_fee = await getDefaultMatchEntryFeeRc(settings);
if (!supabaseConfigured(settings)) {
return { matchId: null, entry_fee };
}
const supabase = createSupabase(settings);
const base = { entry_fee: MATCH_ENTRY_FEE_RC };
const base = { entry_fee };
const payload = userRedId == null ? base : { ...base, user_red: userRedId };
const { data, error } = await supabase.from('matches').insert(payload).select('id').single();
if (error || !data) {
console.error('matches: insert failed', error?.message);
return null;
return { matchId: null, entry_fee };
}
return coerceId(data.id);
return { matchId: coerceId(data.id), entry_fee };
}
/** Dedicated server: rematch row with both players and custom `entry_fee` (RC). */
@@ -156,7 +211,7 @@ function coerceNonNegativeInt(v: unknown): number | null {
}
/**
* Each player pays `entry_fee` RC; winner receives `computeRcPrizeFromEntryFee(entry_fee)` RC; each gets `prizeCc` CC.
* Each player pays `entry_fee` RC; winner receives RC prize after `betFeePercent` server cut; each gets `prizeCc` CC.
* Idempotent callers should set winner only once.
*/
async function applyMatchWinnerEconomy(
@@ -165,12 +220,13 @@ async function applyMatchWinnerEconomy(
userBlue: number | null,
winnerId: number,
entryFee: number,
prizeCc: number
prizeCc: number,
betFeePercent: number
): Promise<string | null> {
const ids = [...new Set([userRed, userBlue].filter((x): x is number => x != null && x >= 1))];
if (ids.length === 0) return null;
const rcPrize = computeRcPrizeFromEntryFee(entryFee);
const rcPrize = computeRcPrizeFromEntryFee(entryFee, betFeePercent);
const { data: rows, error: selErr } = await supabase.from('users').select('id, rc, cc').in('id', ids);
if (selErr) return selErr.message;
@@ -439,6 +495,8 @@ export function registerMatchRoutes(
return;
}
const betFeePercent = await getBetFeePercentFromSettings(settings);
const { data: updated, error: updErr } = await supabase
.from('matches')
.update({ winner_id: winnerId })
@@ -461,7 +519,15 @@ export function registerMatchRoutes(
return;
}
const payErr = await applyMatchWinnerEconomy(supabase, userRed, userBlue, winnerId, entryFee, prizeCc);
const payErr = await applyMatchWinnerEconomy(
supabase,
userRed,
userBlue,
winnerId,
entryFee,
prizeCc,
betFeePercent
);
if (payErr != null) {
console.error('matches: economy after winner failed', matchId, payErr);
await supabase.from('matches').update({ winner_id: null }).eq('id', matchId).eq('winner_id', winnerId);
@@ -469,7 +535,7 @@ export function registerMatchRoutes(
return;
}
const rcPrize = computeRcPrizeFromEntryFee(entryFee);
const rcPrize = computeRcPrizeFromEntryFee(entryFee, betFeePercent);
res.json({
ok: true,
id: matchId,