Jeg er akkurat ferdig med å implementere en versjon av Conways Game of Livet ved hjelp av Java.

Som bare studenter er jeg sikker på at koden min ikke er i nærheten av perfekt, og lurte på om du kunne se på koden min. Hva kan jeg forbedre meg på? Er det raskere måter å implementere visse områder av koden min? Er det overflødig kode som jeg kan trimme bort? Er det en smartere måte å implementere Conways «Game of Life» på?

EDIT:

I håp om å få mer tilbakemelding, her er teorien bak implementeringen min:

For referanse, her er regler for Conways livsspill (hentet fra wikipedia):

  1. Enhver levende celle med færre enn to levende naboer dør, som om den er underbefolkning.
  2. Enhver levende celle med slep eller tre levende naboer lever videre til neste generasjon.
  3. Enhver levende celle med mer enn tre levende naboer dør, som om den er rpopulation
  4. Enhver død celle med nøyaktig tre levende naboer blir en levende celle, som ved reproduksjon.

Oversikt:

  1. A forskjellig syn på Conways livsspill
  2. Uuttalte regler
  3. Forklaring på viktige metoder (og datastrukturer som brukes)

Et annet syn på Conways Game of Life

La oss først forestille oss Game of Life som anxn grid (vi vil også anta at dette rutenettet har koordinater slik at det nederste venstre hjørnet er betegnet som (0,0) og det øvre høyre hjørnet er betegnet som (n, n) der n er et positivt heltall). Dette 2-dimensjonale rutenettet representerer en gruppe på n * n antall celler. Hver rutenettblokk kan betraktes som en celle, som ikke bare lagrer en boolsk verdi (død eller levende) for å beskrive cellens status, men også beskriver dens beliggenhet via koordinatene. I tillegg avgjør den nåværende tilstanden til alle celler hvilke celler som vil dø, fortsette å leve eller bli født i neste generasjon i samsvar med reglene som er funnet ovenfor.

I et annet perspektiv, derimot, Conways spill av livet er veldig likt spillet minesveiper. Vi kan tenke på en levende celle som en gruve, og dens naboer som lagrer antall miner som er nærmest den. På denne måten er vi i stand til enkelt å bruke reglene ovenfor for å bestemme den fremtidige generasjonen (spesielt hvilke celler som skal dø, og hvilke celler som vil bli født).

Hva med cellene som er i live for øyeblikket spørre? Vel, vi kan lett representere disse som et heltall større enn 10, der ens plass indikerer hvor mange levende naboer den nåværende levende cellen har, og de ti stedene indikerer at cellen er i live.

Uuttalte regler

En observasjon som skjedde for meg er at spillet av livet er bare opptatt av levende celler. Bare celler som er i live kan dø, celler som fortsetter å leve, må allerede leve, og celler kan bare bli født hvis de har naboer som er i live. Som et resultat ville det være et fullstendig avfall å sjekke hele rutenettet (tidskompleksitet: O (n ^ 2)) for å bestemme den fremtidige generasjonen av celler. Det ville være mye raskere hvis jeg lagret alle cellene som er i live og sjekket hver levende celle sammen med naboene for å bestemme neste generasjon (som er nøyaktig hva jeg gjorde).

Forklaring på viktige metoder (og datastrukturer som brukes)

fødsel (): itererer over et HashMap som inneholder et nøkkelverdipar av alle levende celler sammen med naboene. Hvis nøkkelverdiparet følger livets regler ovenfor, skyves nøkkelen (en heltall som representerer plasseringen av en celle) på en stabel som inneholder neste generasjon levende celler. Etter hver iterasjon tilbakestilles verdien til rutenettet til 0, og nøkkelverdiparet fjernes fra HashMap.

insertAlive (): spretter bunken og setter den levende cellen inn i rutenettet. Å sette inn en levende celle følger strukturen til minesveiper (naboer til en levende celle økes med 1 og den levende cellen økes med 10 for å indikere at den er i live). Alle naboene og de levende cellene blir deretter satt inn i et HashMap slik at fødsel () kan kjøre ordentlig

printBoard () (skal hete boardToString): bruker en strengbygger til å formatere rutenettet til en streng.

Merk : de fleste kommentarer er tatt ut fordi de ikke legger mye til lesbarheten av 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 laget et poeng med å si at dette er en annen implementering, men det er ingenting som forklarer teorien bak implementeringen din eller de relativt uklare algoritmene og formlene du ‘ bruker.
  • Dette er en populær øvelse, så jeg vil også råde deg til å ta en titt på implementeringene du kan finne på nettet: det ‘ er veldig lærerikt. Jeg liker spesielt denne kjernen som viser en reaktiv implementering i Java 8 (ved bruk av RxJava) – ikke sa det ville imidlertid være en god produksjon -kode.

Svar

Først av alt, jeg tror algoritmen er ganske smart, noe som for min ydmyke erfaring ikke er så vanlig for en student. Så gratulerer hvis du fant på det selv! Hvis du leter etter smarte implementeringer, vil jeg anbefale funksjonelle, for eksempel i Haskell ; se også Korteste spill of life .

Vær oppmerksom på smartness. En god kode skal være lett å lese , lett å forstå . Dette er selvfølgelig ikke alltid mulig når det gjelder kompleks algoritme, men jeg mener at det burde være et mål.

jjjjjjjjjjjj sa:
Merk: de fleste kommentarer er tatt ut fordi de ikke legger til mye for lesbarheten til koden

