Skip to content

Commit 1cd730e

Browse files
authored
Add files via upload
1 parent fc5bd62 commit 1cd730e

1 file changed

Lines changed: 169 additions & 0 deletions

File tree

tools/generate_test_data.py

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
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

Comments
 (0)