Designing Applet: Mandelbrot, BlackJack, and Others

In the previous lecture we have learned how to draw to an off-screen area and use the paint method to swap the off-screen image to the visible area. Our motivation for this technique was to elminate the flickering in our ticker tape applet. This technique is often called 'double-buffering', because you are using two buffers, or graphics contexts, for drawing: one off-screen and one on-screen buffer. This is a standard technique, and you should make sure to understand how it works. 

In today's lecture we will design several other 'bigger' applets: the Mandelbrot fractal drawing applet, a 'BlackJack game' applet, and a 'Paint' or 'Drawing' applet. We will not discuss either applet in complete detail; instead we will cover some of their basic features and design issues only. 



Mandelbrot Applet 

A fractal is a geometric, or mathematical, object that looks similar to the original object when you magnify an arbitrary piece of it. An 'real world' example might be the coast line of a country like England: viewed on a map (i.e. from the 'distance') it looks 'ragged'; zooming in by, say, viewing it from a plane, it still looks just as ragged; zooming in further by, say, walking along the beach, it again looks ragged, and so on. An example of a mathematical object displaying similar properties is the so-called "Mandelbrot set", named after the mathematician Benedroit Mandelbrot. We want to create an applet that can draw this object, set various parameters of the object, and zoom in and out into any parts of the object (to see characteristic the self-similarity of this fractal). 

First, there is a mathematical formula that is used to generate the picture. The formula is, actually, very easy. It is provided here only for completeness, but you do not need to understand the details at all. 

Fix a point (a, b) in the Cartesian coordinate system, and consider the basic formula: 

x*x - y*y + a = x_new
2*x*y + b = y_new
Pick the point (0,0) - the origin - of the cartesian coordinate system, substitute it in the above formula, and call the resulting point (x1, y1).
Take the point (x1, y1), substitute it in the above formula, and call the resulting point (x2, y2)

Keep on doing this. One of two things will happen: 

the resulting points will stay within a certain radius of the origian (say 5.0)
the resulting points will not stay within that radius (and move further and further away)

Depending on this outcome, color the original point (a,b) according to the following scheme: 

if the points stay inside the radius 5, color the original point (a,b) in black
if the points do not stay within that radius, color the original point (a, b) in, say, blue

To make the picture somewhat more interesting, actually choose the following color scheme: 

if the points stay inside the radius 5, color the original point (a,b) in black
if the points do not stay within that radius, color the original point (a, b) in a color that corresponds to how fast the points moved outside that radius (i.e. if (x1, y1) are already outside that radius, choose color A, if (x2, y2) are outside the radius choose color B, etc).

Repeat this procedure with every point (a, b) in the plane, so that every point will be colored either in black or in blue (or in another color). 

Again, it is not necessary to understand the details. Simply consider the following method which will do the math as described above: 

public Color computeColor(double a, double b, int maxIter)
{
   int iter = 0;
   double x = 0, y = 0, tmp = 0;
   boolean escaped = false;

   while ( (iter < maxIter) && (!escaped) )
   {
      tmp = x*x - y*y + a;
      y = 2*x*y + b;
      x = tmp;
      if ( (Math.abs(x) + Math.abs(y)) >= 5.0)
         escaped = true;
      iter++;
   }
   if (escaped)
      return new Color((20*iter) % 255, 0, 0);
   else
      return Color.blue;
}

Now that the math part is clear, here is how we should do the rest of the program, bascially: 

for a = -2 to 2
   for b = -2 to 2
      computeColor for current (a,b) value
      color the point (a,b) in that color

The first problem we have is that we need to convert screen coordinates (where x goes from 0 to size().width and y from 0 to size().height) to 'world' coordinates of our actual coordinate system (where a and b go from -2 to 2). We will do this with the help of a method called toWorld, outlined below. After this conversion, the applet is now easy. Here's our first draft: 

import java.applet.Applet;
import java.awt.*;

public class MandelTest extends Applet
{
   int       pixel = 2, iter  = 40;
   double    xMin = -2.0, xMax = 2.0, 
             yMin = -2.0, yMax = 2.0;

   public void init()
   {      
      setLayout(new BorderLayout());
   }

   private final double toWorld(int i, int iMax, double min, double max)
   {
      return (max - min) / iMax * ((double)i - iMax) + max;
   }

