Quantenspieler (Linux-Magazin, Dezember 2003)

Die aus der Quantenphysik stammenden Superpositionen werden bald als Junctions Einzug in den Perl-6-Kern halten. Diesem revolutionären Konzept nähern wir uns spielerisch: Anhand eines Skripts, das das in amerikanischen Casinos populäre Blackjack spielt.

Neulich fiel mir in einem Buchladen ein kleines Taschenbuch namens ``Winning Casino Play'' [2] auf, in dem ein ausgebuffter Profispieler erklärt, wie man in den Casinos von Las Vegas und Atlantic City zumindest so spielt, dass man eine faire Chance hat. Blackjack (ähnlich dem deutschen ``Siebzehn und Vier''), nahm dort breiten Raum ein, denn es ist eines der Spiele mit den fairsten Gewinnmöglichkeiten der Casino-Szene.

19, 20, 21 ... busted!

Vor dem Gang ins Casino wollte ich allerdings ein bisschen üben und machte mich daran, ein Perl-Skript zur Simulation zu schreiben. Die Regeln: Der Spieler tritt gegen den ``Dealer'' an, den Casionangestellten, der die Karten ausgibt. Gespielt wird mit mehreren Packen von 52-Blatt-Kartenspielen, die gemischt in einem ``Shoe'' genannten Kartenhalter hängen, aus dem der ``Dealer'' die Karten einzeln und elegant hervorzieht.

Es geht darum, soviele Karten zu ziehen, dass ihre Gesamtaugenzahl möglichst genau 21 erreicht. Aber Vorsicht: Wer 21 überschreitet, dessen ``Hand'' verliert automatisch, sie ist ``busted'' (ruiniert), wie der Amerikaner sagt. Dabei zählen die Karten 2 bis 10 jeweils ihren aufgedruckten Wert und die sogenannten ``Face Cards'' (Bube (Jack), Dame (Queen), König (King)) 10 Punkte. Asse zählen wahlweise 1 oder 11.

Achtung, Superposition!

1 oder 11? Richtig, zieht man eine 7, eine 8 und ein As, wäre man mit 7+8+11=26 eigentlich aus dem Spiel. Statt dessen wertet man das As aber einfach als 1 und ist mit 7+8+1=16 weiterhin im Spiel.

Diese Hand weist also nicht eine feste Punktezahl auf, sondern eine Überlagerung zweier Zustände (26,16), von denen wir den für uns günstigen auswählen: 16. Zöge man statt dessen vier Asse, gäbe es sogar vier Zustände (4,14,24,34). Da mit 24 oder 34 freilich kein Blumentopf zu gewinnen ist (``busted''), interessieren davon aber nur zwei: 4, der soft count, und 14, der sogenannte hard count.

Diese Überlagerung mehrerer Zustände nennt man in der Quantenphysik Superposition. Dort kann sich ein Teilchen zum Beispiel zugleich an mehreren Orten aufhalten.

Ist Damian Conways Modul Quantum::Superpositions (erhältlich vom CPAN) installiert, erzeugt man eine Superposition der vorher gezeigten numerischen Werte einfach mit der Funktion any():

    use Quantum::Superpositions;
    my $count = any(4,14,24,34);

Ab diesem Zeitpunkt führt die Variable $count scheinbar vier verschiedene Werte. Ein scheinbar verrückter logischer Ausdruck wie in

    if($count == 4 and 
       $count == 14) {
        print "Stimmt!\n";
    }

liefert einen wahren Wert und der print-Befehl wird ausgeführt. Außerdem liefern logische Vergleiche wie $count <= 21 im Wirkungskreis von Quantum::Superpositions nicht mehr nur wahr oder falsch zurück, sondern eine Superposition der Zustände, die der Bedingung entsprechen. Um aus (4,14,24,34) die irrelevanten Zustände über 21 herauszufiltern, genügt ein einfaches

    $count = ($count <= 21);

und schon steht in $count nur noch any(4,14). Das spart Tipparbeit! Auch arithmetische Operationen auf Superpositionen schreiben sich sehr einfach, da Quantum::Superpositions alle Operatoren überlädt. Steht in $counts der Wert any(4,14), macht

    $counts += 10;

daraus ohne viel Aufhebens any(14,24).

any() bestimmt also eine sogenannte disjunktive Superposition, die man auf jeden ihrer Zustände abklopfen kann und die jede diesbezüglich ankommende logische Abfrage bejaht.

Die zweite von Quantum::Superpositions eingeführte Funktion, all(), legt hingegen fest, dass eine sogenannte konjunktive Superposition alle angegebenen Zustände zugleich führt. Folgende Bedingung ist nicht erfüllt:

    my $count = all(4,14,24,34);
    
    if($count <= 21) {
        print "None busted\n";
    }

denn nicht alle der angegebenen Zustände weisen einen Wert unter 21 auf. all(4,14) <= 21 wäre hingegen wahr gewesen.

Abbildung 1: Von der Kommandozeile aus: Eine Runde Blackjack

Ist die Katze tot?

Prüft man quantenphysikalische Wahrheiten allerdings nach, ist der Spuk vorrüber und mehrere Zustände kollabieren sofort in einen einzigen: Schrödingers Katze ([3]) ist dann tot. Anders in Perl: Dort kann man beliebig herumstochern ohne das System zu zerstören. Um herauszufinden, welche Zustände eine Superposition aufweist, importiert Quantum::Superpositions die Funktion eigenstates(), die einfach eine Liste mit den Zuständen zurückgibt:

    my @counts = eigenstates($count);

