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.
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()
|