Java Battleship-spill

Jeg har fått en problemstilling for å lage et Battleship-spill i java.

Arbeidskoden min (Spring Boot + Web) er plassert her sammen med problemstillingen. https://github.com/ankidaemon/BattleShip

Dette spørsmålet er hovedsakelig fokusert på design. Hjelp meg å finne ut hvordan kan jeg gjøre det frakoblet og bruke passende designmønstre.

StartGame.java – ringe fra kontrolleren

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

Eksempelinngang

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. Spiller1 fyrer først. Hver spiller får en ny sjanse til (hit == vellykket).
2. Slagskip blir plassert horisontalt.
3. Type-Q-skip krever 2 raketter som er truffet for å bli ødelagt.
4. Type-P-skip krever 1 rakettslag for å bli ødelagt.

Inngang
Første linje i inngangen inneholder dimensjoner på kampområdet med bredde og høyde atskilt med mellomrom.
Andre linje vil ha antall (B) slagskip hver spiller har.
Så i neste linje slagskiptype, vil dimensjoner (bredde og høyde) & posisjoner (Y-koordinat og X-koordinat) for spiller-1 og deretter for spiller-2 være gitt atskilt med mellomrom.
Og i neste linje vil Player-1-sekvensen (atskilt med mellomrom) av missiler bli gitt målkoordinater (Y og X) og deretter for sekvens for Player-2.

Begrensninger:
1 < = Bredde på slagområdet (M) < = 9
A < = Kampens område (N ) < = Z
1 < = Antall slagskip < = M * N
Type skip = {P, Q}
1 < = Slagskipets bredde < = M
A < = Slagskipets høyde < = N
1 < = X-koordinat for skip < = M
A < = Y-koordinat for skip < = N

Kommentarer

  • Du kan legge til spillereglene og kommentarene i det minste i de viktigste delene av koden. Du kan også flytte de kritiske delene av koden til underrutiner for å gjøre koden mye mer lesbar, selv om å ringe så mange funksjoner vil gjøre koden langsommere, men i det minste vil den bli mye mer lesbar for andre, slik at de senere kan opprettholde bedre oppdateringene. . Du kan legge til den opprinnelige koden og en mer modulær / kommentert kode ledsaget av spillereglene, kanskje basere kommentarene på disse reglene.
  • @ alt.126 har lagt til regler og innspill. StartGame leser innspill fra en fil, gjør validering og oppretter BattleArea og BattleShips for hver spiller. BattleShips er POJO. BattleArea har metoder for å plassere skip og fileMissiles basert på reglene. Thnx

Svar

ok kan sette hendene på:

Klassenavn for din StartGame er ikke nyttig, endre navnet på det til et mer samsvarende navn, jeg tror som BattleShipGame og start i stedet fra kontrolleren din

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

init – metoden er langt for stor og den gjør ingen init men gjør enda flere ting … så la oss bryte det litt ned:

  • init skal returner en boolsk (eller en Result) som indikerer at init var vellykket.
  • init ser ut som den «sa delegatmetode som betyr det skal være veldig lite logisk – i stedet er det nyttig å legge mest arbeid i metoder
  • bare starte ting og ikke gjøre noen andre ting
  • bruk Player objekter …
  • flytter spilllogikken ut av metoden

det kan se slik ut da

MERK: init-metoden kan forkortes langt mer, men jeg tror jeg påpeker på en god måte hva init egentlig burde gjør …

som nevnt ovenfor, har du flyttet spilllogikken fra init-metoden din og satt 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 nå på denne måten:

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

for BattleShip er det noen flere utgaver som det kan snakkes om. Jeg synes det var en veldig god idé å bruke en klasse Coordinate som ser bra ut ved første øyekast. Men du bruker det ikke konsekvent. Tenk på hvordan det ville vært hvis du brukte Coordinate for at du sendte i stedet for int[] som ville gjort koden din også lettere å lese og matematikken ville være mye lettere. Og ikke bruk en char for skipstypen din, bruk en enum i stedet. Men la oss være ærlige, du har ikke en «posisjon og bredde og høyde». Det du egentlig har er et rektangel – så bruk 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(); } ... } 

