Olen juuri suorittanut version Conwayn Game of Elämä Java-käyttöjärjestelmällä.

Koska olen vain opiskelija, olen varma, että koodini ei ole missään lähellä täydellistä, ja mietin, voisitko tarkastella koodiani. Mitä voin parantaa? Onko olemassa nopeampi tapoja käyttääkö koodin tiettyjä alueita? Onko ylimääräistä koodia, jonka voin leikata pois? Onko olemassa älykkäämpi tapa toteuttaa Conwayn elämänpeli?

MUOKKAA:

Toivottavasti saan lisää palautetta, tässä on toteutukseni taustalla oleva teoria:

Tässä on viitteenä säännöt Conwayn elämänpelistä (otettu wikipediasta):

  1. Jokainen elävä solu, jossa on vähemmän kuin kaksi elävää naapuria, kuolee ikään kuin väestön alle.
  2. Mikä tahansa elävä solu jossa on hinaus tai kolme elävää naapuria elää seuraavalle sukupolvelle.
  3. Jokainen elävä solu, jossa on yli kolme elävää naapuria, kuolee ikään kuin ove populaatio
  4. Kaikista kuolleista soluista, joissa on täsmälleen kolme elävää naapuria, tulee elävä solu ikään kuin lisääntymisen avulla.

Yleiskatsaus:

  1. A erilaiset näkymät Conwayn elämänpelistä
  2. Sanomattomat säännöt
  3. Selitys tärkeistä menetelmistä (ja käytetyistä tietorakenteista)

Eri näkemykset Conwayn elämänpelistä

Kuvitellaan ensin elämän peli ahdistuneena ristikkona (me olettaa myös, että tällä ruudukolla on koordinaatit siten, että vasenta alakulmaa merkitään (0,0) ja oikeaa ylänurkkaa (n, n), jossa n on positiivinen kokonaisluku). Tämä 2-ulotteinen ruudukko edustaa ryhmää n * n solumäärää. Jokaista ruudukkolohkoa voidaan pitää soluna, joka paitsi tallentaa Boolen arvon (kuollut tai elävä) kuvaamaan solun tilaa, mutta myös yksityiskohtaisesti sen sijainnin koordinaattiensa kautta. Lisäksi kaikkien solujen nykytila määrää, mitkä solut kuolevat, jatkavat elämistä tai syntyvät seuraavalle sukupolvelle yllä olevien sääntöjen mukaisesti.

Eri näkökulmasta Conwayn peli elämä on hyvin samanlainen kuin riistan miinanraivaaja. Voimme ajatella elävää solua kaivokseksi, ja sen naapurit varastoivat siihen lähinnä olevien miinojen määrän. Tällä tavalla voimme helposti käyttää yllä olevia sääntöjä tulevan sukupolven määrittämiseen (erityisesti mitkä solut kuolevat ja mitkä solut syntyvät).

Entä solut, jotka ovat tällä hetkellä elossa, saatat ehkä kysyä? No, voimme helposti esittää nämä kokonaislukuna, joka on suurempi kuin 10, missä paikan ilmaisee, kuinka monta elävää naapuria tällä hetkellä elävällä solulla on, ja kymmenen paikkaa osoittaa, että solu on elossa.

Sanomattomat säännöt

Yksi huomioni on, että peli elämän on huolissaan vain elävistä soluista. Ainoastaan elävät solut voivat kuolla, solujen, jotka elävät edelleen, täytyy olla jo eläviä, ja solut voivat syntyä vain, jos heillä on eläviä naapureita. Tämän seurauksena koko ruudukon (aikakompleksisuus: O (n ^ 2)) tarkistaminen solujen tulevan sukupolven määrittämiseksi olisi täydellinen tuhlausta. Olisi paljon nopeampi, jos tallentaisin kaikki tällä hetkellä elävät solut ja tarkistaisin jokaisen elävän solun yhdessä naapureidensa kanssa seuraavan sukupolven määrittämiseksi (juuri sen tein).

Selitys tärkeistä menetelmistä (ja käytetyistä tietorakenteista)

syntymä (): iteroi HashMapissa, joka sisältää avain-arvo-parin kaikki elävät solut naapureidensa kanssa. Jos avain-arvo-pari noudattaa yllä olevia elämän sääntöjä, avain (kokonaislukuarvo, joka edustaa solun sijaintia) työnnetään pinoon, joka sisältää seuraavan sukupolven eläviä soluja. Jokaisen iteraation jälkeen ruudukon arvo palautetaan nollaksi ja avain-arvo-pari poistetaan HashMapista.

