001package ball.game.card;
002/*-
003 * ##########################################################################
004 * Game Applications and Utilities
005 * %%
006 * Copyright (C) 2010 - 2022 Allen D. Ball
007 * %%
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *      http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 * ##########################################################################
020 */
021import java.beans.ConstructorProperties;
022import java.util.ArrayList;
023import java.util.Collection;
024import java.util.Comparator;
025import java.util.List;
026import java.util.Map;
027import java.util.Objects;
028import java.util.TreeMap;
029import java.util.function.Function;
030import java.util.function.Predicate;
031import java.util.regex.Pattern;
032
033import static java.util.Collections.indexOfSubList;
034import static java.util.Collections.reverse;
035import static java.util.Collections.unmodifiableList;
036import static java.util.Collections.unmodifiableMap;
037import static java.util.stream.Collectors.toList;
038import static org.apache.commons.lang3.StringUtils.EMPTY;
039
040/**
041 * Playing {@link Card}.
042 *
043 * {@bean.info}
044 *
045 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball}
046 */
047public class Card implements Comparable<Card> {
048    private static final Comparator<? super Card> COMPARATOR =
049        Comparator
050        .<Card>comparingInt(t -> t.getSuit().ordinal())
051        .thenComparingInt(t -> t.getRank().ordinal());
052
053    private final Suit suit;
054    private final Rank rank;
055    private final transient String string;
056
057    /**
058     * Sole protected constructor.
059     *
060     * @param   suit            The {@link Card} {@link Suit}.
061     * @param   rank            The {@link Card} {@link Rank}.
062     */
063    @ConstructorProperties({ "suit", "rank" })
064    protected Card(Suit suit, Rank rank) {
065        this.rank = rank;
066        this.suit = suit;
067
068        if (rank.equals(Rank.JOKER)) {
069            if (suit != null) {
070                throw new IllegalArgumentException("suit=" + suit + ",rank=" + rank);
071            }
072        }
073
074        switch (rank) {
075        case JOKER:
076            this.string = rank.toString();
077            break;
078
079        default:
080            this.string = rank.toString() + "-" + suit.toString();
081            break;
082        }
083    }
084
085    /**
086     * Method to get the {@link Card} {@link Suit}.
087     *
088     * @return  {@link Suit}
089     */
090    public Suit getSuit() { return suit; }
091
092    /**
093     * Method to get the {@link Card} {@link Rank}.
094     *
095     * @return  {@link Rank}
096     */
097    public Rank getRank() { return rank; }
098
099    /**
100     * Method to get the {@link Card} {@link Color}.
101     *
102     * @return  {@link Suit#getColor()}
103     */
104    public Color getColor() {
105        Suit suit = getSuit();
106
107        return (suit != null) ? suit.getColor() : null;
108    }
109
110    @Override
111    public int compareTo(Card that) {
112        return Objects.compare(this, that, COMPARATOR);
113    }
114
115    @Override
116    public boolean equals(Object object) {
117        return ((object instanceof Card) ? (this.compareTo((Card) object) == 0) : super.equals(object));
118    }
119
120    @Override
121    public int hashCode() { return Objects.hash(getSuit(), getRank()); }
122
123    @Override
124    public String toString() { return string; }
125
126    /**
127     * Static method to parse a {@link String} consistent with
128     * {@link #toString} to a {@link Card}.
129     *
130     * @param   string          The {@link String} to parse.
131     *
132     * @return  The {@link Card}.
133     */
134    public static Card parse(String string) {
135        Card card = null;
136
137        try {
138            var substrings = string.split(Pattern.quote("-"), 2);
139
140            card =
141                new Card((substrings.length > 1) ? Suit.parse(substrings[1]) : null,
142                         Rank.parse(substrings[0]));
143        } catch (Exception exception) {
144            throw new IllegalArgumentException(string, exception);
145        }
146
147        return card;
148    }
149
150    private static <T> Predicate<List<T>> same(Function<T,Predicate<T>> mapper) {
151        return t -> ((! t.isEmpty()) && t.stream().allMatch(mapper.apply(t.get(0))));
152    }
153
154    private static <T,R> List<R> listOf(Collection<T> collection, Function<T,R> mapper) {
155        return collection.stream().map(mapper).collect(toList());
156    }
157
158    /**
159     * {@link Card} rank {@link Enum} type.
160     */
161    public enum Rank implements Predicate<Card> {
162        JOKER,
163        ACE,
164        TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE,
165        TEN, JACK, QUEEN, KING;
166
167        private transient String string = null;
168
169        @Override
170        public boolean test(Card card) {
171            return is(this).test(card.getRank());
172        }
173
174        @Override
175        public String toString() {
176            if (string == null) {
177                switch (this) {
178                case JOKER:
179                    string = super.toString();
180                    break;
181
182                case ACE:
183                case JACK:
184                case QUEEN:
185                case KING:
186                    string = name().substring(0, 1);
187                    break;
188
189                default:
190                    string = String.valueOf(ordinal());
191                    break;
192                }
193            }
194
195            return string;
196        }
197
198        /**
199         * {@include #ACE_HIGH}
200         */
201        public static final List<Rank> ACE_HIGH  =
202            List.of(JOKER,
203                    TWO, THREE, FOUR, FIVE,
204                    SIX, SEVEN, EIGHT, NINE,
205                    TEN, JACK, QUEEN, KING, ACE);
206
207        /**
208         * {@include #ACE_LOW}
209         */
210        public static final List<Rank> ACE_LOW = List.of(values());
211
212        private static final Map<String,Rank> MAP;
213        private static final List<List<Rank>> SEQUENCES;
214
215        static {
216            var map = new TreeMap<String,Rank>(String.CASE_INSENSITIVE_ORDER);
217
218            for (var rank : values()) {
219                map.put(rank.name(), rank);
220                map.put(rank.toString(), rank);
221            }
222
223            MAP = unmodifiableMap(map);
224
225            var high = new ArrayList<Rank>(Rank.ACE_HIGH);
226            var low = new ArrayList<Rank>(Rank.ACE_LOW);
227
228            reverse(high);
229            reverse(low);
230
231            SEQUENCES = List.of(unmodifiableList(high), unmodifiableList(low));
232        }
233
234        /**
235         * {@link Predicate} to test all {@link Card}s are the same
236         * {@link Rank}.
237         */
238        public static final Predicate<List<Card>> SAME = same(Card::getRank);
239
240        /**
241         * {@link Predicate} to test the {@link Card}s make up a sequence.
242         */
243        public static final Predicate<List<Card>> SEQUENCE =
244            t -> ((! t.isEmpty()) && sequence(listOf(t, Card::getRank)));
245
246        private static boolean sequence(List<Rank> list) {
247            return SEQUENCES.stream().anyMatch(t -> indexOfSubList(t, list) >= 0);
248        }
249
250        /**
251         * Method to return a {@link Predicate} to test if a {@link Rank} is
252         * the specified {@link Rank}.
253         *
254         * @param       rank    The {@link Rank}.
255         *
256         * @return      {@link Predicate}
257         */
258        public static Predicate<Rank> is(Rank rank) {
259            return t -> Objects.equals(rank, t);
260        }
261
262        /**
263         * Static method to parse a {@link String} consistent with
264         * {@link #name()} and {@link #toString()} to a {@link Rank}.
265         *
266         * @param       string  The {@link String} to parse.
267         *
268         * @return      The {@link Rank}.
269         */
270        public static Rank parse(String string) {
271            var rank = MAP.get(string);
272
273            if (rank == null) {
274                rank = Enum.valueOf(Rank.class, string);
275            }
276
277            return rank;
278        }
279    }
280
281    /**
282     * {@link Card} color {@link Enum} type.
283     */
284    public enum Color implements Predicate<Card> {
285        BLACK, RED;
286
287        @Override
288        public boolean test(Card card) {
289            return is(this).test(card.getColor());
290        }
291
292        /**
293         * {@link Predicate} to test all {@link Card}s are the same
294         * {@link Color}.
295         */
296        public static final Predicate<List<Card>> SAME = same(Card::getColor);
297
298        /**
299         * Method to return a {@link Predicate} to test if a {@link Color} is
300         * the specified {@link Color}.
301         *
302         * @param       color   The {@link Color}.
303         *
304         * @return      {@link Predicate}
305         */
306        public static Predicate<Color> is(Color color) {
307            return t -> Objects.equals(color, t);
308        }
309    }
310
311    /**
312     * {@link Card} suit {@link Enum} type.
313     */
314    public enum Suit implements Predicate<Card> {
315        CLUBS(Color.BLACK, "\u2667" /* U+2663 */),
316        DIAMONDS(Color.RED, "\u2662" /* U+2666 */),
317        HEARTS(Color.RED, "\u2661" /* U+2665 */),
318        SPADES(Color.BLACK, "\u2664" /* U+2660 */);
319
320        private static final Map<String,Suit> MAP;
321
322        static {
323            var map = new TreeMap<String,Suit>(String.CASE_INSENSITIVE_ORDER);
324
325            for (var suit : values()) {
326                map.put(suit.name(), suit);
327                map.put(suit.name().substring(0, 1), suit);
328                map.put(suit.toString(), suit);
329            }
330
331            MAP = unmodifiableMap(map);
332        }
333
334        private final Color color;
335        private final String string;
336
337        @ConstructorProperties({ "color", EMPTY })
338        private Suit(Color color, String string) {
339            this.color = color;
340            this.string = string;
341        }
342
343        /**
344         * Method to get the {@link Suit} {@link Color}.
345         *
346         * @return      {@link Color}
347         */
348        public Color getColor() { return color; }
349
350        @Override
351        public boolean test(Card card) {
352            return is(this).test(card.getSuit());
353        }
354
355        @Override
356        public String toString() { return string; }
357
358        /**
359         * {@link Predicate} to test all {@link Card}s are the same
360         * {@link Suit}.
361         */
362        public static final Predicate<List<Card>> SAME = same(Card::getSuit);
363
364        /**
365         * Method to return a {@link Predicate} to test if a {@link Suit} is
366         * the specified {@link Suit}.
367         *
368         * @param       suit    The {@link Suit}.
369         *
370         * @return      {@link Predicate}
371         */
372        public static Predicate<Suit> is(Suit suit) {
373            return t -> Objects.equals(suit, t);
374        }
375
376        /**
377         * Static method to parse a {@link String} consistent with
378         * {@link #name()} and {@link #toString()} to a {@link Suit}.
379         *
380         * @param       string  The {@link String} to parse.
381         *
382         * @return      The {@link Suit}.
383         */
384        public static Suit parse(String string) {
385            var suit = MAP.get(string);
386
387            if (suit == null) {
388                suit = Enum.valueOf(Suit.class, string);
389            }
390
391            return suit;
392        }
393    }
394}