commit 2ee9eb1b655e1186d8a10adb2eb8a073bdc2cc79 Author: Hymmel Date: Fri Oct 10 15:32:29 2025 +0200 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c14674c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +/project/target +/project/project \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b872a00 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +# Stage 1: Build the application using SBT +FROM sbtscala/sbt:11.0.11_1.5.5_2.13.6 as builder + +WORKDIR /app + +# Copy project definition files and resolve dependencies first for better layer caching +COPY project/ ./project/ +COPY build.sbt . + +# This step will download all the dependencies +RUN sbt update + +# Copy the rest of the source code +COPY . . + +# Package the application into a WAR file +RUN sbt package + +# Stage 2: Create the runtime image with Tomcat +FROM tomcat:9.0-jdk11-openjdk-slim + +# Remove the default webapps +RUN rm -rf /usr/local/tomcat/webapps/* + +# Copy the WAR file from the builder stage to Tomcat's webapps directory +# The WAR file will be automatically deployed by Tomcat on startup +COPY --from=builder /app/target/scala-2.11/coc-base-analyser_2.11-0.1.war /usr/local/tomcat/webapps/ROOT.war + +EXPOSE 8080 \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4a875ed --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Daniel Holmes + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2a67bc3 --- /dev/null +++ b/README.md @@ -0,0 +1,82 @@ +# Clash of Clans Base Analyser + +Analyses base layouts against sets of war base rules. It runs this analysis in bulk and provides tabulated results for +the current war bases for each member in a clan. Additional to this you can click through to an individual's base to +see a more in depth analysis. + +Analysis is only allowed for pre-configured clans in the app which are assigned an alias for convenience. + + +## Dependencies + + - SBT + - JDK 8+ + - Scala 2.11 + +To find available SBT dependency updates run `sbt dependencyUpdates` + + +## Tests + + - All: `sbt test` + - Individual: `sbt "test-only org.danielholmes.coc.baseanalyser.baseparser.VillageJsonParserSpec"` + - Individual continuous: `sbt ~"test-only org.danielholmes.coc.baseanalyser.baseparser.VillageJsonParserSpec"` + + +## Command Line Utils + +`sbt 'run-main org.danielholmes.coc.baseanalyser.PrintVillage alpha "I AM SPARTA!!1!"'` +`sbt 'run-main org.danielholmes.coc.baseanalyser.PrintAttackPlacements alpha "I AM SPARTA!!1!"'` +`sbt 'run-main org.danielholmes.coc.baseanalyser.ProfileAnalysis alpha "I AM SPARTA!!1!"'` + + +## Running dev version of site (automatically reloads on changes) + +`sbt ~tomcat:start` + +Available at [http://localhost:8080](http://localhost:8080) + +Accessing a clan e.g. [http://localhost:8080/clans/alpha](http://localhost:8080/clans/alpha) + + +## Production Build + +`sbt package` + +then deploy the war as required: + +`target/scala-2.11/coc-base-analyser_2.11-0.1.war` + + +## Game Connection + +The app requires a connection to the Supercell servers to query the village json and other clan members. Note that this +isn't referring to the official [Clash of Clans API](https://developer.clashofclans.com/), but a direct connection to +the game servers how the game does. Once upon a time this project used a product called "Clan Seeker" which has since +been discontinued. It's trivial however to write an agent for the app if there's another such service out there: + + 1. Write a new game connection agent that implements the + [GameConnection Trait](src/main/scala/org/danielholmes/coc/baseanalyser/gameconnection/GameConnection.scala) + 2. Wire in the game connection in the + [Services Trait](src/main/scala/org/danielholmes/coc/baseanalyser/Services.scala) + +At the moment there's a hardcoded stub/testing GameConnection with dummy data + + +## Rules + +The project is very much in a WIP state. Town Hall 8 is pretty fleshed out but TH9 and above don't have many rules. +Also these were modelled off of attack and base building meta from around February 2016. Some newer buildings are also +not present such as the Bomb Tower. + +Rules are pretty quick to build due to the building blocks and infrastructure of a modelled base being in place. + + +## Screenshots + +![Clan Homepage](docs/images/home-1.png?raw=true "Clan Home") +![Base Analysis 1](docs/images/base-1.png?raw=true "Base Analysis 1") +![Base Analysis 2](docs/images/base-2.png?raw=true "Base Analysis 2") +![Base Analysis 3](docs/images/base-3.png?raw=true "Base Analysis 3") +![Base Analysis 4](docs/images/base-4.png?raw=true "Base Analysis 4") +![Bulk Analysis](docs/images/bulk-1.png?raw=true "Bulk Analysis") \ No newline at end of file diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..0dab6c2 --- /dev/null +++ b/TODO.md @@ -0,0 +1,100 @@ +# TODO + +## General TODO + - Obstacles anywhere but outer 3 border tiles rule + - just use generic obstacle render atm + + - Once api up again + - store skeleton trap mode once can see and verify on live data + - store xbow mode once can see and verify on live data + - double check sweeper angles being rendered same as in game once clan seeker up + + - own connection + - purchase from alex + - set up on own small EB app + + - trap access (if leadership go for it) - new, dedicated credentials of own + - Privileged vs unprivileged analysis - warnings if not using traps + - asterixes against rules which traps have an effect + + - password protection (in the wrong hands opposition would see our trap locations) + + - analysis performance, currently too slow + - MapCoordinate trait with underlying FloatMapCoordinate, TileCoordinate, Tile - encourage integer math where possible and prevent widening + - Views of tile sets (e.g. TileBlock returned from matrix + - Redo ranges - should include underlying cached set of tiles + tilecoordinates contained + + - Some TH9 rules + - Queen Charge into wall breakable compartment shouldnt get to 2 air defs + - It should require either a jump spell or 2 wall breaker groups in order to access the queen. + + - on equidistant hog lure, mark it as such or ignore it all together (maybe a pink line showing equidistant, non luring alternative path + + - TH10s without infernos should go under TH 9.5 rules? + + - separate hole in the base rule - just to highlight really bad issues (due to ignoring some others) + + - expand possible trap locations for channel bases. e.g. see spandan and vicious 2.0 an sparta home base + + - Begin on DGB: + - class PossibleDoubleGiantBomb(anchors: (Either[Defense, PossibleTrapLocation], Either[Defense, PossibleTrapLocation]), gbs: (PossibleTrapLocation, PossibleTrapLocation)) + + - BK Trigger rule further tweaks. should show red for all non-compartment tiles floodfilled from triggered + + - clarify AQ range - see iphoto screenshot of greg raid. possibly shown on ppetes war base + + - TH11 rendering - new levels and warden + eagle + + - sbt deploy task + - 3d render + - split map display 2d apart. New inner stage container - drawLine(tile1, tile2) which transforms to 2d or 3d view + - separate rule groups for farm vs arranged + - pass, warning, fail levels (e.g. for minion anchors) + - integration tests + + +## TH8 TODO Rules + - air defs should be a minimum distance apart + - SAMs not next to each other - one kills a dragon + - loon pathing + - must be >= 3 defenses to go through to path to air defs + - OR must be > certain distance + - should also consider air trap placement + - should also consider air sweeper placement + - minimum 3 DGB possible spots (including diagonal) + - minion anchors (warning only, no hard fail, once that functionality is built) + - wb t junction warning + + +## TH8 TODO Rules once traps available + - spring trap locations (resting on defenses) + - skele traps should be on ground and not triggerable during cc lure + - skele traps + air traps not within dgb positions (gives info for cleanup if first hit was with air) + - 3 viable DGB spots (more difficult) + - farm wars - teslas in diff compartment for gowipe + - trash buildings in front of all outer ring defenses + + +## TH9 Rules + - EQ cant connect >2 GB/DGB positions + AQ + - Jump doesnt connect too many AQ, GB + - queen needs to be protected from “suicide dragons” + Specifically, an air sweeper pointed to protect the queen, or (more commonly) a black mine between the queen and the likely dragon entry point + - black bombs within range of queen or air def - to get hounds or suicide drags. red bombs out of range of air defs + - The defenses around your DGB should be more than 4 tiles from an exterior wall + Ensures the defenses aren’t eliminated using a queen walk + + +## TH10 Rules + - cant get 2 infernos with one freeze + + +## Expansion ideas + - Hog pathing analysis - start paths from each tile and be able to select/see individual paths from defense to defense + to show DGB issues + - multiple goals/rulesets: + - farming (protected loot, give away easy shield - one star, but no value for more than) + - war - depending on clan and level, this might be to prevent 1 star, prevent 2 star, or just prevent 3 star + - trophy? + - provide weaknesses for attack types. e.g. drags doesnt consider DGB locations, hogs dont consider air def high hp. + - queen walk pathing from drop point diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..54a45b0 --- /dev/null +++ b/build.sbt @@ -0,0 +1,68 @@ +name := "coc-base-analyser" + +version := "0.1" + +scalaVersion := "2.11.8" + +lazy val root = (project in file(".")).enablePlugins(SbtWeb).enablePlugins(TomcatPlugin) + +libraryDependencies ++= { + val akkaV = "2.4.4" + val sprayV = "1.3.3" + Seq( + "org.apache.commons" % "commons-math3" % "3.6.1", + "io.spray" %% "spray-json" % "1.3.2", + + "io.spray" %% "spray-can" % sprayV, + "io.spray" %% "spray-servlet" % sprayV, + "io.spray" %% "spray-routing" % sprayV, + "io.spray" %% "spray-client" % sprayV, + "com.typesafe.akka" %% "akka-actor" % akkaV, + + "org.scalactic" %% "scalactic" % "3.0.0-SNAP13", + + "javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided", + + "com.softwaremill.macwire" %% "macros" % "2.2.2" % "provided", + "com.softwaremill.macwire" %% "util" % "2.2.2", + "com.softwaremill.macwire" %% "proxy" % "2.2.2", + + "com.github.spullara.mustache.java" % "scala-extensions-2.11" % "0.9.1", + "com.google.guava" % "guava" % "19.0", + + "org.scalatest" % "scalatest_2.11" % "2.2.6" % "test" + ) +} + +lazy val testScalastyle = taskKey[Unit]("testScalastyle") +testScalastyle := org.scalastyle.sbt.ScalastylePlugin.scalastyle.in(Test).toTask("").value +(test in Test) <<= (test in Test) dependsOn testScalastyle + +lazy val compileScalastyle = taskKey[Unit]("compileScalastyle") +compileScalastyle := org.scalastyle.sbt.ScalastylePlugin.scalastyle.in(Compile).toTask("").value +(compile in Compile) <<= (compile in Compile) dependsOn compileScalastyle + +// Runs with tomcat:start, only want for war task +//(packageBin in Compile) <<= (packageBin in Compile) dependsOn (test in Test) + +/* +webappPostProcess := { + webappDir: File => Unit; + + def listFiles(level: Int)(f: File): Unit = { + val indent = ((1 until level) map { _ => " " }).mkString + if (f.isDirectory) { + streams.value.log.info(indent + f.getName + "/") + f.listFiles foreach { listFiles(level + 1) } + } else streams.value.log.info(indent + f.getName) + } + listFiles(1)(webappDir) +} + +pipelineStages in Assets := Seq(concat) +// Doesnt work but need something like this so can copy the staged files in the PostProcess above +packageBin <<= packageBin.dependsOn(WebKeys.stage) + +Concat.groups := Seq( + "js/script-group.js" -> group(Seq("js/script1.js", "js/script2.js")) +)*/ diff --git a/docs/images/base-1.png b/docs/images/base-1.png new file mode 100644 index 0000000..f34795f Binary files /dev/null and b/docs/images/base-1.png differ diff --git a/docs/images/base-2.png b/docs/images/base-2.png new file mode 100644 index 0000000..5be0f3f Binary files /dev/null and b/docs/images/base-2.png differ diff --git a/docs/images/base-3.png b/docs/images/base-3.png new file mode 100644 index 0000000..442afcb Binary files /dev/null and b/docs/images/base-3.png differ diff --git a/docs/images/base-4.png b/docs/images/base-4.png new file mode 100644 index 0000000..d809a0b Binary files /dev/null and b/docs/images/base-4.png differ diff --git a/docs/images/bulk-1.png b/docs/images/bulk-1.png new file mode 100644 index 0000000..97c9d64 Binary files /dev/null and b/docs/images/bulk-1.png differ diff --git a/docs/images/home-1.png b/docs/images/home-1.png new file mode 100644 index 0000000..698fd55 Binary files /dev/null and b/docs/images/home-1.png differ diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..43b8278 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=0.13.11 diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..1cd9e59 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,5 @@ +resolvers += Resolver.sonatypeRepo("releases") + +addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "2.1.0") +addSbtPlugin("net.ground5hark.sbt" % "sbt-concat" % "0.1.8") +addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "0.8.0") \ No newline at end of file diff --git a/project/sbt-updates.sbt b/project/sbt-updates.sbt new file mode 100644 index 0000000..b26bdc4 --- /dev/null +++ b/project/sbt-updates.sbt @@ -0,0 +1 @@ +addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.10") \ No newline at end of file diff --git a/scalastyle-config.xml b/scalastyle-config.xml new file mode 100644 index 0000000..9a8a9f2 --- /dev/null +++ b/scalastyle-config.xml @@ -0,0 +1,107 @@ + + Scalastyle standard configuration + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf new file mode 100644 index 0000000..c4373ab --- /dev/null +++ b/src/main/resources/application.conf @@ -0,0 +1,7 @@ +akka { + log-dead-letters-during-shutdown = off +} + +spray.servlet { + boot-class = "org.danielholmes.coc.baseanalyser.web.WebAppServlet" +} \ No newline at end of file diff --git a/src/main/resources/examples/th5-sample-1.json b/src/main/resources/examples/th5-sample-1.json new file mode 100644 index 0000000..399699f --- /dev/null +++ b/src/main/resources/examples/th5-sample-1.json @@ -0,0 +1 @@ +{"wave_num":6,"exp_ver":1,"active_layout":0,"layout_state":[0,0,0,0,0,0],"buildings":[{"data":1000001,"id":500000000,"lvl":5,"x":30,"y":30,"l1x":25,"l1y":19,"l2x":13,"l2y":26,"l3x":29,"l3y":20,"l4x":29,"l4y":20,"l5x":13,"l5y":26},{"data":1000004,"id":500000001,"lvl":11,"x":22,"y":33,"res_time":87169,"l1x":11,"l1y":31,"l2x":24,"l2y":40,"l3x":41,"l3y":33,"l4x":41,"l4y":33,"l5x":24,"l5y":40},{"data":1000000,"id":500000002,"lvl":5,"x":35,"y":15,"units":[[4000008,2]],"l1x":29,"l1y":39,"l2x":38,"l2y":14,"l3x":40,"l3y":19,"l4x":40,"l4y":19,"l5x":38,"l5y":14},{"data":1000015,"id":500000003,"lvl":0,"x":41,"y":41,"l1x":14,"l1y":39,"l2x":9,"l2y":33,"l3x":36,"l3y":12,"l4x":36,"l4y":12,"l5x":41,"l5y":34},{"data":1000014,"id":500000004,"lvl":3,"x":27,"y":27,"l1x":25,"l1y":24,"l2x":23,"l2y":23,"l3x":25,"l3y":25,"l4x":25,"l4y":25,"l5x":23,"l5y":23},{"data":1000008,"id":500000005,"lvl":9,"x":37,"y":37,"l1x":36,"l1y":25,"l2x":33,"l2y":37,"l3x":34,"l3y":39,"l4x":34,"l4y":39,"l5x":33,"l5y":37},{"data":1000015,"id":500000006,"lvl":0,"x":38,"y":41,"l1x":16,"l1y":39,"l2x":37,"l2y":12,"l3x":32,"l3y":9,"l4x":32,"l4y":9,"l5x":37,"l5y":12},{"data":1000002,"id":500000007,"lvl":11,"x":37,"y":22,"res_time":87173,"l1x":21,"l1y":9,"l2x":41,"l2y":24,"l3x":8,"l3y":20,"l4x":8,"l4y":20,"l5x":41,"l5y":24},{"data":1000003,"id":500000008,"lvl":10,"x":39,"y":30,"l1x":16,"l1y":20,"l2x":26,"l2y":15,"l3x":29,"l3y":34,"l4x":29,"l4y":33,"l5x":26,"l5y":15},{"data":1000005,"id":500000009,"lvl":10,"x":30,"y":39,"l1x":24,"l1y":33,"l2x":27,"l2y":33,"l3x":16,"l3y":32,"l4x":16,"l4y":32,"l5x":27,"l5y":33},{"data":1000006,"id":500000010,"lvl":9,"x":8,"y":27,"unit_prod":{"unit_type":0},"l1x":40,"l1y":27,"l2x":21,"l2y":39,"l3x":30,"l3y":39,"l4x":30,"l4y":39,"l5x":21,"l5y":39},{"data":1000004,"id":500000011,"lvl":11,"x":33,"y":22,"res_time":65399,"l1x":18,"l1y":8,"l2x":36,"l2y":24,"l3x":42,"l3y":25,"l4x":42,"l4y":25,"l5x":36,"l5y":24},{"data":1000006,"id":500000012,"lvl":9,"x":21,"y":10,"unit_prod":{"unit_type":0},"l1x":39,"l1y":36,"l2x":18,"l2y":39,"l3x":7,"l3y":28,"l4x":7,"l4y":28,"l5x":18,"l5y":39},{"data":1000002,"id":500000013,"lvl":11,"x":31,"y":14,"res_time":61037,"l1x":34,"l1y":11,"l2x":41,"l2y":19,"l3x":8,"l3y":17,"l4x":8,"l4y":17,"l5x":41,"l5y":19},{"data":1000009,"id":500000014,"lvl":9,"x":24,"y":30,"l1x":15,"l1y":16,"l2x":12,"l2y":15,"l3x":24,"l3y":35,"l4x":24,"l4y":35,"l5x":12,"l5y":15},{"data":1000008,"id":500000015,"lvl":9,"x":10,"y":32,"l1x":30,"l1y":26,"l2x":9,"l2y":22,"l3x":38,"l3y":25,"l4x":38,"l4y":25,"l5x":9,"l5y":22},{"data":1000000,"id":500000016,"lvl":5,"x":15,"y":35,"units":[[4000008,2]],"l1x":24,"l1y":8,"l2x":32,"l2y":9,"l3x":20,"l3y":5,"l4x":20,"l4y":5,"l5x":32,"l5y":9},{"data":1000002,"id":500000017,"lvl":11,"x":10,"y":17,"res_time":87181,"l1x":37,"l1y":15,"l2x":40,"l2y":35,"l3x":14,"l3y":22,"l4x":14,"l4y":22,"l5x":9,"l5y":34},{"data":1000004,"id":500000018,"lvl":11,"x":14,"y":31,"res_time":61057,"l1x":40,"l1y":24,"l2x":15,"l2y":11,"l3x":40,"l3y":29,"l4x":40,"l4y":29,"l5x":15,"l5y":11},{"data":1000003,"id":500000019,"lvl":10,"x":39,"y":33,"l1x":35,"l1y":22,"l2x":19,"l2y":14,"l3x":22,"l3y":14,"l4x":22,"l4y":14,"l5x":20,"l5y":14},{"data":1000005,"id":500000020,"lvl":10,"x":33,"y":39,"l1x":19,"l1y":16,"l2x":30,"l2y":19,"l3x":18,"l3y":23,"l4x":18,"l4y":23,"l5x":30,"l5y":18},{"data":1000010,"id":500000021,"lvl":7,"x":40,"y":37,"l1x":24,"l1y":39,"l2x":23,"l2y":35,"l3x":36,"l3y":35,"l4x":36,"l4y":35,"l5x":24,"l5y":34},{"data":1000010,"id":500000022,"lvl":7,"x":42,"y":35,"l1x":25,"l1y":39,"l2x":23,"l2y":33,"l3x":15,"l3y":16,"l4x":15,"l4y":16,"l5x":24,"l5y":33},{"data":1000010,"id":500000023,"lvl":7,"x":42,"y":36,"l1x":26,"l1y":39,"l2x":24,"l2y":32,"l3x":15,"l3y":15,"l4x":15,"l4y":15,"l5x":24,"l5y":32},{"data":1000010,"id":500000024,"lvl":7,"x":41,"y":36,"l1x":27,"l1y":39,"l2x":15,"l2y":19,"l3x":15,"l3y":14,"l4x":15,"l4y":14,"l5x":13,"l5y":20}],"decos":[],"respawnVars":{"secondsFromLastRespawn":231386,"respawnSeed":1914935487,"obstacleClearCounter":5,"time_to_gembox_drop":196966,"time_in_gembox_period":228665},"cooldowns":[],"newShopBuildings":[4,0,6,3,6,3,4,1,5,5,225,3,3,4,1,5,0,0,0,3,1,0,1,2,1,0,2,0,1,1,0,0],"newShopTraps":[6,6,3,0,0,4,2,0,2],"newShopDecos":[1,4,0,1,1,4,4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],"last_league_rank":9,"last_alliance_level":7,"last_league_shuffle":1,"last_season_seen":0,"last_news_seen":221,"troop_req_msg":"max hogs and max poison","war_req_msg":"2 witches, 1 barb","war_tutorials_seen":0,"war_base":false,"account_flags":14,"bool_layout_edit_shown_erase":true} \ No newline at end of file diff --git a/src/main/resources/examples/th8-sample-1.json b/src/main/resources/examples/th8-sample-1.json new file mode 100644 index 0000000..273bd4f --- /dev/null +++ b/src/main/resources/examples/th8-sample-1.json @@ -0,0 +1,18 @@ +{"wave_num":6,"exp_ver":1,"active_layout":0,"war_layout":1,"layout_state":[0,0,0,0,0,0],"buildings":[{"data":1000001,"id":500000000,"lvl":7,"x":30,"y":30,"l1x":25,"l1y":19,"l2x":13,"l2y":26,"l3x":29,"l3y":20,"l4x":29,"l4y":20,"l5x":13,"l5y":26},{"data":1000004,"id":500000001,"lvl":11,"x":22,"y":33,"res_time":87169,"l1x":11,"l1y":31,"l2x":24,"l2y":40,"l3x":41,"l3y":33,"l4x":41,"l4y":33,"l5x":24,"l5y":40},{"data":1000000,"id":500000002,"lvl":5,"x":35,"y":15,"units":[[4000008,2]],"l1x":29,"l1y":39,"l2x":38,"l2y":14,"l3x":40,"l3y":19,"l4x":40,"l4y":19,"l5x":38,"l5y":14},{"data":1000015,"id":500000003,"lvl":0,"x":41,"y":41,"l1x":14,"l1y":39,"l2x":9,"l2y":33,"l3x":36,"l3y":12,"l4x":36,"l4y":12,"l5x":41,"l5y":34},{"data":1000014,"id":500000004,"lvl":3,"x":27,"y":27,"l1x":25,"l1y":24,"l2x":23,"l2y":23,"l3x":25,"l3y":25,"l4x":25,"l4y":25,"l5x":23,"l5y":23},{"data":1000008,"id":500000005,"lvl":9,"x":37,"y":37,"l1x":36,"l1y":25,"l2x":33,"l2y":37,"l3x":34,"l3y":39,"l4x":34,"l4y":39,"l5x":33,"l5y":37},{"data":1000015,"id":500000006,"lvl":0,"x":38,"y":41,"l1x":16,"l1y":39,"l2x":37,"l2y":12,"l3x":32,"l3y":9,"l4x":32,"l4y":9,"l5x":37,"l5y":12},{"data":1000002,"id":500000007,"lvl":11,"x":37,"y":22,"res_time":87173,"l1x":21,"l1y":9,"l2x":41,"l2y":24,"l3x":8,"l3y":20,"l4x":8,"l4y":20,"l5x":41,"l5y":24},{"data":1000003,"id":500000008,"lvl":10,"x":39,"y":30,"l1x":16,"l1y":20,"l2x":26,"l2y":15,"l3x":29,"l3y":34,"l4x":29,"l4y":33,"l5x":26,"l5y":15},{"data":1000005,"id":500000009,"lvl":10,"x":30,"y":39,"l1x":24,"l1y":33,"l2x":27,"l2y":33,"l3x":16,"l3y":32,"l4x":16,"l4y":32,"l5x":27,"l5y":33},{"data":1000006,"id":500000010,"lvl":9,"x":8,"y":27,"unit_prod":{"unit_type":0},"l1x":40,"l1y":27,"l2x":21,"l2y":39,"l3x":30,"l3y":39,"l4x":30,"l4y":39,"l5x":21,"l5y":39},{"data":1000004,"id":500000011,"lvl":11,"x":33,"y":22,"res_time":65399,"l1x":18,"l1y":8,"l2x":36,"l2y":24,"l3x":42,"l3y":25,"l4x":42,"l4y":25,"l5x":36,"l5y":24},{"data":1000006,"id":500000012,"lvl":9,"x":21,"y":10,"unit_prod":{"unit_type":0},"l1x":39,"l1y":36,"l2x":18,"l2y":39,"l3x":7,"l3y":28,"l4x":7,"l4y":28,"l5x":18,"l5y":39},{"data":1000002,"id":500000013,"lvl":11,"x":31,"y":14,"res_time":61037,"l1x":34,"l1y":11,"l2x":41,"l2y":19,"l3x":8,"l3y":17,"l4x":8,"l4y":17,"l5x":41,"l5y":19},{"data":1000009,"id":500000014,"lvl":9,"x":24,"y":30,"l1x":15,"l1y":16,"l2x":12,"l2y":15,"l3x":24,"l3y":35,"l4x":24,"l4y":35,"l5x":12,"l5y":15},{"data":1000008,"id":500000015,"lvl":9,"x":10,"y":32,"l1x":30,"l1y":26,"l2x":9,"l2y":22,"l3x":38,"l3y":25,"l4x":38,"l4y":25,"l5x":9,"l5y":22},{"data":1000000,"id":500000016,"lvl":5,"x":15,"y":35,"units":[[4000008,2]],"l1x":24,"l1y":8,"l2x":32,"l2y":9,"l3x":20,"l3y":5,"l4x":20,"l4y":5,"l5x":32,"l5y":9},{"data":1000002,"id":500000017,"lvl":11,"x":10,"y":17,"res_time":87181,"l1x":37,"l1y":15,"l2x":40,"l2y":35,"l3x":14,"l3y":22,"l4x":14,"l4y":22,"l5x":9,"l5y":34},{"data":1000004,"id":500000018,"lvl":11,"x":14,"y":31,"res_time":61057,"l1x":40,"l1y":24,"l2x":15,"l2y":11,"l3x":40,"l3y":29,"l4x":40,"l4y":29,"l5x":15,"l5y":11},{"data":1000003,"id":500000019,"lvl":10,"x":39,"y":33,"l1x":35,"l1y":22,"l2x":19,"l2y":14,"l3x":22,"l3y":14,"l4x":22,"l4y":14,"l5x":20,"l5y":14},{"data":1000005,"id":500000020,"lvl":10,"x":33,"y":39,"l1x":19,"l1y":16,"l2x":30,"l2y":19,"l3x":18,"l3y":23,"l4x":18,"l4y":23,"l5x":30,"l5y":18},{"data":1000010,"id":500000021,"lvl":7,"x":40,"y":37,"l1x":24,"l1y":39,"l2x":23,"l2y":35,"l3x":36,"l3y":35,"l4x":36,"l4y":35,"l5x":24,"l5y":34},{"data":1000010,"id":500000022,"lvl":7,"x":42,"y":35,"l1x":25,"l1y":39,"l2x":23,"l2y":33,"l3x":15,"l3y":16,"l4x":15,"l4y":16,"l5x":24,"l5y":33},{"data":1000010,"id":500000023,"lvl":7,"x":42,"y":36,"l1x":26,"l1y":39,"l2x":24,"l2y":32,"l3x":15,"l3y":15,"l4x":15,"l4y":15,"l5x":24,"l5y":32},{"data":1000010,"id":500000024,"lvl":7,"x":41,"y":36,"l1x":27,"l1y":39,"l2x":15,"l2y":19,"l3x":15,"l3y":14,"l4x":15,"l4y":14,"l5x":13,"l5y":20},{"data":1000010,"id":500000025,"lvl":7,"x":34,"y":42,"l1x":28,"l1y":39,"l2x":14,"l2y":19,"l3x":22,"l3y":18,"l4x":19,"l4y":18,"l5x":15,"l5y":20},{"data":1000010,"id":500000026,"lvl":7,"x":35,"y":42,"l1x":26,"l1y":23,"l2x":13,"l2y":19,"l3x":41,"l3y":28,"l4x":41,"l4y":28,"l5x":34,"l5y":20},{"data":1000010,"id":500000027,"lvl":7,"x":36,"y":42,"l1x":29,"l1y":38,"l2x":17,"l2y":31,"l3x":15,"l3y":13,"l4x":15,"l4y":13,"l5x":17,"l5y":31},{"data":1000010,"id":500000028,"lvl":7,"x":37,"y":42,"l1x":30,"l1y":35,"l2x":17,"l2y":32,"l3x":16,"l3y":13,"l4x":16,"l4y":13,"l5x":17,"l5y":32},{"data":1000010,"id":500000029,"lvl":7,"x":37,"y":41,"l1x":31,"l1y":38,"l2x":17,"l2y":33,"l3x":17,"l3y":13,"l4x":17,"l4y":13,"l5x":17,"l5y":33},{"data":1000010,"id":500000030,"lvl":7,"x":40,"y":26,"l1x":32,"l1y":38,"l2x":16,"l2y":33,"l3x":18,"l3y":13,"l4x":18,"l4y":13,"l5x":16,"l5y":33},{"data":1000010,"id":500000031,"lvl":7,"x":37,"y":40,"l1x":34,"l1y":38,"l2x":15,"l2y":33,"l3x":19,"l3y":13,"l4x":19,"l4y":13,"l5x":15,"l5y":33},{"data":1000010,"id":500000032,"lvl":7,"x":38,"y":40,"l1x":35,"l1y":38,"l2x":14,"l2y":33,"l3x":36,"l3y":38,"l4x":36,"l4y":38,"l5x":14,"l5y":33},{"data":1000010,"id":500000033,"lvl":7,"x":39,"y":40,"l1x":36,"l1y":38,"l2x":13,"l2y":33,"l3x":41,"l3y":27,"l4x":41,"l4y":27,"l5x":13,"l5y":33}, + + {"data":1000019, "lvl":5, "x":31, "y": 33,"l1x":31, "l1y":33}, + {"data":12000002, "lvl":2, "x":24, "y":28,"l1x":24,"l1y":28}, + + {"data":12000000, "lvl":5, "x":3, "y":3,"l1x":24,"l1y":30}, + {"data":12000000, "lvl":5, "x":3, "y":4,"l1x":25,"l1y":30}, + {"data":12000000, "lvl":5, "x":3, "y":5,"l1x":24,"l1y":31}, + {"data":12000000, "lvl":5, "x":3, "y":6,"l1x":25,"l1y":31}, + + {"data":12000001, "lvl":5, "x":3, "y":7,"l1x":16,"l1y":15}, + {"data":12000008, "lvl":2, "x":3, "y":8,"l1x":24,"l1y":24}, + {"data":12000005, "lvl":1, "x":3, "y":9,"l1x":24,"l1y":25}, + {"data":12000006, "lvl":3, "x":3, "y":10,"l1x":24,"l1y":26}, + + {"data":8000021, "lvl":0, "x":3, "y":11,"l1x":38,"l1y":9}, + + {"data":1000010,"id":500000034,"lvl":7,"x":40,"y":36,"l1x":37,"l1y":38,"l2x":12,"l2y":33,"l3x":35,"l3y":38,"l4x":35,"l4y":38,"l5x":12,"l5y":33},{"data":1000010,"id":500000035,"lvl":7,"x":42,"y":34,"l1x":38,"l1y":38,"l2x":11,"l2y":33,"l3x":34,"l3y":38,"l4x":34,"l4y":38,"l5x":11,"l5y":33},{"data":1000010,"id":500000036,"lvl":7,"x":40,"y":38,"l1x":38,"l1y":37,"l2x":11,"l2y":32,"l3x":33,"l3y":38,"l4x":33,"l4y":38,"l5x":11,"l5y":32},{"data":1000010,"id":500000037,"lvl":7,"x":40,"y":39,"l1x":38,"l1y":36,"l2x":11,"l2y":31,"l3x":32,"l3y":38,"l4x":32,"l4y":38,"l5x":11,"l5y":31},{"data":1000010,"id":500000038,"lvl":7,"x":39,"y":26,"l1x":32,"l1y":21,"l2x":11,"l2y":30,"l3x":31,"l3y":38,"l4x":31,"l4y":38,"l5x":11,"l5y":30},{"data":1000010,"id":500000039,"lvl":7,"x":38,"y":26,"l1x":29,"l1y":19,"l2x":11,"l2y":29,"l3x":31,"l3y":37,"l4x":31,"l4y":37,"l5x":11,"l5y":29},{"data":1000010,"id":500000040,"lvl":7,"x":38,"y":25,"l1x":38,"l1y":35,"l2x":12,"l2y":29,"l3x":36,"l3y":34,"l4x":36,"l4y":34,"l5x":12,"l5y":29},{"data":1000010,"id":500000041,"lvl":7,"x":37,"y":25,"l1x":38,"l1y":34,"l2x":12,"l2y":28,"l3x":36,"l3y":33,"l4x":36,"l4y":33,"l5x":12,"l5y":28},{"data":1000010,"id":500000042,"lvl":7,"x":36,"y":25,"l1x":27,"l1y":13,"l2x":12,"l2y":27,"l3x":36,"l3y":36,"l4x":36,"l4y":36,"l5x":12,"l5y":27},{"data":1000010,"id":500000043,"lvl":7,"x":36,"y":24,"l1x":23,"l1y":13,"l2x":12,"l2y":26,"l3x":23,"l3y":37,"l4x":22,"l4y":38,"l5x":12,"l5y":26},{"data":1000010,"id":500000044,"lvl":7,"x":36,"y":23,"l1x":24,"l1y":13,"l2x":12,"l2y":19,"l3x":30,"l3y":37,"l4x":30,"l4y":37,"l5x":34,"l5y":22},{"data":1000010,"id":500000045,"lvl":7,"x":36,"y":22,"l1x":25,"l1y":13,"l2x":12,"l2y":20,"l3x":27,"l3y":38,"l4x":27,"l4y":38,"l5x":12,"l5y":20},{"data":1000010,"id":500000046,"lvl":7,"x":36,"y":21,"l1x":26,"l1y":13,"l2x":12,"l2y":21,"l3x":33,"l3y":32,"l4x":33,"l4y":32,"l5x":12,"l5y":21},{"data":1000010,"id":500000047,"lvl":7,"x":35,"y":21,"l1x":28,"l1y":13,"l2x":12,"l2y":22,"l3x":34,"l3y":32,"l4x":34,"l4y":32,"l5x":12,"l5y":22},{"data":1000010,"id":500000048,"lvl":7,"x":40,"y":40,"l1x":32,"l1y":12,"l2x":12,"l2y":23,"l3x":35,"l3y":32,"l4x":35,"l4y":32,"l5x":12,"l5y":23},{"data":1000010,"id":500000049,"lvl":7,"x":12,"y":8,"l1x":29,"l1y":14,"l2x":12,"l2y":24,"l3x":36,"l3y":32,"l4x":36,"l4y":32,"l5x":12,"l5y":24},{"data":1000010,"id":500000050,"lvl":7,"x":26,"y":39,"l1x":29,"l1y":16,"l2x":12,"l2y":25,"l3x":37,"l3y":32,"l4x":37,"l4y":32,"l5x":12,"l5y":25},{"data":1000010,"id":500000051,"lvl":7,"x":26,"y":38,"l1x":29,"l1y":15,"l2x":18,"l2y":16,"l3x":37,"l3y":31,"l4x":37,"l4y":31,"l5x":18,"l5y":16},{"data":1000010,"id":500000052,"lvl":7,"x":25,"y":38,"l1x":29,"l1y":17,"l2x":18,"l2y":15,"l3x":37,"l3y":30,"l4x":37,"l4y":30,"l5x":18,"l5y":15},{"data":1000010,"id":500000053,"lvl":7,"x":25,"y":37,"l1x":31,"l1y":12,"l2x":18,"l2y":14,"l3x":37,"l3y":29,"l4x":37,"l4y":29,"l5x":18,"l5y":14},{"data":1000010,"id":500000054,"lvl":7,"x":25,"y":36,"l1x":36,"l1y":16,"l2x":18,"l2y":13,"l3x":37,"l3y":28,"l4x":37,"l4y":28,"l5x":18,"l5y":13},{"data":1000010,"id":500000055,"lvl":7,"x":24,"y":36,"l1x":29,"l1y":12,"l2x":18,"l2y":12,"l3x":38,"l3y":28,"l4x":38,"l4y":28,"l5x":18,"l5y":12},{"data":1000010,"id":500000056,"lvl":7,"x":23,"y":36,"l1x":35,"l1y":14,"l2x":18,"l2y":11,"l3x":40,"l3y":28,"l4x":40,"l4y":28,"l5x":18,"l5y":11},{"data":1000010,"id":500000057,"lvl":7,"x":22,"y":36,"l1x":36,"l1y":14,"l2x":18,"l2y":10,"l3x":41,"l3y":25,"l4x":41,"l4y":25,"l5x":18,"l5y":10},{"data":1000010,"id":500000058,"lvl":7,"x":21,"y":36,"l1x":29,"l1y":20,"l2x":18,"l2y":9,"l3x":41,"l3y":24,"l4x":41,"l4y":24,"l5x":18,"l5y":9},{"data":1000010,"id":500000059,"lvl":7,"x":8,"y":35,"l1x":30,"l1y":20,"l2x":18,"l2y":8,"l3x":34,"l3y":23,"l4x":34,"l4y":23,"l5x":18,"l5y":8},{"data":1000010,"id":500000060,"lvl":7,"x":21,"y":35,"l1x":31,"l1y":20,"l2x":30,"l2y":13,"l3x":35,"l3y":23,"l4x":35,"l4y":23,"l5x":30,"l5y":13},{"data":1000010,"id":500000061,"lvl":7,"x":7,"y":35,"l1x":31,"l1y":21,"l2x":19,"l2y":8,"l3x":36,"l3y":23,"l4x":36,"l4y":23,"l5x":19,"l5y":8},{"data":1000010,"id":500000062,"lvl":7,"x":7,"y":34,"l1x":34,"l1y":24,"l2x":20,"l2y":8,"l3x":37,"l3y":23,"l4x":37,"l4y":23,"l5x":20,"l5y":8},{"data":1000010,"id":500000063,"lvl":7,"x":7,"y":33,"l1x":34,"l1y":23,"l2x":21,"l2y":8,"l3x":38,"l3y":23,"l4x":38,"l4y":23,"l5x":21,"l5y":8},{"data":1000010,"id":500000064,"lvl":7,"x":7,"y":32,"l1x":34,"l1y":22,"l2x":22,"l2y":8,"l3x":39,"l3y":24,"l4x":39,"l4y":24,"l5x":22,"l5y":8},{"data":1000010,"id":500000065,"lvl":7,"x":7,"y":31,"l1x":33,"l1y":21,"l2x":23,"l2y":8,"l3x":39,"l3y":23,"l4x":39,"l4y":23,"l5x":23,"l5y":8},{"data":1000010,"id":500000066,"lvl":7,"x":7,"y":30,"l1x":34,"l1y":21,"l2x":24,"l2y":8,"l3x":39,"l3y":22,"l4x":39,"l4y":22,"l5x":24,"l5y":8},{"data":1000010,"id":500000067,"lvl":7,"x":7,"y":29,"l1x":30,"l1y":25,"l2x":25,"l2y":8,"l3x":39,"l3y":21,"l4x":39,"l4y":21,"l5x":25,"l5y":8},{"data":1000010,"id":500000068,"lvl":7,"x":7,"y":28,"l1x":31,"l1y":25,"l2x":26,"l2y":8,"l3x":39,"l3y":20,"l4x":39,"l4y":20,"l5x":26,"l5y":8},{"data":1000010,"id":500000069,"lvl":7,"x":7,"y":27,"l1x":32,"l1y":25,"l2x":27,"l2y":8,"l3x":39,"l3y":19,"l4x":39,"l4y":19,"l5x":27,"l5y":8},{"data":1000010,"id":500000070,"lvl":7,"x":7,"y":26,"l1x":19,"l1y":22,"l2x":28,"l2y":8,"l3x":39,"l3y":18,"l4x":39,"l4y":18,"l5x":28,"l5y":8},{"data":1000013,"id":500000071,"lvl":5,"x":18,"y":25,"l1x":35,"l1y":34,"l2x":15,"l2y":14,"l3x":36,"l3y":15,"l4x":36,"l4y":15,"l5x":15,"l5y":14},{"data":1000007,"id":500000072,"lvl":5,"x":21,"y":37,"l1x":10,"l1y":14,"l2x":8,"l2y":14,"l3x":26,"l3y":10,"l4x":26,"l4y":10,"l5x":8,"l5y":14},{"data":1000010,"id":500000073,"lvl":7,"x":9,"y":35,"l1x":19,"l1y":21,"l2x":29,"l2y":8,"l3x":39,"l3y":17,"l4x":39,"l4y":17,"l5x":29,"l5y":8},{"data":1000010,"id":500000074,"lvl":7,"x":35,"y":8,"l1x":19,"l1y":23,"l2x":30,"l2y":8,"l3x":39,"l3y":16,"l4x":39,"l4y":16,"l5x":30,"l5y":8},{"data":1000010,"id":500000075,"lvl":7,"x":35,"y":7,"l1x":20,"l1y":23,"l2x":31,"l2y":8,"l3x":39,"l3y":15,"l4x":39,"l4y":15,"l5x":31,"l5y":8},{"data":1000010,"id":500000076,"lvl":7,"x":34,"y":7,"l1x":33,"l1y":25,"l2x":31,"l2y":9,"l3x":39,"l3y":14,"l4x":39,"l4y":14,"l5x":31,"l5y":9},{"data":1000010,"id":500000077,"lvl":7,"x":33,"y":7,"l1x":28,"l1y":25,"l2x":31,"l2y":10,"l3x":38,"l3y":14,"l4x":38,"l4y":14,"l5x":31,"l5y":10},{"data":1000010,"id":500000078,"lvl":7,"x":32,"y":7,"l1x":29,"l1y":25,"l2x":23,"l2y":13,"l3x":37,"l3y":14,"l4x":37,"l4y":14,"l5x":35,"l5y":26},{"data":1000010,"id":500000079,"lvl":7,"x":31,"y":7,"l1x":28,"l1y":24,"l2x":23,"l2y":15,"l3x":36,"l3y":14,"l4x":36,"l4y":14,"l5x":25,"l5y":15},{"data":1000010,"id":500000080,"lvl":7,"x":30,"y":7,"l1x":19,"l1y":20,"l2x":24,"l2y":13,"l3x":35,"l3y":14,"l4x":35,"l4y":14,"l5x":34,"l5y":26},{"data":1000010,"id":500000081,"lvl":7,"x":29,"y":7,"l1x":34,"l1y":25,"l2x":23,"l2y":14,"l3x":34,"l3y":14,"l4x":34,"l4y":14,"l5x":25,"l5y":16},{"data":1000010,"id":500000082,"lvl":7,"x":28,"y":7,"l1x":19,"l1y":19,"l2x":25,"l2y":13,"l3x":33,"l3y":14,"l4x":33,"l4y":14,"l5x":25,"l5y":13},{"data":1000010,"id":500000083,"lvl":7,"x":27,"y":7,"l1x":22,"l1y":19,"l2x":26,"l2y":13,"l3x":32,"l3y":14,"l4x":32,"l4y":14,"l5x":26,"l5y":13},{"data":1000010,"id":500000084,"lvl":7,"x":26,"y":7,"l1x":21,"l1y":19,"l2x":27,"l2y":13,"l3x":31,"l3y":14,"l4x":31,"l4y":14,"l5x":27,"l5y":13},{"data":1000010,"id":500000085,"lvl":7,"x":25,"y":7,"l1x":20,"l1y":19,"l2x":29,"l2y":13,"l3x":30,"l3y":14,"l4x":30,"l4y":14,"l5x":29,"l5y":13},{"data":1000010,"id":500000086,"lvl":7,"x":35,"y":9,"l1x":25,"l1y":18,"l2x":28,"l2y":13,"l3x":27,"l3y":14,"l4x":27,"l4y":14,"l5x":28,"l5y":13},{"data":1000010,"id":500000087,"lvl":7,"x":35,"y":10,"l1x":24,"l1y":18,"l2x":31,"l2y":12,"l3x":25,"l3y":13,"l4x":25,"l4y":13,"l5x":31,"l5y":12},{"data":1000010,"id":500000088,"lvl":7,"x":35,"y":11,"l1x":24,"l1y":19,"l2x":31,"l2y":13,"l3x":25,"l3y":12,"l4x":25,"l4y":12,"l5x":31,"l5y":13},{"data":1000010,"id":500000089,"lvl":7,"x":35,"y":12,"l1x":23,"l1y":19,"l2x":31,"l2y":14,"l3x":25,"l3y":10,"l4x":25,"l4y":10,"l5x":31,"l5y":14},{"data":1000010,"id":500000090,"lvl":7,"x":35,"y":13,"l1x":29,"l1y":18,"l2x":32,"l2y":14,"l3x":24,"l3y":10,"l4x":24,"l4y":10,"l5x":32,"l5y":14},{"data":1000010,"id":500000091,"lvl":7,"x":7,"y":25,"l1x":28,"l1y":18,"l2x":33,"l2y":14,"l3x":23,"l3y":10,"l4x":23,"l4y":10,"l5x":33,"l5y":14},{"data":1000010,"id":500000092,"lvl":7,"x":10,"y":35,"l1x":27,"l1y":18,"l2x":34,"l2y":14,"l3x":22,"l3y":10,"l4x":22,"l4y":10,"l5x":34,"l5y":14},{"data":1000010,"id":500000093,"lvl":7,"x":11,"y":35,"l1x":26,"l1y":18,"l2x":35,"l2y":14,"l3x":21,"l3y":10,"l4x":21,"l4y":10,"l5x":35,"l5y":14},{"data":1000010,"id":500000094,"lvl":7,"x":12,"y":35,"l1x":27,"l1y":23,"l2x":36,"l2y":14,"l3x":20,"l3y":10,"l4x":20,"l4y":10,"l5x":36,"l5y":14},{"data":1000010,"id":500000095,"lvl":7,"x":13,"y":35,"l1x":28,"l1y":23,"l2x":37,"l2y":14,"l3x":20,"l3y":11,"l4x":20,"l4y":11,"l5x":37,"l5y":14},{"data":1000010,"id":500000096,"lvl":7,"x":26,"y":11,"l1x":25,"l1y":23,"l2x":37,"l2y":15,"l3x":20,"l3y":12,"l4x":20,"l4y":12,"l5x":37,"l5y":15},{"data":1000010,"id":500000097,"lvl":7,"x":27,"y":11,"l1x":24,"l1y":23,"l2x":37,"l2y":16,"l3x":20,"l3y":13,"l4x":20,"l4y":13,"l5x":37,"l5y":16},{"data":1000002,"id":500000098,"lvl":11,"x":17,"y":10,"res_time":87175,"l1x":23,"l1y":15,"l2x":35,"l2y":41,"l3x":16,"l3y":19,"l4x":16,"l4y":19,"l5x":35,"l5y":41},{"data":1000006,"id":500000099,"lvl":9,"x":27,"y":8,"unit_prod":{"unit_type":0,"t":0,"slots":[{"id":4000008,"cnt":1}]},"l1x":37,"l1y":18,"l2x":14,"l2y":38,"l3x":7,"l3y":23,"l4x":7,"l4y":23,"l5x":14,"l5y":38},{"data":1000004,"id":500000100,"lvl":11,"x":28,"y":18,"res_time":61043,"l1x":39,"l1y":33,"l2x":11,"l2y":11,"l3x":34,"l3y":42,"l4x":34,"l4y":42,"l5x":11,"l5y":11},{"data":1000009,"id":500000101,"lvl":9,"x":30,"y":24,"l1x":25,"l1y":36,"l2x":12,"l2y":30,"l3x":36,"l3y":20,"l4x":36,"l4y":20,"l5x":12,"l5y":30},{"data":1000015,"id":500000102,"lvl":0,"x":28,"y":40,"l1x":12,"l1y":18,"l2x":22,"l2y":37,"l3x":30,"l3y":9,"l4x":30,"l4y":9,"l5x":41,"l5y":36},{"data":1000012,"id":500000103,"lvl":5,"x":27,"y":30,"l1x":21,"l1y":20,"l2x":30,"l2y":29,"l3x":25,"l3y":20,"l4x":25,"l4y":20,"l5x":30,"l5y":29},{"data":1000015,"id":500000104,"lvl":0,"x":26,"y":40,"l1x":37,"l1y":13,"l2x":18,"l2y":23,"l3x":8,"l3y":26,"l4x":8,"l4y":26,"l5x":18,"l5y":23},{"data":1000000,"id":500000105,"lvl":5,"x":12,"y":26,"units":[[4000008,3]],"l1x":10,"l1y":20,"l2x":6,"l2y":28,"l3x":23,"l3y":39,"l4x":23,"l4y":39,"l5x":6,"l5y":28},{"data":1000020,"id":500000106,"lvl":2,"x":41,"y":37,"units":[[26000001,1],[26000000,2]],"unit_prod":{"unit_type":1,"t":0,"slots":[{"id":26000001,"cnt":1}]},"l1x":11,"l1y":27,"l2x":9,"l2y":25,"l3x":17,"l3y":10,"l4x":17,"l4y":10,"l5x":9,"l5y":25},{"data":1000011,"id":500000107,"lvl":5,"x":21,"y":21,"l1x":16,"l1y":24,"l2x":25,"l2y":29,"l3x":30,"l3y":11,"l4x":30,"l4y":11,"l5x":25,"l5y":28},{"data":1000008,"id":500000108,"lvl":9,"x":32,"y":10,"l1x":14,"l1y":36,"l2x":26,"l2y":10,"l3x":18,"l3y":37,"l4x":18,"l4y":37,"l5x":26,"l5y":10},{"data":1000010,"id":500000109,"lvl":7,"x":28,"y":11,"l1x":36,"l1y":15,"l2x":37,"l2y":18,"l3x":20,"l3y":14,"l4x":20,"l4y":14,"l5x":37,"l5y":18},{"data":1000010,"id":500000110,"lvl":7,"x":29,"y":11,"l1x":30,"l1y":12,"l2x":37,"l2y":17,"l3x":20,"l3y":15,"l4x":20,"l4y":15,"l5x":37,"l5y":17},{"data":1000010,"id":500000111,"lvl":7,"x":30,"y":11,"l1x":36,"l1y":17,"l2x":39,"l2y":34,"l3x":20,"l3y":16,"l4x":20,"l4y":16,"l5x":39,"l5y":34},{"data":1000010,"id":500000112,"lvl":7,"x":31,"y":11,"l1x":34,"l1y":14,"l2x":38,"l2y":34,"l3x":28,"l3y":14,"l4x":28,"l4y":14,"l5x":38,"l5y":34},{"data":1000010,"id":500000113,"lvl":7,"x":31,"y":12,"l1x":36,"l1y":18,"l2x":37,"l2y":34,"l3x":25,"l3y":14,"l4x":25,"l4y":14,"l5x":37,"l5y":34},{"data":1000010,"id":500000114,"lvl":7,"x":31,"y":13,"l1x":36,"l1y":19,"l2x":35,"l2y":34,"l3x":26,"l3y":14,"l4x":26,"l4y":14,"l5x":35,"l5y":34},{"data":1000010,"id":500000115,"lvl":7,"x":32,"y":13,"l1x":33,"l1y":13,"l2x":35,"l2y":33,"l3x":29,"l3y":14,"l4x":29,"l4y":14,"l5x":35,"l5y":33},{"data":1000010,"id":500000116,"lvl":7,"x":33,"y":13,"l1x":35,"l1y":21,"l2x":36,"l2y":34,"l3x":29,"l3y":15,"l4x":29,"l4y":15,"l5x":36,"l5y":34},{"data":1000010,"id":500000117,"lvl":7,"x":34,"y":13,"l1x":33,"l1y":38,"l2x":36,"l2y":35,"l3x":29,"l3y":16,"l4x":29,"l4y":16,"l5x":36,"l5y":35},{"data":1000010,"id":500000118,"lvl":7,"x":34,"y":14,"l1x":36,"l1y":20,"l2x":36,"l2y":36,"l3x":29,"l3y":17,"l4x":29,"l4y":17,"l5x":36,"l5y":36},{"data":1000010,"id":500000119,"lvl":7,"x":34,"y":15,"l1x":37,"l1y":21,"l2x":36,"l2y":37,"l3x":28,"l3y":37,"l4x":28,"l4y":37,"l5x":36,"l5y":37},{"data":1000010,"id":500000120,"lvl":7,"x":34,"y":16,"l1x":38,"l1y":21,"l2x":36,"l2y":38,"l3x":41,"l3y":26,"l4x":41,"l4y":26,"l5x":36,"l5y":38},{"data":1000010,"id":500000121,"lvl":7,"x":11,"y":26,"l1x":39,"l1y":21,"l2x":36,"l2y":39,"l3x":25,"l3y":17,"l4x":25,"l4y":17,"l5x":36,"l5y":39},{"data":1000010,"id":500000122,"lvl":7,"x":11,"y":27,"l1x":39,"l1y":22,"l2x":23,"l2y":16,"l3x":23,"l3y":17,"l4x":23,"l4y":17,"l5x":25,"l5y":14},{"data":1000010,"id":500000123,"lvl":7,"x":11,"y":28,"l1x":39,"l1y":23,"l2x":36,"l2y":40,"l3x":21,"l3y":17,"l4x":21,"l4y":17,"l5x":36,"l5y":40},{"data":1000010,"id":500000124,"lvl":7,"x":11,"y":29,"l1x":39,"l1y":24,"l2x":35,"l2y":40,"l3x":20,"l3y":17,"l4x":20,"l4y":17,"l5x":35,"l5y":40},{"data":1000010,"id":500000125,"lvl":7,"x":11,"y":30,"l1x":39,"l1y":25,"l2x":34,"l2y":40,"l3x":19,"l3y":17,"l4x":19,"l4y":17,"l5x":34,"l5y":40},{"data":1000010,"id":500000126,"lvl":7,"x":11,"y":31,"l1x":39,"l1y":26,"l2x":33,"l2y":40,"l3x":18,"l3y":17,"l4x":18,"l4y":17,"l5x":33,"l5y":40},{"data":1000010,"id":500000127,"lvl":7,"x":12,"y":31,"l1x":39,"l1y":27,"l2x":32,"l2y":40,"l3x":16,"l3y":17,"l4x":16,"l4y":17,"l5x":32,"l5y":40},{"data":1000010,"id":500000128,"lvl":7,"x":13,"y":31,"l1x":39,"l1y":28,"l2x":24,"l2y":35,"l3x":15,"l3y":17,"l4x":15,"l4y":17,"l5x":24,"l5y":35},{"data":1000010,"id":500000129,"lvl":7,"x":13,"y":32,"l1x":39,"l1y":29,"l2x":24,"l2y":36,"l3x":14,"l3y":17,"l4x":14,"l4y":17,"l5x":24,"l5y":36},{"data":1000010,"id":500000130,"lvl":7,"x":13,"y":33,"l1x":22,"l1y":23,"l2x":24,"l2y":37,"l3x":13,"l3y":17,"l4x":13,"l4y":17,"l5x":24,"l5y":37},{"data":1000010,"id":500000131,"lvl":7,"x":13,"y":34,"l1x":39,"l1y":30,"l2x":33,"l2y":21,"l3x":12,"l3y":17,"l4x":12,"l4y":17,"l5x":33,"l5y":21},{"data":1000010,"id":500000132,"lvl":7,"x":14,"y":34,"l1x":39,"l1y":31,"l2x":33,"l2y":20,"l3x":11,"l3y":17,"l4x":11,"l4y":17,"l5x":30,"l5y":21},{"data":1000010,"id":500000133,"lvl":7,"x":15,"y":34,"l1x":39,"l1y":32,"l2x":33,"l2y":19,"l3x":11,"l3y":18,"l4x":11,"l4y":18,"l5x":34,"l5y":19},{"data":1000002,"id":500000134,"lvl":11,"x":17,"y":14,"res_time":87179,"l1x":11,"l1y":11,"l2x":29,"l2y":36,"l3x":34,"l3y":25,"l4x":34,"l4y":25,"l5x":29,"l5y":36},{"data":1000004,"id":500000135,"lvl":11,"x":18,"y":28,"res_time":61053,"l1x":25,"l1y":40,"l2x":11,"l2y":34,"l3x":37,"l3y":39,"l4x":37,"l4y":39,"l5x":12,"l5y":34},{"data":1000009,"id":500000136,"lvl":9,"x":14,"y":14,"l1x":15,"l1y":28,"l2x":34,"l2y":15,"l3x":22,"l3y":11,"l4x":22,"l4y":11,"l5x":34,"l5y":15},{"data":1000002,"id":500000137,"lvl":11,"x":14,"y":17,"res_time":87173,"l1x":14,"l1y":8,"l2x":6,"l2y":20,"l3x":26,"l3y":15,"l4x":26,"l4y":15,"l5x":6,"l5y":20},{"data":1000004,"id":500000138,"lvl":11,"x":18,"y":22,"res_time":61055,"l1x":11,"l1y":8,"l2x":13,"l2y":21,"l3x":33,"l3y":11,"l4x":33,"l4y":11,"l5x":13,"l5y":21},{"data":1000013,"id":500000139,"lvl":5,"x":25,"y":18,"l1x":15,"l1y":12,"l2x":37,"l2y":35,"l3x":16,"l3y":14,"l4x":16,"l4y":14,"l5x":38,"l5y":35},{"data":1000010,"id":500000140,"lvl":7,"x":9,"y":25,"l1x":21,"l1y":23,"l2x":34,"l2y":19,"l3x":11,"l3y":19,"l4x":11,"l4y":19,"l5x":36,"l5y":18},{"data":1000010,"id":500000141,"lvl":7,"x":10,"y":25,"l1x":23,"l1y":23,"l2x":35,"l2y":19,"l3x":11,"l3y":20,"l4x":11,"l4y":20,"l5x":35,"l5y":18},{"data":1000010,"id":500000142,"lvl":7,"x":11,"y":25,"l1x":20,"l1y":26,"l2x":36,"l2y":19,"l3x":11,"l3y":21,"l4x":11,"l4y":21,"l5x":34,"l5y":18},{"data":1000010,"id":500000143,"lvl":7,"x":12,"y":25,"l1x":20,"l1y":36,"l2x":37,"l2y":19,"l3x":11,"l3y":22,"l4x":11,"l4y":22,"l5x":37,"l5y":19},{"data":1000010,"id":500000144,"lvl":7,"x":25,"y":9,"l1x":37,"l1y":33,"l2x":38,"l2y":19,"l3x":12,"l3y":22,"l4x":12,"l4y":22,"l5x":38,"l5y":19},{"data":1000010,"id":500000145,"lvl":7,"x":25,"y":10,"l1x":36,"l1y":33,"l2x":39,"l2y":19,"l3x":27,"l3y":33,"l4x":27,"l4y":33,"l5x":39,"l5y":19},{"data":1000010,"id":500000146,"lvl":7,"x":25,"y":11,"l1x":35,"l1y":33,"l2x":40,"l2y":19,"l3x":27,"l3y":34,"l4x":27,"l4y":34,"l5x":40,"l5y":19},{"data":1000010,"id":500000147,"lvl":7,"x":25,"y":12,"l1x":34,"l1y":33,"l2x":40,"l2y":20,"l3x":27,"l3y":35,"l4x":27,"l4y":35,"l5x":40,"l5y":20},{"data":1000010,"id":500000148,"lvl":7,"x":25,"y":8,"l1x":15,"l1y":26,"l2x":40,"l2y":21,"l3x":27,"l3y":36,"l4x":27,"l4y":36,"l5x":40,"l5y":21},{"data":1000010,"id":500000149,"lvl":7,"x":24,"y":8,"l1x":15,"l1y":25,"l2x":40,"l2y":22,"l3x":27,"l3y":37,"l4x":27,"l4y":37,"l5x":40,"l5y":22},{"data":1000010,"id":500000150,"lvl":7,"x":23,"y":8,"l1x":15,"l1y":24,"l2x":32,"l2y":39,"l3x":23,"l3y":38,"l4x":23,"l4y":38,"l5x":32,"l5y":39},{"data":1000010,"id":500000151,"lvl":7,"x":22,"y":8,"l1x":15,"l1y":23,"l2x":31,"l2y":39,"l3x":24,"l3y":38,"l4x":24,"l4y":38,"l5x":31,"l5y":39},{"data":1000010,"id":500000152,"lvl":7,"x":21,"y":8,"l1x":15,"l1y":21,"l2x":30,"l2y":39,"l3x":25,"l3y":38,"l4x":25,"l4y":38,"l5x":30,"l5y":39},{"data":1000010,"id":500000153,"lvl":7,"x":20,"y":8,"l1x":15,"l1y":22,"l2x":29,"l2y":39,"l3x":22,"l3y":37,"l4x":21,"l4y":38,"l5x":29,"l5y":39},{"data":1000010,"id":500000154,"lvl":7,"x":19,"y":8,"l1x":14,"l1y":19,"l2x":28,"l2y":39,"l3x":26,"l3y":38,"l4x":26,"l4y":38,"l5x":28,"l5y":39},{"data":1000010,"id":500000155,"lvl":7,"x":18,"y":8,"l1x":14,"l1y":18,"l2x":27,"l2y":39,"l3x":33,"l3y":18,"l4x":29,"l4y":19,"l5x":27,"l5y":39},{"data":1000010,"id":500000156,"lvl":7,"x":17,"y":8,"l1x":14,"l1y":17,"l2x":26,"l2y":39,"l3x":32,"l3y":18,"l4x":32,"l4y":19,"l5x":26,"l5y":39},{"data":1000010,"id":500000157,"lvl":7,"x":16,"y":8,"l1x":14,"l1y":16,"l2x":25,"l2y":39,"l3x":31,"l3y":18,"l4x":31,"l4y":19,"l5x":25,"l5y":39},{"data":1000010,"id":500000158,"lvl":7,"x":16,"y":34,"l1x":14,"l1y":15,"l2x":39,"l2y":33,"l3x":33,"l3y":19,"l4x":33,"l4y":19,"l5x":39,"l5y":33},{"data":1000010,"id":500000159,"lvl":7,"x":8,"y":25,"l1x":14,"l1y":14,"l2x":39,"l2y":27,"l3x":33,"l3y":20,"l4x":33,"l4y":20,"l5x":39,"l5y":27},{"data":1000010,"id":500000160,"lvl":7,"x":8,"y":24,"l1x":14,"l1y":13,"l2x":39,"l2y":28,"l3x":33,"l3y":21,"l4x":33,"l4y":21,"l5x":39,"l5y":28},{"data":1000010,"id":500000161,"lvl":7,"x":8,"y":23,"l1x":14,"l1y":12,"l2x":39,"l2y":29,"l3x":33,"l3y":22,"l4x":33,"l4y":22,"l5x":39,"l5y":29},{"data":1000010,"id":500000162,"lvl":7,"x":8,"y":22,"l1x":14,"l1y":11,"l2x":22,"l2y":32,"l3x":33,"l3y":23,"l4x":33,"l4y":23,"l5x":22,"l5y":32},{"data":1000010,"id":500000163,"lvl":7,"x":8,"y":21,"l1x":15,"l1y":11,"l2x":22,"l2y":31,"l3x":33,"l3y":24,"l4x":33,"l4y":24,"l5x":22,"l5y":31},{"data":1000010,"id":500000164,"lvl":7,"x":8,"y":20,"l1x":16,"l1y":11,"l2x":23,"l2y":32,"l3x":33,"l3y":25,"l4x":33,"l4y":25,"l5x":23,"l5y":32},{"data":1000011,"id":500000165,"lvl":5,"x":36,"y":30,"l1x":31,"l1y":35,"l2x":17,"l2y":19,"l3x":19,"l3y":19,"l4x":38,"l4y":33,"l5x":17,"l5y":19},{"data":1000015,"id":500000166,"lvl":0,"x":39,"y":27,"l1x":23,"l1y":40,"l2x":26,"l2y":25,"l3x":25,"l3y":8,"l4x":25,"l4y":8,"l5x":27,"l5y":25},{"data":1000024,"id":500000167,"lvl":3,"x":22,"y":18,"l1x":21,"l1y":24,"l2x":32,"l2y":34,"l3x":24,"l3y":29,"l4x":14,"l4y":26,"l5x":32,"l5y":33},{"data":1000006,"id":500000168,"lvl":9,"x":10,"y":21,"unit_prod":{"unit_type":0,"t":0,"slots":[{"id":4000008,"cnt":1}]},"l1x":37,"l1y":39,"l2x":9,"l2y":18,"l3x":10,"l3y":27,"l4x":10,"l4y":27,"l5x":9,"l5y":18},{"data":1000000,"id":500000169,"lvl":5,"x":26,"y":12,"units":[[4000008,3]],"l1x":18,"l1y":37,"l2x":40,"l2y":29,"l3x":7,"l3y":31,"l4x":7,"l4y":31,"l5x":40,"l5y":29},{"data":1000010,"id":500000170,"lvl":7,"x":8,"y":19,"l1x":17,"l1y":11,"l2x":23,"l2y":34,"l3x":33,"l3y":26,"l4x":33,"l4y":26,"l5x":24,"l5y":17},{"data":1000010,"id":500000171,"lvl":7,"x":8,"y":18,"l1x":18,"l1y":11,"l2x":25,"l2y":32,"l3x":30,"l3y":18,"l4x":30,"l4y":19,"l5x":25,"l5y":32},{"data":1000010,"id":500000172,"lvl":7,"x":8,"y":17,"l1x":33,"l1y":14,"l2x":26,"l2y":32,"l3x":29,"l3y":18,"l4x":29,"l4y":18,"l5x":26,"l5y":32},{"data":1000010,"id":500000173,"lvl":7,"x":8,"y":16,"l1x":19,"l1y":12,"l2x":27,"l2y":32,"l3x":28,"l3y":18,"l4x":28,"l4y":18,"l5x":27,"l5y":32},{"data":1000010,"id":500000174,"lvl":7,"x":9,"y":16,"l1x":20,"l1y":12,"l2x":28,"l2y":32,"l3x":27,"l3y":18,"l4x":27,"l4y":18,"l5x":28,"l5y":32},{"data":1000010,"id":500000175,"lvl":7,"x":10,"y":16,"l1x":21,"l1y":12,"l2x":29,"l2y":32,"l3x":26,"l3y":18,"l4x":26,"l4y":18,"l5x":29,"l5y":32},{"data":1000010,"id":500000176,"lvl":7,"x":11,"y":16,"l1x":22,"l1y":12,"l2x":30,"l2y":32,"l3x":25,"l3y":18,"l4x":25,"l4y":18,"l5x":30,"l5y":32},{"data":1000010,"id":500000177,"lvl":7,"x":12,"y":16,"l1x":22,"l1y":13,"l2x":31,"l2y":32,"l3x":22,"l3y":17,"l4x":22,"l4y":17,"l5x":31,"l5y":32},{"data":1000010,"id":500000178,"lvl":7,"x":16,"y":9,"l1x":22,"l1y":14,"l2x":32,"l2y":32,"l3x":24,"l3y":17,"l4x":24,"l4y":17,"l5x":32,"l5y":32},{"data":1000010,"id":500000179,"lvl":7,"x":16,"y":10,"l1x":22,"l1y":15,"l2x":33,"l2y":32,"l3x":22,"l3y":20,"l4x":19,"l4y":21,"l5x":33,"l5y":32},{"data":1000010,"id":500000180,"lvl":7,"x":16,"y":11,"l1x":22,"l1y":18,"l2x":34,"l2y":32,"l3x":22,"l3y":19,"l4x":19,"l4y":20,"l5x":34,"l5y":32},{"data":1000010,"id":500000181,"lvl":7,"x":14,"y":9,"l1x":22,"l1y":17,"l2x":35,"l2y":32,"l3x":22,"l3y":21,"l4x":19,"l4y":19,"l5x":35,"l5y":32},{"data":1000010,"id":500000182,"lvl":7,"x":9,"y":14,"l1x":15,"l1y":19,"l2x":35,"l2y":31,"l3x":21,"l3y":37,"l4x":21,"l4y":37,"l5x":35,"l5y":31},{"data":1000010,"id":500000183,"lvl":7,"x":16,"y":12,"l1x":20,"l1y":25,"l2x":35,"l2y":30,"l3x":21,"l3y":36,"l4x":21,"l4y":36,"l5x":35,"l5y":30},{"data":1000010,"id":500000184,"lvl":7,"x":8,"y":12,"l1x":29,"l1y":30,"l2x":35,"l2y":29,"l3x":20,"l3y":36,"l4x":20,"l4y":36,"l5x":35,"l5y":29},{"data":1000010,"id":500000185,"lvl":7,"x":9,"y":9,"l1x":18,"l1y":19,"l2x":35,"l2y":28,"l3x":19,"l3y":36,"l4x":19,"l4y":36,"l5x":35,"l5y":28},{"data":1000010,"id":500000186,"lvl":7,"x":13,"y":14,"l1x":30,"l1y":32,"l2x":39,"l2y":31,"l3x":18,"l3y":36,"l4x":18,"l4y":36,"l5x":39,"l5y":31},{"data":1000010,"id":500000187,"lvl":7,"x":13,"y":13,"l1x":29,"l1y":27,"l2x":35,"l2y":27,"l3x":17,"l3y":36,"l4x":17,"l4y":36,"l5x":35,"l5y":27},{"data":1000010,"id":500000188,"lvl":7,"x":14,"y":13,"l1x":29,"l1y":31,"l2x":17,"l2y":23,"l3x":16,"l3y":36,"l4x":16,"l4y":36,"l5x":17,"l5y":23},{"data":1000010,"id":500000189,"lvl":7,"x":15,"y":13,"l1x":29,"l1y":26,"l2x":17,"l2y":24,"l3x":15,"l3y":36,"l4x":15,"l4y":36,"l5x":17,"l5y":24},{"data":1000010,"id":500000190,"lvl":7,"x":16,"y":13,"l1x":18,"l1y":12,"l2x":17,"l2y":25,"l3x":15,"l3y":35,"l4x":15,"l4y":35,"l5x":17,"l5y":25},{"data":1000010,"id":500000191,"lvl":7,"x":17,"y":13,"l1x":29,"l1y":28,"l2x":17,"l2y":26,"l3x":15,"l3y":34,"l4x":15,"l4y":34,"l5x":17,"l5y":26},{"data":1000010,"id":500000192,"lvl":7,"x":18,"y":13,"l1x":29,"l1y":29,"l2x":17,"l2y":27,"l3x":15,"l3y":33,"l4x":15,"l4y":33,"l5x":17,"l5y":27},{"data":1000010,"id":500000193,"lvl":7,"x":19,"y":13,"l1x":20,"l1y":24,"l2x":17,"l2y":28,"l3x":15,"l3y":32,"l4x":15,"l4y":32,"l5x":17,"l5y":28},{"data":1000010,"id":500000194,"lvl":7,"x":13,"y":15,"l1x":22,"l1y":33,"l2x":17,"l2y":29,"l3x":13,"l3y":23,"l4x":13,"l4y":23,"l5x":17,"l5y":29},{"data":1000010,"id":500000195,"lvl":7,"x":13,"y":16,"l1x":20,"l1y":31,"l2x":39,"l2y":30,"l3x":13,"l3y":24,"l4x":13,"l4y":24,"l5x":39,"l5y":30},{"data":1000010,"id":500000196,"lvl":7,"x":13,"y":17,"l1x":14,"l1y":30,"l2x":17,"l2y":30,"l3x":13,"l3y":25,"l4x":13,"l4y":25,"l5x":17,"l5y":30},{"data":1000010,"id":500000197,"lvl":7,"x":13,"y":18,"l1x":14,"l1y":29,"l2x":18,"l2y":30,"l3x":13,"l3y":26,"l4x":13,"l4y":26,"l5x":18,"l5y":30},{"data":1000010,"id":500000198,"lvl":7,"x":13,"y":19,"l1x":14,"l1y":28,"l2x":19,"l2y":30,"l3x":13,"l3y":27,"l4x":13,"l4y":27,"l5x":19,"l5y":30},{"data":1000010,"id":500000199,"lvl":7,"x":20,"y":13,"l1x":14,"l1y":27,"l2x":20,"l2y":30,"l3x":13,"l3y":28,"l4x":13,"l4y":28,"l5x":20,"l5y":30},{"data":1000010,"id":500000200,"lvl":7,"x":21,"y":13,"l1x":14,"l1y":33,"l2x":21,"l2y":30,"l3x":13,"l3y":29,"l4x":13,"l4y":29,"l5x":21,"l5y":30},{"data":1000010,"id":500000201,"lvl":7,"x":22,"y":13,"l1x":23,"l1y":33,"l2x":22,"l2y":30,"l3x":13,"l3y":30,"l4x":13,"l4y":30,"l5x":22,"l5y":30},{"data":1000010,"id":500000202,"lvl":7,"x":23,"y":13,"l1x":14,"l1y":31,"l2x":22,"l2y":29,"l3x":13,"l3y":31,"l4x":13,"l4y":31,"l5x":22,"l5y":29},{"data":1000010,"id":500000203,"lvl":7,"x":24,"y":13,"l1x":16,"l1y":35,"l2x":22,"l2y":28,"l3x":24,"l3y":23,"l4x":24,"l4y":23,"l5x":22,"l5y":28},{"data":1000010,"id":500000204,"lvl":7,"x":13,"y":20,"l1x":15,"l1y":35,"l2x":16,"l2y":17,"l3x":24,"l3y":22,"l4x":24,"l4y":22,"l5x":16,"l5y":17},{"data":1000010,"id":500000205,"lvl":7,"x":13,"y":21,"l1x":14,"l1y":32,"l2x":17,"l2y":17,"l3x":23,"l3y":22,"l4x":23,"l4y":22,"l5x":17,"l5y":17},{"data":1000010,"id":500000206,"lvl":7,"x":13,"y":22,"l1x":20,"l1y":27,"l2x":18,"l2y":17,"l3x":22,"l3y":22,"l4x":22,"l4y":22,"l5x":18,"l5y":17},{"data":1000010,"id":500000207,"lvl":7,"x":13,"y":23,"l1x":17,"l1y":27,"l2x":19,"l2y":17,"l3x":21,"l3y":22,"l4x":21,"l4y":22,"l5x":19,"l5y":17},{"data":1000010,"id":500000208,"lvl":7,"x":13,"y":24,"l1x":18,"l1y":27,"l2x":20,"l2y":17,"l3x":20,"l3y":22,"l4x":20,"l4y":22,"l5x":20,"l5y":17},{"data":1000010,"id":500000209,"lvl":7,"x":13,"y":25,"l1x":30,"l1y":34,"l2x":21,"l2y":17,"l3x":19,"l3y":22,"l4x":19,"l4y":22,"l5x":21,"l5y":17},{"data":1000010,"id":500000210,"lvl":7,"x":14,"y":25,"l1x":33,"l1y":12,"l2x":22,"l2y":17,"l3x":18,"l3y":22,"l4x":18,"l4y":22,"l5x":22,"l5y":17},{"data":1000010,"id":500000211,"lvl":7,"x":25,"y":13,"l1x":36,"l1y":21,"l2x":23,"l2y":17,"l3x":17,"l3y":22,"l4x":17,"l4y":22,"l5x":14,"l5y":20},{"data":1000010,"id":500000212,"lvl":7,"x":25,"y":14,"l1x":30,"l1y":36,"l2x":25,"l2y":17,"l3x":17,"l3y":23,"l4x":17,"l4y":23,"l5x":25,"l5y":17},{"data":1000010,"id":500000213,"lvl":7,"x":25,"y":15,"l1x":29,"l1y":13,"l2x":24,"l2y":17,"l3x":17,"l3y":24,"l4x":17,"l4y":24,"l5x":23,"l5y":17},{"data":1000010,"id":500000214,"lvl":7,"x":25,"y":16,"l1x":15,"l1y":20,"l2x":25,"l2y":18,"l3x":14,"l3y":31,"l4x":14,"l4y":31,"l5x":25,"l5y":18},{"data":1000010,"id":500000215,"lvl":7,"x":25,"y":17,"l1x":22,"l1y":16,"l2x":25,"l2y":19,"l3x":17,"l3y":25,"l4x":17,"l4y":25,"l5x":25,"l5y":19},{"data":1000010,"id":500000216,"lvl":7,"x":26,"y":17,"l1x":14,"l1y":35,"l2x":25,"l2y":21,"l3x":17,"l3y":26,"l4x":17,"l4y":26,"l5x":25,"l5y":21},{"data":1000010,"id":500000217,"lvl":7,"x":27,"y":17,"l1x":33,"l1y":33,"l2x":25,"l2y":20,"l3x":17,"l3y":27,"l4x":17,"l4y":27,"l5x":25,"l5y":20},{"data":1000010,"id":500000218,"lvl":7,"x":28,"y":17,"l1x":18,"l1y":18,"l2x":16,"l2y":18,"l3x":17,"l3y":28,"l4x":17,"l4y":28,"l5x":16,"l5y":18},{"data":1000010,"id":500000219,"lvl":7,"x":29,"y":17,"l1x":28,"l1y":38,"l2x":16,"l2y":19,"l3x":17,"l3y":29,"l4x":17,"l4y":29,"l5x":16,"l5y":19},{"data":1000008,"id":500000220,"lvl":9,"x":18,"y":31,"l1x":19,"l1y":13,"l2x":36,"l2y":21,"l3x":31,"l3y":15,"l4x":31,"l4y":16,"l5x":36,"l5y":21},{"data":1000008,"id":500000221,"lvl":9,"x":31,"y":18,"l1x":33,"l1y":15,"l2x":19,"l2y":36,"l3x":10,"l3y":24,"l4x":10,"l4y":24,"l5x":19,"l5y":36},{"data":1000009,"id":500000222,"lvl":9,"x":24,"y":21,"l1x":26,"l1y":14,"l2x":25,"l2y":36,"l3x":12,"l3y":19,"l4x":12,"l4y":19,"l5x":25,"l5y":36},{"data":1000013,"id":500000225,"lvl":5,"x":33,"y":27,"l1x":15,"l1y":32,"l2x":15,"l2y":35,"l3x":19,"l3y":32,"l4x":19,"l4y":32,"l5x":15,"l5y":35},{"data":1000012,"id":500000226,"lvl":5,"x":30,"y":27,"l1x":26,"l1y":28,"l2x":19,"l2y":27,"l3x":18,"l3y":28,"l4x":18,"l4y":28,"l5x":19,"l5y":27},{"data":1000022,"id":500000227,"lvl":0,"x":34,"y":34,"l1x":30,"l1y":29,"l2x":26,"l2y":19,"l3x":21,"l3y":23,"l4x":21,"l4y":23,"l5x":26,"l5y":18},{"data":1000026,"id":500000228,"lvl":3,"x":13,"y":10,"unit_prod":{"unit_type":0},"l1x":34,"l1y":39,"l2x":28,"l2y":40,"l3x":11,"l3y":14,"l4x":11,"l4y":14,"l5x":28,"l5y":40},{"data":1000023,"id":500000229,"lvl":2,"x":22,"y":15,"res_time":0,"l1x":11,"l1y":34,"l2x":31,"l2y":41,"l3x":15,"l3y":37,"l4x":15,"l4y":37,"l5x":31,"l5y":41},{"data":1000028,"id":500000230,"lvl":3,"x":24,"y":24,"aim_angle":45,"aim_angle_draft":45,"aim_angle_war":45,"aim_angle_draft_war":45,"aim_angle2":135,"aim_angle_d2":135,"aim_angle3":45,"aim_angle_d3":45,"aim_angle4":225,"aim_angle_d4":225,"aim_angle5":45,"aim_angle_d5":45,"l1x":29,"l1y":23,"l2x":30,"l2y":17,"l3x":30,"l3y":24,"l4x":31,"l4y":24,"l5x":31,"l5y":16},{"data":1000003,"id":500000231,"lvl":10,"x":36,"y":27,"l1x":27,"l1y":33,"l2x":18,"l2y":32,"l3x":34,"l3y":29,"l4x":34,"l4y":29,"l5x":19,"l5y":32},{"data":1000005,"id":500000232,"lvl":10,"x":27,"y":36,"l1x":32,"l1y":18,"l2x":36,"l2y":27,"l3x":14,"l3y":26,"l4x":24,"l4y":29,"l5x":36,"l5y":27},{"data":1000009,"id":500000233,"lvl":9,"x":21,"y":24,"l1x":36,"l1y":29,"l2x":36,"l2y":31,"l3x":12,"l3y":32,"l4x":12,"l4y":32,"l5x":36,"l5y":31},{"data":1000011,"id":500000234,"lvl":5,"x":31,"y":36,"l1x":30,"l1y":13,"l2x":30,"l2y":23,"l3x":38,"l3y":33,"l4x":20,"l4y":19,"l5x":31,"l5y":22},{"data":1000013,"id":500000235,"lvl":5,"x":27,"y":33,"l1x":21,"l1y":28,"l2x":19,"l2y":9,"l3x":33,"l3y":35,"l4x":33,"l4y":35,"l5x":19,"l5y":9},{"data":1000012,"id":500000236,"lvl":5,"x":18,"y":18,"l1x":31,"l1y":22,"l2x":22,"l2y":18,"l3x":29,"l3y":27,"l4x":29,"l4y":27,"l5x":22,"l5y":18},{"data":1000010,"id":500000237,"lvl":7,"x":30,"y":17,"l1x":30,"l1y":37,"l2x":16,"l2y":20,"l3x":17,"l3y":30,"l4x":17,"l4y":30,"l5x":16,"l5y":20},{"data":1000010,"id":500000238,"lvl":7,"x":31,"y":17,"l1x":30,"l1y":33,"l2x":16,"l2y":22,"l3x":16,"l3y":31,"l4x":16,"l4y":31,"l5x":16,"l5y":22},{"data":1000010,"id":500000239,"lvl":7,"x":32,"y":17,"l1x":38,"l1y":33,"l2x":16,"l2y":21,"l3x":17,"l3y":31,"l4x":17,"l4y":31,"l5x":16,"l5y":21},{"data":1000010,"id":500000240,"lvl":7,"x":33,"y":17,"l1x":31,"l1y":32,"l2x":17,"l2y":22,"l3x":18,"l3y":31,"l4x":18,"l4y":31,"l5x":17,"l5y":22},{"data":1000010,"id":500000241,"lvl":7,"x":15,"y":25,"l1x":33,"l1y":26,"l2x":18,"l2y":22,"l3x":19,"l3y":31,"l4x":19,"l4y":31,"l5x":18,"l5y":22},{"data":1000010,"id":500000242,"lvl":7,"x":16,"y":25,"l1x":33,"l1y":27,"l2x":19,"l2y":22,"l3x":20,"l3y":31,"l4x":20,"l4y":31,"l5x":19,"l5y":22},{"data":1000010,"id":500000243,"lvl":7,"x":17,"y":25,"l1x":33,"l1y":28,"l2x":20,"l2y":22,"l3x":22,"l3y":31,"l4x":22,"l4y":31,"l5x":20,"l5y":22},{"data":1000010,"id":500000244,"lvl":7,"x":17,"y":26,"l1x":33,"l1y":29,"l2x":21,"l2y":22,"l3x":23,"l3y":28,"l4x":23,"l4y":28,"l5x":21,"l5y":22},{"data":1000010,"id":500000245,"lvl":7,"x":17,"y":27,"l1x":33,"l1y":30,"l2x":31,"l2y":22,"l3x":23,"l3y":29,"l4x":23,"l4y":29,"l5x":32,"l5y":21},{"data":1000010,"id":500000246,"lvl":7,"x":17,"y":28,"l1x":33,"l1y":31,"l2x":32,"l2y":22,"l3x":24,"l3y":28,"l4x":24,"l4y":28,"l5x":29,"l5y":21},{"data":1000010,"id":500000247,"lvl":7,"x":17,"y":29,"l1x":33,"l1y":32,"l2x":33,"l2y":22,"l3x":31,"l3y":26,"l4x":31,"l4y":26,"l5x":34,"l5y":21},{"data":1000010,"id":500000248,"lvl":7,"x":17,"y":30,"l1x":32,"l1y":32,"l2x":33,"l2y":23,"l3x":23,"l3y":31,"l4x":23,"l4y":31,"l5x":35,"l5y":22},{"data":1000010,"id":500000249,"lvl":7,"x":17,"y":31,"l1x":22,"l1y":36,"l2x":33,"l2y":24,"l3x":30,"l3y":26,"l4x":30,"l4y":26,"l5x":35,"l5y":23},{"data":1000010,"id":500000250,"lvl":7,"x":17,"y":32,"l1x":16,"l1y":27,"l2x":33,"l2y":25,"l3x":26,"l3y":32,"l4x":26,"l4y":32,"l5x":35,"l5y":24},{"data":1000010,"id":500000251,"lvl":7,"x":17,"y":33,"l1x":15,"l1y":27,"l2x":33,"l2y":26,"l3x":27,"l3y":32,"l4x":27,"l4y":32,"l5x":32,"l5y":26},{"data":1000010,"id":500000252,"lvl":7,"x":17,"y":34,"l1x":16,"l1y":19,"l2x":34,"l2y":27,"l3x":28,"l3y":32,"l4x":28,"l4y":32,"l5x":35,"l5y":25},{"data":1000010,"id":500000253,"lvl":7,"x":34,"y":17,"l1x":38,"l1y":32,"l2x":33,"l2y":27,"l3x":29,"l3y":32,"l4x":29,"l4y":32,"l5x":31,"l5y":26},{"data":1000010,"id":500000254,"lvl":7,"x":34,"y":18,"l1x":20,"l1y":28,"l2x":32,"l2y":27,"l3x":30,"l3y":32,"l4x":30,"l4y":32,"l5x":33,"l5y":26},{"data":1000010,"id":500000255,"lvl":7,"x":34,"y":20,"l1x":20,"l1y":29,"l2x":31,"l2y":27,"l3x":32,"l3y":32,"l4x":32,"l4y":32,"l5x":30,"l5y":26},{"data":1000010,"id":500000256,"lvl":7,"x":34,"y":19,"l1x":20,"l1y":30,"l2x":30,"l2y":27,"l3x":32,"l3y":31,"l4x":32,"l4y":31,"l5x":29,"l5y":26},{"data":1000010,"id":500000257,"lvl":7,"x":34,"y":21,"l1x":14,"l1y":34,"l2x":29,"l2y":27,"l3x":32,"l3y":30,"l4x":32,"l4y":30,"l5x":29,"l5y":27},{"data":1000010,"id":500000258,"lvl":7,"x":33,"y":21,"l1x":19,"l1y":27,"l2x":30,"l2y":22,"l3x":32,"l3y":29,"l4x":32,"l4y":29,"l5x":31,"l5y":21},{"data":1000010,"id":500000259,"lvl":7,"x":18,"y":34,"l1x":21,"l1y":36,"l2x":29,"l2y":22,"l3x":32,"l3y":28,"l4x":32,"l4y":28,"l5x":28,"l5y":21},{"data":1000010,"id":500000260,"lvl":7,"x":19,"y":34,"l1x":23,"l1y":39,"l2x":28,"l2y":22,"l3x":32,"l3y":27,"l4x":32,"l4y":27,"l5x":28,"l5y":22},{"data":1000010,"id":500000261,"lvl":7,"x":20,"y":34,"l1x":23,"l1y":38,"l2x":27,"l2y":22,"l3x":32,"l3y":26,"l4x":32,"l4y":26,"l5x":27,"l5y":22},{"data":1000010,"id":500000262,"lvl":7,"x":21,"y":34,"l1x":23,"l1y":37,"l2x":26,"l2y":22,"l3x":28,"l3y":25,"l4x":28,"l4y":25,"l5x":26,"l5y":22},{"data":1000010,"id":500000263,"lvl":7,"x":21,"y":33,"l1x":23,"l1y":36,"l2x":25,"l2y":22,"l3x":31,"l3y":32,"l4x":31,"l4y":32,"l5x":25,"l5y":22},{"data":1000010,"id":500000264,"lvl":7,"x":21,"y":32,"l1x":23,"l1y":34,"l2x":24,"l2y":22,"l3x":29,"l3y":26,"l4x":29,"l4y":26,"l5x":24,"l5y":22},{"data":1000010,"id":500000265,"lvl":7,"x":32,"y":21,"l1x":23,"l1y":35,"l2x":23,"l2y":22,"l3x":28,"l3y":26,"l4x":28,"l4y":26,"l5x":23,"l5y":22},{"data":1000010,"id":500000266,"lvl":7,"x":31,"y":21,"l1x":17,"l1y":35,"l2x":22,"l2y":22,"l3x":23,"l3y":30,"l4x":23,"l4y":30,"l5x":22,"l5y":22},{"data":1000010,"id":500000267,"lvl":7,"x":30,"y":21,"l1x":17,"l1y":36,"l2x":22,"l2y":23,"l3x":27,"l3y":24,"l4x":27,"l4y":24,"l5x":22,"l5y":23},{"data":1000010,"id":500000268,"lvl":7,"x":29,"y":21,"l1x":18,"l1y":36,"l2x":22,"l2y":24,"l3x":26,"l3y":24,"l4x":26,"l4y":24,"l5x":22,"l5y":24},{"data":1000010,"id":500000269,"lvl":7,"x":28,"y":21,"l1x":18,"l1y":13,"l2x":22,"l2y":25,"l3x":25,"l3y":24,"l4x":25,"l4y":24,"l5x":22,"l5y":25},{"data":1000010,"id":500000270,"lvl":7,"x":21,"y":31,"l1x":18,"l1y":14,"l2x":22,"l2y":26,"l3x":24,"l3y":24,"l4x":24,"l4y":24,"l5x":22,"l5y":26},{"data":1000010,"id":500000271,"lvl":7,"x":21,"y":30,"l1x":18,"l1y":15,"l2x":28,"l2y":27,"l3x":24,"l3y":25,"l4x":24,"l4y":25,"l5x":28,"l5y":27},{"data":1000010,"id":500000272,"lvl":7,"x":21,"y":28,"l1x":18,"l1y":16,"l2x":27,"l2y":27,"l3x":24,"l3y":26,"l4x":24,"l4y":26,"l5x":27,"l5y":27},{"data":1000010,"id":500000273,"lvl":7,"x":27,"y":21,"l1x":17,"l1y":19,"l2x":26,"l2y":27,"l3x":24,"l3y":27,"l4x":24,"l4y":27,"l5x":26,"l5y":27},{"data":1000010,"id":500000274,"lvl":7,"x":27,"y":22,"l1x":18,"l1y":17,"l2x":25,"l2y":27,"l3x":23,"l3y":32,"l4x":23,"l4y":32,"l5x":25,"l5y":27},{"data":1000010,"id":500000275,"lvl":7,"x":27,"y":23,"l1x":19,"l1y":36,"l2x":24,"l2y":27,"l3x":24,"l3y":32,"l4x":24,"l4y":32,"l5x":24,"l5y":27},{"data":1000010,"id":500000276,"lvl":7,"x":27,"y":24,"l1x":30,"l1y":38,"l2x":23,"l2y":27,"l3x":39,"l3y":28,"l4x":39,"l4y":28,"l5x":23,"l5y":27},{"data":1000010,"id":500000277,"lvl":7,"x":21,"y":29,"l1x":29,"l1y":32,"l2x":39,"l2y":26,"l3x":21,"l3y":31,"l4x":21,"l4y":31,"l5x":39,"l5y":26},{"data":1000010,"id":500000278,"lvl":7,"x":22,"y":27,"l1x":28,"l1y":32,"l2x":40,"l2y":23,"l3x":29,"l3y":37,"l4x":29,"l4y":37,"l5x":40,"l5y":23},{"data":1000010,"id":500000279,"lvl":7,"x":26,"y":24,"l1x":27,"l1y":32,"l2x":39,"l2y":32,"l3x":36,"l3y":37,"l4x":36,"l4y":37,"l5x":39,"l5y":32},{"data":1000010,"id":500000280,"lvl":7,"x":26,"y":25,"l1x":26,"l1y":32,"l2x":22,"l2y":27,"l3x":40,"l3y":24,"l4x":40,"l4y":24,"l5x":22,"l5y":27},{"data":1000010,"id":500000281,"lvl":7,"x":24,"y":27,"l1x":25,"l1y":32,"l2x":24,"l2y":39,"l3x":28,"l3y":24,"l4x":28,"l4y":24,"l5x":24,"l5y":39},{"data":1000010,"id":500000282,"lvl":7,"x":23,"y":27,"l1x":24,"l1y":32,"l2x":31,"l2y":11,"l3x":25,"l3y":11,"l4x":25,"l4y":11,"l5x":31,"l5y":11},{"data":1000010,"id":500000283,"lvl":7,"x":26,"y":26,"l1x":23,"l1y":32,"l2x":40,"l2y":26,"l3x":17,"l3y":17,"l4x":17,"l4y":17,"l5x":40,"l5y":26},{"data":1000010,"id":500000284,"lvl":7,"x":25,"y":26,"l1x":21,"l1y":33,"l2x":40,"l2y":24,"l3x":13,"l3y":22,"l4x":13,"l4y":22,"l5x":40,"l5y":24},{"data":1000010,"id":500000285,"lvl":7,"x":24,"y":26,"l1x":20,"l1y":33,"l2x":40,"l2y":25,"l3x":15,"l3y":31,"l4x":15,"l4y":31,"l5x":40,"l5y":25},{"data":1000010,"id":500000286,"lvl":7,"x":21,"y":27,"l1x":20,"l1y":32,"l2x":24,"l2y":38,"l3x":25,"l3y":32,"l4x":25,"l4y":32,"l5x":24,"l5y":38},{"data":1000026,"id":500000288,"lvl":3,"x":10,"y":13,"unit_prod":{"unit_type":0},"l1x":40,"l1y":30,"l2x":38,"l2y":39,"l3x":38,"l3y":36,"l4x":38,"l4y":36,"l5x":38,"l5y":39},{"data":1000023,"id":500000289,"lvl":2,"x":15,"y":22,"res_time":0,"l1x":11,"l1y":37,"l2x":23,"l2y":10,"l3x":12,"l3y":35,"l4x":12,"l4y":35,"l5x":22,"l5y":11},{"data":1000029,"id":500000290,"lvl":1,"x":10,"y":10,"units":[[26000010,1]],"unit_prod":{"unit_type":1},"l1x":29,"l1y":9,"l2x":6,"l2y":23,"l3x":19,"l3y":40,"l4x":19,"l4y":40,"l5x":6,"l5y":23}],"obstacles":[{"data":8000004,"id":503000000,"x":47,"y":10,"loot_multiply_ver":2},{"data":8000007,"id":503000001,"x":1,"y":13,"loot_multiply_ver":2},{"data":8000000,"id":503000002,"x":14,"y":0,"loot_multiply_ver":2},{"data":8000008,"id":503000003,"x":0,"y":34,"loot_multiply_ver":2},{"data":8000006,"id":503000004,"x":29,"y":0,"loot_multiply_ver":2},{"data":8000007,"id":503000005,"x":19,"y":0,"loot_multiply_ver":2},{"data":8000013,"id":503000006,"x":29,"y":47,"loot_multiply_ver":2},{"data":8000007,"id":503000007,"x":7,"y":8,"loot_multiply_ver":2},{"data":8000010,"id":503000008,"x":0,"y":30,"loot_multiply_ver":2},{"data":8000000,"id":503000009,"x":25,"y":0,"loot_multiply_ver":2},{"data":8000004,"id":503000010,"x":24,"y":48,"loot_multiply_ver":2},{"data":8000000,"id":503000011,"x":8,"y":48,"loot_multiply_ver":2},{"data":8000010,"id":503000012,"x":8,"y":42,"loot_multiply_ver":2},{"data":8000000,"id":503000013,"x":1,"y":9,"loot_multiply_ver":2},{"data":8000000,"id":503000014,"x":2,"y":40,"loot_multiply_ver":2},{"data":8000004,"id":503000015,"x":35,"y":0,"loot_multiply_ver":2},{"data":8000007,"id":503000016,"x":1,"y":37,"loot_multiply_ver":2},{"data":8000007,"id":503000017,"x":15,"y":47,"loot_multiply_ver":2},{"data":8000007,"id":503000018,"x":1,"y":19,"loot_multiply_ver":2},{"data":8000000,"id":503000019,"x":0,"y":26,"loot_multiply_ver":2},{"data":8000013,"id":503000020,"x":18,"y":47,"loot_multiply_ver":2},{"data":8000007,"id":503000021,"x":48,"y":17,"loot_multiply_ver":2},{"data":8000010,"id":503000022,"x":48,"y":31,"loot_multiply_ver":2},{"data":8000004,"id":503000023,"x":11,"y":48,"loot_multiply_ver":2},{"data":8000000,"id":503000024,"x":48,"y":25,"loot_multiply_ver":2},{"data":8000008,"id":503000025,"x":48,"y":7,"loot_multiply_ver":2},{"data":8000004,"id":503000026,"x":46,"y":0,"loot_multiply_ver":2},{"data":8000007,"id":503000027,"x":48,"y":34,"loot_multiply_ver":2},{"data":8000010,"id":503000028,"x":1,"y":2,"loot_multiply_ver":2},{"data":8000004,"id":503000029,"x":0,"y":47,"loot_multiply_ver":2},{"data":8000000,"id":503000030,"x":48,"y":38,"loot_multiply_ver":2},{"data":8000010,"id":503000031,"x":1,"y":16,"loot_multiply_ver":2},{"data":8000000,"id":503000032,"x":1,"y":23,"loot_multiply_ver":2},{"data":8000006,"id":503000033,"x":39,"y":48,"loot_multiply_ver":2},{"data":8000036,"id":503000034,"x":48,"y":20,"loot_multiply_ver":2},{"data":8000006,"id":503000035,"x":47,"y":41,"loot_multiply_ver":2},{"data":8000000,"id":503000036,"x":47,"y":28,"loot_multiply_ver":2},{"data":8000007,"id":503000037,"x":33,"y":48,"loot_multiply_ver":2},{"data":8000005,"id":503000038,"x":7,"y":1,"loot_multiply_ver":2},{"data":8000036,"id":503000039,"x":41,"y":10,"loot_multiply_ver":2},{"data":8000036,"id":503000040,"x":7,"y":39,"loot_multiply_ver":1},{"data":8000036,"id":503000041,"x":48,"y":13,"loot_multiply_ver":2},{"data":8000036,"id":503000042,"x":22,"y":2,"loot_multiply_ver":2},{"data":8000036,"id":503000043,"x":42,"y":6,"loot_multiply_ver":2},{"data":8000036,"id":503000044,"x":8,"y":33,"loot_multiply_ver":1},{"data":8000037,"id":503000078,"x":36,"y":48,"loot_multiply_ver":2},{"data":8000037,"id":503000119,"x":44,"y":14,"loot_multiply_ver":2},{"data":8000037,"id":503000160,"x":36,"y":3,"loot_multiply_ver":2},{"data":8000037,"id":503000201,"x":3,"y":5,"loot_multiply_ver":2},{"data":8000037,"id":503000242,"x":17,"y":41,"loot_multiply_ver":2},{"data":8000037,"id":503000243,"x":42,"y":21,"loot_multiply_ver":2},{"data":8000037,"id":503000284,"x":40,"y":1,"loot_multiply_ver":2},{"data":8000037,"id":503000305,"x":32,"y":4,"loot_multiply_ver":2},{"data":8000009,"id":503000306,"x":40,"y":22,"loot_multiply_ver":1},{"data":8000009,"id":503000307,"x":44,"y":41,"loot_multiply_ver":1},{"data":8000009,"id":503000308,"x":43,"y":28,"loot_multiply_ver":1},{"data":8000009,"id":503000309,"x":43,"y":26,"loot_multiply_ver":1},{"data":8000009,"id":503000310,"x":44,"y":24,"loot_multiply_ver":1},{"data":8000023,"id":503000311,"x":43,"y":25,"loot_multiply_ver":1},{"data":8000023,"id":503000312,"x":44,"y":25,"loot_multiply_ver":1},{"data":8000023,"id":503000313,"x":43,"y":24,"loot_multiply_ver":1},{"data":8000023,"id":503000314,"x":42,"y":25,"loot_multiply_ver":1},{"data":8000023,"id":503000315,"x":44,"y":26,"loot_multiply_ver":1},{"data":8000009,"id":503000316,"x":33,"y":45,"loot_multiply_ver":1},{"data":8000009,"id":503000317,"x":34,"y":44,"loot_multiply_ver":1},{"data":8000009,"id":503000318,"x":38,"y":33,"loot_multiply_ver":1},{"data":8000009,"id":503000319,"x":41,"y":25,"loot_multiply_ver":1},{"data":8000009,"id":503000320,"x":42,"y":24,"loot_multiply_ver":1},{"data":8000009,"id":503000321,"x":42,"y":29,"loot_multiply_ver":1},{"data":8000009,"id":503000322,"x":41,"y":29,"loot_multiply_ver":1},{"data":8000009,"id":503000323,"x":42,"y":28,"loot_multiply_ver":1},{"data":8000009,"id":503000324,"x":43,"y":29,"loot_multiply_ver":1},{"data":8000009,"id":503000325,"x":32,"y":43,"loot_multiply_ver":1},{"data":8000009,"id":503000326,"x":31,"y":43,"loot_multiply_ver":1},{"data":8000009,"id":503000327,"x":30,"y":43,"loot_multiply_ver":1},{"data":8000009,"id":503000328,"x":33,"y":43,"loot_multiply_ver":1},{"data":8000009,"id":503000329,"x":29,"y":42,"loot_multiply_ver":1},{"data":8000009,"id":503000330,"x":28,"y":42,"loot_multiply_ver":1},{"data":8000009,"id":503000331,"x":29,"y":43,"loot_multiply_ver":1},{"data":8000009,"id":503000332,"x":26,"y":37,"loot_multiply_ver":1},{"data":8000009,"id":503000333,"x":20,"y":39,"loot_multiply_ver":1},{"data":8000009,"id":503000334,"x":20,"y":38,"loot_multiply_ver":1},{"data":8000009,"id":503000335,"x":27,"y":25,"loot_multiply_ver":1},{"data":8000009,"id":503000336,"x":26,"y":34,"loot_multiply_ver":1},{"data":8000009,"id":503000337,"x":32,"y":22,"loot_multiply_ver":1},{"data":8000009,"id":503000338,"x":35,"y":20,"loot_multiply_ver":1},{"data":8000009,"id":503000339,"x":26,"y":35,"loot_multiply_ver":1},{"data":8000009,"id":503000340,"x":36,"y":20,"loot_multiply_ver":1},{"data":8000009,"id":503000341,"x":28,"y":26,"loot_multiply_ver":1},{"data":8000009,"id":503000342,"x":26,"y":36,"loot_multiply_ver":1},{"data":8000009,"id":503000343,"x":28,"y":25,"loot_multiply_ver":1},{"data":8000009,"id":503000344,"x":29,"y":26,"loot_multiply_ver":1},{"data":8000009,"id":503000345,"x":20,"y":35,"loot_multiply_ver":1},{"data":8000038,"id":503000346,"x":45,"y":3,"defg":117171,"defe":104847,"defde":307,"loot_multiply_ver":1}],"decos":[],"respawnVars":{"secondsFromLastRespawn":231386,"respawnSeed":1914935487,"obstacleClearCounter":5,"time_to_gembox_drop":196966,"time_in_gembox_period":228665},"cooldowns":[],"newShopBuildings":[4,0,6,3,6,3,4,1,5,5,225,3,3,4,1,5,0,0,0,3,1,0,1,2,1,0,2,0,1,1,0,0],"newShopTraps":[6,6,3,0,0,4,2,0,2],"newShopDecos":[1,4,0,1,1,4,4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],"last_league_rank":9,"last_alliance_level":7,"last_league_shuffle":1,"last_season_seen":0,"last_news_seen":221,"troop_req_msg":"max hogs and max poison","war_req_msg":"2 witches, 1 barb","war_tutorials_seen":0,"war_base":true,"account_flags":14,"bool_layout_edit_shown_erase":true} \ No newline at end of file diff --git a/src/main/resources/examples/th9-sample-1.json b/src/main/resources/examples/th9-sample-1.json new file mode 100644 index 0000000..785f9b1 --- /dev/null +++ b/src/main/resources/examples/th9-sample-1.json @@ -0,0 +1 @@ +{"wave_num":6,"exp_ver":1,"active_layout":0,"war_layout":1,"layout_state":[0,0,0,0,0,0],"buildings":[{"data":1000001,"id":500000000,"lvl":8,"x":30,"y":30,"l1x":25,"l1y":19,"l2x":13,"l2y":26,"l3x":29,"l3y":20,"l4x":29,"l4y":20,"l5x":13,"l5y":26},{"data":1000004,"id":500000001,"lvl":11,"x":22,"y":33,"res_time":87169,"l1x":11,"l1y":31,"l2x":24,"l2y":40,"l3x":41,"l3y":33,"l4x":41,"l4y":33,"l5x":24,"l5y":40},{"data":1000000,"id":500000002,"lvl":5,"x":35,"y":15,"units":[[4000008,2]],"l1x":29,"l1y":39,"l2x":38,"l2y":14,"l3x":40,"l3y":19,"l4x":40,"l4y":19,"l5x":38,"l5y":14},{"data":1000015,"id":500000003,"lvl":0,"x":41,"y":41,"l1x":14,"l1y":39,"l2x":9,"l2y":33,"l3x":36,"l3y":12,"l4x":36,"l4y":12,"l5x":41,"l5y":34},{"data":1000014,"id":500000004,"lvl":3,"x":27,"y":27,"l1x":25,"l1y":24,"l2x":23,"l2y":23,"l3x":25,"l3y":25,"l4x":25,"l4y":25,"l5x":23,"l5y":23},{"data":1000008,"id":500000005,"lvl":9,"x":37,"y":37,"l1x":36,"l1y":25,"l2x":33,"l2y":37,"l3x":34,"l3y":39,"l4x":34,"l4y":39,"l5x":33,"l5y":37},{"data":1000015,"id":500000006,"lvl":0,"x":38,"y":41,"l1x":16,"l1y":39,"l2x":37,"l2y":12,"l3x":32,"l3y":9,"l4x":32,"l4y":9,"l5x":37,"l5y":12},{"data":1000002,"id":500000007,"lvl":11,"x":37,"y":22,"res_time":87173,"l1x":21,"l1y":9,"l2x":41,"l2y":24,"l3x":8,"l3y":20,"l4x":8,"l4y":20,"l5x":41,"l5y":24},{"data":1000003,"id":500000008,"lvl":10,"x":39,"y":30,"l1x":16,"l1y":20,"l2x":26,"l2y":15,"l3x":29,"l3y":34,"l4x":29,"l4y":33,"l5x":26,"l5y":15},{"data":1000005,"id":500000009,"lvl":10,"x":30,"y":39,"l1x":24,"l1y":33,"l2x":27,"l2y":33,"l3x":16,"l3y":32,"l4x":16,"l4y":32,"l5x":27,"l5y":33},{"data":1000006,"id":500000010,"lvl":9,"x":8,"y":27,"unit_prod":{"unit_type":0},"l1x":40,"l1y":27,"l2x":21,"l2y":39,"l3x":30,"l3y":39,"l4x":30,"l4y":39,"l5x":21,"l5y":39},{"data":1000004,"id":500000011,"lvl":11,"x":33,"y":22,"res_time":65399,"l1x":18,"l1y":8,"l2x":36,"l2y":24,"l3x":42,"l3y":25,"l4x":42,"l4y":25,"l5x":36,"l5y":24},{"data":1000006,"id":500000012,"lvl":9,"x":21,"y":10,"unit_prod":{"unit_type":0},"l1x":39,"l1y":36,"l2x":18,"l2y":39,"l3x":7,"l3y":28,"l4x":7,"l4y":28,"l5x":18,"l5y":39},{"data":1000002,"id":500000013,"lvl":11,"x":31,"y":14,"res_time":61037,"l1x":34,"l1y":11,"l2x":41,"l2y":19,"l3x":8,"l3y":17,"l4x":8,"l4y":17,"l5x":41,"l5y":19},{"data":1000009,"id":500000014,"lvl":9,"x":24,"y":30,"l1x":15,"l1y":16,"l2x":12,"l2y":15,"l3x":24,"l3y":35,"l4x":24,"l4y":35,"l5x":12,"l5y":15},{"data":1000008,"id":500000015,"lvl":9,"x":10,"y":32,"l1x":30,"l1y":26,"l2x":9,"l2y":22,"l3x":38,"l3y":25,"l4x":38,"l4y":25,"l5x":9,"l5y":22},{"data":1000000,"id":500000016,"lvl":5,"x":15,"y":35,"units":[[4000008,2]],"l1x":24,"l1y":8,"l2x":32,"l2y":9,"l3x":20,"l3y":5,"l4x":20,"l4y":5,"l5x":32,"l5y":9},{"data":1000002,"id":500000017,"lvl":11,"x":10,"y":17,"res_time":87181,"l1x":37,"l1y":15,"l2x":40,"l2y":35,"l3x":14,"l3y":22,"l4x":14,"l4y":22,"l5x":9,"l5y":34},{"data":1000004,"id":500000018,"lvl":11,"x":14,"y":31,"res_time":61057,"l1x":40,"l1y":24,"l2x":15,"l2y":11,"l3x":40,"l3y":29,"l4x":40,"l4y":29,"l5x":15,"l5y":11},{"data":1000003,"id":500000019,"lvl":10,"x":39,"y":33,"l1x":35,"l1y":22,"l2x":19,"l2y":14,"l3x":22,"l3y":14,"l4x":22,"l4y":14,"l5x":20,"l5y":14},{"data":1000005,"id":500000020,"lvl":10,"x":33,"y":39,"l1x":19,"l1y":16,"l2x":30,"l2y":19,"l3x":18,"l3y":23,"l4x":18,"l4y":23,"l5x":30,"l5y":18},{"data":1000010,"id":500000021,"lvl":7,"x":40,"y":37,"l1x":24,"l1y":39,"l2x":23,"l2y":35,"l3x":36,"l3y":35,"l4x":36,"l4y":35,"l5x":24,"l5y":34},{"data":1000010,"id":500000022,"lvl":7,"x":42,"y":35,"l1x":25,"l1y":39,"l2x":23,"l2y":33,"l3x":15,"l3y":16,"l4x":15,"l4y":16,"l5x":24,"l5y":33},{"data":1000010,"id":500000023,"lvl":7,"x":42,"y":36,"l1x":26,"l1y":39,"l2x":24,"l2y":32,"l3x":15,"l3y":15,"l4x":15,"l4y":15,"l5x":24,"l5y":32},{"data":1000010,"id":500000024,"lvl":7,"x":41,"y":36,"l1x":27,"l1y":39,"l2x":15,"l2y":19,"l3x":15,"l3y":14,"l4x":15,"l4y":14,"l5x":13,"l5y":20},{"data":1000010,"id":500000025,"lvl":7,"x":34,"y":42,"l1x":28,"l1y":39,"l2x":14,"l2y":19,"l3x":22,"l3y":18,"l4x":19,"l4y":18,"l5x":15,"l5y":20},{"data":1000010,"id":500000026,"lvl":7,"x":35,"y":42,"l1x":26,"l1y":23,"l2x":13,"l2y":19,"l3x":41,"l3y":28,"l4x":41,"l4y":28,"l5x":34,"l5y":20},{"data":1000010,"id":500000027,"lvl":7,"x":36,"y":42,"l1x":29,"l1y":38,"l2x":17,"l2y":31,"l3x":15,"l3y":13,"l4x":15,"l4y":13,"l5x":17,"l5y":31},{"data":1000010,"id":500000028,"lvl":7,"x":37,"y":42,"l1x":30,"l1y":35,"l2x":17,"l2y":32,"l3x":16,"l3y":13,"l4x":16,"l4y":13,"l5x":17,"l5y":32},{"data":1000010,"id":500000029,"lvl":7,"x":37,"y":41,"l1x":31,"l1y":38,"l2x":17,"l2y":33,"l3x":17,"l3y":13,"l4x":17,"l4y":13,"l5x":17,"l5y":33},{"data":1000010,"id":500000030,"lvl":7,"x":40,"y":26,"l1x":32,"l1y":38,"l2x":16,"l2y":33,"l3x":18,"l3y":13,"l4x":18,"l4y":13,"l5x":16,"l5y":33},{"data":1000010,"id":500000031,"lvl":7,"x":37,"y":40,"l1x":34,"l1y":38,"l2x":15,"l2y":33,"l3x":19,"l3y":13,"l4x":19,"l4y":13,"l5x":15,"l5y":33},{"data":1000010,"id":500000032,"lvl":7,"x":38,"y":40,"l1x":35,"l1y":38,"l2x":14,"l2y":33,"l3x":36,"l3y":38,"l4x":36,"l4y":38,"l5x":14,"l5y":33},{"data":1000010,"id":500000033,"lvl":7,"x":39,"y":40,"l1x":36,"l1y":38,"l2x":13,"l2y":33,"l3x":41,"l3y":27,"l4x":41,"l4y":27,"l5x":13,"l5y":33},{"data":1000010,"id":500000034,"lvl":7,"x":40,"y":36,"l1x":37,"l1y":38,"l2x":12,"l2y":33,"l3x":35,"l3y":38,"l4x":35,"l4y":38,"l5x":12,"l5y":33},{"data":1000010,"id":500000035,"lvl":7,"x":42,"y":34,"l1x":38,"l1y":38,"l2x":11,"l2y":33,"l3x":34,"l3y":38,"l4x":34,"l4y":38,"l5x":11,"l5y":33},{"data":1000010,"id":500000036,"lvl":7,"x":40,"y":38,"l1x":38,"l1y":37,"l2x":11,"l2y":32,"l3x":33,"l3y":38,"l4x":33,"l4y":38,"l5x":11,"l5y":32},{"data":1000010,"id":500000037,"lvl":7,"x":40,"y":39,"l1x":38,"l1y":36,"l2x":11,"l2y":31,"l3x":32,"l3y":38,"l4x":32,"l4y":38,"l5x":11,"l5y":31},{"data":1000010,"id":500000038,"lvl":7,"x":39,"y":26,"l1x":32,"l1y":21,"l2x":11,"l2y":30,"l3x":31,"l3y":38,"l4x":31,"l4y":38,"l5x":11,"l5y":30},{"data":1000010,"id":500000039,"lvl":7,"x":38,"y":26,"l1x":29,"l1y":19,"l2x":11,"l2y":29,"l3x":31,"l3y":37,"l4x":31,"l4y":37,"l5x":11,"l5y":29},{"data":1000010,"id":500000040,"lvl":7,"x":38,"y":25,"l1x":38,"l1y":35,"l2x":12,"l2y":29,"l3x":36,"l3y":34,"l4x":36,"l4y":34,"l5x":12,"l5y":29},{"data":1000010,"id":500000041,"lvl":7,"x":37,"y":25,"l1x":38,"l1y":34,"l2x":12,"l2y":28,"l3x":36,"l3y":33,"l4x":36,"l4y":33,"l5x":12,"l5y":28},{"data":1000010,"id":500000042,"lvl":7,"x":36,"y":25,"l1x":27,"l1y":13,"l2x":12,"l2y":27,"l3x":36,"l3y":36,"l4x":36,"l4y":36,"l5x":12,"l5y":27},{"data":1000010,"id":500000043,"lvl":7,"x":36,"y":24,"l1x":23,"l1y":13,"l2x":12,"l2y":26,"l3x":23,"l3y":37,"l4x":22,"l4y":38,"l5x":12,"l5y":26},{"data":1000010,"id":500000044,"lvl":7,"x":36,"y":23,"l1x":24,"l1y":13,"l2x":12,"l2y":19,"l3x":30,"l3y":37,"l4x":30,"l4y":37,"l5x":34,"l5y":22},{"data":1000010,"id":500000045,"lvl":7,"x":36,"y":22,"l1x":25,"l1y":13,"l2x":12,"l2y":20,"l3x":27,"l3y":38,"l4x":27,"l4y":38,"l5x":12,"l5y":20},{"data":1000010,"id":500000046,"lvl":7,"x":36,"y":21,"l1x":26,"l1y":13,"l2x":12,"l2y":21,"l3x":33,"l3y":32,"l4x":33,"l4y":32,"l5x":12,"l5y":21},{"data":1000010,"id":500000047,"lvl":7,"x":35,"y":21,"l1x":28,"l1y":13,"l2x":12,"l2y":22,"l3x":34,"l3y":32,"l4x":34,"l4y":32,"l5x":12,"l5y":22},{"data":1000010,"id":500000048,"lvl":7,"x":40,"y":40,"l1x":32,"l1y":12,"l2x":12,"l2y":23,"l3x":35,"l3y":32,"l4x":35,"l4y":32,"l5x":12,"l5y":23},{"data":1000010,"id":500000049,"lvl":7,"x":12,"y":8,"l1x":29,"l1y":14,"l2x":12,"l2y":24,"l3x":36,"l3y":32,"l4x":36,"l4y":32,"l5x":12,"l5y":24},{"data":1000010,"id":500000050,"lvl":7,"x":26,"y":39,"l1x":29,"l1y":16,"l2x":12,"l2y":25,"l3x":37,"l3y":32,"l4x":37,"l4y":32,"l5x":12,"l5y":25},{"data":1000010,"id":500000051,"lvl":7,"x":26,"y":38,"l1x":29,"l1y":15,"l2x":18,"l2y":16,"l3x":37,"l3y":31,"l4x":37,"l4y":31,"l5x":18,"l5y":16},{"data":1000010,"id":500000052,"lvl":7,"x":25,"y":38,"l1x":29,"l1y":17,"l2x":18,"l2y":15,"l3x":37,"l3y":30,"l4x":37,"l4y":30,"l5x":18,"l5y":15},{"data":1000010,"id":500000053,"lvl":7,"x":25,"y":37,"l1x":31,"l1y":12,"l2x":18,"l2y":14,"l3x":37,"l3y":29,"l4x":37,"l4y":29,"l5x":18,"l5y":14},{"data":1000010,"id":500000054,"lvl":7,"x":25,"y":36,"l1x":36,"l1y":16,"l2x":18,"l2y":13,"l3x":37,"l3y":28,"l4x":37,"l4y":28,"l5x":18,"l5y":13},{"data":1000010,"id":500000055,"lvl":7,"x":24,"y":36,"l1x":29,"l1y":12,"l2x":18,"l2y":12,"l3x":38,"l3y":28,"l4x":38,"l4y":28,"l5x":18,"l5y":12},{"data":1000010,"id":500000056,"lvl":7,"x":23,"y":36,"l1x":35,"l1y":14,"l2x":18,"l2y":11,"l3x":40,"l3y":28,"l4x":40,"l4y":28,"l5x":18,"l5y":11},{"data":1000010,"id":500000057,"lvl":7,"x":22,"y":36,"l1x":36,"l1y":14,"l2x":18,"l2y":10,"l3x":41,"l3y":25,"l4x":41,"l4y":25,"l5x":18,"l5y":10},{"data":1000010,"id":500000058,"lvl":7,"x":21,"y":36,"l1x":29,"l1y":20,"l2x":18,"l2y":9,"l3x":41,"l3y":24,"l4x":41,"l4y":24,"l5x":18,"l5y":9},{"data":1000010,"id":500000059,"lvl":7,"x":8,"y":35,"l1x":30,"l1y":20,"l2x":18,"l2y":8,"l3x":34,"l3y":23,"l4x":34,"l4y":23,"l5x":18,"l5y":8},{"data":1000010,"id":500000060,"lvl":7,"x":21,"y":35,"l1x":31,"l1y":20,"l2x":30,"l2y":13,"l3x":35,"l3y":23,"l4x":35,"l4y":23,"l5x":30,"l5y":13},{"data":1000010,"id":500000061,"lvl":7,"x":7,"y":35,"l1x":31,"l1y":21,"l2x":19,"l2y":8,"l3x":36,"l3y":23,"l4x":36,"l4y":23,"l5x":19,"l5y":8},{"data":1000010,"id":500000062,"lvl":7,"x":7,"y":34,"l1x":34,"l1y":24,"l2x":20,"l2y":8,"l3x":37,"l3y":23,"l4x":37,"l4y":23,"l5x":20,"l5y":8},{"data":1000010,"id":500000063,"lvl":7,"x":7,"y":33,"l1x":34,"l1y":23,"l2x":21,"l2y":8,"l3x":38,"l3y":23,"l4x":38,"l4y":23,"l5x":21,"l5y":8},{"data":1000010,"id":500000064,"lvl":7,"x":7,"y":32,"l1x":34,"l1y":22,"l2x":22,"l2y":8,"l3x":39,"l3y":24,"l4x":39,"l4y":24,"l5x":22,"l5y":8},{"data":1000010,"id":500000065,"lvl":7,"x":7,"y":31,"l1x":33,"l1y":21,"l2x":23,"l2y":8,"l3x":39,"l3y":23,"l4x":39,"l4y":23,"l5x":23,"l5y":8},{"data":1000010,"id":500000066,"lvl":7,"x":7,"y":30,"l1x":34,"l1y":21,"l2x":24,"l2y":8,"l3x":39,"l3y":22,"l4x":39,"l4y":22,"l5x":24,"l5y":8},{"data":1000010,"id":500000067,"lvl":7,"x":7,"y":29,"l1x":30,"l1y":25,"l2x":25,"l2y":8,"l3x":39,"l3y":21,"l4x":39,"l4y":21,"l5x":25,"l5y":8},{"data":1000010,"id":500000068,"lvl":7,"x":7,"y":28,"l1x":31,"l1y":25,"l2x":26,"l2y":8,"l3x":39,"l3y":20,"l4x":39,"l4y":20,"l5x":26,"l5y":8},{"data":1000010,"id":500000069,"lvl":7,"x":7,"y":27,"l1x":32,"l1y":25,"l2x":27,"l2y":8,"l3x":39,"l3y":19,"l4x":39,"l4y":19,"l5x":27,"l5y":8},{"data":1000010,"id":500000070,"lvl":7,"x":7,"y":26,"l1x":19,"l1y":22,"l2x":28,"l2y":8,"l3x":39,"l3y":18,"l4x":39,"l4y":18,"l5x":28,"l5y":8},{"data":1000013,"id":500000071,"lvl":5,"x":18,"y":25,"l1x":35,"l1y":34,"l2x":15,"l2y":14,"l3x":36,"l3y":15,"l4x":36,"l4y":15,"l5x":15,"l5y":14},{"data":1000007,"id":500000072,"lvl":5,"x":21,"y":37,"l1x":10,"l1y":14,"l2x":8,"l2y":14,"l3x":26,"l3y":10,"l4x":26,"l4y":10,"l5x":8,"l5y":14},{"data":1000010,"id":500000073,"lvl":7,"x":9,"y":35,"l1x":19,"l1y":21,"l2x":29,"l2y":8,"l3x":39,"l3y":17,"l4x":39,"l4y":17,"l5x":29,"l5y":8},{"data":1000010,"id":500000074,"lvl":7,"x":35,"y":8,"l1x":19,"l1y":23,"l2x":30,"l2y":8,"l3x":39,"l3y":16,"l4x":39,"l4y":16,"l5x":30,"l5y":8},{"data":1000010,"id":500000075,"lvl":7,"x":35,"y":7,"l1x":20,"l1y":23,"l2x":31,"l2y":8,"l3x":39,"l3y":15,"l4x":39,"l4y":15,"l5x":31,"l5y":8},{"data":1000010,"id":500000076,"lvl":7,"x":34,"y":7,"l1x":33,"l1y":25,"l2x":31,"l2y":9,"l3x":39,"l3y":14,"l4x":39,"l4y":14,"l5x":31,"l5y":9},{"data":1000010,"id":500000077,"lvl":7,"x":33,"y":7,"l1x":28,"l1y":25,"l2x":31,"l2y":10,"l3x":38,"l3y":14,"l4x":38,"l4y":14,"l5x":31,"l5y":10},{"data":1000010,"id":500000078,"lvl":7,"x":32,"y":7,"l1x":29,"l1y":25,"l2x":23,"l2y":13,"l3x":37,"l3y":14,"l4x":37,"l4y":14,"l5x":35,"l5y":26},{"data":1000010,"id":500000079,"lvl":7,"x":31,"y":7,"l1x":28,"l1y":24,"l2x":23,"l2y":15,"l3x":36,"l3y":14,"l4x":36,"l4y":14,"l5x":25,"l5y":15},{"data":1000010,"id":500000080,"lvl":7,"x":30,"y":7,"l1x":19,"l1y":20,"l2x":24,"l2y":13,"l3x":35,"l3y":14,"l4x":35,"l4y":14,"l5x":34,"l5y":26},{"data":1000010,"id":500000081,"lvl":7,"x":29,"y":7,"l1x":34,"l1y":25,"l2x":23,"l2y":14,"l3x":34,"l3y":14,"l4x":34,"l4y":14,"l5x":25,"l5y":16},{"data":1000010,"id":500000082,"lvl":7,"x":28,"y":7,"l1x":19,"l1y":19,"l2x":25,"l2y":13,"l3x":33,"l3y":14,"l4x":33,"l4y":14,"l5x":25,"l5y":13},{"data":1000010,"id":500000083,"lvl":7,"x":27,"y":7,"l1x":22,"l1y":19,"l2x":26,"l2y":13,"l3x":32,"l3y":14,"l4x":32,"l4y":14,"l5x":26,"l5y":13},{"data":1000010,"id":500000084,"lvl":7,"x":26,"y":7,"l1x":21,"l1y":19,"l2x":27,"l2y":13,"l3x":31,"l3y":14,"l4x":31,"l4y":14,"l5x":27,"l5y":13},{"data":1000010,"id":500000085,"lvl":7,"x":25,"y":7,"l1x":20,"l1y":19,"l2x":29,"l2y":13,"l3x":30,"l3y":14,"l4x":30,"l4y":14,"l5x":29,"l5y":13},{"data":1000010,"id":500000086,"lvl":7,"x":35,"y":9,"l1x":25,"l1y":18,"l2x":28,"l2y":13,"l3x":27,"l3y":14,"l4x":27,"l4y":14,"l5x":28,"l5y":13},{"data":1000010,"id":500000087,"lvl":7,"x":35,"y":10,"l1x":24,"l1y":18,"l2x":31,"l2y":12,"l3x":25,"l3y":13,"l4x":25,"l4y":13,"l5x":31,"l5y":12},{"data":1000010,"id":500000088,"lvl":7,"x":35,"y":11,"l1x":24,"l1y":19,"l2x":31,"l2y":13,"l3x":25,"l3y":12,"l4x":25,"l4y":12,"l5x":31,"l5y":13},{"data":1000010,"id":500000089,"lvl":7,"x":35,"y":12,"l1x":23,"l1y":19,"l2x":31,"l2y":14,"l3x":25,"l3y":10,"l4x":25,"l4y":10,"l5x":31,"l5y":14},{"data":1000010,"id":500000090,"lvl":7,"x":35,"y":13,"l1x":29,"l1y":18,"l2x":32,"l2y":14,"l3x":24,"l3y":10,"l4x":24,"l4y":10,"l5x":32,"l5y":14},{"data":1000010,"id":500000091,"lvl":7,"x":7,"y":25,"l1x":28,"l1y":18,"l2x":33,"l2y":14,"l3x":23,"l3y":10,"l4x":23,"l4y":10,"l5x":33,"l5y":14},{"data":1000010,"id":500000092,"lvl":7,"x":10,"y":35,"l1x":27,"l1y":18,"l2x":34,"l2y":14,"l3x":22,"l3y":10,"l4x":22,"l4y":10,"l5x":34,"l5y":14},{"data":1000010,"id":500000093,"lvl":7,"x":11,"y":35,"l1x":26,"l1y":18,"l2x":35,"l2y":14,"l3x":21,"l3y":10,"l4x":21,"l4y":10,"l5x":35,"l5y":14},{"data":1000010,"id":500000094,"lvl":7,"x":12,"y":35,"l1x":27,"l1y":23,"l2x":36,"l2y":14,"l3x":20,"l3y":10,"l4x":20,"l4y":10,"l5x":36,"l5y":14},{"data":1000010,"id":500000095,"lvl":7,"x":13,"y":35,"l1x":28,"l1y":23,"l2x":37,"l2y":14,"l3x":20,"l3y":11,"l4x":20,"l4y":11,"l5x":37,"l5y":14},{"data":1000010,"id":500000096,"lvl":7,"x":26,"y":11,"l1x":25,"l1y":23,"l2x":37,"l2y":15,"l3x":20,"l3y":12,"l4x":20,"l4y":12,"l5x":37,"l5y":15},{"data":1000010,"id":500000097,"lvl":7,"x":27,"y":11,"l1x":24,"l1y":23,"l2x":37,"l2y":16,"l3x":20,"l3y":13,"l4x":20,"l4y":13,"l5x":37,"l5y":16},{"data":1000002,"id":500000098,"lvl":11,"x":17,"y":10,"res_time":87175,"l1x":23,"l1y":15,"l2x":35,"l2y":41,"l3x":16,"l3y":19,"l4x":16,"l4y":19,"l5x":35,"l5y":41},{"data":1000006,"id":500000099,"lvl":9,"x":27,"y":8,"unit_prod":{"unit_type":0,"t":0,"slots":[{"id":4000008,"cnt":1}]},"l1x":37,"l1y":18,"l2x":14,"l2y":38,"l3x":7,"l3y":23,"l4x":7,"l4y":23,"l5x":14,"l5y":38},{"data":1000004,"id":500000100,"lvl":11,"x":28,"y":18,"res_time":61043,"l1x":39,"l1y":33,"l2x":11,"l2y":11,"l3x":34,"l3y":42,"l4x":34,"l4y":42,"l5x":11,"l5y":11},{"data":1000009,"id":500000101,"lvl":9,"x":30,"y":24,"l1x":25,"l1y":36,"l2x":12,"l2y":30,"l3x":36,"l3y":20,"l4x":36,"l4y":20,"l5x":12,"l5y":30},{"data":1000015,"id":500000102,"lvl":0,"x":28,"y":40,"l1x":12,"l1y":18,"l2x":22,"l2y":37,"l3x":30,"l3y":9,"l4x":30,"l4y":9,"l5x":41,"l5y":36},{"data":1000012,"id":500000103,"lvl":5,"x":27,"y":30,"l1x":21,"l1y":20,"l2x":30,"l2y":29,"l3x":25,"l3y":20,"l4x":25,"l4y":20,"l5x":30,"l5y":29},{"data":1000015,"id":500000104,"lvl":0,"x":26,"y":40,"l1x":37,"l1y":13,"l2x":18,"l2y":23,"l3x":8,"l3y":26,"l4x":8,"l4y":26,"l5x":18,"l5y":23},{"data":1000000,"id":500000105,"lvl":5,"x":12,"y":26,"units":[[4000008,3]],"l1x":10,"l1y":20,"l2x":6,"l2y":28,"l3x":23,"l3y":39,"l4x":23,"l4y":39,"l5x":6,"l5y":28},{"data":1000020,"id":500000106,"lvl":2,"x":41,"y":37,"units":[[26000001,1],[26000000,2]],"unit_prod":{"unit_type":1,"t":0,"slots":[{"id":26000001,"cnt":1}]},"l1x":11,"l1y":27,"l2x":9,"l2y":25,"l3x":17,"l3y":10,"l4x":17,"l4y":10,"l5x":9,"l5y":25},{"data":1000011,"id":500000107,"lvl":5,"x":21,"y":21,"l1x":16,"l1y":24,"l2x":25,"l2y":29,"l3x":30,"l3y":11,"l4x":30,"l4y":11,"l5x":25,"l5y":28},{"data":1000008,"id":500000108,"lvl":9,"x":32,"y":10,"l1x":14,"l1y":36,"l2x":26,"l2y":10,"l3x":18,"l3y":37,"l4x":18,"l4y":37,"l5x":26,"l5y":10},{"data":1000010,"id":500000109,"lvl":7,"x":28,"y":11,"l1x":36,"l1y":15,"l2x":37,"l2y":18,"l3x":20,"l3y":14,"l4x":20,"l4y":14,"l5x":37,"l5y":18},{"data":1000010,"id":500000110,"lvl":7,"x":29,"y":11,"l1x":30,"l1y":12,"l2x":37,"l2y":17,"l3x":20,"l3y":15,"l4x":20,"l4y":15,"l5x":37,"l5y":17},{"data":1000010,"id":500000111,"lvl":7,"x":30,"y":11,"l1x":36,"l1y":17,"l2x":39,"l2y":34,"l3x":20,"l3y":16,"l4x":20,"l4y":16,"l5x":39,"l5y":34},{"data":1000010,"id":500000112,"lvl":7,"x":31,"y":11,"l1x":34,"l1y":14,"l2x":38,"l2y":34,"l3x":28,"l3y":14,"l4x":28,"l4y":14,"l5x":38,"l5y":34},{"data":1000010,"id":500000113,"lvl":7,"x":31,"y":12,"l1x":36,"l1y":18,"l2x":37,"l2y":34,"l3x":25,"l3y":14,"l4x":25,"l4y":14,"l5x":37,"l5y":34},{"data":1000010,"id":500000114,"lvl":7,"x":31,"y":13,"l1x":36,"l1y":19,"l2x":35,"l2y":34,"l3x":26,"l3y":14,"l4x":26,"l4y":14,"l5x":35,"l5y":34},{"data":1000010,"id":500000115,"lvl":7,"x":32,"y":13,"l1x":33,"l1y":13,"l2x":35,"l2y":33,"l3x":29,"l3y":14,"l4x":29,"l4y":14,"l5x":35,"l5y":33},{"data":1000010,"id":500000116,"lvl":7,"x":33,"y":13,"l1x":35,"l1y":21,"l2x":36,"l2y":34,"l3x":29,"l3y":15,"l4x":29,"l4y":15,"l5x":36,"l5y":34},{"data":1000010,"id":500000117,"lvl":7,"x":34,"y":13,"l1x":33,"l1y":38,"l2x":36,"l2y":35,"l3x":29,"l3y":16,"l4x":29,"l4y":16,"l5x":36,"l5y":35},{"data":1000010,"id":500000118,"lvl":7,"x":34,"y":14,"l1x":36,"l1y":20,"l2x":36,"l2y":36,"l3x":29,"l3y":17,"l4x":29,"l4y":17,"l5x":36,"l5y":36},{"data":1000010,"id":500000119,"lvl":7,"x":34,"y":15,"l1x":37,"l1y":21,"l2x":36,"l2y":37,"l3x":28,"l3y":37,"l4x":28,"l4y":37,"l5x":36,"l5y":37},{"data":1000010,"id":500000120,"lvl":7,"x":34,"y":16,"l1x":38,"l1y":21,"l2x":36,"l2y":38,"l3x":41,"l3y":26,"l4x":41,"l4y":26,"l5x":36,"l5y":38},{"data":1000010,"id":500000121,"lvl":7,"x":11,"y":26,"l1x":39,"l1y":21,"l2x":36,"l2y":39,"l3x":25,"l3y":17,"l4x":25,"l4y":17,"l5x":36,"l5y":39},{"data":1000010,"id":500000122,"lvl":7,"x":11,"y":27,"l1x":39,"l1y":22,"l2x":23,"l2y":16,"l3x":23,"l3y":17,"l4x":23,"l4y":17,"l5x":25,"l5y":14},{"data":1000010,"id":500000123,"lvl":7,"x":11,"y":28,"l1x":39,"l1y":23,"l2x":36,"l2y":40,"l3x":21,"l3y":17,"l4x":21,"l4y":17,"l5x":36,"l5y":40},{"data":1000010,"id":500000124,"lvl":7,"x":11,"y":29,"l1x":39,"l1y":24,"l2x":35,"l2y":40,"l3x":20,"l3y":17,"l4x":20,"l4y":17,"l5x":35,"l5y":40},{"data":1000010,"id":500000125,"lvl":7,"x":11,"y":30,"l1x":39,"l1y":25,"l2x":34,"l2y":40,"l3x":19,"l3y":17,"l4x":19,"l4y":17,"l5x":34,"l5y":40},{"data":1000010,"id":500000126,"lvl":7,"x":11,"y":31,"l1x":39,"l1y":26,"l2x":33,"l2y":40,"l3x":18,"l3y":17,"l4x":18,"l4y":17,"l5x":33,"l5y":40},{"data":1000010,"id":500000127,"lvl":7,"x":12,"y":31,"l1x":39,"l1y":27,"l2x":32,"l2y":40,"l3x":16,"l3y":17,"l4x":16,"l4y":17,"l5x":32,"l5y":40},{"data":1000010,"id":500000128,"lvl":7,"x":13,"y":31,"l1x":39,"l1y":28,"l2x":24,"l2y":35,"l3x":15,"l3y":17,"l4x":15,"l4y":17,"l5x":24,"l5y":35},{"data":1000010,"id":500000129,"lvl":7,"x":13,"y":32,"l1x":39,"l1y":29,"l2x":24,"l2y":36,"l3x":14,"l3y":17,"l4x":14,"l4y":17,"l5x":24,"l5y":36},{"data":1000010,"id":500000130,"lvl":7,"x":13,"y":33,"l1x":22,"l1y":23,"l2x":24,"l2y":37,"l3x":13,"l3y":17,"l4x":13,"l4y":17,"l5x":24,"l5y":37},{"data":1000010,"id":500000131,"lvl":7,"x":13,"y":34,"l1x":39,"l1y":30,"l2x":33,"l2y":21,"l3x":12,"l3y":17,"l4x":12,"l4y":17,"l5x":33,"l5y":21},{"data":1000010,"id":500000132,"lvl":7,"x":14,"y":34,"l1x":39,"l1y":31,"l2x":33,"l2y":20,"l3x":11,"l3y":17,"l4x":11,"l4y":17,"l5x":30,"l5y":21},{"data":1000010,"id":500000133,"lvl":7,"x":15,"y":34,"l1x":39,"l1y":32,"l2x":33,"l2y":19,"l3x":11,"l3y":18,"l4x":11,"l4y":18,"l5x":34,"l5y":19},{"data":1000002,"id":500000134,"lvl":11,"x":17,"y":14,"res_time":87179,"l1x":11,"l1y":11,"l2x":29,"l2y":36,"l3x":34,"l3y":25,"l4x":34,"l4y":25,"l5x":29,"l5y":36},{"data":1000004,"id":500000135,"lvl":11,"x":18,"y":28,"res_time":61053,"l1x":25,"l1y":40,"l2x":11,"l2y":34,"l3x":37,"l3y":39,"l4x":37,"l4y":39,"l5x":12,"l5y":34},{"data":1000009,"id":500000136,"lvl":9,"x":14,"y":14,"l1x":15,"l1y":28,"l2x":34,"l2y":15,"l3x":22,"l3y":11,"l4x":22,"l4y":11,"l5x":34,"l5y":15},{"data":1000002,"id":500000137,"lvl":11,"x":14,"y":17,"res_time":87173,"l1x":14,"l1y":8,"l2x":6,"l2y":20,"l3x":26,"l3y":15,"l4x":26,"l4y":15,"l5x":6,"l5y":20},{"data":1000004,"id":500000138,"lvl":11,"x":18,"y":22,"res_time":61055,"l1x":11,"l1y":8,"l2x":13,"l2y":21,"l3x":33,"l3y":11,"l4x":33,"l4y":11,"l5x":13,"l5y":21},{"data":1000013,"id":500000139,"lvl":5,"x":25,"y":18,"l1x":15,"l1y":12,"l2x":37,"l2y":35,"l3x":16,"l3y":14,"l4x":16,"l4y":14,"l5x":38,"l5y":35},{"data":1000010,"id":500000140,"lvl":7,"x":9,"y":25,"l1x":21,"l1y":23,"l2x":34,"l2y":19,"l3x":11,"l3y":19,"l4x":11,"l4y":19,"l5x":36,"l5y":18},{"data":1000010,"id":500000141,"lvl":7,"x":10,"y":25,"l1x":23,"l1y":23,"l2x":35,"l2y":19,"l3x":11,"l3y":20,"l4x":11,"l4y":20,"l5x":35,"l5y":18},{"data":1000010,"id":500000142,"lvl":7,"x":11,"y":25,"l1x":20,"l1y":26,"l2x":36,"l2y":19,"l3x":11,"l3y":21,"l4x":11,"l4y":21,"l5x":34,"l5y":18},{"data":1000010,"id":500000143,"lvl":7,"x":12,"y":25,"l1x":20,"l1y":36,"l2x":37,"l2y":19,"l3x":11,"l3y":22,"l4x":11,"l4y":22,"l5x":37,"l5y":19},{"data":1000010,"id":500000144,"lvl":7,"x":25,"y":9,"l1x":37,"l1y":33,"l2x":38,"l2y":19,"l3x":12,"l3y":22,"l4x":12,"l4y":22,"l5x":38,"l5y":19},{"data":1000010,"id":500000145,"lvl":7,"x":25,"y":10,"l1x":36,"l1y":33,"l2x":39,"l2y":19,"l3x":27,"l3y":33,"l4x":27,"l4y":33,"l5x":39,"l5y":19},{"data":1000010,"id":500000146,"lvl":7,"x":25,"y":11,"l1x":35,"l1y":33,"l2x":40,"l2y":19,"l3x":27,"l3y":34,"l4x":27,"l4y":34,"l5x":40,"l5y":19},{"data":1000010,"id":500000147,"lvl":7,"x":25,"y":12,"l1x":34,"l1y":33,"l2x":40,"l2y":20,"l3x":27,"l3y":35,"l4x":27,"l4y":35,"l5x":40,"l5y":20},{"data":1000010,"id":500000148,"lvl":7,"x":25,"y":8,"l1x":15,"l1y":26,"l2x":40,"l2y":21,"l3x":27,"l3y":36,"l4x":27,"l4y":36,"l5x":40,"l5y":21},{"data":1000010,"id":500000149,"lvl":7,"x":24,"y":8,"l1x":15,"l1y":25,"l2x":40,"l2y":22,"l3x":27,"l3y":37,"l4x":27,"l4y":37,"l5x":40,"l5y":22},{"data":1000010,"id":500000150,"lvl":7,"x":23,"y":8,"l1x":15,"l1y":24,"l2x":32,"l2y":39,"l3x":23,"l3y":38,"l4x":23,"l4y":38,"l5x":32,"l5y":39},{"data":1000010,"id":500000151,"lvl":7,"x":22,"y":8,"l1x":15,"l1y":23,"l2x":31,"l2y":39,"l3x":24,"l3y":38,"l4x":24,"l4y":38,"l5x":31,"l5y":39},{"data":1000010,"id":500000152,"lvl":7,"x":21,"y":8,"l1x":15,"l1y":21,"l2x":30,"l2y":39,"l3x":25,"l3y":38,"l4x":25,"l4y":38,"l5x":30,"l5y":39},{"data":1000010,"id":500000153,"lvl":7,"x":20,"y":8,"l1x":15,"l1y":22,"l2x":29,"l2y":39,"l3x":22,"l3y":37,"l4x":21,"l4y":38,"l5x":29,"l5y":39},{"data":1000010,"id":500000154,"lvl":7,"x":19,"y":8,"l1x":14,"l1y":19,"l2x":28,"l2y":39,"l3x":26,"l3y":38,"l4x":26,"l4y":38,"l5x":28,"l5y":39},{"data":1000010,"id":500000155,"lvl":7,"x":18,"y":8,"l1x":14,"l1y":18,"l2x":27,"l2y":39,"l3x":33,"l3y":18,"l4x":29,"l4y":19,"l5x":27,"l5y":39},{"data":1000010,"id":500000156,"lvl":7,"x":17,"y":8,"l1x":14,"l1y":17,"l2x":26,"l2y":39,"l3x":32,"l3y":18,"l4x":32,"l4y":19,"l5x":26,"l5y":39},{"data":1000010,"id":500000157,"lvl":7,"x":16,"y":8,"l1x":14,"l1y":16,"l2x":25,"l2y":39,"l3x":31,"l3y":18,"l4x":31,"l4y":19,"l5x":25,"l5y":39},{"data":1000010,"id":500000158,"lvl":7,"x":16,"y":34,"l1x":14,"l1y":15,"l2x":39,"l2y":33,"l3x":33,"l3y":19,"l4x":33,"l4y":19,"l5x":39,"l5y":33},{"data":1000010,"id":500000159,"lvl":7,"x":8,"y":25,"l1x":14,"l1y":14,"l2x":39,"l2y":27,"l3x":33,"l3y":20,"l4x":33,"l4y":20,"l5x":39,"l5y":27},{"data":1000010,"id":500000160,"lvl":7,"x":8,"y":24,"l1x":14,"l1y":13,"l2x":39,"l2y":28,"l3x":33,"l3y":21,"l4x":33,"l4y":21,"l5x":39,"l5y":28},{"data":1000010,"id":500000161,"lvl":7,"x":8,"y":23,"l1x":14,"l1y":12,"l2x":39,"l2y":29,"l3x":33,"l3y":22,"l4x":33,"l4y":22,"l5x":39,"l5y":29},{"data":1000010,"id":500000162,"lvl":7,"x":8,"y":22,"l1x":14,"l1y":11,"l2x":22,"l2y":32,"l3x":33,"l3y":23,"l4x":33,"l4y":23,"l5x":22,"l5y":32},{"data":1000010,"id":500000163,"lvl":7,"x":8,"y":21,"l1x":15,"l1y":11,"l2x":22,"l2y":31,"l3x":33,"l3y":24,"l4x":33,"l4y":24,"l5x":22,"l5y":31},{"data":1000010,"id":500000164,"lvl":7,"x":8,"y":20,"l1x":16,"l1y":11,"l2x":23,"l2y":32,"l3x":33,"l3y":25,"l4x":33,"l4y":25,"l5x":23,"l5y":32},{"data":1000011,"id":500000165,"lvl":5,"x":36,"y":30,"l1x":31,"l1y":35,"l2x":17,"l2y":19,"l3x":19,"l3y":19,"l4x":38,"l4y":33,"l5x":17,"l5y":19},{"data":1000015,"id":500000166,"lvl":0,"x":39,"y":27,"l1x":23,"l1y":40,"l2x":26,"l2y":25,"l3x":25,"l3y":8,"l4x":25,"l4y":8,"l5x":27,"l5y":25},{"data":1000024,"id":500000167,"lvl":3,"x":22,"y":18,"l1x":21,"l1y":24,"l2x":32,"l2y":34,"l3x":24,"l3y":29,"l4x":14,"l4y":26,"l5x":32,"l5y":33},{"data":1000006,"id":500000168,"lvl":9,"x":10,"y":21,"unit_prod":{"unit_type":0,"t":0,"slots":[{"id":4000008,"cnt":1}]},"l1x":37,"l1y":39,"l2x":9,"l2y":18,"l3x":10,"l3y":27,"l4x":10,"l4y":27,"l5x":9,"l5y":18},{"data":1000000,"id":500000169,"lvl":5,"x":26,"y":12,"units":[[4000008,3]],"l1x":18,"l1y":37,"l2x":40,"l2y":29,"l3x":7,"l3y":31,"l4x":7,"l4y":31,"l5x":40,"l5y":29},{"data":1000010,"id":500000170,"lvl":7,"x":8,"y":19,"l1x":17,"l1y":11,"l2x":23,"l2y":34,"l3x":33,"l3y":26,"l4x":33,"l4y":26,"l5x":24,"l5y":17},{"data":1000010,"id":500000171,"lvl":7,"x":8,"y":18,"l1x":18,"l1y":11,"l2x":25,"l2y":32,"l3x":30,"l3y":18,"l4x":30,"l4y":19,"l5x":25,"l5y":32},{"data":1000010,"id":500000172,"lvl":7,"x":8,"y":17,"l1x":33,"l1y":14,"l2x":26,"l2y":32,"l3x":29,"l3y":18,"l4x":29,"l4y":18,"l5x":26,"l5y":32},{"data":1000010,"id":500000173,"lvl":7,"x":8,"y":16,"l1x":19,"l1y":12,"l2x":27,"l2y":32,"l3x":28,"l3y":18,"l4x":28,"l4y":18,"l5x":27,"l5y":32},{"data":1000010,"id":500000174,"lvl":7,"x":9,"y":16,"l1x":20,"l1y":12,"l2x":28,"l2y":32,"l3x":27,"l3y":18,"l4x":27,"l4y":18,"l5x":28,"l5y":32},{"data":1000010,"id":500000175,"lvl":7,"x":10,"y":16,"l1x":21,"l1y":12,"l2x":29,"l2y":32,"l3x":26,"l3y":18,"l4x":26,"l4y":18,"l5x":29,"l5y":32},{"data":1000010,"id":500000176,"lvl":7,"x":11,"y":16,"l1x":22,"l1y":12,"l2x":30,"l2y":32,"l3x":25,"l3y":18,"l4x":25,"l4y":18,"l5x":30,"l5y":32},{"data":1000010,"id":500000177,"lvl":7,"x":12,"y":16,"l1x":22,"l1y":13,"l2x":31,"l2y":32,"l3x":22,"l3y":17,"l4x":22,"l4y":17,"l5x":31,"l5y":32},{"data":1000010,"id":500000178,"lvl":7,"x":16,"y":9,"l1x":22,"l1y":14,"l2x":32,"l2y":32,"l3x":24,"l3y":17,"l4x":24,"l4y":17,"l5x":32,"l5y":32},{"data":1000010,"id":500000179,"lvl":7,"x":16,"y":10,"l1x":22,"l1y":15,"l2x":33,"l2y":32,"l3x":22,"l3y":20,"l4x":19,"l4y":21,"l5x":33,"l5y":32},{"data":1000010,"id":500000180,"lvl":7,"x":16,"y":11,"l1x":22,"l1y":18,"l2x":34,"l2y":32,"l3x":22,"l3y":19,"l4x":19,"l4y":20,"l5x":34,"l5y":32},{"data":1000010,"id":500000181,"lvl":7,"x":14,"y":9,"l1x":22,"l1y":17,"l2x":35,"l2y":32,"l3x":22,"l3y":21,"l4x":19,"l4y":19,"l5x":35,"l5y":32},{"data":1000010,"id":500000182,"lvl":7,"x":9,"y":14,"l1x":15,"l1y":19,"l2x":35,"l2y":31,"l3x":21,"l3y":37,"l4x":21,"l4y":37,"l5x":35,"l5y":31},{"data":1000010,"id":500000183,"lvl":7,"x":16,"y":12,"l1x":20,"l1y":25,"l2x":35,"l2y":30,"l3x":21,"l3y":36,"l4x":21,"l4y":36,"l5x":35,"l5y":30},{"data":1000010,"id":500000184,"lvl":7,"x":8,"y":12,"l1x":29,"l1y":30,"l2x":35,"l2y":29,"l3x":20,"l3y":36,"l4x":20,"l4y":36,"l5x":35,"l5y":29},{"data":1000010,"id":500000185,"lvl":7,"x":9,"y":9,"l1x":18,"l1y":19,"l2x":35,"l2y":28,"l3x":19,"l3y":36,"l4x":19,"l4y":36,"l5x":35,"l5y":28},{"data":1000010,"id":500000186,"lvl":7,"x":13,"y":14,"l1x":30,"l1y":32,"l2x":39,"l2y":31,"l3x":18,"l3y":36,"l4x":18,"l4y":36,"l5x":39,"l5y":31},{"data":1000010,"id":500000187,"lvl":7,"x":13,"y":13,"l1x":29,"l1y":27,"l2x":35,"l2y":27,"l3x":17,"l3y":36,"l4x":17,"l4y":36,"l5x":35,"l5y":27},{"data":1000010,"id":500000188,"lvl":7,"x":14,"y":13,"l1x":29,"l1y":31,"l2x":17,"l2y":23,"l3x":16,"l3y":36,"l4x":16,"l4y":36,"l5x":17,"l5y":23},{"data":1000010,"id":500000189,"lvl":7,"x":15,"y":13,"l1x":29,"l1y":26,"l2x":17,"l2y":24,"l3x":15,"l3y":36,"l4x":15,"l4y":36,"l5x":17,"l5y":24},{"data":1000010,"id":500000190,"lvl":7,"x":16,"y":13,"l1x":18,"l1y":12,"l2x":17,"l2y":25,"l3x":15,"l3y":35,"l4x":15,"l4y":35,"l5x":17,"l5y":25},{"data":1000010,"id":500000191,"lvl":7,"x":17,"y":13,"l1x":29,"l1y":28,"l2x":17,"l2y":26,"l3x":15,"l3y":34,"l4x":15,"l4y":34,"l5x":17,"l5y":26},{"data":1000010,"id":500000192,"lvl":7,"x":18,"y":13,"l1x":29,"l1y":29,"l2x":17,"l2y":27,"l3x":15,"l3y":33,"l4x":15,"l4y":33,"l5x":17,"l5y":27},{"data":1000010,"id":500000193,"lvl":7,"x":19,"y":13,"l1x":20,"l1y":24,"l2x":17,"l2y":28,"l3x":15,"l3y":32,"l4x":15,"l4y":32,"l5x":17,"l5y":28},{"data":1000010,"id":500000194,"lvl":7,"x":13,"y":15,"l1x":22,"l1y":33,"l2x":17,"l2y":29,"l3x":13,"l3y":23,"l4x":13,"l4y":23,"l5x":17,"l5y":29},{"data":1000010,"id":500000195,"lvl":7,"x":13,"y":16,"l1x":20,"l1y":31,"l2x":39,"l2y":30,"l3x":13,"l3y":24,"l4x":13,"l4y":24,"l5x":39,"l5y":30},{"data":1000010,"id":500000196,"lvl":7,"x":13,"y":17,"l1x":14,"l1y":30,"l2x":17,"l2y":30,"l3x":13,"l3y":25,"l4x":13,"l4y":25,"l5x":17,"l5y":30},{"data":1000010,"id":500000197,"lvl":7,"x":13,"y":18,"l1x":14,"l1y":29,"l2x":18,"l2y":30,"l3x":13,"l3y":26,"l4x":13,"l4y":26,"l5x":18,"l5y":30},{"data":1000010,"id":500000198,"lvl":7,"x":13,"y":19,"l1x":14,"l1y":28,"l2x":19,"l2y":30,"l3x":13,"l3y":27,"l4x":13,"l4y":27,"l5x":19,"l5y":30},{"data":1000010,"id":500000199,"lvl":7,"x":20,"y":13,"l1x":14,"l1y":27,"l2x":20,"l2y":30,"l3x":13,"l3y":28,"l4x":13,"l4y":28,"l5x":20,"l5y":30},{"data":1000010,"id":500000200,"lvl":7,"x":21,"y":13,"l1x":14,"l1y":33,"l2x":21,"l2y":30,"l3x":13,"l3y":29,"l4x":13,"l4y":29,"l5x":21,"l5y":30},{"data":1000010,"id":500000201,"lvl":7,"x":22,"y":13,"l1x":23,"l1y":33,"l2x":22,"l2y":30,"l3x":13,"l3y":30,"l4x":13,"l4y":30,"l5x":22,"l5y":30},{"data":1000010,"id":500000202,"lvl":7,"x":23,"y":13,"l1x":14,"l1y":31,"l2x":22,"l2y":29,"l3x":13,"l3y":31,"l4x":13,"l4y":31,"l5x":22,"l5y":29},{"data":1000010,"id":500000203,"lvl":7,"x":24,"y":13,"l1x":16,"l1y":35,"l2x":22,"l2y":28,"l3x":24,"l3y":23,"l4x":24,"l4y":23,"l5x":22,"l5y":28},{"data":1000010,"id":500000204,"lvl":7,"x":13,"y":20,"l1x":15,"l1y":35,"l2x":16,"l2y":17,"l3x":24,"l3y":22,"l4x":24,"l4y":22,"l5x":16,"l5y":17},{"data":1000010,"id":500000205,"lvl":7,"x":13,"y":21,"l1x":14,"l1y":32,"l2x":17,"l2y":17,"l3x":23,"l3y":22,"l4x":23,"l4y":22,"l5x":17,"l5y":17},{"data":1000010,"id":500000206,"lvl":7,"x":13,"y":22,"l1x":20,"l1y":27,"l2x":18,"l2y":17,"l3x":22,"l3y":22,"l4x":22,"l4y":22,"l5x":18,"l5y":17},{"data":1000010,"id":500000207,"lvl":7,"x":13,"y":23,"l1x":17,"l1y":27,"l2x":19,"l2y":17,"l3x":21,"l3y":22,"l4x":21,"l4y":22,"l5x":19,"l5y":17},{"data":1000010,"id":500000208,"lvl":7,"x":13,"y":24,"l1x":18,"l1y":27,"l2x":20,"l2y":17,"l3x":20,"l3y":22,"l4x":20,"l4y":22,"l5x":20,"l5y":17},{"data":1000010,"id":500000209,"lvl":7,"x":13,"y":25,"l1x":30,"l1y":34,"l2x":21,"l2y":17,"l3x":19,"l3y":22,"l4x":19,"l4y":22,"l5x":21,"l5y":17},{"data":1000010,"id":500000210,"lvl":7,"x":14,"y":25,"l1x":33,"l1y":12,"l2x":22,"l2y":17,"l3x":18,"l3y":22,"l4x":18,"l4y":22,"l5x":22,"l5y":17},{"data":1000010,"id":500000211,"lvl":7,"x":25,"y":13,"l1x":36,"l1y":21,"l2x":23,"l2y":17,"l3x":17,"l3y":22,"l4x":17,"l4y":22,"l5x":14,"l5y":20},{"data":1000010,"id":500000212,"lvl":7,"x":25,"y":14,"l1x":30,"l1y":36,"l2x":25,"l2y":17,"l3x":17,"l3y":23,"l4x":17,"l4y":23,"l5x":25,"l5y":17},{"data":1000010,"id":500000213,"lvl":7,"x":25,"y":15,"l1x":29,"l1y":13,"l2x":24,"l2y":17,"l3x":17,"l3y":24,"l4x":17,"l4y":24,"l5x":23,"l5y":17},{"data":1000010,"id":500000214,"lvl":7,"x":25,"y":16,"l1x":15,"l1y":20,"l2x":25,"l2y":18,"l3x":14,"l3y":31,"l4x":14,"l4y":31,"l5x":25,"l5y":18},{"data":1000010,"id":500000215,"lvl":7,"x":25,"y":17,"l1x":22,"l1y":16,"l2x":25,"l2y":19,"l3x":17,"l3y":25,"l4x":17,"l4y":25,"l5x":25,"l5y":19},{"data":1000010,"id":500000216,"lvl":7,"x":26,"y":17,"l1x":14,"l1y":35,"l2x":25,"l2y":21,"l3x":17,"l3y":26,"l4x":17,"l4y":26,"l5x":25,"l5y":21},{"data":1000010,"id":500000217,"lvl":7,"x":27,"y":17,"l1x":33,"l1y":33,"l2x":25,"l2y":20,"l3x":17,"l3y":27,"l4x":17,"l4y":27,"l5x":25,"l5y":20},{"data":1000010,"id":500000218,"lvl":7,"x":28,"y":17,"l1x":18,"l1y":18,"l2x":16,"l2y":18,"l3x":17,"l3y":28,"l4x":17,"l4y":28,"l5x":16,"l5y":18},{"data":1000010,"id":500000219,"lvl":7,"x":29,"y":17,"l1x":28,"l1y":38,"l2x":16,"l2y":19,"l3x":17,"l3y":29,"l4x":17,"l4y":29,"l5x":16,"l5y":19},{"data":1000008,"id":500000220,"lvl":9,"x":18,"y":31,"l1x":19,"l1y":13,"l2x":36,"l2y":21,"l3x":31,"l3y":15,"l4x":31,"l4y":16,"l5x":36,"l5y":21},{"data":1000008,"id":500000221,"lvl":9,"x":31,"y":18,"l1x":33,"l1y":15,"l2x":19,"l2y":36,"l3x":10,"l3y":24,"l4x":10,"l4y":24,"l5x":19,"l5y":36},{"data":1000009,"id":500000222,"lvl":9,"x":24,"y":21,"l1x":26,"l1y":14,"l2x":25,"l2y":36,"l3x":12,"l3y":19,"l4x":12,"l4y":19,"l5x":25,"l5y":36},{"data":1000013,"id":500000225,"lvl":5,"x":33,"y":27,"l1x":15,"l1y":32,"l2x":15,"l2y":35,"l3x":19,"l3y":32,"l4x":19,"l4y":32,"l5x":15,"l5y":35},{"data":1000012,"id":500000226,"lvl":5,"x":30,"y":27,"l1x":26,"l1y":28,"l2x":19,"l2y":27,"l3x":18,"l3y":28,"l4x":18,"l4y":28,"l5x":19,"l5y":27},{"data":1000022,"id":500000227,"lvl":0,"x":34,"y":34,"l1x":30,"l1y":29,"l2x":26,"l2y":19,"l3x":21,"l3y":23,"l4x":21,"l4y":23,"l5x":26,"l5y":18},{"data":1000026,"id":500000228,"lvl":3,"x":13,"y":10,"unit_prod":{"unit_type":0},"l1x":34,"l1y":39,"l2x":28,"l2y":40,"l3x":11,"l3y":14,"l4x":11,"l4y":14,"l5x":28,"l5y":40},{"data":1000023,"id":500000229,"lvl":2,"x":22,"y":15,"res_time":0,"l1x":11,"l1y":34,"l2x":31,"l2y":41,"l3x":15,"l3y":37,"l4x":15,"l4y":37,"l5x":31,"l5y":41},{"data":1000028,"id":500000230,"lvl":3,"x":24,"y":24,"aim_angle":45,"aim_angle_draft":45,"aim_angle_war":45,"aim_angle_draft_war":45,"aim_angle2":135,"aim_angle_d2":135,"aim_angle3":45,"aim_angle_d3":45,"aim_angle4":225,"aim_angle_d4":225,"aim_angle5":45,"aim_angle_d5":45,"l1x":29,"l1y":23,"l2x":30,"l2y":17,"l3x":30,"l3y":24,"l4x":31,"l4y":24,"l5x":31,"l5y":16},{"data":1000003,"id":500000231,"lvl":10,"x":36,"y":27,"l1x":27,"l1y":33,"l2x":18,"l2y":32,"l3x":34,"l3y":29,"l4x":34,"l4y":29,"l5x":19,"l5y":32},{"data":1000005,"id":500000232,"lvl":10,"x":27,"y":36,"l1x":32,"l1y":18,"l2x":36,"l2y":27,"l3x":14,"l3y":26,"l4x":24,"l4y":29,"l5x":36,"l5y":27},{"data":1000009,"id":500000233,"lvl":9,"x":21,"y":24,"l1x":36,"l1y":29,"l2x":36,"l2y":31,"l3x":12,"l3y":32,"l4x":12,"l4y":32,"l5x":36,"l5y":31},{"data":1000011,"id":500000234,"lvl":5,"x":31,"y":36,"l1x":30,"l1y":13,"l2x":30,"l2y":23,"l3x":38,"l3y":33,"l4x":20,"l4y":19,"l5x":31,"l5y":22},{"data":1000013,"id":500000235,"lvl":5,"x":27,"y":33,"l1x":21,"l1y":28,"l2x":19,"l2y":9,"l3x":33,"l3y":35,"l4x":33,"l4y":35,"l5x":19,"l5y":9},{"data":1000012,"id":500000236,"lvl":5,"x":18,"y":18,"l1x":31,"l1y":22,"l2x":22,"l2y":18,"l3x":29,"l3y":27,"l4x":29,"l4y":27,"l5x":22,"l5y":18},{"data":1000010,"id":500000237,"lvl":7,"x":30,"y":17,"l1x":30,"l1y":37,"l2x":16,"l2y":20,"l3x":17,"l3y":30,"l4x":17,"l4y":30,"l5x":16,"l5y":20},{"data":1000010,"id":500000238,"lvl":7,"x":31,"y":17,"l1x":30,"l1y":33,"l2x":16,"l2y":22,"l3x":16,"l3y":31,"l4x":16,"l4y":31,"l5x":16,"l5y":22},{"data":1000010,"id":500000239,"lvl":7,"x":32,"y":17,"l1x":38,"l1y":33,"l2x":16,"l2y":21,"l3x":17,"l3y":31,"l4x":17,"l4y":31,"l5x":16,"l5y":21},{"data":1000010,"id":500000240,"lvl":7,"x":33,"y":17,"l1x":31,"l1y":32,"l2x":17,"l2y":22,"l3x":18,"l3y":31,"l4x":18,"l4y":31,"l5x":17,"l5y":22},{"data":1000010,"id":500000241,"lvl":7,"x":15,"y":25,"l1x":33,"l1y":26,"l2x":18,"l2y":22,"l3x":19,"l3y":31,"l4x":19,"l4y":31,"l5x":18,"l5y":22},{"data":1000010,"id":500000242,"lvl":7,"x":16,"y":25,"l1x":33,"l1y":27,"l2x":19,"l2y":22,"l3x":20,"l3y":31,"l4x":20,"l4y":31,"l5x":19,"l5y":22},{"data":1000010,"id":500000243,"lvl":7,"x":17,"y":25,"l1x":33,"l1y":28,"l2x":20,"l2y":22,"l3x":22,"l3y":31,"l4x":22,"l4y":31,"l5x":20,"l5y":22},{"data":1000010,"id":500000244,"lvl":7,"x":17,"y":26,"l1x":33,"l1y":29,"l2x":21,"l2y":22,"l3x":23,"l3y":28,"l4x":23,"l4y":28,"l5x":21,"l5y":22},{"data":1000010,"id":500000245,"lvl":7,"x":17,"y":27,"l1x":33,"l1y":30,"l2x":31,"l2y":22,"l3x":23,"l3y":29,"l4x":23,"l4y":29,"l5x":32,"l5y":21},{"data":1000010,"id":500000246,"lvl":7,"x":17,"y":28,"l1x":33,"l1y":31,"l2x":32,"l2y":22,"l3x":24,"l3y":28,"l4x":24,"l4y":28,"l5x":29,"l5y":21},{"data":1000010,"id":500000247,"lvl":7,"x":17,"y":29,"l1x":33,"l1y":32,"l2x":33,"l2y":22,"l3x":31,"l3y":26,"l4x":31,"l4y":26,"l5x":34,"l5y":21},{"data":1000010,"id":500000248,"lvl":7,"x":17,"y":30,"l1x":32,"l1y":32,"l2x":33,"l2y":23,"l3x":23,"l3y":31,"l4x":23,"l4y":31,"l5x":35,"l5y":22},{"data":1000010,"id":500000249,"lvl":7,"x":17,"y":31,"l1x":22,"l1y":36,"l2x":33,"l2y":24,"l3x":30,"l3y":26,"l4x":30,"l4y":26,"l5x":35,"l5y":23},{"data":1000010,"id":500000250,"lvl":7,"x":17,"y":32,"l1x":16,"l1y":27,"l2x":33,"l2y":25,"l3x":26,"l3y":32,"l4x":26,"l4y":32,"l5x":35,"l5y":24},{"data":1000010,"id":500000251,"lvl":7,"x":17,"y":33,"l1x":15,"l1y":27,"l2x":33,"l2y":26,"l3x":27,"l3y":32,"l4x":27,"l4y":32,"l5x":32,"l5y":26},{"data":1000010,"id":500000252,"lvl":7,"x":17,"y":34,"l1x":16,"l1y":19,"l2x":34,"l2y":27,"l3x":28,"l3y":32,"l4x":28,"l4y":32,"l5x":35,"l5y":25},{"data":1000010,"id":500000253,"lvl":7,"x":34,"y":17,"l1x":38,"l1y":32,"l2x":33,"l2y":27,"l3x":29,"l3y":32,"l4x":29,"l4y":32,"l5x":31,"l5y":26},{"data":1000010,"id":500000254,"lvl":7,"x":34,"y":18,"l1x":20,"l1y":28,"l2x":32,"l2y":27,"l3x":30,"l3y":32,"l4x":30,"l4y":32,"l5x":33,"l5y":26},{"data":1000010,"id":500000255,"lvl":7,"x":34,"y":20,"l1x":20,"l1y":29,"l2x":31,"l2y":27,"l3x":32,"l3y":32,"l4x":32,"l4y":32,"l5x":30,"l5y":26},{"data":1000010,"id":500000256,"lvl":7,"x":34,"y":19,"l1x":20,"l1y":30,"l2x":30,"l2y":27,"l3x":32,"l3y":31,"l4x":32,"l4y":31,"l5x":29,"l5y":26},{"data":1000010,"id":500000257,"lvl":7,"x":34,"y":21,"l1x":14,"l1y":34,"l2x":29,"l2y":27,"l3x":32,"l3y":30,"l4x":32,"l4y":30,"l5x":29,"l5y":27},{"data":1000010,"id":500000258,"lvl":7,"x":33,"y":21,"l1x":19,"l1y":27,"l2x":30,"l2y":22,"l3x":32,"l3y":29,"l4x":32,"l4y":29,"l5x":31,"l5y":21},{"data":1000010,"id":500000259,"lvl":7,"x":18,"y":34,"l1x":21,"l1y":36,"l2x":29,"l2y":22,"l3x":32,"l3y":28,"l4x":32,"l4y":28,"l5x":28,"l5y":21},{"data":1000010,"id":500000260,"lvl":7,"x":19,"y":34,"l1x":23,"l1y":39,"l2x":28,"l2y":22,"l3x":32,"l3y":27,"l4x":32,"l4y":27,"l5x":28,"l5y":22},{"data":1000010,"id":500000261,"lvl":7,"x":20,"y":34,"l1x":23,"l1y":38,"l2x":27,"l2y":22,"l3x":32,"l3y":26,"l4x":32,"l4y":26,"l5x":27,"l5y":22},{"data":1000010,"id":500000262,"lvl":7,"x":21,"y":34,"l1x":23,"l1y":37,"l2x":26,"l2y":22,"l3x":28,"l3y":25,"l4x":28,"l4y":25,"l5x":26,"l5y":22},{"data":1000010,"id":500000263,"lvl":7,"x":21,"y":33,"l1x":23,"l1y":36,"l2x":25,"l2y":22,"l3x":31,"l3y":32,"l4x":31,"l4y":32,"l5x":25,"l5y":22},{"data":1000010,"id":500000264,"lvl":7,"x":21,"y":32,"l1x":23,"l1y":34,"l2x":24,"l2y":22,"l3x":29,"l3y":26,"l4x":29,"l4y":26,"l5x":24,"l5y":22},{"data":1000010,"id":500000265,"lvl":7,"x":32,"y":21,"l1x":23,"l1y":35,"l2x":23,"l2y":22,"l3x":28,"l3y":26,"l4x":28,"l4y":26,"l5x":23,"l5y":22},{"data":1000010,"id":500000266,"lvl":7,"x":31,"y":21,"l1x":17,"l1y":35,"l2x":22,"l2y":22,"l3x":23,"l3y":30,"l4x":23,"l4y":30,"l5x":22,"l5y":22},{"data":1000010,"id":500000267,"lvl":7,"x":30,"y":21,"l1x":17,"l1y":36,"l2x":22,"l2y":23,"l3x":27,"l3y":24,"l4x":27,"l4y":24,"l5x":22,"l5y":23},{"data":1000010,"id":500000268,"lvl":7,"x":29,"y":21,"l1x":18,"l1y":36,"l2x":22,"l2y":24,"l3x":26,"l3y":24,"l4x":26,"l4y":24,"l5x":22,"l5y":24},{"data":1000010,"id":500000269,"lvl":7,"x":28,"y":21,"l1x":18,"l1y":13,"l2x":22,"l2y":25,"l3x":25,"l3y":24,"l4x":25,"l4y":24,"l5x":22,"l5y":25},{"data":1000010,"id":500000270,"lvl":7,"x":21,"y":31,"l1x":18,"l1y":14,"l2x":22,"l2y":26,"l3x":24,"l3y":24,"l4x":24,"l4y":24,"l5x":22,"l5y":26},{"data":1000010,"id":500000271,"lvl":7,"x":21,"y":30,"l1x":18,"l1y":15,"l2x":28,"l2y":27,"l3x":24,"l3y":25,"l4x":24,"l4y":25,"l5x":28,"l5y":27},{"data":1000010,"id":500000272,"lvl":7,"x":21,"y":28,"l1x":18,"l1y":16,"l2x":27,"l2y":27,"l3x":24,"l3y":26,"l4x":24,"l4y":26,"l5x":27,"l5y":27},{"data":1000010,"id":500000273,"lvl":7,"x":27,"y":21,"l1x":17,"l1y":19,"l2x":26,"l2y":27,"l3x":24,"l3y":27,"l4x":24,"l4y":27,"l5x":26,"l5y":27},{"data":1000010,"id":500000274,"lvl":7,"x":27,"y":22,"l1x":18,"l1y":17,"l2x":25,"l2y":27,"l3x":23,"l3y":32,"l4x":23,"l4y":32,"l5x":25,"l5y":27},{"data":1000010,"id":500000275,"lvl":7,"x":27,"y":23,"l1x":19,"l1y":36,"l2x":24,"l2y":27,"l3x":24,"l3y":32,"l4x":24,"l4y":32,"l5x":24,"l5y":27},{"data":1000010,"id":500000276,"lvl":7,"x":27,"y":24,"l1x":30,"l1y":38,"l2x":23,"l2y":27,"l3x":39,"l3y":28,"l4x":39,"l4y":28,"l5x":23,"l5y":27},{"data":1000010,"id":500000277,"lvl":7,"x":21,"y":29,"l1x":29,"l1y":32,"l2x":39,"l2y":26,"l3x":21,"l3y":31,"l4x":21,"l4y":31,"l5x":39,"l5y":26},{"data":1000010,"id":500000278,"lvl":7,"x":22,"y":27,"l1x":28,"l1y":32,"l2x":40,"l2y":23,"l3x":29,"l3y":37,"l4x":29,"l4y":37,"l5x":40,"l5y":23},{"data":1000010,"id":500000279,"lvl":7,"x":26,"y":24,"l1x":27,"l1y":32,"l2x":39,"l2y":32,"l3x":36,"l3y":37,"l4x":36,"l4y":37,"l5x":39,"l5y":32},{"data":1000010,"id":500000280,"lvl":7,"x":26,"y":25,"l1x":26,"l1y":32,"l2x":22,"l2y":27,"l3x":40,"l3y":24,"l4x":40,"l4y":24,"l5x":22,"l5y":27},{"data":1000010,"id":500000281,"lvl":7,"x":24,"y":27,"l1x":25,"l1y":32,"l2x":24,"l2y":39,"l3x":28,"l3y":24,"l4x":28,"l4y":24,"l5x":24,"l5y":39},{"data":1000010,"id":500000282,"lvl":7,"x":23,"y":27,"l1x":24,"l1y":32,"l2x":31,"l2y":11,"l3x":25,"l3y":11,"l4x":25,"l4y":11,"l5x":31,"l5y":11},{"data":1000010,"id":500000283,"lvl":7,"x":26,"y":26,"l1x":23,"l1y":32,"l2x":40,"l2y":26,"l3x":17,"l3y":17,"l4x":17,"l4y":17,"l5x":40,"l5y":26},{"data":1000010,"id":500000284,"lvl":7,"x":25,"y":26,"l1x":21,"l1y":33,"l2x":40,"l2y":24,"l3x":13,"l3y":22,"l4x":13,"l4y":22,"l5x":40,"l5y":24},{"data":1000010,"id":500000285,"lvl":7,"x":24,"y":26,"l1x":20,"l1y":33,"l2x":40,"l2y":25,"l3x":15,"l3y":31,"l4x":15,"l4y":31,"l5x":40,"l5y":25},{"data":1000010,"id":500000286,"lvl":7,"x":21,"y":27,"l1x":20,"l1y":32,"l2x":24,"l2y":38,"l3x":25,"l3y":32,"l4x":25,"l4y":32,"l5x":24,"l5y":38},{"data":1000026,"id":500000288,"lvl":3,"x":10,"y":13,"unit_prod":{"unit_type":0},"l1x":40,"l1y":30,"l2x":38,"l2y":39,"l3x":38,"l3y":36,"l4x":38,"l4y":36,"l5x":38,"l5y":39},{"data":1000023,"id":500000289,"lvl":2,"x":15,"y":22,"res_time":0,"l1x":11,"l1y":37,"l2x":23,"l2y":10,"l3x":12,"l3y":35,"l4x":12,"l4y":35,"l5x":22,"l5y":11},{"data":1000029,"id":500000290,"lvl":1,"x":10,"y":10,"units":[[26000010,1]],"unit_prod":{"unit_type":1},"l1x":29,"l1y":9,"l2x":6,"l2y":23,"l3x":19,"l3y":40,"l4x":19,"l4y":40,"l5x":6,"l5y":23}],"obstacles":[{"data":8000004,"id":503000000,"x":47,"y":10,"loot_multiply_ver":2},{"data":8000007,"id":503000001,"x":1,"y":13,"loot_multiply_ver":2},{"data":8000000,"id":503000002,"x":14,"y":0,"loot_multiply_ver":2},{"data":8000008,"id":503000003,"x":0,"y":34,"loot_multiply_ver":2},{"data":8000006,"id":503000004,"x":29,"y":0,"loot_multiply_ver":2},{"data":8000007,"id":503000005,"x":19,"y":0,"loot_multiply_ver":2},{"data":8000013,"id":503000006,"x":29,"y":47,"loot_multiply_ver":2},{"data":8000007,"id":503000007,"x":7,"y":8,"loot_multiply_ver":2},{"data":8000010,"id":503000008,"x":0,"y":30,"loot_multiply_ver":2},{"data":8000000,"id":503000009,"x":25,"y":0,"loot_multiply_ver":2},{"data":8000004,"id":503000010,"x":24,"y":48,"loot_multiply_ver":2},{"data":8000000,"id":503000011,"x":8,"y":48,"loot_multiply_ver":2},{"data":8000010,"id":503000012,"x":8,"y":42,"loot_multiply_ver":2},{"data":8000000,"id":503000013,"x":1,"y":9,"loot_multiply_ver":2},{"data":8000000,"id":503000014,"x":2,"y":40,"loot_multiply_ver":2},{"data":8000004,"id":503000015,"x":35,"y":0,"loot_multiply_ver":2},{"data":8000007,"id":503000016,"x":1,"y":37,"loot_multiply_ver":2},{"data":8000007,"id":503000017,"x":15,"y":47,"loot_multiply_ver":2},{"data":8000007,"id":503000018,"x":1,"y":19,"loot_multiply_ver":2},{"data":8000000,"id":503000019,"x":0,"y":26,"loot_multiply_ver":2},{"data":8000013,"id":503000020,"x":18,"y":47,"loot_multiply_ver":2},{"data":8000007,"id":503000021,"x":48,"y":17,"loot_multiply_ver":2},{"data":8000010,"id":503000022,"x":48,"y":31,"loot_multiply_ver":2},{"data":8000004,"id":503000023,"x":11,"y":48,"loot_multiply_ver":2},{"data":8000000,"id":503000024,"x":48,"y":25,"loot_multiply_ver":2},{"data":8000008,"id":503000025,"x":48,"y":7,"loot_multiply_ver":2},{"data":8000004,"id":503000026,"x":46,"y":0,"loot_multiply_ver":2},{"data":8000007,"id":503000027,"x":48,"y":34,"loot_multiply_ver":2},{"data":8000010,"id":503000028,"x":1,"y":2,"loot_multiply_ver":2},{"data":8000004,"id":503000029,"x":0,"y":47,"loot_multiply_ver":2},{"data":8000000,"id":503000030,"x":48,"y":38,"loot_multiply_ver":2},{"data":8000010,"id":503000031,"x":1,"y":16,"loot_multiply_ver":2},{"data":8000000,"id":503000032,"x":1,"y":23,"loot_multiply_ver":2},{"data":8000006,"id":503000033,"x":39,"y":48,"loot_multiply_ver":2},{"data":8000036,"id":503000034,"x":48,"y":20,"loot_multiply_ver":2},{"data":8000006,"id":503000035,"x":47,"y":41,"loot_multiply_ver":2},{"data":8000000,"id":503000036,"x":47,"y":28,"loot_multiply_ver":2},{"data":8000007,"id":503000037,"x":33,"y":48,"loot_multiply_ver":2},{"data":8000005,"id":503000038,"x":7,"y":1,"loot_multiply_ver":2},{"data":8000036,"id":503000039,"x":41,"y":10,"loot_multiply_ver":2},{"data":8000036,"id":503000040,"x":7,"y":39,"loot_multiply_ver":1},{"data":8000036,"id":503000041,"x":48,"y":13,"loot_multiply_ver":2},{"data":8000036,"id":503000042,"x":22,"y":2,"loot_multiply_ver":2},{"data":8000036,"id":503000043,"x":42,"y":6,"loot_multiply_ver":2},{"data":8000036,"id":503000044,"x":8,"y":33,"loot_multiply_ver":1},{"data":8000037,"id":503000078,"x":36,"y":48,"loot_multiply_ver":2},{"data":8000037,"id":503000119,"x":44,"y":14,"loot_multiply_ver":2},{"data":8000037,"id":503000160,"x":36,"y":3,"loot_multiply_ver":2},{"data":8000037,"id":503000201,"x":3,"y":5,"loot_multiply_ver":2},{"data":8000037,"id":503000242,"x":17,"y":41,"loot_multiply_ver":2},{"data":8000037,"id":503000243,"x":42,"y":21,"loot_multiply_ver":2},{"data":8000037,"id":503000284,"x":40,"y":1,"loot_multiply_ver":2},{"data":8000037,"id":503000305,"x":32,"y":4,"loot_multiply_ver":2},{"data":8000009,"id":503000306,"x":40,"y":22,"loot_multiply_ver":1},{"data":8000009,"id":503000307,"x":44,"y":41,"loot_multiply_ver":1},{"data":8000009,"id":503000308,"x":43,"y":28,"loot_multiply_ver":1},{"data":8000009,"id":503000309,"x":43,"y":26,"loot_multiply_ver":1},{"data":8000009,"id":503000310,"x":44,"y":24,"loot_multiply_ver":1},{"data":8000023,"id":503000311,"x":43,"y":25,"loot_multiply_ver":1},{"data":8000023,"id":503000312,"x":44,"y":25,"loot_multiply_ver":1},{"data":8000023,"id":503000313,"x":43,"y":24,"loot_multiply_ver":1},{"data":8000023,"id":503000314,"x":42,"y":25,"loot_multiply_ver":1},{"data":8000023,"id":503000315,"x":44,"y":26,"loot_multiply_ver":1},{"data":8000009,"id":503000316,"x":33,"y":45,"loot_multiply_ver":1},{"data":8000009,"id":503000317,"x":34,"y":44,"loot_multiply_ver":1},{"data":8000009,"id":503000318,"x":38,"y":33,"loot_multiply_ver":1},{"data":8000009,"id":503000319,"x":41,"y":25,"loot_multiply_ver":1},{"data":8000009,"id":503000320,"x":42,"y":24,"loot_multiply_ver":1},{"data":8000009,"id":503000321,"x":42,"y":29,"loot_multiply_ver":1},{"data":8000009,"id":503000322,"x":41,"y":29,"loot_multiply_ver":1},{"data":8000009,"id":503000323,"x":42,"y":28,"loot_multiply_ver":1},{"data":8000009,"id":503000324,"x":43,"y":29,"loot_multiply_ver":1},{"data":8000009,"id":503000325,"x":32,"y":43,"loot_multiply_ver":1},{"data":8000009,"id":503000326,"x":31,"y":43,"loot_multiply_ver":1},{"data":8000009,"id":503000327,"x":30,"y":43,"loot_multiply_ver":1},{"data":8000009,"id":503000328,"x":33,"y":43,"loot_multiply_ver":1},{"data":8000009,"id":503000329,"x":29,"y":42,"loot_multiply_ver":1},{"data":8000009,"id":503000330,"x":28,"y":42,"loot_multiply_ver":1},{"data":8000009,"id":503000331,"x":29,"y":43,"loot_multiply_ver":1},{"data":8000009,"id":503000332,"x":26,"y":37,"loot_multiply_ver":1},{"data":8000009,"id":503000333,"x":20,"y":39,"loot_multiply_ver":1},{"data":8000009,"id":503000334,"x":20,"y":38,"loot_multiply_ver":1},{"data":8000009,"id":503000335,"x":27,"y":25,"loot_multiply_ver":1},{"data":8000009,"id":503000336,"x":26,"y":34,"loot_multiply_ver":1},{"data":8000009,"id":503000337,"x":32,"y":22,"loot_multiply_ver":1},{"data":8000009,"id":503000338,"x":35,"y":20,"loot_multiply_ver":1},{"data":8000009,"id":503000339,"x":26,"y":35,"loot_multiply_ver":1},{"data":8000009,"id":503000340,"x":36,"y":20,"loot_multiply_ver":1},{"data":8000009,"id":503000341,"x":28,"y":26,"loot_multiply_ver":1},{"data":8000009,"id":503000342,"x":26,"y":36,"loot_multiply_ver":1},{"data":8000009,"id":503000343,"x":28,"y":25,"loot_multiply_ver":1},{"data":8000009,"id":503000344,"x":29,"y":26,"loot_multiply_ver":1},{"data":8000009,"id":503000345,"x":20,"y":35,"loot_multiply_ver":1},{"data":8000038,"id":503000346,"x":45,"y":3,"defg":117171,"defe":104847,"defde":307,"loot_multiply_ver":1}],"decos":[],"respawnVars":{"secondsFromLastRespawn":231386,"respawnSeed":1914935487,"obstacleClearCounter":5,"time_to_gembox_drop":196966,"time_in_gembox_period":228665},"cooldowns":[],"newShopBuildings":[4,0,6,3,6,3,4,1,5,5,225,3,3,4,1,5,0,0,0,3,1,0,1,2,1,0,2,0,1,1,0,0],"newShopTraps":[6,6,3,0,0,4,2,0,2],"newShopDecos":[1,4,0,1,1,4,4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],"last_league_rank":9,"last_alliance_level":7,"last_league_shuffle":1,"last_season_seen":0,"last_news_seen":221,"troop_req_msg":"max hogs and max poison","war_req_msg":"2 witches, 1 barb","war_tutorials_seen":0,"war_base":true,"account_flags":14,"bool_layout_edit_shown_erase":true} \ No newline at end of file diff --git a/src/main/resources/tile.png b/src/main/resources/tile.png new file mode 100644 index 0000000..4f667c8 Binary files /dev/null and b/src/main/resources/tile.png differ diff --git a/src/main/resources/tile_center.jpg b/src/main/resources/tile_center.jpg new file mode 100644 index 0000000..e099807 Binary files /dev/null and b/src/main/resources/tile_center.jpg differ diff --git a/src/main/resources/tiles.jpg b/src/main/resources/tiles.jpg new file mode 100644 index 0000000..37dab96 Binary files /dev/null and b/src/main/resources/tiles.jpg differ diff --git a/src/main/resources/web/base-analysis.mustache b/src/main/resources/web/base-analysis.mustache new file mode 100644 index 0000000..1d9797d --- /dev/null +++ b/src/main/resources/web/base-analysis.mustache @@ -0,0 +1,115 @@ +{{ + {{/styles}} + + {{$navbar}} + {{#times}} + + {{/times}} + + + {{/navbar}} + + {{$content}} + {{#warning}} +
+
+
{{warning}}
+
+
+ {{/warning}} + +
+
+
+
+
+ +
+
+ {{/content}} + + {{$scripts}} + + + + + + + + + + + + + + + + + + + + {{/scripts}} +{{/base}} \ No newline at end of file diff --git a/src/main/resources/web/base.mustache b/src/main/resources/web/base.mustache new file mode 100644 index 0000000..41be6ed --- /dev/null +++ b/src/main/resources/web/base.mustache @@ -0,0 +1,45 @@ + + + + Clash War Base Analyser - {{$title}}{{/title}} + + + + {{$styles}}{{/styles}} + + + + +
+ {{$content}}{{/content}} +
+ + + {{$scripts}}{{/scripts}} + + \ No newline at end of file diff --git a/src/main/resources/web/clan.mustache b/src/main/resources/web/clan.mustache new file mode 100644 index 0000000..6423950 --- /dev/null +++ b/src/main/resources/web/clan.mustache @@ -0,0 +1,38 @@ +{{ +
+

Bulk Analysis

+ {{bulkAnalysisUrl}} +
+ + +
+
+

Current Players

+ + + + + + + + + + {{#players}} + + + + + + {{/players}} + +
IGNBase Analysis - Active WarBase Analysis - Home
{{ign}}{{warAnalysisUrl}}{{homeAnalysisUrl}}
+
+
+ {{/content}} +{{/base}} \ No newline at end of file diff --git a/src/main/resources/web/error.mustache b/src/main/resources/web/error.mustache new file mode 100644 index 0000000..6b9bb0a --- /dev/null +++ b/src/main/resources/web/error.mustache @@ -0,0 +1,9 @@ +{{{{message}}

+ {{/content}} +{{/base}} \ No newline at end of file diff --git a/src/main/resources/web/index.html b/src/main/resources/web/index.html new file mode 100644 index 0000000..5d8289b --- /dev/null +++ b/src/main/resources/web/index.html @@ -0,0 +1,16 @@ + + + + CoC War Base Analyser + + +
+
+
+

CoC War Base Analyser

+

Nothing to see here... Ask for the link to your clan page from the developer

+
+
+
+ + \ No newline at end of file diff --git a/src/main/resources/web/war-bases.mustache b/src/main/resources/web/war-bases.mustache new file mode 100644 index 0000000..b43b78f --- /dev/null +++ b/src/main/resources/web/war-bases.mustache @@ -0,0 +1,89 @@ +{{ + .glyphicon-remove-sign { + color: #ff5555; + } + button.collapsed .hide-instruction { + display:none; + } + + {{/styles}} + + {{$heading}}{{name}} Bulk War Bases Analysis{{/heading}} + + {{$content}} +
+
+ Analysing: + +
+
+
+ +
+ +
+ +
+
+
+
+ {{/content}} + + {{$scripts}} + + + + + + + + + {{/scripts}} +{{/base}} \ No newline at end of file diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/Facades.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/Facades.scala new file mode 100644 index 0000000..9cbea0d --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/Facades.scala @@ -0,0 +1,71 @@ +package org.danielholmes.coc.baseanalyser + +import java.time.Duration + +import org.danielholmes.coc.baseanalyser.analysis.{AnalysisReport, VillageAnalyser} +import org.danielholmes.coc.baseanalyser.gameconnection.ClanSeekerProtocol.PlayerVillage +import org.danielholmes.coc.baseanalyser.gameconnection.GameConnection +import org.danielholmes.coc.baseanalyser.baseparser.VillageJsonParser +import org.danielholmes.coc.baseanalyser.model.Layout._ +import org.danielholmes.coc.baseanalyser.model.{Layout, Village} +import org.danielholmes.coc.baseanalyser.util.TimedInvocation +import org.danielholmes.coc.baseanalyser.web.PermittedClan +import org.scalactic.{Bad, Good, Or} + +class Facades( + permittedClans: Set[PermittedClan], + gameConnection: GameConnection, + villageJsonParser: VillageJsonParser, + villageAnalyser: VillageAnalyser +) { + def getVillageAnalysis(clanCode: String, playerId: Long, layoutName: String): + ((PermittedClan, Option[AnalysisReport], Village, PlayerVillage, Layout, Duration) Or String) = { + permittedClans.find(_.code == clanCode) + .map(clan => + Layout.values.find(_.toString == layoutName) + .map(layout => + TimedInvocation.run(() => gameConnection.getPlayerVillage(playerId)) match { + case (p: Option[PlayerVillage], connectionDuration: Duration) => + p.filter(_.avatar.clanId == clan.id) + .map(player => + villageJsonParser.parse(player.village.raw) + .getByLayout(layout) + .map(village => { + val analysis = villageAnalyser.analyse(village) + Good((clan, analysis, village, player, layout, connectionDuration)) + }) + .getOrElse(Bad(s"Player ${player.avatar.userName} doesn't have $layout village")) + ) + .getOrElse(Bad(s"id $playerId not found in clan ${clan.name}")) + } + ) + .getOrElse(Bad(s"Layout type $layoutName unknown")) + ) + .getOrElse(Bad(s"Clan with code $clanCode not found")) + } + + def getWarVillageByUserName(clanCode: String, userName: String): Village Or String = { + permittedClans.find(_.code == clanCode) + .map(clan => + gameConnection.getClanDetails(clan.id) + .map(clanDetails => + clanDetails.players.find(_.avatar.userName.equalsIgnoreCase(userName)) + .map(_.avatar.currentHomeId) + .map(userId => + gameConnection.getPlayerVillage(userId) + .map(_.village.raw) + .map(villageJsonParser.parse) + .map( + _.war + .map(Good(_)) + .getOrElse(Bad(s"User $userName has no war village")) + ) + .getOrElse(Bad("Error communicating with servers")) + ) + .getOrElse(Bad(s"Player $userName doesn't exist")) + ) + .getOrElse(Bad("Error communicating with servers")) + ) + .getOrElse(Bad(s"Clan code $clanCode not found")) + } +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/PrintAttackPlacements.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/PrintAttackPlacements.scala new file mode 100644 index 0000000..0a98f4f --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/PrintAttackPlacements.scala @@ -0,0 +1,17 @@ +package org.danielholmes.coc.baseanalyser + +object PrintAttackPlacements extends App with Services { + if (args.length != 2) { + throw new RuntimeException("Must provide clan code and userName arg") + } + + val clanCode = args(0) + val userName = args(1) + + println( + facades.getWarVillageByUserName(clanCode, userName) + .map(stringTroopDropDisplayer.build) + .recover(s => s) + .get + ) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/PrintVillage.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/PrintVillage.scala new file mode 100644 index 0000000..af88ef3 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/PrintVillage.scala @@ -0,0 +1,17 @@ +package org.danielholmes.coc.baseanalyser + +object PrintVillage extends App with Services { + if (args.length != 2) { + throw new RuntimeException("Must provide clan code and userName arg") + } + + val clanCode = args(0) + val userName = args(1) + + println( + facades.getWarVillageByUserName(clanCode, userName) + .map(stringDisplayer.buildColoured) + .recover(s => s) + .get + ) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/ProfileAnalysis.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/ProfileAnalysis.scala new file mode 100644 index 0000000..5c0ed57 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/ProfileAnalysis.scala @@ -0,0 +1,39 @@ +package org.danielholmes.coc.baseanalyser + +import java.time.Duration + +import org.danielholmes.coc.baseanalyser.model.{Tile, TileCoordinate, WallCompartment} +import org.danielholmes.coc.baseanalyser.util.TimedInvocation + +object ProfileAnalysis extends App with Services { + if (args.length != 2) { + throw new RuntimeException("Must provide clan code and userName arg") + } + + val clanCode = args(0) + val userName = args(1) + + private def formatSecs(duration: Duration): String = "%.3f".format(duration.toMillis / 1000.0) + "s" + + println( + facades.getWarVillageByUserName(clanCode, userName) + .map(village => { + villageAnalyser.analyse(village) + .map(report => { + val all = Map( + "Building blocks" -> report.profiling.buildingBlocksSorted.map(t => (t._1, formatSecs(t._2))), + "Rules" -> report.profiling.rulesSorted.map(t => (t._1.shortName, formatSecs(t._2))), + "Total" -> Seq(("", formatSecs(report.profiling.total))) + ) + val lineLength = all.values.flatMap(t => t.map(t => t._1.length + t._2.length + 1)).max + all.mapValues(_.map(t => t._1 + (" " * (lineLength - t._1.length - t._2.length)) + t._2)) + .toSeq + .flatMap(t => Seq("-" * lineLength + s"\n# ${t._1}") ++ t._2) + .mkString("\n") + }) + .getOrElse("Couldn't analyse") + }) + .recover(e => Console.RED + e + Console.RED) + .get + ) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/Services.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/Services.scala new file mode 100644 index 0000000..dac380a --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/Services.scala @@ -0,0 +1,69 @@ +package org.danielholmes.coc.baseanalyser + +import com.github.mustachejava.DefaultMustacheFactory +import com.twitter.mustache.ScalaObjectHandler +import org.danielholmes.coc.baseanalyser.analysis._ +import org.danielholmes.coc.baseanalyser.gameconnection.{ClanSeekerGameConnection, StubGameConnection} +import org.danielholmes.coc.baseanalyser.baseparser.{HardCodedElementFactory, VillageJsonParser} +import org.danielholmes.coc.baseanalyser.stringdisplay.{StringDisplayer, StringTroopDropDisplayer} +import org.danielholmes.coc.baseanalyser.web.{MustacheRenderer, PermittedClan, ViewModelMapper} +import org.scalactic.anyvals.PosInt + +trait Services { + import com.softwaremill.macwire._ + + //lazy val gameConnection = wire[ClanSeekerGameConnection] + lazy val gameConnection = wire[StubGameConnection]; println("WARNING: Test stubbed game connection enabled") + private lazy val elementFactory = wire[HardCodedElementFactory] + lazy val villageJsonParser = wire[VillageJsonParser] + private lazy val th8Rules: Set[Rule] = Set( + wire[HogCCLureRule], + wire[HighHPUnderAirDefRule], + wire[ArcherAnchorRule], + wire[AirSnipedDefenseRule], + wire[MinimumCompartmentsRule], + wire[BKSwappableRule], + wire[EnoughPossibleTrapLocationsRule] + ) + private lazy val th9Rules: Set[Rule] = Set( + wire[HogCCLureRule], + wire[AirSnipedDefenseRule], + wire[WizardTowersOutOfHoundPositionsRule], + wire[QueenWalkedAirDefenseRule], + wire[QueenWontLeaveCompartmentRule], + wire[EnoughPossibleTrapLocationsRule] + ) + private lazy val th10Rules: Set[Rule] = Set( + wire[HogCCLureRule], + wire[AirSnipedDefenseRule], + wire[WizardTowersOutOfHoundPositionsRule], + wire[QueenWalkedAirDefenseRule] + ) + private lazy val th11Rules: Set[Rule] = th10Rules + private lazy val rulesByThLevel = Map( + PosInt(8) -> th8Rules, + PosInt(9) -> th9Rules, + PosInt(10) -> th10Rules, + PosInt(11) -> th11Rules + ) + + private def mustacheFactory = { + val mf = new DefaultMustacheFactory() + mf.setObjectHandler(new ScalaObjectHandler()) + mf + } + + lazy val mustacheRenderer = wire[MustacheRenderer] + + lazy val permittedClans = Set[PermittedClan]( + PermittedClan("alpha", "OneHive Alpha", 154621406673L), + PermittedClan("genesis", "OneHive Genesis", 128850679685L), + PermittedClan("uncool", "Uncool", 103079424453L), + PermittedClan("aerial", "Aerial Assault", 227634713283L) + ) + lazy val villageAnalyser = wire[VillageAnalyser] + lazy val stringDisplayer = wire[StringDisplayer] + lazy val stringTroopDropDisplayer = wire[StringTroopDropDisplayer] + lazy val viewModelMapper = wire[ViewModelMapper] + lazy val facades = wire[Facades] +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/AirSnipedDefenseRule.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/AirSnipedDefenseRule.scala new file mode 100644 index 0000000..9ada0c5 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/AirSnipedDefenseRule.scala @@ -0,0 +1,41 @@ +package org.danielholmes.coc.baseanalyser.analysis + +import org.danielholmes.coc.baseanalyser.model.troops.{Minion, MinionAttackPosition} +import org.danielholmes.coc.baseanalyser.model.{Defense, Target, Village} + +class AirSnipedDefenseRule extends Rule { + def analyse(village: Village): RuleResult = { + val defensesByAir = village.defenses + .partition(_.targets.contains(Target.Air)) + + AirSnipedDefenseRuleResult( + noDefenseRangesCoverMinion(defensesByAir._2, defensesByAir._1), + defensesByAir._1 + ) + } + + private def noDefenseRangesCoverMinion(ground: Set[Defense], air: Set[Defense]): Set[MinionAttackPosition] = { + ground.flatMap(bestNonAirCoveredAttackPosition(_, air)) + } + + private def bestNonAirCoveredAttackPosition(ground: Defense, air: Set[Defense]): Option[MinionAttackPosition] = { + Minion.getAttackFloatCoordinates(ground) + .filter(coordinate => !air.exists(_.range.contains(coordinate))) + .map(MinionAttackPosition(_, ground)) + .headOption + } +} + +case class AirSnipedDefenseRuleResult(snipedDefenses: Set[MinionAttackPosition], airDefenses: Set[Defense]) extends RuleResult { + val success = snipedDefenses.isEmpty + val ruleDetails = AirSnipedDefenseRule.Details +} + +object AirSnipedDefenseRule { + val Details = RuleDetails( + "AirSnipedDefense", + "Air covers Ground Defs", + "Ground Defenses covered by Air", + "No ground only defenses should be reachable by minions or loons" + ) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/AnalysisProfiling.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/AnalysisProfiling.scala new file mode 100644 index 0000000..1f7029d --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/AnalysisProfiling.scala @@ -0,0 +1,13 @@ +package org.danielholmes.coc.baseanalyser.analysis + +import java.time.Duration + +case class AnalysisProfiling(buildingBlocks: Map[String, Duration], rules: Map[RuleDetails, Duration]) { + lazy val rulesDuration = rules.values.fold(Duration.ZERO)(_.plus(_)) + lazy val buildingBlocksDuration = buildingBlocks.values.fold(Duration.ZERO)(_.plus(_)) + + lazy val rulesSorted: List[(RuleDetails, Duration)] = rules.toList.sortBy(_._2).reverse + lazy val buildingBlocksSorted: List[(String, Duration)] = buildingBlocks.toList.sortBy(_._2).reverse + + lazy val total: Duration = rulesDuration.plus(buildingBlocksDuration) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/AnalysisReport.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/AnalysisReport.scala new file mode 100644 index 0000000..46add12 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/AnalysisReport.scala @@ -0,0 +1,5 @@ +package org.danielholmes.coc.baseanalyser.analysis + +import org.danielholmes.coc.baseanalyser.model.Village + +case class AnalysisReport(village: Village, results: Set[RuleResult], profiling: AnalysisProfiling) diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/ArcherAnchorRule.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/ArcherAnchorRule.scala new file mode 100644 index 0000000..0cc41dc --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/ArcherAnchorRule.scala @@ -0,0 +1,42 @@ +package org.danielholmes.coc.baseanalyser.analysis + +import org.danielholmes.coc.baseanalyser.model._ +import org.danielholmes.coc.baseanalyser.model.troops.{ArcherTargeting, Archer} + +// TODO: Shouldn't take into account EagleArtillery since wont be activated. Test this +class ArcherAnchorRule extends Rule { + def analyse(village: Village): RuleResult = { + val targetImmediateArcherGroundDefenses = village.groundTargetingDefenses + .filter({ + case d: DelayedActivation => false + case g: Defense => true + }) + + val safeCoords = village.coordinatesAllowedToDropTroop + .filter(coord => !targetImmediateArcherGroundDefenses.exists(_.range.contains(coord))) + val possibleTargets = Archer.getAllPossibleTargets(village) + + ArcherAnchorRuleResult( + possibleTargets.flatMap(possibleTarget => { + safeCoords.intersect(Archer.getAttackTileCoordinates(possibleTarget)) + .find(_ => true) + .map(coord => ArcherTargeting(coord, possibleTarget)) + }), + targetImmediateArcherGroundDefenses + ) + } +} + +case class ArcherAnchorRuleResult(targeting: Set[ArcherTargeting], aimingDefenses: Set[Defense]) extends RuleResult { + val success = targeting.isEmpty + val ruleDetails = ArcherAnchorRule.Details +} + +object ArcherAnchorRule { + val Details = RuleDetails( + "ArcherAnchor", + "No Arch Anchors", + "No Archer Anchors", + "There should be no unprotected archer anchors" + ) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/BKSwappableRule.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/BKSwappableRule.scala new file mode 100644 index 0000000..4344ec2 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/BKSwappableRule.scala @@ -0,0 +1,81 @@ +package org.danielholmes.coc.baseanalyser.analysis + +import org.danielholmes.coc.baseanalyser.model._ +import org.danielholmes.coc.baseanalyser.model.heroes.BarbarianKingAltar +import org.scalactic.anyvals.{PosDouble, PosZDouble} + +import scala.annotation.tailrec + +class BKSwappableRule extends Rule { + private val CloseEnoughFromDropToSwap = PosDouble(5.5) + private val MinExposedDistance = PosDouble(1.75) + + def analyse(village: Village): RuleResult = { + val exposedTiles = findExposedTiles(village) + BKSwappableRuleResult( + findTouchingTiles( + findTriggerTiles(village, exposedTiles), + exposedTiles, + Set.empty + ) + ) + } + + @tailrec + private def findTouchingTiles(touchingTrigger: Set[Tile], exposedToCheck: Set[Tile], current: Set[Tile]): Set[Tile] = { + touchingTrigger.toList match { + case Nil => current + case head :: tail => + val exposedTouchingTrigger = touchingTrigger.head.neighbours.intersect(exposedToCheck) + findTouchingTiles( + touchingTrigger ++ exposedTouchingTrigger -- current, + exposedToCheck -- exposedTouchingTrigger, + current + head + ) + } + } + + private def findTriggerTiles(village: Village, exposedTiles: Set[Tile]) = { + village.findElementByType[BarbarianKingAltar] + .map(_.range) + .map(_.inset(MinExposedDistance)) + .map(_.allTouchingTiles) + .getOrElse(Set.empty) + .intersect(exposedTiles) + } + + private def findExposedTiles(village: Village) = { + village.elements + .find(_.isInstanceOf[BarbarianKingAltar]) + .map(_.asInstanceOf[BarbarianKingAltar]) + .map(_.range) + .map(_.allTouchingTiles) + .getOrElse(Set.empty) + .filter(inHoleOrOutsideCompartmentAndCloseEnoughToDrop(village, _)) + } + + private def inHoleOrOutsideCompartmentAndCloseEnoughToDrop(village: Village, tile: Tile) = { + village.tilesAllowedToDropTroop.contains(tile) || + (village.wallCompartments.forall(!_.contains(tile)) && closestDropDistance(village, tile) <= CloseEnoughFromDropToSwap) + } + + private def closestDropDistance(village: Village, tile: Tile): PosZDouble = { + village.tilesAllowedToDropTroop + .map(_.distanceTo(tile)) + .min + } +} + +case class BKSwappableRuleResult(exposedTiles: Set[Tile]) extends RuleResult { + val success = exposedTiles.isEmpty + val ruleDetails = BKSwappableRule.Details +} + +object BKSwappableRule { + val Details = RuleDetails( + "BKSwappable", + "BK protected", + "BK should be protected", + "The BK's range should be inside walls so he can't be lureed out and killed early as part of a tanking BK or KS" + ) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/EnoughPossibleTrapLocationsRule.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/EnoughPossibleTrapLocationsRule.scala new file mode 100644 index 0000000..d5d2508 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/EnoughPossibleTrapLocationsRule.scala @@ -0,0 +1,45 @@ +package org.danielholmes.coc.baseanalyser.analysis + +import org.danielholmes.coc.baseanalyser.model.Village +import org.scalactic.anyvals.PosZDouble + +class EnoughPossibleTrapLocationsRule extends Rule { + def analyse(village: Village): RuleResult = { + EnoughPossibleTrapLocationsRuleResult(calculateScore(village)) + } + + def calculateScore(village: Village): PosZDouble = { + PosZDouble.from( + village.possibleInternalLargeTraps + .flatMap(_.tiles) + .groupBy(tile => village.possibleInternalLargeTraps.count(trap => trap.tiles.contains(tile))) + .mapValues(_.size) + .map({ + case (1, c) => EnoughPossibleTrapLocationsRule.TileScore * c + case (_, c) => EnoughPossibleTrapLocationsRule.MultiUseTileScore * c + }) + .sum + ).get + } +} + +case class EnoughPossibleTrapLocationsRuleResult(score: PosZDouble) extends RuleResult { + val success = score >= EnoughPossibleTrapLocationsRule.MinScore + val minScore = EnoughPossibleTrapLocationsRule.MinScore + val ruleDetails = EnoughPossibleTrapLocationsRule.Details +} + +object EnoughPossibleTrapLocationsRule { + val TileScore = 0.25 + + val MultiUseTileScore = TileScore * 0.8 + + val MinScore = 22 + + val Details = RuleDetails( + "EnoughPossibleTrapLocations", + "Enough Possible Traps", + "Enough Possible Trap Locations", + "As well as real trap locations, your base should provide enough decoy locations to keep the attacker unsure and guessing about where the real traps are" + ) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/HighHPUnderAirDefRule.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/HighHPUnderAirDefRule.scala new file mode 100644 index 0000000..8ba4ed0 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/HighHPUnderAirDefRule.scala @@ -0,0 +1,56 @@ +package org.danielholmes.coc.baseanalyser.analysis + +import org.danielholmes.coc.baseanalyser.model._ +import org.danielholmes.coc.baseanalyser.model.defense.AirDefense +import org.danielholmes.coc.baseanalyser.model.special.{ClanCastle, TownHall} +import org.danielholmes.coc.baseanalyser.model.trash.{DarkElixirStorage, ElixirStorage, GoldStorage} +import org.danielholmes.coc.baseanalyser.model.troops.Dragon + +import scala.annotation.tailrec + +class HighHPUnderAirDefRule extends Rule { + def analyse(village: Village): RuleResult = { + val highHPBuildings = village.structures.filter(isHighHPBuilding) + val covered = highHPBuildings.partition(willSomeAirDefCoverDragonShooting(_, village.airDefenses)) + HighHPUnderAirDefRuleResult(covered._2, covered._1) + } + + private def willSomeAirDefCoverDragonShooting(highHP: Structure, airDefs: Set[AirDefense]): Boolean = { + willSomeAirDefCoverDragonShooting(Dragon.getAttackFloatCoordinates(highHP), airDefs) + } + + @tailrec + private def willSomeAirDefCoverDragonShooting(highHPCoords: Set[FloatMapCoordinate], airDefs: Set[AirDefense]): Boolean = { + highHPCoords.toList match { + case Nil => true + case head :: tail => airDefs.exists(_.range.contains(head)) && willSomeAirDefCoverDragonShooting(tail.toSet, airDefs) + } + } + + private def isHighHPBuilding(structure: Structure): Boolean = { + structure match { + case _: GoldStorage => true + case _: DarkElixirStorage => true + case _: ElixirStorage => true + case _: TownHall => true + case _: ClanCastle => true + case _ => false + } + } +} + +case class HighHPUnderAirDefRuleResult(outOfAirDefRange: Set[Structure], inAirDefRange: Set[Structure]) extends RuleResult { + require(outOfAirDefRange.intersect(inAirDefRange).isEmpty) + + val success = outOfAirDefRange.isEmpty + val ruleDetails = HighHPUnderAirDefRule.Details +} + +object HighHPUnderAirDefRule { + val Details = RuleDetails( + "HighHPUnderAirDef", + "Air Covers High HP", + "High HP covered by Air Defenses", + "All high HP buildings should be within range of your air defenses" + ) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/HogCCLureRule.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/HogCCLureRule.scala new file mode 100644 index 0000000..90bedb3 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/HogCCLureRule.scala @@ -0,0 +1,32 @@ +package org.danielholmes.coc.baseanalyser.analysis + +import org.danielholmes.coc.baseanalyser.model._ +import org.danielholmes.coc.baseanalyser.model.troops.{HogTargeting, HogRider} + +class HogCCLureRule extends Rule { + def analyse(village: Village): RuleResult = { + village.clanCastle + .map(_.range) + .map(range => + village.edgeOfHitCoordinatesAllowedToDropTroop + .flatMap(coord => HogRider.findTargets(coord, village).map(HogTargeting(coord, _))) + .filter(_.cutsRadius(range)) + ) + .map(HogCCLureRuleResult) + .getOrElse(HogCCLureRuleResult(Set.empty)) + } +} + +case class HogCCLureRuleResult(targeting: Set[HogTargeting]) extends RuleResult { + val success = targeting.isEmpty + val ruleDetails = HogCCLureRule.Details +} + +object HogCCLureRule { + val Details = RuleDetails( + "HogCCLure", + "No Easy Lure", + "No Easy CC Troops Lure", + "There should be no spaces that allow a hog or giant to lure without first having to destroy a defense" + ) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/MinimumCompartmentsRule.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/MinimumCompartmentsRule.scala new file mode 100644 index 0000000..9afd3aa --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/MinimumCompartmentsRule.scala @@ -0,0 +1,29 @@ +package org.danielholmes.coc.baseanalyser.analysis + +import org.danielholmes.coc.baseanalyser.model.troops.{Minion, MinionAttackPosition} +import org.danielholmes.coc.baseanalyser.model.{Defense, Target, Village, WallCompartment} +import org.scalactic.anyvals.PosInt + +class MinimumCompartmentsRule extends Rule { + def analyse(village: Village): RuleResult = { + MinimumCompartmentsRuleResult(MinimumCompartmentsRule.Min, village.wallCompartments.filter(_.elements.nonEmpty)) + } +} + +case class MinimumCompartmentsRuleResult(minimumCompartments: PosInt, buildingCompartments: Set[WallCompartment]) extends RuleResult { + require(buildingCompartments.forall(_.elements.nonEmpty)) + + val success = buildingCompartments.size >= minimumCompartments + val ruleDetails = MinimumCompartmentsRule.Details +} + +object MinimumCompartmentsRule { + val Min = PosInt(8) + + val Details = RuleDetails( + "MinimumCompartments", + s">= ${Min.toInt} compartments", + s"At least ${Min.toInt} compartments", + "GoWiPe can be slowed down by having enough compartments (with buildings inside them) to hold it up" + ) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/PlayerAnalysisReport.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/PlayerAnalysisReport.scala new file mode 100644 index 0000000..fa60fbe --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/PlayerAnalysisReport.scala @@ -0,0 +1,3 @@ +package org.danielholmes.coc.baseanalyser.analysis + +case class PlayerAnalysisReport(userName: String, townHallLevel: Int, villageReport: Option[AnalysisReport]) diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/QueenWalkedAirDefenseRule.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/QueenWalkedAirDefenseRule.scala new file mode 100644 index 0000000..f809df8 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/QueenWalkedAirDefenseRule.scala @@ -0,0 +1,33 @@ +package org.danielholmes.coc.baseanalyser.analysis + +import org.danielholmes.coc.baseanalyser.model.troops.{ArcherQueen, ArcherQueenAttacking} +import org.danielholmes.coc.baseanalyser.model.Village +import org.danielholmes.coc.baseanalyser.model.defense.AirDefense + + +class QueenWalkedAirDefenseRule extends Rule { + def analyse(village: Village): RuleResult = { + val attackings: Set[ArcherQueenAttacking] = village.airDefenses + .flatMap(el => + ArcherQueen.firstPossibleAttackingCoordinate(el, village.outerTileCoordinates) + .map(ArcherQueenAttacking(_, el)) + ) + val targetings = attackings.map(_.targeting).map(_.asInstanceOf[AirDefense]) + val nonReachableAirDefs = village.airDefenses.diff(targetings) + QueenWalkedAirDefenseRuleResult(attackings, nonReachableAirDefs) + } +} + +case class QueenWalkedAirDefenseRuleResult(attackings: Set[ArcherQueenAttacking], nonReachableAirDefs: Set[AirDefense]) extends RuleResult { + val success = attackings.isEmpty + val ruleDetails = QueenWalkedAirDefenseRule.Details +} + +object QueenWalkedAirDefenseRule { + val Details = RuleDetails( + "QueenWalkedAirDefense", + "AirDef not AQ Walkable", + "Air Defenses not Queen Walkable", + "Air Defenses shouldn't be reachable over a wall by a queen walking outside" + ) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/QueenWontLeaveCompartmentRule.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/QueenWontLeaveCompartmentRule.scala new file mode 100644 index 0000000..c35da6c --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/QueenWontLeaveCompartmentRule.scala @@ -0,0 +1,35 @@ +package org.danielholmes.coc.baseanalyser.analysis + +import org.danielholmes.coc.baseanalyser.model.Village +import org.danielholmes.coc.baseanalyser.model.heroes.ArcherQueenAltar +import org.scalactic.anyvals.PosInt + +class QueenWontLeaveCompartmentRule extends Rule { + def analyse(village: Village): RuleResult = { + village.elements + .find(_.isInstanceOf[ArcherQueenAltar]) + .map(queen => + village.wallCompartments + .find(_.elements.contains(queen)) + .filter(c => queen.block.expandBy(QueenWontLeaveCompartmentRule.MinClearance).tiles.subsetOf(c.innerTiles)) + .map(q => QueenWontLeaveCompartmentRuleResult(true)) + .getOrElse(QueenWontLeaveCompartmentRuleResult(false)) + ) + .getOrElse(QueenWontLeaveCompartmentRuleResult(true)) + } +} + +case class QueenWontLeaveCompartmentRuleResult(success: Boolean) extends RuleResult { + val ruleDetails = QueenWontLeaveCompartmentRule.Details +} + +object QueenWontLeaveCompartmentRule { + val MinClearance = PosInt(3) + + val Details = RuleDetails( + "QueenWontLeaveCompartment", + "AQ in >= 9x9", + "Archer Queen wont leave compartment", + "Your Archer Queen should be within a compartment large enough so that she won't jump out (centre of 9x9)" + ) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/Rule.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/Rule.scala new file mode 100644 index 0000000..7fa899d --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/Rule.scala @@ -0,0 +1,7 @@ +package org.danielholmes.coc.baseanalyser.analysis + +import org.danielholmes.coc.baseanalyser.model.Village + +trait Rule { + def analyse(village: Village): RuleResult +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/RuleDetails.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/RuleDetails.scala new file mode 100644 index 0000000..d38bc5b --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/RuleDetails.scala @@ -0,0 +1,3 @@ +package org.danielholmes.coc.baseanalyser.analysis + +case class RuleDetails(code: String, shortName: String, name: String, description: String) diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/RuleResult.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/RuleResult.scala new file mode 100644 index 0000000..47cb7d4 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/RuleResult.scala @@ -0,0 +1,7 @@ +package org.danielholmes.coc.baseanalyser.analysis + +trait RuleResult { + val success: Boolean + + val ruleDetails: RuleDetails +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/VillageAnalyser.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/VillageAnalyser.scala new file mode 100644 index 0000000..bf96135 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/VillageAnalyser.scala @@ -0,0 +1,57 @@ +package org.danielholmes.coc.baseanalyser.analysis + +import java.time.Duration + +import org.danielholmes.coc.baseanalyser.model.{Tile, Village} +import org.danielholmes.coc.baseanalyser.util.TimedInvocation +import org.scalactic.anyvals.PosInt + +class VillageAnalyser(private val rulesByThLevel: Map[PosInt, Set[Rule]]) { + require(rulesByThLevel.nonEmpty) + + lazy val minTownHallLevel = rulesByThLevel.keys.min + + lazy val maxTownHallLevel = rulesByThLevel.keys.max + + def analyse(village: Village): Option[AnalysisReport] = { + // NOTE: Times taken are thrown out if done in parallel + village.townHallLevel + .flatMap(rulesByThLevel.get) + .map(analyse(_, village)) + } + + private def analyse(rules: Set[Rule], village: Village): AnalysisReport = { + val buildingBlockTimes = runBuildingBlocks(village) + buildReport(village, rules.map(rule => invokeRule(rule, village)), buildingBlockTimes) + } + + private def runBuildingBlocks(village: Village): Map[String, Duration] = { + // Walls? + Seq( + ("Outer Tiles", () => village.outerTiles), + ("Wall Compartments", () => village.wallCompartments), + ("Allowed To Drop", () => village.coordinatesAllowedToDropTroop), + ("Edge Prevent Drop", () => village.edgeOfHitCoordinatesAllowedToDropTroop) + ).map(titleOp => (titleOp._1, TimedInvocation.run(titleOp._2)._2)) + .toMap + } + + private def buildReport( + village: Village, + invocations: Set[(RuleResult, Duration)], + buildingBlockTimes: Map[String, Duration] + ): AnalysisReport = { + AnalysisReport( + village, + invocations.map(_._1), + AnalysisProfiling( + buildingBlockTimes, + invocations.map(i => (i._1.ruleDetails, i._2)).toMap + ) + ) + } + + private def invokeRule(rule: Rule, village: Village): (RuleResult, Duration) = { + TimedInvocation.run(() => rule.analyse(village)) + } +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/WizardTowersOutOfHoundPositionRule.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/WizardTowersOutOfHoundPositionRule.scala new file mode 100644 index 0000000..05eecd9 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/analysis/WizardTowersOutOfHoundPositionRule.scala @@ -0,0 +1,39 @@ +package org.danielholmes.coc.baseanalyser.analysis + +import org.danielholmes.coc.baseanalyser.model.troops.{LavaHound, Minion, MinionAttackPosition, WizardTowerHoundTargeting} +import org.danielholmes.coc.baseanalyser.model._ +import org.danielholmes.coc.baseanalyser.model.defense.{AirDefense, WizardTower} + +class WizardTowersOutOfHoundPositionsRule extends Rule { + def analyse(village: Village): RuleResult = { + val wts = village.elements + .filter(_.isInstanceOf[WizardTower]) + .map(_.asInstanceOf[WizardTower]) + + val wtInRange = wts.map(wt => (wt, village.airDefenses.filter(ad => wt.range.touches(LavaHound.getRestingArea(ad))))) + .filter(_._2.nonEmpty) + .flatMap(pair => pair._2.map(ad => WizardTowerHoundTargeting(pair._1, ad, LavaHound.getRestingArea(ad)))) + + val outOfRange = wts.filterNot(wt => wtInRange.exists(_.tower == wt)) + + WizardTowersOutOfHoundPositionsRuleResult(outOfRange, wtInRange) + } +} + +case class WizardTowersOutOfHoundPositionsRuleResult( + outOfRange: Set[WizardTower], + inRange: Set[WizardTowerHoundTargeting] +) extends RuleResult { + val success = inRange.map(_.tower).size <= outOfRange.size + val ruleDetails = WizardTowersOutOfHoundPositionsRule.Details +} + +object WizardTowersOutOfHoundPositionsRule { + val Details = RuleDetails( + "WizardTowersOutOfHoundPositions", + "WTs avoid hounds", + "Enough Wizard Towers out of hound range", + """Wizard Towers are strong against loons, they shouldn't be too close to air defenses where hounds can tank for them. + |You should have at least 2 that wont target resting hounds""".stripMargin + ) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/baseparser/ElementFactory.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/baseparser/ElementFactory.scala new file mode 100644 index 0000000..2aa9e22 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/baseparser/ElementFactory.scala @@ -0,0 +1,7 @@ +package org.danielholmes.coc.baseanalyser.baseparser + +import org.danielholmes.coc.baseanalyser.model.Element + +abstract class ElementFactory { + def build(raw: RawElement): Option[Element] +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/baseparser/HardCodedElementFactory.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/baseparser/HardCodedElementFactory.scala new file mode 100644 index 0000000..3786ed1 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/baseparser/HardCodedElementFactory.scala @@ -0,0 +1,158 @@ +package org.danielholmes.coc.baseanalyser.baseparser + +import org.danielholmes.coc.baseanalyser.model._ +import org.danielholmes.coc.baseanalyser.model.defense._ +import org.danielholmes.coc.baseanalyser.model.heroes.{ArcherQueenAltar, BarbarianKingAltar, GrandWarden} +import org.danielholmes.coc.baseanalyser.model.special.{ClanCastle, TownHall} +import org.danielholmes.coc.baseanalyser.model.traps._ +import org.danielholmes.coc.baseanalyser.model.trash._ +import org.scalactic.anyvals.{PosInt, PosZInt} + +class HardCodedElementFactory extends ElementFactory { + private def levelAndCoordinateConstructor(constructor: (PosInt, Tile) => Element): (RawElement => Element) = { + raw => constructor(elementLevel(raw.lvl), elementTile(raw)) + } + + private def noLevelConstructor(constructor: (Tile) => Element): (RawElement => Element) = { + raw => constructor(elementTile(raw)) + } + + private def elementLevel(rawLevel: Int): PosInt = PosInt.from(Math.max(1, rawLevel + 1)).get + + private def elementTile(raw: RawElement) = Tile(PosZInt.from(raw.x).get, PosZInt.from(raw.y).get) + + private val decorationConstructor = (element: RawElement) => Decoration(elementTile(element)) + + private val elementConstructorByCode: Map[Int, RawElement => Element] = Map( + // Buildings + 1000001 -> levelAndCoordinateConstructor(TownHall), + 1000000 -> levelAndCoordinateConstructor(ArmyCamp), + 1000002 -> levelAndCoordinateConstructor(ElixirCollector), + 1000003 -> levelAndCoordinateConstructor(ElixirStorage), + 1000004 -> levelAndCoordinateConstructor(GoldMine), + 1000005 -> levelAndCoordinateConstructor(GoldStorage), + 1000006 -> levelAndCoordinateConstructor(Barrack), + 1000007 -> levelAndCoordinateConstructor(Laboratory), + 1000008 -> levelAndCoordinateConstructor(Cannon), + 1000009 -> levelAndCoordinateConstructor(ArcherTower), + 1000010 -> levelAndCoordinateConstructor(Wall), + 1000011 -> levelAndCoordinateConstructor(WizardTower), + 1000012 -> levelAndCoordinateConstructor(AirDefense), + 1000013 -> levelAndCoordinateConstructor(Mortar), + 1000014 -> levelAndCoordinateConstructor(ClanCastle), + 1000015 -> noLevelConstructor(BuilderHut), + //1000016 CommunicationsMast, + //1000017 -> GoblinTownHull, + //1000018 -> GoblinHut, + 1000019 -> levelAndCoordinateConstructor(HiddenTesla), + 1000020 -> levelAndCoordinateConstructor(SpellFactory), + 1000021 -> ((raw: RawElement) => XBow.both(elementLevel(raw.lvl), elementTile(raw))), + 1000022 -> levelAndCoordinateConstructor(BarbarianKingAltar), + 1000023 -> levelAndCoordinateConstructor(DarkElixirCollector), + 1000024 -> levelAndCoordinateConstructor(DarkElixirStorage), + 1000025 -> levelAndCoordinateConstructor(ArcherQueenAltar), + 1000026 -> levelAndCoordinateConstructor(DarkBarrack), + 1000027 -> levelAndCoordinateConstructor(InfernoTower), + 1000028 -> ((raw: RawElement) => AirSweeper(elementLevel(raw.lvl), elementTile(raw), Angle.degrees(raw.aimAngle.get))), + 1000029 -> levelAndCoordinateConstructor(DarkSpellFactory), + 1000030 -> levelAndCoordinateConstructor(GrandWarden), + 1000031 -> levelAndCoordinateConstructor(EagleArtillery), + + // Traps + 12000000 -> levelAndCoordinateConstructor(Bomb), + 12000001 -> noLevelConstructor(SpringTrap), + 12000002 -> levelAndCoordinateConstructor(GiantBomb), + 12000003 -> levelAndCoordinateConstructor(HalloweenBomb), + //12000004 -> levelAndCoordinateConstructor(????), + 12000005 -> levelAndCoordinateConstructor(AirBomb), + 12000006 -> levelAndCoordinateConstructor(SeekingAirMine), + 12000007 -> levelAndCoordinateConstructor(SantaTrap), + 12000008 -> levelAndCoordinateConstructor(SkeletonTrap), + + // Decorations/Obstacles + 18000000 -> decorationConstructor, // Barbarian Statue + 18000001 -> decorationConstructor, // Torch + 18000002 -> decorationConstructor, // Goblin Pole + 18000003 -> decorationConstructor, // White Flag + 18000004 -> decorationConstructor, // Skull Flag + 18000005 -> decorationConstructor, // Flower box 1 + 18000006 -> decorationConstructor, // Flower box 2 + 18000007 -> decorationConstructor, // Windmeter + 18000008 -> decorationConstructor, // Down Arrow Flag + 18000009 -> decorationConstructor, // Up Arrow Flag + 18000010 -> decorationConstructor, // Skull Altar + 18000011 -> decorationConstructor, // USA Flag + 18000012 -> decorationConstructor, // Canada Flag + 18000013 -> decorationConstructor, // Italia Flag + 18000014 -> decorationConstructor, // Germany Flag + 18000015 -> decorationConstructor, // Finland Flag + 18000016 -> decorationConstructor, // Spain Flag + 18000017 -> decorationConstructor, // France Flag + 18000018 -> decorationConstructor, // GBR Flag + 18000019 -> decorationConstructor, // Brazil Flag + 18000020 -> decorationConstructor, // China Flag + 18000021 -> decorationConstructor, // Norway Flag + 18000022 -> decorationConstructor, // Thailand Flag + 18000023 -> decorationConstructor, // Thailand Flag + 18000024 -> decorationConstructor, // India Flag + 18000025 -> decorationConstructor, // Australia Flag + 18000026 -> decorationConstructor, // South Korea Flag + 18000027 -> decorationConstructor, // Japan Flag + 18000028 -> decorationConstructor, // Turkey Flag + 18000029 -> decorationConstructor, // Indonesia Flag + 18000030 -> decorationConstructor, // Netherlands Flag + 18000031 -> decorationConstructor, // Philippines Flag + 18000032 -> decorationConstructor, // Singapore Flag + 18000033 -> decorationConstructor, // PEKKA Statue + 18000034 -> decorationConstructor, // Russia Flag + 18000035 -> decorationConstructor, // Russia Flag + 18000036 -> decorationConstructor, // Greece Flag + + 8000000 -> decorationConstructor, // Pine Tree + 8000001 -> decorationConstructor, // Large Stone + 8000002 -> decorationConstructor, // Small Stone 1 + 8000003 -> decorationConstructor, // Small Stone 2 + 8000004 -> decorationConstructor, // Square Bush + 8000005 -> decorationConstructor, // Square Tree + 8000006 -> decorationConstructor, // Tree Trunk 1 + 8000007 -> decorationConstructor, // Tree Trunk 2 + 8000008 -> decorationConstructor, // Mushrooms + 8000009 -> decorationConstructor, // TombStone + 8000010 -> decorationConstructor, // Fallen Tree + 8000011 -> decorationConstructor, // Small Stone 3 + 8000012 -> decorationConstructor, // Small Stone 4 + 8000013 -> decorationConstructor, // Square Tree 2 + 8000014 -> decorationConstructor, // Stone Pillar 1 + 8000015 -> decorationConstructor, // Large Stone + 8000016 -> decorationConstructor, // Sharp Stone 1 + 8000017 -> decorationConstructor, // Sharp Stone 2 + 8000018 -> decorationConstructor, // Sharp Stone 3 + 8000019 -> decorationConstructor, // Sharp Stone 4 + 8000020 -> decorationConstructor, // Sharp Stone 5 + 8000021 -> decorationConstructor, // Xmas tree + 8000022 -> decorationConstructor, // Hero TombStone + 8000023 -> decorationConstructor, // DarkTombStone + 8000024 -> decorationConstructor, // Passable Stone 1 + 8000025 -> decorationConstructor, // Passable Stone 2 + 8000026 -> decorationConstructor, // Campfire + 8000027 -> decorationConstructor, // Campfire + 8000028 -> decorationConstructor, // Xmas tree2013 + 8000029 -> decorationConstructor, // Xmas TombStone + 8000030 -> decorationConstructor, // Bonus Gembox + 8000031 -> decorationConstructor, // Halloween2014 + 8000032 -> decorationConstructor, // Xmas tree2014 + 8000033 -> decorationConstructor, // Xmas TombStone2014 + 8000034 -> decorationConstructor, // Npc Plant 1 + 8000035 -> decorationConstructor, // Npc Plant 2 + 8000036 -> decorationConstructor // Halloween2015 + ) + + def build(raw: RawElement): Option[Element] = { + Some(raw) + .map( + value => elementConstructorByCode.get(value.data) + .orElse(throw new RuntimeException("No building with code " + raw.data)) + .get(raw) + ) + } +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/baseparser/VillageJsonParser.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/baseparser/VillageJsonParser.scala new file mode 100644 index 0000000..9388e1c --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/baseparser/VillageJsonParser.scala @@ -0,0 +1,90 @@ +package org.danielholmes.coc.baseanalyser.baseparser + +import org.danielholmes.coc.baseanalyser.model.Layout +import org.danielholmes.coc.baseanalyser.model.Village +import spray.json._ + +object VillageJsonProtocol extends DefaultJsonProtocol { + implicit val buildingFormat = jsonFormat16(RawBuilding) + implicit val rawVillageFormat = jsonFormat2(RawVillage) +} + +import VillageJsonProtocol._ + +class VillageJsonParser(elementFactory: ElementFactory) { + def parse(input: String): Villages = { + try { + val rawVillage = input.parseJson.convertTo[RawVillage] + Villages( + parseVillage(rawVillage, b => Some(RawElement(b.data, b.lvl, b.x, b.y, b.aim_angle))), + rawVillage.war_layout + .map(layoutIndex => parseVillage(rawVillage, b => parseWarElement(b, layoutIndex))) + ) + } catch { + case e: JsonParser.ParsingException => throw new InvalidJsonException(e) + case e: DeserializationException => throw new InvalidJsonException(e) + } + } + + private def parseVillage(raw: RawVillage, factory: (RawBuilding) => Option[RawElement]): Village = { + Village( + raw.buildings + .map(factory) + .filter(_.isDefined) + .map(_.get) + .map(elementFactory.build) + .filter(_.nonEmpty) + .map(_.get) + ) + } + + private def parseWarElement(raw: RawBuilding, index: Int): Option[RawElement] = { + parseWarCoordinates(raw, index) + .map(coords => RawElement(raw.data, raw.lvl, coords._1, coords._2, raw.aim_angle_war)) + } + + private def parseWarCoordinates(raw: RawBuilding, index: Int): Option[(Int, Int)] = { + index match { + case 1 => raw.l1x.map(x => (x, raw.l1y.get)) + case 2 => raw.l2x.map(x => (x, raw.l2y.get)) + case 3 => raw.l3x.map(x => (x, raw.l3y.get)) + case 4 => raw.l4x.map(x => (x, raw.l4y.get)) + case 5 => raw.l5x.map(x => (x, raw.l5y.get)) + case _ => throw new RuntimeException(s"Unknown war layout $index") + } + } +} + +case class RawVillage(buildings: Set[RawBuilding], war_layout: Option[Int]) +case class RawBuilding( + data: Int, + lvl: Int, + x: Int, y: Int, + l1x: Option[Int], l1y: Option[Int], + l2x: Option[Int], l2y: Option[Int], + l3x: Option[Int], l3y: Option[Int], + l4x: Option[Int], l4y: Option[Int], + l5x: Option[Int], l5y: Option[Int], + aim_angle: Option[Int], + aim_angle_war: Option[Int] +) +case class RawElement(data: Int, lvl: Int, x: Int, y: Int, aimAngle: Option[Int]) + +object RawElement { + def apply(data: Int, lvl: Int, x: Int, y: Int): RawElement = { + RawElement(data, lvl, x, y, None) + } +} + +case class Villages(home: Village, war: Option[Village]) { + import Layout._ + + def getByLayout(layout: Layout): Option[Village] = { + layout match { + case War => war + case Home => Some(home) + } + } +} + +class InvalidJsonException(cause : Throwable) extends Exception(cause) diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/gameconnection/ClanSeekerGameConnection.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/gameconnection/ClanSeekerGameConnection.scala new file mode 100644 index 0000000..b5cfe79 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/gameconnection/ClanSeekerGameConnection.scala @@ -0,0 +1,110 @@ +package org.danielholmes.coc.baseanalyser.gameconnection + +import akka.actor.ActorSystem +import org.danielholmes.coc.baseanalyser.util.GameConnectionNotAvailableException +import org.scalactic.anyvals.PosZInt +import spray.can.Http.ConnectionAttemptFailedException +import spray.httpx.SprayJsonSupport._ +import spray.json.DefaultJsonProtocol +import spray.http._ +import spray.client.pipelining._ +import spray.httpx.unmarshalling.FromResponseUnmarshaller + +import scala.annotation.tailrec +import scala.concurrent.duration._ +import scala.concurrent.{Await, Future} +import scala.reflect.{ClassTag, classTag} + +object ClanSeekerProtocol extends DefaultJsonProtocol { + case class AvatarSummary(userName: String, currentHomeId: Long, clanId: Long) + case class PlayerSummary(avatar: AvatarSummary) + case class ClanDetails(name: String, players: Set[PlayerSummary]) + case class ClanDetailsResponse(clan: Option[ClanDetails]) + + implicit val AvatarSummaryFormat = jsonFormat3(AvatarSummary) + implicit val PlayerSummaryFormat = jsonFormat1(PlayerSummary) + implicit val ClanDetailsFormat = jsonFormat2(ClanDetails) + implicit val ClanDetailsResponseFormat = jsonFormat1(ClanDetailsResponse) + + case class RawVillage(raw: String) + case class PlayerVillage(avatar: AvatarSummary, village: RawVillage) + case class PlayerVillageResponse(player: Option[PlayerVillage]) + + implicit val VillageFormat = jsonFormat1(RawVillage) + implicit val PlayerVillageFormat = jsonFormat2(PlayerVillage) + implicit val PlayerVillageResponseFormat = jsonFormat1(PlayerVillageResponse) +} +import ClanSeekerProtocol._ + +// TODO: Having problems with types when refactor into common functionality, but should refactor this +class ClanSeekerGameConnection extends GameConnection { + private val maxAttempts = 3 + private val rootUrl = "http://api.clanseeker.co" + private val timeout = 20.seconds + + def getClanDetails(id: Long): Option[ClanDetails] = { + implicit val system = ActorSystem() + import system.dispatcher // execution context for futures + + def tryAgain(attemptNumber: Int): Option[ClanDetails] = { + Thread.sleep(400 * attemptNumber) + attempt(attemptNumber + 1) + } + + //@tailrec + def attempt(attemptNumber: Int): Option[ClanDetails] = { + val pipeline = sendReceive ~> unmarshal[ClanDetailsResponse] + try { + val response = pipeline(Get(s"$rootUrl/clan_details?id=$id")) + val result = Await.result(response, timeout) + if (result.clan.isDefined || attemptNumber == maxAttempts) { + result.clan + } else { + tryAgain(attemptNumber) + } + } catch { + case e: ConnectionAttemptFailedException => + throw new GameConnectionNotAvailableException() + } + } + + try { + attempt(0) + } finally { + system.terminate() + } + } + + def getPlayerVillage(id: Long): Option[PlayerVillage] = { + implicit val system = ActorSystem() + import system.dispatcher // execution context for futures + + def tryAgain(attemptNumber: Int): Option[PlayerVillage] = { + Thread.sleep(400 * attemptNumber) + attempt(attemptNumber + 1) + } + + //@tailrec + def attempt(attemptNumber: Int): Option[PlayerVillage] = { + val pipeline = sendReceive ~> unmarshal[PlayerVillageResponse] + try { + val response = pipeline(Get(s"$rootUrl/player_village?id=$id")) + val result = Await.result(response, timeout) + if (result.player.isDefined || attemptNumber == maxAttempts) { + result.player + } else { + tryAgain(attemptNumber) + } + } catch { + case e: ConnectionAttemptFailedException => + throw new GameConnectionNotAvailableException() + } + } + + try { + attempt(0) + } finally { + system.terminate() + } + } +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/gameconnection/GameConnection.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/gameconnection/GameConnection.scala new file mode 100644 index 0000000..c4ca530 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/gameconnection/GameConnection.scala @@ -0,0 +1,9 @@ +package org.danielholmes.coc.baseanalyser.gameconnection + +import org.danielholmes.coc.baseanalyser.gameconnection.ClanSeekerProtocol.{ClanDetails, ClanDetailsResponse, PlayerVillage, PlayerVillageResponse} + +trait GameConnection { + def getClanDetails(id: Long): Option[ClanDetails] + + def getPlayerVillage(id: Long): Option[PlayerVillage] +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/gameconnection/StubGameConnection.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/gameconnection/StubGameConnection.scala new file mode 100644 index 0000000..3fbef47 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/gameconnection/StubGameConnection.scala @@ -0,0 +1,80 @@ +package org.danielholmes.coc.baseanalyser.gameconnection + +import org.danielholmes.coc.baseanalyser.gameconnection.ClanSeekerProtocol._ +import org.danielholmes.coc.baseanalyser.web.PermittedClan + +class StubGameConnection(private val permittedClans: Set[PermittedClan]) extends GameConnection { + def getClanDetails(id: Long): Option[ClanDetails] = { + permittedClans.find(_.id == id) + .filter(_.code != "uncool") + .map(clan => + ClanDetails( + clan.name, + Set( + PlayerSummary(AvatarSummary("Dakota", id + 1L, id)), + PlayerSummary(AvatarSummary("kottonmouth", id + 2L, id)), + PlayerSummary(AvatarSummary("Valaar", id + 3L, id)), + PlayerSummary(AvatarSummary("Mesoscalevortex", id + 4L, id)), + PlayerSummary(AvatarSummary("Kajla", id + 5L, id)), + PlayerSummary(AvatarSummary("a Noob", id + 6L, id)), + PlayerSummary(AvatarSummary("Ricochet", id + 7L, id)), + PlayerSummary(AvatarSummary("Lazy Ninja", id + 8L, id)), + PlayerSummary(AvatarSummary("san", id + 9L, id)), + PlayerSummary(AvatarSummary("Robbie", id + 10L, id)), + PlayerSummary(AvatarSummary("Kendrall", id + 11L, id)), + PlayerSummary(AvatarSummary("SpikeDragon", id + 12L, id)), + PlayerSummary(AvatarSummary("Jamie", id + 13L, id)), + PlayerSummary(AvatarSummary("joshua", id + 14L, id)), + PlayerSummary(AvatarSummary("Kiara Kong", id + 15L, id)), + PlayerSummary(AvatarSummary("ice ice baby", id + 16L, id)), + PlayerSummary(AvatarSummary("sp@nd@n14", id + 17L, id)), + PlayerSummary(AvatarSummary("Diaz", id + 18L, id)), + + PlayerSummary(AvatarSummary("I AM SPARTA!!1!", id + 100L, id)), + PlayerSummary(AvatarSummary("rektscrub", id + 102L, id)), + PlayerSummary(AvatarSummary("Darth Noobus", id + 103L, id)), + PlayerSummary(AvatarSummary("greg", id + 104L, id)), + PlayerSummary(AvatarSummary("Max", id + 105L, id)), + PlayerSummary(AvatarSummary("ppete", id + 106L, id)), + PlayerSummary(AvatarSummary("Vicious", id + 107L, id)), + PlayerSummary(AvatarSummary("Riggs", id + 108L, id)), + + PlayerSummary(AvatarSummary("Some Mini", id + 1000L, id)) + ) + ) + ) + } + + private def villageJson(name: String): String = { + io.Source.fromInputStream(getClass.getResourceAsStream("/examples/" + name)).mkString + } + + def getPlayerVillage(id: Long): Option[PlayerVillage] = { + permittedClans.map(_.id) + .map(getClanDetails(_)) + .filter(_.isDefined) + .map(_.get) + .flatMap(_.players) + .find(_.avatar.currentHomeId == id) + .map(_.avatar) + .map( + avatarSummary => + if (id < avatarSummary.clanId + 100L) { + PlayerVillage( + avatarSummary, + RawVillage(villageJson("th8-sample-1.json")) + ) + } else if (id < avatarSummary.clanId + 1000L) { + PlayerVillage( + avatarSummary, + RawVillage(villageJson("th9-sample-1.json")) + ) + } else { + PlayerVillage( + avatarSummary, + RawVillage(villageJson("th5-sample-1.json")) + ) + } + ) + } +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/Angle.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/Angle.scala new file mode 100644 index 0000000..4f60c12 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/Angle.scala @@ -0,0 +1,78 @@ +package org.danielholmes.coc.baseanalyser.model + +import scala.annotation.tailrec +import scala.math.floor + +object Angle { + + val Pi = math.Pi + val TwoPi = 2 * Pi + val PiOver2 = Pi / 2 + + val Zero = Angle(0) + val Quarter = Angle(Pi/2) + val Half = Angle(Pi) + val ThreeQuarters = Angle(3*Pi/2) + val Full = Angle(TwoPi) + + implicit class DoubleOps(d: Double) { + def degrees: Angle = Angle.degrees(d) + def radians: Angle = Angle(d) + } + + def degrees(degs: Double): Angle = Angle(degs * Pi / 180.0) + + def normalize(radians: Double): Double = { + val fullCycles = (radians / TwoPi).asInstanceOf[Int] + val possiblyNegative = radians - TwoPi * fullCycles + + if (possiblyNegative < 0) { possiblyNegative + TwoPi } + else { possiblyNegative } + } + + def atan2(y: Double, x: Double): Angle = { + Angle(Math.atan2(y, x)) + } + + def apply(radians: Double): Angle = new Angle(normalize(radians)) +} + +class Angle private (val radians: Double) extends AnyVal with Ordered[Angle] { + import Angle.{Pi, Zero, Half} + + def sin: Double = math.sin(radians) + def cos: Double = math.cos(radians) + def tan: Double = math.tan(radians) + def opposite: Angle = Angle(radians + Pi) + def degrees: Double = radians * 180.0 / Pi + def unary_-(): Angle = Angle(-radians) + def +(other: Angle): Angle = Angle(radians + other.radians) + def -(other: Angle): Angle = Angle(radians - other.radians) + def *(factor: Double): Angle = Angle(radians * factor) + def /(factor: Double): Angle = Angle(radians / factor) + def compare(a: Angle): Int = { + if (this == a) { 0 } + else if (this.radians < a.radians) { -1 } + else { +1 } + } + + private def shiftSin(x: Double) = math.sin(x - radians - Pi) + + def isLeftOf(a: Angle): Boolean = + (a == opposite) || (a != this && shiftSin(a.radians) < 0) + + def isRightOf(a: Angle): Boolean = + (a == opposite) || (a != this && shiftSin(a.radians) > 0) + + def distanceTo(a: Angle): Angle = { + val diff = a - this + if (diff < Angle.Half) diff else -diff + } + + def addUpTo(add: Angle, upTo: Angle): Angle = { + val added = this + add + if (isLeftOf(upTo) != added.isLeftOf(upTo)) upTo else added + } + + override def toString: String = s"Angle($radians)" +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/Block.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/Block.scala new file mode 100644 index 0000000..7a01469 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/Block.scala @@ -0,0 +1,123 @@ +package org.danielholmes.coc.baseanalyser.model + +import org.scalactic.anyvals.{PosInt, PosZDouble, PosZInt} + +import scala.annotation.tailrec + +case class Block(tile: Tile, size: PosInt) { + require(tile.x + size <= TileCoordinate.MaxCoordinate, s"x coord ${tile.x} + $size must be within coordinate system") + require(tile.y + size <= TileCoordinate.MaxCoordinate, s"y coord ${tile.y} + $size must be within coordinate system") + + val x = tile.x + val y = tile.y + private lazy val oppositeTile = tile.offset(size, size) + lazy val oppositeX = oppositeTile.x + lazy val oppositeY = oppositeTile.y + + lazy val isWithinMap = tiles.forall(_.isWithinMap) + + lazy val centre = FloatMapCoordinate(PosZDouble.from(x + size.toDouble / 2.0).get, PosZDouble.from(y + size.toDouble / 2.0).get) + + lazy val internalCoordinates = { + if (size < 2) { + Set.empty + } else { + tile.offset(1, 1) + .matrixOfCoordinatesTo(oppositeTile.offset(-1, -1)) + } + } + + lazy val topLeft: TileCoordinate = tile + + lazy val topRight = topLeft.offset(size, 0) + + lazy val bottomLeft = topLeft.offset(0, size) + + lazy val bottomRight = bottomLeft.offset(size, 0) + + + lazy val leftSide = topLeft.matrixOfCoordinatesTo(bottomLeft) + + lazy val rightSide = topRight.matrixOfCoordinatesTo(bottomRight) + + lazy val topSide = topLeft.matrixOfCoordinatesTo(topRight) + + lazy val bottomSide = bottomLeft.matrixOfCoordinatesTo(bottomRight) + + + lazy val allCoordinates: Set[TileCoordinate] = tile.matrixOfCoordinatesTo(oppositeTile) + + lazy val tiles = tile.matrixOfTilesInDirection(size, size) + + lazy val border = leftSide ++ rightSide ++ bottomSide ++ topSide + + def touchesEdge(coord: TileCoordinate): Boolean = border.contains(coord) + + def findClosestCoordinate(from: FloatMapCoordinate): TileCoordinate = { + possibleIntersectionPoints.min(Ordering.by((_: TileCoordinate) + .distanceTo(from))) + } + + def distanceTo(from: TileCoordinate): PosZDouble = { + findClosestCoordinate(from).distanceTo(from) + } + + def intersects(other: Block): Boolean = { + x < other.oppositeX && oppositeX > other.x && + y < other.oppositeY && oppositeY > other.y + } + + def expandBy(offset: PosInt): Block = expandToSize(PosInt.from(size + (offset * 2)).get) + + def contractBy(offset: PosInt): Block = contractToSize(PosInt.from(size - (offset * 2)).get) + + def expandToSize(newSize: PosInt): Block = { + if (newSize < size) throw new IllegalArgumentException("newSize must be greater than size") + setSize(newSize) + } + + private def contractToSize(newSize: PosInt): Block = { + if (newSize > size) throw new IllegalArgumentException("newSize must be greater than size") + setSize(newSize) + } + + private def setSize(newSize: PosInt): Block = { + if ((size - newSize) % 2 != 0) throw new IllegalArgumentException("Must increase by factors of 2") + if (newSize == size) { + this + } else { + val sizeDiff = newSize - size + Block( + Tile(PosZInt.from(tile.x - sizeDiff / 2).get, PosZInt.from(tile.y - sizeDiff / 2).get), + newSize + ) + } + } + + private lazy val possibleIntersectionPoints: Set[TileCoordinate] = { + tile.matrixOfCoordinatesTo(tile.offset(size, size)) + } +} + +object Block { + val Map = Block(Tile.Origin, TileCoordinate.MaxCoordinate) + + def firstIntersecting(blocks: Set[Block]): Option[(Block, Block)] = { + blocks.map(b => firstIntersecting(b, blocks).map((_, b))) + .headOption + .getOrElse(None) + } + + @tailrec + private def firstIntersecting(block: Block, blocks: Set[Block]): Option[Block] = { + blocks.toList match { + case Nil => None + case head :: tail => + if (block != head && block.intersects(head)) { + Some(head) + } else { + firstIntersecting(block, blocks.tail) + } + } + } +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/Building.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/Building.scala new file mode 100644 index 0000000..275dcad --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/Building.scala @@ -0,0 +1,3 @@ +package org.danielholmes.coc.baseanalyser.model + +trait Building extends Structure diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/Decoration.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/Decoration.scala new file mode 100644 index 0000000..6e51f3e --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/Decoration.scala @@ -0,0 +1,8 @@ +package org.danielholmes.coc.baseanalyser.model + +import org.scalactic.anyvals.PosInt + +case class Decoration(tile: Tile) extends Element { + lazy val level = PosInt(1) + lazy val size = PosInt(2) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/Defense.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/Defense.scala new file mode 100644 index 0000000..da9e5fc --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/Defense.scala @@ -0,0 +1,9 @@ +package org.danielholmes.coc.baseanalyser.model + +import Target._ +import org.danielholmes.coc.baseanalyser.model.range.ElementRange + +trait Defense extends Building { + val range: ElementRange + val targets: Set[Target] +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/DelayedActivation.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/DelayedActivation.scala new file mode 100644 index 0000000..2fb7482 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/DelayedActivation.scala @@ -0,0 +1,7 @@ +package org.danielholmes.coc.baseanalyser.model + +import org.scalactic.anyvals.PosInt + +trait DelayedActivation extends Defense { + val deploymentSpaceRequired: PosInt +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/Element.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/Element.scala new file mode 100644 index 0000000..cb5badb --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/Element.scala @@ -0,0 +1,11 @@ +package org.danielholmes.coc.baseanalyser.model + +import org.scalactic.anyvals.PosInt + +trait Element { + val level: PosInt + + protected val tile: Tile + val size: PosInt + lazy val block = Block(tile, size) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/FloatMapCoordinate.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/FloatMapCoordinate.scala new file mode 100644 index 0000000..2637c11 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/FloatMapCoordinate.scala @@ -0,0 +1,17 @@ +package org.danielholmes.coc.baseanalyser.model + +import org.scalactic.anyvals.PosZDouble +import org.apache.commons.math3.geometry.euclidean.twod.Vector2D + +case class FloatMapCoordinate(x: PosZDouble, y: PosZDouble) { + require(x >= 0.0 && x <= TileCoordinate.MaxCoordinate, s"MapCoordinates.x must be >= 0 <= ${TileCoordinate.MaxCoordinate}, given: $x") + require(y >= 0.0 && y <= TileCoordinate.MaxCoordinate, s"MapCoordinates.y must be >= 0 <= ${TileCoordinate.MaxCoordinate}, given: $y") + + def distanceTo(other: FloatMapCoordinate): PosZDouble = { + PosZDouble.from(Math.hypot(x - other.x, y - other.y)).get + } +} + +object FloatMapCoordinate { + implicit def widenToVector2D(coord: FloatMapCoordinate): Vector2D = new Vector2D(coord.x, coord.y) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/Hidden.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/Hidden.scala new file mode 100644 index 0000000..0562c19 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/Hidden.scala @@ -0,0 +1,3 @@ +package org.danielholmes.coc.baseanalyser.model + +trait Hidden extends Element diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/Layout.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/Layout.scala new file mode 100644 index 0000000..86c5f70 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/Layout.scala @@ -0,0 +1,15 @@ +package org.danielholmes.coc.baseanalyser.model + +object Layout extends Enumeration { + type Layout = Value + + val Home = Value(1, "home") + val War = Value(2, "war") + + def getDescription(layout: Layout): String = { + layout match { + case War => "Active War Base" + case Home => "Home Base" + } + } +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/PossibleLargeTrap.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/PossibleLargeTrap.scala new file mode 100644 index 0000000..32dc12d --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/PossibleLargeTrap.scala @@ -0,0 +1,7 @@ +package org.danielholmes.coc.baseanalyser.model + +case class PossibleLargeTrap(tile: Tile) + +object PossibleLargeTrap { + implicit def widenToBlock(trap: PossibleLargeTrap): Block = Block(trap.tile, 2) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/PreventsTroopDrop.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/PreventsTroopDrop.scala new file mode 100644 index 0000000..eeb2e44 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/PreventsTroopDrop.scala @@ -0,0 +1,8 @@ +package org.danielholmes.coc.baseanalyser.model + +import org.scalactic.anyvals.PosInt + +trait PreventsTroopDrop extends Structure { + lazy val preventTroopDropSize = PosInt.from(size + 2).get + lazy val preventTroopDropBlock = block.expandToSize(preventTroopDropSize) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/StationaryDefensiveBuilding.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/StationaryDefensiveBuilding.scala new file mode 100644 index 0000000..9ce22a4 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/StationaryDefensiveBuilding.scala @@ -0,0 +1,3 @@ +package org.danielholmes.coc.baseanalyser.model + +trait StationaryDefensiveBuilding extends Defense diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/Structure.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/Structure.scala new file mode 100644 index 0000000..80d8293 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/Structure.scala @@ -0,0 +1,19 @@ +package org.danielholmes.coc.baseanalyser.model + +import org.scalactic.anyvals.PosInt + +trait Structure extends Element { + lazy val hitSize = size + lazy val hitBlock = { + if (hitSize == size) { + block + } else { + val posOffset = PosInt.from((size - hitSize) / 2).get + Block(tile.offset(posOffset, posOffset), hitSize) + } + } + + def findClosestHitCoordinate(from: FloatMapCoordinate): TileCoordinate = { + hitBlock.findClosestCoordinate(from) + } +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/Target.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/Target.scala new file mode 100644 index 0000000..479e22a --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/Target.scala @@ -0,0 +1,10 @@ +package org.danielholmes.coc.baseanalyser.model + +object Target extends Enumeration { + type Target = Value + val Ground, Air = Value + + val Both = Target.values + val GroundOnly = Set(Ground) + val AirOnly = Set(Air) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/Tile.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/Tile.scala new file mode 100644 index 0000000..3f47870 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/Tile.scala @@ -0,0 +1,117 @@ +package org.danielholmes.coc.baseanalyser.model + +import org.danielholmes.coc.baseanalyser.util.Memo2 + +import org.scalactic.anyvals.{PosInt, PosZInt, PosZDouble} + +// The various ceremony is for instance pooling. See +// http://stackoverflow.com/questions/20030826/scala-case-class-private-constructor-but-public-apply-method +trait Tile { + val x: PosZInt + val y: PosZInt + val isWithinMap: Boolean + val neighbours: Set[Tile] + val centre: FloatMapCoordinate + def distanceTo(other: Tile): PosZDouble + def centreDistanceTo(other: Tile): PosZDouble + def manhattanDistanceTo(other: Tile): PosZInt + def matrixOfTilesTo(other: Tile): Set[Tile] + def matrixOfTilesTo(other: Tile, step: PosInt): Set[Tile] + def rectangleTo(other: Tile): Set[Tile] + // Should prob allow 0 too, but not negative obviously + def matrixOfTilesInDirection(width: PosInt, height: PosInt): Set[Tile] + val allCoordinates: Set[TileCoordinate] + def offset(xDiff: Int, yDiff: Int): Tile +} + +object Tile { + private val applyMemo = Memo2[PosZInt, PosZInt, Tile](TileImpl) + + def apply(x: PosZInt, y: PosZInt): Tile = applyMemo.apply(x, y) + + val MapSize = PosInt(44) + val OutsideBorder = PosInt(3) + + val MaxCoordinate = PosInt.from(MapSize + (OutsideBorder * 2) - 1).get + + val Origin = Tile(0, 0) + val End = Tile(MaxCoordinate, MaxCoordinate) + val All = Origin.matrixOfTilesTo(End) + + val MapOrigin = Origin.offset(OutsideBorder, OutsideBorder) + val MapEnd = MapOrigin.offset(MapSize - 1, MapSize - 1) + val AllInMap = MapOrigin.matrixOfTilesTo(End) + val AllOutsideMap = All -- AllInMap + val InnerBorder = MapOrigin.offset(-1, -1).rectangleTo(MapEnd.offset(1, 1)) + val AllNotTouchingMap = AllOutsideMap -- InnerBorder + + implicit def widenToTileCoordinate(tile: Tile): TileCoordinate = TileCoordinate(tile.x, tile.y) + + def fromCoordinate(tileCoordinate: TileCoordinate): Tile = { + Tile(tileCoordinate.x, tileCoordinate.y) + } + + private case class TileImpl(x: PosZInt, y: PosZInt) extends Tile { + require((0 to MaxCoordinate).contains(x.toInt), s"Tile.x ($x) must be in [0:$MaxCoordinate]") + require((0 to MaxCoordinate).contains(y.toInt), s"Tile.y ($y) must be in [0:$MaxCoordinate]") + + lazy val isWithinMap = AllInMap.contains(this) + + lazy val neighbours: Set[Tile] = { + Set(up, down, left, right, upLeft, upRight, downLeft, downRight).flatMap(_.iterator) + } + + private lazy val right = tryOffset(1, 0) + private lazy val left = tryOffset(-1, 0) + private lazy val down = tryOffset(0, 1) + private lazy val up = tryOffset(0, -1) + private lazy val upLeft = tryOffset(-1, -1) + private lazy val upRight = tryOffset(1, -1) + private lazy val downRight = tryOffset(1, 1) + private lazy val downLeft = tryOffset(-1, 1) + + private def tryOffset(xOffset: Int, yOffset: Int): Option[Tile] = { + val proposedX = x + xOffset + val proposedY = y + yOffset + if (proposedX >= 0 && proposedX <= Tile.MaxCoordinate && proposedY >= 0 && proposedY <= Tile.MaxCoordinate) { + Some(Tile(PosZInt.from(proposedX).get, PosZInt.from(proposedY).get)) + } else { + None + } + } + + lazy val centre = FloatMapCoordinate(PosZDouble.from(x + 0.5).get, PosZDouble.from(y + 0.5).get) + + def distanceTo(other: Tile): PosZDouble = { + allCoordinates.flatMap(otherCoord => other.allCoordinates.map(_.distanceTo(otherCoord))).min + } + + def centreDistanceTo(other: Tile): PosZDouble = centre.distanceTo(other.centre) + + def manhattanDistanceTo(other: Tile): PosZInt = PosZInt.from(Math.abs(other.x - x) + Math.abs(other.y - y)).get + + // Top + bottom + left + right + def rectangleTo(other: Tile): Set[Tile] = this.matrixOfTilesTo(Tile(other.x, y), 1) ++ + Tile(x, other.y).matrixOfTilesTo(Tile(other.x, other.y), 1) ++ + this.matrixOfTilesTo(Tile(x, other.y)) ++ + Tile(other.x, y).matrixOfTilesTo(Tile(other.x, other.y)) + + def matrixOfTilesTo(other: Tile): Set[Tile] = matrixOfTilesTo(other, 1) + + def matrixOfTilesTo(other: Tile, step: PosInt): Set[Tile] = { + (x to other.x by step).flatMap(newX => (y to other.y by step).map(newY => Tile(PosZInt.from(newX).get, PosZInt.from(newY).get))).toSet + } + + def matrixOfTilesInDirection(width: PosInt, height: PosInt): Set[Tile] = { + matrixOfTilesTo(Tile(PosZInt.from(x + width - 1).get, PosZInt.from(y + height - 1).get)) + } + + lazy val allCoordinates = toTileCoordinate.matrixOfCoordinatesTo(toTileCoordinate.offset(1, 1)) + + private lazy val toTileCoordinate = widenToTileCoordinate(this) + + def offset(xDiff: Int, yDiff: Int): Tile = Tile(PosZInt.from(x + xDiff).get, PosZInt.from(y + yDiff).get) + + override lazy val toString = s"Tile(${x.toInt}, ${y.toInt})" + } +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/TileCoordinate.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/TileCoordinate.scala new file mode 100644 index 0000000..aef9a32 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/TileCoordinate.scala @@ -0,0 +1,87 @@ +package org.danielholmes.coc.baseanalyser.model + +import org.apache.commons.math3.geometry.euclidean.twod.Vector2D +import org.danielholmes.coc.baseanalyser.util.Memo2 +import org.scalactic.anyvals.{PosInt, PosZDouble, PosZInt} + +// The various ceremony is for instance pooling. See +// http://stackoverflow.com/questions/20030826/scala-case-class-private-constructor-but-public-apply-method +trait TileCoordinate { + val x: PosZInt + val y: PosZInt + def distanceTo(other: FloatMapCoordinate): PosZDouble + def offset(xAmount: Int, yAmount: Int): TileCoordinate + def offset(xAmount: Double, yAmount: Double): FloatMapCoordinate + def matrixOfCoordinatesTo(other: TileCoordinate, step: PosInt): Set[TileCoordinate] + def matrixOfCoordinatesTo(other: TileCoordinate): Set[TileCoordinate] + def xAxisCoordsTo(other: TileCoordinate, step: PosInt): Set[TileCoordinate] + def yAxisCoordsTo(other: TileCoordinate, step: PosInt): Set[TileCoordinate] + def neighbours: Set[TileCoordinate] +} + +object TileCoordinate { + private val applyMemo = Memo2[PosZInt, PosZInt, TileCoordinate](TileCoordinateImpl) + def apply(x: PosZInt, y: PosZInt): TileCoordinate = applyMemo.apply(x, y) + + // TODO: Remove this to try and reduce new mapcoordinate creation + implicit def widenToMapCoordinate(coord: TileCoordinate): FloatMapCoordinate = FloatMapCoordinate(coord.x, coord.y) + + implicit def widenToVector2D(coord: TileCoordinate): Vector2D = new Vector2D(coord.x, coord.y) + + val MaxCoordinate = PosInt.from(Tile.MaxCoordinate + 1).get + val Origin = TileCoordinate(0, 0) + val End = TileCoordinate(MaxCoordinate, MaxCoordinate) + val All = Origin.matrixOfCoordinatesTo(End) + val AllEdge = + // Top + Origin.matrixOfCoordinatesTo(TileCoordinate(MaxCoordinate, 0)) ++ + // Bottom + TileCoordinate(0, MaxCoordinate).matrixOfCoordinatesTo(TileCoordinate(MaxCoordinate, MaxCoordinate)) ++ + // Left + Origin.matrixOfCoordinatesTo(TileCoordinate(0, MaxCoordinate)) ++ + // Right + TileCoordinate(MaxCoordinate, 0).matrixOfCoordinatesTo(TileCoordinate(MaxCoordinate, MaxCoordinate)) + + val MapOrigin: TileCoordinate = Tile.MapOrigin + + private case class TileCoordinateImpl(x: PosZInt, y: PosZInt) extends TileCoordinate { + require((0 to MaxCoordinate).contains(x.toInt), s"TileCoordinates.x must be >= 0 <= $MaxCoordinate, given: $x") + require((0 to MaxCoordinate).contains(y.toInt), s"TileCoordinates.y must be >= 0 <= $MaxCoordinate, given: $y") + + def distanceTo(other: FloatMapCoordinate): PosZDouble = PosZDouble.from(Math.hypot(x - other.x, y - other.y)).get + + def offset(xAmount: Int, yAmount: Int): TileCoordinate = { + TileCoordinate(PosZInt.from(x + xAmount).get, PosZInt.from(y + yAmount).get) + } + + def offset(xAmount: Double, yAmount: Double): FloatMapCoordinate = { + FloatMapCoordinate(PosZDouble.from(x + xAmount).get, PosZDouble.from(y + yAmount).get) + } + + def xAxisCoordsTo(other: TileCoordinate, step: PosInt): Set[TileCoordinate] = { + (x to other.x by step).map((newX: Int) => TileCoordinate(PosZInt.from(newX).get, PosZInt.from(y).get)).toSet + } + + def yAxisCoordsTo(other: TileCoordinate, step: PosInt): Set[TileCoordinate] = { + (y to other.y by step).map((newY: Int) => TileCoordinate(PosZInt.from(x).get, PosZInt.from(newY).get)).toSet + } + + def matrixOfCoordinatesTo(other: TileCoordinate, step: PosInt): Set[TileCoordinate] = { + yAxisCoordsTo(other, step) + .flatMap(_.xAxisCoordsTo(other, step)) + } + + def matrixOfCoordinatesTo(other: TileCoordinate): Set[TileCoordinate] = matrixOfCoordinatesTo(other, 1) + + lazy val neighbours: Set[TileCoordinate] = { + Set(up, down, left, right).flatMap(_.iterator) + } + + private lazy val right: Option[TileCoordinate] = Some(x).filter(_ < TileCoordinate.MaxCoordinate).map(x => offset(1, 0)) + private lazy val left: Option[TileCoordinate] = Some(x).filter(_ > 0).map(x => offset(-1, 0)) + private lazy val down: Option[TileCoordinate] = Some(y).filter(_ < TileCoordinate.MaxCoordinate).map(y => offset(0, 1)) + private lazy val up: Option[TileCoordinate] = Some(y).filter(_ > 0).map(y => offset(0, -1)) + + override val toString = s"TileCoordinate($x, $y)" + } +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/Village.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/Village.scala new file mode 100644 index 0000000..bf83b25 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/Village.scala @@ -0,0 +1,117 @@ +package org.danielholmes.coc.baseanalyser.model + +import org.danielholmes.coc.baseanalyser.model.defense.AirDefense +import org.danielholmes.coc.baseanalyser.model.special.{ClanCastle, TownHall} +import org.scalactic.anyvals.PosInt + +import scala.annotation.tailrec +import scala.reflect.{ClassTag, classTag} + +case class Village(elements: Set[Element]) { + private val firstIntersect = Block.firstIntersecting(elements.map(_.block)) + require( + firstIntersect.isEmpty, + s"Elements musn't overlap (currently $firstIntersect overlaps" + ) + + // It's a real world base, it has a town hall level. Maybe this should be a requirement? Only reason doesnt exist is + // for testing which is a poor excuse to harm data model + val townHallLevel = findElementByType[TownHall].map(_.level) + + val clanCastle = findElementByType[ClanCastle] + + lazy val isEmpty = elements.isEmpty + + lazy val structures = getElementsByType[Structure] + + lazy val preventsTroopDropStructures = getElementsByType[PreventsTroopDrop] + + lazy val stationaryDefensiveBuildings = getElementsByType[StationaryDefensiveBuilding] + + lazy val defenses = getElementsByType[Defense] + + lazy val groundTargetingDefenses = defenses.filter(_.targets.contains(Target.Ground)) + + lazy val airDefenses = getElementsByType[AirDefense] + + lazy val buildings = getElementsByType[Building] + + + private lazy val tilesNotAllowedToDropTroop = preventsTroopDropStructures.flatMap(_.preventTroopDropBlock.tiles) + + lazy val tilesAllowedToDropTroop = Tile.All -- tilesNotAllowedToDropTroop + + lazy val coordinatesAllowedToDropTroop: Set[TileCoordinate] = tilesAllowedToDropTroop.flatMap(_.allCoordinates) + + // TODO: Consider when empty village, prob want all + lazy val edgeOfHitCoordinatesAllowedToDropTroop: Set[TileCoordinate] = { + coordinatesAllowedToDropTroop.intersect(preventsTroopDropStructures.map(_.preventTroopDropBlock).flatMap(_.border)) + } + + lazy val wallCompartments: Set[WallCompartment] = { + val innerTiles = Tile.All -- outerTiles -- wallsByTile.keySet + detectAllCompartments(innerTiles, Set.empty) + } + + // TODO: Need to consider channel bases + lazy val possibleInternalLargeTraps = wallCompartments.flatMap(_.possibleLargeTraps) + + lazy val wallTiles = walls.map(_.block.tile) + + private lazy val walls = getElementsByType[Wall] + + lazy val outerTiles: Set[Tile] = detectCompartment(Tile.AllOutsideMap).innerTiles + + lazy val outerTileCoordinates: Set[TileCoordinate] = outerTiles.flatMap(_.allCoordinates) + + private def getElementsByType[T: ClassTag] = + elements.filter(classTag[T].runtimeClass.isInstance(_)).map(_.asInstanceOf[T]) + + def findElementByType[T: ClassTag](): Option[T] = + elements.find(classTag[T].runtimeClass.isInstance(_)).map(_.asInstanceOf[T]) + + @tailrec + private def detectAllCompartments(innerTiles: Set[Tile], current: Set[WallCompartment]): Set[WallCompartment] = { + innerTiles.toList match { + case Nil => current + case head :: tail => + val compartment = detectCompartment(head) + detectAllCompartments(innerTiles -- compartment.innerTiles, current + compartment) + } + } + + private def detectCompartment(toProcess: Tile): WallCompartment = { + detectCompartment(List(toProcess), Set(toProcess), Set.empty, Set.empty) + } + + private def detectCompartment(toProcess: Set[Tile]): WallCompartment = { + detectCompartment(toProcess.toList, toProcess, Set.empty, Set.empty) + } + + @tailrec + private def detectCompartment(toProcess: List[Tile], seenTiles: Set[Tile], currentInnerTiles: Set[Tile], currentWalls: Set[Wall]): WallCompartment = { + toProcess match { + case Nil => WallCompartment( + currentWalls, + currentInnerTiles, + elements.filter(e => currentInnerTiles.contains(e.block.tile)) + ) + case head :: tail => + val notSeenNeighbours = head.neighbours.diff(seenTiles) + val wallNeighbours = notSeenNeighbours.flatMap(wallsByTile.get) + val nonWallNeighbours = notSeenNeighbours.diff(wallNeighbours.map(_.block.tile)) + detectCompartment( + tail ::: nonWallNeighbours.toList, + seenTiles ++ notSeenNeighbours, + currentInnerTiles + head, + currentWalls ++ wallNeighbours + ) + } + } + + private lazy val wallsByTile = walls.map(w => (w.block.tile, w)).toMap +} + +object Village { + val empty = Village(Set.empty) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/Wall.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/Wall.scala new file mode 100644 index 0000000..d8a2389 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/Wall.scala @@ -0,0 +1,7 @@ +package org.danielholmes.coc.baseanalyser.model + +import org.scalactic.anyvals.PosInt + +case class Wall(level: PosInt, tile: Tile) extends PreventsTroopDrop { + val size = PosInt(1) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/WallCompartment.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/WallCompartment.scala new file mode 100644 index 0000000..4362302 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/WallCompartment.scala @@ -0,0 +1,31 @@ +package org.danielholmes.coc.baseanalyser.model + +import scala.annotation.tailrec + +// TODO: Inner tiles should really be derivable from the walls (maybe not a good idea though if already calculated +// during construction +case class WallCompartment(walls: Set[Wall], innerTiles: Set[Tile], elements: Set[Element]) { + //require(walls.nonEmpty) Currently use wall compartment to represent outer area in algorithms. Should change this + require(innerTiles.nonEmpty) + + private lazy val visibleElements = elements.filterNot(_.isInstanceOf[Hidden]) + + lazy val emptyTiles = innerTiles -- visibleElements.map(_.block).flatMap(_.tiles) + + lazy val possibleLargeTraps = findPossibleLargeTraps(emptyTiles.toList, Set.empty) + + private lazy val allTiles = innerTiles ++ walls.map(_.block.tile) + + def contains(tile: Tile): Boolean = allTiles.contains(tile) + + @tailrec + private def findPossibleLargeTraps(tiles: List[Tile], current: Set[PossibleLargeTrap]): Set[PossibleLargeTrap] = { + tiles match { + case Nil => current + case head :: tail => findPossibleLargeTraps( + tail, + current ++ Some(PossibleLargeTrap(head)).filter(b => b.tiles.subsetOf(emptyTiles)) + ) + } + } +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/defense/AirDefense.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/defense/AirDefense.scala new file mode 100644 index 0000000..60340f4 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/defense/AirDefense.scala @@ -0,0 +1,11 @@ +package org.danielholmes.coc.baseanalyser.model.defense + +import org.danielholmes.coc.baseanalyser.model.range.CircularElementRange +import org.danielholmes.coc.baseanalyser.model.{StationaryDefensiveBuilding, PreventsTroopDrop, Target, Tile} +import org.scalactic.anyvals.PosInt + +case class AirDefense(level: PosInt, tile: Tile) extends StationaryDefensiveBuilding with PreventsTroopDrop { + lazy val range = CircularElementRange(block.centre, 10) + val targets = Target.AirOnly + val size = PosInt(3) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/defense/AirSweeper.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/defense/AirSweeper.scala new file mode 100644 index 0000000..d6429b0 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/defense/AirSweeper.scala @@ -0,0 +1,11 @@ +package org.danielholmes.coc.baseanalyser.model.defense + +import org.danielholmes.coc.baseanalyser.model.range.BlindSpotSectorElementRange +import org.danielholmes.coc.baseanalyser.model._ +import org.scalactic.anyvals.PosInt + +case class AirSweeper(level: PosInt, tile: Tile, angle: AirSweeperAngle) extends StationaryDefensiveBuilding with PreventsTroopDrop { + val size = PosInt(2) + val range = BlindSpotSectorElementRange(block.centre, 1.0, 15.0, angle.toAngle, Angle.degrees(120)) + val targets: Set[Target.Target] = Target.AirOnly +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/defense/AirSweeperAngle.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/defense/AirSweeperAngle.scala new file mode 100644 index 0000000..ad2234a --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/defense/AirSweeperAngle.scala @@ -0,0 +1,29 @@ +package org.danielholmes.coc.baseanalyser.model.defense + +import org.danielholmes.coc.baseanalyser.model.Angle +import org.scalactic.anyvals.PosZInt + +trait AirSweeperAngle { + override def toString: String = s"AirSweeperAngle($toInt)" + + def toInt: Int + + def toAngle: Angle +} + +object AirSweeperAngle { + implicit def widenToPosZInt(angle: AirSweeperAngle): PosZInt = PosZInt.from(angle.toInt).get + + implicit def apply(angle: Angle): AirSweeperAngle = { + if (angle.degrees % 45 != 0) { + throw new IllegalArgumentException(s"$angle should be increments of 45") + } + + AirSweeperAngleImpl(angle) + } + + private case class AirSweeperAngleImpl(private val value: Angle) extends AirSweeperAngle { + val toInt = Math.round(value.degrees).toInt + val toAngle = value + } +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/defense/ArcherTower.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/defense/ArcherTower.scala new file mode 100644 index 0000000..0130f91 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/defense/ArcherTower.scala @@ -0,0 +1,11 @@ +package org.danielholmes.coc.baseanalyser.model.defense + +import org.danielholmes.coc.baseanalyser.model.range.CircularElementRange +import org.danielholmes.coc.baseanalyser.model.{StationaryDefensiveBuilding, PreventsTroopDrop, Target, Tile} +import org.scalactic.anyvals.PosInt + +case class ArcherTower(level: PosInt, tile: Tile) extends StationaryDefensiveBuilding with PreventsTroopDrop { + lazy val range = CircularElementRange(block.centre, 10) + val targets = Target.Both + val size = PosInt(3) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/defense/Cannon.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/defense/Cannon.scala new file mode 100644 index 0000000..2fad37e --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/defense/Cannon.scala @@ -0,0 +1,11 @@ +package org.danielholmes.coc.baseanalyser.model.defense + +import org.danielholmes.coc.baseanalyser.model.range.CircularElementRange +import org.danielholmes.coc.baseanalyser.model.{StationaryDefensiveBuilding, PreventsTroopDrop, Target, Tile} +import org.scalactic.anyvals.PosInt + +case class Cannon(level: PosInt, tile: Tile) extends StationaryDefensiveBuilding with PreventsTroopDrop { + lazy val range = CircularElementRange(block.centre, 9) + val size = PosInt(3) + val targets = Target.GroundOnly +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/defense/EagleArtillery.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/defense/EagleArtillery.scala new file mode 100644 index 0000000..8292af6 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/defense/EagleArtillery.scala @@ -0,0 +1,12 @@ +package org.danielholmes.coc.baseanalyser.model.defense + +import org.danielholmes.coc.baseanalyser.model.range.BlindSpotCircularElementRange +import org.danielholmes.coc.baseanalyser.model._ +import org.scalactic.anyvals.PosInt + +case class EagleArtillery(level: PosInt, tile: Tile) extends StationaryDefensiveBuilding with PreventsTroopDrop with DelayedActivation { + lazy val range = BlindSpotCircularElementRange(block.centre, 7, 50) + val deploymentSpaceRequired = PosInt(150) + val size = PosInt(4) + val targets = Target.Both +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/defense/HiddenTesla.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/defense/HiddenTesla.scala new file mode 100644 index 0000000..31f2e3c --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/defense/HiddenTesla.scala @@ -0,0 +1,11 @@ +package org.danielholmes.coc.baseanalyser.model.defense + +import org.danielholmes.coc.baseanalyser.model._ +import org.danielholmes.coc.baseanalyser.model.range.CircularElementRange +import org.scalactic.anyvals.PosInt + +case class HiddenTesla(level: PosInt, tile: Tile) extends StationaryDefensiveBuilding with Hidden { + lazy val range = CircularElementRange(block.centre, 7) // Note: Simplified, shows at 6, then 7 + val targets = Target.Both + val size = PosInt(2) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/defense/InfernoTower.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/defense/InfernoTower.scala new file mode 100644 index 0000000..419eafd --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/defense/InfernoTower.scala @@ -0,0 +1,11 @@ +package org.danielholmes.coc.baseanalyser.model.defense + +import org.danielholmes.coc.baseanalyser.model.range.CircularElementRange +import org.danielholmes.coc.baseanalyser.model.{StationaryDefensiveBuilding, PreventsTroopDrop, Target, Tile} +import org.scalactic.anyvals.PosInt + +case class InfernoTower(level: PosInt, tile: Tile) extends StationaryDefensiveBuilding with PreventsTroopDrop { + lazy val range = CircularElementRange(block.centre, 9) + val targets = Target.Both + val size = PosInt(2) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/defense/Mortar.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/defense/Mortar.scala new file mode 100644 index 0000000..153de33 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/defense/Mortar.scala @@ -0,0 +1,11 @@ +package org.danielholmes.coc.baseanalyser.model.defense + +import org.danielholmes.coc.baseanalyser.model.range.BlindSpotCircularElementRange +import org.danielholmes.coc.baseanalyser.model.{StationaryDefensiveBuilding, PreventsTroopDrop, Target, Tile} +import org.scalactic.anyvals.PosInt + +case class Mortar(level: PosInt, tile: Tile) extends StationaryDefensiveBuilding with PreventsTroopDrop { + lazy val range = BlindSpotCircularElementRange(block.centre, 4, 11) + val size = PosInt(3) + val targets = Target.GroundOnly +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/defense/WizardTower.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/defense/WizardTower.scala new file mode 100644 index 0000000..2e538a6 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/defense/WizardTower.scala @@ -0,0 +1,11 @@ +package org.danielholmes.coc.baseanalyser.model.defense + +import org.danielholmes.coc.baseanalyser.model.range.CircularElementRange +import org.danielholmes.coc.baseanalyser.model.{StationaryDefensiveBuilding, PreventsTroopDrop, Target, Tile} +import org.scalactic.anyvals.PosInt + +case class WizardTower(level: PosInt, tile: Tile) extends StationaryDefensiveBuilding with PreventsTroopDrop { + lazy val range = CircularElementRange(block.centre, 7) + val targets = Target.Both + val size = PosInt(3) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/defense/XBow.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/defense/XBow.scala new file mode 100644 index 0000000..12bd9f5 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/defense/XBow.scala @@ -0,0 +1,45 @@ +package org.danielholmes.coc.baseanalyser.model.defense + +import org.danielholmes.coc.baseanalyser.model.defense.XBowMode.XBowMode +import org.danielholmes.coc.baseanalyser.model.range.CircularElementRange +import org.danielholmes.coc.baseanalyser.model.Target.Target +import org.danielholmes.coc.baseanalyser.model.{StationaryDefensiveBuilding, PreventsTroopDrop, Target, Tile} +import org.scalactic.anyvals.PosInt + +case class XBow(level: PosInt, tile: Tile, private val mode: XBowMode) extends StationaryDefensiveBuilding with PreventsTroopDrop { + val targets = XBowMode.targets(mode) + val size = PosInt(3) + lazy val range = CircularElementRange(block.centre, XBowMode.radiusSize(mode)) +} + +object XBow { + def ground(level: PosInt, tile: Tile): XBow = { + XBow(level, tile, XBowMode.Ground) + } + + def both(level: PosInt, tile: Tile): XBow = { + XBow(level, tile, XBowMode.Both) + } +} + +object XBowMode extends Enumeration { + type XBowMode = Value + + val Ground, Both = Value + + def targets(mode: XBowMode): Set[Target] = { + if (mode == Ground) { + Target.GroundOnly + } else { + Target.Both + } + } + + def radiusSize(mode: XBowMode): PosInt = { + if (mode == Ground) { + 14 + } else { + 11 + } + } +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/heroes/ArcherQueenAltar.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/heroes/ArcherQueenAltar.scala new file mode 100644 index 0000000..4c854f9 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/heroes/ArcherQueenAltar.scala @@ -0,0 +1,11 @@ +package org.danielholmes.coc.baseanalyser.model.heroes + +import org.danielholmes.coc.baseanalyser.model.range.CircularElementRange +import org.danielholmes.coc.baseanalyser.model.{Target, Tile} +import org.scalactic.anyvals.PosInt + +case class ArcherQueenAltar(level: PosInt, tile: Tile) extends HeroAltar { + lazy val range = CircularElementRange(block.centre, 10) + val targets = Target.Both + val size = PosInt(3) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/heroes/BarbarianKingAltar.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/heroes/BarbarianKingAltar.scala new file mode 100644 index 0000000..e7913dd --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/heroes/BarbarianKingAltar.scala @@ -0,0 +1,11 @@ +package org.danielholmes.coc.baseanalyser.model.heroes + +import org.danielholmes.coc.baseanalyser.model.range.CircularElementRange +import org.danielholmes.coc.baseanalyser.model.{Target, Tile} +import org.scalactic.anyvals.PosInt + +case class BarbarianKingAltar(level: PosInt, tile: Tile) extends HeroAltar { + lazy val range = CircularElementRange(block.centre, 9) + val targets = Target.GroundOnly + val size = PosInt(3) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/heroes/GrandWarden.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/heroes/GrandWarden.scala new file mode 100644 index 0000000..ee16686 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/heroes/GrandWarden.scala @@ -0,0 +1,11 @@ +package org.danielholmes.coc.baseanalyser.model.heroes + +import org.danielholmes.coc.baseanalyser.model.range.CircularElementRange +import org.danielholmes.coc.baseanalyser.model.{Target, Tile} +import org.scalactic.anyvals.PosInt + +case class GrandWarden(level: PosInt, tile: Tile) extends HeroAltar { + lazy val range = CircularElementRange(block.centre, 7) + val targets = Target.Both + val size = PosInt(3) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/heroes/HeroAltar.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/heroes/HeroAltar.scala new file mode 100644 index 0000000..87dcb0c --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/heroes/HeroAltar.scala @@ -0,0 +1,5 @@ +package org.danielholmes.coc.baseanalyser.model.heroes + +import org.danielholmes.coc.baseanalyser.model.{Defense, PreventsTroopDrop} + +trait HeroAltar extends Defense with PreventsTroopDrop diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/range/BlindSpotCircularElementRange.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/range/BlindSpotCircularElementRange.scala new file mode 100644 index 0000000..2d2ad99 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/range/BlindSpotCircularElementRange.scala @@ -0,0 +1,13 @@ +package org.danielholmes.coc.baseanalyser.model.range + +import org.danielholmes.coc.baseanalyser.model.{FloatMapCoordinate, Tile, TileCoordinate} +import org.scalactic.anyvals.{PosDouble, PosZInt} + +case class BlindSpotCircularElementRange(centre: FloatMapCoordinate, innerSize: PosDouble, outerSize: PosDouble) extends ElementRange { + require(innerSize < outerSize, "inner should be less than outer") + + def contains(testCoordinate: FloatMapCoordinate): Boolean = { + val distance = testCoordinate.distanceTo(centre) + distance >= innerSize && distance < outerSize + } +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/range/BlindSpotSectorElementRange.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/range/BlindSpotSectorElementRange.scala new file mode 100644 index 0000000..e1f67a2 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/range/BlindSpotSectorElementRange.scala @@ -0,0 +1,21 @@ +package org.danielholmes.coc.baseanalyser.model.range + +import org.danielholmes.coc.baseanalyser.model.{Angle, FloatMapCoordinate, Tile, TileCoordinate} +import org.scalactic.anyvals.PosDouble + +case class BlindSpotSectorElementRange( + centre: FloatMapCoordinate, + innerSize: PosDouble, + outerSize: PosDouble, + angle: Angle, + angleSize: Angle +) extends ElementRange { + private val minAngle: Angle = angle - angleSize / 2 + private val maxAngle: Angle = angle + angleSize / 2 + + def contains(testCoordinate: FloatMapCoordinate): Boolean = { + val distance = testCoordinate.distanceTo(centre) + val testAngle = Angle.atan2(centre.y - testCoordinate.y, centre.x - testCoordinate.x) - Angle.Quarter + distance >= innerSize && distance < outerSize && minAngle.isLeftOf(testAngle) && maxAngle.isRightOf(testAngle) + } +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/range/CircularElementRange.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/range/CircularElementRange.scala new file mode 100644 index 0000000..4971661 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/range/CircularElementRange.scala @@ -0,0 +1,17 @@ +package org.danielholmes.coc.baseanalyser.model.range + +import org.apache.commons.math3.geometry.euclidean.twod.Segment +import org.danielholmes.coc.baseanalyser.model.{FloatMapCoordinate, Tile} +import org.scalactic.anyvals.PosDouble + +case class CircularElementRange(centre: FloatMapCoordinate, size: PosDouble) extends ElementRange { + def contains(testCoordinate: FloatMapCoordinate): Boolean = { + testCoordinate.distanceTo(centre) < size + } + + def inset(amount: PosDouble): CircularElementRange = { + CircularElementRange(centre, PosDouble.from(size - amount).get) + } + + def cutBy(segment: Segment): Boolean = segment.distance(centre) < size +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/range/ElementRange.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/range/ElementRange.scala new file mode 100644 index 0000000..cd8b730 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/range/ElementRange.scala @@ -0,0 +1,18 @@ +package org.danielholmes.coc.baseanalyser.model.range + +import org.danielholmes.coc.baseanalyser.model.{Block, FloatMapCoordinate, Tile} + +trait ElementRange { + def contains(testCoordinate: FloatMapCoordinate): Boolean + + def touchesEdge(tile: Tile): Boolean = { + val touchResults = tile.allCoordinates.partition(contains(_)) + touchResults._1.nonEmpty && touchResults._2.nonEmpty + } + + def touches(tile: Tile): Boolean = tile.allCoordinates.exists(contains(_)) + + def touches(block: Block): Boolean = block.allCoordinates.exists(contains(_)) + + lazy val allTouchingTiles = Tile.All.filter(touches) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/special/ClanCastle.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/special/ClanCastle.scala new file mode 100644 index 0000000..515fd7d --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/special/ClanCastle.scala @@ -0,0 +1,11 @@ +package org.danielholmes.coc.baseanalyser.model.special + +import org.danielholmes.coc.baseanalyser.model.range.CircularElementRange +import org.danielholmes.coc.baseanalyser.model.{Building, PreventsTroopDrop, Tile} +import org.scalactic.anyvals.PosInt + +case class ClanCastle(level: PosInt, tile: Tile) extends Building with PreventsTroopDrop { + val size = PosInt(3) + + lazy val range = CircularElementRange(block.centre, 12.0) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/special/TownHall.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/special/TownHall.scala new file mode 100644 index 0000000..4343690 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/special/TownHall.scala @@ -0,0 +1,8 @@ +package org.danielholmes.coc.baseanalyser.model.special + +import org.danielholmes.coc.baseanalyser.model.{Building, PreventsTroopDrop, Tile} +import org.scalactic.anyvals.PosInt + +case class TownHall(level: PosInt, tile: Tile) extends Building with PreventsTroopDrop { + val size = PosInt(4) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/traps/AirBomb.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/traps/AirBomb.scala new file mode 100644 index 0000000..1dd9877 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/traps/AirBomb.scala @@ -0,0 +1,8 @@ +package org.danielholmes.coc.baseanalyser.model.traps + +import org.danielholmes.coc.baseanalyser.model.Tile +import org.scalactic.anyvals.PosInt + +case class AirBomb(level: PosInt, tile: Tile) extends Trap { + val size = PosInt(1) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/traps/Bomb.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/traps/Bomb.scala new file mode 100644 index 0000000..4b3a248 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/traps/Bomb.scala @@ -0,0 +1,8 @@ +package org.danielholmes.coc.baseanalyser.model.traps + +import org.danielholmes.coc.baseanalyser.model.Tile +import org.scalactic.anyvals.PosInt + +case class Bomb(level: PosInt, tile: Tile) extends Trap { + val size = PosInt(1) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/traps/GiantBomb.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/traps/GiantBomb.scala new file mode 100644 index 0000000..b404e9d --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/traps/GiantBomb.scala @@ -0,0 +1,8 @@ +package org.danielholmes.coc.baseanalyser.model.traps + +import org.danielholmes.coc.baseanalyser.model.Tile +import org.scalactic.anyvals.PosInt + +case class GiantBomb(level: PosInt, tile: Tile) extends Trap { + val size = PosInt(2) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/traps/HalloweenBomb.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/traps/HalloweenBomb.scala new file mode 100644 index 0000000..69aaff9 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/traps/HalloweenBomb.scala @@ -0,0 +1,8 @@ +package org.danielholmes.coc.baseanalyser.model.traps + +import org.danielholmes.coc.baseanalyser.model.Tile +import org.scalactic.anyvals.PosInt + +case class HalloweenBomb(level: PosInt, tile: Tile) extends Trap { + val size = PosInt(1) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/traps/SantaTrap.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/traps/SantaTrap.scala new file mode 100644 index 0000000..c4ac981 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/traps/SantaTrap.scala @@ -0,0 +1,8 @@ +package org.danielholmes.coc.baseanalyser.model.traps + +import org.danielholmes.coc.baseanalyser.model.Tile +import org.scalactic.anyvals.PosInt + +case class SantaTrap(level: PosInt, tile: Tile) extends Trap { + val size = PosInt(1) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/traps/SeekingAirMine.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/traps/SeekingAirMine.scala new file mode 100644 index 0000000..d9c24e9 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/traps/SeekingAirMine.scala @@ -0,0 +1,8 @@ +package org.danielholmes.coc.baseanalyser.model.traps + +import org.danielholmes.coc.baseanalyser.model.Tile +import org.scalactic.anyvals.PosInt + +case class SeekingAirMine(level: PosInt, tile: Tile) extends Trap { + val size = PosInt(1) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/traps/SkeletonTrap.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/traps/SkeletonTrap.scala new file mode 100644 index 0000000..ca337a9 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/traps/SkeletonTrap.scala @@ -0,0 +1,8 @@ +package org.danielholmes.coc.baseanalyser.model.traps + +import org.danielholmes.coc.baseanalyser.model.Tile +import org.scalactic.anyvals.PosInt + +case class SkeletonTrap(level: PosInt, tile: Tile) extends Trap { + val size = PosInt(1) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/traps/SpringTrap.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/traps/SpringTrap.scala new file mode 100644 index 0000000..aebe6d5 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/traps/SpringTrap.scala @@ -0,0 +1,9 @@ +package org.danielholmes.coc.baseanalyser.model.traps + +import org.danielholmes.coc.baseanalyser.model.Tile +import org.scalactic.anyvals.PosInt + +case class SpringTrap(tile: Tile) extends Trap { + val size = PosInt(1) + val level = PosInt(1) // level not even relevant +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/traps/Trap.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/traps/Trap.scala new file mode 100644 index 0000000..559612e --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/traps/Trap.scala @@ -0,0 +1,6 @@ +package org.danielholmes.coc.baseanalyser.model.traps + +import org.danielholmes.coc.baseanalyser.model.{Element, Hidden} + +trait Trap extends Hidden +// TODO: Some sort of activation. For springs its a single tile, for bombs its a circular range. Just reuse range diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/ArmyCamp.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/ArmyCamp.scala new file mode 100644 index 0000000..ebfc7d9 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/ArmyCamp.scala @@ -0,0 +1,9 @@ +package org.danielholmes.coc.baseanalyser.model.trash + +import org.danielholmes.coc.baseanalyser.model.{PreventsTroopDrop, Building, Tile} +import org.scalactic.anyvals.PosInt + +case class ArmyCamp(level: PosInt, tile: Tile) extends Building with PreventsTroopDrop { + val size: PosInt = 5 + override lazy val hitSize: PosInt = 3 +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/Barrack.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/Barrack.scala new file mode 100644 index 0000000..5fa8595 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/Barrack.scala @@ -0,0 +1,8 @@ +package org.danielholmes.coc.baseanalyser.model.trash + +import org.danielholmes.coc.baseanalyser.model.{PreventsTroopDrop, Tile, Building} +import org.scalactic.anyvals.PosInt + +case class Barrack(level: PosInt, tile: Tile) extends Building with PreventsTroopDrop { + val size = PosInt(3) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/BuilderHut.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/BuilderHut.scala new file mode 100644 index 0000000..4dda6bf --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/BuilderHut.scala @@ -0,0 +1,9 @@ +package org.danielholmes.coc.baseanalyser.model.trash + +import org.danielholmes.coc.baseanalyser.model.{PreventsTroopDrop, Tile, Building} +import org.scalactic.anyvals.PosInt + +case class BuilderHut(tile: Tile) extends Building with PreventsTroopDrop { + val size = PosInt(2) + val level = PosInt(1) // level not even relevant for builderhut +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/DarkBarrack.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/DarkBarrack.scala new file mode 100644 index 0000000..2806cdd --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/DarkBarrack.scala @@ -0,0 +1,8 @@ +package org.danielholmes.coc.baseanalyser.model.trash + +import org.danielholmes.coc.baseanalyser.model.{PreventsTroopDrop, Tile, Building} +import org.scalactic.anyvals.PosInt + +case class DarkBarrack(level: PosInt, tile: Tile) extends Building with PreventsTroopDrop { + val size = PosInt(3) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/DarkElixirCollector.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/DarkElixirCollector.scala new file mode 100644 index 0000000..76d5b4b --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/DarkElixirCollector.scala @@ -0,0 +1,8 @@ +package org.danielholmes.coc.baseanalyser.model.trash + +import org.danielholmes.coc.baseanalyser.model.{PreventsTroopDrop, Tile, Building} +import org.scalactic.anyvals.PosInt + +case class DarkElixirCollector(level: PosInt, tile: Tile) extends Building with PreventsTroopDrop { + val size = PosInt(3) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/DarkElixirStorage.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/DarkElixirStorage.scala new file mode 100644 index 0000000..079a65a --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/DarkElixirStorage.scala @@ -0,0 +1,8 @@ +package org.danielholmes.coc.baseanalyser.model.trash + +import org.danielholmes.coc.baseanalyser.model.{PreventsTroopDrop, Tile, Building} +import org.scalactic.anyvals.PosInt + +case class DarkElixirStorage(level: PosInt, tile: Tile) extends Building with PreventsTroopDrop { + val size = PosInt(3) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/DarkSpellFactory.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/DarkSpellFactory.scala new file mode 100644 index 0000000..e8bf143 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/DarkSpellFactory.scala @@ -0,0 +1,8 @@ +package org.danielholmes.coc.baseanalyser.model.trash + +import org.danielholmes.coc.baseanalyser.model.{PreventsTroopDrop, Tile, Building} +import org.scalactic.anyvals.PosInt + +case class DarkSpellFactory(level: PosInt, tile: Tile) extends Building with PreventsTroopDrop { + val size = PosInt(3) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/ElixirCollector.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/ElixirCollector.scala new file mode 100644 index 0000000..5e776cb --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/ElixirCollector.scala @@ -0,0 +1,8 @@ +package org.danielholmes.coc.baseanalyser.model.trash + +import org.danielholmes.coc.baseanalyser.model.{PreventsTroopDrop, Tile, Building} +import org.scalactic.anyvals.PosInt + +case class ElixirCollector(level: PosInt, tile: Tile) extends Building with PreventsTroopDrop { + val size = PosInt(3) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/ElixirStorage.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/ElixirStorage.scala new file mode 100644 index 0000000..717fd22 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/ElixirStorage.scala @@ -0,0 +1,8 @@ +package org.danielholmes.coc.baseanalyser.model.trash + +import org.danielholmes.coc.baseanalyser.model.{PreventsTroopDrop, Tile, Building} +import org.scalactic.anyvals.PosInt + +case class ElixirStorage(level: PosInt, tile: Tile) extends Building with PreventsTroopDrop { + val size = PosInt(3) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/GoldMine.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/GoldMine.scala new file mode 100644 index 0000000..44febd7 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/GoldMine.scala @@ -0,0 +1,8 @@ +package org.danielholmes.coc.baseanalyser.model.trash + +import org.danielholmes.coc.baseanalyser.model.{PreventsTroopDrop, Tile, Building} +import org.scalactic.anyvals.PosInt + +case class GoldMine(level: PosInt, tile: Tile) extends Building with PreventsTroopDrop { + val size = PosInt(3) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/GoldStorage.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/GoldStorage.scala new file mode 100644 index 0000000..c0e6637 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/GoldStorage.scala @@ -0,0 +1,8 @@ +package org.danielholmes.coc.baseanalyser.model.trash + +import org.danielholmes.coc.baseanalyser.model.{PreventsTroopDrop, Tile, Building} +import org.scalactic.anyvals.PosInt + +case class GoldStorage(level: PosInt, tile: Tile) extends Building with PreventsTroopDrop { + val size = PosInt(3) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/Laboratory.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/Laboratory.scala new file mode 100644 index 0000000..cf75536 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/Laboratory.scala @@ -0,0 +1,8 @@ +package org.danielholmes.coc.baseanalyser.model.trash + +import org.danielholmes.coc.baseanalyser.model.{PreventsTroopDrop, Tile, Building} +import org.scalactic.anyvals.PosInt + +case class Laboratory(level: PosInt, tile: Tile) extends Building with PreventsTroopDrop { + val size = PosInt(4) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/SpellFactory.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/SpellFactory.scala new file mode 100644 index 0000000..ae8e72c --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/trash/SpellFactory.scala @@ -0,0 +1,8 @@ +package org.danielholmes.coc.baseanalyser.model.trash + +import org.danielholmes.coc.baseanalyser.model.{PreventsTroopDrop, Tile, Building} +import org.scalactic.anyvals.PosInt + +case class SpellFactory(level: PosInt, tile: Tile) extends Building with PreventsTroopDrop { + val size = PosInt(3) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/Archer.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/Archer.scala new file mode 100644 index 0000000..3df27e3 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/Archer.scala @@ -0,0 +1,12 @@ +package org.danielholmes.coc.baseanalyser.model.troops + +import org.danielholmes.coc.baseanalyser.model.{Structure, Village} +import org.scalactic.anyvals.{PosInt, PosZDouble} + +object Archer extends Troop { + val Range = PosZDouble(3.5) + + override protected def getPrioritisedTargets(village: Village): List[Set[Structure]] = { + getAnyBuildingsTargets(village) + } +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/ArcherQueen.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/ArcherQueen.scala new file mode 100644 index 0000000..2b3a19d --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/ArcherQueen.scala @@ -0,0 +1,12 @@ +package org.danielholmes.coc.baseanalyser.model.troops + +import org.danielholmes.coc.baseanalyser.model._ +import org.scalactic.anyvals.PosZDouble + +object ArcherQueen extends Troop { + val Range = PosZDouble(5) + + override protected def getPrioritisedTargets(village: Village): List[Set[Structure]] = { + getAnyBuildingsTargets(village) + } +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/ArcherQueenAttacking.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/ArcherQueenAttacking.scala new file mode 100644 index 0000000..9087483 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/ArcherQueenAttacking.scala @@ -0,0 +1,9 @@ +package org.danielholmes.coc.baseanalyser.model.troops + +import org.danielholmes.coc.baseanalyser.model.{PreventsTroopDrop, TileCoordinate} + +case class ArcherQueenAttacking(startPosition: TileCoordinate, targeting: PreventsTroopDrop) { + lazy val hitPoint = targeting.findClosestHitCoordinate(startPosition) + + lazy val distance = startPosition.distanceTo(hitPoint) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/ArcherTargeting.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/ArcherTargeting.scala new file mode 100644 index 0000000..469fe87 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/ArcherTargeting.scala @@ -0,0 +1,7 @@ +package org.danielholmes.coc.baseanalyser.model.troops + +import org.danielholmes.coc.baseanalyser.model.{Structure, TileCoordinate} + +case class ArcherTargeting(standingPosition: TileCoordinate, targeting: Structure) { + lazy val hitPoint = targeting.findClosestHitCoordinate(standingPosition) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/Dragon.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/Dragon.scala new file mode 100644 index 0000000..d26e189 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/Dragon.scala @@ -0,0 +1,12 @@ +package org.danielholmes.coc.baseanalyser.model.troops + +import org.danielholmes.coc.baseanalyser.model.{Structure, Village} +import org.scalactic.anyvals.{PosZDouble, PosZInt} + +object Dragon extends Troop { + val Range = PosZDouble(1) + + override protected def getPrioritisedTargets(village: Village): List[Set[Structure]] = { + getAnyBuildingsTargets(village) + } +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/HogRider.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/HogRider.scala new file mode 100644 index 0000000..1954089 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/HogRider.scala @@ -0,0 +1,14 @@ +package org.danielholmes.coc.baseanalyser.model.troops + +import org.danielholmes.coc.baseanalyser.model._ +import org.scalactic.anyvals.{PosInt, PosZDouble} + +object HogRider extends Troop { + val Range = PosZDouble(0) + + val HousingSpace = PosInt(5) + + override protected def getPrioritisedTargets(village: Village): List[Set[Structure]] = { + getDefenseTargetingTargets(village) + } +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/HogTargeting.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/HogTargeting.scala new file mode 100644 index 0000000..c959946 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/HogTargeting.scala @@ -0,0 +1,17 @@ +package org.danielholmes.coc.baseanalyser.model.troops + +import org.apache.commons.math3.geometry.euclidean.twod.{Line, Segment} +import org.danielholmes.coc.baseanalyser.model.range.{CircularElementRange, ElementRange} +import org.danielholmes.coc.baseanalyser.model.{Element, PreventsTroopDrop, Structure, TileCoordinate} + +case class HogTargeting(startPosition: TileCoordinate, targeting: Structure) { + lazy val hitPoint = targeting.findClosestHitCoordinate(startPosition) + + lazy val distance = startPosition.distanceTo(hitPoint) + + def cutsRadius(range: CircularElementRange): Boolean = range.cutBy(asSegment) + + private val asLine = new Line(startPosition, hitPoint, 0.01) + + private val asSegment = new Segment(startPosition, hitPoint, asLine) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/LavaHound.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/LavaHound.scala new file mode 100644 index 0000000..567b7df --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/LavaHound.scala @@ -0,0 +1,10 @@ +package org.danielholmes.coc.baseanalyser.model.troops + +import org.danielholmes.coc.baseanalyser.model.defense.AirDefense +import org.danielholmes.coc.baseanalyser.model.Block + +object LavaHound { + def getRestingArea(airDefense: AirDefense): Block = { + airDefense.block.contractBy(1) + } +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/Minion.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/Minion.scala new file mode 100644 index 0000000..2fbeefb --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/Minion.scala @@ -0,0 +1,12 @@ +package org.danielholmes.coc.baseanalyser.model.troops + +import org.danielholmes.coc.baseanalyser.model.{Structure, Village} +import org.scalactic.anyvals.PosZDouble + +object Minion extends Troop { + val Range = PosZDouble(0.75) + + override protected def getPrioritisedTargets(village: Village): List[Set[Structure]] = { + getAnyBuildingsTargets(village) + } +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/MinionAttackPosition.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/MinionAttackPosition.scala new file mode 100644 index 0000000..4c66bb0 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/MinionAttackPosition.scala @@ -0,0 +1,7 @@ +package org.danielholmes.coc.baseanalyser.model.troops + +import org.danielholmes.coc.baseanalyser.model.{FloatMapCoordinate, Structure} + +case class MinionAttackPosition(startPosition: FloatMapCoordinate, targeting: Structure) { + lazy val hitPoint = targeting.findClosestHitCoordinate(startPosition) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/Troop.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/Troop.scala new file mode 100644 index 0000000..1ef2dfd --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/Troop.scala @@ -0,0 +1,106 @@ +package org.danielholmes.coc.baseanalyser.model.troops + +import org.danielholmes.coc.baseanalyser.model.{Block, _} +import org.scalactic.anyvals.{PosInt, PosZDouble, PosZInt} + +import scala.annotation.tailrec + +trait Troop { + val Range: PosZDouble + + protected def getPrioritisedTargets(village: Village): List[Set[Structure]] + + protected def getDefenseTargetingTargets(village: Village): List[Set[Structure]] = { + List( + village.stationaryDefensiveBuildings.toSet[Structure], + (village.buildings -- village.stationaryDefensiveBuildings).toSet[Structure] + ) + } + + protected def getAnyBuildingsTargets(village: Village): List[Set[Structure]] = { + List(village.buildings.map(_.asInstanceOf[Structure])) + } + + def firstPossibleAttackingCoordinate(element: Element, coordinates: Set[TileCoordinate]): Option[TileCoordinate] = { + firstPossibleAttackingCoordinate(element.block, element.block.border.toList, Set.empty, coordinates) + } + + @tailrec + private def firstPossibleAttackingCoordinate( + block: Block, + toProcess: List[TileCoordinate], + processed: Set[TileCoordinate], + allowed: Set[TileCoordinate] + ): Option[TileCoordinate] = { + toProcess match { + case Nil => None + case head :: tail => + if (allowed.contains(head)) { + Some(head) + } else { + val unProcessedNeighboursWithinRange = head.neighbours.diff(processed) + .filter(coord => block.distanceTo(coord) < Range) + firstPossibleAttackingCoordinate( + block, + tail ::: unProcessedNeighboursWithinRange.toList, + processed ++ unProcessedNeighboursWithinRange, + allowed + ) + } + } + } + + def getAttackFloatCoordinates(structure: Structure): Set[FloatMapCoordinate] = { + val diagonalRange = Math.sqrt(Range / 2) + structure.hitBlock.allCoordinates.map(TileCoordinate.widenToMapCoordinate) ++ + structure.hitBlock.leftSide.map(_.offset(-Range, 0)) ++ + structure.hitBlock.rightSide.map(_.offset(Range, 0)) ++ + structure.hitBlock.topSide.map(_.offset(0, -Range)) ++ + structure.hitBlock.bottomSide.map(_.offset(0, Range)) + + structure.hitBlock.topLeft.offset(-diagonalRange, -diagonalRange) + + structure.hitBlock.topRight.offset(diagonalRange, -diagonalRange) + + structure.hitBlock.bottomLeft.offset(-diagonalRange, diagonalRange) + + structure.hitBlock.bottomRight.offset(diagonalRange, diagonalRange) + } + + def getAttackTileCoordinates(structure: Structure): Set[TileCoordinate] = { + structure.hitBlock.topLeft.offset(-Range.toInt, -Range.toInt) + .matrixOfCoordinatesTo(structure.hitBlock.bottomRight.offset(Range.toInt, Range.toInt)) + .filter(coord => structure.hitBlock.distanceTo(coord) <= Range) + } + + def findReachableTargets(coordinate: TileCoordinate, village: Village): Set[Structure] = { + findClosestTargets(getPrioritisedTargets(village), coordinate, (d: PosZDouble) => d < Range) + } + + def getAllPossibleTargets(village: Village): Set[Structure] = { + getPrioritisedTargets(village).flatMap(structures => structures).toSet + } + + def findTargets(coordinate: TileCoordinate, village: Village): Set[Structure] = { + findClosestTargets(getPrioritisedTargets(village), coordinate, (d: PosZDouble) => true) + } + + @tailrec + private def findClosestTargets(targets: List[Set[Structure]], coordinate: TileCoordinate, distanceFilter: PosZDouble => Boolean): Set[Structure] = { + targets match { + case Nil => Set.empty + case head :: tail => + val setTargets = findClosestTargets(head, coordinate, distanceFilter) + setTargets.toList match { + case Nil => findClosestTargets(tail, coordinate, distanceFilter) + case result: List[Structure] => result.toSet + } + } + } + + private def findClosestTargets(targets: Set[Structure], coordinate: TileCoordinate, distanceFilter: PosZDouble => Boolean): Set[Structure] = { + targets.groupBy(_.hitBlock.distanceTo(coordinate)) + .toSeq + .filter(t => distanceFilter(t._1)) + .sortBy(_._1) + .headOption + .map(_._2) + .getOrElse(Set.empty) + } +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/WizardTowerHoundTargeting.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/WizardTowerHoundTargeting.scala new file mode 100644 index 0000000..de6924e --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/model/troops/WizardTowerHoundTargeting.scala @@ -0,0 +1,6 @@ +package org.danielholmes.coc.baseanalyser.model.troops + +import org.danielholmes.coc.baseanalyser.model.Block +import org.danielholmes.coc.baseanalyser.model.defense.{AirDefense, WizardTower} + +case class WizardTowerHoundTargeting(tower: WizardTower, airDefense: AirDefense, houndTarget: Block) diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/stringdisplay/StringDisplayer.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/stringdisplay/StringDisplayer.scala new file mode 100644 index 0000000..cec53cd --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/stringdisplay/StringDisplayer.scala @@ -0,0 +1,139 @@ +package org.danielholmes.coc.baseanalyser.stringdisplay + +import org.danielholmes.coc.baseanalyser.model._ +import org.danielholmes.coc.baseanalyser.model.defense._ +import org.danielholmes.coc.baseanalyser.model.heroes.{ArcherQueenAltar, BarbarianKingAltar} +import org.danielholmes.coc.baseanalyser.model.range.ElementRange +import org.danielholmes.coc.baseanalyser.model.special.{ClanCastle, TownHall} + +import scala.annotation.tailrec + +class StringDisplayer { + import StringDisplayer._ + + def build(base: Village): String = buildString(buildCollection(base)) + + def buildColoured(base: Village): String = { + build(base).toIterable + .map(c => colorChar(c) + c.toString) + .mkString("") + Console.RESET + } + + private def colorChar(char: Char): String = { + if (WallChars.contains(char)) { + Console.WHITE + } else { + Colors.toVector(Math.abs(char.hashCode) % Colors.size) + } + } + + private def buildString(collection: Seq[Seq[Char]]): String = { + collection.map(_ :+ "\n") + .map(_.mkString("")) + .mkString("") + } + + private def buildCollection(base: Village): Seq[Seq[Char]] = { + drawBoundary( + drawCCRadius( + base, + drawElements(base.elements.toList, List.fill[Char](Tile.MaxCoordinate + 1, Tile.MaxCoordinate + 1) { ' ' }) + ) + ) + } + + private def drawCCRadius(village: Village, current: List[List[Char]]): List[List[Char]] = { + village.clanCastle + .map(_.range) + .map(drawCCRadius(_, current, Tile.All.toList)) + .getOrElse(current) + } + + @tailrec + private def drawCCRadius(range: ElementRange, current: List[List[Char]], tiles: List[Tile]): List[List[Char]] = { + tiles match { + case Nil => current + case head :: tail => { + val newCurrent = if (range.touchesEdge(tiles.head)) { draw(current, head, '^') } else { current } + drawCCRadius(range, newCurrent, tail) + } + } + } + + @tailrec + private def drawElements(elements: List[Element], current: List[List[Char]]): List[List[Char]] = { + elements match { + case Nil => current + case head :: tail => drawElements(tail, drawElement(head, current)) + } + } + + private def drawElement(element: Element, current: List[List[Char]]): List[List[Char]] = { + drawElement(characterForElement(element), element.block.tiles.toList, current) + } + + @tailrec + private def drawElement(char: Char, tiles: List[Tile], current: List[List[Char]]): List[List[Char]] = { + tiles match { + case Nil => current + case head :: tail => drawElement(char, tail, draw(current, head, char)) + } + } + + private def draw(map: List[List[Char]], tile: Tile, char: Char): List[List[Char]] = { + map.patch(tile.y, Seq(map(tile.y).patch(tile.x, Seq(char), 1)), 1) + } + + private def characterForElement(element: Element): Char = { + element match { + case _: ArcherTower => 'A' + case _: BarbarianKingAltar => 'B' + case _: AirDefense => 'D' + case _: Cannon => 'C' + case _: InfernoTower => 'I' + case _: Mortar => 'M' + case _: ArcherQueenAltar => 'Q' + case _: AirSweeper => 'S' + case _: HiddenTesla => 'T' + case _: WizardTower => 'W' + case _: XBow => 'X' + case _: ClanCastle => '@' + case _: TownHall => '#' + case _: Wall => '+' + case _ => + val possibles = Vector('1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '*', '/', '\\', '<', '>') + val charSeed = element.getClass.getName + possibles.toVector(Math.abs(charSeed.hashCode) % possibles.size) + } + } + + private def drawBoundary(current: List[List[Char]]): List[List[Char]] = { + (HorizontalWall :: verticalWall(current, List.empty)) :+ HorizontalWall + } + + @tailrec + private def verticalWall(inner: List[List[Char]], current: List[List[Char]]): List[List[Char]] = { + inner match { + case Nil => current + case head :: tail => verticalWall(tail, current :+ ((WallVert :: head) :+ WallVert)) + } + } +} + +object StringDisplayer { + private val WallCorner = '+' + private val WallHor = '-' + private val WallVert = '|' + private val WallChars = Set(WallCorner, WallHor, WallVert) + + private val Colors = Set( + Console.MAGENTA, + Console.BLUE, + Console.CYAN, + Console.GREEN, + Console.RED, + Console.YELLOW + ) + + private val HorizontalWall: List[Char] = (WallCorner :: List.fill[Char](TileCoordinate.MaxCoordinate) { WallHor }) :+ WallCorner +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/stringdisplay/StringTroopDropDisplayer.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/stringdisplay/StringTroopDropDisplayer.scala new file mode 100644 index 0000000..c064532 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/stringdisplay/StringTroopDropDisplayer.scala @@ -0,0 +1,28 @@ +package org.danielholmes.coc.baseanalyser.stringdisplay + +import org.danielholmes.coc.baseanalyser.model._ + +import scala.annotation.tailrec + +class StringTroopDropDisplayer { + def build(village: Village): String = { + build( + village.coordinatesAllowedToDropTroop.toSeq, + List.fill[Char](TileCoordinate.MaxCoordinate + 1, TileCoordinate.MaxCoordinate + 1) { ' ' } + ).map(_ :+ "\n") + .map(_.mkString("")) + .mkString("") + } + + @tailrec + private def build(coords: Seq[TileCoordinate], current: List[List[Char]]): List[List[Char]] = { + coords match { + case Nil => current + case head :: tail => build(tail, drawCoord(head, current)) + } + } + + private def drawCoord(coord: TileCoordinate, current: List[List[Char]]): List[List[Char]] = { + current.patch(coord.y, Seq(current(coord.y).patch(coord.x, Seq('+'), 1)), 1) + } +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/util/ElementsBuilder.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/util/ElementsBuilder.scala new file mode 100644 index 0000000..f74765e --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/util/ElementsBuilder.scala @@ -0,0 +1,53 @@ +package org.danielholmes.coc.baseanalyser.util + +import org.danielholmes.coc.baseanalyser.model.{Element, Tile, Village, Wall} +import org.scalactic.anyvals.{PosInt, PosZInt} + +object ElementsBuilder { + def elementFence(origin: Tile, width: PosInt, height: PosInt): Set[Element] = { + wallFence(origin, width, height).map(_.asInstanceOf[Element]) + } + + def wallFence(origin: Tile, width: PosInt, height: PosInt): Set[Wall] = { + rectangle(origin, width, height, 1, Wall(1, _)) + } + + def rectangle[T <: Element](origin: Tile, xTimes: PosInt, yTimes: PosInt, step: PosInt, builder: (Tile) => T): Set[T] = { + ElementsBuilder.repeatX(origin, xTimes, step, builder) ++ + ElementsBuilder.repeatX(origin.offset(0, (yTimes - 1) * step), xTimes, step, builder) ++ + ElementsBuilder.repeatY(origin.offset(0, step), PosInt.from(yTimes - 2).get, step, builder) ++ + ElementsBuilder.repeatY(origin.offset((xTimes - 1) * step, step), PosInt.from(yTimes - 2).get, step, builder) + } + + def repeatX[T <: Element](origin: Tile, times: PosInt, step: PosInt, builder: (Tile) => T): Set[T] = { + Range(0, times) + .map(origin.x + _ * step) + .map((x: Int) => Tile(PosZInt.from(x).get, origin.y)) + .map(builder.apply) + .toSet + } + + private def repeatY[T <: Element](origin: Tile, times: PosInt, step: PosInt, builder: (Tile) => T): Set[T] = { + Range(0, times) + .map(origin.y + _ * step) + .map((y: Int) => Tile(origin.x, PosZInt.from(y).get)) + .map(builder.apply) + .toSet + } + + def fromString[T <: Element](input: String, origin: Tile, builder: (Tile) => T): Set[T] = { + input.split("\n") + .zipWithIndex + .flatMap(row => { + row._1 + .zipWithIndex + .filter(_._1 != ' ') + .map(col => builder.apply(Tile(PosZInt.from(origin.x + col._2).get, PosZInt.from(origin.y + row._2).get))) + }) + .toSet + } + + def villageFromString(input: String, origin: Tile, builder: (Tile) => Element): Village = { + Village(fromString(input, origin, builder)) + } +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/util/GameConnectionNotAvailableException.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/util/GameConnectionNotAvailableException.scala new file mode 100644 index 0000000..0008ce7 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/util/GameConnectionNotAvailableException.scala @@ -0,0 +1,3 @@ +package org.danielholmes.coc.baseanalyser.util + +class GameConnectionNotAvailableException extends Exception diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/util/Memo2.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/util/Memo2.scala new file mode 100644 index 0000000..468d04a --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/util/Memo2.scala @@ -0,0 +1,10 @@ +package org.danielholmes.coc.baseanalyser.util + +import scala.collection.mutable + +case class Memo2[A,B,C](f: (A, B) => C) extends ((A, B) => C) { + private val cache = mutable.Map.empty[(A, B), C] + def apply(a: A, b: B): C = { + cache.getOrElseUpdate((a, b), f(a, b)) + } +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/util/TimedInvocation.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/util/TimedInvocation.scala new file mode 100644 index 0000000..7b08842 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/util/TimedInvocation.scala @@ -0,0 +1,12 @@ +package org.danielholmes.coc.baseanalyser.util + +import java.time.Duration + +object TimedInvocation { + def run[T](op: () => T): (T, Duration) = { + val start = System.currentTimeMillis + val result = op.apply() + val duration = Duration.ofMillis(System.currentTimeMillis - start) + (result, duration) + } +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/web/ModelViewModels.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/web/ModelViewModels.scala new file mode 100644 index 0000000..d732077 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/web/ModelViewModels.scala @@ -0,0 +1,81 @@ +package org.danielholmes.coc.baseanalyser.web + +case class TileCoordinateViewModel(x: Int, y: Int) +case class MapCoordinateViewModel(x: Double, y: Double) + +sealed trait ElementRangeViewModel { + val typeName: String +} +case class CircularElementRangeViewModel(inner: Double, outer: Double, override val typeName: String = "Circular") extends ElementRangeViewModel +case class BlindSpotSectorElementRangeViewModel( + angle: Double, + angleSize: Double, + innerSize: Double, + outerSize: Double, + override val typeName: String = "Sector" +) extends ElementRangeViewModel + +case class BlockViewModel(x: Int, y: Int, size: Int) +case class TileViewModel(x: Int, y: Int) + +sealed trait ElementViewModel { + def id: String + def typeName: String + def block: BlockViewModel +} +case class DecorationElementViewModel( + override val id: String, + override val typeName: String, + override val block: BlockViewModel +) extends ElementViewModel +case class TrapElementViewModel( + override val id: String, + override val typeName: String, + level: Int, + override val block: BlockViewModel +) extends ElementViewModel +case class BaseStructureElementViewModel( + override val id: String, + override val typeName: String, + level: Int, + override val block: BlockViewModel, + noTroopDropBlock: BlockViewModel +) extends ElementViewModel +case class DefenseElementViewModel( + override val id: String, + override val typeName: String, + level: Int, + override val block: BlockViewModel, + noTroopDropBlock: BlockViewModel, + range: ElementRangeViewModel +) extends ElementViewModel +case class HiddenTeslaViewModel( + override val id: String, + override val typeName: String, + level: Int, + override val block: BlockViewModel, + range: ElementRangeViewModel +) extends ElementViewModel +case class ClanCastleElementViewModel( + override val id: String, + override val typeName: String, + level: Int, + override val block: BlockViewModel, + noTroopDropBlock: BlockViewModel, + range: ElementRangeViewModel +) extends ElementViewModel +case class VillageViewModel( + elements: Set[ElementViewModel], + wallCompartments: Set[WallCompartmentViewModel], + possibleInternalLargeTraps: Set[PossibleLargeTrapViewModel] +) { + require(elements.toList.map(_.id).distinct.size == elements.size, "All ids must be unique") +} + +case class HogTargetingViewModel(startPosition: TileCoordinateViewModel, targetingId: String, hitPoint: TileCoordinateViewModel) +case class ArcherTargetingViewModel(standingPosition: TileCoordinateViewModel, targetingId: String, hitPoint: TileCoordinateViewModel) +case class ArcherQueenAttackingViewModel(standingPosition: TileCoordinateViewModel, targetingId: String, hitPoint: TileCoordinateViewModel) +case class WallCompartmentViewModel(id: String, walls: Set[String], innerTiles: Set[TileViewModel], elementIds: Set[String]) +case class PossibleLargeTrapViewModel(x: Int, y: Int) +case class WizardTowerHoundTargetingViewModel(tower: String, airDefense: String, houndTarget: BlockViewModel) +case class MinionAttackPositionViewModel(startPosition: MapCoordinateViewModel, targetingId: String, hitPoint: TileCoordinateViewModel) diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/web/MustacheRenderer.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/web/MustacheRenderer.scala new file mode 100644 index 0000000..335a9ab --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/web/MustacheRenderer.scala @@ -0,0 +1,16 @@ +package org.danielholmes.coc.baseanalyser.web + +import java.io.StringWriter + +import com.github.mustachejava.MustacheFactory + +class MustacheRenderer(val mustacheFactory: MustacheFactory) { + def render(name: String, vars: Object): String = { + val mustache = mustacheFactory.compile(name) + + val writer = new StringWriter() + mustache.execute(writer, vars).flush() + + writer.toString + } +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/web/PermittedClan.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/web/PermittedClan.scala new file mode 100644 index 0000000..dfab027 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/web/PermittedClan.scala @@ -0,0 +1,3 @@ +package org.danielholmes.coc.baseanalyser.web + +case class PermittedClan(code: String, name: String, id: Long) diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/web/RuleViewModels.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/web/RuleViewModels.scala new file mode 100644 index 0000000..3e36097 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/web/RuleViewModels.scala @@ -0,0 +1,87 @@ +package org.danielholmes.coc.baseanalyser.web + +sealed trait RuleResultViewModel { + val code: String + val title: String + val description: String + val success: Boolean +} + +case class RuleResultSummaryViewModel(shortName: String, success: Boolean) + +case class HogCCLureResultViewModel( + success: Boolean, + targetings: Set[HogTargetingViewModel], + code: String, + title: String, + description: String +) extends RuleResultViewModel +case class ArcherAnchorResultViewModel( + success: Boolean, + targetings: Set[ArcherTargetingViewModel], + aimingDefenses: Set[String], + code: String, + title: String, + description: String +) extends RuleResultViewModel +case class HighHPUnderAirDefResultViewModel( + success: Boolean, + outOfAirDefRange: Set[String], + inAirDefRange: Set[String], + code: String, + title: String, + description: String +) extends RuleResultViewModel +case class AirSnipedDefenseResultViewModel( + success: Boolean, + attackPositions: Set[MinionAttackPositionViewModel], + airDefenses: Set[String], + code: String, + title: String, + description: String +) extends RuleResultViewModel +case class MinimumCompartmentsResultViewModel( + success: Boolean, + minimumCompartments: Int, + compartments: Set[String], + code: String, + title: String, + description: String +) extends RuleResultViewModel +case class BKSwappableResultViewModel( + success: Boolean, + exposedTiles: Set[TileViewModel], + code: String, + title: String, + description: String +) extends RuleResultViewModel +case class WizardTowersOutOfHoundPositionsResultViewModel( + success: Boolean, + outOfRange: Set[String], + inRange: Set[WizardTowerHoundTargetingViewModel], + code: String, + title: String, + description: String +) extends RuleResultViewModel +case class QueenWalkedAirDefenseResultViewModel( + success: Boolean, + attackings: Set[ArcherQueenAttackingViewModel], + nonReachableAirDefs: Set[String], + code: String, + title: String, + description: String +) extends RuleResultViewModel +case class QueenWontLeaveCompartmentRuleResultViewModel( + success: Boolean, + code: String, + title: String, + description: String +) extends RuleResultViewModel +case class EnoughPossibleTrapLocationsRuleResultViewModel( + success: Boolean, + score: Double, + minScore: Double, + code: String, + title: String, + description: String +) extends RuleResultViewModel diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/web/TopLevelViewModels.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/web/TopLevelViewModels.scala new file mode 100644 index 0000000..2d94bd4 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/web/TopLevelViewModels.scala @@ -0,0 +1,31 @@ +package org.danielholmes.coc.baseanalyser.web + +case class AnalysisReportSummaryViewModel( + townHallLevel: Int, + resultSummaries: Set[RuleResultSummaryViewModel], + connectionTime: String, + analysisTime: String +) + +case class AnalysisReportViewModel(village: VillageViewModel, results: Set[RuleResultViewModel]) + +case class CantAnalyseVillageViewModel(village: VillageViewModel, message: String) + +case class ExceptionViewModel(uri: String, exceptionType: String, message: String, trace: List[String]) + +case class BaseAnalysisViewModel( + mapTiles: Int, + borderTiles: Int, + clanName: String, + playerIgn: String, + layoutDescription: String, + report: String, + warning: Option[String], + times: BaseAnalysisProfilingViewModel +) + +case class BaseAnalysisProfilingViewModel( + connection: String, + analysis: String, + times: Seq[(String, Seq[(String, String)])] +) diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/web/ViewModelMapper.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/web/ViewModelMapper.scala new file mode 100644 index 0000000..8f9ce47 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/web/ViewModelMapper.scala @@ -0,0 +1,379 @@ +package org.danielholmes.coc.baseanalyser.web + +import java.time.Duration +import java.util.{Base64, UUID} + +import org.danielholmes.coc.baseanalyser.analysis._ +import org.danielholmes.coc.baseanalyser.gameconnection.ClanSeekerProtocol.PlayerVillage +import org.danielholmes.coc.baseanalyser.model.Layout.Layout +import org.danielholmes.coc.baseanalyser.model._ +import org.danielholmes.coc.baseanalyser.model.defense.HiddenTesla +import org.danielholmes.coc.baseanalyser.model.heroes.HeroAltar +import org.danielholmes.coc.baseanalyser.model.range.{BlindSpotCircularElementRange, BlindSpotSectorElementRange, CircularElementRange, ElementRange} +import org.danielholmes.coc.baseanalyser.model.special.ClanCastle +import org.danielholmes.coc.baseanalyser.model.troops._ +import spray.http.Uri +import spray.httpx.SprayJsonSupport._ +import spray.json._ +import ViewModelProtocol._ +import org.danielholmes.coc.baseanalyser.model.traps.Trap + +class ViewModelMapper { + def exception(uri: Uri, e: Exception): ExceptionViewModel = { + ExceptionViewModel( + uri.toString, + e.getClass.getName, + e.getMessage, + e.getStackTrace + .map((el: StackTraceElement) => s"${el.getClassName}.${el.getMethodName} - ${el.getFileName}:${el.getLineNumber}") + .toList + ) + } + + def analysisSummary(userName: String, report: AnalysisReport, connectionDuration: Duration): AnalysisReportSummaryViewModel = { + AnalysisReportSummaryViewModel( + report.village.townHallLevel.get, + report.results.map(ruleResultSummary), + formatSecs(connectionDuration), + formatSecs(report.profiling.total) + ) + } + + private def ruleResultSummary(ruleResult: RuleResult): RuleResultSummaryViewModel = { + RuleResultSummaryViewModel(ruleResult.ruleDetails.shortName, ruleResult.success) + } + + def baseAnalysis( + clan: PermittedClan, + player: PlayerVillage, + layout: Layout, + analysis: AnalysisReport, + connectionDuration: Duration + ): BaseAnalysisViewModel = { + baseAnalysis( + clan, + player, + layout, + analysis, + connectionDuration, + None + ) + } + + private def baseAnalysis( + clan: PermittedClan, + player: PlayerVillage, + layout: Layout, + analysis: AnalysisReport, + connectionDuration: Duration, + warning: Option[String] + ): BaseAnalysisViewModel = { + BaseAnalysisViewModel( + Tile.MapSize.toInt, + Tile.OutsideBorder.toInt, + clan.name, + player.avatar.userName, + Layout.getDescription(layout), + analysisReport(analysis).toJson.compactPrint, + warning, + baseAnalysisProfiling(connectionDuration, analysis.profiling) + ) + } + + def baseAnalysisError( + clan: PermittedClan, + player: PlayerVillage, + layout: Layout, + village: Village, + connectionDuration: Duration, + warning: String + ): BaseAnalysisViewModel = { + baseAnalysis( + clan, + player, + layout, + AnalysisReport(village, Set.empty, AnalysisProfiling(Map.empty, Map.empty)), + connectionDuration, + Some(warning) + ) + } + + private def baseAnalysisProfiling( + connectionDuration: Duration, + profiling: AnalysisProfiling + ): BaseAnalysisProfilingViewModel = { + BaseAnalysisProfilingViewModel( + formatSecs(connectionDuration), + formatSecs(profiling.rulesDuration.plus(profiling.buildingBlocksDuration)), + Seq( + ("Game Connection", Seq(("Total", formatSecs(connectionDuration)))), + ( + s"Building blocks (${formatSecs(profiling.buildingBlocksDuration)})", + profiling.buildingBlocksSorted.map(t => (t._1, formatSecs(t._2))) + ), + ( + s"Analysis (${formatSecs(profiling.rulesDuration)})", + profiling.rulesSorted.map(t => (t._1.shortName, formatSecs(t._2))) + ) + ) + ) + } + + private def formatSecs(duration: Duration): String = { + "%.3f".format(duration.toMillis / 1000.0) + "s" + } + + def analysisReport(report: AnalysisReport): AnalysisReportViewModel = { + AnalysisReportViewModel(village(report.village), report.results.map(ruleResult)) + } + + def cantAnalyseVillage(invalidVillage: Village, message: String): CantAnalyseVillageViewModel = { + CantAnalyseVillageViewModel(village(invalidVillage), message) + } + + private def minionAttackPosition(position: MinionAttackPosition): MinionAttackPositionViewModel = { + MinionAttackPositionViewModel( + mapCoordinate(position.startPosition), + objectId(position.targeting), + tileCoordinate(position.hitPoint) + ) + } + + private def ruleResult(result: RuleResult): RuleResultViewModel = { + result match { + case h: HogCCLureRuleResult => HogCCLureResultViewModel( + h.success, + h.targeting + .groupBy(_.targeting) + .values + .map(_.minBy(_.distance)) + .map(hogTargeting) + .toSet, + h.ruleDetails.code, + h.ruleDetails.name, + h.ruleDetails.description + ) + case h: ArcherAnchorRuleResult => ArcherAnchorResultViewModel( + h.success, + h.targeting + .groupBy(_.targeting) + .values + .map(_.head) + .map(archerTargeting) + .toSet, + h.aimingDefenses.map(objectId), + h.ruleDetails.code, + h.ruleDetails.name, + h.ruleDetails.description + ) + case a: HighHPUnderAirDefRuleResult => HighHPUnderAirDefResultViewModel( + a.success, + a.outOfAirDefRange.map(objectId), + a.inAirDefRange.map(objectId), + a.ruleDetails.code, + a.ruleDetails.name, + a.ruleDetails.description + ) + case a: AirSnipedDefenseRuleResult => AirSnipedDefenseResultViewModel( + a.success, + a.snipedDefenses.map(minionAttackPosition), + a.airDefenses.map(objectId), + a.ruleDetails.code, + a.ruleDetails.name, + a.ruleDetails.description + ) + case m: MinimumCompartmentsRuleResult => MinimumCompartmentsResultViewModel( + m.success, + m.minimumCompartments, + m.buildingCompartments.map(objectId), + m.ruleDetails.code, + m.ruleDetails.name, + m.ruleDetails.description + ) + case b: BKSwappableRuleResult => BKSwappableResultViewModel( + b.success, + b.exposedTiles.map(tile), + b.ruleDetails.code, + b.ruleDetails.name, + b.ruleDetails.description + ) + case w: WizardTowersOutOfHoundPositionsRuleResult => WizardTowersOutOfHoundPositionsResultViewModel( + w.success, + w.outOfRange.map(objectId), + w.inRange.map(wizardTowerHoundTargeting), + w.ruleDetails.code, + w.ruleDetails.name, + w.ruleDetails.description + ) + case q: QueenWalkedAirDefenseRuleResult => QueenWalkedAirDefenseResultViewModel( + q.success, + q.attackings.map(archerQueenAttacking), + q.nonReachableAirDefs.map(objectId), + q.ruleDetails.code, + q.ruleDetails.name, + q.ruleDetails.description + ) + case q: QueenWontLeaveCompartmentRuleResult => QueenWontLeaveCompartmentRuleResultViewModel( + q.success, + q.ruleDetails.code, + q.ruleDetails.name, + q.ruleDetails.description + ) + case t: EnoughPossibleTrapLocationsRuleResult => EnoughPossibleTrapLocationsRuleResultViewModel( + t.success, + t.score, + t.minScore, + t.ruleDetails.code, + t.ruleDetails.name, + t.ruleDetails.description + ) + case _ => throw new RuntimeException(s"Don't know how to create view model for ${result.getClass.getSimpleName}") + } + } + + private def archerQueenAttacking(attacking: ArcherQueenAttacking): ArcherQueenAttackingViewModel = { + ArcherQueenAttackingViewModel( + tileCoordinate(attacking.startPosition), + objectId(attacking.targeting), + tileCoordinate(attacking.hitPoint) + ) + } + + private def hogTargeting(targeting: HogTargeting): HogTargetingViewModel = { + HogTargetingViewModel( + tileCoordinate(targeting.startPosition), + objectId(targeting.targeting), + tileCoordinate(targeting.hitPoint) + ) + } + + private def archerTargeting(targeting: ArcherTargeting): ArcherTargetingViewModel = { + ArcherTargetingViewModel( + tileCoordinate(targeting.standingPosition), + objectId(targeting.targeting), + tileCoordinate(targeting.hitPoint) + ) + } + + private def wallCompartment(compartment: WallCompartment): WallCompartmentViewModel = { + WallCompartmentViewModel( + objectId(compartment), + compartment.walls.map(objectId), + compartment.innerTiles.map(tile), + compartment.elements.map(objectId) + ) + } + + private def tile(tile: Tile): TileViewModel = TileViewModel(tile.x, tile.y) + + private def village(village: Village): VillageViewModel = { + VillageViewModel( + village.elements.map(element), + village.wallCompartments.map(wallCompartment), + village.possibleInternalLargeTraps.map(possibleInternalLargeTrap) + ) + } + + private def possibleInternalLargeTrap(trap: PossibleLargeTrap): PossibleLargeTrapViewModel = { + PossibleLargeTrapViewModel(trap.tile.x, trap.tile.y) + } + + private def element(element: Element): ElementViewModel = { + val typeName = element.getClass.getSimpleName + element match { + case d: StationaryDefensiveBuilding => + d match { + case p: PreventsTroopDrop => DefenseElementViewModel ( + objectId (d), + typeName, + element.level, + block(d.block), + block(p.preventTroopDropBlock), + elementRange(d.range) + ) + case h: HiddenTesla => HiddenTeslaViewModel( + objectId (d), + typeName, + element.level, + block(d.block), + elementRange (d.range) + ) + case _ => throw new RuntimeException(s"Can't map ${element.getClass.getSimpleName}") + } + case h: HeroAltar => DefenseElementViewModel ( + objectId(h), + typeName, + element.level, + block(h.block), + block(h.preventTroopDropBlock), + elementRange(h.range) + ) + case c: ClanCastle => ClanCastleElementViewModel( + objectId(c), + typeName, + c.level, + block(c.block), + block(c.preventTroopDropBlock), + elementRange(c.range) + ) + case p: PreventsTroopDrop => BaseStructureElementViewModel( + objectId(p), + typeName, + p.level, + block(p.block), + block(p.preventTroopDropBlock) + ) + case t: Trap => TrapElementViewModel( + objectId(t), + typeName, + t.level, + block(t.block) + ) + case d: Decoration => DecorationElementViewModel( + objectId(d), + typeName, + block(d.block) + ) + case e: Element => throw new RuntimeException(s"Can't map ${element.getClass.getSimpleName}") + } + } + + private def wizardTowerHoundTargeting(targeting: WizardTowerHoundTargeting): WizardTowerHoundTargetingViewModel = { + WizardTowerHoundTargetingViewModel( + objectId(targeting.tower), + objectId(targeting.airDefense), + block(targeting.houndTarget) + ) + } + + private def elementRange(range: ElementRange): ElementRangeViewModel = { + range match { + case c: CircularElementRange => CircularElementRangeViewModel(0, c.size) + case b: BlindSpotCircularElementRange => CircularElementRangeViewModel(b.innerSize, b.outerSize) + case w: BlindSpotSectorElementRange => BlindSpotSectorElementRangeViewModel( + w.angle.degrees, + w.angleSize.degrees, + w.innerSize, + w.outerSize + ) + case _ => throw new RuntimeException("Can't render element range") + } + + } + + private def block(block: Block): BlockViewModel = { + BlockViewModel(block.x, block.y, block.size) + } + + private def tileCoordinate(coord: TileCoordinate): TileCoordinateViewModel = { + TileCoordinateViewModel(coord.x, coord.y) + } + + private def mapCoordinate(coord: FloatMapCoordinate): MapCoordinateViewModel = { + MapCoordinateViewModel(coord.x, coord.y) + } + + private def objectId(obj: Object): String = { + new String(Base64.getEncoder.encode(UUID.nameUUIDFromBytes(obj.toString.getBytes).toString.getBytes)).substring(0, 9) + } +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/web/ViewModelProtocol.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/web/ViewModelProtocol.scala new file mode 100644 index 0000000..4d2c641 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/web/ViewModelProtocol.scala @@ -0,0 +1,97 @@ +package org.danielholmes.coc.baseanalyser.web + +import spray.json.{DefaultJsonProtocol, JsValue, RootJsonFormat} + +object ViewModelProtocol extends DefaultJsonProtocol { + implicit object ElementJsonFormat extends RootJsonFormat[ElementViewModel] { + def write(e: ElementViewModel): JsValue = e match { + case d: DefenseElementViewModel => defenseElementFormat.write(d) + case d: HiddenTeslaViewModel => hiddenTeslaFormat.write(d) + case c: ClanCastleElementViewModel => clanCastleElementFormat.write(c) + case s: BaseStructureElementViewModel => baseStructureElementFormat.write(s) + case t: TrapElementViewModel => trapElementFormat.write(t) + case d: DecorationElementViewModel => decorationElementFormat.write(d) + } + + def read(value: JsValue): ElementViewModel = { + throw new NotImplementedError() + } + } + + implicit object RuleResultJsonFormat extends RootJsonFormat[RuleResultViewModel] { + def write(r: RuleResultViewModel): JsValue = r match { + case h: HogCCLureResultViewModel => hogCCLureResultFormat.write(h) + case a: ArcherAnchorResultViewModel => archerAnchorResultFormat.write(a) + case a: HighHPUnderAirDefResultViewModel => highHPUnderAirDefResultFormat.write(a) + case a: AirSnipedDefenseResultViewModel => airSnipedDefenseResultFormat.write(a) + case m: MinimumCompartmentsResultViewModel => minimumCompartmentsResultFormat.write(m) + case b: BKSwappableResultViewModel => bkSwappableResultFormat.write(b) + case w: WizardTowersOutOfHoundPositionsResultViewModel => wizardTowersOutOfHoundPositionsResultFormat.write(w) + case q: QueenWalkedAirDefenseResultViewModel => queenWalkedAirDefenseResultFormat.write(q) + case q: QueenWontLeaveCompartmentRuleResultViewModel => queenWontLeaveCompartmentResultFormat.write(q) + case t: EnoughPossibleTrapLocationsRuleResultViewModel => enoughPossibleTrapLocationsResultFormat.write(t) + case _ => throw new RuntimeException(s"Don't know how to serialise ${r.getClass.getSimpleName}") + } + + def read(value: JsValue): RuleResultViewModel = { + throw new NotImplementedError() + } + } + + implicit object ElementRangeJsonFormat extends RootJsonFormat[ElementRangeViewModel] { + def write(e: ElementRangeViewModel): JsValue = e match { + case c: CircularElementRangeViewModel => circularElementRangeFormat.write(c) + case w: BlindSpotSectorElementRangeViewModel => blindSpotSectorElementRangeFormat.write(w) + case _ => throw new RuntimeException(s"Can't render ${e.getClass.getSimpleName}") + } + + def read(value: JsValue): ElementRangeViewModel = { + throw new NotImplementedError() + } + } + + implicit val circularElementRangeFormat = jsonFormat3(CircularElementRangeViewModel) + implicit val blindSpotSectorElementRangeFormat = jsonFormat5(BlindSpotSectorElementRangeViewModel) + + implicit val tileCoordinateFormat = jsonFormat2(TileCoordinateViewModel) + implicit val mapCoordinateFormat = jsonFormat2(MapCoordinateViewModel) + implicit val blockFormat = jsonFormat3(BlockViewModel) + implicit val tileFormat = jsonFormat2(TileViewModel) + implicit val wallCompartmentFormat = jsonFormat4(WallCompartmentViewModel) + implicit val possibleLargeTrapFormat = jsonFormat2(PossibleLargeTrapViewModel) + + implicit val decorationElementFormat = jsonFormat3(DecorationElementViewModel) + implicit val trapElementFormat = jsonFormat4(TrapElementViewModel) + implicit val baseStructureElementFormat = jsonFormat5(BaseStructureElementViewModel) + implicit val hiddenTeslaFormat = jsonFormat5(HiddenTeslaViewModel) + implicit val defenseElementFormat = jsonFormat6(DefenseElementViewModel) + implicit val clanCastleElementFormat = jsonFormat6(ClanCastleElementViewModel) + + implicit val archerTargetingFormat = jsonFormat3(ArcherTargetingViewModel) + implicit val minionAttackPositionFormat = jsonFormat3(MinionAttackPositionViewModel) + implicit val hogTargetingFormat = jsonFormat3(HogTargetingViewModel) + implicit val archerQueenAttackingFormat = jsonFormat3(ArcherQueenAttackingViewModel) + implicit val wizardTowerHoundTargetingFormat = jsonFormat3(WizardTowerHoundTargetingViewModel) + + implicit val hogCCLureResultFormat = jsonFormat5(HogCCLureResultViewModel) + implicit val archerAnchorResultFormat = jsonFormat6(ArcherAnchorResultViewModel) + implicit val highHPUnderAirDefResultFormat = jsonFormat6(HighHPUnderAirDefResultViewModel) + implicit val airSnipedDefenseResultFormat = jsonFormat6(AirSnipedDefenseResultViewModel) + implicit val minimumCompartmentsResultFormat = jsonFormat6(MinimumCompartmentsResultViewModel) + implicit val bkSwappableResultFormat = jsonFormat5(BKSwappableResultViewModel) + implicit val wizardTowersOutOfHoundPositionsResultFormat = jsonFormat6(WizardTowersOutOfHoundPositionsResultViewModel) + implicit val queenWalkedAirDefenseResultFormat = jsonFormat6(QueenWalkedAirDefenseResultViewModel) + implicit val queenWontLeaveCompartmentResultFormat = jsonFormat4(QueenWontLeaveCompartmentRuleResultViewModel) + implicit val enoughPossibleTrapLocationsResultFormat = jsonFormat6(EnoughPossibleTrapLocationsRuleResultViewModel) + + implicit val villageFormat = jsonFormat3(VillageViewModel) + implicit val analysisReportFormat = jsonFormat2(AnalysisReportViewModel) + implicit val cantAnalyseVillageFormat = jsonFormat2(CantAnalyseVillageViewModel) + implicit val baseAnalysisProfilingFormat = jsonFormat3(BaseAnalysisProfilingViewModel) + implicit val baseAnalysisFormat = jsonFormat8(BaseAnalysisViewModel) + + implicit val resultSummaryFormat = jsonFormat2(RuleResultSummaryViewModel) + implicit val analysisReportSummaryFormat = jsonFormat4(AnalysisReportSummaryViewModel) + + implicit val exceptionFormat = jsonFormat4(ExceptionViewModel) +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/web/WebApp.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/web/WebApp.scala new file mode 100644 index 0000000..9bd4691 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/web/WebApp.scala @@ -0,0 +1,19 @@ +package org.danielholmes.coc.baseanalyser.web + +import akka.actor.{ActorSystem, Props} +import akka.io.IO +import akka.pattern.ask +import akka.util.Timeout +import spray.can.Http +import spray.servlet.WebBoot + +import scala.concurrent.duration._ + +trait WebApp { + implicit val system = ActorSystem("WebApp") + val apiActor = system.actorOf(Props[WebAppServiceActor], "webAppActor") +} + +class WebAppServlet extends WebBoot with WebApp { + override val serviceActor = apiActor +} diff --git a/src/main/scala/org/danielholmes/coc/baseanalyser/web/WebAppServiceActor.scala b/src/main/scala/org/danielholmes/coc/baseanalyser/web/WebAppServiceActor.scala new file mode 100644 index 0000000..03fe305 --- /dev/null +++ b/src/main/scala/org/danielholmes/coc/baseanalyser/web/WebAppServiceActor.scala @@ -0,0 +1,224 @@ +package org.danielholmes.coc.baseanalyser.web + +import java.time.Duration + +import akka.actor.{Actor, ActorContext} +import org.danielholmes.coc.baseanalyser.Services +import spray.routing._ +import spray.http._ +import MediaTypes._ +import akka.util.Timeout + +import scala.concurrent.duration._ +import spray.httpx.SprayJsonSupport._ +import spray.json._ +import ViewModelProtocol._ +import com.google.common.net.UrlEscapers +import org.danielholmes.coc.baseanalyser.analysis.AnalysisReport +import org.danielholmes.coc.baseanalyser.gameconnection.ClanSeekerProtocol.{ClanDetails, PlayerSummary} +import org.danielholmes.coc.baseanalyser.model.Layout.Layout +import org.danielholmes.coc.baseanalyser.model.{Layout, Tile} +import org.danielholmes.coc.baseanalyser.util.GameConnectionNotAvailableException +import spray.util.LoggingContext + +class WebAppServiceActor extends Actor with HttpService with Services { + def actorRefFactory: ActorContext = context + + def receive: Actor.Receive = runRoute(route) + + implicit val timeout = Timeout(120.seconds) + + override def timeoutRoute: Route = complete(StatusCodes.InternalServerError, "Took too long") + + implicit def exceptionHandler(implicit log: LoggingContext): ExceptionHandler = + ExceptionHandler { + case g: GameConnectionNotAvailableException => + errorPage( + StatusCodes.ServiceUnavailable, + "Connection to Game Servers not available", + """We're currently using a third party service for this which can be unreliable. + | It's usually only temporary though and worth trying again shortly""".stripMargin + ) + case e: Exception => + respondWithMediaType(`application/json`) { + requestUri { uri => + log.error(e, uri.toString + " - " + e.getMessage) + complete(StatusCodes.InternalServerError, viewModelMapper.exception(uri, e)) + } + } + } + + implicit def rejectionHandler(implicit log: LoggingContext): RejectionHandler = + RejectionHandler { + case Nil => notFoundPage("Page doesn't exist") + } + + private def generatePlayerAnalysisUrl(clanCode: String, player: PlayerSummary, layout: Layout) = { + val escape = (part: String) => UrlEscapers.urlPathSegmentEscaper().escape(part) + s"/clans/${escape(clanCode)}/players/${escape(player.avatar.currentHomeId.toString)}/${layout.toString}" + } + + private def generatePlayerAnalysisSummaryUrl(clanCode: String, player: PlayerSummary, layout: Layout) = { + val escape = (part: String) => UrlEscapers.urlPathSegmentEscaper().escape(part) + s"/clans/${escape(clanCode)}/players/${escape(player.avatar.currentHomeId.toString)}/${layout.toString}/summary" + } + + private def notFoundPage(message: String) = { + errorPage(StatusCodes.NotFound, "Not Found", message) + } + + private def errorPage(status: StatusCode, title: String, message: String) = { + respondWithMediaType(`text/html`) { + complete( + status, + mustacheRenderer.render("web/error.mustache", Map("title" -> title, "message" -> message)) + ) + } + } + + private def getClanByCode(code: String)(handler: (ClanDetails) => Route): Route = { + get { + permittedClans.find(_.code == code) + .map(clan => + gameConnection.getClanDetails(clan.id) + .getOrElse(throw new GameConnectionNotAvailableException) + ) + .map(handler) + .getOrElse(notFoundPage(s"Clan with code $code not found")) + } + } + + val route: Route = + handleExceptions(exceptionHandler) { + handleRejections(rejectionHandler) { + compressResponse() { + respondWithMediaType(`text/html`) { + pathSingleSlash { + getFromResource("web/index.html") + } ~ + path("clans" / Segment) { (clanCode) => + getClanByCode(clanCode) { + (clanDetails) => + complete( + mustacheRenderer.render( + "web/clan.mustache", + Map( + "name" -> clanDetails.name, + "bulkAnalysisUrl" -> s"/clans/$clanCode/war-bases", + "players" -> clanDetails.players + .toSeq + .sortBy(_.avatar.userName.toLowerCase) + .map(p => Map( + "ign" -> p.avatar.userName, + "warAnalysisUrl" -> generatePlayerAnalysisUrl(clanCode, p, Layout.War), + "homeAnalysisUrl" -> generatePlayerAnalysisUrl(clanCode, p, Layout.Home) + )) + ) + ) + ) + } + } ~ + path("clans" / Segment / "war-bases") { clanCode => + getClanByCode(clanCode) { + (clanDetails) => + complete( + mustacheRenderer.render( + "web/war-bases.mustache", + Map( + "name" -> clanDetails.name, + "players" -> clanDetails.players + .map(p => Map( + "id" -> p.avatar.currentHomeId, + "ign" -> p.avatar.userName, + "analysisUrl" -> generatePlayerAnalysisUrl(clanCode, p, Layout.War), + "analysisSummaryUrl" -> generatePlayerAnalysisSummaryUrl(clanCode, p, Layout.War) + )) + ) + ) + ) + } + } ~ + path("clans" / Segment / "players" / LongNumber / Segment) { (clanCode, playerId, layoutName) => + get { + facades.getVillageAnalysis(clanCode, playerId, layoutName) + .map({ + case (clan, report, village, player, layout, connectionDuration) => + report.map(analysis => + complete( + mustacheRenderer.render( + "web/base-analysis.mustache", + viewModelMapper.baseAnalysis( + clan, + player, + layout, + analysis, + connectionDuration + ) + ) + ) + ) + .getOrElse( + complete( + mustacheRenderer.render( + "web/base-analysis.mustache", + viewModelMapper.baseAnalysisError( + clan, + player, + layout, + village, + connectionDuration, + s"""${player.avatar.userName} village can't be analysed - currently only supporting + |TH${villageAnalyser.minTownHallLevel.toInt}-${villageAnalyser.maxTownHallLevel.toInt}""".stripMargin + ) + ) + ) + ) + }) + .recover(notFoundPage) + .get + } + } + } ~ + path("sys" / "exception") { + get { + complete { + println("Exception on purpose") + throw new RuntimeException("Some exception happened") + } + } + } ~ + path("sys" / "timeout") { ctx => + println("Provoking Timeout") + // we simply let the request drop to provoke a timeout + } ~ + respondWithMediaType(`application/json`) { + path("clans" / Segment / "players" / LongNumber / Segment / "summary") { (clanCode, playerId, layoutName) => + get { + facades.getVillageAnalysis(clanCode, playerId, layoutName) + .map({ + case (clan, report, village, player, layout, connectionDuration) => + report + .map(analysis => + complete(viewModelMapper.analysisSummary( + player.avatar.userName, + analysis, + connectionDuration + )) + ) + .getOrElse( + complete( + StatusCodes.BadRequest, + s""""${player.avatar.userName} village can't be analysed - currently only supporting + |TH${villageAnalyser.minTownHallLevel.toInt}-${villageAnalyser.maxTownHallLevel.toInt}"""".stripMargin + ) + ) + }) + .recover(message => complete(StatusCodes.NotFound, message.toJson.compactPrint)) + .get + } + } + } + } + } + } +} diff --git a/src/main/webapp/.ebextensions/01_gzip_httpd.config b/src/main/webapp/.ebextensions/01_gzip_httpd.config new file mode 100644 index 0000000..6a10870 --- /dev/null +++ b/src/main/webapp/.ebextensions/01_gzip_httpd.config @@ -0,0 +1,5 @@ +container_commands: + 01_gzip_httpd: + command: "cp .ebextensions/enable_mod_deflate.conf /etc/httpd/conf.d" + 02_gzip_httpd: + command: "/etc/init.d/httpd reload" \ No newline at end of file diff --git a/src/main/webapp/.ebextensions/enable_mod_deflate.conf b/src/main/webapp/.ebextensions/enable_mod_deflate.conf new file mode 100644 index 0000000..a5eb923 --- /dev/null +++ b/src/main/webapp/.ebextensions/enable_mod_deflate.conf @@ -0,0 +1,29 @@ +# Restrict compression to these MIME types +AddOutputFilterByType DEFLATE text/plain +AddOutputFilterByType DEFLATE text/html +AddOutputFilterByType DEFLATE text/xml +AddOutputFilterByType DEFLATE text/css +AddOutputFilterByType DEFLATE application/json +AddOutputFilterByType DEFLATE application/xml +AddOutputFilterByType DEFLATE application/xhtml+xml +AddOutputFilterByType DEFLATE application/rss+xml +AddOutputFilterByType DEFLATE application/javascript +AddOutputFilterByType DEFLATE application/x-javascript +AddOutputFilterByType DEFLATE image/png +AddOutputFilterByType DEFLATE image/gif +AddOutputFilterByType DEFLATE image/jpeg + +# Level of compression (Highest 9 - Lowest 1) +DeflateCompressionLevel 9 + +# Netscape 4.x has some problems. +BrowserMatch ^Mozilla/4 gzip-only-text/html + +# Netscape 4.06-4.08 have some more problems +BrowserMatch ^Mozilla/4\.0[678] no-gzip + +# MSIE masquerades as Netscape, but it is fine +BrowserMatch \bMSI[E] !no-gzip !gzip-only-text/html + +# Make sure proxies don't deliver the wrong content +Header append Vary User-Agent env=!dont-vary \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000..3e98f30 --- /dev/null +++ b/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,32 @@ + + + + spray.servlet.Initializer + + + + SprayConnectorServlet + spray.servlet.Servlet30ConnectorServlet + true + + + + default + /js/* + + + + default + /css/* + + + + default + /images/* + + + + SprayConnectorServlet + /* + + \ No newline at end of file diff --git a/src/main/webapp/css/base-analysis.css b/src/main/webapp/css/base-analysis.css new file mode 100644 index 0000000..7fbdaba --- /dev/null +++ b/src/main/webapp/css/base-analysis.css @@ -0,0 +1,25 @@ +.panel .glyphicon-ok-sign { + color: #55bb55; +} +.panel .glyphicon-remove-sign { + color: #dd4444; +} +.panel .do-show { + display: none; + float: right; + color: #dd4444; + font-size: 12px; +} +.panel.failed .do-show { + display: block; +} +.panel.failed.active .do-show { + display: none; +} + +.panel .panel-heading { + cursor: pointer; +} +.panel .panel-heading:hover a { + text-decoration: underline; +} \ No newline at end of file diff --git a/src/main/webapp/css/main.css b/src/main/webapp/css/main.css new file mode 100644 index 0000000..e63f12a --- /dev/null +++ b/src/main/webapp/css/main.css @@ -0,0 +1,3 @@ +h1 { + font-size: 24px; +} \ No newline at end of file diff --git a/src/main/webapp/images/buildings-sprite.png b/src/main/webapp/images/buildings-sprite.png new file mode 100644 index 0000000..36308e0 Binary files /dev/null and b/src/main/webapp/images/buildings-sprite.png differ diff --git a/src/main/webapp/images/walls.png b/src/main/webapp/images/walls.png new file mode 100644 index 0000000..195f4c9 Binary files /dev/null and b/src/main/webapp/images/walls.png differ diff --git a/src/main/webapp/js/base-analysis/DisplaySettings.js b/src/main/webapp/js/base-analysis/DisplaySettings.js new file mode 100644 index 0000000..652d597 --- /dev/null +++ b/src/main/webapp/js/base-analysis/DisplaySettings.js @@ -0,0 +1,35 @@ +"use strict"; + +var DisplaySettings = function(jStorage) { + var KEY = "baseDisplaySettings"; + var showGridChanged = new signals.Signal(); + + var getStored = function() { + return _.extend({ showGrid: false }, jStorage.get(KEY, {})); + }; + + var store = function() { + jStorage.set(KEY, { showGrid: showGrid }); + }; + + var setShowGrid = function(newShowGrid) { + if (newShowGrid != showGrid) { + showGrid = newShowGrid; + showGridChanged.dispatch(); + store(); + } + }; + + var getShowGrid = function() { + return showGrid; + }; + + var initStored = getStored(); + var showGrid = initStored.showGrid; + + return { + setShowGrid: setShowGrid, + getShowGrid: getShowGrid, + showGridChanged: showGridChanged + }; +}; \ No newline at end of file diff --git a/src/main/webapp/js/base-analysis/MapDisplay2d.js b/src/main/webapp/js/base-analysis/MapDisplay2d.js new file mode 100644 index 0000000..486a4e6 --- /dev/null +++ b/src/main/webapp/js/base-analysis/MapDisplay2d.js @@ -0,0 +1,759 @@ +"use strict"; + +var MapDisplay2d = function(canvas, mapConfig, model, displaySettings) { + var stage = new createjs.Stage(canvas); + stage.autoClear = false; + var bgContainer = new createjs.Container(); + stage.addChild(bgContainer); + var buildingsContainer = new createjs.Container(); + stage.addChild(buildingsContainer); + var extrasContainer = new createjs.Container(); + stage.addChild(extrasContainer); + + var successColour = "#77ff77"; + var failColour = "#ff4444"; + + var buildingSheet = null; + var wallSheet = null; + + var hashCode = function(str){ + var hash = 0; + if (str.length == 0) return hash; + for (var i = 0; i < str.length; i++) { + var char = str.charCodeAt(i); + hash = ((hash<<5)-hash)+char; + hash = hash & hash; + } + return hash; + }; + + var randomColour = function(seed) { + var base = hashCode("" + seed).toString(16).slice(2, 8); + return '#' + base + Array(6 - base.length + 1).join("0"); + }; + + var renderElementRanges = function(mapDimensions, elements) { + _.chain(elements) + .map(function (elementToDraw) { + switch (elementToDraw.range.typeName) { + case "Sector": + return createSectorElementRangeDisplay(mapDimensions, elementToDraw); + case "Circular": + return createCircularElementRangeDisplay(mapDimensions, elementToDraw); + default: + console.log("Can't render", elementToDraw.range) + } + }) + .each(function (circle) { + extrasContainer.addChild(circle); + }); + }; + + var createSectorElementRangeDisplay = function(mapDimensions, element) { + var angleSizeRadians = element.range.angleSize / 180 * Math.PI; + var minLine = createSectorLineDetails(Math.PI / 2, element.range.innerSize, element.range.outerSize); + var maxLine = createSectorLineDetails(Math.PI / 2 - angleSizeRadians, element.range.innerSize, element.range.outerSize); + + var display = new createjs.Shape(); + display.x = mapDimensions.toCanvasCoord(element.block.x + element.block.size / 2); + display.y = mapDimensions.toCanvasCoord(element.block.y + element.block.size / 2); + + display.graphics + .beginStroke("#ffffff") + .moveTo(mapDimensions.toCanvasSize(minLine.coord1.x), mapDimensions.toCanvasSize(minLine.coord1.y)) + .lineTo(mapDimensions.toCanvasSize(minLine.coord2.x), mapDimensions.toCanvasSize(minLine.coord2.y)) + .arc(0, 0, mapDimensions.toCanvasSize(element.range.outerSize), 0, angleSizeRadians) + .lineTo(mapDimensions.toCanvasSize(maxLine.coord1.x), mapDimensions.toCanvasSize(maxLine.coord1.y)) + .arc(0, 0, mapDimensions.toCanvasSize(element.range.innerSize), angleSizeRadians, 0, true); + + display.rotation = 180 + element.range.angle + (element.range.angleSize - element.range.angle) / 2; + return display; + }; + + var createSectorLineDetails = function(angle, innerSize, outerSize) { + return { + coord1: { + x: Math.sin(angle) * innerSize, + y: Math.cos(angle) * innerSize + }, + coord2: { + x: Math.sin(angle) * outerSize, + y: Math.cos(angle) * outerSize + } + }; + }; + + var createLine = function(coord1, coord2, colour, mapDimensions) { + var line = new createjs.Shape(); + line.graphics + .beginStroke(colour) + .moveTo(0, 0) + .lineTo( + mapDimensions.toCanvasSize(coord1.x - coord2.x), + mapDimensions.toCanvasSize(coord1.y - coord2.y) + ); + line.x = mapDimensions.toCanvasCoord(coord2.x); + line.y = mapDimensions.toCanvasCoord(coord2.y); + return line; + }; + + var createCircularElementRangeDisplay = function(mapDimensions, element) { + var allInfo = new createjs.Container(); + var outerCircle = new createjs.Shape(); + outerCircle.graphics + .beginStroke("#ffffff") + .beginFill("rgba(255,255,255,0.05)") + .drawCircle( + 0, + 0, + mapDimensions.toCanvasSize(element.range.outer) + ); + allInfo.addChild(outerCircle); + + if (element.range.inner) { + var innerCircle = new createjs.Shape(); + innerCircle.graphics + .beginStroke("#ffaaaa") + .beginFill("rgba(255,80,80,0.05)") + .drawCircle( + 0, + 0, + mapDimensions.toCanvasSize(element.range.inner) + ); + allInfo.addChild(innerCircle); + } + + var lineSize = 1; + var vert = new createjs.Shape(); + vert.graphics + .beginStroke("#ffffff") + .moveTo(0, -mapDimensions.toCanvasSize(lineSize / 2)) + .lineTo(0, mapDimensions.toCanvasSize(lineSize / 2)); + allInfo.addChild(vert); + var hor = new createjs.Shape(); + hor.graphics + .beginStroke("#ffffff") + .moveTo(-mapDimensions.toCanvasSize(lineSize / 2), 0) + .lineTo(mapDimensions.toCanvasSize(lineSize / 2), 0); + allInfo.addChild(hor); + + allInfo.x = mapDimensions.toCanvasCoord(element.block.x + element.block.size / 2); + allInfo.y = mapDimensions.toCanvasCoord(element.block.y + element.block.size / 2); + + return allInfo; + }; + + var renderElementRangesByIds = function(mapDimensions, ids) { + renderElementRanges(mapDimensions, model.getVillageElementsByIds(ids)); + }; + + var renderElementRangesByTypeName = function(mapDimensions, typeName) { + renderElementRanges( + mapDimensions, + model.getVillageElementsByTypeName(typeName) + ); + }; + + var renderWizardTowersOutOfHoundPositions = function(result, mapDimensions) { + eachBuildingDisplay( + result.outOfRange, + function (buildingContainer) { + applyColour(buildingContainer, 0, 1, 0); + } + ); + + var inRangeTowerIds = _.pluck(result.inRange, 'tower'); + eachBuildingDisplay( + inRangeTowerIds, + function (buildingContainer) { + applyColour(buildingContainer, 1, 0, 0); + } + ); + eachBuildingDisplay( + _.pluck(result.inRange, 'airDefense'), + function (buildingContainer) { + applyColour(buildingContainer, 1, 0.8, 0.8); + } + ); + _.chain(result.inRange) + .pluck('houndTarget') + .each(function(target) { + var display = new createjs.Shape(); + display.alpha = 0.5; + display.graphics + .beginFill(failColour) + .drawRect(0, 0, mapDimensions.toCanvasSize(target.size), mapDimensions.toCanvasSize(target.size)); + display.x = mapDimensions.toCanvasCoord(target.x); + display.y = mapDimensions.toCanvasCoord(target.y); + extrasContainer.addChild(display); + }); + renderElementRangesByIds(mapDimensions, _.union(inRangeTowerIds, result.outOfRange)); + }; + + var renderQueenWalkedAirDefense = function(result, mapDimensions) { + eachBuildingDisplay( + result.nonReachableAirDefs, + function (buildingContainer) { + applyColour(buildingContainer, 0, 1, 0); + } + ); + eachBuildingDisplay( + _.pluck(result.attackings, 'targetingId'), + function (buildingContainer) { + applyColour(buildingContainer, 1, 0, 0); + } + ); + _.each( + result.attackings, + function(attacking) { + extrasContainer.addChild(createLine(attacking.hitPoint, attacking.standingPosition, failColour, mapDimensions)); + } + ); + }; + + var renderEnoughPossibleTrapLocations = function(result, mapDimensions) { + var colour = result.success ? successColour : failColour; + var display = new createjs.Shape(); + _.each( + model.getReport().village.possibleInternalLargeTraps, + function(trap) { + display.graphics + .beginFill(colour) + .drawRect( + mapDimensions.toCanvasCoord(trap.x), + mapDimensions.toCanvasCoord(trap.y), + mapDimensions.toCanvasSize(2), + mapDimensions.toCanvasSize(2) + ) + .endFill(); + } + ); + display.filters = [ + new createjs.ColorFilter( + 1, 1, 1, 0.4, + 0, 0, 0, 0 + ) + ]; + display.cache(0, 0, 1500, 1500); + extrasContainer.addChild(display); + }; + + var renderActiveRule = function(mapDimensions) { + if (!model.hasActiveRule()) { + return; + } + + var result = _.findWhere(model.getReport().results, { 'code': model.getActiveRuleCode() }); + switch (result.code) { + case 'ArcherAnchor': + renderArcherAnchor(result, mapDimensions); + break; + case 'HogCCLure': + renderHogCCLure(result, mapDimensions); + break; + case 'HighHPUnderAirDef': + renderHighHPUnderAirDef(result, mapDimensions); + break; + case 'AirSnipedDefense': + renderAirSnipedDefense(result, mapDimensions); + break; + case 'MinimumCompartments': + renderMinimumCompartments(result, mapDimensions); + break; + case 'BKSwappable': + renderBKSwappable(result, mapDimensions); + break; + case 'WizardTowersOutOfHoundPositions': + renderWizardTowersOutOfHoundPositions(result, mapDimensions); + break; + case 'QueenWalkedAirDefense': + renderQueenWalkedAirDefense(result, mapDimensions); + break; + case 'QueenWontLeaveCompartment': + renderQueenWontLeaveCompartment(result, mapDimensions); + break; + case 'EnoughPossibleTrapLocations': + renderEnoughPossibleTrapLocations(result, mapDimensions); + break; + default: + console.error('Don\'t know how to render active rule: ' + result.code); + } + }; + + var applyColour = function(display, r, g, b) { + display.filters = [ + new createjs.ColorFilter( + r, g, b, 1, + 0, 0, 0, 0 + ) + ]; + display.cache(0, 0, 1000, 1000); + }; + + var renderBKSwappable = function(result, mapDimensions) { + var bk = model.getVillageElementByTypeName("BarbarianKingAltar"); + if (bk == null) { + return; + } + + if (!result.success) { + var exposedMask = new createjs.Shape(); + exposedMask.graphics.beginFill(successColour); + _.each(result.exposedTiles, function (tile) { + exposedMask.graphics.drawRect( + mapDimensions.toCanvasCoord(tile.x), + mapDimensions.toCanvasCoord(tile.y), + mapDimensions.toCanvasSize(1), + mapDimensions.toCanvasSize(1) + ); + }); + + var bkRadiusFill = new createjs.Shape(); + bkRadiusFill.graphics + .beginFill(failColour) + .drawCircle( + 0, + 0, + mapDimensions.toCanvasSize(bk.range.outer) + ); + bkRadiusFill.x = mapDimensions.toCanvasCoord(bk.block.x + bk.block.size / 2); + bkRadiusFill.y = mapDimensions.toCanvasCoord(bk.block.y + bk.block.size / 2); + bkRadiusFill.alpha = 0.6; + bkRadiusFill.mask = exposedMask; + + extrasContainer.addChild(bkRadiusFill); + } + + renderElementRangesByTypeName(mapDimensions, "BarbarianKingAltar"); + }; + + var renderQueenWontLeaveCompartment = function(result, mapDimensions) { + var compartment = model.getVillageArcherQueenCompartment(); + if (compartment == null) { + return; + } + + highlightCompartment( + compartment, + result.success ? successColour : failColour, + mapDimensions + ) + }; + + var renderMinimumCompartments = function(result, mapDimensions) { + _.each( + model.getVillageCompartmentsByIds(result.compartments), + function(compartment) { + highlightCompartment(compartment, randomColour(compartment.walls.join("|")), mapDimensions); + } + ); + }; + + var highlightCompartment = function(compartment, colour, mapDimensions) { + var innerDisplay = new createjs.Shape(); + innerDisplay.graphics.beginFill(colour); + _.each(compartment.innerTiles, function(innerTile) { + innerDisplay.graphics.drawRect( + mapDimensions.toCanvasCoord(innerTile.x), + mapDimensions.toCanvasCoord(innerTile.y), + mapDimensions.toCanvasSize(1), + mapDimensions.toCanvasSize(1) + ); + }); + innerDisplay.alpha = 0.85; + extrasContainer.addChild(innerDisplay); + + var wallDisplay = new createjs.Shape(); + wallDisplay.graphics.beginFill(colour); + _.each(compartment.walls, function(wallId) { + var element = model.getVillageElementById(wallId); + wallDisplay.graphics.drawRect( + mapDimensions.toCanvasCoord(element.block.x), + mapDimensions.toCanvasCoord(element.block.y), + mapDimensions.toCanvasSize(1), + mapDimensions.toCanvasSize(1) + ); + }); + wallDisplay.alpha = 0.5; + extrasContainer.addChild(wallDisplay); + }; + + var renderAirSnipedDefense = function(result, mapDimensions) { + eachBuildingDisplay( + _.pluck(result.attackPositions, 'targetingId'), + function (buildingContainer) { + applyColour(buildingContainer, 1, 0, 0); + } + ); + _.chain(result.attackPositions) + .map(function(targeting) { + return createLine(targeting.hitPoint, targeting.startPosition, failColour, mapDimensions); + }) + .each(function(display) { extrasContainer.addChild(display); }); + renderElementRangesByIds(mapDimensions, result.airDefenses); + }; + + var renderArcherAnchor = function(result, mapDimensions) { + _.chain(result.targetings) + .map(function (targeting) { + extrasContainer.addChild(createLine(targeting.hitPoint, targeting.standingPosition, failColour, mapDimensions)); + return _.findWhere(buildingsContainer.children, { 'id': targeting.targetingId }); + }) + .each(function(buildingContainer) { + applyColour(buildingContainer, 1, 0, 0); + }); + renderElementRangesByIds(mapDimensions, result.aimingDefenses); + }; + + var renderHogCCLure = function(result, mapDimensions) { + _.chain(result.targetings) + .map(function (targeting) { + extrasContainer.addChild(createLine(targeting.hitPoint, targeting.startPosition, failColour, mapDimensions)); + return _.findWhere(buildingsContainer.children, { 'id': targeting.targetingId }); + }) + .each(function (buildingContainer) { + applyColour(buildingContainer, 1, 0, 0); + }); + renderElementRangesByTypeName(mapDimensions, "ClanCastle"); + }; + + var renderHighHPUnderAirDef = function(result, mapDimensions) { + eachBuildingDisplay( + result.outOfAirDefRange, + function (buildingContainer) { + applyColour(buildingContainer, 1, 0, 0); + } + ); + eachBuildingDisplay( + result.inAirDefRange, + function (buildingContainer) { + applyColour(buildingContainer, 0, 1, 0); + } + ); + renderElementRangesByTypeName(mapDimensions, "AirDefense"); + }; + + var eachBuildingDisplay = function(ids, operation) { + _.chain(ids) + .map(function (id) { + var building = _.findWhere(buildingsContainer.children, { 'id': id }); + if (building == null) { + console.error("No building with id " + id); + } + return building; + }) + .each(operation); + }; + + var renderGrass = function(mapDimensions) { + var grass = new createjs.Shape(); + grass.graphics + .beginFill("#598c02") + .drawRect( + mapDimensions.toCanvasCoord(0), + mapDimensions.toCanvasCoord(0), + mapDimensions.toCanvasSize(mapDimensions.totalTiles), + mapDimensions.toCanvasSize(mapDimensions.totalTiles) + ); + grass.graphics + .beginFill("#8cbf15") + .drawRect( + mapDimensions.toCanvasCoord(mapDimensions.borderTiles), + mapDimensions.toCanvasCoord(mapDimensions.borderTiles), + mapDimensions.toCanvasSize(mapDimensions.mapTiles), + mapDimensions.toCanvasSize(mapDimensions.mapTiles) + ); + _.chain(_.range(mapDimensions.mapTiles)) + .each(function(col) { + _.chain(_.range(mapDimensions.mapTiles)) + .each(function (row) { + if ((row + col) % 2 == 0) { + grass.graphics + .beginFill("#87ba10") + .drawRect( + mapDimensions.toCanvasCoord(mapDimensions.borderTiles + col), + mapDimensions.toCanvasCoord(mapDimensions.borderTiles + row), + mapDimensions.toCanvasSize(1), + mapDimensions.toCanvasSize(1) + ); + } + }) + }); + bgContainer.addChild(grass); + }; + + var render2dPreventTroopDrops = function(mapDimensions) { + var allPrevents = new createjs.Container(); + _.chain(model.getReport().village.elements) + .reject(function(e) { return !e.noTroopDropBlock; }) + .map(function(element) { + var prevent = new createjs.Shape(); + prevent.graphics + .beginFill("rgba(255,255,255,1)") + .drawRect( + 0, + 0, + mapDimensions.toCanvasSize(element.noTroopDropBlock.size), + mapDimensions.toCanvasSize(element.noTroopDropBlock.size) + ); + prevent.x = mapDimensions.toCanvasCoord(element.noTroopDropBlock.x); + prevent.y = mapDimensions.toCanvasCoord(element.noTroopDropBlock.y); + return prevent; + }) + .each(function(e) { allPrevents.addChild(e); }); + allPrevents.filters = [ + new createjs.ColorFilter( + 1, 1, 1, 0.15, + 0, 0, 0, 0 + ) + ]; + allPrevents.cache(0, 0, 1000, 1000); + bgContainer.addChild(allPrevents); + }; + + var render2dImageBuildings = function(mapDimensions) { + for (var i in model.getReport().village.elements) { + var element = model.getReport().village.elements[i]; + if (element.typeName == "Wall") { + renderWallImage(element, mapDimensions); + continue; + } + + renderBuildingImage(element, mapDimensions); + } + }; + + var renderWallImage = function(element, mapDimensions) { + buildingsContainer.addChild(renderElementImage(element, mapDimensions, wallSheet)); + }; + + var renderBuildingImage = function(element, mapDimensions) { + var display = renderElementImage(element, mapDimensions, buildingSheet); + if (element.noTroopDropBlock) { + var grass = new createjs.Shape(); + grass.graphics + .beginFill("#6fa414") + .drawRect( + 0, + 0, + mapDimensions.toCanvasSize(element.block.size), + mapDimensions.toCanvasSize(element.block.size) + ); + display.addChildAt(grass, 0); + } + buildingsContainer.addChild(display); + }; + + var renderElementImage = function(element, mapDimensions, sheet) { + var elementContainer = new createjs.Container(); + elementContainer.x = mapDimensions.toCanvasCoord(element.block.x); + elementContainer.y = mapDimensions.toCanvasCoord(element.block.y); + elementContainer.id = element.id; + + var bitmap = sheet.create(element, mapDimensions); + var finalWidth = bitmap.sourceRect.width * bitmap.scaleX; + var finalHeight = bitmap.sourceRect.height * bitmap.scaleY; + bitmap.x = (mapDimensions.toCanvasSize(element.block.size) - finalWidth) / 2; + bitmap.y = (mapDimensions.toCanvasSize(element.block.size) - finalHeight) / 2; + elementContainer.addChild(bitmap); + + return elementContainer; + }; + + var setAssets = function(newAssets) { + buildingSheet = new RedMoonBuildingSpriteSheet(newAssets.redMoonBuildings); + wallSheet = new RedMoonWallSpriteSheet(newAssets.redMoonWalls); + render(); + }; + + var render = function() { + stage.clear(); + bgContainer.removeAllChildren(); + extrasContainer.removeAllChildren(); + buildingsContainer.removeAllChildren(); + + if (buildingSheet == null || wallSheet == null) { + var text = new createjs.Text("Loading Images" + new Array(1 + parseInt(_.now() / 1000) % 4).join('.'), "14px monospace", "#000000"); + text.x = 5; + text.y = 5; + extrasContainer.addChild(text); + stage.update(); + + setTimeout(render, 1000); + + return; + } + + if (!model.hasReport()) { + stage.update(); + return; + } + + render2d(); + + stage.update(); + }; + + /*function renderSpriteSheetDebug() { + var canvasContext = canvas.getContext("2d"); + var useScale = Math.min( + canvas.width / assets.buildings.width, + canvas.height / assets.buildings.height + ); + canvasContext.drawImage( + assets.buildings, + 0, + 0, + assets.buildings.width, + assets.buildings.height, + 0, + 0, + assets.buildings.width * useScale, + assets.buildings.height * useScale + ); + + for (var i in buildingsSheet) { + var sheetDef = buildingsSheet[i]; + canvasContext.strokeStyle = randomColour(i); + // First gap + canvasContext.strokeRect( + sheetDef.x * useScale, + sheetDef.y * useScale, + sheetDef.gap / 2 * useScale, + sheetDef.height * useScale + ); + for (var j = 0; j < 13; j++) { + var startX = sheetDef.x + j * (sheetDef.gap + sheetDef.width) + sheetDef.gap / 2; + // image + canvasContext.strokeRect( + startX * useScale, + sheetDef.y * useScale, + sheetDef.width * useScale, + sheetDef.height * useScale + ); + // Gap + canvasContext.strokeRect( + (startX + sheetDef.width) * useScale, + sheetDef.y * useScale, + sheetDef.gap * useScale, + sheetDef.height * useScale + ); + } + } + }*/ + + var renderGrid = function(mapDimensions) { + if (!displaySettings.getShowGrid()) { + return; + } + + var colour = "#ff0000"; + var alpha = 0.3; + var mapIndexes = _.range(mapDimensions.totalTiles); + _.chain(mapIndexes) + .map(function(col) { + var strokeSize = col % 5 == 0 ? 2 : 1; + var line = new createjs.Shape(); + line.x = mapDimensions.toCanvasCoord(col); + line.y = 0; + line.alpha = alpha; + line.graphics + .beginStroke(colour) + .setStrokeStyle(strokeSize) + .moveTo(0, 0) + .lineTo(0, mapDimensions.toCanvasCoord(mapDimensions.totalTiles)); + return line; + }) + .each(function(display) { extrasContainer.addChild(display); }); + + _.chain(mapIndexes) + .map(function(row) { + var strokeSize = row % 5 == 0 ? 2 : 1; + var line = new createjs.Shape(); + line.x = 0; + line.y = mapDimensions.toCanvasCoord(row); + line.alpha = alpha; + line.graphics + .beginStroke(colour) + .setStrokeStyle(strokeSize) + .moveTo(0, 0) + .lineTo(mapDimensions.toCanvasCoord(mapDimensions.totalTiles), 0); + return line; + }) + .each(function(display) { extrasContainer.addChild(display); }); + + _.chain(mapIndexes) + .filter(function(mapIndex) { return mapIndex != 0 && mapIndex % 5 == 0; }) + .map(function(mapIndex) { + var textSize = Math.ceil(canvas.width / 50); + + var rowLeft = new createjs.Text(mapIndex, textSize + "px monospace", colour); + rowLeft.x = 0; + rowLeft.y = mapDimensions.toCanvasCoord(mapIndex); + rowLeft.textBaseline = "top"; + + var colTop = new createjs.Text(mapIndex, textSize + "px monospace", colour); + colTop.x = mapDimensions.toCanvasCoord(mapIndex); + colTop.y = 0; + colTop.textBaseline = "top"; + + var rowRight = new createjs.Text(mapIndex, textSize + "px monospace", colour); + rowRight.x = mapDimensions.toCanvasCoord(mapDimensions.totalTiles - mapDimensions.hiddenBorder); + rowRight.y = mapDimensions.toCanvasCoord(mapIndex); + rowRight.textBaseline = "top"; + rowRight.textAlign = "right"; + + var colBottom = new createjs.Text(mapIndex, textSize + "px monospace", colour); + colBottom.x = mapDimensions.toCanvasCoord(mapIndex); + colBottom.y = mapDimensions.toCanvasCoord(mapDimensions.totalTiles - mapDimensions.hiddenBorder); + colBottom.textBaseline = "bottom"; + + return [rowLeft, colTop, rowRight, colBottom]; + }) + .flatten() + .each(function(display) { extrasContainer.addChild(display); }); + }; + + var render2d = function() { + var mapDimensions = new MapDimensions(_.extend(mapConfig, { + totalTiles: mapConfig.mapTiles + mapConfig.borderTiles * 2, + canvasSize: Math.min($(canvas).width(), $(canvas).height()) + })); + renderGrass(mapDimensions); + //render2dRandomSolidColourElements(mapDimensions); + render2dPreventTroopDrops(mapDimensions); + render2dImageBuildings(mapDimensions); + renderActiveRule(mapDimensions); + renderGrid(mapDimensions); + }; + + var MapDimensions = function(props) { + var hiddenBorder = 2; + var tileSize = props.canvasSize / (props.totalTiles - 2 * hiddenBorder); + + var toCanvasCoord = function(coord) { + return (coord - hiddenBorder) * tileSize; + }; + + var toCanvasSize = function(size) { + return size * tileSize; + }; + + return { + mapTiles: props.mapTiles, + borderTiles: props.borderTiles, + totalTiles: props.totalTiles, + hiddenBorder: hiddenBorder, + toCanvasCoord: toCanvasCoord, + toCanvasSize: toCanvasSize + }; + }; + + displaySettings.showGridChanged.add(_.bind(render, this)); + + return { + render: render, + setAssets: setAssets, + canvas: canvas + }; +}; \ No newline at end of file diff --git a/src/main/webapp/js/base-analysis/Model.js b/src/main/webapp/js/base-analysis/Model.js new file mode 100644 index 0000000..7e47f35 --- /dev/null +++ b/src/main/webapp/js/base-analysis/Model.js @@ -0,0 +1,124 @@ +"use strict"; + +var Model = function() { + var currentReport = null; + var activeRuleCode = null; + var reportChanged = new signals.Signal(); + var ruleChanged = new signals.Signal(); + + var setReport = function(newReport) { + if (newReport == currentReport) { + return; + } + + currentReport = newReport; + clearActiveRule(); + reportChanged.dispatch(); + + if (currentReport != null) { + var firstFailedRule = _.chain(currentReport.results) + .findWhere({ "success": false }) + .value(); + if (firstFailedRule != null) { + setActiveRuleByCode(firstFailedRule.code); + } + } + }; + + var clearReport = function() { + setReport(null); + }; + + var getReport = function() { + return currentReport; + }; + + var getVillageElementById = function(id) { + return _.findWhere(currentReport.village.elements, { 'id': id }); + }; + + var getVillageElementsByIds = function(ids) { + return _.filter(currentReport.village.elements, function (element) { + return _.contains(ids, element.id); + }); + }; + + var getVillageCompartmentsByIds = function(ids) { + return _.filter(currentReport.village.wallCompartments, function (compartment) { + return _.contains(ids, compartment.id); + }); + }; + + var getVillageArcherQueenCompartment = function() { + return _.find( + currentReport.village.wallCompartments, + function(compartment) { + return _.some( + compartment.elementIds, + function(elementId) { + return getVillageElementById(elementId).typeName == "ArcherQueenAltar"; + } + ); + } + ); + }; + + var getVillageElementsByTypeName = function(typeName) { + return _.where(currentReport.village.elements, { 'typeName': typeName }); + }; + + var getVillageElementByTypeName = function(typeName) { + return _.findWhere(currentReport.village.elements, { 'typeName': typeName }); + }; + + var hasReport = function() { + return currentReport != null; + }; + + var hasActiveRule = function() { + return activeRuleCode != null; + }; + + var getActiveRuleCode = function() { + return activeRuleCode; + }; + + var clearActiveRule = function() { + setActiveRuleByCode(null); + }; + + var setActiveRuleByCode = function(newActiveRuleCode) { + if (newActiveRuleCode == activeRuleCode) { + return; + } + + if (newActiveRuleCode != null && _.findWhere(report.results, { 'code': newActiveRuleCode }) == null) { + console.error("No rule code in current report:", newActiveRuleCode, report); + return; + } + + activeRuleCode = newActiveRuleCode; + ruleChanged.dispatch(); + }; + + return { + getReport: getReport, + setReport: setReport, + hasReport: hasReport, + clearReport: clearReport, + getVillageElementById: getVillageElementById, + getVillageElementsByIds: getVillageElementsByIds, + getVillageElementsByTypeName: getVillageElementsByTypeName, + getVillageElementByTypeName: getVillageElementByTypeName, + getVillageCompartmentsByIds: getVillageCompartmentsByIds, + getVillageArcherQueenCompartment: getVillageArcherQueenCompartment, + + hasActiveRule: hasActiveRule, + getActiveRuleCode: getActiveRuleCode, + setActiveRuleByCode: setActiveRuleByCode, + clearActiveRule: clearActiveRule, + + reportChanged: reportChanged, + ruleChanged: ruleChanged + }; +}; \ No newline at end of file diff --git a/src/main/webapp/js/base-analysis/Preloader.js b/src/main/webapp/js/base-analysis/Preloader.js new file mode 100644 index 0000000..5ee23de --- /dev/null +++ b/src/main/webapp/js/base-analysis/Preloader.js @@ -0,0 +1,19 @@ +"use strict"; + +var Preloader = function() { + return { + loadAssets: function(callback) { + var queue = new createjs.LoadQueue(); + + var handleAssetsLoadComplete = function() { + callback(queue); + }; + + queue.on("complete", handleAssetsLoadComplete, this); + queue.loadManifest([ + { id: "redMoonBuildings", src:"/images/buildings-sprite.png" }, + { id: "redMoonWalls", src:"/images/walls.png" } + ]); + } + }; +}; \ No newline at end of file diff --git a/src/main/webapp/js/base-analysis/RedMoonBuildingSpriteSheet.js b/src/main/webapp/js/base-analysis/RedMoonBuildingSpriteSheet.js new file mode 100644 index 0000000..1a5656e --- /dev/null +++ b/src/main/webapp/js/base-analysis/RedMoonBuildingSpriteSheet.js @@ -0,0 +1,274 @@ +"use strict"; + +var RedMoonBuildingSpriteSheet = function(image) { + var defs = { + "SkeletonTrap": { + x: 1092, + y: 304, + width: 24, + height: 32, + gap: 2, + levelMultiplier: 0.5 + }, + "AirBomb": { + x: 994, + y: 297, + width: 25, + height: 40, + gap: 0, + levelMultiplier: 0.5 + }, + "SeekingAirMine": { + x: 1044, + y: 299, + width: 25, + height: 39, + gap: 0, + levelMultiplier: 0.5 + }, + "SpringTrap": { + x: 969, + y: 311, + width: 25, + height: 26, + gap: 0 + }, + "Bomb": { + x: 893, + y: 313, + width: 17, + height: 23, + gap: 9, + levelMultiplier: 0.5 + }, + "GiantBomb": { + x: 896, + y: 439, + width: 37, + height: 39, + gap: 12, + levelMultiplier: 0.5 + }, + "TownHall": { + x: 0, + y: 5, + width: 90, + height: 90, + gap: 12 + }, + "Laboratory": { + x: 0, + y: 118, + width: 80, + height: 64, + gap: 22 + }, + "BuilderHut": { + x: 697, + y: 342, + width: 36, + height: 38, + gap: 0 + }, + "BarbarianKingAltar": { + x: 749, + y: 339, + width: 60, + height: 43, + gap: 0 + }, + "ArcherQueenAltar": { + x: 824, + y: 338, + width: 59, + height: 42, + gap: 0 + }, + "ArmyCamp": { + x: 0, + y: 196, + width: 121, + height: 93, + gap: 8 + }, + "ClanCastle": { + x: 0, + y: 308, + width: 75, + height: 77, + gap: 1 + }, + "Barrack": { + x: 0, + y: 417, + width: 69, + height: 61, + gap: 7 + }, + "ArcherTower": { + x: 0, + y: 507, + width: 60, + height: 70, + gap: 16 + }, + "Cannon": { + x: 0, + y: 628, + width: 60, + height: 47, + gap: 16 + }, + "AirDefense": { + x: 0, + y: 704, + width: 60, + height: 64, + gap: 16 + }, + "XBow": { + x: 700, + y: 715, + width: 64, + height: 57, + gap: 10 + }, + "WizardTower": { + x: 0, + y: 1092, + width: 60, + height: 63, + gap: 16 + }, + "DarkElixirCollector": { + x: 648, + y: 1094, + width: 60, + height: 59, + gap: 16 + }, + "Mortar": { + x: 0, + y: 1209, + width: 60, + height: 44, + gap: 16 + }, + "DarkElixirStorage": { + x: 645, + y: 1194, + width: 65, + height: 63, + gap: 11 + }, + "GoldMine": { + x: 0, + y: 813, + width: 60, + height: 52, + gap: 16 + }, + "GoldStorage": { + x: 0, + y: 899, + width: 69, + height: 69, + gap: 7 + }, + "InfernoTower": { + x: 893, + y: 903, + width: 33, + height: 60, + gap: 16 + }, + "DarkBarrack": { + x: 0, + y: 995, + width: 69, + height: 69, + gap: 7 + }, + "SpellFactory": { + x: 550, + y: 1005, + width: 68, + height: 57, + gap: 7 + }, + "ElixirCollector": { + x: 0, + y: 1289, + width: 62, + height: 60, + gap: 14 + }, + "ElixirStorage": { + x: 0, + y: 1385, + width: 66, + height: 66, + gap: 10 + }, + "DarkSpellFactory": { + x: 912, + y: 1398, + width: 66, + height: 49, + gap: 10 + }, + "HiddenTesla": { + x: 0, + y: 1491, + width: 49, + height: 54, + gap: 0 + }, + "AirSweeper": { + x: 546, + y: 1503, + width: 49, + height: 44, + gap: 0 + }, + "Decoration": { + "//": "just a dummy sprite for the moment - decorations not fully implemented", + x: 912, + y: 1498, + width: 10, + height: 10, + gap: 10 + } + }; + + var create = function(element, mapDimensions) { + var sheetDef = defs[element.typeName]; + if (sheetDef == null) { + console.error("Cannot render " + element.typeName); + var display = new createjs.Container(); + display.sourceRect = new createjs.Rectangle(0, 0, 0, 0); + return display; + } + var widthRatio = mapDimensions.toCanvasSize(element.block.size) / sheetDef.width; + var heightRatio = mapDimensions.toCanvasSize(element.block.size) / sheetDef.height; + var useScale = Math.min(widthRatio, heightRatio); + var sheetIndex = element.level - 1; + if (sheetDef.levelMultiplier) { + sheetIndex = Math.floor((element.level - 1) * sheetDef.levelMultiplier); + } + var bitmap = new createjs.Bitmap(image); + bitmap.sourceRect = new createjs.Rectangle( + sheetDef.x + sheetIndex * (sheetDef.width + sheetDef.gap) + sheetDef.gap / 2, + sheetDef.y, + sheetDef.width, + sheetDef.height + ); + bitmap.scaleX = useScale; + bitmap.scaleY = useScale; + return bitmap; + }; + + return { + create: create + }; +}; \ No newline at end of file diff --git a/src/main/webapp/js/base-analysis/RedMoonWallSpriteSheet.js b/src/main/webapp/js/base-analysis/RedMoonWallSpriteSheet.js new file mode 100644 index 0000000..767bfcd --- /dev/null +++ b/src/main/webapp/js/base-analysis/RedMoonWallSpriteSheet.js @@ -0,0 +1,34 @@ +"use strict"; + +var RedMoonWallSpriteSheet = function(image) { + // TODO: Nearly repeated in building sprite sheet, sheetIndex only difference + var create = function(element, mapDimensions) { + var sheetDef = { + x: 80, + y: 10 + (element.level - 1) * 52, + width: 14, + height: 26, + gap: 0 + }; + + var widthRatio = mapDimensions.toCanvasSize(element.block.size) / sheetDef.width; + var heightRatio = mapDimensions.toCanvasSize(element.block.size) / sheetDef.height; + var useScale = Math.min(widthRatio, heightRatio); + var sheetIndex = 0; + + var bitmap = new createjs.Bitmap(image); + bitmap.sourceRect = new createjs.Rectangle( + sheetDef.x + sheetIndex * (sheetDef.width + sheetDef.gap) + sheetDef.gap / 2, + sheetDef.y, + sheetDef.width, + sheetDef.height + ); + bitmap.scaleX = useScale; + bitmap.scaleY = useScale; + return bitmap; + }; + + return { + create: create + } +}; \ No newline at end of file diff --git a/src/main/webapp/js/base-analysis/Ui.js b/src/main/webapp/js/base-analysis/Ui.js new file mode 100644 index 0000000..9ccaaf1 --- /dev/null +++ b/src/main/webapp/js/base-analysis/Ui.js @@ -0,0 +1,136 @@ +"use strict"; + +var Ui = function($, model, mapDisplay, window, displaySettings) { + var reportValid = false; + var sizeValid = false; + var activeRuleValid = false; + var panelGroup = $("#results-panel-group"); + + var renderTemplate = function (selector, vars) { + var template = $(selector).html(); + Mustache.parse(template, ['[[', ']]']); + return Mustache.render(template, vars); + }; + + var invalidateReport = function() { + reportValid = false; + render(); + }; + + var invalidateSize = function() { + sizeValid = false; + render(); + }; + + var invalidateRule = function() { + activeRuleValid = false; + render(); + }; + + var renderMapSize = function() { + if (sizeValid) { + return; + } + + var panelGroup = $("#results-panel-group"); + $(mapDisplay.canvas).hide(); + + // Don't know why width - padding is the desired width, found by trial and error + var padding = ($(mapDisplay.canvas).parent().outerWidth() - $(mapDisplay.canvas).parent().width()) / 2; + var canvasSize; + if (Math.abs(panelGroup.offset().top - $(mapDisplay.canvas).parent().offset.top) < 50) { + // Vertical column + canvasSize = $(mapDisplay.canvas).parent().width() - padding; + } else { + // side by side + var proposedBasedOnHeight = $(window.document).height() - $(mapDisplay.canvas).parent().offset().top - 20; + var proposedBasedOnWidth = $(window.document).width() - $(mapDisplay.canvas).parent().offset().left - 40; + canvasSize = Math.min(proposedBasedOnWidth, proposedBasedOnHeight); + } + mapDisplay.canvas.width = canvasSize; + mapDisplay.canvas.height = canvasSize; + $(mapDisplay.canvas).show(); + + sizeValid = true; + }; + + var renderReport = function() { + if (reportValid) { + return; + } + + reportValid = true; + panelGroup.empty(); + if (!model.hasReport()) { + $("#report").hide(); + return; + } + + $("#report").show(); + var panels = _.map( + model.getReport().results, + function (result) { + return $(renderTemplate( + "#result-panel", + { + id: result.code, + title: result.title, + description: result.description, + ruleCode: result.code, + success: result.success + } + )); + } + ); + panelGroup.append(panels); + }; + + var renderRule = function() { + if (activeRuleValid) { + return; + } + + activeRuleValid = true; + var panels = $("#results-panel-group").find(".panel"); + panels.collapse('hide'); + if (model.hasActiveRule()) { + panels.filter("#panel-" + model.getActiveRuleCode()) + .find("[role=tabpanel]") + .collapse('show'); + } + }; + + var render = function() { + renderMapSize(); + renderReport(); + renderRule(); + mapDisplay.render(); + }; + + model.reportChanged.add(_.bind(invalidateReport, this)); + model.ruleChanged.add(_.bind(invalidateRule, this)); + + $(document).ready(function () { + $("#results-panel-group") + .on("show.bs.collapse", function(event) { + model.setActiveRuleByCode($(event.target).data("rule-code")); + }) + .on("hide.bs.collapse", function(event) { + if (model.getActiveRuleCode() == $(event.target).data("rule-code")) { + model.clearActiveRule(); + } + }); + + var showGrid = $("form#display-settings [name=grid]"); + showGrid.prop("checked", displaySettings.getShowGrid()); + showGrid.on("change", function(event) { + displaySettings.setShowGrid($(event.currentTarget).is(":checked")); + }); + }); + + $(window).on("resize", _.bind(invalidateSize, this)); + + return { + render: render + } +}; \ No newline at end of file diff --git a/src/main/webapp/js/base-analysis/main.js b/src/main/webapp/js/base-analysis/main.js new file mode 100644 index 0000000..8fa6adb --- /dev/null +++ b/src/main/webapp/js/base-analysis/main.js @@ -0,0 +1,22 @@ +"use strict"; + +$(document).ready(function() { + // Globals mapConfig, report, document, window + + var model = new Model(); + + var displaySettings = new DisplaySettings(jQuery.jStorage); + var display = new MapDisplay2d(document.getElementById("villageImage"), mapConfig, model, displaySettings); + var ui = new Ui(jQuery, model, display, window, displaySettings); + + var preloader = new Preloader(); + preloader.loadAssets(function(queue) { + display.setAssets({ + "redMoonBuildings": queue.getResult("redMoonBuildings"), + "redMoonWalls": queue.getResult("redMoonWalls") + }); + ui.render(); + }); + + model.setReport(report); +}); \ No newline at end of file diff --git a/src/main/webapp/js/console-polyfill.js b/src/main/webapp/js/console-polyfill.js new file mode 100644 index 0000000..8e651aa --- /dev/null +++ b/src/main/webapp/js/console-polyfill.js @@ -0,0 +1,17 @@ +// Console-polyfill. MIT license. +// https://github.com/paulmillr/console-polyfill +// Make it safe to do console.log() always. +(function(global) { + "use strict"; + global.console = global.console || {}; + var con = global.console; + var prop, method; + var dummy = function() {}; + var properties = ['memory']; + var methods = ('assert,clear,count,debug,dir,dirxml,error,exception,group,' + + 'groupCollapsed,groupEnd,info,log,markTimeline,profile,profiles,profileEnd,' + + 'show,table,time,timeEnd,timeline,timelineEnd,timeStamp,trace,warn').split(','); + while (prop = properties.pop()) if (!con[prop]) con[prop] = {}; + while (method = methods.pop()) if (typeof con[method] !== 'function') con[method] = dummy; + // Using `this` for web workers & supports Browserify / Webpack. +})(typeof window === 'undefined' ? this : window); \ No newline at end of file diff --git a/src/main/webapp/js/war-bases/main.js b/src/main/webapp/js/war-bases/main.js new file mode 100644 index 0000000..43c688c --- /dev/null +++ b/src/main/webapp/js/war-bases/main.js @@ -0,0 +1,177 @@ +"use strict"; + +$(document).ready(function() { + var LOAD_BATCH_SIZE = 3; + var toLoad = players.slice(0); + var loading = []; + var results = []; + + var townHallContainerTemplate = $("#town-hall-container-template").html(); + Mustache.parse(townHallContainerTemplate, ['[[', ']]']); + var resultsContainer = $("#results"); + var problemsContainer = $("#problems"); + var loadingContainer = $("#loading"); + + var addPermanentError = function(player, message) { + results.push({ player: player, error: message }); + loading = _.reject(loading, _.matcher(player)); + loadNext(); + }; + + var addTemporaryError = function(player, message, numAttempts) { + results.push({ player: player, error: message + ", trying again shortly", numAttempts: numAttempts }); + loading = _.reject(loading, _.matcher(player)); + toLoad.push(player); + loadNext(); + }; + + var addResult = function(player, report) { + results.push({ player: player, report: report }); + loading = _.reject(loading, _.matcher(player)); + loadNext(); + }; + + var performLoadPlayer = function(player, attemptNum) { + loading.push(player); + jQuery.getJSON(player.analysisSummaryUrl) + .done(function (report) { + addResult(player, report); + }) + .fail(function (response) { + if (response.status == 404 || response.status == 400) { + addPermanentError(player, response.responseJSON); + return; + } + + if (response.status == 503) { + addTemporaryError(player, "Game Servers connection not available", attemptNum); + return; + } + + addTemporaryError(player, 'Unknown error encountered', attemptNum); + }); + }; + + var loadPlayer = function(player, attemptNum) { + var timeout = Math.pow(attemptNum - 1, 1.5) * 1000; + if (timeout == 0) { + performLoadPlayer(player, 1); + return; + } + + console.log("Loading", player.ign, "with timeout " + timeout); + setTimeout(_.partial(performLoadPlayer, player, attemptNum), timeout); + }; + + var loadNext = function() { + while (toLoad.length > 0 && loading.length < LOAD_BATCH_SIZE) { + var next = _.head(toLoad); + toLoad = _.tail(toLoad); + + var attemptNum = 1; + var report = _.find(results, function(result) { return result.player == next; }); + if (report != null && report.numAttempts) { + attemptNum = report.numAttempts + 1; + } + loadPlayer(next, attemptNum); + } + + render(); + }; + + var renderProblem = function(result) { + problemsContainer.removeClass("hidden").show(); + var problemId = "problem-" + result.player.id; + if (problemsContainer.find("#" + problemId).size() == 0) { + problemsContainer.append( + $("
").attr("id", problemId) + .html(result.player.ign + ": " + result.error) + ); + } + }; + + var ensureTownHallContainerRendered = function(result) { + var townHallContainerId = "town-hall-table-" + result.report.townHallLevel; + var townHallContainer = $("#" + townHallContainerId); + if (townHallContainer.size() == 0) { + townHallContainer = $(Mustache.render(townHallContainerTemplate, { + "containerId": townHallContainerId, + "level": result.report.townHallLevel, + "rules": _.pluck(result.report.resultSummaries, 'shortName') + })); + resultsContainer.append(townHallContainer); + + new Clipboard(townHallContainer.find("[data-clipboard-target]").get(0)); + } + return townHallContainer; + }; + + var createResultRow = function(rowId, result, anyError) { + var ruleOrder = _.map( + resultsContainer.find("table thead th.result-col"), + function(col) { return $(col).data("rule"); } + ); + + return $("").attr("id", rowId) + .addClass(anyError ? 'danger' : '') + .append($("").append(result.player.ign)) + .append($("").append($("").attr("href", result.player.analysisUrl).attr("target", "_blank").html('Go to result'))) + .append( + _.map( + _.sortBy(result.report.resultSummaries, function(summary) { return ruleOrder.indexOf(summary.shortName); }), + function(summary) { + return $("").append(summary.success ? '' : $('').addClass('glyphicon glyphicon-remove-sign')); + } + ) + ) + .append($("").append(" " + result.report.connectionTime + " | " + result.report.analysisTime)); + }; + + var render = function() { + // Loading + if (loading.length == 0) { + loadingContainer.hide(); + } else { + loadingContainer.show(); + loadingContainer.find(".loading-names").html(_.pluck(loading, 'ign').join(", ")); + if (toLoad.length > 0) { + loadingContainer.find(".queued-button").show(); + loadingContainer.find(".queued-count").html(toLoad.length); + loadingContainer.find(".queued-names").html(_.pluck(toLoad, 'ign').join(', ')); + } else { + loadingContainer.find(".queued-button").hide(); + } + } + + // Table Results + _.each( + results, + function(result) { + var rowId = "player-row-" + result.player.id; + if ($("#" + rowId).size() > 0) { + return; + } + + if (result.report == null) { + renderProblem(result); + return; + } + + var townHallContainer = ensureTownHallContainerRendered(result); + + var anyError = _.some(result.report.resultSummaries, function(summary) { return !summary.success; }); + createResultRow(rowId, result, anyError) + .appendTo(townHallContainer.find("tbody")) + .slideDown(); + + if (anyError) { + townHallContainer.find(".plain-text-summary") + .find("textarea") + .append(result.player.ign + ": " + _.pluck(result.report.resultSummaries, 'shortName').join(', ') + "\n"); + } + } + ); + }; + + loadNext(); +}); \ No newline at end of file diff --git a/src/test/scala/org/danielholmes/coc/baseanalyser/analysis/AirSnipedDefenseRuleSpec.scala b/src/test/scala/org/danielholmes/coc/baseanalyser/analysis/AirSnipedDefenseRuleSpec.scala new file mode 100644 index 0000000..faa52b5 --- /dev/null +++ b/src/test/scala/org/danielholmes/coc/baseanalyser/analysis/AirSnipedDefenseRuleSpec.scala @@ -0,0 +1,39 @@ +package org.danielholmes.coc.baseanalyser.analysis + +import org.danielholmes.coc.baseanalyser.model._ +import org.danielholmes.coc.baseanalyser.model.defense.{AirDefense, AirSweeper, Cannon} +import org.danielholmes.coc.baseanalyser.model.trash.BuilderHut +import org.danielholmes.coc.baseanalyser.model.troops.{Minion, MinionAttackPosition} +import org.scalatest._ + +class AirSnipedDefenseRuleSpec extends FlatSpec with Matchers { + val rule = new AirSnipedDefenseRule + + "AirSnipedDefenseRule" should "return no violation for base without defenses" in { + rule.analyse(Village.empty).success should be (true) + } + + it should "return sniped ground when no air def" in { + val cannon = Cannon(1, Tile.MapOrigin) + val result = rule.analyse(Village(Set(cannon))).asInstanceOf[AirSnipedDefenseRuleResult] + result.success should be (false) + result.snipedDefenses.size should be (1) + result.snipedDefenses.map(_.targeting) should be (Set(cannon)) + } + + it should "return no sniped ground when air def" in { + val airDef = AirDefense(1, Tile(6, 6)) + rule.analyse(Village(Set(Cannon(1, Tile.MapOrigin), airDef))) should be (AirSnipedDefenseRuleResult( + Set.empty, + Set(airDef) + )) + } + + it should "return no sniped ground when air sweeper" in { + val airSweeper = AirSweeper(1, Tile(10, 10), Angle.Quarter * 7) + rule.analyse(Village(Set(BuilderHut(Tile(4, 4)), airSweeper))) should be (AirSnipedDefenseRuleResult( + Set.empty, + Set(airSweeper) + )) + } +} diff --git a/src/test/scala/org/danielholmes/coc/baseanalyser/analysis/ArcherAnchorRuleSpec.scala b/src/test/scala/org/danielholmes/coc/baseanalyser/analysis/ArcherAnchorRuleSpec.scala new file mode 100644 index 0000000..2caae44 --- /dev/null +++ b/src/test/scala/org/danielholmes/coc/baseanalyser/analysis/ArcherAnchorRuleSpec.scala @@ -0,0 +1,55 @@ +package org.danielholmes.coc.baseanalyser.analysis + +import org.danielholmes.coc.baseanalyser.model._ +import org.danielholmes.coc.baseanalyser.model.defense.{AirDefense, ArcherTower, Cannon} +import org.danielholmes.coc.baseanalyser.model.trash.BuilderHut +import org.scalatest._ + +class ArcherAnchorRuleSpec extends FlatSpec with Matchers { + val rule = new ArcherAnchorRule + + "ArcherAnchorRule" should "return no violation for base without element" in { + rule.analyse(Village.empty).success should be (true) + } + + it should "return violation for base with element and no defenses" in { + rule.analyse(Village(Set(BuilderHut(Tile.MapOrigin)))).success should be (false) + } + + it should "return success for base with element covered by ground shooting" in { + val village = Village( + Set( + BuilderHut(Tile(5, 5)), + ArcherTower(1, Tile(7, 7)) + ) + ) + rule.analyse(village).success should be (true) + } + + it should "return fail for base with element covered by air shooting" in { + val village = Village( + Set( + BuilderHut(Tile(5, 5)), + AirDefense(1, Tile(7, 7)) + ) + ) + rule.analyse(village).success should be (false) + } + + it should "return success for base with wall not covered by ground shooting" in { + val village = Village( + Set( + Wall(1, Tile(5, 5)), + ArcherTower(1, Tile(30, 30)) + ) + ) + rule.analyse(village).success should be (true) + } + + it should "return defenses that can hit archer" in { + val at = ArcherTower(1, Tile(30, 30)) + val cannon = Cannon(1, Tile(36, 36)) + val village = Village(Set(at, AirDefense(1, Tile(33, 33)), cannon)) + rule.analyse(village).asInstanceOf[ArcherAnchorRuleResult].aimingDefenses should be (Set(at, cannon)) + } +} diff --git a/src/test/scala/org/danielholmes/coc/baseanalyser/analysis/BKSwappableRuleSpec.scala b/src/test/scala/org/danielholmes/coc/baseanalyser/analysis/BKSwappableRuleSpec.scala new file mode 100644 index 0000000..207834c --- /dev/null +++ b/src/test/scala/org/danielholmes/coc/baseanalyser/analysis/BKSwappableRuleSpec.scala @@ -0,0 +1,97 @@ +package org.danielholmes.coc.baseanalyser.analysis + +import org.danielholmes.coc.baseanalyser.model._ +import org.danielholmes.coc.baseanalyser.model.heroes.BarbarianKingAltar +import org.danielholmes.coc.baseanalyser.model.trash.{ArmyCamp, Barrack} +import org.danielholmes.coc.baseanalyser.stringdisplay.StringDisplayer +import org.danielholmes.coc.baseanalyser.util.ElementsBuilder +import org.scalatest._ + +class BKSwappableRuleSpec extends FlatSpec with Matchers { + val rule = new BKSwappableRule + + "BKSwappableRule" should "return success for empty village" in { + rule.analyse(Village.empty) should be (BKSwappableRuleResult(Set.empty)) + } + + it should "return fail for BK on his own" in { + rule.analyse(Village(Set(BarbarianKingAltar(1, Tile(1, 1))))).success should be (false) + } + + it should "return success for deep walled BK" in { + val elements = + ElementsBuilder.elementFence(Tile(9, 9), 27, 27) ++ + ElementsBuilder.repeatX(Tile(10, 10), 5, 5, ArmyCamp(1, _)) ++ + ElementsBuilder.repeatX(Tile(10, 15), 5, 5, ArmyCamp(1, _)) ++ + Set[Element]( + ArmyCamp(1, Tile(10, 20)), ArmyCamp(1, Tile(15, 20)), BarbarianKingAltar(1, Tile(21, 21)), ArmyCamp(1, Tile(25, 20)), ArmyCamp(1, Tile(30, 20)) + ) ++ + ElementsBuilder.repeatX(Tile(10, 25), 5, 5, ArmyCamp(1, _)) ++ + ElementsBuilder.repeatX(Tile(10, 30), 5, 5, ArmyCamp(1, _)) + rule.analyse(Village(elements)).success should be (true) + } + + it should "return fail for exposed BK" in { + val elements = + ElementsBuilder.elementFence(Tile(9, 9), 27, 27) ++ + ElementsBuilder.repeatX(Tile(10, 10), 5, 5, ArmyCamp(1, _)) ++ + ElementsBuilder.repeatX(Tile(10, 15), 5, 5, ArmyCamp(1, _)) ++ + Set[Element]( + ArmyCamp(1, Tile(10, 20)), ArmyCamp(1, Tile(15, 20)), ArmyCamp(1, Tile(20, 20)), Barrack(1, Tile(26, 21)), + BarbarianKingAltar(1, Tile(29, 21)), Barrack(1, Tile(32, 21)) + ) ++ + ElementsBuilder.repeatX(Tile(10, 25), 5, 5, ArmyCamp(1, _)) ++ + ElementsBuilder.repeatX(Tile(10, 30), 5, 5, ArmyCamp(1, _)) + + val result = rule.analyse(Village(elements)) + result.success should be (false) + } + + it should "return success for very deep non-walled BK" in { + val elements = + ElementsBuilder.repeatX(Tile(5, 5), 7, 5, ArmyCamp(1, _)) ++ + ElementsBuilder.repeatX(Tile(5, 10), 7, 5, ArmyCamp(1, _)) ++ + ElementsBuilder.repeatX(Tile(5, 15), 7, 5, ArmyCamp(1, _)) ++ + Set[Element]( + ArmyCamp(1, Tile(5, 20)), ArmyCamp(1, Tile(10, 20)), ArmyCamp(1, Tile(15, 20)), + BarbarianKingAltar(1, Tile(21, 21)), + ArmyCamp(1, Tile(25, 20)), ArmyCamp(1, Tile(30, 20)), ArmyCamp(1, Tile(35, 20)) + ) ++ + ElementsBuilder.repeatX(Tile(5, 25), 7, 5, ArmyCamp(1, _)) ++ + ElementsBuilder.repeatX(Tile(5, 30), 7, 5, ArmyCamp(1, _)) ++ + ElementsBuilder.repeatX(Tile(5, 35), 7, 5, ArmyCamp(1, _)) + val result = rule.analyse(Village(elements)) + result.success should be (true) + } + + it should "return success for only slightly exposed BK" in { + val elements = + ElementsBuilder.elementFence(Tile(4, 4), 15, 15) ++ + ElementsBuilder.repeatX(Tile(6, 6), 3, 4, Barrack(1, _)) ++ + Set[Element]( + Barrack(1, Tile(6, 10)), + BarbarianKingAltar(1, Tile(10, 10)), + Barrack(1, Tile(14, 10)) + ) ++ + ElementsBuilder.repeatX(Tile(6, 14), 3, 4, Barrack(1, _)) + val result = rule.analyse(Village(elements)) + result.success should be (true) + } + + it should "return correct exposed tiles for offset BK" in { + val elements = + ElementsBuilder.elementFence(Tile(4, 4), 14, 15) ++ + ElementsBuilder.repeatX(Tile(6, 6), 3, 4, Barrack(1, _)) ++ + Set[Element]( + Barrack(1, Tile(6, 10)), + BarbarianKingAltar(1, Tile(10, 10)), + Barrack(1, Tile(14, 10)) + ) ++ + ElementsBuilder.repeatX(Tile(6, 14), 3, 4, Barrack(1, _)) + + val result = rule.analyse(Village(elements)) + result.success should be (false) + result.asInstanceOf[BKSwappableRuleResult].exposedTiles should contain (Tile(18, 11)) + result.asInstanceOf[BKSwappableRuleResult].exposedTiles should not contain (Tile(3, 11)) + } +} diff --git a/src/test/scala/org/danielholmes/coc/baseanalyser/analysis/EnoughPossibleTrapLocationsRuleSpec.scala b/src/test/scala/org/danielholmes/coc/baseanalyser/analysis/EnoughPossibleTrapLocationsRuleSpec.scala new file mode 100644 index 0000000..042d211 --- /dev/null +++ b/src/test/scala/org/danielholmes/coc/baseanalyser/analysis/EnoughPossibleTrapLocationsRuleSpec.scala @@ -0,0 +1,95 @@ +package org.danielholmes.coc.baseanalyser.analysis + +import org.danielholmes.coc.baseanalyser.model._ +import org.danielholmes.coc.baseanalyser.model.defense.HiddenTesla +import org.danielholmes.coc.baseanalyser.model.traps.GiantBomb +import org.danielholmes.coc.baseanalyser.util.ElementsBuilder +import org.scalactic.anyvals.{PosZDouble, PosZInt} +import org.scalatest._ + +class EnoughPossibleTrapLocationsRuleSpec extends FlatSpec with Matchers { + val rule = new EnoughPossibleTrapLocationsRule + + "EnoughPossibleTrapLocationsRule" should "return violation for base without wall compartments" in { + rule.analyse(Village.empty).success should be (false) + } + + it should "return no violation for base with large wall compartments" in { + val walls = ElementsBuilder.elementFence(Tile.MapOrigin, 40, 40) + rule.analyse(Village(walls)).success should be (true) + } + + it should "return violation if only 20 possibilities" in { + val walls = Range.inclusive(1, 4) + .flatMap(row => + Range.inclusive(1, 5) + .map(col => Tile(PosZInt.from(col * 4).get, PosZInt.from(row * 4).get)) + .flatMap(ElementsBuilder.elementFence(_, 4, 4)) + ) + .toSet + val village = Village(walls) + assert(village.possibleInternalLargeTraps.size == 20) + + rule.analyse(village).success should be (false) + } + + it should "return no violation if 24 possibilities" in { + val elements = create24CompartmentsHolding(t => None) + val village = Village(elements) + assert(village.possibleInternalLargeTraps.size == 24) + + rule.analyse(village).success should be (true) + } + + it should "return no violation if 24 possibilities with real Teslas" in { + val elements = create24CompartmentsHolding(t => Some(HiddenTesla(1, t))) + val village = Village(elements) + assert(village.possibleInternalLargeTraps.size == 24) + + rule.analyse(village).success should be (true) + } + + it should "return no violation if 24 possibilities with real Giant Bomb" in { + val elements = create24CompartmentsHolding(t => Some(GiantBomb(1, t))) + val village = Village(elements) + assert(village.possibleInternalLargeTraps.size == 24) + + rule.analyse(village).success should be (true) + } + + it should "return violation if 24 possibilities with Decoration" in { + val elements = create24CompartmentsHolding(t => Some(Decoration(t))) + val village = Village(elements) + assert(village.possibleInternalLargeTraps.isEmpty) + + rule.analyse(village).success should be (false) + } + + it should "allocate an equal score for 2 separate 2x2s as 1 3x3" in { + val villageSeparate2x2 = Village(ElementsBuilder.elementFence(Tile(10, 10), 4, 4) ++ ElementsBuilder.wallFence(Tile(15, 15), 4, 4)) + val village3x3 = Village(ElementsBuilder.elementFence(Tile(10, 10), 5, 5)) + + rule.calculateScore(village3x3) should be (rule.calculateScore(villageSeparate2x2)) + } + + it should "give a higher score for 2 separate 2x2s then 1 3x2" in { + val villageSeparate2x2 = Village(ElementsBuilder.elementFence(Tile(10, 10), 4, 4) ++ ElementsBuilder.wallFence(Tile(15, 15), 4, 4)) + val village3x2 = Village(ElementsBuilder.elementFence(Tile(10, 10), 5, 4)) + + rule.calculateScore(village3x2) should be < rule.calculateScore(villageSeparate2x2) + } + + it should "single trap should score 1.0" in { + rule.calculateScore(Village(ElementsBuilder.elementFence(Tile(10, 10), 4, 4))) should be (PosZDouble(1.0)) + } + + private def create24CompartmentsHolding(factory: Tile => Option[Element]): Set[Element] = { + Range.inclusive(1, 4) + .flatMap(row => + Range.inclusive(1, 6) + .map(col => Tile(PosZInt.from(col * 4).get, PosZInt.from(row * 4).get)) + .flatMap(t => ElementsBuilder.elementFence(t, 4, 4) ++ factory.apply(t.offset(1, 1))) + ) + .toSet + } +} diff --git a/src/test/scala/org/danielholmes/coc/baseanalyser/analysis/HighHPUnderAirDefRuleSpec.scala b/src/test/scala/org/danielholmes/coc/baseanalyser/analysis/HighHPUnderAirDefRuleSpec.scala new file mode 100644 index 0000000..03372f8 --- /dev/null +++ b/src/test/scala/org/danielholmes/coc/baseanalyser/analysis/HighHPUnderAirDefRuleSpec.scala @@ -0,0 +1,43 @@ +package org.danielholmes.coc.baseanalyser.analysis + +import org.danielholmes.coc.baseanalyser.model._ +import org.danielholmes.coc.baseanalyser.model.defense.AirDefense +import org.danielholmes.coc.baseanalyser.model.trash.GoldStorage +import org.scalatest._ + +class HighHPUnderAirDefRuleSpec extends FlatSpec with Matchers { + val rule = new HighHPUnderAirDefRule + + "HighHPUnderAirDefRule" should "return no violation for base without air def" in { + rule.analyse(Village.empty).success should be (true) + } + + it should "return pass for base with air def and no storages" in { + rule.analyse(Village(Set(AirDefense(1, Tile.MapOrigin)))).success should be (true) + } + + it should "return fail for base with air def and storage outside" in { + val storage = GoldStorage(1, Tile(30, 30)) + rule.analyse(Village(Set(AirDefense(1, Tile.MapOrigin), storage))) should be (HighHPUnderAirDefRuleResult(Set(storage), Set.empty)) + } + + it should "return fail for base with air def cutting storage" in { + val storageOutside = GoldStorage(1, Tile(7, 11)) + val storageInside = GoldStorage(1, Tile(7, 5)) + rule.analyse(Village(Set(AirDefense(1, Tile.MapOrigin), storageOutside, storageInside))) should be + HighHPUnderAirDefRuleResult(Set(storageOutside), Set(storageInside)) + } + + it should "return true for base with air def just covering storage" in { + rule.analyse(Village(Set(AirDefense(1, Tile.MapOrigin), GoldStorage(1, Tile(5, 7))))).success should be (true) + } + + it should "return success for base with storage requiring 2 air defs for full coverage" in { + val village = Village(Set( + AirDefense(1, Tile(5, 5)), + GoldStorage(1, Tile(14, 5)), + AirDefense(1, Tile(23, 5)) + )) + rule.analyse(village).success should be (true) + } +} diff --git a/src/test/scala/org/danielholmes/coc/baseanalyser/analysis/HogCCLureRuleSpec.scala b/src/test/scala/org/danielholmes/coc/baseanalyser/analysis/HogCCLureRuleSpec.scala new file mode 100644 index 0000000..fb9c5dc --- /dev/null +++ b/src/test/scala/org/danielholmes/coc/baseanalyser/analysis/HogCCLureRuleSpec.scala @@ -0,0 +1,31 @@ +package org.danielholmes.coc.baseanalyser.analysis + +import org.danielholmes.coc.baseanalyser.model._ +import org.danielholmes.coc.baseanalyser.model.special.ClanCastle +import org.danielholmes.coc.baseanalyser.model.trash.Barrack +import org.danielholmes.coc.baseanalyser.model.troops.HogRider +import org.scalatest._ + +class HogCCLureRuleSpec extends FlatSpec with Matchers { + val rule = new HogCCLureRule + + "HogCCLureRule" should "return no violation for base without CC" in { + rule.analyse(Village.empty).success should be (true) + } + + it should "return violation for base with CC and no blocking" in { + rule.analyse(Village(Set(ClanCastle(1, Tile.MapOrigin)))).success should be (false) + } + + it should "return success for base with paths blocked off" in { + val ccPosition = Tile(20, 20) + val cc = ClanCastle(1, ccPosition) + val village = Village( + Set(cc) ++ + Tile(2, 2).matrixOfTilesTo(Tile(41, 41), 3) + .filter(_ != ccPosition) + .map(Barrack(1, _)) + ) + rule.analyse(village).success should be (true) + } +} diff --git a/src/test/scala/org/danielholmes/coc/baseanalyser/analysis/MinimumCompartmentsRuleSpec.scala b/src/test/scala/org/danielholmes/coc/baseanalyser/analysis/MinimumCompartmentsRuleSpec.scala new file mode 100644 index 0000000..235caa3 --- /dev/null +++ b/src/test/scala/org/danielholmes/coc/baseanalyser/analysis/MinimumCompartmentsRuleSpec.scala @@ -0,0 +1,37 @@ +package org.danielholmes.coc.baseanalyser.analysis + +import org.danielholmes.coc.baseanalyser.model._ +import org.danielholmes.coc.baseanalyser.model.trash.BuilderHut +import org.danielholmes.coc.baseanalyser.util.ElementsBuilder +import org.scalactic.anyvals.{PosInt, PosZInt} +import org.scalatest._ + +class MinimumCompartmentsRuleSpec extends FlatSpec with Matchers { + val rule = new MinimumCompartmentsRule + + "MinimumCompartmentsRule" should "return violation for empty village" in { + rule.analyse(Village.empty) should be (MinimumCompartmentsRuleResult(8, Set.empty)) + } + + it should "return violation for 1 compartment" in { + rule.analyse(Village(ElementsBuilder.elementFence(Tile(1, 1), 3, 3))) should be + (MinimumCompartmentsRuleResult(8, Set.empty)) + } + + it should "return no violation for 8 compartments" in { + val elements = Range(0, 8) + .map(x => PosZInt.from(x * 4).get) + .map(Tile.MapOrigin.offset(_, 1)) + .flatMap(t => ElementsBuilder.elementFence(t, 4, 4) + BuilderHut(t.offset(1, 1))) + .toSet + rule.analyse(Village(elements)).success should be (true) + } + + it should "return violation for 8 empty compartments" in { + val elements = Range.inclusive(1, 22, 3) + .map(PosInt.from(_).get) + .flatMap(x => ElementsBuilder.elementFence(Tile(x, 1), 3, 3)) + .toSet + rule.analyse(Village(elements)).success should be (false) + } +} diff --git a/src/test/scala/org/danielholmes/coc/baseanalyser/analysis/QueenWalkedAirDefenseRuleSpec.scala b/src/test/scala/org/danielholmes/coc/baseanalyser/analysis/QueenWalkedAirDefenseRuleSpec.scala new file mode 100644 index 0000000..19cbeb2 --- /dev/null +++ b/src/test/scala/org/danielholmes/coc/baseanalyser/analysis/QueenWalkedAirDefenseRuleSpec.scala @@ -0,0 +1,45 @@ +package org.danielholmes.coc.baseanalyser.analysis + +import org.danielholmes.coc.baseanalyser.model._ +import org.danielholmes.coc.baseanalyser.model.defense.AirDefense +import org.danielholmes.coc.baseanalyser.model.trash.Barrack +import org.danielholmes.coc.baseanalyser.util.ElementsBuilder +import org.scalatest._ + +class QueenWalkedAirDefenseRuleSpec extends FlatSpec with Matchers { + val rule = new QueenWalkedAirDefenseRule + + "QueenWalkedAirDefenseRuleSpec" should "return no violation for empty village" in { + rule.analyse(Village.empty).success should be (true) + } + + it should "return violation for non-walled air defense" in { + val ad = AirDefense(1, Tile(1, 1)) + val result = rule.analyse(Village(Set(ad))).asInstanceOf[QueenWalkedAirDefenseRuleResult] + result.success should be (false) + result.attackings.head.targeting should be (ad) + result.nonReachableAirDefs should be (empty) + } + + it should "return success for deep-walled air defense" in { + val ad = AirDefense(1, Tile(15, 15)) + val elements: Set[Element] = ElementsBuilder.elementFence(Tile(10, 10), 13, 13) ++ + ElementsBuilder.rectangle(Tile(12, 12), 3, 3, 3, Barrack(1, _)) ++ + Set(ad) + val result = rule.analyse(Village(elements)).asInstanceOf[QueenWalkedAirDefenseRuleResult] + result.success should be (true) + result.attackings should be (empty) + result.nonReachableAirDefs should contain (ad) + } + + it should "return fail for shallow-walled air defense" in { + val ad = AirDefense(1, Tile(15, 15)) + val elements: Set[Element] = ElementsBuilder.elementFence(Tile(11, 11), 11, 11) ++ + ElementsBuilder.rectangle(Tile(12, 12), 3, 3, 3, Barrack(1, _)) ++ + Set(ad) + val result = rule.analyse(Village(elements)).asInstanceOf[QueenWalkedAirDefenseRuleResult] + result.success should be (false) + result.attackings.head.targeting should be (ad) + result.nonReachableAirDefs should be (empty) + } +} diff --git a/src/test/scala/org/danielholmes/coc/baseanalyser/analysis/QueenWontLeaveCompartmentRuleSpec.scala b/src/test/scala/org/danielholmes/coc/baseanalyser/analysis/QueenWontLeaveCompartmentRuleSpec.scala new file mode 100644 index 0000000..0db2bdc --- /dev/null +++ b/src/test/scala/org/danielholmes/coc/baseanalyser/analysis/QueenWontLeaveCompartmentRuleSpec.scala @@ -0,0 +1,36 @@ +package org.danielholmes.coc.baseanalyser.analysis + +import org.danielholmes.coc.baseanalyser.model._ +import org.danielholmes.coc.baseanalyser.model.heroes.ArcherQueenAltar +import org.danielholmes.coc.baseanalyser.stringdisplay.StringDisplayer +import org.danielholmes.coc.baseanalyser.util.ElementsBuilder +import org.scalatest._ + +class QueenWontLeaveCompartmentRuleSpec extends FlatSpec with Matchers { + val rule = new QueenWontLeaveCompartmentRule + + "QueenWontLeaveCompartmentRule" should "return no violation for base without element" in { + rule.analyse(Village.empty).success should be (true) + } + + it should "return no violation for base with walls but without queen" in { + val walls = ElementsBuilder.elementFence(Tile(10, 10), 5, 5) + rule.analyse(Village(walls)).success should be (true) + } + + it should "return violation for base with queen but no compartments" in { + rule.analyse(Village(Set(ArcherQueenAltar(1, Tile(1, 1))))).success should be (false) + } + + it should "return no violation for base with queen inside 9x9" in { + val aq = ArcherQueenAltar(1, Tile(13, 13)) + val walls = ElementsBuilder.elementFence(Tile(9, 9), 11, 11) + rule.analyse(Village(walls + aq)).success should be (true) + } + + it should "return violation for base with queen inside 7x7" in { + val aq = ArcherQueenAltar(1, Tile(13, 13)) + val walls = ElementsBuilder.elementFence(Tile(10, 10), 9, 9) + rule.analyse(Village(walls + aq)).success should be (false) + } +} diff --git a/src/test/scala/org/danielholmes/coc/baseanalyser/analysis/WizardTowersOutOfHoundPositionsRuleSpec.scala b/src/test/scala/org/danielholmes/coc/baseanalyser/analysis/WizardTowersOutOfHoundPositionsRuleSpec.scala new file mode 100644 index 0000000..57ea8fe --- /dev/null +++ b/src/test/scala/org/danielholmes/coc/baseanalyser/analysis/WizardTowersOutOfHoundPositionsRuleSpec.scala @@ -0,0 +1,64 @@ +package org.danielholmes.coc.baseanalyser.analysis + +import org.danielholmes.coc.baseanalyser.model._ +import org.danielholmes.coc.baseanalyser.model.defense.{AirDefense, WizardTower} +import org.danielholmes.coc.baseanalyser.model.troops.WizardTowerHoundTargeting +import org.danielholmes.coc.baseanalyser.util.ElementsBuilder +import org.scalatest._ + +class WizardTowersOutOfHoundPositionsRuleSpec extends FlatSpec with Matchers { + val rule = new WizardTowersOutOfHoundPositionsRule + + "WizardTowersOutOfHoundPositionsRule" should "return success for no air defs" in { + val wt = WizardTower(1, Tile(1, 1)) + val result = rule.analyse(Village(Set(wt))) + result.success should be (true) + result should be (WizardTowersOutOfHoundPositionsRuleResult(Set(wt), Set.empty)) + } + + it should "return success for no wiz towers" in { + val ad = AirDefense(1, Tile(1, 1)) + val result = rule.analyse(Village(Set(ad))) + result.success should be (true) + result should be (WizardTowersOutOfHoundPositionsRuleResult(Set.empty, Set.empty)) + } + + it should "return fail for wt in range of air def" in { + val wt = WizardTower(1, Tile(4, 4)) + val ad = AirDefense(1, Tile(1, 1)) + val result = rule.analyse(Village(Set(ad, wt))) + result.success should be (false) + result should be (WizardTowersOutOfHoundPositionsRuleResult(Set.empty, Set(WizardTowerHoundTargeting(wt, ad, ad.block.contractBy(1))))) + } + + it should "return succeed for wt out of range of air def" in { + val wt = WizardTower(1, Tile(20, 4)) + val ad = AirDefense(1, Tile(1, 1)) + val result = rule.analyse(Village(Set(ad, wt))) + result.success should be (true) + result should be (WizardTowersOutOfHoundPositionsRuleResult(Set(wt), Set.empty)) + } + + it should "return success for half wts in range of air def" in { + val wtInRange = WizardTower(1, Tile(4, 4)) + val wtOutRange = WizardTower(1, Tile(35, 35)) + val ad = AirDefense(1, Tile(1, 1)) + val result = rule.analyse(Village(Set(ad, wtInRange, wtOutRange))) + result.success should be (true) + result should be (WizardTowersOutOfHoundPositionsRuleResult(Set(wtOutRange), Set(WizardTowerHoundTargeting(wtInRange, ad, ad.block.contractBy(1))))) + } + + it should "count a WT in range of 2 air defs only once" in { + val wtInRange = WizardTower(1, Tile(4, 4)) + val wtOutRange = WizardTower(1, Tile(35, 35)) + val ad1 = AirDefense(1, Tile(1, 1)) + val ad2 = AirDefense(1, Tile(1, 4)) + val result = rule.analyse(Village(Set(ad1, ad2, wtInRange, wtOutRange))) + result.success should be (true) + result should be (WizardTowersOutOfHoundPositionsRuleResult( + Set(wtOutRange), + Set(WizardTowerHoundTargeting(wtInRange, ad1, ad1.block.contractBy(1)), + WizardTowerHoundTargeting(wtInRange, ad2, ad2.block.contractBy(1)))) + ) + } +} diff --git a/src/test/scala/org/danielholmes/coc/baseanalyser/baseparser/HardCodedElementFactorySpec.scala b/src/test/scala/org/danielholmes/coc/baseanalyser/baseparser/HardCodedElementFactorySpec.scala new file mode 100644 index 0000000..c654937 --- /dev/null +++ b/src/test/scala/org/danielholmes/coc/baseanalyser/baseparser/HardCodedElementFactorySpec.scala @@ -0,0 +1,36 @@ +package org.danielholmes.coc.baseanalyser.baseparser + +import org.danielholmes.coc.baseanalyser.model._ +import org.danielholmes.coc.baseanalyser.model.defense.{AirSweeper, Cannon} +import org.danielholmes.coc.baseanalyser.model.special.TownHall +import org.scalatest._ + +class HardCodedElementFactorySpec extends FlatSpec with Matchers { + val factory = new HardCodedElementFactory + + "Hardcoded Building Factory" should "reject unknown code" in { + a[RuntimeException] should be thrownBy { + factory.build(RawElement(-1, 9, 1, 2)) + } + } + + it should "create town hall" in { + factory.build(RawElement(1000001, 9, 3, 3)) should contain (TownHall(10, Tile(3, 3))) + } + + it should "create construction buildings as level 1" in { + factory.build(RawElement(1000008, -1, 3, 4)) should contain (Cannon(1, Tile(3, 4))) + } + + it should "ignore obstacles" in { + factory.build(RawElement(8000000, 9, 5, 6)) should be (None) + } + + it should "ignore decorations" in { + factory.build(RawElement(18000000, 9, 5, 6)) should be (None) + } + + it should "parse correct air sweeper" in { + factory.build(RawElement(1000028, 4, 5, 6, Some(45))) should contain (AirSweeper(5, Tile(5, 6), Angle.degrees(45))) + } +} diff --git a/src/test/scala/org/danielholmes/coc/baseanalyser/baseparser/VillageJsonParserSpec.scala b/src/test/scala/org/danielholmes/coc/baseanalyser/baseparser/VillageJsonParserSpec.scala new file mode 100644 index 0000000..2358ca2 --- /dev/null +++ b/src/test/scala/org/danielholmes/coc/baseanalyser/baseparser/VillageJsonParserSpec.scala @@ -0,0 +1,82 @@ +package org.danielholmes.coc.baseanalyser.baseparser + +import org.danielholmes.coc.baseanalyser.model._ +import org.scalatest._ +import org.scalactic.anyvals.{PosInt, PosZInt} + +class VillageJsonParserSpec extends FlatSpec with Matchers { + val buildingFactory = StubElementFactory + + val parser = new VillageJsonParser(buildingFactory) + + "A Village JSON Parser" should "throw an exception if invalid json provided" in { + a[InvalidJsonException] should be thrownBy { + parser.parse("something random[ {") + } + } + + it should "return empty village is empty input" in { + val villages = parser.parse("""{"war_base": false, "buildings":[]}""") + + villages should be (Villages(Village.empty, None)) + } + + it should "return simple village" in { + val result = parser.parse("""{"exp_ver":1, "war_base": false, "buildings":[{ "data": 1000001, "lvl": 1, "x": 21, "y": 20 }]}""") + + result should be (Villages(Village(Set(StubBaseElement(1, Tile(21, 20), None))), None)) + } + + it should "return village without ignored elements" in { + val result = parser.parse("""{"exp_ver":1, "war_base": false, "buildings":[{ "data": 999999, "lvl": 1, "x": 21, "y": 20 }]}""") + + result should be (Villages(Village.empty, None)) + } + + it should "return war village" in { + val result = parser.parse( + """{"exp_ver":1, "war_layout": 4,"war_base": true, + |"buildings":[{ "data": 1000001, "lvl": 1, "x": 20, "y": 20, "l4x": 30, "l4y": 30 }]}""".stripMargin) + + result should be (Villages( + Village(Set(StubBaseElement(1, Tile(20, 20), None))), + Some(Village(Set(StubBaseElement(1, Tile(30, 30), None)))) + )) + } + + it should "return war element aim angle" in { + val result = parser.parse( + """{"exp_ver":1, "war_layout": 4,"war_base": true, + |"buildings":[{ "data": 1000029, "lvl": 1, "x": 20, "y": 20, "l4x": 30, "l4y": 30, "aim_angle": 90, "aim_angle_war": 45 }]}""".stripMargin) + + result should be (Villages( + Village(Set(StubBaseElement(1, Tile(20, 20), Some(90)))), + Some(Village(Set(StubBaseElement(1, Tile(30, 30), Some(45))))) + )) + } + + it should "return war village without not yet placed buildings" in { + val result = parser.parse( + """{"exp_ver":1, "war_layout": 4,"war_base": true, + |"buildings":[ + |{ "data": 1000001, "lvl": 1, "x": 20, "y": 20, "l4x": 30, "l4y": 30 }, + |{ "data": 1000001, "lvl": 2, "x": 40, "y": 40 }]}""".stripMargin) + + result should be (Villages( + Village(Set(StubBaseElement(1, Tile(20, 20), None), StubBaseElement(2, Tile(40, 40), None))), + Some(Village(Set(StubBaseElement(1, Tile(30, 30), None)))) + )) + } +} + +object StubElementFactory extends ElementFactory { + def build(raw: RawElement): Option[Element] = { + Some(raw) + .filter(_.data != 999999) + .map(r => StubBaseElement(PosInt.from(r.lvl).get, Tile(PosZInt.from(r.x).get, PosZInt.from(r.y).get), raw.aimAngle)) + } +} + +case class StubBaseElement(level: PosInt, tile: Tile, aimAngle: Option[Int]) extends Element { + val size = PosInt(3) +} diff --git a/src/test/scala/org/danielholmes/coc/baseanalyser/gameconnection/ClanSeekerAkkaServiceAgentSpec.scala b/src/test/scala/org/danielholmes/coc/baseanalyser/gameconnection/ClanSeekerAkkaServiceAgentSpec.scala new file mode 100644 index 0000000..f39e3c8 --- /dev/null +++ b/src/test/scala/org/danielholmes/coc/baseanalyser/gameconnection/ClanSeekerAkkaServiceAgentSpec.scala @@ -0,0 +1,20 @@ +package org.danielholmes.coc.baseanalyser.gameconnection + +import org.scalatest._ + +// Specs ignored at the moment because clan seeker not running +class ClanSeekerAkkaServiceAgentSpec extends FlatSpec with Matchers { + val client = new ClanSeekerGameConnection + + /* "Clan Seeker Service Client" */ ignore should "get clan details for OH alpha" in { + client.getClanDetails(154621406673L).get.name should be ("OneHive Alpha") + } + + ignore should "get player village for Dakota" in { + client.getPlayerVillage(223343461050L).get.village.raw.isEmpty should be (false) + } + + ignore should "get empty for non-existent" in { + client.getPlayerVillage(999999999999999L) should be (empty) + } +} diff --git a/src/test/scala/org/danielholmes/coc/baseanalyser/model/BlockSpec.scala b/src/test/scala/org/danielholmes/coc/baseanalyser/model/BlockSpec.scala new file mode 100644 index 0000000..85ebb22 --- /dev/null +++ b/src/test/scala/org/danielholmes/coc/baseanalyser/model/BlockSpec.scala @@ -0,0 +1,79 @@ +package org.danielholmes.coc.baseanalyser.model + +import org.scalatest._ + +class BlockSpec extends FlatSpec with Matchers { + val block = Block(Tile(5, 5), 3) + + "Block" should "find closest coordinate in x axis left" in { + block.findClosestCoordinate(TileCoordinate(0, 5)) should be (TileCoordinate(5, 5)) + } + + it should "find closest coordinate in x axis right" in { + block.findClosestCoordinate(TileCoordinate(10, 5)) should be (TileCoordinate(8, 5)) + } + + it should "find closest coordinate in y axis up" in { + block.findClosestCoordinate(TileCoordinate(5, 0)) should be (TileCoordinate(5, 5)) + } + + it should "find closest coordinate in y axis down" in { + block.findClosestCoordinate(TileCoordinate(5, 10)) should be (TileCoordinate(5, 8)) + } + + it should "find closest coordinate diagonally" in { + block.findClosestCoordinate(TileCoordinate(10, 10)) should be (TileCoordinate(8, 8)) + } + + it should "return empty internal coords when 1x1" in { + Block(Tile(0, 0), 1).internalCoordinates should be (Set.empty) + } + + it should "return empty internal coords when 2x2" in { + Block(Tile(0, 0), 2).internalCoordinates should contain theSameElementsAs Set(TileCoordinate(1, 1)) + } + + it should "return correct internal coords when 3x3 or more" in { + Block(Tile(0, 0), 3).internalCoordinates should contain theSameElementsAs + Set(TileCoordinate(1, 1), TileCoordinate(2, 1), TileCoordinate(1, 2), TileCoordinate(2, 2)) + } + + it should "return true intersect for overlapping items" in { + Block(Tile.Origin, 2).intersects(Block(Tile(1, 1), 2)) should be (true) + } + + it should "return false any intersect for no items" in { + Block.firstIntersecting(Set.empty) should be (empty) + } + + it should "return false any intersect for one item" in { + Block.firstIntersecting(Set(Block(Tile.Origin, 2))) should be (empty) + } + + it should "return false any intersect for edge touching items" in { + Block.firstIntersecting(Set(Block(Tile.Origin, 2), Block(Tile(0, 2), 2))) should be (empty) + } + + it should "return true any intersect for overlapping items" in { + val b1 = Block(Tile.Origin, 2) + val b2 = Block(Tile(1, 1), 2) + Block.firstIntersecting(Set(b1, b2)).get should be ((b2, b1)) + } + + it should "return correct all tiles" in { + Block(Tile.Origin, 2).tiles should contain theSameElementsAs Set(Tile.Origin, Tile(0, 1), Tile(1, 0), Tile(1, 1)) + } + + it should "return correct all coordinates" in { + Block(Tile.Origin, 2).allCoordinates should contain theSameElementsAs + Set( + TileCoordinate(0, 0), TileCoordinate(1, 0), TileCoordinate(2, 0), + TileCoordinate(0, 1), TileCoordinate(1, 1), TileCoordinate(2, 1), + TileCoordinate(0, 2), TileCoordinate(1, 2), TileCoordinate(2, 2) + ) + } + + it should "expand to size correctly" in { + Block(Tile(1, 1), 2).expandToSize(4) should be (Block(Tile(0, 0), 4)) + } +} diff --git a/src/test/scala/org/danielholmes/coc/baseanalyser/model/HogTargetingSpec.scala b/src/test/scala/org/danielholmes/coc/baseanalyser/model/HogTargetingSpec.scala new file mode 100644 index 0000000..460b5c4 --- /dev/null +++ b/src/test/scala/org/danielholmes/coc/baseanalyser/model/HogTargetingSpec.scala @@ -0,0 +1,28 @@ +package org.danielholmes.coc.baseanalyser.model + +import org.danielholmes.coc.baseanalyser.model.range.CircularElementRange +import org.danielholmes.coc.baseanalyser.model.trash.BuilderHut +import org.danielholmes.coc.baseanalyser.model.troops.HogTargeting +import org.scalatest._ + +class HogTargetingSpec extends FlatSpec with Matchers { + "HogTargeting" should "return correct cutting result for non-cutting" in { + HogTargeting(TileCoordinate(0, 0), BuilderHut(Tile(10, 0))) + .cutsRadius(CircularElementRange(FloatMapCoordinate(5, 5), 1)) shouldBe false + } + + it should "return correct cutting result for just cutting" in { + HogTargeting(TileCoordinate(0, 0), BuilderHut(Tile(10, 0))) + .cutsRadius(CircularElementRange(FloatMapCoordinate(5, 2), 2)) shouldBe false + } + + it should "return correct cutting result for middle cutting" in { + HogTargeting(TileCoordinate(0, 0), BuilderHut(Tile(10, 0))) + .cutsRadius(CircularElementRange(FloatMapCoordinate(5, 1), 2)) shouldBe true + } + + it should "return correct cutting result for random" in { + HogTargeting(TileCoordinate(0, 0), BuilderHut(Tile(0, 5))) + .cutsRadius(CircularElementRange(FloatMapCoordinate(0, 10), 1)) shouldBe false + } +} diff --git a/src/test/scala/org/danielholmes/coc/baseanalyser/model/TileCoordinateSpec.scala b/src/test/scala/org/danielholmes/coc/baseanalyser/model/TileCoordinateSpec.scala new file mode 100644 index 0000000..045de4c --- /dev/null +++ b/src/test/scala/org/danielholmes/coc/baseanalyser/model/TileCoordinateSpec.scala @@ -0,0 +1,16 @@ +package org.danielholmes.coc.baseanalyser.model + +import org.scalatest._ +import org.scalactic.anyvals.PosZDouble + +class TileCoordinateSpec extends FlatSpec with Matchers { + val coord = TileCoordinate(5, 5) + + "Tile Coordinate" should "find correct x axis right distance" in { + coord.distanceTo(TileCoordinate(10, 5)) should be (PosZDouble(5)) + } + + it should "find correct diagonal distance" in { + coord.distanceTo(TileCoordinate(8, 9)) should be (PosZDouble(5)) + } +} diff --git a/src/test/scala/org/danielholmes/coc/baseanalyser/model/TileSpec.scala b/src/test/scala/org/danielholmes/coc/baseanalyser/model/TileSpec.scala new file mode 100644 index 0000000..d888bbc --- /dev/null +++ b/src/test/scala/org/danielholmes/coc/baseanalyser/model/TileSpec.scala @@ -0,0 +1,55 @@ +package org.danielholmes.coc.baseanalyser.model + +import org.scalatest._ +import org.scalactic.anyvals.{PosZInt, PosZDouble} + +class TileSpec extends FlatSpec with Matchers { + "Tile" should "return correct matrix of tiles" in { + Tile(2, 2).matrixOfTilesInDirection(2, 2) should contain theSameElementsAs + Set(Tile(2, 2), Tile(2, 3), Tile(3, 2), Tile(3, 3)) + } + + it should "return correct rectangle of tiles" in { + Tile(2, 2).rectangleTo(Tile(4, 4)) should contain theSameElementsAs + Set( + Tile(2, 2), Tile(3, 2), Tile(4, 2), + Tile(2, 3), /*Tile(3, 2),*/ Tile(4, 3), + Tile(2, 4), Tile(3, 4), Tile(4, 4) + ) + } + + it should "return correct neighbours" in { + Tile(1, 1).neighbours should be (Set( + Tile(0, 0), Tile(1, 0), Tile(2, 0), + Tile(0, 1), /*Tile(1, 1),*/ Tile(2, 1), + Tile(0, 2), Tile(1, 2), Tile(2, 2) + )) + } + + it should "return correct neighbours at origin" in { + Tile(0, 0).neighbours should be (Set( + /*Tile(0, 0), */ Tile(1, 0), + Tile(0, 1), Tile(1, 1) + )) + } + + it should "return correct neighbours at end" in { + Tile(Tile.MaxCoordinate, Tile.MaxCoordinate).neighbours should be (Set( + Tile(PosZInt.from(Tile.MaxCoordinate - 1).get, PosZInt.from(Tile.MaxCoordinate - 1).get), + Tile(PosZInt.from(Tile.MaxCoordinate - 1).get, Tile.MaxCoordinate), + Tile(Tile.MaxCoordinate, PosZInt.from(Tile.MaxCoordinate - 1).get) + )) + } + + it should "return correct distance to touching" in { + Tile(1, 0).distanceTo(Tile(2, 0)) should be (PosZDouble(0)) + } + + it should "return correct distance to x difference" in { + Tile(0, 0).distanceTo(Tile(11, 0)) should be (PosZDouble(10)) + } + + it should "return correct distance to diagonal" in { + Tile(10, 10).distanceTo(Tile(6, 5)) should be (PosZDouble(5)) + } +} diff --git a/src/test/scala/org/danielholmes/coc/baseanalyser/model/VillageSpec.scala b/src/test/scala/org/danielholmes/coc/baseanalyser/model/VillageSpec.scala new file mode 100644 index 0000000..5113b8a --- /dev/null +++ b/src/test/scala/org/danielholmes/coc/baseanalyser/model/VillageSpec.scala @@ -0,0 +1,85 @@ +package org.danielholmes.coc.baseanalyser.model + +import org.danielholmes.coc.baseanalyser.model.trash.{Barrack, BuilderHut} +import org.danielholmes.coc.baseanalyser.util.ElementsBuilder +import org.scalatest._ + +class VillageSpec extends FlatSpec with Matchers { + "Village" should "return every coordinate when empty" in { + Village.empty.coordinatesAllowedToDropTroop should contain theSameElementsAs TileCoordinate.All + } + + it should "return all with hit areas excluded" in { + val builderPlacement = TileCoordinate(2, 2).matrixOfCoordinatesTo(TileCoordinate(4, 4)) + val expected = TileCoordinate.All.toSet -- builderPlacement + + val village = Village(Set(BuilderHut(Tile(2, 2)))) + val result = village.coordinatesAllowedToDropTroop + result should have size expected.size + result should contain theSameElementsAs expected + } + + it should "return all with hit areas excluded when on edge of map" in { + val builderPlacement = TileCoordinate.MapOrigin.matrixOfCoordinatesTo(TileCoordinate.MapOrigin.offset(2, 2)) + val expected = TileCoordinate.All.toSet -- builderPlacement + + val village = Village(Set(BuilderHut(Tile.MapOrigin))) + val result = village.coordinatesAllowedToDropTroop + result should have size expected.size + result should contain theSameElementsAs expected + } + + it should "disallow overlapping elements" in { + a[IllegalArgumentException] should be thrownBy { + Village( + Set( + Barrack(1, Tile.MapOrigin), + Barrack(1, Tile.MapOrigin.offset(1, 1)) + ) + ) + } + } + + it should "not include intersections of attack placements in coordinates allowed to drop troop" in { + val village = Village(Set(BuilderHut(Tile(1, 1)), BuilderHut(Tile(5, 1)))) + + village.coordinatesAllowedToDropTroop should not contain TileCoordinate(4, 1) + village.coordinatesAllowedToDropTroop should not contain TileCoordinate(4, 2) + village.coordinatesAllowedToDropTroop should not contain TileCoordinate(4, 3) + } + + it should "return no compartments for empty village" in { + Village.empty.wallCompartments should be (empty) + } + + it should "return no compartments for village with walls but no compartments" in { + ElementsBuilder.villageFromString("WWW\nW W\n WW", Tile(1, 1), Wall(1, _)).wallCompartments should be (empty) + } + + it should "return single simple compartment" in { + val walls = ElementsBuilder.wallFence(Tile(5, 5), 3, 3) + Village(walls.map(_.asInstanceOf[Element])).wallCompartments should be (Set(WallCompartment( + walls, Set(Tile(6, 6)), Set.empty + ))) + } + + it should "return single compartment with a building inside" in { + val walls = ElementsBuilder.elementFence(Tile(6, 6), 5, 5) + val barrack = Barrack(1, Tile(7, 7)) + + Village(walls + barrack).wallCompartments should be (Set(WallCompartment( + walls.map(_.asInstanceOf[Wall]), + Tile(7, 7).matrixOfTilesTo(Tile(9, 9)), + Set(barrack) + ))) + } + + it should "return multiple compartments" in { + val walls1 = ElementsBuilder.fromString("WWW\nW W\nWWW", Tile(6, 6), Wall(1, _)) + val walls2 = ElementsBuilder.fromString("WWW\nW W\nWWW", Tile(16, 6), Wall(1, _)) + Village((walls1 ++ walls2).map(_.asInstanceOf[Element])).wallCompartments should be (Set( + WallCompartment(walls1, Set(Tile(7, 7)), Set.empty), + WallCompartment(walls2, Set(Tile(17, 7)), Set.empty) + )) + } +} diff --git a/src/test/scala/org/danielholmes/coc/baseanalyser/model/WallCompartmentSpec.scala b/src/test/scala/org/danielholmes/coc/baseanalyser/model/WallCompartmentSpec.scala new file mode 100644 index 0000000..c2be656 --- /dev/null +++ b/src/test/scala/org/danielholmes/coc/baseanalyser/model/WallCompartmentSpec.scala @@ -0,0 +1,32 @@ +package org.danielholmes.coc.baseanalyser.model + +import org.danielholmes.coc.baseanalyser.model.trash.Barrack +import org.danielholmes.coc.baseanalyser.util.ElementsBuilder +import org.scalatest._ + +class WallCompartmentSpec extends FlatSpec with Matchers { + "WallCompartment" should "return empty tiles for no buildings" in { + val walls = ElementsBuilder.wallFence(Tile.MapOrigin, 3, 3) + val inner = Set(Tile.MapOrigin.offset(1, 1)) + WallCompartment(walls, inner, Set.empty).emptyTiles should contain theSameElementsAs inner + } + + it should "return empty tiles for building inside" in { + val walls = ElementsBuilder.wallFence(Tile.MapOrigin, 6, 5) + val inner = Tile.MapOrigin.offset(1, 1).matrixOfTilesTo(Tile.MapOrigin.offset(4, 3)) + val buildings = Set[Element](Barrack(1, Tile.MapOrigin.offset(2, 1))) + WallCompartment(walls, inner, buildings).emptyTiles should contain theSameElementsAs + Tile.MapOrigin.offset(1, 1).matrixOfTilesTo(Tile.MapOrigin.offset(1, 3)) + } + + it should "return empty possible large traps for 1x1 only" in { + WallCompartment(ElementsBuilder.wallFence(Tile.MapOrigin, 3, 3), Set(Tile.MapOrigin.offset(1, 1)), Set.empty).possibleLargeTraps should be (empty) + } + + it should "return 2 possible large traps for 3x2 empty space" in { + val walls = ElementsBuilder.wallFence(Tile.MapOrigin, 5, 4) + val inner = Tile.MapOrigin.offset(1, 1).matrixOfTilesTo(Tile.MapOrigin.offset(3, 2)) + WallCompartment(walls, inner, Set.empty).possibleLargeTraps should contain theSameElementsAs + Set(PossibleLargeTrap(Tile.MapOrigin.offset(1, 1)), PossibleLargeTrap(Tile.MapOrigin.offset(2, 1))) + } +} diff --git a/src/test/scala/org/danielholmes/coc/baseanalyser/model/range/BlindSpotSectorElementRangeSpec.scala b/src/test/scala/org/danielholmes/coc/baseanalyser/model/range/BlindSpotSectorElementRangeSpec.scala new file mode 100644 index 0000000..6d3a554 --- /dev/null +++ b/src/test/scala/org/danielholmes/coc/baseanalyser/model/range/BlindSpotSectorElementRangeSpec.scala @@ -0,0 +1,18 @@ +package org.danielholmes.coc.baseanalyser.model.range + +import org.danielholmes.coc.baseanalyser.model.{Angle, FloatMapCoordinate, TileCoordinate} +import org.scalatest._ + +class BlindSpotSectorElementRangeSpec extends FlatSpec with Matchers { + "BlindSpotSectorElementRange" should "contains inside" in { + BlindSpotSectorElementRange(TileCoordinate(10, 10), 1.0, 10.0, Angle.Quarter, Angle.Half).contains(FloatMapCoordinate(15.0, 10.0)) should be (true) + } + + it should "not contain outside" in { + BlindSpotSectorElementRange(TileCoordinate(10, 10), 1.0, 10.0, Angle.Quarter, Angle.Half).contains(FloatMapCoordinate(25.0, 10.0)) should be (false) + } + + it should "not contain outside opposite direction" in { + BlindSpotSectorElementRange(TileCoordinate(10, 10), 1.0, 10.0, Angle.Quarter, Angle.Half).contains(FloatMapCoordinate(5.0, 10.0)) should be (false) + } +} diff --git a/src/test/scala/org/danielholmes/coc/baseanalyser/model/troops/HogRiderSpec.scala b/src/test/scala/org/danielholmes/coc/baseanalyser/model/troops/HogRiderSpec.scala new file mode 100644 index 0000000..18d32c6 --- /dev/null +++ b/src/test/scala/org/danielholmes/coc/baseanalyser/model/troops/HogRiderSpec.scala @@ -0,0 +1,54 @@ +package org.danielholmes.coc.baseanalyser.model.troops + +import org.danielholmes.coc.baseanalyser.model._ +import org.danielholmes.coc.baseanalyser.model.defense.{AirSweeper, ArcherTower, Cannon} +import org.danielholmes.coc.baseanalyser.model.heroes.BarbarianKingAltar +import org.danielholmes.coc.baseanalyser.model.trash.Barrack +import org.scalatest._ + +class HogRiderSpec extends FlatSpec with Matchers { + val origin = TileCoordinate(0, 0) + + "HogRider" should "return defense target" in { + val at = ArcherTower(1, Tile(1, 1)) + HogRider.findTargets(origin, Village(Set(at))) should contain (at) + } + + it should "return non-defense target if none available" in { + val barrack = Barrack(1, Tile(1, 1)) + HogRider.findTargets(origin, Village(Set(barrack))) should contain (barrack) + } + + it should "return empty if only wall available" in { + HogRider.findTargets(origin, Village(Set(Wall(1, Tile.MapOrigin)))) should be (empty) + } + + it should "return closest defense target if available" in { + val at = ArcherTower(1, Tile(10, 10)) + val closeBarrack = Barrack(1, Tile(2, 2)) + HogRider.findTargets(origin, Village(Set(at, closeBarrack))) should contain (at) + } + + it should "not include heroes as target" in { + val bk = BarbarianKingAltar(1, Tile(10, 10)) + val closeBarrack = Barrack(1, Tile(2, 2)) + HogRider.findTargets(origin, Village(Set(bk, closeBarrack))) should contain (closeBarrack) + } + + it should "include air sweeper as target" in { + val as = AirSweeper(1, Tile(10, 10), Angle.Half) + val closeBarrack = Barrack(1, Tile(2, 2)) + HogRider.findTargets(origin, Village(Set(as, closeBarrack))) should contain (as) + } + + it should "return none for empty village" in { + HogRider.findTargets(origin, Village.empty) shouldBe empty + } + + it should "return all equidistant defense targets" in { + val at = ArcherTower(1, Tile(0, 10)) + val cannon = Cannon(1, Tile(10, 0)) + val closeBarrack = Barrack(1, Tile(2, 2)) + HogRider.findTargets(origin, Village(Set(at, cannon, closeBarrack))) should be (Set(at, cannon)) + } +} diff --git a/src/test/scala/org/danielholmes/coc/baseanalyser/model/troops/TroopSpec.scala b/src/test/scala/org/danielholmes/coc/baseanalyser/model/troops/TroopSpec.scala new file mode 100644 index 0000000..6031a63 --- /dev/null +++ b/src/test/scala/org/danielholmes/coc/baseanalyser/model/troops/TroopSpec.scala @@ -0,0 +1,36 @@ +package org.danielholmes.coc.baseanalyser.model.troops + +import org.danielholmes.coc.baseanalyser.model.trash.{ArmyCamp, BuilderHut} +import org.danielholmes.coc.baseanalyser.model._ +import org.scalactic.anyvals.{PosDouble, PosZDouble} +import org.scalatest._ + +class TroopSpec extends FlatSpec with Matchers { + "Troop" should "return correct points can attack building from" in { + val LowCornerCoord = PosDouble.from(2.0 - Math.sqrt(0.75 / 2)).get + val HighCornerCoord = PosDouble.from(6.0 - LowCornerCoord).get + ExampleTroop.getAttackFloatCoordinates(BuilderHut(Tile(2, 2))) should contain theSameElementsAs + (Set( + FloatMapCoordinate(LowCornerCoord, LowCornerCoord), FloatMapCoordinate(2, 1.25), FloatMapCoordinate(3, 1.25), + FloatMapCoordinate(4, 1.25), FloatMapCoordinate(HighCornerCoord, LowCornerCoord), + FloatMapCoordinate(1.25, 2), FloatMapCoordinate(2, 2), FloatMapCoordinate(3, 2), FloatMapCoordinate(4, 2), FloatMapCoordinate(4.75, 2), + FloatMapCoordinate(1.25, 3), FloatMapCoordinate(2, 3), FloatMapCoordinate(3, 3), FloatMapCoordinate(4, 3), FloatMapCoordinate(4.75, 3), + FloatMapCoordinate(1.25, 4), FloatMapCoordinate(2, 4), FloatMapCoordinate(3, 4), FloatMapCoordinate(4, 4), FloatMapCoordinate(4.75, 4), + FloatMapCoordinate(LowCornerCoord, HighCornerCoord), FloatMapCoordinate(2, 4.75), FloatMapCoordinate(3, 4.75), + FloatMapCoordinate(4, 4.75), FloatMapCoordinate(HighCornerCoord, HighCornerCoord) + )) + } + + it should "find targets with correct hit area" in { + val armyCamp = ArmyCamp(1, Tile(10, 10)) + ExampleTroop.findTargets(TileCoordinate(10, 10), Village(Set(armyCamp))) should contain theSameElementsAs Set(armyCamp) + } +} + +object ExampleTroop extends Troop { + val Range = PosZDouble(0.75) + + override protected def getPrioritisedTargets(village: Village): List[Set[Structure]] = { + getAnyBuildingsTargets(village) + } +} diff --git a/src/test/scala/org/danielholmes/coc/baseanalyser/stringdisplay/StringDisplayerSpec.scala b/src/test/scala/org/danielholmes/coc/baseanalyser/stringdisplay/StringDisplayerSpec.scala new file mode 100644 index 0000000..b32c80e --- /dev/null +++ b/src/test/scala/org/danielholmes/coc/baseanalyser/stringdisplay/StringDisplayerSpec.scala @@ -0,0 +1,93 @@ +package org.danielholmes.coc.baseanalyser.stringdisplay + +import org.danielholmes.coc.baseanalyser.model._ +import org.danielholmes.coc.baseanalyser.model.defense.ArcherTower +import org.danielholmes.coc.baseanalyser.model.special.{ClanCastle, TownHall} +import org.scalatest._ + +class StringDisplayerSpec extends FlatSpec with Matchers { + val DrawnRowSize = Tile.MaxCoordinate + 1 + 2 + 1 // All tiles, borders and a new line char + val displayer = new StringDisplayer + + "String Displayer" should "display empty base" in { + displayer.build(Village.empty) should be (EMPTY) + } + + it should "display origin town hall" in { + val result = displayer.build(Village(Set(TownHall(1, Tile(1, 1))))) + for (row <- 2 to 5) { + for (col <- 2 to 5) { + result.charAt(row * DrawnRowSize + col) should be('#') + } + } + } + + it should "display end of earth archer tower" in { + val result = displayer.build(Village(Set(ArcherTower(1, Tile(41, 41))))) + for (row <- 42 to 44) { + for (col <- 42 to 44) { + result.charAt(row * DrawnRowSize + col) should be('A') + } + } + } + + it should "display end of map cc radius without exception" in { + displayer.build(Village(Set(ClanCastle(1, Tile(41, 41))))) + //result.charAt(1600) should be('^') + } + + val EMPTY = +"""+--------------------------------------------------+ +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | ++--------------------------------------------------+ +""" +} diff --git a/src/test/scala/org/danielholmes/coc/baseanalyser/util/ElementsBuilderSpec.scala b/src/test/scala/org/danielholmes/coc/baseanalyser/util/ElementsBuilderSpec.scala new file mode 100644 index 0000000..17707b6 --- /dev/null +++ b/src/test/scala/org/danielholmes/coc/baseanalyser/util/ElementsBuilderSpec.scala @@ -0,0 +1,14 @@ +package org.danielholmes.coc.baseanalyser.util + +import org.danielholmes.coc.baseanalyser.model.{Tile, Wall} +import org.scalatest._ + +class ElementsBuilderSpec extends FlatSpec with Matchers { + "ElementsBuilder" should "return correctly built elements" in { + ElementsBuilder.fromString("WWW\nW W\nWWW", Tile(10, 10), Wall(1, _)) should be (Set( + Wall(1, Tile(10, 10)), Wall(1, Tile(11, 10)), Wall(1, Tile(12, 10)), + Wall(1, Tile(10, 11)), /*Wall(1, Tile(11, 11)),*/ Wall(1, Tile(12, 11)), + Wall(1, Tile(10, 12)), Wall(1, Tile(11, 12)), Wall(1, Tile(12, 12)) + )) + } +}