Corsi Block Tapping - Hilfe!

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Kabee
User
Beiträge: 2
Registriert: Donnerstag 21. Januar 2016, 15:41

Guten Tag Zusammen,

es ist immer unangenehm mit einer Frage in einem neuen Forum aufzutauchen, jedoch ist dies ja auch meist der Grund weshalb man überhaupt ein Forum aufsucht..

Ich studiere Psychologie und im Rahmen das Studiums lernen wird Grundlegende Programmierkentnisse um gewisse Dinge selbst realisieren zu können. Ein Beispiel davon ist der Corsi Blocktapping Task, bei dem es darum geht sich eine Reihenfolge von Quadraten zu merken, die nach und nach aufleuchten und diese danach zu wiederholen. (YouTube - Corsi Block Tapping - Sind Videos zu finden, zum besseren Verständnis). Als Projekt gilt es nun, dieses "Spiel" selbst nachzuprogrammieren, was mich in gewissen Bereichen vor größere Heraussforderungen stellt, als ich erwartete.

Das Programm ist in soweit programmiert, dass er mir die Anordnung der quadrate auf dem Bildschirm anzeigt. Nun geht es zu der Aufgabe, dem Programm zu sagen, dass er eine zufällige Reihenfolge der Quadrate erstellen soll und diese Quadrate auch auf dem Bildschirm anzeigt. Dies natürlich aufsteigend mit jedem Trial, bis alle 9 Quadrate genutzt werden.

Hier mein Ansatz:

Ich habe ein Dictionary erstellt, indem von 0-8 die Koordinaten aller 9 Quadrate hinterlegt ist.

Code: Alles auswählen

# ...
    button_dictionary = {0: ["", 70, 60, 60, 60],
                    1: ["", 100, 210, 60, 60],
                    2: ["", 100, 320, 60, 60],
                    3: ["", 200, 40, 60, 60],
                    4: ["", 320, 120, 60, 60],
                    5: ["", 420, 80, 60, 60],
                    6: ["", 380, 240, 60, 60],
                    7: ["", 300, 350, 60, 60],
                    8: ["", 400, 420, 60, 60]}
Des weiteren habe ich eine Liste erstellt mit den Werten 0-8.

Code: Alles auswählen

# ...
    stimuli_list = [0, 1, 2, 3, 4, 5, 6, 7, 8]
Nun habe ich eine Funktion definiert, mit den Paramtern welches Quadrat er zeichnen soll und in welcher Farbe.

Code: Alles auswählen

def draw_button(button, black = col_black, red = col_red):
    mouse = pygame.mouse.get_pos()
    click = pygame.mouse.get_pressed()
    
    if button[1] + button[3] > mouse[0] > button[1] and button[2] + button[4] > mouse[1] > button[2]:
        pygame.draw.rect(screen, red, (button[1], button[2], button[3], button[4]))
    
    else:
        pygame.draw.rect(screen, black, (button[1], button[2], button[3], button[4]))
(Mit einem Mousover-Effekt, wie man sieht)

Dazu gibt es eine Variable die die Zahl der trials zählt.

Code: Alles auswählen

num_loopbutton = 0
Nun wird bei jeden Programmstart die Liste 0-8 zufällig sortiert und die Variable der trials erhöht sich mit jedem Trial um 1.

Um nun die Quadrate auf den Screen zu zeichnen, habe ich in den Paramtern der Funktion angegeben, er soll anhand der Trialvariable den Wert aus der entsprechenden Postion der Liste nehmen und mit dem Wert der liste wiederrum die passenden Koordinaten aus dem Dictionary. -> Schwer zu erklären, ich hoffe einigermaßen verständlich.

Dies klappt scheinbar einigermaßen, jedoch nimmt er nur das erste Quadrat und wechselt die Farbe zu rot. (Normal soll er für 1 Sec rot anzeigen, dann wieder schwarz und dann das nächste Quadrat).

Code: Alles auswählen

