Chapter 23: the game, built and played

Chapter 23 is the whole book in one program: a number-guessing game, built from a record, a class, six small functions and a thin program that ties them to the keyboard - the biggest example in the book. The IDE's own tools make the structure - every field's declaration, every constructor's and method's and function's whole signature - so what is left to type is the logic no tool can write. To play it in the IDE: Help > Lessons > Writing code…, or ek9 -ide -lesson book-game.

The EK9 IDE as the lesson opens

What you'll build

The lesson builds this program from a file holding only its module line - the finished file, as the book prints it.

#!ek9
defines module book.guess

  defines record

    Score
      who as String: String()
      attempts as Integer: Integer()

      default Score() as pure

      Score() as pure
        ->
          name as String
          tries as Integer

        who :=? String(name)
        attempts :=? Integer(tries)

      operator $ as pure
        <- rtn as String: `${who} (${attempts})`

      operator <=> as pure
        -> other as Score
        <- rtn as Integer: attempts <=> other.attempts

      operator #? as pure
        <- rtn as Integer: #?attempts

      default operator ?

  defines class

    //Holds one game: the secret, how many tries are allowed, and how it is going.
    Game
      secret as Integer: Integer()
      maxTries as Integer: Integer()
      tries as Integer: 0
      solved as Boolean: false

      default private Game() as pure

      Game() as pure
        ->
          chosen as Integer
          allowed as Integer

        secret :=? Integer(chosen)
        maxTries :=? Integer(allowed)

      //Take one guess. Count it, say which way to go, and note a win.
      guess()
        -> number as Integer
        <- rtn as String: String()
        tries += 1
        rtn: hint(secret, number)
        if number == secret
          solved := true

      over() as pure
        <- rtn as Boolean: solved or tries >= maxTries

      triesUsed() as pure
        <- rtn <- Integer(tries)

      report() as pure
        -> player as String
        <- rtn as String: String()
        rtn: solved <- `Well done ${player} - ${tries} tries.` : `Out of tries - the number was ${secret}.`

      default operator ?

  defines function

    //Compare a guess against the secret and say which way to go.
    hint() as pure
      ->
        secret as Integer
        guess as Integer
      <- rtn as String: String()

      order <- guess <=> secret
      rtn: order < 0 <- "higher" : order > 0 <- "lower" : "correct"

    //Put a score board in order: fewest tries first.
    rank() as pure
      -> board as List of Score
      <- rtn as List of Score: List() of Score
      rtn: cat board | sort | collect as List of Score

    //The scores already on the board when a new game starts.
    startingBoard()
      <- rtn as List of Score: List() of Score
      rtn += Score("Ada", 3)
      rtn += Score("Alan", 5)

    //Add this game's score to the opening board.
    finalBoard()
      ->
        player as String
        tries as Integer
      <- rtn as List of Score: List() of Score

      rtn: startingBoard()
      rtn += Score(player, tries)

    //Render the ranked board as one block of text, best first.
    boardLines()
      -> board as List of Score
      <- rtn as String: ""
      for entry in rank(board)
        rtn += `  ${entry}\n`

    //Work out the player's name from the command-line options.
    playerName()
      -> argv as List of String
      <- rtn as String: String()
      usage <- "Usage: Guess [-p name]"
      supported <- {
        "-p": ":"
      }
      rtn: GetOpt(":", supported, usage).options(argv).getOrDefault("-p", "Player")

  defines program

    Guess()
      -> argv as List of String

      stdin <- Stdin()
      stdout <- Stdout()

      player <- playerName(argv)
      high <- 100
      maxTries <- 7
      game <- Game(OS().random(1, high), maxTries)

      stdout.println(`I am thinking of a number from 1 to ${high}. You have ${maxTries} tries.`)

      while not game.over()
        number <- Integer(stdin.next())
        stdout.println(`  ${number}: ${game.guess(number)}`)

      stdout.println(game.report(player))
      stdout.println("Fewest tries wins:")
      stdout.print(boardLines(finalBoard(player, game.triesUsed())))

Step 1: Score — a record, with its fields

Choose Code > New Construct > Record…

The EK9 IDE once this is done: Choose Code > New Construct > Record…

Score is the record's name. Add who (String) and attempts (Integer), each given a starting Value - String()/Integer(), a freshly made but unset value - so the record needs no constructor of its own yet. Choose Preview.

