Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion plugin/src/shared/sc/plugin2021/Field.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@ import sc.api.plugins.IField
/**
* Beschreibt die Farbe eines bestimmten Felds.
* @property coordinates die Position des Felds, als [Coordinates]
* @property content Die Farbe des Felds, als [FieldContent]
* @property content Die Farbe des Felds, als [FieldContent] oder [Color]
*/
@XStreamAlias(value = "field")
class Field(val coordinates: Coordinates, val content: FieldContent): IField {

constructor(coordinates: Coordinates, content: Color): this(coordinates, +content) {}

val isEmpty = content == FieldContent.EMPTY

override fun toString(): String = "'$content $coordinates'"

override fun equals(other: Any?): Boolean {
Expand Down
172 changes: 96 additions & 76 deletions plugin/src/shared/sc/plugin2021/util/GameRuleLogic.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ object GameRuleLogic {
@JvmStatic
fun performMove(gameState: GameState, move: Move) {
if (Constants.VALIDATE_MOVE)
validateMoveColor(gameState, move)
validateMoveColor(gameState, move, true)

when (move) {
is SkipMove -> performSkipMove(gameState)
Expand All @@ -53,31 +53,51 @@ object GameRuleLogic {
gameState.lastMove = move
}

/** Check if the given [move] has the right [Color]. */
/** Prüfe, ob die Farbe des gegebenen [Move]s der aktiven Farbe des [GameState]s entspricht. */
@JvmStatic
private fun validateMoveColor(gameState: GameState, move: Move) {
fun validateMoveColor(gameState: GameState, move: Move, throws: Boolean = false): MoveMistake? {
if (move.color != gameState.currentColor)
throw InvalidMoveException("Expected move from ${gameState.currentColor}", move)
if (throws) {
throw InvalidMoveException("Expected move from ${gameState.currentColor}", move)
} else {
return MoveMistake.WRONG_COLOR
}
return null
}

/** Check if the given [move] is able to be performed for the given [gameState]. */
/**
* Prüfe, ob der gegebene [SetMove] gesetzt werden könnte.
* @param throws wenn true, wirft die Methode bei invaliden Zügen einen Fehler.
*
* @return gibt einen [MoveMistake] zurück, wenn der Zug nicht valide wahr, ansonsten null
*/
@JvmStatic
private fun validateSetMove(gameState: GameState, move: SetMove) {
fun validateSetMove(gameState: GameState, move: SetMove, throws: Boolean = false): MoveMistake? {
// Check whether the color's move is currently active
validateMoveColor(gameState, move)
return validateMoveColor(gameState, move, throws) ?:
// Check whether the shape is valid
validateShape(gameState, move.piece.kind, move.color)
validateShape(gameState, move.piece.kind, move.color, throws) ?:
// Check whether the piece can be placed
validateSetMove(gameState.board, move)
validateSetMove(gameState.board, move, throws) ?:

if (isFirstMove(gameState)) {
// Check if it is placed correctly in a corner
if (move.piece.coordinates.none { isOnCorner(it)})
throw InvalidMoveException("The Piece isn't located in a corner", move)
if (throws) {
throw InvalidMoveException("The Piece isn't located in a corner", move)
} else {
MoveMistake.NOT_IN_CORNER
}
else null
} else {
// Check if the piece is connected to at least one tile of same color by corner
if (move.piece.coordinates.none { cornersOnColor(gameState.board, it, move.color) })
throw InvalidMoveException("${move.piece} shares no corner with another piece of same color", move)
if (move.piece.coordinates.none { cornersOnColor(gameState.board, Field(it, move.color)) })
if (throws) {
throw InvalidMoveException("${move.piece} shares no corner with another piece of same color", move)
} else {
MoveMistake.NO_SHARED_CORNER
}
else null
}
}

Expand All @@ -87,7 +107,7 @@ object GameRuleLogic {
validateSetMove(gameState, move)

if (Constants.VALIDATE_MOVE)
validateSetMove(gameState, move)
validateSetMove(gameState, move, true)

performSetMove(gameState.board, move)
gameState.undeployedPieceShapes(move.color).remove(move.piece.kind)
Expand All @@ -100,50 +120,75 @@ object GameRuleLogic {
gameState.tryAdvance()
}

/** Validate the [PieceShape] of a [SetMove] depending on the current [GameState]. */
/**
* Prüfe, ob der gegebene Spielstein auf dem Spielfeld platziert werden könnte.
* Fehler treten auf, wenn
* - im ersten Zug nicht der vorgegebene Stein
* - in nachfolgenden Zügen bereits gesetzte Steine
* gesetzt werden würde(n).
*/
@JvmStatic
private fun validateShape(gameState: GameState, shape: PieceShape, color: Color = gameState.currentColor) {
fun validateShape(gameState: GameState, shape: PieceShape, color: Color = gameState.currentColor, throws: Boolean = false): MoveMistake? {
if (isFirstMove(gameState)) {
if (shape != gameState.startPiece)
throw InvalidMoveException("$shape is not the requested first shape, ${gameState.startPiece}")
if (throws) {
throw InvalidMoveException("$shape is not the requested first shape, ${gameState.startPiece}")
} else {
return MoveMistake.WRONG_SHAPE
}
} else {
if (!gameState.undeployedPieceShapes(color).contains(shape))
throw InvalidMoveException("Piece $shape has already been placed before")
if (throws) {
throw InvalidMoveException("Piece $shape has already been placed before")
} else {
return MoveMistake.DUPLICATE_SHAPE
}
}
return null
}

/**
* Prüft, ob der gegebene [Move] zulässig ist.
* Prüfe, ob der gegebene [Move] zulässig ist.
* @param gameState der aktuelle Spielstand
* @param move der zu überprüfende Zug
*
* @return ob der Zug zulässig ist
*/
@JvmStatic
fun isValidSetMove(gameState: GameState, move: SetMove) =
try {
validateSetMove(gameState, move)
true
} catch (e: InvalidMoveException) {
false
}
validateSetMove(gameState, move) == null

/** Validate a [SetMove] on a [board]. */
/** Prüfe, ob der gegebene [SetMove] auf dem [Board] platziert werden kann. */
@JvmStatic
private fun validateSetMove(board: Board, move: SetMove) {
fun validateSetMove(board: Board, move: SetMove, throws: Boolean = false): MoveMistake? {
// throw IndexOutOfBounds if the initial position only is out of bounds
board[move.piece.position]
move.piece.coordinates.forEach {
try {
board[it]
} catch (e: ArrayIndexOutOfBoundsException) {
throw InvalidMoveException("Field $it is out of bounds", move)
if (throws) {
throw InvalidMoveException("Field $it is out of bounds", move)
} else {
return MoveMistake.OUT_OF_BOUNDS
}
}
// Checks if a part of the piece is obstructed
if (board.isObstructed(it))
throw InvalidMoveException("Field $it already belongs to ${board[it].content}", move)
if (throws) {
throw InvalidMoveException("Field $it already belongs to ${board[it].content}", move)
} else {
return MoveMistake.OBSTRUCTED
}
// Checks if a part of the piece would border on another piece of same color
if (bordersOnColor(board, it, move.color))
throw InvalidMoveException("Field $it already borders on ${move.color}", move)
if (bordersOnColor(board, Field(it, move.color)))
if (throws) {
throw InvalidMoveException("Field $it already borders on ${move.color}", move)
} else {
return MoveMistake.TOUCHES_SAME_COLOR
}
}
return null
}

/** Place a Piece on the given [board] according to [move]. */
Expand All @@ -154,42 +199,52 @@ object GameRuleLogic {
}
}

@JvmStatic
fun validateSkipMove(gameState: GameState, throws: Boolean = false): MoveMistake? {
if (isFirstMove(gameState))
if (throws) {
throw InvalidMoveException("Can't Skip on first round", SkipMove(gameState.currentColor))
} else {
return MoveMistake.SKIP_FIRST_TURN
}
return null
}

/** Skip a turn. */
@JvmStatic
private fun performSkipMove(gameState: GameState) {
if (!gameState.tryAdvance())
logger.error("Couldn't proceed to next turn!")
if (isFirstMove(gameState))
throw InvalidMoveException("Can't Skip on first round", SkipMove(gameState.currentColor))
validateSkipMove(gameState, true)
}

/** Check if the given [position] already borders on another piece of same [color]. */
/** Prüfe, ob das gegebene [Field] bereits an eins mit gleicher Farbe angrenzt. */
@JvmStatic
private fun bordersOnColor(board: Board, position: Coordinates, color: Color): Boolean = listOf(
fun bordersOnColor(board: Board, field: Field): Boolean = listOf(
Vector(1, 0),
Vector(0, 1),
Vector(-1, 0),
Vector(0, -1)).any {
try {
board[position + it].content == +color
board[field.coordinates + it].content == field.content && !field.isEmpty
} catch (e: ArrayIndexOutOfBoundsException) { false }
}

/** Return true if the given [Coordinates] touch a corner of a field of same color. */
/** Prüfe, ob das gegebene Feld an die Ecke eines Feldes gleicher Farbe angrenzt. */
@JvmStatic
private fun cornersOnColor(board: Board, position: Coordinates, color: Color): Boolean = listOf(
fun cornersOnColor(board: Board, field: Field): Boolean = listOf(
Vector(1, 1),
Vector(1, -1),
Vector(-1, -1),
Vector(-1, 1)).any {
try {
board[position + it].content == +color
board[field.coordinates + it].content == field.content && !field.isEmpty
} catch (e: ArrayIndexOutOfBoundsException) { false }
}

/** Return true if the given [Coordinates] are a corner. */
/** Prüfe, ob die gegebene Position eine Ecke des Spielfelds ist. */
@JvmStatic
private fun isOnCorner(position: Coordinates): Boolean =
fun isOnCorner(position: Coordinates): Boolean =
Corner.values().any { it.position == position }

/** Gib zurück, ob sich der [GameState] noch in der ersten Runde befindet. */
Expand All @@ -209,41 +264,6 @@ object GameRuleLogic {
fun getPossibleMoves(gameState: GameState) =
streamPossibleMoves(gameState).toSet()

/** Return a list of all possible SetMoves, regardless of whether it's the first round. */
@JvmStatic
private fun getAllPossibleMoves(gameState: GameState) =
streamAllPossibleMoves(gameState).toSet()

/** Return a list of possible SetMoves if it's the first round. */
@JvmStatic
private fun getPossibleStartMoves(gameState: GameState) =
streamPossibleStartMoves(gameState).toSet()

/**
* Return a list of all moves, impossible or not.
* There's no real usage, except maybe for cases where no Move validation happens
* if `Constants.VALIDATE_MOVE` is false, then this function should return the same
* Set as `::getPossibleMoves`
*/
@JvmStatic
private fun getAllMoves(): Set<SetMove> {
val moves = mutableSetOf<SetMove>()
for (color in Color.values()) {
for (shape in PieceShape.values()) {
for (rotation in Rotation.values()) {
for (flip in listOf(false, true)) {
for (y in 0 until Constants.BOARD_SIZE) {
for (x in 0 until Constants.BOARD_SIZE) {
moves.add(SetMove(Piece(color, shape, rotation, flip, Coordinates(x, y))))
}
}
}
}
}
}
return moves
}

/** Entferne alle Farben, die keine Steine mehr auf dem Feld platzieren können. */
@JvmStatic
fun removeInvalidColors(gameState: GameState) {
Expand Down Expand Up @@ -286,4 +306,4 @@ object GameRuleLogic {
}
}
}.filter { isValidSetMove(gameState, it) }
}
}
20 changes: 20 additions & 0 deletions plugin/src/shared/sc/plugin2021/util/MoveException.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package sc.plugin2021.util

/**
* Wird optional bei Validierung von Zügen zurückgegeben, falls ein Zug nicht valide ist.
* MoveMistakes entstehen bei Zügen, die theoretisch möglich sein könnten,
* es aber bei dem jeweiligen Spielstand nicht sind.
*/
enum class MoveMistake(val msg: String = "") {
WRONG_COLOR("Die Farbe des Zuges ist nicht an der Reihe"),
NOT_IN_CORNER("Der erste Zug muss auf eine freie Ecke gesetzt werden"),
NO_SHARED_CORNER("Alle Teile müssen ein vorheriges Teil gleicher Farbe über mindestens eine Ecke berühren"),
WRONG_SHAPE("Der erste Zug muss den festgelegten Spielstein setzen"),
SKIP_FIRST_TURN("Der erste Zug muss einen Stein setzen"),
DUPLICATE_SHAPE("Der gewählte Stein wurde bereits gesetzt"),
OUT_OF_BOUNDS("Der Spielstein passt nicht vollständig auf das Spielfeld"),
OBSTRUCTED("Der Spielstein würde eine andere Farbe überlagern"),
TOUCHES_SAME_COLOR("Der Spielstein berührt ein Feld gleicher Farbe");

override fun toString(): String = msg
}