├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .scalafmt.conf
├── README.md
├── asset
├── architecture.svg
├── printed-state.png
├── react_impact.png
├── react_impact.svg
├── referee.svg
├── robot.png
└── screenshot.png
├── bin
└── playHead.sh
├── build.sbt
├── perf
└── caribbean.txt
├── project
├── build.properties
└── plugins.sbt
└── src
├── main
└── scala
│ └── com
│ └── truelaurel
│ ├── algorithm
│ ├── alphabeta
│ │ ├── AlphaBetaAi.scala
│ │ └── AlphaBetaAi2.scala
│ ├── dichotomy
│ │ └── Dichotomy.scala
│ ├── game
│ │ ├── GameRules.scala
│ │ ├── GameState.scala
│ │ ├── Outcome.scala
│ │ └── RulesFor2p.scala
│ ├── graph
│ │ ├── BreathFirstShortestPathFinder.scala
│ │ └── FloydWarshall.scala
│ ├── mcts
│ │ ├── MctsAi.scala
│ │ ├── MctsNode.scala
│ │ └── Results.scala
│ ├── metaheuristic
│ │ ├── evolutionstrategy
│ │ │ └── MuPlusLambda.scala
│ │ ├── genetic
│ │ │ ├── AssessedSolution.scala
│ │ │ ├── GeneticAlgorithm.scala
│ │ │ ├── GeneticRepresentation.scala
│ │ │ ├── RandomizationMutation.scala
│ │ │ └── UniformCrossover.scala
│ │ ├── hillclimbing
│ │ │ ├── HillClimbing.scala
│ │ │ ├── HillClimbingWithReplacement.scala
│ │ │ └── SteepestAscentHillClimbingWithReplacement.scala
│ │ ├── model
│ │ │ ├── Problem.scala
│ │ │ └── Solution.scala
│ │ ├── simulatedannealing
│ │ │ └── SimulatedAnnealing.scala
│ │ └── tweak
│ │ │ ├── BoundedVectorConvolution.scala
│ │ │ └── NoiseGenerators.scala
│ └── perf
│ │ ├── AlphaBetaPerf.scala
│ │ ├── DummyMcts.scala
│ │ ├── MctsDebugPerf.scala
│ │ └── MctsPerf.scala
│ ├── codingame
│ ├── challenge
│ │ ├── GameAccumulator.scala
│ │ ├── GameBot.scala
│ │ ├── GameIO.scala
│ │ ├── GameLoop.scala
│ │ ├── GameSimulator.scala
│ │ └── Undoer.scala
│ ├── logging
│ │ └── CGLogger.scala
│ └── tool
│ │ ├── benchmark
│ │ └── HelloWorldBenchmark.scala
│ │ └── bundle
│ │ ├── Bundler.scala
│ │ ├── BundlerIo.scala
│ │ └── BundlerMain.scala
│ ├── collection
│ ├── ArrayUtil.scala
│ └── Collectionsl.scala
│ ├── fp
│ ├── Unfold.scala
│ └── state
│ │ └── State.scala
│ ├── math
│ ├── Mathl.scala
│ └── geometry
│ │ ├── Circle.scala
│ │ ├── Pos.scala
│ │ ├── Vectorl.scala
│ │ ├── grid
│ │ ├── BitGrid.scala
│ │ ├── FastGrid.scala
│ │ ├── GridData.scala
│ │ └── Masks.scala
│ │ └── hexagons
│ │ ├── Cube.scala
│ │ └── Hexagons.scala
│ ├── physics
│ └── Collision.scala
│ ├── samplegames
│ ├── dummy
│ │ └── DummyRules.scala
│ ├── gomoku
│ │ ├── GomokuBoard.scala
│ │ ├── GomokuDemo.scala
│ │ └── GomokuRules.scala
│ ├── stones
│ │ ├── Demo3Stones.scala
│ │ ├── Move.scala
│ │ └── Rules.scala
│ └── wondev
│ │ ├── WondevChallenge.scala
│ │ ├── analysis
│ │ ├── WarFogCleaner.scala
│ │ └── WondevEvaluator.scala
│ │ ├── benchmark
│ │ └── WondevBenchmark.scala
│ │ ├── domain
│ │ ├── MutableWondevState.scala
│ │ ├── WondevAction.scala
│ │ ├── WondevContext.scala
│ │ └── WondevState.scala
│ │ ├── io
│ │ └── WondevIO.scala
│ │ ├── simulation
│ │ └── WondevSimulator.scala
│ │ ├── strategy
│ │ ├── WondevBotDebug.scala
│ │ └── WondevMinimax.scala
│ │ ├── warmup
│ │ └── WondevWarmup.scala
│ │ └── worksheet
│ │ └── wondev.sc
│ └── time
│ ├── Chronometer.scala
│ ├── CountStopper.scala
│ └── Stopper.scala
└── test
└── scala
└── com
└── truelaurel
├── algorithm
├── alphabeta
│ └── AlphaBetaAiTest.scala
├── dichotomy
│ └── DichotomyTest.scala
├── graph
│ ├── BreathFirstShortestPathTest.scala
│ └── ShortestPathTest.scala
├── mcts
│ ├── MctsNodeTest.scala
│ └── ResultsTest.scala
└── metaheuristic
│ ├── evolutionstrategy
│ ├── MuPlusLambdaTest.scala
│ ├── RockThrowingProblem.scala
│ └── RockThrowingSolution.scala
│ ├── genetic
│ ├── MoveSetGeneticRepresentation.scala
│ ├── Pod.scala
│ ├── Referee.scala
│ └── WayPoint.scala
│ └── simulatedannealing
│ └── LanderBasicProblem.scala
├── codingame
└── bundler
│ └── BundlerTest.scala
├── collection
└── CollectionslTest.scala
├── compilation
├── Compilation.scala
└── CompilationTest.scala
├── math
├── MathlTest.scala
└── geometry
│ ├── CircleTest.scala
│ ├── VectorlTest.scala
│ ├── grid
│ ├── BitGridSpec.scala
│ └── GridDataSpec.scala
│ └── hexagons
│ └── OffsetTest.scala
├── physics
└── DiskTest.scala
├── samplegames
├── gomoku
│ └── MctsGomokuTest.scala
└── wondev
│ ├── analysis
│ └── WarFogCleanerTest.scala
│ └── simulation
│ └── WondevSimulatorTest.scala
└── time
├── ChronometerTest.scala
└── CountStopperTest.scala
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on: [pull_request]
3 | jobs:
4 | build:
5 | runs-on: ubuntu-latest
6 | steps:
7 | - uses: actions/checkout@v1
8 | - uses: olafurpg/setup-scala@v5
9 | - name: Test
10 | run: sbt scalafmtCheckAll test
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | ### SBT template
3 | # Simple Build Tool
4 | # http://www.scala-sbt.org/release/docs/Getting-Started/Directories.html#configuring-version-control
5 |
6 | target/
7 | lib_managed/
8 | src_managed/
9 | project/boot/
10 | .history
11 | .cache
12 |
13 | ### JetBrains template
14 | .idea
15 |
16 | ### mac os
17 | .DS_Store
18 |
19 | ### Kit
20 |
21 | brutaltester
22 |
23 | ### Vi
24 | *.swp
25 |
26 |
--------------------------------------------------------------------------------
/.scalafmt.conf:
--------------------------------------------------------------------------------
1 | version = 2.5.3
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CodinGame Scala Kit
2 | I love Both CodinGame and Scala because they make programming fun.
3 |
4 | CodinGame is great and it would be better if I could
5 | - Separate codes into packages and files
6 | - Reuse existing codes
7 | - Generate the required Player file automatically
8 | - Avoid tedious inter-IDE copy/paste
9 | - Unit test the behavior of my Bot
10 | - Control source code versions with Git
11 | - Stay in my favorite IDE [Intellij](https://www.jetbrains.com/idea/)
12 |
13 | This kit achieves these goals through a source code Bundler which assembles source codes from different packages and files into a single one.
14 | Once we remove the constraint imposing us to code in one file, we can organize codes better and make them more reusable.
15 |
16 | With continuous building/running feature in SBT, we can generate the fat Player file as soon as code is modified.
17 | Thanks to CodinGame Sync, the generated bundle can be synchronized to the online IDE automatically.
18 |
19 | # Example Bot
20 | The example Bot [Ghost in the Cell](https://www.codingame.com/multiplayer/bot-programming/ghost-in-the-cell) ranked `59/3509` overall and `2/50` for Scala language.
21 |
22 | # Setting Up
23 |
24 | ## Pre-requisite
25 | - Intellij
26 | - Scala/SBT
27 | - Git
28 | - Chrome
29 | - CodinGame Sync
30 |
31 | ## Step-by-Step Guide
32 |
33 | 1. Clone the _CodinGame Scala Kit_ with Git
34 |
35 | `git clone https://github.com/truelaurel/CodinGame-Scala-Kit.git`
36 |
37 | 2. Import the SBT project in Intellij
38 | 3. Open a terminal and fire SBT, hit
39 |
40 | `~test` to compile and run unit tests continuously
41 |
42 | 4. Open a second terminal and fire SBT, hit
43 |
44 | `~runMain com.truelaurel.codingame.tool.bundle.BundlerMain GhostInTheCell.scala` to bundle destination file continuously
45 |
46 | 5. Open CodinGame Sync to synchronize Player.scala file continuously to online IDE
47 |
48 | ## CodinGame Scala Kit integration into your own git depo
49 |
50 | Using CodinGame Scala Kit as a git submodule inside your own git depot you can both store your solutions into
51 | a private depot, and still use and contribute to this scala kit.
52 | [All instructions are available within this example project](https://github.com/dacr/codingame-with-scalakit-example)
53 |
54 | ## Screenshot
55 | 
56 |
57 |
58 |
59 | # FAQ
60 | 1. How Bundler works?
61 |
62 | The Bundler reads a source file. Recursively, it replaces the import statements by source files found in this project.
63 |
64 | By default, the Bundler scans the `Player.scala` file from `src` folder and assembles all dependant source codes in a uber `Player.scala` file under `target` folder.
65 |
66 | 2. Can I reuse codes from third party?
67 |
68 | No, the Bundler only scans source files included in the project.
69 |
70 | 3. Is Java Supported?
71 |
72 | No, the Bundler only inlines imported source codes and doesn't adapt Java code to Scala.
73 | If you prefer Java, I strongly recommend Manwe's great [Competitive Programming](https://github.com/Manwe56/competitive-programming) tools.
74 |
75 | # More questions?
76 | Let's discuss it in the [CodinGame forum](https://www.codingame.com/forum/t/codingame-scala-kit/2645/1)!
77 |
--------------------------------------------------------------------------------
/asset/architecture.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/asset/printed-state.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huiwang/codingame-scala-kit/f270954fce9768c37f33d192b635dd89d776fb09/asset/printed-state.png
--------------------------------------------------------------------------------
/asset/react_impact.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huiwang/codingame-scala-kit/f270954fce9768c37f33d192b635dd89d776fb09/asset/react_impact.png
--------------------------------------------------------------------------------
/asset/react_impact.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/asset/referee.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/asset/robot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huiwang/codingame-scala-kit/f270954fce9768c37f33d192b635dd89d776fb09/asset/robot.png
--------------------------------------------------------------------------------
/asset/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/huiwang/codingame-scala-kit/f270954fce9768c37f33d192b635dd89d776fb09/asset/screenshot.png
--------------------------------------------------------------------------------
/bin/playHead.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # generate new bot at head
4 | sbt stage
5 |
6 | # update head
7 | cp -rf target/universal/stage/ brutaltester/head
8 |
9 | # run games locally
10 | cd brutaltester
11 | java -jar cg-brutaltester.jar -r "java -jar cg-ww.jar" -p1 "./head/bin/player" -p2 "./best/bin/player" -t 2 -n 100 -l "./logs/"
12 | cd -
--------------------------------------------------------------------------------
/build.sbt:
--------------------------------------------------------------------------------
1 | name := "CodinGame-Scala-Kit"
2 | version := "0.1.0"
3 | scalaVersion := "2.13.1"
4 |
5 | // solve a weird issue with java dependencies (trait Approving)
6 | // see https://stackoverflow.com/questions/43751394/package-cats-contains-object-and-package-with-same-name-implicits &
7 | // https://github.com/druid-io/tranquility/blob/master/build.sbt
8 | scalacOptions := Seq("-Yresolve-term-conflict:object")
9 |
10 |
11 | resolvers += Resolver.mavenLocal
12 |
13 | libraryDependencies += "org.scalatest" %% "scalatest" % "3.1.2" % "test"
14 | libraryDependencies += "com.github.writethemfirst" % "approvals-java" % "0.12.0" % "test"
15 | libraryDependencies += "org.scala-lang" % "scala-compiler" % "2.13.1" % "test"
16 | libraryDependencies += "org.scalameta" %% "scalafmt-core" % "2.5.2"
17 |
18 | enablePlugins(JmhPlugin)
19 | enablePlugins(JavaAppPackaging)
20 |
--------------------------------------------------------------------------------
/perf/caribbean.txt:
--------------------------------------------------------------------------------
1 | # Three rounds
2 |
3 | [info] Benchmark Mode Cnt Score Error Units
4 | [info] CaribbeanBenchmark.caribbean avgt 10 55.816 ± 1.463 ms/op
5 |
6 | [info] Benchmark Mode Cnt Score Error Units
7 | [info] CaribbeanBenchmark.caribbean avgt 10 54.277 ± 1.409 ms/op
8 |
9 | [info] Benchmark Mode Cnt Score Error Units
10 | [info] CaribbeanBenchmark.caribbean avgt 10 50.786 ± 1.053 ms/op
11 |
12 | [info] Benchmark Mode Cnt Score Error Units
13 | [info] CaribbeanBenchmark.caribbean avgt 10 47.541 ± 0.665 ms/op
14 |
15 | [info] Benchmark Mode Cnt Score Error Units
16 | [info] CaribbeanBenchmark.caribbean avgt 10 39.215 ± 7.869 ms/op
17 |
18 | [info] Benchmark Mode Cnt Score Error Units
19 | [info] CaribbeanBenchmark.caribbean avgt 10 36.720 ± 0.775 ms/op
20 |
21 | [info] Benchmark Mode Cnt Score Error Units
22 | [info] CaribbeanBenchmark.caribbean avgt 10 34.602 ± 0.549 ms/op
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version = 1.3.12
2 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | logLevel := Level.Warn
2 | addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.3")
3 | addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.3")
4 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.0")
5 |
6 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/algorithm/alphabeta/AlphaBetaAi.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.alphabeta
2 |
3 | import com.truelaurel.algorithm.game._
4 |
5 | import scala.annotation.tailrec
6 |
7 | /**
8 | * @param heuristic must represent higher chance of success for state.nextPlayer
9 | */
10 | case class AlphaBetaAi[S <: GameState[Boolean], M](
11 | rules: RulesFor2p[S, M],
12 | heuristic: S => Double
13 | ) {
14 |
15 | val MIN = Double.MinValue
16 | val MAX = Double.MaxValue
17 |
18 | def chooseMove(state: S, depth: Int): M = {
19 | val sorted = sortedMoves(state)
20 | best(sorted, MIN, MAX, state, depth).move.getOrElse(sorted.head)
21 | }
22 |
23 | def sortedMoves(state: S): Seq[M] = {
24 | rules.validMoves(state).sortBy(m => heuristic(rules.applyMove(state, m)))
25 | }
26 |
27 | def negamax(state: S, depth: Int, alpha: Double, beta: Double): Double = {
28 | val player = state.nextPlayer
29 | rules.outcome(state) match {
30 | case Wins(`player`) => MAX
31 | case Wins(_) => MIN
32 | case Draw => 0
33 | case Undecided =>
34 | val moves = sortedMoves(state)
35 | if (depth == 0 || moves.isEmpty) heuristic(state)
36 | else best(moves, alpha, beta, state, depth).score
37 | }
38 | }
39 |
40 | @tailrec
41 | final def best(
42 | moves: Seq[M],
43 | alpha: Double,
44 | beta: Double,
45 | state: S,
46 | depth: Int,
47 | currentBest: Option[M] = None
48 | ): ScoredMove = {
49 | if (beta > alpha && moves.nonEmpty) {
50 | val move = moves.head
51 | val nextState = rules.applyMove(state, move)
52 | val evaluation = -negamax(nextState, depth - 1, -beta, -alpha)
53 | val newBest = if (evaluation > alpha) Some(move) else currentBest
54 | best(
55 | moves.tail,
56 | alpha max evaluation,
57 | beta max evaluation,
58 | state,
59 | depth,
60 | newBest
61 | )
62 | } else ScoredMove(alpha, currentBest)
63 | }
64 |
65 | case class ScoredMove(score: Double, move: Option[M])
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/algorithm/alphabeta/AlphaBetaAi2.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.alphabeta
2 |
3 | import com.truelaurel.algorithm.game._
4 | import com.truelaurel.samplegames.wondev.domain.{
5 | MutableWondevState,
6 | WondevAction
7 | }
8 |
9 | /**
10 | * @param heuristic must represent higher chance of success for state.nextPlayer
11 | */
12 | case class AlphaBetaAi2(
13 | rules: RulesFor2p[MutableWondevState, WondevAction],
14 | heuristic: MutableWondevState => Double,
15 | moveHeuristc: (WondevAction, MutableWondevState) => Double
16 | ) {
17 |
18 | type S = MutableWondevState
19 | type M = WondevAction
20 |
21 | val MIN = Double.MinValue
22 | val MAX = Double.MaxValue
23 |
24 | def bestMove(state: S, depth: Int): M = {
25 | val moves = rules.validMoves(state).sortBy(moveHeuristc(_, state))
26 |
27 | var value = MIN
28 | var i = 0
29 | var currentAlpha = MIN
30 | var bestMove = null.asInstanceOf[M]
31 | while (i < moves.size) {
32 | val move = moves(i)
33 |
34 | val valueForMove = alphabeta(
35 | rules.applyMove(state, move),
36 | depth - 1,
37 | currentAlpha,
38 | MAX,
39 | maximizingPlayer = false
40 | )
41 | state.writable.undo()
42 | if (valueForMove > value) {
43 | bestMove = move
44 | value = valueForMove
45 | currentAlpha = valueForMove
46 | }
47 | i += 1
48 | }
49 | bestMove
50 | }
51 |
52 | def alphabeta(
53 | state: S,
54 | depth: Int,
55 | alpha: Double,
56 | beta: Double,
57 | maximizingPlayer: Boolean
58 | ): Double = {
59 | if (depth == 0) {
60 | heuristic(state)
61 | } else {
62 | val moves = rules.validMoves(state).sortBy(m => moveHeuristc(m, state))
63 | if (moves.isEmpty) {
64 | heuristic(state)
65 | } else {
66 | if (maximizingPlayer) {
67 | var value = MIN
68 | var i = 0
69 | var currentAlpha = alpha
70 | while (i < moves.size) {
71 | val move = moves(i)
72 | value = Math.max(
73 | value,
74 | alphabeta(
75 | rules.applyMove(state, move),
76 | depth - 1,
77 | currentAlpha,
78 | beta,
79 | maximizingPlayer = false
80 | )
81 | )
82 | state.writable.undo()
83 | currentAlpha = Math.max(currentAlpha, value)
84 | if (beta <= currentAlpha)
85 | return value
86 | i += 1
87 | }
88 | value
89 | } else {
90 | var value = MAX
91 | var i = 0
92 | var currentBeta = beta
93 | while (i < moves.size) {
94 | val move = moves(i)
95 | value = Math.min(
96 | value,
97 | alphabeta(
98 | rules.applyMove(state, move),
99 | depth - 1,
100 | alpha,
101 | currentBeta,
102 | maximizingPlayer = true
103 | )
104 | )
105 | state.writable.undo()
106 | currentBeta = Math.min(currentBeta, value)
107 | if (currentBeta <= alpha)
108 | return value
109 | i += 1
110 | }
111 | value
112 | }
113 | }
114 | }
115 | }
116 |
117 | case class ScoredMove(score: Double, move: Option[M])
118 |
119 | }
120 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/algorithm/dichotomy/Dichotomy.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.dichotomy
2 |
3 | object Dichotomy {
4 |
5 | /**
6 | * Find a target value between the lower and upper bound where we can't go lower anymore
7 | *
8 | * @param low lower bound
9 | * @param high upper bound
10 | * @param lower evaluation function guiding if we should go lower by taking the lower half interval
11 | * @return the target value where we can't go lower anymore
12 | */
13 | def search(low: Int, high: Int, lower: Int => Boolean): Int = {
14 | if (low == high) low
15 | else {
16 | val mid = (low + high) / 2
17 | if (lower(mid)) {
18 | search(low, mid, lower)
19 | } else {
20 | search(mid + 1, high, lower)
21 | }
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/algorithm/game/GameRules.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.game
2 |
3 | import scala.annotation.tailrec
4 | import scala.util.Random
5 |
6 | trait GameRules[P, S <: GameState[P], M] {
7 | def initial: S
8 |
9 | def validMoves(state: S): Seq[M]
10 |
11 | def applyMove(state: S, move: M): S
12 |
13 | def outcome(state: S): Outcome[P]
14 |
15 | @tailrec
16 | final def judge(
17 | players: Map[P, S => M],
18 | debug: S => Unit,
19 | state: S = initial
20 | ): Outcome[P] = {
21 | debug(state)
22 | outcome(state) match {
23 | case Undecided =>
24 | val p = players(state.nextPlayer)
25 | val m = p(state)
26 | judge(players, debug, applyMove(state, m))
27 | case o => o
28 | }
29 | }
30 |
31 | def randomMove(s: S): M = {
32 | val moves = validMoves(s)
33 | require(moves.nonEmpty, "no valid moves in state " + s)
34 | moves(Random.nextInt(moves.size))
35 | }
36 |
37 | def randomPlay(state: S): Outcome[P] =
38 | playUntilEnd(randomMove)(state)
39 |
40 | def playUntilEnd(selectMove: S => M)(state: S): Outcome[P] = {
41 | @tailrec
42 | def playRec(s: S): Outcome[P] = {
43 | outcome(s) match {
44 | case Undecided => playRec(applyMove(s, selectMove(s)))
45 | case decided => decided
46 | }
47 | }
48 |
49 | playRec(state)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/algorithm/game/GameState.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.game
2 |
3 | trait GameState[P] {
4 | def nextPlayer: P
5 | }
6 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/algorithm/game/Outcome.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.game
2 |
3 | sealed trait Outcome[+P]
4 |
5 | case class Wins[P](p: P) extends Outcome[P]
6 |
7 | case object Undecided extends Outcome[Nothing]
8 |
9 | case object Draw extends Outcome[Nothing]
10 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/algorithm/game/RulesFor2p.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.game
2 |
3 | trait RulesFor2p[S <: GameState[Boolean], M] extends GameRules[Boolean, S, M] {
4 |
5 | def judge(
6 | truePl: S => M,
7 | falsePl: S => M,
8 | debug: S => Unit
9 | ): Outcome[Boolean] =
10 | judge(Map(true -> truePl, false -> falsePl), debug, initial)
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/algorithm/graph/BreathFirstShortestPathFinder.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.graph
2 |
3 | class BreathFirstShortestPathFinder[T](
4 | graph: Map[T, Vector[T]],
5 | obstacles: Set[T]
6 | ) {
7 | //elements at flood front and path toward it from search source
8 | type Paths = Map[T, Vector[T]]
9 |
10 | def findPath(source: T, target: T): Vector[T] = {
11 | findPaths(source, Set(target)).getOrElse(source, Vector.empty)
12 | }
13 |
14 | def findPaths(source: T, targets: Set[T]): Paths = {
15 | path(targets, Map(source -> Vector.empty), Set.empty, Map.empty)
16 | .mapValues(_.reverse)
17 | .toMap
18 | }
19 |
20 | private def path(
21 | targets: Set[T],
22 | paths: Paths,
23 | visited: Set[T],
24 | found: Paths
25 | ): Paths = {
26 | if (targets.isEmpty) {
27 | found
28 | } else {
29 | val reached = paths.keySet
30 | if (reached.subsetOf(visited)) {
31 | //nothing more to explore
32 | found
33 | } else {
34 | val reachedTargets = reached.intersect(targets)
35 | path(
36 | targets -- reachedTargets,
37 | newNeighborsWithHistory(paths, visited),
38 | reached ++ visited,
39 | found ++ paths.filterKeys(reachedTargets)
40 | )
41 | }
42 | }
43 | }
44 |
45 | private def newNeighborsWithHistory(
46 | fringes: Paths,
47 | visited: Set[T]
48 | ): Paths = {
49 | fringes.flatMap({
50 | case (elem, history) =>
51 | graph
52 | .getOrElse(elem, Vector.empty)
53 | .filterNot(visited)
54 | .filterNot(obstacles)
55 | .map(neighbor => (neighbor, neighbor +: history))
56 | .toMap
57 | })
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/algorithm/graph/FloydWarshall.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.graph
2 |
3 | import scala.collection.mutable
4 | import scala.collection.mutable.ArrayBuffer
5 |
6 | case class Edge(from: Int, to: Int, distance: Int)
7 |
8 | case class Iti(distance: Int, path: Vector[Int])
9 |
10 | /**
11 | * Floyd–Warshall algorithm
12 | */
13 | object ShortestPath {
14 | def shortestItinearies(
15 | vertexCount: Int,
16 | edges: Vector[Edge]
17 | ): Map[Int, Map[Int, Iti]] = {
18 | val n = vertexCount
19 | val inf = Int.MaxValue
20 |
21 | // Initialize distance matrix.
22 | val ds = Array.fill[Int](n, n)(inf)
23 | for (i <- 0 until n) ds(i)(i) = 0
24 | edges.foreach(e => ds(e.from)(e.to) = e.distance)
25 | // Initialize next vertex matrix.
26 | val ns = Array.fill[Int](n, n)(-1)
27 |
28 | // Here goes the magic!
29 | for (k <- 0 until n; i <- 0 until n; j <- 0 until n)
30 | if (
31 | ds(i)(k) != inf && ds(k)(j) != inf && ds(i)(k) + ds(k)(j) < ds(i)(j)
32 | ) {
33 | ds(i)(j) = ds(i)(k) + ds(k)(j)
34 | ns(i)(j) = k
35 | }
36 |
37 | // Helper function to carve out paths from the next vertex matrix.
38 | def extractPath(path: ArrayBuffer[Int], i: Int, j: Int) {
39 | if (ds(i)(j) == inf) return
40 | val k = ns(i)(j)
41 | if (k != -1) {
42 | extractPath(path, i, k)
43 | path.append(k)
44 | extractPath(path, k, j)
45 | }
46 | }
47 |
48 | // Extract paths.
49 | val itenearies = mutable.Map[Int, Map[Int, ArrayBuffer[Int]]]()
50 | for (i <- 0 until n) {
51 | val ps = mutable.Map[Int, ArrayBuffer[Int]]()
52 | for (j <- 0 until n)
53 | if (ds(i)(j) != inf) {
54 | val p = new ArrayBuffer[Int]()
55 | p.append(i)
56 | if (i != j) {
57 | extractPath(p, i, j)
58 | p.append(j)
59 | }
60 | ps(j) = p
61 | }
62 | itenearies(i) = ps.toMap
63 | }
64 |
65 | // Return extracted paths.
66 | itenearies.map {
67 | case (from, dests) => {
68 | from -> dests.map {
69 | case (dest, path) => {
70 | val (totalDist, _) = path.foldLeft(0, from) {
71 | case ((total, pre), cur) =>
72 | (total + ds(pre)(cur), cur)
73 | }
74 | dest -> Iti(totalDist, path.toVector)
75 | }
76 | }
77 | }
78 | }.toMap
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/algorithm/mcts/MctsAi.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.mcts
2 |
3 | import com.truelaurel.algorithm.game.{GameRules, GameState, Outcome}
4 |
5 | import scala.annotation.tailrec
6 |
7 | case class MctsAi[P, S <: GameState[P], M](rules: GameRules[P, S, M])(
8 | stopCondition: MctsNode[P, S, M] => Boolean,
9 | randomPlay: S => Outcome[P] = rules.randomPlay _
10 | ) {
11 |
12 | def chooseMove(state: S): M = {
13 | @tailrec
14 | def iterate(node: MctsNode[P, S, M]): M =
15 | if (stopCondition(node)) {
16 | node.bestMove
17 | } else iterate(node.step)
18 |
19 | iterate(makeNode(state))
20 | }
21 |
22 | def chooseMoveCount(state: S): (M, Int) = {
23 | @tailrec
24 | def iterate(node: MctsNode[P, S, M], nodes: Int = 0): (M, Int) =
25 | if (stopCondition(node)) {
26 | (node.bestMove, nodes)
27 | } else iterate(node.step, nodes + 1)
28 |
29 | iterate(makeNode(state))
30 | }
31 |
32 | def makeNode(state: S): MctsNode[P, S, M] =
33 | MctsNode[P, S, M](state, rules, randomPlay)
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/algorithm/mcts/MctsNode.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.mcts
2 |
3 | import com.truelaurel.algorithm.game._
4 |
5 | import scala.annotation.tailrec
6 |
7 | case class MctsNode[P, State <: GameState[P], Move](
8 | state: State,
9 | rules: GameRules[P, State, Move],
10 | randomPlay: State => Outcome[P],
11 | results: Results = Results(),
12 | children: Map[Move, MctsNode[P, State, Move]] =
13 | Map.empty[Move, MctsNode[P, State, Move]]
14 | ) {
15 |
16 | def bestMove: Move =
17 | children.mapValues(_.results.score).minBy(_._2)._1
18 |
19 | @tailrec
20 | final def steps(count: Int): MctsNode[P, State, Move] =
21 | if (count == 0) this
22 | else step.steps(count - 1)
23 |
24 | def step: MctsNode[P, State, Move] =
25 | deepStep._2
26 |
27 | def moveToExplore: Move = {
28 | val validMoves = rules.validMoves(state)
29 | validMoves.find(m => children.get(m).isEmpty) match {
30 | case Some(unexploredMove) => unexploredMove
31 | case None =>
32 | val movesResults = validMoves.map(m => m -> children(m).results)
33 | Results.mostPromisingMove(movesResults)
34 | }
35 | }
36 |
37 | def debugText: String = {
38 | for {
39 | (move, n) <- children
40 | r @ Results(played, _) = n.results
41 | wins = r.won.toInt
42 | } yield s"$wins/$played for $move\n"
43 | }.mkString
44 |
45 | private def deepStep: (Outcome[P], MctsNode[P, State, Move]) = {
46 | rules.outcome(state) match {
47 | case Undecided =>
48 | val move = moveToExplore
49 | val (outcome, updatedChild) = children.get(move) match {
50 | case Some(child) =>
51 | child.deepStep
52 | case None =>
53 | expand(move)
54 | }
55 | (
56 | outcome,
57 | copy(
58 | results = results.withOutcome(state.nextPlayer, outcome),
59 | children = children + (move -> updatedChild)
60 | )
61 | )
62 | case decided =>
63 | (
64 | decided,
65 | copy(results = results.withOutcome(state.nextPlayer, decided))
66 | )
67 | }
68 | }
69 |
70 | private def expand(move: Move): (Outcome[P], MctsNode[P, State, Move]) = {
71 | val nextState = rules.applyMove(state, move)
72 | val outcome = rules.outcome(nextState) match {
73 | case Undecided => randomPlay(nextState)
74 | case decided => decided
75 | }
76 | val childNode = copy(
77 | state = nextState,
78 | results = Results().withOutcome(state.nextPlayer, outcome),
79 | children = Map.empty[Move, MctsNode[P, State, Move]]
80 | )
81 | (outcome, childNode)
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/algorithm/mcts/Results.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.mcts
2 |
3 | import com.truelaurel.algorithm.game.{Outcome, Wins}
4 |
5 | // score is delta of myWins - otherWins
6 | case class Results(played: Int = 0, score: Int = 0) {
7 |
8 | def withOutcome(outcome: Outcome[Boolean]): Results =
9 | withOutcome(true, outcome)
10 |
11 | def withOutcome[P](player: P, outcome: Outcome[P]): Results =
12 | copy(
13 | played = played + 1,
14 | score = score + (outcome match {
15 | case Wins(`player`) => 1
16 | case Wins(_) => -1
17 | case _ => 0
18 | })
19 | )
20 |
21 | def uct(total: Int): Double = {
22 | if (played == 0) Double.MaxValue
23 | else lost / played + math.sqrt(2 * math.log(total) / played)
24 | }
25 |
26 | def lost: Double = (played - score) / 2.0
27 | def won: Double = (played + score) / 2.0
28 |
29 | /*
30 | Ex :
31 | 5 for true
32 | 2 for false
33 | score = 3
34 | played = 7
35 |
36 |
37 | Demonstration :
38 |
39 | s = t-f
40 | p = t+f
41 |
42 | t = s+f
43 | t = p -f
44 | s+f = p-f
45 | 2f = p-s
46 | f = (p-s)/2
47 | t = (p+s)/2
48 |
49 | */
50 | }
51 |
52 | object Results {
53 | // faster using a (sorted) TreeMap ?
54 | // results must be a non empty map
55 | def mostPromisingMove[M](results: Iterable[(M, Results)]): M = {
56 | val total = results.map(_._2.played).sum
57 | results.maxBy(_._2.uct(total))._1
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/algorithm/metaheuristic/evolutionstrategy/MuPlusLambda.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.metaheuristic.evolutionstrategy
2 |
3 | import com.truelaurel.algorithm.metaheuristic.model.{Problem, Solution}
4 | import com.truelaurel.time.Stopper
5 |
6 | /**
7 | *
8 | * @param lambda number of children generated by the parents
9 | * @param mu number of parents selected
10 | */
11 | class MuPlusLambda(mu: Int, lambda: Int, stopper: Stopper) {
12 | require(lambda > 0)
13 | require(mu > 0 && mu <= lambda)
14 |
15 | private val parentsRange = 0 until lambda
16 | private val tweakedRange = 0 until lambda / mu
17 |
18 | def search[S <: Solution](problem: Problem[S]): S = {
19 | stopper.start()
20 | var parents = parentsRange
21 | .map(_ => problem.randomSolution())
22 | .map(s => (s, s.quality))
23 | .sortBy(_._2)
24 | .map(_._1)
25 | var bestSolution = parents.last
26 | while (!stopper.willOutOfTime) {
27 | //truncation selection
28 | val greatest = parents
29 | .map(s => (s, s.quality))
30 | .sortBy(_._2)
31 | .map(_._1)
32 | .takeRight(mu)
33 | bestSolution = if (bestSolution.quality > greatest.last.quality) {
34 | bestSolution
35 | } else greatest.last
36 | parents = greatest ++ greatest.flatMap(s =>
37 | tweakedRange.map(_ => problem.tweakSolution(s))
38 | )
39 | }
40 | bestSolution
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/algorithm/metaheuristic/genetic/AssessedSolution.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.metaheuristic.genetic
2 |
3 | /**
4 | * We don't use case class because neither hashcode nor equal is needed
5 | */
6 | final class AssessedSolution[S](val solution: S, val quality: Double)
7 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/algorithm/metaheuristic/genetic/GeneticAlgorithm.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.metaheuristic.genetic
2 |
3 | import com.truelaurel.math.Mathl
4 | import com.truelaurel.time.Stopper
5 |
6 | class GeneticAlgorithm[S](
7 | popSize: Int,
8 | tournamentSize: Int,
9 | eliteSize: Int,
10 | stopper: Stopper
11 | ) {
12 | require(popSize % 2 == 0, "population size must be even")
13 | require(eliteSize % 2 == 0, "elite size must be even")
14 | require(tournamentSize > 0, "tournament size must be greater than zero")
15 |
16 | private val fullPop = (0 until popSize).toVector
17 | private val halfPop = (0 until (popSize - eliteSize) / 2).toVector
18 |
19 | def search(repr: GeneticRepresentation[S]): S = {
20 | stopper.start()
21 | var parents =
22 | fullPop.map(_ => repr.assess(repr.randomSolution)).sortBy(_.quality)
23 | var best = null.asInstanceOf[AssessedSolution[S]]
24 |
25 | while (!stopper.willOutOfTime) {
26 | best = better(best, parents.last)
27 |
28 | parents = (halfPop.flatMap(_ => {
29 | val parentA = tournamentSelect(parents)
30 | val parentB = tournamentSelect(parents)
31 | val (childA, childB) = repr.crossover(parentA, parentB)
32 | val mutatedA = repr.mutate(childA)
33 | val mutatedB = repr.mutate(childB)
34 | val assessedA = repr.assess(mutatedA)
35 | val assessedB = repr.assess(mutatedB)
36 | Vector(assessedA, assessedB)
37 | }) ++ parents.takeRight(eliteSize)).sortBy(_.quality)
38 | }
39 | best.solution
40 | }
41 |
42 | private def better(
43 | bestSoFar: AssessedSolution[S],
44 | bestInGeneration: AssessedSolution[S]
45 | ) = {
46 | if (bestSoFar == null || bestInGeneration.quality > bestSoFar.quality)
47 | bestInGeneration
48 | else bestSoFar
49 | }
50 |
51 | private def tournamentSelect(population: Vector[AssessedSolution[S]]) = {
52 | var best = pickRandomIndividual(population)
53 | var i = 2
54 | while (i <= tournamentSize) {
55 | val next = pickRandomIndividual(population)
56 | if (next.quality > best.quality) best = next
57 | i = i + 1
58 | }
59 | best.solution
60 | }
61 |
62 | private def pickRandomIndividual[T](population: Vector[T]) = {
63 | population(Mathl.random.nextInt(popSize))
64 | }
65 |
66 | }
67 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/algorithm/metaheuristic/genetic/GeneticRepresentation.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.metaheuristic.genetic
2 |
3 | trait GeneticRepresentation[S] {
4 |
5 | def randomSolution: S
6 |
7 | def crossover(solutionA: S, solutionB: S): (S, S)
8 |
9 | def mutate(solution: S): S
10 |
11 | def assess(solution: S): AssessedSolution[S]
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/algorithm/metaheuristic/genetic/RandomizationMutation.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.metaheuristic.genetic
2 |
3 | import com.truelaurel.math.Mathl
4 |
5 | /**
6 | * Created by hwang on 27/05/2017.
7 | */
8 | object RandomizationMutation {
9 |
10 | /**
11 | * @param v vector to be mutated
12 | * @param randomLegalElem function that provides a random legal elem
13 | * @param p mutation probability, usually lower than 1/l where l is the length of input vector
14 | * @tparam T type of element
15 | * @return randomly mutated vector
16 | */
17 | def mutate[T](
18 | v: Vector[T],
19 | randomLegalElem: Int => T,
20 | p: Double
21 | ): Vector[T] = {
22 | v.zipWithIndex.map(pair =>
23 | if (p >= Mathl.random.nextDouble()) randomLegalElem(pair._2) else pair._1
24 | )
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/algorithm/metaheuristic/genetic/UniformCrossover.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.metaheuristic.genetic
2 |
3 | import com.truelaurel.math.Mathl
4 |
5 | /**
6 | * Created by hwang on 27/05/2017.
7 | */
8 | object UniformCrossover {
9 |
10 | def crossover[T](
11 | v: Vector[T],
12 | w: Vector[T],
13 | p: Double
14 | ): (Vector[T], Vector[T]) = {
15 | require(v.size == w.size, "two vectors should have the same size")
16 | v.zip(w)
17 | .map(pair => if (p >= Mathl.random.nextDouble()) pair.swap else pair)
18 | .unzip
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/algorithm/metaheuristic/hillclimbing/HillClimbing.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.metaheuristic.hillclimbing
2 |
3 | import com.truelaurel.algorithm.metaheuristic.model.{Problem, Solution}
4 | import com.truelaurel.time.Chronometer
5 |
6 | import scala.concurrent.duration.Duration
7 |
8 | /**
9 | * Only better solution leads to new exploration
10 | */
11 | class HillClimbing(duration: Duration) {
12 |
13 | val chrono = new Chronometer(duration)
14 |
15 | def search[S <: Solution](problem: Problem[S]): S = {
16 | var solution = problem.randomSolution()
17 | chrono.start()
18 | while (!chrono.willOutOfTime) {
19 | val tweaked = problem.tweakSolution(solution)
20 | solution = if (tweaked.quality > solution.quality) tweaked else solution
21 | }
22 | solution
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/algorithm/metaheuristic/hillclimbing/HillClimbingWithReplacement.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.metaheuristic.hillclimbing
2 |
3 | import com.truelaurel.algorithm.metaheuristic.model.{Problem, Solution}
4 | import com.truelaurel.time.Chronometer
5 |
6 | import scala.concurrent.duration.Duration
7 |
8 | /**
9 | * Replace next starting solution directly with the tweaked solution
10 | * This allows to make more exploration even the tweaked one is not better
11 | */
12 | class HillClimbingWithReplacement(duration: Duration) {
13 |
14 | val chrono = new Chronometer(duration)
15 |
16 | def search[S <: Solution](problem: Problem[S]): S = {
17 | var solution = problem.randomSolution()
18 | var bestSolution = solution
19 | chrono.start()
20 | while (!chrono.willOutOfTime) {
21 | solution = problem.tweakSolution(solution)
22 | if (solution.quality > bestSolution.quality) {
23 | bestSolution = solution
24 | }
25 | }
26 | bestSolution
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/algorithm/metaheuristic/hillclimbing/SteepestAscentHillClimbingWithReplacement.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.metaheuristic.hillclimbing
2 |
3 | import com.truelaurel.algorithm.metaheuristic.model.{Problem, Solution}
4 | import com.truelaurel.time.Chronometer
5 |
6 | import scala.concurrent.duration.Duration
7 |
8 | /**
9 | * By having a high selection pressure, we eliminate many poor solutions and stick with the best one.
10 | * Therefore, we can have more exploitation with a high pressure.
11 | */
12 | class SteepestAscentHillClimbingWithReplacement(
13 | duration: Duration,
14 | selectionPressure: Int
15 | ) {
16 |
17 | val chrono = new Chronometer(duration)
18 | val selectionRange: Range = 0 until selectionPressure
19 |
20 | def search[S <: Solution](problem: Problem[S]): S = {
21 | var solution = problem.randomSolution()
22 | var bestSolution = solution
23 | chrono.start()
24 | while (!chrono.willOutOfTime) {
25 | solution = selectionRange
26 | .map(_ => problem.tweakSolution(solution))
27 | .maxBy(_.quality)
28 | if (solution.quality > bestSolution.quality) {
29 | bestSolution = solution
30 | }
31 | }
32 | bestSolution
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/algorithm/metaheuristic/model/Problem.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.metaheuristic.model
2 |
3 | /**
4 | * Representation of a candidate solution. It defines how to initialize and tweak a candidate.
5 | */
6 | trait Problem[S <: Solution] {
7 |
8 | def randomSolution(): S
9 |
10 | def tweakSolution(solution: S): S
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/algorithm/metaheuristic/model/Solution.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.metaheuristic.model
2 |
3 | /**
4 | * Created by hwang on 13/02/2017.
5 | */
6 | trait Solution {
7 | def quality: Double
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/algorithm/metaheuristic/simulatedannealing/SimulatedAnnealing.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.metaheuristic.simulatedannealing
2 |
3 | import com.truelaurel.algorithm.metaheuristic.model.{Problem, Solution}
4 | import com.truelaurel.time.Chronometer
5 |
6 | import scala.concurrent.duration.Duration
7 | import scala.util.Random
8 |
9 | class SimulatedAnnealing(
10 | duration: Duration,
11 | initTemperature: Double,
12 | coolingRate: Double
13 | ) {
14 | val chronometer = new Chronometer(duration)
15 | val random = Random
16 |
17 | def search[S <: Solution](problem: Problem[S]): S = {
18 | var solution = problem.randomSolution()
19 | var best = solution
20 | var t = initTemperature
21 | chronometer.start()
22 | while (!chronometer.willOutOfTime) {
23 | val tweaked = problem.tweakSolution(solution)
24 | if (
25 | tweaked.quality > solution.quality ||
26 | random.nextDouble() < Math.exp((tweaked.quality - solution.quality) / t)
27 | ) {
28 | solution = tweaked
29 | }
30 |
31 | if (solution.quality > best.quality) {
32 | best = solution
33 | }
34 |
35 | t *= coolingRate
36 | }
37 | best
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/algorithm/metaheuristic/tweak/BoundedVectorConvolution.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.metaheuristic.tweak
2 |
3 | import com.truelaurel.math.Mathl
4 |
5 | /**
6 | * Created by hwang on 12/02/2017.
7 | */
8 | class BoundedVectorConvolution(
9 | noiseProbability: Double,
10 | min: Double,
11 | max: Double
12 | ) {
13 |
14 | def tweak(v: Vector[Double], noiseGenerator: () => Double): Vector[Double] = {
15 | v.map(elem => {
16 | if (noiseProbability >= Mathl.random.nextDouble()) {
17 | var tweaked = elem
18 | do {
19 | tweaked = noiseGenerator.apply() + elem
20 | } while (tweaked < min || tweaked > max)
21 | tweaked
22 | } else {
23 | elem
24 | }
25 | })
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/algorithm/metaheuristic/tweak/NoiseGenerators.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.metaheuristic.tweak
2 |
3 | import com.truelaurel.math.Mathl
4 |
5 | /**
6 | * Created by hwang on 12/02/2017.
7 | */
8 | object NoiseGenerators {
9 |
10 | type G = () => Double
11 |
12 | def uniform(halfRange: Double, center: Double = 0): G =
13 | () => center + Mathl.randomBetween(-halfRange, halfRange)
14 |
15 | def gaussian(mean: Double, stdDerivation: Double): G =
16 | () => mean + stdDerivation * Mathl.random.nextGaussian()
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/algorithm/perf/AlphaBetaPerf.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.perf
2 |
3 | import com.truelaurel.algorithm.alphabeta.AlphaBetaAi
4 | import com.truelaurel.math.geometry.Pos
5 | import com.truelaurel.samplegames.dummy.{DummyBoard, DummyRules}
6 | import com.truelaurel.samplegames.gomoku.GomokuRules
7 | import org.openjdk.jmh.annotations._
8 |
9 | import scala.util.Random
10 |
11 | @State(Scope.Benchmark)
12 | class AlphaBetaPerf {
13 | val rules = DummyRules(20, 10)
14 | val rules74 = GomokuRules(7, 4)
15 | val rules33 = GomokuRules(3, 3)
16 |
17 | @Benchmark
18 | def dummyDepth3(): Int = {
19 | AlphaBetaAi(rules, heuristic).chooseMove(rules.initial, 3)
20 | }
21 |
22 | @Benchmark
23 | def dummyDepth5(): Int = {
24 | AlphaBetaAi(rules, heuristic).chooseMove(rules.initial, 5)
25 | }
26 |
27 | def heuristic(s: DummyBoard): Double = Random.nextDouble
28 |
29 | @Benchmark
30 | def gomoku74Depth2(): Pos = {
31 | AlphaBetaAi(rules74, rules74.centerHeuristic).chooseMove(rules74.initial, 2)
32 | }
33 |
34 | @Benchmark
35 | def gomoku33Depth9(): Pos = {
36 | AlphaBetaAi(rules33, rules33.centerHeuristic).chooseMove(rules33.initial, 9)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/algorithm/perf/DummyMcts.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.perf
2 |
3 | import com.truelaurel.algorithm.mcts.MctsNode
4 | import com.truelaurel.samplegames.dummy.DummyRules
5 |
6 | object DummyMcts {
7 | type DummyMove = Int
8 | val rules = DummyRules()
9 | val dummyRoot = MctsNode(rules.initial, rules, rules.randomPlay)
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/algorithm/perf/MctsDebugPerf.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.perf
2 |
3 | object MctsDebugPerf {
4 | def main(args: Array[String]): Unit = {
5 | DummyMcts.dummyRoot.steps(1000000)
6 | }
7 |
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/algorithm/perf/MctsPerf.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.perf
2 |
3 | import com.truelaurel.algorithm.mcts.MctsNode
4 | import com.truelaurel.samplegames.gomoku.GomokuRules
5 | import org.openjdk.jmh.annotations._
6 |
7 | @State(Scope.Benchmark)
8 | class MctsPerf {
9 |
10 | val rules3 = GomokuRules(3, 3)
11 | val gomoku3 = new MctsNode(rules3.initial, rules3, rules3.randomPlay)
12 | val rules7 = GomokuRules(7, 5)
13 | val gomoku7 = new MctsNode(rules7.initial, rules7, rules7.randomPlay)
14 |
15 | @Benchmark
16 | def gomoku3in100steps() = {
17 | gomoku3.steps(100)
18 | }
19 |
20 | @Benchmark
21 | def gomoku7in100steps() = {
22 | gomoku7.steps(100)
23 | }
24 |
25 | @Benchmark
26 | def dummy100steps() = {
27 | DummyMcts.dummyRoot.steps(100)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/codingame/challenge/GameAccumulator.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.codingame.challenge
2 |
3 | /**
4 | * Created by hwang on 09/07/2017.
5 | */
6 | trait GameAccumulator[Context, State, Action] {
7 |
8 | /**
9 | * Accumulates information derived from the current state and selected actions into a new game context that will be
10 | * used in the next round.
11 | *
12 | * In certain cases, the input state doesn't include all known information. These information must be calculated from
13 | * historical actions and states. For example, it could action cool down, previously observed positions in fog of war.
14 | *
15 | * @param context the current context which may contain historical events.
16 | * @param state the current state
17 | * @param action actions performed for the current round
18 | * @return a new context accumulated with historical events including those generated from the current round
19 | */
20 | def accumulate(context: Context, state: State, action: Action): Context
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/codingame/challenge/GameBot.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.codingame.challenge
2 |
3 | /**
4 | * Created by hwang on 09/07/2017.
5 | */
6 | trait GameBot[State, Action] {
7 |
8 | /**
9 | * Reacts to the given game state by playing one or more actions
10 | *
11 | * @param state current state of the game
12 | * @return one or more actions to play
13 | */
14 | def react(state: State): Action
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/codingame/challenge/GameIO.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.codingame.challenge
2 |
3 | /**
4 | * Created by hwang on 09/07/2017.
5 | */
6 | trait GameIO[Context, State, Action] {
7 |
8 | /**
9 | * Reads game context from the referee system. A context stores game's global information
10 | */
11 | def readContext: Context
12 |
13 | /**
14 | * Reads current state from the referee system. A state provides information for the current turn
15 | */
16 | def readState(turn: Int, context: Context): State
17 |
18 | /**
19 | * Writes action to the referee system
20 | */
21 | def writeAction(state: State, action: Action)
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/codingame/challenge/GameLoop.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.codingame.challenge
2 |
3 | import com.truelaurel.codingame.logging.CGLogger
4 |
5 | class GameLoop[Context, State, Action](
6 | gameIO: GameIO[Context, State, Action],
7 | myPlayer: GameBot[State, Action],
8 | accumulator: GameAccumulator[Context, State, Action],
9 | turns: Int = 200
10 | ) {
11 | def run(): Unit = {
12 | CGLogger.startOfRound = System.nanoTime
13 | val initContext = gameIO.readContext
14 | CGLogger.info("GameInit")
15 | (1 to turns).foldLeft(initContext) {
16 | case (c, turn) =>
17 | val state = gameIO.readState(turn, c)
18 | CGLogger.startOfRound = System.nanoTime
19 | CGLogger.info(state)
20 | val actions = myPlayer.react(state)
21 | CGLogger.info("GameReact")
22 | gameIO.writeAction(state, actions)
23 | accumulator.accumulate(c, state, actions)
24 | }
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/codingame/challenge/GameSimulator.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.codingame.challenge
2 |
3 | trait GameSimulator[State, Action] {
4 |
5 | /**
6 | * Impacts the provided action on the given state and
7 | * produces a new state according to the defined game rule
8 | *
9 | * @param state the starting state
10 | * @param action action selected based on the starting state
11 | * @return an updated state after actions impact
12 | */
13 | def simulate(state: State, action: Action): State
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/codingame/challenge/Undoer.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.codingame.challenge
2 |
3 | import scala.collection.mutable.ArrayBuffer
4 |
5 | object Undoer {
6 | def of(undoers: ArrayBuffer[() => Unit]): () => Unit = { () =>
7 | {
8 | var i = 0
9 | while (i < undoers.length) {
10 | undoers(i)()
11 | i += 1
12 | }
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/codingame/logging/CGLogger.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.codingame.logging
2 |
3 | /**
4 | * Created by hwang on 29/04/2017.
5 | */
6 | object CGLogger {
7 |
8 | val info = 0
9 | val debug = 1
10 | val warn = -1
11 |
12 | var current = info
13 |
14 | var startOfRound: Long = 0
15 |
16 | def time: Long = (System.nanoTime - startOfRound) / 1000000
17 |
18 | def debug(message: Any): Unit = log(message, debug)
19 |
20 | def info(message: Any): Unit = log(message, info)
21 |
22 | private def log(message: Any, level: Int): Unit = {
23 | if (level <= current) {
24 | System.err.println(s"$time ms - $message")
25 | }
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/codingame/tool/benchmark/HelloWorldBenchmark.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.codingame.tool.benchmark
2 |
3 | import org.openjdk.jmh.annotations.Benchmark
4 |
5 | // This class is there to check during CI that jmh benchmarks are not broken in SBT.
6 | class HelloWorldBenchmark {
7 |
8 | @Benchmark
9 | def wellHelloThere(): Unit = {
10 | // this method was intentionally left blank.
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/codingame/tool/bundle/Bundler.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.codingame.tool.bundle
2 |
3 | import java.io.File
4 |
5 | import org.scalafmt.Scalafmt
6 |
7 | case class Bundler(fileName: String, io: BundlerIo) {
8 | type PackageContents = Map[String, String]
9 |
10 | def bundle(): Unit = {
11 | val outputFileContent = buildOutput
12 | val formattedOutputFileContent = Scalafmt.format(outputFileContent).get
13 | io.save(fileName, formattedOutputFileContent)
14 | }
15 |
16 | def buildOutput: String = {
17 | val file = io.findFile(fileName)
18 | val content = transformFile(file).mkString("\n")
19 | stripComments(content)
20 | }
21 |
22 | def dependentFiles(file: File, fileLines: List[String]): List[File] = {
23 | val filesInSameFolder = io.filesInFolder(file.getParentFile)
24 | val filesFromImport = fileLines.flatMap(filesFromLine)
25 | filesFromImport ++ filesInSameFolder
26 | }
27 |
28 | def filesList(
29 | queue: List[File],
30 | contents: Map[File, List[String]]
31 | ): List[File] =
32 | queue match {
33 | case Nil => Nil
34 | case file :: t =>
35 | val fileLines = io.readFile(file)
36 | val needed = dependentFiles(file, fileLines)
37 | val nextContents = contents + (file -> fileLines)
38 | val nextQueue = (needed ++ t).distinct.diff(nextContents.keySet.toSeq)
39 | filesList(nextQueue, nextContents) ++ needed :+ file
40 | }
41 |
42 | def transformFile(file: File): List[String] = {
43 | val allFiles = filesList(List(file), Map.empty).distinct
44 | val packageContents = allFiles.foldLeft(Map.empty[String, String]) {
45 | case (contents, `file`) =>
46 | transformSingleFile(file, contents, forceToRoot = true)
47 | case (contents, otherFile) => transformSingleFile(otherFile, contents)
48 | }
49 | packageContents.map { case (n, c) => formatPackage(n, c) }.toList
50 | }
51 |
52 | def formatPackage(name: String, content: String): String =
53 | if (name == "") content
54 | else
55 | s"""package $name {
56 | |$content
57 | |}""".stripMargin
58 |
59 | def add(
60 | pkgName: String,
61 | content: String,
62 | contents: PackageContents
63 | ): PackageContents = {
64 | val value = contents.getOrElse(pkgName, "") + "\n" + content
65 | contents.updated(pkgName, value)
66 | }
67 |
68 | def stripComments(x: String, s: String = "/*", e: String = "*/"): String = {
69 | val a = x indexOf s
70 | val b = x indexOf (e, a + s.length)
71 | if (a == -1 || b == -1) x
72 | else stripComments(x.take(a) + x.drop(b + e.length), s, e)
73 | }
74 |
75 | private def transformSingleFile(
76 | f: File,
77 | packagesContents: PackageContents,
78 | forceToRoot: Boolean = false
79 | ): PackageContents = {
80 | val lines = io.readFile(f)
81 | val (pkgLines, rest) = lines.span(_.startsWith("package"))
82 | val result = rest.map(_.trim).filterNot("".==).mkString("\n")
83 | pkgLines match {
84 | case Nil => add("", result, packagesContents)
85 | case List(pkgLine) =>
86 | val pkgName = if (forceToRoot) "" else pkgLine.drop("package ".size)
87 | add(pkgName, result, packagesContents)
88 | case _ =>
89 | throw new Exception(
90 | "Bundler does not support multiple packages declaration"
91 | )
92 | }
93 | }
94 |
95 | private def filesFromLine(line: String): List[File] =
96 | if (
97 | !line.startsWith("import") || Seq("scala", "java")
98 | .exists(i => line.startsWith(s"import $i"))
99 | )
100 | Nil
101 | else {
102 | val imported = line.split(" ").tail.mkString
103 | val packageElements =
104 | imported.replaceAll("_", "").replaceAll("\\{.*\\}", "").split("\\.")
105 | val subFolder = io.findFolder(packageElements)
106 | io.filesInFolder(subFolder)
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/codingame/tool/bundle/BundlerIo.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.codingame.tool.bundle
2 |
3 | import java.io.{File, PrintWriter}
4 | import java.nio.file.{Files, Paths, FileVisitOption}
5 | import java.util.Objects
6 |
7 | import scala.io.Source
8 | import scala.util.control.NonFatal
9 |
10 | trait BundlerIo {
11 | def readFile(file: File): List[String]
12 |
13 | def findFolder(packageElements: Array[String]): File
14 |
15 | def save(fileName: String, content: String): Unit
16 |
17 | def findFile(fileName: String): File
18 |
19 | def filesInFolder(folder: File): List[File]
20 | }
21 |
22 | case class StdBundlerIo(srcFolder: String = "./src/main/scala")
23 | extends BundlerIo {
24 |
25 | def readFile(file: File): List[String] = {
26 | println(s"reading from $file")
27 | try {
28 | Source.fromFile(file).getLines().toList
29 | } catch {
30 | case NonFatal(e) =>
31 | println("Error while reading file " + file)
32 | e.printStackTrace()
33 | throw e
34 | }
35 | }
36 |
37 | def findFolder(packageElements: Array[String]): File = {
38 | packageElements.foldLeft(new File(srcFolder)) {
39 | case (folder, pkg) =>
40 | val f = new File(folder, pkg)
41 | //TODO : could only import files listed in { cl1, cl2 }
42 | if (f.isDirectory) f else folder
43 | }
44 | }
45 |
46 | def save(fileName: String, content: String): Unit = {
47 | val destFolder: String = "./target"
48 | val destFile = new File(destFolder, fileName)
49 | val pw = new PrintWriter(destFile)
50 | try {
51 | println(s"writing to $destFile")
52 | pw.write(content)
53 | } finally pw.close()
54 |
55 | }
56 |
57 | def findFile(fileName: String): File = {
58 | Files
59 | .find(
60 | Paths.get("src"),
61 | Int.MaxValue,
62 | (path, _) => path.endsWith(fileName),
63 | FileVisitOption.FOLLOW_LINKS
64 | )
65 | .findAny()
66 | .orElseThrow(() => new IllegalArgumentException(s"$fileName not found"))
67 | .toFile
68 | }
69 |
70 | def filesInFolder(folder: File): List[File] = {
71 | Objects.requireNonNull(folder, "Folder should not be null")
72 | val files =
73 | folder.listFiles((pathname: File) => !pathname.getName.startsWith("."))
74 | Objects.requireNonNull(
75 | files,
76 | "visibleFiles should not be null in folder " + folder
77 | )
78 | files.filterNot(_.isDirectory).toList.sortBy(_.getAbsolutePath)
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/codingame/tool/bundle/BundlerMain.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.codingame.tool.bundle
2 |
3 | object BundlerMain {
4 | def main(args: Array[String]): Unit = {
5 | if (args.isEmpty) {
6 | println("Input file name must be provided")
7 | sys.exit(1)
8 | }
9 | args.foreach { fileName =>
10 | Bundler(fileName, StdBundlerIo()).bundle()
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/collection/ArrayUtil.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.collection
2 |
3 | object ArrayUtil {
4 | def update[T](arr: Array[T], i: Int, v: T): Array[T] = {
5 | val res = arr.clone()
6 | res(i) = v
7 | res
8 | }
9 |
10 | def update[T](arr: Array[T], i: Int, f: T => T): Array[T] = {
11 | val res = arr.clone()
12 | res(i) = f(arr(i))
13 | res
14 | }
15 |
16 | def fastNotContains3(elts: Array[Int], e: Int): Boolean = {
17 | if (elts(0) == e) false
18 | else if (elts(1) == e) false
19 | else elts(2) != e
20 | }
21 |
22 | def fastContains2(elts: Array[Int], e: Int): Boolean = {
23 | if (elts(0) == e) true
24 | else elts(1) == e
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/collection/Collectionsl.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.collection
2 |
3 | /**
4 | * Created by hwang on 25/12/2016.
5 | */
6 | object Collectionsl {
7 |
8 | def adjust[K, V](map: Map[K, V])(key: K)(mapper: V => V): Map[K, V] = {
9 | map.get(key) match {
10 | case Some(v) => map.updated(key, mapper(v))
11 | case None => map
12 | }
13 | }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/fp/Unfold.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.fp
2 |
3 | object Unfold {
4 | def unfold[A, S](z: S)(f: S => Option[(A, S)]): Stream[A] =
5 | f(z) match {
6 | case None => Stream.empty
7 | case Some((elt, state)) =>
8 | elt #:: unfold(state)(f)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/fp/state/State.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.fp.state
2 |
3 | trait State[S, A] {
4 | val run: S => (S, A)
5 |
6 | def eval(s: S): A =
7 | apply(s)._2
8 |
9 | def apply(s: S): (S, A) =
10 | run(s)
11 |
12 | def map[B](f: A => B): State[S, B] =
13 | State { s: S =>
14 | val (s1, a) = run(s)
15 | (s1, f(a))
16 | }
17 |
18 | def flatMap[B](f: A => State[S, B]): State[S, B] =
19 | State { s: S =>
20 | val (s1, a) = run(s)
21 | f(a)(s1)
22 | }
23 | }
24 |
25 | object State {
26 |
27 | def state[S, A](a: A): State[S, A] = State { s: S => (s, a) }
28 |
29 | def apply[S, A](f: S => (S, A)): State[S, A] =
30 | new State[S, A] {
31 | final val run = f
32 | }
33 |
34 | def get[S]: State[S, S] = State { s: S => (s, s) }
35 | def gets[S, A](f: S => A): State[S, A] = State { s: S => (s, f(s)) }
36 | def modify[S](f: S => S): State[S, Unit] = State { s: S => (f(s), ()) }
37 |
38 | def reduce[S, A](states: Iterable[State[S, A]]): State[S, A] =
39 | sequence(states).map(_.last)
40 |
41 | def sequence[S, B](fs: Iterable[State[S, B]]): State[S, Vector[B]] =
42 | State(s => sequence_(fs, Vector(), s))
43 | private def sequence_[S, B](
44 | fs: Iterable[State[S, B]],
45 | out: Vector[B],
46 | nextS: S
47 | ): (S, Vector[B]) =
48 | if (fs.isEmpty) (nextS, out)
49 | else {
50 | val (h, t) = (fs.head, fs.tail)
51 | val (s, a) = h(nextS)
52 | sequence_(t, out :+ a, s)
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/math/Mathl.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.math
2 |
3 | import scala.util.Random
4 |
5 | /**
6 | * Created by hwang on 01/12/2016.
7 | */
8 | object Mathl {
9 |
10 | val random = new Random(62638886242411L)
11 |
12 | def halfUp(d: Double): Int =
13 | ((d.abs * 2 + 1) / 2).toInt * (if (d > 0) 1 else -1)
14 |
15 | def almostEqual(d1: Double, d2: Double): Boolean =
16 | Math.abs(d1 - d2) <= 0.000001
17 |
18 | def randomBetween(min: Double, max: Double): Double = {
19 | min + random.nextDouble() * (max - min)
20 | }
21 |
22 | @inline
23 | def sqr(x: Int) = x * x
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/math/geometry/Circle.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.math.geometry
2 |
3 | import scala.math.sqrt
4 |
5 | case class Circle(center: Pos, radius: Int) {
6 | def sqr(x: Double) = x * x
7 |
8 | // http://nains-games.over-blog.com/2014/12/intersection-de-deux-cercles.html
9 | // https://pastebin.com/5hT3G3V8
10 | def intersections(ci: Circle): Seq[Pos] = {
11 | val rc1 = radius
12 | val rc2 = ci.radius
13 | val xc1 = center.x
14 | val yc1 = center.y
15 | val xc2 = ci.center.x
16 | val yc2 = ci.center.y
17 |
18 | if (yc1 == yc2) {
19 | val a = (xc1 - xc2).abs
20 | val XIa = (sqr(rc2) - sqr(a) - sqr(rc1)) / (-2 * a)
21 | val root = sqr(rc2) - sqr(a - XIa)
22 | if (root >= 0) {
23 | val YIa = yc1 + sqrt(root)
24 | val YIb = yc1 - sqrt(root)
25 | Seq(new Pos(XIa, YIa), new Pos(XIa, YIb)).distinct
26 | } else Seq.empty
27 | } else {
28 | val a = (-sqr(xc1) - sqr(yc1) + sqr(xc2) + sqr(yc2) + sqr(rc1) - sqr(
29 | rc2
30 | )) / (2 * (yc2 - yc1))
31 | val d = (xc2.toDouble - xc1) / (yc2 - yc1)
32 | val A = sqr(d) + 1
33 | val B = -2 * xc1 + 2 * yc1 * d - 2 * a * d
34 | val C = sqr(xc1) + sqr(yc1) - 2 * yc1 * a + sqr(a) - sqr(rc1)
35 | val delta = sqr(B) - 4 * A * C
36 | if (delta >= 0) {
37 | val XIa = (-B + sqrt(delta)) / (2 * A)
38 | val XIb = (-B - sqrt(delta)) / (2 * A)
39 | val YIa = a - ((-B + sqrt(delta)) / (2 * A)) * d
40 | val YIb = a - ((-B - sqrt(delta)) / (2 * A)) * d
41 | Seq(new Pos(XIa, YIa), new Pos(XIb, YIb)).distinct
42 | } else Seq.empty
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/math/geometry/Pos.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.math.geometry
2 |
3 | case class Pos(x: Int, y: Int) {
4 | def this(x: Double, y: Double) = this(x.toInt, y.toInt)
5 |
6 | def neighbours4: Seq[Pos] =
7 | Seq(Pos(x + 1, y), Pos(x - 1, y), Pos(x, y - 1), Pos(x, y + 1))
8 |
9 | def +(pos: Pos): Pos = Pos(x + pos.x, y + pos.y)
10 |
11 | def /(r: Int): Pos = Pos(x / r, y / r)
12 |
13 | def %(r: Int): Pos = Pos(x % r, y % r)
14 |
15 | def neighborIn(direction: Direction): Pos =
16 | direction match {
17 | case N => Pos(x, y - 1)
18 | case S => Pos(x, y + 1)
19 | case W => Pos(x - 1, y)
20 | case E => Pos(x + 1, y)
21 | case NE => Pos(x + 1, y - 1)
22 | case SE => Pos(x + 1, y + 1)
23 | case NW => Pos(x - 1, y - 1)
24 | case SW => Pos(x - 1, y + 1)
25 | }
26 |
27 | def distance(pos: Pos): Int =
28 | Math.max(Math.abs(x - pos.x), Math.abs(y - pos.y))
29 |
30 | def distanceEuclide(pos: Pos): Double =
31 | Math.sqrt(Math.pow(x - pos.x, 2) + Math.pow(y - pos.y, 2))
32 | }
33 |
34 | object Pos {
35 | val right: (Int, Int) = (1, 0)
36 | val down: (Int, Int) = (0, 1)
37 | val downRight: (Int, Int) = (1, 1)
38 | val downLeft: (Int, Int) = (-1, 1)
39 | val all = Seq(right, down, downRight, downLeft)
40 | }
41 |
42 | sealed trait Direction {
43 | def similar: Array[Direction]
44 | }
45 |
46 | case object N extends Direction {
47 | val similar: Array[Direction] = Array(NE, N, NW)
48 | }
49 |
50 | case object W extends Direction {
51 | val similar: Array[Direction] = Array(NW, W, SW)
52 | }
53 |
54 | case object S extends Direction {
55 | val similar: Array[Direction] = Array(SE, S, SW)
56 | }
57 |
58 | case object E extends Direction {
59 | val similar: Array[Direction] = Array(NE, SE, E)
60 | }
61 |
62 | case object NW extends Direction {
63 | val similar: Array[Direction] = Array(N, W, NW)
64 | }
65 |
66 | case object NE extends Direction {
67 | val similar: Array[Direction] = Array(NE, N, E)
68 | }
69 |
70 | case object SW extends Direction {
71 | val similar: Array[Direction] = Array(S, W, SW)
72 | }
73 |
74 | case object SE extends Direction {
75 | val similar: Array[Direction] = Array(SE, E, S)
76 | }
77 |
78 | object Direction {
79 |
80 | /**
81 | * @param size of the square
82 | * @return the valid neighbors
83 | */
84 | def neighborsOf(pos: Pos, size: Int): Seq[Pos] = {
85 | Direction.all
86 | .map(d => pos.neighborIn(d))
87 | .filter(p => p.x < size && p.x >= 0 && p.y < size && p.y >= 0)
88 | }
89 |
90 | val all = Seq(N, W, S, E, SW, SE, NW, NE)
91 |
92 | def apply(dir: String): Direction =
93 | dir match {
94 | case "N" => N
95 | case "S" => S
96 | case "W" => W
97 | case "E" => E
98 | case "NE" => NE
99 | case "SE" => SE
100 | case "NW" => NW
101 | case "SW" => SW
102 | case _ => throw new IllegalArgumentException("unknown direction " + dir)
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/math/geometry/Vectorl.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.math.geometry
2 |
3 | import com.truelaurel.math.Mathl
4 |
5 | object Vectorls {
6 | val origin = Vectorl(0, 0)
7 | val axisX = Vectorl(1, 0)
8 | val axisY = Vectorl(0, 1)
9 | }
10 |
11 | case class Vectorl(x: Double, y: Double) {
12 | lazy val mag2: Double = x * x + y * y
13 | lazy val mag: Double = Math.sqrt(mag2)
14 | lazy val norm: Vectorl = if (mag > 0) this * (1.0 / mag) else Vectorl(0, 0)
15 |
16 | def /(factor: Double): Vectorl = this * (1.0 / factor)
17 |
18 | def +(that: Vectorl): Vectorl = Vectorl(x + that.x, y + that.y)
19 |
20 | def -(that: Vectorl): Vectorl = Vectorl(x - that.x, y - that.y)
21 |
22 | def pivotTo(desired: Vectorl, maxDegree: Double): Vectorl = {
23 | if (mag2 == 0 || angleInDegreeBetween(desired) <= maxDegree) {
24 | desired.norm
25 | } else {
26 | if (this.perDotProduct(desired) > 0) {
27 | rotateInDegree(maxDegree).norm
28 | } else {
29 | rotateInDegree(-maxDegree).norm
30 | }
31 | }
32 | }
33 |
34 | def rotateInDegree(degree: Double): Vectorl =
35 | rotateInRadian(Math.toRadians(degree))
36 |
37 | def rotateInRadian(radians: Double): Vectorl = {
38 | val rotated = angleInRadian + radians
39 | Vectorl(Math.cos(rotated), Math.sin(rotated)) * mag
40 | }
41 |
42 | def *(factor: Double): Vectorl = Vectorl(x * factor, y * factor)
43 |
44 | private def angleInRadian: Double = Math.atan2(y, x)
45 |
46 | def angleInDegreeBetween(other: Vectorl): Double = {
47 | Math.toDegrees(angleInRadianBetween(other))
48 | }
49 |
50 | def angleInRadianBetween(other: Vectorl): Double = {
51 | val result = this.dotProduct(other) / (this.mag * other.mag)
52 | if (result >= 1.0) 0 else Math.acos(result)
53 | }
54 |
55 | def perDotProduct(that: Vectorl): Double = perp.dotProduct(that)
56 |
57 | def dotProduct(that: Vectorl): Double = x * that.x + y * that.y
58 |
59 | def perp = Vectorl(-y, x)
60 |
61 | def between(v1: Vectorl, v2: Vectorl): Boolean = {
62 | this.perDotProduct(v1) * this.perDotProduct(v2) < 0
63 | }
64 |
65 | def truncate = Vectorl(x.toInt, y.toInt)
66 |
67 | def round = Vectorl(Mathl.halfUp(x), Mathl.halfUp(y))
68 |
69 | override def equals(o: Any): Boolean =
70 | o match {
71 | case that: Vectorl =>
72 | Mathl.almostEqual(x, that.x) && Mathl.almostEqual(y, that.y)
73 | case _ => false
74 | }
75 |
76 | private def angleInDegree: Double = Math.toDegrees(angleInRadian)
77 |
78 | }
79 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/math/geometry/grid/BitGrid.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.math.geometry.grid
2 |
3 | case class BitGrid(gridData: GridData, masks: Masks) {
4 | def empty: Boolean = gridData.empty
5 |
6 | def complete: Boolean =
7 | masks.isComplete(gridData)
8 |
9 | def free = gridData.free
10 |
11 | def used = gridData.used
12 |
13 | def +(r: Int, c: Int) = {
14 | copy(gridData = gridData + (r, c))
15 | }
16 |
17 | def -(r: Int, c: Int) = {
18 | copy(gridData = gridData - (r, c))
19 | }
20 |
21 | def addCol(c: Int) = copy(gridData = gridData.addCol(c))
22 |
23 | def addCol(c: Int, minRow: Int, maxRow: Int) =
24 | copy(gridData = gridData.addCol(c, minRow, maxRow))
25 |
26 | def addRow(r: Int) = copy(gridData = gridData.addRow(r))
27 |
28 | def addRow(r: Int, minCol: Int, maxCol: Int) =
29 | copy(gridData = gridData.addRow(r, minCol, maxCol))
30 |
31 | def addDiag1(r: Int, c: Int, length: Int) =
32 | copy(gridData = gridData.addDiag1(r, c, length))
33 |
34 | def addDiag2(r: Int, c: Int, length: Int) =
35 | copy(gridData = gridData.addDiag2(r, c, length))
36 | }
37 |
38 | object BitGrid {
39 |
40 | def apply(size: Int, needed: Int): BitGrid =
41 | BitGrid(GridData(size), Masks(size, needed))
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/math/geometry/grid/FastGrid.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.math.geometry.grid
2 |
3 | import com.truelaurel.math.geometry._
4 |
5 | /**
6 | * Square grid where positions are represented by numbers.
7 | * Provides fast access to neighbors.
8 | *
9 | * @param size the length of one side of the square
10 | */
11 | case class FastGrid(size: Int) {
12 |
13 | /**
14 | * This may return an invalid neighbor if called in a direction outside grid !
15 | */
16 | def neighborIn(pos: Int, d: Direction): Int =
17 | d match {
18 | case N => pos - size
19 | case W => pos - 1
20 | case S => pos + size
21 | case E => pos + 1
22 | case NW => pos - size - 1
23 | case NE => pos - size + 1
24 | case SW => pos + size - 1
25 | case SE => pos + size + 1
26 | }
27 |
28 | def checkedNeighborIn(p: Int, d: Direction): Option[Int] = {
29 | val po = pos(p)
30 | val canN = po.y > 0
31 | val canS = po.y < size - 1
32 | val canE = po.x < size - 1
33 | val canW = po.x > 0
34 | d match {
35 | case N if canN => Some(p - size)
36 | case W if canW => Some(p - 1)
37 | case S if canS => Some(p + size)
38 | case E if canE => Some(p + 1)
39 | case NW if canN && canW => Some(p - size - 1)
40 | case NE if canE && canN => Some(p - size + 1)
41 | case SW if canW && canS => Some(p + size - 1)
42 | case SE if canE && canS => Some(p + size + 1)
43 | case _ => None
44 | }
45 | }
46 |
47 | val center: Int = pos(Pos(size / 2, size / 2))
48 |
49 | val size2: Int = size * size
50 |
51 | /**
52 | * Only valid neighbors are listed here.
53 | */
54 | val namedNeighbors: Array[Array[(Direction, Int)]] = for {
55 | p <- (0 until size2).toArray
56 | po = pos(p)
57 | } yield namedNeighborsImpl(po)
58 |
59 | val neighbors: Array[Array[Int]] = namedNeighbors.map(a => a.map(_._2))
60 |
61 | private def namedNeighborsImpl(p: Pos) = {
62 | for {
63 | d <- Direction.all.toArray
64 | r = p.neighborIn(d)
65 | if isValid(r)
66 | } yield (d: Direction) -> pos(r)
67 | }
68 |
69 | def pos(p: Pos): Int =
70 | p.x + p.y * size
71 |
72 | def pos(p: Int): Pos = {
73 | val x = p % size
74 | val y = p / size
75 | Pos(x, y)
76 | }
77 |
78 | def isValid(p: Int): Boolean = p >= 0 && p < size2
79 |
80 | def isValid(pos: Pos): Boolean =
81 | pos.x < size && pos.x >= 0 && pos.y < size && pos.y >= 0
82 | }
83 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/math/geometry/grid/GridData.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.math.geometry.grid
2 |
3 | import com.truelaurel.math.geometry.Pos
4 |
5 | import scala.collection.BitSet
6 |
7 | case class GridData(size: Int, rows: Array[Long]) {
8 |
9 | lazy val free: Iterable[(Int, Int)] =
10 | for {
11 | r <- 0 until size
12 | row = rows(r)
13 | c <- 0 until size
14 | if (row & (1 << c)) == 0
15 | } yield (r, c)
16 | lazy val used: Iterable[(Int, Int)] =
17 | for {
18 | r <- 0 until size
19 | row = rows(r)
20 | c <- 0 until size
21 | if (row & (1 << c)) != 0
22 | } yield (r, c)
23 | lazy val usedPos: Set[Pos] =
24 | used.map { case (r, c) => Pos(r, c) }.toSet
25 |
26 | def empty: Boolean = rows.forall(0.==)
27 |
28 | def neighbours4(p: Pos): Seq[Pos] =
29 | p.neighbours4.filter(isValidPos)
30 |
31 | def isValidPos(p: Pos): Boolean =
32 | p.x >= 0 &&
33 | p.y >= 0 &&
34 | p.x < size &&
35 | p.y < size
36 |
37 | def freeNeighbours4(p: Pos): Seq[Pos] =
38 | p.neighbours4.filter(pos => isValidPos(pos) && isFree(pos))
39 |
40 | def isFree(p: Pos): Boolean =
41 | isFree(p.x, p.y)
42 |
43 | def isFree(r: Int, c: Int): Boolean =
44 | (rows(r) & (1 << c)) == 0
45 |
46 | def -(r: Int, c: Int): GridData = {
47 | copy(rows = rows.updated(r, rows(r) & ~(1 << c)))
48 | }
49 |
50 | def isUsed(p: Pos): Boolean =
51 | isUsed(p.x, p.y)
52 |
53 | def isUsed(r: Int, c: Int): Boolean =
54 | (rows(r) & (1 << c)) != 0
55 |
56 | def addCol(c: Int, minRow: Int = 0, maxRow: Int = size) =
57 | (minRow until maxRow).foldLeft(this) {
58 | case (bg, r) => bg + (r, c)
59 | }
60 |
61 | def addRow(r: Int, minCol: Int = 0, maxCol: Int = size) =
62 | (minCol until maxCol).foldLeft(this) {
63 | case (bg, c) => bg + (r, c)
64 | }
65 |
66 | def addDiag1(r: Int, c: Int, size: Int = size) =
67 | (0 until size).foldLeft(this) {
68 | case (bg, i) => bg + (r + i, c + i)
69 | }
70 |
71 | def addDiag2(r: Int, c: Int, size: Int = size) =
72 | (0 until size).foldLeft(this) {
73 | case (bg, i) => bg + (r + i, c - i)
74 | }
75 |
76 | def +(r: Int, c: Int): GridData = {
77 | copy(rows = rows.updated(r, rows(r) | 1 << c))
78 | }
79 |
80 | def subMatrix(r0: Int, c0: Int, subSize: Int): Long = {
81 | var sum = 0L
82 | var x = 0
83 | while (x < subSize) {
84 | val r = x + r0
85 | var y = 0
86 | while (y < subSize) {
87 | val c = y + c0
88 | val i = x * subSize + y
89 | val bit = (1L << i) & (rows(r) >> c << i)
90 | sum += bit
91 | y += 1
92 | }
93 | x += 1
94 | }
95 | sum
96 | }
97 | }
98 |
99 | object GridData {
100 | val fullGridCache = collection.mutable.Map.empty[Int, BitSet]
101 |
102 | def apply(size: Int): GridData =
103 | GridData(size, rows = Array.fill(size)(0L))
104 |
105 | def fullGrid(size: Int) =
106 | fullGridCache.getOrElseUpdate(
107 | size, {
108 | val all = for {
109 | r <- 0 until size
110 | c <- 0 until size
111 | } yield r * size + c
112 | BitSet(all: _*)
113 | }
114 | )
115 |
116 | def full(size: Int) = GridData(size, Array.fill(size)((1L << size + 1) - 1))
117 | }
118 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/math/geometry/grid/Masks.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.math.geometry.grid
2 |
3 | case class Masks(size: Int, needed: Int) {
4 | val empty = GridData(size)
5 |
6 | val matrixIndices: Seq[(Int, Int)] = for {
7 | r0 <- 0 to size - needed
8 | c0 <- 0 to size - needed
9 | } yield (r0, c0)
10 |
11 | val matricesCompleted: Set[Long] =
12 | (preComputedMatricesRows ++ preComputedMatricesCols :+ preComputedMatricesDiag1 :+ preComputedMatricesDiag2).toSet
13 |
14 | def isComplete(grid: GridData): Boolean =
15 | matrixIndices.exists {
16 | case (r0, c0) =>
17 | val gsm = grid.subMatrix(r0, c0, needed)
18 | matricesCompleted.exists(m => (m & gsm) == m)
19 | }
20 |
21 | def preComputedMatricesRows: Seq[Long] =
22 | (0 until needed).map(row =>
23 | (for {
24 | c <- 0 until needed
25 | i = row * needed + c
26 | bit = 1L << i
27 | } yield bit).sum
28 | )
29 |
30 | def preComputedMatricesCols: Seq[Long] =
31 | (0 until needed).map(col =>
32 | (for {
33 | r <- 0 until needed
34 | i = r * needed + col
35 | bit = 1L << i
36 | } yield bit).sum
37 | )
38 |
39 | def preComputedMatricesDiag1: Long =
40 | (for {
41 | r <- 0 until needed
42 | c = r
43 | i = r * needed + c
44 | bit = 1L << i
45 | } yield bit).sum
46 |
47 | def preComputedMatricesDiag2: Long =
48 | (for {
49 | r <- 0 until needed
50 | c = needed - 1 - r
51 | i = r * needed + c
52 | bit = 1L << i
53 | } yield bit).sum
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/math/geometry/hexagons/Cube.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.math.geometry.hexagons
2 |
3 | /**
4 | * Created by hwang on 15/04/2017.
5 | */
6 | case class Cube(x: Int, y: Int, z: Int) {
7 |
8 | def toOffset: Offset = {
9 | val newX = x + (z - (z & 1)) / 2
10 | val newY = z
11 | Offset(newX, newY)
12 | }
13 |
14 | def neighbor(orientation: Int): Cube = {
15 | Cube(
16 | x + Cube.directions(orientation)(0),
17 | y + Cube.directions(orientation)(1),
18 | z + Cube.directions(orientation)(2)
19 | )
20 | }
21 |
22 | def distanceTo(that: Cube): Int =
23 | (Math.abs(x - that.x) + Math.abs(y - that.y) + Math.abs(z - that.z)) / 2
24 |
25 | }
26 |
27 | object Cube {
28 | val directions: Array[Array[Int]] = Array[Array[Int]](
29 | Array(1, -1, 0),
30 | Array(+1, 0, -1),
31 | Array(0, +1, -1),
32 | Array(-1, +1, 0),
33 | Array(-1, 0, +1),
34 | Array(0, -1, +1)
35 | )
36 |
37 | def apply(x: Int, y: Int): Cube = {
38 | Offset(x, y).toCube
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/math/geometry/hexagons/Hexagons.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.math.geometry.hexagons
2 |
3 | /**
4 | * Created by hwang on 15/04/2017.
5 | */
6 | case class Offset(x: Int, y: Int) {
7 | def toCube: Cube = {
8 | val xp = x - (y - (y & 1)) / 2
9 | val zp = y
10 | val yp = -(xp + zp)
11 | Cube(xp, yp, zp)
12 | }
13 |
14 | def angle(target: Offset): Double = {
15 | val dy = (target.y - this.y) * Math.sqrt(3) / 2
16 | val dx = target.x - this.x + ((this.y - target.y) & 1) * 0.5
17 | var angle = -Math.atan2(dy, dx) * 3 / Math.PI
18 | if (angle < 0) angle += 6
19 | else if (angle >= 6) angle -= 6
20 | angle
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/physics/Collision.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.physics
2 |
3 | import com.truelaurel.math.geometry.Vectorl
4 |
5 | case class Disk(p: Vectorl, v: Vectorl, r: Double, m: Double = 1.0) {}
6 |
7 | object DiskMover {
8 | def move(disk: Disk, time: Double): Disk = {
9 | disk.copy(p = disk.p + (disk.v * time))
10 | }
11 | }
12 |
13 | object Collision {
14 | def collideTime(d1: Disk, d2: Disk): Option[Double] = {
15 | collideTime(d1.p, d1.v, d1.r, d2.p, d2.v, d2.r)
16 | }
17 |
18 | def collideTime(
19 | p1: Vectorl,
20 | v1: Vectorl,
21 | r1: Double,
22 | p2: Vectorl,
23 | v2: Vectorl,
24 | r2: Double
25 | ): Option[Double] = {
26 | val dr = p2 - p1
27 | val dv = v2 - v1
28 | val dvdr = dv.dotProduct(dr)
29 | if (dvdr > 0) return None
30 | val dvdv = dv.dotProduct(dv)
31 | if (dvdv == 0) return None
32 | val drdr = dr.dotProduct(dr)
33 | val sigma = r1 + r2
34 | val d = (dvdr * dvdr) - dvdv * (drdr - sigma * sigma)
35 | if (d < 0) return None
36 | Some(-(dvdr + Math.sqrt(d)) / dvdv)
37 | }
38 |
39 | def bounceOffWithMinimumImpulse(
40 | p1: Vectorl,
41 | v1: Vectorl,
42 | m1: Double,
43 | p2: Vectorl,
44 | v2: Vectorl,
45 | m2: Double,
46 | minImpulse: Double
47 | ): (Vectorl, Vectorl) = {
48 | val impulse = computeImpulse(p1, v1, m1, p2, v2, m2)
49 | val adjusted =
50 | impulse.norm * (impulse.mag * 0.5 + minImpulse).max(impulse.mag)
51 | bouncedSpeed(v1, m1, v2, m2, adjusted)
52 | }
53 |
54 | def bounceOff(d1: Disk, d2: Disk): (Disk, Disk) = {
55 | val (v1, v2) = bounceOff(d1.p, d1.v, d1.m, d2.p, d2.v, d2.m)
56 | (d1.copy(v = v1), d2.copy(v = v2))
57 | }
58 |
59 | def bounceOff(
60 | p1: Vectorl,
61 | v1: Vectorl,
62 | m1: Double,
63 | p2: Vectorl,
64 | v2: Vectorl,
65 | m2: Double
66 | ): (Vectorl, Vectorl) = {
67 | val impulse = computeImpulse(p1, v1, m1, p2, v2, m2)
68 |
69 | bouncedSpeed(v1, m1, v2, m2, impulse)
70 | }
71 |
72 | private def bouncedSpeed(
73 | v1: Vectorl,
74 | m1: Double,
75 | v2: Vectorl,
76 | m2: Double,
77 | impulse: Vectorl
78 | ) = {
79 | (v1 + impulse / m1, v2 - impulse / m2)
80 | }
81 |
82 | private def computeImpulse(
83 | p1: Vectorl,
84 | v1: Vectorl,
85 | m1: Double,
86 | p2: Vectorl,
87 | v2: Vectorl,
88 | m2: Double
89 | ) = {
90 | val dr = p2 - p1
91 | val dv = v2 - v1
92 | val drdr = dr.dotProduct(dr)
93 | val dvdr = dr.dotProduct(dv)
94 | val massCoefficient = (m1 + m2) / (m1 * m2)
95 |
96 | val impulse = dr * 2.0 * dvdr / (massCoefficient * drdr)
97 | impulse
98 | }
99 |
100 | def move(disk: Disk, time: Double): Disk = DiskMover.move(disk, time)
101 |
102 | }
103 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/samplegames/dummy/DummyRules.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.samplegames.dummy
2 |
3 | import com.truelaurel.algorithm.game._
4 |
5 | case class DummyRules(branching: Int = 100, depth: Int = 20)
6 | extends RulesFor2p[DummyBoard, Int] {
7 | val initial: DummyBoard = DummyBoard(Nil, false)
8 |
9 | val moves = 1 to branching
10 |
11 | def validMoves(state: DummyBoard): Seq[Int] = moves
12 |
13 | def applyMove(state: DummyBoard, move: Int): DummyBoard =
14 | DummyBoard(move :: state.played, !state.nextPlayer)
15 |
16 | def outcome(state: DummyBoard): Outcome[Boolean] =
17 | if (state.played.size < depth) Undecided
18 | else if (state.played.head % 2 == 0) Wins(true)
19 | else Wins(false)
20 | }
21 |
22 | case class DummyBoard(played: List[Int], nextPlayer: Boolean)
23 | extends GameState[Boolean]
24 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/samplegames/gomoku/GomokuBoard.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.samplegames.gomoku
2 |
3 | import com.truelaurel.algorithm.game.GameState
4 | import com.truelaurel.math.geometry.Pos
5 | import com.truelaurel.math.geometry.grid.GridData
6 |
7 | import scala.annotation.tailrec
8 |
9 | object GomokuBoard {
10 | def apply(size: Int): GomokuBoard =
11 | GomokuBoard(
12 | size,
13 | dataTrue = GridData(size),
14 | dataFalse = GridData(size),
15 | dataFree = GridData(size, rows = Array.fill(size)((1 << (size + 1)) - 1))
16 | )
17 | }
18 |
19 | case class GomokuBoard(
20 | size: Int,
21 | dataTrue: GridData,
22 | dataFalse: GridData,
23 | dataFree: GridData,
24 | nextPlayer: Boolean = false
25 | ) extends GameState[Boolean] {
26 |
27 | lazy val playedFalse: Set[Pos] = dataFalse.usedPos
28 | lazy val playedTrue: Set[Pos] = dataTrue.usedPos
29 | lazy val free: Set[Pos] = dataFree.usedPos
30 |
31 | def dataNext = if (nextPlayer) dataTrue else dataFalse
32 |
33 | def dataLast = if (nextPlayer) dataFalse else dataTrue
34 |
35 | def play(x: Int, y: Int): GomokuBoard = play(Pos(x, y))
36 |
37 | def play(p: Pos): GomokuBoard =
38 | forcePlay(p, nextPlayer)
39 |
40 | def forcePlay(p: Pos, player: Boolean): GomokuBoard = {
41 | if (player)
42 | copy(
43 | nextPlayer = !player,
44 | dataFree = dataFree - (p.x, p.y),
45 | dataTrue = dataTrue + (p.x, p.y)
46 | )
47 | else
48 | copy(
49 | nextPlayer = !player,
50 | dataFree = dataFree - (p.x, p.y),
51 | dataFalse = dataFalse + (p.x, p.y)
52 | )
53 | }
54 |
55 | def remove(p: Pos): GomokuBoard =
56 | copy(
57 | dataFree = dataFree + (p.x, p.y),
58 | dataTrue = dataTrue - (p.x, p.y),
59 | dataFalse = dataFalse - (p.x, p.y)
60 | )
61 |
62 | def isFree(p: Pos): Boolean = dataFree.isUsed(p)
63 |
64 | override def toString: String = toText
65 |
66 | def toText: String =
67 | Seq
68 | .tabulate(size) { y =>
69 | Seq
70 | .tabulate(size) { x =>
71 | if (playedFalse(Pos(x, y))) 'F'
72 | else if (playedTrue(Pos(x, y))) 'T'
73 | else ' '
74 | }
75 | .mkString
76 | }
77 | .mkString("\n") + s"\n${nextPlayer.toString.toUpperCase.head} to play"
78 |
79 | def maxLength(player: Boolean) = {
80 | val played = if (player) playedTrue else playedFalse
81 |
82 | @tailrec
83 | def length(direction: (Int, Int), from: Pos, l: Int): Int = {
84 | val pos = Pos(from.x + direction._1, from.y + direction._2)
85 | if (played(pos)) length(direction, pos, l + 1)
86 | else l
87 | }
88 |
89 | if (played.isEmpty) 0
90 | else {
91 | val lengths = for {
92 | d <- Pos.all
93 | p <- played
94 | } yield length(d, p, 1)
95 | lengths.max
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/samplegames/gomoku/GomokuDemo.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.samplegames.gomoku
2 |
3 | import com.truelaurel.algorithm.alphabeta.AlphaBetaAi
4 | import com.truelaurel.algorithm.mcts.MctsAi
5 | import com.truelaurel.math.geometry.Pos
6 | import com.truelaurel.time.Chronometer
7 |
8 | import scala.concurrent.duration.DurationInt
9 |
10 | object GomokuDemo {
11 | val rules = GomokuRules(7, 4)
12 |
13 | def main(args: Array[String]): Unit = {
14 | val outcome = rules.judge(
15 | truePl = mctsMove,
16 | falsePl = alphaBetaMove,
17 | s => println(s.toText)
18 | )
19 | println(outcome)
20 | }
21 |
22 | def mctsMove(s: GomokuBoard): Pos = {
23 | val chronometer = new Chronometer(5.seconds)
24 | chronometer.start()
25 | MctsAi(rules)(_ => chronometer.willOutOfTime).chooseMove(s)
26 | }
27 |
28 | def alphaBetaMove(s: GomokuBoard): Pos =
29 | AlphaBetaAi(rules, rules.centerHeuristic).chooseMove(s, 3)
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/samplegames/gomoku/GomokuRules.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.samplegames.gomoku
2 |
3 | import com.truelaurel.algorithm.game._
4 | import com.truelaurel.math.geometry.Pos
5 | import com.truelaurel.math.geometry.grid.{BitGrid, GridData, Masks}
6 |
7 | import scala.math.abs
8 |
9 | case class GomokuRules(size: Int, lengthToWin: Int)
10 | extends RulesFor2p[GomokuBoard, Pos] {
11 |
12 | val initial: GomokuBoard = GomokuBoard(size)
13 | val masks = Masks(size, lengthToWin)
14 |
15 | def validMoves(state: GomokuBoard): Seq[Pos] = state.free.toSeq
16 |
17 | def applyMove(state: GomokuBoard, move: Pos): GomokuBoard =
18 | state.play(move)
19 |
20 | def outcome(b: GomokuBoard) =
21 | if (hasWon(b, true)) Wins(true)
22 | else if (hasWon(b, false)) Wins(false)
23 | else if (b.free.isEmpty) Draw
24 | else Undecided
25 |
26 | def hasWon(b: GomokuBoard, player: Boolean) = {
27 | val data = if (player) b.dataTrue else b.dataFalse
28 | BitGrid(data, masks).complete
29 | }
30 |
31 | def centerHeuristic(state: GomokuBoard): Double = {
32 | def score(data: GridData) =
33 | data.usedPos.map {
34 | case Pos(x, y) => 1.0 / (1 + abs(x - size / 2) + abs(y - size / 2))
35 | }.sum
36 |
37 | val opp = score(state.dataLast)
38 | val mine = score(state.dataNext)
39 | mine - opp
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/samplegames/stones/Demo3Stones.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.samplegames.stones
2 |
3 | import com.truelaurel.algorithm.mcts.MctsAi
4 | import com.truelaurel.samplegames.gomoku.GomokuBoard
5 | import com.truelaurel.time.Chronometer
6 |
7 | import scala.concurrent.duration.DurationInt
8 |
9 | object Demo3Stones {
10 |
11 | def main(args: Array[String]): Unit = {
12 | val outcome =
13 | Rules.judge(truePl = aiMove, falsePl = aiMove, s => println(s.toText))
14 | println(outcome)
15 | }
16 |
17 | def aiMove(s: GomokuBoard): Move = {
18 | val chronometer = new Chronometer(5.seconds)
19 | chronometer.start()
20 | MctsAi(Rules)(_ => chronometer.willOutOfTime).chooseMove(s)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/samplegames/stones/Move.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.samplegames.stones
2 |
3 | import com.truelaurel.math.geometry.Pos
4 |
5 | sealed trait Move
6 |
7 | case class Slide(from: Pos, to: Pos) extends Move
8 |
9 | case class Add(pos: Pos) extends Move
10 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/samplegames/stones/Rules.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.samplegames.stones
2 |
3 | import com.truelaurel.algorithm.game._
4 | import com.truelaurel.math.geometry.Pos
5 | import com.truelaurel.math.geometry.grid.{BitGrid, Masks}
6 | import com.truelaurel.samplegames.gomoku.GomokuBoard
7 |
8 | //Terni Lapilli game (3 stones)
9 | object Rules extends RulesFor2p[GomokuBoard, Move] {
10 |
11 | val initial: GomokuBoard = GomokuBoard(3)
12 |
13 | val center = Pos(1, 1)
14 | val masks = Masks(3, 3)
15 |
16 | def validMoves(state: GomokuBoard): Seq[Move] = {
17 | val placed = if (state.nextPlayer) state.dataTrue else state.dataFalse
18 | val free = state.free.toSeq
19 | if (placed.used.size < 3) free.map(Add)
20 | else
21 | for {
22 | from <- placed.usedPos.toSeq
23 | diags = if (from == center) free else Seq(center).intersect(free)
24 | to <- (placed.neighbours4(from) ++ diags).distinct
25 | if state.isFree(to)
26 | } yield Slide(from, to)
27 | }
28 |
29 | def applyMove(state: GomokuBoard, move: Move): GomokuBoard =
30 | move match {
31 | case Add(p) => state.play(p)
32 | case Slide(from, to) => state.remove(from).play(to)
33 | }
34 |
35 | def outcome(b: GomokuBoard) =
36 | if (hasWon(b, true)) Wins(true)
37 | else if (hasWon(b, false)) Wins(false)
38 | else if (b.free.isEmpty) Draw
39 | else Undecided
40 |
41 | def hasWon(b: GomokuBoard, player: Boolean) = {
42 | val data = if (player) b.dataTrue else b.dataFalse
43 | BitGrid(data, masks).complete
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/samplegames/wondev/WondevChallenge.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.samplegames.wondev
2 |
3 | import com.truelaurel.codingame.logging.CGLogger
4 | import com.truelaurel.math.geometry.Pos
5 | import com.truelaurel.samplegames.wondev.analysis.WarFogCleaner
6 | import com.truelaurel.samplegames.wondev.simulation.WondevSimulator
7 | import com.truelaurel.samplegames.wondev.domain.{WondevAction, WondevState}
8 | import com.truelaurel.samplegames.wondev.io.WondevIO
9 | import com.truelaurel.samplegames.wondev.strategy.MinimaxPlayer
10 | import com.truelaurel.samplegames.wondev.warmup.WondevWarmup
11 |
12 | // ~runMain com.truelaurel.codingame.tool.bundle.BundlerMain WondevChallenge.scala
13 | // cp -r target/universal/stage/ brutaltester/head
14 |
15 | // java -jar cg-brutaltester.jar -r "java -jar cg-ww.jar" -p1 "./head/bin/player" -p2 "./best/bin/player" -t 2 -n 100 -l "./logs/"
16 | // cp -rf head/ best/
17 |
18 | object Player {
19 | def main(args: Array[String]): Unit = {
20 | CGLogger.current = CGLogger.info
21 |
22 | var turn = 0
23 | var context = WondevIO.readContext
24 |
25 | val time = System.nanoTime()
26 | for (i <- 0 until 5) {
27 | WondevWarmup.warmup()
28 | }
29 | System.err.println(
30 | "warmup elt: " + (System.nanoTime() - time) / 1000000 + "ms"
31 | )
32 |
33 | try {
34 | while (true) {
35 | val state = WondevIO.readState(turn, context)
36 |
37 | val start = System.nanoTime()
38 | val predictedActions = WondevSimulator.nextLegalActions(state)
39 | if (state.legalActions.toSet != predictedActions.toSet) {
40 | CGLogger.info("action prediction not working")
41 | }
42 |
43 | CGLogger.info(context)
44 | CGLogger.info(state)
45 | val restrictedOppoScope =
46 | WarFogCleaner.restrictOppoScope(state, context)
47 |
48 | val clearedState = WarFogCleaner.removeFog(state, restrictedOppoScope)
49 | val action = MinimaxPlayer.react(clearedState)
50 | context = context.copy(
51 | previousState = state,
52 | previousAction = action,
53 | previousOppoScope = restrictedOppoScope
54 | )
55 | WondevIO.writeAction(state, action)
56 | turn += 1
57 | }
58 | } catch {
59 | case e: Throwable => e.printStackTrace()
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/samplegames/wondev/analysis/WarFogCleaner.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.samplegames.wondev.analysis
2 |
3 | import com.truelaurel.math.geometry.Pos
4 | import com.truelaurel.samplegames.wondev.simulation.WondevSimulator
5 | import com.truelaurel.samplegames.wondev.domain.{
6 | MutableWondevState,
7 | WondevAction,
8 | WondevContext,
9 | WondevState
10 | }
11 |
12 | import scala.collection.mutable.ArrayBuffer
13 |
14 | /**
15 | * Created by hwang on 15/07/2017.
16 | */
17 | object WarFogCleaner {
18 |
19 | def restrictOppoScope(
20 | observed: WondevState,
21 | context: WondevContext
22 | ): Set[Set[Pos]] = {
23 | restrictOppoScope(
24 | observed,
25 | context.previousState,
26 | context.previousAction,
27 | context.previousOppoScope
28 | )
29 | }
30 |
31 | def restrictOppoScope(
32 | observedRaw: WondevState,
33 | previousStateRaw: WondevState,
34 | previousAction: WondevAction,
35 | previousOppoScope: Set[Set[Pos]]
36 | ): Set[Set[Pos]] = {
37 | val observed = MutableWondevState.fromSlowState(observedRaw)
38 | if (previousAction == null) {
39 | val oppoUnits = observed.readable.opUnits
40 | val visibleOppo = oppoUnits.filter(WondevContext.isVisible)
41 | val myUnits = observed.readable.myUnits
42 | val occupables = observed.readable.feasibleOppo()
43 | val oppoScope = occupables
44 | .subsets(2)
45 | .toSet
46 | .filter(set =>
47 | hasSameUnvisibleOppo(2 - visibleOppo.length, set, myUnits)
48 | )
49 | val increased = observed.readable.findIncreasedCell
50 | if (increased.isDefined) {
51 | //oppo started the game
52 | oppoScope.filter(oppoSet =>
53 | visibleOppo.forall(oppoSet.contains)
54 | && oppoSet.exists(_.distance(increased.get) == 1)
55 | )
56 | } else {
57 | oppoScope.filter(oppoSet => visibleOppo.forall(oppoSet.contains))
58 | }
59 | } else {
60 | val previousState = MutableWondevState.fromSlowState(previousStateRaw)
61 | val restricted = collection.mutable.ArrayBuffer.empty[Set[Pos]]
62 | val previousScope = previousOppoScope.toArray
63 | val observedOppo = observed.readable.opUnits
64 | val visibleOppo = observedOppo.filter(WondevContext.isVisible)
65 | val observedSelf = observed.readable.myUnits
66 | var i = 0
67 | while (i < previousScope.length) {
68 | val oppoSet = previousScope(i)
69 | previousState.writable.start()
70 | val oppoUpdated = previousState.writable.setOppo(oppoSet)
71 | previousState.writable.end()
72 | val myActionApplied = WondevSimulator.next(oppoUpdated, previousAction)
73 | val oppoLegalActions = WondevSimulator.nextLegalActions(previousState)
74 | findConsistentState(
75 | oppoLegalActions,
76 | myActionApplied,
77 | observed,
78 | visibleOppo,
79 | observedSelf,
80 | observedOppo,
81 | restricted
82 | )
83 | myActionApplied.writable.undo()
84 | oppoUpdated.writable.undo()
85 | i += 1
86 | }
87 | if (restricted.isEmpty) previousOppoScope else restricted.toSet
88 | }
89 | }
90 |
91 | def findConsistentState(
92 | legalActions: Seq[WondevAction],
93 | myActionApplied: MutableWondevState,
94 | observed: MutableWondevState,
95 | visibleOppo: Array[Pos],
96 | observedSelf: Array[Pos],
97 | observedOppo: Array[Pos],
98 | restricted: ArrayBuffer[Set[Pos]]
99 | ): Unit = {
100 | var i = 0
101 | while (i < legalActions.size) {
102 | val action = legalActions(i)
103 | val simulated = WondevSimulator.next(myActionApplied, action)
104 | if (
105 | consistent(simulated, observed, visibleOppo, observedSelf, observedOppo)
106 | ) {
107 | restricted.append(simulated.readable.opUnits.toSet)
108 | }
109 | simulated.writable.undo()
110 | i += 1
111 | }
112 | }
113 |
114 | def consistent(
115 | simulated: MutableWondevState,
116 | observed: MutableWondevState,
117 | visibleOppo: Array[Pos],
118 | observedSelf: Array[Pos],
119 | observedOppo: Array[Pos]
120 | ): Boolean = {
121 | val simulatedOppo = simulated.readable.opUnits
122 | val simulatedSelf = simulated.readable.myUnits
123 | (observedSelf sameElements simulatedSelf) &&
124 | visibleOppo.forall(simulatedOppo.contains) &&
125 | hasSameUnvisibleOppo(2 - visibleOppo.length, simulatedOppo, observedSelf) &&
126 | observed.readable.hasSameHeightMap(simulated)
127 | }
128 |
129 | private def hasSameUnvisibleOppo(
130 | size: Int,
131 | simulatedOppo: Iterable[Pos],
132 | observedSelf: Iterable[Pos]
133 | ) = {
134 | simulatedOppo.count(pos => observedSelf.forall(_.distance(pos) > 1)) == size
135 | }
136 |
137 | def removeFog(state: WondevState, oppoScope: Set[Set[Pos]]): WondevState = {
138 | state.copy(units = state.units.take(2) ++ oppoScope.head.toSeq)
139 | }
140 |
141 | }
142 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/samplegames/wondev/analysis/WondevEvaluator.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.samplegames.wondev.analysis
2 |
3 | import java.util
4 |
5 | import com.truelaurel.math.geometry.{Direction, Pos}
6 | import com.truelaurel.samplegames.wondev.domain._
7 |
8 | /**
9 | * Created by hwang on 24/06/2017.
10 | */
11 | object WondevEvaluator {
12 |
13 | def evaluate(state: MutableWondevState): Double = {
14 | evaluateUnits(state, state.readable.myUnits) - 2.0 * evaluateUnits(
15 | state,
16 | state.readable.opUnits.filter(WondevContext.isVisible)
17 | )
18 | }
19 |
20 | private def evaluateUnits(
21 | state: MutableWondevState,
22 | units: Seq[Pos]
23 | ): Double = {
24 | units
25 | .map(unit => {
26 | countExits(state, unit, 3) + state.readable.heightOf(unit)
27 | })
28 | .sum
29 | }
30 |
31 | private def countExits(
32 | state: MutableWondevState,
33 | unit: Pos,
34 | depth: Int
35 | ): Double = {
36 | val queue = new util.LinkedList[Pos]()
37 | var reached = 0.0
38 | queue.add(unit)
39 | while (!queue.isEmpty) {
40 | val next = queue.remove()
41 | val neighbors = state.readable.neighborOf(next)
42 | val reachableHeight = state.readable.heightOf(next) + 1
43 | val distance1 = unit.distance(next)
44 | if (distance1 != 0) {
45 | reached += 1.0 / distance1
46 | }
47 | neighbors
48 | .foreach(neighbor => {
49 | val distance2 = unit.distance(neighbor)
50 | val neighborHeight = state.readable.heightOf(neighbor)
51 | if (
52 | distance2 > distance1 &&
53 | distance2 <= depth &&
54 | WondevContext.isPlayable(neighborHeight) &&
55 | neighborHeight <= reachableHeight &&
56 | state.readable.isFree(neighbor)
57 | ) {
58 | queue.add(neighbor)
59 | }
60 | })
61 | }
62 | reached
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/samplegames/wondev/benchmark/WondevBenchmark.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.samplegames.wondev.benchmark
2 |
3 | import java.util.concurrent.TimeUnit
4 |
5 | import com.truelaurel.math.geometry.Pos
6 | import com.truelaurel.samplegames.wondev.analysis.WarFogCleaner
7 | import com.truelaurel.samplegames.wondev.domain._
8 | import com.truelaurel.samplegames.wondev.strategy.MinimaxPlayer
9 | import org.openjdk.jmh.annotations._
10 |
11 | /**
12 | * jmh:run -prof jmh.extras.JFR -i 1 -wi 1 -f1 -t1 WondevBenchmark
13 | */
14 | @State(Scope.Benchmark)
15 | class WondevBenchmark {
16 |
17 | val player = MinimaxPlayer
18 | val observed = WondevState(
19 | 7,
20 | Map(
21 | Pos(2, 5) -> 0,
22 | Pos(1, 5) -> -1,
23 | Pos(5, 0) -> -1,
24 | Pos(0, 2) -> -1,
25 | Pos(0, 0) -> -1,
26 | Pos(5, 2) -> 0,
27 | Pos(5, 1) -> -1,
28 | Pos(4, 0) -> -1,
29 | Pos(3, 4) -> 0,
30 | Pos(6, 4) -> -1,
31 | Pos(6, 6) -> -1,
32 | Pos(3, 1) -> 0,
33 | Pos(6, 1) -> -1,
34 | Pos(4, 1) -> 0,
35 | Pos(6, 2) -> -1,
36 | Pos(2, 0) -> -1,
37 | Pos(0, 3) -> 0,
38 | Pos(4, 4) -> 1,
39 | Pos(3, 0) -> 0,
40 | Pos(1, 6) -> -1,
41 | Pos(0, 5) -> -1,
42 | Pos(3, 6) -> 0,
43 | Pos(6, 5) -> -1,
44 | Pos(1, 1) -> -1,
45 | Pos(6, 3) -> 0,
46 | Pos(3, 5) -> 0,
47 | Pos(4, 6) -> -1,
48 | Pos(4, 5) -> 0,
49 | Pos(1, 4) -> 1,
50 | Pos(2, 6) -> -1,
51 | Pos(0, 4) -> -1,
52 | Pos(5, 4) -> 0,
53 | Pos(3, 2) -> 0,
54 | Pos(1, 3) -> 0,
55 | Pos(2, 2) -> 0,
56 | Pos(5, 5) -> -1,
57 | Pos(4, 2) -> 0,
58 | Pos(2, 4) -> 0,
59 | Pos(0, 1) -> -1,
60 | Pos(5, 3) -> 0,
61 | Pos(3, 3) -> 0,
62 | Pos(2, 3) -> 0,
63 | Pos(1, 2) -> 0,
64 | Pos(2, 1) -> 0,
65 | Pos(4, 3) -> 0,
66 | Pos(6, 0) -> -1,
67 | Pos(1, 0) -> -1,
68 | Pos(5, 6) -> -1,
69 | Pos(0, 6) -> -1
70 | ),
71 | List(Pos(3, 4), Pos(4, 2), Pos(2, 4), Pos(-1, -1)),
72 | List(
73 | MoveBuild(0, Pos(4, 4), Pos(5, 4)),
74 | MoveBuild(0, Pos(4, 4), Pos(4, 3)),
75 | MoveBuild(0, Pos(4, 4), Pos(5, 3)),
76 | MoveBuild(0, Pos(4, 4), Pos(3, 3)),
77 | MoveBuild(0, Pos(4, 4), Pos(4, 5)),
78 | MoveBuild(0, Pos(4, 4), Pos(3, 5)),
79 | MoveBuild(0, Pos(4, 4), Pos(3, 4)),
80 | MoveBuild(0, Pos(3, 3), Pos(4, 3)),
81 | MoveBuild(0, Pos(3, 3), Pos(3, 2)),
82 | MoveBuild(0, Pos(3, 3), Pos(2, 2)),
83 | MoveBuild(0, Pos(3, 3), Pos(3, 4)),
84 | MoveBuild(0, Pos(3, 3), Pos(4, 4)),
85 | MoveBuild(0, Pos(3, 3), Pos(2, 3)),
86 | MoveBuild(0, Pos(4, 3), Pos(5, 3)),
87 | MoveBuild(0, Pos(4, 3), Pos(5, 2)),
88 | MoveBuild(0, Pos(4, 3), Pos(3, 2)),
89 | MoveBuild(0, Pos(4, 3), Pos(4, 4)),
90 | MoveBuild(0, Pos(4, 3), Pos(5, 4)),
91 | MoveBuild(0, Pos(4, 3), Pos(3, 4)),
92 | MoveBuild(0, Pos(4, 3), Pos(3, 3)),
93 | MoveBuild(0, Pos(2, 3), Pos(3, 3)),
94 | MoveBuild(0, Pos(2, 3), Pos(2, 2)),
95 | MoveBuild(0, Pos(2, 3), Pos(3, 2)),
96 | MoveBuild(0, Pos(2, 3), Pos(1, 2)),
97 | MoveBuild(0, Pos(2, 3), Pos(3, 4)),
98 | MoveBuild(0, Pos(2, 3), Pos(1, 4)),
99 | MoveBuild(0, Pos(2, 3), Pos(1, 3)),
100 | MoveBuild(0, Pos(3, 5), Pos(4, 5)),
101 | MoveBuild(0, Pos(3, 5), Pos(3, 4)),
102 | MoveBuild(0, Pos(3, 5), Pos(4, 4)),
103 | MoveBuild(0, Pos(3, 5), Pos(3, 6)),
104 | MoveBuild(0, Pos(3, 5), Pos(2, 5)),
105 | MoveBuild(0, Pos(4, 5), Pos(4, 4)),
106 | MoveBuild(0, Pos(4, 5), Pos(5, 4)),
107 | MoveBuild(0, Pos(4, 5), Pos(3, 4)),
108 | MoveBuild(0, Pos(4, 5), Pos(3, 6)),
109 | MoveBuild(0, Pos(4, 5), Pos(3, 5)),
110 | MoveBuild(0, Pos(2, 5), Pos(3, 5)),
111 | MoveBuild(0, Pos(2, 5), Pos(3, 4)),
112 | MoveBuild(0, Pos(2, 5), Pos(1, 4)),
113 | MoveBuild(0, Pos(2, 5), Pos(3, 6)),
114 | MoveBuild(1, Pos(5, 2), Pos(4, 1)),
115 | MoveBuild(1, Pos(5, 2), Pos(5, 3)),
116 | MoveBuild(1, Pos(5, 2), Pos(6, 3)),
117 | MoveBuild(1, Pos(5, 2), Pos(4, 3)),
118 | MoveBuild(1, Pos(5, 2), Pos(4, 2)),
119 | MoveBuild(1, Pos(4, 1), Pos(3, 0)),
120 | MoveBuild(1, Pos(4, 1), Pos(4, 2)),
121 | MoveBuild(1, Pos(4, 1), Pos(5, 2)),
122 | MoveBuild(1, Pos(4, 1), Pos(3, 2)),
123 | MoveBuild(1, Pos(4, 1), Pos(3, 1)),
124 | MoveBuild(1, Pos(3, 1), Pos(4, 1)),
125 | MoveBuild(1, Pos(3, 1), Pos(3, 0)),
126 | MoveBuild(1, Pos(3, 1), Pos(3, 2)),
127 | MoveBuild(1, Pos(3, 1), Pos(4, 2)),
128 | MoveBuild(1, Pos(3, 1), Pos(2, 2)),
129 | MoveBuild(1, Pos(3, 1), Pos(2, 1)),
130 | MoveBuild(1, Pos(4, 3), Pos(5, 3)),
131 | MoveBuild(1, Pos(4, 3), Pos(4, 2)),
132 | MoveBuild(1, Pos(4, 3), Pos(5, 2)),
133 | MoveBuild(1, Pos(4, 3), Pos(3, 2)),
134 | MoveBuild(1, Pos(4, 3), Pos(4, 4)),
135 | MoveBuild(1, Pos(4, 3), Pos(5, 4)),
136 | MoveBuild(1, Pos(4, 3), Pos(3, 3)),
137 | MoveBuild(1, Pos(5, 3), Pos(6, 3)),
138 | MoveBuild(1, Pos(5, 3), Pos(5, 2)),
139 | MoveBuild(1, Pos(5, 3), Pos(4, 2)),
140 | MoveBuild(1, Pos(5, 3), Pos(5, 4)),
141 | MoveBuild(1, Pos(5, 3), Pos(4, 4)),
142 | MoveBuild(1, Pos(5, 3), Pos(4, 3)),
143 | MoveBuild(1, Pos(3, 3), Pos(4, 3)),
144 | MoveBuild(1, Pos(3, 3), Pos(3, 2)),
145 | MoveBuild(1, Pos(3, 3), Pos(4, 2)),
146 | MoveBuild(1, Pos(3, 3), Pos(2, 2)),
147 | MoveBuild(1, Pos(3, 3), Pos(4, 4)),
148 | MoveBuild(1, Pos(3, 3), Pos(2, 3)),
149 | MoveBuild(1, Pos(3, 2), Pos(4, 2)),
150 | MoveBuild(1, Pos(3, 2), Pos(3, 1)),
151 | MoveBuild(1, Pos(3, 2), Pos(4, 1)),
152 | MoveBuild(1, Pos(3, 2), Pos(2, 1)),
153 | MoveBuild(1, Pos(3, 2), Pos(3, 3)),
154 | MoveBuild(1, Pos(3, 2), Pos(4, 3)),
155 | MoveBuild(1, Pos(3, 2), Pos(2, 3)),
156 | MoveBuild(1, Pos(3, 2), Pos(2, 2)),
157 | PushBuild(0, Pos(2, 4), Pos(1, 3)),
158 | PushBuild(0, Pos(2, 4), Pos(1, 4))
159 | ),
160 | true
161 | )
162 |
163 | val previousAction = MoveBuild(0, Pos(3, 4), Pos(4, 4))
164 |
165 | val previousOppoScope = Set(
166 | Set(Pos(2, 5), Pos(1, 2)),
167 | Set(Pos(2, 5), Pos(1, 3)),
168 | Set(Pos(1, 3), Pos(2, 1)),
169 | Set(Pos(3, 0), Pos(2, 3)),
170 | Set(Pos(2, 3), Pos(1, 2)),
171 | Set(Pos(2, 2), Pos(2, 3)),
172 | Set(Pos(6, 3), Pos(4, 5)),
173 | Set(Pos(2, 5), Pos(2, 2)),
174 | Set(Pos(2, 3), Pos(2, 1)),
175 | Set(Pos(6, 3), Pos(3, 5)),
176 | Set(Pos(0, 3), Pos(2, 1)),
177 | Set(Pos(2, 4), Pos(2, 1)),
178 | Set(Pos(2, 5), Pos(6, 3)),
179 | Set(Pos(1, 3), Pos(2, 4)),
180 | Set(Pos(3, 0), Pos(2, 2)),
181 | Set(Pos(3, 5), Pos(1, 3)),
182 | Set(Pos(2, 2), Pos(1, 2)),
183 | Set(Pos(1, 4), Pos(2, 3)),
184 | Set(Pos(4, 5), Pos(2, 2)),
185 | Set(Pos(3, 0), Pos(2, 4)),
186 | Set(Pos(0, 3), Pos(1, 4)),
187 | Set(Pos(2, 5), Pos(2, 1)),
188 | Set(Pos(3, 5), Pos(1, 2)),
189 | Set(Pos(0, 3), Pos(4, 5)),
190 | Set(Pos(3, 6), Pos(2, 4)),
191 | Set(Pos(2, 2), Pos(2, 1)),
192 | Set(Pos(3, 6), Pos(1, 4)),
193 | Set(Pos(6, 3), Pos(1, 3)),
194 | Set(Pos(3, 0), Pos(6, 3)),
195 | Set(Pos(2, 5), Pos(2, 3)),
196 | Set(Pos(3, 0), Pos(4, 5)),
197 | Set(Pos(3, 5), Pos(2, 1)),
198 | Set(Pos(2, 4), Pos(2, 3)),
199 | Set(Pos(3, 6), Pos(4, 5)),
200 | Set(Pos(6, 3), Pos(2, 3)),
201 | Set(Pos(1, 3), Pos(2, 3)),
202 | Set(Pos(0, 3), Pos(2, 2)),
203 | Set(Pos(2, 5), Pos(4, 5)),
204 | Set(Pos(3, 5), Pos(2, 4)),
205 | Set(Pos(3, 6), Pos(3, 5)),
206 | Set(Pos(1, 4), Pos(2, 1)),
207 | Set(Pos(3, 6), Pos(2, 3)),
208 | Set(Pos(2, 2), Pos(2, 4)),
209 | Set(Pos(4, 5), Pos(2, 1)),
210 | Set(Pos(0, 3), Pos(2, 4)),
211 | Set(Pos(3, 5), Pos(1, 4)),
212 | Set(Pos(0, 3), Pos(2, 3)),
213 | Set(Pos(2, 5), Pos(0, 3)),
214 | Set(Pos(3, 0), Pos(2, 1)),
215 | Set(Pos(0, 3), Pos(3, 5)),
216 | Set(Pos(3, 0), Pos(3, 6)),
217 | Set(Pos(2, 5), Pos(3, 6)),
218 | Set(Pos(3, 5), Pos(4, 5)),
219 | Set(Pos(4, 5), Pos(2, 4)),
220 | Set(Pos(6, 3), Pos(1, 2)),
221 | Set(Pos(6, 3), Pos(1, 4)),
222 | Set(Pos(3, 0), Pos(1, 2)),
223 | Set(Pos(2, 5), Pos(2, 4)),
224 | Set(Pos(4, 5), Pos(1, 2)),
225 | Set(Pos(4, 5), Pos(2, 3)),
226 | Set(Pos(3, 0), Pos(1, 3)),
227 | Set(Pos(2, 5), Pos(3, 0)),
228 | Set(Pos(2, 5), Pos(1, 4)),
229 | Set(Pos(3, 6), Pos(2, 2)),
230 | Set(Pos(2, 4), Pos(1, 2)),
231 | Set(Pos(1, 4), Pos(2, 4)),
232 | Set(Pos(3, 6), Pos(6, 3)),
233 | Set(Pos(0, 3), Pos(3, 6)),
234 | Set(Pos(6, 3), Pos(2, 1)),
235 | Set(Pos(6, 3), Pos(2, 4)),
236 | Set(Pos(3, 0), Pos(1, 4)),
237 | Set(Pos(1, 4), Pos(2, 2)),
238 | Set(Pos(1, 4), Pos(1, 2)),
239 | Set(Pos(1, 3), Pos(2, 2)),
240 | Set(Pos(0, 3), Pos(1, 3)),
241 | Set(Pos(3, 5), Pos(2, 2)),
242 | Set(Pos(0, 3), Pos(3, 0)),
243 | Set(Pos(1, 3), Pos(1, 2)),
244 | Set(Pos(1, 2), Pos(2, 1)),
245 | Set(Pos(3, 6), Pos(2, 1)),
246 | Set(Pos(3, 0), Pos(3, 5)),
247 | Set(Pos(3, 6), Pos(1, 2)),
248 | Set(Pos(3, 6), Pos(1, 3)),
249 | Set(Pos(0, 3), Pos(6, 3)),
250 | Set(Pos(0, 3), Pos(1, 2)),
251 | Set(Pos(4, 5), Pos(1, 3)),
252 | Set(Pos(1, 4), Pos(1, 3)),
253 | Set(Pos(3, 5), Pos(2, 3)),
254 | Set(Pos(4, 5), Pos(1, 4)),
255 | Set(Pos(2, 5), Pos(3, 5)),
256 | Set(Pos(6, 3), Pos(2, 2))
257 | )
258 |
259 | val previousState = WondevState(
260 | 7,
261 | Map(
262 | Pos(2, 5) -> 0,
263 | Pos(1, 5) -> -1,
264 | Pos(5, 0) -> -1,
265 | Pos(0, 2) -> -1,
266 | Pos(0, 0) -> -1,
267 | Pos(5, 2) -> 0,
268 | Pos(5, 1) -> -1,
269 | Pos(4, 0) -> -1,
270 | Pos(3, 4) -> 0,
271 | Pos(6, 4) -> -1,
272 | Pos(6, 6) -> -1,
273 | Pos(3, 1) -> 0,
274 | Pos(6, 1) -> -1,
275 | Pos(4, 1) -> 0,
276 | Pos(6, 2) -> -1,
277 | Pos(2, 0) -> -1,
278 | Pos(0, 3) -> 0,
279 | Pos(4, 4) -> 0,
280 | Pos(3, 0) -> 0,
281 | Pos(1, 6) -> -1,
282 | Pos(0, 5) -> -1,
283 | Pos(3, 6) -> 0,
284 | Pos(6, 5) -> -1,
285 | Pos(1, 1) -> -1,
286 | Pos(6, 3) -> 0,
287 | Pos(3, 5) -> 0,
288 | Pos(4, 6) -> -1,
289 | Pos(4, 5) -> 0,
290 | Pos(1, 4) -> 0,
291 | Pos(2, 6) -> -1,
292 | Pos(0, 4) -> -1,
293 | Pos(5, 4) -> 0,
294 | Pos(3, 2) -> 0,
295 | Pos(1, 3) -> 0,
296 | Pos(2, 2) -> 0,
297 | Pos(5, 5) -> -1,
298 | Pos(4, 2) -> 0,
299 | Pos(2, 4) -> 0,
300 | Pos(0, 1) -> -1,
301 | Pos(5, 3) -> 0,
302 | Pos(3, 3) -> 0,
303 | Pos(2, 3) -> 0,
304 | Pos(1, 2) -> 0,
305 | Pos(2, 1) -> 0,
306 | Pos(4, 3) -> 0,
307 | Pos(6, 0) -> -1,
308 | Pos(1, 0) -> -1,
309 | Pos(5, 6) -> -1,
310 | Pos(0, 6) -> -1
311 | ),
312 | List(Pos(4, 3), Pos(4, 2), Pos(-1, -1), Pos(-1, -1)),
313 | List(
314 | MoveBuild(0, Pos(5, 3), Pos(6, 3)),
315 | MoveBuild(0, Pos(5, 3), Pos(5, 2)),
316 | MoveBuild(0, Pos(5, 3), Pos(5, 4)),
317 | MoveBuild(0, Pos(5, 3), Pos(4, 4)),
318 | MoveBuild(0, Pos(5, 3), Pos(4, 3)),
319 | MoveBuild(0, Pos(5, 2), Pos(4, 1)),
320 | MoveBuild(0, Pos(5, 2), Pos(5, 3)),
321 | MoveBuild(0, Pos(5, 2), Pos(6, 3)),
322 | MoveBuild(0, Pos(5, 2), Pos(4, 3)),
323 | MoveBuild(0, Pos(3, 2), Pos(3, 1)),
324 | MoveBuild(0, Pos(3, 2), Pos(4, 1)),
325 | MoveBuild(0, Pos(3, 2), Pos(2, 1)),
326 | MoveBuild(0, Pos(3, 2), Pos(3, 3)),
327 | MoveBuild(0, Pos(3, 2), Pos(4, 3)),
328 | MoveBuild(0, Pos(3, 2), Pos(2, 3)),
329 | MoveBuild(0, Pos(3, 2), Pos(2, 2)),
330 | MoveBuild(0, Pos(4, 4), Pos(5, 4)),
331 | MoveBuild(0, Pos(4, 4), Pos(4, 3)),
332 | MoveBuild(0, Pos(4, 4), Pos(5, 3)),
333 | MoveBuild(0, Pos(4, 4), Pos(3, 3)),
334 | MoveBuild(0, Pos(4, 4), Pos(4, 5)),
335 | MoveBuild(0, Pos(4, 4), Pos(3, 5)),
336 | MoveBuild(0, Pos(4, 4), Pos(3, 4)),
337 | MoveBuild(0, Pos(5, 4), Pos(5, 3)),
338 | MoveBuild(0, Pos(5, 4), Pos(6, 3)),
339 | MoveBuild(0, Pos(5, 4), Pos(4, 3)),
340 | MoveBuild(0, Pos(5, 4), Pos(4, 5)),
341 | MoveBuild(0, Pos(5, 4), Pos(4, 4)),
342 | MoveBuild(0, Pos(3, 4), Pos(4, 4)),
343 | MoveBuild(0, Pos(3, 4), Pos(3, 3)),
344 | MoveBuild(0, Pos(3, 4), Pos(4, 3)),
345 | MoveBuild(0, Pos(3, 4), Pos(2, 3)),
346 | MoveBuild(0, Pos(3, 4), Pos(3, 5)),
347 | MoveBuild(0, Pos(3, 4), Pos(4, 5)),
348 | MoveBuild(0, Pos(3, 4), Pos(2, 5)),
349 | MoveBuild(0, Pos(3, 4), Pos(2, 4)),
350 | MoveBuild(0, Pos(3, 3), Pos(4, 3)),
351 | MoveBuild(0, Pos(3, 3), Pos(3, 2)),
352 | MoveBuild(0, Pos(3, 3), Pos(2, 2)),
353 | MoveBuild(0, Pos(3, 3), Pos(3, 4)),
354 | MoveBuild(0, Pos(3, 3), Pos(4, 4)),
355 | MoveBuild(0, Pos(3, 3), Pos(2, 4)),
356 | MoveBuild(0, Pos(3, 3), Pos(2, 3)),
357 | MoveBuild(1, Pos(5, 2), Pos(4, 1)),
358 | MoveBuild(1, Pos(5, 2), Pos(5, 3)),
359 | MoveBuild(1, Pos(5, 2), Pos(6, 3)),
360 | MoveBuild(1, Pos(5, 2), Pos(4, 2)),
361 | MoveBuild(1, Pos(4, 1), Pos(3, 0)),
362 | MoveBuild(1, Pos(4, 1), Pos(4, 2)),
363 | MoveBuild(1, Pos(4, 1), Pos(5, 2)),
364 | MoveBuild(1, Pos(4, 1), Pos(3, 2)),
365 | MoveBuild(1, Pos(4, 1), Pos(3, 1)),
366 | MoveBuild(1, Pos(3, 1), Pos(4, 1)),
367 | MoveBuild(1, Pos(3, 1), Pos(3, 0)),
368 | MoveBuild(1, Pos(3, 1), Pos(3, 2)),
369 | MoveBuild(1, Pos(3, 1), Pos(4, 2)),
370 | MoveBuild(1, Pos(3, 1), Pos(2, 2)),
371 | MoveBuild(1, Pos(3, 1), Pos(2, 1)),
372 | MoveBuild(1, Pos(5, 3), Pos(6, 3)),
373 | MoveBuild(1, Pos(5, 3), Pos(5, 2)),
374 | MoveBuild(1, Pos(5, 3), Pos(4, 2)),
375 | MoveBuild(1, Pos(5, 3), Pos(5, 4)),
376 | MoveBuild(1, Pos(5, 3), Pos(4, 4)),
377 | MoveBuild(1, Pos(3, 3), Pos(3, 2)),
378 | MoveBuild(1, Pos(3, 3), Pos(4, 2)),
379 | MoveBuild(1, Pos(3, 3), Pos(2, 2)),
380 | MoveBuild(1, Pos(3, 3), Pos(3, 4)),
381 | MoveBuild(1, Pos(3, 3), Pos(4, 4)),
382 | MoveBuild(1, Pos(3, 3), Pos(2, 4)),
383 | MoveBuild(1, Pos(3, 3), Pos(2, 3)),
384 | MoveBuild(1, Pos(3, 2), Pos(4, 2)),
385 | MoveBuild(1, Pos(3, 2), Pos(3, 1)),
386 | MoveBuild(1, Pos(3, 2), Pos(4, 1)),
387 | MoveBuild(1, Pos(3, 2), Pos(2, 1)),
388 | MoveBuild(1, Pos(3, 2), Pos(3, 3)),
389 | MoveBuild(1, Pos(3, 2), Pos(2, 3)),
390 | MoveBuild(1, Pos(3, 2), Pos(2, 2))
391 | ),
392 | true
393 | )
394 |
395 | @Benchmark
396 | @BenchmarkMode(Array(Mode.AverageTime))
397 | @OutputTimeUnit(TimeUnit.MICROSECONDS)
398 | def wondevMinimax(): Unit = {
399 | WarFogCleaner.restrictOppoScope(
400 | observed,
401 | previousState,
402 | previousAction,
403 | previousOppoScope
404 | )
405 | }
406 |
407 | }
408 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/samplegames/wondev/domain/MutableWondevState.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.samplegames.wondev.domain
2 |
3 | import com.truelaurel.algorithm.game.GameState
4 | import com.truelaurel.math.geometry.Pos
5 |
6 | import scala.collection.mutable.ArrayBuffer
7 |
8 | class MutableWondevState(
9 | val size: Int,
10 | private val units: Array[Pos],
11 | private val height: Array[Array[Int]],
12 | var nextPlayer: Boolean
13 | ) extends GameState[Boolean] {
14 |
15 | private val neighborTable = WondevContext.neighborMapBySize(size)
16 |
17 | private val freeCellTable: Array[Array[Boolean]] = extractFreeCellTable
18 |
19 | val writable = new Writable()
20 |
21 | val readable = new Readable()
22 |
23 | class Writable {
24 |
25 | type Operation = () => Unit
26 |
27 | private var stack: List[List[Operation]] = Nil
28 | private var current: List[Operation] = Nil
29 |
30 | def start(): Unit = {
31 | current = Nil
32 | }
33 |
34 | def setOppo(oppo: Set[Pos]): MutableWondevState = {
35 | val oldUnit2 = units(2)
36 | val oldUnit3 = units(3)
37 | val oppoSeq = oppo.toSeq
38 | val newUnit2 = oppoSeq.head
39 | val newUnit3 = oppoSeq(1)
40 | units(2) = newUnit2
41 | units(3) = newUnit3
42 | val oldFree2 = readable.isFree(units(2))
43 | val oldFree3 = readable.isFree(units(3))
44 | setOccupied(freeCellTable, units(2))
45 | setOccupied(freeCellTable, units(3))
46 | push(() => {
47 | units(2) = oldUnit2
48 | units(3) = oldUnit3
49 | freeCellTable(newUnit2.x)(newUnit2.y) = oldFree2
50 | freeCellTable(newUnit3.x)(newUnit3.y) = oldFree3
51 | })
52 | }
53 |
54 | def moveUnit(id: Int, to: Pos): MutableWondevState = {
55 | val from: Pos = doMove(id, to)
56 | push(() => {
57 | doMove(id, from)
58 | })
59 | }
60 |
61 | def increaseHeight(pos: Pos): MutableWondevState = {
62 | height(pos.x)(pos.y) += 1
63 | push(() => {
64 | height(pos.x)(pos.y) -= 1
65 | })
66 | }
67 |
68 | def swapPlayer(): MutableWondevState = {
69 | doSwap()
70 | push(() => {
71 | doSwap()
72 | })
73 | }
74 |
75 | private def doSwap() = {
76 | nextPlayer = !nextPlayer
77 | }
78 |
79 | def end(): Unit = {
80 | stack = current :: stack
81 | }
82 |
83 | def undo(): Unit = {
84 | stack.head.foreach(operation => operation.apply())
85 | stack = stack.tail
86 | }
87 |
88 | private def push(operation: Operation): MutableWondevState = {
89 | current = operation :: current
90 | MutableWondevState.this
91 | }
92 | }
93 |
94 | private def doMove(id: Int, to: Pos) = {
95 | val from = units(id)
96 | units(id) = to
97 | freeCellTable(from.x)(from.y) = true
98 | freeCellTable(to.x)(to.y) = false
99 | from
100 | }
101 |
102 | class Readable {
103 | def heightOf(p: Pos): Int = height(p.x)(p.y)
104 |
105 | def neighborOf(pos: Pos): Array[Pos] = {
106 | neighborTable(pos.x)(pos.y)
107 | }
108 |
109 | def isFree(pos: Pos): Boolean = freeCellTable(pos.x)(pos.y)
110 |
111 | def unitAt(id: Int): Pos = units(id)
112 |
113 | def unitIdOf(pos: Pos): Int = units.indexOf(pos)
114 |
115 | def feasibleOppo(): Set[Pos] = {
116 | val feasible: ArrayBuffer[Pos] = ArrayBuffer.empty
117 | var i = 0
118 | while (i < size) {
119 | var j = 0
120 | while (j < size) {
121 | val pos = Pos(i, j)
122 | if (
123 | WondevContext
124 | .isPlayable(heightOf(pos)) && units(0) != pos && units(1) != pos
125 | ) {
126 | feasible.append(pos)
127 | }
128 | j += 1
129 | }
130 | i += 1
131 | }
132 | feasible.toSet
133 | }
134 |
135 | def findIncreasedCell: Option[Pos] = {
136 | var i = 0
137 | while (i < size) {
138 | var j = 0
139 | while (j < size) {
140 | val pos = Pos(i, j)
141 | if (heightOf(pos) == 1) {
142 | return Some(pos)
143 | }
144 | j += 1
145 | }
146 | i += 1
147 | }
148 | None
149 | }
150 |
151 | def myUnits: Array[Pos] = units.take(2)
152 |
153 | def opUnits: Array[Pos] = units.takeRight(2)
154 |
155 | def hasSameHeightMap(that: MutableWondevState): Boolean = {
156 | var i = 0
157 | while (i < height.length) {
158 | val row = height(i)
159 | var j = 0
160 | while (j < row.length) {
161 | val h = row(j)
162 | if (that.height(i)(j) != h) {
163 | return false
164 | }
165 | j += 1
166 | }
167 | i += 1
168 | }
169 | true
170 | }
171 | }
172 |
173 | private def extractFreeCellTable = {
174 | val occupyTable: Array[Array[Boolean]] = Array.fill(size, size)(true)
175 | units.foreach(u => setOccupied(occupyTable, u))
176 | occupyTable
177 | }
178 |
179 | private def setOccupied(occupyTable: Array[Array[Boolean]], u: Pos) = {
180 | if (u.x != -1) {
181 | occupyTable(u.x)(u.y) = false
182 | }
183 | }
184 |
185 | def canEqual(other: Any): Boolean = other.isInstanceOf[MutableWondevState]
186 |
187 | override def equals(other: Any): Boolean =
188 | other match {
189 | case that: MutableWondevState =>
190 | (that canEqual this) &&
191 | size == that.size &&
192 | (units sameElements that.units) &&
193 | height.zip(that.height).forall { case (a, b) => a.sameElements(b) } &&
194 | nextPlayer == that.nextPlayer
195 | case _ => false
196 | }
197 |
198 | override def hashCode(): Int = {
199 | val state = Seq(size, units, height, nextPlayer)
200 | state.map(_.hashCode()).foldLeft(0)((a, b) => 31 * a + b)
201 | }
202 |
203 | def copy(): MutableWondevState = {
204 | new MutableWondevState(size, units.clone(), height.clone(), nextPlayer)
205 | }
206 |
207 | override def toString: String =
208 | s"FastWondevState(size=$size, units=${units.mkString("[", ",", "]")}, " +
209 | s"height=${height.map(_.mkString("[", ",", "]")).mkString("[", ",", "]")}, nextPlayer=$nextPlayer)"
210 | }
211 |
212 | object MutableWondevState {
213 | def fromSlowState(wondevState: WondevState): MutableWondevState = {
214 | val height: Array[Array[Int]] =
215 | Array.fill(wondevState.size)(Array.fill(wondevState.size)(0))
216 | for {
217 | (pos, h) <- wondevState.heightMap
218 | } {
219 | height(pos.x)(pos.y) = h
220 | }
221 | new MutableWondevState(
222 | wondevState.size,
223 | wondevState.units.toArray,
224 | height,
225 | wondevState.nextPlayer
226 | )
227 | }
228 | }
229 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/samplegames/wondev/domain/WondevAction.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.samplegames.wondev.domain
2 |
3 | import com.truelaurel.math.geometry.Pos
4 |
5 | sealed trait WondevAction {}
6 |
7 | sealed case class MoveBuild(unitIndex: Int, move: Pos, build: Pos)
8 | extends WondevAction
9 |
10 | sealed case class PushBuild(unitIndex: Int, build: Pos, push: Pos)
11 | extends WondevAction
12 |
13 | case object AcceptDefeat extends WondevAction
14 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/samplegames/wondev/domain/WondevContext.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.samplegames.wondev.domain
2 |
3 | import com.truelaurel.math.geometry.{Direction, Pos}
4 |
5 | case class WondevContext(
6 | size: Int,
7 | players: Int,
8 | previousState: WondevState = null,
9 | previousAction: WondevAction = null,
10 | previousOppoScope: Set[Set[Pos]] = null
11 | ) {}
12 | object WondevContext {
13 |
14 | def neighborsOf(pos: Pos, size: Int): Set[Pos] = {
15 | Direction.all
16 | .map(d => pos.neighborIn(d))
17 | .filter(p => p.x < size && p.x >= 0 && p.y < size && p.y >= 0)
18 | .toSet
19 | }
20 |
21 | def isVisible(pos: Pos): Boolean = pos.x != -1
22 |
23 | val neighborMapBySize = Map(
24 | 5 -> computeNeighborTable(5),
25 | 6 -> computeNeighborTable(6),
26 | 7 -> computeNeighborTable(7)
27 | )
28 |
29 | def computeNeighborTable(size: Int): Array[Array[Array[Pos]]] = {
30 | val neighborsTable: Array[Array[Array[Pos]]] = Array.ofDim(size, size)
31 |
32 | for {
33 | x <- 0 until size
34 | y <- 0 until size
35 | pos = Pos(x, y)
36 | neighbors = neighborsOf(pos, size).toArray
37 | } {
38 | neighborsTable(x)(y) = neighbors
39 | }
40 | neighborsTable
41 | }
42 |
43 | val pushTargets = Map(
44 | 5 -> computePushTargets(5),
45 | 6 -> computePushTargets(6),
46 | 7 -> computePushTargets(7)
47 | )
48 |
49 | private def computePushTargets(size: Int) = {
50 | val map = (for {
51 | x <- 0 until size
52 | y <- 0 until size
53 | pos = Pos(x, y)
54 | neighbor <- neighborMapBySize(size)(x)(y)
55 | neighborNeighbor = neighborMapBySize(size)(neighbor.x)(neighbor.y)
56 | pushable = neighborNeighbor.filter(nn =>
57 | nn.distanceEuclide(pos) > 2.0 || (nn.distance(pos) == 2 && pos
58 | .distanceEuclide(neighbor) == 1.0)
59 | )
60 | } yield (pos, neighbor) -> pushable).toMap
61 | map
62 | }
63 |
64 | def isPlayable(height: Int): Boolean = height > -1 && height < 4
65 | }
66 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/samplegames/wondev/domain/WondevState.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.samplegames.wondev.domain
2 |
3 | import com.truelaurel.algorithm.game.GameState
4 | import com.truelaurel.math.geometry.Pos
5 |
6 | case class WondevState(
7 | size: Int,
8 | heightMap: Map[Pos, Int],
9 | units: Seq[Pos],
10 | legalActions: Seq[WondevAction],
11 | nextPlayer: Boolean = true
12 | ) extends GameState[Boolean] {}
13 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/samplegames/wondev/io/WondevIO.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.samplegames.wondev.io
2 |
3 | import com.truelaurel.codingame.challenge.GameIO
4 | import com.truelaurel.math.geometry.{Direction, Pos}
5 | import com.truelaurel.samplegames.wondev.domain._
6 |
7 | import scala.io.StdIn
8 |
9 | object WondevIO extends GameIO[WondevContext, WondevState, WondevAction] {
10 | def readContext: WondevContext = {
11 | val size = StdIn.readInt()
12 | val unitsperplayer = StdIn.readInt()
13 | WondevContext(size, unitsperplayer)
14 | }
15 |
16 | def readState(turn: Int, context: WondevContext): WondevState = {
17 |
18 | val rows = Seq.fill(context.size)(StdIn.readLine())
19 | val myUnits = Seq.fill(context.players) {
20 | val Array(unitx, unity) =
21 | for (i <- StdIn.readLine() split " ") yield i.toInt
22 | Pos(unitx, unity)
23 | }
24 | val opUnits = Seq.fill(context.players) {
25 | val Array(unitx, unity) =
26 | for (i <- StdIn.readLine() split " ") yield i.toInt
27 | Pos(unitx, unity)
28 | }
29 |
30 | val units = myUnits ++ opUnits
31 |
32 | val legalactions = Seq.fill(StdIn.readInt()) {
33 | val Array(_type, _index, dir1, dir2) = StdIn.readLine() split " "
34 | val index = _index.toInt
35 | val unit = units(index)
36 | val d1 = Direction(dir1)
37 | val d2 = Direction(dir2)
38 | val target1 = unit.neighborIn(d1)
39 | val target2 = target1.neighborIn(d2)
40 | if (_type == "MOVE&BUILD") {
41 | MoveBuild(index, target1, target2)
42 | } else {
43 | PushBuild(index, target1, target2)
44 | }
45 | }
46 |
47 | val heights = (for {
48 | (row: String, y: Int) <- rows.zipWithIndex
49 | (cell: Char, x) <- row.zipWithIndex
50 | h = if (cell == '.') -1 else (cell - '0')
51 | } yield Pos(x, y) -> h).toMap
52 | WondevState(context.size, heights, units, legalactions)
53 | }
54 |
55 | override def writeAction(state: WondevState, action: WondevAction): Unit = {
56 | action match {
57 | case MoveBuild(unitIndex, move, build) =>
58 | val pos = state.units(unitIndex)
59 | val d1 = Direction.all.find(d => pos.neighborIn(d) == move).get
60 | val d2 = Direction.all.find(d => move.neighborIn(d) == build).get
61 | System.out.println(s"MOVE&BUILD $unitIndex $d1 $d2")
62 | case PushBuild(unitIndex, build, push) =>
63 | val pos = state.units(unitIndex)
64 | val d1 = Direction.all.find(d => pos.neighborIn(d) == build).get
65 | val d2 = Direction.all.find(d => build.neighborIn(d) == push).get
66 | System.out.println(s"PUSH&BUILD $unitIndex $d1 $d2")
67 | case AcceptDefeat => System.out.println("ACCEPT-DEFEAT")
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/samplegames/wondev/simulation/WondevSimulator.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.samplegames.wondev.simulation
2 |
3 | import com.truelaurel.math.geometry.Pos
4 | import com.truelaurel.samplegames.wondev.analysis.WondevEvaluator
5 | import com.truelaurel.samplegames.wondev.domain._
6 |
7 | import scala.collection.mutable.ArrayBuffer
8 |
9 | object WondevSimulator {
10 | def nextLegalActions(state: WondevState): Seq[WondevAction] =
11 | nextLegalActions(MutableWondevState.fromSlowState(state))
12 |
13 | def next(
14 | fromState: MutableWondevState,
15 | wondevAction: WondevAction
16 | ): MutableWondevState = {
17 | fromState.writable.start()
18 | wondevAction match {
19 | case MoveBuild(unitIndex, move, build) =>
20 | fromState.writable.moveUnit(unitIndex, move)
21 | if (
22 | fromState.readable
23 | .isFree(build) || (build == fromState.readable.unitAt(unitIndex))
24 | ) {
25 | fromState.writable.increaseHeight(build)
26 | }
27 | case PushBuild(_, build, push) =>
28 | if (fromState.readable.isFree(push)) {
29 | val pushedUnitIndex = fromState.readable.unitIdOf(build)
30 | fromState.writable.moveUnit(pushedUnitIndex, push)
31 | fromState.writable.increaseHeight(build)
32 | }
33 | case AcceptDefeat =>
34 | throw new IllegalStateException("Unable to simulate this defeat action")
35 | }
36 | fromState.writable.swapPlayer()
37 | fromState.writable.end()
38 | fromState
39 | }
40 |
41 | def nextLegalActions(state: MutableWondevState): Seq[WondevAction] = {
42 |
43 | val myStart = if (state.nextPlayer) 0 else 2
44 | val opStart = if (state.nextPlayer) 2 else 0
45 | val myUnits =
46 | if (state.nextPlayer) state.readable.myUnits else state.readable.opUnits
47 |
48 | val actions: Array[WondevAction] = Array.ofDim(128)
49 | var aid = 0
50 | var id = myStart
51 | while (id < myStart + 2) {
52 | val unit = state.readable.unitAt(id)
53 | val h = state.readable.heightOf(unit)
54 | var nid = 0
55 | val neighbors1 = state.readable.neighborOf(unit)
56 | while (nid < neighbors1.length) {
57 | val target1 = neighbors1(nid)
58 | val h1 = state.readable.heightOf(target1)
59 | if (
60 | WondevContext
61 | .isPlayable(h1) && h + 1 >= h1 && state.readable.isFree(target1)
62 | ) {
63 | val neighbors2 = state.readable.neighborOf(target1)
64 | var nid2 = 0
65 | while (nid2 < neighbors2.length) {
66 | val target2 = neighbors2(nid2)
67 | val h2 = state.readable.heightOf(target2)
68 | if (
69 | WondevContext.isPlayable(h2) && (state.readable.isFree(
70 | target2
71 | ) || target2 == unit || myUnits.forall(_.distance(target2) > 1))
72 | ) {
73 | actions(aid) = MoveBuild(id, target1, target2)
74 | aid += 1
75 | }
76 | nid2 += 1
77 | }
78 | }
79 | nid += 1
80 | }
81 |
82 | var oid = opStart
83 | while (oid < opStart + 2) {
84 | val target1 = state.readable.unitAt(oid)
85 | if (WondevContext.isVisible(target1) && unit.distance(target1) == 1) {
86 | val pushTargets: Array[Pos] =
87 | WondevContext.pushTargets(state.size)(unit, target1)
88 |
89 | var pid = 0
90 | while (pid < pushTargets.length) {
91 | val target2 = pushTargets(pid)
92 | val h2 = state.readable.heightOf(target2)
93 | if (
94 | WondevContext.isPlayable(h2) && state.readable.heightOf(
95 | target1
96 | ) + 1 >= h2 && (myUnits.forall(
97 | _.distance(target2) > 1
98 | ) || state.readable.isFree(target2))
99 | ) {
100 | actions(aid) = PushBuild(id, target1, target2)
101 | aid += 1
102 | }
103 | pid += 1
104 | }
105 | }
106 |
107 | oid += 1
108 | }
109 | id += 1
110 | }
111 | actions.take(aid)
112 | }
113 |
114 | }
115 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/samplegames/wondev/strategy/WondevBotDebug.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.samplegames.wondev.strategy
2 |
3 | import com.truelaurel.math.geometry.Pos
4 | import com.truelaurel.samplegames.wondev.analysis.WarFogCleaner
5 | import com.truelaurel.samplegames.wondev.domain.{
6 | MoveBuild,
7 | PushBuild,
8 | WondevState
9 | }
10 | import com.truelaurel.samplegames.wondev.io.WondevIO
11 |
12 | /**
13 | * Created by hwang on 24/06/2017.
14 | */
15 | object WondevBotDebug {
16 |
17 | def main(args: Array[String]): Unit = {
18 |
19 | val player = MinimaxPlayer
20 | val observed = WondevState(
21 | 6,
22 | Map(
23 | Pos(2, 5) -> 0,
24 | Pos(1, 5) -> 0,
25 | Pos(5, 0) -> 0,
26 | Pos(0, 2) -> 0,
27 | Pos(0, 0) -> 0,
28 | Pos(5, 2) -> 0,
29 | Pos(5, 1) -> 0,
30 | Pos(4, 0) -> -1,
31 | Pos(3, 4) -> 2,
32 | Pos(3, 1) -> -1,
33 | Pos(4, 1) -> 0,
34 | Pos(2, 0) -> 0,
35 | Pos(0, 3) -> 0,
36 | Pos(4, 4) -> 0,
37 | Pos(3, 0) -> 0,
38 | Pos(0, 5) -> 0,
39 | Pos(1, 1) -> 1,
40 | Pos(3, 5) -> 0,
41 | Pos(4, 5) -> 0,
42 | Pos(1, 4) -> 0,
43 | Pos(0, 4) -> 0,
44 | Pos(5, 4) -> 0,
45 | Pos(3, 2) -> 0,
46 | Pos(1, 3) -> 0,
47 | Pos(2, 2) -> 0,
48 | Pos(5, 5) -> 0,
49 | Pos(4, 2) -> 0,
50 | Pos(2, 4) -> 0,
51 | Pos(0, 1) -> 1,
52 | Pos(5, 3) -> 0,
53 | Pos(3, 3) -> 0,
54 | Pos(2, 3) -> 0,
55 | Pos(1, 2) -> 0,
56 | Pos(2, 1) -> -1,
57 | Pos(4, 3) -> 0,
58 | Pos(1, 0) -> -1
59 | ),
60 | List(Pos(0, 0), Pos(3, 3), Pos(3, 2), Pos(1, 1)),
61 | List(
62 | MoveBuild(0, Pos(0, 1), Pos(0, 0)),
63 | MoveBuild(0, Pos(0, 1), Pos(0, 2)),
64 | MoveBuild(0, Pos(0, 1), Pos(1, 2)),
65 | MoveBuild(1, Pos(4, 3), Pos(5, 3)),
66 | MoveBuild(1, Pos(4, 3), Pos(4, 2)),
67 | MoveBuild(1, Pos(4, 3), Pos(5, 2)),
68 | MoveBuild(1, Pos(4, 3), Pos(4, 4)),
69 | MoveBuild(1, Pos(4, 3), Pos(5, 4)),
70 | MoveBuild(1, Pos(4, 3), Pos(3, 4)),
71 | MoveBuild(1, Pos(4, 3), Pos(3, 3)),
72 | MoveBuild(1, Pos(4, 2), Pos(5, 2)),
73 | MoveBuild(1, Pos(4, 2), Pos(4, 1)),
74 | MoveBuild(1, Pos(4, 2), Pos(5, 1)),
75 | MoveBuild(1, Pos(4, 2), Pos(4, 3)),
76 | MoveBuild(1, Pos(4, 2), Pos(5, 3)),
77 | MoveBuild(1, Pos(4, 2), Pos(3, 3)),
78 | MoveBuild(1, Pos(2, 2), Pos(2, 3)),
79 | MoveBuild(1, Pos(2, 2), Pos(3, 3)),
80 | MoveBuild(1, Pos(2, 2), Pos(1, 3)),
81 | MoveBuild(1, Pos(2, 2), Pos(1, 2)),
82 | MoveBuild(1, Pos(4, 4), Pos(5, 4)),
83 | MoveBuild(1, Pos(4, 4), Pos(4, 3)),
84 | MoveBuild(1, Pos(4, 4), Pos(5, 3)),
85 | MoveBuild(1, Pos(4, 4), Pos(3, 3)),
86 | MoveBuild(1, Pos(4, 4), Pos(4, 5)),
87 | MoveBuild(1, Pos(4, 4), Pos(5, 5)),
88 | MoveBuild(1, Pos(4, 4), Pos(3, 5)),
89 | MoveBuild(1, Pos(4, 4), Pos(3, 4)),
90 | MoveBuild(1, Pos(2, 4), Pos(3, 4)),
91 | MoveBuild(1, Pos(2, 4), Pos(2, 3)),
92 | MoveBuild(1, Pos(2, 4), Pos(3, 3)),
93 | MoveBuild(1, Pos(2, 4), Pos(1, 3)),
94 | MoveBuild(1, Pos(2, 4), Pos(2, 5)),
95 | MoveBuild(1, Pos(2, 4), Pos(3, 5)),
96 | MoveBuild(1, Pos(2, 4), Pos(1, 5)),
97 | MoveBuild(1, Pos(2, 4), Pos(1, 4)),
98 | MoveBuild(1, Pos(2, 3), Pos(3, 3)),
99 | MoveBuild(1, Pos(2, 3), Pos(2, 2)),
100 | MoveBuild(1, Pos(2, 3), Pos(1, 2)),
101 | MoveBuild(1, Pos(2, 3), Pos(2, 4)),
102 | MoveBuild(1, Pos(2, 3), Pos(3, 4)),
103 | MoveBuild(1, Pos(2, 3), Pos(1, 4)),
104 | MoveBuild(1, Pos(2, 3), Pos(1, 3)),
105 | PushBuild(0, Pos(1, 1), Pos(1, 2)),
106 | PushBuild(0, Pos(1, 1), Pos(2, 2)),
107 | PushBuild(1, Pos(3, 2), Pos(4, 1))
108 | ),
109 | true
110 | )
111 |
112 | val previousAction = MoveBuild(1, Pos(3, 3), Pos(3, 4))
113 |
114 | val previousOppoScope = Set(
115 | Set(Pos(5, 5), Pos(2, 2)),
116 | Set(Pos(3, 0), Pos(2, 2)),
117 | Set(Pos(2, 2), Pos(1, 2)),
118 | Set(Pos(4, 2), Pos(0, 2)),
119 | Set(Pos(4, 2), Pos(2, 2)),
120 | Set(Pos(5, 4), Pos(2, 2)),
121 | Set(Pos(5, 4), Pos(0, 2)),
122 | Set(Pos(2, 2), Pos(4, 3))
123 | )
124 |
125 | val previousState = WondevState(
126 | 6,
127 | Map(
128 | Pos(2, 5) -> 0,
129 | Pos(1, 5) -> 0,
130 | Pos(5, 0) -> 0,
131 | Pos(0, 2) -> 0,
132 | Pos(0, 0) -> 0,
133 | Pos(5, 2) -> 0,
134 | Pos(5, 1) -> 0,
135 | Pos(4, 0) -> -1,
136 | Pos(3, 4) -> 1,
137 | Pos(3, 1) -> -1,
138 | Pos(4, 1) -> 0,
139 | Pos(2, 0) -> 0,
140 | Pos(0, 3) -> 0,
141 | Pos(4, 4) -> 0,
142 | Pos(3, 0) -> 0,
143 | Pos(0, 5) -> 0,
144 | Pos(1, 1) -> 1,
145 | Pos(3, 5) -> 0,
146 | Pos(4, 5) -> 0,
147 | Pos(1, 4) -> 0,
148 | Pos(0, 4) -> 0,
149 | Pos(5, 4) -> 0,
150 | Pos(3, 2) -> 0,
151 | Pos(1, 3) -> 0,
152 | Pos(2, 2) -> 0,
153 | Pos(5, 5) -> 0,
154 | Pos(4, 2) -> 0,
155 | Pos(2, 4) -> 0,
156 | Pos(0, 1) -> 0,
157 | Pos(5, 3) -> 0,
158 | Pos(3, 3) -> 0,
159 | Pos(2, 3) -> 0,
160 | Pos(1, 2) -> 0,
161 | Pos(2, 1) -> -1,
162 | Pos(4, 3) -> 0,
163 | Pos(1, 0) -> -1
164 | ),
165 | List(Pos(0, 0), Pos(2, 4), Pos(-1, -1), Pos(-1, -1)),
166 | List(
167 | MoveBuild(0, Pos(0, 1), Pos(1, 1)),
168 | MoveBuild(0, Pos(0, 1), Pos(0, 0)),
169 | MoveBuild(0, Pos(0, 1), Pos(0, 2)),
170 | MoveBuild(0, Pos(0, 1), Pos(1, 2)),
171 | MoveBuild(0, Pos(1, 1), Pos(2, 0)),
172 | MoveBuild(0, Pos(1, 1), Pos(0, 0)),
173 | MoveBuild(0, Pos(1, 1), Pos(1, 2)),
174 | MoveBuild(0, Pos(1, 1), Pos(2, 2)),
175 | MoveBuild(0, Pos(1, 1), Pos(0, 2)),
176 | MoveBuild(0, Pos(1, 1), Pos(0, 1)),
177 | MoveBuild(1, Pos(3, 4), Pos(4, 4)),
178 | MoveBuild(1, Pos(3, 4), Pos(3, 3)),
179 | MoveBuild(1, Pos(3, 4), Pos(4, 3)),
180 | MoveBuild(1, Pos(3, 4), Pos(2, 3)),
181 | MoveBuild(1, Pos(3, 4), Pos(3, 5)),
182 | MoveBuild(1, Pos(3, 4), Pos(4, 5)),
183 | MoveBuild(1, Pos(3, 4), Pos(2, 5)),
184 | MoveBuild(1, Pos(3, 4), Pos(2, 4)),
185 | MoveBuild(1, Pos(2, 3), Pos(3, 3)),
186 | MoveBuild(1, Pos(2, 3), Pos(2, 2)),
187 | MoveBuild(1, Pos(2, 3), Pos(3, 2)),
188 | MoveBuild(1, Pos(2, 3), Pos(1, 2)),
189 | MoveBuild(1, Pos(2, 3), Pos(2, 4)),
190 | MoveBuild(1, Pos(2, 3), Pos(3, 4)),
191 | MoveBuild(1, Pos(2, 3), Pos(1, 4)),
192 | MoveBuild(1, Pos(2, 3), Pos(1, 3)),
193 | MoveBuild(1, Pos(3, 3), Pos(4, 3)),
194 | MoveBuild(1, Pos(3, 3), Pos(3, 2)),
195 | MoveBuild(1, Pos(3, 3), Pos(4, 2)),
196 | MoveBuild(1, Pos(3, 3), Pos(2, 2)),
197 | MoveBuild(1, Pos(3, 3), Pos(3, 4)),
198 | MoveBuild(1, Pos(3, 3), Pos(4, 4)),
199 | MoveBuild(1, Pos(3, 3), Pos(2, 4)),
200 | MoveBuild(1, Pos(3, 3), Pos(2, 3)),
201 | MoveBuild(1, Pos(1, 3), Pos(2, 3)),
202 | MoveBuild(1, Pos(1, 3), Pos(1, 2)),
203 | MoveBuild(1, Pos(1, 3), Pos(2, 2)),
204 | MoveBuild(1, Pos(1, 3), Pos(0, 2)),
205 | MoveBuild(1, Pos(1, 3), Pos(1, 4)),
206 | MoveBuild(1, Pos(1, 3), Pos(2, 4)),
207 | MoveBuild(1, Pos(1, 3), Pos(0, 4)),
208 | MoveBuild(1, Pos(1, 3), Pos(0, 3)),
209 | MoveBuild(1, Pos(2, 5), Pos(3, 5)),
210 | MoveBuild(1, Pos(2, 5), Pos(2, 4)),
211 | MoveBuild(1, Pos(2, 5), Pos(3, 4)),
212 | MoveBuild(1, Pos(2, 5), Pos(1, 4)),
213 | MoveBuild(1, Pos(2, 5), Pos(1, 5)),
214 | MoveBuild(1, Pos(3, 5), Pos(4, 5)),
215 | MoveBuild(1, Pos(3, 5), Pos(3, 4)),
216 | MoveBuild(1, Pos(3, 5), Pos(4, 4)),
217 | MoveBuild(1, Pos(3, 5), Pos(2, 4)),
218 | MoveBuild(1, Pos(3, 5), Pos(2, 5)),
219 | MoveBuild(1, Pos(1, 5), Pos(2, 5)),
220 | MoveBuild(1, Pos(1, 5), Pos(1, 4)),
221 | MoveBuild(1, Pos(1, 5), Pos(2, 4)),
222 | MoveBuild(1, Pos(1, 5), Pos(0, 4)),
223 | MoveBuild(1, Pos(1, 5), Pos(0, 5)),
224 | MoveBuild(1, Pos(1, 4), Pos(2, 4)),
225 | MoveBuild(1, Pos(1, 4), Pos(1, 3)),
226 | MoveBuild(1, Pos(1, 4), Pos(2, 3)),
227 | MoveBuild(1, Pos(1, 4), Pos(0, 3)),
228 | MoveBuild(1, Pos(1, 4), Pos(1, 5)),
229 | MoveBuild(1, Pos(1, 4), Pos(2, 5)),
230 | MoveBuild(1, Pos(1, 4), Pos(0, 5)),
231 | MoveBuild(1, Pos(1, 4), Pos(0, 4))
232 | ),
233 | true
234 | )
235 |
236 | val updated = WarFogCleaner.restrictOppoScope(
237 | observed,
238 | previousState,
239 | previousAction,
240 | previousOppoScope
241 | )
242 |
243 | val removed = WarFogCleaner.removeFog(observed, updated)
244 |
245 | val action = MinimaxPlayer.react(removed)
246 |
247 | WondevIO.writeAction(removed, action)
248 | }
249 | }
250 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/samplegames/wondev/strategy/WondevMinimax.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.samplegames.wondev.strategy
2 |
3 | import com.truelaurel.algorithm.alphabeta.AlphaBetaAi2
4 | import com.truelaurel.algorithm.game.{Outcome, RulesFor2p, Undecided}
5 | import com.truelaurel.samplegames.wondev.analysis.WondevEvaluator
6 | import com.truelaurel.samplegames.wondev.domain._
7 | import com.truelaurel.samplegames.wondev.simulation.WondevSimulator
8 |
9 | /**
10 | * Created by hwang on 28/06/2017.
11 | */
12 | class WondevMinimax(val initial: MutableWondevState)
13 | extends RulesFor2p[MutableWondevState, WondevAction] {
14 |
15 | override def validMoves(state: MutableWondevState): Seq[WondevAction] = {
16 | WondevSimulator.nextLegalActions(state)
17 | }
18 |
19 | override def applyMove(
20 | state: MutableWondevState,
21 | move: WondevAction
22 | ): MutableWondevState = WondevSimulator.next(state, move)
23 |
24 | override def outcome(state: MutableWondevState): Outcome[Boolean] = Undecided
25 | }
26 |
27 | object MinimaxPlayer {
28 | def react(state: WondevState): WondevAction = {
29 |
30 | if (state.legalActions.isEmpty) AcceptDefeat
31 | else {
32 | val fastWondevState = MutableWondevState.fromSlowState(state)
33 | val rules = new WondevMinimax(fastWondevState)
34 | val minimax = AlphaBetaAi2(rules, WondevEvaluator.evaluate, moveScore)
35 | val action = minimax.bestMove(rules.initial, depth = 2)
36 | action
37 | }
38 | }
39 |
40 | def moveScore(action: WondevAction, state: MutableWondevState): Double = {
41 | -1.0 * (action match {
42 | case MoveBuild(unitIndex, move, build) => state.readable.heightOf(build)
43 | case PushBuild(unitIndex, build, push) =>
44 | 10 + state.readable.heightOf(build)
45 | case AcceptDefeat => 0.0
46 | })
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/samplegames/wondev/worksheet/wondev.sc:
--------------------------------------------------------------------------------
1 | import com.truelaurel.math.geometry.Pos
2 |
3 | val result: Set[Pos] = Set.empty
4 |
5 | result.forall(_.x > 0)
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/time/Chronometer.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.time
2 |
3 | import scala.concurrent.duration.Duration
4 |
5 | /**
6 | * Created by hwang on 12/02/2017.
7 | */
8 | class Chronometer(duration: Duration) extends Stopper {
9 | val budget: Long = duration.toNanos
10 | private var startTime: Long = 0
11 | private var elapsed: Long = 0
12 | private var maxTurnElapsed: Long = -1
13 |
14 | override def start(): Unit = {
15 | startTime = mark
16 | maxTurnElapsed = -1
17 | elapsed = 0
18 | }
19 |
20 | def mark: Long = {
21 | System.nanoTime()
22 | }
23 |
24 | override def willOutOfTime: Boolean = {
25 | val untilNow = mark - startTime
26 | val currentTurnElapsed = untilNow - elapsed
27 | maxTurnElapsed = Math.max(maxTurnElapsed, currentTurnElapsed)
28 | val remaining = budget - untilNow
29 | elapsed = untilNow
30 | //System.err.println(s"remaining $remaining max $maxTurnElapsed")
31 | remaining < maxTurnElapsed
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/time/CountStopper.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.time
2 |
3 | /**
4 | * Created by hwang on 20/04/2017.
5 | */
6 | class CountStopper(count: Int) extends Stopper {
7 | private var remaining = count
8 |
9 | override def start(): Unit = {}
10 |
11 | override def willOutOfTime: Boolean = {
12 | remaining = remaining - 1
13 | remaining < 0
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/scala/com/truelaurel/time/Stopper.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.time
2 |
3 | /**
4 | * Created by hwang on 20/04/2017.
5 | */
6 | trait Stopper {
7 |
8 | def start(): Unit
9 |
10 | def willOutOfTime: Boolean
11 | }
12 |
--------------------------------------------------------------------------------
/src/test/scala/com/truelaurel/algorithm/alphabeta/AlphaBetaAiTest.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.alphabeta
2 |
3 | import com.truelaurel.algorithm.game._
4 | import org.scalatest.{FlatSpec, Matchers}
5 |
6 | class AlphaBetaAiTest extends FlatSpec with Matchers {
7 |
8 | behavior of "AlphaBetaAi"
9 |
10 | val dummyRules = new RulesFor2p[DummyState, String] {
11 | def initial = DummyState("a", true)
12 |
13 | def validMoves(state: DummyState): Seq[String] =
14 | state.name match {
15 | case "a" => Seq("b", "f")
16 | case "b" => Seq("c", "d")
17 | case "f" => Seq("e", "g")
18 | case "g" => Seq("z")
19 | case "d" => Seq("x", "y")
20 | case _ => Nil
21 | }
22 |
23 | // a
24 | // / \
25 | // b f
26 | // / \ / \
27 | // c d e g
28 | // / \ \
29 | // x y z
30 |
31 | def applyMove(state: DummyState, move: String): DummyState =
32 | DummyState(move, !state.nextPlayer)
33 |
34 | def outcome(state: DummyState): Outcome[Boolean] =
35 | state.name match {
36 | case "y" => Wins(false)
37 | case "e" => Wins(true)
38 | case "c" => Draw
39 | case _ => Undecided
40 | }
41 | }
42 | val dummyUndecidedRules = new RulesFor2p[IntsState, Int] {
43 | def initial = IntsState()
44 |
45 | def validMoves(state: IntsState): Seq[Int] =
46 | Seq(3, 5, 7, 1, 9, 2, 10, 4, 6, 8)
47 |
48 | def applyMove(state: IntsState, move: Int): IntsState =
49 | state.play(move)
50 |
51 | def outcome(state: IntsState): Outcome[Boolean] = Undecided
52 | }
53 |
54 | def heuristic(state: DummyState): Double =
55 | state.name match {
56 | case "x" => 2
57 | case "z" => 4
58 | case _ => 0
59 | }
60 |
61 | it should "select best move" in {
62 | val chosenMove =
63 | AlphaBetaAi(dummyRules, heuristic).chooseMove(dummyRules.initial, 3)
64 | chosenMove shouldBe "b"
65 | }
66 |
67 | def heur(state: IntsState) =
68 | if (state.nextPlayer)
69 | state.trueNumbers.sum - state.falseNumbers.sum
70 | else
71 | state.falseNumbers.sum - state.trueNumbers.sum
72 |
73 | case class DummyState(name: String, nextPlayer: Boolean)
74 | extends GameState[Boolean]
75 |
76 | case class IntsState(
77 | nextPlayer: Boolean = true,
78 | trueNumbers: List[Int] = Nil,
79 | falseNumbers: List[Int] = Nil
80 | ) extends GameState[Boolean] {
81 |
82 | def play(i: Int): IntsState =
83 | if (nextPlayer)
84 | copy(false, trueNumbers = i :: trueNumbers)
85 | else copy(true, falseNumbers = i :: falseNumbers)
86 |
87 | }
88 |
89 | it should "select move with higher heuristic and depth 1" in {
90 | val chosenMove = AlphaBetaAi(dummyUndecidedRules, heur)
91 | .chooseMove(dummyUndecidedRules.initial, 1)
92 | chosenMove shouldBe 10
93 | }
94 |
95 | it should "select move with higher heuristic and depth 2" in {
96 | val chosenMove = AlphaBetaAi(dummyUndecidedRules, heur)
97 | .chooseMove(dummyUndecidedRules.initial, 2)
98 | chosenMove shouldBe 10
99 | }
100 |
101 | it should "select move with higher heuristic and depth 3" in {
102 | val chosenMove = AlphaBetaAi(dummyUndecidedRules, heur)
103 | .chooseMove(dummyUndecidedRules.initial, 3)
104 | chosenMove shouldBe 10
105 | }
106 |
107 | }
108 |
--------------------------------------------------------------------------------
/src/test/scala/com/truelaurel/algorithm/dichotomy/DichotomyTest.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.dichotomy
2 |
3 | import com.truelaurel.algorithm.dichotomy.Dichotomy._
4 | import org.scalatest.{FlatSpec, Matchers}
5 |
6 | /**
7 | * Created by Wei on 14/06/2017.
8 | */
9 | class DichotomyTest extends FlatSpec with Matchers {
10 |
11 | behavior of "Dichotomy Search"
12 |
13 | it should "return the cloest value of the criteria" in {
14 | // if criteria returns true,
15 | // it means that the current value is still higher than expection
16 | // thus we should guess a lower value
17 | search(3, 9, _ > 5) should equal(6)
18 | search(3, 9, _ >= 5) should equal(5)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/test/scala/com/truelaurel/algorithm/graph/BreathFirstShortestPathTest.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.graph
2 |
3 | import org.scalatest.{FlatSpec, Matchers}
4 |
5 | /**
6 | * Created by hwang on 15/04/2017.
7 | */
8 | class BreathFirstShortestPathTest extends FlatSpec with Matchers {
9 |
10 | behavior of "BreathFirstShortestPathTest"
11 |
12 | it should "findPaths with just one level" in {
13 | val graph = Map(
14 | 1 -> Vector(2, 3)
15 | )
16 | val obstacles: Set[Int] = Set.empty
17 | val finder = new BreathFirstShortestPathFinder(graph, obstacles)
18 | finder.findPaths(1, Set(2, 3)) should be(
19 | Map(2 -> Vector(2), 3 -> Vector(3))
20 | )
21 | }
22 |
23 | it should "findPaths with multiple levels" in {
24 | val graph = Map(
25 | 1 -> Vector(2, 3),
26 | 3 -> Vector(5),
27 | 2 -> Vector(4),
28 | 4 -> Vector(6)
29 | )
30 | val obstacles: Set[Int] = Set.empty
31 | val finder = new BreathFirstShortestPathFinder(graph, obstacles)
32 | finder.findPaths(1, Set(5, 6)) should be(
33 | Map(5 -> Vector(3, 5), 6 -> Vector(2, 4, 6))
34 | )
35 | }
36 |
37 | it should "findPaths with multiple levels and obstacles" in {
38 | val graph = Map(
39 | 1 -> Vector(2, 3),
40 | 3 -> Vector(5),
41 | 2 -> Vector(4, 7),
42 | 4 -> Vector(5, 6)
43 | )
44 | val obstacles = Set(3)
45 | val finder = new BreathFirstShortestPathFinder(graph, obstacles)
46 | finder.findPaths(1, Set(5, 6)) should be(
47 | Map(5 -> Vector(2, 4, 5), 6 -> Vector(2, 4, 6))
48 | )
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/src/test/scala/com/truelaurel/algorithm/graph/ShortestPathTest.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.graph
2 |
3 | import org.scalatest.{FlatSpec, Matchers}
4 |
5 | /**
6 | * Created by hwang on 02/03/2017.
7 | */
8 | class ShortestPathTest extends FlatSpec with Matchers {
9 |
10 | behavior of "ShortestPathSpec"
11 |
12 | it should "testShortestItinearies" in {
13 | val itinearies = ShortestPath.shortestItinearies(2, Vector(Edge(0, 1, 2)))
14 |
15 | itinearies should be(
16 | Map(
17 | 0 -> Map(
18 | 1 -> Iti(2, Vector(0, 1)),
19 | 0 -> Iti(0, Vector(0))
20 | ),
21 | 1 -> Map(
22 | 1 -> Iti(0, Vector(1))
23 | )
24 | )
25 | )
26 | }
27 |
28 | it should "shortest path should handle more elements" in {
29 | ShortestPath.shortestItinearies(
30 | 5,
31 | Vector(
32 | Edge(2, 1, 4),
33 | Edge(2, 3, 3),
34 | Edge(1, 3, -2),
35 | Edge(4, 2, -1),
36 | Edge(3, 4, 2)
37 | )
38 | ) should be(
39 | Map(
40 | 0 -> Map(0 -> Iti(0, Vector(0))),
41 | 1 -> Map(
42 | 2 -> Iti(-1, Vector(1, 3, 4, 2)),
43 | 4 -> Iti(0, Vector(1, 3, 4)),
44 | 1 -> Iti(0, Vector(1)),
45 | 3 -> Iti(-2, Vector(1, 3))
46 | ),
47 | 2 -> Map(
48 | 2 -> Iti(0, Vector(2)),
49 | 4 -> Iti(4, Vector(2, 1, 3, 4)),
50 | 1 -> Iti(4, Vector(2, 1)),
51 | 3 -> Iti(2, Vector(2, 1, 3))
52 | ),
53 | 3 -> Map(
54 | 2 -> Iti(1, Vector(3, 4, 2)),
55 | 4 -> Iti(2, Vector(3, 4)),
56 | 1 -> Iti(5, Vector(3, 4, 2, 1)),
57 | 3 -> Iti(0, Vector(3))
58 | ),
59 | 4 -> Map(
60 | 2 -> Iti(-1, Vector(4, 2)),
61 | 4 -> Iti(0, Vector(4)),
62 | 1 -> Iti(3, Vector(4, 2, 1)),
63 | 3 -> Iti(1, Vector(4, 2, 1, 3))
64 | )
65 | )
66 | )
67 | }
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/src/test/scala/com/truelaurel/algorithm/mcts/MctsNodeTest.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.mcts
2 |
3 | import com.truelaurel.algorithm.game._
4 | import org.scalatest.{FlatSpec, Matchers}
5 |
6 | class MctsNodeTest extends FlatSpec with Matchers {
7 |
8 | behavior of "MctsNode"
9 |
10 | val dummyRules = new RulesFor2p[DummyState, Int] {
11 | def initial = DummyState(Seq(1, 2, 3, 4), true)
12 |
13 | def validMoves(state: DummyState): Seq[Int] = state.s
14 |
15 | def applyMove(state: DummyState, move: Int): DummyState =
16 | DummyState(state.s.diff(Seq(move)), !state.nextPlayer)
17 |
18 | def outcome(state: DummyState): Outcome[Boolean] =
19 | state.s match {
20 | case Seq(1) => if (state.nextPlayer) Wins(false) else Wins(true)
21 | case _ => Undecided
22 | }
23 |
24 | }
25 | val rootNode = MctsNode(dummyRules.initial, dummyRules, randomPlay)
26 |
27 | def randomPlay(state: DummyState): Outcome[Boolean] =
28 | dummyRules.outcome(state)
29 |
30 | case class DummyState(s: Seq[Int], nextPlayer: Boolean)
31 | extends GameState[Boolean]
32 |
33 | it should "select least played node" in {
34 | val child1 =
35 | MctsNode(DummyState(Seq(2), false), dummyRules, randomPlay, Results(1, 0))
36 | val root = MctsNode(
37 | DummyState(Seq(1, 2), true),
38 | dummyRules,
39 | randomPlay,
40 | Results(1, 0),
41 | Map(1 -> child1)
42 | )
43 |
44 | root.moveToExplore shouldBe 2
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/test/scala/com/truelaurel/algorithm/mcts/ResultsTest.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.mcts
2 |
3 | import com.truelaurel.algorithm.game.Wins
4 | import org.scalatest.{FlatSpec, Matchers}
5 |
6 | class ResultsTest extends FlatSpec with Matchers {
7 |
8 | behavior of "Results"
9 |
10 | it should "keep wins" in {
11 | val res = Results()
12 | res.played shouldBe 0
13 | res.score shouldBe 0
14 |
15 | val p1 = res.withOutcome(Wins(true))
16 | p1.played shouldBe 1
17 | p1.score shouldBe 1
18 | p1.won shouldBe 1
19 | p1.lost shouldBe 0
20 | }
21 |
22 | it should "keep losses" in {
23 | val res = Results()
24 |
25 | val p1 = res.withOutcome(Wins(false))
26 | p1.played shouldBe 1
27 | p1.score shouldBe -1
28 | p1.won shouldBe 0
29 | p1.lost shouldBe 1
30 | }
31 |
32 | it should "find best move with current results" in {
33 | val moves = Map(
34 | 1 -> Results(played = 3, score = 2),
35 | 2 -> Results(played = 3, score = 1)
36 | )
37 |
38 | Results.mostPromisingMove(moves) shouldBe 2
39 | }
40 |
41 | it should "find move with no results" in {
42 | val moves = Map(
43 | 1 -> Results(played = 3, score = 2),
44 | 3 -> Results(played = 0, score = 0),
45 | 2 -> Results(played = 3, score = 1)
46 | )
47 |
48 | Results.mostPromisingMove(moves) shouldBe 3
49 | Results.mostPromisingMove(moves) shouldBe 3
50 | }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/src/test/scala/com/truelaurel/algorithm/metaheuristic/evolutionstrategy/MuPlusLambdaTest.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.metaheuristic.evolutionstrategy
2 |
3 | import com.truelaurel.time.CountStopper
4 | import org.scalatest.{FlatSpec, Matchers}
5 |
6 | class MuPlusLambdaTest extends FlatSpec with Matchers {
7 |
8 | behavior of "MuPlusLambda"
9 |
10 | val muSize = 20
11 | val popSize = 100
12 | val stopper = new CountStopper(1000)
13 |
14 | val mpl = new MuPlusLambda(muSize, popSize, stopper)
15 |
16 | it can "search for a good angle to throw a rock far" in {
17 | val result = mpl.search[RockThrowingSolution](new RockThrowingProblem)
18 | (result.degree - 45.0) should be < 1.0
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/src/test/scala/com/truelaurel/algorithm/metaheuristic/evolutionstrategy/RockThrowingProblem.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.metaheuristic.evolutionstrategy
2 |
3 | import com.truelaurel.algorithm.metaheuristic.model.Problem
4 |
5 | class RockThrowingProblem extends Problem[RockThrowingSolution] {
6 |
7 | override def randomSolution(): RockThrowingSolution = new RockThrowingSolution
8 |
9 | override def tweakSolution(
10 | solution: RockThrowingSolution
11 | ): RockThrowingSolution = {
12 | val newsolution = new RockThrowingSolution
13 | newsolution.setDegree(
14 | solution.degree + scala.util.Random.nextDouble * 10 - 5
15 | )
16 | newsolution
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/test/scala/com/truelaurel/algorithm/metaheuristic/evolutionstrategy/RockThrowingSolution.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.metaheuristic.evolutionstrategy
2 |
3 | import com.truelaurel.algorithm.metaheuristic.model.Solution
4 |
5 | class RockThrowingSolution extends Solution {
6 | var degree: Double = scala.util.Random.nextDouble * 90
7 |
8 | def setDegree(newdegree: Double): Unit =
9 | degree = Math.max(0.0, Math.min(90.0, newdegree))
10 |
11 | override def quality: Double = {
12 | val gravity = 1.0
13 | val initialSpeed = 1.0
14 | val verticalSpeed = initialSpeed * Math.sin(degree / 180 * Math.PI)
15 | val horizontalSpeed = initialSpeed * Math.cos(degree / 180 * Math.PI)
16 | val flightDuration = verticalSpeed / gravity * 2
17 | val distance = flightDuration * horizontalSpeed
18 | distance
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/test/scala/com/truelaurel/algorithm/metaheuristic/genetic/MoveSetGeneticRepresentation.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.metaheuristic.genetic
2 |
3 | import scala.util.Random
4 |
5 | case class MoveSetGeneticRepresentation(ref: Referee = null)
6 | extends GeneticRepresentation[MoveSet] {
7 | override def mutate(solution: MoveSet): MoveSet = {
8 | for (i <- 0 until 20) {
9 | if (Random.nextInt.abs % 100 < 10)
10 | solution.vecarr(i) = Vec(Random.nextInt.abs % 200, Random.nextInt % 180)
11 | }
12 | solution
13 | }
14 |
15 | override def randomSolution: MoveSet = {
16 | val vecarr = new Array[Vec](20)
17 | for (i <- 0 until 20)
18 | vecarr(i) = Vec(Random.nextInt.abs % 200, Random.nextInt % 180)
19 | MoveSet(vecarr)
20 | }
21 |
22 | override def crossover(
23 | solutionA: MoveSet,
24 | solutionB: MoveSet
25 | ): (MoveSet, MoveSet) = {
26 | val vecarrA = new Array[Vec](20)
27 | val vecarrB = new Array[Vec](20)
28 | for (i <- 0 until 20) {
29 | val vA = solutionA.vecarr(i)
30 | val sA = vA.speed
31 | val rA = vA.rotate
32 | val vB = solutionB.vecarr(i)
33 | val sB = vB.speed
34 | val rB = vB.rotate
35 | val nsA = (sA + Random.nextDouble * (sB - sA)).toInt
36 | val nrA = (rA + Random.nextDouble * (rB - rA)).toInt
37 | vecarrA(i) = Vec(nsA, nrA)
38 | val nsB = (sA + Random.nextDouble * (sB - sA)).toInt
39 | val nrB = (rA + Random.nextDouble * (rB - rA)).toInt
40 | vecarrB(i) = Vec(nsB, nrB)
41 | }
42 | (MoveSet(vecarrA), MoveSet(vecarrB))
43 | }
44 |
45 | override def assess(solution: MoveSet): AssessedSolution[MoveSet] =
46 | new AssessedSolution[MoveSet](solution, ref.evaluate(solution))
47 | }
48 |
--------------------------------------------------------------------------------
/src/test/scala/com/truelaurel/algorithm/metaheuristic/genetic/Pod.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.metaheuristic.genetic
2 |
3 | case class Vec(speed: Int = 0, rotate: Int = 0)
4 |
5 | case class MoveSet(vecarr: Array[Vec]) {}
6 |
7 | class Pod {
8 | var x: Int = 0
9 | var y: Int = 0
10 | var speedx: Int = 0
11 | var speedy: Int = 0
12 | var rotate: Int = 0
13 | var count: Int = 0
14 |
15 | def move(v: Vec): (Int, Int) = {
16 | rotate += Math.min(18, Math.max(-18, v.rotate - rotate))
17 | val tmpspeed = Math.min(200, Math.max(0, v.speed))
18 | val tmpag = rotate * Math.PI / 180
19 | val tmpspeedx = Math.cos(tmpag) * tmpspeed
20 | val tmpspeedy = Math.sin(tmpag) * tmpspeed
21 | speedx = (speedx * 0.85 + tmpspeedx).toInt
22 | speedy = (speedy * 0.85 + tmpspeedy).toInt
23 | x += speedx
24 | y += speedy
25 | (x, y)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/test/scala/com/truelaurel/algorithm/metaheuristic/genetic/Referee.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.metaheuristic.genetic
2 |
3 | case class Referee(wps: Array[WayPoint] = null) {
4 | val wpsize = wps.size
5 |
6 | def evaluate(ms: MoveSet): Double = {
7 | val pod = new Pod
8 | var i = 1
9 | for (v <- ms.vecarr) {
10 | pod.move(v)
11 | if (wps(i % wpsize).touch(pod)) {
12 | i += 1
13 | }
14 | }
15 | i * 1000 - wps(i % wpsize).dist(pod)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/test/scala/com/truelaurel/algorithm/metaheuristic/genetic/WayPoint.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.metaheuristic.genetic
2 |
3 | class WayPoint(val x: Int, val y: Int) {
4 | var next: WayPoint = null
5 |
6 | def dist(p: Pod): Double =
7 | Math.sqrt(Math.pow(x - p.x, 2) + Math.pow(y - p.y, 2))
8 |
9 | def touch(p: Pod): Boolean = dist(p) < 400
10 | }
11 |
--------------------------------------------------------------------------------
/src/test/scala/com/truelaurel/algorithm/metaheuristic/simulatedannealing/LanderBasicProblem.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.algorithm.metaheuristic.simulatedannealing
2 |
3 | import com.truelaurel.algorithm.metaheuristic.model.{Problem, Solution}
4 | import com.truelaurel.fp.Unfold
5 |
6 | import scala.concurrent.duration.DurationInt
7 | import scala.math._
8 | import scala.util.Random
9 |
10 | // This file demonstrates the usage of the "simulated annealing" algorithm
11 | // for a Mars Lander simple problem (https://www.codingame.com/ide/puzzle/mars-lander-episode-1)
12 | object LanderBasicDemo {
13 | def main(args: Array[String]): Unit = {
14 | val initialState =
15 | LanderBasicState(y = 1000, vy = 100, fuel = 500, power = 0)
16 | val sol = new SimulatedAnnealing(200.millis, 1000, 0.99)
17 | .search(initialState.problem)
18 | println(sol.quality, initialState.endState(sol.moves), sol)
19 | }
20 | }
21 |
22 | case class LanderBasicState(y: Int, vy: Int, fuel: Int, power: Int) {
23 | state =>
24 |
25 | object problem extends Problem[LanderBasicSolution] {
26 |
27 | def randomSolution(): LanderBasicSolution = {
28 | val moves = Unfold.unfold(state) { s =>
29 | if (s.y <= 0 || s.fuel <= 0 || s.y > 3000) None
30 | else {
31 | val nextMove = s.genNextMove
32 | val nextState = s.apply(nextMove)
33 | Some((nextMove, nextState))
34 | }
35 | }
36 | LanderBasicSolution(moves)
37 | }
38 |
39 | def tweakSolution(solution: LanderBasicSolution): LanderBasicSolution = {
40 | val r = Random.nextInt(solution.moves.size)
41 | val kept = solution.moves.take(r)
42 | val intermediateState = endState(kept)
43 | val newEndMoves = intermediateState.problem.randomSolution().moves
44 | LanderBasicSolution(kept ++ newEndMoves)
45 | }
46 | }
47 |
48 | case class LanderBasicSolution(moves: Stream[LanderBasicMove])
49 | extends Solution {
50 | val quality: Double = {
51 | val finalState = endState(moves)
52 | if (finalState.y > 3000) -finalState.y
53 | else if (finalState.canLandSafely) 1000 + finalState.fuel
54 | else finalState.vy
55 | }
56 | }
57 |
58 | def endState(movesToApply: Stream[LanderBasicMove]): LanderBasicState =
59 | movesToApply match {
60 | case h #:: t if !canLandSafely => apply(h).endState(t)
61 | case _ => this
62 | }
63 |
64 | def canLandSafely: Boolean = y == 0 && vy.abs <= 40
65 |
66 | val angle = 0
67 | val G = 3.711
68 |
69 | def actualThrust(move: LanderBasicMove): Int =
70 | if (fuel <= 0) (move.thrust - 1) max 0
71 | else if (move.thrust > power) (power + 1) min 4
72 | else if (move.thrust < power) (power - 1) max 0
73 | else power
74 |
75 | def apply(move: LanderBasicMove): LanderBasicState = {
76 | // https://www.codingame.com/forum/t/mars-lander-physics-simulator/1459/2
77 | val thrust = actualThrust(move)
78 | val ny = y + vy + 0.5 * (cos(angle * Pi / 180) * thrust - G)
79 | val nvy = vy + (cos(angle * Pi / 180) * thrust - G)
80 | LanderBasicState(ny.toInt, nvy.toInt, fuel - thrust, thrust)
81 | }
82 |
83 | val initialMove = LanderBasicMove(power)
84 |
85 | def genNextMove: LanderBasicMove = {
86 | val r = Random.nextInt(3)
87 | val move = state.initialMove
88 | val nextMove =
89 | if (r == 0 && move.thrust < 4) LanderBasicMove(move.thrust + 1)
90 | else if (r == 1 && move.thrust > 0) LanderBasicMove(move.thrust - 1)
91 | else move
92 | nextMove
93 | }
94 | }
95 |
96 | case class LanderBasicMove(thrust: Int)
97 |
--------------------------------------------------------------------------------
/src/test/scala/com/truelaurel/codingame/bundler/BundlerTest.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.codingame.bundler
2 |
3 | import java.io.File
4 |
5 | import com.truelaurel.codingame.tool.bundle.{Bundler, BundlerIo, StdBundlerIo}
6 | import com.truelaurel.compilation.Compilation._
7 | import org.scalactic._
8 | import org.scalatest.{FlatSpec, Matchers}
9 |
10 | class BundlerTest extends FlatSpec with Matchers {
11 | behavior of "Bundler"
12 |
13 | it should "copy simple content" in {
14 | //GIVEN
15 | val inputName = "Demo.scala"
16 | val content = "object Demo extends App"
17 | val io = prepareMockIo(Map(inputName -> content))
18 | //WHEN
19 | val output = Bundler(inputName, io).buildOutput
20 | //THEN
21 | output should equal(content)(after being linefeedNormalised)
22 | compiles(output) shouldBe true
23 | }
24 |
25 | it should "move simple content in root package" in {
26 | //GIVEN
27 | val inputName = "pkg/Demo.scala"
28 | val content =
29 | """package pkg
30 | |object Demo extends App""".stripMargin
31 | val io = prepareMockIo(Map(inputName -> content))
32 | //WHEN
33 | val output = Bundler(inputName, io).buildOutput
34 | //THEN
35 | output should equal("object Demo extends App")(
36 | after being linefeedNormalised
37 | )
38 | compiles(output) shouldBe true
39 | }
40 |
41 | it should "keep scala imports" in {
42 | //GIVEN
43 | val inputName = "Demo.scala"
44 | val content =
45 | """import scala.io.Source._
46 | |object Demo extends App""".stripMargin
47 | val io = prepareMockIo(Map(inputName -> content))
48 | //WHEN
49 | val output = Bundler(inputName, io).buildOutput
50 | //THEN
51 | output should equal(content)(after being linefeedNormalised)
52 | compiles(output) shouldBe true
53 | }
54 |
55 | it should "strip multiline comments" in {
56 | //GIVEN
57 | val inputName = "Demo.scala"
58 | val content =
59 | """object Demo extends App {
60 | |/*
61 | |comment out
62 | |*/
63 | |println("hello")
64 | |}""".stripMargin
65 | val io = prepareMockIo(Map(inputName -> content))
66 | //WHEN
67 | val output = Bundler(inputName, io).buildOutput
68 | //THEN
69 | output should equal("""object Demo extends App {
70 | |
71 | |println("hello")
72 | |}""".stripMargin)(after being linefeedNormalised)
73 | compiles(output) shouldBe true
74 | }
75 |
76 | it should "copy also file in same folder" in {
77 | //GIVEN
78 | val inputName = "pkg/Demo.scala"
79 | val content = "object Demo extends App"
80 | val utilName = "pkg/Util.scala"
81 | val utilContent = "object Util { def abs(x:Int) = if(x>0) x else -x }"
82 | val io = prepareMockIo(Map(inputName -> content, utilName -> utilContent))
83 | //WHEN
84 | val output = Bundler("Demo.scala", io).buildOutput
85 | //THEN
86 | val expected =
87 | """object Demo extends App
88 | |object Util { def abs(x:Int) = if(x>0) x else -x }""".stripMargin
89 | output should equal(expected)(after being linefeedNormalised)
90 | compiles(output) shouldBe true
91 | }
92 |
93 | it should "keep import Util._" in {
94 | //GIVEN
95 | val inputName = "pkg/Demo.scala"
96 | val content =
97 | """import util.Util._
98 | |object Demo extends App""".stripMargin
99 | val utilName = "util/Util.scala"
100 | val utilContent =
101 | """package util
102 | |object Util { def abs(x:Int) = if(x>0) x else -x }""".stripMargin
103 | val io = prepareMockIo(Map(inputName -> content, utilName -> utilContent))
104 | //WHEN
105 | val output = Bundler("Demo.scala", io).buildOutput
106 | //THEN
107 | val expected =
108 | """package util {
109 | |object Util { def abs(x:Int) = if(x>0) x else -x }
110 | |}
111 | |
112 | |import util.Util._
113 | |object Demo extends App""".stripMargin
114 | output should equal(expected)(after being linefeedNormalised)
115 | compiles(output) shouldBe true
116 | }
117 |
118 | it should "resolve import from another package" in {
119 | //GIVEN
120 | val inputName = "Demo.scala"
121 | val content =
122 | """import util.Util
123 | |object Demo extends App""".stripMargin
124 | val utilName = "util/Util.scala"
125 | val utilContent =
126 | """package util
127 | |object Util { def abs(x:Int) = if(x>0) x else -x }""".stripMargin
128 | val io = prepareMockIo(Map(inputName -> content, utilName -> utilContent))
129 | //WHEN
130 | val output = Bundler(inputName, io).buildOutput
131 | //THEN
132 | val expected =
133 | """package util {
134 | |object Util { def abs(x:Int) = if(x>0) x else -x }
135 | |}
136 | |
137 | |import util.Util
138 | |object Demo extends App""".stripMargin
139 | output should equal(expected)(after being linefeedNormalised)
140 | compiles(output) shouldBe true
141 | }
142 |
143 | it should "resolve 2 imports from another package" in {
144 | //GIVEN
145 | val inputName = "Demo.scala"
146 | val content =
147 | """import util.Util
148 | |object Demo extends App""".stripMargin
149 | val utilName = "util/Util.scala"
150 | val utilContent =
151 | """package util
152 | |object Util { def abs(x:Int) = if(x>0) x else -x }""".stripMargin
153 | val utilName2 = "util/Util2.scala"
154 | val utilContent2 =
155 | """package util
156 | |object Util2 { def sqr(x:Int) = x * x }""".stripMargin
157 | val io = prepareMockIo(
158 | Map(
159 | inputName -> content,
160 | utilName -> utilContent,
161 | utilName2 -> utilContent2
162 | )
163 | )
164 | //WHEN
165 | val output = Bundler(inputName, io).buildOutput
166 | //THEN
167 | val expected =
168 | """package util {
169 | |object Util { def abs(x:Int) = if(x>0) x else -x }
170 | |object Util2 { def sqr(x:Int) = x * x }
171 | |}
172 | |
173 | |import util.Util
174 | |object Demo extends App""".stripMargin
175 | output should equal(expected)(after being linefeedNormalised)
176 | compiles(output) shouldBe true
177 | }
178 |
179 | it should "resolve imports from 2 other packages with the same class name" in {
180 | //GIVEN
181 | val inputName = "Demo.scala"
182 | val content =
183 | """import util2.Util
184 | |object Demo extends App {
185 | | Util.sqr(3)
186 | |}
187 | |""".stripMargin
188 | val utilName = "util/Util.scala"
189 | val utilContent =
190 | """package util
191 | |object Util { def abs(x:Int) = if(x>0) x else -x }""".stripMargin
192 | val utilName2 = "util2/Util.scala"
193 | val utilContent2 =
194 | """package util2
195 | |
196 | |import util.Util._
197 | |
198 | |object Util { def sqr(x:Int) = abs(x * x) }""".stripMargin
199 | val io = prepareMockIo(
200 | Map(
201 | inputName -> content,
202 | utilName -> utilContent,
203 | utilName2 -> utilContent2
204 | )
205 | )
206 | //WHEN
207 | val output = Bundler(inputName, io).buildOutput
208 | //THEN
209 | val expected =
210 | """package util {
211 | | object Util { def abs(x:Int) = if(x>0) x else -x }
212 | |}
213 | |
214 | |package util2 {
215 | | import util.Util._
216 | |
217 | | object Util { def sqr(x:Int) = abs(x * x) }
218 | |}
219 | |
220 | |import util2.Util
221 | |
222 | |object Demo extends App {
223 | | Util.sqr(3)
224 | |}
225 | |""".stripMargin
226 | output should equal(expected)(after being linefeedNormalised)
227 | compiles(output) shouldBe true
228 | }
229 |
230 | it should "compile Wondev sample code" in {
231 | val output = Bundler("WondevChallenge.scala", StdBundlerIo()).buildOutput
232 | compiles(output) shouldBe true
233 | }
234 |
235 | private def prepareMockIo(fileContents: Map[String, String]): BundlerIo =
236 | new BundlerIo {
237 | val root = new File(".")
238 |
239 | val contentsByFile = fileContents.map {
240 | case (pathName, content) => new File(root, pathName) -> content
241 | }
242 |
243 | def filesInFolder(folder: File): List[File] =
244 | contentsByFile.keys.toList
245 | .filter(_.getParentFile == folder)
246 |
247 | def findFolder(packageElements: Array[String]): File =
248 | packageElements.init.foldLeft(root) {
249 | case (folder, pkg) => new File(folder, pkg)
250 | }
251 |
252 | def findFile(fileName: String): File =
253 | new File(root, fileContents.keys.find(fn => fn.endsWith(fileName)).get)
254 |
255 | def save(fileName: String, content: String): Unit = ???
256 |
257 | def readFile(file: File): List[String] = {
258 | val content = contentsByFile(file)
259 | content.split("""\r\n|\n""").toList
260 | }
261 | }
262 |
263 | private def linefeedNormalised: Uniformity[String] =
264 | new AbstractStringUniformity {
265 | def normalized(s: String): String =
266 | s.replaceAll("\r\n", "\n")
267 | .split("\n")
268 | .map(_.trim)
269 | .filterNot("".==)
270 | .mkString("\n")
271 |
272 | override def toString: String = "linefeedNormalised"
273 | }
274 |
275 | }
276 |
--------------------------------------------------------------------------------
/src/test/scala/com/truelaurel/collection/CollectionslTest.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.collection
2 |
3 | import com.truelaurel.collection.Collectionsl._
4 | import org.scalatest.{FlatSpec, Matchers}
5 |
6 | /**
7 | * Created by Wei on 14/06/2017.
8 | */
9 | class CollectionslTest extends FlatSpec with Matchers {
10 |
11 | behavior of "Adjust Function"
12 |
13 | it should "Update the assigned key with the assigned function" in {
14 | adjust(Map(1 -> 2))(1)(_ + 1)(1) should equal(3)
15 | }
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/src/test/scala/com/truelaurel/compilation/Compilation.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.compilation
2 |
3 | import scala.reflect.runtime.currentMirror
4 | import scala.tools.reflect.ToolBox
5 |
6 | object Compilation {
7 |
8 | def compiles(code: String): Boolean = {
9 | val toolBox = currentMirror.mkToolBox()
10 | toolBox.parse(code)
11 | true
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/test/scala/com/truelaurel/compilation/CompilationTest.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.compilation
2 |
3 | import com.truelaurel.compilation.Compilation.compiles
4 | import org.scalatest.{FlatSpec, Matchers}
5 |
6 | import scala.tools.reflect.ToolBoxError
7 |
8 | class CompilationTest extends FlatSpec with Matchers {
9 | behavior of "Compilation"
10 |
11 | it should "compile valid code" in {
12 | compiles("object Toto extends App { println() }") shouldBe true
13 | }
14 |
15 | it should "reject invalid code" in {
16 | intercept[ToolBoxError] {
17 | compiles("object Toto extends App { println) }")
18 | }
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/src/test/scala/com/truelaurel/math/MathlTest.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.math
2 |
3 | import com.truelaurel.math.Mathl._
4 | import org.scalatest.{FlatSpec, Matchers}
5 |
6 | /**
7 | * Created by Wei on 14/06/2017.
8 | */
9 | class MathlTest extends FlatSpec with Matchers {
10 |
11 | behavior of "Random Number"
12 |
13 | it can "change" in {
14 | for (i <- 0 until 100)
15 | random.nextInt should not equal (random.nextInt)
16 | }
17 |
18 | it can "set a range" in {
19 | for (i <- 0 until 100) {
20 | val value = randomBetween(-1.0, 1.0)
21 | value should be <= 1.0
22 | value should be >= -1.0
23 | }
24 | }
25 |
26 | behavior of "Half Up"
27 |
28 | it should "have different behavior for positive and negative number" in {
29 | val value1 = 1.5
30 | val value2 = -1.5
31 | halfUp(value1).abs should not equal (halfUp(value2))
32 | }
33 |
34 | behavior of "Almost Equal"
35 |
36 | it should "have no difference dealing with computing error" in {
37 | val value1 = 0.0
38 | val value2 = 0.000001
39 | for (i <- 0 until 100)
40 | almostEqual(
41 | randomBetween(value1, value2),
42 | randomBetween(value1, value2)
43 | ) should equal(true)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/test/scala/com/truelaurel/math/geometry/CircleTest.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.math.geometry
2 |
3 | import org.scalatest.{FlatSpec, Matchers}
4 |
5 | class CircleTest extends FlatSpec with Matchers {
6 |
7 | "intersection" should "be empty for circles too far away" in {
8 | Circle(Pos(100, 150), 100)
9 | .intersections(Circle(Pos(500, 150), 100)) shouldBe empty
10 | }
11 |
12 | it should "be a single point when tangent" in {
13 | Circle(Pos(0, 0), 100).intersections(Circle(Pos(200, 0), 100)) shouldBe Seq(
14 | Pos(100, 0)
15 | )
16 | Circle(Pos(0, 400), 200)
17 | .intersections(Circle(Pos(600, 400), 400)) shouldBe Seq(Pos(200, 400))
18 | }
19 |
20 | it should "be a 2 points when secant" in {
21 | Circle(Pos(0, 0), 100).intersections(Circle(Pos(0, 75), 100)) should
22 | contain theSameElementsAs Seq(Pos(92, 37), Pos(-92, 37))
23 |
24 | Circle(Pos(0, 50), 100).intersections(Circle(Pos(50, 0), 100)) should
25 | contain theSameElementsAs Seq(Pos(91, 91), Pos(-41, -41))
26 | //detects rounding bug
27 | Circle(Pos(1735, 542), 180)
28 | .intersections(Circle(Pos(1476, 490), 220)) should
29 | contain theSameElementsAs List(Pos(1664, 376), Pos(1605, 667))
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/test/scala/com/truelaurel/math/geometry/VectorlTest.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.math.geometry
2 |
3 | import org.scalactic.TolerantNumerics
4 | import org.scalatest.{FlatSpec, Matchers}
5 |
6 | /**
7 | * Created by hwang on 04/04/2017.
8 | */
9 | class VectorlTest extends FlatSpec with Matchers {
10 |
11 | behavior of "Vectorl"
12 | implicit val doubleEquality = TolerantNumerics.tolerantDoubleEquality(0.01)
13 |
14 | val vectorl = Vectorl(1, 1)
15 | val sqrtOf2: Double = Math.sqrt(2)
16 |
17 | it should "rotate in degrees" in {
18 | vectorl.rotateInDegree(90) should be(Vectorl(-1, 1))
19 | vectorl.rotateInDegree(45) should be(Vectorl(0, sqrtOf2))
20 | vectorl.rotateInDegree(180) should be(Vectorl(-1, -1))
21 | vectorl.rotateInDegree(270) should be(Vectorl(1, -1))
22 | vectorl.rotateInDegree(-45) should be(Vectorl(sqrtOf2, 0))
23 | }
24 |
25 | it should "rotate in radians" in {
26 | vectorl.rotateInRadian(Math.PI / 2) should be(Vectorl(-1, 1))
27 | vectorl.rotateInRadian(Math.PI / 4) should be(Vectorl(0, sqrtOf2))
28 | vectorl.rotateInRadian(Math.PI) should be(Vectorl(-1, -1))
29 | vectorl.rotateInRadian(-Math.PI / 4) should be(Vectorl(sqrtOf2, 0))
30 | }
31 |
32 | it should "compute angle between two vectors" in {
33 | vectorl.angleInDegreeBetween(Vectorl(1, 0)) should equal(45.0)
34 | vectorl.angleInDegreeBetween(Vectorl(0, 1)) should equal(45.0)
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/src/test/scala/com/truelaurel/math/geometry/grid/BitGridSpec.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.math.geometry.grid
2 |
3 | import org.scalatest.{FlatSpec, Matchers}
4 |
5 | class BitGridSpec extends FlatSpec with Matchers {
6 |
7 | "a BitGrid" should "initially be empty" in {
8 | val bg = BitGrid(3, 3)
9 | bg.complete shouldBe false
10 | bg.empty shouldBe true
11 | }
12 |
13 | it should "detect a full row" in {
14 | val bg = BitGrid(3, 3) + (0, 0) + (0, 1) + (0, 2)
15 | bg.empty shouldBe false
16 | bg.complete shouldBe true
17 | }
18 |
19 | it should "detect a full row with extra" in {
20 | val bg = BitGrid(3, 3) + (0, 0) + (0, 1) + (0, 2) + (1, 0)
21 | bg.empty shouldBe false
22 | bg.complete shouldBe true
23 | }
24 |
25 | it should "detect a diag1" in {
26 | val bg = BitGrid(3, 3) + (0, 0) + (1, 1) + (2, 2)
27 | bg.empty shouldBe false
28 | bg.complete shouldBe true
29 | }
30 |
31 | it should "detect a diag2" in {
32 | val bg = BitGrid(3, 3) + (0, 2) + (1, 1) + (2, 0)
33 | bg.empty shouldBe false
34 | bg.complete shouldBe true
35 | }
36 |
37 | it should "detect incomplete" in {
38 | val bg = BitGrid(3, 3) + (0, 1) + (1, 1) + (2, 0)
39 | bg.empty shouldBe false
40 | bg.complete shouldBe false
41 | }
42 |
43 | for (c <- 0 to 2) {
44 | it should s"detect a full col in pos $c" in {
45 | val empty = BitGrid(3, 3)
46 | val bg = empty.addCol(c)
47 | bg.empty shouldBe false
48 | bg.complete shouldBe true
49 | }
50 | }
51 |
52 | it should "detect a full col in a large grid" in {
53 | val empty = BitGrid(19, 19)
54 | val bg = empty.addCol(11)
55 | bg.empty shouldBe false
56 | bg.complete shouldBe true
57 | }
58 |
59 | it should "compute submatrix long value" in {
60 | val gd = GridData.full(19)
61 | for {
62 | r <- 0 to 16
63 | c <- 0 to 16
64 | gsm = gd.subMatrix(r, c, 3)
65 | } gsm shouldBe (1L << 9) - 1
66 | }
67 |
68 | it should "detect a full row - 1 in a large grid" in {
69 | val empty = BitGrid(19, 19)
70 | val bg = empty.addCol(11) - (5, 11)
71 | bg.empty shouldBe false
72 | bg.complete shouldBe false
73 | }
74 |
75 | it should "list free cells" in {
76 | val empty = BitGrid(3, 3)
77 | empty.free.size shouldBe 9
78 | }
79 |
80 | it should "list free cells (medium)" in {
81 | val size = 5
82 | val empty = BitGrid(size, size)
83 | val bg = empty.addCol(size / 2) - (size / 4, size / 2)
84 | bg.free.size shouldBe (size * (size - 1) + 1)
85 | }
86 |
87 | it should "list free cells (large)" in {
88 | val size = 19
89 | val empty = BitGrid(size, size)
90 | val bg = empty.addCol(size / 2) - (size / 4, size / 2)
91 | bg.free.size shouldBe (size * (size - 1) + 1)
92 | }
93 |
94 | it should "list used cells (large)" in {
95 | val size = 19
96 | val empty = BitGrid(size, size)
97 | val bg = empty.addCol(size / 2) - (size / 4, size / 2)
98 | bg.used.size shouldBe (size * size - (size * (size - 1) + 1))
99 | }
100 |
101 | it should "detect a partial row " in {
102 | val empty = BitGrid(19, 5)
103 | val bg = empty.addRow(1, 5, 10)
104 | bg.empty shouldBe false
105 | bg.complete shouldBe true
106 | }
107 |
108 | it should "detect a partial col" in {
109 | val empty = BitGrid(19, 6)
110 | val bg = empty.addCol(17, 11, 17)
111 | bg.empty shouldBe false
112 | bg.complete shouldBe true
113 | }
114 |
115 | it should "detect a partial diag1" in {
116 | val empty = BitGrid(19, 6)
117 | val bg = empty.addDiag1(3, 3, 6)
118 | bg.empty shouldBe false
119 | bg.complete shouldBe true
120 | }
121 |
122 | it should "detect a partial diag2" in {
123 | val empty = BitGrid(19, 6)
124 | val bg = empty.addDiag2(12, 18, 6)
125 | bg.empty shouldBe false
126 | bg.complete shouldBe true
127 | }
128 |
129 | }
130 |
--------------------------------------------------------------------------------
/src/test/scala/com/truelaurel/math/geometry/grid/GridDataSpec.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.math.geometry.grid
2 |
3 | import com.truelaurel.math.geometry.Pos
4 | import org.scalatest.{FlatSpec, Matchers}
5 |
6 | class GridDataSpec extends FlatSpec with Matchers {
7 |
8 | "a GridData" should "list free neighbours (vert/horiz)" in {
9 | // AA.
10 | // .X.
11 | // ...
12 | val grid = GridData(3) + (0, 0) + (1, 0)
13 | grid.freeNeighbours4(Pos(1, 1)) should contain theSameElementsAs Seq(
14 | Pos(0, 1),
15 | Pos(2, 1),
16 | Pos(1, 2)
17 | )
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/src/test/scala/com/truelaurel/math/geometry/hexagons/OffsetTest.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.math.geometry.hexagons
2 |
3 | import org.scalatest.{FlatSpec, Matchers}
4 |
5 | /**
6 | * Created by hwang on 20/04/2017.
7 | */
8 | class OffsetTest extends FlatSpec with Matchers {
9 |
10 | behavior of "OffsetTest"
11 |
12 | it should "compute angle" in {
13 | Offset(4, 6).angle(Offset(4, 6)) should be(0)
14 | Offset(4, 6).angle(Offset(5, 6)) should be(0)
15 | Offset(4, 6).angle(Offset(4, 5)) should be(1)
16 | Offset(4, 6).angle(Offset(3, 6)) should be(3)
17 | Offset(4, 6).angle(Offset(4, 7)) should be(5)
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/src/test/scala/com/truelaurel/physics/DiskTest.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.physics
2 |
3 | import com.truelaurel.math.geometry.Vectorl
4 | import org.scalatest.{FlatSpec, Matchers}
5 |
6 | /**
7 | * Created by hwang on 01/12/2016.
8 | */
9 | class DiskTest extends FlatSpec with Matchers {
10 | val disk: Disk = Disk(p = Vectorl(2, 1), v = Vectorl(2, 3), r = 1)
11 |
12 | val diskCollisionResolver = Collision
13 |
14 | behavior of "A disk"
15 |
16 | it can "move" in {
17 | diskCollisionResolver.move(disk, 1.0) should equal(
18 | Disk(Vectorl(4, 4), Vectorl(2, 3), 1)
19 | )
20 | }
21 |
22 | behavior of "A disk for collision detection"
23 |
24 | it should "return none when it moves away from another disk" in {
25 | diskCollisionResolver.collideTime(
26 | disk,
27 | Disk(Vectorl(-2, -5), Vectorl(-2, -3), 1)
28 | ) should equal(None)
29 | }
30 |
31 | it should "return none when it's relatively still than another disk" in {
32 | diskCollisionResolver.collideTime(
33 | disk,
34 | Disk(Vectorl(10, 10), Vectorl(2, 3), 2)
35 | ) should equal(None)
36 | }
37 |
38 | it should "return none when another disk is moving too fast" in {
39 | diskCollisionResolver.collideTime(
40 | disk,
41 | Disk(Vectorl(10, 10), Vectorl(-100, 0), 2)
42 | ) should equal(None)
43 | }
44 |
45 | it should "return collision time when collision is going to happen" in {
46 | diskCollisionResolver.collideTime(
47 | disk,
48 | Disk(Vectorl(5, 1), Vectorl(-2, 3), 1)
49 | ) should equal(Some(0.25))
50 | }
51 |
52 | behavior of "A disk for bounce off"
53 |
54 | it should "return disks going to their opposite way in case of face-to-face collision" in {
55 | val goingRight: Disk = Disk(Vectorl(3, 3), Vectorl(1, 0), 1)
56 | val goingLeft: Disk = Disk(Vectorl(5, 3), Vectorl(-1, 0), 1)
57 | val (d1, d2) = diskCollisionResolver.bounceOff(goingRight, goingLeft)
58 | d1 should equal(Disk(Vectorl(3, 3), Vectorl(-1, 0), 1))
59 | d2 should equal(Disk(Vectorl(5, 3), Vectorl(1, 0), 1))
60 | }
61 |
62 | it should "return disks going to opposite way with different speed in case of heavy-light face-to-face collision" in {
63 | val goingRight: Disk = Disk(Vectorl(3, 3), Vectorl(1, 0), 1, 2)
64 | val goingLeft: Disk = Disk(Vectorl(5, 3), Vectorl(-1, 0), 1, 1)
65 |
66 | val (d1, d2) = diskCollisionResolver.bounceOff(goingRight, goingLeft)
67 | d1 should equal(Disk(Vectorl(3, 3), Vectorl(-1 / 3.0, 0), 1, 2))
68 | d2 should equal(Disk(Vectorl(5, 3), Vectorl(5 / 3.0, 0), 1, 1))
69 | }
70 |
71 | }
72 |
--------------------------------------------------------------------------------
/src/test/scala/com/truelaurel/samplegames/gomoku/MctsGomokuTest.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.samplegames.gomoku
2 |
3 | import com.truelaurel.algorithm.mcts.MctsAi
4 | import com.truelaurel.math.geometry.Pos
5 | import org.scalatest.{FlatSpec, Matchers}
6 |
7 | class MctsGomokuTest extends FlatSpec with Matchers {
8 |
9 | behavior of "MctsAi for Gomoku"
10 |
11 | val rules = GomokuRules(3, 3)
12 | val ai = MctsAi(rules)(_.results.played > 50)
13 |
14 | it should "find the best move for false when it's a win" in {
15 | val root = GomokuBoard(3)
16 | val falseToWin = root.play(2, 0).play(0, 0).play(2, 2).play(1, 1)
17 | println(falseToWin.toText)
18 | ai.chooseMove(falseToWin) shouldBe Pos(2, 1)
19 | }
20 |
21 | it should "find the best move for true when it's a win" in {
22 | val root = GomokuBoard(3)
23 | // F T
24 | // FTF
25 | // X
26 | val trueToWin = root.play(0, 0).play(0, 2).play(1, 0).play(1, 1).play(2, 1)
27 | println(trueToWin.toText)
28 | ai.chooseMove(trueToWin) shouldBe Pos(2, 0)
29 | }
30 |
31 | it should "find the best move for false to avoid sure defeat" in {
32 | val root = GomokuBoard(3)
33 | // FT
34 | //
35 | // T F
36 | val trueToWin = root.play(1, 0).play(2, 0).play(2, 2).play(0, 2)
37 | println(trueToWin.toText)
38 | ai.chooseMove(trueToWin) shouldBe Pos(1, 1)
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/src/test/scala/com/truelaurel/time/ChronometerTest.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.time
2 |
3 | import java.util.concurrent.TimeUnit
4 |
5 | import org.scalatest.{FlatSpec, Matchers}
6 |
7 | import scala.concurrent.duration.Duration
8 |
9 | /**
10 | * Created by Wei on 14/06/2017.
11 | */
12 | class ChronometerTest extends FlatSpec with Matchers {
13 |
14 | behavior of "Chronometer"
15 |
16 | val duration = Duration(1, TimeUnit.SECONDS)
17 |
18 | it should "return false while counter still alive" in {
19 | val cs = new Chronometer(duration) {
20 |
21 | var predefinedClock = new PredefinedClock(Vector(0, duration.toNanos / 2))
22 |
23 | override def mark: Long = {
24 | predefinedClock.tick()
25 | }
26 | }
27 | cs.start()
28 | cs.willOutOfTime should equal(false)
29 | }
30 |
31 | it should "return true after counter ran out" in {
32 | val cs = new Chronometer(duration) {
33 |
34 | var predefinedClock = new PredefinedClock(Vector(0, duration.toNanos * 2))
35 |
36 | override def mark: Long = {
37 | predefinedClock.tick()
38 | }
39 | }
40 | cs.start()
41 | cs.willOutOfTime should equal(true)
42 | }
43 | }
44 |
45 | class PredefinedClock(values: Vector[Long]) {
46 | private var i = -1
47 |
48 | def tick(): Long = {
49 | i += 1
50 | require(i < values.size, "All predefined values are consumed")
51 | values(i)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/test/scala/com/truelaurel/time/CountStopperTest.scala:
--------------------------------------------------------------------------------
1 | package com.truelaurel.time
2 |
3 | import org.scalatest.{FlatSpec, Matchers}
4 |
5 | /**
6 | * Created by Wei on 14/06/2017.
7 | */
8 | class CountStopperTest extends FlatSpec with Matchers {
9 |
10 | behavior of "CountStopper"
11 |
12 | val stopcount: Int = 200
13 | val cs = new CountStopper(stopcount)
14 | cs.start()
15 |
16 | it should "return false while counter still alive" in {
17 | for (i <- 0 until stopcount)
18 | cs.willOutOfTime should equal(false)
19 | }
20 |
21 | it should "return true after counter ran out" in {
22 | cs.willOutOfTime should equal(true)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------