Minesweeper Problem

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
Kurmuro
User
Beiträge: 4
Registriert: Sonntag 15. September 2019, 11:50

Hi
Ich versuche momentan Hobbymäßig Python zu lernen.
Dafür habe ich mir eine Youtube Tutorial Reihe vorgenommen welche ich einmal komplett durcharbeite und auch selbst modifizieren will.
In dem einen Teil geht es darum ein Minesweeper Programm zu schreiben (ist unten verlinkt)
Ich bin jetzt bis zu Minute 54 gekommen. Ab dem Zeitpunkt sollte die Grundsätzliche Spiel Visualisierung vorhanden sein, heißt die Bomben werden in dem Feld verteilt und jedes Kästchen wird abgefragt wie viel bomben um es rum hat und passt dem entspechend seine zahl an.
Bis zu den Bomben klappt alles einwandfrei nur Stimmen meine Zahlen nicht. Ich hab den Code jetzt schon mehrmals mit dem im Video verglichen (dort stimmt es), finde allerdings den unterschied/fehler nicht...

Schonmal Danke im voraus :D

Hier sieht man das die Zahlen nicht stimmen
Bild
Bild
Bild

Das Video um welches es sich handelt:
https://www.youtube.com/watch?v=diuLxpU2pWg&t=3241s

Mein Code auf github:
https://github.com/Kurmuro/lernen

Oder direkt hier:

Code: Alles auswählen

import pygame as pg
from dataclasses import dataclass
import random as rnd

auflösung = 500
raster = 9
anzMinen = 10
abstand = auflösung // raster


pg.init()
pg.display.set_caption('Minesweeper')
screen = pg.display.set_mode([auflösung, auflösung])


cell_normal = pg.transform.scale(pg.image.load(
    'Teil_10_ms_cell_normal.gif'), (abstand, abstand))
cell_marked = pg.transform.scale(pg.image.load(
    'Teil_10_ms_cell_marked.gif'), (abstand, abstand))
cell_mine = pg.transform.scale(pg.image.load(
    'Teil_10_ms_cell_mine.gif'), (abstand, abstand))
cell_selected = []
for n in range(9):
    cell_selected.append(pg.transform.scale(pg.image.load(
        f'Teil_10_ms_cell_{n}.gif'), (abstand, abstand)))


matrix = []
benachbarteFelder = [(-1, -1), (-1, 0), (-1, 1), (0, -1),
                     (0, 1), (1, -1), (1, 0), (1, 1)]


@dataclass
class Cell():
    spalte: int
    zeile: int
    mine: bool = False
    selected: bool = False
    flagged: bool = False
    anzMinenDrumRum: int = 0

    def show(self):
        pos = (self.spalte*abstand, self.zeile*abstand)
        if self.selected:
            if self.mine:
                screen.blit(cell_mine, pos)
            else:
                screen.blit(cell_selected[self.anzMinenDrumRum], pos)
        else:
            if self.flagged:
                screen.blit(cell_marked, pos)
            else:
                screen.blit(cell_normal, pos)

    def anzMinenErmitteln(self):
        for pos in benachbarteFelder:
            neueZeile = self.zeile + pos[0]
            neueSpalte = self.spalte + pos[1]
            if neueZeile >= 0 and neueZeile < raster and neueSpalte >= 0 and neueSpalte < raster:
                if matrix[neueZeile*raster+neueSpalte].mine:
                    self.anzMinenDrumRum += 1


