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