/*******************************************************************************
* Solitaire
*
* Copyright (C) 2016 by Martin P. Robillard
*
* See: https://github.com/prmr/Solitaire
*
* This program 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*******************************************************************************/
package ca.mcgill.cs.stg.solitaire.model;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
import ca.mcgill.cs.stg.solitaire.ai.GreedyPlayingStrategy;
import ca.mcgill.cs.stg.solitaire.ai.PlayingStrategy;
import ca.mcgill.cs.stg.solitaire.cards.Card;
import ca.mcgill.cs.stg.solitaire.cards.Card.Rank;
import ca.mcgill.cs.stg.solitaire.cards.Card.Suit;
import ca.mcgill.cs.stg.solitaire.cards.Deck;
/**
* Keeps track of the current state of the game and provides
* a facade to it. Implements the Singleton design pattern.
*
* The game state can logically be separated into four distinct
* conceptual elements: the deck, the discard pile, the four
* "suit stacks" where completed suits are accumulated, and the
* seven "working stacks" where cards can be accumulated in sequences
* of alternating suit colors.
*
* To prevent
* this class from degenerating into a God class, responsibilities
* are separated into package-private "manager" classes
* in charge of managing the state. However, these manager classes
* are not responsible for notifying observers.
*/
public final class GameModel implements GameModelView
{
private static final GameModel INSTANCE = new GameModel();
private static final Move NULL_MOVE = new Move()
{
@Override
public void perform()
{} // Does nothing on purpose
@Override
public boolean isNull()
{
return true;
}
@Override
public void undo()
{} // Does nothing on purpose
};
private final Move aDiscardMove = new Move()
{
@Override
public void perform()
{
assert !isEmptyDeck();
aDiscard.push(aDeck.draw());
aMoves.push(this);
notifyListeners();
}
@Override
public boolean isNull()
{
return false;
}
@Override
public void undo()
{
assert !isEmptyDiscardPile();
aDeck.push(aDiscard.pop());
notifyListeners();
}
};
private Deck aDeck = new Deck();
private Stack<Move> aMoves = new Stack<>();
private Stack<Card> aDiscard = new Stack<>();
private SuitStackManager aSuitStacks = new SuitStackManager();
private WorkingStackManager aWorkingStacks = new WorkingStackManager();
private List<GameModelListener> aListeners = new ArrayList<>();
private PlayingStrategy aPlayingStrategy = new GreedyPlayingStrategy();
/**
* Represents anywhere a card can be placed in
* Solitaire.
*/
public interface Location
{}
/**
* Places where a card can be obtained.
*/
public enum CardSources implements Location
{ DISCARD_PILE }
/**
* Represents the different stacks where cards
* can be accumulated.
*/
public enum StackIndex implements Location
{ FIRST, SECOND, THIRD, FOURTH, FIFTH, SIXTH, SEVENTH }
/**
* Represents the different stacks where completed
* suits can be accumulated.
*/
public enum SuitStackIndex implements Location
{
FIRST, SECOND, THIRD, FOURTH;
}
private GameModel()
{
reset();
}
/**
* @return The number of cards in the suit stacks.
*/
public int getScore()
{
return aSuitStacks.getScore();
}
/**
* Try to automatically make a move.
* This may result in nothing happening
* if the auto-play strategy cannot make a
* decision.
* @return whether a move was performed or not.
*/
public boolean tryToAutoPlay()
{
Move move = aPlayingStrategy.computeNextMove(this);
move.perform();
return !move.isNull();
}
/**
* @return The singleton instance for this class.
*/
public static GameModel instance()
{
return INSTANCE;
}
/**
* Registers an observer for the state of the game model.
* @param pListener A listener to register.
*/
public void addListener(GameModelListener pListener)
{
aListeners.add(pListener);
}
private void notifyListeners()
{
for( GameModelListener listener : aListeners )
{
listener.gameStateChanged();
}
}
/**
* Restores the model to the state
* corresponding to the start of a new game.
*/
public void reset()
{
aMoves.clear();
aDeck.shuffle();
aDiscard.clear();
aSuitStacks.initialize();
aWorkingStacks.initialize(aDeck);
notifyListeners();
}
/**
* @return True if the game is completed.
*/
public boolean isCompleted()
{
return aSuitStacks.getScore() == Rank.values().length * Suit.values().length;
}
@Override
public boolean isEmptyDeck()
{
return aDeck.size() == 0;
}
@Override
public boolean isEmptyDiscardPile()
{
return aDiscard.size() == 0;
}
@Override
public boolean isEmptySuitStack(SuitStackIndex pIndex)
{
return aSuitStacks.isEmpty(pIndex);
}
/**
* Obtain the card on top of the suit stack for
* pIndex without discarding it.
* @param pIndex The index of the stack to check
* @return The card on top of the stack.
* @pre !isEmptySuitStack(pIndex)
*/
public Card peekSuitStack(SuitStackIndex pIndex)
{
assert !isEmptySuitStack(pIndex);
return aSuitStacks.peek(pIndex);
}
@Override
public Card peekDiscardPile()
{
assert aDiscard.size() != 0;
return aDiscard.peek();
}
/**
* @param pCard A card to locate
* @return The game location where this card currently is.
* @pre the card is in a location where it can be found and moved.
*/
private Location find(Card pCard)
{
if( !aDiscard.isEmpty() && aDiscard.peek() == pCard )
{
return CardSources.DISCARD_PILE;
}
for( SuitStackIndex index : SuitStackIndex.values() )
{
if( !aSuitStacks.isEmpty(index) && aSuitStacks.peek(index) == pCard )
{
return index;
}
}
for( StackIndex index : StackIndex.values() )
{
if( aWorkingStacks.contains(pCard, index))
{
return index;
}
}
assert false; // We did not find the card: the precondition was not met.
return null;
}
/**
* Undoes the last move.
*/
public void undoLast()
{
if( !aMoves.isEmpty() )
{
aMoves.pop().undo();
}
}
/**
* @return If there is a move to undo.
*/
public boolean canUndo()
{
return !aMoves.isEmpty();
}
/*
* Removes the moveable card from pLocation.
*/
private void absorbCard(Location pLocation)
{
if( pLocation == CardSources.DISCARD_PILE )
{
assert !aDiscard.isEmpty();
aDiscard.pop();
}
else if( pLocation instanceof SuitStackIndex )
{
assert !aSuitStacks.isEmpty((SuitStackIndex)pLocation);
aSuitStacks.pop((SuitStackIndex)pLocation);
}
else
{
assert pLocation instanceof StackIndex;
aWorkingStacks.pop((StackIndex)pLocation);
}
}
private void move(Card pCard, Location pDestination)
{
Location source = find(pCard);
if( source instanceof StackIndex && pDestination instanceof StackIndex )
{
aWorkingStacks.moveWithin(pCard, (StackIndex)source, (StackIndex) pDestination);
}
else
{
absorbCard(source);
if( pDestination instanceof SuitStackIndex )
{
aSuitStacks.push(pCard, (SuitStackIndex)pDestination);
}
else if( pDestination == CardSources.DISCARD_PILE )
{
aDiscard.push(pCard);
}
else
{
assert pDestination instanceof StackIndex;
aWorkingStacks.push(pCard, (StackIndex)pDestination);
}
}
notifyListeners();
}
@Override
public Card[] getStack(StackIndex pIndex)
{
return aWorkingStacks.getStack(pIndex);
}
@Override
public boolean isVisibleInWorkingStack(Card pCard)
{
return aWorkingStacks.contains(pCard) && aWorkingStacks.isVisible(pCard);
}
/**
* Get the sub-stack consisting of pCard and all
* the other cards below it.
* @param pCard The top card of the sub-stack
* @param pIndex The position of the stack to return.
* @return A non-empty sequence of cards.
* @pre pCard is in stack pIndex
*/
public Card[] getSubStack(Card pCard, StackIndex pIndex)
{
return aWorkingStacks.getSequence(pCard, pIndex);
}
@Override
public boolean isLegalMove(Card pCard, Location pDestination )
{
if( pDestination instanceof SuitStackIndex )
{
return aSuitStacks.canMoveTo(pCard, (SuitStackIndex) pDestination);
}
else if( pDestination instanceof StackIndex )
{
return aWorkingStacks.canMoveTo(pCard, (StackIndex) pDestination);
}
else
{
return false;
}
}
@Override
public Move getNullMove()
{
return NULL_MOVE;
}
@Override
public Move getDiscardMove()
{
return aDiscardMove;
}
@Override
public Move getCardMove(Card pCard, Location pDestination)
{
Location source = find( pCard );
if( source instanceof StackIndex && aWorkingStacks.revealsTop(pCard, (StackIndex)source))
{
return new CompositeMove(new CardMove(pCard, pDestination), new RevealTopMove((StackIndex)source) );
}
return new CardMove(pCard, pDestination);
}
/**
* A move that represents the intention to move pCard
* to pDestination, possibly including all cards stacked
* on top of pCard if pCard is in a working stack.
*/
private class CardMove implements Move
{
private Card aCard;
private Location aOrigin;
private Location aDestination;
CardMove(Card pCard, Location pDestination)
{
aCard = pCard;
aDestination = pDestination;
aOrigin = find(pCard);
}
@Override
public void perform()
{
assert isLegalMove(aCard, aDestination);
move(aCard, aDestination);
aMoves.push(this);
}
@Override
public boolean isNull()
{
return false;
}
@Override
public void undo()
{
move(aCard, aOrigin);
}
}
/**
* reveals the top of the stack.
*
*/
private class RevealTopMove implements Move
{
private final StackIndex aIndex;
RevealTopMove(StackIndex pIndex)
{
aIndex = pIndex;
}
@Override
public void perform()
{
aWorkingStacks.showTop(aIndex);
aMoves.push(this);
notifyListeners();
}
@Override
public boolean isNull()
{
return false;
}
@Override
public void undo()
{
aWorkingStacks.hideTop(aIndex);
aMoves.pop().undo();
notifyListeners();
}
}
}