Sniff is a "Scratch-like" programming language that's designed to help Scratchers move gently from Scratch to more conventional languages. They can start writing programs, without having to learn a new language because Sniff is based on Scratch. They learn a little more about variables, compiling, syntax errors (!), and they can have fun controlling real hardware while they're doing it.

Wednesday, 18 February 2015

Adventures in Minecraft (ch 3)

Last post I started working through Adventures in Minecraft (available from all good book sellers), using Sniff instead of Python. I covered Chapters 1&2, displaying messages on the Minecraft screen, and detecting the players position. In chapter 3 we start building stuff with blocks.

One of the more basic Python examples places a block of stone next to the player:

import mcpi.minecraft as minecraft
import mcpi.block as block
mc = minecraft.Minecraft.create()
pos = mc.player.getTilePos()
mc.setBlock(pos.x+3, pos.y, pos.z, block.STONE.id)

In Sniff this becomes:
make world minecraft device
make mcX number
make mcY number
make mcZ number
make blockType number
make blockData number

when start
.tell world to "getPos"
.change mcX by 3
.set blockType to 1
.tell world to "setBlock"

Sniff doesn't have a list of block types programmed into it, so we need to specify stone as block type 1 (Air is 0, earth/grass is 2 - the rest you can look up!). As we saw last time, the Sniff code has more lines, but avoids a lot of bizarre syntax. Static typing and declarations mean that doing something dumb gets flagged by the compiler rather doing something odd at runtime.

This is JavaScript which is the worst. But it makes clear why dynamic typing is bad!

If we skip the import/device declarations, which are getting a bit tedious, the next thing to do is build a tower:

when start
.tell world to "getPos"
.change mcX by 3
.set blockType to 1
.repeat 50
..tell world to "setBlock"
..change mcY by 1

and in Python:

pos = mc.player.getTilePos()
for a in range(50):
    mc.setBlock(pos.x+3, pos.y+a, pos.z, block.STONE.id)

Sniff lacks a for loop, because Scratch doesn't have one. Technically it would be easy enough to add one, but its really hard to describe a for loop in words, in a way that actually makes sense to a non-programmer. In this case though the repeat 50 makes it clear we want to place 50 blocks, and change mcY by 1 makes it clear that they're going to be in a vertical column. I'm not sure the same can be said for the Python version.


At this stage its useful to be able to clear a space in the world to build some stuff - or at least demolish some of the rubble that's left around from your earlier experiments. In Python thats:

pos = mc.player.getTilePos()
size = int(raw_input("size of area to clear? "))
mc.setBlocks(pos.x, pos.y, pos.z, pos.x+size, pos.y+size, pos.z+size,
             block.AIR.id)


But in Sniff I didn't implement the setBlocks function of the Minecraft API. It seemed a bit redundant. Better to learn how to clear an area by getting some practise with loops:

.ask "size of area to clear? " and wait
.set size to value of answer
.
.tell world to "getPos"
.set blockType to 0
.
.repeat size
..repeat size
...repeat size
.... tell world to "setBlock" 
....change mcY by 1
...change mcY by -size
...change mcX by 1
..change mcX by -size
..change mcZ by 1


It's better to for students to work this one out for themselves, rather than just have a pre-built version provided. It's a little slower, but I think its worth it to have the code visible. Having written our own version  its useful to separate it out into a fill script, which we can include in future programs:

make sizeX number 
make sizeY number 
make sizeZ number 
when fill
.repeat sizeZ
..repeat sizeX
...repeat sizeY
.... tell world to "setBlock" 
....change mcY by 1
...change mcY by -sizeY
...change mcX by 1
..change mcX by -sizeX
..change mcZ by 1 
.change mcZ by -sizeZ

As you probably know Scratch 1.4 (the version that runs on Pi) doesn't have functions, but rather scripts provide the same core functionality of breaking code into manageable/reusable chunks. The semantics are a little different between a broadcast and a function call, but for a new programmer, the important concept of modularisation is the same. Scratch and Sniff also lack parameters (or local variables) which ultimatly limits the size of the project that can be managed, but for small projects like these its not a problem. Scope is a concept that students often struggle to grasp, so being able to defer it for a while is probably for the best. In fact I think getting in a mess with too many globals is a necessary mistake for everyone to make. Only when you're totally confused do you realise why you need scoping. 

