package netgame.fivecarddraw; import netgame.common.*; import java.awt.*; import java.awt.event.*; import javax.swing.*; import java.io.IOException; import java.net.URL; /** * A window for one player in a two-player networked game of * Five Card Draw Poker. The window is created by the main * program, netgem.fivecarddraw.*. *
When a PokerWindow is opened, it establishes a * a connection to a PokerHub at a specified host and port. * Once two players have connected to the hub, the hub will * manage a game of poker between the two players. Each * player is given $1000 to start. One player acts a * "dealer", and the dealer clicks a "DEAL" button to start * the game. (The role of dealer alternates between players.) * After the cards are dealt, there is a first round of * betting. The non-dealer player bets first. When either * player matches the other's bet, the betting round ends and * each player has a chance to discard some cards and draw * new ones. Then there is a second round of betting. When * that round end, the players' hands are compared, and the * winner is announced. (Players also have the choice of * folding, instead of betting, which will end the game * immediately.) Note that the player's amount of money * can become negative, and nothing is done when that * happens. The game continues until one or the other * player quits. The other player is notified so that * the second player's program can also be terminated. *
For details of the communication protocol that * is used between the PokerWindows and the PokerHub, * see the comments on the PokerHub class. */ public class PokerWindow extends JFrame { /** * The constructor sets up the window and makes it visible on the screen. * It starts a thread that will open a connection to a PokerHub. * The window will become operational when the game stops, or it will be closed * and the program terminated if the connection attempt fails. * @param hubHostName the host name or IP address where the PokerHub is listening. * @param hubPort the port number where the PokerHub is listening. */ public PokerWindow(final String hubHostName, final int hubPort) { super("NetPoker"); ClassLoader cl = getClass().getClassLoader(); URL imageURL = cl.getResource("netgame/fivecarddraw/cards.png"); // Image required for drawing cards. cardImages = Toolkit.getDefaultToolkit().createImage(imageURL); display = new Display(); setContentPane(display); pack(); setResizable(false); setLocation(200,100); setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); addWindowListener( new WindowAdapter() { // A listener to end the program when the user closes the window. public void windowClosing(WindowEvent evt) { doQuit(); } }); display.addMouseListener( new MouseAdapter(){ // Respond to clicks on the display by calling the doClick() method. public void mousePressed(MouseEvent evt) { doClick(evt.getX(),evt.getY()); } }); setVisible(true); new Thread() { // A thread to open the connection to the server. public void run() { try { final PokerClient c = new PokerClient(hubHostName,hubPort); SwingUtilities.invokeLater(new Runnable() { public void run() { connection = c; if (c.getID() == 1) { // This is Player #1. Still have to wait for second player to // connect. Change the message display to reflect that fact. messageFromServer.setText("Waiting for an opponent to connect..."); } } }); } catch (final IOException e) { // Error while trying to connect to the server. Tell the // user, and end the program. Use SwingUtilties.invokeLater() // because this happens in a thread other than the GUI event thread. SwingUtilities.invokeLater(new Runnable() { public void run() { dispose(); JOptionPane.showMessageDialog(null,"Could not connect to " + hubHostName +".\nError: " + e); System.exit(0); } }); } } }.start(); } // ---------------------- Private inner classes ----------------------------------- /** * A PokerClient is a netgame client that handles communication * with the PokerHub. It is used by the PokerWindow class to * send messages to the hub. When messages are received from * the hub, it takes an appropriate action. */ private class PokerClient extends Client { /** * Connect to a PokerHub at a specified hostname and port number. */ public PokerClient(String hubHostName, int hubPort) throws IOException { super(hubHostName, hubPort); } /** * This method is called when a message from the hub is received * by this client. If the message is of type PokerGameState, * then the newState() method in the PokerWindow class is called * to handle the change in the state of the game. If the message * is of type String, it represents a message that is to be * displayed to the user; the string is displayed in the JLabel * messageFromServer. If the message is of type PokerCard[], * then it is the opponent's hand. This had is sent when the * game has ended and the player gets to see the opponent's hand. *
Note that this method is called from a separate thread, not * from the GUI event thread. In order to avoid synchronization * issues, this method uses SwingUtilties.invokeLater() to carry * out its task in the GUI event thread. */ protected void messageReceived(final Object message) { SwingUtilities.invokeLater(new Runnable() { public void run() { if (message instanceof PokerGameState) newState( (PokerGameState)message ); else if (message instanceof String) messageFromServer.setText( (String)message ); else if (message instanceof PokerCard[]) { opponentHand = (PokerCard[])message; display.repaint(); } } }); } /** * This method is called when the hub shuts down. That is a signal * that the opposing player has quit the game. The user is informed * of this, and the program is terminated. */ protected void serverShutdown(String message) { SwingUtilities.invokeLater(new Runnable() { public void run() { JOptionPane.showMessageDialog(PokerWindow.this, "Your opponent has quit.\nThe game is over."); System.exit(0); } }); } } // end nested class PokerClient /** * The display class defines a JPanel that is used as the content * pane for the PokerWindow. */ private class Display extends JPanel { final Color brown = new Color(130,70,0); final Color green = new Color(0,100,0); /** * The constructor creates labels, buttons, and a text field and adds * them to the panel. An action listener of type ButtonHandler is created * and is added to all the buttons and the text field. */ Display() { setLayout(null); // Layout will be done by hand. setPreferredSize(new Dimension(675,585)); setBackground(new Color(255,245,200)); setBorder(BorderFactory.createLineBorder(brown, 3)); opponentsMoney = makeLabel(100,10,400,30,24,green); messageFromServer = makeLabel(30,205,500,25,16,brown); message = makeLabel(30,325,500,25,16,brown); pot = makeLabel(150,255,300,45,38,green); money = makeLabel(100,535,400,30,24,green); messageFromServer.setText("WAITING FOR CONNECTION"); ButtonHandler listener = new ButtonHandler(); dealButton = makeButton("DEAL",575,60,listener); drawButton = makeButton("DRAW",575,120,listener); betButton = makeButton("BET",575,180,listener); passButton = makeButton("PASS",575,290,listener); callButton = makeButton("CALL",575,350,listener); foldButton = makeButton("FOLD",575,410,listener); quitButton = makeButton("QUIT",575,470,listener); quitButton.setEnabled(true); betInput = new JTextField(); betInput.setMargin(new Insets(2,2,2,2)); betInput.setEditable(false); add(betInput); betInput.setBounds(595,220,64,28); betInput.addActionListener(listener); } /** * Utility routine used by constructor to make a label and add it to the * panel. The label has specified bounds, font size, and color, and its * text is initially empty. */ JLabel makeLabel(int x, int y, int width, int height, int fontSize, Color color) { JLabel label = new JLabel(); add(label); label.setBounds(x,y,width,height); label.setOpaque(false); label.setForeground(color); label.setFont(new Font("Serif", Font.BOLD, fontSize)); return label; } /** * Utility routine used by the constructor to make a button and add it * to the panel. The button has a specified text and (x,y) position and * is 80-by-35 pixels. An action listener is added to the button. */ JButton makeButton(String text, int x, int y, ActionListener listener) { JButton button = new JButton(text); add(button); button.setEnabled(false); button.setBounds(x,y,80,35); setFont(new Font("SansSerif", Font.BOLD, 24)); button.addActionListener(listener); return button; } /** * The paint component just draws the cards, when appropriate. The remaining * content of the panel consists of sub-components (labels, buttons, text field). */ protected void paintComponent(Graphics g) { super.paintComponent(g); if (state == null) { // Still waiting for connections. Don't draw anything. return; } if (state.hand == null) { // This happens only while waiting for the first hand to be dealt. // Draw outlines of the card locations for this player's hand. g.setColor(brown); for (int x = 25; x < 500; x += 105) g.drawRect(x,380,80,124); } else { // Draw the cards in this player's hand. for (int i = 0; i < 5; i++) { if (discard != null && discard[i]) drawCard(g,null,25+i*105,380); else drawCard(g,state.hand[i],25+i*105,380); } } if (state.hand == null) { // This happens only while waiting for the first hand to be dealt. // Draw outlines of the card locations for the opponent's hand. g.setColor(brown); for (int x = 25; x < 500; x += 105) g.drawRect(x,70,80,124); } else if (opponentHand == null) { // The opponent's hand exists but is unknown. Draw it as face-down cards. for (int i = 0; i < 5; i++) drawCard(g,null,25+i*105,70); } else { // The opponent's hand is known. Draw the cards. for (int i = 0; i < 5; i++) drawCard(g,opponentHand[i],25+i*105,70); } } } // end nested class Display /** * A class to define the action listener that responds when the user clicks * a button or presses return while typing in the text field. Note that * once an action is taken, the buttons that were enabled are disabled, * to prevent the user from generating extra messages while the hub is * processing the user's action. */ private class ButtonHandler implements ActionListener { public void actionPerformed(ActionEvent evt) { Object src = evt.getSource(); if (src == quitButton) { // end the program doQuit(); } else if (src == dealButton) { // send "deal" as a message to the hub, which will start the next hand of the game dealButton.setEnabled(false); connection.send("deal"); } else if (src == foldButton) { // send "fold" as a message to the hub, which will end the game because this user folded foldButton.setEnabled(false); betButton.setEnabled(false); passButton.setEnabled(false); callButton.setEnabled(false); betInput.setEditable(false); betInput.setText(""); connection.send("fold"); } else if (src == passButton) { // send the integer 0 as a message, indicating that the user places no bet; // this is only possible for the first bet in a betting round foldButton.setEnabled(false); betButton.setEnabled(false); passButton.setEnabled(false); callButton.setEnabled(false); betInput.setEditable(false); betInput.setText(""); connection.send(new Integer(0)); } else if (src == callButton) { // send an integer equal to the minimum possible bet as a message to the hub; // this means "see" in the first betting round and "call" in the second, and // it will end the current betting round. foldButton.setEnabled(false); betButton.setEnabled(false); passButton.setEnabled(false); callButton.setEnabled(false); betInput.setEditable(false); betInput.setText(""); connection.send(new Integer(state.amountToSee)); } else if (src == drawButton) { // Send the list of cards that the user wants to discard as a message to // the hub. The cards are recorded in the discard array. int ct = 0; for (int i = 0; i < 5; i++) { // count the number of discarded cards. if (discard[i]) ct++; } if (ct == 0) { int resp = JOptionPane.showConfirmDialog(PokerWindow.this, "Are you sure that you want to draw ZERO cards?\n" +"If not, click 'No' and select the cards taht" +"you want to discard.", "Discarding no cards?", JOptionPane.YES_NO_OPTION); if (resp != JOptionPane.YES_OPTION) return; } int[] cardNums = new int[ct]; int j = 0; for (int i = 0; i < 5; i++) { // Put indices of discarded cards into an array to be send to the hub. if (discard[i]) cardNums[j++] = i; } discard = null; // We are finished with the discard array. drawButton.setEnabled(false); connection.send(cardNums); } else if (src == betButton || src == betInput) { // User wants to place a bet. Check that the bet is legal. If it is, // send the bet amount as a message to the hub. int amount; try { amount = Integer.parseInt(betInput.getText().trim()); if (amount <= 0) throw new NumberFormatException(); } catch (NumberFormatException e) { JOptionPane.showMessageDialog(PokerWindow.this, "The bet amount must be\na legal positive integer."); betInput.selectAll(); betInput.requestFocus(); return; } if ( (state.status == PokerGameState.RAISE_SEE_OR_FOLD_ROUND_1 || state.status == PokerGameState.RAISE_CALL_OR_FOLD_ROUND_2) && amount < state.amountToSee) { JOptionPane.showMessageDialog(PokerWindow.this, "Your bet must be at least " + state.amountToSee + "\n to match your opponent's bet."); betInput.selectAll(); betInput.requestFocus(); return; } betInput.setEditable(false); betInput.setText(""); connection.send(new Integer(amount)); } } } // end nested class ButtonHandler // ------------------- Private member variables and methods --------------------------- private PokerClient connection; // Handles communication with the PokerHub; used to send messages to the hub. private PokerGameState state; // Represents the state of the game, as seen by this player. The state is // received as a message from the hub whenever the state changes. This // variable changes only in the newState() method. private boolean[] discard; // When the player is discarding cards, this array tells which cards the // player wants to discard. discard[i] is true if player is discarding // the i-th card in the hand. private PokerCard[] opponentHand; // The opponent's hand. This variable is dull during the playing of a // hand. It becomes non-null if the opponent's hand is sent to this // player at the end of one hand of poker. private Display display; // The content pane of the window, defined by the inner class, Display. private Image cardImages; // An image holding pictures of all the cards. The Image is loaded // as a resource by the PokerWindow constructor from a resource file // "netgame/fivecarddraw/cards.png." (The program will be non-functional // if that resource file is not there.) private JButton dealButton; // User interface components that are be added to the display panel. private JButton drawButton; private JButton betButton; private JButton callButton; private JButton passButton; private JButton foldButton; private JButton quitButton; private JTextField betInput; private JLabel message; private JLabel messageFromServer; private JLabel money, opponentsMoney, pot; /** * This method is called when a new PokerGameState is received from the PokerHub. * It changes the GUI and the window's state to match the new game state. The * new state is also stored in the instance variable named state. */ private void newState(PokerGameState state) { this.state = state; // Set the enabled status of the buttons to enable actions that are appropriate in the current state. dealButton.setEnabled(state.status == PokerGameState.DEAL); drawButton.setEnabled(state.status == PokerGameState.DRAW); betButton.setEnabled(state.status == PokerGameState.BET_OR_FOLD || state.status == PokerGameState.RAISE_SEE_OR_FOLD_ROUND_1 || state.status == PokerGameState.RAISE_CALL_OR_FOLD_ROUND_2); foldButton.setEnabled(state.status == PokerGameState.BET_OR_FOLD || state.status == PokerGameState.RAISE_SEE_OR_FOLD_ROUND_1 || state.status == PokerGameState.RAISE_CALL_OR_FOLD_ROUND_2); passButton.setEnabled(state.status == PokerGameState.BET_OR_FOLD); callButton.setEnabled(state.status == PokerGameState.RAISE_SEE_OR_FOLD_ROUND_1 || state.status == PokerGameState.RAISE_CALL_OR_FOLD_ROUND_2); // Set the name of callButton to "CALL" during the second round of // betting and to "SEE" at other times. if (state.status == PokerGameState.RAISE_CALL_OR_FOLD_ROUND_2) callButton.setText("CALL"); else callButton.setText("SEE"); // When it's time for this player to make a bet, enable the betInput text field, // set its content to be the minimum possible bet plus $10, and select the // text input so that the user can simply type the bet and press return. The // betInput is not editable except when its time for the user to place a bet. // Once the user places the bet (or sees, calls, passes, or folds), the // betInput is once again made empty and uneditable. if (state.status == PokerGameState.RAISE_SEE_OR_FOLD_ROUND_1 || state.status == PokerGameState.RAISE_CALL_OR_FOLD_ROUND_2 || state.status == PokerGameState.BET_OR_FOLD) { if ( ! betInput.isEditable() ) { int suggestedBet; if ( state.status == PokerGameState.BET_OR_FOLD) suggestedBet = 10; else suggestedBet = state.amountToSee + 10; betInput.setText("" + suggestedBet); betInput.setEditable(true); betInput.selectAll(); betInput.requestFocus(); } } // Show the current amounts of the user's money, the opponent's money, and the pot. money.setText("You have $ " + state.money); opponentsMoney.setText("Your opponent has $ " + state.opponentMoney); if (state.status != PokerGameState.DEAL && state.status != PokerGameState.WAIT_FOR_DEAL) opponentHand = null; pot.setText("Pot: $ " + state.pot); // If it's time for the user to select cards to be discarded, create the // discard array that will record which cards have been selected to be discarded. if (state.status == PokerGameState.DRAW && discard == null) { discard = new boolean[5]; } // Set the JLable, message, to show instructions to the user that are appropriate for the state. switch (state.status) { case PokerGameState.DEAL: message.setText("Click the DEAL button to start the game."); break; case PokerGameState.DRAW: message.setText("Click the cards you want to discard, then click DRAW."); break; case PokerGameState.BET_OR_FOLD: message.setText("Place your BET, PASS, or FOLD."); break; case PokerGameState.RAISE_SEE_OR_FOLD_ROUND_1: message.setText("Place your BET, SEE, or FOLD."); break; case PokerGameState.RAISE_CALL_OR_FOLD_ROUND_2: message.setText("Place your BET, CALL, or FOLD."); break; case PokerGameState.WAIT_FOR_BET: message.setText("Waiting for opponent to bet."); break; case PokerGameState.WAIT_FOR_DEAL: message.setText("Waiting for opponent to deal."); break; case PokerGameState.WAIT_FOR_DRAW: message.setText("Waiting for opponent to draw."); break; } repaint(); } // end newState() /** * This method is called when the user clicks the display at the * point (x,y). Clicks are ignored except when the user is * selecting cards to discard (that is when state.status is * PokerGameState.DRAW). While discarding, if the user clicks * a card, that card is turned over. */ private void doClick(int x, int y) { if (state == null || state.status != PokerGameState.DRAW) return; for (int i = 0; i < 5; i++) { if (y > 380 && y < 503 && x > 25+i*105 && x < 104+i*105) { // Clicked on card number i. Toggle the value in // discard[i]. If the card is face up, it will be // selected for discarding and will now appear face down. // If the card is already selected for discarding, // it will be de-selected. discard[i] = !discard[i]; repaint(); break; } } } /** * This method is called when the user clicks the "QUIT" * button or closed the window. The client disconnects * from the server before terminating the program. * This will be seen by the Hub, which will inform the * other player's program (if any), so that that program * can also terminate. */ private void doQuit() { dispose(); // Close the window. if (connection != null) { connection.disconnect(); try { // time for the disconnect message to be sent. Thread.sleep(500); } catch (InterruptedException e) { } } System.exit(0); } /** * Draws a card in a 79x123 pixel rectangle with its * upper left corner at a specified point (x,y). Drawing the card * requires the resource file "netgame/fivecarddraw/cards.png". * @param g The non-null graphics context used for drawing the card. * @param card The card that is to be drawn. If the value is null, then a * face-down card is drawn. * @param x the x-coord of the upper left corner of the card * @param y the y-coord of the upper left corner of the card */ public void drawCard(Graphics g, PokerCard card, int x, int y) { int cx; // x-coord of upper left corner of the card inside cardsImage int cy; // y-coord of upper left corner of the card inside cardsImage if (card == null) { cy = 4*123; // coords for a face-down card. cx = 2*79; } else { if (card.getValue() == PokerCard.ACE) cx = 0; else cx = (card.getValue()-1)*79; switch (card.getSuit()) { case PokerCard.CLUBS: cy = 0; break; case PokerCard.DIAMONDS: cy = 123; break; case PokerCard.HEARTS: cy = 2*123; break; default: // spades cy = 3*123; break; } } g.drawImage(cardImages,x,y,x+79,y+123,cx,cy,cx+79,cy+123,this); } } // end class PokerWindow