Welcome to Stellar Magnate¶
In the late 1980s I played a game called Planetary Travel by Brian Winn. The game was a textual trading game where you had to take a small amount of money and a space ship and travel between the planets of our solar system to make money.
Stellar Magnate is a game written in that same genre but updated and enhanced. Unlike the original Integer Basic that Planetary Travel was written in, this is written in Python and makes use of asynchronous programming, abstractions to allow multiple user interfaces, and other modern programming practices.
Stellar Magnate was written both to enjoy a little bit of nostalgia and to have a practical problem on which to experiment with new technologies. I hope that the game is somewhat enjoyable and the core is simple enough that new aspiring programmers can take a look at how it works to make it their own.
Design Documentation¶
These pages document the design decisions for Stellar Magnate
Mechanics¶
This document describes the game mechanics
Market¶
Current¶
Random pricing based upon a normal distribution.
Future¶
Need a cyclical market pricing. This way prices rise or fall for a certain duration. This makes the user take more care to judge whether the item’s price is on its way up or down.
Cycle should be mostly time based so that we can recalculate the price when a ship actually lands there rather than for every tick.
Will need to save the current state of the market so that loading a game starts in the same place.
- Markets only have so many goods that they’re willing to buy or sell
- Markets should be rated for population, industrialization, agriculture, mining. These affect supply and demand of categories of goods.
- Markets will accept categories of product even if they don’t sell them.
- Supply and demand drives prices. So if a market does not sell an item,
there will be a low supply. However, demand needs to be based off of how
different it is from what is already there.
- So perhaps categories need to be hierarchical. The farther away on the hierarchy from something already sold, the less demand
- Supply and demand drives prices. So if a market does not sell an item,
there will be a low supply. However, demand needs to be based off of how
different it is from what is already there.
Ship¶
Current¶
- Purchase more cargo space
Future¶
- Purchase different ships
- Each ship that you can purchase has different characteristics
Fleets¶
- We can organize ships into separate fleets
- Can send fleets off to trade in different locations
- Give fleets different orders about buying and selling
Finances¶
Current¶
One bank. Accessible whenever the user is on Earth.
Future¶
Multiple banks.
- The Syndicate: * Can do business everywhere * Loans money * High interest rates * Deposit low interest rate * 100% safe * May be inaccessible 50% of the time but never twice in a row
- System wide bank – Solarian National Bank * May not be present on pirate worlds. * Standard loans * Deposit low-medium interest * Safe unless war
- Capital planet bank – First Terran Bank and Trust * Available on a subset of system worlds * May open/close branches. Pick X% most industrial/civilized in-system worlds. * May sometimes bankrupts but bailout 75-95% of owed money * standard loan * moderate interest * War may lose all
- Planetary bank – Mercury Savings and Loan * Available 1 world * Low interest loans * Deposits moderate interest * Sometimes bankrupts bailout 0-50% * War may lose all
Events¶
PubMarine is used to pass events from the UI to the Dispatcher. These are the events that are emitted over the course of the program. Events normally report information about changes but we also use it as a general bus between the frontend and backend to pass commands and return data as well. This allows the frontend and backend to operate asynchronously in some cases, decouples the precise APIs from each other, and allows us to pass a single PubPen object around to handle communication instead of having to pass references to each object whose methods we wanted to invoke.
User events¶
User events return information about user objects.
-
user.cash.
update
(new_cash: int, old_cash: int)¶ Emitted when a change occurs in the amount of a user’s cash on hand
Parameters: - new_cash (int) – The amount of cash a user now has
- old_cash – The amount of cash the user had before
-
user.
login_success
(username: string)¶ Emitted when a user logs in successfully.
Parameters: username (string) – The username that successfully logged in
-
user.
login_failure
(msg: string)¶ Emitted when a login attempt fails
Parameters: msg (string) – A message explaining why the attempt failed
-
user.
info
(username: string, cash: int, location: string)¶ Emitted in response to a
query.user.info()
. This comtains all relevant information about a user.Parameters:
Ship Events¶
Ship events return information about ship objects.
-
ship.cargo.
update
(amount_left: ManifestEntry, free_space: int, filled_hold: int)¶ Emitted when a ship’s cargo manifest changes (commodities are bought and sold or transferred to a warehouse)
Parameters:
-
ship.
destinations
(destinations: list)¶ Emitted when the destinations a ship can travel to changes. This usually means that the ship has moved to a new location which has different options.
Parameters: destinations (list) – A list of strings showing where the ship can travel from here.
-
ship.equip.
update
(holdspace: int)¶ Emitted when a ship’s equipment changes
Parameters: cargo_space – The total cargo space in the ship currently has
-
ship.
info
(ship_type: string, free_space: int, filled_space: int, manifest: dict of ManifestEntry)¶ Emitted in response to a
query.ship.info()
. This contains all relevant information about a ship.Parameters:
-
ship.
moved
(new_location: string, old_location: string)¶ Emitted when a ship changes location.
Parameters:
Market Events¶
Market events carry information about a specific market to the client.
-
market.
event
(location, commodity, price, msg: string)¶ Emitted when an event occurs at a market. This is for informational purposes. The client may choose to display the message for game flavour. Once markets become stateful, this may become more useful.
Parameters: msg (string) – A message about the market
-
market.{location}.info(prices: dict)
Emitted in response to a
query.market.{location}.info()
. This carries information about prices of all commodities in a market.Parameters: prices (dict) – A mapping of commodity name to its current price
-
market.{location}.purchased(commodity: string, quantity: int)
This contains information when a user successfully purchases a commodity at a specific market.
Parameters:
-
market.{location}.sold(commodity: string, quantity: int)
This contains information when a user successfully sold a commodity at a specific market.
Parameters:
Action Events¶
Action events signal the dispatcher to perform an action on behalf of the user.
-
action.ship.
movement_attempt
(destination: string)¶ Emitted when the user requests that the ship be moved. This can trigger a
ship.moved()
orship.movement_failure()
event.Parameters: destination (string) – The location to attempt to move the ship to
-
action.user.
login_attempt
(username: string, password: string)¶ Emitted when the user submits credentials to login. This can trigger a
user.login_success()
oruser.login_failure()
event.Parameters:
-
action.user.
order
(order: magnate.ui.event_api.Order)¶ Emitted when the user requests that a commodity be bought from a market. Triggers one of
market.{location}.purchased()
,market.{location}.sold()
, oruser.order_failure()
.Parameters: order (magnate.ui.event_api.Order) – All the details necessary to buy or sell this commodity. See also
magnate.ui.event_api.Order
Query Events¶
These events are requests from the frontend for information from the backend. This could simply be to get information during initialization or it could be to resynchronize a cache of the values if it’s noticed that something is off.
-
query.cargo.
info
()¶ Emitted to retrieve a complete record of the cargoes that are being carried in a ship. This triggers a
ship.cargo()
event.
-
query.market.{location}.info()
Emitted to retrieve a complete record of commodities to buy and sell at a location.
-
query.user.
info
(username: string)¶ Emitted to retrieve a complete record of the user from the backend.
Parameters: username (string) – The user about whom to retrieve information
-
query.warehouse.{location}.info()
Emitted to retrieve a complete record of the cargoes being held in a location’s warehouse.
UI Events¶
UI events are created by a single user interface plugin for internal
communication. For instance, a menu might want to communicate that a new
window needs to be opened and populated with data. All UI events should be
namespaced under ui.[PLUGINNAME]
so as not to conflict with other plugins.
Urwid Interface¶
These are UI Events used by the Urwid interface. Urwid has its own event system but using it requires that the widget that wants to observe the event must have a reference to the widget that emits it. When dealing with a deep hierarchy of widgets it can be painful to pass these references around so the Urwid interface makes use of our pubmarine event dispatcher for some things.
-
ui.urwid.
message
(msg: string, severity=MsgType.info: magnate.ui.urwid.message_win.MsgType)¶ Emitted to have the message window display a new message.
Parameters: - msg – The message to display to the user
- severity – Optional value that tells whether the message is merely informational or informs the error of some error. The message_win will display more severe messages with special highlighting.
Data Model¶
The data in Stellar Magnate is divided into two categories: static data that defines base information and dynamic data that changes over the course of the game.
Static data is shipped in yaml files with the program. When a new Stellar Magnate game is started, the yaml files are loaded into a database to initialize the program.
The dynamic data is generated by the game as it moves along. It is persisted into separate tables in the database as the game progresses.
Static data and dynamic data reside in different tables in the database but the dynamic data can reference the static tables.
Static Data¶
Shipped format¶
Game Dependent¶
These are various records that only the game ships because there needs to be new game logic whenever something is added to them.
location_type: | enumeration of known location types. :surface: On the surface of a celestial body :orbital: Orbiting a celestial body in the system :dome: Exists under a dome because the ecosystem is otherwise non-habitable |
---|---|
planetoid_type: | enumeration of planetoid types - oceanic - gas giant - rocky - oxygen - methane |
moon_type: | enumeration of planetoid types |
commodity_type: | enumeration of known commodities. - food - metal - fuel - low bulk chemical - low bulk machine - high bulk chemical - high bulk machine |
finance_type: | enumeration of known bank types. - mafia - system - capital - planetary |
order_status_type: | |
The status of a transaction - presale - submitter - rejected - finalized |
|
ship_type: |
|
equipment: | |
ship: |
|
property: |
|
ship parts: |
|
Extendable by Server Owners¶
system: | list of stellar systems
|
---|
Someday locations will have other characteristics like:
- Orbit velocity
- Initial position
- Orbit radius
- type jump will have locations that can be jumped to
- population. Population affects the amount of commodities that a location needs or produces
- population_origin: list of stellar systems that the population originates in. Affects
- commodities for sale
- commodity consumption and production information
- Production should be of a specific commodity
- Consumption can be of a commodity or commodity_type
- These numbers should be weightings. Production weighting affects how likely (or how much) a place is to sell an item
- Consumption affects how much of an item a place is likely to take
commodity: | list of commodities that can be bought and sold :name: name of the commodity :type: Type of the commodity :mean_price: average price of the commodity :standard_deviation: How much money is there in one std deviation :depreciation_rate: How quickly the value of a product degrades :space: Volume the commodity consumes |
---|---|
financial_institution: | |
list of financial institutions in these stellar systems :name: name of the institution :type: type of institution |
Someday commodity should grow a weight attribute. Weight will affect a fee for transporting the commodity to the ship. (Perhaps. Perhaps there needs to be a way for this to be offset. Rockets are inefficient weight. Space elevators are volume constrained. Counter grav can land a whole ship but only high tech worlds and weightless environments (orbital facilities) have it?) To implement this, locations would need to grow a surface_to_orbit type
event: | list of events that can change the price of goods :msg: Description of the event. Like headline and subheading of a news article :affects: list of commodities affected by the event
|
||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
ship: | A user’s ship :type: Type of ship :mean_price: Average price of the ship :standard_deviation: How much the ship’s price fluctuates :depreciation_rate: How quickly the resale value of the ship drops :holdspace: How much space the ship has for cargo :weaponmount: How many weapons the ship can have ready to fire |
Database Schema¶
This closely mirrors the Shipped format. The static data should live in separate db tables from dynamic data and be associated via foreign keys. This allows for easier changes to the static data if an update occurs.
Tables for static data should all have _data as a suffix
Linter¶
There should be a static_data linter that does the following:
- Verifies the schema
- Checks everything for spelling
- Assembles a list of commodity types (in commodity and in event) and asks for confirmation if any of the commodity_types are unknown
- All location names must be unique within a stellar_system
- All stellar_system names must be unique within the game
- All commodity names must be unique within their system
- All types (for locations, commodities, etc) must be known to the game
Dynamic Data¶
These pieces of data are per game instance. They change as the game progresses
Database Schema¶
All records have a system created id
players: | Table of players :username: Player’s handle :display_name: If set, an alternate name the player can go by :password: hashed password that the player can use to verify themselves :cash: Amount of cash on hand |
||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
epoch: | Number of “ticks” since the game was created. Eventually a number of calculations will include the epoch |
||||||||||||||||||
bank_accounts: | Table of bank accounts that players have :bank_id: foreign key to bank :loan amount: amount on loan :loan_rate: interest rate |
||||||||||||||||||
commodity: |
|
||||||||||||||||||
order: |
|
||||||||||||||||||
manifest: |
|
||||||||||||||||||
ship: |
|
||||||||||||||||||
ship_parts: |
|
Urwid UI¶
The Urwid UI backend is a text based backend. It implements a series of static screens and menus that the user interacts with to manage their fleet.
See also
Displaying on the console uses the Urwid library
Naming Conventions¶
Toplevel UI elements in the Urwid code are called Screens. Most Screens are single area forms. The
MainScreen
is the exception to this. This complex Screen is composed of
various constant status windows displaying information to the user about their ship and
surroundings, a menubar to select options from and an area for contextual content which the user can
interact with. The widgets displaying information are calld Windows and those that display the
contextual content are called Displays.
Each Display shows the user information upon which they can act. For instance, the
MarketDisplay
allows the user to select Commodities
to buy and sell.
Sometimes a Display needs to popup a subelement that allows the user to input more information. These widgets are named Dialogs.
Mockups¶
Here there are various mockups of screens in the Urwid UI
Splash Screen¶
+------------------------+
Stellar Magnate
1.0
(c) Toshio Kuratomi
+------------------------+
Status Bar¶
This is part of the frame around the Main Window:
+=Name: Hiormi -------------- Location: Earth -+
Travel Display¶
This is simply a text menu that allows the user to choose a destination planet:
+-----------------------+
| (1) Mercury
| (2) Venus
| (3) Earth
| (4) Luna
| (5) Mars
| (6) Jupiter
| (7) Saturn
| (8) Uranus
| (9) Neptune
| (0) Pluto
|
| (!) Jump
+-----------------------+
Market Display¶
Displays Commodities
that the user can buy and sell to turn a profit:
+- Commodity -- Price - Quantity --- Hold ------ Warehouse --+
| (1) Grain $10 7.5E+200 7.5E+200 7.5E+200
| (2) Metal $100 1.7E+5 10 100
| (3) Weapons $2000 3.4E+21 1000 0
| (4) Drugs $10.1K 2.0E+10 10 0
+------------------------------------------------------------+
Cargo Order Dialog¶
The Cargo Order Dialog lets the user input quantities of a Commodity
that
they wish to buy or sell. The Dialog has a way to toggle between buying or selling the commodity.
Buy Mode:
+------------------------+
| (o) Buy ( ) Sell
| Hold: XXX Warehouse: YYY
| Total cost: $XXX
| hold/warehouse (H)/(W)
| Quantity [_____] [MAX]
+------------------------+
Sell Mode:
+------------------------+
| ( ) Buy (o) Sell
| Hold: XXX Warehouse: YYY
| Total sale: $XXX
| Quantity [_____] [MAX]
+------------------------+
Port Display¶
We will eventually have distinct types of ships to buy and sell but for the initial release we’ll just adopt the Planetary Travel style of having a spaceship that we can buy additional cargo modules for. The Port Display lets the user buy equipment (additional cargo modules, warehouse space, shipboard weapons, etc):
+- Equipment -------- Price --- Current --+
| (1) Cargo Space $10K 1000
| (2) Lasers $5000 1
| (3) Warehouse $15K 20000
+-----------------------------------------+
Equipment Order Dialog¶
The Equipment Order Dialog lets the user fill in additional information for buying equipment for their ship:
+------- Hold space - $42 ---+
| Total Sale: $0
| (o) Buy (o) Sell
| Current Amount:
| [MAX] Quantity [_____]
| [Place Order][Cancel]
+------------------------+
Info Window¶
The Info Window sits alongside the Display in the Main Screen and shows an overview of statistics about the player and ship:
+-----------------+
| Ship:
| Minnow
| Type:
| Freighter
| Free Space:
| 1000
| Cargo:
| 500
| Warehouse:
| 10000
| Transshipment:
| 10
| Bank:
| $1K
| Cash:
| $1.5Mil
| Loan:
| $0
|
+-----------------+
Financial Display¶
This allows the user to deposit money and take out loans:
+-----------------+
| (1) The Syndicate
| (2) Solarian National Bank
| (3) First Terran Bank and Trust
| (4) Mercury Savings and Loan
+-----------------+
The Syndicate: | Mob; high limit; high interest loan. Present anywhere. Deposit: low interest, 100% safe, but may be 0-50% inaccessible at any given time |
---|---|
System-wide bank: | |
May not be present on pirate world. std loan. Deposit: low interest, Safe unless war. | |
Capital planet bank: | |
Available on subset of system worlds. May open/close branches. std loan. Deposit moderate interest. Sometimes bankrupts but bailout 75-95%. War, may lose all | |
Planetary bank: | Available 1 world. low interest loan. Deposit moderate interest. Sometimes bankrupts. bailout 0-50%. War, may lose all. |
Financial Dialog¶
Allows the user to select what they want to do at the financial institution they selected:
+--------------+
| (d)eposit
| (w)ithdraw
| (i)ncrease loan
| (r)epay loan
+--------------+
Ship Update¶
These are some thoughts on how to change the Ships for later versions
Fleet¶
+------------+
| (s)hip
| (w)eapons
+------------+
Ship¶
+- (B)uy ---------------+- (S)ell -----------+
| |
| (1) Scout [x2] $1K | (1) Tug [x3] $1K
| (2) Tug [x1] $1.2K | (2) Freighter [x1] $500
| (3) Freighter $1.3K |
| (4) Cruiser $1M |
| (5) Carrier $8T |
+-----------------------+--------------------+
Purchase Ship¶
+------------------------+
| Ship Type: Scout
| Cargo: 100
| Weapon Space: 5
| Upkeep: $20K/yr
| Cost per: $1,000
| Supply 3
|
| Total cost: $XXX
| Purchase Quantity [_____] [MAX]
+------------------------+
Weapons¶
+- (B)uy----------------+--- (S)ell ------------+
| (1) Laser [x10] $1K | (1) Laser [x2] $500
+-----------------------+-----------------------+
Purchase Weapons¶
+------------------------+
| Weapon Type: Laser
| Space: 5
| Upkeep: $1K/yr
| Cost per: $1,000
| Supply: 10
|
| Total cost: $XXX
| Quantity [_____] [MAX]
+------------------------+