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

Я разрабатываю RogueLike на python, и я стараюсь изо всех сил использовать ООП и свои небольшие знания, чтобы создать курс python для студентов.

mapRogue = ['~~~~~~~~~~',
            '~~~~.....Y',
            'YYYYY+YYYY',
            'YYYY....YY']

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

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

Класс DOOR с использованием наследования

class Tile(object):
    #a tile of the map and its properties
    def __init__(self, name, char, position, blocked, color=(255, 255, 255), bgcolor=(0, 0, 0)):
        self.name = name
        self.blocked = blocked
        self.char = char
        self.color = color
        self.bgcolor = bgcolor
        self.position = position

class Door(Tile):
    def __init__(self,  name, char, position, blocked, bgcolor, key,color=(255, 255, 255), open=False ):
        Tile.__init__( self,name, char, position, blocked, color, bgcolor)
        self.state = open
        self.key = key

    def opening(self, key):
        if self.key == key:
            self.state = True

tilesObject = {".": {"name": 'floor', "obj": Tile, "bgcolor": (233, 207, 177), "block": False},
               "Y": {"name": 'forest', "obj": Tile, "bgcolor": (25, 150, 64), "block": True},
               "~": {"name": 'water', "obj": Tile, "bgcolor": (10, 21, 35), "block": False},
               "+": {"name": 'doors', "obj": Door, "bgcolor": (10, 10, 25), "block": False}}
import types
def load(mymap):
    tileMap = []
    x, y = (0,0)
    for line in mymap:
        tileLine = []
        for value in line:
            try:
                tile = tilesObject[value]
            except KeyError:
                return "Error on key"
            if tile["obj"].__name__ ==  "Door":
                obj = tile["obj"](name=tile["name"], position=(x, y), char=value, blocked=tile["block"],bgcolor=tile["bgcolor"], key="42isTheKey", open=False)
            else:
                obj = tile["obj"](name=tile["name"], position=(x, y), char=value, blocked=tile["block"],bgcolor=tile["bgcolor"])

            x += 1
            tileLine.append(obj)
        x = 0
        y += 1
        tileMap.append(tileLine)
    return tileMap


for line in load(mapRogue):
    for obj in line:
        print obj , "\n"

Класс DOOR с использованием композиции

Я подозреваю, что есть другой ответ, использующий шаблон композиции и/или стратегии, поэтому я пытаюсь украсить объект Tile поведением Door, но я заблокирован этим словарем...

На самом деле я безуспешно пробовал несколько решений, у вас есть предложение помочь мне решить эту проблему концепции, используя элегантный oop и python?

class Tile(object):
    #a tile of the map and its properties
    def __init__(self, name, char, position, blocked, color=(255, 255, 255), bgcolor=(0, 0, 0), door=None):
        self.name = name
        self.blocked = blocked
        self.char = char
        self.color = color
        self.bgcolor = bgcolor
        self.door = door
        self.position = position

# Door decorate the Tile object using composition
class Door(object):
    def __init__(self, key, open=False):
        self.state = open
        self.key = key

    def opening(self, key):
        if self.key == key:
            self.state = True

tilesObject = {".": {"name": 'floor', "obj": Tile, "bgcolor": (233, 207, 177), "block": False},
               "Y": {"name": 'forest', "obj": Tile, "bgcolor": (25, 150, 64), "block": True},
               "~": {"name": 'water', "obj": Tile, "bgcolor": (10, 21, 35), "block": False},
               "+": {"name": 'doors', "obj": Door, "bgcolor": (10, 10, 25), "block": False}}

def load(mymap):
    tileMap = []
    x, y = (0,0)
    for line in mymap:
        tileLine = []
        for value in line:
            try:
                tile = tilesObject[value]
            except KeyError:
                return "Error on key"

            # Here i need to detect when obj is Door 
                    # because i need to define a special Tile 
                    # decorated by Door behavior, 
                    # so it seems this is not a good solution :/

            x += 1
            tileLine.append(obj)
        x = 0
        y += 1
        tileMap.append(tileLine)
    return tileMap

Обновление с некоторой информацией:

Спасибо за ответ @User и @Hyperborreus, вы правы, здесь я упрощаю свой пример, и в моем коде есть два слоя:

  • Tile которые не двигаются,
  • и GameObjects, который может двигаться, атаковать, защищаться и выполнять множество других функций, оформленных с использованием composition, как в этом руководстве здесь

