Ein Hangman Spiel programmieren: Teil 1

  Roland Lötscher   Lesezeit: 15 Minuten Auf Mastodon ansehen

Wie lassen sich in GNOME Workbench UI-Definitionen durch Code zu einer funktionierenden App ausbauen? In diesem Beitrag wird dies anhand des bekannten Hangman Spiels demonstriert.

ein hangman spiel programmieren: teil 1

In dieser zweiteiligen Serie geht es darum, ein Hangman Spiel auf Basis der GNOME Bibliotheken Gtk und Libadwaita zu programmieren. In diesem ersten Teil erstellen wir das Spiel innerhalb von GNOME Workbench. Im zweiten Teil wird das Spiel dann als eigenständige App weiterentwickelt. Dieser Beitrag versteht sich als Folge auf den früheren Beitrag, in welchem GNOME Workbench 47 anhand eines Beispiels zur Erzeugung einer grafischen Benutzeroberfläche vorgestellt wurde.

Der Code zu diesem Beitrag befindet sich im Repository https://github.com/rolandlo/HangmanWorkbench. Klickt man auf Commits, sieht man zudem die einzelnen Schritte, nach welchen dieser Artikel strukturiert ist.

Die grafische Benutzeroberfläche (Blueprints und CSS)

Für ein Hangman Spiel brauchen wir:

  1. eine schrittweise aufgebaute Zeichnung eines Galgenmännchens, 
  2. das zu erratende Wort, in welchem noch nicht erratene Buchstaben durch Platzhalter _ ersetzt sind,
  3. Knöpfe, zum Raten von Buchstaben

All dies definieren wir im folgenden Blueprint:

using Gtk 4.0;
using Adw 1;

Adw.ToastOverlay overlay {
  Adw.StatusPage {
    title: "Hang Man";
    height-request: 1020;
    width-request: 550;
    description: "Errate das Wort, oder du wirst gehängt!";

    Box {
      orientation: vertical;
      spacing: 24;

      DrawingArea drawing_area {
        content-width: 300;
        content-height: 300;
      }

      Label word {
        label: "_____";

        styles [
          "secret",
        ]
      }

      FlowBox flow_box {
        selection-mode: none;
      }
    }
  }
}

Hier ist

  1. DrawingArea ein Zeichenbereich, in welchem das Galgenmännchen programmatisch (Schritt für Schritt) gezeichnet werden soll
  2. Label das Wort (aktuell bestehend aus Platzhalter-Zeichen)
  3. FloxBox ein Behälter für die Knöpfe, die programmatisch hinzugefügt werden sollen

Diese drei Objekte sind in eine vertikale Box gepackt (stehen also untereinander im Layout). Die Box ist in eine Libadwaita Status-Seite (Adw.StatusPage) mit passendem Titel und Beschreibung gepackt und jene wiederum in ein Adw.ToastOverlay. Letzteres dient dazu, dass wir in dieser Status-Seite Toasts anzeigen können, wenn die Benutzer:in das Worträtsel gelöst hat oder gehängt wurde.

Für das Wort wird hier der Stil "secret" angewendet und jener ist benutzerdefiniert im CSS Tab in Workbench:

.secret {
  color: @blue_1; /* Adwaita Named Color */
  font-size: 80pt;
  letter-spacing: 12pt;
}

Für das zu erratende Wort wird also blaue Schrift in Schriftgrösse 80pt mit Buchstaben-Zwischenräumen von 12pt gewählt.

Der Schwerpunkt dieses Artikels liegt auf der Programmierung und alle folgenden Schritte beziehen sich auf Python-Code.

Import und Definition der Objekte

Im Code-Reiter importieren wir Gtk 4 und das Workbench Modul und definieren die Objekte, welchen wir bereits in der Blueprint Datei Namen verliehen haben:

import gi

gi.require_version("Gtk", "4.0")
from gi.repository import Gtk

import workbench

flow_box = workbench.builder.get_object("flow_box")
word = workbench.builder.get_object("word")
overlay = workbench.builder.get_object("overlay")
drawing_area = workbench.builder.get_object("drawing_area")

So ähnlich kann der Start des Python-Codes in einem Workbench-Projekt praktisch immer geschrieben werden. Dabei steht gi übrigens für GObject Introspection. Dies ist die magische Zutat, welche es erlaubt, die GNOME C-Bibliotheken in einer Vielzahl von Sprachbindungen zu verwenden.

Eventuell müssen noch andere GNOME Bibliotheken (wie GObject, GLib, Gio, Adw etc.) oder andere Module importiert werden. Wir werden dies nachholen, wenn sich der Bedarf stellt.

Nun sieht das Workbench Fenster wie folgt aus:

Python Code, CSS, Blueprint und Vorschau sind bequem im Überblick (von links nach rechts) zu sehen.

Knöpfe für Buchstaben hinzufügen

Die Knöpfe für die 26 Buchstaben (A-Z) fügen wir nun programmatisch ein (am Ende der Python-Datei):

def append_letters():
    for ascii in range(65, 65 + 26):
        letter = chr(ascii)
        button = Gtk.Button(label=letter)
        button.add_css_class("pill")
        button.add_css_class("circular")
        flow_box.append(button)
append_letters()

Der Buchstaben "A" hat ASCII-Code 65, der Buchstaben "B" ASCII-Code 66 etc. Für jeden Buchstaben wird zu unserer FlowBox ein Knopf hinzugefügt und dafür verwenden wir die vordefinierten CSS-Klassen "pill" und "circular".

Es wäre auch möglich gewesen, diese Knöpfe im Blueprint zu definieren, aber dann hätten wir alle Buchstaben einzeln aufzählen müssen und das hätte nicht nur die Blueprint-Datei unübersichtlich gemacht. Es wäre auch mühsam geworden, Änderungen vorzunehmen. Daher war der programmatische Weg definitiv zu bevorzugen.

Das Galgenmännchen zeichnen

In einer DrawingArea kann mithilfe von Zeichenbefehlen mit der Cairo-Bibliothek gezeichnet werden. Für das Galgenmännchen kommen wir mit geraden Strichen und einem Kreis (für den Kopf) aus. Das Koordinatensystem hat links oben die Koordinate (0,0).  Wir zeichnen Schritt für Schritt die einzelnen Teile. Im folgenden Code-Stück steht cr für den Cairo-Kontext, in welchem die Zeichenbefehle stattfinden; width und height stehen für die zur Verfügung gestellte Höhe und Breite des Zeichenbereichs, wobei wir die (effektiv genutzte) Breite auf höchstens 80 % der Höhe des Zeichenbereichs beschränken, damit die Figur nicht unförmig wird.

Die zwei wichtigsten Befehle in der folgenden Zeichenfunktion draw sind:

  1. cr.move_to(x,y), um den Zeichencursor an die Position (x, y) zu bewegen, ohne zu zeichnen
  2. cr.line_to(x,y), um einen geraden Strich von der aktuellen Position bis zur Position (x, y) zu ziehen
def draw(_drawingarea, cr, width, height):
    width = min(width, 0.8 * height)
    cr.set_source_rgba(1, 0, 1, 1) # magenta
    # Horizontale am Boden
    cr.move_to(0, height)
    cr.line_to(width, height)
    # Pfosten
    cr.move_to(0.2 * width, height)
    cr.line_to(0.2 * width, 0)
    # Querbalken
    cr.line_to(0.7 * width, 0)
    # Seil
    cr.line_to(0.7 * width, 0.2 * height)
    # Kopf
    cr.arc(0.7 * width, 0.3 * height, 0.1 * height, 1.5 * math.pi, 3.5 * math.pi)
    # Körper
    cr.move_to(0.7 * width, 0.4 * height)
    cr.line_to(0.7 * width, 0.7 * height)
    # linker Arm
    cr.move_to(0.7 * width, 0.45 * height)
    cr.line_to(0.6 * width, 0.55 * height)
    # rechter Arm
    cr.move_to(0.7 * width, 0.45 * height)
    cr.line_to(0.8 * width, 0.55 * height)
    # linkes Bein
    cr.move_to(0.7 * width, 0.7 * height)
    cr.line_to(0.6 * width, 0.8 * height)
    # rechtes Bein
    cr.move_to(0.7 * width, 0.7 * height)
    cr.line_to(0.8 * width, 0.8 * height)
    cr.stroke()

Beim Zeichnen des (kreisförmigen) Kopfs brauchen wir die Kreiszahl math.pi

wofür wir (oben bei den import Anweisungen) das math-Modul importieren:

import math

Um nun unsere DrawingArea mithilfe unserer Funktion draw zeichnen zu lassen, schreiben wir ganz unten im Code:

drawing_area.set_draw_func(draw)

Wenn nun der Code mit dem "Run"-Knopf (rechts oben in Workbench) ausgeführt wird, erhält man folgende Ansicht:

Schrittweise Erzeugung und Zurücksetzen

Das Galgenmännchen sollte natürlich nicht sofort in einem einzigen Schritt gezeichnet werden. Es sollte Schritt für Schritt gezeichnet werden, mit jedem fehlerhaften Buchstaben ein Strich mehr.

Dazu führen wir nun eine (globale) Variable step ein. Diese soll sich (ausgehend vom Wert 0) bei jedem fehlerhaften Buchstaben um eins erhöhen und beim Zurücksetzen auf 0 zurückspringen. In der Zeichenfunktion draw wird die Variable wie folgt verwendet (Code-Ausschnitt):

    if step>=1: # Horizontale am Boden
      cr.move_to(0, height)
      cr.line_to(width, height)

    if step>=2: # Pfosten
      cr.move_to(0.2 * width, height)
      cr.line_to(0.2 * width, 0)

    if step>=3: # Querbalken
      cr.line_to(0.7 * width, 0)

