コンウェイのゲームオブライフのバージョンの実装が完了しましたJavaを使った生活。

大学生なので、私のコードは完璧に近いものではないと確信しており、私のコードを見てみませんか。何を改善できますか?もっと速い方法はありますか?コードの特定の領域を実装するには?削除できる余分なコードはありますか?コンウェイのライフゲームを実装するよりスマートな方法はありますか?

編集:

より多くのフィードバックを受け取ることを期待して、これが私の実装の背後にある理論です:

参考までに、ここにコンウェイのライフゲームのルール(ウィキペディアから引用):

  1. 生きている隣人が2人未満の生きている細胞は、人口不足のように死にます。
  2. 生きている細胞2つまたは3つの生きている隣人が次の世代に生き続けます。
  3. 3つ以上の生きている隣人がいる生きているセルはすべて死ぬ。 rpopulation
  4. ちょうど3つの生きている隣人がいる死んだ細胞は、まるで生殖のように生きた細胞になります。

概要:

  1. Aコンウェイのライフゲームに関するさまざまな見方
  2. 暗黙のルール
  3. 重要な方法(および使用されるデータ構造)の説明

コンウェイのライフゲームの別の見方

まず、ライフゲームを軸グリッドとして想像してみましょう(また、このグリッドは、左下隅が(0,0)として示され、右上隅が(n、n)として示されるような座標を持っていると想定します。ここで、nは正の整数です)。この2次元グリッドは、n * n個のセルのグループを表します。各グリッドブロックはセルと考えることができます。セルは、セルのステータスを説明するブール値(デッドまたはアライブ)を格納するだけでなく、座標を介してその位置を詳細に示します。さらに、すべての細胞の現在の状態によって、上記のルールに従って、どの細胞が死ぬか、生き続けるか、次世代に生まれるかが決まります。

ただし、別の見方をすれば、コンウェイのゲームof lifeは、ゲームの掃海艇と非常によく似ています。生きているセルは地雷であり、その隣のセルはそれに最も近い地雷の数を格納していると考えることができます。このようにして、上記のルールを簡単に使用して、将来の世代(特に、どの細胞が死に、どの細胞が生まれるか)を決定できます。

現在生きている細胞についてはどうでしょうか。尋ねますか?ええと、これらを10より大きい整数として簡単に表すことができます。ここで、1の場所は、現在生きているセルの生きている隣人の数を示し、10の場所は、セルが生きていることを示します。

不文律

私が思いついたのは、ゲームだということです。生命のは生きている細胞だけに関心があります。生きている細胞だけが死ぬことができ、生き続ける細胞はすでに生きている必要があり、細胞は生きている隣人がいる場合にのみ生まれることができます。その結果、グリッド全体(時間計算量:O(n ^ 2))をチェックして、将来の世代のセルを決定することは完全に無駄になります。現在生きているすべてのセルを保存し、生きている各セルを隣接するセルと一緒にチェックして、次世代を決定すると、はるかに高速になります(これはまさに私が行ったことです)。

重要なメソッド(および使用されるデータ構造)の説明

birth():キーと値のペアを含むHashMapを反復処理します。すべての生きている細胞とその隣人。キーと値のペアが上記のゲームオブライフのルールに従っている場合、キー(セルの場所を表す整数値)は、次世代の生きているセルを含むスタックにプッシュされます。各反復の後、グリッドの値は0にリセットされ、キーと値のペアがHashMapから削除されます。

insertAlive():スタックをポップし、生きているセルをグリッドに挿入します。ライブセルの挿入は、マインスイーパの構造に従います(ライブセルの隣接セルは1ずつ増加し、生きているセルは10ずつ増加して、生きていることを示します)。次に、すべてのネイバーとアライブセルがHashMapに配置され、birth()が適切に実行されるようになります

printBoard()(boardToStringという名前にする必要があります):stringbuilderを使用してグリッドを文字列にフォーマットします。

:ほとんどのコメントは、の読みやすさをあまり向上させないため、削除されています。コード

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

コメント

  • 作成した’これは別の実装であると言う点ですが、実装の背後にある理論や、’使用している比較的あいまいなアルゴリズムや式を説明するものは何もありません。
  • これは人気のある演習なので、オンラインで見つけることができる実装も確認することをお勧めします。’非常に有益です。特にこの要点は、Java 8でのリアクティブな実装を示しています(RxJavaを使用)—saではありませんただし、これは優れた本番コードになります。

回答

最初に全体として、アルゴリズムはかなりスマートだと思います。これは、私の謙虚な経験では、大学生にはそれほど一般的ではありません。自分で思いついたらおめでとうございます!スマートな実装をお探しの場合は、機能的な実装をお勧めします。たとえば、Haskellのです。最短ゲームもご覧ください。

さて、賢さに注意してください。優れたコードは読みやすいわかりやすい。もちろん、複雑なアルゴリズムを扱う場合、これは常に可能であるとは限りませんが、そうあるべきだと思います。ターゲット。

