Java Battleship-spil

Jeg har fået en problemopgave for at oprette et Battleship-spil i java.

Min arbejdskode (Spring Boot + Web) er placeret her sammen med problemangivelsen. https://github.com/ankidaemon/BattleShip

Dette spørgsmål er stort set fokuseret på design, vær venlig at hjælpe mig med at finde ud af, hvordan kan jeg få det afkoblet og anvende egnede designmønstre.

StartGame.java – kaldes fra 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; } } 

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

Eksempelindgang

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 

Regler
1. Player1 fyrer først. Hver spiller får en ny chance indtil (hit == vellykket).
2. Slagskibe placeres vandret.
3. Type-Q skib kræver 2 missiler ramt for at blive ødelagt.
4. Type-P-skib kræver 1 missilhit for at blive ødelagt.

Indtast
Første linje i inputet indeholder dimensioner af kampområdet med bredde og højde adskilt af plads.
Anden linje vil have antal (B) slagskibe, som hver spiller har.
Derefter i den næste linje slagskibstype vil dimensioner (bredde og højde) & positioner (Y-koordinat og X-koordinat) for Player-1 og derefter til Player-2 være givet adskilt af plads.
Og i den næste linje gives Player-1 sekvens (adskilt af mellemrum) af missiler målplaceringskoordinater (Y og X) og derefter til sekvens for Player-2.

Begrænsninger:
1 < = Bredden af kampområdet (M) < = 9
A < = Kampens område (N ) < = Z
1 < = Antal slagskibe < = M * N
Type skib = {P, Q}
1 < = Slagskibets bredde < = M
A < = Slagskibshøjde < = N
1 < = X-koordinat for skib < = M
A < = Y-koordinat for skib < = N

Kommentarer

  • Du kan tilføje spillereglerne og kommentarerne i det mindste i de vigtigste dele af koden. Du kan også flytte disse kritiske dele af koden til underrutiner for at gøre koden meget mere læselig, selvom det at kalde så mange funktioner ville gøre koden langsommere, men i det mindste bliver den meget mere læsbar for andre, så de senere kan opretholde opdateringerne bedre . Du kan tilføje den originale kode og en mere modulær / kommenteret kode ledsaget af spillereglerne, måske basere kommentarerne på disse regler.
  • @ alt.126 har tilføjet regler og input. StartGame læser input fra en fil, foretager validering og opretter BattleArea og BattleShips for hver spiller. BattleShips er POJO. BattleArea har metoder til at placere skibe og fileMissiles baseret på reglerne. Thnx

Svar

ok lad os lægge hænder på:

Klassenavn til din StartGame er ikke nyttigt, omdøb det til et mere matchende navn, jeg synes ligesom BattleShipGame og start i stedet for fra din controller

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

init – metoden er langt for stor, og den gør ingen init men gør endnu flere ting … så lad os nedbryde det lidt:

  • init skal returner en boolsk (eller en Result), der angiver, at init var vellykket.
  • init ser ud som det “sa delegatmetode hvilket betyder der skal være meget lidt logisk ind – i stedet er det nyttigt at lægge mest arbejde på metoder
  • bare starte ting og ikke gøre andre ting
  • brug Player objekter …
  • flytter spillogikken ud af metoden

det kunne se sådan ud

BEMÆRK: init-metoden kunne forkortes langt mere, men jeg tror, jeg påpeger på en god måde, hvad init virkelig skal gør …

som nævnt ovenfor har du flyttet spillogikken ud af din init-metode og lagt den i playGame() -metoden.

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 ville starte nu på denne måde:

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

til dit BattleShip er der nogle flere udgaver, som du kan tale om. Jeg synes, det var en meget god idé at bruge en klasse Coordinate, der ser godt ud ved første øjekast. Men du bruger det ikke konsekvent. Tænk på, hvordan det ville være, hvis du brugte Coordinate til dit skib i stedet for int[], der ville gøre din kode er også lettere at læse, og matematikken ville være meget lettere. Og brug ikke en char til din skibstype, brug en enum i stedet. Men lad os være ærlige, du har ikke en “position og bredde og højde”, hvad du virkelig har er et rektangel – så brug et rektangel!

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

