DEV Community

Cover image for Gopher Gym: Quiz Game - Part 1
Adil 🧩
Adil 🧩

Posted on • Edited on

Gopher Gym: Quiz Game - Part 1

29 May 2019 at 05:20

I started my morning with some cowboy coffee; which is coffee brewed in a saucepan, since I seem to have misplaced my aero press, as I listened to Go for beginners, and discovered Gophercises, a set of coding exercises in Go aimed at people who’ve done a few tutorials but have kinda lost steam. I felt like that described me, so I’ve decided to tackle these exercises while journalling my progress and learning in this blog.

/I have a peculiar writing style because I write in tandem with coding, so there’s a lot of tense switching, so apologies in advance. All comments and criticisms are encouraged, no matter how harsh or abrasive, as long as they are constructive!/

I’m going to tackle the first challenge, GitHub - gophercises/quiz: Ex 1 - Run timed quizzes via the command line.

So the first thing I need to do is make a new go project. I create a new repository on github, clone it into my $GOPATH/src/github.com/adilw3nomad, and made a simple main.go file that prints Hello! when it’s run.

I run go build && ./GopherQuiz, which compiles my program into a binary called GopherQuiz and runs said file. Hello! was printed to the command line. Success! I now have something to work with.

Lets go over the exercise details; I’m going to make the points which I feel are important to the domain in bold.

Create a program that will read in a quiz provided via a CSV file (more details below) and will then give the quiz to a user keeping track of how many questions they get right and how many they get incorrect. Regardless of whether the answer is correct or wrong the next question should be asked immediately afterwards.
The CSV file should default to problems.csv (example shown below), but the user should be able to customize the filename via a flag.

I like to break down stuff like this into bullet points;

  • There’s a quiz, which has questions.
  • Questions can be either right or wrong, ergo they have an answer.
  • There’s a score, or at least a concept of a score
  • The quiz is created from a CSV file with questions and answers

That’s the domain of the application; now let’s have a think about the behaviour. The application has a single dependency; it requires a CSV file to create the quiz. Without a CSV file, there is no quiz, and the app is useless. So the first thing we need to do is get our app to load and read a CSV file. I won’t worry about the filename flag just yet, or even the content of the CSV. All I want to focus on is loading and reading a CSV file into the application.

  1. Create array/slice to hold quiz items. (Question, Answer)
  2. Read each line of the CSV and create a quiz item from it.
  3. Iterate through the slice, printing the question to stdout
  4. Accept an answer from the user
  5. If answer matches the user’s input, increase score counter by one.

I’m going to do this in the worst way possible; with no tests, all in the main function. Then I’ll write some tests for it, and then I’ll refactor, knowing that my coce still works thanks to the tests.

31 May 2019 at 06:36

func main() {
  csvFile, err := os.Open(problems.csv)
  if err != nil {
    log.Fatal(err)
  }
  defer csvFile.Close()

  reader := csv.NewReader(csvFile)
  records, err := reader.ReadAll()

  if err != nil {
    log.Fatal(err)
  }

  for i := 0; i < len(records); i++ {
    fmt.Println("Question: ", records[i][0])
    fmt.Println("Answer: ", records[i][1])
  }
}

So this is what I have so far; I open the file using the os package, and do not close it until the program stops running. I then create a 2 dimensional array of each of the CSV elements using csv.Reader.ReadAll. Finally, to see what I’m working with, I loop through the records and print each one. protip; PrintLn is useful for just printing out whatever as long as it’s printable.

So I now have my CSV input, what do I want to do with it? I want to

  • Ask each question
  • Accept an user’s answer
  • Compare it to the correct answer
  • Increase a counter if they are the same.

What does it mean to ask a question in a command line app? It means writing to stdout. What does it mean to accept an answer? Reading from stdin. I’ve already got writing to stdout covered by using PrintLn, and in order to read from stdin, I’m going to use the bufio package.

