Объектно-ориентированное программирование

Классы

Теоретические сведения

Цель работы

Научится создавать новые типы и конструкторы объектов с помощью классов.

Задание

В этой работе нужно переписать игру из предыдущей работы с использованием классов с небольшим добавлением функционала.

Класс Point

Класс Point предназначен для хранения координат поля на игровой доске. Класс очень простой, поэтому, чтобы не тратить время на объяснения что нужно сделать, просто приведем его код:

class Point(
    val x: Int,
    val y: Int
) 

Класс Board

Класс Board отвечает за информацию о положении на игровой доске.

Инициализация объектов

Основной конструктор класса выглядит следующим образом:

class Board(
    val cells: Array<Array<Char>>
) 

В классе должно быть два вторичных конструктора.

Первый вторичный конструктор создает доску из строки. Для его реализации нужно создать в объекте-компаньоне функцию stringToArray(string: String): Array<Char> для перевода строки в массив символов (подсказка: эта функция вызывает функцию Array() и передает ей правильные аргументы), (еще одна подсказка: не забывайте тестировать даже такие простые функции). В самом конструкторе эта функция вызывается для создания каждого элемента внешнего массива (подсказка: тут используется та же функция Array), а в качестве аргумента ей передается подстрока исходной строки, полученная с помощью функции substring для объектов типа String

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

Завершая вопрос инициализации объектов этого класса, добавьте в объект-компаньон переменную var size = 3, отвечающую за размер доски. Все доски во время игры должны быть одного размера, поэтому данная переменная должна быть статическая. Изменяемость переменной обусловлена тем, что при запуске игра должна иметь возможность установить размер доски.

Методы доступа к полям доски

Для безусловного чтения создайте два оператора:

operator fun get(point: Point): Char
operator fun get(point: Array<Int>): Char

Для безопасного чтения создайте функцию, которая проверяет корректность переданных координат

fun getOrNull(point: Point): Char?

После каждого хода в игре будет создаваться новая доска. Для этого реализуйте функцию, которая возвращает новую доску с записанным в нужное поле символом:

fun setAndCopy(point: Point, c: Char)

Проверять корректность координат в этой функции не нужно, поскольку она будет вызываться после проверки правильности хода.

Служебные функции

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

val isFill: Boolean

Для расчета можно использовать внутреннюю функцию класса, можно эту функцию сделать геттером свойства isFill.

Функция печати теперь будет иметь сигнатуру:

 override fun toString(): String

Вместо вывода текста непосредственно на консоль наша программа сначала будет с помощью функций toString различных классов формировать текст, а затем выводить его на консоль целиком. Такой подход упростит тестирование и отладку отдельных классов. Отдельно нужно обратить внимание на ключевое слово override, означающее переопределение функции. Подробнее с этим понятием мы ознакомимся в следующих темах. На данном этапе просто укажите этот модификатор, что позволит корректно выводить на печать или вставлять в другие строки значение объектов для классов, у которых определена такая функция.

Класс State

Класс State нужен для хранения информации о состоянии игры. Состояние игры включает в себя информацию о доске и очереди хода:

class State(
    val board: Board = Board(),
    val turn: Char = 'X',
) 

Для того, что-бы разнообразить используемые приемы программирования, вместо копирующего конструктора реализуем в этом классе функцию copy(): State.

Перепишите в класс State функцию checkWin из предыдущей работы.

При инициализации состояния мы можем рассчитать, закончилась ли игра и с каким результатом. Для этого создайте следующее свойство.

val gameResult: String?  

В случае, если игра закончилась, gameResult содержит строку с сообщением о результате (текст сообщения можно найти в тесте в 1 лабораторной работе). Если игра не закончилась, gameResult = null.

Реализуйте в классе функцию хода:

fun step(point: Point): State?

Данная функцию проверяет, можно ли сделать ход в переданную ей point. Если нет – возвращает null. Если да – возвращает новое состояние игры (с новой доской и новым символ для хода).

Реализуйте в классе функцию override fun toString(): String, которая в случае, если игра закончилась, возвращает строку информацию о победителе или о ничье, а остальных случаях возвращает строку с текущим положением на доске и информации о том, чья очередь хода (см. тесты в первой работе).

Класс Game

Инициализация

Класс Game содержит массив с состояниями игры и индекс текущего состояния. Конструктор класса:

class Game(
    state: State = State()
) 

Массив состояний и индекс текущего состояния не входят в конструктор и должны инициализироваться в теле класса:

val states = ArrayList<State>()
var indexState = 0 

Обратите внимание, что state в данном случае является параметром конструктора, но не является свойством класса. Все состояния будут хранится в массиве состояний. Если хранить текущее состояние дополнительно еще и в отдельной переменной, то каждый раз текущее состояния нужно будет обновлять в двух местах (в массиве и в этой переменной), что неудобно и может привести к ошибкам.

Что-бы не потерять начальное состояние игры, переданное в аргументе конструктора, его нужно переписать в массив состояний. Сделать это нужно в блоке init класса.

Для удобства использования класса, в нем стоит завести также свойство state, по которой будет доступно текущее состояние. При этом это свойство не будет текущее состояние в своем поле, а использовать геттер, который будет доставать его из массива состояний.

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

var gameOver = false
Функции игры.

Основное назначение класса Game – делать ходы. Для этого создайте функцию:

fun step(point: Point): Boolean

Она должна проверить корректность хода в точку point (если нет – вернуть false), вычислить новое состояние и обновить массив состояний и индекс текущего состояний и вернуть true.

В нашей игре предусмотрена возможность брать ходы назад (и вперед). Для этого нужно реализовать функцию:

fun takeBack(shift: Int): Boolean 

Она должна проверить возможность перехода на состояние на shift ходов относительно текущего (если нет – вернуть false) и обновить соответствующим образом индекс текущего состояния и массив состояний.

Если игра закончилась, то функция override fun toString(): String возвращает результата игры, иначе она возвращает номер хода и текущее состояние (см. тесты в первой работе).

Файл main.kt

В этом файле находится основной код программы. Он не содержит ничего нового с точки зрения изучаемой темы, и для экономии времени и возможности проверки разрабатываемых классов приведем его полностью:

package lab3

import outputConsole
import java.io.BufferedReader
import java.io.InputStream
import java.io.PrintStream

fun game(
    inputStream: InputStream = System.`in`,
    output: PrintStream = outputConsole
) {
    val reader = BufferedReader(inputStream.reader())

    val game = Game()
    var finish = false
    output.print(game)
    do {
        output.print("Ваш ход или команда\n")
        val arr = reader.readLine().split(" ")
        if (arr.size != 2)
            finish = true
        else {
            val x = arr[0].toIntOrNull()
            val y = arr[1].toIntOrNull()
            if (x == null || y == null)
                output.print("Неверные координаты или команда\n")
            else {
                if (x == -1)
                    if (game.takeBack(y))
                        output.print(game)
                    else
                        output.print("Неправильная команда\n")
                else
                    if (game.step(Point(x, y)))
                        output.print(game)
            }
        }
    } while (!finish && !game.gameOver)
}


fun main() {
    game()
}