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}