Skip to content

Commit 4d7094e

Browse files
committed
Add VURC and V5RC Middle School support
1 parent 39d5b20 commit 4d7094e

File tree

6 files changed

+238
-89
lines changed

6 files changed

+238
-89
lines changed

app/src/main/java/com/sunkensplashstudios/VRCRoboScout/EventTeamsView.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,15 +93,18 @@ fun EventTeamsView(eventId: Int, eventTeamsViewModel: EventTeamsViewModel = view
9393

9494
fun fetchTeamsList() {
9595
CoroutineScope(Dispatchers.Default).launch {
96-
val event = eventDataTransferManager.getEvent(eventId) ?: Event(eventId, false)
96+
if (eventTeamsViewModel.teams.isNotEmpty()) {
97+
return@launch
98+
}
99+
val event = eventDataTransferManager.getEvent(eventId) ?: return@launch
97100
if (division != null) {
98101
event.fetchTeams()
99102
event.fetchRankings(division)
100103
withContext(Dispatchers.Main) {
101104
eventTeamsViewModel.teams =
102105
Event.sortTeamsByNumber(event.rankings[division]!!.map {
103106
event.getTeam(it.team.id) ?: Team()
104-
}).toMutableList()
107+
}, if (event.program.id == 4) "College" else "Not College").toMutableList()
105108
loading = false
106109
}
107110
}

app/src/main/java/com/sunkensplashstudios/VRCRoboScout/LookupView.kt

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ class LookupViewModel : ViewModel() {
118118
withContext(Dispatchers.Main) {
119119
team.value = fetchedTeam
120120
wsEntry.value = API.worldSkillsFor(fetchedTeam)
121-
vdaEntry.value = API.vdaFor(fetchedTeam)
121+
vdaEntry.value = API.vdaFor(fetchedTeam, true)
122122
avgRanking.doubleValue = fetchedTeam.averageQualifiersRanking()
123123
awardCounts = fetchedAwardCounts
124124
fetchedTeams.value = fetchedTeam.id != 0
@@ -155,6 +155,8 @@ class LookupViewModel : ViewModel() {
155155

156156
scraperParams["page"] = page
157157

158+
scraperParams["seasonId"] = UserSettings(applicationContext!!).getSelectedSeasonId()
159+
158160
val formatter = SimpleDateFormat("dd-MMM-yyyy", Locale.ENGLISH)
159161

160162
scraperParams["from_date"] = if (name.isNullOrEmpty()) formatter.format(Date()) else "01-Jan-1970"
@@ -170,7 +172,7 @@ class LookupViewModel : ViewModel() {
170172
return@launch
171173
}
172174
val data = RoboScoutAPI.roboteventsRequest(
173-
requestUrl = "/seasons/${season ?: UserSettings(applicationContext!!)}/events",
175+
requestUrl = "/seasons/${season ?: UserSettings(applicationContext!!).getSelectedSeasonId()}/events",
174176
params = mapOf("sku" to skuArray)
175177
)
176178
withContext(Dispatchers.Main) {
@@ -192,6 +194,8 @@ class LookupViewModel : ViewModel() {
192194
@Composable
193195
fun LookupView(lookupViewModel: LookupViewModel = viewModels["lookup_view"] as LookupViewModel, navController: NavController) {
194196

197+
val userSettings = UserSettings(LocalContext.current)
198+
195199
Column(
196200
modifier = Modifier.fillMaxSize()
197201
) {
@@ -223,7 +227,8 @@ fun LookupView(lookupViewModel: LookupViewModel = viewModels["lookup_view"] as L
223227
lookupViewModel.page.intValue -= 1
224228
lookupViewModel.fetchEvents(
225229
name = lookupViewModel.eventName.value,
226-
page = lookupViewModel.page.intValue
230+
page = lookupViewModel.page.intValue,
231+
grade = if (userSettings.getGradeLevel() == "Middle School") 2 else if (userSettings.getGradeLevel() == "High School") 3 else null
227232
)
228233
}) {
229234
Icon(
@@ -245,7 +250,8 @@ fun LookupView(lookupViewModel: LookupViewModel = viewModels["lookup_view"] as L
245250
lookupViewModel.page.intValue += 1
246251
lookupViewModel.fetchEvents(
247252
name = lookupViewModel.eventName.value,
248-
page = lookupViewModel.page.intValue
253+
page = lookupViewModel.page.intValue,
254+
grade = if (userSettings.getGradeLevel() == "Middle School") 2 else if (userSettings.getGradeLevel() == "High School") 3 else null
249255
)
250256
}) {
251257
Icon(
@@ -594,7 +600,7 @@ fun TeamLookup(lookupViewModel: LookupViewModel, navController: NavController) {
594600
onClick = { }
595601
)
596602
DropdownMenuItem(
597-
text = { Text("Winrate: ${lookupViewModel.vdaEntry.value.totalWinningPercent}%") },
603+
text = { Text("Winrate: ${lookupViewModel.vdaEntry.value.totalWinningPercent.round(1)}%") },
598604
onClick = { }
599605
)
600606
DropdownMenuItem(
@@ -703,10 +709,20 @@ fun TeamLookup(lookupViewModel: LookupViewModel, navController: NavController) {
703709

704710
@Composable
705711
fun EventLookup(lookupViewModel: LookupViewModel, navController: NavController) {
712+
val userSettings = UserSettings(LocalContext.current)
706713

707714
val keyboardController = LocalSoftwareKeyboardController.current
708715
val isFocused = remember { mutableStateOf(false) }
709716

717+
LaunchedEffect(Unit) {
718+
if (lookupViewModel.events.value.isEmpty()) {
719+
lookupViewModel.fetchEvents(
720+
grade = if (userSettings.getGradeLevel() == "Middle School") 2 else if (userSettings.getGradeLevel() == "High School") 3 else null
721+
)
722+
lookupViewModel.page.intValue = 1
723+
}
724+
}
725+
710726
Column(
711727
horizontalAlignment = Alignment.CenterHorizontally,
712728
modifier = Modifier.fillMaxSize()
@@ -756,7 +772,7 @@ fun EventLookup(lookupViewModel: LookupViewModel, navController: NavController)
756772
keyboardActions = KeyboardActions(
757773
onDone = {
758774
keyboardController?.hide()
759-
lookupViewModel.fetchEvents(name = lookupViewModel.eventName.value, page = 1)
775+
lookupViewModel.fetchEvents(name = lookupViewModel.eventName.value, page = 1, grade = if (userSettings.getGradeLevel() == "Middle School") 2 else if (userSettings.getGradeLevel() == "High School") 3 else null)
760776
lookupViewModel.page.intValue = 1
761777
}),
762778
placeholder = {

app/src/main/java/com/sunkensplashstudios/VRCRoboScout/RoboScoutAPI.kt

Lines changed: 106 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -282,9 +282,10 @@ class RoboScoutAPI {
282282
}
283283

284284
suspend fun roboteventsCompetitionScraper(params: MutableMap<String, Any> = mutableMapOf()): List<String> {
285-
var requestUrl = "https://www.robotevents.com/robot-competitions/vex-robotics-competition"
286285
var params = params
287286

287+
println(params)
288+
288289
if (!params.containsKey("page")) {
289290
params["page"] = 1
290291
}
@@ -299,10 +300,9 @@ class RoboScoutAPI {
299300
6 -> params["level_class_id"] = 13
300301
}
301302

302-
when (params["grade_level_id"] as? Int) {
303-
1 -> params["grade_level_id"] = 2
304-
2 -> params["grade_level_id"] = 3
305-
}
303+
val competition = if (API.seasonIdMap[0].find { it.id == params["seasonId"]} != null) "vex-robotics-competition" else "college-competition"
304+
305+
val requestUrl = "https://www.robotevents.com/robot-competitions/$competition"
306306

307307
val skuArray = mutableListOf<String>()
308308

@@ -327,12 +327,12 @@ class RoboScoutAPI {
327327
println("RobotEvents Scraper (page ${params["page"] as? Int ?: 0}): ${response.call.request.url}")
328328
//println(response.bodyAsText())
329329

330-
val pattern = "https://www\\.robotevents\\.com/robot-competitions/vex-robotics-competition/RE-VRC([+-]?(?=\\.\\d|\\d)(?:\\d+)?(?:\\.?\\d*))(?:[Ee]([+-]?\\d+))?([+-]?(?=\\.\\d|\\d)(?:\\d+)?(?:\\.?\\d*))(?:[Ee]([+-]?\\d+))?\\.html"
330+
val pattern = "$requestUrl/RE-[A-Z0-9]*([+-]?(?=\\.\\d|\\d)(?:\\d+)?\\.?\\d*)(?:[Ee]([+-]?\\d+))?([+-]?(?=\\.\\d|\\d)(?:\\d+)?\\.?\\d*)(?:[Ee]([+-]?\\d+))?\\.html"
331331
val regex = Regex(pattern, RegexOption.IGNORE_CASE)
332332
val matches = regex.findAll(response.bodyAsText())
333333

334334
for (match in matches) {
335-
skuArray.add(match.value.replace("https://www.robotevents.com/robot-competitions/vex-robotics-competition/", "").replace(".html", ""))
335+
skuArray.add(match.value.replace("$requestUrl/", "").replace(".html", ""))
336336
}
337337
println("Matches: $skuArray")
338338

@@ -405,7 +405,7 @@ class RoboScoutAPI {
405405
try {
406406
val response = client.get("https://www.robotevents.com/api/seasons/${season ?: API.selectedSeasonId()}/skills") {
407407
url {
408-
parameters.append("grade_level", "High School")
408+
parameters.append("grade_level", gradeLevel)
409409
}
410410
}
411411

@@ -440,7 +440,6 @@ class RoboScoutAPI {
440440
val json = Json.parseToJsonElement(response.bodyAsText())
441441

442442
json.jsonArray.forEach { element ->
443-
//println("Element: $element")
444443
val vdaEntry: VDAEntry = jsonWorker.decodeFromJsonElement(element)
445444
this.vdaCache.add(vdaEntry)
446445
}
@@ -463,14 +462,56 @@ class RoboScoutAPI {
463462
}
464463
}
465464

466-
fun vdaFor(team: Team): VDAEntry {
467-
return try {
465+
suspend fun vdaFor(team: Team, fetchRobotEventsMatchStatistics: Boolean = false): VDAEntry {
466+
val vda = try {
468467
this.vdaCache.first {
469468
it.teamNumber == team.number
470469
}
471470
} catch (e: NoSuchElementException) {
472471
VDAEntry()
473472
}
473+
if (fetchRobotEventsMatchStatistics) {
474+
var totalWins = 0
475+
var totalLosses = 0
476+
var totalTies = 0
477+
var totalAP = 0
478+
var totalWP = 0
479+
480+
val seasonIndex = API.seasonIdMap[if (API.selectedProgramId() == 4) 1 else 0].indexOfFirst { it.id == API.selectedSeasonId() }
481+
val season = API.seasonIdMap[if (team.grade == "College") 1 else 0][seasonIndex]
482+
483+
val reRankingsData = roboteventsRequest("/teams/${team.id}/rankings", mapOf("season" to season.id))
484+
val reRankings = reRankingsData.map { jsonWorker.decodeFromJsonElement<TeamRanking>(it) }
485+
for (eventRankings in reRankings) {
486+
println("${eventRankings.event.name}: ${eventRankings.wins} wins, ${eventRankings.losses} losses, ${eventRankings.ties} ties, ${eventRankings.ap} AP, ${eventRankings.wp} WP")
487+
totalWins += eventRankings.wins
488+
totalLosses += eventRankings.losses
489+
totalTies += eventRankings.ties
490+
totalAP += eventRankings.ap
491+
totalWP += eventRankings.wp
492+
}
493+
494+
val matches = team.matchesForSeason(season.id)
495+
for (match in matches.filterNot { listOf(Round.PRACTICE, Round.QUALIFICATION).contains(it.roundType) }) {
496+
if (match.winningAlliance() == match.allianceFor(team)) {
497+
totalWins += 1
498+
} else if (match.winningAlliance() != null) {
499+
totalLosses += 1
500+
} else {
501+
totalTies += 1
502+
}
503+
}
504+
505+
vda.totalWins = totalWins.toDouble()
506+
vda.totalLosses = totalLosses.toDouble()
507+
vda.totalTies = totalTies.toDouble()
508+
vda.totalMatches = (totalWins + totalLosses + totalTies).toDouble()
509+
vda.totalWinningPercent = (totalWins / vda.totalMatches) * 100
510+
vda.apPerMatch = totalAP / vda.totalMatches
511+
vda.wpPerMatch = totalWP / vda.totalMatches
512+
vda.awpPerMatch = (totalWP - 2 * totalWins - totalTies) / vda.totalMatches
513+
}
514+
return vda
474515
}
475516

476517
}
@@ -479,6 +520,7 @@ class RoboScoutAPI {
479520
class Program {
480521
var id: Int = 0
481522
var name: String = ""
523+
var code: String = ""
482524
}
483525

484526
@Serializable
@@ -681,6 +723,7 @@ class Event {
681723
var end: String = ""
682724
@kotlinx.serialization.Transient var endDate: Date? = null
683725
var season: ShortSeason = ShortSeason()
726+
var program: Program = Program()
684727
var location: Location = Location()
685728
@kotlinx.serialization.Transient var matches: MutableMap<Division, MutableList<Match>> = mutableMapOf<Division, MutableList<Match>>()
686729
var teams: MutableList<Team> = mutableListOf<Team>()
@@ -694,8 +737,11 @@ class Event {
694737
@kotlinx.serialization.Transient var livestreamLink: String? = null
695738

696739
init {
697-
this.startDate = RoboScoutAPI.roboteventsDate(this.start, true)
698-
this.endDate = RoboScoutAPI.roboteventsDate(this.end, true)
740+
try {
741+
this.startDate = RoboScoutAPI.roboteventsDate(this.start, true)
742+
this.endDate = RoboScoutAPI.roboteventsDate(this.end, true)
743+
}
744+
catch (_: java.text.ParseException) { }
699745
}
700746

701747
constructor(id: Int = 0, fetch: Boolean = true) {
@@ -937,16 +983,30 @@ class Event {
937983
}
938984

939985
companion object {
940-
fun sortTeamsByNumber(teams: List<Team>): List<Team> {
941-
// Teams can be:
942-
// 229V, 4082B, 10C, 2775V, 9364C, 9364A
943-
// These teams are first sorted by the letter part of their team.number, then by the number part
944-
// The sorted list for the above teams:
945-
// 10C, 229V, 2775V, 4082B, 9364A, 9364C
946-
// Sort by letter part (remove all non-letter characters and sort)
947-
val sortedTeams = teams.sortedBy { it.number.replace(Regex("[^A-Za-z]"), "") }
948-
// Sort by number part (remove all non-number characters and sort)
949-
return sortedTeams.sortedBy { it.number.replace(Regex("[^0-9]"), "").toInt() }
986+
fun sortTeamsByNumber(teams: List<Team>, gradeLevel: String): List<Team> {
987+
if (gradeLevel != "College") {
988+
// Teams can be:
989+
// 229V, 4082B, 10C, 2775V, 9364C, 9364A
990+
// These teams are first sorted by the letter part of their team.number, then by the number part
991+
// The sorted list for the above teams:
992+
// 10C, 229V, 2775V, 4082B, 9364A, 9364C
993+
// Sort by letter part (remove all non-letter characters and sort)
994+
val sortedTeams = teams.sortedBy { it.number.replace(Regex("[^A-Za-z]"), "") }
995+
// Sort by number part (remove all non-number characters and sort)
996+
return sortedTeams.sortedBy { it.number.replace(Regex("[^0-9]"), "").toIntOrNull() }
997+
}
998+
else {
999+
// Teams can be:
1000+
// UCF, GATR1, GATR2, BLRS2, PYRO
1001+
// These teams are first sorted by the number part of their team.number, then by the letter part
1002+
// The sorted list for the above teams:
1003+
// BLRS2, GATR1, GATR2, PYRO, UCF
1004+
// Sort by number part (remove all non-number characters and sort)
1005+
// If there is no number part, the team should go above all teams with the same letter part that have a number part
1006+
val sortedTeams = teams.sortedBy { it.number.replace(Regex("[^0-9]"), "").toIntOrNull() }
1007+
// Sort by letter part (remove all non-letter characters and sort)
1008+
return sortedTeams.sortedBy { it.number.replace(Regex("[^A-Za-z]"), "") }
1009+
}
9501010
}
9511011
}
9521012

@@ -1007,12 +1067,12 @@ class Team : MutableState<Team> {
10071067

10081068
if (this.id != 0) {
10091069
runBlocking {
1010-
res = RoboScoutAPI.roboteventsRequest("/teams/$id", mapOf("program" to 1))
1070+
res = RoboScoutAPI.roboteventsRequest("/teams/$id")
10111071
}
10121072
}
10131073
else if (this.number.isNotEmpty()) {
10141074
runBlocking {
1015-
res = RoboScoutAPI.roboteventsRequest("/teams", mapOf("number" to number, "program" to 1))
1075+
res = RoboScoutAPI.roboteventsRequest("/teams", mapOf("number" to number, "grade" to listOf("Middle School", "High School", "College")))
10161076
}
10171077
}
10181078
else {
@@ -1047,8 +1107,27 @@ class Team : MutableState<Team> {
10471107
return matches
10481108
}
10491109

1050-
suspend fun fetchEvents(season: Int? = null) {
1051-
val data = RoboScoutAPI.roboteventsRequest("/events", mapOf("team" to id, "season" to (season ?: API.selectedSeasonId())))
1110+
suspend fun matchesForSeason(season: Int): List<Match> {
1111+
val data = RoboScoutAPI.roboteventsRequest("/teams/${this.id}/matches", mapOf("season" to season))
1112+
val matches = mutableListOf<Match>()
1113+
for (match in data) {
1114+
val fetchedMatch: Match = jsonWorker.decodeFromJsonElement(match)
1115+
matches.add(fetchedMatch)
1116+
}
1117+
matches.sortBy { it.instance }
1118+
matches.sortBy { it.roundType }
1119+
return matches
1120+
}
1121+
1122+
suspend fun fetchEvents(season: Int? = null) {
1123+
val data: List<JsonObject>
1124+
if (season == null) {
1125+
val seasonIndex = API.seasonIdMap[if (API.selectedProgramId() == 4) 1 else 0].indexOfFirst { it.id == API.selectedSeasonId() }
1126+
data = RoboScoutAPI.roboteventsRequest("/events", mapOf("team" to id, "season" to (API.seasonIdMap[if (this.grade == "College") 1 else 0][seasonIndex].id)))
1127+
}
1128+
else {
1129+
data = RoboScoutAPI.roboteventsRequest("/events", mapOf("team" to id, "season" to season))
1130+
}
10521131
events.clear()
10531132
for (event in data) {
10541133
val fetchedEvent: Event = jsonWorker.decodeFromJsonElement(event)

0 commit comments

Comments
 (0)