Java-Schlachtschiff-Spiel

Ich habe eine Problembeschreibung zum Erstellen eines Schlachtschiff-Spiels in Java erhalten.

Mein Arbeitscode (Spring Boot + Web) wird hier zusammen mit der Problemstellung platziert. https://github.com/ankidaemon/BattleShip

Diese Frage konzentriert sich hauptsächlich auf das Design. Bitte helfen Sie mir herauszufinden, wie Kann ich es entkoppeln und geeignete Entwurfsmuster anwenden?

StartGame.java – Aufruf vom Controller

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

Beispieleingabe

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 

Regeln
1. Spieler1 wird zuerst ausgelöst. Jeder Spieler erhält eine weitere Chance bis (Treffer == erfolgreich).
2. Schlachtschiffe werden horizontal platziert.
3. Typ-Q-Schiff benötigt 2 Treffer, um zerstört zu werden.
4. Typ-P-Schiff benötigt 1 Raketentreffer, um zerstört zu werden.

Geben Sie Die erste Zeile der Eingabe enthält Dimensionen des Schlachtfelds, deren Breite und Höhe durch Leerzeichen getrennt sind.
Die zweite Zeile enthält die Anzahl (B) der Schlachtschiffe, die jeder Spieler hat.
Dann werden in der nächsten Zeile Schlachtschifftyp Abmessungen (Breite und Höhe) & Positionen (Y-Koordinate und X-Koordinate) für Spieler-1 und dann für Spieler-2 sein durch Leerzeichen getrennt gegeben.
Und dann wird in der nächsten Zeile die durch das Leerzeichen getrennte Sequenz von Raketen-1 der Raketen die Zielortkoordinaten (Y und X) und dann die Sequenz für Spieler-2 angegeben.

Einschränkungen:
1 < = Breite des Schlachtfelds (M) < = 9
A < = Höhe des Schlachtfelds (N. ) < = Z
1 < = Anzahl der Schlachtschiffe < = M. * N
Schiffstyp = {P, Q}
1 < = Breite des Schlachtschiffs < = M
A < = Höhe des Schlachtschiffs < = N
1 < = X-Koordinate des Schiffes < = M
A < = Y-Koordinate des Schiffs < = N

Kommentare

  • Sie können die Spielregeln und die Kommentare hinzufügen Zumindest in den wichtigsten Teilen des Codes. Sie können diese kritischen Teile des Codes auch in Unterprogramme verschieben, um den Code besser lesbar zu machen, obwohl das Aufrufen so vieler Funktionen den Code langsamer machen würde, aber zumindest für andere viel lesbarer wird, damit sie die Aktualisierungen später besser verwalten können . Sie können den Originalcode und einen modulareren / kommentierten Code hinzufügen, der von den Spielregeln begleitet wird, wobei die Kommentare möglicherweise auf diesen Regeln basieren.
  • @ alt.126 haben Regeln und Eingaben hinzugefügt. StartGame liest die Eingabe aus einer Datei, führt eine Validierung durch und erstellt BattleArea und BattleShips für jeden Spieler. Schlachtschiffe sind POJO. BattleArea verfügt über Methoden zum Platzieren von Schiffen und FileMissiles basierend auf den Regeln. Danke

Antwort

OK, lassen Sie uns die Hände auflegen:

Klassenname für Ihr StartGame ist nicht hilfreich. Benennen Sie es in einen passenderen Namen um. Ich denke, BattleShipGame und Starten Sie das Spiel stattdessen von Ihrem Controller aus.

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

init – Methode ist viel zu groß und macht kein init , aber noch mehr … also lassen Sie uns das ein wenig aufschlüsseln:

  • init sollte Geben Sie einen Booleschen Wert (oder einen Result) zurück, der angibt, dass init erfolgreich war.
  • init sieht so aus, als ob es sich um eine „delegierte Methode“ handelt Es sollte sehr wenig Logik geben – stattdessen ist es nützlich, die meiste Arbeit in Methoden zu stecken.
  • Initiieren Sie einfach Dinge und tun Sie keine anderen Dinge.
  • Verwenden Sie Player Objekte …
  • verschieben die Spielelogik aus der Methode

es könnte dann so aussehen

HINWEIS: Die init-Methode könnte weitaus kürzer sein, aber ich denke, ich weise auf eine gute Weise darauf hin, was init wirklich sollte do …

Wie oben erwähnt, haben Sie die Spielelogik aus Ihrer Init-Methode entfernt und in die playGame() -Methode eingefügt.

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

Die BattleShipGame würde jetzt folgendermaßen beginnen:

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

Für Ihr BattleShip gibt es einige weitere Probleme, über die gesprochen werden kann. Ich denke, es war eine sehr gute Idee, eine Klasse Coordinate zu verwenden, die auf den ersten Blick gut aussieht. Aber Sie verwenden es nicht konsequent. Überlegen Sie, wie es wäre, wenn Sie Coordinate für Ihr Schiff anstelle von int[] verwenden würden Ihr Code ist auch einfacher zu lesen und die Mathematik wäre viel einfacher. Verwenden Sie für Ihren Shiptype kein char, sondern eine Aufzählung. Aber seien wir ehrlich, Sie haben keine „Position, Breite und Höhe“, was Sie wirklich haben, ist ein Rechteck – verwenden Sie also ein Rechteck!

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

