Der Java-Kurs: Codeknacker
Willemers Informatik-Ecke
Arrays Kurs Code-Knacker mit GUI

Bevor Sie hier weiterarbeiten, sollten Sie die Grundkenntnisse in Java verstanden haben und zumindest bis zu den Arrays gekommen sein. Falls dem nicht so ist, sollten Sie den Java-Kurs durcharbeiten.

Spielregeln

Der Computer denkt sich eine zufällige vierstellige Zahl aus, deren Ziffern zwischen 1 und 6 liegen. Der Spieler versucht die Zahl zu raten, indem er immer wieder eine vierstellige Zahl eintippt.

Damit der Spieler nicht alle 10.000 denkbaren Kombinationen durchprobieren muss, erhält er Tipps vom Computer in Form von zwei Zahlen:

Mit diesen Hilfen an der Hand versucht der Spieler, sich dem richtigen Geheimcode zu nähern. Hat er die richtige Kombination gefunden, endet das Spiel.

Geheimzahlen erstellen und einmal Raten

Im ersten Anlauf wird ein Array geheim mit zufälligen Werten zwischen 1 und 6 erstellt.

Anschließend lassen wir den Spieler eine vierstellige Zahl eintippen. Wir testen weder, ob es wirklich vier Stellen sind, noch ob es auch alles Ziffern sind.

Nun soll das Programm prüfen, ob ein Volltreffer da ist. Dazu benötigt man eine Schleife, die durch alle vier Positionen geht und zählt, wenn es Übereinstimmungen gibt.

Bevor Sie die Lösung eintippen, sollten Sie erst einmal selbst versuchen. Man lernt Programmieren nur dadurch, dass man es tut.

Die Berechnung einer zufälligen Zahl errechnet sich durch die Bibliotheksfunktion nextInt, die in dem Paket java.util und dort in der Klasse Random steckt.

Für die Eingabe befindet sich im selben Paket die Klasse Scanner. Die Methode nextLine liefert eine komplette Eingabezeile als String.

package code;

public class CodeKnacker {

    public static void main(String[] args) {
        int geheim[] = new int[4];
        java.util.Random zufall = new java.util.Random();
        java.util.Scanner input = new java.util.Scanner(System.in);
        for (int i=0; i<4; i++) {
            geheim[i] = zufall.nextInt(6) + 1;
            System.out.print(geheim[i]); // debug
        }
        int treffer = 0;
        System.out.println("Gebe vierstelligen Code mit Ziffern zwischen 1 und 6 ein:");
        String eingabe = input.nextLine();
        for (int i=0; i<4; i++) {
            if (eingabe.charAt(i)-'0' == geheim[i]) {
                treffer++;
            }
        }
        System.out.println(treffer);
    }
}
Die Schleife prüft, ob die Ziffern von Geheimzahl und Rateversuch exakt an einer Stelle übereinstimmen. Dann wird die Variable treffer hochgezählt. Nach Ende der Schleife wird die Zahl der Treffer ausgegeben.

Ziffern finden, die nicht an der gleichen Position sind

Wir versuchen nun, die Ziffern zu finden, die zwar im Geheimcode enthalten sind, aber nicht an der richtigen Stelle.

Alle mit allen vergleichen

Dazu müssen wir jedes Feld der Eingabe mit jedem Feld der Geheimzahl vergleichen. In solchen Fällen müssen zwei Schleifen ineinander verschachtelt werden.

for (int i=0; i<4; i++) {
    for (int g=0; g<4; g++) {
        if (eingabe.charAt(i)-'0' == geheim[g]) {
            drin++;
        }
    }
}
Natürlich zählen wir dabei auch die Ziffern mit, die wir schon als Treffer gezählt haben. Das ist leicht zu regeln, indem man einfach die Treffer abzieht.
drin = drin - treffer
Aber ganz richtig ist das Ergebnis nicht. Es krankt an dem Auftreten von doppelten Ziffern. Wenn man beispielsweise im Rateversuch vier gleiche Ziffern eintippt und diese nur einmal im Geheimcode vorkommt, werden für alle vier je eine Übereinstimmung gezählt.

