7  Memory game

Heute, du wirst ein klassisches Memory-Spiel schreiben: Es liegen acht Karten mit der Rückseite nach oben, du kannst beliebige zwei davon umdrehen und wenn sie identisch sind, werden sie vom Tisch entfernt. Wenn sie unterschiedlich sind, werden die Karten wieder “mit der Rückseite nach oben” gedreht.

Bevor wir loslegen, erstelle einen neuen Ordner für das Spiel und darin einen Unterordner namens Bilder. Lade dann Hühnerbilder herunter1 und entpacke sie in den Bilder-Unterordner. Hol dir auch das Übungsheft!

7.1 Kapitelkonzepte

7.2 Variablen als Kisten (unveränderliche Objekte)

In diesem Spiel wirst du Wörterbücher verwenden. Das sind veränderliche, wie Listen, im Gegensatz zu “normalen” unveränderlichen Werten (Ganzzahlen, Fließkommazahlen, Zeichenfolgen). Du musst diese Unterscheidung lernen, da sich diese beiden Arten von Objekten (Werten) unter bestimmten Umständen sehr unterschiedlich verhalten, was sowohl gut (Macht!) als auch schlecht (merkwürdiges unerwartetes Verhalten!) ist.

Du erinnerst dich vielleicht an das Variable-als-Box Bild, das ich verwendet habe, um Variablen einzuführen. Kurz gesagt, eine Variable kann als “Box” mit einem Variablennamen darauf und einem Wert darin gedacht werden. Wenn du diesen Wert verwendest oder ihn einer anderen Variable zuweist, kannst du davon ausgehen, dass Python eine Kopie davon erstellt2 und diese Kopie in eine andere “Box” der Variablen legt. Wenn du den Wert einer Variable ersetzt, nimmst du den alten Wert heraus, zerstörst ihn (indem du ihn in ein nahes schwarzes Loch wirfst, nehme ich an), erstellst einen neuen und legst ihn in die “Box” der Variablen. Wenn du eine Variable basierend auf ihrem aktuellen Zustand veränderst, geschieht dasselbe. Du nimmst den Wert heraus, erstellst einen neuen Wert (indem du den ursprünglichen Wert addierst oder eine andere Operation durchführst), zerstörst den alten und legst den neuen wieder in die “Box” der Variablen. Wichtig ist, dass obwohl eine Variable verschiedene unveränderliche Werte haben kann (wir änderten die Variable imole bei jeder Runde), der unveränderliche Wert selbst nie verändert wird. Er wird ersetzt durch einen anderen unveränderlichen Wert, verändert sich jedoch nie3.

Der Kasten-Metapher erklärt, warum Gültigkeitsbereiche so funktionieren, wie sie es tun. Jeder Gültigkeitsbereich hat seinen eigenen Satz an Kästen und wann immer du Informationen zwischen Gültigkeitsbereichen austauschst, z.B. von einem globalen Skript zu einer Funktion, wird eine Kopie eines Wertes (von einer Variablen) erstellt und in einen neuen Kasten (z.B. einen Parameter) innerhalb der Funktion gelegt. Wenn eine Funktion einen Wert zurückgibt, wird er kopiert und in einen der Kästen im globalen Skript gelegt (Variable, der du den zurückgegebenen Wert zugewiesen hast), usw.

Das gilt aber nur für unveränderliche Objekte (Werte) wie Zahlen, Strings, logische Werte usw. sowie Tupel (siehe unten, was das ist). Wie du sicher schon erraten hast, bedeutet das, dass es auch andere veränderliche Objekte gibt und sie sich sehr unterschiedlich verhalten.

7.3 Variablen als Post-it-Zettel (veränderliche Objekte)

Veränderliche Objekte sind zum Beispiel Listen oder Wörterbücher4, also Dinge, die sich verändern können. Der entscheidende Unterschied besteht darin, dass unveränderliche Objekte als in ihrer Größe fixiert betrachtet werden können. Eine Zahl benötigt so viele Bytes zur Speicherung, genau wie ein gegebener String (obwohl ein anderer String mehr oder weniger Bytes erfordern würde). Trotzdem ändern sie sich nicht, sie werden erstellt und zerstört, wenn sie nicht mehr benötigt werden, aber nie wirklich aktualisiert.

Mutable Objekte können verändert werden5. Zum Beispiel kannst du Elemente zu deiner Liste hinzufügen oder sie entfernen oder durcheinander bringen. Das gilt auch für Wörterbücher. Das Machen solcher Objekte unveränderlich wäre rechnerisch ineffizient: Jedes Mal, wenn du einen Wert hinzufügst, wird eine (lange) Liste zerstört und neu erstellt, nur mit diesem einen zusätzlichen Wert. Daher aktualisiert Python einfach das originale Objekt. Aus Effizienzgründen für weitere Berechnungen werden diese Objekte nicht kopiert, wenn du sie einer anderen Variablen zuweist oder als Parameterwert verwendest, sondern per Referenz übergeben. Das bedeutet, dass die Variable nicht mehr ein “Kasten”, in den du Werte hineinlegst, sondern ein “Aufkleber”, den du auf ein Objekt (eine Liste, ein Wörterbuch) klebst. Und du kannst so viele Aufkleber auf ein Objekt kleben, wie du willst, doch es bleibt immer noch dasselbe Objekt!

Was soll das denn jetzt? Wenn du bedenkst, dass eine Variable nur ein Aufkleber (von vielen) auf einem veränderlichen Objekt ist, versuche rauszufinden, was der folgende Code ausgeben wird:

x = [1, 2, 3]
y = x
y.append(4)
print(x)

Mach Übung #1

Hä? Genau das meine ich mit “Sticker auf demselben Objekt”. Erstellen wir eine Liste und kleben ein x-Sticker darauf. Dann weisen wir die gleiche Liste y zu, d.h. wir kleben ein y-Sticker auf dieselbe Liste. Da sowohl x als auch y Sticker auf dem gleichen Objekt sind, sind sie effektiv Synonyme. In diesem speziellen Fall spielt es keine Rolle, welchen Variablennamen du verwendest, um das Objekt zu ändern, sie sind einfach zwei Sticker, die nebeneinander auf der gleichen Liste hängen. Nochmals, nur zur Erinnerung, das wäre nicht der Fall für unveränderliche Werte wie Zahlen, wo sich die Dinge so verhalten würden, wie du es erwarten würdest.

Dieser “Variable-als-Aufkleber”, auch bekannt als “Wertübergabe per Referenz”, hat wichtige Auswirkungen auf Funktionsaufrufe, da er deinen Gültigkeitsbereich bricht, ohne je eine Warnung auszugeben. Schau dir den folgenden Code an und versuche herauszufinden, was die Ausgabe sein wird.

def ändere_es(y):
    y.append(4)


x = [1, 2, 3]
ändere_es(x)
print(x)

Mach Übung #2.

