gjset.server.ServerGameController.java Source code

Java tutorial

Introduction

Here is the source code for gjset.server.ServerGameController.java

Source

package gjset.server;

/* 
 *  LEGAL STUFF
 * 
 *  This file is part of Combo Cards.
 *  
 *  Combo Cards is Copyright 2008-2010 Artless Entertainment
 *  
 *  Set is a registered trademark of Set Enterprises. 
 *  
 *  This project is in no way affiliated with Set Enterprises, 
 *  but the authors of Combo Cards are very grateful for
 *  them creating such an excellent card game.
 *  
 *  Combo Cards is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *   
 *  Combo Cards is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details
 *   
 *  You should have received a copy of the GNU General Public License
 *  along with Combo Cards.  If not, see <http://www.gnu.org/licenses/>.
 */

import gjset.GameConstants;
import gjset.data.Card;
import gjset.data.Player;

import java.util.Iterator;
import java.util.List;
import java.util.Observable;
import java.util.Observer;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

import org.dom4j.DocumentFactory;
import org.dom4j.Element;

/**
 * This class handles all operations for the server.  Whenever something changes, all players are updated.
 */
public class ServerGameController implements ServerMessageHandler, Observer {
    private GameModel model;
    private DocumentFactory documentFactory;
    private GameServer server;
    private Queue<ServerMessage> messageQueue;
    private Thread messageProcessingThread;

    /**
     * Create a ServerGameController
     * 
     * This constructor instantiates and initializes all of the game model data.
     * @param gamePort 
     *
     */
    public ServerGameController(GameServer server) {
        this.server = server;
        server.addMessageHandler(this);

        model = new GameModel();
        model.addObserver(this);

        documentFactory = DocumentFactory.getInstance();

        createMessageQueue();
    }

    /**
     * Return the game model used by this controller. (Currently just used for test purposes.)
     *
     * @return
     */
    public GameModel getModel() {
        return model;
    }

    /**
     * Start a new game and update the players with the relevant information.
     *
     */
    public void startNewGame() {
        model.startNewGame();

        updateAllPlayers();
    }

    /**
     * Handle a message incoming from the server.
     */
    public void handleMessage(PlayerClientHandler client, Element messageElement) {
        ServerMessage message = new ServerMessage(client, messageElement);
        messageQueue.offer(message);
    }

    /**
     * Send a game update to the new client.
     *
     * @param client
     * @see gjset.server.ServerMessageHandler#handleNewClient(gjset.server.PlayerClientHandler)
     */
    public Element bindClient(PlayerClientHandler client, String username) {
        // Verify that we can do this.
        Player player = model.getExistingPlayer(username);

        if (player == null) {
            if (model.getGameState() == GameConstants.GAME_STATE_NOT_STARTED
                    && model.getPlayers().size() < GameConstants.MAX_PLAYERS) {
                // Create a new player in the model.
                player = model.addNewPlayer(username);
                client.setPlayer(player);

                // Return the player information.
                Element commandResponse = getCommandResponse(true, null);

                Element newPlayerElement = documentFactory.createElement("newplayer");
                newPlayerElement.add(player.getXMLRepresentation());

                commandResponse.add(newPlayerElement);

                return commandResponse;
            } else {
                return getCommandResponse(false, "Unable to create a new player at this time.");
            }

        } else {
            client.setPlayer(player);

            // Return the player information.
            Element commandResponse = getCommandResponse(true, null);

            Element newPlayerElement = documentFactory.createElement("newplayer");
            newPlayerElement.add(player.getXMLRepresentation());

            commandResponse.add(newPlayerElement);

            return commandResponse;
        }
    }

    /**
     * After a change event from the model, update all of the players.
     *
     * @param arg0
     * @param arg1
     * @see java.util.Observer#update(java.util.Observable, java.lang.Object)
     */
    public void update(Observable modelObservable, Object updateType) {
        Boolean updateWasSolicited = (Boolean) updateType;

        // Only update for unsolicited updates.  Solicited updates can fend for themselves.
        if (updateType != null && !updateWasSolicited.booleanValue()) {
            updateAllPlayers();
        }
    }

    /**
     * Destroy this object.
     *
     */
    public void destroy() {
        model = null;
        server = null;
        documentFactory = null;
        messageProcessingThread.interrupt();
    }

    /**
     * Create the message queue system.
     *
     */
    private void createMessageQueue() {
        messageQueue = new ConcurrentLinkedQueue<ServerMessage>();

        messageProcessingThread = new Thread(new Runnable() {
            public void run() {
                // Look until the server vanishes.
                while (server != null) {
                    // Check to see if we can get any messages out of the queue.
                    ServerMessage message = messageQueue.poll();

                    if (message != null) {
                        processMessage(message);
                    }

                    // Yield to the other threads in the system.
                    Thread.yield();
                }
            }
        });

        messageProcessingThread.start();
    }