Bei einem Versuch war die Geheimzahl 5536. Der Rateversuch 3354 lieferte vier Treffer an nicht exakter Stelle, obwohl es zwei sind. Sowohl die doppelte 5 in der Geheimzahl als auch die doppelte 3 im Rateversuch werden doppelt gezählt.

Korrektes Zählen der Übereinstimmungen

Spielen wir das Problem noch einmal durch:

Geheimcode 3 1 4 2
Rateversuch 1 5 1 6

An der ersten und in der dritten Stelle des Rateversuchs würde die Überprüfung eine Übereinstimmung mit der zweiten Stelle in der Geheimzahl auftreten.

Es gibt aber noch eine Problemstellung, die das folgende Szenario aufzeigt.

Geheimcode 1 5 1 6
Rateversuch 3 1 4 2

Hier würde der umgekehrte Fall eintreten, dass der zweite Rateversuch die erste und dritte Ziffer des Geheimcodes findet und diese dann doppelt zählt.

Vielleicht sollten Sie nun schon einmal knobeln, ob Ihnen eine Lösung einfällt.

Der Lösungsansatz wäre, dass bei einer Übereinstimmung der gefundenen Wert im Geheimcode gelöscht wird, damit er nicht in einer weiteren Runde noch einmal gezählt wird, beispielsweise mit einer negativen Zahl. Dasselbe sollte im Rateversuch passieren, sinnvollerweise mit einer anderen negativen Zahl, damit nicht doch noch eine Übereinstimmung der negativen Zahlen gefunden wird.

drin = 0;
int[] rate = new int[4];
for (int i=0; i<4; i++) {
    rate[i] = eingabe.charAt(i)-'0';
}

for (int r=0; r<4; r++) {
    for (int g=0; g<4; g++) {
        if (rate[r] == geheim[g]) {
            drin++;
            rate[r] = -1;
            geheim[g] = -2;
        }
    }
}
drin = drin - treffer;
System.out.print(treffer);
System.out.print(":");
System.out.println(drin);
Hier ist nun rate nicht mehr ein String, sondern eine Liste, die wir aus dem Eingabestring generieren, weil man im String keine Elemente verändern kann.

Wiederholen bis zum geknackten Code

Dem Spieler sollen mehrere Runden gegönnt werden, bis er den Code findet. Dazu muss eine große while-Schleife über die Eingabe und die anschließende Prüfung gelegt werden.

Dabei stellen wir fest, dass wir beim zweiten Raten nicht den durch die Markierungen veränderten Geheimcode zum Raten anbieten können. Wir müssen also in jeder Runde den Originalcode einmal kopieren, damit jede Runde mit der gleichen Umgebung beginnt.

package code;

public class CodeKnacker {

    public static void main(String[] args) {
        int geheimX[] = new int[4];
        java.util.Random zufall = new java.util.Random();
        java.util.Scanner input = new java.util.Scanner(System.in);
        for (int i = 0; i < 4; i++) {
            geheimX[i] = zufall.nextInt(6) + 1;
            System.out.print(geheimX[i]); // debug
        }
        int treffer = 0;
        while (treffer < 4) {

            // Wir benötigen eine Kopie von geheimX
            int geheim[] = new int[4];
            for (int i = 0; i < 4; i++) {
                geheim[i] = geheimX[i];
            }

            int drin = 0;
            System.out.println("Gebe vierstelligen Code mit Ziffern zwischen 1 und 6 ein:");
            String eingabe = input.nextLine();
            // Eine Kopie vom Rateversuch in einem int-Array
            int[] rate = new int[4];
            for (int i = 0; i < 4; i++) {
                rate[i] = eingabe.charAt(i) - '0';
            }
            // Treffersuche
            for (int i = 0; i < 4; i++) {
                if (rate[i] == geheim[i]) {
                    treffer++;
                }
            }
            // Vergleich aller mit allen
            for (int r = 0; r < 4; r++) {
                for (int g = 0; g < 4; g++) {
                    if (rate[r] == geheim[g]) {
                        drin++;
                        rate[r] = -1;
                        geheim[g] = -2;
                    }
                }
            }
            drin = drin - treffer;
            System.out.print(treffer);
            System.out.print(":");
            System.out.println(drin);
        }
        System.out.println("Gewonnen");
    }
}

Realisierung mit Funktionen

Methoden beziehungsweise Funktionen ermöglichen uns die Zusammenfassung von Anweisungen. Dadurch kann zunächst geschrieben werden, welche Funktionalitäten das Programm hat. Diesen Ansatz nennt man Top-Down-Programmierung.

In Java als streng objektorientierter Programmiersprache unterscheidet man Funktionen und Methoden sehr genau. Methoden werden über ein Objekt gerufen. Funktionen sind unabhängig von einem Objekt. Sie sind daran erkennbar, dass ihnen das Schlüsselwort static vorangestellt wird.

Das Spiel läuft in einer Schleife, bis alle vier Ziffern geraten wurden. In dieser Schleife gibt der Spieler einen Versuch ein. Der Versuch wird auf Übereinstimmungen mit der Geheimzahl verglichen. In jeder Runde wird dem Spieler der Erfolg seiner Bemühungen angezeigt.

geheimZahlZiehen();
int treffer = 0;
while (treffer < 4) {
    input("Dein Versuch: ")
    vergleiche();
    zeigeErgebnis();
}
System.out.println("Gewonnen");
Es fehlen die Daten, die von den Funktionen durchgereicht werden. Wir ergänzen die gezogene Geheimzahl und den vom Spieler angegebene Versuch, sowie das Ergebnis der Suche.

int[] geheim = geheimZahlZiehen();
int treffer = 0;
while (treffer < 4) {
    String ratestr = input("Dein Versuch: ");
    Ergebnis ergebnis = vergleiche(geheim, ratestr);
    zeigeErgebnis(ergebnis);
    treffer = ergebnis.treffer;
}
System.out.println("Gewonnen");
Etwas putzig sieht der Datentyp Ergebnis aus. Wir kommen noch darauf.

Geheime Zahl ziehen

Anschließend müssen die Funktionen definiert werden. Den korrekten Rahmen einer Funktion kann man getrost der IDE Eclipse überlassen. Derzeit dürfte geheimZahlZiehen rot unterkringelt sein. Nun schiebt man die Maus über den unterkringelten Bezeichner und wartet einen Moment. Alternativ kann man auch auf den roten Kreis links der Zeile klicken. In beiden Fällen schlägt Eclipse eine Lösung vor:
 Create method geheimZahlZiehen
Der Text ist ein Link. Auf diesen klicken Sie und schon entsteht die Methode mit allen Parametern. Nur Ausfüllen muss man noch selbst.

Das Ziehen der Geheimzahl erzeugt eine Liste von vier Zahlen zwischen 1 und 6. Wir erzeugen ein Array und füllen es mit der Methode nextInt aus der Klasse Random. Das neue Array geben wir per return zurück.

private static int[] geheimZahlZiehen() {
    int geheim[] = new int[4];
    java.util.Random zufall = new java.util.Random();
    for (int i = 0; i < 4; i++) {
        geheim[i] = zufall.nextInt(6) + 1;
        System.out.print(geheim[i]); // debug
    }
    return geheim;
}
Die Funktion geheimZahlZiehen ist nicht besonders komplex, zumindest überschaubar. Wir können uns nun der nächsten Teilaufgabe zuwenden.

Eingabe

Für die Eingabe gibt es in Java keinen so einfachen Befehl. Der Zugriff auf die Bibliotheksfunktionen sieht etwas abschreckend aus. Verstecken wir sie doch in der Funktion input.
private static String input(String prompt) {
    java.util.Scanner input = new java.util.Scanner(System.in);
    System.out.print(prompt);
    String eingabe = input.nextLine();
    return eingabe;
}

Der Typ Ergebnis

Der Typ für das Ergebnis ist erforderlich, weil Java nur einen Rückgabewert für eine Funktion zulässt. Wir haben aber zwei. Beides sind ganzzahlige Werte. Man könnte sich mit einem Array von int behelfen. Aber dann muss man sich merken, welches treffer und welches drin ist.

Besser ist da eine einfache Klasse. Wir bitten wiederum Eclipse, uns die benötigte Klasse zu erstellen, indem wir auf die Fehlermeldung klicken. Es entsteht eine neue Datei mit dem Namen Ergebnis.java.

package code;

public class Ergebnis {

}
Da tragen wir einfach die Werte treffer und drin ein und initialisieren sie gerade bei der Gelegenheit mit 0.
package code;

public class Ergebnis {
    int treffer = 0;
    int drin = 0;
}
Nun kann man über ein Objekt der Klasse Ergebnis auf die beiden Variablen mit ihren Namen zugreifen. Man muss sich nicht merken, an welcher Stelle sie in einem Array stehen. Außerdem kann man später weitere Werte beliebiger Typen einbauen.

Auswertung des Rateversuchs

Die Auswertung des Rateversuchs erfolgt in der Funktion vergleiche und das ist schon komplizierter. Im ersten Schritt zerlegen wir die Aufgabe in den exakten Vergleich an der aktuellen Position und den schwierigeren Part, zu prüfen, ob Übereinstimmungen an anderer Stelle sind.

Für die spätere Rückgabe müssen wir ein Objekt der Klasse Ergebnis anlegen. Das funktioniert ganz ähnlich wie beim Anlegen eines Arrays. Anschließend füllen wir die Elemente nacheinander mit zwei Funktionen, die noch zu schreiben sind.

private static Ergebnis vergleiche(int[] geheim, String ratestr) {
    Ergebnis e = new Ergebnis();
    e.treffer = vergleicheExakt(geheim, ratestr);
    e.drin = vergleicheDrin(geheim, ratestr);
    return e;
}
Beim ermitteln der Übereinstimmung an anderer Position kommt es zu dem Problem, dass Zahlen eventuell doppelt gezählt werden. In der vorigen Version haben wir das Problem gelöst, indem wir gefundene Elemente markiert haben. Das werden wir auch hier anwenden. Allerdings müssen wir dazu eine Kopie der Geheimzahl verwenden, damit wir sie in der nächsten Runde noch zur Verfügung haben. Auch die geratene Zahl muss markiert werden. Hier ist die Ausgangslage anders, weil es sich um einen String handelt, der nicht so leicht verändert werden kann. Also erzeugen wir aus dem String eine Liste. Es kommen also zwei weitere Funktionsaufrufe in die Funktion vergleiche.
private static Ergebnis vergleiche(int[] geheim, String ratestr) {
    int[] geheimX = kopiereGeheimzahl(geheim);
    int[] rateListe = stringToListe(ratestr);
    Ergebnis e = new Ergebnis();
    e.treffer = vergleicheExakt(geheimX, rateListe);
    e.drin = vergleicheDrin(geheimX, rateListe);
    return e;
}
Das Kopieren der Gemeinzahl kann nicht durch eine einfache Zuweisung geschehen, weil sonst nur ein zusätzlicher Verweis entstehen würde. Es muss explizit eine neue Liste entstehen.
private static int[] kopiereGeheimzahl(int[] geheim) {
    int[] kopie = new int[4];
    for (int i=0; i<4; i++) {
        kopie[i] = geheim[i];
    }
    return kopie;
}
Um aus dem String des Rateversuchs eine Liste zu machen, durchläuft stringToListe den String und erzeugt jeweils ein neues Listenelement. Dabei konvertiert es den Buchstaben zu der entsprechenden Zahl.
private static int[] stringToListe(String ratestr) {
    int[] rate = new int[4];
    for (int i=0; i<4; i++) {
        rate[i] = ratestr.charAt(i) - '0';
    }
    return rate;
}

Vergleiche

Der Vergleich auf einen echten Treffer ist noch recht einfach. Die Funktion durchläuft in einer Schleife die beiden Arrays und zählt, wie oft eine Übereinstimmung vorliegt. Das besondere Problem ist, dass die gefundenen Treffer bei der Überprüfung des Enthaltenseins nicht ein weiteres Mal gezählt werden dürfen. Darum ersetzt man sowohl den Rateversuch als auch die Geheimzahl so, dass der Wert nicht wieder passt. Darum wird beim Raten -1 und bei der Geheimzahl -2 verwendet.

