5
\$\begingroup\$

I have a single file of a java Bingo program that I want to split into three separate classes (BingoMain, BingoGUI, BingoCard). Extending a program across several files is something I have never really done, so a little help is appreciated. I also want to add the ability to extend the current program to include functionality for up to 5 players. I know that all the code I need is pretty much in here already. Which lines/parts would go in which class and what would I edit in order to add the desired functionality? I don't expect any code, just some directions to get me on my way.

import java.awt.Container;
import java.awt.Font;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import javax.swing.*;



public class Bingo extends JFrame {

      public static void main (String[] args) {

         Integer[][] bingoCard = new Integer[5][5];
         boolean[][] called = new boolean[][] {  {false,false,false,false,false},
                                                 {false,false,false,false,false},
                                                 {false,false,true ,false,false},
                                                 {false,false,false,false,false},
                                                 {false,false,false,false,false}};
         JFrame myBingoGUI;
         /*
          *  Fill an array with 15 numbers (1-15) for the "B" column, put the numbers in random order, and
          *  put the first 5 of those numbers in the "B" column of the bingo card. Continue with I,N,G,and O columns.
          */
         Integer[] row = new Integer[15];

         for (int i=0; i<5; i++) {
            fillRow(row, i*15+1);
            randomize (row);
            putRowInBingoCard(bingoCard,i,row);
         }
         /*
          *   Print the bingo card.
          */
         myBingoGUI=new JFrame();
         myBingoGUI.setSize(250, 250);
         myBingoGUI.setLocation(400, 400);
         myBingoGUI.setTitle("BINGO");
         myBingoGUI.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
         myBingoGUI.setVisible(true);
         Container myContentPane = myBingoGUI.getContentPane();
         JTextArea myTextArea = new JTextArea();
         myTextArea.setFont(new Font("Monospaced", Font.PLAIN,14));
         myContentPane.add(myTextArea);
         myTextArea.append(toStringBingoCard(bingoCard,called));
         System.out.println(toStringBingoCard(bingoCard,called));
      /*
       *   Play the game:   
       */
         while (!winner(called)) {
            String calledNumber = JOptionPane.showInputDialog(null,"Enter a BINGO call:");
            int calledColumn=-1;
            /*
             *   The first character of the input string is converted to a column number between 0 and 4
             */
            if (Character.toUpperCase(calledNumber.charAt(0))=='B') calledColumn=0;
            if (Character.toUpperCase(calledNumber.charAt(0))=='I') calledColumn=1;
            if (Character.toUpperCase(calledNumber.charAt(0))=='N') calledColumn=2;
            if (Character.toUpperCase(calledNumber.charAt(0))=='G') calledColumn=3;
            if (Character.toUpperCase(calledNumber.charAt(0))=='O') calledColumn=4;  
            /*
             *  The remainder of the input string is converted to an integer
             */
            int calledValue = Integer.parseInt(calledNumber.substring(1,calledNumber.length()));
            /*
             *   The matrix of called numbers is update to show the number has been called.
             */
            for (int i=0;i<5;i++)
               if (bingoCard[i][calledColumn]==calledValue) called[i][calledColumn]=true;
            /*
             *   Print the updated BINGO card.
             */
            myTextArea.setText("");
            myTextArea.append(toStringBingoCard(bingoCard,called));
            System.out.println(toStringBingoCard(bingoCard,called));

         } // while
         myTextArea.append("\n\nBINGO!!");

      } // main

      //======================================================================================================

