Placing the Fruit
A freely moving snake is nice. But it get's a bit dull if that's all there is. To make it a bit more of a challenge, you'd need to add something to change the snake. The classic approach is to let the snake "eat" fruits. That's a good place to start.
const snake = new Snake()let fruit: Pointlet prevState: u8let frameCount = 0
To place (and eat) a fruit, you first need to make a variable for this. Since it's simply a point on the grid, a struct point
will do:
struct snake snake;int frame_count = 0;uint8_t prev_state = 0;struct point fruit;
// TODO
To place (and eat) a fruit, you first need to make a variable for this. Since it's simply a point on the grid, Point
will do:
frameCount = 0 prevState uint8 fruit Point
require "wasm4"local Snake = require "snake"local Point, Snake = Snake.Point, Snake.Snake
local snake = Snake.init()local fruit: Pointlocal frame_count = 0local prev_state = 0
// TODO
// TODO
// TODO
To place (and eat) a fruit, you first need to define it in Game
. Since it's simply a point on the grid, Point
will do:
// src/game.rsuse crate::snake::{Point, Snake};use crate::wasm4;
pub struct Game { snake: Snake, frame_count: u32, fruit: Point,}
impl Game { pub fn new() -> Self { Self { snake: Snake::new(), frame_count: 0, fruit: Point { x: 0, y: 0 }, } }}
;; snake.direction = 0x19a0;; snake.body_length = 0x19a8;; snake.body = 0x19ac;; frame_count = 0x262c;; fruit = 0x2630
const w4 = @import("wasm4.zig");const Snake = @import("snake.zig").Snake;const Point = @import("snake.zig").Point;
var snake = Snake.init();var fruit: Point = undefined;var frame_count: u32 = 0;var prev_state: u8 = 0;
#
Random NumbersAssemblyScript provides us with the Math.random
function. It returns a floating point value between 0
and 0.999999999
. But since we only deal with integer values, it's a good idea to create a helper function:
function rnd(n: i32 = 20): u16 { return u16(Math.floor(Math.random() * n))}
This allows you to call rnd(20)
to get a number between 0
and 19
. Now you can change the fruit declaration:
const snake = new Snake()const fruit = new Point(rnd(20), rnd(20))let prevState: u8let frameCount = 0
C provides us with pretty much everything we could need to create random numbers. Since we have already included stdlib.h
we can use the rand
function to generate random numbers. rand()
returns and int
between 0
and
RAND_MAX
which is guaranteed to be at least a 16-bit value. To limit the value returned by rand()
to the screen grid coordinates we can divide it by 20
and use the remainder as our value using the modulus operator:
fruit.x = rand()%20;fruit.y = rand()%20;
As we can not use the system time to seed our random function, we need to explore some other ways of randomizing our fruit placement: as it stands every time we run the game the fruit will always appear at the same locations and the same sequence. In order to seed the random number generator we need a value that will be different every time the game starts, unfortunately we do not have access to any but once the game starts we can use a combination of the frame_count
and user input to seed the randomness as the user will not always press the button at the exact same time.
To set the seed when the user inputs something, add the following line of code at the start of the input
function in main.c
:
void input(){ srand(frame_count); ...
The initial fruit placement will be the same every run, but subsequent placements will be based off of this seed which should hopefully be unique each time.
// TODO
Go provides us with pretty much everything we could need to create random numbers. The package math/rand
contains a handy function: Intn(n int) int
. It takes an integer and returns a random value between 0 and n-1. If you think, placing something like this in start
would be a good idea:
fruit.X = rand(20) fruit.Y = rand(20)
You'd be surprised that this pretty much crashes. So we can't use the standard random-functions of Go? No, we can. We just have to make our own instance of the number generator:
frameCount = 0 prevState uint8 fruit Point rnd func(int) int
Now you can use it like this:
rnd = rand.New(rand.NewSource(1)).Intn fruit.X = rnd(20) fruit.Y = rnd(20)
The 1
is the seed. But having a fixed seed is not a good idea. You might be tempted to use time.Now().Unix()
. But this will crash the came with a nice field 'runtime.ticks' is not a Function
.
Since the standard time
is not an option, how about you use something that is time related and is already in your project? Like frameCount
?
rnd = rand.New(rand.NewSource(int64(frameCount))).Intn fruit.X = rnd(20) fruit.Y = rnd(20)
Works. But since this is the start
-function, frameCount
is pretty much always 0
. That's why here's a small exercise for you: change the seed after the first key was pressed.
Check the for nil
Keep in mind, that the value of rnd
is nil
if it wasn't initialized yet.
Nelua provides us a math
library with a random
function, which when called with two integers m
and n
it returns a pseudo-random integer with uniform distribution in the range [m
, n
].
This allows you to call math.random(0, 19)
to get a number between 0
and 19
. Now you can change the fruit declaration:
local math = require 'math'
local snake = Snake.init()local fruit: Point = { x = math.random(0, 19), y = math.random(0,19) }local frame_count = 0local prev_state = 0
// TODO
// TODO
// TODO
There are several random random generators available on crates.io, including some that are cryptographically secure.
We don't need anything our snake game to be cryptographically secure so we'll use fastrand
crate:
it provides fastrand::Rng
, a simple seedable pseudo-random number generator.
Let's add fastrand
to Cargo.toml
:
[dependencies]buddy-alloc = { version = "0.4.1", optional = true }lazy_static = "1.4.0"fastrand = "1.6.0"
We'll then store an instance of fastrand::Rng
in our Game
and initialize fruit coordinates with
Rng::i32:
a method that returns a random i32
number within the input range.
// src/game.rsuse crate::snake::{Point, Snake};use crate::wasm4;use fastrand::Rng;
pub struct Game { rng: Rng, snake: Snake, frame_count: u32, fruit: Point,}
impl Game { pub fn new() -> Self { let rng = Rng::with_seed(235);
Self { frame_count: 0, snake: Snake::new(), prev_gamepad: 0, fruit: Point { x: rng.i32(0..20), y: rng.i32(0..20), }, rng, } }}
The WebAssembly text format doesn't provide any random number generator, so we'll have to write our own. Xorshift is a reasonable choice.
;; Initialize the random state to 1234.(global $random-state (mut i32) (i32.const 1234))
(func $rnd (param $n i32) (result i32) (local $x i32)
;; x = random-state ;; x ^= x << 13 ;; x ^= x >> 17 ;; x ^= x << 5 ;; random-state = x (global.set $random-state (local.tee $x (i32.xor (local.tee $x (i32.xor (local.tee $x (i32.xor (local.tee $x (global.get $random-state)) (i32.shl (local.get $x) (i32.const 13)))) (i32.shr_u (local.get $x) (i32.const 17)))) (i32.shl (local.get $x) (i32.const 5)))))
;; convert a random i32 in the range [0, 2**32) to a random f32 in the range ;; [0, 1). Then multiply by `n` to convert it to a f32 in the range [0, n). ;; Finally convert it back to an i32. (i32.trunc_f32_u (f32.mul (f32.mul (f32.convert_i32_u (i32.shr_u (local.get $x) (i32.const 8))) (f32.const 0x1p-24)) (f32.convert_i32_u (local.get $n)))))
This allows you to call rnd(20)
to get a number between 0
and 19
. Now you can randomly initialize the fruit position:
(func (export "start") (i32.store (global.get $PALETTE0) (i32.const 0xfbf7f3)) (i32.store (global.get $PALETTE1) (i32.const 0xe5b083)) (i32.store (global.get $PALETTE2) (i32.const 0x426e5d)) (i32.store (global.get $PALETTE3) (i32.const 0x20283d))
;; fruit.x = rnd(20) (i32.store (i32.const 0x2630) (call $rnd (i32.const 20)))
;; fruit.y = rnd(20) (i32.store (i32.const 0x2634) (call $rnd (i32.const 20))))
Zig's standard library has a couple of different random number generators, including some that are meant to be cryptographically secure. We don't need anything our snake game to be cryptographically secure, so we'll just use std.rand.DefaultPrng
, where Prng means pseudo-random number generator. To start, we'll need to import std
and initialize the prng:
const w4 = @import("wasm4.zig");const std = @import("std");const Snake = @import("snake.zig").Snake;const Point = @import("snake.zig").Point;
var snake = Snake.init();var fruit: Point = undefined;var frame_count: u32 = 0;var prev_state: u8 = 0;var prng: std.rand.DefaultPrng = undefined;var random: std.rand.Random = undefined;
export fn start() void { w4.PALETTE.* = .{ 0xfbf7f3, 0xe5b083, 0x426e5d, 0x20283d, }; prng = std.rand.DefaultPrng.init(0); random = prng.random();}
We can call random.intRangeLessThan(T, at_least, less_than)
to get a number between at_least
and less_than
, with the type of T
. You can wrap that in a helper function if you'd like:
fn rnd(max: i32) i32 { return random.intRangeLessThan(i32, 0, max);}
Now use it for the location of the fruit:
export fn start() void { w4.PALETTE.* = .{ 0xfbf7f3, 0xe5b083, 0x426e5d, 0x20283d, }; prng = std.rand.DefaultPrng.init(0); random = prng.random(); fruit = Point.init(rnd(20), rnd(20));}
#
Importing PNG FilesImporting images in WASM-4 works a bit different compared to other game engines and Fantasy Consoles. Images have to meet certain criteria:
- PNG only
- Index only
- 4 colors max
Indexed PNG files can be created by several image apps like Aseprite or GIMP.
The image we import is a 8x8 PNG file with exactly 4 colors:
This image is zoomed by 800%.
This is the original image. You can download it to proceed.
Now you need to import the image. For this, the WASM-4 CLI tool w4
comes with another tool: png2src
. You can use it like this:
w4 png2src --assemblyscript fruit.png
This will output the following content in the terminal:
const fruitWidth = 8;const fruitHeight = 8;const fruitFlags = 1; // BLIT_2BPPconst fruit = memory.data<u8>([ 0x00,0xa0,0x02,0x00,0x0e,0xf0,0x36,0x5c,0xd6,0x57,0xd5,0x57,0x35,0x5c,0x0f,0xf0 ]);
To get it into a an existing file, use the >>
operator. Like this:
w4 png2src --assemblyscript fruit.png >> main.ts
This will add the previous lines to your main.ts
and causes an error because "fruit" already exists. Just rename the new fruit to fruitSprite
and move it somewhere else. Also: You can remove the other stuff added, you won't need it for this project:
const snake = new Snake()const fruit = new Point(rnd(20), rnd(20))let frameCount = 0let prevState: u8const fruitSprite = memory.data<u8>([ 0x00,0xa0,0x02,0x00,0x0e,0xf0,0x36,0x5c,0xd6,0x57,0xd5,0x57,0x35,0x5c,0x0f,0xf0 ])
With that out of the way, it's time to actually render the newly imported sprite.
Now you need to import the image. For this, the WASM-4 CLI tool w4
comes with another tool: png2src
. You can use it like this:
w4 png2src --c fruit.png
This will output some code ending in the following to the terminal:
const uint8_t fruit_sprite[] = { 0x00,0xa0,0x02,0x00,0x0e,0xf0,0x36,0x5c,0xd6,0x57,0xd5,0x57,0x35,0x5c,0x0f,0xf0};
We need to add that array to the top of our main.c
file:
#include "wasm4.h"#include "snake.h"#include <stdlib.h>
struct snake snake;int frame_count = 0;uint8_t prev_state = 0;struct point fruit;const uint8_t fruit_sprite[] = { 0x00,0xa0,0x02,0x00,0x0e,0xf0,0x36,0x5c,0xd6,0x57,0xd5,0x57,0x35,0x5c,0x0f,0xf0};
With that out of the way, it's time to actually render the newly imported sprite.
// TODO
Now you need to import the image. For this, the WASM-4 CLI tool w4
comes with another tool: png2src
. You can use it like this:
w4 png2src --go fruit.png
This will output the following content in the terminal:
const fruitWidth = 8const fruitHeight = 8const fruitFlags = 1 // BLIT_2BPPvar fruit = [16]byte { 0x00,0xa0,0x02,0x00,0x0e,0xf0,0x36,0x5c,0xd6,0x57,0xd5,0x57,0x35,0x5c,0x0f,0xf0 }
To get it into a an existing file, use the >>
operator. Like this:
w4 png2src --go fruit.png >> main.go
This will add the previous lines to your main.go
and causes an error because "fruit" already exists. Just rename the new fruit to fruitSprite
and move it somewhere else. Also: You can remove the other stuff added, you won't need it for this project:
frameCount = 0 prevState uint8 fruit Point rnd func(int) int fruitSprite = [16]byte{0x00, 0xa0, 0x02, 0x00, 0x0e, 0xf0, 0x36, 0x5c, 0xd6, 0x57, 0xd5, 0x57, 0x35, 0x5c, 0x0f, 0xf0}
With that out of the way, it's time to actually render the newly imported sprite.
Now you need to import the image. For this, the WASM-4 CLI tool w4
comes with another tool: png2src
. You can use it like this:
w4 png2src --nelua fruit.png
This will output the following content in the terminal:
-- fruitlocal fruit_width <comptime> = 8local fruit_height <comptime> = 8local fruit_flags <comptime> = 1 -- BLIT_2BPPlocal fruit: [16]uint8 = { 0x00,0xa0,0x02,0x00,0x0e,0xf0,0x36,0x5c,0xd6,0x57,0xd5,0x57,0x35,0x5c,0x0f,0xf0 }
To get it into a an existing file, use the >>
operator. Like this:
w4 png2src --nelua fruit.png >> main.nelua
This will add the previous lines to your main.nelua
and it will shadow the previous fruit
variable. Just rename the new fruit to fruit_sprite
and move it somewhere else. Also: You can remove the other stuff added, you won't need it for this project:
local snake = Snake.init()local fruit: Point = { x = math.random(0, 19), y = math.random(0,19) }local frame_count = 0local prev_state = 0local fruit_sprite: [16]uint8 = { 0x00,0xa0,0x02,0x00,0x0e,0xf0,0x36,0x5c,0xd6,0x57,0xd5,0x57,0x35,0x5c,0x0f,0xf0 }
With that out of the way, it's time to actually render the newly imported sprite.
// TODO
// TODO
// TODO
Now you need to import the image. For this, the WASM-4 CLI tool w4
comes with another tool: png2src
. You can use it like this:
w4 png2src --rust fruit.png
This will output the following content in the terminal:
const FRUIT_WIDTH: u32 = 8;const FRUIT_HEIGHT: u32 = 8;const FRUIT_FLAGS: u32 = 1; // BLIT_2BPPconst FRUIT: [u8; 16] = [ 0x00,0xa0,0x02,0x00,0x0e,0xf0,0x36,0x5c,0xd6,0x57,0xd5,0x57,0x35,0x5c,0x0f,0xf0 ];
Let's copy FRUIT
and add it to our project;
we'll also rename it FRUIT_SPRITE
.
// src/game.rsuse crate::snake::{Point, Snake};use crate::wasm4;use fastrand::Rng;
const FRUIT_SPRITE: [u8; 16] = [ 0x00, 0xa0, 0x02, 0x00, 0x0e, 0xf0, 0x36, 0x5c, 0xd6, 0x57, 0xd5, 0x57, 0x35, 0x5c, 0x0f, 0xf0,];
pub struct Game { rng: Rng, snake: Snake, frame_count: u32, fruit: Point,}
Now you need to import the image. For this, the WASM-4 CLI tool w4
comes with another tool: png2src
. You can use it like this:
w4 png2src --wat fruit.png
This will output the following content in the terminal:
;; fruit;; fruit_width = 8;;; fruit_height = 8;;; fruit_flags = 1; // BLIT_2BPP
(data (i32.const ???) "\00\a0\02\00\0e\f0\36\5c\d6\57\d5\57\35\5c\0f\f0")
To get it into a an existing file, use the >>
operator. Like this:
w4 png2src --wat fruit.png >> main.wat
This will add the previous lines to your main.wat
and causes an error because it doesn't know where to put the data. Let's put it at 0x2638, after the fruit position:
(data (i32.const 0x2638) "\00\a0\02\00\0e\f0\36\5c\d6\57\d5\57\35\5c\0f\f0")
With that out of the way, it's time to actually render the newly imported sprite.
Now you need to import the image. For this, the WASM-4 CLI tool w4
comes with another tool: png2src
. You can use it like this:
w4 png2src --zig fruit.png
This will output the following content in the terminal:
const fruit_width = 8;const fruit_height = 8;const fruit_flags = 1; // BLIT_2BPPconst fruit = [16]u8{ 0x00,0xa0,0x02,0x00,0x0e,0xf0,0x36,0x5c,0xd6,0x57,0xd5,0x57,0x35,0x5c,0x0f,0xf0 };
To get it into a an existing file, use the >>
operator (or copy and paste). Like this:
w4 png2src --zig fruit.png >> main.zig
This will add the previous lines to your main.zig
and causes an error because "fruit" already exists. Just rename the new fruit to fruitSprite
and move it somewhere else. Also: You can remove the other stuff added, you won't need it for this project:
var snake = Snake.init();var fruit: Point = undefined;var frame_count: u32 = 0;var prev_state: u8 = 0;var random: std.rand.Random = undefined;const fruit_sprite = [16]u8{ 0x00,0xa0,0x02,0x00,0x0e,0xf0,0x36,0x5c,0xd6,0x57,0xd5,0x57,0x35,0x5c,0x0f,0xf0 };
With that out of the way, it's time to actually render the newly imported sprite.
#
Rendering a PNG FileRendering the sprite is rather simple. Just call the blit
function of w4:
// Blit draws a sprite at position `x`, `y` and uses DRAW_COLORS accordinglyfunction blit(spritePtr: usize, x: i32, y: i32, width: u32, height: u32, flags: u32): void;
In practice it looks like this:
export function update(): void { frameCount++
input()
if (frameCount % 15 == 0) { snake.update() } snake.draw()
w4.blit(fruitSprite, fruit.x * 8, fruit.y * 8, 8, 8, w4.BLIT_2BPP)}
But since you set the drawing colors, you need to change the drawing colors too:
snake.draw()
store<u16>(w4.DRAW_COLORS, 0x4320) w4.blit(fruitSprite, fruit.x * 8, fruit.y * 8, 8, 8, w4.BLIT_2BPP)
This way, w4 uses the color palette in its default configuration. Except for one thing: The background will be transparent.
Rendering the sprite is rather simple. Just call the blit
function of w4:
// blit draws a sprite at position x, y and uses DRAW_COLORS accordinglyvoid blit (const uint8_t* data, int32_t x, int32_t y, uint32_t width, uint32_t height, uint32_t flags);
In practice it looks like this:
void update () { frame_count++;
input();
if (frame_count % 15 == 0) { snake_update(&snake); }
snake_draw(&snake);
blit(fruit_sprite,fruit.x*8,fruit.y*8,8,8,BLIT_2BPP);}
But since you set the drawing colors, you need to change the drawing colors too:
snake_draw(&snake);
*DRAW_COLORS = 0x4320; blit(fruit_sprite,fruit.x*8,fruit.y*8,8,8,BLIT_2BPP);
This way, w4 uses the color palette in its default configuration. Except for one thing: The background will be transparent.
// TODO
Rendering the sprite is rather simple. Just call the Blit
function of w4:
// Blit draws a sprite at position X, Y and uses DRAW_COLORS accordinglyfunc Blit(sprite *byte, x, y int, width, height, flags uint)
In practice it looks like this:
//go:export updatefunc update() { frameCount++
input()
if frameCount%15 == 0 { snake.Update() }
snake.Draw()
w4.Blit(&fruitSprite[0], fruit.X*8, fruit.Y*8, 8, 8, w4.BLIT_2BPP)}
But since you set the drawing colors, you need to change the drawing colors too:
snake.Draw()
*w4.DRAW_COLORS = 0x4320 w4.Blit(&fruitSprite[0], fruit.X*8, fruit.Y*8, 8, 8, w4.BLIT_2BPP)
This way, w4 uses the color palette in its default configuration. Except for one thing: The background will be transparent.
Rendering the sprite is rather simple. Just call the blit
function of w4:
// Blit draws a sprite at position `x`, `y` and uses DRAW_COLORS accordinglyglobal function blit(data: *[0]uint8, x: int32, y: int32, width: uint32, height: uint32, flags: uint32)
In practice it looks like this:
local function update() frame_count = frame_count + 1
input()
if frame_count % 15 == 0 then snake:update() end
snake:draw()
blit(fruit_sprite, fruit.x * 8, fruit.y * 8, 8, 8, BLIT_2BPP)end
But since you set the drawing colors, you need to change the drawing colors too:
snake:draw()
$DRAW_COLORS = 0x4320 blit(fruit_sprite, fruit.x * 8, fruit.y * 8, 8, 8, BLIT_2BPP)
This way, w4 uses the color palette in its default configuration. Except for one thing: The background will be transparent.
// TODO
// TODO
// TODO
Rendering the sprite is rather simple. Just call the blit function of w4:
pub fn blit(sprite: &[u8], x: i32, y: i32, width: u32, height: u32, flags: u32);
In practice it looks like this:
// src/game.rs inside impl Game {} block pub fn update(&mut self) { self.frame_count += 1;
if self.frame_count % 15 == 0 { self.snake.update(); } self.snake.draw();
wasm4::blit( &FRUIT_SPRITE, self.fruit.x * 8, self.fruit.y * 8, 8, 8, wasm4::BLIT_2BPP, ); }
But since you set the drawing colors, you need to change the drawing colors too:
// src/game.rs inside impl Game {} block pub fn update(&mut self) { self.frame_count += 1;
if self.frame_count % 15 == 0 { self.snake.update(); } self.snake.draw();
set_draw_color(0x4320); wasm4::blit( &FRUIT_SPRITE, self.fruit.x * 8, self.fruit.y * 8, 8, 8, wasm4::BLIT_2BPP, ); }
This way, w4 uses the color palette in its default configuration. Except for one thing: The background will be transparent.
Rendering the sprite is rather simple. Just call the blit
function of w4:
;; Copies pixels to the framebuffer.(import "env" "blit" (func $blit (param i32 i32 i32 i32 i32 i32)))
In practice it looks like this:
(func (export "update") ...
;; Draw fruit. (call $blit (i32.const 0x2638) (i32.mul (i32.load (i32.const 0x2630)) (i32.const 8)) (i32.mul (i32.load (i32.const 0x2634)) (i32.const 8)) (i32.const 8) (i32.const 8) (i32.const 1)))
But since you set the drawing colors, you need to change the drawing colors too:
(func (export "update") ...
;; Set fruit colors. (i32.store16 (global.get $DRAW_COLORS) (i32.const 0x4320))
;; Draw fruit. (call $blit (i32.const 0x2638) (i32.mul (i32.load (i32.const 0x2630)) (i32.const 8)) (i32.mul (i32.load (i32.const 0x2634)) (i32.const 8)) (i32.const 8) (i32.const 8) (i32.const 1)))
This way, w4 uses the color palette in it's default configuration. Except for one thing: The background will be transparent.
Rendering the sprite is rather simple. Just call the blit
function of w4:
/// Copies pixels to the framebuffer.pub extern fn blit(sprite: [*]const u8, x: i32, y: i32, width: i32, height: i32, flags: u32) void;
In practice it looks like this:
export fn update() void { frame_count += 1;
input();
if (frame_count % 15 == 0) { snake.update(); } snake.draw();
w4.blit(&fruit_sprite, fruit.x * 8, fruit.y * 8, 8, 8, w4.BLIT_2BPP);}
But since you set the drawing colors, you need to change the drawing colors too:
snake.draw();
w4.DRAW_COLORS.* = 0x4320; w4.blit(&fruit_sprite, fruit.x * 8, fruit.y * 8, 8, 8, w4.BLIT_2BPP);
This way, w4 uses the color palette in its default configuration. Except for one thing: The background will be transparent.