Da Listen per Referenz an Funktion übergeben werden, sind die Änderungen der Liste beim Aufrufer auch gültig. Das ist in diesem Fall sehr praktisch, weil die Listen ja später genau so an vergleicheDrin übergeben werden.

private static int vergleicheExakt(int[] geheim, int[] rateListe) {
    int treffer = 0;
    for (int i=0; i<4; i++) {
        if (rateListe[i]==geheim[i]) {
            treffer++;
            rateListe[i] = -1;
            geheim[i] = -2;
        }
    }
    return treffer;
}
Beim Vergleich, ob eine geratene Ziffer an irgendeine anderen Stelle der Geheimzahl auftaucht, benötigt man zwei ineinander geschachtelte Schleifen. So wird jede mit jeder verglichen. Tritt eine Übereinstimmung auf, wird diese gezählt. Ein direkter Treffer kann es nicht sein, weil sie bereits markiert wurden. Damit die gefundene Übereinstimmung nicht noch einmal gezählt wird, wird sie auch hier markiert.
private static int vergleicheDrin(int[] geheim, int[] rateListe) {
    int drin = 0;
    for (int r=0; r<4; r++) {
        for (int g=0; g<4; g++) {
            if (rateListe[r] == geheim[g]) {
                drin++;
                rateListe[r] = -1;
                geheim[g] = -2;
            }
        }
    }
    return drin;
}

Ergebnis anzeigen

Um das Ergebnis anzuzeigen, wird eine eigene Funktion geschrieben, die die Bestandteile eines Objekts der Klasse Ergebnis der Funktion System.out.println zuführt.
private static void zeigeErgebnis(Ergebnis ergebnis) {
    System.out.print(ergebnis.treffer);
    System.out.print(":");
    System.out.println(ergebnis.drin);
}

Zusammenfassung

Zu guter Letzt soll das Programm noch einmal als Ganzes gezeigt werden. Jede der Funktionen ist nur wenige Zeilen lang und dadurch ist die Komplexität gesunken. Wenn sprechende Namen verwendet werden, ist das Zusammenspiel recht übersichtlich. Wenn Ihnen an der einen oder anderen Stelle etwas kompliziert vorkam, ist dies genau die Stelle, wo Sie unbedingt einen Kommentar einfügen sollten, damit Sie das Listing beim nächsten Mal flüssig lesen können.

Die Zahlenliterale wie 4, 6, -1 oder -2 sollten so in einem Programm nicht erscheinen, sondern durch Konstanten ersetzt werden. Das final steht dafür, dass diese im Programmcode nicht einfach verändert werden kann und das static sorgt dafür, dass sie von überall zugreifbar sind.

package code;

public class CodeKnackerFkt {

    static final int CODEZAHLEN = 4;
    static final int CODEMAX = 6;
    static final int MARKRATE = -1;
    static final int MARKGEHEIM = -2;
    
    public static void main(String[] args) {
        int[] geheim = geheimZahlZiehen();
        int treffer = 0;
        while (treffer < CODEZAHLEN) {
            String ratestr = input("Dein Versuch: ");
            Ergebnis ergebnis = vergleiche(geheim, ratestr);
            zeigeErgebnis(ergebnis);
            treffer = ergebnis.treffer;
        }
        System.out.println("Gewonnen");        
    }

    private static void zeigeErgebnis(Ergebnis ergebnis) {
        System.out.print(ergebnis.treffer);
        System.out.print(":");
        System.out.println(ergebnis.drin);
    }

    private static Ergebnis vergleiche(int[] geheim, String ratestr) {
        int[] geheimX = kopiereGeheimzahl(geheim);
        int[] rateListe = stringToListe(ratestr);
        Ergebnis e = new Ergebnis();
        e.treffer = vergleicheExakt(geheimX, rateListe);
        e.drin = vergleicheDrin(geheimX, rateListe);
        return e;
    }


    private static int vergleicheDrin(int[] geheim, int[] rateListe) {
        int drin = 0;
        for (int r=0; r<CODEZAHLEN; r++) {
            for (int g=0; g<CODEZAHLEN; g++) {
                if (rateListe[r] == geheim[g]) {
                    drin++;
                    rateListe[r] = MARKRATE;
                    geheim[g] = MARKGEHEIM;
                }
            }
        }
        return drin;
    }

    private static int vergleicheExakt(int[] geheim, int[] rateListe) {
        int treffer = 0;
        for (int i=0; i<CODEZAHLEN; i++) {
            if (rateListe[i]==geheim[i]) {
                treffer++;
                rateListe[i] = MARKRATE;
                geheim[i] = MARKGEHEIM;
            }
        }
        return treffer;
    }

    private static int[] stringToListe(String ratestr) {
        int[] rate = new int[CODEZAHLEN];
        for (int i=0; i<CODEZAHLEN; i++) {
            rate[i] = ratestr.charAt(i) - '0';
        }
        return rate;
    }

    private static int[] kopiereGeheimzahl(int[] geheim) {
        int[] kopie = new int[CODEZAHLEN];
        for (int i=0; i<CODEZAHLEN; i++) {
            kopie[i] = geheim[i];
        }
        return kopie;
    }

    private static String input(String prompt) {
        java.util.Scanner input = new java.util.Scanner(System.in);
        System.out.print(prompt);
        String eingabe = input.nextLine();
        return eingabe;
    }

    private static int[] geheimZahlZiehen() {
        int geheim[] = new int[CODEZAHLEN];
        java.util.Random zufall = new java.util.Random();
        for (int i = 0; i < CODEZAHLEN; i++) {
            geheim[i] = zufall.nextInt(CODEMAX) + 1;
            System.out.print(geheim[i]); // debug
        }
        return geheim;
    }

}

Eine Codeknacker-Klasse

Nun soll eine Klasse CodeKnacher entstehen. Ein Objekt dieser Klasse sollte die Geheimzahlen als Attribut enthalten und nur die Variablen nach außen liefern, die dort auch wirklich benötigt werden.

Aus den Funktionen werden nun Methoden gemacht. Dazu müssen alle static-Deklarationen verschwinden. Da sich die komplette Spiellogik in einer Klasse abspielt, kann sie sowohl in eine Konsolenversion als auch in eine grafische Anwendung eingebunden werden.

Ich habe bereits alle Funktionen zu Methoden umgewandelt, indem ich static entfernt habe. Nur bei der Funktion input habe ich es belassen, da diese unabhängig von der Spiellogik, aber abhängig von der Oberfläche (Terminal oder GUI) ist.

Beginnen wir nun damit, die main-Funktion umzustellen, damit sie ein Objekt der eigenen Klasse verwendet.

package code;

public class CodeKnacker {

    static final int CODEZAHLEN = 4;
    static final int CODEMAX = 6;
    static final int MARKRATE = -1;
    static final int MARKGEHEIM = -2;
    
    public static void main(String[] args) {
        CodeKnacker spiel = new CodeKnacker();
        int[] geheim = spiel.geheimZahlZiehen();
        int treffer = 0;
        while (treffer < CODEZAHLEN) {
            String ratestr = input("Dein Versuch: ");
            Ergebnis ergebnis = spiel.vergleiche(geheim, ratestr);
            spiel.zeigeErgebnis(ergebnis);
            treffer = ergebnis.treffer;
        }
        System.out.println("Gewonnen");        
    }
Nun sollte das Array geheim als Attribut in das Objekt wandern. Dazu müssen die Methoden geheimZahlZiehen und vergleich angepasst werden.

package code;

public class CodeKnacker {

    static final int CODEZAHLEN = 4;
    static final int CODEMAX = 6;
    static final int MARKRATE = -1;
    static final int MARKGEHEIM = -2;
    
    private int[] geheim;
    
    public static void main(String[] args) {
        CodeKnacker spiel = new CodeKnacker();
        spiel.geheimZahlZiehen();
        int treffer = 0;
        while (treffer < CODEZAHLEN) {
            String ratestr = input("Dein Versuch: ");
            Ergebnis ergebnis = spiel.vergleiche(ratestr);
            spiel.zeigeErgebnis(ergebnis);
            treffer = ergebnis.treffer;
        }
        System.out.println("Gewonnen");        
    }
Die Methode geheimZahlZiehen muss angepasst werden, dass sie die Geheimzahl nicht mehr zurückgibt, sondern im Attibut ablegt. Da das Ziehen einer Geheimzahl vor jedem Spiel automatisch geschehen sollte, ist es sinnvoll, die ganze Methode als Konstruktor zu realisieren. Dann muss sie nicht einmal mehr aufgerufen werden.

Anstatt wie bisher ein Array zu erzeugen, wird in der ersten Zeile auf das Attribut verwiesen, hier durch ein this hervorgehoben. An sich ist das gar nicht erforderlich. Zur Demonstration ist das this in den weiteren Verwendungen von geheim nicht eingebaut worden.

Ein Konstruktor kennt keinen Rückgabetyp, nicht einmal void. Damit hat sich auch die return-Anweisung erübrigt.

public CodeKnacker() {
    this.geheim = new int[CODEZAHLEN];
    java.util.Random zufall = new java.util.Random();
    for (int i = 0; i < CODEZAHLEN; i++) {
        geheim[i] = zufall.nextInt(CODEMAX) + 1;
        System.out.print(geheim[i]); // debug
    }
}
Bei der Methode vergleiche wird die Geheimzahl als Parameter entfernt. Da der bisherige Parameter genauso hieß wie das neue Attribut, muss außer dem Entfernen des Parameters nichts weiter gemacht werden.

private Ergebnis vergleiche(String ratestr) {
    int[] geheimX = kopiereGeheimzahl(geheim);
    int[] rateListe = stringToListe(ratestr);
    Ergebnis e = new Ergebnis();
    e.treffer = vergleicheExakt(geheimX, rateListe);
    e.drin = vergleicheDrin(geheimX, rateListe);
    return e;
}

Die Klasse Ergebnis

Unschön ist noch, dass das Ergebnis durch eine Methode der Klasse CodeKnacker durchgeführt wird. Die Klasse Ergebnis sollte sich selbst darum kümmern. Noch besser wäre, wenn sie die Methode toString implementiert. Dann wird aus dem Ergebnis ein String und die Methode System.out.println kann ein Ergebnis direkt ausgeben.

Es ist auch nicht schön, dass direkt auf die Attribute von Ergebnis zugegriffen wird. Die Anzahl der Treffer werden benötigt, damit man die Spielschleife beenden kann. Also benötigen wir eine Methode getTreffer. In der Methode vergleiche werden die beiden Parameter gesetzt. Das kann durch Parameter im Konstruktor erfolgen.

Die Klasse Ergebnis wird eleganter:

package code;

public class Ergebnis {
    private int treffer = 0;
    private int drin = 0;
    
    public Ergebnis(int treffer, int drin) {
        this.treffer = treffer;
        this.drin = drin;
    }
    
    @Override
    public String toString() {
        return "" + treffer + ":" + drin;
    }
    
    public int getTreffer() {
        return treffer;
    }
}
Anschließend wird der Compiler mit der Methode vergleiche unzufrieden. Wir ändern sie, indem wir die Ergebnisse von vergleicheTreffer und vergleicheDrin einfach als Parameter dem Konstruktor zuführen.

private Ergebnis vergleiche(String ratestr) {
    int[] geheimX = kopiereGeheimzahl(geheim);
    int[] rateListe = stringToListe(ratestr);
    Ergebnis e = new Ergebnis(
            vergleicheExakt(geheimX, rateListe),
            vergleicheDrin(geheimX, rateListe));
    return e;
}
Anschließend ändert sich auch die main-Funktion ein letztes Mal.

public static void main(String[] args) {
    CodeKnacker spiel = new CodeKnacker();
    int treffer = 0;
    while (treffer < CODEZAHLEN) {
        String ratestr = input("Dein Versuch: ");
        Ergebnis ergebnis = spiel.vergleiche(ratestr);
        System.out.println(ergebnis);
        treffer = ergebnis.getTreffer();
    }
    System.out.println("Gewonnen");        
}
Nun sollten wir die main-Funktion aus der Klasse CodeKnacker entfernen. Dann sieht man deutlich, welche Restarbeiten noch erforderlich sind. Die neue Klasse heißt Main und realisiert ein Codeknackerspiel auf der Konsole.

package code;

public class Main {
    
    public static void main(String[] args) {
        CodeKnacker spiel = new CodeKnacker();
        int treffer = 0;
        while (treffer < CodeKnacker.CODEZAHLEN) {
            String ratestr = input("Dein Versuch: ");
            Ergebnis ergebnis = spiel.vergleiche(ratestr);
            System.out.println(ergebnis);
            treffer = ergebnis.getTreffer();
        }
        System.out.println("Gewonnen");        
    }
    
    private static String input(String prompt) {
        java.util.Scanner input = new java.util.Scanner(System.in);
        System.out.print(prompt);
        String eingabe = input.nextLine();
        return eingabe;
    }
}
Die Funktion input wird aus der Klasse CodeKnacker entfernt und in die neue Klasse Main übernommen.

Es gibt eine Fehlermeldung, dass vergleiche nicht aufrufbar ist. Also muss diese als public definiert werden.

Die Klasse CodeKnacker im Überblick

package code;

public class CodeKnacker {

    static final int CODEZAHLEN = 4;
    static final int CODEMAX = 6;
    static final int MARKRATE = -1;
    static final int MARKGEHEIM = -2;
    
    private int[] geheim;    

    public Ergebnis vergleiche(String ratestr) {
        int[] geheimX = kopiereGeheimzahl(geheim);
        int[] rateListe = stringToListe(ratestr);
        Ergebnis e = new Ergebnis(
                vergleicheExakt(geheimX, rateListe),
                vergleicheDrin(geheimX, rateListe));
        return e;
    }

    private int vergleicheDrin(int[] geheim, int[] rateListe) {
        int drin = 0;
        for (int r=0; r<CODEZAHLEN; r++) {
            for (int g=0; g<CODEZAHLEN; g++) {
                if (rateListe[r] == geheim[g]) {
                    drin++;
                    rateListe[r] = MARKRATE;
                    geheim[g] = MARKGEHEIM;
                }
            }
        }
        return drin;
    }

    private int vergleicheExakt(int[] geheim, int[] rateListe) {
        int treffer = 0;
        for (int i=0; i<CODEZAHLEN; i++) {
            if (rateListe[i]==geheim[i]) {
                treffer++;
                rateListe[i] = MARKRATE;
                geheim[i] = MARKGEHEIM;
            }
        }
        return treffer;
    }

    private int[] stringToListe(String ratestr) {
        int[] rate = new int[CODEZAHLEN];
        for (int i=0; i<CODEZAHLEN; i++) {
            rate[i] = ratestr.charAt(i) - '0';
        }
        return rate;
    }

    private int[] kopiereGeheimzahl(int[] geheim) {
        int[] kopie = new int[CODEZAHLEN];
        for (int i=0; i<CODEZAHLEN; i++) {
            kopie[i] = geheim[i];
        }
        return kopie;
    }

    public CodeKnacker() {
        this.geheim = new int[CODEZAHLEN];
        java.util.Random zufall = new java.util.Random();
        for (int i = 0; i < CODEZAHLEN; i++) {
            geheim[i] = zufall.nextInt(CODEMAX) + 1;
            System.out.print(geheim[i]); // debug
        }
    }
}
Nach außen stellt die Klasse den Konstruktor und die Methode vergleiche zur Verfügung. Wer mag, könnte dem Konstruktor als Parameter die Anzahl der Codezahlen übergeben. Die Klasse müsste dann angepasst werden, indem die Konstante durch ein Attribut ersetzt wird.

Weiter zu der GUI-Version von Code-Knacker