├── .gitignore ├── .scalafmt.conf ├── LICENSE ├── README.md ├── build.sbt ├── project ├── build.properties └── plugins.sbt └── src ├── main └── scala │ └── mcs │ ├── Algebra.scala │ ├── Interpreters.scala │ ├── Main.scala │ ├── Programs.scala │ ├── data │ └── Games.scala │ ├── samegame │ ├── Models.scala │ └── SameGame.scala │ └── util │ ├── ListUtils.scala │ └── SameGameMetrics.scala └── test └── scala └── mcs └── GameInstanceTests.scala /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | style = defaultWithAlign 2 | maxColumn = 160 3 | includeCurlyBraceInSelectChains = false 4 | danglingParentheses = true 5 | 6 | continuationIndent { 7 | callSite = 2 8 | defnSite = 2 9 | } 10 | 11 | newlines { 12 | penalizeSingleSelectMultiArgList = false 13 | alwaysBeforeElseAfterCurlyIf = true 14 | penalizeSingleSelectMultiArgList = false 15 | } 16 | 17 | project { 18 | git = true 19 | excludeFilters = [ 20 | target/ 21 | ] 22 | } 23 | 24 | binPack { 25 | parentConstructors = true 26 | } 27 | 28 | rewrite { 29 | rules = [RedundantBraces, RedundantParens, SortImports, PreferCurlyFors] 30 | } 31 | 32 | align { 33 | arrowEnumeratorGenerator = true 34 | tokens.add = [":=", "+=", "++="] 35 | openParenCallSite = false 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Leif Battermann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nested Monte Carlo tree search for SameGame 2 | 3 | Purely functional nested Monte Carlo tree search for solving SameGame. 4 | 5 | See also [Algebraic API Design - Types, Functions, Properties](https://typelevel.org/blog/2019/02/06/algebraic-api-design.html). 6 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := "mcs" 2 | version := "1.0" 3 | scalaVersion := "2.12.8" 4 | 5 | enablePlugins(JavaAppPackaging) 6 | 7 | libraryDependencies ++= Seq( 8 | "org.typelevel" %% "cats-effect" % "1.1.0", 9 | "org.scalanlp" %% "breeze" % "1.0-RC2", 10 | "org.scalatest" %% "scalatest" % "3.0.5" % "test", 11 | "org.scalacheck" %% "scalacheck" % "1.14.0" % "test" 12 | ) 13 | 14 | scalacOptions ++= Seq( 15 | "-deprecation", // Emit warning and location for usages of deprecated APIs. 16 | "-encoding", 17 | "utf-8", // Specify character encoding used by source files. 18 | "-explaintypes", // Explain type errors in more detail. 19 | "-feature", // Emit warning and location for usages of features that should be imported explicitly. 20 | "-language:existentials", // Existential types (besides wildcard types) can be written and inferred 21 | "-language:experimental.macros", // Allow macro definition (besides implementation and application) 22 | "-language:higherKinds", // Allow higher-kinded types 23 | "-language:implicitConversions", // Allow definition of implicit functions called views 24 | "-unchecked", // Enable additional warnings where generated code depends on assumptions. 25 | "-Xcheckinit", // Wrap field accessors to throw an exception on uninitialized access. 26 | "-Xfatal-warnings", // Fail the compilation if there are any warnings. 27 | "-Xfuture", // Turn on future language features. 28 | "-Xlint:adapted-args", // Warn if an argument list is modified to match the receiver. 29 | "-Xlint:by-name-right-associative", // By-name parameter of right associative operator. 30 | "-Xlint:constant", // Evaluation of a constant arithmetic expression results in an error. 31 | "-Xlint:delayedinit-select", // Selecting member of DelayedInit. 32 | "-Xlint:doc-detached", // A Scaladoc comment appears to be detached from its element. 33 | "-Xlint:inaccessible", // Warn about inaccessible types in method signatures. 34 | "-Xlint:infer-any", // Warn when a type argument is inferred to be `Any`. 35 | "-Xlint:missing-interpolator", // A string literal appears to be missing an interpolator id. 36 | "-Xlint:nullary-override", // Warn when non-nullary `def f()' overrides nullary `def f'. 37 | "-Xlint:nullary-unit", // Warn when nullary methods return Unit. 38 | "-Xlint:option-implicit", // Option.apply used implicit view. 39 | "-Xlint:package-object-classes", // Class or object defined in package object. 40 | "-Xlint:poly-implicit-overload", // Parameterized overloaded implicit methods are not visible as view bounds. 41 | "-Xlint:private-shadow", // A private field (or class parameter) shadows a superclass field. 42 | "-Xlint:stars-align", // Pattern sequence wildcard must align with sequence component. 43 | "-Xlint:type-parameter-shadow", // A local type parameter shadows a type already in scope. 44 | "-Xlint:unsound-match", // Pattern match may not be typesafe. 45 | "-Yno-adapted-args", // Do not adapt an argument list (either by inserting () or creating a tuple) to match the receiver. 46 | "-Ypartial-unification", // Enable partial unification in type constructor inference 47 | "-Ywarn-dead-code", // Warn when dead code is identified. 48 | "-Ywarn-extra-implicit", // Warn when more than one implicit parameter section is defined. 49 | "-Ywarn-inaccessible", // Warn about inaccessible types in method signatures. 50 | "-Ywarn-infer-any", // Warn when a type argument is inferred to be `Any`. 51 | "-Ywarn-nullary-override", // Warn when non-nullary `def f()' overrides nullary `def f'. 52 | "-Ywarn-nullary-unit", // Warn when nullary methods return Unit. 53 | "-Ywarn-numeric-widen", // Warn when numerics are widened. 54 | "-Ywarn-unused:implicits", // Warn if an implicit parameter is unused. 55 | "-Ywarn-unused:imports", // Warn if an import selector is not referenced. 56 | "-Ywarn-unused:locals", // Warn if a local definition is unused. 57 | "-Ywarn-unused:params", // Warn if a value parameter is unused. 58 | "-Ywarn-unused:patvars", // Warn if a variable bound in a pattern is unused. 59 | "-Ywarn-unused:privates", // Warn if a private member is unused. 60 | "-Ywarn-value-discard" // Warn when non-Unit expression results are unused. 61 | ) 62 | 63 | scalacOptions in (Compile, console) --= Seq("-Ywarn-unused:imports", "-Xfatal-warnings") 64 | 65 | scalacOptions in (Test, console) --= Seq("-Ywarn-unused:imports", "-Xfatal-warnings") 66 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.2.6 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.16") 2 | -------------------------------------------------------------------------------- /src/main/scala/mcs/Algebra.scala: -------------------------------------------------------------------------------- 1 | package mcs 2 | 3 | import cats.Show 4 | 5 | final case class Result[Move, Score]( 6 | moves: List[Move], 7 | score: Score 8 | ) 9 | 10 | final case class GameState[Move, Position, Score]( 11 | playedMoves: List[Move], 12 | score: Score, 13 | position: Position, 14 | ) 15 | 16 | trait Game[F[_], Move, Position, Score] { 17 | def applyMove(gameState: GameState[Move, Position, Score], move: Move): GameState[Move, Position, Score] 18 | def legalMoves(gameState: GameState[Move, Position, Score]): List[Move] 19 | def simulation(gameState: GameState[Move, Position, Score]): F[GameState[Move, Position, Score]] 20 | def isPrefixOf: List[Move] => List[Move] => Boolean 21 | def next(currentPath: List[Move], bestPath: List[Move]): Option[Move] 22 | } 23 | 24 | object Game { 25 | object laws { 26 | import cats.Monad 27 | import cats.implicits._ 28 | 29 | def simulationIsTerminal[F[_]: Monad, Move, Position, Score]( 30 | gameState: GameState[Move, Position, Score] 31 | )(implicit ev: Game[F, Move, Position, Score]): F[Boolean] = 32 | ev.simulation(gameState).map(ev.legalMoves).map(_.isEmpty) 33 | 34 | def legalMoveModifiesGameState[F[_]: Monad, Move, Position, Score](gameState: GameState[Move, Position, Score], 35 | move: Move)(implicit ev: Game[F, Move, Position, Score]): Boolean = { 36 | val legalMoves = ev.legalMoves(gameState) 37 | val nextGameState = ev.applyMove(gameState, move) 38 | !legalMoves.contains(move) || (nextGameState.position != gameState.position && nextGameState.playedMoves.length == gameState.playedMoves.length + 1) 39 | } 40 | } 41 | } 42 | 43 | trait Logger[F[_]] { 44 | def log[T: Show](t: T): F[Unit] 45 | } 46 | 47 | object Logger { 48 | def apply[F[_]]()(implicit ev: Logger[F]): Logger[F] = ev 49 | } 50 | -------------------------------------------------------------------------------- /src/main/scala/mcs/Interpreters.scala: -------------------------------------------------------------------------------- 1 | package mcs 2 | 3 | import cats.{Eq, Show} 4 | import cats.effect.IO 5 | import cats.implicits._ 6 | import mcs.samegame._ 7 | import mcs.util.ListUtils 8 | 9 | object Interpreters { 10 | 11 | type Move = samegame.Position 12 | type BoardPosition = samegame.Game 13 | 14 | implicit val game: Game[IO, Move, BoardPosition, Int] = new Game[IO, Move, BoardPosition, Int] { 15 | def applyMove(gameState: GameState[Move, BoardPosition, Int], move: Move): GameState[Move, BoardPosition, Int] = { 16 | val gs = samegame.SameGame.applyMove(move, gameState.position) 17 | GameState(move :: gameState.playedMoves, samegame.SameGame.score(gs), gs) 18 | } 19 | 20 | def legalMoves(gameState: GameState[Move, BoardPosition, Int]): List[Move] = 21 | samegame.SameGame.legalMoves(gameState.position) 22 | 23 | def simulation(gameState: GameState[Move, BoardPosition, Int]): IO[GameState[Move, BoardPosition, Int]] = { 24 | val tabuColor = samegame.SameGame.predominantColor(gameState.position) 25 | val moves = legalMoves(gameState) 26 | moves.partition(m => samegame.SameGame.color(gameState.position, m) == tabuColor) match { 27 | case (Nil, Nil) => IO.pure(gameState) 28 | case (tabuMoves, Nil) => 29 | IO(scala.util.Random.nextInt(tabuMoves.length)) 30 | .map(i => applyMove(gameState, tabuMoves(i))) 31 | .flatMap(gs => simulation(gs)) 32 | case (_, ms) => 33 | IO(scala.util.Random.nextInt(ms.length)) 34 | .map(i => applyMove(gameState, ms(i))) 35 | .flatMap(gs => simulation(gs)) 36 | } 37 | } 38 | 39 | def isPrefixOf: List[Move] => List[Move] => Boolean = currentPath => bestPath => ListUtils.isSuffixOf(currentPath, bestPath)(Eq.fromUniversalEquals) 40 | 41 | def next(currentPath: List[Move], bestPath: List[Move]): Option[Move] = 42 | if (isPrefixOf(currentPath)(bestPath) && currentPath.length < bestPath.length) { 43 | bestPath(bestPath.length - 1 - currentPath.length).some 44 | } 45 | else { 46 | None 47 | } 48 | } 49 | 50 | implicit val loggerIO: Logger[IO] = new Logger[IO] { 51 | def log[T: Show](t: T): IO[Unit] = IO(println(t.show)) 52 | } 53 | 54 | implicit val showCell: Show[CellState] = Show.show { 55 | case Empty => "-" 56 | case Filled(Green) => "0" 57 | case Filled(Blue) => "1" 58 | case Filled(Red) => "2" 59 | case Filled(Brown) => "3" 60 | case Filled(Gray) => "4" 61 | } 62 | 63 | implicit val showMove: Show[Move] = 64 | Show.show(p => show"(${p.col}, ${p.row})") 65 | 66 | implicit val showList: Show[List[Move]] = 67 | Show.show(_.map(_.show).mkString("[", ", ", "]")) 68 | 69 | implicit val showResult: Show[Result[Move, Int]] = 70 | Show.show(result => show">>> Improved sequence found\n>>> Score: ${result.score.show}, Moves: ${result.moves.reverse.show}") 71 | 72 | implicit val showBoard: Show[Board] = 73 | Show.show(_.columns.map(col => col.cells.map(_.show).reverse).transpose.map(_.mkString("[", ",", "]")).mkString("\n")) 74 | 75 | implicit val showGame: Show[BoardPosition] = Show.show { 76 | case InProgress(board, score) => show"$board\n\nScore: $score (game in progress)" 77 | case Finished(board, score) => show"$board\n\nScore: $score (game finished)" 78 | } 79 | 80 | implicit val showGameState: Show[GameState[Move, BoardPosition, Int]] = Show.show(t => show""" 81 | |${t.position} 82 | | 83 | |Moves: ${t.playedMoves.reverse} 84 | |""".stripMargin) 85 | 86 | val showGameStateAsQueryParams: Show[GameState[Move, BoardPosition, Int]] = 87 | Show.show(t => show"""Moves: ${t.playedMoves.reverse.map(p => s"move=${p.col}%2C${p.row}").mkString("&")} 88 | | 89 | |Score: ${t.score} 90 | |""".stripMargin) 91 | 92 | val showGameStateAsJsFunctionCalls: Show[GameState[Move, BoardPosition, Int]] = 93 | Show.show(t => show"""Moves: ${t.playedMoves.reverse.map(p => s"sg_remove(${p.col},${14 - p.row})").mkString(";")} 94 | | 95 | |Score: ${t.score} 96 | |""".stripMargin) 97 | 98 | implicit val positionEq: Eq[Move] = Eq.fromUniversalEquals 99 | } 100 | -------------------------------------------------------------------------------- /src/main/scala/mcs/Main.scala: -------------------------------------------------------------------------------- 1 | package mcs 2 | 3 | import java.util.concurrent.Executors 4 | 5 | import cats.effect.concurrent.Ref 6 | import cats.effect.{ContextShift, ExitCode, IO, IOApp} 7 | import cats.implicits._ 8 | import mcs.Interpreters._ 9 | import mcs.Programs.SearchState 10 | import mcs.samegame.SameGame 11 | 12 | import scala.util.Try 13 | 14 | object Main extends IOApp { 15 | private val threadPool = Executors.newFixedThreadPool(Runtime.getRuntime.availableProcessors()) 16 | 17 | override protected implicit def contextShift: ContextShift[IO] = IO.contextShift(scala.concurrent.ExecutionContext.fromExecutor(threadPool)) 18 | 19 | private val (position, best) = data.Games.jsGames10 20 | private val score = SameGame.score(position) 21 | private val gameState = GameState(playedMoves = List.empty[Move], score = score, position = position) 22 | 23 | private def startSearch(level: Int, best: Option[Result[samegame.Position, Int]]): IO[Unit] = 24 | for { 25 | cores <- IO(Runtime.getRuntime.availableProcessors()) 26 | _ <- IO(println(s"Available processors: $cores")) 27 | _ <- IO(println(s"Nesting level: $level")) 28 | ref <- Ref.of[IO, Option[Result[Move, Int]]](None) 29 | _ <- Programs.nestedMonteCarloTreeSearch[IO, IO.Par, Move, BoardPosition, Int](SearchState(gameState, best), ref, level) 30 | _ <- IO(threadPool.shutdown()) 31 | } yield () 32 | 33 | def run(args: List[String]): IO[ExitCode] = 34 | startSearch(args.headOption.flatMap(arg => Try(arg.toInt).toOption).getOrElse(1), args.drop(1).headOption.filter(_ == "best").flatMap(_ => best)) 35 | .as(ExitCode.Success) 36 | } 37 | -------------------------------------------------------------------------------- /src/main/scala/mcs/Programs.scala: -------------------------------------------------------------------------------- 1 | package mcs 2 | 3 | import cats.effect.concurrent.Ref 4 | import cats.implicits._ 5 | import cats.{Eq, Monad, Parallel, Show} 6 | 7 | object Programs { 8 | 9 | final case class SearchState[Move, Position, Score]( 10 | gameState: GameState[Move, Position, Score], 11 | bestSequence: Option[Result[Move, Score]], 12 | ) 13 | 14 | private def chooseNextMove[F[_]: Monad: Logger, Move, Position, Score]( 15 | bestTotal: Ref[F, Option[Result[Move, Score]]], 16 | currentState: GameState[Move, Position, Score], 17 | nextState: GameState[Move, Position, Score], 18 | currentBestSequence: Option[Result[Move, Score]], 19 | simResult: GameState[Move, Position, Score] 20 | )(implicit game: Game[F, Move, Position, Score], ord: Ordering[Score], showResult: Show[Result[Move, Score]]): F[SearchState[Move, Position, Score]] = { 21 | val nextSearchState = currentBestSequence match { 22 | case None => 23 | SearchState(nextState, Result(simResult.playedMoves, simResult.score).some) 24 | case Some(currentBest) => 25 | if (ord.gteq(simResult.score, currentBest.score)) { 26 | SearchState(nextState, Result(simResult.playedMoves, simResult.score).some) 27 | } 28 | else { 29 | // If none of the moves improve or equals best sequence, the move of the best sequence is played 30 | game 31 | .next(currentState.playedMoves, currentBest.moves) 32 | .fold(SearchState(nextState, Result(simResult.playedMoves, simResult.score).some))( 33 | m => SearchState(game.applyMove(currentState, m), currentBest.some) 34 | ) 35 | } 36 | } 37 | 38 | bestTotal.modify { 39 | case None => 40 | val betterSequence = Result(simResult.playedMoves, simResult.score) 41 | (betterSequence.some, betterSequence.some) 42 | case Some(best) => 43 | if (ord.gt(simResult.score, best.score)) { 44 | val betterSequence = Result(simResult.playedMoves, simResult.score) 45 | (betterSequence.some, betterSequence.some) 46 | } 47 | else { 48 | (best.some, None) 49 | } 50 | }.flatMap { 51 | case None => ().pure[F] 52 | case Some(better) => Logger[F].log(better) 53 | }.as(nextSearchState) 54 | } 55 | 56 | private def nestedSearch[F[_]: Monad: Logger, G[_], Move: Eq, Position, Score]( 57 | searchState: SearchState[Move, Position, Score], 58 | bestTotal: Ref[F, Option[Result[Move, Score]]], 59 | numLevels: Int, 60 | level: Int 61 | )(move: Move)(implicit game: Game[F, Move, Position, Score], 62 | par: Parallel[F, G], 63 | ord: Ordering[Score], 64 | showGameState: Show[GameState[Move, Position, Score]], 65 | showResult: Show[Result[Move, Score]]): F[(GameState[Move, Position, Score], GameState[Move, Position, Score])] = { 66 | val nextState = game.applyMove(searchState.gameState, move) 67 | val bestSequence = searchState.bestSequence.filter(x => game.isPrefixOf(nextState.playedMoves)(x.moves)) 68 | val simulationResult = 69 | search[F, G, Move, Position, Score](SearchState(nextState, bestSequence), bestTotal, numLevels, level - 1) 70 | .map(_.gameState) 71 | simulationResult.map((_, nextState)) 72 | } 73 | 74 | private def search[F[_]: Monad: Logger, G[_], Move: Eq, Position, Score]( 75 | searchState: SearchState[Move, Position, Score], 76 | bestTotal: Ref[F, Option[Result[Move, Score]]], 77 | numLevels: Int, 78 | level: Int 79 | )(implicit game: Game[F, Move, Position, Score], 80 | par: Parallel[F, G], 81 | ord: Ordering[Score], 82 | showGameState: Show[GameState[Move, Position, Score]], 83 | showResult: Show[Result[Move, Score]]): F[SearchState[Move, Position, Score]] = { 84 | val legalMoves = game.legalMoves(searchState.gameState) 85 | for { 86 | _ <- if (level == numLevels) Logger[F].log(searchState.gameState) else ().pure[F] 87 | result <- if (legalMoves.isEmpty) { 88 | searchState.pure[F] 89 | } 90 | else { 91 | val results = (numLevels, level) match { 92 | case (1, _) => 93 | legalMoves.parTraverse { move => 94 | val nextState = game.applyMove(searchState.gameState, move) 95 | game.simulation(nextState).map((_, nextState)) 96 | } 97 | case (_, 1) => 98 | legalMoves.traverse { move => 99 | val nextState = game.applyMove(searchState.gameState, move) 100 | game.simulation(nextState).map((_, nextState)) 101 | } 102 | case (_, 2) => 103 | legalMoves.parTraverse(nestedSearch(searchState, bestTotal, numLevels, level)(_)) 104 | case _ => 105 | legalMoves.traverse(nestedSearch(searchState, bestTotal, numLevels, level)(_)) 106 | } 107 | results 108 | .map(_.maxBy(_._1.score)) 109 | .flatMap { 110 | case (simulationResult, nextState) => 111 | chooseNextMove[F, Move, Position, Score](bestTotal, searchState.gameState, nextState, searchState.bestSequence, simulationResult) 112 | } 113 | .flatMap(search[F, G, Move, Position, Score](_, bestTotal, numLevels, level)) 114 | } 115 | } yield result 116 | } 117 | 118 | def nestedMonteCarloTreeSearch[F[_]: Monad: Logger, G[_], Move: Eq, Position, Score]( 119 | searchState: SearchState[Move, Position, Score], 120 | bestTotal: Ref[F, Option[Result[Move, Score]]], 121 | level: Int 122 | )(implicit game: Game[F, Move, Position, Score], 123 | par: Parallel[F, G], 124 | ord: Ordering[Score], 125 | showGameState: Show[GameState[Move, Position, Score]], 126 | showResult: Show[Result[Move, Score]]): F[SearchState[Move, Position, Score]] = 127 | search[F, G, Move, Position, Score](searchState, bestTotal, level, level) 128 | } 129 | -------------------------------------------------------------------------------- /src/main/scala/mcs/data/Games.scala: -------------------------------------------------------------------------------- 1 | package mcs.data 2 | 3 | import mcs.samegame._ 4 | import cats.implicits._ 5 | import mcs.{GameState, Interpreters, Result} 6 | 7 | object Games { 8 | private def jsGames01Raw(): (List[List[Int]], Option[Result[Position, Int]]) = 9 | ( 10 | List( 11 | List(1, 1, 3, 2, 0, 0, 1, 1, 2, 0, 1, 2, 0, 3, 3), 12 | List(0, 0, 3, 2, 4, 3, 3, 0, 4, 4, 2, 3, 2, 3, 1), 13 | List(1, 3, 0, 3, 1, 3, 3, 0, 3, 4, 1, 4, 3, 2, 1), 14 | List(0, 2, 2, 1, 2, 4, 2, 4, 4, 3, 3, 0, 4, 0, 4), 15 | List(1, 1, 3, 0, 0, 2, 0, 0, 2, 0, 1, 2, 3, 4, 1), 16 | List(1, 4, 2, 4, 1, 3, 4, 3, 3, 3, 2, 3, 0, 4, 0), 17 | List(2, 4, 1, 0, 3, 0, 3, 1, 1, 4, 0, 0, 3, 1, 4), 18 | List(2, 4, 4, 1, 4, 0, 1, 2, 1, 2, 1, 2, 0, 3, 0), 19 | List(1, 4, 3, 2, 3, 2, 3, 1, 1, 2, 2, 4, 0, 1, 4), 20 | List(0, 0, 1, 4, 3, 1, 0, 0, 3, 2, 1, 4, 3, 2, 4), 21 | List(0, 4, 3, 1, 4, 2, 4, 4, 4, 0, 0, 4, 4, 0, 1), 22 | List(1, 2, 0, 3, 1, 3, 1, 1, 1, 2, 3, 3, 4, 0, 1), 23 | List(4, 1, 2, 3, 4, 4, 0, 3, 0, 3, 4, 0, 1, 4, 0), 24 | List(3, 3, 1, 0, 0, 0, 0, 3, 3, 4, 0, 2, 1, 0, 2), 25 | List(2, 4, 3, 1, 4, 1, 3, 1, 1, 0, 1, 3, 1, 4, 3) 26 | ), 27 | None 28 | ) 29 | 30 | private def jsGames10Raw(): (List[List[Int]], Option[Result[Position, Int]]) = 31 | ( 32 | List( 33 | List(0, 2, 3, 1, 2, 1, 3, 1, 0, 2, 2, 3, 1, 4, 0), 34 | List(3, 0, 0, 3, 3, 2, 4, 2, 0, 4, 0, 4, 2, 0, 1), 35 | List(1, 2, 2, 2, 4, 2, 3, 0, 0, 2, 0, 3, 4, 1, 3), 36 | List(1, 1, 2, 4, 2, 2, 3, 4, 2, 2, 3, 0, 3, 1, 3), 37 | List(0, 0, 1, 3, 1, 4, 1, 1, 2, 1, 2, 1, 0, 0, 4), 38 | List(1, 4, 4, 0, 0, 3, 1, 2, 3, 4, 0, 3, 2, 0, 3), 39 | List(4, 1, 3, 4, 1, 2, 0, 2, 4, 3, 2, 3, 0, 0, 4), 40 | List(1, 4, 4, 4, 2, 0, 4, 4, 3, 2, 3, 2, 2, 1, 3), 41 | List(4, 0, 1, 1, 3, 2, 4, 0, 2, 1, 3, 3, 3, 2, 2), 42 | List(1, 3, 2, 1, 2, 1, 2, 2, 3, 4, 3, 1, 4, 0, 4), 43 | List(1, 2, 2, 4, 4, 0, 1, 4, 0, 0, 0, 1, 3, 3, 4), 44 | List(1, 3, 1, 1, 0, 1, 0, 2, 3, 1, 1, 0, 1, 0, 0), 45 | List(0, 0, 1, 2, 2, 0, 0, 4, 4, 4, 1, 3, 2, 0, 3), 46 | List(4, 2, 3, 4, 4, 1, 1, 0, 3, 4, 1, 4, 2, 2, 2), 47 | List(2, 4, 1, 0, 3, 3, 3, 3, 4, 1, 1, 2, 3, 1, 1) 48 | ), 49 | Result( 50 | List( 51 | (0, 8), 52 | (2, 6), 53 | (13, 5), 54 | (4, 6), 55 | (12, 0), 56 | (13, 3), 57 | (9, 0), 58 | (11, 10), 59 | (12, 2), 60 | (11, 7), 61 | (9, 13), 62 | (10, 11), 63 | (4, 0), 64 | (10, 4), 65 | (4, 8), 66 | (5, 9), 67 | (14, 4), 68 | (4, 8), 69 | (13, 2), 70 | (7, 10), 71 | (2, 11), 72 | (14, 4), 73 | (3, 3), 74 | (5, 0), 75 | (6, 3), 76 | (10, 2), 77 | (5, 2), 78 | (1, 3), 79 | (11, 1), 80 | (1, 1), 81 | (11, 2), 82 | (0, 8), 83 | (2, 3), 84 | (7, 3), 85 | (5, 2), 86 | (5, 0), 87 | (6, 0), 88 | (6, 1), 89 | (14, 5), 90 | (10, 3), 91 | (7, 5), 92 | (1, 5), 93 | (1, 4), 94 | (14, 1), 95 | (5, 0), 96 | (4, 1), 97 | (9, 1), 98 | (11, 0), 99 | (10, 0), 100 | (7, 1), 101 | (1, 0), 102 | (0, 0), 103 | (0, 1), 104 | (0, 0), 105 | (0, 0), 106 | (3, 0), 107 | (0, 0), 108 | (0, 0), 109 | (0, 0), 110 | (0, 0) 111 | ).map(p => Position(p._1, p._2)).reverse, 112 | 3215 113 | ).some 114 | ) 115 | 116 | val jsGames01: (Game, Option[Result[Position, Int]]) = (SameGame.apply(jsGames01Raw()._1), jsGames01Raw()._2) 117 | val jsGames10: (Game, Option[Result[Position, Int]]) = (SameGame.apply(jsGames10Raw()._1), jsGames10Raw()._2) 118 | 119 | val foo: GameState[Position, Game, Int] = { 120 | val (position, b) = Games.jsGames10 121 | val score = SameGame.score(position) 122 | val gameState = mcs.GameState(playedMoves = List.empty[Position], score = score, position = position) 123 | 124 | val result = b.map { x => 125 | x.moves.drop(20).foldRight(gameState) { 126 | case (m, state) => 127 | Interpreters.game.applyMove(state, m) 128 | } 129 | } 130 | result.get 131 | } 132 | 133 | val game1: (Game, Option[Result[Position, Int]]) = (SameGame.apply(board15x15), None) 134 | 135 | def board(size: Int): (Game, Option[Result[Position, Int]]) = 136 | (SameGame.apply(board15x15.take(size).map(_.take(size))), None) 137 | 138 | def board15x15: List[List[Int]] = 139 | List( 140 | List(3, 0, 4, 2, 1, 2, 2, 0, 0, 3, 4, 3, 2, 3, 1), 141 | List(1, 4, 4, 2, 3, 2, 0, 0, 0, 3, 3, 1, 0, 1, 1), 142 | List(0, 4, 4, 3, 1, 2, 0, 2, 2, 1, 0, 0, 2, 0, 1), 143 | List(1, 3, 2, 1, 4, 1, 3, 4, 4, 1, 1, 3, 3, 0, 2), 144 | List(1, 0, 2, 3, 4, 1, 3, 2, 0, 1, 1, 4, 1, 4, 3), 145 | List(0, 1, 1, 1, 0, 4, 0, 0, 0, 0, 4, 0, 0, 4, 2), 146 | List(1, 3, 3, 2, 3, 0, 2, 0, 1, 4, 0, 0, 3, 2, 0), 147 | List(1, 2, 4, 0, 2, 2, 3, 2, 4, 2, 4, 4, 4, 1, 1), 148 | List(1, 1, 3, 3, 4, 3, 3, 0, 4, 1, 2, 3, 4, 1, 4), 149 | List(0, 2, 4, 1, 4, 3, 4, 3, 2, 2, 1, 1, 4, 4, 4), 150 | List(1, 1, 1, 0, 4, 4, 1, 4, 2, 0, 1, 3, 2, 4, 4), 151 | List(4, 0, 3, 3, 2, 2, 4, 4, 3, 4, 4, 4, 4, 2, 2), 152 | List(3, 4, 2, 3, 2, 4, 0, 0, 4, 4, 1, 4, 3, 3, 4), 153 | List(4, 2, 4, 3, 2, 3, 2, 0, 4, 0, 0, 4, 0, 0, 0), 154 | List(0, 0, 1, 3, 1, 0, 3, 2, 2, 1, 4, 4, 4, 2, 1) 155 | ) 156 | 157 | } 158 | -------------------------------------------------------------------------------- /src/main/scala/mcs/samegame/Models.scala: -------------------------------------------------------------------------------- 1 | package mcs.samegame 2 | 3 | final case class Position(col: Int, row: Int) 4 | 5 | sealed trait Color 6 | case object Green extends Color 7 | case object Blue extends Color 8 | case object Red extends Color 9 | case object Brown extends Color 10 | case object Gray extends Color 11 | 12 | sealed trait CellState 13 | final case class Filled(color: Color) extends CellState 14 | case object Empty extends CellState 15 | 16 | final case class Cell(position: Position, state: CellState) 17 | final case class Group(color: Color, positions: Set[Position]) 18 | final case class Column(cells: List[CellState]) extends AnyVal 19 | final case class Board private (columns: List[Column]) extends AnyVal 20 | 21 | sealed trait Game 22 | final case class InProgress(board: Board, score: Int) extends Game 23 | final case class Finished(board: Board, score: Int) extends Game 24 | 25 | object Color { 26 | def apply(n: Int): Color = 27 | n % 5 match { 28 | case 0 => Green 29 | case 1 => Blue 30 | case 2 => Red 31 | case 3 => Brown 32 | case 4 => Gray 33 | } 34 | } 35 | 36 | object Position { 37 | def left(pos: Position): Position = 38 | Position(pos.col - 1, pos.row) 39 | 40 | def right(pos: Position): Position = 41 | Position(pos.col + 1, pos.row) 42 | 43 | def up(pos: Position): Position = 44 | Position(pos.col, pos.row + 1) 45 | 46 | def down(pos: Position): Position = 47 | Position(pos.col, pos.row - 1) 48 | } 49 | 50 | object Column { 51 | implicit class CellMapper(column: Column) { 52 | def map(f: (CellState, Int) => CellState): Column = 53 | Column(column.cells.zipWithIndex.map { case (cs, i) => f(cs, i) }) 54 | 55 | def shiftDown: Column = { 56 | val nonEmptyCells = column.cells 57 | .filter(!CellState.isEmpty(_)) 58 | 59 | val diff = column.cells.length - nonEmptyCells.length 60 | 61 | Column(nonEmptyCells ++ List.fill(diff)(Empty)) 62 | } 63 | } 64 | 65 | def empty(height: Int): Column = Column((1 to height).map(_ => Empty).toList) 66 | } 67 | 68 | object CellState { 69 | def isEmpty(cellState: CellState): Boolean = 70 | cellState match { 71 | case Empty => true 72 | case _ => false 73 | } 74 | } 75 | 76 | object Board { 77 | implicit class ColumnMapper(board: Board) { 78 | def map(f: (Column, Int) => Column): Board = 79 | Board(board.columns.zipWithIndex.map { case (c, i) => f(c, i) }) 80 | 81 | def shiftLeft: Board = { 82 | val nonEmptyColumns = board.columns 83 | .filter(column => !CellState.isEmpty(column.cells.head)) 84 | 85 | val diff = board.columns.length - nonEmptyColumns.length 86 | val height = board.columns.head.cells.length 87 | Board(nonEmptyColumns ++ List.fill(diff)(Column.empty(height))) 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/scala/mcs/samegame/SameGame.scala: -------------------------------------------------------------------------------- 1 | package mcs.samegame 2 | 3 | import Column.CellMapper 4 | import Board.ColumnMapper 5 | import cats.implicits._ 6 | 7 | object SameGame { 8 | private val bonus = 1000 9 | 10 | private def sqr(x: Int): Int = x * x 11 | 12 | private def calcScore(group: Group) = sqr(group.positions.size - 2) 13 | 14 | private def penalty(numberOfFilledCells: Int) = -sqr(numberOfFilledCells - 2) 15 | 16 | private def getCellState(board: Board, position: Position): CellState = { 17 | val width = board.columns.length 18 | val height = board.columns.head.cells.length 19 | if (position.col >= 0 && position.col < width && position.row >= 0 && position.row < height) { 20 | board.columns(position.col).cells(position.row) 21 | } 22 | else { 23 | Empty 24 | } 25 | } 26 | 27 | private def findAdjacentWithSameColor(board: Board, position: Position): Set[Position] = 28 | getCellState(board, position) match { 29 | case Filled(color) => 30 | Set( 31 | Position.up(position), 32 | Position.right(position), 33 | Position.down(position), 34 | Position.left(position) 35 | ).map(p => (getCellState(board, p), p)) 36 | .filter { 37 | case (Filled(c), _) => c == color 38 | case _ => false 39 | } 40 | .map(_._2) 41 | 42 | case Empty => Set() 43 | } 44 | 45 | private def hasValidMoves(board: Board): Boolean = 46 | board.columns.zipWithIndex.exists { 47 | case (column, colIndex) => 48 | column.cells.zipWithIndex.exists { 49 | case (_, rowIndex) => 50 | findAdjacentWithSameColor(board, Position(colIndex, rowIndex)).nonEmpty 51 | } 52 | } 53 | 54 | private def filledCells(board: Board): Int = 55 | board.columns 56 | .foldLeft(0)( 57 | (total, column) => 58 | column.cells.foldLeft(total)( 59 | (count, cell) => 60 | cell match { 61 | case Filled(_) => count + 1 62 | case Empty => count 63 | } 64 | ) 65 | ) 66 | 67 | private def findGroup(board: Board, position: Position): Option[Group] = { 68 | def find(toSearch: Set[Position], group: Set[Position]): Set[Position] = 69 | if (toSearch.isEmpty) { 70 | group 71 | } 72 | else { 73 | val head = toSearch.head 74 | val cellsWithSameColor = findAdjacentWithSameColor(board, head) 75 | val cellsFoundSoFar = group + head 76 | val stillToSearch = (cellsWithSameColor ++ toSearch.tail) -- cellsFoundSoFar 77 | find(stillToSearch, cellsFoundSoFar) 78 | } 79 | 80 | getCellState(board, position) match { 81 | case Filled(color) => 82 | val positions = find(Set(position), Set.empty) 83 | if (positions.size > 1) { 84 | Some(Group(color, positions)) 85 | } 86 | else { 87 | None 88 | } 89 | case _ => None 90 | } 91 | } 92 | 93 | private def removeGroup(board: Board, group: Group): Board = 94 | board.map { 95 | case (column, colIndex) => 96 | column.map { 97 | case (cell, rowIndex) => 98 | if (group.positions.contains(Position(colIndex, rowIndex))) { 99 | Empty 100 | } 101 | else { 102 | cell 103 | } 104 | }.shiftDown 105 | }.shiftLeft 106 | 107 | private def play(board: Board, position: Position): Option[(Board, Int)] = 108 | findGroup(board, position) 109 | .map(g => (removeGroup(board, g), calcScore(g))) 110 | 111 | private def board(game: Game): Board = 112 | game match { 113 | case InProgress(board, _) => board 114 | case Finished(board, _) => board 115 | } 116 | 117 | private def cellColor(cellState: CellState): Map[Color, Int] = 118 | cellState match { 119 | case Filled(c) => Map(c -> 1) 120 | case Empty => Map.empty 121 | } 122 | 123 | def colors(game: Game): List[Color] = 124 | board(game).columns 125 | .map(_.cells.foldMap(cellColor)) 126 | .combineAll 127 | .keys 128 | .toList 129 | 130 | def predominantColor(game: Game): Option[Color] = 131 | board(game).columns 132 | .map(_.cells.foldMap(cellColor)) 133 | .combineAll 134 | .toList match { 135 | case Nil => None 136 | case list => list.maxBy(_._2)._1.some 137 | } 138 | 139 | def color(game: Game, position: Position): Option[Color] = 140 | getCellState(board(game), position) match { 141 | case Filled(c) => c.some 142 | case Empty => None 143 | } 144 | 145 | def evaluateGameState(board: Board, score: Int): Game = { 146 | def isEmpty(board: Board): Boolean = filledCells(board) == 0 147 | 148 | if (hasValidMoves(board)) { 149 | InProgress(board, score) 150 | } 151 | else if (isEmpty(board)) { 152 | Finished(board, score + bonus) 153 | } 154 | else { 155 | Finished(board, score + penalty(filledCells(board))) 156 | } 157 | } 158 | 159 | def applyMove(position: Position, game: Game): Game = 160 | game match { 161 | case InProgress(board, score) => 162 | play(board, position).map { case (b, s) => evaluateGameState(b, s + score) }.getOrElse(game) 163 | case Finished(_, _) => game 164 | } 165 | 166 | def legalMoves(game: Game): List[Position] = 167 | game match { 168 | case InProgress(board, _) => 169 | board.columns.zipWithIndex.flatMap { case (col, colIndex) => col.cells.zipWithIndex.map { case (_, rowIndex) => Position(colIndex, rowIndex) } } 170 | .flatMap(pos => findGroup(board, pos).toList) 171 | .distinct 172 | .flatMap(g => g.positions.headOption.toList) 173 | case Finished(_, _) => Nil 174 | } 175 | 176 | def score(game: Game): Int = 177 | game match { 178 | case InProgress(_, score) => score 179 | case Finished(_, score) => score 180 | } 181 | 182 | def apply(board: List[List[Int]]): Game = 183 | SameGame.evaluateGameState(Board(board.map(c => Column(c.map(s => Filled(Color(s)))))), 0) 184 | } 185 | -------------------------------------------------------------------------------- /src/main/scala/mcs/util/ListUtils.scala: -------------------------------------------------------------------------------- 1 | package mcs.util 2 | 3 | import cats.Eq 4 | import cats.implicits._ 5 | 6 | object ListUtils { 7 | def isSuffixOf[A: Eq](xs: List[A], ys: List[A]): Boolean = 8 | if (xs.length > ys.length) { 9 | false 10 | } 11 | else { 12 | ys.drop(ys.length - xs.length) === xs 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/scala/mcs/util/SameGameMetrics.scala: -------------------------------------------------------------------------------- 1 | package mcs.util 2 | 3 | import mcs.samegame._ 4 | 5 | import scala.annotation.tailrec 6 | import cats.implicits._ 7 | import breeze.stats._ 8 | 9 | object SameGameMetrics { 10 | // some impure code to try stuff 11 | 12 | type Result = mcs.Result[Position, Int] 13 | type GameState = mcs.GameState[Position, Game, Int] 14 | 15 | @tailrec 16 | private def random(gameState: GameState, moveStats: Map[Int, List[Double]]): (GameState, Map[Int, List[Double]]) = { 17 | val moves = SameGame.legalMoves(gameState.position) 18 | moves match { 19 | case Nil => (gameState, moveStats) 20 | case _ => 21 | val i = scala.util.Random.nextInt(moves.length) 22 | val move = moves(i) 23 | val gs = SameGame.applyMove(move, gameState.position) 24 | val updatedStats = moveStats |+| Map(gameState.playedMoves.length -> List(moves.length.toDouble)) 25 | random(mcs.GameState(move :: gameState.playedMoves, SameGame.score(gs), gs), updatedStats) 26 | } 27 | } 28 | 29 | private val (position, _) = mcs.data.Games.jsGames10 30 | private val score = SameGame.score(position) 31 | private val gameState = mcs.GameState(playedMoves = List.empty[Position], score = score, position = position) 32 | 33 | def runRndSimulationsAndPrintMetrics(n: Int): Unit = { 34 | val init = Map.empty[Int, List[Double]] 35 | val stats = (0 until n).foldLeft(init) { 36 | case (acc, _) => 37 | random(gameState, acc)._2 38 | } 39 | 40 | val withStats = stats.mapValues(xs => meanAndVariance(xs)) 41 | 42 | withStats.toList.sortBy(_._1).foreach { 43 | case (num, meanAndVar) => 44 | println(s"""move: $num 45 | | - avg number of child nodes: ${meanAndVar.mean.toInt} (variance: ${f"${meanAndVar.variance}%2.2f"} count: ${meanAndVar.count}) 46 | """.stripMargin) 47 | } 48 | 49 | val complexity = withStats.values.map(_.mean).foldLeft(1.0) { 50 | case (acc, x) => 51 | acc * x 52 | } 53 | 54 | println(s"complexity: ~10^${math.log10(complexity).toInt}") 55 | } 56 | 57 | /* 58 | scala> mcs.util.SameGameMetrics.runRndSimulationsAndPrintMetrics(20) 59 | move: 0 60 | - avg number of child nodes: 45 (variance: 0.00 count: 20) 61 | 62 | move: 1 63 | - avg number of child nodes: 44 (variance: 2.56 count: 20) 64 | 65 | move: 2 66 | - avg number of child nodes: 43 (variance: 3.27 count: 20) 67 | 68 | move: 3 69 | - avg number of child nodes: 42 (variance: 2.27 count: 20) 70 | 71 | move: 4 72 | - avg number of child nodes: 42 (variance: 2.37 count: 20) 73 | 74 | move: 5 75 | - avg number of child nodes: 41 (variance: 2.45 count: 20) 76 | 77 | move: 6 78 | - avg number of child nodes: 40 (variance: 3.04 count: 20) 79 | 80 | move: 7 81 | - avg number of child nodes: 40 (variance: 6.05 count: 20) 82 | 83 | move: 8 84 | - avg number of child nodes: 39 (variance: 5.82 count: 20) 85 | 86 | move: 9 87 | - avg number of child nodes: 38 (variance: 4.47 count: 20) 88 | 89 | move: 10 90 | - avg number of child nodes: 37 (variance: 3.84 count: 20) 91 | 92 | move: 11 93 | - avg number of child nodes: 36 (variance: 4.68 count: 20) 94 | 95 | move: 12 96 | - avg number of child nodes: 35 (variance: 3.08 count: 20) 97 | 98 | move: 13 99 | - avg number of child nodes: 34 (variance: 3.71 count: 20) 100 | 101 | move: 14 102 | - avg number of child nodes: 34 (variance: 3.01 count: 20) 103 | 104 | move: 15 105 | - avg number of child nodes: 32 (variance: 4.26 count: 20) 106 | 107 | move: 16 108 | - avg number of child nodes: 32 (variance: 3.61 count: 20) 109 | 110 | move: 17 111 | - avg number of child nodes: 31 (variance: 3.99 count: 20) 112 | 113 | move: 18 114 | - avg number of child nodes: 31 (variance: 4.91 count: 20) 115 | 116 | move: 19 117 | - avg number of child nodes: 29 (variance: 4.98 count: 20) 118 | 119 | move: 20 120 | - avg number of child nodes: 28 (variance: 6.77 count: 20) 121 | 122 | move: 21 123 | - avg number of child nodes: 28 (variance: 7.00 count: 20) 124 | 125 | move: 22 126 | - avg number of child nodes: 27 (variance: 5.80 count: 20) 127 | 128 | move: 23 129 | - avg number of child nodes: 26 (variance: 6.98 count: 20) 130 | 131 | move: 24 132 | - avg number of child nodes: 25 (variance: 5.31 count: 20) 133 | 134 | move: 25 135 | - avg number of child nodes: 25 (variance: 6.34 count: 20) 136 | 137 | move: 26 138 | - avg number of child nodes: 24 (variance: 4.80 count: 20) 139 | 140 | move: 27 141 | - avg number of child nodes: 23 (variance: 6.01 count: 20) 142 | 143 | move: 28 144 | - avg number of child nodes: 22 (variance: 6.16 count: 20) 145 | 146 | move: 29 147 | - avg number of child nodes: 22 (variance: 7.29 count: 20) 148 | 149 | move: 30 150 | - avg number of child nodes: 21 (variance: 8.45 count: 20) 151 | 152 | move: 31 153 | - avg number of child nodes: 21 (variance: 8.45 count: 20) 154 | 155 | move: 32 156 | - avg number of child nodes: 20 (variance: 8.77 count: 20) 157 | 158 | move: 33 159 | - avg number of child nodes: 20 (variance: 6.43 count: 20) 160 | 161 | move: 34 162 | - avg number of child nodes: 19 (variance: 6.26 count: 20) 163 | 164 | move: 35 165 | - avg number of child nodes: 18 (variance: 5.78 count: 20) 166 | 167 | move: 36 168 | - avg number of child nodes: 17 (variance: 4.53 count: 20) 169 | 170 | move: 37 171 | - avg number of child nodes: 17 (variance: 6.16 count: 20) 172 | 173 | move: 38 174 | - avg number of child nodes: 16 (variance: 4.79 count: 20) 175 | 176 | move: 39 177 | - avg number of child nodes: 15 (variance: 4.26 count: 20) 178 | 179 | move: 40 180 | - avg number of child nodes: 15 (variance: 5.21 count: 20) 181 | 182 | move: 41 183 | - avg number of child nodes: 14 (variance: 6.37 count: 20) 184 | 185 | move: 42 186 | - avg number of child nodes: 13 (variance: 7.31 count: 20) 187 | 188 | move: 43 189 | - avg number of child nodes: 12 (variance: 8.24 count: 20) 190 | 191 | move: 44 192 | - avg number of child nodes: 12 (variance: 6.72 count: 20) 193 | 194 | move: 45 195 | - avg number of child nodes: 11 (variance: 7.31 count: 20) 196 | 197 | move: 46 198 | - avg number of child nodes: 10 (variance: 5.75 count: 20) 199 | 200 | move: 47 201 | - avg number of child nodes: 10 (variance: 9.10 count: 20) 202 | 203 | move: 48 204 | - avg number of child nodes: 10 (variance: 9.73 count: 20) 205 | 206 | move: 49 207 | - avg number of child nodes: 9 (variance: 7.94 count: 20) 208 | 209 | move: 50 210 | - avg number of child nodes: 9 (variance: 8.34 count: 20) 211 | 212 | move: 51 213 | - avg number of child nodes: 8 (variance: 6.48 count: 20) 214 | 215 | move: 52 216 | - avg number of child nodes: 7 (variance: 7.50 count: 20) 217 | 218 | move: 53 219 | - avg number of child nodes: 7 (variance: 6.64 count: 20) 220 | 221 | move: 54 222 | - avg number of child nodes: 6 (variance: 5.32 count: 20) 223 | 224 | move: 55 225 | - avg number of child nodes: 5 (variance: 5.47 count: 20) 226 | 227 | move: 56 228 | - avg number of child nodes: 5 (variance: 6.16 count: 20) 229 | 230 | move: 57 231 | - avg number of child nodes: 4 (variance: 6.15 count: 20) 232 | 233 | move: 58 234 | - avg number of child nodes: 4 (variance: 5.79 count: 18) 235 | 236 | move: 59 237 | - avg number of child nodes: 4 (variance: 5.09 count: 18) 238 | 239 | move: 60 240 | - avg number of child nodes: 3 (variance: 4.13 count: 17) 241 | 242 | move: 61 243 | - avg number of child nodes: 3 (variance: 2.97 count: 13) 244 | 245 | move: 62 246 | - avg number of child nodes: 3 (variance: 2.09 count: 12) 247 | 248 | move: 63 249 | - avg number of child nodes: 2 (variance: 1.76 count: 11) 250 | 251 | move: 64 252 | - avg number of child nodes: 2 (variance: 1.29 count: 11) 253 | 254 | move: 65 255 | - avg number of child nodes: 2 (variance: 1.36 count: 8) 256 | 257 | move: 66 258 | - avg number of child nodes: 2 (variance: 1.57 count: 7) 259 | 260 | move: 67 261 | - avg number of child nodes: 2 (variance: 0.97 count: 6) 262 | 263 | move: 68 264 | - avg number of child nodes: 2 (variance: 0.25 count: 4) 265 | 266 | move: 69 267 | - avg number of child nodes: 2 (variance: 0.67 count: 4) 268 | 269 | move: 70 270 | - avg number of child nodes: 1 (variance: 0.92 count: 4) 271 | 272 | move: 71 273 | - avg number of child nodes: 2 (variance: 2.00 count: 2) 274 | 275 | move: 72 276 | - avg number of child nodes: 2 (variance: 0.00 count: 1) 277 | 278 | move: 73 279 | - avg number of child nodes: 1 (variance: 0.00 count: 1) 280 | 281 | complexity: ~10^82 282 | */ 283 | } 284 | -------------------------------------------------------------------------------- /src/test/scala/mcs/GameInstanceTests.scala: -------------------------------------------------------------------------------- 1 | package mcs 2 | 3 | import cats.effect.IO 4 | import mcs.Interpreters._ 5 | import mcs.samegame._ 6 | import org.scalacheck.Gen 7 | import org.scalacheck.Gen._ 8 | import org.scalatest._ 9 | import org.scalatest.prop.PropertyChecks 10 | 11 | class GameInstanceTests extends PropSpec with PropertyChecks with Matchers { 12 | def colEmpty(size: Int): List[CellState] = List.fill(size)(Empty) 13 | 14 | def colNonEmpty(size: Int): Gen[List[CellState]] = 15 | for { 16 | numFilled <- choose(1, size) 17 | filled <- listOfN(numFilled, choose(0, 5).map(c => Filled(Color(c)))) 18 | } yield filled ++ colEmpty(size - filled.length) 19 | 20 | def gameState(min: Int, max: Int): Gen[GameState[Move, BoardPosition, Int]] = 21 | for { 22 | size <- choose(min, max) 23 | numFilled <- choose(min, size) 24 | nonEmpty <- listOfN(numFilled, colNonEmpty(size)).map(_.map(Column(_))) 25 | empty <- listOfN(size - numFilled, const(colEmpty(size))).map(_.map(Column(_))) 26 | score <- choose(0, 1452) 27 | } yield 28 | GameState( 29 | playedMoves = List.empty[Position], 30 | position = SameGame.evaluateGameState(Board(nonEmpty ++ empty), score), 31 | score = score 32 | ) 33 | 34 | def move(boardSize: Int): Gen[Move] = 35 | for { 36 | col <- choose(0, boardSize) 37 | row <- choose(0, boardSize) 38 | } yield Position(col, row) 39 | 40 | property("simulation is terminal") { 41 | forAll(gameState(4, 8)) { gs => 42 | Game.laws.simulationIsTerminal[IO, Move, BoardPosition, Int](gs).unsafeRunSync() 43 | } 44 | } 45 | 46 | property("legal move modifies game state") { 47 | forAll(gameState(4, 8), move(8)) { 48 | case (gs, m) => 49 | Game.laws.legalMoveModifiesGameState[IO, Move, BoardPosition, Int](gs, m) 50 | } 51 | } 52 | } 53 | --------------------------------------------------------------------------------