Link to the Internet Archive, where all files are stored
The project
Trainyard is a 2010 iOS game, from the old-school iPhone/iPod Touch era. It was really popular! Almost 25000 people created more than 162000 custom levels for the game. People are still fans to this day — every once in a while, someone finds their old iOS device and boots the game back up.
Unfortunately, the level server — the online component that allowed users to download levels — is no longer running. This means that none of the 162000 levels can be downloaded anymore. Until now!
I talked to Matt Rix, the creator of Trainyard, and he graciously gave me a copy of all 162000 user-created levels and gave me permission to carry out a preservation project for these levels.
He was also wonderful enough to, as of 2025, restore the online leaderboard. While it’s still not possible to download the levels directly to your device, you can browse them online!
This project and associated resources are all you need to bring these levels back locally on your iOS device.
The archive
Attached to this documentation, on archive.org, are the following files:
- documentation: instructions on how to restore the levels if you own a copy of the game.
- engi_puzzles.csv: the database of all levels, including level and solution data (everything needed to restore the level), creator name (pseudonymized), level title, description, difficulty, views, likes etc. There is also a link to each puzzle on trainyard.ca.
- level_images.zip: preview images for every level in the database, grouped in subfolders by level number (you’ll get it).
- extra_images.txt is a list of all levels that have preview images but are missing from the database
- missing_images.txt is a list of all levels that are in the database but are missing a preview image
- blueprint_images.zip: preview images of solutions to custom puzzles submitted to the online leaderboard. solutions to built-in puzzles are not included. there is no corresponding database or author information for these solutions. restoring these into a machine-readable form or associating them with the original level is beyond the scope of this project, but it should be doable with an image analyzer.
- missing_images.txt is a list of all blueprint images that are missing, assuming that blueprints are sequentially numbered
- trainyard_engineer_db.csv: a special form of the level database that is compatible with the way Trainyard stores levels internally.
- creator_id_name.csv: a list of creator IDs and their (pseudonymized) names.
- examples: a folder of example level database files, if you want to directly transfer levels to the device using method 2 below.
Bringing the levels back
What you need:
- an old iPhone, iPod Touch, or iPad with iOS 8.x or lower. I got mine from Elite Obsolete Electronics but there are many sellers.
- the full Trainyard game (not Trainyard Express). If you already have an old device with it installed, you’re golden. While it’s no longer possible to buy the game, you can re-download it from the iTunes store, even on very old devices (as of 2025), if you have access to the Apple ID that you used to buy the game. dead link to iTunes
Method 1: redraw the levels
This is the easiest way. Look through engi_puzzles.csv for puzzles with names or descriptions (or popularity) that you find interesting. Then look up the corresponding image in level_images.zip and go into the level editor and re-create the level. Easy!
For the 80-something levels that are missing a preview image, you’ll need to read the level off the puzzleString
column directly. (It’s an advanced procedure, but I have provided instructions below.)
Method 2: import levels into the database
This method assumes you know your way around iOS, and that your device is jailbroken.
Prerequisites:
- jailbroken iOS device with iOS 8 or lower
- working SSH on your iOS device and a way to transfer files to and from it, for example using WinSCP.
- file manager on your device, such as Filza from Cydia
- sqlite editor, such as DB Browser for SQLite
Get the necessary files:
- First, we will find where the level database is. You’ll need the UUID of the Trainyard app installation, which is different for everyone. Using Filza, go to Favorites (⭐ icon at the bottom) → Apps manager (not Applications) → Trainyard → info (ℹ️) icon → Data. If you look at the top, you will see a UUID such as
70244475-484A-4D2E-AC7B-F9D808C3FC21
(yours will be different). Keep in mind that there are two UUIDs; you don’t want the bundle, you want the data. - Open your SCP browser, log into your iOS device using the
mobile
user, and navigate to/private/var/mobile/Containers/Data/Application/<UUID>/
, where<UUID>
is the UUID you found in step 1. - You’re going to be downloading two files:
Documents/trainyardEngineer.db
(this contains the levels that are stored on the device) andLibrary/Application Support/creators.plist
(this contains the list of creator IDs for the creators of the downloaded levels).
Add your levels to the database:
- Make a backup of
trainyardEngineer.db
andcreators.plist
before you edit them. - Open
trainyard_engineer_db.csv
and pick some of the levels that you like. I made an example in examples/selected_levels.csv: these are my levels (they’re pretty good!). OpentrainyardEngineer.db
with SQLite Browser and simply copy the corresponding rows over. The schemas are already compatible, so you don’t need to worry about that. But you do need to ensure that theid
column is still unique. And clear out anyNULL
s to be empty strings instead (backspace on SQLite Browser). Write your changes to the database (File → Write Changes in SQLite Browser, which will overwrite the file). - Now edit
creators.plist
to add the creator IDs and creator names for all the creators whose levels you added intrainyardEngineer.db
. You’ll find the information increator_id_name.csv
. This step is optional, but it’s good to give credit.
I have included example trainyardEngineer.db
and creators.plist
that are tested and working, so you get an idea of how the files should look like after you’re all done.
Now upload the files back to your iOS device:
- Force quit Trainyard.
- Copy the
trainyardEngineer.db
andcreators.plist
files to their original location. - Open Trainyard and go to Saved Puzzles. You’ll see the levels you just added.
Hooray! Enjoy the puzzles, and don’t forget to play my levels too!
Side project: reverse-engineering the puzzleString
The most important piece of information in the level database is the puzzleString
, which encodes the level itself. In this side project, I will be reverse-engineering this string and show you how to reconstruct a level just by looking at the puzzleString
.
Components
I first created a number of test puzzles that I could compare with their string. By looking at them, I was able to deduce the following:
- every puzzle starts with the string
hh
- pieces are arranged on the 7x7 grid in normal English reading order (left-to-right, top-to-bottom).
- every piece is represented by an uppercase letter. There are 5 piece types:
- Blank: one or more digits
0
-9
- Rock:
R
- Outlet:
O
followed by several letters that indicate the orientation and contents - Goal:
G
followed by several letters that indicate the orientation and contents - Painter:
P
followed by two letters that indicate the orientation and color - Splitter:
S
followed by one letter that indicates the orientation
- Blank: one or more digits
The Blank
The blank is simply an empty space between pieces where the player can place tracks.
Blank spaces between pieces are represented by digits 0
-9
. 1
means 1 blank, 2
means 2 blanks, etc. 0
means 10 blanks. If there are more than 10 consecutive blanks, the numbers are concatenated, e.g. 08
represents 18 blank spaces, 003
represents 23 blank spaces, and 0000
represents 40 blank spaces.
Edge case: if the blanks are at the end, only the zeroes are kept. For example, if there are 24 blanks at the end of the puzzle, instead of 004
, the puzzleString will end with 00
. If there are 9 or fewer consecutive blanks at the end, the blanks will be omitted.
The Rock
A rock is an obstacle that doesn’t do anything except prevent the player from placing tracks.
There’s not much to say about this one. If there’s an R
, there’s a rock.
The Outlet
One or more trains emerge from an outlet in a single direction, in a defined color sequence.
The outlet is a variable-length string, for example Osa
, OuzJ
, or OydHpQ
.
I quickly deduced that the first letter indicates the number N of emerging trains and the orientation:
1 train | 2 trains | 3 trains | 4 trains | 5 trains | 6 trains | 7 trains | 8 trains | 9 trains | |
---|---|---|---|---|---|---|---|---|---|
North | a | b | c | d | e | f | g | h | i |
East | j | k | l | m | n | o | p | q | r |
South | s | t | u | v | w | x | y | z | A |
West | B | C | D | E | F | G | H | I | J |
By looking at how the length of the string changes based on the number of trains, I also figured out that the next ceil(N/2) letters encode the train colors (groups of 2 in one letter, essentially).
But how are the train colors encoded? By staring at the test patterns more, it became obvious. There’s a table of letters, and you look up the first and 2nd train in this table:
2nd: RED | 2nd: YLW | 2nd: BLU | 2nd: ORG | 2nd: GRN | 2nd: PUR | 2nd: BRN | |
---|---|---|---|---|---|---|---|
1st: RED | a | b | c | d | e | f | g |
1st: YLW | h | i | j | k | l | m | n |
1st: BLU | o | p | q | r | s | t | u |
1st: ORG | v | w | x | y | z | A | B |
1st: GRN | C | D | E | F | G | H | I |
1st: PUR | J | K | L | M | N | O | P |
1st: BRN | Q | R | S | T | U | V | W |
For example, “s” means a Blue train followed by a Green train.
If there is an odd number of trains, the last (nonexistent) train is Red.
Let’s try a real piece: OybiiQ
O
: this is an outlet.y
: South-facing, 7 trains.b
: Red train, Yellow train.i
: Yellow train, Yellow train.i
: Yellow train, Yellow train.Q
: Brown train, Red train. (There’s only 7 trains, so the last train doesn’t matter). Conclusion: South-facing outlet, with trains: RED YLW YLW YLW YLW YLW BRN.
Looking at the screenshot, we can confirm this:
The
OybiiQ
piece
There’s a subtle point here: O
can mean “outlet”, but it can also mean “Purple train, Purple train”. Which one it is can be determined contextually: the number of characters in an outlet piece is deterministic, determined by the 2nd character (which encodes the orientation and number of trains).
The Goal
One or more trains arrive at a goal; the goal accepts trains from one or more directions; the trains must enter the goal in a defined color sequence.
The goal is slightly different from the outlet. It’s also a variable-length string: GiaC
, Gefaiq
, etc.
The first letter encodes the orientation of the piece (which of the N, S, E, W sides accept trains):
_ | N | E | NE | |
---|---|---|---|---|
_ | invalid | b | c | d |
S | e | f | g | h |
W | i | j | k | l |
SW | m | n | o | p |
For example, l
indicates a goal that accepts trains from the North, East, and West. Note that it is not possible for a goal to not accept trains.
The second letter encodes how many trains (N) the goal accepts: a
= 1 train, b
= 2 trains, …, i
= 9 trains.
The next ceil(N/2) letters encode the train colors, using the same table as the outlet.
Let’s try a real piece: GpgbrHQ
G
: this is a goal.p
: this goal accepts trains from any direction (North, East, South, and West).g
: 7 trains.b
: Red train, Yellow train.r
: Blue train, Orange train.H
: Green train, Purple train.Q
: Brown train, Red train. (There’s only 7 trains, so the last train doesn’t matter.)
And, once again, we can confirm that this is correct.
The Painter
A painter changes a train’s color; it accepts trains from exactly 2 directions: a train enters the painter from either direction and exits out the other direction.
The painter is a fixed-length string: Pax
, Pbx
, Peh
etc.
The first letter encodes the color of the painter, similar to the outlet color table: a
is Red, b
is Yellow, c
is Blue, d
is Orange, e
is Green, f
is Purple, and g
is Brown.
The second letter encodes the orientation of the painter:
N | E | S | W | |
---|---|---|---|---|
N | invalid | b | c | d |
E | h | invalid | j | k |
S | o | p | invalid | r |
W | v | w | x | invalid |
The two different encodings indicate which side was last edited in the puzzle creator: rows were edited first, columns were edited 2nd. Gameplay-wise, these two are the same: the painter is a bidirectional piece. This is needed for the editor, so that when you tap on various directions the editor will keep the most recent direction you chose and will change the older one.
Let’s try a real piece: Pax
P
: this is a painter.a
: the painter is Red.x
: the painter accepts trains from the West and the South. (In the editor, the puzzle creator tapped on West first, and then on South.)
The Splitter
A train enters a splitter from the given direction; it will then be split into two trains (and, if it’s a composite color, the train’s color will be split into its primary components) which exit perpendicularly from the direction of entry.
The splitter’s encoding is simple. It is one of the following strings: Pa
, Pb
, Pc
, Pd
.
Pa
: trains enter from the NorthPb
: trains enter from the EastPc
: trains enter from the SouthPd
: trains enter from the West
Real examples
Let’s try one big example to make sure we got everything. Here’s one of the featured puzzles, called “Backup”. Let’s see if we can encode it by hand.
Medium-difficulty puzzle “Backup” by Superjustinbros.
(Try it yourself!)
Solution:
- Beginning of puzzle:
hh
- 3 blanks:
3
- Goal, facing west, 1 orange train:
Giav
- 6 blanks:
6
- Goal, facing west, 1 orange train:
Giav
- 3 blanks:
3
- Splitter, facing east:
Sb
- 5 blanks:
5
- Rock:
R
- Goal, facing north, 2 blue trains:
Gbbq
- Rock:
R
- 4 blanks:
4
- Outlet, facing west, 2 trains (red and yellow):
OCb
- 7 blanks:
7
- Outlet, facing north, 1 blue train:
Oao
- 2 rocks:
RR
- 6 blanks:
6
- Rock:
R
- Blank:
1
- Outlet, facing north, 1 blue train:
Oao
- 2 blanks: nothing, because it’s the end of the puzzle and there are less than 10 blanks
Reverse-engineered puzzleString: hh3Giav6Giav3Sb5RGbbqR4OCb7OaoRR6R1Oao
Actual puzzleString: hh3Giav6Giav3Sb5RGbbqR4OCb7OaoRR6R1Oao
Yay! We got it right! This allows us to reconstruct the entire level just from the puzzleString
, or create a puzzleString
from a level image.
Acknowledgements
Thank you to Matt Rix for creating this incredible game. I have many fond memories of playing it back in the day.
Doubly thank you to Matt for providing me with the levels database and trusting me to carry out this restoration. This is my fifth media preservation project, and I am proud to have been part of it. Onwards and upwards!