Właśnie zakończyłem wdrażanie wersji Conwaya Game of Życie przy użyciu Javy.

Będąc tylko studentem collegeu, jestem pewien, że mój kod nie jest nawet bliski ideału, i zastanawiałem się, czy możesz spojrzeć na mój kod. Co mogę ulepszyć? Czy są szybsze sposoby zaimplementować określone obszary mojego kodu? Czy istnieje nadmiar kodu, który mogę usunąć? Czy istnieje mądrzejszy sposób na zaimplementowanie gry Conway „Game of Life”?

EDYTUJ:

W nadziei na otrzymanie większej liczby opinii, oto teoria stojąca za moją implementacją:

Dla odniesienia, oto zasady gry w życie Conwaya (zaczerpnięte z wikipedii):

  1. Każda żywa komórka z mniej niż dwoma sąsiadami umiera, jak gdyby przez zbyt małą liczbę ludności.
  2. Każda żywa komórka z holownikiem lub trzema żywymi sąsiadami żyją dla następnego pokolenia.
  3. Każda żywa komórka z więcej niż trzema żywymi sąsiadami umiera, jak gdyby rpopulation
  4. Każda martwa komórka z dokładnie trzema żywymi sąsiadami staje się żywą komórką, jakby przez rozmnażanie.

Przegląd:

  1. A inne spojrzenie na Grę w życie Conwaya
  2. Zasady niewypowiedziane
  3. Wyjaśnienie ważnych metod (i zastosowanych struktur danych)

Inne spojrzenie na Grę w życie Conwaya

Najpierw wyobraźmy sobie Grę w życie jako siatkę niepokoju (my przyjmie również, że ta siatka ma takie współrzędne, że lewy dolny róg jest oznaczony jako (0,0), a prawy górny róg jest oznaczony jako (n, n), gdzie n jest dodatnią liczbą całkowitą). Ta dwuwymiarowa siatka reprezentuje grupę n * n liczby komórek. Każdy blok siatki można traktować jako komórkę, która nie tylko przechowuje wartość logiczną (martwa lub żywa) opisującą stan komórki, ale także określa jej lokalizację za pomocą współrzędnych. Ponadto aktualny stan wszystkich komórek decyduje o tym, które komórki umrą, będą nadal żyć lub narodzić się w następnym pokoleniu zgodnie z zasadami opisanymi powyżej.

Jednak z innej perspektywy gra Conwaya życia jest bardzo podobny do gry Saper. Możemy myśleć o żywej komórce jak o minie, a jej sąsiedzi przechowują liczbę min, które są najbliżej niej. W ten sposób jesteśmy w stanie łatwo wykorzystać powyższe reguły do określenia przyszłego pokolenia (szczególnie, które komórki umrą i które się narodzą).

A co z komórkami, które obecnie żyją, możesz zapytać? Cóż, możemy z łatwością przedstawić je jako liczbę całkowitą większą niż 10, gdzie miejsce jedności wskazuje, ilu żywych sąsiadów ma obecnie żywa komórka, a miejsca dziesiątki wskazują, że komórka żyje.

Zasady niewypowiedziane

Spostrzeżenie, które przyszło mi do głowy, to fakt, że gra życia dotyczy tylko żywych komórek. Tylko żywe komórki mogą umrzeć, komórki, które nadal żyją, muszą już żyć, a komórki mogą się rodzić tylko wtedy, gdy mają żywych sąsiadów. W rezultacie sprawdzenie całej siatki (złożoność czasowa: O (n ^ 2)) w celu określenia przyszłej generacji komórek byłoby całkowitym marnotrawstwem. Byłoby dużo szybciej, gdybym przechowywał wszystkie aktualnie żywe komórki i sprawdzał każdą żywą komórkę wraz z sąsiadami w celu określenia następnej generacji (co jest dokładnie tym, co zrobiłem).

Wyjaśnienie ważnych metod (i używanych struktur danych)

birth (): iteruje po HashMap zawierającej parę klucz-wartość wszystkie żywe komórki wraz z sąsiadami. Jeśli para klucz-wartość jest zgodna z powyższymi zasadami gry w życie, klucz (liczba całkowita, która reprezentuje położenie komórki) jest następnie umieszczana na stosie zawierającym następną generację żywych komórek. Po każdej iteracji wartość siatki jest resetowana do 0, a para klucz-wartość jest usuwana z mapy HashMap.

