Compare commits

..

10 Commits

Author SHA1 Message Date
5cad85e049 Add license (MIT) 2024-01-04 00:27:36 -06:00
5c9f828916 Update plugins 2020-06-03 01:22:43 -05:00
f2f0151dfb Add openjfx maven plugin 2019-03-12 23:24:53 -04:00
809ca45b75 Remove fx from project name 2019-02-20 09:06:56 -05:00
1aaa1ea5e3 Upgrade to Java/JavaFX 11 2019-01-25 10:45:38 -05:00
5153077104 Add screenshot 2019-01-25 10:45:38 -05:00
65b27a7335 Make MoveTracker its own class 2019-01-25 10:45:35 -05:00
43bf9f9adf Format using tabs 2019-01-25 10:45:29 -05:00
1b0d06f20c Remove LICENSE.md 2019-01-25 10:45:08 -05:00
cfe68c0889 Fix win/loss alert type 2015-04-20 00:26:01 -04:00
40 changed files with 1190 additions and 1144 deletions

View File

@ -1,6 +1,6 @@
The MIT License (MIT)
MIT License
Copyright (c) 2015 Charles Gould
Copyright (c) 2020 Charles Gould
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -19,3 +19,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,12 +1,14 @@
FreeCell implementation using the JavaFX Canvas API.
FreeCell implementation using the JavaFX Canvas API ([screenshot](freecell.png))
Prerequisites:
- Java 8+ installed
- Java 11+ installed
- Maven 3+ installed
To start a game, in the root folder run the command `mvn jfx:run`.
To start a game, run the command `mvn javafx:run`
Controls:
- F2 to restart
- Ctrl-X to undo
- Ctrl-Y to redo

BIN
freecell.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 KiB

47
pom.xml
View File

@ -4,33 +4,64 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.charego</groupId>
<artifactId>freecellfx</artifactId>
<version>0.1</version>
<artifactId>freecell</artifactId>
<version>0.2</version>
<organization>
<name>charego.com</name>
</organization>
<properties>
<javafx.version>11.0.2</javafx.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.3</version>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<release>11</release>
</configuration>
</plugin>
<plugin>
<groupId>com.zenjava</groupId>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.0.0-M3</version>
<executions>
<execution>
<id>enforce-versions</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<requireJavaVersion>
<version>11.0</version>
</requireJavaVersion>
<requireMavenVersion>
<version>3.5.0</version>
</requireMavenVersion>
</rules>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>8.1.2</version>
<version>0.0.4</version>
<configuration>
<mainClass>com.charego.freecellfx.FreeCellApplication</mainClass>
<mainClass>com.charego.freecell.FreeCellApplication</mainClass>
</configuration>
</plugin>
</plugins>

View File

@ -0,0 +1,41 @@
package com.charego.freecell;
import com.charego.freecell.model.Game;
import com.charego.freecell.view.GameCanvas;
import com.charego.freecell.view.GameMenuBar;
import javafx.application.Application;
import javafx.application.Platform;
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 {
VBox root = new VBox();
root.setBackground(new Background(new BackgroundImage(new Image("/deck/FELT.jpg"), BackgroundRepeat.NO_REPEAT,
BackgroundRepeat.NO_REPEAT, BackgroundPosition.CENTER, BackgroundSize.DEFAULT)));
Game game = new Game();
GameMenuBar menuBar = new GameMenuBar();
GameCanvas canvas = new GameCanvas(game, 731, 600);
menuBar.setNewGameAction(canvas.getNewGameAction());
menuBar.setUndoAction(canvas.getUndoAction());
menuBar.setRedoAction(canvas.getRedoAction());
menuBar.setExitAction(e -> Platform.exit());
root.getChildren().addAll(menuBar, canvas);
Scene scene = new Scene(root);
stage.setTitle("FreeCell");
stage.setScene(scene);
stage.setResizable(false);
stage.show();
}
}

View File

@ -0,0 +1,103 @@
package com.charego.freecell.model;
import com.charego.freecell.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<Rank, Suit, Image> 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;
}
}
}

View File

@ -0,0 +1,53 @@
package com.charego.freecell.model;
import com.charego.freecell.model.Card.Rank;
import com.charego.freecell.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<Card> 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<Card>) cards);
}
@Override
public String toString() {
return cards.toString();
}
}

View File

@ -0,0 +1,150 @@
package com.charego.freecell.model;
import java.util.ArrayList;
import java.util.List;
import com.charego.freecell.model.action.MoveAction;
import com.charego.freecell.model.pile.Cell;
import com.charego.freecell.model.pile.Foundation;
import com.charego.freecell.model.pile.Pile;
import com.charego.freecell.model.pile.Tableau;
public class Game {
private List<Cell> cells = new ArrayList<>(4);
private List<Foundation> foundations = new ArrayList<>(4);
private List<Tableau> 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<Cell> getCells() {
return cells;
}
public List<Foundation> getFoundations() {
return foundations;
}
public List<Tableau> 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);
}
}
}

