gjset.server.game.GameModel.java Source code

Java tutorial

Introduction

Here is the source code for gjset.server.game.GameModel.java

Source

package gjset.server.game;

import gjset.GameConstants;
import gjset.data.Card;
import gjset.data.PlayerData;
import gjset.tools.CountdownTimer;

import java.util.Iterator;
import java.util.List;
import java.util.Observable;
import java.util.Vector;

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

/* 
 *  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/>.
 */

/**
 * This class stores all of the information used to represent the game.
 * 
 * Note that the model does not do any checking internally for whether or not
 * a particular action is valid.  It assumes that anyone that is manipulating this data
 * knows what they're doing.
 * 
 * Generally speaking, the controller is graced with the need to verify the validity of incoming actions.
 */
public class GameModel extends Observable {
    // Store the cards and their current states. 
    private Deck deck;
    private CardTable cardTable;

    // Stores the current state of the game.
    private int gameState;
    private int setCallerId;
    private boolean isLastSetCorrect;

    // Right now there's only a single player.
    private PlayerData[] players;

    private CountdownTimer setTimer;
    private CountdownTimer displayTimer;

    private DocumentFactory documentFactory;

    private static final int SET_TIME = 3500;
    private static final int DISPLAY_TIME = 1000;

    public GameModel() {
        // There is no active game at this time.
        gameState = GameConstants.GAME_STATE_NOT_STARTED;

        // Create the deck.
        deck = new Deck();

        //Create the card table.
        cardTable = new CardTable();

        // Create the player array.
        players = new PlayerData[GameConstants.MAX_PLAYERS];

        // Set the default set caller id
        setCallerId = 0;

        // Create a set timer to abort an incoming set.
        setTimer = new CountdownTimer(SET_TIME, new Runnable() {
            public void run() {
                handleSetTimeout();
            }
        });

        // Create a display timer to resume gameplay after the results of a set are displayed.
        displayTimer = new CountdownTimer(DISPLAY_TIME, new Runnable() {
            public void run() {
                resumeGame();
            }
        });

        // Get a DocumentFactory for use in creating XML representations of the game model.
        documentFactory = DocumentFactory.getInstance();
    }

    /**
     * Return the current state of the game.
     *
     * @return
     */
    public int getGameState() {
        return gameState;
    }

    /**
     * Return the deck.
     *
     * @return
     */
    public Deck getDeck() {
        return deck;
    }

    /**
     * Return the card table we're using.
     *
     * @return
     */
    public CardTable getCardTable() {
        return cardTable;
    }

    /**
     * Return the the player that called set.
     *
     * @return
     */
    public PlayerData getSetCaller() {
        return players[setCallerId - 1];
    }

    /**
     * Returns whether the last set called was correct.
     * 
     * @return boolean
     */
    public boolean getLastSetCorrect() {
        return isLastSetCorrect;
    }

    /**
     * Return the full list of players.
     *
     * @return
     */
    public List<PlayerData> getPlayers() {
        Vector<PlayerData> playerList = new Vector<PlayerData>();

        for (int i = 0; i < players.length; i++) {
            if (players[i] != null) {
                playerList.add(players[i]);
            }
        }

        return playerList;
    }

    /**
     * Return an existing player associated with the indicated username.
     * 
     * If none exists, null is returned.
     *
     * @param username
     * @return
     */
    public PlayerData getExistingPlayer(String username) {
        for (int i = 0; i < players.length; i++) {
            PlayerData player = players[i];
            if (player != null && player.getName().equals(username)) {
                return player;
            }
        }

        return null;
    }

    /**
     * Create a new player and then return it.
     * @param username 
     *
     * @return
     */
    public PlayerData addNewPlayer(String username) {
        // Find the first empty slot.
        int id = 1;
        PlayerData player = players[id - 1];
        while (player != null) {
            id++;
            player = players[id - 1];
        }

        // Then add a new player to that slot.
        player = new PlayerData(id, username);
        players[id - 1] = player;
        return player;
    }

    /**
     * 
     * Start a new game.
     *
     */
    public synchronized void startNewGame() {
        // Shuffle the cards
        deck.shuffle();

        // reset the scores
        for (int i = 0; i < players.length; i++) {
            PlayerData player = players[i];
            if (player != null) {
                player.resetScore();
            }
        }

        //Clear the card table.
        cardTable.removeCards();

        //Deal 12 cards to the table.
        cardTable.addCards(deck.drawCards(12));

        // Set the game active flag to true.
        gameState = GameConstants.GAME_STATE_IDLE;

        // Reset the set caller.
        setCallerId = 0;

        // Reset the timers.
        setTimer.cancel();
        displayTimer.cancel();

        // Reset the last set flag.
        isLastSetCorrect = false;

        // Notify observers that the model has changed.
        setChanged();
        notifyObservers();
    }

