Skip to content

Commit

Permalink
Discord bot UI, some ux improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
lychanl committed Apr 26, 2021
1 parent a4cc2c9 commit e07d16b
Show file tree
Hide file tree
Showing 10 changed files with 145 additions and 29 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,7 @@ dmypy.json
# vscode

.vscode/

# secrets

/secrets/
53 changes: 32 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,41 @@ It is a hobby project. If you are interested in developement, feel free to add a

## Command line game

It requires python version 3 and PyYAML package.
It requires python version 3.7 or above and packages from `requirements-cli.txt` (PyYAML).

To run the game:

- Enter `code` directory
- Run `python3 main.py --world ../worlds/default cli`
- Try it! Create player with `start` command, and try commands `me` or `here` to see what's going on and what else can you do!
- Run `python3 code/main.py --world worlds/default cli`
- Try it! Create player with `start` command, and try commands `me` or `here` to see what's going on and what else can you do! (`help` might be useful too)

## Discord game

It requires python version 3.7 or above and packages from `requirements.txt` (PyYAML, discord).

To run the game:

- Prepare a discord bot that you want to use.
- Prepare configuration file - specify at least the bot token, but you may want to limit the game to a single channel or specify the prefix. See `discord.sample.yaml` for an example.
- Run `python3 code/main.py --world worlds/default --client-config your-discord-config.yaml discord`

WARNING: The game will reset if the program is reset. This will change in future version.

## Progress:

I'm working to create something that could be called the _alpha_ version.

Goals (developement):
- [x] Fully configurable worlds
- [x] Localized texts
- [ ] Discord bot UI (single global game, for now)
- [x] In-game help