We can use this as part of the next exercise to build a house. This starts by filling a large volume with cobblestone, and then hollowing it out by replacing the interior with air.

make size number
make oX number
make oY number
make oZ number

when start
.set size to 20
.tell world to "getPos"
.set oX to mcX + 2 
.set oY to mcY 
.set oZ to mcZ
.set mcX to oX
.set mcY to oY
.set mcZ to oZ
.set sizeX to size
.set sizeY to size
.set sizeZ to size
.set blockType to 4
.broadcast fill and wait 
.
.change mcX by 1 
.change mcZ by 1 
.change sizeX by -2
.change sizeY by -1
.change sizeZ by -2
.set blockType to 0
.broadcast fill and wait 

In Python that's much shorter:
SIZE = 20
pos = mc.player.getTilePos()
x = pos.x + 2
y = pos.y
z = pos.z
mc.setBlocks(x, y, z, x+SIZE, y+SIZE, z+SIZE, block.COBBLESTONE.id)
mc.setBlocks(x+1, y, z+1, x+SIZE-2, y+SIZE-1, z+SIZE-2, block.AIR.id)

Generally shorter is good, and as an experienced programmer having to set up every parameter by assigning it to a named variable feels slow and clunky. However setBlocks() takes 7 parameters. It's way too easy to get one of them wrong, miss one out, swap a couple over (and it won't necessarily complain because its polymorphic! sometimes it takes 8 parameters. We won't be covering polymorphism in KS3 thanks!). Breaking the variables out a line at a time means you can think about them one at a time, and you can assign them in any order. This will also help if you're pair programming or explaining the code to someone, as you can refer to each assignment by line, rather than having them all in the same place.

BuildHouse continues on in a similar fashion. It's actually a surprisingly hard exercise - or at least its hard to rush it. It requires systematic planning and thinking about the coordinates. I got myself in a mess in a few places where I thought I could rush it and ended up getting confused.

.#Carpet
.set mcY to oY-1
.set sizeY to 1
.set blockType to 35
.set blockData to 14
.broadcast fill and wait 
.
.#roof
.set mcX to oX
.set mcY to oY+size
.set mcZ to oZ
.set sizeX to size
.set sizeY to 1
.set sizeZ to size
.set blockType to 17
.broadcast fill and wait
.#MAKE HOLES IN FRONT WALL
.set mcZ to oZ
.set sizeZ to 1 
.
.#DOOR
.set mcX to oX+size/2 -1
.set mcY to 0
.set sizeY to 0
.set sizeX to 2
.set sizeY to 3
.set blockType to 0
.broadcast fill and wait 
.
.#WINDOWS
.set mcY to oY+size/2+3
.set sizeX to size/2-6
.set sizeY to size/2-6
.set blockType to 20
.
.set mcX to oX+3
.broadcast fill and wait
.set mcX to oX+size/2+3
.broadcast fill and wait 

Note that for the carpet we've used blockData to set the colour of the wool.

The code is much longer in Sniff, as we have to assign the positions to the variables mcX, mcY, mcZ. However there's an unexpected bonus in defining the positions this way. The code for the windows in Python is:

mc.setBlocks(x+3, y+SIZE-3, z, midx-3, midy+3, z, block.GLASS.id)
mc.setBlocks(midx+3, y+SIZE-3, z, x+SIZE-3, midy+3, z, block.GLASS.id)

There's no clear relationship between the position of the two windows, when in the Sniff code we position the windows in Z and Y once, and select glass once. We can then place windows along the house all at the same level, just by changing then their X position. We could change all the windows to glass panes with a single change rather than changing every one.

Once that's done, we can move all of that into a script for the final example from this chapter: building a street of houses.


when start
.set size to 20
.tell world to "getPos"
.set oX to mcX + 2 
.set oY to mcY 
.set oZ to mcZ
.
.repeat 5
..broadcast house and wait
..change oX by size

And in Python:
pos = mc.player.getTilePos()
x = pos.x + 2
y = pos.y
z = pos.z
  
for h in range(5):
    house()
    x = x + SIZE

These are virtually identical, and yet there are still pitfalls in the Python for an inexperienced programmer.

No comments:

Post a Comment