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`, `π`.
|
- Values: `Unset`, `π`.
|
||||||
- Primitive/class symbols: `Object`, `Int`, `Real`, `Bool`, `Char`, `String`, `Class`, `Callable`.
|
- 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`.
|
- 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`.
|
- Async types: `Deferred`, `CompletableDeferred`, `Mutex`, `Flow`, `FlowBuilder`.
|
||||||
- Delegation types: `Delegate`, `DelegateContext`.
|
- Delegation types: `Delegate`, `DelegateContext`.
|
||||||
- Regex types: `Regex`, `RegexMatch`.
|
- 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`.
|
- 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>`.
|
- 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>`.
|
- Host iterator bridge: `KotlinIterator<T>`.
|
||||||
|
- Random APIs: `extern object Random`, `extern class SeededRandom`.
|
||||||
|
|
||||||
### 4.2 High-use extension APIs
|
### 4.2 High-use extension APIs
|
||||||
- Iteration/filtering: `forEach`, `filter`, `filterFlow`, `filterNotNull`, `filterFlowNotNull`, `drop`, `dropLast`, `takeLast`.
|
- 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).
|
- `$~` (last regex match object).
|
||||||
- `TODO(message?)` utility.
|
- `TODO(message?)` utility.
|
||||||
- `StackTraceEntry` class.
|
- `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)
|
## 5. Additional Built-in Modules (import explicitly)
|
||||||
- `import lyng.observable`
|
- `import lyng.observable`
|
||||||
|
|||||||
23
docs/math.md
23
docs/math.md
@ -110,6 +110,29 @@ For example:
|
|||||||
assert( 5.clamp(0..10) == 5 )
|
assert( 5.clamp(0..10) == 5 )
|
||||||
>>> void
|
>>> 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
|
## Scientific constant
|
||||||
|
|
||||||
| name | meaning |
|
| name | meaning |
|
||||||
|
|||||||
@ -26,9 +26,6 @@ val DROP_FRAMES_BASE = 15
|
|||||||
val DROP_FRAMES_MIN = 3
|
val DROP_FRAMES_MIN = 3
|
||||||
val FRAME_DELAY_MS = 35
|
val FRAME_DELAY_MS = 35
|
||||||
val RESIZE_WAIT_MS = 250
|
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 ROTATION_KICKS = [0, -1, 1, -2, 2]
|
||||||
val ANSI_ESC = "\u001b["
|
val ANSI_ESC = "\u001b["
|
||||||
val ANSI_RESET = ANSI_ESC + "0m"
|
val ANSI_RESET = ANSI_ESC + "0m"
|
||||||
@ -442,10 +439,8 @@ if (!Console.isSupported()) {
|
|||||||
|
|
||||||
val board: Board = createBoard(boardW, boardH)
|
val board: Board = createBoard(boardW, boardH)
|
||||||
|
|
||||||
var rng = 1337
|
|
||||||
fun nextPieceId() {
|
fun nextPieceId() {
|
||||||
rng = (rng * RNG_A + RNG_C) % RNG_M
|
Random.next(1..7)
|
||||||
(rng % PIECES.size) + 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val state: GameState = GameState(
|
val state: GameState = GameState(
|
||||||
|
|||||||
@ -20,6 +20,8 @@ package net.sergeych.lyng
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.yield
|
import kotlinx.coroutines.yield
|
||||||
import net.sergeych.lyng.Script.Companion.defaultImportManager
|
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.CmdFunction
|
||||||
import net.sergeych.lyng.bytecode.CmdVm
|
import net.sergeych.lyng.bytecode.CmdVm
|
||||||
import net.sergeych.lyng.miniast.*
|
import net.sergeych.lyng.miniast.*
|
||||||
@ -30,6 +32,7 @@ import net.sergeych.lyng.stdlib_included.rootLyng
|
|||||||
import net.sergeych.lynon.ObjLynonClass
|
import net.sergeych.lynon.ObjLynonClass
|
||||||
import net.sergeych.mp_tools.globalDefer
|
import net.sergeych.mp_tools.globalDefer
|
||||||
import kotlin.math.*
|
import kotlin.math.*
|
||||||
|
import kotlin.random.Random as KRandom
|
||||||
|
|
||||||
@Suppress("TYPE_INTERSECTION_AS_REIFIED_WARNING")
|
@Suppress("TYPE_INTERSECTION_AS_REIFIED_WARNING")
|
||||||
class Script(
|
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 {
|
val defaultImportManager: ImportManager by lazy {
|
||||||
ImportManager(rootScope, SecurityManager.allowAll).apply {
|
ImportManager(rootScope, SecurityManager.allowAll).apply {
|
||||||
addPackage("lyng.stdlib") { module ->
|
addPackage("lyng.stdlib") { module ->
|
||||||
module.eval(Source("lyng.stdlib", rootLyng))
|
module.eval(Source("lyng.stdlib", rootLyng))
|
||||||
ObjKotlinIterator.bindTo(module.requireClass("KotlinIterator"))
|
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 ->
|
addPackage("lyng.observable") { module ->
|
||||||
module.addConst("Observable", ObjObservable)
|
module.addConst("Observable", ObjObservable)
|
||||||
|
|||||||
@ -172,4 +172,73 @@ class StdlibTest {
|
|||||||
|
|
||||||
""".trimIndent())
|
""".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 pow(x: Object, y: Object): Real
|
||||||
extern fun sqrt(x: 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 =~ / !~.
|
// Last regex match result, updated by =~ / !~.
|
||||||
var $~: Object? = null
|
var $~: Object? = null
|
||||||
|
|
||||||
|
|||||||
@ -12,9 +12,10 @@ Current focus
|
|||||||
|
|
||||||
Key recent changes
|
Key recent changes
|
||||||
- Updated AI helper docs to reflect static typing, type expressions, and compile-time-only name resolution.
|
- 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
|
Known failing tests
|
||||||
- Not checked in this session.
|
- None in :lynglib:jvmTest after Random/SeededRandom integration.
|
||||||
|
|
||||||
Last test run
|
Last test run
|
||||||
- Not checked in this session.
|
- `./gradlew :lynglib:jvmTest` (PASS).
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user