Click here to Skip to main content
15,887,596 members
Articles / Programming Languages / C#

The New, Improved Star Trek Game

Rate me:
Please Sign up or sign in to vote.
4.76/5 (9 votes)
30 Aug 2008CPOL12 min read 40.9K   869   31   7
Windows Form Implementation of the Classic Game

Introduction

This code implements the legendary "Star Trek" game, using C# and the .NET Framework.

Background

My first experience with the Star Trek game was in the mid 70s on an HP 3000. I was using a Texas Instruments Silent 700 terminal to do a dial-up connection. This device had an acoustic coupler on the back, making it appear it had Mickey Mouse ears. You dialled the computer's number and then placed the hand set into the acoustic coupler and you were ready to go. The Silent 700 had a keyboard and thermal printer for output. Playing star trek on it was quite an experience.

I later upgraded to a Tandy 1000. It had 256KB of RAM and dual floppy drives and I thought I had it made now. My son was about 12 years old by this time, and we bought David Ahl's book of computer games and keyed in the star trek game source. We had a great time, but had grandiose ideas on how we could improve the game. We were going to make the galaxy three dimensional, and have the Klingons converge on the Enterprise as the game progressed. Somehow, we never found the time to develop the game.

When I saw the announcement of the summer programming contest to reproduce the old game, I actually found my design notes from years ago. I also still have the book, but it was in the attic, and in July, the temperature up there approaches 120 degrees. I decided to see if I could find any information on the web so I Googled "star trek game" and was amazed at all the information on this venerable game. There were histories on the original author and how the game had been ported to various platforms, and even copies of the original source for download.

Since my recollection on all the details of how the game worked was rather faint, I decided the best way to obtain the requirements was to use the original basic source code which I downloaded from here.

I decided it was time to finish the project my son and I had started some twenty years earlier and enter the programming contest. My job now is as a manager of programmers and I don't get to do much development myself anymore. We are currently in the process of rewriting our mission critical applications from mainframe and UNIX to Service Oriented Architecture and will be using C# and the .NET Framework. I have been trying to teach myself C# for some time and thought this would be an excellent opportunity to see how far I've come.

Although I am not an expert in either computer games or .NET programming, I decided to give it a shot. I've been a member of Code Project for over a year, have gotten a lot of valuable tips and code, but have never made a contribution, and felt it's about time. Experienced .NET and C# programmers may not get a lot from my efforts, but hopefully you'll have fun with the game. I had a lot of fun working on it.

My approach was to use two phases. In the first phase, I would get all the functionality of the game implemented as a C# Console Application. This would get the basic game action defined and the code checked out. The second phase was to modernize the user interface by implementing the game as a Windows Forms application.

Phase One - The Console Application

Once I had the original source code in hand, I set about writing my own version in C#. Looking at the original source, I decided to keep the original variable names so I wouldn't get confused trying to replicate the code. The plan was to go back later and use more meaningful names than the one-letter, one-number names of basic. Ah, the best laid plans...

One quick note if you download the console app and try to run it. I built it in a directory named C:\Projects\Trek1. The program is hard coded to look for the list of instructions in this directory. If you use a different directory, you will have to change this constant in the ShowInstructions() routine.

The biggest obstacle I faced was the display of the Short Range Sensor depiction of the current quadrant.

The original author used a very clever technique of defining a series of strings based on a 32 character print line. I decided not to attempt to replicate his logic in my console app. Instead, I used a string array to represent the contents of the current quadrant with a print routine.

Listing 1. Printing the Current Quadrant Array

C#
StrSensorDisplay[0] = String.Format("     STARDATE {0}", T);
StrSensorDisplay[1] = String.Format("     CONDITION {0}", strStatus);
StrSensorDisplay[2] = String.Format("     QUADRANT ({0}, {1})", Q1, Q2);
StrSensorDisplay[3] = String.Format("     SECTOR ({0}, {1})", S1, S2);
StrSensorDisplay[4] = String.Format("     TOTAL ENERGY {0}", E);
StrSensorDisplay[5] = String.Format("     PHOTON TORPEDOES {0}", P);
StrSensorDisplay[6] = String.Format("     SHIELDS {0}", S);

Console.WriteLine(" 0  1  2  3  4  5  6  7 ");

Console.WriteLine("________________________");