      /*
       *   This method is just here for purposes of testing the program while it is being developed
       */
      public static void printArray (Integer[] a) {
         for (int i=0; i<a.length; i++)
            System.out.print(" "+ a[i].intValue());
         System.out.println();
      }
      /*
       *   Fill an array of Integers with 15 numbers with a given starting value.
       */
      public static void fillRow(Integer[] theRow, int startValue){
         for (int i = 0; i< 15; i++)
            theRow[i]= new Integer(startValue++);
      }
      /*
       *   Take the array of sequential Integers and put them in random order.
       */
      public static void randomize (Integer[] theArray) {
         Collections.shuffle(Arrays.asList(theArray));
      }
      /*
       *  The BINGO card needs only five of the shuffled numbers, so just put in the first five.
       */
      public static void putRowInBingoCard(Integer[][]card, int column, Integer[] row){
         for (int i=0; i<5; i++)
            card[i][column] = row[i];
      }
      /*
       *   Print the BINGO card.
       */
      public static void printBingoCard(Integer[][] bcard,boolean[][] called) {
         System.out.println(" B     I     N     G     O");
         for (int i=0;i<5;i++) {
            for (int j=0; j<5;j++) {

               if (i==2 && j==2) System.out.print("<**>");
               else if (called[i][j]) System.out.print("<"+bcard[i][j]+">");
               else
                  System.out.print(" "+bcard[i][j]+" ");
               if (bcard[i][j]<10) System.out.print("   ");
               else System.out.print("  ");


            } // for j   
            System.out.println();   
         }  // for i      
      } //printBingoCard

      public static String toStringBingoCard(Integer[][] bcard,boolean[][] called) {
         String returnString="";
         returnString+= (" B     I     N     G     O \n");
         for (int i=0;i<5;i++) {
            for (int j=0; j<5;j++) {

               if (i==2 && j==2) returnString+= ("<**>");
               else if (called[i][j]) returnString+= ("<"+bcard[i][j]+">");
               else
                  returnString+= (" "+bcard[i][j]+" ");
               if (bcard[i][j]<10) returnString+= ("   ");
               else returnString+= ("  ");


            } // for j   
            returnString+= "\n";   
         }  // for i  
         return returnString;
      } //toStringBingoCard

      public static boolean winner(boolean[][] called){
         boolean iAmAWinner = true;
         // winner in a row?
         for (int i=0;i<5;i++) {
            for (int j=0;j<5;j++)
               iAmAWinner &= called[i][j];
            if(iAmAWinner) return true;
            else 
               iAmAWinner= true;
         } //for

         // winner in a column?
         for (int i=0;i<5;i++) {
            for (int j=0;j<5;j++)
               iAmAWinner &= called[j][i];
            if(iAmAWinner) return true;
            else 
               iAmAWinner= true;
         } //for

         // winner in a diagonal?
         iAmAWinner=true;
         for (int i=0;i<5;i++) 
            iAmAWinner &= called[i][i];
            if(iAmAWinner) return true;

         iAmAWinner=true;
         for (int i=4;i>=0;i--) 
            iAmAWinner &= called[i][4-i];
            if(iAmAWinner) return true;


         return false;
      }
\$\endgroup\$
3
\$\begingroup\$

When you have a working code, even if not in ideal shape, you should not try to impose preconceived architecture on it. Instead make small changes that do not change behavior, but takes you to your goal one step closer, or alleviate one problem. This method of evolving code while maintaining functionality is called Refactoring.

This is a number of small changes I made. I try give the names of automatic refactorings from Eclipse as much as I can.

  1. Remove extend Jframe from Bingo

  2. Extract method: called column parsing to int calledColumnOf(String) method

  3. Extract method: called value parsing to int calledValueOf(String)

  4. Introduce parameter object: toStringBingoCard(bingoCard, called) --> toStringBingoCard(new BingoCard(bingoCard, called))

  5. Move method: toStringBingoCard -> BingoCard.toString

    • Copy&paste (only for a moment) toStringBingoCard to toStringBingoCard_temp
    • Override toString as just return toStringBingoCard_temp(this);
    • Inline method toStringBingoCard_temp
    • change body of toStringBingoCard as jus return bingoCard.toString()
    • Inline method toStringBingoCard
  6. Extract local variable: BingoCard theBingoCard = new BingoCard(bingoCard, called)

  7. Remove unused method: printBingoCard

  8. Remove unused method: printArray

  9. Extract method: the loop that updates called to void numberCalled(Integer[][], boolean[][] ...)

  10. Make numberCalled and instance method of BingoCard as with toString

  11. Introduce parameter object: numberCalled to BingoNumber

  12. Extract method: code to get called number to getCalledNumber