# ...
        if STATE == "PREPARE_STIMULUS":

            random.shuffle(stimuli_list)
            print (stimuli_list)
            STATE = "PRESENT_STIMULUS"     
                
        elif STATE == "PRESENT_STIMULUS":

            if startingtime == 0:
                startingtime = time()
                
                
            elif (time() - startingtime) > 1.0:
                num_loopbutton + 1
                 
                if num_loopbutton == numBlocksToRemember:
                    startingtime = 0
                    num_loopbutton = 0
                    STATE = "WAIT_FOR_RESPONSE"               
                else:
                    startingtime = time()
Ich werde gleich in das Virtuelle Betriebssystem wechseln und die entsprechenden Parts des Quellcodes zur besseren Veranschaulichung hier dazu kopieren.

Gibt es hier jemanden der mein Problem versteht und mir vielleicht auf die Sprünge helfen kann? Vielleicht ist auch mein kompletter Ansatz lächerlich, jedoch habe ich erst vor 6 Wochen mit dem programmieren begonnen und das auch nur "nebensächlich".

Viele Grüße und bereits sehr seeehr vielen Dank im Voraus,

Sven

Ich hoffe bisher war alles verständlich.
Zuletzt geändert von Anonymous am Donnerstag 21. Januar 2016, 16:33, insgesamt 1-mal geändert.
Grund: Quelltext in Python-Codebox-Tags gesetzt.
BlackJack

@Kabee: Wenn man bei Senso die Anzahl der Knöpfe verdoppelt und den Ton weglässt, hat man also etwas mit einem wissenschaftlichen Namen. :-)

Ich sehe in dem gezeigten Quelltext nirgends wo das Rechteck gelb gezeichnet wird/werden soll‽

Ein paar Anmerkungen zum Quelltext: Ein Wörterbuch mit ganzzahligen Schlüsseln die von Null aufsteigend und zusammenhängend sind, ist eigentlich eine Liste als Wörterbuch geschrieben. Man kann das ändern ohne das sich das Programm anders verhält. Allerdings ist dann der Name `button_dictionary` für eine Liste irreführend. Deshalb sollte man solche Grunddatentypen nicht mit in den Namen schreiben. Bei Containertypen ist die Mehrzahl der Bezeichnung für ein Element meistens ein passender Name. `buttons` in diesem Fall.

Falls die originale Reihenfolge der Listen welche die Schaltflächen beschreiben nicht erhalten werden muss, könnte man dann auch die `stimuli_list`-Indirektion weglassen und direkt die `buttons`-Liste an `random.shuffle()` übergeben.

Was soll die Leere Zeichenkette in den Listen für die Schaltflächen bedeuten? Die Frage würde sich nicht stellen wenn man statt Elementen die über ”magische” Indexwerte referenziert werden, etwas verwendet wo man den Einzelteilen Namen geben kann. Also im einfachsten Fall ein Wörterbuch, aber besser ein eigener Datentyp, denn so eine Schaltfläche hat ja nicht nur Koordinaten, sondern auch noch anderen Zustand und verhalten. Es wäre übersichtlicher wenn man das nicht alles über das ganze Programm und verschiedene Datenstrukturen verteilt, sondern zu einem Objekt zusammenfasst.

Für Rechtecke mit Koordinaten und verschiedene Tests gibt es in Pygame den `Rect`-Typ. Das wird dann kürzer und lesbarer. Also zum Beispiel ``if button.rect.collidepoint(pygame.mouse.get_pos()):`` anstelle der `mouse` und `button` Indexzugriffsorgie in `draw_button()`.

Werte, ausser Konstanten, sollten Funktionen und Methoden als Argumente betreten und gegebenfalls als Rückgabewerte verlassen. `screen` kommt aus dem ”Nichts” in `draw_button()`. Vermutlich steht das auf Modulebene. Dort gehören aber nur Definitionen von Konstanten, Funktionen, und Klassen hin. Das Hauptprogramm steht üblicherweise in einer `main()`-Funktion. Programme die auf globalen Variablen operieren werden schnell sehr unübersichtlich, und damit schlecht weiter zu entwickeln oder zu warten.

Konstanten werden per Konvention komplett in Grossbuchstaben geschrieben. `col_black` und `col_red` sollten also so geschrieben werden. Das es sich um Farben handelt, wird man wohl auch ohne den `col_`-Zusatz verstehen. Im Gegenzug ist `STATE` eigentlich `state`, denn Konstanten verändert man ja nicht.

Wenn ich mir die `STATE`-Verarbeitung so anschaue, habe ich eine Vermutung die Funktion wird am Ende zu viele lokale Namen haben. Das hat ja sogar ein bisschen mit dem Thema des Programms zu tun: Menschen können nur x Dinge auf einmal behalten bevor man jedes mal überlegen und nachlesen muss was welcher Name eigentlich konkret bedeutet. Mehr als 10 bis 15 lokale Namen sollte man nicht haben. Eher weniger. Eine Funktion sollte eine in sich geschlossene Sache erledigen. Je mehr Zustände Du da behandelst, um so mehr unterschiedliche Dinge werden in der Funktion erledigt, oft mit unterschiedlichen ”Sätzen” von Namen und immer mit der Gefahr, dass man irgendwann den Überblick verliert und Daten zwischen den Zuständen verändert werden die eigentlich nichts miteinander zu tun haben. Das würde man besser in eigene Funktionen oder Methoden auslagern.

Die Zeichenketten für die Zustände könnte man dann auch loswerden in dem man die Funktionen oder Methoden so schreibt, das sie alle die gleiche Signatur haben und sie dann direkt als Zustandswert verwendet, und jede dieser Funktionen den Folgezustand zurückgeben lässt. Damit vereinfacht sich die Hauptschleife drastisch.

Statt die Zeitabstände selber zu stoppen würden sich Timer-Ereignisse anbieten.

Insgesamt ist das Problem so komplex das sich objektorientierte Programmierung (OOP) lohnt um das übersichtlicher zu machen.

Ist Pygame eine Vorgabe? Das ist ja doch ziemlich „low level“.
Kabee
User
Beiträge: 2
Registriert: Donnerstag 21. Januar 2016, 15:41

Haha, ja, einen Quellcode für Senso habe ich auch zur Hilfe genommen um ein wenig Input zu bekommen :D

Deine Antwort muss ich nun erstmal in Ruhe durchlesen, zum Thema PyGame ist das jedoch so eine Sache. Wir haben bisher nur damit gearbeitet, jedoch haben wir keine direkte Vorgabe bekommen was genutzt werden kann.

Inwiefern wäre es durch objektorientierte Programmierung leichter?

Zu den anderen Punkten:

Das Dictionary inkl. der anführenden leeren Zeichenkette wurde mir so von einer studentischen Hilfe empfohlen, ohne das ich ihre Erklärung wirklich nachvollziehen konnte. Dein Ratschlag wäre also, auf den Gebrauch eines Dictionarys + Liste zu verzichten und alles in einer Liste zusammenzufassen und random.shuffle() direkt auf diese anzuwenden?

Den weiteren Einwand, alles in einem Objekt zusammenzufassen, was mit den Schaltflächen zu tun hat, kann ich nachvollziehen.
Ich habe auf meinem Weg auch die Befürchtung gehabt im Nachhinein nicht mit dem Befehl .collidepoint() arbeiten zu können, da die einzelnen .rect Befehle in der draw_button Funktion stecken.

Jetzt wäre die Frage: Kann ich es dann so simpel machen, dass ich in der Liste jedem Wert von 0-8 direkt den Befehl pygame.draw.rect mit den entsprechenden Koordinaten zuordne und diese Liste direkt shuffeln lasse? Dann könnte ich einfach immer die Position ausgeben lassen, die mit dem Wert des aktuellen Versuchs übereinstimmen oder niedriger sind.


Danke das du hilft, so ganz ohne Ansatz kann man sich echt in solche Sachen verbeißen.

EDIT: Achja, hier noch der Teil des Codes der dann die Kästchen verändern soll

Code: Alles auswählen

draw_button(button_dictionary[stimuli_list[num_loopbutton]], col_red, col_black)
BlackJack

@Kabee: OOP macht es leichter so ein Programm sinnvoll zu strukturieren. GUI-Programmierung ist in der Regel ereignisbasierte Programmierung, dass heisst man programmiert nicht mehr einen linearen Programmablauf, sondern man reagiert auf Ereignisse und verändert daraufhin den Zustand des Programms. Wenn man das rein funktional machen möchte, muss man irgendwann ziemlich viel Zustand über Argumente und Rückgabewerte in der Gegend herum reichen. Oder man steckt den Zustand in Datenstrukturen die man übergibt und dann in den Funktionen verändert. Dann ist man eigentlich schon fast bei OOP angelangt, nur ohne die Sprachunterstützung dafür zu benutzen. Die gibt es aber und die macht es einfacher zu schreiben und zu lesen als wenn man das mit Funktionen selbst nachbaut was die Sprache schon bietet.

Wofür ist das erste Element denn gedacht? Wie gesagt, das ist der Nachteil von Listen mit ”anonymen” Elementen — der Leser weiss nicht so ohne weiteres was die bedeuten. Schon ein Wörterbuch kann da hilfreich sein:

Code: Alles auswählen

# Anstatt:
button = ['', 70, 60, 60, 60]
# Verständlicher:
button = {'?': '', 'x': 70, 'y': 60, 'width': 60, 'height': 60}
Wie man sieht konnte ich die Bedeutung der Zahlen alle erraten, aber dazu musste ich erst den anderen Code lesen um sicher zu sein. Bei der leeren Zeichenkette musste ich mir irgendeinen Schlüssel ausdenken, weil ich ohne den Code zu sehen der etwas damit anstellt, nicht einmal ansatzweise eine Ahnung habe was der Wert an Index 0 bedeuten könnte.

Da Breite und Höhe immer gleich sind, könnte man auch einfach eine Angabe, zum Beispiel 'size', stattdessen verwenden.

Code: Alles auswählen

def create_button(x, y, size):
    return {'?': '', 'x': x, 'y': y, 'size': size}

def button_collides_with_point(button, point):
    x, y = point
    return (
      button['x'] <= x <= button['x'] + button['size']
      and button['y'] <= y <= button['y'] + button['size']
    )
Oder aber wie schon gesagt, gleich `pygame.Rect`-Exemplare:

Code: Alles auswählen

def create_button(x, y, size):
    return {'?': '', 'rect': Rect(x, y, size, size)}

def button_collides_with_point(button, point):
    return button['rect'].collidepoint(point)
Ja, ich würde Indirektionen vermeiden wo man sie nicht braucht und gleich eine Liste mit Button-Objekten erstellen. Wenn man die immer auch in der Originalreihenfolge benötigt, kann man davon ja eine Kopie erstellen die man dann `random.shuffle()` übergibt. Oder man verwendet `random.sample()` um sich eine zufällige Anzahl von Elemente als Liste geben zu lassen, ohne die Ausgangsliste zu verändern.

Du kannst die `pygame.draw.rect()`-Funktion nicht direkt in die Liste stecken. Man kann sich aber eine Funktion schreiben, die eine `button`-Datenstruktur übergeben bekommt, und die anderen nötigen Werte:

Code: Alles auswählen

def draw_button(button, screen):
    pygame.draw.rect(
        screen, ON_COLOR if button['state'] else OFF_COLOR, button['rect']
    )
Hier gehe ich davon aus, dass der `button` einen 'state'-Wert hat, welcher entweder `True` oder `False` ist. Je nach dem ob der Button ”leuchtet”/”angeschaltet” ist oder nicht. Und das es zwei Konstanten auf Modulebene gibt, welche die Farben für ”an” und ”aus” repräsentieren.

Auf ”Kollision” mit der Maus würde ich da nicht drin testen, denn das gehört eher nicht in so eine Funktion. Denn das will man ja eigentlich nicht jedes mal machen wenn die Schaltfläche neu gezeichnet wird, sondern nur wenn der Benutzer mit der Maus klickt. Und da, damit der Benutzer das Verhalten erlebt was er von Schaltflächen in anderen Programmen gewohnt ist, auch erst wenn er die Maustaste loslässt und nicht schon wenn er sie drückt. Das wäre etwas für eine Funktion die mit der/einer Sammlung von Buttons operiert, die ihrerseits eine Zusammenfassung von verschiedenen Werten ist:

Code: Alles auswählen

def mouse_release_on_buttons(buttons, point):
    for button in buttons['items']:
        if button_collides_with_point(button, point):
            if not button['state']:
                button['state'] = ON
                buttons['selected_items'].append(button)
                draw_button(button, buttons['screen'])
Das ist jetzt nicht getestet und nicht komplett durchgeplant, aber so ungefähr könnte das aussehen wenn man sich Funktionen für diese Aufgabe schreibt, die auf Datenstrukturen mit den Werten operieren. Und zwar im Grunde schon objektorientiert, denn die verschiedenen Wörterbuch”arten” und die Funktionen die darauf operieren, ergeben zusammengenommen von der Idee her Objekte. Wenn man dann tatsächlich die OOP-Konstrukte verwendet, die von Python zur Verfügung gestellt werden, kann man den Code noch etwas schöner und kompakter schreiben. Zum einen weil man die Funktionsnamen kürzen kann, da zwei verschiedene Datentypen Methoden mit gleichen Namen haben können, man also nicht wie bei den Funktionen darauf achten muss, dass die Namen (modul)global eindeutig sind. Zum anderen weil man die ”magischen” Methoden implementieren kann damit die eigenen Datentypen mit Python's Schlüsselworten und Operatoren verwendet werden können. Die letzte Funktion könnte man als Methode dann beispielsweise so schreiben:

Code: Alles auswählen

class Buttons(object):
    
    # ...

    def __getitem__(self, index):
        return self.items[index]

    def on_mouse_release(self, point):
        for button in self:
            if point in button and not button.state:
                button.state = ON
                self.selected_items.append(button)
                button.draw(self.screen)
Wie gesagt, alles recht ungeplant und aus der Hüfte geschossen — muss jetzt nicht der beste Ansatz sein. Wahrscheinlich ist `Buttons` auch eher ein übergeordneter Typ der eher `Game` oder `World` heissen sollte. Ich würde mir zuerst Gedanken über das Modell/die Geschäftslogik ohne die GUI machen, und welche Daten und Operationen man da identifizieren und zu Objekten zusammenfassen kann.

Bei Pygame hat man das ”Problem”, dass man sich die Ereignisschleife und die Abbildung auf Behandlungsroutinen selber basteln muss. GUI-Toolkits bieten so etwas in der Regel schon von Haus aus. Da muss man nicht selber durch die Ereignisse gehen auf Maustaste loslassen prüfen und dann alle Schaltflächen durchgehen und prüfen ob/über welcher die Maustaste losgelassen wurde, sondern da kann man einfach sagen: Rufe diese Funktion oder Methode auf wenn die Maustaste über diesem GUI-Objekt losgelassen wurde.
BlackJack

Ich hab's mal mit Tk statt Pygame versucht:

Code: Alles auswählen

#!/usr/bin/env python
# coding: utf8
from __future__ import absolute_import, division, print_function
import random
import Tkinter as tk
from functools import partial

BLOCK_SIZE = 60
BLOCKS_SIZE = 500
BLOCK_COORDINATES = [
    (70, 60),
    (100, 210),
    (100, 320),
    (200, 40),
    (320, 120),
    (420, 80),
    (380, 240),
    (300, 350),
    (400, 420),
]
BLOCK_COUNT = len(BLOCK_COORDINATES)
ON, OFF = True, False
ON_COLOR, OFF_COLOR = 'red', 'black'
SEQUENCE_DELAY = 1000


class Block(object):

    def __init__(self):
        self.state = OFF


class Blocks(object):

    def __init__(self, items=()):
        self.items = list(items)

    def __cmp__(self, other):
        return cmp(self.items, other.items)

    def __len__(self):
        return len(self.items)

    def __getitem__(self, index):
        return self.items[index]

    def add(self, block):
        self.items.append(block)

    def turn_off(self):
        for block in self:
            block.state = OFF

    def sample(self, count):
        return type(self)(random.sample(self, count))

    @classmethod
    def from_count(cls, count):
        return cls(Block() for _ in xrange(count))


class CorsiBlockTapping(object):
    
    def __init__(self):
        self.blocks = Blocks.from_count(BLOCK_COUNT)
        self.level = 0
        self.excpected = None
        self.selected = None

    def prepare_level(self):
        if self.check_selected():
            self.level = min(self.level + 1, len(self.blocks))
        self.excpected = self.blocks.sample(self.level)
        self.selected = Blocks()
        self.blocks.turn_off()

    def iter_expected(self):
        return iter(self.excpected)

    def add_selected(self, block):
        self.selected.add(block)

    def check_selected(self):
        return self.selected == self.excpected


class BlockUI(object):

    def __init__(self, canvas, coordinate, block):
        self.canvas = canvas
        self.block = block
        x, y = coordinate
        self.tag = self.canvas.create_rectangle(
            x, y, x + BLOCK_SIZE, y + BLOCK_SIZE, fill=OFF_COLOR
        )

    @property
    def state(self):
        return self.block.state

    @state.setter
    def state(self, value):
        self.block.state = value
        self.update_display()

    def bind(self, sequence, callback):
        self.canvas.tag_bind(self.tag, sequence, partial(callback, self))

    def update_display(self):
        self.canvas.itemconfig(
            self.tag, fill=ON_COLOR if self.state else OFF_COLOR
        )


class BlocksUI(tk.Canvas):

    def __init__(self, parent, blocks, on_selected, on_sequence_finished):
        tk.Canvas.__init__(self, parent, width=BLOCKS_SIZE, height=BLOCKS_SIZE)
        self.blocks = blocks
        self.on_selected = on_selected
        self.on_sequence_finished = on_sequence_finished
        self.is_active = False
        self.block2ui = dict()
        for block, coordinate in zip(self.blocks, BLOCK_COORDINATES):
            block_ui = BlockUI(self, coordinate, block)
            block_ui.bind('<Button-1>', self.on_mouse_button)
            self.block2ui[block] = block_ui

    def on_mouse_button(self, block_ui, _event=None):
        if self.is_active and not block_ui.state:
            block_ui.state = ON
            self.on_selected(block_ui)

    def update_display(self):
        for block_ui in self.block2ui.itervalues():
            block_ui.update_display()

    def _play_step(self, block_iter, current_block=None):
        if current_block:
            self.block2ui[current_block].state = OFF
        try:
            current_block = next(block_iter)
        except StopIteration:
            self.is_active = True
            self.on_sequence_finished()
        else:
            self.block2ui[current_block].state = ON
            self.after(
                SEQUENCE_DELAY, self._play_step, block_iter, current_block
            )
        
    def play_sequence(self, blocks):
        self.is_active = False
        self.update_display()
        self.after(SEQUENCE_DELAY, self._play_step, iter(blocks))


class CorsiBlockTappingUI(tk.Frame):

    def __init__(self, parent, corsi_block_tapping):
        tk.Frame.__init__(self, parent)
        self.corsi_block_tapping = corsi_block_tapping
        self.blocks_ui = BlocksUI(
            self,
            self.corsi_block_tapping.blocks,
            self.on_block_selected,
            self.on_sequence_finished,
        )
        self.blocks_ui.pack(side=tk.TOP)
        frame = tk.Frame(self)
        self.level_label = tk.Label(frame)
        self.level_label.pack(side=tk.LEFT)
        self.done_button = tk.Button(frame, text='GO', command=self.on_done)
        self.done_button.pack(side=tk.RIGHT)
        frame.pack(side=tk.TOP, fill=tk.X)
        self.update_display()

    def update_display(self):
        self.level_label['text'] = 'Level: {0}'.format(
            self.corsi_block_tapping.level
        )

    def on_block_selected(self, block_ui):
        self.corsi_block_tapping.add_selected(block_ui.block)

    def on_sequence_finished(self):
        self.done_button.config(state=tk.NORMAL, text='Done')

    def on_done(self):
        self.done_button['state'] = tk.DISABLED
        self.level_label['background'] = (
            'green' if self.corsi_block_tapping.check_selected() else 'red'
        )
        self.corsi_block_tapping.prepare_level()
        self.update_display()
        self.blocks_ui.play_sequence(self.corsi_block_tapping.excpected)


def main():
    root = tk.Tk()
    root.title('Corsi Block Tapping')
    frame = CorsiBlockTappingUI(root, CorsiBlockTapping())
    frame.pack()
    root.mainloop()


if __name__ == '__main__':
    main()
Antworten