I might be done with the Stellar application for the moment, but I don’t think I’m done with the space theme in Parrot yet.
Back in the ancient days, there was a nifty game called Star Trader. You and your friends were interstellar merchants trying to earn a few credits (or whatever) in a cold and uncaring universe. Star Trader has had many popular descendants, which have evolved over the generations into games like Trade Wars Rising, Oolite and Eve Online. Those games are interactive and fun and great ways to kill many hours, but I’ve got an itch for something old school. I want to revisit the joy of a text interface that demands your imagination work overtime while you figure out what is going on.
It is possible that I have been playing Dwarf Fortress a little bit too much for my own good.
This one is going to take some work. It is a fairly elaborate game. The map is random, markets change, and merchants can be haggled with. I can use the original code as a resource, but not very well. The listing I could find was written in a HP-BASIC dialect that I am unfamiliar with. So I have to do more than just copy the game. I’ll have to make a game inspired by Star Trader instead. That seems to be what all the cool kids are doing - assuming you use a rather flexible definition of “cool.”
I talked about using a text interface, but I know that eventually I will want to choose my own interface for the game. Players can choose their own approach, and bored coders will be able to create new ones. I will start by keeping the game logic as abstract as I can, and worry about the details of play later.
SpaceTrade Summary
Space Trade is a turn-based game in which one or more players assumes the role of an interstellar merchant in the future. The game has a fixed number of turns, determined during game setup. Players are competing to have a pilot with the highest worth at the end of the game. The single player goal is to beat her own previous high scores.
Game play occurs on a map of star systems. Each star system has a trade classification, which makes the price of goods vary from one system to the next. There is a port in every system for traders to buy and sell goods, or to upgrade their ship’s capabilities. Traders may attempt to haggle for a more favorable price, but this might not work. As the game progresses, markets may change based on trade activity. A glut of a particular good could temporarily reduce its value, or a run on that good could temporarily increase its value.
Traders may encounter hazards such as planetoids or pirates while travelling between systems. The results of these encounters could be cargo loss or damage to the trader’s ship. If a ship accumulates enough damage without repair, it could be destroyed. Destruction of a ship ends the game for that trader.
Development Tasks
My summary is a little vague compared to your average game, but there are a lot of juicy programming tasks in there.
- Creating a star
- Building a star map
- Creating a new trader
- Buying cargo
- Selecting and travelling to a new system
- Selling cargo
- Dealing with changing markets
- Haggling with merchants
- Coping with environmental hazards (pirates, planetoids, etcetera)
- Enabling multiple players
- Upgrading a ship
- Scoring the endgame
- Tracking high scores
- Saving a game in play
- Loading a saved game
At each stage, we will work on the simple text interface and add randomization to make gameplay interesting.
I have never written a game in Parrot before. I have not written many games in any language. I understand if one of your questions is “why not use language X?” - where X is Python, Perl, Ruby, Rakudo, D, or something else. I might use language X another time, but then it would be part of the X Babysteps rather than the Parrot Babysteps.
Another question might be “Are we ever going to use Parrot to write a language?” Actually, yes. I’m going to put together a simple script language that handles game behavior. Not a powerful megasmart language for high end projects, but something for building the star map and playing the game itself. It will be used for saving and sharing games, and inevitably for hacking game details. Hey, what fun is a game if you can’t hack it?
That’s three more on-going development tasks, then:
- Developing an interactive user shell
- Randomizing game play elements
- Creating a game scripting language
This is more complex than Stellar, and it will take more than a few steps to finish it. I am certain there will be a lot of new Parrot territory to explore.
This should be fun. Let’s get started!
Setting up the project
Thanks to Stellar, I already know how I like to prepare my workspace for a new project. The setup from parrot-babysteps-09-simple-projects will provide the starting point for SpaceTrade.
The setup.pir
script will start out the same as the one used for Stellar.
There is one basic feature I want to get out of the way before I start handling game logic. User interaction is important. Oh sure, there may eventually be interfaces in Curses or SDL, but all that’s needed for now is a simple command line shell. This shell will be used to examine the nuts and bolts of SpaceTrade and to play a simple text-based version of the game.
The SpaceTrade Interactive Shell
I believe that every interactive shell needs a few minimal components to be useful.
- A command to quit
- A command to get help
- A reasonable way to handle invalid input
A sample session with such a minimal shell might look like this:
Why do I imagine this shell having commands prefixed by a :
character? Well, “normal” commands would look normal, but behavior like getting help or quitting the game are only important for dealing with the shell. I want those special shell commands to look different from the normal game commands.
Of course, I may change my mind later. I am fickle.
What is the smallest amount of code I can use to get this end result and still feel comfortable?
This works, but it doesn’t look right.
For a start, the commands are kind of a mess. When I add commands, I will have to add both an if
check in the READLINE
section and a line of output in the SHOW_USAGE
section. Then there are the blocks I would have to add to provide that actual functionality. No, I do not like this at all. The shell commands should be better organized so that adding and managing features is as easy as possible.
One approach would be to add a registry which stores the commands recognized by the shell.
Creating a Command Registry
The idea is that I could have a simple structure that stores information about available commands, and the application could add commands as needed. Let’s start with a simple Hash and two subroutines for adding and evaluating shell commands.
The first sub that’s needed is register_command
, which will add a :dude
entry in the commands
Hash with appropriate information.
There is no special magic going on here. command[':dude']
points to a Hash containing a subroutine name and an explanation of the command. commands
is returned to the caller once the new command has been added.
You can probably figure out what I expect to happen from the test code. I have a say_dude
sub, and somehow I expect the shell to figure out how to call that sub when I ask for it by sending the :dude
command. We’ve actually already done this, back when we were grabbing the chomp
sub in /post/2009/06-files-and-hashes/. The get_global
variable opcode will look for a variable with a specified name and return it to us if it exists.
There is one new bit of strangeness here, though:
This is called a “complex key,” and lets us directly access the values in the Hash held at commands[name]
. Each index in a complex key is separated by a semicolon (;
) character. Without a complex key, we might have to do something like this:
I did not realize I could use a complex key until I scanned the variables chapter of the Parrot PIR Book. It is important to keep reviewing documentation, even if you think you already know a solution. Remember: regardless of what you know, there is probably a better way.
It is time to add basic error handling to the shell. evaluate_command
needs to handle two major error cases.
- User tries a command that doesn’t exist
- User tries a command that points to a nonexistent subroutine.
Okay, let’s add the tests.
evaluate_command
is a little more complicated now, but it is still manageable.
One thing that might catch your attention is the if_null
opcode.
This will check if command_sub
is null, and branch to INVALID_COMMAND
if the subroutine we just tried to grab is indeed null. To be perfectly honest with you, I’m not sure if a branch is the same as a goto
. It behaves the same in this code, so for now I will pretend that it is the same.
Setting Up Those Default Shell Commands
This ends up working pretty much the same as the earlier code did, and it’s a bit more flexible. Is this how we make programming languages in Parrot? Well, no. This is not how we make programming languages in Parrot. This is a very simple shell which will have a few simple commands, but try to pass everything else off to the game itself. Proper language development is still a few Babysteps away.
The test code that has already been written shows a clear path for registering default commands. All that’s needed is the subroutines that will be invoked when the command is called.
There’s a problem.
The problem is that I had to cheat on default_help
. See, the way that I set up evaluate_commands
is to directly invoke the registered subroutine without any arguments. I would prefer that default_help
examined the currently registered commands and provided a real summary. It should even include my magnificent :dude
command in the summary.
How am I supposed to do this? Let’s start by rewriting default_help
the way it should work: by preparing a sorted list of registered commands and their summaries.
A little explanation about default_help
couldn’t hurt. Hashes use their own special tricks to make storing their elements more effective, which means you have no guarantee of getting them in any particular order. I want to see the commands in alphabetical order, so I will have to handle the ordering myself. I did that by first building a list of keys.
Once that list was constructed, it needed to be put in some sort of order. Luckily, the Array PMCs come with a prepackaged sort()
method - a special subroutine that works directly with the elements of the array.
The default sort behavior works for me. In this case they will be sorted more or less alphabetically.
Now, I could add a lot of code to evaluate_command
that will magically determine what sort of arguments are required by the command, and to behave appropriately. But before I go doing a significant rewrite - how about an experiment? Maybe I can just call every command with commands
as a parameter, and see what happens in the tests.
You have to be willing to experiment, because the results may occasionally surprise you.
How about that - it worked. PIR subroutines will apparently ignore positional parameters that they didn’t ask for, which means that evaluate_command
can call say_dude
and default_help
with the same parameter list and nothing bad will happen.
The New and Slightly Improved Shell
A lot of work has gone into making the shell easier to use for me and people who want to hack on the game in the future. Let’s apply that work to the run_shell
subroutine itself.
It’s certainly shorter than what I started with. How well does it work?
It isn’t perfect, but it will work for the moment. This new shell has most of the core behavior from the original, and we have shown that it will not be hard to add new commands. There is still a large part of me that thinks the code for the shell should be tucked into its own corner, where it cannot get mixed up with the code for the actual game. That will have to wait for the next step, though.
Backlinks
Added to vault 2024-01-15. Updated on 2024-01-26