commit c2bcbe9f325e6e1d0776c927dfcb2b281202aa06 Author: Charles Gould Date: Sat Apr 18 01:26:08 2015 -0400 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5626008 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Maven +target/ + +# Eclipse +.settings/ +*.classpath +*.project + +# IntelliJ IDEA +*.idea +*.iml diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..61914ca --- /dev/null +++ b/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + + com.charego + freecellfx + 0.1 + + + charego.com + + + + UTF-8 + + + + + + maven-compiler-plugin + 3.3 + + 1.8 + 1.8 + + + + com.zenjava + javafx-maven-plugin + 8.1.2 + + com.charego.freecellfx.FreeCellApplication + + + + + + diff --git a/src/main/java/com/charego/freecellfx/FreeCellApplication.java b/src/main/java/com/charego/freecellfx/FreeCellApplication.java new file mode 100644 index 0000000..13ec81f --- /dev/null +++ b/src/main/java/com/charego/freecellfx/FreeCellApplication.java @@ -0,0 +1,36 @@ +package com.charego.freecellfx; + +import com.charego.freecellfx.model.Game; +import com.charego.freecellfx.view.GameCanvas; +import javafx.application.Application; +import javafx.scene.Scene; +import javafx.scene.image.Image; +import javafx.scene.layout.*; +import javafx.stage.Stage; + +public class FreeCellApplication extends Application { + + public static void main(String[] args) { + launch(FreeCellApplication.class, args); + } + + @Override + public void start(Stage stage) throws Exception { + AnchorPane root = new AnchorPane(); + root.setBackground(new Background(new BackgroundImage(new Image( + "/deck/FELT.jpg"), BackgroundRepeat.NO_REPEAT, + BackgroundRepeat.NO_REPEAT, BackgroundPosition.CENTER, + BackgroundSize.DEFAULT))); + + GameCanvas canvas = new GameCanvas(new Game(), 731, 600); + root.getChildren().add(canvas); + Scene scene = new Scene(root); + + stage.setTitle("FreeCell"); + stage.getIcons().add(new Image("/icons/DIAMOND.jpg")); + stage.setScene(scene); + stage.setResizable(false); + stage.show(); + } + +} diff --git a/src/main/java/com/charego/freecellfx/model/Card.java b/src/main/java/com/charego/freecellfx/model/Card.java new file mode 100644 index 0000000..984021e --- /dev/null +++ b/src/main/java/com/charego/freecellfx/model/Card.java @@ -0,0 +1,104 @@ +package com.charego.freecellfx.model; + +import com.charego.freecellfx.util.DoubleKeyedMap; +import javafx.scene.image.Image; +import javafx.scene.paint.Color; + +/** + * Represents a playing card with the ability to turn between its face and + * backside images. + */ +public class Card { + + private static DoubleKeyedMap faceImages; + public static final Image backImage = new Image("/deck/CARDBACK.png"); + public static final double width = backImage.getWidth(); + public static final double height = backImage.getHeight(); + + public final Rank rank; + public final Suit suit; + public final Color color; + private boolean faceUp; + + public Card(Rank rank, Suit suit) { + this.rank = rank; + this.suit = suit; + this.color = suit.color(); + this.faceUp = false; + } + + /** + * Returns the backside image of a card. + */ + public static Image backImage() { + return backImage; + } + + /** + * Returns the face image of a card. + */ + public static Image faceImage(Rank rank, Suit suit) { + if (faceImages == null) { + faceImages = new DoubleKeyedMap<>(); + } + if (!faceImages.contains(rank, suit)) { + faceImages.put(rank, suit, new Image("/deck/" + rank.value + + suit.firstLetter + ".png")); + } + return faceImages.get(rank, suit); + } + + /** + * Returns the card's face image if its face is up or its backside image + * otherwise. + */ + public Image image() { + return faceUp ? faceImage(rank, suit) : backImage(); + } + + /** + * Turns the card over, negating its face up status. + */ + public void turn() { + faceUp = !faceUp; + } + + @Override + public boolean equals(Object other) { + if (other == null || !(other instanceof Card)) { + return false; + } + Card card = (Card) other; + return rank == card.rank && suit == card.suit; + } + + @Override + public String toString() { + return rank + " of " + suit; + } + + public enum Rank { + ACE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN, JACK, QUEEN, KING; + + /** + * 1 for Ace up to 13 for King. + */ + public final int value = ordinal() + 1; + } + + public enum Suit { + SPADES, HEARTS, DIAMONDS, CLUBS; + + /** + * S for SPADES, H for HEARTS, D for DIAMONDS, or C for CLUBS. + */ + public final Character firstLetter = name().charAt(0); + + /** + * Black or red. + */ + public Color color() { + return (this == SPADES || this == CLUBS) ? Color.BLACK : Color.RED; + } + } +} diff --git a/src/main/java/com/charego/freecellfx/model/Deck.java b/src/main/java/com/charego/freecellfx/model/Deck.java new file mode 100644 index 0000000..deef5b4 --- /dev/null +++ b/src/main/java/com/charego/freecellfx/model/Deck.java @@ -0,0 +1,53 @@ +package com.charego.freecellfx.model; + +import com.charego.freecellfx.model.Card.Rank; +import com.charego.freecellfx.model.Card.Suit; + +import java.util.Collections; +import java.util.Deque; +import java.util.LinkedList; +import java.util.List; + +/** + * Represents a deck with the ability to deal and shuffle. + */ +public class Deck { + + private Deque cards = new LinkedList<>(); + + private Deck() { + } + + /** + * Makes a 52-card, unshuffled deck. + */ + public static Deck newDeck() { + Deck deck = new Deck(); + for (Rank rank : Rank.values()) { + for (Suit suit : Suit.values()) { + deck.cards.push(new Card(rank, suit)); + } + } + return deck; + } + + /** + * Deals a card from the top of the deck. + */ + public Card deal() { + return cards.pop(); + } + + /** + * Shuffles the deck. + */ + @SuppressWarnings("unchecked") + public void shuffle() { + Collections.shuffle((List) cards); + } + + @Override + public String toString() { + return cards.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/com/charego/freecellfx/model/Game.java b/src/main/java/com/charego/freecellfx/model/Game.java new file mode 100644 index 0000000..6751e2d --- /dev/null +++ b/src/main/java/com/charego/freecellfx/model/Game.java @@ -0,0 +1,195 @@ +package com.charego.freecellfx.model; + +import com.charego.freecellfx.model.pile.Cell; +import com.charego.freecellfx.model.pile.Pile; +import com.charego.freecellfx.model.action.MoveAction; +import com.charego.freecellfx.model.pile.Foundation; +import com.charego.freecellfx.model.pile.Tableau; + +import java.util.ArrayList; +import java.util.Deque; +import java.util.LinkedList; +import java.util.List; + +public class Game { + private List cells = new ArrayList<>(4); + private List foundations = new ArrayList<>(4); + private List tableaux = new ArrayList<>(8); + private MoveTracker moveTracker = new MoveTracker(); + + public Game() { + for (int i = 0; i < 4; i++) { + cells.add(new Cell()); + foundations.add(new Foundation()); + } + for (int i = 0; i < 8; i++) + tableaux.add(new Tableau()); + newGame(); + } + + public List getCells() { + return cells; + } + + public List getFoundations() { + return foundations; + } + + public List getTableaux() { + return tableaux; + } + + /** + * Moves cards from one pile to another, if the move is valid. + */ + public boolean tryMove(Pile fromPile, Pile toPile) { + int cardsMoved = toPile.moveFrom(fromPile); + if (cardsMoved > 0) { + moveTracker.addMove(new MoveAction(fromPile, toPile, cardsMoved)); + return true; + } + return false; + } + + /** + * Redo a move that was previously undone. + * + * @return true if another redo can be performed + */ + public boolean redo() { + if (moveTracker.hasNextMove()) { + moveTracker.getNextMove().redo(); + } + return moveTracker.hasNextMove(); + } + + /** + * Undo a previous move. + * + * @return true if another undo can be performed + */ + public boolean undo() { + if (moveTracker.hasLastMove()) { + moveTracker.getLastMove().undo(); + } + return moveTracker.hasLastMove(); + } + + /** + * Returns true if the game cannot be lost. + */ + public boolean isWon() { + for (Pile pile : tableaux) { + if (!pile.isInOrder()) { + return false; + } + } + return true; + } + + /** + * Returns true if the game cannot be won. + */ + public boolean isLost() { + // Are free cells full? + for (Pile pile : cells) { + if (pile.isEmpty()) { + return false; + } + } + // Can you not move to any tableau? + for (Pile pile : tableaux) { + for (Pile tableau : tableaux) { + if (pile.canMoveFrom(tableau)) { + return false; + } + } + for (Pile cell : cells) { + if (pile.canMoveFrom(cell)) { + return false; + } + } + } + // Can you not move to any home cell? + for (Pile pile : foundations) { + for (Pile tableau : tableaux) { + if (pile.canMoveFrom(tableau)) { + return false; + } + } + for (Pile cell : cells) { + if (pile.canMoveFrom(cell)) { + return false; + } + } + } + return true; + } + + public void newGame() { + Deck deck = Deck.newDeck(); + deck.shuffle(); + moveTracker.clearMoves(); + tableaux.forEach(Pile::clear); + cells.forEach(Pile::clear); + foundations.forEach(Pile::clear); + // Deal 6 cards to each tableau. + for (int i = 0; i < 6; i++) { + for (int j = 0; j < 8; j++) { + Card card = deck.deal(); + card.turn(); + tableaux.get(j).addCard(card); + } + } + // Deal an additional card to first 4. + for (int i = 0; i < 4; i++) { + Card card = deck.deal(); + card.turn(); + tableaux.get(i).addCard(card); + } + } + + private static class MoveTracker { + private final Deque nextMoves = new LinkedList<>(); + private final Deque previousMoves = new LinkedList<>(); + + public void clearMoves() { + nextMoves.clear(); + previousMoves.clear(); + } + + public boolean hasLastMove() { + return !previousMoves.isEmpty(); + } + + public MoveAction getLastMove() { + MoveAction lastMove = previousMoves.pop(); + nextMoves.push(lastMove); + return lastMove; + } + + public boolean hasNextMove() { + return !nextMoves.isEmpty(); + } + + public MoveAction getNextMove() { + MoveAction nextMove = nextMoves.pop(); + previousMoves.push(nextMove); + return nextMove; + } + + public void addMove(MoveAction move) { + MoveAction nextMove = nextMoves.peek(); + /* + * if new move differs from saved next move, clear the remaining + * next moves + */ + if (move.equals(nextMove)) { + nextMoves.pop(); + } else { + nextMoves.clear(); + } + previousMoves.push(move); + } + } +} diff --git a/src/main/java/com/charego/freecellfx/model/action/Action.java b/src/main/java/com/charego/freecellfx/model/action/Action.java new file mode 100644 index 0000000..68c00bb --- /dev/null +++ b/src/main/java/com/charego/freecellfx/model/action/Action.java @@ -0,0 +1,7 @@ +package com.charego.freecellfx.model.action; + +public interface Action { + void redo(); + + void undo(); +} diff --git a/src/main/java/com/charego/freecellfx/model/action/MoveAction.java b/src/main/java/com/charego/freecellfx/model/action/MoveAction.java new file mode 100644 index 0000000..10d3f03 --- /dev/null +++ b/src/main/java/com/charego/freecellfx/model/action/MoveAction.java @@ -0,0 +1,36 @@ +package com.charego.freecellfx.model.action; + +import com.charego.freecellfx.model.pile.Pile; + +public class MoveAction implements Action { + private final Pile fromPile; + private final Pile toPile; + private final int numCards; + + public MoveAction(Pile fromPile, Pile toPile, int numCards) { + this.fromPile = fromPile; + this.toPile = toPile; + this.numCards = numCards; + } + + @Override + public void redo() { + toPile.moveFromBlindly(fromPile, numCards); + } + + @Override + public void undo() { + fromPile.moveFromBlindly(toPile, numCards); + } + + @Override + public boolean equals(Object other) { + if (other == null || !(other instanceof MoveAction)) { + return false; + } + MoveAction action = (MoveAction) other; + return fromPile == action.fromPile + && toPile == action.toPile + && numCards == action.numCards; + } +} diff --git a/src/main/java/com/charego/freecellfx/model/pile/AbstractPile.java b/src/main/java/com/charego/freecellfx/model/pile/AbstractPile.java new file mode 100644 index 0000000..b6e5d23 --- /dev/null +++ b/src/main/java/com/charego/freecellfx/model/pile/AbstractPile.java @@ -0,0 +1,48 @@ +package com.charego.freecellfx.model.pile; + +import com.charego.freecellfx.model.Card; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Iterator; + +abstract class AbstractPile implements Pile { + + private Deque stack = new ArrayDeque<>(); + + @Override + public void addCard(Card card) { + stack.push(card); + } + + @Override + public Card removeCard() { + return stack.pop(); + } + + @Override + public Card topCard() { + return stack.peek(); + } + + @Override + public void clear() { + stack.clear(); + } + + @Override + public boolean isEmpty() { + return stack.isEmpty(); + } + + @Override + public int size() { + return stack.size(); + } + + @Override + public Iterator iterator() { + return stack.descendingIterator(); + } + +} diff --git a/src/main/java/com/charego/freecellfx/model/pile/Cell.java b/src/main/java/com/charego/freecellfx/model/pile/Cell.java new file mode 100644 index 0000000..b188c3c --- /dev/null +++ b/src/main/java/com/charego/freecellfx/model/pile/Cell.java @@ -0,0 +1,32 @@ +package com.charego.freecellfx.model.pile; + +public class Cell extends AbstractPile { + + @Override + public boolean canMoveFrom(Pile other) { + return isEmpty() && !other.isEmpty(); + } + + @Override + public int moveFrom(Pile other) { + if (canMoveFrom(other)) { + addCard(other.removeCard()); + return 1; + } + return 0; + } + + @Override + public void moveFromBlindly(Pile other, int numCards) { + if (numCards != 1) { + throw new IllegalArgumentException("numCards must be 1"); + } + addCard(other.removeCard()); + } + + @Override + public boolean isInOrder() { + return true; + } + +} diff --git a/src/main/java/com/charego/freecellfx/model/pile/Foundation.java b/src/main/java/com/charego/freecellfx/model/pile/Foundation.java new file mode 100644 index 0000000..e1cd3b7 --- /dev/null +++ b/src/main/java/com/charego/freecellfx/model/pile/Foundation.java @@ -0,0 +1,46 @@ +package com.charego.freecellfx.model.pile; + +import static com.charego.freecellfx.model.Card.Rank; +import static com.charego.freecellfx.model.Card.Suit; + +public class Foundation extends AbstractPile { + + @Override + public boolean canMoveFrom(Pile other) { + if (other.isEmpty()) { + return false; + } + Rank otherRank = other.topCard().rank; + Suit otherSuit = other.topCard().suit; + if (isEmpty()) { + return otherRank == Rank.ACE; + } + Rank thisRank = topCard().rank; + Suit thisSuit = topCard().suit; + return otherSuit == thisSuit + && otherRank.value == thisRank.value + 1; + } + + @Override + public int moveFrom(Pile other) { + if (canMoveFrom(other)) { + addCard(other.removeCard()); + return 1; + } + return 0; + } + + @Override + public void moveFromBlindly(Pile other, int numCards) { + if (numCards != 1) { + throw new IllegalArgumentException("numCards must be 1"); + } + addCard(other.removeCard()); + } + + @Override + public boolean isInOrder() { + return true; + } + +} diff --git a/src/main/java/com/charego/freecellfx/model/pile/Pile.java b/src/main/java/com/charego/freecellfx/model/pile/Pile.java new file mode 100644 index 0000000..9d13303 --- /dev/null +++ b/src/main/java/com/charego/freecellfx/model/pile/Pile.java @@ -0,0 +1,59 @@ +package com.charego.freecellfx.model.pile; + +import com.charego.freecellfx.model.Card; + +public interface Pile extends Iterable { + + /** + * Adds a card to the top of the pile. + */ + void addCard(Card card); + + /** + * Removes the top card from the pile. + */ + Card removeCard(); + + /** + * Returns true if the contents of another pile can be moved to this pile. + */ + boolean canMoveFrom(Pile other); + + /** + * Moves cards from another pile to this pile, if the tryMove is valid. + * + * @return the number of cards moved (i.e., possibly 0) + */ + int moveFrom(Pile other); + + /** + * Moves cards from another pile to this pile, whether or not the tryMove is valid. + */ + void moveFromBlindly(Pile other, int numCards); + + /** + * Returns the top card in the pile, if a card is present. + */ + Card topCard(); + + /** + * Returns true if the contents of the pile are in order. + */ + boolean isInOrder(); + + /** + * Clears the contents of the pile. + */ + void clear(); + + /** + * Returns true if the pile is empty. + */ + boolean isEmpty(); + + /** + * Returns the size of the pile. + */ + int size(); + +} diff --git a/src/main/java/com/charego/freecellfx/model/pile/Tableau.java b/src/main/java/com/charego/freecellfx/model/pile/Tableau.java new file mode 100644 index 0000000..8162569 --- /dev/null +++ b/src/main/java/com/charego/freecellfx/model/pile/Tableau.java @@ -0,0 +1,140 @@ +package com.charego.freecellfx.model.pile; + +import com.charego.freecellfx.model.Card; + +import java.util.Deque; +import java.util.LinkedList; + +public class Tableau extends AbstractPile { + + /** + * Maintains the ordering of the cards within the pile. + */ + private Deque orderStack = new LinkedList<>(); + + @Override + public void addCard(Card card) { + countAddedCard(card); + super.addCard(card); + } + + private void countAddedCard(Card card) { + if (isEmpty() || orderStack.isEmpty()) { + orderStack.push(1); + } else if (card.color != topCard().color && + card.rank.value == topCard().rank.value - 1) { + orderStack.push(orderStack.pop() + 1); + } else { + orderStack.push(1); + } + } + + @Override + public Card removeCard() { + countRemovedCard(); + return super.removeCard(); + } + + private void countRemovedCard() { + int topInOrder = orderStack.pop(); + if (topInOrder > 1) { + orderStack.push(topInOrder - 1); + } + } + + @Override + public boolean canMoveFrom(Pile other) { + if (other.isEmpty()) { + return false; + } + if (isEmpty()) { + return true; + } + int rankDiff = topCard().rank.value - other.topCard().rank.value; + if (rankDiff < 1) { + return false; + } + if (other instanceof Foundation || other instanceof Cell) { + return rankDiff == 1 && topCard().color != other.topCard().color; + } + if (other instanceof Tableau) { + int otherTopInOrder = ((Tableau) other).topInOrder(); + if (otherTopInOrder >= rankDiff) { + switch (rankDiff % 2) { + case 0: + return topCard().color == other.topCard().color; + case 1: + return topCard().color != other.topCard().color; + } + } + } + return false; + } + + @Override + public int moveFrom(Pile other) { + if (!canMoveFrom(other)) { + return 0; + } + if (other instanceof Foundation || other instanceof Cell) { + addCard(other.removeCard()); + return 1; + } + if (other instanceof Tableau) { + if (isEmpty()) { + // Move all ordered cards to this pile + int otherTopInOrder = ((Tableau) other).topInOrder(); + moveFrom(other, otherTopInOrder); + return otherTopInOrder; + } else { + // Move ordered cards until they reach the current top card + int rankDiff = topCard().rank.value - other.topCard().rank.value; + moveFrom(other, rankDiff); + return rankDiff; + } + } + throw new IllegalStateException(); + } + + /** + * Moves {@code n} cards from the other pile to this pile. + */ + private void moveFrom(Pile other, int n) { + Deque topCards = new LinkedList<>(); + for (int i = 0; i < n; i++) { + topCards.addLast(other.removeCard()); + } + while (!topCards.isEmpty()) { + addCard(topCards.removeLast()); + } + } + + @Override + public void moveFromBlindly(Pile other, int numCards) { + moveFrom(other, numCards); + } + + @Override + public void clear() { + super.clear(); + orderStack.clear(); + } + + @Override + public boolean isInOrder() { + return (orderStack.size() < 2); + } + + /** + * Returns the number of cards in order at the top of the pile. + */ + public int topInOrder() { + return orderStack.size() == 0 ? 0 : orderStack.peek(); + } + + @Override + public String toString() { + return orderStack.toString(); + } + +} \ No newline at end of file diff --git a/src/main/java/com/charego/freecellfx/util/DoubleKeyedMap.java b/src/main/java/com/charego/freecellfx/util/DoubleKeyedMap.java new file mode 100644 index 0000000..72acc12 --- /dev/null +++ b/src/main/java/com/charego/freecellfx/util/DoubleKeyedMap.java @@ -0,0 +1,34 @@ +package com.charego.freecellfx.util; + +import java.util.HashMap; +import java.util.Map; + +/** + * Maps a pair of coordinates to an object. + * + * @param the first coordinate + * @param the second coordinate + * @param the treasure + */ +public class DoubleKeyedMap { + + private Map> map = new HashMap<>(); + + public boolean contains(K1 firstKey, K2 secondKey) { + return map.containsKey(firstKey) && map.get(firstKey).containsKey(secondKey); + } + + public V get(K1 firstKey, K2 secondKey) { + return map.get(firstKey).get(secondKey); + } + + /** + * Returns the old value if it exists, or null. + */ + public V put(K1 firstKey, K2 secondKey, V newValue) { + if (!map.containsKey(firstKey)) { + map.put(firstKey, new HashMap<>()); + } + return map.get(firstKey).put(secondKey, newValue); + } +} diff --git a/src/main/java/com/charego/freecellfx/view/GameCanvas.java b/src/main/java/com/charego/freecellfx/view/GameCanvas.java new file mode 100644 index 0000000..ab0cdb5 --- /dev/null +++ b/src/main/java/com/charego/freecellfx/view/GameCanvas.java @@ -0,0 +1,169 @@ +package com.charego.freecellfx.view; + +import com.charego.freecellfx.model.Card; +import com.charego.freecellfx.model.Game; +import com.charego.freecellfx.model.pile.Pile; +import com.charego.freecellfx.view.pile.CascadingPileView; +import com.charego.freecellfx.view.pile.PileView; +import com.charego.freecellfx.view.pile.StackedPileView; +import javafx.event.EventHandler; +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.control.Alert; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.input.MouseEvent; + +import java.util.ArrayList; +import java.util.List; + +public class GameCanvas extends Canvas { + + private final Game game; + private final GraphicsContext gc; + private final PileView[] pileViews; + + private PileView fromPile; + private boolean winMessageShown; + + public GameCanvas(Game game, double width, double height) { + super(width, height); + this.game = game; + this.gc = getGraphicsContext2D(); + this.pileViews = constructPileViews(game); + setOnKeyReleased(new KeyReleaseHandler()); + setOnMouseClicked(new MouseClickHandler()); + updateView(); + } + + private static PileView[] constructPileViews(Game game) { + List list = new ArrayList<>(); + for (Pile pile : game.getCells()) { + list.add(new StackedPileView(pile)); + } + for (Pile pile : game.getFoundations()) { + list.add(new StackedPileView(pile)); + } + for (Pile pile : game.getTableaux()) { + list.add(new CascadingPileView(pile)); + } + return list.toArray(new PileView[16]); + } + + private void checkEndingConditions() { + if (winMessageShown) { + return; + } + if (game.isWon()) { + winMessageShown = true; + new Alert(Alert.AlertType.CONFIRMATION, "You won!").showAndWait(); + } else if (game.isLost()) { + new Alert(Alert.AlertType.CONFIRMATION, "You lost!").showAndWait(); + } + } + + private PileView findPile(double x, double y) { + int columnIndex = 0; + x -= 15; + while (x > 90) { + x -= 90; + columnIndex++; + } + boolean columnPressed = x < Card.width; + if (!columnPressed) { + return null; + } + if (y > 15 && y < 15 + Card.height) { + return pileViews[columnIndex]; + } + if (y > 120) { + PileView pileView = pileViews[columnIndex + 8]; + Pile pile = pileView.getPile(); + double pileHeight = 120 + (pile.size() - 1) * CascadingPileView.CARD_MARGIN + Card.height; + if (y < pileHeight) { + return pileView; + } + } + return null; + } + + public void updateView() { + gc.clearRect(0, 0, super.getWidth(), super.getHeight()); + double x = 15; + for (int i = 0; i < 8; i++) { + pileViews[i].paint(gc, x, 15); + x += 90; + } + x = 15; + for (int i = 8; i < 16; i++) { + pileViews[i].paint(gc, x, 120); + x += 90; + } + } + + private class KeyReleaseHandler implements EventHandler { + @Override + public void handle(KeyEvent e) { + if (e.isControlDown()) { + if (e.getCode() == KeyCode.Y) { + game.redo(); + updateView(); + } else if (e.getCode() == KeyCode.Z) { + game.undo(); + updateView(); + } + } else { + if (e.getCode() == KeyCode.F2) { + game.newGame(); + winMessageShown = false; + if (fromPile != null && fromPile.isSelected()) { + fromPile.toggleSelected(); + } + fromPile = null; + updateView(); + } + } + } + } + + private class MouseClickHandler implements EventHandler { + @Override + public void handle(MouseEvent e) { + PileView clickedPile = findPile(e.getX(), e.getY()); + if (clickedPile == null) { + if (fromPile != null) { + fromPile.toggleSelected(); + } + fromPile = null; + return; + } + if (fromPile == null) { + fromPile = clickedPile; + fromPile.toggleSelected(); + updateView(); + return; + } + if (fromPile == clickedPile) { + // Double click: try moving to a foundation. + fromPile.toggleSelected(); + for (int i = 4; i < 8; i++) { + if (game.tryMove(fromPile.getPile(), pileViews[i].getPile())) { + updateView(); + checkEndingConditions(); + break; + } + } + fromPile = null; + } else { + // Try moving to other cell. + fromPile.toggleSelected(); + if (game.tryMove(fromPile.getPile(), clickedPile.getPile())) { + updateView(); + checkEndingConditions(); + } + fromPile = null; + } + } + } + +} diff --git a/src/main/java/com/charego/freecellfx/view/pile/CascadingPileView.java b/src/main/java/com/charego/freecellfx/view/pile/CascadingPileView.java new file mode 100644 index 0000000..6ff19e2 --- /dev/null +++ b/src/main/java/com/charego/freecellfx/view/pile/CascadingPileView.java @@ -0,0 +1,50 @@ +package com.charego.freecellfx.view.pile; + +import com.charego.freecellfx.model.pile.Pile; +import com.charego.freecellfx.model.Card; +import com.charego.freecellfx.model.pile.Tableau; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.paint.Color; + +public class CascadingPileView extends PileView { + + public static final int CARD_MARGIN = 22; + public static final int TOP_MARGIN = 0; + + public CascadingPileView(Pile pile) { + super(pile); + } + + @Override + public void paint(GraphicsContext gc, double x, double y) { + if (pile.isEmpty()) { + // draw an outline + gc.setStroke(Color.YELLOW); + gc.strokeRect(x, y, Card.width, Card.height); + if (isSelected()) { + // highlight empty cell + gc.setFill(highlight); + gc.fillRect(x, y, Card.width, Card.height); + } + } else { + double yCurrent = y; + // draw the cards in a cascade from top to bottom + for (Card card : pile) { + gc.drawImage(card.image(), x, yCurrent); + yCurrent += CARD_MARGIN; + } + if (isSelected()) { + // highlight ordered cards + if (yCurrent > TOP_MARGIN) { + yCurrent -= CARD_MARGIN; + } + int multiplier = ((Tableau) pile).topInOrder(); + int heightOfRect = (multiplier - 1) * CARD_MARGIN; + gc.setFill(highlight); + gc.fillRect(x, yCurrent - heightOfRect, Card.width, heightOfRect + + Card.height); + } + } + } + +} diff --git a/src/main/java/com/charego/freecellfx/view/pile/PileView.java b/src/main/java/com/charego/freecellfx/view/pile/PileView.java new file mode 100644 index 0000000..6da31a1 --- /dev/null +++ b/src/main/java/com/charego/freecellfx/view/pile/PileView.java @@ -0,0 +1,31 @@ +package com.charego.freecellfx.view.pile; + +import com.charego.freecellfx.model.pile.Pile; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.paint.Color; + +public abstract class PileView { + + protected static final Color highlight = new Color(1, 1, 0, 0.6); + protected final Pile pile; + private boolean isSelected = false; + + public PileView(Pile pile) { + this.pile = pile; + } + + public Pile getPile() { + return pile; + } + + public abstract void paint(GraphicsContext gc, double x, double y); + + public boolean isSelected() { + return isSelected; + } + + public void toggleSelected() { + isSelected = !isSelected; + } + +} diff --git a/src/main/java/com/charego/freecellfx/view/pile/StackedPileView.java b/src/main/java/com/charego/freecellfx/view/pile/StackedPileView.java new file mode 100644 index 0000000..24a4d4f --- /dev/null +++ b/src/main/java/com/charego/freecellfx/view/pile/StackedPileView.java @@ -0,0 +1,30 @@ +package com.charego.freecellfx.view.pile; + +import com.charego.freecellfx.model.pile.Pile; +import com.charego.freecellfx.model.Card; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.paint.Color; + +public class StackedPileView extends PileView { + + public StackedPileView(Pile pile) { + super(pile); + } + + @Override + public void paint(GraphicsContext gc, double x, double y) { + if (pile.isEmpty()) { + // draw an outline + gc.setStroke(Color.YELLOW); + gc.strokeRect(x, y, Card.width, Card.height); + } else { + // draw the top card + gc.drawImage(pile.topCard().image(), x, y); + } + if (isSelected()) { + gc.setFill(highlight); + gc.fillRect(x, y, Card.width, Card.height); + } + } + +} diff --git a/src/main/resources/deck/10C.png b/src/main/resources/deck/10C.png new file mode 100644 index 0000000..888cdd6 Binary files /dev/null and b/src/main/resources/deck/10C.png differ diff --git a/src/main/resources/deck/10D.png b/src/main/resources/deck/10D.png new file mode 100644 index 0000000..2dcf08e Binary files /dev/null and b/src/main/resources/deck/10D.png differ diff --git a/src/main/resources/deck/10H.png b/src/main/resources/deck/10H.png new file mode 100644 index 0000000..30f93d2 Binary files /dev/null and b/src/main/resources/deck/10H.png differ diff --git a/src/main/resources/deck/10S.png b/src/main/resources/deck/10S.png new file mode 100644 index 0000000..66ea2f1 Binary files /dev/null and b/src/main/resources/deck/10S.png differ diff --git a/src/main/resources/deck/11C.png b/src/main/resources/deck/11C.png new file mode 100644 index 0000000..70c085b Binary files /dev/null and b/src/main/resources/deck/11C.png differ diff --git a/src/main/resources/deck/11D.png b/src/main/resources/deck/11D.png new file mode 100644 index 0000000..2b98730 Binary files /dev/null and b/src/main/resources/deck/11D.png differ diff --git a/src/main/resources/deck/11H.png b/src/main/resources/deck/11H.png new file mode 100644 index 0000000..99d1ef3 Binary files /dev/null and b/src/main/resources/deck/11H.png differ diff --git a/src/main/resources/deck/11S.png b/src/main/resources/deck/11S.png new file mode 100644 index 0000000..ca08035 Binary files /dev/null and b/src/main/resources/deck/11S.png differ diff --git a/src/main/resources/deck/12C.png b/src/main/resources/deck/12C.png new file mode 100644 index 0000000..fd9d3b5 Binary files /dev/null and b/src/main/resources/deck/12C.png differ diff --git a/src/main/resources/deck/12D.png b/src/main/resources/deck/12D.png new file mode 100644 index 0000000..465d962 Binary files /dev/null and b/src/main/resources/deck/12D.png differ diff --git a/src/main/resources/deck/12H.png b/src/main/resources/deck/12H.png new file mode 100644 index 0000000..f0f7d79 Binary files /dev/null and b/src/main/resources/deck/12H.png differ diff --git a/src/main/resources/deck/12S.png b/src/main/resources/deck/12S.png new file mode 100644 index 0000000..f8d5deb Binary files /dev/null and b/src/main/resources/deck/12S.png differ diff --git a/src/main/resources/deck/13C.png b/src/main/resources/deck/13C.png new file mode 100644 index 0000000..cf1f723 Binary files /dev/null and b/src/main/resources/deck/13C.png differ diff --git a/src/main/resources/deck/13D.png b/src/main/resources/deck/13D.png new file mode 100644 index 0000000..17ba2c2 Binary files /dev/null and b/src/main/resources/deck/13D.png differ diff --git a/src/main/resources/deck/13H.png b/src/main/resources/deck/13H.png new file mode 100644 index 0000000..7274ece Binary files /dev/null and b/src/main/resources/deck/13H.png differ diff --git a/src/main/resources/deck/13S.png b/src/main/resources/deck/13S.png new file mode 100644 index 0000000..3072ea3 Binary files /dev/null and b/src/main/resources/deck/13S.png differ diff --git a/src/main/resources/deck/1C.png b/src/main/resources/deck/1C.png new file mode 100644 index 0000000..3ff8aa6 Binary files /dev/null and b/src/main/resources/deck/1C.png differ diff --git a/src/main/resources/deck/1D.png b/src/main/resources/deck/1D.png new file mode 100644 index 0000000..d98a7c4 Binary files /dev/null and b/src/main/resources/deck/1D.png differ diff --git a/src/main/resources/deck/1H.png b/src/main/resources/deck/1H.png new file mode 100644 index 0000000..534cf09 Binary files /dev/null and b/src/main/resources/deck/1H.png differ diff --git a/src/main/resources/deck/1S.png b/src/main/resources/deck/1S.png new file mode 100644 index 0000000..536a3be Binary files /dev/null and b/src/main/resources/deck/1S.png differ diff --git a/src/main/resources/deck/2C.png b/src/main/resources/deck/2C.png new file mode 100644 index 0000000..c46843a Binary files /dev/null and b/src/main/resources/deck/2C.png differ diff --git a/src/main/resources/deck/2D.png b/src/main/resources/deck/2D.png new file mode 100644 index 0000000..8278642 Binary files /dev/null and b/src/main/resources/deck/2D.png differ diff --git a/src/main/resources/deck/2H.png b/src/main/resources/deck/2H.png new file mode 100644 index 0000000..ed95e56 Binary files /dev/null and b/src/main/resources/deck/2H.png differ diff --git a/src/main/resources/deck/2S.png b/src/main/resources/deck/2S.png new file mode 100644 index 0000000..ae7ba22 Binary files /dev/null and b/src/main/resources/deck/2S.png differ diff --git a/src/main/resources/deck/3C.png b/src/main/resources/deck/3C.png new file mode 100644 index 0000000..d9435b4 Binary files /dev/null and b/src/main/resources/deck/3C.png differ diff --git a/src/main/resources/deck/3D.png b/src/main/resources/deck/3D.png new file mode 100644 index 0000000..5132549 Binary files /dev/null and b/src/main/resources/deck/3D.png differ diff --git a/src/main/resources/deck/3H.png b/src/main/resources/deck/3H.png new file mode 100644 index 0000000..72a3bda Binary files /dev/null and b/src/main/resources/deck/3H.png differ diff --git a/src/main/resources/deck/3S.png b/src/main/resources/deck/3S.png new file mode 100644 index 0000000..c8d38f0 Binary files /dev/null and b/src/main/resources/deck/3S.png differ diff --git a/src/main/resources/deck/4C.png b/src/main/resources/deck/4C.png new file mode 100644 index 0000000..cc72540 Binary files /dev/null and b/src/main/resources/deck/4C.png differ diff --git a/src/main/resources/deck/4D.png b/src/main/resources/deck/4D.png new file mode 100644 index 0000000..dd2418b Binary files /dev/null and b/src/main/resources/deck/4D.png differ diff --git a/src/main/resources/deck/4H.png b/src/main/resources/deck/4H.png new file mode 100644 index 0000000..30d06de Binary files /dev/null and b/src/main/resources/deck/4H.png differ diff --git a/src/main/resources/deck/4S.png b/src/main/resources/deck/4S.png new file mode 100644 index 0000000..2c93a21 Binary files /dev/null and b/src/main/resources/deck/4S.png differ diff --git a/src/main/resources/deck/5C.png b/src/main/resources/deck/5C.png new file mode 100644 index 0000000..451ac6a Binary files /dev/null and b/src/main/resources/deck/5C.png differ diff --git a/src/main/resources/deck/5D.png b/src/main/resources/deck/5D.png new file mode 100644 index 0000000..4bf1de9 Binary files /dev/null and b/src/main/resources/deck/5D.png differ diff --git a/src/main/resources/deck/5H.png b/src/main/resources/deck/5H.png new file mode 100644 index 0000000..a8a4236 Binary files /dev/null and b/src/main/resources/deck/5H.png differ diff --git a/src/main/resources/deck/5S.png b/src/main/resources/deck/5S.png new file mode 100644 index 0000000..5cf3bcf Binary files /dev/null and b/src/main/resources/deck/5S.png differ diff --git a/src/main/resources/deck/6C.png b/src/main/resources/deck/6C.png new file mode 100644 index 0000000..9293ed3 Binary files /dev/null and b/src/main/resources/deck/6C.png differ diff --git a/src/main/resources/deck/6D.png b/src/main/resources/deck/6D.png new file mode 100644 index 0000000..b21a3ee Binary files /dev/null and b/src/main/resources/deck/6D.png differ diff --git a/src/main/resources/deck/6H.png b/src/main/resources/deck/6H.png new file mode 100644 index 0000000..5a26cbb Binary files /dev/null and b/src/main/resources/deck/6H.png differ diff --git a/src/main/resources/deck/6S.png b/src/main/resources/deck/6S.png new file mode 100644 index 0000000..edc65b5 Binary files /dev/null and b/src/main/resources/deck/6S.png differ diff --git a/src/main/resources/deck/7C.png b/src/main/resources/deck/7C.png new file mode 100644 index 0000000..aca12f2 Binary files /dev/null and b/src/main/resources/deck/7C.png differ diff --git a/src/main/resources/deck/7D.png b/src/main/resources/deck/7D.png new file mode 100644 index 0000000..8d97797 Binary files /dev/null and b/src/main/resources/deck/7D.png differ diff --git a/src/main/resources/deck/7H.png b/src/main/resources/deck/7H.png new file mode 100644 index 0000000..1a99572 Binary files /dev/null and b/src/main/resources/deck/7H.png differ diff --git a/src/main/resources/deck/7S.png b/src/main/resources/deck/7S.png new file mode 100644 index 0000000..ee77ee9 Binary files /dev/null and b/src/main/resources/deck/7S.png differ diff --git a/src/main/resources/deck/8C.png b/src/main/resources/deck/8C.png new file mode 100644 index 0000000..539f327 Binary files /dev/null and b/src/main/resources/deck/8C.png differ diff --git a/src/main/resources/deck/8D.png b/src/main/resources/deck/8D.png new file mode 100644 index 0000000..48dbe15 Binary files /dev/null and b/src/main/resources/deck/8D.png differ diff --git a/src/main/resources/deck/8H.png b/src/main/resources/deck/8H.png new file mode 100644 index 0000000..6e274b2 Binary files /dev/null and b/src/main/resources/deck/8H.png differ diff --git a/src/main/resources/deck/8S.png b/src/main/resources/deck/8S.png new file mode 100644 index 0000000..b7446cb Binary files /dev/null and b/src/main/resources/deck/8S.png differ diff --git a/src/main/resources/deck/9C.png b/src/main/resources/deck/9C.png new file mode 100644 index 0000000..2228cec Binary files /dev/null and b/src/main/resources/deck/9C.png differ diff --git a/src/main/resources/deck/9D.png b/src/main/resources/deck/9D.png new file mode 100644 index 0000000..d5833b2 Binary files /dev/null and b/src/main/resources/deck/9D.png differ diff --git a/src/main/resources/deck/9H.png b/src/main/resources/deck/9H.png new file mode 100644 index 0000000..f649c8d Binary files /dev/null and b/src/main/resources/deck/9H.png differ diff --git a/src/main/resources/deck/9S.png b/src/main/resources/deck/9S.png new file mode 100644 index 0000000..273a1fe Binary files /dev/null and b/src/main/resources/deck/9S.png differ diff --git a/src/main/resources/deck/CARDBACK.png b/src/main/resources/deck/CARDBACK.png new file mode 100644 index 0000000..c116591 Binary files /dev/null and b/src/main/resources/deck/CARDBACK.png differ diff --git a/src/main/resources/deck/FELT.jpg b/src/main/resources/deck/FELT.jpg new file mode 100644 index 0000000..e274f1f Binary files /dev/null and b/src/main/resources/deck/FELT.jpg differ diff --git a/src/main/resources/icons/DIAMOND.jpg b/src/main/resources/icons/DIAMOND.jpg new file mode 100644 index 0000000..64b1337 Binary files /dev/null and b/src/main/resources/icons/DIAMOND.jpg differ diff --git a/src/main/resources/icons/LOST.png b/src/main/resources/icons/LOST.png new file mode 100644 index 0000000..445702c Binary files /dev/null and b/src/main/resources/icons/LOST.png differ diff --git a/src/main/resources/icons/WON.png b/src/main/resources/icons/WON.png new file mode 100644 index 0000000..e71398f Binary files /dev/null and b/src/main/resources/icons/WON.png differ