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;
 }
}
asked Jun 23, 2022 at 11:56
\$\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)

answered Jun 28, 2022 at 9:57
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.