February 4, 2009

Days and Nights

My first trip through Kithicor Forrest when like so:

"A Zombie Trooper begins to cast a spell."
"You have been slain by a Zombie Trooper."

Here I learned the lesson of not trying to pass through Kithicor Forrest at night. I would have to wait 12 hours for daylight (or 36 actual minutes). Kithicor is a zone in the online game Everquest. Days in Everquest are 72 real-world minutes long.

I've been thinking about how to handle the passage of time. I knew that I wanted days to be much shorter than real time. World of Warcraft uses actual time. As a player, I found EQ's system much more engaging. It would actually get dark, vendors went inside their homes, werewolves came out, and Kithicor Forrest filled with $*@#& zombie troopers.

WoW just lowers the gamma a touch. Boring. My guess is Blizzard chose real time to simplify the scheduling of in-world events with real-world holidays. The downside is you can't ask players to wait eight hours while an NPC goes to bed so vendors never sleep.

I decided to go with Real Time x 12 where a game day equals two real hours. That gives us an hour of daylight followed by an hour of night for sneaky rogue-style stuff.

I also wanted to (roughly) align game years to real months. I decided to junk game months entirely since they were only about two and half actual days long. You've probably seen the Chinese Calendar where they cycle through years named after animals. That's what I wanted. Named years that cycle as real months pass. You get an illusion of years passing contained within an endless, MUD friendly cycle.

Now, how to implement it?

Some games use a tick counter that tracks time but my design isn't based around game ticks -- plus I want time to pass even if the server is taken offline. Worse, I really don't want it to revert to 00:00:00 whenever I restart it like an unplugged VCR.

My scheduler was already polling time.time() which returns the number of seconds since Jan 1, 1970. Since this is an imaginary world, I can get away with a few things. Real years are about 365 and 1/4 days long, hence the need for leap days. If I use a solar year instead of a calendar year I can forget about leap days.

## Number of seconds in a solar year
SOLAR_YEAR = 31556925.215999998

To align the first game year I decided to define a mini-epoch of January 1st, 2009. Consulting the UNIX timestamp generator gave me:

## UNIX_ADJ is used to align the start of game time with Jan 1, 2009 00:00 GMT.
UNIX_ADJ = 1230768000.0

Next, I defined a game cycle that gave me a spot to speed up time in case I want to test things:

## GAME_CYCLE describes the relation of twelve game years to one real year.
## If you want to speed up or slow down time change the divisor.
GAME_CYCLE = SOLAR_YEAR / 1.0

Now, let's define some constants for computing relative days. As mentioned above, we probably wont use GAME_MONTH, GAME_DAY, or GAME_SECOND for anything. Instead, we'll describe the date in Julian terms like 'Day 240 of the Year of the Ominous Sounding Noun'. In our perfect orbit, game years are 360 game days long.

GAME_YEAR = GAME_CYCLE / 12.0
GAME_MONTH = GAME_YEAR / 12.0
GAME_DAY = GAME_MONTH / 30.0
GAME_JULIAN = GAME_YEAR / 360.0
GAME_HOUR = GAME_DAY / 24.0
GAME_MINUTE = GAME_HOUR / 60.0
GAME_SECOND = GAME_MINUTE / 60.0

The calculations are pretty simple. We can define CENTURY_OFFSET to tweak the displayed century in case you wanted the game to begin in 1100 or Stardate 26,301.

tstamp = time.time() - UNIX_ADJ
minute = int((tstamp % GAME_HOUR) / GAME_MINUTE)
hour = int((tstamp % GAME_DAY) / GAME_HOUR)
day = int((tstamp % GAME_MONTH) / GAME_DAY)
julian = int((tstamp % GAME_YEAR) / GAME_JULIAN) + 1
month = int((tstamp % GAME_YEAR) / GAME_MONTH)
numbered_year = int(tstamp / GAME_YEAR) + CENTURY_OFFSET
named_year= numbered_year % 12

Finally, we can also compute the current amount of sunlight for outdoor areas:

def sunlight(self):
"""
Calculate the current sunlight level.
Return an integer value in the range of 0 (Midnight) to 12 (Noon).
"""
tstamp = THE_TIME - UNIX_ADJ
hour = int((tstamp % GAME_DAY) / GAME_HOUR)
return int(12 - abs(12 - hour))

