daily pastebin goal
73%
SHARE
TWEET

Untitled

a guest Jan 18th, 2019 61 Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. /* Tribal Wars 2 Farmbot v2018-12-15
  2. This bot farms barbarian villages in Tribal Wars 2. It reads your battle reports and sends troops to your farms again.
  3.  
  4. ## Features Of This Bot
  5.  
  6. ### Easy To Configure
  7. + Reads battle reports to identify your farm villages.
  8. + Uses the ‘Attack Again’ function in the battle report to attack each target village with your preferred composition of the army.
  9. + Uses the chrome web browser to support in-game configuration (e.g. reports filter).
  10.  
  11. ### Efficient
  12. + Automatically activates correct villages to attack from the same villages again.
  13. + Improves efficiency of units distribution: Skips combination of attacking and defending village for which an attack has already been sent in the current cycle.
  14. + Applies heuristics to reduce the number of reports which need to be read to learn about all farming villages coordinates.
  15.  
  16. ### Safe
  17. + Supports random breaks between farming cycles.
  18. + Stops the farming when the configured time limit is met to avoid perpetual activity on your account.
  19.  
  20. For details about how it works, see https://forum.botengine.org/t/farm-manager-tribal-wars-2-farmbot/1406
  21.  
  22. bot-catalog-tags:tribal-wars-2
  23. */
  24.  
  25. using System;
  26. using System.Diagnostics;
  27. using System.Linq;
  28. using PuppeteerSharp;
  29. using System.Collections.Generic;
  30.  
  31.  
  32. //  Minimum timespan to break between farming cycles.
  33. const int breakBetweenCycleDurationMinSeconds = 60 * 60;
  34.  
  35. //  Upper limit of a random additional time for breaking between farming cycles.
  36. const int breakBetweenCycleDurationRandomAdditionMaxSeconds = 60 * 20;
  37.  
  38. const int numberOfFarmCyclesToRepeatMin = 1;
  39.  
  40. const int numberOfFarmCyclesToRepeatRandomAdditionMax = 0;
  41.  
  42. //  The bot ends a farming cycle when it has seen this many reports in a row with already covered coordinates of attacking and defending villages.
  43. const int numberOfConsecutiveReportsWithAlreadyCoveredCoordinatesToEndCycle = 30;
  44.  
  45. const int waitForReportListButtonTimespanMaxSeconds = 120;
  46. const int inGameDelaySeconds = 10;
  47. const int cycleDurationMaxSeconds = 60 * 40;
  48.  
  49.  
  50. /*
  51. 2018-08-10 Observed HTML:
  52. <a href="#" class="icon-60x60-reports new-messages animation-jump" ng-click="open('report-list')" id="report-button" ng-class="{'new-messages animation-jump': gameDataModel.getNewReportCount() > 0}" tooltip="" tooltip-content="Berichte" internal-id="53">
  53. */
  54. static string openReportListButtonXPath => "//a[@id='report-button']";
  55.  
  56. /*
  57. 2018-08-10 Observed HTML:
  58. <a href="#" ng-class="{'btn-icon btn-orange': selectedTab != TAB_TYPES.Battle}">
  59. */
  60. static string battleReportsButtonXPath => "//a[contains(@ng-class,'selectedTab != TAB_TYPES.Battle')]";
  61.  
  62. /*
  63. 2018-08-10 Observed HTML:
  64. <li ng-repeat="report in reports" class="list-item row-even" ng-show="$index >= firstVisible &amp;&amp; $index <= lastVisible" ng-class="{'row-even': $index % 2 == 0, 'row-odd': $index % 2 == 1}">
  65. */
  66. static string reportListItemXPath => "//li[@ng-repeat='report in reports']";
  67.  
  68. /*
  69. 2018-08-11 Observed HTML:
  70. <a href="#" class="size-34x34 btn-orange btn-border btn-footer-small icon-34x34-attack" ng-click="attackAgain()" tooltip="" tooltip-content="Nochmal angreifen" internal-id="140"></a>
  71. */
  72. static string inReportAttackAgainButtonXPath => "//a[@ng-click='attackAgain()']";
  73.  
  74. /*
  75. 2018-08-11 Observed HTML:
  76. <a href="#" class="btn-orange btn-form btn-border no-padding" ng-class="{'btn-grey': armyEmpty || targetIsNoobProtected || officerOffersVisible}" ng-click="sendArmy('attack')" tooltip="" tooltip-content="Angreifen" internal-id="293">
  77. */
  78. static string inSendArmyFormSendAttackButtonXPath => "//a[@ng-click=\"sendArmy('attack')\"]";
  79.  
  80. /*
  81. 2018-08-18 Observed HTML (as button was enabled):
  82. <a href="#" ng-hide="noNavigation" class="size-34x34 btn-report-header icon-26x26-arrow-right btn-orange" ng-class="{true : 'btn-grey icon-inactive', false : 'btn-orange' }[(curReportIndex >= reportList.length - 1) &amp;&amp; lastPage]" ng-click="pageReport(1)"></a>
  83. */
  84. static string inReportViewNavigateToNextReportButtonXPath => "//a[@ng-click='pageReport(1)']";
  85.  
  86. static string inBattleReportViewDetailsXPath => "//*[contains(@class, 'report-battle')]//table[@ng-show='showCasual']";
  87.  
  88. static string inBattleReportViewToggleDetailsButtonXPath => "//*[contains(@class, 'report-battle')]//*[@ng-click='toggleCasual()']";
  89.  
  90. static string inReportViewScrollWrapXPath => "//*[@ng-controller='ReportController']//*[contains(@class, 'scroll-wrap')]/parent::*";
  91.  
  92. static string openVillageInfoButtonXPath => "//*[@ng-click='openVillageInfo(character.getSelectedVillage().getId())']";
  93.  
  94. static string reportJumpToAttackerVillageXPath => "//*[starts-with(@ng-click, 'jumpToVillage(report[type].attVillage')]";
  95.  
  96. static string mapVillageContextMenuItemActivateXPath => "//*[contains(@class, 'context-menu-item') and contains(@class, 'activate')]";
  97.  
  98. static string customArmyWindowCloseButtonXPath => "//*[@ng-controller='ModalCustomArmyController']//*[@ng-click='closeWindow()']";
  99.  
  100. static string browserPageVisibilityGuide => "If you see this happen all the time, make sure that the game is visible in the web browser.";
  101.  
  102. static string switchVillageChatWindowGuide => "If you see this happen frequently, make sure that no chat window is open in the game. (https://forum.botengine.org/t/farm-manager-tribal-wars-2-farmbot/1406/108?u=viir)";
  103.  
  104. Host.Log("Welcome! - ¡Bienvenido! - Bienvenue! ---- This is the Tribal Wars 2 Farmbot. I read your battle reports and send troops to your farms again. To learn more about how I work, see https://forum.botengine.org/t/tribal-wars-2-farmbot-2018/1330. In case you have any questions, feel free to ask at https://forum.botengine.org");
  105.  
  106. var browserPage = WebBrowser.OpenNewBrowserPage();
  107.  
  108. browserPage.SetViewportAsync(new ViewPortOptions { Width = 1112, Height = 726, }).Wait();
  109.  
  110. browserPage.Browser.PagesAsync().Result.Except(new []{browserPage}).Select(page => { page.CloseAsync().Wait(); return 4;}).ToList();
  111.  
  112. Host.Log("Looks like opening the web browser was successful.");
  113.  
  114. Host.Delay(1);
  115.  
  116. var sessionReport = new CycleReport{ BeginTime = Host.GetTimeContinuousMilli() / 1000 };
  117.  
  118. var logSessionStats = new Action(() =>
  119.     {
  120.         Host.Delay(1);
  121.         Host.Log("In this session, " + sessionReport.StatisticsText);
  122.         Host.Delay(1);
  123.     });
  124.  
  125. var cycleCount = RandomIntFromMinimumAndRandomAddition(numberOfFarmCyclesToRepeatMin, numberOfFarmCyclesToRepeatRandomAdditionMax);
  126.  
  127. var villageLocationsToNotAttack = new Dictionary<VillageLocation, BattleReportDetails>();
  128.  
  129. for(int cycleIndex = 0; ; ++cycleIndex)
  130. {
  131.     Host.Log("Starting cycle " + cycleIndex + " of " + cycleCount + ".");
  132.  
  133.     if(0 < cycleIndex)
  134.     {
  135.         Host.Log("This is not the first farming cycle, I ask the browser to reload the page to get to a clean state.");
  136.         browserPage.ReloadAsync(new NavigationOptions { Timeout = 15000, WaitUntil = new []{WaitUntilNavigation.DOMContentLoaded} }).Wait();
  137.         Host.Delay(1111);
  138.     }
  139.  
  140.     Host.Delay(1111);
  141.  
  142.     var waitForReportListButtonStopwatch = Stopwatch.StartNew();
  143.  
  144.     while(true)
  145.     {
  146.         Host.Log("I did not find the button to open the report list. Maybe the game is still loading. (The location of the current page is '" + browserPage.Url + "'). If you have not done this yet, please log in and enter the game in the web browser I opened when the script started. For now, I keep looking for that button to appear....");
  147.    
  148.         if(WaitForOpenReportListButton(15000) != null)
  149.             break;
  150.  
  151.         if(waitForReportListButtonTimespanMaxSeconds < waitForReportListButtonStopwatch.Elapsed.TotalSeconds)
  152.         {
  153.             var message = "Did not find the button to open the report list while waiting for " + ((int)waitForReportListButtonStopwatch.Elapsed.TotalSeconds) + "seconds. Therefore I stop the bot.";
  154.             Host.Log(message);
  155.             throw new ApplicationException(message);
  156.         }
  157.     }
  158.  
  159.     Host.Log("Found the button to open the report list.");
  160.     Host.Log("Please configure filtering in the reports list now. I will wait for " + inGameDelaySeconds + " seconds before continuing. In case you need more time to configure the game, you can pause the bot.");
  161.  
  162.     Host.Delay(1000 * inGameDelaySeconds);
  163.  
  164.     var urlWithoutCharIdMatch = System.Text.RegularExpressions.Regex.Match(browserPage.Url ?? "", ".*(?<!&character_id=.*)");
  165.  
  166.     Host.Log("Url without character id: " + (urlWithoutCharIdMatch.Success ? ("'" + urlWithoutCharIdMatch.Value + "'") : "no match"));
  167.  
  168.     var openReportListButton = WaitForOpenReportListButton(1000);
  169.  
  170.     if (openReportListButton == null)
  171.         throw new NotImplementedException("Did not find openReportListButton.");
  172.  
  173.     Host.Log("Found the button to open the report list. I click on this button....");
  174.  
  175.     Host.Delay(333);
  176.  
  177.     if(!AttemptClickAndLogError(() => WaitForOpenReportListButton(1000)))
  178.         throw new NotImplementedException("Did not find openReportListButton.");
  179.  
  180.     var battleReportsButton = WaitForReference(() =>
  181.         browserPage.XPathAsync(battleReportsButtonXPath).Result.FirstOrDefault(), 10000);
  182.  
  183.     if(battleReportsButton == null)
  184.     {
  185.         Host.Log("It looks like opening the report list was not successful. Please make sure the zoom level in the browser window is set to 100%.");
  186.         throw new NotImplementedException("Did not find button to switch to battle reports.");
  187.     }
  188.  
  189.     var firstReportItem = WaitForReference(() =>
  190.         browserPage.XPathAsync(reportListItemXPath).Result.FirstOrDefault(), 3000);
  191.  
  192.     if (firstReportItem == null)
  193.         throw new NotImplementedException("Did not find any report in the report list.");
  194.  
  195.     Host.Log("Found at least one report item in the reports list.");
  196.  
  197.     //  AttemptClickAndLogError(() => openReportListButton);
  198.  
  199.     Host.Delay(444);
  200.  
  201.     var reports = WaitForReference(() => browserPage.XPathAsync(reportListItemXPath).Result, 1000);
  202.  
  203.     if(reports == null)
  204.         throw new NotImplementedException("Did not find the reports.");
  205.  
  206.     var parsedReportItems = reports.Select(ParseReportListItem).ToList();
  207.  
  208.     //  Host.Log("parsedReportItems:\n" + Newtonsoft.Json.JsonConvert.SerializeObject(parsedReportItems));
  209.     Host.Log("Number of parsed report items: " + parsedReportItems?.Count);
  210.  
  211.     Host.Log("Found a report in the report list. I click on it.");
  212.  
  213.     Host.Delay(333);
  214.  
  215.     AttemptClickAndLogError(() => parsedReportItems.FirstOrDefault().htmlElement);
  216.  
  217.     var cycleReport = new CycleReport{ BeginTime = Host.GetTimeContinuousMilli() / 1000 };
  218.  
  219.     var statisticsLogLastTime = 0;
  220.  
  221.     var logCycleStats = new Action(() =>
  222.         {
  223.             Host.Delay(1);
  224.             Host.Log("In the current cycle, " + cycleReport.StatisticsText);
  225.             Host.Delay(1);
  226.         });
  227.  
  228.     var numberOfConsecutiveReportsWithAlreadyCoveredCoordinatesInCycle = 0;
  229.     var numberOfConsecutiveReportsWithSameCaption = 0;
  230.     string lastReportCaption = null;
  231.  
  232.     while(true)
  233.     {
  234.         var cycleDuration = Host.GetTimeContinuousMilli() / 1000 - cycleReport.BeginTime;
  235.  
  236.         if(cycleDurationMaxSeconds < cycleDuration)
  237.         {
  238.             Host.Log("Stopping after " + cycleDuration + " seconds for safety.");
  239.             break;
  240.         }
  241.  
  242.         {
  243.             var currentTime = (int)(Host.GetTimeContinuousMilli() / 1000);
  244.             var statisticsLogLastTimeAge = currentTime - statisticsLogLastTime;
  245.    
  246.             if(60 * 5 < statisticsLogLastTimeAge)
  247.             {
  248.                 logCycleStats();
  249.                 statisticsLogLastTime = currentTime;
  250.                 logSessionStats();
  251.             }
  252.         }
  253.  
  254.         Host.Delay(777);
  255.  
  256.         var customArmyWindowCloseButton =
  257.             WaitForReference(() =>
  258.                 browserPage.XPathAsync(customArmyWindowCloseButtonXPath).Result?.FirstOrDefault(), 333);
  259.  
  260.         if(customArmyWindowCloseButton != null)
  261.         {
  262.             Host.Log("Looks like there is still an army window open. I click on the button to close it.");
  263.    
  264.             AttemptClickAndLogError(() => customArmyWindowCloseButton);
  265.    
  266.             Host.Delay(555);
  267.    
  268.             continue;
  269.         }
  270.  
  271.         var currentReportHeader = WaitForReference(() =>
  272.             browserPage.XPathAsync("//div[contains(@class,'report-header-wrapper')]").Result?.FirstOrDefault(), 333);
  273.  
  274.         var currentReportCaption =
  275.             GetHtmlElementInnerText(currentReportHeader)?.Trim();
  276.  
  277.         Host.Log("Caption of the current open report: '" + ReportCaptionTextInSingleLine(currentReportCaption) + "'");
  278.  
  279.         if(lastReportCaption == currentReportCaption)
  280.             ++numberOfConsecutiveReportsWithSameCaption;
  281.         else
  282.             numberOfConsecutiveReportsWithSameCaption = 0;
  283.  
  284.         if(cycleReport.ReportsSeen.Any(report => report.Caption == currentReportCaption))
  285.         {
  286.             var messageBase = "Hey, I have seen a report with the same caption before! ";
  287.  
  288.             if(2 < numberOfConsecutiveReportsWithSameCaption)
  289.             {
  290.                 Host.Log(messageBase + "This has happened " + numberOfConsecutiveReportsWithSameCaption + " consecutive times. This could be caused by the the bug in the Tribal Wars 2 Web App which breaks reports display (https://forum.botengine.org/t/bug-in-tribal-wars-2-web-app-broken-report-display/1457). I start the process to recover from this bug.");
  291.  
  292.                 var reportCaptionTimeMatch =
  293.                     System.Text.RegularExpressions.Regex.Match(lastReportCaption ?? "", @"\d{1,2}\:\d{2}\:\d{2}");
  294.  
  295.                 var reportTimeToContinueAt =
  296.                     reportCaptionTimeMatch.Success ? reportCaptionTimeMatch.Value?.Trim() : null;
  297.  
  298.                 if(!ReloadAndEnterReportAtTime(reportTimeToContinueAt))
  299.                 {
  300.                     Host.Log("Failed to reload the report list and continue. I end this cycle.");
  301.                     break;
  302.                 }
  303.                 numberOfConsecutiveReportsWithSameCaption = 0;
  304.                 numberOfConsecutiveReportsWithAlreadyCoveredCoordinatesInCycle = 0;
  305.             }
  306.             else
  307.             {
  308.                 Host.Log(messageBase + "I skip this report and continue with the next one. " + browserPageVisibilityGuide);
  309.             }
  310.  
  311.             goto navigateToNextReport;
  312.         }
  313.  
  314.         var battleReportDetailsContainer =
  315.             WaitForReference(() => browserPage.XPathAsync(inBattleReportViewDetailsXPath).Result.FirstOrDefault(), 333);
  316.  
  317.         if(battleReportDetailsContainer == null)
  318.         {
  319.             Host.Log("Did not find details container in battle report. Maybe this is a different kind of report, so I continue with the next report.");
  320.             goto navigateToNextReport;
  321.         }
  322.  
  323.         var battleReportDetailsContainerClass =
  324.             battleReportDetailsContainer?.GetPropertyAsync("className").Result?.JsonValueAsync().Result?.ToString();
  325.    
  326.         if(battleReportDetailsContainerClass.Contains("ng-hide"))
  327.         {
  328.             Host.Log("Report details seem to be hidden, I try to open the report details.");
  329.    
  330.             if(!AttemptClickAndLogError(() => WaitForReference(() =>
  331.                 browserPage.XPathAsync(inBattleReportViewToggleDetailsButtonXPath).Result.FirstOrDefault(), 333)))
  332.             {
  333.                 Host.Log("Did not find the button to toggle the report details. I end this cycle");
  334.                 break;
  335.             }
  336.    
  337.             Host.Delay(555);
  338.         }
  339.  
  340.         Host.Log("Parse the battle report details....");
  341.  
  342.         var parseBattleReportDetails = ParseBattleReportDetails(battleReportDetailsContainer);
  343.  
  344.         if(parseBattleReportDetails.IsFail)
  345.         {
  346.             Host.Log("Failed to parse the battle report details (" + parseBattleReportDetails.Error + "). I skip this report.");
  347.             goto navigateToNextReport;
  348.         }
  349.  
  350.         var battleReportDetails = parseBattleReportDetails.Result;
  351.  
  352.         battleReportDetails.Caption = currentReportCaption;
  353.  
  354.         Host.Log("Battle report details:\n" + battleReportDetails);
  355.         Host.Delay(1);
  356.  
  357.         cycleReport.ReportsSeen.Add(battleReportDetails);
  358.         sessionReport.ReportsSeen.Add(battleReportDetails);
  359.  
  360.         if(!battleReportDetails.DefenderIsBarbarian)
  361.         {
  362.             villageLocationsToNotAttack[battleReportDetails.DefenderVillageLocation] = battleReportDetails;
  363.             Host.Log("Looks like the defending village was not a barbarian village. I skip this report.");
  364.             goto navigateToNextReport;
  365.         }
  366.  
  367.         BattleReportDetails otherReportIndicatingToNotAttackHere;
  368.  
  369.         if(villageLocationsToNotAttack.TryGetValue(battleReportDetails.DefenderVillageLocation, out otherReportIndicatingToNotAttackHere))
  370.         {
  371.             var sourceReportCaption = ReportCaptionTextInSingleLine(otherReportIndicatingToNotAttackHere.Caption);
  372.             Host.Log("The details of report (‘" + sourceReportCaption + "’) indicated that I should not attack at " + battleReportDetails.DefenderVillageLocation + ". I skip this report.");
  373.             goto navigateToNextReport;
  374.         }
  375.  
  376.         if(cycleReport.ReportsForWhichAttackHasBeenSentAgain.Any(report =>
  377.             report.AttackerVillageLocation.Equals(battleReportDetails.AttackerVillageLocation) &&
  378.             report.DefenderVillageLocation.Equals(battleReportDetails.DefenderVillageLocation)))
  379.         {
  380.             Host.Log("An attack from " + battleReportDetails.AttackerVillageLocation + " to " + battleReportDetails.DefenderVillageLocation + " has already been sent in this cycle. I skip this report and continue with the next one.");
  381.             Host.Delay(1);
  382.  
  383.             ++numberOfConsecutiveReportsWithAlreadyCoveredCoordinatesInCycle;
  384.  
  385.             if(numberOfConsecutiveReportsWithAlreadyCoveredCoordinatesToEndCycle <
  386.                 numberOfConsecutiveReportsWithAlreadyCoveredCoordinatesInCycle)
  387.             {
  388.                 Host.Log("The last " + numberOfConsecutiveReportsWithAlreadyCoveredCoordinatesInCycle + " reports I have seen contained combinations of attacking and defending village coordinates for which I have already sent attacks in this cycle. Because of this, I do not expect to find any more new farm coordinates in the next reports. I end this cycle.");
  389.                 break;
  390.             }
  391.  
  392.             goto navigateToNextReport;
  393.         }
  394.  
  395.         numberOfConsecutiveReportsWithAlreadyCoveredCoordinatesInCycle = 0;
  396.  
  397.         Host.Delay(1);
  398.         Host.Log("This report looks like I should attack here.");
  399.    
  400.         var currentActiveVillageLocation = ReadCurrentActiveVillageLocation();
  401.    
  402.         if(!currentActiveVillageLocation.HasValue)
  403.         {
  404.             Host.Log("Failed to read the current active village location. I stop this cycle.");
  405.             break;
  406.         }
  407.    
  408.         Host.Log("Current active village location is " + currentActiveVillageLocation);
  409.    
  410.         Host.Delay(1);
  411.    
  412.         if(!currentActiveVillageLocation.Equals(battleReportDetails.AttackerVillageLocation))
  413.         {
  414.             Host.Log("Begin switching to village " + battleReportDetails.AttackerVillageLocation + ".");
  415.    
  416.             var scrollExpression =
  417.                 javascriptExpressionToGetFirstElementFromXPath("//*[@ng-controller='ReportController']//*[contains(@class, 'scroll-wrap')]/parent::*") +
  418.                 ".scroll(0, 230)";
  419.    
  420.             var scrollResult = browserPage.EvaluateExpressionAsync(scrollExpression).Result;
  421.  
  422.             if(!AttemptClickAndLogError(() =>
  423.                 WaitForReference(() => browserPage.XPathAsync(reportJumpToAttackerVillageXPath).Result.FirstOrDefault(), 400)))
  424.             {
  425.                 Host.Log("I did not find the button to jump to attackers village. I skip this report and continue with the next one.");
  426.                 goto navigateToNextReport;
  427.             }
  428.  
  429.             Host.Delay(1777);
  430.  
  431.             if(!AttemptClickAndLogError(() => WaitForReference(() =>
  432.                 browserPage.XPathAsync(mapVillageContextMenuItemActivateXPath).Result.FirstOrDefault(), 3333)))
  433.             {
  434.                 Host.Log("I did not find the context menu button to activate the village. I skip this report.");
  435.                 goto navigateToNextReport;
  436.             }
  437.  
  438.             Host.Log("I clicked on the menu button to activate the village. Now I wait for the game to show " + battleReportDetails.AttackerVillageLocation + " as the active village.");
  439.  
  440.             Host.Delay(555);
  441.  
  442.             for(var i = 0; i < 7 && !currentActiveVillageLocation.Equals(battleReportDetails.AttackerVillageLocation); ++i)
  443.             {
  444.                 Host.Delay(111 + i * 222);
  445.                 currentActiveVillageLocation = ReadCurrentActiveVillageLocation();
  446.             }
  447.  
  448.             if(!currentActiveVillageLocation.Equals(battleReportDetails.AttackerVillageLocation))
  449.             {
  450.                 /* Several users reported that switching the village failed when the chat window was open in the game:
  451.                 + https://forum.botengine.org/t/farm-manager-tribal-wars-2-farmbot/1406/108?u=viir
  452.                 + https://forum.botengine.org/t/farm-manager-tribal-wars-2-farmbot/1406/115?u=viir
  453.                 */
  454.                 Host.Log("I failed to switch to the originally attacking village. I skip this report. " + switchVillageChatWindowGuide);
  455.                 goto navigateToNextReport;
  456.             }
  457.         }
  458.  
  459.         Host.Log("Try to find and click the button to attack again.");
  460.  
  461.         Host.Delay(555);
  462.    
  463.         if(!AttemptClickAndLogError(() => WaitForReference(() =>
  464.             browserPage.XPathAsync(inReportAttackAgainButtonXPath).Result.FirstOrDefault(), 1000)))
  465.         {
  466.             Host.Log("I did not find the context menu button to attack again. I skip this report.");
  467.             goto navigateToNextReport;
  468.         }
  469.  
  470.         Host.Log("Try to find and click the button to send the attack.");
  471.  
  472.         Host.Delay(555);
  473.  
  474.         if(!AttemptClickAndLogError(() => WaitForReference(() =>
  475.             browserPage.XPathAsync(inSendArmyFormSendAttackButtonXPath).Result.FirstOrDefault(), 3000)))
  476.         {
  477.             Host.Log("Did not find button to send attack. I skip this report.");
  478.             goto navigateToNextReport;
  479.         }
  480.  
  481.         cycleReport.ReportsForWhichAttackHasBeenSentAgain.Add(battleReportDetails);
  482.         sessionReport.ReportsForWhichAttackHasBeenSentAgain.Add(battleReportDetails);
  483.  
  484.         Host.Log("session_number_of_attacks_sent: " + sessionReport.ReportsForWhichAttackHasBeenSentAgain.Count);
  485.  
  486. navigateToNextReport:
  487.  
  488.         lastReportCaption = currentReportCaption ?? lastReportCaption;
  489.  
  490.         Host.Delay(1333); // Wait some time to allow browser to process the request following the click.
  491.    
  492.         var navigateToNextReportButton = WaitForReference(() =>
  493.             browserPage.XPathAsync(inReportViewNavigateToNextReportButtonXPath).Result.FirstOrDefault(), 1000);
  494.    
  495.         if(navigateToNextReportButton == null)
  496.         {
  497.             Host.Log("Did not find the button to navigate to the next report. I stop this cycle.");
  498.             break;
  499.         }
  500.    
  501.         var navigateToNextReportButtonClassName = navigateToNextReportButton?.GetPropertyAsync("className").Result;
  502.    
  503.         var navigateToNextReportButtonClassNameJsonValue = navigateToNextReportButtonClassName?.JsonValueAsync().Result;
  504.    
  505.         var buttonIsOrange = navigateToNextReportButtonClassNameJsonValue?.ToString()?.Contains("btn-orange");
  506.    
  507.         if(buttonIsOrange != true)
  508.         {
  509.             Host.Log("The button to navigate to the next report seems to be disabled.");
  510.             break;
  511.         }
  512.    
  513.         Host.Log("I click on the button to navigate to the next report.");
  514.         Host.Delay(333);
  515.         AttemptClickAndLogError(() => navigateToNextReportButton);
  516.    
  517.         Host.Delay(1333);
  518.     }
  519.  
  520.     logCycleStats();
  521.  
  522.     if(cycleCount <= cycleIndex + 1)
  523.         break;
  524.  
  525.     var breakDuration = RandomIntFromMinimumAndRandomAddition(breakBetweenCycleDurationMinSeconds, breakBetweenCycleDurationRandomAdditionMaxSeconds);
  526.  
  527.     Host.Log("I am done with this cycle. I will wait for " + (breakDuration / 60) + " minutes before continuing....");
  528.     Host.Delay(breakDuration * 1000);
  529. }
  530.  
  531. logSessionStats();
  532. Host.Log("I am done with this session. I wait for one minute before terminating the script....");
  533. Host.Delay(1000 * 60);
  534.  
  535. string ReportCaptionTextInSingleLine(string reportCaptionText) =>
  536.     reportCaptionText?.Trim()?.Replace("\n", " - ");
  537.  
  538. struct BattleReportDetails
  539. {
  540.     public string Caption;
  541.  
  542.     public VillageLocation AttackerVillageLocation;
  543.  
  544.     public VillageLocation DefenderVillageLocation;
  545.  
  546.     public bool DefenderIsBarbarian;
  547.  
  548.     override public string ToString() =>
  549.         "{ " + string.Join(" , ", new []{
  550.             @"""AttackerVillageLocation"": """ + AttackerVillageLocation.ToString() + @"""",
  551.             @"""DefenderVillageLocation"": """ + DefenderVillageLocation.ToString() + @"""",
  552.             @"""DefenderIsBarbarian"": """ + DefenderIsBarbarian + @"""",
  553.         }
  554.         ) + " }";
  555. }
  556.  
  557. struct VillageLocation
  558. {
  559.     public int X, Y;
  560.  
  561.     override public string ToString() =>
  562.         X.ToString() + "|" + Y.ToString();
  563. }
  564.  
  565. class ReportListItem
  566. {
  567.     public string timeText;
  568.  
  569.     public ElementHandle htmlElement;
  570. }
  571.  
  572. VillageLocation? ReadCurrentActiveVillageLocation()
  573. {
  574.     var openVillageInfoButton =
  575.         WaitForReference(() => browserPage.XPathAsync(openVillageInfoButtonXPath).Result.FirstOrDefault(), 100);
  576.  
  577.     var parseResult = SingleVillageLocationContainedInText(GetHtmlElementInnerText(openVillageInfoButton));
  578.  
  579.     if(parseResult.IsFail)
  580.         return null;
  581.  
  582.     return parseResult.Result;
  583. }
  584.  
  585. ErrorStringOrGenericResult<BattleReportDetails> ParseBattleReportDetails(ElementHandle battleReportDetailsHtmlElement)
  586. {
  587.     try
  588.     {
  589.         var attackerDetails =
  590.             battleReportDetailsHtmlElement.XPathAsync(".//table[(.//*[contains(@class, 'attack')]) and (.//*[contains(@class, 'report-village')])]").Result?.SingleOrDefault();
  591.  
  592.         var defenderDetails =
  593.             battleReportDetailsHtmlElement.XPathAsync(".//table[(.//*[contains(@class, 'defense')]) and (.//*[contains(@class, 'report-village')])]").Result?.SingleOrDefault();
  594.  
  595.         if(attackerDetails == null)
  596.             return Error<BattleReportDetails>("Did not find attacker details.");
  597.  
  598.         if(defenderDetails == null)
  599.             return Error<BattleReportDetails>("Did not find defender details.");
  600.  
  601.         var attackerVillageLocation = VillageLocationFromReportParty(attackerDetails);
  602.  
  603.         if(attackerVillageLocation.IsFail)
  604.             return Error<BattleReportDetails>("Failed to parse attacker village location: " + attackerVillageLocation.Error);
  605.  
  606.         var defenderCharacterInfoButton =
  607.             defenderDetails.XPathAsync(".//*[contains(@class, 'character-info') and contains(@class, 'btn')]").Result?.FirstOrDefault();
  608.  
  609.         if(defenderCharacterInfoButton == null)
  610.             return Error<BattleReportDetails>("Did not find defender character info button.");
  611.  
  612.         var defenderCharacterInfoButtonClass =
  613.             defenderCharacterInfoButton?.GetPropertyAsync("className").Result?.JsonValueAsync().Result?.ToString();
  614.  
  615.         if(defenderCharacterInfoButtonClass == null)
  616.             return Error<BattleReportDetails>("Failed to read class from defender character info button.");
  617.  
  618.         var defenderIsBarbarian = defenderCharacterInfoButtonClass?.Contains("btn-grey") ?? false;
  619.  
  620.         var defenderVillageLocation = VillageLocationFromReportParty(defenderDetails);
  621.  
  622.         if(defenderVillageLocation.IsFail)
  623.             return Error<BattleReportDetails>("Failed to parse defender village location: " + defenderVillageLocation.Error);
  624.  
  625.         return Success(new BattleReportDetails
  626.         {
  627.             AttackerVillageLocation = attackerVillageLocation.Result,
  628.             DefenderVillageLocation = defenderVillageLocation.Result,
  629.             DefenderIsBarbarian = defenderIsBarbarian,
  630.         });
  631.     }
  632.     catch(Exception e)
  633.     {
  634.         return Error<BattleReportDetails>("Failed with Exception: " + e.ToString());
  635.     }
  636. }
  637.  
  638. ErrorStringOrGenericResult<VillageLocation> VillageLocationFromReportParty(ElementHandle reportPartyHtmlElement)
  639. {
  640.     var village = reportPartyHtmlElement.XPathAsync(".//*[contains(@class,'report-village')]").Result.FirstOrDefault();
  641.  
  642.     if(null == village)
  643.         return Error<VillageLocation>("Did not find village element.");
  644.  
  645.     return SingleVillageLocationContainedInText(GetHtmlElementInnerText(village)?.Trim());
  646. }
  647.  
  648. ErrorStringOrGenericResult<VillageLocation> SingleVillageLocationContainedInText(string text)
  649. {
  650.     var locationMatches = System.Text.RegularExpressions.Regex.Matches(text ?? "", @"\(\s*(\d+)\s*\|s*(\d+)s*\)");
  651.  
  652.     if(locationMatches.Count != 1)
  653.         return Error<VillageLocation>("Number of matches is " + locationMatches.Count);
  654.  
  655.     var locationMatch = locationMatches.OfType<System.Text.RegularExpressions.Match>().Single();
  656.  
  657.     if(!locationMatch.Success)
  658.         return Error<VillageLocation>("No match of location in text: " + text);
  659.  
  660.     int locationX, locationY;
  661.  
  662.     if(!int.TryParse(locationMatch.Groups[1].Value, out locationX))
  663.         return Error<VillageLocation>("Failed to parse locationX.");
  664.  
  665.     if(!int.TryParse(locationMatch.Groups[2].Value, out locationY))
  666.         return Error<VillageLocation>("Failed to parse locationY.");
  667.  
  668.     return Success(new VillageLocation
  669.     {
  670.         X = locationX,
  671.         Y = locationY,
  672.     });
  673. }
  674.  
  675. bool ReloadAndEnterReportAtTime(string reportTimeToContinueAt, int maxNumberOfAttempts = 5)
  676. {
  677.     Host.Log("I begin the process to restart the report UI and continue at report with time '" + reportTimeToContinueAt + "'.");
  678.  
  679.     for(var attemptNumber = 1; attemptNumber <= maxNumberOfAttempts; ++attemptNumber)
  680.     {
  681.         Host.Log("Attempt number " + attemptNumber + ". I close the report list.");
  682.  
  683.         for(var i = 0; i < 3; ++i)
  684.         {
  685.             Host.Delay(333);
  686.  
  687.             var closeWindowButton = WaitForReference(() =>
  688.                 browserPage.XPathAsync("//*[@ng-click='closeWindow()']").Result.FirstOrDefault(), 111);
  689.  
  690.             closeWindowButton?.ClickAsync().Wait();
  691.         }
  692.  
  693.         if(!AttemptClickAndLogError(() => WaitForOpenReportListButton(1000)))
  694.         {
  695.             Host.Log("I did not find the button to open the report list.");
  696.             return false;
  697.         }
  698.  
  699.         var pageSize = 100;
  700.  
  701.         Host.Log("I clicked the button to open the report list. Next, I set the page size to " + pageSize + ".");
  702.  
  703.         Host.Delay(3333);
  704.  
  705.         var setPageSizeButton = WaitForReference(() =>
  706.             browserPage.XPathAsync("//*[@ng-click='setLimit(_limit)' and contains(., '" + pageSize + "')]").Result.FirstOrDefault(), 111);
  707.  
  708.         setPageSizeButton?.ClickAsync().Wait();
  709.  
  710.         Host.Delay(3333);
  711.  
  712.         Host.Log("I parse the reports in there to find the best to continue with.");
  713.  
  714.         var reports = WaitForReference(() => browserPage.XPathAsync(reportListItemXPath).Result, 1000);
  715.  
  716.         if(reports == null)
  717.         {
  718.             Host.Log("Did not find the report items.");
  719.             return false;
  720.         }
  721.  
  722.         var parsedReportItems = reports.Select(ParseReportListItem).ToList();
  723.  
  724.         Host.Log("I parsed " + parsedReportItems?.Count + " report items.");
  725.  
  726.         if(!(0 < parsedReportItems?.Count))
  727.         {
  728.             //  Empty report list could again be an effect of the bug in Tribal Wars 2 web app as described at https://forum.botengine.org/t/bug-in-tribal-wars-2-web-app-broken-report-display/1457, which implies that closing and opening the report list again could help.
  729.             continue;
  730.         }
  731.  
  732.         var matchingReportItem =
  733.             0 < reportTimeToContinueAt?.Length ?
  734.             parsedReportItems.FirstOrDefault(reportItem => reportItem.timeText?.Contains(reportTimeToContinueAt) ?? false)
  735.             : null;
  736.  
  737.         var reportItemToContinueWith = matchingReportItem;
  738.  
  739.         if(matchingReportItem == null)
  740.         {
  741.             Host.Log("I did not find a report item with time of '" + reportTimeToContinueAt + "'. I continue with the last report in the list.");
  742.             reportItemToContinueWith = parsedReportItems.LastOrDefault();
  743.         }
  744.  
  745.         var reportItemToContinueWithText = GetHtmlElementInnerText(reportItemToContinueWith.htmlElement);
  746.  
  747.         Host.Log("Report item to continue with: '" + ReportCaptionTextInSingleLine(reportItemToContinueWithText) + "'");
  748.  
  749.         var parsedReportItemsUpToReportToContinueWith =
  750.             parsedReportItems.Take(parsedReportItems.IndexOf(reportItemToContinueWith) + 1)
  751.             .ToList();
  752.  
  753.         return OpenReportItemInReportList(parsedReportItemsUpToReportToContinueWith);
  754.     }
  755.  
  756.     return false;
  757. }
  758.  
  759. string GetHtmlElementInnerText(ElementHandle htmlElement) =>
  760.     htmlElement?.GetPropertyAsync("innerText")?.Result?.JsonValueAsync()?.Result?.ToString();
  761.  
  762. bool AttemptClickAndLogError(Func<ElementHandle> getHtmlElement)
  763. {
  764.     //  https://github.com/GoogleChrome/puppeteer/issues/1769
  765.  
  766.     for(int attemptIndex = 0; attemptIndex < 2; ++attemptIndex)
  767.     {
  768.         try
  769.         {
  770.             var htmlElement = getHtmlElement();
  771.  
  772.             htmlElement.HoverAsync().Wait();
  773.  
  774.             Host.Delay(333);
  775.  
  776.             htmlElement.ClickAsync().Wait();
  777.  
  778.             return true;
  779.         }
  780.         catch(Exception e)
  781.         {
  782.             if(0 < attemptIndex)
  783.                 Host.Log("Failed to hover or click on attempt " + attemptIndex + " with exception:\n" + e);
  784.         }
  785.     }
  786.  
  787.     return false;
  788. }
  789.  
  790. ReportListItem ParseReportListItem(ElementHandle htmlElement)
  791. {
  792.     var reportDateElement = htmlElement.XPathAsync(".//*[contains(@class, 'report-date')]").Result.FirstOrDefault();
  793.  
  794.     var timeText = GetHtmlElementInnerText(reportDateElement);
  795.  
  796.     return new ReportListItem
  797.     {
  798.         timeText = timeText,
  799.         htmlElement = htmlElement,
  800.     };
  801. }
  802.  
  803. bool OpenReportItemInReportList(IReadOnlyList<ReportListItem> reportItemsUpToReportToOpen)
  804. {
  805.     var reportToOpen = reportItemsUpToReportToOpen.LastOrDefault();
  806.  
  807.     var reportItemDisplayText =
  808.         ReportCaptionTextInSingleLine(GetHtmlElementInnerText(reportToOpen.htmlElement));
  809.  
  810.     Host.Log("I start the process to open the report '" + reportItemDisplayText + "' which is at position " + (reportItemsUpToReportToOpen.Count - 1) + " in the report list.");
  811.  
  812.     /*
  813.     2018-08-30
  814.     Playing around with `scrollIntoView` in the reports list, I observed that it did not work for items which are further from the current viewport.
  815.     */
  816.  
  817.     var scrollStepsReportItems =
  818.         reportItemsUpToReportToOpen
  819.         .Where((report, i) => i % 3 == 0 || report == reportToOpen)
  820.         .ToList();
  821.  
  822.     Host.Log("I start to scroll down....");
  823.  
  824.     var scrollOperationsResults =
  825.         scrollStepsReportItems
  826.         .Select(reportItem =>
  827.         {
  828.             Host.Delay(111);
  829.             var scrollResult = ScrollHtmlElementIntoViewEnd(browserPage, reportItem.htmlElement).Result;
  830.             Host.Delay(111);
  831.             return scrollResult;
  832.         }).ToList();
  833.  
  834.     Host.Log("scrollOperationsResults: " + String.Join(";", scrollOperationsResults));
  835.  
  836.     if(AttemptClickAndLogError(() => reportToOpen.htmlElement))
  837.     {
  838.         Host.Log("Clicked on report '" + reportItemDisplayText + "'.");
  839.         return true;
  840.     }
  841.     else
  842.     {
  843.         Host.Log("Failed to click on report '" + reportItemDisplayText + "'.");
  844.         return false;
  845.     }
  846. }
  847.  
  848. ElementHandle WaitForOpenReportListButton(int timeoutMilli) =>
  849.     WaitForReference(() =>
  850.     {
  851.         try
  852.         {
  853.             /*
  854.             2018-08-11 Sporadically observed exception here:
  855.             An exception of type 'System.AggregateException' occurred in System.Private.CoreLib.dll but was not handled in user code: 'One or more errors occurred.'
  856.             Inner exceptions found, see $exception in variables window for more details.
  857.             Innermost exception      PuppeteerSharp.MessageException : Protocol error (Runtime.callFunctionOn): Cannot find context with specified id
  858.             */
  859.             return browserPage.XPathAsync(openReportListButtonXPath).Result.FirstOrDefault();
  860.         }
  861.         catch (AggregateException) // consider adding a filter here.
  862.         {
  863.             return null;
  864.         }
  865.     }, timeoutMilli);
  866.  
  867. System.Threading.Tasks.Task<string> ScrollHtmlElementIntoViewEnd(Page browserPage, ElementHandle htmlElement)
  868. {
  869.     return
  870.         browserPage.EvaluateFunctionAsync<string>(
  871.             @"(htmlElement) => htmlElement.scrollIntoView({behavior: ""instant"", block: ""end"", inline: ""nearest""})",
  872.             htmlElement);
  873. }
  874.  
  875. static string javascriptExpressionToGetFirstElementFromXPath(string xpath) =>
  876.     "document.evaluate(\"" + xpath + "\", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue";
  877.  
  878. T WaitForReference<T>(Func<T> attemptGetReference, int timeoutMilli)
  879.     where T : class
  880. {
  881.     var stopwatch = Stopwatch.StartNew();
  882.  
  883.     while (true)
  884.     {
  885.         var reference = attemptGetReference();
  886.  
  887.         if (reference != null)
  888.             return reference;
  889.  
  890.         if (timeoutMilli < stopwatch.ElapsedMilliseconds)
  891.             return null;
  892.  
  893.         Host.Delay(333);
  894.     }
  895. }
  896.  
  897. ErrorStringOrGenericResult<T> Error<T>(string error) =>
  898.     new ErrorStringOrGenericResult<T>{Error = error};
  899.  
  900. ErrorStringOrGenericResult<T> Success<T>(T result) =>
  901.     new ErrorStringOrGenericResult<T>{Result = result};
  902.  
  903. struct ErrorStringOrGenericResult<T>
  904. {
  905.     public bool IsSuccess => Error == null;
  906.     public bool IsFail => Error != null;
  907.  
  908.     public string Error;
  909.     public T Result;
  910. }
  911.  
  912. class CycleReport
  913. {
  914.     public Int64 BeginTime;
  915.     readonly public List<BattleReportDetails> ReportsSeen = new List<BattleReportDetails>();
  916.     readonly public List<BattleReportDetails> ReportsForWhichAttackHasBeenSentAgain = new List<BattleReportDetails>();
  917.  
  918.     public IEnumerable<VillageLocation> AttacksOriginVillages =>
  919.         ReportsForWhichAttackHasBeenSentAgain.Select(report => report.AttackerVillageLocation).Distinct();
  920.  
  921.     public string StatisticsText =>
  922.         "I have looked at " + ReportsSeen.Count +
  923.         " reports and sent " + ReportsForWhichAttackHasBeenSentAgain.Count +
  924.         " attacks from " + AttacksOriginVillages.Count() + " villages.";
  925. }
  926.  
  927. int RandomIntFromMinimumAndRandomAddition(int min, int additionMax)
  928. {
  929.     var addition =
  930.         additionMax < 1 ? 0 : (new Random((int)Host.GetTimeContinuousMilli()).Next() % additionMax);
  931.  
  932.     return min + addition;
  933. }
RAW Paste Data
We use cookies for various purposes including analytics. By continuing to use Pastebin, you agree to our use of cookies as described in the Cookies Policy. OK, I Understand
 
Top