|
My brain is built of paths and slides and ladders and lasers and I have invited all of you to enter its pavilion. My brain, as you enter, will smell of tangerines and brand-new running shoes.
 Team management... yayy! |
Posted - 3/28/2008 5:25:50 AM | Every team eventually faces a fundamentally difficult question: who is in charge?
Someone must take responsibility for the game's overall direction. If there are multiple programmers, someone needs to set up some technical standards and practices, handle decisions like what libraries and tools to use, and so on. Artists, sound engineers, musicians, and even managers themselves face similar challenges.
So how do we know who to put in charge of a team? There's a few different options. Today I'm going to look specifically at technical/programming leads, because it's been on my mind a lot lately. (Plus, as a bonus, that's the area I'm most familiar with - meaning I have a better chance of sounding competent.)
First, let's introduce the players in today's little drama.
Leadership by Seniority
This is a fairly easy choice: the programmer who has been around the longest gets the job. The unspoken assumption is that this person will have the most experience and therefore the greatest pool of wisdom to draw upon when making decisions.
Leadership by Inertia
A common place to see this is in dying, overly bureaucratic companies where the culture places a huge emphasis on things like status, org charts, and so on. Essentially, someone got made a tech lead once for some reason, and that became his title. With that, he's been a tech lead ever since. Shuffling the org chart to give him a more appropriate position is too much of a pain, so he stays in the position, usually regardless of ability or genuine qualification.
Leadership by Committee
Dangerous dragons lurk here - in this variant, there isn't actually a team lead; everyone pitches in their opinions and votes on decisions. Although the democratic veneer may be appealing, this is rarely a good idea, for reasons we will explore shortly.
Leadership by Cabal
This is a lot like leadership by committee, except the people making all the decisions aren't officially in any position of power. They just sort of form a de facto cabinet that runs the kingdom. It is common for this form to arise when the official tech lead is especially incompetent.
Leadership by Qualification
This is the gold standard, the one we all wish we could see, but hardly ever do. This is like winning the career lottery - lots of people try, very few succeed. In short, leadership by qualification is when you get a tech lead who is a really brilliant manager, and handles his people exceedingly well.
Wait a minute... did you say good manager?
Now for the punch line: a good tech lead's technical skills are a distant secondary concern.
Yes, he's going to be working with programmers. Yes, he needs to know how to evaluate libraries, toolkits, and so on. Yes, he needs to understand the architecture of the game and even possibly build parts of it himself.
But when it comes to picking a great tech lead, you don't want to pick the best programmer in the house. It's tempting and may seem obvious, but it's a mistake.
First of all, you want your best programmers doing what they do best - programming. Managing a team takes time and effort, and that time and energy is limited - it can't be spent on programming and management simultaneously.
Secondly, being a brilliant programmer has no bearing whatsoever on one's management skills.
The Shootout: How to pick your tech lead
Leadership by seniority is a bad option: it ignores the important point that programming is not management. I personally have 16 years of programming experience, and zero years of management experience. If I were to enter a shop where everyone else had, say, 10 or fewer years of programming experience, that in no way means I'm the best candidate for technical lead. I'd rather work for a tech lead with 5 years of programming and 10 years of management than vice versa.
Inertia is also a bad option, for hopefully obvious reasons. If your place of employ has a track record of leaving people in poorly fitting positions for long periods of time, it's time for you to change your organization or change your organization.
So we've knocked down two options... what about a committee? That way, we can get input from all the management-experienced people, and all of the programmers at the same time! Perfect!
Well, except for all the flaws. Committees mean meetings, and having a meeting every time a decision needs to be made is wasteful. Secondly, it introduces complex dependency chains - every time work needs to be done, people must go through the "tech decision committee". It will inevitably take longer to get a decision from a group of people than from a single point of authority, which slows down your entire production process.
Committees also have a tendency to settle towards least-common-denominator results. If everyone is on equal footing when discussing a decision, then it can be hard if not impossible to push for improved practices or decisions.
Consider a poor schmuck named Joe who is a really great software designer. Joe is stuck on a team with no single lead - all decisions are made by a committee made up of Joe himself and four other programmers. Now Joe is passionate about his work - he reads constantly, does outside projects to improve his skills, and is always concerned about producing maximum-quality work. His coworkers, on the other hand, are just there to punch the clock and cash the paycheck.
Suppose Joe wants to change from waterfall to a more agile development approach. He has four other voices to overcome on the committee. Who do you think is going to win that discussion? Joe's the new guy - he can't convince the old-timers that there is a better way. Even if he gets one or two, they're likely to compromise with the unconvinced committee members, just to get the decision made faster.
So committees aren't really a good choice.
Cabal leadership isn't any good, either. By nature, tech lead cabals are not formed by official sanction; insead, they arise out of necessity in a dysfunctional environment. A good way to recognize the existence of a tech lead cabal is to observe the difference between what the official lead says and what actually gets done.
If the real results are worse, there's no cabal - just incompetence all around. If the results are even par, the cabal is either wasting their time with inflated egos, or doesn't exist; more than likely everyone is just toeing the line and secretly wishing they had a cabal. However, if your tech lead is making specs and decisions which are quietly overturned for better options, you can bet you have a good infestation of cabal leadership going on.
It can be hard to fight the tendency to form cabals, especially if you're a member. Well, Bob the tech lead isn't going to give in anyways, so let's say screw it and just go do our own thing. Every month or so we'll straighten up for a while, just to keep Bob content, and never push him over the edge to where he makes us stop.
However, this is a toxic way to work. Anyone on the team who isn't aware of the cabal, or doesn't follow their leadership, is going to introduce complications. Some code will be good, other bits poor. Standards will be broken and conventions applied inconsistently. Decisions will be haphazard and lacking a unified focus.
Worse, cabals tend to engender bitterness and resentment. Ironically, some of the most bitter people can be found at the top of the cabal's hierarchy, where people are under the greatest pressure to both try and keep the project afloat, and not step on the toes of the "official" leadership.
Last Man Standing
So we've had our shootout and pretty much decided that all forms of management suck. Now, don't break out your Anarchy bumper stickers just yet - there's one method left that works pretty well: let the most qualified guy be the technical lead.
You would think this would be an obvious choice, but it rarely is. For one thing, defining who is most qualified can be a serious challenge in and of itself. Then there's the matter of displacing someone in favor of the new candidate; this can easily spell disaster if not handled carefully and with great consideration for the opinions and feelings of everyone concerned.
There's a billion articles written already about what makes a good technical manager, but here's my personal checklist:
- Up to date on technology and industry best practices
- Competent enough with programming to know how to resolve disagreements over how to implement things
- Familiar with every aspect of the code base
- Capable of getting people to do what he wants, like adhere to standards and practices
- Strong communication skills - nobody can be a great lead without talking to people, constantly and in depth
- Attention to detail
- Uncompromisingly dedicated to quality - never willing to let "little things" slide if they undermine code quality and/or maintainability
- Highly organized - can keep track of all the details that arise in any nontrivial development project
- Cares about the success of the team
Push for the best leadership you can get. You owe it to yourself and the rest of your team. Just be gentle - leadership roles are (unfortunately) often tangled up with egos. Be sure you approach the matter from the perspective of what is best for the team as a whole. Anyone who prizes his own ego or status above the success of the team probably needs a pink slip anyways.
If you don't already have a single, nominated leader, gather up a meeting of your entire technical staff and nominate someone. If you have a tech lead above you, make sure you kindly suggest things for them to do to help their leadership style. Never go over anyone's head if you can avoid it; don't go up two layers on the org chart and complain about your boss.
Lastly, if you're in a position to have a tech lead below you, arrange a meeting with the lead himself and all of the staff he supervises. Encourage everyone to air their concerns and suggestions.
Then be sure to act on them.
Change is not a four letter word
The final idea I'd like to leave you with here is that change is not a bad thing. Changing your tech lead's habits for the better is always a good thing. Even changing the person filling the role can be beneficial.
Never fear pushing for change, even if you're on the bottom of the hierarchy. So long as you can argue successfully that your suggestions are for the greater good, you will be fine. If you get fired for trying to improve your workplace, then chances are you're better off some place else.
For those higher up on the chart, remember that your job is to keep the lower levels of the team hierarchy working smoothly. Getting overly set in your ways is dangerous, especially in a business that moves as fast as game technology. Be open to fresh new ideas - being open just means willing to listen and act, not necessarily that you have to take on every single suggestion hurled your way.
The biggest problem that businesses can have is adapting too slowly. The rest of the universe is changing, with or without you. If you don't change along with it, you'd better be ready for extinction.
| |
 And now, something about work |
