diff --git a/config/scouting/2026/graphs_2026.py b/config/scouting/2026/graphs_2026.py
new file mode 100644
index 0000000..db15c3d
--- /dev/null
+++ b/config/scouting/2026/graphs_2026.py
@@ -0,0 +1,448 @@
+'''
+python graphs_2026.py
+
+ --event tba/frc event key
+ --csv filename of tpw data
+ --baseFilePath base filesystem path
+ --team team to be analyzed | !! Only use for --mode 0 !!
+ --teamList comma separated list of teams to by analyzed | !! Only use for --mode 1 or 2 !! | ex. --teamList 254,1679,1072 --mode 2
+ --mode 0 - fuel scoring over time | 1 - team(s) avg score spread radar chart | 2 - team(s) percentage scoring + performance of best score/perf | 3 - auto vs teleop fuel chart
+ --theme null/0 - light theme for --mode 1 & 2 | 1 - dark theme for --mode 1 & 2
+
+tba cached data must be in file named: event-tba.json
+
+returns graph/chart to html file:
+
+ mode filename
+ 0 [event]-[team1]-fuel_analysis.html
+ 1 [team1]_[team2]_[team3]_etc_spreadChart.html
+ 2 [team1]_[taem2]_[team3]_etc_CTBchart.html
+ 3 [event]-[team1]-auto_vs_teleop.html
+
+caches parsed data to json file:
+
+ filename: parsed_tpw_data_[event].json
+'''
+
+import numpy as np
+from collections import OrderedDict
+import json
+import os
+import math
+import sys
+import csv
+import matplotlib.pyplot as plt
+import mpld3
+from mpld3 import plugins
+import matplotlib.pyplot as plt
+import plotly
+import plotly.express as plty
+import plotly.graph_objects as go
+import plotly.io as pio
+import pandas as pd
+
+rawArgs = sys.argv[1:]
+args = {}
+for i in range(len(rawArgs)):
+ if rawArgs[i] == "--event" and "event" not in args:
+ args["event"] = rawArgs[i + 1]
+ i += 1
+ elif rawArgs[i] == "--csv" and "csv" not in args:
+ args["csv"] = rawArgs[i + 1]
+ i += 1
+ elif rawArgs[i] == "--baseFilePath" and "baseFilePath" not in args:
+ args["baseFilePath"] = rawArgs[i + 1]
+ i += 1
+ elif rawArgs[i] == "--team" and "team" not in args:
+ args["team"] = rawArgs[i + 1]
+ i += 1
+ elif rawArgs[i] == "--teamList" and "teamList" not in args:
+ args["teamList"] = rawArgs[i + 1].split(',')
+ i += 1
+ elif rawArgs[i] == "--mode" and "mode" not in args:
+ args["mode"] = rawArgs[i + 1]
+ i += 1
+ elif rawArgs[i] == "--theme" and "theme" not in args:
+ args["theme"] = rawArgs[i + 1]
+ i += 1
+
+event = args["event"]
+base = args["baseFilePath"]
+tpw_csv = args["csv"]
+
+template = 'plotly'
+if 'theme' in args and int(args["theme"]) == 0:
+ template = 'plotly'
+elif 'theme' in args and int(args["theme"]) == 1:
+ template = 'plotly_dark'
+
+def avg(data):
+ if data != []:
+ data = np.array([data])
+ return np.mean(data)
+ else:
+ return 0
+
+def std(data):
+ if data != []:
+ data = np.array([data])
+ return np.std(data)
+ else:
+ return 0
+
+def max(data):
+ if data != []:
+ data = np.array([data])
+ return np.max(data)
+ else:
+ return 0
+
+def min(data):
+ if data != []:
+ data = np.array([data])
+ return np.min(data)
+ else:
+ return 0
+
+tpw_path = base + tpw_csv
+
+def getData():
+ team_data = OrderedDict()
+ data_length = 0
+
+ if os.path.exists(tpw_path):
+ with open(tpw_path, "r") as file:
+ TPW_data = csv.DictReader(file)
+ for x in TPW_data:
+ data_length += 1
+ if x['team'] not in team_data:
+ team_data[x['team']] = [x]
+ else:
+ team_data[x['team']].append(x)
+ else:
+ raise Exception("Could not find TPW file")
+
+ parsed_tpw_data = OrderedDict()
+ for team, dict in team_data.items():
+ afgps = list()
+ tfgps = list()
+ afgpts = {}
+ tfgpts = {}
+ l1climbs = list()
+ egcpts = list() # endgame climb points
+ defe = list()
+ speed = list()
+ driver = list()
+ stab = list()
+ inta = list()
+ uptime = list()
+ avg_auto_points = list()
+ avg_tele_points = list()
+ matches = {}
+
+ for x in dict:
+ auto_fuel_pieces = x['auto fuel scoring'][1:len(x['auto fuel scoring']) - 1].split(", ")
+ tele_fuel_pieces = x['teleop fuel scoring'][1:len(x['teleop fuel scoring']) - 1].split(", ")
+ afgps.append(auto_fuel_pieces)
+ tfgps.append(tele_fuel_pieces)
+ l1climbs.append(x.get('l1 climb', '').lower() == 'true' or x.get('l1 climb', '') == True)
+
+ c_lev = int(x['climb level'])
+ if c_lev == 0:
+ egcpts.append(0)
+ elif c_lev == 1:
+ egcpts.append(10)
+ elif c_lev == 2:
+ egcpts.append(20)
+ elif c_lev >= 3:
+ egcpts.append(30)
+
+ try:
+ defe.append(int(x["defense skill"]))
+ speed.append(int(x["speed"]))
+ stab.append(int(x["stability"]))
+ inta.append(int(x["intake consistency"]))
+ driver.append(int(x["driver skill"]))
+ uptime.append(153000 - int(x["brick time"]))
+ except:
+ defe.append(3)
+ speed.append(3)
+ stab.append(3)
+ inta.append(3)
+ driver.append(3)
+ uptime.append(100)
+
+ try:
+ matches[x['match']][(x[''])] = {
+ 'auto': auto_fuel_pieces,
+ 'teleop': tele_fuel_pieces
+ }
+ except:
+ matches[x['match']] = {x['']: {
+ 'auto': auto_fuel_pieces,
+ 'teleop': tele_fuel_pieces
+ }}
+
+ for i in range(len(afgps)):
+ afgpts[i] = 0
+ for j in range(len(afgps[i])):
+ val = afgps[i][j]
+ if val == "fsa":
+ afgpts[i] = afgpts.get(i, 0) + 1
+ else:
+ afgpts[i] = afgpts.get(i, 0) + 0
+ if l1climbs[i]:
+ afgpts[i] = afgpts.get(i, 0) + 15
+ avg_auto_points.append(afgpts[i])
+ for i in range(len(tfgps)):
+ tfgpts[i] = 0
+ for j in range(len(tfgps[i])):
+ val = tfgps[i][j]
+ if val == "fsa":
+ tfgpts[i] = tfgpts.get(i, 0) + 1
+ elif val == "fp":
+ tfgpts[i] = tfgpts.get(i, 0) + 0
+ else:
+ tfgpts[i] = tfgpts.get(i, 0) + 0
+ avg_tele_points.append(tfgpts[i])
+
+ data_tpw = OrderedDict()
+ data_tpw['avg-tele'] = avg(avg_tele_points)
+ data_tpw['avg-auto'] = avg(avg_auto_points)
+ data_tpw['avg-climb'] = avg(egcpts)
+ data_tpw['avg-def'] = avg(defe)
+ data_tpw['avg-driv'] = avg(driver)
+ data_tpw['avg-speed'] = avg(speed)
+ data_tpw['avg-stab'] = avg(stab)
+ data_tpw['avg-inta'] = avg(inta)
+ data_tpw['avg-upt'] = avg(uptime)
+ data_tpw['matches'] = matches
+ data_tpw['tpw-std'] = std(avg_auto_points) + std(avg_tele_points) + std(egcpts)
+ data_tpw["tpw-score"] = data_tpw['avg-auto'] + data_tpw['avg-tele'] + data_tpw['avg-climb']
+ parsed_tpw_data[team] = data_tpw
+
+ with open(base + 'parsed_tpw_data_'+event+'.json', 'w') as f:
+ f.write(json.dumps({'lines': data_length, 'data': parsed_tpw_data}, default=int))
+ f.close()
+ return parsed_tpw_data
+
+def getDataLength():
+ data_length = 0
+ if os.path.exists(tpw_path):
+ with open(tpw_path, "r") as file:
+ TPW_data = csv.DictReader(file)
+ for x in TPW_data:
+ data_length += 1
+ else:
+ raise Exception("Could not find TPW file")
+
+ return data_length
+
+
+if os.path.exists(base + 'parsed_tpw_data_'+event+'.json'):
+ with open(base + 'parsed_tpw_data_'+event+'.json') as f:
+ loaded = json.loads(f.read())
+ if loaded['lines'] == getDataLength():
+ parsed_tpw_data = loaded['data']
+ f.close()
+ else:
+ f.close()
+ parsed_tpw_data = getData()
+else:
+ parsed_tpw_data = getData()
+
+def shotSummary(team):
+ data = parsed_tpw_data[str(team)]
+ game_pieces = pd.DataFrame(columns = ["Match", "AutoScored", "TeleopScored", "TeleopPassed", "Total Fuel"])
+ for r in data['matches']:
+ auto_scored_list = list()
+ tele_scored_list = list()
+ tele_passed_list = list()
+ for s in data['matches'][r]:
+ entry = data['matches'][r][s]
+ if isinstance(entry, dict):
+ a_scored = 0
+ for e in entry.get('auto', []):
+ if e == "fsa":
+ a_scored += 1
+ t_scored = 0
+ t_passed = 0
+ for e in entry.get('teleop', []):
+ if e == "fsa":
+ t_scored += 1
+ elif e == "fp":
+ t_passed += 1
+ auto_scored_list.append(a_scored)
+ tele_scored_list.append(t_scored)
+ tele_passed_list.append(t_passed)
+ else:
+ auto_scored_list.append(0)
+ tele_scored_list.append(0)
+ tele_passed_list.append(0)
+ auto_avg = avg(auto_scored_list)
+ tele_avg = avg(tele_scored_list)
+ pass_avg = avg(tele_passed_list)
+ total = auto_avg + tele_avg + pass_avg
+ game_pieces.loc[r] = [r, auto_avg, tele_avg, pass_avg, total]
+ return game_pieces
+
+def radarChartSpread(teams):
+ categories = ['auto points', 'teleop points', 'climb points', 'total points', 'auto points']
+ fig = go.Figure()
+ fn = ''
+ for team in teams:
+ team = str(team)
+ fn += team + '-'
+
+ s_team = [parsed_tpw_data[team]["avg-auto"], parsed_tpw_data[team]["avg-tele"], parsed_tpw_data[team]["avg-climb"], parsed_tpw_data[team]["tpw-score"], parsed_tpw_data[team]["avg-auto"]]
+
+ fig.add_trace(go.Scatterpolar(
+ r=s_team,
+ theta=categories,
+ fill='toself',
+ name=team
+ ))
+
+ fig.update_layout(
+ title_font_family="Sitka",
+ title_font_color="green",
+ title = "Average Point Spread",
+ width = 600,
+ height = 600,
+ template=template)
+
+ plotly.offline.plot(fig, filename=base + event + "-" + fn + 'standard-radar.html', auto_open=False)
+
+def getBest():
+ auto = 0
+ tele = 0
+ total = 0
+ drive = 0
+ defe = 0
+ stab = 0
+ upt = 0
+ speed = 0
+ inta = 0
+
+ for team, dict in parsed_tpw_data.items():
+ if dict["avg-auto"] > auto:
+ auto = dict["avg-auto"]
+ if dict["avg-tele"] > tele:
+ tele = dict["avg-tele"]
+ if dict["tpw-score"] > total:
+ total = dict["tpw-score"]
+ if dict["avg-driv"] > drive:
+ drive = dict["avg-driv"]
+ if dict["avg-def"] > defe:
+ defe = dict["avg-def"]
+ if dict["avg-stab"] > stab:
+ stab = dict["avg-stab"]
+ if dict["avg-upt"] > upt:
+ upt = dict["avg-upt"]
+ if dict["avg-speed"] > speed:
+ speed = dict["avg-speed"]
+ if dict["avg-inta"] > inta:
+ inta = dict["avg-inta"]
+ return [auto, tele, total, drive, defe, stab, upt, speed, inta]
+
+#CTB: Compare To Best
+def radarChartCTB(teams):
+ categories = ["auto pts", "teleop pts", "total pts", "drive skill", "defense", "stability", "uptime", "speed", "intake", "auto pts"]
+ maxes = getBest()
+ for i in range(len(maxes)):
+ if(maxes[i] == 0):
+ maxes[i] = 0.000000001
+
+ fig = go.Figure()
+ avgerage_spread = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
+ fn = ''
+
+ for team in teams:
+ team = str(team)
+ fn += team + '-'
+
+ s_team = [parsed_tpw_data[team]["avg-auto"]/maxes[0], parsed_tpw_data[team]["avg-tele"]/maxes[1], parsed_tpw_data[team]["tpw-score"]/maxes[2], parsed_tpw_data[team]["avg-driv"]/maxes[3], parsed_tpw_data[team]["avg-def"]/maxes[4], parsed_tpw_data[team]["avg-stab"]/maxes[5], parsed_tpw_data[team]["avg-upt"]/maxes[6], parsed_tpw_data[team]["avg-speed"]/maxes[7], parsed_tpw_data[team]["avg-inta"]/maxes[8], parsed_tpw_data[team]["avg-auto"]/maxes[0]]
+
+ fig.add_trace(go.Scatterpolar(
+ r=s_team,
+ theta=categories,
+ fill='toself',
+ name=team
+ ))
+
+ fig.add_trace(go.Scatterpolar(
+ r=avgerage_spread,
+ theta=categories,
+ fill='none',
+ name="Best Achieved"
+ ))
+
+ fig.update_layout(
+ title_font_family="Sitka",
+ title_font_color="green",
+ title = "Stats Percent of Best",
+ width = 600,
+ height = 600,
+ template=template)
+
+ plotly.offline.plot(fig, filename=base + event + "-" + fn + 'max-radar.html', auto_open=False)
+
+def overTimeFuelChart(team):
+ dataS = shotSummary(team)
+
+ fig, ax = plt.subplots()
+
+ x_Y = dataS['Match']
+ y_AS = dataS['AutoScored']
+ y_TS = dataS['TeleopScored']
+ y_TP = dataS['TeleopPassed']
+ y_T = dataS['Total Fuel']
+ ax.plot(x_Y, y_AS, label = 'Auto Scored')
+ ax.plot(x_Y, y_TS, label = 'Teleop Scored')
+ ax.plot(x_Y, y_TP, label = 'Teleop Passed')
+ ax.plot(x_Y, y_T, label = 'Total')
+
+ plt.title('Fuel Scoring Over Time ' + str(team))
+ plt.xlabel('Match Num')
+ plt.ylabel('Num')
+ plt.legend()
+ ax.plot()
+ html_fig = mpld3.fig_to_html(fig)
+ write = open(base + event + '-' + team + '-fuel_analysis.html', "w")
+ write.write(html_fig)
+ write.close()
+
+def autoVsTeleopChart(team):
+ dataS = shotSummary(team)
+
+ fig, ax = plt.subplots()
+
+ x_Y = dataS['Match']
+ y_AS = dataS['AutoScored']
+ y_TS = dataS['TeleopScored']
+ y_TP = dataS['TeleopPassed']
+ y_T = dataS['Total Fuel']
+ ax.plot(x_Y, y_AS, label = 'Auto Scored')
+ ax.plot(x_Y, y_TS, label = 'Teleop Scored')
+ ax.plot(x_Y, y_TP, label = 'Teleop Passed')
+ ax.plot(x_Y, y_T, label = 'Total')
+
+ plt.title('Auto vs Teleop Fuel Scoring ' + str(team))
+ plt.xlabel('Match Num')
+ plt.ylabel('Num')
+ plt.legend()
+ ax.plot()
+ html_fig = mpld3.fig_to_html(fig)
+ write = open(base + event + '-' + team + '-auto_vs_teleop.html', "w")
+ write.write(html_fig)
+ write.close()
+
+
+if int(args["mode"]) == 0:
+ overTimeFuelChart(str(args["team"]))
+elif int(args["mode"]) == 1:
+ radarChartSpread(args["teamList"])
+elif int(args["mode"]) == 2:
+ radarChartCTB(args["teamList"])
+elif int(args["mode"]) == 3:
+ autoVsTeleopChart(str(args["team"]))
diff --git a/config/scouting/2026/graphs_2026.ts b/config/scouting/2026/graphs_2026.ts
new file mode 100644
index 0000000..8aa119c
--- /dev/null
+++ b/config/scouting/2026/graphs_2026.ts
@@ -0,0 +1,784 @@
+import { parsedRow } from ".";
+
+interface teamData {
+ [key: string]: any[];
+}
+
+interface parsedTPWData {
+ [team: string]: {
+ "avg-tele": number;
+ "avg-auto": number;
+ "avg-climb": number;
+ "avg-def": number;
+ "avg-driv": number;
+ "avg-speed": number;
+ "avg-stab": number;
+ "avg-inta": number;
+ "avg-upt": number;
+ matches: any;
+ "tpw-std": number;
+ "tpw-score": number;
+ };
+}
+
+interface shotSummary {
+ Match: string;
+ AutoScored: number;
+ TeleopScored: number;
+ TeleopPassed: number;
+ TotalFuel: number;
+}
+
+interface chartConfig {
+ type: string;
+ data: {
+ labels: string[];
+ datasets: {
+ label: string;
+ data: number[] | number[][];
+ backgroundColor?: string | string[];
+ borderColor?: string | string[];
+ borderWidth?: number;
+ }[];
+ };
+ options: {
+ plugins?: {
+ title?: {
+ display: boolean;
+ text: string;
+ };
+ legend?: {
+ display: boolean;
+ position: string;
+ labels?: {
+ font: {
+ size: number;
+ };
+ };
+ };
+ tooltip?: {
+ bodyFont?: {
+ size: number;
+ };
+ titleFont?: {
+ size: number;
+ };
+ };
+ };
+ aspectRatio?: number;
+ onResize?: (chart: any, size: any) => void;
+ responsive: boolean;
+ maintainAspectRatio?: boolean;
+ scales?: {
+ x?: {
+ title: {
+ display: boolean;
+ text: string;
+ };
+ };
+ y?: {
+ title: {
+ display: boolean;
+ text: string;
+ };
+ ticks?: {
+ stepSize?: number;
+ min?: number;
+ maxTicksLimit?: number;
+ };
+ min?: number;
+ max?: number;
+ suggestedMin?: number;
+ suggestedMax?: number;
+ beginAtZero: boolean;
+ };
+ r?: {
+ beginAtZero: boolean;
+ max?: number;
+ };
+ };
+ };
+}
+
+// sum and average taken from https://gist.github.com/dggluz/365527824f9f521055baa3532b1d46e7
+const sum = (numbers: number[]) =>
+ numbers.reduce((total, aNumber) => total + aNumber, 0);
+const avg = (numbers: number[]) => sum(numbers) / numbers.length;
+
+// std taken from https://decipher.dev/30-seconds-of-typescript/docs/standardDeviation/
+const std = (arr) => {
+ const mean = arr.reduce((acc, val) => acc + val, 0) / arr.length;
+ return Math.sqrt(
+ arr
+ .reduce((acc, val) => acc.concat((val - mean) ** 2), [])
+ .reduce((acc, val) => acc + val, 0) / arr.length
+ );
+};
+
+function getData(data): any {
+ let team_data: teamData = {};
+ if (!data) {
+ throw new Error("No data given to getData()");
+ }
+ for (let x of data) {
+ if (team_data[x["team"]] == null) {
+ team_data[x["team"]] = [x];
+ } else {
+ team_data[x["team"]].push(x);
+ }
+ }
+ let parsed_tpw_data: parsedTPWData = {};
+ for (let team in team_data) {
+ let afgps = [];
+ let tfgps = [];
+ let afgpts = {};
+ let tfgpts = {};
+ let l1climbs = [];
+ let egcpts = [];
+ let defe = [];
+ let speed = [];
+ let driver = [];
+ let stab = [];
+ let inta = [];
+ let uptime = [];
+ let avg_auto_points = [];
+ let avg_tele_points = [];
+ let matches = {};
+ for (let x of team_data[team]) {
+ let auto_fuel_pieces = x["auto fuel scoring"]
+ .slice(1, x["auto fuel scoring"].length - 1)
+ .split(", ");
+ let tele_fuel_pieces = x["teleop fuel scoring"]
+ .slice(1, x["teleop fuel scoring"].length - 1)
+ .split(", ");
+ let game_pieces = auto_fuel_pieces.concat(tele_fuel_pieces);
+ afgps.push(auto_fuel_pieces);
+ tfgps.push(tele_fuel_pieces);
+ l1climbs.push(x["l1 climb"] === true || x["l1 climb"] === "true");
+ let climb_lev = parseInt(x["climb level"]);
+ if (climb_lev == 0) {
+ egcpts.push(0);
+ } else if (climb_lev == 1) {
+ egcpts.push(10);
+ } else if (climb_lev == 2) {
+ egcpts.push(20);
+ } else if (climb_lev >= 3) {
+ egcpts.push(30);
+ }
+
+ try {
+ defe.push(parseInt(x["defense skill"]));
+ speed.push(parseInt(x["speed"]));
+ stab.push(parseInt(x["stability"]));
+ inta.push(parseInt(x["intake consistency"]));
+ driver.push(parseInt(x["driver skill"]));
+ uptime.push(153000 - parseInt(x["brick time"]));
+ } catch {
+ defe.push(3);
+ speed.push(3);
+ stab.push(3);
+ inta.push(3);
+ driver.push(3);
+ uptime.push(100);
+ }
+
+ try {
+ matches[x["match"]][x[""]] = {
+ auto: auto_fuel_pieces,
+ teleop: tele_fuel_pieces
+ };
+ } catch {
+ matches[x["match"]] = {
+ [x[""]]: {
+ auto: auto_fuel_pieces,
+ teleop: tele_fuel_pieces
+ }
+ };
+ }
+ }
+ for (let i = 0; i < afgps.length; ++i) {
+ for (let j = 0; j < afgps[i].length; ++j) {
+ let val = afgps[i][j];
+ if (val == "fsa") {
+ afgpts[i] = (afgpts[i] || 0) + 1;
+ } else {
+ afgpts[i] = (afgpts[i] || 0) + 0;
+ }
+ }
+ if (l1climbs[i]) {
+ afgpts[i] = (afgpts[i] || 0) + 15;
+ }
+ avg_auto_points.push(afgpts[i]);
+ }
+ for (let i = 0; i < tfgps.length; ++i) {
+ for (let j = 0; j < tfgps[i].length; ++j) {
+ let val = tfgps[i][j];
+ if (val == "fsa") {
+ tfgpts[i] = (tfgpts[i] || 0) + 1;
+ } else if (val == "fp") {
+ tfgpts[i] = (tfgpts[i] || 0) + 0;
+ } else {
+ tfgpts[i] = (tfgpts[i] || 0) + 0;
+ }
+ }
+ avg_tele_points.push(tfgpts[i]);
+ }
+ let data_tpw: parsedTPWData[string] = {
+ "avg-tele": avg(avg_tele_points),
+ "avg-auto": avg(avg_auto_points),
+ "avg-climb": avg(egcpts),
+ "avg-def": avg(defe),
+ "avg-driv": avg(driver),
+ "avg-speed": avg(speed),
+ "avg-stab": avg(stab),
+ "avg-inta": avg(inta),
+ "avg-upt": avg(uptime),
+ matches: matches,
+ "tpw-std":
+ std(avg_auto_points) + std(avg_tele_points) + std(egcpts),
+ "tpw-score": 0
+ };
+ data_tpw["tpw-score"] =
+ data_tpw["avg-auto"] + data_tpw["avg-tele"] + data_tpw["avg-climb"];
+ parsed_tpw_data[team] = data_tpw;
+ }
+ return parsed_tpw_data;
+}
+
+function shotSummary(parsed_data: parsedTPWData, team: string): shotSummary[] {
+ const data = parsed_data[team];
+ const gamePieces: shotSummary[] = [];
+
+ if (!data || !data.matches) {
+ throw new Error(`No data found for team: ${team}`);
+ }
+
+ for (const match in data.matches) {
+ const autoScoredArr: number[] = [];
+ const teleScoredArr: number[] = [];
+ const telePassedArr: number[] = [];
+ for (const x in data.matches[match]) {
+ const entry = data.matches[match][x];
+ let aScored = 0;
+ for (const e of entry.auto) {
+ if (e == "fsa") {
+ aScored += 1;
+ }
+ }
+ let tScored = 0;
+ let tPassed = 0;
+ for (const e of entry.teleop) {
+ if (e == "fsa") {
+ tScored += 1;
+ } else if (e == "fp") {
+ tPassed += 1;
+ }
+ }
+ autoScoredArr.push(aScored);
+ teleScoredArr.push(tScored);
+ telePassedArr.push(tPassed);
+ }
+ const autoAvg = avg(autoScoredArr);
+ const teleAvg = avg(teleScoredArr);
+ const passAvg = avg(telePassedArr);
+ const total = autoAvg + teleAvg + passAvg;
+ gamePieces.push({
+ Match: match,
+ AutoScored: autoAvg,
+ TeleopScored: teleAvg,
+ TeleopPassed: passAvg,
+ TotalFuel: total
+ });
+ }
+ return gamePieces;
+}
+
+function radarChartSpread(
+ parsed_data: parsedTPWData,
+ teams: string[]
+): chartConfig {
+ const categories = [
+ "auto points",
+ "teleop points",
+ "climb points",
+ "total points"
+ ];
+ const datasets = teams.map((team) => {
+ const t = parsed_data[team];
+ if (!t) {
+ throw new Error(`No data found for team: ${team}`);
+ }
+ const tvals = [
+ t["avg-auto"],
+ t["avg-tele"],
+ t["avg-climb"],
+ t["tpw-score"]
+ ];
+ const color = () =>
+ `rgba(${Math.floor(Math.random() * 255)}, ${Math.floor(
+ Math.random() * 255
+ )}, ${Math.floor(Math.random() * 255)}, 0.5)`;
+ const border = color();
+ const background = border.replace("0.5", "0.2");
+ return {
+ label: `Team ${team}`,
+ data: tvals,
+ backgroundColor: background,
+ borderColor: border,
+ borderWidth: 2
+ };
+ });
+ return {
+ type: "radar",
+ data: {
+ labels: categories,
+ datasets
+ },
+ options: {
+ plugins: {
+ title: {
+ display: true,
+ text: "Average Point Spread"
+ }
+ },
+ responsive: true,
+ scales: {
+ r: {
+ beginAtZero: true
+ }
+ }
+ }
+ };
+}
+
+function getBest(parsed_data: parsedTPWData): number[] {
+ let auto = 0;
+ let tele = 0;
+ let total = 0;
+ let drive = 0;
+ let defe = 0;
+ let stab = 0;
+ let upt = 0;
+ let speed = 0;
+ let inta = 0;
+ for (const team in parsed_data) {
+ const stats = parsed_data[team];
+ if (stats["avg-auto"] > auto) auto = stats["avg-auto"];
+ if (stats["avg-tele"] > tele) tele = stats["avg-tele"];
+ if (stats["tpw-score"] > total) total = stats["tpw-score"];
+ if (stats["avg-driv"] > drive) drive = stats["avg-driv"];
+ if (stats["avg-def"] > defe) defe = stats["avg-def"];
+ if (stats["avg-stab"] > stab) stab = stats["avg-stab"];
+ if (stats["avg-upt"] > upt) upt = stats["avg-upt"];
+ if (stats["avg-speed"] > speed) speed = stats["avg-speed"];
+ if (stats["avg-inta"] > inta) inta = stats["avg-inta"];
+ }
+ return [auto, tele, total, drive, defe, stab, upt, speed, inta];
+}
+
+function radarChartCTB(parsed_data: parsedTPWData, teams: string[]): any {
+ const categories = [
+ "auto pts",
+ "teleop pts",
+ "total pts",
+ "drive skill",
+ "defense",
+ "stability",
+ "uptime",
+ "speed",
+ "intake"
+ ];
+ let maxes = getBest(parsed_data).map((value) =>
+ value === 0 ? 1e-9 : value
+ );
+ const datasets = teams.map((team) => {
+ const t = parsed_data[team];
+ if (!t) {
+ throw new Error(`No data found for team: ${team}`);
+ }
+ const s_team = [
+ t["avg-auto"] / maxes[0],
+ t["avg-tele"] / maxes[1],
+ t["tpw-score"] / maxes[2],
+ t["avg-driv"] / maxes[3],
+ t["avg-def"] / maxes[4],
+ t["avg-stab"] / maxes[5],
+ t["avg-upt"] / maxes[6],
+ t["avg-speed"] / maxes[7],
+ t["avg-inta"] / maxes[8]
+ ];
+ const color = () =>
+ `rgba(${Math.floor(Math.random() * 255)}, ${Math.floor(
+ Math.random() * 255
+ )}, ${Math.floor(Math.random() * 255)}, 0.5)`;
+ const border = color();
+ const background = border.replace("0.5", "0.2");
+
+ return {
+ label: `Team ${team}`,
+ data: s_team,
+ backgroundColor: background,
+ borderColor: border,
+ borderWidth: 2
+ };
+ });
+ const avgspread = Array(categories.length).fill(1);
+ datasets.push({
+ label: "Best Achieved",
+ data: avgspread,
+ backgroundColor: "rgba(0, 0, 0, 0.1)",
+ borderColor: "rgba(0, 0, 0, 0.7)",
+ borderWidth: 2
+ });
+ return {
+ type: "radar",
+ data: {
+ labels: categories,
+ datasets
+ },
+ options: {
+ plugins: {
+ title: {
+ display: true,
+ text: "Stats Percent of Best"
+ }
+ },
+ responsive: true,
+ scales: {
+ r: {
+ max: 1
+ }
+ }
+ }
+ };
+}
+
+function overTimeFuelChart(
+ parsed_data: parsedTPWData,
+ team: string
+): chartConfig {
+ const dataS: shotSummary[] = shotSummary(parsed_data, team);
+ const labels = dataS.map((x) => x.Match);
+ const datasets = [
+ {
+ label: "Auto Scored",
+ data: dataS.map((x) => x.AutoScored),
+ backgroundColor: "rgba(75, 192, 192, 0.2)",
+ borderColor: "rgba(75, 192, 192, 1)",
+ borderWidth: 2,
+ fill: false
+ },
+ {
+ label: "Teleop Scored",
+ data: dataS.map((x) => x.TeleopScored),
+ backgroundColor: "rgba(54, 162, 235, 0.2)",
+ borderColor: "rgba(54, 162, 235, 1)",
+ borderWidth: 2,
+ fill: false
+ },
+ {
+ label: "Teleop Passed",
+ data: dataS.map((x) => x.TeleopPassed),
+ backgroundColor: "rgba(255, 206, 86, 0.2)",
+ borderColor: "rgba(255, 206, 86, 1)",
+ borderWidth: 2,
+ fill: false
+ },
+ {
+ label: "Total Fuel",
+ data: dataS.map((x) => x.TotalFuel),
+ backgroundColor: "rgba(153, 102, 255, 0.2)",
+ borderColor: "rgba(153, 102, 255, 1)",
+ borderWidth: 2,
+ fill: false
+ }
+ ];
+ return {
+ type: "line",
+ data: {
+ labels,
+ datasets
+ },
+ options: {
+ plugins: {
+ title: {
+ display: true,
+ text: `Fuel Scoring Over Time for Team ${team}`
+ },
+ legend: {
+ display: true,
+ position: "top",
+ labels: {
+ font: {
+ size: 12
+ }
+ }
+ }
+ },
+ aspectRatio: 2,
+ maintainAspectRatio: true,
+ responsive: true,
+ scales: {
+ x: {
+ title: {
+ display: true,
+ text: "Match Num"
+ }
+ },
+ y: {
+ title: {
+ display: true,
+ text: "Num"
+ },
+ beginAtZero: true
+ }
+ }
+ }
+ };
+}
+
+function autoVsTeleopChart(
+ parsed_data: parsedTPWData,
+ team: string
+): chartConfig {
+ const dataS = shotSummary(parsed_data, team);
+ const labels = dataS.map((x) => x.Match);
+ const datasets = [
+ {
+ label: "Auto Scored",
+ data: dataS.map((x) => x.AutoScored),
+ backgroundColor: "rgba(255, 99, 132, 0.2)",
+ borderColor: "rgba(255, 99, 132, 1)",
+ borderWidth: 2,
+ fill: false
+ },
+ {
+ label: "Teleop Scored",
+ data: dataS.map((x) => x.TeleopScored),
+ backgroundColor: "rgba(54, 162, 235, 0.2)",
+ borderColor: "rgba(54, 162, 235, 1)",
+ borderWidth: 2,
+ fill: false
+ },
+ {
+ label: "Teleop Passed",
+ data: dataS.map((x) => x.TeleopPassed),
+ backgroundColor: "rgba(255, 206, 86, 0.2)",
+ borderColor: "rgba(255, 206, 86, 1)",
+ borderWidth: 2,
+ fill: false
+ },
+ {
+ label: "Total Fuel",
+ data: dataS.map((x) => x.TotalFuel),
+ backgroundColor: "rgba(201, 203, 207, 0.2)",
+ borderColor: "rgba(201, 203, 207, 1)",
+ borderWidth: 2,
+ fill: false
+ }
+ ];
+
+ return {
+ type: "line",
+ data: {
+ labels,
+ datasets
+ },
+ options: {
+ plugins: {
+ title: {
+ display: true,
+ text: `Auto vs Teleop Fuel Scoring for Team ${team}`
+ },
+ legend: {
+ display: true,
+ position: "top",
+ labels: {
+ font: {
+ size: 12
+ }
+ }
+ }
+ },
+ aspectRatio: 2,
+ maintainAspectRatio: true,
+ responsive: true,
+ scales: {
+ x: {
+ title: {
+ display: true,
+ text: "Match Num"
+ }
+ },
+ y: {
+ title: {
+ display: true,
+ text: "Num"
+ },
+ beginAtZero: true
+ }
+ }
+ }
+ };
+}
+
+function scoreProportions(
+ parsed_data: parsedTPWData,
+ team: string
+): chartConfig {
+ const dataS = shotSummary(parsed_data, team);
+ const avgs = {
+ "Auto Scored": avg(dataS.map((x) => x.AutoScored)),
+ "Teleop Scored": avg(dataS.map((x) => x.TeleopScored)),
+ "Teleop Passed": avg(dataS.map((x) => x.TeleopPassed))
+ };
+ const labels = Object.keys(avgs);
+ const data = Object.values(avgs);
+ return {
+ type: "pie",
+ data: {
+ labels: labels,
+ datasets: [
+ {
+ label: "Scoring",
+ data: data,
+ backgroundColor: [
+ "rgba(255, 99, 132, 0.5)",
+ "rgba(54, 162, 235, 0.5)",
+ "rgba(255, 206, 86, 0.5)"
+ ],
+ borderColor: [
+ "rgba(255, 99, 132, 1)",
+ "rgba(54, 162, 235, 1)",
+ "rgba(255, 206, 86, 1)"
+ ],
+ borderWidth: 1
+ }
+ ]
+ },
+ options: {
+ plugins: {
+ title: {
+ display: true,
+ text: `Average Scoring Proportion for Team ${team}`
+ },
+ legend: {
+ display: true,
+ position: "top",
+ labels: {
+ font: {
+ size: 12
+ }
+ }
+ }
+ },
+ responsive: true
+ }
+ };
+}
+
+function climbDistribution(
+ parsed_data: parsedTPWData,
+ team: string
+): chartConfig {
+ const data_team = parsed_data[team];
+ if (!data_team || !data_team.matches) {
+ throw new Error(`No data found for team: ${team}`);
+ }
+
+ // Count climb levels across all matches from raw data
+ // Since we only store fuel data in matches, we need to recompute from source
+ // For now, use the average climb which gives us the distribution info
+ // We'll use a bar chart showing the avg climb points per match trend
+ const dataS = shotSummary(parsed_data, team);
+ const labels = dataS.map((x) => x.Match);
+
+ // Show fuel efficiency: scored per match as stacked bar
+ const datasets = [
+ {
+ label: "Auto Scored",
+ data: dataS.map((x) => x.AutoScored),
+ backgroundColor: "rgba(75, 192, 192, 0.5)",
+ borderColor: "rgba(75, 192, 192, 1)",
+ borderWidth: 1
+ },
+ {
+ label: "Teleop Scored",
+ data: dataS.map((x) => x.TeleopScored),
+ backgroundColor: "rgba(54, 162, 235, 0.5)",
+ borderColor: "rgba(54, 162, 235, 1)",
+ borderWidth: 1
+ },
+ {
+ label: "Teleop Passed",
+ data: dataS.map((x) => x.TeleopPassed),
+ backgroundColor: "rgba(255, 206, 86, 0.5)",
+ borderColor: "rgba(255, 206, 86, 1)",
+ borderWidth: 1
+ }
+ ];
+
+ return {
+ type: "bar",
+ data: {
+ labels,
+ datasets
+ },
+ options: {
+ responsive: true,
+ plugins: {
+ title: {
+ display: true,
+ text: `Fuel Breakdown per Match for Team ${team}`
+ },
+ tooltip: {
+ bodyFont: {
+ size: 10
+ },
+ titleFont: {
+ size: 14
+ }
+ }
+ },
+ aspectRatio: 2,
+ maintainAspectRatio: true,
+ scales: {
+ x: {
+ title: {
+ display: true,
+ text: "Match Num"
+ }
+ },
+ y: {
+ title: {
+ display: true,
+ text: "Count"
+ },
+ beginAtZero: true
+ }
+ }
+ }
+ };
+}
+
+export function getGraph(
+ mode: number,
+ parsed_data: parsedRow[],
+ teamS: string | string[]
+) {
+ const array_modes = [1, 2];
+ if (array_modes.includes(mode))
+ teamS = typeof teamS == "string" ? [teamS] : teamS;
+
+ const allowed_modes = typeof teamS == "string" ? [0, 3, 4, 5] : [1, 2];
+ if (!allowed_modes.includes(mode)) {
+ throw new Error(`Invalid mode: ${mode} in getGraph func`);
+ }
+ const tpw_data = getData(parsed_data);
+ if (mode == 0) return overTimeFuelChart(tpw_data, teamS as string);
+ if (mode == 1) return radarChartSpread(tpw_data, teamS as string[]);
+ if (mode == 2) return radarChartCTB(tpw_data, teamS as string[]);
+ if (mode == 3) return autoVsTeleopChart(tpw_data, teamS as string);
+ if (mode == 4) return scoreProportions(tpw_data, teamS as string);
+ if (mode == 5) return climbDistribution(tpw_data, teamS as string);
+}
diff --git a/config/scouting/2026/index.ts b/config/scouting/2026/index.ts
index 8da1905..4405dc7 100644
--- a/config/scouting/2026/index.ts
+++ b/config/scouting/2026/index.ts
@@ -2,7 +2,10 @@ import { execSync, exec } from "child_process";
import * as fs from "fs";
import { getMatchesFull } from "../../../helpers/tba";
import { getAllDataByEvent } from "../../../helpers/scouting";
-import accuracy2025 from "./accuracy";
+import accuracy2026 from "./accuracy";
+import { getGraph } from "./graphs_2026";
+import { computePrediction } from "./predictions_2026";
+import { computeRankings } from "./rankings_2026";
export interface parsedRow {
match: number;
@@ -10,11 +13,11 @@ export interface parsedRow {
alliance: string;
leave: boolean;
"fuel ground intake": boolean;
- "fuel station intake": boolean;
- "can ferry": boolean;
+ "outpost intake": boolean;
+ "passing from neutral zone": boolean;
"traverse under trench": boolean;
"traverse over bump": boolean;
- "auto l1 climb": boolean;
+ "l1 climb": boolean;
"auto fuel scoring": string;
"teleop fuel scoring": string;
"climb level": number;
@@ -26,6 +29,7 @@ export interface parsedRow {
speed: number;
stability: number;
"intake consistency": number;
+ "balls per second": number;
scouter: string;
comments: string;
accuracy: number | "";
@@ -45,12 +49,12 @@ export function categories() {
dataType: "boolean"
},
{
- name: "Fuel Station Intake",
+ name: "Outpost Intake",
identifier: "26-2",
dataType: "boolean"
},
{
- name: "Can Ferry",
+ name: "Passing from Neutral Zone",
identifier: "26-3",
dataType: "boolean"
},
@@ -65,12 +69,12 @@ export function categories() {
dataType: "boolean"
},
{
- name: "Auto L1 Climb",
+ name: "L1 Climb",
identifier: "26-6",
dataType: "boolean"
},
- { name: "Auto Fuel Scoring", identifier: "26-7", dataType: "array" }, // 'asf' 'amf' | auto score, auto miss
- { name: "Teleop Fuel Scoring", identifier: "26-8", dataType: "array" }, // 'saf', 'maf', 'hif' | score active, miss active, shot inactive
+ { name: "Auto Fuel Scoring", identifier: "26-7", dataType: "array" }, // 'fsa' | scored
+ { name: "Teleop Fuel Scoring", identifier: "26-8", dataType: "array" }, // 'fsa', 'fp' | scored, passed
{ name: "Climb Level", identifier: "26-9" },
{ name: "Climb Time", identifier: "26-10" },
{ name: "Brick Time", identifier: "26-11" },
@@ -95,7 +99,8 @@ export function categories() {
identifier: "26-20",
dataType: "array"
},
- { name: "Teleop Fuel Count", identifier: "26-21" }
+ { name: "Teleop Fuel Count", identifier: "26-21" },
+ { name: "Balls Per Second", identifier: "26-22" }
];
}
@@ -133,13 +138,13 @@ export function layout() {
},
{
type: "checkbox",
- label: "Fuel Station Intake",
+ label: "Outpost Intake",
default: false,
data: "26-2"
},
{
type: "checkbox",
- label: "Can Ferry?",
+ label: "Passing from Neutral Zone",
default: false,
data: "26-3"
},
@@ -157,7 +162,7 @@ export function layout() {
},
{
type: "checkbox",
- label: "Did L1 Climb?",
+ label: "L1 Climb",
default: false,
data: "26-6"
},
@@ -170,103 +175,18 @@ export function layout() {
restricts: ["cage_time", "defense_time"]
},
{
- type: "locations",
- increment: 8,
- src: {
- type: "function",
- definition: ((state) =>
- `/img/2026hub.png`).toString()
- },
- default: {
- locations: [],
- values: [],
- counter: 0
- },
+ type: "counter",
+ src: "/img/2026hub.png",
+ location: 0,
data: {
values: "26-7",
locations: "26-18",
counter: "26-19"
},
- rows: 1,
- columns: 1,
- orientation: 0,
- flip: false,
- disabled: [],
- marker: {
- type: "function",
- definition: ((state) => {
- return `${state.locations
- .filter((location) =>
- ["fsa", "fsi"].includes(
- location.value
- )
- )
- .map((location, i, arr) => {
- if (i > 5) {
- return "";
- } else {
- let colors = [
- "#ebebeb",
- "#fd3f0d",
- "#b700ff",
- "#5300ff",
- "#000000"
- ];
- if (
- arr.length > 18 ||
- i + 12 < arr.length
- ) {
- return `
`;
- } else if (
- arr.length > 12 ||
- i + 6 < arr.length
- ) {
- return ``;
- } else if (
- arr.length > 6 ||
- i < arr.length
- ) {
- return ``;
- }
- }
- return "";
- })
- .filter(
- (marker) => marker != ""
- )
- .slice(0, 6)
- .join("")}`;
- }).toString()
- },
- // TODO: Score Active, Missed Active, Shot Inactive
options: [
{
label: "Scored",
- value: "fsa",
- tracks: ["fsi"],
- type: "counter",
- show: {
- type: "function",
- definition: ((state) => {
- return state.index == 0;
- }).toString()
- }
- },
- {
- label: "Missed",
- value: "fma",
- tracks: ["fmi"],
- type: "counter",
- show: {
- type: "function",
- definition: ((state) => {
- return state.index == 0;
- }).toString()
- }
+ value: "fsa"
}
]
}
@@ -296,13 +216,13 @@ export function layout() {
},
{
type: "checkbox",
- label: "Fuel Station Intake",
+ label: "Outpost Intake",
default: false,
data: "26-2"
},
{
type: "checkbox",
- label: "Can Ferry?",
+ label: "Passing from Neutral Zone",
default: false,
data: "26-3"
},
@@ -335,119 +255,31 @@ export function layout() {
restricts: ["cage_time", "brick_time"]
},
{
- type: "locations",
- increment: 5,
- src: {
- type: "function",
- definition: ((state) =>
- `/img/2026hub.png`).toString()
- },
- default: {
- locations: [],
- values: [],
- counter: 0
- },
+ type: "counter",
+ src: "/img/2026hub.png",
+ location: 0,
data: {
values: "26-8",
locations: "26-20",
counter: "26-21"
},
- rows: 1,
- columns: 1,
- orientation: 0,
- flip: false,
- disabled: [],
- marker: {
- type: "function",
- definition: ((state) => {
- return `${state.locations
- .filter((location) =>
- [
- "fsa",
- "fma",
- "fhi"
- ].includes(location.value)
- )
- .map((location, i, arr) => {
- if (i > 5) {
- return "";
- } else {
- let colors = [
- "#ebebeb",
- "#fd3f0d",
- "#b700ff",
- "#5300ff",
- "#000000"
- ];
- if (
- arr.length > 18 ||
- i + 12 < arr.length
- ) {
- return ``;
- } else if (
- arr.length > 12 ||
- i + 6 < arr.length
- ) {
- return ``;
- } else if (
- arr.length > 6 ||
- i < arr.length
- ) {
- return ``;
- }
- }
- return "";
- })
- .filter(
- (marker) => marker != ""
- )
- .slice(0, 6)
- .join("")}`;
- }).toString()
- },
- // TODO: Score Active, Missed Active, Shot Inactive
options: [
{
- label: "Scored (Active)",
- value: "fsa",
- tracks: ["fma", "fhi"],
- type: "counter",
- show: {
- type: "function",
- definition: ((state) => {
- return state.index == 0;
- }).toString()
- }
- },
- {
- label: "Missed (Active)",
- value: "fma",
- tracks: ["fsa", "fhi"],
- type: "counter",
- show: {
- type: "function",
- definition: ((state) => {
- return state.index == 0;
- }).toString()
- }
+ label: "Scored",
+ value: "fsa"
},
{
- label: "Shot (Inactive)",
- value: "fhi",
- tracks: ["fsa", "fma"],
- type: "counter",
- show: {
- type: "function",
- definition: ((state) => {
- return state.index == 0;
- }).toString()
- }
+ label: "Passed",
+ value: "fp"
}
]
+ },
+ {
+ type: "textbox",
+ placeholder:
+ "Enter notes here (and include team number if scouting practice matches)...",
+ default: "",
+ data: "comments"
}
]
}
@@ -563,6 +395,15 @@ export function layout() {
"/img/star-filled.png"
]
},
+ {
+ type: "rating",
+ label: "Balls Per Second",
+ default: 0,
+ data: "26-22",
+ max: 11,
+ step: 1,
+ maxLabel: "10+"
+ },
{
type: "textbox",
placeholder:
@@ -707,7 +548,7 @@ function find(entry, type, categories, category, fallback: any = "") {
}
export function formatData(data, categories, teams) {
- return `entry,match,team,alliance,leave,"fuel ground intake","fuel station intake","can ferry","traverse under trench","traverse over bump","auto l1 climb","auto fuel scoring","teleop fuel scoring","climb level","climb time","brick time","defense time","driver skill","defense skill",speed,stability,"intake consistency",scouter,comments,accuracy,timestamp\n${data
+ return `entry,match,team,alliance,leave,"fuel ground intake","outpost intake","passing from neutral zone","traverse under trench","traverse over bump","l1 climb","auto fuel scoring","teleop fuel scoring","climb level","climb time","brick time","defense time","driver skill","defense skill",speed,stability,"intake consistency","balls per second",scouter,comments,accuracy,timestamp\n${data
.map((entry, i) => {
return [
i,
@@ -746,6 +587,7 @@ export function formatData(data, categories, teams) {
parseInt(find(entry, "ratings", categories, "26-15", "")),
parseInt(find(entry, "ratings", categories, "26-16", "")),
parseInt(find(entry, "ratings", categories, "26-17", "")),
+ parseInt(find(entry, "ratings", categories, "26-22", 0)),
JSON.stringify(
`${entry.contributor.username || "username"} (${
teams[entry.contributor.team] || 0
@@ -791,13 +633,13 @@ export function parseFormatted(format: string): parsedRow[] {
alliance: columns[3],
leave: columns[4] === "true",
"fuel ground intake": columns[5] === "true",
- "fuel station intake": columns[6] === "true",
- "can ferry": columns[7] === "true",
+ "outpost intake": columns[6] === "true",
+ "passing from neutral zone": columns[7] === "true",
"traverse under trench": columns[8] === "true",
"traverse over bump": columns[9] === "true",
- "auto l1 climb": columns[10] === "true",
- "auto fuel scoring": parseArr(columns[11]).join(", "),
- "teleop fuel scoring": parseArr(columns[12]).join(", "),
+ "l1 climb": columns[10] === "true",
+ "auto fuel scoring": columns[11],
+ "teleop fuel scoring": columns[12],
"climb level": parseInt(columns[13], 10),
"climb time": parseInt(columns[14], 10),
"brick time": parseInt(columns[15], 10),
@@ -807,22 +649,22 @@ export function parseFormatted(format: string): parsedRow[] {
speed: parseInt(columns[19], 10),
stability: parseInt(columns[20], 10),
"intake consistency": parseInt(columns[21], 10),
- scouter: columns[22].replace(/^"|"$/g, ""),
- comments: columns[23].replace(/^"|"$/g, ""),
- accuracy: columns[24] ? parseFloat(columns[24]) : "",
- timestamp: parseInt(columns[25], 10)
+ "balls per second": parseInt(columns[22], 10),
+ scouter: columns[23].replace(/^"|"$/g, ""),
+ comments: columns[24].replace(/^"|"$/g, ""),
+ accuracy: columns[25] ? parseFloat(columns[25]) : "",
+ timestamp: parseInt(columns[26], 10)
};
});
}
let parsedScoring = {
- fsa: "Scored when Active",
- fma: "Missed when Active",
- fhi: "Shot when Inactive"
+ fsa: "Scored",
+ fp: "Passed"
};
export function formatParsedData(data, categories, teams) {
- return `entry,match,team,alliance,leave,"fuel ground intake","fuel station intake","can ferry","traverse under trench","traverse over bump","auto l1 climb","auto fuel scoring","teleop fuel scoring","climb level","climb time","brick time","defense time","driver skill","defense skill",speed,stability,"intake consistency",scouter,comments,accuracy,timestamp\n${data
+ return `entry,match,team,alliance,leave,"fuel ground intake","outpost intake","passing from neutral zone","traverse under trench","traverse over bump","l1 climb","auto fuel scoring","teleop fuel scoring","climb level","climb time","brick time","defense time","driver skill","defense skill",speed,stability,"intake consistency","balls per second",scouter,comments,accuracy,timestamp\n${data
.map((entry, i) => {
return [
i,
@@ -850,12 +692,17 @@ export function formatParsedData(data, categories, teams) {
find(entry, "abilities", categories, "26-6", false)
? "true"
: "false",
- `"${find(entry, "data", categories, "26-7", [])
- .map((entry) => parsedScoring[entry])
- .join("\\n")}"`,
- `"${find(entry, "data", categories, "26-8", [])
- .map((entry) => parsedScoring[entry])
- .join("\\n")}"`,
+ `"${(() => {
+ const arr = find(entry, "data", categories, "26-7", []);
+ const scored = arr.filter((e) => e === "fsa").length;
+ return `${scored} Scored`;
+ })()} "`,
+ `"${(() => {
+ const arr = find(entry, "data", categories, "26-8", []);
+ const scored = arr.filter((e) => e === "fsa").length;
+ const passed = arr.filter((e) => e === "fp").length;
+ return `${scored} Scored | ${passed} Passed`;
+ })()} "`,
["none", "level 1", "level 2", "level 3"][
parseInt(find(entry, "abilities", categories, "26-9", 0))
],
@@ -896,6 +743,9 @@ export function formatParsedData(data, categories, teams) {
find(entry, "ratings", categories, "26-17", "") + 1
)
),
+ parseInt(find(entry, "ratings", categories, "26-22", 0)) >= 11
+ ? "10+"
+ : parseInt(find(entry, "ratings", categories, "26-22", 0)),
JSON.stringify(
`${entry.contributor.username || "username"} (${
teams[entry.contributor.team] || 0
@@ -913,19 +763,12 @@ export function formatParsedData(data, categories, teams) {
.join("\n")}`;
}
-/*
-
export interface picklist {
team: string;
- "avg-auto-pieces": number;
- "avg-tele-pieces": number;
- "avg-l1": number;
- "avg-l2": number;
- "avg-l3": number;
- "avg-l4": number;
- "avg-proc": number;
- "avg-net": number;
- "deep-climbs": number;
+ "avg-auto-fuel": number;
+ "avg-tele-fuel": number;
+ "avg-tele-passed": number;
+ "high-climbs": number;
}
export async function formPicklist(
@@ -939,15 +782,10 @@ export async function formPicklist(
const t = t1.team_number;
let dat = data[t];
if (!dat) continue;
- let autoPieces = 0;
- let telePieces = 0;
- let l1 = 0;
- let l2 = 0;
- let l3 = 0;
- let l4 = 0;
- let proc = 0;
- let net = 0;
- let deepClimbs = 0;
+ let autoFuel = 0;
+ let teleFuel = 0;
+ let telePassed = 0;
+ let highClimbs = 0;
let total = 0;
for (const d of dat) {
if (!d || !d.accuracy || !d.accuracy.calculated) {
@@ -955,60 +793,34 @@ export async function formPicklist(
}
let acc = d.accuracy.percentage;
- autoPieces += acc * find(d, "counters", categories, "26-20", 0);
- telePieces += acc * find(d, "counters", categories, "26-24", 0);
- let autocoral = find(d, "data", categories, "26-18", []);
- let telecoral = find(d, "data", categories, "26-22", []);
- let autol4 = autocoral.filter((el) => el == 0).length;
- let autol3 = autocoral.filter((el) => el == 1).length;
- let autol2 = autocoral.filter((el) => el == 2).length;
- let autol1 = autocoral.filter((el) => el == 3).length;
- let telel4 = telecoral.filter((el) => el == 0).length;
- let telel3 = telecoral.filter((el) => el == 1).length;
- let telel2 = telecoral.filter((el) => el == 2).length;
- let telel1 = telecoral.filter((el) => el == 3).length;
- l1 += acc * (autol1 + telel1);
- l2 += acc * (autol2 + telel2);
- l3 += acc * (autol3 + telel3);
- l4 += acc * (autol4 + telel4);
- let autoalgae = find(d, "data", categories, "26-17", []);
- let telealgae = find(d, "data", categories, "26-21", []);
- let autoNe = autoalgae.filter((el) => el == 4).length;
- let autoPr = autoalgae.filter((el) => el == 5).length;
- let teleNe = telealgae.filter((el) => el == 4).length;
- let telePr = telealgae.filter((el) => el == 5).length;
- net += acc * (autoNe + teleNe);
- proc += acc * (autoPr + telePr);
- let climb = find(d, "abilities", categories, "26-8", 0);
- if (acc > 0.5) deepClimbs += climb == 3 ? 1 : 0;
+ let autoFuelData = find(d, "data", categories, "26-7", []);
+ let teleFuelData = find(d, "data", categories, "26-8", []);
+ let autoScored = autoFuelData.filter((el) => el === "fsa").length;
+ let teleScored = teleFuelData.filter((el) => el === "fsa").length;
+ let telePas = teleFuelData.filter((el) => el === "fp").length;
+ autoFuel += acc * autoScored;
+ teleFuel += acc * teleScored;
+ telePassed += acc * telePas;
+ let climb = parseInt(find(d, "abilities", categories, "26-9", 0));
+ if (acc > 0.5) highClimbs += climb >= 3 ? 1 : 0;
total += acc;
}
if (dat.length == 0 || total == 0) {
analysis.push({
team: t,
- "avg-auto-pieces": NaN,
- "avg-tele-pieces": NaN,
- "avg-l1": NaN,
- "avg-l2": NaN,
- "avg-l3": NaN,
- "avg-l4": NaN,
- "avg-proc": NaN,
- "avg-net": NaN,
- "deep-climbs": NaN
+ "avg-auto-fuel": NaN,
+ "avg-tele-fuel": NaN,
+ "avg-tele-passed": NaN,
+ "high-climbs": NaN
});
} else {
analysis.push({
team: t,
- "avg-auto-pieces": autoPieces / total,
- "avg-tele-pieces": telePieces / total,
- "avg-l1": l1 / total,
- "avg-l2": l2 / total,
- "avg-l3": l3 / total,
- "avg-l4": l4 / total,
- "avg-proc": proc / total,
- "avg-net": net / total,
- "deep-climbs": deepClimbs
+ "avg-auto-fuel": autoFuel / total,
+ "avg-tele-fuel": teleFuel / total,
+ "avg-tele-passed": telePassed / total,
+ "high-climbs": highClimbs
});
}
}
@@ -1017,29 +829,24 @@ export async function formPicklist(
}
function formatPicklist(analysis) {
- return `entry,team,"avg auto pieces","avg tele pieces","avg l1","avg l2","avg l3","avg l4","avg processor","avg net","# of deep climbs"\n${analysis
+ return `entry,team,"avg auto fuel","avg tele fuel","avg tele passed","# of high climbs"\n${analysis
.map((entry, i) => {
return [
i,
entry.team || 0,
- entry["avg-auto-pieces"],
- entry["avg-tele-pieces"],
- entry["avg-l1"],
- entry["avg-l2"],
- entry["avg-l3"],
- entry["avg-l4"],
- entry["avg-proc"],
- entry["avg-net"],
- entry["deep-climbs"]
+ entry["avg-auto-fuel"],
+ entry["avg-tele-fuel"],
+ entry["avg-tele-passed"],
+ entry["high-climbs"]
].join(",");
})
.join("\n")}`;
-} */
+}
export function notes() {
return ``;
}
-/*
+
function run(command) {
return new Promise(async (resolve, reject) => {
exec(command, (error, stdout, stderr) => {
@@ -1215,13 +1022,13 @@ export async function analysis(event, teamNumber) {
analyzed.push({
type: "config",
category: "score",
- label: "Algae Scoring",
+ label: "Fuel Scoring",
value: graph0
});
analyzed.push({
type: "config",
category: "score",
- label: "Coral Scoring",
+ label: "Auto vs Teleop",
value: graph3
});
analyzed.push({
@@ -1233,7 +1040,7 @@ export async function analysis(event, teamNumber) {
analyzed.push({
type: "config",
category: "score",
- label: "Shot Accuracy",
+ label: "Fuel Breakdown",
value: graph5
});
analyzed.push({
@@ -1366,24 +1173,23 @@ export async function predict(event, redTeamNumbers, blueTeamNumbers) {
}
export async function accuracy(event, matches, data, categories, teams) {
- return await accuracy2025(event, matches, data, categories, teams);
+ return await accuracy2026(event, matches, data, categories, teams);
}
-/*
export async function tps(data, categories, teams) {
return data.map((entry) => {
- if(!entry.event.startsWith("2024")) {
+ if (!entry.event.startsWith("2026")) {
return {
silentlyFail: true,
- hash: event.hash
+ hash: entry.hash
};
}
return {
silentlyFail: false,
- hash: event.hash,
+ hash: entry.hash,
entry: {
metadata: {
- event: entry.event || "2024all-prac",
+ event: entry.event || "2026all-prac",
match: {
level: "qm",
number: entry.match || 0,
@@ -1398,29 +1204,128 @@ export async function tps(data, categories, teams) {
}
},
abilities: {
- "auto-leave-starting-zone": find(entry, "abilities", categories, "24-0", false),
- "ground-pick-up": find(entry, "abilities", categories, "24-1", false),
- "auto-center-line-pick-up": find(entry, "abilities", categories, "24-18", false),
- "teleop-stage-level-2024": parseInt(find(entry, "abilities", categories, "24-4", false)),
- "teleop-spotlight-2024": find(entry, "abilities", categories, "24-5", false)
+ "auto-leave-starting-zone": find(
+ entry,
+ "abilities",
+ categories,
+ "26-0",
+ false
+ ),
+ "fuel-ground-intake": find(
+ entry,
+ "abilities",
+ categories,
+ "26-1",
+ false
+ ),
+ "outpost-intake": find(
+ entry,
+ "abilities",
+ categories,
+ "26-2",
+ false
+ ),
+ "passing-from-neutral-zone": find(
+ entry,
+ "abilities",
+ categories,
+ "26-3",
+ false
+ ),
+ "traverse-under-trench": find(
+ entry,
+ "abilities",
+ categories,
+ "26-4",
+ false
+ ),
+ "traverse-over-bump": find(
+ entry,
+ "abilities",
+ categories,
+ "26-5",
+ false
+ ),
+ "l1-climb": find(
+ entry,
+ "abilities",
+ categories,
+ "26-6",
+ false
+ ),
+ "climb-level-2026": parseInt(
+ find(entry, "abilities", categories, "26-9", 0)
+ )
+ },
+ counters: {
+ "auto-fuel-count": parseInt(
+ find(entry, "counters", categories, "26-19", 0)
+ ),
+ "teleop-fuel-count": parseInt(
+ find(entry, "counters", categories, "26-21", 0)
+ )
},
- counters: {},
data: {
- "auto-scoring-2024": find(entry, "data", categories, "24-2", []),
- "teleop-scoring-2024": find(entry, "data", categories, "24-3", []),
+ "auto-fuel-scoring-2026": find(
+ entry,
+ "data",
+ categories,
+ "26-7",
+ []
+ ),
+ "teleop-fuel-scoring-2026": find(
+ entry,
+ "data",
+ categories,
+ "26-8",
+ []
+ ),
+ "auto-fuel-locations": find(
+ entry,
+ "data",
+ categories,
+ "26-18",
+ []
+ ),
+ "teleop-fuel-locations": find(
+ entry,
+ "data",
+ categories,
+ "26-20",
+ []
+ ),
notes: entry.comments || ""
},
ratings: {
- "driver-skill": parseInt(find(entry, "ratings", categories, "24-9", 0)),
- "defense-skill": parseInt(find(entry, "ratings", categories, "24-10", 0)),
- speed: parseInt(find(entry, "ratings", categories, "24-11", 0)),
- stability: parseInt(find(entry, "ratings", categories, "24-12", 0)),
- "intake-consistency": parseInt(find(entry, "ratings", categories, "24-13", 0))
+ "driver-skill": parseInt(
+ find(entry, "ratings", categories, "26-13", 0)
+ ),
+ "defense-skill": parseInt(
+ find(entry, "ratings", categories, "26-14", 0)
+ ),
+ speed: parseInt(
+ find(entry, "ratings", categories, "26-15", 0)
+ ),
+ stability: parseInt(
+ find(entry, "ratings", categories, "26-16", 0)
+ ),
+ "intake-consistency": parseInt(
+ find(entry, "ratings", categories, "26-17", 0)
+ ),
+ "balls-per-second": parseInt(
+ find(entry, "ratings", categories, "26-22", 0)
+ )
},
timers: {
- "brick-time": parseInt(find(entry, "timers", categories, "24-7", 0))
- "defense-time": parseInt(find(entry, "timers", categories, "24-8", 0)),
- "stage-time-2024": parseInt(find(entry, "timers", categories, "24-6", 0))
+ "climb-time": parseInt(
+ find(entry, "timers", categories, "26-10", 0)
+ ),
+ "brick-time": parseInt(
+ find(entry, "timers", categories, "26-11", 0)
+ ),
+ "defense-time": parseInt(
+ find(entry, "timers", categories, "26-12", 0)
+ )
}
},
privacy: [
@@ -1444,20 +1349,19 @@ export async function tps(data, categories, teams) {
};
});
}
-*/
-const scouting2025 = {
+const scouting2026 = {
categories,
layout,
preload,
formatData,
formatParsedData,
- notes
- /* formPicklist,
+ notes,
+ formPicklist,
analysis,
compare,
predict,
accuracy,
- tps*/
+ tps
};
-export default scouting2025;
+export default scouting2026;
diff --git a/config/scouting/2026/predictions_2026.py b/config/scouting/2026/predictions_2026.py
new file mode 100644
index 0000000..c63a0f7
--- /dev/null
+++ b/config/scouting/2026/predictions_2026.py
@@ -0,0 +1,449 @@
+'''
+python predictions_2026.py
+
+ --event tba/frc event key
+ --csv filename of tpw data
+ --baseFilePath base filesystem path
+ --b1 blue team 1
+ --b2 blue team 2
+ --b3 blue team 3
+ --r1 red team 1
+ --r2 red team 2
+ --r3 red team 3
+ --match the match number up to which data should be used
+
+tba cached data must be in file named: [event]-tba.json
+
+stores prediction in json file:
+
+ filename: [event]-[r1]-[r2]-[r3]-[b1]-[b2]-[b3]-prediction.json
+
+caches parsed data to json file:
+
+ filename: parsed_tpw_data_[event].json
+'''
+
+
+import numpy as np
+from collections import OrderedDict
+import json
+import os
+import math
+import sys
+import csv
+import pandas as pd
+
+rawArgs = sys.argv[1:]
+args = {}
+for i in range(len(rawArgs)):
+ if rawArgs[i] == "--event" and "event" not in args:
+ args["event"] = rawArgs[i + 1]
+ i += 1
+ elif rawArgs[i] == "--csv" and "csv" not in args:
+ args["csv"] = rawArgs[i + 1]
+ i += 1
+ elif rawArgs[i] == "--baseFilePath" and "baseFilePath" not in args:
+ args["baseFilePath"] = rawArgs[i + 1]
+ i += 1
+ elif rawArgs[i] == "--b1" and "b1" not in args:
+ args["b1"] = rawArgs[i + 1]
+ i += 1
+ elif rawArgs[i] == "--b2" and "b2" not in args:
+ args["b2"] = rawArgs[i + 1]
+ i += 1
+ elif rawArgs[i] == "--b3" and "b3" not in args:
+ args["b3"] = rawArgs[i + 1]
+ i += 1
+ elif rawArgs[i] == "--r1" and "r1" not in args:
+ args["r1"] = rawArgs[i + 1]
+ i += 1
+ elif rawArgs[i] == "--r2" and "r2" not in args:
+ args["r2"] = rawArgs[i + 1]
+ i += 1
+ elif rawArgs[i] == "--r3" and "r3" not in args:
+ args["r3"] = rawArgs[i + 1]
+ i += 1
+ elif rawArgs[i] == "--match" and "match" not in args:
+ args["match"] = rawArgs[i + 1]
+ i += 1
+
+try:
+ match_num = args["match"]
+except:
+ match_num = 100000000
+event = args["event"]
+base = args["baseFilePath"]
+tpw_csv = args["csv"]
+parsed_data = OrderedDict()
+
+def avg(data):
+ if data != []:
+ data = np.array([data])
+ return np.mean(data)
+ else:
+ return 0
+
+def std(data):
+ if data != []:
+ data = np.array([data])
+ return np.std(data)
+ else:
+ return 0
+
+def max(data):
+ if data != []:
+ data = np.array([data])
+ return np.max(data)
+ else:
+ return 0
+
+def min(data):
+ if data != []:
+ data = np.array([data])
+ return np.min(data)
+ else:
+ return 0
+
+def copy(li1):
+ li_copy = []
+ li_copy.extend(li1)
+ return li_copy
+
+tpw_path = base + tpw_csv
+path = base + event + "-tba.json"
+if os.path.exists(path):
+ with open(base + event + "-tba.json", "r") as file:
+ data = json.load(file)
+else:
+ raise Exception("Could not find TBA file")
+
+team_data = OrderedDict()
+matches_data = OrderedDict()
+
+for x in (data):
+ try:
+ blue_teams = x["alliances"]["blue"]["team_keys"]
+ red_teams = x["alliances"]["red"]["team_keys"]
+
+ blue_score = x["alliances"]["blue"]["score"]
+ red_score = x["alliances"]["red"]["score"]
+
+ blue_fouls = x["score_breakdown"]["blue"]["foulCount"] + x["score_breakdown"]["blue"]["techFoulCount"]
+ red_fouls = x["score_breakdown"]["red"]["foulCount"] + x["score_breakdown"]["red"]["techFoulCount"]
+
+ blue_teleop = x["score_breakdown"]["blue"]["teleopPoints"]
+ red_teleop = x["score_breakdown"]["red"]["teleopPoints"]
+
+ for y in blue_teams:
+ match_data = OrderedDict()
+ match_data["score"] = blue_score
+ match_data["fouls"] = blue_fouls
+ match_data["teleop"] = blue_teleop
+
+ try:
+ count = len(team_data[y[3:]])
+ team_data[y[3:]][count] = match_data
+ except:
+ team_data[y[3:]] = OrderedDict()
+ team_data[y[3:]][0] = match_data
+
+ for y in red_teams:
+ match_data = OrderedDict()
+ match_data["score"] = red_score
+ match_data["fouls"] = red_fouls
+ match_data["teleop"] = red_teleop
+
+ try:
+ count = len(team_data[y[3:]])
+ team_data[y[3:]][count] = match_data
+ except:
+ team_data[y[3:]] = OrderedDict()
+ team_data[y[3:]][0] = match_data
+ except:
+ continue
+
+for team, dict in team_data.items():
+ scores = list()
+ fouls = list()
+ teleop = list()
+
+
+ for match in team_data[team].items():
+ scores.append(match[1]["score"])
+ fouls.append(match[1]["fouls"])
+ teleop.append(match[1]["teleop"])
+
+ data = OrderedDict()
+ data["avg-score"] = avg(scores)
+ data["std-score"] = std(scores)
+ data["avg-fouls"] = avg(fouls)
+ data["std-teleop"] = std(teleop)
+ parsed_data[team] = data
+
+def getData():
+ team_data = OrderedDict()
+ data_length = 0
+
+ if os.path.exists(tpw_path):
+ with open(tpw_path, "r") as file:
+ TPW_data = csv.DictReader(file)
+ for x in TPW_data:
+ data_length += 1
+ if x['team'] not in team_data:
+ team_data[x['team']] = [x]
+ else:
+ team_data[x['team']].append(x)
+ else:
+ raise Exception("Could not find TPW file")
+
+ parsed_tpw_data = OrderedDict()
+ for team, dict in team_data.items():
+ afgps = list()
+ tfgps = list()
+ afgpts = {}
+ tfgpts = {}
+ l1climbs = list()
+ egcpts = list() # endgame climb points
+ defe = list()
+ speed = list()
+ driver = list()
+ stab = list()
+ inta = list()
+ uptime = list()
+ avg_auto_points = list()
+ avg_tele_points = list()
+ matches = {}
+
+ for x in dict:
+ auto_fuel_pieces = x['auto fuel scoring'][1:len(x['auto fuel scoring']) - 1].split(", ")
+ tele_fuel_pieces = x['teleop fuel scoring'][1:len(x['teleop fuel scoring']) - 1].split(", ")
+ game_pieces = auto_fuel_pieces + tele_fuel_pieces
+ afgps.append(auto_fuel_pieces)
+ tfgps.append(tele_fuel_pieces)
+ l1climbs.append(x.get('l1 climb', '').lower() == 'true' or x.get('l1 climb', '') == True)
+
+ c_lev = int(x['climb level'])
+ if c_lev == 0:
+ egcpts.append(0)
+ elif c_lev == 1:
+ egcpts.append(10)
+ elif c_lev == 2:
+ egcpts.append(20)
+ elif c_lev >= 3:
+ egcpts.append(30)
+
+ try:
+ defe.append(int(x["defense skill"]))
+ speed.append(int(x["speed"]))
+ stab.append(int(x["stability"]))
+ inta.append(int(x["intake consistency"]))
+ driver.append(int(x["driver skill"]))
+ uptime.append(153000 - int(x["brick time"]))
+ except:
+ defe.append(3)
+ speed.append(3)
+ stab.append(3)
+ inta.append(3)
+ driver.append(3)
+ uptime.append(100)
+
+ try:
+ matches[x['match']][(x[''])] = game_pieces
+ except:
+ matches[x['match']] = {x['']: game_pieces}
+
+ for i in range(len(afgps)):
+ afgpts[i] = 0
+ for j in range(len(afgps[i])):
+ val = afgps[i][j]
+ if val == "fsa":
+ afgpts[i] = afgpts.get(i, 0) + 1
+ else:
+ afgpts[i] = afgpts.get(i, 0) + 0
+ if l1climbs[i]:
+ afgpts[i] = afgpts.get(i, 0) + 15
+ avg_auto_points.append(afgpts[i])
+ for i in range(len(tfgps)):
+ tfgpts[i] = 0
+ for j in range(len(tfgps[i])):
+ val = tfgps[i][j]
+ if val == "fsa":
+ tfgpts[i] = tfgpts.get(i, 0) + 1
+ elif val == "fp":
+ tfgpts[i] = tfgpts.get(i, 0) + 0
+ else:
+ tfgpts[i] = tfgpts.get(i, 0) + 0
+ avg_tele_points.append(tfgpts[i])
+
+ data_tpw = OrderedDict()
+ data_tpw['avg-tele'] = avg(avg_tele_points)
+ data_tpw['avg-auto'] = avg(avg_auto_points)
+ data_tpw['avg-climb'] = avg(egcpts)
+ data_tpw['avg-def'] = avg(defe)
+ data_tpw['avg-driv'] = avg(driver)
+ data_tpw['avg-speed'] = avg(speed)
+ data_tpw['avg-stab'] = avg(stab)
+ data_tpw['avg-inta'] = avg(inta)
+ data_tpw['avg-upt'] = avg(uptime)
+ data_tpw['matches'] = matches
+ data_tpw['tpw-std'] = std(avg_auto_points) + std(avg_tele_points) + std(egcpts)
+ data_tpw["tpw-score"] = data_tpw['avg-auto'] + data_tpw['avg-tele'] + data_tpw['avg-climb']
+ parsed_tpw_data[team] = data_tpw
+
+ with open(base + 'parsed_tpw_data_'+event+'.json', 'w') as f:
+ f.write(json.dumps({'lines': data_length, 'data': parsed_tpw_data}, default=int))
+ f.close()
+ return parsed_tpw_data
+
+def getDataLength():
+ data_length = 0
+ if os.path.exists(tpw_path):
+ with open(tpw_path, "r") as file:
+ TPW_data = csv.DictReader(file)
+ for x in TPW_data:
+ data_length += 1
+ else:
+ raise Exception("Could not find TPW file")
+
+ return data_length
+
+
+if os.path.exists(base + 'parsed_tpw_data_'+event+'.json'):
+ with open(base + 'parsed_tpw_data_'+event+'.json') as f:
+ loaded = json.loads(f.read())
+ if loaded['lines'] == getDataLength():
+ parsed_tpw_data = loaded['data']
+ f.close()
+ else:
+ f.close()
+ parsed_tpw_data = getData()
+else:
+ parsed_tpw_data = getData()
+
+def tba_predict(b1, b2, b3, r1, r2, r3):
+ b1 = str(b1)
+ b2 = str(b2)
+ b3 = str(b3)
+ r1 = str(r1)
+ r2 = str(r2)
+ r3 = str(r3)
+
+ bas = max([parsed_data[b1]['avg-score'], parsed_data[b2]['avg-score'], parsed_data[b3]['avg-score']]) + min([parsed_data[b1]['avg-score'], parsed_data[b2]['avg-score'], parsed_data[b3]['avg-score']])
+ ras = max([parsed_data[r1]['avg-score'], parsed_data[r2]['avg-score'], parsed_data[r3]['avg-score']]) + min([parsed_data[r1]['avg-score'], parsed_data[r2]['avg-score'], parsed_data[r3]['avg-score']])
+
+ baf = max([parsed_data[b1]['avg-fouls'], parsed_data[b2]['avg-fouls'], parsed_data[b3]['avg-fouls']]) + min([parsed_data[b1]['avg-fouls'], parsed_data[b2]['avg-fouls'], parsed_data[b3]['avg-fouls']])
+ raf = max([parsed_data[r1]['avg-fouls'], parsed_data[r2]['avg-fouls'], parsed_data[r3]['avg-fouls']]) + min([parsed_data[r1]['avg-fouls'], parsed_data[r2]['avg-fouls'], parsed_data[r3]['avg-fouls']])
+
+ batstd = avg([parsed_data[b1]['std-teleop'], parsed_data[b2]['std-teleop'], parsed_data[b3]['std-teleop']])
+ ratstd = avg([parsed_data[r1]['std-teleop'], parsed_data[r2]['std-teleop'], parsed_data[r3]['std-teleop']])
+
+ bmstd = min([parsed_data[b1]['std-score'], parsed_data[b2]['std-score'], parsed_data[b3]['std-score']])
+ rmstd = min([parsed_data[r1]['std-score'], parsed_data[r2]['std-score'], parsed_data[r3]['std-score']])
+
+ bmxstd = max([parsed_data[b1]['std-score'], parsed_data[b2]['std-score'], parsed_data[b3]['std-score']])
+ rmxstd = max([parsed_data[r1]['std-score'], parsed_data[r2]['std-score'], parsed_data[r3]['std-score']])
+
+ bastd = std([parsed_data[b1]['std-score'], parsed_data[b2]['std-score'], parsed_data[b3]['std-score']])
+ rastd = std([parsed_data[r1]['std-score'], parsed_data[r2]['std-score'], parsed_data[r3]['std-score']])
+
+ brstd = bmxstd - bmstd
+ rrstd = rmxstd - rmstd
+
+ bluescore = bas - bastd - brstd + raf - batstd
+ redscore = ras - rastd - rrstd + baf - ratstd
+
+ if bluescore > redscore:
+ return {'winner':'blue', 'blue-predicted': bluescore, 'red-predicted': redscore, 'blue-percent':bluescore/(bluescore + redscore), 'red-percent':redscore/(bluescore + redscore)}
+ else:
+ return {'winner':'red', 'blue-predicted': bluescore, 'red-predicted': redscore, 'blue-percent':bluescore/(bluescore + redscore), 'red-percent':redscore/(bluescore + redscore)}
+
+def tpw_predict(b1, b2, b3, r1, r2, r3):
+ b1 = str(b1)
+ b2 = str(b2)
+ b3 = str(b3)
+ r1 = str(r1)
+ r2 = str(r2)
+ r3 = str(r3)
+
+ bas = max([parsed_tpw_data[b1]['tpw-score'], parsed_tpw_data[b2]['tpw-score'], parsed_tpw_data[b3]['tpw-score']]) + min([parsed_tpw_data[b1]['tpw-score'], parsed_tpw_data[b2]['tpw-score'], parsed_tpw_data[b3]['tpw-score']])
+ ras = max([parsed_tpw_data[r1]['tpw-score'], parsed_tpw_data[r2]['tpw-score'], parsed_tpw_data[r3]['tpw-score']]) + min([parsed_tpw_data[r1]['tpw-score'], parsed_tpw_data[r2]['tpw-score'], parsed_tpw_data[r3]['tpw-score']])
+
+ bmstd = min([parsed_tpw_data[b1]['tpw-std'], parsed_tpw_data[b2]['tpw-std'], parsed_tpw_data[b3]['tpw-std']])
+ rmstd = min([parsed_tpw_data[r1]['tpw-std'], parsed_tpw_data[r2]['tpw-std'], parsed_tpw_data[r3]['tpw-std']])
+
+ bmxstd = max([parsed_tpw_data[b1]['tpw-std'], parsed_tpw_data[b2]['tpw-std'], parsed_tpw_data[b3]['tpw-std']])
+ rmxstd = max([parsed_tpw_data[r1]['tpw-std'], parsed_tpw_data[r2]['tpw-std'], parsed_tpw_data[r3]['tpw-std']])
+
+ bastd = avg([parsed_tpw_data[b1]['tpw-std'], parsed_tpw_data[b2]['tpw-std'], parsed_tpw_data[b3]['tpw-std']])
+ rastd = avg([parsed_tpw_data[r1]['tpw-std'], parsed_tpw_data[r2]['tpw-std'], parsed_tpw_data[r3]['tpw-std']])
+
+ brstd = bmxstd - bmstd
+ rrstd = rmxstd - rmstd
+
+ baat = avg([parsed_tpw_data[b1]['avg-tele'] + parsed_tpw_data[b1]['avg-auto'], parsed_tpw_data[b2]['avg-tele'] + parsed_tpw_data[b2]['avg-auto'], parsed_tpw_data[b3]['avg-tele'] + parsed_tpw_data[b3]['avg-auto']])
+ raat = avg([parsed_tpw_data[r1]['avg-tele'] + parsed_tpw_data[r1]['avg-auto'], parsed_tpw_data[r2]['avg-tele'] + parsed_tpw_data[r2]['avg-auto'], parsed_tpw_data[r3]['avg-tele'] + parsed_tpw_data[r3]['avg-auto']])
+
+ bd = avg([parsed_tpw_data[b1]['avg-def'], parsed_tpw_data[b2]['avg-def'], parsed_tpw_data[b3]['avg-def']])
+ rd = avg([parsed_tpw_data[r1]['avg-def'], parsed_tpw_data[r2]['avg-def'], parsed_tpw_data[r3]['avg-def']])
+
+ bdr = avg([parsed_tpw_data[b1]['avg-driv'], parsed_tpw_data[b2]['avg-driv'], parsed_tpw_data[b3]['avg-driv']])
+ rdr = avg([parsed_tpw_data[r1]['avg-driv'], parsed_tpw_data[r2]['avg-driv'], parsed_tpw_data[r3]['avg-driv']])
+
+ bspd = avg([parsed_tpw_data[b1]['avg-speed'], parsed_tpw_data[b2]['avg-speed'], parsed_tpw_data[b3]['avg-speed']])
+ rspd = avg([parsed_tpw_data[r1]['avg-speed'], parsed_tpw_data[r2]['avg-speed'], parsed_tpw_data[r3]['avg-speed']])
+
+ bstab = avg([parsed_tpw_data[b1]['avg-stab'], parsed_tpw_data[b2]['avg-stab'], parsed_tpw_data[b3]['avg-stab']])
+ rstab = avg([parsed_tpw_data[r1]['avg-stab'], parsed_tpw_data[r2]['avg-stab'], parsed_tpw_data[r3]['avg-stab']])
+
+ binta = avg([parsed_tpw_data[b1]['avg-inta'], parsed_tpw_data[b2]['avg-inta'], parsed_tpw_data[b3]['avg-inta']])
+ rinta = avg([parsed_tpw_data[r1]['avg-inta'], parsed_tpw_data[r2]['avg-inta'], parsed_tpw_data[r3]['avg-inta']])
+
+ bmix = bd + bdr + bspd + bstab + binta
+ rmix = rd + rdr + rspd + rstab + rinta
+
+ bluescore = baat + bas - bastd - brstd
+ redscore = raat + ras - rastd - rrstd
+
+ if bluescore > redscore:
+ return {'winner':'blue', 'blue-predicted': bluescore, 'red-predicted': redscore, 'bp': bmix, 'rp': rmix, 'blue-percent':bluescore/(bluescore + redscore), 'red-percent':redscore/(bluescore + redscore)}
+ else:
+ return {'winner':'red', 'blue-predicted': bluescore, 'red-predicted': redscore, 'bp': bmix, 'rp': rmix, 'blue-percent':bluescore/(bluescore + redscore), 'red-percent':redscore/(bluescore + redscore)}
+
+def predict(b1, b2, b3, r1, r2, r3):
+ b1 = str(b1)
+ b2 = str(b2)
+ b3 = str(b3)
+ r1 = str(r1)
+ r2 = str(r2)
+ r3 = str(r3)
+
+ tpw = tpw_predict(b1, b2, b3, r1, r2, r3)
+
+ if len(parsed_data) >= len(parsed_tpw_data):
+ tba = tba_predict(b1, b2, b3, r1, r2, r3)
+
+ bs1 = tba["blue-predicted"]
+ bs2 = tpw["blue-predicted"]
+ bs3 = tpw["bp"]
+
+ rs1 = tba["red-predicted"]
+ rs2 = tpw["red-predicted"]
+ rs3 = tpw["rp"]
+
+ bp = bs1 + bs2 + 5*(bs3 - rs3)
+ rp = rs1 + rs2 + 5*(rs3 - bs3)
+ else:
+ bp = tpw["blue-predicted"]
+ rp = tpw["red-predicted"]
+
+ if bp > rp:
+ winner = 'blue'
+ else:
+ winner = 'red'
+
+ return {'winner': winner, 'blue': bp, 'red': rp}
+
+results = predict(args["b1"], args["b2"], args["b3"], args["r1"], args["r2"], args["r3"])
+
+with open(base + event + "-" + args["r1"] + "-" + args["r2"] + "-" + args["r3"] + "-" + args["b1"] + "-" + args["b2"] + "-" + args["b3"] + "-prediction.json", "w") as f:
+ json.dump(results, f)
diff --git a/config/scouting/2026/predictions_2026.ts b/config/scouting/2026/predictions_2026.ts
new file mode 100644
index 0000000..9370bda
--- /dev/null
+++ b/config/scouting/2026/predictions_2026.ts
@@ -0,0 +1,448 @@
+import fs from "fs";
+import { parsedRow } from ".";
+
+interface teamData {
+ [key: string]: any[];
+}
+
+interface matchData {
+ score: number;
+ fouls: number;
+ teleop: number;
+}
+
+interface tbaTeamData {
+ [matchIndex: number]: matchData;
+}
+
+interface prediction {
+ winner: "blue" | "red";
+ bluePredicted: number;
+ redPredicted: number;
+ bp?: number;
+ rp?: number;
+ bluePercent: number;
+ redPercent: number;
+}
+
+interface endPrediction {
+ winner: "blue" | "red";
+ blue: number;
+ red: number;
+ match?: any;
+ win?: any;
+}
+
+interface parsedData {
+ [team: string]: {
+ "avg-score": number;
+ "std-score": number;
+ "avg-fouls": number;
+ "std-teleop": number;
+ };
+}
+
+interface tbaData {
+ alliances: {
+ blue: {
+ team_keys: string[];
+ score: number;
+ };
+ red: {
+ team_keys: string[];
+ score: number;
+ };
+ };
+ score_breakdown: {
+ blue: {
+ foulCount: number;
+ techFoulCount: number;
+ teleopPoints: number;
+ };
+ red: {
+ foulCount: number;
+ techFoulCount: number;
+ teleopPoints: number;
+ };
+ };
+}
+
+interface parsedTPWData {
+ [team: string]: {
+ "avg-tele": number;
+ "avg-auto": number;
+ "avg-climb": number;
+ "avg-def": number;
+ "avg-driv": number;
+ "avg-speed": number;
+ "avg-stab": number;
+ "avg-inta": number;
+ "avg-upt": number;
+ matches: any;
+ "tpw-std": number;
+ "tpw-score": number;
+ "r-score"?: number;
+ };
+}
+
+// sum and average taken from https://gist.github.com/dggluz/365527824f9f521055baa3532b1d46e7
+const sum = (numbers: number[]) =>
+ numbers.reduce((total, aNumber) => total + aNumber, 0);
+const avg = (numbers: number[]) => sum(numbers) / numbers.length;
+
+// std taken from https://decipher.dev/30-seconds-of-typescript/docs/standardDeviation/
+const std = (arr) => {
+ const mean = arr.reduce((acc, val) => acc + val, 0) / arr.length;
+ return Math.sqrt(
+ arr
+ .reduce((acc, val) => acc.concat((val - mean) ** 2), [])
+ .reduce((acc, val) => acc + val, 0) / arr.length
+ );
+};
+
+function getData(data): any {
+ let team_data: teamData = {};
+ if (!data) {
+ throw new Error("No data given to getData()");
+ }
+ for (let x of data) {
+ if (team_data[x["team"]] == null) {
+ team_data[x["team"]] = [x];
+ } else {
+ team_data[x["team"]].push(x);
+ }
+ }
+ let parsed_tpw_data: parsedTPWData = {};
+ for (let team in team_data) {
+ let afgps = [];
+ let tfgps = [];
+ let afgpts = {};
+ let tfgpts = {};
+ let l1climbs = [];
+ let egcpts = [];
+ let defe = [];
+ let speed = [];
+ let driver = [];
+ let stab = [];
+ let inta = [];
+ let uptime = [];
+ let avg_auto_points = [];
+ let avg_tele_points = [];
+ let matches = {};
+ for (let x of team_data[team]) {
+ let auto_fuel_pieces = x["auto fuel scoring"]
+ .slice(1, x["auto fuel scoring"].length - 1)
+ .split(", ");
+ let tele_fuel_pieces = x["teleop fuel scoring"]
+ .slice(1, x["teleop fuel scoring"].length - 1)
+ .split(", ");
+ let game_pieces = auto_fuel_pieces.concat(tele_fuel_pieces);
+ afgps.push(auto_fuel_pieces);
+ tfgps.push(tele_fuel_pieces);
+ l1climbs.push(x["l1 climb"] === true || x["l1 climb"] === "true");
+ let climb_lev = parseInt(x["climb level"]);
+ if (climb_lev == 0) {
+ egcpts.push(0);
+ } else if (climb_lev == 1) {
+ egcpts.push(10);
+ } else if (climb_lev == 2) {
+ egcpts.push(20);
+ } else if (climb_lev >= 3) {
+ egcpts.push(30);
+ }
+ try {
+ defe.push(parseInt(x["defense skill"]));
+ speed.push(parseInt(x["speed"]));
+ stab.push(parseInt(x["stability"]));
+ inta.push(parseInt(x["intake consistency"]));
+ driver.push(parseInt(x["driver skill"]));
+ uptime.push(153000 - parseInt(x["brick time"]));
+ } catch {
+ defe.push(3);
+ speed.push(3);
+ stab.push(3);
+ inta.push(3);
+ driver.push(3);
+ uptime.push(100);
+ }
+ try {
+ matches[x["match"]][x[""]] = game_pieces;
+ } catch {
+ matches[x["match"]] = { [x[""]]: game_pieces };
+ }
+ }
+ for (let i = 0; i < afgps.length; ++i) {
+ for (let j = 0; j < afgps[i].length; ++j) {
+ let val = afgps[i][j];
+ if (val == "fsa") {
+ afgpts[i] = (afgpts[i] || 0) + 1;
+ } else {
+ afgpts[i] = (afgpts[i] || 0) + 0;
+ }
+ }
+ if (l1climbs[i]) {
+ afgpts[i] = (afgpts[i] || 0) + 15;
+ }
+ avg_auto_points.push(afgpts[i]);
+ }
+ for (let i = 0; i < tfgps.length; ++i) {
+ for (let j = 0; j < tfgps[i].length; ++j) {
+ let val = tfgps[i][j];
+ if (val == "fsa") {
+ tfgpts[i] = (tfgpts[i] || 0) + 1;
+ } else if (val == "fp") {
+ tfgpts[i] = (tfgpts[i] || 0) + 0;
+ } else {
+ tfgpts[i] = (tfgpts[i] || 0) + 0;
+ }
+ }
+ avg_tele_points.push(tfgpts[i]);
+ }
+ let data_tpw: parsedTPWData[string] = {
+ "avg-tele": avg(avg_tele_points),
+ "avg-auto": avg(avg_auto_points),
+ "avg-climb": avg(egcpts),
+ "avg-def": avg(defe),
+ "avg-driv": avg(driver),
+ "avg-speed": avg(speed),
+ "avg-stab": avg(stab),
+ "avg-inta": avg(inta),
+ "avg-upt": avg(uptime),
+ matches: matches,
+ "tpw-std":
+ std(avg_auto_points) + std(avg_tele_points) + std(egcpts),
+ "tpw-score": 0
+ };
+ data_tpw["tpw-score"] =
+ data_tpw["avg-auto"] + data_tpw["avg-tele"] + data_tpw["avg-climb"];
+ parsed_tpw_data[team] = data_tpw;
+ }
+ return parsed_tpw_data;
+}
+
+function tbaFile(path: string): parsedData {
+ if (!fs.existsSync(path)) {
+ throw new Error("TBA file does not exist");
+ }
+ const data: tbaData[] = JSON.parse(fs.readFileSync(path, "utf-8"));
+ const tdata: { [team: string]: tbaTeamData } = {};
+ const pdata: parsedData = {};
+ for (const match of data) {
+ try {
+ const bteams = match.alliances.blue.team_keys;
+ const rteams = match.alliances.red.team_keys;
+ const bscore = match.alliances.blue.score;
+ const rscore = match.alliances.red.score;
+ const bfouls =
+ match.score_breakdown.blue.foulCount +
+ match.score_breakdown.blue.techFoulCount;
+ const rfouls =
+ match.score_breakdown.red.foulCount +
+ match.score_breakdown.red.techFoulCount;
+ const bteleop = match.score_breakdown.blue.teleopPoints;
+ const rteleop = match.score_breakdown.red.teleopPoints;
+ for (const team of bteams) {
+ let key = team.slice(3);
+ let match: matchData = {
+ score: bscore,
+ fouls: bfouls,
+ teleop: bteleop
+ };
+ if (!tdata[key]) {
+ tdata[key] = {};
+ }
+ let mind = Object.keys(tdata[key]).length;
+ tdata[key][mind] = match;
+ }
+ for (const team of rteams) {
+ let key = team.slice(3);
+ let match: matchData = {
+ score: rscore,
+ fouls: rfouls,
+ teleop: rteleop
+ };
+ if (!tdata[key]) {
+ tdata[key] = {};
+ }
+ let mind = Object.keys(tdata[key]).length;
+ tdata[key][mind] = match;
+ }
+ } catch (error) {
+ console.error("Error during tba match processing: ", error);
+ continue;
+ }
+ }
+ for (const team in tdata) {
+ const scores: number[] = [];
+ const fouls: number[] = [];
+ const teleop: number[] = [];
+ for (const match of Object.values(tdata[team])) {
+ scores.push(match.score);
+ fouls.push(match.fouls);
+ teleop.push(match.teleop);
+ }
+ pdata[team] = {
+ "avg-score": avg(scores),
+ "std-score": std(scores),
+ "avg-fouls": avg(fouls),
+ "std-teleop": std(teleop)
+ };
+ }
+ return pdata;
+}
+
+function tbaPredict(
+ b1: number,
+ b2: number,
+ b3: number,
+ r1: number,
+ r2: number,
+ r3: number,
+ tbaData: parsedData
+): prediction {
+ const bteams = [String(b1), String(b2), String(b3)];
+ const rteams = [String(r1), String(r2), String(r3)];
+
+ const bascores = bteams.map((team) => tbaData[team]["avg-score"]);
+ const bas = Math.max(...bascores) + Math.min(...bascores);
+ const bafouls = bteams.map((team) => tbaData[team]["avg-fouls"]);
+ const baf = Math.max(...bafouls) + Math.min(...bafouls);
+ const batstd = avg(bteams.map((team) => tbaData[team]["std-teleop"]));
+ const bstdscores = bteams.map((team) => tbaData[team]["std-score"]);
+ const bmstd = Math.min(...bstdscores);
+ const bmxstd = Math.max(...bstdscores);
+ const bastd = std(bstdscores);
+ const brstd = bmxstd - bmstd;
+
+ const rascores = rteams.map((team) => tbaData[team]["avg-score"]);
+ const ras = Math.max(...rascores) + Math.min(...rascores);
+ const rafouls = rteams.map((team) => tbaData[team]["avg-fouls"]);
+ const raf = Math.max(...rafouls) + Math.min(...rafouls);
+ const ratstd = avg(rteams.map((team) => tbaData[team]["std-teleop"]));
+ const rstdscores = rteams.map((team) => tbaData[team]["std-score"]);
+ const rmstd = Math.min(...rstdscores);
+ const rmxstd = Math.max(...rstdscores);
+ const rastd = std(rstdscores);
+ const rrstd = rmxstd - rmstd;
+
+ const bluescore = bas - bastd - brstd + raf - batstd;
+ const redscore = ras - rastd - rrstd + baf - ratstd;
+ const bpercent = bluescore / (bluescore + redscore);
+ const rpercent = redscore / (bluescore + redscore);
+ const w = bluescore > redscore ? "blue" : "red";
+
+ return {
+ winner: w,
+ bluePredicted: bluescore,
+ redPredicted: redscore,
+ bluePercent: bpercent,
+ redPercent: rpercent
+ };
+}
+
+function tpwPredict(
+ b1: number,
+ b2: number,
+ b3: number,
+ r1: number,
+ r2: number,
+ r3: number,
+ tpwData: parsedTPWData
+): prediction {
+ const bteams = [String(b1), String(b2), String(b3)];
+ const rteams = [String(r1), String(r2), String(r3)];
+
+ const bascores = bteams.map((team) => tpwData[team]["tpw-score"]);
+ const bas = Math.max(...bascores) + Math.min(...bascores);
+ const bstdscores = bteams.map((team) => tpwData[team]["tpw-std"]);
+ const bmstd = Math.min(...bstdscores);
+ const bmxstd = Math.max(...bstdscores);
+ const bastd = avg(bstdscores);
+ const brstd = bmxstd - bmstd;
+ const baat = avg(
+ bteams.map(
+ (team) => tpwData[team]["avg-tele"] + tpwData[team]["avg-auto"]
+ )
+ );
+ const bd = avg(bteams.map((team) => tpwData[team]["avg-def"]));
+ const bdr = avg(bteams.map((team) => tpwData[team]["avg-driv"]));
+ const bspd = avg(bteams.map((team) => tpwData[team]["avg-speed"]));
+ const bstab = avg(bteams.map((team) => tpwData[team]["avg-stab"]));
+ const binta = avg(bteams.map((team) => tpwData[team]["avg-inta"]));
+ const bmix = bd + bdr + bspd + bstab + binta;
+
+ const rascores = rteams.map((team) => tpwData[team]["tpw-score"]);
+ const ras = Math.max(...rascores) + Math.min(...rascores);
+ const rstdscores = rteams.map((team) => tpwData[team]["tpw-std"]);
+ const rmstd = Math.min(...rstdscores);
+ const rmxstd = Math.max(...rstdscores);
+ const rastd = avg(rstdscores);
+ const rrstd = rmxstd - rmstd;
+ const raat = avg(
+ rteams.map(
+ (team) => tpwData[team]["avg-tele"] + tpwData[team]["avg-auto"]
+ )
+ );
+ const rd = avg(rteams.map((team) => tpwData[team]["avg-def"]));
+ const rdr = avg(rteams.map((team) => tpwData[team]["avg-driv"]));
+ const rspd = avg(rteams.map((team) => tpwData[team]["avg-speed"]));
+ const rstab = avg(rteams.map((team) => tpwData[team]["avg-stab"]));
+ const rinta = avg(rteams.map((team) => tpwData[team]["avg-inta"]));
+ const rmix = rd + rdr + rspd + rstab + rinta;
+
+ const bluescore = baat + bas - bastd - brstd;
+ const redscore = raat + ras - rastd - rrstd;
+ const bpercent = bluescore / (bluescore + redscore);
+ const rpercent = redscore / (bluescore + redscore);
+ const w = bluescore > redscore ? "blue" : "red";
+
+ return {
+ winner: w,
+ bluePredicted: bluescore,
+ redPredicted: redscore,
+ bp: bmix,
+ rp: rmix,
+ bluePercent: bpercent,
+ redPercent: rpercent
+ };
+}
+
+export function computePrediction(
+ b1: number,
+ b2: number,
+ b3: number,
+ r1: number,
+ r2: number,
+ r3: number,
+ data: parsedRow[],
+ base: string,
+ event: string
+): endPrediction {
+ const path = base + event + "-tba.json";
+ const data_tba = tbaFile(path);
+ const data_tpw: parsedTPWData = getData(data);
+
+ const tpw = tpwPredict(b1, b2, b3, r1, r2, r3, data_tpw);
+ let bp: number;
+ let rp: number;
+ if (Object.keys(data_tba).length >= Object.keys(data_tpw).length) {
+ const tba = tbaPredict(b1, b2, b3, r1, r2, r3, data_tba);
+ const bs1 = tba.bluePredicted;
+ const bs2 = tpw.bluePredicted;
+ const bs3 = tpw.bp;
+ const rs1 = tba.redPredicted;
+ const rs2 = tpw.redPredicted;
+ const rs3 = tpw.rp;
+ bp = bs1 + bs2 + 5 * (bs3 - rs3);
+ rp = rs1 + rs2 + 5 * (rs3 - bs3);
+ } else {
+ bp = tpw.bluePredicted;
+ rp = tpw.redPredicted;
+ }
+ const w = bp > rp ? "blue" : "red";
+ return {
+ winner: w,
+ blue: bp,
+ red: rp
+ };
+}
diff --git a/config/scouting/2026/rankings_2026.py b/config/scouting/2026/rankings_2026.py
new file mode 100644
index 0000000..8470b3d
--- /dev/null
+++ b/config/scouting/2026/rankings_2026.py
@@ -0,0 +1,224 @@
+'''
+python rankings_2026.py
+
+ --event tba/frc event key
+ --csv filename of tpw data
+ --baseFilePath base filesystem path
+
+stores rankings in json file:
+
+ filename: [event]-rankings.json
+
+caches parsed data to json file:
+
+ filename: parsed_tpw_data_[event].json
+'''
+
+
+import numpy as np
+from collections import OrderedDict
+import json
+import os
+import math
+import sys
+import csv
+import pandas as pd
+
+rawArgs = sys.argv[1:]
+args = {}
+for i in range(len(rawArgs)):
+ if rawArgs[i] == "--event" and "event" not in args:
+ args["event"] = rawArgs[i + 1]
+ i += 1
+ elif rawArgs[i] == "--csv" and "csv" not in args:
+ args["csv"] = rawArgs[i + 1]
+ i += 1
+ elif rawArgs[i] == "--baseFilePath" and "baseFilePath" not in args:
+ args["baseFilePath"] = rawArgs[i + 1]
+ i += 1
+
+event = args["event"]
+base = args["baseFilePath"]
+tpw_csv = args["csv"]
+
+def avg(data):
+ if data != []:
+ data = np.array([data])
+ return np.mean(data)
+ else:
+ return 0
+
+def std(data):
+ if data != []:
+ data = np.array([data])
+ return np.std(data)
+ else:
+ return 0
+
+def max(data):
+ if data != []:
+ data = np.array([data])
+ return np.max(data)
+ else:
+ return 0
+
+def min(data):
+ if data != []:
+ data = np.array([data])
+ return np.min(data)
+ else:
+ return 0
+
+tpw_path = base + tpw_csv
+
+def getData():
+ team_data = OrderedDict()
+ data_length = 0
+
+ if os.path.exists(tpw_path):
+ with open(tpw_path, "r") as file:
+ TPW_data = csv.DictReader(file)
+ for x in TPW_data:
+ data_length += 1
+ if x['team'] not in team_data:
+ team_data[x['team']] = [x]
+ else:
+ team_data[x['team']].append(x)
+ else:
+ raise Exception("Could not find TPW file")
+
+ parsed_tpw_data = OrderedDict()
+ for team, dict in team_data.items():
+ afgps = list()
+ tfgps = list()
+ afgpts = {}
+ tfgpts = {}
+ l1climbs = list()
+ egcpts = list() # endgame climb points
+ defe = list()
+ speed = list()
+ driver = list()
+ stab = list()
+ inta = list()
+ uptime = list()
+ avg_auto_points = list()
+ avg_tele_points = list()
+ matches = {}
+
+ for x in dict:
+ auto_fuel_pieces = x['auto fuel scoring'][1:len(x['auto fuel scoring']) - 1].split(", ")
+ tele_fuel_pieces = x['teleop fuel scoring'][1:len(x['teleop fuel scoring']) - 1].split(", ")
+ game_pieces = auto_fuel_pieces + tele_fuel_pieces
+ afgps.append(auto_fuel_pieces)
+ tfgps.append(tele_fuel_pieces)
+ l1climbs.append(x.get('l1 climb', '').lower() == 'true' or x.get('l1 climb', '') == True)
+
+ c_lev = int(x['climb level'])
+ if c_lev == 0:
+ egcpts.append(0)
+ elif c_lev == 1:
+ egcpts.append(10)
+ elif c_lev == 2:
+ egcpts.append(20)
+ elif c_lev >= 3:
+ egcpts.append(30)
+
+ try:
+ defe.append(int(x["defense skill"]))
+ speed.append(int(x["speed"]))
+ stab.append(int(x["stability"]))
+ inta.append(int(x["intake consistency"]))
+ driver.append(int(x["driver skill"]))
+ uptime.append(153000 - int(x["brick time"]))
+ except:
+ defe.append(3)
+ speed.append(3)
+ stab.append(3)
+ inta.append(3)
+ driver.append(3)
+ uptime.append(100)
+
+ try:
+ matches[x['match']][(x[''])] = game_pieces
+ except:
+ matches[x['match']] = {x['']: game_pieces}
+
+ for i in range(len(afgps)):
+ afgpts[i] = 0
+ for j in range(len(afgps[i])):
+ val = afgps[i][j]
+ if val == "fsa":
+ afgpts[i] = afgpts.get(i, 0) + 1
+ else:
+ afgpts[i] = afgpts.get(i, 0) + 0
+ if l1climbs[i]:
+ afgpts[i] = afgpts.get(i, 0) + 15
+ avg_auto_points.append(afgpts[i])
+ for i in range(len(tfgps)):
+ tfgpts[i] = 0
+ for j in range(len(tfgps[i])):
+ val = tfgps[i][j]
+ if val == "fsa":
+ tfgpts[i] = tfgpts.get(i, 0) + 1
+ elif val == "fp":
+ tfgpts[i] = tfgpts.get(i, 0) + 0
+ else:
+ tfgpts[i] = tfgpts.get(i, 0) + 0
+ avg_tele_points.append(tfgpts[i])
+
+ data_tpw = OrderedDict()
+ data_tpw['avg-tele'] = avg(avg_tele_points)
+ data_tpw['avg-auto'] = avg(avg_auto_points)
+ data_tpw['avg-climb'] = avg(egcpts)
+ data_tpw['avg-def'] = avg(defe)
+ data_tpw['avg-driv'] = avg(driver)
+ data_tpw['avg-speed'] = avg(speed)
+ data_tpw['avg-stab'] = avg(stab)
+ data_tpw['avg-inta'] = avg(inta)
+ data_tpw['avg-upt'] = avg(uptime)
+ data_tpw['matches'] = matches
+ data_tpw['tpw-std'] = std(avg_auto_points) + std(avg_tele_points) + std(egcpts)
+ data_tpw["tpw-score"] = data_tpw['avg-auto'] + data_tpw['avg-tele'] + data_tpw['avg-climb']
+ parsed_tpw_data[team] = data_tpw
+
+ with open(base + 'parsed_tpw_data_'+event+'.json', 'w') as f:
+ f.write(json.dumps({'lines': data_length, 'data': parsed_tpw_data}, default=int))
+ f.close()
+ return parsed_tpw_data
+
+def getDataLength():
+ data_length = 0
+ if os.path.exists(tpw_path):
+ with open(tpw_path, "r") as file:
+ TPW_data = csv.DictReader(file)
+ for x in TPW_data:
+ data_length += 1
+ else:
+ raise Exception("Could not find TPW file")
+
+ return data_length
+
+
+if os.path.exists(base + 'parsed_tpw_data_'+event+'.json'):
+ with open(base + 'parsed_tpw_data_'+event+'.json') as f:
+ loaded = json.loads(f.read())
+ if loaded['lines'] == getDataLength():
+ parsed_tpw_data = loaded['data']
+ f.close()
+ else:
+ f.close()
+ parsed_tpw_data = getData()
+else:
+ parsed_tpw_data = getData()
+
+for team, dict in parsed_tpw_data.items():
+ parsed_tpw_data[team]['r-score'] = parsed_tpw_data[team]["tpw-score"] - parsed_tpw_data[team]["tpw-std"] + parsed_tpw_data[team]["avg-driv"] + parsed_tpw_data[team]["avg-speed"] + parsed_tpw_data[team]["avg-stab"] + parsed_tpw_data[team]["avg-inta"]
+
+sorted_dict = OrderedDict(sorted(parsed_tpw_data.items(), key=lambda x: x[1]["r-score"]))
+public_dict = OrderedDict()
+
+for team, dict in sorted_dict.items():
+ public_dict[team] = {"off-score": dict["r-score"], "def-score": dict["avg-def"]}
+
+with open(base + event + "-rankings.json", "w") as f:
+ json.dump(public_dict, f)
diff --git a/config/scouting/2026/rankings_2026.ts b/config/scouting/2026/rankings_2026.ts
new file mode 100644
index 0000000..e1da786
--- /dev/null
+++ b/config/scouting/2026/rankings_2026.ts
@@ -0,0 +1,188 @@
+import { parsedRow } from ".";
+
+interface teamData {
+ [key: string]: any[];
+}
+
+interface parsedTPWData {
+ [team: string]: {
+ "avg-tele": number;
+ "avg-auto": number;
+ "avg-climb": number;
+ "avg-def": number;
+ "avg-driv": number;
+ "avg-speed": number;
+ "avg-stab": number;
+ "avg-inta": number;
+ "avg-upt": number;
+ matches: any;
+ "tpw-std": number;
+ "tpw-score": number;
+ "r-score"?: number;
+ };
+}
+
+interface rankings {
+ [team: string]: {
+ "off-score": number;
+ "def-score": number;
+ };
+}
+
+// sum and average taken from https://gist.github.com/dggluz/365527824f9f521055baa3532b1d46e7
+const sum = (numbers: number[]) =>
+ numbers.reduce((total, aNumber) => total + aNumber, 0);
+const avg = (numbers: number[]) => sum(numbers) / numbers.length;
+
+// std taken from https://decipher.dev/30-seconds-of-typescript/docs/standardDeviation/
+const std = (arr) => {
+ const mean = arr.reduce((acc, val) => acc + val, 0) / arr.length;
+ return Math.sqrt(
+ arr
+ .reduce((acc, val) => acc.concat((val - mean) ** 2), [])
+ .reduce((acc, val) => acc + val, 0) / arr.length
+ );
+};
+
+function getData(data): any {
+ let team_data: teamData = {};
+ if (!data) {
+ throw new Error("No data given to getData()");
+ }
+ for (let x of data) {
+ if (team_data[x["team"]] == null) {
+ team_data[x["team"]] = [x];
+ } else {
+ team_data[x["team"]].push(x);
+ }
+ }
+ let parsed_tpw_data: parsedTPWData = {};
+ for (let team in team_data) {
+ let afgps = [];
+ let tfgps = [];
+ let afgpts = {};
+ let tfgpts = {};
+ let l1climbs = [];
+ let egcpts = [];
+ let defe = [];
+ let speed = [];
+ let driver = [];
+ let stab = [];
+ let inta = [];
+ let uptime = [];
+ let avg_auto_points = [];
+ let avg_tele_points = [];
+ let matches = {};
+ for (let x of team_data[team]) {
+ let auto_fuel_pieces = x["auto fuel scoring"]
+ .slice(1, x["auto fuel scoring"].length - 1)
+ .split(", ");
+ let tele_fuel_pieces = x["teleop fuel scoring"]
+ .slice(1, x["teleop fuel scoring"].length - 1)
+ .split(", ");
+ let game_pieces = auto_fuel_pieces.concat(tele_fuel_pieces);
+ afgps.push(auto_fuel_pieces);
+ tfgps.push(tele_fuel_pieces);
+ l1climbs.push(x["l1 climb"] === true || x["l1 climb"] === "true");
+ let climb_lev = parseInt(x["climb level"]);
+ if (climb_lev == 0) {
+ egcpts.push(0);
+ } else if (climb_lev == 1) {
+ egcpts.push(10);
+ } else if (climb_lev == 2) {
+ egcpts.push(20);
+ } else if (climb_lev >= 3) {
+ egcpts.push(30);
+ }
+ try {
+ defe.push(parseInt(x["defense skill"]));
+ speed.push(parseInt(x["speed"]));
+ stab.push(parseInt(x["stability"]));
+ inta.push(parseInt(x["intake consistency"]));
+ driver.push(parseInt(x["driver skill"]));
+ uptime.push(153000 - parseInt(x["brick time"]));
+ } catch {
+ defe.push(3);
+ speed.push(3);
+ stab.push(3);
+ inta.push(3);
+ driver.push(3);
+ uptime.push(100);
+ }
+ try {
+ matches[x["match"]][x[""]] = game_pieces;
+ } catch {
+ matches[x["match"]] = { [x[""]]: game_pieces };
+ }
+ }
+ for (let i = 0; i < afgps.length; ++i) {
+ for (let j = 0; j < afgps[i].length; ++j) {
+ let val = afgps[i][j];
+ if (val == "fsa") {
+ afgpts[i] = (afgpts[i] || 0) + 1;
+ } else {
+ afgpts[i] = (afgpts[i] || 0) + 0;
+ }
+ }
+ if (l1climbs[i]) {
+ afgpts[i] = (afgpts[i] || 0) + 15;
+ }
+ avg_auto_points.push(afgpts[i]);
+ }
+ for (let i = 0; i < tfgps.length; ++i) {
+ for (let j = 0; j < tfgps[i].length; ++j) {
+ let val = tfgps[i][j];
+ if (val == "fsa") {
+ tfgpts[i] = (tfgpts[i] || 0) + 1;
+ } else if (val == "fp") {
+ tfgpts[i] = (tfgpts[i] || 0) + 0;
+ } else {
+ tfgpts[i] = (tfgpts[i] || 0) + 0;
+ }
+ }
+ avg_tele_points.push(tfgpts[i]);
+ }
+ let data_tpw: parsedTPWData[string] = {
+ "avg-tele": avg(avg_tele_points),
+ "avg-auto": avg(avg_auto_points),
+ "avg-climb": avg(egcpts),
+ "avg-def": avg(defe),
+ "avg-driv": avg(driver),
+ "avg-speed": avg(speed),
+ "avg-stab": avg(stab),
+ "avg-inta": avg(inta),
+ "avg-upt": avg(uptime),
+ matches: matches,
+ "tpw-std":
+ std(avg_auto_points) + std(avg_tele_points) + std(egcpts),
+ "tpw-score": 0
+ };
+ data_tpw["tpw-score"] =
+ data_tpw["avg-auto"] + data_tpw["avg-tele"] + data_tpw["avg-climb"];
+ parsed_tpw_data[team] = data_tpw;
+ }
+ return parsed_tpw_data;
+}
+
+export function computeRankings(data: parsedRow[]): rankings {
+ const parsed_data = getData(data);
+ for (let team in parsed_data)
+ parsed_data[team]["r-score"] =
+ parsed_data[team]["tpw-score"] -
+ parsed_data[team]["tpw-std"] +
+ parsed_data[team]["avg-driv"] +
+ parsed_data[team]["avg-speed"] +
+ parsed_data[team]["avg-stab"] +
+ parsed_data[team]["avg-inta"];
+ const sorted = Object.entries(parsed_data).sort(
+ (a, b) => (b[1]["r-score"] || 0) - (a[1]["r-score"] || 0)
+ );
+ const pranks: rankings = {};
+ for (const [team, data] of sorted) {
+ pranks[team] = {
+ "off-score": data["r-score"] || 0,
+ "def-score": data["avg-def"]
+ };
+ }
+ return pranks;
+}
diff --git a/static/css/scouting.css b/static/css/scouting.css
index f7111c6..e252f44 100644
--- a/static/css/scouting.css
+++ b/static/css/scouting.css
@@ -2685,6 +2685,199 @@ div.component-score-counter div.tally {
user-select: none;
}
+/* COUNTER */
+
+.component-counter {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin: 0px 2% 0px 2%;
+ gap: 0;
+ padding: 0.8rem;
+ background: #ffffff;
+ border-radius: var(--standard-border-radius);
+ box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px;
+}
+
+.component-counter .counter-image {
+ display: none;
+}
+
+.component-counter .counter-image img {
+ width: 80px;
+ height: auto;
+ object-fit: contain;
+}
+
+.component-counter .counter-counters {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+}
+
+.component-counter .counter-total {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 8px;
+ padding: 0 0 6px 0;
+ margin: 0;
+}
+
+.component-counter .counter-total-label,
+.component-counter .counter-tally {
+ font-size: 1rem;
+ font-weight: 700;
+ color: var(--contentColor);
+ user-select: none;
+ margin: 0;
+}
+
+.component-counter .counter-row {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0;
+ padding: 8px 0;
+ width: 100%;
+}
+
+.component-counter .counter-row + .counter-row {
+ border-top: 1px solid rgba(0, 0, 0, 0.08);
+}
+
+.component-counter .counter-label {
+ font-size: 0.9rem;
+ font-weight: 600;
+ user-select: none;
+ color: var(--contentColor);
+ margin: 0;
+ padding-bottom: 8px;
+}
+
+.component-counter .counter-controls {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 6px;
+ width: 100%;
+ align-items: stretch;
+}
+
+.component-counter .counter-controls > div {
+ padding: 0;
+ display: grid;
+ place-items: center;
+ cursor: pointer;
+ touch-action: manipulation;
+ -webkit-tap-highlight-color: transparent;
+ border-radius: 5px;
+ transition: background-color 0.15s ease, color 0.15s ease;
+ user-select: none;
+}
+
+.component-counter .counter-controls > .counter-plus {
+ grid-row: 2;
+ grid-column: 1 / 4;
+ min-height: 56px;
+ background-color: var(--primaryBackgroundColor);
+ border: 2px solid var(--primaryBackgroundColor);
+}
+
+.component-counter .counter-controls > .counter-plus:active {
+ background-color: var(--primaryDarkerBackgroundColor);
+ border-color: var(--primaryDarkerBackgroundColor);
+}
+
+.component-counter .counter-controls > .counter-plus svg {
+ width: 26px;
+ height: 26px;
+ fill: var(--primaryContentColor);
+}
+
+.component-counter .counter-controls > .counter-bulk[data-amount="10"] {
+ grid-row: 2;
+ grid-column: 4;
+ min-height: 56px;
+ background-color: var(--primaryBackgroundColor);
+ border: 2px solid var(--primaryBackgroundColor);
+ font-size: 1.1rem;
+ font-weight: 700;
+ color: var(--primaryContentColor);
+}
+
+.component-counter .counter-controls > .counter-bulk[data-amount="10"]:active {
+ background-color: var(--primaryDarkerBackgroundColor);
+ border-color: var(--primaryDarkerBackgroundColor);
+}
+
+.component-counter .counter-controls > .counter-bulk[data-amount="5"],
+.component-counter .counter-controls > .counter-minus,
+.component-counter .counter-controls > .counter-bulk[data-amount="-5"],
+.component-counter .counter-controls > .counter-bulk[data-amount="-10"] {
+ grid-row: 3;
+ min-height: 42px;
+ background-color: transparent;
+ border: 2px solid var(--primaryBackgroundColor);
+ color: var(--primaryBackgroundColor);
+ font-weight: 700;
+ font-size: 0.9rem;
+}
+
+.component-counter .counter-controls > .counter-bulk[data-amount="5"]:active,
+.component-counter .counter-controls > .counter-minus:active,
+.component-counter .counter-controls > .counter-bulk[data-amount="-5"]:active,
+.component-counter .counter-controls > .counter-bulk[data-amount="-10"]:active {
+ background-color: var(--primaryBackgroundColor);
+ color: var(--primaryContentColor);
+}
+
+.component-counter .counter-controls > .counter-bulk[data-amount="5"] {
+ grid-column: 1;
+}
+.component-counter .counter-controls > .counter-minus {
+ grid-column: 2;
+}
+.component-counter .counter-controls > .counter-bulk[data-amount="-5"] {
+ grid-column: 3;
+}
+.component-counter .counter-controls > .counter-bulk[data-amount="-10"] {
+ grid-column: 4;
+}
+
+.component-counter .counter-controls > .counter-minus svg {
+ width: 16px;
+ height: 16px;
+ fill: var(--primaryBackgroundColor);
+}
+
+.component-counter .counter-controls > .counter-minus:active svg {
+ fill: var(--primaryContentColor);
+}
+
+.component-counter .counter-count {
+ grid-row: 1;
+ grid-column: 1 / -1;
+ font-size: 1.6rem;
+ font-weight: 800;
+ min-width: auto;
+ text-align: center;
+ padding: 0 0 2px 0;
+ user-select: none;
+ color: var(--primaryBackgroundColor);
+}
+
+.component-counter .counter-mobile-label {
+ display: block;
+ font-size: 1rem;
+ font-weight: 700;
+ color: var(--contentColor);
+ user-select: none;
+ margin: 0;
+ padding: 0 0 4px 0;
+ text-align: center;
+}
+
/* MAIN APP */
.component-layout-preset-manager {
@@ -2728,7 +2921,8 @@ div.preset {
div.preset > div.component-layout-rows div.component-score-counter,
div.preset > div.component-layout-rows div.component-textarea,
- div.preset > div.component-layout-rows div.component-locations {
+ div.preset > div.component-layout-rows div.component-locations,
+ div.preset > div.component-layout-rows div.component-counter {
grid-column: 1 / -1;
margin: 0px 1% 0px 1%;
}
diff --git a/static/js/scoutingsdk.js b/static/js/scoutingsdk.js
index b01202d..3e8cd84 100644
--- a/static/js/scoutingsdk.js
+++ b/static/js/scoutingsdk.js
@@ -2222,6 +2222,12 @@ ${_this.escape(teamNumber)} (Blue ${i + 1})
autoHeight: true
}));
+ columnDefs.forEach((def) => {
+ if (def.field === "match" || def.field === "team") {
+ def.pinned = "left";
+ }
+ });
+
columnDefs[0].width = 75;
columnDefs[0].sortable = false;
columnDefs[0].filter = false;
@@ -2322,6 +2328,12 @@ ${_this.escape(teamNumber)} (Blue ${i + 1})
autoHeight: true
}));
+ columnDefs.forEach((def) => {
+ if (def.field === "match" || def.field === "team") {
+ def.pinned = "left";
+ }
+ });
+
columnDefs[0].width = 75;
columnDefs[0].filter = false;
columnDefs[0].sortable = false;
@@ -3745,6 +3757,7 @@ ${_this.escape(teamNumber)} (Blue ${i + 1})
"locations",
"pagebutton",
"scorecount",
+ "counter",
"pagebar",
"checkbox",
"timer",
@@ -4309,6 +4322,7 @@ ${_this.escape(teamNumber)} (Blue ${i + 1})
else if (controlLabel === "Missed") values.push("amp");
locations.push(5);
}
+
dcounter++;
await saveData();
};
@@ -4520,6 +4534,225 @@ ${_this.escape(teamNumber)} (Blue ${i + 1})
id
)}">${htmlcont}`
);
+ } else if (component.type == "counter") {
+ let id = _this.random();
+
+ let src = "";
+ if (component.src != null) {
+ if (
+ typeof component.src === "object" &&
+ component.src.type === "function"
+ ) {
+ src = eval(component.src.definition)(getState());
+ } else {
+ src = component.src.toString();
+ }
+ }
+
+ let values = [];
+ let locations = [];
+ let dcounter = 0;
+ let location =
+ component.location != null ? component.location : 0;
+
+ if (component.data) {
+ values = checkNull(data.data[component.data.values], []);
+ locations = checkNull(
+ data.data[component.data.locations],
+ []
+ );
+ dcounter = checkNull(
+ data.counters[component.data.counter],
+ 0
+ );
+ }
+
+ const hub = async () => {
+ await _this.setData(
+ "data",
+ component.data.locations,
+ locations
+ );
+ await _this.setData("data", component.data.values, values);
+ await _this.setData(
+ "counters",
+ component.data.counter,
+ dcounter
+ );
+ };
+
+ let options =
+ component.options instanceof Array ? component.options : [];
+
+ let rows = options
+ .map((opt, optIndex) => {
+ let optId = `${id}-hubopt-${optIndex}`;
+ let initCount = values.filter(
+ (v) => v === opt.value
+ ).length;
+ return `
+
+
${_this.escape(
+ opt.label
+ )}
+
+
+
+10
+
+5
+
+
-5
+
-10
+
${initCount}
+
+
`;
+ })
+ .join("");
+
+ let totalInit = options.reduce(
+ (sum, opt) =>
+ sum + values.filter((v) => v === opt.value).length,
+ 0
+ );
+
+ pendingFunctions.push(async () => {
+ options.forEach((opt, optIndex) => {
+ let optId = `${id}-hubopt-${optIndex}`;
+ let row = document.getElementById(optId);
+ if (!row) return;
+ let ecount = row.querySelector(".counter-count");
+ let plus = row.querySelector(".counter-plus");
+ let minus = row.querySelector(".counter-minus");
+ let etally = element.querySelector(
+ `[data-id="${_this.escape(id)}"] .counter-tally`
+ );
+
+ let tally = () => {
+ if (etally) {
+ etally.innerText = options.reduce(
+ (sum, o) =>
+ sum +
+ values.filter((v) => v === o.value)
+ .length,
+ 0
+ );
+ }
+ };
+
+ plus.addEventListener("click", async (e) => {
+ e.stopPropagation();
+ let cur = parseInt(ecount.innerText) || 0;
+ if (opt.max !== undefined && cur >= opt.max) {
+ return;
+ }
+ values.push(opt.value);
+ locations.push(location);
+ dcounter++;
+ ecount.innerText = cur + 1;
+ tally();
+ await hub();
+ });
+
+ minus.addEventListener("click", async (e) => {
+ e.stopPropagation();
+ let cur = parseInt(ecount.innerText) || 0;
+ if (cur <= 0) {
+ return;
+ }
+ const idv = values.lastIndexOf(opt.value);
+ if (idv !== -1) {
+ values.splice(idv, 1);
+ }
+ const idl = locations.lastIndexOf(location);
+ if (idl !== -1) {
+ locations.splice(idl, 1);
+ }
+ dcounter = Math.max(0, dcounter - 1);
+ ecount.innerText = cur - 1;
+ tally();
+ await hub();
+ });
+
+ let bulk = row.querySelectorAll(".counter-bulk");
+ bulk.forEach((b) => {
+ b.addEventListener("click", async (e) => {
+ e.stopPropagation();
+ let amount = parseInt(
+ b.getAttribute("data-amount")
+ );
+ let cur = parseInt(ecount.innerText) || 0;
+
+ if (amount > 0) {
+ if (opt.max !== undefined) {
+ amount = Math.min(
+ amount,
+ opt.max - cur
+ );
+ }
+ if (amount <= 0) return;
+ for (let i = 0; i < amount; i++) {
+ values.push(opt.value);
+ locations.push(location);
+ dcounter++;
+ }
+ ecount.innerText = cur + amount;
+ } else {
+ let smaller = Math.min(
+ Math.abs(amount),
+ cur
+ );
+ if (smaller <= 0) return;
+ for (let i = 0; i < smaller; i++) {
+ const idv = values.lastIndexOf(
+ opt.value
+ );
+ if (idv !== -1) {
+ values.splice(idv, 1);
+ }
+
+ const idl =
+ locations.lastIndexOf(location);
+ if (idl !== -1) {
+ locations.splice(idl, 1);
+ }
+
+ dcounter = Math.max(0, dcounter - 1);
+ }
+ ecount.innerText = cur - smaller;
+ }
+ tally();
+ await hub();
+ });
+ });
+ });
+
+ await hub();
+ });
+
+ resolve(`
+
+ ${
+ src
+ ? `
`
+ : ""
+ }
+
+
Hub
+
+ Total
+ ${totalInit}
+
+ ${rows}
+
+
+ `);
} else if (component.type == "pagebutton") {
let id = _this.random();
let page = -1;
@@ -4605,6 +4838,21 @@ ${_this.escape(teamNumber)} (Blue ${i + 1})
refers
) {
presets[j].classList.remove("none");
+
+ presets[j]
+ .querySelectorAll(
+ "textarea[data-key]"
+ )
+ .forEach((textarea) => {
+ let key =
+ textarea.getAttribute(
+ "data-key"
+ );
+ if (data.data[key] != null) {
+ textarea.value =
+ data.data[key];
+ }
+ });
} else {
presets[j].classList.add("none");
}
@@ -5327,11 +5575,13 @@ ${_this.escape(teamNumber)} (Blue ${i + 1})
id
)}">
Comments
- Scoring ability? Stability? Fouls? Issues?
+ Scoring ability? Stability? Fouls? Issues? Shooting type? (turret, shoot on the move..) Beached?
Team Number? (if practice match)