1+ import json
2+ import re
3+ import requests
4+ import sys
5+ import random
6+ import csv
7+ from pprint import pprint
8+ import argparse
9+
10+ # Set to True for verbose console output during execution
11+ DEFAULT_EVENT = "2025mndu"
12+ OUTPUT_FILENAME = "test_scouting_data.tsv"
13+ # Use '\t' for TSV (Excel friendly) or ',' for CSV
14+ SEPARATOR = '\t '
15+
16+ def get_config_file (filename ):
17+ """Loads JSON data from within backticks in a JS file using a context manager."""
18+ try :
19+ with open (filename , 'r' ) as f :
20+ txt = f .read ().replace ('\n ' , '' )
21+ # Regex to capture content between backticks
22+ match = re .search (r"`(.*?)`" , txt )
23+ if match :
24+ config = json .loads (match .group (1 ))
25+ return config
26+ else :
27+ print ("Error: Could not find config string between backticks." )
28+ sys .exit (- 1 )
29+ except (FileNotFoundError , json .JSONDecodeError ) as e :
30+ print (f"File Error: { e } " )
31+ sys .exit (- 1 )
32+
33+ def get_field_list (config ):
34+ """Flattens the config sections into a single list of fields."""
35+ nodes = ["prematch" , "auton" , "teleop" , "endgame" , "postmatch" ]
36+ return [field for n in nodes for field in config .get (n , [])]
37+
38+ def get_event_schedule (event ):
39+ """Fetches match schedule from TBA API using f-strings for URL construction."""
40+ key = "<YOUR TBA API KEY HERE>"
41+ url = f"https://www.thebluealliance.com/api/v3/event/{ event } /matches/simple"
42+ try :
43+ resp = requests .get (url , headers = {"X-TBA-Auth-Key" : key })
44+ resp .raise_for_status ()
45+ matches = resp .json ()
46+ except Exception as e :
47+ print (f"API Error: { e } " )
48+ sys .exit (- 1 )
49+
50+ sched = {}
51+ for m in matches :
52+ if m ['comp_level' ] == 'qm' :
53+ blue = [num [3 :] for num in m ['alliances' ]['blue' ]['team_keys' ]]
54+ red = [num [3 :] for num in m ['alliances' ]['red' ]['team_keys' ]]
55+ sched [m ['match_number' ]] = [blue , red ]
56+ return sched
57+
58+ # --- Patterns and Random Logic ---
59+ PATTERNS = [
60+ [['Low' , 'Low' , 'Med' ], ['Med' , 'Low' , 'Low' ]],
61+ [['Low' , 'Low' , 'Low' ], ['Low' , 'Low' , 'Low' ]],
62+ [['Med' , 'Med' , 'Med' ], ['Med' , 'Med' , 'Med' ]],
63+ [['High' , 'High' , 'High' ], ['High' , 'High' , 'High' ]],
64+ [['Low' , 'Med' , 'High' ], ['High' , 'Med' , 'Low' ]],
65+ [['Med' , 'High' , 'Low' ], ['Med' , 'Low' , 'High' ]],
66+ [['High' , 'Low' , 'Med' ], ['Low' , 'Med' , 'High' ]],
67+ [['Med' , 'Low' , 'Med' ], ['Low' , 'Med' , 'Low' ]],
68+ [['Med' , 'High' , 'Med' ], ['High' , 'Med' , 'High' ]]
69+ ]
70+
71+ def assign_level (match_num , robot_idx , color ):
72+ """Assign a skill level to a team number"""
73+ pattern_idx = (match_num - 1 ) % len (PATTERNS )
74+ alliance_idx = 0 if color == 'blue' else 1
75+ return PATTERNS [pattern_idx ][alliance_idx ][robot_idx ]
76+
77+ def get_rand_weight (level ):
78+ """Provides specific performance floors/ceilings based on assigned tier."""
79+ # # Low between 0 and 30% of Max
80+ # # Med between 0% and 60% of Max
81+ # # High between 0% and 100% of Max
82+ if level == 'Low' :
83+ return random .uniform (0.0 , 0.6 ) if random .random () > 0.3 else 0.0
84+ if level == 'Med' :
85+ return random .uniform (0.0 , 0.6 )
86+ return random .uniform (0.0 , 1.0 ) # High robot
87+
88+ def generate_ci_value (level , field ):
89+ """Generates coordinates for clickable images."""
90+ valid = list (map (int , field .get ('allowableResponses' , '' ).split ()))
91+ if not valid :
92+ x , y = map (int , field .get ('dimensions' , '7 10' ).split ())
93+ valid = list (range (x * y ))
94+
95+ count = 1 if field .get ('clickRestriction' ) == 'one' else round (field .get ('expectedMax' , 25 ) * get_rand_weight (level ))
96+ return str (random .choices (valid , k = count ))
97+
98+ def gen_data (level , fields , context ):
99+ dispatch = {
100+ 'scouter' : lambda f : 'gen' ,
101+ 'event' : lambda f : context ['event' ],
102+ 'level' : lambda f : 'qm' ,
103+ 'match' : lambda f : str (context ['match_num' ]),
104+ 'robot' : lambda f : f"{ context ['color' ]} { context ['robot_num' ]+ 1 } " ,
105+ 'team' : lambda f : str (context ['team' ]),
106+ 'clickable_image' : lambda f : generate_ci_value (level , f ),
107+ 'counter' : lambda f : str (round (f .get ('expectedMax' , 12 ) * get_rand_weight (level ))),
108+ 'number' : lambda f : str (f .get ('min' , 0 ) + round ((f .get ('max' , 100 ) - f .get ('min' , 0 )) * get_rand_weight (level ))),
109+ 'bool' : lambda f : str (round (1 * get_rand_weight (level ))),
110+ 'radio' : lambda f : (list (f ['choices' ].keys ())[- 1 ] if get_rand_weight (level ) <= 0.3 else random .choice (list (f ['choices' ].keys ()))),
111+ 'timer' : lambda f : str (round ((1 - get_rand_weight (level )) * 30 , 2 )),
112+ 'text' : lambda f : f"Generated for { level } tier"
113+ }
114+ return [dispatch .get (field ['type' ], lambda f : "NA" )(field ) for field in fields ]
115+
116+ def parse_args ():
117+ """Parse command line arguments with defaults."""
118+ parser = argparse .ArgumentParser (description = 'Generate test scouting data' )
119+ parser .add_argument ('-c' , '--config' , default = './rebuilt_config.js' ,
120+ help = 'Config filename (default: ./rebuilt_config.js)' )
121+ parser .add_argument ('-o' , '--output' , default = OUTPUT_FILENAME ,
122+ help = f'Output filename (default: { OUTPUT_FILENAME } )' )
123+ parser .add_argument ('-d' , '--delimiter' , default = 'tab' , choices = ['tab' , 'comma' ],
124+ help = 'Delimiter type: tab or comma (default: tab)' )
125+ return parser .parse_args ()
126+
127+ def main ():
128+ args = parse_args ()
129+ delimiter = '\t ' if args .delimiter == 'tab' else ','
130+
131+ config = get_config_file (args .config )
132+ field_list = get_field_list (config )
133+ schedule = get_event_schedule (DEFAULT_EVENT )
134+
135+ team_levels = {}
136+ data_rows = []
137+
138+ # Process match schedule
139+ for m_num in sorted (schedule ):
140+ for alliance_idx , color_key in enumerate (['blue' , 'red' ]):
141+ for r_idx in range (3 ):
142+ team = schedule [m_num ][alliance_idx ][r_idx ]
143+
144+ if team not in team_levels :
145+ team_levels [team ] = assign_level (m_num , r_idx , color_key )
146+
147+ context = {
148+ 'event' : DEFAULT_EVENT ,
149+ 'match_num' : m_num ,
150+ 'team' : team ,
151+ 'robot_num' : r_idx ,
152+ 'color' : 'b' if color_key == 'blue' else 'r'
153+ }
154+ data_rows .append (gen_data (team_levels [team ], field_list , context ))
155+
156+ # Export to File using CSV module for robust formatting
157+ header = [f ['name' ] for f in field_list ]
158+ try :
159+ with open (OUTPUT_FILENAME , 'w' , newline = '' ) as f :
160+ writer = csv .writer (f , delimiter = SEPARATOR )
161+ writer .writerow (header )
162+ writer .writerows (data_rows )
163+ print (f"Successfully exported { len (data_rows )} rows to { OUTPUT_FILENAME } " )
164+ except IOError as e :
165+ print (f"File Write Error: { e } " )
166+ sys .exit (- 1 )
167+
168+ if __name__ == "__main__" :
169+ main ()
0 commit comments