Posted - 3/14/2008 1:49:14 AM | Game Programming and Krystal Burgers1
Since something interesting is going on at work, I thought I'd share. Well, interesting and something I can discuss publicly, which is a rare combo.
The last couple of days have been spent in an intense bug hunting frenzy. Earlier this week, I was working on a small demo for an upcoming milestone, when a couple of memory bugs cropped up. What started out as a memory leak became a nasty chase through a convoluted inheritance hierarchy, dozens of misapplied templates, and some truly backwards code design.
Ironically, just at the end of last week, I'd signed off on that code as more or less usable, which is why I was doing a demo in the first place - I had vouched for the technology as solid enough to be showing off, albeit in a rudimentary form.
Apoch's n++th Law of Software Development:
You cannot truly judge the quality of a block of code until you must locate a bug in it.
Corollary
The longer you use a piece of code without having to hunt down a bug in it, the closer you can come to saying the code works correctly. However, this approach is asymptotic - you will never get there.
Fixing the Bugs
After a few hours of analysis, I determined that salvaging the code was out of the question. It would take less time and effort to rearchitect it and then copy/paste the functionality back in.
So that's what I've been doing lately. There's a huge chunk of work finished and ready for a version control commit, but a few major elements are still missing. As of right now there's actually less functionality working than when I set out to build the demo, so... bummer, I guess. That's the way things go.
Postmortem: Dissecting the Insects
So how did this situation come about in the first place? It's a simple and sadly common problem.
About a year ago, we had a newcomer to the team, who was interested in working on the engine and doing some game logic stuff. This particular technology was already written in another scripting language for an old engine, and needed to be ported to C++ for future use. The demand was minimal, so he was under no time pressure, and it seemed like a good project for a beginner.
As it turns out, the entire thing suffered badly as a result. He made a valiant effort, to be fair, but the result just wasn't up to par. The original programmer of the technology did some work to make it a bit better, but by and large it was a low-priority item, and thus languished in disrepair for months.
Worse still, the new guy disappeared after a few weeks, and nobody seems to know what happened - or, if they do, they're keeping quiet. It's unfortunate from a production point of view that a lot of time and code was wasted, but it's worse that a potential team member who had been trained (partially) on our workflow pipeline has been lost.
The Newbie Problem
This introduces a difficult question. Let's say we have a team of programmers, all of whom have an average skill level of X. Then a new guy joins up, with a skill level of Y, where Y is significantly lower than X.
It seems at first glance that no matter what J. Random Newbie does, the team suffers as a result. If he works on critical technology, chances are it will fail because it isn't up to the team's standards. If he works on something low priority, chances are it will crop up as a hidden cost when the item becomes high priority at some point in the future - and these surprises can bite you hard.
My personal opinion is that the only way to really do this is to let Mr. Newbie observe for a while, almost like a pair programming arrangement. Assign him to rotate between existing coders, and have them explain what they do, how and why they make their decisions, and so on.
It may seem like an annoyance and a drag on the veterans, but in my estimation, it will pay off immensely, for three reasons. First, the veteran's work is subjected to scrutiny - both his own and J. Random's - which is likely to increase its quality. Secondly, chances are the veteran would have to give up some time to slow his pace down and meet Mr. Newbie on his own level anyways; for example, if Newbie does Random Module Z, Veteran will have to integrate with Z at some point, and likely discover all kinds of things that need to be fixed or improved.
The third point, though, is the one I feel is most important: mentoring newbies in this manner creates veterans much faster than the old "sink or swim" method. It reduces turnover. It decreases stress, because everyone expects the training to be going on - it's all too easy for veterans to just expect the new guys to be up to speed, and get frustrated when that is not the case. Last, but definitely not least, it raises the overall quality of your code.
I'm not really in a position to institute this kind of policy at Egosoft, nor do we currently really have anyone who could realistically be put into a apprentice-type position; but next time the situation arises, I'm definitely going to be putting forward an argument for it.
Bipolar Update
I've worked almost solid since 6:30 this morning until now (about 1:30 AM), minus a couple of hours for meals and a brief nap. It's starting to take its toll on my motivation, but not my mental energy. That's a really good sign.
This is the first really strenuous push I've made at work in a very long time. It's refreshing to be back at a point where I can sprint when I need to. Time will tell if I end up crashing and burning afterwards, but so far none of the signs of a typical manic buildup/crash are manifesting. Of course, that might just be because I'm on so many drugs that all the symptoms are utterly masked.
In any case, it looks like I'm finally coming out of the long tunnel of bipolar disorder and starting to regain some semblance of normal functional life. Just don't judge my state of dysfunction by looking at my apartment - it's a wreck.
1 - Yes, Krystals. We don't get the Holy White Castle here in Atlanta, and although Krystal is a very distant - nigh-on-blasphemous - second place, it suffices to satisfy the occasional urgent crave. This is especially true during crunch time.
| |
 Ein Doppelpost |
