Проектирование программ "сверху-вниз"

Давайте немного глубже посмотрим в целом на парадигму программирования с использованием функций, условий, циклов, различных типов данных. Все это в целом определяет так называемое структурное программирование. И здесь важно не только уметь формально использовать все те конструкции языка, о которых мы говорили на протяжении наших занятий, но и научиться правильно строить архитектуру программы. Давайте я покажу пример проектирования «сверху-вниз» на примере создания игры «Сапер».

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

 

Как будем ее программировать? Изначально перед нами пустой текстовый файл. И мы вначале пропишем функцию запуска игры в целом:

def startGame():
    """Функция запуска игры: отображается игровое поле,
        игрок открывает любую закрытую клетку,
        результат проверяется на наличие мины или
        выигрышной ситуации
    """
    pass

Далее, вызываем эту функцию:

startGame()
print("Игра завершена")

И когда она завершит свою работу, то по нашему замыслу завершается и сама игра. Причем, сама функция пока ничего не делает, там указан оператор pass, который формально образует тело функции, но никаких действий не выполняет. У нас получился самый высокий уровень в проектировании нашей игры. Своего рода – первая итерация в создании программы. Причем, смотрите, вот эти многострочные комментарии всегда может прочитать сторонний программист, или же вызвать в консоли функцию:

help(startGame)

для просмотра этой подсказки. Рекомендуется такие комментарии писать во всех ключевых функциях проектируемой программы.

Далее, программист решает: ага, нужна вспомогательная функция для отображения игрового поля и выше определяет ее:

def show():
    """Функция отображения состояния текущего
        игрового поля
    """
    pass

Пока она тоже ничего не делает, т.к. идет процесс создания архитектуры программы в целом. И раз есть функция отображения поля, то это поле нужно где-то создавать (располагать мины). Появляется функция:

def createGame():
    """Создание игрового поля: расположение мин
        и подсчет числа мин вокруг клеток без мин
    """
    pass

Затем, на этом же уровне проектирования нам, очевидно, понадобится функция для ввода координат клетки игроком:

def goPlayer():
    """Функция для ввода пользователем координат
        закрытой клетки игрового поля
    """
    pass

Далее, функция проверки текущего состояния игры:

def isFinish():
    """Определение текущего состояния игры:
        выиграли, проиграли, игра продолжается
    """
    pass

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

def startGame():
    """Функция запуска игры: отображается игровое поле,
        игрок открывает любую закрытую клетку,
        результат проверяется на наличие мины или
        выигрышной ситуации
    """
    while isFinish():
        show()
        goPlayer()

Отлично, общий вид программы у нас определен:

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

Одно из них пусть называется PM, а второе – P. Далее, нужно решить где создать эти переменные: внутри функции startGame или глобальными в начале текста программы. Так как глобальных переменных в любой программе должно быть минимум, то объявим их внутри startGame:

    P = [-2]*N*N
    PM = [0]*N*N

Здесь у поля P значение -2 будет говорить о том, что клетка еще не открыта. Значение -1 у поля PM – стоит мина, а любое неотрицательное число у обоих списков – открытая клетка без мины и указанием мин вокруг нее.

А вот величину N – размер игрового поля и число мин M на игровом поле, сделаем для простоты глобальными:

N, M = (5, 10) # размер игрового поля NxN и число мин M

Теперь у нас все готово для реализации первой функции createGame. Сначала мы произвольным образом расставим мины на поле PM. И договоримся так: если клетка имеет значение >= 0, то мины в ней нет и число показывает количество мин вокруг текущей клетки. А любое отрицательное значение – мина есть. Для генерации случайных значений подключим модуль random в начале программы:

import random

и далее, запишем реализацию функции createGame. Ей понадобится доступ к переменной PM, добавим ее в качестве аргумента:

def createGame(PM):

и, затем, в самой функции:

    rng = random.Random()
    n = M
    while n>0:
        i = rng.randrange(N)  # случайное целое [0; N)
        j = rng.randrange(N)
        if PM[i*N+j] != 0:
            continue
        PM[i*N+j] = -1
        n -= 1

