Debugging of tutorial

This commit is contained in:
Griatch 2022-07-18 16:58:45 +02:00
parent 604769f762
commit 73d8f24b7c
8 changed files with 189 additions and 391 deletions

View file

@ -30,7 +30,7 @@ from evennia.contrib.tutorials.evadventure.objects import (
EvAdventureRunestone,
EvAdventureWeapon,
)
from evennia.contrib.tutorials.evadventure.rooms import EvAdventureRoom
from evennia.contrib.tutorials.evadventure.rooms import EvAdventurePvPRoom, EvAdventureRoom
# CODE
@ -65,7 +65,7 @@ create_object(
# A combat room evtechdemo#01
# with a static enemy
combat_room = create_object(EvAdventureRoom, key="Combat Arena", aliases=("evtechdemo#01",))
combat_room = create_object(EvAdventurePvPRoom, key="Combat Arena", aliases=("evtechdemo#01",))
# link to/back to hub
hub_room = search_object("evtechdemo#00")[0]
create_object(

View file

@ -1,5 +1,5 @@
"""
Base Character and NPCs.
Character class.
"""
@ -8,336 +8,8 @@ from evennia.typeclasses.attributes import AttributeProperty
from evennia.utils.utils import lazy_property
from . import rules
from .enums import Ability, WieldLocation
from .objects import WeaponEmptyHand
class EquipmentError(TypeError):
pass
class EquipmentHandler:
"""
_Knave_ puts a lot of emphasis on the inventory. You have CON_DEFENSE inventory
slots. Some things, like torches can fit multiple in one slot, other (like
big weapons and armor) use more than one slot. The items carried and wielded has a big impact
on character customization - even magic requires carrying a runestone per spell.
The inventory also doubles as a measure of negative effects. Getting soaked in mud
or slime could gunk up some of your inventory slots and make the items there unusuable
until you clean them.
"""
save_attribute = "inventory_slots"
def __init__(self, obj):
self.obj = obj
self._load()
def _load(self):
"""
Load or create a new slot storage.
"""
self.slots = self.obj.attributes.get(
self.save_attribute,
category="inventory",
default={
WieldLocation.WEAPON_HAND: None,
WieldLocation.SHIELD_HAND: None,
WieldLocation.TWO_HANDS: None,
WieldLocation.BODY: None,
WieldLocation.HEAD: None,
WieldLocation.BACKPACK: [],
},
)
def _count_slots(self):
"""
Count slot usage. This is fetched from the .size Attribute of the
object. The size can also be partial slots.
"""
slots = self.slots
wield_usage = sum(
getattr(slotobj, "size", 0) or 0
for slot, slotobj in slots.items()
if slot is not WieldLocation.BACKPACK
)
backpack_usage = sum(
getattr(slotobj, "size", 0) or 0 for slotobj in slots[WieldLocation.BACKPACK]
)
return wield_usage + backpack_usage
def _save(self):
"""
Save slot to storage.
"""
self.obj.attributes.add(self.save_attribute, self.slots, category="inventory")
@property
def max_slots(self):
"""
The max amount of equipment slots ('carrying capacity') is based on
the constitution defense.
"""
return getattr(self.obj, Ability.CON.value, 1) + 10
def validate_slot_usage(self, obj):
"""
Check if obj can fit in equipment, based on its size.
Args:
obj (EvAdventureObject): The object to add.
Raise:
EquipmentError: If there's not enough room.
"""
size = getattr(obj, "size", 0)
max_slots = self.max_slots
current_slot_usage = self._count_slots()
if current_slot_usage + size > max_slots:
slots_left = max_slots - current_slot_usage
raise EquipmentError(
f"Equipment full ($int2str({slots_left}) slots "
f"remaining, {obj.key} needs $int2str({size}) "
f"$pluralize(slot, {size}))."
)
return True
@property
def armor(self):
"""
Armor provided by actually worn equipment/shield. For body armor
this is a base value, like 12, for shield/helmet, it's a bonus, like +1.
We treat values and bonuses equal and just add them up. This value
can thus be 0, the 'unarmored' default should be handled by the calling
method.
Returns:
int: Armor from equipment. Note that this is the +bonus of Armor, not the
'defense' (to get that one adds 10).
"""
slots = self.slots
return sum(
(
# armor is listed using its defense, so we remove 10 from it
# (11 is base no-armor value in Knave)
getattr(slots[WieldLocation.BODY], "armor", 11) - 10,
# shields and helmets are listed by their bonus to armor
getattr(slots[WieldLocation.SHIELD_HAND], "armor", 0),
getattr(slots[WieldLocation.HEAD], "armor", 0),
)
)
@property
def weapon(self):
"""
Conveniently get the currently active weapon or rune stone.
Returns:
obj or None: The weapon. None if unarmored.
"""
# first checks two-handed wield, then one-handed; the two
# should never appear simultaneously anyhow (checked in `use` method).
slots = self.slots
weapon = slots[WieldLocation.TWO_HANDS]
if not weapon:
weapon = slots[WieldLocation.WEAPON_HAND]
if not weapon:
weapon = WeaponEmptyHand()
return weapon
def display_loadout(self):
"""
Get a visual representation of your current loadout.
Returns:
str: The current loadout.
"""
slots = self.slots
weapon_str = "You are fighting with your bare fists"
shield_str = " and have no shield."
armor_str = "You wear no armor"
helmet_str = " and no helmet."
two_hands = slots[WieldLocation.TWO_HANDS]
if two_hands:
weapon_str = f"You wield {two_hands} with both hands"
shield_str = " (you can't hold a shield at the same time)."
else:
one_hands = slots[WieldLocation.WEAPON_HAND]
if one_hands:
weapon_str = f"You are wielding {one_hands} in one hand."
shield = slots[WieldLocation.SHIELD_HAND]
if shield:
shield_str = f"You have {shield} in your off hand."
armor = slots[WieldLocation.BODY]
if armor:
armor_str = f"You are wearing {armor}"
helmet = slots[WieldLocation.BODY]
if helmet:
helmet_str = f" and {helmet} on your head."
return f"{weapon_str}{shield_str}\n{armor_str}{helmet_str}"
def use(self, obj):
"""
Make use of item - this makes use of the object's wield slot to decide where
it goes. If it doesn't have any, it goes into backpack.
Args:
obj (EvAdventureObject): Thing to use.
Raises:
EquipmentError: If there's no room in inventory. It will contains the details
of the error, suitable to echo to user.
Notes:
If using an item already in the backpack, it should first be `removed` from the
backpack, before applying here - otherwise, it will be added a second time!
this will cleanly move any 'colliding' items to the backpack to
make the use possible (such as moving sword + shield to backpack when wielding
a two-handed weapon). If wanting to warn the user about this, it needs to happen
before this call.
"""
# first check if we have room for this
self.validate_slot_usage(obj)
slots = self.slots
use_slot = getattr(obj, "inventory_use_slot", WieldLocation.BACKPACK)
if use_slot is WieldLocation.TWO_HANDS:
# two-handed weapons can't co-exist with weapon/shield-hand used items
slots[WieldLocation.WEAPON_HAND] = slots[WieldLocation.SHIELD_HAND] = None
slots[use_slot] = obj
elif use_slot in (WieldLocation.WEAPON_HAND, WieldLocation.SHIELD_HAND):
# can't keep a two-handed weapon if adding a one-handede weapon or shield
slots[WieldLocation.TWO_HANDS] = None
slots[use_slot] = obj
elif use_slot is WieldLocation.BACKPACK:
# backpack has multiple slots.
slots[use_slot].append(obj)
else:
# for others (body, head), just replace whatever's there
slots[use_slot] = obj
# store new state
self._save()
def add(self, obj):
"""
Put something in the backpack specifically (even if it could be wield/worn).
"""
# check if we have room
self.validate_slot_usage(obj)
self.slots[WieldLocation.BACKPACK].append(obj)
self._save()
def can_remove(self, leaving_object):
"""
Called to check if the object can be removed.
"""
return True # TODO - some things may not be so easy, like mud
def remove(self, obj_or_slot):
"""
Remove specific object or objects from a slot.
Args:
obj_or_slot (EvAdventureObject or WieldLocation): The specific object or
location to empty. If this is WieldLocation.BACKPACK, all items
in the backpack will be emptied and returned!
Returns:
list: A list of 0, 1 or more objects emptied from the inventory.
"""
slots = self.slots
ret = []
if isinstance(obj_or_slot, WieldLocation):
if obj_or_slot is WieldLocation.BACKPACK:
# empty entire backpack
ret.extend(slots[obj_or_slot])
slots[obj_or_slot] = []
else:
ret.append(slots[obj_or_slot])
slots[obj_or_slot] = None
elif obj_or_slot in self.slots.values():
# obj in use/wear slot
for slot, objslot in slots.items():
if objslot is obj_or_slot:
slots[slot] = None
ret.append(objslot)
elif obj_or_slot in slots[WieldLocation.BACKPACK]:
# obj in backpack slot
try:
slots[WieldLocation.BACKPACK].remove(obj_or_slot)
ret.append(obj_or_slot)
except ValueError:
pass
if ret:
self._save()
return ret
def get_wieldable_objects_from_backpack(self):
"""
Get all wieldable weapons (or spell runes) from backpack. This is useful in order to
have a list to select from when swapping your wielded loadout.
Returns:
list: A list of objects with a suitable `inventory_use_slot`. We don't check
quality, so this may include broken items (we may want to visually show them
in the list after all).
"""
return [
obj
for obj in self.slots[WieldLocation.BACKPACK]
if obj.inventory_use_slot
in (WieldLocation.WEAPON_HAND, WieldLocation.TWO_HANDS, WieldLocation.SHIELD_HAND)
]
def get_wearable_objects_from_backpack(self):
"""
Get all wearable items (armor or helmets) from backpack. This is useful in order to
have a list to select from when swapping your worn loadout.
Returns:
list: A list of objects with a suitable `inventory_use_slot`. We don't check
quality, so this may include broken items (we may want to visually show them
in the list after all).
"""
return [
obj
for obj in self.slots[WieldLocation.BACKPACK]
if obj.inventory_use_slot in (WieldLocation.BODY, WieldLocation.HEAD)
]
def get_usable_objects_from_backpack(self):
"""
Get all 'usable' items (like potions) from backpack. This is useful for getting a
list to select from.
Returns:
list: A list of objects that are usable.
"""
character = self.obj
return [obj for obj in self.slots[WieldLocation.BACKPACK] if obj.at_pre_use(character)]
from .equipment import EquipmentHandler
from .quests import EvAdventureQuestHandler
class LivingMixin:
@ -407,6 +79,16 @@ class LivingMixin:
"""
pass
def at_loot(self, looted):
"""
Called when looting another entity.
Args:
looted: The thing to loot.
"""
looted.get_loot()
def get_loot(self, looter):
"""
Called when being looted (after defeat).
@ -418,8 +100,8 @@ class LivingMixin:
max_steal = rules.dice.roll("1d10")
owned = self.coin
stolen = max(max_steal, owned)
self.coin -= stolen
looter.coin += stolen
self.coins -= stolen
looter.coins += stolen
self.location.msg_contents(
f"$You(looter) loots $You() for {stolen} coins!",
@ -434,6 +116,9 @@ class LivingMixin:
Args:
defeated_enemy (Object): The enemy soon to loot.
Returns:
bool: If False, no looting is allowed.
"""
pass
@ -481,6 +166,11 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
"""Allows to access equipment like char.equipment.worn"""
return EquipmentHandler(self)
@lazy_property
def quests(self):
"""Access and track quests"""
return EvAdventureQuestHandler(self)
@property
def weapon(self):
return self.equipment.weapon
@ -544,14 +234,18 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
the death table.
"""
rules.dice.roll_death(self)
if self.hp > 0:
# still alive, but lost some stats
self.location.msg_contents(
"|y$You() $conj(stagger) back and fall to the ground - alive, "
"but unable to move.|n",
from_obj=self,
)
if self.location.allow_death:
rules.dice.roll_death(self)
if self.hp > 0:
# still alive, but lost some stats
self.location.msg_contents(
"|y$You() $conj(stagger) back and fall to the ground - alive, "
"but unable to move.|n",
from_obj=self,
)
else:
self.location.msg_contents("|y$You() $conj(yield), beaten and out of the fight.|n")
self.hp = self.hp_max
def at_death(self):
"""
@ -562,3 +256,17 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
"|r$You() $conj(collapse) in a heap.\nDeath embraces you ...|n",
from_obj=self,
)
def at_pre_loot(self):
"""
Called before allowing to loot. Return False to block enemy looting.
"""
# don't allow looting in pvp
return not self.location.allow_pvp
def get_loot(self, looter):
"""
Called when being looted.
"""
pass

View file

@ -111,7 +111,7 @@ from .enums import Ability
from .npcs import EvAdventureNPC
COMBAT_HANDLER_KEY = "evadventure_turnbased_combathandler"
COMBAT_HANDLER_INTERVAL = 60
COMBAT_HANDLER_INTERVAL = 30
class CombatFailure(RuntimeError):
@ -156,7 +156,7 @@ class CombatAction:
self.combatant = combatant
self.uses = 0
def msg(self, message, broadcast=False):
def msg(self, message, broadcast=True):
"""
Convenience route to the combathandler msg-sender mechanism.
@ -520,9 +520,9 @@ class CombatActionBlock(CombatAction):
if is_success:
# managed to stop the target from fleeing/disengaging
self.combathandler.unflee(fleeing_target)
self.msg("$You() blocks the retreat of $You({fleeing_target.key})")
self.msg(f"$You() $conj(block) the retreat of $You({fleeing_target.key})")
else:
self.msg("$You({fleeing_target.key}) dodges away from you $You()!")
self.msg(f"$You({fleeing_target.key}) dodges away from you $You()!")
class CombatActionDoNothing(CombatAction):
@ -660,7 +660,7 @@ class EvAdventureCombatHandler(DefaultScript):
# start a timer to echo a warning to everyone 15 seconds before end of round
if self.interval >= 0:
# set -1 for unit tests
warning_time = 15
warning_time = 10
self._warn_time_task = delay(
self.interval - warning_time, self._warn_time, warning_time
)
@ -766,9 +766,9 @@ class EvAdventureCombatHandler(DefaultScript):
for ally in allies:
for enemy in defeated_enemies:
try:
ally.pre_loot(enemy)
enemy.get_loot(ally)
ally.post_loot(enemy)
if ally.pre_loot(enemy):
enemy.get_loot(ally)
ally.post_loot(enemy)
except Exception:
logger.log_trace()
self.stop_combat()
@ -844,7 +844,8 @@ class EvAdventureCombatHandler(DefaultScript):
if combatant in self.combatants:
self.combatants.remove(combatant)
self.combatant_actions.pop(combatant, None)
combatant.ndb._evmenu.close_menu()
if combatant.ndb._evmenu:
combatant.ndb._evmenu.close_menu()
del combatant.db.combathandler
def start_combat(self):
@ -867,6 +868,7 @@ class EvAdventureCombatHandler(DefaultScript):
"""
for combatant in self.combatants:
self.remove_combatant(combatant)
self.delete()
def get_enemy_targets(self, combatant, excluded=None, all_combatants=None):
"""
@ -1131,8 +1133,10 @@ def _select_target_helper(caller, raw_string, targets, **kwargs):
text = f"Select target for |w{action_key}|n."
# make the apply-self option always the first one, give it key 0
kwargs["action_target"] = caller
options = [{"key": "0", "desc": "(yourself)", "goto": (_register_action, kwargs)}]
if caller in targets:
targets.remove(caller)
kwargs["action_target"] = caller
options = [{"key": "0", "desc": "(yourself)", "goto": (_register_action, kwargs)}]
# filter out ourselves and then make options for everyone else
for inum, combatant in enumerate(targets):
kwargs["action_target"] = combatant
@ -1385,6 +1389,9 @@ def join_combat(caller, *targets, session=None):
if not location:
raise CombatFailure("Must have a location to start combat.")
if caller.hp <= 0:
raise CombatFailure("You can't start a fight in your current condition!")
if not getattr(location, "allow_combat", False):
raise CombatFailure("This is not the time and place for picking a fight.")
@ -1402,6 +1409,9 @@ def join_combat(caller, *targets, session=None):
# it's safe to add a combatant to the same combat more than once
combathandler.add_combatant(caller, session=session)
for target in targets:
if target.hp <= 0:
caller.msg(f"{target.get_display_name(caller)} is already out of it.")
continue
combathandler.add_combatant(target)
if created:

View file

@ -8,8 +8,9 @@ from evennia import DefaultCharacter
from evennia.typeclasses.attributes import AttributeProperty
from .characters import LivingMixin
from .enums import Ability
from .enums import Ability, WieldLocation
from .objects import WeaponEmptyHand
from .rules import dice
class EvAdventureNPC(LivingMixin, DefaultCharacter):
@ -114,6 +115,9 @@ class EvAdventureMob(EvAdventureNPC):
"""
# chance (%) that this enemy will loot you when defeating you
loot_chance = AttributeProperty(75)
def ai_combat_next_action(self, combathandler):
"""
Called to get the next action in combat.
@ -150,3 +154,55 @@ class EvAdventureMob(EvAdventureNPC):
"""
self.at_death()
def at_loot(self, looted):
"""
Called when mob gets to loot a PC.
"""
if dice.roll("1d100") > self.loot_chance:
# don't loot
return
if looted.coins:
# looter prefer coins
loot = dice.roll("1d20")
if looted.coins < loot:
self.location.msg_location(
"$You(looter) loots $You() for all coin!",
from_obj=looted,
mapping={"looter": self},
)
else:
self.location.msg_location(
"$You(looter) loots $You() for |y{loot}|n coins!",
from_obj=looted,
mapping={"looter": self},
)
elif hasattr(looted, "equipment"):
# go through backpack, first usable, then wieldable, wearable items
# and finally stuff wielded
stealable = looted.equipment.get_usable_objects_from_backpack()
if not stealable:
stealable = looted.equipment.get_wieldable_objects_from_backpack()
if not stealable:
stealable = looted.equipment.get_wearable_objects_from_backpack()
if not stealable:
stealable = [looted.equipment.slots[WieldLocation.SHIELD_HAND]]
if not stealable:
stealable = [looted.equipment.slots[WieldLocation.HEAD]]
if not stealable:
stealable = [looted.equipment.slots[WieldLocation.ARMOR]]
if not stealable:
stealable = [looted.equipment.slots[WieldLocation.WEAPON_HAND]]
if not stealable:
stealable = [looted.equipment.slots[WieldLocation.TWO_HANDS]]
stolen = looted.equipment.remove(choice(stealable))
stolen.location = self
self.location.msg_location(
"$You(looter) steals {stolen.key} from $You()!",
from_obj=looted,
mapping={"looter": self},
)

View file

@ -38,6 +38,7 @@ class EvAdventureQuest:
key = "basequest"
desc = "This is the base quest. It will just step through its steps immediately."
start_step = "start"
end_text = "This quest is completed!"
# help entries for quests
help_start = "You need to start first"
@ -49,6 +50,7 @@ class EvAdventureQuest:
self.questhandler = questhandler
self.current_step = start_step
self.completed = False
@property
def quester(self):
@ -59,17 +61,20 @@ class EvAdventureQuest:
Call this to end the quest.
"""
self.current_step
self.completed = True
def progress(self):
def progress(self, *args, **kwargs):
"""
This is called whenever the environment expects a quest may be complete.
This will determine which quest-step we are on, run check_<stepname>, and if it
succeeds, continue with complete_<stepname>
succeeds, continue with complete_<stepname>.
Args:
*args, **kwargs: Will be passed into the check/complete methods.
"""
if getattr(self, f"check_{self.current_step}")():
getattr(self, f"complete_{self.current_step}")()
if getattr(self, f"check_{self.current_step}")(*args, **kwargs):
getattr(self, f"complete_{self.current_step}")(*args, **kwargs)
def help(self):
"""
@ -93,7 +98,7 @@ class EvAdventureQuest:
# step methods
def check_start(self):
def check_start(self, *args, **kwargs):
"""
Check if the starting conditions are met.
@ -104,7 +109,7 @@ class EvAdventureQuest:
"""
return True
def complete_start(self):
def complete_start(self, *args, **kwargs):
"""
Completed start. This should change `.current_step` to the next step to complete
and call `self.progress()` just in case the next step is already completed too.
@ -114,10 +119,10 @@ class EvAdventureQuest:
self.current_step = "end"
self.progress()
def check_end(self):
def check_end(self, *args, **kwargs):
return True
def complete_end(self):
def complete_end(self, *args, **kwargs):
self.quester.msg("Quest complete!")
self.end_quest()

View file

@ -164,7 +164,11 @@ class EvAdventureRollEngine:
modtxt = f" + {modifier}" if modifier > 0 else f" - {abs(modifier)}"
qualtxt = f" ({quality.value}!)" if quality else ""
txt = f"{rolltxt}={dice_roll} + {bonus_type.value}{bontxt}{modtxt} -> |w{result}{qualtxt}|n"
txt = (
f"rolled {dice_roll} on {rolltxt} "
f"+ {bonus_type.value}{bontxt}{modtxt} vs "
f"{target} -> |w{result}{qualtxt}|n"
)
return (dice_roll + bonus + modifier) > target, quality, txt

View file

@ -195,7 +195,8 @@ class EvAdventureTurnbasedCombatActionTest(EvAdventureMixin, BaseEvenniaTest):
mock_randint.return_value = 11 # 11 + 1 str will hit beat armor 11
self._run_action(combat_turnbased.CombatActionAttack, self.target)
self.assertEqual(self.target.hp, -7)
self.assertTrue(self.target not in self.combathandler.combatants)
# after this the combat is over
self.assertIsNone(self.combathandler.pk)
@patch("evennia.contrib.tutorials.evadventure.combat_turnbased.rules.randint")
def test_stunt_fail(self, mock_randint):
@ -293,7 +294,7 @@ class EvAdventureTurnbasedCombatActionTest(EvAdventureMixin, BaseEvenniaTest):
# second flee should remove combatant
self._run_action(combat_turnbased.CombatActionFlee, None)
self.assertTrue(self.combatant not in self.combathandler.combatants)
self.assertIsNone(self.combathandler.pk)
@patch("evennia.contrib.tutorials.evadventure.combat_turnbased.rules.randint")
def test_flee__blocked(self, mock_randint):

View file

@ -6,38 +6,38 @@ They provide some useful string and conversion methods that might
be of use when designing your own game.
"""
import os
import gc
import sys
import types
import math
import threading
import re
import textwrap
import random
import inspect
import traceback
import importlib
import importlib.util
import importlib.machinery
import importlib.util
import inspect
import math
import os
import random
import re
import sys
import textwrap
import threading
import traceback
import types
from ast import literal_eval
from simpleeval import simple_eval
from unicodedata import east_asian_width
from twisted.internet.task import deferLater
from twisted.internet.defer import returnValue # noqa - used as import target
from twisted.internet import threads, reactor
from collections import OrderedDict, defaultdict
from inspect import getmembers, getmodule, getmro, ismodule, trace
from os.path import join as osjoin
from inspect import ismodule, trace, getmembers, getmodule, getmro
from collections import defaultdict, OrderedDict
from unicodedata import east_asian_width
from django.apps import apps
from django.conf import settings
from django.core.exceptions import ValidationError as DjangoValidationError
from django.core.validators import validate_email as django_validate_email
from django.utils import timezone
from django.utils.html import strip_tags
from django.utils.translation import gettext as _
from django.apps import apps
from django.core.validators import validate_email as django_validate_email
from django.core.exceptions import ValidationError as DjangoValidationError
from evennia.utils import logger
from simpleeval import simple_eval
from twisted.internet import reactor, threads
from twisted.internet.defer import returnValue # noqa - used as import target
from twisted.internet.task import deferLater
_MULTIMATCH_TEMPLATE = settings.SEARCH_MULTIMATCH_TEMPLATE
_EVENNIA_DIR = settings.EVENNIA_DIR
@ -2714,10 +2714,24 @@ def run_in_main_thread(function_or_method, *args, **kwargs):
return threads.blockingCallFromThread(reactor, function_or_method, *args, **kwargs)
_INT2STR_MAP_NOUN = {0: "no", 1: "one", 2: "two", 3: "three", 4: "four", 5: "five", 6: "six",
7: "seven", 8: "eight", 9: "nine", 10: "ten", 11: "eleven", 12: "twelve"}
_INT2STR_MAP_NOUN = {
0: "no",
1: "one",
2: "two",
3: "three",
4: "four",
5: "five",
6: "six",
7: "seven",
8: "eight",
9: "nine",
10: "ten",
11: "eleven",
12: "twelve",
}
_INT2STR_MAP_ADJ = {1: "1st", 2: "2nd", 3: "3rd"} # rest is Xth.
def int2str(self, number, adjective=False):
"""
Convert a number to an English string for better display; so 1 -> one, 2 -> two etc