using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Text; namespace GW2_Matchup_Probability { struct Record { public Single Rating; public Int32 Index; } class Program { static void Main(String[] args) { String[] WN = new String[64]; // world names Double[] WR = new Double[64]; // world ratings Double[] WD = new Double[64]; // world deviations Double[] WV = new Double[64]; // world volatilities Double[] RD = new Double[64]; // world random deviations (see https://www.guildwars2.com/en/news/big-changes-coming-to-wvw-matchups/) Int32 N = 0; // defaults Int64 TRIALS = 10000000000; // 10 billion trials takes hours to run, for interactive use choose a smaller number (10 million is good) Double BASE = 40.0; // default base variation (see https://www.guildwars2.com/en/news/big-changes-coming-to-wvw-matchups/) Double MULTIPLIER = 1.0; // default deviation variation (see https://www.guildwars2.com/en/news/big-changes-coming-to-wvw-matchups/) Int32 MODE = 0; // parse command-line arguments Int32 ap = 0; while (ap < args.Length) { String arg = args[ap++]; if (arg == null) { // skip } else if (arg.Substring(0, 2).Equals("-n", StringComparison.OrdinalIgnoreCase)) { arg = arg.Substring(2); if (arg.Length == 0) arg = args[ap++]; TRIALS = Int64.Parse(arg); } else if (arg.Substring(0, 2).Equals("-b", StringComparison.OrdinalIgnoreCase)) { arg = arg.Substring(2); if (arg.Length == 0) arg = args[ap++]; BASE = Double.Parse(arg); } else if (arg.Substring(0, 2).Equals("-m", StringComparison.OrdinalIgnoreCase)) { arg = arg.Substring(2); if (arg.Length == 0) arg = args[ap++]; MULTIPLIER = Double.Parse(arg); } else if (arg.Substring(0, 2).Equals("-0", StringComparison.OrdinalIgnoreCase)) { MODE = 0; } else if (arg.Substring(0, 2).Equals("-1", StringComparison.OrdinalIgnoreCase)) { MODE = 1; } else if (arg.Substring(0, 2).Equals("-2", StringComparison.OrdinalIgnoreCase)) { MODE = 2; } else if (arg.Substring(0, 2).Equals("-3", StringComparison.OrdinalIgnoreCase)) { MODE = 3; } } // read tab-separated input String buf; while ((buf = Console.In.ReadLine()) != null) { String[] f = buf.Split('\t'); if (f.Length < 5) throw new ApplicationException(buf); WN[N] = f[1].Trim(); // world name WR[N] = Double.Parse(f[2]); // world rating WD[N] = Double.Parse(f[3]); // world deviation WV[N] = Double.Parse(f[4]); // world volatility RD[N] = BASE + MULTIPLIER * WD[N]; // random deviation (see https://www.guildwars2.com/en/news/big-changes-coming-to-wvw-matchups/) N++; } Random Rnd = new Random(); // calculate new world ratings, deviations and volatilities based on old ratings, deviations and volatilities and current game scores if (MODE == 0) { Int32[] I = new Int32[3]; // I[i] is the index used to locate world data in WN[], WR[], WD[], WV[], RD[] Double[] mu = new Double[N]; // Glicko-2 mu (old rating) Double[] phi = new Double[N]; // Glicko-2 phi (old deviation) Double[] sigma = new Double[N]; // Glicko-2 sigma (old volatility) Double[] mu1 = new Double[N]; // Glicko-2 mu prime (new rating) Double[] phi1 = new Double[N]; // Glicko-2 phi prime (new deviation) Double[] sigma1 = new Double[N]; // Glicko-2 sigma prime (new volatility) Double[] WR1 = new Double[N]; // new world rating (GW2 rating scale) Double[] WD1 = new Double[N]; // new world deviation (GW2 rating scale) // Glicko-2 Step 2: convert values to Glicko-2 scale // see http://www.glicko.net/glicko/glicko2.pdf and http://www.guildwars2guru.com/news/884-the-math-behind-wvw-ratings/ for (Int32 i = 0; i < N; i++) { mu[i] = (WR[i] - 1500.0) / 173.7178; // Glicko-2 rating phi[i] = WD[i] / 173.7178; // Glicko-2 deviation sigma[i] = WV[i]; // Glicko-2 volatility } // get world names from GW2 API JSON.Value worlds = CallAPI("https://api.guildwars2.com/v1/world_names.json"); // get WvW matches from GW2 API JSON.Value matches = CallAPI("https://api.guildwars2.com/v1/wvw/matches.json")["wvw_matches"]; // for each match, get scores and calculate new ratings for (Int32 m = 0; m < matches.Count; m++) { JSON.Value match = matches[m]; // get match scores from GW2 API JSON.Value scores = CallAPI(String.Concat("https://api.guildwars2.com/v1/wvw/match_details.json?match_id=", match["wvw_match_id"].ToString()))["scores"]; // figure out the index for each world in this match I[0] = GetWorldIndex(WN, GetWorldName(worlds, match["red_world_id"].ToString())); // scores[0] is the score for the red world I[1] = GetWorldIndex(WN, GetWorldName(worlds, match["blue_world_id"].ToString())); // scores[1] is the score for the blue world I[2] = GetWorldIndex(WN, GetWorldName(worlds, match["green_world_id"].ToString())); // scores[2] is the score for the green world if ((I[0] == -1) || (I[1] == -1) || (I[2] == -1)) continue; // skip this match if the worlds aren't listed in the input // calculate new ratings for each of the 3 worlds for (Int32 i = 0; i < 3; i++) { // rating calculation for world i // Glicko-2 Steps 3 and 4: calculate v and capital Delta // see http://www.glicko.net/glicko/glicko2.pdf and http://www.guildwars2guru.com/news/884-the-math-behind-wvw-ratings/ Double v = 0.0; // Glicko-2 v (estimated variance) Double Delta = 0.0; // Glicko-2 capital Delta (estimated rating improvement) for (Int32 j = 1; j < 3; j++) { Double g = fn_g(phi[I[(i+j)%3]]); // (i+j)%3 always falls in the range [0..2] Double E = fn_E(mu[I[i]], mu[I[(i+j)%3]], phi[I[(i+j)%3]]); Double s = scores[i].AsDouble / (scores[i].AsDouble + scores[(i+j)%3].AsDouble); s = (Math.Sin((s - 0.5) * Math.PI) + 1.0) / 2.0; v += g * g * E * (1.0 - E); Delta += g * (s - E); } v = 1.0 / v; Delta = v * Delta; // Glicko-2 Step 5: iterative procedure to calculate sigma prime Double a = Math.Log(sigma[I[i]] * sigma[I[i]]); Double A = a; Double B; if (Delta * Delta > phi[I[i]] * phi[I[i]] + v) { B = Math.Log(Delta * Delta - phi[I[i]] * phi[I[i]] - v); } else { Double k = 1.0; while (fn_f(a - k * Math.Sqrt(0.6 * 0.6), Delta, phi[I[i]], v, sigma[I[i]]) < 0.0) k += 1.0; B = a - k * Math.Sqrt(0.6 * 0.6); } Double fA = fn_f(A, Delta, phi[I[i]], v, sigma[I[i]]); Double fB = fn_f(B, Delta, phi[I[i]], v, sigma[I[i]]); while (Math.Abs(B - A) > 0.000001) { Double C = A + (A - B) * fA / (fB - fA); Double fC = fn_f(C, Delta, phi[I[i]], v, sigma[I[i]]); if (fC * fB < 0.0) { A = B; fA = fB; } else { fA = fA / 2.0; } B = C; fB = fC; } sigma1[I[i]] = Math.Exp(A / 2.0); // Glicko-2 Step 6: calculate phi star Double phi_star = Math.Sqrt(phi[I[i]] * phi[I[i]] + sigma1[I[i]] * sigma1[I[i]]); // Glicko-2 Step 7: calculate phi prime and mu prime phi1[I[i]] = 1.0 / Math.Sqrt(1.0 / (phi_star * phi_star) + 1.0 / v); mu1[I[i]] = mu[I[i]] + phi1[I[i]] * phi1[I[i]] * Delta / v; // Delta = v Sigma[g(){s - E()}], therefore Sigma[G(){s - E()}] = Delta / v // Glicko-2 Step 8: convert back to original scale WR1[I[i]] = 173.7178 * mu1[I[i]] + 1500.0; WD1[I[i]] = 173.7178 * phi1[I[i]]; } } // sort worlds according to their new ratings Record[] R = new Record[N]; for (Int32 i = 0; i < N; i++) { R[i].Rating = (Single)(WR1[i]); R[i].Index = i; } for (Int32 i = 0; i < N; i++) { for (Int32 j = i + 1; j < N; j++) { if (R[j].Rating > R[i].Rating) { Record t = R[i]; R[i] = R[j]; R[j] = t; } } } // display output for (Int32 i = 0; i < N; i++) { Int32 j = R[i].Index; Console.Write(i + 1); Console.Write('\t'); Console.Write(WN[j]); Console.Write('\t'); Console.Write(WR1[j].ToString("F4")); Console.Write('\t'); Console.Write(WD1[j].ToString("F4")); Console.Write('\t'); Console.Write(sigma1[j].ToString("F4")); // also output rating movement Console.Write('\t'); Double mv = WR1[j] - WR[j]; if (mv > 0.0) Console.Write('+'); Console.Write(mv.ToString("F4")); // also output ranking movement (only if rank changes) j -= i; if (j != 0) { Console.Write('\t'); if (j > 0) Console.Write('+'); Console.Write(j); } Console.WriteLine(); } } // calculate the likelihood of getting a particular opponent using Monte Carlo method else if (MODE == 1) { Int64[,] P = new Int64[N, N]; // P[i,j] counts the number of times world i gets world j as an opponent Record[] MR = new Record[N]; // MR[] is used to order worlds by match rating for (Int64 trial = 0; trial < TRIALS; trial++) { // calculate match ratings // see https://www.guildwars2.com/en/news/big-changes-coming-to-wvw-matchups/. for (Int32 i = 0; i < N; i++) { MR[i].Rating = (Single)(WR[i] + RD[i] * (Rnd.NextDouble() * 2.0 - 1.0)); MR[i].Index = i; } // sort worlds by match rating for (Int32 i = 0; i < N; i++) { for (Int32 j = i + 1; j < N; j++) { if (MR[j].Rating > MR[i].Rating) { Record t = MR[i]; MR[i] = MR[j]; MR[j] = t; } } } for (Int32 i = 0; i < N; ) { // simulate matchups by choosing groups of 3 servers: a, b, c Int32 a = MR[i++].Index; Int32 b = MR[i++].Index; Int32 c = MR[i++].Index; // increment counters recording who got matched with whom P[a, b]++; P[a, c]++; P[b, a]++; P[b, c]++; P[c, a]++; P[c, b]++; } } // display results for (Int32 i = 0; i < N; i++) { Console.WriteLine(WN[i]); for (Int32 j = 0; j < N; j++) { if (i == j) continue; // a server never plays itself Console.Write((100.0 * P[i, j] / TRIALS).ToString("F6")); Console.Write('\t'); Console.WriteLine(WN[j]); } Console.WriteLine(); } } // calculate the likelihood of getting a particular pair of opponents using Monte Carlo method else if (MODE == 2) { Int64[,,] P = new Int64[N, N, N]; // P[i,j,k] counts the number of times green world i gets blue world j and red world k as opponents Record[] MR = new Record[N]; // MR[] is used to order worlds by match rating for (Int64 trial = 0; trial < TRIALS; trial++) { // calculate match ratings // see https://www.guildwars2.com/en/news/big-changes-coming-to-wvw-matchups/. for (Int32 i = 0; i < N; i++) { MR[i].Rating = (Single)(WR[i] + RD[i] * (Rnd.NextDouble() * 2.0 - 1.0)); MR[i].Index = i; } // sort worlds by match rating for (Int32 i = 0; i < N; i++) { for (Int32 j = i + 1; j < N; j++) { if (MR[j].Rating > MR[i].Rating) { Record t = MR[i]; MR[i] = MR[j]; MR[j] = t; } } } for (Int32 i = 0; i < N; ) { // simulate matchups by choosing groups of 3 servers: a, b, c Int32 a = MR[i++].Index; Int32 b = MR[i++].Index; Int32 c = MR[i++].Index; // increment counters recording who got matched with whom P[a, b, c]++; } } // display results for (Int32 i = 0; i < N; i++) { Console.WriteLine(WN[i]); for (Int32 j = 0; j < N; j++) { if (j == i) continue; // a server never plays itself for (Int32 k = j + 1; k < N; k++) { // count the number of times worlds i, j and k played, disregarding map colors Int64 n = 0; n += P[i, j, k]; n += P[i, k, j]; n += P[j, i, k]; n += P[j, k, i]; n += P[k, i, j]; n += P[k, j, i]; if (n == 0) continue; // don't show anything if there were no recorded matchups at all Console.Write((100.0 * n / TRIALS).ToString("F6")); Console.Write('\t'); Console.Write(WN[j]); Console.Write('\t'); Console.WriteLine(WN[k]); } } Console.WriteLine(); } } // calculate the likelihood of any particular matchup using Monte Carlo method else if (MODE == 3) { Int64[,,] P = new Int64[N, N, N]; // P[i,j,k] counts the number of times green world i gets blue world j and red world k as opponents Record[] MR = new Record[N]; // MR[] is used to order worlds by match rating for (Int64 trial = 0; trial < TRIALS; trial++) { // calculate match ratings // see https://www.guildwars2.com/en/news/big-changes-coming-to-wvw-matchups/. for (Int32 i = 0; i < N; i++) { MR[i].Rating = (Single)(WR[i] + RD[i] * (Rnd.NextDouble() * 2.0 - 1.0)); MR[i].Index = i; } // sort worlds by match rating for (Int32 i = 0; i < N; i++) { for (Int32 j = i + 1; j < N; j++) { if (MR[j].Rating > MR[i].Rating) { Record t = MR[i]; MR[i] = MR[j]; MR[j] = t; } } } for (Int32 i = 0; i < N; ) { // simulate matchups by choosing groups of 3 servers: a, b, c Int32 a = MR[i++].Index; Int32 b = MR[i++].Index; Int32 c = MR[i++].Index; // increment counters recording who got matched with whom P[a, b, c]++; } } // display results for (Int32 i = 0; i < N; i++) { for (Int32 j = 0; j < N; j++) { if (j == i) continue; // a server never plays itself for (Int32 k = 0; k < N; k++) { if ((k == j) || (k == i)) continue; // a server never plays itself Int64 n = P[i, j, k]; if (n == 0) continue; // don't show anything if there were no recorded matchups at all Console.Write((100.0 * n / TRIALS).ToString("F6")); Console.Write('\t'); Console.Write(WN[i]); Console.Write('\t'); Console.Write(WN[j]); Console.Write('\t'); Console.WriteLine(WN[k]); } } } } } // Call the GW2 API private static JSON.Value CallAPI(String url) { // fetch the webpage named by the URL HttpWebRequest req = WebRequest.Create(url) as HttpWebRequest; req.Method = "GET"; JSON.Value retval = null; try { HttpWebResponse rsp = req.GetResponse() as HttpWebResponse; if (rsp.StatusCode == HttpStatusCode.OK) { // read the webpage and parse it into a JSON value Stream s = rsp.GetResponseStream(); StreamReader sr = new StreamReader(s, Encoding.UTF8); retval = JSON.Value.ReadFrom(sr); sr.Close(); } rsp.Close(); } catch (Exception ex) { throw ex; } return retval; } // Determine the name of a world, given its API id private static String GetWorldName(JSON.Value worlds, String id) { for (Int32 i = 0; i < worlds.Count; i++) { JSON.Value world = worlds[i]; if (world["id"] == id) return world["name"].ToString(); } return null; } // Determine the index for a world, given its name private static Int32 GetWorldIndex(String[] worlds, String name) { for (Int32 i = 0; i < worlds.Length; i++) { if (worlds[i] == name) return i; } return -1; } // Glicko-2 function g() private static Double fn_g(Double phi) { return 1.0 / Math.Sqrt(1.0 + 3.0*(phi*phi)/(Math.PI*Math.PI)); } // Glicko-2 function E() private static Double fn_E(Double mu, Double muj, Double phij) { return 1.0 / (1.0 + Math.Exp(fn_g(phij) * (muj - mu))); } // Glicko-2 function f() private static Double fn_f(Double x, Double Delta, Double phi, Double v, Double sigma) { Double n1 = Math.Exp(x) * (Delta * Delta - phi * phi - v - Math.Exp(x)); Double d1 = phi * phi + v + Math.Exp(x); d1 = 2.0 * d1 * d1; Double n2 = x - Math.Log(sigma * sigma); Double d2 = 0.6 * 0.6; return n1 / d1 - n2 / d2; } } }