January 30, 2010

Character Stats

I have finally settled on a set of character stats to use for the game. I've been torn over using a generic (some may say tired) but immediately recognizable D&D inspired set like STR, DEX, INT, WIS, CON, CHR versus making up completely different ones with the risk they'd seem needlessly exotic and labored.

I wanted to split them evenly between physical and mental attributes with as little overlap as possible. For example, I've always found dexterity and agility awkwardly close to each other.


Physical
Stats
  • Brawn - strength, heartiness, and balance.
  • Vigor - stamina, pain tolerance, and body recuperation rate.
  • Precision - dexterity, grace, accuracy, and physical reaction time.

Mental Stats
  • Knowledge - memory, education, and life experience (street smarts).
  • Faith - confidence, willpower, zealotry, and mental recuperation rate.
  • Cunning - intelligence, ingenuity, and mental reaction time.
An even split meant either 4, 6, or 8 stats -- 4 was too few and 8 was too many. We want different character guilds/classes to prize different stat combinations without things getting out of hand. Let's also try to make mental attributes appealing to melee classes. Monks should want faith and rogues cunning. Faith might seem like an odd word but it will fit in the setting I'm planning. Lastly, let's drop the wisdom versus intelligence caster meme.


Application

For every 10 points in a given stat, the character will receive a 1% bonus towards related skills. In the case of negative stats, characters will suffer a 1% penalty per negative stat point.

January 27, 2010

Key-Value Data Loading

I wanted to follow up with an example of loading an avatar's persistent dictionaries from a key-value store in the SQLite database;


def load_account(client, name, uuid):
"""
load a previously created account.
"""
client.send('\nWelcome back, %s. Your last visit was %s.\n\n' %
(name, last_on(name)))
## Create an in-game Avatar from stored data
avatar = Avatar(client)
## Read from database
avatar.profile = fetch_kv_dict(uuid, 'profile')
avatar.resources = fetch_kv_dict(uuid, 'resources')
avatar.skills = fetch_kv_dict(uuid, 'skills')
avatar.worn = fetch_kv_dict(uuid, 'worn')
avatar.carried = fetch_kv_dict(uuid, 'carried')
avatar.abilities.update(fetch_kv_set(uuid, 'abilities'))
play_account(avatar)


Just in case it wasn't clear. You may notice an absence of a 'stats' dictionary. Character stats will be computed based on race + gear, so aren't saved.

Persistence

I've had a few questions about how I plan to handle persistence, as in saving the states of game objects in case the server is restarted. The quick answer is I'm not -- with the exception of player character data. Bogboa is using two forms of data storage; YAML files and an SQLite database. I'll discuss each.


YAML Files

Currently, YAML is the only third-party library used in Bogboa. I didn't want to use any really, but YAML is such a human friendly format that I went with it.

On startup, the world is created from one or more directory trees filled with YAML files that define individual rooms, items, races, etc. This is a one-way conversion where each YAML file is parsed into a Python dictionary that is used to set attributes of game classes. One of my long-term goals is to support reading these YAML trees from zip files so that designers could release adventure modules that contain complete dungeons with new monsters and items that load seamlessly into existing game worlds. This is one of the reasons I'm using UUID's for keys, since they don't require multiple developers to coordinate or negotiate around potential collisions like vnums do.

So restarting the server cleans every floor of dropped loot and every dead NPC is alive again. This appeals to me because it's simple and troubleshooting scripts is easier from known states.


SQLite Database

Starting in Python 2.5, SQLite3 is part of the standard library (and a kick-ass little SQL database) which makes using it a non-brainer.

My early tables were pretty vanilla with columns; (name, race, level, guild, stat1, stat2, etc) and I was always changing them. Also, I was using a lot of sub-tables to hold information like skills, inventory, and character flags. These sub-tables all had nearly identical structure; (character_uuid, skill_name) and (character_uuid, inventory_slot).

At the same time, I was making structural changes to how I split player from their avatars (which is why the codebase is currently b0rked). I knew that the closer I handled avatars like NPCs the easier things would be down the road. Moving all the various traits into dictionaries was helpful and this transition to key-value thinking migrated to how I stored character data as well.

What I wanted was a way to load whole dictionaries at a time but update individual key-value pairs on the fly. When you log in, your character's profile dictionary is retrieved containing (name, race, guild, level, ...) and when you gain a level, your characters (profile + level) record is incremented by one and if the server crashes a moment later you, hopefully, didn't lose it.

One of the nuisances of SQL is you often have to do a SELECT before you can save data because sometimes you cannot be sure if you need to use the INSERT or the UPDATE command. SQLite supports a non-standard but convenient variation of this called INSERT OR UPDATE. The catch is the key has to be unique.

What I did was make the key = (Character UUID + Some Catergory Name + Key Name);

CREATE TABLE IF NOT EXISTS key_value
(
uuid TEXT,
category TEXT,
key TEXT,
value TEXT,
PRIMARY KEY(uuid, category, key)
);


In the above example, the category name would be 'profile'.

Reading all the rows of a category would look like;

SELECT key, value
FROM key_value
WHERE uuid = ? AND category = ?;



And saving an individual key-value would look like;

INSERT OR REPLACE INTO key_value (uuid, category, key, value)
VALUES (?, ?, ?, ?);

January 16, 2010

Everything I need to know I learned from D&D

Clever presentation by a guy who compares software development to playing Dungeons and Dragons:

All I Need To Know About Life I learned From Dungeons and Dragons. An IgniteOKC Talk. from Chad Henderson on Vimeo.

January 7, 2010

MUD Game Programming

I believe the only book ever written as howto for creating a MUD was MUD Game Programming by Ron Pelton. Originally published in late 2003, it's been out of print for a while and I finally managed to snag a used copy for a reasonable price. It even came with an unopened CD.

I don't expect to glean a great deal from it having already written an asynchronous server, plus Pelton is using C++ where I'm doing everything in Python. Still, it seems like a quality book with a lot of example code. Books on game programming tend to suffer a poor signal to noise ratio. There's a ton of dreck out there, especially garbage written by industry insiders who have no clue about programming but feel like they can help you with their experience interviewing other non-programmers. Ugh. Even worse are the ones aimed at teens -- clearly targeting the well-intentioned but useless gift from Mom category.

Pardon the rant.

Anyways, I've read the first couple chapters on networking and socket programming and Pelton is delivering the goods. He is specific and writes in an friendly, engaging manner. He gets extra points for covering Linux in addition to Windows. Recommended (if you can find it).

January 2, 2010

Work Around

--start of edit

Well, it turned out that the approach listed below does not find methods from base classes. I revised it to use __getattribute__() instead which returns a bound instance of the class but that only lives for the scope of the cmd_driver() method:


def cmd_driver(self):
## call the driver method for current state
self.__getattribute__(self.state)()
---end of edit


My last post detailed a garbage collection issue I was having when an instance of the User class referenced itself using a bound variable. I got around it changing my state property to a string variable holding the name of the method I wanted to call and then calling said method via some class introspection;
def cmd_driver(self):
## call the driver method for current state via class introspection
self.__class__.__dict__[self.state](self)


That kind of introspection always feels like duct tape.

I should point out that Python's garbage collector probably would have deleted those User instances eventually and that I could have manually ended the connection by calling socket.close(). I prefer the immediate garbage collection closure because it's an indicator that everything is running smoothly.