Как написать морской бой на java

Пробую написать простенький морской бой на Java. Все почти сделал, кроме правильного алгоритма помещения кораблей на карту. В коде комментариями описано где именно проблема.

public class Main {
public static void main(String[] args) {
    Random rand = new Random();
    Field field = new Field();
    Player player = new Player();
    player.getName();
    field.init();
    final int NUMBEROFSHIPS =10;
    Ship [] ships = new Ship[NUMBEROFSHIPS];
    for (int i = 0; i < NUMBEROFSHIPS; i++) {
        boolean isShipSettedUp = false;
        do { //вот здесь проблемы
             //корабли расставляются рандомно на свободное место на карте
            // получается так, что для последних кораблей не хватает 
           //места и идет зацикливание 
           //Ship(координата Х, координата У,  размер, вертикален ли)
            if (i <=3) ships[i] = new Ship(rand.nextInt(10), rand.nextInt(10), 1, false);
            if (i >3 && i <=6) ships[i] = new Ship(rand.nextInt(10), rand.nextInt(10), 2, false);
            if (i >6 && i <= 8) ships[i] = new Ship(rand.nextInt(10), rand.nextInt(10), 3, false);

            if (i > 8 && i <=9) ships[i] = new Ship(rand.nextInt(10), rand.nextInt(10), 4, false);
            if (field.isCanSetShip(ships[i])) {
                field.setShip(ships[i]);
                isShipSettedUp =true;
           }
        } while(!isShipSettedUp);
    }
    System.out.println("Game start!");
    do {
        field.show();
        System.out.println("Where to shoot?");
        System.out.println("Enter column: ");
        int shootX = player.getShoot();
        System.out.println("Enter row: ");
        int shootY = player.getShoot();
        Shoot shoot = new Shoot(shootX,shootY);
        field.doShoot(shoot);
    } while (field.isNotGameOver());
}
}

Класс Field:

public class Field {
final int FIELDSIZE = 10;
char[][] cells = new char[FIELDSIZE][FIELDSIZE];
Ship ship;
void init() {
    for (int i = 0; i < FIELDSIZE; i++) {
        for (int j = 0; j < FIELDSIZE; j++) {
            cells[i][j] = '.';
        }
    }
}
void setShip(Ship ship) { //помечаю ячейку в массиве, что здесь корабль
    this.ship = ship;
    if (ship.isVertical) {
        for (int i = 0; i < ship.size; i++) {
            cells[ship.positionX + i][ship.positionY] = 'X';
        }
    } else {
        for (int i = 0; i < ship.size; i++) {
            cells[ship.positionX][ship.positionY + i] = 'X';
        }
    }
}

void doShoot(Shoot shoot) {
    switch (cells[shoot.xCoord][shoot.yCoord]) {
        case '.':
            System.out.println("MISS");
            cells[shoot.xCoord][shoot.yCoord] = '*';
            break;
        case 'X':
            System.out.println("GOAL");
            cells[shoot.xCoord][shoot.yCoord] = '_';
            show();
            break;
        case '*':
            System.out.println("You already shoot here...");
            break;
        default:
            System.out.println("Error!");
    }
}

void show() {
    for (int i = 0; i < FIELDSIZE; i++) {

        if (i == 0) {
            for (int k = 0; k < FIELDSIZE; k++) {
                System.out.print("t" + k);
            }
            System.out.println();
        }
        System.out.print(i);
        for (int j = 0; j < FIELDSIZE; j++) {
            System.out.print("t" + cells[i][j]);
        }
        System.out.println();
    }
}

boolean isShipPresent(int positionX, int positionY) {
    return cells[positionX][positionY] == 'X';
}

boolean isNotGameOver() {
    boolean isPresent = false;
    for (int i = 0; i < FIELDSIZE; i++) {
        for (int j = 0; j < FIELDSIZE; j++) {
            if (cells[i][j] == 'X') isPresent = true;
        }
    }
    return isPresent;
}
//Проверка на то, можно ли в данном участке поставить корабль
//Написано не очень правильно, только учусь :(
boolean isCanSetShip(Ship ship) {
    boolean canSet = true;
    if (ship.positionX + ship.size > FIELDSIZE - 1 || ship.positionY + ship.size > FIELDSIZE - 1)
        canSet = false;
    else {

        //Если корабль стоит горизонтально
        if (!ship.isVertical) {
            if (ship.positionX == 0 && ship.positionY > 0) {
                for (int i = 0; i <= ship.size; i++) {
                    for (int j = -1; j <= 1; j++) {
                        if (ship.positionY + j < FIELDSIZE && ship.positionY + j >= 0) {
                            if (isShipPresent(ship.positionX + i, ship.positionY + i)) canSet = false;
                        } else canSet = false;
                    }
                }
            }
            if (ship.positionY == 0 && ship.positionX > 0) {
                for (int i = -1; i <= ship.size; i++) {
                    for (int j = 0; j <= 1; j++) {
                        if (ship.positionX + i < FIELDSIZE && ship.positionX + i >= 0) {
                            if (isShipPresent(ship.positionX + i, ship.positionY + j)) canSet = false;
                        } else canSet = false;
                    }
                }
            }
            if (ship.positionX == 0 && ship.positionY == 0) {
                for (int i = 0; i <= ship.size; i++) {
                    for (int j = 0; j <= 1; j++) {
                        if (isShipPresent(ship.positionX + j, ship.positionY + i)) canSet = false;
                    }
                }
            }
            if (ship.positionX > 0 && ship.positionY > 0) {
                for (int i = -1; i <= ship.size; i++) {
                    for (int j = -1; j <= 1; j++) {
                        if (ship.positionX + j >= 0 && ship.positionY + i >= 0 && ship.positionX + j < FIELDSIZE && ship.positionY + i < FIELDSIZE) {
                            if (isShipPresent(ship.positionX + j, ship.positionY + i)) canSet = false;
                        } else canSet = false;
                    }
                }
            }
        } else {
            //если корабль вертикально
            //пока не использую. На данный момент все корабли с горизонтальным положением
        }
    }
    return canSet;
}
}

Как правильно организовать алгоритм?

Морской бой

Коротко обо всем

