저는 Conway의 Game of Java를 사용하는 삶.
대학생이기 때문에 내 코드가 거의 완벽하지 않다고 확신하며 내 코드를 볼 수 있는지 궁금합니다. 무엇을 개선 할 수 있습니까? 더 빠른 방법이 있습니까? 내 코드의 특정 영역을 구현하려면 어떻게해야합니까? 제거 할 수있는 초과 코드가 있습니까? Conway의 Game of Life를 구현하는 더 현명한 방법이 있습니까?
편집 :
더 많은 피드백을 받기를 바라는 내 구현 이론은 다음과 같습니다.
참고로 다음은 Conway의 삶의 게임 규칙 (wikipedia에서 가져옴) :
- 살아있는 이웃이 두 개 미만인 모든 살아있는 세포는 인구가 부족한 것처럼 죽습니다.
- 모든 살아있는 세포 견인 또는 세 명의 살아있는 이웃이 다음 세대로 살아갑니다.
- 세 명 이상의 살아있는 이웃이있는 모든 살아있는 세포는 마치 ove처럼 죽습니다. rpopulation
- 정확히 세 개의 살아있는 이웃이있는 죽은 세포는 마치 번식에 의한 것처럼 살아있는 세포가됩니다.
개요 :
- A Conway의 Game of Life에 대한 다른 전망
- 무언 적 규칙
- 중요한 방법 (및 사용 된 데이터 구조)에 대한 설명
Conway의 Game of Life에 대한 다른 전망
먼저 생명의 게임을 anxn 그리드 (우리는 또한이 그리드에는 왼쪽 하단 모서리가 (0,0)으로 표시되고 오른쪽 상단 모서리가 (n, n) (n은 양의 정수)로 표시되는 좌표가 있다고 가정합니다. 이 2 차원 그리드는 n * n 개의 셀 그룹을 나타냅니다. 각 그리드 블록은 셀로 생각할 수 있으며, 셀의 상태를 설명하는 부울 값 (dead 또는 alive)을 저장할뿐만 아니라 좌표를 통해 위치를 자세히 설명합니다. 또한 모든 세포의 현재 상태는 위에 나온 규칙에 따라 어떤 세포가 죽거나, 계속 살거나, 다음 세대에 태어날 것인지를 결정합니다.
그러나 다른 관점에서 Conway의 게임은 생명의 지뢰 찾기 게임과 매우 유사합니다. 우리는 살아있는 세포를 광산으로 생각할 수 있으며, 이웃 세포는 가장 가까운 광산 수를 저장하고 있습니다. 이런 식으로 위의 규칙을 사용하여 미래 세대 (특히 어떤 세포가 죽고 어떤 세포가 태어날 것인지)를 쉽게 결정할 수 있습니다.
현재 살아있는 세포는 어떻습니까? 물어보기? 음, 우리는 이것을 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"); } } }
댓글
- 내가 ‘ 작성했습니다. 이것이 다른 구현이라고 말하는 요점이지만 구현이면의 이론이나 사용중인 상대적으로 모호한 알고리즘 및 공식을 설명 할 것이 없습니다. ‘
- 이것은 인기있는 연습이므로 온라인에서 찾을 수있는 구현을 살펴 보도록 권합니다. ‘ 매우 유익합니다. 특히 이 요점 은 sa가 아닌 Java 8 (RxJava 사용)의 반응 형 구현을 보여줍니다. 그래도 좋은 프로덕션 코드가 될 것입니다.
답변
첫 번째 모두, 알고리즘이 꽤 영리하다고 생각합니다. 내 겸손한 경험으로는 대학생에게는 그리 일반적이지 않습니다. 그래서 당신이 그것을 스스로 생각해 냈다면 축하합니다! 스마트 한 구현을 찾고 있다면 기능적인 구현을 권장합니다 (예 : Haskell의 ). 또한 가장 짧은 게임을 참조하세요. 삶의 .
이제 현명함을 조심하세요. 좋은 코드는 읽기 쉬워야합니다. , 이해하기 쉬움 . 물론 복잡한 알고리즘을 다룰 때 항상 가능한 것은 아니지만 그래야한다고 생각합니다. 대상입니다.
jjjjjjjjjjjj 메시지 :
참고 : 대부분의 댓글은 추가되지 않기 때문에 삭제되었습니다. 코드의 가독성에 크게 기여
주석의 요점은 사람들이 코드를 이해하도록 돕는 것입니다 (일반적으로 말하면 iv id = “c4d933d144 ” 무엇이 “가 아니라 “>
왜 “). 여기에서 사람들의 이해를 돕기 위해 게시물에 많은 텍스트를 추가해야했습니다. 이상적으로는 “코드가 다음과 같기 때문에 필요하지 않습니다.
- 자체 문서화,
- 복잡하거나 암묵적인 내용을 정리하기 위해 주석 처리됨
예를 들어 다음은 코드의 표현력을 높이기 위해 코드를 빠르게 재 작성한 것입니다.
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는 ImmutableGeneration :: nextGeneration 입니다. 살아있는 셀 집합이 주어지면 기본적으로 1) 모든 이웃 셀을 계산 한 다음 2) 살아있을 셀만 유지합니다. 셀이 살아 있는지 또는 죽을지를 나타내는 규칙은 Rule.java 에서 구현됩니다.
수정 : 개인 간결성 대 장황함에 대한 의견 의견에 답하기
우선, 정답은 없다고 생각합니다. 그것은 모두 절충안과 개인적인 취향에 관한 것입니다. 이름 지정은 어렵고 “주제에 대한 많은 기사를 찾을 수 있습니다.
컴퓨터 과학에는 캐시 무효화와 이름 지정이라는 두 가지 어려운 부분이 있습니다.
— Phil Karlton
내 생각은 간결함은 유쾌하지만 모호함을 유발할 수 있다는 것입니다. 특히 숨겨진 모호함은 위협입니다.가장 먼저 떠오르는 예는 실수로 단위를 혼합하는 것입니다.
// Everything looks good... double pathLength = distanceFromGoal + distanceToTarget; // ... but adding units makes easy to spot bugs double pathLengthInKilometers = distanceFromGoalInMeters + distanceToTargetInMillimeters;
긴 이름은 코드를 읽기 어렵게 만듭니다. 두 가지를 고려하여 줄일 수 있습니다.
- 컨텍스트 (예 : 둘러싸는 메서드 / 클래스 / 패키지 이름),
- 범위 (로컬 변수 3 줄 메서드는 짧은 이름으로 괜찮을 수 있지만 전체 코드베이스에서 여러 번 사용되는 함수는 더 긴 이름이 필요할 수 있습니다.
그것도
Google의 명명 규칙 .
마지막 참고로 매우 긴 이름은 코드 냄새로 간주 될 수 있습니다. 일반적으로 문제는 결속력 부족 입니다 (클래스 / 메소드는 너무 많은 다른 작업을 수행합니다. 다시 한 번 말하지만 이에 대한 명확한 측정 항목이 없습니다. 개발자의 느낌). 예를 들어, 제가 제안한 코드에서 populateWorldWithAliveCellsFromPreviousGeneration
는 책임을 맡은 방법으로 생각할 수 있습니다. 1) 다음 세대에 생존 할 세포를 계산하고 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)); } ... }
하지만 논리를 너무 많은 클래스로 분할하면 아키텍처가 복잡해지고 흐름을 이해하기가 더 어려워집니다.
As a 결론 저는 프로젝트에 대한 사전 지식이없고 코드의 기능과 이유를 이해해야하는 개발자가 모든 코드를 수정할 수 있다는 점을 기억하시기 바랍니다. 회귀를 도입하지 않고 유지하거나 부품을 재사용 할 수 있도록합니다. 은밀한 것은 없습니다. 장단점 만 있고 선택할 때 중요한 것은 다음과 같습니다.
- 장단점을 식별 할 수 있습니다.
- 장단점을 이해합니다. 각 대안을 고의로 선택합니다.
(그러나 너무 많은 압력을 가하지 마십시오. KISS 및 코드는 이후에 리팩토링 될 수 있습니다.)
댓글
- 댓글을 보내 주셔서 감사합니다. 살아있는 셀만 저장하는 것에 대해 생각하지 않았을 것이기 때문에 댓글을 달아 주셔서 기쁩니다 (이것은 내 코드를 많이 변경하고 제 생각에는 훨씬 더 좋습니다). 변수 명으로 명료 함과 간결함 사이의 균형에 대한 귀하의 의견을 조금 묻고 싶었습니다. 즉, 프로그램이 너무 장황한시기를 어떻게 확인할 수 있습니까? 이는 올바른 변수 이름을 만드는 데 매우 오랜 시간을 소비해야하거나 코드의 논리 및 디자인에 문제가 있다는 것을 의미합니까? 다시 한 번 감사드립니다.
- 제 의견을 공유하기 위해 제 답변을 수정했습니다. ‘ 기본적으로 ” 정답이 없다고 말하는 텍스트가 많이 있습니다. ‘은 모두 장단점에 관한 것이므로 선택할 때 장단점을 고려하십시오 “.
답변
가독성과 관련하여 작은 권장 사항이 하나 있습니다. printBoard
라는 메서드가있는 경우 일반적으로 보드를 인쇄 할 것으로 예상합니다. 이 방법의 더 나은 이름은 boardToString
입니다.