├── .gitignore ├── README.md ├── animation ├── __init__.py └── maps.py ├── data ├── data.json └── default.json ├── maps ├── __init__.py ├── home.py ├── river.py └── street.py ├── popo ├── __init__.py ├── character.py ├── color.py ├── config.py ├── g.py ├── game.py ├── pprint.py └── views.py └── start.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.pyc 3 | */__pycache__ 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # POPO Game coding is game 2 | 3 | 「泡泡世界」是一个文字版的 RPG 游戏框架,目的是为 Python 初学者提供一个简单有趣的实践项目,轻松开发出属于自己的游戏,开发者只需要数十行代码即可扩展游戏场所、位置和互动操作。 4 | 5 | 6 | ## 系统及软件需求 7 | 8 | - 本项目为纯 Python 项目,支持 Windows/MacOS/Linux 等系统 9 | - python 3.7+ 10 | 11 | 12 | ## 快速开始: 13 | 14 | 1. 下载代码到本地,如果是解压包,请解压 15 | 16 | 2. 安装依赖 17 | 18 | ```shell 19 | pip install -U colored 20 | ``` 21 | 22 | 3. 为了更好的体验游戏,Windows 系统下命令行终端建议使用 Hyper 23 | 24 | - 请自行下载安装:[官网下载](https://hyper.is/) 25 | 26 | - 把 Hyper 添加到右键菜单 27 | 28 | 在你的电脑上打开 Hyper,依次点击左上角 `菜单按钮` -> `plugins` -> `install Hyper CLI command in PATH` 29 | 然后,在任意文件夹下,右键,可以发现有一个选项:`Open Hyper here`,点击即可在当前目录打开 Hyper 30 | 31 | - Hyper 还支持一些插件和主题,感兴趣的可自己选择安装。[Hyper 主题](https://hyper.is/themes) 32 | 33 | 4. 在命令行终端中启动游戏 34 | 35 | - 进入已下载的项目文件夹 `popo`(如果是下载的压缩包,解压后可能是文件夹 `pop-main`)下,右键点击 `Open Hyper here` 36 | 37 | > 或者在打开的 CMD 终端上,切换到已下载的项目文件夹 38 | 39 | - 执行如下命令: 40 | 41 | ```shell 42 | python start.py 43 | ``` 44 | 45 | ## 代码架构 46 | 47 | ```shell 48 | popo # 项目文件夹,如果是下载的压缩包,解压后可能是文件夹 `pop-main` 49 | │ 50 | │ README.md # 说明文档 51 | │ start.py # 启动脚本: python start.py 52 | │ 53 | ├─animation # 字符画地图,过场字符动画 54 | │ │ maps.py # 地图字符画定义文件 55 | │ │ __init__.py 56 | │ 57 | ├─data # 游戏数据文件夹,请勿修改 58 | │ data.json # 实际使用的数据文件 59 | │ default.json # 初始配置,当重建角色时使用 60 | │ 61 | ├─maps # 地图模块,开发者实际在这个目录编写游戏逻辑代码 62 | │ │ home.py # 场所模块:家。自带模块,家是初始场所,也是顶级位置,不能修改 63 | │ │ river.py # 场所模块:河流。自带模块,可继续二次开发 64 | │ │ street.py # 场所模块:街道。自带模块,可继续二次开发 65 | │ └─ __init__.py 66 | │ 67 | └─popo # POPO Game 核心代码文件夹 68 | │ character.py # 角色模块:1.提供角色创建、删除、重命名等操作 2. 提供角色的数据接口 69 | │ color.py # 颜色模块:把文本配置为美观的颜色打印,或逐字打印 70 | │ config.py # 配置文件,用户自定义的地图模块,需要在此文件中的 maps 列表中配置 71 | │ g.py # 全局变量模块 72 | │ game.py # 游戏核心框架模块,提供启动,场所切换,位置切换等功能 73 | │ pprint.py # 提供特色的打印函数 74 | │ views.py # 提供各种组合数据的美观输出组件 75 | └─ __init__.py 76 | ``` 77 | 78 | ## 开发地图 79 | 80 | ### 1. 场所构思 81 | 82 | 在泡泡的世界里,场所是一个小的活动空间,如:街道、河流、学校,每个场所应该有几个能够发生故事场景的位置,如「街道场所」有面馆(故事场景:吃饭),有水果店(故事场景:打零工赚钱)。 83 | 84 | ### 2. 场所模块 85 | 86 | 场所模块定义在 maps/ 文件夹下,其中 home.py 默认为「家」,不能修改。street.py, river.py 为框架自带,供游戏体验、开发参考或二次开发。 87 | 88 | ### 3. 开发场所模块 89 | 90 | - 在 maps/ 文件夹下创建一个 .py 文件,如: school.py,代码请参考 street.py,如下为简化代码及说明: 91 | 92 | ```python 93 | fruit = { # fruit: 这是你需要定义的位置变量名 94 | 'name': '泡泡水果店', # name: 位置名称 95 | 'class': 'Fruit', # class: 位置的类名称,本例为 Fruit,后面需真实定义 class Fruit 96 | 'actions': {'1': {'name': '收银', 'func': 'cash'}, # actions: 本位置中支持的互动操作集合,'1', '2' 为命令编号,必须是字符类型 97 | '2': {'name': '领工钱', 'func': 'wage'} # │ name: 互动操作名称 98 | } # └─ func: 互动操作的函数名称,该函数应写在本位置类中 \ 99 | } # 本例中 cash 和 wage 函数都应该写在Fruit类中 100 | 101 | noodle = { 102 | 'name': '重庆小面馆', 103 | 'class': 'Noodle', 104 | 'actions': {'1': {'name': '点单', 'func': 'order'}, 105 | '2': {'name': '开吃', 'func': 'dine'}, 106 | '3': {'name': '买单', 'func': 'pay'} 107 | } 108 | } 109 | 110 | 111 | class App(): # 框架约定的场所类名称,统一为 App,不可修改 112 | 113 | def __init__(self, _id, module): # 框架约定,不可修改 114 | self.id = _id # 框架约定,不可修改 115 | self.module = module # 框架约定,不可修改 116 | 117 | self.name = '泡泡大街' # name: 定义场所名称 118 | self.level = 1 # level: 定义角色能够进入此场所的最低等级(待实现) 119 | self.scenes = {'1': fruit, '2': noodle} # scenes: 定义本场所包含的位置集合,'1', '2' 为地点编号,必须是字符类型 \ 120 | # fruit, noodle 为上面自定义的位置信息变量名 121 | 122 | class Fruit(): # 定义位置类: Fruit 123 | ... # 略过,详情见 street.py 文件 124 | ... 125 | ... 126 | 127 | 128 | class Noodle(): # 定义位置类: Noodle 129 | 130 | def __init__(self): 131 | pass 132 | 133 | def order(self, text): # 在位置信息中定义的 order 互动操作函数,必须在此位置类中定义 134 | color.next('开发中...', header=True) 135 | 136 | def dine(self, text): 137 | color.next('开发中...', header=True) # 在位置信息中定义的 dine 互动操作函数,必须在此位置类中定义 138 | 139 | def pay(self, text): 140 | color.next('开发中...', header=True) # 在位置信息中定义的 pay 互动操作函数,必须在此位置类中定义 141 | 142 | ``` 143 | 144 | - 编写自定义的互动操作 145 | 146 | 请参考 street.py 中 Fruit 的互动操作 cash, wage 的实现 147 | 148 | - 上述自定义场所模块开发完成后,需在 popo/config.py 中配置到 maps 列表中。例如,我们写了一个 school.py 场所模块,则按如下示例添加即可。 149 | 150 | ```python 151 | # 此处配置 maps/ 文件夹下(除了 home.py 外)的文件名,不配置的则不会导入 152 | maps = ['street', 'river', 'school'] 153 | 154 | ``` 155 | 156 | - 启动游戏,试试看你写的游戏吧:) 157 | 158 | ```shell 159 | python start.py 160 | ``` -------------------------------------------------------------------------------- /animation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popogame/popo/eacbb7537017841265194fc0c0d6ebd10227c1fc/animation/__init__.py -------------------------------------------------------------------------------- /animation/maps.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | home = '''\ 4 | ____________________________________________________________________ 5 | | 6 | | 7 | | 8 | /^^^^^^\\ 9 | /========\\ 10 | /| /| | |\\ 11 | ____________________________ | || |家|______________________________ 12 | ^^^^^^^ . |||||||||||| o~| |__|/ |__| 13 | |Fruit| ^ | Noodle | | | 14 | |_# \_| /|\ |(~~) {_| TTT| | 15 | ----------------------------------| 16 | 1. 泡泡大街 | 17 | ----------------------------------| 18 | __________________________________|__________________________________''' -------------------------------------------------------------------------------- /data/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "status": { 4 | "level": 1, 5 | "hp": 100, 6 | "max_hp": 100, 7 | "battle": 100, 8 | "iq": 100, 9 | "power": 100 10 | }, 11 | "coins": { 12 | "balance": 1000, 13 | "day_pay": 0, 14 | "day_earn": 0, 15 | "day_in": 0 16 | }, 17 | "date": 0, 18 | "hours": 0, 19 | "create_time": 0, 20 | "last_login": 0 21 | } -------------------------------------------------------------------------------- /data/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "status": { 4 | "level": 1, 5 | "hp": 100, 6 | "max_hp": 100, 7 | "battle": 100, 8 | "iq": 100, 9 | "power": 100 10 | }, 11 | "coins": { 12 | "balance": 1000, 13 | "day_pay": 0, 14 | "day_earn": 0, 15 | "day_in": 0 16 | }, 17 | "date": 0, 18 | "hours": 0, 19 | "create_time": 0, 20 | "last_login": 0 21 | } -------------------------------------------------------------------------------- /maps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popogame/popo/eacbb7537017841265194fc0c0d6ebd10227c1fc/maps/__init__.py -------------------------------------------------------------------------------- /maps/home.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class App(): 4 | 5 | def __init__(self, _id, module): 6 | self.id = _id 7 | self.module = module 8 | 9 | self.name = '家' 10 | self.level = 1 11 | -------------------------------------------------------------------------------- /maps/river.py: -------------------------------------------------------------------------------- 1 | from popo import g 2 | from popo import color 3 | 4 | 5 | 6 | lane = { 7 | 'name': '滨河跑道', 8 | 'class': 'Lane', 9 | 'actions': {'1': {'name': '慢跑', 'func': 'walk'}, 10 | '2': {'name': '休息', 'func': 'rest'} 11 | } 12 | } 13 | 14 | 15 | bay = { 16 | 'name': '钓鱼小湾', 17 | 'class': 'Bay', 18 | 'actions': {'1': {'name': '投食', 'func': 'feed'}, 19 | '2': {'name': '下钩', 'func': 'fishing'}, 20 | '3': {'name': '收工', 'func': 'ending'} 21 | } 22 | } 23 | 24 | 25 | class App(): 26 | 27 | def __init__(self, _id, module): 28 | self.id = _id 29 | self.module = module 30 | 31 | self.name = '富春河畔' 32 | self.level = 5 33 | self.scenes = {'1': lane, '2': bay} 34 | 35 | 36 | class Lane(): 37 | 38 | def __init__(self): 39 | pass 40 | 41 | def walk(self, text): 42 | color.next('开发中...', header=True) 43 | 44 | def rest(self, text): 45 | color.next('开发中...', header=True) 46 | 47 | 48 | class Bay(): 49 | 50 | def __init__(self): 51 | pass 52 | 53 | def feed(self, text): 54 | color.next('开发中...', header=True) 55 | 56 | def fishing(self, text): 57 | color.next('开发中...', header=True) 58 | 59 | def ending(self, text): 60 | color.next('开发中...', header=True) 61 | -------------------------------------------------------------------------------- /maps/street.py: -------------------------------------------------------------------------------- 1 | from random import randint, sample 2 | from popo import g 3 | from popo import color # color 模块提供彩色打印、逐字打印等效果 4 | 5 | 6 | fruit = { # fruit: 这是你需要定义的位置变量名 7 | 'name': '泡泡水果店', # name: 位置名称 8 | 'class': 'Fruit', # class: 位置的类名称,本例为 Fruit,后面需真实定义 class Fruit 9 | 'desc': '可以打零工赚钱的地方', # desc: 可写可不写,此位置的补充描述 10 | 'actions': {'1': {'name': '收银', # actions: 本位置中支持的互动操作集合,'1', '2' 为命令编号,必须是字符类型 11 | 'func': 'cash', 12 | 'desc': '打个零工赚点小钱吧'}, # desc: 可写可不写,此交互操作的补充描述 13 | 14 | '2': {'name': '领工钱', 'func': 'wage'} # name: 互动操作名称 15 | } # func: 互动操作的函数名称,该函数应写在本位置类中 \ 16 | } # 本例中 cash 和 wage 函数都应该写在Fruit类中 17 | 18 | noodle = { 19 | 'name': '重庆小面馆', 20 | 'class': 'Noodle', 21 | 'actions': {'1': {'name': '点单', 'func': 'order'}, 22 | '2': {'name': '开吃', 'func': 'dine'}, 23 | '3': {'name': '买单', 'func': 'pay'} 24 | } 25 | } 26 | 27 | 28 | class App(): # 框架约定的场所类名称,统一为 App,不可修改 29 | 30 | def __init__(self, _id, module): # 框架约定,不可修改 31 | self.id = _id # 框架约定,不可修改 32 | self.module = module # 框架约定,不可修改 33 | 34 | self.name = '泡泡大街' # name: 定义场所名称 35 | self.level = 1 # level: 定义角色能够进入此场所的最低等级(待实现) 36 | self.scenes = {'1': fruit, '2': noodle} # scenes: 定义本场所包含的位置集合,'1', '2' 为地点编号,必须是字符类型 \ 37 | # fruit, noodle 为上面自定义的位置信息,一定要完整正确 38 | self.desc = '商业大街,请尽情探索吧' # 可写可不写,此场所的补偿描述 39 | 40 | prices = {'橘子': 3, '葡萄': 12, '毛桃': 9, '香蕉': 6, '苹果': 7, '榴莲': 48} 41 | 42 | 43 | class Fruit(): 44 | 45 | def __init__(self): 46 | self.paid = 0 # 定义玩家赚的工钱 47 | self.fine = 0 # 定义玩家被扣罚的工钱 48 | 49 | def cash(self, text): 50 | 51 | cart = sample(prices.keys(), randint(1, 4)) # 在购物车中随机生成1~4种要购买的水果 52 | weight = {} # 水果重量表 53 | total = 0 # 预期总价格 54 | # color.info 打印提示性信息,浅灰色字体 55 | color.info('\n有一位顾客的购物清单如下:\n') 56 | color.info('-----------------------') 57 | for fruit in cart: 58 | weight[fruit] = randint(1, 9) # 为购物车中的水果称重(此处为 1~9 随机赋值) 59 | total += prices[fruit] * weight[fruit] # 计算预期总价格 60 | # color.option 打印重点内容或用户选项,以 0.1 秒间隔逐字打印,蓝色字体 61 | color.option(f'{fruit:>4} {prices[fruit]:>2} * {weight[fruit]}') # 打印购物清单给用户查看 62 | color.info('-----------------------') 63 | # color._input 彩色字体提示用户输入信息 64 | text = color._input('总价格:') # 提示用户计算出总价格 65 | if text == str(total): # 用户输入和预期总价格比较,注意:用户输入都是字符串,需把 total 转化为 str 66 | self.paid += 10 # 用户计算正确,佣金 +10 67 | # color.talk 打印对话,以 0.1 秒间隔逐字打印,粉色字体 68 | color.talk('收银正确,请继续加油吧~') # 打印恭喜对话, 69 | else: # 如果用户输入的计算结果有误 70 | self.fine += 10 # 罚款记录项 +10 71 | g.player.coins_plus(-10) # 实际扣除操作,用户金币 -10 72 | color.talk('啊哦,收银错误,倒扣 10 金币!') # 打印失败对话 73 | color.talk(f'你现在总金币为: {g.player.coins} 金币。') 74 | 75 | def wage(self, text): 76 | 77 | balance = self.paid - self.fine # 结余 78 | paid = self.paid # 临时保持工钱变量 79 | fine = self.fine # 临时保持罚金变量 80 | 81 | if self.paid > 0: 82 | g.player.coins_plus(self.paid) # 支付工钱,即玩家金币 + paid 83 | 84 | self.paid = 0 # 清空玩家待结算工钱 85 | self.fine = 0 # 清空玩家结算钱被扣的工钱 86 | 87 | if fine > 0: 88 | color.talk(f'钱都收错了,真是不用心啊,你被罚了 {fine} 金币!') 89 | if paid > 0: 90 | color.talk(f'不过好在你也算对了 {paid // 10} 次,赚了 {paid} 金币!') 91 | if balance == 0: 92 | color.talk(f'罚金与工钱正好抵消,你今天白干了,小兄die~') 93 | elif balance > 0: 94 | color.talk(f'扣去罚金,你今天赚了 {balance} 金币!') 95 | else: 96 | color.talk(f'真是白痴啊,金币没赚到,还被扣去 {balance} 金币!') 97 | else: 98 | if paid == 0: 99 | color.talk('你今天偷懒了吧,都没来上班,还想要工钱?') 100 | else: 101 | color.talk(f'恭喜你,今天赚了 {balance} 金币!') 102 | 103 | 104 | color.talk(f'你现在总金币为: {g.player.coins} 金币!') 105 | 106 | 107 | class Noodle(): # 定义位置类: Noodle 108 | 109 | def __init__(self): 110 | pass 111 | 112 | def order(self, text): # 在位置信息中定义的 order 互动操作函数,必须在此位置类中定义 113 | color.next('开发中...', header=True) 114 | 115 | def dine(self, text): 116 | color.next('开发中...', header=True) # 在位置信息中定义的 dine 互动操作函数,必须在此位置类中定义 117 | 118 | def pay(self, text): 119 | color.next('开发中...', header=True) # 在位置信息中定义的 pay 互动操作函数,必须在此位置类中定义 120 | -------------------------------------------------------------------------------- /popo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popogame/popo/eacbb7537017841265194fc0c0d6ebd10227c1fc/popo/__init__.py -------------------------------------------------------------------------------- /popo/character.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from time import sleep 4 | from popo import color 5 | from popo import g 6 | from popo import views as v 7 | 8 | 9 | class Character: 10 | 11 | def __init__(self): 12 | 13 | with open('data/data.json', 'r', encoding='utf8')as f: 14 | self.data = json.load(f) 15 | 16 | # 初始化角色: 读取角色属性,如果无名称,则提示用户输入名字 17 | name = self.data['name'] 18 | if not name: 19 | name = color._input(' 一个角色名字:') 20 | self.data['name'] = name 21 | self.save() 22 | color.info('\n# 创建人物成功\n') 23 | 24 | # 已有名字,直接进入角色 25 | else: 26 | color.info('\n# 角色加载成功') 27 | 28 | @property 29 | def coins(self): 30 | return self.data['coins']["balance"] 31 | 32 | def coins_plus(self, value): 33 | if isinstance(value, int): 34 | self.data['coins']["balance"] += value 35 | self.save() 36 | 37 | @property 38 | def level(self): 39 | return self.data["status"]["level"] 40 | 41 | def level_plus(self, value): 42 | if isinstance(value, int): 43 | self.data["status"]["level"] += value 44 | self.save() 45 | 46 | @property 47 | def hp(self): 48 | return self.data["status"]["hp"] 49 | 50 | def hp_plus(self, value): 51 | if isinstance(value, int): 52 | self.data["status"]["hp"] += value 53 | self.save() 54 | 55 | @property 56 | def max_hp(self): 57 | return self.data["status"]["max_hp"] 58 | 59 | def max_hp_plus(self, value): 60 | if isinstance(value, int): 61 | self.data["status"]["max_hp"] += value 62 | self.save() 63 | 64 | @property 65 | def battle(self): 66 | return self.data["status"]["battle"] 67 | 68 | def battle_plus(self, value): 69 | if isinstance(value, int): 70 | self.data["status"]["battle"] += value 71 | self.save() 72 | 73 | def show(self, _map=True): 74 | 75 | v.show(_map) 76 | 77 | def save(self): 78 | 79 | with open('data/data.json', 'w', encoding='utf8')as f: 80 | json.dump(self.data, f) 81 | 82 | def exit(self): 83 | g.clear() 84 | self.show(_map=False) 85 | 86 | color.tilte('~~ 即将退出游戏,欢迎下次光临 ~~', time=0.1) 87 | exit(0) 88 | 89 | def rename(self, name): 90 | 91 | if name.strip(): 92 | self.data['name'] = name 93 | self.save() 94 | else: 95 | color.error('--- 用户名无效 ---') 96 | 97 | def delete(self): 98 | 99 | with open('data/default.json', 'r', encoding='utf8')as f: 100 | self.data = json.load(f) 101 | self.save() 102 | 103 | os.system("cls") 104 | color.error('--- 当前角色已删除 ---\n') 105 | color.info('--- 游戏即将退出,请重启动游戏 ---') 106 | exit(0) 107 | -------------------------------------------------------------------------------- /popo/color.py: -------------------------------------------------------------------------------- 1 | from colored import fg, attr 2 | from popo import pprint as p 3 | from popo import views as v 4 | 5 | 6 | reset = attr('reset') 7 | 8 | 9 | def next(message='', header=False): 10 | if header: 11 | print() 12 | message = fg(242) + message + reset 13 | back = fg(114) + '↵' + reset 14 | input(message + back) 15 | 16 | 17 | def _input(message=''): 18 | print() 19 | notice = fg(114) + '> 请输入' + reset 20 | message = fg(242) + message + reset 21 | while True: 22 | text = input(notice + message + ' ').strip() 23 | if text: 24 | break 25 | return text 26 | 27 | 28 | def tilte(text, header=True, end=True, time=0.1): # 标题提示语,首尾换行,逐字打印 29 | color = fg(4) 30 | p.char_print(color + text + reset, header, end, time) 31 | 32 | 33 | def info(text, end=True): # 系统信息,浅灰色,直接打印,自定义尾换行 34 | color = fg(242) 35 | p.text_print(color + text + reset, end) 36 | 37 | 38 | def caption(text, end=False): # 系统重要信息,如用户名,版本 39 | color = fg(69) 40 | p.text_print(color + text + reset, end) 41 | 42 | 43 | def error(text, end=True): # 错误信息,需空一行打印 44 | color = fg(166) 45 | p.text_print(color + '\n' + text + reset, end) 46 | 47 | 48 | def talk(text, header=True, end=False, time=0.1): # 对话信息 49 | color = fg(182) 50 | p.char_print(color + text + reset, header, end, time) 51 | next() 52 | 53 | 54 | def option(text, header=False, end=True, time=0.1): # 用户选项 55 | color = fg(26) 56 | p.char_print(color + text + reset, header, end, time) 57 | 58 | 59 | def div(text, end=True): 60 | color = fg(240) 61 | p.text_print(color + text + reset, end) 62 | 63 | 64 | def label(text, end=False): # 标签名,首尾都不换行 65 | color = fg(12) 66 | p.text_print(color + text + reset, end) 67 | 68 | 69 | def vname(text, end=False): 70 | color = fg(140) 71 | p.text_print(color + text + reset, end) 72 | 73 | 74 | def profile(text): 75 | color = fg(140) 76 | p.text_print(color + text + reset) 77 | 78 | 79 | def index(text, end=True): 80 | color = fg(221) 81 | p.text_print(color + text + reset, end) 82 | 83 | 84 | def sub(text, end=True): # 下标信息,浅灰色,直接打印,自定义尾换行 85 | color = fg(239) 86 | p.text_print(color + ' (' + text + ')' + reset, end) 87 | 88 | 89 | def vmap(text): 90 | color = fg(221) 91 | p.text_print(color + text + '\n' + reset) 92 | 93 | 94 | def confirm(text): 95 | color = fg(208) 96 | return color + text + reset 97 | 98 | 99 | def sendkey(text): 100 | notice = fg(114) + '> 请输入' + reset 101 | cmd = fg(242) + ' 指令' + reset 102 | _or = fg(172) + ' | ' + reset 103 | message = fg(242) + text + reset 104 | return notice + cmd + _or + message + ' ' 105 | 106 | 107 | def rename(text): 108 | return fg(114) + text + reset 109 | -------------------------------------------------------------------------------- /popo/config.py: -------------------------------------------------------------------------------- 1 | # 配置文件 2 | 3 | name = '泡泡世界' 4 | author = 'tonglei' 5 | version = '0.1' 6 | 7 | # 此处配置 maps/ 文件夹下(除了home外)的文件名,不配置的则不会导入 8 | maps = ['street', 'river'] 9 | 10 | # help 命令不可更改 11 | help = [ 12 | 'where: 查看角色当前位置', 13 | 'status: 查看角色当前状态', 14 | 'help: 查看帮助', 15 | 'exit: 退出游戏', 16 | 'name: 查看角色名称', 17 | 'rename: 修改角色名称 ', 18 | 'delete: 删除账号' 19 | 'author: 查看作者', 20 | 'version: 查看版本' 21 | ] 22 | -------------------------------------------------------------------------------- /popo/g.py: -------------------------------------------------------------------------------- 1 | import os 2 | player = '' 3 | 4 | maps = {} 5 | scenes = {} 6 | 7 | current_area = None 8 | current_scene = None 9 | current_cmd = None 10 | no_this_cmd = False 11 | 12 | 13 | def clear(): 14 | os.system("cls") -------------------------------------------------------------------------------- /popo/game.py: -------------------------------------------------------------------------------- 1 | import signal 2 | from importlib import import_module 3 | from popo.character import Character 4 | from popo import g 5 | from popo import color 6 | from popo import config as u 7 | from popo import views as v 8 | 9 | 10 | def loading(): 11 | 12 | # 导入地图/位置 13 | u.maps = ['home'] + u.maps 14 | home_scenes = {} 15 | for (i, mod) in enumerate(u.maps): 16 | module = import_module('maps.' + mod) 17 | # 初始化场所模块,值为 App 类的实例 18 | g.maps[mod] = getattr(module, 'App')(str(i), mod) 19 | g.scenes[mod] = {} 20 | 21 | if i > 0: 22 | if hasattr(g.maps[mod], 'desc'): 23 | home_scenes[str(i)] = {'name': g.maps[mod].name, 'desc': g.maps[mod].desc} 24 | else: 25 | home_scenes[str(i)] = {'name': g.maps[mod].name} 26 | g.maps['home'].scenes = home_scenes 27 | 28 | 29 | def switch_place(): 30 | 31 | text = g.current_cmd 32 | if text is None: 33 | text = v.sendkey('场所编号:') 34 | 35 | flag = False 36 | if text != '0': 37 | for place in g.maps.values(): 38 | if place.id == text: 39 | g.current_place = place 40 | flag = True 41 | if not flag: 42 | color.error('--- 没有此场所编号 ---', end=False) 43 | color.next() 44 | else: 45 | color.info('~ 你已经在家里了 ~') 46 | 47 | g.current_cmd = None 48 | 49 | 50 | def switch_scene(): 51 | 52 | text = g.current_cmd 53 | if text is None: 54 | text = v.sendkey('位置编号:') 55 | 56 | if text == '0': 57 | g.current_place = g.maps['home'] 58 | else: 59 | scenes = g.current_place.scenes 60 | if text in scenes: 61 | g.current_scene = scenes[text] 62 | g.current_scene['id'] = text 63 | g.current_cmd = None 64 | else: 65 | color.error('--- 当前场所下没有此位置编号 ---', end=False) 66 | color.next() 67 | g.current_cmd = None 68 | 69 | 70 | def start(): 71 | 72 | g.player.show(_map=False) 73 | color.next('\n按回车键继续') 74 | g.current_place = g.maps['home'] 75 | 76 | while True: 77 | # '0': 退出当前位置(scene)或场所(place) 78 | if g.current_cmd == '0': 79 | # 当前位置(scene)存在,则只退出当前位置,保留当前场所(place) 80 | if g.current_scene: 81 | g.current_scene = None 82 | g.current_cmd = v.sendkey('位置编号:') 83 | # 当前位置(scene)不存在,,则回家,提示输入场所编号 84 | else: 85 | g.current_place = g.maps['home'] 86 | g.current_cmd = None 87 | 88 | # 如果是家,则提示输入场所编号 89 | elif g.current_place.name == '家': 90 | switch_place() 91 | 92 | # 如果位置为空,则提示输入位置编号 93 | elif g.current_scene is None: 94 | switch_scene() 95 | # 有场所和位置 96 | else: 97 | # 当面交互命令为空,则提示输入交互命令 98 | if g.current_cmd is None: 99 | if g.no_this_cmd: 100 | g.no_this_cmd = False 101 | color.error('--- 当前位置下没有此命令编号 ---', end=False) 102 | color.next() 103 | g.current_cmd = v.sendkey('命令编号:') 104 | # 有交互命令,执行交互命令 105 | else: 106 | if g.current_cmd in g.current_scene['actions']: 107 | if g.current_scene['id'] not in g.scenes[g.current_place.module]: 108 | module = import_module('maps.' + g.current_place.module) 109 | # 初始化位置类,值为 current_scene['class'] 类的实例 110 | g.scenes[g.current_place.module][g.current_scene['id']] = getattr( 111 | module, g.current_scene['class'])() 112 | 113 | scene = g.scenes[g.current_place.module][g.current_scene['id']] 114 | func = g.current_scene['actions'][g.current_cmd]['func'] 115 | getattr(scene, func)(g.current_cmd) 116 | else: 117 | g.no_this_cmd = True 118 | 119 | g.current_cmd = None 120 | 121 | 122 | def signal_handler(signal, frame): 123 | 124 | if g.player: 125 | g.player.exit() 126 | else: 127 | exit(-1) 128 | 129 | 130 | def app(): 131 | 132 | g.clear() 133 | 134 | signal.signal(signal.SIGINT, signal_handler) 135 | 136 | color.tilte(f'~~ 欢迎来到{u.name} ~~', header=False, time=0.1) 137 | 138 | # 读取角色 139 | loading() 140 | g.player = Character() 141 | start() 142 | -------------------------------------------------------------------------------- /popo/pprint.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | 3 | 4 | # 段落打印,段首空一行 5 | def p_print(text): 6 | 7 | print('\n' + text) 8 | 9 | 10 | # 文本打印,自定义尾部换行 11 | def text_print(text, end=True): 12 | if end: 13 | print(text) 14 | else: 15 | print(text, end='') 16 | 17 | 18 | # 逐个字符打印 19 | def char_print(text, header=True, end=True, time=0): 20 | if header: 21 | print() 22 | for c in text: 23 | print(c, end='', flush=True) 24 | if time: 25 | sleep(time) 26 | if end: 27 | print() 28 | -------------------------------------------------------------------------------- /popo/views.py: -------------------------------------------------------------------------------- 1 | from popo import g 2 | from popo import config as u 3 | from popo import color 4 | from animation import maps 5 | 6 | 7 | def confirm(): 8 | print() 9 | text = input(color.confirm('> 按 y 确认, 按其他键跳过: ')) 10 | 11 | if str(text).lower() == 'y': 12 | return True 13 | else: 14 | return False 15 | 16 | 17 | def sendkey(message='请输入:'): 18 | while True: 19 | g.clear() 20 | current() 21 | print() 22 | text = input(color.sendkey(message)).strip() 23 | if text: 24 | if text.lower() == 'where': 25 | print() 26 | current(_map=False) 27 | color.next('\n按回车继续') 28 | elif text.lower() == 'status': 29 | color.info('\n# 角色当前状态如下:\n') 30 | g.player.show(_map=False) 31 | color.next('\n按回车继续') 32 | elif text.lower() == 'help': 33 | print() 34 | for msg in u.help: 35 | color.info(msg) 36 | color.next('\n按回车继续') 37 | elif text.lower() == 'exit': 38 | y = confirm() 39 | if y: 40 | g.player.exit() 41 | elif text.lower() == 'author': 42 | color.caption('\n' + u.author) 43 | color.next(' ') 44 | elif text.lower() == 'version': 45 | color.caption('\n' + u.version) 46 | color.next(' ') 47 | elif text.lower() == 'name': 48 | color.caption('\n' + g.player.data['name']) 49 | color.next(' ') 50 | elif text.lower() == 'rename': 51 | y = confirm() 52 | if y: 53 | print() 54 | name = input(color.rename('请输入新的用户名: ')) 55 | g.player.rename(name) 56 | 57 | color.info('\n当前用户名: ', end=False) 58 | color.caption(f' {g.player.data["name"]}') 59 | color.next(' ') 60 | elif text.lower() == 'delete': 61 | y = confirm() 62 | if y: 63 | g.player.delete() 64 | else: 65 | print() 66 | else: 67 | break 68 | return text 69 | 70 | 71 | def current(_map=True): 72 | if _map: 73 | color.vmap(maps.home) 74 | color.label('当前场所: ') 75 | place_name = g.current_place.name 76 | place_module = g.current_place.module 77 | color.vname(place_name) 78 | 79 | if g.current_scene: 80 | color.info(' | ', end=False) 81 | color.label('当前位置: ') 82 | color.vname(g.current_scene['name']) 83 | else: 84 | if place_module == 'home': 85 | color.info('\n\n当前可以进入的场所如下:\n') 86 | else: 87 | color.info('\n\n当前可以进入的位置如下:\n') 88 | for _id, scene in g.current_place.scenes.items(): 89 | if 'desc' in scene: 90 | color.index(f'{_id}. {scene["name"]}', end=False) 91 | color.sub(scene['desc']) 92 | else: 93 | color.index(f'{_id}. {scene["name"]}') 94 | if place_module != 'home': 95 | color.info('0. 返回上一层') 96 | 97 | if g.current_scene: 98 | color.info('\n\n当前支持的命令如下:\n') 99 | for _id, action in g.current_scene['actions'].items(): 100 | if 'desc' in action: 101 | color.index(f'{_id}. {action["name"]}', end=False) 102 | color.sub(action['desc']) 103 | else: 104 | color.index(f'{_id}. {action["name"]}') 105 | color.info('0. 返回上一层') 106 | 107 | 108 | def menu(): 109 | print() 110 | for message in g.help: 111 | color.info(message) 112 | 113 | 114 | def show(_map=True): 115 | if _map: 116 | color.vmap(maps.home) 117 | 118 | color.div('-'*6 + '当前状态' + '-'*6) 119 | 120 | color.label('等 级: ') 121 | color.profile(f'{g.player.level}') 122 | 123 | color.label('生命值: ') 124 | color.profile(f'{g.player.hp} / {g.player.max_hp}') 125 | 126 | color.label('武力值: ') 127 | color.profile(f'{g.player.battle}') 128 | 129 | color.label('金 币: ') 130 | color.profile(f'{g.player.coins}') 131 | 132 | color.div('-'*20) 133 | -------------------------------------------------------------------------------- /start.py: -------------------------------------------------------------------------------- 1 | from popo.game import app 2 | 3 | app() --------------------------------------------------------------------------------