for (I = 0; I<8; I++)
{
    Console.Write("{0:3}{1:3}{2:3}{3:3}{4:3}{5:3}{6:3}{7:3}",
        StrSector[0, I], StrSector[1, I], StrSector[2, I],
        StrSector[3, I], StrSector[4, I], StrSector[5, I],
        StrSector[6, I], StrSector[7, I]);

    if (I < 7)
        Console.WriteLine(StrSensorDisplay[I]);
    else
        Console.WriteLine();
}

Console.WriteLine("________________________");

Here's how it looks on screen:

Sample Image - maximum width is 600 pixels

Figure 1 - Console App Screen.

Points of Interest

One thing I ran into, and still don't fully understand, was the behaviour of the Console.Read command. This was my first attempt to replicate the functionality of the basic input command. For the most part, user input is a one character response to a prompt. It appeared the character buffer was never emptied and previous responses were being picked up before the user had the opportunity to respond to prompts. The .NET class library documentation warned about using Console.Read and advised using Console.ReadLine instead. When I switched to this command, I was able to obtain user input as I expected.

Phase Two - The Windows Forms Application

Once I had the console application working, I set about the task of upgrading the user interface, implementing the game as a Windows Forms Application.

My objectives in modernizing the user interface were to speed up the game play and make more information on-screen. The original basic program was written for a teletype machine and when you were using a hard copy device, that worked fine. But if you were on a PC like the Tandy 1000, the short range sensor scan and other information scrolled off screen rather quickly. Also, the repetitive back-and-forth between the player and the computer, with the computer prompting and the player responding, made an awkward tempo for the game.

Points of Interest

One of the first things I did to speed up game flow, was to implement the various commands as buttons on a tool bar. These can be seen in figure 6, the main screen. Things like short and long range sensors, damage control report, and status report, were now on screen all the time and didn't require buttons.

In the console application, I was never able to get fractional course selections to work. In other words, if the computer routine said to use course 3.5 when firing a torpedo, it always missed. In all my testing and debugging, I'm not sure the original code worked. For this reason, I took out the option to use the computer to calculate the torpedo track. Since the status report was on-screen all the time, the only remaining computer function was the cumulative galaxy map. So you will see a button whose text says "Galactic Map", not "Computer".

In order to control the screen, I made the application a Multiple Document Interface or MDI application. I was amazed at how easy this was to implement in a Windows Forms application. All you need to do is set the IsMdiContainer property of the main form to true in the designer, and then set the MDI Parent property of the child form, to the main form before activating the child. The following code, from the FormLoad event handler of the main form, shows how to do this.

Note, I am also setting some public properties in the child in code, which allow it to display the information needed. StrSector is the string array storing the contents of the current quadrant. D1 is the damage control rating for the short range sensors.

Listing 2 - Invoking a child MDI Form

C#
shortrangescan.MdiParent = this;
shortrangescan.Top = panel1.Bottom;
shortrangescan.Left = panel1.Width / 3;
shortrangescan.Width = 258;
shortrangescan.Height = 208;
shortrangescan.StrSector = StrSector;
shortrangescan.D1 = D[1];
shortrangescan.Show();

Where I wanted to control the painting of the form very specifically, I used a child form and put the code in the Paint event. Here is the code for the Short Range Sensor display. This is another example of how information stays On-Screen. This child window is never closed until the game is over, and is repainted whenever appropriate using Invalidate and Update commands.

Listing 3 - Short Range Sensor Scan Form Paint Event

C#
private void ShortScanForm_Paint(object sender, PaintEventArgs e)
{
    int I = 0;
    float x1 = 0.0F;
    float y1 = 0.0F;
    float x2 = 0.0F;
    float y2 = 0.0F;
    Pen linePen = new Pen(Color.WhiteSmoke, 1);

    Font drawfont = new Font("Courier New", 10);
    SolidBrush drawbrush = new SolidBrush(Color.WhiteSmoke);

    if (D1 < 0)
    {
        e.Graphics.DrawString("Short Range Sensors are out!!!",
        drawfont, (Brush)drawbrush, 10.0F, 110.0F);
        return;
    }

    e.Graphics.DrawString(" 0  1  2  3  4  5  6  7",
        drawfont, (Brush)drawbrush, 20.0F, 10.0F);

    for (int x = 0; x < 8; x++)
    {
        e.Graphics.DrawString(String.Format("{0}", x),
            drawfont, (Brush)drawbrush, 15.0F,
            (float)(30.0F + (x * 20)));
    }

    x1 = 20.0F;
    y1 = 26.0F;
    x2 = 220.0F;
    y2 = 26.0F;
    e.Graphics.DrawLine(linePen, x1, y1, x2, y2);

    for (I = 0; I < 8; I++)
    {
        String scanline = String.Format("{0:3}{1:3}{2:3}{3:3}{4:3}{5:3}{6:3}{7:3}",
            StrSector[0, I], StrSector[1, I], StrSector[2, I],
            StrSector[3, I], StrSector[4, I], StrSector[5, I],
            StrSector[6, I], StrSector[7, I]);

        e.Graphics.DrawString(scanline,
            drawfont, (Brush)drawbrush, 20.0F,
            (float)(30.0F + (I * 20)));
    }

    x1 = 20.0F;
    y1 = 184.0F;
    x2 = 220.0F;
    y2 = 184.0F;
    e.Graphics.DrawLine(linePen, x1, y1, x2, y2);
}