The EK9 IDE just before choosing Preview: Score is the record's name

The preview shows Score about to be written, both fields together - and default operator ? with them: a record that holds fields must say when it counts as set. Choose Apply.

The EK9 IDE just before choosing Apply: The preview shows Score about to be written, both fields together - and default operator ? with them

Select the Score line, and above it type the blank line every chapter example (but 16) opens its first member with. Saves when typed.

The EK9 IDE once this is done: Select the Score line, and above it type the blank line every chapter example (but 16) opens its first member with

Step 2: Building a Score, blank then complete

Right-click the Score line and choose Add Constructor > No-Arg (default, pure) - a blank Score to fall back on.

The EK9 IDE once this is done: Right-click the Score line and choose Add Constructor > No-Arg (default, pure) - a blank Score to fall back on

Right-click the Score line again and choose Add Constructor > From All Fields (pure) - a value built complete, in one step, added straight after the one just made.

The EK9 IDE once this is done: Right-click the Score line again and choose Add Constructor > From All Fields (pure) - a value built complete, in one step, added straight after the one just made

Select the stub, from Score() as pure down to its last assignment, and type the constructor's whole job: name/tries in, who and attempts set with :=? over String(...)/Integer(...). Saves when typed.

The EK9 IDE once this is done: Select the stub, from Score() as pure down to its last assignment, and type the constructor's whole job

Step 3: Score's own operators

Right-click the Score line and choose Add Operator > $ (string) > skeleton.

The EK9 IDE once this is done: Right-click the Score line and choose Add Operator > $ (string) > skeleton

Select the stub line and type $'s own text form - the name, then the tries in parentheses. Saves when typed.

The EK9 IDE once this is done: Select the stub line and type $'s own text form - the name, then the tries in parentheses

Right-click the Score line and choose Add Operator > <=> (compare) > skeleton - not default: Score orders by attempts alone, not both fields. A hand-written <=> needs a hand-written #? over the same fields (E07184), so the tool writes BOTH stubs together, in this one call.

The EK9 IDE once this is done: Right-click the Score line and choose Add Operator > <=> (compare) > skeleton - not default

Select the stub, from <- rtn as Integer: 0 down to its last line, and type the real comparison: attempts <=> other.attempts, in one line. Saves when typed.

The EK9 IDE once this is done: Select the stub, from <- rtn as Integer

Select the #? stub line and type the real hash: #? attempts alone. Saves when typed.

The EK9 IDE once this is done: Select the #? stub line and type the real hash

Step 4: Game — a class, with its first fields

Choose Code > New Construct > Class…

The EK9 IDE once this is done: Choose Code > New Construct > Class…

Game is the class's name. Closed is already selected - EK9's default. Type Game and choose Preview.

The EK9 IDE just before choosing Preview: Game is the class's name

The preview shows Game about to be written - a shell, nothing inside it yet. Choose Apply.

The EK9 IDE just before choosing Apply: The preview shows Game about to be written - a shell, nothing inside it yet

Select the Game line, and above it type the comment explaining what a game holds. Saves when typed.

The EK9 IDE once this is done: Select the Game line, and above it type the comment explaining what a game holds

Right-click the Game line and choose Add Field…

The EK9 IDE once this is done: Right-click the Game line and choose Add Field…

Type secret as Integer: Integer() - the chosen number, not yet known. Choose OK.

The EK9 IDE just before choosing OK: Type secret as Integer

The preview shows secret added - and default operator ? with it: a class that holds fields must say when it counts as set. Choose Apply.

The EK9 IDE just before choosing Apply: The preview shows secret added - and default operator ? with it

Right-click the Game line and choose Add Field…

The EK9 IDE once this is done: Right-click the Game line and choose Add Field…

Type maxTries as Integer: Integer() - how many tries the game allows. Choose OK.

The EK9 IDE just before choosing OK: Type maxTries as Integer

The preview shows maxTries added, beside secret. Choose Apply.

The EK9 IDE just before choosing Apply: The preview shows maxTries added, beside secret

Step 5: Building a Game, blank then complete

Right-click the Game line and choose Add Constructor > Private No-Arg (default, pure) - private, so nothing outside the class can build a half-made game.

The EK9 IDE once this is done: Right-click the Game line and choose Add Constructor > Private No-Arg (default, pure) - private, so nothing outside the class can build a half-made game

