Schnelles Malrechnen für den Sohn

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
Mardor
User
Beiträge: 27
Registriert: Montag 9. Februar 2015, 19:07

Hallo,

mein Sohn muss in der Schule schnelles Malrechnen lernen. Meine Idee war ein Programm zu schreiben das Malaufgaben stellt. Für jede richtige Antwort gibt es +10 Punkte, für jede falsche Antwort gibt es -10 Punkte. Soweit sogut. Da dies aber schnell gehen soll soll hier noch ein Timer rein. Dieser beginnt bei 10 Sekunden. Ist die Antwort korrekt dann wird die Zeit um eine Sekunde minimiert, ist die Antwort falsch dann bleibt die Zeit gleich. Wird innerhalb der Zeit keine Eingabe getätigt soll dies als falsche Antwort gewertet werden.

Soweit sogut:

Mein Ansatz soweit ist

Code: Alles auswählen

from threading import Timer
from random import randint


def Spielregeln():
	print ("")
	print ("--- 1x1 bis 12x12 Spiel ---")
	print ("")
	print ("Spielregeln: Du musst so schnell als Möglich 100 Punkte erreichen")
	print ("Richtige Antworten geben 10 Punkte, aber auch 1 Sekunde weniger Zeit für die nächste Antwort")
	print ("Falsche Antworten oder nicht beantwortete Frage geben 10 Minuspunkte")
	print ("")

def multirand():
	ran1 = (randint(1,12))
	ran2 = (randint(1,10))
	erg = ran1 * ran2
	return ran1,ran2, erg

timeout = 10
punkt = 0

Spielregeln()

while punkt <= 100:
	t = Timer(timeout, print, ['Sorry, times up'])
	t.start()

	ran1,ran2,erg = multirand()


	print ("Wieviel ist " + str(ran1) + " * " + str(ran2) + " ?")
	prompt = "Du hast %d Sekunden für die korrekte Antwort ...\n" % timeout
	answer = input(prompt)

	if answer == erg:
		print ("Antwort ist korrekt")
		punkt =+ 10

	if answer != erg:
		print ("Antwort ist falsch")
		punkt =- 10

	t.cancel()
Nun habe ich das Problem, da ich auch bei korrekter Antwort nie in die "Antwort korrekt" Schleife komme. Auch weis ich nicht genau wie ich den Zeitablauf genau definieren soll.

Ich wäre sehr froh wenn mir hier jemand helfen könnte.

Gruß Mardor
Zuletzt geändert von Anonymous am Montag 26. September 2016, 17:39, insgesamt 1-mal geändert.
Grund: Quelltext in Python-Codebox-Tags gesetzt.
BlackJack

@Mardor: Also zum Quelltext erst einmal: Der Code auf Modulebene der keine Konstanten, Funktionen, oder Klassen definiert, sollte selbst in einer Funktion stecken. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst.

Mit Grossbuchstaben fangen üblicherweise nur Klassen an (und Konstanten, die komplett in Grossbuchstaben geschrieben werden) und Funktionen werden nach der Tätigkeit benannt, die sie ausführen um sie von eher passiven Werten unterscheiden zu können. `Spielregeln` ist da sehr irreführend weil es wie eine Klasse geschrieben wird und ein Hauptwort ist, das die Tätigkeit der Funktion nicht wirklich beschreibt. An der Stelle wo der Aufruf steht, fragt man sich dann warum das Objekt das anscheinend Spielregeln modelliert, nicht an einen Namen gebunden wird.

`multirand()` ist auch nicht wirklich selbsterklärend und die lokalen Namen sind komische Abkürzungen. Warum nicht `ergebnis` schreiben wenn man `ergebnis` meint?

`punkt` wäre für mein Gefühl eher `punkte`.

Zwischen Funktionsname und öffnender Klammer des Aufrufs gehört kein Leerzeichen.

Zeichenketten und Werte mit `str()` und ``+`` zusammenbauen ist eher BASIC und nicht Python. In Python gibt es dafür Zeichenkettenformatierung mit der `format()`-Methode auf Zeichenketten. Oder dem ``%``-Operator, das ist aber die alte Variante.

