]> git.sesse.net Git - pkanalytics/commitdiff
Add a rudimentary start of the viewer.
authorSteinar H. Gunderson <sgunderson@bigfoot.com>
Wed, 10 May 2023 15:46:50 +0000 (17:46 +0200)
committerSteinar H. Gunderson <sgunderson@bigfoot.com>
Wed, 10 May 2023 15:46:50 +0000 (17:46 +0200)
ultimate.js [new file with mode: 0644]
viewer.html [new file with mode: 0644]

diff --git a/ultimate.js b/ultimate.js
new file mode 100644 (file)
index 0000000..b65a2b3
--- /dev/null
@@ -0,0 +1,231 @@
+'use strict';
+
+// No frameworks, no compilers, no npm, just JavaScript. :-)
+
+let json;
+
+fetch('ultimate.json')
+   .then(response => response.json())
+   .then(response => { process_matches(response); });
+
+function attribute_player_time(player, to, from) {
+       if (player.on_field_since > from) {
+               // Player came in while play happened (without a stoppage!?).
+               player.playing_time_ms += to - player.on_field_since;
+       } else {
+               player.playing_time_ms += to - from;
+       }
+}
+
+function take_off_field(player, t, live_since) {
+       if (live_since === null) {
+               // Play isn't live, so nothing to do.
+       } else {
+               attribute_player_time(player, t, live_since);
+       }
+       player.on_field_since = null;
+}
+
+function add_cell(tr, element_type, text) {
+       let element = document.createElement(element_type);
+       element.textContent = text;
+       tr.appendChild(element);
+}
+
+function process_matches(json) {
+       let players = {};
+       for (const player of json['players']) {
+               players[player['player_id']] = {
+                       'name': player['name'],
+                       'number': player['number'],
+
+                       'goals': 0,
+                       'assists': 0,
+                       'hockey_assists': 0,
+                       'catches': 0,
+                       'touches': 0,
+                       'num_throws': 0,
+                       'throwaways': 0,
+                       'drops': 0,
+
+                       'defenses': 0,
+                       'interceptions': 0,
+                       'points_played': 0,
+                       'playing_time_ms': 0,
+
+                       'offensive_soft_plus': 0,
+                       'offensive_soft_minus': 0,
+                       'defensive_soft_plus': 0,
+                       'defensive_soft_minus': 0,
+
+                       'pulls': 0,
+                       'pull_times': [],
+                       'oob_pulls': 0,
+
+                       // Internal.
+                       'last_point_seen': null,
+                       'on_field_since': null,
+               };
+       }
+
+       for (const match of json['matches']) {
+               let handler = null;
+               let prev_handler = null;
+               let live_since = null;
+               let offense = null;
+               let puller = null;
+               let pull_started = null;
+               let point_num = 0;
+               for (const [q,p] of Object.entries(players)) {
+                       p.on_field_since = null;
+                       p.last_point_seen = null;
+               }
+               for (const e of match['events']) {
+                       let t = e['t'];
+                       let type = e['type'];
+                       let p = players[e['player']];
+
+                       // Point count management
+                       if (p !== undefined && type !== 'out' && p.last_point_seen !== point_num) {
+                               p.last_point_seen = point_num;
+                               ++p.points_played;
+                       }
+                       if (type === 'goal' || type === 'their_goal') {
+                               ++point_num;
+                       }
+
+                       // Sub management
+                       if (type === 'in' && p.on_field_since === null) {
+                               p.on_field_since = t;
+                       } else if (type === 'out') {
+                               take_off_field(p, t, live_since);
+                       }
+
+                       // Liveness management
+                       if (type === 'pull' || type === 'their_pull' || type === 'restart') {
+                               live_since = t;
+                       } else if (type === 'goal' || type === 'their_goal' || type === 'stoppage') {
+                               for (const [q,p] of Object.entries(players)) {
+                                       if (p.on_field_since !== null) {
+                                               attribute_player_time(p, t, live_since);
+                                       }
+                               }
+                               live_since = null;
+                       }
+
+                       // Pull management
+                       if (type === 'pull') {
+                               puller = e['player'];
+                               pull_started = t;
+                               ++p.pulls;
+                       } else if (type === 'in' || type === 'out' || type === 'stoppage' || type === 'restart' || type === 'unknown' || type === 'set_defense' || type === 'set_offense') {
+                               // No effect on pull.
+                       } else if (type === 'pull_landed' && puller !== null) {
+                               players[puller].pull_times.push(t - pull_started);
+                       } else if (type === 'pull_oob' && puller !== null) {
+                               ++players[puller].oob_pulls;
+                       } else {
+                               // Not pulling (if there was one, we never recorded its outcome, but still count it).
+                               puller = pull_started = null;
+                       }
+
+                       // Offense/defense management (TODO: use it for actual counting)
+                       if (type === 'set_defense' || type === 'goal' || type === 'throwaway' || type === 'drop') {
+                               offense = false;
+                       } else if (type === 'set_offense' || type === 'their_goal' || type === 'their_throwaway' || type === 'defense' || type === 'interception') {
+                               offense = true;
+                       }
+
+                       // Event management
+                       if (type === 'catch' || type === 'goal') {
+                               if (handler !== null) {
+                                       ++players[handler].num_throws;
+                                       ++p.catches;
+                               }
+
+                               ++p.touches;
+                               if (type === 'goal') {
+                                       if (prev_handler !== null) {
+                                               ++players[prev_handler].hockey_assists;
+                                       }
+                                       if (handler !== null) {
+                                               ++players[handler].assists;
+                                       }
+                                       ++p.goals;
+                                       handler = prev_handler = null;
+                               } else {
+                                       // Update hold history.
+                                       prev_handler = handler;
+                                       handler = e['player'];
+                               }
+                       } else if (type === 'throwaway') {
+                               ++p.throwaways;
+                               handler = prev_handler = null;
+                       } else if (type === 'drop') {
+                               ++p.drops;
+                               handler = prev_handler = null;
+                       } else if (type === 'defense') {
+                               ++p.defenses;
+                       } else if (type === 'interception') {
+                               ++p.interceptions;
+                               ++p.defenses;
+                               ++p.touches;
+                               prev_handler = null;
+                               handler = e['player'];
+                       } else if (type === 'offensive_soft_plus' || type === 'offensive_soft_minus' || type === 'defensive_soft_plus' || type === 'defensive_soft_minus') {
+                               ++p[type];
+                       } else if (type !== 'in' && type !== 'out' && type !== 'pull' &&
+                                  type !== 'their_pull' && type !== 'restart' && type !== 'goal' &&
+                                  type !== 'their_goal' && type !== 'stoppage' && type !== 'unknown' &&
+                                  type !== 'set_defense' && type !== 'goal' && type !== 'throwaway' &&
+                                  type !== 'drop' && type !== 'set_offense' && type !== 'their_goal' &&
+                                  type !== 'pull' && type !== 'pull_landed' && type !== 'pull_oob' &&
+                                  type !== 'their_throwaway' && type !== 'defense' && type !== 'interception') {
+                               console.log("Unknown event:", e);
+                       }
+               }
+       }
+
+       let rows = [];
+
+       {
+               let header = document.createElement('tr');
+               add_cell(header, 'th', 'Player');
+               add_cell(header, 'th', '+/-');
+               add_cell(header, 'th', 'Soft +/-');
+               add_cell(header, 'th', 'Points played');
+               add_cell(header, 'th', 'Time played');
+               rows.push(header);
+       }
+
+       for (const [q,p] of Object.entries(players)) {
+               let row = document.createElement('tr');
+               let pm = p.goals + p.assists + p.hockey_assists + p.defenses - p.throwaways - p.drops;
+               let soft_pm = p.offensive_soft_plus + p.defensive_soft_plus - p.offensive_soft_minus - p.defensive_soft_minus;
+               add_cell(row, 'td', p.name);  // TODO: number?
+               add_cell(row, 'td', pm > 0 ? ('+' + pm) : pm);
+               add_cell(row, 'td', soft_pm > 0 ? ('+' + soft_pm) : soft_pm);
+               add_cell(row, 'td', p.points_played);
+               add_cell(row, 'td', Math.floor(p.playing_time_ms / 60000) + ' min');
+               rows.push(row);
+               console.log(p.name + " played " + p.points_played + " points (" + Math.floor(p.playing_time_ms / 60000) + " min), " + p.goals + " goals, " + p.assists + " assists, plus/minus: " + pm);
+       }
+       document.getElementById('stats').replaceChildren(...rows);
+
+       console.log("PULL STATS");
+       for (const [q,p] of Object.entries(players)) {
+               if (p.pulls === 0) {
+                       continue;
+               }
+               let sum_time = 0;
+               for (const t of p.pull_times) {
+                       sum_time += t;
+               }
+               let avg_time = 1e-3 * sum_time / p.pulls;
+               let msg = p.name + ' did ' + p.pulls + ' pull(s), ' + p.oob_pulls + ' OOB';
+               if (p.oob_pulls < p.pulls) {
+                       msg += ', avg. hangtime ' + avg_time.toFixed(1) + ' sec for others';
+               }
+               console.log(msg, p.pull_times);
+       }
+}
diff --git a/viewer.html b/viewer.html
new file mode 100644 (file)
index 0000000..569f696
--- /dev/null
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<!-- TODO: make something nice to package up the ultimate.json and everything into one file -->
+<html>
+  <head>
+    <title>Plastkast Analytics</title>
+    <link rel="stylesheet" type="text/css" href="ultimate.css">
+    <script src="ultimate.js"></script>
+  </head>
+  <body>
+    <table id="stats">
+    </table>
+  </body>
+</html>
+