Format using tabs

This commit is contained in:
Charles Gould 2015-06-23 15:13:26 -04:00
parent 1b0d06f20c
commit 43bf9f9adf
17 changed files with 823 additions and 827 deletions

View File

@ -12,32 +12,30 @@ import javafx.stage.Stage;
public class FreeCellApplication extends Application { public class FreeCellApplication extends Application {
public static void main(String[] args) { public static void main(String[] args) {
launch(FreeCellApplication.class, args); launch(FreeCellApplication.class, args);
} }
@Override @Override
public void start(Stage stage) throws Exception { public void start(Stage stage) throws Exception {
VBox root = new VBox(); VBox root = new VBox();
root.setBackground(new Background(new BackgroundImage(new Image( root.setBackground(new Background(new BackgroundImage(new Image("/deck/FELT.jpg"), BackgroundRepeat.NO_REPEAT,
"/deck/FELT.jpg"), BackgroundRepeat.NO_REPEAT, BackgroundRepeat.NO_REPEAT, BackgroundPosition.CENTER, BackgroundSize.DEFAULT)));
BackgroundRepeat.NO_REPEAT, BackgroundPosition.CENTER,
BackgroundSize.DEFAULT)));
Game game = new Game(); Game game = new Game();
GameMenuBar menuBar = new GameMenuBar(); GameMenuBar menuBar = new GameMenuBar();
GameCanvas canvas = new GameCanvas(game, 731, 600); GameCanvas canvas = new GameCanvas(game, 731, 600);
menuBar.setNewGameAction(canvas.getNewGameAction()); menuBar.setNewGameAction(canvas.getNewGameAction());
menuBar.setUndoAction(canvas.getUndoAction()); menuBar.setUndoAction(canvas.getUndoAction());
menuBar.setRedoAction(canvas.getRedoAction()); menuBar.setRedoAction(canvas.getRedoAction());
menuBar.setExitAction(e -> Platform.exit()); menuBar.setExitAction(e -> Platform.exit());
root.getChildren().addAll(menuBar, canvas); root.getChildren().addAll(menuBar, canvas);
Scene scene = new Scene(root); Scene scene = new Scene(root);
stage.setTitle("FreeCell"); stage.setTitle("FreeCell");
stage.setScene(scene); stage.setScene(scene);
stage.setResizable(false); stage.setResizable(false);
stage.show(); stage.show();
} }
} }

View File

@ -10,95 +10,94 @@ import javafx.scene.paint.Color;
*/ */
public class Card { public class Card {
private static DoubleKeyedMap<Rank, Suit, Image> faceImages; private static DoubleKeyedMap<Rank, Suit, Image> faceImages;
public static final Image backImage = new Image("/deck/CARDBACK.png"); public static final Image backImage = new Image("/deck/CARDBACK.png");
public static final double width = backImage.getWidth(); public static final double width = backImage.getWidth();
public static final double height = backImage.getHeight(); public static final double height = backImage.getHeight();
public final Rank rank; public final Rank rank;
public final Suit suit; public final Suit suit;
public final Color color; public final Color color;
private boolean faceUp; private boolean faceUp;
public Card(Rank rank, Suit suit) { public Card(Rank rank, Suit suit) {
this.rank = rank; this.rank = rank;
this.suit = suit; this.suit = suit;
this.color = suit.color(); this.color = suit.color();
this.faceUp = false; this.faceUp = false;
} }
/** /**
* Returns the backside image of a card. * Returns the backside image of a card.
*/ */
public static Image backImage() { public static Image backImage() {
return backImage; return backImage;
} }
/** /**
* Returns the face image of a card. * Returns the face image of a card.
*/ */
public static Image faceImage(Rank rank, Suit suit) { public static Image faceImage(Rank rank, Suit suit) {
if (faceImages == null) { if (faceImages == null) {
faceImages = new DoubleKeyedMap<>(); faceImages = new DoubleKeyedMap<>();
} }
if (!faceImages.contains(rank, suit)) { if (!faceImages.contains(rank, suit)) {
faceImages.put(rank, suit, new Image("/deck/" + rank.value faceImages.put(rank, suit, new Image("/deck/" + rank.value + suit.firstLetter + ".png"));
+ suit.firstLetter + ".png")); }
} return faceImages.get(rank, suit);
return faceImages.get(rank, suit); }
}
/** /**
* Returns the card's face image if its face is up or its backside image * Returns the card's face image if its face is up or its backside image
* otherwise. * otherwise.
*/ */
public Image image() { public Image image() {
return faceUp ? faceImage(rank, suit) : backImage(); return faceUp ? faceImage(rank, suit) : backImage();
} }
/** /**
* Turns the card over, negating its face up status. * Turns the card over, negating its face up status.
*/ */
public void turn() { public void turn() {
faceUp = !faceUp; faceUp = !faceUp;
} }
@Override @Override
public boolean equals(Object other) { public boolean equals(Object other) {
if (other == null || !(other instanceof Card)) { if (other == null || !(other instanceof Card)) {
return false; return false;
} }
Card card = (Card) other; Card card = (Card) other;
return rank == card.rank && suit == card.suit; return rank == card.rank && suit == card.suit;
} }
@Override @Override
public String toString() { public String toString() {
return rank + " of " + suit; return rank + " of " + suit;
} }
public enum Rank { public enum Rank {
ACE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN, JACK, QUEEN, KING; ACE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN, JACK, QUEEN, KING;
/** /**
* 1 for Ace up to 13 for King. * 1 for Ace up to 13 for King.
*/ */
public final int value = ordinal() + 1; public final int value = ordinal() + 1;
} }
public enum Suit { public enum Suit {
SPADES, HEARTS, DIAMONDS, CLUBS; SPADES, HEARTS, DIAMONDS, CLUBS;
/** /**
* S for SPADES, H for HEARTS, D for DIAMONDS, or C for CLUBS. * S for SPADES, H for HEARTS, D for DIAMONDS, or C for CLUBS.
*/ */
public final Character firstLetter = name().charAt(0); public final Character firstLetter = name().charAt(0);
/** /**
* Black or red. * Black or red.
*/ */
public Color color() { public Color color() {
return (this == SPADES || this == CLUBS) ? Color.BLACK : Color.RED; return (this == SPADES || this == CLUBS) ? Color.BLACK : Color.RED;
} }
} }
} }

