Ищу отзывы о моей программе дизайна - PullRequest
0 голосов
/ 05 декабря 2009

Я знаю, что SO для вопросов, но в целом цель состоит в том, чтобы помочь людям учиться, поэтому я решил попробовать свои силы в обмене некоторым кодом и запросом отзывов о нем.


Я хочу создать программу, которая будет опираться на случайные числа, в частности кости. Они будут представлены в виде «2D6», «4D10 + 3», «2D2 + 3D3» и так далее, и так далее. Поэтому я намеревался создать модуль ролика для игры в кости, который мог бы принимать ввод, как в этой форме.

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

Это все еще WIP, и я еще не начал модульные тесты.

Ссылка на код

#!/usr/bin/env python3
"""
Created by Teifion Jordan
http://woarl.com

Notes: The roller does not correctly apply * and / signs:
A + B * C is worked out as (A + B) * C, not A + (B * C) as would be correct
"""

import random
import re
import math

class Roller_dict (object):
    """A 'dictionary' that stores rollers, if it's not got that roller it'll make a new one"""
    def __init__(self, generator=random.randint):
        super(Roller_dict, self).__init__()
        self.rollers = {}

        # Generator is used to supply a "rigged" random function for testing purposes
        self.generator = generator

    def __call__(self, constructor):
        constructor = constructor.replace(" ", "")
        if constructor not in self.rollers:
            self.rollers[constructor] = Roller(constructor, self.generator)

        return self.rollers[constructor]()

# Regular expressions used by the Roller class
# Compiled here to save time if we need to make lots of Roller objects
pattern_split       = re.compile(r"(\+|-|\*|/)")
pattern_constant    = re.compile(r"([0-9]*)")
pattern_die         = re.compile(r"([0-9]*)[Dd]([0-9]*)")
pattern_sign        = re.compile(r"^(\+|-|\*|/)")

class Roller (object):
    def __call__(self):
        return self.roll()

    def __init__(self, constructor, generator=random.randint):
        super(Roller, self).__init__()
        self.items = []
        self.rebuild(constructor)
        self.generator = generator

    def rebuild(self, constructor):
        """Builds the Roller from a new constructor string"""
        # First we need to split it up
        c = pattern_split.split(constructor.replace(" ", ""))

        # Check for exceptions
        if len(c) == 0:
            raise Exception('String "%s" did not produce any splits' % constructor)

        # Stitch signs back into their sections
        parts = []
        last_p = ""
        for p in c:
            if p in "+-*/":
                last_p = p
                continue

            if last_p != "":
                p = "%s%s" % (last_p, p)
                last_p = ""

            parts.append(p)

        # We have the parts, now we need to evaluate them into items
        for p in parts:
            # Look for a sign, default to positive
            sign = pattern_sign.search(p)
            if sign == None: sign = "+"
            else: sign = sign.groups()[0]

            # Strip out the sign, we're left with just the pure value
            body = p.replace(sign, "")

            # Now we find out what our main body is

            # Die
            value = pattern_die.search(body)
            if value != None:
                # Sign, Number, Sides
                self.items.append(("die", sign, int(value.groups()[0]), int(value.groups()[1])))
                continue

            # Constant
            value = pattern_constant.search(body)
            if value != None:
                self.items.append(("constant", sign, int(value.groups()[0])))
                continue

            # No matches
            raise Exception('The part string "%s" had no matches' % body)

    def roll(self):
        """Rolls the die/dice and returns the result"""
        result = 0

        for i in self.items:
            # Get value
            if i[0] == "die":           value = self._derive_die(i[2], i[3])
            elif i[0] == "constant":    value = self._derive_constant(i[2])
            else: raise Exception('No handler for item type "%s"' % i[0])

            # Apply sign
            if i[1] == "+":     result += value
            elif i[1] == "-":   result -= value
            elif i[1] == "*":   result *= value
            elif i[1] == "/":   result /= value

        return result

    def _derive_die(self, number, sides):
        result = 0
        for n in range(0, number):
            result += self.generator(0, sides)

        return result

    def _derive_constant(self, value):
        return value

# Useful for running the tests to make sure that it uses "random" numbers
false_numbers = (int(math.cos(x)*5)+5 for x in range(0,1000))
def false_numbers_func(*args):
    return false_numbers.next()

# If it's main, run unit tests?
if __name__ == '__main__':
    r = Roller_dict(false_numbers_func)

    print(r("2D6"))
    print(r("2D6"))
    print(r("2D6"))

Ответы [ 3 ]

3 голосов
/ 05 декабря 2009

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

Класс для бросания кубиков относительно легко написать. Я делаю две вещи, которые вы не делаете: отображение знаков на операции (использование карты означает отсутствие необходимости писать логику, плюс возможность ее многократного использования) и возможность объединения Roller объектов в простой связанный список, поэтому что вызов roll в начале списка сворачивает их все и подводит итог.

import random
R = random.Random()

class Roller(object):
    # map signs to operations
    op = { "+" : lambda a,b: a+b,
           "-" : lambda a,b: a-b,
           "*" : lambda a,b: a*b,
           "/" : lambda a,b: a/b }

    def __init__(self, dice, sides, sign=None, modifier=0):
        self.dice = dice
        self.sides = sides
        self.sign = sign
        self.modifier = modifier
        self.next_sign = None
        self.next_roller = None

    def roll(self):
        self.dice_rolled = [R.randint(1, self.sides) for n in range(self.dice)]
        result = sum(dice_rolled)
        if self.sign:
            result = self.op[self.sign](result, self.modifier)
        if self.next_sign and self.next_roller:
            result = self.op[self.next_sign](result, self.next_roller.roll())
        return result

Это сравнительно легко проверить. Обратите внимание, что dice_rolled сохраняется как атрибут, чтобы вам было легче писать модульные тесты.

Следующий шаг - выяснить, как анализировать входные данные. Такого рода работы:

>>> p = """
(?P<next_sign>[-+*/])?
(?P<dice>[\d]+)
[\s]*D[\s]*
(?P<sides>[\d]+)
# trailing sign and modifier are optional, but if one is present both must be
([\s]*(?P<sign>[-+/*])[\s]*(?P<modifier>[\d]+))?"""
>>> r = re.compile(p, re.VERBOSE+re.IGNORECASE)
>>> m=r.match('2 d 20 +1')
>>> m.group('dice'), m.group('sides'), m.group('sign'), m.group('modifier')
('2', '20', '+', '1')
>>> r.findall('3D6*2-1D4+1*2D6-1')
[('', '3', '6', '*2', '*', '2'), ('-', '1', '4', '+1', '+', '1'), ('*', '2', '6', '-1', '-', '1')]

Существует синтаксическая лексическая двусмысленность - 2D6+1D4 анализируется как 2D6+1, за которым следует непревзойденный D4, и мне не очевидно, как исправить это в регулярном выражении. Может быть, это можно исправить с помощью отрицательного прогнозирующего утверждения.

В любом случае, как только регулярное выражение будет исправлено, остается только обработать результаты r.findall, чтобы создать цепочку из Roller объектов. И сделайте это методом класса, если вы действительно копаете инкапсуляцию.

1 голос
/ 05 декабря 2009

Страница с примерами pyparsing включает в себя аналогичный синтаксический анализатор и ролик , включая следующие тестовые примеры:

D5+2d6*3-5.5+4d6
D5+2d6*3-5.5+4d6.takeHighest(3)
2d6*3-5.5+4d6.minRoll(2).takeHighest(3)

Первые 30 строк или около того сценария содержат анализатор, остальные содержат оценщик, включая код отладки, показывающий броски роллов.

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

1 голос
/ 05 декабря 2009

Внешне есть PEP08 ; в частности, использование 4 пробелов для отступа по сравнению с использованием символов табуляции.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...