insertAlive (): ponnahtaa pinon ja lisää elävän solun ruudukkoon. Elävän solun sijoittaminen seuraa miinanraivaajan rakennetta (elävän solun naapureita lisätään yhdellä ja elävää solua lisätään 10: llä, mikä tarkoittaa, että se on elossa). Kaikki naapurit ja elävät solut laitetaan sitten HashMapiin, jotta syntymä () voi toimia kunnolla.

printBoard () (pitäisi olla nimeltään boardToString): muotoilee ruudukon merkkijonoksi merkkijononrakentajalla.

Huomaa : Suurin osa kommenteista on otettu pois, koska ne eivät lisää paljon niiden luettavuutta koodi

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

Kommentit

  • Olet ’ tehnyt sanomalla, että tämä on erilainen toteutus, mutta mikään ei selitä toteutuksen taustalla olevaa teoriaa tai suhteellisen hämäriä algoritmeja ja kaavoja, joita ’ käytät.
  • Tämä on suosittu harjoitus, joten suosittelen myös, että katsot netissä olevia toteutuksia: se ’ on erittäin opettavainen. Pidän erityisesti aiheesta tämä ydin , joka näyttää reaktiivisen toteutuksen Java 8: ssa (käyttäen RxJavaa) – ei sa se olisi kuitenkin hyvä tuotanto -koodi.

Vastaa

Ensimmäinen kaikki, mielestäni algoritmi on melko älykäs, mikä ei nöyrän kokemukseni mukaan ole niin yleistä korkeakouluopiskelijoille. Joten onnittelut, jos keksit sen itse! Jos etsit älykkäitä toteutuksia, suosittelen toiminnallisia, esim. Haskellissa ; katso myös Lyhin peli elämän .

Varo nyt älykkyyttä. Hyvän koodin tulisi olla helppo lukea , helppo ymmärtää . Tämä ei tietenkään ole aina mahdollista käsiteltäessä monimutkaista algoritmia, mutta mielestäni sen pitäisi olla kohde.

jjjjjjjjjjjjj sanoi:
Huomaa: Suurin osa kommenteista on otettu pois, koska niitä ei lisätä paljon koodin luettavuudesta

Kommenttien tarkoituksena on auttaa ihmisiä ymmärtämään koodiasi (keskitytään yleensä ” miksi ” eikä ” mitä ”). Tässä sinun on lisättävä viestiisi paljon tekstiä auttaaksesi ihmisiä ymmärtämään. Ihannetapauksessa tätä ei tarvita, koska koodi on:

  • itse dokumentoitu,
  • kommentoitu monimutkaisten / epäsuorien asioiden selvittämiseksi.

Esimerkiksi tässä on koodin nopea uudelleenkirjoittaminen yrittää tehdä koodista ilmeisempi:

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

Nimetin enimmäkseen joitain muuttujia / menetelmiä ja otin käyttöön joitain apuohjelmatapoja. Koodi on vähän pidempi ja tuntuu enemmän sanallinen, mutta onko IMHO myös helpommin ymmärrettävissä. Se on silti hyvin menettelyllinen (mikä ei sinänsä ole huono, etenkin tällaisen yksinkertaisen ohjelman kohdalla), mutta voit yrittää lisätä ilmeikkyyttä lisäämällä uusia luokkia kuten Board tai Cell. Löydät tällaisia OO-toteutuksia GitHubista .

Koodissasi voi olla myös muistiongelmia isojen levyjen kanssa. Itse asiassa board[][] -muuttuja tallentaa kaikki solut, myös kuolleet. 10000 x 10000 -levyllä, joka sisältää vain ~ 5/6 solua, ”tuhlataan paljon muistia. Ratkaisu on käyttää harmaata taulukkoa (pohjimmiltaan sisältää vain eläviä soluja).

Lisähuomautuksena yritin muutama vuosi sitten myös mallintaa erittäin konfiguroitavan GoL: n ” pure ” OO-tapa; koodini on GitHubissa , jos haluat tarkistaa sen. Menetelmä, jolla lasketaan seuraavan sukupolven maailma on ImmutableGeneration :: nextGeneration ; kun otetaan huomioon joukko eläviä soluja, se pohjimmiltaan: 1) laskee kaikki naapurisolut ja sitten 2) pitää vain elossa olevat. Säännöt, jotka osoittavat, onko solu elossa vai kuollut, on otettu käyttöön Rule.java -sovelluksessa.


