Ich habe gerade die Implementierung einer Version von Conways Spiel von abgeschlossen Leben mit Java.

Da ich nur ein Student bin, bin ich mir sicher, dass mein Code nicht annähernd perfekt ist, und habe mich gefragt, ob Sie sich meinen Code ansehen können. Was kann ich verbessern? Gibt es schnellere Möglichkeiten? Gibt es überschüssigen Code, den ich entfernen kann? Gibt es eine intelligentere Möglichkeit, Conways Spiel des Lebens zu implementieren?

BEARBEITEN:

In der Hoffnung, mehr Feedback zu erhalten, ist hier die Theorie hinter meiner Implementierung:

Als Referenz hier die Regeln für Conways Lebensspiel (aus Wikipedia):

  1. Jede lebende Zelle mit weniger als zwei lebenden Nachbarn stirbt wie durch Unterbevölkerung.
  2. Jede lebende Zelle Mit zwei oder drei lebenden Nachbarn lebt die nächste Generation weiter.
  3. Jede lebende Zelle mit mehr als drei lebenden Nachbarn stirbt wie von oben rpopulation
  4. Jede tote Zelle mit genau drei lebenden Nachbarn wird wie durch Reproduktion zu einer lebenden Zelle.

Übersicht:

  1. A. Unterschiedliche Ansichten zu Conways Spiel des Lebens
  2. Unausgesprochene Regeln
  3. Erläuterung wichtiger Methoden (und verwendeter Datenstrukturen)

Eine andere Sichtweise auf Conways Spiel des Lebens

Stellen wir uns das Spiel des Lebens zunächst als ängstliches Gitter vor (wir wird auch annehmen, dass dieses Gitter Koordinaten hat, so dass die untere linke Ecke als (0,0) und die obere rechte Ecke als (n, n) bezeichnet wird, wobei n eine positive ganze Zahl ist). Dieses zweidimensionale Gitter repräsentiert eine Gruppe von n * n Zellen. Jeder Gitterblock kann als Zelle betrachtet werden, die nicht nur einen Booleschen Wert (tot oder lebendig) speichert, um den Status der Zelle zu beschreiben, sondern auch ihre Position über ihre Koordinaten angibt. Darüber hinaus bestimmt der aktuelle Zustand aller Zellen, welche Zellen gemäß den oben genannten Regeln sterben, weiterleben oder in der nächsten Generation geboren werden.

In einer anderen Perspektive jedoch Conways Spiel des Lebens ist dem Spiel Minensuchboot sehr ähnlich. Wir können uns eine lebendige Zelle als eine Mine vorstellen, und ihre Nachbarn speichern die Anzahl der Minen, die ihr am nächsten liegen. Auf diese Weise können wir die obigen Regeln leicht verwenden, um die zukünftige Generation zu bestimmen (insbesondere welche Zellen sterben und welche Zellen geboren werden).

Was ist mit den Zellen, die derzeit möglicherweise leben? Fragen? Nun, wir können diese leicht als eine ganze Zahl größer als 10 darstellen, wobei der Ort des einen angibt, wie viele lebende Nachbarn die aktuell lebende Zelle hat, und der Platz der Zehn angibt, dass die Zelle lebt.

Unausgesprochene Regeln

Eine Beobachtung, die mir einfiel, war, dass das Spiel des Lebens ist nur über lebende Zellen besorgt. Nur lebende Zellen können sterben, Zellen, die weiterleben, müssen bereits leben, und Zellen können nur geboren werden, wenn sie lebende Nachbarn haben. Infolgedessen wäre die Überprüfung des gesamten Gitters (Zeitkomplexität: O (n ^ 2)) zur Bestimmung der zukünftigen Generation von Zellen eine völlige Verschwendung. Es wäre viel schneller, wenn ich alle derzeit lebenden Zellen speichern und jede lebende Zelle zusammen mit ihren Nachbarn überprüfen würde, um die nächste Generation zu bestimmen (genau das habe ich getan).

Erläuterung wichtiger Methoden (und verwendeter Datenstrukturen)

