Быстрая игра, сделанная за считанные минуты с помощью простого кода Python

Введение

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

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

Просто взгляните на мою статистику Github!

Как проблематично, оказывается, я никогда в жизни не оставлял блокнотов. Тем не менее, счетчик языков, используемый в этом примере, является своего рода мошенничеством, потому что он определяется размером файла — Jupyter Notebook содержит много данных о самых разных вещах, в нем гораздо больше кода, чем просто обычный код, который есть моя точка зрения. При этом, поскольку Python занимает всего лишь 0,08% от размера моих файлов на Github, я думаю, что мне, вероятно, следует начать проект Python. Код для этого проекта доступен в следующем репозитории:



Получение основных визуальных эффектов

Сегодня мы собираемся создать аккуратный визуальный интерфейс для просмотра выходных данных игры через CLI с ASCII-графикой… Вот ссылка на Github для первой половины ветки этой статьи. Вот ссылка на ветку:



Давайте сначала импортируем все наши зависимости и любые новые зависимости, которые мы могли получить:

import click as clk
from numpy import random as rnd
from time import sleep
from os import system, name

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

class PlayGrid:
def __init__(self, players):
        self.players = players

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

class PlayGrid:
def __init__(self, players):
        self.players = players
        draw_grid()
    def update(self, message):
        pass
def move(dir, speed):
        pass
def draw_grid():
    pass

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



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

В любом случае… Это будет лучше, если я просто напишу код и объясню после, так как я думаю, что композиция будет более подходящей и сделает систему в целом более понятной.

Сетки

def empty_grid():
        self.grid = dict()
        str = ""
        for row in range(1, 10):
            str = ""
            for i in range(1, 10):
                str += "`"
            self.grid[row] = str
        return(self.grid)

Первая функция, которую я добавляю в этот класс, будет функцией, которая будет использоваться для создания пустой версии, в которую будут добавляться наши маленькие игроки. Я собираюсь просто заполнить некоторые строки символом “`". Мы сможем индексировать фактическую строку, с которой мы работаем, вызвав словарь self.grid, и мы сможем использовать пары строковых значений словаря для установки индексов по char индивидуально. Мы будем вызывать это внутри нашей другой функции сетки:

def draw_grid():
        self.empty_grid()

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

def draw_grid():
        self.empty_grid()
        print_s = "\n"

Псевдоним print_s является сокращением от строки печати. Наши предыдущие данные вместе с их будущими мутациями и серией этих регулярных выражений станут нашим окончательным однострочным оператором печати с предоставленным ему только одним типом, что довольно удобно. Мы будем использовать эту функцию для тестирования функции empty_grid() путем итеративного объединения строк и последующей их печати. Вот что я придумал:

def draw_grid():
        empty_grid()
        print_s = "\n"
        for key in self.grid:
            print_s = print_s + self.grid[key] + "\n"
        return(print_s)
def empty_grid():
        self.grid = dict()
        str = ""
        for row in range(1, 10):
            str = ""
            for i in range(1, 10):
                str += "`"
            self.grid[row] = str

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

Выкладка инициализации

Теперь мы на секунду сосредоточимся на инициализации и функции, которая потребуется для обновления всей распечатки. Эта функция обновления теперь будет вызывать функцию Grid. Мне также пришлось сделать функцию clear().

def clear():
    if name == 'nt':
        _ = system('cls')
    else:
        _ = system('clear')

Эта функция методизирована и определена глобально, так как цель состоит в том, чтобы быстро очистить весь REPL с помощью этой команды. Он просто вызывает системную команду очистки для соответствующего типа терминала. Мы спрашиваем, является ли имя NT, как в Windows NT, если это так, мы используем cls. Если это не так, мы используем clear, потому что в большинстве случаев другие системы будут Unix-line. Вот функция обновления:

def update(self, message):
        clear()
        grid = self.draw_grid()
        print(grid)
        print(string("\n", message))