In other cases, where the information to be displayed was simple text, I used controls on the main form. The Damage Control panel is an example.

damage control pic goes here

Figure 2 - Damage Control Panel.

Listing 4 - Damage Control Panel

C#
public void DamageControlReport()
{
    warp_txt.Text = String.Format("{0:#0.000}", D[0]);
    if (D[0] < 0.0)
        warp_txt.BackColor = Color.Red;
    else
        warp_txt.BackColor = Color.LightGray;

    short_txt.Text = String.Format("{0:#0.000}", D[1]);
    if (D[1] < 0.0)
        short_txt.BackColor = Color.Red;
    else
        short_txt.BackColor = Color.LightGray;

    long_txt.Text = String.Format("{0:#0.000}", D[2]);
    if (D[2] < 0.0)
        long_txt.BackColor = Color.Red;
    else
        long_txt.BackColor = Color.LightGray;

    phaser_txt.Text = String.Format("{0:#0.000}", D[3]);
    if (D[3] < 0.0)
    {
        phaser_txt.BackColor = Color.Red;
        Phaser_Btn.Enabled = false;
    }
    else
    {
        phaser_txt.BackColor = Color.LightGray;
        Phaser_Btn.Enabled = true;
    }

    photon_txt.Text = String.Format("{0:#0.000}", D[4]);
    if (D[4] < 0.0)
    {
        photon_txt.BackColor = Color.Red;
        Torpedo_Btn.Enabled = false;
    }
    else
    {
        photon_txt.BackColor = Color.LightGray;
        Torpedo_Btn.Enabled = true;
    }

    dmgctl_txt.Text = String.Format("{0:#0.000}", D[5]);
    if (D[5] < 0.0)
    {
        dmgctl_txt.BackColor = Color.Red;
        dmgctl_panel.Hide();
    }
    else
    {
        dmgctl_txt.BackColor = Color.LightGray;
        dmgctl_panel.Show();
    }

    shield_txt.Text = String.Format("{0:#0.000}", D[6]);
    if (D[6] < 0.0)
    {
        shield_txt.BackColor = Color.Red;
        shield_btn.Enabled = false;
    }
    else
    {
        shield_txt.BackColor = Color.LightGray;
        shield_btn.Enabled = true;
    }

    computer_txt.Text = String.Format("{0:#0.000}", D[7]);
    if (D[7] < 0.0)
    {
        computer_txt.BackColor = Color.Red;
        Computer_Btn.Enabled = false;
    }
    else
    {
        computer_txt.BackColor = Color.LightGray;
        Computer_Btn.Enabled = true;
    }
}

One of my biggest problems when playing the original game, was selecting the course for warp navigation and torpedo firing. It seemed I could never keep them straight. I always had to keep a copy of the instructions in front of me and refer to the little course map whenever I had to input course information. Figure 3 is the course legend that stays on screen at all times.

Sample Image - maximum width is 600 pixels

Figure 3 - The Course Legend.

To manage the feedback from the program, I wanted a scrolling window that functioned like the console application. I used a list box and this met my needs perfectly. As an item is added to the list, it is highlighted, which causes it to scroll into view automatically. Here is the code to add an item and highlight it, with the listbox itself shown after.

Listing 5 - Add Item to Listbox and Highlight

C#
if (S < 200)
{
    if (listBox1.SelectedIndex > -1)
        listBox1.SetSelected(listBox1.SelectedIndex, false);
    listBox1.Items.Add(String.Format("SHIELDS DANGEROUSLY LOW"));
    listBox1.SetSelected(MessageEntries++, true);
}

listbox pic goes here

