diff --git a/build.gradle.kts b/build.gradle.kts index d48ee9b..908afc7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -27,12 +27,17 @@ tasks.named("test") { useJUnitPlatform() } +tasks.register("Assignment3") { + classpath = sourceSets.main.get().runtimeClasspath + main = "com.anthonycicchetti.cs5004.assignment3.Main" +} + tasks.register("Assignment4") { classpath = sourceSets.main.get().runtimeClasspath main = "com.anthonycicchetti.cs5004.assignment4.Main" } -tasks.register("Assignment3") { +tasks.register("Assignment5") { classpath = sourceSets.main.get().runtimeClasspath - main = "com.anthonycicchetti.cs5004.assignment3.Main" -} \ No newline at end of file + main = "com.anthonycicchetti.cs5004.assignment5.Main" +} diff --git a/src/main/kotlin/com/anthonycicchetti/cs5004/assignment5/Board.kt b/src/main/kotlin/com/anthonycicchetti/cs5004/assignment5/Board.kt new file mode 100644 index 0000000..5325fb0 --- /dev/null +++ b/src/main/kotlin/com/anthonycicchetti/cs5004/assignment5/Board.kt @@ -0,0 +1,130 @@ +package com.anthonycicchetti.cs5004.assignment5 + +import com.anthonycicchetti.cs5004.assignment5.model.Tile +import java.lang.IllegalArgumentException +import kotlin.math.absoluteValue + +class Board() : MarbleSolitaireModel { + val backingState: MutableList + + private fun invalidBoardTile(row: Int, column: Int): Boolean { + return ((row <= 1 || row >= 5) + && (column <= 1 || column >= 5)) + } + + constructor(emptyRow: Int, emptyColumn: Int) : this() { + if (!invalidBoardTile(emptyRow, emptyColumn)) { + this.backingState.remove(Tile(3, 3, false)) + this.backingState.add(Tile(3, 3, true)) + this.backingState.remove(Tile(emptyRow, emptyColumn, true)) + this.backingState.add(Tile(emptyRow, emptyColumn, false)) + } else { + throw IllegalArgumentException("Invalid empty cell position ($emptyRow, $emptyColumn)") + } + } + + init { + val eventualBackingState = mutableListOf() + for (column in 0..6) { + for (row in 0..6) { + if (invalidBoardTile(row, column)) { + eventualBackingState.add(Tile(row, column, null)) + } else { + eventualBackingState.add(Tile(row, column, true)) + } + } + } + if (eventualBackingState.contains(Tile(3, 3, true))) { + eventualBackingState.remove(Tile(3, 3, true)) + eventualBackingState.add(Tile(3, 3, false)) + } + backingState = eventualBackingState.sorted().toMutableList() + } + + private fun tileIsOccupied(row: Int, column: Int): Boolean { + return backingState.contains(Tile(row, column, true)) + } + + private fun getTileAt(row: Int, column: Int): Tile { + return backingState.filter { it.row == row && it.column == column }[0] + } + + private fun validMove(fromRow: Int, fromCol: Int, toRow: Int, toCol: Int): Boolean { + if ((fromRow - toRow).absoluteValue != 2 && (fromCol - toCol == 0) + || (fromCol - toCol).absoluteValue != 2 && (fromRow - toRow == 0)) { + return false + } + if (invalidBoardTile(fromRow, fromCol) || invalidBoardTile(toRow, toCol)) { + return false + } + val avgRow = (fromRow + toRow) / 2 + val avgCol = (fromCol + toCol) / 2 + if (tileIsOccupied(avgRow, avgCol) + && !tileIsOccupied(toRow, toCol) + && tileIsOccupied(fromRow, fromCol)){ + return true + } + return false + } + + override fun move(fromRow: Int, fromCol: Int, toRow: Int, toCol: Int) { + if (validMove(fromRow, fromCol, toRow, toCol)) { + val avgRow = (fromRow + toRow) / 2 + val avgCol = (fromCol + toCol) / 2 + + backingState.remove(Tile(fromRow, fromCol, true)) + backingState.add(Tile(fromRow, fromCol, false)) + backingState.remove(Tile(avgRow, avgCol, true)) + backingState.add(Tile(avgRow, avgCol, false)) + backingState.remove(Tile(toRow, toCol, false)) + backingState.add(Tile(toRow, toCol, true)) + } + + backingState.sort() + } + + override fun isGameOver(): Boolean { + if (getScore() == 1) { + return true + } else { + for (tile in backingState) { + val localRow = tile.row + val localCol = tile.column + val plusRow = localRow + 2 + val plusCol = tile.column + 2 + val minusRow = localRow - 2 + val minusCol = tile.column - 2 + if (validMove(localRow, localCol, plusRow, localCol) + || validMove(localRow, localCol, localRow, plusCol) + || validMove(localRow, localCol, minusRow, localCol) + || validMove(localRow, localCol, localRow, minusCol)) { + return false + } + } + } + return true + } + + override fun getGameState(): String { + val sb = StringBuilder() + var rowBuilder = StringBuilder() + + for (row in 0..6) { + backingState.filter { it.row == row }.sorted().forEach { + rowBuilder.append( + when (it.occupied) { + true -> "O" + false -> "X" + null -> " " + } + ).append(" ") + } + sb.append(rowBuilder.toString()).append("\n") + rowBuilder = StringBuilder() + } + return sb.toString().trimEnd('\n') + } + + override fun getScore(): Int = backingState.count { it.occupied ?: false } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/anthonycicchetti/cs5004/assignment5/Main.kt b/src/main/kotlin/com/anthonycicchetti/cs5004/assignment5/Main.kt new file mode 100644 index 0000000..45a75b0 --- /dev/null +++ b/src/main/kotlin/com/anthonycicchetti/cs5004/assignment5/Main.kt @@ -0,0 +1,17 @@ +package com.anthonycicchetti.cs5004.assignment5 + +object Main { + @JvmStatic + fun main(args: Array) { + val boardOne = Board() + println(boardOne.getGameState()) + + val boardTwo = Board(2, 3) + println(boardTwo.getGameState()) + + val board = Board() + board.move(3,1, 3,3) + + println(board.getGameState()) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/anthonycicchetti/cs5004/assignment5/MarbleSolitaireModel.kt b/src/main/kotlin/com/anthonycicchetti/cs5004/assignment5/MarbleSolitaireModel.kt new file mode 100644 index 0000000..a2a1596 --- /dev/null +++ b/src/main/kotlin/com/anthonycicchetti/cs5004/assignment5/MarbleSolitaireModel.kt @@ -0,0 +1,46 @@ +package com.anthonycicchetti.cs5004.assignment5 + +/** + * This interface represents the operations offered by the marble solitaire + * model. One object of the model represents one game of marble solitaire + */ +public interface MarbleSolitaireModel { + /** + * Move a single marble from a given position to another given position. + * A move is valid only if the from and to positions are valid. Specific + * implementations may place additional constraints on the validity of a move. + * @param fromRow the row number of the position to be moved from + * (starts at 0) + * @param fromCol the column number of the position to be moved from + * (starts at 0) + * @param toRow the row number of the position to be moved to + * (starts at 0) + * @param toCol the column number of the position to be moved to + * (starts at 0) + * @throws IllegalArgumentException if the move is not possible + */ + fun move(fromRow: Int, fromCol: Int, toRow: Int, toCol: Int): Unit + + /** + * Determine and return if the game is over or not. A game is over if no + * more moves can be made. + * @return true if the game is over, false otherwise + */ + fun isGameOver(): Boolean + + /** + * Return a string that represents the current state of the board. The + * string should have one line per row of the game board. Each slot on the + * game board is a single character (O, X or space for a marble, empty and + * invalid position respectively). Slots in a row should be separated by a + * space. Each row has no space before the first slot and after the last slot. + * @return the game state as a string + */ + fun getGameState(): String; + + /** + * Return the number of marbles currently on the board. + * @return the number of marbles currently on the board + */ + fun getScore(): Int; +} \ No newline at end of file diff --git a/src/main/kotlin/com/anthonycicchetti/cs5004/assignment5/model/Tile.kt b/src/main/kotlin/com/anthonycicchetti/cs5004/assignment5/model/Tile.kt new file mode 100644 index 0000000..4d3f631 --- /dev/null +++ b/src/main/kotlin/com/anthonycicchetti/cs5004/assignment5/model/Tile.kt @@ -0,0 +1,17 @@ +package com.anthonycicchetti.cs5004.assignment5.model + +data class Tile(val row: Int, val column: Int, val occupied: Boolean?): Comparable { + override fun compareTo(other: Tile): Int { + return when { + this.row < other.row -> return -1 + this.row == other.row -> when { + this.column < other.column -> return -1 + this.column == other.column -> return 0 + this.column > other.column -> return 1 + else -> 0 + } + this.row > other.row -> return 1 + else -> 0 + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/anthonycicchetti/cs5004/assignment5/MarbleSolitaireModelTest.kt b/src/test/kotlin/com/anthonycicchetti/cs5004/assignment5/MarbleSolitaireModelTest.kt new file mode 100644 index 0000000..a2e836e --- /dev/null +++ b/src/test/kotlin/com/anthonycicchetti/cs5004/assignment5/MarbleSolitaireModelTest.kt @@ -0,0 +1,108 @@ +package com.anthonycicchetti.cs5004.assignment5 + +import com.anthonycicchetti.cs5004.assignment5.Board +import com.anthonycicchetti.cs5004.assignment5.model.Tile +import org.junit.jupiter.api.Test + +import org.junit.jupiter.api.Assertions.* + +internal class BoardTest { + @Test + fun `A Valid Board is Created By Default`() { + val board = Board() + val expected = + """ O O O + O O O +O O O O O O O +O O O X O O O +O O O O O O O + O O O + O O O """ + + assertEquals(expected, board.getGameState()) + } + + @Test + fun `A Valid Board is Created With a Different Empty Hole`() { + val board = Board(2, 3) + + assertFalse(board.backingState.contains(Tile(2, 3, true))) + } + + @Test + fun `A Different-Holed Board is Printed Correctly`() { + val board = Board(2,3) + val expected = +""" O O O + O O O +O O O X O O O +O O O O O O O +O O O O O O O + O O O + O O O """ + + assertEquals(expected, board.getGameState()) + } + + @Test + fun `Board is printed correctly after a move`() { + val board = Board() + val movedBoard = +""" O O O + O O O +O O O O O O O +O X X O O O O +O O O O O O O + O O O + O O O """ + + board.move(3,1,3,3) + assertEquals(movedBoard, board.getGameState()) + } + + @Test + fun `Score is counted correctly on new board`() { + val board = Board() + + assertEquals(32, board.getScore()) + } + + @Test + fun `Score is counted correct on new non-default board`() { + val board = Board(1,4) + + assertEquals(32, board.getScore()) + } + + @Test + fun `Score is counted correctly on board with missing marbles`() { + val board = Board() + + board.move(3, 1, 3, 3) + assertEquals(31, board.getScore()) + } + + @Test + fun `Game is not over when game is started`() { + val board = Board() + + assertFalse(board.isGameOver()) + } + + @Test + fun `Game is not over when game is started with a nonstandard board`() { + val board = Board(2,3) + + assertFalse(board.isGameOver()) + } + + @Test + fun `Game is not over after one move`() { + val board = Board() + + board.move(3, 1, 3, 3) + assertFalse(board.isGameOver()) + } + + +}