Die beiden ``if``\s wären besser ein ``if`` und ein ``else``, denn wenn die beiden Bedingungen schliessen sich gegenseitig aus, also braucht man nicht beide zu prüfen.

Die erste Bedingung trifft allerdings niemals zu, denn `answer` ist eine Zeichenkette und `erg` ist eine Zahl. Die sind niemals gleich.

Wenn Du die Eingabe in eine Zahl umwandelst, bekommst Du das Problem das `punkt` entweder an 10 oder an -10 gebunden wird, wo Du sicher um 10 erhöhen oder verringern wolltest. ``punkt =+ 10`` kann man auch so schreiben: ``punkt = +10``. Du wolltest sicher ``punkt += 10``. Und bei - analog.

Eine Texteingabe mit Zeitbeschränkung wäre schön, ist aber nur sehr umständlich zu haben und in der Regel muss man da auch für Windows und Linux/Mac/Unix unterschiedliche Lösungen schreiben oder die Plattformunabhängigkeit aufgeben.

Da würde ich eher eine GUI-Lösung schreiben. Und dort dann mit dem jeweiligen Mechanismus den die verwendete GUI-Bibliothek für nebenläufige Aktionen bietet. Bei Tk beispielsweise die `after()`-Methode auf Widgets.

Vor GUI sollte man sich mal mit objektorientierter Programmierung (OOP) beschäftigen, denn die braucht man dafür.
Mardor
User
Beiträge: 27
Registriert: Montag 9. Februar 2015, 19:07

Hallo BlackJack,

erstmal tausend Dank für deine Korrektur des Codes. Ich bin wirklich froh für jede deiner Anmerkungen und Korrekturen.
Das hat mir wirklich geholfen und ich habe viel dabei gelernt.

Ich habe den Code daraufhin abgeändert und wollte fragen, ob du nochmals drüberschauen kannst (als Kontrolle ob ich alles verstanden habe).

Code: Alles auswählen

from threading import Timer
from random import randint

def anzeige_spielregeln():
	print("")
	print("--- 1x1 bis 12x12 Spiel ---")
	print("")
	print("Spielregeln: Du musst so schnell als Möglich 100 punktee erreichen")
	print("Richtige Antworten geben 10 punktee, aber auch 1 Sekunde weniger Zeit für die nächste Antwort")
	print("Falsche Antworten oder nicht beantwortete Frage geben 10 Minuspunktee")
	print("")

def ermittlung_zufallszahlen():
	zufallszahl1 = (randint(1,12))
	zufallszahl2 = (randint(1,10))
	ergebnis_zufallszahl = zufallszahl1 * zufallszahl2
	return zufallszahl1,zufallszahl2, ergebnis_zufallszahl

timeout = 10
punkte = 0

anzeige_spielregeln()

while punkte <= 100:
	t = Timer(timeout, print, ['Sorry, times up'])
	t.start()

	zufallszahl1,zufallszahl2,ergebnis_zufallszahl = ermittlung_zufallszahlen()

	print("Wieviel ist {} * {} ?".format(str(zufallszahl1),str(zufallszahl2)))
	prompt = "Du hast %d Sekunden für die korrekte Antwort ...\n" % timeout
	answer = input(prompt)

	if int(answer) == ergebnis_zufallszahl:
		print("Antwort ist korrekt")
		punkte += 10
	else:
		print("Antwort ist falsch")
		punkte -= 10

	t.cancel()
Ein paar Punkte sind bei mir aber noch offen, die ich nicht verstehe:
@Mardor: Also zum Quelltext erst einmal: Der Code auf Modulebene der keine Konstanten, Funktionen, oder Klassen definiert, sollte selbst in einer Funktion stecken. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst.
Wo/wie genau würde ich main() aufrufen ? Vor der Funktion für die Spielregeln ?
Eine Texteingabe mit Zeitbeschränkung wäre schön, ist aber nur sehr umständlich zu haben und in der Regel muss man da auch für Windows und Linux/Mac/Unix unterschiedliche Lösungen schreiben oder die Plattformunabhängigkeit aufgeben.
Das Programm nur unter Ubuntu 16.04 laufen, niemals unter Windows. Gibt es da etwas spezielles ohne gleich ganz in die GUI Schiene einzusteigen ?