insertAlive (): zdejmuje stos i wstawia aktywną komórkę do siatki. Wstawienie żywej komórki jest zgodne ze strukturą trałowca (liczba sąsiadów żywej komórki zostanie zwiększona o 1, a żywa komórka zostanie zwiększona o 10, co oznacza, że żyje). Wszyscy sąsiedzi i żywe komórki są następnie umieszczane w HashMap, aby funkcja birth () działała poprawnie

printBoard () (powinno się nazywać boardToString): używa kompilatora ciągów do sformatowania siatki w łańcuch.

Uwaga : większość komentarzy została usunięta, ponieważ nie zwiększają czytelności kod

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

Komentarze

  • Zrobiłeś ' chodzi o to, aby powiedzieć, że jest to inna implementacja, ale nie ma nic, co wyjaśniałoby teorię Twojej implementacji lub stosunkowo niejasne algorytmy i formuły, których ' używasz.
  • Jest to popularne ćwiczenie, więc radzę również przyjrzeć się implementacjom, które można znaleźć w Internecie: jest ono ' bardzo pouczające. Szczególnie podoba mi się to streszczenie , które pokazuje reaktywną implementację w Javie 8 (używającej RxJava) – nie sa mimo wszystko byłby to dobry kod produkcyjny .

Odpowiedź

Pierwsza z wszystko, myślę, że algorytm jest dość inteligentny, co z mojego skromnego doświadczenia nie jest tak powszechne dla studentów. Gratulacje, jeśli wymyśliłeś to sam! Jeśli szukasz inteligentnych implementacji, polecam funkcjonalne, np. w Haskell ; zobacz też Najkrótsza gra życia .

Strzeż się sprytu. Dobry kod powinien być łatwy do odczytania , łatwe do zrozumienia . Oczywiście nie zawsze jest to możliwe w przypadku złożonych algorytmów, ale uważam, że powinno to być cel.

jjjjjjjjjjj powiedział:
Uwaga: większość komentarzy została usunięta, ponieważ nie dodają bardzo do czytelności kodu

Celem komentarzy jest pomoc ludziom w zrozumieniu kodu (ogólnie rzecz biorąc, skup się na ” dlaczego ” zamiast na ” what „). Tutaj, aby pomóc innym zrozumieć, musisz dodać dużo tekstu do swojego posta. W idealnym przypadku nie jest to potrzebne, ponieważ kod jest:

  • samodokumentowany,
  • skomentowany w celu wyjaśnienia złożonych / niejawnych rzeczy.

Na przykład, oto szybkie przepisanie kodu, aby uczynić go bardziej wyrazistym:

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

Przeważnie zmieniłem nazwy niektórych zmiennych / metod i wprowadziłem kilka metod narzędziowych. Kod jest nieco dłuższy i wydaje się bardziej gadatliwy, ale jest też łatwiejszy do zrozumienia w IMHO. Nadal jest bardzo proceduralny (co nie jest złe samo w sobie, szczególnie dla tak prostego programu), ale możesz spróbować dodać więcej ekspresji, wprowadzając nowe klasy, takie jak Board lub Cell. Takie implementacje OO” znajdziesz „ na GitHub .

Twój kod może również napotkać problemy z pamięcią w przypadku dużych płyt. Rzeczywiście, zmienna board[][] przechowuje wszystkie komórki, nawet martwe. W przypadku tablicy o wymiarach 10000 x 10000 zawierającej tylko ~ 5/6 komórek marnujesz dużo pamięci. Rozwiązaniem jest użycie rzadkiej tablicy (w zasadzie zestaw zawierający tylko żywe komórki).

Na marginesie, kilka lat temu próbowałem również modelować wysoce konfigurowalne GoL w ” czystym ” OO sposób; mój kod jest na GitHub , jeśli chcesz to sprawdzić. Metoda obliczająca następną generację świat to ImmutableGeneration :: nextGeneration ; biorąc pod uwagę zestaw żywych komórek, w zasadzie: 1) oblicz wszystkie sąsiednie komórki, a następnie 2) zachowaj tylko te, które będą żywe. Reguły wskazujące, czy komórka będzie żywa czy martwa, są zaimplementowane w Rule.java .


