-
Notifications
You must be signed in to change notification settings - Fork 0
Tutorial: Writing a Guessing Game in Novika
You'd need to build or install Novika.
Printing in Novika is known as echo
ing. echo
picks up (pops) one form from the stack, and displays it in the console.
Make a Novika source file, guess.nk, and enter the following code:
'Hello, World' echo
Next, run the file:
$ novika guess.nk
Hello, World
Congratulations! You've written a Novika program.
Let's change the "Hello, World" message to a welcome banner for our upcoming guessing game.
'Guess the number 0-100' echo
'Hit Ctrl-C or Ctrl-D to quit' echo
We use multiple echo
s to output multiple lines.
To ask the user to enter something, in our case a number, we can use readLine
. readLine
picks up one form from the stack, whose quote (aka string) version will be used as the prompt. readLine
leaves one or two forms on the stack. The top form is always a boolean, that is, a yes/no, for whether the user accepted (true
/yes) or rejected (false
/no) the prompt. If the user accepted the prompt, the second-from-top form will be the user's answer.
'Guess the number 0-100' echo
'Hit Ctrl-C or Ctrl-D to quit' echo
'> ' readLine
stack echo
Running the above, we get
$ novika guess.nk
Guess the number 0-100
Hit Ctrl-C or Ctrl-D to quit
> 12
[ '12' true ]
$ novika guess.nk
Guess the number 0-100
Hit Ctrl-C or Ctrl-D to quit
> <Ctrl-C or Ctrl-D>
[ false ]
Note how the program exits after we enter something, regardless of whether we accepted or rejected the prompt. The easiest way to fix that is to wrap our readLine
in an infinite loop, and make it so that when the prompt is rejected, the loop breaks (that is, stops looping).
loop
is the best way to set up an infinite loop. Its prefix form, loop:
, is even better, because it makes the code a bit more readable.
'Guess the number 0-100' echo
'Hit Ctrl-C or Ctrl-D to quit' echo
loop: [
'> ' readLine not => break
stack echo
]
Running this program, we get:
$ novika guess.nk
Guess the number 0-100
Hit Ctrl-C or Ctrl-D to quit
> 12
[ '12' ]
> 34
[ '34' ]
> <Ctrl-C or Ctrl-D>
As you can see, the number we enter is on the stack. But it's a quote (observe the '
s)! Novika numbers are called decimals. Let's use parseDecimal
to convert a quote into a decimal.
'Guess the number 0-100' echo
'Hit Ctrl-C or Ctrl-D to quit' echo
loop: [
'> ' readLine not => break
parseDecimal echo
]
Try running the above. The result won't be much different, but now the number we enter is actually a Novika decimal under the hood. This allows us to do math with it, compare it with other decimals, etc.
Our guessing game will need a random number that the player will have to guess. Novika provides the word L randTo: H
; let's use it to generate the number. In our guessing game, we'll have a fixed random number range 0 (our Low) - 100 (our High). Feel free to tweak these numbers to your liking, or generate them randomly too.
To get yourself acquainted with randTo:
, you can try playing with it in the REPL:
$ novika repl
>>> 0 randTo: 100
[ 8 ]
>>> 0 randTo: 1 "flip a coin"
[ 8 0 ]
>>> 0 randTo: 1
[ 8 0 1 ]
>>> 0 randTo: 1
[ 8 0 1 0 ]
The numbers you'll get will probably be different.
Let's generate the number in guess.nk, above the loop.
'Guess the number 0-100' echo
'Hit Ctrl-C or Ctrl-D to quit' echo
0 randTo: 100 p $: number
loop: [
'> ' readLine not => break
parseDecimal echo
]
The word $:
picks up the form on top of the stack, and saves it in the block where it was opened (read: evaluated) under the name that follows. $:
functions similarly to variable =
in other languages.
Note how we use p
(found before $:
) for inline debug echo.
If you run guess.nk multiple times in a row, you'd get different numbers each time (unless you're really lucky).
Next, we'll need three branches. If the user entered a number less (branch 1) or more (branch 2) than the number that we've generated, we'd like to echo "More" or "Less", correspondingly. However, if the numbers are equal (branch 3), then we'd like to end the game and tell the user they've guessed the number.
choose
comes to the rescue. It's the high-level way to do such kind of branching in Novika. choose
expects a block to be at the top of the stack. This block must contain an even number of forms, where Nth form is the condition, and N + 1th is the body. choose
then matches each such "arm" against the second-from-top form.
'Guess the number 0-100' echo
'Hit Ctrl-C or Ctrl-D to quit' echo
0 randTo: 100 p $: number
loop: [
'> ' readLine not => break
parseDecimal
[
[ number < ] [ 'More!' echo ]
[ number > ] [ 'Less!' echo ]
[ number = ] [
'Yup, it\'s ' number ~ echo
break
]
] choose
]
Running this program, we get:
$ novika guess.nk
Guess the number 0-100
Hit Ctrl-C or Ctrl-D to quit
46
> 12
More!
> 50
Less!
> 46
Yup, it's 46
Note how we use ~
to stitch the quote 'Yup, it\'s '
with number
. This operation is known as string concatenation in some languages. Note also how '
is escaped using \
so it doesn't end the quote literal prematurely.
Of course we don't want the user to quit after the first number they've gueesed!
Nested loops are hard, but Novika is flexible enough to compensate. For one, break
isn't a keyword like in other languages; it's a word as much as any other. Words have definitions, and break
has one, too. Its definition is aware of which loop it should break, and it does just that.
Why are we even talking about this, though? Well, the line '> ' readLine not => break
won't exit from the outer loop -- but we would like it to. Otherwise, the user would have to terminate the game by hand (either via Ctrl-C (which may not work) or a task manager).
We can bind the definition of the outer loop's break
to quitGame
. To get the definition, we can use #break here
or this -> break
. Let's use the latter.
'Guess the number 0-100' echo
'Hit Ctrl-C or Ctrl-D to quit' echo
loop: [
this -> break @: quitGame
0 randTo: 100 p $: number
loop: [
'> ' readLine not => quitGame
parseDecimal
[
[ number < ] [ 'More!' echo ]
[ number > ] [ 'Less!' echo ]
[ number = ] [
'Yup, it\'s ' number ~ echo
'Next round!' echo
break
]
] choose
]
]
Other solutions are possible, of course. For one, you can bind outer loop's this
to a word named, say, game
, and to break it use game.break
. And yes, you can use booleans for this; but please go use C if you want to do that. Let's stick with the current solution, though, because it's interesting.
Last but not least, let's add a score. Let's count the number of attempts the user made at guessing the current number -- this will be our score. By taking the minimum of the current score and the best score, we can calculate the next best score. If the best score is 0, we'll just use the current score for the best score.
Finally, let's also remove the debug print p
.
'Guess the number 0-100' echo
'Hit Ctrl-C or Ctrl-D to quit' echo
0 $: bestScore
loop: [
this -> break @: quitGame
1 $: score
0 randTo: 100 $: number
loop: [
'> ' readLine not => quitGame
parseDecimal
[
[ number < ] [ 'More!' echo ]
[ number > ] [ 'Less!' echo ]
[ number = ] [
'Yup, it\'s ' number ~ echo
'Score: ' score ~ echo
bestScore zero? br:
"Yes -- it's zero!" score
"No -- it's not" [ bestScore score 2min ]
=: bestScore
'Best score: ' bestScore ~ echo
'Next round!' echo
break
]
] choose
score 1 + =: score
]
]
If the user mistypes something, our guessing game will crash with a fairly long traceback. It does describe the problem rather well -- for us, programmers. But it's not what we want the user to see. Instead, let's wrap parseDecimal
in a block that'll catch any error that happens within it (a death handler block), and show a friendlier error message to the user.
'Guess the number 0-100' echo
'Hit Ctrl-C or Ctrl-D to quit' echo
0 $: bestScore
loop: [
this -> break @: quitGame
1 $: score
0 randTo: 100 $: number
loop: [
'> ' readLine not => quitGame
[
[ 'Please enter a number...' echo next ] @: __died__
parseDecimal
] open
[
[ number < ] [ 'More!' echo ]
[ number > ] [ 'Less!' echo ]
[ number = ] [
'Yup, it\'s ' number ~ echo
'Score: ' score ~ echo
bestScore zero? br:
"Yes -- it's zero!" score
"No -- it's not" [ bestScore score 2min ]
=: bestScore
'Best score: ' bestScore ~ echo
'Next round!' echo
break
]
] choose
score 1 + =: score
]
]
Note the use of next
inside the death handler to avoid incrementing the current score.
Here's some gameplay, now.
$ novika guess.nk
Guess the number 0-100
Hit Ctrl-C or Ctrl-D to quit
> 12
More!
> 50
Less!
> typo
Please enter a number...
> 20
Less!
> 18
Less!
> 15
More!
> 16
More!
> 17
Yup, it's 17
Score: 10
Best score: 10
Next round!