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:

  1. 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.
  2. 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.
  3. You’re going to be downloading two files: Documents/trainyardEngineer.db (this contains the levels that are stored on the device) and Library/Application Support/creators.plist (this contains the list of creator IDs for the creators of the downloaded levels).

Add your levels to the database:

  1. Make a backup of trainyardEngineer.db and creators.plist before you edit them.
  2. 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!). Open trainyardEngineer.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 the id column is still unique. And clear out any NULLs 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).
  3. Now edit creators.plist to add the creator IDs and creator names for all the creators whose levels you added in trainyardEngineer.db. You’ll find the information in creator_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:

  1. Force quit Trainyard.
  2. Copy the trainyardEngineer.db and creators.plist files to their original location.
  3. 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:

  1. every puzzle starts with the string hh
  2. pieces are arranged on the 7x7 grid in normal English reading order (left-to-right, top-to-bottom).
  3. every piece is represented by an uppercase letter. There are 5 piece types:
    1. Blank: one or more digits 0-9
    2. Rock: R
    3. Outlet: O followed by several letters that indicate the orientation and contents
    4. Goal: G followed by several letters that indicate the orientation and contents
    5. Painter: P followed by two letters that indicate the orientation and color
    6. Splitter: S followed by one letter that indicates the orientation

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 train2 trains3 trains4 trains5 trains6 trains7 trains8 trains9 trains
Northabcdefghi
Eastjklmnopqr
SouthstuvwxyzA
WestBCDEFGHIJ

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: RED2nd: YLW2nd: BLU2nd: ORG2nd: GRN2nd: PUR2nd: BRN
1st: REDabcdefg
1st: YLWhijklmn
1st: BLUopqrstu
1st: ORGvwxyzAB
1st: GRNCDEFGHI
1st: PURJKLMNOP
1st: BRNQRSTUVW

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):

_NENE
_invalidbcd
Sefgh
Wijkl
SWmnop

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:

NESW
Ninvalidbcd
Ehinvalidjk
Sopinvalidr
Wvwxinvalid

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 North
  • Pb: trains enter from the East
  • Pc: trains enter from the South
  • Pd: 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!