EDYTUJ : osobisty opinia na temat zwięzłości kontra gadatliwość, jeśli chodzi o n odpowiadając na komentarz

Przede wszystkim uważam, że nie ma dobrych odpowiedzi: chodzi o kompromisy i osobiste preferencje. Nazewnictwo jest trudne i „znajdziesz mnóstwo artykułów na ten temat.

W informatyce są tylko dwie trudne rzeczy: unieważnianie pamięci podręcznej i nazywanie rzeczy
– Phil Karlton

Uważam, że konkluzja jest przyjemna, ale może prowadzić do niejasności, a niejasność, zwłaszcza ukryta, jest zagrożeniem.Pierwszy przykład, jaki przychodzi mi do głowy, to pomyłkowe mieszanie jednostek:

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

To Mówi się, że długie nazwy sprawiają, że kod jest trudniejszy do odczytania. Można je zredukować biorąc pod uwagę dwie rzeczy:

  • kontekst (np. Nazwa otaczającej metody / klasy / pakietu),
  • zasięg (lokalna zmienna w metoda 3-liniowa może być dobra przy krótkiej nazwie, podczas gdy funkcja używana wielokrotnie w całej bazie kodu może wymagać dłuższego).

To również jest zalecane przez Konwencje nazewnictwa Google .

Na koniec, ponieważ zasugerowałeś, że bardzo długie nazwy mogą być postrzegane jako zapach kodu. Zwykle problemem jest brak spójności (klasa / metoda robi zbyt wiele różnych rzeczy – po raz kolejny nie ma jasnych wskaźników na ten temat, wszystko zależy od uczucie dewelopera). Na przykład w kodzie, który zaproponowałem, możemy pomyśleć o populateWorldWithAliveCellsFromPreviousGeneration jako metodzie odpowiedzialnej za: 1) obliczanie komórek, które będą żywe w następnym pokoleniu i 2) zaludnienie świata. Moglibyśmy więc podzielić go na dwie części: populateWorldWith(aliveCellsFromPreviousGeneration()).

W ten sam sposób możemy zebrać atrybuty, których nazwa kończy się na ” atNextGeneration ” pod nową klasą 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)); } ... }  

Jednak podzielenie logiki na zbyt wiele klas zwiększy również złożoność architektury i utrudni zrozumienie przepływu.

Podsumowując Radzę pamiętać, że każdy fragment kodu może zostać zmodyfikowany przez programistów, którzy nie mają wcześniejszej wiedzy na temat projektu i którzy muszą zrozumieć, co robi kod i dlaczego robi to, aby mogli go konserwować lub ponownie używać części bez wprowadzania regresji. Nie ma żadnej srebrnej kuli: są tylko kompromisy, a przy dokonywaniu wyboru liczy się to, że:

  • możesz określić kompromis,
  • rozumiesz wady i zalety każdą alternatywę i świadomie wybierz jedną z nich.

(ale nie naciskaj na siebie zbyt mocno: KISS i pamiętaj, że kod może być później refaktoryzowany)

Komentarze

  • Dziękuję bardzo za komentarz. Tak się cieszę, że skomentowałeś, ponieważ nigdy bym nie pomyślał o przechowywaniu tylko żywych komórek (to zmienia wiele w moim kodzie i moim zdaniem znacznie poprawia). Chciałem trochę zapytać o twoją opinię na temat równowagi między jasnością przy nazwach zmiennych a zwięzłością. Innymi słowy, w jaki sposób można określić, czy program jest zbyt szczegółowy? Czy to oznacza, że musisz spędzić niezwykle dużo czasu na tworzeniu odpowiednich nazw zmiennych, czy też jest coś nieprawidłowego w Twojej logice i projekcie kodu? Jeszcze raz dziękuję.
  • Zredagowałem odpowiedź, aby podzielić się opinią na jej temat. To ' bardzo dużo tekstu mówiącego w zasadzie, że ” nie ma dobrych odpowiedzi, to ' chodzi o kompromisy, więc rozważ wady i zalety przy dokonywaniu wyboru „.

Odpowiedź

Mam tylko jedną małą rekomendację dotyczącą czytelności. Gdy masz metodę o nazwie printBoard, normalnie oczekuje się, że wydrukuje tablicę . Lepsza nazwa dla tej metody to boardToString.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *