Ho appena finito di implementare una versione di Conway “s Game of La vita usando Java.

Essendo solo uno studente universitario, sono sicuro che il mio codice non è neanche lontanamente perfetto e mi chiedevo se potessi guardare il mio codice. Cosa posso migliorare? Esistono modi più veloci per implementare alcune aree del mio codice? Cè del codice in eccesso che posso tagliare? Cè un modo più intelligente di implementare “Il gioco della vita di Conway”?

EDIT:

Nella speranza di ricevere più feedback, ecco la teoria alla base della mia implementazione:

Per riferimento, ecco i regole per il gioco della vita di Conway (tratte da wikipedia):

  1. Qualsiasi cellula viva con meno di due vicini vivi muore, come per sottopopolazione.
  2. Qualsiasi cellula viva con due o tre vicini vivi sopravvive alla generazione successiva.
  3. Qualsiasi cellula vivente con più di tre vicini vivi muore, come se rpopolazione
  4. Ogni cellula morta con esattamente tre vicini vivi diventa una cellula viva, come per riproduzione.

Panoramica:

  1. A prospettive diverse sul gioco della vita di Conway
  2. Regole non dette
  3. Spiegazione di metodi importanti (e strutture dati utilizzate)

Una prospettiva diversa sul gioco della vita di Conway

Immaginiamo prima il gioco della vita come una griglia (noi assumerà anche che questa griglia abbia coordinate tali che langolo in basso a sinistra è indicato come (0,0) e langolo in alto a destra è indicato come (n, n) dove n è un numero intero positivo). Questa griglia bidimensionale rappresenta un gruppo di n * n numero di celle. Ogni blocco della griglia può essere pensato come una cella, che non solo memorizza un valore booleano (morto o vivo) per descrivere lo stato della cella, ma descrive anche la sua posizione tramite le sue coordinate. Inoltre, lo stato corrente di tutte le cellule determina quali cellule moriranno, continueranno a vivere o nasceranno nella generazione successiva secondo le regole sopra riportate.

In una prospettiva diversa, tuttavia, il gioco di Conway della vita è molto simile al gioco dragamine. Possiamo pensare a una cella viva come a una miniera e ai suoi vicini che immagazzinano il numero di mine più vicine ad essa. In questo modo, siamo in grado di utilizzare facilmente le regole di cui sopra per determinare la generazione futura (in particolare quali cellule moriranno e quali nasceranno).

Che dire delle cellule attualmente vive potresti Chiedi? Ebbene, possiamo facilmente rappresentarli come un numero intero maggiore di 10, dove il posto di uno indica quanti vicini vivi ha la cella attualmente viva, e i posti di dieci indicano che la cella è viva.

Regole non dette

Unosservazione che mi è venuta in mente è che il gioco della vita si preoccupa solo delle cellule vive. Solo le cellule vive possono morire, le cellule che continuano a vivere devono già essere vive e le cellule possono nascere solo se hanno vicini vivi. Di conseguenza, controllare lintera griglia (complessità temporale: O (n ^ 2)) per determinare la futura generazione di celle sarebbe un completo spreco. Sarebbe molto più veloce se memorizzassi tutte le cellule attualmente vive e controllassi ogni cellula viva insieme a quelle vicine per determinare la generazione successiva (che è esattamente quello che ho fatto).

Spiegazione di metodi importanti (e strutture dati utilizzate)

birth (): itera su una HashMap contenente una coppia chiave-valore di tutte le cellule vive insieme ai suoi vicini. Se la coppia chiave-valore segue le regole del gioco della vita sopra, la chiave (un valore intero che rappresenta la posizione di una cella) viene quindi inserita in una pila che contiene la generazione successiva di celle vive. Dopo ogni iterazione, il valore della griglia viene reimpostato su 0 e la coppia chiave-valore viene rimossa da HashMap.

insertAlive (): apre lo stack e inserisce la cella attiva nella griglia. Linserimento di una cella viva segue la struttura di dragamine (i vicini di una cella viva verranno incrementati di 1 e la cella viva verrà incrementata di 10 per indicare che è viva). Tutti i vicini e le celle vive vengono quindi inseriti in una HashMap in modo che birth () possa funzionare correttamente

printBoard () (dovrebbe essere chiamato boardToString): usa un costruttore di stringhe per formattare la griglia in una stringa.

Nota : la maggior parte dei commenti sono stati eliminati perché non aggiungono molto alla leggibilità di il codice

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

Commenti

  • Hai ‘ fatto un punto per dire che questa è unimplementazione diversa, ma non cè nulla che spieghi la teoria alla base della tua implementazione o gli algoritmi e le formule relativamente oscuri che ‘ stai utilizzando.
  • Questo è un esercizio molto diffuso, quindi ti consiglio anche di dare unocchiata alle implementazioni che puoi trovare online: ‘ è molto istruttivo. Mi piace particolarmente questa sintesi che mostra unimplementazione reattiva in Java 8 (utilizzando RxJava) – non sa tuttavia, sarebbe un buon codice di produzione .

Risposta

Prima di tutto, penso che lalgoritmo sia abbastanza intelligente, il che, per la mia modesta esperienza, non è così comune per uno studente universitario. Quindi congratulazioni se lhai inventato da solo! Se “stai cercando implementazioni intelligenti, ti consiglierei quelle funzionali, ad esempio in Haskell ; vedi anche Gioco più breve della vita .

Ora, fai attenzione allintelligenza. Un buon codice dovrebbe essere facile da leggere , facile da capire . Questo ovviamente non è sempre possibile quando si ha a che fare con algoritmi complessi, ma credo che dovrebbe essere un obiettivo.

jjjjjjjjjjjj ha detto:
Nota: la maggior parte dei commenti è stata tolta perché non aggiungono molto alla leggibilità del codice

Lo scopo dei commenti è aiutare le persone a capire il tuo codice (in generale, concentrati sul ” why ” anziché su ” what “). Qui, per aiutare le persone a capire che dovevi aggiungere molto di testo al tuo post. Idealmente questo non è necessario perché il codice è:

  • auto-documentato,
  • commentato per chiarire cose complesse / implicite.

Ad esempio, ecco una rapida riscrittura del codice nel tentativo di renderlo più espressivo:

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

Ho principalmente rinominato alcune variabili / metodi e introdotto alcuni metodi di utilità. Il codice è un po più lungo e sembra più verboso ma IMHO è anche più facile da capire. È ancora molto procedurale (il che non è male di per sé, specialmente per un programma così semplice) ma potresti provare ad aggiungere più espressività introducendo nuove classi come Board o Cell. Troverai tali implementazioni OO su GitHub .

Il tuo codice potrebbe anche incorrere in problemi di memoria con schede di grandi dimensioni. In effetti, la tua variabile board[][] memorizza tutte le celle, anche quelle morte. Con una scheda 10000 x 10000 contenente solo ~ 5/6 celle sprecherai molta memoria. Una soluzione è utilizzare un sparse array (fondamentalmente, un set contenente solo celle vive).

Come nota a margine, alcuni anni fa ho anche provato a modellare un GoL altamente configurabile in un ” puro ” OO way; il mio codice è su GitHub se vuoi provarlo. Il metodo che calcola la prossima generazione di world è ImmutableGeneration :: nextGeneration ; dato un insieme di celle vive, fondamentalmente: 1) calcola tutte le celle vicine quindi 2) conserva solo quelle che saranno vive. Le regole che indicano se una cella sarà viva o morta sono implementate in Rule.java .


EDIT : personal opinione sulla concisione contro verbosità quando si tratta di n sto per rispondere a un commento

Prima di tutto, credo che non ci siano risposte giuste: si tratta solo di compromessi e preferenze personali. Dare un nome è difficile e troverai molti articoli sullargomento.

Ci sono solo due cose difficili in Informatica: linvalidazione della cache e lassegnazione di nomi
– Phil Karlton

La mia opinione è che la concettività è piacevole ma può portare ad ambiguità e lambiguità, soprattutto quella nascosta, è una minaccia.Il primo esempio che mi viene in mente è mescolare erroneamente le unità:

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

che detto, i nomi lunghi rendono il codice più difficile da leggere. Possono essere ridotti tenendo conto di due cose:

  • il contesto (ad es. Nome del metodo / classe / pacchetto che lo racchiude),
  • lambito (una variabile locale in un metodo a 3 righe può andare bene con un nome breve, mentre una funzione utilizzata più volte nellintera base di codice potrebbe averne bisogno di uno più lungo).

Questo è anche ciò che è consigliato da Convenzioni di denominazione di Google .

Come ultima nota, come hai suggerito i nomi molto lunghi possono essere visti come odori di codice. Di solito, il problema è mancanza di coesione (la classe / il metodo fa cose troppo diverse – ancora una volta, nessuna metrica chiara su questo “sta a sensazione dello sviluppatore). Ad esempio, nel codice che ho proposto possiamo pensare a populateWorldWithAliveCellsFromPreviousGeneration come a un metodo con responsabilità: 1) calcolare le cellule che saranno vive alla prossima generazione e 2) popolare il mondo. Potremmo quindi dividerlo in due: populateWorldWith(aliveCellsFromPreviousGeneration()).

Allo stesso modo potremmo raccogliere gli attributi il cui nome termina con ” atNextGeneration ” sotto una nuova Generation classe:

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

Ma suddividere la logica in troppe classi aumenterà anche la complessità dellarchitettura e renderà più difficile la comprensione del flusso.

Come un conclusione ti consiglio di tenere presente che qualsiasi parte di codice è suscettibile di essere modificata da sviluppatori che non hanno alcuna conoscenza precedente del progetto e che devono capire cosa fa il codice e perché lo fa in modo che possano mantenerlo o riutilizzare parti senza introdurre regressioni. Non ci sono silverbullet: solo compromessi e ciò che conta quando fai una scelta è che:

  • puoi identificare il compromesso,
  • comprendi i pro e i contro di ciascuna alternativa e scegline una consapevolmente.

(ma non esercitare troppa pressione su di te: BACIO e ricorda che il codice può essere modificato in seguito)

Commenti

  • Grazie mille per il tuo commento. Sono così contento che tu abbia commentato perché non avrei mai pensato di memorizzare solo le celle vive (questo cambia molto il mio codice, e secondo me lo rende molto migliore). Volevo chiederti un po della tua opinione sullequilibrio tra essere chiari con i nomi delle variabili ed essere concisi. In altre parole, come puoi determinare se il programma è troppo dettagliato? Ciò significa che devi impiegare un tempo straordinariamente lungo per creare i nomi delle variabili giusti o che cè qualcosa di difettoso nella tua logica e nella progettazione del codice? Grazie ancora.
  • Ho modificato la mia risposta per condividere la mia opinione in merito. ‘ un sacco di testo che sostanzialmente dice che ” non ci sono risposte giuste, ‘ si tratta di compromessi, quindi pensa ai pro e ai contro quando fai una scelta “.

Risposta

Ho solo un piccolo consiglio sulla leggibilità. Quando hai un metodo chiamato printBoard, normalmente ti aspetteresti che stampi la scheda . Un nome migliore per quel metodo sarebbe boardToString.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *