package netgame.fivecarddraw; import javafx.application.Platform; import javafx.stage.Stage; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.ButtonType; import javafx.scene.control.TextField; import javafx.scene.control.Alert; import javafx.scene.layout.BorderPane; import javafx.scene.layout.VBox; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.paint.Color; import javafx.scene.image.Image; import javafx.scene.text.Font; import javafx.scene.text.FontWeight; import javafx.geometry.Insets; import javafx.event.ActionEvent; import javafx.event.EventHandler; import netgame.common.*; import java.io.IOException; import java.util.Optional; /** * A window for one player in a two-player networked game of * Five Card Draw Poker. The window is created by the main * program, netgame.fivecarddraw.Main. *

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 ends, 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 Stage { 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 the player is discarding // the i-th card in the hand. private PokerCard[] opponentHand; // The opponent's hand. This variable is null 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 Canvas canvas; // The canvas where the game is displayed, 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 Button dealButton; // User interface components shown along the right side of the window. private Button drawButton; private Button betButton; private Button callButton; private Button passButton; private Button foldButton; private Button quitButton; private TextField betInput; private String message = ""; // text that is displayed on the canvas private String messageFromServer = ""; private String money = "", opponentsMoney = "", pot = ""; /** * 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 starts, 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) { cardImages = new Image("netgame/fivecarddraw/cards.png"); messageFromServer = "WAITING FOR CONNECTION"; canvas = new Canvas(550,575); drawBoard(); canvas.setOnMousePressed( evt -> doClick(evt.getX(),evt.getY()) ); betInput = new TextField(); betInput.setEditable(false); betInput.setPrefColumnCount(5); VBox.setMargin(betInput,new Insets(0,10,0,15)); VBox controls = new VBox(); EventHandler listener = this::doAction; dealButton = makeButton("DEAL",listener,controls); drawButton = makeButton("DRAW",listener,controls); betButton = makeButton("BET:",listener,controls); controls.getChildren().add(betInput); passButton = makeButton("PASS",listener,controls); callButton = makeButton("CALL",listener,controls); foldButton = makeButton("FOLD",listener,controls); quitButton = makeButton("QUIT",listener,controls); quitButton.setDisable(false); BorderPane root = new BorderPane(canvas); root.setRight(controls); setScene(new Scene(root)); setOnHiding( e -> doQuit() ); setTitle("NetPoker"); setResizable(false); setX(200); setY(100); show(); new Thread( () -> connect(hubHostName,hubPort) ).start(); } // end start() /** * Utility routine used by the constructor to make a button, add it * to the VBox, and add a listener for ActionEvents. All buttons will * have the same size, 95x40. */ private Button makeButton(String text, EventHandler listener, VBox box) { Button button = new Button(text); button.setDisable(true); button.setPrefSize(95,40); button.setFont(Font.font(null, FontWeight.BOLD, 15)); button.setOnAction(listener); box.getChildren().add(button); VBox.setMargin(button, new Insets(30,10,0,10)); return button; } /** * When the window is created, this method is called in a separate * thread to make the connection to the server. If an error * occurs, the program is terminated. */ private void connect(String hostName, int serverPortNumber) { PokerClient c; try { c = new PokerClient(hostName, serverPortNumber); int id = c.getID(); Platform.runLater( () -> { connection = c; if (id == 1) { // This is Player #1. Still have to wait for second player to // connect. Change the message display to reflect that fact. messageFromServer = "Waiting for an opponent to connect..."; drawBoard(); } }); } catch (Exception e) { Platform.runLater( () -> { Alert alert = new Alert(Alert.AlertType.INFORMATION, "Sorry, could not connect to\n" + hostName + " on port " + serverPortNumber + "\nShutting down."); alert.showAndWait(); System.exit(0); }); } } // ---------------------- 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 as * messageFromServer. If the message is of type PokerCard[], * then it is the opponent's hand. That hand 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 Platform.runLater() to carry * out its task in the GUI event thread. */ protected void messageReceived(final Object message) { Platform.runLater( () -> { if (message instanceof PokerGameState) newState( (PokerGameState)message ); else if (message instanceof String) { messageFromServer = (String)message ; drawBoard(); } else if (message instanceof PokerCard[]) { opponentHand = (PokerCard[])message; drawBoard(); } }); } /** * 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) { Platform.runLater( () -> { showMessage("Your opponent has quit.\nThe game is over."); System.exit(0); }); } } // end nested class PokerClient private static Font font16 = Font.font(16); //fonts for use in drawBoard private static Font font24 = Font.font(24); private static Font font38 = Font.font(38); private void drawBoard() { GraphicsContext g = canvas.getGraphicsContext2D(); g.setFill(Color.BEIGE); g.fillRect(0,0,canvas.getWidth(), canvas.getHeight()); g.setStroke(Color.DARKRED); g.setLineWidth(8); g.strokeRect(0,0,canvas.getWidth(),canvas.getHeight()); g.setFill(Color.GREEN); g.setFont(font24); g.fillText(opponentsMoney,100,40); g.fillText(money,100,550); g.setFont(font38); g.fillText(pot,150,300); g.setFill(Color.DARKRED); g.setFont(font16); g.fillText(message,30,355); g.fillText(messageFromServer,30,230); 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.setStroke(Color.DARKRED); g.setLineWidth(2); for (int x = 25; x < 500; x += 105) g.strokeRect(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.setStroke(Color.DARKRED); g.setLineWidth(2); for (int x = 25; x < 500; x += 105) g.strokeRect(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); } } private void showMessage(String message) { Alert alert = new Alert(Alert.AlertType.INFORMATION, message); alert.showAndWait(); } /** * A class to define the action event handler that responds when the user clicks * a button. The button that was clicked is given by evt.getSource(). 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. */ public void doAction(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.setDisable(true); 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.setDisable(true); betButton.setDisable(true); passButton.setDisable(true); callButton.setDisable(true); 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.setDisable(true); betButton.setDisable(true); passButton.setDisable(true); callButton.setDisable(true); betInput.setEditable(false); betInput.setText(""); connection.send(Integer.valueOf(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.setDisable(true); betButton.setDisable(true); passButton.setDisable(true); callButton.setDisable(true); betInput.setEditable(false); betInput.setText(""); connection.send(Integer.valueOf(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) { Alert alert = new Alert(Alert.AlertType.CONFIRMATION, "Are you sure you want to draw NO cards?\n" +"If not, click 'No', and select\n" + "the cards that you want to discard.", ButtonType.NO, ButtonType.YES); Optional resp = alert.showAndWait(); if (! resp.isPresent() || resp.get() == ButtonType.NO) 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 sent to the hub. if (discard[i]) cardNums[j++] = i; } discard = null; // We are finished with the discard array. drawButton.setDisable(true); connection.send(cardNums); } else if (src == betButton) { // 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) { showMessage("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) { showMessage("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(Integer.valueOf(amount)); } } /** * 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.setDisable(state.status != PokerGameState.DEAL); drawButton.setDisable(state.status != PokerGameState.DRAW); betButton.setDisable(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.setDisable(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.setDisable(state.status != PokerGameState.BET_OR_FOLD); callButton.setDisable(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. The // betInput is not editable except when it's 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 = "You have $ " + state.money; opponentsMoney = "Your opponent has $ " + state.opponentMoney; if (state.status != PokerGameState.DEAL && state.status != PokerGameState.WAIT_FOR_DEAL) opponentHand = null; pot = "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 message to show instructions to the user that are appropriate for the state. switch (state.status) { case PokerGameState.DEAL: message = "Click the DEAL button to start the game."; break; case PokerGameState.DRAW: message = "Click the cards you want to discard, then click DRAW."; break; case PokerGameState.BET_OR_FOLD: message = "Place your BET, PASS, or FOLD."; break; case PokerGameState.RAISE_SEE_OR_FOLD_ROUND_1: message = "Place your BET, SEE, or FOLD."; break; case PokerGameState.RAISE_CALL_OR_FOLD_ROUND_2: message = "Place your BET, CALL, or FOLD."; break; case PokerGameState.WAIT_FOR_BET: message = "Waiting for opponent to bet."; break; case PokerGameState.WAIT_FOR_DEAL: message = "Waiting for opponent to deal."; break; case PokerGameState.WAIT_FOR_DRAW: message = "Waiting for opponent to draw."; break; } drawBoard(); } // 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(double x, double 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]; drawBoard(); 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() { hide(); // 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(GraphicsContext 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, cx,cy,79,123, x,y,79,123 ); } } // end class PokerWindow