dimensionen af rektanglet (bredde / højde) og mængden af livspoint kan bestemmes af 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 er nu langt lettere at brug, tænk på, hvor simpelt du kan placeShips nu:

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

al kode ovenfor er ikke blevet kompileret, så der kan være nogle stavefejl, og mange metoder er ikke engang implementeret endnu (som Rectangle.contains() eller andre).

resume

men lad “se på hvad vi har nu:

  • du kan ændre skibstypen ganske let uden at ændre nogen kode !!! (du skal blot tilføje en anden skibstype i ShipType)
  • du har reduceret kompleksiteten af din kode meget langt, du don “t har farlig beregning ioner.
  • du har adskilte bekymringer, objekterne gør nu, hvad de skal gøre
  • du kan let ændre din kode til en anden spiller (tre-spiller spil)
  • du kunne teste din kode nu

Kommentarer

  • godt jeg ved, at der stadig er så mange problemer her, jeg kunne ikke ‘ t håndterer dem alle – dette svar var helt ovenfra og kom ikke i detaljer

Svar

For at afkoble skal du sørge for, at dine funktioner ikke behøver at ringe til hinanden for at udføre deres arbejde. Hvis det er muligt, er de eneste funktioner, der skal kalde andre, driverfunktionerne i hvert delsystem, der er beregnet til at blive kaldt som API-grænsefladen.

Tænk over, hvordan du har brug for at portere en masse kode bare for at tilføje en nyttig funktion, hvis den funktion kalder på andre funktioner eller bruger variabler fra andre store undersystemer. Hvis du gør en ekstra indsats for at implementere denne funktion på en måde, som den ikke afhænger af noget andet, selvom det ligner duplikeret kode, vil du være i stand til individuelt at porte den til et andet program, og endnu mere, hvis du lad det ikke afhænge af sprogfunktioner eller biblioteksfunktioner, der ikke er til stede på hver enkelt kompilator og programmeringssprog, du bruger, for at gøre det muligt at porte enhver kode, du skriver, til ethvert miljø, du har brug for , det er hvad ” s kaldes afkobling .

Som du kan se, kan afkobling anvendes på compiler, sprog, systemmiljø, funktioner og undersystemniveauer. Det kan involvere duplikering af kode og omskrivning for at have selvstændige, afhængighedsløse rutiner. Det kan også antyde, at du bruger mere bredt standardiserede funktioner til at gøre koden mere bærbar, og det kan også være nødvendigt, at du arbejder på at implementere eller portere manglende funktionalitet til alle dine programmerings- / systemmiljøer selv, så uanset hvilket system / sprog / kompilator du har brug, vil du altid have den samme funktionalitet tilgængelig.





Om designmønstre.

Hvis du vil gøre din kode meget genanvendelig og hvis du vil have det til at vare i årtier, kan du bruge den lave tilgang til programmering af CPU-samling.

Tænk på en opgave eller mikrotask, du vil udføre på en måde, der altid tager den samme type parametre, og det vil altid returnere et resultat på nøjagtig samme måde.

Giv det derefter et MEGET specifikt navn til denne rutine. Dette vil være en opcode, en instruktion, implementeret som en funktion / subrutine, som du kan genbruge ligesom enhver anden native CPU-instruktion. Denne måde at designe koden på er meget genanvendelig og stabil. Hvis du vil have en variation i hvad du skal gøre, skal du bare tilføje en ny opcode-funktion i stedet for at ødelægge den tidligere gyldige funktionalitet.

Anvendelse af dette i hele programmet som den vigtigste designtilgang kan gøre koden mere strengt struktureret endnu lettere at følge.

Kommentarer

  • Tak, @ alt.126. Mens dette er en generaliseret løsning (+1), håbede jeg faktisk mere på Java-designmønstre / lavede mindre objekt / fjern overflødig kode osv.

Skriv et svar

Din e-mailadresse vil ikke blive publiceret. Krævede felter er markeret med *