Mit den drei Funktionen any(), all() und eigenstates() lassen sich atemraubende Programmkonstrukte formen. Um zum Beispiel den soft count der beschriebenen Karten, also das Minimum aus any(4,14,24,34) zu bestimmen, reicht:

    my $counts = any(4,14,24,34);
    my $soft = 
     ($counts <= all(eigenstates($counts));

denn der logische Vergleich liefert eine Superposition aller Zustände in $counts, die kleiner oder gleich als alle Zustände der Superposition sind -- die klassische Definition von Minimum.

Perlklasse Kartenschuh

Zur Implementierung: Listing Blackjack.pm enthält zwei Klassen: Blackjack::Shoe, die den Kartenschuh abstrahiert, aus dem der Dealer die Karten auf den Tisch zieht und Blackjack::Hand, die eine Spielkarten-``Hand'' repräsentiert, also entweder die Karten des Spielers oder die des Dealers.

Der Kartenschuh nutzt das Modul Algorithm::GenerateSequence vom CPAN, um ein paar Packen von Kartenspielen zu generieren. Dessen Konstruktor new() nimmt Referenzen auf Arrays entgegen, deren Elemente er miteinander kombiniert. Enthält der erste Array alle Farben (Heart/Diamond/Spade/Club) und der zweite alle Werte (A 2 3 4 5 6 7 8 9 10 J Q K) des Kartenspiels, liefert die as_list()-Methode eine Liste von Kombinationen, die den einzelnen Karten entsprechen: Heart A (Herz As), Heart 2, ... Club Q (Pik Dame), Club K (Pik König). Die entstehende Liste repliziert der nachfolgenden x-Operator mit der Anzahl der gewünschten Kartenspiele im Schuh (Zeile 36).

Das Modul Algorithm::Numerical::Shuffle schließlich exportiert die shuffle-Methode, die die Elemente eines als Referenz übergebenen Arrays nach dem Fisher-Yates-Verfahren durcheinanderwirbelt. Die reshuffle()-Methode des Blackjack::Shoe-Objekts stopft eine in der Instanzvariablen nof_decks definierte Anzahl von 52-Blatt-Kartenspielen in den Schuh.

remaining() liefert die Anzahl der im Schuh verbleibenden Karten zurück -- eine Möglichkeit für den Dealer, sicherzustellen, dass er das Spiel mit den restlichen Packen bestreiten kann. draw_card() zieht eine Karte aus dem Schuh und liefert sie als Refernz auf einen Array zurück, der als erstes Element die Farbe (Heart, Diamond, Spade, Club) und als zweites den aufgedruckten Wert (A, 2, 3, ..., J, Q, K) enthält.

Die Klasse Blackjack::Hand repräsentiert das Prinzip eines Mitspielers, der eine Reihe von Karten hält. Sie befasst sich mit dem Ziehen von Karten aus dem Schuh und deren Bewertung -- egal ob der Spieler oder der Dealer die entsprechenden Karten hält. Der Konstruktor

    Blackjack::Hand->new(shoe => $shoe)

verbindet den Spielteilnehmer mit dem Karten-Schuh, aus dem neue Karten ins Spiel kommen. Die draw()-Methode befördert jeweils eine Karte in die ``Hand''.

Punkte zählen mit Superpositionen

Die count()-Methode ab Zeile 83 zählt die Augen einer ``Hand'' und gibt das Ergebnis wahlweise als Superposition, als hard count ($hand->count("hard")) oder als soft count $hand->count("soft") zurück.

Dafür iteriert die for-Schleife ab Zeile 89 durch die Karten, prüft die Einzelwerte und addiert sie auf. Die Variable $count speichert dabei die Superposition möglicher ``Hand''-Werte und nutzt die weiter oben erwähnte Tatsache, dass Quantum::Superpositions den +-Operator überladen hat, sodass $counts += 10 einfach alle Superpositionen in $counts um 10 erhöht. Ein As hingegen verdoppelt die Anzahl der Superpositionen und zählt zu einer Hälfte 1, zur anderen 11 hinzu:

    $counts = any($counts+1, $counts+11);

Zeile 100 entfernt daraus sofort alle 21 überschreitenden Werte. Falls die Superposition danach keine Werte mehr führt, eigenstates($counts) also eine leere Liste zurückgibt, hat der Spieler endgültig die 21 überschritten und das Blatt ist wertlos (busted). Ab Zeile 107 ermittelt die count()-Methode dann noch hard- und soft count mit den oben gezeigten Tricks zur Minimums- bzw. Maximumsbestimmung. Die int()-Funktion befreit die Superposition von ihrem Spezialgebahren und macht einen Skalar daraus.

Blackjack gewinnt

Falls das Blatt eines Spielers genau ein As und eine 10 Punkte zählende Karte enthält, zählt es als ``Blackjack'' und sticht alle anderen 21 zählenden Blätter aus. Die ab Zeile 119 definierte Methode blackjack() ermittelt diese Situation, indem sie prüft, ob der Blattwert als Superposition sowohl 11 als auch 21 ist und ob das Blatt aus genau zwei Karten besteht.

Die score()-Methode ermittelt Gewinn oder Verlust eines Blattes gegen das Blackjack::Hand-Objekt des Dealers, das sie als Parameter entgegennimmt. Das Ergebnis ist bei Verlust negativ, bei Gewinn positiv. Und noch einige Besonderheiten sind zu beachten: Ein Blackjack eines Spielers zählt 1:1.5, während ein Dealer mit einem Blackjack nur den einfachen Einsatz des Spielers einkassiert. Und überschreitet der Spieler 21, ist sein Blatt ``Busted'' und er verliert, auch wenn der Dealer später 21 überschreitet.

Textfarben und Tastenfeuer

Listing blackjack zeigt ein Skript, mit dem man fast wie in Las Vegas gegen einen Computer-Dealer Blackjack spielen kann. Es nutzt die beiden in Blackjack.pm definierten Klassen für den Kartenschuh des Dealers und die beiden Blätter von Spieler und Dealer. Zur farbigen Textausgabe kommt Text::ANSIColor zum Einsatz, welches wegen des überrreichten Tags :constants Konstanten wie BOLD, RED, BLUE oder RESET exportiert, hinter denen sich die Terminal-Escape-Sequenzen verstecken, um die Textausgabe zu verdicken, farblich zu verschönern, oder in den Normalmodus zurückzusetzen.

Text::ReadKey erlaubt es im Raw Mode (eingeleitet mit ReadMode 4), die Werte gedrückter Tasten einzufangen, ohne dass der Benutzer die Enter-Taste bedienen muss. Ein anschließend abgesetztes ReadMode 0 lässt das Terminal wieder in den Cooked Mode zurückspringen, in dem nur ganze Zeilen eingelesen werden, wenn der Benutzer auf Enter klopft und bis dahin bei gedrückten Tasten keine Rückmeldung ans Programm erfolgt.

Ist der Benutzer am Zug, hat er laut der Anzeige

    [H]it/[S]tand/[Q]uit

die Wahl zwischen Hit (eine weitere Karte ziehen), Stand (weitere Karten abzulehnen) und Quit (das Spiel abzubrechen), indem er eine der Tasten H, S oder Q antippt.

Abbildung 1 zeigt einen typischen Spielverlauf. Der Dealer beginnt das Spiel und zieht zwei Karten, von denen er allerdings nur eine aufdeckt. Dann erhält der Spieler zwei Karten und darf neue Karten anfordern, falls ihm die Augenzahl seines Blatts noch zu gering erscheint. Lehnt er ab, beginnt der Dealer nach folgendem, fest vorgegebenen Verfahren sein eigenes Blatt zu spielen: Ist der gesamte soft count unter 17, zieht er eine neue Karte. Bei 17 oder mehr stoppt er (unabhängig vom Blatt des Spielers) und zahlt Gewinne aus oder sackt Spielerverluste ein.

Wer Lust hat, darf Blackjack.pm nutzen, um ein Spiel mit graphischer Schnittstelle oder einen TCP/IP-Server zu schreiben, der über's Netz Blackjack spielt. Viel Spaß damit, oder wie man in Las Vegas sagt: ``Good Luck!''

Listing 1: Blackjack.pm

    001 ###########################################
    002 # Blackjack.pm
    003 # Mike Schilli, 2003 (m@perlmeister.com)
    004 ###########################################
    005 use warnings; use strict;
    006 
    007 #==========================================
    008 package Blackjack::Shoe; #=================
    009 #==========================================
    010 
    011 use Algorithm::GenerateSequence;
    012 use Algorithm::Numerical::Shuffle 
    013     qw(shuffle);
    014 
    015 ###########################################
    016 sub new {
    017 ###########################################
    018     my($class, @options) = @_;
    019 
    020     my $self = {nof_decks => 1, @options};
    021 
    022     bless $self, $class;
    023     $self->reshuffle();
    024     return $self;
    025 }
    026 
    027 ###########################################
    028 sub reshuffle {
    029 ###########################################
    030     my($self) = @_;
    031 
    032     my @cards = 
    033       (Algorithm::GenerateSequence->new(
    034        [qw( Heart Diamond Spade Club )],
    035        [qw( A 2 3 4 5 6 7 8 9 10 J Q K )])
    036        ->as_list()) x $self->{nof_decks};
    037 
    038     $self->{cards} = shuffle \@cards;
    039 }
    040 
    041 ###########################################
    042 sub remaining {
    043 ###########################################
    044     my($self) = @_;
    045 
    046     return scalar @{$self->{cards}};
    047 }
    048 
    049 ###########################################
    050 sub draw_card {
    051 ###########################################
    052     my($self) = @_;
    053 
    054     return shift @{$self->{cards}};
    055 }
    056 
    057 #==========================================
    058 package Blackjack::Hand; #=================
    059 #==========================================
    060 use Quantum::Superpositions;
    061 use Log::Log4perl qw(:easy);
    062 
    063 ###########################################
    064 sub new {
    065 ###########################################
    066     my($class, @options) = @_;
    067 
    068     my $self = { cards => [], @options };
    069 
    070     die "No shoe" if !exists $self->{shoe};
    071     bless $self, $class;
    072 }
    073 
    074 ###########################################
    075 sub draw {
    076 ###########################################
    077     my($self) = @_;
    078 
    079     push @{$self->{cards}}, 
    080          $self->{shoe}->draw_card();
    081 }
    082 
    083 ###########################################
    084 sub count {
    085 ###########################################
    086     my($self, $how) = @_;
    087 
    088     my $counts = any(0);
    089 
    090     for(@{$self->{cards}}) {
    091         if($_->[1] =~ /\d/) {
    092             $counts += $_->[1];
    093         } elsif($_->[1] eq 'A') {
    094             $counts = any($counts+1, 
    095                           $counts+11);
    096         } else {
    097             $counts += 10;
    098         }
    099     }
    100 
    101     DEBUG "counts(before)=$counts";
    102 
    103        # Delete busted hands
    104     $counts = ($counts <= 21);
    105                              
    106     DEBUG "counts(after)=$counts";
    107 
    108        # Busted!!
    109     return undef if ! eigenstates($counts);
    110 
    111     return $counts unless defined $how;
    112 
    113     if($how eq "hard") {
    114             # Return minimum
    115         return int($counts <= 
    116                all(eigenstates($counts)));
    117     } elsif($how eq "soft") {
    118             # Return maxium
    119         return int($counts >= 
    120                all(eigenstates($counts)));
    121     }
    122 }
    123 
    124 ###########################################
    125 sub blackjack {
    126 ###########################################
    127     my($self) = @_;
    128 
    129     my $c = $self->count();
    130 
    131     return 1 if $c == 21 and $c == 11 and 
    132              @{$self->{cards}} == 2;
    133     return 0;
    134 }
    135 
    136 ###########################################
    137 sub as_string {
    138 ###########################################
    139     my($self) = @_;
    140 
    141     return "[" . join(',', map({ "@$_" } 
    142                 @{$self->{cards}})) .  "]";
    143 }
    144 
    145 ###########################################
    146 sub count_as_string {
    147 ###########################################
    148     my($self) = @_;
    149 
    150     return $self->busted() ?
    151      "Busted" : $self->blackjack() ?
    152      "Blackjack" : $self->count("soft");
    153 }
    154 
    155 ###########################################
    156 sub busted {
    157 ###########################################
    158     my($self) = @_;
    159 
    160     return ! defined $self->count();
    161 }
    162 
    163 ###########################################
    164 sub score {
    165 ###########################################
    166     my($self, $dealer) = @_;
    167 
    168     return -1 if $self->busted();
    169 
    170     return 1 if $dealer->busted();
    171 
    172     return 0 if $self->blackjack() and
    173                 $dealer->blackjack();
    174 
    175     return 1.5 if $self->blackjack();
    176 
    177     return -1 if $dealer->blackjack();
    178 
    179     return $self->count("soft") <=>
    180            $dealer->count("soft");
    181 }
    182 
    183 1;

Listing 2: blackjack

    01 #!/usr/bin/perl
    02 ###########################################
    03 # play - Blackjack against Las Vegas Dealer
    04 # Mike Schilli, 2003 (m@perlmeister.com)
    05 ###########################################
    06 use warnings; use strict;
    07 
    08 use Blackjack;
    09 use Term::ANSIColor qw(:constants);
    10 use Term::ReadKey;
    11 
    12 $| = 1; my $total = 0;
    13 
    14 my $shoe = Blackjack::Shoe->new(
    15                            nof_decks => 4);
    16 {
    17   if($shoe->remaining() < 52) {
    18     print "Shuffling ...\n";
    19     $shoe->reshuffle();
    20   }
    21 
    22   my $player = Blackjack::Hand->new(
    23              shoe => $shoe);
    24   my $dealer = Blackjack::Hand->new(
    25              shoe => $shoe);
    26 
    27   $dealer->draw();
    28   P(RED, "D", $dealer);
    29   $dealer->draw();
    30 
    31   $player->draw();
    32   $player->draw();
    33 
    34   while(!$player->busted()) {
    35     P(BLUE, "P", $player);
    36     print "([H]it/[S]tand/[Q]uit) ";
    37     ReadMode 4;
    38     my $move = ReadKey(0);
    39     ReadMode 0;
    40     print "\r";
    41     last if $move =~ /^s/i;
    42     exit 0 if $move =~ /^q/i;
    43     $player->draw();
    44   }
    45 
    46   P(BLUE, "P", $player);
    47 
    48   while(!$dealer->busted() and 
    49         $dealer->count("soft") < 17) {
    50     P(RED, "D", $dealer);
    51     $dealer->draw();
    52   }
    53 
    54   P(RED,  "D", $dealer);
    55 
    56   $total += $player->score($dealer);
    57 
    58   print "Score: ", 
    59         $player->score($dealer), 
    60         ", Total: ", $total, "\n\n";
    61 
    62   redo;
    63 }
    64 
    65 sub P { # Print status in color
    66     print(BOLD, $_[0], "$_[1]", "[",
    67     $_[2]->count_as_string(), "]",
    68     RESET, ": ", $_[2]->as_string(), "\n")
    69 }

Infos

[1]
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2003/12/Perl

[2]
Avery Cardoza, ``Winning Casino Play'', Cardoza Publishing, 3rd Ed., 2003, ISBN 1-58042-090-7

[3]
Illustration des Gedankenexperiments Schrödingers mit seiner Katze: http://mist.npl.washington.edu/npl/int_rep/tiqm/TI_fig_09.html

Michael Schilli

arbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com.