View File

@ -13,41 +13,41 @@ import java.util.List;
*/ */
public class Deck { public class Deck {
private Deque<Card> cards = new LinkedList<>(); private Deque<Card> cards = new LinkedList<>();
private Deck() { private Deck() {
} }
/** /**
* Makes a 52-card, unshuffled deck. * Makes a 52-card, unshuffled deck.
*/ */
public static Deck newDeck() { public static Deck newDeck() {
Deck deck = new Deck(); Deck deck = new Deck();
for (Rank rank : Rank.values()) { for (Rank rank : Rank.values()) {
for (Suit suit : Suit.values()) { for (Suit suit : Suit.values()) {
deck.cards.push(new Card(rank, suit)); deck.cards.push(new Card(rank, suit));
} }
} }
return deck; return deck;
} }
/** /**
* Deals a card from the top of the deck. * Deals a card from the top of the deck.
*/ */
public Card deal() { public Card deal() {
return cards.pop(); return cards.pop();
} }
/** /**
* Shuffles the deck. * Shuffles the deck.
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public void shuffle() { public void shuffle() {
Collections.shuffle((List<Card>) cards); Collections.shuffle((List<Card>) cards);
} }
@Override @Override
public String toString() { public String toString() {
return cards.toString(); return cards.toString();
} }
} }

View File

@ -12,184 +12,184 @@ import java.util.LinkedList;
import java.util.List; import java.util.List;
public class Game { public class Game {
private List<Cell> cells = new ArrayList<>(4); private List<Cell> cells = new ArrayList<>(4);
private List<Foundation> foundations = new ArrayList<>(4); private List<Foundation> foundations = new ArrayList<>(4);
private List<Tableau> tableaux = new ArrayList<>(8); private List<Tableau> tableaux = new ArrayList<>(8);
private MoveTracker moveTracker = new MoveTracker(); private MoveTracker moveTracker = new MoveTracker();
public Game() { public Game() {
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
cells.add(new Cell()); cells.add(new Cell());
foundations.add(new Foundation()); foundations.add(new Foundation());
} }
for (int i = 0; i < 8; i++) for (int i = 0; i < 8; i++)
tableaux.add(new Tableau()); tableaux.add(new Tableau());
newGame(); newGame();
} }
public List<Cell> getCells() { public List<Cell> getCells() {
return cells; return cells;
} }
public List<Foundation> getFoundations() { public List<Foundation> getFoundations() {
return foundations; return foundations;
} }
public List<Tableau> getTableaux() { public List<Tableau> getTableaux() {
return tableaux; return tableaux;
} }
/** /**
* Moves cards from one pile to another, if the move is valid. * Moves cards from one pile to another, if the move is valid.
*/ */
public boolean tryMove(Pile fromPile, Pile toPile) { public boolean tryMove(Pile fromPile, Pile toPile) {
int cardsMoved = toPile.moveFrom(fromPile); int cardsMoved = toPile.moveFrom(fromPile);
if (cardsMoved > 0) { if (cardsMoved > 0) {
moveTracker.addMove(new MoveAction(fromPile, toPile, cardsMoved)); moveTracker.addMove(new MoveAction(fromPile, toPile, cardsMoved));
return true; return true;
} }
return false; return false;
} }
/** /**
* Redo a move that was previously undone. * Redo a move that was previously undone.
* *
* @return true if another redo can be performed * @return true if another redo can be performed
*/ */
public boolean redo() { public boolean redo() {
if (moveTracker.hasNextMove()) { if (moveTracker.hasNextMove()) {
moveTracker.getNextMove().redo(); moveTracker.getNextMove().redo();
} }
return moveTracker.hasNextMove(); return moveTracker.hasNextMove();
} }
/** /**
* Undo a previous move. * Undo a previous move.
* *
* @return true if another undo can be performed * @return true if another undo can be performed
*/ */
public boolean undo() { public boolean undo() {
if (moveTracker.hasLastMove()) { if (moveTracker.hasLastMove()) {
moveTracker.getLastMove().undo(); moveTracker.getLastMove().undo();
} }
return moveTracker.hasLastMove(); return moveTracker.hasLastMove();
} }
/** /**
* Returns true if the game cannot be lost. * Returns true if the game cannot be lost.
*/ */
public boolean isWon() { public boolean isWon() {
for (Pile pile : tableaux) { for (Pile pile : tableaux) {
if (!pile.isInOrder()) { if (!pile.isInOrder()) {
return false; return false;
} }
} }
return true; return true;
} }
/** /**
* Returns true if the game cannot be won. * Returns true if the game cannot be won.
*/ */
public boolean isLost() { public boolean isLost() {
// Are free cells full? // Are free cells full?
for (Pile pile : cells) { for (Pile pile : cells) {
if (pile.isEmpty()) { if (pile.isEmpty()) {
return false; return false;
} }
} }
// Can you not move to any tableau? // Can you not move to any tableau?
for (Pile pile : tableaux) { for (Pile pile : tableaux) {
for (Pile tableau : tableaux) { for (Pile tableau : tableaux) {
if (pile.canMoveFrom(tableau)) { if (pile.canMoveFrom(tableau)) {
return false; return false;
} }
} }
for (Pile cell : cells) { for (Pile cell : cells) {
if (pile.canMoveFrom(cell)) { if (pile.canMoveFrom(cell)) {
return false; return false;
} }
} }
} }
// Can you not move to any home cell? // Can you not move to any home cell?
for (Pile pile : foundations) { for (Pile pile : foundations) {
for (Pile tableau : tableaux) { for (Pile tableau : tableaux) {
if (pile.canMoveFrom(tableau)) { if (pile.canMoveFrom(tableau)) {
return false; return false;
} }
} }
for (Pile cell : cells) { for (Pile cell : cells) {
if (pile.canMoveFrom(cell)) { if (pile.canMoveFrom(cell)) {
return false; return false;
} }
} }
} }
return true; return true;
} }
public void newGame() { public void newGame() {
Deck deck = Deck.newDeck(); Deck deck = Deck.newDeck();
deck.shuffle(); deck.shuffle();
moveTracker.clearMoves(); moveTracker.clearMoves();
tableaux.forEach(Pile::clear); tableaux.forEach(Pile::clear);
cells.forEach(Pile::clear); cells.forEach(Pile::clear);
foundations.forEach(Pile::clear); foundations.forEach(Pile::clear);
// Deal 6 cards to each tableau. // Deal 6 cards to each tableau.
for (int i = 0; i < 6; i++) { for (int i = 0; i < 6; i++) {
for (int j = 0; j < 8; j++) { for (int j = 0; j < 8; j++) {
Card card = deck.deal(); Card card = deck.deal();
card.turn(); card.turn();
tableaux.get(j).addCard(card); tableaux.get(j).addCard(card);
} }
} }
// Deal an additional card to first 4. // Deal an additional card to first 4.
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
Card card = deck.deal(); Card card = deck.deal();
card.turn(); card.turn();
tableaux.get(i).addCard(card); tableaux.get(i).addCard(card);
} }
} }
private static class MoveTracker { private static class MoveTracker {
private final Deque<MoveAction> nextMoves = new LinkedList<>(); private final Deque<MoveAction> nextMoves = new LinkedList<>();
private final Deque<MoveAction> previousMoves = new LinkedList<>(); private final Deque<MoveAction> previousMoves = new LinkedList<>();
public void clearMoves() { public void clearMoves() {
nextMoves.clear(); nextMoves.clear();
previousMoves.clear(); previousMoves.clear();
} }
public boolean hasLastMove() { public boolean hasLastMove() {
return !previousMoves.isEmpty(); return !previousMoves.isEmpty();
} }
public MoveAction getLastMove() { public MoveAction getLastMove() {
MoveAction lastMove = previousMoves.pop(); MoveAction lastMove = previousMoves.pop();
nextMoves.push(lastMove); nextMoves.push(lastMove);
return lastMove; return lastMove;
} }
public boolean hasNextMove() { public boolean hasNextMove() {
return !nextMoves.isEmpty(); return !nextMoves.isEmpty();
} }
public MoveAction getNextMove() { public MoveAction getNextMove() {
MoveAction nextMove = nextMoves.pop(); MoveAction nextMove = nextMoves.pop();
previousMoves.push(nextMove); previousMoves.push(nextMove);
return nextMove; return nextMove;
} }
public void addMove(MoveAction move) { public void addMove(MoveAction move) {
MoveAction nextMove = nextMoves.peek(); MoveAction nextMove = nextMoves.peek();
/* /*
* if new move differs from saved next move, clear the remaining * if new move differs from saved next move, clear the remaining
* next moves * next moves
*/ */
if (move.equals(nextMove)) { if (move.equals(nextMove)) {
nextMoves.pop(); nextMoves.pop();
} else { } else {
nextMoves.clear(); nextMoves.clear();
} }
previousMoves.push(move); previousMoves.push(move);
} }
} }
} }