Figure 4 - Message Panel.

In the code, we check to see if shields are less than 200 units of energy. If so, we want to warn the player. When listbox1.SelectedIndex is greater than -1, there is an item already selected, or highlighted. SetSelected, with the second argument as false, turns off the selection. We then add the item to the listbox and set it as selected. We are also keeping a running count of how many entries have been added.

One of the things I find somewhat annoying with some Windows applications, is when the app goes wandering all over the desktop everytime you start up. Back in my MFC days, I had devised a method for storing the main window size and location in the windows registry. If the window was moved during execution, the new location was automatically stored.

Searching for a similar technique, I found there was a wrapper class for the Win32 registry functions. You have to add the statement using Microsoft.Win32; to your source and then there are some rather easy to use registry manipulation functions available.

While working on this project, I discovered the Application Configuration File approach, which Microsoft seems to now favor over using the registry. However, I had cut my windows programming teeth back in the Windows 3.1 days where the use of ini files was prevalent. When Windows NT came out, I found the registry a much more satisfying programming technique. I guess I'll have to adapt at some point, but not while trying to finish this project.

Here is the registry code, which goes in the Form_Load event of the main form:

Listing 6 - Get Window Attributes from Registry

C#
// Obtain or create window location settings from registry
RegistryKey reg1 = Registry.CurrentUser;
RegistryKey reg2;

reg2 = reg1.OpenSubKey("Software\\MyProjects\\CSTrek\\Settings");
if (reg2 == null)
{
    this.Height = 557;
    this.Left = 156;
    this.Top = 45;
    this.Width = 730;
    reg2 = reg1.CreateSubKey("Software\\MyProjects\\CSTrek");
    if (reg2 != null)
    {
        RegistryKey reg3 = reg2.CreateSubKey("Settings");
        if (reg3 != null)
        {
            reg3.SetValue("height", 557);
            reg3.SetValue("left", 156);
            reg3.SetValue("top", 45);
            reg3.SetValue("width", 730);
        }
        reg3.Close();
    }
}
else
{
    this.Height = (int)reg2.GetValue("height", 557);
    this.Left = (int)reg2.GetValue("left", 156);
    this.Top = (int)reg2.GetValue("top", 45);
    this.Width = (int)reg2.GetValue("width", 730);
}

reg1.Close();
reg2.Close();

Then, to save any potentially new attributes, in the Form_Closing event handler:

Listing 7 - Save Window Attributes to Registry

C#
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
    string SubKey = "Software\\MyProjects\\CSTrek\\Settings";
    RegistryKey reg1 = Registry.CurrentUser;
    RegistryKey reg2 = reg1.OpenSubKey(SubKey,
        RegistryKeyPermissionCheck.ReadWriteSubTree);
    if (reg2 != null)
    {
        reg2.SetValue("height", (int)this.Height);
        reg2.SetValue("left", (int)this.Left);
        reg2.SetValue("top", (int)this.Top);
        reg2.SetValue("width", (int)this.Width);
    }
    reg1.Close();
    reg2.Close();
}

When obtaining user input, I decided to use dialogs and found them really easy to implement in .NET. A form is built like any other, but you turn on the DialogResult properties and use ShowDialog() instead of the plain Show() function and you have a dialog.

Listing 8 - Display a Dialog Form

C#
CourseDlg dlg = new CourseDlg();
dlg.label1.Visible = true;
if (D[0] < 0)
{
    dlg.WarpTrackBar.Enabled = false;
    dlg.Warp_TxtBox.Enabled = false;
}
else
{
    dlg.WarpTrackBar.Enabled = true;
    dlg.Warp_TxtBox.Enabled = true;
}
dlg.label2.Visible = true;
dlg.Impulse_TxtBox.Visible = true;
dlg.ImpulseTrackBar.Visible = true;

if (dlg.ShowDialog() == DialogResult.OK)
{
    C1 = dlg.CourseTrackBar.Value;
    warp = dlg.WarpTrackBar.Value;
    impulse = dlg.ImpulseTrackBar.Value;
}
else
    return;

An instance variable of the dialog class is declared, and if you define them as public, they can be set before invoking the dialog itself. I use this approach to check the status of the warp drives, and disable the track bar if they are not operational. I also use a track bar labeled "Impulse Power", which is always available. I used this to simulate the original game's technique of using a warp factor less than 1, to navigate within the current quadrant.

Here is what the dialog looks like on screen:

Coure Dialog goes here