// omitted unchanged code for brevity
for I := 0; I < len(records); I++ {
  // Print the question out
  fmt.Println(Question: , records[I][0])
  // Read from stdin for answer
  reader := bufio.NewReader(os.Stdin)
  fmt.Print(Enter your answer now: )
  // Expect answer to be given once they hit return
  text, _ := reader.ReadString(‘\n)

  if text == records[I][1] {
    fmt.Println(Correct!)
  } else {
    fmt.Println(WRONG! Answer is: , records[I][1])
  }
}

What we’ve done here is loop through the lines of the CSV, print the question out, then instantiate a new buffered I/O reader which uses the os.Stdin interface. Next we print an instruction to the user, and use the reader.ReadString method to read their input. The parameter passed to ReadString is the delimiter that lets the reader know when to stop reading. Then a simple conditional statement checks to see if the answer is correct.

Pretty sweet, except it doesn’t work! I suspect it’s because I am not making the assertion correctly. Let’s do some debugging!. But first, I’m going to take a break and play a game of Autochess :D

31 May 2019 at 08:22

A game of autochess (and some laundry time) later, I’m back and ready to roll. So, first I hypothesise as to why it doesn’t recognise correct answers; I posit that the type of variables I am comparing (text and records[I][1]) are not the same. Let’s find out the types of each of these by printing them.

We know (thanks to vscode’s Go package), that reader.ReadString returns a string. What does csv.Reader.ReadAll return? A 2-dimensional slice of with strings. Hm. Using this little bit of nasty code, I can confirm they are both the same type. So what the hell is going on!

fmt.Println(fmt.Sprintf(%T, text)) // string
fmt.Println(fmt.Sprintf(%T, records[I][1])) // string

Time for a new hypothesis; but first, some refactoring. I’m finding it a bit difficult to make sense of what I’m writing, mainly because of all these horrible names, like records[I][1].
I’m going to turn these record’s into what they are; items in a quiz. How, you say? By making a struct!

Go’s structs are typed collections of fields. They’re useful for grouping data together to form records, and are pretty essential if you want to do OOP in Go. Go isn’t a pure object oriented language, but through methods and structs, you can write object-oriented code.

Here’s how to define a struct in Go, the aptly named quizItem

type quizItem struct {
  question string
  answer   string
}

We start with the keyword type, since Go is statically typed, and we are defining a new type of variable. Followed by the name of the struct, quizItem, and finally the keyword struct. In the body of the curly braces, we define the structs attributes, followed by their type.

Here’s how the quiz loop looks now that we’ve added a struct

/// omitted unchanged code for brevity
for I := 0; I < len(records); I++ {
  // Create quizItem object
  quizItem := quizItem{records[I][0], records[I][1]}
  // Print out the question
  fmt.Println(Question: , quizItem.question)
  // Create reader and allow user to input their answer
  reader := bufio.NewReader(os.Stdin)
  fmt.Print(Enter your answer now: )
  // Expect answer to be given once they hit return
  text, _ := reader.ReadString(‘\n)

  if text == quizItem.answer {
    fmt.Println(Correct!)
  } else {
    fmt.Println(WRONG! Answer is: , quizItem.answer)
  }
}

As you can see on line 3, instantiating the struct is pretty straight forward, and now the code is a hell of lot more readable. Now that it’s easier to read, time to make a new hypothesis as to why the answer comparison does not work. I hypothesise that perhaps I’m making an assumption on what the value of text is going to be.

Let’s print it out and check! I’ll print the user’s answer before running the comparison check, so now the code looks like this

/// omitted unchanged code for brevity
for I := 0; I < len(records); I++ {
  // Create quizItem object
  quizItem := quizItem{records[I][0], records[I][1]}
  // Print out the question
  fmt.Println(Question:, quizItem.question)
  // Create reader and allow user to input their answer
  reader := bufio.NewReader(os.Stdin)
  fmt.Print(Enter your answer now: )
  // Expect answer to be given once they hit return
  text, _ := reader.ReadString(‘\n)
  fmt.Println(Your answer is:, text)

  if text == quizItem.answer {
    fmt.Println(Correct!)
  } else {
    fmt.Println(WRONG! Answer is:, quizItem.answer)
  }
}
$ go build && ./GopherQuiz
Question: 5+5
Enter your answer now:10