Wie haben wir es geschafft, eine globale Variable von innerhalb der Funktion zu ändern? Haben wir nicht den lokalen Parameter der Funktion verändert? Ja, genau das ist das Problem beim Übergeben per Referenz. Dein Funktionsparameter ist nur ein weiteres Etikett auf dem gleichen Objekt, also musst du dir immer noch Sorgen um globale Variablen machen (deshalb hast du die Funktion geschrieben und über Bereiche gelernt!), auch wenn es so aussieht, als bräuchtest du das nicht. Wenn du verwirrt bist, bist du in guter Gesellschaft. Das ist einer der überraschendsten und verwirrendsten Aspekte in Python, der Menschen6 immer wieder auf die Füße fällt. Lass uns noch ein paar Übungen machen, bevor ich dir zeige, wie man das Problem der Gültigkeitsbereiche für veränderliche Objekte löst.

Mach Übung #3.

7.4 Tuple: eine gefrorene Liste

Die weisen Leute, die Python geschaffen haben, waren sich des Problems bewusst, das die Variable-als-Aufkleber-Problematik verursacht. Daher haben sie eine unveränderliche Version einer Liste hinzugefügt, die als Tuple bezeichnet wird. Es handelt sich um eine “gefrorene” Liste von Werten, die du durchlaufen, auf deren Elemente per Index zugreifen oder herausfinden kannst, wie viele Elemente sie hat, aber du kannst sie nicht ändern. Kein Hinzufügen, Entfernen, Ersetzen von Werten usw. Für dich bedeutet das, dass eine Variable mit einer gefrorenen Liste eine Schachtel und nicht ein Aufkleber ist und dass sie sich wie jedes andere “normale” unveränderliche Objekt verhält. Du kannst ein tuple durch die Verwendung von runden Klammern erstellen.

ich_bin_ein_tuple = (1, 2, 3)

Du kannst es durchlaufen, z.B.,

ich_bin_ein_tuple = (1, 2, 3)
for zahl in ich_bin_ein_tuple:
    print(zahl)
1
2
3

Aber wie ich schon sagte, das Anhängen wird einen Fehler werfen

i_am_a_tuple = (1, 2, 3)

# wirft einen AttributeError: 'tuple' Objekt hat keine 'append' Eigenschaft
i_am_a_tuple.append(4)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[3], line 4
      1 i_am_a_tuple = (1, 2, 3)
      3 # wirft einen AttributeError: 'tuple' Objekt hat keine 'append' Eigenschaft
----> 4 i_am_a_tuple.append(4)

AttributeError: 'tuple' object has no attribute 'append'

Gleiches gilt für das Versuchen, es zu ändern

i_am_a_tuple = (1, 2, 3)

# wirft einen TypeError: 'tuple' Objekt unterstützt keine Elementzuweisung
i_am_a_tuple[1] = 1
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[4], line 4
      1 i_am_a_tuple = (1, 2, 3)
      3 # wirft einen TypeError: 'tuple' Objekt unterstützt keine Elementzuweisung
----> 4 i_am_a_tuple[1] = 1

TypeError: 'tuple' object does not support item assignment

Das bedeutet, dass wenn du eine Liste von Werten an eine Funktion übergeben musst und keine Verbindung zum ursprünglichen Variablen haben möchtest, du stattdessen ein Tupel von Werten an die Funktion übergeben solltest. Die Funktion hat immer noch eine Liste von Werten, aber die Verbindung zum ursprünglichen Listenobjekt ist jetzt unterbrochen. Du kannst eine Liste in ein Tupel mit tuple() umwandeln. Behalte im Hinterkopf, dass tuple() eine gefrorene Kopie der Liste erstellt. Was wird unten passieren?

x = [1, 2, 3]
y = tuple(x)
x.append(4)
print(y)

Mache Übung #4.

Also, wie du sicher schon gemerkt hast, wenn y = tuple(x), erstellt Python eine Kopie der Listenwerte, friert sie ein (sie sind jetzt unveränderlich) und packt sie in das “y”-Fach. Daher hat alles, was du mit der ursprünglichen Liste machst, keine Auswirkung auf das unveränderliche “y”.

Ganz im Gegenteil, du “taust” ein Tuple auf, indem du es mit list() in eine Liste verwandelst. Bitte beachte, dass es eine neue Liste erstellt, die keine Beziehung zu jeder anderen existierenden Liste hat, selbst wenn die Werte gleich sind oder ursprünglich aus einer von ihnen stammten!

Mach Übung #5.

Okay, ich hab doch gesagt, dass list() eine neue Liste erstellt, oder? Das bedeutet, du kannst es verwenden, um eine Kopie einer Liste direkt zu erstellen, ohne einen Zwischenschritt über ein Tupel. Auf diese Weise kannst du zwei unterschiedliche Listen mit identischen Werten haben. Du kannst das gleiche Ergebnis auch erreichen, indem du eine gesamte Liste schneidest, z. B. list(x) ist das gleiche wie x[:].

Mach Übung #6.

Hier hat y = list(x) eine neue Liste erstellt (die ein exaktes Abbild der mit dem “x”-Etikett versehenen war) und das “y”-Etikett wurde auf dieser neuen Liste angebracht, während das “x” weiterhin an der ursprünglichen hing.

Wenn dir schwindelig wird, tut es mir leid, aber es wird noch schlimmer. Der folgende Absatz behandelt ein relativ fortgeschrittenes Szenario, aber ich möchte, dass du darüber Bescheid weißt, denn die Dinge verhalten sich extrem entgegen der Intuition und ich bin selbst ein paar Mal darauf hereingefallen und es hat jedes Mal ewig gedauert, das Problem zu lösen. Also, was passiert, wenn du ein Tupel (unveränderlich!) hast, das eine Liste (veränderbar) enthält? Wie ich bereits erwähnt habe, kannst du das Element selbst nicht ändern, aber dieses Element ist lediglich ein Verweis auf die Liste (ein Aufkleber auf einem veränderbaren Objekt!), also kannst du trotzdem mit der Liste selbst herumspielen, obwohl das Tupel unveränderlich ist. Außerdem macht das Erstellen einer Kopie eines Tupels lediglich eine Kopie des Verweises, der immer noch auf dieselbe Liste zeigt! Also könntest du denken, dass alles in Ordnung ist, weil es sich ja um Tupel handelt, und dann von genau diesem Umstand überrascht werden. Hier ist ein Beispiel für ein solches Durcheinander:

tuple_1 = tuple([1, ["A", "B"], 2])
tuple_2 = tuple_1

# Das funktioniert (richtig) nicht
tuple_1[0] = ["C", "D"]

# Aber wir können das erste Element der Liste in "C" und das zweite in "D" ändern
# Der Verweis auf die Liste ist eingefroren, aber die Liste selbst ist veränderbar!
tuple_1[1][0] = "C"
tuple_2[1][1] = "D"

print(tuple_1)
print(tuple_2)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[5], line 5
      2 tuple_2 = tuple_1
      4 # Das funktioniert (richtig) nicht
