Initial commit
11
.gitignore
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
# Maven
|
||||
target/
|
||||
|
||||
# Eclipse
|
||||
.settings/
|
||||
*.classpath
|
||||
*.project
|
||||
|
||||
# IntelliJ IDEA
|
||||
*.idea
|
||||
*.iml
|
39
pom.xml
Normal file
@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.charego</groupId>
|
||||
<artifactId>freecellfx</artifactId>
|
||||
<version>0.1</version>
|
||||
|
||||
<organization>
|
||||
<name>charego.com</name>
|
||||
</organization>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.3</version>
|
||||
<configuration>
|
||||
<source>1.8</source>
|
||||
<target>1.8</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>com.zenjava</groupId>
|
||||
<artifactId>javafx-maven-plugin</artifactId>
|
||||
<version>8.1.2</version>
|
||||
<configuration>
|
||||
<mainClass>com.charego.freecellfx.FreeCellApplication</mainClass>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
@ -0,0 +1,36 @@
|
||||
package com.charego.freecellfx;
|
||||
|
||||
import com.charego.freecellfx.model.Game;
|
||||
import com.charego.freecellfx.view.GameCanvas;
|
||||
import javafx.application.Application;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
public class FreeCellApplication extends Application {
|
||||
|
||||
public static void main(String[] args) {
|
||||
launch(FreeCellApplication.class, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Stage stage) throws Exception {
|
||||
AnchorPane root = new AnchorPane();
|
||||
root.setBackground(new Background(new BackgroundImage(new Image(
|
||||
"/deck/FELT.jpg"), BackgroundRepeat.NO_REPEAT,
|
||||
BackgroundRepeat.NO_REPEAT, BackgroundPosition.CENTER,
|
||||
BackgroundSize.DEFAULT)));
|
||||
|
||||
GameCanvas canvas = new GameCanvas(new Game(), 731, 600);
|
||||
root.getChildren().add(canvas);
|
||||
Scene scene = new Scene(root);
|
||||
|
||||
stage.setTitle("FreeCell");
|
||||
stage.getIcons().add(new Image("/icons/DIAMOND.jpg"));
|
||||
stage.setScene(scene);
|
||||
stage.setResizable(false);
|
||||
stage.show();
|
||||
}
|
||||
|
||||
}
|
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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -0,0 +1,169 @@
|
||||
package com.charego.freecellfx.view;
|
||||
|
||||
import com.charego.freecellfx.model.Card;
|
||||
import com.charego.freecellfx.model.Game;
|
||||
import com.charego.freecellfx.model.pile.Pile;
|
||||
import com.charego.freecellfx.view.pile.CascadingPileView;
|
||||
import com.charego.freecellfx.view.pile.PileView;
|
||||
import com.charego.freecellfx.view.pile.StackedPileView;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.canvas.Canvas;
|
||||
import javafx.scene.canvas.GraphicsContext;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class GameCanvas extends Canvas {
|
||||
|
||||
private final Game game;
|
||||
private final GraphicsContext gc;
|
||||
private final PileView[] pileViews;
|
||||
|
||||
private PileView fromPile;
|
||||
private boolean winMessageShown;
|
||||
|
||||
public GameCanvas(Game game, double width, double height) {
|
||||
super(width, height);
|
||||
this.game = game;
|
||||
this.gc = getGraphicsContext2D();
|
||||
this.pileViews = constructPileViews(game);
|
||||
setOnKeyReleased(new KeyReleaseHandler());
|
||||
setOnMouseClicked(new MouseClickHandler());
|
||||
updateView();
|
||||
}
|
||||
|
||||
private static PileView[] constructPileViews(Game game) {
|
||||
List<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;
|
||||
}
|
||||
}
|
||||
|
||||
private class KeyReleaseHandler implements EventHandler<KeyEvent> {
|
||||
@Override
|
||||
public void handle(KeyEvent e) {
|
||||
if (e.isControlDown()) {
|
||||
if (e.getCode() == KeyCode.Y) {
|
||||
game.redo();
|
||||
updateView();
|
||||
} else if (e.getCode() == KeyCode.Z) {
|
||||
game.undo();
|
||||
updateView();
|
||||
}
|
||||
} else {
|
||||
if (e.getCode() == KeyCode.F2) {
|
||||
game.newGame();
|
||||
winMessageShown = false;
|
||||
if (fromPile != null && fromPile.isSelected()) {
|
||||
fromPile.toggleSelected();
|
||||
}
|
||||
fromPile = null;
|
||||
updateView();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class MouseClickHandler implements EventHandler<MouseEvent> {
|
||||
@Override
|
||||
public void handle(MouseEvent e) {
|
||||
PileView clickedPile = findPile(e.getX(), e.getY());
|
||||
if (clickedPile == null) {
|
||||
if (fromPile != null) {
|
||||
fromPile.toggleSelected();
|
||||
}
|
||||
fromPile = null;
|
||||
return;
|
||||
}
|
||||
if (fromPile == null) {
|
||||
fromPile = clickedPile;
|
||||
fromPile.toggleSelected();
|
||||
updateView();
|
||||
return;
|
||||
}
|
||||
if (fromPile == clickedPile) {
|
||||
// Double click: try moving to a foundation.
|
||||
fromPile.toggleSelected();
|
||||
for (int i = 4; i < 8; i++) {
|
||||
if (game.tryMove(fromPile.getPile(), pileViews[i].getPile())) {
|
||||
updateView();
|
||||
checkEndingConditions();
|
||||
break;
|
||||
}
|
||||
}
|
||||
fromPile = null;
|
||||
} else {
|
||||
// Try moving to other cell.
|
||||
fromPile.toggleSelected();
|
||||
if (game.tryMove(fromPile.getPile(), clickedPile.getPile())) {
|
||||
updateView();
|
||||
checkEndingConditions();
|
||||
}
|
||||
fromPile = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
BIN
src/main/resources/deck/10C.png
Normal file
After Width: | Height: | Size: 623 B |
BIN
src/main/resources/deck/10D.png
Normal file
After Width: | Height: | Size: 554 B |
BIN
src/main/resources/deck/10H.png
Normal file
After Width: | Height: | Size: 630 B |
BIN
src/main/resources/deck/10S.png
Normal file
After Width: | Height: | Size: 596 B |
BIN
src/main/resources/deck/11C.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
src/main/resources/deck/11D.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
src/main/resources/deck/11H.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
src/main/resources/deck/11S.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
src/main/resources/deck/12C.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
src/main/resources/deck/12D.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
src/main/resources/deck/12H.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
src/main/resources/deck/12S.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
src/main/resources/deck/13C.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
src/main/resources/deck/13D.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
src/main/resources/deck/13H.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
src/main/resources/deck/13S.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
src/main/resources/deck/1C.png
Normal file
After Width: | Height: | Size: 440 B |
BIN
src/main/resources/deck/1D.png
Normal file
After Width: | Height: | Size: 388 B |
BIN
src/main/resources/deck/1H.png
Normal file
After Width: | Height: | Size: 453 B |
BIN
src/main/resources/deck/1S.png
Normal file
After Width: | Height: | Size: 583 B |
BIN
src/main/resources/deck/2C.png
Normal file
After Width: | Height: | Size: 457 B |
BIN
src/main/resources/deck/2D.png
Normal file
After Width: | Height: | Size: 411 B |
BIN
src/main/resources/deck/2H.png
Normal file
After Width: | Height: | Size: 474 B |
BIN
src/main/resources/deck/2S.png
Normal file
After Width: | Height: | Size: 463 B |
BIN
src/main/resources/deck/3C.png
Normal file
After Width: | Height: | Size: 505 B |
BIN
src/main/resources/deck/3D.png
Normal file
After Width: | Height: | Size: 440 B |
BIN
src/main/resources/deck/3H.png
Normal file
After Width: | Height: | Size: 508 B |
BIN
src/main/resources/deck/3S.png
Normal file
After Width: | Height: | Size: 511 B |
BIN
src/main/resources/deck/4C.png
Normal file
After Width: | Height: | Size: 474 B |
BIN
src/main/resources/deck/4D.png
Normal file
After Width: | Height: | Size: 435 B |
BIN
src/main/resources/deck/4H.png
Normal file
After Width: | Height: | Size: 494 B |
BIN
src/main/resources/deck/4S.png
Normal file
After Width: | Height: | Size: 486 B |
BIN
src/main/resources/deck/5C.png
Normal file
After Width: | Height: | Size: 553 B |
BIN
src/main/resources/deck/5D.png
Normal file
After Width: | Height: | Size: 478 B |
BIN
src/main/resources/deck/5H.png
Normal file
After Width: | Height: | Size: 578 B |
BIN
src/main/resources/deck/5S.png
Normal file
After Width: | Height: | Size: 558 B |
BIN
src/main/resources/deck/6C.png
Normal file
After Width: | Height: | Size: 510 B |
BIN
src/main/resources/deck/6D.png
Normal file
After Width: | Height: | Size: 463 B |
BIN
src/main/resources/deck/6H.png
Normal file
After Width: | Height: | Size: 531 B |
BIN
src/main/resources/deck/6S.png
Normal file
After Width: | Height: | Size: 520 B |
BIN
src/main/resources/deck/7C.png
Normal file
After Width: | Height: | Size: 610 B |
BIN
src/main/resources/deck/7D.png
Normal file
After Width: | Height: | Size: 513 B |
BIN
src/main/resources/deck/7H.png
Normal file
After Width: | Height: | Size: 606 B |
BIN
src/main/resources/deck/7S.png
Normal file
After Width: | Height: | Size: 609 B |
BIN
src/main/resources/deck/8C.png
Normal file
After Width: | Height: | Size: 612 B |
BIN
src/main/resources/deck/8D.png
Normal file
After Width: | Height: | Size: 525 B |
BIN
src/main/resources/deck/8H.png
Normal file
After Width: | Height: | Size: 625 B |
BIN
src/main/resources/deck/8S.png
Normal file
After Width: | Height: | Size: 618 B |
BIN
src/main/resources/deck/9C.png
Normal file
After Width: | Height: | Size: 679 B |
BIN
src/main/resources/deck/9D.png
Normal file
After Width: | Height: | Size: 533 B |
BIN
src/main/resources/deck/9H.png
Normal file
After Width: | Height: | Size: 634 B |
BIN
src/main/resources/deck/9S.png
Normal file
After Width: | Height: | Size: 608 B |
BIN
src/main/resources/deck/CARDBACK.png
Normal file
After Width: | Height: | Size: 239 B |
BIN
src/main/resources/deck/FELT.jpg
Normal file
After Width: | Height: | Size: 184 KiB |
BIN
src/main/resources/icons/DIAMOND.jpg
Normal file
After Width: | Height: | Size: 8.2 KiB |
BIN
src/main/resources/icons/LOST.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
src/main/resources/icons/WON.png
Normal file
After Width: | Height: | Size: 1.8 KiB |