fpl-solver / frontend /src /utils /fplLogic.js
AnayShukla's picture
Clean Production Release
f7cecf3
// src/utils/fplLogic.js
export const CHIP_CONFIG = {
wc: {
label: "Wildcard", short: "WC", desc: "Unlimited free transfers for 1 GW. FTs reset to max after.",
dot: "bg-yellow-400", text: "text-yellow-400", activeBg: "bg-yellow-500 text-slate-950",
inactiveBg: "bg-slate-800 text-slate-500 hover:bg-slate-700", border: "border-yellow-700/40", badge: "bg-yellow-500/20 text-yellow-300",
},
fh: {
label: "Free Hit", short: "FH", desc: "Unlimited transfers for 1 GW. Squad reverts to original after.",
dot: "bg-orange-400", text: "text-orange-400", activeBg: "bg-orange-500 text-slate-950",
inactiveBg: "bg-slate-800 text-slate-500 hover:bg-slate-700", border: "border-orange-700/40", badge: "bg-orange-500/20 text-orange-300",
},
bb: {
label: "Bench Boost", short: "BB", desc: "All 15 squad players score points this GW.",
dot: "bg-emerald-400", text: "text-emerald-400", activeBg: "bg-emerald-500 text-slate-950",
inactiveBg: "bg-slate-800 text-slate-500 hover:bg-slate-700", border: "border-emerald-700/40", badge: "bg-emerald-500/20 text-emerald-300",
},
tc: {
label: "Triple Captain", short: "TC", desc: "Captain earns 3× points instead of 2× this GW.",
dot: "bg-purple-400", text: "text-purple-400", activeBg: "bg-purple-500 text-white",
inactiveBg: "bg-slate-800 text-slate-500 hover:bg-slate-700", border: "border-purple-700/40", badge: "bg-purple-500/20 text-purple-300",
},
};
export const getPlayerPrice = (p) => {
// 1. Exact FPL Calculation if we have both purchase_price and now_cost
let pp = p.purchase_price !== undefined ? Number(p.purchase_price) : undefined;
let nc = p.now_cost !== undefined ? Number(p.now_cost) : undefined;
// FPL API sometimes sends prices multiplied by 10 (e.g., 43 for 4.3m)
if (pp > 20) pp = pp / 10;
if (nc > 20) nc = nc / 10;
if (pp !== undefined && nc !== undefined) {
if (nc > pp) {
// Gain is 0.1m for every 0.2m increase.
// To avoid floating point errors, multiply by 10, floor the difference/2, and divide by 10.
const diff = Math.round((nc - pp) * 10);
const gain = Math.floor(diff / 2);
return (Math.round(pp * 10) + gain) / 10;
} else {
// FPL Rule: You take full losses on price drops.
return nc;
}
}
// 2. Fallbacks if purchase_price isn't explicitly available
if (p.selling_price !== undefined && p.selling_price !== null) {
const sp = Number(p.selling_price);
return sp > 20 ? sp / 10 : sp;
}
if (p.sell_price !== undefined && p.sell_price !== null) {
const sp = Number(p.sell_price);
return sp > 20 ? sp / 10 : sp;
}
if (p.Price !== undefined) return Number(p.Price);
if (nc !== undefined) return nc;
return 0;
};
export function normalizeBenchGkFirst(teamData, gw) {
if (!teamData.length || teamData.length < 15 || !gw) return teamData;
const starters = teamData.slice(0, 11);
const bench = teamData.slice(11, 15);
const getEV = (p) => Number(p[`${gw}_Pts`]) || 0;
const nonBlank = bench.filter((p) => !p.isBlank);
const blanks = bench.filter((p) => p.isBlank);
const gk = nonBlank.find((p) => p.Pos === "G");
const outfield = nonBlank.filter((p) => p.Pos !== "G");
outfield.sort((a, b) => getEV(b) - getEV(a));
const ordered = gk ? [gk, ...outfield] : [...outfield];
const newBench = [...ordered, ...blanks];
while (newBench.length < 4) {
newBench.push({
ID: `blank_pad_${Date.now()}_${newBench.length}`,
isBlank: true, Pos: "M", Name: "", Team: "", Price: 0,
});
}
return [...starters, ...newBench.slice(0, 4)];
}
export function countSquadByPos(players) {
const c = { G: 0, D: 0, M: 0, F: 0 };
players.filter((p) => !p.isBlank && p.Pos).forEach((p) => {
if (c[p.Pos] !== undefined) c[p.Pos] += 1;
});
return c;
}
export function isValidFplSquad(players) {
const c = countSquadByPos(players);
return c.G === 2 && c.D === 5 && c.M === 5 && c.F === 3;
}
export const getOptimalLayout = (players, gw) => {
if (!players.length || !gw || players.some((p) => p.isBlank)) return null;
const getEV = (p) => Number(p[`${gw}_Pts`]) || 0;
let gks = players.filter((p) => p.Pos === "G").sort((a, b) => getEV(b) - getEV(a));
let defs = players.filter((p) => p.Pos === "D").sort((a, b) => getEV(b) - getEV(a));
let mids = players.filter((p) => p.Pos === "M").sort((a, b) => getEV(b) - getEV(a));
let fwds = players.filter((p) => p.Pos === "F").sort((a, b) => getEV(b) - getEV(a));
const starters = [];
if (gks.length) starters.push(gks.shift());
starters.push(...defs.splice(0, 3), ...mids.splice(0, 2), ...fwds.splice(0, 1));
const remaining = [...defs, ...mids, ...fwds].sort((a, b) => getEV(b) - getEV(a));
starters.push(...remaining.splice(0, 11 - starters.length));
const finalStarters = [
...starters.filter((p) => p.Pos === "G"),
...starters.filter((p) => p.Pos === "D"),
...starters.filter((p) => p.Pos === "M"),
...starters.filter((p) => p.Pos === "F"),
];
const benchGk = gks.length ? gks[0] : null;
const benchRest = remaining.sort((a, b) => getEV(b) - getEV(a));
const bench = benchGk ? [benchGk, ...benchRest] : benchRest;
const topStarters = [...finalStarters].sort((a, b) => getEV(b) - getEV(a));
return {
optimalArray: [...finalStarters, ...bench],
cap: topStarters[0]?.ID,
vice: topStarters[1]?.ID,
};
};