  13. Move method: getCalledNumber to a class NumberCaller, make it an instance method.

  14. Extract method: extract everything in the main(...) except initialization to a method called gameLoop

  15. Introduce parameter object: theBingoCard, bingoCardView, numberCaller to BingoGame

  16. Move method: make gameLoop and instance method of BingoGame

  17. Move method: getInitialCard and helpers to a new class BingoCardFactory.

  18. Rename method: getInitialCard to create.

  19. Move method: createView to a new class BingoViewFactory.

  20. Rename method: createView to create.

  21. Encapsulate field: BingoNumbers fields. no setters.

  22. Encapsulate field: BingoGames fields. no setters.

End result thus far:

public class Bingo {
    public static void main(String[] args) {
        BingoCard theBingoCard = BingoCardFactory.create();
        BingoCardView bingoCardView = BingoViewFactory.creteView();
        NumberCaller numberCaller = new NumberCaller();

        BingoGame bingoGame = new BingoGame(theBingoCard, bingoCardView, numberCaller);

        bingoGame.gameLoop();
    }
}


public class BingoCard {
    private Integer[][] bcard;
    private boolean[][] called;

    public BingoCard(Integer[][] bcard, boolean[][] called) {
        this.bcard = bcard;
        this.called = called;
    }

    @Override
    public String toString() {
           // former toStringBingoCard method....
    }

    public void numberCalled(BingoNumber bingoNumber) {
        for (int i = 0; i < 5; i++)
        if (this.bcard[i][bingoNumber.getColumn()] == bingoNumber.getValue())
            this.called[i][bingoNumber.getColumn()] = true;
    }

    public boolean isWinner() {
           // former winner method....
    }
}


public class BingoCardFactory {
    public static BingoCard create() {
        Integer[][] bingoCard = new Integer[5][5];
        boolean[][] called = new boolean[][] {
                { false, false, false, false, false },
                { false, false, false, false, false },
                { false, false, true, false, false },
                { false, false, false, false, false },
                { false, false, false, false, false } };

        for (int i = 0; i < 5; i++) {
            Integer[] row = new Integer[15];
            fillRow(row, i * 15 + 1);
            randomize(row);
            putRowInBingoCard(bingoCard, i, row);
        }

        BingoCard theBingoCard = new BingoCard(bingoCard, called);
        return theBingoCard;
    }

    private static void fillRow(Integer[] theRow, int startValue) {
        for (int i = 0; i < 15; i++)
            theRow[i] = new Integer(startValue++);
    }

    private static void randomize(Integer[] theArray) {
        Collections.shuffle(Arrays.asList(theArray));
    }

    private static void putRowInBingoCard(Integer[][] card, int column,
            Integer[] row) {
        for (int i = 0; i < 5; i++)
            card[i][column] = row[i];
    }

}

public class BingoCardView {
    private JTextArea myTextArea;

    public BingoCardView(JTextArea myTextArea) {
        this.myTextArea = myTextArea;
    }

    public void updateBingoCardView(BingoCard theBingoCard) {
        this.myTextArea.setText(theBingoCard.toString());
    }
    public void showWinMessage() {
        this.myTextArea.append("\n\nBINGO!!");
    }
}

public class BingoGame {
    private BingoCard bingoCard;
    private BingoCardView bingoCardView;
    private NumberCaller numberCaller;

    public BingoGame(BingoCard bingoCard, BingoCardView bingoCardView,
            NumberCaller numberCaller) {
       //......
    }

    public void gameLoop() {
        this.bingoCardView.updateBingoCardView(this.bingoCard);

        while (!this.bingoCard.isWinner()) {
            BingoNumber bingoNumber = this.numberCaller.getCalledNumber();
            this.bingoCard.numberCalled(bingoNumber);

            this.bingoCardView.updateBingoCardView(this.bingoCard);
            System.out.println(this.bingoCard.toString());

        } 
        this.bingoCardView.showWinMessage();
    }
}

public class BingoNumber {
    private int column;
    private int value;

    public BingoNumber(int column, int value) {
        this.column = column;
        this.value = value;
    }

    public int getColumn() {
        return column;
    }

