diff --git a/app/components/StandingsTable.tsx b/app/components/StandingsTable.tsx index 4867c24..8a6be01 100644 --- a/app/components/StandingsTable.tsx +++ b/app/components/StandingsTable.tsx @@ -136,7 +136,7 @@ export function StandingsTable({ )} {row.teamName} - {row.totalPoints.toFixed(1)} + {row.totalPoints.toFixed(2)} {showPlacementBreakdown && ( <> diff --git a/app/components/navbar.tsx b/app/components/navbar.tsx index 1a53726..d6bce77 100644 --- a/app/components/navbar.tsx +++ b/app/components/navbar.tsx @@ -45,6 +45,11 @@ export function Navbar({ isAdmin }: NavbarProps) { How To Play + + + Rules + + {isAdmin && ( @@ -110,6 +115,13 @@ export function Navbar({ isAdmin }: NavbarProps) { > How To Play + setOpen(false)} + > + Rules + {isAdmin && ( formatDate(label as string)} - formatter={(value: number) => [value.toFixed(1), '']} + formatter={(value: number) => [value.toFixed(2), '']} /> {standing.actualPoints !== null && standing.actualPoints !== undefined - ? standing.actualPoints.toFixed(1) - : standing.totalPoints.toFixed(1)} + ? standing.actualPoints.toFixed(2) + : standing.totalPoints.toFixed(2)} {standing.projectedPoints !== null && standing.projectedPoints !== undefined ? (
- {standing.projectedPoints.toFixed(1)} + {standing.projectedPoints.toFixed(2)} {standing.participantsRemaining > 0 && ( - +{(standing.projectedPoints - (standing.actualPoints ?? standing.totalPoints)).toFixed(1)} EV + +{(standing.projectedPoints - (standing.actualPoints ?? standing.totalPoints)).toFixed(2)} EV )}
diff --git a/app/components/standings/TeamScoreBreakdown.tsx b/app/components/standings/TeamScoreBreakdown.tsx index c99bece..3fcec73 100644 --- a/app/components/standings/TeamScoreBreakdown.tsx +++ b/app/components/standings/TeamScoreBreakdown.tsx @@ -93,14 +93,14 @@ export function TeamScoreBreakdown({
- {breakdown.actualPoints.toFixed(1)} + {breakdown.actualPoints.toFixed(2)}
Actual Points
{breakdown.projectedPoints > breakdown.actualPoints && (
- {breakdown.projectedPoints.toFixed(1)} + {breakdown.projectedPoints.toFixed(2)} projected
)} @@ -122,7 +122,7 @@ export function TeamScoreBreakdown({
- {breakdown.actualPoints.toFixed(1)} + {breakdown.actualPoints.toFixed(2)}

From {breakdown.completedCount} finished @@ -138,10 +138,10 @@ export function TeamScoreBreakdown({

- {breakdown.projectedPoints.toFixed(1)} + {breakdown.projectedPoints.toFixed(2)}

- +{(breakdown.projectedPoints - breakdown.actualPoints).toFixed(1)} expected value + +{(breakdown.projectedPoints - breakdown.actualPoints).toFixed(2)} expected value

@@ -250,7 +250,7 @@ export function TeamScoreBreakdown({
-
{sportTotal.toFixed(1)}
+
{sportTotal.toFixed(2)}
points
@@ -291,13 +291,13 @@ export function TeamScoreBreakdown({ {pick.isComplete ? ( - {pick.points > 0 ? pick.points.toFixed(1) : '0.0'} + {pick.points > 0 ? pick.points.toFixed(2) : '0.0'} ) : pick.projectedPoints !== null ? (
Projected: - {pick.projectedPoints.toFixed(1)} + {pick.projectedPoints.toFixed(2)}
) : ( diff --git a/app/components/standings/__tests__/StandingsTable.test.tsx b/app/components/standings/__tests__/StandingsTable.test.tsx index 0ea95aa..46bb86f 100644 --- a/app/components/standings/__tests__/StandingsTable.test.tsx +++ b/app/components/standings/__tests__/StandingsTable.test.tsx @@ -93,9 +93,9 @@ describe("StandingsTable", () => { it("should display total points for each team", () => { renderWithRouter(); - 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", () => { diff --git a/app/components/standings/__tests__/TeamScoreBreakdown.test.tsx b/app/components/standings/__tests__/TeamScoreBreakdown.test.tsx index 7be2f9c..7af6bc2 100644 --- a/app/components/standings/__tests__/TeamScoreBreakdown.test.tsx +++ b/app/components/standings/__tests__/TeamScoreBreakdown.test.tsx @@ -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(); diff --git a/app/routes.ts b/app/routes.ts index 95d7b64..82cf73a 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -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 diff --git a/app/routes/how-to-play.tsx b/app/routes/how-to-play.tsx index f4b3450..48518b0 100644 --- a/app/routes/how-to-play.tsx +++ b/app/routes/how-to-play.tsx @@ -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 (
-

How To Play

+

How To Play

+

+ 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. +

-
+
+ + {/* The Big Picture */}
-

🎯 The Goal

-

- 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! +

The Big Picture

+

+ 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.

+ {/* Building Your Roster */}
-

- πŸ“‹ Building Your Roster -

-

- Your roster has spots for different sports. You'll draft teams or - individual athletes depending on the sport: +

Building Your Roster

+

+ 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.

-
    -
  • Each sport has at least one required spot on your roster
  • -
  • - You'll also get flex spots that can be filled with any sport you - choose -
  • -
  • - Pick wisely - you're stuck with your roster for the whole season! -
  • -
-
- -
-

πŸ† How Scoring Works

-

- Points are awarded based on final standings. The better your pick - finishes, the more points you earn: +

+ Extra draft rounds beyond the number of sports create{" "} + flex spots. + 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.

-
-
-
- πŸ₯‡ 1st Place - 100 pts -
-
- πŸ₯ˆ 2nd Place - 70 pts -
-
- πŸ₯‰ 3rd Place - 50 pts -
-
- 4th Place - 40 pts -
-
- 5th Place - 25 pts -
-
- 6th Place - 25 pts -
-
- 7th Place - 15 pts -
-
- 8th Place - 15 pts -
-
-
-

- Note: If nobody drafted a team that finishes in the top 8, those - points aren't awarded to anyone. -

-
- -
-

- β›³ Special Scoring for Golf, Tennis & Counter-Strike -

-

- Golf, tennis, and Counter-Strike work a bit differently since they - each have multiple major tournaments throughout the year: -

-
    -
  • - Players/teams earn qualifying points at each major tournament - throughout the year -
  • -
  • - After all majors are complete, we add up everyone's qualifying - points -
  • -
  • - The top 8 by total qualifying points then earn the regular scoring - points (100, 70, 50, 40, 25, 25, 15, 15) -
  • -
  • This rewards consistency across all the major tournaments!
  • -
-
-

Qualifying points per major:

-
- 1st Place8 QP - 2nd Place5 QP - 3rd Place3 QP - 4th Place2 QP - 5th Place1 QP -
+
+

+ Example:{" "} + 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. +

+ {/* How Points Work */}
-

🎲 The Draft

-

- Before the season starts, you'll participate in a snake draft: +

How Points Work

+

+ 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: +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Finish + + Points +
1st Place + 100 +
2nd Place + 70 +
3rd Place + 50 +
4th Place + 40 +
5th–6th Place + 25 +
7th–8th Place + 15 +
+
+

+ 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.

-
    -
  • - Snake draft means the order reverses each round - (if you pick last in round 1, you pick first in round 2) -
  • -
  • You'll have a time limit to make each pick
  • -
  • - Once you draft a team or athlete, they're yours for the entire - season -
  • -
  • No trading picks or players - choose carefully!
  • -
+ {/* Major-Based Sports */}
-

πŸ“… During the Season

-

- Once the season starts, it's all about watching your picks compete: +

+ Golf, Tennis, CS2 β€” Sports with Majors +

+

+ Some sports don't have a single championship β€” they have multiple + major tournaments spread throughout the year. For these sports, + players earn{" "} + + qualifying points (QP) + {" "} + at each major: +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Major Finish + + Qualifying Points +
1st + 20 QP +
2nd + 14 QP +
3rd + 10 QP +
4th + 8 QP +
5th–6th + 5 QP +
7th–8th + 3 QP +
9th–12th + 2 QP +
13th–16th + 1 QP +
+
+

+ 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.

-
    -
  • No trades - your roster is locked in
  • -
  • No waivers or free agent pickups
  • -
  • Just sit back and cheer for your teams and athletes!
  • -
+ {/* The Draft */} +
+

The Draft

+

+ 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. +

+ + {/* Chess Clock */} +
+

+ The Chess Clock β€” A Brackt Original +

+

+ Most fantasy drafts use a simple countdown: you get X seconds per + pick and that's it. Brackt does it differently with a{" "} + + Fischer increment timer + + , borrowed from competitive chess. +

+

+ Every participant starts the draft with a personal time bank. + Your clock counts down while it's your turn. When you make a + pick,{" "} + + time is added back + {" "} + to your bank. Any time you don't use carries over to your next + pick. +

+

+ 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. +

+
+ Example:{" "} + 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. +
+
+ + {/* Draft Queue & Autodraft */} +
+

Draft Queue & Autodraft

+

+ Before and during the draft you can build a{" "} + draft queue{" "} + β€” an ordered list of picks you want to make. When it's your turn, + you can pick manually or let your queue guide you. +

+

+ If your time bank runs out,{" "} + autodraft{" "} + 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. +

+
+
+ + {/* During the Season */} +
+

After the Draft

+

+ 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. +

+

+ 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. +

+
+ + {/* Tips */}
-

πŸ’‘ Quick Tips

-
    +

    Quick Tips

    +
    • - βœ“ Diversify: Don't put all your eggs in one - basket - spread your picks across different sports + + Know your sports. + {" "} + Understanding how each sport determines its top 8 (playoffs vs. + regular season vs. bracket) is the biggest edge you can have.
    • - βœ“ Know the format: Some sports score based on - playoffs, others on regular season performance + + Build your queue early. + {" "} + 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.
    • - βœ“ Draft early: The best teams and athletes go - fast! + + Pick fast early on. + {" "} + The chess clock rewards quick decisions. Bank extra time for the + tricky later rounds.
    • - βœ“ Have fun: This is about enjoying sports across - the calendar year + + Flex spots are strategic. + {" "} + Use them to double up on sports you feel confident about, not + just to fill the roster.
-
-

- Ready to compete? + {/* Links */} +

+

+ Want the full breakdown? Read the{" "} + + official rules + + .

- Create a League - +
+
); diff --git a/app/routes/rules.tsx b/app/routes/rules.tsx new file mode 100644 index 0000000..d9b9c48 --- /dev/null +++ b/app/routes/rules.tsx @@ -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 ( +
+

Official Rules

+

+ 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. +

+ +
+ + {/* Object */} +
+

Object

+
+

+ 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. +

+
+ + {/* Rosters */} +
+

Rosters

+
+
+

+ 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. +

+

+ Every participant must draft at least one team or individual from + each sport included in the league. +

+

+ Any draft rounds beyond the number of sports are considered{" "} + flex rounds + . Picks made in flex rounds can be from any sport, including + sports that already have a dedicated roster spot filled. +

+
+
+ + {/* Scoring */} +
+

Scoring

+
+
+ +
+

+ 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. +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Finish + + Points +
1st Place + 100 +
2nd Place + 70 +
3rd Place + 50 +
4th Place + 40 +
5th–6th Place + 25 +
7th–8th Place + 15 +
+
+
+ +

+ 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. +

+ +
+

+ 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. +

+
+

Examples:

+

+ Tie between 2nd and 3rd: (70 + 50) Γ· 2 ={" "} + + 60 points each + +

+

+ Three-way tie for 6th, 7th, and 8th: (25 + 15 + 15) Γ· 3 ={" "} + + 18 points each + +

+
+
+
+
+ + {/* Major-Based Sports Scoring */} +
+

+ Major-Based Sports Scoring +

+
+
+

+ 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. +

+ +

+ At each major tournament, participants earn qualifying points + based on their finish: +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Finish + + Qualifying Points +
1st + 20 QP +
2nd + 14 QP +
3rd + 10 QP +
4th + 8 QP +
5th–6th + 5 QP +
7th–8th + 3 QP +
9th–12th + 2 QP +
13th–16th + 1 QP +
+
+ +

+ Qualifying points are not added to a participant's total league + score directly. They are used only to determine final rankings + within that sport. +

+ +

+ 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. +

+ +

+ 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. +

+
+
+ + {/* Season Standings Tiebreakers */} +
+

+ Season Standings Tiebreakers +

+
+
+

+ If two or more participants finish the season with the same total + score, the following tiebreakers are applied in order: +

+
    +
  1. Most 1st-place finishes across all sports
  2. +
  3. Most 2nd-place finishes
  4. +
  5. Most 3rd-place finishes
  6. +
  7. + This pattern continues down through each place until the tie is + broken +
  8. +
+
+
+ + {/* The Draft */} +
+

The Draft

+
+
+

+ 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. +

+

+ Brackt uses a{" "} + + Fischer increment timer + {" "} + 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. +

+

+ The commissioner selects the draft speed, which determines both + the starting bank and the per-pick increment: +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Speed + + Starting Bank + + Per-Pick Increment +
Fast1 minute+10 seconds
Standard2 minutes+15 seconds
Slow8 hours+1 hour
Very Slow12 hours+1 hour
+
+

+ 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. +

+

Pick trading is not allowed.

+
+
+ + {/* During the Season */} +
+

During the Season

+
+
+

+ Once the draft is complete, rosters are locked for the entire + season. There is no trading of picks or athletes between + participants. +

+

+ There are no waivers or free-agent pickups. Every participant + competes with the roster they drafted. +

+
+
+ +
+
+ ); +}