Poenget med kommentarer er å hjelpe folk til å forstå koden din (generelt sett fokusere på » hvorfor » i stedet for på » hva «). Her, for å hjelpe folk å forstå, måtte du legge til mye tekst i innlegget ditt. Ideelt sett er dette ikke nødvendig fordi koden er:

  • selvdokumentert,
  • kommentert for å fjerne komplekse / implisitte ting.

For eksempel, her er en rask omskriving av koden din i et forsøk på å gjøre koden mer uttrykksfull:

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øpte for det meste noen variabler / metoder og introduserte noen verktøymetoder. Koden er litt lengre og føles mer verbose men er IMHO også lettere å forstå. Det er fremdeles veldig prosessuelt (som ikke er dårlig i seg selv, spesielt for et så enkelt program), men det kan være lurt å prøve å legge til mer ekspressivitet ved å introdusere nye klasser som Board eller Cell. Du vil finne slike OO-implementeringer på GitHub .

Koden din kan også komme i minneproblemer med store tavler. Faktisk lagrer board[][] variabelen alle cellene, til og med døde. Med et 10000 x 10000-kort som bare inneholder ~ 5/6 celler, vil du kaste bort mye minne. En løsning er å bruke et sparsomt utvalg (i utgangspunktet en sett som bare inneholder levende celler).

Som en sidemerknad prøvde jeg også for noen år siden å modellere en svært konfigurerbar GoL i en » ren » OO måte; koden min er på GitHub hvis du vil sjekke den ut. Metoden som beregner neste generasjon av verden er ImmutableGeneration :: nextGeneration ; gitt et sett med levende celler, beregner det i utgangspunktet: 1) alle naboceller, deretter 2) beholder bare de som vil være i live. Regler som indikerer om en celle vil være i live eller død, er implementert i Rule.java .


REDIGER : personlig mening om kortfattethet kontra ordlyd når det gjelder n aming å svare på en kommentar

Først og fremst tror jeg at det ikke er noen riktige svar: det handler om avveininger og personlige preferanser. Navngivning er vanskelig, og du vil finne mange artikler om emnet.

Det er bare to vanskelige ting i datavitenskap: cache-ugyldiggjøring og navngivning av ting
– Phil Karlton

Min oppfatning er at konsesjon er hyggelig, men kan føre til uklarheter. Og uklarhet, spesielt skjult, er en trussel.Det første eksemplet som kommer til meg er feilaktig å blande enheter:

 // 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, gjør lange navn koden vanskeligere å lese. De kan reduseres ved å ta to ting i betraktning:

  • konteksten (f.eks. Navnet på den vedlagte metoden / klassen / pakken),
  • omfanget (en lokal variabel i en 3-linjers metode kan være bra med et kort navn, mens en funksjon som brukes flere ganger på tvers av hele kodebasen kan trenge en lengre).

Det er også det som anbefales av Googles navnekonvensjoner .

Som en siste merknad, som du foreslo, kan veldig lange navn bli sett på som kodelukt. Vanligvis er problemet mangel på samhold (klassen / metoden gjør for mye forskjellige ting – nok en gang, ingen klare beregninger på dette, det er opp til utviklerens følelse). For eksempel, i koden jeg foreslo, kan vi tenke på populateWorldWithAliveCellsFromPreviousGeneration som en metode som holder ansvarsområder: 1) beregne cellene som vil være i live ved neste generasjon og 2) befolke verden. Vi kunne dermed dele den i to: populateWorldWith(aliveCellsFromPreviousGeneration()).

På samme måte kunne vi samle attributtene som navnet ender 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 å dele logikken i for mange klasser vil også øke arkitekturkompleksiteten og gjøre det vanskeligere å forstå flyten.

Som en konklusjon Jeg vil råde deg til å huske på at alle kodestykker kan modifiseres av utviklere som ikke har noen forkunnskaper om prosjektet, og som må forstå hva koden gjør, og hvorfor det gjør det slik at de kan vedlikeholde det eller gjenbruke deler uten å innføre regresjoner. Det er ingen silverbullet: bare kompromisser, og det som betyr noe når du tar et valg er at:

  • du kan identifisere kompromisset,
  • du forstår fordeler og ulemper med hvert alternativ og velg ett av dem bevisst.

(men ikke skyv for mye press på deg: KISS og husk at koden kan ombygges deretter)

Kommentarer

  • Tusen takk for kommentaren din. Jeg er så glad du kommenterte fordi jeg aldri hadde tenkt på å bare lagre de levende cellene (dette endrer mye av koden min, og etter min mening gjør den mye bedre). Jeg ønsket å spørre litt om din mening om balansen mellom å være tydelig med variable navn og å være kortfattet. Med andre ord, hvordan kan du bestemme når programmet er for ordentlig? Betyr det at du må bruke usedvanlig lang tid på å lage de riktige variabelnavnene, eller at det er noe feil med logikken og utformingen av koden? Takk igjen.
  • Jeg redigerte svaret mitt for å dele mitt syn på det. Det ‘ er mye tekst som i utgangspunktet sier at » det ikke er noen riktige svar, det ‘ handler om kompromisser, så tenk på fordeler og ulemper når du tar et valg «.

Svar

Jeg har bare en liten anbefaling angående lesbarhet. Når du har en metode som heter printBoard, forventer du normalt at den skriver ut tavlen . Et bedre navn for den metoden ville være boardToString.

Legg igjen en kommentar

Din e-postadresse vil ikke bli publisert. Obligatoriske felt er merket med *