View File

@ -1,7 +1,7 @@
package com.charego.freecellfx.model.action; package com.charego.freecellfx.model.action;
public interface Action { public interface Action {
void redo(); void redo();
void undo(); void undo();
} }

View File

@ -3,34 +3,32 @@ package com.charego.freecellfx.model.action;
import com.charego.freecellfx.model.pile.Pile; import com.charego.freecellfx.model.pile.Pile;
public class MoveAction implements Action { public class MoveAction implements Action {
private final Pile fromPile; private final Pile fromPile;
private final Pile toPile; private final Pile toPile;
private final int numCards; private final int numCards;
public MoveAction(Pile fromPile, Pile toPile, int numCards) { public MoveAction(Pile fromPile, Pile toPile, int numCards) {
this.fromPile = fromPile; this.fromPile = fromPile;
this.toPile = toPile; this.toPile = toPile;
this.numCards = numCards; this.numCards = numCards;
} }
@Override @Override
public void redo() { public void redo() {
toPile.moveFromBlindly(fromPile, numCards); toPile.moveFromBlindly(fromPile, numCards);
} }
@Override @Override
public void undo() { public void undo() {
fromPile.moveFromBlindly(toPile, numCards); fromPile.moveFromBlindly(toPile, numCards);
} }
@Override @Override
public boolean equals(Object other) { public boolean equals(Object other) {
if (other == null || !(other instanceof MoveAction)) { if (other == null || !(other instanceof MoveAction)) {
return false; return false;
} }
MoveAction action = (MoveAction) other; MoveAction action = (MoveAction) other;
return fromPile == action.fromPile return fromPile == action.fromPile && toPile == action.toPile && numCards == action.numCards;
&& toPile == action.toPile }
&& numCards == action.numCards;
}
} }

