2
\$\begingroup\$

This is my first Scala program, so I'd appreciate any feedback but especially feedback specific to Scala, for example situations where I could have utilised a feature better or where I've done something unidiomatic.

import scala.collection.mutable.HashMap

object Main extends App {
  // Type alias to shorten the Teams enum
  type Team = Teams.Value;

  // Represents a position on the board
  type Coord = (Int, Int)

  // The number of tiles in a row or column of the board
  val BOARD_SIZE = 3;

  // Driver method call to start game loop
  Game.runGame();

  object Game {
    // The team that is due to place an item on the board
    private var teamToMove = Teams.Crosses;

    // True if game is running i.e the game hasn't ended yet due to a win or stalemate
    private var isGameRunning = true;

    // The board being played on by both teams
    private var board = new Board();

    // The driver method that runs the game loop
    def runGame(): Unit = {
      while (isGameRunning) {
        UI.displayBoard(board);

        UI.getUserMove(teamToMove) match {
          case None             => UI.displayInvalidMoveMessage();
          case Some((row, col)) => executeMoveAt(row, col)
        }
      }

      UI.displayBoard(board);
    }

    // Given a move, check if move is valid and apply it to board if it is
    def executeMoveAt(row: Int, col: Int) = {
      if (
        !board.isWithinBounds(row, col) || board.tileAt(row, col) != Teams.None
      ) {
        UI.displayInvalidMoveMessage();
      } else {
        board.setTileAt(row, col, teamToMove);
        handleCaseWhereMoveIsWinner(row, col);
        handleStalemateCase();
        switchTeamToMove();
      }
    }

    def handleCaseWhereMoveIsWinner(row: Int, col: Int): Unit = {
      if (board.isWinner((row, col), teamToMove)) {
        UI.displayWinnerMessage(teamToMove);
        isGameRunning = false;
      }
    }

    def handleStalemateCase(): Unit = {
      if (board.isStalemate()) {
        UI.displayStalemateMessage();
        isGameRunning = false;
      }
    }

    def switchTeamToMove(): Unit = {
      if (teamToMove == Teams.Crosses) {
        teamToMove = Teams.Noughts;
      } else {
        teamToMove = Teams.Crosses;
      }
    }

  }

  class Board {
    // A type alias for the data structure used to represent the board internally
    private type InternalBoard = Array[Array[Team]];

    // The data structure used to represent the board internally
    private var internalBoard = genEmptyInternalBoard();

    // Get the value of the tile at some coordinate
    def tileAt(row: Int, col: Int): Team = {
      return internalBoard(row)(col);
    }

    // Set the tile at some coordinate a given value
    def setTileAt(row: Int, col: Int, team: Team): Unit = {
      internalBoard(row)(col) = team;
    }

    // True if a coordinate is within the bounds of the board
    def isWithinBounds(row: Int, col: Int): Boolean = {
      return row >= 0 && row < BOARD_SIZE && col >= 0 && col < BOARD_SIZE;
    }

    def isStalemate(): Boolean = {
      var isStalemate = true;

      for (row <- 0 to BOARD_SIZE - 1) {
        for (col <- 0 to BOARD_SIZE - 1) {
          if (tileAt(row, col) == Teams.None) {
            isStalemate = false;
          }
        }
      }

      return isStalemate;
    }

    def isWinner(coord: Coord, team: Team): Boolean = {
      return isDiagonalWinner(team) || isSidewaysWinner(coord, team);
    }

    // True if a move by some team has resulted in them winning through a vertical / horizontal N in a row
    def isSidewaysWinner(coord: Coord, team: Team): Boolean = {
      val (row, col) = coord;
      var columnAdjacencyCount = 0;
      var rowAdjacencyCount = 0;

      for (i <- 0 to BOARD_SIZE - 1) {
        if (tileAt(row, i) == team)
          columnAdjacencyCount += 1;
        if (tileAt(i, col) == team) {
          rowAdjacencyCount += 1;
        }
      }

      return rowAdjacencyCount == BOARD_SIZE || columnAdjacencyCount == BOARD_SIZE;
    }

