[Technical Devlog] Timely Transports
Welcome to the technical devlog for Timely Transports
Timely Transports is the first ever hybrid board + smartphone game. (I, still, need to come up with a catchy short name for these types of games. If you have any good ideas, let me know.)
Here I'll talk briefly about how I created the game interface (for on your smartphone), but spent a lot of time explaining how I created a tool that randomly generates a game board (to print and play on).
If you haven't played the game (or checked out the page), be sure to do so! Not only is it a lot of fun, this article also won't make sense otherwise.
I just said this was the "first ever hybrid" game, and that might have raised some question marks for some boardgame-savvy readers.
"Some boardgames already have a "companion app" you can download and use! You are not the first, Pandaqi, stop telling lies!"
Perhaps surprisingly, I don't like those. Why not?
They create an extra setup step. Someone needs to find, download and install a whole app (which also works on their particular smartphone). Unless you always play with the exact same group, you might need to do this multiple times because nobody has the app yet.
App stores are annoying. They update all their terms of service all the time, and they only need to think that you're doing something wrong, and your app is immediately gone. Just gone. (I have too much personal experience with this.) And I'm not even mentioning actual updates to these stores/platforms, which could render apps unusable within a year or so.
They aren't easily updated. Again, you need to bundle the app, go through the update process at your store, and then force players to update the app.
No, instead of an app, I wanted it to be a website.
I tried to use only the most basic website code, to optimize support for all sorts of browsers and old phones.
Whenever I need to fix a bug, or finetune some details in the algorithm, or have an update to the game, I can just update the website. The next time you play -- the next time you visit the website -- it's automatically the latest version!
And people can just create a backup of these games. You can just download the website (don't forget to download the external JS and CSS files as well!). If my server ever goes down, or I somehow stop supporting the games, you can still play them and even improve them yourself.
The web page
I struggled with the layout of the web page a lot. It needed to do all these things:
Explain the game to new players/someone who just stumbled upon the website. (Also, make them enthusiastic about it. Just make everything look nice and clear.)
Give a short guide to using the game interface, plus settings, and a clear button to press.
Give a short guide to using the board generator, plus settings, and a clear button to press.
Below is an image of the current webpage (in case you can't visit it now, or it has changed in the meantime):
At first, I had one settings menu at the top, which influenced both the game interface and the board generator. Why? Because they had many overlapping settings (such as number of players and difficulty).
In the end, however, this just didn't work. The settings menu became way too large, because at least 50% of the settings did not overlap. Also, it's intimidating to load a page and immediately see a bunch of settings you need to understand. And about 50% of the people will then just ignore this and scroll straight down, never even seeing those settings.
So, I decided to start with the marketing pitch (as you should always do): a nice header image, clear tagline for the game, big old download button, and a slightly longer explanation underneath.
Then I get the game interface, which has a short explanation, then its specific settings, and then the button.
Again, at first I tried to make the interface work on the same page, but that was just asking for trouble. Now the button leads you to a new page which only contains the game interface and nothing else. Way cleaner, way easier to get a full screen game interface.
Lastly, you get the board generation, with a short explanation, settings and then the button.
I considered automatically generating a board, to impress new visitors and immediately give them something to print. But that made the page laggy on old smartphones, as it took a few seconds to create a board.
This order is important! You only need to print a game board once, so that part of the page will rarely be visited. Most players will need the game interface, so that needed to be on top.
The Game Interface
We can be short about this one, as there's really nothing special going on here.
Once loaded, it retrieves your settings from that same localStorage, and starts the game using that configuration.
The game is just a bunch of vehicle icons which listen for taps/presses/clicks. If you do so, it starts a timer. If you do so when a timer is running, it stops it (but only if the timer is in "overtime", not while you're still waiting for it to finish).
Similarly, you can tap the score text to get +1 point. (Players need to keep track of their own score, which so far has been no problem at all. Especially because everyone can see each other's phones all the time, so cheating is not an option.)
And similarly (again), new messages and events pop up, which get a click-event (to make it go away) and a timer (to make sure you click it soon enough).
The reasons for these decisions are all explained in detail in my regular devlog: [Devlog] Timely Transports. This is just about the technical implementation!
Sure, there were some nasty issues to solve (such as different screen sizes, where to place the upgrade button, how to make a dynamic timer bar that responds to changes in length and color), but that's not so interesting. It's what I call stumbling through computer-ness, as you slowly realize some stupid decisions that your specific framework/programming language/screen resolution takes.
If you want, you can check the code itself, it's quite short and simple for a complete working game interface.
(I make no apologies for code messiness :p The code is quite clean, but I'm still learning the best practices when making these types of games, so I'm surely doing things in a way that is needlessly complicated.)
Instead, let's continue to the real beast!
Random Board Generation
When I got the idea of "randomly generating a game board you can print", I was wildly enthusiastic ... and also too optimistic.
I thought: "well, I've done random generation before, surely I can just randomly place stuff according to some rules, connect them, and voila -- we have a game board!"
No. No, you cannot.
Within one hour of building this project, I realized how wrong this assumption was. I had a working generator that randomly placed cities and connected them with a straight line ... and it looked awful and was too chaotic to play on.
What are the issues here?
First of all, there's the issue of clarity. You can't "zoom in" or "rotate the camera" on a printed game board, like you would in a computer game. Everything needs to be clearly visible and legible, separated by enough space, no matter the layout of the map.
Second of all, there's the issue of completeness. When you play this game, everyone has four vehicles and there are four goods (in total) to move around. With a completely random generation, it's quite possible that some of those elements don't even appear on the map! Which is terrible, because it makes the game unbalanced or straight-up impossible. Instead, you need to ensure there is always at least one route for each vehicle and one city that requires a certain good.
Lastly, there's the issue of fairness. Each player has a capital, which is where they start and also the only place where new vehicles can enter the board. If your capital is cut off from the rest of the world ... well, you're going to have a shitty game. Instead, you need to keep some score that represents how "good" a capital is, and ensure that capitals are roughly equal in score.
So, how did we solve these issues?
Everybody make some noise!
Step 1, as always, uses noise to generate a random landscape. I started with Perlin Noise, but switched to Simplex Noise as it gives better results on a grid.
(Why? Perlin Noise will always yield a noise value of 0 if the coordinates you enter are integers. In practice, you get a map with lots of completely straight vertical/horizontal lines, which is ugly.)
I loop through all grid cells, take the average of the noise value of the four corners, and save that as the definitive value.
(Why? After many experiments, I found this to be the best way to capture the roughness of continuous noise on a grid. If I just sample one noise value -- say, the center of the grid square -- there's a good chance two squares next to each other both sample the value "0", whilst completely missing a range of super high values between them.)
This approach does smooth the noise, as you're taking weighted averages all the time. Even though the noise takes values between -1 and 1, after averaging the extreme values were usually -0.7 to 0.7.
But that's fine!
Now that I have a grid of values, I convert it to a terrain. After some trial-and-error, the water line became -0.4. (Everything below that is water, everything above is land.) But you can raise this value, for example, and get a huge sea with some tiny islands in it.
(This image already has paths between cities, which I'll discuss soon. I just didn't have any earlier screenshots of the progress, sorry. And yes, the colors are ugly, that's how it always starts :p)
This step confirmed a famous quote from the game industry (I'm paraphrasing):
"Whenever you write a new feature, start with the absolute dumbest and most naïve approach possible. It will often turn out to be the right one and save you a lot of time."
A year ago, I worked on a "pirate game" that needed to know the boundaries of islands. (This project eventually went nowhere, but it taught me everything I use these days in my newer projects. To prove this, it will soon pop up again!)
I copied this code and changed it to get the boundaries of lakes instead.
How does it work? Really simple: it loops through all water tiles and checks the neighbours (top, right, bottom, left). If any of the neighbours is a land tile, then this water tile must be at the edge!
Why do I need the boundaries of lakes? Because traditionally, and especially in the jungle area (in which this game is placed), people placed settlements near running water. I want to do that as well!
The city placement algorithm now became very simple:
Pick a random tile at the edge of water.
Check if any other city is nearby (I don't want cities too close to each other)
If so, retry with a new tile
If not, continue
Place the city on this tile, disallow all tiles within a certain radius.
Below is the code for this whole algorithm.
This was the moment I realized why the approach above worked so well: it allowed me to create both routes over land and over sea, because each city was on the bridge between both worlds.
This is an overview of the algorithm:
Go through all cities.
Check all cities that have not created their connections yet.
Check path over water.
If it doesn't exist, check path over land.
Once we have all possible paths, sort them based on distance.
Remove those that are too long, then pick random from the available ones.
(If at the end of generation, a city has no connections, give it an airport => this will change in later versions, as you can probably already see the flaw in this reasoning.)
If you do this, you automatically get a collection of paths over land and over sea, like this:
Again, this is a map from a later stage of development. (I should really learn to take screenshots that better suit the devlog.)
Side note: in the previous section I talked about creating a list of "water edge tiles". I used this list for another very important thing as well! I used it to create an outline for each body of water. If you look closely at the image, you'll see that each lake has a thick blue border. This makes a huge difference in the clarity of the map.
But, you might ask, how do you find the path between two cities?
Aha! The "pirate game" returns! In that game, I needed to very quickly find the shortest route between all trading harbours in the game ... on a regular basis. There were usually 10-20 harbours, which needed to update their routes roughly once every 5 turns.
As such, my pathfinding algorithm had to be fast and customizable.
That's why I wrote my own pathfinding script:
It uses A*
But on a grid => all nodes are automatically connected to all neighbours (top, right, bottom, left)
With some space where I can input very specific changes/requirements/additions
I won't explain the first two parts, as there are countless videos and Wikipedia pages on that. (And I don't even think my implementation is that good, I only know it's fast and does exactly what I want.)
But the third part will benefit from some examples.
Issue #1: if I create paths between random cities, there's a high chance these will nearly overlap or run side by side for a few tiles. This looks ugly. In real life, such paths would be "merged" with an intersection.
So, in my pathfinding algorithm, I said the following: "if this neighbour already has a path of the same type as you, give it an extremely low weight."
In other words, make that connection so cheap, that the algorithm will probably take it. This way, routes "snap" to existing routes of the same type.
(This isn't perfect, but it solves 99% of the cases.)
Issue #2: If I create a path over land ... the shortest path will almost always be one that goes along the shoreline. The water is the obstacle. The shortest path around the water is by literally following the edge of the water. This looks ugly.
Instead, I want paths over land to stay near the middle of the land, and paths over water to stay near the center of the water.
To do so, I said the following: "when creating a path over land, give nodes with a higher noise value a lower weight" (For water paths, the inverse holds.)
Small changes, but vastly improved results. (All because of that unfinished pirate game!)
Okay, so we have connections between cities which are as short/optimal as we can reasonably expect.
How do we make them look nice? I created an image for each road type (water, car, train) containing 4 frames. Each frame was one of the possible configurations such a road could have.
Then I replaced my rectangles (from previous images) with this spritesheet, checked which of our neighbours had the same path type, and chose the correct frame and rotation.
This is a monstrous piece of code which I'm not going to show. There are probably better ways.
(I basically run through all neighbours and track the longest sequence of existing neighbours. If I have three subsequent neighbour connections, starting from the left, I know I need a 3-way connection which is rotated 180 degrees. Yeah, it was annoying to write. But once it works, it looks really nice!)
This was also the point where I chose the nicer colors. (I wanted it to give a more "faded" look, instead of bright colors.) I created a quick image for the cities, and saw ... that the map was quite empty and most roads looked boring. How do we solve this?
Pathfinding is interesting. It's an algorithm that becomes faster and more useful, when you have a world that is more filled and more complex. It's one of the few algorithms to do that -- most get slower and more unwieldy when there's more stuff.
So, I created a second noise map, and used this to place forests around the world. That's how I get these "blobs" of forest, which looks significantly better than just placing random trees all over the map.
I also looped through all tiles again to determine random tree types for each forest. It is, again, a quite naïve approach that works:
"Hey, I am a tree, and there's another tree next to me! Let's set my type to the type of that other tree!"
This way, each connected set of trees will eventually have an identical type.
With trees in the world, my pathfinding algorithm had way less space, which means roads snapped together more often and they created more interesting paths. (Of course, I added the exception in the algorithm that forest tiles are completely forbidden.)
After some experimentation, I realized there weren't really any restrictions on this part of the algorithm. Yes, I could try to write something that spaces out goods (so no two cities close to each other want the same thing), but that would actually make the game ... easier and more boring. Whatever good you wanted to deliver, there would always be a city within very close range.
Instead, I allowed the algorithm to be completely free. Sometimes you need to travel half the world to deliver a fruit, and that's when things get interesting: will you take the risk? Are those 2 points worth it? Or do you wait and hope for something better to come along?
I only implemented the following restriction: each good must appear at least twice.
Initialize an empty list
Go through the list of goods, add each of them to the list TWICE
Fill the remaining space with random goods.
Shuffle ( = randomize) this list
Then I loop through the cities in order, giving each city one good at a time, until the list is empty.
Two issues remain:
What is the "remaining space"? At first I wanted 2*\<number of cities> goods, because I thought that was a good idea. It wasn't. It made the board way too full, hard to read, but easy to play (because each city wanted many goods). Instead, it now averages 1.5 good per city, which means roughly 50% will only want one type of good, and the other 50% want two types of goods.
Don't allow the same good to be added on a city twice! It's quite stupid if a city wants bees for 4 points ... and bees for 5 points. So, before adding the good, I check if it's already there. If so, I just continue to the next city.
Below is the code for this algorithm.
(I don't know if I need to say these, but these code samples are usually about 80% of the actual code. I leave out the uninteresting bits, or parts that might be confusing, and some things are just a function call (like "cityHasRoomForGood()" - I think everyone understands what that function is doing)
With all elements in place, there was another issue with space. (In my regular devlog I also keep talking about how I ran out of space. That's the real issue here.)
Each city had the following elements:
A big icon (to recognize the city and whose it is)
A big city name
One or two wanted goods ( + a custom price)
An airport (optional)
That's a lot to display! I spent quite some time moving stuff around (as you can also see in my screenshots).
For example, airports were first an icon displayed next to the city name. But that's very hard to miss (and it can overlap with neighbouring cities). Similarly, displaying the goods above the city name also allowed the situation where two cities overlapped each other.
I eventually ended with the following setup:
The city icon is literally the player shape, so it's hard to miss
The city name is underneath the city.
The goods are above the city.
If there's an airport, it becomes a thick underline (with a "wings pattern") underneath the city name.
Because all elements are spaced around the city, and cities are at least X squares apart (as mandated by my generation algorithm), it (almost) never happens that something overlaps.
One final problem remained: where should the airports go?
Currently, they were placed on any city without connection, and then randomly on some other cities (with 20% probability or so). But that didn't fix anything.
Instead, once generation is done, we usually end up with several "groups" of connected cities. See the image below: all cities on the left are their own group, cut off from the cities on the right. (And that's why the generator placed an airport on Bisto and Camor.)
To always connect these, without fail, I needed one airport per "connection group".
Thus, when adding connections, I kept track of a "connectionGroup" variable on each city. When you connect with another city that already has a group, you copy it. If that doesn't happen, you start a new group yourself.
This code is not perfect. It sometimes happens, because of a weird order in which cities/connections are evaluated, that one or two cities are considered their own group (when they are clearly not).
However, this is acceptable, because in the worst-case scenario, this means we have 1 or 2 airports too many. As long as we have enough airports to connect any city to any other city, I'm fine with it.
(Due to this "overshooting" of the desired target, I do not place any extra airports anymore, because most of the maps already have those built in.)
Solving the Boardgame Issues (CCF)
Remember those three issues I described at the start? Clarity, Completeness and Fairness? Despite all my best efforts so far, it's almost impossible to eradicate these issues just by writing a strong random generation algorithm.
As I've learned, you need fail-safes. Pieces of code ("exceptions") that only trigger when a board is just too bad to continue.
During playtesting, I found that cities without any connections were very easy to miss. They weren't used at all, because nobody realized they existed. Plopping down an airport did not help at all.
So, when a city cannot find any connection, I simply move it completely to a new position.
(This initially brought some issues, with cities being placed directly on top of existing cities, and such. But that was easily solved by checking the distance to all other cities, and only placing something if that distance is greater than, say, 3 tiles.)
Once the full generation is done (all cities and connections placed), I check if there is at least one path of each type in the game (road, boat, train, plane).
If not, I try to brute force it into the game. I go through all cities, check all other cities (with which a city isn't already connected), until I find a valid path of the wanted type.
This almost never fails ... unless such a path simply doesn't exist, which is most likely on the lowest player counts, as the board is way smaller then.
What do we do? We use the age-old trick of ... starting all over again! The generation code is inside a while loop, which will keep running as long as there is something that triggers a "generation failed" state.
(This can also happen, for example, if we run out of water edge locations, because that means we cannot place or move cities anymore. Again, most likely on the smaller maps.)
Important remark: this is also why I always split the code into a generation and visualization part.
I could already start drawing the cities, icons, connections, terrain during generation ... but then I'd have to remove or change all of that when the generation fails. Or when I decide to add a feature later on that requires more flexible visuals.
Instead, I generate everything and save the results in some nice lists. Only once I'm sure generation is done and successful, I actually go through the lists and draw everything in the right way.
This was something I first noticed during playtesting, and did not think about at all when I was designing the original game.
I was playing a two player game. My capital had only one outgoing connection and two steps until I could reach an airport. The other player had a capital with five outgoing connections and an airport on the city itself.
That wasn't a fair board. At all.
So I had two options:
Somehow ensure all capitals have an equal number of connections, wanted goods, airport/no airport, etcetera
Somehow calculate a score for capitals and then give the lowest scoring cities a bonus at the start of the game (of a few points)
The first one quickly proved infeasible. It was hard enough generating working, playable, good-looking maps as it is. Checking if all capitals were equal basically meant the algorithm threw its results away hundreds of times before giving an answer (you know, that other fail-safe I described above), which took way too long.
On to the second solution! I calculated these factors (weighted by their importance):
Size of connection group
Number of connections
Value of goods (that can be delivered here)
First, I sort the list to get the highest scoring capital. (Because, for all these factors, larger is better.)
Then, for each capital, I take the difference with this maximum value, divide that by 3, and give that as the bonus.
(For example, the best city has score 26. Another city has 19. Then it gets a (26-19) / 3 = 7 / 3 = 4 point bonus at the start of the game.)
It's not a perfect solution, but the computer usually finds the worst cities and gives them a bonus that, in my experience, levels the playing field.
There were two ugly bits left at this point.
Paths were "snapping" to each other so well, that all maps had almost exclusively overlapping paths: each path was both a regular road (for jeeps) and a railroad (for trains). This made the game way too easy, as you could get anywhere with both of these vehicles.
So, in the pathfinder algorithm, I only allow "crossover" (or "snapping") with a certain probability (somewhere near 5%). If that check fails, the path is not allowed at all to merge with an existing path.
(It's a bit of a hacky way to do this, creating a pathfinding algorithm based on chance, but it works really well.)
The second problem were these "ugly boxes" (as I've come to call them in the code). Parts of the map where these useless rectangles appeared (see top center, around Camorr):
There's no reason for these paths to create such rectangles -- it wouldn't happen on a real map -- so I tried to prevent these as much as possible.
How? By sweeping over the whole map (all tiles, one by one), checking the neighbours for such an ugly rectangle, and then checking if one of the squares can be removed.
When can a square be removed? If it has no connections outside of this ugly box (so no outgoing connections). This means I can just remove the square and all cities should still be connected.
Now, this code is also way longer than you'd like, but I really saw no better way to do this.
This does not remove all of them. For that, I'd need a smarter algorithm, or multiple sweeps (at least 5 or 10). But this is fine for now, because I rarely get these ugly boxes anymore.
So, that is how I created a tool that randomly generates a board you can actually use (when printed) in a board game. I hope it was interesting and that you learned something from the problems and how I tackled them.
If you have any questions, or you've played the game and want to give feedback, just let me know!
I'm very happy with the results. It works better than I expected (especially for a first try) and it looks better than I could have hoped (given that computers don't have a sense of visual design and gameboards are usually meticulously handcrafted).
I'll surely continue creating these kinds of tools and games. In fact, I have a few ideas and tricks up my sleeve that might be even more awesome ...
(I don't want to spoil too much, because it's still in early stages and I'm not quite certain if it will work, but you can think of it as "random generation for board games ... on steroids!")
This game heavily relies on a grid, some noise and pathfinding. I've done this a few times now, so I want to try something else! The next games will probably feature different methods of random world/map generation.
Thanks for reading, until the next devlog,