MUOKKAA : henkilökohtainen mielipide ytimekkyydestä ja sanattomuudesta, kun on kyse n: stä vastaan kommenttiin

Ensinnäkin uskon, ettei ole oikeita vastauksia: kaikki koskee kompromisseja ja henkilökohtaisia mieltymyksiä. Nimeäminen on vaikeaa ja löydät paljon artikkeleita aiheesta.

Tietojenkäsittelytieteessä on vain kaksi vaikeaa asiaa: välimuistin mitätöinti ja asioiden nimeäminen
– Phil Karlton

Minun mielestäni ytimekkyys on miellyttävä, mutta voi johtaa epäselvyyksiin. Ja epäselvyys, erityisesti piilotettu, on uhka.Ensimmäinen mielestäni tuleva esimerkki on virheellinen yksiköiden sekoittaminen:

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

Se sanotaan, pitkät nimet tekevät koodista vaikeampaa lukea. Niitä voidaan vähentää ottamalla huomioon kaksi asiaa:

  • konteksti (esim. Liitettävän menetelmän / luokan / paketin nimi),
  • laajuus (paikallinen muuttuja 3-rivinen menetelmä voi olla hieno lyhyellä nimellä, kun taas toiminto, jota käytetään useita kertoja koko kooditietokannassa, saattaa tarvita pidemmän).

Se on myös se, mitä Googlen nimeämiskäytännöt .

Viimeisenä huomautuksena, kuten ehdotit, hyvin pitkiä nimiä voidaan pitää koodihajuina. Yleensä ongelmana on yhteenkuuluvuuden puute (luokka / menetelmä tekee liikaa erilaisia asioita – jälleen kerran, ei selkeitä mittareita tästä, se on jopa kehittäjän tunne). Esimerkiksi ehdottamassani koodissa voimme ajatella populateWorldWithAliveCellsFromPreviousGeneration -menetelmää, jolla on vastuu: 1) laskea seuraavan sukupolven elossa olevat solut ja 2) asuttaa maailma. Voisimme siten jakaa sen kahteen osaan: populateWorldWith(aliveCellsFromPreviousGeneration()).

Samalla tavalla voisimme kerätä attribuutit, joiden nimi päättyy ” atNextGeneration ” uudessa Generation luokassa:

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

Mutta logiikan jakaminen liian moniin luokkiin lisää myös arkkitehtuurin monimutkaisuutta ja vaikeuttaa työn ymmärtämistä.

Koska Johtopäätöksenä kehotan teitä pitämään mielessä, että mitä tahansa koodikappaletta voidaan muokata kehittäjillä, joilla ei ole aikaisempaa tietoa projektista ja joiden on ymmärrettävä, mitä koodi tekee ja miksi se tekee sen voidakseen ylläpitää sitä tai käyttää osia uudelleen ilman regressioita. Ei ole hopeakuoppaa: vain kompromisseja, ja valinnalla on merkitystä sillä, että:

  • voit tunnistaa kompromissin,
  • ymmärrät edut ja haitat kukin vaihtoehto ja valitse yksi niistä tietoisesti.

(mutta älä työnnä sinua liikaa: KISS ja muista, että koodi voidaan korjata sen jälkeen).

Kommentit

  • Kiitos paljon kommentistasi. Olen niin iloinen, että kommentoit, koska en olisi koskaan ajatellut pelkästään elävien solujen tallentamista (tämä muuttaa paljon koodiani ja mielestäni tekee siitä paljon paremman). Halusin kysyä hieman mielipiteestäsi tasapainosta selvän muuttujan nimen ja ytimekkyyden välillä. Toisin sanoen, kuinka voit selvittää, milloin ohjelma on liian yksityiskohtainen? Tarkoittaako tämä sitä, että joudut viettämään poikkeuksellisen pitkän ajan oikeiden muuttujien nimien luomisessa tai että logiikassa ja koodin suunnittelussa on jotain vikaa? Kiitos vielä kerran.
  • Muokkasin vastaustani jakamaan näkemykseni siitä. Se ’ on paljon tekstiä, jossa sanotaan, että ” ei ole oikeita vastauksia, se ’ Kaikki koskevat kompromisseja, joten ajattele hyviä ja huonoja puolia tehdessäsi valintaa ”.

Vastaa

Minulla on vain yksi pieni suositus luettavuudesta. Kun sinulla on menetelmä nimeltä printBoard, odotat yleensä, että se tulostaa taulun . Parempi nimi tälle menetelmälle olisi boardToString.

Vastaa

Sähköpostiosoitettasi ei julkaista. Pakolliset kentät on merkitty *