Figure 5 - The Course Dialog Form.

This dialog actually does double-duty, serving as the form to obtain the player's desired course when firing a photon torpedo. This was accomplished by disabling the warp and impulse power components before displaying the dialog.

Listing 8 - Displaying the Torpedo Course Dialog

C#
CourseDlg dlg = new CourseDlg();
dlg.label1.Visible = false;
dlg.Warp_TxtBox.Visible = false;
dlg.WarpTrackBar.Visible = false;
dlg.label2.Visible = false;
dlg.Impulse_TxtBox.Visible = false;
dlg.ImpulseTrackBar.Visible = false;
if (dlg.ShowDialog() == DialogResult.OK)
{
    C1 = dlg.CourseTrackBar.Value;
    // In case we someday figure out how to do fractional courses.
    Cf = (double)(C1 * 1);
}
else
    return;

In the form designer, the various dialog buttons are implemented by setting the DialogResult property of the button to one of OK, CANCEL, YES, NO, etc. I found this really easy to use and you are able to implement some dialogs without having to write any code in the dialog class itself. However, I wanted to validate the response in some cases. For instance, in the Shield Control dialog, I didn't want the player to request more energy for the shields than was available. To do this, I did not set the DialogResult property of my OK button in the designer. Instead, I used the button click event to do the validation and then set the DialogResult property, as shown below.

Listing 9 - Implementing a Dialog OK Button

C#
private void button1_Click(object sender, EventArgs e)
{
    EShields = Convert.ToInt32(ShieldEnergy_TxtBox.Text);
    if ((EAvail - EShields) > 0)
    {
        this.DialogResult = DialogResult.OK;
    }
    else
    {
        label5.Visible = true;
    }
}

Putting it all together, here is a rather scrunched view of the main screen. I had to squeeze things together, in order to get down to the 600 pixel maximum width requirement. But I promise it looks much better in the included form project, if you download and compile it.

Main Screen goes here

Figure 6 - The Main Game Screen.

Known Anomalies

One thing I found in the meager amount of testing I was able to do, was when you navigated to a quadrant inhabited by Klingons and also having a Star Base. If you happened to enter the quadrant in a sector adjacent to the Star Base, you were in a docked position. The total energy was set to 3000, you are protected by the Star Base's shields, but your shields are set to 0. If you try to navigate away from the Star Base, the Klingons will fire on you and you are done! However, in this position, you have unlimited energy and can blast away with phasers until all the Klingons in the quadrant are destroyed.

Also, I implemented the end of game as a dialog that gets displayed and asks if you want to play again. In my testing, I was not able to encounter all possible ways for the game to end, so I am not sure all the combinations flow properly.

Conclusion

So all you trekies out there, I hope you enjoy this modernized version of this classic game. It was a lot of fun putting together, and I learned a lot about Windows Forms programming in .NET.

History

  • 30th August, 2008: Initial post

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Retired Retired
United States United States
Dan retired from the business world in November, 2014. But his programming experience includes C/C++, C#, Oracle SQL and PL/SQL, MFC, WCF, gSOAP. He has developed systems for everything from IBM, Burroughs and Honeywell mainframes to Radio Shack PCs. He does not have a favorite platform, just enjoys solving business problems by applying modern technology.

Dan is the father of two, and has two wonderful grandchildren he now gets to see more often, now that he is retired.

His current interests include MFC projects to do simulations of baseball and football. Hobbies he was unable to pursue when still working.

Comments and Discussions

 
QuestionTrying a similar approach using Swift and iOS Pin
David Parker17-Apr-22 3:16
David Parker17-Apr-22 3:16 
QuestionCan't unzip files Pin
Member 1484757629-May-20 7:54
Member 1484757629-May-20 7:54 
AnswerRe: Can't unzip files Pin
Member 1484757629-May-20 8:05
Member 1484757629-May-20 8:05 
GeneralMy vote of 5 Pin
Reza Ahmadi6-Mar-12 0:38
Reza Ahmadi6-Mar-12 0:38 
GeneralRedesign Pin
Jason McBurney2-Sep-08 4:43
Jason McBurney2-Sep-08 4:43 
GeneralNice to see a Mouldy Oldie Back! Pin
Oakman31-Aug-08 4:46
Oakman31-Aug-08 4:46 
GeneralRe: Nice to see a Mouldy Oldie Back! Pin
djc203231-Aug-08 14:01
professionaldjc203231-Aug-08 14:01 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.