----> 5 tuple_1[0] = ["C", "D"]
      7 # Aber wir können das erste Element der Liste in "C" und das zweite in "D" ändern
      8 # Der Verweis auf die Liste ist eingefroren, aber die Liste selbst ist veränderbar!
      9 tuple_1[1][0] = "C"

TypeError: 'tuple' object does not support item assignment

Verwirrend? Aber sowas von! Wenn du das Gefühl hast, von diesem ganzen immutabel/mutabel, Tuple/Liste, Kopie/Referenz-Konfusion überfordert zu sein, bist du einfach nur ein normaler Mensch. Ich verstehe die (rechnerischen) Gründe dafür, auf diese Weise vorzugehen, ich bin mir dieser Unterschiede bewusst und weiß, wie nützlich sie sein können, aber es erwischt mich immer noch hin und wieder auf dem falschen Fuß! Also, mein Rat: sei vorsichtig und überprüfe deinen Code mit einem Debugger, wann immer du Listen oder Dictionaries zuweist, sie an Funktionen übergibst, Kopien erstellst, Listen in Listen hast, usw. Sei dir bewusst, dass Dinge vielleicht nicht so funktionieren, wie du denkst, dass sie sollten!

7.5 Minimaler Code

Jetzt aber genug mit der Theorie, lass uns das Spiel programmieren. Wie immer fangen wir mit einem minimalen Code an (versuche es selbst zu schreiben, anstatt es von dem letzten Spiel zu kopieren):

Importiere die benötigten PsychoPy-Module.

Erstelle ein Fenster in einer nützlichen Größe und mit nützlichen Einheiten.

Warte auf einen Tastendruck.

Schließe das Fenster.

Das erste, was du entscheiden musst, ist die Fenstergröße in Pixeln und welche Einheiten das Größen- und Platzieren von Karten einfacher machen. Jedes Hühnerbild ist 240×400 Pixel groß und für das Spiel benötigen wir Platz für genau 4×2 Bilder, d.h. unser Fenster muss 4 Karten breit und 2 Karten hoch sein. Vergiss nicht, die Datei zu dokumentieren!

Schreib deinen Code in code01.py.

7.6 Ein Bild zeichnen

Heute werden wir keine abstrakten und langweiligen Kreise mehr verwenden, um Maulwürfe darzustellen, sondern echte Hühnerbilder verwenden (siehe Anweisungen oben zum Herunterladen). Das Verwenden eines Bildreizes in PsychoPy ist sehr einfach, da es sich wie andere visuelle Reize verhält, die du bereits kennst. Zunächst musst du ein neues Objekt erstellen, indem du visual.ImageStim(...) aufrufst. Du kannst die vollständige Liste der Parameter in der Dokumentation finden, aber für unsere ursprünglichen Absichten müssen wir nur drei davon übergeben:

  • unsere Fenster-Variable: win.
  • Dateiname des Bildes: image="Images/r01.png" (Bilder befinden sich in einem Unterordner, daher müssen wir einen relativen Pfad verwenden).
  • Größe: size=(???,???). Das musst du selbst berechnen. Wenn du norm Einheiten gewählt hast, wie ich, dann ist das Fenster 2 Einheiten breit und 2 Einheiten hoch, aber für höhe ist es 1 Einheit hoch und aspekt-verhältnis Einheiten breit. Wir möchten ein 4×2 Bild haben, welche Größe (beide Breite und Höhe) hat jedes Bild in den Einheiten deiner Wahl?

Zeichne das Huhn (es sollte in der Mitte des Bildschirms erscheinen).

Mach weiter mit code02.py.

7.7 Bild plazieren (Index zur Position)

Standardmäßig wird unser Bild in der Mitte des Bildschirms platziert, was für einen typischen psychophysikalischen Versuch, der Stimuli auf der Fixierung zeigt (die sich auch typischerweise in der Mitte des Bildschirms befindet), überraschenderweise nützlich ist. Allerdings müssen wir acht Bilder an ihren bestimmten Positionen zeichnen. Du musst eine Funktion erstellen, die einen Bildindex (der von 0 bis 7 geht) entgegennimmt und eine Liste mit einem Paar von Werten mit seiner Position auf dem Bildschirm zurückgibt. Unten siehst du eine Skizze, wie der Index der Position entspricht. Beachte, dass die Bildposition (pos) der Mitte des Bildes entspricht.

Kartenstandort-Index

Nenn die Funktion position_from_index. Sie sollte einen Argument (index) entgegennehmen und eine Liste mit [<x>, <y>] Koordinaten in den PsychoPy-Einheiten (ab jetzt nehme ich an, dass diese norm sind) zurückgeben. Du kannst dann diesen Wert für das Argument pos der ImageStim() verwenden.

Die Berechnung könnte kompliziert aussehen, also lass mich dir helfen loszulegen. Wie kannst du die x-Koordinate für die oberste Reihe berechnen? Wenn du dich nur auf die oberste Reihe konzentrierst, wird es einfacher, weil hier der Spaltenindex gleich dem Gesamtindex ist: Die linke Spalte ist 0, die nächste ist 1 usw. Du brauchst eine einfache Algebra von \(x = a_x + b_x \cdot Spalte\). Du kannst beide \(a_x\) und \(b_x\) leicht ableiten, wenn du die Positionen der ersten und zweiten Karten von Hand berechnest. Das Gleiche gilt für die y-Koordinate. Angenommen, du weißt die Reihe, die entweder 0 (oberste Reihe) oder 1 (unterste Reihe) ist, kannst du \(y = a_y + b_y \cdot Reihe\) berechnen (beachte, dass die Reihen von oben nach unten gehen, aber die PsychoPy-Koordinaten von unten nach oben gehen).

Aber, höre ich dich sagen, du hast keine Zeilen- und Spaltenindizes, sondern nur den Gesamtindex? Um diese zu berechnen, musst du nur im Kopf behalten, dass jede Zeile vier Karten hat. Dann kannst du zwei spezielle Divisionsoperatoren verwenden: den Bodenschneide-Operator // und die Modulo-Operatoren %. Der erstere gibt nur den ganzzahligen Teil der Division zurück, also ist 4 // 3 ist 1 (weil 4/3 1,33333 ist) und 1 // 4 ist 0 (weil 1/4 0,25 ist). Letzterer gibt die verbleibenden Ganzzahlen zurück, also 4 % 3ist 1 und 1 % 4 ist 1. Diese beiden Operatoren reichen aus, um die Zeilen- und Spaltenindizes zu berechnen.