for n in range(raster*raster):
    matrix.append(Cell(n // raster, n % raster))

while anzMinen > 0:
    x = rnd.randrange(raster*raster)
    if not matrix[x].mine:
        matrix[x].mine = True
        anzMinen -= 1

for objekt in matrix:
    objekt.anzMinenErmitteln()


weitermachen = True
while weitermachen:
    for event in pg.event.get():
        if event.type == pg.QUIT:
            weitermachen = False
    for objekt in matrix:
        objekt.selected = True
        objekt.show()
    pg.display.flip()
pg.quit()
Benutzeravatar
__blackjack__
User
Beiträge: 13236
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Kurmuro: Iiiih Typannotationen. *Wenn* man die macht, sollte man auch unbedingt regelmässig prüfen ob die auch zum Code passen beziehungsweise ob der Code zu den Typannotationen passt. Was verwendest Du denn dafür? ``mypy`` beschwert sich nämlich das `matrix` eine Typannotation braucht. Zumindest solange das *vor* `Cell` definiert wird.

Ich verwende da lieber das externe `attrs`-Package (`attr`-Modul), da muss man keine Typen angeben.

Verwende keine kryptischen Abkürzungen. Das `random`-Modul hat so einen schönen passenden Namen, es gibt keinen Grund das als `rnd` abzukürzen. Vor allem, da der Name dann auch nur ein einziges mal verwendet wird. Es gibt bestimmte Module die werden Abgekürzt importiert weil man sie im Code sehr oft benötigt, weil die Module sehr viele Namen zur Verfügung stellen die man in einem Modul verwendet. Das sollte aber die Ausnahme und nicht die Regel sein.

Auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst. Was der Code auf Modulebene auf keinen Fall machen sollte ist irgendwelche Fenster öffnen oder eine Hauptschleife laufen lassen. Man muss ein Modul ohne solche Seiteneffekte importieren können. Zum Beispiel zur Fehlersuche, für automatisierte Tests, zum erstellen von Dokumentation, Analysewerkzeugen die den Code importieren, und so weiter. Um dem Fehler auf die Spur zu kommen wäre es zum Beispiel sinnvoll wenn man die Matrix auch manuell erzeugen könnte, statt zufällig und gezielt den Code mit verschiedenen Testmatrizen laufen lassen könnte.

Funktionen und Methoden sollten alles was sie ausser Konstanten benötigen als Argument(e) übergeben bekommen. `Cell.show()` braucht `screen` als Argument und `Cell.anzMinenErmitteln()` braucht `matrix`.

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

Umlaute in Namen sollte man vermeiden. Python selbst kann das zwar, aber nicht alle Werkzeuge die man auf Python-Code loslassen kann, kommen damit zurecht. Die Mischung aus Deutsch und Englisch ist aber insgesamt sehr unschön. Wonach wird denn entschieden welcher Namen Deutsch und welcher Englisch ist?

Bei `cell_selected` verstehe ich nicht wie der Name zustande kommt und man kann am Namen auch nicht erkennen, dass es sich um eine Sequenz handelt, statt eines Einzelwertes.

Beim laden der Bilder wird immer das selbe Codemuster ausgeführt – das schreit nach einer Funktion. Für das laden der Nummernbilder bietet sich eine „list comprehension“ an.

`benachbarteFelder` ist eine Fehlerquelle die man überprüfen sollte, oder besser vermeiden in dem man diese Tupel nicht manuell hin schreibt, sondern durch Code erzeugt. Auch hier bietet sich wieder eine „list comprehension“ an.

Der Code der die Minen platziert ist problematisch. Falls die Anzahl der Minen grösser ist als die Anzahl der Felder, dann ist das eine Endlosschleife. Aber selbst wenn die Anzahl der Minen ins Spielfeld passt, läuft das ganze um so ineffizienter, je weniger Platz im Feld ist. Das ganze geht effizienter und sogar deutlich kürzer mit `random.sample()`.

Wenn in der `matrix` Objekte vom Typ `Cell` stecken, dann ist der Name `objekt` viel zu generisch.

Die Anzahl der angrenzenden Minen würde ich mit `None` initialisieren, damit es deutlich auffällt (`IndexError`) wenn man versucht `show()` zu verwenden *bevor* dieser Wert berechnet wurde.

Der Code in allen Zweigen bei `Cell.show()` macht letztlich das gleiche. Das einzige was sich unterscheidet ist das Bild was im Code verwendet wird. Da sollte man die Gemeinsamkeiten herausziehen, also in den Zweigen nur das Bild ermitteln und dann *einmal* danach den Code schreiben, der das dann ”blittet”.

Am Anfang von `anzMinenErmitteln()` sollte man die Anzahl auf 0 setzen, sonst kann man die Methode nicht mehr als einmal aufrufen ohne das die Daten inkonsistent werden.

Die relativen Positionen in der Schleife würde ich gleich an sinnvolle Namen binden statt da mit nichtssagenden Indexwerten drauf zuzugreifen.

Der Präfix `neue*` macht nicht wirklich Sinn. Es gibt im gleichen Namensraum ja keine `alte*`-Variante von den Namen von der man sich abgrenzen müsste.

Für das Testen der Grenzen von `spalte` und `zeile` braucht man nur ein ``and`` und verkettete Vergleiche wie sie in der Mathematik üblich sind.

Zwischenstand (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
import random

from attr import attrib, attrs
import pygame  # type: ignore

#
# TODO Clean up that german english naming mix in this module.
#
AUFLOESUNG = 500
RASTER = 9
MINEN_ANZAHL = 10
assert MINEN_ANZAHL <= RASTER ** 2, "too many mines"
ABSTAND = AUFLOESUNG // RASTER


def load_cell_image(filename):
    return pygame.transform.scale(
        pygame.image.load(filename), (ABSTAND, ABSTAND)
    )


NORMAL_CELL_IMAGE = load_cell_image("Teil_10_ms_cell_normal.gif")
MARKED_CELL_IMAGE = load_cell_image("Teil_10_ms_cell_marked.gif")
MINE_CELL_IMAGE = load_cell_image("Teil_10_ms_cell_mine.gif")
NUMBER_IMAGES = [load_cell_image(f"Teil_10_ms_cell_{i}.gif") for i in range(9)]
#
# TODO Replace by code.  Chained ``for``\s or `itertools.product()`.
#
BENACHBARTE_FELDER = [
    (-1, -1),
    (-1, 0),
    (-1, 1),
    (0, -1),
    (0, 1),
    (1, -1),
    (1, 0),
    (1, 1),
]
assert len(BENACHBARTE_FELDER) == len(NUMBER_IMAGES) == 8


@attrs
class Cell:
    spalte = attrib()
    zeile = attrib()
    is_mined = attrib(default=False)
    is_selected = attrib(default=False)
    is_flagged = attrib(default=False)
    anzahl_angrenzender_minen = attrib(default=None)

    def blit(self, screen):
        if self.is_selected:
            image = (
                MINE_CELL_IMAGE
                if self.is_mined
                else NUMBER_IMAGES[self.anzahl_angrenzender_minen]
            )
        else:
            image = MARKED_CELL_IMAGE if self.is_flagged else NORMAL_CELL_IMAGE
        screen.blit(image, (self.spalte * ABSTAND, self.zeile * ABSTAND))

    def mimenanzahl_aktualisieren(self, matrix):
        self.anzahl_angrenzender_minen = 0
        for zeilen_delta, spalten_delta in BENACHBARTE_FELDER:
            zeile = self.zeile + zeilen_delta
            spalte = self.spalte + spalten_delta
            if 0 <= zeile < RASTER and 0 <= spalte < RASTER:
                if matrix[zeile * RASTER + spalte].is_mined:
                    self.anzahl_angrenzender_minen += 1
        assert 0 <= self.anzahl_angrenzender_minen <= len(BENACHBARTE_FELDER)


def main():
    pygame.init()
    pygame.display.set_caption("Minesweeper")
    screen = pygame.display.set_mode([AUFLOESUNG, AUFLOESUNG])

    matrix = [Cell(n // RASTER, n % RASTER) for n in range(RASTER ** 2)]

    for index in random.sample(range(RASTER ** 2), MINEN_ANZAHL):
        matrix[index].is_mined = True

    for cell in matrix:
        cell.mimenanzahl_aktualisieren()

    weitermachen = True
    while weitermachen:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                weitermachen = False

        for cell in matrix:
            cell.is_selected = True
            cell.blit(screen)

        pygame.display.flip()

    pygame.quit()


if __name__ == "__main__":
    main()
Du solltest noch mal überprüfen ob `BENACHBARTE_FELDER` tatsächlich korrekt ist.

Und dann Testcode in einem weiteren Modul schreiben schreiben der das Modul mit dem Spiel importiert und nichtzufällige Matrizen prüft. Zum Beispiel ob bei einer komplett leeren Matrix das erwartete Ergebnis kommt. Und bei einer wo nur ein einziges Feld vermint ist. Wenn da nicht das gewünschte Ergebnis kommt, sieht man am tatsächlichen Ergebnis vielleicht ein Muster das einen auf das Problem im Code stösst.

Dazu müsstest Du mehr Code aus der Hauptfunktion in eigene Funktionen heraus ziehen. Zum Beispiel wirst Du für den Testcode das erstellen einer leere Matrix benötigen, und das aktualisieren der Anzahlen in den Zellen, nachdem Minen platziert wurden.

Um das Testen einfacher zu machen, würde es sich auch anbieten eine Funktion zu schreiben die aus einer Zeichenkette die das Minenfeld beschreibt eine Matrix mit Zellen zu erstellen. Und den umgekehrten Weg, eine Matrix bei der die Minen um die Zellen gezählt wurden, wieder als Zeichenkette umwandeln für einen einfachen Vergleich mit dem erwarteten Ergebnis.

Code: Alles auswählen

from minefield importiert minefield_from_bomb_map, minefield_to_string

def test_mine_counts(bomb_map, expected):
    minefield = minefield_from_bomb_map(bomb_map)
    assert minefield_to_string(minefield) == expected


if __name__ == "__main__":
    test_mine_counts(
        ".........\n"
        ".........\n"
        ".........\n"
        ".........\n"
        ".........\n"
        ".........\n"
        ".........\n"
        ".........\n"
        ".........\n",
        ".........\n"
        ".........\n"
        ".........\n"
        ".........\n"
        ".........\n"
        ".........\n"
        ".........\n"
        ".........\n"
        ".........\n"
    )
    test_mine_counts(
        "#........\n"
        ".........\n"
        ".........\n"
        ".........\n"
        ".........\n"
        ".........\n"
        ".........\n"
        ".........\n"
        ".........\n",
        "#1.......\n"
        "11.......\n"
        ".........\n"
        ".........\n"
        ".........\n"
        ".........\n"
        ".........\n"
        ".........\n"
        ".........\n"
    )
    test_mine_counts(
        ".........\n"
        ".#.......\n"
        ".........\n"
        ".........\n"
        ".........\n"
        ".........\n"
        ".........\n"
        ".........\n"
        ".........\n",
        "111......\n"
        "1#1......\n"
        "111......\n"
        ".........\n"
        ".........\n"
        ".........\n"
        ".........\n"
        ".........\n"
        ".........\n"
    )
    # ...
Die Matrix/das Minenfeld selbst auch als Klasse zu modellieren würde sich hier anbieten. Dann kann man `__str__()` implementieren und eine Klassenmethode um aus einer Zeichenkette ein Minenfeld zu erstellen.

Für den Testcode könnte man sich dann auch mal `pytest` anschauen.
Please call it what it is: copyright infringement, not piracy. Piracy takes place in international waters, and involves one or more of theft, murder, rape and kidnapping. Making an unauthorized copy of a piece of software is not piracy, it is an infringement of a government-granted monopoly.
Benutzeravatar
ThomasL
User
Beiträge: 1367
Registriert: Montag 14. Mai 2018, 14:44
Wohnort: Kreis Unna NRW

Ich mache es mal etwas einfacher; Du hast eine Zeile vergessen.

Code: Alles auswählen

for objekt in matrix:
    if not objekt.mine: # DIESE ZEILE FEHLT
        objekt.anzMinenErmitteln()
Ich bin Pazifist und greife niemanden an, auch nicht mit Worten.
Für alle meine Code Beispiele gilt: "There is always a better way."
https://projecteuler.net/profile/Brotherluii.png
Kurmuro
User
Beiträge: 4
Registriert: Sonntag 15. September 2019, 11:50

Wow danke für die ausführliche antwort 😲
Wie gesagt bin ich noch blutiger Anfänger der das ganze noch lernen will, weshalb ich mir stellen aus deiner antwort erstmal übersetzen muss 😅
Aber werde versuchen das alles umzusetzen 👍😁
Benutzeravatar
__blackjack__
User
Beiträge: 13236
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@ThomasL: Ich verstehe nicht warum das eine Lösung sein soll‽ Klar, man kann sich die Berechnung *sparen* wenn eine Mine auf dem Feld liegt, aber stören sollte es doch auch nicht, vor allem sollte es keinen Einfluss auf die Berechnung der Felder ohne Minen haben‽
Please call it what it is: copyright infringement, not piracy. Piracy takes place in international waters, and involves one or more of theft, murder, rape and kidnapping. Making an unauthorized copy of a piece of software is not piracy, it is an infringement of a government-granted monopoly.
Kurmuro
User
Beiträge: 4
Registriert: Sonntag 15. September 2019, 11:50

__blackjack__ hat geschrieben: Sonntag 15. September 2019, 20:03 @ThomasL: Ich verstehe nicht warum das eine Lösung sein soll‽ Klar, man kann sich die Berechnung *sparen* wenn eine Mine auf dem Feld liegt, aber stören sollte es doch auch nicht, vor allem sollte es keinen Einfluss auf die Berechnung der Felder ohne Minen haben‽
Das stimmt.
Ändert leider an dem Fehler absolut garnichts.
Werde es wohl so rausfinden müssen wie du es beschrieben hast. Bisher ist ja alles nur ein nachschreiben und versuchen zu verstehen wie der ablauf funktioniert, eventuell selbst etwas rumprobieren. Ich weiß ja noch nichtmal was ein Modul sein soll oder was ein externes Package etc. ist 😅
Bis ich mich in die Materie reingelesen hab dauert es wohl noch ne weile 😂
Kurmuro
User
Beiträge: 4
Registriert: Sonntag 15. September 2019, 11:50

Hab es jetzt mal so getestet wie vorgeschlagen wurde und es hat mich wirklich zum Ziel geführt :D
Also nur eine Bombe gezielt platziert, dabei ist mir aufgefallen das die Zahlen immer Achsen verschoben waren (wär doch ein cooler schwerer spielmodus xD)

Wenn es interessiert:

Code: Alles auswählen

def anzMinenErmitteln(self):
        for pos in benachbarteFelder:
            neueZeile = self.zeile + pos[0]
            neueSpalte = self.spalte + pos[1]
            if neueZeile >= 0 and neueZeile < raster and neueSpalte >= 0 and neueSpalte < raster:
                if matrix[neueZeile*raster+neueSpalte].mine: #hier sind neueZeile und neueSpalte vertauscht
                    self.anzMinenDrumRum += 1
Benutzeravatar
ThomasL
User
Beiträge: 1367
Registriert: Montag 14. Mai 2018, 14:44
Wohnort: Kreis Unna NRW

__blackjack__ hat geschrieben: Sonntag 15. September 2019, 20:03 @ThomasL: Ich verstehe nicht warum das eine Lösung sein soll‽ Klar, man kann sich die Berechnung *sparen* wenn eine Mine auf dem Feld liegt, aber stören sollte es doch auch nicht, vor allem sollte es keinen Einfluss auf die Berechnung der Felder ohne Minen haben‽
Ich habe seinen Code mit dem Github Code des Youtube Videos verglichen und diese Zeile fehlte.
Ungetestet da ich keine Zeit hatte die Bilder zu downloaden.
Ich bin Pazifist und greife niemanden an, auch nicht mit Worten.
Für alle meine Code Beispiele gilt: "There is always a better way."
https://projecteuler.net/profile/Brotherluii.png
Antworten