Vielen Dank und Gruß Mardor
Zuletzt geändert von Anonymous am Montag 26. September 2016, 19:41, insgesamt 1-mal geändert.
Grund: Quelltext in Python-Codebox-Tags gesetzt.
BlackJack

@Mardor: Man kann eine Funktion erst dann aufrufen wenn sie definiert ist. Üblicherweise ist die `main()`-Funktion die letzte Funktion die definiert wird, also da wo jetzt gerade Dein Hauptprogramm steht. Und ganz am Ende steht dann dieses Idiom:
if __name__ == '__main__':
main()
Dadurch wird `main()` aufgerufen wenn man das Modul als Programm ausführt, aber nicht wenn man es importiert. So kann man beispielsweise das Modul importieren um einzelne Funktionen zu testen, manuell in einer Python-Shell oder automatisiert mit Unit-Tests.

Unter unixoiden Systemen kann man entweder mit dem `select`-Modul und `sys.stdin` arbeiten, oder sich mit dem `signal`-Modul eine Alarm-Unterbrechung basteln.
Mardor
User
Beiträge: 27
Registriert: Montag 9. Februar 2015, 19:07

Hallo BlackJack,

ich denke ich habe nun alles eingefügt und geändert. Die Anforderungen habe ich einfach so angepasst, dass das Spiel nach 10 Sekunden vorbei ist. Allerdings schaffe ich es nicht ohne Fehlermeldung aus dem Programm zu kommen. Ich habe diverse Versionen ausprobiert. Weder wie in manchen Internetseiten beschrieben: sys.exit, quit(), exit etc. funktionieren.

Code: Alles auswählen

from threading import Timer
import random
from random import randint
import sys

def anzeige_spielregeln():
	print("")
	print("--- 1x1 bis 10x10 Spiel ---")
	print("")
	print("Spielregeln: Du musst so schnell als Möglich die Zielpunkte erreichen")
	print("Richtige Antworten geben 10 Punkte")
	print("Falsche Antworten geben 20 Minuspunkte")
	print("")

def ermittlung_zufallszahlen():
	random.seed()
	zufallszahl1 = (randint(1,10))
	zufallszahl2 = (randint(1,10))
	ergebnis_zufallszahl = zufallszahl1 * zufallszahl2
	return zufallszahl1,zufallszahl2, ergebnis_zufallszahl

def anzeige_verloren():
	print("Du hast zu lange gebraucht und hast damit verloren")
	sys.exit()

def main():

	timeout = 10
	punkte = 0
	versuche = 0
	zielpunkte = 100

	anzeige_spielregeln()

	while punkte < int(zielpunkte):
		versuche += 1
		t = Timer(timeout, anzeige_verloren)
		t.start()

		zufallszahl1,zufallszahl2,ergebnis_zufallszahl = ermittlung_zufallszahlen()

		print("")
		print("Wieviel ist {} * {} ?".format(str(zufallszahl1),str(zufallszahl2)))
		prompt = "Du hast %d Sekunden für die korrekte Antwort ...\n" % timeout
		answer = input(prompt)

		if int(answer) == ergebnis_zufallszahl:
			punkte += 10
			print("Antwort ist KORREKT, du hast jetzt {} Punkte".format(str(punkte)))
		else:
			print("Antwort ist FALSCH, du hast jetzt {} Punkte".format(str(punkte)))
			punkte -= 20

		t.cancel()

	print("")
	print("Du hast die {} Punkte mit insgesamt {} versuchen geschafft".format(str(zielpunkte),str(versuche)))

if __name__=='__main__':
    main()
Hättest du noch einen Tipp ?

Gruß Mardor
BlackJack

@Mardor: Das `random.seed()` wirfst Du am besten gleich wieder raus. Diese Funktion ist im besten fall einfach überflüssig und sorgt in schlechteren Fällen für *weniger* Zufall. Man braucht die eigentlich nur wenn man eine immer gleiche Abfolge von ”Zufallszahlen” braucht, zum Beispiel für Testzwecke oder wiederholbare und vergleichbare Messungen von Laufzeiten.

`zielpunkte` ist eine ganze Zahl, was soll denn da die `int()`-Funktion für einen Sinn machen? Die Klammern um die `randind()`-Aufrufe sind unnötig. Ebenso die `str()`-Aufrufe bei den `format()`-Argumenten.

Den Prozess kann man nur sauber aus dem Hauptthread aus beenden. Wie gesagt, die üblichen Lösungen sind `select` oder `signal`. Wobei ich `select` für einfacher verständlich und durchschaubar halte. Oder eben gleich eine GUI die dann auch nicht nur unter Linux & Co. funktioniert.

Die Benutzereingabe ist nicht besonders robust. Wenn man kurz vor dem erreichen der 100 Punkte steht und sich in der hektik vertippt und etwas eingibt was nicht zu einer ganzen Zahl gehört, dann sollte das Programm IMHO das als falsche Antwort werten und nicht komplett abbrechen. So etwas frustriert den Benutzer schnell.
Sirius3
User
Beiträge: 17712
Registriert: Sonntag 21. Oktober 2012, 17:20

@Mardor: eine Lese-Funktion könnte so aussehen:

Code: Alles auswählen

import select                                                                                                                        
def input_with_timeout(prompt="> ", timeout=10):
    print(prompt, end='')
    ready, _, _ = select.select([sys.stdin], [], [], timeout)
    if ready:                                                                                                                            
        return sys.stdin.readline()
    else:
        return None
Das hat aber das Problem, das bereits Eingegebenes nicht verworfen wird. Vielleicht solltest Du Dir curses anschauen?
BlackJack

@Mardor: Beim ersten Quelltext sieht man übrigens einen guten Grund warum man Redundanz vermeiden sollte. Der Spielregeltext sagt es ist das 1x1 bis 12x12 Spiel, aber eigentlich ist es das 1x1 bis 12x10 Spiel. In den folgenden Quelltexten stimmen die Zahlen dann zwar überein, aber die Gefahr das das wieder ”kaputt” geht, besteht weiterhin. Wenn man die Grenzen anpassen will, muss man daran denken *alle* Vorkommen der Zahl anzupassen. Sowohl als Zahlen, als auch in Texten. So etwas umgeht man üblicherweise durch definieren von Konstanten, so dass man den Quelltext nur noch an dieser Stelle einmal ändern muss, und sich das automatisch auf das gesamte Programm auswirkt.

Hier mal ein Ansatz das mit einer GUI umzusetzen und mit der ursprünglich geplanten Regel das die Antwortzeiten kürzer werden wenn man die Zeit überschreitet:

Code: Alles auswählen

from random import randint
import tkinter as tk
from tkinter.messagebox import showerror, showinfo

LOWER_LIMIT = 1
UPPER_LIMIT = 12
POINTS_FOR_CORRECT_ANSWER = 10
POINTS_FOR_INCORRECT_ANSWER = -20
GOAL = 100
RULES_TEXT = (
    '\n'
    '— {0}×{0} bis {1}×{1} Spiel —\n'
    '\n'
    'Du musst so schnell als möglich die Zielpunkte erreichen.\n'
    'Richtige Antworten geben {2} Punkte.\n'
    'Falsche Antworten geben {3} Minuspunkte.\n'
    '\n'.format(
        LOWER_LIMIT,
        UPPER_LIMIT,
        POINTS_FOR_CORRECT_ANSWER,
        -POINTS_FOR_INCORRECT_ANSWER,
    )
)


class MultiplicationGame(object):

    def __init__(self):
        self.timeout = 10
        self.points = 0
        self.trial_count = 0
        self.operand_a = None
        self.operand_b = None

    @property
    def is_over(self):
        return self.timeout == 0 or self.points >= GOAL

    def _adjust_points(self, amount):
        self.points += amount
        self.trial_count += 1

    def start_round(self):
        self.operand_a = randint(LOWER_LIMIT, UPPER_LIMIT)
        self.operand_b = randint(LOWER_LIMIT, UPPER_LIMIT)
        return self.operand_a, self.operand_b

    def check_result(self, value):
        result = (
            False if value is None else self.operand_a * self.operand_b == value
        )
        self._adjust_points(
            POINTS_FOR_CORRECT_ANSWER if result else POINTS_FOR_INCORRECT_ANSWER
        )
        return result

    def timed_out(self):
        self.timeout -= 1
        self._adjust_points(POINTS_FOR_INCORRECT_ANSWER)


