Hra Java Battleship

Dostal jsem prohlášení o problému, abych vytvořil hru Battleship v Javě.

Zde je umístěn můj pracovní kód (Spring Boot + Web) spolu s prohlášením o problému. https://github.com/ankidaemon/BattleShip

Tato otázka se zaměřuje hlavně na design, pomozte mi zjistit, jak mohu to oddělit a použít vhodné návrhové vzory.

StartGame.java – volání z řadiče

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

Ukázkový vstup

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 

Pravidla
1. Nejprve vystřelí hráč 1. Každý hráč dostane další šanci do (zásah == úspěšný).
2. Bitevní lodě budou umístěny vodorovně.
3. Loď typu Q vyžaduje 2 zasažené rakety, aby byla zničena.
4. Loď typu P vyžaduje ke svému zničení 1 zásah rakety.

Vstup
První řádek vstupu obsahuje rozměry bitevní oblasti se šířkou a výškou oddělenou mezerou.
Druhá řada bude mít počet (B) bitevních lodí, které má každý hráč.
Pak v dalším typu bitevní lodi budou rozměry (šířka a výška) & pozice (souřadnice Y a souřadnice X) pro hráče 1 a poté pro hráče 2 dané oddělené mezerou.
A pak v další řadě bude uvedena sekvence hráčů 1 (oddělená mezerou) souřadnic cílového umístění raket (Y a X) a poté pro sekvenci pro hráče-2.

Omezení:
1 < = Šířka oblasti bitvy (M) < = 9
A < = Výška oblasti bitvy (N ) < = Z
1 < = počet bitevních lodí < = M * N
Typ lodi = {P, Q}
1 < = Šířka bitevní lodi < = M
A < = výška bitevní lodi < = N
1 < = X souřadnice lodi < = M
A < = Y souřadnice lodi < = N

Komentáře

  • Můžete přidat pravidla hry a komentáře alespoň v nejdůležitějších částech kódu. Tyto kritické části kódu můžete také přesunout do podprogramů, aby byl kód mnohem čitelnější, ačkoli volání tolika funkcí by kód zpomalilo, ale přinejmenším se stane mnohem čitelnějším pro ostatní, aby si mohli později udržovat lepší aktualizace . Můžete přidat původní kód a více modulární / komentovaný kód doprovázený pravidly hry, možná založit komentáře na těchto pravidlech.
  • @ alt.126 přidali pravidla a vstup. StartGame načte vstup ze souboru, provede ověření a vytvoří BattleArea a BattleShips pro každého hráče. BattleShips jsou POJO. BattleArea má metody umisťování lodí a fileMissiles na základě pravidel. Thnx

Odpověď

ok, necháme ruce:

Název třídy pro StartGame není užitečné, přejmenujte jej na shodnější název, myslím, že jako BattleShipGame a místo toho spusťte hru z vašeho ovladače

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

the init – metoda je daleko velká a nedělá init , ale dělá ještě více věcí … tak to trochu rozeberme:

  • init by měl vrátit logickou hodnotu (nebo Result), která označuje, že init byl úspěšný.
  • init vypadá jako „sa delegate methode což znamená Mělo by to být velmi málo logické insade – místo toho je užitečné dát většinu práce do metod
  • pouze iniciovat věci a nedělat žádné jiné věci
  • používat Player objekty …
  • přesunout logiku hry z metody

mohlo by to vypadat takto

POZNÁMKA: metoda init může být zkrácena mnohem více, ale myslím, že v dobrém naznačím, co by init mělo skutečně být dělat …

jak bylo uvedeno výše, přesunuli jste logiku hry z vaší metody init a vložili ji do metody 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; } 

BattleShipGame by teď začal takto:

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

pro vaši BattleShip existuje několik dalších problémů, o kterých se dá mluvit. Myslím, že byl velmi dobrý nápad použít třídu Coordinate, která na první pohled vypadá dobře. Ale nepoužíváte to důsledně. Přemýšlejte o tom, jaké by to bylo, kdybyste použili Coordinate pro svou loď místo int[], které by váš kód se také snáze čte a matematika by byla mnohem jednodušší. A nepoužívejte pro svůj typ lodi char, místo toho použijte výčet. Ale buďme upřímní, nemáte „pozici a šířku a výšku“, to, co ve skutečnosti máte, je obdélník – použijte tedy obdélník!

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

rozměr obdélníku (šířka / výška) a počet životních bodů lze určit pomocí 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 je nyní mnohem snazší použijte, přemýšlejte o tom, jak jednoduše můžete placeShips nyní:

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

celý výše uvedený kód nebyl sestaven, takže zde mohou to být některé pravopisné chyby a mnoho metod ještě není ani implementováno (například Rectangle.contains() nebo jiné).

shrnutí

Podívejte se na to, co teď máme:

  • typ lodi můžete snadno změnit bez úpravy jakékoli kód !!! (jednoduše musíte přidat další typ lodi v ShipType)
  • velmi jste snížili složitost svého kódu, nemusíte „Nemám nebezpečný výpočet ionty.
  • máte oddělené obavy, objekty nyní dělají to, co mají dělat
  • můžete snadno změnit svůj kód pro jiného hráče (hra pro tři hráče)
  • svůj kód můžete nyní otestovat

komentáře

  • vím, že zde stále existuje tolik problémů, že jsem nemohl ‚ nezvládáme je všechny – tato odpověď byla úplně nahoře a nedostala se do žádných podrobností

odpověď

Chcete-li oddělení zrušit, musíte se ujistit, že vaše funkce si pro svou práci nemusí navzájem volat. Pokud je to možné, jediné funkce, které by měly volat ostatní, jsou funkce ovladače každého subsystému, které se mají nazývat jako rozhraní API.

Přemýšlejte o tom, jak byste museli přenést spoustu kódu jen pro přidání užitečná funkce, pokud tato funkce volá jiné funkce nebo používá proměnné z jiných velkých subsystémů. Pokud vynaložíte další úsilí na implementaci této funkce způsobem, který nezávisí na ničem jiném, i když vypadá jako duplikovaný kód, budete ji moci individuálně přenést do jiného programu, a ještě více, pokud nezáleží na jazykových funkcích nebo knihovních funkcích, které nejsou k dispozici v každém překladači a programovacím jazyce, který používáte, aby bylo možné přenášet jakýkoli kód, který napíšete, do libovolného prostředí, které potřebujete , to je co s názvem decoupling .

Jak vidíte, decoupling lze aplikovat na úrovni kompilátoru, jazyka, systémového prostředí, funkcí a subsystému. Může to zahrnovat duplikování kódu a přepisování, abyste měli samostatné rutiny bez závislosti. Může to také znamenat použití více standardizovaných funkcí, aby byl kód lépe přenosný, a také může být nutné, abyste si sami implementovali nebo přenesli chybějící funkce do všech svých programovacích / systémových prostředí, takže bez ohledu na systém / jazyk / kompilátor vás použití, budete mít vždy k dispozici stejnou funkcionalitu.





O návrhových vzorech.

Pokud chcete, aby byl váš kód opětovně použitelný a pokud chcete, aby to trvalo desetiletí, můžete použít nízkoúrovňový přístup k programování sestavení CPU.

Přemýšlejte o úkolu nebo mikrotaskování, které chcete provést způsobem, který bude vždy trvat stejným typem parametry a to vždy vrátí výsledek přesně stejným způsobem.

Pak mu dejte VELMI specifický název této rutiny. Bude to operační kód, instrukce implementovaná jako funkce / podprogram, kterou můžete znovu použít stejně jako jakoukoli jinou nativní instrukci CPU. Tento způsob návrhu kódu je vysoce opakovaně použitelný a stabilní. Chcete-li variantu, co dělat, jednoduše přidáte novou funkci opcode místo toho, abyste zničili předchozí platnou funkčnost.

Pokud ji použijete v celém programu jako hlavní přístup k designu, může to kód ještě zpřísnit. jednodušší na sledování.

Komentáře

  • Děkuji, @ alt.126. I když se jedná o zobecněné řešení (+1), ve skutečnosti jsem doufal více v návrhové vzory Java / vytváření méně objektů / odstranění nadbytečného kódu atd.

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *