Sunday, September 18, 2011

Arithmetic Tutor Program (or, structuring and debugging loops)

Problem:

Write a program that poses a series of simple arithmetic problems for a student to answer.

Your program should meet these requirements:
• It should ask a series of five questions. As with any such limit, the number of questions should be coded as a named constant so that it can easily be changed.
• Each question should consist of a single addition or subtraction problem involving just two numbers, such as “What is 2 + 3?” or “What is 11 – 7?”. The type of problem—addition or subtraction—should be chosen randomly for each question.
• To make sure the problems are appropriate for students in the first or second grade, none of the numbers involved, including the answer, should be less than 0 or greater than 20. This restriction means that your program should never ask questions like “What is 11 + 13?” or “What is 4 – 7?” because the answers are outside the legal range. Within these constraints, your program should choose the numbers randomly.
• The program should give the student three chances to answer each question. If the student gives the correct answer, your program should indicate that fact in some properly congratulatory way and go on to the next question. If the student does not get the answer in three tries, the program should give the answer and go on to another problem.

(Roberts Ch 6, problem 7)

This was a fun problem to write, though debugging took a long time. Here was how I split up the problem, given the requirements. There are three major parts of this program:
1) Generate the arithmatic program.
2) Present it to the student and evaluate his answer
3) Stop the program after 5 presented problems.

