Maths Tutor in GWT

Whenever you go to a Java conference nowadays, there will be at least one session dedicated to the Google Web Toolkit (GWT). You will be blown away by incredible AJAX feats, all coded in Java. Then you will hear that you do not need to worry about cross-browser JavaScript code, because GWT takes care of this for you. On Google's webpage we see this paragraph: "GWT shields you from worrying too much about cross-browser incompatibilities. If you stick to built-in widgets and composites, your applications will work similarly on the most recent versions of Internet Explorer, Firefox, and Safari. (Opera, too, most of the time.) DHTML user interfaces are remarkably quirky, though, so make sure to test your applications thoroughly on every browser."
I decided to spend a few days investigating GWT to gauge its applicability for my website. It became clear quite early that mixing GWT with an existing multi-page website would be a challenge. Also, I found that a lot of the ideas I tried worked perfectly in Firefox but failed dismally in Internet Explorer. You do have to test everywhere. However, the effects are stunning.
A few years ago, I wrote a simple Maths (that's what we call Math in South Africa) tutor for my son Maxi. We called it "Maxi Maths". It was simply a command line Java program, that would feed him questions and then measure the time it took him to answer. Functionally it was perfect in that it did exactly what we needed, and no more. By practicing his arithmetic, he regularly finished in the top four of his class.
After some time, we decided to "upgrade" to a Swing application, which worked quite well. From there we went over to an Applet and downgraded it to AWT. We had the usual Applet issues, and eventually did a JSP page. However, the JSP page had the typical page reload issues. We wanted to have a richer client experience. So last week, we decided to try this in GWT, mainly to satisfy our curiosity, but also to learn more about GWT.
Here is "Maxi Maths" in GWT and the older JSP version. When you try them out, note the page refreshing that happens with the JSP page whenever an answer is submitted. (Clue: Sometimes when you press an incorrect key, unexpected things happen.)
Before I show you the code, you need to prepare your system a bit. First off, download the GWT and install it. Now run the applicationCreator batch file shipped with the GWT, as follows:

applicationCreator net.kabutz.maxi.gwt.maths.client.Maths
  
This will prepare a nice directory structure for you, with two batch files: Maths-compile and Maths-shell. Call them both and you will see a nice scaffolding application that gets you going on using GWT.

Maths Game Foundation

Next we create the maths game. A very simple maths tutor that asks you a bunch of questions and then gives you a score. Each question extends the MathsQuestion abstract class. We have catered for plus, minus, multiply and divide, but it could be extended for other operators like remainder.
Note that GWT is limited in which classes can be compiled to JavaScript. For example, I could call the Math.random() method, but could not instantiate java.util.Random. Also, you cannot use any reflection code in the client part of GWT. If you add a server component, you can do anything that Java would normally allow under Servlets.
package net.kabutz.maxi.gwt.maths.client.game;

public abstract class MathsQuestion {
  private int answer;
  private String question;

  protected int random(int max) {
    return (int) (Math.random() * max) + 1;
  }

  protected void setQuestion(int first, int second,
                             char operator, int answer) {
    this.question = first + " " + operator + " " + second + " = ";
    this.answer = answer;
  }

  public String getQuestion() {
    return question;
  }

  public int getAnswer() {
    return answer;
  }
}
  
We then have four concrete maths questions. I must admit that initially I used a switch statement for the questions. However, a better Design Pattern is to use the Strategy pattern, on which this design is based. It would allow us to add new types of questions without modifying the rest of our code.
package net.kabutz.maxi.gwt.maths.client.game;

public class PlusQuestion extends MathsQuestion {
  public PlusQuestion(int upto) {
    int first = random(upto - (upto / 10));
    int second = random(upto - first - 1);
    setQuestion(first, second, '+', first + second);
  }
}

package net.kabutz.maxi.gwt.maths.client.game;

public class MinusQuestion extends MathsQuestion {
  public MinusQuestion(int upto) {
    int first = random(upto);
    int second = random(first - 1);
    setQuestion(first, second, '-', first - second);
  }
}

package net.kabutz.maxi.gwt.maths.client.game;

public class TimesQuestion extends MathsQuestion {
  public TimesQuestion(int upto) {
    int first = random((int) Math.sqrt(upto));
    int num = random(upto);
    int second = num / first;
    setQuestion(first, second, '×', first * second);
  }
}

package net.kabutz.maxi.gwt.maths.client.game;

public class DivideQuestion extends MathsQuestion {
  public DivideQuestion(int upto) {
    int top = random(upto);
    int second = random((int) Math.sqrt(top));
    int first = top / second * second;
    setQuestion(first, second, '÷', first / second);
  }
}
  
We put these together in a MathsBean and MathsGameBean. Each MathsBean has a pointer to a MathsQuestion, which we could see as a strategy instance. We could also generate these questions on the server and return them as XML to the browser. I am not showing it here, but it would be really easy to do. You need to just let the classes implement com.google.gwt.user.client.rpc.IsSerializable. The GWT assistance in IntelliJ IDEA 6 is excellent and will help you get all the pieces fitting together (demo).
package net.kabutz.maxi.gwt.maths.client.game;

public class MathsBean {
  private final MathsQuestion strategy;
  private boolean correct = false;

  public MathsBean(int upto) {
    strategy = makeRandomQuestion(upto);
  }
  private MathsQuestion makeRandomQuestion(int upto) {
    switch ((int) (Math.random() * 4)) {
      default:
      case 0: return new PlusQuestion(upto);
      case 1: return new MinusQuestion(upto);
      case 2: return new TimesQuestion(upto);
      case 3: return new DivideQuestion(upto);
    }
  }
  public String getQuestion() {
    return strategy.getQuestion();
  }
  public boolean checkAnswer(int answer) {
    return correct = strategy.getAnswer() == answer;
  }
  public int getAnswer() {
    return strategy.getAnswer();
  }
  public boolean isCorrect() {
    return correct;
  }
}
  
The MathsGameBean holds a number of MathsBeans and remembers how long we have been answering the questions for. Each MathsBean knows whether it was answered correctly or not, so we do not keep the information inside the MathsGameBean, but look it up on request.
package net.kabutz.maxi.gwt.maths.client.game;

public class MathsGameBean {
  private int currentQuestion = 0;
  private long start = System.currentTimeMillis();
  private final MathsBean[] questions;

  public MathsGameBean(int questions, int upto) {
    this.questions = new MathsBean[questions];
    for (int i = 0; i < questions; i++) {
      this.questions[i] = new MathsBean(upto);
    }
  }
  public boolean hasNext() {
    return currentQuestion < questions.length;
  }
  public MathsBean next() {
    return questions[currentQuestion++];
  }
  public int getScore() {
    int correct = 0;
    for (int i = 0; i < questions.length; i++) {
      correct += questions[i].isCorrect() ? 1 : 0;
    }
    return correct;
  }
  public int getTotalQuestions() {
    return questions.length;
  }
  public int getSeconds() {
    return (int) ((System.currentTimeMillis() - start + 500) / 1000);
  }
  public int getRate() {
    return getTotalQuestions() * 60 / getSeconds();
  }
}
  
We use these classes to write a simple Maths Tutor.

Binding Into GWT

The main entry class that we will use is Maths. It has two panels, MathsGameSetupPanel and MathsGamePlayPanel, which contain components used to either setup the game, or play it.
Here is the magic of GWT. All of the Java code you have seen up to now, and the Java code that we will show next, is turned into JavaScript code. This is supposed to be portable between browsers, but is not always. You still need to test it on all browser / OS combinations!
Since we are swapping between the "setup" and "play" pages, we implemented a very basic state pattern for managing the state transitions. There are many ways of implementing the state pattern. In this approach, we pass a pointer to Maths into the state, to allow it to switch to the next page. We do that with the methods setupGame() and playGame().
package net.kabutz.maxi.gwt.maths.client;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.user.client.ui.*;

public class Maths implements EntryPoint {
  private Panel currentPanel;

  public void onModuleLoad() {
    setupGame();
  }

  public void setupGame() {
    setPanel(new MathsGameSetupPanel(this));
  }

  public void playGame(int questions, int upto) {
    setPanel(new MathsGamePlayPanel(this, questions, upto));
  }

  private void setPanel(Panel panel) {
    if (currentPanel != null) {
      RootPanel.get().remove(currentPanel);
    }
    currentPanel = panel;
    RootPanel.get().add(currentPanel);
  }
}
  
We create the setup panel with two text boxes and a submit button. The CSS style used is "game-style", described further down in the newsletter. The default values for the number of questions is "10" and the default maximum number is "50". It looks like a typical Swing application, but generates JavaScript when you compile it with GWT.
package net.kabutz.maxi.gwt.maths.client;

import com.google.gwt.user.client.ui.*;

public class MathsGameSetupPanel extends VerticalPanel {
  private TextBox questions = new TextBox();
  private TextBox upToNumber = new TextBox();

  public MathsGameSetupPanel(final Maths owner) {
    setStyleName("game-style");

    questions.setText("10");
    upToNumber.setText("50");

    add(makeInput("Number of questions:", questions));
    add(makeInput("Up To Number:", upToNumber));
    add(new Button("Play Game!", new ClickListener() {
      public void onClick(Widget widget) {
        int numQues = Integer.parseInt(questions.getText());
        int upto = Integer.parseInt(upToNumber.getText());
        owner.playGame(numQues, upto);
      }
    }));
  }

  private Panel makeInput(String label, TextBox inputText) {
    HorizontalPanel inputPanel = new HorizontalPanel();
    inputPanel.add(new Label(label));
    inputPanel.add(inputText);
    return inputPanel;
  }
}
  
The actual game play panel shows the question and a textbox where we can enter the answer. We register a keyboard listener on the answer textbox. When we press enter, we check the answer, showing the correct answer if necessary. (Clue: We can add listeners to all sorts of characters with the KeyboardListener.) We apply a different style to the remark label, depending on whether it was correct or not.
Part of the checkAnswer() method involves checking whether we have further questions. If we do, we display them, otherwise we show the final score and display a button to "Play again!".
package net.kabutz.maxi.gwt.maths.client;

import com.google.gwt.user.client.ui.*;
import net.kabutz.maxi.gwt.maths.client.game.*;

public class MathsGamePlayPanel extends VerticalPanel {
  private MathsGameBean mathsGame;
  private MathsBean maths;
  private TextBox answerBox = new TextBox();
  private Label questionLabel = new Label();
  private Label remark = new Label();
  private Maths owner;

  public MathsGamePlayPanel(Maths owner, int questions, int upto) {
    setStyleName("game-style");
    this.owner = owner;
    mathsGame = new MathsGameBean(questions, upto);
    maths = mathsGame.next();

    questionLabel.setText(maths.getQuestion());

    answerBox.setMaxLength(10);
    answerBox.setWidth("60px");
    answerBox.addKeyboardListener(new KeyboardListenerAdapter() {
      public void onKeyPress(Widget widget, char c, int i) {
        if (c == 13) {
          checkAnswer();
        }
      }
    });

    Panel questionPanel = new HorizontalPanel();
    questionPanel.add(questionLabel);
    questionPanel.add(answerBox);

    add(questionPanel);
    add(remark);
  }

  private void checkAnswer() {
    int answer = Integer.parseInt(answerBox.getText());
    if (maths.checkAnswer(answer)) {
      remark.setText("Well Done!");
      remark.setStyleName("correct-answer");
    } else {
      remark.setText("Nope! " + maths.getQuestion() +
          maths.getAnswer() + " not " + answer);
      remark.setStyleName("wrong-answer");
    }
    answerBox.setText("");

    if (mathsGame.hasNext()) {
      maths = mathsGame.next();
      questionLabel.setText(maths.getQuestion());
    } else {
      add(new Label(mathsGame.getScore() + " / " +
          mathsGame.getTotalQuestions()));
      add(new Label(mathsGame.getRate() + " answers per minute"));
      add(new Button("Play again!", new ClickListener() {
        public void onClick(Widget widget) {
          owner.setupGame();
        }
      }));
      answerBox.setEnabled(false);
    }
  }
}
  
The HTML page would be rather basic. We could name the element that we add our panels to, but in our case all we want to do is to add and remove panels to the bottom of the page. Here is the Maths.html file, positioned in the "public" directory constructed by the GWT applicationCreator.
<html>
  <head>
    <title>Welcome to Maxi Maths</title>
    <link rel=stylesheet href="Maths.css">
    <meta name='gwt:module' content='net.kabutz.maxi.gwt.maths.Maths'>
  </head>
  <body>
    <script language="javascript" src="gwt.js"></script>
    <h1>Welcome to Maxi Maths</h1>
  </body>
</html>
  
There is quite a lot we can do with CSS. The styles are applied from within the GWT code with setStyleName("name"). Here we see the Maths.css file, which should be in the same directory as the Maths.html file:
body {
  font-family: sans-serif;
  font-size: small;
}

.game-style {
  background-color: #00FF00;
  border: 3px solid orange;
}

.game-style * {
  padding: 6px;
  font-family: cursive;
  font-size: large;
}

.correct-answer {
  font-style: italic;
  color: blue;
}

.wrong-answer {
  font-style: oblique;
  color: red;
}
  
If you put all this together, you get a nice little maths tutor to amuse your kids with.

Further Thoughts on GWT

GWT seems a more comfortable approach for a Java programmer than scratching around in JavaScript. A lot of the coding approaches with Swing can be applied to GWT. However, you do need to control the browsers that your clients will use to connect to your webpage. If someone tries to connect with IE 5, they might just see a blank page.

created by Dr. Heinz M. Kabutz and Maximilian F. Kabutz

0 komentar:

Posting Komentar

Twitter Delicious Facebook Digg Stumbleupon Favorites More

 
This Theme Modified by Kapten Andre based on Structure Theme from MIT-style License by Jason J. Jaeger