Goals (gameplay):
- [x] Creating a player
- [x] Locations, travelling
- [x] Action points system, with regenerating action points
- [x] Location-based actions
- [x] Player-based actions
- [x] Items, resources, trading (with 'NPCs' only, for now)
- [x] Skills
- [x] Monsters, fighting
For a list of features for future versions see [https://github.com/lychanl/discord-text-rpg/issues]

## Changelog

#### 0.0.1
- configurable worlds
- localized texts
- discord bot UI (single global game, for now)
- in-game help
- creating a player
- locations, travelling
- action points system, with regenerating action points
- location-based actions
- player-based actions
- items, resources, trading (with 'NPCs' only, for now)
- skills
- monsters, fighting
65 changes: 65 additions & 0 deletions code/dtrpg/io/dcio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from dtrpg.io.text_io import TextIO

from asyncio import Lock
from discord import Client, Message
from traceback import print_exception
from typing import Sequence, TYPE_CHECKING

if TYPE_CHECKING:
from dtrpg.core import Game


class DiscordBotIO(Client, TextIO):
LIMIT = 2000

def __init__(self, game: 'Game', token: str, channel: str = None, prefix: str = ''):
self._token = token
self._action_lock = Lock()
self._game_channel = channel
self._prefix = prefix

TextIO.__init__(self, game)
Client.__init__(self)

def run(self) -> None:
Client.run(self, self._token)

def split_messages(self, messages: Sequence[str]) -> Sequence[str]:
if not messages:
return None
out = [messages[0]]
for msg in messages[1:]:
if len(msg) + len(out[-1]) + 1 < self.LIMIT:
out[-1] = out[-1] + '\n' + msg
else:
out[-1] = [msg]

return out

async def on_message(self, message: Message) -> None:
if message.author == self.user:
return

if not self._game_channel or str(message.channel) != self._game_channel:
return

if not message.content.startswith(self._prefix):
return

content = message.content[len(self._prefix):]

try:
async with self._action_lock:
out = self.command(message.author.id, content)

if out:
for msg in self.split_messages(out):
await message.channel.send(msg)

except Exception as e:
print("An exception has occured!")
print_exception(type(e), e, e.__traceback__)
print("Message:")
print(message.content)
print("Sender:")
print(message.author)
2 changes: 1 addition & 1 deletion code/dtrpg/io/text_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def __init__(self, value: str):


class TextIO:
def __init__(self, game: Game):
def __init__(self, game: Game, **kwargs):
self._game = game
self._basic_commands = {
'start': self._start,
Expand Down
38 changes: 34 additions & 4 deletions code/main.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
import sys

if sys.version_info.major < 3 or sys.version_info.major == 3 and sys.version_info.minor < 7:
raise RuntimeError(f"Python version must be at least 3.7. Current: {sys.version}")

from dtrpg.core import Game
from dtrpg.data.data_loader import DataLoader
from dtrpg.io import CommandLineIO

import argparse
import os
import yaml


INTERFACES = {
'cli': CommandLineIO
}

try:
from dtrpg.io.dcio import DiscordBotIO
INTERFACES['discord'] = DiscordBotIO
except ImportError:
pass


def prepare_game(schema_path: str, world_path: str, config_name: str, locale_path: str) -> Game:
loader = DataLoader()
Expand All @@ -19,10 +31,22 @@ def prepare_game(schema_path: str, world_path: str, config_name: str, locale_pat
return Game(config, objects)


def main(schema_path: str, world_path: str, config_name: str, locale_path: str, interface_name: str) -> None:
def load_client_config(path: str) -> dict:
if path:
with open(path, 'r') as f:
return yaml.safe_load(f)
else:
return {}


def main(
schema_path: str, world_path: str, config_name: str, locale_path: str, interface_name: str, client_config: str
) -> None:
game = prepare_game(schema_path, world_path, config_name, locale_path)

interface = INTERFACES[interface_name](game)
config_data = load_client_config(client_config)

interface = INTERFACES[interface_name](game, **config_data)
interface.run()


Expand All @@ -32,7 +56,7 @@ def main(schema_path: str, world_path: str, config_name: str, locale_path: str,
parser.add_argument(
'--schema',
type=str,
default=os.path.join('..', 'worlds', 'schema.yaml'),
default=os.path.join(os.path.dirname(__file__), '..', 'worlds', 'schema.yaml'),
help='Path to schema file'
)
parser.add_argument('--world', type=str, required=True, help='Path to the directory with world files')
Expand All @@ -44,6 +68,12 @@ def main(schema_path: str, world_path: str, config_name: str, locale_path: str,
help='Path to locales directory (default `locales` in world files'
)
parser.add_argument('--locale', type=str, default=None, help='Locale, default - first directory in locales')
parser.add_argument(
'--client-config',
type=str,
default=None,
help='Client configuration file'
)
parser.add_argument('interface', type=str, choices=INTERFACES.keys(), help='Interface')

args = parser.parse_args()
Expand All @@ -52,4 +82,4 @@ def main(schema_path: str, world_path: str, config_name: str, locale_path: str,
locale = args.locale or next(iter(os.listdir(locales_path)))
locale_path = os.path.join(locales_path, locale)

main(args.schema, args.world, args.config, locale_path, args.interface)
main(args.schema, args.world, args.config, locale_path, args.interface, args.client_config)
1 change: 1 addition & 0 deletions code/requirements-cli.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
PyYAML
3 changes: 2 additions & 1 deletion code/requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
PyYAML
-r requirements-cli.txt
discord-py >= 1.5
2 changes: 1 addition & 1 deletion code/test/test_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ def test_smoke(self) -> None:
+ r'Player attacks Forest Rat and hits, dealing 1 damage.*'
+ r'Player was defeated!.*Forest Rat wins!.*'
+ r'You regain consciousness after some time.*'
+ r'You are tired and injured, but alive. You get 1 health points. You loose 8 action points.*')
+ r'You are tired and injured, but alive. You get 1 health points. You lose 8 action points.*')

# TACTICS

Expand Down
4 changes: 4 additions & 0 deletions discord.sample.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Not a real token. Use token of your bot.
token: ABCDEFGHIJKLMNOPQRSTUVWX.abcdef.ABCDEFGHIJKLMNOP-abcdefghij
channel: game-channel
prefix: ?
2 changes: 1 addition & 1 deletion worlds/default/locales/en/default.csv
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ TravelAction,NAME,Travel to {self.to.strings['NAME']}
TravelEventResult,EVENT_NOW,You travel to {self.to.strings['NAME']}.
CreatureResource,YOU_HAVE,"You have {self.strings['VALUE', {'value': self.value}]}"
CreatureResource,VALUE,"{self.resource.strings['VALUE', {'value': self.value}]}"
ResourceChangeEventResult,EVENT_NOW,"{self.strings['CAUSE_NOW']}{' '.join([('You get ' if v > 0 else 'You loose ') + r.strings['VALUE', {'value': abs(v)}] + '.' for r, v in self.resource_changes.items() if not v == 0])}"
ResourceChangeEventResult,EVENT_NOW,"{self.strings['CAUSE_NOW']}{' '.join([('You get ' if v > 0 else 'You lose ') + r.strings['VALUE', {'value': abs(v)}] + '.' for r, v in self.resource_changes.items() if not v == 0])}"
ResourceChangeEventResult,CAUSE_NOW,
ItemReceivedEventResult,EVENT_NOW,"{self.strings['CAUSE_NOW']}You get {self.number}x {self.item.strings['NAME']}.{'You dropped ' + str(self.overflow.stack.number) + ' because you have no more space.' if self.overflow else ''}"
ItemReceivedEventResult,CAUSE_NOW,
Expand Down

0 comments on commit e07d16b

Please sign in to comment.