View File

@ -0,0 +1,56 @@
package com.charego.freecell.model;
import java.util.Deque;
import java.util.LinkedList;
import com.charego.freecell.model.action.MoveAction;
/**
* Records moves to enable redo and undo actions.
*/
public class MoveTracker {
private final Deque<MoveAction> nextMoves = new LinkedList<>();
private final Deque<MoveAction> 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 there is a saved next move and it differs from the player's move,
* clear the remaining next moves because the paths have diverged.
*/
if (move.equals(nextMove)) {
nextMoves.pop();
} else {
nextMoves.clear();
}
previousMoves.push(move);
}
}

View File

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

View File

@ -0,0 +1,34 @@
package com.charego.freecell.model.action;
import com.charego.freecell.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;
}
}

View File

@ -0,0 +1,48 @@
package com.charego.freecell.model.pile;
import com.charego.freecell.model.Card;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Iterator;
abstract class AbstractPile implements Pile {
private Deque<Card> 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<Card> iterator() {
return stack.descendingIterator();
}
}

View File

@ -0,0 +1,32 @@
package com.charego.freecell.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;
}
}

View File

@ -0,0 +1,45 @@
package com.charego.freecell.model.pile;
import static com.charego.freecell.model.Card.Rank;
import static com.charego.freecell.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;
}
}

View File

@ -0,0 +1,60 @@
package com.charego.freecell.model.pile;
import com.charego.freecell.model.Card;
public interface Pile extends Iterable<Card> {
/**
* 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();
}

View File

@ -0,0 +1,139 @@
package com.charego.freecell.model.pile;
import com.charego.freecell.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<Integer> 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<Card> 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();
}
}

View File

@ -0,0 +1,37 @@
package com.charego.freecell.util;
import java.util.HashMap;
import java.util.Map;
/**
* Maps a pair of coordinates to an object.
*
* @param <K1>
* the first coordinate
* @param <K2>
* the second coordinate
* @param <V>
* the treasure
*/
public class DoubleKeyedMap<K1, K2, V> {
private Map<K1, Map<K2, V>> 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);
}
}

View File

@ -0,0 +1,169 @@
package com.charego.freecell.view;
import com.charego.freecell.model.Card;
import com.charego.freecell.model.Game;
import com.charego.freecell.model.pile.Pile;
import com.charego.freecell.view.pile.CascadingPileView;
import com.charego.freecell.view.pile.PileView;
import com.charego.freecell.view.pile.StackedPileView;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Alert;
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);
setOnMouseClicked(new MouseClickHandler());
updateView();
}
private static PileView[] constructPileViews(Game game) {
List<PileView> 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.INFORMATION, "You won!").showAndWait();
} else if (game.isLost()) {
new Alert(Alert.AlertType.INFORMATION, "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;
}
}
public EventHandler<ActionEvent> getNewGameAction() {
return e -> {
game.newGame();
winMessageShown = false;
if (fromPile != null && fromPile.isSelected()) {
fromPile.toggleSelected();
}
fromPile = null;
updateView();
};
}
public EventHandler<ActionEvent> getUndoAction() {
return e -> {
game.undo();
updateView();
};
}
public EventHandler<ActionEvent> getRedoAction() {
return e -> {
game.redo();
updateView();
};
}
private class MouseClickHandler implements EventHandler<MouseEvent> {
@Override
public void handle(MouseEvent e) {
PileView clickedPile = findPile(e.getX(), e.getY());
if (clickedPile == null) {
if (fromPile != null) {
fromPile.toggleSelected();
}
fromPile = null;
updateView();
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())) {
checkEndingConditions();
break;
}
}
fromPile = null;
updateView();
} else {
// Try moving to other cell.
fromPile.toggleSelected();
if (game.tryMove(fromPile.getPile(), clickedPile.getPile())) {
checkEndingConditions();
}
fromPile = null;
updateView();
}
}
}
}

View File

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

View File

@ -0,0 +1,49 @@
package com.charego.freecell.view.pile;
import com.charego.freecell.model.pile.Pile;
import com.charego.freecell.model.Card;
import com.charego.freecell.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);
}
}
}
}

View File

@ -0,0 +1,31 @@
package com.charego.freecell.view.pile;
import com.charego.freecell.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;
}
}

View File

@ -0,0 +1,30 @@
package com.charego.freecell.view.pile;
import com.charego.freecell.model.pile.Pile;
import com.charego.freecell.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);
}
}
}

