TL;DR
- Le Problème: Optimisation de la complexité temporelle et spatiale des structures de données.
- L'Approche: Problème style CTCI 7.1 pour débutants: concevoir des classes de cartes réutilisables (Card, Deck, Hand) et les spécialiser pour le blackjack avec BlackJackCard et BlackJackHand, as souples inclus.
- Complexité: Compromis optimal entre temps et mémoire avec gestion des cas limites.
Un jeu standard, c'est cinquante-deux cartes: quatre couleurs, treize rangs chacune. Le blackjack s'appuie dessus avec ses propres règles de score (figures valent dix, as valent un ou onze). En entretien, on veut une conception de classes générale, puis une spécialisation propre pour le jeu.
Ce billet est un enseignement original pour débutants en Java. Même famille de prompts OOD d'entretien, pas une copie de livre. Partie de la série CTCI en Java. Le chapitre 7, conception orientée objet, commence ici.
1. Analogie de la table
Imagine une table de casino.
- Card: une carte physique. Couleur et rang ne changent pas après impression.
- Deck: le sabot. Tu mélanges, tu donnes une carte à la fois, et tu sais quand il est vide.
- Hand: les cartes qu'un joueur tient maintenant. On ajoute des cartes, on les score selon les règles du jeu.
- Blackjack: les mêmes cartes, une autre table de valeurs. Un as est souple tant que le compter comme onze ferait sauter la main.
Si tu codes en dur la "valeur blackjack" dans un Card générique, tout autre jeu (poker, bataille, rami) hérite de mauvaises règles. Laisse la valeur ouverte. Spécialise le scoring là où vit le jeu.
2. Énoncé en mots simples
Objectif: concevoir des classes pour un jeu de cartes générique, puis montrer comment les sous-classer pour le blackjack.
Types que l'on nomme souvent:
| Type | Rôle |
|---|---|
Suit |
Club, Diamond, Heart, Spade (enum) |
Card |
une couleur + valeur de face (1-13 ou As-Roi) |
Deck |
liste de cartes; mélanger, donner, cartes restantes |
Hand |
collection de cartes tenues par un joueur |
BlackJackCard |
carte qui connaît la valeur de face au blackjack |
BlackJackHand |
main qui score le blackjack (as souples, bust) |
À clarifier en entretien:
- Un seul jeu ou sabot multi-jeux? (Commence avec un jeu de 52.)
- Jokers inclus? (En général non.)
- Encodage de face: as = 1, valet = 11, dame = 12, roi = 13 convient à la classe de base.
- Le score blackjack est plus une affaire de main que de carte unique quand les as peuvent basculer entre 1 et 11.
- Jeu vide au deal: renvoyer
null, lever une exception ou optional; choisis un contrat.
Forme des méthodes (esquisse):
// Deck
void shuffle();
Card dealCard();
int remainingCards();
// Hand
void addCard(Card c);
int score(); // spécifique au jeu dans les sous-classes
// BlackJackHand
boolean isBusted();
boolean is21();
boolean isBlackjack(); // natural 2 cartes à 21
3. Réfléchir d'abord
Séparer l'identité de la valeur de jeu
Une carte est une couleur et un rang. C'est stable. Ce qu'elle vaut dépend du jeu.
Deux options propres:
- Le
Cardde base exposevalue()comme rang de face (1-13). Le blackjack mappe vers 1, 2, ..., 10, 10, 10, 10. value()abstrait surCard, concret dansBlackJackCard.
L'option 1 est plus simple pour une bibliothèque multi-jeux. L'option 2 convient quand l'exercice est "sous-classe pour le blackjack". Ce billet utilise un Card de base avec valeur de face, puis BlackJackCard qui redéfinit la valeur effective pour le scoring, et place la logique multi-as sur BlackJackHand.
Deck est un conteneur mélanger + donner
Deck possède un ArrayList<Card> (ou LinkedList). La construction remplit 52 cartes. shuffle utilise Collections.shuffle (ou Fisher-Yates à la main si la librairie est interdite). dealCard retire de la fin (ou avance un index) en O(1).
Ne reconstruis pas toute la liste à chaque donne. Un index dealt marche aussi: les cartes après l'index restent dans le sabot.
Hand est une liste; le scoring est polymorphe
Hand tient des cartes. Un score() générique peut sommer les valeurs de face. BlackJackHand redéfinit: compte les as, somme les non-as, puis assigne à chaque as 11 ou 1 sans dépasser 21 si possible.
Héritage vs composition
- Héritage:
BlackJackCard extends Card,BlackJackHand extends Hand. Correspond au libellé classique ("subclass"). - Composition: un
BlackjackScoreren stratégie sur unHandplat. Plus propre si beaucoup de jeux ensuite; mentionne-le si l'intervieweur creuse.
Pour ce problème, l'héritage est la première réponse attendue.
Algorithme des as souples (cœur du scoring)
- Somme chaque non-as comme min(face, 10). Compte les as.
- Total = nonAceSum + as (chaque as commence à 1).
- Pour chaque as, si total + 10 <= 21, ajoute 10 (promeut cet as de 1 à 11). Au plus une promotion est utile pour un seul total de main sous les règles standard, mais la boucle est claire et correcte.
Exemple: As + 6 = soft 17 (1+6+10). As + As + 9 = 21 (1+1+9+10). As + 10 + 10 = 21 avec as à 1.
4. Solution Java
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
enum Suit {
CLUB, DIAMOND, HEART, SPADE
}
/** Generic playing card. faceValue: 1 = Ace, 11 = Jack, 12 = Queen, 13 = King. */
class Card {
private final int faceValue; // 1..13
private final Suit suit;
public Card(int faceValue, Suit suit) {
if (faceValue < 1 || faceValue > 13) {
throw new IllegalArgumentException("faceValue must be 1..13");
}
this.faceValue = faceValue;
this.suit = suit;
}
public int getFaceValue() {
return faceValue;
}
public Suit getSuit() {
return suit;
}
/** Default: raw face rank. Games can map this. */
public int value() {
return faceValue;
}
@Override
public String toString() {
return faceLabel() + " of " + suit;
}
private String faceLabel() {
switch (faceValue) {
case 1: return "Ace";
case 11: return "Jack";
case 12: return "Queen";
case 13: return "King";
default: return String.valueOf(faceValue);
}
}
}
/** Deck of 52 cards. */
class Deck {
private final List<Card> cards = new ArrayList<>();
private int dealt = 0; // next index to deal; cards before it are gone
public Deck() {
for (Suit s : Suit.values()) {
for (int v = 1; v <= 13; v++) {
cards.add(new Card(v, s));
}
}
}
/** Build a blackjack shoe: same 52 ranks, but BlackJackCard instances. */
public static Deck blackjackDeck() {
Deck d = new Deck(true);
return d;
}
private Deck(boolean blackjack) {
for (Suit s : Suit.values()) {
for (int v = 1; v <= 13; v++) {
cards.add(blackjack ? new BlackJackCard(v, s) : new Card(v, s));
}
}
}
public void shuffle() {
// only shuffle remaining cards if mid-shoe; full reshuffle resets dealt
Collections.shuffle(cards.subList(dealt, cards.size()), new Random());
}
public void shuffleFresh(Random rng) {
dealt = 0;
Collections.shuffle(cards, rng);
}
public Card dealCard() {
if (remainingCards() == 0) {
return null;
}
return cards.get(dealt++);
}
public int remainingCards() {
return cards.size() - dealt;
}
}
/** Generic hand: just cards. */
class Hand {
protected final List<Card> cards = new ArrayList<>();
public void addCard(Card c) {
if (c == null) {
throw new IllegalArgumentException("card is null");
}
cards.add(c);
}
public List<Card> getCards() {
return Collections.unmodifiableList(cards);
}
public int size() {
return cards.size();
}
/** Sum of face values (not blackjack rules). */
public int score() {
int sum = 0;
for (Card c : cards) {
sum += c.value();
}
return sum;
}
}
/** Card scored for blackjack: Ace 1 or 11 handled by the hand; face cards 10. */
class BlackJackCard extends Card {
public BlackJackCard(int faceValue, Suit suit) {
super(faceValue, suit);
}
public boolean isAce() {
return getFaceValue() == 1;
}
public boolean isFaceCard() {
int v = getFaceValue();
return v >= 11 && v <= 13;
}
/** Fixed contribution before soft-ace adjustment: Ace counts as 1 here. */
@Override
public int value() {
int v = getFaceValue();
if (v == 1) {
return 1;
}
if (v >= 10) {
return 10; // 10, J, Q, K
}
return v;
}
}
/** Blackjack hand: soft aces, bust, natural blackjack. */
class BlackJackHand extends Hand {
@Override
public int score() {
int total = 0;
int aces = 0;
for (Card c : cards) {
if (c instanceof BlackJackCard) {
BlackJackCard bj = (BlackJackCard) c;
total += bj.value(); // ace as 1, faces as 10
if (bj.isAce()) {
aces++;
}
} else {
// tolerate plain cards: map like blackjack
int v = c.getFaceValue();
if (v == 1) {
total += 1;
aces++;
} else if (v >= 10) {
total += 10;
} else {
total += v;
}
}
}
// promote aces from 1 to 11 while staying <= 21
while (aces > 0 && total + 10 <= 21) {
total += 10;
aces--;
}
return total;
}
public boolean isBusted() {
return score() > 21;
}
public boolean is21() {
return score() == 21;
}
/** Natural: exactly two cards totaling 21 (Ace + 10-value). */
public boolean isBlackjack() {
return cards.size() == 2 && score() == 21;
}
}
Parcours du scoring:
| Main | Calcul | Score |
|---|---|---|
| As, 6 | 1+6, promeut as +10 | 17 (soft) |
| Roi, Dame | 10+10 | 20 |
| As, Roi | 1+10, promeut +10 | 21 (blackjack si deux cartes) |
| As, As, 9 | 1+1+9, une promotion +10 | 21 |
| 10, 9, 5 | 10+9+5 | 24 (bust) |
Esquisse de la boucle de donne:
Deck shoe = Deck.blackjackDeck();
shoe.shuffleFresh(new Random(42));
BlackJackHand player = new BlackJackHand();
player.addCard(shoe.dealCard());
player.addCard(shoe.dealCard());
System.out.println(player.score());
System.out.println(player.isBlackjack());
Note sur la fabrique: le constructeur privé Deck(boolean blackjack) garde un seul endroit qui remplit couleurs et rangs. En entretien design pur, tu peux sauter la fabrique et ne dessiner que l'UML plus la méthode de scoring.
5. Notes de conception et complexité
| Pièce | Temps | Espace / notes |
|---|---|---|
| Construire le jeu | O(1) | 52 fixes (ou 52 * n multi-jeux) |
shuffle |
O(N) | N restantes ou jeu complet |
dealCard |
O(1) | avancer l'index; évite remove(0) sur ArrayList |
Hand.addCard |
O(1) amorti | append sur la liste |
BlackJackHand.score |
O(K) | K cartes en main (petit en pratique) |
Checklist de conception appréciée:
- Encapsulation: valeur de face et couleur finales sur
Card. - Enum pour la couleur: pas de chaînes magiques, facile à parcourir.
- Point d'extension: le scoring vit sur la main (et le map de valeur de carte), pas éparpillé dans la boucle du croupier.
- Contrat de donne nulle: documente
dealCard() -> nullquand le jeu est vide pour que l'appelant regarderemainingCards(). - Immuabilité des cartes: elles ne changent pas de couleur en cours de partie; les mains changent de membres.
Sabot multi-jeux: construis n * 52 cartes de la même façon, ou compose List<Deck>. Le scoring se fiche du nombre de jeux.
6. Cas limites et erreurs fréquentes
Les intervieweurs poussent ici:
- Jeu vide:
dealCardrenvoienull(ou lève). N'indexe jamais au-delà decards.size(). - Mélanger en cours de main: soit tu mélanges seulement le reste, soit tu remets
dealtà zéro pour un reshuffle complet. Énonce les deux. - As compté deux fois: ne compte pas l'as comme 11 dans
BlackJackCard.value()et encore dans la main. Garde l'as à 1 sur la carte; promeus sur la main. - Plusieurs as: As+As+9 = 21, pas 31. La boucle de promotion doit s'arrêter à 21.
- Bust avec as: As+10+10 doit rester 21 avec as à 1, pas forcer 31.
- Natural vs 21 plus tard:
isBlackjackc'est deux cartes et 21. Un 21 en cinq cartes n'est pas un natural. - Mélanger un
Cardplain dansBlackJackHand: soit tu l'interdis, soit tu mappe les faces comme dans la boucle de score ci-dessus. - Jokers / jeux incomplets: hors scope sauf demande; énonce l'hypothèse.
Erreurs fréquentes:
- Coder le blackjack uniquement dans
Card. Le poker ne le réutilise pas proprement. ArrayList.remove(0)pour donner. O(N) par carte. Utilise un index ou retire de la fin.- Oublier que les figures valent 10. Le valet n'est pas 11 points au blackjack.
- Renvoyer tous les totaux possibles sans choisir le meilleur <= 21. La logique soft a besoin d'un seul score pour le bust.
- Pas d'enum pour les couleurs. Des couleurs en string invitent les typos.
- Champs publics mutables sur la carte. Préfère private final + getters.
Idée minimale de smoke:
BlackJackHand h = new BlackJackHand();
h.addCard(new BlackJackCard(1, Suit.SPADE)); // Ace
h.addCard(new BlackJackCard(13, Suit.HEART)); // King
assert h.score() == 21;
assert h.isBlackjack();
BlackJackHand soft = new BlackJackHand();
soft.addCard(new BlackJackCard(1, Suit.CLUB));
soft.addCard(new BlackJackCard(6, Suit.DIAMOND));
assert soft.score() == 17;
BlackJackHand bust = new BlackJackHand();
bust.addCard(new BlackJackCard(10, Suit.CLUB));
bust.addCard(new BlackJackCard(9, Suit.CLUB));
bust.addCard(new BlackJackCard(5, Suit.CLUB));
assert bust.isBusted();
7. Récap pour un ami
Deck of Cards est un démarrage OOD, pas un algorithme dur:
- Modélise Card comme couleur + valeur de face. La couleur est un enum. La face reste 1-13.
- Deck construit 52 cartes, mélange et donne en O(1) avec un curseur.
- Hand tient des cartes. Le score générique peut sommer les faces.
- BlackJackCard mappe les faces vers les pips blackjack (as 1 au niveau carte, 10 pour 10/J/Q/K).
- BlackJackHand score les as souples: les as commencent à 1, passent à 11 tant que le total reste <= 21. Bust et blackjack natural sont de fines couches sur ce score.
- Garde les règles du jeu sur la sous-classe pour qu'un jeu plain serve encore à d'autres jeux.
Si tu peux dessiner les cinq types, expliquer pourquoi la promotion de l'as vit sur la main, et donner sans remove O(N), tu maîtrises le 7.1. Suite du chapitre 7: routage d'appels du call center et niveaux d'employés.
Série
- Guide: Guide de la série CTCI
- Précédent: Poison
- Suivant: Call Center
