Jeg er lige færdig med at implementere en version af Conways Game of Livet ved hjælp af Java.

At være kun en universitetsstuderende er jeg sikker på, at min kode ikke er næsten perfekt og spekulerede på, om du kunne se på min kode. Hvad kan jeg forbedre på? Er der hurtigere måder at implementere bestemte områder af min kode? Er der overskydende kode, som jeg kan trimme væk? Er der en smartere måde at implementere Conways “Game of Life” på?

REDIGER:

I håb om at modtage mere feedback er her teorien bag min implementering:

Til reference er her regler for Conways livsspil (taget fra wikipedia):

  1. Enhver levende celle med færre end to levende naboer dør, som ved underbefolkning.
  2. Enhver levende celle med slæb eller tre levende naboer lever videre til næste generation.
  3. Enhver levende celle med mere end tre levende naboer dør, som om ved rpopulation
  4. Enhver død celle med nøjagtigt tre levende naboer bliver en levende celle, som ved reproduktion.

Oversigt:

  1. A forskellige udsigter til Conways livsspil
  2. Uudtalte regler
  3. Forklaring på vigtige metoder (og anvendte datastrukturer)

Et andet syn på Conways Game of Life

Lad os først forestille os Game of Life som anxn grid (vi vil også antage, at dette gitter har koordinater, således at det nederste venstre hjørne betegnes som (0,0) og det øverste højre hjørne er betegnet som (n, n) hvor n er et positivt heltal). Dette 2-dimensionelle gitter repræsenterer en gruppe på n * n antal celler. Hver gitterblok kan betragtes som en celle, der ikke kun gemmer en boolsk værdi (død eller levende) for at beskrive cellens status, men også detaljer om dens placering via dens koordinater. Derudover bestemmer den aktuelle tilstand for alle celler, hvilke celler der skal dø, fortsætte med at leve eller blive født i den næste generation i overensstemmelse med reglerne ovenfor.

I et andet perspektiv er Conways spil dog af livet er meget lig spillet minesveger. Vi kan tænke på en levende celle som en mine og dens naboer, der lagrer antallet af miner, der er tættest på den. På denne måde er vi i stand til nemt at bruge ovenstående regler til at bestemme den fremtidige generation (især hvilke celler der skal dø, og hvilke celler der bliver født).

Hvad med de celler, der i øjeblikket er i live, måske Spørg? Nå, vi kan let repræsentere disse som et heltal større end 10, hvor ens sted angiver, hvor mange levende naboer den aktuelt levende celle har, og de ti steder indikerer, at cellen lever.

Uudtalte regler

En bemærkning, der opdagede mig, er at spillet af livet er kun bekymret for levende celler. Kun celler, der er i live, kan dø, celler, der fortsætter med at leve, skal allerede leve, og celler kan kun fødes, hvis de har naboer, der lever. Som et resultat ville det være et fuldstændigt spild at kontrollere hele gitteret (tidskompleksitet: O (n ^ 2)) for at bestemme den fremtidige generation af celler. Det ville være meget hurtigere, hvis jeg lagrede alle de i øjeblikket levende celler og tjekkede hver levende celle sammen med deres naboer for at bestemme den næste generation (hvilket er præcis hvad jeg gjorde).

Forklaring på vigtige metoder (og anvendte datastrukturer)

fødsel (): itereres over et HashMap indeholdende et nøgleværdipar af alle levende celler sammen med sine naboer. Hvis nøgleværdiparet følger livets regler ovenfor, skubbes nøglen (en heltalværdi, der repræsenterer placeringen af en celle) på en stak, der indeholder den næste generation af levende celler. Efter hver iteration nulstilles gitterets værdi til 0, og nøgleværdipar fjernes fra HashMap.

insertAlive (): springer stakken og indsætter den levende celle i gitteret. Indsættelse af en levende celle følger minesvegerens struktur (naboer til en levende celle forøges med 1, og den levende celle øges med 10 for at angive, at den lever). Alle naboerne og de levende celler placeres derefter i et HashMap, så fødslen () kan køre ordentligt

printBoard () (skal have navnet boardToString): bruger en strengbygger til at formatere gitteret til en streng.

Bemærk : de fleste kommentarer er taget ud, fordi de ikke tilføjer meget til læsbarheden af koden

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

Kommentarer

  • Du ‘ har lavet et punkt med at sige, at dette er en anden implementering, men der er intet, der forklarer teorien bag din implementering eller de relativt obskure algoritmer og formler, du ‘ bruger.
  • Dette er en populær øvelse, så jeg vil også råde dig til at se på de implementeringer, du kan finde online: det ‘ er meget lærerigt. Jeg kan især godt lide denne kerne , der viser en reaktiv implementering i Java 8 (ved hjælp af RxJava) – ikke sa det ville dog være en god produktion kode.

Svar

Første af alt sammen, jeg synes algoritmen er ret smart, hvilket for min ydmyge oplevelse ikke er så almindeligt for en universitetsstuderende. Så tillykke, hvis du selv kom på det! Hvis du “leder efter smarte implementeringer, vil jeg anbefale funktionelle, f.eks. i Haskell ; se også Korteste spil of life .

Pas nu på smartness. En god kode skal være let at læse , let at forstå . Det er selvfølgelig ikke altid muligt, når man beskæftiger sig med kompleks algoritme, men jeg mener, at det skal være et mål.

jjjjjjjjjjj sagde:
Bemærk: de fleste kommentarer er taget ud, fordi de ikke tilføjer meget til læsbarheden af koden

Pointen med kommentarer er at hjælpe folk med at forstå din kode (generelt set fokusere på ” hvorfor ” snarere end på ” hvad “). Her, for at hjælpe folk med at forstå, måtte du tilføje meget tekst til dit indlæg. Ideelt set er dette ikke nødvendigt, fordi koden er:

  • selvdokumenteret,
  • kommenteret for at rydde komplekse / implicitte ting op.

For eksempel er her en hurtig omskrivning af din kode i et forsøg på at gøre koden mere ekspressiv:

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

Jeg omdøbte for det meste nogle variabler / metoder og introducerede nogle værktøjsmetoder. Koden er lidt længere og føles mere verbose men er IMHO også lettere at forstå. Det er stadig meget proceduremæssigt (hvilket i sig selv ikke er dårligt, især for et så simpelt program), men det kan være en god idé at prøve at tilføje mere ekspressivitet ved at introducere nye klasser som Board eller Cell. Du finder sådanne OO-implementeringer på GitHub .

Din kode kan også løbe ind i hukommelsesproblemer med store kort. Faktisk gemmer din board[][] variabel alle cellerne, selv døde. Med et 10000 x 10000 kort, der kun indeholder ~ 5/6 celler, spilder du meget hukommelse. En løsning er at bruge et sparsomt array (dybest set en sæt, der kun indeholder levende celler).

Som en sidebemærkning forsøgte jeg for et par år siden også at modellere en stærkt konfigurerbar GoL i en ” ren ” OO måde; min kode er på GitHub , hvis du vil tjekke den ud. Metoden beregner den næste generation af verden er ImmutableGeneration :: nextGeneration ; givet et sæt levende celler, beregner det grundlæggende: 1) beregner alle naboceller og derefter 2) beholder kun dem, der vil være i live. Regler, der angiver, om en celle vil være i live eller død, implementeres i Rule.java .


REDIGER : personlig mening om kortfattethed kontra bredhed, når det kommer til n aming at svare på en kommentar

Først og fremmest tror jeg, at der ikke er nogen rigtige svar: det handler om kompromiser og personlige præferencer. Navngivning er hård, og du finder masser af artikler om emnet.

Der er kun to hårde ting i datalogi: cache ugyldiggørelse og navngivning af ting
– Phil Karlton

Min opfattelse er, at koncentration er behagelig, men kan føre til uklarheder. Og tvetydighed, især skjult, er en trussel.Det første eksempel, der kommer til mig, er ved fejlagtigt at blande enheder:

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

At Når det er sagt, gør lange navne koden sværere at læse. De kan reduceres ved at tage to ting i betragtning:

  • konteksten (f.eks. Navnet på den vedlagte metode / klasse / pakke),
  • omfanget (en lokal variabel i en 3-linjemetode kan være fint med et kort navn, mens en funktion, der bruges flere gange på tværs af hele kodebasen, muligvis kræver en længere).

Det er også det, der anbefales af Googles navngivningskonventioner .

Som en sidste bemærkning, som du foreslog, kan meget lange navne ses som kodelugt. Normalt er problemet manglende samhørighed (klassen / metoden gør for meget forskellige ting – endnu en gang, ingen klare målinger på dette, det er op til udviklerens følelse). For eksempel kan vi i den kode, jeg foreslog, tænke på populateWorldWithAliveCellsFromPreviousGeneration som en metode, der holder ansvarsområder: 1) beregning af cellerne, der vil være i live ved næste generation og 2) befolket verden. Vi kunne således opdele det i to: populateWorldWith(aliveCellsFromPreviousGeneration()).

På samme måde kunne vi samle attributterne, hvilket navn slutter med ” atNextGeneration ” under en ny 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)); } ... }  

Men at opdele logikken i for mange klasser vil også øge arkitekturens kompleksitet og gøre det sværere at forstå strømmen.

Som en konklusion Jeg vil råde dig til at huske på, at ethvert stykke kode er modtageligt for at blive ændret af udviklere, der ikke har nogen tidligere viden om projektet, og som skal forstå, hvad koden gør, og hvorfor det gør det, så de kan vedligeholde det eller genbruge dele uden at indføre regressioner. Der er ingen sølvkugle: kun kompromiser, og hvad der betyder noget, når du træffer et valg, er at:

  • du kan identificere kompromiset,
  • du forstår fordele og ulemper ved hvert alternativ og vælg et af dem bevidst.

(men tryk ikke for meget pres på dig: KISS og husk at koden kan refaktoriseres derefter)

Kommentarer

  • Mange tak for din kommentar. Jeg er så glad for, at du kommenterede, fordi jeg aldrig ville have tænkt på kun at opbevare de levende celler (dette ændrer meget af min kode, og efter min mening gør det meget bedre). Jeg ville gerne spørge lidt om din mening om balancen mellem at være klar med variabelnavne og at være kortfattet. Med andre ord, hvordan kan du bestemme, hvornår programmet er for detaljeret? Betyder det, at du skal bruge ekstraordinær lang tid på at skabe de rigtige variabelnavne, eller at der er noget defekt med din logik og design af koden? Tak igen.
  • Jeg redigerede mit svar for at dele min opfattelse af det. Det ‘ er meget tekst, der grundlæggende siger, at ” der ikke er nogen rigtige svar, det ‘ alt om kompromiser, så tænk på fordele og ulemper, når du træffer et valg “.

Svar

Jeg har bare en lille anbefaling vedrørende læsbarhed. Når du har en metode, der hedder printBoard, forventer du normalt, at den udskriver tavlen . Et bedre navn til denne metode ville være boardToString.

Skriv et svar

Din e-mailadresse vil ikke blive publiceret. Krævede felter er markeret med *