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;
}
}