Recebi uma declaração de problema para criar um jogo de navio de guerra em java.
Meu código de trabalho (Spring Boot + Web) é colocado aqui junto com a declaração do problema. https://github.com/ankidaemon/BattleShip
Esta questão é principalmente focada no design, por favor me ajude a descobrir, como posso torná-lo desacoplado e aplicar padrões de design adequados.
StartGame.java – sendo chamado do controlador
@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; } }
Exemplo de entrada
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
Regras
1. O Jogador1 disparará primeiro. Cada jogador terá outra chance até (acerto == sucesso).
2. Os navios de guerra serão colocados horizontalmente.
3. A nave Tipo-Q requer 2 mísseis atingidos para ser destruída. A nave Tipo-P requer 1 ataque de míssil para ser destruída.
Entrada
A primeira linha da entrada contém as dimensões da área de batalha com largura e altura separadas por espaço.
A segunda linha terá o número (B) de navios de guerra que cada jogador possui.
Então, na próxima linha tipo de navio de guerra, as dimensões (largura e altura) & posições (coordenada Y e coordenada X) para o Jogador 1 e então para o Jogador 2 serão dado separado por espaço.
E então na próxima linha a sequência do Jogador-1 (separada por espaço) de coordenadas de localização do alvo dos mísseis (Y e X) será dada e então para a sequência do Jogador-2.
Restrições:
1 < = Largura da área de batalha (M) < = 9
A < = Altura da área de batalha (N ) < = Z
1 < = Número de navios de guerra < = M * N
Tipo de navio = {P, Q}
1 < = Largura do navio de guerra < = M
A < = Altura do navio de guerra < = N
1 < = Coordenada X do navio < = M
A < = Coordenada Y do navio < = N
Comentários
- Você pode adicionar as regras do jogo e os comentários pelo menos nas partes mais importantes do código. Você também pode mover essas partes críticas do código para sub-rotinas para tornar o código muito mais legível, embora chamar tantas funções tornaria o código mais lento, mas pelo menos se tornará muito mais legível para outros para que possam manter melhor as atualizações posteriormente . Você poderia adicionar o código original e um código mais modular / comentado acompanhado das regras do jogo, talvez baseando os comentários nessas regras.
- @ alt.126 adicionou regras e entradas. StartGame lê a entrada de um arquivo, faz validação e cria BattleArea e BattleShips para cada jogador. Navios de batalha são POJO. BattleArea tem métodos para colocar navios e fileMissiles com base nas regras. Thnx
Resposta
ok vamos colocar as mãos em:
Nome da classe para o seu StartGame
não é útil, renomeie-o com um nome mais correspondente, acho que como BattleShipGame
e inicie o jogo em vez de seu controlador
BattleShipGame game = new BattleShipGame(); game.start();
o init
– o método é muito grande e não faz init , mas faz ainda mais coisas … então vamos decompô-lo um pouco:
- o init deve retorna um booleano (ou um
Result
) que indica que o init foi bem-sucedido. - init parece que “um método delegado que significa deve haver muito pouca lógica interna – em vez disso, é útil colocar a maior parte do trabalho em métodos
- apenas init coisas e não fazer mais nada
- use
Player
objetos … - mova a lógica do jogo para fora do método
poderia ser assim
NOTA: o método init pode ser encurtado muito mais, mas acho que aponto de uma boa forma o que init
realmente deveria faça …
como mencionado acima, você tirou a lógica do jogo do seu método init e a colocou no método 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; }
o BattleShipGame
começaria agora desta maneira:
public void start(){ init(); Result result = playGame(); ... //do whatever you want with your result - for example print it into the console }
para o seu BattleShip existem mais alguns assuntos que podem ser comentados. Acho que foi uma ideia muito boa usar uma classe Coordinate
que parece boa à primeira vista. Mas você não o usa em conseqüência. Pense em como seria se você usasse Coordinate
para enviar em vez de int[]
que faria seu código também fica mais fácil de ler e a matemática seria muito mais fácil. E não use um char
para seu tipo de envio, use um enum. Mas vamos ser honestos, você não tem uma “posição, largura e altura”, o que você realmente tem é um retângulo – então use um retângulo!
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(); } ... }
a dimensão do retângulo (largura / altura) e a quantidade de pontos de vida podem ser determinados pelo 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(); } }
O BattleArea é agora muito mais fácil de usar, pense em quão simples você pode placeShips
agora:
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); } } }
todo o código acima não foi compilado então lá podem haver alguns erros de ortografia e muitos métodos ainda não foram implementados (como Rectangle.contains()
ou outros).
resumo
mas vamos “vejamos o que temos agora:
- você pode alterar o tipo de navio facilmente sem modificar qualquer código !!! (você simplesmente tem que adicionar outro tipo de envio em
ShipType
) - você reduziu a complexidade do seu código muito, você não “t ter cálculos perigosos íons.
- você tem interesses separados, os objetos agora fazem o que devem fazer
- você pode facilmente alterar seu código para outro jogador (jogo para três jogadores)
- você pode testar seu código agora
Comentários
- bem, eu sei que ainda há tantos problemas aqui que eu não poderia ‘ para lidar com todos eles – esta resposta estava na visão superior, não entrando em nenhum detalhe
Resposta
Para desacoplar, você deve ter certeza de que suas funções não precisam chamar umas às outras para fazer seu trabalho. Se possível, as únicas funções que devem chamar outras são as funções de driver de cada subsistema que devem ser chamadas como a interface da API.
Pense em como você precisaria portar uma grande quantidade de código apenas para adicionar uma função útil se essa função chamar outras funções ou usar variáveis de outros grandes subsistemas. Se você fizer um esforço adicional para implementar essa função de uma maneira que ela não dependa de mais nada, mesmo que pareça código duplicado, você poderá portá-lo individualmente para outro programa, e ainda mais se você não o faça depender de recursos de linguagem ou recursos de biblioteca que não estão presentes em cada compilador e linguagem de programação que você usa para tornar possível portar qualquer código que você escrever para qualquer ambiente de que precisar , isso é o que é chamado de desacoplamento .
Como você pode ver, o desacoplamento pode ser aplicado no compilador, na linguagem, no ambiente do sistema, nas funções e nos níveis do subsistema. Pode envolver a duplicação de código e a reescrita para ter rotinas independentes e sem dependência. Também pode implicar o uso de recursos padronizados mais amplamente para tornar o código mais portátil, e também pode precisar que você trabalhe na implementação ou portabilidade da funcionalidade ausente para todos os seus ambientes de programação / sistema, de forma que não importa o sistema / linguagem / compilador que você usar, você sempre terá a mesma funcionalidade disponível.
Sobre padrões de design.
Se você deseja tornar seu código altamente reutilizável e se você quiser que dure décadas, pode usar a abordagem de baixo nível de programação de montagem de CPU.
Pense em uma tarefa ou microtarefa que deseja executar de uma maneira que sempre terá o mesmo tipo de parâmetros e que sempre retornará um resultado exatamente da mesma maneira.
Então dê um nome MUITO específico para esta rotina. Este será um opcode, uma instrução, implementada como uma função / sub-rotina, que você pode reutilizar como qualquer outra instrução nativa da CPU. Essa maneira de projetar o código é altamente reutilizável e estável. Se você quiser uma variação no que fazer, basta adicionar uma nova função de opcode em vez de destruir a funcionalidade válida anterior.
Aplicar isso em todo o programa como a abordagem de design principal pode tornar o código ainda mais estritamente estruturado mais fácil de seguir.
Comentários
- Obrigado, @ alt.126. Embora esta seja uma solução generalizada (+1), eu estava realmente esperando mais nos padrões de design Java / tornando menos objetos / removendo código redundante, etc.