Здесь происходит несколько вещей, во-первых, любой предыдущий вывод очищается. После этого мы назначаем переменную в области видимости этой функции, называемую сеткой, которая является просто возвратом от self.draw_grid. Здесь нам не обязательно вызывать return, но в данном случае это удобно, потому что мы вообще не хотим, чтобы наша сетка видоизменялась, пока мы с ней работаем. Если бы мы использовали область видимости класса для такого определения, она могла бы измениться в другом месте во время работы этой функции.

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

def __init__(self, players):
        self.players = players
        update()

Все, что я здесь делаю, это назначаю атрибуты класса player предоставленным параметрам player, а затем вызываю update. Теперь давайте полностью рассмотрим этот класс:

class PlayGrid:
def __init__(self, players):
        self.players = players
        self.update("Hello")
def update(self, message):
        clear()
        grid = self.draw_grid()
        print(grid)
        print(string("\n", message))
def draw_grid(self):
        self.empty_grid()
        print_s = "\n"
        for key in self.grid:
            print_s = print_s + self.grid[key] + "\n"
        return(print_s)
def empty_grid(self):
        self.grid = dict()
        str = ""
        for row in range(1, 10):
            str = ""
            for i in range(1, 10):
                str += "`"
            self.grid[row] = str

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

[emmac@fedora CharacterClash]$ python3 character_clash.py
`````````
`````````
`````````
`````````
`````````
`````````
`````````
`````````
`````````
Traceback (most recent call last):
  File "/home/emmac/dev/CharacterClash/character_clash.py", line 48, in <module>
    main()
  File "/home/emmac/dev/CharacterClash/character_clash.py", line 8, in main
    game = PlayGrid(players)
  File "/home/emmac/dev/CharacterClash/character_clash.py", line 16, in __init__
    self.update("Hello")
  File "/home/emmac/dev/CharacterClash/character_clash.py", line 22, in update
    print(string("\n", message))

Упс

Я сделал критическую ошибку «Я программист Джулия, извините». Нам нужно использовать оператор сложения для конкатенации этих строк:

print("\n" + message)
`````````
`````````
`````````
`````````
`````````
`````````
`````````
`````````
`````````
Hello
Hello

Игроки

К счастью, в этой игре главное — результат.

и наблюдайте, как заблудшие души с искусственным интеллектом сражаются насмерть для вашего развлечения в вашем веб-браузере ™.

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

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

# Sword # bow # assassin
# Classes = ["o/", "o)", "o-"]
# stats = [speed, damage, range, time]
stats_dict = {"o/" : [2, 25, 2, 3],
"o)" : [2, 35, 3, 5],
"o-" : [3, 20, 3, 2]}

Вот наш базовый класс:

class Player:
    def __init__(self, pos):
        pass

Загрузка данных

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

class Player:
    def __init__(self, pos):
        self.pos = []
        self.health = 100
        self.blocking = True
        self.attacking = False
        type = random.choice(stats_dict.keys())
        self.speed = stats_dict[type][1]

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

class Player:
    def __init__(self, pos):
        self.pos = []
        self.health = 100
        self.blocking = True
        self.attacking = False
        type = random.choice(stats_dict.keys())
        self.speed = stats_dict[type][0]
        self.damage = stats_dict[type][1]
        self.range = stats_dict[type][2]
        self.time = stats_dict[type][3]

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

stats_dict = {"o/" : [2, 25, 2, 3],
"o)" : [2, 35, 3, 5],
"o-" : [3, 20, 3, 2]}
class Player:
    def __init__(self, pos):
        self.pos = []
        self.health = 100
        self.blocking = True
        self.attacking = False
        type = random.choice(stats_dict.keys())
        self.speed = stats_dict[type][0]
        self.damage = stats_dict[type][1]
        self.range = stats_dict[type][2]
        self.time = stats_dict[type][3]
        self.symbol = type
    def walk(self, pos):
        pass
    def move(self, players):
        pass