Posted - 3/10/2008 6:27:57 PM | I goofed around a little bit on Fugue today and ended up with a nice new extension. Rather than atomic operations like addition/subtraction/etc. having side effects, they are now simply r-values, and in order for the program to do anything, you have to invoke an assignment operator. So now the unit tests look like this:
using namespace VM::Operations;
Assign(scope, L"baz", MultiplyIntegralVariables(scope, L"foo", L"bar")).Execute();
Assign(scope, L"quux", MultiplyIntegralVariableConstant(scope, L"baz", 2)).Execute();
This paves the way for more richness in the VM, since it is now able to chain operations and do other nifty things with deferred/lazy evaluation. It's also pretty minimal in terms of overhead in the VM side, but that's still not something I'm terribly concerned about - once an Epoch compiler is written for the Fugue system, the interpreter's overhead becomes irrelevant. Since the interpreter is largely just for bootstrapping and experimentation purposes, it can afford to chew a few extra bytes here and there.
The next thing I want to do is allow simple debug output operations, so that I can add control flow structures and see them working. Once control flow is in place, the next phase is getting richer data type support in place (right now everything is an integer or boolean). Specifically, I want support for lists and multidimensional arrays. That will make it possible to sit down and code the Sudoku solver, which is my first "hey look the language actually does something" app.
I haven't decided if I'll write the solver in the same hacked style as the unit tests (i.e. by calling the VM directly to construct expressions and statements) or if I'll go ahead and do the parser in there at some point. I'm leaning towards putting off the parser for as long as possible, though. Maybe some intermediate hack involving C++ macros... hmm...
Anyways, forward progress on Epoch. Woot.
| |
 GDC Postmortem |
