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.
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.
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 |
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.
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''.
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.
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.
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!''
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;
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 }
Michael Schilliarbeitet 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. |