View File

@ -8,41 +8,41 @@ import java.util.Iterator;
abstract class AbstractPile implements Pile { abstract class AbstractPile implements Pile {
private Deque<Card> stack = new ArrayDeque<>(); private Deque<Card> stack = new ArrayDeque<>();
@Override @Override
public void addCard(Card card) { public void addCard(Card card) {
stack.push(card); stack.push(card);
} }
@Override @Override
public Card removeCard() { public Card removeCard() {
return stack.pop(); return stack.pop();
} }
@Override @Override
public Card topCard() { public Card topCard() {
return stack.peek(); return stack.peek();
} }
@Override @Override
public void clear() { public void clear() {
stack.clear(); stack.clear();
} }
@Override @Override
public boolean isEmpty() { public boolean isEmpty() {
return stack.isEmpty(); return stack.isEmpty();
} }
@Override @Override
public int size() { public int size() {
return stack.size(); return stack.size();
} }
@Override @Override
public Iterator<Card> iterator() { public Iterator<Card> iterator() {
return stack.descendingIterator(); return stack.descendingIterator();
} }
} }

View File

@ -2,31 +2,31 @@ package com.charego.freecellfx.model.pile;
public class Cell extends AbstractPile { public class Cell extends AbstractPile {
@Override @Override
public boolean canMoveFrom(Pile other) { public boolean canMoveFrom(Pile other) {
return isEmpty() && !other.isEmpty(); return isEmpty() && !other.isEmpty();
} }
@Override @Override
public int moveFrom(Pile other) { public int moveFrom(Pile other) {
if (canMoveFrom(other)) { if (canMoveFrom(other)) {
addCard(other.removeCard()); addCard(other.removeCard());
return 1; return 1;
} }
return 0; return 0;
} }
@Override @Override
public void moveFromBlindly(Pile other, int numCards) { public void moveFromBlindly(Pile other, int numCards) {
if (numCards != 1) { if (numCards != 1) {
throw new IllegalArgumentException("numCards must be 1"); throw new IllegalArgumentException("numCards must be 1");
} }
addCard(other.removeCard()); addCard(other.removeCard());
} }
@Override @Override
public boolean isInOrder() { public boolean isInOrder() {
return true; return true;
} }
} }

View File

@ -5,42 +5,41 @@ import static com.charego.freecellfx.model.Card.Suit;
public class Foundation extends AbstractPile { public class Foundation extends AbstractPile {
@Override @Override
public boolean canMoveFrom(Pile other) { public boolean canMoveFrom(Pile other) {
if (other.isEmpty()) { if (other.isEmpty()) {
return false; return false;
} }
Rank otherRank = other.topCard().rank; Rank otherRank = other.topCard().rank;
Suit otherSuit = other.topCard().suit; Suit otherSuit = other.topCard().suit;
if (isEmpty()) { if (isEmpty()) {
return otherRank == Rank.ACE; return otherRank == Rank.ACE;
} }
Rank thisRank = topCard().rank; Rank thisRank = topCard().rank;
Suit thisSuit = topCard().suit; Suit thisSuit = topCard().suit;
return otherSuit == thisSuit return otherSuit == thisSuit && otherRank.value == thisRank.value + 1;
&& otherRank.value == thisRank.value + 1; }
}
@Override @Override
public int moveFrom(Pile other) { public int moveFrom(Pile other) {
if (canMoveFrom(other)) { if (canMoveFrom(other)) {
addCard(other.removeCard()); addCard(other.removeCard());
return 1; return 1;
} }
return 0; return 0;
} }
@Override @Override
public void moveFromBlindly(Pile other, int numCards) { public void moveFromBlindly(Pile other, int numCards) {
if (numCards != 1) { if (numCards != 1) {
throw new IllegalArgumentException("numCards must be 1"); throw new IllegalArgumentException("numCards must be 1");
} }
addCard(other.removeCard()); addCard(other.removeCard());
} }
@Override @Override
public boolean isInOrder() { public boolean isInOrder() {
return true; return true;
} }
} }

View File

@ -4,56 +4,57 @@ import com.charego.freecellfx.model.Card;
public interface Pile extends Iterable<Card> { public interface Pile extends Iterable<Card> {
/** /**
* Adds a card to the top of the pile. * Adds a card to the top of the pile.
*/ */
void addCard(Card card); void addCard(Card card);
/** /**
* Removes the top card from the pile. * Removes the top card from the pile.
*/ */
Card removeCard(); Card removeCard();
/** /**
* Returns true if the contents of another pile can be moved to this pile. * Returns true if the contents of another pile can be moved to this pile.
*/ */
boolean canMoveFrom(Pile other); boolean canMoveFrom(Pile other);
/** /**
* Moves cards from another pile to this pile, if the tryMove is valid. * Moves cards from another pile to this pile, if the tryMove is valid.
* *
* @return the number of cards moved (i.e., possibly 0) * @return the number of cards moved (i.e., possibly 0)
*/ */
int moveFrom(Pile other); int moveFrom(Pile other);
/** /**
* Moves cards from another pile to this pile, whether or not the tryMove is valid. * Moves cards from another pile to this pile, whether or not the tryMove is
*/ * valid.
void moveFromBlindly(Pile other, int numCards); */
void moveFromBlindly(Pile other, int numCards);
/** /**
* Returns the top card in the pile, if a card is present. * Returns the top card in the pile, if a card is present.
*/ */
Card topCard(); Card topCard();
/** /**
* Returns true if the contents of the pile are in order. * Returns true if the contents of the pile are in order.
*/ */
boolean isInOrder(); boolean isInOrder();
/** /**
* Clears the contents of the pile. * Clears the contents of the pile.
*/ */
void clear(); void clear();
/** /**
* Returns true if the pile is empty. * Returns true if the pile is empty.
*/ */
boolean isEmpty(); boolean isEmpty();
/** /**
* Returns the size of the pile. * Returns the size of the pile.
*/ */
int size(); int size();
} }