    /**
     * 
     * process the indicated message from the server.
     *
     * @param message
     */
    private void processMessage(ServerMessage message) {
        // Start by getting the player id.
        int playerId = message.client.getPlayerId();

        // Now see if this was a command.
        Element responseElement = null;
        Element commandElement = message.rootElement.element("command");
        if (commandElement != null) {
            String commandType = commandElement.attributeValue("type", "");
            if (commandType.equals("drawcards")) {
                responseElement = requestDrawCards(playerId);
            } else if (commandType.equals("callset")) {
                responseElement = callSet(playerId);
            } else if (commandType.equals("selectcard")) {
                Element cardElement = commandElement.element("card");
                Card card = new Card(cardElement);
                responseElement = toggleSelection(playerId, card);
            } else if (commandType.equals("joinasplayer")) {
                // This is an initialization message.
                String username = commandElement.element("username").getText();

                responseElement = bindClient(message.client, username);
            } else if (commandType.equals("startgame")) {
                // For now, we'll just automatically start the game.
                startNewGame();
            } else if (commandType.equals("dropout")) {
                handleDropOut(message.client);
            }
        }

        // See if we need to send a response to the player.
        if (responseElement != null) {
            // Add the original command so that the client can cross reference it, if necessary.
            responseElement.add(commandElement.createCopy());

            // Send the result back to the player that sent it in.
            message.client.sendMessage(responseElement);

            // Check to see if we need to update all players with new data.
            String result = responseElement.attributeValue("result", "false");
            if (result.equals("success")) {
                updateAllPlayers();
            }
        }
    }

    /**
     * Request more cards be drawn from the deck.
     *
     * @return An XML tag containing the results of the command
     */
    private Element requestDrawCards(int playerId) {
        boolean success = false;
        String failureReason = null;

        int gameState = model.getGameState();

        // Only draw cards in single player mode or when the game is idle.
        if (gameState == GameConstants.GAME_STATE_IDLE
                || (model.getPlayers().size() == 1 && gameState == GameConstants.GAME_STATE_SET_CALLED)) {
            model.getCardTable().unSelectCards();

            Deck deck = model.getDeck();
            CardTable cardTable = model.getCardTable();

            if (deck.getRemainingCards() < 3) {
                // If we're out of cards, do nothing.
                failureReason = "No cards remaining";
            } else if (cardTable.getNumCards() < CardTable.CARD_LIMIT) {
                // We *CAN* draw the cards! YAY!  So 

                // Register ourself as one of those wishing to draw cards.
                model.setDesireToDrawCards(playerId, true);

                if (model.allPlayersWantToDraw()) {
                    model.drawCards();
                }

                success = true;
            } else {
                // There are plenty of cards on the table.  No need to draw more.
                failureReason = "Sufficient cards already on table";
            }
        } else {
            // The game is not in the correct state to draw cards.
            failureReason = "Game is not idle.";
        }

        return getCommandResponse(success, failureReason);
    }

    /**
     * Request that this player calls set.
     *
     * @return
     */
    private Element callSet(int playerId) {
        System.out.println("Player " + playerId + " is calling set.");
        // Verify that we can actually call set.
        if (model.getGameState() == GameConstants.GAME_STATE_IDLE) {
            model.callSet(playerId);
            return getCommandResponse(true, null);
        } else {
            return getCommandResponse(false, "Set has already been called");
        }
    }

    /**
     * Request card selection for the indicated player.
     *
     * @param playerId
     * @param card
     * @return
     */
    private Element toggleSelection(int playerId, Card card) {
        // First toggle selection on the card.
        int gameState = model.getGameState();
        Element commandResponse = null;

        if (gameState == GameConstants.GAME_STATE_IDLE) {
            // Call set.
            model.callSet(playerId);

            // Select the card.
            model.toggleCardSelection(card);

            commandResponse = getCommandResponse(true, null);
        } else if (gameState == GameConstants.GAME_STATE_SET_CALLED && playerId == model.getSetCaller().getId()) {
            // Allow the next card to be selected.
            model.toggleCardSelection(card);

            commandResponse = getCommandResponse(true, null);
        } else {
            // Abort early if we can't select a card.
            commandResponse = getCommandResponse(false, "You can't select cards");
        }

        // Now see if we've got 3 cards selected.
        List<Card> selectedCards = model.getCardTable().getSelectedCards();
        if (selectedCards.size() == 3) {
            // We do have 3 cards!  Check for a set!
            Element messageAddendum = documentFactory.createElement("setresult");

            // Check if this is a set.
            boolean setSelected = model.resolveSet(true);
            if (setSelected) {
                // Append a message about the result of the set.
                messageAddendum.addAttribute("isset", "true");
            } else {
                // Append a message about the result of the set.
                messageAddendum.addAttribute("isset", "false");
            }

            commandResponse.add(messageAddendum);
        }

        return commandResponse;
    }

    /**
     * Return your basic success response.
     *
     * @param success  True if successful, false if not.
     * @param reason Adds a reason that can be added to the command response.
     * 
     * @return
     */
    private Element getCommandResponse(boolean success, String reason) {
        // Create our response
        String resultText = "failed";
        if (success) {
            resultText = "success";
        }

        Element responseElement = documentFactory.createElement("commandresponse");
        responseElement.addAttribute("result", resultText);

        if (reason != null) {
            Element reasonElement = documentFactory.createElement("reason");
            reasonElement.setText(reason);
            responseElement.add(reasonElement);
        }

        return responseElement;
    }

    /**
     * send game updates to all of the players.
     *
     */
    private void updateAllPlayers() {
        // Start by building the update XML.
        Element gameUpdate = model.getUpdateRepresentation();

        List<PlayerClientHandler> clients = server.getClients();
        Iterator<PlayerClientHandler> iterator = clients.iterator();

        while (iterator.hasNext()) {
            PlayerClientHandler client = iterator.next();
            client.sendMessage(gameUpdate.createCopy());
        }

    }

    /**
     * Handle this client dropping out.
     *
     * @param client
     * @return
     */
    private void handleDropOut(PlayerClientHandler client) {
        if (client.getPlayerId() == 1) {
            // Shut down the whole bloody server.
            server.destroy();
            model.onHide();
            destroy();
        } else {
            model.removePlayer(client.getPlayerId());
            client.destroy();

            updateAllPlayers();
        }
    }
}