View File

@ -1,43 +0,0 @@
package com.charego.freecellfx;
import com.charego.freecellfx.model.Game;
import com.charego.freecellfx.view.GameCanvas;
import com.charego.freecellfx.view.GameMenuBar;
import javafx.application.Application;
import javafx.application.Platform;
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 {
VBox root = new VBox();
root.setBackground(new Background(new BackgroundImage(new Image(
"/deck/FELT.jpg"), BackgroundRepeat.NO_REPEAT,
BackgroundRepeat.NO_REPEAT, BackgroundPosition.CENTER,
BackgroundSize.DEFAULT)));
Game game = new Game();
GameMenuBar menuBar = new GameMenuBar();
GameCanvas canvas = new GameCanvas(game, 731, 600);
menuBar.setNewGameAction(canvas.getNewGameAction());
menuBar.setUndoAction(canvas.getUndoAction());
menuBar.setRedoAction(canvas.getRedoAction());
menuBar.setExitAction(e -> Platform.exit());
root.getChildren().addAll(menuBar, canvas);
Scene scene = new Scene(root);
stage.setTitle("FreeCell");
stage.setScene(scene);
stage.setResizable(false);
stage.show();
}
}

View File

@ -1,104 +0,0 @@
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<Rank, Suit, Image> 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;
}
}
}

View File

@ -1,53 +0,0 @@
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<Card> 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<Card>) cards);
}
@Override
public String toString() {
return cards.toString();
}
}

View File

@ -1,195 +0,0 @@
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<Cell> cells = new ArrayList<>(4);
private List<Foundation> foundations = new ArrayList<>(4);
private List<Tableau> 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<Cell> getCells() {
return cells;
}
public List<Foundation> getFoundations() {
return foundations;
}
public List<Tableau> 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<MoveAction> nextMoves = new LinkedList<>();
private final Deque<MoveAction> 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);
}
}
}

View File

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

View File

@ -1,36 +0,0 @@
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;
}
}

View File

@ -1,48 +0,0 @@
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<Card> 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<Card> iterator() {
return stack.descendingIterator();
}
}

View File

@ -1,32 +0,0 @@
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;
}
}

View File

@ -1,46 +0,0 @@
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;
}
}

View File

@ -1,59 +0,0 @@
package com.charego.freecellfx.model.pile;
import com.charego.freecellfx.model.Card;
public interface Pile extends Iterable<Card> {
/**
* 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();
}

View File

@ -1,140 +0,0 @@
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<Integer> 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<Card> 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();
}
}

View File

@ -1,34 +0,0 @@
package com.charego.freecellfx.util;
import java.util.HashMap;
import java.util.Map;
/**
* Maps a pair of coordinates to an object.
*
* @param <K1> the first coordinate
* @param <K2> the second coordinate
* @param <V> the treasure
*/
public class DoubleKeyedMap<K1, K2, V> {
private Map<K1, Map<K2, V>> 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);
}
}

View File

@ -1,169 +0,0 @@
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.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Alert;
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);
setOnMouseClicked(new MouseClickHandler());
updateView();
}
private static PileView[] constructPileViews(Game game) {
List<PileView> 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;
}
}
public EventHandler<ActionEvent> getNewGameAction() {
return e -> {
game.newGame();
winMessageShown = false;
if (fromPile != null && fromPile.isSelected()) {
fromPile.toggleSelected();
}
fromPile = null;
updateView();
};
}
public EventHandler<ActionEvent> getUndoAction() {
return e -> {
game.undo();
updateView();
};
}
public EventHandler<ActionEvent> getRedoAction() {
return e -> {
game.redo();
updateView();
};
}
private class MouseClickHandler implements EventHandler<MouseEvent> {
@Override
public void handle(MouseEvent e) {
PileView clickedPile = findPile(e.getX(), e.getY());
if (clickedPile == null) {
if (fromPile != null) {
fromPile.toggleSelected();
}
fromPile = null;
updateView();
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())) {
checkEndingConditions();
break;
}
}
fromPile = null;
updateView();
} else {
// Try moving to other cell.
fromPile.toggleSelected();
if (game.tryMove(fromPile.getPile(), clickedPile.getPile())) {
checkEndingConditions();
}
fromPile = null;
updateView();
}
}
}
}

View File

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

View File

@ -1,50 +0,0 @@
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);
}
}
}
}

View File

@ -1,31 +0,0 @@
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;
}
}

View File

@ -1,30 +0,0 @@
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);
}
}
}

View File

@ -0,0 +1,5 @@
module com.charego.freecell {
requires javafx.controls;
exports com.charego.freecell to javafx.graphics;
}