    public int getValue() {
        return value;
    }
}

public class BingoViewFactory {
    public static BingoCardView creteView() {
        JFrame myBingoGUI;
        myBingoGUI = new JFrame();
        myBingoGUI.setSize(250, 250);
        myBingoGUI.setLocation(400, 400);
        myBingoGUI.setTitle("BINGO");
        myBingoGUI.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        myBingoGUI.setVisible(true);
        Container myContentPane = myBingoGUI.getContentPane();
        JTextArea myTextArea = new JTextArea();
        myTextArea.setFont(new Font("Monospaced", Font.PLAIN, 14));
        myContentPane.add(myTextArea);
        BingoCardView bingoCardView = new BingoCardView(myTextArea);
        return bingoCardView;
    }

}

public class NumberCaller {
    public BingoNumber getCalledNumber() {
        String calledNumber = JOptionPane.showInputDialog(null,
                "Enter a BINGO call:");
        int calledColumn = calledColumnOf(calledNumber);
        int calledValue = calledValueOf(calledNumber);

        BingoNumber bingoNumber = new BingoNumber(calledColumn, calledValue);
        return bingoNumber;
    }

    private static int calledValueOf(String calledNumber) {
        int calledValue = Integer.parseInt(calledNumber.substring(1,
                calledNumber.length()));
        return calledValue;
    }

    private static int calledColumnOf(String calledNumber) {
        int calledColumn = -1;
        if (Character.toUpperCase(calledNumber.charAt(0)) == 'B')
            calledColumn = 0;
        if (Character.toUpperCase(calledNumber.charAt(0)) == 'I')
            calledColumn = 1;
        if (Character.toUpperCase(calledNumber.charAt(0)) == 'N')
            calledColumn = 2;
        if (Character.toUpperCase(calledNumber.charAt(0)) == 'G')
            calledColumn = 3;
        if (Character.toUpperCase(calledNumber.charAt(0)) == 'O')
            calledColumn = 4;
        return calledColumn;
    }
}

We can go on still much further. There still remains many things to be fixed. But in this state each code snippet is still recognizable from the original, to me at least.

Note, keeping to single responsibility principle, we ended up with more classes than initially asked for.

Separating classes according to functionality allows us to further modify our program more easily.

See for example, how easy would it be to change the view to use console instead of a JFrame.

A view does not extend JFrame or anything. It can be a frame, many frames, just console, or a network server. We do not know or care.

Same goes for the number caller also. It is quite easy to re-imagine this program as one of the clients of a server, where numbers are called centrally.

Next on the refactoring list should be, before altering behavior, extracting interfaces of these classes, such that it follows LID part of SOLID principles.

\$\endgroup\$
  • \$\begingroup\$ I might have got bored and skipped some steps. I can explaing any steps that you cannot follow. \$\endgroup\$ – abuzittin gillifirca Nov 26 '13 at 10:17
8
\$\begingroup\$

Review of Existing Code

  • Not object oriented: As you probably already realize, your code is written in a procedural style, not object-oriented, which is the norm in Java. You can tell because every function is static. Note that although you declared public class Bingo extends JFrame, it runs just the same if you remove extends JFrame. What's actually happening is that your myBingoGUI is the JFrame.
  • Prefer unboxed types: Unless you have a good reason to use boxed types (Integer), you should use the unboxed primitive instead (int). Boxed types make your code clumsy and incur unnecessary overhead. Although you do need to use an ArrayList<Integer> to take advantage of Collections.shuffle() while randomizing the board, I would still store the board using an unboxed int[][].
  • Initialization of called: This takes less code:

    boolean[][] called = new boolean[5][5];
    called[5/2][5/2] = true;
    
  • Helper functions should be private: Some of your functions are obviously intended for internal use only — for example, fillRow(), randomize(), putRowInBingoCard(). You would never want any other code to call putRowInBingoCard(), so mark it private instead of public. That goes for randomize() too — even though the function has some conceivable general utility, you wouldn't want to let a hypothetical Blackjack game use Bingo.randomize(), so don't expose it.
  • Misnomer: fillRow() and putRowInBingoCard() actually work on a column, not a row.
  • Dead code: printBingoCard() is never used. Likewise with printArray(), as you noted.
  • Simplification: The code to assign calledColumn and calledValue is unnecessarily verbose. The following is equivalent:

    int calledColumn = "BINGO".indexOf(Character.toUpperCase(calledNumber.charAt(0)));
    int calledValue = Integer.parseInt(calledNumber.substring(1));
    
  • Error handling: If you input an empty string, it crashes with a StringIndexOutOfBoundsException. If you give it just a number, it crashes with an ArrayIndexOutOfBoundsException. If you hit "Cancel" in the input dialog, it crashes with a NullPointerException. (I would expect it to exit gracefully — call dispose() on the frame and break from the loop.)

  • Use a StringBuilder: For repeated concatenation, use a StringBuilder for efficiency.
  • Naming convention for predicates: In Java, a convention is to name a method isSomething() if the method takes no arguments, performs a test with no side effects, and returns a boolean. Therefore, you should rename winner() to isWinner().

OOP Refactoring: First step

Notice that you pass the arrays bingoCard and called nearly everywhere. That is the state that should be stored as instance variables in a BingoCard object. I'll provide an outline of a BingoCard class for you to fill in:

public class BingoCard {
    private int[][] numbers;
    private boolean[][] called;

    public BingoCard() {
        numbers = new int[5][5];
        called = new boolean[5][5];
        called[5/2][5/2] = true;

        // Fill in numbers randomly as appropriate
        ...
    }

    public String toString() {
        ...
    }

    public boolean isWinner() {
        ...
    }

    public void mark(String calledNumber) throws IllegalArgumentException {
        ...
    }

    // Let's keep this here for now...
    public static void main (String[] args) {
        BingoCard card = new BingoCard();

        JFrame myBingoGUI;
        myBingoGUI=new JFrame();
        myBingoGUI.setSize(250, 250);
        myBingoGUI.setLocation(400, 400);
        myBingoGUI.setTitle("BINGO");
        myBingoGUI.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        myBingoGUI.setVisible(true);
        Container myContentPane = myBingoGUI.getContentPane();
        JTextArea myTextArea = new JTextArea();
        myTextArea.setFont(new Font("Monospaced", Font.PLAIN,14));
        myContentPane.add(myTextArea);

        myTextArea.setText(card.toString());
        System.out.println(card);

        // Play the game:   
        String error = "";
        String calledNumber = null;
        while (!card.isWinner() &&
               null != (calledNumber = JOptionPane.showInputDialog(null, error + "Enter a BINGO call:")) {
            try {
                card.mark(calledNumber);
                error = "";
            } catch (IllegalArgumentException badCall) {
                error = "Invalid input: " + badCall.getMessage() + "\n";
            }

            // Print the updated BINGO card.
            myTextArea.setText(card.toString());
            System.out.println(card);

       }
       if (card.isWinner()) {
           myTextArea.append("\n\nBINGO!!");
       }
       if (null == calledNumber) {
           myBingoUI.dispose();
       }
    }
}

If you need to, you can add helper methods. Remember, however, such helper methods should not be part of the class's interface, and should therefore be marked private. (Personally, I don't think that helpers are needed, when about a dozen lines of code in the constructor would suffice.)

Second Step: Separate UI from Model

The purpose of the BingoCard class is to represent the state of a card. The next step is to purge UI code from BingoCard. You should then be able to reuse the same BingoCard for a text console UI, a Swing UI, or a web-based UI.

The BingoCard and its UI should be loosely coupled. The UI needs to know when the state of a BingoCard has changed, so that it can redraw it. A common mechanism for this communication is to use a listener. Augment BingoCard with the following members:

public class BingoCard {
    public static interface Listener {
        void handleBingoCardChanged(BingoCard card);
    }

    private HashSet<Listener> listeners = new HashSet<Listener>;

    // ... the previously developed the BingoCard code here ...

    public void addListener(Listener l) {
        this.listeners.add(l);
    }

    public void removeListener(Listener l) {
        this.listeners.remove(l);
    }