View File

@ -7,134 +7,133 @@ import java.util.LinkedList;
public class Tableau extends AbstractPile { public class Tableau extends AbstractPile {
/** /**
* Maintains the ordering of the cards within the pile. * Maintains the ordering of the cards within the pile.
*/ */
private Deque<Integer> orderStack = new LinkedList<>(); private Deque<Integer> orderStack = new LinkedList<>();
@Override @Override
public void addCard(Card card) { public void addCard(Card card) {
countAddedCard(card); countAddedCard(card);
super.addCard(card); super.addCard(card);
} }
private void countAddedCard(Card card) { private void countAddedCard(Card card) {
if (isEmpty() || orderStack.isEmpty()) { if (isEmpty() || orderStack.isEmpty()) {
orderStack.push(1); orderStack.push(1);
} else if (card.color != topCard().color && } else if (card.color != topCard().color && card.rank.value == topCard().rank.value - 1) {
card.rank.value == topCard().rank.value - 1) { orderStack.push(orderStack.pop() + 1);
orderStack.push(orderStack.pop() + 1); } else {
} else { orderStack.push(1);
orderStack.push(1); }
} }
}
@Override @Override
public Card removeCard() { public Card removeCard() {
countRemovedCard(); countRemovedCard();
return super.removeCard(); return super.removeCard();
} }
private void countRemovedCard() { private void countRemovedCard() {
int topInOrder = orderStack.pop(); int topInOrder = orderStack.pop();
if (topInOrder > 1) { if (topInOrder > 1) {
orderStack.push(topInOrder - 1); orderStack.push(topInOrder - 1);
} }
} }
@Override @Override
public boolean canMoveFrom(Pile other) { public boolean canMoveFrom(Pile other) {
if (other.isEmpty()) { if (other.isEmpty()) {
return false; return false;
} }
if (isEmpty()) { if (isEmpty()) {
return true; return true;
} }
int rankDiff = topCard().rank.value - other.topCard().rank.value; int rankDiff = topCard().rank.value - other.topCard().rank.value;
if (rankDiff < 1) { if (rankDiff < 1) {
return false; return false;
} }
if (other instanceof Foundation || other instanceof Cell) { if (other instanceof Foundation || other instanceof Cell) {
return rankDiff == 1 && topCard().color != other.topCard().color; return rankDiff == 1 && topCard().color != other.topCard().color;
} }
if (other instanceof Tableau) { if (other instanceof Tableau) {
int otherTopInOrder = ((Tableau) other).topInOrder(); int otherTopInOrder = ((Tableau) other).topInOrder();
if (otherTopInOrder >= rankDiff) { if (otherTopInOrder >= rankDiff) {
switch (rankDiff % 2) { switch (rankDiff % 2) {
case 0: case 0:
return topCard().color == other.topCard().color; return topCard().color == other.topCard().color;
case 1: case 1:
return topCard().color != other.topCard().color; return topCard().color != other.topCard().color;
} }
} }
} }
return false; return false;
} }
@Override @Override
public int moveFrom(Pile other) { public int moveFrom(Pile other) {
if (!canMoveFrom(other)) { if (!canMoveFrom(other)) {
return 0; return 0;
} }
if (other instanceof Foundation || other instanceof Cell) { if (other instanceof Foundation || other instanceof Cell) {
addCard(other.removeCard()); addCard(other.removeCard());
return 1; return 1;
} }
if (other instanceof Tableau) { if (other instanceof Tableau) {
if (isEmpty()) { if (isEmpty()) {
// Move all ordered cards to this pile // Move all ordered cards to this pile
int otherTopInOrder = ((Tableau) other).topInOrder(); int otherTopInOrder = ((Tableau) other).topInOrder();
moveFrom(other, otherTopInOrder); moveFrom(other, otherTopInOrder);
return otherTopInOrder; return otherTopInOrder;
} else { } else {
// Move ordered cards until they reach the current top card // Move ordered cards until they reach the current top card
int rankDiff = topCard().rank.value - other.topCard().rank.value; int rankDiff = topCard().rank.value - other.topCard().rank.value;
moveFrom(other, rankDiff); moveFrom(other, rankDiff);
return rankDiff; return rankDiff;
} }
} }
throw new IllegalStateException(); throw new IllegalStateException();
} }
/** /**
* Moves {@code n} cards from the other pile to this pile. * Moves {@code n} cards from the other pile to this pile.
*/ */
private void moveFrom(Pile other, int n) { private void moveFrom(Pile other, int n) {
Deque<Card> topCards = new LinkedList<>(); Deque<Card> topCards = new LinkedList<>();
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
topCards.addLast(other.removeCard()); topCards.addLast(other.removeCard());
} }
while (!topCards.isEmpty()) { while (!topCards.isEmpty()) {
addCard(topCards.removeLast()); addCard(topCards.removeLast());
} }
} }
@Override @Override
public void moveFromBlindly(Pile other, int numCards) { public void moveFromBlindly(Pile other, int numCards) {
moveFrom(other, numCards); moveFrom(other, numCards);
} }
@Override @Override
public void clear() { public void clear() {
super.clear(); super.clear();
orderStack.clear(); orderStack.clear();
} }
@Override @Override
public boolean isInOrder() { public boolean isInOrder() {
return (orderStack.size() < 2); return (orderStack.size() < 2);
} }
/** /**
* Returns the number of cards in order at the top of the pile. * Returns the number of cards in order at the top of the pile.
*/ */
public int topInOrder() { public int topInOrder() {
return orderStack.size() == 0 ? 0 : orderStack.peek(); return orderStack.size() == 0 ? 0 : orderStack.peek();
} }
@Override @Override
public String toString() { public String toString() {
return orderStack.toString(); return orderStack.toString();
} }
} }

