Je viens de terminer l’implémentation d’une version de Conway « s Game of La vie avec Java.
Nétant quun étudiant à luniversité, je suis sûr que mon code est loin dêtre parfait, et je me demandais si vous pouviez regarder mon code. Que puis-je améliorer? Existe-t-il des moyens plus rapides pour implémenter certaines zones de mon code? Y a-t-il un excès de code que je peux supprimer? Existe-t-il un moyen plus intelligent de mettre en œuvre le jeu de la vie de Conway?
EDIT:
Dans lespoir de recevoir plus de commentaires, voici la théorie derrière mon implémentation:
Pour référence, voici la règles pour le jeu de la vie de Conway (tiré de wikipedia):
- Toute cellule vivante avec moins de deux voisins vivants meurt, comme par sous-population.
- Toute cellule vivante avec remorquage ou trois voisins vivants vivent sur la prochaine génération.
- Toute cellule vivante avec plus de trois voisins vivants meurt, comme si par ove rpopulation
- Toute cellule morte avec exactement trois voisins vivants devient une cellule vivante, comme par reproduction.
Vue d’ensemble:
- A perspectives différentes sur le jeu de la vie de Conway
- Règles tacites
- Explication des méthodes importantes (et des structures de données utilisées)
Une autre perspective sur le jeu de la vie de Conway
Imaginons dabord le jeu de la vie comme une grille anxn (nous supposera également que cette grille a des coordonnées telles que le coin inférieur gauche est noté (0,0) et le coin supérieur droit est noté (n, n) où n est un entier positif). Cette grille bidimensionnelle représente un groupe de n * n nombre de cellules. Chaque bloc de grille peut être considéré comme une cellule, qui stocke non seulement une valeur booléenne (morte ou vivante) pour décrire létat de la cellule, mais détaille également son emplacement via ses coordonnées. De plus, létat actuel de toutes les cellules détermine quelles cellules mourront, continueront de vivre ou naîtront dans la prochaine génération conformément aux règles ci-dessus.
Dans une perspective différente, cependant, le jeu de Conway de la vie est très similaire au jeu dragueur de mines. On peut considérer une cellule vivante comme une mine, et ses voisins stockant le nombre de mines qui en sont les plus proches. De cette façon, nous pouvons facilement utiliser les règles ci-dessus pour déterminer la génération future (en particulier quelles cellules mourront et quelles cellules naîtront).
Quen est-il des cellules actuellement en vie? interroger? Eh bien, nous pouvons facilement les représenter comme un entier supérieur à 10, où la place de un indique le nombre de voisins vivants de la cellule actuellement vivante, et les dix places indiquent que la cellule est vivante.
Règles non dites
Une observation qui mest venue à lesprit est que le jeu de la vie ne se préoccupe que des cellules vivantes. Seules les cellules vivantes peuvent mourir, les cellules qui continuent à vivre doivent déjà être vivantes et les cellules ne peuvent naître que si elles ont des voisins vivants. En conséquence, vérifier la totalité de la grille (complexité temporelle: O (n ^ 2)) pour déterminer la future génération de cellules serait un gaspillage complet. Ce serait beaucoup plus rapide si je stockais toutes les cellules actuellement vivantes et vérifiais chaque cellule vivante avec leurs voisines pour déterminer la génération suivante (ce que jai fait exactement).
Explication des méthodes importantes (et des structures de données utilisées)
birth (): itère sur un HashMap contenant une paire clé-valeur de toutes les cellules vivantes avec ses voisins. Si la paire clé-valeur suit le jeu des règles de la vie ci-dessus, la clé (une valeur entière qui représente lemplacement dune cellule) est ensuite poussée sur une pile qui contient la prochaine génération de cellules vivantes. Après chaque itération, la valeur de la grille est remise à 0 et la paire clé-valeur est supprimée du HashMap.
insertAlive (): fait apparaître la pile et insère la cellule vivante dans la grille. Linsertion dune cellule vivante suit la structure du démineur (les voisins dune cellule vivante seront incrémentés de 1 et la cellule vivante sera incrémentée de 10 pour indiquer quelle est vivante). Tous les voisins et les cellules vivantes sont ensuite mis dans un HashMap afin que birth () puisse fonctionner correctement
printBoard () (devrait être nommé boardToString): utilise un constructeur de chaînes pour formater la grille en une chaîne.
Remarque : la plupart des commentaires ont été supprimés car ils najoutent pas grand-chose à la lisibilité de le code
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"); } } }
Commentaires
- Vous ‘ avez fait un point de dire que cest une implémentation différente, mais il ny a rien pour expliquer la théorie derrière votre implémentation ou les algorithmes et formules relativement obscurs que vous ‘ utilisez.
- Cet exercice est populaire, je vous conseille donc également de jeter un œil aux implémentations que vous pouvez trouver en ligne: il ‘ est très instructif. Jaime particulièrement cet élément qui montre une implémentation réactive dans Java 8 (en utilisant RxJava) – pas sa ying ce serait un bon code de production cependant.
Réponse
Premier de tout, je pense que lalgorithme est assez intelligent, ce qui nest, pour mon humble expérience, pas si courant pour un étudiant. Alors félicitations si vous lavez inventé par vous-même! Si vous « recherchez des implémentations intelligentes, je recommanderais des implémentations fonctionnelles, par exemple dans Haskell ; voir aussi Jeu le plus court de la vie .
Maintenant, méfiez-vous de lintelligence. Un bon code doit être facile à lire , facile à comprendre . Ce nest bien sûr pas toujours possible lorsquil sagit dalgorithmes complexes, mais je pense que cela devrait être une cible.
jjjjjjjjjjjj a dit:
Remarque: la plupart des commentaires ont été supprimés car ils ne sajoutent pas beaucoup à la lisibilité du code
Le but des commentaires est daider les gens à comprendre votre code (en général, concentrez-vous sur le » pourquoi » plutôt que sur » quoi « ). Ici, pour aider les gens à comprendre, vous deviez ajouter beaucoup de texte à votre message. Idéalement, ce nest pas nécessaire car le code est:
- auto-documenté,
- commenté pour effacer les éléments complexes / implicites.
Par exemple, voici une réécriture rapide de votre code pour tenter de rendre le code plus expressif:
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(); } }
Jai principalement renommé certaines variables / méthodes et introduit des méthodes utilitaires. Le code est un peu plus long et semble plus verbeux mais à mon humble avis aussi plus facile à comprendre. Il est toujours très procédural (ce qui nest pas mauvais en soi, surtout pour un programme aussi simple) mais vous pouvez essayer dajouter plus dexpressivité en introduisant de nouvelles classes telles que Board
ou Cell
. Vous trouverez de telles implémentations OO sur GitHub .
Votre code peut également rencontrer des problèmes de mémoire avec de grandes cartes. En effet, votre variable board[][]
stocke toutes les cellules, même mortes. Avec une carte de 10000 x 10000 ne contenant que ~ 5/6 cellules, vous gaspillerez beaucoup de mémoire. Une solution consiste à utiliser un tableau épars (en gros, un ensemble contenant uniquement des cellules vivantes).
En guise de remarque, il y a quelques années, jai également essayé de modéliser un GoL hautement configurable dans un » pur » OO way; mon code est sur GitHub si vous voulez le vérifier. La méthode de calcul de la prochaine génération du world is ImmutableGeneration :: nextGeneration ; étant donné un ensemble de cellules vivantes, il: 1) calcule toutes les cellules voisines puis 2) ne garde que celles qui seront vivantes. Les règles indiquant si une cellule sera vivante ou morte sont implémentées dans Rule.java .
EDIT : personnel opinion sur la concision versus la verbosité quand il sagit de n je suis prêt à répondre à un commentaire
Tout d’abord, je pense qu’il n’ya pas de bonnes réponses: tout est question de compromis et de préférences personnelles. Le nommage est difficile et vous trouverez de nombreux articles sur le sujet.
Il ny a que deux choses difficiles en informatique: linvalidation du cache et la dénomination des choses
– Phil Karlton
Je pense que la concision est agréable mais peut conduire à des ambiguïtés. Et lambiguïté, surtout cachée, est une menace.Le premier exemple qui me vient à lesprit est le mélange dunités par erreur:
// Everything looks good... double pathLength = distanceFromGoal + distanceToTarget; // ... but adding units makes easy to spot bugs double pathLengthInKilometers = distanceFromGoalInMeters + distanceToTargetInMillimeters;
Que étant dit, les noms longs rendent le code plus difficile à lire. Ils peuvent être réduits en tenant compte de deux choses:
- le contexte (par exemple le nom de la méthode / classe / package englobante),
- la portée (une variable locale dans une méthode de 3 lignes peut convenir avec un nom court alors quune fonction utilisée plusieurs fois dans toute la base de code peut en nécessiter une plus longue).
Cest aussi ce que conseille Conventions de dénomination de Google .
Pour terminer, comme vous lavez suggéré, les noms très longs peuvent être considérés comme des odeurs de code. Habituellement, le problème est un manque de cohésion (la classe / méthode fait trop de choses différentes – encore une fois, pas de métrique claire à ce sujet, cest à sentiment du développeur). Par exemple, dans le code que jai proposé, nous pouvons considérer populateWorldWithAliveCellsFromPreviousGeneration
comme une méthode qui assume des responsabilités: 1) calculer les cellules qui seront vivantes à la prochaine génération et 2) peupler le monde. Nous pourrions ainsi le scinder en deux: populateWorldWith(aliveCellsFromPreviousGeneration())
.
De la même manière nous pourrions rassembler les attributs dont le nom se termine par » atNextGeneration » sous une nouvelle classe 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)); } ... }
Mais diviser la logique en trop de classes augmentera également la complexité de larchitecture et rendra plus difficile la compréhension du flux.
En tant que conclusion Je vous conseille de garder à lesprit que tout morceau de code est susceptible dêtre modifié par des développeurs nayant aucune connaissance préalable du projet et qui doivent comprendre ce que fait le code et pourquoi il le fait pour quils puissent le maintenir ou réutiliser des pièces sans introduire de régressions. Il ny a pas de solution miracle: seulement des compromis, et ce qui compte lorsque vous faites un choix, cest que:
- vous pouvez identifier le compromis,
- vous comprenez les avantages et les inconvénients de chaque alternative et choisissez-en une en connaissance de cause.
(mais ne vous mettez pas trop de pression: KISS et rappelez-vous que le code peut être remanié par la suite)
Commentaires
- Merci beaucoup pour votre commentaire. Je suis tellement content que vous ayez commenté car je naurais jamais pensé à ne stocker que les cellules vivantes (cela change beaucoup de mon code et, à mon avis, le rend beaucoup mieux). Je voulais vous demander un peu votre opinion sur léquilibre entre être clair avec des noms variables et être concis. En dautres termes, comment pouvez-vous déterminer si le programme est trop détaillé? Cela signifie-t-il que vous devez passer un temps extraordinairement long à créer les bons noms de variables ou quil y a quelque chose de défectueux dans votre logique et la conception du code? Merci encore.
- Jai modifié ma réponse pour partager mon opinion à ce sujet. ‘ est beaucoup de texte disant essentiellement que » il ny a pas de bonnes réponses, ‘ Tout est question de compromis, alors pensez aux avantages et aux inconvénients lorsque vous faites un choix « .
Réponse
Jai juste une petite recommandation concernant la lisibilité. Lorsque vous disposez dune méthode appelée printBoard
, vous vous attendez normalement à ce quelle imprime le tableau . Un meilleur nom pour cette méthode serait boardToString
.