erstes py"game"

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
Antworten
je_manth
User
Beiträge: 2
Registriert: Donnerstag 1. Februar 2024, 09:44

Hallo zusammen,
ich bin noch sehr frisch in Python und möchte hier mein erstes "Game" vorstellen. Hab hier mit random, date, time, Datei speichern, Tastatur- und Mausabfrage rum gespielt. Es macht was es 'soll' aber vermutlich könnte man den Code noch etwas eleganter schreiben?!
Das wäre meine Bitte an euch - mir hierzu Tips geben.
Vielen Dank im Voraus

..................................................................................................................................................

# auf dem Screen werden an zufälligen Stellen Punkte in zufälliger Größe und zufälliger
# Farbe erzeugt. Diese müssen mit der Maus geklickt werden.
# Es wird die Reaktionszeit gemessen.
# Die durchschnittliche ReaktZeit wird in einer Datei "zeiten.txt" gespeichert.

import pygame
import random as rnd
import sys, datetime
from os import path
from datetime import timedelta
from pygame.locals import *
from bt_vars import *

# Initialisierung von pg - IMMER nötig
pygame.init()
# Erzeugung des Screens und Benamung in der Titelzeile
SCR = pygame.display.set_mode((SCR_W, SCR_H))
pygame.display.set_caption("BT-Punkte fangen")

#global aktiv
# diverse var werden definiert
clock = pygame.time.Clock()
FPS = FPS
font = pygame.font.SysFont("Arial",30)
f_p = path.realpath("res/zeiten.txt")
# hat das Spiel begonnen (True) oder noch nicht (False). Für die Anzeige der Regeln
aktiv = False
ANZ_RUNDEN = ANZ_RUNDEN_GES
# liste die alle Dauer Werte aufnimmt
l_Dauer = [0]
l_Dauer_avg = sum(l_Dauer)/len(l_Dauer)
start_time = datetime.datetime.now()
dt_avg = 0

class Ball():

def __init__(self,x,y):
self.new_ball(x,y)

def draw_ball(self):
pygame.draw.circle(SCR, self.col_ball_PK, (self.x + self.ball_rad, self.y + self.ball_rad), self.ball_rad)

def new_ball(self,x,y):
self.x = x
self.y = y
self.ball_rad = rnd.randint(5, 20)
self.col_ball_PK = (10,rnd.randint(1,255),rnd.randint(1,255))
self.rect = Rect(self.x, self.y, self.ball_rad, self.ball_rad)
start_time = get_Time()

def mouse_ball(self, pos):
# Prüfung, ob die Maus die gleichen Koordinaten wie der Punkt hat
#global ANZ_RUNDEN, aktiv
if pos[0] >= self.rect.x and pos[0] <= (self.rect.x + self.ball_rad*2) and pos[1] >= self.rect.y and pos[1] <= self.rect.y + self.ball_rad*2:
# Timestamp wird ausgelesen
end_time = get_Time()
# die Dauer der beiden Mouseklicks wird errechnet
dauer_time = round(timedelta.total_seconds(end_time - start_time),4)
# die Dauer jeder Runde wird an die List angehängt
l_Dauer.append(dauer_time)
# ein neuer Ball wird aufgerufen
ball.new_ball(rnd.randint(15, SCR_W - 40),rnd.randint(15, SCR_H - 40))

def get_Time():
# liest die aktuelle Uhrzeit aus
return datetime.datetime.now()

def anz_reaktion():
global dt_avg, dt, l_Dauer
# Berechnung der Reaktionszeit. Wenn erst ein Element in der Liste, dann wird dieses angezeigt
# wenn mehr als zwei in der Liste, werden die letzten beiden Elemente subtrahiert
if len(l_Dauer) <= 1:
dt = round(l_Dauer[0],4)
else:
dt = round(l_Dauer[-1] - l_Dauer[-2],4)
draw_text("Reaktionszeit: " + str(dt), font, col_text, 50, SCR_H // 7 -40)
dt_avg = round(sum(l_Dauer)/len(l_Dauer)-1, 4)

def rahmen():
#Zeichnet den Rahmen um das Spielfeld (nur für die Optik)
pygame.draw.rect(SCR, col_rahmen_PK, [0, 0, SCR_W, SCR_H], 15)

# stellt Text auf dem Bildschirm dar
def draw_text(text, font, col_text, x, y):
img = font.render(text, True, col_text)
SCR.blit(img, (x, y))

def spielbeschreibung():
# wird angezeigt bevor das Spiel los geht
draw_text("- - - P u n k t e f a n g e n - - -", font, col_text, 50, SCR_H // 7 -40)
draw_text("Klicken Sie mit der Maus auf den Punkt.", font, col_text, 50, SCR_H // 7)
draw_text("Gefragt ist Geschwindigkeit.", font, col_text, 50, SCR_H // 7 + 80)
draw_text("Größe,Farbe und Ort variieren.", font, col_text, 50, SCR_H // 7 + 40)
draw_text("Drücken Sie die Space-Taste zum Start", font, col_text, 50, SCR_H // 7 + 120)

def start_sequenz():
global aktiv, start_time, dt_avg, ANZ_RUNDEN
# Spiel wird gestartet, Startzeit wird abgefragt und ein erster Ball erzeugt
SCR.fill(col_bg_PK)
aktiv = True
start_time = get_Time()
ANZ_RUNDEN = ANZ_RUNDEN_GES
dt_avg = 0
ball.new_ball(rnd.randint(15, SCR_W - 40),rnd.randint(15, SCR_H - 40))

def neu_ende():
# Abfrage ob noch eine Runde gespielt werden soll oder nicht
global run
SCR.fill(col_bg_PK)

t = datetime.datetime.now()
t1 = t.strftime('%d-%m-%Y %H:%M:%S')
draw_text("Wollen Sie nochmal? j / n", font, col_text, 50, SCR_H // 7 + 260)
pygame.time.wait(1000)
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_j:
with open (f_p, "a") as dat:
dat.write("Zeiten von " + str(t1) + ": " + str(dt_avg))
dat.write("\n")
start_sequenz()

if event.key == pygame.K_n:
run = False
pygame.QUIT

# hier wird ein ball der Klasse Ball außerhalb des Spielfeldes erzeugt
ball = Ball(-100,-100,)

# die var fragt ab, ob das Spiel eigentlich noch läuft, wenn wahr, dann geht und bleibt es in der eigentlichen SpielSchleife
run = True
### hier beginnt die eigentliche Spielschleife ###
while run: #and ANZ_RUNDEN > 0:

clock.tick(FPS)
SCR.fill(col_bg_PK)
rahmen()

# Zeigt die Spielregeln
if aktiv == False:
spielbeschreibung()

if aktiv:
anz_reaktion()

# Abfrage ob eine Taste oder Maus gedrückt wurde
for event in pygame.event.get():
# wenn die Aktion das X rechts oben im Spielfenster war, dann wird das Spiel beendet
if event.type == pygame.QUIT:
run = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
start_sequenz()
#ball.new_ball(rnd.randint(15, SCR_W - 40),rnd.randint(15, SCR_H - 40))

if event.type == pygame.MOUSEBUTTONUP:
# ermittelt wo der Maouszeiger gerade steht (x,y)
pos = pygame.mouse.get_pos()
# vergleicht, ob die Mouse Position der des Balls gleich ist
ball.mouse_ball(pos)
ANZ_RUNDEN -= 1

ball.draw_ball()

if ANZ_RUNDEN == 0:
# ein Durchgang ist vorbei, Sprung in die fun mit j / n Abfrage
aktiv = False
neu_ende()

# für die immer wiederkehrende neue Anzeige des Screens
pygame.display.update()

# beendet das Spiel
pygame.quit

.............................................................................................................................................................

in dieser Datei (bt_vars.py) stehen einige Variablen:

.............................................................................................................................................................

import pygame
import random as rnd
# Constanten und Variablen

global ball_PK

# Größe der Spiel Screens
SCR_W = 1600
SCR_H = 700

# Anzahl der Runden, die das Spiel laufen soll
ANZ_RUNDEN_GES = 3
# FPS
FPS = 60
# Farben - bt_PunktKlick
col_bg_PK = (204, 204, 204)
col_rahmen_PK = (255, 153, 0)
col_text = (51, 51, 102)

dt = 0
...................
vielen Dank
...................
Benutzeravatar
__blackjack__
User
Beiträge: 13117
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@je_manth: Die Aufteilung auf zwei Module macht keinen Sinn. `bt_vars` sind ja einfach nur ein paar Konstanten die man auch einfach im Hauptmodul hätte definieren können.

Die Importe in diesem zweiten Modul werden dort überhaupt nicht verwendet.

`sys` wird importiert aber nirgends verwendet. `timedelta` wird importiert und falsch verwendet. Man ruft Methoden auf den Objekten auf, nicht auf der Klasse mit dem Objekt als Argument. `random` sollte nicht als `rnd` abgekürzt werden.

Statt `os.path` verwendet man das `pathlib`-Modul. Wobei der `realpath()`-Aufruf auch gar nicht nötig ist.

`datetime` ist das falsch Werkzeug um vergangene Zeit zu messen, insbesondere wenn das dann auch noch lokale Zeit ist, wo so etwas wie Zeitumstellungen und Schaltsekunden passieren kann. `time.momotonic()` ist das Mittel der Wahl hier.

Namen sollten keine kryptischen Abkürzungen enthalten. Die sollen dem Leser klar vermitteln was der Wert für eine Bedeutung hat, und keine Rätsel aufgeben. Wenn man `SCREEN_WIDTH` meint, sollte man nicht nur `SCR_W` schreiben. Dann kann man sich auch den erklärenden Kommentar sparen und man versteht das nicht nur dort wo der Wert definiert wird, sondern auch überall wo er verwendet wird.

Man sollte auch normale Wortreihenfolgen in zusammengesetzen Namen einhalten und kein „Yoda-Sprech“. Also aus `ANZ_RUNDEN_GES` nicht `ANZAHL_RUNDEN_GESAMT` machen, sondern `GESAMTRUNDENANZAHL`. Oder statt `col_bg_PK` besser `background_color`. Was das `PK` in dem Namen bedeuten soll — keine Ahnung. Darum sind Abkürzungen schlecht. Und selbst der Autor selbst vergisst so etwas wenn er in den Quelltext länger nicht reingeschaut hat, gerne mal.

Grundatentypen haben in Namen nichts verloren, auch nicht als Kürzel. `l_Dauer` könnte beispielsweise `reaktionszeiten` heissen, dann muss man da auch nicht Kommentieren das in der Liste Reaktionszeiten stehen.

``global`` hat auf Modulebene keinen Effekt. Und in Funktionen und Methoden hat das in einem sauberen Programm nichts zu suchen. Funktionen und Methoden bekommen alles was sie ausser Konstanten benötigen als Argumet(e) übergeben. Ergebnisse gibt man als Rückgabewert an den Aufrufer zurück. Man operiert nicht undurchsichtig und fehleranfällig auf globalen Variablen. Die sollte es gar nicht geben, denn Auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst.

Namen werden in Python klein_mit_unterstrichen geschrieben. Ausnahmen sind Konstanten (KOMPLETT_GROSS) und Klassen (PascalCase).

Funktionen werden üblicherweise nach der Tätigkeit benannt die sie durchführen, damit der Leser weiss was sie tun, und um sie leichter von eher passiven Werten unterscheiden zu können. Also `zeichne_rahmen()` statt `rahmen()`.

Kommentare sollen dem Leser einen Mehrwert über den Code geben. Faustregel: Kommentare beschreiben nicht *was* der Code macht, denn das steht da bereits als Code, sondern warum er das macht. Sofern das nicht offensichtlich ist. Offensichtlich ist in aller Regel auch was in der Dokumentation von Python und den verwendeten Bibliotheken steht. Und bevor man einen Namen Kommentiert, sollte man schauen ob man den nicht besser wählen kann, so dass man ihn nicht kommentieren muss.

`dt` wird nur in einer Funktion verwendet, und dort bei jedem Aufruf neu berechnet, ist aber trotzdem eine globale Variable. Das macht keinen Sinn.

``FPS = FPS`` macht offensichtlich keinen Sinn.

`l_Dauer_avg` wird definiert, aber nirgends verwendet.

Man macht keine Vergleiche mit literalen Wahrheitswerten. Bei dem Vergleich kommt doch nur wieder ein Wahrheitswert bei heraus. Entweder der, den man sowieso schon hatte; dann kann man den auch gleich nehmen. Oder das Gegenteil davon; dafür gibt es ``not``.

Wenn man etwas prüft und im nächsten ``if`` das Gegenteil prüft, ist das in der Regel kein eigenes ``if`` sondern ein ``else``.

Wenn man nacheinander sich gegenseitig ausschliessende Bedingungen prüft, nimmt man auch dafür keine ``if``\s sondern ``elif``.

`anz_reaktion()` ist ein schönes Beispiel warum Abkürzungen so blöd sind. An anderer Stelle steht `anz` für Anzahl, hier aber wohl für `anzeigen`. Die Funktion macht auch zu viel. Die zeigt nicht nur was an, sondern berechnet auch noch die durchschnittliche Reaktionszeit, obwohl die hier überhaupt gar nicht gebraucht wird, sondern an ganz anderer Stelle. Und man muss die auch nicht ständig neu berechnen, sondern nur wenn man braucht/anzeigt.

Das zusammenstückeln von Zeichenketten und Werten mittels ``+`` und `str()` ist eher BASIC als Python. Dafür gibt es die `format()`-Methode auf Zeichenketten und f-Zeichenkettenliterale.

`round()` verwendet man nur wenn man tatsächlich mit gerundeten Werten weiterrechnen will. Für die Anzeige von gerundeten Werten erledigt man das über die Formatspezifikation.

Die Klasse ist komisch strukturiert, beziehungsweise die Methodennamen sind falsch. `new_ball()` erzeugt gar keinen neuen Ball sondern ändert den bestehenden. `mouse_ball()` sagt nichts sinnvolles aus. Und das gehört auch nicht wirklich als Methode auf den Ball.

Man wiederholt den Klassennamen nicht in Attributnamen, das die Farbe und der Radius zum Ball gehört, sieht man ja bereits daran, dass die Attribute auf dem Ball-Objekt definiert sind.

Der Ball braucht keine `x` und `y`-Attribute denn das ist redundant weil die Information bereits in `rect` steckt. `rect` hat die falsche Grösse, denn das die Breite/Höhe ist *zweimal* der Radius. Und dann kann man auch das `Rect`-Objekt verwenden um zu testen, ob die Position innerhalb dieses Rechtecks liegt, statt das selbst nachzuprogrammieren.

Statt den Ball zu verändern würde man einfach einen neuen erstellen. Dann entspricht das was passiert, auch den Methodennamen und Kommentaren.

`new_ball()` wird an mehreren Stellen mit dem gleichen Code zum erstellen einer Zufallsposition aufgerufen — dieser Code sollte nur einmal existieren.

Beim öffnen von Textdateien sollte man immer explizit die Kodierung angeben.

Zwischenstand (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
#
# Auf dem screeneen werden an zufälligen Stellen Punkte in zufälliger Größe und
# zufälliger Farbe erzeugt. Diese müssen mit der Maus geklickt werden. Es wird
# die Reaktionszeit gemessen. Die durchschnittliche Reaktionszeit wird in einer
# Datei "zeiten.txt" gespeichert.
#
import random
import time
from datetime import datetime as DateTime

import pygame

SCREEN_WIDTH = 1600
SCREEN_HEIGHT = 700

GESAMTRUNDENANZAHL = 3
FPS = 60

BACKGROUND_COLOUR = (204, 204, 204)
FRAME_COLOUR = (255, 153, 0)
TEXT_COLOUR = (51, 51, 102)
TIMES_FILENAME = "res/zeiten.txt"


class Ball:
    def __init__(self, position, radius, colour):
        self.radius = radius
        self.colour = colour
        diameter = self.radius * 2
        self.rect = pygame.Rect(position, (diameter, diameter))

    def draw(self, surface):
        pygame.draw.circle(surface, self.colour, self.rect.center, self.radius)

    def check_collision(self, position):
        return self.rect.collidepoint(position)

    @classmethod
    def create_random(cls):
        return cls(
            (
                random.randint(15, SCREEN_WIDTH - 40),
                random.randint(15, SCREEN_HEIGHT - 40),
            ),
            random.randint(5, 20),
            (10, random.randint(1, 255), random.randint(1, 255)),
        )


get_time = time.monotonic


def zeichne_rahmen(surface):
    pygame.draw.rect(surface, FRAME_COLOUR, surface.get_rect(), 15)


def draw_text(surface, text, font, text_colour, position):
    surface.blit(font.render(text, True, text_colour), position)


def zeige_spielbeschreibung(surface, font):
    baseline = surface.get_height() // 7
    y_offset = -40
    for text in [
        "- - - P u n k t e   f a n g e n - - -",
        "Klicken Sie mit der Maus auf den Punkt.",
        "Größe, Farbe und Ort variieren.",
        "Gefragt ist Geschwindigkeit.",
        "Drücken Sie die Space-Taste zum Start",
    ]:
        draw_text(surface, text, font, TEXT_COLOUR, (50, baseline + y_offset))
        y_offset += 40


def starte_spiel(surface):
    surface.fill(BACKGROUND_COLOUR)
    return True, get_time(), GESAMTRUNDENANZAHL, Ball.create_random()


def zeige_reaktionszeit(surface, font, reaktionszeiten):
    reaktionszeit = (
        reaktionszeiten[0]
        if len(reaktionszeiten) <= 1
        else reaktionszeiten[-1] - reaktionszeiten[-2]
    )
    draw_text(
        surface,
        f"Reaktionszeit: {reaktionszeit:.4f}",
        font,
        TEXT_COLOUR,
        (50, surface.get_height() // 7 - 40),
    )


def frage_nach_neuem_spiel(surface, font, durchschnittliche_reaktionszeit):
    surface.fill(BACKGROUND_COLOUR)
    draw_text(
        surface,
        "Wollen Sie nochmal? j / n",
        font,
        TEXT_COLOUR,
        (50, surface.get_height() // 7 + 260),
    )
    pygame.time.wait(1000)
    for event in pygame.event.get():
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_j:
                with open(TIMES_FILENAME, "a", encoding="ascii") as datei:
                    datei.write(
                        f"Zeiten von {DateTime.now():%d-%m-%Y %H:%M:%S}:"
                        f" {durchschnittliche_reaktionszeit:.4f}\n"
                    )
                return True

            elif event.key == pygame.K_n:
                return False

    return False


def main():
    pygame.init()
    screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
    pygame.display.set_caption("BT-Punkte fangen")

    clock = pygame.time.Clock()
    font = pygame.font.SysFont("Arial", 30)
    #
    # Hat das Spiel begonnen (True) oder noch nicht (False). Für die Anzeige der
    # Regeln.
    #
    aktiv = False
    rundenanzahl = None
    reaktionszeiten = [0]
    ball = Ball((-100, -100), 1, (0, 0, 0))  # Außerhalb des Spielfeldes.
    run = True
    while run:
        clock.tick(FPS)
        screen.fill(BACKGROUND_COLOUR)
        zeichne_rahmen(screen)

        if aktiv:
            zeige_reaktionszeit(screen, font, reaktionszeiten)
        else:
            zeige_spielbeschreibung(screen, font)

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                run = False

            elif event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
                aktiv, start_time, rundenanzahl, ball = starte_spiel(screen)

            elif event.type == pygame.MOUSEBUTTONUP:
                if ball.check_collision(pygame.mouse.get_pos()):
                    reaktionszeiten.append(get_time() - start_time)
                    ball = Ball.create_random()

                rundenanzahl -= 1

        ball.draw(screen)

        if rundenanzahl == 0:
            aktiv = False
            if frage_nach_neuem_spiel(
                screen, font, sum(reaktionszeiten) / len(reaktionszeiten) - 1
            ):
                aktiv, start_time, rundenanzahl, ball = starte_spiel(screen)

        pygame.display.update()

    pygame.quit()


if __name__ == "__main__":
    main()
Das ist wie das Original noch nicht ganz fehlerfrei weil man die Ereignisschleifen nicht vermischen kann, denn so kann es passieren das Ereignisse die früher passiert sind, nach welchen verarbeitet werden die später passiert sind.

Die Vermischung von letztlich drei “Bildschirmen“ auf diese Weise `aktiv` und einer extra Funktion die auch noch mal eine Ereignisschleife enthält ist fehlerhaft und auch ziemlich unübersichtlich. Das sollte man sauberer trennen.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
je_manth
User
Beiträge: 2
Registriert: Donnerstag 1. Februar 2024, 09:44

und ich war so stolz, das es überhaupt schon lief ;-)

aber danke für den Input und das überarbeitete Script (hätte ich ja nicht mit gerechnet)
werde das jetzt mal durcharbeiten ;-)

(die Kommentare waren tatsächlich eher für mich bestimmt ;-) und die tlws komischen Var-Namen (_PK = PunkteKlick) resultieren daher, das am Ende ein Programm raus kommen soll, das aus mehreren Modulen besteht. Daher auch eine Datei (vars.py) in der ich dann alle Konstanten und Var aus den anderen Modulen sammeln wollte. Hätte ich vielleicht erwähnen sollen)
...................
vielen Dank
...................
Benutzeravatar
__blackjack__
User
Beiträge: 13117
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@je_manth: Die Namen aus einem Modul haben ja nichts mit den Namen aus einem anderen Modul zu tun, also muss man da keine kryptischen Abkürzungen dran hängen. Und Variablen gehören sowieso nicht auf Modulebene, höchstens Konstanten. Das heisst ein Modul das `vars` heisst, ist an sich schon mal falsch. Zumal der Name bereits für eine eingebaute Funktion “vergeben“ ist.

Bei mehr als einem Modul sollte man die in einem Package zusammenfassen, damit nur *ein* Name in Konkurrenz zu den anderen Modulen und Packages steht die sonst noch so installiert sind.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Antworten