    // True if a move by some team has resulted in them winning through a diagonal N in a row
    def isDiagonalWinner(team: Team): Boolean = {
      var posGradient = true;
      var negGradient = true;

      for (i <- 1 to BOARD_SIZE - 1) {
        if (hasNoPosGradientForIndex(i)) {
          posGradient = false;
        }

        if (hasNoNegGradientForIndex(i)) {
          negGradient = false;
        }
      }

      return posGradient || negGradient;
    }

    private def hasNoPosGradientForIndex(i: Int): Boolean = {
      return tileAt(i, i) != tileAt(i - 1, i - 1) || tileAt(
        i - 1,
        i - 1
      ) == Teams.None
    }

    private def hasNoNegGradientForIndex(i: Int): Boolean = {
      return tileAt(BOARD_SIZE - 1 - i, i) != tileAt(
        BOARD_SIZE - i,
        i - 1
      ) || tileAt(BOARD_SIZE - i, i - 1) == Teams.None
    }

    private def genEmptyInternalBoard(): InternalBoard = {
      val internalBoard = Array.ofDim[Team](BOARD_SIZE, BOARD_SIZE);

      for (row <- 0 to BOARD_SIZE - 1) {
        for (col <- 0 to BOARD_SIZE - 1) {
          internalBoard(row)(col) = Teams.None;
        }
      }

      return internalBoard;
    }
  }

  object UI {
    private val teamToDisplayStr =
      HashMap(Teams.Crosses -> "X", Teams.Noughts -> "O", Teams.None -> " ")

    def displayBoard(board: Board): Unit = {
      for (row <- 0 to BOARD_SIZE - 1) {
        print(" ")
        for (col <- 0 to BOARD_SIZE - 1) {
          print(teamToDisplayStr.get(board.tileAt(row, col)).get)

          if (col < BOARD_SIZE - 1) {
            print(" | ")
          }
        }

        if (row < BOARD_SIZE - 1) {
          println(" ")
          println("   |   |")
          println("---|---|---")
          println("   |   |")
        }
      }

      println("   |   |")
      println();
    }

    def getUserMove(teamToMove: Team): Option[Coord] = {
      val teamToMoveStr = teamToDisplayStr.get(teamToMove).get;

      println(
        s"$teamToMoveStr to move! Select where you want to place in coordinate format"
      )
      println(
        "For example, \"(1, 1)\" would make a move at the top left corner"
      )

      return parseUserInputAsCoord(scala.io.StdIn.readLine());
    }

    def displayInvalidMoveMessage(): Unit = {
      println("Move is invalid!");
    }

    def displayWinnerMessage(team: Team): Unit = {
      val winningTeam = teamToDisplayStr.get(team).get;

      println(s"$winningTeam has won!");
    }

    def displayStalemateMessage(): Unit = {
      println("Game has ended in a stalemate!");
    }

    private def parseUserInputAsCoord(coordAsStr: String): Option[Coord] = {
      val coordAsArray = coordAsStr
        .filter(char => char.isDigit || char == ',')
        .split(",");

      coordAsArray match {
        case Array(col, row) =>
          return Some((row.toInt - 1, col.toInt - 1));
        case _ =>
          return None;
      }
    }
  }

  object Teams extends Enumeration {
    val Noughts, Crosses, None = Value;
  }
}

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

I've learned a fair amount of Scala conventions since posting this, so I'll leave a few comments on my own code for anyone who comes across this.

The way the Map is used can be improved. For the purpose of setting up a correspondence between enums and how they're displayed on the console, a mutable map isn't needed. An immutable map would have sufficed while also providing the safety of knowing it can't be accidentally modified later on. When getting values from the map it would be less code to call map(key) rather than calling map.get(key).get

The return keyword isn't needed in Scala. Scala blocks of code (i.e anything in between a pair of curly brackets {}) automatically return what the last expression evaluates to, so simply stating the variable is enough.

for (i <- 0 to BOARD_SIZE - 1) can be rewritten as for (i <- 0 until BOARD_SIZE)

New contributor
L68967gy is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.