Skip to content

Commit 845f90b

Browse files
authored
feat(plugin): add getPossibleMoves function (#276)
IMPORTANT: * The tests are incomplete yet. Technically, if the move validation tests are foul proof, then everything works already but, ideally, we should write more, proper tests * This implementation requires quite a bit of overhead on initialisation but allows quicker calculation of possible moves thereafter
1 parent e4c03fd commit 845f90b

File tree

8 files changed

+280
-43
lines changed

8 files changed

+280
-43
lines changed

plugin/src/shared/sc/plugin2021/Board.kt

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package sc.plugin2021
33
import com.thoughtworks.xstream.annotations.XStreamAlias
44
import sc.api.plugins.IBoard
55
import sc.plugin2021.util.Constants
6-
import sc.plugin2021.Field
76

87
@XStreamAlias(value = "board")
98
class Board(
@@ -51,3 +50,27 @@ class Board(
5150
}
5251
}
5352
}
53+
54+
/** The four corners on the Board, used to calculate the position of a piece in a corner. */
55+
enum class Corner(val position: Coordinates) {
56+
UPPER_LEFT(Coordinates(0, 0)) {
57+
override fun align(area: Vector): Coordinates = position
58+
},
59+
UPPER_RIGHT(Coordinates(Constants.BOARD_SIZE - 1, 0)) {
60+
override fun align(area: Vector): Coordinates = Coordinates(position.x - area.dx, position.y)
61+
},
62+
LOWER_RIGHT(Coordinates(Constants.BOARD_SIZE - 1, Constants.BOARD_SIZE - 1)) {
63+
override fun align(area: Vector): Coordinates = position - area
64+
},
65+
LOWER_LEFT(Coordinates(0, Constants.BOARD_SIZE - 1)) {
66+
override fun align(area: Vector): Coordinates = Coordinates(position.x, position.y - area.dy)
67+
};
68+
69+
/** Returns the position a piece of given area has to be placed at to lie in the respective corner. */
70+
abstract fun align(area: Vector): Coordinates
71+
72+
companion object {
73+
/** Returns a set of the positions of all corners. */
74+
fun asSet(): Set<Coordinates> = values().map { it.position }.toSet()
75+
}
76+
}

plugin/src/shared/sc/plugin2021/Coordinates.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package sc.plugin2021
22

33
import com.thoughtworks.xstream.annotations.XStreamAlias
44
import com.thoughtworks.xstream.annotations.XStreamAsAttribute
5+
import kotlin.math.min
56