Meine Empfehlung wäre erstmal, mit einzelnen Formeln in Jupyter Notebook rumzuspielen. Das macht es einfacher, Dinge auszuprobieren und das Ergebnis zu sehen, verschiedene Werte in Formeln einzugeben usw. Wenn du dir sicher bist, dass der Code funktioniert, verwandle ihn in eine Funktion, dokumentiere ihn und pack ihn in eine separate Datei (utilities.py, vergiss nicht, einen Kommentar oben in der Datei zu hinterlassen!). Dann kannst du ihn in das Hauptskript einbinden und damit die Karte plazieren. Versuche verschiedene Indizes und stell sicher, dass die Karte an der richtigen Stelle erscheint. Und wenn was nicht so läuft wie erwartet, setz einen Breakpoint und gehe Schritt für Schritt durch das Programm, während du die Variablen im Auge behältst.

Füge position_from_index in utilities.py ein. Füge aktualisierten Code in code03.py ein.

7.8 Rückseite der Karte

Ein Hühnerbild ist die Vorderseite einer Karte, aber das Spiel beginnt mit den Karten verdeckt, also sollte der Spieler ihre Rückseiten sehen. Wir werden ein einfaches Rechteck als Rückseite verwenden. Wähle eine schöne Kombination aus fillColor (Innen) und lineColor (Umrandung) Farben, das einzige Requirement ist, dass sie unterschiedlich sind, da man sie sonst nicht auseinanderhalten kann. Ändere deinen Code, um das Bild (Vorderseite der Karte) und das Rechteck (Rückseite der Karte) nebeneinander zu zeichnen (z.B., wenn das Bild an der Position mit Index 0 ist, sollte das Rechteck an der Position 1 oder 4 sein). So kannst du überprüfen, ob die Größen übereinstimmen und ob sie korrekt positioniert sind.

Füge deinen Code in code04.py ein.

7.9 Wörterbücher

Jede Karte, die wir verwenden, hat viele Eigenschaften: Eine Vorderseite (Bild), eine Rückseite (Rechteck) und andere Eigenschaften wie z.B. welche Seite gezeigt werden soll oder ob die Karte bereits vom Bildschirm entfernt wurde. Das erfordert einen Container, damit wir all diese relevanten Informationen in einer einzigen Variablen speichern können. Wir könnten diese Werte in einer Liste speichern und numerische Indizes verwenden, um auf einzelne Elemente zuzugreifen (z.B. karte[0] wäre das Vorderseitenbild, aber karte[2] würde die aktive Seite angeben), aber Indizes haben an sich keine Bedeutung, also wäre es schwierig herauszufinden, was karte[0] von karte[2] unterscheidet. Python hat eine Lösung für solche Fälle: Wörterbücher.

Ein Dictionary ist ein Container, der Informationen mit Schlüssel : Wert-Paaren speichert. Das ist ähnlich wie beim Nachschlagen einer Bedeutung oder Übersetzung (Wert) eines Wortes (Schlüssel) in einem echten Dictionary, daher der Name. Um ein Dictionary zu erstellen, verwendest du geschweifte Klammern {<Schlüssel1> : <Wert1>}, {<Schlüssel2> : <Wert2>,...} oder erstellst es über dict(<Schlüssel1>=<Wert1>, <Schlüssel2>=<Wert2>,...). Beachte, dass die zweite Version strenger ist, da Schlüssel den Regeln für Variablennamen folgen müssen, während bei der Version mit geschweiften Klammern Schlüssel beliebige Zeichenfolgen sein können.

buch = {"Autor" : "Walter Moers",
        "Titel": "Die 13½ Leben des Käpt'n Blaubär"}

# oder, äquivalent
buch = dict(Autor="Walter Moers",
            Titel="Die 13½ Leben des Käpt'n Blaubär")

Sobald du ein Dictionary erstellt hast, kannst du auf jedes Feld zugreifen oder es ändern, indem du seinen Schlüssel verwendest, z.B. print(book["Autor"]) oder book["Autor"] = "Moers, W.". Du kannst auch neue Felder hinzufügen, indem du ihnen Werte zuweist, z.B., book["Veröffentlichung Jahr"] = 1999. Kurz gesagt, du kannst eine Kombination aus <dictionary-variable>[<key>] verwenden, genau wie du eine normale Variable verwenden würdest. Das ist ähnlich wie die Verwendung von list[index], der Unterschied besteht darin, dass index eine ganze Zahl sein muss, während key jeder hashable Wert sein kann.7

7.10 Eine Karte mit einem Dictionary darstellen

Deine Karte hat folgende Eigenschaften, also werden diese als Key-Value-Einträge in einem Dictionary gespeichert.

  1. "front": Vorderseite (Bild einer Henne).
  2. "back": Rückseite (Rechteck).
  3. "filename": Identität auf der Karte, die wir später verwenden werden, um zu überprüfen, ob der Spieler zwei identische Karten (ihre Dateinamen stimmen überein) oder zwei verschiedene geöffnet hat.
  4. "side": kann entweder "front" oder "back" sein, Informationen darüber, welche Seite oben ist (auf dem Bildschirm gezeichnet). Setze es auf "back", da initially alle Karten mit der Rückseite nach oben liegen. Du kannst es jedoch jederzeit temporär auf "front" setzen, um zu sehen, wie die Karten verteilt sind.
  5. "show": ein logischer Wert, setze ihn auf True. Wir werden ihn später verwenden, um Karten zu markieren, die vom Tisch entfernt sind und daher nicht angezeigt werden. Initially sind alle Karten gezeigt, also sollten alle Karten mit "show" gleich True erstellt werden.

Erstelle eine Dictionary-Variable (nenn sie card) und fülle sie mit relevanten Werten (verwende entweder "front" und "back" für den "side"-Schlüssel) und Reizen (du kannst PsychoPy-Reize in ein Dictionary packen, genau wie wir sie zuvor in eine Liste gepackt haben). Ändere deinen Code so, dass er das richtige Bild basierend auf dem Wert des "side"-Eintrags zeichnet. Beachte, dass du keinen if-Ausschnitt dafür benötigst! Denke über einen Schlüssel nach, den du benötigst, um auf diese beiden Seiten zuzugreifen, und den Wert, den du für den "side"-Schlüssel hast.

Mach weiter mit code05.py.

7.11 Die Kartenfabrik

Du hast den Code, um eine Karte zu erstellen, aber wir brauchen acht davon. Das erfordert definitiv eine Funktion. Schreibe eine Funktion (setze sie in utilities.py, um die Hauptdatei aufzuräumen), die drei Parameter annimmt.

  1. ein Fenster-Variable (die brauchst du, um PsychoPy-Stimuli zu erstellen),
  2. ein Dateiname,
  3. Karten-Positionsindex.

Und zurück gibt’s ein Dictionary, genau wie das, das du erstellt hast. Du hast den Code schon, musst ihn nur in eine Funktion packen und dokumentieren. Ruf die Funktion create_card_path8 auf und nutz sie im Hauptskript, um das card-Dictionary zu erstellen. Überleg dir jetzt, welche Bibliotheken du in utilities.py importieren musst.

Füge create_card_path in utilities.py ein. Füge den Code in code06.py ein.

7.12 Dateiliste abrufen

Für eine einzelne Karte haben wir einfach den Namen einer Bilddatei sowie ihren Speicherort hartcodiert. Allerdings möchten wir für ein richtiges Spiel (oder ein Experiment) flexibler sein und automatisch bestimmen, welche Dateien sich im Ordner Bilder befinden. Dies wird von der os Bibliothek abgedeckt, die verschiedene Hilfsmittel für die Arbeit mit deinem Betriebssystem und insbesondere mit Dateien und Verzeichnissen enthält. Speziell gibt os.listdir(path=“.”) eine Liste mit den Dateinamen aller Dateien in einem durch den Pfad angegebenen Ordner zurück. Standardmäßig ist es der aktuelle Pfad (path="."). Du kannst jedoch auch einen relativen Pfad verwenden - os.listdir("Bilder"), vorausgesetzt, dass Bilder ein Unterordner in deinem aktuellen Verzeichnis ist - oder einen absoluten Pfad os.listdir("E:/Lehre/Python/MemoryGame/Bilder") (in meinem Fall)9.

Versuchs das mal in einem Jupyter Notebook (vergiss nicht, die os Bibliothek zu importieren). Du solltest eine Liste von 8 Dateien erhalten, die als [r|l][index].png codiert sind, wobei r oder l die Richtung angeben, in die das Huhn schaut. Allerdings benötigen wir für unser Spiel nur vier Bilder (4 × 2 = 8 Karten). Daher müssen wir eine Teilmenge davon auswählen, z.B. nur Hühner, die nach links oder rechts schauen. Hier werden wir uns auf Hühner konzentrieren, die nach links schauen, was bedeutet, dass wir nur Dateien auswählen müssen, die mit “l” beginnen. Um diese Filterung zu erleichtern, werden wir einen coolen Python-Trick namens List Comprehensions verwenden.

7.13 List comprehension

List comprehension bietet eine elegante und leicht lesbare Möglichkeit, Elemente einer Liste zu erstellen, zu ändern und/oder zu filtern und eine neue Liste zu erstellen. Die allgemeine Struktur ist

neue_liste = [<Transformation-des-Elements>
              for element in alte_liste 
              if <Bedingung-gegebenes-Element>]

Lassen wir uns Beispiele anschauen, um zu verstehen, wie es funktioniert. Stell dir vor, du hast eine Liste zahlen = [1, 2, 3] und du musst jede Zahl um 1 erhöhen10. Du kannst dies tun, indem du eine neue Liste erstellst und in der -Teil zu jedem Element 1 addierst.

zahlen = [1, 2, 3]
zahlen_plus_1 = [element + 1 for element in zahlen]

Das ist äquivalent zu:

zahlen = [1, 2, 3]
zahlen_plus_1 = []
for element in zahlen:
    zahlen_plus_1.append(element + 1)

Oder, stell dir vor, du musst jeden Eintrag in einen String umwandeln. Das kannst du einfach so machen:

zahlen = [1, 2, 3]
zahlen_als_zeichenketten = [str(element) for element in zahlen]

Und hier ist die äquivalente Form mit einer normalen for-Schleife:

zahlen = [1, 2, 3]
zahlen_als_zeichenketten = []
for element in zahlen:
    zahlen_als_zeichenketten.append(str(element))

Beide Versionen kannst du in Jupyter-Zellen schreiben und überprüfen, ob die Ergebnisse gleich sind.

Mach Übung #7.

Jetzt implementiere den folgenden Code mithilfe von List Comprehension. Überprüfe, ob die Ergebnisse stimmen.

zeichenketten = ['1', '2', '3']
zahlen = []
for zeichenkette in zeichenketten:
    zahlen.append(int(zeichenkette) + 10)

Mach Übung #8 im Jupyter-Notizbuch.

Wie oben erwähnt, kannst du auch eine bedingte Anweisung verwenden, um zu filtern, welche Elemente an die neue Liste übergeben werden. In unserem Zahlenbeispiel können wir Zahlen, die größer als 1 sind, beibehalten:

zahlen = [1, 2, 3]
zahlen_größer_als_1 = [element for element in zahlen if element > 1]

Manchmal wird dieselbe Anweisung in drei Zeilen geschrieben, anstatt in einer, um das Lesen zu erleichtern:

zahlen = [1, 2, 3]
zahlen_größer_als_1 = [element
                       for element in zahlen
                       if element > 1]

Du kannst natürlich die Transformation und die Filterung in einer einzigen Anweisung kombinieren. Erstell einen Code, der alle Elemente unter 2 herausfiltert und ihnen 4 hinzufügt.

Mach Übung #9 im Jupyter-Notizbuch.

7.14 Liste der relevanten Dateien abrufen

Verwende List Comprehension, um eine Liste von Dateien zu erstellen, auf denen das Huhn nach links schaut, d.h. Dateien mit Dateinamen, die mit “l” beginnen. Verwende .startswith(), um zu überprüfen, ob es mit “l” beginnt, und speichere die Liste in der filenames-Variablen. Teste deinen Code in einem Jupyter Notebook. Du solltest eine Liste von vier Dateien erhalten.

7.15 Listen-Operationen

Unsere Liste besteht aus vier eindeutigen Dateinamen, aber im Spiel sollte jede Karte zweimal auftreten. Es gibt mehrere Möglichkeiten, Listen zu duplizieren. Hier nutzen wir dies als Gelegenheit, um über Listen-Operationen zu lernen. Python-Listen implementieren zwei Operationen:

  • Listen zusammenfügen: <list1> + <list2>.
a = [1, 2, 3]
b = [4, 5, 6]
a + b
[1, 2, 3, 4, 5, 6]

Beachte, dass dies eine neue Liste produziert und dass dies daher nicht äquivalent ist zur extend-Methode a.extend(b)! Das + erstellt eine neue Liste, .extend() erweitert die ursprüngliche Liste a.11

  • Listen-Vervielfältigung: <list> * <integer-value> erstellt eine neue Liste, indem sie die ursprüngliche Liste <integer-value>-mal kopiert. Zum Beispiel:
a = [1, 2, 3]
b = 4
a * b
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]

Nutze entweder die Operation oder die .extend() Methode, um eine Liste zu erstellen, in der jeder Dateiname zweimal vorkommt. Tipp: Du kannst die Listendarstellung direkt auf die Dateinamen-Liste anwenden, die du über List Comprehension erstellt hast (also repliziere es in derselben Zeile). Versuche diesen Code in einem Jupyter Notebook auszuführen.

7.16 Schleifen über Index und Element mit Listenummerierung

Da wir nun eine Liste von Dateinamen haben, können wir daraus eine Liste von Karten erstellen. Unsere Wörterbuchfunktion benötigt sowohl den Index als auch den Dateinamen. Letzterer ist das Element der Liste, ersterer ist der Index dieses Elements. Du könntest den Index mit der range()-Funktion aufbauen, aber Python hat eine bessere Lösung dafür: die enumerate()-Funktion! Wenn du statt über eine Liste über enumerate() iterierst, bekommst du ein Tupel mitboth (index, value). Hier ist ein Beispiel:

