From a09b33e1588147ad4ab0560e7b064de9330509f5 Mon Sep 17 00:00:00 2001 From: React User Date: Mon, 4 May 2026 12:54:30 +0000 Subject: [PATCH] economy and rematch --- app.ts | 6 ++- games/soccar/soccar_Data/Log.txt | 4 +- games/soccar/soccar_Data/Logs/245.txt | 20 +++++++++ games/soccar/soccar_Data/Logs/246.txt | 21 +++++++++ games/soccar/soccar_Data/Logs/247.txt | 22 ++++++++++ games/soccar/soccar_Data/Logs/248.txt | 63 +++++++++++++++++++++++++++ matches.ts | 59 +++++++++++++++++++------ types.ts | 2 + 8 files changed, 181 insertions(+), 16 deletions(-) create mode 100644 games/soccar/soccar_Data/Logs/245.txt create mode 100644 games/soccar/soccar_Data/Logs/246.txt create mode 100644 games/soccar/soccar_Data/Logs/247.txt create mode 100644 games/soccar/soccar_Data/Logs/248.txt diff --git a/app.ts b/app.ts index 00adb4f..5955280 100644 --- a/app.ts +++ b/app.ts @@ -8,6 +8,7 @@ import { insertMatchForNewRoom, insertRematchMatch, loadUsernamesForUserPair, + computeRcPrizeFromEntryFee, MATCH_ENTRY_FEE_RC, registerMatchRoutes, updateMatchUserBlue, @@ -98,17 +99,20 @@ function roomResponseForViewer(room: Room, viewerUserId: number): Room { const idx = room.Players.findIndex((p) => p.UserId === viewerUserId); const your_team: 'red' | 'blue' | null = idx === 0 ? 'red' : idx === 1 ? 'blue' : null; const entry_fee = room.entry_fee ?? MATCH_ENTRY_FEE_RC; - return { ...room, entry_fee, your_team }; + const rc_prize = computeRcPrizeFromEntryFee(entry_fee); + return { ...room, entry_fee, rc_prize, your_team }; } /** Same envelope as `POST /internal/rematch` success: both sides when seated, else `null` for open slot. */ function roomMatchSuccessEnvelope(room: Room): MatchRoomSuccessEnvelope { const entry_fee = room.entry_fee ?? MATCH_ENTRY_FEE_RC; + const rc_prize = computeRcPrizeFromEntryFee(entry_fee); const redId = room.Players[0]?.UserId; const blueId = room.Players[1]?.UserId; return { ok: true, entry_fee, + rc_prize, for_red: redId != null ? roomResponseForViewer(room, redId) : null, for_blue: blueId != null ? roomResponseForViewer(room, blueId) : null, }; diff --git a/games/soccar/soccar_Data/Log.txt b/games/soccar/soccar_Data/Log.txt index 5dcba6f..d8fdb71 100644 --- a/games/soccar/soccar_Data/Log.txt +++ b/games/soccar/soccar_Data/Log.txt @@ -1,3 +1,3 @@ -Logger initiated at 05/02/2026 22:12:20 +Logger initiated at 05/04/2026 12:01:54 -[05/02/2026 22:12:20] Configured dedicated match reporting for match id 244 with secret 38516e39d3406225b7c3015a66e5d25c54e9b2024c0755212122bdcea3e3a328 and internal api base https://kickkingsapi.playpoolstudios.com +[05/04/2026 12:01:54] Configured dedicated match reporting for match id 248 with secret 38516e39d3406225b7c3015a66e5d25c54e9b2024c0755212122bdcea3e3a328 and internal api base https://kickkingsapi.playpoolstudios.com diff --git a/games/soccar/soccar_Data/Logs/245.txt b/games/soccar/soccar_Data/Logs/245.txt new file mode 100644 index 0000000..95da359 --- /dev/null +++ b/games/soccar/soccar_Data/Logs/245.txt @@ -0,0 +1,20 @@ +[05/04/2026 09:10:06] Starting server at port 26950 +[05/04/2026 09:10:06] Failed to fetch red and blue names, isServer? +[05/04/2026 09:10:06] Starting auto close watchdog +[05/04/2026 09:10:06] Active player connections: 0 +[05/04/2026 09:10:10] Active player connections: 1 +[05/04/2026 09:10:11] Client 15 set team to Red +[05/04/2026 09:10:12] Dedicated match PATCH success: 200 {"ok":true,"id":245} +[05/04/2026 09:10:15] Active player connections: 2 +[05/04/2026 09:10:15] Game started On Server +[05/04/2026 09:10:15] Client 16 set team to Red +[05/04/2026 09:10:19] Active player connections: 1 +[05/04/2026 09:10:20] Active player connections: 0 +[05/04/2026 09:10:25] Server will be closed due to no players in, 60 +[05/04/2026 09:10:35] Server will be closed due to no players in, 50 +[05/04/2026 09:10:45] Server will be closed due to no players in, 40 +[05/04/2026 09:10:55] Server will be closed due to no players in, 30 +[05/04/2026 09:11:05] Server will be closed due to no players in, 20 +[05/04/2026 09:11:15] Server will be closed due to no players in, 10 +[05/04/2026 09:11:25] All players left, exiting +[05/04/2026 09:11:26] Dedicated match PATCH success: 200 {"ok":true,"id":245} diff --git a/games/soccar/soccar_Data/Logs/246.txt b/games/soccar/soccar_Data/Logs/246.txt new file mode 100644 index 0000000..b1b48a4 --- /dev/null +++ b/games/soccar/soccar_Data/Logs/246.txt @@ -0,0 +1,21 @@ +[05/04/2026 09:58:29] Starting server at port 26684 +[05/04/2026 09:58:29] Failed to fetch red and blue names, isServer? +[05/04/2026 09:58:29] Starting auto close watchdog +[05/04/2026 09:58:29] Active player connections: 0 +[05/04/2026 09:58:34] Server will be closed due to no players in, 60 +[05/04/2026 09:58:34] Active player connections: 1 +[05/04/2026 09:58:34] Client 15 set team to Red +[05/04/2026 09:58:35] Active player connections: 2 +[05/04/2026 09:58:35] Game started On Server +[05/04/2026 09:58:35] Dedicated match PATCH success: 200 {"ok":true,"id":246} +[05/04/2026 09:58:35] Client 16 set team to Red +[05/04/2026 10:12:02] Active player connections: 1 +[05/04/2026 10:14:21] Active player connections: 0 +[05/04/2026 10:14:26] Server will be closed due to no players in, 60 +[05/04/2026 10:14:36] Server will be closed due to no players in, 50 +[05/04/2026 10:14:46] Server will be closed due to no players in, 40 +[05/04/2026 10:14:56] Server will be closed due to no players in, 30 +[05/04/2026 10:15:06] Server will be closed due to no players in, 20 +[05/04/2026 10:15:16] Server will be closed due to no players in, 10 +[05/04/2026 10:15:26] All players left, exiting +[05/04/2026 10:15:28] Dedicated match PATCH success: 200 {"ok":true,"id":246} diff --git a/games/soccar/soccar_Data/Logs/247.txt b/games/soccar/soccar_Data/Logs/247.txt new file mode 100644 index 0000000..5f3fcdf --- /dev/null +++ b/games/soccar/soccar_Data/Logs/247.txt @@ -0,0 +1,22 @@ +[05/04/2026 11:54:28] Starting server at port 26052 +[05/04/2026 11:54:29] Failed to fetch red and blue names, isServer? +[05/04/2026 11:54:29] Starting auto close watchdog +[05/04/2026 11:54:29] Active player connections: 0 +[05/04/2026 11:54:33] Active player connections: 1 +[05/04/2026 11:54:34] Client 15 set team to Red +[05/04/2026 11:54:34] Active player connections: 2 +[05/04/2026 11:54:34] Game started On Server +[05/04/2026 11:54:34] Client 16 set team to Red +[05/04/2026 11:54:35] Dedicated match PATCH success: 200 {"ok":true,"id":247} +[05/04/2026 11:54:37] force = 70 * 0.9955804 = 69.69063 +[05/04/2026 11:54:37] launching puck at (4.94, -0.80) with (-344.01, 55.48) force +[05/04/2026 11:55:39] Active player connections: 1 +[05/04/2026 11:55:40] Active player connections: 0 +[05/04/2026 11:55:45] Server will be closed due to no players in, 60 +[05/04/2026 11:55:55] Server will be closed due to no players in, 50 +[05/04/2026 11:56:05] Server will be closed due to no players in, 40 +[05/04/2026 11:56:15] Server will be closed due to no players in, 30 +[05/04/2026 11:56:25] Server will be closed due to no players in, 20 +[05/04/2026 11:56:35] Server will be closed due to no players in, 10 +[05/04/2026 11:56:45] All players left, exiting +[05/04/2026 11:56:46] Dedicated match PATCH success: 200 {"ok":true,"id":247} diff --git a/games/soccar/soccar_Data/Logs/248.txt b/games/soccar/soccar_Data/Logs/248.txt new file mode 100644 index 0000000..c952b97 --- /dev/null +++ b/games/soccar/soccar_Data/Logs/248.txt @@ -0,0 +1,63 @@ +[05/04/2026 12:01:54] Starting server at port 26825 +[05/04/2026 12:01:54] Failed to fetch red and blue names, isServer? +[05/04/2026 12:01:54] Starting auto close watchdog +[05/04/2026 12:01:55] Active player connections: 0 +[05/04/2026 12:02:00] Server will be closed due to no players in, 60 +[05/04/2026 12:02:00] Active player connections: 1 +[05/04/2026 12:02:00] Client 15 set team to Blue +[05/04/2026 12:02:00] Active player connections: 2 +[05/04/2026 12:02:00] Game started On Server +[05/04/2026 12:02:01] Client 16 set team to Red +[05/04/2026 12:02:01] Dedicated match PATCH success: 200 {"ok":true,"id":248} +[05/04/2026 12:02:02] Dedicated match PATCH success: 200 {"ok":true,"id":248} +[05/04/2026 12:02:05] force = 70 * 0.9955804 = 69.69063 +[05/04/2026 12:02:05] launching puck at (4.96, -0.65) with (-345.53, 45.01) force +[05/04/2026 12:02:11] force = 70 * 0.9955804 = 69.69063 +[05/04/2026 12:02:11] launching puck at (-4.60, 1.96) with (320.69, -136.31) force +[05/04/2026 12:02:17] force = 70 * 0.9955804 = 69.69063 +[05/04/2026 12:02:17] launching puck at (-1.29, -4.83) with (90.25, 336.56) force +[05/04/2026 12:02:23] force = 70 * 0.9955804 = 69.69063 +[05/04/2026 12:02:23] launching puck at (-4.97, 0.55) with (346.37, -38.07) force +[05/04/2026 12:02:27] force = 70 * 0.9955804 = 69.69063 +[05/04/2026 12:02:27] launching puck at (4.62, -1.91) with (-321.95, 133.30) force +[05/04/2026 12:02:28] Red goal scored, red score is now 1 +[05/04/2026 12:02:36] force = 70 * 0.9955804 = 69.69063 +[05/04/2026 12:02:36] launching puck at (0.05, 5.00) with (-3.29, -348.44) force +[05/04/2026 12:02:42] force = 70 * 0.9955804 = 69.69063 +[05/04/2026 12:02:42] launching puck at (2.22, -4.48) with (-154.84, 312.16) force +[05/04/2026 12:02:49] force = 70 * 0.9955804 = 69.69063 +[05/04/2026 12:02:49] launching puck at (0.79, -4.94) with (-55.40, 344.02) force +[05/04/2026 12:02:54] force = 70 * 0.9955804 = 69.69063 +[05/04/2026 12:02:54] launching puck at (-1.28, -4.83) with (89.05, 336.88) force +[05/04/2026 12:03:05] force = 70 * 0.9955804 = 69.69063 +[05/04/2026 12:03:05] launching puck at (3.37, -3.70) with (-234.68, 257.57) force +[05/04/2026 12:03:09] force = 70 * 0.9955804 = 69.69063 +[05/04/2026 12:03:09] launching puck at (-0.72, 4.95) with (50.50, -344.77) force +[05/04/2026 12:03:16] force = 70 * 0.9955804 = 69.69063 +[05/04/2026 12:03:16] launching puck at (-0.86, 4.93) with (59.61, -343.32) force +[05/04/2026 12:03:17] Blue goal scored, blue score is now 1 +[05/04/2026 12:03:27] force = 70 * 0.3396594 = 23.77616 +[05/04/2026 12:03:27] launching puck at (0.00, -1.09) with (-0.06, 25.89) force +[05/04/2026 12:03:32] force = 70 * 0.9955804 = 69.69063 +[05/04/2026 12:03:32] launching puck at (-3.18, 3.86) with (221.33, -269.14) force +[05/04/2026 12:03:39] force = 70 * 0.9955804 = 69.69063 +[05/04/2026 12:03:39] launching puck at (0.13, -5.00) with (-8.80, 348.34) force +[05/04/2026 12:03:39] Red goal scored, red score is now 2 +[05/04/2026 12:03:48] force = 70 * 0.9955804 = 69.69063 +[05/04/2026 12:03:48] launching puck at (-3.23, 3.82) with (225.10, -265.98) force +[05/04/2026 12:03:56] force = 70 * 0.898546 = 62.89822 +[05/04/2026 12:03:56] launching puck at (0.10, -3.97) with (-6.07, 250.01) force +[05/04/2026 12:03:57] Red goal scored, red score is now 3 +[05/04/2026 12:03:58] Dedicated match PATCH success: 200 {"ok":true,"id":248} +[05/04/2026 12:03:59] Dedicated match PATCH success: 200 {"ok":true,"id":248,"winner_id":5,"economy":{"entry_fee_rc":21,"winner_rc_bonus":34,"participant_cc":100}} +[05/04/2026 12:04:08] Rematch requested +[05/04/2026 12:04:39] Active player connections: 1 +[05/04/2026 12:04:43] Active player connections: 0 +[05/04/2026 12:04:48] Server will be closed due to no players in, 60 +[05/04/2026 12:04:58] Server will be closed due to no players in, 50 +[05/04/2026 12:05:08] Server will be closed due to no players in, 40 +[05/04/2026 12:05:18] Server will be closed due to no players in, 30 +[05/04/2026 12:05:28] Server will be closed due to no players in, 20 +[05/04/2026 12:05:38] Server will be closed due to no players in, 10 +[05/04/2026 12:05:48] All players left, exiting +[05/04/2026 12:05:48] Dedicated match PATCH success: 200 {"ok":true,"id":248} diff --git a/matches.ts b/matches.ts index 37f0d44..238658f 100644 --- a/matches.ts +++ b/matches.ts @@ -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 { 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, }, }); }); diff --git a/types.ts b/types.ts index 336759c..0444b38 100644 --- a/types.ts +++ b/types.ts @@ -49,6 +49,8 @@ export interface Room { match_id?: number; /** RC entry fee for this match (matches `public.matches.entry_fee`). */ entry_fee?: number; + /** Net RC prize pool for the winner after server fee (derived from `entry_fee`). */ + rc_prize?: number; /** Only on GET / responses: authenticated user’s team (Players[0]=red, Players[1]=blue) */ your_team?: 'red' | 'blue' | null; }