Your answer is: 10
WRONG! Answer is: 10
Question: 7+3
Enter your answer now:

Something here is odd; there a new line after printing the user’s answer. Let’s investigate it by looking at the base16 value of the string. We can do this by using fmt.Printf(“%x”, text), where %x is a format modifier signifying that I want the string to formatted in base16. Let’s write some code to test this out;

fmt.Println("Your answer is:", text)
fmt.Printf("Your answer in base16 is %x \n", text)
fmt.Printf("The expected answer in base16 is %x \n", quizItem.answer)
$ go build && ./GopherQuiz
Question: 5+5
Enter your answer now:10
Your answer is: 10

Your answer in base16 is 31300a
The expected answer in base16 is 3130

Aha! My suspicions are confirmed; there /is/ a difference between the two. The user input ends in 0a, which is hex code for linefeed.

What’s happened here is that when reading from stdin, we took everything up to and including \n, which is the reason why there was a new line after printing the user’s input.

Now that we know the issue, how can we solve it? The simplest way would be to remove the linefeed character from the input before comparing. Let’s use the strings package, which is a library of tools for manipulating strings. A quick test:

text = strings.TrimSuffix(text, “\n)
fmt.Printf(Your answer trimmed in base16 is %x \n, text)
fmt.Printf(The expected answer in base16 is %x \n, quizItem.answer)
$ go build && ./GopherQuiz
Question: 5+5
Enter your answer now:10
Your answer is: 10

Your answer trimmed in base16 is 3130
The expected answer in base16 is 3130

Success! So what we’ve done here is use the TrimSuffix method, passing it the text we want to apply the trim to, and the suffix we wish to trim. With this, the quiz should work! Here’s a quick reminder of how the entire program looks;

package main

import (
"bufio"
"encoding/csv"
"fmt"
"log"
"os"
"strings"
)

type quizItem struct {
  question string
  answer   string
}

func main() {
  csvFile, err := os.Open("problems.csv")
  if err != nil {
    log.Fatal(err)
  }
  defer csvFile.Close()

  reader := csv.NewReader(csvFile)
  records, err := reader.ReadAll()

  if err != nil {
    log.Fatal(err)
  }

  for i := 0; i < len(records); i++ {
    // Create quizItem object
    quizItem := quizItem{records[i][0], records[i][1]}
    // Print out the question
    fmt.Println("Question:", quizItem.question)
    // Create reader and allow user to input their answer
    reader := bufio.NewReader(os.Stdin)
    fmt.Print("Enter your answer now: ")
    // Expect answer to be given once they hit return
    text, _ := reader.ReadString('\n')
    fmt.Println("Your answer is:", text)
    // Trim the newline suffix from the input
    text = strings.TrimSuffix(text, "\n")
    if text == quizItem.answer {
      fmt.Println("Correct! Well done")
    } else {
      fmt.Println("WRONG! Answer is:", quizItem.answer)
    }
  }

}

And this is how it looks like in action!

So the next things we need to do are;

  • Keep track of the score, and output how many are correct/wrong
  • Add a flag that allows the CSV file to be customised.

But before we go any further, we must have tests! Having tests helps you understand your code, saves you time from having to manually test things, and also gives you a little rush every time it all goes green.

Follow me on twitter if you wanna ask me questions or have any suggestions!

Top comments (2)

Collapse
 
lordrahl90 profile image
Alugbin Abiodun Olutola

Yay...... I eventually tried it out. Your post was inspiring. Cant wait for the test part. I'd really like to see how you go about the testing.

Here, github.com/LordRahl90/quizmanager/... check it out.

Collapse
 
lordrahl90 profile image
Alugbin Abiodun Olutola

I will make this my exercise during the day.... Thanks for this post.