etc.

Beim Zurücksetzen muss nicht nur die Variable step zurückgesetzt werden. Auch muss ein zufälliges Wort gewählt werden und die später beim Anklicken der Buchstaben deaktivierten Knöpfe müssen reaktiviert werden. Wir definieren dazu (provisorisch) eine Wörterliste, von welcher die zufälligen Wörter kommen, sowie eine Liste von deaktivierten Knöpfe:

word_list = ["KLAUS", "HEINO", "ANNA", "SUSANNE", "GERTRUD", "HELGA", "HANS", "FRANZ", "KUNO", "KATHARINA"]
deactivated_list = []

und definieren die Zurücksetz-Funktion dann wie folgt:

def reset():
    global step
    global secret
    step = 0
    secret = random.choice(word_list)
    word.set_label("_" * len(secret))

    for button in deactivated_list:
        button.set_sensitive(True)

    drawing_area.queue_draw()

Hier wird auch das random-Modul eingesetzt, welches (oben im Code, bei den Import Anweisungen) zu importieren ist:

import random

Das Zurücksetzen muss auch am Anfang der Programm-Ausführung durchgeführt werden, also fügen wir unten im Code (bei append_letters()) auch die Zeile

reset()

ein.

Auf Knopf-Druck reagieren

Wird auf einen der Buchstaben Knöpfe geklickt, müssen wir

  1. den entsprechenden Buchstaben deaktivieren (und den entsprechenden Knopf der Liste deactivated_list hinzufügen)
  2. prüfen, ob der Buchstabe im Wort vorkommt (und bei negativem Ausgang die Variable step inkrementieren)
  3. im Wort an jeder Stelle, an welchem der Buchstabe vorkommt, den Platzhalter _ durch den Buchstaben ersetzen
  4. die Galgenmännchen-Zeichnung aktualisieren

All dies geschieht im folgenden Code-Block:

def on_clicked(button):
    text = word.get_label()
    guess = button.get_label()
    deactivated_list.append(button)
    button.set_sensitive(False)
    correct = False
    for i in range(len(secret)):
        if secret[i] == guess:
            correct = True
            text = text[:i] + guess + text[i + 1 :]
    word.set_label(text)
    if not correct:
        global step
        step += 1
    drawing_area.queue_draw()

Diese Funktion muss ausgeführt werden, wenn der Knopf gedrückt wird. Dies geschieht über den Befehl

button.connect("clicked", on_clicked)

in der Funktion append_letters (nach der Definition der Variable button).

Rückmeldungen bei Spielentscheid und Neustart

Nun lässt sich das Spiel bereits einmal durchspielen. Wir möchten allerdings noch eine Rückmeldung erhalten, wenn wir:

  1. das Spiel verloren haben und gehängt werden müssen (wenn die Variable step den Wert 10 erreicht) oder
  2. das Spiel gewonnen haben (es gibt keine Platzhalter-Zeichen _ mehr im angezeigten Wort)

Ausserdem soll das Spiel in beiden Fällen neu beginnen (mit einem neu gewählten Wort), was wir durch Ausführung der Funktion reset erreichen können. 

Die Rückmeldungen haben wir in der Blueprint-Datei bereits durch das Toast-Overlay vorbereitet. Wir lassen also in beiden Fällen einen passenden Toast erscheinen, der für eine kurze Zeit (2 Sekunden) angezeigt wird und beim Ableben das Zurücksetzen auslöst. Dies erreichen wir durch Anhängung der folgenden Code-Zeilen an die Funktion on_clicked:

    if step == 10:
        toast = Adw.Toast(
            title="Du bist gehängt!!",
            timeout=2,
        )
        toast.connect("dismissed", lambda _: reset())
        overlay.add_toast(toast)
    if "_" not in text:
        toast = Adw.Toast(
            title="Jippie, erraten!!",
            timeout=2,
        )
        toast.connect("dismissed", lambda _: reset())
        overlay.add_toast(toast)

Da wir hier Adw.Toast (von Libadwaita) einsetzen, müssen wir oben im Code noch Adw importieren:

gi.require_version("Adw", "1")
from gi.repository import Gtk, Adw

Bei der zweiten Zeile handelt es sich nur um eine Ergänzung einer bestehenden Zeile zum Import von Gtk.

Nun ist das Spiel beliebig wiederholbar. Natürlich sollte man die Wortliste noch ergänzen oder einfach durch eine wesentlich längere Liste ersetzen. Sonst weiss man bereits am Anfang, dass einige Buchstaben in keinem Wort vorkommen. Hier noch eine Ansicht des Spiels kurz vor Ende einer Durchführung:

Titelbild: https://pixabay.com/photos/winters-gibbet-elsdon-3763870/

Quellen:

Tags

Hangman, Gnome, GTK, Workbench, Python, Spiel

Es wurden noch keine Kommentare verfasst, sei der erste!