From 65a0b0a6fe94fcf99d028c54b3e1d9b338d9853f Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 5 May 2023 22:19:24 +0200 Subject: [PATCH] feat(Board): generate random fish distribution on game board This commit refactors the generateFields() method in the companion object to produce a better distribution of fish on the game board. Previously, the method randomly generated the fish placement with a maximum of 5 holes per side, and a minimum of 5 1-fish fields per side. The remainingFish variable was used to keep track of the number of fish left to place, and maxholes was used to ensure the maximum number of holes was not exceeded. The new implementation replaces the previous algorithm with a weighted probability distribution. The method now uses a list of Field objects and their corresponding probabilities to determine the number of fish that should be placed on each field. To ensure that the sum of the probabilities is 1, an IllegalArgumentException is thrown if the weighted sum does not equal 1. The method then determines the range of possible fish values that can be placed on the field, based on the number of fields remaining to be filled and the sum of fish already placed. The method then generates a random float between 0 and 1 and selects the corresponding fish value based on the probability distribution. The selected value is then assigned to the current field. Finally, the method shuffles the board to ensure that the placement of fish is random, and returns the resulting game board. Overall, this refactor results in a more evenly distributed placement of fish on the board, and eliminates the likelihood of having less than 8 1-fish fields. It also adds a constant distribution of (BoardSize*BoardSize*2) so 128 Fish on the Board. --- plugin/src/main/kotlin/sc/plugin2023/Board.kt | 144 ++++++++++++------ 1 file changed, 96 insertions(+), 48 deletions(-) diff --git a/plugin/src/main/kotlin/sc/plugin2023/Board.kt b/plugin/src/main/kotlin/sc/plugin2023/Board.kt index 3e30b29be..8e45fe8a7 100644 --- a/plugin/src/main/kotlin/sc/plugin2023/Board.kt +++ b/plugin/src/main/kotlin/sc/plugin2023/Board.kt @@ -2,93 +2,141 @@ package sc.plugin2023 import com.thoughtworks.xstream.annotations.XStreamAlias import sc.api.plugins.* +import kotlin.math.min +import kotlin.math.roundToInt import kotlin.random.Random import sc.plugin2023.util.PluginConstants as Constants /** - * Klasse welche eine Spielbrett darstellt. Bestehend aus einem + * Klasse welche ein Spielbrett darstellt. Bestehend aus einem * zweidimensionalen Array aus Feldern * * @author soed */ @XStreamAlias(value = "board") -class Board(fields: TwoDBoard = generateFields()): RectangularBoard(fields) { - - constructor(board: Board): this(board.gameField.clone()) - +class Board(fields: TwoDBoard = generateFields()) : RectangularBoard(fields) { + + constructor(board: Board) : this(board.gameField.clone()) + override fun isValid(coordinates: Coordinates) = - (coordinates.x + coordinates.y) % 2 == 0 && - coordinates.x >= 0 && - super.isValid(coordinates.copy(coordinates.x / 2)) - + (coordinates.x + coordinates.y) % 2 == 0 && + coordinates.x >= 0 && + super.isValid(coordinates.copy(coordinates.x / 2)) + /** Gibt das Feld an den gegebenen Koordinaten zurück. */ override operator fun get(x: Int, y: Int) = - super.get(x / 2, y) - + super.get(x / 2, y) + /** Ersetzt die Fische des Feldes durch einen Pinguin. * @return Anzahl der ersetzten Fische. */ operator fun set(position: Coordinates, team: Team?): Int { - if(!isValid(position)) + if (!isValid(position)) outOfBounds(position) val field = gameField[position.y][position.x / 2] gameField[position.y][position.x / 2] = Field(penguin = team) return field.fish } - + fun possibleMovesFrom(pos: Coordinates) = - Vector.DoubledHex.directions.flatMap { vector -> - (1 until Constants.BOARD_SIZE).map { - Move.run(pos, vector * it) - }.takeWhile { getOrEmpty(it.to).fish > 0 } - } - + Vector.DoubledHex.directions.flatMap { vector -> + (1 until Constants.BOARD_SIZE).map { + Move.run(pos, vector * it) + }.takeWhile { getOrEmpty(it.to).fish > 0 } + } + /** Returns a list of the non-null filter outputs */ fun filterFields(filter: (Field, Coordinates) -> T?): Collection = - gameField.flatMapIndexed { y, row -> - row.mapIndexedNotNull { x, field -> - filter(field, Coordinates.doubledHex(x, y)) - } + gameField.flatMapIndexed { y, row -> + row.mapIndexedNotNull { x, field -> + filter(field, Coordinates.doubledHex(x, y)) } - + } + fun getPenguins() = - filterFields { field, coordinates -> - field.penguin?.let { Pair(coordinates, it) } - } - + filterFields { field, coordinates -> + field.penguin?.let { Pair(coordinates, it) } + } + fun getOrEmpty(key: Coordinates?) = key?.let { getOrNull(it) } ?: Field() - + override val entries: Set> get() = filterFields { f, coordinates -> FieldPosition(coordinates, f) }.toSet() - + override fun clone(): Board = Board(this) - + companion object { /** Generiert ein neues Spielfeld mit zufällig auf dem Spielbrett verteilten Fischen. */ private fun generateFields(seed: Int = Random.nextInt()): TwoDBoard { - var remainingFish = Constants.BOARD_SIZE * Constants.BOARD_SIZE val random = Random(seed) - println("Board Seed: $seed") - var maxholes = 5 - // Pro Hälfte 32 Felder, mind. 27 Schollen - // Maximal (64-20)/2 = 22 2-Fisch-Schollen, - // also immer mindestens 5 1-Fisch-Schollen pro Seite - return List(Constants.BOARD_SIZE / 2) { - MutableList(Constants.BOARD_SIZE) { - val rand = random.nextInt(remainingFish) - if(rand < maxholes) { - maxholes-- - return@MutableList Field() + println("Board seed: $seed") + val length = Constants.BOARD_SIZE + val width = Constants.BOARD_SIZE + val weightedInts = + listOf(Field(0) to 0.1f, Field(1) to 0.2f, Field(2) to 0.4f, Field(3) to 0.2f, Field(4) to 0.1f) + val totalSum = length * width + val halfWidth = width / 2 + val halfEnforcedOnes = Constants.BOARD_SIZE / 2 + val arr: TwoDBoard = List(length) { MutableList(width) { Field(0) } } + var countOne = 0 + + for (i in 0 until length) { + for (j in 0 until halfWidth) { + if (i * halfWidth + j < halfEnforcedOnes) { + arr[i][j] = Field(1) + countOne += 1 + continue } - val fish = (rand - maxholes) / 20 + 1 - remainingFish -= fish - Field(fish) + + val currentSum = arr.sumOf { it -> it.sumOf { it.fish } } + val notFilled = totalSum - (i * halfWidth + j) + + val weightedSum = weightedInts.sumOf { it.second.toDouble() } + if (weightedSum.roundToInt() != 1) { + throw IllegalArgumentException("The sum of the probabilities must be 1. It is $weightedSum") + } + + val lowestPossible = weightedInts.filter { it.first.fish >= (totalSum - currentSum) / notFilled } + .minOf { it.first.fish } + val highestPossible = min(totalSum - currentSum, weightedInts.maxOf { it.first.fish }) + + val possibleValues = + weightedInts.filter { it.first.fish in lowestPossible..highestPossible }.map { it.first.fish } + val possibleWeights = + weightedInts.filter { it.first.fish in lowestPossible..highestPossible }.map { it.second } + + val value = random.nextFloat() + var cumulativeWeight = 0f + var index = 0 + while (index < possibleValues.size && cumulativeWeight + possibleWeights[index] < value) { + cumulativeWeight += possibleWeights[index] + index++ + } + + arr[i][j] = Field(possibleValues[index]) + countOne += if (arr[i][j].fish == 1) 1 else 0 } - }.let { + } + + for (i in 0 until length) { + for (j in 0 until halfWidth) { + val x = random.nextInt(length) + val y = random.nextInt(halfWidth) + arr[i][j] = arr[x][y].also { arr[x][y] = arr[i][j] } + } + } + + for (i in 0 until length) { + for (j in 0 until halfWidth) { + arr[i] += arr[length - i - 1][halfWidth - j - 1] + } + } + + return arr.let { it + it.asReversed().map { list -> MutableList(Constants.BOARD_SIZE) { index -> list[Constants.BOARD_SIZE - index - 1].clone() } } } + } - } }