buchstaben = ['a', 'b', 'c']
for index, buchstabe in enumerate(buchstaben):
    print('%d: %s'%(index, buchstabe))
0: a
1: b
2: c

Und so kannst du enumerate() für List Comprehension verwenden.

buchstaben = ['a', 'b', 'c']
["%d: %s"%(index, buchstabe) for index, buchstabe in enumerate(buchstaben)]
['0: a', '1: b', '2: c']

7.17 Pfad berechnen

Ursprünglich haben wir den Dateinamen als "Images/r01.png" angegeben. Das hat funktioniert, aber jetzt haben wir viele Dateinamen, die wir mit dem Ordnernamen verbinden müssen, um eine Pfadzeichenfolge zu bilden. Dazu kommt, dass die meisten Betriebssysteme mit Windows uneins sind, wo / (Vorwärtsschslash) oder \ (Backslash) für Pfade verwendet werden sollten. Um deinen Code plattformunabhängig und damit robuster zu gestalten, musst du eine Dateinamenzeichenfolge mit der join-Funktion im path-Submodul erstellen. Du kannst dazu die os-Bibliothek importieren und os.path.join(...) aufrufen (das ist meine persönliche Vorliebe). Oder du kannst den gleichen Ansatz wie bei PsychoPy-Modulen verwenden und path aus os importieren, um den Code zu verkürzen. Oder natürlich kannst du auch join direkt importieren, aber ich finde, dass das Fehlen von Bibliotheksinformationen während der Verwendung die Verständlichkeit erschwert (auch wenn der Code noch kürzer ist).

join nimmt Pfadkomponenten als Parameter und fügt sie zusammen, um das OS-Format zu entsprechen. Zum Beispiel würde os.path.join("Python seminar", "Memory game", "memory01.py") unter Windows 'Python seminar\\Memory game\\memory01.py' zurückgeben. Da wir mehrere Dateien laden müssen, wird sich der Dateiname ändern. Der Ordner, in dem sich die Bilder befinden, wird jedoch gleich bleiben und es wäre, wie üblich, eine gute Idee, ihn in eine formal erklärte CONSTANTE zu verwandeln.

Erstelle die Funktion create_card auf der Grundlage von create_card_path, so dass sie annimmt, dass der Parameter filename nur der Dateiname ohne die Ordnerinformationen ist und daher den Pfad durch join mit dem Ordnernamen (definiert als Konstante in diesem Modul!) erstellt. Du musst nun das "Images/" in dem Wert, den du übergibst, weglassen. Teste, dass der Code wie zuvor funktioniert!

Erstelle create_card in utilities.py. Füge aktualisierten Code in code07.py ein.

7.18 Ein Kartenspiel

Lass uns alle Codeschnipsel zusammenfügen, die wir brauchen, um die Dateinamen von Karten herauszufinden, sie zu duplizieren und die Karten mithilfe von Dateinamen und Index zu erstellen.

Kopier den Code zum Erstellen einer duplizierten Liste von Dateinamen, die du in deinem Jupyter-Notizbuch getestet hast, in dein Hauptskript (code08.py). Verwende dann enumerate und List Comprehension über die enumerierten duplizierten Dateinamen, um cards (Mehrzahl, ersetzt deine Singular card-Variable) mit der create_card-Funktion zu erstellen, die du zuvor geschrieben hast. Aktualisiere deinen Zeichencode, um alle Karten zu durchlaufen und zu zeichnen. Wenn deine Standard-“seite” auf “rückseite” eingestellt ist, sieht das alles pretty langweilig aus. Ändere das auf “vorderseite”, um ihre Gesichter zu sehen.

Mach weiter mit code08.py.

7.19 Karten mischen

Wenn du Karten ziehst, wirst du feststellen, dass das Duplizieren der Dateinamenliste eine sehr ordentliche Reihenfolge produziert, die das Spielspielen einfach (und langweilig) macht. Wir müssen die Dateinamenliste vor der Erstellung von cards shuffle() mischen. Beachte, dass shuffle() die Listenelemente an Ort und Stelle mischt, indem es die Tatsache nutzt, dass die Liste veränderbar ist. Das bedeutet, du rufst einfach die Funktion auf und übergibst die Liste als Argument. Die Liste wird verändert, nichts wird zurückgegeben und nichts muss der filenames-Variablen zugewiesen werden.

Mach weiter mit code09.py.

7.20 Lass uns eine Pause machen!

Wir haben viel geschafft, also könnte es ein guter Zeitpunkt sein, um eine Pause einzulegen und deinen Code für meine Überprüfung einzureichen.

7.21 Hauptschleife hinzufügen

Jetzt haben wir einen gemischten Kartensatz, den wir anzeigen, bis ein Spieler eine Taste drückt. Ändere den Code, um eine Hauptpräsentationsschleife zu haben, ähnlich wie die, die wir hatten, als wir mit PsychoPy-Reizen experimentiert haben. Zuvor haben wir eine logische gameover-Variable verwendet, um die while-Schleife zu steuern. Hier werden wir zwei Gründe haben, die Schleife zu beenden: Der Spieler hat die Escape-Taste gedrückt oder er hat das Spiel gewonnen. Daher lass uns eine Zeichenfolge-Variable game_state verwenden, die auf "laufend" initialisiert wird. Wiederhole die Schleife, solange game_state gleich "laufend" ist, aber ändere es auf "abbruch", wenn ein Spieler die Escape-Taste drückt. Du musst auch waitKeys() durch getKeys() ersetzen.

Mach weiter mit code10.py.

7.22 Mausklick erkennen

Im Spiel wird der Spieler einzelne Karten anklicken, um sie umzudrehen. Bevor du eine Maus in PsychoPy verwenden kannst, musst du sie über den Aufruf mouse = event.Mouse(visible=True, win=win) erstellen, wobei win das PsychoPy-Fenster ist, das du bereits erstellt hast. Dieser Code sollte direkt unter der Zeile appear, in der du das Fenster selbst erstellst.

Jetzt kannst du mit der Methode mouse.getPressed() überprüfen, ob die linke Maustaste gedrückt wurde. Sie gibt ein Dreier-Tupel zurück mit True/False-Werten, die angeben, ob jede der drei Tasten momentan gedrückt wird. Verwende es in der Hauptschleife, damit, wenn der Spieler die linke Taste drückt (ihr Index in der zurückgegebenen Liste ist 0), du die "seite" der ersten Karte (also die Karte mit Index 0 in der Liste) auf "vorderseite" änderst. Dies setzt voraus, dass du die Karte mit ihrer "rückseite" initialisiert hast, natürlich. Wenn du den Code ausführst und irgendwo klickst, sollte die erste Karte umgedreht werden.

