Skip to content

Tutorial: Writing a Guessing Game in Novika

Alexey Yurchenko edited this page May 4, 2023 · 2 revisions

Introduction

You'd need to build or install Novika.

Printing

Printing in Novika is known as echoing. 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 echos to output multiple lines.

Reading user input

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 ]

Asking for numbers in a loop

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.

Generating a random number

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).

Less, More, or Equal

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.

An outer loop

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
  ]
]

Catching errors

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!