View File

@ -6,29 +6,32 @@ import java.util.Map;
/** /**
* Maps a pair of coordinates to an object. * Maps a pair of coordinates to an object.
* *
* @param <K1> the first coordinate * @param <K1>
* @param <K2> the second coordinate * the first coordinate
* @param <V> the treasure * @param <K2>
* the second coordinate
* @param <V>
* the treasure
*/ */
public class DoubleKeyedMap<K1, K2, V> { public class DoubleKeyedMap<K1, K2, V> {
private Map<K1, Map<K2, V>> map = new HashMap<>(); private Map<K1, Map<K2, V>> map = new HashMap<>();
public boolean contains(K1 firstKey, K2 secondKey) { public boolean contains(K1 firstKey, K2 secondKey) {
return map.containsKey(firstKey) && map.get(firstKey).containsKey(secondKey); return map.containsKey(firstKey) && map.get(firstKey).containsKey(secondKey);
} }
public V get(K1 firstKey, K2 secondKey) { public V get(K1 firstKey, K2 secondKey) {
return map.get(firstKey).get(secondKey); return map.get(firstKey).get(secondKey);
} }
/** /**
* Returns the old value if it exists, or null. * Returns the old value if it exists, or null.
*/ */
public V put(K1 firstKey, K2 secondKey, V newValue) { public V put(K1 firstKey, K2 secondKey, V newValue) {
if (!map.containsKey(firstKey)) { if (!map.containsKey(firstKey)) {
map.put(firstKey, new HashMap<>()); map.put(firstKey, new HashMap<>());
} }
return map.get(firstKey).put(secondKey, newValue); return map.get(firstKey).put(secondKey, newValue);
} }
} }

View File

