This commit is contained in:
React User
2026-05-04 08:50:50 +00:00
parent 146510ddeb
commit b104d35a9d
140 changed files with 685 additions and 6289 deletions
+143 -4
View File
@@ -1,11 +1,11 @@
import express, { Express, Request, Response } from 'express';
import * as crypto from 'crypto';
import type { SupabaseClient } from '@supabase/supabase-js';
import { Settings } from './types';
import { Room, Settings } from './types';
import { createSupabase, supabaseConfigured } from './auth_bridge';
/** Per player on the match: deducted when a winner is recorded. */
const MATCH_ENTRY_FEE_RC = 21;
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. */
@@ -26,7 +26,8 @@ function coerceId(id: unknown): number | null {
export async function insertMatchForNewRoom(settings: Settings, userRedId?: number): Promise<number | null> {
if (!supabaseConfigured(settings)) return null;
const supabase = createSupabase(settings);
const payload = userRedId == null ? {} : { user_red: userRedId };
const base = { entry_fee: MATCH_ENTRY_FEE_RC };
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);
@@ -35,6 +36,70 @@ export async function insertMatchForNewRoom(settings: Settings, userRedId?: numb
return coerceId(data.id);
}
/** Dedicated server: rematch row with both players and custom `entry_fee` (RC). */
export async function insertRematchMatch(
settings: Settings,
userRedId: number,
userBlueId: number,
entryFee: number
): Promise<number | null> {
if (!supabaseConfigured(settings)) return null;
const supabase = createSupabase(settings);
const { data, error } = await supabase
.from('matches')
.insert({
user_red: userRedId,
user_blue: userBlueId,
entry_fee: entryFee,
})
.select('id')
.single();
if (error || !data) {
console.error('matches: rematch insert failed', error?.message);
return null;
}
return coerceId(data.id);
}
/** Load `username` for two user ids (order not preserved in result). */
export async function loadUsernamesForUserPair(
settings: Settings,
userA: number,
userB: number
): Promise<{ nameById: Map<number, string> } | null> {
if (!supabaseConfigured(settings)) return null;
const supabase = createSupabase(settings);
const { data, error } = await supabase.from('users').select('id, username').in('id', [userA, userB]);
if (error || !data || data.length !== 2) {
if (error) console.error('matches: load usernames failed', error.message);
return null;
}
const nameById = new Map<number, string>();
for (const row of data as { id: unknown; username: unknown }[]) {
const id = coerceId(row.id);
if (id == null || typeof row.username !== 'string' || row.username.length < 1) return null;
nameById.set(id, row.username);
}
if (!nameById.has(userA) || !nameById.has(userB)) return null;
return { nameById };
}
/** Same JSON shape for GET / (room found) and POST /internal/rematch success. */
export type MatchRoomSuccessEnvelope = {
ok: true;
entry_fee: number;
for_red: Room | null;
for_blue: Room | null;
};
export type CreateRematchRoomResult = MatchRoomSuccessEnvelope | { ok: false; status: number; error: string };
export type CreateRematchRoomFn = (args: {
userRedId: number;
userBlueId: number;
entryFee: number;
}) => Promise<CreateRematchRoomResult>;
/** Latest 10 matches for this user (by created_at); counts wins/losses from winner_id only. */
export async function getPlayerLast10MatchRecord(
settings: Settings,
@@ -141,7 +206,11 @@ function rejectUnlessDedicatedServer(req: Request, res: Response, settings: Sett
return false;
}
export function registerMatchRoutes(app: Express, settings: Settings): void {
export function registerMatchRoutes(
app: Express,
settings: Settings,
deps?: { createRematchRoom?: CreateRematchRoomFn }
): void {
const jsonParser = express.json();
/**
@@ -167,6 +236,76 @@ export function registerMatchRoutes(app: Express, settings: Settings): void {
});
});
/** Public: RC balance for a player (`users.id` / matchmaking UserId). */
app.get('/players/:playerId/rc', async (req: Request, res: Response) => {
if (!supabaseConfigured(settings)) {
res.status(503).json({ ok: false, error: 'Database not configured' });
return;
}
const playerId = coerceId(req.params.playerId);
if (playerId == null || playerId < 1) {
res.status(400).json({ ok: false, error: 'Invalid player id' });
return;
}
const supabase = createSupabase(settings);
const { data, error } = await supabase.from('users').select('rc').eq('id', playerId).maybeSingle();
if (error) {
res.status(500).json({ ok: false, error: error.message });
return;
}
if (!data) {
res.status(404).json({ ok: false, error: 'Player not found' });
return;
}
const rc = Number((data as { rc: unknown }).rc ?? 0);
res.json({ ok: true, player_id: playerId, rc });
});
/**
* Dedicated server only: open a room with two players and a custom RC entry fee.
* Response body matches GET / on room success: `{ ok, entry_fee, for_red, for_blue }`.
*/
app.post('/internal/rematch', jsonParser, async (req: Request, res: Response) => {
if (rejectUnlessDedicatedServer(req, res, settings)) return;
if (!deps?.createRematchRoom) {
res.status(503).json({ ok: false, error: 'Rematch handler not configured' });
return;
}
const body = req.body as Record<string, unknown>;
const userRedId = coerceId(body.user_red_id);
const userBlueId = coerceId(body.user_blue_id);
const rawFee = body.entry_fee;
const entryFee =
typeof rawFee === 'bigint'
? Number(rawFee)
: typeof rawFee === 'number' && Number.isFinite(rawFee)
? Math.trunc(rawFee)
: typeof rawFee === 'string' && /^-?\d+$/.test(rawFee.trim())
? parseInt(rawFee.trim(), 10)
: null;
if (userRedId == null || userRedId < 1 || userBlueId == null || userBlueId < 1) {
res.status(400).json({ ok: false, error: 'user_red_id and user_blue_id must be positive integers' });
return;
}
if (userRedId === userBlueId) {
res.status(400).json({ ok: false, error: 'user_red_id and user_blue_id must differ' });
return;
}
if (entryFee == null || !Number.isInteger(entryFee) || entryFee < 0) {
res.status(400).json({ ok: false, error: 'entry_fee must be a non-negative integer' });
return;
}
const result = await deps.createRematchRoom({ userRedId, userBlueId, entryFee });
if (!result.ok) {
res.status(result.status).json({ ok: false, error: result.error });
return;
}
res.json(result);
});
app.patch('/internal/match/:matchId', jsonParser, async (req: Request, res: Response) => {
if (rejectUnlessDedicatedServer(req, res, settings)) return;