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
` + : "" + } +
+
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)