Introduction
TIMPIST is - shockingly - a Tempest-like game. One James Wood designed it in Microsoft's Extended BASIC . . . in 1986. It is here presented as a port to C# 2.0. This article introduces gaming concepts like threading and double-buffering. Also I offer a parser for the CoCo's Extended-BASIC DRAW command, and I rip off Colin Angus Mackay's "Beep" function. Any and all of this should be of help to any other Nerds Of Age whose DeLoreans are still under warranty.
Background
TIMPIST itself is explained in the front screen:
(Yes, "Fire with BUTTON !!" was part of the original text. I am hoping that it becomes a meme.)
So - 1986! Don't we all miss those days? when you had the brawn, the Pet Shop Boys had the brains, and Michael Douglas towed a massive cell phone through Wall Street. Also in 1986, thousands of amateur coders were out to provide an arcade experience for the home computer. For the TRS-80 Color Computer alone, we had Color Computer News, Color Computer Magazine and - dragging on to the bitter end - the free newsletter CoCo-Ads. The "ads" therein weren't just ads for products; coders would advertise themselves, by submitting their own work under their own names and usually including their mailing addresses. (It was much like CodeProject in its way.)
One such offering was TIMPIST, which game ended up in the September 1986 issue of CoCo-Ads and which issue might have been among the last issues sent to me. The reason I've picked on this game is that, in 1986, I spent hours typing it all in, and I only got to play it for about fifteen minutes, and then I turned off the computer - forgetting to save the game first. And not long after that we got a new computer and there was no question of redoing all my work. Waaah!
I wasn't finished yet! It has been eating at me for over a quarter-century, that I didn't finish this game. Of all the computer magazines I ever had, this one was the one I kept, for that reason.
So now I've caught the (black and) white whale. It is ported - it's as close to "done" as it needs to be. And the experience has paid off; it taught me a lot about game design.
Using the code
The simplest way to use the code is just to download "TIMPIST.exe.zip", put its files into some folder somewhere, run the executable ".exe" and have fun. Yay!
Oh, wait, we are on the CodeProject. There is supposed to be code involved. Oh, all right. (Killjoys.)
I wrote this with Visual Studio 2012. The solution might target .NET 4.5, but the projects target .NET 2.0. So, if you are on an earlier Visual Studio, you might want to create a new solution and attach the three vbproj's here thereto.
As to the three projects in this solution. One is the game itself, which is credited entirely to James W Wood, since here I was just his code-monkey. Another is the music library, which monkey was our very own Colin Angus Mackay. The third is my contribution, which is a parser for the Extended-BASIC "DRAW". I'll start with my own stuff first.
DRAW ""
The Color Computer lacked the RAM for arcade-time graphics of the detail of today. The 16K+ versions supplied instead, in its "Extended BASIC" library, a means to draw up simple small sprites. This means was the DRAW command. The command would parse a string and keep track of the pen's state. Nerds Of Age will immediately recognise the Turtle system, if not from CoCo then at least from LOGO.
Nowadays, runtime spriting isn't much needed, since we can just draw the graphics in Paint and screencap those bad boys. But we nostalgia buffs are going to need it ...
So, in case you don't own a DeLorean, first connect the CoCoDrawParser project in with your solution. This can be done by "Add Reference".
To set up the state, instantiate a TurtleDrawer
. It must be hooked into a
Graphics
object. I am also demanding a foreground Pen object. In this example, that pen is going to be the same throughout the game, so it's a const (well, readonly static).
private static readonly Pen PEN_FORE = new Pen(new SolidBrush(Color.White));
...
CocoDrawParser.TurtleDrawer td = new TurtleDrawer(thisGraphics, PEN_FORE);
Then, draw the string. The following code will lift the pen, set the pen to (30,30), put down the pen, scale to "4", and draw right-down-left-up. That's a rectangle.
td.Draw("BM30,30S4R195D135L195U135");
For the parsed commands I have included not much more than what is necessary for this game. If, in sha'llah, I port more games over to .NET, I'll fill the rest in. Or, one of you can do it!
To get into the parser itself, I use a Strategy pattern (with hints of State). I keep each command in its own function. First I define the standard function format, which always takes a string input; and I define a hashtable of characters-to-delegates:
private delegate void DrawFunc(string s);
private Dictionary<char,DrawFunc> _parser;
At instantiation, I assign characters to the function in question:
_parser = new Dictionary<char,DrawFunc>()
{
{'M', new DrawFunc(Move)},
...
}
The Draw
command takes in the string which must be parsed. I loop through that string. Once I figure out what command is next, I get its argument and I invoke its function:
_parser[command](strarg);
[So, you ask - why not just use the switch()
statement that is right there in the C and VB languages? My answer is that I ... don't like switch()
. Especially here when such a large part of the code revolves around assigning the function to a condition. The Strategy Pattern forces the coder to write self-documenting functions. We can see right away that there is going to be a "Move" command and we can go find its code in the _parser.]
Threading
One finds out pretty quickly that the CoCo didn't offer much in the way of object-oriented or even structured programming. But one can also uncover general points of commonality amongst action games like this one.
Any action game has two competitors: the player, and the enemy. The player acts on his own time and the enemy, famously, acts on his own time. . .
The Tempest-like genre is a little simpler in that the enemy is simply the clock. When the player confronts that "enemy", it is just "Fire With BUTTON !!" at the right place; and that action resets the clock. That frees us up not to care too much about the enemy's thread.
At the same time it is important, in a game of this tiny size, not to over-design. I mean the original thing was posted as a two-page entry in a free newsletter for heavens' sake.
To handle the enemy - the "weapon" - a Timer_Tick
event will fire at set times, and increment a counter. When the counter gets to the maximum (5, here) the game punishes the player by the LoseLife() routine.
private void timer1_Tick(object sender, EventArgs e)
{
if (_weapon.AtEnd())
LoseLife();
else
{
Song playG = new Song(DEFAULT_COCO_TEMPO);
playG.Notes.Add(new Note(Duration.Quarter, Pitch.G, DEFAULT_OCTAVE));
playG.Play();
RefreshBoard();
_weapon.IncrementThreat();
}
}
To handle the player's action, it is best to create listeners for the player's control - or, better, to find those listeners already implemented - and to react to the player's events. The player's field is the picturebox. The picturebox happily comes with its very own MouseMove
and MouseClick
events.
Note that when the threads are separated, even here where only two threads run, we can no longer assume that the threads will be in sync. So if the game is doing something time-consuming that is not part of gameplay - like the explosion sequence in
LoseLife()
- the game should stop the timer while that is going on.
More insidiously, the enemy might have won already when the player is still moving the mouse. I found that such conflict occurred when refreshing the screen from the player perspective. So, in the player's event handlers, I put in code to check that the enemy hadn't won before I refreshed the screen.
This is how MouseMove
is implemented. I restrict the player to the rim of the field. If the player's mouse is in the right part of the board then I accept that decision into the _player
object.
private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
{
if (_backBuffer != null)
{
int x = e.X;
int y = e.Y;
if (x > 210)
x = 230;
else if (x < 40)
x = 20;
if (y > 150)
y = 170;
else if (y < 40)
y = 20;
if (((x != _player.x) || (y != _player.y))
&& ((x == 20) || (x == 230) || (y == 20) || (y == 170)))
{
_player.x = x;
_player.y = y;
if (!_weapon.AtEnd())
RefreshBoard();
}
}
}
And this is how the MouseClick
is handled. (Remember "AWinnerIsYou"; I'm going to bring that up again in the double-buffering bit.)
private void pictureBox1_MouseClick(object sender, MouseEventArgs e)
{
_g.DrawLine(PEN_FORE, _player.x + 2, _player.y + 2, 127, 97);
Song playCEA = new Song(DEFAULT_COCO_TEMPO);
playCEA.Notes.Add(new Note(Duration.Quarter, Pitch.C, DEFAULT_OCTAVE));
playCEA.Notes.Add(new Note(Duration.Quarter, Pitch.E, DEFAULT_OCTAVE));
playCEA.Notes.Add(new Note(Duration.Quarter, Pitch.A, DEFAULT_OCTAVE));
playCEA.Play();
if (_player.InSector(_weapon.Sector))
{
AWinnerIsYou();
RefreshBoard();
}
else if (!_weapon.AtEnd())
RefreshBoard();
}
The double buffer
If your playing board is just the black screen, double-buffering does not matter. But who wants to play on a black screen? If you are on a background that is not supposed
to change, and there are moving parts about, you are going to want:
- to refresh the screen and
- not to be dealing with flickering and redrawing everything.
That is where the venerable double buffer pattern comes in. In the Color Computer,
this was done by Paging, and the game would flip from page to page by PMODE. In .NET 2.0, we capture a rectangle of graphic and store it in BufferedGraphics
.
private BufferedGraphics _backBuffer;
The concept is that you separate out the stuff that changes moment by moment - you, and the enemy - and you put the background stuff in a buffer.
_backBuffer = MakeBoard(BufferedGraphicsManager.Current);
private BufferedGraphics MakeBoard(BufferedGraphicsContext currentContext)
{
BufferedGraphics PMODE3 = currentContext.Allocate(_g, pictureBox1.DisplayRectangle);
Graphics backGraphics = PMODE3.Graphics;
CocoDrawParser.TurtleDrawer td = new TurtleDrawer(backGraphics, PEN_FORE);
td.Draw("BM30,30S4R195D135L195U135");
td.Draw("BM112,88R28D20L28U20");
...
return PMODE3;
}
When you and/or the enemy makes a move, the game imposes the buffer on your old position(s), as I do in RefreshBoard()
:
_backBuffer.Render(_g);
and redraws the moving parts.
The back-buffer can change too, but it's expected not to change as often. So your score and your lives will be written to that buffer when they need to be written.
Here is the increase-score example, "AWinnerIsYou":
private void AWinnerIsYou()
{
timer1.Stop();
Song.Beep(2217 >> 2, 1 << 9);
_score += (_weapon.WhichOne + 1) * 10;
_backBuffer.Graphics.FillRectangle(PEN_BACK.Brush,
110, 3, 91, 11);
WriteScore();
_weapon = Weapon.GenerateNew();
if (_score > 500)
timer1.Interval = 600;
else if (_score > 1000)
timer1.Interval = 300;
RefreshBoard();
timer1.Start();
}
And when the buffer is re-imposed later, that new information will be there.
Points of Interest
I was impressed that the concept of double-buffering was known so early. PMODE ain't got no time to flicker!
The biggest headache I had wasn't in writing a DRAW parser. That was somewhat fun actually.
The biggest headache was sound. Extended BASIC just let you play a bunch of notes in a string. There is no native .NET way to do that.
I could find no real .NET way of simply accessing the sound - of just PLAY'ing "note A, note B" in a quick string and being done with it (like in Extended BASIC).
I looked everywhere. Then I said "to heck with it" and just ripped off four classes out
of Mackay's .NET 1 "Beep" project and I do hope that he does not sue.
History
- 14 December 2012: Posted.