economy and rematch
This commit is contained in:
+46
-13
@@ -6,9 +6,14 @@ import { createSupabase, supabaseConfigured } from './auth_bridge';
|
||||
|
||||
/** Per player on the match: deducted when a winner is recorded. */
|
||||
export const MATCH_ENTRY_FEE_RC = 21;
|
||||
/** Added to the winning player's RC (after entry fee). */
|
||||
const MATCH_WINNER_RC_BONUS = 34;
|
||||
/** Added to each player's CC when a winner is recorded. */
|
||||
|
||||
/** `prize_pool = entry_fee * 2`, `server_fee = ceil(prize_pool / 10)`, returns `prize_pool - server_fee`. */
|
||||
export function computeRcPrizeFromEntryFee(entryFee: number): number {
|
||||
const prizePool = entryFee * 2;
|
||||
const serverFee = Math.ceil(prizePool / 10);
|
||||
return prizePool - serverFee;
|
||||
}
|
||||
/** Added to each player's CC when a winner is recorded (default if `matches.prize_cc` is null). */
|
||||
const MATCH_PARTICIPANT_CC_REWARD = 100;
|
||||
|
||||
function coerceId(id: unknown): number | null {
|
||||
@@ -88,6 +93,7 @@ export async function loadUsernamesForUserPair(
|
||||
export type MatchRoomSuccessEnvelope = {
|
||||
ok: true;
|
||||
entry_fee: number;
|
||||
rc_prize: number;
|
||||
for_red: Room | null;
|
||||
for_blue: Room | null;
|
||||
};
|
||||
@@ -132,18 +138,40 @@ export async function getPlayerLast10MatchRecord(
|
||||
return { l10_wins, l10_losses };
|
||||
}
|
||||
|
||||
function coerceNonNegativeInt(v: unknown): number | null {
|
||||
if (v == null) return null;
|
||||
if (typeof v === 'bigint') {
|
||||
const n = Number(v);
|
||||
return Number.isSafeInteger(n) && n >= 0 ? n : null;
|
||||
}
|
||||
if (typeof v === 'number' && Number.isFinite(v)) {
|
||||
const t = Math.trunc(v);
|
||||
return t >= 0 ? t : null;
|
||||
}
|
||||
if (typeof v === 'string' && /^-?\d+$/.test(v.trim())) {
|
||||
const n = parseInt(v.trim(), 10);
|
||||
return Number.isNaN(n) || n < 0 ? null : n;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry fee from each participant, winner bonus RC, CC for both. Idempotent callers should set winner only once.
|
||||
* Each player pays `entry_fee` RC; winner receives `computeRcPrizeFromEntryFee(entry_fee)` RC; each gets `prizeCc` CC.
|
||||
* Idempotent callers should set winner only once.
|
||||
*/
|
||||
async function applyMatchWinnerEconomy(
|
||||
supabase: SupabaseClient,
|
||||
userRed: number | null,
|
||||
userBlue: number | null,
|
||||
winnerId: number
|
||||
winnerId: number,
|
||||
entryFee: number,
|
||||
prizeCc: 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 { data: rows, error: selErr } = await supabase.from('users').select('id, rc, cc').in('id', ids);
|
||||
if (selErr) return selErr.message;
|
||||
if (!rows || rows.length !== ids.length) return 'One or more players not found in users';
|
||||
@@ -153,9 +181,9 @@ async function applyMatchWinnerEconomy(
|
||||
if (uid == null) continue;
|
||||
const baseRc = Number((row as { rc: unknown }).rc ?? 0);
|
||||
const baseCc = Number((row as { cc: unknown }).cc ?? 0);
|
||||
let newRc = baseRc - MATCH_ENTRY_FEE_RC;
|
||||
if (uid === winnerId) newRc += MATCH_WINNER_RC_BONUS;
|
||||
const newCc = baseCc + MATCH_PARTICIPANT_CC_REWARD;
|
||||
let newRc = baseRc - entryFee;
|
||||
if (uid === winnerId) newRc += rcPrize;
|
||||
const newCc = baseCc + prizeCc;
|
||||
const { error: upErr } = await supabase.from('users').update({ rc: newRc, cc: newCc }).eq('id', uid);
|
||||
if (upErr) return upErr.message;
|
||||
}
|
||||
@@ -386,7 +414,7 @@ export function registerMatchRoutes(
|
||||
const supabase = createSupabase(settings);
|
||||
const { data: row, error: fetchErr } = await supabase
|
||||
.from('matches')
|
||||
.select('user_red, user_blue')
|
||||
.select('user_red, user_blue, entry_fee, prize_cc')
|
||||
.eq('id', matchId)
|
||||
.maybeSingle();
|
||||
|
||||
@@ -401,6 +429,10 @@ export function registerMatchRoutes(
|
||||
|
||||
const userRed = coerceId((row as { user_red: unknown }).user_red);
|
||||
const userBlue = coerceId((row as { user_blue: unknown }).user_blue);
|
||||
const entryFeeRaw = coerceNonNegativeInt((row as { entry_fee: unknown }).entry_fee);
|
||||
const entryFee = entryFeeRaw ?? MATCH_ENTRY_FEE_RC;
|
||||
const prizeCcRaw = coerceNonNegativeInt((row as { prize_cc: unknown }).prize_cc);
|
||||
const prizeCc = prizeCcRaw ?? MATCH_PARTICIPANT_CC_REWARD;
|
||||
const winnerId = side === 'red' ? userRed : userBlue;
|
||||
if (winnerId == null) {
|
||||
res.status(400).json({ ok: false, error: `${side} slot has no user on this match` });
|
||||
@@ -429,7 +461,7 @@ export function registerMatchRoutes(
|
||||
return;
|
||||
}
|
||||
|
||||
const payErr = await applyMatchWinnerEconomy(supabase, userRed, userBlue, winnerId);
|
||||
const payErr = await applyMatchWinnerEconomy(supabase, userRed, userBlue, winnerId, entryFee, prizeCc);
|
||||
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);
|
||||
@@ -437,14 +469,15 @@ export function registerMatchRoutes(
|
||||
return;
|
||||
}
|
||||
|
||||
const rcPrize = computeRcPrizeFromEntryFee(entryFee);
|
||||
res.json({
|
||||
ok: true,
|
||||
id: matchId,
|
||||
winner_id: winnerId,
|
||||
economy: {
|
||||
entry_fee_rc: MATCH_ENTRY_FEE_RC,
|
||||
winner_rc_bonus: MATCH_WINNER_RC_BONUS,
|
||||
participant_cc: MATCH_PARTICIPANT_CC_REWARD,
|
||||
entry_fee_rc: entryFee,
|
||||
rc_prize: rcPrize,
|
||||
participant_cc: prizeCc,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user