Categories
General Uncategorized

Dark Patterns: Buying a Bahncard at Deutsche Bahn

How do two well liked companies, deutsche Bahn and adobe, advertise their subscriptions? Badly.

Deutsche Bahn, the fully state-owned railway company, is a well-liked company running most major long-distance railways in Germany. So, it will come as a surprise to many that I have some complaints about them. Today, however, I won’t talk about the numerous delays, cancellations and frankly bad service interactions I had with them in the past. Today is about dark patterns that cost me 500€ for nothing.

I apologise in advance for using mostly German material, as it is the primary language. I thought it wasimportant to use it. Also, DB manages to make the English part even worse by breaking their reference to additional material (see header image). Also, if your sarcasm detector is broken, you might have trouble with some sections.

A dark pattern is a design mechanism to get people to buy, do or allow things they don’t actually want. Examples include: Forced continuity of free trials with paid subscriptions, hiding the true costs of a service, and many, many more.

These are especially prevalent with subscriptions, so it might not come as a surprise that we will look at subscriptions today. At first, let’s look at two other well-liked and respected companies: Adobe and Deutsche Telekom. Both make use of subscriptions: One for an ongoing connection to the internet and one for software. This is how they are advertised on their website.

Both note prices in Currency Per Timeframe: €/month or €/year. Telekom even includes some discounts: 9.95€/month and in smaller font: 43.95€/month starting with the 4th month! Adobe clearly states they are selling a monthly subscription (“Monats-Abo”) or a yearly subscription, but paid monthly. There is no misunderstanding. (Note: Adobe is highly criticised for its subscription practices.)

Based on these examples, let’s see how Deutsche Bahn advertises their BahnCards. A BahnCard is a discount offer; it is not a product or service in itself, but rather allows you to get discounts. They exist on various levels: Normal and Business, 25/50/100, Probe or Normal, first or second class. I’ll limit this to the first class BahnCard 50, as that’s the one I got and the Probe version, which is only valid for 3-month period.

A Bahncard 50 gives you a 50% discount on regular fares (which are rarely paid by most casual users) and a 25% discount on discounted tickets. After using it for a year, I’d say it is a very bad deal for most casual travellers, but that’s beside the point. I bought mine on sale (50% off), but those are not available at the time of screenshotting.

The “weitere Informationen” link lets us see additional details about this order and an FAQ section with important information.

The details page does not contain a mention of a subscription. It mentions cancellation.

It even contains a neat question about the cost of the BahnCard 50! That’s nice. 492 Euro for the first-class card. We knew that, it was clearly advertised! Silly. We can even link to this section specifically.

Even the “How much does it cost” FAQ entry does not mention a subscription.

So: Is this a subscription? Yes, of course it is! The Probe Bahncard page actually mentions this.

As well as the sales process, once you hit “jetzt bestellen” (buy now).

Deutsche Bahn states here that it is a subscription. This is the clearest mention of the subscription, but it does not indicate the prices are recurrent prices and only states it in text.

In case you missed it, it is the text in the centre of the screen, you won’t see the word “Abonnement” again. I used a fake account to generate some screenshots of the current sales process, but Deutsche Bahn changes it around every now and then, so neither can I be sure it looked like this when I bought my Bahncard, nor is it guaranteed to look like this when you go through the process.

I have some objections to the layout of this page. In my opinion, this text is designed to make people not take notice of the subscription. No mention of the timeframe or recurrent nature of the price. For completion: The probe Bahncard screen does not mention the price of the full Bahncard (492€) at all, only the probe price.

Let’s head on to the final sales screen; the intermediate screens only take in the user information and provide no additional information on the Bahncard.

Final sales screen contains the mention of an automatic extension, avoids the mention of the word “Abonnement” here and does not highlight prices as recurrent. If you buy a probe Bahncard it does not mention the price of the recurrent bahncard. (All personal data is fake.)

There is zero indication of the payments (the value of 492) being recurrent (“492.00€/year”); the probe version does not contain the price of the full Bahncard at all. The mention of the subscription does not use the word “Abonnement” (subscription) this time, instead says that it gets automatically extended by one year unless you cancel. It has no visual weight, unlike the “bahnde-Newsletter” abonnement (so if you ctrl+f “abo” you’ll end up in that section), and there is no option to not get the Bahncard as a non-subscription.

You buy the Bahncard and receive a confirmation email for your subscription. Although it does not contain the word Abonnement, any mention of renewal, the price of the subscription (only the reduced price of the sale in my case) or any indication of cancellation. You also receive a welcome email, letting you know about the advantages of your Bahncard, which, again, contains no information about cancellation, renewal, subscription, or prices.

You Now Have A Subscripton. How Can You Tell?

Congratulations, you bought a Bahncard subscription. You used it for a while and decided it is not worth the cost. You don’t remember getting a subscription, but you want to make sure! Unfortunately, you don’t try the obvious thing, but make the mistake of checking your actual account at Deutsche Bahn. So, where to search for the subscription?

Deutsche Bahn thought about that! They have a subscription portal called Aboportal. It is clearly labelled in the account menu, right next to the Bahncard option! Amazing. Here’s what it looks like for me right now:

The subscription portal of DeutscheBahn contains no mention of the Bahncard subscription.

I had two subscriptions in the past for Deutschland-Tickets; they were cancelled long ago, and they still show up. They have a lot of space for additional menu items; it seems they don’t believe in separating functions or user-friendly design — that’s just my opinion. No Bahncard subscription, though. I would not fault you for believing you do not have a subscription active after this screen and just quitting, but let’s try more.

So it has to be in the Bahncard section. Here are two entries, one is my extended Bahncard I never wanted, and one is my Bahncard Business that, fortunately, my company provides. Can you tell if, and which, is a subscription?

It’s very obvious if you know what you need to look out for. If you hit the “options” selector, there will be a third entry saying “Cancel Subscription” for a subscription. Also, Bahncard Business is not available as a subscription, so it can’t be one.

Lastly, when are you informed of the renewal? The cancellation time is 4 weeks before the end of validity. 21 days, or three weeks, before renewal, you will receive a notification for your new bahncard. It is too late now. You are stuck with it for another year.

If you cancel instantly anyway, you will receive an automated cancellation reply with the following neat sentence:

Ihrem Wunschtermin können wir leider nicht entsprechen, da eine Kündigung nur bis spätestens vier Wochen vor Ende der Laufzeit möglich ist. Dafür bitten wir um Verständnis.

Automated DB Email for cancellation

Translation: “Unfortunately, we cannot accommodate your requested date, as termination is only possible up to four weeks before the end of the term. We apologise for any inconvenience.” Deutsche Bahn has fully incorporated the subscription trap into the response. They are aware of it, they know you do not want it, and they let you know: They will not cancel.

Contacting Deutsche Bahn Support

If you are remotely acquainted with German support standards, you can probably skip this section. Deutsche Bahn has no intention of providing any form of good support.

First, you’ll have trouble finding a way to contact them, that is, not by phone (or in person). After some searching, you’ll find a contact form linked in some places that does not work. In the end, I managed to get a response from this email address: bahncard-service@bahn.de

I sent them a message about how I tried to figure out that I have a subscription recently and did not find anything, and how I never wanted this renewal, how I was never informed of the increased cost (as I bought it on sale). To prevent any misinformation about my email, here is the translated original:

Last year, I purchased a Bahncard as part of a special offer.
A few weeks ago, I checked to see if I could find any information about a subscription, as I don’t want another Bahncard—I only needed it for one year.
I couldn’t find any information in my original confirmation email or on the website, and the subscription portal doesn’t list it either.

Today, I suddenly received a message saying that I will receive a new Bahncard in three weeks.
I canceled it immediately online, but unfortunately I was told that it was too late and that I should have done so at least four weeks before the expiration date.
The only email with the words “subscription” and “Bahncard” is the new Bahncard email.
Furthermore, the email does not contain any information about any costs associated with the Bahncard.

???

Translation of my email to the support.

As far as I can tell, I received a standardised response. You can find the translated version here:

Thank you for your email dated September 21, 2025.

For all offers on www.bahn.de, the contract is only concluded at the end of the booking process.

Before completing your purchase, we will inform you about the terms and conditions for purchasing and using the BahnCard.

You acknowledged this directly during the online booking process and accepted it by clicking “Buy now.”

From October 1 to October 13, 2024, the BahnCard 50 was available at a discounted price.

After that, the BahnCard will be converted into a regular subscription.

We regret that you disagree with our decision to cancel your BahnCard.

The BahnCard subscription can be canceled annually up to four weeks before the end of the validity period. The date of receipt of the cancellation by BahnCard Service is decisive.

We ask for your understanding that we are sticking to our decision.

We have still scheduled the cancellation of your BahnCard **** for **. **** 2026.

Translated with DeepL.com (free version)

Translation of the support email.

Another back and forth led to the same result. This problem is common enough that a lawyer put out a default letter template for refusing, which I used for my second letter. It comes up on Reddit frequently, although I do not understand the people who defend this.

As far as I can tell, the previous attempts by Verbraucherschutz to sue Deutsche Bahn over this were unsuccessful. The layman’s summary I got from this court decision: As the Bahncard is a discount card and not a subscription to a product or service itself, it does not fall under the consumer protections that the Verbraucherschutz sued over.

Deutsche Bahn: No Reputation To Lose

The Deutsche Bahn has no reputation they lose. The discourse is dominated by their terrible service, delays and cancellations. They have no incentive, as a company, to improve other sections like the Bahncard sales tactics. They, as a company, have nothing to lose.

We as a society have a lot to lose. Deutsche Bahn is 100% owned by the state of Germany. We consumers do not cleanly separate companies from their owners. Many even think of Deutsche Bahn as a state company. Feeling cheated by your government is not good.

Further, Deutsche Bahn is a core infrastructure company for more eco-friendly transportation. Driving people, like me, away from using the Bahn is bad in so many ways. I have a high desire for fairness; if I feel cheated, I’d rather take a higher loss to myself than support the person or company that cheated me. However, I have little recourse in this whole debacle, but I’d rather drive my car more, or even rent one, than use Deutsche Bahn… and I really like taking trains. I love being lazy while the landscapes fly by instead of focusing on the road and getting annoyed at other drivers. All this, despite not a single one of my ~20 train journeys the last year being on time, and a dozen of them not even honouring my reservations, being overrun or flat out cancelled. None of that made me quit taking the train. Feeling cheated by Deutsche Bahn sure does.


This post was written as anger management. This whole desaster of communcation, despite “only” being 500€, cost me so much productivity, motivation and more. I hate dealing with these kind of things. It followed me around all day, made me lose sleep, just because it annoyed me so much. Now I feel better. So for some fun: My first email to Deutsche Bahn netted me 8 recipient confirmation emails by deutsche Bahn, neatly spaced by around 3 hours each. Then they took 2 days to respond to my first email. For the second email I received only 1 confirmation, but got to wait 15 days for a response.

Categories
General

Income and Happiness

Today, I cam upon an interesting research article: “Income and emotional well-being: A conflict resolved.” The article presents the results of a new study regarding the dependence of income and well-being, often used, at least by laymen such as myself, as a proxy for happiness.

The findings are interesting as they seem to fit better with many peoples intuition, more money makes more happy, sending the article to reddits ‘not the onion’ subreddit. The subreddit collects articles that seem satirical based on their title – everyone knows nobody reads the content.

Anyways, the findings discuss the previously found leveling-off of well-being improvements after a certain point, i.e., $75.000 per year, by Kahneman and Deaton. This new study shows their data suggests this is only the case for the unhappiest 20% of high earners, while otherwise the log-linear relationship holds: More money makes more happy.

These “discussions” on reddit usually go a certain way, e.g., Empathetic_Orch says:

I make under 30k a year so this is pretty obvious to me. If I could make 60k a year I’d be happy.

Reddit

This is just an example of a popular kind of comment, I see quite often. I was curious what the results actually said and how they are presented. The article focuses on the change around the point of $100k household income and uses this as its only new visualization. Note though, that this shows the log of the income.

The authors provide their data easily – Thank You! – so it is simple for everyone to create new visualizations. So i threw the data in a few visualizations tools to make myself happy.

Violin plot of Killingsworth, Kahneman and Mellers data on the income-wellbeing relationship, using the income brackets used in the data.

First I wanted an overview of the distributions, to get a feeling for the data. Seaborn has a nice and simple way to plot violin plots. The quantiles of the distribution, shown as dashed lines within the violines, seem to indicate a fairly clear upwards trend over the brackets.

It is important to note, that the brackets organized in this way, still are mostly logarithmic, as they get larger and larger. To alleviate this, I computed the 10th and 90th percentile in addition to the quartiles.

