Skip to content

Commit 7ef399c

Browse files
committed
feat(BasicExtension): add weighted random variable entry
1 parent 3a2ded8 commit 7ef399c

File tree

1 file changed

+81
-0
lines changed

1 file changed

+81
-0
lines changed
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package com.typewritermc.basic.entries.variables
2+
3+
import com.typewritermc.core.books.pages.Colors
4+
import com.typewritermc.core.extension.annotations.Entry
5+
import com.typewritermc.core.extension.annotations.Min
6+
import com.typewritermc.core.extension.annotations.Default
7+
import com.typewritermc.core.extension.annotations.VariableData
8+
import com.typewritermc.core.interaction.randomSeed
9+
import com.typewritermc.core.utils.Generic
10+
import com.typewritermc.engine.paper.entry.entries.ConstVar
11+
import com.typewritermc.engine.paper.entry.entries.Var
12+
import com.typewritermc.engine.paper.entry.entries.VarContext
13+
import com.typewritermc.engine.paper.entry.entries.VariableEntry
14+
import com.typewritermc.engine.paper.entry.entries.getData
15+
import java.util.Collections.emptyList
16+
import kotlin.random.Random
17+
18+
@Entry(
19+
"weighted_random_variable",
20+
"A variable that returns a weighted random value of the given values",
21+
Colors.GREEN,
22+
"streamline:dices-entertainment-gaming-dices-solid",
23+
)
24+
@VariableData(WeightedRandomVariableData::class)
25+
/**
26+
* The `WeightedRandomVariableEntry` is a variable that returns one configured value based on weight.
27+
*
28+
* ## How could this be used?
29+
* This can be used for loot rolls, branching dialogue, or any case where outcomes should not have equal chance.
30+
*/
31+
class WeightedRandomVariableEntry(
32+
override val id: String = "",
33+
override val name: String = "",
34+
val values: List<WeightedValue> = emptyList(),
35+
) : VariableEntry {
36+
override fun <T : Any> get(context: VarContext<T>): T {
37+
val dataValues = context.getData<WeightedRandomVariableData>()?.values ?: emptyList()
38+
val allValues = (values + dataValues).filter { it.weight > 0 }
39+
40+
require(allValues.isNotEmpty()) {
41+
"Weighted random variable '$id' has no values with a positive weight"
42+
}
43+
44+
val selected = allValues.weightedRandom(Random(context.interactionContext.randomSeed()))
45+
val selectedValue = selected.value.get(context.player, context.interactionContext)
46+
val value = selectedValue.get(context.klass)
47+
?: throw IllegalStateException(
48+
"Could not cast generic value ${selectedValue.data} to ${context.klass.qualifiedName}",
49+
)
50+
51+
return value
52+
}
53+
}
54+
55+
data class WeightedRandomVariableData(
56+
val values: List<WeightedValue> = emptyList(),
57+
)
58+
59+
data class WeightedValue(
60+
val value: Var<Generic> = ConstVar(Generic.Empty),
61+
@Min(1)
62+
@Default("1")
63+
val weight: Int = 1,
64+
)
65+
66+
private fun List<WeightedValue>.weightedRandom(random: Random): WeightedValue {
67+
require(isNotEmpty()) {
68+
"Cannot select a weighted random value from an empty list"
69+
}
70+
71+
if (size == 1) {
72+
return first()
73+
}
74+
75+
val cumulativeWeights = scan(0.0) { acc, entry -> acc + entry.weight.toDouble() }.drop(1)
76+
val sampledWeight = random.nextDouble() * cumulativeWeights.last()
77+
78+
return zip(cumulativeWeights)
79+
.first { (_, cumulativeWeight) -> cumulativeWeight > sampledWeight }
80+
.first
81+
}

0 commit comments

Comments
 (0)