    private void fireChangeEvent() {
        for (Listener l : this.listeners) {
            l.handleBingoCardChanged(this);
        }
    }
}

You should be able to figure out where to call fireChangeEvent().

Then you can implement the UI as

public class BingoUI extends JFrame implements BingoCard.Listener {
    private JTextArea textArea;

    public BingoUI(BingoCard card) {
        // Rough outline
        this.setSize(...);
        ...
        this.textArea = new JTextArea();
        card.addChangeListener(this);
        this.textArea.setText(card.toString());
    }

    public void handleBingoCardChanged(BingoCard card) {
        this.textArea.setText(card.toString());
        if (card.isWinner()) {
            this.textArea.append("\n\nBINGO!!");
        }
    }
}

At this point, you could implement a main() method in BingoUI. The initialization code will be much shorter, since you'll simply be calling the BingoCard and BingoUI constructors. The main loop will be not much different.

Generalizing to Multiple Players

I'll leave you with a challenge to implement a two-player game:

public class BingoMain {
    public BingoMain(int players) { ... }
    public void play() { ... }

    public static void main(String[] args) {
        new BingoMain(2).play();
    }
}
\$\endgroup\$
  • \$\begingroup\$ Sanity check: When you're done, BingoCard should only need to import java.util.*; BingoUI should only need to import java.awt.* and javax.swing.*. \$\endgroup\$ – 200_success Nov 11 '13 at 9:57
  • \$\begingroup\$ I don't really see, using what you call boxed types, as being clumsy. I can possible see it having extra overhead at runtime, but question if it has that much of an impact. I think this is more a personal preference thing, but I'm willing to be persuaded otherwise. Of coarse, I'm just nit picking a bit here, as either way doesn't hurt, in my opinion. Great answer. \$\endgroup\$ – James Hurford Nov 15 '13 at 3:43
3
\$\begingroup\$

Your choice of classes to split the code in to is a little off, in my opinion. I would create three classes and an interface...

The interface would look like this:

package bingo;

public interface BingoCardListener {
    public void setup(String name, int columns, int rows, int[][] values);
    public void called(int col, int row, int value);
    public void complete(boolean winner);
}

This is used by the BingoCard class to tell anyone who's interested in what is happening to it.

The three classes I would have are:

  1. BingoMain -> The main() method, and the control flow for selecting (and notifying) numbers.
  2. BingoCard -> This controls what the card state is. Changes to the state will be communicated to any listeners.
  3. BingoCardPanel -> This is the visual representation of the BingoCard. It implements the BingoCardListener interface.

I have taken the liberty of adding 5 cards (and their panels) to the system. It makes the logic for why things are done this way more sensible.

I have messed around with a few things in your code, but here are the three classes I have. Note, each BingoCard class chooses its own values, and manages its own state. The logic about what is selected and whether it wins or not is completely contained within BingoCard.

BingoCardPanel is purely a display tool. It does not contain any Bingo logic, but it changes the display state to match the BingoCard.

BingoMain is the main method only (and some constants).

I have put everything in the package 'bingo'.

The BingoCard class is a logical place to start:

package bingo;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Random;

public class BingoCard {

    public static int[] randomCol(int startValue, final int cols, final int rows, final int colrange){
        // this method essentially chooses 5 random values from a set of 15.
        int[] candidates = new int[colrange];
        for (int i = 0; i < candidates.length; i++) {
            candidates[i] = startValue + i;
        }
         // shuffle the candidates. If only there was an Arrays.shuffle(...).
        final Random rand = new Random(System.nanoTime());
        // Fischer-Yates shuffle.
        // http://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
        int i = candidates.length;
        while (--i > 0) {
            int tmp = candidates[i];
            int swap = rand.nextInt(i + 1);
            candidates[i] = candidates[swap];
            candidates[swap] = tmp;
        }
        // pull the first count values from the suffled result.
        return Arrays.copyOf(candidates, rows);
    }

    private final boolean[][] called; // = new boolean[COLS][ROWS];
    private int remaining; // how many values need to be called....
    private final int[][] values; // = new int[COLS][];
    private final String name;
    private final ArrayList<BingoCardListener> listeners = new ArrayList<>();
    private final int rows, cols;