geburt (): iteriert über eine HashMap, die ein Schlüssel-Wert-Paar von enthält alle lebenden Zellen zusammen mit ihren Nachbarn. Wenn das Schlüssel-Wert-Paar den oben genannten Spielregeln folgt, wird der Schlüssel (ein ganzzahliger Wert, der den Standort einer Zelle darstellt) auf einen Stapel verschoben, der die nächste Generation lebendiger Zellen enthält. Nach jeder Iteration wird der Wert des Rasters auf 0 zurückgesetzt und das Schlüssel-Wert-Paar aus der HashMap entfernt.

insertAlive (): Fügt den Stapel ein und fügt die lebende Zelle in das Raster ein. Das Einfügen einer lebenden Zelle folgt der Struktur des Minensuchboots (Nachbarn einer lebenden Zelle werden um 1 erhöht, und die lebende Zelle wird um 10 erhöht, um anzuzeigen, dass sie lebt). Alle Nachbarn und lebenden Zellen werden dann in eine HashMap eingefügt, damit child () ordnungsgemäß ausgeführt werden kann.

printBoard () (sollte boardToString heißen): Verwendet einen Stringbuilder, um das Raster in einen String zu formatieren.

Hinweis : Die meisten Kommentare wurden entfernt, da sie die Lesbarkeit von nicht wesentlich verbessern der Code

CellularAutomaton.java

package first; public abstract class CellularAutomaton{ public abstract String lifeCycle(); public abstract boolean rules(int num); } 

GameOfLife.java

 package first; import java.util.Stack; import java.util.HashMap; import java.util.Iterator; import java.util.Map; public class GameOfLife extends CellularAutomaton { int board[][]; int dim; Stack<Integer> stackCells; HashMap<Integer, Integer> hmapCells; public gameOfLife(int d, Stack<Integer> s){ board = new int[d][d]; dim = d; stackCells = s; hmapCells = new HashMap<>(); } public boolean rules(int num){ return num == 3 || num == 12 || num == 13; } private void birth() { Iterator<Map.Entry<Integer,Integer>> it=hmapCells.entrySet().iterator(); while(it.hasNext()) { Map.Entry<Integer,Integer> pair = it.next(); int key = pair.getKey(); if(rules(pair.getValue())){ stackCells.add(key); } board[key/dim][key%dim] = 0; it.remove(); } } private void insertAlive() { while(!stackCells.isEmpty()) { int cell = stackCells.pop(); int x = cell / dim; int y = cell % dim; int startX = (x <= 0) ? 0 : x - 1; int startY = (y <= 0) ? 0 : y - 1; int endX = (x >= dim - 1) ? x + 1 : x + 2; int endY = (y >= dim - 1) ? y + 1 : y + 2; for(int i = startX; i < endX; ++i) { for(int j = startY; j < endY; ++j) { hmapCells.put(i * dim + j, ++board[i][j]); } } hmapCells.put(cell, board[x][y] += 9); } } private String printBoard() { StringBuilder s = new StringBuilder(); for(int elements[] : board) { for(int element : elements) { if(element >= 10){ s.append("* "); } else { s.append(" "); } } s.append("\n"); } return s.toString(); } public String lifeCycle() { birth(); insertAlive(); return printBoard(); } }  

Simulation.java