Rektangelets dimensjon (bredde / høyde) og mengden av livspoeng kan bestemmes av 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 nå langt enklere å bruk, tenk på hvor enkelt du kan placeShips nå:

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

all kode ovenfor er ikke kompilert så der kan være noen stavefeil, og mange metoder er ikke engang implementert ennå (som Rectangle.contains() eller andre).

sammendrag

men la «se på hva vi har nå:

  • du kan endre skipstypen ganske enkelt uten å endre noen -kode !!! (du må ganske enkelt legge til en annen skipstype i ShipType)
  • du har redusert kodenes kompleksitet veldig langt, du trenger ikke «t har farlig beregning ioner.
  • du har separate bekymringer, objektene gjør nå det de skal gjøre
  • du kan enkelt endre koden for en annen spiller (tre-spillerspill)
  • du kan teste koden din nå

Kommentarer

  • vel, jeg vet det fortsatt er så mange problemer her at jeg ikke kunne ‘ t håndtere dem alle – dette svaret var helt ovenfra og kom ikke inn i noen detaljer

Svar

For å koble fra må du sørge for at funksjonene dine ikke trenger å ringe hverandre for å gjøre jobben sin. Hvis det er mulig, er de eneste funksjonene som skal kalle andre driverfunksjonene til hvert delsystem som er ment å kalles som API-grensesnittet.

Tenk på hvordan du trenger å portere mye kode bare for å legge til en nyttig funksjon hvis den funksjonen kaller andre funksjoner eller bruker variabler fra andre store delsystemer. Hvis du gjør ytterligere innsats for å implementere den funksjonen på en måte som den ikke avhenger av noe annet, selv om det ser ut som duplisert kode, vil du kunne portere den til et annet program, og enda mer hvis du ikke la det avhenge av språkfunksjoner eller biblioteksfunksjoner som ikke er tilstede på hver enkelt kompilator og programmeringsspråk du bruker for å gjøre det mulig å portere hvilken som helst kode du skriver til ethvert miljø du trenger , det er hva » kalles frakobling .

Som du kan se, kan frakobling brukes på kompilator, språk, systemmiljø, funksjoner og undersystemnivåer. Det kan innebære duplisering av kode og omskriving for å ha frittstående, avhengighetsløse rutiner. Det kan også innebære å bruke mer standardiserte funksjoner for å gjøre koden mer bærbar, og det kan også trenge at du jobber med å implementere eller portere manglende funksjonalitet til alle programmerings- / systemmiljøene dine, slik at uansett system / språk / kompilator du bruk, vil du alltid ha samme funksjonalitet tilgjengelig.





Om designmønstre.

Hvis du vil gjøre koden din veldig gjenbrukbar og hvis du vil at den skal vare i flere tiår, kan du bruke lavt nivå tilnærming til programmering av CPU-montering.

Tenk på en oppgave eller mikrotask du vil utføre på en måte som alltid vil ta samme type parametere, og som alltid vil gi et resultat på nøyaktig samme måte.

Deretter gir det et VELDIG spesifikt navn til denne rutinen. Dette vil være en opcode, en instruksjon, implementert som en funksjon / underrutine, som du kan gjenbruke akkurat som alle andre native CPU-instruksjoner. Denne måten å designe koden på er svært gjenbrukbar og stabil. Hvis du vil ha en variasjon i hva du skal gjøre, legger du bare til en ny opcode-funksjon i stedet for å ødelegge den forrige gyldige funksjonaliteten.

Hvis du bruker dette gjennom hele programmet som hoveddesigntilnærming, kan koden bli strengere strukturert ennå lettere å følge.

Kommentarer

  • Takk, @ alt.126. Selv om dette er en generalisert løsning (+1), håpet jeg faktisk mer på Java-designmønstre / å lage mindre objekt / fjerne overflødig kode osv.

Legg igjen en kommentar

Din e-postadresse vil ikke bli publisert. Obligatoriske felt er merket med *