├── .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 | ![alt tag](./asset/screenshot.png) 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 |
Bot
Bot
Input
Input
Output
Output
State
State
Action
Action
Data
Data
Logic
Logic
Simulation
Simulation
-------------------------------------------------------------------------------- /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 |
Action
Action
Impact
Impact
State
State
React
React
-------------------------------------------------------------------------------- /asset/referee.svg: -------------------------------------------------------------------------------- 1 | 2 |
Referee
Referee
Bot
Bot
Bot
Bot
-------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------