Крестики-нолики на Java MVC с одиночной режиме


На практике шаблона MVC и Unittesting в Java я решил сделать простое консольное приложение крестики-нолики.

Особенности этого приложения являются:

  • Мультиплеер-Режим
  • Синглплеер-режим (что всегда приводит к ничьей)

Мои вопросы:

  • Я применил понятие шаблона MVC правильно?
  • Я реализовал unit-тестов для класса SimpleAI. Мои тесты подходят? Я могу сделать их более динамичными(сейчас их просто проверить на конкретном случае)?
  • Есть какие-то тяжелые не противопоказаны в моем коде я должен смотреть в будущее?

Вот ссылка на GitHub: https://github.com/Baumgartner-Lukas/TTT.git

Код:

Вид:

import controller.GameController;
import controller.SimpleAI;
import model.GameBoard;
import view.GameFieldView;

import java.io.IOException;

public class TicTacToe {

    public static void main(String[] args) throws IOException {
        GameBoard model = new GameBoard();
        GameFieldView view = new GameFieldView();
        SimpleAI sai = new SimpleAI();
        GameController controller = new GameController(model, view, sai);

        controller.play();
    }
}

Модель:

Камни:

package model;

public enum Stone {
        X("X"), O("O"), NONE(" ");

        private final String stone;

        Stone(String stone){
            this.stone = stone;
        }

    @Override
    public String toString() {
        return stone;
    }
}

Игровое поле:

package model;

public class GameBoard {
    public static final int SIZE = 3;
    public static final int TURNS = SIZE * SIZE;

    private Stone grid[][] = new Stone[SIZE][SIZE];

    //Fill the new GameBoard with NONE(" ") Stones
    public GameBoard(){
        for(int r = 0; r < SIZE; r++){
            for(int c = 0; c < SIZE; c++){
                grid[r][c] = Stone.NONE;
            }
        }
    }

    public Stone getStone(int row, int col) {
        return grid[row][col];
    }

    public void setStone(int row, int col, Stone stone) {
            grid[row][col] = stone;
    }
}

Контроллер:

package controller;

import model.GameBoard;
import model.Stone;
import view.GameFieldView;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class GameController {
    private BufferedReader reader;
    private GameFieldView view;
    private GameBoard model;
    private SimpleAI simpleAI;
    private boolean SimpleAIisActive = false;


    public GameController(GameBoard model, GameFieldView view, SimpleAI sai) {
        this.model = model;
        this.view = view;
        this.simpleAI = sai;
        this.reader = new BufferedReader(new InputStreamReader(System.in));
    }

    private int counter = 0; //counter to determine which players turn it is and when the max. turns are reached

    public void play() throws IOException {
        int[] input = new int[2];
        System.out.println("Single- or Multiplayer (S | M): ");
        String opt = reader.readLine();
        if (opt.trim().toLowerCase().equals("s")) {
            SimpleAIisActive = true;
        }
        while (counter < GameBoard.TURNS) {
            //print Field
            view.printGameField(model);
            if (SimpleAIisActive && counter % 2 == 1) {
                simpleAI.updateGameBoard(model);
                if (hasWon()) {
                    System.out.printf("%n AI has won! You noob %n");
                    view.printGameField(model);
                    return;
                }
                System.out.printf("%n AI-Turn %n%n");
                view.printGameField(model);
                counter++;
            }
            //prompt players for their turn
            try {
                input = prompt();
                while (input[0] < 0 || input[0] > 2 || input[1] < 0 || input[1] > 2) {
                    System.err.printf("Row and Col must be between 0 - 2. %n");
                    input = prompt();
                }
                while (!isValidMove(input)) {
                    System.err.printf("This field is already taken! %n");
                    input = prompt();
                }
            } catch (IOException ioe) {
                System.err.println("Error reading input");
                ioe.printStackTrace();
            }
            placeStone(input);
            if (hasWon()) {
                view.printGameField(model);
                System.out.printf("%nPlayer %d has won! GG EZ %n", counter % 2 + 1);
                return;
            }
            counter++;
        }
        view.printGameField(model);
        System.out.println("Game finished with a draw!");
    }

    /**
     * For readability
     *
     * @return returns True if one of those conditions is true
     */
    private boolean hasWon() {
        return checkStraight() || checkDiagonal();
    }

    /**
     * Checks if there are 3 same Stones in a diagonal line
     *
     * @return returns true if 3 Stones are found. False if not.
     */
    private boolean checkDiagonal() {
        //middle Stone
        Stone s = model.getStone(1, 1);
        return s != Stone.NONE && (
                s == model.getStone(0, 0) && s == model.getStone(2, 2) ||
                        s == model.getStone(0, 2) && s == model.getStone(2, 0));
    }

    /**
     * Checks if there are 3 same Stones in a straight line
     *
     * @return returns true if 3 Stones are found. False if not.
     */
    protected boolean checkStraight() {
        int i = 0;
        while (i < 3) {
            Stone sCol = model.getStone(0, i);
            Stone sRow = model.getStone(i, 0);
            if (sCol == model.getStone(1, i) && sCol == model.getStone(2, i) && sCol != Stone.NONE) return true;
            if (sRow == model.getStone(i, 1) && sRow == model.getStone(i, 2) && sRow != Stone.NONE) return true;
            i++;
        }
        return false;
    }

    /**
     * Checks if the user puts a Stone on a valid (empty) position on the board
     *
     * @param input row and col of field where to set the stone
     * @return returns true if the input field is empty
     */
    protected boolean isValidMove(int[] input) {
        int row = input[0];
        int col = input[1];
        return (model.getStone(row, col) == Stone.NONE);
    }

    protected void placeStone(int[] input) {
        int row = input[0];
        int col = input[1];

        if (counter % 2 == 0) {
            model.setStone(row, col, Stone.X);
        } else {
            model.setStone(row, col, Stone.O);
        }
    }

    /**
     * Prompts the player for the position where to set the stone
     *
     * @return returns the inputarray [0] = row, [1] = col
     * @throws IOException Throws an Exception if the inputvalues are out of bound of the gamefield
     */
    private int[] prompt() throws IOException {
        int player;
        int[] input = new int[2];
        player = counter % 2 + 1;
        System.out.println("==========");
        System.out.printf("It is player %d's turn! %n", player);
        System.out.println("Give Row: ");
        input[0] = Integer.parseInt(reader.readLine());
        System.out.println("Give Col: ");
        input[1] = Integer.parseInt(reader.readLine());

        return input;
    }
}

Одиночная "Ай":

package controller;

import model.GameBoard;
import model.Stone;

import static java.util.concurrent.ThreadLocalRandom.current;

public class SimpleAI {
    int counter = 1;

    public SimpleAI() {
    }

    /**
     * public method to use in the GameController class
     * @param model model of the current game board
     */
    protected void updateGameBoard(GameBoard model) {
        alwaysDraw(model);
        counter++;
    }

    /**
     * Adds stones randomly on the field. Easiest difficulty.
     * @param model model of the current game board
     */
    private void addRandomStone(GameBoard model) {
        int row = getRandomNumber();
        int col = getRandomNumber();
        while (model.getStone(row, col) != Stone.NONE) {
            row = getRandomNumber();
            col = getRandomNumber();
        }
        model.setStone(row, col, Stone.O);
    }

    /**
     * Adds stones in a way to the board, that should always lead to a draw
     * @param model model of the current game board
     */
    private void alwaysDraw(GameBoard model) {
        //if there is no stone set in the middle, set a stone in the middle
        if (counter == 1) {
            if (model.getStone(1, 1) == (Stone.NONE) && counter == 1) {
                model.setStone(1, 1, Stone.O);
            } else {
                //if there is a stone in the middle, set the stone in one of the edges
                model.setStone(getRandomEvenNumber(), getRandomEvenNumber(), Stone.O);
            }
        } else {
            if (!checkDiagonal(model)) {
                if (!checkRows(model)) {
                    if (!checkCols(model)) {
                        if (!checkCorners(model)) {
                            checkStraights(model);
                        }
                    }
                }
            }
        }
    }

    /**
     * checks if there is a free space on any of the middle lanes(0:1, 2:1, 1:0, 1:2)
     * @param model current model of the game board
     */
    private void checkStraights(GameBoard model) {
        int r = getRandomNumber();
        int c = getRandomNumber();
        if(model.getStone(r, c) == Stone.NONE && (r + c > 0 && r + c < 4)) {
            model.setStone(r, c, Stone.O);
        }else{
            checkStraights(model);
        }

    }

    /**
     * checks if any of the corners of the game board is free to set a stone
     * @param model current model of the game board
     * @return  true if there was a free corner and a friendly stone was set
     *          false if no corner was empty
     */
    private boolean checkCorners(GameBoard model) {
        int cornerCount = 0;
        for (int r = 0; r < 2; r++) {
            for (int c = 0; c < 2; c++) {
                if (model.getStone(r * 2, c * 2) == Stone.X) {
                    cornerCount++;
                    if (cornerCount < 2 && model.getStone(r * 2, c * 2) == Stone.NONE) {
                        model.setStone(r * 2, c * 2, Stone.O);
                        return true;
                    }
                }
            }
        }
        return false;
    }

    /**
     * Checks if there are two enemy stones already in a diagonal position. If so, make the according counter move
     * If there is no enemy stone in the middle, skip that check.
     * @param model model of the current game board
     * @return  false if there is no enemy stone in the middle.
     *          true if there are two enemy stones in a diagonal pos and a counter move was made.
     */
    private boolean checkDiagonal(GameBoard model) {
        if (model.getStone(1, 1) != Stone.X) return false;
        if (model.getStone(1, 1) == Stone.X &&
                model.getStone(0, 0) == Stone.X &&
                model.getStone(2, 2) != Stone.O) {
            model.setStone(2, 2, Stone.O);
            return true;
        } else if (model.getStone(1, 1) == Stone.X &&
                model.getStone(0, 2) == Stone.X &&
                model.getStone(2, 0) != Stone.O) {
            model.setStone(2, 0, Stone.O);
            return true;
        } else if (model.getStone(1, 1) == Stone.X &&
                model.getStone(2, 0) == Stone.X &&
                model.getStone(0, 2) != Stone.O) {
            model.setStone(0, 2, Stone.O);
            return true;
        } else if (model.getStone(1, 1) == Stone.X &&
                model.getStone(2, 2) == Stone.X &&
                model.getStone(0, 0) != Stone.O) {
            model.setStone(0, 0, Stone.O);
            return true;
        }
        return false;
    }

    /**
     * Checks all rows if two enemy stones are in the same row
     * @param model model of the current game board
     * @return  false if there are no two enemy stones in the same row.
     *          true if there are two enemy stones in the same row and a counter move was made
     */
    private boolean checkRows(GameBoard model) {
        for (int r = 0; r < 3; r++) {
            int stoneCount = 0;
            for (int c = 0; c < 3; c++) {
                if (model.getStone(r, c) == Stone.X) {
                    stoneCount++;
                }else if(model.getStone(r,c) == Stone.O){
                    stoneCount--;
                }
            }
            if (stoneCount == 2) {
                counterMoveRow(model, r);
                return true;
            }
        }
        return false;
    }

    /**
     * Checks columns if for enemy stones
     * @param model model of the current game board
     * @return  false if threre are no two enemy stones in the same column
     *          true if there are two enemy stones in the same column and a counter move was made
     */
    private boolean checkCols(GameBoard model) {
        for (int c = 0; c < 3; c++) {
            int stoneCount = 0;
            for (int r = 0; r < 3; r++) {
                if (model.getStone(r, c) == Stone.X) {
                    stoneCount++;
                }else if(model.getStone(r,c) == Stone.O) stoneCount--;
            }
            if (stoneCount == 2) {
                counterMoveCol(model, c);
                return true;
            }
        }
        return false;
    }

    /**
     * Sets a friendly stone in the appropriate position
     * @param model model of the current game board
     * @param c column in which the two enemy stones were found
     */
    private void counterMoveCol(GameBoard model, int c) {
        for (int r = 0; r < 3; r++) {
            if (model.getStone(r, c) == Stone.NONE) model.setStone(r, c, Stone.O);
        }
    }

    /**
     * Sets a friendly stone in the appropriate position
     * @param model model of the current game board
     * @param r row in which the two enemy stones were found
     */
    private void counterMoveRow(GameBoard model, int r) {
        for (int c = 0; c < 3; c++) {
            if (model.getStone(r, c) == Stone.NONE) model.setStone(r, c, Stone.O);
        }
    }

    /**
     * generates a random integer number with a range between 0 and 2
     * @return random int between 0 and 2
     */
    private int getRandomNumber() {
        return current().nextInt(0, 3);
    }

    /**
     * generates an even random number (0 or 2)
     * used to setting a stone in one of the corners
     * @return random even int (0 or 2)
     */
    private int getRandomEvenNumber() {
        return current().nextInt(0, 2) * 2;
    }

}

Тесты:

package controller;

import model.Stone;
import org.junit.Before;
import model.GameBoard;
import org.junit.Test;

import static org.junit.Assert.*;

public class SimpleAITest {
    private GameBoard model;
    private SimpleAI sai;

    @Before
    public void setUp() throws Exception {
        model = new GameBoard();
        sai = new SimpleAI();

        sai.counter = 2;
    }

    @Test
    public void stoneIsSetCorrectlyRowCheck() {
        setUpForRowCheck();

        sai.updateGameBoard(model);

        assertEquals(Stone.O, model.getStone(0, 2));
    }

    @Test
    public void stoneIsSetCorrectlyColCheck() {
        setUpForColCheck();

        sai.updateGameBoard(model);

        assertEquals(Stone.O, model.getStone(0, 2));
    }

    @Test
    public void stoneIsSetCorrectlyDiagonalCheck() {
        setUpForDiagonal();

        sai.updateGameBoard(model);

        assertEquals(Stone.O, model.getStone(2, 0));
    }

    @Test
    public void stoneIsSetCorrectlyStraightCheck() {
        setUpForStraight();

        sai.updateGameBoard(model);

        assertTrue(model.getStone(1,0) == Stone.O ||
                model.getStone(1,2) == Stone.O);
    }


    private void setUpForStraight() {
        model.setStone(0, 1, Stone.X);
        model.setStone(2, 1, Stone.X);

        model.setStone(1, 1, Stone.X);
    }

    private void setUpForRowCheck() {
        model.setStone(0, 0, Stone.X);
        model.setStone(0, 1, Stone.X);
        model.setStone(2, 0, Stone.X);
        model.setStone(2, 2, Stone.X);

        model.setStone(1, 1, Stone.O);
        model.setStone(2, 1, Stone.O);
    }

    private void setUpForColCheck() {
        model.setStone(2, 2, Stone.X);
        model.setStone(1, 2, Stone.X);

        model.setStone(1, 1, Stone.O);

    }

    private void setUpForDiagonal() {
        model.setStone(1, 1, Stone.X);
        model.setStone(0, 2, Stone.X);

        model.setStone(2, 2, Stone.O);
    }
}


316
4
задан 28 февраля 2018 в 09:02 Источник Поделиться
Комментарии
2 ответа

Спасибо за ваш код.


Я применил понятие шаблона MVC правильно?

Нет.

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

В вашей реализации контроллера делает взаимодействие с пользователем.



Я реализовал unit-тестов для класса SimpleAI. Мои тесты подходят?

Unit-тестов имеют более одной цели:


  • УЦ уточните желаемое поведение тестируемого кода

  • УЦ документа текущее поведение тестируемого кода

  • УЦ являются примерами того, как использовать проверенный код

Вы можете увидеть себя, насколько хорошо ваш код достигает каждой цели...



Я могу сделать их более динамичными(сейчас их просто проверить на конкретном случае)?

UnitTest предназначены , чтобы быть точным.

Каждый метод тест проверяет одного предположения о поведении тестируемого кода. Поэтому нельзя писать "универсального" теста для повторного использования с другими код для проверки.



Есть какие-то тяжелые не противопоказаны в моем коде я должен смотреть в будущее?

Именования

Найти хорошие имена-это самая сложная часть в программировании. Так что всегда берите время, чтобы продумать свои имена идентификаторов.

Пожалуйста, прочитайте (и следовать) именование Java конвенций.

Переменной SimpleAIisActive следует начинать со строчной буквы и так как он держит boolean следует начать с is, has, can или так это может быть isSimpleAiActive.

избегайте имена одного символа

Поскольку количество символов очень ограничено и в большинстве языков, вы скоро кончатся имена. Это означает, что вы должны либо выбрать другого персонажа, который не так явно связаны с целью переменной. И/или вы должны "использовать" имена переменных в различных контекстах. Как делает ваш код трудно читать и понять других людей. (имейте в виду, что вы не что другое лицо самостоятельно, если вы посмотрите на ваш код в несколько месяц!)

С другой стороны в Java-длина имени идентификатора, является практически неограниченным. Нет штрафа за длинные имена идентификаторов. Поэтому не скупитесь с буквами при выборе имен.

предпочитаю решения OOish за процедурных подходов

Нет ничего плохого в процедурный подходы в целом, но в Java - это объектно-ориентированный (ОО) язык программирования, и если вы хотите стать хорошим Java-программистом , то вы должны начать решать проблемы в ОО способом.

Но ООП не означает "разделить" код в случайных классов.

Конечной целью ООП является, чтобы уменьшить дублирование кода, улучшают читаемость и использовать поддержку, а также продление кода.

Делаем ООП означает, что вы будете следовать определенным принципам, которые (среди прочих):


  • сокрытие информации / заключения

  • единая ответственность / разделение

  • одном уровне абстракции

  • Поцелуй (сохранить его простым (и) глупо.)

  • Dry (не повторяй себя.)

  • "Скажи! Не спрашивай".

  • Закон Деметры ("не разговаривай с незнакомцами!")

В ВЫ код изменить текущего пользователя пример процедурного подхода. У вас есть счетчик переменной и вычислить текущий пользователь, основываясь на том, что каждый раз.

Если вы считаете текущий пользователь является объектом лице Stone вы могли бы это так:

private static final int CURRENT_PLAYER = 0;
private final List<Stone> players = new ArrayList<>(Arrays.asList(Stone.X,Stone.O));
//...
protected void placeStone(int[] input) {
int row = input[0];
int col = input[1];
model.setStone(row, col, players.get(CURRENT_PLAYER));
}
//...
protected void updateGameBoard(GameBoard model) {
alwaysDraw(model);
players.add(players.remove(CURRENT_PLAYER));
}
//...
System.out.printf("%nPlayer %d has won! GG EZ %n", players.get(CURRENT_PLAYER));

2
ответ дан 28 февраля 2018 в 08:02 Источник Поделиться


не могли бы вы пожалуйста ответить на этот вопрос более подробно? "1: Unit-тестов имеют более чем одну цель: [...]"

УЦ уточните желаемое поведение тестируемого кода

