Zeit für einen Lottogewinn

Lotto spielen kann ja jeder. Eine eigene Lotterie implementieren ist in C++ aber auch nicht wirklich schwieriger.

Video

Quelltext

#include <iostream>
#include <random>
#include <set>
#include <tuple>
#include <algorithm>
#include <iterator>

using namespace std;

typedef tuple<set<unsigned int>, unsigned int> lottozahlen;

lottozahlen readNumbers()
{
    lottozahlen numbers;

    cout << "Bitte 6 Zahlen (mit Leerzeichen getrennt) eingeben.\n";
    while (get<0>(numbers).size() < 6 && cin) {
        unsigned int number;
        cin >> number;
        if (number < 1 || number > 49) {
            cerr << "Zahlen müssen zwischen 1 und 49 liegen. Ignoriere " << 
		    number << "\n";
        }
        else if (get<0>(numbers).find(number) != get<0>(numbers).end()) {
            cerr << "Die Zahl " << number << " ist bereits vorhanden.\n";
        }
        else {
            get<0>(numbers).insert(number);
        }
    }

    cout << "Bitte eine Superzahl zwischen 0 und 9 eingeben.\n";

    do {
        cin >> get<1>(numbers);
        if (get<1>(numbers) > 9) {
            cerr << "Die Superzahl muss zwischen 0 und 9 liegen.\n";
        }
        else {
            break;
        }
    } while (true);

    return numbers;
}

lottozahlen drawNumbers()
{
    lottozahlen numbers;

    random_device dev;
    auto seed = dev();
    cout << "Initialisiere Pseudozufallszahlengenerator mit Seed " << seed << endl;

    default_random_engine engine(seed);
    cout << "Pseudozufallszahlengenerator liefert Werte im Bereich [" << 
	    default_random_engine::min() << ", " << default_random_engine::max() << 
	    "]\n";

    uniform_int_distribution<unsigned int> numbers_dist(1, 49);
    cout << "Ziehe Zahlen\n";
    while (get<0>(numbers).size() < 6) {
        get<0>(numbers).insert(numbers_dist(engine)); // ineffizient!
    }

    uniform_int_distribution<unsigned int> sz_dist(0,9);
    cout << "Ziehe Superzahl\n";
    get<1>(numbers) = sz_dist(engine);

    return numbers;
}

tuple<unsigned int, bool> checkNumbers(lottozahlen user, lottozahlen generated) 
{
    set<unsigned int> matches;
    set_intersection(get<0>(user).begin(), get<0>(user).end(),
                     get<0>(generated).begin(), get<0>(generated).end(),
                     inserter(matches, matches.begin()));
    return make_tuple<unsigned int, bool>(
		  matches.size(), 
		  get<1>(user) == get<1>(generated));
}

ostream &operator<<(ostream &os, lottozahlen const &lz) 
{
    os << "Zahlen: ";
    for (auto elem : get<0>(lz)) {
        os << elem << " ";
    }
    os << "\nSuperzahl: " << get<1>(lz) << endl;
    return os;
}

int main()
{
    auto numbers = readNumbers();
    auto generated = drawNumbers();

    cout << "Eingebene Zahlen:\n";
    cout << numbers;

    cout << "Gezogene Zahlen:\n";
    cout << generated;

    auto result = checkNumbers(numbers, generated);
    cout << get<0>(result) << " Richtige";
    if (get<1>(result)) {
        cout << " + Superzahl";
    }
    cout << endl;

    return 0;
}

Erklärung

C++11 bietet ziemlich flexible Möglichkeiten zur Generierung von Zufallszahlen. Die Arbeit wird dabei geteilt zwischen Engines und Distributions. Die Engines sind Pseudozufallszahlengeneratoren, welche innerhalb eines festen Bereiches zufällige Werte liefern. Im Beispiel oben ist das die default_random_engine (Zeile 55). Diese Objekte repräsentieren einen bestimmten internen Zustand, der nach einer komplexen Ableitungsfunktion verändert wird. Jedesmal, wenn der operator() aufgerufen wird, liefert die Engine einen weiteren Zufallswert und ändert ihren internen Zustand. Gute Pseudozufallszahlengeneratoren sind schnell, liefern ihre Werte nicht vorhersagbar (ohne den internen Zustand zu kennen) und wiederholen sich sehr lang nicht (haben also eine lange Periode). Letztendlich sind sie aber alle deterministisch: kennt man den internen Zustand, kann man die nächste Zahl vorhersagen.