jjjjjjjjjjjjのコメント:
注:ほとんどのコメントは、追加されていないため削除されています。コードの読みやすさに大いに

コメントのポイントは、人々があなたのコードを理解するのを助けることです(一般的に言えば、” ” what “ではなく、なぜ”)。ここでは、人々が理解できるように、投稿にたくさんのテキストを追加する必要がありました。コードは次のとおりであるため、理想的にはこれは必要ありません。

  • 自己文書化、
  • 複雑/暗黙的なものをクリアするためにコメントされています。

たとえば、コードをより表現力豊かにするために、コードを簡単に書き直します。

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

ほとんどの場合、いくつかの変数/メソッドの名前を変更し、いくつかのユーティリティメソッドを導入しました。コードは少し長く、感じがよくなります。冗長ですが、IMHOも理解しやすいです。それでも非常に手続き的です(特にこのような単純なプログラムの場合、それ自体は悪くありません)が、またはCell。このようなOO実装はGitHub で見つかります。

コードは、大きなボードでメモリの問題が発生する可能性もあります。実際、board[][]変数は、死んだセルも含め、すべてのセルを 格納します。 〜5 / 6セルのみを含む10000 x 10000ボードでは、多くのメモリを浪費します。解決策は、スパース配列を使用することです(基本的に、生きている細胞のみを含むセット)。

補足として、数年前、”純粋な

OOの方法。チェックアウトしたい場合は私のコードがGitHubにあります。次世代を計算する方法world is ImmutableGeneration :: nextGeneration ;生きているセルのセットが与えられると、基本的に次のようになります。1)すべての隣接セルを計算し、2)生きているセルのみを保持します。セルが生きているか死んでいるかを示すルールは、 Rule.java に実装されています。


編集 個人 nに関しては簡潔さと冗長性に関する意見コメントに答えることを目指しています

まず第一に、正しい答えはないと思います。それはすべて、トレードオフと個人的な好みに関するものです。命名は難しく、このテーマに関する記事はたくさんあります。

コンピュータサイエンスには、キャッシュの無効化と命名の2つだけ難しいことがあります
— Phil Karlton

私の考えでは、簡潔さは快適ですが、あいまいさをもたらす可能性があります。あいまいさ、特に隠されたものは脅威です。私の頭に浮かぶ最初の例は、誤って単位を混合することです。

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

それそうは言っても、長い名前はコードを読みにくくします。これらは、2つのことを考慮に入れることで減らすことができます:

  • コンテキスト(たとえば、囲むメソッド/クラス/パッケージの名前)、
  • スコープ(のローカル変数短い名前では3行の方法で問題ないかもしれませんが、コードベース全体で複数回使用される関数には長い方法が必要になる場合があります。

これも

Googleの命名規則。

最後の注意として、非常に長い名前はコードの臭いとして表示される場合があります。通常、問題はまとまりの欠如です(クラス/メソッドの動作が多すぎます。繰り返しになりますが、これに関する明確な指標はありません。開発者の気持ち)。たとえば、私が提案したコードでは、populateWorldWithAliveCellsFromPreviousGenerationを、1)次世代で生きる細胞を計算し、2)世界に住むという責任を負う方法と考えることができます。したがって、2つに分割できます:populateWorldWith(aliveCellsFromPreviousGeneration())

同じ方法で、名前が” atNextGeneration “新しい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)); } ... }  

ただし、ロジックを分割しすぎると、アーキテクチャが複雑になり、フローを理解しにくくなります。

結論コードのどの部分も、プロジェクトに関する事前の知識がなく、コードの機能と理由を理解する必要がある開発者によって変更される可能性があることに注意してください。 これを行うので、リグレッションを発生させることなく、パーツを維持または再利用できます。特効薬はありません。トレードオフのみです。選択するときに重要なのは、次のとおりです。

  • トレードオフを特定できます。
  • 次の長所と短所を理解します。

(ただし、あまり圧力をかけないでください: KISS とその後、コードをリファクタリングできることを忘れないでください)

コメント

  • コメントありがとうございます。生きている細胞だけを保存することだけを考えたことはなかったので、コメントしてくれてとてもうれしいです(これは私のコードの多くを変更し、私の意見ではそれをはるかに良くします)。変数名を明確にすることと簡潔にすることのバランスについて、あなたの意見を少しお聞きしたいと思います。言い換えれば、プログラムが冗長すぎる場合をどのように判断できますか?それは、正しい変数名を作成するのに非常に長い時間を費やす必要があることを意味しますか、それともコードのロジックと設計に問題があることを意味しますか?もう一度ありがとう。
  • 回答を編集して、意見を共有しました。 ‘基本的に、”正しい答えはないというテキストがたくさんあります。’トレードオフがすべてなので、選択するときは長所と短所を考慮してください”。

回答

読みやすさに関する小さな推奨事項が1つだけあります。 printBoardというメソッドがある場合、通常はボードを印刷すると予想されます。そのメソッドのより適切な名前は、boardToStringです。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です