These were my smaller steps:
* Generate two numbers (0-20) and an operator (comes from boolean true/false). Use random generator instance.
* Put them together and assess them so the program "knows" the correct answer
* If the result (“problemAnswer”) is not between 0-20, don't print anything and start again
* If it's between 0-20, then present the problem to the user. Declare "int userAnswer = readln(print the equation here)".
* To keep track of user attempts, start a local variable "countUserAttempts" and make it start at 1
* If problemAnswer == userAnswer, break from the loop (print that it's right) and start again
* If problemAnswer != userAnswer, then print it's wrong. Add +1 to the “countUserAttempts” variable and send them back thru the inner loop to try again.
* Let user submit a second answer. If it's right then print that it's correct and start again. If it's wrong then add +1 to the countUserAttempts variable (so now it’s three) and send them back thru.
* If it’s wrong again, then it will make countUserAttempts equal 4 so it will break from the inner “while” loop and roll down to the next argument and print the correct answer.

What it looks like:


/* File: ArithmaticTutor
 * This program shows a series of arithmetic problems to the user and evaluates his or her input.
 * In accordance with the problem specs, we only present components between 0 and 20, and make
 * sure that the answer (sum or difference) is between 0 and 20 as well before giving it to the user.
 * We make up a problem on the fly using 2 methods that each use a random generator instance;
 * one to generate the numbers to in our problem, and a second to decide whether the operator is plus 
 * or minus. There's also a method to collect the user's input. There are two count-keeping variables:
 * one, "countPresentedProblems," makes sure that we give the user only 5 problems (or 
 * however many are allowed by the "maxProblems" constant. The other, "countUserAttempts,"
 * which is inside the inner "while" loop, keeps track of user attempts to make sure 
 * he or she does only 3 attempts. If the user gets the answer right in 3 or fewer attempts, we print 
 * in 3 or fewer attempts, we print "Right!" and move on to the next problem. If not,
 * we just tell the right answer and move on.
 * 
 */
    import acm.program.*;
    import acm.util.RandomGenerator;

    public class ArithmaticTutor extends ConsoleProgram {
        public void run() {
            println("Welcome to Math Tutor! Prepare to be challenged.");
            int countPresentedProblems = 0; 
            while (countPresentedProblems < maxProblems) {
                // First we put together all the stuff needed for our problem and assess the first problem ourselves 
                int firstNumber = generateNumber(0,20); 
                int secondNumber = generateNumber(0,20); 
                boolean isPlusNotMinus = plusNotMinus();
                int programSolution = evaluateProblem(firstNumber, secondNumber, isPlusNotMinus);
                // Then we present it to the student (if the answer is between 0 and 20)
                if (programSolution > 0 && programSolution < 20){
                    countPresentedProblems +=1;
                    int userAnswer = getUserAnswer(firstNumber, secondNumber, isPlusNotMinus);
                    int countUserAttempts = 1;
                    while (countUserAttempts < 3 && userAnswer != programSolution) {
                        userAnswer = readInt ("Wrong. Try another answer:");
                        countUserAttempts += 1;
                    }
                    if (userAnswer == programSolution) {
                        println( "Right!");
                    }
                    else {
                        println("No, the correct answer is " + programSolution); 
                    }
                }
            }
         }
        private final static int maxProblems = 5;
  
        private RandomGenerator rgen = RandomGenerator.getInstance();
  
        private int generateNumber(int min, int max) {
            int number = rgen.nextInt(min, max);
            return number;
        }
        private boolean plusNotMinus() {
            return rgen.nextBoolean();
        }
        private int evaluateProblem (int firstNumber, int secondNumber, boolean plusNotMinus){
            if (plusNotMinus) {
                return firstNumber + secondNumber;
            }
            else {
                return firstNumber - secondNumber;
            }
        }
        private int getUserAnswer (int firstNumber, int secondNumber, boolean plusNotMinus){
            if (plusNotMinus) {
                return readInt("What is " + firstNumber + "+" + secondNumber + "?");
            }
            else {
                return readInt("What is " + firstNumber + "-" + secondNumber + "?");
            }
        } 
}
What made it hard: Keeping track of your loops. Poorly structured loops kept on causing bugs! For example there was a bug where if you entered 2 wrong answers in a row, it fell back to the “Wrong, the correct answer is [programAnswer]” part, even though it should wait till 3 wrong entries. The problem was that the part of the code that says “Wrong, the correct answer is [programAnswer]” was *inside* the "while" loop, so once we evaluated that it was wrong it would print the correct answer then still re-prompt for another attempt! Then I moved that “No, the correct answer is [correct answer]” part to outside the nearest loop, but then it started getting run for every submitted answer, even if they got it right! Here are my other bloopers, along with a brief explanations of what was wrong with the code when they appeared.

8 comments:

  1. your approach is generate random problems and then ensuring that the answer for the problem is between 0 and 20.

    you might be able to simplify the program significantly if you work backwards: choose two numbers between 0 and 20. call the higher number 'h' and the lower number 'a'. compute b = h - a. because h > a, we b > 0. now, choose addition or subtraction. if you chose addition, give the user the problem a + b = ?, and if you chose subtraction, give the user the problem h - a = ?

    that approach means you don't have to evaulate the answer to your problem, and you won't generate problems that fall outside your range.

    ReplyDelete
  2. Thanks Mark! After my boyfriend (an eng) took a look at this he suggested the same approach as you so that you're only evaluating the problem if it would fall within the acceptable range and save computing power.

    The problem is, isn't it not equally distributed the number of addition and subtraction problems that come up? Not that that's a requirement of the problem, but it is potentially a drawback if you're trying to drill students. I'll have to think through whether that's in fact the case...

    To me the biggest drawback was that it's less readable...but I know a good eng should be able to think of many approaches to a given problem, and in any case it would be more readable to an experienced eng!

    ReplyDelete
  3. The distribution is a little weird. For 0, it's 11, then it alternates between 20 and 21 going up to 19. Not exactly uniform, but not horrible either. Renee's solution provides uniform distribution of answers. Probably, uniform distribution is ideal, but, it's not deterministic. I wonder .. what would a uniform distribution that was deterministic look like.

    ReplyDelete
  4. Btw, I left that last part as a challenge. :)

    ReplyDelete
  5. We were talking the other day about constraint-based programming, and I wanted to show you how it could be done with Haskell's list monad. Enjoy.

    module Main where

    import Control.Monad (guard, forever)
    import System.Random (randomRIO)

    data Operator = Addition | Subtraction deriving (Eq)
    data Question = Question Int Operator Int

    instance Show Operator where
    show op | op == Addition = "+"
    | op == Subtraction = "-"

    instance Show Question where
    show (Question x op y) = unwords [show x, show op, show y]

    answer :: Question -> Int
    answer (Question x op y) |
    op == Addition = x + y
    op == Subtraction = x - y

    questions :: [Question]
    questions = do
    x <- [0..20]
    y <- [0..20]
    operator <- [Addition, Subtraction]
    let question = Question x operator y
    answer' = answer question
    guard (0 <= answer' && answer' <= 20)
    return question

    main :: IO ()
    main = forever $ do
    question <- choose questions
    putStrLn ("Q: What is " ++ show question ++ "?")
    input <- getLine
    if read input == answer question
    then putStrLn "You're right!"
    else putStrLn ("Wrong, " ++ show question ++ " = " ++ show (answer question))
    where choose xs = randomRIO (0, length xs - 1) >>= return . (xs !!)

    ReplyDelete
  6. This comment has been removed by the author.

    ReplyDelete
  7. Here's my attempt. Didn't get around to the 3 guesses part, but maybe I'll work on that another time.
    http://pastebin.com/JRNFZfLj

    ReplyDelete