From: Steinar H. Gunderson Date: Wed, 16 Oct 2019 07:50:05 +0000 (+0200) Subject: Compute and fill in playoffs. X-Git-Url: https://git.sesse.net/?a=commitdiff_plain;h=64c11ffc84930a031dffd586d403d7a1faf7d9cb;p=ultimatescore Compute and fill in playoffs. --- diff --git a/config.js b/config.js index 9711956..d9371e1 100644 --- a/config.js +++ b/config.js @@ -17,5 +17,35 @@ var ultimateconfig = { 'point_total_start_row': 10, 'ranking_list_start_row': 17, - 'ranking_list_explain_row': 23 + 'ranking_list_explain_row': 23, + 'explain_third_cell': 'Results!X36', + + 'playoff_games': [ + // Teams Field Row Match name Mark name Group name + [ 'X5', 'Y5', 0, 0, 'X5/Y5', false, 'Playoffs 13th–15th' ], + [ 'X1', 'Y2', 1, 0, 'X1/Y2', false, 'Quarterfinals' ], + [ 'X2', 'Y3', 0, 1, 'X2/Y3', false, 'Quarterfinals' ], + [ 'Y1', 'X2', 1, 1, 'Y1/X2', false, 'Quarterfinals' ], + [ 'Z1', 'X3', 2, 1, 'Z1/X3', false, 'Quarterfinals' ], + [ 'X4', 'Z3', 0, 2, 'X4/Z3', false, 'Playoffs 9th–12th' ], + [ 'X5', 'Z5', 1, 2, 'X5/Z5', false, 'Playoffs 13th–15th' ], + [ 'Y3', 'Z3', 2, 2, 'Y3/Z3', false, 'Playoffs 9th–12th' ], + [ 'W X1/Y2', 'W Z1/X3', 0, 3, 'Semi 1', true, 'Semifinals' ], + [ 'L X1/Y2', 'L Z1/X3', 1, 3, 'L-semi 1', true, 'Playoffs 5th–8th' ], + [ 'W X2/Y3', 'W Y2/X2', 0, 4, 'Semi 2', true, 'Semifinals' ], + [ 'L X2/Y3', 'L Y2/X2', 1, 4, 'L-semi 2', true, 'Playoffs 5th–8th' ], + [ 'Y5', 'Z5', 2, 4, 'Y5/Z5', false, 'Playoffs 13th–15th' ], + [ 'W X4/Z3', 'W Y3/Z3', 0, 5, '9th', true, 'Match for 9th' ], + [ 'L X4/Z3', 'L Y3/Z3', 1, 5, '11th', true, 'Match for 11th' ], + [ 'L semi 1', 'L semi 2', 0, 6, '3rd', true, 'Bronze final' ], + [ 'W L-semi 1', 'W L-semi 2', 1, 6, '5th', true, 'Match for 5th' ], + [ 'L L-semi 1', 'L L-semi 2', 2, 6, '7th', true, 'Match for 7th' ], + [ 'W semi 1', 'W semi 2', 0, 7, 'Final', true, 'Final' ] + ], + 'playoff_games_start_row': 26, + 'playoff_games_cols': [ + [ 3, 4, 5, 6 ], + [ 8, 9, 10, 11 ], + [ 13, 14, 15, 16 ], + ] }; diff --git a/update_sheets.js b/update_sheets.js index 641c94d..b03c48d 100644 --- a/update_sheets.js +++ b/update_sheets.js @@ -65,39 +65,267 @@ function possibly_update_oauth_key(cb) { } } -function publish_group_rank(group_name) +function publish_group_rank(response, group_name) { - get_group(group_name, function(response, group_name) { - let updates = []; - let cols = ultimateconfig['score_sheet_cols'][group_name]; + let updates = []; + let cols = ultimateconfig['score_sheet_cols'][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); + + // Write the points total to the unsorted columns. + for (let i = 0; i < teams.length; ++i) { + let row = ultimateconfig['point_total_start_row'] + i; + updates.push({ "range": cols[2] + row, "values": [ [ teams[i].pts ] ] }); + } + + let tiebreakers = []; + teams = rank(games, teams, 1, tiebreakers); + + // Write the ranking table, from scratch. + for (let i = 0; i < teams.length; ++i) { + let row = ultimateconfig['ranking_list_start_row'] + i; + updates.push({ "range": cols[0] + row, "values": [ [ teams[i].rank ] ] }); + updates.push({ "range": cols[1] + row, "values": [ [ teams[i].mediumname ] ] }); + updates.push({ "range": cols[2] + row, "values": [ [ teams[i].pts ] ] }); + } + + let tb_str = ""; + if (tiebreakers.length != 0) { + tb_str = tiebreakers.join("\n"); + } + updates.push({ "range": cols[0] + ultimateconfig['ranking_list_explain_row'], "values": [ [ tb_str ] ]}); + + let json = { + "valueInputOption": "USER_ENTERED", + "data": 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) {}, current_oauth_access_token); + }); +} - let teams = parse_teams_from_spreadsheet(response); - let games = parse_games_from_spreadsheet(response, group_name, false); - apply_games_to_teams(games, teams); +function montecarlo(responses) { + let pseudo_group_names = ['X', 'Y', 'Z']; + let real_group_names = ['A', 'B', 'C']; + let teams = [], games = [], teams_to_idx = []; - // Write the points total to the unsorted columns. - for (let i = 0; i < teams.length; ++i) { - let row = ultimateconfig['point_total_start_row'] + i; - updates.push({ "range": cols[2] + row, "values": [ [ teams[i].pts ] ] }); + 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); + + 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 = []; + for (let group_idx = 0; group_idx < responses.length; ++group_idx) { + // 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 = []; + for (const game of games[group_idx]) { + 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]; + let idx2 = teams_to_idx[group_idx][games_copy[i].name2]; + if (idx1 === undefined || idx2 === undefined) continue; + 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. + let score1 = 0, score2 = 0; + let r = Math.floor(Math.random() * 26); + if (r < 13) { + score1 = 13; + score2 = r; + teams_copy[idx1].pts += 2; + } else { + score1 = r - 13; + score2 = 13; + teams_copy[idx2].pts += 2; + } + games_copy[i].score1 = score1; + games_copy[i].score2 = score2; + ++teams_copy[idx1].nplayed; + ++teams_copy[idx2].nplayed; + teams_copy[idx1].goals += score1; + teams_copy[idx2].goals += score2; + teams_copy[idx1].gd += score1; + teams_copy[idx2].gd += score2; + teams_copy[idx1].gd -= score2; + teams_copy[idx2].gd -= score1; + } else { + continue; + } + } + + // Now rank according to the simulation. + let tiebreakers = []; + teams_copy = rank(games_copy, teams_copy, 1, tiebreakers); + + // See if we have conflicting information with other simulations. + if (simulation_idx == 0) { + for (let i = 0; i < teams[group_idx].length; ++i) { + let idx = teams_to_idx[group_idx][teams_copy[i].name]; + teams[group_idx][idx].simulated_rank = teams_copy[i].rank; + } + } else { + for (let i = 0; i < teams[group_idx].length; ++i) { + let idx = teams_to_idx[group_idx][teams_copy[i].name]; + if (teams[group_idx][idx].simulated_rank !== teams_copy[i].rank) { + teams[group_idx][idx].simulated_rank = null; + } + } + } + + if (!busted_thirds) { + let any_third_found = false; + for (let i = 0; i < teams[group_idx].length; ++i) { + // Store the third. + if (i == 2 || teams_copy[i].rank == 3) { + if (any_third_found) { + busted_thirds = true; + } else { + teams_copy[i].group_idx = group_idx; + thirds.push(teams_copy[i]); + any_third_found = true; + } + } + } + } + } + + // Also rank thirds. + if (!busted_thirds) { + let tiebreakers = []; + let ranked = rank_thirds([], thirds, 1, tiebreakers); + if (simulation_idx == 0) { + third_groups = ranked; + } else { + for (let i = 0; i < responses.length; ++i) { + if (third_groups[i].group_idx !== ranked[i].group_idx) { + third_groups[i].group_idx = null; + } + } + } } + } - let tiebreakers = []; - teams = rank(games, teams, 1, tiebreakers); + let replacements = []; + for (let group_idx = 0; group_idx < responses.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] ]); + } + } - // Write the ranking table, from scratch. - for (let i = 0; i < teams.length; ++i) { - let row = ultimateconfig['ranking_list_start_row'] + i; - updates.push({ "range": cols[0] + row, "values": [ [ teams[i].rank ] ] }); - updates.push({ "range": cols[1] + row, "values": [ [ teams[i].mediumname ] ] }); - updates.push({ "range": cols[2] + row, "values": [ [ teams[i].pts ] ] }); + for (let group_idx = 0; group_idx < responses.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 ]); + } } + } + + return replacements; +} + +function names_for_team(team, expansions) { + if (expansions.hasOwnProperty(team)) { + return expansions[team]; + } + let longteam = team.replace("W ", "Win. ").replace("L ", "Los. "); + return [ longteam, longteam, team ]; +} + +function do_replacements(str, replacements) { + for (const r of replacements) { + str = str.replace(r[0], r[1]); + } + return str; +} - let tb_str = ""; - if (tiebreakers.length != 0) { - tb_str = tiebreakers.join("\n"); +function fill_playoff(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 ]; } - updates.push({ "range": cols[0] + ultimateconfig['ranking_list_explain_row'], "values": [ [ tb_str ] ]}); + } + + let games = ultimateconfig['playoff_games']; + get_results('Results', function(response) { + let updates = []; + let game_num = 0; + 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 cell_team1 = "Results!" + String.fromCharCode(cols[0] + 65) + row; + let cell_score1 = "Results!" + String.fromCharCode(cols[1] + 65) + row; + let cell_score2 = "Results!" + String.fromCharCode(cols[2] + 65) + row; + let cell_team2 = "Results!" + String.fromCharCode(cols[3] + 65) + row; + updates.push({ "range": cell_team1, "values": [ [ team1 ] ] }); + updates.push({ "range": cell_team2, "values": [ [ team2 ] ] }); + let score1 = response['values'][row - 1][cols[1]]; + let score2 = response['values'][row - 1][cols[2]]; + let game_name = game[4]; + let game_name2 = game_name.replace("Semi", "semi"); + + if (parseInt(score1) >= 0 && parseInt(score2) >= 0 && score1 != score2) { + if (parseInt(score1) > parseInt(score2)) { + replacements.unshift(["W " + game_name, team1]); + replacements.unshift(["L " + game_name, team2]); + replacements.unshift(["W " + game_name2, team1]); + replacements.unshift(["L " + game_name2, team2]); + } else { + replacements.unshift(["W " + game_name, team2]); + replacements.unshift(["L " + game_name, team1]); + replacements.unshift(["W " + game_name2, team2]); + replacements.unshift(["L " + game_name2, team1]); + } + } else if (game[5]) { + score1 = score2 = ""; + updates.push({ "range": cell_score1, "values": [ [ game[4] ] ] }); + } + + if (game[2] == 0) { // Stream field. + // Game. + updates.push({ + "range": "Playoffs!A" + (game_num + 32) + ":J" + (game_num + 32), + "values": [ [ team1, team2, score1, score2, "", "", "", 7, 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", "data": updates @@ -105,14 +333,35 @@ function publish_group_rank(group_name) 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) {}, current_oauth_access_token); }); - }); } +function get_results(sheet_name, cb) +{ + let req = new XMLHttpRequest(); + req.onload = function(e) { + cb(JSON.parse(req.responseText), sheet_name); + }; + req.open('GET', 'https://sheets.googleapis.com/v4/spreadsheets/' + ultimateconfig['score_sheet_id'] + '/values/\'' + sheet_name + '\'!A1:Q50?key=' + ultimateconfig['api_key']); + req.send(); +} + function publish_group_ranks() { - publish_group_rank('Group A'); - publish_group_rank('Group B'); - publish_group_rank('Group C'); + 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]); + }); + }); + }); } function get_ranked(response, group_name) {