A => .bsp/sbt.json +1 -0
@@ 1,1 @@
+{"name":"sbt","version":"1.4.4","bspVersion":"2.0.0-M5","languages":["scala"],"argv":["/usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java","-Xms100m","-Xmx100m","-classpath","/usr/share/sbt/bin/sbt-launch.jar","xsbt.boot.Boot","-bsp"]}<
\ No newline at end of file
A => .gitignore +13 -0
@@ 1,13 @@
+# Freedesktop directory file
+.directory
+
+# Eclipse project files
+.classpath
+.cache-main
+.cache-tests
+.project
+.settings
+
+# Compiled binaries
+target/
+bin/<
\ No newline at end of file
A => README.md +6 -0
@@ 1,6 @@
+# Can't Stop Me Now Telegram Bot
+---
+
+This is a first, ugly and fast implementation of a Telegram Bot that let's you play [Can't Stop](https://en.wikipedia.org/wiki/Can't_Stop_(board_game)) in Telegram.
+
+This is a simple project to try out my refactoring skills. I will document every commit hoping that someone finds it useful.
A => build.sbt +78 -0
@@ 1,78 @@
+// The simplest possible sbt build file is just one line:
+
+scalaVersion := "2.12.12"
+// That is, to create a valid sbt build, all you've got to do is define the
+// version of Scala you'd like your project to use.
+
+// ============================================================================
+
+// Lines like the above defining `scalaVersion` are called "settings". Settings
+// are key/value pairs. In the case of `scalaVersion`, the key is "scalaVersion"
+// and the value is "2.13.3"
+
+// It's possible to define many kinds of settings, such as:
+
+name := "Can't Stop Me Now Telegram Bot"
+organization := "org.gDanix"
+version := "0.1"
+
+// Note, it's not required for you to define these three settings. These are
+// mostly only necessary if you intend to publish your library's binaries on a
+// place like Sonatype or Bintray.
+
+
+// Want to use a published library in your project?
+// You can define other libraries as dependencies in your build like this:
+
+libraryDependencies ++= Seq(
+ "org.augustjune" %% "canoe" % "0.5.1"
+ )
+
+// Here, `libraryDependencies` is a set of dependencies, and by using `+=`,
+// we're adding the scala-parser-combinators dependency to the set of dependencies
+// that sbt will go and fetch when it starts up.
+// Now, in any Scala file, you can import classes, objects, etc., from
+// scala-parser-combinators with a regular import.
+
+// TIP: To find the "dependency" that you need to add to the
+// `libraryDependencies` set, which in the above example looks like this:
+
+// "org.scala-lang.modules" %% "scala-parser-combinators" % "1.1.2"
+
+// You can use Scaladex, an index of all known published Scala libraries. There,
+// after you find the library you want, you can just copy/paste the dependency
+// information that you need into your build file. For example, on the
+// scala/scala-parser-combinators Scaladex page,
+// https://index.scala-lang.org/scala/scala-parser-combinators, you can copy/paste
+// the sbt dependency from the sbt box on the right-hand side of the screen.
+
+// IMPORTANT NOTE: while build files look _kind of_ like regular Scala, it's
+// important to note that syntax in *.sbt files doesn't always behave like
+// regular Scala. For example, notice in this build file that it's not required
+// to put our settings into an enclosing object or class. Always remember that
+// sbt is a bit different, semantically, than vanilla Scala.
+
+// ============================================================================
+
+// Most moderately interesting Scala projects don't make use of the very simple
+// build file style (called "bare style") used in this build.sbt file. Most
+// intermediate Scala projects make use of so-called "multi-project" builds. A
+// multi-project build makes it possible to have different folders which sbt can
+// be configured differently for. That is, you may wish to have different
+// dependencies or different testing frameworks defined for different parts of
+// your codebase. Multi-project builds make this possible.
+
+// Here's a quick glimpse of what a multi-project build looks like for this
+// build, with only one "subproject" defined, called `root`:
+
+// lazy val root = (project in file(".")).
+// settings(
+// inThisBuild(List(
+// organization := "ch.epfl.scala",
+// scalaVersion := "2.13.3"
+// )),
+// name := "hello-world"
+// )
+
+// To learn more about multi-project builds, head over to the official sbt
+// documentation at http://www.scala-sbt.org/documentation.html
A => project/build.properties +1 -0
@@ 1,1 @@
+sbt.version=1.4.4
A => project/plugins.sbt +1 -0
@@ 1,1 @@
+addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "5.2.4")
A => src/main/scala/org/gDanix/games/cant_stop_me_now_telegram_bot/CSMNT.scala +294 -0
@@ 1,294 @@
+package org.gDanix.games.cant_stop_me_now_telegram_bot
+
+import PRNG._
+import cats.data.State
+import scala.math.abs
+
+object CSMNT {
+ class DiceResult(val value: Int, override val toString: String)
+ object One extends DiceResult(1, "⚀")
+ object Two extends DiceResult(2, "⚁")
+ object Three extends DiceResult(3, "⚂")
+ object Four extends DiceResult(4, "⚃")
+ object Five extends DiceResult(5, "⚄")
+ object Six extends DiceResult(6, "⚅")
+
+ implicit object DiceResultComparator extends Ordering[DiceResult] {
+ override def compare (x: DiceResult, y: DiceResult): Int = x.value - y.value
+ }
+
+ def long2DiceResult(input: Long): DiceResult = (scala.math.abs(input) % 6) match {
+ case 0L => One
+ case 1L => Two
+ case 2L => Three
+ case 3L => Four
+ case 4L => Five
+ case 5L => Six
+ }
+
+ type Combinations =
+ ((Int, Int),
+ (Int, Int),
+ (Int, Int))
+
+ def getCombinations: ((DiceResult, DiceResult, DiceResult, DiceResult)) => Combinations = { case (fst, snd, thrd, fth) =>
+ (( fst.value + snd.value, thrd.value + fth.value),
+ ( fth.value + snd.value, thrd.value + fst.value),
+ ( thrd.value + snd.value, fth.value + fst.value))
+ }
+
+ def rollDices(prng: PRNG): ((DiceResult, DiceResult, DiceResult, DiceResult), PRNG) = {
+ val firstLong = prng.getLong
+ val firstResult = long2DiceResult(firstLong)
+ val secondPrng = prng.next
+ val secondLong = secondPrng.getLong
+ val secondResult = long2DiceResult(secondLong)
+ val thirdPrng = secondPrng.next
+ val thirdLong = thirdPrng.getLong
+ val thirdResult = long2DiceResult(thirdLong)
+ val fourthPrng = thirdPrng.next
+ val fourthLong = fourthPrng.getLong
+ val fourthResult = long2DiceResult(fourthLong)
+ ((firstResult, secondResult, thirdResult, fourthResult), fourthPrng.next)
+ }
+
+ // If you have to add another color, must be also added to the 'getNewColor' function below
+ trait Color
+ case object Black extends Color
+ case object Red extends Color
+ case object Green extends Color
+ case object Blue extends Color
+ case object Yellow extends Color
+
+ case class Player(name: String, color: Color)
+
+ case class Token(player: Player, column: Column, height: Int, temporal: Boolean)
+
+ type ColumnID = Int
+
+ case class Column(id: ColumnID, closed: Option[Player])
+
+ def getColumnID: (DiceResult, DiceResult) => ColumnID = { case (diceResult1, diceResult2) => diceResult1.value + diceResult2.value }
+
+
+ def getColumnMaxHeight(column: ColumnID): Int = {
+ val centerDev = abs(7-column)
+ 13-2*centerDev
+ }
+
+ case class Board(tokens: List[Token], columns: Map[ColumnID, Column])
+
+ object Board {
+ private val initialColumns: Map[ColumnID, Column] = {
+ Map (
+ 2 -> Column(2, None),
+ 3 -> Column(3, None),
+ 4 -> Column(4, None),
+ 5 -> Column(5, None),
+ 6 -> Column(6, None),
+ 7 -> Column(7, None),
+ 8 -> Column(8, None),
+ 9 -> Column(9, None),
+ 10 -> Column(10, None),
+ 11 -> Column(11, None),
+ 12 -> Column(12, None),
+ )
+ }
+
+ val emptyBoard: Board = Board(Nil, initialColumns)
+
+ }
+
+
+ trait GameStatus
+ object AddingPlayers extends GameStatus
+ object TurnStart extends GameStatus
+ object ChoosingContinue extends GameStatus
+ object DicesRolled extends GameStatus
+
+ case class Game(players: List[Player], currentPlayer: Int, currentStatus: GameStatus, board: Board)
+
+
+ object Game {
+
+ val MaximumPlayersReachedException = "No more players allowed (only 4)"
+ val PlayerIsAlreadyInTheGame = "The player is already in"
+ val NotValidPlayer = "You're not a valid player"
+
+ def getNewPlayerColor(currentColorsInUse: List[Color]): Either[String, Color] = {
+ val allColors: Set[Color] = Set(Red, Green, Blue, Yellow)
+ val availableColors: Set[Color] = (allColors -- currentColorsInUse)
+ availableColors.headOption.toRight(MaximumPlayersReachedException)
+ }
+
+ def addPlayer(name: String, currentGame: Game): Either[String, (Player, Game)] =
+ if(currentGame.players.filter(_.name == name).nonEmpty) Left(PlayerIsAlreadyInTheGame) else {
+ val colorsInUse: List[Color] = currentGame.players.map(_.color)
+ for {
+ newPlayerColor <- getNewPlayerColor(colorsInUse)
+ newPlayer: Player = Player(name, newPlayerColor)
+ newPlayerList: List[Player] = newPlayer :: currentGame.players
+ newGame = currentGame.copy(players=newPlayerList)
+ } yield (newPlayer, newGame)
+ }
+
+ private def showGameAddingPlayers(game: Game): String = {
+ val players: String = game.players.map(p => s" ${p.color}: ${p.name}").mkString("\n")
+ val playersDesc: String = if(players.isEmpty()) "No hay jugadores!" else s"Jugadores: \n$players"
+
+ playersDesc
+ }
+
+ private def showGameTurn(game: Game): String = {
+
+ val boardInfo: String = game.players.map { player =>
+ val introLine = s" ${player.name}: \n"
+ val playerTokens = game.board.tokens.filter(_.player == player).groupBy(_.column.id).toList.sortBy(_._1).map{
+ case (columnID, t1 :: t2 :: Nil) => if(t1.temporal) {
+ val tokenString = s"${t2.height} (temp: ${t1.height})/ max: ${getColumnMaxHeight(columnID)}"
+ s"Column $columnID: $tokenString"
+ } else {
+ val tokenString = s"${t1.height} (temp: ${t2.height})/ max: ${getColumnMaxHeight(columnID)}"
+ s"Column $columnID: $tokenString"
+ }
+ case (columnID, token :: Nil) => {
+ val tokenString = if(token.temporal) s"0 (temp: ${token.height})/ max: ${getColumnMaxHeight(columnID)}" else s"${token.height}/ max: ${getColumnMaxHeight(columnID)}"
+ s"Column $columnID: $tokenString"
+ }
+ }.mkString("\n")
+ introLine + playerTokens
+ }.mkString("\n")
+
+ boardInfo
+ }
+
+ def showGame(game: Game): String = game.currentStatus match {
+ case AddingPlayers => showGameAddingPlayers(game)
+ case _ => showGameTurn(game)
+ }
+
+ def init(config: Game, prng: PRNG): (Game, PRNG) = {
+ val firstTurn = prng.getLong % config.players.size
+ val newGame = config.copy(currentPlayer = firstTurn.toInt, currentStatus = TurnStart)
+ (newGame, prng.next)
+ }
+
+ def setNextTurnPlayer(config:Game): Game =
+ config.copy(currentPlayer = (config.currentPlayer + 1) % config.players.size)
+
+ def cleanTemporalTokens(config:Game): Game = {
+ val newTokenList = config.board.tokens.filter(!_.temporal)
+ val newBoard = config.board.copy(tokens = newTokenList)
+ config.copy(board = newBoard)
+ }
+
+ def assignTemporalTokens(config: Game): Game = {
+ val curTokens = config.board.tokens.filter(token => token.temporal)
+ val curPlayer = config.players(config.currentPlayer)
+ val nextTokenList = curTokens.foldLeft(config.board.tokens){ case (tokens, token) =>
+ token.copy(temporal = false) :: (tokens.filterNot(t => t.column == token.column && t.player == curPlayer))
+ }
+ val newBoard = config.board.copy(tokens = nextTokenList)
+ config.copy(board = newBoard)
+ }
+
+ def closeColumns(config: Game): Game = {
+ val nextColumns = config.board.columns.map{ case (id, column) =>
+ val maxToken = config.board.tokens.maxBy(_.height)
+ if(maxToken.height == getColumnMaxHeight(id)) (id, column.copy(closed = Some(maxToken.player)))
+ else (id, column)
+ }
+
+ val nextTokens = config.board.tokens.filter(t => nextColumns(t.column.id).closed.isEmpty)
+
+ val newBoard = config.board.copy(tokens = nextTokens, columns = nextColumns)
+ config.copy(board = newBoard)
+ }
+
+ def checkIfCurrentPlayerWins(config: Game): Option[Player] = {
+ val currentPlayer = config.players(config.currentPlayer)
+ val columnsClosedByPlayer = config.board.columns.filter{case(_, column) => column.closed.contains(currentPlayer)}
+ if(columnsClosedByPlayer.size >= 3) Some(currentPlayer)
+ else None
+ }
+
+ class CombinationsPossibilities(val list: List[Int])
+ case class BothPossible(a: Int, b: Int) extends CombinationsPossibilities(List(a, b)) { override val toString: String = s"/${a}and${b}" }
+ case class OneToChoosePossible(a: Int, b: Int) extends CombinationsPossibilities(List(a, b)) { override val toString: String = s"/${a} /${b}" }
+ case class OnePossible(a: Int) extends CombinationsPossibilities(List(a)) { override val toString: String = s"/${a}" }
+ case object NonePossible extends CombinationsPossibilities(List()) { override val toString: String = s"" }
+
+ def isColumnClosed(columnID: ColumnID, config: Game): Boolean = config.board.columns(columnID).closed.isDefined
+
+ def getNumTemporalTokensAvailable(config: Game): Int = 3 - (config.board.tokens.filter(_.temporal).size)
+
+ def addToken(token: Token, config: Game): Game = {
+ val newTokenList = token :: config.board.tokens
+ val newBoard = config.board.copy(tokens = newTokenList)
+ config.copy(board = newBoard)
+ }
+
+ def deleteToken(token: Token, config: Game): Game = {
+ val newTokenList = config.board.tokens.filter(_ != token)
+ val newBoard = config.board.copy(tokens = newTokenList)
+ config.copy(board = newBoard)
+ }
+
+ def attemptMove(columnToMove: Int, config: Game): Option[Game] = for {
+ column <- {
+ val ret = config.board.columns.get(columnToMove)
+ ret
+ }
+ _ <- {
+ val ret = if(isColumnClosed(columnToMove, config)) None else Some()
+ ret
+ }
+ currentPlayer = config.players(config.currentPlayer)
+ tokens = config.board.tokens.filter(t => t.temporal && t.column.id == columnToMove && t.player == currentPlayer)
+ game <- {
+ if(tokens.isEmpty){
+ if(getNumTemporalTokensAvailable(config) > 0) Some(addToken(Token(currentPlayer, column, 0, true), config))
+ else None
+ } else {
+ val maxToken = tokens.maxBy(_.height)
+ if(maxToken.height < (getColumnMaxHeight(columnToMove)-1)) Some(deleteToken(maxToken, (addToken(maxToken.copy(height = maxToken.height + 1), config))))
+ else None
+ }
+ }
+ } yield game
+
+ private def filterOneCombination(a: Int, b: Int, numTokensAvailable: Int, config: Game): CombinationsPossibilities = {
+ lazy val bothMovesResult = for {
+ afterFirstMove <- attemptMove(a, config)
+ afterSecondMove <- attemptMove(b, afterFirstMove)
+ } yield afterSecondMove
+
+ lazy val aMoveResult = attemptMove(a, config)
+ lazy val bMoveResult = attemptMove(b, config)
+
+ if(bothMovesResult.isDefined) BothPossible(a, b)
+ else (aMoveResult, bMoveResult) match {
+ case (Some(_), Some(_)) => OneToChoosePossible(a, b)
+ case (None, Some(_)) => OnePossible(b)
+ case (Some(_), None) => OnePossible(a)
+ case (None, None) => NonePossible
+ }
+ }
+
+
+
+ def filter(combinations: Combinations, config: Game): (CombinationsPossibilities, CombinationsPossibilities, CombinationsPossibilities) = {
+ val temporalTokensPlaced = config.board.tokens.filter(_.temporal)
+ val numTemporalTokensAvailable = 3 - temporalTokensPlaced.size
+
+
+ val ((a1, b1), (a2, b2), (a3, b3)) = combinations
+
+ (filterOneCombination(a1, b1, numTemporalTokensAvailable, config),
+ filterOneCombination(a2, b2, numTemporalTokensAvailable, config),
+ filterOneCombination(a3, b3, numTemporalTokensAvailable, config))
+
+ }
+
+ }
+}
A => src/main/scala/org/gDanix/games/cant_stop_me_now_telegram_bot/Main.scala +157 -0
@@ 1,157 @@
+package org.gDanix.games.cant_stop_me_now_telegram_bot
+
+
+import org.gDanix.games.cant_stop_me_now_telegram_bot.CSMNT._
+import org.gDanix.games.cant_stop_me_now_telegram_bot.CSMNT.Game._
+import cats.effect.{ IO, IOApp, ExitCode, Timer }
+import canoe.api._
+import canoe.syntax._
+import fs2.Stream
+import canoe.models.messages.TextMessage
+import java.util.GregorianCalendar
+import canoe.models.Chat
+import scala.util.Try
+
+
+object MainTest extends App {
+
+}
+
+
+object Main extends IOApp {
+
+ var currentGame = Game(Nil, -1, AddingPlayers, Board.emptyBoard)
+ var prng = PRNG.fromSeed(System.currentTimeMillis())
+
+ def run(args: List[String]): IO[ExitCode] =
+ Stream
+ .resource(TelegramClient.global[IO](args(0)))
+ .flatMap { implicit client => Bot.polling[IO].follow(resetGame, showGame, addMe, startGame) }
+ .compile.drain.as(ExitCode.Success)
+
+ def resetGame[F[_]: TelegramClient: Timer]: Scenario[F, Unit] =
+ for {
+ chat <- Scenario.expect(command("resetgame").chat)
+ _ = { currentGame = Game(Nil, -1, AddingPlayers, Board.emptyBoard) }
+ _ <- Scenario.eval(chat.send("Ok!"))
+ } yield ()
+
+ def showGame[F[_]: TelegramClient: Timer]: Scenario[F, Unit] =
+ for {
+ chat <- Scenario.expect(command("showgame").chat)
+ _ <- Scenario.eval(chat.send(Game.showGame(currentGame)))
+ } yield ()
+
+ def addMe[F[_]: TelegramClient: Timer]: Scenario[F, Unit] =
+ for {
+ messageInfo <- Scenario.expect(command("addme"))
+ _ <- if(currentGame.currentStatus == AddingPlayers) commandAddPlayer(messageInfo) else Scenario.eval(messageInfo.chat.send("La partida ya está empezada"))
+ } yield ()
+
+ def commandAddPlayer[F[_]: TelegramClient: Timer](messageInfo: TextMessage): Scenario[F, Unit] = {
+ val chat = messageInfo.chat
+ val messageOrExc = for {
+ author <- messageInfo.from.toRight(NotValidPlayer)
+ newPlayerAndGame <- addPlayer(author.firstName, currentGame)
+ (newPlayer , newGame) = newPlayerAndGame
+ _ = { currentGame = newGame }
+ } yield s"Ok! Añadido: $newPlayer"
+
+ val message = messageOrExc.merge
+
+ for{
+ _ <- Scenario.eval(chat.send(message))
+ } yield ()
+ }
+
+ def startGame[F[_]: TelegramClient: Timer]: Scenario[F, Unit] =
+ for {
+ chat <- Scenario.expect(command("startgame").chat)
+ _ <- if(currentGame.currentStatus != AddingPlayers){
+ Scenario.eval(chat.send("The game has already started"))
+ } else {
+ if(currentGame.players.size == 0) {
+ Scenario.eval(chat.send("No players to start the game"))
+ } else {
+ val (nextGame, nextPrng) = Game.init(currentGame, prng); currentGame = nextGame; prng = nextPrng
+ Scenario.eval(chat.send("Ok!")).flatMap(_ => comienzaTurno(chat))
+ }
+ }
+ } yield ()
+
+ def comienzaTurno[F[_]: TelegramClient: Timer](chat: Chat): Scenario[F, Unit] =
+ for {
+ _ <- Scenario.eval(chat.send(s"Your turn, ${currentGame.players(currentGame.currentPlayer).name}"))
+ _ <- tiraDados(chat)
+ } yield ()
+
+ def parseOption(combinations: (CombinationsPossibilities, CombinationsPossibilities, CombinationsPossibilities), text: String): Option[Either[Int, (Int, Int)]] = {
+ def parseResult: CombinationsPossibilities => Option[Either[Int, (Int, Int)]] = {
+ case BothPossible(a, b) => Some(Right(a, b))
+ case OnePossible(a) => Some(Left(a))
+ case NonePossible => None
+ }
+
+ (combinations._1.toString, combinations._2.toString, combinations._3.toString) match {
+ case (`text`, _, _) => parseResult(combinations._1)
+ case (_, `text`, _) => parseResult(combinations._2)
+ case (_, _, `text`) => parseResult(combinations._3)
+ case _ => combinations match {
+ case (OneToChoosePossible(a, b), _, _) if(s"/$a" == text || s"/$b" == text) => if(s"/$a" == text) Some(Left(a)) else Some(Left(b))
+ case (_, OneToChoosePossible(a, b), _) if(s"/$a" == text || s"/$b" == text) => if(s"/$a" == text) Some(Left(a)) else Some(Left(b))
+ case (_, _, OneToChoosePossible(a, b)) if(s"/$a" == text || s"/$b" == text) => if(s"/$a" == text) Some(Left(a)) else Some(Left(b))
+ case _ => None
+ }
+ }
+ }
+
+ def askOptions[F[_]: TelegramClient: Timer](chat: Chat, combinations: (CombinationsPossibilities, CombinationsPossibilities, CombinationsPossibilities)): Scenario[F, Either[Int, (Int, Int)]] =
+ for {
+ _ <- Scenario.eval(chat.send(s"Possible moves:\n${combinations._1}\n${combinations._2}\n${combinations._3}"))
+ message <- Scenario.expect(textMessage)
+ parsedFinally <- parseOption(combinations, message.text) match {
+ case Some(parsed) => Scenario.pure[F](parsed)
+ case None => Scenario.eval(chat.send("Incorrect option")).flatMap(_ => askOptions(chat, combinations))
+ }
+ } yield parsedFinally
+
+ def tiraDados[F[_]: TelegramClient: Timer](chat: Chat): Scenario[F, Unit] = {
+ val (rolledDices, nextPRNG) = rollDices(prng)
+ prng = nextPRNG
+ val combinations = getCombinations(rolledDices)
+ val filteredCombinations = Game.filter(combinations, currentGame)
+ if(filteredCombinations == (NonePossible, NonePossible, NonePossible)) {
+ for{
+ _ <- Scenario.eval(chat.send(s"Dice roll: ${rolledDices._1}${rolledDices._2}${rolledDices._3}${rolledDices._4}"))
+ _ <- Scenario.eval(chat.send("No possible moves..."))
+ _ = { currentGame = cleanTemporalTokens(setNextTurnPlayer(currentGame))}
+ _ <- comienzaTurno(chat)
+ } yield ()
+ } else {
+ for {
+ _ <- Scenario.eval(chat.send(s"Dice roll: ${rolledDices._1}${rolledDices._2}${rolledDices._3}${rolledDices._4}"))
+ option <- askOptions(chat, filteredCombinations)
+ _ = option match {
+ case Left(columnID) => { currentGame = attemptMove(columnID, currentGame).get }
+ case Right((aColumnID, bColumnID)) => { currentGame = attemptMove(bColumnID, attemptMove(aColumnID, currentGame).get).get }
+ }
+ _ = { currentGame = closeColumns(currentGame) }
+ _ <- if(checkIfCurrentPlayerWins(currentGame).isDefined) {
+ Scenario.eval(chat.send(s"Congratulations!! Player ${currentGame.players(currentGame.currentPlayer).name} wins!!"))
+ } else {
+ for {
+ _ <- Scenario.eval(chat.send(Game.showGame(currentGame) + "\n\n/pass or /continue"))
+ nextAction <- Scenario.expect(textMessage)
+ _ <- if(nextAction.text == "/continue") tiraDados(chat)
+ else {
+ currentGame = setNextTurnPlayer(assignTemporalTokens(currentGame))
+ Scenario.eval(chat.send(s"I'll take it as a pass")).flatMap(_ => comienzaTurno(chat))
+ }
+ } yield ()
+ }
+ } yield ()
+ }
+
+ }
+
+}
A => src/main/scala/org/gDanix/games/cant_stop_me_now_telegram_bot/PRNG.scala +16 -0
@@ 1,16 @@
+package org.gDanix.games.cant_stop_me_now_telegram_bot
+
+import cats.data.State
+
+object PRNG {
+ type PRNGState = State[PRNG, Long]
+
+ final case class PRNG(seed: Long) {
+ lazy val next: PRNG = PRNG(seed * 6364136223846793005L + 1442695040888963407L)
+ val getLong: Long = seed
+ lazy val toState: State[PRNG, Long] = State(current => (current.next, current.getLong))
+ }
+
+ def fromSeed(seed:Long): PRNG = PRNG(seed).next
+// def fromSeed(seed: Long): PRNGState = PRNG(seed).next.toState
+}