67
@XStreamAlias(value = "coordinates")
78
data class Coordinates(
@@ -34,5 +35,9 @@ data class Vector(
3435
operator fun times(scalar: Int): Vector {
3536
return Vector(scalar * dx, scalar * dy)
3637
}
38+
39+
operator fun compareTo(other: Vector): Int =
40+
min(other.dx - dx, other.dy - dy)
41+
3742
operator fun unaryPlus(): Coordinates = Coordinates(dx, dy)
3843
}

plugin/src/shared/sc/plugin2021/Piece.kt

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,6 @@ package sc.plugin2021
22

33
import com.thoughtworks.xstream.annotations.XStreamAlias
44
import com.thoughtworks.xstream.annotations.XStreamAsAttribute
5-
import com.thoughtworks.xstream.annotations.XStreamOmitField
6-
import sc.plugin2021.util.align
7-
import sc.plugin2021.util.flip
8-
import sc.plugin2021.util.print
9-
import sc.plugin2021.util.rotate
105

116
/** A Piece has a color, a position and a normalised shape. */
127
@XStreamAlias(value = "piece")
@@ -23,6 +18,15 @@ class Piece(@XStreamAsAttribute val color: Color = Color.BLUE,
2318
position: Coordinates = Coordinates.origin):
2419
this(color, PieceShape.shapes.getValue(kind), rotation, isFlipped, position)
2520

21+
constructor(color: Color = Color.BLUE,
22+
kind: PieceShape = PieceShape.MONO,
23+
shape: Set<Coordinates>,
24+
position: Coordinates = Coordinates.origin):
25+
this(color, kind, kind.variants.getValue(shape), position)
26+
27+
private constructor(color: Color, kind: PieceShape, transformation: Pair<Rotation, Boolean>, position: Coordinates):
28+
this(color, kind, transformation.first, transformation.second, position)
29+
2630
val shape: Set<Coordinates>
2731
get() = lazyShape()
2832

plugin/src/shared/sc/plugin2021/PieceShape.kt

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@ package sc.plugin2021
33
import com.thoughtworks.xstream.annotations.XStreamAlias
44
import com.thoughtworks.xstream.annotations.XStreamAsAttribute
55
import com.thoughtworks.xstream.annotations.XStreamOmitField
6-
import sc.plugin2021.util.Constants
7-
import sc.plugin2021.util.align
8-
import sc.plugin2021.util.rotate
9-
import sc.plugin2021.util.flip
6+
import sc.plugin2021.util.*
107
import kotlin.math.max
118

129
@XStreamAlias(value = "shape")
@@ -36,25 +33,44 @@ enum class PieceShape(coordinates: Set<Coordinates>) {
3633

3734
@XStreamAsAttribute
3835
val coordinates: Set<Coordinates> = coordinates.align()
36+
3937
@XStreamAsAttribute
40-
val dimension: Vector
38+
val dimension: Vector = coordinates.area()
4139

4240
val asVectors: Set<Vector> by lazy {coordinates.map {it - Coordinates.origin}.toSet()}
41+
4342
@XStreamOmitField
4443
val size: Int = coordinates.size
4544

45+
/** All different variants. Boiler plate for faster calculation of possible moves. */
46+
val variants: Map<Set<Coordinates>, Pair<Rotation, Boolean>>
47+
48+
val transformations: Map<Pair<Rotation, Boolean>, Set<Coordinates>>
49+
4650
init {
47-
var dx = 0
48-
var dy = 0
49-
coordinates.forEach {
50-
dx = max(it.x, dx)
51-
dy = max(it.y, dy)
51+
val mapVariants = mutableMapOf<Set<Coordinates>, Pair<Rotation, Boolean>>()
52+
val mapTransformations = mutableMapOf<Pair<Rotation, Boolean>, Set<Coordinates>>()
53+
for (rotation in Rotation.values()) {
54+
for (flip in listOf(false, true)) {
55+
val shape = coordinates.rotate(rotation).flip(flip)
56+
if (mapVariants[shape] == null) mapVariants += shape to Pair(rotation, flip)
57+
mapTransformations += Pair(rotation, flip) to shape
58+
}
5259
}
53-
dimension = Vector(dx, dy)
60+
variants = mapVariants
61+
transformations = mapTransformations
5462
}
5563

56-
/** Applies all the given transformations. */
64+
/** Does the same thing as transform, providing the index operator. */
65+
operator fun get(rotation: Rotation, shouldFlip: Boolean): Set<Coordinates> =
66+
transform(rotation, shouldFlip)
67+
68+
/** Returns a shape with all transformations applied. */
5769
fun transform(rotation: Rotation, shouldFlip: Boolean): Set<Coordinates> =
70+
transformations[rotation to shouldFlip] ?: emptySet()
71+
72+
/** Applies all the given transformations manually instead of looking them up. */
73+
fun legacyTransform(rotation: Rotation, shouldFlip: Boolean): Set<Coordinates> =
5874
coordinates.rotate(rotation).flip(shouldFlip)
5975

6076
companion object {

plugin/src/shared/sc/plugin2021/util/GameRuleLogic.kt

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package sc.plugin2021.util
33
import org.slf4j.LoggerFactory
44
import sc.plugin2021.*
55
import sc.shared.InvalidMoveException
6-
import kotlin.properties.Delegates
76

87
object GameRuleLogic {
98
val logger = LoggerFactory.getLogger(GameRuleLogic::class.java)
@@ -126,11 +125,8 @@ object GameRuleLogic {
126125
}
127126

128127
@JvmStatic
129-
fun isOnCorner(position: Coordinates): Boolean = listOf(
130-
Coordinates(0, 0),
131-
Coordinates(Constants.BOARD_SIZE - 1, 0),
132-
Coordinates(Constants.BOARD_SIZE - 1, Constants.BOARD_SIZE - 1),
133-
Coordinates(0, Constants.BOARD_SIZE - 1)).contains(position)
128+
fun isOnCorner(position: Coordinates): Boolean =
129+
Corner.asSet().contains(position)
134130

135131
/** Returns a random pentomino which is not the `x` one (Used to get a valid starting piece). */
136132
@JvmStatic
@@ -142,17 +138,57 @@ object GameRuleLogic {
142138
/** Returns a list of all possible SetMoves. */
143139
@JvmStatic
144140
fun getPossibleMoves(gameState: GameState): Set<SetMove> {
145-
if (gameState.round == 1) return getPossibleStartMoves(gameState)
141+
// TODO: Use appropriate move calculation here
142+
if (gameState.deployedPieces.getValue(gameState.currentColor).isEmpty()) return getPossibleStartMoves(gameState)
146143
val color = gameState.currentColor
147-
148-
return emptySet()
144+
145+
val moves = mutableSetOf<SetMove>()
146+
gameState.undeployedPieceShapes.getValue(color).map {
147+
val area = it.coordinates.area()
148+
for (y in 0 until Constants.BOARD_SIZE - area.dy)
149+
for (x in 0 until Constants.BOARD_SIZE - area.dx)
150+
for (variant in it.variants)
151+
moves += SetMove(Piece(color, it, variant.key, Coordinates(x, y)))
152+
}
153+
return moves
149154
}
150155

151156
/** Returns a list of possible SetMoves if it's the first round. */
152157
@JvmStatic
153158
fun getPossibleStartMoves(gameState: GameState): Set<SetMove> {
154159
val color = gameState.currentColor
155-
156-
return emptySet()
160+
val kind = gameState.startPiece
161+
val moves = mutableSetOf<SetMove>()
162+
for (variant in kind.variants) {
163+
for (corner in Corner.values()) {
164+
moves.add(SetMove(Piece(color, kind, variant.key, corner.align(variant.key.area()))))
165+
}
166+
}
167+
return moves.filterValidMoves(gameState)
168+
}
169+
170+
/**
171+
* Returns a list of all moves, impossible or not.
172+
* There's no real usage, except maybe for cases where no Move validation happens
173+
* if `Constants.VALIDATE_MOVE` is false, then this function should return the same
174+
* Set as `::getPossibleMoves`
175+
*/
176+
@JvmStatic
177+
fun getAllMoves(gameState: GameState): Set<SetMove> {
178+
val moves = mutableSetOf<SetMove>()
179+
for (color in Color.values()) {
180+
for (shape in PieceShape.values()) {
181+
for (rotation in Rotation.values()) {
182+
for (flip in listOf(false, true)) {
183+
for (y in 0 until Constants.BOARD_SIZE) {
184+
for (x in 0 until Constants.BOARD_SIZE) {
185+
moves.add(SetMove(Piece(color, shape, rotation, flip, Coordinates(x, y))))
186+
}
187+
}
188+
}
189+
}
190+
}
191+
}
192+
return moves
157193
}
158194
}

plugin/src/shared/sc/plugin2021/util/SetHelpers.kt

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package sc.plugin2021.util
22

33
import sc.plugin2021.*
4+
import sc.shared.InvalidMoveException
5+
import java.lang.IndexOutOfBoundsException
46

57
/**
68
* A Collection of methods callable on specific Sets or functions that take Sets as input.
@@ -54,13 +56,27 @@ fun Set<Coordinates>.align(): Set<Coordinates> {
5456
}.toSet()
5557
}
5658

59+
/** Returns the rectangular area the Set of Coordinates lies in. */
60+
fun Set<Coordinates>.area(): Vector {
61+
var dx = 0
62+
var dy = 0
63+
forEach {
64+
dx = kotlin.math.max(it.x, dx)
65+
dy = kotlin.math.max(it.y, dy)
66+
}
67+
return Vector(dx, dy)
68+
}
69+
5770
/** Prints an ascii art of the piece. */
58-
fun Set<Coordinates>.print(dimension: Vector = Vector(4, 5)) {
71+
fun Set<Coordinates>.print(dimension: Vector = area()) {
5972
printShapes(this, dimension = dimension)
6073
}
6174

6275
/** Prints all given shapes next to each other. */
6376
fun printShapes(vararg shapes: Set<Coordinates>, dimension: Vector = Vector(4, 5)) {
77+
if (shapes.any{it.area() < dimension})
78+
throw IndexOutOfBoundsException("The largest shape has to fit in the given dimension")
79+
6480
val width = shapes.size * (dimension.dx + 1)
6581
val array = Array(dimension.dy * width) {FieldContent.EMPTY.letter}
6682
for (n in array.indices) {
@@ -77,3 +93,15 @@ fun printShapes(vararg shapes: Set<Coordinates>, dimension: Vector = Vector(4, 5
7793
}
7894
println()
7995
}
96+
97+
/** Filters all moves, returning only those who pass the validation functions. */
98+
fun Set<SetMove>.filterValidMoves(gameState: GameState): Set<SetMove> =
99+
filter {
100+
try {
101+
GameRuleLogic.validateMoveColor(gameState, it)
102+
GameRuleLogic.validateSetMove(gameState, it)
103+
true
104+
} catch(e: InvalidMoveException) {
105+
false
106+
}
107+
}.toSet()

0 commit comments

Comments
 (0)