Используя pygame, я отображаю весь свой объект Tiles с помощью функции draw_tile().

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

Так что, вероятно, вы правы с вашим предложением словаря определения игры, мне нужно изменить способ создания экземпляра объекта, определение двери/плитки oop остается прежним, но когда я читаю исходную карту строк, которая содержит элемент, дверь, а также статический объект, я разделяю экземпляр gameObject и экземпляр Tile..

Идея словаря для создания экземпляра элемента на карте rogueLike, определенной в списке строк, основана на идее, основанной здесь: https://bitbucket.org/BigYellowCactus/dropout/

Возможно, создатель этого кода, @dominic-kexel, также может помочь нам в этом вопросе?


person reyman64    schedule 08.10.2013    source источник
comment
Почему вас раздражает приближение наследства?   -  person Hyperboreus    schedule 09.10.2013
comment
Тест if не очень хорош, и если у меня есть 10 типов плиток, у меня есть десять, если проверить имя класса, это не очень гибко, поэтому, если есть другое решение, более элегантное, чтобы сделать то же самое, это здорово?   -  person reyman64    schedule 09.10.2013


Ответы (2)


ИМХО, вы должны проводить различие между «тайлами» (лежащей в основе базовой картой) и «объектами» (вещами, с которыми игрок может взаимодействовать, например, открывающимися дверями, атакующими драконами или ямами, которые убивают).

Если вы хотите сравнить это с 3D-видеоигрой, «плитки» будут средой, с которой вы не можете взаимодействовать, а «объекты» будут кликабельными вещами.

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

Затем объекты помещаются поверх плиток.

Представьте, что у вас есть карта с большим количеством пола и стен, и в двух позициях у вас есть две двери. Все «плитки» ведут себя одинаково (вы можете ходить по полу, независимо от того, какая плитка пола), но вы будете бодаться головой о стену (независимо от того, где стена). Но двери разные: для одной двери требуется «Зеленый ключ», а для другой двери - «Вышитый ключ пикси-диссидента».

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

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

Таким образом, полное определение игры может выглядеть так:

game = {'baseMap': '#here comes your 2D array of tiles',
'objects': [ {'class': Door, 'position': (x, y), 'other Door arguments': ...}, 
{'class': Door, 'position': (x2, y2), 'other Door arguments': ...},
{'class': Dragon, 'position': (x3, y3), 'dragon arguments': ...}, ] }

Затем для создания экземпляров фактических объектов (объект в смысле OO, а не в смысле игры) пройдитесь по этому определению и вызовите c'tor каждого объекта с элементами словаря в качестве аргументов ключевого слова (двойная звездочка). Это только один возможный подход из многих.

Для рендеринга отобразите представление плитки, если плитка пуста, или представление объекта, если на плитке есть объект.


Вот что я имею в виду под двойной звездочкой:

class Door:
    def __init__ (self, position, colour, creaking = True):
        print (position, colour, creaking)

objDefs = [...,
          {'class': Door, 'kwargs': {'position': (2, 3), 'colour': 'black'} },
          ...]

#Here you actually iterate over objDefs
objDef = objDefs [1]
obj = objDef ['class'] (**objDef ['kwargs'] )

Большое изменение:

Это идея того, как можно реализовать рендеринг карты как с тайлами, так и с объектами. (Просто мои два цента):

#! /usr/bin/python3.2

colours = {'white': 7, 'green': 2, 'blue': 4, 'black': 0, 'yellow': 3}

class Tile:
    data = {'.': ('Floor', 'white', True),
        'Y': ('Forest', 'green', False),
        '~': ('Water', 'blue', False) }

    def __init__ (self, code, position):
        self.code = code
        self.position = position
        self.name, self.colour, self.passable = Tile.data [code]

    def __str__ (self):
        return '\x1b[{}m{}'.format (30 + colours [self.colour], self.code)

class GameObject:
    #here got he general interfaces common to all game objects
    def __str__ (self):
        return '\x1b[{}m{}'.format (30 + colours [self.colour], self.code)

class Door (GameObject):
    def __init__ (self, code, position, colour, key):
        self.code = code
        self.position = position
        self.colour = colour
        self.key = key

    def close (self): pass
        #door specific interface

class Dragon (GameObject):
    def __init__ (self, code, position, colour, stats):
        self.code = code
        self.position = position
        self.colour = colour
        self.stats = stats

    def bugger (self): pass
        #dragon specific interface