@ -18,152 +18,152 @@ import java.util.List;
public class GameCanvas extends Canvas { public class GameCanvas extends Canvas {
private final Game game; private final Game game;
private final GraphicsContext gc; private final GraphicsContext gc;
private final PileView[] pileViews; private final PileView[] pileViews;
private PileView fromPile; private PileView fromPile;
private boolean winMessageShown; private boolean winMessageShown;
public GameCanvas(Game game, double width, double height) { public GameCanvas(Game game, double width, double height) {
super(width, height); super(width, height);
this.game = game; this.game = game;
this.gc = getGraphicsContext2D(); this.gc = getGraphicsContext2D();
this.pileViews = constructPileViews(game); this.pileViews = constructPileViews(game);
setOnMouseClicked(new MouseClickHandler()); setOnMouseClicked(new MouseClickHandler());
updateView(); updateView();
} }
private static PileView[] constructPileViews(Game game) { private static PileView[] constructPileViews(Game game) {
List<PileView> list = new ArrayList<>(); List<PileView> list = new ArrayList<>();
for (Pile pile : game.getCells()) { for (Pile pile : game.getCells()) {
list.add(new StackedPileView(pile)); list.add(new StackedPileView(pile));
} }
for (Pile pile : game.getFoundations()) { for (Pile pile : game.getFoundations()) {
list.add(new StackedPileView(pile)); list.add(new StackedPileView(pile));
} }
for (Pile pile : game.getTableaux()) { for (Pile pile : game.getTableaux()) {
list.add(new CascadingPileView(pile)); list.add(new CascadingPileView(pile));
} }
return list.toArray(new PileView[16]); return list.toArray(new PileView[16]);
} }
private void checkEndingConditions() { private void checkEndingConditions() {
if (winMessageShown) { if (winMessageShown) {
return; return;
} }
if (game.isWon()) { if (game.isWon()) {
winMessageShown = true; winMessageShown = true;
new Alert(Alert.AlertType.INFORMATION, "You won!").showAndWait(); new Alert(Alert.AlertType.INFORMATION, "You won!").showAndWait();
} else if (game.isLost()) { } else if (game.isLost()) {
new Alert(Alert.AlertType.INFORMATION, "You lost!").showAndWait(); new Alert(Alert.AlertType.INFORMATION, "You lost!").showAndWait();
} }
} }
private PileView findPile(double x, double y) { private PileView findPile(double x, double y) {
int columnIndex = 0; int columnIndex = 0;
x -= 15; x -= 15;
while (x > 90) { while (x > 90) {
x -= 90; x -= 90;
columnIndex++; columnIndex++;
} }
boolean columnPressed = x < Card.width; boolean columnPressed = x < Card.width;
if (!columnPressed) { if (!columnPressed) {
return null; return null;
} }
if (y > 15 && y < 15 + Card.height) { if (y > 15 && y < 15 + Card.height) {
return pileViews[columnIndex]; return pileViews[columnIndex];
} }
if (y > 120) { if (y > 120) {
PileView pileView = pileViews[columnIndex + 8]; PileView pileView = pileViews[columnIndex + 8];
Pile pile = pileView.getPile(); Pile pile = pileView.getPile();
double pileHeight = 120 + (pile.size() - 1) * CascadingPileView.CARD_MARGIN + Card.height; double pileHeight = 120 + (pile.size() - 1) * CascadingPileView.CARD_MARGIN + Card.height;
if (y < pileHeight) { if (y < pileHeight) {
return pileView; return pileView;
} }
} }
return null; return null;
} }
public void updateView() { public void updateView() {
gc.clearRect(0, 0, super.getWidth(), super.getHeight()); gc.clearRect(0, 0, super.getWidth(), super.getHeight());
double x = 15; double x = 15;
for (int i = 0; i < 8; i++) { for (int i = 0; i < 8; i++) {
pileViews[i].paint(gc, x, 15); pileViews[i].paint(gc, x, 15);
x += 90; x += 90;
} }
x = 15; x = 15;
for (int i = 8; i < 16; i++) { for (int i = 8; i < 16; i++) {
pileViews[i].paint(gc, x, 120); pileViews[i].paint(gc, x, 120);
x += 90; x += 90;
} }
} }
public EventHandler<ActionEvent> getNewGameAction() { public EventHandler<ActionEvent> getNewGameAction() {
return e -> { return e -> {
game.newGame(); game.newGame();
winMessageShown = false; winMessageShown = false;
if (fromPile != null && fromPile.isSelected()) { if (fromPile != null && fromPile.isSelected()) {
fromPile.toggleSelected(); fromPile.toggleSelected();
} }
fromPile = null; fromPile = null;
updateView(); updateView();
}; };
} }
public EventHandler<ActionEvent> getUndoAction() { public EventHandler<ActionEvent> getUndoAction() {
return e -> { return e -> {
game.undo(); game.undo();
updateView(); updateView();
}; };
} }
public EventHandler<ActionEvent> getRedoAction() { public EventHandler<ActionEvent> getRedoAction() {
return e -> { return e -> {
game.redo(); game.redo();
updateView(); updateView();
}; };
} }
private class MouseClickHandler implements EventHandler<MouseEvent> { private class MouseClickHandler implements EventHandler<MouseEvent> {
@Override @Override
public void handle(MouseEvent e) { public void handle(MouseEvent e) {
PileView clickedPile = findPile(e.getX(), e.getY()); PileView clickedPile = findPile(e.getX(), e.getY());
if (clickedPile == null) { if (clickedPile == null) {
if (fromPile != null) { if (fromPile != null) {
fromPile.toggleSelected(); fromPile.toggleSelected();
} }
fromPile = null; fromPile = null;
updateView(); updateView();
return; return;
} }
if (fromPile == null) { if (fromPile == null) {
fromPile = clickedPile; fromPile = clickedPile;
fromPile.toggleSelected(); fromPile.toggleSelected();
updateView(); updateView();
return; return;
} }
if (fromPile == clickedPile) { if (fromPile == clickedPile) {
// Double click: try moving to a foundation. // Double click: try moving to a foundation.
fromPile.toggleSelected(); fromPile.toggleSelected();
for (int i = 4; i < 8; i++) { for (int i = 4; i < 8; i++) {
if (game.tryMove(fromPile.getPile(), pileViews[i].getPile())) { if (game.tryMove(fromPile.getPile(), pileViews[i].getPile())) {
checkEndingConditions(); checkEndingConditions();
break; break;
} }
} }
fromPile = null; fromPile = null;
updateView(); updateView();
} else { } else {
// Try moving to other cell. // Try moving to other cell.
fromPile.toggleSelected(); fromPile.toggleSelected();
if (game.tryMove(fromPile.getPile(), clickedPile.getPile())) { if (game.tryMove(fromPile.getPile(), clickedPile.getPile())) {
checkEndingConditions(); checkEndingConditions();
} }
fromPile = null; fromPile = null;
updateView(); updateView();
} }
} }
} }
} }

View File

@ -11,44 +11,44 @@ import javafx.scene.input.KeyCombination;
public class GameMenuBar extends MenuBar { public class GameMenuBar extends MenuBar {
private final MenuItem newGame; private final MenuItem newGame;
private final MenuItem undoMove; private final MenuItem undoMove;
private final MenuItem redoMove; private final MenuItem redoMove;
private final MenuItem exitGame; private final MenuItem exitGame;
public GameMenuBar() { public GameMenuBar() {
super(); super();
newGame = new MenuItem("New"); newGame = new MenuItem("New");
undoMove = new MenuItem("Undo"); undoMove = new MenuItem("Undo");
redoMove = new MenuItem("Redo"); redoMove = new MenuItem("Redo");
exitGame = new MenuItem("Exit"); exitGame = new MenuItem("Exit");
setAccelerators(); setAccelerators();
Menu gameMenu = new Menu("Game"); Menu gameMenu = new Menu("Game");
gameMenu.getItems().addAll(newGame, undoMove, redoMove, exitGame); gameMenu.getItems().addAll(newGame, undoMove, redoMove, exitGame);
super.getMenus().add(gameMenu); super.getMenus().add(gameMenu);
} }
private void setAccelerators() { private void setAccelerators() {
newGame.setAccelerator(new KeyCodeCombination(KeyCode.F2)); newGame.setAccelerator(new KeyCodeCombination(KeyCode.F2));
undoMove.setAccelerator(new KeyCodeCombination(KeyCode.Z, KeyCombination.CONTROL_DOWN)); undoMove.setAccelerator(new KeyCodeCombination(KeyCode.Z, KeyCombination.CONTROL_DOWN));
redoMove.setAccelerator(new KeyCodeCombination(KeyCode.Y, KeyCombination.CONTROL_DOWN)); redoMove.setAccelerator(new KeyCodeCombination(KeyCode.Y, KeyCombination.CONTROL_DOWN));
exitGame.setAccelerator(new KeyCodeCombination(KeyCode.ESCAPE)); exitGame.setAccelerator(new KeyCodeCombination(KeyCode.ESCAPE));
} }
public void setNewGameAction(EventHandler<ActionEvent> handler) { public void setNewGameAction(EventHandler<ActionEvent> handler) {
newGame.setOnAction(handler); newGame.setOnAction(handler);
} }
public void setUndoAction(EventHandler<ActionEvent> handler) { public void setUndoAction(EventHandler<ActionEvent> handler) {
undoMove.setOnAction(handler); undoMove.setOnAction(handler);
} }
public void setRedoAction(EventHandler<ActionEvent> handler) { public void setRedoAction(EventHandler<ActionEvent> handler) {
redoMove.setOnAction(handler); redoMove.setOnAction(handler);
} }
public void setExitAction(EventHandler<ActionEvent> handler) { public void setExitAction(EventHandler<ActionEvent> handler) {
exitGame.setOnAction(handler); exitGame.setOnAction(handler);
} }
} }

