Lezione 7 di 7 · 22 min di lettura

Riepilogo e sfida finale

Il modulo in una tabella, gli errori da riconoscere, tre quiz di controllo e una sfida con un mazzo di carte, due mani e le regole del blackjack.

I tuoi tipi, le tue parole

All’inizio del modulo i tuoi programmi parlavano la lingua del computer: int, double, String, e liste parallele da tenere allineate a mano. Adesso sai:

  • raccogliere valori diversi in una struct, inizializzarla per posizione o per nome, annidarla, copiarla;
  • dare ai tuoi tipi dei metodi, scegliendo tra self per leggere e &self per modificare;
  • tenere collezioni di struct, modificarle senza cadere nella copia del foreach, cercarle e ordinarle con un criterio tuo;
  • descrivere scelte chiuse con gli enum, con i loro valori associati e gli switch che non dimenticano nessun caso;
  • dare nomi nuovi ai tipi esistenti con alias, e creare tipi incompatibili tra loro con typedef.

Il filo che lega tutto: rendere impossibili gli stati sbagliati. Un Player non può perdere il suo punteggio per strada, un Direction non può valere 7, un Meters non può finire dove servono Feet. Ogni errore che il compilatore rifiuta è un errore che non dovrai cercare di notte.

La pagina da tenere accanto

CosaCome si scrive
Structstruct Player { String name; int score; } (fuori dalle funzioni, maiuscola)
Per posizione / per nomePlayer a = { "Ada", 42 }; / Player b = { .name = "Ada" };
Copia e modificaPlayer c = { ...a, .score = 0 };
Campi annidatis.to.x = 10; e { .to.y = 7 }
Metodo che leggefn int Rect.area(self) => self.width * self.height;
Metodo che modificafn void Rect.scale(&self, int f) { self.width *= f; }
Chiamatar.area(), equivale a Rect.area(r)
Array di structPlayer[*] team = { { "Ada", 72 }, { "Grace", 42 } };
Modificare nel foreachforeach (&p : team) p.score += 1;
Ordinare con un criteriosort::quicksort(&list, &by_score); con fn int by_score(Player a, Player b)
Enumenum Direction { NORTH, EAST, SOUTH, WEST }
Nome corto / completoDirection d = NORTH; / Direction.NORTH dove il tipo non si deduce
Numero d’ordined.ordinal, Direction::from_ordinal(1)
Tutti i valoriforeach (x : Direction::values)
Valori associatienum Planet : (double gravity) { EARTH { 9.81 }, ... } e p.gravity
Costanti con numeri tuoiconstdef HttpStatus { OK = 200, NOT_FOUND = 404 }
Soprannomealias Score = int;
Tipo distintotypedef Meters @constinit = double;
Distinto ma convertibile al tipo basetypedef Money = inline long;

Errori che ormai riconosci

MessaggioCosa hai fatto
Names of structs must start with an uppercase letter.struct player: i tipi vogliono la maiuscola
'Point' does not support comparisons.== tra due struct: confronta i campi
There is no field or method 'Player.points'.Un campo o un metodo che non esiste (o scritto male)
Enum value WEST was not handled in the switch...Uno switch su un enum senza tutti i casi
The enum type cannot be inferred, so in that case you need to use the qualified name...Un nome corto dove il tipo non si deduce
Implicitly casting 'Feet' to 'Meters' is not permitted...Hai mescolato due typedef diversi

E due errori che il compilatore non vede: un metodo che modifica self invece di &self (cambia una copia), e un foreach (p : team) che modifica p (idem).

Quiz di controllo

Quiz

Cosa stampa? Rect r = { 2, 3 }; Rect q = r; q.scale(2); io::printn(r.area()); (con scale(&self, int f) che moltiplica i lati)

Quiz

Con typedef Id = int; e fn void load(Id id), quale chiamata compila?

Quiz

Con enum Day { MON, TUE, WED }, cosa stampa io::printn(Day::from_ordinal((Day.WED.ordinal + 1) % 3));?

La sfida: blackjack

Un mazzo di carte è il banco di prova perfetto per questo modulo: due enum (semi e valori), una struct (la carta), una collezione (il mazzo), e delle regole da tradurre in metodi.

Esercizio · sul tuo computer

Blackjack

Scrivi cards.c3.

1. I tipi. Un enum Suit con un valore associato letter ("H", "D", "C", "S" per cuori, quadri, fiori, picche, in quest’ordine). Un enum Rank con due valori associati, label e points: dal TWO ("2", 2 punti) al TEN ("10", 10 punti), poi JACK, QUEEN e KING ("J", "Q", "K", 10 punti ciascuno) e infine ACE ("A", 11 punti). Una struct Card con rank e suit, e un metodo name() che restituisce per esempio "10H" o "AS" (con tformat).

2. Il mazzo. Una List{Card} riempita con due foreach annidati su Suit::values e Rank::values: 52 carte, tutti i cuori dal 2 all’asso, poi i quadri, e così via. Stampa Deck: con il numero di carte, , from con il nome della prima e to con quello dell’ultima.

3. Le mani. Una struct Hand con owner (String) e cards (una List{Card}), e tre metodi: take(card) aggiunge una carta; score() calcola i punti; print() stampa il nome allineato a sinistra su 7 caratteri, le carte (ognuna preceduta da uno spazio) e = con il punteggio.

La regola del punteggio: somma i punti delle carte, ma l’asso vale 11 oppure 1. Quindi, finché il totale supera 21 e hai assi contati come 11, togli 10 e considera quell’asso come un 1.

