X-Git-Url: https://git.sesse.net/?p=ultimatescore;a=blobdiff_plain;f=update_sheets.js;h=3a7b0f7f9611decc700e18dfd4b201017e45bb9d;hp=a9dcfe53c15942b83faa82a4903a0b1e5a827a8d;hb=HEAD;hpb=007efa1462d402147a75a59169e5ad4db74e1c69 diff --git a/update_sheets.js b/update_sheets.js index a9dcfe5..2438135 100644 --- a/update_sheets.js +++ b/update_sheets.js @@ -65,16 +65,14 @@ 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); + teams.sort(function(a, b) { return a.seeding - b.seeding }); // Write the points total to the unsorted columns. if (config['point_total_start_row'] !== null) { @@ -110,39 +108,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 = []; - for (let group_idx = 0; group_idx < responses.length; ++group_idx) { + let thirds = [], ignoreds = []; + 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 = []; - for (const game of games[group_idx]) { + let games_copy = [], games_with_synth = []; + 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]; @@ -151,7 +150,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) { @@ -173,7 +172,15 @@ function montecarlo(responses) { teams_copy[idx2].gd += score2; teams_copy[idx1].gd -= score2; teams_copy[idx2].gd -= score1; + + games_with_synth.push({ + "name1": games_copy[i].name1, + "name2": games_copy[i].name2, + "score1": score1, + "score2": score2 + }); } else { + games_with_synth.push(games_copy[i]); continue; } } @@ -211,18 +218,39 @@ function montecarlo(responses) { } } } + if (ultimateconfig['kick_fifth_from_third'] && teams_copy.length >= 5) { + if (teams_copy[4].rank != 5) { + // A real tie for fifth; the rules are unclear, so just give up. + busted_thirds = true; + } else { + ignoreds.push(teams_copy[4]); + } + } } + + calc_groups.push({ + "games": games_with_synth, + "teams": teams_copy + }); } // Also rank thirds. if (!busted_thirds) { 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 < 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) { - if (third_groups[i].group_idx !== ranked[i].group_idx) { + for (let i = 0; i < groups_to_calc.length; ++i) { + if (third_groups[i].group_idx !== ranked[i].group_idx || // Different from a previous simulation. + (i < (third_groups.length - 1) && ranked[i].rank === ranked[i + 1].rank) || // Disallow ties. + (i > 0 && ranked[i].rank === ranked[i - 1].rank)) { // Disallow ties. third_groups[i].group_idx = null; } } @@ -231,13 +259,33 @@ 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) { + // These are pretty hard-coded, but that's probably fine. Must come after we've concretized X, Y, etc. + for (const group_name of real_group_names) { + let teams = filter_teams_by_group(all_teams, groups, 'Group ' + group_name); + if (teams.length >= 5) { + for (const other_group_name of real_group_names) { + replacements.push([ group_name + other_group_name + '5', group_name + '5' ]); + } + for (const other_group_name of pseudo_group_names) { + replacements.push([ group_name + other_group_name + '5', group_name + '5' ]); + } + } else { // Perhaps a bit overkill. + for (const other_group_name of real_group_names) { + replacements.push([ group_name + other_group_name + '5', other_group_name + '5' ]); + } + for (const other_group_name of pseudo_group_names) { + replacements.push([ group_name + other_group_name + '5', other_group_name + '5' ]); + } + } + } + + 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 ]); @@ -270,19 +318,16 @@ function do_replacements(str, replacements) { return str; } -function fill_playoff(replacements, teams) { +function fill_playoff(all_teams, groups, replacements) { 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); @@ -335,26 +380,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. - updates.push({ - "range": "Playoffs!A" + (game_num + 32) + ":J" + (game_num + 32), - "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", @@ -364,10 +389,23 @@ 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-13th', function(response_l) { publish_group_rank(response_l, 'Playoffs 9th-13th'); }); - }, 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. + // Note that this requires the Groups sheet to pick out + // the right teams from the group matches in the Results sheet! + 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); + } }); }); } @@ -383,28 +421,56 @@ 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); }); }); }); } -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']); + fill_playoff(teams, groups, replacements); // To get the replacements. + 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; @@ -412,6 +478,10 @@ function get_ranked(response, group_name) { // Pick out everything that is at rank N _or_ avoids rank N by lack of tiebreakers only. function pick_out_rank(teams, rank, candidates) { + if (teams.length < rank) { + return; + } + let lowest_rank = teams[rank - 1].rank; let count = 0; @@ -433,24 +503,106 @@ function pick_out_rank(teams, rank, candidates) { } } +function addsign(x) +{ + if (x < 0) { + return "−" + (-x); + } else if (x == 0) { + return "=0"; + } else { + return "+" + x; + } +} + function publish_best_thirds() { - 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'); + if (!ultimateconfig['best_thirds']) return; + 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); pick_out_rank(B, 3, candidates); pick_out_rank(C, 3, candidates); + let ignoreds = []; + let ignored_games = [], ignored_games_expl = []; + if (ultimateconfig['kick_fifth_from_third']) { + let ignoreds_A = [], ignoreds_B = [], ignoreds_C = []; + pick_out_rank(A, 5, ignoreds_A); + pick_out_rank(B, 5, ignoreds_B); + pick_out_rank(C, 5, ignoreds_C); + + if (ignoreds_A.length >= 2) { + ignoreds_A = [ ignoreds_A[ignoreds_A.length - 1] ]; + } + if (ignoreds_B.length >= 2) { + ignoreds_B = [ ignoreds_B[ignoreds_B.length - 1] ]; + } + if (ignoreds_C.length >= 2) { + ignoreds_C = [ ignoreds_C[ignoreds_C.length - 1] ]; + } + ignoreds = ignoreds_A.concat(ignoreds_B).concat(ignoreds_C); + + // Protect the “candidates” array, so that apply_games_to_teams() further down + // doesn't modify it (we want to compare old and new). + A = jsonclone(A); + B = jsonclone(B); + C = jsonclone(C); + + // Recompute scores (but not ranks!) without the 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); + for (const game of ignored_games) { + if (candidates_to_idx[game[0]] !== undefined || + candidates_to_idx[game[1]] !== undefined) { + if (game[2]) { + ignored_games_expl.push("Ignoring (arbitrarily) " + game[0] + "–" + game[1]); + } else { + ignored_games_expl.push("Ignoring " + game[0] + "–" + game[1]); + } + } + } + + let new_teams = A.concat(B).concat(C); + let new_teams_to_idx = make_teams_to_idx(new_teams); + + // Move back the scores (points, gd, goals). + for (let cand of candidates) { + let new_version = new_teams[new_teams_to_idx[cand.shortname]]; + if (cand.pts != new_version.pts || + cand.gd != new_version.gd || + cand.goals != new_version.goals) { + cand.pts = new_version.pts; + cand.gd = new_version.gd; + cand.goals = new_version.goals; + ignored_games_expl.push(cand.shortname + " at " + cand.pts + " pts, " + addsign(new_version.gd) + " GD"); + } + } + } + let tiebreakers = []; let text = ""; if (candidates.length >= 2) { let ranked = rank_thirds([], candidates, 1, tiebreakers); - text = "Best thirds: " + ranked[0].mediumname + ", " + ranked[1].mediumname + "\n" + tiebreakers.join("\n"); + let best_thirds = ranked.filter(function(team) { return team.rank <= 2; }); + if (best_thirds.length == 2) { + text = "Best thirds: " + best_thirds.map(function(team) { return team.mediumname }).join(', ') + "\n"; + if (ignored_games_expl.length > 0) { + text += ignored_games_expl.join("; ") + "\n"; + } + text += tiebreakers.join("\n"); + } } let updates = []; updates.push({ "range": ultimateconfig['explain_third_cell'], "values": [ [ text ] ] });