View File

@ -8,43 +8,42 @@ import javafx.scene.paint.Color;
public class CascadingPileView extends PileView { public class CascadingPileView extends PileView {
public static final int CARD_MARGIN = 22; public static final int CARD_MARGIN = 22;
public static final int TOP_MARGIN = 0; public static final int TOP_MARGIN = 0;
public CascadingPileView(Pile pile) { public CascadingPileView(Pile pile) {
super(pile); super(pile);
} }
@Override @Override
public void paint(GraphicsContext gc, double x, double y) { public void paint(GraphicsContext gc, double x, double y) {
if (pile.isEmpty()) { if (pile.isEmpty()) {
// draw an outline // draw an outline
gc.setStroke(Color.YELLOW); gc.setStroke(Color.YELLOW);
gc.strokeRect(x, y, Card.width, Card.height); gc.strokeRect(x, y, Card.width, Card.height);
if (isSelected()) { if (isSelected()) {
// highlight empty cell // highlight empty cell
gc.setFill(highlight); gc.setFill(highlight);
gc.fillRect(x, y, Card.width, Card.height); gc.fillRect(x, y, Card.width, Card.height);
} }
} else { } else {
double yCurrent = y; double yCurrent = y;
// draw the cards in a cascade from top to bottom // draw the cards in a cascade from top to bottom
for (Card card : pile) { for (Card card : pile) {
gc.drawImage(card.image(), x, yCurrent); gc.drawImage(card.image(), x, yCurrent);
yCurrent += CARD_MARGIN; yCurrent += CARD_MARGIN;
} }
if (isSelected()) { if (isSelected()) {
// highlight ordered cards // highlight ordered cards
if (yCurrent > TOP_MARGIN) { if (yCurrent > TOP_MARGIN) {
yCurrent -= CARD_MARGIN; yCurrent -= CARD_MARGIN;
} }
int multiplier = ((Tableau) pile).topInOrder(); int multiplier = ((Tableau) pile).topInOrder();
int heightOfRect = (multiplier - 1) * CARD_MARGIN; int heightOfRect = (multiplier - 1) * CARD_MARGIN;
gc.setFill(highlight); gc.setFill(highlight);
gc.fillRect(x, yCurrent - heightOfRect, Card.width, heightOfRect gc.fillRect(x, yCurrent - heightOfRect, Card.width, heightOfRect + Card.height);
+ Card.height); }
} }
} }
}
} }

View File

@ -6,26 +6,26 @@ import javafx.scene.paint.Color;
public abstract class PileView { public abstract class PileView {
protected static final Color highlight = new Color(1, 1, 0, 0.6); protected static final Color highlight = new Color(1, 1, 0, 0.6);
protected final Pile pile; protected final Pile pile;
private boolean isSelected = false; private boolean isSelected = false;
public PileView(Pile pile) { public PileView(Pile pile) {
this.pile = pile; this.pile = pile;
} }
public Pile getPile() { public Pile getPile() {
return pile; return pile;
} }
public abstract void paint(GraphicsContext gc, double x, double y); public abstract void paint(GraphicsContext gc, double x, double y);
public boolean isSelected() { public boolean isSelected() {
return isSelected; return isSelected;
} }
public void toggleSelected() { public void toggleSelected() {
isSelected = !isSelected; isSelected = !isSelected;
} }
} }

View File

@ -7,24 +7,24 @@ import javafx.scene.paint.Color;
public class StackedPileView extends PileView { public class StackedPileView extends PileView {
public StackedPileView(Pile pile) { public StackedPileView(Pile pile) {
super(pile); super(pile);
} }
@Override @Override
public void paint(GraphicsContext gc, double x, double y) { public void paint(GraphicsContext gc, double x, double y) {
if (pile.isEmpty()) { if (pile.isEmpty()) {
// draw an outline // draw an outline
gc.setStroke(Color.YELLOW); gc.setStroke(Color.YELLOW);
gc.strokeRect(x, y, Card.width, Card.height); gc.strokeRect(x, y, Card.width, Card.height);
} else { } else {
// draw the top card // draw the top card
gc.drawImage(pile.topCard().image(), x, y); gc.drawImage(pile.topCard().image(), x, y);
} }
if (isSelected()) { if (isSelected()) {
gc.setFill(highlight); gc.setFill(highlight);
gc.fillRect(x, y, Card.width, Card.height); gc.fillRect(x, y, Card.width, Card.height);
} }
} }
} }