From e195912832238ecd33510a1e2c42dd05e39a786e Mon Sep 17 00:00:00 2001 From: Adwin120 Date: Mon, 17 Jan 2022 03:17:40 +0100 Subject: [PATCH 1/3] implement pvp through console --- HumanConsolePlayer.scala | 27 +++++++++++++ KalahaBoard.scala | 85 ++++++++++++++++++++++++++++++++++++++++ Main.scala | 7 ++++ Pit.scala | 10 +++++ Player.scala | 6 +++ Server.scala | 24 ++++++++++++ 6 files changed, 159 insertions(+) create mode 100644 HumanConsolePlayer.scala create mode 100644 KalahaBoard.scala create mode 100644 Main.scala create mode 100644 Pit.scala create mode 100644 Player.scala create mode 100644 Server.scala diff --git a/HumanConsolePlayer.scala b/HumanConsolePlayer.scala new file mode 100644 index 00000000..18f7492d --- /dev/null +++ b/HumanConsolePlayer.scala @@ -0,0 +1,27 @@ +import scala.io.StdIn + +class HumanConsolePlayer extends Player { + def log(message: String) = println(s"Player$id: $message") + var id: Int = _ + override def notifyOfIllegalMovement(): Unit = + log("This move is illegal, try again") + + override def sendResult(scores: (Int, Int)): Unit = + val (player1Score, player2Score) = scores + val scoreDiffrence = player1Score - player2Score + if scoreDiffrence == 0 then + log("Draw!") + else + val playerWon = if scoreDiffrence > 0 then 1 else 2 + if playerWon == this.id then + log("You won! c:") + else + log("You lost! :c") + + log(s"player1: $player1Score - player2: $player2Score") + + override def requestMove(board: KalahaBoard): Int = + board.printBoard() + log(s"Choose a house: (0-${board.housesPerSide - 1})") + StdIn.readInt() +} diff --git a/KalahaBoard.scala b/KalahaBoard.scala new file mode 100644 index 00000000..cd69019e --- /dev/null +++ b/KalahaBoard.scala @@ -0,0 +1,85 @@ +import scala.annotation.tailrec + +class IllegalMoveException(message: String) extends Exception(message) +class KalahaBoard(val housesPerSide: Int, val initialSeedAmount: Int): + val player1Houses = Array.tabulate(housesPerSide)(i => House(initialSeedAmount, 1, i)) + val player2Houses = Array.tabulate(housesPerSide)(i => House(initialSeedAmount, 2, i)) + val player1Store = Store(0, 1) + val player2Store = Store(0, 2) + + @tailrec + private def nextPitToSow(playerId: Int)(currentPit: Pit): Pit = + val maxHouseIndex = housesPerSide - 1 + currentPit match + case Store(_, 1) => player2Houses(0) + case Store(_, 2) => player1Houses(0) + case House(_, 1, `maxHouseIndex`) => if playerId == 1 then player1Store else nextPitToSow(playerId)(player1Store) + case House(_, 1, i) => player1Houses(i + 1) + case House(_, 2, `maxHouseIndex`) => if playerId == 2 then player2Store else nextPitToSow(playerId)(player2Store) + case House(_, 2, i) => player2Houses(i + 1) + + def houseByIds(playerId: Int, houseIndex: Int) = (if playerId == 1 then player1Houses else player2Houses)(houseIndex) + + def getOppositeHouse(house: House) = + val House(_, playerId, index) = house + (if playerId == 1 then player1Houses else player2Houses)(housesPerSide - index - 1) + + def getOppositePlayerId(playerId: Int) = if playerId == 1 then 2 else 1 + + def scores = (player1Store.seeds, player2Store.seeds) + + def isMoveLegal(playerId: Int, houseIndex: Int) = + val House(seeds, _, _) = houseByIds(playerId, houseIndex) + seeds > 0 + + //returns id of player that will be moving next + def makeAMove(currentPlayer: Int, chosenMove: Int) = + if !isMoveLegal(currentPlayer, chosenMove) then throw new IllegalMoveException(s"house $chosenMove is empty") + val chosenHouse = houseByIds(currentPlayer, chosenMove) + val seedsToSow = chosenHouse.clear() + @tailrec + def sow(currentPit: Pit, seedsLeft: Int): Int = + currentPit.increment() + if seedsLeft > 1 then + sow(nextPitToSow(currentPlayer)(currentPit), seedsLeft - 1) + else + currentPit match + case Store(_, _) => currentPlayer + case House(1, `currentPlayer`, _) => + val oppositeHouse = getOppositeHouse(currentPit.asInstanceOf[House]) + val playersStore = if currentPlayer == 1 then player1Store else player2Store + playersStore.add(oppositeHouse.clear() + currentPit.clear()) + getOppositePlayerId(currentPlayer) + case _ => getOppositePlayerId(currentPlayer) + + + sow(nextPitToSow(currentPlayer)(chosenHouse), seedsToSow) + + def collectAllSeeds(playerId: Int) = + val playerHouses = if playerId == 1 then player1Houses else player2Houses + val allSeeds = ( + for( + i <- playerHouses + ) yield i.clear() + ).sum + (if playerId == 1 then player1Store else player2Store).add(allSeeds) + + def endGame(): Boolean = + val canPlayer1Move = !player1Houses.forall(house => house.seeds == 0) + if !canPlayer1Move then collectAllSeeds(2) + val canPlayer2Move = !player2Houses.forall(house => house.seeds == 0) + if !canPlayer2Move then collectAllSeeds(1) + !canPlayer1Move || !canPlayer2Move + + def printBoard() = + val stringBuilder = new StringBuilder() + stringBuilder ++= ((housesPerSide - 1) to 0 by -1).mkString(" | ") + stringBuilder ++= "\n\n" + stringBuilder ++= player2Houses.map(_.seeds).reverse.mkString(" | ") + stringBuilder ++= "\n" + stringBuilder ++= "\t\t" ++= player2Store.seeds.toString ++= "\t" ++= player1Store.seeds.toString + stringBuilder ++= "\n" + stringBuilder ++= player1Houses.map(_.seeds).mkString(" | ") + stringBuilder ++= "\n\n" + stringBuilder ++= (0 until housesPerSide).mkString(" | ") + println(stringBuilder.result()) \ No newline at end of file diff --git a/Main.scala b/Main.scala new file mode 100644 index 00000000..22e94a3e --- /dev/null +++ b/Main.scala @@ -0,0 +1,7 @@ +object Main: + def main(args: Array[String]): Unit = { + val player1 = new HumanConsolePlayer() + val player2 = new HumanConsolePlayer() + val server = new Server(player1, player2) + server.play(6, 4) + } diff --git a/Pit.scala b/Pit.scala new file mode 100644 index 00000000..c53addc9 --- /dev/null +++ b/Pit.scala @@ -0,0 +1,10 @@ +sealed trait Pit: + var seeds: Int + def increment(): Unit = this.seeds += 1 + def add(amount: Int): Unit = this.seeds += amount + def clear(): Int = + val t = this.seeds + this.seeds = 0 + t +case class House(var seeds: Int, ownerId: Int, index: Int) extends Pit +case class Store(var seeds: Int, ownerId: Int) extends Pit diff --git a/Player.scala b/Player.scala new file mode 100644 index 00000000..74f6a3bb --- /dev/null +++ b/Player.scala @@ -0,0 +1,6 @@ +trait Player { + var id: Int + def requestMove(board: KalahaBoard): Int + def notifyOfIllegalMovement(): Unit + def sendResult(scores: (Int, Int)): Unit +} diff --git a/Server.scala b/Server.scala new file mode 100644 index 00000000..8c014085 --- /dev/null +++ b/Server.scala @@ -0,0 +1,24 @@ +import scala.concurrent.{Await, Future} +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration.DurationInt +class Server(val player1: Player, val player2: Player): + player1.id = 1 + player2.id = 2 + + def oppositePlayer(player: Player) = if player.id == 1 then player2 else player1 + + def play(housesPerSide: Int, initialSeedAmount: Int) = + val gameBoard = new KalahaBoard(housesPerSide, initialSeedAmount) + var currentPlayer = player1 + while !gameBoard.endGame() do + val moveIndex = Await.result(Future { + currentPlayer.requestMove(gameBoard) + }, 30.seconds) + if !gameBoard.isMoveLegal(currentPlayer.id, moveIndex) then + currentPlayer.notifyOfIllegalMovement() + else + val nextPlayerId = gameBoard.makeAMove(currentPlayer.id, moveIndex) + currentPlayer = if nextPlayerId == 1 then player1 else player2 + + player1.sendResult(gameBoard.scores) + player2.sendResult(gameBoard.scores) \ No newline at end of file From 8e3dc4ee470dd5a85ea2d871118e8dcfcef93aaf Mon Sep 17 00:00:00 2001 From: Adwin120 Date: Tue, 18 Jan 2022 02:25:45 +0100 Subject: [PATCH 2/3] add computerPlayer --- ComputerPlayer.scala | 49 ++++++++++++++++++++++++++++++++ HumanConsolePlayer.scala | 13 +++++++-- KalahaBoard.scala | 60 ++++++++++++++++++++++++++++------------ Player.scala | 5 ++-- Server.scala | 27 +++++++++++------- 5 files changed, 122 insertions(+), 32 deletions(-) create mode 100644 ComputerPlayer.scala diff --git a/ComputerPlayer.scala b/ComputerPlayer.scala new file mode 100644 index 00000000..9f3fa425 --- /dev/null +++ b/ComputerPlayer.scala @@ -0,0 +1,49 @@ +import scala.concurrent.{Await, Future} +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration.Duration + +class ComputerPlayer extends Player: + var id: Int = _ + + override def notifyOfIllegalMovement(): Unit = ??? + + override def notifyOfGameInterrupted(playerWonId: Int): Unit = ??? + + override def sendResult(scores: (Int, Int)): Unit = + val (player1Scores, player2Scores) = scores + println(s"Player1: $player1Scores, Player2: $player2Scores") + + def boardStaticEvaluation(board: KalahaBoard) = + val (player1Scores, player2Scores) = board.scores + val scoreDiff = player1Scores - player2Scores + if id == 1 then scoreDiff else -scoreDiff + + def minimax(board: KalahaBoard, depth: Int, playerId: Int): Int = + if depth == 0 || board.endGame() then + boardStaticEvaluation(board) + else + if playerId == id then + var maxEval = Integer.MIN_VALUE + for(i <- 0 until board.housesPerSide) + val nextBoard = board.copy() + val nextPlayer = nextBoard.makeAMove(playerId, i) + val eval = minimax(nextBoard, depth - 1, nextPlayer) + if eval > maxEval then maxEval = eval + + maxEval + else + var minEval = Integer.MAX_VALUE + for(i <- 0 until board.housesPerSide) + val nextBoard = board.copy() + val nextPlayer = nextBoard.makeAMove(playerId, i) + val eval = minimax(nextBoard, depth - 1, nextPlayer) + if eval < minEval then minEval = eval + + minEval + + override def requestMove(board: KalahaBoard): Int = + val futures = for(i <- 0 until board.housesPerSide) yield Future { + minimax(board, 5, id) + } + val scores = Await.result(Future.sequence(futures), Duration.Inf) + scores.zipWithIndex.maxBy(_._1)._2 diff --git a/HumanConsolePlayer.scala b/HumanConsolePlayer.scala index 18f7492d..975256b9 100644 --- a/HumanConsolePlayer.scala +++ b/HumanConsolePlayer.scala @@ -1,11 +1,18 @@ import scala.io.StdIn -class HumanConsolePlayer extends Player { - def log(message: String) = println(s"Player$id: $message") +class HumanConsolePlayer extends Player: var id: Int = _ + def messageColor = if id == 1 then Console.GREEN else if id == 2 then Console.BLUE else Console.WHITE + def log(message: String) = println(s"$messageColor Player$id: $message ${Console.RESET}") override def notifyOfIllegalMovement(): Unit = log("This move is illegal, try again") + override def notifyOfGameInterrupted(playerWonId: Int): Unit = + if playerWonId != id then + log("You have ran out of time, you lost!") + else + log("your opponent has ran out of time, you won!") + override def sendResult(scores: (Int, Int)): Unit = val (player1Score, player2Score) = scores val scoreDiffrence = player1Score - player2Score @@ -24,4 +31,4 @@ class HumanConsolePlayer extends Player { board.printBoard() log(s"Choose a house: (0-${board.housesPerSide - 1})") StdIn.readInt() -} + diff --git a/KalahaBoard.scala b/KalahaBoard.scala index cd69019e..ed5ba84b 100644 --- a/KalahaBoard.scala +++ b/KalahaBoard.scala @@ -1,11 +1,34 @@ import scala.annotation.tailrec class IllegalMoveException(message: String) extends Exception(message) -class KalahaBoard(val housesPerSide: Int, val initialSeedAmount: Int): - val player1Houses = Array.tabulate(housesPerSide)(i => House(initialSeedAmount, 1, i)) - val player2Houses = Array.tabulate(housesPerSide)(i => House(initialSeedAmount, 2, i)) - val player1Store = Store(0, 1) - val player2Store = Store(0, 2) +class KalahaBoard( + val housesPerSide: Int, + val initialSeedAmount: Int, + val player1Houses: Array[House], + val player2Houses: Array[House], + val player1Store: Store, + val player2Store: Store + ): + + def this(housesPerSide: Int, initialSeedAmount: Int) = + this( + housesPerSide, + initialSeedAmount, + Array.tabulate(housesPerSide)(i => House(initialSeedAmount, 1, i)), + Array.tabulate(housesPerSide)(i => House(initialSeedAmount, 2, i)), + Store(0, 1), + Store(0, 2) + ) + + def copy() = + new KalahaBoard( + housesPerSide, + initialSeedAmount, + player1Houses.clone(), + player2Houses.clone(), + player1Store.copy(), + player2Store.copy() + ) @tailrec private def nextPitToSow(playerId: Int)(currentPit: Pit): Pit = @@ -22,15 +45,18 @@ class KalahaBoard(val housesPerSide: Int, val initialSeedAmount: Int): def getOppositeHouse(house: House) = val House(_, playerId, index) = house - (if playerId == 1 then player1Houses else player2Houses)(housesPerSide - index - 1) + (if playerId == 1 then player2Houses else player1Houses)(housesPerSide - index - 1) def getOppositePlayerId(playerId: Int) = if playerId == 1 then 2 else 1 def scores = (player1Store.seeds, player2Store.seeds) def isMoveLegal(playerId: Int, houseIndex: Int) = - val House(seeds, _, _) = houseByIds(playerId, houseIndex) - seeds > 0 + if houseIndex < 0 || houseIndex > housesPerSide - 1 then + false + else + val House(seeds, _, _) = houseByIds(playerId, houseIndex) + seeds > 0 //returns id of player that will be moving next def makeAMove(currentPlayer: Int, chosenMove: Int) = @@ -73,13 +99,13 @@ class KalahaBoard(val housesPerSide: Int, val initialSeedAmount: Int): def printBoard() = val stringBuilder = new StringBuilder() - stringBuilder ++= ((housesPerSide - 1) to 0 by -1).mkString(" | ") - stringBuilder ++= "\n\n" - stringBuilder ++= player2Houses.map(_.seeds).reverse.mkString(" | ") - stringBuilder ++= "\n" - stringBuilder ++= "\t\t" ++= player2Store.seeds.toString ++= "\t" ++= player1Store.seeds.toString - stringBuilder ++= "\n" - stringBuilder ++= player1Houses.map(_.seeds).mkString(" | ") - stringBuilder ++= "\n\n" - stringBuilder ++= (0 until housesPerSide).mkString(" | ") + stringBuilder ++= Console.WHITE + stringBuilder += ' ' ++= ((housesPerSide - 1) to 0 by -1).mkString(" ") += '\n' + stringBuilder ++= Console.BLUE + stringBuilder += '(' ++= player2Houses.map(_.seeds).reverse.mkString(")(") += ')' += '\n' + stringBuilder += ' '++= player2Store.seeds.toString ++= "\t\t\t\t" ++= Console.GREEN ++= player1Store.seeds.toString += '\n' + stringBuilder += '(' ++= player1Houses.map(_.seeds).mkString(")(") += ')' += '\n' + stringBuilder ++= Console.WHITE + stringBuilder += ' ' ++= (0 until housesPerSide).mkString(" ") + stringBuilder ++= Console.RESET println(stringBuilder.result()) \ No newline at end of file diff --git a/Player.scala b/Player.scala index 74f6a3bb..88709244 100644 --- a/Player.scala +++ b/Player.scala @@ -1,6 +1,7 @@ -trait Player { +trait Player: var id: Int def requestMove(board: KalahaBoard): Int def notifyOfIllegalMovement(): Unit + def notifyOfGameInterrupted(playerWonId: Int): Unit def sendResult(scores: (Int, Int)): Unit -} + diff --git a/Server.scala b/Server.scala index 8c014085..ea2b8c3a 100644 --- a/Server.scala +++ b/Server.scala @@ -1,3 +1,4 @@ +import java.util.concurrent.TimeoutException import scala.concurrent.{Await, Future} import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration.DurationInt @@ -6,19 +7,25 @@ class Server(val player1: Player, val player2: Player): player2.id = 2 def oppositePlayer(player: Player) = if player.id == 1 then player2 else player1 - def play(housesPerSide: Int, initialSeedAmount: Int) = val gameBoard = new KalahaBoard(housesPerSide, initialSeedAmount) var currentPlayer = player1 - while !gameBoard.endGame() do - val moveIndex = Await.result(Future { - currentPlayer.requestMove(gameBoard) - }, 30.seconds) - if !gameBoard.isMoveLegal(currentPlayer.id, moveIndex) then - currentPlayer.notifyOfIllegalMovement() - else + try + while !gameBoard.endGame() do + val moveIndex = Await.result(Future { + var move = currentPlayer.requestMove(gameBoard.copy()) + while !gameBoard.isMoveLegal(currentPlayer.id, move) do + currentPlayer.notifyOfIllegalMovement() + move = currentPlayer.requestMove(gameBoard.copy()) + + move + }, 30.seconds) val nextPlayerId = gameBoard.makeAMove(currentPlayer.id, moveIndex) currentPlayer = if nextPlayerId == 1 then player1 else player2 - player1.sendResult(gameBoard.scores) - player2.sendResult(gameBoard.scores) \ No newline at end of file + player1.sendResult(gameBoard.scores) + player2.sendResult(gameBoard.scores) + catch + case e: TimeoutException => + player1.notifyOfGameInterrupted(oppositePlayer(currentPlayer).id) + player2.notifyOfGameInterrupted(oppositePlayer(currentPlayer).id) \ No newline at end of file From 40e41fabb144d78a62ce5963d9be1dffc3faec69 Mon Sep 17 00:00:00 2001 From: Adwin120 Date: Thu, 20 Jan 2022 05:40:37 +0100 Subject: [PATCH 3/3] fix ai --- ComputerPlayer.scala | 23 +++++++++++++++++------ HumanConsolePlayer.scala | 3 +++ KalahaBoard.scala | 4 ++-- Main.scala | 36 ++++++++++++++++++++++++++++++------ Server.scala | 5 ++++- 5 files changed, 56 insertions(+), 15 deletions(-) diff --git a/ComputerPlayer.scala b/ComputerPlayer.scala index 9f3fa425..01c94654 100644 --- a/ComputerPlayer.scala +++ b/ComputerPlayer.scala @@ -5,9 +5,15 @@ import scala.concurrent.duration.Duration class ComputerPlayer extends Player: var id: Int = _ + def messageColor = if id == 1 then Console.GREEN else if id == 2 then Console.BLUE else Console.WHITE + + def log(message: String) = println(s"$messageColor ComPlayer$id: $message ${Console.RESET}") + + def opponentId = if id == 1 then 2 else 1 + override def notifyOfIllegalMovement(): Unit = ??? - override def notifyOfGameInterrupted(playerWonId: Int): Unit = ??? + override def notifyOfGameInterrupted(playerWonId: Int): Unit = log(":)") override def sendResult(scores: (Int, Int)): Unit = val (player1Scores, player2Scores) = scores @@ -24,7 +30,7 @@ class ComputerPlayer extends Player: else if playerId == id then var maxEval = Integer.MIN_VALUE - for(i <- 0 until board.housesPerSide) + for(i <- 0 until board.housesPerSide if board.isMoveLegal(playerId, i)) val nextBoard = board.copy() val nextPlayer = nextBoard.makeAMove(playerId, i) val eval = minimax(nextBoard, depth - 1, nextPlayer) @@ -33,7 +39,7 @@ class ComputerPlayer extends Player: maxEval else var minEval = Integer.MAX_VALUE - for(i <- 0 until board.housesPerSide) + for(i <- 0 until board.housesPerSide if board.isMoveLegal(playerId, i)) val nextBoard = board.copy() val nextPlayer = nextBoard.makeAMove(playerId, i) val eval = minimax(nextBoard, depth - 1, nextPlayer) @@ -42,8 +48,13 @@ class ComputerPlayer extends Player: minEval override def requestMove(board: KalahaBoard): Int = - val futures = for(i <- 0 until board.housesPerSide) yield Future { - minimax(board, 5, id) + val futures = for(i <- 0 until board.housesPerSide if board.isMoveLegal(id, i)) yield Future { + val nextBoard = board.copy() + val nextPlayer = nextBoard.makeAMove(id, i) + (i, minimax(nextBoard, 10, nextPlayer)) } val scores = Await.result(Future.sequence(futures), Duration.Inf) - scores.zipWithIndex.maxBy(_._1)._2 +// val scores = futures + val move = scores.maxBy(_._2)._1 + log(s"moving $move") + move diff --git a/HumanConsolePlayer.scala b/HumanConsolePlayer.scala index 975256b9..a82d46d5 100644 --- a/HumanConsolePlayer.scala +++ b/HumanConsolePlayer.scala @@ -2,8 +2,11 @@ import scala.io.StdIn class HumanConsolePlayer extends Player: var id: Int = _ + def messageColor = if id == 1 then Console.GREEN else if id == 2 then Console.BLUE else Console.WHITE + def log(message: String) = println(s"$messageColor Player$id: $message ${Console.RESET}") + override def notifyOfIllegalMovement(): Unit = log("This move is illegal, try again") diff --git a/KalahaBoard.scala b/KalahaBoard.scala index ed5ba84b..42bac926 100644 --- a/KalahaBoard.scala +++ b/KalahaBoard.scala @@ -24,8 +24,8 @@ class KalahaBoard( new KalahaBoard( housesPerSide, initialSeedAmount, - player1Houses.clone(), - player2Houses.clone(), + player1Houses.clone().map(house => house.copy()), + player2Houses.clone().map(house => house.copy()), player1Store.copy(), player2Store.copy() ) diff --git a/Main.scala b/Main.scala index 22e94a3e..52e6d4a6 100644 --- a/Main.scala +++ b/Main.scala @@ -1,7 +1,31 @@ +import scala.annotation.tailrec +import scala.io.StdIn + object Main: - def main(args: Array[String]): Unit = { - val player1 = new HumanConsolePlayer() - val player2 = new HumanConsolePlayer() - val server = new Server(player1, player2) - server.play(6, 4) - } + def main(args: Array[String]): Unit = + userInterfaceLoop() + + + @tailrec + def userInterfaceLoop(): Unit = + print("Choose mode (1 - comp vs comp, 2 - comp vs player, 3 - player vs player): ") + val mode = StdIn.readInt() + if mode < 1 || mode > 3 then + println("there is no mode like that") + userInterfaceLoop() + else + val players = + if mode == 1 then + (new ComputerPlayer(), new ComputerPlayer()) + else if mode == 2 then + (new HumanConsolePlayer(), new ComputerPlayer()) + else + (new HumanConsolePlayer(), new HumanConsolePlayer()) + + val server = new Server(players._1, players._2) + server.play(6, 4) + print("Do you want to play again? (y/n) : ") + if StdIn.readBoolean() then + userInterfaceLoop() + else + () \ No newline at end of file diff --git a/Server.scala b/Server.scala index ea2b8c3a..56e71e18 100644 --- a/Server.scala +++ b/Server.scala @@ -7,6 +7,7 @@ class Server(val player1: Player, val player2: Player): player2.id = 2 def oppositePlayer(player: Player) = if player.id == 1 then player2 else player1 + def play(housesPerSide: Int, initialSeedAmount: Int) = val gameBoard = new KalahaBoard(housesPerSide, initialSeedAmount) var currentPlayer = player1 @@ -15,11 +16,13 @@ class Server(val player1: Player, val player2: Player): val moveIndex = Await.result(Future { var move = currentPlayer.requestMove(gameBoard.copy()) while !gameBoard.isMoveLegal(currentPlayer.id, move) do + println(s"ILLEGAL MOVE $move by player ${currentPlayer.id}") + gameBoard.printBoard() currentPlayer.notifyOfIllegalMovement() move = currentPlayer.requestMove(gameBoard.copy()) move - }, 30.seconds) + }, 120.seconds) val nextPlayerId = gameBoard.makeAMove(currentPlayer.id, moveIndex) currentPlayer = if nextPlayerId == 1 then player1 else player2