Compare commits
No commits in common. "5cad85e04928ab21ac8f57c15861d8ca95551f8d" and "c8e84b8a792d4ff85b2d633aa1b3d4d6bb736761" have entirely different histories.
5cad85e049
...
c8e84b8a79
@ -1,6 +1,6 @@
|
|||||||
MIT License
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2020 Charles Gould
|
Copyright (c) 2015 Charles Gould
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
@ -19,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
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
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
|
|
@ -1,14 +1,12 @@
|
|||||||
FreeCell implementation using the JavaFX Canvas API ([screenshot](freecell.png))
|
FreeCell implementation using the JavaFX Canvas API.
|
||||||
|
|
||||||
Prerequisites:
|
Prerequisites:
|
||||||
|
- Java 8+ installed
|
||||||
- Java 11+ installed
|
|
||||||
- Maven 3+ installed
|
- Maven 3+ installed
|
||||||
|
|
||||||
To start a game, run the command `mvn javafx:run`
|
To start a game, in the root folder run the command `mvn jfx:run`.
|
||||||
|
|
||||||
Controls:
|
Controls:
|
||||||
|
|
||||||
- F2 to restart
|
- F2 to restart
|
||||||
- Ctrl-X to undo
|
- Ctrl-X to undo
|
||||||
- Ctrl-Y to redo
|
- Ctrl-Y to redo
|
||||||
|
BIN
freecell.png
BIN
freecell.png
Binary file not shown.
Before Width: | Height: | Size: 653 KiB |
47
pom.xml
47
pom.xml
@ -4,64 +4,33 @@
|
|||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
<groupId>com.charego</groupId>
|
<groupId>com.charego</groupId>
|
||||||
<artifactId>freecell</artifactId>
|
<artifactId>freecellfx</artifactId>
|
||||||
<version>0.2</version>
|
<version>0.1</version>
|
||||||
|
|
||||||
<organization>
|
<organization>
|
||||||
<name>charego.com</name>
|
<name>charego.com</name>
|
||||||
</organization>
|
</organization>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<javafx.version>11.0.2</javafx.version>
|
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.openjfx</groupId>
|
|
||||||
<artifactId>javafx-controls</artifactId>
|
|
||||||
<version>${javafx.version}</version>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
<plugin>
|
<plugin>
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
<version>3.8.1</version>
|
<version>3.3</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<release>11</release>
|
<source>1.8</source>
|
||||||
|
<target>1.8</target>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>com.zenjava</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>
|
<artifactId>javafx-maven-plugin</artifactId>
|
||||||
<version>0.0.4</version>
|
<version>8.1.2</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<mainClass>com.charego.freecell.FreeCellApplication</mainClass>
|
<mainClass>com.charego.freecellfx.FreeCellApplication</mainClass>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,103 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,150 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
package com.charego.freecell.model.action;
|
|
||||||
|
|
||||||
public interface Action {
|
|
||||||
void redo();
|
|
||||||
|
|
||||||
void undo();
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,48 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,60 +0,0 @@
|
|||||||
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();
|
|
||||||
|
|
||||||
}
|
|
@ -1,139 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,169 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,54 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,49 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,43 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
104
src/main/java/com/charego/freecellfx/model/Card.java
Normal file
104
src/main/java/com/charego/freecellfx/model/Card.java
Normal file
@ -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<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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
53
src/main/java/com/charego/freecellfx/model/Deck.java
Normal file
53
src/main/java/com/charego/freecellfx/model/Deck.java
Normal file
@ -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<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();
|
||||||
|
}
|
||||||
|
}
|
195
src/main/java/com/charego/freecellfx/model/Game.java
Normal file
195
src/main/java/com/charego/freecellfx/model/Game.java
Normal file
@ -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<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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package com.charego.freecellfx.model.action;
|
||||||
|
|
||||||
|
public interface Action {
|
||||||
|
void redo();
|
||||||
|
|
||||||
|
void undo();
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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<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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
32
src/main/java/com/charego/freecellfx/model/pile/Cell.java
Normal file
32
src/main/java/com/charego/freecellfx/model/pile/Cell.java
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
59
src/main/java/com/charego/freecellfx/model/pile/Pile.java
Normal file
59
src/main/java/com/charego/freecellfx/model/pile/Pile.java
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
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();
|
||||||
|
|
||||||
|
}
|
140
src/main/java/com/charego/freecellfx/model/pile/Tableau.java
Normal file
140
src/main/java/com/charego/freecellfx/model/pile/Tableau.java
Normal file
@ -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<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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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 <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);
|
||||||
|
}
|
||||||
|
}
|
169
src/main/java/com/charego/freecellfx/view/GameCanvas.java
Normal file
169
src/main/java/com/charego/freecellfx/view/GameCanvas.java
Normal file
@ -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.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
54
src/main/java/com/charego/freecellfx/view/GameMenuBar.java
Normal file
54
src/main/java/com/charego/freecellfx/view/GameMenuBar.java
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
31
src/main/java/com/charego/freecellfx/view/pile/PileView.java
Normal file
31
src/main/java/com/charego/freecellfx/view/pile/PileView.java
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,5 +0,0 @@
|
|||||||
module com.charego.freecell {
|
|
||||||
requires javafx.controls;
|
|
||||||
|
|
||||||
exports com.charego.freecell to javafx.graphics;
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user