First layout of evadventure

This commit is contained in:
Griatch 2022-03-16 00:12:21 +01:00
parent b413e0f7f2
commit a07ef8e3c4
11 changed files with 690 additions and 0 deletions

View file

@ -0,0 +1,30 @@
# EvAdventure
Contrib by Griatch 2022
A complete example MUD using Evennia. This is the final result of what is
implemented if you follow the Getting-Started tutorial. It's recommended
that you follow the tutorial step by step and write your own code. But if
you prefer you can also pick apart or use this as a starting point for your
own game.
## Features
- Uses a MUD-version of the [Knave](https://rpggeek.com/rpg/50827/knave) old-school
fantasy ruleset by Ben Milton (classless and overall compatible with early
edition D&D), released under the Creative Commons Attribution (all uses,
including commercial are allowed
as long as attribution is given).
- Character creation using an editable character sheet
- Weapons, effects, healing and resting
- Two alternative combat systems (turn-based and twitch based)
- Magic (three spells)
- NPC/mobs with simple AI.
- Simple Quest system.
- Small game world.
- Coded using best Evennia practices, with unit tests.
## Installation
TODO

View file

@ -0,0 +1,354 @@
"""
Random tables - adopted from _Knave_.
"""
# Character generation tables
character_generation = {
"physique": [
"athletic",
"brawny",
"corpulent",
"delicate",
"gaunt",
"hulking",
"lanky",
"ripped",
"rugged",
"scrawny",
"short",
"sinewy",
"slender",
"flabby",
"statuesque",
"stout",
"tiny",
"towering",
"willowy",
"wiry",
],
"face": [
"bloated",
"blunt",
"bony",
"chiseled",
"delicate",
"elongated",
"patrician",
"pinched",
"hawkish",
"broken",
"impish",
"narrow",
"ratlike",
"round",
"sunken",
"sharp",
"soft",
"square",
"wide",
"wolfish",
],
"skin": [
"battle scar",
"birthmark",
"burn scar",
"dark",
"makeup",
"oily",
"pale",
"perfect",
"pierced",
"pockmarked",
"reeking",
"tattooed",
"rosy",
"rough",
"sallow",
"sunburned",
"tanned",
"war paint",
"weathered",
"whip scar",
],
"hair": [
"bald",
"braided",
"bristly",
"cropped",
"curly",
"disheveled",
"dreadlocks",
"filthy",
"frizzy",
"greased",
"limp",
"long",
"luxurious",
"mohawk",
"oily",
"ponytail",
"silky",
"topknot",
"wavy",
"wispy",
],
"clothing": [
"antique",
"bloody",
"ceremonial",
"decorated",
"eccentric",
"elegant",
"fashionable",
"filthy",
"flamboyant",
"stained",
"foreign",
"frayed",
"frumpy",
"livery",
"oversized",
"patched",
"perfumed",
"rancid",
"torn",
"undersized",
],
"virtue": [
"ambitious",
"cautious",
"courageous",
"courteous",
"curious",
"disciplined",
"focused",
"generous",
"gregarious",
"honest",
"honorable",
"humble",
"idealistic",
"just",
"loyal",
"merciful",
"righteous",
"serene",
"stoic",
"tolerant",
],
"vice": [
"aggressive",
"arrogant",
"bitter",
"cowardly",
"cruel",
"deceitful",
"flippant",
"gluttonous",
"greedy",
"irascible",
"lazy",
"nervous",
"prejudiced",
"reckless",
"rude",
"suspicious",
"vain",
"vengeful",
"wasteful",
"whiny",
],
"speech": [
"blunt",
"booming",
"breathy",
"cryptic",
"drawling",
"droning",
"flowery",
"formal",
"gravelly",
"hoarse",
"mumbling",
"precise",
"quaint",
"rambling",
"rapid-fire",
"dialect",
"slow",
"squeaky",
"stuttering",
"whispery",
],
"background": [
"alchemist",
"beggar",
"butcher",
"burglar",
"charlatan",
"cleric",
"cook",
"cultist",
"gambler",
"herbalist",
"magician",
"mariner",
"mercenary",
"merchant",
"outlaw",
"performer",
"pickpocket",
"smuggler",
"student",
"tracker",
],
"mifortuntes": [
"abandoned",
"addicted",
"blackmailed",
"condemned",
"cursed",
"defrauded",
"demoted",
"discredited",
"disowned",
"exiled",
"framed",
"haunted",
"kidnapped",
"mutilated",
"poor",
"pursued",
"rejected",
"replaced",
"robbed",
"suspected",
],
"alignment": [
('1-5', "law"),
('6-15', "neutrality"),
('16-20', "chaos"),
],
"armor": [
('1-3', "no armor"),
('4-14', "gambeson"),
('15-19', "brigandine"),
('20', "chain"),
],
"helmets and shields": [
('1-13', "no helmet"),
('14-16', "helmet"),
('17-19', "shield"),
('20', "helmet and shield"),
],
"starting weapon": [ # note: these are all d6 dmg weapons
('1-7', "dagger",
'8-13', "club",
'14-20', "staff"),
],
"dungeoning gear": [
"rope, 50ft",
"pulleys",
"candles, 5",
"chain, 10ft",
"chalk, 10",
"crowbar",
"tinderbox",
"grap. hook",
"hammer",
"waterskin",
"lantern",
"lamp oil",
"padlock",
"manacles",
"mirror",
"pole, 10ft",
"sack",
"tent",
"spikes, 5",
"torches, 5",
],
"general gear 1": [
"air bladder",
"bear trap",
"shovel",
"bellows",
"grease",
"saw",
"bucket",
"caltrops",
"chisel",
"drill",
"fish. rod",
"marbles",
"glue",
"pick",
"hourglass",
"net",
"tongs",
"lockpicks",
"metal file",
"nails",
],
"general gear 2": [
"incense",
"sponge",
"lens",
"perfume",
"horn",
"bottle",
"soap",
"spyglass",
"tar pot",
"twine",
"fake jewels",
"blank book",
"card deck",
"dice set",
"cook pots",
"face paint",
"whistle",
"instrument",
"quill & ink",
"small bell",
],
"name": [
"Abbo", "Adelaide", "Ellis", "Eleanor", "Lief", "Luanda", "Ablerus", "Agatha",
"Eneto", "Elizabeth", "Luke", "Lyra", "Acot", "Aleida", "Enio", "Elspeth", "Martin",
"Mabel", "Alexander", "Alexia", "Eral", "Emeline", "Merrick", "Maerwynn", "Almanzor",
"Alianor", "Erasmus", "Emma", "Mortimer", "Malkyn", "Althalos", "Aline", "Eustace",
"Emmony", "Ogden", "Margaret", "Ancelot", "Alma", "Everard", "Enna", "Oliver", "Margery",
"Asher", "Alys", "Faustus", "Enndolynn", "Orion", "Maria", "Aster", "Amabel", "Favian",
"Eve", "Oswald", "Marion", "Balan", "Amice", "Fendrel", "Evita", "Pelagon", "Matilda",
"Balthazar", "Anastas", "Finn", "Felice", "Pello", "Millicent", "Barat", "Angmar",
"Florian", "Fern", "Peyton", "Mirabelle", "Bartholomew", "Annabel", "Francis", "Floria",
"Philip", "Muriel", "Basil", "Arabella", "Frederick", "Fredegonde", "Poeas", "Nabarne",
"Benedict", "Ariana", "Gaidon", "Gillian", "Quinn", "Nell", "Berinon", "Ayleth", "Gavin",
"Gloriana", "Ralph", "Nesea", "Bertram", "Barberry", "Geoffrey", "Godeleva", "Randolph",
"Niree", "Beves", "Barsaba", "Gerard", "Godiva", "Reginald", "Odette", "Bilmer",
"Basilia", "Gervase", "Gunnilda", "Reynold", "Odila", "Blanko", "Beatrix", "Gilbert",
"Gussalen", "Richard", "Oria", "Bodo", "Benevolence", "Giles", "Gwendolynn", "Robert",
"Osanna", "Borin", "Bess", "Godfrey", "Hawise", "Robin", "Ostrythe", "Bryce", "Brangian",
"Gregory", "Helena", "Roger", "Ottilia", "Carac", "Brigida", "Gringoire", "Helewise",
"Ronald", "Panope", "Caspar", "Brunhild", "Gunthar", "Hester", "Rowan", "Paternain",
"Cassius", "Camilla", "Guy", "Hildegard", "Rulf", "Pechel", "Cedric", "Canace", "Gyras",
"Idony", "Sabin", "Pepper", "Cephalos", "Cecily", "Hadrian", "Isabella", "Sevrin",
"Petronilla", "Chadwick", "Cedany", "Hedelf", "Iseult", "Silas", "Phrowenia", "Charillos",
"Christina", "Hewelin", "Isolde", "Simon", "Poppy", "Charles", "Claramunda", "Hilderith",
"Jacquelyn", "Solomon", "Quenell", "Chermon", "Clarice", "Humbert", "Jasmine", "Stephen",
"Raisa", "Clement", "Clover", "Hyllus", "Jessamine", "Terrowin", "Reyna", "Clifton",
"Collette", "Ianto", "Josselyn", "Thomas", "Rixende", "Clovis", "Constance", "Ibykos",
"Juliana", "Tristan", "Rosamund", "Cyon", "Damaris", "Inigo", "Karitate", "Tybalt",
"Rose", "Dain", "Daphne", "Itylus", "Katelyn", "Ulric", "Ryia", "Dalmas", "Demona",
"James", "Katja", "Walter", "Sarah", "Danor", "Dimia", "Jasper", "Katrina", "Wander",
"Seraphina", "Destrian", "Dione", "Jiles", "Kaylein", "Warin", "Thea", "Domeka",
"Dorothea", "Joffridus", "Kinna", "Waverly", "Trillby", "Donald", "Douce", "Jordan",
"Krea", "Willahelm", "Wendel", "Doran", "Duraina", "Joris", "Kypris", "William",
"Wilberga", "Dumphey", "Dyota", "Josef", "Landerra", "Wimarc", "Winifred", "Eadmund",
"Eberhild", "Laurence", "Larraza", "Wystan", "Wofled", "Eckardus", "Edelot", "Leofrick",
"Linet", "Xalvador", "Wymarc", "Edward", "Edyva", "Letholdus", "Loreena", "Zane", "Ysmay",
],
}

View file

@ -0,0 +1,259 @@
"""
MUD ruleset based on the _Knave_ OSR tabletop RPG by Ben Milton (modified for MUD use).
The rules are divided into three parts:
- Character generation - these are rules only used when creating a character.
- Improvement - these are rules used with experience to improve the character
over time.
- Actions - all in-game interactions (making use of the character's abilities)
are defined as discreet _actions_ in the game. An action is the smallest rule
unit to accomplish something with rule support. While in a tabletop game you
have a human game master to arbitrate, the computer requires exactness. While
free-form roleplay is also possible, only the actions defined here will have a
coded support.
"""
from dataclasses import dataclass
from .utils import roll
from .random_tables import character_generation as chargen_table
# Basic rolls
def saving_throw(bonus, advantage=False, disadvantage=False):
"""
To save, roll d20 + relevant Attrribute bonus > 15 (always 15).
Args:
advantage (bool): Roll 2d20 and use the bigger number.
disadvantage (bool): Roll 2d20 and use the smaller number.
Returns:
bool: If the save was passed or not.
Notes:
Advantage and disadvantage cancel each other out.
Example:
Trying to overcome the effects of poison, roll d20 + Constitution-bonus above 15.
"""
if not (advantage or disadvantage) or (advantage and disadvantage):
# normal roll
dice_roll = roll("1d20")
elif advantage:
dice_roll = max(roll("1d20"), roll("1d20"))
else:
dice_roll = min(roll("1d20"), roll("1d20"))
return (dice_roll + bonus) > 15
def roll_attribute_bonus():
"""
For the MUD version, we use a flat bonus and let the user redistribute it. This
function (unused by default) implements the original Knave random generator for
the Attribute bonus, if you prefer producing more 'unbalanced' characters.
The Attribute bonus is generated by rolling the lowest value of 3d6.
Returns:
int: The randomly generated Attribute bonus.
"""
return min(roll("1d6"), roll("1d6"), roll("1d6"))
def roll_random_table(dieroll, table, table_choices):
"""
Make a roll on a random table.
Args:
dieroll (str): The dice to roll, like 1d6, 1d20, 3d6 etc).
table_choices (iterable): If a list of single elements, the die roll
should fully encompass the table, like a 1d20 roll for a table
with 20 elements. If each element is a tuple, the first element
of the tuple is assumed to be a string 'X-Y' indicating the
range of values that should match the roll.
Returns:
Any: The result of the random roll.
Example:
`roll table_choices = [('1-5', "Blue"), ('6-9': "Red"), ('10', "Purple")]`
Notes:
If the roll is outside of the listing, the closest edge value is used.
"""
roll_result = roll(dieroll)
if isinstance(table_choices[0], (tuple, list)):
# tuple with range conditional, like ('1-5', "Blue") or ('10', "Purple")
max_range = -1
min_range = 10**6
for (valrange, choice) in table_choices:
minval, *maxval = valrange.split('-', 1)
minval = abs(int(minval))
maxval = abs(int(maxval[0]) if maxval else minval)
# we store the largest/smallest values so far in case we need to use them
max_range = max(max_range, maxval)
min_range = min(min_range, minval)
if minval <= roll_result <= maxval:
return choice
# if we have no result, we are outside of the range, we pick the edge values. It is also
# possible the range contains 'gaps', but that'd be an error in the random table itself.
if roll_result > max_range:
return max_range
else:
return min_range
else:
# regular list - one line per value.
roll_result = max(1, min(len(table_choices), roll_result))
return table_choices[roll_result - 1]
# character generation
@dataclass
class CharAttribute:
"""
A character Attribute, like strength or wisdom, has a _bonus_, used
to improve the result of doing a related action. It also has a _defense_ value
which is always 10 points higher than the bonus. For example, to attack
someone, you'd have to roll d20 + `strength bonus` to beat the `strength defense`
of the enemy.
"""
bonus: str = 0
@property
def defense(self):
return bonus + 10
class CharacterGeneration:
"""
This collects all the rules for generating a new character. An instance of this class can be
used to track all the stats during generation and will be used to apply all the data to the
character at the end. This class instance can also be saved temporarily to make sure a user
is not losing their half-created character.
Note:
Unlike standard Knave, characters will come out more similar here. This is because in
a table top game it's fun to roll randomly and have to live with a crappy roll - but
online players can (and usually will) just disconnect and reroll until they get values
they are happy with.
So, in standard Knave, the character's attribute bonus is rolled randomly and will give a
value 1-6; and there is no guarantee for 'equal' starting characters. Instead we
homogenize the results to a flat +2 bonus and let people redistribute the
points afterwards. This also allows us to show off some more advanced concepts in the
chargen menu, but you can also easily make it random like in base Knave by using the
(currently unused, but included) `roll_attribute_bonus` function above to get the bonus
instead of the flat +2.
In the same way, Knave uses a d8 roll to get the initial hit points. Instead we use a
flat max of 8 HP to start, in order to give players a little more survivability.
We *will* roll random start equipment though. Contrary to standard Knave, we'll also
randomly assign the starting weapon among a small selection of equal-dmg weapons (since
there is no GM to adjudicate a different choice).
"""
def __init__(self):
"""
Initialize starting values
"""
# name will likely be modified later
self.name = roll_random_table('1d282', chargen_table['name'])
# base attribute bonuses
self.strength = CharAttribute(bonus=2)
self.dexterity = CharAttribute(bonus=2)
self.constitution = CharAttribute(bonus=2)
self.intelligence = CharAttribute(bonus=2)
self.wisdom = CharAttribute(bonus=2)
self.charisma = CharAttribute(bonus=2)
self.armor = CharAttribute(bonus=1) # un-armored default
# physical attributes (only for rp purposes)
self.physique = roll_random_table('1d20', chargen_table['physique'])
self.face = roll_random_table(chargen_table['1d20', 'face'])
self.skin = roll_random_table(chargen_table['1d20', 'skin'])
self.hair = roll_random_table(chargen_table['1d20', 'hair'])
self.clothing = roll_random_table(chargen_table['1d20', 'clothing'])
self.virtue = roll_random_table(chargen_table['1d20', 'virtue'])
self.vice = roll_random_table(chargen_table['1d20', 'vice'])
self.background = roll_random_table(chargen_table['1d20', 'background'])
self.misfortune = roll_random_table(chargen_table['1d20', 'misfortune'])
self.alignment = roll_random_table(chargen_table['1d20', 'alignment'])
# same for all
self.exploration_speed = 120
self.combat_speed = 40
self.hp = 0
self.xp = 0
self.level = 1
# random equipment
self.armor = roll_random_table('1d20', chargen_table['armor'])
_helmet_and_shield = roll_random_table('1d20', chargen_table["helmets and shields"])
self.helmet = "helmet" if "helmet" in _helmet_and_shield else "none"
self.shield = "shield" if "shield" in _helmet_and_shield else "none"
self.weapon = roll_random_table(chargen_table['1d20', "starting_weapon"])
self.equipment = [
"ration",
"ration",
roll_random_table(chargen_table['1d20', "dungeoning gear"]),
roll_random_table(chargen_table['1d20', "dungeoning gear"]),
roll_random_table(chargen_table['1d20', "general gear 1"]),
roll_random_table(chargen_table['1d20', "general gear 2"]),
]
def adjust_attribute(self, source_attribute, target_attribute, value):
"""
Redistribute bonus from one attribute to another. The resulting values
must not be lower than +1 and not above +6.
Args:
source_attribute (str): The name of the attribute to deduct bonus from, like 'strength'
target_attribute (str): The attribute to give the bonus to, like 'dexterity'.
value (int): How much to change. This is always 1 for the current chargen.
Raises:
ValueError: On input error, using invalid values etc.
Notes:
We assume the strings are provided by the chargen, so we don't do
much input validation here, we do make sure we don't overcharge ourselves though.
"""
# we use getattr() to fetch the CharaAttribute of e.g. the .strength property etc
source_current_bonus = getattr(self, source_attribute).bonus
target_current_bonus = getattr(self, target_attribute).bonus
if source_current_val - value < 1:
raise ValueError(f"You can't reduce the {source_attribute} bonus below +1.")
if target_current_val + value > 6:
raise ValueError(f"You can't increase the {target_attribute} bonus above +6.")
# all is good, apply the change.
setattr(self, source_attribute, CharAttribute(bonus=source_current_val - value))
setattr(self, target_attribute, CharAttribute(bonus=source_current_val + value))
def apply(self, character):
"""
Once the chargen is complete, call this to transfer all the data to the character
permanently.
"""

View file

@ -0,0 +1,47 @@
"""
Various utilities.
"""
from random import randint
def roll(roll_string, max_number=10):
"""
NOTE: In evennia/contribs/rpg/dice/ is a more powerful dice roller with
more features, such as modifiers, secret rolls etc. This is much simpler and only
gets a simple sum of normal rpg-dice.
Args:
roll_string (str): A roll using standard rpg syntax, <number>d<diesize>, like
1d6, 2d10 etc. Max die-size is 1000.
max_number (int): The max number of dice to roll. Defaults to 10, which is usually
more than enough.
Returns:
int: The rolled result - sum of all dice rolled.
Raises:
TypeError: If roll_string is not on the right format or otherwise doesn't validate.
Notes:
Since we may see user input to this function, we make sure to validate the inputs (we
wouldn't bother much with that if it was just for developer use).
"""
max_diesize = 1000
roll_string = roll_string.lower()
if 'd' not in roll_string:
raise TypeError(f"Dice roll '{roll_string}' was not recognized. Must be `<number>d<dicesize>`.")
number, diesize = roll_string.split('d', 1)
try:
number = int(number)
diesize = int(diesize)
except Exception:
raise TypeError(f"The number and dice-size of '{roll_string}' must be numerical.")
if 0 < number > max_number:
raise TypeError(f"Invalid number of dice rolled (must be between 1 and {max_number})")
if 0 < diesize > max_diesize:
raise TypeError(f"Invalid die-size used (must be between 1 and {max_diesize} sides)")
# At this point we know we have valid input - roll and all dice together
return sum(randint(1, diesize) for _ in range(number))