Most fejeztem be a Conway játékának egyik verzióját Élet a Java használatával.

Mivel csak főiskolai hallgató vagyok, biztos vagyok benne, hogy a kódom nincs tökéletesen tökéletesen, és arra gondoltam, hogy megnézheted-e a kódomat. Mit javíthatok? Vannak gyorsabb módszerek végrehajtani a kódom bizonyos területeit? Van-e olyan kód, amelyet levághatok? Van-e intelligensebb módszer a Conway életjátékának megvalósítására?

SZERKESZTÉS:

A további visszajelzések reményében íme a megvalósításom mögött meghúzódó elmélet:

Hivatkozásként íme a a Conway életjátékának szabályai (átvették a wikipédiából):

  1. Bármely élő sejt, amelynek kevesebb, mint két élő szomszédja van, meghal, mintha alulnépesülnének.
  2. Bármely élő sejt vontatással vagy három élő szomszéd él tovább a következő generáció számára.
  3. Bármely élő sejt, amelynél háromnál több élő szomszéd van, úgy hal meg, mintha ove rpopulation
  4. Bármely, pontosan három élő szomszéddal rendelkező elhalt sejt élő szaporodássá válik élő sejtdé.

Áttekintés:

  1. A a Conway életjátékának eltérő szemlélete
  2. Kimondatlan szabályok
  3. A fontos módszerek (és a felhasznált adatszerkezetek) magyarázata

Más szemlélet a Conway életjátékában

Először képzeljük el az Élet játékát szorongó rácsként (mi azt is feltételezi, hogy ennek a rácsnak olyan koordinátái vannak, hogy a bal alsó sarkot (0,0), a jobb felső sarkot (n, n) jelöljük, ahol n pozitív egész szám). Ez a kétdimenziós rács n * n sejtek csoportját képviseli. Minden rácsblokk sejtnek tekinthető, amely nemcsak egy Boole-értéket (halott vagy életben) tárol a cella állapotának leírására, hanem koordinátáin keresztül részletezi a helyét is. Ezenkívül az összes sejt jelenlegi állapota meghatározza, hogy mely sejtek fognak meghalni, tovább élni vagy születni a következő generációban a fenti szabályok szerint.

Más perspektívában azonban Conway játéka az élet nagyon hasonlít a vadak aknavetőjéhez. Gondolhatunk egy élő sejtre, mint egy aknára, és szomszédaival, tárolva a hozzá legközelebb álló aknák számát. Ily módon könnyen használhatjuk a fenti szabályokat a jövő generációjának meghatározásához (különösen mely sejtek fognak elpusztulni és mely sejtek fognak születni).

Mi a helyzet a jelenleg élő sejtekkel? kérdez? Nos, ezeket könnyedén 10-nél nagyobb egész számként ábrázolhatjuk, ahol az egyik helye azt jelzi, hogy hány élő szomszédja van a jelenleg élő sejtnek, és a tíz hely azt jelzi, hogy a sejt él.

Kimondatlan szabályok

Az egyik észrevétel felmerült bennem, hogy a játék az élet csak az élő sejtek miatt aggódik. Csak az élő sejtek halhatnak meg, a tovább élő sejteknek már élniük kell, és a sejtek csak akkor születhetnek, ha vannak szomszédaik, akik élnek. Ennek eredményeként a teljes rács (időbonyolultság: O (n ^ 2)) ellenőrzése a sejtek jövőbeli generációjának meghatározása érdekében teljes pazarlás lenne. Sokkal gyorsabb lenne, ha tárolnám az összes jelenleg élő sejtet, és minden egyes élő sejtet a szomszédaikkal együtt ellenőriznék, hogy meghatározzam a következő generációt (pontosan ezt tettem).

Fontos módszerek (és felhasznált adatstruktúrák) magyarázata

birth (): egy HashMapon keresztül ismétlődik, amely kulcs-érték pár az összes élő sejt a szomszédaival együtt. Ha a kulcs-érték pár az élet fenti szabályainak játékát követi, akkor a kulcsot (egy egész számot, amely egy cella helyét képvisel) egy olyan veremre tolják, amely az élő sejtek következő generációját tartalmazza. Minden iteráció után a rács értéke 0-ra áll vissza, és a kulcs-érték pár eltávolításra kerül a HashMap-ból.