package first; import java.util.Stack; public class Simulation { public static void main(String args[]) throws InterruptedException{ int dim = 70; Stack<Integer> init = new Stack<>(); //all vals pushed to init is of the form: xPos * dim + yPos init.push(351); init.push(352); init.push(421); init.push(422); init.push(245); init.push(246); init.push(315); init.push(316); init.push(361); init.push(431); init.push(501); init.push(292); init.push(572); init.push(223); init.push(643); init.push(224); init.push(644); init.push(435); init.push(296); init.push(576); init.push(367); init.push(437); init.push(507); init.push(438); init.push(231); init.push(301); init.push(371); init.push(232); init.push(302); init.push(372); init.push(163); init.push(443); init.push(165); init.push(445); init.push(95); init.push(515); GameOfLife gOL = new GameOfLife(dim, init); while(true) { System.out.print(gOL.lifeCycle()); Thread.sleep(100); System.out.print("\033[H\033[2J"); } } } 

Kommentare

  • Sie haben ‚ gemacht Ein Punkt zu sagen, dass dies eine andere Implementierung ist, aber es gibt nichts, was die Theorie hinter Ihrer Implementierung oder die relativ undurchsichtigen Algorithmen und Formeln, die Sie ‚ verwenden, erklärt.
  • Dies ist eine beliebte Übung, daher würde ich Ihnen auch raten, sich die Implementierungen anzusehen, die Sie online finden können: ‚ ist sehr lehrreich. Ich mag besonders dieser Kern , der eine reaktive Implementierung in Java 8 (mit RxJava) zeigt – nicht sa Es wäre allerdings ein guter Produktionscode .

Antwort

Erste von Alles in allem denke ich, dass der Algorithmus ziemlich klug ist, was meiner bescheidenen Erfahrung nach für einen College-Studenten nicht so üblich ist. Also herzlichen Glückwunsch, wenn Sie es sich selbst ausgedacht haben! Wenn Sie nach intelligenten Implementierungen suchen, würde ich funktionale empfehlen, z. B. in Haskell ; siehe auch Kürzestes Spiel des Lebens .

Hüten Sie sich jetzt vor Schlauheit. Ein guter Code sollte leicht zu lesen sein , leicht zu verstehen . Dies ist natürlich nicht immer möglich, wenn es um komplexe Algorithmen geht, aber ich glaube, dass es so sein sollte ein Ziel.

jjjjjjjjjjjj sagte:
Hinweis: Die meisten Kommentare wurden entfernt, weil sie nicht hinzugefügt werden Sehr zur Lesbarkeit des Codes

Der Sinn von Kommentaren besteht darin, den Menschen das Verständnis Ihres Codes zu erleichtern (im Allgemeinen sollten Sie sich auf warum “ anstatt auf der “ was „). Um den Leuten das Verständnis zu erleichtern, mussten Sie Ihrem Beitrag viel Text hinzufügen. Im Idealfall wird dies nicht benötigt, da der Code wie folgt lautet:

  • selbstdokumentiert,
  • kommentiert, um komplexe / implizite Dinge zu löschen.

Im Folgenden finden Sie eine kurze Umschreibung Ihres Codes, um den Code aussagekräftiger zu gestalten:

GameOfLife.java

 /** * Computes the next state of the automaton by using Conway"s original rules. */ public class GameOfLife extends CellularAutomaton { /** * Stores all cells in a two-dimensional matrix. The value stored is * the number of live neighbors of the cell, +10 if the cell is alive. */ private int board[][]; private int dim; /* * index(cell) = cellX * dim + cellY */ private Stack<Integer> indexesOfCellsAliveAtNextGeneration; private HashMap<Integer, Integer> cellsMaybeAliveAtNextGeneration; public GameOfLife(int d, Stack<Integer> s){ board = new int[d][d]; dim = d; indexesOfCellsAliveAtNextGeneration = s; cellsMaybeAliveAtNextGeneration = new HashMap<>(); } public String newGeneration() { populateWorldWithAliveCellsFromPreviousGeneration(); computeCellsMaybeAliveAtNextGeneration(); return boardAsString(); } private void populateWorldWithAliveCellsFromPreviousGeneration() { for (Map.Entry<Integer, Integer> cell : cellsMaybeAliveAtNextGeneration.entrySet()) { int cellIndex = cell.getKey(); int cellValue = cell.getValue(); if(willBeAlive(cellValue)){ indexesOfCellsAliveAtNextGeneration.add(cellIndex); } board[cellIndex/dim][cellIndex%dim] = 0; } } private static boolean willBeAlive(int cell){ return (!isAlive(cell) && nbOfNeighbors(cell) == 3) || (isAlive(cell) && (nbOfNeighbors(cell) == 2 || nbOfNeighbors(cell) == 3)); } private static boolean isAlive(int cell) { return cell >= 10; } private static int nbOfNeighbors(int cell) { return cell % 10; } private void computeCellsMaybeAliveAtNextGeneration() { cellsMaybeAliveAtNextGeneration.clear(); while(!indexesOfCellsAliveAtNextGeneration.isEmpty()) { int cellIndex = indexesOfCellsAliveAtNextGeneration.pop(); int cellX = cellIndex / dim; int cellY = cellIndex % dim; int topLeftNeighbourX = (cellX <= 0) ? 0 : cellX - 1; int topLeftNeighbourY = (cellY <= 0) ? 0 : cellY - 1; int bottomRightNeighbourX = (cellX >= dim - 1) ? cellX + 1 : cellX + 2; int bottomRightNeighbourY = (cellY >= dim - 1) ? cellY + 1 : cellY + 2; // Iterate through every cell"s neighbor to increate their neighbor number for(int i = topLeftNeighbourX; i < bottomRightNeighbourX; ++i) { for(int j = topLeftNeighbourY; j < bottomRightNeighbourY; ++j) { boolean isNeighbor = i != cellX || j != cellY; if (isNeighbor) { int neighborIndex = i * dim + j; cellsMaybeAliveAtNextGeneration.put(neighborIndex, incrementedNumberOfNeighbors(i, j)); } } } cellsMaybeAliveAtNextGeneration.put(cellIndex, makeAlive(cellX, cellY)); } } private int incrementedNumberOfNeighbors(int x, int y) { return ++board[x][y]; } private int makeAlive(int x, int y) { return board[x][y] += 10; } private String boardAsString() { StringBuilder s = new StringBuilder(); for(int[] cells : board) { for(int cell : cells) { if(isAlive(cell)){ s.append("* "); } else { s.append(" "); } } s.append("\n"); } return s.toString().trim(); } }  

Ich habe meistens einige Variablen / Methoden umbenannt und einige Dienstprogrammmethoden eingeführt. Der Code ist etwas länger und fühlt sich mehr an ausführlich, aber meiner Meinung nach auch leichter zu verstehen. Es ist immer noch sehr prozedural (was an sich nicht schlecht ist, insbesondere für ein so einfaches Programm), aber Sie möchten vielleicht versuchen, mehr Ausdruckskraft zu verleihen, indem Sie neue Klassen wie oder Cell. Sie finden solche OO-Implementierungen auf GitHub . P. >

Ihr Code kann auch bei großen Karten auf Speicherprobleme stoßen. In der Tat speichert Ihre Variable board[][] alle Zellen, auch tote. Mit einer 10000 x 10000-Karte mit nur ~ 5/6 Zellen verschwenden Sie viel Speicher. Eine Lösung besteht darin, ein spärliches Array zu verwenden (im Grunde genommen a Set, das nur lebende Zellen enthält).

Als Randnotiz habe ich vor einigen Jahren auch versucht, eine hoch konfigurierbare GoL in einer “ pure “ OO way; mein Code befindet sich auf GitHub , wenn Sie es überprüfen möchten. Die Methode zur Berechnung der nächsten Generation der Welt ist ImmutableGeneration :: nextGeneration ; bei einer Reihe von lebenden Zellen gilt Folgendes: 1) Berechnen Sie alle Nachbarzellen, dann 2) behalten Sie nur diejenigen, die am Leben bleiben. Regeln, die angeben, ob eine Zelle lebt oder tot ist, sind in Rule.java implementiert.


EDIT : personal Meinung zu Prägnanz versus Ausführlichkeit, wenn es um n geht Ich möchte einen Kommentar beantworten

Zunächst einmal glaube ich, dass es keine richtigen Antworten gibt: Es geht um Kompromisse und persönliche Vorlieben. Das Benennen ist schwierig und Sie werden viele Artikel zu diesem Thema finden.

In der Informatik gibt es nur zwei schwierige Dinge: Ungültigmachen des Caches und Benennen von Dingen
– Phil Karlton

Ich gehe davon aus, dass Prägnanz angenehm ist, aber zu Mehrdeutigkeiten führen kann. Und Mehrdeutigkeiten, insbesondere verborgene, sind eine Bedrohung.Das erste Beispiel, das mir in den Sinn kommt, ist das irrtümliche Mischen von Einheiten:

 // Everything looks good... double pathLength = distanceFromGoal + distanceToTarget; // ... but adding units makes easy to spot bugs double pathLengthInKilometers = distanceFromGoalInMeters + distanceToTargetInMillimeters;  

Das Allerdings erschweren lange Namen das Lesen des Codes. Sie können reduziert werden, indem zwei Dinge berücksichtigt werden:

  • der Kontext (z. B. Name der einschließenden Methode / Klasse / Paket),
  • der Bereich (eine lokale Variable in Eine dreizeilige Methode kann mit einem kurzen Namen in Ordnung sein, während eine Funktion, die mehrmals in der gesamten Codebasis verwendet wird, möglicherweise eine längere benötigt.

Dies wird auch von Namenskonventionen von Google .

Als letzte Anmerkung, wie Sie vorgeschlagen haben, können sehr lange Namen als Codegeruch angesehen werden. Normalerweise ist das Problem ein Mangel an Kohäsion (die Klasse / Methode macht zu viele verschiedene Dinge – auch hier gibt es keine klaren Metriken, es liegt an Entwicklergefühl). In dem von mir vorgeschlagenen Code können wir uns beispielsweise populateWorldWithAliveCellsFromPreviousGeneration als eine Methode vorstellen, die Verantwortlichkeiten trägt: 1) Berechnung der Zellen, die bei der nächsten Generation am Leben sein werden, und 2) Besiedlung der Welt. Wir könnten es also in zwei Teile teilen: populateWorldWith(aliveCellsFromPreviousGeneration()).

Auf die gleiche Weise könnten wir die Attribute sammeln, deren Name mit “ atNextGeneration “ unter einer neuen Generation -Klasse:

 public class GameOfLife extends CellularAutomaton { private Generation lastGeneration; public String newGeneration() { this.lastGeneration = lastGeneration.nextGeneration(); return this.lastGeneration.toString(); } } public class Generation { public Generation nextGeneration() { return new Generation(aliveAtNextGeneration(this.aliveCells)); } ... }  

Die Aufteilung der Logik in zu viele Klassen erhöht jedoch auch die Komplexität der Architektur und erschwert das Verständnis des Ablaufs.

Als Schlussfolgerung Ich würde Ihnen raten, sich vor Augen zu halten, dass jeder Code von Entwicklern geändert werden kann, die keine Vorkenntnisse über das Projekt haben und die verstehen müssen, was der Code tut und warum tut dies, damit sie es warten oder Teile wiederverwenden können, ohne Regressionen einzuführen. Es gibt kein Silverbullet: nur Kompromisse, und was zählt, wenn Sie eine Wahl treffen, ist Folgendes:

  • Sie können den Kompromiss identifizieren,
  • Sie verstehen die Vor- und Nachteile von jede Alternative und wählen Sie eine davon wissentlich aus.

(aber üben Sie nicht zu viel Druck auf Sie aus: KISS und Denken Sie daran, dass der Code danach überarbeitet werden kann.)

Kommentare

  • Vielen Dank für Ihren Kommentar. Ich bin so froh, dass Sie einen Kommentar abgegeben haben, weil ich nie daran gedacht hätte, nur die lebenden Zellen zu speichern (dies ändert einen Großteil meines Codes und macht ihn meiner Meinung nach viel besser). Ich wollte ein wenig nach Ihrer Meinung zum Gleichgewicht zwischen Klarheit mit Variablennamen und Prägnanz fragen. Mit anderen Worten, wie können Sie feststellen, wann das Programm zu ausführlich ist? Bedeutet das, dass Sie außerordentlich viel Zeit damit verbringen müssen, die richtigen Variablennamen zu erstellen, oder dass Ihre Logik und das Design des Codes fehlerhaft sind? Nochmals vielen Dank.
  • Ich habe meine Antwort bearbeitet, um meine Meinung dazu zu teilen. Es ‚ ist eine Menge Text, der im Grunde sagt, dass “ es keine richtigen Antworten gibt, es ‚ dreht sich alles um Kompromisse. Denken Sie also bei Ihrer Auswahl an Vor- und Nachteile „.

Antwort

Ich habe nur eine kleine Empfehlung zur Lesbarkeit. Wenn Sie eine Methode namens printBoard haben, erwarten Sie normalerweise, dass die Karte ausgedruckt wird. Ein besserer Name für diese Methode wäre boardToString.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.