Далее, в этой же функции рассчитаем число мин вокруг каждой клетки без мин:

    # вычисляем количество мин вокруг клетки
    for i in range(N):
        for j in range(N):
            if PM[i*N+j] >= 0:
                PM[i*N+j] = getTotalMines(PM, i, j)

Здесь мы создадим вспомогательную функцию getTotalMines:

def getTotalMines(PM, i, j):
    n = 0
    for k in range(-1,2):
        for l in range(-1,2):
            x = i+k
            y = j+l
            if x < 0 or x >= N or y < 0 or y >= N:
                continue
            if PM[x*N+y] < 0:
                n += 1
 
    return n

Все, наша функция createGame готова. Далее, реализуем функцию show и с ее помощью проверим: правильно ли работает createGame:

def show(pole):
    """Функция отображения состояния текущего
        игрового поля
    """
 
    for i in range(N):
        for j in range(N):
            print( str(pole[i*N+j]).rjust(3), end="" )
        print()

И вызовем ее в startGame:

    createGame(PM)
    show(PM)

Следующая функция goPlayer будет иметь такую реализацию:

def goPlayer():
    """Функция для ввода пользователем координат
        закрытой клетки игрового поля
    """
    flLoopInput = True
    while flLoopInput:
        x, y = input("Введите координату через пробел: ").split()
        if not x.isdigit() or not y.isdigit():
            print("Координаты введены неверно")
            continue
       
        x = int(x)-1
        y = int(y)-1
 
        if x < 0 or x >= N or y < 0 or y >= N:
            print("Координаты выходят за пределы поля")
            continue
 
        flLoopInput = False
 
    return (x, y)

Проверим работу этой функции:

    createGame(PM)
    show(PM)
    goPlayer()

Осталось реализовать последнюю функцию isFinish. Здесь договоримся так: если функция возвращает:

  • 1 – игра продолжается;
  • -1 – игрок наступил на мину и игра проиграна;
  • -2 – игрок открыл все клетки без мин – игра выиграна.

Для реализации функции нам потребуются переменные P и PM:

def isFinish(PM, P):
    """Определение текущего состояния игры:
        выиграли, проиграли, игра продолжается
    """
    for i in range(N*N):
        if P[i] != -2 and PM[i] < 0: return -1
        if P[i] == -2 and PM[i] >= 0: return 1
 
    return -2

И окончательно запишем тело функции startGame:

def startGame():
    """Функция запуска игры: отображается игровое поле,
        игрок открывает любую закрытую клетку,
        результат проверяется на наличие мины или
        выигрышной ситуации
    """
    P = [-2]*N*N
    PM = [0]*N*N
 
    createGame(PM)
    while isFinish(PM, P) > 0:
        show(P)
        x,y = goPlayer()
 
        P[x*N+y] = PM[x*N+y]

Если теперь запустить игру, то при ее завершении непонятно: выиграли мы или проиграли. Модифицируем немного функцию startGame:

def startGame():
    """Функция запуска игры: отображается игровое поле,
        игрок открывает любую закрытую клетку,
        результат проверяется на наличие мины или
        выигрышной ситуации
    """
    P = [-2]*N*N
    PM = [0]*N*N
 
    createGame(PM)
    finishState = isFinish(PM, P)
    while finishState > 0:
        show(P)
        x,y = goPlayer()
 
        P[x*N+y] = PM[x*N+y]
        finishState = isFinish(PM, P)
 
    return finishState

И, далее, ее вызов:

res = startGame()
if res == -1:
    print("Вы проиграли")
else:
    print("Вы выиграли")

Теперь мы видим результат игры. (Для более наглядного понимания создания игры смотрите видео этого занятия). Вот пример того как происходит проектирование программ «сверху-вниз». В качестве примера попробуйте аналогичным образом реализовать игру «крестики-нолики».

Видео по теме