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}