September 21, 2008

YAML

One of my goals was to have zero dependencies -- where all you would need is a recent released of Python to run the MUD.

I think I'm going to add one though - PyYAML. Because of how I'm doing object properties, I need to let designers script events for rooms, objects, and monsters. Which means a lot of text editing. Early on, I was considering XML for this, but it's a painful format to edit manually. YAML on the other hand, is a very tight, clean format meant for straight editing.

September 20, 2008

Subversion and Junk Files

BogBoa is hosted on GoogleCode.com which uses Subversion for version control. I really like Subversion but often you need to tell it to ignore junk files like *.pyc (compiled Python programs) and *.aux (LaTex leftovers).

You can tell Subversion to ignore specific types of files by editing .subversion/config in your home directory and adding a line like:

global-ignores = *.pyc *.log *.out *.toc *.aux *.sqlite *.pdf


September 19, 2008

Test from Email

This is a test message sent from email.

January 29, 2008

Signs, Portents, and Little White Stickers

Remember that scene in Time Bandits when the giant creator's head was chasing them shouting, "RETURN WHAT YOU HAVE STOLEN"? Now picture the frozen noggin of Walt Disney saying it in the voice of a certain immortal/undead rodent.

I really like getting used books from Amazon. You can save a lot off the retail price and, so far, I've had great luck with regards to the condition of the books.

Last week I ordered Designing Virtual Worlds by Richard A. Bartle. Mr. Bartle famously co-wrote the first MUD. It arrived yesterday and I saw this sticker on the contents page:Interesting, eh? I sent Mr. Bartle an email and some scans which he added to his blog. He was even kind enough not to mention what a cheap bastard I was for buying it used.

I just hit chapter two and, so far, it is an excellent and entertaining read -- especially for someone like me that grew up on text adventure games and then bounced around MMORPG's.

January 26, 2008

"You are in a maze of twisty little passages, all alike"

When I was a kid, a pack of us used to play Dungeons & Dragons and I was often coerced into the role of Dungeon Master. Yes, it was as nerdy as it sounds but hey, it was fun and really, really cheap entertainment. One adventure I ran my victims, err ... cohorts through required them jumping blindly into portals to go from the current section into the next (really, it was just a way to buy time as I made up the next part). Plus, I could make up anything without needing it be consistent with the previous setting.

They hated the portals.

They were right to. It was a cheap and crummy design.

I'm fleshing out the geography system for the MUD, e.g. moving from one location to another. Historically, these are called 'rooms' with simplistic, direction based linking between them. You're in 'The Dining Hall' and go 'north' to be in the 'The Kitchen'. The simplicity of this design lends itself to a text based MUD. I could sweat area dimensions and the coordinates of players, creatures, and objects but how do I convey that to a player in a meaningful way using words alone? If you have to kill the goblin king to get his scepter, do you really care what bearing and how many meters away he is?

One concern I had was dropping players into a lady and the tiger scenario where they type 'east' and suddenly find themselves in certain death. The answer to this is the same as it is in books. Foreshadowing. Alan Quartermain doesn't sail his canoe smack into the headhunters' camp. He finds skulls on sticks, burned settlements, gross nasty things that all say, 'turn the hell back, Dude.'

In other words, it's not a programming problem.

January 20, 2008

Parsing Commands

This is a topic I thought about for a long time. My family thought I was just slacking about like a bum but I was really thinking. The challenge is converting what the player types into function calls -- only some function calls need more information than others and in differing order. I started with a huge, ugly IF-THEN sequence but quickly hated it. Plus, it wasn't very clean to expand.

I knew I wanted a dictionary lookup that would map verbs to functions and I wanted flexible control over which verbs different players could use -- 'equip sword' vs. 'cast fireball at Chadu'. The only common theme is every command begins with a verb.

I think my solution is both elegant and functional. I changed the dictionary to map verbs to functions and parsers. So the verb "tell" uses parser.dialogue (extract the recipient and the message) and "shout" uses parser.monoloque (no target, everything is the message).

Here's a snippet of where the Player class processes a line of input:
cmd = self.get_cmd()
verb, words = split_verb(cmd)

if self.has_ability(verb):
parser, function = shared.ABILITY_DICT[verb]
args = parser(words)
function(self, *args)


Notice the call to self.has_ability? We can grant and revoke abilities easily.