   public Color computeColor(double a, double b, int maxIter)
   {
      int iter = 0;
      double x = 0, y = 0, tmp = 0;
      boolean escaped = false;

      while ((iter < maxIter) && (!escaped))
      {
         tmp = x*x - y*y + a;
         y = 2*x*y + b;
         x = tmp;
         if ( (Math.abs(x) + Math.abs(y)) >= 5.0)
            escaped = true;
         iter++;
      }
      if (escaped)
         return new Color((20*iter) % 255, 0, 0);
      else
         return Color.blue;
   }

   public void paint(Graphics g)
   {
      for (int ix = 0; ix < size().width; ix += pixel)
      {
         for (int iy = 0; iy < size().height; iy +=pixel)
         {
            double a = toWorld(ix, size().width,xMin,xMax);
            double b = toWorld(iy, size().height, yMax, yMin);
            g.setColor(computeColor(a,b,iter));
            g.fillRect(ix,iy,pixel,pixel);
         }
      }
   }
}

Note that since Java does not provide a method to color a single pixel, we have chosen instead to draw a small rectangle whose size is given by the variable 'pixel'. That makes our applet actually more flexible, as we will see soon. 

Click here to see the current applet in action


When you actually take a look at the applet in the above link you will notice one main problem with it: we have put all the drawing code inside the 'paint' method. That code consists of a double loop, and the 'computeColor' method itself is a loop. Therefore, we have a triple loop inside our paint method, and the number of times the inner most code will execute is, for a pixel size of 2, approximately: 

(size().width/pixel) * (size().height/pixel) *i ter = 
                               (350 * 300) / 4 * 40 =  1,050,000

While that is what happens to be necessary to generate the picture, putting these computations inside the paint method means that every time the window needs updating, the entire computation will start all over again. That is, of course, a huge waste. Once the picture is computed, there should really be no need to recompute it. Moreover, while the computation is happening, our computer is busy. We can not even flip pages to 'interrupt' the computation. We must wait, whether we like it or not, until the entire picutre is drawn. For a 'good' fractal program, however, we need to address and fix these problems: 

to eliminate the 'constant recomputing' problem: move the actual drawing code out of the paint method and use instead an off-screen area for the drawing. This will be slightly different from our previous double-buffering technique: we will continue to draw to the off-screen area until the computation is finished. After that, we simply want to use the off-screen area as a 'storage' area to hold the entire picture and swap into the visible area whenever necessary but without recomputation.
to eliminate the 'wait while computing' problem: handle the actual computation in a seperate thread.


Moreover, we should also consider the following improvements: 

The computation should happen entirely in the background. Even if a user looks at another page, the computation should continue until the picture is finished. When the user returns to the page, the finished picture will display without recomputing. To recall, which methods of an applet are called upon visiting and exiting a web page ?
We should provide a method to change the current parameters to allow zooming in and out: changing the a and b range will zoom in and out, while changing the value of pixel will improve our picture quality (and increase the computational effort, unfortunately). We will use an extension of the Dialog class for this.
We don't want to have to wait for the current picture to complete before changing any parameters. Therefore, all changes to the parameters, whether manually or later using the mouse, should result in an immediate recomputation of the picture. 


I have moved the new and improved applet to a seperate page. Make sure you look at the page to start the computation, then switch back to this page, and then return to the page in a few seconds. The computation should continue even when you are not looking at the page. 

Click here to see the new and improved Mandelbrot applet


As our final improvement, we should implement the following features: 

We should provide a mechanism to zoom in by draging the mouse in addition to the possibility of changing the coordinates manually
We should provide a mechanism to display, in real-world coordinates, the current coordinates of the mouse pointer


While we don't have enough time to discuss in detail how the 'rubber-band mouse dragging' works, you might want to carefully look at the source code. This 'dragging' mechanism can work, with suitable modifications, in any other drawing program. 

Click here to see the final version of the Mandelbrot applet


BlackJack Game 

Next, we want to discuss an applet that lets us play a variant of BlackJack. First, recall the rules of the game (these may not be the official Las Vegas rules, but it will suffice for our purposes).


BlackJack is played on a "BlackJack Table". A dealer sits on one side of the table, and there are (usually) five seats on the other side. Up to five players can join a game, but only when a round is finished. Each player can bet a certain amount of money. The dealer then starts by dealing each player a card. The player looks at the card, then either requests another card, or passes, or goes bust. The goal is to get as close to 21 points as possible. If a player has more than 21 points, he or she is bust and must say so. The player receives cards until he or she passes; then it's the next player's turn. Once every player either passes or is bust, the dealer goes through the same ritual. When the dealer is done, the scores are compared. Everybody with scores of 21, or scores higher than or equal to the dealer's wins and gets double the money. The dealer gets the money from the players that lost or went bust. To count, each card counts its face value, except for picture cards (which count 10) and an ace (which counts either 11 or 1). 