    /**
     * Draw more cards from the deck and place them on the card table.
     *
     */
    public synchronized void drawCards() {
        resetCardDrawDesires();

        // Draw 3 new cards to add to the table.
        cardTable.addCards(deck.drawCards(3));

        // Check to see if the game might be over.
        if (deck.getRemainingCards() == 0) {
            checkForEndofGame();
        }

        // Notify observers that the model has changed.
        setChanged();
        notifyObservers();
    }

    /**
     * Find the player with the indicated id and set their draw cards desire.
     *
     * @param playerId
     * @param value
     */
    public void setDesireToDrawCards(int playerId, boolean value) {
        for (int i = 0; i < players.length; i++) {
            if (players[i] != null && players[i].getId() == playerId) {
                players[i].setWantsToDraw(value);
                return;
            }
        }
    }

    /**
     * Hunt through all players to see who wants to draw cards.
     * Return true if all players have this desire.
     *
     * @return
     */
    public boolean allPlayersWantToDraw() {
        for (int i = 0; i < players.length; i++) {
            if (players[i] != null && !players[i].getWantsToDraw()) {
                return false;
            }
        }

        // If we made it this far, everyone wants to draw.
        return true;
    }

    /**
     * Cause the model to engage call set mode for the indicated player.
     *
     * @param playerId
     */
    public synchronized void callSet(int playerId) {
        resetCardDrawDesires();

        gameState = GameConstants.GAME_STATE_SET_CALLED;
        setCallerId = playerId;

        // If this isn't a single player game, start a timer.
        if (getPlayers().size() > 1) {
            setTimer.start();
        }

        // Notify observers that the model has changed.
        setChanged();
        notifyObservers();
    }

    /**
     * Toggle selection on the indicated card. 
     *
     * @param card
     */
    public synchronized void toggleCardSelection(Card card) {
        cardTable.toggleSelection(card);

        // Notify observers that the model has changed.
        setChanged();
        notifyObservers();
    }

    /**
     * Resolves the selected cards to determine if they're a set.
     *
     * @param isWithinTime   indicates whether the set selection was completed within the time limit
     * 
     * @return Return true if this actually was a set.
     */
    public synchronized boolean resolveSet(boolean isWithinTime) {
        // Start by clearing the set timer.
        setTimer.cancel();

        // Get the player object associated with the player that selected these cards.
        PlayerData setPlayer = getSetCaller();

        List<Card> selectedCards = cardTable.getSelectedCards();
        isLastSetCorrect = checkForSet(selectedCards);

        if (isLastSetCorrect) {
            // SCORE!
            setPlayer.addPoints(GameConstants.SET_POINTS);
        } else {
            // Take away points
            setPlayer.addPenalty(GameConstants.SET_PENALTY);
        }

        // Switch to the "Set Finished" gameState for now.
        gameState = GameConstants.GAME_STATE_SET_FINISHED;

        // Start the displayTimer which will switch us back to the Idle gameState.
        displayTimer.start();

        // Notify observers that the model has changed.
        setChanged();
        // If time ran out, this is an unsolicited update and we need to process it accordingly.
        notifyObservers(new Boolean(isWithinTime));

        return isLastSetCorrect;
    }

    /**
     * Remove the player with the indicated id.
     *
     * @param playerId
     */
    public void removePlayer(int playerId) {
        players[playerId - 1] = null;
    }

    /**
     * Create the XML representation of the GameModel as a game update.
     *
     * @return
     */
    public synchronized Element getUpdateRepresentation() {
        Element root = documentFactory.createElement("gameupdate");

        // Start with the deck size.
        Element deckElement = documentFactory.createElement("deck");
        deckElement.setText("" + deck.getRemainingCards());
        root.add(deckElement);

        // Move onto the game state.
        Element gameStateElement = documentFactory.createElement("gamestate");
        gameStateElement.addAttribute("state", "" + gameState);

        // Include the set caller if appropriate.
        if (gameState == GameConstants.GAME_STATE_SET_CALLED
                || gameState == GameConstants.GAME_STATE_SET_FINISHED) {
            Element setCallerElement = documentFactory.createElement("setcaller");
            setCallerElement.setText("" + getSetCaller().getId());
            gameStateElement.add(setCallerElement);

            // Also include whether the set was valid or not, if appropriate.
            if (gameState == GameConstants.GAME_STATE_SET_FINISHED) {
                Element setCorrectElement = documentFactory.createElement("setcorrect");
                setCorrectElement.setText(new Boolean(isLastSetCorrect).toString());
                gameStateElement.add(setCorrectElement);
            }
        }

        root.add(gameStateElement);

        // Now do the players.
        Element playersElement = documentFactory.createElement("players");

        Iterator<PlayerData> iterator = getPlayers().iterator();
        while (iterator.hasNext()) {
            PlayerData player = iterator.next();
            playersElement.add(player.getXMLRepresentation());
        }
        root.add(playersElement);

        // Then the card table.
        Element cardTableElement = cardTable.getRepresentation();
        root.add(cardTableElement);

        return root;
    }