Die Abmessung des Rechtecks (Breite / Höhe) und die Anzahl der Lebenspunkte können durch den Schiffstyp bestimmt werden.

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

Die BattleArea ist jetzt viel einfacher zu erreichen Überlegen Sie, wie einfach Sie placeShips jetzt verwenden können:

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

Der gesamte obige Code wurde dort nicht kompiliert Möglicherweise gibt es einige Rechtschreibfehler und viele Methoden sind noch nicht einmal implementiert (wie Rectangle.contains() oder andere).

Zusammenfassung

, aber lassen Sie Schauen Sie sich an, was wir jetzt haben:

  • Sie können den Schiffstyp ganz einfach ändern, ohne eine Code !!! (Sie müssen einfach einen weiteren Shiptype in ShipType hinzufügen)
  • Sie haben die Komplexität Ihres Codes sehr weit reduziert, Sie tun es nicht Ich habe keine gefährliche Berechnung Wenn Sie Bedenken haben, tun die Objekte jetzt das, was sie tun sollen.
  • Sie können Ihren Code leicht für einen anderen Spieler ändern (Drei-Spieler-Spiel).
  • Sie könnten Ihren Code jetzt testen.

Kommentare

  • Nun, ich weiß, dass es hier immer noch so viele Probleme gibt, dass ich nicht ‚ nicht alle behandeln – diese Antwort befand sich in der Draufsicht und ging nicht auf Details ein.

Antwort

Um sich zu entkoppeln, müssen Sie sicherstellen, dass sich Ihre Funktionen nicht gegenseitig anrufen müssen, um ihre Arbeit zu erledigen. Wenn möglich, sollten nur die Treiberfunktionen jedes Subsystems aufgerufen werden, die wie die API-Schnittstelle aufgerufen werden sollen.

Überlegen Sie, wie Sie viel Code nur zum Hinzufügen portieren müssten Eine nützliche Funktion, wenn diese Funktion andere Funktionen aufruft oder Variablen von anderen großen Subsystemen verwendet. Wenn Sie zusätzliche Anstrengungen unternehmen, um diese Funktion so zu implementieren, dass sie von nichts anderem abhängt, auch wenn sie wie duplizierter Code aussieht, können Sie sie einzeln auf ein anderes Programm portieren, und noch mehr, wenn Sie dies tun Lassen Sie es nicht von Sprach- oder Bibliotheksfunktionen abhängen, die nicht in jedem einzelnen Compiler und jeder Programmiersprache vorhanden sind, die Sie verwenden, um den von Ihnen geschriebenen Code in eine von Ihnen benötigte Umgebung zu portieren. s wird als Entkopplung bezeichnet.

Wie Sie sehen, kann die Entkopplung auf Compiler-, Sprach-, Systemumgebung-, Funktions- und Subsystemebene angewendet werden. Möglicherweise müssen Sie Code duplizieren und neu schreiben, um eigenständige Routinen ohne Abhängigkeit zu erhalten. Dies kann auch bedeuten, dass allgemein standardisierte Funktionen verwendet werden, um den Code portabler zu machen, und dass Sie möglicherweise daran arbeiten, fehlende Funktionen selbst in alle Ihre Programmier- / Systemumgebungen zu implementieren oder zu portieren, damit Sie unabhängig von System / Sprache / Compiler sind Bei Verwendung steht Ihnen immer dieselbe Funktionalität zur Verfügung.





Informationen zu Entwurfsmustern.

Wenn Sie Ihren Code in hohem Maße wiederverwendbar machen möchten Wenn Sie möchten, dass es jahrzehntelang anhält, können Sie den Low-Level-Ansatz der CPU-Assembly-Programmierung verwenden.

Überlegen Sie sich eine Aufgabe oder Mikrotask, die Sie auf eine Weise ausführen möchten, die immer dieselbe Art von Aufgabe hat Parameter und das gibt immer ein Ergebnis auf genau die gleiche Weise zurück.

Geben Sie dieser Routine dann einen SEHR spezifischen Namen. Dies ist ein Opcode, eine Anweisung, die als Funktion / Unterroutine implementiert ist und die Sie wie jede andere native CPU-Anweisung wiederverwenden können. Diese Art des Entwurfs des Codes ist in hohem Maße wiederverwendbar und stabil. Wenn Sie eine Variation in der Vorgehensweise wünschen, fügen Sie einfach eine neue Opcode-Funktion hinzu, anstatt die zuvor gültige Funktionalität zu zerstören.

Wenn Sie dies im gesamten Programm als Hauptentwurfsansatz anwenden, kann der Code noch strenger strukturiert werden einfacher zu folgen.

Kommentare

  • Vielen Dank, @ alt.126. Während dies eine verallgemeinerte Lösung (+1) ist, hoffte ich tatsächlich mehr auf Java-Entwurfsmuster / weniger Objekte erstellen / redundanten Code entfernen usw.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.