This commit is contained in:
Hymmel 2025-10-10 15:32:29 +02:00
commit 2ee9eb1b65
188 changed files with 7395 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/target
/project/target
/project/project

29
Dockerfile Normal file
View 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
View 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
View 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
![Clan Homepage](docs/images/home-1.png?raw=true "Clan Home")
![Base Analysis 1](docs/images/base-1.png?raw=true "Base Analysis 1")
![Base Analysis 2](docs/images/base-2.png?raw=true "Base Analysis 2")
![Base Analysis 3](docs/images/base-3.png?raw=true "Base Analysis 3")
![Base Analysis 4](docs/images/base-4.png?raw=true "Base Analysis 4")
![Bulk Analysis](docs/images/bulk-1.png?raw=true "Bulk Analysis")

100
TODO.md Normal file
View 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 arent 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

BIN
docs/images/base-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

BIN
docs/images/base-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

BIN
docs/images/base-4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

BIN
docs/images/bulk-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
docs/images/home-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

1
project/build.properties Normal file
View file

@ -0,0 +1 @@
sbt.version=0.13.11

5
project/plugins.sbt Normal file
View 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
View file

@ -0,0 +1 @@
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.10")

107
scalastyle-config.xml Normal file
View 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>

View file

@ -0,0 +1,7 @@
akka {
log-dead-letters-during-shutdown = off
}
spray.servlet {
boot-class = "org.danielholmes.coc.baseanalyser.web.WebAppServlet"
}

View 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}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

BIN
src/main/resources/tile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View 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}}

View 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>

View 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}}

View file

@ -0,0 +1,9 @@
{{<base}}
{{$title}}{{title}}{{/title}}
{{$heading}}{{title}}{{/heading}}
{{$content}}
<p class="text-danger">{{message}}</p>
{{/content}}
{{/base}}

View 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>

View 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}}

View file

@ -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"))
}
}

View file

@ -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
)
}

View file

@ -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
)
}

View file

@ -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
)
}

View file

@ -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]
}

View file

@ -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"
)
}

View file

@ -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)
}

View file

@ -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)

View file

@ -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"
)
}

View file

@ -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"
)
}

View file

@ -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"
)
}

View file

@ -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"
)
}

View file

@ -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"
)
}

View file

@ -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"
)
}

View file

@ -0,0 +1,3 @@
package org.danielholmes.coc.baseanalyser.analysis
case class PlayerAnalysisReport(userName: String, townHallLevel: Int, villageReport: Option[AnalysisReport])

View file

@ -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"
)
}

View file

@ -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)"
)
}

View file

@ -0,0 +1,7 @@
package org.danielholmes.coc.baseanalyser.analysis
import org.danielholmes.coc.baseanalyser.model.Village
trait Rule {
def analyse(village: Village): RuleResult
}

View file

@ -0,0 +1,3 @@
package org.danielholmes.coc.baseanalyser.analysis
case class RuleDetails(code: String, shortName: String, name: String, description: String)

View file

@ -0,0 +1,7 @@
package org.danielholmes.coc.baseanalyser.analysis
trait RuleResult {
val success: Boolean
val ruleDetails: RuleDetails
}

View file

@ -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))
}
}

View file

@ -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
)
}

View file

@ -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]
}

View file

@ -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)
)
}
}

View file

@ -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)

View file

@ -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()
}
}
}

View file

@ -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]
}

View file

@ -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"))
)
}
)
}
}

View file

@ -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)"
}

View file

@ -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)
}
}
}
}

View file

@ -0,0 +1,3 @@
package org.danielholmes.coc.baseanalyser.model
trait Building extends Structure

View file

@ -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)
}

View file

@ -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]
}

View file

@ -0,0 +1,7 @@
package org.danielholmes.coc.baseanalyser.model
import org.scalactic.anyvals.PosInt
trait DelayedActivation extends Defense {
val deploymentSpaceRequired: PosInt
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -0,0 +1,3 @@
package org.danielholmes.coc.baseanalyser.model
trait Hidden extends Element

View file

@ -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"
}
}
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -0,0 +1,3 @@
package org.danielholmes.coc.baseanalyser.model
trait StationaryDefensiveBuilding extends Defense

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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})"
}
}

View file

@ -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)"
}
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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))
)
}
}
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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
}
}
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)
}

Some files were not shown because too many files have changed in this diff Show more