Ik ben net klaar met het implementeren van een versie van Conways Game of Leven met Java.

Omdat ik nog maar een student ben, weet ik zeker dat mijn code lang niet perfect is, en ik vroeg me af of je naar mijn code kon kijken. Wat kan ik verbeteren? Zijn er snellere manieren om bepaalde delen van mijn code te implementeren? Is er overtollige code die ik kan wegsnijden? Is er een slimmere manier om Conways Game of Life te implementeren?

EDIT:

In de hoop meer feedback te ontvangen, is hier de theorie achter mijn implementatie:

Ter referentie, hier zijn de regels voor Conways levensspel (overgenomen van wikipedia):

  1. Elke levende cel met minder dan twee levende buren sterft, alsof het gaat om onderbevolking.
  2. Elke levende cel met slepen of drie levende buren leven door naar de volgende generatie.
  3. Elke levende cel met meer dan drie levende buren sterft, alsof door ove Bevolking
  4. Elke dode cel met precies drie levende buren wordt een levende cel, alsof het door reproductie gaat.

Overzicht:

  1. A andere kijk op Conways Game of Life
  2. Onuitgesproken regels
  3. Uitleg van belangrijke methoden (en gebruikte gegevensstructuren)

Een andere kijk op Conways Game of Life

Laten we ons eerst het Game of Life voorstellen als een angstig raster (we zal ook aannemen dat dit raster coördinaten heeft zodat de linkerbenedenhoek wordt aangeduid als (0,0) en de rechterbovenhoek wordt aangeduid als (n, n) waar n een positief geheel getal is). Dit 2-dimensionale raster vertegenwoordigt een groep van n * n aantal cellen. Elk rasterblok kan worden gezien als een cel, die niet alleen een Booleaanse waarde (dood of levend) opslaat om de status van de cel te beschrijven, maar ook de locatie aangeeft via de coördinaten. Bovendien bepaalt de huidige toestand van alle cellen welke cellen zullen afsterven, blijven leven of geboren worden in de volgende generatie in overeenstemming met de bovenstaande regels.

In een ander perspectief is het spel van Conway echter van het leven lijkt erg op het spel mijnenveger. We kunnen een levende cel zien als een mijn, en de buren die het aantal mijnen opslaan dat er het dichtst bij is. Op deze manier kunnen we de bovenstaande regels gemakkelijk gebruiken om de toekomstige generatie te bepalen (met name welke cellen zullen afsterven en welke cellen zullen worden geboren).

Hoe zit het met de cellen die momenteel in leven zijn? vragen? Welnu, we kunnen deze gemakkelijk weergeven als een geheel getal groter dan 10, waarbij de plaats van de ene aangeeft hoeveel levende buren de momenteel levende cel heeft, en de plaatsen van de tien geven aan dat de cel leeft.

Onuitgesproken regels

Een observatie die bij me opkwam, is dat het spel van het leven is alleen bezorgd over levende cellen. Alleen levende cellen kunnen afsterven, cellen die blijven leven, moeten al in leven zijn en cellen kunnen alleen geboren worden als ze buren hebben die in leven zijn. Als gevolg hiervan zou het een complete verspilling zijn om het hele raster (tijdcomplexiteit: O (n ^ 2)) te controleren om de toekomstige generatie cellen te bepalen. Het zou een stuk sneller zijn als ik alle momenteel levende cellen zou opslaan en elke levende cel samen met hun buren zou controleren om de volgende generatie te bepalen (en dat is precies wat ik deed).

Uitleg van belangrijke methoden (en gebruikte gegevensstructuren)

birth (): herhaalt een HashMap met een sleutel / waarde-paar alle levende cellen samen met zijn buren. Als het sleutel / waarde-paar de regels van het leven hierboven volgt, wordt de sleutel (een geheel getal dat de locatie van een cel vertegenwoordigt) op een stapel geduwd die de volgende generatie levende cellen bevat. Na elke iteratie wordt de waarde van het raster gereset naar 0 en wordt het sleutel / waarde-paar verwijderd uit de HashMap.

