init
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
/target
|
||||
/project/target
|
||||
/project/project
|
||||
29
Dockerfile
Normal file
|
|
@ -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
|
||||
21
LICENSE
Normal file
|
|
@ -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.
|
||||
82
README.md
Normal file
|
|
@ -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
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
100
TODO.md
Normal file
|
|
@ -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
|
||||
68
build.sbt
Normal file
|
|
@ -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"))
|
||||
)*/
|
||||
BIN
docs/images/base-1.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
docs/images/base-2.png
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
docs/images/base-3.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
docs/images/base-4.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
docs/images/bulk-1.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
docs/images/home-1.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
1
project/build.properties
Normal file
|
|
@ -0,0 +1 @@
|
|||
sbt.version=0.13.11
|
||||
5
project/plugins.sbt
Normal file
|
|
@ -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")
|
||||
1
project/sbt-updates.sbt
Normal file
|
|
@ -0,0 +1 @@
|
|||
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.10")
|
||||
107
scalastyle-config.xml
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
<scalastyle>
|
||||
<name>Scalastyle standard configuration</name>
|
||||
<check level="warning" class="org.scalastyle.file.FileTabChecker" enabled="true"></check>
|
||||
<check level="warning" class="org.scalastyle.file.FileLengthChecker" enabled="true">
|
||||
<parameters>
|
||||
<parameter name="maxFileLength"><![CDATA[800]]></parameter>
|
||||
</parameters>
|
||||
</check>
|
||||
<check level="warning" class="org.scalastyle.file.HeaderMatchesChecker" enabled="false">
|
||||
<parameters>
|
||||
<parameter name="header"><![CDATA[// Copyright (C) 2011-2012 the original author or authors.
|
||||
// See the LICENCE.txt file distributed with this work for additional
|
||||
// information regarding copyright ownership.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.]]></parameter>
|
||||
</parameters>
|
||||
</check>
|
||||
<check level="warning" class="org.scalastyle.scalariform.SpacesAfterPlusChecker" enabled="true"></check>
|
||||
<check level="warning" class="org.scalastyle.file.WhitespaceEndOfLineChecker" enabled="true"></check>
|
||||
<check level="warning" class="org.scalastyle.scalariform.SpacesBeforePlusChecker" enabled="true"></check>
|
||||
<check level="warning" class="org.scalastyle.file.FileLineLengthChecker" enabled="true">
|
||||
<parameters>
|
||||
<parameter name="maxLineLength"><![CDATA[160]]></parameter>
|
||||
<parameter name="tabSize"><![CDATA[4]]></parameter>
|
||||
</parameters>
|
||||
</check>
|
||||
<check level="warning" class="org.scalastyle.scalariform.ClassNamesChecker" enabled="true">
|
||||
<parameters>
|
||||
<parameter name="regex"><![CDATA[[A-Z][A-Za-z]*]]></parameter>
|
||||
</parameters>
|
||||
</check>
|
||||
<check level="warning" class="org.scalastyle.scalariform.ObjectNamesChecker" enabled="true">
|
||||
<parameters>
|
||||
<parameter name="regex"><![CDATA[[A-Z][A-Za-z]*]]></parameter>
|
||||
</parameters>
|
||||
</check>
|
||||
<check level="warning" class="org.scalastyle.scalariform.PackageObjectNamesChecker" enabled="true">
|
||||
<parameters>
|
||||
<parameter name="regex"><![CDATA[^[a-z][A-Za-z]*$]]></parameter>
|
||||
</parameters>
|
||||
</check>
|
||||
<check level="warning" class="org.scalastyle.scalariform.EqualsHashCodeChecker" enabled="true"></check>
|
||||
<check level="warning" class="org.scalastyle.scalariform.IllegalImportsChecker" enabled="true">
|
||||
<parameters>
|
||||
<parameter name="illegalImports"><![CDATA[sun._,java.awt._]]></parameter>
|
||||
</parameters>
|
||||
</check>
|
||||
<check level="warning" class="org.scalastyle.scalariform.ParameterNumberChecker" enabled="true">
|
||||
<parameters>
|
||||
<parameter name="maxParameters"><![CDATA[8]]></parameter>
|
||||
</parameters>
|
||||
</check>
|
||||
<check level="warning" class="org.scalastyle.scalariform.NoWhitespaceBeforeLeftBracketChecker" enabled="true"></check>
|
||||
<check level="warning" class="org.scalastyle.scalariform.NoWhitespaceAfterLeftBracketChecker" enabled="true"></check>
|
||||
<check level="warning" class="org.scalastyle.scalariform.ReturnChecker" enabled="true"></check>
|
||||
<check level="warning" class="org.scalastyle.scalariform.NullChecker" enabled="true"></check>
|
||||
<check level="warning" class="org.scalastyle.scalariform.NoCloneChecker" enabled="true"></check>
|
||||
<check level="warning" class="org.scalastyle.scalariform.NoFinalizeChecker" enabled="true"></check>
|
||||
<check level="warning" class="org.scalastyle.scalariform.CovariantEqualsChecker" enabled="true"></check>
|
||||
<check level="warning" class="org.scalastyle.scalariform.StructuralTypeChecker" enabled="true"></check>
|
||||
<check level="warning" class="org.scalastyle.scalariform.NumberOfTypesChecker" enabled="true">
|
||||
<parameters>
|
||||
<parameter name="maxTypes"><![CDATA[30]]></parameter>
|
||||
</parameters>
|
||||
</check>
|
||||
<check level="warning" class="org.scalastyle.scalariform.CyclomaticComplexityChecker" enabled="true">
|
||||
<parameters>
|
||||
<parameter name="maximum"><![CDATA[10]]></parameter>
|
||||
</parameters>
|
||||
</check>
|
||||
<check level="warning" class="org.scalastyle.scalariform.UppercaseLChecker" enabled="true"></check>
|
||||
<check level="warning" class="org.scalastyle.scalariform.SimplifyBooleanExpressionChecker" enabled="true"></check>
|
||||
<check level="warning" class="org.scalastyle.scalariform.IfBraceChecker" enabled="true">
|
||||
<parameters>
|
||||
<parameter name="singleLineAllowed"><![CDATA[true]]></parameter>
|
||||
<parameter name="doubleLineAllowed"><![CDATA[false]]></parameter>
|
||||
</parameters>
|
||||
</check>
|
||||
<check level="warning" class="org.scalastyle.scalariform.MethodLengthChecker" enabled="true">
|
||||
<parameters>
|
||||
<parameter name="maxLength"><![CDATA[50]]></parameter>
|
||||
</parameters>
|
||||
</check>
|
||||
<check level="warning" class="org.scalastyle.scalariform.MethodNamesChecker" enabled="false">
|
||||
<parameters>
|
||||
<parameter name="regex"><![CDATA[^[a-z][A-Za-z0-9]*$]]></parameter>
|
||||
</parameters>
|
||||
</check>
|
||||
<check level="warning" class="org.scalastyle.scalariform.NumberOfMethodsInTypeChecker" enabled="true">
|
||||
<parameters>
|
||||
<parameter name="maxMethods"><![CDATA[30]]></parameter>
|
||||
</parameters>
|
||||
</check>
|
||||
<check level="warning" class="org.scalastyle.scalariform.PublicMethodsHaveTypeChecker" enabled="true"></check>
|
||||
<check level="warning" class="org.scalastyle.file.NewLineAtEofChecker" enabled="true"></check>
|
||||
<check level="warning" class="org.scalastyle.file.NoNewLineAtEofChecker" enabled="false"></check>
|
||||
</scalastyle>
|
||||
7
src/main/resources/application.conf
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
akka {
|
||||
log-dead-letters-during-shutdown = off
|
||||
}
|
||||
|
||||
spray.servlet {
|
||||
boot-class = "org.danielholmes.coc.baseanalyser.web.WebAppServlet"
|
||||
}
|
||||
1
src/main/resources/examples/th5-sample-1.json
Normal file
|
|
@ -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}
|
||||
18
src/main/resources/examples/th8-sample-1.json
Normal file
1
src/main/resources/examples/th9-sample-1.json
Normal file
BIN
src/main/resources/tile.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
src/main/resources/tile_center.jpg
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src/main/resources/tiles.jpg
Normal file
|
After Width: | Height: | Size: 17 KiB |
115
src/main/resources/web/base-analysis.mustache
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
{{<base}}
|
||||
{{$title}}{{clanName}} - {{playerIgn}} - {{layoutDescription}}{{/title}}
|
||||
|
||||
{{$heading}}{{clanName}} - {{playerIgn}} - {{layoutDescription}}{{/heading}}
|
||||
|
||||
{{$styles}}
|
||||
<link rel="stylesheet" type="text/css" href="/css/base-analysis.css" />
|
||||
{{/styles}}
|
||||
|
||||
{{$navbar}}
|
||||
{{#times}}
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"
|
||||
aria-expanded="false"><span class="glyphicon glyphicon-time"></span> {{connection}} | {{analysis}} <span class="caret"></span></a>
|
||||
<div class="dropdown-menu" style="min-width:280px">
|
||||
{{#times}}
|
||||
<h5>{{_1}}</h5>
|
||||
<table class="table table-condensed table-bordered">
|
||||
<tbody>
|
||||
{{#_2}}
|
||||
<tr><td class="col-md-9">{{_1}}</td><td class="col-md-3 text-right">{{_2}}</td></tr>
|
||||
{{/_2}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{/times}}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
{{/times}}
|
||||
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"
|
||||
aria-expanded="false">Display Settings <span class="caret"></span></a>
|
||||
<form id="display-settings" class="dropdown-menu">
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input name="grid" type="checkbox"> Show grid
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
{{/navbar}}
|
||||
|
||||
{{$content}}
|
||||
{{#warning}}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="alert alert-warning">{{warning}}</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/warning}}
|
||||
|
||||
<div id="report" class="row">
|
||||
<div class="col-lg-3 col-md-4">
|
||||
<div id="results-panel-group" class="panel-group" role="tablist" aria-multiselectable="false"></div>
|
||||
</div>
|
||||
<div class="col-lg-9 col-md-8" id="map-container">
|
||||
<canvas id="villageImage"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
{{/content}}
|
||||
|
||||
{{$scripts}}
|
||||
<script id="result-panel" type="x-tmpl-mustache">
|
||||
<div id="panel-[[id]]" class="panel panel-default [[#success]]success[[/success]] [[^success]]failed[[/success]]">
|
||||
<div class="panel-heading" role="tab" id="heading-[[id]]" role="button" class="collapsed" data-toggle="collapse" data-parent="#results-panel-group" href="#collapse-[[id]]" aria-expanded="false" aria-controls="collapse-[[id]]">
|
||||
<h4 class="panel-title">
|
||||
[[#success]]
|
||||
<span class="glyphicon glyphicon-ok-sign"></span>
|
||||
[[/success]]
|
||||
[[^success]]
|
||||
<span class="glyphicon glyphicon-remove-sign"></span>
|
||||
[[/success]]
|
||||
<a href="#">
|
||||
[[title]]
|
||||
</a>
|
||||
<a class="do-show">show me</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="collapse-[[id]]" class="panel-collapse collapse" role="tabpanel" aria-labelledby="heading-[[id]]" data-rule-code="[[ruleCode]]">
|
||||
<div class="panel-body">[[description]]</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
var report = {{{report}}};
|
||||
var mapConfig = {
|
||||
mapTiles: {{mapTiles}},
|
||||
borderTiles: {{borderTiles}}
|
||||
};
|
||||
</script>
|
||||
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/js-signals/1.0.0/js-signals.min.js"></script>
|
||||
<script type="text/javascript" src="//code.createjs.com/easeljs-0.8.2.min.js"></script>
|
||||
<script type="text/javascript" src="//code.createjs.com/preloadjs-0.6.2.min.js"></script>
|
||||
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
|
||||
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/mustache.js/2.2.1/mustache.min.js"></script>
|
||||
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/jStorage/0.4.12/jstorage.min.js"></script>
|
||||
<script type="text/javascript" src="/js/console-polyfill.js"></script>
|
||||
<script type="text/javascript" src="/js/base-analysis/Preloader.js"></script>
|
||||
<script type="text/javascript" src="/js/base-analysis/Model.js"></script>
|
||||
<script type="text/javascript" src="/js/base-analysis/RedMoonBuildingSpriteSheet.js"></script>
|
||||
<script type="text/javascript" src="/js/base-analysis/RedMoonBuildingSpriteSheet.js"></script>
|
||||
<script type="text/javascript" src="/js/base-analysis/RedMoonWallSpriteSheet.js"></script>
|
||||
<script type="text/javascript" src="/js/base-analysis/DisplaySettings.js"></script>
|
||||
<script type="text/javascript" src="/js/base-analysis/MapDisplay2d.js"></script>
|
||||
<script type="text/javascript" src="/js/base-analysis/Ui.js"></script>
|
||||
<script type="text/javascript" src="/js/base-analysis/main.js"></script>
|
||||
{{/scripts}}
|
||||
{{/base}}
|
||||
45
src/main/resources/web/base.mustache
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Clash War Base Analyser - {{$title}}{{/title}}</title>
|
||||
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap-theme.min.css" integrity="sha384-fLW2N01lMqjakBkx3l/M9EahuwpSfeNvV63J5ezn3uZzapT0u7EYsXMjQV+0En5r" crossorigin="anonymous">
|
||||
<link rel="stylesheet" type="text/css" href="/css/main.css" />
|
||||
{{$styles}}{{/styles}}
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-default navbar-inverse navbar-static-top">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar-contents" aria-expanded="false">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand">{{$heading}}{{/heading}}</a>
|
||||
</div>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbar-contents">
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Note! Early release <span class="caret"></span></a>
|
||||
<div class="dropdown-menu alert alert-info">
|
||||
This is an early release. The game connection is unreliable and you might need to refresh. Traps are
|
||||
not yet implemented. Any feedback or bug reports are much appreciated
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
{{$navbar}}{{/navbar}}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container-fluid">
|
||||
{{$content}}{{/content}}
|
||||
</div>
|
||||
<script type="text/javascript" src="//code.jquery.com/jquery-2.2.1.min.js"></script>
|
||||
<script type="text/javascript" src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
|
||||
{{$scripts}}{{/scripts}}
|
||||
</body>
|
||||
</html>
|
||||
38
src/main/resources/web/clan.mustache
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
{{<base}}
|
||||
{{$title}}{{ name }}{{/title}}
|
||||
|
||||
{{$heading}}{{ name }}{{/heading}}
|
||||
|
||||
{{$content}}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h4>Bulk Analysis</h4>
|
||||
<a href="{{bulkAnalysisUrl}}">{{bulkAnalysisUrl}}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h4>Current Players</h4>
|
||||
<table class="table table-hover table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>IGN</th>
|
||||
<th>Base Analysis - Active War</th>
|
||||
<th>Base Analysis - Home</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#players}}
|
||||
<tr>
|
||||
<td>{{ign}}</td>
|
||||
<td><a href="{{warAnalysisUrl}}">{{warAnalysisUrl}}</a></td>
|
||||
<td><a href="{{homeAnalysisUrl}}">{{homeAnalysisUrl}}</a></td>
|
||||
</tr>
|
||||
{{/players}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{{/content}}
|
||||
{{/base}}
|
||||
9
src/main/resources/web/error.mustache
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{{<base}}
|
||||
{{$title}}{{title}}{{/title}}
|
||||
|
||||
{{$heading}}{{title}}{{/heading}}
|
||||
|
||||
{{$content}}
|
||||
<p class="text-danger">{{message}}</p>
|
||||
{{/content}}
|
||||
{{/base}}
|
||||
16
src/main/resources/web/index.html
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>CoC War Base Analyser</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>CoC War Base Analyser</h3>
|
||||
<p>Nothing to see here... Ask for the link to your clan page from the developer</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
89
src/main/resources/web/war-bases.mustache
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
{{<base}}
|
||||
{{$title}}{{ name }} Bulk War Bases Analysis{{/title}}
|
||||
|
||||
{{$styles}}
|
||||
<style type="text/css">
|
||||
.glyphicon-remove-sign {
|
||||
color: #ff5555;
|
||||
}
|
||||
button.collapsed .hide-instruction {
|
||||
display:none;
|
||||
}
|
||||
</style>
|
||||
{{/styles}}
|
||||
|
||||
{{$heading}}{{name}} Bulk War Bases Analysis{{/heading}}
|
||||
|
||||
{{$content}}
|
||||
<div class="row">
|
||||
<div id="loading" class="col-md-12">
|
||||
Analysing: <span class="loading-names"></span>
|
||||
<button class="btn btn-link btn-xs queued-button" data-toggle="collapse"
|
||||
data-target="#loading-queued-names" aria-expanded="false" aria-controls="loading-queued-names">+ <span class="queued-count"></span> queued</button>
|
||||
<div id="loading-queued-names" class="queued-names text-muted collapse"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 hidden" id="problems">
|
||||
<h4>Problems</h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div id="results" class="col-md-12">
|
||||
</div>
|
||||
</div>
|
||||
{{/content}}
|
||||
|
||||
{{$scripts}}
|
||||
<script id="town-hall-container-template" type="x-tmpl-mustache">
|
||||
<div id="[[containerId]]" class="row">
|
||||
<div class="col-md-12">
|
||||
<h4>Town Hall [[level]]</h4>
|
||||
|
||||
<div class="plain-text-summary">
|
||||
<button class="btn btn-primary collapsed" type="button" data-toggle="collapse" data-target="#plain-text-summary-[[level]]" aria-expanded="false" aria-controls="plain-text-summary-[[level]]">
|
||||
Plain Text Summary of Errors
|
||||
<span class="hide-instruction">(Hide)</span>
|
||||
</button>
|
||||
<div id="plain-text-summary-[[level]]" class="collapse">
|
||||
<button class="btn" data-clipboard-target="#plain-text-summary-text-[[level]]">
|
||||
Copy to clipboard
|
||||
</button>
|
||||
<textarea id="plain-text-summary-text-[[level]]" class="form-control" rows="6" readonly="readonly"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-bordered table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>IGN</th>
|
||||
<th>Analysis</th>
|
||||
[[#rules]]
|
||||
<th data-rule="[[.]]" class="result-col col-[[.]]">[[.]]</th>
|
||||
[[/rules]]
|
||||
<th><span class="glyphicon glyphicon-time"></span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
var players = [
|
||||
{{#players}}
|
||||
{ "id": "{{id}}", "ign": "{{ign}}", "analysisUrl": "{{analysisUrl}}", "analysisSummaryUrl": "{{analysisSummaryUrl}}" },
|
||||
{{/players}}
|
||||
];
|
||||
</script>
|
||||
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
|
||||
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/mustache.js/2.2.1/mustache.min.js"></script>
|
||||
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/clipboard.js/1.5.10/clipboard.min.js"></script>
|
||||
<script type="text/javascript" src="/js/console-polyfill.js"></script>
|
||||
<script type="text/javascript" src="/js/war-bases/main.js"></script>
|
||||
{{/scripts}}
|
||||
{{/base}}
|
||||
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -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"
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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"
|
||||
)
|
||||
}
|
||||
|
|
@ -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"
|
||||
)
|
||||
}
|
||||
|
|
@ -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"
|
||||
)
|
||||
}
|
||||
|
|
@ -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"
|
||||
)
|
||||
}
|
||||
|
|
@ -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"
|
||||
)
|
||||
}
|
||||
|
|
@ -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"
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package org.danielholmes.coc.baseanalyser.analysis
|
||||
|
||||
case class PlayerAnalysisReport(userName: String, townHallLevel: Int, villageReport: Option[AnalysisReport])
|
||||
|
|
@ -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"
|
||||
)
|
||||
}
|
||||
|
|
@ -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)"
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package org.danielholmes.coc.baseanalyser.analysis
|
||||
|
||||
import org.danielholmes.coc.baseanalyser.model.Village
|
||||
|
||||
trait Rule {
|
||||
def analyse(village: Village): RuleResult
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package org.danielholmes.coc.baseanalyser.analysis
|
||||
|
||||
case class RuleDetails(code: String, shortName: String, name: String, description: String)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package org.danielholmes.coc.baseanalyser.analysis
|
||||
|
||||
trait RuleResult {
|
||||
val success: Boolean
|
||||
|
||||
val ruleDetails: RuleDetails
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -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"))
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)"
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package org.danielholmes.coc.baseanalyser.model
|
||||
|
||||
trait Building extends Structure
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package org.danielholmes.coc.baseanalyser.model
|
||||
|
||||
import org.scalactic.anyvals.PosInt
|
||||
|
||||
trait DelayedActivation extends Defense {
|
||||
val deploymentSpaceRequired: PosInt
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package org.danielholmes.coc.baseanalyser.model
|
||||
|
||||
trait Hidden extends Element
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package org.danielholmes.coc.baseanalyser.model
|
||||
|
||||
trait StationaryDefensiveBuilding extends Defense
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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})"
|
||||
}
|
||||
}
|
||||
|
|
@ -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)"
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||