insertAlive (): beugrik a verembe, és beilleszti az élő cellát a rácsba. Élő sejt behelyezése követi az aknavető szerkezetét (az élő sejt szomszédait 1-gyel, az élő sejtet pedig 10-gyel növeljük, hogy jelezzük, hogy életben van). Ezután az összes szomszédot és élő cellát egy HashMap-ba helyezzük, hogy a birth () megfelelően fusson.

printBoard () (boardToString nevet kell megadni): egy stringbuilder segítségével formázza a rácsot stringekké.

Megjegyzés : A legtöbb megjegyzést kivettük, mert nem sokat tesznek hozzá az olvashatósághoz a kód

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

Megjegyzések

  • Ön ‘ mondhatni, hogy ez egy másik megvalósítás, de semmi sem magyarázza a megvalósítás mögött álló elméletet, vagy a viszonylag homályos algoritmusokat és képleteket, amelyeket ‘ használ.
  • Ez egy népszerű gyakorlat, ezért azt is tanácsolom, hogy vessen egy pillantást az online megoldásokra: ‘ nagyon tanulságos. Különösen szeretem a ez a lényeg , amely reaktív megvalósítást mutat a Java 8-ban (az RxJava használatával) – nem sa mégis jó produkciós kód lenne.

Válasz

Először az algoritmus szerintem elég okos, ami szerény tapasztalataim szerint nem annyira gyakori egy egyetemista számára. Szóval gratulálok, ha egyedül találta ki! Ha intelligens megvalósításokat keres, akkor funkcionálisakat ajánlanék, például a Haskellben ; lásd még: Legrövidebb játék az élet .

Most óvakodjon az okosságtól. A jó kódnak könnyen olvashatónak kell lennie , könnyen érthető . Ez természetesen nem mindig lehetséges komplex algoritmus esetén, de úgy gondolom, hogy egy cél.

jjjjjjjjjjjjjj mondta:
Megjegyzés: A legtöbb megjegyzést kivettük, mert nem adják hozzá sok a kód olvashatóságához

A megjegyzések lényege, hogy segítsék az embereket a kód megértésében (általában a ” miért “, nem pedig a ” mi “). Ahhoz, hogy segítsen az embereknek megérteni, sok szöveget kellett hozzáadnia a bejegyzéséhez. Ideális esetben erre nincs szükség, mert a kód:

  • öndokumentált,
  • kommentálva van az összetett / implicit dolgok tisztázása érdekében.

Itt található például a kód gyors átírása a kód kifejezőbbé tétele érdekében:

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

Többnyire átneveztem néhány változót / metódust, és bevezettem néhány segédprogramot. A kód egy kicsit hosszabb, és jobban érzi magát bőbeszédű, de az IMHO is könnyebben érthető. Még mindig nagyon eljárási jellegű (ami önmagában nem rossz, főleg egy ilyen egyszerű program esetében), de érdemes megpróbálnia kifejezőbbé tenni új osztályok bevezetésével, például Board vagy Cell. Megtalálja az ilyen OO megvalósításokat a GitHubon .

A kód nagy memóriakártyákkal is memóriaproblémákba ütközhet. Valójában a board[][] változója tárolja az összes cellát, még az elhaltakat is. A csak ~ 5/6 cellát tartalmazó 10000 x 10000 táblával sok memóriát pazarolhat. Megoldás lehet egy ritka tömb használata (alapvetően egy csak élő sejteket tartalmazó készlet).

Megjegyzésként néhány évvel ezelőtt megpróbáltam egy nagyon konfigurálható GoL modellezését egy ” pure ” OO módon; a kódom a GitHubon van , ha meg akarja nézni. A módszer a következő generáció kiszámítására szolgál A világ ImmutableGeneration :: nextGeneration ; egy élő sejtkészletet figyelembe véve alapvetően: 1) kiszámítja az összes szomszéd sejtet, majd 2) csak azokat tartja életben, amelyek életben maradnak. A Rule.java alkalmazásban a cella életben vagy holtban történő életre hívására vonatkozó szabályok vannak érvényben.