Income BracketP10Q1MedianQ3P90
$10k-$20k45.986753.1450061.175068.9490077.1774
$20k-$30k47.006053.6010061.878570.2560078.6860
$30k-$40k47.422055.3490062.126070.1840077.6935
$40k-$50k48.261054.9140062.200070.5460078.2786
$50k-$60k47.743255.1700062.825070.6810078.8980
$60k-$70k48.682055.3910062.949071.1130078.8512
$70k-$80k48.928055.2710063.096071.4560079.0130
$80k-$90k49.106456.0080063.304070.9760079.2354
$90k-$100k49.302556.2570063.434071.5467579.2570
$100k-$125k49.932256.4470063.545071.4030079.2282
$125k-$150k49.729056.6140063.790072.0690079.4690
$150k-$200k49.853056.8650064.191071.9370079.9730
$200k-$300k50.654057.7070065.000073.4170081.0800
$300k-$500k50.991558.0962565.622073.8955081.2555
$500k-$750k49.662857.2767564.713073.2862581.8000
10th percentile, first quartile, median, thrid quartile and 90th percentile of the well-being indicator for each income bracket that had data.

Based on this information, I created a log-regression for a new visualization.

Logarithmic regression of the summary statistics of the data. The red band shows the inter quartile range (Q1 to Q3) of the well-being indicator across income brackets, while the blue band shows the range from 10th to 90th percentile (the center 80% of the population) of the given bracket. The xticks show the midpoint of the income brackets, similar to the original article.

The authors of the original article note: There is not enough data to make any meaningful conclusions for the >$500k bracket, so lets ignore the seeming drop for the richest respondents to the survey.

For me, the more important thing to draw from this, is more related to the reddit comment I showed as an example. If you are unhappy at $30k, would you be happy at $60k?
Who knows, you are an individual not a statistic.

But, for a large population, the answer is who knows… While someone earning ~$400k unhappily (10th percentile) is still unhappier than 75% of people earning ~$15k, a person moving up in earning from $15k to $400k, might become happier. That’s not in the data.

It is important to realize though, that well-being is a wide spectrum within any earning bracket. There are many happy low-earners, as there are many unhappy high-earners.

So, best of luck /r/Empathetic_Orch, I hope you can make it to $60k quickly, and that it actually makes you happy. 🙂

The notebook used as python script.

ps: This is a random blogpost and I am not a professional in the relevant field. I just do this for fun, if you base any policy on a blogpost by and idiot like me, you are an idiot as well.

Categories
General Stories

The Beach – A Short Story

A starry night.

His bare foot touched the warm, but dark water of the sea. The wet feeling was welcome after this long walk. Standing still in this location was the payoff for a, surprisingly tough, journey from the closest village. It used to be easy to get here! Was it because it was night? No – he remembered well coming here at night before. Was it because the path deteriorated? The townsfolk must have neglected it. Surely, they have no use for the beauty of this spot, so maintaining the path was not worth it to them. Nonetheless it was a treasure for him.

A wave of unusually cold water washed over his feet. He backed off from the sea – he felt betrayed. Just a few steps away, he realized the ridiculousness of that feeling and sat down. Was this not what he should expect from the sea? Was this not even the reason he came here? The sea did not betray him – he betrayed his own expectations.

Slowly he sat down, his gaze drifts from one oncoming wave to the next, until it reaches the horizon. He noticed the lights of a far away ship, competing with the stars above his head and the dim lights of the village behind him. None could really claim the title of brightest, and not only because the moon was already holding that. He laid down on his back, taking in the full, yellowish glowing circle of the brightest.

A refreshing cool wind let him shudder for a moment. When they were here together, this never was a problem – for obvious reasons. They would just cuddle closer for those short moments and enjoy the otherwise cozy temperatures. He could barely remember coming here alone at night anymore. Did he bring a jacket before? He didn’t today at least. It shouldn’t be much of a hassle to just wait it out! A few moments of freezing or returning early? An easy choice.

A loud caw of a probably colorful bird caught his attention. It must have been close. The moon did not plan on making it easy for him to spot it. Another one, slightly further away – was the bird leaving? Or was a second bird approaching him? He slowly turned around, using his hands to keep himself steady. His eyes needed some time to adapt, too, after staring at the bright moon. Only a few seconds. It was barely enough to catch a glimpse of the bird flying through the trees he himself emerged from not that long ago.

Now that he focused on the trees, he notices some activity. A few smaller birds were happily chirping, while taking turns fishing for bugs in the air. A flying squirrel sailing through the night – a sight he hadn’t even noticed by day! How could he have missed all of this when wandering by? Was he too focused on his goal? His own expectation of arriving? The short distraction made him lose sight of the squirrel, something he instantly started to regret.

Regret. A word which can create a powerful downwards spiral of thoughts in many heads. He came here to for the experience of the past, but somehow it turned out to be so different. Was it tainted by the time they spent here together? Or was the, nearly magical, power of the experience before just unattainable? Coming back, just for the sake of going back.

When they came here the first time, he wanted to share the mystical nature of his favorite spot. The view far above the sea. The reflection of the sun, the stars, the moon. No matter the time they would come here, it was always beautiful. But today it felt like he didn’t belong here, even the road tried to stop him. He should go back after all, for the sake of accepting fate as it was. He got up.

He stopped. Had he given up? Yes. Had he not accepted his fate all the time? Was it him putting in the effort? No; he did not want to give up. There was so much to do, so much to say to each other, so much beauty and warmth to experience together!

Some splashes of warm water from the waves hit his feet. The lukewarm wind slightly pushed him to the road home. He looked forward to the path taking him there.

Categories
Programming

Learning By Joking: A dockerized PHP FizzBuzz API

It’s no secret that we learn best by doing something: we remember vocabulary by repeating them, or better use them in sentences. We study math by solving problems. We hone our writing skills by writing. More often than not, those don’t have a well figured out point. Sometimes, though, we create a dockerized fizzbuzz web API.

GitHub logo I designed for FABI, the fizzbuzz API.

Fizzbuzz?

Fizzbuzz is a surprisingly infamous children’s game: one after another the children count by saying out the number aloud. But if the number is a multiple of three, they have to say “fizz” instead. For multiples of five, it is “buzz.” If the number is a multiple of five and three, the child has to say “fizzbuzz.”

The game gained infamy with computer scientist and programmer applicants though! How so? As a simple challenge, i.e., write a loop that prints out the numbers up to 20 but print it with the rules of fizzbuzz, it is used as a screening for job interviews. The insight it provides is: has the applicant lied about their basic programming knowledge?

Thanks to this popularity fizzbuzz is often used as a starting problem on programming challenge platforms. Or as a starting point, similar to hello-world. But it is inefficient and boring to copy paste or rewrite solutions to fizzbuzz in all languages we know to farm points on such a platofrm. So we’ll just write it once and call it. Abstracting it away! (Writing external REST API calls is so much more fun!)

Let’s start with a simple fizzbuzz function for a single number in PHP. Why PHP? Because I happen to have a webhoster that natively supports it, making testing and iterating on it fast and simple. Here is the fizzbuzz.php:

<?php
function fizzbuzz($fb)
{
if($fb<1 || !is_numeric($fb)){
return “”;
}else if($fb % 15 == 0){
return “FizzBuzz”;
}else if($fb % 3 == 0){
return “Fizz”;
}else if($fb %5 == 0){
return “Buzz”;
}else{
return $fb;
}
}

Simple and straight forward, as we would expect from a topclass fizzbuzz. Don’t use it in production just yet, though, it’s not battle tested yet.

A First PhP Implementation

Building on this function, we would like to offer two API endpoints:

  • getting the fizzbuzz value for a single element,
  • getting an array of all fizzbuzz values up to a given value.

As we are providing an API, we should stick to a common format for data. Our desired API endpoints have no complex data, so we stick with JSON, which is well supported for PHP.

Let’s take a look at exact.php the file we will use to realize the first API endpoint:

<?php
header('Content-Type: application/json');
include 'fizzbuzz.php';

$fb = $_GET["fb"];

if(!is_numeric($fb)){
echo json_encode("");
}else{
echo json_encode(fizzbuzz($fb));
}

A few points of interest, starting from the top: we add the expected content type header, to let our recipients know they receive JSON formated data. Setting headers needs to be done before any output. To ensure this condition, we defer includes after the header statements, as included files might output. We can’t trust the author of fizzbuzz.php after all.

We read the get parameter “fb” so, for now, we have to call this file using exact.php?fb=<number> to get the API response. We will fix this later, so we can call our API in a PHP-agnostic way.

