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.