Skip to content

Commit

Permalink
Dice uses Xorshift algo, not scala.util.Random
Browse files Browse the repository at this point in the history
  • Loading branch information
davesmith00000 committed Nov 12, 2024
1 parent 5cd29fd commit 1fc9817
Show file tree
Hide file tree
Showing 10 changed files with 246 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ object PhysicsWorldBenchmarks:

object TestWorlds:

val dice: Dice = Dice.fromSeed(0)
val dice: Dice = Dice.default

val basicWorld: World[MyTag] =
val circles =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class PathFindingTests extends munit.FunSuite {

val searchGrid = SearchGrid.generate(start, end, List(impassable), 3, 3)

val path: List[Coords] = searchGrid.locatePath(Dice.fromSeed(0))
val path: List[Coords] = searchGrid.locatePath(Dice.default)

val possiblePaths: List[List[Coords]] = List(
List(start, Coords(2, 2), Coords(1, 2), end),
Expand All @@ -45,7 +45,7 @@ class PathFindingTests extends munit.FunSuite {

val searchGrid = SearchGrid.generate(start, end, List(impassable), 3, 3)

val path: List[Coords] = searchGrid.locatePath(Dice.fromSeed(0))
val path: List[Coords] = searchGrid.locatePath(Dice.default)

val possiblePaths: List[List[Coords]] = List(
List(start, Coords(0, 2), Coords(1, 2), Coords(2, 2), end),
Expand All @@ -67,7 +67,7 @@ class PathFindingTests extends munit.FunSuite {

val searchGrid = SearchGrid.generate(start, end, List(impassable), 3, 3)

val path: List[Coords] = searchGrid.locatePath(Dice.fromSeed(0))
val path: List[Coords] = searchGrid.locatePath(Dice.default)

val possiblePaths: List[List[Coords]] = List(
List(start, Coords(2, 0), Coords(2, 1), Coords(2, 2), end),
Expand All @@ -89,7 +89,7 @@ class PathFindingTests extends munit.FunSuite {

val searchGrid = SearchGrid.generate(start, end, List(impassable), 3, 3)

val path: List[Coords] = searchGrid.locatePath(Dice.fromSeed(0))
val path: List[Coords] = searchGrid.locatePath(Dice.default)

val possiblePaths: List[List[Coords]] = List(
List(start, Coords(2, 2), Coords(2, 1), Coords(2, 0), end),
Expand All @@ -111,7 +111,7 @@ class PathFindingTests extends munit.FunSuite {

val searchGrid = SearchGrid.generate(start, end, List(impassable), 3, 3)

val path: List[Coords] = searchGrid.locatePath(Dice.fromSeed(0))
val path: List[Coords] = searchGrid.locatePath(Dice.default)

val possiblePaths: List[List[Coords]] = List(
List(start, Coords(2, 2), Coords(1, 2), Coords(0, 2), end),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ final class GameEngine[StartUpData, GameModel, ViewModel](

audioPlayer.addAudioAssets(accumulatedAssetCollection.sounds)

val dice = Dice.fromSeed((if firstRun then 0 else gameLoopInstance.runningTimeReference).toLong)
val dice = if firstRun then Dice.default else Dice.fromSeed(gameLoopInstance.runningTimeReference.toLong)

if firstRun then platform = new Platform(parentElement, gameConfig, globalEventStream, dynamicText)

Expand Down
13 changes: 8 additions & 5 deletions indigo/indigo/src/main/scala/indigo/shared/Context.scala
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ object Context:
),
Services(
boundaryLocator,
scala.util.Random(),
scala.util.Random(Dice.DefaultSeed),
_captureScreen
)
)
Expand Down Expand Up @@ -98,7 +98,7 @@ object Context:
new Frame(dice, time, input)

val initial: Frame =
new Frame(Dice.fromSeed(0), GameTime.zero, InputState.default)
new Frame(Dice.default, GameTime.zero, InputState.default)

trait Services:
def bounds: Services.Bounds
Expand Down Expand Up @@ -180,7 +180,8 @@ object Context:
def nextString(length: Int): String
def nextPrintableChar(): Char
def setSeed(seed: Long): Unit
def shuffle[T](xs: List[T]): List[T]
def shuffle[A](xs: List[A]): List[A]
def shuffle[A](xs: Batch[A]): Batch[A]
def alphanumeric(take: Int): List[Char]

object Random:
Expand All @@ -201,7 +202,8 @@ object Context:
def nextString(length: Int): String = _random.nextString(length)
def nextPrintableChar(): Char = _random.nextPrintableChar()
def setSeed(seed: Long): Unit = _random.setSeed(seed)
def shuffle[T](xs: List[T]): List[T] = _random.shuffle(xs)
def shuffle[A](xs: List[A]): List[A] = _random.shuffle(xs)
def shuffle[A](xs: Batch[A]): Batch[A] = Batch.fromList(_random.shuffle(xs.toList))
def alphanumeric(take: Int): List[Char] = _random.alphanumeric.take(take).toList

val noop: Random =
Expand All @@ -220,7 +222,8 @@ object Context:
def nextString(length: Int): String = ""
def nextPrintableChar(): Char = ' '
def setSeed(seed: Long): Unit = ()
def shuffle[T](xs: List[T]): List[T] = xs
def shuffle[A](xs: List[A]): List[A] = xs
def shuffle[A](xs: Batch[A]): Batch[A] = xs
def alphanumeric(take: Int): List[Char] = List.fill(take)(' ')

trait Screen:
Expand Down
145 changes: 132 additions & 13 deletions indigo/indigo/src/main/scala/indigo/shared/dice/Dice.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import indigo.shared.time.Millis
import indigo.shared.time.Seconds

import scala.annotation.tailrec
import scala.util.Random

/** The Dice primitive supplies a consistent way to get psuedo-random values into your game.
*
Expand All @@ -20,6 +19,9 @@ trait Dice:

/** The seed value of the dice. The dice supplied in the `FrameContext` has the seed set to the current running time
* of the game in milliseconds.
*
* If the seed value is 0, it is replaced with the `DefaultSeed` value in order for the underlying PRNG to work
* correctly.
*/
def seed: Long

Expand All @@ -31,6 +33,14 @@ trait Dice:
*/
def roll(sides: Int): Int

/** Roll a Long from 1 to the number of sides on the dice (inclusive)
*/
def rollLong: Long

/** Roll a Long from 1 to the specified number of sides (inclusive), using this dice instance as the seed.
*/
def rollLong(sides: Int): Long

/** Roll an Int from 0 to the number of sides on the dice (inclusive)
*/
def rollFromZero: Int
Expand Down Expand Up @@ -67,14 +77,21 @@ trait Dice:
*/
def shuffle[A](items: List[A]): List[A]

def shuffle[A](items: Batch[A]): Batch[A] =
Batch.fromSeq(shuffle(items.toList))
/** Shuffles a Batch of values into a random order
*/
def shuffle[A](items: Batch[A]): Batch[A]

override def toString: String =
s"Dice(seed = ${seed.toString()})"
val seedValue =
if seed == 0L then s"${seed.toString} (substituted with ${Dice.DefaultSeed.toString})"
else seed.toString

s"Dice(seed = $seedValue)"

object Dice:

val DefaultSeed: Long = 42L

/** Construct a 'max int' sided dice using a time in seconds (converted to millis) as the seed.
*/
def fromSeconds(time: Seconds): Dice =
Expand All @@ -87,10 +104,17 @@ object Dice:

/** Construct a 'max int' sided dice from a given seed value. This is the default dice presented by the
* `FrameContext`.
*
* The implementation of this method uses the Xorshift algorithm to generate random numbers, which has a problem: A
* seed value of 0 will produce a value of 0. This is a known issue with the algorithm, and while it is not a bug, it
* will cause unexpected behaviour. Hence, if the seed value is 0, it is replaced with the `DefaultSeed` value.
*/
def fromSeed(seed: Long): Dice =
Sides.MaxInt(seed)

def default: Dice =
Sides.MaxInt(DefaultSeed)

private val isPositive: Int => Boolean =
_ > 0

Expand Down Expand Up @@ -127,14 +151,20 @@ object Dice:
*/
def loaded(fixedTo: Int): Dice =
new Dice {
val seed: Long = 0
val seed: Long = Dice.DefaultSeed

def roll: Int =
fixedTo

def roll(sides: Int): Int =
fixedTo

def rollLong: Long =
fixedTo.toLong

def rollLong(sides: Int): Long =
fixedTo.toLong

def rollFromZero: Int =
fixedTo

Expand All @@ -158,30 +188,45 @@ object Dice:

def shuffle[A](items: List[A]): List[A] =
items

def shuffle[A](items: Batch[A]): Batch[A] =
items
}

/** Constructs a dice with a given number of sides and a seed value.
*/
def diceSidesN(sides: Int, seedValue: Long): Dice =
new Dice {
val seed: Long = seedValue

val r: Random = new Random(seed)
// The Xorshift algorithm requires a seed value that is not 0, as a seed of 0 will produce a value of 0.
val seed: Long = if seedValue == 0L then DefaultSeed else seedValue

private val r: RandomImpl = new RandomImpl(seed.toInt)

/** Roll an Int from 1 to the number of sides on the dice (inclusive)
*
* @return
*/
def roll: Int =
roll(sides)
r.nextInt(sides) + 1

/** Roll an Int from 1 to the specified number of sides (inclusive)
*
* @param sides
* @return
*/
def roll(sides: Int): Int =
r.nextInt(sanitise(sides)) + 1
r.nextInt(sides) + 1

/** Roll a Long from 1 to the number of sides on the dice (inclusive)
*/
def rollLong: Long =
r.nextLong(sides) + 1

/** Roll a Long from 1 to the specified number of sides (inclusive), using this dice instance as the seed.
*/
def rollLong(sides: Int): Long =
r.nextLong(sides) + 1

/** Roll an Int from 0 to the number of sides on the dice (exclusive)
*
Expand All @@ -196,7 +241,7 @@ object Dice:
* @return
*/
def rollFromZero(sides: Int): Int =
roll(sides) - 1
r.nextInt(sides)

/** Roll an Int from the range provided (inclusive)
*
Expand Down Expand Up @@ -229,7 +274,7 @@ object Dice:
* @return
*/
def rollAlphaNumeric(length: Int): String =
r.alphanumeric.take(length).mkString
r.alphanumeric(length)

/** Produces a random alphanumeric string 16 characters long
*
Expand All @@ -238,13 +283,21 @@ object Dice:
def rollAlphaNumeric: String =
rollAlphaNumeric(16)

/** Shuffles a list of values into a random order
/** Shuffles a Batch of values into a random order
*
* @param items
* @return
*/
def shuffle[A](items: List[A]): List[A] =
def shuffle[A](items: Batch[A]): Batch[A] =
r.shuffle(items)

/** Shuffles a List of values into a random order
*
* @param items
* @return
*/
def shuffle[A](items: List[A]): List[A] =
r.shuffle(Batch.fromList(items)).toList
}

/** Pre-constructed dice with a fixed number of sides, rolls are includive and start at 1, not 0. You need to provide
Expand Down Expand Up @@ -319,3 +372,69 @@ object Dice:
/** A sixteen-sided dice.
*/
def Sixteen(seed: Long): Dice = diceSidesN(16, seed)

/** A simple random number generator based on the Xorshift algorithm.
*/
@SuppressWarnings(Array("scalafix:DisableSyntax.var"))
final class RandomImpl(_seed: Int) {

private var seed: Int = _seed

// Xorshift for 32-bit integers, the backbone of this random number generator
def nextInt(): Int = {
var x = seed
x ^= x << 13
x ^= x >>> 17
x ^= x << 5
seed = x
x
}

// Generate an Int up to a limit value
def nextInt(limit: Int): Int =
Math.abs(nextInt()) % limit

// Generate a Long by combining two nextInt() calls
def nextLong(): Long =
(nextInt().toLong << 32) | (nextInt().toLong & 0xffffffffL)

// Generate a Long up to a limit value
def nextLong(limit: Int): Long =
Math.abs(nextLong()) % limit

// Generates a Float between 0.0 and 1.0
// Use of the bit mask ensures the result is positive while preserving the number distribution.
def nextFloat(): Float =
(nextInt().toLong & 0xffffffffL) / (1L << 32).toFloat

// Generates a Double between 0.0 and 1.0
def nextDouble(): Double =
(nextLong() & Long.MaxValue) / (Long.MaxValue.toDouble + 1)

// Generates a Boolean
def nextBoolean(): Boolean =
nextInt() % 2 == 0

// Fisher-Yates shuffle for List[A]
// More efficient but less readable than `items.sortBy(_ => nextInt())`
def shuffle[A](items: Batch[A]): Batch[A] =
val array = items.toJSArray

for (i <- array.indices.reverse) {
val j = nextInt().abs % (i + 1)
val temp = array(i)
array(i) = array(j)
array(j) = temp
}

Batch(array)

// Generate an alphanumeric string of a given length
def alphanumeric(limit: Int): String =
val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
(1 to limit).map(_ => chars(nextInt().abs % chars.length)).mkString

// Method to reset the seed for reproducibility
def setSeed(newSeed: Int): Unit =
seed = newSeed
}
Loading

0 comments on commit 1fc9817

Please sign in to comment.