Will man das vermeiden, dann bietet C++11 mit dem random_device ein Objekt an, was eine systemspezifische Quelle für echten Zufall repräsentiert. Unter Unix ist das bspw. das /dev/random-Device, in dem der Betriebssystemkern aus verschiedenen Quellen (Tastenanschläge, Interrupts, Mausbewegungen oder echte Zufallshardware) Entropie sammelt. Dieses Device hat allerdings einen Nachteil: es bietet nur begrenzt echten Zufall und degeneriert dann systemspezifisch zu einem Pseudozufallszahlengenerator. Daher nutzt man es typischerweise nur, um die eigentlichen Engines mit einem zufälligen Startwert (Seed) zu versehen und so Wiederholungen in deren Folgen sehr unwahrscheinlich zu machen.

Jede Engine liefert Zahlen in einem festen Bereich. Der ist mittels der statischen Member min() und max() abfragbar (Zeile 56/57). Außerdem liefern die Engines ihre Werte gleichverteilt. Will man das nun ändern, so kann man eine Verteilungsfunktion auf die Engine aufsetzen. Diese nimmt die Ausgabe der Engine und passt sie anhand ihrer Vorgaben an, bevor sie Werte zurückliefert. Im Beispiel findet die uniform_int_distribution Verwendung (Zeile 60), die eine Gleichverteilung in einem festlegbaren Bereicht bietet. Wir begrenzen den Bereich auf die für Lotto notwendigen Zahlen 1 bis 49. Aus dieser Verteilung können wir dann so lange Zahlen ziehen, bis wir 6 verschiedene zusammen haben. Auf einer Engine können auch mehrere Verteilungen arbeiten ohne sich in die Quere zu kommen. Ist die Periode der Engine im Verhältnis zur Anzahl der Verteilungen lang genug, dann wird man auch keine Muster in der Ausgabe erkennen können.

Die Funktion drawNumbers(), wie auch die Funktion readNumbers() müssen jeweils 6 Zahlen und eine Superzahl zurückliefern. Um nun keine Klasse für diesen Zweck schreiben zu müssen, nehmen wir hier ein Tupel aus zwei Elementen: std::tuple<std::set<unsigned int>, unsigned int>. Das erste Tuple-Feld ist ein Set mit den eigentlichen Zahlen, das zweite Feld ist ein einfacher unsigned integer mit der Superzahl. Mittels make_tuple() (Zeile 79) können wir bequem Tupel anlegen und aus Funktionen zurückliefern. Wollen wir hingegen auf ein bestimmtes Element eines Tupels zugreifen, dann hilft uns die Template-Funktion get<>(). Diese nimmt den Index des Elements als Parameter und leitet dann aus dem übergebenen Tupel den passenden Rückgabetyp ab, um den Zugriff auf das entsprechende Feld zur Verfügung zu stellen. 

Zuguterletzt müssen wir noch prüfen, wieviele Zahlen der Nutzer nun richtig geraten hat (im Video ja nun nicht gerade erfolgreich). Die einfachste Variante dazu ist die Bildung der Schnittmenge zwischen den eingegebenen und den gezogenen Zahlen. Das erledigt std::set_intersection() aus der <algorithm>-Bibliothek. Diese Funktion nimmt zwei Bereiche durch Iteratoren begrenzt und schreibt in einen Ausgabeiterator die Elemente, die in beiden Bereichen vorkommen. Die Funktion erwartet die beiden Bereiche sortiert. Im Beispiel ist das durch std::set automatisch gegeben. Wenn man allerdings vector oder list verwendet, dann muss man selbst sortieren.

Die Ergebnisse der Prüfung (Anzahl Richtige und Superzahl) liefern wir wieder in einem Tupel zurück und geben sie entsprechend aus. Wie im Video zu sehen ist habe ich zwar nichts gewonnen, aber wenigstens kann ich nun kostenlos spielen so oft ich möchte.