diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..6792c975 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Default ignored files +KalahaGame/out/ diff --git a/KalahaGame/KalahaGame.iml b/KalahaGame/KalahaGame.iml new file mode 100644 index 00000000..6844c4bf --- /dev/null +++ b/KalahaGame/KalahaGame.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/KalahaGame/akka-actor_2.13-2.6.18.jar b/KalahaGame/akka-actor_2.13-2.6.18.jar new file mode 100644 index 00000000..1a9f391e Binary files /dev/null and b/KalahaGame/akka-actor_2.13-2.6.18.jar differ diff --git a/KalahaGame/config-1.4.0.jar b/KalahaGame/config-1.4.0.jar new file mode 100644 index 00000000..5030c9f7 Binary files /dev/null and b/KalahaGame/config-1.4.0.jar differ diff --git a/KalahaGame/scala-java8-compat_2.13-1.0.0.jar b/KalahaGame/scala-java8-compat_2.13-1.0.0.jar new file mode 100644 index 00000000..e455e02c Binary files /dev/null and b/KalahaGame/scala-java8-compat_2.13-1.0.0.jar differ diff --git a/KalahaGame/scala-library-2.13.7.jar b/KalahaGame/scala-library-2.13.7.jar new file mode 100644 index 00000000..c6e7f067 Binary files /dev/null and b/KalahaGame/scala-library-2.13.7.jar differ diff --git a/KalahaGame/src/application.conf b/KalahaGame/src/application.conf new file mode 100644 index 00000000..2b978666 --- /dev/null +++ b/KalahaGame/src/application.conf @@ -0,0 +1,3 @@ +akka{ + log-dead-letters = off +} diff --git a/KalahaGame/src/game/Board.scala b/KalahaGame/src/game/Board.scala new file mode 100644 index 00000000..b09c6428 --- /dev/null +++ b/KalahaGame/src/game/Board.scala @@ -0,0 +1,129 @@ +package game + +class Board(){ + var player1houses = Array(6, 6, 6, 6, 6, 6, 0) + var player2houses = Array(6, 6, 6, 6, 6, 6, 0) + val baseIndex = 6 + + def setForTest(p1ar: Array[Int], p2ar: Array[Int]): Unit = { + player1houses = p1ar + player2houses = p2ar + } + + def move(player: Int, house: Int): Boolean = { + var playerHouses = Array[Int]() + var opponentHouses = Array[Int]() + + player match{ + case 1 => + playerHouses = player1houses + opponentHouses = player2houses + case 2 => + playerHouses = player2houses + opponentHouses = player1houses + case _ => + } + var nextMove = false + + if(house < 1 || house > 6) then + throw new IllegalArgumentException("House number must be between 1 and 6") + else{ + val houseIndex = house - 1 + if playerHouses(houseIndex) == 0 then + throw new IllegalArgumentException("Cannot take seeds from an empty house") + else{ + var seeds = playerHouses(houseIndex) + playerHouses(houseIndex) = 0 + var currentHouse = houseIndex + 1 + while(seeds > 0) { + while (currentHouse < 7 && seeds > 0) { + if(seeds == 1 && currentHouse < 6 && playerHouses(currentHouse) == 0) then { + playerHouses(baseIndex) += opponentHouses(getOppositeHouseIndex(currentHouse)) + 1 + opponentHouses(getOppositeHouseIndex(currentHouse)) = 0 + seeds -= 1 + } else{ + playerHouses(currentHouse) += 1 + seeds -= 1 + currentHouse += 1 + } + } + if seeds == 0 && currentHouse == 7 then + nextMove = true + else { + currentHouse = 0 + while (currentHouse < 6 && seeds > 0) { + opponentHouses(currentHouse) += 1 + seeds -= 1 + currentHouse += 1 + } + currentHouse = 0 + + } + } + nextMove + } + } + + } + def getOppositeHouseIndex(myHouseIndex: Int): Int = { + 5 - myHouseIndex + } + + def isFinished(): Boolean = { + var sum1 = 0 + var sum2 = 0 + + for(i <- 0 to 5){ + sum1 += player1houses(i) + sum2 += player2houses(i) + } +// println(sum1) +// println(sum2) + sum1 == 0 || sum2 == 0 + } + + def getFinalScore(): (Int, Int) ={ + var p1score = 0 + var p2score = 0 + + for(i <- 0 to 6){ + p1score += player1houses(i) + p2score += player2houses(i) + } + + (p1score, p2score) + } + + def copyBoard(): Board ={ + val copy = new Board() + copy.player1houses = player1houses.clone() + copy.player2houses = player2houses.clone() + copy + + } + + def printBoard(): Unit ={ + println(" Player 2 ") + println(" 6 5 4 3 2 1") + println("------------------------------------------------") + var counter = player2houses.size - 2 + print(" ") + while(counter >= 0){ + print(" ( " + player2houses(counter) + " ) ") + counter -= 1 + } + println() + counter = 0 + println("( "+player2houses(baseIndex)+" )" + " " +"( "+ player1houses(baseIndex)+" )" ) + print(" ") + while(counter < player1houses.size - 1){ + print(" ( " + player1houses(counter) + " ) ") + counter += 1 + } + println() + println("------------------------------------------------") + println(" 1 2 3 4 5 6") + println(" Player 1 ") + } + +} diff --git a/KalahaGame/src/game/GameServer.scala b/KalahaGame/src/game/GameServer.scala new file mode 100644 index 00000000..5044ce30 --- /dev/null +++ b/KalahaGame/src/game/GameServer.scala @@ -0,0 +1,159 @@ +package game + +import akka.actor.{Actor, ActorRef, Kill, PoisonPill, Props, actorRef2Scala} +import akka.pattern.* +import akka.util.Timeout +import game.Board +import game.GameServer.{MoveChoice, NextMove, StartGame} +import KalahaGame.system +import players.* +import Player.{MoveRequest, GameOver} + +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration.* +import scala.io.StdIn.readInt +import scala.util.{Failure, Random, Success} + + + +class GameServer(player1Mode: Int, player2Mode: Int) extends Actor{ + val board = new Board() + var currentPlayer = 1 + var nextMove = false + implicit private val timeout: Timeout = Timeout(30.seconds) + val player1 = { + if player1Mode == 1 then system.actorOf(Props(HumanPlayer(1)), "player1") + else system.actorOf(Props(Computer(1, board)), "player1") + } + + val player2 = { + if player2Mode == 1 then system.actorOf(Props(HumanPlayer(2)), "player2") + else system.actorOf(Props(Computer(2, board)), "player2") + } + + def receive = { + case StartGame => + printGameBoard() + requestMove() + case NextMove => requestMove() + } + + def requestMove(): Unit ={ + printTurnMessage() + val f = getCurrentPlayer() ? MoveRequest + f.onComplete{ + case Success(MoveChoice(pNum, choice)) => + if checkIfPossible(pNum, choice) then + makeMove(pNum, choice) + else + println("This move is not possible") + requestMove() + + case Failure(ex) => + printEndMessage() + changeCurrentPlayer() + walkoverForPlayer(currentPlayer) + } + } + + def makeMove(playerNum: Int, house: Int): Unit ={ + printPlayersChoiceMessage(house) + Thread.sleep(100) + nextMove = board.move(playerNum, house) + if gameFinished() then + printGameBoard() + printEndMessage() + printFinalScore() + endGame() + else + if nextMove then { + printNextMoveMessage() + printGameBoard() + }else{ + changeCurrentPlayer() + printGameBoard() + } + self ! NextMove + } + + def getRandomMove(playerNum: Int): Int = { + val rand = new Random() + var choice = rand.nextInt(5) + 1 + while(!checkIfPossible(playerNum, choice)){ + choice = rand.nextInt(5) + 1 + } + choice + } + + def checkIfPossible(playerNum: Int, house: Int): Boolean ={ + playerNum match{ + case 1 => board.player1houses(house - 1) > 0 + case 2 => board.player2houses(house - 1) > 0 + } + } + + def printTurnMessage(): Unit ={ + println(s"Player $currentPlayer your turn") + } + def printNextMoveMessage(): Unit ={ + println(s"Player $currentPlayer has next move") + } + def printPlayersChoiceMessage(choice: Int): Unit ={ + println(s"Player $currentPlayer choice: $choice") + } + + def getCurrentPlayer(): ActorRef ={ + if currentPlayer == 1 then player1 + else player2 + } + + def changeCurrentPlayer(): Unit ={ + if currentPlayer == 1 then currentPlayer = 2 + else currentPlayer = 1 + } + + def printGameBoard(): Unit = { + board.printBoard() + } + + def gameFinished(): Boolean = { + board.isFinished() + } + + def printEndMessage(): Unit ={ + println("\n-------------------GAME OVER--------------------\n") + } + + + def endGame(): Unit ={ + player1 ! GameOver + player2 ! GameOver + self ! PoisonPill + context.system.terminate() + System.exit(0) + } + + def printFinalScore(): Unit ={ + val (s1, s2): (Int, Int) = board.getFinalScore() + println(" Final score ") + println(s" Player 1: $s1 Player 2: $s2 ") + if s1 > s2 then + println(" Player 1 is the winner!") + else if s2 > s1 then + println(" Player 2 is the winner!") + else println("It's a draw!") + println() + } + + def walkoverForPlayer(playerNum: Int): Unit ={ + println(" Walkover!") + println(s" Player $playerNum wins!\n") + endGame() + } +} + +object GameServer{ + case class MoveChoice(val playerNum: Int, val choice: Int) + case class StartGame() + case class NextMove() +} diff --git a/KalahaGame/src/game/KalahaGame.scala b/KalahaGame/src/game/KalahaGame.scala new file mode 100644 index 00000000..93a8a15c --- /dev/null +++ b/KalahaGame/src/game/KalahaGame.scala @@ -0,0 +1,63 @@ +package game + +import akka.actor.{ActorSystem, Props} +import GameServer.StartGame + +import scala.io.StdIn.{readInt, readLine} + +object KalahaGame extends App { + + val system = ActorSystem("ServerSystem") + var p1choice = -1 + var p2choice = -1 + + println("Welcome to Kalaha Game! \n") + + println("Instructions:\n\n" + + "The Kalaha board has 6 small pits, called houses, on each side and a big pit, called store, at each end. \n" + + "The object of the game is to capture more seeds than your opponent.\n" + + "Start of the game:" + + "1. At the beginning of the game, six seeds are placed in each house.\n" + + "2. Each player controls the six houses and their seeds on the player's side of the board.\n" + + "3. The player's score is the number of seeds in the store to their right.\n" + + "4. Players take turns sowing their seeds. On a turn, the player removes all seeds from one of the houses under their control.\n" + + "Moving counter-clockwise, the player drops one seed in each house in turn, including the player's own store but not their opponent's.\n" + + "To choose their moves players have maximum of 30 seconds, after which the game will be terminated\n" + + "5. If the last sown seed lands in an empty house owned by the player, and the opposite house contains seeds,\n" + + "both the last seed and the opposite seeds are captured and placed into the player's store.\n" + + "6. If the last sown seed lands in the player's store, the player gets an additional move.\n" + + "End of game:\n" + + "If one of the players runs out of the time, the game finishes with WALKOVER.\n" + + "When one player no longer has any seeds in any of their houses, the game ends.\n" + + "The other player moves all remaining seeds to their store, and the player with the most seeds in their store wins.\n") + + println("Are you ready? Press ENTER\n") + readLine() + + println("Choose your players' modes: \n") + + while (p1choice != 1 && p1choice != 2) { + println("Choose Player 1: ") + println("1 - Human player 2 - Bot player") + try { + p1choice = readInt() + } catch { + case e => println("Invalid input, must be a number") + } + } + while (p2choice != 1 && p2choice != 2) { + println("Choose Player 2: ") + println("1 - Human player 2 - Bot player") + try { + p2choice = readInt() + } catch { + case e => println("Invalid input, must be a number") + } + } + + val server = system.actorOf(Props(new GameServer(p1choice, p2choice))) + + println("The game is beginning!\n") + + server ! StartGame +} diff --git a/KalahaGame/src/players/Computer.scala b/KalahaGame/src/players/Computer.scala new file mode 100644 index 00000000..b01b274d --- /dev/null +++ b/KalahaGame/src/players/Computer.scala @@ -0,0 +1,131 @@ +package players + + +import game.Board + +import scala.util.Random + +class Computer(playerNum: Int, val b: Board) extends Player(playerNum){ + + def board = b + def chooseMove(): Int = { + Thread.sleep(3000) + simulateMoves(b) + } + + def simulateMoves(board: Board): Int = { + var myHouses: Array[Int] = Array() + + if playerNum == 1 then { + myHouses = board.player1houses + } + else { + myHouses = board.player2houses + } + var movesToChoose = Array[Int]() + + var bestOption = -1 + var bestPoints = -100 + for (i <- 0 to myHouses.size - 2) { + if (myHouses(i) != 0) { + val points = getPointsFromMove(i + 1, board.copyBoard()) + if ((points._1 - points._2) >= bestPoints) then { + if((points._1 - points._2) == bestPoints){ + movesToChoose = i +: movesToChoose + }else{ + movesToChoose = Array(i) + bestOption = i + bestPoints = (points._1 - points._2) + } + } + } + } + val myChoice = new Random().between(0, movesToChoose.size) + movesToChoose(myChoice) + 1 + } + + def getPointsFromMove(house: Int, board: Board): (Int, Int) = { + var initialPoints = 0 + var myHouses: Array[Int] = Array() + var opponentsHouses: Array[Int] = Array() + var opponentsNum = 0 + + if playerNum == 1 then { + opponentsNum = 2 + myHouses = board.player1houses + opponentsHouses = board.player2houses + initialPoints = board.player1houses(board.baseIndex) + } + else { + opponentsNum = 1 + myHouses = board.player2houses + opponentsHouses = board.player1houses + initialPoints = board.player2houses(board.baseIndex) + } + + val cond = board.move(playerNum, house) + if (cond) then { + var bestOption = -1 + var bestPoints = -1 + var bestResult = (0, 0) + for (i <- 0 to myHouses.size - 2) { + if (myHouses(i) != 0) { + val result = getPointsFromMove(i + 1, board.copyBoard()) + if ((result._1 - result._2) > bestPoints) then { + bestResult = result + bestPoints = (result._1 - result._2) + } + } + } + (bestResult._1 + 1, bestResult._2) + } + else { + var pointsAfterMove = myHouses(board.baseIndex) + + var bestOption = -1 + var bestOpponentsPoints = -1 + for (i <- 0 to opponentsHouses.size - 2) { + if (opponentsHouses(i) != 0) { + val points = getOpponentsPointsFromMove(opponentsNum, i + 1, board.copyBoard()) + if (points > bestOpponentsPoints) then { + bestOption = i + bestOpponentsPoints = points + } + } + } + (pointsAfterMove - initialPoints, bestOpponentsPoints) + } + } + + def getOpponentsPointsFromMove(player: Int, house: Int, board: Board): Int = { + var initialPoints = 0 + var opponentsHouses: Array[Int] = Array() + + if player == 1 then { + opponentsHouses = board.player1houses + initialPoints = board.player1houses(board.baseIndex) + } + else { + opponentsHouses = board.player2houses + initialPoints = board.player2houses(board.baseIndex) + } + if (board.move(player, house)) then { + var bestOption = -1 + var bestOpponentsPoints = -1 + for (i <- 0 to opponentsHouses.size - 2) { + if (opponentsHouses(i) != 0) { + val points = getOpponentsPointsFromMove(player, i + 1, board.copyBoard()) + if (points > bestOpponentsPoints) then { + bestOption = i + bestOpponentsPoints = points + } + } + } + bestOpponentsPoints + 1 + } + else { + var pointsAfterMove = opponentsHouses(board.baseIndex) + pointsAfterMove - initialPoints + } + } +} diff --git a/KalahaGame/src/players/HumanPlayer.scala b/KalahaGame/src/players/HumanPlayer.scala new file mode 100644 index 00000000..d515eec8 --- /dev/null +++ b/KalahaGame/src/players/HumanPlayer.scala @@ -0,0 +1,27 @@ +package players + +import scala.io.StdIn.* +import scala.util.Random + +class HumanPlayer(override val playerNum: Int) extends Player(playerNum) { + + def chooseMove(): Int = { + var correctArg = false + var choice = -1 + while (!correctArg) { + println("Choose a house: ") + try { + choice = readInt() + if (choice < 1 || choice > 6) { + println("House number must between 1 and 6") + } else { + correctArg = true + } + } catch { + case e => println("Ivalid input format, must be integer number") + } + } + choice + } +} + diff --git a/KalahaGame/src/players/Player.scala b/KalahaGame/src/players/Player.scala new file mode 100644 index 00000000..ac116678 --- /dev/null +++ b/KalahaGame/src/players/Player.scala @@ -0,0 +1,21 @@ +package players + +import akka.actor.TypedActor.dispatcher +import akka.actor.{Actor, PoisonPill, actorRef2Scala} +import game.GameServer.MoveChoice +import players.Player.{GameOver, MoveRequest} + +object Player{ + case class MoveRequest() + case class GameOver() +} + +abstract class Player(val playerNum: Int) extends Actor{ + + def receive = { + case MoveRequest => sender() ! MoveChoice(playerNum, chooseMove()) + case GameOver => self ! PoisonPill + } + + def chooseMove(): Int +} diff --git a/README.md b/README.md new file mode 100644 index 00000000..c8c8b52e --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Kalaha Game +This is a console app written in Scala presenting electronic version of wooden board game Kalaha. +The game supports 3 modes: two human players, human player with computer and simulation of two computers playing.