To play, click on the "Play BlackJack" button and when the game is loaded (which takes a little), click on the "Join" button to join a game.

JAVA APPLET


When looking at this descriptions of the game from the standpoint of object-oriented programming, we need to identify the objects that we need to design to make up our complete applet. The following objects seem to jump at us: 

BlackJack:
The basic applet to set everything up and initialize all needed objects
Table: 
the top-level class that will govern the complete game
Seat: 
a spot at the table through which a player can communicate with the table
Player: 
the class that acts as a bridge between the user and the applet
Dealer: 
a special kind of player that plays by slightly different rules that a regular player
Deck of Cards: 
contains the cards and deals them to whoever needs one
Card: 
a playing card


Next, we need to identify the various public methods and perhaps some fields that each of these classes should posses so that we can better understand if our current objects are sufficient for our purpose. 

BlackJack:
Fields:
   private Table   table  = null;
   private Button  Start  = null;
Initiator:
   public void init()
Methods
   public boolean action(Event e, Object arg)
Table: 
Fields:
   public  static int    WAITING = 1, PLAYING = 2;
   private static int    numSeats         = 5;    
   private Button HelpButton, AddPlayer, DelPlayer, NewBet, TakeCard, Done;
   private DeckofCards deck;
   private Seat[]      seats = new Seat[5];
   private Seat        bank;
   private int         numPlayers = 0,activeSeat = 0, gameStatus = WAITING;
   private Player           aPlayer;
   private DialogBet        betDialog;
   private DialogPlayerName nameDialog;
Constructor:
   public Table()
Methods:
   private int findPlayerOfStatus(int status, int startAt)
   private void adjustPlayerStatus()
   private void handleAddPlayer()
   private void handleDelPlayer()
   private void handleQuit()
   private void handleAbout()
   private void handleNewBet()
   private void handleTakeCard()
   private void handleDone()
   public boolean handleEvent(Event e)
   public boolean action(Event e, Object arg)
Private Methods
   private void collectCards()
   private void payUp()
   private void handleBank(

Seat: 

Fields:
   private Player player;
   private SeatCanvas  canvas  = new SeatCanvas();
   private TextArea    display = new TextArea(3,20);
   private Label       status  = new Label();
   private boolean selected = false;
Constructor:
   public Seat(int i)
Methods
   public void setSelected(boolean _selected)
   public int getPlayerStatus()
   public void setPlayerStatus(int _ID)
   public void delPlayer()
   public void addPlayer(Player _player)
   public Player getPlayer()
   public void println(String msg)
   public void display()
   public void paint(Graphics g) 

Player: 

Fields:
   public final int NONE    = -1;
   public final int ACTIVE  = 1;
   public final int WAITING = 2;
   public final int REMOVE  = 3;
   private int   status = NONE;       // status of player at seat
   private Vector hand;               // holds the current cards
   private String name;               // name of player
   private int    value  = 0;         // total value of cards
   private int    purse  = 0;         // total money available
   private int    bet    = 2;         // currently betting this amount
   private int    seatID = -1;        // seat ID at Table
   public boolean hadEnough = false;  // wants more cards
   public boolean isBust    = false;  // too many cards
Constructor
   public Player(String _name, int _purse, int _bet)
Methods  
   public void setStatus(int _status)
   public int getStatus()
   public int getBet()
   public void setBet(int _bet)
   public String getName()
   public void setName(String _name)
   public int getPurse()
   public void setPurse(int _purse)
   public int getValue()
   private void think()
   public int getNumCards()
   public String getCardAt(int i)
   public void takeCard(Card aCard)
   public void dropCards()
Dealer: 
Not sure yet, but should extend Player. The "think" method should differ, probably.
Deck of Cards: 
Fields:
   private int  numCards = 0;
   private int  numInDeck = 0;
   private Card theCards[];
   String[] cardValues = {"Ace", "King", "Queen", "Jack", 
                           "10", "9", "8", "7", "6", "5", "4

", "3", "2"};
   String[] cardSuits = {"diamonds","hearts","spades", "clubs"};
   String[] cardColors = {"red", "black"};
Constructor
   public DeckofCards(int _numCards)
Methods
   public Card deal()
   public int getNumCardsInDeck()
   public void display()
   public void shuffle()
Private Methods
   private int randomInt(int max)
Card: 
Fields:
   private String color;
   private String name;
   private String suit;
Constructor:
   public Card(String _name, String _suit)
Methods:
   public void setName(String _name) 
   public String toString()
   public void display()
   public int valueOf()