diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..01586d6 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "files.exclude": { + "**/.classpath": true, + "**/.project": true, + "**/.settings": true, + "**/.factorypath": true + } +} \ No newline at end of file diff --git a/README.md b/README.md index 2073a25..6767ec6 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,19 @@ # java small java projects + +## Freecell Solver +### Compile +```shell +mkdir build && javac common/*.java freecell/*.java -d build +``` +### Run +```shell +pushd build && java freecell.FreecellSolution --deal 1 && popd +``` +### JAR File +```shell +pushd build +jar cvfe freecell-solver.jar freecell.FreecellSolution common/*.class freecell/*.class +popd +java -jar build/freecell-solver.jar --deal 1 +``` diff --git a/common/ByteStack.java b/common/ByteStack.java new file mode 100644 index 0000000..4fff72c --- /dev/null +++ b/common/ByteStack.java @@ -0,0 +1,150 @@ +package common; + +import java.util.Arrays; + +public final class ByteStack implements Comparable { + private int _size; + private int _available; + private byte _data[]; + + public ByteStack(final byte data[], final int size) { + _data = data; + _available = _data.length; + _size = Math.min(Math.max(size, 0), _available); + } + + public ByteStack(final byte data[]) { + this(data, 0); + } + + /** + * Creates an empty Stack. + */ + public ByteStack() { + this(new byte[64]); + } + + /** + * Tests if this stack is empty. + * + * @return {@code true} if the stack is empty; {@code false} otherwise. + */ + public boolean isEmpty() { + return _size == 0; + } + + /** + * Looks at the top of this stack without removing it from the stack. + * + * @return the top of the stack + */ + public byte peek() { + return _data[_size - 1]; + } + + /** + * Removes the top of this stack and returns it as the value of this function. + * + * @return the top of the stack + */ + public byte pop() { + return _data[--_size]; + } + + /** + * Pushes an item onto the top of this stack. + * + * @param value an item to push. + */ + public void push(final byte value) { + if (_size == _available) { + _available *= 2; + _data = Arrays.copyOf(_data, _available); + } + _data[_size++] = value; + } + + public void push(final ByteStack values) { + final int S = values._size; + if (_size + S > _available) { + _available += Math.max(_available, S); + _data = Arrays.copyOf(_data, _available); + } + final byte D[] = values._data; + for (int i = 0; i < S; i++) { + _data[_size++] = D[i]; + } + } + + /** + * Removes all of the elements from the stack. It will be empty after this call returns. + */ + public void clear() { + _size = 0; + } + + /** + * Returns the element at the specified position in the stack. + * @param index - index of the element to return + * @return the element at the specified index + */ + public byte get(final int index) { + return _data[index]; + } + + /** + * Returns an array containing all of the elements in this stack. + * @return an array + */ + public byte[] toArray() { + return Arrays.copyOf(_data, _size); + } + + public int size() { + return _size; + } + + public int available() { + return _available; + } + + /** + * Returns a string representation of this stack. + * @return a string + */ + @Override + public String toString() { + return new String(_data, 0, _size); + } + + @Override + public int compareTo(ByteStack other) { + // Since: 9 + // int cmp = Byte.compare(_data, 0, _size, other._data, 0, other._size); + if (_size != other._size) { + return _size - other._size; + } + for (int i = 0; i < _size; i++) { + if (_data[i] != other._data[i]) { + return _data[i] - other._data[i]; + } + } + return 0; + } + + public static void main(String[] args) { + var stack = new ByteStack(); + for (byte b = 32; b < 127; b++) { + stack.push(b); + } + + System.out.println("ASCII Printables: " + stack); + + var reversal = new ByteStack(); + while (stack.size() > 0) { + reversal.push(stack.pop()); + } + + System.out.println("In opposite direction: " + reversal); + } +} diff --git a/common/Deck.java b/common/Deck.java new file mode 100644 index 0000000..9eaa208 --- /dev/null +++ b/common/Deck.java @@ -0,0 +1,111 @@ +package common; + +public class Deck { + // Standard 52-card deck + public static final byte + CARD_NUM = 52, // SUIT_NUM * RANK_NUM + SUIT_NUM = 4, + RANK_NUM = 13; + + public static final char + SUITS[] = { 'S', 'D', 'C', 'H' }, + RANKS[] = { 'A', '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K' }; + + public static final int + COLOR_POS = 5, + COLOR_FLAG = 1 << COLOR_POS, + RANK_MASK = COLOR_FLAG - 1; + + public static byte cardOf(int index) { + return (byte)('A' + ((index >>> 1) | ((index & 1) << COLOR_POS))); + } + + // Returns 0 for blacks (spades and clubs) and 1 for reds (diamonds and hearts) + public static int colorOf(byte card) { + return ((card & COLOR_FLAG) >>> COLOR_POS); + } + + public static boolean isBlack(byte card) { + return colorOf(card) == 0; + } + + public static boolean isRed(byte card) { + return !isBlack(card); + } + + // Card index is defined as: suit + rank * SUIT_NUM + public static int indexOf(int s, int r) { + return s + r * SUIT_NUM; + } + + // Returns the rank of a card. + // Cards are ranked, from 0 to 12: A, 2, 3, 4, 5, 6, 7, 8, 9, T, J, Q and K. + public static int rankOf(byte card) { + return (((card - 1) & RANK_MASK) >>> 1); + } + + // Returns the suit of a card. The order of suits: Spades, Diamonds, Clubs and Hearts. + public static int suitOf(byte card) { + return (((card - 1) & 1) << 1) | colorOf(card); + } + + public static boolean isTableau(byte cardA, byte cardB) { + return (rankOf(cardA) == rankOf(cardB) + 1 && colorOf(cardA) != colorOf(cardB)); + } + + // Returns a set of optionally shuffled playing cards. + public static byte[] deal(long seed) { + byte cards[] = new byte[CARD_NUM]; + for (int i = 0; i < CARD_NUM; i++) { + cards[i] = cardOf(i); + } + + if (seed >= 0) { + // System.out.println("SEED: " + seed); + // use LCG algorithm to pick up cards from the deck + // http://en.wikipedia.org/wiki/Linear_congruential_generator + final double m = 0x80000000L; + final double a = 1103515245L; + final double c = 12345L; + + for (int i = 0; i < CARD_NUM; i++) { + seed = (long) ((a * seed + c) % m); + + // swap cards + final int j = (int)(seed % CARD_NUM); + if (i != j) { + final byte card = cards[i]; + cards[i] = cards[j]; + cards[j] = card; + } + } + } + + return cards; + } + + public static String toString(byte card) { + int suit = suitOf(card); + int rank = rankOf(card); + boolean black = isBlack(card); + + return "Card #" + indexOf(suit, rank) + " " + RANKS[rank] + SUITS[suit] + (black ? " black" : " red"); + } + + public static String toString(byte[] cards) { + var buf = new StringBuilder(); + for (byte card : cards) { + buf.append(toString(card)).append('\n'); + } + return buf.toString(); + } + + public static void main(String[] args) { + var cards = deal(-1); + System.out.println(toString(cards)); + + var str = new String(cards); + System.out.println("As string:"); + System.out.println(str); + } +} diff --git a/common/IntStack.java b/common/IntStack.java new file mode 100644 index 0000000..0c528b0 --- /dev/null +++ b/common/IntStack.java @@ -0,0 +1,116 @@ +package common; + +import java.util.Arrays; + +public final class IntStack { + private int _size; + private int _available; + private int _data[]; + + public IntStack(final int data[], final int size) { + _data = data; + _available = _data.length; + _size = Math.min(Math.max(size, 0), _available); + } + + public IntStack(final int data[]) { + this(data, 0); + } + + /** + * Creates an empty Stack. + */ + public IntStack() { + this(new int[16]); + } + + /** + * Tests if this stack is empty. + * + * @return {@code true} if the stack is empty; {@code false} otherwise. + */ + public boolean isEmpty() { + return _size == 0; + } + + /** + * Looks at the top of this stack without removing it from the stack. + * + * @return the top of the stack + */ + public int peek() { + return _data[_size - 1]; + } + + /** + * Removes the top of this stack and returns it as the value of this function. + * + * @return the top of the stack + */ + public int pop() { + return _data[--_size]; + } + + /** + * Pushes an item onto the top of this stack. + * + * @param value an item to push. + */ + public void push(final int value) { + if (_size == _available) { + _available *= 2; + _data = Arrays.copyOf(_data, _available); + } + _data[_size++] = value; + } + + /** + * Removes all of the elements from the stack. It will be empty after this call returns. + */ + public void clear() { + _size = 0; + } + + /** + * Returns the element at the specified position in the stack. + * @param index - index of the element to return + * @return the element at the specified index + */ + public int get(final int index) { + return _data[index]; + } + + /** + * Returns an array containing all of the elements in this stack. + * @return an array + */ + public int[] toArray() { + return Arrays.copyOf(_data, _size); + } + + public int size() { + return _size; + } + + public int available() { + return _available; + } + + /** + * Returns a string representation of this stack. + * @return a string + */ + @Override + public String toString() { + final var buf = new StringBuilder(); + var prefix = ""; + buf.append('['); + for (int i = 0; i < _size; i++) { + buf.append(prefix); + buf.append(Integer.toString(_data[i])); + prefix = ", "; + } + buf.append(']'); + return buf.toString(); + } +} \ No newline at end of file diff --git a/freecell-solver-test.csv b/freecell-solver-test.csv new file mode 100644 index 0000000..17f7aeb --- /dev/null +++ b/freecell-solver-test.csv @@ -0,0 +1,10 @@ +1,85,151c186d60106e6f6b1bfb1f184140484bd85d535bf51f313616e1313e3535393a4a73767b707a795ac90a2727272c2928590958085b5a0b1a79197a0a79e8286954580e010bc8db3b48040a096a4a1a6b68e949fb +2,83,481c106d64d46d69676b17195e5036303f396956597935363a1a707303747909e92e292aea7a7b2b75272e28585b68586b121838785801050608d80d070a0ada1a4a497948494d4bfb3bcb5b6a4a4b0b2bdae8 +3,94,5925202c2529292d7e7b7813101fd2621d191218616b63f65f56f65f56595ad902434d34d4f43d7f727a37d73d3949393a3b3858ca6a1a01454909074c494b570502090b0a6b4a2b7a606b1a6768f808282b78287b2b7828387bca5beada +4,82,56503c3d3b5e5a5b0b5f353b5be515181b3e3901030bdb2b2b656b3b252d262ada1a0405040af07d7370707f79171969606a6818280879093a5a4a41525a4a434b4a3a48084968e8f809291959d9185868c9 +5,89,28614c4b3434396d696b6965c6161c1e1f14151818385158737020d17d79e959f9c71c2e242b1b2f2851525b5858454b1b28eb3b38010e02e2120131010e0b0afacada5aea4a3a49294a2a2b6a7b7c780b4a4a49196918c819 +6,98,536c7d7e72d262e7676d656b4e46c64c40104f4af41f12171b1914c15c515a59587879c9d91c193d31363a30387858d12d252656d62d2429252b2a5b7b7878ca0c090105090b286b59490509070aca6a1afb6b1b6a1a3c386b6ad82848597b4bc8ea +7,89,651c1d1e101a1b18da6d6f6b2141462621d4e12d2b2e29d272707d7bfb7f07e70e0b5b0250c2513c313b343a3878faea5a0e0bcb6c6b6f690979194a545258781848d9191a49194acae8281829092a29384b0b09f8284c4aca +8,83,3856561c1d19c95c56585b1b020e050f08181ad10d05e575762e24747b20303525252a6a3a3b4b636a6b4a4b0b42616b61693938e9191a595a0a61681819595ac818384c4b2b4a4b2e28dbe8787959f95aca59 +9,91,7475756c6d65696bc976797c27c72c25232b13d71d13231e194f4247f20f0949c94c4248181851541451596952d25d525a3a3bea3a4a353a393e3b5b1a50584b7a373138080b282b2a6a78187b2b2a0a0978297b0a0bd8c9e828fb +10,87,7c7d787151515e575b5804203435252625297919d13d3f3b3a1beb1e1b3563623213121419f91f1a7aea6e676a3a6368e8581838595a4a010309590e0a454a46494b7b7849db5b1a2bca4a486bf8283b0b0929e92c28c8 diff --git a/freecell/FreecellBasis.java b/freecell/FreecellBasis.java new file mode 100644 index 0000000..0f66431 --- /dev/null +++ b/freecell/FreecellBasis.java @@ -0,0 +1,76 @@ +package freecell; + +public class FreecellBasis { + public final int + DESK_SIZE, + PILE_START, PILE_NUM, PILE_END, + BASE_START, BASE_NUM, BASE_END, + CELL_START, CELL_NUM, CELL_END; + + FreecellBasis( + final int PILE_NUM, // cascades + final int CELL_NUM, // open cells + final int BASE_NUM // foundation piles + ) { + this.PILE_NUM = PILE_NUM; + this.CELL_NUM = CELL_NUM; + this.BASE_NUM = BASE_NUM; + this.DESK_SIZE = PILE_NUM + CELL_NUM + BASE_NUM; + + this.PILE_START = 0; + this.PILE_END = this.BASE_START = this.PILE_START + this.PILE_NUM; + this.BASE_END = this.CELL_START = this.BASE_START + this.BASE_NUM; + this.CELL_END = this.CELL_START + this.CELL_NUM; + } + + boolean isPile(final int index) { + return index >= PILE_START && index < PILE_END; + } + + boolean isBase(final int index) { + return index >= BASE_START && index < BASE_END; + } + + boolean isCell(final int index) { + return index >= CELL_START && index < CELL_END; + } + + int toMove(final int giver, final int taker) { + return giver + taker * DESK_SIZE; + } + + int toGiver(final int move) { + return move % DESK_SIZE; + } + + int toTaker(final int move) { + return move / DESK_SIZE; + } + + String getSpotName(final int index) { + if (isBase(index)) { + return "base " + (index - BASE_START); + } + if (isPile(index)) { + return "pile " + (index - PILE_START); + } + if (isCell(index)) { + return "cell " + (index - CELL_START); + } + return "unknown " + index; + } + + public static void main(String[] args) { + FreecellBasis basis = new FreecellBasis(8, 4, 4); + + System.out.println("Basis Test"); + System.out.println("DESK_SIZE: " + basis.DESK_SIZE); + System.out.println("BASE_NUM: " + basis.BASE_NUM); + System.out.println("CELL_NUM: " + basis.CELL_NUM); + System.out.println("PILE_NUM: " + basis.PILE_NUM); + + for (int i = 0; i < basis.DESK_SIZE; i++) { + System.out.println("SPOT #" + i + "\n\t" + basis.getSpotName(i)); + } + } +} diff --git a/freecell/FreecellDesk.java b/freecell/FreecellDesk.java new file mode 100644 index 0000000..4f5f069 --- /dev/null +++ b/freecell/FreecellDesk.java @@ -0,0 +1,150 @@ +package freecell; + +import java.util.Arrays; + +import common.ByteStack; +import common.Deck; + +public class FreecellDesk extends FreecellBasis { + private ByteStack _key = new ByteStack(); + private final ByteStack _buffer[]; + protected final ByteStack _desk[]; + + public FreecellDesk(final int PILE_NUM, final int CELL_NUM, final int BASE_NUM) { + super(PILE_NUM, CELL_NUM, BASE_NUM); + + _buffer = new ByteStack[PILE_NUM]; + _desk = new ByteStack[DESK_SIZE]; + for (int i = 0; i < DESK_SIZE; i++) { + _desk[i] = new ByteStack(); + } + } + + public int countEmptyCells() { + int count = 0; + for (int i = CELL_START; i < CELL_END; i++) { + if (_desk[i].isEmpty()) { + count++; + } + } + return count; + } + + public int countEmptyPiles() { + int count = 0; + for (int i = PILE_START; i < PILE_END; i++) { + if (_desk[i].isEmpty()) { + count++; + } + } + return count; + } + + public boolean isSolved() { + for (int i = BASE_START; i < BASE_END; i++) { + if (_desk[i].size() < Deck.RANK_NUM) { + return false; + } + } + return true; + } + + public int countSolved() { + int count = 0; + for (int i = BASE_START; i < BASE_END; i++) { + count += _desk[i].size(); + } + return count; + } + + public int countEmpty() { + return countEmptyCells() + countEmptyPiles(); + } + + private static final byte BASES[] = { '_', 'A', '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K' }; + + public void baseToKey(ByteStack key) { + for (int i = BASE_START; i < BASE_END; i++) { + key.push(BASES[_desk[i].size()]); + } + } + + public void pileToKey(ByteStack key) { + int size = 0; + for (int i = PILE_START; i < PILE_END; i++) { + if (!_desk[i].isEmpty()) { + _buffer[size++] = _desk[i]; + } + } + if (size > 0) { + Arrays.sort(_buffer, 0, size); + for (int i = 0; i < size; i++) { + key.push(_buffer[i]); + key.push((byte)','); + } + key.pop(); + } + } + + public void toKey(ByteStack key) { + key.clear(); + baseToKey(key); + pileToKey(key); + } + + public String toKey() { + toKey(_key); + return _key.toString(); + } + + int getEmptyCell() { + for (int i = CELL_START; i < CELL_END; i++) { + if (_desk[i].isEmpty()) { + return i; + } + } + return -1; + } + + int getEmptyPile() { + for (int i = PILE_START; i < PILE_END; i++) { + if (_desk[i].isEmpty()) { + return i; + } + } + return -1; + } + + int getBase(final byte card) { + final int suit = Deck.suitOf(card); + final int rank = Deck.rankOf(card); + + for (int i = BASE_START + suit; i < BASE_END; i += Deck.SUIT_NUM) { + if (_desk[i].size() == rank) { + return i; + } + } + return -1; + } + + @Override + public String toString() { + final var buf = new StringBuilder(); + var prefix = ""; + for (int i = 0; i < DESK_SIZE; i++) { + buf.append(prefix); + buf.append(lineToString(_desk[i])); + prefix = ","; + } + return buf.toString(); + } + + public static String lineToString(final ByteStack line) { + final var buf = new StringBuilder(); + for (int i = 0; i < line.size(); i++) { + buf.append(Deck.RANKS[Deck.rankOf(line.get(i))]); + buf.append(Deck.SUITS[Deck.suitOf(line.get(i))]); + } + return buf.toString(); + } +} diff --git a/freecell/FreecellGame.java b/freecell/FreecellGame.java new file mode 100644 index 0000000..1e8cbdc --- /dev/null +++ b/freecell/FreecellGame.java @@ -0,0 +1,430 @@ +package freecell; + +import common.Deck; +import common.IntStack; + +public class FreecellGame extends FreecellDesk { + protected IntStack _path = new IntStack(); + + public FreecellGame(final int PILE_NUM, final int CELL_NUM, final int BASE_NUM) { + super(PILE_NUM, CELL_NUM, BASE_NUM); + } + + /** + * Clears the game. + */ + public void clear() { + for (int i = DESK_SIZE; i-- > 0;) { + _desk[i].clear(); + } + _path.clear(); + } + + /** + * Pops the last card from the `source` line and add it to the `destination` + * + * @param giver source line + * @param taker destination line + */ + public void moveCard(final int giver, final int taker) { + _path.push(toMove(giver, taker)); + _desk[taker].push(_desk[giver].pop()); + } + + public void moveCard(final int move) { + _path.push(move); + _desk[toTaker(move)].push(_desk[toGiver(move)].pop()); + } + + public void forward(final int[] path) { + for (final int move : path) { + _path.push(move); + // move source => destination + _desk[toTaker(move)].push(_desk[toGiver(move)].pop()); + } + } + + public void backward(final int mark) { + while (_path.size() > mark) { + final int move = _path.pop(); + // move destination => source + _desk[toGiver(move)].push(_desk[toTaker(move)].pop()); + } + } + + public boolean isMoveForward(final int move) { + return _path.isEmpty() || _path.peek() != toMove(toTaker(move), toGiver(move)); + } + + public int moveCardsToBases() { + int count = 0; + for (boolean next = true; next;) { + next = false; + for (int giver = 0; giver < DESK_SIZE; giver++) { + if (isBase(giver) || _desk[giver].isEmpty()) { + continue; + } + final int taker = getBase(_desk[giver].peek()); + if (taker >= 0) { + moveCard(giver, taker); + count++; + next = true; + } + } + } + return count; + } + + public int moveCardsAuto() { + final int[] ranks = getBaseMinRanks(); + for (int giver = 0; giver < DESK_SIZE; giver++) { + if (isBase(giver) || _desk[giver].isEmpty()) { + continue; + } + final byte card = _desk[giver].peek(); + if (Deck.rankOf(card) <= ranks[Deck.colorOf(card)] + 1) { + final int taker = getBase(card); + if (taker >= 0 ) { + moveCard(giver, taker); + return 1 + moveCardsAuto(); + } + } + } + return 0; + } + + public int getBaseMinRank() { + int rank = _desk[BASE_START].size(); + for (int i = 1; i < BASE_NUM; i++) { + rank = Math.min(rank, _desk[BASE_START + i].size()); + } + return rank; + } + + public boolean canMoveToCell() { + final int taker = getEmptyCell(); + if (taker >= 0) { + for (int giver = PILE_START; giver < PILE_END; giver++) { + if (!_desk[giver].isEmpty() && isMoveForward(toMove(giver, taker))) { + return true; + } + } + } + return false; + } + + public void getMovesToCell(final IntStack moves) { + final int taker = getEmptyCell(); + if (taker >= 0) { + for (int giver = PILE_START; giver < PILE_END; giver++) { + if (!_desk[giver].isEmpty()) { + final int move = toMove(giver, taker); + if (isMoveForward(move)) { + moves.push(move); + } + } + } + } + } + + public boolean canMoveToPile() { + final int taker = getEmptyPile(); + if (taker >= 0) { + // 1. Test piles: + for (int giver = PILE_START; giver < PILE_END; giver++) { + if (_desk[giver].size() > 1 && isMoveForward(toMove(giver, taker))) { + return true; + } + } + // 2. Test cells: + for (int giver = CELL_START; giver < CELL_END; giver++) { + if (_desk[giver].size() > 0 && isMoveForward(toMove(giver, taker))) { + return true; + } + } + } + return false; + } + + public void getMovesToPile(final IntStack moves) { + final int taker = getEmptyPile(); + if (taker >= 0) { + // 1. Test piles: + for (int giver = PILE_START; giver < PILE_END; giver++) { + if (_desk[giver].size() > 1) { + final int move = toMove(giver, taker); + if (isMoveForward(move)) { + moves.push(move); + } + } + } + // 2. Test cells: + for (int giver = CELL_START; giver < CELL_END; giver++) { + if (_desk[giver].size() > 0) { + final int move = toMove(giver, taker); + if (isMoveForward(move)) { + moves.push(move); + } + } + } + } + } + + public boolean canMoveToBase() { + // 1. Test piles: + for (int giver = PILE_START; giver < PILE_END; giver++) { + if (!_desk[giver].isEmpty()) { + final byte card = _desk[giver].peek(); + final int suit = Deck.suitOf(card); + final int rank = Deck.rankOf(card); + for (int taker = BASE_START + suit; taker < BASE_END; taker += Deck.SUIT_NUM) { + if (_desk[taker].size() == rank) { + if (isMoveForward(toMove(giver, taker))) { + return true; + } + } + } + } + } + + // 2. Test cells: + for (int giver = CELL_START; giver < CELL_END; giver++) { + if (!_desk[giver].isEmpty()) { + final byte card = _desk[giver].peek(); + final int suit = Deck.suitOf(card); + final int rank = Deck.rankOf(card); + for (int taker = BASE_START + suit; taker < BASE_END; taker += Deck.SUIT_NUM) { + if (_desk[taker].size() == rank) { + if (isMoveForward(toMove(giver, taker))) { + return true; + } + } + } + } + } + + return false; + } + + public void getMovesToBase(IntStack moves) { + // 1. Test piles: + for (int giver = PILE_START; giver < PILE_END; giver++) { + if (!_desk[giver].isEmpty()) { + final byte card = _desk[giver].peek(); + final int suit = Deck.suitOf(card); + final int rank = Deck.rankOf(card); + for (int taker = BASE_START + suit; taker < BASE_END; taker += Deck.SUIT_NUM) { + if (_desk[taker].size() == rank) { + final int move = toMove(giver, taker); + if (isMoveForward(move)) { + moves.push(move); + } + } + } + } + } + + // 2. Test cells: + for (int giver = CELL_START; giver < CELL_END; giver++) { + if (!_desk[giver].isEmpty()) { + final byte card = _desk[giver].peek(); + final int suit = Deck.suitOf(card); + final int rank = Deck.rankOf(card); + for (int taker = BASE_START + suit; taker < BASE_END; taker += Deck.SUIT_NUM) { + if (_desk[taker].size() == rank) { + final int move = toMove(giver, taker); + if (isMoveForward(move)) { + moves.push(move); + } + } + } + } + } + } + + // Get opposite color bases minimal ranks. + public int[] getBaseMinRanks() { + final int ranks[] = { _desk[BASE_START + 1].size(), _desk[BASE_START].size() }; + for (int i = 2; i < BASE_NUM;) { + ranks[1] = Math.min(ranks[1], _desk[BASE_START + i++].size()); + ranks[0] = Math.min(ranks[0], _desk[BASE_START + i++].size()); + } + return ranks; + } + + public boolean canMoveToTableau() { + // 1. Test piles: + for (int giver = PILE_START; giver < PILE_END; giver++) { + if (!_desk[giver].isEmpty()) { + final byte card = _desk[giver].peek(); + for (int taker = PILE_START; taker < PILE_END; taker++) { + if (giver != taker && !_desk[taker].isEmpty() && Deck.isTableau(_desk[taker].peek(), card)) { + if (isMoveForward(toMove(giver, taker))) { + return true; + } + } + } + } + } + + // 2. Test cells: + for (int giver = CELL_START; giver < CELL_END; giver++) { + if (!_desk[giver].isEmpty()) { + final byte card = _desk[giver].peek(); + for (int taker = PILE_START; taker < PILE_END; taker++) { + if (!_desk[taker].isEmpty() && Deck.isTableau(_desk[taker].peek(), card)) { + if (isMoveForward(toMove(giver, taker))) { + return true; + } + } + } + } + } + + // 3. Test bases: + // We can take cards from bases only to form a tableau. + + // Get opposite color bases minimal ranks. + final int ranks[] = getBaseMinRanks(); + + for (int giver = BASE_START; giver < BASE_END; giver++) { + if (!_desk[giver].isEmpty()) { + final byte card = _desk[giver].peek(); + final int rank = Deck.rankOf(card); + final int color = Deck.colorOf(card); + + if (rank > ranks[color] + 1) { + for (int taker = PILE_START; taker < PILE_END; taker++) { + if (!_desk[taker].isEmpty() && Deck.isTableau(_desk[taker].peek(), card)) { + if (isMoveForward(toMove(giver, taker))) { + return true; + } + } + } + } + } + } + + return false; + } + + public void getMovesToTableau(IntStack moves) { + // 1. Test piles: + for (int giver = PILE_START; giver < PILE_END; giver++) { + if (!_desk[giver].isEmpty()) { + final byte card = _desk[giver].peek(); + for (int taker = PILE_START; taker < PILE_END; taker++) { + if (giver != taker && !_desk[taker].isEmpty() && Deck.isTableau(_desk[taker].peek(), card)) { + final int move = toMove(giver, taker); + if (isMoveForward(move)) { + moves.push(move); + } + } + } + } + } + + // 2. Test cells: + for (int giver = CELL_START; giver < CELL_END; giver++) { + if (!_desk[giver].isEmpty()) { + final byte card = _desk[giver].peek(); + for (int taker = PILE_START; taker < PILE_END; taker++) { + if (!_desk[taker].isEmpty() && Deck.isTableau(_desk[taker].peek(), card)) { + final int move = toMove(giver, taker); + if (isMoveForward(move)) { + moves.push(move); + } + } + } + } + } + + // 3. Test bases: + // We can take cards from bases only to form a tableau. + + // Get opposite color bases minimal ranks. + final int ranks[] = getBaseMinRanks(); + + for (int giver = BASE_START; giver < BASE_END; giver++) { + if (!_desk[giver].isEmpty()) { + final byte card = _desk[giver].peek(); + final int rank = Deck.rankOf(card); + final int color = Deck.colorOf(card); + + if (rank > ranks[color] + 1) { + for (int taker = PILE_START; taker < PILE_END; taker++) { + if (!_desk[taker].isEmpty() && Deck.isTableau(_desk[taker].peek(), card)) { + final int move = toMove(giver, taker); + if (isMoveForward(move)) { + moves.push(move); + } + } + } + } + } + } + } + + public boolean hasNextMove() { + return canMoveToCell() || canMoveToPile() || canMoveToBase() || canMoveToTableau(); + } + + public void getMoves(final IntStack moves) { + moves.clear(); + getMovesToBase(moves); + getMovesToTableau(moves); + getMovesToCell(moves); + getMovesToPile(moves); + } + + public void rewind() { + backward(0); + } + + /** + * Makes a new deal. + * + * @param seed seed number + */ + byte[] deal(final int seed) { + final var cards = Deck.deal(seed); + + clear(); + for (int i = 0; i < cards.length; i++) { + _desk[PILE_START + (i % PILE_NUM)].push(cards[i]); + } + return cards; + } + + public static void main(String[] args) { + FreecellGame game = new FreecellGame(8, 4, 4); + final int seed = 417; + game.deal(seed); + + System.out.println("Deal: " + seed); + System.out.println("Game: " + game); + int count = game.moveCardsAuto(); + System.out.println("Auto: " + count); + System.out.println("Game: " + game); + System.out.println("Key: " + game.toKey()); + + System.out.println("Rewinding..."); + game.rewind(); + System.out.println("Game: " + game); + IntStack moves = new IntStack(); + count = 0; + while (true) { + moves.clear(); + game.getMovesToBase(moves); + if (moves.isEmpty()) { + break; + } else { + game.moveCard(moves.get(0)); + count++; + System.out.println("Move: " + count); + System.out.println("Key: " + game.toKey()); + } + } + System.out.println("Game: " + game); + } +} diff --git a/freecell/FreecellSolution.java b/freecell/FreecellSolution.java new file mode 100644 index 0000000..343221e --- /dev/null +++ b/freecell/FreecellSolution.java @@ -0,0 +1,70 @@ +package freecell; + +public class FreecellSolution { + public static class Commands { + public static final String + d = "-d", + deal = "--deal", + s = "-s", + split = "--split"; + } + + public static void main(String[] args) { + if (args.length < 1 || (args.length & 1) == 1) { + usage(); + return; + } + + final var game = new FreecellSolver(); + game.debug = false; + int deal = -1; + + for (int i = 0; i < args.length; i+=2) { + String command = args[i]; + try { + int value = Integer.decode(args[i + 1]); + + if (command.equalsIgnoreCase(Commands.d) + || command.equalsIgnoreCase(Commands.deal)) { + deal = value; + game.deal(deal); + } else if (command.equalsIgnoreCase(Commands.s) + || command.equalsIgnoreCase(Commands.split)) { + game.setInputSize(value); + } + } catch (NumberFormatException ex) { + System.err.println("Couldn't parse integer from '" + args[i + 1] + + "' for command '" + command + "'"); + usage(); + return; + } + } + + if (deal < 0) { + System.err.println("Deal number should be > 0."); + usage(); + return; + } + + var path = game.solve(); + if (path != null) { + System.out.println("" + deal + ',' + path.length + ',' + game.pathToString(path)); + System.exit(0); + } else { + System.err.println("Unsolved ;-("); + System.exit(1); + } + } + + public static void usage() { + System.out.println("Usage:"); + System.out.println("\t" + Commands.d + ", " + Commands.deal + + "\t\tdeal number (>= 0)"); + System.out.println("\t" + Commands.s + ", " + Commands.split + + "\t\tinput split size [" + + FreecellSolver.INPUT_MIN + ", " + + FreecellSolver.INPUT_MAX + "]" + ); + System.exit(-1); + } +} diff --git a/freecell/FreecellSolver.java b/freecell/FreecellSolver.java new file mode 100644 index 0000000..e47aea1 --- /dev/null +++ b/freecell/FreecellSolver.java @@ -0,0 +1,249 @@ +package freecell; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; + +import common.Deck; +import common.IntStack; + +public final class FreecellSolver extends FreecellGame { + public static final int + TOTAL_MAX = 5000000, + INPUT_MIN = 1000, + INPUT_MAX = 100000; + + public boolean debug = false; + + public FreecellSolver() { + super(8, 4, 4); + setInputSize(8888); + } + + public void setInputSize(final int value) { + _inputSize = Math.min(Math.max(value, INPUT_MIN), INPUT_MAX); + } + + public int getInputSize() { + return _inputSize; + } + + private List _feed = new ArrayList<>(); + private final Map _done = new HashMap<>(); + private final SortedMap> _pool = new TreeMap<>(); + private int[] _solution = null; + private int _iteration = 0; + private final IntStack _nextMoves = new IntStack(); + private int _inputSize = INPUT_MIN; + + private final Comparator _comparator = new Comparator<>() { + @Override + public int compare(final int[] a, final int[] b) { + if (a.length != b.length) { + return a.length - b.length; + } + for (int i = 0; i < a.length; i++) { + if (a[i] != b[i]) { + // Actually we could also analyze which move is better. + return a[i] - b[i]; + } + } + return 0; + } + }; + + private boolean _shouldSolve(final int pathLength) { + if (_solution != null) { + final int cards = Deck.CARD_NUM - countSolved(); + return _solution.length > pathLength + cards; + } + return true; + } + + private void _prepare() { + _feed.clear(); + _done.clear(); + _pool.clear(); + + _solution = null; + _iteration = 0; + + rewind(); + moveCardsAuto(); + + _done.put(toKey(), _path.size()); + _feed.add(_path.toArray()); + } + + private void _splitOutput() { + final int feedSize = _feed.size(); + if (feedSize > _inputSize) { + for (final int[] path : _feed) { + rewind(); + forward(path); + + final int solved = countSolved(); + final int cells = countEmptyCells(); + final int piles = countEmptyPiles(); + final Integer KEY = Integer + .valueOf((int) -Math.round(90.00 * solved + 3.00 * cells + 4.25 * piles - 2.75 * path.length)); + + if (_pool.containsKey(KEY)) { + _pool.get(KEY).add(path); + } else { + final SortedSet set = new TreeSet<>(_comparator); + set.add(path); + _pool.put(KEY, set); + } + } + _feed.clear(); + } + } + + private void _getNextInput() { + if (_feed.isEmpty() && !_pool.isEmpty()) { + final Integer KEY = _pool.firstKey(); + final var set = _pool.get(KEY); + final int size = set.first().length; + for (final int[] path : set) { + if (size != path.length) { + break; + } else { + _feed.add(path); + } + } + + if (set.size() == _feed.size()) { + _pool.remove(KEY); + } else { + set.removeAll(_feed); + } + } + } + + private boolean _nextIteration() { + final int doneSize = _done.size(); + if (_solution != null && doneSize > TOTAL_MAX) { + return false; + } + + final int oldSize = _pool.size(); + + _splitOutput(); + _getNextInput(); + + if (_feed.isEmpty()) { + return false; + } + + final int newSize = _pool.size(); + if (debug) { + if (oldSize < newSize) { + System.out.print('+'); + } else if (oldSize > newSize) { + System.out.print('-'); + } else { + System.out.print('='); + } + } + + _iteration++; + if (debug && _iteration % 100 == 0) { + System.out.println("\n" + (_iteration / 100) + " [" + (doneSize * 100 / TOTAL_MAX) + "% " + newSize + ']'); + } + + final var input = _feed; + _feed = new ArrayList<>(); + + for (final int[] path : input) { + rewind(); + forward(path); + if (_shouldSolve(path.length + 1)) { + getMoves(_nextMoves); + for (int i = 0; i < _nextMoves.size(); i++) { + backward(path.length); + moveCard(_nextMoves.get(i)); + if (_shouldSolve(_path.size()) && hasNextMove()) { + moveCardsAuto(); + final String key = toKey(); + final Integer value = _done.get(key); + if (value == null || value.intValue() > _path.size()) { + final int[] next = _path.toArray(); + moveCardsToBases(); + if (isSolved()) { + if (_solution == null || _solution.length > _path.size()) { + _solution = _path.toArray(); + + if (debug) { + System.out.println(); + System.out.println("*********"); + System.out.println("Solution: " + _solution.length); + System.out.println(pathToString(_solution)); + System.out.println("*********"); + } + } + + } else { + _feed.add(next); + _done.put(key, Integer.valueOf(next.length)); + } + } + } + } + } + } + + return _feed.size() > 0 || _pool.size() > 0; + } + + public static char toChar(final int n) { + if (n >= 0 && n < 10) { + return (char) ('0' + n); + } else { + return (char) ('a' + n - 10); + } + } + + public String pathToString(final int[] path) { + final var buf = new StringBuilder(path.length); + for (final var move : path) { + buf.append(toChar(toGiver(move))); + buf.append(toChar(toTaker(move))); + } + return buf.toString(); + } + + public int[] solve() { + _prepare(); + while (_nextIteration()) + ; + return _solution; + } + + public static void main(final String[] args) { + final int deal = 8; + final var game = new FreecellSolver(); + game.debug = true; + game.deal(deal); + System.out.println("DESK: " + game.toString()); + System.out.println("KEY: " + game.toKey()); + + final var path = game.solve(); + if (path != null) { + System.out.println("Solved!"); + System.out.println("" + deal + + ',' + path.length + + ',' + game.pathToString(path)); + // game.rewind(); + // game.forward(path); + } else { + System.out.println("Unsolved ;-("); + } + } +} diff --git a/game-list.csv b/game-list.csv new file mode 100644 index 0000000..7ab2b73 --- /dev/null +++ b/game-list.csv @@ -0,0 +1,26 @@ +deal,mark,path +1,81,6c6d606e6b3656161f18101beb1e18e14e4048c83c32314b5b31393a4af17f737b5b707a79191a0a5aca272cdb2a272d29190928180879090a7828c9395158d80c0d0b6b18fb010a6a091a6bc9daeb4968 +2,82,481c105d506e646f6970606b1019e43e74d43d3639e95e59093579d9493d3a1a23292a3a7a7b2b5beb2e232878d858121808e8584d49424e414b6b0b05cb0b0608f828070938e90a0a7a5ada6a4a1a4b0b2b +3,84,594c4d7e7b78c4d45424202c2429295413101d1f19595af96212187f707a313739323aca4a6a6b3b3858616568f84c4849085829281845060249094101494b0f04090bcb0a4bdb4a5a6b1b2a1a2bea1b28387afb +4,80,3c3d3b563e505f5a5b0b3beb3e39c3f313181b010c0bdb2b2b632d6bcb2c2f2aca040c1a040a606a68f07f7aca754a753a7379474a4c4b4a1419ca091828495908485868e8384959d9187939f9386839 +5,80,287c707d7e474b343f3967c06c696b69e91e195919492924f414242b1b18183868212852565b5858683805060356050f02070a1acada4a4b2a4a2b4b6b7b3b303a4a6aeb7a4a4a49f97971597808180b +6,85,4c4d4e404f4af4d4643d343fe35e535a7464fa676f636b101614171b195958787939f93f213929252b2a5bfa36387858090f757b78782807090b4b791937380309070a39586a284a6bfa184beb4ada4b7b4a78ca4b +7,86,1c1d1e101a1b18da65616d6b4f461426f62f26212b7170e62e29727bdb0d020b01315b50353b343a38e85e567a2a5a4a545158786848080b181b656a6768386b686909d9e92959494a79c909fa1a19184b0b094c4aca +8,76,38631c6d1e19c9636956565c59585b1b242509e5391e1a2f292a35707a31303a6a4a5afa595a7f7b3b09750a7909fb0f0908e8386b4b4e47206168f918c81bdb18384beb284a084b580b0a7a +9,90,6c6d4e6f696bc92cd646262436262b37c35716171b5c575a2a1a6a3d303b4a6bfb47e24e6a4f48f41f1818565831353868737818717458757274527a7579f978c808070939690b1b49596a294a6b4b6adb2a0a0929e90a280b7b +10,87,303c3d3151515e5f5b3b3a51757b7818eb0e45057bf7454b4f6424f46f6bdb6deb6e68f838c3232023297919161c1f1b1519c9391c1a187a6adae878fa3a5a01260609395949030a5a4a5b6b0b5a0949394a48c8182868 +11,90,35151c1d171e1b1f31d132373d38d36d62676b5b53c32b5b502c2b626a5b565aea6aca7a46241a2c2b140b0e0a01cb0c080aea2e0af82fca7c1a202971757672727939e93878392938c928495918485868f80849d94b694b494b +12,79,38676c6d6e6b0b2024c02c2f2b3b3810673b12e61e1b19565b51593919f93f3afa5ada7d7b767f7a7929c9437c78474349f9434f4aea0a6a0b0a6b09081818d8285838293b6838c8010b7b0a1afa09 +13,78,1b5c5d5e515f5ad51d15d5121d1b1a5b2a2b5a5bcbeb7c454b7b3b1e6b4b4148047534357531c1313839e8f8787969636a24583a2a2829d909595a0b0308680a18283a6809285949194a1a49194a +14,76,3c3d5e305fc03c30323a52535b5975787b7a0acb1c0b3b131b2bcb2c2bdb7a5b450a1a1519656a696d69292a4a0939490309050b0aea203a2e2328183878f8e8c808586809594958d84a5b49 +15,101,0c0d320209523e394f4942d4f46d6f62f2626f68f81f76141016195909f97f795070d070785d575b1b183135382838232858256be535230e0b271537212321256526f62f2afaea5a0a5b0b5a0a5b5a4a7a280e0b5b4b7beb4a404b4969196878d8091a39ca +16,76,4a1c18626b6d61646836345e34545f5159393a3976797a1a2a131a194956151b242b26292b3b4b4a2428d87276783848496869fb484d434b68eb58db0b0a4b0b050a280a18ca096939496a5a +17,102,2c242d2e707421285453c4545a1c5158756525e57e727a1f17f71f13141a1b2b7b7a62646b0768061803f6060f0171070929c9f94939d930393231393c384850583d385258e86943684e4fd84d49694545494aca4c4b680b3b0a2beafb1b2a5bda1a5a7a1bcb +18,83,70737b7c7a072d572e272a2f2bdb3d323638424a483bd40d0b0ad01d141b3a1aeb1e181b5b31396a65624bd26d6368691939c8e8f901030b09d9595c565d5879d8c868484b450b4a2a7a29792a781a29395a28 +19,83,4c4020424d405e40545f545059794929e56ef96925293fd93d39d9c925192c2a1d151a141b6b5a4b4a5b5a3ada7a747b5b3538386818716368587808020d0405060b380ada0a5a0a0b2beb09284bfb18486bc8 +20,83,3c3d404a3e3a63606a6b67601f161a2a2bc25c5ae25e5b5956252a2620d02d2925d5323d38181b181959085809087138d87d030a7a797b5b39692b680969d90902010d73494a4a0a4a0b1bfb1878cb2b38dbe8 +21,81,4a7c50757d7e7f7a37171a371318381a17d72a2d2ada2d2beb3bdb5d5a586b18315e6134395929f1616f69e96838280204030e09d979020b1b187819c9184cf84841094958194b2b4a7bcb2a7a4a7beb3a +22,80,2c2d2e23536525292f1218f8726fe26e68696b58d8767bf60d0f091619e13079395e5659e65e32345b5835363a3878e8587a1a386a0a1b6b0b186a2a6b296a2a4a690a2949c90a4b0a484b1b4bdbfb49 +23,72,287a2c232d2e2bdb297d127279e9121e121a5a7a6a636f6bd9fb5d5f5b2b3beb3e103b38583a1868582a68e8783e393808e80e380b4149f94a29d92a4b4a1b09c9e9010a4b090a1a +24,79,092c2a282b292a1216101d1ad13d31303a313e38e37343737a7b424e4b1b0f6b6a0602030b530ac05c5b586acb5c2b626861691959187819f9183918293ac82a3978d93a4a68394b2b0b2838eb0a09 +25,78,404c32343234383d3a531373101a1b532a5e585beb2b181e18212b7f7b717a2029792919491a4a61676a69685849790aca4c454b3b0b0a045b051b01270928d83868f8394959e8093a78ca0b1b0a \ No newline at end of file