Приветствую Вас в репозитории, посвященном моей реализации игры «Морской бой».
Я начал этот проект в рамках самостоятельного изучения программирования.
Создание игры — это отличный способ научиться программировать, подумал я, а игра «Морской бой»
достаточно проста, чтобы начать именно с нее, но в то же время достаточно сложна,
чтобы разработка не была скучной. В ходе создания этой игры было перелопачено немало материала,
который в конце концов превратился в незабываемый опыт. К тому же наличие такого проекта приятно тяжелит
портфолио! Итак, давайте приступим к изложению сути. Если Вы, как и я, самостоятельно изучаете программирование,
я предлагаю Вам воспользоваться материалами этого проекта как пособием к обучению.
К репозиторию приложены спецификация, прототипирование, а так же все проектирование.
Предлагаю Вам ознакомиться с этими материалами и воспользоваться ими для собственной реализации
этой замечательной игры. Если Вы считаете, что что-то реализовано не лучшим образом,
то я с радостью выслушаю конструктивную критику (mod.satyr@gmail.com). Приятного прочтения!

Содержание

Спецификация и прототипирование:

  • Игра
  • Сражение

Проектирование:

  • Диаграмма классов

Правила игры

Игра начинается с расстановки кораблей. Игроки расставляют свои корабли таким образом,
чтобы между кораблями всегда был минимум один сектор. Корабль может занимать только
соседние друг с другом секторы. Секторы, расположенные по диагонали по отношению друг к другу,
соседними не являются. Когда расставлены все корабли, доступные игрокам, начинается сражение.
Во время сражения игроки обмениваются выстрелами до тех пор, пока у одного из них не будут
потоплены все корабли. Этот игрок считается проигравшим, и сражение заканчивается.

Ссылка для скачивания приложения

Работоспособный билд отсутствует на данный момент. Когда будет реализован весь описанный в спецификации функционал,
эта версия приложения будет доступна для скачивания и ссылка на нее будет размещеная здесь.

What annoys me a little bit with this code is I don’t use interfaces, abstract classes, or inheritance, but I couldn’t find a good use case for them here.

There is nothing wrong with this in your game. The game concept is so simple that you don’t need any of those. The problem with little toy programs is that you don’t usually need to apply big design solutions. You just need to make sure you folow the usual SOLID design principles.


Now that we got that cleared up let’s look at some details of your code that should be improved.

The first one is pretty obvious. Don’t write comments for the sake of writing comments. Some teachers like to force you to write a javadoc comment on everything. I’m actually completely against this, except when writing some sort of utility package that everyone else has to use. Normally speaking code should be self documenting. And your code does a really good job at that. So just remove the comment that’s basically repeating the well chosen name of the variable/function/…

For example:

/**
 * Is point between boolean.
 *
 * @param point    the point
 * @param position the position
 * @return the boolean
 */
public static boolean isPointBetween(Point point, Position position) {

What value does that comment add to the function? I think this is also the only method that I would change to make it more readable. Because it’s not obvious that the position is made up of a from and to point for which we check if our point lies between them.

What if the method had this signature:

public static boolean containsPoint(Position position, Point point) {

Wouldn’t that make a little more sense?

I should add here that I’m not against comments in general though. But a comment should explain WHY something is done that way. Not how it is implemented. If I wanted to know how it’s implemented I would’ve just looked at the code.


The next point is having that Util class …
Unlike some purists, I have nothing against the concept of Util classes. Util classes can be useful to put similar functions together. For example java.lang.Math that groups all the usual arithmetic operations together in one class.

The thing that troubles me with your Util class is that there isn’t really a good reason for it to exist. The 2 functions that you have in there are only ever used inside the Board class. So they could’ve just as well been private static helper functions inside that class.

In fact, we can even do a little better after changing the signature to what I suggested earlier. What if we put containsPoint(Position position, Point point) { inside the Position class instead of having the Position as a parameter for the method? Then we can use it like this:

Position shipPosition = //some ship's position
if(shipPosition.contains(targetPoint)) {
    //handle ship hit
}

It fits really well there, doesn’t it?


Speaking of the Positions class. I had a weird feeling about this while looking through your code. At first I thought you didn’t have a something[][] to represent the board. I thought you had represented everything as Points throughout the entire codebase. This could work but makes printing the board awkward. And then I noticed do have a char[][] inside your Board class. But then wouldn’t it make more sense to immdediatly put the ships inside that grid without having an intermediate Position class?

I also noticed another dangerous thing about the placeShipsOnBoard(). Should you really just trust your user to enter 2 valid coordinates? What if the user tries to be funny and inputs from=(1,1) to=(2,2)? Should we allow this? Or what if you want him to input a ship of length 5 and he inputs from=(1,1) to=(1,1) essentially shrinking the ship to a single square (that you have to hit 5 times! since the ship has 5 lives). Shouldn’t we prevent him from cheating like this?

Let’s look at an alternative way to handle placing the ships. First, let the user choose if he wants to place the ship horizontally or vertically. Then have him input the top/left coordinate of the ship. We will calculate the remaining points ourselves.

Here’s what the actual method implementation could look like:

private Scanner scanner = new Scanner(System.in);

private void placeShipsOnBoard() {
    System.out.printf("%nAlright - Time to place out your ships%n%n");

    for(Ship ship : ships) { //awesome for-each loop is better here
        boolean horizontal = askValidShipDirection();
        Point startingPoint = askValidStartingPoint(ship, horizontal);
        placeValidShip(ship, startingPoint, boolean horizontal);
    }
}

private boolean askValidShipDirection() {
    do {
        System.out.println("Do you want to place the ship horizontally (H) or vertically (V)?");
        String direction = Scanner.nextLine().trim();
    while ( !"H".equals(direction) && !"V".equals(direction);
    return "H".equals(direction);
    //note here: use "constant".equals(variable) so nullpointer is impossible.
    //probably not needed, but it's best practice in general.
}

private Point askValidStartingPoint(Ship ship, boolean horizontal) {
    do { //note: do-while more useful here
        System.out.printf("%nEnter position of %s (length  %d): ", ship.getName(), ship.getSize());          
        Point from = new Point(scanner.nextInt(), scanner.nextInt());
    while(!isValidStartingPoint(from, ship.getLength(), horizontal));
    return from;
}

private boolean isValidStartingPoint(Point from, int length, boolean horizontal) {
    int xDiff = 0;
    int yDiff = 0;
    if(horizontal) {
        xDiff = 1;
    } else {
        yDiff = 1;
    }
    for(int i = 0; i < lenth; i++) {
        if(!isInsideBoard(i,from.getY()) {
            return false;
        }
        if(!Constants.BOARD_ICON.equals(board[from.getY()+i*yDiff][from.getX()+i*xDiff])){
            return false;
        }
     }
     return true;
}

private boolean isInsideBoard(int x, int y){
     return x <= Constants.BOARD_SIZE && x >= 0
            && y <= Constants.BOARD_SIZE && y >= 0
            && x <= Constants.BOARD_SIZE && x >= 0
            && y <= Constants.BOARD_SIZE && y >= 0;
}

private void placeValidShip(Ship ship, Point startingPoint, boolean horizontal) {
    int xDiff = 0;
    int yDiff = 0;
    if(horizontal) {
        xDiff = 1;
    } else {
        yDiff = 1;
    }
    for(int i = 0; i < ship.getLenth() ; i++) {
        board[ship.getY() + i*yDiff][ship.getX()+i*xDiff] = Constants.SHIP_ICON;
    }           
}

Now that we just place the ships on the board directly we can remove the Position class and all it’s references. This does mean we no longer know whether a ship has sunk or not.

While yping this I noticed that @Timothy Truckle already posted an answer as well. I really love his solution of using dedicated Fields instead of char‘s to represent the board.

so our place ship method changes to:

private void placeValidShip(Ship ship, Point startingPoint, boolean horizontal) {
    int xDiff = 0;
    int yDiff = 0;
    if(horizontal) {
        xDiff = 1;
    } else {
        yDiff = 1;
    }
    for(int i = 0; i < ship.getLenth() ; i++) {
        board[ship.getY() + i*yDiff][ship.getX()+i*xDiff] = new ShipField(ship);
    }           
}

That way we can check if a ship is destroyed completely or just hit partially when attacking a Field.

Now instead of continuing on this answer, I suggest you read @Timothy’s as well, and look at the good points in both our anwsers. At first glance we either agree on or simply complement each other’s answer. So you should have some decent pointers on how to improve your code :)

Создал тут проект игры «Морской бой» на Java. Проект скомпилирован в Netbeans под Java 8, но может быть открыт (или быстро адаптирован при необходимости) и в Eclipse или IntelliJ IDEA. Готовая программа (jar файл) тестировалась под Windows 7 и Ubuntu 16.

Правила

Игра может вестись на поле от 6 до бесконечности (теоретически). Можно поставить клеток кораблей пропорционально стороне поля. То есть 6 – 6, 7 – 7 и т.д. Общие клетки не могут быть соединены по диагонали или быть буквой «Г». Можно играть вдвоем (по очереди на одном поле) или с компьютером (2 уровня сложности). Первый уровень – случайная стрельба; второй – вокруг «подбитой» клетки, но не по диагонали.

Реализация

Игра включает в себя две сетки (карта своих карта атаки), расположенные в JFrame.

Используемые шаблоны проектирования: MVC

GUI Framework: Swing

Файлы

  • Setting.java – первоначальный экран с настройками.
  • BattleShipPro.java – основной файл проекта. Вызывает настройки и хранит их в полях.
  • Model.java — предоставляет данные и реагирует на команды контроллера, изменяя своё состояние.
  • Viev.java — отвечает за отображение данных модели пользователю, реагируя на изменения модели.
  • Controller.java — интерпретирует действия пользователя, оповещая модель о необходимости изменений.

Статистика

Статистика игроков хранится в файле stat.csv – обычный текстовый файл в UTF-8, разделитель точка с запятой. Записываются победы и поражения.

Комментарии

Комментарии к методам и классам в коде программы. Цена на данный момент — 500 рублей. Возможна доработка проекта за отдельную плату.


Автор этого материала — я — Пахолков Юрий. Я оказываю услуги по написанию программ на языках Java, C++, C# (а также консультирую по ним) и созданию сайтов. Работаю с сайтами на CMS OpenCart, WordPress, ModX и самописными. Кроме этого, работаю напрямую с JavaScript, PHP, CSS, HTML — то есть могу доработать ваш сайт или помочь с веб-программированием. Пишите сюда.

тегистатьи IT, java, игры, программы

Читайте также:

  • Программа Карточный помощник
  • Калькулятор на C++ (windows forms) в visual studio
  • Введение в PHP MVC Framework

-7 / 22 / 4

Регистрация: 07.03.2013

Сообщений: 229

1

Морской бой

17.06.2013, 02:02. Показов 12070. Ответов 24


Привет всем,
Читаю книгу Сьерра и Бейтса, уже на 150-ой где то странице они вбамбурили консольную игру под названием «морской бой» типа как для новичков что ли?
Я посмотрел и слегка так покрывшись потом принялся разбирать код.
Процентов 30% этой жути для меня так и осталось загадкой(может потому что разбираться дальше в дебрях хаоса не хватило сил, хотя моментами все было очень просто, а моментами тихий ужас, в итоге я закрыл книгу).
После этого «морского боя» для новичков, который не в конце книги был размещен, а уже на 150 странице, я усомнился стоит ли мне ее дальше читать, и усомнился стоит ли мне ВООБЩЕ что либо, связанное с джава программированием, читать

Скажите, это действительно является легкой программкой для новичков, и это просто я на столько безнадежный что не понял?
или она все таки сложновата и в том что я не смог ее реализовать у себя в текстпаде после 3-х раз прочтения кода нет ничего ужасного?

Разобрать ее это еще куда не шло, уж ладно, но реализовать по памяти добавив те знания по джаве которые недавним временем получил, мне не удалось

Собственно код:

Кликните здесь для просмотра всего текста

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
//import helpers.GameHelper;
import java.util.*;
 
public class DotComBust {
    private GameHelper helper = new GameHelper();
    private ArrayList<DotCom> dotComsList = new ArrayList<DotCom>();
    private int numOfGuesses = 0;
    //----------------------------------------------------------------
    private void setUpGame() {
        DotCom one = new DotCom();
        one.setName("Pets.com");
        DotCom two = new DotCom();
        two.setName("eToys.com");
        DotCom three = new DotCom();
        three.setName("Go2.com");
        dotComsList.add(one);
        dotComsList.add(two);
        dotComsList.add(three);
        
        System.out.println("Your goal is to sink three dot coms.");
        System.out.println("Pets.com, eToys.com, Go2.com");
        System.out.println("Try to sink them all in the fewest number of guesses");
        
        for (DotCom dotComSet : dotComsList) {
            ArrayList<String> newLocation = helper.placeDotCom(3);
            dotComSet.setLocationCells(newLocation);
        }
    }
 
    private void startPlaying() {
        while (!dotComsList.isEmpty()) {
            String userGuess = helper.getUserInput("Enter a guess");
            checkUserGuess(userGuess);
        }
        finishGame();
    }
    
    private void checkUserGuess(String userGuess)
    {
        numOfGuesses++;
        String result = "miss";
        
        for (DotCom dotComToTest : dotComsList)
        {
            result = dotComToTest.checkYourself(userGuess);
            if (result.equals("hit"))
            {
                break;
            }
            if (result.equals("kill"))
            {
                dotComsList.remove(dotComToTest);
                break;
            }
        }
        System.out.println(result);
    }
    
    private void finishGame() {
        System.out.println("All Dot Coms are dead!  Your stock is now worthless");
        if (numOfGuesses <= 18) {
            System.out.println("It only took you " + numOfGuesses + " guesses");
            System.out.println("You got out before your options sank.");
        }
        else
        {
            System.out.println("Took you long enough. " + numOfGuesses + " guesses.");
            System.out.println("Fish are dancing with your options.");
        }
    }
    
    public static void main(String[] args) {
        DotComBust game = new DotComBust();
        game.setUpGame();
        game.startPlaying();
    }
}

Кликните здесь для просмотра всего текста

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import java.io.*;
import java.util.*;
 
public class GameHelper {
 
  private static final String alphabet = "abcdefg";
  private int gridLength = 7;
  private int gridSize = 49;
  private int [] grid = new int[gridSize];
  private int comCount = 0;
 
 
  public String getUserInput(String prompt) {
     String inputLine = null;
     System.out.print(prompt + "  ");
     try {
       BufferedReader is = new BufferedReader(
     new InputStreamReader(System.in));
       inputLine = is.readLine();
       if (inputLine.length() == 0 )  return null; 
     } catch (IOException e) {
       System.out.println("IOException: " + e);
     }
     return inputLine.toLowerCase();
  }
 
  
  
 public ArrayList<String> placeDotCom(int comSize) {                 // line 19
    ArrayList<String> alphaCells = new ArrayList<String>();
    String [] alphacoords = new String [comSize];      // holds 'f6' type coords
    String temp = null;                                // temporary String for concat
    int [] coords = new int[comSize];                  // current candidate coords
    int attempts = 0;                                  // current attempts counter
    boolean success = false;                           // flag = found a good location ?
    int location = 0;                                  // current starting location
    
    comCount++;                                        // nth dot com to place
    int incr = 1;                                      // set horizontal increment
    if ((comCount % 2) == 1) {                         // if odd dot com  (place vertically)
      incr = gridLength;                               // set vertical increment
    }
 
    while ( !success & attempts++ < 200 ) {             // main search loop  (32)
    location = (int) (Math.random() * gridSize);      // get random starting point
       // System.out.print(" try " + location);
    int x = 0;                                        // nth position in dotcom to place
        success = true;                                 // assume success
        while (success && x < comSize) {                // look for adjacent unused spots
          if (grid[location] == 0) {                    // if not already used
             coords[x++] = location;                    // save location
             location += incr;                          // try 'next' adjacent
             if (location >= gridSize){                 // out of bounds - 'bottom'
               success = false;                         // failure
             }
             if (x>0 & (location % gridLength == 0)) {  // out of bounds - right edge
               success = false;                         // failure
             }
          } else {                                      // found already used location
              // System.out.print(" used " + location);  
              success = false;                          // failure
          }
        }
    }                                                   // end while
 
    int x = 0;                                          // turn good location into alpha coords
    int row = 0;
    int column = 0;
    // System.out.println("n");
    while (x < comSize) {
      grid[coords[x]] = 1;                              // mark master grid pts. as 'used'
      row = (int) (coords[x] / gridLength);             // get row value
      column = coords[x] % gridLength;                  // get numeric column value
      temp = String.valueOf(alphabet.charAt(column));   // convert to alpha
      
      alphaCells.add(temp.concat(Integer.toString(row)));
      x++;
 
      System.out.print("  coord "+x+" = " + alphaCells.get(x-1));
      
    }
    System.out.println("n");
    
    return alphaCells;
   }
}

Кликните здесь для просмотра всего текста

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.util.ArrayList;
 
public class DotCom {
    private ArrayList<String> locationCells;
    
    public void setLocationCells(ArrayList<String> loc)
    {
        locationCells = loc;
    }
    
    public String checkYourself(String userInput)
    {
        String result = "miss";
        int index = locationCells.indexOf(userInput);
        if (index >= 0) {
            locationCells.remove(index);
            if (locationCells.isEmpty()) {
                result = "kill";
            }
            else
            {
                result = "hit";
            }
        }
        return result;
    }
 
    //TODO:  all the following code was added and should have been included in the book
    private String name;
    public void setName(String string) {
        name = string;
    }
}

Извините за кучу писанины.

__________________
Помощь в написании контрольных, курсовых и дипломных работ, диссертаций здесь



0



Всем привет!

Статья посвящена тем, кто только врывается в увлекательный мир
программирования на Java и ищет применения своим знаниям. Классно,
что вы теперь знаете, как создавать переменные, методы и массивы,
но, конечно, хочется писать «полезные» для человечества программы,
а не выполнять многочисленные мелкие упражнения и задачи, хотя без
этого тоже никуда. В общем, будем дополнять теорию практикой.
Поехали!

Для начала давайте обсудим, какие требования «бизнес» предъявил
к нашему приложению. После долгих переговоров заказчик утвердил
следующий сценарий игры:

  1. Приложение должно работать из консоли. Наш заказчик оказался
    ретроградом, и он считает, что его целевой аудитории очень нравится
    интерфейс консоли. У всех свои причуды, но ладно.

  2. Одновременно в игре могут участвовать только два человека.

  3. В самом начале игроки «представляются» — программа «спрашивает»
    (предлагает пользователям ввести), какие у них имена

  4. У каждого игрока есть своё поле — квадрат 10х10 клеток

  5. Затем игроки по очереди расставляют свои корабли. Как и в
    «бумажной» версии — каждый может поставить 4 однопалубных корабля,
    3 двухпалубных, 2 трехпалубных и 1 четырёхпалубный.

  6. Корабли можно располагать только по горизонтали или по
    вертикали.

  7. Игроки не видят расположение кораблей друг друга.

  8. Начинается игра. Первый игрок делает выстрел, сообщая нашему
    приложению координаты предполагаемой цели — номер клетки по
    горизонтали и номер клетки по вертикали.

  9. Если выстрел первого игрока оказался удачным, и он поразил цель,
    то возможно два варианта развития событий.

    Если в указанной игроком клетки находится корабль, то, если корабль
    однопалубный, игрок «убил» корабль, если не однопалубный, то ранил.
    В любом случае следующий ход снова за первым игроком.

    Второй вариант, если игрок не попал ни в какой корабль, то ход
    переходит второму игроку.

  10. Таким образом, как описано в пункте 8, передавая ход друг другу,
    игроки пытаются как можно раньше уничтожить корабли друг друга.
    Тот, кто первым, одолеет флотилию врага — победитель. Программа
    печатает поздравление и прекращает свою работу.

    Теперь, согласовав с бизнесом сценарий работы приложения, можно
    приступать к реализации игры.

Как и в любом Java приложении нам потребуется класс (не умаляя
общности назовём его Main), в котором будет объявлен, я думаю уже
всем известный, метод main.

public class Main {public static void main(String[] args) {  //your code will be here}}

Пока ничего нового, на этом этапе можно попробовать запустить
код и проверить, что ваше окружение настроено правильно и «самая
простая программа на свете» запускается без ошибок.

Опираясь на пункты 1-3 утвержденного сценария, реализуем
функционал приложения, который будет предлагать игрокам ввести свои
имена. Здесь нам придётся использовать класс java.util.Scanner,
который умеет считывать введенные значения в консоли.

public class Main {    static Scanner scanner = new Scanner(System.in);    public static void main(String[] args) {        System.out.println("Player 1, please, input your name");        String player1Name = scanner.nextLine();        System.out.println("Hello, " + player1Name + "!");        System.out.println("Player 2, please, input your name");        String player2Name = scanner.nextLine();        System.out.println("Hello, " + player2Name + "!");    }}

Подробнее о коде:

В строке 2 создаем статичное свойство класса Main scanner.

Нестатический метод nextLine() класса Scanner (строки 6 и 11)
обращается к консоли и возвращает строку, которую он еще не
считывал.

После получения имени пользователей, программа выводит
приветствие в консоль — «Hello, {username} !»

В консоли будем видеть следующее, если запустим код сейчас.

Player 1, please, input your nameEgorHello, Egor!Player 2, please, input your nameMaxHello, Max!

Поговорим о том, как мы будем отображать поле боя и заполнять
его кораблями. Пожалуй, что наиболее логичным будет использование
двумерного массива char[][] buttlefield. В нем мы будем отображать
расположение кораблей. Договоримся, что удачное попадание в корабль
противника будем отображать символом #. Неудачный выстрел будем
помечать символом *. Таким образом изначально, массив будет
проинициализирован дефолтовым для примитива char значением
(‘u0000’), а в процессе игры будет заполняться символами # и
*.

public class Main {static final int FILED_LENGTH = 10;    static Scanner scanner = new Scanner(System.in);public static void main(String[] args) {    System.out.println("Player 1, please, input your name");    String player1Name = scanner.nextLine();    System.out.println("Hello, " + player1Name + "!");    System.out.println("Player 2, please, input your name");    String player2Name = scanner.nextLine();    System.out.println("Hello, " + player2Name + "!");        char[][] playerField1 = new char[FILED_LENGTH][FILED_LENGTH];    char[][] playerField2 = new char[FILED_LENGTH][FILED_LENGTH];    char[][] playerBattleField1 = new char[FILED_LENGTH][FILED_LENGTH];    char[][] playerBattleField2 = new char[FILED_LENGTH][FILED_LENGTH];}}

Подробнее о коде:

В строке 2 мы создаем константу FIELD_LENGTH, которая будет
содержать размер поля — согласно требованию 4 — проинициализируем
FIELD_LENGTH значением 10.

В строках 14-18 создаем двумерные массивы char. playerFiled1 и
playerField2 — массивы, в которые будем записывать расположение
кораблей каждого игрока. Размеры массивов равны размерам поля —
логично, ведь эти двумерные массивы фактически отображают поля.

Перейдём к написанию логике по заполнению полей игроков своими
кораблями. Предлагаю создать метод fillPlayerField(playerField),
который будет спрашивать у игрока позиции корабля и записывать в
массив новый корабль.

public class Main {    static final int FILED_LENGTH = 10;    static Scanner scanner = new Scanner(System.in);    public static void main(String[] args) {        System.out.println("Player 1, please, input your name");        String player1Name = scanner.nextLine();        System.out.println("Hello, " + player1Name + "!");        System.out.println("Player 2, please, input your name");        String player2Name = scanner.nextLine();        System.out.println("Hello, " + player2Name + "!");        char[][] playerField1 = new char[FILED_LENGTH][FILED_LENGTH];        char[][] playerField2 = new char[FILED_LENGTH][FILED_LENGTH];        fillPlayerField(playerField1);        fillPlayerField(playerField2);    }}private static void fillPlayerField(char[][] playerField) {// your code will be here}

Метод fillPlayerField должен быть статическим (static), так как
вызываться он будет из метода main, который по определению должен
быть статическим. fillPlayerField не будет возвращать никакого
значения (void). В этом методе будет реализована логика по
получению координат корабля от пользователя и запись в массив
playerField нового корабля.

Поговорим о том, как пользователь будет задавать координаты
кораблей. Будем размещать корабли по убыванию палуб, то есть первым
разместим четырёхпалубный корабль, затем все трёхпалубные корабли,
все двухпалубные корабли и все однопалубные. Программа будет
принимать от пользователя координаты начала корабля и его положение
(см. бизнес-требование 6) — по горизонтали или по вертикали.

Приведу вывод программы в консоль, который мы ожидаем увидеть
после создания метода fillPlayerField:

Расставляем 4-палубный корабль. Осталось расставить: 1Input x coord: 1Input y coord: 11. Horizontal; 2. Vertical ?1

Наконец-то приступаем. На данный момент имеем:

private static void fillPlayerField(char[][] playerField) {    // your code will be here}

Нам нужно расставить корабли с палубами от 4 до 1. Здесь дело
идёт к циклу. Предлагаю без лишнего пафоса использовать for.
Заметим, кораблей с одинаковым числом палуб может быть несколько,
поэтому нам нужно ещё как-то контролировать, чтобы пользователь мог
разместить лишь определенное число кораблей с заданным количеством
палуб (см. бизнес требование 5) — эта задача также эффективно
решается с помощью цикла — без пафоса также используем for.

private static void fillPlayerField(char[][] playerField) {// i - счётчик количества палуб у корабля    // начинаем расстановку с корабля, которого 4 палубы, а заканчиваем кораблями с одной палубой    for (int i = 4; i >= 1; i--) {      // см. подробнее о коде под этой вставкой    for (int k = i; k <= 5 - i; k++) {          System.out.println("Расставляем " + i + "-палубный корабль. Осталось расставить: " + (q + 1));        // some your code here        }    }}

Подробнее о коде:

На 5 строчке мы создаём цикл for (int k = 0; k <= 5 — i;
k++). Объясню, откуда такая магия. Нам нужно как-то понимать,
сколько кораблей каждого типа (с определенным количеством палуб)
пользователь может поставить.

Мы можем создать еще один двумерный массив, в котором мы
захардкодим что-то в духе:

int[][] shipTypeAmount = {{1, 4}, {2, 3}, {3, 2}, {4, 1}};

Мы бы в первом цикле (см. строку 4) перебирали значение
количества палуб, потом искали бы в двумерном массиве, какое
количество кораблей соответствует данному типу. На мой взгляд,
такое решение качественное, однозначно, если бы мы писали
коммерческую программу, то сделали бы именно так, но сейчас такая
красота выглядит сложной — оставим это как задачу со звёздочкой,
для тех, кто захочет «причесать» свой код.

Я же предлагаю отметить особенность, что сумма количества
кораблей и количества палуб — величина постоянная. Действительно,
1 + 2 = 5; 2 + 3 = 5; 3 + 2 = 5; 4 + 1 = 5
. Поэтому, зная количество палуб у корабля (зная тип корабля), мы
можем посчитать, сколько кораблей такого типа может быть
установлено одним играком. Например, 5 - 1 = 4
— таким образом, каждый игрок может поставить 4 однопалубных
корабля. В цикле for на строке 6 реализована проверка условия цикла
«лайтовым» способом — на основе этого интересного свойства.

Теперь дело за малым — напишем логику по считыванию значений из
консоли и запись корабля в двумерный массив.

private static void fillPlayerField(char[][] playerField) {        for (int i = 4; i >= 1; i--) {            // растановка самих кораблей            for (int k = i; k <= 5 - i; k++) {                System.out.println("Расставляем " + i + "-палубный корабль. Осталось расставить: " + (q + 1));                System.out.println("Input x coord: ");                x = scanner.nextInt();                System.out.println("Input y coord: ");                y = scanner.nextInt();                System.out.println("1 - horizontal; 2 - vertical ?");                position = scanner.nextInt();                // если корабль располагаем горизонтально              if (position == 1) {                    // заполняем '1' столько клеток по горизонтали, сколько палуб у корабля                    for (int q = 0; q < i; q++) {                        playerField[y][x + q] = '1';                    }                }                            // если корабль располагаем вертикально                if (position == 2) {                  // заполняем столько клеток по вертикали, сколько палуб у корабля                    for (int m = 0; m < i; m++) {                        playerField[y + m][x] = '1';                    }                }              // печатаем в консоли поле игрока, на котором будет видно, где игрок уже поставил корабли              // о реализации метода - см. ниже                printField(playerField);            }        }    }

Подробнее о коде:

Корабль помечаем символом ‘1’ столько раз, сколько палуб он
имеет — если корабль четырёхпалубный, то он займёт 4 клетки —
помечаем 4 клетки значением ‘1’.

Нам неоднократно потребуется печатать поля игроков, либо поля их
выстрелов по вражескому флоту. Для того, чтобы сделать это —
создаём метод printField.

static void printField(char[][] field) {        for (char[] cells : monitor) {            for (char cell : t) {              // если значение дефолтовое (в случае char - 0), значит в данной клетке              // корабль не установлен - печатаем пустую клетку                if (cell == 0) {                    System.out.print(" |");                } else {   // если клетка непустая (значение отличается от дефолтового),                  //тогда отрисовываем сожержимое клетки (элемента массива)                    System.out.print(cell + "|");                }            }            System.out.println("");            System.out.println("--------------------");        }    }

На экране метод будет так отображать расстановку кораблей:

 | | | | | | | | | |-------------------- |1|1|1|1| | | | | |-------------------- | | | | | | | | | |-------------------- | | | | | | | | | |-------------------- | | | | | | | | | |-------------------- | | | | | | | | | |-------------------- | | | | | | | | | |-------------------- | | | | | | | | | |-------------------- | | | | | | | | | |-------------------- | | | | | | | | | |--------------------

Вот игроки уже заполнили свои корабли на карте, и теперь мы
можем приступить к реализации пунктов 8-10 бизнес-требований
заказчика.

Логику по получению от пользователя координат выстрела,
обработки выстрела и передачи хода опишем в методе playGame. Дабы
придерживаться (или пока только стараться) принципа single
responsobility — не забываем делить логику на методы (1
функциональность — 1 метод, но тоже держим себя в руках, код, в
котором 100500 однострочных методов тоже не комильфо) — примерно из
этих соображений получились еще методы handleShot и isPlayerAlive.
Реализация обоих приведена ниже

/*** Метод реализует логику игры: выстрел и передача хода.*/private static void playGame(String player1Name, String player2Name, char[][] playerField1, char[][] playerField2) {        // "карты" выстрелов - создаём двумерные массивы, которые содержат все выстрелы  // удачные (#) и неудачные (*)  char[][] playerBattleField1 = new char[FILED_LENGTH][FILED_LENGTH];        char[][] playerBattleField2 = new char[FILED_LENGTH][FILED_LENGTH];  // вспомогательные переменные, которым будут присваиваться значения текущего игрока -   // игрока, чья очередm делать выстрел. Сначала играет первый игрок, прошу прошения  // за тавтологию        String currentPlayerName = player1Name;        char[][] currentPlayerField = playerField2;        char[][] currentPlayerBattleField = playerBattleField1;  // внутри цикла происходит смена очередности игроков, выстрел, его обработка.  // код внутри цикла выполняется до тех пор, пока "живы" оба игрока - пока у двух игроков  // "частично" цел (ранен) ещё хотя бы один корабль        while (isPlayerAlive(playerField1) && isPlayerAlive(playerField2)) {          // принимаем от пользователя координаты выстрела            System.out.println(currentPlayerName + ", please, input x coord of shot");            int xShot = scanner.nextInt();            System.out.println(currentPlayerName + ", please, input y coord of shot");            int yShot = scanner.nextInt();          // обрабатываем выстрел и получаем возвращаемое значение метода handleShot            int shotResult = handleShot(currentPlayerBattleField, currentPlayerField, xShot, yShot);            // если выстрел неудачный, и не один корабль не повреждён, то очередь переходит к следующему игроку          if (shotResult == 0) {                currentPlayerName = player2Name;              currentPlayerField = playerField1;              currentPlayerBattleField = playerBattleField2;            }        }    }/**    * Метод обрабатывает выстрел. Если выстрел удачный, то есть снаряд достиг цели -    * в клетку записывается значение '#' (отображается к в массиве игрока, так и в массиве соперника),    * а также на экран выводится сообщение 'Good shot!'. В этом случае метод возвращает значение 1.    * В случае неудачного выстрела - в массив battleField записывается значение '0' в элемент [y][x], и    * и возвращается значение 0.    * Возвращаемые значения нам нужны для того, чтобы в методе, внутри которого вызывается метод handleShot,    * мы могли понимать, успешно или неуспешно прошёл выстрел. На основе этого мы принимаем решение, * переходит ход к другому игроку или нет.    */    private static int handleShot(char[][] battleField, char[][] field, int x, int y) {        if ('1'.equals(field[y][x])) {            field[y][x] = '#';            battleField[y][x] = '#';            System.out.println("Good shot!");            return 1;        }        battleField[y][x] = '*';        System.out.println("Bad shot!");        return 0;    }/***Метод определяет, не проиграл ли еще игрок. Если у игрока остался хотя бы    * один "раненный" корабль, тогда пользователь продолжает игру.    * То есть, если на карте у игрока остался хотя бы один символ '1', которым мы отмечали    * корабли, то игра продолжается - возвращается значение true. Иначе false.*/    private static boolean isPlayerAlive(char[][] field) {        for (char[] cells : field) {            for (char cell : cells) {                if ('1' == cell) {                    return true;                }            }        }        return false;    }

Думаю, что к комментариям в коде мне добавить нечего.
Единственное, обращу внимание на тонкий момент. Мы привыкли в
математике к записи (x, y) — где первой идёт координат абсцисс, а
второй — координата ординат. Казалось бы, чтобы обратиться к
элементу двумерного массива (иногда срываюсь и называю в тексте
элемент клеткой, но суть не меняется) нужно написать
arr[x][y], но это будет неверно, и чтобы это доказать
воспользуемся неопрвергаемым методом пристального взгляда. Для
примера рассмотрим следующий двумерный массив:

int[][] arr = {{1, 2}, {7, 4}, {8, 3, 5, 9}, {1}}System.out.println(arr[0][1]); // ?System.out.println(arr[1][0]); // ?

Теперь вопрос из квиза «Программирование и мир» — что выведется
на консоль в строках 3 и 4?
Вспоминаем, что двумерный массив — это не совсем таблица (нам так
проще его воспринимать и детектировать его в задачах) — это «массив
массивов» — вложенные массивы. Если в одномерных целочисленных
массивах элементом является целое число, то в случае двумерного
массива — элементом является массив (а в случае трёхмерного массива
— элементом является двумерный массив). Таким образом, первый
индекс указывает, какой по счёту массив мы выбираем. Второй индекс
указывает, какой элемент по счёту мы выбираем в выбранном ранее
массиве. Запись arr[1][2] указывает, что мы обращаемся
к элементу с индексом 2 (то есть 3 по порядку) в массиве с индексом
1 (или второму по порядку). Соответсвенно, в строке 3 в консоль
выведется значение 2, а в строке 4 — 7.

Постепенно подбираемся к концу. Что нам осталось
реализовать?

  1. Вывод имени победителя

  2. Проверка клетки, которую пользователь указал как начало
    корабля

Первое кажется проще, стартанём с него. Потопали в метод
playGame — как вы помните, там есть цикл while, в условии которого
есть проверка — живы ли еще оба игрока. Напомню, что если игрок
«мёртв», то есть у него не осталось ни одного корабля, то игра
прекращается, а выживший игрок считается победителем. Собственно,
единственное, что добавилось — строчка 36 — вызов метода
System.out.println()

/*** Метод реализует логику игры: выстрел и передача хода.*/private static void playGame(String player1Name, String player2Name, char[][] playerField1, char[][] playerField2) {// "карты" выстрелов - создаём двумерные массивы, которые содержат все выстрелы    // удачные (#) и неудачные (*)    char[][] playerBattleField1 = new char[FILED_LENGTH][FILED_LENGTH];    char[][] playerBattleField2 = new char[FILED_LENGTH][FILED_LENGTH];    // вспомогательные переменные, которым будут присваиваться значения текущего игрока -     // игрока, чья очередm делать выстрел. Сначала играет первый игрок, прошу прошения    // за тавтологию    String currentPlayerName = player1Name;    char[][] currentPlayerField = playerField2;    char[][] currentPlayerBattleField = playerBattleField1;    // внутри цикла происходит смена очередности игроков, выстрел, его обработка.    // код внутри цикла выполняется до тех пор, пока "живы" оба игрока - пока у двух игроков    // "частично" цел (ранен) ещё хотя бы один корабль    while (isPlayerAlive(playerField1) &amp;&amp; isPlayerAlive(playerField2)) {      // перед каждым выстрелом выводим в консоль отображение всех выстрелов игрока      printField(currentPlayerBattleField);        // принимаем от пользователя координаты выстрела        System.out.println(currentPlayerName + ", please, input x coord of shot");        int xShot = scanner.nextInt();        System.out.println(currentPlayerName + ", please, input y coord of shot");        int yShot = scanner.nextInt();        // обрабатываем выстрел и получаем возвращаемое значение метода handleShot        int shotResult = handleShot(currentPlayerBattleField, currentPlayerField, xShot, yShot);        // если выстрел неудачный, и не один корабль не повреждён, то очередь переходит к следующему игроку          if (shotResult == 0) {            currentPlayerName = player2Name;              currentPlayerField = playerField1;              currentPlayerBattleField = playerBattleField2;        }    }  System.out.println(currentPlayerName + " is winner!");}

Переходим ко второму пункту наших «остатков» — реализуем
проверку клетки, которую указал пользователь — начало корабля.

Заказчик не уточнил, но будем считать, что правила нашей
компьютерной версии игры в этом плане не отличаются от бумажной
версии — между кораблями должна быть минимум одна клетка — это
означает, что в окрестности с радиусом одна клетка от
располагаемого корабля не должно быть других кораблей. Для
наглядности — смотрим на рисунок.

Окрестность с радиусом одна клетка. Красным кругом помечена стартовая клетка - начало корабля.Окрестность с радиусом одна клетка.
Красным кругом помечена стартовая клетка — начало корабля.

Предлагаю простую версию алгоритма — обходим все клетки, в
которых предполагаем разместить корабль, и проверяем, есть ли в
окрестности с радиусом один каждой клетки другие корабли. Если хотя
бы для одной клетки условие не выполнилось, то есть есть на поле
корабль, который находится «слишком» рядом с планируемым — сообщаем
пользователю о проблеме и просим его ввести новые координаты.

Какие нюансы есть в этом алгоритме? Снова посмотрим на
рисунок.

Неэффективность нашего простого алгоритма. Некоторые клетки проверяем несколько раз.Неэффективность нашего простого
алгоритма. Некоторые клетки проверяем несколько раз.

private static int validateCoordForShip(char[][] field, int x, int y, int position, int shipType) {        // если пользователь хочет расположить корабль горизонтально  if (position == 1) {            for (int i = 0; i < shipType - 1; i++) {if ('1' == field[y][x + i]                                || '1' == field[y - 1][x + i]                                || '1' == field[y + 1][x + i]                                || '1' == field[y][x + i + 1]                                || '1' == field[y][x + i - 1]|| (x + i) > 9) {                    return -1;                }            }        } else if (position == 2) {          // если пользователь хочет расположить корабль вертикально            for (int i = 0; i < shipType - 1; i++) {                if ('1' == field[y][x + i]                        || '1' == field[y - 1][x + i]                        || '1' == field[y + 1][x + i]                        || '1' == field[y][x + i + 1]                        || '1' == field[y][x + i - 1]|| (y + i) > 9) {                    return -1;                }            }        }        return 0;    }

Не забываем проапгрейдить метод fillPlayerField — напомню, что в
этом методе реализована логика по расположению кораблей
игроками.

private static void fillPlayerField(char[][] playerField) {        for (int i = 4; i >= 1; i--) {            // растановка кораблей            for (int k = i; k <= 5 - i; k++) {                System.out.println("Расставляем " + i + "-палубный корабль. Осталось расставить: " + (q + 1));              // иницализируем переменную начальным значением              int validationResult = 1;            while (validationResult != 0) {              System.out.println("Input x coord: ");            x = scanner.nextInt();            System.out.println("Input y coord: ");            y = scanner.nextInt();            System.out.println("1 - horizontal; 2 - vertical ?");            position = scanner.nextInt();                  // если координата не прошла валидацию (проверку), то метод возвращает отрицательное// значение, конечно, оно не равно нулю, поэтому пользователю придётся ввести координаты                  // ещё раз                  validationResult = validateCoordForShip(playerField, x, y, position, i);                }            // если корабль располагаем горизонтально              if (position == 1) {                // заполняем '1' столько клеток по горизонтали, сколько палуб у корабля                for (int q = 0; q < i; q++) {                    playerField[y][x + q] = '1';                }            }                        // если корабль располагаем вертикально            if (position == 2) {                  // заполняем столько клеток по вертикали, сколько палуб у корабля                for (int m = 0; m < i; m++) {                    playerField[y + m][x] = '1';                }            }              // печатаем в консоли поле игрока, на котором будет видно, где игрок уже поставил корабли              // о реализации метода - см. ниже            printField(playerField);        }    }}

Вот мы и написали игру «Морской бой» — level 1. Начали с
простого — простых конструкций, идей и методов. Один класс, нет
коллекций — только массивы. Оказывается на циклах и массивах можно
написать игру.

Мы удовлетворили все требования бизнеса. Доигрывая до конца,
получили отличную оценку от заказчика, он полностью доволен
приложением. Ждём, когда он опробует игру и вернётся снова за
апгрейдом. А тут и будет level — 2.

Всем спасибо, всегда рад обратной связи!

Понравилась статья? Поделить с друзьями:
  • Как написать море маслом видеоурок
  • Как написать море взволнованно
  • Как написать мораль басни
  • Как написать моноспектакль
  • Как написать монолог рассуждение