insertAlive (): plaatst de stapel en voegt de levende cel in het raster in. Het invoegen van een levende cel volgt de structuur van een mijnenveger (buren van een levende cel worden met 1 verhoogd en de levende cel met 10 om aan te geven dat deze in leven is). Alle buren en levende cellen worden dan in een HashMap geplaatst zodat birth () correct kan draaien

printBoard () (zou boardToString moeten heten): gebruikt een stringbuilder om het raster op te maken in een string.

Opmerking : de meeste commentaren zijn verwijderd omdat ze niet veel toevoegen aan de leesbaarheid van de 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"); } } } 

Reacties

  • U ‘ hebt gemaakt een punt om te zeggen dat dit een andere implementatie is, maar er is niets om de theorie achter je implementatie uit te leggen of de relatief obscure algoritmen en formules die je ‘ hergebruikt.
  • Dit is een populaire oefening, dus ik zou je ook aanraden om de implementaties die je online kunt vinden te bekijken: het is ‘ erg leerzaam. Ik vind vooral deze kern die een reactieve implementatie in Java 8 laat zien (met behulp van RxJava) – niet sa het zou echter een goede productie code zijn.

Antwoord

Ten eerste allemaal denk ik dat het algoritme behoorlijk slim is, wat, voor mijn bescheiden ervaring, niet zo gebruikelijk is voor een student. Dus gefeliciteerd als je het zelf hebt bedacht! Als u “op zoek bent naar slimme implementaties, raad ik functionele implementaties aan, bijvoorbeeld in Haskell ; zie ook Kortste spel van het leven .

Pas nu op voor slimheid. Een goede code moet gemakkelijk te lezen zijn , gemakkelijk te begrijpen . Dit is natuurlijk niet altijd mogelijk als het om een complex algoritme gaat, maar ik denk dat het zou moeten zijn een doelwit.

jjjjjjjjjjjjj zei:
Opmerking: de meeste opmerkingen zijn verwijderd omdat ze niet toevoegen veel aan de leesbaarheid van de code

Het punt van commentaar is om mensen te helpen uw code te begrijpen (in het algemeen gesproken, focus op de ” waarom ” in plaats van op de ” wat “). Om mensen te helpen begrijpen, moest je veel tekst aan je bericht toevoegen. Idealiter is dit niet nodig omdat de code:

  • zelfgedocumenteerd is,
  • becommentarieerd om complexe / impliciete dingen op te ruimen.

Hier is bijvoorbeeld een snelle herschrijving van uw code in een poging om de code expressiever te maken:

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(); } }  

Ik heb meestal een aantal variabelen / methoden hernoemd en enkele hulpprogramma-methoden geïntroduceerd. De code is iets langer en voelt meer aan uitgebreid maar is IMHO ook gemakkelijker te begrijpen. Het is nog steeds erg procedureel (wat niet per se slecht is, vooral niet voor zon eenvoudig programma), maar je zou kunnen proberen om meer expressiviteit toe te voegen door nieuwe klassen te introduceren, zoals Board of Cell. U” zult dergelijke OO-implementaties vinden op GitHub .

Uw code kan ook geheugenproblemen tegenkomen bij grote borden. Inderdaad, uw board[][] variabele slaat alle cellen op, zelfs dode. Met een kaart van 10.000 x 10.000 die slechts ~ 5/6 cellen bevat, verspilt u veel geheugen. Een oplossing is om een sparse array te gebruiken (in feite een set met alleen levende cellen).

Als een kanttekening, een paar jaar geleden probeerde ik ook een zeer configureerbare GoL te modelleren in een ” pure ” OO manier; mijn code staat op GitHub als je het wilt bekijken. De methode die de volgende generatie van de world is ImmutableGeneration :: nextGeneration ; gegeven een set van levende cellen, is het in feite: 1) bereken alle burencellen en 2) bewaar alleen degenen die in leven zullen zijn. Regels die aangeven of een cel zal leven of dood zijn, worden geïmplementeerd in Rule.java .


BEWERK : persoonlijk mening over beknoptheid versus breedsprakigheid als het gaat om n ben van plan een opmerking te beantwoorden.

Allereerst denk ik dat er geen goede antwoorden zijn: het gaat allemaal om afwegingen en persoonlijke voorkeuren. Naamgeving is moeilijk en je zult veel artikelen over het onderwerp vinden.

Er zijn slechts twee moeilijke dingen in de informatica: cache-ongeldigverklaring en naamgeving
– Phil Karlton

Mijn mening is dat beknoptheid prettig is, maar kan leiden tot dubbelzinnigheden. En dubbelzinnigheid, vooral de verborgen, is een bedreiging.Het eerste voorbeeld dat bij me opkomt is het per ongeluk mengen van eenheden:

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

Dat Dat gezegd hebbende, maken lange namen de code moeilijker te lezen. Ze kunnen worden verkleind door rekening te houden met twee dingen:

  • de context (bijv. Naam van de insluitende methode / klasse / pakket),
  • het bereik (een lokale variabele in een methode met drie regels kan prima zijn met een korte naam, terwijl een functie die meerdere keren wordt gebruikt in de hele codebase mogelijk een langere naam nodig heeft.

Dat is ook wat wordt geadviseerd door De naamgevingsconventies van Google .

Als laatste opmerking, zoals u suggereerde, kunnen zeer lange namen worden gezien als code-geuren. Meestal is het probleem een gebrek aan samenhang (de klasse / methode doet teveel verschillende dingen – nogmaals, er zijn geen duidelijke statistieken over, het is aan gevoel van ontwikkelaar). In de code die ik heb voorgesteld, kunnen we bijvoorbeeld populateWorldWithAliveCellsFromPreviousGeneration beschouwen als een methode met verantwoordelijkheden: 1) het berekenen van de cellen die bij de volgende generatie zullen leven en 2) het bevolken van de wereld. We zouden het dus in twee kunnen splitsen: populateWorldWith(aliveCellsFromPreviousGeneration()).

Op dezelfde manier konden we de attributen verzamelen waarvan de naam eindigt op ” atNextGeneration ” onder een nieuwe 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)); } ... }  

Maar het splitsen van de logica in te veel klassen zal ook de complexiteit van de architectuur vergroten en het moeilijker maken om de stroom te begrijpen.

Als een conclusie Ik zou je willen adviseren om in gedachten te houden dat elk stukje code kan worden gewijzigd door ontwikkelaars die geen voorkennis hebben van het project en die moeten begrijpen wat de code doet en waarom het doet het zodat ze het kunnen behouden of onderdelen kunnen hergebruiken zonder regressies te introduceren. Er is geen silverbullet: alleen afwegingen, en het gaat erom wanneer je een keuze maakt:

  • je kunt de afweging identificeren,
  • je begrijpt de voor- en nadelen van elk alternatief en kies er bewust een van.

(maar oefen niet te veel druk uit: KISS en onthoud dat code daarna kan worden geherstructureerd)

Opmerkingen

  • Heel erg bedankt voor je opmerking. Ik ben zo blij dat je commentaar hebt gegeven, want ik had er nooit aan gedacht om alleen de levende cellen op te slaan (dit verandert veel van mijn code en maakt het naar mijn mening een stuk beter). Ik wilde iets vragen over uw mening over de balans tussen duidelijk zijn met variabelenamen en beknopt zijn. Met andere woorden, hoe kun je bepalen wanneer het programma te uitgebreid is? Betekent dit dat u buitengewoon lang moet besteden aan het maken van de juiste variabelenamen of dat er iets mis is met uw logica en ontwerp van de code? Nogmaals bedankt.
  • Ik heb mijn antwoord bewerkt om mijn mening erover te delen. Het ‘ is een hoop tekst die in feite zegt dat ” er geen goede antwoorden zijn, het is ‘ het draait allemaal om afwegingen, dus denk na over voor- en nadelen bij het maken van een keuze “.

Antwoord

Ik heb slechts een kleine aanbeveling met betrekking tot leesbaarheid. Als je een methode hebt die printBoard heet, zou je normaal gesproken verwachten dat deze het bord afdrukt. Een betere naam voor die methode is boardToString.

Geef een reactie

Het e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *