Add rules page, rewrite how-to-play, and fix scoring display (#20)
- Add new /rules route with official league rules covering rosters, scoring, major-based QP scoring, tiebreakers, draft, and season rules - Rewrite how-to-play page with a tutorial/marketing tone, highlighting the Fischer increment draft clock as a novel feature - Add Rules link to navbar (desktop and mobile) - Align QP values in both pages with DEFAULT_QP_VALUES in code - Fix tiebreaker description to match placement-count logic in code - Update all fantasy point displays from toFixed(1) to toFixed(2) across StandingsTable, TeamScoreBreakdown, and PointProgressionChart - Update tests to match new two-decimal-place point display format Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7a8ea60e77
commit
e9798263b5
10 changed files with 721 additions and 166 deletions
|
|
@ -136,7 +136,7 @@ export function StandingsTable({
|
|||
)}
|
||||
<TableCell className="font-medium">{row.teamName}</TableCell>
|
||||
<TableCell className="text-right font-semibold">
|
||||
{row.totalPoints.toFixed(1)}
|
||||
{row.totalPoints.toFixed(2)}
|
||||
</TableCell>
|
||||
{showPlacementBreakdown && (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -45,6 +45,11 @@ export function Navbar({ isAdmin }: NavbarProps) {
|
|||
<Link to="/how-to-play">How To Play</Link>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
|
||||
<Link to="/rules">Rules</Link>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
{isAdmin && (
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
|
||||
|
|
@ -110,6 +115,13 @@ export function Navbar({ isAdmin }: NavbarProps) {
|
|||
>
|
||||
How To Play
|
||||
</Link>
|
||||
<Link
|
||||
to="/rules"
|
||||
className="block px-4 py-2 text-lg font-medium hover:bg-accent rounded-md transition-colors"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
Rules
|
||||
</Link>
|
||||
{isAdmin && (
|
||||
<Link
|
||||
to="/admin"
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ export function PointProgressionChart({ chartData, teams }: PointProgressionChar
|
|||
borderRadius: '6px',
|
||||
}}
|
||||
labelFormatter={(label) => formatDate(label as string)}
|
||||
formatter={(value: number) => [value.toFixed(1), '']}
|
||||
formatter={(value: number) => [value.toFixed(2), '']}
|
||||
/>
|
||||
<Legend
|
||||
wrapperStyle={{ paddingTop: '20px' }}
|
||||
|
|
|
|||
|
|
@ -64,18 +64,18 @@ export function StandingsTable({
|
|||
</TableCell>
|
||||
<TableCell className="text-right font-semibold">
|
||||
{standing.actualPoints !== null && standing.actualPoints !== undefined
|
||||
? standing.actualPoints.toFixed(1)
|
||||
: standing.totalPoints.toFixed(1)}
|
||||
? standing.actualPoints.toFixed(2)
|
||||
: standing.totalPoints.toFixed(2)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{standing.projectedPoints !== null && standing.projectedPoints !== undefined ? (
|
||||
<div className="flex flex-col">
|
||||
<span className="font-semibold text-primary">
|
||||
{standing.projectedPoints.toFixed(1)}
|
||||
{standing.projectedPoints.toFixed(2)}
|
||||
</span>
|
||||
{standing.participantsRemaining > 0 && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
+{(standing.projectedPoints - (standing.actualPoints ?? standing.totalPoints)).toFixed(1)} EV
|
||||
+{(standing.projectedPoints - (standing.actualPoints ?? standing.totalPoints)).toFixed(2)} EV
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -93,14 +93,14 @@ export function TeamScoreBreakdown({
|
|||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-4xl font-bold text-primary">
|
||||
{breakdown.actualPoints.toFixed(1)}
|
||||
{breakdown.actualPoints.toFixed(2)}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Actual Points
|
||||
</div>
|
||||
{breakdown.projectedPoints > breakdown.actualPoints && (
|
||||
<div className="text-xl font-semibold text-electric mt-1">
|
||||
{breakdown.projectedPoints.toFixed(1)}
|
||||
{breakdown.projectedPoints.toFixed(2)}
|
||||
<span className="text-xs text-muted-foreground ml-1">projected</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -122,7 +122,7 @@ export function TeamScoreBreakdown({
|
|||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{breakdown.actualPoints.toFixed(1)}
|
||||
{breakdown.actualPoints.toFixed(2)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
From {breakdown.completedCount} finished
|
||||
|
|
@ -138,10 +138,10 @@ export function TeamScoreBreakdown({
|
|||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-primary">
|
||||
{breakdown.projectedPoints.toFixed(1)}
|
||||
{breakdown.projectedPoints.toFixed(2)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
+{(breakdown.projectedPoints - breakdown.actualPoints).toFixed(1)} expected value
|
||||
+{(breakdown.projectedPoints - breakdown.actualPoints).toFixed(2)} expected value
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
@ -250,7 +250,7 @@ export function TeamScoreBreakdown({
|
|||
</CardDescription>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold">{sportTotal.toFixed(1)}</div>
|
||||
<div className="text-2xl font-bold">{sportTotal.toFixed(2)}</div>
|
||||
<div className="text-xs text-muted-foreground">points</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -291,13 +291,13 @@ export function TeamScoreBreakdown({
|
|||
<TableCell className="text-right">
|
||||
{pick.isComplete ? (
|
||||
<span className="font-semibold">
|
||||
{pick.points > 0 ? pick.points.toFixed(1) : '0.0'}
|
||||
{pick.points > 0 ? pick.points.toFixed(2) : '0.0'}
|
||||
</span>
|
||||
) : pick.projectedPoints !== null ? (
|
||||
<div className="flex flex-col items-end">
|
||||
<span className="text-xs text-muted-foreground">Projected:</span>
|
||||
<span className="font-semibold text-primary">
|
||||
{pick.projectedPoints.toFixed(1)}
|
||||
{pick.projectedPoints.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -93,9 +93,9 @@ describe("StandingsTable", () => {
|
|||
it("should display total points for each team", () => {
|
||||
renderWithRouter(<StandingsTable standings={mockStandings} leagueId={mockLeagueId} seasonId={mockSeasonId} />);
|
||||
|
||||
expect(screen.getByText("250.5")).toBeInTheDocument();
|
||||
expect(screen.getByText("245.0")).toBeInTheDocument();
|
||||
expect(screen.getByText("200.0")).toBeInTheDocument();
|
||||
expect(screen.getByText("250.50")).toBeInTheDocument();
|
||||
expect(screen.getByText("245.00")).toBeInTheDocument();
|
||||
expect(screen.getByText("200.00")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show empty state when no standings", () => {
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ describe("TeamScoreBreakdown", () => {
|
|||
);
|
||||
|
||||
expect(screen.getByText("Test Team")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("150.0").length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText("150.00").length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("should show team rank badge", () => {
|
||||
|
|
@ -244,7 +244,7 @@ describe("TeamScoreBreakdown", () => {
|
|||
|
||||
// NFL total should be 100 (Team A)
|
||||
// F1 total should be 50 (Driver B)
|
||||
const pointCells = screen.getAllByText(/^\d+\.\d$/)
|
||||
const pointCells = screen.getAllByText(/^\d+\.\d\d$/)
|
||||
.map((el) => parseFloat(el.textContent!));
|
||||
|
||||
expect(pointCells).toContain(100.0);
|
||||
|
|
@ -323,9 +323,9 @@ describe("TeamScoreBreakdown", () => {
|
|||
/>
|
||||
);
|
||||
|
||||
// Should have 100.0 and 50.0 for completed picks, and "-" for pending
|
||||
// Should have 100.00 and 50.00 for completed picks, and "-" for pending
|
||||
const pointValues = screen.getAllByRole("cell")
|
||||
.filter((cell) => cell.textContent?.match(/^\d+\.\d$|^-$/));
|
||||
.filter((cell) => cell.textContent?.match(/^\d+\.\d\d$|^-$/));
|
||||
|
||||
expect(pointValues.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
|
@ -432,7 +432,7 @@ describe("TeamScoreBreakdown", () => {
|
|||
|
||||
// Should still show team name and points
|
||||
expect(screen.getByText("Test Team")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("150.0").length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText("150.00").length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// But not show rank badge
|
||||
expect(screen.queryByText(/Rank #/)).not.toBeInTheDocument();
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ export default [
|
|||
route("api/autodraft/update", "routes/api/autodraft.update.ts"),
|
||||
route("user-profile", "routes/user-profile.tsx"),
|
||||
route("how-to-play", "routes/how-to-play.tsx"),
|
||||
route("rules", "routes/rules.tsx"),
|
||||
route("test-socket", "routes/test-socket.tsx"),
|
||||
|
||||
// Admin routes
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { Link } from "react-router";
|
||||
|
||||
export function meta() {
|
||||
return [
|
||||
{ title: "How To Play - Brackt" },
|
||||
|
|
@ -12,185 +14,344 @@ export function meta() {
|
|||
export default function HowToPlay() {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8 max-w-4xl">
|
||||
<h1 className="text-4xl font-bold mb-8">How To Play</h1>
|
||||
<h1 className="text-4xl font-bold mb-2">How To Play</h1>
|
||||
<p className="text-lg text-muted-foreground mb-10">
|
||||
Brackt is a multi-sport fantasy league where you draft athletes and
|
||||
teams across dozens of sports, then watch your roster compete all year
|
||||
long. Here's everything you need to know to get started.
|
||||
</p>
|
||||
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-10">
|
||||
|
||||
{/* The Big Picture */}
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mb-4">🎯 The Goal</h2>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
Draft teams and athletes across multiple sports and earn points
|
||||
based on how well they perform. The player with the most points at
|
||||
the end of the season wins!
|
||||
<h2 className="text-2xl font-semibold mb-3">The Big Picture</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Before the season starts, every participant in your league holds a
|
||||
draft — picking real athletes and teams across a set of sports
|
||||
chosen by your commissioner. Over the following months, those picks
|
||||
compete in their actual seasons. When it's all said and done,
|
||||
whoever earned the most points from their picks wins.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* Building Your Roster */}
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mb-4">
|
||||
📋 Building Your Roster
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground mb-4">
|
||||
Your roster has spots for different sports. You'll draft teams or
|
||||
individual athletes depending on the sport:
|
||||
<h2 className="text-2xl font-semibold mb-3">Building Your Roster</h2>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Your league includes a set of sports selected by the commissioner —
|
||||
think NFL, NBA, golf, Formula 1, tennis, and more. Your roster has
|
||||
one dedicated spot for each sport, so you need to draft at least
|
||||
one pick from every sport in the league.
|
||||
</p>
|
||||
<ul className="list-disc list-inside space-y-2 text-muted-foreground ml-4">
|
||||
<li>Each sport has at least one required spot on your roster</li>
|
||||
<li>
|
||||
You'll also get flex spots that can be filled with any sport you
|
||||
choose
|
||||
</li>
|
||||
<li>
|
||||
Pick wisely - you're stuck with your roster for the whole season!
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mb-4">🏆 How Scoring Works</h2>
|
||||
<p className="text-lg text-muted-foreground mb-4">
|
||||
Points are awarded based on final standings. The better your pick
|
||||
finishes, the more points you earn:
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Extra draft rounds beyond the number of sports create{" "}
|
||||
<span className="font-semibold text-foreground">flex spots</span>.
|
||||
Flex picks can come from any sport — great for grabbing a second
|
||||
player in a sport where you see value, or doubling down on a safe
|
||||
bet.
|
||||
</p>
|
||||
<div className="bg-muted p-6 rounded-lg">
|
||||
<div className="grid grid-cols-2 gap-x-8 gap-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-semibold">🥇 1st Place</span>
|
||||
<span className="text-xl font-bold">100 pts</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-semibold">🥈 2nd Place</span>
|
||||
<span className="text-xl font-bold">70 pts</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-semibold">🥉 3rd Place</span>
|
||||
<span className="text-xl font-bold">50 pts</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-semibold">4th Place</span>
|
||||
<span className="text-xl font-bold">40 pts</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-semibold">5th Place</span>
|
||||
<span className="text-xl font-bold">25 pts</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-semibold">6th Place</span>
|
||||
<span className="text-xl font-bold">25 pts</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-semibold">7th Place</span>
|
||||
<span className="text-xl font-bold">15 pts</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-semibold">8th Place</span>
|
||||
<span className="text-xl font-bold">15 pts</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-4 italic">
|
||||
Note: If nobody drafted a team that finishes in the top 8, those
|
||||
points aren't awarded to anyone.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mb-4">
|
||||
⛳ Special Scoring for Golf, Tennis & Counter-Strike
|
||||
</h2>
|
||||
<p className="text-lg text-muted-foreground mb-4">
|
||||
Golf, tennis, and Counter-Strike work a bit differently since they
|
||||
each have multiple major tournaments throughout the year:
|
||||
</p>
|
||||
<ul className="list-disc list-inside space-y-2 text-muted-foreground ml-4">
|
||||
<li>
|
||||
Players/teams earn qualifying points at each major tournament
|
||||
throughout the year
|
||||
</li>
|
||||
<li>
|
||||
After all majors are complete, we add up everyone's qualifying
|
||||
points
|
||||
</li>
|
||||
<li>
|
||||
The top 8 by total qualifying points then earn the regular scoring
|
||||
points (100, 70, 50, 40, 25, 25, 15, 15)
|
||||
</li>
|
||||
<li>This rewards consistency across all the major tournaments!</li>
|
||||
</ul>
|
||||
<div className="bg-muted p-4 rounded-lg mt-4">
|
||||
<p className="font-semibold mb-2">Qualifying points per major:</p>
|
||||
<div className="grid grid-cols-2 gap-1 text-sm text-muted-foreground">
|
||||
<span>1st Place</span><span className="font-medium">8 QP</span>
|
||||
<span>2nd Place</span><span className="font-medium">5 QP</span>
|
||||
<span>3rd Place</span><span className="font-medium">3 QP</span>
|
||||
<span>4th Place</span><span className="font-medium">2 QP</span>
|
||||
<span>5th Place</span><span className="font-medium">1 QP</span>
|
||||
</div>
|
||||
<div className="bg-muted rounded-lg p-4 text-sm text-muted-foreground">
|
||||
<p>
|
||||
<span className="font-semibold text-foreground">Example:</span>{" "}
|
||||
Your league has 20 sports and 24 draft rounds. That means you
|
||||
fill 20 dedicated sport spots and have 4 flex picks to use
|
||||
however you like.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* How Points Work */}
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mb-4">🎲 The Draft</h2>
|
||||
<p className="text-lg text-muted-foreground mb-4">
|
||||
Before the season starts, you'll participate in a snake draft:
|
||||
<h2 className="text-2xl font-semibold mb-3">How Points Work</h2>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Points are awarded based on where your picks finish — 1st through
|
||||
8th. The better they finish, the more you earn. Your commissioner
|
||||
can adjust point values before the draft, but here's what a typical
|
||||
league looks like:
|
||||
</p>
|
||||
<div className="bg-muted rounded-lg overflow-hidden mb-4">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-border">
|
||||
<th className="text-left p-3 font-semibold text-foreground">
|
||||
Finish
|
||||
</th>
|
||||
<th className="text-right p-3 font-semibold text-foreground">
|
||||
Points
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
<tr>
|
||||
<td className="p-3">1st Place</td>
|
||||
<td className="p-3 text-right font-semibold text-foreground">
|
||||
100
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-3">2nd Place</td>
|
||||
<td className="p-3 text-right font-semibold text-foreground">
|
||||
70
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-3">3rd Place</td>
|
||||
<td className="p-3 text-right font-semibold text-foreground">
|
||||
50
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-3">4th Place</td>
|
||||
<td className="p-3 text-right font-semibold text-foreground">
|
||||
40
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-3">5th–6th Place</td>
|
||||
<td className="p-3 text-right font-semibold text-foreground">
|
||||
25
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-3">7th–8th Place</td>
|
||||
<td className="p-3 text-right font-semibold text-foreground">
|
||||
15
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
If nobody in your league drafted a team that finishes in the top 8,
|
||||
those points aren't awarded to anyone. Drafting rare picks can pay
|
||||
off.
|
||||
</p>
|
||||
<ul className="list-disc list-inside space-y-2 text-muted-foreground ml-4">
|
||||
<li>
|
||||
<strong>Snake draft</strong> means the order reverses each round
|
||||
(if you pick last in round 1, you pick first in round 2)
|
||||
</li>
|
||||
<li>You'll have a time limit to make each pick</li>
|
||||
<li>
|
||||
Once you draft a team or athlete, they're yours for the entire
|
||||
season
|
||||
</li>
|
||||
<li>No trading picks or players - choose carefully!</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{/* Major-Based Sports */}
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mb-4">📅 During the Season</h2>
|
||||
<p className="text-lg text-muted-foreground mb-4">
|
||||
Once the season starts, it's all about watching your picks compete:
|
||||
<h2 className="text-2xl font-semibold mb-3">
|
||||
Golf, Tennis, CS2 — Sports with Majors
|
||||
</h2>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Some sports don't have a single championship — they have multiple
|
||||
major tournaments spread throughout the year. For these sports,
|
||||
players earn{" "}
|
||||
<span className="font-semibold text-foreground">
|
||||
qualifying points (QP)
|
||||
</span>{" "}
|
||||
at each major:
|
||||
</p>
|
||||
<div className="bg-muted rounded-lg overflow-hidden mb-4">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-border">
|
||||
<th className="text-left p-3 font-semibold text-foreground">
|
||||
Major Finish
|
||||
</th>
|
||||
<th className="text-right p-3 font-semibold text-foreground">
|
||||
Qualifying Points
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
<tr>
|
||||
<td className="p-3">1st</td>
|
||||
<td className="p-3 text-right font-semibold text-foreground">
|
||||
20 QP
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-3">2nd</td>
|
||||
<td className="p-3 text-right font-semibold text-foreground">
|
||||
14 QP
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-3">3rd</td>
|
||||
<td className="p-3 text-right font-semibold text-foreground">
|
||||
10 QP
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-3">4th</td>
|
||||
<td className="p-3 text-right font-semibold text-foreground">
|
||||
8 QP
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-3">5th–6th</td>
|
||||
<td className="p-3 text-right font-semibold text-foreground">
|
||||
5 QP
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-3">7th–8th</td>
|
||||
<td className="p-3 text-right font-semibold text-foreground">
|
||||
3 QP
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-3">9th–12th</td>
|
||||
<td className="p-3 text-right font-semibold text-foreground">
|
||||
2 QP
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-3">13th–16th</td>
|
||||
<td className="p-3 text-right font-semibold text-foreground">
|
||||
1 QP
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p className="text-muted-foreground">
|
||||
QP don't go straight to your score — they determine the final
|
||||
ranking within that sport. Once all the majors are done, total QP
|
||||
are added up, and whoever's ranked 1st through 8th earns the
|
||||
standard league points. Consistency across majors is the key.
|
||||
</p>
|
||||
<ul className="list-disc list-inside space-y-2 text-muted-foreground ml-4">
|
||||
<li>No trades - your roster is locked in</li>
|
||||
<li>No waivers or free agent pickups</li>
|
||||
<li>Just sit back and cheer for your teams and athletes!</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{/* The Draft */}
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mb-3">The Draft</h2>
|
||||
<p className="text-muted-foreground mb-6">
|
||||
The draft is a snake draft — pick order reverses every round. If
|
||||
you pick last in round 1, you pick first in round 2. No trading
|
||||
picks, and once you make a selection it's yours for the whole
|
||||
season.
|
||||
</p>
|
||||
|
||||
{/* Chess Clock */}
|
||||
<div className="border rounded-lg p-5 mb-6 space-y-3">
|
||||
<h3 className="text-lg font-semibold">
|
||||
The Chess Clock — A Brackt Original
|
||||
</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Most fantasy drafts use a simple countdown: you get X seconds per
|
||||
pick and that's it. Brackt does it differently with a{" "}
|
||||
<span className="font-semibold text-foreground">
|
||||
Fischer increment timer
|
||||
</span>
|
||||
, borrowed from competitive chess.
|
||||
</p>
|
||||
<p className="text-muted-foreground">
|
||||
Every participant starts the draft with a personal time bank.
|
||||
Your clock counts down while it's your turn. When you make a
|
||||
pick,{" "}
|
||||
<span className="font-semibold text-foreground">
|
||||
time is added back
|
||||
</span>{" "}
|
||||
to your bank. Any time you don't use carries over to your next
|
||||
pick.
|
||||
</p>
|
||||
<p className="text-muted-foreground">
|
||||
Pick quickly early on and you'll build up a bigger bank for the
|
||||
later rounds when decisions get harder. Deliberate too long and
|
||||
your bank shrinks. It rewards decisiveness without penalizing you
|
||||
for taking a moment when you really need it.
|
||||
</p>
|
||||
<div className="bg-muted rounded-md p-3 text-sm text-muted-foreground">
|
||||
<span className="font-semibold text-foreground">Example:</span>{" "}
|
||||
On Standard speed, you start with 2 minutes. You make your first
|
||||
pick in 30 seconds — 15 seconds are added back, leaving you with
|
||||
1 minute 45 seconds for your next turn. Keep picking fast and
|
||||
that bank grows.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Draft Queue & Autodraft */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-lg font-semibold">Draft Queue & Autodraft</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Before and during the draft you can build a{" "}
|
||||
<span className="font-semibold text-foreground">draft queue</span>{" "}
|
||||
— an ordered list of picks you want to make. When it's your turn,
|
||||
you can pick manually or let your queue guide you.
|
||||
</p>
|
||||
<p className="text-muted-foreground">
|
||||
If your time bank runs out,{" "}
|
||||
<span className="font-semibold text-foreground">autodraft</span>{" "}
|
||||
kicks in automatically. It'll grab the first available pick from
|
||||
your queue. If your queue is empty or everything in it has
|
||||
already been taken, it'll grab from the top of the available player pool.
|
||||
Keep your queue updated and you'll never get stuck with a random
|
||||
pick.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* During the Season */}
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mb-3">After the Draft</h2>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Once the draft ends, your roster is locked. No trades, no waivers,
|
||||
no pickups. Just watch your picks compete across the year and see
|
||||
how your draft decisions play out. Points accumulate as sports
|
||||
seasons wrap up, and standings update in real time.
|
||||
</p>
|
||||
<p className="text-muted-foreground">
|
||||
If there's a tie in the final standings, it's broken by who has
|
||||
more 1st-place finishes. If that's still a tie, it goes to
|
||||
2nd-place finishes, then 3rd, and so on.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* Tips */}
|
||||
<section className="bg-primary/5 p-6 rounded-lg border border-primary/20">
|
||||
<h2 className="text-2xl font-semibold mb-4">💡 Quick Tips</h2>
|
||||
<ul className="space-y-2 text-muted-foreground">
|
||||
<h2 className="text-xl font-semibold mb-4">Quick Tips</h2>
|
||||
<ul className="space-y-3 text-muted-foreground">
|
||||
<li>
|
||||
✓ <strong>Diversify:</strong> Don't put all your eggs in one
|
||||
basket - spread your picks across different sports
|
||||
<span className="font-semibold text-foreground">
|
||||
Know your sports.
|
||||
</span>{" "}
|
||||
Understanding how each sport determines its top 8 (playoffs vs.
|
||||
regular season vs. bracket) is the biggest edge you can have.
|
||||
</li>
|
||||
<li>
|
||||
✓ <strong>Know the format:</strong> Some sports score based on
|
||||
playoffs, others on regular season performance
|
||||
<span className="font-semibold text-foreground">
|
||||
Build your queue early.
|
||||
</span>{" "}
|
||||
The draft moves fast. Having a ranked queue ready means you'll
|
||||
always get a pick you're happy with, even if you step away.
|
||||
</li>
|
||||
<li>
|
||||
✓ <strong>Draft early:</strong> The best teams and athletes go
|
||||
fast!
|
||||
<span className="font-semibold text-foreground">
|
||||
Pick fast early on.
|
||||
</span>{" "}
|
||||
The chess clock rewards quick decisions. Bank extra time for the
|
||||
tricky later rounds.
|
||||
</li>
|
||||
<li>
|
||||
✓ <strong>Have fun:</strong> This is about enjoying sports across
|
||||
the calendar year
|
||||
<span className="font-semibold text-foreground">
|
||||
Flex spots are strategic.
|
||||
</span>{" "}
|
||||
Use them to double up on sports you feel confident about, not
|
||||
just to fill the roster.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="text-center pt-8">
|
||||
<p className="text-xl text-muted-foreground mb-6">
|
||||
Ready to compete?
|
||||
{/* Links */}
|
||||
<section className="flex flex-col sm:flex-row gap-4 items-center justify-between pt-2">
|
||||
<p className="text-muted-foreground">
|
||||
Want the full breakdown? Read the{" "}
|
||||
<Link to="/rules" className="underline underline-offset-4 hover:text-foreground transition-colors">
|
||||
official rules
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<a
|
||||
href="/leagues/new"
|
||||
className="inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground shadow hover:bg-primary/90 h-11 px-8 text-base"
|
||||
<Link
|
||||
to="/leagues/new"
|
||||
className="inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground shadow hover:bg-primary/90 h-11 px-8 text-base whitespace-nowrap"
|
||||
>
|
||||
Create a League
|
||||
</a>
|
||||
</Link>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
381
app/routes/rules.tsx
Normal file
381
app/routes/rules.tsx
Normal file
|
|
@ -0,0 +1,381 @@
|
|||
export function meta() {
|
||||
return [
|
||||
{ title: "Rules - Brackt" },
|
||||
{
|
||||
name: "description",
|
||||
content: "Official rules for Brackt multi-sport fantasy leagues",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default function Rules() {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8 max-w-4xl">
|
||||
<h1 className="text-4xl font-bold mb-2">Official Rules</h1>
|
||||
<p className="text-muted-foreground mb-10">
|
||||
These rules govern all Brackt leagues. Commissioners may adjust scoring
|
||||
values and draft settings within the options available in league
|
||||
settings, but the core structure below applies to every league.
|
||||
</p>
|
||||
|
||||
<div className="space-y-10">
|
||||
|
||||
{/* Object */}
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mb-1">Object</h2>
|
||||
<div className="border-b mb-4" />
|
||||
<p className="text-muted-foreground">
|
||||
The goal is to draft athletes and teams across multiple sports, then
|
||||
earn points based on how they finish at the end of their respective
|
||||
seasons. The participant with the most points at the end of the
|
||||
league season wins.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* Rosters */}
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mb-1">Rosters</h2>
|
||||
<div className="border-b mb-4" />
|
||||
<div className="space-y-3 text-muted-foreground">
|
||||
<p>
|
||||
The commissioner sets which sports are included in the league and
|
||||
how many draft rounds are held. The number of draft rounds must be
|
||||
at least equal to the number of sports in the league.
|
||||
</p>
|
||||
<p>
|
||||
Every participant must draft at least one team or individual from
|
||||
each sport included in the league.
|
||||
</p>
|
||||
<p>
|
||||
Any draft rounds beyond the number of sports are considered{" "}
|
||||
<span className="font-semibold text-foreground">flex rounds</span>
|
||||
. Picks made in flex rounds can be from any sport, including
|
||||
sports that already have a dedicated roster spot filled.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Scoring */}
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mb-1">Scoring</h2>
|
||||
<div className="border-b mb-4" />
|
||||
<div className="space-y-6 text-muted-foreground">
|
||||
|
||||
<div>
|
||||
<p className="mb-3">
|
||||
Points are awarded based on where a drafted team or individual
|
||||
finishes in their sport. The default point values are listed
|
||||
below. Commissioners may adjust these values before the draft
|
||||
begins.
|
||||
</p>
|
||||
<div className="bg-muted rounded-lg overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-border">
|
||||
<th className="text-left p-3 font-semibold text-foreground">
|
||||
Finish
|
||||
</th>
|
||||
<th className="text-right p-3 font-semibold text-foreground">
|
||||
Points
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
<tr>
|
||||
<td className="p-3">1st Place</td>
|
||||
<td className="p-3 text-right font-semibold text-foreground">
|
||||
100
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-3">2nd Place</td>
|
||||
<td className="p-3 text-right font-semibold text-foreground">
|
||||
70
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-3">3rd Place</td>
|
||||
<td className="p-3 text-right font-semibold text-foreground">
|
||||
50
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-3">4th Place</td>
|
||||
<td className="p-3 text-right font-semibold text-foreground">
|
||||
40
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-3">5th–6th Place</td>
|
||||
<td className="p-3 text-right font-semibold text-foreground">
|
||||
25
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-3">7th–8th Place</td>
|
||||
<td className="p-3 text-right font-semibold text-foreground">
|
||||
15
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
If a team or individual finishes in a point-scoring position but
|
||||
was not drafted by anyone, those points are not awarded to anyone.
|
||||
A participant who drafted the next-highest finisher still earns
|
||||
zero points for that position.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<p className="mb-3">
|
||||
When two or more teams or individuals tie for a point-scoring
|
||||
position within a sport, the points for all tied positions are
|
||||
combined and split equally among them, rounded to the nearest
|
||||
whole point.
|
||||
</p>
|
||||
<div className="bg-muted rounded-lg p-4 text-sm space-y-2">
|
||||
<p className="font-semibold text-foreground">Examples:</p>
|
||||
<p>
|
||||
Tie between 2nd and 3rd: (70 + 50) ÷ 2 ={" "}
|
||||
<span className="font-semibold text-foreground">
|
||||
60 points each
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
Three-way tie for 6th, 7th, and 8th: (25 + 15 + 15) ÷ 3 ={" "}
|
||||
<span className="font-semibold text-foreground">
|
||||
18 points each
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Major-Based Sports Scoring */}
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mb-1">
|
||||
Major-Based Sports Scoring
|
||||
</h2>
|
||||
<div className="border-b mb-4" />
|
||||
<div className="space-y-4 text-muted-foreground">
|
||||
<p>
|
||||
Some sports — such as golf, tennis, and Counter-Strike — are
|
||||
scored across multiple major tournaments throughout the year
|
||||
rather than a single season-ending event. These sports use a
|
||||
qualifying point (QP) system.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
At each major tournament, participants earn qualifying points
|
||||
based on their finish:
|
||||
</p>
|
||||
|
||||
<div className="bg-muted rounded-lg overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-border">
|
||||
<th className="text-left p-3 font-semibold text-foreground">
|
||||
Finish
|
||||
</th>
|
||||
<th className="text-right p-3 font-semibold text-foreground">
|
||||
Qualifying Points
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
<tr>
|
||||
<td className="p-3">1st</td>
|
||||
<td className="p-3 text-right font-semibold text-foreground">
|
||||
20 QP
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-3">2nd</td>
|
||||
<td className="p-3 text-right font-semibold text-foreground">
|
||||
14 QP
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-3">3rd</td>
|
||||
<td className="p-3 text-right font-semibold text-foreground">
|
||||
10 QP
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-3">4th</td>
|
||||
<td className="p-3 text-right font-semibold text-foreground">
|
||||
8 QP
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-3">5th–6th</td>
|
||||
<td className="p-3 text-right font-semibold text-foreground">
|
||||
5 QP
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-3">7th–8th</td>
|
||||
<td className="p-3 text-right font-semibold text-foreground">
|
||||
3 QP
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-3">9th–12th</td>
|
||||
<td className="p-3 text-right font-semibold text-foreground">
|
||||
2 QP
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-3">13th–16th</td>
|
||||
<td className="p-3 text-right font-semibold text-foreground">
|
||||
1 QP
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Qualifying points are not added to a participant's total league
|
||||
score directly. They are used only to determine final rankings
|
||||
within that sport.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Once the final major tournament is complete, qualifying points
|
||||
from all majors are totaled. Those cumulative totals determine
|
||||
the sport's final rankings, and the standard point values from
|
||||
the Scoring section are then awarded accordingly.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Ties in qualifying points are split equally and rounded to the
|
||||
nearest hundredth of a qualifying point before final rankings are
|
||||
determined. The same tie-splitting logic from the Scoring section
|
||||
then applies when awarding league points.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Season Standings Tiebreakers */}
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mb-1">
|
||||
Season Standings Tiebreakers
|
||||
</h2>
|
||||
<div className="border-b mb-4" />
|
||||
<div className="space-y-3 text-muted-foreground">
|
||||
<p>
|
||||
If two or more participants finish the season with the same total
|
||||
score, the following tiebreakers are applied in order:
|
||||
</p>
|
||||
<ol className="list-decimal list-inside space-y-2 ml-4">
|
||||
<li>Most 1st-place finishes across all sports</li>
|
||||
<li>Most 2nd-place finishes</li>
|
||||
<li>Most 3rd-place finishes</li>
|
||||
<li>
|
||||
This pattern continues down through each place until the tie is
|
||||
broken
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* The Draft */}
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mb-1">The Draft</h2>
|
||||
<div className="border-b mb-4" />
|
||||
<div className="space-y-3 text-muted-foreground">
|
||||
<p>
|
||||
The draft is a snake draft. Draft order reverses at the start of
|
||||
each new round — if you pick last in round 1, you pick first in
|
||||
round 2, and so on.
|
||||
</p>
|
||||
<p>
|
||||
Brackt uses a{" "}
|
||||
<span className="font-semibold text-foreground">
|
||||
Fischer increment timer
|
||||
</span>{" "}
|
||||
for all drafts. Each participant has a personal time bank. Your
|
||||
clock counts down while it's your turn, and a fixed amount of
|
||||
time is added back to your bank each time you make a pick. Time
|
||||
not used on one pick carries over to future picks.
|
||||
</p>
|
||||
<p>
|
||||
The commissioner selects the draft speed, which determines both
|
||||
the starting bank and the per-pick increment:
|
||||
</p>
|
||||
<div className="bg-muted rounded-lg overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-border">
|
||||
<th className="text-left p-3 font-semibold text-foreground">
|
||||
Speed
|
||||
</th>
|
||||
<th className="text-right p-3 font-semibold text-foreground">
|
||||
Starting Bank
|
||||
</th>
|
||||
<th className="text-right p-3 font-semibold text-foreground">
|
||||
Per-Pick Increment
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
<tr>
|
||||
<td className="p-3">Fast</td>
|
||||
<td className="p-3 text-right">1 minute</td>
|
||||
<td className="p-3 text-right">+10 seconds</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-3">Standard</td>
|
||||
<td className="p-3 text-right">2 minutes</td>
|
||||
<td className="p-3 text-right">+15 seconds</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-3">Slow</td>
|
||||
<td className="p-3 text-right">8 hours</td>
|
||||
<td className="p-3 text-right">+1 hour</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-3">Very Slow</td>
|
||||
<td className="p-3 text-right">12 hours</td>
|
||||
<td className="p-3 text-right">+1 hour</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p>
|
||||
If a participant's time bank runs out, the system will
|
||||
automatically draft on their behalf. Autodraft picks from the
|
||||
participant's pre-set draft queue first. If the queue is empty or
|
||||
all queued picks have already been taken, the system selects from
|
||||
the top of the available player pool.
|
||||
</p>
|
||||
<p>Pick trading is not allowed.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* During the Season */}
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mb-1">During the Season</h2>
|
||||
<div className="border-b mb-4" />
|
||||
<div className="space-y-3 text-muted-foreground">
|
||||
<p>
|
||||
Once the draft is complete, rosters are locked for the entire
|
||||
season. There is no trading of picks or athletes between
|
||||
participants.
|
||||
</p>
|
||||
<p>
|
||||
There are no waivers or free-agent pickups. Every participant
|
||||
competes with the roster they drafted.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue