diff --git a/src/app/page.tsx b/src/app/page.tsx
index 07001d7..b21f979 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -18,7 +18,7 @@ import {
type MatchmakerLogSource,
} from "@/lib/matchmaker-log-source";
import { readMatchmakerLogFile } from "@/lib/matchmaker-logs-server";
-import { fetchEntryHoldPerPlayerCoinsByMatchIds } from "@/lib/match-entry-hold-from-ledger";
+import { fetchMatchEntryHoldAndFeePerPlayerCoinsByMatchIds } from "@/lib/match-entry-hold-from-ledger";
import type {
AdminMatchRow,
DbMatch,
@@ -144,21 +144,25 @@ export default async function Home({
matchesError = matchesRes.error.message;
} else {
const rawMatches = (matchesRes.data ?? []) as DbMatch[];
- const holdByMatch = await fetchEntryHoldPerPlayerCoinsByMatchIds(
- supabase,
- rawMatches
- .map((m) => Number(m.id))
- .filter((id) => Number.isFinite(id)),
- );
+ const { holdPerPlayer, feePerPlayer } =
+ await fetchMatchEntryHoldAndFeePerPlayerCoinsByMatchIds(
+ supabase,
+ rawMatches
+ .map((m) => Number(m.id))
+ .filter((id) => Number.isFinite(id)),
+ );
matches = rawMatches.map((m) => {
const idNum = Number(m.id);
- const hold = Number.isFinite(idNum)
- ? holdByMatch.get(idNum)
- : undefined;
+ const hold =
+ Number.isFinite(idNum) ? holdPerPlayer.get(idNum) : undefined;
+ const fee =
+ Number.isFinite(idNum) ? feePerPlayer.get(idNum) : undefined;
return {
...m,
entryHoldPerPlayerCoins:
hold !== undefined ? hold.toString() : null,
+ entryFeePerPlayerCoins:
+ fee !== undefined ? fee.toString() : null,
};
});
}
diff --git a/src/components/admin-dashboard.tsx b/src/components/admin-dashboard.tsx
index bd0d0d0..7963363 100644
--- a/src/components/admin-dashboard.tsx
+++ b/src/components/admin-dashboard.tsx
@@ -66,11 +66,6 @@ function formatTs(value: string | null): string {
}
}
-function matchEntryFeeCoinString(v: DbMatch["entry_fee"]): string | null {
- if (v == null) return null;
- return txAmountToBigInt(v).toString();
-}
-
function formatPrizeCcChip(v: DbMatch["prize_cc"]): string {
if (v == null) return "—";
const b = txAmountToBigInt(v);
@@ -819,7 +814,7 @@ export function AdminDashboard({
matchId={m.id}
statusLabel={statusLabel(m.status)}
createdAtLabel={formatTs(m.created_at)}
- entryFeeCoins={matchEntryFeeCoinString(m.entry_fee)}
+ entryFeeCoins={m.entryFeePerPlayerCoins}
entryHoldPerPlayerCoins={m.entryHoldPerPlayerCoins}
prizeCcLabel={formatPrizeCcChip(m.prize_cc)}
winnerId={m.winner_id}
diff --git a/src/components/match-history-battle-card.tsx b/src/components/match-history-battle-card.tsx
index 0e57324..c59db79 100644
--- a/src/components/match-history-battle-card.tsx
+++ b/src/components/match-history-battle-card.tsx
@@ -4,6 +4,40 @@ import Link from "next/link";
import { ClickableUserId } from "@/components/clickable-user-id";
import { formatRcLabelFromCoinsBigInt } from "@/lib/coins-rc";
+/** Ledger rows are per player; both players pay → show `1.0 RC x 2 = 2.0 RC`. */
+function rcPerPlayerTimesTwoLine(coinStr: string | null): string {
+ if (coinStr == null || String(coinStr).trim() === "") return "—";
+ try {
+ const perPlayer = BigInt(String(coinStr).trim());
+ const bothPlayers = perPlayer * BigInt(2);
+ return `${formatRcLabelFromCoinsBigInt(perPlayer)} x 2 = ${formatRcLabelFromCoinsBigInt(bothPlayers)}`;
+ } catch {
+ return "—";
+ }
+}
+
+function entryCombinedPerPlayerTimesTwoLine(
+ feeCoins: string | null,
+ holdCoins: string | null,
+): string {
+ if (
+ feeCoins == null ||
+ holdCoins == null ||
+ String(feeCoins).trim() === "" ||
+ String(holdCoins).trim() === ""
+ ) {
+ return "—";
+ }
+ try {
+ const perPlayer =
+ BigInt(String(feeCoins).trim()) + BigInt(String(holdCoins).trim());
+ const bothPlayers = perPlayer * BigInt(2);
+ return `${formatRcLabelFromCoinsBigInt(perPlayer)} x 2 = ${formatRcLabelFromCoinsBigInt(bothPlayers)}`;
+ } catch {
+ return "—";
+ }
+}
+
type PlayerSide = {
id: number | null;
username: string | null;
@@ -16,7 +50,7 @@ type Props = {
matchId: number;
statusLabel: string;
createdAtLabel: string;
- /** `matches.entry_fee` as coin string (bigint); RC shown in footer. */
+ /** Ledger `entry_fee` / player as coin string; null if unknown. */
entryFeeCoins: string | null;
/** Ledger `entry_hold` / player as coin string; null if unknown. */
entryHoldPerPlayerCoins: string | null;
@@ -28,35 +62,6 @@ type Props = {
winnerId: number | string | null;
};
-function rcLabelFromCoinString(s: string | null): string {
- if (s == null || String(s).trim() === "") return "—";
- try {
- return formatRcLabelFromCoinsBigInt(BigInt(String(s).trim()));
- } catch {
- return "—";
- }
-}
-
-function entryTotalRcLabel(
- feeCoins: string | null,
- holdCoins: string | null,
-): string {
- if (
- feeCoins == null ||
- holdCoins == null ||
- String(feeCoins).trim() === "" ||
- String(holdCoins).trim() === ""
- ) {
- return "—";
- }
- try {
- const sum = BigInt(String(feeCoins).trim()) + BigInt(String(holdCoins).trim());
- return formatRcLabelFromCoinsBigInt(sum);
- } catch {
- return "—";
- }
-}
-
function idsMatch(
a: number | string | null | undefined,
b: number | string | null | undefined,
@@ -185,9 +190,12 @@ export function MatchHistoryBattleCard({
right,
winnerId,
}: Props) {
- const entryRc = entryTotalRcLabel(entryFeeCoins, entryHoldPerPlayerCoins);
- const entryFeeRc = rcLabelFromCoinString(entryFeeCoins);
- const entryHoldRc = rcLabelFromCoinString(entryHoldPerPlayerCoins);
+ const entryLine = entryCombinedPerPlayerTimesTwoLine(
+ entryFeeCoins,
+ entryHoldPerPlayerCoins,
+ );
+ const entryHoldLine = rcPerPlayerTimesTwoLine(entryHoldPerPlayerCoins);
+ const entryFeeLine = rcPerPlayerTimesTwoLine(entryFeeCoins);
const prizeCcDisplay = prizeCcLabel.trim() === "" ? "—" : prizeCcLabel;
const hasWinner =
winnerId != null &&
@@ -238,16 +246,16 @@ export function MatchHistoryBattleCard({
Time {createdAtLabel}
- Entry {entryRc}
+ Entry {entryLine}
- Entry hold {entryHoldRc}
+ Entry hold {entryHoldLine}
Prize {prizeCcDisplay} CC
-
- Entry fee {entryFeeRc}
+
+ Entry fee {entryFeeLine}
;
+ counts: Map;
+};
+
+function bumpAgg(agg: PerRemarkAgg, matchId: number, amount: bigint): void {
+ agg.sums.set(matchId, (agg.sums.get(matchId) ?? BigInt(0)) + amount);
+ agg.counts.set(matchId, (agg.counts.get(matchId) ?? 0) + 1);
+}
+
+function avgPerMatch(agg: PerRemarkAgg): Map {
+ const out = new Map();
+ for (const [mid, sum] of agg.sums) {
+ const c = agg.counts.get(mid) ?? 0;
+ if (c > 0) out.set(mid, sum / BigInt(c));
+ }
+ return out;
+}
+
+export type MatchEntryHoldAndFeePerPlayerCoins = {
+ holdPerPlayer: Map;
+ feePerPlayer: Map;
+};
+
/**
- * For each match_id, average `entry_hold` debit amount (same per player when
- * both rows exist; sum/count handles odd counts).
+ * For each match_id, average debit amount for `entry_hold` and `entry_fee`
+ * (per player when two rows exist; sum/count handles odd counts).
*/
-export async function fetchEntryHoldPerPlayerCoinsByMatchIds(
+export async function fetchMatchEntryHoldAndFeePerPlayerCoinsByMatchIds(
supabase: SupabaseClient,
matchIds: number[],
-): Promise