Java로 Battleship 게임을 생성하기위한 문제 설명을 받았습니다.
내 작업 코드 (Spring Boot + Web)가 문제 설명과 함께 여기에 배치됩니다. https://github.com/ankidaemon/BattleShip
이 질문은 주로 디자인에 초점을 맞추고 있습니다. 분리하여 적절한 디자인 패턴을 적용 할 수 있습니까?
StartGame.java-컨트롤러에서 호출 받기
@Component public class StartGame { private static final Logger logger = LoggerFactory.getLogger(StartGame.class); public String init(File inputFile) throws FileNotFoundException, InputException { // TODO Auto-generated method stub ArrayList<BattleShips> p1s = new ArrayList<BattleShips>(); ArrayList<BattleShips> p2s = new ArrayList<BattleShips>(); int areaWidth = 0; int areahight = 0; ArrayList<Coordinate> player1missiles = null; ArrayList<Coordinate> player2missiles = null; try{ Scanner sc = new Scanner(inputFile); areaWidth = sc.nextInt(); if(areaWidth>9 || areaWidth<1){ raiseException("Supplied area width is invalid.",sc); } areahight = sc.next().toUpperCase().charAt(0) - 64; if(areahight>25 || areahight<0){ raiseException("Supplied area height is invalid.",sc); } sc.nextLine(); int noOfships = sc.nextInt(); if(noOfships>areahight*areaWidth || noOfships<1){ raiseException("Supplied no of ships is invalid.",sc); } sc.nextLine(); for (int j = 0; j < noOfships; j++) { char typeOfShip = sc.next().toUpperCase().charAt(0); if(typeOfShip!="P" && typeOfShip!="Q"){ raiseException("Supplied type of ship is invalid.",sc); } int shipWidth = sc.nextInt(); if(shipWidth>areaWidth || shipWidth<0){ raiseException("Supplied ship width is invalid.",sc); } int shiphight = sc.nextInt(); if(shiphight>areahight || shiphight<0){ raiseException("Supplied ship height is invalid.",sc); } BattleShips ship; for (int i = 0; i <= 1; i++) { char[] locCharArr = sc.next().toUpperCase().toCharArray(); int[] loc = new int[2]; loc[0] = locCharArr[0] - 65; loc[1] = locCharArr[1] - 49; if(loc[0]>areahight || loc[0]<0 || loc[1]>areaWidth || loc[1]<0){ raiseException("Supplied ship location is invalid.",sc); } ship = new BattleShips(shipWidth, shiphight, typeOfShip, loc); if (i % 2 == 0) p1s.add(ship); else p2s.add(ship); } sc.nextLine(); } player1missiles = returnMissileCoordinates(sc.nextLine()); player2missiles = returnMissileCoordinates(sc.nextLine()); sc.close(); }catch(InputMismatchException e){ throw new InputException("Invalid Input supplied.",ErrorCode.INVALIDINPUT); } BattleArea player1 = new BattleArea("player1", areaWidth, areahight, p1s); BattleArea player2 = new BattleArea("player2", areaWidth, areahight, p2s); player1.placeShips(); player2.placeShips(); while (!player1.isLost() && !player2.isLost()) { for (int i = 0; i < player1missiles.size();) { Coordinate c = player1missiles.get(i); while (player1.fireMissile(c, player2)) { player1missiles.remove(i); if (i < player1missiles.size()) { c = player1missiles.get(i); } else break; } if (player1missiles.size() > 0) { player1missiles.remove(i); } break; } for (int j = 0; j < player2missiles.size();) { Coordinate c = player2missiles.get(j); while (player2.fireMissile(c, player1)) { player2missiles.remove(j); if (j < player2missiles.size()) { c = player2missiles.get(j); } else break; } if (player2missiles.size() > 0) { player2missiles.remove(j); } break; } } if (player1.isLost()) { logger.info("-------------------------"); logger.info("Player 2 has Won the Game"); logger.info("-------------------------"); return "Player 2 has Won the Game"; } else { logger.info("-------------------------"); logger.info("Player 1 has Won the Game"); logger.info("-------------------------"); return "Player 1 has Won the Game"; } } private static ArrayList<Coordinate> returnMissileCoordinates(String nextLine) { // TODO Auto-generated method stub ArrayList<Coordinate> tmp = new ArrayList<Coordinate>(); String[] arr = nextLine.split("\\ "); Coordinate tmpC; for (String s : arr) { char[] charArr = s.toCharArray(); tmpC = new Coordinate(charArr[1] - 49, charArr[0] - 65); tmp.add(tmpC); } return tmp; } private void raiseException(String message, Scanner sc) throws InputException { sc.close(); throw new InputException(message, ErrorCode.INVALIDINPUT); } }
BattleArea.java
public class BattleArea { private static final Logger logger = LoggerFactory.getLogger(BattleArea.class); private String belongsTo; private int width,height; private ArrayList<BattleShips> battleShips; private Set<Coordinate> occupied=new TreeSet<Coordinate>(); private int[][] board=null; private boolean lost=false; public BattleArea(String belongsTo, int width, int height, ArrayList<BattleShips> battleShips) { super(); this.belongsTo = belongsTo; this.width = width; this.height = height; this.battleShips = battleShips; this.board=new int[this.width][this.height]; } public void placeShips(){ for(BattleShips ship:this.battleShips){ int x=ship.getLocation()[1]; int y=ship.getLocation()[0]; if(ship.getWidth()+x>this.width || ship.getHeight()+y>this.height){ logger.error("Coordinate x-"+x+" y-"+y+" for "+this.belongsTo+" is not avilable."); throw new ProhibitedException("Ship cannot be placed in this location.",ErrorCode.OUTOFBATTLEAREA); }else{ Coordinate c=new Coordinate(x, y); if(occupied.contains(c)){ logger.error("Coordinate x-"+c.getX()+" y-"+c.getY()+" for "+this.belongsTo+" is already occupied."); throw new ProhibitedException("Ship cann"t be placed in this location.",ErrorCode.ALREADYOCCUPIED); }else{ Coordinate tempC; for(int i=x;i<ship.getWidth()+x;i++){ for(int j=y;j<ship.getHeight()+y;j++){ logger.debug("Placing at x-"+i+" y-"+j+" for "+this.belongsTo); tempC=new Coordinate(i, j); occupied.add(tempC); if(ship.getTypeOfShip()=="P"){ board[i][j]=1; }else if(ship.getTypeOfShip()=="Q"){ board[i][j]=2; } } } } } } } public boolean fireMissile(Coordinate c, BattleArea enemyBattleArea){ int x=c.getX(); int y=c.getY(); logger.info("Firing at "+enemyBattleArea.belongsTo+" x-"+x+" y-"+y+" :"); if(enemyBattleArea.board[x][y]!=0){ if(enemyBattleArea.board[x][y]==-1){ logger.debug("Already blasted!"); return false; } else if(enemyBattleArea.board[x][y]==1){ Coordinate temp=new Coordinate(x,y); enemyBattleArea.occupied.remove(temp); enemyBattleArea.board[x][y]=-1; if(enemyBattleArea.occupied.size()==0){ enemyBattleArea.setLost(true); } logger.debug("Suucessfully blasted!!"); return true; }else{ enemyBattleArea.board[x][y]=enemyBattleArea.board[x][y]-1; logger.debug("Half life left!!"); return true; } }else{ logger.debug("Missed"); return false; } } public boolean isLost() { return lost; } public void setLost(boolean lost) { this.lost = lost; } }
BattleShips.java
public class BattleShips { private int width,height; private char typeOfShip; private int[] location; public BattleShips(int width, int height, char typeOfShip, int[] loc) { super(); this.width = width; this.height = height; this.typeOfShip = typeOfShip; this.location = loc; } public int getWidth() { return width; } public int getHeight() { return height; } public char getTypeOfShip() { return typeOfShip; } public int[] getLocation() { return location; } }
Coordinate.java
public class Coordinate implements Comparable<Coordinate> { private int x,y; public Coordinate(int x, int y) { super(); this.x = x; this.y = y; } @Override public String toString() { return "Coordinate [x=" + x + ", y=" + y + "]"; } @Override public int compareTo(Coordinate o) { // TODO Auto-generated method stub if(this.x==o.x && this.y==o.y) return 0; else if(this.x<o.x && this.y<o.y) return -1; else return 1; } public int getX() { return x; } public int getY() { return y; } }
샘플 입력
5 E 2 Q 1 1 A1 B2 P 2 1 D4 C3 A1 B2 B2 B3 A1 B2 B3 A1 D1 E1 D4 D4 D5 D5
규칙
1. Player1이 먼저 실행됩니다. 각 플레이어는 (hit == 성공)까지 또 다른 기회를 얻게됩니다.
2. 전함은 수평으로 배치됩니다.
3. Type-Q 함선을 파괴하려면 미사일 2 발이 필요합니다.
4. Type-P 함선을 파괴하려면 미사일 1 발이 필요합니다.
입력
입력의 첫 번째 줄에는 너비와 높이가 공백으로 구분 된 전투 영역의 크기가 포함됩니다.
두 번째 라인에는 각 플레이어가 보유한 전함 수 (B)가 있습니다.
그런 다음 다음 라인 전함 유형에서 차원 (너비 및 높이) & 위치 (Y 좌표 및 X 좌표)는 Player-1 및 Player-2의 위치가됩니다. 공백으로 구분됩니다.
그리고 다음 줄에 Player-1의 미사일 시퀀스 (공백으로 구분됨)에서 목표 위치 좌표 (Y 및 X)가 제공되고 Player-2에 대한 시퀀스가 제공됩니다.
제약 조건 :
1 < = 전투 지역의 너비 (M) < = 9
A < = 전투 지역의 높이 (N ) < = Z
1 < = 전함 수 < = M * N
군함 유형 = { P, Q}
1 < = 전함 폭 < = M
A < = 전함 높이 < = N
1 < = 선박의 X 좌표 < = M
A < = 선박의 Y 좌표 < = N
댓글
- 게임 규칙과 댓글을 추가 할 수 있습니다. 적어도 코드의 가장 중요한 부분에서. 또한 코드의 중요한 부분을 서브 루틴으로 이동하여 코드를 훨씬 더 읽기 쉽게 만들 수 있습니다.하지만 너무 많은 함수를 호출하면 코드가 느려질 수 있지만 적어도 다른 사람들이 더 잘 읽을 수 있으므로 나중에 더 나은 업데이트를 유지할 수 있습니다. . 원래 코드와 게임 규칙과 함께 더 모듈 식 / 주석이있는 코드를 추가 할 수 있으며, 해당 규칙에 대한 주석을 기반으로 할 수 있습니다.
- @ alt.126은 규칙과 입력을 추가했습니다. StartGame은 파일에서 입력을 읽고 유효성 검사를 수행하며 각 플레이어에 대해 BattleArea 및 BattleShip을 생성합니다. BattleShips는 POJO입니다. BattleArea에는 규칙에 따라 함선을 배치하고 미사일을 배치하는 방법이 있습니다. Thnx
Answer
ok가 직접 해보겠습니다.
StartGame
는 도움이되지 않습니다. 더 일치하는 이름으로 이름을 바꿉니다. BattleShipGame
및 컨트롤러에서 게임 시작
BattleShipGame game = new BattleShipGame(); game.start();
init
-메소드가 너무 크지 않고 init 을 수행하지 않지만 더 많은 작업을 수행합니다. 그러니 좀 더 자세히 살펴 보겠습니다.
- init should 초기화가 성공했음을 나타내는 부울 (또는
Result
)을 반환합니다. - init는 “ 대리자 방법 처럼 보입니다. 논리가 거의 없어야합니다. 대신 대부분의 작업을 메서드에 넣는 것이 유용합니다.
- 일을 초기화하고 다른 작업을 수행하지 않습니다.
- 객체 …
- 게임 로직을 메소드 밖으로 이동
다음과 같을 수 있습니다.
참고 : init 메서드는 훨씬 더 짧아 질 수 있지만 init
가 실제로 무엇을해야하는지 좋은 방법으로 지적합니다. do …
위에서 언급했듯이 게임 로직을 init 메소드에서 이동하여 playGame()
메소드에 넣었습니다.
public Result playGame(){ Result result = new Result(); Scores score = new Score(); while (!player1.isLost() && !player2.isLost()) { for (int i = 0; i < player1missiles.size();) { ... } } result.setWinner(playerOne); result.setScore(scores); return result; }
BattleShipGame
는 이제 다음과 같은 방식으로 시작됩니다.
public void start(){ init(); Result result = playGame(); ... //do whatever you want with your result - for example print it into the console }
BattleShip에 대해 이야기 할 수있는 몇 가지 문제가 더 있습니다. 언뜻보기에 좋아 보이는 Coordinate
클래스를 사용하는 것이 아주 좋은 생각이라고 생각합니다. 하지만 결과적으로 사용하지는 않습니다. 배송을 위해 int[]
대신 Coordinate
를 사용하면 어떻게 될지 생각해보십시오. 코드는 읽기 쉽고 수학도 훨씬 쉬울 것입니다. shiptype에 char
를 사용하지 말고 대신 enum을 사용하세요. 하지만 솔직히 “위치와 너비와 높이”가 없습니다. 실제로 가지고있는 것은 직사각형이므로 직사각형을 사용하세요!
public class BattleShips { private ShipType shipType; private Rectangle bounds; private int lifePoints; public BattleShips(ShipType typeOfShip, Coordinate pos) { super(); shipType = typeOfShip; bounds = new Rectangle(shipType.getDimension, pos); lifePoints = shipType.getLifePoints(); } public Rectangle getBounds() { return bounds(); } ... }
직사각형의 크기 (너비 / 높이)와 라이프 포인트의 양은 ShipType에 의해 결정될 수 있습니다.
public Enum Shiptype { DESTROYER(2,4,2), SUBMARINE(1,3,1), ...; //don"t use shiptype P or shiptype Q private final Dimension dimension; final int lifePoints; public ShipType(int w, int h, int life){ dimension = new Dimension(w,h); lifePoints = life; } public Dimension getDimension(){ return dimension; } public int getLifePoints(){ return lifePoints(); } }
BattleArea는 이제 훨씬 더 쉽게 사용, 지금 얼마나 간단한 지 생각해보십시오. placeShips
:
public class BattleArea { private Player owner; private Rectangle boardBounds; private List<BattleShips> battleShips; private List<Coordinates> board; public BattleArea(Player owner, Rectangle bounds, List<BattleShips> battleShips) { super(); this.owner = owner; this.dimension = dimension; this.battleShips = battleShips; board = createBoard(); } public void placeShips(){ List<BattleShip> placedShips = new ArrayList<>(); for(BattleShips ship:this.battleShips){ Bound shipBounds = ship.getBounds(); if(!boardBounds.contains(shipBounds)){ throw new ProhibitedException( "Ship cannot be placed in this location.",ErrorCode.OUTOFBATTLEAREA); } for (BattleShip placedShip: placedShips){ if (bounds.intersects(placedShip.getBounds()){ throw new ProhibitedException( "Ship cann"t be placed in this location.",ErrorCode.ALREADYOCCUPIED); } } placedShips.add(battleShip); } } public boolean fireMissile(Coordinate c, BattleArea enemyBattleArea){ BattleShip shipAt = enemyBattleArea.getShipAt(c); if(shipAt == null){ return false; }else{ handleDamge(shipAt, enemyBattleArea); return true; } } private void handleDamage(BattleShip opponent, BattleArea area){ int lifePointsLeft = opponent.getLifePoints() - 1; //hardcoded damage (that"s bad) if(lifPoints > 0){ //Log damage done }else{ //log destroyed area.removeBattleShip(opponent); } } }
위의 모든 코드는 컴파일되지 않았으므로 철자 오류 일 수 있으며 많은 메소드가 아직 구현되지 않았을 수 있습니다 (예 : Rectangle.contains()
또는 기타).
요약
하지만 “지금 우리가 가지고있는 것을 살펴보십시오.
- 모든 코드 !!! (
ShipType
에서 다른 shiptype을 추가하기 만하면됩니다.) - 코드의 복잡성을 매우 줄였습니다. “ 위험한 계산 없음 이온.
- 관심을 분리했습니다. 이제 개체가해야 할 일을합니다.
- 다른 플레이어 (3 인 게임)를 위해 코드를 쉽게 변경할 수 있습니다.
- 지금 코드를 테스트 할 수 있습니다.
댓글
- 여기에 여전히 많은 문제가 있다는 것을 알고 있습니다. ‘ 모든 항목을 처리하지 마십시오.이 답변은 세부 정보가 포함되지 않은 최상위보기에있었습니다.
답변
분리하려면 함수가 작업을 수행하기 위해 서로 호출 할 필요가 없는지 확인해야합니다. 가능하다면 다른 함수를 호출해야하는 유일한 함수는 API 인터페이스처럼 호출되도록 의도 된 각 하위 시스템의 드라이버 함수입니다.
추가하기 위해 많은 코드를 이식해야하는 방법을 생각해보세요. 해당 함수가 다른 함수를 호출하거나 다른 대형 하위 시스템의 변수를 사용하는 경우 유용한 함수입니다. 중복 된 코드처럼 보이지만 다른 프로그램에 의존하지 않는 방식으로 해당 기능을 구현하기 위해 추가 노력을 기울이면 개별적으로 다른 프로그램으로 이식 할 수 있습니다. 작성하는 모든 코드를 필요한 환경으로 이식 할 수 있도록하기 위해 사용하는 모든 단일 컴파일러 및 프로그래밍 언어에없는 언어 기능이나 라이브러리 기능에 의존하지 마십시오 . 디커플링 이라고합니다.
보시다시피 디커플링은 컴파일러, 언어, 시스템 환경, 함수 및 하위 시스템 수준에서 적용 할 수 있습니다. 독립 실행 형의 종속성없는 루틴을 갖도록 코드를 복제하고 다시 작성해야 할 수 있습니다. 또한 코드의 이식성을 높이기 위해보다 광범위하게 표준화 된 기능을 사용함을 의미 할 수 있으며 시스템 / 언어 / 컴파일러에 관계없이 모든 프로그래밍 / 시스템 환경에 누락 된 기능을 구현하거나 이식하는 작업을 직접 수행해야 할 수도 있습니다. 사용하면 항상 동일한 기능을 사용할 수 있습니다.
디자인 패턴 정보
코드의 재사용 가능성을 높이고 싶은 경우 수십 년 동안 지속되기를 원한다면 CPU 어셈블리 프로그래밍의 저수준 접근 방식을 사용할 수 있습니다.
항상 동일한 유형을 취하는 방식으로 수행하려는 작업 또는 마이크로 작업에 대해 생각해보십시오. 매개 변수는 항상 똑같은 방식으로 결과를 반환합니다.
그런 다음이 루틴에 매우 구체적인 이름을 지정합니다. 이것은 다른 기본 CPU 명령어와 마찬가지로 재사용 할 수있는 함수 / 서브 루틴으로 구현 된 명령 인 opcode입니다. 이 코드 디자인 방식은 재사용 가능성이 높고 안정적입니다. 수행 할 작업의 변형을 원하면 이전의 유효한 기능을 파괴하는 대신 새 opcode 함수를 추가하기 만하면됩니다.
이를 주요 설계 접근 방식으로 프로그램 전체에 적용하면 코드를보다 엄격하게 구조화 할 수 있습니다. 이해하기 쉽습니다.
댓글
- 감사합니다. @ alt.126. 이것은 일반화 된 솔루션 (+1)이지만 실제로 Java 디자인 패턴에 더 많은 것을 기대하고 객체를 줄이고 중복 코드를 제거하는 등의 작업을 수행했습니다.