Помогите разделить функцию на несколько классов

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

Нужно разделить таким образом:

В Локацию поместить чтение внешнего файла, хранение текущей локации, смену текущей локации ( + парсинг данных с регуляркой)

В Герое учитывать состояние героя, его опыт, оставшееся время, проверять жив ли он(кончилось ли время)

Во Враге хранить опыт+время, состояние (жив/мертв, можно будет вместо удаления из списка использовать, или проверять и удалять мертвых)

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

И при попытке всё это разделить код ломается, у меня в одних методах получаются данные для разных классов. Можете подсказать, как можно проще всего поделить код(хотя-бы частично), чтобы не перепечатывать всё заново

import re
import json
from csv import writer
from decimal import Decimal
from datetime import datetime

remaining_time = '123456.0987654321'
# если изначально не писать число в виде строки - теряется точность!
field_names = ['current_location', 'current_experience', 'current_date']


class Enemy:
    enemy_initialization = r'(Mob|Boss)_exp(\d+)_tm(\d+)'

    def initialize(self, enemy):
        finding_enemy = re.findall(self.enemy_initialization, enemy)
        health = int(finding_enemy[0][1])
        kill_time = Decimal(finding_enemy[0][2])
        return health, kill_time


class Location:
    location_initialization = r'(Location_\w+|Hatch)_tm([\d+|\d/.]+)'

    def initialize(self, location):
        finding_location = re.findall(self.location_initialization, location)
        name_location = finding_location[0][0]
        throw_time = Decimal(finding_location[0][1])
        return name_location, throw_time

class Game:

    def __init__(self, remining_time, csv_names):
        self.enemy = Enemy()
        self.location_class = Location()
        self.current_experience = 0
        self.elapsed_time = 0
        self.remaining_time = Decimal(remining_time)
        with open('dungeon.csv', 'a', newline='', encoding='utf8') as csv_file:
            csv_writer = writer(csv_file, )
            csv_writer.writerow(csv_names)

    def turning(self, locations_branching, killed_enemies, enemies):
        locations = []
        for current_location, environment_in in locations_branching.items():
            if environment_in == "You are winner":
                return 'You are winner'
            for item in environment_in:
                if isinstance(item, dict):
                    locations.append(item)
                elif isinstance(item, str):
                    if not killed_enemies:
                        enemies.append(item)
            return current_location, enemies, locations

    def current_state(self, current_location, enemies, locations):
        with open('dungeon.csv', 'a', newline='', encoding='utf8') as csv_file:
            csv_writer = writer(csv_file, )
            available_actions = []
            if enemies:
                available_actions.append("Уничтожить врага")
            if locations:
                available_actions.append("Сходить в другую локацию")
            available_actions.append("Выиграть игру")

            csv_writer.writerow([current_location, self.current_experience, datetime.now()])

            print(f" Вы сейчас в {current_location} и у вас {self.current_experience} опыта. ")
            print(f"До наводнения {self.remaining_time} секунд.")
            print(f"{self.elapsed_time} времени уже прошло.")
            showing_enemies = list(map(lambda enm: '- Враг: ' + enm, enemies))
            showing_locations = list(map(lambda loc: '- Вход в локацию: ' + list(loc.keys())[0], locations))
            print("Перед вами:")
            print(*showing_enemies, sep='\n')
            print(*showing_locations, sep='\n')
            print(f"Пришло время выбора:")
            available_actions = list(map(lambda act:
                                         str(available_actions.index(act) + 1)
                                         + '.'
                                         + act,
                                         available_actions))
            print(*available_actions, sep='\n')
            return available_actions

    def choising(self, action_length):
        available_choices = [str(i + 1) for i in range(action_length)]
        while True:
            option = input('Какое действие выберете? : ')
            if option in available_choices:
                break
        return option

    def killing(self, enemies):
        print('Доступные для уничтожения враги:')
        attacking_enemies = []
        for i in range(len(enemies)):
            attacking_enemies.append(str(i + 1) + '.' + enemies[i])
        print(*attacking_enemies, sep='\n')
        choose = self.choising(len(enemies))
        exp, tm = self.enemy.initialize(enemies[int(choose) - 1])
        self.current_experience += exp
        self.elapsed_time += tm
        self.remaining_time -= tm
        return choose

    def step_into_location(self, locations):
        print('Доступные для входа локации:')
        locations_for_action = \
            list(map(lambda x:
                     str(locations.index(x) + 1)
                     + '.Пройти в локацию: '
                     + list(x.keys())[0],
                     locations))
        print(*locations_for_action, sep='\n')
        choose = self.choising(len(locations))

        curr_location, tm = self.location_class.initialize(locations_for_action[int(choose) - 1])
        self.remaining_time -= tm
        self.elapsed_time += tm

        return choose

    def process(self):
        enemies_scope = []
        killed_enemies = False
        with open('rpg.json', 'r', encoding='utf8') as rpg:
            locations_branching = json.load(rpg)
        while True:
            turn = self.turning(locations_branching, killed_enemies, enemies_scope)
            if turn == 'You are winner':
                if self.current_experience >= 280:
                    print(turn)
                    return "win"
                else:
                    print('Вы слишком быстро добрались до выхода и чтобы доказать своё превосходство решаете '
                          'вернуться к началу и еще раз всех уничтожить')
                    return
            else:
                current_location, enemies, locations = turn
                if not locations or self.remaining_time <= 0:
                    print('Вы слишком быстро добрались до выхода и чтобы доказать своё превосходство решаете '
                          'вернуться к началу и еще раз всех уничтожить')
                    return
                todo_choice = self.current_state(current_location, enemies, locations)

                action = self.choising(len(todo_choice))
                action = todo_choice[int(action) - 1][2:]
                if action == 'Уничтожить врага':
                    dead_enemy = int(self.killing(enemies_scope)) - 1
                    enemies_scope.remove(enemies_scope[dead_enemy])
                    killed_enemies = True
                elif action == 'Сходить в другую локацию':
                    location_choice = int(self.step_into_location(locations)) - 1
                    next_location = locations[location_choice]
                    locations_branching = next_location
                    killed_enemies = False
                    enemies_scope = []
                elif action == 'Выиграть игру':
                    print('Вы решили подождать наводнения и волной вас вынесло к выходу.')
                    return 'exit'

    def go(self):
        while True:
            win = self.process()
            if win in ['win', 'exit']:
                print('Прекрасная игра! Вы выходите победителем. Времени в запасе у вас осталось: ', self.remaining_time)
                break


game = Game(remining_time=remaining_time, csv_names=field_names)
game.go()

Текст ниже расположен в файле rpg.json, который сама игра берет для анализа

{
  "Location_0_tm0": [
    "Mob_exp10_tm0",
    {
      "Location_1_tm1040": [
        "Mob_exp20_tm200",
        "Mob_exp20_tm200",
        {
          "Location_3_tm33000": [
            {
              "Location_7_tm33300": [
                {
                  "Location_10_tm55100": [
                    "Mob_exp25_tm1",
                    "Mob_exp30_tm1",
                    "Mob_exp20_tm1",
                    "Mob_exp24_tm1",
                    {
                      "Location_12_tm0.0987654320": [
                        "Boss100_exp100_tm10",
                        "Boss200_exp30_tm10"
                      ]
                    }
                  ]
                }
              ]
            }
          ]
        }
      ]
    },
    {
      "Location_2_tm33300": [
        "Mob_exp20_tm1677",
        {
          "Location_4_tm44000": [
            "Mob_exp10_tm40",
            "Mob_exp15_tm40",
            "Mob_exp20_tm40"
          ]
        },
        {
          "Location_5_tm55100": [
            {
              "Location_8_tm30000": [
                "Mob_exp20_tm820",
                "Mob_exp25_tm825",
                "Mob_exp30_tm830",
                "Mob_exp35_tm835",
                "Mob_exp40_tm840"
              ]
            },
            {
              "Location_9_tm26000": [
                "Mob_exp30_tm30",
                {
                  "Location_11_tm4000": [
                    "Boss_exp100_tm1040",
                    {
                      "Location_B2_tm2000": [
                        "Mob_exp40_tm50",
                        "Mob_exp40_tm50",
                        "Mob_exp40_tm50",
                        {
                          "Hatch_tm159.098765432": "You are winner"
                        }
                      ]
                    }
                  ]
                }
              ]
            }
          ]
        },
        {
          "Location_6_tm30000": [
            "Boss_exp280_tm10400000",
            {
              "Location_B1_tm0.098765432": [
                "Mob_exp10_tm0",
                "Mob_exp10_tm0",
                "Mob_exp10_tm0",
                "Mob_exp10_tm0",
                "Mob_exp10_tm0"
              ]
            }
          ]
        }
      ]
    }
  ]
}

Ответы (1 шт):

Автор решения: Daniil Loban

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

Основная логика для такого рефакторинга.

  1. Согласно функционалу классов вынести поля и методы в нужные классы
  2. Преобразовать старые вызовы в новые вызовы из других классов
  3. Уменьшать число передаваемых параметров в методы (убирать лишнее)
  4. Разбивать функции на более мелкие (в том числе для повторного использования)
  5. Стремится к тому чтобы в основном потоке исполнения использовались только классы и их методы
  6. Желательно почитать про ООП, инверсию зависимостей, чистый код, дзен

надеюсь я не зря потратил время (давно не писал на питоне) и это будет полезно =)

    import os
    import re
    import json
    from csv import writer
    from decimal import Decimal
    from datetime import datetime

    remaining_time = Decimal('123456.0987654321')
    field_names = ['current_location', 'current_experience', 'current_date']

    # функция очистки экрана (linux # win?)
    clear = lambda: os.system('clear') # clear = lambda: os.system('cls')

    class Enemy:
        '''
        Во Враге хранить опыт + время, 
        состояние (жив/мертв, можно будет вместо удаления из списка использовать, 
        или проверять и удалять мертвых)
        '''
        def __init__(self, enemy):
            enemy_initialization = r'(Mob|Boss)_exp(\d+)_tm(\d+)'
            finding_enemy = re.findall(enemy_initialization, enemy)
            self.name = enemy
            self.health = int(finding_enemy[0][1])
            self.kill_time = Decimal(finding_enemy[0][2])
        def kill_self(self):
            self.health = 0

    class Hero(object):
        '''
        В Герое учитывать состояние героя, его опыт, 
        оставшееся время, проверять жив ли он(кончилось ли время)
        '''
        def __init__(self, remaining_time):
            self.state = 0
            self.current_experience = 0
            self.elapsed_time = 0
            self.remaining_time = remaining_time;
        def spent_time(self, time):
            self.elapsed_time += time
            self.remaining_time -= time
        def kill_enemy(self, enemy):
            self.current_experience += enemy.health
            self.spent_time(enemy.kill_time); 
            enemy.kill_self() 

    class Location:
        '''
         В Локацию поместить чтение внешнего файла, 
         хранение текущей локации, смену текущей локации ( + парсинг данных с регуляркой)
        '''
        def __init__(self):
            self.file_data = None
            self.current_location = None
            self.locations = []
            self.read_JSON()
        def change(self, location):
            location_initialization = r'(Location_\w+|Hatch)_tm([\d+|\d/.]+)'
            finding_location = re.findall(location_initialization, location)
            name_location = finding_location[0][0]
            throw_time = Decimal(finding_location[0][1])
            self.current_location = name_location
            return name_location, throw_time
        def go_to_location(self, index):
            self.branching = self.locations[index]    
        def read_JSON(self):
            with open('rpg.json', 'r', encoding='utf8') as rpg:
               self.branching = json.load(rpg)
        def save_csv(self, csv_names):
            with open('dungeon.csv', 'a', newline='', encoding='utf8') as csv_file:
                csv_writer = writer(csv_file, )
                csv_writer.writerow(csv_names)

    '''
    Игра - общий класс, регулирующий взаимодействие всех остальных. 
    В нем выбор пользователя и запуск нужных методов и всё остальное что нужно.
    '''
    class Game:
        def __init__(self, remaining_time, csv_names):
            self.enemies = [] 
            self.hero = Hero(remaining_time);
            self.location_class = Location()
            self.location_class.save_csv(csv_names)

        def choising(self, action_length):
            available_choices = [str(i + 1) for i in range(action_length)]
            while True:
                option = input('Какое действие выберете? : ')
                if option in available_choices:
                    break
            return int(option)

        def list_enemies(self):     
            print('Доступные для уничтожения враги:')
            attacking_enemies = []
            for i in range(len(self.enemies)):
                attacking_enemies.append(str(i + 1) + '.' + self.enemies[i].name)
            print(*attacking_enemies, sep='\n')

        def kill_enemy(self):
            self.list_enemies()
            choose = self.choising(len(self.enemies))
            self.hero.kill_enemy(self.enemies[choose - 1])
        
        def show_state_info(self): 
            clear()
            print(f" Вы сейчас в {self.location_class.current_location} и у вас {self.hero.current_experience} опыта. ")
            print(f"До наводнения {self.hero.remaining_time} секунд.")
            print(f"{self.hero.elapsed_time} времени уже прошло.")
            showing_enemies = list(map(lambda enm: '- Враг: ' + enm.name, self.enemies))
            showing_locations = list(map(lambda loc: '- Вход в локацию: ' + list(loc.keys())[0], self.location_class.locations))
            print("Перед вами:")
            print(*showing_enemies, sep='\n')
            print(*showing_locations, sep='\n')

        ########   

        def turning(self, killed_enemies):
            self.location_class.locations = []
            self.enemies = []
            for current_location, environment_in in self.location_class.branching.items():
                if environment_in == "You are winner": 
                    return 'You are winner'
                for item in environment_in:
                    if isinstance(item, dict):
                        self.location_class.locations.append(item)
                    elif isinstance(item, str) and not killed_enemies:
                        self.enemies.append(Enemy(item))
                self.location_class.current_location = current_location


        def current_state(self):
            with open('dungeon.csv', 'a', newline='', encoding='utf8') as csv_file:
                csv_writer = writer(csv_file, )
                available_actions = []
                if self.enemies:
                    available_actions.append("Уничтожить врага")
                if self.location_class.locations:
                    available_actions.append("Сходить в другую локацию")
                available_actions.append("Выиграть игру")
                csv_writer.writerow([self.location_class.current_location , self.hero.current_experience, datetime.now()])
                self.show_state_info()
                print(f"Пришло время выбора:")
                available_actions = list(map(lambda act:
                                             str(available_actions.index(act) + 1)
                                             + '.'
                                             + act,
                                             available_actions))
                print(*available_actions, sep='\n')
                return available_actions

        def get_locations_for_action (self):
            return \
                list(map(lambda x:
                         str(self.location_class.locations.index(x) + 1)
                         + '.Пройти в локацию: '
                         + list(x.keys())[0],
                         self.location_class.locations))
                 

        def step_into_location(self, locations):
            locations_for_action = self.get_locations_for_action()
            print('Доступные для входа локации:')
            print(*locations_for_action, sep='\n')
            choose = self.choising(len(self.location_class.locations))
            curr_location, tm = self.location_class.change(locations_for_action[int(choose) - 1])
            self.hero.spent_time(tm)
            return choose

        def process(self):
            killed_enemies = False
            self.location_class.read_JSON()

            while True:
                turn = self.turning(killed_enemies)
                if turn == 'You are winner':
                    if self.hero.current_experience >= 280:
                        print(turn)
                        return "win"
                    else:
                        print('Вы слишком быстро добрались до выхода и чтобы доказать своё превосходство решаете '
                              'вернуться к началу и еще раз всех уничтожить')
                        return
                else:
                    if not self.location_class.locations or self.hero.remaining_time <= 0:
                        print('Вы слишком быстро добрались до выхода и чтобы доказать своё превосходство решаете '
                              'вернуться к началу и еще раз всех уничтожить')
                        return
                    todo_choice = self.current_state()
                    action = self.choising(len(todo_choice))
                    action = todo_choice[int(action) - 1][2:]
                    
                    if action == 'Уничтожить врага':
                        self.kill_enemy()
                        killed_enemies = True
                    
                    elif action == 'Сходить в другую локацию':
                        location_choice = int(self.step_into_location(self.location_class.locations)) - 1
                        self.location_class.go_to_location(location_choice)    
                        killed_enemies = False
                        self.enemies = []
                    elif action == 'Выиграть игру':
                        print('Вы решили подождать наводнения и волной вас вынесло к выходу.')
                        return 'exit'

        def go(self):
            while True:
                win = self.process()
                if win in ['win', 'exit']:
                    print('Прекрасная игра! Вы выходите победителем. Времени в запасе у вас осталось: ', self.hero.remaining_time)
                    break


    game = Game(remaining_time=remaining_time, csv_names=field_names)
    game.go()
→ Ссылка