Jag har precis implementerat en version av Conways Game of Livet med Java.
Eftersom jag bara är högskolestudent är jag säker på att min kod inte är nästan perfekt och undrade om du kunde titta på min kod. Vad kan jag förbättra på? Finns det snabbare sätt för att implementera vissa områden i min kod? Finns det överflödig kod som jag kan klippa bort? Finns det ett smartare sätt att implementera Conways Game of Life?
EDIT:
I hopp om att få mer feedback, här är teorin bakom min implementering:
För referens, här är regler för Conways livsspel (hämtat från wikipedia):
- Alla levande celler med färre än två levande grannar dör, som om de är underbefolkade.
- Alla levande celler med släp eller tre levande grannar lever vidare till nästa generation.
- Alla levande celler med mer än tre levande grannar dör, som om de rpopulation
- Alla döda celler med exakt tre levande grannar blir en levande cell, som genom reproduktion.
Översikt:
- A annorlunda syn på Conways livsspel
- Outtalade regler
- Förklaring av viktiga metoder (och datastrukturer som används)
En annorlunda syn på Conways Game of Life
Låt oss först föreställa oss Game of Life som angelägen rutnät (vi antar också att detta rutnät har koordinater så att det nedre vänstra hörnet betecknas som (0,0) och det övre högra hörnet betecknas som (n, n) där n är ett positivt heltal). Detta tvådimensionella rutnät representerar en grupp av n * n antal celler. Varje rutnät kan ses som en cell som inte bara lagrar ett booleskt värde (död eller levande) för att beskriva cellens status utan också beskriver dess plats via dess koordinater. Dessutom bestämmer det aktuella tillståndet för alla celler vilka celler som kommer att dö, fortsätta att leva eller födas i nästa generation i enlighet med reglerna ovan.
I ett annat perspektiv, dock Conways spel of life liknar spelet minesvepare. Vi kan tänka på en levande cell som en gruva och dess grannar som lagrar antalet gruvor som ligger närmast den. På det här sättet kan vi enkelt använda reglerna ovan för att bestämma den framtida generationen (särskilt vilka celler som kommer att dö och vilka celler som kommer att födas).
Vad sägs om de celler som för närvarande lever du kanske fråga? Vi kan enkelt representera dessa som ett heltal större än 10, där ens plats indikerar hur många levande grannar den för närvarande levande cellen har, och de tio platserna indikerar att cellen lever.
Outtalade regler
En iakttagelse som föll på mig är att spelet av livet är bara bekymrad över levande celler. Endast celler som lever kan dö, celler som fortsätter att leva måste redan leva och celler kan bara födas om de har grannar som lever. Som ett resultat skulle det vara ett fullständigt slöseri att kontrollera hela rutnätet (tidskomplexitet: O (n ^ 2)) för att bestämma den framtida generationen av celler. Det skulle vara mycket snabbare om jag lagrade alla celler som för närvarande lever och kollade varje levande cell tillsammans med sina grannar för att bestämma nästa generation (vilket är exakt vad jag gjorde).
Förklaring av viktiga metoder (och datastrukturer som används)
födelse (): itereras över en HashMap som innehåller ett nyckel-värdepar av alla levande celler tillsammans med sina grannar. Om nyckel-värde-paret följer livsreglerna ovan, trycks nyckeln (ett heltal som representerar platsen för en cell) på en stapel som innehåller nästa generation av levande celler. Efter varje iteration återställs rutnätets värde till 0 och nyckel-värdeparet tas bort från HashMap.
insertAlive (): poppar stacken och infogar den levande cellen i rutnätet. Att infoga en levande cell följer strukturen hos minesveparen (grannar till en levande cell ökas med 1 och den levande cellen ökas med 10 för att beteckna att den lever). Alla grannar och levande celler placeras sedan i en HashMap så att födelse () kan köras ordentligt
printBoard () (ska kallas boardToString): använder en strängbyggare för att formatera rutnätet till en sträng.
Obs : de flesta kommentarer har tagits ut eftersom de inte lägger mycket till läsbarheten 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 gjort en poäng att säga att detta är en annan implementering, men det finns inget som förklarar teorin bakom din implementering eller de relativt dunkla algoritmer och formler som du ’ använder.
- Detta är en populär övning så jag skulle också rekommendera dig att ta en titt på implementeringarna du kan hitta online: det ’ är väldigt lärorikt. Jag gillar särskilt denna kärna som visar en reaktiv implementering i Java 8 (med RxJava) – inte sa det skulle dock vara en bra produktion -kod.
Svar
Först av allt, jag tycker att algoritmen är ganska smart vilket för min ödmjuka erfarenhet inte är så vanligt för en högskolestudent. Så grattis om du själv kom på det! Om du letar efter smarta implementeringar skulle jag rekommendera funktionella, t.ex. i Haskell ; se även Kortaste spelet of life .
Akta dig nu för smarthet. En bra kod ska vara lätt att läsa , lätt att förstå . Det är naturligtvis inte alltid möjligt när det gäller komplex algoritm men jag tror att det borde vara ett mål.
jjjjjjjjjjjj sade:
Obs! De flesta kommentarer har tagits ut eftersom de inte lägger till mycket till läsbarheten för koden
Poängen med kommentarer är att hjälpa människor att förstå din kod (fokusera generellt på ” varför ” snarare än på ” vad ”). Här, för att hjälpa människor att förstå, var du tvungen att lägga till mycket text i ditt inlägg. Helst behövs inte detta eftersom koden är:
- självdokumenterad,
- kommenterad för att rensa komplexa / implicita saker.
Till exempel är här en snabb omskrivning av din kod i ett försök att göra koden mer uttrycksfull:
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(); } }
Jag bytte namn på vissa variabler / metoder och introducerade några verktygsmetoder. Koden är lite längre och känns mer verbose men är IMHO också lättare att förstå. Det är fortfarande väldigt procedurellt (vilket i sig inte är dåligt, särskilt för ett så enkelt program) men du kanske vill försöka lägga till mer uttrycksfullhet genom att införa nya klasser som Board
eller Cell
. Du hittar sådana OO-implementeringar på GitHub .
Din kod kan också stöta på minnesproblem med stora kort. Faktum är att din board[][]
-variabel lagrar alla cellerna, även döda. Med ett 10000 x 10000-kort som endast innehåller ~ 5/6 celler kommer du att slösa mycket minne. En lösning är att använda en gles array (i princip en uppsättning som endast innehåller levande celler).
Som en sidoteckning försökte jag för några år sedan också modellera en mycket konfigurerbar GoL i en ” ren ” OO sätt; min kod finns på GitHub om du vill kolla in den. Metoden som beräknar nästa generation av världen är ImmutableGeneration :: nextGeneration ; ges en uppsättning levande celler, i grund och botten: 1) beräknar alla grannceller sedan 2) behåll bara de som kommer att leva. Regler som anger om en cell kommer att leva eller är död implementeras i Rule.java .
REDIGERA : personlig åsikt om kortfattning kontra ordalighet när det gäller n jag vill svara på en kommentar
Först och främst tror jag att det inte finns några rätta svar: det handlar om avvägningar och personliga preferenser. Namngivning är svårt och du hittar massor av artiklar om ämnet.
Det finns bara två svåra saker i datavetenskap: cache-ogiltigförklaring och namnge saker
– Phil Karlton
Min uppfattning är att koncentration är trevlig men kan leda till tvetydigheter. Och tvetydighet, särskilt dold, är ett hot.Det första exemplet som jag tänker på är att felaktigt blanda enheter:
// Everything looks good... double pathLength = distanceFromGoal + distanceToTarget; // ... but adding units makes easy to spot bugs double pathLengthInKilometers = distanceFromGoalInMeters + distanceToTargetInMillimeters;
Att som sagt, långa namn gör koden svårare att läsa. De kan minskas genom att ta hänsyn till två saker:
- sammanhanget (t.ex. namnet på den omslutande metoden / klassen / paketet),
- omfånget (en lokal variabel i en 3-rads metod kan vara bra med ett kort namn medan en funktion som används flera gånger över hela kodbasen kan behöva en längre).
Det är också det som rekommenderas av Googles namnkonventioner .
Som en sista anmärkning, som du föreslog, kan mycket långa namn ses som kodlukt. Vanligtvis är frågan brist på sammanhållning (klassen / metoden gör för mycket olika saker – än en gång, inga tydliga mått på detta, det är upp till utvecklarens känsla). I koden som jag föreslog kan vi till exempel tänka på populateWorldWithAliveCellsFromPreviousGeneration
som en metod med ansvar: 1) beräkna cellerna som kommer att leva vid nästa generation och 2) befolka världen. Vi kunde alltså dela det i två: populateWorldWith(aliveCellsFromPreviousGeneration())
.
På samma sätt kunde vi samla attributen vilket namn slutar med ” atNextGeneration ” under en ny Generation
-klass:
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 att dela logiken i för många klasser kommer också att öka arkitekturens komplexitet och göra det svårare att förstå flödet.
Som en slutsats Jag skulle rekommendera dig att komma ihåg att varje kodkod kan modifieras av utvecklare som inte har någon förkunskap om projektet och som måste förstå vad koden gör och varför det gör det så att de kan underhålla det eller återanvända delar utan att införa regressioner. Det finns ingen silverkula: bara avvägningar, och det som är viktigt när du väljer är att:
- du kan identifiera avvägningen,
- du förstår fördelarna och nackdelarna med varje alternativ och välj en av dem medvetet.
(men tryck inte för mycket tryck på dig: KISS och kom ihåg att koden kan omformas därefter)
Kommentarer
- Tack så mycket för din kommentar. Jag är så glad att du kommenterade eftersom jag aldrig skulle ha funderat på att bara lagra de levande cellerna (detta ändrar mycket av min kod och gör enligt min mening det mycket bättre). Jag ville fråga lite om din åsikt om balansen mellan att vara tydlig med variabla namn och att vara kortfattad. Med andra ord, hur kan du bestämma när programmet är för ordentligt? Betyder det att du måste spendera utomordentligt lång tid på att skapa rätt variabelnamn eller att det är något fel med din logik och utformning av koden? Tack igen.
- Jag redigerade mitt svar för att dela min åsikt om det. Det ’ är mycket text som i princip säger att ” att det inte finns några rätta svar, det ’ allt om kompromisser så tänk på fördelar och nackdelar när du gör ett val ”.
Svar
Jag har bara en liten rekommendation angående läsbarhet. När du har en metod som heter printBoard
, förväntar du dig normalt att den skriver ut tavlan . Ett bättre namn för den metoden skulle vara boardToString
.