Right-click the Game line again and choose Add Constructor > From All Fields (pure) - over secret and maxTries, the only two fields there are so far.

The EK9 IDE once this is done: Right-click the Game line again and choose Add Constructor > From All Fields (pure) - over secret and maxTries, the only two fields there are so far

Select the stub, from Game() as pure down to its last assignment, and type the constructor's whole job: chosen/allowed in, secret and maxTries set with :=? over Integer(...). Saves when typed.

The EK9 IDE once this is done: Select the stub, from Game() as pure down to its last assignment, and type the constructor's whole job

Right-click the Game line and choose Add Field…

The EK9 IDE once this is done: Right-click the Game line and choose Add Field…

Type tries as Integer: 0 - how many guesses used so far, started at zero (a field that starts unset here would mean over() could never become true - this is the bug the book's own first test caught). Choose OK.

The EK9 IDE just before choosing OK: Type tries as Integer

The preview shows tries landing right after maxTries - ahead of both constructors, where every field belongs. Choose Apply.

The EK9 IDE just before choosing Apply: The preview shows tries landing right after maxTries - ahead of both constructors, where every field belongs

Right-click the Game line and choose Add Field…

The EK9 IDE once this is done: Right-click the Game line and choose Add Field…

Type solved as Boolean: false - whether this game has been won yet. Choose OK.

The EK9 IDE just before choosing OK: Type solved as Boolean

The preview shows solved landing last among the fields, all four together. Choose Apply.

The EK9 IDE just before choosing Apply: The preview shows solved landing last among the fields, all four together

Step 6: guess — taking one guess

Right-click the Game line and choose Add Method…

The EK9 IDE once this is done: Right-click the Game line and choose Add Method…

guess is the method's name - one parameter, number as Integer, returning String; not pure, since it changes tries and solved. Add the parameter, fill the rest in, and choose Apply.

The EK9 IDE just before choosing Apply: guess is the method's name - one parameter, number as Integer, returning String; not pure, since it changes tries and solved

Select the Game line's guess() and above it type the comment - the blank line above it is already there (the tool's own separator between this method and the constructor before it). Saves when typed.

The EK9 IDE once this is done: Select the Game line's guess() and above it type the comment - the blank line above it is already there (the tool's own separator between this method and the constructor before it)

Select the stub, from <- rtn as String? down to its //AI line, and type guess's whole job: count the try, work out the hint, note a win. Saves when typed. guess calls hint(), which is not written yet, so the Problems tab shows one error (E50001) until part 8 writes it.

The EK9 IDE once this is done: Select the stub, from <- rtn as String? down to its //AI line, and type guess's whole job

Step 7: over, triesUsed and report

Right-click the Game line and choose Add Method…

The EK9 IDE once this is done: Right-click the Game line and choose Add Method…

over is the name, Boolean the return type, pure ticked - over only reads the game's state. Fill it in and choose Apply.

The EK9 IDE just before choosing Apply: over is the name, Boolean the return type, pure ticked - over only reads the game's state

Select the stub, from <- rtn as Boolean: false down to its //AI line, and type the real test: solved or tries >= maxTries. Saves when typed.

The EK9 IDE once this is done: Select the stub, from <- rtn as Boolean

Right-click the Game line and choose Add Method…

The EK9 IDE once this is done: Right-click the Game line and choose Add Method…

triesUsed is the name, Integer the return type, pure ticked. Fill it in and choose Apply.

The EK9 IDE just before choosing Apply: triesUsed is the name, Integer the return type, pure ticked

Select the stub, from <- rtn as Integer: 0 down to its //AI line, and type the real answer: tries handed back as an Integer in its own right. Saves when typed.

The EK9 IDE once this is done: Select the stub, from <- rtn as Integer

Right-click the Game line and choose Add Method…

The EK9 IDE once this is done: Right-click the Game line and choose Add Method…

report is the name - one parameter, player as String, returning String, pure ticked. Add the parameter, fill the rest in, and choose Apply.

The EK9 IDE just before choosing Apply: report is the name - one parameter, player as String, returning String, pure ticked

Select the stub, from <- rtn as String? down to its //AI line, and type report's whole job: a ternary choosing the win or loss sentence. Saves when typed.

The EK9 IDE once this is done: Select the stub, from <- rtn as String? down to its //AI line, and type report's whole job

Step 8: hint and rank — two pure functions

Choose Code > New Construct > Function…

The EK9 IDE once this is done: Choose Code > New Construct > Function…

hint is the function's name. Add two parameters, secret and guess, both Integer, set Returns to String, and tick pure. Choose Preview.

The EK9 IDE just before choosing Preview: hint is the function's name

The preview shows hint about to be written, pure, two Integers in and String out. Choose Apply.

The EK9 IDE just before choosing Apply: The preview shows hint about to be written, pure, two Integers in and String out

Select the hint() as pure line, and above it type the comment explaining what hint compares. Saves when typed.

The EK9 IDE once this is done: Select the hint() as pure line, and above it type the comment explaining what hint compares

Select the stub, from <- rtn as String? down to its //AI line, and type the real comparison: order from <=>, then two ternaries in a row. Saves when typed.

The EK9 IDE once this is done: Select the stub, from <- rtn as String? down to its //AI line, and type the real comparison

Choose Code > New Construct > Function…

The EK9 IDE once this is done: Choose Code > New Construct > Function…

rank is the name. Add one parameter, board as List of Score, set Returns to List of Score, and tick pure. Choose Preview.

The EK9 IDE just before choosing Preview: rank is the name

The preview shows rank about to be written, added to the same defines function block as hint. Choose Apply.

The EK9 IDE just before choosing Apply: The preview shows rank about to be written, added to the same defines function block as hint

Select the rank() as pure line, and above it type the comment explaining what rank does - the blank line above it is already there. Saves when typed.

The EK9 IDE once this is done: Select the rank() as pure line, and above it type the comment explaining what rank does - the blank line above it is already there

Select the stub, from <- rtn as List of Score? down to its //AI line, and type the one-line pipeline: sort, then collect. Saves when typed.

The EK9 IDE once this is done: Select the stub, from <- rtn as List of Score? down to its //AI line, and type the one-line pipeline

Step 9: The board helpers

Choose Code > New Construct > Function…

The EK9 IDE once this is done: Choose Code > New Construct > Function…

startingBoard is the name - no parameters, not pure. Set Returns to List of Score and choose Preview.

The EK9 IDE just before choosing Preview: startingBoard is the name - no parameters, not pure

The preview shows startingBoard about to be written. Choose Apply.

The EK9 IDE just before choosing Apply: The preview shows startingBoard about to be written

Select startingBoard() and above it type the comment - the blank line above it is already there. Saves when typed.

The EK9 IDE once this is done: Select startingBoard() and above it type the comment - the blank line above it is already there

Select the stub, from <- rtn as List of Score: List() of Score down to its //AI line, and type the board's two opening scores. Saves when typed.

The EK9 IDE once this is done: Select the stub, from <- rtn as List of Score

Choose Code > New Construct > Function…

The EK9 IDE once this is done: Choose Code > New Construct > Function…

finalBoard is the name - two parameters, player as String and tries as Integer, not pure. Set Returns to List of Score and choose Preview.

The EK9 IDE just before choosing Preview: finalBoard is the name - two parameters, player as String and tries as Integer, not pure

The preview shows finalBoard about to be written. Choose Apply.

The EK9 IDE just before choosing Apply: The preview shows finalBoard about to be written

Select finalBoard() and above it type the comment - the blank line above it is already there. Saves when typed.

The EK9 IDE once this is done: Select finalBoard() and above it type the comment - the blank line above it is already there

Select the stub, from <- rtn as List of Score? down to its //AI line, and type the real body: start from startingBoard(), add this game's score. Saves when typed.

The EK9 IDE once this is done: Select the stub, from <- rtn as List of Score? down to its //AI line, and type the real body

Choose Code > New Construct > Function…

The EK9 IDE once this is done: Choose Code > New Construct > Function…

boardLines is the name - one parameter, board as List of Score, not pure. Set Returns to String and choose Preview.

The EK9 IDE just before choosing Preview: boardLines is the name - one parameter, board as List of Score, not pure

The preview shows boardLines about to be written. Choose Apply.

The EK9 IDE just before choosing Apply: The preview shows boardLines about to be written

Select boardLines() and above it type the comment - the blank line above it is already there. Saves when typed.

The EK9 IDE once this is done: Select boardLines() and above it type the comment - the blank line above it is already there

Select the stub, from <- rtn as String? down to its //AI line, and type the loop that builds one line of text per ranked entry. Saves when typed.

The EK9 IDE once this is done: Select the stub, from <- rtn as String? down to its //AI line, and type the loop that builds one line of text per ranked entry

Step 10: playerName — reading the command line

Choose Code > New Construct > Function…

The EK9 IDE once this is done: Choose Code > New Construct > Function…

playerName is the name - one parameter, argv as List of String, not pure. Set Returns to String and choose Preview.

The EK9 IDE just before choosing Preview: playerName is the name - one parameter, argv as List of String, not pure

The preview shows playerName about to be written, the last member of defines function. Choose Apply.

The EK9 IDE just before choosing Apply: The preview shows playerName about to be written, the last member of defines function

Select playerName() and above it type the comment - the blank line above it is already there. Saves when typed.

The EK9 IDE once this is done: Select playerName() and above it type the comment - the blank line above it is already there

Select the stub, from <- rtn as String? down to its //AI line, and type GetOpt's whole job: the usage line, the -p option it supports, and the lookup with its default. Saves when typed.

The EK9 IDE once this is done: Select the stub, from <- rtn as String? down to its //AI line, and type GetOpt's whole job

Step 11: The program — a thin shell

Choose Code > New Construct > Program…

The EK9 IDE once this is done: Choose Code > New Construct > Program…

Guess is the program's name - one CLI parameter, argv as List of String, in the SAME dialog's own CLI parameters table. Add it, type Guess, and choose Preview.

The EK9 IDE just before choosing Preview: Guess is the program's name - one CLI parameter, argv as List of String, in the SAME dialog's own CLI parameters table

The preview shows Guess about to be written, taking argv. Choose Apply.

The EK9 IDE just before choosing Apply: The preview shows Guess about to be written, taking argv

Select the Guess() line, and above it type the blank line every chapter example (but 16) opens its first member with. Saves when typed.

The EK9 IDE once this is done: Select the Guess() line, and above it type the blank line every chapter example (but 16) opens its first member with

Select the stub, from require argv? down to its //AI line, and type the whole interactive loop: a Stdin and Stdout, the player's name, a random secret, the while loop reading guesses, then the report and the board. Saves when typed.

The EK9 IDE once this is done: Select the stub, from require argv? down to its //AI line, and type the whole interactive loop

Step 12: Playing it, through the debugger

Click the debug (bug) icon beside the Guess() line - no saved configuration exists for it yet.

The EK9 IDE once this is done: Click the debug (bug) icon beside the Guess() line - no saved configuration exists for it yet

Choose + New Configuration… - this run needs a fixed seed, which only a saved configuration can carry.

The EK9 IDE once this is done: Choose + New Configuration… - this run needs a fixed seed, which only a saved configuration can carry

Type 100 into Debug seed - a fixed seed makes OS().random draw the same secret every time, so this run can be scripted to a win. Choose Create.

The EK9 IDE just before choosing Create: Type 100 into Debug seed - a fixed seed makes OS().random draw the same secret every time, so this run can be scripted to a win

Choose Debug - the new configuration, with its fixed seed, is already selected.

The EK9 IDE just before choosing Debug: Choose Debug - the new configuration, with its fixed seed, is already selected

Type 50 into Standard Input and choose Enter.

The EK9 IDE once this is done: Type 50 into Standard Input and choose Enter

Type 25 into Standard Input and choose Enter.

The EK9 IDE once this is done: Type 25 into Standard Input and choose Enter

Type 37 into Standard Input and choose Enter.

The EK9 IDE once this is done: Type 37 into Standard Input and choose Enter

Type 43 into Standard Input and choose Enter.

The EK9 IDE once this is done: Type 43 into Standard Input and choose Enter

Four tries, and the win: Guess prints the report, then the board - your own score (Player, 4 tries) sorted in with Ada's and Alan's. The secret was fixed by the seed, so this exact game can be replayed any time.

The result

Guess.ek9 now builds, compiles and plays exactly as chapter 23 describes it: a Score record, a Game class, six small functions and the program that connects them to Stdin, OS().random and GetOpt - built by the tools, logic you typed - then played for real, through the debugger: a win in four tries, the board printed with your own score alongside Ada and Alan's. Running the fifteen tests that cover everything but the interactive loop, and a short look at the Journal, are book-game-journal.

Where this lesson comes from