Leg deinen Mausklick-Code vor das Ziehen von Karten. Im Moment macht es keinen Unterschied, aber es wird später nützlich sein, da es uns ermöglichen wird, den aktuellen Zustand der Karte (also direkt nachdem sie von einem Spieler umgedreht wurde) zu ziehen.

Mach weiter mit code11.py.

7.23 Position zu Index

Momentan wird die erste Karte umgedreht, wenn du irgendwo klickst. Aber die Karte, die du umdrehst, sollte die Karte sein, auf die der Spieler geklickt hat. Dafür müssen wir eine Funktion index_from_position implementieren, die das Gegenteil von position_from_index ist. Sie sollte ein Argument pos entgegennehmen, das ein Tupel aus (<x>, <y>)-Werten ist (eine Mausposition innerhalb des Fensters), und eine ganzzahlige Kartenindex zurückgeben. Du hast Float-Werte (mit Dezimalpunkten) im pos-Argument (weil es sich von -1 bis 1 für norm Einheiten bewegt) und standardmäßig werden die Werte, die du daraus berechnest, auch Float sein. Allerdings muss ein Index ganzzahlig sein, also musst du ihn in einen int()-Aufruf einwickeln, bevor du ihn zurückgibst.

Gehen wir rückwärts — von Position zu Index — finde ich (IMHO) es einfacher. Zuerst musst du darüber nachdenken, wie du eine x-Koordinate (die von -1 bis 1 geht) in einen Spaltenindex (der von 0 bis 3 geht) umwandeln kannst, wenn du 4 Spalten hast (zeichne es auf Papier, um die Mathematik einfacher zu verstehen). Ähnlich übersetzt du y (von -1 bis 1) in einen Zeilenindex, wenn es nur zwei Zeilen gibt. Sobald du den Zeilen- und Spaltenindex kennst, kannst du den Index selbst berechnen, wobei du im Hinterkopf behältst, dass es vier Karten in einer Zeile gibt. Wie bei position_from_index denke ich, dass es einfacher ist, erst mit den Formeln in einem Jupyter Notebook zu spielen, bevor du den Code in eine Funktion umwandelst, dokumentierst und ihn in utilities.py aufnimmst.

Füge index_from_position in utilities.py ein.

7.24 Klicke auf eine ausgewählte Karte, um sie umzudrehen

Jetzt, wo du eine Funktion hast, die einen Index aus einer Position zurückgibt (vergiss nicht, sie zu importieren), kannst du die Karte umdrehen, auf die der Spieler geklickt hat. Dazu musst du den Code zum Umdrehen der Karte innerhalb des Codes wenn die linke Maustaste gedrückt wurde erweitern. Hol dir die Position der Maus innerhalb des Fensters, indem du mouse.getPos() aufrufst. Dies wird ein Paar (x, y)-Werte zurückgeben, das du an deine index_from_position()-Funktion übergeben kannst. Diese wird ihrerseits den Index der Karte zurückgeben, auf die der Spieler geklickt hat. Ändere die "seite" einer Karte mit diesem Index auf "vorderseite". Teste den Code, indem du verschiedene Karten umdrehst und stelle sicher, dass es die Karte ist, auf die du geklickt hast, die sich umdreht. Und wie immer, zögere nicht, einen Breakpoint innerhalb der if-Anweisung zu setzen, um die tatsächlichen Mauspositionswerte und wie sie in den Index übersetzt werden zu überprüfen, wenn etwas nicht funktioniert.

Mach weiter mit code12.py.

7.25 Offene Karten im Auge behalten

Im eigentlichen Spiel darf ein Spieler nur zwei Karten gleichzeitig umdrehen. Wenn sie übereinstimmen, werden sie entfernt. Wenn nicht, werden sie wieder auf ihre Rückseiten gedreht. Das bedeutet, wir müssen aufzeichnen, welche und wie viele Karten oben liegen. Wir können das immer durch eine List Comprehension herausfinden, indem wir nach Karten suchen, deren "seite" "vorderseite" ist. Aber das veränderliche Wesen von Dictionaries bietet uns eine einfachere Lösung. Wir erstellen eine neue Liste (nennen wir sie face_up) und fügen Karten hinzu. Dictionaries sind veränderlich, also wird eine Referenz auf das gleiche Dictionary-Objekt in beiden Listen vorhanden sein (die gleiche Kartendictionary hat zwei Sticker darauf, einen von der cards-Liste und einen von der face_up-Liste). Auf diese Weise wissen wir, welche Karten oben liegen (jene, die in der Liste sind) und wir wissen, wie viele (Länge der face_up-Liste).

Aber pass auf, dass du keine Karte zweimal hinzufügst (das würde unsere “wie viele Karten sind oben” Zahl durcheinanderbringen). Es gibt mehrere Möglichkeiten, das zu verhindern. Angenommen, icard ist der Index der Karte, den du über position_to_index() aus der Mausposition berechnet hast, du kannst einfach überprüfen, ob diese Karte "seite" "vorderseite" ist. Alternativ kannst du überprüfen, ob diese Karte bereits in der face_up-Liste ist. Auf jeden Fall wird dir das sagen, ob die Karte oben ist. Wenn sie es nicht ist, solltest du ihre "seite" auf "vorderseite" setzen und sie zur face_up-Liste hinzufügen.

Mach das hier, öffne ein paar Karten. Dann setz einen Breakpoint, um das Programm anzuhalten und zu überprüfen, ob die face_up-Liste genau diese (diese Anzahl) Karten enthält. Wenn es mehr hat, funktionieren deine face-up-Prüfungen nicht. Setz einen Breakpoint darauf und gehe durch den Code, um zu sehen, was passiert.

Mach weiter mit code13.py.

7.26 Nur zwei Karten aufdecken

Jetzt müssen wir überprüfen, ob ein Spieler genau zwei Karten aufgedeckt hat. In deinem Code sollten die Mausprüfungen vor dem Zeichencode sein. Das bedeutet, dass die Karten sofort nach einem Klick mit der Vorderseite nach oben gezogen werden. Sobald sie gezogen wurden, überprüfe die Länge von face_up, ob sie gleich 2 ist:

  • Mach eine Pause von ~0.5 s12 mit wait, damit der Spieler beide Karten sehen kann.
  • Klapp beide Karten um (d.h. setze ihre "seite" auf "rückseite").
  • Nimm sie aus der face_up-Liste raus (siehe .clear() Methode).

Mach weiter mit code14.py.

7.27 Ein passendes Paar vom Tisch nehmen

Unser Code dreht die Karten wieder um, selbst wenn du ein passendes Paar gefunden hast, aber wir müssen sie vom Tisch nehmen. Sobald du zwei Karten in der face_up-Liste hast, musst du überprüfen, ob sie das gleiche Huhn haben, d.h., ihre Dateinamen sind gleich. Wenn ja, setzt du das "show"-Feld auf False. Wenn nicht, setzt du ihre "seite" auf "rückseite" (was dein Code bereits tut). Auf jeden Fall musst du das Programm pausieren, damit der Spieler sie sehen und die face_up-Liste löschen kann (sie sind entweder vom Tisch oder mit der Rückseite nach oben, definitiv nicht mit der Vorderseite nach oben).