Posted - 3/10/2008 10:17:37 AM | My GDC coverage is all wrapped up, so at Drew's suggestion I'm doing a quick postmortem of the entire GDC experience. I know it's a couple weeks late, but I've been focusing on higher priority stuff lately since I technically wasn't there for press coverage in the first place 
Good Things
- Overall I really enjoyed the show. There was a lot of really great content - maybe too much - and got to see all but one of the sessions that I really wanted to get into. Even that one I managed to catch the tail end of.
- Having lots of contacts from GDNet helped open a few doors. I got to hang out in the press lounge a couple times because I could walk in "for a meeting with GDNet" - which also meant free access to the exclusive press-only wireless, which was a lot smoother (and more secure) than the open conference WLAN.
- Being bold with networking works well. I talked to several people about Epoch and worked my way into the annual AI programmers' dinner just by being willing to fight through the crowd and say something relevant. I think a lot of the "luminary" types appreciate it when someone comes up to them to truly talk shop rather than just bask in their glow, so to speak. I know I would in their position.
- I scheduled extra days on either end of the show. It was really important I think to help get acclimated to the area beforehand, and decompress a bit before dealing with the trip home afterwards. Plus I got to play tourist for a day, which was good clean fun. I will definitely be doing this again for future GDCs.
- I aggressively scheduled the entire week ahead of time, leaving slots to go explore the expo hall, time to just relax, and so on. I knew all of my sessions and had a complete plan for where I had to be at any given time of the day, all in my Palm. This proved to be extremely useful since by the end of the week I didn't have the mental energy to deal with such things; I could just rely on my plan and be comfortable knowing I'd end up in the right place.
Bad Things
- My laptop battery had no life left in it, and wouldn't hold a charge. This meant I was chained to wall sockets if I wanted to take notes or hit the internet. Wall sockets are a very, very valuable resource at GDC, so I didn't get to use my laptop very much. Most of the time it was just dead weight I had to lug around (17" notebooks aren't exactly portable). Thankfully, there was plenty of free note paper and pens available, so I got all my note-taking done anyways - just with a very sore hand afterwards.
- The weather was total crap. I hear next year will be in March, which should hopefully alleviate some of that. Maybe.
- I stayed at a hotel (the Westin St. Francis, for those who may attend in the future) which offered no free wireless. In the year 2008, when the hobos on the street offer free wireless access from their little fire barrels, it is absolutely idiotic that a nice hotel demands money to get on the internet. Screw the Westin. Next year I'll pick a better hotel, preferably the same one where everyone else is staying, so that we can party and such more effectively.
- I didn't budget very carefully and nearly killed my checking account. Next year I need to prepare a much better budget and make sure I have transferred enough money from savings into checking before making the trip. I'm too paranoid to use my online banking to do this on the road, so it needs to be done ahead of time.
- My voice recorder (just a cheap Sansa thing) didn't handle much of anything real well. It works fine for journalism-style recording where you talk directly at the little tiny mic, but for trying to record a session in a large room, it just didn't cut it. I might look at the settings to see if I can reconfigure it and try again, but overall, I was better served by just writing fast.
Conclusions
I'm definitely going back next year if I can. The experience was great and highly productive. Most everything went about as smoothly as can be expected, so I don't have any major plans for change for next time around.
Now I just have to find the patience to wait a whole bloody year for the next one 
| |
 More GDC coverage |
Posted - 3/8/2008 3:48:31 PM | I've posted four new GDC articles - go forth and enjoy.
| |
 And now, the moment you've all been waiting for |
Posted - 3/7/2008 9:15:59 AM | In my spare hours here and there over the past week, I've started doing a little something which I hope will be of interest to the larger GDNet community. It's been a long time since the Epoch language project thread hurdled its way up to one of the longest threads in GDNet history, but now something fruitful is finally coming of the concept.
Epoch now has a functioning virtual machine: Say hello to Fugue

I decided to take a rather different route for the implementation. Traditionally when I've done a toy language, I've started with a parser and gotten it generating a token stream, and then worked from there on acting on that stream, be it feeding into a compiler or dumping the output into an interpreter that executes it "live."
This time, I'm building an infrastructure that you could call something of an executable compiler pipeline. The same code is intended to work for both an interpreter/VM and a compiler.
For example, there is a class representing a lexical variable scope, which can be asked to dump a list of all its variables. This can be used by a compiler to statically assign stack space for those variables; it can be used at runtime by an interpreter for reflection purposes; and so on.
As another example, the built-in addition operator is represented by a class as well. It can "serialize" itself during the compile process to produce sensible output based on the target language. For example, serializing addition to C would produce "(a + b)", and serializing to assembler would produce "ADD b, a".
This will provide hooks for easy drop-in of compiler back ends, similar to how GCC works. It also allows deep introspection in the VM itself.
The interesting thing is that there is actually no parser or syntax for the language right now. Programs are "built" by writing code in the VM's unit test module, and instantiating the wrapper classes for each first-class language construct from C++. The VM class then executes those wrappers and produces results.
This is what the unit test for addition looks like:
bool AdditionTest::Test()
{
VM::Scope scope;
scope.AddIntegralVariable(L"foo", 3);
scope.AddIntegralVariable(L"bar", 7);
scope.AddIntegralVariable(L"baz", 0);
scope.AddIntegralVariable(L"quux", 0);
VM::Operations::SumIntegrals(scope, L"baz", L"foo", L"bar").Execute();
VM::Operations::SumIntegrals(scope, L"quux", L"baz", 32).Execute();
UNITTEST_ASSUMPTION(scope.GetIntegralVariableValue(L"foo") == 3);
UNITTEST_ASSUMPTION(scope.GetIntegralVariableValue(L"bar") == 7);
UNITTEST_ASSUMPTION(scope.GetIntegralVariableValue(L"baz") == 10);
UNITTEST_ASSUMPTION(scope.GetIntegralVariableValue(L"quux") == 42);
return true;
}
Note the way that a SumIntegrals object is created, and then Execute() is called to actually "run" the operation. This decouples the execution of code from its representation, meaning that a SumIntegrals object can actually be passed around unevaluated as a first-class expression construct. The potential uses for this are myriad, obviously, but the primary value of it is that it allows an easy swap between imperative and functional styles based purely on how the VM chains its Execute() calls.
As of right now, the VM only supports integer variables and booleans, and only has the addition operation. However, the framework is pretty well defined and adding more features should move quickly. Look for consistent updates over the next several weeks. Finishing off the arithmetic operators, implementing basic control logic, and getting the IO library working are all on the list.
My goal is to eventually get the VM to the point where I can run a simple Sudoku solver in one of the unit tests. Once I've reached that point, I will switch to working on adding the syntactical front-end to the system.
So there you have it: Epoch is officially in production.
| |
 Handy code snippet |
Posted - 3/5/2008 2:19:04 AM | The Problem
Often when working in C++ I find myself in a situation where I want to be able to quickly drop some formatted output in a specific way. My usual standby is to use a lot of intermediate stringstream objects, like this:
void OutputMessage(const std::wstring& message);
void FooFunction()
{
int foo = 4;
std::wstring bar = "Stuff!";
{
std::wostringstream msg;
msg << "State of foo is " << foo << " and bar is " << bar;
OutputMessage(msg.str());
}
}
This allows any given kind of output function we want: a message box, output to a custom console in a game, logging to a file, sending over a network debug connection, and so on.
However, I find this unwieldy and annoying, especially when I have to dot my code with lots of inline stringstreams. Using this method is fine for quick and dirty debug logging, but it can quickly sap performance since it wastes a lot of time allocating memory.
The Solution
To address this issue, I whipped up the following simple class:
class OutputStream
{
public:
~OutputStream()
{
Flush();
}
public:
OutputStream& Flush()
{
std::wstring outstr = Stream.str();
if(!outstr.empty())
{
OutputMessage(outstr);
Stream.str(L"");
}
return *this;
}
template<typename T>
OutputStream& operator << (const T& val)
{
Stream << val;
return *this;
}
OutputStream& operator << (std::basic_ostream<wchar_t, std::char_traits<wchar_t> >&
(__cdecl *ptr)(std::basic_ostream<wchar_t, std::char_traits<wchar_t> >&))
{
(*ptr)(Stream);
return Flush();
}
protected:
std::wostringstream Stream;
};
Examining the Solution in Detail
Let's walk through the code.
The first bit in the destructor ensures that if the object goes out of scope, any remaining output is dumped. The actual Flush() function is responsible for doing that. Included is a check to avoid dumping the output if there isn't anything to say. Flush() automatically clears out the buffer so that you can easily reuse the same object to do multiple output runs.
The next part is where the magic happens. The first operator << does the heavy lifting; any of your standard output usages will flow through here. Note that all this really does is defer the work of creating the buffer to the standard wostringstream class. This also means that any type which can be used with a standard output stream will work with OutputStream as well.
The second operator << is designed to allow you to use std::endl to flush the stream automatically. This means that piping endl into an OutputStream is effectively a call to Flush(), which in turn calls our custom OutputMessage() function.
Using the Code
Putting it into action is easy:
{
OutputStream stream;
stream << "Foo" << 42 << "baz";
}
OutputStream stream2;
stream2 << "Quux" << 343 << "Zimbabwe" << std::endl;
So now you can have all the advantages of easy stream formatting like std::cout would give you, except you can couple it easily to any kind of output function you want, or even multiple output types at the same time.
Further Expansion
In this example, I've used a free function OutputMessage() which presumably actually displays or otherwise handles the messages from the buffer. However, you may find this too simple. A good extension would be to make a class with a pure virtual method OutputMessage(), and turn OutputStream into a template class that can be "attached" to different types of outputters. For example, you could implement a MessageBoxOutputter (which derives from the abstract base class) and then use OutputStream<MessageBoxOutputter> to output messages via a message dialog box.
Another thing to note is that this example does not support the full set of IO formatting functionality provided in the C++ standard library - just endl and other similar IO manipulators. Extending this should be easy: just pass the calls through to the Stream member variable and let it do all the work.
One final note: I have assumed here the use of MBCS/Unicode. If you are still living in the 1990s and refuse to use Unicode, just remove the "w" from wstring and wostringstream, and change wchar_t to char.
Enjoy!
| |
 Shameless pimpology 101 |
Posted - 3/4/2008 6:56:56 AM | Want a job? Dig it. Dig it if you can.
| |
In locus hic, omnes res dementes sunt.
|
| S | M | T | W | T | F | S | | | | | | | | 2 | 3 | | | 6 | | | 9 | | 11 | 12 | 13 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | | 29 | 30 | 31 | | | | | |
OPTIONS
Track this Journal
ARCHIVES
July, 2009
June, 2009
May, 2009
April, 2009
March, 2009
February, 2009
January, 2009
October, 2008
September, 2008
August, 2008
July, 2008
June, 2008
May, 2008
April, 2008
March, 2008
February, 2008
January, 2008
December, 2007
November, 2007
October, 2007
September, 2007
August, 2007
July, 2007
June, 2007
May, 2007
April, 2007
March, 2007
February, 2007
January, 2007
December, 2006
November, 2006
October, 2006
September, 2006
August, 2006
July, 2006
June, 2006
May, 2006
April, 2006
March, 2006
February, 2006
January, 2006
December, 2005
November, 2005
October, 2005
|