Модульный тест сделать (вроде).

Один важный момент о модульных тестов заключается в том, что они испытывают единицы "общественное" поведение в изоляции. Это означает, что любой другой код, не принадлежащих к тестируемой единицы должны быть заменены тестовых дублей. Самый простой способ для создания тестовых двойников, чтобы использовать насмешливый рамок (как Mockito).

"Общественное поведение" блока-это возвращение ценности и ее связи с другими единицами (и не обязательно public методы). Насмешливый рамок, обеспечивают инфраструктуру для проверки этого сообщения.

Вы код проверяет блок SimpleAI. Вы проверить поведение тестируемого блока, глядя на изменение состояния другого объекта: GameBoard. Это зависит от блока GameBoard чтобы работать правильно. Это хорошо для различных типов тестирования: интеграционное тестирование, которое проверяет, что блоки работают вместе правильно. Но для модульного тестирования следствием является то, что, если тест не пройден, вы не можете сразу сказать, если это из-за дефекта в блоке SimpleAI или в блок GameBoard. У вас есть для отладки.

Если ваш тест был, как это было бы никаких сомнений:

public class SimpleAITest {        
@Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
@Mock private GameBoard model;
private SimpleAI sai;

@Before
public void setUp() throws Exception {
doReturn(Stone.NONE).when(model).getStone(anyInt(), anyInt());
sai = new SimpleAI();
sai.counter=2;
}

@Test
public void stoneIsSetCorrectlyRowCheck() {
setUpForRowCheck();

sai.updateGameBoard(model);

Mockito.verify(model).setStone(0, 2, Stone.O));
}
private void setUpForRowCheck() {
doReturn(Stone.X).when(model).getStone(0, 0);
doReturn(Stone.X).when(model).getStone(0, 1);
doReturn(Stone.X).when(model).getStone(2, 0);
doReturn(Stone.X).when(model).getStone(2, 2);

doReturn(Stone.O).when(model).getStone(1, 1);
doReturn(Stone.O).when(model).getStone(2, 1);
}
}

Так как ваша текущая модель-это всего лишь оболочка для массива, можно утверждать, что этот "перебор" с выплатами нет, и Ваше здоровье в порядке.

Ноэто учебный пример.

Что если ваша модель есть в базе? С использованием реальных баз данных в тесты будет делать их медленно, и они не удастся, если ваша база данных не доступна по каким-то причинам (плохая сеть, служба не запущена и тому подобное).

Или что, если у вас модель какая-то проверка?
Е. Г.: предварительное условие является недействительным, поскольку игроку X не может быть 2 больше камней на доске, то игрок O. Если ваш совет будет убедиться, что вы не могли сделать эту установку.

Короче: ваши тесты провалятся на то причин...

Поэтому для модульных тестов в подготовке проектов вам следует использовать глумится зависимостей просто привыкнуть к этому. В реальных проектах вы только глумиться зависимостей, которые имеют бизнес-логики (которая Ваша GameBoard класса нет).

УЦ документа текущее поведение тестируемого кода

Это начинается с имени тестового метода.


@Test
public void stoneIsSetCorrectlyRowCheck() {

Это название оставляет нас с вопросом: Что значит "правильный" в смысле?

Имя должно выражать надежду проверил как можно точнее. В отличие от метода имена в коде метод испытания имена должны быть многословным:

@Test
public void preferesCornerOverEdgeToBlockOpponentsWin() {

УЦ являются примерами того, как использовать проверенный код

Там не много, чтобы сказать здесь.

Тот факт, что у вас есть модульные тесты достигнет этой цели.

Но вы должны внимательно посмотреть на тест:


  • - имя вызываемого метода действительно выражают то, что он делает?

  • Контрольная объясните значение параметров

  • Контрольная объяснить (для этого теста) соответствующего свойства параметров?

1
ответ дан 1 марта 2018 в 08:03 Источник Поделиться