Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions O21.Game/Engine/EntityId.fs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@ namespace O21.Game.Engine

module EntityId =
[<Struct>]
type FishId = FishId of int
with static member empty = FishId 0
type FishId = FishId of uint64
with
static member empty = FishId 0UL
static member prefix = 1us
[<Struct>]
type BombId = BombId of int
with static member empty = BombId 0
type BombId = BombId of uint64
with
static member empty = BombId 0UL
static member prefix = 2us
[<Struct>]
type BonusId = BonusId of int
with static member empty = BonusId 0
type BonusId = BonusId of uint64
with
static member empty = BonusId 0UL
static member prefix = 3us

let (|IsFishId|_|) (id: objnull) =
match id with
Expand Down
16 changes: 7 additions & 9 deletions O21.Game/Engine/GameEngine.fs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ type GameEngine = {
let finalHandler =
(fun (engine: GameEngine) ->
{ engine with
ParticlesSource = this.ParticlesSource.Tick(this.CurrentLevel, this.Player)
ParticlesSource = this.ParticlesSource.Tick(this.CurrentLevel, this.Player, this.Random)
}, [||])

(mainHandler >-> finalHandler) this
Expand Down Expand Up @@ -330,15 +330,13 @@ type GameEngine = {
let fromExplosiveBullets =
engine.Bullets
|> Array.fold (fun acc b ->
if b.Explosive
if b.Explosive && b.Tick(level).IsNone
then
if b.Tick(level).IsNone then
Array.append acc
(Bullet.SpawnBulletsInPattern (BulletsPattern.Circle 8)
{ b with
Explosive = false
Lifetime = 0 - GameRules.BulletFromExplosiveLifetime - GameRules.BulletLifetime })
else acc
Array.append acc
(Bullet.SpawnBulletsInPattern (BulletsPattern.Circle 8)
{ b with
Explosive = false
Lifetime = 0 - GameRules.BulletFromExplosiveLifetime - GameRules.BulletLifetime })
else
acc) [||]
{ engine with
Expand Down
1 change: 1 addition & 0 deletions O21.Game/Engine/GameRules.fs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ let LevelSizeInTiles = Vector(50, 25)
let BombTriggerOffset = -15
let LifebuoySpawnProbability = 0.2 // TODO[#237]: Compare with the original
let LifeBonusSpawnProbability = seq {
yield 0.
yield! [|0.25; 0.125;|]
yield! Array.create 7 0.0625
while true do yield 0.
Expand Down
14 changes: 7 additions & 7 deletions O21.Game/Engine/ParticlesSource.fs
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,29 @@ type ParticlesSource = {
Particles: Particle[]
Timer: GameTimer
} with
member this.Tick(level: Level, player: Player): ParticlesSource =
member this.Tick(level: Level, player: Player, random: ReproducibleRandom): ParticlesSource =
let particles = this.Particles |> Array.choose(_.Tick(level))
let timer = this.Timer.Tick()

if timer.HasExpired then
let timer = timer.Reset()
{
Particles = particles |> Array.append ([|this.GenerateFromPlayer(player)|] |> Array.choose id)
Timer = { timer with Period = this.PickRandom GameRules.ParticlesPeriodRange }
Particles = particles |> Array.append ([|this.GenerateFromPlayer(player, random)|] |> Array.choose id)
Timer = { timer with Period = this.PickRandom(GameRules.ParticlesPeriodRange, random) }
}
else
{
Particles = particles
Timer = timer
}

member private this.PickRandom(range:int array) =
let index = Random.Shared.Next(range.Length)
member private this.PickRandom(range: int array, random: ReproducibleRandom) =
let index = random.NextExcluding(range.Length)
range[index]

member private this.GenerateFromPlayer(player: Player) =
member private this.GenerateFromPlayer(player: Player, random: ReproducibleRandom) =
let startPosition = GameRules.NewParticlePosition (player.TopForward, player.Direction)
let offset = this.PickRandom GameRules.ParticlesOffsetRange
let offset = this.PickRandom(GameRules.ParticlesPeriodRange, random)
let initialSpeed = -player.Velocity.Y
let speed = GameRules.ParticleSpeed + if initialSpeed < 0 then 0 else initialSpeed
Some {
Expand Down
128 changes: 128 additions & 0 deletions O21.Game/Engine/RandomGenerator.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// SPDX-FileCopyrightText: 2025 O21 contributors <https://github.com/ForNeVeR/O21>
//
// SPDX-License-Identifier: MIT

namespace O21.Game.Engine

open System
open System.Numerics
open O21.Game.MemoryHelpers

/// <summary>
/// Implementation of xoshiro256++ <br/>
/// See original algorithm <a href="https://prng.di.unimi.it/xoshiro256plusplus.c">here</a>
/// </summary>
type RandomGenerator(seed: int64) =

let randState: uint64[] = Array.zeroCreate 4

let jump = [|
0x180ec6d33cfd0abaUL; 0xd5a61266f0c9392cUL
0xa9582618e03fc9aaUL; 0x39abdc4529b1661cUL
|]

let longJump = [|
0x76e15d3efefdcbbfUL; 0xc5004e441c522fb3UL
0x77710069854ee241UL; 0x39109bb02acbe635UL
|]

/// Use splitmix64 for init (recommended by the authors)
let initializeState (seed: int64) =
let mutable x = uint64 seed
let next() =
x <- x + 0x9E3779B97F4A7C15UL
let mutable z = x
z <- (z ^^^ (z >>> 30)) * 0xBF58476D1CE4E5B9UL
z <- (z ^^^ (z >>> 27)) * 0x94D049BB133111EBUL
z ^^^ (z >>> 31)

for i = 0 to 3 do
randState[i] <- next()

let rotl (x: uint64) (k: int) =
BitOperations.RotateLeft(x, k)

do initializeState(seed)

member this.Seed = seed

member this.Next() : uint64 =
let result = rotl (randState[0] + randState[3]) 23 + randState[0]
let t = randState[1] <<< 17

randState[2] <- randState[2] ^^^ randState[0]
randState[3] <- randState[3] ^^^ randState[1]
randState[1] <- randState[1] ^^^ randState[2]
randState[0] <- randState[0] ^^^ randState[3]

randState[2] <- randState[2] ^^^ t
randState[3] <- rotl randState[3] 45

result

/// [0.0, 1.0)
member this.NextDouble() =
float (this.Next() >>> 11) * (1.0 / 9007199254740992.0)

/// [minValue, maxValue)
member this.NextDouble(minValue: float, maxValue: float) =
minValue + (maxValue - minValue) * this.NextDouble()

/// [0, maxValue)
member this.Next(?maxValue: int) =
let maxValue = defaultArg maxValue Int32.MaxValue
if maxValue <= 0 then invalidArg "maxValue" "Must be positive"
let threshold = (0x7FFFFFFFUL / uint64 maxValue) * uint64 maxValue
let rec loop() =
let r = this.Next() &&& 0x7FFFFFFFUL // 31 bits for positive integers
if r < threshold then int (r % uint64 maxValue)
else loop()
loop()

/// [minValue, maxValue)
member this.Next(minValue: int, maxValue: int) =
if minValue > maxValue then invalidArg "minValue" "minValue cannot be greater than maxValue"
minValue + this.Next(maxValue - minValue)

member this.NextBool() =
(this.Next() &&& 1UL) = 0UL

member this.NextBool probability =
if probability <= 0.0 then false
elif probability >= 1.0 then true
else this.NextDouble() < probability

member this.NextBytes(buffer: byte[]) =
for i in 0 .. 8 .. buffer.Length - 1 do
let randomValue = this.Next()
for j in 0 .. min 7 (buffer.Length - i - 1) do
buffer[i + j] <- byte ((randomValue >>> (j * 8)) &&& 0xFFUL)

member this.SaveState(buffer: Span<uint64>) =
if buffer.Length < 4 then
invalidArg (nameof(buffer)) "State must have exactly 4 elements"
randState.CopyTo buffer

member this.LoadState(state: Span<uint64>) =
if state.Length <> 4 then
invalidArg (nameof(state)) "State must have exactly 4 elements"
state.CopyTo randState

member private this.JumpBase(jumps: uint64[]) =
let newState = stackalloc<uint64> 4
for i in 0 .. 3 do
for b in 0 .. 63 do
if (jumps[i] &&& (1UL <<< b)) <> 0UL then
for j in 0 .. 3 do
newState[j] <- newState[j] ^^^ randState[j]
this.Next() |> ignore

let generator = RandomGenerator(0)
generator.LoadState(newState)
generator

member this.Jump() =
this.JumpBase(jump)

member this.LongJump() =
this.JumpBase(longJump)
21 changes: 12 additions & 9 deletions O21.Game/Engine/ReproducibleRandom.fs
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,22 @@ namespace O21.Game.Engine

open System
open System.Collections.Generic
open O21.Game.U95
open O21.Game.Engine.EntityId
open O21.Game.U95

type ReproducibleRandom private (backend: Random, idGenerator: SequentialIdGenerator) = // TODO[#276]: Not really reproducible for now. Make it so.
type ReproducibleRandom private (backend: RandomGenerator, idGenerator: SequentialIdGenerator) =

/// Creates a reproducible instance that's guaranteed
/// (TODO[#276]: in the future, that is)
/// to have reproducible number sequence generated across all of the supported platforms.
static member FromSeed(seed: int): ReproducibleRandom = ReproducibleRandom(Random(seed), SequentialIdGenerator())
static member FromSeed(seed: int64): ReproducibleRandom =
let baseGenerator = RandomGenerator(seed)
ReproducibleRandom(baseGenerator, SequentialIdGenerator(baseGenerator.Jump()))

/// <summary>
/// <para>Will choose a random seed to instantiate a new instance.</para>
/// <para>This is meant to be persisted together with the game data, to save replays.</para>
/// </summary>
static member ChooseRandomSeed(): int = Random.Shared.Next()
static member ChooseRandomSeed() = int64 <| Random.Shared.NextInt64()

/// <summary>Generates a random number in range from zero to <paramref name="boundary"/>.</summary>
/// <param name="boundary">A range boundary that's <b>excluded</b> from the range.</param>
Expand All @@ -29,16 +32,16 @@ type ReproducibleRandom private (backend: Random, idGenerator: SequentialIdGener
backend.Next 100 >= 50

member _.NextFishId() =
idGenerator.GetFishId()
idGenerator.NextIdWithPrefix(FishId.prefix) |> FishId

member _.NextBombId() =
idGenerator.GetBombId()
idGenerator.NextIdWithPrefix(BombId.prefix) |> BombId

member _.NextBonusId() =
idGenerator.GetBonusId()
idGenerator.NextIdWithPrefix(BonusId.prefix) |> BonusId
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly I don't understand why these methods were introduced in the ReproducibleRandom class to begin with; they are not supposed to be random at all.

Let's not touch these and SequentialIdGenerator in this PR? I'll open a separate issue to move them out of this type.

The SequentialIdGenerator was supposed to be sequential, not random. Moreover, random by its very definition is not unique, and uniqueness is what we need from the identifiers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, let's move it another PR

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#319.


member _.Chance(probability: float): bool =
backend.NextDouble() < probability
backend.NextBool(probability)

member _.RandomChoice<'a> (choices: IList<'a>): 'a =
if choices.Count = 0 then
Expand Down
32 changes: 13 additions & 19 deletions O21.Game/Engine/SequentialIdGenerator.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,18 @@

namespace O21.Game.Engine

open EntityId

type SequentialIdGenerator() =
let mutable nextFishId = 0
let mutable nextBombId = 0
let mutable nextBonusId = 0

member _.GetFishId() =
let newId = nextFishId + 1
nextFishId <- newId
FishId <| newId
type SequentialIdGenerator(generator: RandomGenerator) =
let mutable counter = 0UL

member _.GetBombId() =
let newId = nextBombId + 1
nextBombId <- newId
BombId <| newId
member this.NextId() : uint64 =
counter <- counter + 1UL

member _.GetBonusId() =
let newId = nextBonusId + 1
nextBonusId <- newId
BonusId <| newId
let randomPart = generator.Next()
let combined = counter ^^^ randomPart

if combined = 0UL then 1UL else combined

member this.NextIdWithPrefix (prefix: uint16) : uint64 =
let id = this.NextId()
let prefix = uint64 prefix <<< 48
(id &&& 0x0000FFFFFFFFFFFFUL) ||| prefix
15 changes: 15 additions & 0 deletions O21.Game/MemoryHelpers.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-FileCopyrightText: 2025 O21 contributors <https://github.com/ForNeVeR/O21>
//
// SPDX-License-Identifier: MIT

module O21.Game.MemoryHelpers

#nowarn 9
open System
open FSharp.NativeInterop

let inline stackalloc<'T when 'T: unmanaged> (length: int) : Span<'T> =
let ptr = NativePtr.stackalloc<'T> length
let span = Span<'T>(NativePtr.toVoidPtr ptr, length)
span.Clear()
span
2 changes: 2 additions & 0 deletions O21.Game/O21.Game.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ SPDX-License-Identifier: MIT

<Svg Include="Artwork\submarine.svg" />
<None Include="Svg.props" />
<Compile Include="MemoryHelpers.fs" />
<Compile Include="Paths.fs" />
</ItemGroup>

Expand Down Expand Up @@ -56,6 +57,7 @@ SPDX-License-Identifier: MIT
<Compile Include="U95\Level.fs" />
<Compile Include="U95\U95Data.fs" />
<Compile Include="AnimationType.fs" />
<Compile Include="Engine\RandomGenerator.fs" />
<Compile Include="Engine\EntityId.fs" />
<Compile Include="Engine\SequentialIdGenerator.fs" />
<Compile Include="Engine\ReproducibleRandom.fs" />
Expand Down
Loading