Thursday, December 15, 2011

Program: Dice Bag

It's finally finished! Took a lot longer than it should have, and is messy as hell, but it's done. Now, let us analyze the code, shall we? First, the Class header. Basically, the top of and identification for this program.

public class DiceBag extends JFrame implements ActionListener {

The important part is the "public class DiceBag." That names the program, and tells the computer that everything within that bracket at the end of the line and its partner at the bottom of the code belongs to the DiceBag program. The rest of the line is for implementing a GUI (Graphical User Interface) and allowing the program to recognize when you've clicked a button. Next, we look at the Main method. The Main is an essential part of any non-applet Java program. It's where the program goes to start everything. This is what Dice Bag's Main method looks like:

public static void main(String[] args) {
        JFrame dB = new DiceBag();
        dB.setVisible(true);
    }

Not much to look at, is it? Especially for being so important. Basically, all it's saying is to call on the DiceBag method to create a new window, then display that window. So, how is this messy? Well, let's look at the DiceBag method.

public DiceBag() {
        setTitle("Dice Bag");
        setSize(250,250);
        setLocation(500,200);
        setLayout(new GridLayout(7,4));   
        setDefaultCloseOperation(EXIT_ON_CLOSE);
               
        Label plus = new Label("+");
        Label plus2 = new Label("+");
        Label plus3 = new Label("+");
        Label plus4 = new Label("+");
        Label plus5 = new Label("+");
        Label plus6 = new Label("+");
        Label plus7 = new Label("+");
       
        d4.addActionListener(this);
        d6.addActionListener(this);
        d8.addActionListener(this);
        d10.addActionListener(this);
        d12.addActionListener(this);
        d20.addActionListener(this);
        d100.addActionListener(this);
       
        add(nD4);
        add(d4);
        add(plus);
        add(p4);
       
        add(nD6);
        add(d6);
        add(plus2);
        add(p6);
       
        add(nD8);
        add(d8);
        add(plus3);
        add(p8);
       
        add(nD10);
        add(d10);
        add(plus4);
        add(p10);
       
        add(nD12);
        add(d12);
        add(plus5);
        add(p12);
       
        add(nD20);
        add(d20);
        add(plus6);
        add(p20);
       
        add(nD100);
        add(d100);
        add(plus7);
        add(p100);
    }

This type of method is known as a constructor, and is the only type of method allowed to share the same name as the Class it is contained in. A Class is Java's word for a program. Well, that's not entirely accurate. A Class is an object that can be use by a program, but every program is, itself, a Class. It's complicated, I know, but don't think on it too hard. That bit's not real important to grasp. Look at the first block of lines. The first line tells the program to display its name in the top bar of the window it creates. the next two lines set the dimensions of the window, and its position on your screen. The fourth line tells it what layout to use when we start adding components, and the final line tells it to shut down the program when you click on the close button in the top bar. All the other lines? All they do is create and place the various buttons and text fields that the program uses.

Once all this is set up, the Main makes it all visible, and it looks like this:
It ain't much, especially for the volume of code it took to create it, but it works for what we need.
Before we go on, I want to note something here. For those of you familiar with programming, you'll probably have realized this, but for those of you who aren't I wanted to let you know. Every Button, Text Field, and Label I used to create this window was declared within the Class brackets, but outside any Method brackets. The significance of this is that, because of that, they are World variables. Meaning they can be accessed and manipulated by any Method within the Class. Local variables, ones declared and defined within a Method, can only be accessed and manipulated by the Method they were created in. So all those components already existed before the Constructor Method was called, but they were not defined until after it was finished. Code blocks 2 and 3 of the constructor defined them. The remaining blocks placed them onto the window.

Okay, now we have a pretty window with buttons and fields, but now what? The constructor didn't call on another method. The program has stopped. Well, remember the first thing we looked at? The Class header said it extends JFrame, which is what it used to create the window in the first place, and implements ActionListener. ActionListener is a handy little thing that "listens" for any action taken by the user. Like, say, clicking on a button. Any program that uses ActionListener must have a Method called "actionPerformed." That Method in Dice Bag looks like this:

public void actionPerformed(ActionEvent arg0) {
        Object b = arg0.getSource();

            if (b == d4){
                nD4 = runCheckN(nD4);
                p4 = runCheckP(p4);
                rollDice(Integer.parseInt(nD4.getText()), Integer.parseInt(p4.getText()), 4);
            }

            if (b == d6){
                nD6 = runCheckN(nD6);
                p6 = runCheckP(p6);
                rollDice(Integer.parseInt(nD6.getText()), Integer.parseInt(p6.getText()), 6);
            }
       
            if (b == d8){
                nD8 = runCheckN(nD8);
                p8 = runCheckP(p8);
                rollDice(Integer.parseInt(nD8.getText()), Integer.parseInt(p8.getText()), 8);
            }
       
            if (b == d10){
                nD10 = runCheckN(nD10);
                p10 = runCheckP(p10);
                rollDice(Integer.parseInt(nD10.getText()), Integer.parseInt(p10.getText()), 10);
            }
       
            if (b == d12){
                nD12 = runCheckN(nD12);
                p12 = runCheckP(p12);
                rollDice(Integer.parseInt(nD12.getText()), Integer.parseInt(p12.getText()), 12);
            }
       
            if (b == d20){
                nD20 = runCheckN(nD20);
                p20 = runCheckP(p20);
                rollDice(Integer.parseInt(nD20.getText()), Integer.parseInt(p20.getText()), 20);
            }
       
            if (b == d100){
                nD100 = runCheckN(nD100);
                p100 = runCheckP(p100);
                rollDice(Integer.parseInt(nD100.getText()), Integer.parseInt(p100.getText()), 100);
            }
    }

Whenever you click one something in the window created by the Constructor Method, it runs this Method. This Method then runs a series of If Statements to see if what you clicked on is supposed to do anything. In this case, if you didn't click on one of the seven buttons, the program ignores the click. However, if you did click on a button, it figures out which button you clicked, and take the appropriate action. In the case of Dice Bag, the actions performed for each button are nearly identical. The only difference is the Text Fields being referenced, and the size of the virtual die or dice being rolled. So, let's look at the Methods that are called, in the order they are called.

The first line of every If Statement says "nD# = runCheckN(nD#);". This tells the program to take the Text Field that was placed before the button clicked, and change the value contained in it according to the instructions in the runCheckN Method. The runCheckN Method is largely a test for a valid value, and will only change an invalid value.

private TextField runCheckN(TextField n) {
        try{
            int test = Integer.parseInt(n.getText());
        }
        catch (NumberFormatException ex){
            n.setText("1");
        }
       
        return n;
    }

First thing to notice is that, up until now, every Method has begun with either "public/private methodName," or "public/private void methodName." That's because the Methods we've looked at up to this point haven't returned any values. This one does. It returns a TextField object. Also, when called, it requires that a TextField object be given to it for evaluation. Dice Bag always gives it the TextField object it is checking and potentially changing the value of. Now, this Method uses the Try/Catch statements that are used in Java to prevent a program from crashing if a critical error occurs. Within the Try brackets, I told the computer to attempt to turn the contents of the TextField into and "int." An int, which is short for integer, is one of the most basic variables a Java program can use. However, a TextField contains a String, which is a string of characters produced by a keyboard. As such, a TextField object can contain a value equal to anything you can type on your keyboard. There are ways of turning them all into numbers, which is what is required in order for them to fit within an int variable, but that's not exactly what we want. We want to make sure that the user has typed in a number into the TextField object to begin with. To this end, we create a new, local int variable called "test," and try to put the contents of the TextField into it. If the user typed in a number, then this works fine, and the Method completes without really doing anything. However, if the user left the field blank, or put anything but a number into it, it creates an error that the program doesn't know how to deal with. Normally, this results in the program simply shutting down. However, because it happened inside the Try Statement, it instead passes it to the Catch Statement.

The first part of the Catch Statement says "(NumberFormatException ex)," meaning it is looking for a specific kind of error. Any other errors will not be caught, and will result in the program crashing. However, there are no other errors that can occur at this point, so it works fine. When one of these errors is passed to it, it carries out the instructions inside of its brackets. In this case, it merely changes the value contained within the TextField object from an invalid value to the number 1. Because the TextField object originally passed to this Method represented the number of dice to be rolled, an invalid value results in only one die of the appropriate size being rolled.

Whether the value is changed or not, the TextField object is then returned, and the original nD# Text Field is changed to the value of "n".

The next Method called is nearly identical to the one we just looked at. However, it deals with the other TextField object in the row of the button clicked, p#, which represents the bonus (or penalty) applied to the roll.

private TextField runCheckP(TextField p) {
        try{
            int test = Integer.parseInt(p.getText());
        }
        catch (NumberFormatException ex){
            p.setText("0");
        }
       
        return p;
    }

Method runCheckP does exactly the same thing as runCheckN, except it changes the value, if there is an error, to 0. By default, no bonuses or penalties will be applied to the roll.

This brings us to the last line of all those If Statements: "rollDice(Integer.parseInt(nD#.getText()), Integer.parseInt(p#.getText()), #);." This line calls the rollDice Method, passing to it the contents of the TextField objects, after converting them into int values, as well as the number of sides of the die type being rolled. The Method is as follows:

private void rollDice(int tN, int tP, int d) {
        int t = 0;
        Random die = new Random();
        String result = "(";
       
        for (int i=0; i < tN; i++){
            int roll = 1 + die.nextInt(d);
           
            if (i == 0){
                result = result + roll;
            }
            else {
                result = result + " + " + roll;
            }
           
            t = t + roll;
        }
       
        t = t + tP;
       
        result = result + " ) + " + tP + " = " + t;
       
        JFrame displayResult = new DiceBag(result);
        displayResult.setVisible(true);
    }

First thing it does is create a new int variable called "t." This variable will be our total after everything is added up. The next line creates a random number generator that will act as our dice. The third line creates a String type variable to allow us to print the result onto the screen. Then we move on to a For Loop. Wonderful things. They create an internal int variable that starts at whatever value you want, checks it against a certain condition, then incrementally increases or decreases the value of the internal variable. This repeats until the condition is no longer met. In this case, the internal variable starts at 0, and increases until it is no longer less than the number dice we're rolling. Once it equals the number of dice, the loop breaks, and the program continues. The If/Else statement within the For Loop makes sure that the "+" isn't added to the front of the first die roll, but is added to each subsequent roll. To make this clearer, if we wanted to roll 3 4-sided dice, with no bonuses or penalties, and we got results of 3, 2, and 4, then the program would display a result of "(3 + 2 + 4) + 0 = 9." Without the for loop, either we'd have to do away with the plus signs all together, or we'd end up with a plus sign in front of the 3. Neither is what we want. Outside of the If/Else Statement, but still inside the For Loop, the variable "t" is being added to each time, adding the roll to the current total.

Once the For Loop breaks, the Method finishes making the String that it will use to display the results. Then, it gets interesting. The Method calls a Constructor, but not the same Constructor the Main Method called, despite having the same name. See, this time, the Method passes the result String to the constructor. This simple difference tells the computer to run a completely different Constructor. This one, in fact:

public DiceBag(String r){
        add(resultField);
        resultField.setText(r);
        setSize(500,100);
        setLocation(600,400);
    }

This Constructor creates a new window, placing in it a pre-declared Label, and setting the text in the Label to the result String. Then it sets the size and position on the screen before displaying the result. The only problem I can see with this, and I'm not sure it's an actual problem, is that there is no line that tells it to actually close when you click on the close button. It will set the window to invisible, but it might be still using memory. Also, every time you click on a button in the main window, it might be creating an entirely new window, instead of just editing the existing window. By the time a good night of gaming has gone by, your computer's memory might be cluttered with hundreds of invisible result windows. However, adding that functionality as I did with the main window might shut down the entire program, not just that window. It's something I will experiment with in future versions of the program, but, for now, this works.

So, there it is. My dice bag program. If anyone reading this has suggestions for better way to program the same results, please, by all means let me know. I do read the comments on this blog. Not that there are many, yet, but I do. Also, I've already figured out that, instead of declaring 7 Labels, and setting them all to say "+", I can instead create them on the spot. Less lines that way. Until next time!

3 comments:

  1. 1st suggestion for user-friendliness: instead of just a text box for dice numbers, maybe include +/- buttons to add-subtract the numbered dice? Maybe a clear-all-entries button to wipe the dice numbers clean?

    2nd suggestion: I would make the whole panel of text-box + labels + button + action listener to be one whole object (encapsulate all that in a JPanel/JPanel subclass). Then you could have a program that has unlimited options for however many sides you want on a dice.
    (also I think it would reduce the number of lines in your code drastically.)
    Look into the JMenu/JMenuItem classes in the javax.swing package and use that for the user to select whether or not they want to add a new #-sided dice. Also think about an option to clear all the dice on the screen if the users want a fresh start for whatever reason.

    -Josh :)

    ReplyDelete
  2. And this is why we love you. I will see if I can't do that with my next version of the program. However, those ideas also happen to work perfectly for my next project, which I need to start working on first. So, yeah, my next project will have a much better V1.0 with those suggestions. Domo arigato!

    ReplyDelete
  3. If I see this on the Android market, I demand a 10% share of everything.

    ReplyDelete