Wir müssen unseren Code anpassen, auch um das "show"-Feld korrekt zu verarbeiten. Zunächst musst du deinen Zeichencode anpassen, um nur die Karten zu zeichnen, die gezeigt werden sollen. Zweitens, beim Verarbeiten des Mausklicks musst du überprüfen, ob die Karte nicht bereits oben liegt und ob sie gezeigt wird (ansonsten könntest du unsichtbare Karten “öffnen”).

Mach weiter mit code15.py.

7.28 Das Spiel ist vorbei, wenn alle Karten vom Tisch sind

Wenn dein Code korrekt funktioniert, kannst du alle Karten vom Tisch nehmen, sodass nur noch der graue Bildschirm übrig bleibt. Aber das sollte der Punkt sein, an dem das Spiel endet und dich zu deinem Erfolg gratuliert. Schreibe eine Funktion remaining_cards, die die Liste mit Karten (also unsere cards-Liste) entgegennimmt und zurückgibt, wie viele Karten noch gezeigt werden (ihr "show"-Feld ist True). Du brauchst auf jeden Fall eine for-Schleife dafür, aber die Implementierung kann sehr unterschiedlich sein. Du könntest eine zusätzliche Zählervariable verwenden, die du auf 0 initialisierst und dann um eins erhöhst (siehe += für eine Abkürzung). Alternativ kannst du List Comprehensions verwenden, um alle Karten herauszufiltern, die nicht gezeigt werden, und die Länge dieser Liste zurückzugeben (eine einzeilige Lösung). Implementiere diese Funktion in utilities.py und beende die Schleife, indem du game_state auf "victory" setzt. Nach der Schleife kannst du die game_state-Variable überprüfen und wenn der Spieler gewonnen hat, eine Gratulationsnachricht anzeigen (TextStim, beachte, dass du nicht einmal eine Variable dafür erstellen musst, du kannst ein Objekt erstellen und draw() darauf aufrufen, also z.B. visual.TextStim(...).draw()) und auf eine Tasteneingabe warten, bevor du das Fenster schließt.

Implementiere remaining_cards in utilities.py. Füge deinen Code in code16.py ein.

7.29 Mach’s schnell!

Es gibt verschiedene Möglichkeiten, um die Geschwindigkeit in diesem Spiel zu quantifizieren. Du könntest die Anzahl der Paare betrachten, die der Spieler öffnen musste, bis sie alle gelöst hat (je weniger, desto besser). Oder du könntest messen, wie schnell der Spieler es in Sekunden geschafft hat. Oder du könntest eine Kombination aus diesen beiden Maßnahmen verwenden. Lassen wir uns die zweite Option - die gesamte benötigte Zeit - als Gelegenheit nutzen, um PsychoPy Uhren kennenzulernen.

Die beiden Klassen, die dich am meisten interessieren werden, sind Clock und CountdownTimer. Der einzige Unterschied zwischen den beiden ist, dass Clock bei 0 startet und die verstrichene Zeit zählt, sodass seine getTime()-Methode nur positive Werte zurückgibt. Im Gegensatz dazu startet der CountdownTimer mit einem von dir initialisierten Wert und zählt die verbleibende Zeit herunter. Wichtig ist, dass er nicht stoppt, wenn er 0 erreicht, also wirst du schließlich mit negativer verbleibender Zeit enden. Daher überprüfst du bei Clock, ob die verstrichene Zeit länger als ein vordefinierter Wert ist, während du beim CountdownTimer mit einem vordefinierten Wert startest und überprüfst, ob die verbleibende Zeit über Null liegt. Beachte, dass es nicht garantiert ist, dass die verbleibende Zeit genau Null ist. Im Gegenteil, es ist sehr unwahrscheinlich, dass dies jemals passiert, also vergleiche nie Float-Werte mit exakten Zahlen13.

Hier interessieren wir uns für die verstrichene Zeit, also ist Clock die offensichtliche Wahl. Erstelle eine Uhr vor der Spielschleife und verwende die verstrichene Zeit in der Glückwunschnachricht.

Mach weiter mit code17.py.

7.30 Wie kannst du es verbessern?

Super Spiel, aber du kannst es immer verbessern: Highscore, mehrere Runden, usw. Der Himmel ist die Grenze!


  1. Die Bilder stammen von Kevin David Pointon und wurden von OpenClipart heruntergeladen. Sie sind Public Domain und können frei verwendet und verbreitet werden.↩︎

  2. Nicht wirklich, aber das erleichtert das Verständnis.↩︎

  3. Ein Metaphernversuch: Du kannst verschiedene Hemden tragen, also verändert sich dein Aussehen (Variable), aber jedes einzelne Hemd (mögliche Werte) bleibt gleich (wir ignorieren hier den Verschleiß), unabhängig davon, ob du es trägst (der Wert wird einer Variable zugewiesen) oder nicht.↩︎

  4. Kommen bald!↩︎

  5. Verwende das Metapher des Aussehens: Du kannst dein Aussehen ändern, indem du ein anderes (immutables) Hemd trägst oder indem du deine Frisur änderst. Dein Haar ist mutabel, du trägst nicht an verschiedenen Tagen ein anderes, um anders auszusehen, du musst es ändern, um anders auszusehen.↩︎

  6. Na ja, zumindest mich!↩︎

  7. Unveränderliche Werte sind hashable, während veränderliche wie Dictionaries und Listen nicht sind. Das liegt daran, dass veränderliche Objekte während der Ausführung des Programms verändert werden können und daher als Schlüssel unbrauchbar sind. Das bedeutet, es ist schwierig, auf ein Dictionary zuzugreifen, wenn der Schlüssel sich ändern kann, wenn du darauf zugreifen musst.↩︎

  8. Diese Funktion geht davon aus, dass du einen vollständigen relativen Pfad zu der Datei angibst. Später werden wir eine Version dieser Funktion erstellen, die die Ordnerinformationen selbst anhängen wird.↩︎

  9. Verwende einen absoluten Pfad nur, wenn es die einzige Option ist, da er fast sicherlich auf einem anderen Rechner deinen Code unterbrechen wird.↩︎

  10. Ein sehr willkürlich gewähltes Beispiel!↩︎

  11. Du wirst später über praktische Auswirkungen davon lernen. Vorerst behalte im Kopf, dass scheinbar identische Ausgaben fundamental unterschiedlich sein können.↩︎

  12. Wähle die Dauer, die du magst!↩︎

  13. Allgemeiner gesagt, vergleiche nie Float-Werte mit exakten Zahlen. Sie sind tricky, da die darunterliegende Darstellung nicht garantiert, dass die Berechnung genau die Zahl produziert, die sie sollte: .1 +.1 +.1 ==.3 ist überraschend False, probiere es selbst!↩︎