Reading
Required: These notes!
Recommended:
Java in a Nutshell,
TO APPEAR.
Before we jump into Object Oriented Programming ...
Before we jump into Object Oriented Programming, we will round out
our introduction to the structure of Java programs by consdering
how we would write the kind of procedural programs we're used to
in the Java lanaguage / run-time environment. In theory this is
something you should not do! You should use OOP with Java.
However, seeing how the pieces fit together in a familiar kind of
program will be valuable, and put you in the right place from
which to begin our investigation of OOP.
A problem to motivate us
Suppose we wanted to write a program that allowed the user to play
the card
game
Blindman's bluff poker
against a single AI opponent.
The idea is that each player gets a card, but only gets to see
their opponent's card, not their own. A bet is placed by one
player, then the next player raises, calls or folds. In case of a
raise, the first player can either call or fold. "Call" means
match the bet, "fold" means give up (other player gets money), and
"raise" means match the bet and then add some more. Let's say
there's a 10cent ante (initial amount put in before the game
starts) and a 50cent maximum bet. In our game, the AI and the user
both start with $5. The game ends when one player is out of
money, or at any point the user wants to stop. We might not
finish the game in class, but it'll give us a motivational
problem. We should certainly get through the gameplay minus the
betting.
Representing a cards:
We're going to use the same representation of a card as an int
that we did in IC210, summarized below:
- represent suits by number according to these rules:
0 = ♣, 1 = ♦, 2 = ♥, 3 = ♠
-
represent face values according to these rules:
2 = 2, 3 = 3, ..., 10 = 10, 11 = J(jack), 12 = Q(queen),
13 = K(king), 14 = A(ace)
-
represent a card as a number by the rule:
cardvalue = 20 × suit + facevalue
and (suit = cardvalue/20, facevalue=cardvalue%20)
So, for example, the card
Q♥ → (suit=2,facevalue=12) → 20×2 + 12
= 52
In the other direction,
cardnumber 74 = 20×3 + 14 (i.e. 74/20 = 3, 74%20 = 14) →
(suit=3,facevalue=14) → A♠
Our Unix terminal program understands
"UTF-8" character encodings, which means we can
print card suits to the screen.
Here are the suits and the corresponding string for
printing them in unicode.
♣ ← "\u2663",
♦ ← "\u2666",
♥ ← "\u2665",
♠ ← "\u2660".
A little bottom-up design - Cards: classes as
collections of functions
Let's do a bit of bottom-up design. We know we're going to want
to have functions to take care of basic card manipulation
functionality: building a card, returning pretty string form of a
card, making and shuffling decks, etc. Bottom-up design says
let's go ahead and write those functions and test them before we
go on to actually write our game. The structure of Java programs
makes this really nice and easy to do! Why? Well you can use a
class as a simple way of collecting functions that do similar
things into a nice tidy package. The
main function
in that class can be used simply as a way to test those functions,
not as an interesting program in and of itself. Later on, when
we're ready to use these functions in a real program, they just
stay in their class ... no copying and pasting, no #includes, no
separating .h from .cpp files. Very convenient!
public class Card
{
public static int makeCard(int face, int suit) { return 20*suit + face; }
public static int cardToSuit(int card) { return card/20; }
public static int cardToFace(int card) { return card%20; }
public static String cardToString(int card)
{
String[] suits = {"\u2663","\u2666","\u2665","\u2660"};
String[] faces = {"","","2","3","4","5","6","7","8","9","10","J","Q","K","A"};
return faces[cardToFace(card)] + suits[cardToSuit(card)];
}
public static void main(String[] args)
{
int c1 = makeCard(10,0);
System.out.println(cardToString(c1));
}
}
Hopefully this code is mostly self-explanatory.
Note how the nice Java array-initializer syntax makes it easy
to implement
cardToString!
The
main is
there to test our functions, and if you compile and run everything
looks good:
~/$ javac Card1.java
~/$ java Card1
10♣
The next step is to add functions for making and shuffling decks.
That's pretty straightforward. Take a look at the complete
version of
Card.java.
Calling the Card functions from another file
Now our actual game program is going to want to call these
functions. But how? The answer is that outside of the class Card
we name these functions by their full names: Card.
name.
For example, we'd call makeCard like this:
Card.makeCard(3,1)
It's a little wordy, of course, but it allows to easily reuse
these functions in any Java program we like! So, let's start off
our game by creating, shuffling and printing a deck. This is
going to be a new class called
Game, defined of
course in a new file
Game.java.
| Game.java |
Card.java |
import java.util.*;
public class Game
{
public static void main(String[] args)
{
Random rand = new Random(System.currentTimeMillis());
int[] D = Card.makeDeck();
Card.shuffle(D,rand);
for(int top = 0; top < D.length; top++)
System.out.println(Card.cardToString(D[top]));
}
} |
import java.util.*;
public class Card
{
public static int makeCard(int face, int suit) { return 20*suit + face; }
public static int cardToSuit(int card) { return card/20; }
public static int cardToFace(int card) { return card % 20; }
public static String cardToString(int card) { ... }
public static int[] makeDeck() { ... }
public static void shuffle(int[] D, Random rand) { ... }
public static void main(String[] args)
{
Random rand = new Random(System.currentTimeMillis());
int[] D = makeDeck();
shuffle(D,rand);
for(int i = 0; i < 52; i++)
System.out.println(cardToString(D[i]));
}
} |
So, at this point you have a program that consists of two
files
Game.java and
Car.java. You
compile them both, either individually, or together like this:
javac Game.java Card.java
The order in which you compile them is irrelevent. To run the
program, you only specify
Game, like this:
java Game
What about Card? Well, here's where Java's organization works
nicely. When the JVM exectues the call to
function
Card.makeDeck(), it knows it needs to load
the classfile
Card.class, because that's where the
class
Card is defined. As long as both classfiles
are in the current directory, Java finds them and loads them as
needed.
Players: classes as collections of data (like structs)
We're going to want to represent two players (and maybe more
later). For each player we need to represent the amount of money
it has and its current card. Hopefully you'd find it natural to
use a struct for this in C/C++. In Java, we have classes not
structs. We can use classes just like structs, though,
i.e. simply as a collection of data. The class for a player will
look like this:
public class Player
{
int card;
double money;
}
Where we put this definition is a bit up to us. We could put it
inside the definition of the class
Game, where it
would actually be a part of Game. In fact, its full name would be
Game.Player.
[Note: if we do this, the definition will have to be
static
class Player { ... }. Why this would be needed in this
case will be discussed in a few lessons.]
Or we could put it in its own
file,
Player.java, in which case it would be an
independent type, not part of or dependent on anything. We'll
chose the latter, as it's the more common and straightforward way
of doing things. Now we can go ahead and create an object of type
player to represent the user or the AI. Remember that
Player pu; only declares the reference pu, it doesn't
actually create a player object. That's done with a call
to
new, like this:
Player pu = new Player();
This is called
instantiating and object of type
Player.
The object itself, i.e. the thing pu refers-to/points-to is said to
be an
instance of class
Player. The class
itself is a type, not an object. It is a blueprint for creating
Player objects. Only the call to
new Player()
creates new Player objects. We refer to data members of a Player
object with a . like this:
pu.money = 5.00;. Here's
a program:
| Game.java |
Player.java |
Card.java |
import java.util.*;
public class Game
{
public static void main(String[] args)
{
Random rand = new Random(System.currentTimeMillis());
int[] D = Card.makeDeck();
Card.shuffle(D,rand);
int top = 0;
Player pu = new Player();
pu.money = 5.00;
pu.card = D[top++];
System.out.println("Player has $" + pu.money + " and card "
+ Card.cardToString(pu.card));
}
} |
public class Player
{
int card;
double money;
} |
import java.util.*;
public class Card
{
public static int makeCard(int face, int suit) { return 20*suit + face; }
public static int cardToSuit(int card) { return card/20; }
public static int cardToFace(int card) { return card % 20; }
public static String cardToString(int card) { ... }
public static int[] makeDeck() { ... }
public static void shuffle(int[] D, Random rand) { ... }
public static void main(String[] args)
{
Random rand = new Random(System.currentTimeMillis());
int[] D = makeDeck();
shuffle(D,rand);
for(int i = 0; i < 52; i++)
System.out.println(cardToString(D[i]));
}
} |
Once again, we need to compile each .java file, individually or
together makes no difference.
In fact,
Card.java hasn't changed, so it doesn't need
to be recompiled.
Then we run the program by calling
the JVM with
Game. The other .class files will be
loaded by the JVM as they are needed.
~/$ javac Game.java Player.java
~/$ java Game
Player has $5.0 and card K♥
From here on out it's just programming
From here on out it's just programming. Not much Java-specific.
Here's an implementation of the game up to but not including
dealing with betting. Note: In one card poker, the high face
value wins, ties are broken by comparing suit, for which
spades beats hearts which beats diamonds which beats clubs.
Check out
Game.java
which implements all but the betting.
Problems
TO APPEAR