4. La distribuzione. Senza numeri casuali (li vedremo più avanti), le carte si prendono dal mazzo “mescolato” con una formula: la carta numero i, per i da 0 a 5, è deck[(i * 19 + 12) % 52]. Le carte con i pari vanno al "Player", quelle con i dispari al "Dealer". (Usa sz per i: è un indice.) Stampa le due mani.

5. Il verdetto. Con uno switch senza espressione: se il giocatore supera 21 vince il banco; altrimenti se il banco supera 21, o il giocatore ha di più, vince il giocatore; a parità è pareggio; altrimenti vince il banco. Stampa Player wins, Dealer wins o Draw.

6. I semi. Conta quante carte di ogni seme sono state distribuite (in tutte e due le mani) usando un int[4] indicizzato con suit.ordinal, e stampa ogni seme con il suo nome allineato a sinistra su 8 caratteri e il conteggio.

Mostra una soluzione (prima prova da solo!)
cards.c3
import std::io;
import std::collections::list;

enum Suit : (String letter)
{
	HEARTS   { "H" },
	DIAMONDS { "D" },
	CLUBS    { "C" },
	SPADES   { "S" },
}

enum Rank : (String label, int points)
{
	TWO   { "2", 2 },
	THREE { "3", 3 },
	FOUR  { "4", 4 },
	FIVE  { "5", 5 },
	SIX   { "6", 6 },
	SEVEN { "7", 7 },
	EIGHT { "8", 8 },
	NINE  { "9", 9 },
	TEN   { "10", 10 },
	JACK  { "J", 10 },
	QUEEN { "Q", 10 },
	KING  { "K", 10 },
	ACE   { "A", 11 },
}

struct Card
{
	Rank rank;
	Suit suit;
}

fn String Card.name(self) => string::tformat("%s%s", self.rank.label, self.suit.letter);

struct Hand
{
	String owner;
	List{Card} cards;
}

fn void Hand.take(&self, Card c) => self.cards.push(c);

fn int Hand.score(self)
{
	int total = 0;
	int aces = 0;
	foreach (c : self.cards)
	{
		total += c.rank.points;
		if (c.rank == ACE) aces++;
	}
	while (total > 21 && aces > 0)
	{
		total -= 10;
		aces--;
	}
	return total;
}

fn void Hand.print(self)
{
	io::printf("%-7s", self.owner);
	foreach (c : self.cards) io::printf(" %s", c.name());
	io::printfn(" = %d", self.score());
}

fn void main()
{
	List{Card} deck;
	foreach (suit : Suit::values)
	{
		foreach (rank : Rank::values) deck.push({ rank, suit });
	}
	io::printfn("Deck: %d cards, from %s to %s", deck.len(), deck[0].name(), deck[^1].name());

	Hand player = { .owner = "Player" };
	Hand dealer = { .owner = "Dealer" };
	for (sz i = 0; i < 6; i++)
	{
		Card c = deck[(i * 19 + 12) % 52];
		if (i % 2 == 0)
		{
			player.take(c);
		}
		else
		{
			dealer.take(c);
		}
	}
	player.print();
	dealer.print();

	int p = player.score();
	int d = dealer.score();
	switch
	{
		case p > 21: io::printn("Dealer wins");
		case d > 21 || p > d: io::printn("Player wins");
		case p == d: io::printn("Draw");
		default: io::printn("Dealer wins");
	}

	int[4] per_suit;
	foreach (c : player.cards) per_suit[c.suit.ordinal]++;
	foreach (c : dealer.cards) per_suit[c.suit.ordinal]++;
	foreach (suit : Suit::values)
	{
		io::printfn("%-8s %d", suit, per_suit[suit.ordinal]);
	}
}

I punti delicati:

  • L’asso: la mano del giocatore vale 11 + 10 + 10 = 31 contando l’asso 11; il while toglie 10 una volta e arriva a 21. Il ciclo (non un semplice if) serve per le mani con più assi: due assi e un nove fanno 31, poi 21.
  • deck.push({ rank, suit }): la carta si crea al volo tra graffe, per posizione. Il mazzo esce in ordine perché i cicli seguono l’ordine degli enum.
  • L’ordine dei case nel verdetto: “il giocatore sballa” va controllato per primo, perché se il giocatore supera 21 perde anche se il banco supera 21 a sua volta. Sono le regole del blackjack.
  • int[4] indicizzato con ordinal: un piccolo contatore per ogni valore dell’enum. Se i semi fossero di più, basterebbe cambiare il 4 (o, meglio, usare Suit::values.len).
  • La formula di distribuzione: 19 e 52 non hanno divisori comuni, quindi (i * 19 + 12) % 52 visita tutte le 52 posizioni senza ripetersi prima di aver finito il mazzo. È un modo semplice di “mescolare” in modo prevedibile.

Cosa succede nel Modulo 5

Più volte in questo modulo abbiamo girato attorno a un problema: cosa restituisce find se il giocatore non c’è? Abbiamo usato -1, una convenzione fragile (e se -1 fosse un valore valido?). E list.pop() su una lista vuota? E "12x".to_int()? Nel prossimo modulo arrivano gli optional e la gestione degli errori di C3: un modo per dire “qui potrebbe non esserci un risultato, ed ecco perché”, che il compilatore ti obbliga a gestire. È una delle idee più originali del linguaggio.

Segna questa lezione come completata. Hai costruito un mazzo di carte da zero, e le regole di un gioco vero stanno in trenta righe di metodi.