Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions chessfut-be/adapter/http/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package http
import (
"crypto/subtle"
"log/slog"
"net"
"net/http"
"runtime"
"strings"
Expand Down Expand Up @@ -75,8 +76,22 @@ func writeErrorMessage(w http.ResponseWriter, status int, publicMessage string)
}

func clientIP(r *http.Request) string {
if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" {
return strings.Split(fwd, ",")[0]
fwd := r.Header.Get("X-Forwarded-For")
if fwd == "" {
return r.RemoteAddr
}
return r.RemoteAddr

parts := strings.Split(fwd, ",")
for i := len(parts) - 1; i >= 0; i-- {
candidate := strings.TrimSpace(parts[i])
ip := net.ParseIP(candidate)
if ip != nil && !isPrivateOrInternalIP(ip) {
return candidate
}
}
return strings.TrimSpace(parts[0])
}

func isPrivateOrInternalIP(ip net.IP) bool {
return ip.IsPrivate() || ip.IsLoopback()
}
21 changes: 14 additions & 7 deletions chessfut-be/application/service/card_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ const (
longGameMoves = 80.0

maxAnchorDeviation = 0.45

inactivityFactor = 0.65
)

const maxPlausibleFideRating = 2900
Expand Down Expand Up @@ -219,20 +221,25 @@ func effectiveFideRating(title domain.Title, fideRating int) int {
// everything else (attribute shaping, OVR) is built around. FIDE dominates
// when present (or assumed from title) since it's independently verified and
// tightly banded; chess.com activity contributes a smaller nudge on top.
// A player with zero games in every chess.com format has no platform-verified
// performance to blend in — their FIDE-derived score is discounted by
// inactivityFactor instead of granted in full, so an inactive titled account
// never outranks someone who's actually proven their strength on chess.com.
// Without FIDE or a title, chess.com rating alone drives it — no hard
// ceiling, a genuinely strong untitled player can still land high. A player
// with zero recorded rating in every format (an unused/inactive account) is
// floored well below the chesscom sigmoid's ~30 base, so a blank account can
// never outrank someone with real game history.
// ceiling, a genuinely strong untitled player can still land high.
func anchorScore(title domain.Title, stats domain.PlayerStats) float64 {
fide := effectiveFideRating(title, stats.FideRating)
peak := peakRating(stats)

if peak <= 0 {
return statFloor
if fide <= 0 {
return statFloor
}
fideScore := fideBase + fideRange/(1+math.Exp(-fideK*(float64(fide)-fideX0)))
return math.Max(fideScore*inactivityFactor, statFloor)
}

chesscomScore := chesscomBase + chesscomRange/(1+math.Exp(-chesscomK*(peak-chesscomX0)))

fide := effectiveFideRating(title, stats.FideRating)
if fide <= 0 {
return chesscomScore
}
Expand Down
10 changes: 10 additions & 0 deletions chessfut-fe/src/components/panels/ScoutingMetricsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ function fideRatingLabel(card: Card): string | null {
return null;
}

function hasNoRecordedGames(card: Card): boolean {
return card.bullet.rating === 0 && card.blitz.rating === 0 && card.rapid.rating === 0;
}

export function ScoutingMetricsPanel({ card }: { card: Card }) {
const fideLabel = fideRatingLabel(card);
const hasUnreliableRating =
Expand Down Expand Up @@ -79,6 +83,12 @@ export function ScoutingMetricsPanel({ card }: { card: Card }) {
FIDE rating isn&apos;t linked on Chess.com — the {card.title} title&apos;s minimum norm rating was assumed instead.
</p>
)}
{card.title && hasNoRecordedGames(card) && (
<p className="mt-2 text-xs text-white/40">
No recorded Chess.com games in any format — this OVR is based on FIDE strength alone, discounted since
it&apos;s unproven on this platform.
</p>
)}
{hasUnreliableRating && (
<p className="mt-2 text-xs text-white/40">
Some ratings are based on limited recent games and may shift as more are played.
Expand Down
10 changes: 9 additions & 1 deletion chessfut-fe/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@ import type { Card, LeaderboardResponse } from "@/types/card.types";
const API_URL = process.env.NEXT_PUBLIC_API_URL;

async function fetchJSON<T>(path: string): Promise<T> {
const res = await fetch(`${API_URL}${path}`, { cache: "no-store" });
const requestHeaders: HeadersInit = {};
if (typeof window === "undefined") {
const { headers } = await import("next/headers");
const incoming = await headers();
const forwardedFor = incoming.get("x-forwarded-for");
if (forwardedFor) requestHeaders["x-forwarded-for"] = forwardedFor;
}

const res = await fetch(`${API_URL}${path}`, { cache: "no-store", headers: requestHeaders });
if (!res.ok) {
throw new Error(`Request failed: ${res.status}`);
}
Expand Down
Loading