Функция move(players) служит для получения массива игроков и выбора того, что делать, основываясь на этом. На данный момент все это будет отложено, так как сейчас мы фактически вернемся к нашему старому классу PlayGrid, а затем начнем отображать позиции этих игроков.

Объединение элементов

Нам нужно будет определить новую функцию в нашем классе PlayGrid, чтобы взять наш словарь сетки из более раннего и изменить его, чтобы он содержал эти символы. Хорошая новость заключается в том, что мы можем просто использовать индексы для отображения наших игроков в нужном месте.

class PlayGrid:
        # Essentials
    def __init__(self, players):
        self.players = players
        self.update("Character Crash Game Started")
def update(self, message):
        clear()
        grid = self.draw_grid()
        print(grid)
        print("\n" + message)
# Player Management
    def draw_players(grid):

Рисование игроков

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

for player in self.players:
            # True = right, False = left
            if player.facing == True
                modifier = 1
            else:
                modifier = -1

Нам нужно будет добавить этот модификатор к индексу, чтобы определить, должно ли значение быть позади или перед символом. Остальное мы обработаем внутри класса Player. Мы пока не будем всего этого делать, так как нам нужно написать функцию. Вы могли заметить, что я также открыл цикл for. Это очень важно, потому что нам нужно обращаться к данным каждого игрока индивидуально, чтобы выполнить с ними необходимую работу.

Методология, лежащая в основе этого, проста. В словаре ключами являются y нашей координатной плоскости. Они просто генерируются из генератора диапазонов, как мы это делаем в функции empty_grid(). Затем у нас есть x, который является парой значений для словаря. Имея это в виду, чтобы установить символ в определенную позицию, нам нужно проиндексировать словарь с помощью нашего ключа y, который занимает вторую позицию в нашем списке pos, а затем нам нужно проиндексировать возврат этого с помощью нашего значения x, которое — это позиция, в которой символ, который нам нужно заменить, находится в нашей строке.

# [0] = x, [1] = y
            newpos = player.pos
            x, y = newpos[0]
            grid[y][x] = player.symbol[0]
            grid[y][x + modifier] = player.symbol[1]

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

def draw_players(grid):
        for player in self.players:
            # True = right, False = left
            if player.facing == True
                modifier = 1
            else:
                modifer = -1
                # [0] = x, [1] = y
            newpos = player.pos
            x, y = newpos[0]
            grid[y][x] = player.symbol[0]
            grid[y][x + modifier] = player.symbol[1]
        return(grid)

Теперь, когда мы написали эту функцию, мы собираемся написать еще одну функцию, которая будет называться make_moves(players). Эта функция просто вызовет функцию перемещения для наших игроков.

def make_moves(players):
        [player.move(players) for player in players]
        return

Перемещение в этом смысле не похоже на перемещение из [x,y] в [x,y], для этого и предназначена наша функция walk(). Вместо этого наша функция перемещения определяет их очередь что-то делать. Теперь мы вернемся к нашему классу Player и разберем базовую структуру того, как эта штука может первоначально реагировать на окружающую среду. В будущем я собираюсь внедрить алгоритм машинного обучения для этого проекта, и это сделает этот проект намного круче.

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

def move(self, players):
        if self.blocking == False
            self.pos += 1
            self.blocking = True
        elif self.blocking == True:
            self.pos -= 1
            self.blocking = False
self.blocking = False

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

def move(self, players):
        if self.blocking == False:
            self.pos += 1
            self.blocking = True
        if self.blocking == True:
            self.pos -= 1
            self.blocking = False
# self.blocking = False

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

class Player:
    def __init__(self, pos):
        self.pos = []
        self.health = 100
        self.blocking = True
        self.attacking = False
        type = random.choice(stats_dict.keys())
        self.speed = stats_dict[type][0]
        self.damage = stats_dict[type][1]
        self.range = stats_dict[type][2]
        self.time = stats_dict[type][3]
        self.symbol = type
        self.facing = True

До сих пор…

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