SZERKESZTÉS : személyes vélemény a tömörségről a versengésről, ha n válaszolok egy megjegyzésre

Először is úgy gondolom, hogy nincsenek helyes válaszok: minden a kompromisszumokról és a személyes preferenciákról szól. A névadás nehéz, és rengeteg cikket talál a témában.

A Computer Science-ben csak két nehéz dolog van: a gyorsítótár érvénytelenítése és a dolgok elnevezése
– Phil Karlton

Véleményem szerint az összefogás kellemes, de kétértelműségekhez vezethet. És a kétértelműség, különösen a rejtett, fenyegetést jelent.Az első példa, amely eszembe jut, az egységek tévedése:

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

Ez a hosszú nevek megnehezítik a kód olvasását. Két dolog figyelembe vételével csökkenthetők:

  • a kontextus (pl. A csatoló módszer / osztály / csomag neve),
  • a hatókör (helyi változó a egy háromsoros módszer jól használható rövid névvel, míg az egész kódbázisban többször használt függvénynek hosszabbra lehet szüksége.

Ezt is javasolja a A Google elnevezési szokásai .

Utolsó megjegyzésként, ahogy Ön javasolta, a nagyon hosszú nevek kódszagnak tekinthetők. Általában a probléma a kohézió hiánya (az osztály / módszer túl sok mindent megtesz – még egyszer: nincsenek egyértelmű mérőszámok erről, legfeljebb fejlesztő érzése). Például az általam javasolt kódban úgy gondolhatjuk, hogy a populateWorldWithAliveCellsFromPreviousGeneration olyan módszer, amely felelősséget visel: 1) a következő generációnál életben lévő sejtek kiszámítása és 2) a világ népesítése. Így ketté oszthatnánk: populateWorldWith(aliveCellsFromPreviousGeneration()).

Ugyanígy összegyűjthetnénk azokat az attribútumokat is, amelyek neve ” atNextGeneration ” egy új Generation osztály alatt:

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

De a logika túl sok osztályra bontása növeli az architektúra összetettségét és megnehezíti a folyamat megértését.

Mint Következtetés Azt tanácsolom, ne feledje, hogy bármelyik kódrészletet módosíthatják azok a fejlesztők, akiknek nincsenek előzetes ismereteik a projektről, és akiknek meg kell érteniük, mit csinál a kód, és miért azért teszi, hogy fenntarthassák vagy újrafelhasználhassák az alkatrészeket anélkül, hogy regressziókat vezetnének be. Nincs ezüstbetét: csak kompromisszumok, és a választáskor az számít:

  • azonosíthatja a kompromisszumot,
  • megérti az előnyeit és hátrányait mindegyik alternatívát, és tudatosan válasszon egyet közülük.

(de ne gyakoroljon túl nagy nyomást rád: KISS és ne feledje, hogy a kódot ezután újra lehet alakítani)

Megjegyzések

  • Nagyon köszönöm a megjegyzését. Nagyon örülök, hogy megjegyzést fűztél, mert soha nem gondoltam volna, hogy csak az élő cellákat tárolom (ez sokat változtat a kódomon, és véleményem szerint sokkal jobbá teszi). Szeretnék egy kicsit megkérdezni a véleményedről a változó nevekkel való egyértelműség és a tömörség közötti egyensúlyról. Más szavakkal, hogyan lehet meghatározni, ha a program túl bőbeszédű? Ez azt jelenti, hogy rendkívül hosszú időt kell töltenie a megfelelő változónevek létrehozásával, vagy hogy valami hibás a logikájában és a kód kialakításában? Még egyszer köszönöm.
  • Szerkesztettem a válaszomat, hogy megosszam róla a véleményemet. A ‘ sok szöveg alapvetően azt mondja, hogy ” nincsenek helyes válaszok, ez ‘ mind a kompromisszumokról szól, ezért gondoljon az előnyökre és hátrányokra a választáskor “.

Válasz

Csak egy kis javaslatom van az olvashatóságra vonatkozóan. Ha van egy printBoard nevű módszer, akkor általában azt várja, hogy kinyomtassa a táblát . A módszer jobb neve boardToString lenne.

Vélemény, hozzászólás?

Az email címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük