WordGuess – Wordle-Klon als Konsolen-App

Ich denke, dass ihr schon etwas von dem Spiel Wordle gehört hat. Dieses hat im Herbst vergangenen Jahres für Aufsehen gesorgt, ähnlich wie vor fast 10 Jahren Flappy Bird. Bei dem Spiel muss man ein fünfstelliges Wort innerhalb von sechs Versuchen erraten. Dabei bekommt man nur die Information, ob ein Buchstabe zwar im Wort vorkommt, aber nicht an der richtigen Stelle platziert wurde oder ob der Buchstabe bereits an der richtigen Stelle im Wort eingefügt wurde. Das Spielprinzip ist daher recht einfach und daher habe ich mir gedacht, dass wir das Spiel doch einmal als Konsolen-App mit .NET6 bauen werden.

Als Ausgangslage öffnen wir Visual Studio 2022 und erstellen ein neues Projekt. Dabei wählen wir Console App als Template aus.

Anschließend vergeben wir unserem Projekt einen Namen, z.B. WordGuess.

Wie bereits angekündigt, wollen wir .NET6 als Framework verwenden und daher stellen wir sicher, dass dieses im nächsten Schritt auch ausgewählt ist.

Um uns das Leben ein wenig einfacher zu machen, für wir Spectre.Console unserem Projekt als NuGet-Package hinzu.

Nun erstellen wir einen Ordner Assets und darin eine JSON-Datei mit dem Namen words.json. Diese Datei enthält eine Liste von fünfstelligen englischen Begriffen. Sie stellt somit unsere Basis dar, aus dem unser Spiel später ein zufälliges Wort ermitteln wird, welches dann erraten werden muss. Stellt sicher, dass ihr die Build Action auf Embedded Resource stellt, denn ansonsten können wir die Datei später leider nicht laden. Das vollständige Projekt findet ihr auf GitHub und dort findet ihr euch eine kleine words.json-Datei, welche ein Vielzahl von Begriffen beinhaltet.

Nun erstellen wir uns eine weiteren Ordner Commands und fügen hier eine neue Klasse WordGuessCommand hinzu. Diese Klasse wird die vollständige Spiellogik beinhalten. Zunächst nutzen wir Command aus dem Namespace Spectre.Console.Cli als Basis-Klasse. Dadurch müssen wir jetzt auch die Execute-Methode implementieren.

internal class WordGuessCommand : Command
{
    public override int Execute(CommandContext context)
    {
        AnsiConsole.Write(new FigletText("Word Guess")
            .LeftAligned()
            .Color(Color.Blue));

        var allWordAsArray = GetAllWords();
        var wordToGuess = allWordAsArray[new Random()
            .Next(0, allWordAsArray.Length)];
        var numberOfGuesses = 0;

        while (numberOfGuesses < 6)
        {
            var guessedWord = GetWordFromUser();

            if (allWordAsArray.Contains(guessedWord))
            {
                var wordMatches = 0;

                for (var i = 0; i < guessedWord.Length; i++)
                {
                    if (wordToGuess[i] == guessedWord[i])
                    {
                        AnsiConsole.Markup($"[green]{guessedWord[i]}[/]");
                        wordMatches++;
                    }
                    else if (wordToGuess.Contains(guessedWord[i]))
                    {
                        AnsiConsole.Markup($"[yellow]{guessedWord[i]}[/]");
                    }
                    else
                    {
                        AnsiConsole.Markup($"[red]{guessedWord[i]}[/]");
                    }
                }

                AnsiConsole.WriteLine();

                if (wordMatches == wordToGuess.Length)
                {
                    AnsiConsole.MarkupLine("[green]You win![/]");
                    break;
                }

                if (numberOfGuesses == 5 && wordMatches != wordToGuess.Length)
                {
                    AnsiConsole.MarkupLine("[red]You lose![/]");
                    break;
                }

                numberOfGuesses++;
            }
            else
            {
                AnsiConsole.MarkupLine("[red]The word isn't a valid word.[/]");
            }
        }


        return 0;

        static string[] GetAllWords()
        {
            var assembly = Assembly.GetExecutingAssembly();

            var fileName = $"WordGuess.Assets.words.json";

            using Stream stream = assembly.GetManifestResourceStream(fileName);
            using StreamReader reader = new(stream);

            return JsonSerializer.Deserialize<string[]>(reader.ReadToEnd());
        }

        static string GetWordFromUser()
        {
            return AnsiConsole.Prompt(
                new TextPrompt<string>("Enter a FIVE letter word:")
                    .Validate(word => word.Length == 5
    ? ValidationResult.Success()
    : ValidationResult.Error("[yellow]Your entered word doesn't have FIVE letters.[/]")))
                .ToUpper();
        }
    }
}