    public BingoCard(final String name, final int columns, final int rows, final int colrange) {
        this.name = name;
        this.cols = columns;
        this.rows = rows;
        called = new boolean[columns][rows];
        called[columns / 2][rows / 2] = true;
        remaining = columns * rows - 1;
        values = new int[columns][];
        for (int i = 0; i < columns; i++) {
            values[i] = randomCol((i * colrange) + 1, columns, rows, colrange);
        }
    }

    public void addListener(BingoCardListener listener) {
        listeners.add(listener);

        // listener has been added. Let's catch it up with our current state.
        listener.setup(name, cols, rows, values);

        for (int c = 0; c < called.length; c++) {
            for (int r = 0; r < called[c].length; r++) {
                if (called[c][r]) {
                    listener.called(c, r, values[c][r]);
                }
            }
        }
    }

    public boolean called(final int col, final int value) {
        for (int r = 0; r < values[col].length; r++) {
            if (values[col][r] == value) {
                // we have a match;
                if (!called[col][r]) {
                    called[col][r] = true;
                    remaining--;
                    for (BingoCardListener listener : listeners) {
                        listener.called(col, r, value);
                    }
                } else {
                    System.out.println("For some reason value " + value + " has been called multiple times");
                }
            }
        }
        return remaining <= 0;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        for (int r = 0; r < rows; r++) {
            for (int c = 0; c < cols; c++) {
                if (called[c][r]) {
                    sb.append(String.format(" < %02d > ", values[c][r]));
                } else {
                    sb.append(String.format("   %02d   ", values[c][r]));
                }
            }
            sb.append("\n");
        }
        sb.setLength(sb.length() - 1);
        return sb.toString();
    }

    public void gameOver() {
        for (BingoCardListener listener : listeners) {
            listener.complete(remaining <= 0);
        }
    }

}

The toString() method is perhaps the most complicated ... ;-) Kidding, the randomCol() method is the most complex, but there are other ways to do it and I don't like converting ints to Integers and messing with Collections.shuffle() when I don't have to.

The BingoCardPanel is the next logical thing to show. It is actually quite simple, when you get around the fact that it really only contains a 2D array of Labels. The order the array is initialized is row-at-a-time. This is different to the BingoCard which does things column-at-a-time. This is OK, though, and makes sense for displaying things...

Really there's nothing complicated in this class except for a potential bug if you ever use more than 5 columns ("BINGO".substring(6,1) will fail - fix it):

package bingo;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.GridLayout;

import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.plaf.basic.BasicBorders;

public class BingoCardPanel extends JPanel implements BingoCardListener {

    private final JPanel valuepanel = new JPanel();
    private final JLabel namelabel = new JLabel("Unknown");
    private JLabel[][] labels = null;

    public BingoCardPanel() {
        super();
        setBorder(BasicBorders.getInternalFrameBorder());
        setLayout(new BorderLayout(0, 5));
        add(namelabel, BorderLayout.PAGE_START);
        add(valuepanel, BorderLayout.CENTER);
    }

    @Override
    public void setup(String name, int columns, int rows, int[][] values) {
        namelabel.setText(name);
        namelabel.setHorizontalAlignment(SwingConstants.CENTER);
        GridLayout layout = new GridLayout(rows + 1, columns, 5, 5);

        String bingo = "BINGO";
        for (int i = 0; i < columns; i++) {
            JLabel label = new JLabel(bingo.substring(i, i+1));
            label.setHorizontalAlignment(SwingConstants.CENTER);
            valuepanel.add(label);
        }
        valuepanel.setLayout(layout);
        labels = new JLabel[columns][rows];
        for (int r = 0; r < rows; r++) {
            for (int c = 0; c < columns; c++) {
                labels[c][r] = new JLabel("" + values[c][r]);
                labels[c][r].setHorizontalAlignment(SwingConstants.CENTER);
                valuepanel.add(labels[c][r]);
            }
        }

    }

