Add stdlib Random API and migrate tetris RNG
This commit is contained in:
parent
d9d7cafec8
commit
794553d81d
@ -20,6 +20,7 @@ Sources: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt`, `lynglib/s
|
||||
- Values: `Unset`, `π`.
|
||||
- Primitive/class symbols: `Object`, `Int`, `Real`, `Bool`, `Char`, `String`, `Class`, `Callable`.
|
||||
- Collections/types: `Iterable`, `Iterator`, `Collection`, `Array`, `List`, `ImmutableList`, `Set`, `ImmutableSet`, `Map`, `ImmutableMap`, `MapEntry`, `Range`, `RingBuffer`.
|
||||
- Random: singleton `Random` and class `SeededRandom`.
|
||||
- Async types: `Deferred`, `CompletableDeferred`, `Mutex`, `Flow`, `FlowBuilder`.
|
||||
- Delegation types: `Delegate`, `DelegateContext`.
|
||||
- Regex types: `Regex`, `RegexMatch`.
|
||||
@ -30,6 +31,7 @@ Sources: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt`, `lynglib/s
|
||||
- Exceptions/delegation base: `Exception`, `IllegalArgumentException`, `NotImplementedException`, `Delegate`.
|
||||
- Collections and iterables: `Iterable<T>`, `Iterator<T>`, `Collection<T>`, `Array<T>`, `List<T>`, `ImmutableList<T>`, `Set<T>`, `ImmutableSet<T>`, `Map<K,V>`, `ImmutableMap<K,V>`, `MapEntry<K,V>`, `RingBuffer<T>`.
|
||||
- Host iterator bridge: `KotlinIterator<T>`.
|
||||
- Random APIs: `extern object Random`, `extern class SeededRandom`.
|
||||
|
||||
### 4.2 High-use extension APIs
|
||||
- Iteration/filtering: `forEach`, `filter`, `filterFlow`, `filterNotNull`, `filterFlowNotNull`, `drop`, `dropLast`, `takeLast`.
|
||||
@ -48,6 +50,8 @@ Sources: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt`, `lynglib/s
|
||||
- `$~` (last regex match object).
|
||||
- `TODO(message?)` utility.
|
||||
- `StackTraceEntry` class.
|
||||
- `Random.nextInt()`, `Random.nextFloat()`, `Random.next(range)`, `Random.seeded(seed)`.
|
||||
- `SeededRandom.nextInt()`, `SeededRandom.nextFloat()`, `SeededRandom.next(range)`.
|
||||
|
||||
## 5. Additional Built-in Modules (import explicitly)
|
||||
- `import lyng.observable`
|
||||
|
||||
23
docs/math.md
23
docs/math.md
@ -110,6 +110,29 @@ For example:
|
||||
assert( 5.clamp(0..10) == 5 )
|
||||
>>> void
|
||||
|
||||
## Random values
|
||||
|
||||
Lyng stdlib provides a global random singleton and deterministic seeded generators:
|
||||
|
||||
| name | meaning |
|
||||
|--------------------------|---------|
|
||||
| Random.nextInt() | random `Int` from full platform range |
|
||||
| Random.nextFloat() | random `Real` in `[0,1)` |
|
||||
| Random.next(range) | random value from the given finite range |
|
||||
| Random.seeded(seed) | creates deterministic generator |
|
||||
| SeededRandom.nextInt() | deterministic random `Int` |
|
||||
| SeededRandom.nextFloat() | deterministic random `Real` in `[0,1)` |
|
||||
| SeededRandom.next(range) | deterministic random value from range |
|
||||
|
||||
Examples:
|
||||
|
||||
val rng = Random.seeded(1234)
|
||||
assert( rng.next(1..10) in 1..10 )
|
||||
assert( rng.next('a'..<'f') in 'a'..<'f' )
|
||||
assert( rng.next(0.0..<1.0) >= 0.0 )
|
||||
assert( rng.next(0.0..<1.0) < 1.0 )
|
||||
>>> void
|
||||
|
||||
## Scientific constant
|
||||
|
||||
| name | meaning |
|
||||
|
||||
@ -26,9 +26,6 @@ val DROP_FRAMES_BASE = 15
|
||||
val DROP_FRAMES_MIN = 3
|
||||
val FRAME_DELAY_MS = 35
|
||||
val RESIZE_WAIT_MS = 250
|
||||
val RNG_A = 1103515245
|
||||
val RNG_C = 12345
|
||||
val RNG_M = 2147483647
|
||||
val ROTATION_KICKS = [0, -1, 1, -2, 2]
|
||||
val ANSI_ESC = "\u001b["
|
||||
val ANSI_RESET = ANSI_ESC + "0m"
|
||||
@ -442,10 +439,8 @@ if (!Console.isSupported()) {
|
||||
|
||||
val board: Board = createBoard(boardW, boardH)
|
||||
|
||||
var rng = 1337
|
||||
fun nextPieceId() {
|
||||
rng = (rng * RNG_A + RNG_C) % RNG_M
|
||||
(rng % PIECES.size) + 1
|
||||
Random.next(1..7)
|
||||
}
|
||||
|
||||
val state: GameState = GameState(
|
||||
|
||||
@ -20,6 +20,8 @@ package net.sergeych.lyng
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.yield
|
||||
import net.sergeych.lyng.Script.Companion.defaultImportManager
|
||||
import net.sergeych.lyng.bridge.bind
|
||||
import net.sergeych.lyng.bridge.bindObject
|
||||
import net.sergeych.lyng.bytecode.CmdFunction
|
||||
import net.sergeych.lyng.bytecode.CmdVm
|
||||
import net.sergeych.lyng.miniast.*
|
||||
@ -30,6 +32,7 @@ import net.sergeych.lyng.stdlib_included.rootLyng
|
||||
import net.sergeych.lynon.ObjLynonClass
|
||||
import net.sergeych.mp_tools.globalDefer
|
||||
import kotlin.math.*
|
||||
import kotlin.random.Random as KRandom
|
||||
|
||||
@Suppress("TYPE_INTERSECTION_AS_REIFIED_WARNING")
|
||||
class Script(
|
||||
@ -588,11 +591,97 @@ class Script(
|
||||
}
|
||||
}
|
||||
|
||||
private fun seededRandomFromThis(scope: Scope, thisObj: Obj): KRandom {
|
||||
val instance = thisObj as? ObjInstance
|
||||
?: scope.raiseIllegalState("SeededRandom method requires instance receiver")
|
||||
val stored = instance.kotlinInstanceData
|
||||
if (stored is KRandom) return stored
|
||||
return KRandom.Default.also { instance.kotlinInstanceData = it }
|
||||
}
|
||||
|
||||
private suspend fun sampleRangeValue(scope: Scope, random: KRandom, range: ObjRange): Obj {
|
||||
if (range.start == null || range.start.isNull || range.end == null || range.end.isNull) {
|
||||
scope.raiseIllegalArgument("Random.next(range) requires a finite range")
|
||||
}
|
||||
val start = range.start
|
||||
val end = range.end
|
||||
|
||||
// Real ranges without explicit step are sampled continuously.
|
||||
if (!range.hasExplicitStep &&
|
||||
start is Numeric &&
|
||||
end is Numeric &&
|
||||
(start !is ObjInt || end !is ObjInt)
|
||||
) {
|
||||
val from = start.doubleValue
|
||||
val to = end.doubleValue
|
||||
if (from > to || (!range.isEndInclusive && from == to)) {
|
||||
scope.raiseIllegalArgument("Random.next(range) got an empty numeric range")
|
||||
}
|
||||
if (from == to) return ObjReal(from)
|
||||
val upperExclusive = if (range.isEndInclusive) to.nextUp() else to
|
||||
if (upperExclusive <= from) {
|
||||
scope.raiseIllegalArgument("Random.next(range) got an empty numeric range")
|
||||
}
|
||||
return ObjReal(random.nextDouble(from, upperExclusive))
|
||||
}
|
||||
|
||||
// Discrete sampling for stepped ranges and integer/char ranges.
|
||||
var picked: Obj? = null
|
||||
var count = 0L
|
||||
range.enumerate(scope) { value ->
|
||||
count += 1
|
||||
if (random.nextLong(count) == 0L) {
|
||||
picked = value
|
||||
}
|
||||
true
|
||||
}
|
||||
if (count <= 0L || picked == null) {
|
||||
scope.raiseIllegalArgument("Random.next(range) got an empty range")
|
||||
}
|
||||
return picked
|
||||
}
|
||||
|
||||
val defaultImportManager: ImportManager by lazy {
|
||||
ImportManager(rootScope, SecurityManager.allowAll).apply {
|
||||
addPackage("lyng.stdlib") { module ->
|
||||
module.eval(Source("lyng.stdlib", rootLyng))
|
||||
ObjKotlinIterator.bindTo(module.requireClass("KotlinIterator"))
|
||||
val seededRandomClass = module.requireClass("SeededRandom")
|
||||
module.bind("SeededRandom") {
|
||||
init { data = KRandom.Default }
|
||||
addFun("nextInt") {
|
||||
val rnd = seededRandomFromThis(requireScope(), thisObj)
|
||||
ObjInt.of(rnd.nextInt().toLong())
|
||||
}
|
||||
addFun("nextFloat") {
|
||||
val rnd = seededRandomFromThis(requireScope(), thisObj)
|
||||
ObjReal(rnd.nextDouble())
|
||||
}
|
||||
addFun("next") {
|
||||
val rnd = seededRandomFromThis(requireScope(), thisObj)
|
||||
val range = requiredArg<ObjRange>(0)
|
||||
sampleRangeValue(requireScope(), rnd, range)
|
||||
}
|
||||
}
|
||||
module.bindObject("Random") {
|
||||
addFun("nextInt") {
|
||||
ObjInt.of(KRandom.Default.nextInt().toLong())
|
||||
}
|
||||
addFun("nextFloat") {
|
||||
ObjReal(KRandom.Default.nextDouble())
|
||||
}
|
||||
addFun("next") {
|
||||
val range = requiredArg<ObjRange>(0)
|
||||
sampleRangeValue(requireScope(), KRandom.Default, range)
|
||||
}
|
||||
addFun("seeded") {
|
||||
val seed = requiredArg<ObjInt>(0).value.toInt()
|
||||
val instance = call(seededRandomClass) as? ObjInstance
|
||||
?: requireScope().raiseIllegalState("SeededRandom() did not return an object instance")
|
||||
instance.kotlinInstanceData = KRandom(seed)
|
||||
instance
|
||||
}
|
||||
}
|
||||
}
|
||||
addPackage("lyng.observable") { module ->
|
||||
module.addConst("Observable", ObjObservable)
|
||||
|
||||
@ -172,4 +172,73 @@ class StdlibTest {
|
||||
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRandomSeededDeterministic() = runTest {
|
||||
eval("""
|
||||
val a = Random.seeded(123456)
|
||||
val b = Random.seeded(123456)
|
||||
|
||||
for( i in 1..20 ) {
|
||||
assertEquals(a.nextInt(), b.nextInt())
|
||||
assertEquals(a.nextFloat(), b.nextFloat())
|
||||
assertEquals(a.next(1..100), b.next(1..100))
|
||||
}
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRandomNextFloatBounds() = runTest {
|
||||
eval("""
|
||||
for( i in 1..400 ) {
|
||||
val x = Random.nextFloat()
|
||||
assert(x >= 0.0)
|
||||
assert(x < 1.0)
|
||||
}
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRandomNextRangeVariants() = runTest {
|
||||
eval("""
|
||||
val rnd = Random.seeded(77)
|
||||
|
||||
for( i in 1..300 ) {
|
||||
val x = rnd.next(10..<20)
|
||||
assert(x in 10..<20)
|
||||
}
|
||||
|
||||
val allowed = [0, 3, 6, 9]
|
||||
for( i in 1..300 ) {
|
||||
val x = rnd.next(0..9 step 3)
|
||||
assert(x in allowed)
|
||||
}
|
||||
|
||||
for( i in 1..300 ) {
|
||||
val ch = rnd.next('a'..<'f')
|
||||
assert(ch in 'a'..<'f')
|
||||
}
|
||||
|
||||
for( i in 1..300 ) {
|
||||
val rf = rnd.next(1.5..<4.5)
|
||||
assert(rf >= 1.5)
|
||||
assert(rf < 4.5)
|
||||
}
|
||||
|
||||
val qAllowed = [0.0, 0.25, 0.5, 0.75, 1.0]
|
||||
for( i in 1..300 ) {
|
||||
val q = rnd.next(0.0..1.0 step 0.25)
|
||||
assert(q in qAllowed)
|
||||
}
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRandomRejectsOpenRange() = runTest {
|
||||
eval("""
|
||||
assertThrows(IllegalArgumentException) { Random.next(1..) }
|
||||
assertThrows(IllegalArgumentException) { Random.next(..10) }
|
||||
assertThrows(IllegalArgumentException) { Random.next(..) }
|
||||
""".trimIndent())
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,6 +82,19 @@ extern fun ln(x: Object): Real
|
||||
extern fun pow(x: Object, y: Object): Real
|
||||
extern fun sqrt(x: Object): Real
|
||||
|
||||
class SeededRandom {
|
||||
extern fun nextInt(): Int
|
||||
extern fun nextFloat(): Real
|
||||
extern fun next<T>(range: Range): T
|
||||
}
|
||||
|
||||
extern object Random {
|
||||
extern fun nextInt(): Int
|
||||
extern fun nextFloat(): Real
|
||||
extern fun next<T>(range: Range): T
|
||||
extern fun seeded(seed: Int): SeededRandom
|
||||
}
|
||||
|
||||
// Last regex match result, updated by =~ / !~.
|
||||
var $~: Object? = null
|
||||
|
||||
|
||||
@ -12,9 +12,10 @@ Current focus
|
||||
|
||||
Key recent changes
|
||||
- Updated AI helper docs to reflect static typing, type expressions, and compile-time-only name resolution.
|
||||
- Added stdlib random API: `Random` and deterministic `SeededRandom` with `nextInt`, `nextFloat`, and generic `next(range)`.
|
||||
|
||||
Known failing tests
|
||||
- Not checked in this session.
|
||||
- None in :lynglib:jvmTest after Random/SeededRandom integration.
|
||||
|
||||
Last test run
|
||||
- Not checked in this session.
|
||||
- `./gradlew :lynglib:jvmTest` (PASS).
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user