    /**
     * Destroy the model.
     *
     */
    public synchronized void destroy() {
        players = null;
        setTimer.cancel();
        setTimer = null;
        displayTimer.cancel();
        displayTimer = null;
    }

    /**
     * Tell all players to stop wanting to draw cards.
     *
     */
    private void resetCardDrawDesires() {
        for (int i = 0; i < players.length; i++) {
            if (players[i] != null) {
                players[i].setWantsToDraw(false);
            }
        }
    }

    /**
     * Deal with the fact that no set was called within the allotted time.
     *
     */
    private void handleSetTimeout() {
        // Verify that the model is still in the same state it was.
        if (gameState == GameConstants.GAME_STATE_SET_CALLED) {
            // Resolve the set call so that other players are able to call set.
            resolveSet(false);
        }
    }

    /**
     * Resume the game from the "Set Finished" game state, removing, drawing, and de-selecting
     * cards as necessary.
     */
    private synchronized void resumeGame() {
        // Make sure that the model is still in the "Set Finished" state before making any changes.
        if (gameState == GameConstants.GAME_STATE_SET_FINISHED) {
            // Figure out which cards are selected.
            List<Card> selectedCards = cardTable.getSelectedCards();

            // If the set call was successful, remove the Set and replace cards as necessary.
            if (isLastSetCorrect) {
                // Check to see if we can draw more cards.
                if (deck.getRemainingCards() > 0 && cardTable.getNumCards() <= 12) {
                    // Draw the new cards and place them on the table.
                    cardTable.replaceCards(selectedCards, deck.drawCards(3));
                } else {
                    // We aren't allowed to draw new cards, so just remove the selected ones.
                    cardTable.removeCards(selectedCards);
                }
            }

            // De-select any cards that are still selected.
            cardTable.unSelectCards();

            // For now, we immediately return to the idle state.
            gameState = GameConstants.GAME_STATE_IDLE;
            setCallerId = 0;

            // Check to see if this is the end of the game.
            checkForEndofGame();

            // Notify observers that the model has changed.
            setChanged();
            // This is an unsolicited update, too.
            notifyObservers(new Boolean(false));
        }
    }

    /**
     *  This function checks a vector of cards to determine if they are a set.
     */
    private boolean checkForSet(List<Card> cards) {
        // Make sure there are three cards.  If not, it's obviously not a set.
        if (cards.size() != 3) {
            return false;
        }

        // Check each property
        for (int property = 1; property <= 4; property++) {
            // System.out.println("Checking property " + property);

            // Check the first two cards against each other.
            if (cards.get(0).getProperty(property) == cards.get(1).getProperty(property)) {
                // System.out.println("The first two cards match in this property!");
                // The first two cards match. The next two should match as well.
                if (cards.get(1).getProperty(property) != cards.get(2).getProperty(property)) {
                    // System.out.println("But the second two do not.  This is not a set.");
                    return false;
                }
            } else {
                // System.out.println("The first two cards do not match in this property!");
                // The first two cards do not match. The next two should not match either.
                if (cards.get(1).getProperty(property) == cards.get(2).getProperty(property)
                        || cards.get(0).getProperty(property) == cards.get(2).getProperty(property)) {
                    // System.out.println("But there are two that do.  This is not a set.");
                    return false;
                }
            }
        }

        return true;
    }

    /**
     * This method checks to see if the game is over.  
     * The game is considered over when the deck is empty and there are no sets on the board.
     */
    private void checkForEndofGame() {
        // If there are still cards in the deck, the game is not yet over.
        if (deck.getRemainingCards() > 0)
            return;

        // Now check all the cards to see if there are any sets there.
        List<Card> cards = cardTable.getCards();

        for (int i = 0; i < cards.size(); i++) {
            Card card1 = cards.get(i);
            for (int j = i + 1; j < cards.size(); j++) {
                Card card2 = cards.get(j);
                //System.out.println("Checking " + card1 + " and " + card2);
                Card testCard = finishSet(card1, card2);
                //System.out.println("Found " + testCard);

                // If the remaining cards contains the test card, the game is still on.
                if (cards.contains(testCard))
                    return;
                //System.out.println("But the card was not on the table.");
            }
        }

        // We made it this far, there are no sets remaining.
        gameState = GameConstants.GAME_STATE_GAME_OVER;
    }

    /**
     * This function takes two cards and returns the only possible card that completes the set.
     * 
     * This method is used to determine if there are any sets on the table.
     * 
     */
    private Card finishSet(Card card1, Card card2) {
        Card card3 = new Card();
        // Generate each property on the card.
        for (int property = 1; property <= 4; property++) {
            // Check if the first two cards match. If they do, then the third card will also match.
            if (card1.getProperty(property) == card2.getProperty(property)) {
                card3.setProperty(property, card1.getProperty(property));
            } else {
                // The cards differ. Find out which value they are not using.
                for (int value = 1; value < 4; value++) {
                    if (card1.getProperty(property) != value && card2.getProperty(property) != value) {
                        card3.setProperty(property, value);
                    }
                }
            }
        }

        return card3;
    }
}