Jeu Java Battleship

Jai reçu une déclaration de problème pour créer un jeu Battleship en java.

Mon code de travail (Spring Boot + Web) est placé ici avec lénoncé du problème. https://github.com/ankidaemon/BattleShip

Cette question est principalement axée sur la conception, aidez-moi à comprendre comment puis-je le découpler et appliquer des modèles de conception appropriés.

StartGame.java – être appelé depuis le contrôleur

@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; } } 

Exemple dentrée

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 

Règles
1. Player1 se déclenchera en premier. Chaque joueur aura une autre chance jusquà (hit == réussi).
2. Les cuirassés seront placés horizontalement.
3. Un vaisseau de type Q nécessite 2 missiles touchés pour être détruit.
4. Un vaisseau de type P nécessite 1 missile pour être détruit.

Entrée
La première ligne de lentrée contient les dimensions de la zone de combat dont la largeur et la hauteur sont séparées par un espace.
La deuxième ligne aura le nombre (B) de cuirassés de chaque joueur.
Ensuite, dans la ligne suivante type de cuirassé, les dimensions (largeur et hauteur) & positions (coordonnée Y et coordonnée X) pour le joueur 1 puis pour le joueur 2 seront donné séparés par un espace.
Ensuite, dans la ligne suivante, la séquence du joueur 1 (séparée par un espace) des coordonnées de lemplacement de la cible des missiles (Y et X) sera donnée, puis pour la séquence du joueur 2.

Contraintes:
1 < = Largeur de la zone de combat (M) < = 9
A < = Hauteur de la zone de combat (N ) < = Z
1 < = Nombre de cuirassés < = M * N
Type de navire = {P, Q}
1 < = Largeur du cuirassé < = M
A < = Hauteur du cuirassé < = N
1 < = Coordonnée X du navire < = M
A < = Coordonnée Y du navire < = N

Commentaires

  • Vous pouvez ajouter les règles du jeu, et les commentaires au moins dans les parties les plus importantes du code. Vous pouvez également déplacer ces parties critiques du code vers des sous-routines pour rendre le code beaucoup plus lisible, même si appeler autant de fonctions rendrait le code plus lent, mais au moins il deviendra beaucoup plus lisible pour les autres afin quils puissent plus tard maintenir mieux les mises à jour . Vous pouvez ajouter le code original et un code plus modulaire / commenté accompagné des règles du jeu, en basant peut-être les commentaires sur ces règles.
  • @ alt.126 ont ajouté des règles et des entrées. StartGame lit lentrée dun fichier, effectue la validation et crée BattleArea et BattleShips pour chaque joueur. Les BattleShips sont des POJO. BattleArea a des méthodes pour placer des vaisseaux et des fileMissiles en fonction des règles. Thnx

Réponse

ok, mettons la main sur:

Nom de classe pour votre StartGame nest pas utile, renommez-le en un nom plus correspondant, je pense comme BattleShipGame et démarrez le jeu à la place depuis votre manette

BattleShipGame game = new BattleShipGame(); game.start(); 

le init – la méthode est beaucoup trop grande et elle ne fait pas init mais fait encore plus de choses … alors décomposons cela un peu:

  • init devrait renvoie un booléen (ou un Result) qui indique que linitialisation a réussi.
  • init ressemble à « une méthode déléguée ce qui signifie il devrait y avoir très peu de logique insde – au lieu de cela, il est utile de mettre la plupart du travail dans les méthodes
  • simplement lancer les choses et ne pas faire autre chose
  • utiliser Player objets …
  • déplacer la logique du jeu hors de la méthode

cela pourrait ressembler à ceci alors

REMARQUE: la méthode init pourrait être beaucoup plus raccourcie, mais je pense que je souligne dune bonne manière ce que init devrait vraiment faire …

comme mentionné ci-dessus, vous avez déplacé la logique du jeu hors de votre méthode init et lavez mise dans la méthode 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; } 

le BattleShipGame commencerait maintenant de cette manière:

public void start(){ init(); Result result = playGame(); ... //do whatever you want with your result - for example print it into the console } 

pour votre BattleShip, il y a dautres problèmes dont on peut parler. Je pense que cétait une très bonne idée dutiliser une classe Coordinate qui a lair bien à première vue. Mais vous ne lutilisez pas de manière conséquente. Pensez à ce que ce serait si vous utilisiez Coordinate pour votre vaisseau au lieu de int[] qui ferait votre code est également plus facile à lire et les calculs seront beaucoup plus faciles. Et nutilisez pas de char pour votre type de navire, utilisez plutôt une énumération. Mais soyons honnêtes, vous n’avez pas de «position, largeur et hauteur», ce que vous avez vraiment est un rectangle – alors utilisez un rectangle!

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

la dimension du Rectangle (largeur / hauteur) et le nombre de points de vie peuvent être déterminés par le 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(); } } 

Le BattleArea est maintenant beaucoup plus facile à utilisez, pensez à la simplicité avec laquelle vous pouvez placeShips maintenant:

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

tout le code ci-dessus na pas été compilé donc là peut être des fautes dorthographe et de nombreuses méthodes ne sont même pas encore implémentées (comme Rectangle.contains() ou autres).

résumé

mais laissez « Regardez ce que nous avons maintenant:

  • vous pouvez changer le type de navire assez facilement sans modifier tout code !!! (il vous suffit dajouter un autre type de bateau dans ShipType)
  • vous avez réduit la complexité de votre code très loin, vous ne « Je nai pas de calcul dangereux
  • vous avez des préoccupations distinctes, les objets font maintenant ce quils sont censés faire
  • vous pouvez facilement changer votre code pour un autre joueur (partie à trois joueurs)
  • vous pouvez tester votre code maintenant

Commentaires

  • eh bien je sais quil y a encore tellement de problèmes ici que je ne pourrais pas ‘ t les gérer tous – cette réponse était sur la vue tout en haut sans entrer dans les détails

Réponse

Pour découpler, vous devez vous assurer que vos fonctions nont pas besoin de sappeler pour faire leur travail. Si possible, les seules fonctions qui devraient en appeler dautres sont les fonctions de pilote de chaque sous-système qui sont destinées à être appelées comme linterface API.

Pensez à la façon dont vous auriez besoin de porter beaucoup de code juste pour ajouter une fonction utile si cette fonction appelle dautres fonctions ou utilise des variables dautres grands sous-systèmes. Si vous faites un effort supplémentaire pour implémenter cette fonction de manière à ce quelle ne dépende de rien dautre, même si cela ressemble à du code dupliqué, vous pourrez la porter individuellement vers un autre programme, et même plus si vous ne le faites pas dépendre des fonctionnalités du langage ou des fonctionnalités de la bibliothèque qui ne sont pas présentes sur chaque compilateur et langage de programmation que vous utilisez pour rendre possible le portage de tout code que vous écrivez dans nimporte quel environnement dont vous avez besoin , cest quoi s appelé découplage .

Comme vous pouvez le voir, le découplage peut être appliqué au niveau du compilateur, du langage, de lenvironnement système, des fonctions et du sous-système. Cela peut impliquer la duplication du code et la réécriture pour avoir des routines autonomes sans dépendances. Cela peut également impliquer lutilisation de fonctionnalités plus largement standardisées pour rendre le code plus portable, et il peut également être nécessaire que vous travailliez vous-même à limplémentation ou au portage des fonctionnalités manquantes dans tous vos environnements de programmation / système afin que quel que soit le système / langage / compilateur que vous utiliser, vous aurez toujours la même fonctionnalité disponible.





À propos des modèles de conception.

Si vous voulez rendre votre code hautement réutilisable et si vous voulez que cela dure des décennies, vous pouvez utiliser lapproche de bas niveau de la programmation dassemblage de CPU.

Pensez à une tâche ou une microtâche que vous souhaitez effectuer dune manière qui prendra toujours le même type de paramètres et qui retournera toujours un résultat exactement de la même manière.

Ensuite, donnez-lui un nom TRÈS spécifique à cette routine. Ce sera un opcode, une instruction, implémentée comme une fonction / sous-routine, que vous pourrez réutiliser comme nimporte quelle autre instruction CPU native. Cette façon de concevoir le code est hautement réutilisable et stable. Si vous voulez une variante de ce quil faut faire, il vous suffit dajouter une nouvelle fonction opcode au lieu de détruire la fonctionnalité valide précédente.

Lapplication de cela dans tout le programme comme approche de conception principale peut rendre le code encore plus strictement structuré plus facile à suivre.

Commentaires

  • Merci, @ alt.126. Bien quil sagisse dune solution généralisée (+1), jespérais en fait plus sur les modèles de conception Java / créer moins dobjet / supprimer le code redondant, etc.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *