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.

Tuesday, 24 February 2015

Adventures in Minecraft (Chapter 4)

Chapter 4 of the book Adventures in Minecraft, is called "interacting with blocks" and brings together the previous two chapters to make blocks appear and disappear in reaction to the player.

The first example is called safeFeet and simply detects whats under the player to decide if he's standing on something solid.

make world minecraft device
make networkPeer string
make message string
make mcX number
make mcY number
make mcZ number
make blockType number
make blockData number

when start
.set networkPeer to "raspberrypi.local."
.forever
..wait 0.5 secs
..broadcast safeFeet and wait

when safeFeet
.tell world to "getPos"
.change mcY by -1
.tell world to "getBlock"
.if blockType = 0 or blockType=8 or blockType=9
..set message to "not safe"
.else
..set message to "safe"
.tell world to "show"


That's pretty simple (and we've put it in a separate script to be "nice"). We simply get the position of the player, and then ask for the block underneath our current position. If that block is either air or water, then we're not safe, otherwise we're safe.

From there its a small step to replace that brick with something else, so that if we're in the air, we create something to stop us falling:

when magicBridge
.tell world to "getPos"
.change mcY by -1
.tell world to "getBlock"
.if blockType = 0 or blockType=8 or blockType=9
..set blockType to 20
..tell world to "setBlock"

For the final trick in this series of exercises, when we create a block we add its position to a list. Then when we get back to safety we go through and delete the blocks:

make bridge list of number

when vanishingBridge
.tell world to "getPos"
.change mcY by -1
.tell world to "getBlock"
.if blockType = 0 or blockType=8 or blockType=9
..set blockType to 20
..tell world to "setBlock"
..add mcX to bridge
..add mcY to bridge
..add mcZ to bridge
.else
..if (not blockType=20) and length of bridge>0
...set mcX to item 1 of bridge
...set mcY to item 2 of bridge
...set mcZ to item 3 of bridge
...delete item 1 of bridge
...delete item 1 of bridge
...delete item 1 of bridge
...set blockType to 0
...tell world to "setBlock"
...wait 0.25 secs

In the Python version we have structures, so we can create a list of points:

def buildBridge():
  pos = mc.player.getTilePos()
  b = mc.getBlock(pos.x, pos.y-1, pos.z)
 if b == block.AIR.id or b == block.WATER_FLOWING.id or
                     b ==block.WATER_STATIONARY.id:
    mc.setBlock(pos.x, pos.y-1, pos.z, block.GLASS.id)
    coordinate = [pos.x, pos.y-1, pos.z]
    bridge.append(coordinate)
  elif b != block.GLASS.id:
    if len(bridge) > 0:
      coordinate = bridge.pop()
      mc.setBlock(coordinate[0], coordinate[1], coordinate[2], block.AIR.id)
      time.sleep(0.25) 

This is conceptually a little better, as the 3d coordinates are stored in a single element of the "bridge" list, but doesn't really make much difference. Also in the Sniff version we manually add elements to the end, and remove from the beginning, while in Python we use append and pop. Both of these add an extra layer of "something to learn/remember" rather than "something to understand", so they're not  necessarily a clear cut benefit.

There is one further gotcha... Sniff stores the players position to its full sub-block accuracy. This works fine most of the time, but if we're at x=10.9 we're actually still in square 10. When we place a block 10.9 gets rounded to 11, it ends up in the wrong place, and we fall (this is besides the issues of simply not placing the blocks fast enough!). If we add

.set mcX to round (mcX - 0.5)
.set mcY to round (mcY - 0.5)
.set mcZ to round (mcZ - 0.5)

after getPos, then this rounds the value down to the nearest whole block, rather than doing regular up/down rounding, and the bridge works a little better.


The next section deals with hitting things with the sword. This is pretty specific and took me a while to figure out - it HAS to be the sword, and it has to be HITTING (right mouse) not smashing (left).

make diamondX number
make diamondY number
make diamondZ number

when checkHit
.tell world to "getHits"
.if blockType>0
..if mcX=diamondX and mcY=diamondY and mcZ=diamondZ
...set message to join "Hit " [blockData]
...tell world to "show"


when start
.set networkPeer to "raspberrypi.local."
.
.tell world to "getPos"
.change mcX by 4
.
.set mcX to round mcX
.set mcY to round mcY
.set mcZ to round mcZ
.
.set blockType to 57
.tell world to "setBlock"
.
.set diamondX to mcX
.set diamondY to mcY
.set diamondZ to mcZ
.
.forever
..wait 0.25 secs
..broadcast checkHit and wait

So here we create a diamond (57) block and then wait for the player to hit it. Once again we round the players position to a whole number so we can be sure that we're acctually placing the block where we think we are the block. After placing the block we just keep calling getHit to see if anything has happened. The exact values returned by getHits are a little vague - It's not well documented in the minecraft pi docs what blockType will be, but 0 means we didn't hit anything! blockData is used to tell us which face of the diamond we hit - try it and see.

The chapter concludes with a longer example called Sky Hunt which combines the previous exercises to make larger game. I've not implemented it, as I was keen to move on to chapter 5 which I'll post later in the week.

Once again thanks to the Authors for writing the original Python versions. Go buy the book for a fuller explanation of whats going on here.

Monday, 23 February 2015

OCR GCSE Computing A453 in Sniff

Let me preface this post with a disclaimer. I'm do not teach GCSE Computing. I know very little about the marking and assessment criteria for OCR examinations. I'm a programmer, University lecturer, and researcher. I develop Sniff. I do not have any insight into the workings of OCR. The code presented here may or may not be assessed favourably if it were presented for assessment. In fact I'm pretty sure that the actually code is a small part of the OCR assessment. Analysis, testing and documentation most likely each carry as much weight as the program does. While I obviously did those elements in order to complete the tasks here I'm just considering the code (and if it was for an exam I'd put more comments in!), so don't consider these model answers - they're just stuff I wrote!

With that out the way... I've heard protests from teachers that some schools were using Scratch almost exclusively for all work through to GCSE. They considered this ridiculous dumbing down, and students need to learn Python, Java, BASIC, COBOL or some other "proper" language.

While I think Scratch is powerful, I think it probably is a bit limiting to be using it though to the end of KS4. Personally (and lets face it we're pretty biased in this), I would consider Scratch great for KS2, and then migrate kids to Sniff as they're ready in KS3. For KS4 I think the argument that they need to be using something "more powerful" holds up for kids who are progressing well, and are likely to go on to A Level and/or are likely to program seriously/professionally in later life. However I was interested in how well Sniff holds up at KS4 - for less strong programmers, do we need to disrupt their learning by hopping them between programming languages when basic concepts are more important.


To that end I downloaded the OCR Exemplar Work  (see pages 33 & 34) which sets out three programming tasks which are supposed to be representitive of GCSE programming assignments. I was interested to see how easily these could be implemented in Sniff.  These are obviously not the assignments in current use (I hope! I'm sure teachers remember last years debacle!), and in fact are quite old, but are probably still indicative.

Task 1 : Animal Ages

We're asked to enter a type of animal (cat or dog), and then convert their animal age to human years using a provided formulae.

make isDog boolean
make animalAge number
make humanAge number

when start
.repeat until answer="cat" or answer="dog"
..ask "Animal Type?" and wait
.
.set isDog to (answer="dog")
.
.ask "age (animal years)?" and wait
.set animalAge to value of answer
.
.if isDog
..broadcast doDog and wait
.else
..broadcast doCat and wait
.say [humanAge]

when doDog
.set humanAge to 0
.if animalAge > 2
..change humanAge by (animalAge-2)*4
..set animalAge to 2
.
.change humanAge by animalAge*11

when doCat
.set humanAge to 0
.if animalAge > 2
..change humanAge by (animalAge-2)*4
..set animalAge to 2
.
.if animalAge > 1
..change humanAge by (animalAge-1)*10
..set animalAge to 1
.


.change humanAge by animalAge*15

The OCR document include model answers, but its unclear if these are good, average or poor models! Here we've taken a slightly more elegant approach of starting with the older age ranges and working backwards - this is more important for the cat which has three age bands. The model answers also may or may not handle fractional ages - the docs say they don't though the code looks like it might. Its unclear if this is a requirement.

Task 2: System Password

For this task we need to read in a password, check that its at least 6 and not more than 12 characters. The task then proposes a mechnism for assessing password strength. It's actually slightly ambiguous as the mechanism isn't particularly smart, and the wording doesn't say you should use that approach, just that its and approach! You could easily do something better, but I really think we should do it their way.

make testCharacters string #characters we're looking for
make charFound boolean   #return value
make answerIndex number   #how far through answer
make setIndex number   #how far through char list

when checkChars
.set charFound to no
.set answerIndex to 1
.repeat length of answer
..set setIndex to 1
..repeat length of testCharacters
...if letter answerIndex of answer = letter setIndex of testCharacters
....set charFound to yes
....stop script #optional
...change setIndex by 1
..change answerIndex by 1



make score number
make lengthOK boolean
when start
.repeat until length of answer > 5 and length of answer<13
..ask "Password?" and wait
..if length of answer <6
...say "Too Short"
..if length of answer >12
...say "Too Long"
.
.say "length OK"
.
.set score to 0
.
.set testCharacters to "abcdefghijklmnopqrstuvwxyz"
.broadcast checkChars and wait
.if charFound
..say "lower case OK"
..change score by 1
.
.set testCharacters to "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
.broadcast checkChars and wait
.if charFound
..say "Upper case OK"
..change score by 1
.
.set testCharacters to "0123456789"
.broadcast checkChars and wait
.if charFound
..say "number OK"
..change score by 1
.
.if score=1
..say "weak"
.if score=2
..say "medium"
.if score=3
..say "strong"

In their model answers (written in what looks a bit like BBC BASIC) they can convert letters to their ASCII code, and compare them against the hardcoded values for the upper/lower/numeric range. This is poor practice, but we can't do it that way in Sniff anyway as there's no way to get ASCII values. We can compare strings, so that "b">"a", but probably not a good idea. Instead we start by writing a script to check if any of the letters of "answer" are in the string "testCharacters". This adds to the complexity a bit, but makes the code more flexible and portable.

The rest is pretty straight forwards and obvious. We loop till we get an answer of the right length, then check for each of the letter groups, incrementing score if its found. Finally we print out how got the score is.

Task 3: Hi Score Table

This is probably the most complex task. Load a set of names, and scores from a file, edit them, and save them out again. None of this is particularly difficult, but I felt the task was poorly specified. The question was specific enough that the examiners obviously had a particular set of tasks and style of operations in mind, but they didn't actually say what they were! For example "locate records by name or hi-score". What exactly does each locate by hi-score mean? I took it to mean just find the highest score, but it could mean display a hi score table, sorted into order, or even allow it to be queried for scores that match a certain pattern (>100 and the last digit is 3?). The model answer prints out a list of  all the scores, not sorted, and mention in its analysis that they didn't get them sorted. However "display a sorted list" isn't a specified requirement. I don't know, so I chose the option to display the name of the player with the highest score.

In part because I was uncertain of the requirements (but also because its good practise) I build scripts to handle all of the operations:

make names list of string
make scores list of number

make nativeFile device
make fileData string
make fileOK boolean

when readFile
.delete all of names
.delete all of scores
.set fileData to "score.txt"
.tell nativeFile to "startRead"
.if not fileOK
..say "File not loaded (will be created)"
.repeat until not fileOK
..tell nativeFile to "readString"
..if fileOK
...add fileData to names
...tell nativeFile to "readString"
...add value of fileData to scores
.tell nativeFile to "endRead"

make count number
when writeFile
.set fileData to "score.txt"
.tell nativeFile to "startWrite"
.if not fileOK
..stop script
.
.set count to 1
.repeat length of names
..set fileData to item count of names
..tell nativeFile to "writeString"
..set fileData to [item count of scores]
..tell nativeFile to "writeString"
..change count by 1
.tell nativeFile to "endRead"

make playerName string
make playerNumber number

make bestScore number
when showHiScore
.if length of scores=0
..say "No scores recorded"
..stop script
.
.set count to 1
.set playerNumber to 1
.set bestScore to 0
.repeat length of scores
..if item count of scores>bestScore
...set playerNumber to count
...set bestScore to item count of scores
..change count by 1
.
.say "best score:"
.say item playerNumber of names
.say [item playerNumber of scores]
.say ""


when findPlayer
.set playerNumber to 1
.repeat until playerNumber > length of names
..if playerName = item playerNumber of names
...stop script
..change playerNumber by 1
.set playerNumber to 0

when createPlayer
.broadcast findPlayer and wait
.if not playerNumber = 0
..say join playerName " already exists" 
..stop script
.
.add playerName to names
.add 0 to scores
.set playerNumber to length of names
.say join "added " playerName

when deletePlayer
.broadcast findPlayer and wait
.if playerNumber = 0
..say "no such player"
.else
..delete item playerNumber of names
..delete item playerNumber of scores
..say join"deleted " playerName

make newScore number
when updatePlayer
.broadcast findPlayer and wait
.if playerNumber = 0
..say "no such player"
.else
..replace item playerNumber of scores with newScore
..say join"updated " playerName

when showScoreForPlayer
.broadcast findPlayer and wait
.if playerNumber = 0
..say "no such player"
.else
..say join "Current score:" [item playerNumber of scores]



Then I wrote a "start" script to link them all together.

when start
.broadcast readFile and wait
.
.repeat until answer = "6"
..say ""
..say "1) show highest score"
..say "2) check players score"
..say "3) create player"
..say "4) update player"
..say "5) delete player"
..say "6) quit"
..ask "enter option:" and wait
..
..if answer = "1"
...broadcast showHiScore and wait
..
..if answer = "2"
...ask "Enter Playername:" and wait
...set playerName to answer
...broadcast showScoreForPlayer and wait
..
..if answer = "3"
...ask "Enter Playername:" and wait
...set playerName to answer
...broadcast createPlayer and wait
..
..if answer = "4"
...ask "Enter Playername:" and wait
...set playerName to answer
...ask "new score:" and wait
...set newScore to value of answer
...broadcast updatePlayer and wait
..
..if answer = "5"
...ask "Enter Playername:" and wait
...set playerName to answer
...broadcast deletePlayer and wait
...
.
.broadcast writeFile and wait
.if fileOK
..say "Data saved"
.else
..say "write error"

In part the problem with this task is that it makes no sense. If this were a "real" task, then the back end scripts would be called by a game of some kind, so the main script is really more of a test script.

Review

So there we are - three tasks from OCR computing, written in Sniff, without any issues. So lets review what we learnt. Sniff is perfectly capable of completing these tasks, in a clean and clear way. Sniff's main limitations are lack of "real" functions and limited data structures. The data structures weren't an issue at all - they weren't needed for code of this compexity (I could have recorded a hi score as a structure containing a name and a score, but that would have been overkill, and the existing solution worked well). In terms of use of functions, task 3 would have been a little clearer with proper parameters, but it wasn't a significant issue. I used scripts to modularise all of the tasks, while none of the model answers provided by OCR used Procedures, even though BBC BASIC supports them very well (including parameters, and local variables).

Tasks 1 presented no issues that Sniff didn't handle trivially.

Task 2 raised the minor issue of Sniff not giving access to low level ASCII representation of characters (or in fact necessarily using ASCII at all). The way we've done it is a bit more code, but is clearer, more explicit and ultimately better, as its not character set dependant, and can be extended to handle arbitrary groups of characters and symbols. (if I was doing this in "real" work I'd use C functions like "isUpper()"). It's certainly clearer than the model answers "If c >=65" style solution.

Task 3 again raised no signifigant issues, though true functions would have helped. In fact it would be harder in BASIC or C which use array data types rather lists.

Tasks 1&2 could both be completed using essentially identical code in Scratch. Task 3 couldn't be done in Scratch as it explicitly requires file access. However other than the readFile and writeFile scripts, the actually work could be done in Scratch. This raises the interesting option of developing and prototyping (parts of) the code in Scratch, and then finalising in Sniff. There is no automatic mechanism for going between the two, but I've found it useful to keep Scratch installed, and use it to show fragments of Sniff code to students, in visual form. (As a slight aside, another exemplar specifically required implementation in Scratch - that seems a bit odd, as the assignments are supposed to be language neutral).

In both Tasks 2&3 the lack of a for loop in Sniff makes things a little more complex. Instead we need to write:

.set index to 1
.repeat 50
..do stuff with index
..change index by 1

While this is nice and clear, its a bit long winded, and its easy to forget to increment index each time around. So far we've avoided "extending" Sniff more than absolutely necessary. One of the main issues in adding a for loop is that its quite hard to figure out what it would look like. However here we can see a possible notation emerging:

.repeat 50 using index
..do stuff with index

Where the loop would go around 50 times, with index being set from 1 to 50. I quite like this, and it would make the code to both Tasks 2 & 3 clearer and simpler. Similar code frequently crops up in the Minecraft examples.

[UPDATE: This syntax is now supported in Sniff 16 and later. Updated versions of the code presented here which uses these features is included in the Sniff download]

Conclusion

The results are in - Sniff can easily complete the OCR GCSE Computing programming tasks. Any issues are minor, and likely to be no worse than quirks found in other languages. While there are good reasons to move strong students on to a "real" language, for students who are struggling to meet the "two languages, on of them text based" requirement, then Sniff is perfectly capable.

Sunday, 22 February 2015

Wedo Thermostatic Fan

Now we've got a thermometer for the wedo, we can start doing some fun things!

The most obvious is a wedo thermostat which controls a fan. I quickly hooked up a couple of lego propeller blades to a medium PF motor (like the one in the standard wedo kit), and attached that to channel 2 on the wedo, and attached the wedo temperature sensor to channel 1.

Calibration is definitely a bit dodgey, but between sitting on my desk and grasping the sensor in my hand I got raw readings of between 0.6 and 0.7 (your sensor might report differently, so check for yourself).

make wedo device
make wedoConnector number
make wedoValue number

when start

.forever
..set wedoConnector to 1
..tell wedo to "readRaw"
..#say [wedoValue]
..
..set wedoValue to (wedoValue-0.6)*10
..if wedoValue <0 
...set wedoValue to 0
..if wedoValue >1 
...set wedoValue to 1
..#say [wedoValue]
..
..set wedoConnector to 2
..tell wedo to "setMotorSpeed"

..wait 0.5 secs

All we need to do is subtract 0.6 and multiply by 10 to get a value that should be between 0 and 1. Just in case it goes outside this range we check and clamp the value - otherwise the fan could start going backwards (which isn't going to warm the room up!).

Having built version 1.0, I realised it wasn't actually moving much air - everything was working as it was supposed to,  but the blades just weren't moving fast enough. On a normal project this would be a problem, but... it's LEGO! another minute rummaging through the parts bucket and I'd made a gearbox, which sped up the fan so that it was at least moving some air!





If you hold the sensor in your hand the fan speeds up (as it thinks it's hot), and if you cool it, it slows down! AWESOME!

For extra awesome you can piggyback the PF Servo on the back of the motor, connect a pointer to it, and it gives you an analog(ish) temperature readout!

Friday, 20 February 2015

Wedo Thermometer

Minecraft has been a lot of fun the last week to so, and I'll be back with more of it next week, but I thought it was time to get back to basics, play with some Lego, and do some science!

The Wedo has amazing untapped potential as a simple and convenient way of connecting computers and Lego. It's based on the Lego Power Functions system which allows it to drive motors, and lights, but it also can use special Wedo sensors. Unfortunately Lego only make two sensors: the distance sensor and the tilt sensor. While that's a great start they're really missing out on what could be an amazing science tool, if only more stuff could be hooked up easily.

Well as it turns out, it can... provided you're prepared to do a little DIY work.The wired of the Wedo cable are (from bottom to top) 0V, C1, C2, and 9V. When driving a motor C1 and C2 are used to provide power to the motor, while sensors (and the IR remote) draw power from the 0/9V lines, and signal the Wedo hub by applying a voltage to C1 and C2. The C1 wire is used to tell the software what kind of thing is attached, while the C2 line is used to provide the reading. The hub converts both voltages to a number between 0 and 255, and sends this over USB to the computer.

Sniff doesn't really use the C1 line, as it doesn't auto detect like the lego software does (though you can use the wedoScan command to read that value and check whats plugged in). If we can generate a suitable voltage and apply it to C2 we can hook anything we like to the Wedo!

One of the things I keep coming back to over and over is measuring temperature. If we could use this approach to make a Wedo thermometer we could start doing more science with lego. And it turns out to be really cheap and easy.

Ingredients:

1xLego Power Functions extension cable #8886(£2.99 from Lego)
1x10K NTC Thermister (about £1 on ebay - treat yourself to the waterproof version)
1x10K resistor (about £0.02)
1xcup of hot water (available around the home)
1xcup of ice (available around the home)
1xglass of red wine (available around the home)
A regular thermometer

Cut the power functions cable about an inch from the end THAT DOESNT PLUG INTO THE WEDO. This might be handy for something, so don't cut that end too short, but we can put the short end in the spares box for now, and keep the cable that fits the Wedo hub.

Laying it flat with the bare end of the cable to the right, solder the 10K resistor between the bottom wire (earth) and third wire (C2). Wire the Thermistor between the third wire and the 4th/top wire (9v).

Check nothing is shorting out, and plug into the wedo. It should report that nothing is connected, but it you read a value back you should see you're getting a reading that varies with temperature. I added a readRaw function to the Sniff wedo device to work with unofficial hardware, so I could write the code:

make wedo device
make wedoConnector number
make wedoValue number

when start
.forever
..set wedoConnector to 2
..tell wedo to "readRaw"
..say [wedoValue]

..wait 0.5 secs

(this won't compile in Sniff 14 - you'll need to wait for 15). Warming and cooling the device by just holding it tightly shows that the value changes with temperature.

Now drink the glass of red wine. This stage is strictly optional, but highly recommended (unless you're under 18 or live in the USA, in which case... you know... drugs are bad).

Now we need to calibrate it.


I used a ds18 thermometer connected to an Arduino and put both the new sensor and the ds18 into first ice and then a cup of hot water, recording the values on each.

0.87=50.25deg
0.5607=1.13deg

Now we just need to fit a straight line through that (these things are supposed to be pretty linear, but we'll see!).

50.25 =0.87 *m+c
1.13= 0.56*m+c

Subtract:

49.12 1=0.31*m

m=49.12/0.31

m=158.4

Substitute into either equation:

c=50.25-0.87*158.4

c=-87.56

So to convert from a reading to a temperature we multiply by 158.4 then subtract 87.6.

Just sitting on the bench I got a reading of 0.67, which would be 18.5 degrees - about right. 


The only thing left to do is revisit the coffee going cold experiment. There are a couple of versions of coffee.sniff included in the current Sniff release, and its one of the very first science experiments I did with Sniff. I hooked up a ds18 thermometer to an Arduino, attached a tft screen, and plotted a graph as the sensor recorded the temperature of my coffee as it went cold. I later repeated it with a Raspberry Pi.

I reworked the code to use the Wedo sensor, and plot the results in a window. The code is 99% the same as before.

One of the first things that becomes clear is that we're never going to get a lot of accuracy - the wedo is an 8 bit analog to digital converter, which in this case means we're going to only get readings in about 0.5 degree steps, even if our calibration is spot on.

That means we get gaps in the graph, so it doesn't look quite so pretty as the ds18. However it does show the slight curve we expect from this experiment.  We could probably tweak the components to get a slightly better accuracy, but realistically we need to treat the Wedo as what it is - a fun and easy way to hook things up and try things out - not a precision instrument. It would be perfectly good for tracking the temperature of the classroom to see show it getting hotter and colder throughout the day.

We can hook a light sensor up in pretty much the same way... More on that another time.



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.