BGA Pythia - 7 Wonders Architects game helper

Visual aid that extends BGA game interface with useful information

  1. // ==UserScript==
  2. // @name BGA Pythia - 7 Wonders Architects game helper
  3. // @description Visual aid that extends BGA game interface with useful information
  4. // @namespace https://github.com/dpavliuchkov/bga-pythia
  5. // @author https://github.com/dpavliuchkov
  6. // @version 1.2.6
  7. // @license MIT
  8. // @include *boardgamearena.com/*
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. // System variables - don't edit
  13. const Enable_Logging = false;
  14. const Is_Inside_Game = /\?table=[0-9]*/.test(window.location.href);
  15. const BGA_Player_Scoreboard_Id_Prefix = "overall_player_board_";
  16. const BGA_Player_Score_Right_Id_Prefix = "player_name_";
  17. const BGA_Player_Score_Main_Id_Prefix = "playerarea_";
  18. const BGA_Progress_Id_Prefix = "pg_";
  19. const Player_Score_Right_Id_Prefix = "pythia_score_right_";
  20. const Player_Score_Main_Id_Prefix = "pythia_score_main_";
  21. const Progress_Worth_Id_Prefix = "pythia_progress_worth_";
  22. const Player_Score_Span_Class = "player_score_value";
  23. const Player_Leader_Class = "pythia_leader";
  24. const Progress_Worth_Class = "progress_worth";
  25. const Cat_Card_Type_Id = 16;
  26. const Politics_Progress_Type_Id = 2;
  27. const Decor_Progress_Type_Id = 6;
  28. const Strategy_Progress_Type_Id = 8;
  29. const Education_Progress_Type_Id = 12;
  30. const Culture_Progress_Type_Id = 13;
  31. const Cat_Pawn_Type_Id = 17;
  32. const Decor_Points = 4;
  33.  
  34. // progress tokens - type args
  35. // 1 - science draw
  36. // 2 - cat politics
  37. // 3 - wood / clay
  38. // 4 - ??
  39. // 5 - double gold
  40. // 6 - decor
  41. // 7 - wonder stages
  42. // 8 - victories
  43. // 9 - two shields
  44. // 10 - horns
  45. // 11 - ??
  46. // 12 - education
  47. // 13 - culture
  48. // 14 - engineering
  49. // 15 - ??
  50. // 16 - stone gold
  51. // 17 - cat pawn
  52.  
  53. // big game https://boardgamearena.com/1/sevenwondersarchitects?table=227901083
  54.  
  55.  
  56. // Main Pythia object
  57. var pythia = {
  58. isStarted: false,
  59. isFinished: false,
  60. dojo: null,
  61. game: null,
  62. mainPlayerId: null,
  63. players: [],
  64.  
  65. // Init Pythia
  66. init: function() {
  67. this.isStarted = true;
  68. // Check if the site was loaded correctly
  69. if (!window.parent || !window.parent.dojo || !window.parent.gameui.gamedatas) {
  70. return;
  71. }
  72. this.dojo = window.parent.dojo;
  73. this.game = window.parent.gameui.gamedatas;
  74.  
  75. const playerOrder = this.game.playerorder;
  76. this.playersCount = playerOrder.length;
  77. this.mainPlayerId = playerOrder[0];
  78.  
  79. // Prepare player objects and containers
  80. const keys = Object.keys(this.game.players);
  81. for (const playerId of keys) {
  82. this.players[playerId] = {
  83. bgaScore: 0,
  84. wonderStages: 0,
  85. totalCatCards: 0,
  86. totalWarVictories: 0,
  87. totalProgressTokens: 0,
  88. hasDecor: false,
  89. hasCulture: false,
  90. hasEducation: false,
  91. };
  92. this.renderPythiaContainers(playerId);
  93. }
  94.  
  95. // Prepare progress tokens
  96. this.progressTokens = {};
  97. this.initProgressTokens();
  98.  
  99. this.setStyles();
  100.  
  101. // Connect event handlers to follow game progress
  102. this.dojo.subscribe("updateScore", this, "recordScoreUpdate");
  103. this.dojo.subscribe("getProgress", this, "recordProgressToken");
  104. this.dojo.subscribe("getCard", this, "recordGetCard");
  105. this.dojo.subscribe("flipWonderStage", this, "recordWonderStage");
  106. this.dojo.subscribe("conflictResult", this, "recordWarResult");
  107. this.dojo.subscribe("showProgress", this, "recordProgressShow");
  108. this.dojo.subscribe("victory", this, "recordVictory");
  109.  
  110. if (Enable_Logging) console.log("PYTHIA: My eyes can see everything!");
  111. return this;
  112. },
  113.  
  114. // Record new scores
  115. recordScoreUpdate: function(data) {
  116. if (Enable_Logging) console.log("PYTHIA: scores updated - I got", data);
  117.  
  118. // Input check
  119. if (!data || !data.args) {
  120. return;
  121. }
  122.  
  123. const playerId = data.args.player_id;
  124. this.players[playerId].bgaScore = data.args.score;
  125.  
  126. const totalScore = this.getPlayerScore(playerId);
  127. this.renderPlayerScore(playerId, totalScore);
  128.  
  129. const leaderId = this.getLeader();
  130. this.renderLeader(leaderId);
  131. },
  132.  
  133. // Record which card a player got
  134. recordGetCard: function(data) {
  135. if (Enable_Logging) console.log("PYTHIA: player took a card - I got", data);
  136.  
  137. // Input check
  138. if (!data || !data.args || !data.args.card) {
  139. return;
  140. }
  141.  
  142. const playerId = data.args.player_id;
  143. var playerObjectChanged = false;
  144.  
  145. // Increment if player drew a cat card
  146. if (data.args.card.type == Cat_Card_Type_Id) {
  147. this.players[playerId].totalCatCards++;
  148. playerObjectChanged = true;
  149. }
  150.  
  151. // Update score values for progress tokens
  152. if (playerObjectChanged && playerId == this.mainPlayerId) {
  153. this.updateAllProgressWorth();
  154. }
  155. },
  156.  
  157. // Record when a wonder stage was built
  158. recordWonderStage: function(data) {
  159. if (Enable_Logging) console.log("PYTHIA: wonder stage built - I got", data);
  160.  
  161. // Input check
  162. if (!data || !data.args || !data.args.player_id) {
  163. return;
  164. }
  165.  
  166. const playerId = data.args.player_id;
  167. this.players[playerId].wonderStages++; // increase a counter of built wonder stages
  168.  
  169. // 5th wonder stage means the game is over
  170. if (this.players[playerId].wonderStages == 5) {
  171. this.isFinished = true;
  172. }
  173. },
  174.  
  175. // Record which progress a player got
  176. recordProgressToken: function(data) {
  177. if (Enable_Logging) console.log("PYTHIA: player took a progress token - I got", data);
  178.  
  179. // Input check
  180. if (!data || !data.args || !data.args.progress) {
  181. return;
  182. }
  183.  
  184. // Skip movements of the cat pawn
  185. if (data.args.progress.type_arg == Cat_Pawn_Type_Id) {
  186. return;
  187. }
  188.  
  189. const playerId = data.args.player_id;
  190. const token = data.args.progress;
  191.  
  192. // Track progress tokens that can give victory points
  193. if (token.type_arg == Decor_Progress_Type_Id) {
  194. this.players[playerId].hasDecor = true;
  195. }
  196. if (token.type_arg == Culture_Progress_Type_Id) {
  197. this.players[playerId].hasCulture = true;
  198. }
  199. if (token.type_arg == Education_Progress_Type_Id) {
  200. this.players[playerId].hasEducation = true;
  201. }
  202.  
  203. // Increment total progress tokens
  204. this.players[playerId].totalProgressTokens++;
  205.  
  206. // Remove this token from the open list
  207. delete this.progressTokens[token.id];
  208.  
  209. // Remove progress worth container
  210. this.dojo.destroy(Progress_Worth_Id_Prefix + token.id);
  211.  
  212. // Update score values for progress tokens
  213. if (playerId == this.mainPlayerId) {
  214. this.updateAllProgressWorth();
  215. }
  216. },
  217.  
  218. // Record war results
  219. recordWarResult: function(data) {
  220. if (Enable_Logging) console.log("PYTHIA: war has ended - I got", data);
  221.  
  222. // Input check
  223. if (!data || !data.args || !data.args.score) {
  224. return;
  225. }
  226.  
  227. // Update who got military win tokens
  228. const warResults = data.args.score;
  229. for (const playerId in warResults) {
  230. this.players[playerId].totalWarVictories += warResults[playerId].length;
  231. }
  232.  
  233. // Update score values for progress tokens
  234. this.updateAllProgressWorth();
  235. },
  236.  
  237. // Record which new progress token was shown
  238. recordProgressShow: function(data) {
  239. if (Enable_Logging) console.log("PYTHIA: new progress token displayed - I got", data);
  240.  
  241. // Input check
  242. if (!data || !data.args || !data.args.progress) {
  243. return;
  244. }
  245.  
  246. // Add this token to the open list
  247. const token = data.args.progress;
  248. this.progressTokens[token.id] = token.type_arg;
  249.  
  250. // Update score values for progress tokens
  251. const progressWorth = this.getProgressWorth(token.type_arg, this.mainPlayerId);
  252. this.renderProgressWorth(token.id, progressWorth);
  253. },
  254.  
  255. // Record that the game has ended
  256. recordVictory: function(data) {
  257. if (Enable_Logging) console.log("PYTHIA: game has finished - I got", data);
  258.  
  259. // Input check
  260. if (!data || !data.args || !data.args.score) {
  261. return;
  262. }
  263.  
  264. this.isFinished = true;
  265. const finalScores = data.args.score;
  266.  
  267. const keys = Object.keys(finalScores);
  268. for (const playerId of keys) {
  269. this.renderPlayerScore(playerId, finalScores[playerId].total);
  270. }
  271.  
  272. const leaderId = this.getLeader();
  273. this.renderLeader(leaderId);
  274. },
  275.  
  276. renderProgressWorth: function(progressId, worth = 0) {
  277. // Clean previous value
  278. this.dojo.destroy(Progress_Worth_Id_Prefix + progressId);
  279.  
  280. // Render progress worth
  281. this.dojo.place(
  282. "<span id='" + Progress_Worth_Id_Prefix + progressId + "'" +
  283. " class='" + Progress_Worth_Class + "'>⭐" + worth + "</span>",
  284. BGA_Progress_Id_Prefix + progressId,
  285. "first");
  286. },
  287.  
  288. // Update total player score
  289. renderPlayerScore: function(playerId, score = 0) {
  290. var playerScore = this.dojo.byId(Player_Score_Main_Id_Prefix + playerId);
  291. if (playerScore) {
  292. this.dojo.query("#" + Player_Score_Main_Id_Prefix + playerId)[0]
  293. .innerHTML = "⭐" + score;
  294. this.dojo.query("#" + Player_Score_Right_Id_Prefix + playerId)[0]
  295. .innerHTML = "⭐" + score;
  296. }
  297. },
  298.  
  299. // Add border and position of the leader player
  300. renderLeader: function(leaderId) {
  301. // Clean previous leader
  302. this.dojo.query("." + Player_Leader_Class).removeClass(Player_Leader_Class);
  303.  
  304. // Mark new leader
  305. this.dojo.addClass(BGA_Player_Scoreboard_Id_Prefix + leaderId, Player_Leader_Class);
  306. this.dojo.addClass(BGA_Player_Score_Main_Id_Prefix + leaderId, Player_Leader_Class);
  307. },
  308.  
  309. // Render player containers
  310. renderPythiaContainers: function(playerId) {
  311. // Insert war score container in scores table
  312. if (!this.dojo.byId(Player_Score_Main_Id_Prefix + playerId)) {
  313. const mainPlayerArea = this.dojo.query("#" + BGA_Player_Score_Main_Id_Prefix + playerId + " .stw_name_holder");
  314. this.dojo.place(
  315. "<span id='" + Player_Score_Main_Id_Prefix + playerId + "'" +
  316. "class='" + Player_Score_Span_Class + "'>⭐0</span>",
  317. mainPlayerArea[0],
  318. "first");
  319.  
  320. this.dojo.place(
  321. "<span id='" + Player_Score_Right_Id_Prefix + playerId + "'" +
  322. "class='" + Player_Score_Span_Class + "'>⭐0</span>",
  323. BGA_Player_Score_Right_Id_Prefix + playerId,
  324. "first");
  325. }
  326. },
  327.  
  328. // Called at the game start to detect which progress tokens were drawn
  329. initProgressTokens: function() {
  330. const tokens = this.dojo.query("#science .progress");
  331. for (const token of tokens) {
  332. if (!token.style || !token.id) {
  333. continue;
  334. }
  335.  
  336. const tokenId = parseInt(token.id.substr(3));
  337. if (tokenId == 0) {
  338. continue;
  339. }
  340.  
  341. // Calculate token background - to find which token is displayed
  342. const posX = Math.abs(parseInt(token.style.backgroundPositionX) / 100);
  343. const posY = Math.abs(parseInt(token.style.backgroundPositionY) / 100);
  344.  
  345. var tokenType;
  346. // Relevant tokens
  347. if (posY == 0 && posX == 2) {
  348. tokenType = Politics_Progress_Type_Id;
  349. } else if (posY == 1 && posX == 2) {
  350. tokenType = Decor_Progress_Type_Id;
  351. } else if (posY == 2 && posX == 0) {
  352. tokenType = Strategy_Progress_Type_Id;
  353. } else if (posY == 3 && posX == 0) {
  354. tokenType = Education_Progress_Type_Id;
  355. } else if (posY == 3 && posX == 1) {
  356. tokenType = Culture_Progress_Type_Id;
  357. } else {
  358. // Not relevant token
  359. tokenType = 0;
  360. }
  361.  
  362. this.progressTokens[tokenId] = tokenType;
  363. const progressWorth = this.getProgressWorth(tokenType, this.mainPlayerId);
  364. this.renderProgressWorth(tokenId, progressWorth);
  365. }
  366. },
  367.  
  368. // Update worth of all visible progress tokens
  369. updateAllProgressWorth: function() {
  370. for (var tokenId in this.progressTokens) {
  371. const tokenType = this.progressTokens[tokenId];
  372. const progressWorth = this.getProgressWorth(tokenType, this.mainPlayerId);
  373. this.renderProgressWorth(tokenId, progressWorth);
  374. }
  375. },
  376.  
  377. // Calculate how much this progress is worth for this player
  378. getProgressWorth: function(tokenType, playerId) {
  379. const player = this.players[playerId];
  380. var pointsWorth = 0;
  381.  
  382. switch (parseInt(tokenType)) {
  383. case Politics_Progress_Type_Id:
  384. pointsWorth = player.totalCatCards;
  385. break;
  386.  
  387. case Decor_Progress_Type_Id:
  388. pointsWorth = Decor_Points;
  389. break;
  390.  
  391. case Strategy_Progress_Type_Id:
  392. pointsWorth = player.totalWarVictories;
  393. break;
  394.  
  395. case Education_Progress_Type_Id:
  396. pointsWorth = 2 + player.totalProgressTokens * 2;
  397. break;
  398.  
  399. case Culture_Progress_Type_Id:
  400. pointsWorth = 4;
  401. if (player.hasCulture) {
  402. pointsWorth = 8;
  403. }
  404. break;
  405. }
  406.  
  407. if (player.hasEducation) {
  408. pointsWorth += 2;
  409. }
  410.  
  411. return pointsWorth;
  412. },
  413.  
  414. // Get total player score at teh current moment of game
  415. getPlayerScore: function(playerId) {
  416. const player = this.players[playerId];
  417. var totalScore = player.bgaScore;
  418.  
  419. // If game not finished - add 4 points for decor progress
  420. if (!this.isFinished && player.hasDecor) {
  421. totalScore += Decor_Points;
  422. }
  423. return totalScore;
  424. },
  425.  
  426. // Calculate who is the current leader
  427. getLeader: function() {
  428. // Find leader
  429. var totalScores = [];
  430. const keys = Object.keys(this.players);
  431. for (const playerId of keys) {
  432. totalScores.push(
  433. [playerId,
  434. this.getPlayerScore(playerId),
  435. this.players[playerId].wonderStages
  436. ]);
  437. }
  438. totalScores.sort(function(a, b) {
  439. return b[1] - a[1] || b[2] - a[2];
  440. });
  441.  
  442. return totalScores[0][0];
  443. },
  444.  
  445. // Set Pythia CSS styles
  446. setStyles: function() {
  447. this.dojo.query("body").addClass("pythia_enabled");
  448. this.dojo.place(
  449. "<style type='text/css' id='Pythia_Styles'>" +
  450. "." + Player_Leader_Class + " .player-name" +
  451. ", ." + Player_Leader_Class + " a" +
  452. ", ." + Player_Leader_Class + " .stw_name_holder" +
  453. " { background-color: green; border-radius: 5px; color: white ! important; } " +
  454. ".stw_name_holder ." + Player_Score_Span_Class + " { margin-right: 10px; margin-top: -6px; } " +
  455. "#science ." + Progress_Worth_Class + " { display: block; position: relative; top: -32px; left: 25%; height: 25px; width: 34px; background: #F5EDE6; padding-left: 4px; padding-bottom: 3px; border-radius: 10px; } " +
  456. "</style>", "game_play_area_wrap", "last");
  457. }
  458. };
  459.  
  460. function sleep(ms) {
  461. return new Promise(resolve => setTimeout(resolve, ms));
  462. }
  463.  
  464. function isObjectEmpty(object) {
  465. return typeof(object) == "undefined" ||
  466. (Object.keys(object).length === 0 && object.constructor === Object);
  467. }
  468.  
  469. // Everything starts here
  470. var onload = async function() {
  471. if (Is_Inside_Game) {
  472. await sleep(3000); // Wait for BGA to load dojo and 7W scripts
  473. if (!window.parent || !window.parent.gameui || !window.parent.gameui.game_name ||
  474. window.parent.gameui.game_name != "sevenwondersarchitects") {
  475. return;
  476. }
  477.  
  478. // Prevent multiple launches
  479. if (window.parent.isPythiaStarted) {
  480. return;
  481. } else {
  482. if (Enable_Logging) console.log("PYTHIA: I have come to serve you");
  483. window.parent.isPythiaStarted = true;
  484. window.parent.pythia = pythia.init();
  485. }
  486. }
  487. };
  488.  
  489. if (document.readyState === "complete") {
  490. onload();
  491. } else {
  492. (addEventListener || attachEvent).call(window, addEventListener ? "load" : "onload", onload);
  493. }