class Map:
    def __init__ (self, codeMap, objects):
        self.tiles = [ [Tile (c, (x, y) ) for x, c in enumerate (line) ] for y, line in enumerate (codeMap) ]
        self.objects = {obj ['args'] ['position']: obj ['cls'] (**obj ['args'] ) for obj in objects}

    def __str__ (self):
        return '\n'.join (
            ''.join (str (self.objects [ (x, y) ] if (x, y) in self.objects else tile)
                for x, tile in enumerate (line) )
            for y, line in enumerate (self.tiles)
            ) + '\n\x1b[0m'

mapRouge = ['~~~~~~~~~~',
            '~~~~.....Y',
            'YYYYY.YYYY',
            'YYYY....YY']

objects = [ {'cls': Door,
        'args': {'code': '.', 'position': (5, 2), 'colour': 'black',
        'key': 'Ancient Key of Constipation'} },
    {'cls': Dragon,
        'args': {'code': '@',  'position': (7, 3), 'colour': 'yellow',
        'stats': {'ATK': 20, 'DEF': 20} } } ]

theMap = Map (mapRouge, objects)
print (theMap)

И это результат:

введите здесь описание изображения

person Hyperboreus    schedule 08.10.2013
comment
Спасибо за ответ, вы правы, я упростил свой пример, но в моем коде есть два слоя: Tile, который не двигается, и GameObjects, который может двигаться, атаковать, защищаться и выполнять множество других функций. оформлено с использованием composition, как здесь roguebasin.roguelikedevelopment.org/ и т. д. Так что, вероятно, вы правы в своем предложении определения игры, возможно, мне нужно переместить Door в список GameObjects. Можете ли вы привести пример или ссылку на эту двойную звездочку, мне интересно? - person reyman64; 09.10.2013
comment
Я отредактировал свой ответ и включил пример того, что я имею в виду под kwargs и двойной звездочкой. - person Hyperboreus; 09.10.2013
comment
Еще раз спасибо, как вы думаете, с помощью этой техники можно передать аргумент во время итерации? Если я хочу указать (x, y) объекта на основе моего цикла по 2DList строки, например? - person reyman64; 09.10.2013
comment
Либо передайте кортеж позиции в словарь kwarg и передайте этот словарь, либо передайте его в качестве первого позиционного аргумента перед kwargs. Оба пути жизнеспособны. - person Hyperboreus; 09.10.2013
comment
Спасибо @Hyperboreus за вашу помощь, возможно, другие люди добавят другой ответ позже, но сейчас я подтверждаю ваш отличный ответ - person reyman64; 09.10.2013
comment
@ reyman64 Пожалуйста, посмотрите мое последнее редактирование. Это один из многих возможных способов реализации карты. Может быть, вы сможете почерпнуть из него одну или две идеи. (Пожалуйста, запустите его на консоли, которая понимает escape-последовательности ANSI, чтобы вы могли видеть цвета). - person Hyperboreus; 09.10.2013

Вот простое решение вашей проблемы:

kw = tile.copy()
cls = kw.pop('obj')
obj = cls(**kw)

делает то же самое, что

        if tile["obj"].__name__ ==  "Door":
            obj = tile["obj"](name=tile["name"], position=(x, y), char=value, blocked=tile["block"],bgcolor=tile["bgcolor"], key="42isTheKey", open=False)
        else:
            obj = tile["obj"](name=tile["name"], position=(x, y), char=value, blocked=tile["block"],bgcolor=tile["bgcolor"])

И это потому, что я ценю то, что вы делаете:

Я бы согласился с Гипербореем в том, что вы делаете разницу между положением = плитка и тем, что находится сверху, если плитка.

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

class Tile:
    def __init__(self):
        self.left_tile = None
        self.right_tile = None
        ...
        self.content = [FreeSpaceToWalk()]

    def can_I_walk_there(self, person):
        for content in self.content:
            if not content.can_I_walk_there(person): return False
        return True

Таким образом, вы можете делать порталы, соединяя плитки, не являющиеся соседями по положению.

Вот некоторое содержание:

class Door:
    password = '42'
    def can_I_walk_there(self, person):
        return person.what_is_the_password_for(self) == self.password

class FreeSpaceToWalk:
    def can_I_walk_there(self, person):
        return True

class Wall:
    def can_I_walk_there(self, person):
        return False
person User    schedule 08.10.2013