Skip to content

Commit 95335e6

Browse files
committed
ui revamp + fix prediction features
1 parent 17d6163 commit 95335e6

125 files changed

Lines changed: 13904 additions & 1076 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/common/src/logic/predictions/event-simulation.ts

Lines changed: 107 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
*/
77

88
import { predictMatchSimple } from "./match-prediction";
9+
import { DESCRIPTORS } from "../descriptors/descriptor-list";
10+
import { Season } from "../Season";
911

1012
export interface TeamRecord {
1113
teamNumber: number;
@@ -15,6 +17,11 @@ export interface TeamRecord {
1517
rankingPoints: number;
1618
totalPoints: number;
1719
opr: number;
20+
rpBonusRates?: {
21+
movement: number;
22+
goal: number;
23+
pattern: number;
24+
};
1825
}
1926

2027
export interface Match {
@@ -26,6 +33,18 @@ export interface Match {
2633
played: boolean;
2734
redScore?: number;
2835
blueScore?: number;
36+
scores?: {
37+
red?: {
38+
movementRp?: boolean;
39+
goalRp?: boolean;
40+
patternRp?: boolean;
41+
};
42+
blue?: {
43+
movementRp?: boolean;
44+
goalRp?: boolean;
45+
patternRp?: boolean;
46+
};
47+
};
2948
}
3049

3150
export interface SimulationResult {
@@ -46,13 +65,51 @@ export interface EventSimulationConfig {
4665
currentSeason?: number;
4766
}
4867

68+
type RpType = "TotalPoints" | "Record" | "DecodeRP";
69+
70+
function getRpType(currentSeason?: number): RpType {
71+
if (!currentSeason || !(currentSeason in DESCRIPTORS)) return "Record";
72+
return DESCRIPTORS[currentSeason as Season].rankings.rp;
73+
}
74+
75+
function clampRate(rate: number | undefined): number {
76+
if (typeof rate !== "number" || Number.isNaN(rate)) return 0;
77+
if (rate < 0) return 0;
78+
if (rate > 1) return 1;
79+
return rate;
80+
}
81+
82+
function getAllianceBonusRate(
83+
teamNumbers: number[],
84+
teamRpRates: Map<number, TeamRecord["rpBonusRates"]>,
85+
key: "movement" | "goal" | "pattern"
86+
): number {
87+
if (teamNumbers.length === 0) return 0;
88+
const rates = teamNumbers.map((team) => clampRate(teamRpRates.get(team)?.[key]));
89+
const total = rates.reduce((sum, value) => sum + value, 0);
90+
return clampRate(total / rates.length);
91+
}
92+
93+
function rollBonus(rate: number): number {
94+
return Math.random() < rate ? 1 : 0;
95+
}
96+
4997
/**
5098
* Simulate a single match outcome
5199
*/
52100
function simulateMatch(
53101
match: Match,
54-
teamOPRs: Map<number, number>
55-
): { redScore: number; blueScore: number; redWins: boolean } {
102+
teamOPRs: Map<number, number>,
103+
teamRpRates: Map<number, TeamRecord["rpBonusRates"]>,
104+
rpType: RpType
105+
): {
106+
redScore: number;
107+
blueScore: number;
108+
redWins: boolean;
109+
tied: boolean;
110+
redRp: number;
111+
blueRp: number;
112+
} {
56113
const redOPR1 = teamOPRs.get(match.redTeam1) || 0;
57114
const redOPR2 = teamOPRs.get(match.redTeam2) || 0;
58115
const blueOPR1 = teamOPRs.get(match.blueTeam1) || 0;
@@ -68,30 +125,50 @@ function simulateMatch(
68125
const redScore = Math.max(0, Math.round(prediction.predictedRedScore + redNoise));
69126
const blueScore = Math.max(0, Math.round(prediction.predictedBlueScore + blueNoise));
70127

128+
const redWon = redScore > blueScore;
129+
const tied = redScore === blueScore;
130+
131+
let redRp = 0;
132+
let blueRp = 0;
133+
134+
if (rpType === "TotalPoints") {
135+
redRp = redScore;
136+
blueRp = blueScore;
137+
} else {
138+
if (rpType === "DecodeRP") {
139+
redRp = redWon ? 3 : tied ? 1 : 0;
140+
blueRp = !redWon && !tied ? 3 : tied ? 1 : 0;
141+
142+
const redTeams = [match.redTeam1, match.redTeam2].filter((team) => team);
143+
const blueTeams = [match.blueTeam1, match.blueTeam2].filter((team) => team);
144+
145+
const redBonus =
146+
rollBonus(getAllianceBonusRate(redTeams, teamRpRates, "movement")) +
147+
rollBonus(getAllianceBonusRate(redTeams, teamRpRates, "goal")) +
148+
rollBonus(getAllianceBonusRate(redTeams, teamRpRates, "pattern"));
149+
const blueBonus =
150+
rollBonus(getAllianceBonusRate(blueTeams, teamRpRates, "movement")) +
151+
rollBonus(getAllianceBonusRate(blueTeams, teamRpRates, "goal")) +
152+
rollBonus(getAllianceBonusRate(blueTeams, teamRpRates, "pattern"));
153+
154+
redRp += redBonus;
155+
blueRp += blueBonus;
156+
} else {
157+
redRp = redWon ? 2 : tied ? 1 : 0;
158+
blueRp = !redWon && !tied ? 2 : tied ? 1 : 0;
159+
}
160+
}
161+
71162
return {
72163
redScore,
73164
blueScore,
74-
redWins: redScore > blueScore,
165+
redWins: redWon,
166+
tied,
167+
redRp,
168+
blueRp,
75169
};
76170
}
77171

78-
/**
79-
* Calculate ranking points for a match (season-specific)
80-
* This is a simplified version - actual RP calculation varies by season
81-
*/
82-
function calculateRankingPoints(
83-
_score: number,
84-
won: boolean,
85-
tied: boolean,
86-
_season?: number
87-
): number {
88-
// Simplified: 2 RP for win, 1 for tie, 0 for loss
89-
// Real implementation would include bonus RPs based on score thresholds
90-
if (won) return 2;
91-
if (tied) return 1;
92-
return 0;
93-
}
94-
95172
/**
96173
* Run a single simulation iteration
97174
*/
@@ -101,6 +178,12 @@ function runSingleSimulation(
101178
teamOPRs: Map<number, number>,
102179
season?: number
103180
): Map<number, TeamRecord> {
181+
const rpType = getRpType(season);
182+
const teamRpRates = new Map<number, TeamRecord["rpBonusRates"]>();
183+
teams.forEach((team) => {
184+
teamRpRates.set(team.teamNumber, team.rpBonusRates);
185+
});
186+
104187
// Clone team records
105188
const simTeams = new Map<number, TeamRecord>();
106189
teams.forEach((team) => {
@@ -111,14 +194,14 @@ function runSingleSimulation(
111194
const unplayedMatches = matches.filter((m) => !m.played);
112195

113196
for (const match of unplayedMatches) {
114-
const result = simulateMatch(match, teamOPRs);
197+
const result = simulateMatch(match, teamOPRs, teamRpRates, rpType);
115198

116199
const redTeam1 = simTeams.get(match.redTeam1)!;
117200
const redTeam2 = simTeams.get(match.redTeam2)!;
118201
const blueTeam1 = simTeams.get(match.blueTeam1)!;
119202
const blueTeam2 = simTeams.get(match.blueTeam2)!;
120203

121-
const tied = result.redScore === result.blueScore;
204+
const tied = result.tied;
122205
const redWon = result.redWins;
123206
const blueWon = !redWon && !tied;
124207

@@ -129,7 +212,7 @@ function runSingleSimulation(
129212
else team.losses++;
130213

131214
team.totalPoints += result.redScore;
132-
team.rankingPoints += calculateRankingPoints(result.redScore, redWon, tied, season);
215+
team.rankingPoints += result.redRp;
133216
});
134217

135218
// Update records for blue alliance
@@ -139,7 +222,7 @@ function runSingleSimulation(
139222
else team.losses++;
140223

141224
team.totalPoints += result.blueScore;
142-
team.rankingPoints += calculateRankingPoints(result.blueScore, blueWon, tied, season);
225+
team.rankingPoints += result.blueRp;
143226
});
144227
}
145228

packages/web/src/lib/components/Card.svelte

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,30 @@
2121
--side-gap: var(--lg-gap);
2222
2323
position: relative;
24-
width: min-content;
25-
max-width: calc(100% - 2 * var(--side-gap));
26-
min-width: min(var(--requested-width), 100% - 2 * var(--side-gap));
24+
width: 100%;
25+
max-width: min(var(--requested-width), calc(100% - 2 * var(--side-gap)));
26+
min-width: 0;
2727
}
2828
2929
.vis {
3030
background-color: var(--fg-color);
31-
border: 1px solid var(--sep-color);
32-
border-radius: 8px;
31+
border: var(--border-width) solid var(--sep-color);
32+
border-radius: var(--card-radius);
33+
box-shadow: var(--card-shadow);
34+
position: relative;
35+
36+
padding: calc(var(--lg-pad) * 1.1);
37+
padding-left: calc(var(--lg-pad) * 1.1 + 6px);
38+
}
3339
34-
padding: var(--lg-pad);
40+
.vis::before {
41+
content: "";
42+
position: absolute;
43+
top: 0;
44+
bottom: 0;
45+
left: 0;
46+
width: 6px;
47+
background: var(--theme-color);
3548
}
3649
3750
@media (max-width: 550px) {

packages/web/src/lib/components/DataFromFirst.svelte

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
<script lang="ts">
22
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons";
33
import InfoIconRow from "./InfoIconRow.svelte";
4+
import { t } from "$lib/i18n";
45
</script>
56

67
<InfoIconRow icon={faCheckCircle}>
78
<a href="https://ftc-events.firstinspires.org/services/API" tabindex="-1">
8-
Data from <em>FIRST</em>
9+
{$t("data.from", "Data from")} <em>FIRST</em>
910
</a>
1011
</InfoIconRow>
1112

packages/web/src/lib/components/ErrorPage.svelte

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import Card from "./Card.svelte";
55
import Head from "./Head.svelte";
66
import WidthProvider from "./WidthProvider.svelte";
7+
import { t } from "$lib/i18n";
78
89
export let status: number | null = null;
910
export let message: string | null = null;
@@ -12,25 +13,31 @@
1213
$: computedMessage = message ?? $page?.error?.message;
1314
</script>
1415

15-
<Head title="FTCScout" />
16+
<Head title="FTCStats" />
1617

1718
<WidthProvider width="800px">
1819
<Card vis={false}>
1920
<div class="inner">
2021
<h1>{computedStatus}</h1>
2122

2223
{#if computedStatus == 404 && computedMessage == "Not Found"}
23-
<p class="top">Not Found: {$page.url.pathname}</p>
24+
<p class="top">{$t("error.not-found", "Not Found:")} {$page.url.pathname}</p>
2425
<p class="bottom">
25-
The page you are looking for doesn't exist. If we linked you here, reach out at
26+
{$t(
27+
"error.not-found-desc",
28+
"The page you are looking for doesn't exist. If we linked you here, reach out at"
29+
)}
2630
<a href="mailto:{EMAIL}">{EMAIL}</a>.
2731
</p>
2832
{:else if computedStatus == 404}
2933
<p class="top">{computedMessage}</p>
3034
{:else}
3135
<p class="top">{computedMessage}</p>
3236
<p class="bottom">
33-
There appears to be an error. If the issue persists, reach out at
37+
{$t(
38+
"error.generic",
39+
"There appears to be an error. If the issue persists, reach out at"
40+
)}
3441
<a href="mailto:{EMAIL}">{EMAIL}</a>.
3542
</p>
3643
{/if}

packages/web/src/lib/components/Head.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
<link rel="preconnect" href="https://fonts.googleapis.com" />
1414
<link rel="preconnect" href="https://fonts.gstatic.com" />
1515
<link
16-
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;900&display=swap"
16+
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=Newsreader:wght@400;600;700&family=Inter:wght@400;600;700&display=swap"
1717
rel="stylesheet"
1818
/>
1919

packages/web/src/lib/components/Modal.svelte

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
import Fa from "svelte-fa";
44
import { fade } from "svelte/transition";
55
import { clickOutside, quickFocus } from "../util/directives";
6+
import { t } from "$lib/i18n";
67
78
export let shown = false;
89
export let titleText: string;
9-
export let closeText = "Close";
10+
export let closeText: string | null = null;
1011
export let close: (() => void) | null = null;
1112
13+
$: resolvedCloseText = closeText ?? $t("common.close", "Close");
14+
1215
function _close() {
1316
if (close == null) {
1417
shown = false;
@@ -30,7 +33,7 @@
3033
<div class="scroll-wrapper" tabindex="-1" use:quickFocus>
3134
<slot />
3235
</div>
33-
<button class="close" on:click={_close}> {closeText} </button>
36+
<button class="close" on:click={_close}> {resolvedCloseText} </button>
3437
</div>
3538
</div>
3639
{/if}
@@ -55,7 +58,8 @@
5558
.content-wrapper {
5659
background: var(--fg-color);
5760
58-
border-radius: 8px;
61+
border-radius: var(--card-radius);
62+
border: var(--border-width) solid var(--sep-color);
5963
6064
display: flex;
6165
flex-direction: column;
@@ -64,7 +68,7 @@
6468
max-width: 100%;
6569
position: relative;
6670
67-
box-shadow: -2px 2px 10px 3px rgba(0, 0, 0, 10%);
71+
box-shadow: var(--card-shadow);
6872
}
6973
7074
.title-wrapper {
@@ -100,12 +104,13 @@
100104
}
101105
102106
.close {
103-
background: var(--theme-color);
104-
color: var(--theme-text-color);
107+
background: var(--form-bg-color);
108+
color: var(--text-color);
105109
font-weight: bold;
106110
107111
border: none;
108-
border-radius: 0 0 8px 8px;
112+
border-top: var(--border-width) solid var(--sep-color);
113+
border-radius: 0 0 var(--card-radius) var(--card-radius);
109114
110115
padding: var(--lg-pad);
111116

0 commit comments

Comments
 (0)