def main():
    players = []
    game = PlayGrid(players)
    game.update("Hello")
#    while len(game.players) < 1:
    sleep(2)

Здесь нам нужно добавить игрока в список игроков. Это довольно просто, мы просто создадим нового игрока — в конце концов, единственное, что ему сейчас требуется, — это позиция. Вот переработанная функция main():

def main():
    players = []
    players.append(Player([5, 6]))
    game = PlayGrid(players)
    game.update("Up")
#    while len(game.players) < 1:
    sleep(2)
    game.update("Down")
    sleep(2)
    game.update("Up")
    sleep(2)
    game.update("Down")

Надеюсь, я правильно это помню!

[emmac@fedora CharacterClash]$ python3 character_clash.py
File "/home/emmac/dev/CharacterClash/character_clash.py", line 43, in draw_players
    x, y = newpos[0], newpos[1]
IndexError: list index out of range

Давайте посмотрим…

Проблема пришла отсюда:

class Player:
    def __init__(self, pos):
        self.pos = []

Я хотел предоставить pos, а затем установить его, однако он был установлен в пустой список — забавно.

File "/home/emmac/dev/CharacterClash/character_clash.py", line 44, in draw_players
    grid[y][x] = player.symbol[0]
TypeError: 'str' object does not support item assignment

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



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

Решение нашей проблемы

Для решения этой проблемы мы собираемся восстать против Python, сделав это невероятно простым. Первое, что мы собираемся сделать, это привести нашу строку к типу списка. Мы знаем, что можем установить индекс этого типа, поэтому мы знаем, что этот метод будет эффективным. Затем мы будем использовать str.join(), чтобы соединить нашу строку с нашим новым списком строк.

>>> list("Hello")
['H', 'e', 'l', 'l', 'o']
>>> "".join(list("Hello"))
'Hello'
>>>

Давайте вернемся к функции, которая вызывает это:

def draw_players(self, grid):
        for player in self.players:
            # True = right, False = left
            if player.facing == True:
                modifier = 1
            else:
                modifer = -1
                # [0] = x, [1] = y
            newpos = player.pos
            x, y = newpos[0], newpos[1]
            grid[y][x] = player.symbol[0]
            grid[y][x] + modifier] = player.symbol[1]
        return(grid)

Мы начнем с преобразования сетки в список в нижней части цикла for:

newpos = player.pos
current = list(grid[newpos[1]])

Теперь у нас есть current, который представляет собой строку нашего текущего столбца, полученную путем получения нашего значения y, второй позиции в нашем списке newpos (1, а не 2).

Вот последняя новая функция:

def draw_players(self, grid):
        for player in self.players:
            # True = right, False = left
            if player.facing == True:
                modifier = 1
            else:
                modifer = -1
                # [0] = x, [1] = y
            newpos = player.pos
            current = list(grid[newpos[1])
current[newpos[0]] = player.symbol[0]
            current [newpos + modifier] = player.symbol[2]
            current = "".join(current)
            grid[y] = current
        return(grid)

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

def draw_players(self, grid):
        for player in self.players:
            # True = right, False = left
            if player.facing == True:
                modifier = 1
            else:
                modifer = -1
                # [0] = x, [1] = y
            newpos = player.pos
            current = list(grid[newpos[1]])
            current[newpos[0]] = player.symbol[0]
            current[newpos[0] + modifier] = player.symbol[1]
            current = "".join(current)
            self.grid[newpos[1]] = current

Мне также пришлось сделать несколько настроек здесь и там, в основном

  • Пришлось подправить функцию move() плеера, позиции почему-то просто вызывали .pos, не индексировались.
  • Было несколько глупых ошибок при индексировании и места, где я забыл написать self.
  • Мне пришлось немного изменить основную функцию и возвращаемые функции draw_grid(), empty_grid() и update().

Вот наш новый класс:

class PlayGrid:
        # Essentials
    def __init__(self, players):
        self.players = players
        self.update("Character Clash Game Started")
def update(self, message):
        clear()
        self.empty_grid()
        self.draw_players(self.grid)
        self.make_moves()
        print(self.draw_grid())
        print("\n" + message)
# Player Management
    def draw_players(self, grid):
        for player in self.players:
            # True = right, False = left
            if player.facing == True:
                modifier = 1
            else:
                modifer = -1
                # [0] = x, [1] = y
            newpos = player.pos
            current = list(grid[newpos[1]])
            current[newpos[0]] = player.symbol[0]
            current[newpos[0] + modifier] = player.symbol[1]
            current = "".join(current)
            self.grid[newpos[1]] = current
def make_moves(self):
        [player.move(self.players) for player in self.players]
# Grid
    def draw_grid(self):
        print_s = "\n"
        for key in self.grid:
            print_s = print_s + self.grid[key] + "\n"
        return(print_s)
def empty_grid(self):
        self.grid = dict()
        str = ""
        for row in range(1, 30):
            str = ""
            for i in range(1, 100):
                str += "`"
            self.grid[row] = str

А вот и наша новая функция main():

def main():
    players = []
    players.append(Player([5, 6]))
    game = PlayGrid(players)
    game.update("Up")
#    while len(game.players) < 1:
    sleep(2)
    game.update("Down")
    sleep(2)
    game.update("Up")
    sleep(2)
    game.update("Down")

Теперь давайте запустим его!

[emmac@fedora CharacterClash]$ python3 character_clash.py

Не наполовину плохо!

Движение/Поиск пути

Следующее большое препятствие, которое нам нужно преодолеть, — это движение. Как мы собираемся сделать так, чтобы персонажи определяли, как двигаться в каждом кадре? Ну, мы начнем с добавления в класс новых данных, чтобы показать некоторые вещи об окружающем нас мире. Всякий раз, когда я программирую искусственный интеллект для этого проекта, они становятся функциями нашей модели. Прежде чем мы углубимся в это, я также хотел упомянуть одну вещь — код для этого проекта пока находится в ветке основных функций, теперь мы собираемся перейти к боевой ветке. Эта ветвь будет больше сосредоточена на передвижении и боях, так что наши новые формы действительно будут что-то делать.

Вот ссылка на ветку, над которой мы работали раньше:



А вот ссылка на тот, на котором мы сейчас находимся:



Давайте вернемся к нашей функции перемещения:

def move(self, players):
        if self.blocking == False:
            self.pos[1] -= 1
            self.blocking = True
        elif self.blocking == True:
            self.pos[1] += 1
            self.blocking = False

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

двигаться()

def move(self, players):
         self.blocking = False

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

class Player:
    def __init__(self, pos):
        self.pos = pos
        self.health = 100
        self.blocking = True
        self.attacking = False
        type = random.choice(["o/", "o)", "o-"])
        self.speed = stats_dict[type][0]
        self.damage = stats_dict[type][1]
        self.range = stats_dict[type][2]
        self.time = stats_dict[type][3]
        self.symbol = type
        self.facing = True
        self.attacking = False
        self.pursuing = None
        self.attackavailable = False
        self.attacks_available = []
        self.turns = 0

Значение self.turns будет работать в системе поворотов, для которой после этого мы создадим менеджер. Мы рассмотрим специфику этого, как только окажемся там. А пока давайте сосредоточимся на функции, которая будет направлять этих игроков в том, что они делают:

def move(self, players):
         self.blocking = False
         self.attacking = False
         selection = 1
         selections = []
         param = ""

Первым делом мы инициализируем некоторые переменные. Их много, но по уважительной причине — это простой подход к созданию некоторого условного поведения, но алгоритм, безусловно, имеет смысл и потенциально может быть расширен. Когда все будет сказано и сделано, я планирую, чтобы этот класс вместо этого вызывал ИИ. Я также добавил, чтобы установить для атаки значение false, поскольку, если мы сейчас движемся, мы не можем делать ни того, ни другого — что может иметь больше смысла, когда мы рассмотрим основные правила игры и то, как будет работать система управления.

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

for player in players:
             if player.pos[1] == self.pos[1] and player.pos[0] == self.pos[0]:
                 pass
             else:
                 if attackavailable == True:
                     # walk = 1, block = 2, attack = 3
                     if self.health > 45 and index in attacks_available:
                         if player.attacking == True:
                             selection = 3
                             self.pursuing = index
                         else:
                         if player.health > 35 and self.health < 50:
                                 self.pursuing = player.pos
                                 selection = 2
                 else:
                     selection = random.choice([1, 2, 3])
             selections.append(selection)

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

mu = sum(selections) / len(selections)
         selection = int(round(mu))

Затем, наконец, у меня будут вызовы функций для каждого решения в конце:

if selection == 1:
             if self.pursuing != None:
                 self.pursue()
             else:
                 self.random_walk()
         if selection == 2:
              pass
         if selection == 3:
              pass

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

def random_walk(self):
                # 1 r, 2 l, 3 up, 4, down
        dir = random.choice([1, 2, 3, 4])
        if dir == 1:
            self.pos[0] += self.speed
        elif dir == 2:
            self.pos[0] -= self.speed
        elif dir == 3:
            self.pos[1] += self.speed
        self.turns = 1

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

Блок/Атака

Если вы помните, ранее я заявлял, что не добавлял в этот класс функцию преследования(). Причина, по которой я сделал это, заключалась в том, чтобы проверить точность значения преследования. Это потому, что всякий раз, когда этот метод вызывается, мы должны получить сообщение об ошибке. Однако нам нужна еще одна функция, чтобы это работало, и эта функция — функция, доступная для атаки. Чтобы запустить эту функцию, я собираюсь написать тот же цикл, который мы написали ранее. Единственная разница в том, что на этот раз я хочу определить, находится ли значение в пределах досягаемости для атаки, вот мой первый взлом такой функции:

def attack_available(self, players):
        for player in players:
            if player.pos[0] != self.pos[0] and self.pos[1] != player.pos[1]:
                if abs(player.pos[0] - self.pos[0]) <= self.range:
                    self.attacks_available.append(player.id)
                elif abs(player.pos[1] - self.pos[1]) <= self.range:
                    self.attacks_available.append(player.id)

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

def main():
    players = []
    players.append(Player([5, 6], 1))
    players.append(Player([40, 20], 2))
    game = PlayGrid(players)
    for i in range(1, 25):
        sleep(3)
        game.update("".join(["Iteration: ", str(i)]))

Этот код работает безупречно. Теперь немного подправим.

Сенсорные окна

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

def random_walk(self):
                # 1 r, 2 l, 3 up, 4, down
        dir = random.choice([1, 2, 3, 4])
        self.walk(dir)
def walk(self, dir):
        if dir == 1:
            if not self.pos[0] + self.speed >= CHAR_H - 1:
                self.pos[0] += self.speed
                self.facing = True
elif dir == 2:
            if not self.pos[0] - self.speed <= 2:
                self.pos[0] -= self.speed
                self.facing = False
elif dir == 3:
            if not self.pos[0] - self.speed <= 2:
                self.pos[1] -= self.speed
        elif dir == 4:
            if not self.pos[0] + self.speed >= CHAR_W - 1:
                self.pos[1] += self.speed
        self.turns += 1

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

def main():
    players = []
    players.append(Player([11, 20], 0))
    players.append(Player([5, 6], 1))
    players.append(Player([20, 10], 2))
    game = PlayGrid(players)
    for i in range(1, 50):
        sleep(.5)
        game.update("".join(["Iteration: ", str(i)]))
class PlayGrid:
        # Essentials
    def __init__(self, players):
        self.players = players
        self.update("Character Clash Game Started")
def update(self, message):
        clear()
        self.empty_grid()
        self.draw_players(self.grid)
        self.make_moves()
        print(self.draw_grid())
        print("\n" + message)
# Player Management
    def draw_players(self, grid):
        for player in self.players:
            modifier = 0
            # True = right, False = left
            if player.facing == True:
                modifier = 1
            else:
                modifer = -1
                # [0] = x, [1] = y
            newpos = player.pos
            current = list(grid[newpos[1]])
            current[newpos[0]] = player.symbol[0]
            current[newpos[0] + modifier] = player.symbol[1]
            current = "".join(current)
            self.grid[newpos[1]] = current
def make_moves(self):
        [player.move(self.players) for player in self.players]
# Grid
    def draw_grid(self):
        print_s = "\n"
        for key in self.grid:
            print_s = print_s + self.grid[key] + "\n"
        return(print_s)
def empty_grid(self):
        self.grid = dict()
        str = ""
        for row in range(1, CHAR_W):
            str = ""
            for i in range(1, CHAR_H):
                str += "`"
            self.grid[row] = str
# Sword # bow # assassin
# Classes = ["o/", "o)", "o-"]
# stats = [speed, damage, range, time]
stats_dict = {"o/" : [2, 25, 2, 3],
"o)" : [2, 35, 3, 4],
"o-" : [3, 20, 1, 2]}
class Player:
    def __init__(self, pos, id):
        self.id = id
        self.pos = pos
        self.health = 100
        self.blocking = True
        self.attacking = False
        type = random.choice(["o/", "o)", "o-"])
        self.speed = stats_dict[type][0]
        self.damage = stats_dict[type][1]
        self.range = stats_dict[type][2]
        self.time = stats_dict[type][3]
        self.symbol = type
        self.facing = True
        self.attacking = False
        self.pursuing = None
        self.attackavailable = False
        self.attacks_available = []
        self.turns = 0
        # Base
    def random_walk(self):
                # 1 r, 2 l, 3 up, 4, down
        dir = random.choice([1, 2, 3, 4])
        self.walk(dir)
def walk(self, dir):
        if dir == 1:
            if not self.pos[0] + self.speed >= CHAR_H - 1:
                self.pos[0] += self.speed
                self.facing = True
elif dir == 2:
            if not self.pos[0] - self.speed <= 2:
                self.pos[0] -= self.speed
                self.facing = False
elif dir == 3:
            if not self.pos[0] - self.speed <= 2:
                self.pos[1] -= self.speed
        elif dir == 4:
            if not self.pos[0] + self.speed >= CHAR_W - 1:
                self.pos[1] += self.speed
        self.turns += 1
# Behaviors
    def attack_available(self, players):
        for player in players:
            if player.pos[0] != self.pos[0] and self.pos[1] != player.pos[1]:
                if abs(player.pos[0] - self.pos[0]) <= self.range:
                    self.attacks_available.append(player.id)
                elif abs(player.pos[1] - self.pos[1]) <= self.range:
                    self.attacks_available.append(player.id)
def move(self, players):
         self.attacks_available = []
         self.blocking = False
         self.attacking = False
         self.attack_available(players)
         selection = 1
         selections = [1, 1, 1, 1, 2, 2]
         if len(self.attacks_available) > 0:
             selections.append(3)
         selection = random.choice(selections)
         if selection == 1:
             if self.pursuing != None:
                 self.pursue()
             else:
                 self.random_walk()
         if selection == 2:
              self.blocking = True
              self.turns += 1
         if selection == 3:
             self.attacking = True
             self.call_attack()
    def call_attack(self):
        pass
    def pursue(self):
        pass

Заключение

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

Все, что понадобится этому коду, когда мы возобновим работу над этой частью, — это некоторый код атаки и менеджер, который будет запускать эти атаки и другие вещи, сопоставленные с правилами и логикой игры. Большое спасибо за чтение, я надеюсь, что этот проект был для вас максимально приятным, и я надеюсь, что вы взволнованы тем, как далеко я собираюсь зайти, как я! Хорошего дня!

Еще одна вещь, вот GIF того, что мы создали: