Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add a way to seed the rng to avoid always identical games #9

Merged
merged 1 commit into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
52 changes: 38 additions & 14 deletions examples/snake.roc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Model : {
snake : Snake,
fruit : Fruit,
fruitSprite : Sprite,
gameStarted : Bool,
}

main : Program
Expand All @@ -28,8 +29,6 @@ init : Task Model []
init =
{} <- setColorPalette |> Task.await

startingFruit <- getRandomFruit startingSnake |> Task.await

fruitSprite = Sprite.new {
data: [0x00, 0xa0, 0x02, 0x00, 0x0e, 0xf0, 0x36, 0x5c, 0xd6, 0x57, 0xd5, 0x57, 0x35, 0x5c, 0x0f, 0xf0],
bpp: BPP2,
Expand All @@ -40,30 +39,55 @@ init =
Task.ok {
frameCount: 0,
snake: startingSnake,
fruit: startingFruit,
fruit: {x: 0, y: 0},
fruitSprite,
gameStarted: Bool.false,
}

update : Model -> Task Model []
update = \model ->
if snakeIsDead model.snake then
{} <- drawGame model |> Task.await
{} <- W4.setTextColors { fg: blue, bg: white } |> Task.await
# TODO: maybe count and display score under game over message.
{} <- W4.text "Game Over!" { x: 40, y: 72 } |> Task.await
Task.ok model
update = \prev ->
# Update frame count
model = { prev & frameCount: prev.frameCount + 1 }

if !model.gameStarted then
runTitleScreen model
else if snakeIsDead model.snake then
runEndScreen model
else
runGame model

runTitleScreen : Model -> Task Model []
runTitleScreen = \model ->
{} <- W4.text "Press X to start!" { x: 15, y: 72 } |> Task.await

gamepad <- W4.getGamepad Player1 |> Task.await

if gamepad.button1 then
# Seed the randomness with number of frames since the start of the game.
# This makes the game feel like it is truely randomly seeded cause players won't always start on the same frame.
{} <- W4.seedRand model.frameCount |> Task.await

# Generate the starting fruit.
fruit <- getRandomFruit startingSnake |> Task.await

Task.ok { model & gameStarted: Bool.true, fruit }
else
Task.ok model

runEndScreen : Model -> Task Model []
runEndScreen = \model ->
{} <- drawGame model |> Task.await
{} <- W4.setTextColors { fg: blue, bg: white } |> Task.await

{} <- W4.text "Game Over!" { x: 40, y: 72 } |> Task.await
Task.ok model

runGame : Model -> Task Model []
runGame = \prev ->
runGame = \model ->

# Get gamepad
gamepad <- W4.getGamepad Player1 |> Task.await

# Update frame
model = { prev & frameCount: prev.frameCount + 1 }

# Update snake
(snake, ate) =
updateSnake model.snake gamepad model.frameCount model.fruit
Expand Down
2 changes: 2 additions & 0 deletions platform/Effect.roc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ hosted Effect
line,
hline,
vline,
seedRand,
rand,
randRangeLessThan,
blit,
Expand Down Expand Up @@ -48,6 +49,7 @@ oval : I32, I32, U32, U32 -> Effect {}
line : I32, I32, I32, I32 -> Effect {}
hline : I32, I32, U32 -> Effect {}
vline : I32, I32, U32 -> Effect {}
seedRand : U64 -> Effect {}
rand : Effect I32
randRangeLessThan : I32, I32 -> Effect I32
blit : List U8, I32, I32, U32, U32, U32 -> Effect {}
Expand Down
28 changes: 28 additions & 0 deletions platform/W4.roc
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ interface W4
line,
hline,
vline,
seedRand,
rand,
randBetween,
trace,
Expand Down Expand Up @@ -463,13 +464,35 @@ getNetplay =
Ok Disabled
|> InternalTask.fromEffect

## Seeds the global pseudo-random number generator.
##
## ```
## {} <- W4.seedRand framesSinceStart |> Task.await
## ```
##
## Wasm4 exposes no way to seed a random number generator.
## As such, anything random will be exactly the same on every run by default.
## To work around this, it is suggested to count the number of frames the user is on
## the title screen before starting the game and use that to seed the prng.
##
seedRand : U64 -> Task {} []
seedRand = \s ->
Effect.seedRand s
|> Effect.map Ok
|> InternalTask.fromEffect

## Generate a pseudo-random number.
##
## ```
## # pseudo-random number between Num.minI32 and Num.maxI32 (inclusive of both)
## i <- W4.rand |> Task.await
## ```
##
## Warning: Wasm4 exposes no way to seed a random number generator.
## As such, anything random will be exactly the same on every run by default.
## To work around this, it is suggested to count the number of frames the user is on
## the title screen before starting the game and use that to seed the prng.
##
rand : Task I32 []
rand =
Effect.rand
Expand All @@ -485,6 +508,11 @@ rand =
## i <- W4.randBetween {start: 0, before: 100} |> Task.await
## ```
##
## Warning: Wasm4 exposes no way to seed a random number generator.
## As such, anything random will be exactly the same on every run by default.
## To work around this, it is suggested to count the number of frames the user is on
## the title screen before starting the game and use that to seed the prng.
##
randBetween : { start : I32, before : I32 } -> Task I32 []
randBetween = \{ start, before } ->
Effect.randRangeLessThan start before
Expand Down
5 changes: 5 additions & 0 deletions platform/host.zig
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,11 @@ export fn roc_fx_getNetplay() callconv(.C) u8 {
return w4.NETPLAY.*;
}

export fn roc_fx_seedRand(seed: u64) callconv(.C) void {
prng = std.rand.DefaultPrng.init(seed);
rnd = prng.random();
}

export fn roc_fx_rand() callconv(.C) i32 {
return rnd.int(i32);
}
Expand Down