Lastly, we check the read value, as we do not trust the author of our fizzbuzz function to do the right thing and return the JSON encoded response from the function. A simple call to exact.php?fb?=15 will now return the string “FizzBuzz” and our browser recognizes it as JSONdata.

We can do the same for our range endpoint, using a file called to.php. We use the same parameter name, headers and checks, but resulting in the much more interesting result of a JSON array when calling to.php?fb=15.

<?php
header('Content-Type: application/json');
include 'fizzbuzz.php';

$fb = $_GET["fb"];

if(!is_numeric($fb)){
echo json_encode("");
}else{
$results = [];
for($i = 1; $i <= $fb; $i++){
$results[] = fizzbuzz($i);
}
echo json_encode($results);
}

Time to relax.

Pretty API Endpoints

Unfortunately, we are currently restricted to PHP files that are called with parameters. This is unpleasant to document and use, and terrible to migrate to a new platform: we might need to switch to a high-performance fizzbuzz solution once our fizzbuzz service takes off.

Our current solution, hosting it on a PHP based provider, can do that: it is running apache with an enabled rewrite engine. This allows us to specify rewrite rules in a .htaccess file in our folder:

RewriteEngine on
RewriteRule ^/?to/([0–9]+)$ /to.php?fb=$1 [NC,L]
RewriteRule ^/?([0–9]+)$ /exact.php?fb=$1 [NC,L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ /404.php [NC,L]

First, we have to instruct the apache http server to activate the engine. Afterwards, we can specify rewrite rules. As htaccess files can contain many declarations, a rewrite rule is initiated with the string “RewriteRule”. A regular expression defines which requests we want to capture, followed by the destination we want to rewrite to. Lastly, we have further attributes.

The rules will be applied top to bottom, once a match is found, the attributes in brackets determine if further rules are applied: L specifies no further matching after applying this rule. The other used attribute, NC, explicitly tells apache to ignore case.

Within the regular expression, we can capture information: using brackets we tell the regular expression, that we want to preserve this part of the match and inject it in the position of “$1”.

As a final touch on the htaccess file, we want to rewrite any other request to a custom 404 reaction. We add a rewrite condition, which limits the next line of the configuration, to only apply if no matching file exists. This is necessary to prevent rewriting of our filenames to.php and exact.php.

Using this file, requests to /to/15 will be forwarded correctly to our previous implementation, as well as /10232 to our exact.php file.

Deployment

After finishing the productive part of our endeavour, it is time to think of deployment and providing the thirsty masses with easy access to our open source fizzbuzz solution (until we find a way to monetize a fizzbuzz SaaS of course).

You might think one of the reasons to chose PHP was simple deployment. But simple is in the eye of the beholder. If you are not already subscribed to such a service, you will need a PHP installation, an apache webserver, and maybe even a Linux! If someone deploys our software on an Nginx, it will not work, too. So the answer is obvious: docker.

To provide a docker version of our software, we require a Dockerfile. A simple textbased file specifying our requirements and actions. We can also choose a base image to use for our software. Fortunately, PHP provides apache based docker images.

FROM php:7.4-apache 
RUN a2enmod rewrite
COPY src/ /var/www/html/
EXPOSE 80/tcp

We need to activate Apaches rewrite mod, which can simply be done using the run command. While run would allow us to install arbitrary software, we do not need anything else. Copy instructs docker do use our source folder at a specific place inside our docker container. Expose tells it that we require port 80 for TCP.

We can build a container image from our dockerfile using ‘docker build -t fizzbuzz .’ and run it using ‘docker run fizzbuzz -p 8080:80’, which will allow us to access the fizzbuzz API on ‘http://localhost:8080’.

Finally, we can share the generated image with all our clients or even distribute it via docker hub.

All sources can be found on GitHub. The actual docker container is also available on docker hub and running on fizzbuzz.ketzu.net.

https://github.com/ketzu/fizzbuzz

Categories
Uncategorized

The Shitty Cactus Journey

Shitty Cactus Fake release ad.
Fake ad I created after seeing release ads by other games.

Like many devs, I started a few projects. And like many devs, I never really finished them. My self made website CMS? There’s no admin panel. Once the front was done, I just started editing the tables using phpMyAdmin.
My shitty idle game? It was stuck on version 0.11 with 7 half finished, or not even started, features. Or the insanely terrible 3D weekend game challenge.

But that 3D project gave me an idea.
The basic capsule shape with the weird texture looked like a cactus to me — with enough fantasy at least. How about honoring that cactus, and make it into an actual game?

The 3D version didn’t work well, so how about a 2D platformer?
Climbing up and up, evading deadly enemies, swinging on ropes!
I instantly started with some character sketches.

Shitty Cactus character design.

It had to be a cactus. I already wondered how many people would question the jump abilities of a cactus though. Quickly I set up a unity project and started working.

The inital cactus design looked terrible in unity, quite a disappointment. So I created a smaller one, but tried to keep at least the “facial expression”.
But I really wanted to keep the big design for something.

A game needs videos!

David, motivated by Humble Bundle

When I came across the 2D Animation Humble Bundle, I knew what I had to do. It was so obvious: A game needs videos! So without thinking, I bought it. It wasn’t as useful as I hoped, and probably a waste of money, as there are open source, or at least free, animation solutions out there. But it resulted in a nice, according to my standards, intro video.

When I finished the video after hours and hours of work, I felt like I was missing something. The game for the video! After setting up the unity project and creating a smaller character, I had done nothing.
No gameplay, no menu — just emptiness.

Art and Features: Not Easily Seperatable

I decided to scrap some features I had imagined.
Just getting the base gameplay down.
Jumping and platforms.
I started with the jumping.

The GDC talk on jumps made me create my own jumping physics.
I even redid all the math, because I needed it in my own notation.

Jump calculations.

By now I believe, I should have stuck with unity mechanics, but I thought I had to do it manually.
Implementing the jump and movement created a lot of problems later on, without much of a payoff: I couldn’t use many unity provided helpers, but had to solve most of the problems (e.g., getting stuck in platforms, not correctly using normals and more).
But in the end, it worked somewhat as I expected: I could set jump height and length, or duration, and have a jump with those properties, which came in useful much later on platform generation.

Creating platforms took quite a lot of trial and error, until I found some that didn’t look completely terrible.

Art design of shitty cactus.

Originally I wanted to scale the platforms during the level.
This turned out to be much harder than I expected.
Getting the tiles correctly was the easy part, but the level generation was tricky.

At first I just created 4 platforms on each plane with random distances from each other.
They mostly clipped into each other.
I created an array of distance ratios of the game width, which summed up to 1 when including the width of the platforms. While that fixed the clipping, the level was extremly boring. Each layer was at roughly the maximum jump strength from the last and everything looked very uniform.

I needed a better level generator.
So I wrote down all the constraints I had.

Platform calculations.

Most of them based on the jump properties, but also the width and height of the platform.
I made many many miscalculations, forgot as many details as I remembered, but in the end it… kinda worked.
Let’s say it was better than the previous one, and actually playable. I looked upon the game, and felt a little proud, but also that it was terrible.

I had two friends play the game (shoutout to Lazitus and Gariot, your suffering won’t be forgotten).

The worst bug was: Getting stuck in clouds, helplessly moving around.
Unless you let go of the controls, then it would magically move you upwards! At least it wasn’t gamebreaking. I tried a lot of things. I came across Platform Effector 2D and noticed: unity had what i needed. Platform effectors modify the behaviour of collisions with platforms, e.g., allows a character to jump through from one side!

Remember when I said it was a mistake to implement the mnovement myself?
Those don’t work if you do this. I tried implementing them myself, and it mostly worked. And by that, I mean: I could do as much as before, but still had the bug if the angle of collision was too low. So I scrapped all the complicated code, and used my simple version again, but put in more margin of safety and it mostly worked as well.
Finally a finished game!

Visual Only User Interface

Unless you count having a UI instead of just an endless level. Then it was unpolished and unsueable! As I wanted to do everything myself, I created some UI sketches, which I then made into UI elements.

Overview of the Shittycacus UI Design Process

I had to build my own ring indicator and combined upgrade elements, but I didn’t have upgrades yet anyways, so I didn’t put too much thought into it. Maybe I should have.

One of my design goals was being able to use it without any language, so everyone that would like to play it, could do so.
Mostly because I didn’t think there were more than 1 interested players if I restricted it to any language — I did call it ‘Shitty Cactus’ after all.

I made up some rough ranges for upgrade ideas and tested them.
Jumping too high, would be detrimental, making it much harder.
The same for many others.
After some hours of playtesting, I came up with some values, split them and made up some costs. I linked everything in the UI and felt I was done.
But the game felt open ended and unsatisfactory.

I decided that the game needed an outro video.
It was easily created, if you disregard 10 hours of getting back into the animator software and failing to create anything in the way I imagined it.
But how and when to play the video?

I tested some more, and felt 1200 was a good height to unlock it. But unlocking it, and switching to the play scene, would kill the progress.
There’s a good and an easy solution to that! The good one, storing the state and being able to reload it. That would be useful in many other cases, too. Obviously I went with the easy one: Unlocking it, and you have to watch it in the main menu (or simply in the folder if you have the offline version).

Finally, I felt, I had a full game.
But now I wanted to feel like a real game dev, so I decided to publish the game on steam for fun. It was quite the interesting experience.
The requirements are easy: Pay 100$, fill out some tax forms, create lots of game art, have the shop page in “coming soon” mode for at least two weeks, and you get to release the game!

What steam doesn’t tell you: Once the shop page is online, you will get indie-game-marketing spam. It would have been nice, if they felt less than fully automated emails with a small placeholder for the game name. But that’s a lot to ask for such a small and terrible game.
Though, I didn’t take this too seriously, so I didn’t respond.

Instead, I gave my friends each a free key and one told me: “It would be cool to have leaderboards.” That was the day I realised: There are leaderboards on steam.

Steam Leaderboards and Achievements

There is a nice implementation of the Steamworks steam SDK for C#, and therefore unity. It was especially easy to get into. Adding some statistics and leaderboards was covered by the example, so they could be added with little code.
It was already time for my first patch!

As they say: Never play on patch day — and I enforced that! I never set up the steam pipeline, instead I uploadzip archives of the game folder. Those get preprocessed by Steam and are then published to players.
This time, I zipped the folder. Not the contents. This made the game unplayable, as steam was configured to search “shitty cactus.exe” only in the root folder. Quickly I created a new patch and uploaded it, hoping none of my 3 players would notice — so I could tell them instead personally.

I worked so well, I started to look into steam achievements, the culmination of steam! It’s quite easy, if you base them on existing statistics.
When you do that, they get awarded automatically, once a statistic above the threshold is submitted.
Otherwise, you manually have to set the achievement from code.
I opted to include only easy achievements for now, and created seven of them: Reach some height or reach a set amount of coins. One achievement for height was masked as a “play at least once” achievement, by setting the required game to height. I included a “don’t forget to jump!” in the description.
Each achievement needs two pieces of art: Unlocked and not unlocked.
Turns out, greyed out achievements are only a recommendation for not yet unlocked achievements.

After setting them up and publishing the changes, they were live instantly.
It turned out, I only manged to get 3 out of 7, while a friend of mine and a reviewer from japan created highscores that felt insane to me.

Marketing Attempts

Lastly, I wanted to try a feature of steam I didn’t know existed: Curator connect. You can send up to 5 keys to up to 100 curators and reviewers.
To be honest, I just randomly selected reviewers at first and wrote “don’t play it, I just wanted to try the feature”.

In the end, 10 people bought the game, and one actually played it. The best thing about all of this, was the feeling when that one guy, a person I don’t know personally, reviewed the game without even being asked to: “It’s aight”.

I was lowkey proud of it and created a short imgur post.
I even linked to a temporary free download of the game (without steam features), because I felt like I am writing an advert, which I didn’t like.

After retracting the keys from the curators, I wroter a nicer message this time, something like “Seems like the game is not as bad as I thought, it would be nice to get a review, thanks.” I guess in the end, if you want someone to play your game, you have to tell people. And people don’t like bad self degrading humor in advertisements and even less in call outs to reviewers. Maybe I should stop prefixing my game names with ‘Shitty’.

That’s it.
I continue to work on the game sometimes, currently I plan to add a leaderboard for shortest time to 1200, I want to try the steam sale feature, too. But I won’t do anything big with it.

Once I lose interest, I will put the whole project on github (probably january ’21 or later), but for now the repo is locked.
It’s not a great game, but it’s not terrible either.

Don’t make the mistakes I made, believe in your game if you want it to be a success — whatever that means to you.
But at least I can say: I shipped a game (and lost some money while doing it), which is a success in itself.

Good luck everyone!