]> git.sesse.net Git - ultimatescore/commitdiff
Pick out games directly from the results sheet.
authorSteinar H. Gunderson <sgunderson@bigfoot.com>
Wed, 29 Sep 2021 17:43:58 +0000 (19:43 +0200)
committerSteinar H. Gunderson <sgunderson@bigfoot.com>
Wed, 29 Sep 2021 17:43:58 +0000 (19:43 +0200)
This is a huge change of all kinds of parsing. It parses the list of
games from the results sheet instead of requiring separate group A/B/C
sheets that can go badly out of sync.

carousel.js
config.js
score.html
update_sheets.js

index 0ea2e40478a516d1017c249c271b004e5eb8a95a..04e9e731a0e47f53cdaa3aebf6618c25f0d7d861 100644 (file)
@@ -363,7 +363,7 @@ function rank_thirds(games, teams, start_rank, tiebreakers) {
 
 function parse_teams_from_spreadsheet(response) {
        let teams = [];
-       for (let i = 2; response.values[i].length >= 1; ++i) {
+       for (let i = 1; i < response.values.length && response.values[i].length >= 1; ++i) {
                teams.push({
                        "name": response.values[i][0],
                        "mediumname": response.values[i][1],
@@ -409,7 +409,54 @@ function parse_games_from_spreadsheet(response, group_name, include_unplayed) {
        return games;
 };
 
-function apply_games_to_teams(games, teams, ignored_teams, ret_ignored_games)
+function get_team_code(teams, str) {
+       for (const team of teams) {
+               if (team.name === str || team.mediumname === str || team.shortname === str) {
+                       return team.shortname;
+               }
+       }
+       return str;
+}
+
+function get_all_group_games(teams, groups, cb) {
+       get_sheet('Results', function(response) {
+               let games = [];
+               for (const region of ultimateconfig['group_match_scores']) {
+                       for (let row = region.first_row; row <= region.last_row; ++row) {
+                               let team1 = get_team_code(teams, response.values[row - 1][region.team1_column]);
+                               let team2 = get_team_code(teams, response.values[row - 1][region.team2_column]);
+                               if (team1 === undefined || team2 === undefined || team1 === '' || team2 === '' || team1 === null || team2 === null) {
+                                       continue;
+                               }
+                               let group_name = region.group_name;
+                               if (group_name === undefined) {
+                                       // Infer group from whatever group both teams are in.
+                                       for (const [group, teams] of Object.entries(groups)) {
+                                               if (teams.indexOf(team1) != -1 && teams.indexOf(team2) != -1) {
+                                                       group_name = group;
+                                                       break;
+                                               }
+                                       }
+                               }
+                               let game = {
+                                       "name1": team1,
+                                       "name2": team2,
+                                       "score1": parseInt(response.values[row - 1][region.team1_score_column]),
+                                       "score2": parseInt(response.values[row - 1][region.team2_score_column]),
+                                       "group_name": group_name
+                               };
+                               if (region.stream_time_column !== undefined) {
+                                       game["streamtime"] = response.values[row - 1][region.stream_time_column].replace('.', ':');
+                                       game["streamday"] = region.stream_day;
+                               }
+                               games.push(game);
+                       }
+               }
+               cb(games);
+       });
+};
+
+function apply_games_to_teams(games, teams, group_name, ignored_teams, ret_ignored_games)
 {
        let teams_to_idx = make_teams_to_idx(teams);
        let ignored_teams_idx;
@@ -425,6 +472,9 @@ function apply_games_to_teams(games, teams, ignored_teams, ret_ignored_games)
                teams[i].pts = 0;
        }
        for (let i = 0; i < games.length; ++i) {
+               if (games[i].group_name !== group_name) {
+                       continue;
+               }
                let idx1 = teams_to_idx[games[i].name1];
                let idx2 = teams_to_idx[games[i].name2];
                if (games[i].score1 === undefined || games[i].score2 === undefined ||
@@ -484,11 +534,19 @@ function filter_teams(teams, response)
        return teams.filter(function(team) { return team.ngames > 0; });
 }
 
+// So that we can just have one team list, and let membership be defined by the group list.
+function filter_teams_by_group(teams, groups, group)
+{
+       return teams.filter(function(team) {
+               return groups[group].indexOf(team.shortname) != -1;
+       });
+}
+
 function display_group_parsed(teams, games, group_name)
 {
        document.getElementById('entire-bug').style.display = 'none';
 
-       apply_games_to_teams(games, teams);
+       apply_games_to_teams(games, teams, group_name);
        let tiebreakers = [];
        teams = rank(games, teams, 1, tiebreakers);
 
@@ -575,12 +633,6 @@ function clear_carousel(table)
 // Stream schedule
 let max_list_len = 7;
 
-function display_stream_schedule(response, group_name) {
-       let teams = parse_teams_from_spreadsheet(response);
-       let games = parse_games_from_spreadsheet(response, group_name, true);
-       display_stream_schedule_parsed(teams, games, 0);
-};
-
 function sort_game_list(games) {
        games = games.filter(function(game) { return game.streamtime !== undefined && game.streamtime.match(/[0-9]+:[0-9]+/) != null; });
        games.sort(function(a, b) {
@@ -679,27 +731,58 @@ function display_stream_schedule_parsed(teams, games, page) {
        carousel.style.display = 'table';
 };
 
-function get_group(group_name, cb)
+function get_sheet(sheet_name, cb)
 {
        let req = new XMLHttpRequest();
        req.onload = function(e) {
-               cb(JSON.parse(req.responseText), group_name);
+               cb(JSON.parse(req.responseText));
        };
-       req.open('GET', 'https://sheets.googleapis.com/v4/spreadsheets/' + ultimateconfig['score_sheet_id'] + '/values/\'' + group_name + '\'!A1:J50?key=' + ultimateconfig['api_key']);
+       req.open('GET', 'https://sheets.googleapis.com/v4/spreadsheets/' + ultimateconfig['score_sheet_id'] + '/values/\'' + sheet_name + '\'!A1:Z50?key=' + ultimateconfig['api_key']);
        req.send();
 }
 
-function showgroup(group_name)
+function get_group(group_name, cb)
+{
+       get_sheet(group_name, function(response) {
+               cb(response, group_name);
+       });
+}
+
+function get_teams(cb)
+{
+       get_sheet('Teams', function(response) {
+               cb(parse_teams_from_spreadsheet(response));
+       });
+}
+
+function get_groups(cb)
 {
-       get_group(group_name, function(response, group_name) {
-               let teams = parse_teams_from_spreadsheet(response);
-               let games = parse_games_from_spreadsheet(response, group_name, false);
-               teams = filter_teams(teams, response);
-               display_group_parsed(teams, games, group_name);
-               publish_group_rank(response, group_name);  // Update the spreadsheet in the background.
+       get_sheet('Groups', function(response) {
+               let groups = {};
+               for (let i = 1; i < response.values.length && response.values[i].length >= 1; ++i) {
+                       let team = response.values[i][0];
+                       let group = response.values[i][1];
+                       if (groups[group] === undefined) {
+                               groups[group] = [];
+                       }
+                       groups[group].push(team);
+               }
+               cb(groups);
        });
 }
 
+function showgroup(group_name)
+{
+       get_teams(function(teams) {
+               get_groups(function(groups) {
+                       get_all_group_games(teams, groups, function(games) {
+                               teams = filter_teams_by_group(teams, groups, group_name);
+                               display_group_parsed(teams, games, group_name);
+                               publish_group_rank(response, group_name);  // Update the spreadsheet in the background.
+                       });
+               });
+       });
+}
 
 function showgroup_from_state()
 {
@@ -715,28 +798,17 @@ function hidetable()
 
 function showschedule(page)
 {
-       let teams = [];
-       let games = [];
-       let groups_to_get = [
-               'Group A',
-               'Group B',
-               'Group C',
-               'Playoffs 9th-13th',
-               'Playoffs'
-       ];
-       let num_left = groups_to_get.length;
-
-       let cb = function(response, group_name) {
-               teams = teams.concat(parse_teams_from_spreadsheet(response));
-               games = games.concat(parse_games_from_spreadsheet(response, group_name, true));
-               if (--num_left == 0) {
-                       display_stream_schedule_parsed(teams, games, 0);
-               }
-       };
-
-       for (const group of groups_to_get) {
-               get_group(group, cb);
-       }
+       get_teams(function(teams) {
+               get_groups(function(groups) {
+                       get_all_group_games(teams, groups, function(games) {
+                               get_all_playoff_games(teams, groups, games, function(playoff_games) {
+                                       games = games.concat(playoff_games);
+                                       games = games.filter(function(game) { return game.streamday !== undefined; });
+                                       display_stream_schedule_parsed(teams, games, 0);
+                               });
+                       });
+               });
+       });
 };
 
 function do_series(series)
@@ -754,52 +826,41 @@ function do_series_internal(series, idx)
 
 function showcarousel()
 {
-       let teams_per_group = [];
-       let games_per_group = [];
-       let combined_teams = [];
-       let combined_games = [];
        let groups_to_get = [
                'Group A',
                'Group B',
                'Group C',
-               'Playoffs 9th-13th',
-               'Playoffs'
+               'Playoffs',
+               'Playoffs 9th–11th',
+               'Playoffs 12th–14th'
        ];
-       let num_left = groups_to_get.length;
-
-       let cb = function(response, group_name) {
-               let teams = parse_teams_from_spreadsheet(response);
-               let games = parse_games_from_spreadsheet(response, group_name, true);
-               teams = filter_teams(teams, response);
-               teams_per_group[group_name] = teams;
-               games_per_group[group_name] = games;
-
-               combined_teams = combined_teams.concat(teams);
-               combined_games = combined_games.concat(games);
-               if (--num_left == 0) {
-                       let series = [
-                               [ 13000, function() { display_group_parsed(teams_per_group['Group A'], games_per_group['Group A'], 'Group A'); } ],
-                               [ 2000, function() { hidetable(); } ],
-                               [ 13000, function() { display_group_parsed(teams_per_group['Group B'], games_per_group['Group B'], 'Group B'); } ],
-                               [ 2000, function() { hidetable(); } ],
-                               [ 13000, function() { display_group_parsed(teams_per_group['Group C'], games_per_group['Group C'], 'Group C'); } ],
-                               [ 2000, function() { hidetable(); } ],
-                               [ 13000, function() { display_group_parsed(teams_per_group['Playoffs 9th-13th'], games_per_group['Playoffs 9th-13th'], 'Playoffs 9th–13th'); } ],
-                               [ 2000, function() { hidetable(); } ]
-                       ];
-                       let num_pages = find_num_pages(combined_games);
-                       for (let page = 0; page < num_pages; ++page) {
-                               series.push([ 13000, function() { display_stream_schedule_parsed(combined_teams, combined_games, page); } ]);
-                               series.push([ 2000, function() { hidetable(); } ]);
-                       }
-
-                       do_series(series);
-               }
-       };
+       get_teams(function(teams) {
+               get_groups(function(groups) {
+                       get_all_group_games(teams, groups, function(games) {
+                               get_all_playoff_games(teams, groups, games, function(playoff_games) {
+                                       games = games.concat(playoff_games);
+                                       games = games.filter(function(game) { return game.streamday !== undefined; });
+
+                                       let series = [
+                                               [ 13000, function() { display_group_parsed(filter_teams_by_group(teams, groups, 'Group A'), games, 'Group A'); } ],
+                                               [ 2000, function() { hidetable(); } ],
+                                               [ 13000, function() { display_group_parsed(filter_teams_by_group(teams, groups, 'Group B'), games, 'Group B'); } ],
+                                               [ 2000, function() { hidetable(); } ],
+                                               [ 13000, function() { display_group_parsed(filter_teams_by_group(teams, groups, 'Group C'), games, 'Group C'); } ],
+                                               [ 2000, function() { hidetable(); } ],
+                                               // We don't show the playoff groups, since we don't even know whether they have data.
+                                       ];
+                                       let num_pages = find_num_pages(games);
+                                       for (let page = 0; page < num_pages; ++page) {
+                                               series.push([ 13000, function() { display_stream_schedule_parsed(teams, games, page); } ]);
+                                               series.push([ 2000, function() { hidetable(); } ]);
+                                       }
 
-       for (const group of groups_to_get) {
-               get_group(group, cb);
-       }
+                                       do_series(series);
+                               });
+                       });
+               });
+       });
 };
 
 function stopcarousel()
index 7987bc54f1d7da9543c5d50be8b26c5d15d1d2b8..d9ef9fa40147e37a951320db749b651aa14b7c06 100644 (file)
--- a/config.js
+++ b/config.js
@@ -53,7 +53,92 @@ var ultimateconfig = {
                        'rank_offset': 12
                },
        },
-       'explain_third_cell': 'Results!S27',
+       'explain_third_cell': 'Results!S26',
+       'group_match_scores': [
+               {
+                       'stream_time_column': 1,  // 0-indexed.
+                       'team1_column': 3,
+                       'team1_score_column': 4,
+                       'team2_score_column': 5,
+                       'team2_column': 6,
+                       'first_row': 7,  // 1-indexed.
+                       'last_row': 19,  // 1-indexed, Inclusive.
+                       'stream_day': 6
+               },
+               {
+                       'team1_column': 8,
+                       'team1_score_column': 9,
+                       'team2_score_column': 10,
+                       'team2_column': 11,
+                       'first_row': 7,
+                       'last_row': 19
+               },
+               {
+                       'team1_column': 13,
+                       'team1_score_column': 14,
+                       'team2_score_column': 15,
+                       'team2_column': 16,
+                       'first_row': 7,
+                       'last_row': 19
+               },
+               // Playoffs 9th–11th. (NOTE: These counts as both playoffs and groups?
+               // Would be problematic for schedules if they were on stream field.)
+               {
+                       'team1_column': 13,
+                       'team1_score_column': 14,
+                       'team2_score_column': 15,
+                       'team2_column': 16,
+                       'first_row': 28,
+                       'last_row': 28,
+                       'group_name': 'Playoffs 9th–11th'
+               },
+               {
+                       'team1_column': 13,
+                       'team1_score_column': 14,
+                       'team2_score_column': 15,
+                       'team2_column': 16,
+                       'first_row': 30,
+                       'last_row': 30,
+                       'group_name': 'Playoffs 9th–11th'
+               },
+               {
+                       'team1_column': 8,
+                       'team1_score_column': 9,
+                       'team2_score_column': 10,
+                       'team2_column': 11,
+                       'first_row': 33,
+                       'last_row': 33,
+                       'group_name': 'Playoffs 9th–11th'
+               },
+               // Playoffs 12th–14th.
+               {
+                       'team1_column': 13,
+                       'team1_score_column': 14,
+                       'team2_score_column': 15,
+                       'team2_column': 16,
+                       'first_row': 29,
+                       'last_row': 29,
+                       'group_name': 'Playoffs 12th–14th'
+               },
+               {
+                       'team1_column': 13,
+                       'team1_score_column': 14,
+                       'team2_score_column': 15,
+                       'team2_column': 16,
+                       'first_row': 31,
+                       'last_row': 31,
+                       'group_name': 'Playoffs 12th–14th'
+               },
+               {
+                       'team1_column': 13,
+                       'team1_score_column': 14,
+                       'team2_score_column': 15,
+                       'team2_column': 16,
+                       'first_row': 33,
+                       'last_row': 33,
+                       'group_name': 'Playoffs 12th–14th'
+               },
+       ],
 
        'playoff_games': [
                // Teams                        Field   Row    Match name     Mark name    Group name              Game day (default 7)
index fe2fe2de5252a197308ddaab1c195c886dff9701..7e0e289cb67a199a1daa4a042e218df79f6a6196 100644 (file)
@@ -98,7 +98,8 @@
         <a href="javascript:stopcarousel();showgroup('Group A')">show group A</a>
         <a href="javascript:stopcarousel();showgroup('Group B')">show group B</a>
         <a href="javascript:stopcarousel();showgroup('Group C')">show group C</a>
-        <a href="javascript:stopcarousel();showgroup('Playoffs 9th-13th')">show group L</a>
+        <a href="javascript:stopcarousel();showgroup('Playoffs 9th–11th')">show group L1</a>
+        <a href="javascript:stopcarousel();showgroup('Playoffs 12th–14th')">show group L2</a>
         <a href="javascript:stopcarousel();showschedule()">show schedule</a>
         <a href="javascript:stopcarousel();showcarousel()">show carousel</a>
         <a href="javascript:stopcarousel();hidetable()">table out</a>
index fb7c96d6814d6ec89c41692dbc0b8ce3190f1a34..9a1fff4531abbfe116f6d1d2cf55795092f54d29 100644 (file)
@@ -65,16 +65,13 @@ function possibly_update_oauth_key(cb) {
        }
 }
 
-function publish_group_rank(response, group_name)
+function publish_group_rank(teams, games, group_name)
 {
        let updates = [];
        let config = ultimateconfig['group_cells'][group_name];
        let cols = config['score_sheet_cols'];
 
-       let teams = parse_teams_from_spreadsheet(response);
-       let games = parse_games_from_spreadsheet(response, group_name, false);
-       apply_games_to_teams(games, teams);
-       teams = filter_teams(teams, response);
+       apply_games_to_teams(games, teams, group_name);
 
        // Write the points total to the unsorted columns.
        if (config['point_total_start_row'] !== null) {
@@ -110,40 +107,40 @@ function publish_group_rank(response, group_name)
        });
 }
 
-function montecarlo(responses) {
+function montecarlo(all_teams, groups, games, groups_to_calc) {
        let pseudo_group_names = ['X', 'Y', 'Z'];
-       let real_group_names = ['A', 'B', 'C'];
-       let teams = [], games = [], teams_to_idx = [];
+       let real_group_names = ['A', 'B', 'C'];  // Better be corresponding to groups_to_calc...
+       let teams_to_idx = [];
 
        let third_groups = [];
        let busted_thirds = false;
 
-       for (const response of responses) {
-               let teams_group = parse_teams_from_spreadsheet(response);
-               let games_group = parse_games_from_spreadsheet(response, 'irrelevant group name', true);
-               apply_games_to_teams(games_group, teams_group);
-
+       // Split teams by group.
+       let teams = [];
+       for (let group_idx = 0; group_idx < groups_to_calc.length; ++group_idx) {
+               let teams_group = filter_teams_by_group(all_teams, groups, groups_to_calc[group_idx]);
                teams.push(teams_group);
-               games.push(games_group);
                teams_to_idx.push(make_teams_to_idx(teams_group));
        }
 
        for (let simulation_idx = 0; simulation_idx < 100; ++simulation_idx) {  // 100 seems to be enough.
                let thirds = [], ignoreds = [];
-               let groups = [];
-               for (let group_idx = 0; group_idx < responses.length; ++group_idx) {
+               let calc_groups = [];
+
+               for (let group_idx = 0; group_idx < groups_to_calc.length; ++group_idx) {
+                       let teams_copy = [];
+                       for (const team of teams[group_idx]) {
+                               teams_copy.push(Object.assign({}, team));
+                       }
+
                        // Fill in random results. We deliberately use a uniform [-13,+13]
                        // model here, since we are interested in the extremal results.
                        // Of course, not all real games go to 13, but the risk of that
                        // influencing the tiebreakers is very slim.
                        let games_copy = [], games_with_synth = [];
-                       for (const game of games[group_idx]) {
+                       for (const game of games) {
                                games_copy.push(Object.assign({}, game));
                        }
-                       let teams_copy = [];
-                       for (const team of teams[group_idx]) {
-                               teams_copy.push(Object.assign({}, team));
-                       }
 
                        for (let i = 0; i < games_copy.length; ++i) {
                                let idx1 = teams_to_idx[group_idx][games_copy[i].name1];
@@ -152,7 +149,7 @@ function montecarlo(responses) {
                                if (games_copy[i].score1 === undefined || games_copy[i].score2 === undefined ||
                                    isNaN(games_copy[i].score1) || isNaN(games_copy[i].score2) ||
                                    games_copy[i].score1 == games_copy[i].score2) {
-                                       // These were skipped by apply_games_to_teams() above.
+                                       // These were skipped by apply_games_to_teams() earlier.
                                        let score1 = 0, score2 = 0;
                                        let r = Math.floor(Math.random() * 26);
                                        if (r < 13) {
@@ -230,7 +227,7 @@ function montecarlo(responses) {
                                }
                        }
 
-                       groups.push({
+                       calc_groups.push({
                                "games": games_with_synth,
                                "teams": teams_copy
                        });
@@ -241,15 +238,15 @@ function montecarlo(responses) {
                        let tiebreakers = [];
                        if (ultimateconfig['kick_fifth_from_third']) {
                                // Recompute scores (but not ranks!) without the ignored games. (thirds point to these objects.)
-                               for (let group_idx = 0; group_idx < responses.length; ++group_idx) {
-                                       apply_games_to_teams(groups[group_idx].games, groups[group_idx].teams, ignoreds);
+                               for (let group_idx = 0; group_idx < groups_to_calc.length; ++group_idx) {
+                                       apply_games_to_teams(calc_groups[group_idx].games, calc_groups[group_idx].teams, groups_to_calc[group_idx], ignoreds);
                                }
                        }
                        let ranked = rank_thirds([], thirds, 1, tiebreakers);
                        if (simulation_idx == 0) {
                                third_groups = ranked;
                        } else {
-                               for (let i = 0; i < responses.length; ++i) {
+                               for (let i = 0; i < groups_to_calc.length; ++i) {
                                        if (third_groups[i].group_idx !== ranked[i].group_idx) {
                                                third_groups[i].group_idx = null;
                                        }
@@ -259,13 +256,13 @@ function montecarlo(responses) {
        }
 
        let replacements = [];
-       for (let group_idx = 0; group_idx < responses.length; ++group_idx) {
+       for (let group_idx = 0; group_idx < groups_to_calc.length; ++group_idx) {
                if (third_groups[group_idx].group_idx !== null) {
                        replacements.push([ pseudo_group_names[group_idx], real_group_names[third_groups[group_idx].group_idx] ]);
                }
        }
 
-       for (let group_idx = 0; group_idx < responses.length; ++group_idx) {
+       for (let group_idx = 0; group_idx < groups_to_calc.length; ++group_idx) {
                for (let i = 0; i < teams[group_idx].length; ++i) {
                        if (teams[group_idx][i].simulated_rank !== null) {
                                replacements.push([ real_group_names[group_idx] + teams[group_idx][i].simulated_rank, teams[group_idx][i].shortname ]);
@@ -298,19 +295,16 @@ function do_replacements(str, replacements) {
        return str;
 }
 
-function fill_playoff(replacements, teams) {
+function fill_playoff(all_teams, groups, replacements, teams) {
        let team_expansions = {};
-       for (const group of teams) {
-               for (const team of group) {
-                       team_expansions[team.name] = team_expansions[team.mediumname] = team_expansions[team.shortname] =
-                               [ team.name, team.mediumname, team.shortname ];
-               }
+       for (const team of all_teams) {
+               team_expansions[team.name] = team_expansions[team.mediumname] = team_expansions[team.shortname] =
+                       [ team.name, team.mediumname, team.shortname ];
        }
 
        let games = ultimateconfig['playoff_games'];
        get_results('Results', function(response) {
                let updates = [], meta_updates = [];
-               let game_num = 0;
                for (const game of games) {
                        let team1 = do_replacements(game[0], replacements);
                        let team2 = do_replacements(game[1], replacements);
@@ -363,27 +357,6 @@ function fill_playoff(replacements, teams) {
                                updates.push({ "range": cell_score1, "values": [ [ game[4] ] ] });
                                meta_updates.push({ "mergeCells": { "range": range, "mergeType": "MERGE_ALL" }});
                        }
-
-                       if (game[2] == 0) {  // Stream field.
-                               // Game.
-                               let ss_row = ultimateconfig['playoff_games_start_row_detail_sheet'] + game_num;
-                               updates.push({
-                                       "range": "Playoffs!A" + ss_row + ":J" + ss_row,
-                                       "values": [ [ team1, team2, score1, score2, "", "", "", game_day, response['values'][row - 1][1].replace(".",":"), game[6] ] ]
-                               });
-
-                               // Team codes.
-                               updates.push({
-                                       "range": "Playoffs!A" + (2 * game_num + 3) + ":C" + (2 * game_num + 3),
-                                       "values": [ names_for_team(team1, team_expansions) ]
-                               });
-                               updates.push({
-                                       "range": "Playoffs!A" + (2 * game_num + 4) + ":C" + (2 * game_num + 4),
-                                       "values": [ names_for_team(team2, team_expansions) ]
-                               });
-
-                               ++game_num;
-                       }
                }
                let json = {
                        "valueInputOption": "USER_ENTERED",
@@ -393,10 +366,21 @@ function fill_playoff(replacements, teams) {
                        "requests": meta_updates
                };
                possibly_update_oauth_key(function() {
-                       post_json('https://sheets.googleapis.com/v4/spreadsheets/' + ultimateconfig['score_sheet_id'] + '/values:batchUpdate?key=' + ultimateconfig['api_key'], json, function(response) {
-                               get_group('Playoffs 9th-14th', function(response_l) { publish_group_rank(response_l, 'Playoffs 9th-14th'); });
-                       }, current_oauth_access_token);
-                       post_json('https://sheets.googleapis.com/v4/spreadsheets/' + ultimateconfig['score_sheet_id'] + ':batchUpdate?key=' + ultimateconfig['api_key'], meta_json, function(response) {}, current_oauth_access_token);
+                       if (updates.length > 0) {
+                               post_json('https://sheets.googleapis.com/v4/spreadsheets/' + ultimateconfig['score_sheet_id'] + '/values:batchUpdate?key=' + ultimateconfig['api_key'], json, function(response) {
+                                       get_all_group_games(all_teams, groups, function(group_games) {
+                                               // NOTE: filter_teams_by_group will be delayed by one cycle
+                                               // after W P1 etc. becomes determined for the first time.
+                                               let teams_l1 = filter_teams_by_group(all_teams, groups, 'Playoffs 9th–11th');
+                                               let teams_l2 = filter_teams_by_group(all_teams, groups, 'Playoffs 12th–14th');
+                                               publish_group_rank(teams_l1, group_games, 'Playoffs 9th–11th');
+                                               publish_group_rank(teams_l2, group_games, 'Playoffs 12th–14th');
+                                       });
+                               }, current_oauth_access_token);
+                       }
+                       if (meta_updates.length > 0) {
+                               post_json('https://sheets.googleapis.com/v4/spreadsheets/' + ultimateconfig['score_sheet_id'] + ':batchUpdate?key=' + ultimateconfig['api_key'], meta_json, function(response) {}, current_oauth_access_token);
+                       }
                });
        });
 }
@@ -412,28 +396,55 @@ function get_results(sheet_name, cb)
 }
 
 function publish_group_ranks() {
-       get_group('Group A', function(response_a) {
-               get_group('Group B', function(response_b) {
-                       get_group('Group C', function(response_c) {
-                               publish_group_rank(response_a, 'Group A');
-                               publish_group_rank(response_b, 'Group B');
-                               publish_group_rank(response_c, 'Group C');
-
-                               let replacements = montecarlo([response_a, response_b, response_c]);
-                               let team_a = parse_teams_from_spreadsheet(response_a);
-                               let team_b = parse_teams_from_spreadsheet(response_b);
-                               let team_c = parse_teams_from_spreadsheet(response_c);
-                               fill_playoff(replacements, [team_a, team_b, team_c]);
+       get_teams(function(teams) {
+               get_groups(function(groups) {
+                       get_all_group_games(teams, groups, function(games) {
+                               let teams_a = filter_teams_by_group(teams, groups, 'Group A');
+                               let teams_b = filter_teams_by_group(teams, groups, 'Group B');
+                               let teams_c = filter_teams_by_group(teams, groups, 'Group C');
+                               publish_group_rank(teams_a, games, 'Group A');
+                               publish_group_rank(teams_b, games, 'Group B');
+                               publish_group_rank(teams_c, games, 'Group C');
+
+                               let replacements = montecarlo(teams, groups, games, ['Group A', 'Group B', 'Group C']);
+                               fill_playoff(teams, groups, replacements, [teams_a, teams_b, teams_c]);
                        });
                });
        });
 }
 
-function get_ranked(response, group_name) {
-       let teams = parse_teams_from_spreadsheet(response);
-       let games = parse_games_from_spreadsheet(response, group_name, false);
-       apply_games_to_teams(games, teams);
-       teams = filter_teams(teams, response);
+function get_all_playoff_games(teams, groups, group_games, cb) {
+       let replacements = montecarlo(teams, groups, group_games, ['Group A', 'Group B', 'Group C']);
+       let games = ultimateconfig['playoff_games'];
+       get_results('Results', function(response) {
+               let playoff_games = [];
+               for (const game of games) {
+                       let team1 = do_replacements(game[0], replacements);
+                       let team2 = do_replacements(game[1], replacements);
+                       let row = ultimateconfig['playoff_games_start_row'] + game[3];
+                       let cols = ultimateconfig['playoff_games_cols'][game[2]];
+                       let score1 = response['values'][row - 1][cols[1]];
+                       let score2 = response['values'][row - 1][cols[2]];
+                       let streamday = game[7];
+                       if (streamday === undefined && game[2] === 0) {  // Stream field is by default on stream.
+                               streamday = 7;
+                       }
+                        playoff_games.push({
+                                "name1": team1,
+                                "name2": team2,
+                                "score1": parseInt(score1),
+                                "score2": parseInt(score2),
+                                "streamday": streamday,
+                                "streamtime": response['values'][row - 1][1].replace('.', ':'),
+                                "group_name": game[6]
+                        });
+               }
+               cb(playoff_games);
+       });
+}
+
+function get_ranked(teams, games, group_name) {
+       apply_games_to_teams(games, teams, group_name);
        let tiebreakers = [];
        teams = rank(games, teams, 1, tiebreakers);
        return teams;
@@ -479,12 +490,15 @@ function addsign(x)
 
 function publish_best_thirds() {
        if (!ultimateconfig['best_thirds']) return;
-       get_group('Group A', function(response_a) {
-               get_group('Group B', function(response_b) {
-                       get_group('Group C', function(response_c) {
-                               let A = get_ranked(response_a, 'Group A');
-                               let B = get_ranked(response_b, 'Group B');
-                               let C = get_ranked(response_c, 'Group C');
+       get_teams(function(teams) {
+               get_groups(function(groups) {
+                       get_all_group_games(teams, groups, function(games) {
+                               let teams_a = filter_teams_by_group(teams, groups, 'Group A');
+                               let teams_b = filter_teams_by_group(teams, groups, 'Group B');
+                               let teams_c = filter_teams_by_group(teams, groups, 'Group C');
+                               let A = get_ranked(teams_a, games, 'Group A');
+                               let B = get_ranked(teams_b, games, 'Group B');
+                               let C = get_ranked(teams_c, games, 'Group C');
 
                                let candidates = [];
                                pick_out_rank(A, 3, candidates);
@@ -517,12 +531,9 @@ function publish_best_thirds() {
                                        C = jsonclone(C);
 
                                        // Recompute scores (but not ranks!) without the ignored games.
-                                       let games_a = parse_games_from_spreadsheet(response_a, 'Group A', false);
-                                       apply_games_to_teams(games_a, A, ignoreds, ignored_games);
-                                       let games_b = parse_games_from_spreadsheet(response_b, 'Group B', false);
-                                       apply_games_to_teams(games_b, B, ignoreds, ignored_games);
-                                       let games_c = parse_games_from_spreadsheet(response_c, 'Group C', false);
-                                       apply_games_to_teams(games_c, C, ignoreds, ignored_games);
+                                       apply_games_to_teams(games, A, 'Group A', ignoreds, ignored_games);
+                                       apply_games_to_teams(games, B, 'Group B', ignoreds, ignored_games);
+                                       apply_games_to_teams(games, C, 'Group C', ignoreds, ignored_games);
 
                                        // Filter out ignored games involving the candidate thirds.
                                        let candidates_to_idx = make_teams_to_idx(candidates);