class MultiplicationGameUI(tk.Frame):

    def __init__(self, parent):
        tk.Frame.__init__(self, parent)
        
        self.timer_id = None

        frame = tk.LabelFrame(self, text='Spielregeln')
        tk.Label(frame, text=RULES_TEXT).pack()
        frame.pack(side=tk.TOP)

        frame = tk.Frame(self)
        question_frame = tk.LabelFrame(frame, text='Aufgabe')
        self.question_var = tk.StringVar()
        tk.Label(question_frame, textvariable=self.question_var).pack()
        question_frame.pack(side=tk.LEFT, fill=tk.X, expand=True)

        points_frame = tk.LabelFrame(frame, text='Punkte')
        self.points_var = tk.IntVar()
        tk.Label(points_frame, textvariable=self.points_var).pack()
        points_frame.pack(side=tk.LEFT)
        
        countdown_frame = tk.LabelFrame(frame, text='Zeit')
        self.countdown_var = tk.IntVar()
        tk.Label(countdown_frame, textvariable=self.countdown_var).pack()
        countdown_frame.pack(side=tk.LEFT)
        frame.pack(side=tk.TOP, fill=tk.X)

        frame = tk.Frame(self)
        answer_frame = tk.LabelFrame(frame, text='Antwort')
        self.answer_var = tk.StringVar()
        self.answer_entry = tk.Entry(answer_frame, textvariable=self.answer_var)
        self.answer_entry.pack(fill=tk.X)
        self.answer_entry.bind('<Return>', self.do_check_result)
        answer_frame.pack(side=tk.LEFT, fill=tk.X, expand=True)

        self.button = tk.Button(frame)
        self.button.pack(side=tk.LEFT, fill=tk.BOTH)
        frame.pack(side=tk.TOP, fill=tk.X)
        self.create_new_game()

    def create_new_game(self):
        self.game = MultiplicationGame()
        for var in [
            self.question_var,
            self.points_var,
            self.countdown_var,
            self.answer_var
        ]:
            var.set('')
        self.button.configure(text='Start', command=self.do_start)
        self.button.focus()

    def do_start(self):
        self.button.configure(text='OK', command=self.do_check_result)
        self.do_round()

    def do_countdown(self):
        value = self.countdown_var.get()
        if value > 0:
            self.countdown_var.set(value - 1)
            self.timer_id = self.after(1000, self.do_countdown)
        else:
            self.timer_id = None
            self.game.timed_out()
            self.do_round()

    def do_check_result(self, _event=None):
        if self.timer_id is not None:
            self.after_cancel(self.timer_id)
            self.timer_id = None
            try:
                answer = int(self.answer_var.get())
            except ValueError:
                answer = None
            self.game.check_result(answer)
            self.do_round()

    def do_round(self):
        self.points_var.set(self.game.points)

        if self.game.is_over:
            if self.game.timeout == 0:
                showerror(
                    'Ooooooh...',
                    'Du hast zu oft die Zeit überschritten und nur'
                    ' {0} Punkte erreicht.'.format(self.game.points),
                )
            elif self.game.points >= GOAL:
                showinfo(
                    'Yeaah!',
                    'Du hast die {0} Punkte mit insgesamt {1} Versuchen'
                    ' erreicht.'.format(GOAL, self.game.trial_count),
                )
            else:
                assert False, 'neither won nor zero timeout should not happen'
            self.create_new_game()

        else:
            operand_a, operand_b = self.game.start_round()
            self.question_var.set(
                'Wieviel ist {0} × {1}'.format(operand_a, operand_b)
            )
            self.answer_var.set('')
            self.answer_entry.focus()
            self.countdown_var.set(self.game.timeout + 1)
            self.do_countdown()


def main():
    root = tk.Tk()
    root.title('Multiplikationsspiel')
    game_ui = MultiplicationGameUI(root)
    game_ui.pack()
    root.mainloop()


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