From 2d274aff6ff0ad82bae70750fbdfa75c4715002b 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 | 82 ++++++++++++++++++- 1 file changed, 79 insertions(+), 3 deletions(-) diff --git a/plugin/src/main/kotlin/sc/plugin2023/Board.kt b/plugin/src/main/kotlin/sc/plugin2023/Board.kt index 2e3f81b8e..61d0fc1c7 100644 --- a/plugin/src/main/kotlin/sc/plugin2023/Board.kt +++ b/plugin/src/main/kotlin/sc/plugin2023/Board.kt @@ -3,6 +3,8 @@ package sc.plugin2023 import com.thoughtworks.xstream.annotations.XStreamAlias import com.thoughtworks.xstream.annotations.XStreamImplicit import sc.api.plugins.* +import kotlin.math.min +import kotlin.math.roundToInt import sc.framework.deepCopy import kotlin.random.Random import sc.plugin2023.util.PenguinConstants as Constants @@ -11,7 +13,7 @@ import sc.plugin2023.util.PenguinConstants as Constants @XStreamAlias(value = "board") class Board( @XStreamImplicit(itemFieldName = "row") - override val gameField: MutableTwoDBoard = generateFields() + override val gameField: MutableTwoDBoard = generateFields(), ): RectangularBoard(), IBoard { constructor(board: Board): this(board.gameField.deepCopy()) @@ -69,9 +71,10 @@ class Board( companion object { /** Generiert ein neues Spielfeld mit zufällig auf dem Spielbrett verteilten Fischen. */ private fun generateFields(seed: Int = Random.nextInt()): MutableTwoDBoard { - var remainingFish = Constants.BOARD_SIZE * Constants.BOARD_SIZE val random = Random(seed) - println("Board Seed: $seed") + println("Board seed: $seed") + + var remainingFish = Constants.BOARD_SIZE * Constants.BOARD_SIZE var maxholes = 5 // Pro Hälfte 32 Felder, mind. 27 Schollen // Maximal (64-20)/2 = 22 2-Fisch-Schollen, @@ -95,5 +98,78 @@ class Board( } } + private fun generateFieldsRandom(seed: Int = Random.nextInt()): MutableTwoDBoard { + val random = Random(seed) + 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 fields: TwoDBoard = Array(length) { Array(width) { Field(0) } } + var countOne = 0 + + for(i in 0 until length) { + for(j in 0 until halfWidth) { + if(i * halfWidth + j < halfEnforcedOnes) { + fields[i][j] = Field(1) + countOne += 1 + continue + } + + val currentSum = fields.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++ + } + + fields[i][j] = Field(possibleValues[index]) + countOne += if(fields[i][j].fish == 1) 1 else 0 + } + } + + for(i in 0 until length) { + for(j in 0 until halfWidth) { + val x = random.nextInt(length) + val y = random.nextInt(halfWidth) + fields[i][j] = fields[x][y].also { fields[x][y] = fields[i][j] } + } + } + + for(i in 0 until length) { + for(j in 0 until halfWidth) { + // TODO ?? + // fields[i] += fields[length - i - 1][halfWidth - j - 1] + } + } + + return fields.let { + it + it.reversedArray().map { list -> + Array(Constants.BOARD_SIZE) { index -> list[Constants.BOARD_SIZE - index - 1].deepCopy() } + } + } + } } }