--- /dev/null
+'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);
+ }
+}