Tocmai am terminat de implementat o versiune a jocului Conway Viața folosind Java.
Fiind doar un student, sunt sigur că codul meu nu este aproape perfect și mă întrebam dacă puteți vedea codul meu. Ce pot îmbunătăți? Există modalități mai rapide să implementez anumite zone ale codului meu? Există un cod în exces pe care să îl pot elimina? Există un mod mai inteligent de a implementa Conway „s Game of Life?
EDIT:
În speranța de a primi mai multe feedback-uri, iată teoria din spatele implementării mele:
Pentru referință, iată reguli pentru jocul vieții lui Conway (preluat din Wikipedia):
- Orice celulă vie cu mai puțin de doi vecini vii moare, ca și cum ar fi subpopulație.
- Orice celulă vie cu remorcă sau trei vecini vii trăiesc pentru generația următoare.
- Orice celulă vie cu mai mult de trei vecini vii moare, ca și cum ar fi populație
- Orice celulă moartă cu exact trei vecini vii devine o celulă vie, ca prin reproducere.
Prezentare generală:
- A perspective diferite despre Jocul vieții lui Conway
- Reguli nerostite
- Explicația metodelor importante (și a structurilor de date utilizate)
O perspectivă diferită asupra jocului vieții lui Conway
Să ne imaginăm mai întâi jocul vieții ca o grilă de anxietate (noi va presupune, de asemenea, că această grilă are coordonate astfel încât colțul din stânga jos este notat ca (0,0) și colțul din dreapta sus este notat ca (n, n) unde n este un număr întreg pozitiv). Această grilă bidimensională reprezintă un grup de n * n număr de celule. Fiecare bloc de grilă poate fi gândit ca o celulă, care nu numai stochează o valoare booleană (moartă sau vie) pentru a descrie starea celulei, ci și detalii locația acesteia prin intermediul coordonatelor sale. În plus, starea actuală a tuturor celulelor determină ce celule vor muri, vor continua să trăiască sau se vor naște în generația următoare în conformitate cu regulile de mai sus.
Într-o altă perspectivă, totuși, jocul lui Conway of life este foarte asemănător cu jocul minesweeper. Ne putem gândi la o celulă vie ca la o mină și vecinii săi care stochează numărul de mine care sunt cele mai apropiate de ea. În acest fel, putem folosi cu ușurință regulile de mai sus pentru a determina generația viitoare (în special ce celule vor muri și care celule se vor naște).
Ce s-ar putea să spui despre celulele care sunt în viață în prezent? cere? Ei bine, le putem reprezenta cu ușurință ca un număr întreg mai mare de 10, unde locul unuia indică câți vecini vii are celula vie în prezent, iar locurile celor zece indică faptul că celula este vie.
Reguli nerostite
O observație care mi s-a întâmplat este că jocul al vieții este preocupat doar de celulele vii. Numai celulele care sunt în viață pot muri, celulele care continuă să trăiască trebuie să fie deja vii, iar celulele se pot naște numai dacă au vecini care sunt în viață. Ca urmare, verificarea întregii rețele (complexitatea timpului: O (n ^ 2)) pentru a determina generația viitoare de celule ar fi o deșeuri complete. Ar fi mult mai rapid dacă aș stoca toate celulele vii în prezent și aș verifica fiecare celulă vie împreună cu vecinii lor pentru a determina următoarea generație (exact ceea ce am făcut).
Explicația metodelor importante (și a structurilor de date utilizate)
birth (): iterează pe un HashMap care conține o pereche cheie-valoare de toate celulele vii împreună cu vecinii săi. Dacă perechea cheie-valoare urmează jocul regulilor vieții de mai sus, cheia (o valoare întreagă care reprezintă locația unei celule) este apoi împinsă pe un teanc care conține următoarea generație de celule vii. După fiecare iterație, valoarea grilei este resetată la 0, iar perechea cheie-valoare este eliminată din HashMap.
insertAlive (): deschide stiva și introduce celula vie în grilă. Inserarea unei celule vii urmează structura măturătorului (vecinii unei celule vii vor fi incrementate cu 1, iar celula vie va fi mărită cu 10 pentru a indica faptul că este vie). Toți vecinii și celulele vii sunt apoi puse într-un HashMap astfel încât birth () să poată rula corect
printBoard () (ar trebui să fie numit boardToString): folosește un stringbuilder pentru a formata grila într-un șir.
Notă : majoritatea comentariilor au fost eliminate deoarece nu adaugă mult la lizibilitatea codul
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"); } } }
Comentarii
- Ați făcut ‘ un punct de a spune că aceasta este o implementare diferită, dar nu există nimic care să explice teoria din spatele implementării dvs. sau algoritmii și formulele relativ obscure pe care ‘ le utilizați.
- Acesta este un exercițiu popular, așa că v-aș sfătui să aruncați o privire asupra implementărilor pe care le puteți găsi online: ‘ este foarte instructiv. Îmi place în special acest esențial care arată o implementare reactivă în Java 8 (folosind RxJava) – nu sa totuși, ar fi un cod bun de producție .
Răspuns
În primul rând totul, cred că algoritmul este destul de inteligent, ceea ce, pentru umila mea experiență, nu este atât de obișnuit pentru un student. Așadar, felicitări dacă ai venit cu el singur! Dacă sunteți în căutarea unor implementări inteligente, aș recomanda unele funcționale, de ex. în Haskell ; consultați și Cel mai scurt joc de viață .
Acum, ferește-te de inteligență. Un cod bun ar trebui să fie ușor de citit , ușor de înțeles . Desigur, acest lucru nu este întotdeauna posibil atunci când este vorba despre algoritmul complex, dar cred că ar trebui să fie o țintă.
jjjjjjjjjjjj a spus:
Notă: majoritatea comentariilor au fost scoase pentru că nu adaugă mult pentru lizibilitatea codului
Scopul comentariilor este de a ajuta oamenii să înțeleagă codul dvs. (în general vorbind, concentrați-vă pe ” de ce ” mai degrabă decât pe ” ce „). Aici, pentru a ajuta oamenii să înțeleagă, ați trebuit să adăugați mult text la postarea dvs. În mod ideal, acest lucru nu este necesar deoarece codul este:
- autodocumentat,
- comentat pentru a șterge lucrurile complexe / implicite.
De exemplu, iată o rescriere rapidă a codului dvs., în încercarea de a face codul mai expresiv:
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(); } }
Am redenumit majoritatea unor variabile / metode și am introdus câteva metode de utilitate. Codul este puțin mai lung și se simte mai mult detaliat, dar IMHO este, de asemenea, mai ușor de înțeles. Este încă foarte procedural (ceea ce nu este rău în sine, mai ales pentru un program atât de simplu), dar poate doriți să încercați să adăugați mai multă expresivitate prin introducerea unor noi clase precum Board
sau Cell
. Veți găsi astfel de implementări OO pe GitHub .
Codul dvs. poate avea probleme de memorie cu plăcile mari. Într-adevăr, variabila dvs. board[][]
stochează toate celulele, chiar și cele moarte. Cu o placă de 10000 x 10000 care conține doar ~ 5/6 celule, veți pierde multă memorie. O soluție este să utilizați o matrice rar (practic, o set care conține doar celule vii).
Ca o notă laterală, acum câțiva ani am încercat să modelez un GoL extrem de configurabil într-un ” pur ” OO way; codul meu este pe GitHub dacă doriți să îl verificați. Metoda de calcul a următoarei generații a lumea este ImmutableGeneration :: nextGeneration ; dat fiind un set de celule vii, practic: 1) calculează toate celulele vecine, apoi 2) păstrează doar pe cele care vor fi în viață. Regulile care indică dacă o celulă va fi vie sau moartă sunt implementate în Rule.java .
EDIT : personal opinie despre concizie versus verbozitate când vine vorba de n urmează să răspund la un comentariu
În primul rând, cred că nu există răspunsuri corecte: este vorba doar de compromisuri și preferințe personale. Numirea este dificilă și veți găsi o mulțime de articole pe această temă.
Există doar două lucruri dificile în informatică: invalidarea cache-ului și denumirea lucrurilor
– Phil Karlton
Consider că concivitatea este plăcută, dar poate duce la ambiguități. Și ambiguitatea, mai ales cea ascunsă, este o amenințare.Primul exemplu care îmi vine în minte este amestecarea greșită a unităților:
// Everything looks good... double pathLength = distanceFromGoal + distanceToTarget; // ... but adding units makes easy to spot bugs double pathLengthInKilometers = distanceFromGoalInMeters + distanceToTargetInMillimeters;
Că fiind spus, numele lungi fac codul mai greu de citit. Acestea pot fi reduse luând în considerare două lucruri:
- contextul (de exemplu, numele metodei / clasei / pachetului de anexare),
- domeniul de aplicare (o variabilă locală în o metodă cu 3 linii poate fi bună cu un nume scurt, în timp ce o funcție utilizată de mai multe ori pe întreaga bază de cod poate avea nevoie de una mai lungă).
De asemenea, este recomandat de Convențiile de numire Google .
Ca ultimă notă, așa cum ați sugerat că numele foarte lungi pot fi văzute ca mirosuri de cod. De obicei, problema este o lipsă de coeziune (clasa / metoda face lucruri prea diferite – încă o dată, nu există valori clare în acest sens, este de până la sentimentul dezvoltatorului). De exemplu, în codul pe care l-am propus ne putem gândi la populateWorldWithAliveCellsFromPreviousGeneration
ca la o metodă care deține responsabilități: 1) calcularea celulelor care vor fi vii la generația următoare și 2) popularea lumii. O putem împărți astfel în două: populateWorldWith(aliveCellsFromPreviousGeneration())
.
În același mod am putea aduna atributele al căror nume se termină cu ” atNextGeneration ” sub o nouă clasă Generation
:
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)); } ... }
Dar împărțirea logicii în prea multe clase va crește, de asemenea, complexitatea arhitecturii și va face mai greu de înțeles fluxul.
concluzie Vă sfătuiesc să rețineți că orice bucată de cod poate fi modificată de dezvoltatorii care nu au cunoștințe anterioare despre proiect și care trebuie să înțeleagă ce face codul și o face astfel încât să o poată întreține sau reutiliza piese fără a introduce regresii. Nu există argint: doar compromisuri și ceea ce contează atunci când faci o alegere este că:
- poți identifica compromisul,
- înțelegi avantajele și dezavantajele fiecare alternativă și alegeți una dintre ele cu bună știință.
(dar nu faceți prea multă presiune asupra dumneavoastră: KISS și amintiți-vă că codul poate fi refactorizat ulterior)
Comentarii
- Vă mulțumesc foarte mult pentru comentariu. Mă bucur atât de mult că ai comentat pentru că nu m-aș fi gândit niciodată să stochez doar celulele vii (acest lucru modifică mult codul meu și, după părerea mea, îl face mult mai bun) Am vrut să vă întreb un pic despre părerea dvs. despre echilibrul dintre a fi clar cu nume variabile și a fi concis. Cu alte cuvinte, cum puteți determina când programul este prea detaliat? Asta înseamnă că trebuie să petreceți un timp extraordinar de lung creând numele variabilelor potrivite sau că există ceva defect în logica și designul codului dvs.? Vă mulțumim din nou.
- Mi-am editat răspunsul pentru a-mi împărtăși opinia. ‘ este o mulțime de text care spune practic că ” nu există răspunsuri corecte, ‘ Este vorba despre compromisuri, deci gândiți-vă la argumente pro și contra atunci când faceți o alegere „.
Răspuns
Am doar o mică recomandare cu privire la lizibilitate. Când aveți o metodă numită printBoard
, în mod normal vă așteptați să tipărească placa . Un nume mai bun pentru această metodă ar fi boardToString
.