Zunächst lassen wir uns Word Guess auf dem Screen ausgeben, quasi als kleiner Willkommensbildschirm. Anschließend laden wir uns unsere Wort-Liste aus der JSON-Datei und wählen per Zufall ein Wort aus. Außerdem setzen wir noch die Anzahl der Versuche auf 0, weil das Spiel ja jetzt erst startet. Da der Nutzer stets sechs Versuche zum Erraten des Wortes zur Verfügung hat, nutzen wir eine While-Schleife, welche unsere numberOfGuesses-Variable überprüft.

Zunächst lassen wir uns vom Nutzer ein Wort geben. Hier findet auch in der Methode GetWordFromUser bereits ein wenig Validierung statt, denn so schauen wir, dass das Wort fünf Zeichen lang ist. Anschließend überprüfen wir, dass das eingegeben Wort in unserer Wortliste enthalten ist, denn ansonsten könnte der Nutzer auch Worte wie AAAAA oder AEIOU ausprobieren. Sollte dies nicht der Fall sein, so beginnt die Abfrage nach einem Wort von vorne. Sollte das Wort ein valides Wort sein, so überprüfen wir nun jeden Buchstaben einzeln, ob dieser entweder an der richtigen Stelle (Darstellung in grün), an der falschen Stelle (Darstellung in gelb) oder gar nicht im Wort enthalten ist (Darstellung in rot). Zu guter Letzt schauen wir noch, ob das eingegebene Wort dem gesuchten Wort entspricht, in diesem Fall können wir die While-Schleife beenden und dem Nutzer zum gewonnenen Spiel gratulieren. Ansonsten schauen wir, dass die Anzahl der Versuche noch passt und starten einen neuen Eingabeprozess durch den Nutzer. Ihr seht, dass die eigentliche Logik gar nicht mal so komplex ist.

Nun öffnen wir noch die Datei Program.cs und entfernen die „Hello World!“-Ausgabe. Anschließend erstellen wir uns eine neue CommandApp und konfigurieren diese entsprechend mit unserem WordGuessCommand.

var app = new CommandApp();

app.Configure(config =>
{
    config.AddCommand<WordGuessCommand>("word-guess");
});

return await app.RunAsync(args);

Bevor wir unsere Konsolen-App nun einmal ausprobieren wollen, öffnen wir die Properties von unserem Projekt und suche hier den Abschnitt Debug.

Hier klicken wir auf den Link Open debug launch profiles UI und tragen entsprechend word-guess ein, denn dies ist der Befehl für unser Spiel.

Anschließend können wir unser Spiel starten und haben jetzt eine erste Version von Wordle mit wenig Code-Zeilen erschaffen.

Nun kann man das Spielprinzip erweitern, so lassen sich natürlich nicht nur fünfstellige Wörter, sondern auch Wörter anderer Längen erraten. Oder aber man stellt auch eine deutschsprachige words.json-Datei bereit, so dass der Nutzer verschiedene Sprachen spielen kann. Der Fantasie sind hier keine Grenzen gesetzt. Den aktuellen Code-Stand findet ihr auch auf GitHub.

Eigene API mit ASP.NET Core: Todo API Entwicklung der Dr. Windows App Fluent Terminal: UWP-Terminal für Windows