    @Override
    public void called(final int col, final int row, int value) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                labels[col][row].setOpaque(true);
                labels[col][row].setBackground(Color.GREEN);
            }
        });

    }

    @Override
    public void complete(final boolean winner) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                setBackground(winner ? Color.GREEN : Color.RED);
            }
        });
    }

}

Finally, BingoMain does the control loops. I have added some validation, and other controls. Note, it is now only a single main method.

package bingo;
import java.awt.Container;
import java.awt.GridLayout;

import javax.swing.JFrame;
import javax.swing.JOptionPane;



public class BingoMain extends JFrame {

    private static final int ROWS = 5;
    private static final int COLS = 5;
    private static final int MAX_BINGO = 15 * COLS;

    public static void main (String[] args) throws InterruptedException {

//        "BINGO".substring(6,1);

        JFrame myBingoGUI=new JFrame();
        myBingoGUI.setSize(600, 600);
        myBingoGUI.setLocation(100, 100);
        myBingoGUI.setTitle("BINGO");
        myBingoGUI.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        Container myContentPane = myBingoGUI.getContentPane();
        myContentPane.setLayout(new GridLayout(0, 4));

        BingoCard[] cards = new BingoCard[5];
        for (int i = 0; i < cards.length; i++) {
            cards[i] = new BingoCard("Card " + (i + 1), COLS, ROWS, MAX_BINGO / COLS);
            BingoCardPanel cpanel = new BingoCardPanel();
            cards[i].addListener(cpanel);
            myContentPane.add(cpanel);
        }

        myBingoGUI.setVisible(true);

        System.out.println(cards[0]);
        System.out.println();
        /*
         *   Play the game:   
         */
        boolean winner = false;
        while (!winner) {
            String error = "";
            int calledValue = -1;
            int calledColumn=-1;
            do {
                String calledNumber = JOptionPane.showInputDialog(null, error + " Enter a BINGO call:");
                error = "";
                calledColumn = -1;
                calledValue = -1;

                if (calledNumber == null) {
                    Thread.sleep(5000);
                    error = "Cancelled... ";
                    continue;
                }
                /*
                 *   The first character of the input string is converted to a column number between 0 and 4
                 */
                if (Character.toUpperCase(calledNumber.charAt(0))=='B') calledColumn=0;
                if (Character.toUpperCase(calledNumber.charAt(0))=='I') calledColumn=1;
                if (Character.toUpperCase(calledNumber.charAt(0))=='N') calledColumn=2;
                if (Character.toUpperCase(calledNumber.charAt(0))=='G') calledColumn=3;
                if (Character.toUpperCase(calledNumber.charAt(0))=='O') calledColumn=4;
                if (calledColumn < 0) {
                    error = "Called Column '" + Character.toUpperCase(calledNumber.charAt(0)) + "' must be one of BINGO";
                } else {
                    /*
                     *  The remainder of the input string is converted to an integer
                     */
                    try {
                        calledValue = Integer.parseInt(calledNumber.substring(1,calledNumber.length()));
                        if (calledValue < 1 || calledValue > MAX_BINGO) {
                            error = "Illegal value " + calledValue + " (1 <= value <= " + MAX_BINGO + ")";
                        }
                    } catch (NumberFormatException nfe) {
                        error = "Illegal number " + calledNumber.substring(1,calledNumber.length());
                    }
                }
            } while (error.length() != 0);
            /*
             *   The matrix of called numbers is update to show the number has been called.
             */
            for (BingoCard card : cards) {
                if (card.called(calledColumn, calledValue)) {
                    winner = true;
                }
            }
            if (winner) {
                for (BingoCard card : cards) {
                    card.gameOver();
                }
            }
            System.out.println(cards[0]);
            System.out.println();

        } // while

    } // main

}
\$\endgroup\$
  • \$\begingroup\$ very helpful thank you. Was able to start most of it. I am trying to add file and quit buttons at the top of the window in a menuBar. I can get the file and quit buttons on, but I am having trouble adding the listener, partly because I am not sure where in the code or class it would go. I would post the code I'm trying to use but not sure how. Thanks \$\endgroup\$ – Sgacedas Dec 8 '13 at 0:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.