diff --git a/ComputerPlayer.scala b/ComputerPlayer.scala new file mode 100644 index 00000000..01c94654 --- /dev/null +++ b/ComputerPlayer.scala @@ -0,0 +1,60 @@ +import scala.concurrent.{Await, Future} +import scala.concurrent.ExecutionContext.Implicits.global +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 = log(":)") + + 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 if board.isMoveLegal(playerId, i)) + 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 if board.isMoveLegal(playerId, i)) + 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 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) +// val scores = futures + val move = scores.maxBy(_._2)._1 + log(s"moving $move") + move diff --git a/HumanConsolePlayer.scala b/HumanConsolePlayer.scala new file mode 100644 index 00000000..a82d46d5 --- /dev/null +++ b/HumanConsolePlayer.scala @@ -0,0 +1,37 @@ +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") + + 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 + 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..42bac926 --- /dev/null +++ b/KalahaBoard.scala @@ -0,0 +1,111 @@ +import scala.annotation.tailrec + +class IllegalMoveException(message: String) extends Exception(message) +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().map(house => house.copy()), + player2Houses.clone().map(house => house.copy()), + player1Store.copy(), + player2Store.copy() + ) + + @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 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) = + 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) = + 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 ++= 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/Main.scala b/Main.scala new file mode 100644 index 00000000..52e6d4a6 --- /dev/null +++ b/Main.scala @@ -0,0 +1,31 @@ +import scala.annotation.tailrec +import scala.io.StdIn + +object Main: + 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/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..88709244 --- /dev/null +++ b/Player.scala @@ -0,0 +1,7 @@ +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 new file mode 100644 index 00000000..56e71e18 --- /dev/null +++ b/Server.scala @@ -0,0 +1,34 @@ +import java.util.concurrent.TimeoutException +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 + try + while !gameBoard.endGame() do + 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 + }, 120.seconds) + val nextPlayerId = gameBoard.makeAMove(currentPlayer.id, moveIndex) + currentPlayer = if nextPlayerId == 1 then player1 else player2 + + 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