Shared posts

13 Jun 22:19

Lessons Learned from Find Matt’s Cats

by Matt Roszak

Hey everyone.

I think it’s time to write about all the things that caused me trouble during the development of Find Matt’s Cats – just to get my thoughts out there, and to remind myself not to make these mistakes again in the future. To release all of the bad feelings, or whatever.

For anyone not familiar – Find Matt’s Cats is a hidden-object game that I worked on part-time for a little over 3 years. It’s a Flash game designed to run in a Flashplayer emulator called Ruffle. It’s currently available on Steam/Windows, will likely come to Android later, and I’m still working on more content for it.


Many Name Changes

First of all, the game went through many different titles!

The game was originally codenamed “Hidden Cats”, which was quickly changed to“Hidden Cats and Dogs”, to differentiate it from similar games. But I only chose this title so I’d have something to put on the Steam store page, which I maybe rushed out a bit too early – I didn’t really have a feel for what the final game would look like yet.

The name was changed to “Matt’s Hidden Cats” once I decided to add story, and to include the characters from my Epic Battle Fantasy games. This name was way catchier too.

But that still wasn’t enough. About a month before release, and a couple of days before Steam’s Next Fest, I was alerted that the name “Hidden Cats” had been trademarked, and the multiple developers that were using the term in their game titles were being threatened with legal action! And the company that trademarked it wasn’t even the first to use it in the name of a hidden-object game!

Frustrating.

Anyway, there wasn’t time for me to do anything other than to quickly rebrand my game to “Find Matt’s Cats” – this new name is catchy too, but I’m still salty about the change. At least all of this drama generated a bit of interest for the game on Reddit and on the Steam forums.

I don’t think the last-minute name change particularly hurt the game’s reach – anyone following the project would easily recognise it as still the same game – but it did waste a lot of time as assets and store pages had to be redesigned. Perhaps it was a bit reckless to use a term that many developers were using in their titles – but I figured the practice was so widespread it was basically the name of a small genre by that point.



Poorly Defined Scope

Find Matt’s Cats was by far the biggest game I’ve made from scratch – all of my other Steam games are sequels to much smaller projects. This made it difficult to plan where to spend the most resources, as I wasn’t sure which parts of the game players would be the most interested in, or if the core gameplay was even interesting enough to begin with. I had no idea if the game had too little content to be taken seriously, or if it had too much content and most players wouldn’t finish it. I didn’t know if the average hidden-object fan wanted an easier or more challenging experience – so I made sure the game had options to cater to everyone.

So this caused a lot of fear, doubt and uncertainty, and made game development quite miserable.
I kept adding more and more content and options due to these doubts.

I added a story to the game to get Epic Battle Fantasy fans interested. I didn’t particularly want to do it, but I took the chance to try out some new storytelling ideas. I played around with some visual novel mechanics. I like how it turned out, but it felt a little bit forced. This decision probably doubled the number of sales though.

The game turned out better as a result of adding so many features and aiming to capture a broad demographic, but it was no fun to work on it with this mindset. The responses to the game on social media were decent, so I could tell the game wouldn’t be a flop – but that wasn’t enough to keep me motivated. I flip flopped between being proud of the game and feeling like I could be doing something better with my time. I felt like this a lot when working on Bullet Heaven 2 as well – if it’s not the best game I’ve ever worked on, then I wonder if it’s even worth making? Oh well…

The game started off as a test of Ruffle’s capabilities, and maybe I should have left it as a small project to test the waters. But once things got going, my mindset was “I don’t want to make a sequel to this, I better make it as good as it can be… but maybe making a good game in a niche casual genre still won’t feel very fulfilling…”

I don’t like feeling this way, but it seems unavoidable. I only feel a little bit better about it since the launch.


Showing Off the Game


The death of Flash content on the web messed up my process a lot. Flash made it easy to share everything I was working on – small demos, interactive animations, incomplete segments of games – these could all be shared on several platforms. Now it’s a lot harder to collect feedback early on – I’ve either got to make videos, or put together a downloadable demo on Itch or something. It’s more work for me, and more barriers for my audience.

It still would have been possible to share bits of the game on my website using Ruffle though. I definitely could have done that. I was a bit hesitant to show off too much of the game, which in retrospect sounds a bit silly, since that’s never been a problem with my previous games – where I usually showed every single thing except the final boss.

I felt that with this game the main reward for playing was seeing how each new level looked, and I didn’t want to spoil that. It doesn’t have the sort of mechanical depth my other games have.

Anyway, it felt bad finishing work on a level and then trying to keep it a secret from the world. So perhaps my biggest regret is this – not showing off the game more while working on it. It was mostly just Ronja that tested levels for me.


Revisiting Old Levels

Because the game was poorly planned out, a lot of levels I made early on had to be updated. Collectable currency was added much later on, along with collectable artworks. Way more art assets were available towards the end of the project, so I had to sprinkle some of those into the old levels too. I also had to make sure that levels were compatible with options added later – for example, making sure that disabling animations didn’t obscure any goals behind moving objects.

I enjoyed making levels, so I made too many early on in the project. Ideally I would have made just enough levels to test new features as I programmed them, and then made the rest of the levels once all mechanics and options were programmed.

To some extent this issue was unavoidable, but it did slow things down a lot, and a more complete prototype in the beginning could have helped things go more smoothly long-term.



Saving System Chaos

When it comes to the core gameplay, the saving system was one of the last features I programmed. I kept adding more collectables, and more mechanics, and I didn’t at all think about how I would save the state of each level. There was no data structure for tracking the overall state – only each individual object was changed a bit when the player interacted with it. Containers and buildings were open only because they were showing frame 2 rather than frame 1. Gems were collected because when the player clicked on them they became invisible and stayed that way, and the number of gems collected went up.

None of these objects had any unique identifier. They were simply identified by the order in which they were loaded into a level. The game doesn’t even have a way to calculate in advance how many collectibles there are in each level. I have to enter the numbers shown on the level-select menu manually. This was one of the big limitations of using Flash/Animate as my level editor, rather than programming a dedicated level editor. A lot of important meta-data is calculated during runtime while the level is starting up – like if an object should be attached to another object, should it be marked as hidden behind an obstacle, the ID number of each goal – these are all just calculated based on the level layout. A cleaner approach would have been to calculate this stuff when the level was created, and have those things known in advance.

This was all further complicated by each difficulty setting having it’s own separate save data, and different objects being hidden or visible on different settings.

This was also the first game I’ve made where it autosaves every tiny bit of progress. Every single item collected or cutscene played saves the game, and it took some effort to make sure this happened only at the correct places in the code and didn’t lead to any invalid save states. For example – I can’t assume the player returns to the level select menu after finishing a level, and triggers the next cutscene. They could just exit the game after finishing the level – the level state would need to be saved and the cutscene would need to trigger some other time.

And finally, I also added the option to delete different types of data separately. The player can choose to reset progress on Easy, Normal or Hard modes, reset cutscenes, reset gems and currency, reset achievements, etc. This was appreciated by players, but it did take some effort, and I’m still not sure that it’s impossible to put your data into an invalid state by selectively deleting parts.

Anyway, it’s important to think about your saving system as you plan new features for your game, and not at the last minute. The saving system in Find Matt’s Cats became a mess of spaghetti code as new features kept getting added. It hurts me when I need to make changes to it – which I will need to do as I have more content planned for the game.


Simple Features Were Not So Simple

There were several examples of fun ideas that I only implemented because I thought they would be easy, but I should have cut them early on in the game’s development when I realised they may complicate things.

One such idea was the option to replace the game’s mascot with different cats and other animals.

The mascot appears in many different poses, and sometimes wears hats and other accessories. They appear in every level, and several menus. To support other types of mascots, I had to draw every type in every possible pose, and code their hats and accessories to be disabled if the mascot had horns or big hair, etc. And the player can change options during a level – so the mascot had to update correctly if the player changed it! This all turned out to be a pain, and still doesn’t work correctly sometimes. It should have been massively simplified.

Another example was menu animations.

My past games were usually animated at 30 frames per second, and menu animations were often non-existent due to the performance limitations of Flashplayer. This game was animated at 60 frames per second, and runs much better, so I wanted menus to be smooth and fun – buttons bounce when hovered over, there’s more transitional animations, and the player has a lot of options for customising the interface. The level select menu itself is even a playable level! And since I want to release this game on mobile later, I had to make sure that menus were friendly for both mouse and touch controls – and some keyboard shortcuts too!

Players generally enjoyed how the interface turned out, and this stuff was worth doing, but it was a lot of extra (and often frustrating) work! And not all of the visual effects were necessary.



Music Issues

My games usually have music created by a single artist – Ziyan Su, previously known as Phyrnna. She wasn’t available to work much on Find Matt’s Cats, so she only made 3 tracks this time. And due to the poor planning of the game’s content, I wasn’t sure until the very end exactly what sort of music I would need. My games generally have one main music track for each biome, but in this game the levels in each world often had very different vibes from each other, so a bigger variety of music would be needed.

I ended up using a lot of stock music tracks from various artists
– for example, you may have noticed Kevin MacLeod in the credits. My initial concern was that this would make the game’s music feel cheap, but in the end my music choices were fitting, and players enjoyed the wide selection of tracks. No one complained about stock music being used.

The issue was that I had to learn about how Content ID worked and make sure that none of these tracks got flagged when people made videos of the game. Turns out that it’s complicated – even if music is free to use, it has to be in the Content ID system or else some troll might claim it as their work. And often the licenses require that the artist be credited in the video description – which in the case of gaming content generally means the name of the game has to be mentioned in the video title or description, which wasn’t always done and caused issues for a couple of content creators.

Another downside was that I couldn’t distribute a soundtrack for the game, and just had to tell players “go find all the tracks one by one – they’re all free somewhere”.

Anyway, I would suggest always commissioning music if you have the budget to do so, so you have complete control over distribution rights. But in this project I didn’t have anyone I was ready to work with, and I didn’t know what tracks to ask for anyway.


Too Many People Were Involved

Between the extra musicians, fan artists, and translators, there was a LOT of people that I needed to contact, credit correctly, send Steam keys to, and pay. And a lot of them were not professionals, but hobbyists. So this caused a lot of headaches and awkwardness. Some people would be slow to respond (or disappear off the face of the Earth), emails would end up in spam, and a lot of this boring stuff just took up way too much of my time.

In future I need to organise this better and be much more selective about including people in a project.
I should probably try to keep a tidy spreadsheet with everyone’s relevant details, and make sure I have more reliable contact method than just email.

Translations were a bit messy because I left them a bit late – I should have finished the script earlier. In my hurry, I invited too many people to help out with some languages.

The game features a gallery of art made by fans, and I wanted to use this art to make some beautiful Steam trading cards too. I almost forgot this, but developers actually make a small amount of money from these trading cards when they are bought and sold by players. It’s not a lot, but over a decade, it can start to add up. I didn’t want to take advantage of fan artists, so I had to sort out fair payments for everyone. The cards turned out great, but it would have been less work to just use my own existing art!


Contacting Content Creators

Because this game was aimed at a broader, more casual audience than my other games, I felt pressure to reach potential players that were not familiar with my other work. I spent more time trying to market the game than I usually would…

Me and Ronja created a spreadsheet of all the content creators that have played my games in the past, along with over a hundred that have played hidden-object or cozy games and may be interested in this one. We collected contact details and I made a detailed information page about Find Matt’s Cats to email to everyone, along with a free Steam key.

Well, I think this was largely a waste of time. I think a lot of my emails ended up in spam folders, and the only content creators who responded were ones who already knew who I was. Websites for distributing Steam keys like KeyMailer feel like ransomware and are horrible to use, for both developers and content creators.

Most content creators who were interested in playing Find Matt’s Cats would either just buy it, contact me through social media (mostly Twitter), or someone in their audience will suggest it and gift it to them.

I should mention that it was worth being generous with Steam keys, and even tiny content creators were helpful – not so much for marketing, but for playtesting. I got a lot of helpful feedback watching videos of the first few levels of the game, and these helped me smooth out some design wrinkles.

In future, I think I will just write quick messages to my existing contacts, and leave it at that.


Marketing That Worked

The most effective way of collecting Steam wishlists was to publish a demo for the game, and then take part in Steam’s Next Fest. Find Matt’s Cats did reasonably well with the Next Fest algorithm – above average for a small indie game. This was a good chance to get feedback from players too. I kept the demo simple – it’s just the first 10 levels of the game – just enough content to gather some feedback on every mechanic in the game.

Another great way of collecting wishlists was making News/Announcement posts on my other Steam games, and mentioning I was working on a new game at the end of each post. The cross promotion option didn’t lead to a lot of traffic – the posts had to be mainly news about the other games.

But essentially… the easiest way to get attention on Steam… is to already have some successful games on there. No surprises there.


YouTube Stuff

I fairly recently started making text/image posts on YouTube (I didn’t know that was a thing). My posts seem to get a lot more engagement there than on other platforms! So it’s definitely something to consider if you have a YouTube channel.

Me and Ronja put quite a lot of effort into making YouTube videos about the game, in the style of in-game cutscenes, and also some silly live-action shorts. Some of these flopped while others got a lot of attention. Generally videos landed between 5k and 30k views. I think this is a viable way of marketing a game – but it’s hard work! You need to make a lot of videos to figure out what works, and constantly come up with catchy ideas that will do well with the algorithm. And it’s not enough for the video to get lots of views – it also needs to contain a call to action and actually get people interested in the game! So while our videos helped build hype for the game, I’m not sure it was worth it, considering the amount of work that went into them. But it can be more efficient if you overlap it with other jobs – for example, livestreaming a game while playtesting it.

2 Left Thumbs made a video about Hidden Cat games and briefly mentioned Find Matt’s Cats in it, and that resulted in way more Steam wishlists than my own videos. He’s very good at making marketing content, and it goes to show how much skill is required to make engaging videos that actually sell products on YouTube.



Other Marketing Efforts

As always, I share my development progress on my social media – Twitter, BlueSky, Facebook, Discord, and Newgrounds. I post art, level previews, funny dialogue – and talk about new features I’m working on. This helps me gauge interest in different features, and I also just enjoy sharing whatever it is I’m doing. Generally Twitter has still been the best platform for reaching out to other developers, content creators, fan artists and other helpful contacts, but it’s also the platform that wastes the most of my time and causes me a lot of stress. Discord has been the best platform for interacting with the most dedicated fans, fostering a fan art community, and collecting bug reports and other feedback – thankfully Ronja manages that one, and it causes her stress instead of me.

I spent some time posting on Reddit, and this worked to an extent. I found some small communities that were interested in exactly this sort of game – r/wimmelbilder and r/isometric to name some – and this lead to some wishlists. The larger subreddits are quite restrictive about self-promotion, and the more general subreddits require much more catchy posts to get any attention on. Overall, I think promoting yourself on Reddit is worthwhile so long as you don’t spend too much time on it, or if you just enjoy posting there. People were overall very polite.

I posted the Find Matt’s Cats demo on Newgrounds! This went fairly smoothly – it’s the same as the Steam demo, with a few music tracks removed to reduce the file size. It generated some modest hype and won a cash prize. Definitely worth doing if your game can run in a browser. (Ruffle doesn’t run quite as smoothly in a browser, but the hardware requirements are lower there, so it gave me some helpful insight into its capabilities)


Finally: I Didn’t Get to Play it!

Something that has bothered me throughout the whole project is that I don’t get to play any levels made by other people!

With the other games that I’ve made, the hardest difficulty option is always fairly challenging, even for me, so I at least get to experience the challenge aspect of those games. With this game – there’s nothing like that. I don’t remember exactly where everything is hidden, but I know all the tricks, and there won’t be any interesting surprises for me.

Originally, Ronja was meant to create some levels, but the process ended up being quite complicated as development progressed, and it was too much for her.

So I think this game won’t feel finished until I create an in-game level editor powerful enough to create content that entertains me.

My work continues…

17 May 18:19

Speedrun

Usain Bolt holds the world record in the 100 meter speedrun.
11 May 21:30

Manuals Plus: The Wrap-Up

by Jason Scott

Just a little over ten years ago, I was notified about a big warehouse of manuals that was going to be discarded in a few days. Bursting with energy, I drove down, discussed things with the owner, and, soliciting and ringing a very loud bell, assembled dozens of people and tens of thousands of dollars over the course of saving this collection from being discarded. Naturally, the next step would simply be to digitize them all.

That took longer.

As of a short time ago, a collection of 13,000 manuals now lives on the Internet Archive. It is, essentially, all the manuals that will be digitized or could be digitized, sans sets I’ll explain about shortly.

In other words, the loop is now complete. Saved, stored, moved, now online for anyone to read.

If somehow you missed this apparently core event of what people think of when they think of me, there’s so many weblog posts it’s almost weird to list them all:

In Realtime: Saving 25,000 Manuals
In Realtime: Prepping for the Transfer of 25,000 Manuals
In Realtime: We are Barely Halfway Done
In Realtime: Day 2 Felt Like Week 4
In Realtime: It is Done (Done physically saving the manuals, anyway)
A Small Dark Detour
In Realtime: Post-Mortem
A Little Bit of the Manuals
In Realtime: Some Initial Sorting and the Power of Two
The Manual Rescue: A High and Low Day
The Manual Rescue: Take Two, and Please Help
In Realtime: Digital Heaven (And a Call for Donations)

That last one really felt like the end (dated February 2024), but in fact it took a little longer to finish it all off. And that happened this year.

For anyone who doesn’t want to scroll through a dozen long-winded and repetitive posts about the process (aka “I ain’t reading all that – I’m sorry, or congratulations”), I’ll summarize it as:

Was told about a warehouse of manuals being thrown out. Came down to discuss the situation. Negotiated a week hold. Got dozens of people to show up, thousands of dollars to pack it all up and move it and store it, moved it from one storage unit to another location (closed coffeehouse in a mall), moved it all to California, got volunteers to sort the hell out of it, ended up with dozens of pallets, determined some not to be scanned, a group covered it being scanned, we’re done.

Only took eleven years and one tiny heart attack.

And there we are: a collection of 13,000 manuals.

Let’s go quickly down some frequently-yelled questions from the crowd, and what’s next.

First, what do you mean there are some unscanned manuals?

Well, funny story. Two companies still have manuals as a product and as part of their product line and are preferring to go through them, as well as that, in the event they make them public, their scans will be much better and thorough than the pile in this collection. Those companies are HP (now Agilent Technologies and Keysight) and Tektronix. So, it made no sense to scan those pallets. We still have everything, and if it came down to it, manuals truly lost could be found, but the cost to speculatively scan them would have doubled everything, at least. So we have the manuals, just not digital forms.

Next, who ended up covering for all this scanning?

Money was really the reason this all took so long. Scanning thousands of manuals, some of them hundreds of pages long, is an expensive project unless you truly think you can just make Steven and His Epson Perfection scanner do this on the weekends. (You can’t.)

A round of soliciting general folks was beneficial to the tune of thousands, and that helped cover it. But the biggest boost came from the Digitial Library of Amateur Radio Communications (DLARC), a funded group whose mission for a few years has been to gather as much Amateur Radio history as possible. A signficant percentage of the manuals were radio-oriented, DLARC paid for the general scanning, and it all slid over the finish line as a result. Thank you, DLARC.

Do You Have Your Usual Buffet of Random Thoughts?

Always do.

Certainly, the whole of the project was a success, but along the scale of human time and effort, it was difficult. That was a lot of lifting, a lot of driving, a ton of money and a ton of emotional aspects far outweighing what one would expect for piles of paper. Of course, they’re not just piles of paper – they’re entire outlooks of how technology works, how to teach users to do their own maintenance and process to take care of equipment, and they’re evidence as well as celebration of the wonder of engineering. They have value on multiple levels to the contemporary space, and just the graphic design and typography alone could consume a summer.

The largest hold-up was neither will nor the effort – it was money and funding. Yes, you can get a few volunteers to do work, and if there was some sort of golden fifty manuals that everyone absolutely needed online, those could have been done – but doing hundreds of thousands of scanning of pages in an orderly, tracked and quality-assured methodology just takes funding. There’s an entire discussion in whether any volunteer effort should be used at all, and I can assure you that I did get contacted by or was shown individuals in the professional space who believed it better that the manuals go into a trash compactor than any volunteer labor be used, but that’s how it always is in life regarding what resources go where. If money had not been the object, this whole thing would have been done in months.

For when people inevitably say they were sure a different manual was available and it wasn’t HP or Tektronix and yet it appears not to be scanned, that means it was sold off before my team arrived, or it was lost before we arrived, or it was in some way basically jammed somewhere strange. Those volunteers were brutally intense going through that warehouse. The warehouse shots look bigger because there were sometimes a dozen copies per unique manual.

Everything else I ever wanted to say was in those dozen entries listed above, so if you want to settle back and enjoy those, go for it.

Also, I wish to shout out the heroic efforts of the Bitsavers Project, a completely-separate-from-me entirely independent effort to scan manuals, digital artifacts and more. They’ve done so much more, for so longer, and so consistently.

What’s Left?

These manuals have very basic metadata. If people see a manual needs more description, or better description, or they want to just note bad entries and so on, leave a review under the item and I’ll integrate it in. You can always do a full-text search across the manuals and you’ll see text you’re looking for (it’s the “search text contents” option under the collection search). All are welcome to contribute that as it strikes them.

Would You Do It All Over Again?

Like the Defenestrations of Prague, I’d do it again, even if it took a hundred years.

28 Apr 08:25

Earthset with an iPhone

by Keighley Rockcliffe

Explanation: What does it mean for the Earth to set? Artemis II Commander Reid Wiseman gave us another spectacular view of Earth from their historic flyby of the Moon. Commander Wiseman's video, taken with an iPhone at 8x zoom, shows our entire planet gradually blocked from view by the Moon. On the Earth, the 24-hour planetary rotation causes the Sun to set below your horizon every night. However, on Artemis II the Earthset was caused not by the Moon’s rotation but by the spacecraft moving behind the Moon (at about 55 seconds in this video). Once rare, views of Earth are now taken many times a day from many spacecraft, including NASA’s SWOT (Surface Water and Ocean Topography) satellite tracking freshwater resources and USGS Landsat 8 and 9 satellites supporting water management for farmers, for example. Space agencies around our home planet now work together to provide unique and ever-improving views of our Earth.
02 Apr 11:38

telecheck and tyms past

Years ago, when I was in college, I had one of those friends who never quite had it together. You know the type; I'm talking lost a debit card and took three months to get a new one because of some sort of "mixup" with the credit union that I think consisted mostly of not calling them for three months. In the mean time, our mutual friend ended up in a quandry: at WalMart, at one in the morning, with a $2 purchase and no cash. Well, this was no problem for that particular space case: he had his checkbook.

If you think about it, it's actually pretty remarkable that grocery stores accept personal checks. It's a very high risk form of payment. Even if the check is genuine, the customer could be writing it against an empty account. On top of that, with modern printers and the declining use of MICR, forging checks is trivial. When you offer a check, the retailer has very little to go on to decide whether or not you're good for the money. Surely, fraud must run out of hand—and yet, just about every major grocer still accepts personal checks.

Retail point-of-sale acceptance of personal checks is the product of an intriguing industry that handles all the challenges of checks at once: a combination of digital payment network, credit reporting firm, insurer, and debt collector known as a check guarantee service. The check guarantee is older than the ATM, and depending on how you squint, check guarantees are quite possibly the first form of real-time, telecommunications-based point of sale payment processing.


Harry M. Flagg was born in Frankfurt in 1935, but spent most of his childhood in Milwaukee, Wisconsin. He attended MIT, major unknown, and graduated in 1957. I think he was probably an ROTC student, because some sort of Navy service took him from Massachusetts to Hawaii, where just a few years later he was out of the Navy and working as some sort of "management consultant." Flagg was entrepreneurial to his core, so while I knew few details about it his consulting work is unsurprising given the wide variety of business ventures he was soon involved in. We can be fairly confident, though, that his clients included retailers—retailers who struggled with personal checks. In 19641, Flagg quit consulting to focus on checks alone.

His idea was straightforward: keep a list of people known to pass bad checks. When a retailer is given a check, they just check the list, and if the writer's name appears they should turn the check away. As legend has it, Flagg took the idea to a Boy Scout meeting where he happened to describe it to a crowd of Honolulu business leaders, one that presumably included his soon cofounder Bob Baer. They agreed on an informal arrangement: Honolulu businesses would report writers of bad checks to Flagg's consulting office, where his small staff would look up names on request. It was such a success that Flagg's staff were soon overwhelmed. Tracking the writers of bad checks became Flagg's full time business.

TeleCheck newspaper ad

He christened his new venture TeleCheck—Tele, perhaps, for Telephone, or Telecommunications. Whether his MIT education or his Navy experience, something had introduced Flagg to the potential of the computer. Having seen his busy office staff, taking calls and digging through files, he imagined TeleCheck as a centralized, real-time computer system. By the time he announced the new company, an IBM system was already on order. General manager George Duncan set about designing and testing the process, and somewhere along the way they picked up the engineering talent to build a database for questionable checks.

As explained in TeleCheck's ads, accepting checks required only a phone call. Once connected to a TeleCheck operator, customers curtly said their TeleCheck account number followed by the driver's license number of the person who had written them a check. By the time TeleCheck matured, they settled on a system of three possible results: a "code 1" indicated a low-risk check that TeleCheck would guarantee. A "code 3" meant that TeleCheck didn't have any specific evidence against the check writer, but the value of the check or other risk patterns meant that TeleCheck was not willing to guarantee it. Worst of all was "code 4," telling the retailer that TeleCheck would absolutely not guarantee the check, because the writer already owed them money.

A 1964 newspaper photo shows TeleCheck operator Dorothy Nicholson sitting at the console of an IBM 1440, Harry Flagg looking on from the side. She's answering the phone with her left hand, right hand poised on the keyboard of the teletypewriter. This is probably a staged shot for the newspaper, I sincerely hope that they found Dorothy a headset (admittedly a surprisingly expensive proposition in the 1960s). It also contradicts claims in other newspaper articles that TeleCheck didn't go into operation until 1965, but I think that there was an extended "trial" phase before the service was generally available. I pay attention to these details because they tell us something about the company's early days. Flagg brought on quite a few business partners, so many that I struggle to keep track of them, and I assume that the computer was much of the reason. They were probably renting it, but that rate would have apparently been at least $1,500 a month, equivalent to about ten times that today. TeleCheck had capital. I assume that many of their early customers, taken from that Honolulu Boy Scout meeting, were investors as well.

Into the IBM 1440, TeleCheck combined several data sources: they formed a partnership with the Honolulu Police, from which they received copies of police reports on bad checks. They invited banks to submit records of bad checks they'd received. This information formed what Baer called a "positive" credit file. Instead of collecting data on all consumers, TeleCheck collected data on only the writers of bad checks. This distinction doesn't seem particularly interesting today, but TeleCheck really leaned into it, perhaps because consumer credit bureaus were both a growing business and a growing source of controversy in the mid-1960s. It probably served TeleCheck's interests to maintain some space between themselves and proto-Equifax organizations like the Hawaii Credit Bureau.

You might wonder about the business model; one of the advantages of checks is that they are relatively cheap to process. TeleCheck charged businesses a fee, at least initially set at 2%, but that wasn't just for the risk database. For the merchants, TeleCheck actually had a much more compelling offer than tracking check frauds. TeleCheck would guarantee each check they approved. If a merchant accepted a check on TeleCheck's advice, and the check bounced, TeleCheck would reimburse the merchant. In exchange, it asked for the bad check to be endorsed over to TeleCheck themselves.

Eating the cost of these bad checks could have been rough on TeleCheck's books, but they had their reasons. First, the reimbursements gave their customers a clear incentive to submit every bad check to TeleCheck. While TeleCheck marketing emphasized police and bank sources, it's clear that the primary source of their data was always their own customers.

You might realize that the guarantee service could create a new kind of fraud: a business might fabricate a bad check, or even knowingly accept one, and then let TeleCheck reimburse it. TeleCheck's insurance scheme was closely coupled to their credit bureau scheme. In other words, TeleCheck was able to control their risk on reimbursing bounced checks by making the decision of whether or not to accept the check at all. For businesses to claim reimbursement on a check, they had to prove that TeleCheck had agreed to guarantee it. They did that with an authorization number, a four-digit code provided over the phone that the cashier wrote on the back of the check before it was deposited.

Second, it was a business of its own: TeleCheck was a debt collector. And not just any debt collector, but one with the leverage of control over check acceptance at hundreds of businesses. TeleCheck presented this as a simple arrangement that does seem quite charming compared to the modern credit reporting industry: you could pass one bad check on TeleCheck's dime, but only one. Your identification remained in TeleCheck's database of unacceptable risks until you contacted them and made good on the original bounce. In other words, rip off TeleCheck and you'll never pay by check in this town again.

When businesses rejected checks, due to a negative TeleCheck response, they were instructed to provide the customer a "courtesy card" with an explanation of TeleCheck's operation, ways to contact them, and a reference number for the database entry that lead to the decline.


One of the interesting things about TeleCheck is its place in the history of check guarantee and its rapid growth. TeleCheck was not the first check guarantee service, Flagg personally knew of at least one other in New York City. For that reason, and likely others, TeleCheck had been given legal advice that they could not protect their business model by patents or other means. Flagg told the Honolulu Star-Advisor that "this means that we have to expand just as fast as possible before others get the same idea." And expand they did.

TeleCheck had barely started commercial operations, perhaps not started them at all, when they renamed from TeleCheck to TeleCheck International, signaling ambitions far beyond Hawaii. Existing operations were moved to TeleCheck Hawaii, a subsidiary, which would soon be joined by TeleCheck New York.

Today, checks seem an odd way to pay at retail because of the ubiquity and stronger security guarantees of debit and credit cards. In the 1960s, though, card payments were not widely available—if you weren't carrying cash, you paid by check. Checks were particularly problematic in the case of travelers, and that explains TeleCheck's Hawaiian origins. Checks are much easier to confidently accept when the bank, or even better the writer, are known to the merchant. That usually meant that personal checks had to be drawn on a local bank, at the very minimum, and initially even TeleCheck only guaranteed checks from Hawaiian banks.

But what, then, of tourists? Merchants would sometimes accept out-of-town checks with additional identification measures that ranged from copying down a driver's license to taking a photograph and thumbprint. Most just didn't, expecting visitors to obtain "travelers checks" issued by a well-known national bank and then usually cashed at another of that bank's own branches. Besides the inconvenience to the traveler, tourism economies like Honolulu's must have acutely felt the unwillingness of visitors to spend money when it involved multiple preparatory steps.

TeleCheck knew this going in, so expansion was inevitable. TeleCheck Hawaii and TeleCheck New York were set up as independent operations with their own computers and databases, but they were connected: bad check records from each were automatically transmitted to the other. As TeleCheck expanded, data sharing between regional operations built a distributed nationwide database, one that allowed a merchant in, say, Honolulu to accept a personal check from New York under full guarantee. If you asked Flagg, or Baer, all that was needed for complete nationwide acceptance of personal checks was a TeleCheck computer in every major city. Within a few years, TeleCheck International reorganized as TeleCheck Services, a franchise corporation. They started recruiting franchises in every state and, within a decade, Canada.

TeleCheck grew extremely rapidly, the kind of growth we might call a "unicorn" today. In 1969, TeleCheck estimated that their seven full-time operators took about 70,000 calls a month, 100,000 in the holiday shopping season. They guaranteed $6.75 million in purchases each month and paid out over a million a year in bad check reimbursements.

Check guarantees weren't everything, though. TeleCheck also diversified, expanding into just about every business they could think of until it started to seem comical. The same year that TeleCheck started, they acquired a company called Professional Services Inc. that did something we would now recognize as medical billing. It only took a couple of years for TeleCheck to dominate the Hawaiian medical and dental billing industry. In the words of one journalist, TeleCheck was hooked on computers, and hunted for any opportunity to make money off of the IBM 1440 and the larger machines that soon joined it.

Consider, for example, Match-Mate: the premier computerized matchmaking service of 1960s Hawaii. Lonely islanders filled out a questionnaire, conveniently distributed as a newspaper ad, and mailed it in with a payment of $7.00—by check, of course. The questionnaires were entered into TeleCheck computers and participants received a report with two likely matches and a booklet entitled Dining in the Islands. Considering that the book alone would "retail for $5.00 and has a full purchase price of $75.00" it was quite the deal for love. Well, maybe not, Match-Mate didn't last for long. It's interesting though that, alongside the address of TeleCheck International, the newspaper ads mentioned a CDC 3100.

Match-Mate questionnaire

The IBM 1440 was something of a budget computer, intended as a lower-end alternative to the "flagship" IBM 1401 mainframe for the many small businesses and accounting firms that couldn't afford a 1401. Within a couple of years, TeleCheck appeared in a directory of computer services provider as a consulting, accounting, payroll, etc. data processing firm with a Honeywell 200, a semi-clone of the IBM 1401 that could mostly run the same software, so they had apparently upgraded. Then, in 1966, Match-Mate associates TeleCheck with the CDC 3100. The 3100 was the runt of the CDC 3000 family but still ran about $120,000, over a million dollars today. Once again, all of the machines were likely rented, but still... in its first two years, TeleCheck acquired more computers than most established businesses would over five.

Some of TeleCheck's side ventures were quite logical. They had accounting and payroll businesses, which naturally fit the transaction processing skillset they had built for check guarantees. Consumer credit cards emerged during the 1960s, and TeleCheck was enthusiastic about those too. I don't think the scale of this operation was ever that large, but TeleCheck apparently handled online verification for some retailer credit cards in Hawaii, quite possibly by treating them as a special kind of check (a trick that TeleCheck would use repeatedly over the years to offer new services over existing equipment). Once again leading me to suspect that Flagg was doing some kind of engineering at MIT and in the Navy, TeleCheck's software situation seemed sophisticated for the 1960s. Newspaper articles describe real-time multitasking between batch processing of medical billing folios and online check inquiries. In a couple of years, they threw some sort of telephone order business and construction supply catalog into the mix.

But TeleCheck didn't keep to its computer roots. By the end of the 1960s, TeleCheck owned Honolulu Business College and Cannon's College of Commerce. They owned Minneapolis-based Boatel, manufacturer of houseboats and snowmobiles (Flagg seemed to have moved to Minneapolis at some point along the way). TeleCheck's Marine Science Division, made up mostly of subsidiary Pacific Submersibles, operated a Perry PC5C research submarine for which they were building a custom robotic manipulator. In 1968, TeleCheck Hawaii announced a complete rebranding to Data-Pac, a name that would better reflect their diversified interests. The franchise parent, TeleCheck International, kept the TeleCheck name.

In 1972, TeleCheck International went bankrupt.


The story of Harry Flagg is a complicated one, and I do not think that I have all of the information. There are just certain, you know, oddities. In the mid-'60s, Flagg was repeatedly lauded as the founder of TeleCheck. Robert Baer seems to have been around from the beginning, but it's not until later on, in the '70s and '80s, that he is widely referred to as TeleCheck's founder. Flagg is conspicuously absent from these versions of the company's history.

Similarly, the 1972 bankruptcy, triggered by the parent of a company TeleCheck had acquired calling the loans it granted to facilitate the acquisition, left little paper trail. Well, it was a bankruptcy, so there's a voluminous docket of the legal and financial details, but TeleCheck's leadership and their thinking are now opaque. We do know this: after the 1972 bankruptcy, Flagg was no longer in charge of TeleCheck International.

In 2005, a court ruled for the FTC in its case against Trek Alliance and its founders, including Harry Flagg and his son Kale Flagg. Flagg had moved to Arizona and founded Trek Alliance in 1997, a vague company with a confusing set of subsidiaries that included some sort of sales training. Primarily, though, Trek—which is unrelated to the better-known bicycle manufacturer—was a pyramid scheme. At least, that's what the court ruled. According to the FTC's complaint, Trek's "Independent Business Owners" sold water filters, cleaning products, nutritional supplements, and beauty aids. Trek's compensation plan, "one of the most lucrative in the industry," included a series of Bonuses and a 22-level Pay Plan assigned according to dollar volume of an Independent Business Owner's "downline."

This is, of course, the gold standard of business excellence in modern Utah, but Flagg was in Arizona and the 2000s. He and the other parties settled, denying fault but agreeing to shut down Trek. Together with their insurance company, they paid millions in restitution and suffered a permanent injunction against involvement in any multi-level marketing schemes.

Honestly, I'm not inclined to view Flagg as a fraudster, although it would be deliciously ironic considering where he started. I think he was just a little too ambitious and not quite cautious enough, entangling himself in everything that sounded like a good idea until it was just too many things to keep up. Still, how remarkable it is that the creator of the nation's most successful anti-check-fraud scheme would become separated from it, only to later be caught cashing checks from the top of a pyramid scheme. Now that's vision.


Despite TeleCheck's over-expansion and leadership troubles, the company was unstoppable. Baer became president of TeleCheck Hawaii while his son, Jeffery Baer, moved to Denver and established the headquarters of TeleCheck Services Inc., the new parent company of the TeleCheck franchise system. Those franchises multiplied: here in the Southwest, TeleCheck Texas was founded in 1982 and rebranded to TeleCheck Southwest two years later, when it bought the franchise rights for New Mexico, Oklahoma, and Arkansas. TeleCheck Texas had processed $325 million in checks in 1983. Elsewhere, there were TeleCheck franchises operating in more than half of the US states, several Canadian provinces, and Hong Kong.

Along with expansion came technical improvements. TeleCheck Hawaii, perhaps because of its origin as the first TeleCheck franchise, was always independently minded and eventually left the franchise system to go it alone as Uni-Check. Before they left, the introduced the first interactive voice response (IVR) check verification system. The fully-automated, touch-tone based IVR system expanded to other TeleCheck franchises, who named the automated voice "Samantha." TeleCheck liked it so much that they bought the developer, a company called Real-Share.

TeleCheck had other ideas, as well. Perhaps inspired by Ma Bell's "dataphone" efforts, TeleCheck launched the first point of sale electronic payment terminal I know of, if you are generous about the definition 2. The first-generation TeleCheck Terminal, introduced 1980, was a protrusion of the front of a standard pyramid phone that read magnetic stripes and sent them over the phone line. Instead of typing the digits from a check and driver's license on their phone keypads, merchants could call into TeleCheck and just swipe a card. Of course, this only worked for cards, drivers licenses and credit cards that TeleCheck processed, but it was a hit nonetheless.

Nationwide expansion of the TeleCheck service necessarily entailed nationwide expansion of the TeleCheck network. With each franchise operating independent computers that shared records with the other, TeleCheck was a sophisticated, networked operation by the standards of the time. As it turned out, they had help, from one of the most advanced computer networking companies. In 1980, TeleCheck Services was acquired by Tymshare.

Tymshare is, to me, one of the signature brands of the era of Business Computing. As the name suggests, twee spelling and all 3, Tymshare started out as a pay-by-the-minute time-sharing provider. Founded in the same year as TeleCheck by two ex-General Electric Computer employees, Tymshare grew out of UC Berkeley by selling time on an SDS 940 computer (initially borrowed from UC, later rented themselves) running the Berkeley Timesharing System. The combination of the 940 computer (itself mostly designed by UC Berkeley), the BTS operating system, and the dial-in timesharing model made Tymshare one of the most affordable routes to serious computing. The company was tremendously successful, but time-sharing was a short lived industry. Computers were getting faster, smaller, and cheaper every year, so the set of customers that needed a computer but could not afford their own got smaller and smaller. Tymshare probably saw that coming, because like early TeleCheck, they aggressively diversified into just about anything that a computer could do—including check guarantees.

As Tymshare grew, they purchased more computers, and larger. In the late '60s, they were operating dozens of machines running a largely custom operating system. They had economized on many of their acquisitions by running the acquired software on their time-sharing fleet, which was efficient but challenging for applications like TeleCheck that were designed around a central computer (for each franchise). Tymshare's business wasn't as simple as connecting a user to their nearest computer; they needed to accept dial-in calls from around the nation and then connect them to whichever computer ran the requested application, potentially on the other side of the country.

The solution, remarkably prescient of later wide-area networks, was a cutting-edge architecture of Varian 620 minicomputers that controlled banks of telephone modems and forwarded traffic to a set of SDS 940 computers called "supervisors." The role of the "supervisor" was very much what we would call a "router" today: they packetized data from the Varians and then routed it to other 940s according to a virtual circuit switching scheme. While the system initially connected dial-in users to 940s, it was readily extended to building arbitrary circuits between any of the serial interfaces of the Varian "edge nodes."

A business that wanted to offer a dial-in service to a wide area could save a tremendous amount of money on phone lines and modem banks by instead purchasing a small number of leased lines to a Tymshare data center. Their users could then call any of the Tymshare access phone numbers, where they would receive a command prompt from the answering Varian. When they typed the keyword assigned to the interconnected computer system, Tymshare's network built a circuit from a modem in one data center to a modem in another, connecting the caller to the customer's computer across their own nationwide network.

In 1976, Tymshare separated their network infrastructure into a separate company, Tymnet, which registered as a telecommunications carrier. Tymnet would ultimately outlive Tymshare itself, becoming the "industrial internet" before the contemporary buzzword or, really, the internet that spawned it. Despite some technical challenges originating from Tymnet's proto-internet architecture, everything from credit card transactions to supply chain notifications to consumer dial-up ISP traffic ran over Tymnet for decades after. Tymnet provided modem bank services to AOL, for example, and some of the vintage 1970s Tymshare dial-in numbers are still in service with various contract modem providers.


After its Tymshare acquisition, TeleCheck had a nationwide computer network, significant computer capacity, and an appetite for technical sophistication. It was a troubling time for a check guarantee firm, though. A new technology called Electronic Funds Transfer, or EFT, let retailers withdraw money directly from customer's accounts using only their debit cards. TeleCheck and Tymshare had actually found some business processing these transactions, but for the most part it was a separate industry that competed with checks. Baer cautioned that there was no reason to panic, as consumers would continue to use checks for many years to come, but there were clearly other things underway at TeleCheck. They were building their own transaction processing network.

I started thinking about TeleCheck because I was pumping gas and looking around for anything to distract me from how much it costs these days. Attached to the fuel dispenser, in between a half dozen other mandatory regulatory notices, was a sticker with the bright red and white TeleCheck logo. Rather than the "Your Check Is Welcome Here" verbiage used by early TeleCheck decals (back when many retailers did not yet accept personal checks from unknown customers), this one had decidedly less interesting text: "When you provide a check as payment, you authorize us either to use information from your check to make a one-time electronic fund transfer from your account or to process the payment as a check transaction."

In 1984, Tymshare, and TeleCheck with it, were sold to McDonnell-Douglas. McD-D, as I like to call them, was a formidable aerospace and defense contractor that was feeling an acute need to diversify as "peace broke out." They are also known, perhaps mostly due to their 1997 merger with Boeing and its effects on that company, as a bit of a backwater for actual engineering. They didn't hold on to Tymshare for very long, just a few years to give TeleCheck the curious property of a check guarantee service backed by F-15s.

Around the same time they were acquired, TeleCheck introduced a next-generation payment terminal that incorporated an MICR check reader along with support for newly standardized credit cards. This terminal followed the same basic model, of calling in via Tymnet and then sending the card or check contents over the phone line. This method of handling credit cards proved short lived as the industry reorganized and security requirements became far stricter, but it got TeleCheck equipment into a huge number of retailers. In 1989, McD-D, unsatisfied with the finance industry and perhaps ginning up the Gulf War, sold their entire software division. TeleCheck's increasing success as a general payments processor no doubt helped attract the buyer: First Data.

First Data probably qualifies as obscure, as they have few consumer-facing options, but they're a giant in payment processing. First Data provided the original infrastructure for both Visa and MasterCard before becoming part of American Express, who continued to operate the company as a general financial information systems company until they spun it out again. Back in the early '90s, though, TeleCheck found itself as a subsidiary of a company that also processed credit and debit transactions. It was only natural to unify those business lines into one, which TeleCheck called Electronic Check Acceptance.

Picture with me, in your mind's eye, the Verifone TRANZ 330. You have no idea what I'm talking about, of course, but if you're about my age or older you would recognize one. The Tranz 330 was the first widely successful credit card payments terminal, and anchors the Verifone brand name as a manufacturer of devices that verified payments over the phone. In practice, the TRANZ 330's main purpose was to read the data from a credit card, accept a keyed-in dollar amount, and then connect (usually over Tymnet) to a backend computer to authorize the transaction.

It could do much the same for checks: one of the features of the TRANZ 330 was check authorization, designed specifically for TeleCheck. The cashier could press a key to select check authorization, follow prompts to enter the check information and swipe the writer's driver's license, and then get back an authorization code (or a decline) on the terminal's screen.

The TRANZ 330 represented a milestone in two ways. It was the first payment terminal with TeleCheck support that resembled modern payment terminals in function. Earlier TeleCheck terminals were primarily phones with a payment card reader attached to them, the TRANZ 330 was not a phone at all and managed modem calls transparently to the user. Second, it represented a shift from TeleCheck providing a complete end-to-end solution to TeleCheck as one of a number of services supported by a common payment frontend.

After the First Data acquisition, TeleCheck was further integrated into other payment equipment, but it won back the branding. The TeleCheck Accelera and TeleCheck Eclipse, both manufactured by Verifone but bearing the TeleCheck logo, looked very much like every other credit card terminal but with the addition of an MICR check reader and slip printer. The added hardware allowed the terminals to read the check automatically, and also to print the authorization code on the back.

When these devices were introduced, the expectation was that a merchant would use the terminal to "authorize" a check (getting a TeleCheck guarantee on it), and then separately deposit the check with their bank. This wasn't all that different from how credit card transactions were handled at the time, with authorization usually done in real time (if at all) and "capture" (the actual payment) submitted as part of a batch at the end of the day. There was still a lot of paper handling involved, though, and during the 1990s it was clear that shipping slips of paper around the country was not a suitable long-term plan for checking.

Since the 1980s, a system called the Automated Clearing House (ACH) had been brewing among various bank coalitions and, later, the Federal Reserve Bank and an organization called NACHA: the National Automated Clearing House Association. ACH was one of the first standardized forms of computer payment, basically just a specification for a text file that contained the basic information for check-like transactions. Instead of exchanging paper checks, banks uploaded a text file to the ACH and then downloaded another text file later. Those files represented transactions in and out, processed in the banks and the ACH platform as a once-daily batch. ACH caught on for many of the same purposes that checks had fulfilled, like payroll (commonly called "direct deposit") and payments to savvy billers that wanted to avoid card payment fees (commonly called "e-check").


I put a lot of time into writing this, and I hope that you enjoy reading it. If you can spare a few dollars, consider supporting me on ko-fi. You'll receive an occasional extra, subscribers-only post, and defray the costs of providing artisanal, hand-built world wide web directly from Albuquerque, New Mexico.


Since an ACH transaction is pretty much just a line in a text file with the same numbers you would find on a check, it is superficially possible to take someone's check, copy down the numbers, and then enter it as an ACH transaction. In actual practice this wasn't allowed—until 2000. That year, NACHA adopted a provisional set of rules for "point of purchase entry," the on-the-fly creation of ACH transactions from a point of sale system. In the real world of retail, where cashiers are not excited to ask for someone's bank account and routing numbers, wait for the consumer to figure them out, and then try to key them in correctly, this pretty much universally turned into "check conversion."

The idea of check conversion is pretty simple: you "pay by check," but when the cashier takes the check from you, they actually put it into a terminal that reads the check information and uses it to create a technically unrelated transaction in an ACH batch. Most retailers use slip printers that add some tracking information (like a transaction number) to the back of the check, and stamp it "void" with a message that it has been "converted" into an ACH transaction. Some retailers would even return the voided check to the consumer as a sort of "receipt" of the ACH transaction, although I don't believe this practice is still common.

To the average person, there is hardly any difference: you write a number on a check, sign it, the money eventually disappears from your account. Since there are differences in the legalities of check and ACH processing, though, businesses are required to disclose that they convert checks to ACH. The main difference, in practice, is that ACH transactions tend to clear more quickly than checks. That does cause occasional problems for consumers who are "kiting," writing checks that they do not yet have the money to cover, since they may be counting on the slower process of exchanging slips of paper. In our modern world, traditional processing of checks has been completely eliminated in favor of image-based processing, which is something like ACH conversion built into the check clearing process itself. That means that the actual difference in processing time between ACH and checks is no longer as significant (although the availability of same-day ACH potentially complicates this, I do not believe that NACHA rules allow check conversions to be opted in for same-day clearing). The whole story of check conversion has become mostly a forgotten detail of the beautiful tapestry of transaction processing.

Naturally, TeleCheck integrated ACH conversion into their product. Many businesses now handle checks via payment terminals that perform a TeleCheck authorization and ACH conversion in one step, all facilitated by TeleCheck for a fee that is a bit lower than an equivalent payment card transaction. The function of writing TeleCheck authorization numbers is integrated into the slip printer, which used to endorse checks but now decorates them with ACH conversion details.


In 2019, First Data was acquired by financial technology giant Fiserv. Fiserv continues to operate TeleCheck to this day, but with ACH conversion now a commodity service and retail of personal checks so standard that we are forgetting about it, TeleCheck has started to look less like an interesting technology company and more like every other credit bureau.

Here's one of the reasons I find TeleCheck so interesting: search for them. I mean, just type it into Google or whatever. What do you see?

A very minimal corporate website, curiously at "getassistance.telecheck.com" (the bare "telecheck.com" redirects to the subdomain as well), with zero information except for law enforcement contact info, a form to look up declined checks, and a set of mandatory regulatory notices. "Victims of Human Trafficking" is a top-level navigation item, peeking out from above the hero banner of typing hands.

TeleCheck is now a ghost, a specter of financial technology past. Baer's 1980s predictions about EFT not cutting into TeleCheck's business fared well only if you ignore the closely related phenomenon of credit card networks. Nationwide, check volume, especially at retail, has collapsed to almost nothing. Fiserv continues to operate TeleCheck as, essentially, a legacy cash cow. They don't market the service at all, and maintain only the brand presence that is legally mandated of credit bureaus.

TeleCheck has a twisting, confusing corporate history, and besides Flagg's larger-than-life ambitions, credit reporting and debt collection are the reasons why. Consumer credit bureaus started to get a bad rap in the 1960s and have never recovered, they must be among the most hated brands in America today. Debt collectors have never had many friends. TeleCheck is, in various ways, both of those things, and they are now more important functions than technicalities of handling checks.

TeleCheck has been the target of dozens of lawsuits under the Fair Credit Reporting Act. To be fair, I don't think that they're worse than usual in this regard. One of the major implications of the FCRA is that credit bureaus can be liable for having incorrect information about consumers, but prior to the passage of the FCRA most credit bureaus were, shall we say, fast and loose about details.

Let's consider an example, of TeleCheck's practice of linked identities. If a person is writing bad checks serially, they will probably not write bad checks from the same account every time. So, as part of the "30 million facts" that TeleCheck's 1980s ads claimed their computers contained, TeleCheck saved relationships between identifiers. If you presented a driver's license and a check from one account, and then a month later at another store presented a driver's license and a check from a different account, TeleCheck permanently recorded the association between all three.

Say the first check bounced. If someone else, months later, at a different store, presented a check from the second account, TeleCheck would decline it. Why? Because they followed the links, from the second bank account to your driver's license to the first account. We might recognize this as taint analysis, and TeleCheck would follow connections through multiple steps until a bad check written by one person could result in "Code 4" declines of checks on different accounts by different people. To say that this was controversial with affected consumers understates the problems, and the way that account linking worked (especially between people of whom TeleCheck otherwise had no evidence of a relationship) became a legal morass. Several of TeleCheck's franchises seem to have left the TeleCheck brand in an effort to manage their risk, especially considering state-specific regulations on credit reporting.

As another way to manage regulatory complexity and liability, TeleCheck spun out its debt collection function into an independent company (although also owned by First Data) called TRS. Or TRS Recovery Services. Here's the thing, I am 90% sure that TRS stands for TeleCheck Recovery Services, but their own website says "TRS Recovery Services (TRS)", which would imply TeleCheck Recovery Services Recovery Services. I think the intent of the whole thing was to divorce the TRS brand from TeleCheck for reputational reasons (consider TRS has a totally different logo), which lead to some "TRS doesn't stand for anything" nonsense. TRS has a website that is almost but not quite identical to TeleCheck, with mandatory regulatory notices only, and they are universally hated as the people that hound you for life over a bounced check at Walmart.

Telecheck newspaper ad

Within the story of TeleCheck we see the full arc of payments technology: 1960s idealism at a new world in which everyone's checks are welcome, 2020s cynicism with an ailing conglomerate interested mostly in not losing lawsuits. TeleCheck is completely unexciting, the cartoon opposite of innovation, but it is very much still with it. Did you know that you can pay at amazon.com via direct ACH withdrawal from your checking account? Mass retailers are still surprisingly likely to accept checks as payment, and they are still, for the most part, doing that via TeleCheck. Even small businesses don't have to miss out: Fiserv also owns Clover, and Clover integrates TeleCheck electronic acceptance.

Deep inside Amazon's help system, under Payment Issues, an article explains how to "Correct a Failed Checking Account Authorization." Besides making sure you typed your account number correctly, the advice is: Call TRS Recovery Solutions. Sometime, somewhere, you must have written a bad check. A nationwide network of computers took note. Honolulu businessmen hobnobbed at a Boy Scout council meeting. TeleCheck took phone calls, McDonnell-Douglas forever changed Boeing, Harry Flagg went on to running MLMs. "Many Hawaii organizations currently are buying time on comparable computer facilities on the Mainland," Flagg said, when they bought the CDC 3100, the first in Hawaii. "Our installation will save them time and money." It might find them a date, too. The Nai'a explores the ocean, two small business colleges fold, two companies from Hawaii make competing claims about an obscure part of history. You're at WalMart, the total is $2, and the cashier is saying something about "Code 4." These things are all connected. They are all connected by Tymnet.

  1. Reported founding dates for TeleCheck range from 1964 to 1966, but a newspaper article about Flagg's new company ran in 1964 so that's what I'm going with. I think it took them 1-2 years to start operation.

  2. This is actually an interesting distinction, because Verifone is also a Hawaiian company and claims to have invented the first telephone-based payment terminal in 1981. That another Hawaiian company had a similar device in 1980 makes you wonder if they all knew each other.

  3. Tymshare was actually named after its founder, LaRoy Tymes, which is awesome.

28 Mar 22:36

Announcing Regenerator 2000

I am excited to announce the release of Regenerator 2000, a modern take on the classic Regenerator tool for the Commodore 64 and other 6502-based computers.

Regenerator 2000 Logo

Regenerator 2000 is an interactive disassembler for the CPU 6502, focused mostly on Commodore 8-bit computers. It features a modern Terminal User Interface (TUI) with features like x-ref, undo/redo, arrows, keyboard-driven navigation, and more! Better yet, it is multiplatform and runs natively on Windows, macOS, and Linux.

20 Mar 00:11

My Dinner With AI

I’ve been very critical of AI but have never really used it in depth and I feel that needs to change.

Don’t criticize what you don’t know.

I’m going to ignore the moral, ethical and privacy implementation of AI and just focus on the practical. Morally and ethically, AI is a train wreck. But I’m not going to focus on that.

I’m also going to only focus on AI for programming as that is my area of expertise, at least in this case.

I’ve done a lot of looking into how LLM’s work and I get the basics in the same way that I understand quantum physics. Enough to make me entertaining at a cocktail party, but not much after that.

I am starting a new game proto-type project using Raylib and it feels like this is a good place to experiment with AI. So for the next 30 days I’m going to use AI for programming and see how it goes.

I’ve been programming C since 1987 and C++ since 1993 so I have a lot of experience (although const in all but it’s basic form still messes me up, but I thinks that true for a lot of people).

I am using Claude AI (basic subscription) cli for general programming, Co-pilot in VSCode and Gemini for quick unrelated to programming questions.

Gemini

I was only using Gemini on the free trier that everyone with a Goggle account gets and it was worthless for programming, so we’ll skip it.

Copilot

Copilot comes integrated with VSCode, so it was easy to try out and doesn’t cost anything (I already pay for Github).

Copilot treads this line between being somewhat useful and maddeningly annoying.

Copilot is good at realizing I’m going to make the same small change to the next 10 lines and offer (mostly correctly) to make it for me. It’s not making big AI decision about my code base, just that I’m going to add Ox in front of a list of ints or that I’m going to fill out an enum. Good stuff. I do find that useful in the same way Intellisense can be useful.

What Copilot is not good at is making larger AI decisions about what I’m trying to add right after the if(. I’ve found it to be mostly wrong. It’s double or triple irritating that it interrupts my flow by popping up a huge block of text that is wrong with the excitement of a poorly trained intern on their first day.

I wish I could keep the first part and disable the second, but I have not found a way to do that. What it needs is a char limit to the amount of new text it will try and insert. If it’s about 20 characters then STFU.

Claude Code

Again, ignoring the ethics of the company that make Claude Code.

What Claude code is good at is doing rote things that don’t need a lot of (ironically) “Intelligence”.

I asked it to make be a .yml file for Github actions for building Mac, Linux and Windows version of my game and it did a 99% right version. Only thing wrong was the way it named the executable, but anyone could make that mistake. Maybe I could have been clearer.

Which brings me to a point. The clearer you are the better AI is. But at some point I’ve spent so much time writing the spec that I could have just done it myself. And it’s not just a high level spec, you have to get into the weeds.

I asked Claude Code to write a .py program that did texture packing, taking a folder of .png files and making a sprite sheet with a .json file describing where everything was. Basically what TexturePacker does.

After a few seconds I had to stop it because it was freely pulling in python packages that I didn’t have.

I then told it to use Pillow (PIL) and nothing else that wasn’t standard.

After a few minutes it had a runnable program, but not correct. It was putting the .json and sheet .png in different places despite me tell it they should be together. It was also processing the .png files twice, once to check for changes and another before adding them. I called it out and it apologized profusely and fix it.

There were several other instances where it wrote c++ code that was technically correct, but horrible inefficient like passing std::string around when it could have done std::string &. I caught this, but a junior dev might not have.

I had it write some c++ code to read the sprite sheet it had created and it took 5 failed attempts to get the origins right for rotation and scaling. I finally fixed it myself but it was a frustrating back and forth. Are new devs being trained to think this is normal?

I also had it write code so billboard sprites in my 3D particle system were always camera-facing. It never got that right and had generated such mess of code that I just scrapped the idea. Math is not my strong point so I didn’t feel like I could just fix it.

I also had a instance where a file was being read from the wrong path and instead of prepending the right path it tried to completely rewrite my library.

Ironically it also had a problem with const. It recompiled the program three times randomly changing where const appeared. I feel for ya.

I have spent a lot time over this experiment correcting AI.

I could go on and on. But I won’t.

Conclusions

AI for coding is very impressive. It is a lot more then a fancy auto-complete.

But it can also be very wrong and you don’t really know it unless you look closely and have the experience to do that and I fear that new devs won’t ever get that experience. I hear about new devs freaking out when they run out of tokens for AI. They are lost without it.

I have a few friends who work at companies where their managers are telling them to use AI. This isn’t coming from “it’s a useful tool” place, but rather that AI feels like magic to some managers who don’t program.

Someone once said that “AI feels amazing when it’s talking about a subject you know nothing about, but is laughably wrong when you do”. I couldn’t agree more.

I doubt I’ll use AI beyond this 30 day experiment. I spend too much time correcting it plus I enjoy programming.

Someone else also said “I want AI to do my laundry so I can make Art, not make my Art so I can do laundry”.

I also worry that AI was trained on real programmers, but now it will just be training on itself. There is no good end to that story.

I blame the I in AI. It’s not Intelligent and it’s important to remember that. It’s a really good encyclopedia of (stolen) knowledge but AI is not figuring things out on it’s own.

Autopilots in modern planes are amazing. They can take off and land the plane. But pilots are also legally requited to hand fly the plane several times a month. Training requires them to hand fly the simulators or simulators with broken autopilots. Pilots use the auto pilot as a tool not a crutch. It’s also not called a AI Pilot (yet).

More conclusions

AI will get better. It might even get as good as we see in Star Trek. I know I felt like Geordi programming the computer in engineering at times.

I grew up in a time where everyone was afraid computers would take their jobs. And they were right. Computers did. There are fewer longshoremen because of automated ports, short-haul commercial flights don’t need three pilots anymore, there aren’t rooms full of accountants hand writing figures into a book.

That will happen with AI.

Let’s just hope it also doesn’t make us too stupid in the process.

Of course, someone from 1793 would say the same thing about me as I could not survive the night in the wilderness.

In the meantime, I’m going to enjoy programming and not being a baby sitter to an AI programmer.

01 Dec 11:53

2026 Theme Announcement

In 2026, EMF goes to space!

Not just outer space, though – we want to celebrate discovery, engineering, exploration, and fascination with our home planet, too. From distant galaxies to our pale blue dot, neutron stars to the smallest microbacteria, we want you to get excited about what fascinates you out there.

In the coming months we’ll be publishing further inspiration, graphics, concepts, and posters for you to share and reuse in your own projects.

We’re looking for volunteers to help us ahead of the event to design and assemble site decoration, and contribute plants to our orbital greenhouse. If you know your way around a sewing machine or potting shed, or want to help the Design team in other ways, sign up here!

We love to see your contributions about whatever gets you nerdy and excited – we hope you’ll be inspired to join in.

It’s a very big universe out there. Gazing up at the night sky it can be hard to tell a satellite from a star from a galaxy, but further study is rewarded with a deeper understanding.

Find something that fascinates you, from the whorls on your fingertips to the spiral arms of a galaxy. Take that second look, and a third, and a fourth. Show us what you see, and help others explore.

We may be standing on a pale blue dot, but there’s definitely a lot going on down here.

20 Nov 23:54

TV: Video Power, en español Video Poder

by karakenio [Ze]

Tengo un vago recuerdo de infancia (1993?), de haber visto en TV por cable, un programa infantil de concurso donde los niños concursantes tenían un traje de velcro y salían corriendo a contra-tiempo para pegar en su cuerpo todos los videjuegos que pudieran. Tipo esos concursos donde la gente se lleva todo lo que pueda poner en su carrito de supermercado en menos de 1 minuto.

Sin llegar a ser un fever dream [sueño de fiebre], tengo la imagen muy borrosa de todo aquello; y lo más notable para mí es que siento que solo yo conozco o recuerdo ese programa. Lo habré visto 1 ó 2 veces; o tal vez vi un comercial (?).

Pero, ajá: Logré resolver el misterio,
como ha pasado otras veces en el blog.

Posteé en Facebook (mi plataforma principal online):

De vez en cuando me viene el recuerdo de un programa de TV por cable de los 90s. Donde los chamos concursantes tenían X segundos para agarrar todos los videjuegos que pudieran de los stands.
¿Alguien sabe de lo que hablo?
Si me acuerdo luego, busco bien.
Pero es de esas vainas que siento que solo yo vi.

Lo iba a dejar hasta ahí, y seguir con mis cosas. Pero me respondió Luis Daniel Urea con esta pista:

En USA network si no me equivoco

Y bueno, tuve que cerrar el negocio, para poder perder el tiempo que quedaba del día a ver si resolvía el misterio. Y sí, BOOM:

30 años después, y todavía te podría poner en mi Top de sueños a cumplir, el salir corriendo con un chaleco de colores y agarrando todos los juegos de Nintendo Americano [NES] que pueda. Por cierto en Caracas (Venezuela?) al NES le decíamos Nintendo Americano (una expresión «adeca», como dijo un pana alguna vez).

(Por cierto hace como 3 años, hicieron en una feria de antiguedades ‘tianguis’, una piñata para adultos, con juguetes piñateros de los 80 y 90s, para que los niños de 40 años pudiera revivir esa experienca nostálgica. Yo creo que a una reedición de Video Power le iría bien hoy en día).

No creas que hay mucha info al respecto de este programa, o un gran cult following. A pesar de haberme quitado mi duda inicial y descubrir el misterio, el que Video Power no tenga muchos fans, hace que siga manteniendo ese status de ser algo ‘que solo yo conozco/aprecio’. Te dejo este texto traducido:

Video Poder es una serie estadounidense de acción real y animación que se emitió originalmente en sindicación entre 1990 y 1992. Las dos temporadas de la serie fueron muy diferentes en cuanto a temática.

La primera temporada tenía un segmento de acción real en el que el actor Stivi Paskoski, en su papel, revisaba y adelantaba juegos que estaban disponibles o que iban a salir para consolas, además de dar pistas y consejos a los jugadores que tenían problemas para completar ciertas tareas en los juegos, y un segmento animado titulado «The Power Team», en el que Johnny Arcade (interpretado por Paskoski) lideraba a un grupo de personajes de juegos de NES publicados por Acclaim Entertainment, que intentaban recuperar los cartuchos de juego que los enviarían a casa, que habían sido robados por Mr. Big (el principal antagonista del juego de Midway, NARC).

La segunda temporada, por su parte, reestructuró completamente el formato. Aunque la serie seguía centrándose principalmente en los videojuegos, se convirtió en un programa de juegos, con Paskoski como presentador y Terry Lee Torok como copresentador. El segmento The Power Team también fue eliminado de la serie.

Un clip de lo más legendario del show, el final:

Pueden ver más episodios y extractos del programa en Youtube; imperdibles los comerciales, en los videos del comienzo los tienen sin editar. Mi aporte a la humanidad fue el GIF del comienzo, este post en español*, y puede que saque unas t-shirts con el logo (avísame si quieres comprar una).

* Hey, el programa se llegó a traducir al español y se llamaba Video Poder [info].

Si tienes la misma duda que Ciro Durán en mi post de FB:

Ya que estamos aquí, recuerdan un programa de concursos en cable donde los chamos concursantes estaban como en un set tipo juego de plataformas? Era obvio que era un chromakey/green screen así que los chamos estaban siempre perdidos tratando de llegar al otro lado. Pero eso, no recuerdo el nombre de ese programa de TV

La respuesta la puedes encontrar por acá, Nickelodeon Arcade (que por cierto yo también la tenía como recuerdo borroso):


El blog es gratuito, pero se mantiene con aportes de sus lectores habituales y esporádicos. Cualquier colaboración en bienvenida. Suscríbete en Patreon para más contenido (!) También acepto comisiones para trabajos creativos y de diseño/ilustración. En caso de pobreza extrema, pudieras tan solo agradecer en los comments.



20 Apr 13:56

Anchor Bolts

The biggest expense was installing the mantle ducts to keep the carbonate-silicate cycle operating.
08 Mar 13:44

Not so random music flow

by Ernesto Hernández-Novich
Posted on 2025-03-06 by Ernesto Hernández-Novich Tags: music, haskell

My previous post described a simple, yet sound example (pun intended), relating the use of entropy to generate a reasonable pleasant melody.

I complained about the perceived rigidness in terms of composition (pun twice as intended). Those are the shackles of straightforward imperative code. I posit a data-flow or stream-based mindset, supported by an expressive language with already available abstractions, leads to better composability.

I also mentioned in passing that I would rather have an algebraic representation for music, be it randomnly generated or not. This post will not explore that yet, but will hopefully set an understandable playing ground (thrice the pun!) on top of which to build.

Let’s rewrite while exploiting Haskell’s static type system to ensure explicit low-level value conversion, and a few niceties to make our toy CLI tool amenable.

A Haskell redo

String is the Poor Man’s datatype. – me, mocking the unityped and the barely typed, since 2005

Programmers who rely on strings to model things are doomed to trip, unless they are extremely disciplined and in good shape. They rarely are either. The Perl prototype I wrote is string based, because the language provides no cheap practical alternative. That’s why I had to open() the file with a particular flag, and go from Perl-string to Perl-character, and then to machine integer, back and forth. And interleaving I/O with transformations being careful of said intertwining. Pain and suffering.

We can do much better.

Haskell provides many «string-like» datatypes. We use [String] to teach, Text for human-readable things, and ByteString for binary data. Since we are going to read an arbitrarily long stream of bytes from /dev/urandom, we will use lazy ByteStrings: they are implemented in a way that us programmers don’t need to worry about buffering, instead thinking of «the contents» as a very long string of Word8 (unsigned bytes) that can be manipulated with an API that behaves just like String.

Even though they «look like lists» and their API resembles list manipulation, they are extremely performant: the compiler will analyze the way functions are composed, and transform the code into tight assembler loops, thanks to code transformations that are possible only over pure code. I’ve talked about it for regular lists, it’s called fusion, and makes ByteString related stuff extremely performant.

This re-write will also make it easier to change the music scale, having a single implementation that can do both 8-bit and 32-bit quantization. I’ll use additional function parameters to «pass down» this information: there is a cleaner equivalent solution we will explore in a future post.

Rewriting the tone generator

We will be processing a lazy ByteString, each element being a Word8 value. Using one Word8 value, choose the particular note from a scale, compute its base amplitude in relation to A440, and then amplify it. The thing is, Haskell has a static type system, Word8 and Double (needed for sin) don’t mix, forcing us to write explicit conversions.

type Scale = [Word8]

major :: Scale
major = [0,2,4,5,7,9,11,12]

aSampleU8 :: Scale -> Word8 -> Double -> Word8
aSampleU8 s w8 t =
  round $ volume * sin ( a4
                       * 2.0 ** ( fromIntegral n / 12.0 )
                       * t
                       )
  where
    a4 :: Double
    a4 = 440 * pi
    n :: Word8
    n = s !! (fromIntegral w8 `mod` length s)

I’ve chosen to model Scale as a simple list of Word8, with exactly the same intentions as my previous post. You can guess there’s another value minor :: Scale with the corresponding interval steps.

Note how aSampleU8’s first argument s is a Scale. That way, we can call it with major, minor, or whatever other weird scale you come up with, and the computation will still be the same. A common idiom in functional programming, particularly helpful when the language supports currying, is to place less general arguments first, so they can be provided partially, to complete the function application at a later place with «hardcoded» context, so to speak.

The second argument is the Word8 value read from the entropy source. Since we need to use the same seed value w8 to generate multiple points of the sinusoid, it becomes less general than the time, the third argument.

The function implementation is basically a re-write of the Perl version, but using explicit conversions. I’ve written the literal Double values so they’re easy to identify. We need two explicit conversions here, and it’s a great opportunity to learn about

fromIntegral :: (Integral a, Num b) => a -> b

able to take any Integral (Int, Word8, Integer,…) and do an explicit conversion (not a coercion, how uneducated) into any destination type that is Num (any numeric type). The particular fromIntegral implementation to use will be inferred by the target type, because it is mandatory for every Num type to have

fromInteger :: Integer -> a

and Integer is Integral. If the source type is not Integer, it has to be Integral which mandates

toInteger :: a -> Integer

The compiler is able to insert the optimized composition fromInteger . toInteger, possibly fusing it at the assembly level (read as, the right bit-shuffling).

That’s why this line

    n = s !! (fromIntegral w8 `mod` length s)

works nicely. We have length producing an Int, which forces

ghci> :type mod
mod :: Integral a => a -> a -> a
ghci> :type mod @Int
mod @Int :: Integral Int => Int -> Int -> Int

We can’t use w8 :: Word8 directly as mod’s first argument, but then

ghci> :type fromIntegral @Word8 @Int
fromIntegral @Word8 @Int
  :: (Integral Word8, Num Int) => Word8 -> Int

The resulting Int matches the type for the second argument of (!!), needed to get the proper Word8 out of the list. Not to worry, this will turn into extremely efficient machine code, but the conversions are explicit and proven to be right by the type system and not my (over)confidence. It’s impossible for a type conversion bug to happen here, and I don’t need to write tests to feel warm and fuzzy about it.

Figuring out why and how fromIntegral works within the argument for sin is left as an exercise for the reader. Also how round makes our Double into a Word8.

Collect ALL the points!

The Perl code resorted to nested loops so that foreach random sample, a chunk of the sinusoid was generated. Haskell only has recursion. But we rather use implicit recursion, because they are folds that, in turn, become very efficient machine code.

So, given a particular Scale and one Word8 sample, we can generate a collection of Word8 samples like this

import qualified Data.ByteString.Lazy as B

toSamplesU8 :: Scale -> Word8 -> B.ByteString
toSamplesU8 s w8 = B.pack $ map (aSampleU8 s w8) [0, 0.0001 .. 1]

Implicit iteration provided by map and, as anticipated, currying aSampleU8 by fixing both the scale and single sample. Now, the result is a plain [Word8] that requires packing to become a proper ByteString.

Now, picture the arbitrarily long stream of Word8 coming from entropy. We can take one of those, and turn it into a new sub-stream of Word8 – the points of the sinusoid. We want to repeatedly do this over the stream, but create a combined stream: we want to concatenate the resulting sub-streams produced by each sample-mapping generation. This is a frequent operation on regular lists as well as BytesString streams you must learn to identify it:

phase :: (B.ByteString -> B.ByteString)
      -> Scale
      -> B.ByteString -> B.ByteString
phase t s r = t $ B.concatMap (toSamplesU8 s) r

Given the scale s and the arbitrarily long entropy stream r, concatMap will apply the curried toSamplesU8 s to each element of the stream, and combine all the resulting partial streams into a single output stream. We don’t have to worry about buffering, we don’t have to worry about «how many», we don’t have to worry (in this case) about memory leaks, because fusion will take care of making this a very efficient loop.

Now, there’s a mistery t argument that, if you read the type signature, turns out to be a transformation function that takes a ByteString and produces a new one. We’ll use that to make our program more flexible.

Let’s step back for a second and contemplate how we can use what we have so far. Having phase we effectively abstracted the problem of generating the sound phases given a Scale and the stream of entropy. It should come as no surprise that there’s a way to read a file and turn it into a lazy ByteString, and a way to print a lazy ByteString. In this context, lazy means the library will take care of buffering and «reading on demand», as well as interleaving the reading with the printing. «Wait, are you saying the [Word8] is a lie and there’s never a list in memory?»… precisely.

So, we can actually have our string of random stuff with a simple

B.readFile "/dev/urandom" >>= B.putStr . phase id major

because the stream generated by B.readFile is fed to phase id major (the major scale and the identity function), to then get printed. It doesn’t get much cleaner than that, thanks to the IO Monad and currying.

Since my sound device accepts unsigned 8-bit integers, I can make this into an executable and it works in the same way the 8-bit Perl script did: except faster and using constant memory.

I find your lack of fusion disturbing

I wrote two Perl scripts, one for unsigned 8-bit output, another for signed 32-bit little endian integers. The first parameter for phase was placed there so that, thanks to currying, arbitrary conversions can be placed while the stream is being produced. For unsigned 8-bit nothing needs converting, so we put id, and the compiler is smart enough to «do nothing».

So, how do we go about converting a ByteString in such a way that we take four Word8s at a time, interpret them as a 32-bit little endian integers, and then tuck them back as Word8 so they become a new ByteString?

If you ever need to grab bytes from the wire (or a disk) and analyze their structure (network package, audio file, cryptographic material) and them punt them elsewhere, you are unmarshalling and marshalling. Haskell’s ecosystem provides a brutally performant solution to this problem for ByteString, thanks to the binary library.

The library provides two pure monads: Get to express generic multi-step unmarshalling, and Put to express generic multi-step marshalling. In our case, we’ll use Get to express «turn a ByteString into a list of 32-bit unsigned integers», and Put to express «turn a list of 32-bit unsigned integers into a ByteString», and them sequence their effects together. I’m going to say it again, for the haters in the back: thanks to fusion this will become a machine language tight loop.

Consider

    getInt32s :: B.ByteString -> [Int32]
    getInt32s bs = runGet getInt32le four : getInt32s rest
      where
        (four,rest) = B.splitAt 4 bs

receiving an arbitrarily long ByteString. It grabs the first four elements, while keeping the rest. Then it «runs» the Get monad to extract a single 32-bit little endian integer (Int32), putting it as the first element of a plain Haskell list. Then it uses explicit recursion to continue working. Remember, laziness works such that the Int32 can be immediately used by whatever function is consuming it – the list will never exist. Let me say my line: thanks to fusion.

Now consider

    toS32LEs :: [Int32] -> Put
    toS32LEs = mapM_ putInt32le

which is a beautiful example of why monads are so expressive. This function is obviously written to feed from getInt32s, but using a «monadic map»: putInt32le is a monadic action that works within the Put monad, by taking a single Int32 and marshalling it, but only when the Put monad is effectively performed.

Now mapM_ has a very interesting type

ghci> :type mapM_
mapM_ :: (Foldable t, Monad m) => (a -> m b) -> t a -> m ()
ghci> :type mapM_ putInt32le
mapM_ putInt32le :: Foldable t => t GHC.Int.Int32 -> PutM ()

as it allows to map a monadic action, putInt32le in this case, over every value inside a Foldable (and [Int32] is a Foldable). The result type is a bit puzzling as it produces an empty value () within the PutM monad. Turns out, mapM_ is actually building the sequence of steps, one per each element of the list, and then sequencing them one after the other ready to go, but it does not perform them: the list of individual values becomes a sequence of monadic actions that will parse them when the PutM monad is run, i.e. a «program» for PutM has been created, but not run yet.

It follows we need to write

asS32LEs :: B.ByteString -> B.ByteString
asS32LEs = runPut . toS32LEs . getInt32s

The argument being implicit («point free style» or η-conversion), getInt32s unmarshalls from Word8 stream into a [Int32] fed to a lazy-generated infinitely long sequence of putInt32le steps that are performed by runPut. In comes an arbitrarily long ByteString, its elements unmarshalled four at a time, turned into Int32, that then marshalled into a ByteString. Guess what, lists are never built, there’s exactly one toS32LEs being performed at a time. That’s what implicit recursion, laziness, and… fusion, get you. You write at an extremely high level of abstraction, don’t need to know how many, how to buffer… just connect the stream.

MVP! MVP! MVP!

We can finally put together a main program that can selectively use the major or minor scale, and spit unsigned bytes or signed 32-bit little endian marshalled as unsigned bytes (they are the same but different). It looks like this

data S = Major | Minor
       deriving (Show,Eq)

data F = Low | High
       deriving (Show,Eq)

data Opts = Opts { scale  :: S
                 , format :: F
                 }
          deriving SHow

main :: IO ()
main = do
  opts <- execParser options
  let s = case scale opts of
            Major -> major
            Minor -> minor
  let f = case format opts of
            Low   -> id
            High  -> asS32LEs
  B.readFile "/dev/urandom" >>= B.putStr . phase f s

We «parse» some options that will obviously come from the command line arguments, to get an opts value. The couple of let statements use the fields of the opts value to set s as the desired scale, and f as the desired transformation function, and then just the processing stream we’ve described before. I named my executable mm-exe, and run it like this

$ stack exec mm-exe -- --help
M -- random music generator

Usage: mm-exe [-m|--minor] [-h|--high]

  Random music generator

  Available options:
    -m,--minor               Minor scale
    -h,--high                High resolution
    -h,--help                Show this help text
$ stack exec mm-exe | aplay -c 2 -f U8 -r 8000
Playing raw data 'stdin' : Unsigned 8 bit, Rate 8000 Hz, Stereo
(... major scale noises ...)
$ stack exec mm-exe -- --minor| aplay -c 2 -f U8 -r 8000
Playing raw data 'stdin' : Unsigned 8 bit, Rate 8000 Hz, Stereo
(... minor scale noises ...)
$ stack exec mm-exe -- --minor --high | aplay -c 2 -f S32_LE -r 8000
Playing raw data 'stdin' : Signed 32 bit Little Endian, Rate 8000 Hz, Stereo
(... minor scale higher quality noises ...)
$ stack exec mm-exe -- --wtf
Invalid option `--wtf'

Usage: mm-exe [-m|--minor] [-h|--high]

  Random music generator

All this CLI argument functionality comes from the absolutely wonderful optparse-applicative library. Had to write this, hopefully self-explanatory, program and flag definitions

options :: ParserInfo Opts
options = info (flags <**> helper)
               (fullDesc <> progDesc "Random music generator"
                         <> header "M -- random music generator"
               )
  where
    flags = Opts <$> flag Major Minor ( long "minor" <> short 'm' <> help "Minor scale")
                 <*> flag Low   High  ( long "high"  <> short 'h' <> help "High resolution")

and make sure to execParser options at the top of main.

Ship it!

Play on

Ignoring extra functionality and type signatures, the basic Haskell version is about as long as the Perl one. However, there’s way better composability here. If we need more output formats, we simply write an additional marshalling part. Refactoring is easier, as there are no nested loop with interleaved side effects: stream processing is 100% pure Haskell, only needing I/O at both ends. The resulting code is a very tight loop interleaving I/O with data transformation but the compiler is going to write it for me.

There’s still one thing bugging me: this program outputs raw bytes so music is being produced by the hardware thanks to aplay. The stream produces a wave corresponding to a tone but not the tones themselves. I would rather replace phase with something like

notes :: B.ByteString -> Music

where Music represents, among other things, pitches and octaves. Then have said Music transformed in the way musicians do (transpose, reverse, arrange in chords), and then have it play with different instruments.

That would make it quite my tempo…

08 Mar 13:43

The Balatro Timeline

by LocalThunk

It’s been approximately 3 years since I began work on Balatro - and in that time I have personally documented almost nothing about the journey. This is something that has bothered me since the game launched. I am constantly forgetting major moments in development or milestones. It’s about time I start writing down what happened, I say better late than never!

This is an account of everything related to Balatro development in chronological order, up to and including the launch day. This should tell the story but more importantly it will serve as a source of truth for me when I look back on this time in a few years to recall the details correctly. I already notice talking to friends, players, and media that I contradict myself on some of the details from time to time. I’ll try and keep this up to date as I remember more about what happened.


December 13th, 2021

  • This is the creation date for template, a folder in my Learning/Lua/ directory. This is the creation of the project that would eventually become Balatro.

  • I had saved up about 3 weeks of vacation time from my IT job, and since they didn’t allow people to accrue vacation year over year I just took off a bunch of time in December to work on some kind of project. At the very beginning of this time off I was working on a game called Autohike. I had been working on this game for a few months at this point but I wasn’t really feeling it anymore, so very early into my vacation I pivoted to cleaning up the code I had created and making a template for a new game.

  • The initial idea on Dec 13th was to create an online multiplayer version of Big Cheat, a game that a few friends and I had invented many years prior and played a ton based on the card games Big 2 and Cheat.

  • This whole first week was mostly me trying to upgrade the engine I built with Autohike to better fit a card game.

December 20th, 2021

  • This is the creation date for CardGame, my current production working folder for all Balatro source code. I never bothered changing the name.

  • I remember spending a long time making custom pixel art for the red deck back and all the playing cards. It was the first time I had tried making proper pixel art.

End of December 2021

  • I had a very weird prototype. There were no Jokers, there were no blinds to select, the upgrade system consisted of randomly choosing a card from the deck and slapping a weird ‘enhancement’ on it. But it definitely had the bones of Balatro at this point, so it was pretty clear that I abandoned the online Big Cheat idea immediately.

  • The game had the CHIP X MULT mechanic already. I really don’t know where this idea came from but it seemed very natural for a scoring system. Earlier in December I was watching videos of Luck Be a Landlord from Northernlion and that whole score attack concept really fuelled the direction I wanted to go with this game from this point onward.

  • I also made a very conscious effort not to play any more roguelike games starting now. I want to be crystal clear here and say that this was not because I thought it would result in a better game, this was because making games is my hobby, releasing them and making money from them is not, so naively exploring roguelike design (and especially deckbuilder design, since I had never played one before) was part of the fun for me. I wanted to make mistakes, I wanted to reinvent the wheel, I didn’t want to borrow tried-and-true designs from existing games. That likely would have resulted in a more tight game but it would have defeated the purpose of what I love about making games.

  • I started sharing progress with a few of my friends, but no builds were shared yet.

January 2022

  • My vacation is over, but I am now fully immersed and obsessed. This is my favourite feeling in the world. When I was back in University I would routinely stay up until the wee hours of the morning working on my weird game projects and greet my parents while they made their morning coffee. I got back into this groove and starting in January I was hooked.

  • Evenings and weekends were Joker Poker time (my working title for Balatro, likely decided on early this month).

  • I iterate through some weird versions of the game, including a version where the only way to upgrade anything is to upgrade the cards in your deck in a sort of pseudo-shop, and those cards can be upgraded multiple times (think like Super Auto Pets, pets have different XP/levels when combined, same idea)

February 2022

  • I started adding Jokers to the game. I also was temporarily working remotely out of province for a few months starting at this time and found that my Balatro dev time took a pretty big hit.

  • I started adding different boss blinds to the game.

March 2022

  • Early March marked an important event in the history of Balatro, and I wanted to describe why things happened the way they did. I stopped working on the project entirely.

  • I have been making games for about 10 years now and I have been doing visual art projects for much longer, and a very important habit I have developed for creative hobby projects is to stop working on something when I no longer feel the drive. This is for 2 main reasons; first, it allows me to move on to the next idea without totally burning out on the last thing. Second, and more importantly in this case, it allows me to take time off guilt free and possibly come back to the project later on without wrapping it in negative emotions.

    That leads us to…

May 2022

  • We are so back. This is the last month I am away from home but I start carving out my evenings and weekends to work on Joker Poker again.

  • My brain is teeming with ideas, I’m so excited about the game again, and I have a ton of momentum.

  • I create (and quickly scrap) a bunch of new systems to test, such as a separate currency for rerolls outside of $, and a ‘golden seal’ to be added to playing cards when you skip all blinds that returns that card to hand after it has been played

  • It is during this burst of productivity that I first start thinking about a possible Steam release of the game. This was the first time in my then 8 years of game development that I had even considered publicly releasing a game I had made. Normally they just end up going to a couple of friends, but honestly the main purpose of my games wasn’t for them to be consumed, but rather for them just to be made.

  • Another factor me starting to consider a Steam launch was that my partner was almost finished her PhD and the writing was on the wall that we may be moving out of province for good as she looked for a job. If this was the case, I would have to quit my job and look for work. I thought why not try and get a game developer job somewhere? To accomplish that, I thought having a Steam game in my portfolio would be a big benefit.

  • I really didn’t have any commercial aspirations at this time. All I knew about Steam is that there were about a million games on there and that very few could realistically make a living, so I didn’t think it was possible to consider it.

  • It was in during this month that my eventual developer name - LocalThunk - was incepted.

  • My partner was learning to code in R at the time, and she asked me “How do you name your variables?” I went on some rant about casing, using descriptive words, underscores, etc. She waits until I am finished and says “I like to call mine thunk”. I thought that was just about the funniest thing I had ever heard.

  • The way variables are declared in Lua is (sometimes) with the local keyword, thus local thunk was born! I wouldn’t choose this name for quite a while yet but this is the moment I looked back on when I was finally ready to create a developer handle online.

June 2022

  • Still working on the game (details of what exactly I was adding at this point are fuzzy). I think by this time Joker Poker has economy, a bunch of Jokers, and generally resembles the current version of Balatro.

  • I share a new version of the game with some friends.

August 2022

  • One of those friends gets back to me months later with very high praise. Unexpectedly high. He mentions that he has played the beta for dozens of hours.

  • In all my years of sharing projects with friends I had never received feedback like that. I couldn’t believe it, and it really pushed me over the edge to fully flesh out this idea. It was really great knowing that this game was actually being enjoyed by someone I know, and I wanted to prove to him, my other friends, my partner, my family, and myself that I could make something really fun and interesting.

  • At this point I started really increasing scope. More Jokers, Controller support, touch support, Jimbo the cheeky tutorial character, a soundtrack, proper sound effects, special editions for cards, and way more got added to the docket. I felt like scope creep incarnate, and it was amazing.

January 2023

  • As anticipated, my partner got a really great job in a different province and I quit my job to move there with her. I was really excited for this new chapter in our lives, and I also decided to not look for work for the next 3-6 months and develop Joker Poker full time.

  • It was the perfect opportunity to work as a game dev full time (even if for free) and I didn’t know if I would ever have the chance again. I had some money saved up, so starting this month I really leaned into it.

February 2023

  • I took a brief vacation to surf in California with some friends. I showed them the new build I had been working on - a version that would end up on Steam within a few months. This version now had Vouchers!

March 2023

  • This month I contacted Luis Clemente on the freelance website Fiverr and he delivered an absolutely amazing soundtrack for Joker Poker. Really knocked it out of the park. I was very nervous about this because it was (at the time) the only money I had spent or planned to spend on the game.

  • I also showed the game to some friends during a discord call, and one friend made the suggestion that there should be a flaming effect on your score if you get a really good hand. I hated the idea at first, but I sat with it for a while and knew I was wrong. That flaming score effect really fit perfectly with the game I was trying to create. Sure glad I listened to him!

  • I workshopped a list of ~20 names for the game. I saw that the game ‘Joker Poker’ was already an app on the app store, but beyond that it felt just a bit too silly for the vibe I was shooting for. I sent this list of new names to my friends and got all of their feedback. Balatro was among that list but nobody picked it. Something about it really stuck with me so I just went with it.

April 2023

  • I started creating store assets this month. I created a trailer, got my screenshots, and paid the very scary $100 Steam fee to upload a game. I also had a pretty massive code refactor at this point.

  • The name is officially Balatro.

  • I was pretty overwhelmed by Steam, just starting to look into things like how to launch an indie game? was really freaking me out. There are so many games released on Steam every day and so much noise about what you should and shouldn’t do, what type of game you should make, what method you should use to market it. It was a real shock to me that even though I had this game that I poured so much time into that it didn’t really seem to matter. I didn’t think this game would sell any copies before this and my initial research into Steam reinforced that idea to me.

May 2023

  • I downloaded Slay the Spire and played it for the first time. Holy shit. now that is a game.

  • I did this because I was having some troubles in my controller implementation and I wanted to see how they handled controller inputs for a card game but I ended up getting sucked in. Thank goodness I avoided playing it until now because I surely would have just copied their incredible design (intentionally or subconsciously).

  • In late May, I uploaded the first public Beta build to Steam, set my store page live, and saw exactly what I expected to happen happened: nothing. The fact that I wasn’t anticipating any fanfare probably saved me some heartache because I didn’t end up feeling dejected from the very tepid response my free Beta was getting from players.

  • Feedback started coming in from the Beta players. This marks the beginning of a more player-focused development strategy I would adhere to instead of just listening to my gut all the time.

  • By end of May, Balatro had 48 wishlists on Steam

Early June 2023

  • This is about the time when Balatro stopped being a pure hobby for me.

  • YouTubers started covering the game. It was mostly small and indie game specific channels, but the attention definitely started growing over time. The Discord server I had made for feedback about the game started taking up more of my time. However, at this time there was nothing to indicate that the game was going to start popping off.

  • Even so - I started really thinking about the commercial viability of the game. Could I actually make some money from this? I saw a glimmer of a possibility that this might turn into something.

  • My partner and I went on a big backcountry canoe trip for a few days. During this trip we talked about the future of this game and what it meant for my work. Even with the mild momentum I was ready to move on, and since back in January I told her this would be at most 6 months I felt that I shouldn’t push it too much longer. It had already been a year and a half of dev at this point.

  • I prepared to launch the game in 2 weeks. The release date went live on my Steam page, and I started thinking about moving on to looking for a job again and getting back into normal software/IT work.

  • I got a DM on twitter from a scout at Playstack, my eventual publisher. I was super excited but this also complicated things. This was a very tumultuous time in the history of the game because I was in limbo between nothing will come of this game and I want to move on with my life and what if I could do this as a job?

  • By June 10th, Balatro had 183 wishlists on Steam

Mid June 2023

  • Dan Gheesling plays Balatro on his stream. This is a massive streamer for my game (and a great guy!), and it starts making my wishlist numbers spike.

  • Other medium sized Youtubers and streamers start playing the game, it is building momentum. At this point the game was spreading entirely from word of mouth. I had posted a few things on Reddit and Twitter but really nothing that ever gained meaningful traction; now the game had inertia by itself.

  • It becomes clear that I should pursue the game a little further. I am approached by a few different publishers around now. I get some really great support and advice from my parents about everything going on. I strongly consider going it alone, but I also know that I need some help with all the public facing things, marketing, possible porting and just navigating this new thing I knew nothing about.

  • I hire a lawyer to help me with all the contract negotiations, help me set up a business, and I decide to sign with Playstack after speaking to studios that have worked with them in the past. They can replace whatever salary I would have made from an IT job, so this is my job now!

  • While this was a very exciting time, it was also incredibly stressful. I’m not used to dealing with so many people, and I felt pressure from so many different directions that I wasn’t expecting.

  • I created a demo version of the game that only lasted 50 rounds after my free Beta tester queue started getting bigger. At this point I had 300 beta testers for the full game and didn’t really need any more. This demo strategy wasn’t the best idea, but it allowed people to see what Balatro was all about without just getting the full game for free. The demo was getting around 10-20 concurrent players, which was really mind blowing for me at the time.

  • By end of June, Balatro had 2440 wishlists on Steam

July 2023

  • I release big updates around this time for the playtesters. I add the skip tag system, and I totally revamp the tutorial to basically what it is today.

  • The trajectory for the game appears at this time to be big enough for me to replace my income for a year at least. This was so exciting to me, I hadn’t really thought about it before because I was doing this work on my weekends and during my evenings anyway.

  • After a bunch of negotiations I finally sign a publishing deal with Playstack. I won’t go too far into whether or not people should or shouldn’t have a publisher, but for me at this time it was crucial that I have some publishing help. They outlined a very sensible release plan for the game (one that we adhered to almost exactly until launch in Feb 2024) and they really expanded the audience a ton with support for more languages and porting on day 1 for every major console.

  • My demo is getting some attention from streamers still, but one day in mid July, Northernlion was pitched the game on his stream from Dan Gheesling and ended up playing it that same day. I was in the chat watching, totally dumbfounded, that someone I watched on Twitch/YouTube for over a decade at that point was playing my game. I text all my friends about it, they are all shocked as well. At the time I was more concerned about a potential crash or something going horribly wrong but I did also try to just sit, watch, and enjoy the surreal moment.

  • Northernlion was playing the 50 round demo version - and after he quickly ran through all 50 rounds I got his attention and gave him a key to play the full beta version. I think some people weren’t super fond of this because he got special treatment - but honestly I was more in fan mode than dev mode. Looking back I’m glad I gave him the key - that was a massive moment for my game and even though it was tricky to navigate.

  • Playstack and I decide that we should take down the demo and come up with a better strategy than limiting players to 50 rounds

  • By end of July, Balatro had 28,661 wishlists on Steam

August 2023

  • The new demo plan is in place, and most of August is me preparing for a September release of this demo. The plan now is to make the demo content limited instead of round limited. This means that players can play as much as they want, which is much more sensible.

  • The demo should be ready a couple weeks before the Fall 2023 Next Fest in October. Next Fest is a festival set up by Steam to shine a spotlight on upcoming games for the platform. While we weren’t planning on adding Balatro as an official entry in the October 2023 Next Fest - we thought having a demo available at the same time as the festival might gather some incidental attention.

  • This is when my sleep and heart started having issues. I have talked about this a tiny bit in the past, but I really struggled with my physical and mental health from this time onward all the way up to launch. This was entirely because of the stress around dealing with the public, players, and the pressure to get everything done before February 2024.

  • We start exploring the feasibility of porting the game to different platforms. I won’t talk about this much more because of the agreements around those platforms, but suffice it to say this ended up being a very large portion of my work time over the coming months

  • By end of August, Balatro had 38,011 wishlists on Steam

September 2023

  • I start converting all the text in the game to an external english file to facilitate localization to other languages. I didn’t plan on having this game translated when it was a hobby so I had to do this all after the fact. It didn’t end up being too horrible, just some rework that I could have saved if I knew I was doing this from the beginning.

  • Near the end of September, I launch the new demo on Steam. People generally really like it! I try and make some tweaks based on player feedback, but it’s clear this approach is much better than limiting players to 50 rounds.

  • The last few months I have been very involved in the Balatro Discord server. I try and chat with players all the time, and I try to take their criticisms seriously. I feel like the demo and beta players knew more about how to play the game properly than I did, so I found it was really important to listen to what they had to say about the game in order to properly assess what I need to change or tweak for balance.

  • By the time the September demo is live, there are tons of players chatting in the Discord server all the time. I feel pretty overwhelmed by it all but I feel like I need to interact with them constantly. I didn’t want to waste the opportunity, and we also chatted a lot about general design things that players might be interested to see in the full 1.0 version of the game.

  • By end of September, Balatro had 49,791 wishlists on Steam

October 2023

  • Next Fest goes live early in the month, and as anticipated we do catch a lot of attention for having a demo live at the same time. We get pretty good coverage in general around this time from YouTubers, streamers, and even some media.

  • We originally planned on taking the demo down a week or so after the Next Fest ended, but so many people kept playing it that we decided to keep that version of the demo live for quite a while longer.

  • I started finalizing the content plan for the 1.0 version of the game. The biggest addition is the inclusion of a sort of ‘ascension’ system. This is from Slay the Spire (see? Told you I’d steal from it) but I think it was a super cool way to add difficulty and give players a sort of checklist to work through.

  • I also had a meeting with Playstack this month where I described to them the final content in the game, including ‘120 Jokers’. Later that week I had another meeting with them, and someone mentioned something about 150 Jokers. I couldn’t remember if I accidentally said I was going to make 150 or if they misheard me, but either way I thought that 150 was a much better number so I added 30 more Jokers to the plan.

  • I can’t remember if it was now or in September, but we enlist the help of Maarten De Meyer to help with our porting efforts. Maarten was absolutely instrumental in getting Balatro on every platform, and frankly he’s a Love2D wizard.

  • My sleep and my heart are getting worse. Every few nights I need to sleep sitting upright on the couch because sleeping while lying down kept getting interrupted by my heart. I stupidly thought that I couldn’t handle going to the doctor while dealing with all the dev and business stuff swirling around at the time. I felt totally overwhelmed.

  • By end of October, Balatro had 77,380 wishlists on Steam

November 2023

  • We keep the demo up for the entire month of November since our player numbers are still pretty high. More of the same from previous months. This is basically crunch time, so for the next couple months I’m just working on finishing everything in my plan.

  • The game is to be launched right after the following Next Fest scheduled for February 2024. This means that we want a new version of the demo with slightly different content to be available for that Next Fest, so I am working on that on the side.

  • Additionally, we have an idea to make an invitational tournament for streamers with an early build of this new version of the demo. This is scheduled to happen some time in late January 2024.

  • By end of November, Balatro had 87,280 wishlists on Steam

December 2023

  • More of the same. By this time it’s clear we can launch on every platform thanks to monumental effort by both Maarten and his wizardry and also a massive amount of optimization to my code base that I had worked on over the previous months.

  • We decide to take down the demo by the end of December because we want some breathing room before we launch the next iteration

  • By end of December, Balatro had 94,212 wishlists on Steam

January 2024

  • My heart is really bothering me. I routinely can’t sleep until the sun comes up, and my mental health is really suffering. I love working on the game but working on it so publicly and with such an intensity for so long is really catching up with me.

  • One night I am watching the movie The Abyss with my partner and suddenly I get tunnel vision, my heart is absolutely pounding and I feel like something is seriously wrong. I sit on the couch for a while, totally freaked out. I’m really scared.

  • I call my doctor and see him the next morning for an appointment. He assures me that this wasn’t a heart attack or heart failure but an anxiety attack. I am not normally an anxious person and have never had issues with this in the past but I think the intense stress for such a long time has done quite a bit of damage.

  • He asks me if my work has been stressful lately. I don’t even know how to explain.

  • January 19th is the invitational tournament. We have 6 streamers participate. This was actually a really great moment for me to sit back and watch the fruits of my labour for once - not work on the game and just enjoy.

  • My partner and I order sushi, sit down and watch the multi-stream broadcast by MurphyObv playing a hilarious character. It was probably my favourite moment of the entire pre-launch phase of the game. Just really wonderful to see this silly game come together for a moment and watch a bunch of people participate like a community.

  • Hafu wins, because of course she does. She’s Hafu!

  • We announce the launch date of the game; February 20th

  • By end of January, Balatro had 114,977 wishlists on Steam

February 2024

  • I was still working on implementing stuff in the game in February believe it or not. I was in super crunch mode.

  • I had a private server made a little while earlier with testers from the Balatro community that I felt exhibited an exceptional ability to give feedback, understand the vision of the game, and analyze the flaws with it. I think this was instrumental in making sure that the 1.0 version of the game really started fun out the gate. I chatted with them a ton this month.

  • I knew there would be tweaks after launch but this was important to ensure that something catastrophic didn’t happen.

  • We have a really strong Next Fest. We get a ton of coverage from streamers, YouTubers, and media yet again. Balatro is one of the most played games of the festival.

  • I start properly playing the game myself about a week before launch - and it’s actually fun. I have a pretty emotional moment where I feel like I did the thing I set out to do. Finally. I made the fun game I wanted to make.

  • We have an absurd amount of momentum leading up to launch day, far more than what anyone was expecting.

  • February 19th rolls around. The 19th is the review embargo day, and Playstack did a truly exceptional job getting this game in the hands of traditional media to write their reviews before launch. I didn’t know what to expect, this was a pretty weird game after all. Average ratings of 6 or 7?

  • The first big review is from PC Gamer: 91. Playstack and I are on a call when they break the news to me, and I can tell they are pretty shocked. I am shocked. That rating doesn’t make any sense.

  • More ratings pour in, and by the time the day is done we are sitting above 90 on both Metacritic and OpenCritic. I wasn’t even thinking that this was a possibility, but it sure did build a ton of hype for launch day. I don’t think I would have rated Balatro higher than an 8 and I made the damn thing.

  • By launch day, Balatro has 208,401 wishlists on Steam

  • February 20th, 8am PST is when the game is supposed to go live across all platforms. We launch it 15 minutes early. I cannot describe to you how nervous I am that I’ll be patching like a madman not only all day, but all month. I told my partner that I won’t be making any plans until the end of March because I fully anticipated the sky to fall and for me to be working 24/7 to fix the inevitable disaster.

  • To my shock - nothing goes wrong. People love the game, they’re having a great time. I think there may have been some small bugs but definitely nothing massive, nothing like what I was anticipating. Streamers are playing it, media is writing about it. I have so many texts from friends and family. It is the most surreal day of my life.

  • The launch is 10-20 times larger than we were anticipating.

  • One of the moments that sticks out to me is the moment I first checked the Steam page for sales. I checked this page for the first time a couple hours after launch almost on a whim, not expecting to see any sales information yet, and we had already sold 50,000 copies on Steam. The numbers on that page made no sense. This was meant for just a few of my friends and yet somehow all these strangers chose to buy it. The revenue on that page after just a couple hours was over $600,000, far more money than I’ve made in my entire life. By the end of launch day, Balatro has sold 119,000 units on Steam alone.

  • The other moment from that day that sticks out is later that day when my partner got home from work. She had been following along all day and struggling to get any work done, and when she got home she gave me a great big hug. I wasn’t sure I’d even survive the launch but here we were. I could not have done this without her. We ordered burgers for supper and popped a bottle of champagne to celebrate.

27 Feb 09:20

This is how I GHCi

by Ernesto Hernández-Novich
Posted on 2025-02-15 by Ernesto Hernández-Novich Tags: haskell

Being a Haskell developer means going back and forth between a programming editor and the REPL (GHCi). You go a long way, faster, when you try things in the REPL to «get a feel», before making them part of your actual programs. That’s why I always have a terminal window running ghci, generally started as stack repl, because I prefer Stack to handle per-project dependencies.

I configure my preferred Stack Resolver for the «global project» under ~/.stack/global-project/stack.yaml

resolver: ltrs-23.8
packages: []
extra-deps: []

and then install several Haskell tools into my local path. I get up-to-date outstanding Haskell applications such as pandoc and Shake, and always use this global project for trying things without having to start a new project.

Once installed, a «vanilla» REPL startup would look like this

$ stack repl

Note: No project package targets specified, so a plain ghci will be started with
      no package hiding or package options.

      You are using snapshot: lts-23.8

      If you want to use package hiding and options, then you can try one of the
      following:

      * If you want to start a different project configuration than
        /home/emhn/.stack/global-project/stack.yaml, then you can use stack init
        to create a new stack.yaml for the packages in the current directory.

      * If you want to use the project configuration at
      * /home/emhn/.stack/global-project/stack.yaml,
      * then you can add to its 'packages' field.

Configuring GHCi with the following packages:
GHCi, version 9.8.4: https://www.haskell.org/ghc/  :? for help
Loaded GHCi configuration from /home/emhn/.cache/stack/ghci-script/2a3bbd58/ghci-script
ghci>

It’s possible to customize GHCi’s configuration by having a file named .ghci placed at the project’s root directory. That is, you could have different customizations for different projects. If you place the file at ~/.ghci, it will become your personal configuration both for the «global project» and any other project, unless overriden by per project ones.

What do I need?

In my line of work and research interests, I often have to deal with obscure libraries, Unicode, deeply nested data structures, and «make it faster so the natives go crazy». This means I need GHCI to provide, at least:

  • Type-contextual automatic conversion for literal strings. This saves me having to type the explicit conversion from «poor man’s datatype» run-of-the-mill literal strings into the tagged type a library might need. A consequence of becoming all too familiar with the plethora of Haskell string types,

  • Selective access to documentation. Not only to get the obvious answer to «what does this function do?», but also to ask the cleverer «is there a function with this type signature?».

  • A way to print glyphs for Unicode String ([Char]), when I’m dealing with a toy experiment, or libraries that haven’t switched to Text. This would be my «standard» mode of operations, since it’s easier to experiment with Unicode String and then switch everything to Text.

  • A way to pretty-print results, when I know them to be complex and need the extra readability. Say long lists of sum types with product types inside. This would be my «alternate» mode of operations.

  • The result’s general type signature, and runtime statistics for the last evaluated expression.

  • Lean history management, basic name auto completion, and editing capabilites matching my editor of choice. Keep in mind I usually have a programming editor open, so these editing needs are for whatever I’m trying within GHCi.

Let’s address these needs.

hoogle searches

Hoogle is a Haskell API to search libraries either by explicit function name (e.g. search for map) or by approximate type signature (e.g. search for (a -> b) -> [a] -> [b]). And it’s possible to use it offline from the command-line.

While having internet access, install the hoogle executable, and have it download the latest API collection for all libraries available from Stackage.

$ stack install hoogle
(... downloads and builds from source ...)
$ ls -l .local/bin/hoogle
-rwxr-xr-x 1 emhn emhn 42882464 Feb 10 09:07 .local/bin/hoogle
$ hoogle generate
(... downloads ALL THE API's ...)

From that point on, you will have offline immediate access to hoogle searches over the local database. Just repeat the hoogle generate every now and then to update the database.

There are two major ways to use it:

  • «What does a function do?»

    $ hoogle --info map
    map :: (a -> b) -> [a] -> [b]
    base Prelude
    map f xs is the list obtained by
    applying f to each element of xs, i.e.,
    
    
    map f [x1, x2, ..., xn] == [f x1, f x2, ..., f xn]
    map f [x1, x2, ...] == [f x1, f x2, ...]
    
    
    
    >>> map (+1) [1, 2, 3]
  • «Are there functions with this particular type signature?»

    $ hoogle '(a -> b) -> [a] -> [b]'
    Prelude map :: (a -> b) -> [a] -> [b]
    Data.List map :: (a -> b) -> [a] -> [b]
    GHC.Base map :: (a -> b) -> [a] -> [b]
    GHC.List map :: (a -> b) -> [a] -> [b]
    GHC.OldList map :: (a -> b) -> [a] -> [b]
    Test.Hspec.Discover map :: (a -> b) -> [a] -> [b]
    Distribution.Compat.Prelude.Internal map :: (a -> b) -> [a] -> [b]
    Prelude.Compat map :: () => (a -> b) -> [a] -> [b]
    BasePrelude map :: () => (a -> b) -> [a] -> [b]
    Data.GI.Base.ShortPrelude map :: (a -> b) -> [a] -> [b]
    -- plus more results not shown, pass --count=20 to see more

Unicode and Pretty-Printing

The simplest libraries available for these tasks can be installed within the «global project» with

$ stack install utf8-string
$ stack install unicode-show
$ stack install pretty-show

Coming up with a working .ghci

GHCi will read commands from .ghci on startup. These commands are either built in GHCi commands as described in its documentation, or Haskell expressions that get evaluated. The following are the current contents of my ~/.ghci in the same order they appear.

I start by setting up my preferred external editor and a lean prompt. That’s all I need, really.

:set editor      /usr/bin/gvim
:set prompt      "λ> "
:set prompt-cont " | "

The type-contextual automatic conversion of literal strings is a frequenly used GHC extension, so I enable it

:set -XOverloadedStrings

You can integrate any external program into GHCi by defining a new GHCi command. These new commands can have any name we like, and be a combination of GHCi builtins alongside Haskell code.

I can run hoogle from the command line, by virtue of it being installed in ~/.local/bin/ and that directory being mentioned in my PATH. GHCi has the ability to run arbitrary shell commands using the built-in :!, so I could run :!hoogle manually, but passing arguments becomes tedious and tricky. I rather define a GHCi command that builds a line that will be evaluated by the shell, and also shell escape the argument by wrapping it with single quotes («quoting»), and any internal single quote as well.

let qArg arg = "'" ++ concatMap (\c -> if c == '\'' then "'\"'\"'" else [c]) arg ++ "'"

:def! search pure . (":! hoogle " ++) . qArg
:def! manual pure . (":! hoogle --info " ++) . qArg

First, note that qArg is a straight Haskell function definition, but using let because that’s what GHCi would need. Then, the :search command is defined as the expression

pure . (":! hoogle " ++) . qArg

SO, when I type

ghci> :search (a -> b) -> [a] -> [b]

it gets evaluated as

    (pure . (":! hoogle " ++) . qArg) "(a -> b) -> [a] -> [b]"
      {- (.) definition, twice -}
    pure ((":! hoogle " ++) ( qArg "(a -> b) -> [a] -> [b]" ))
      {- apply qArg -}
    pure ((":! hoogle " ++) "'(a -> b) -> [a] -> [b]'")
      {- apply sectioned (++) -}
    pure (":! hoogle '(a -> b) -> [a] -> [b]'")
      {- produce value in IO context for GHCi -}
    ":! hoogle '(a -> b) -> [a] -> [b]'"

and then GHCi executes the line: nothing more than built-in :!, running hoogle, its output being displayed

ghci> :search (a -> b) -> [a] -> [b]
Prelude map :: (a -> b) -> [a] -> [b]
Data.List map :: (a -> b) -> [a] -> [b]
GHC.Base map :: (a -> b) -> [a] -> [b]
GHC.List map :: (a -> b) -> [a] -> [b]
GHC.OldList map :: (a -> b) -> [a] -> [b]
Test.Hspec.Discover map :: (a -> b) -> [a] -> [b]
Distribution.Compat.Prelude.Internal map :: (a -> b) -> [a] -> [b]
Prelude.Compat map :: () => (a -> b) -> [a] -> [b]
BasePrelude map :: () => (a -> b) -> [a] -> [b]
Data.GI.Base.ShortPrelude map :: (a -> b) -> [a] -> [b]
-- plus more results not shown, pass --count=20 to see more

It should be easy to add the --count option to list as many suggestions as you want. The :manual command, is the shortcut to get a function’s documentation

λ> :manual map
map :: (a -> b) -> [a] -> [b]
base Prelude
map f xs is the list obtained by
applying f to each element of xs, i.e.,


map f [x1, x2, ..., xn] == [f x1, f x2, ..., f xn]
map f [x1, x2, ...] == [f x1, f x2, ...]



>>> map (+1) [1, 2, 3]

As for printing expression results, and being able to go back and forth from my preferred Unicode mode to the optional «pretty printing» mode, I need a couple of commands. Let’s load two of the three libraries above

import Text.Show.Pretty (ppShow)
import Text.Show.Unicode (uprint)

Yes, plain Haskell import because we’re going to write some code to build a couple of GHCi commands. These commands require multiple lines, so we’re going to use GHCi multiline form, mostly for readability reasons,

:{
:def! pretty   const $ pure $ unlines [
  ":unset +s +t",
  "pp = putStrLn . ppShow",
  ":seti -interactive-print pp",
  ":set +s +t"
]
:}

to define the new GHCI command pretty taking no arguments. Thanks to unlines, all lines in the list will be combined with a '\n' in between them, creating a long line with the <ENTER>s I would type interactively. The bracketing with :unset and :set is to reduce the «noise» when loading this definition: those flags print type information and statistics for every command.

The interesting part is function pp’s definition. Every time you evaluate a Haskell expression within GHCi, it will try to use Haskell’s print over the result, as long as the value’s type has a Show instance. Now

ppShow :: Show a => a -> String

takes any value having a Show instance, and transforms it into a «pretty printed» String. Using putStrLn on that will naturally print it, conforming to expected GHCi behavior. Finally, setting interactive-print to the newly defined function pp, makes GHCi use it for printing results, instead of the default print. Contrast:

ghci> [1,2,3]
[1,2,3]
ghci> :pretty
ghci> [1,2,3]
[ 1 , 2 , 3 ]

The definition for unicode should be easy to follow as well

:{
:def! unicode   const $ pure $ unlines [
  ":unset +s +t",
  ":seti -interactive-print uprint",
  ":set +s +t"
]
:}

Haskell String are Unicode strings, but print does not convert Unicode entities into the corresponding glyphs, hence the need to use uprint. Contrast:

ghci> "toño en acción"
"to\241o en acci\243n"
ghci> :unicode
ghci> "toño en acción"
"toño en acción"

I also have a convenience function to quickly reload ~/.ghci without needing to restart GHCi. This let’s me make temporary changes to ~/.ghci and load it quickly. At this point, it should be self-explanatory

:def! rr const $ pure ":script ~/.ghci"

The last two commands in my ghci establish my preferred mode of printing, and enable resulting type and evaluation statistics

:unicode
:set +s +t

That last line is the one enabling runtime statistics (+s) and result value type (+t) for every evaluated expression, i.e.

λ> map (*2) $ take 5 $ filter even [1..]
[4,8,12,16,20]
it :: Integral b => [b]             <-- +t
(0.01 secs, 117,760 bytes)          <-- +s

That’s why definitios for :search and :manual disable and reenable them. And since I have a :rr command for reloading, then ~/.ghci’s first line has to be

:unset +s +t

so that I we don’t get messages for every expression being redefined when reloading.

History and editing capabilites

GHCi uses the powerful Haskeline library for command-line editing and history management. Haskeline settings go in ~/.haskeline and they are actual Haskell values using Haskeline constructors. My preferences are

editMode: Vi
completionType: menuCompletion
completionPromptLimit: Just 8
historyDuplicates: IgnoreConsecutive
maxHistorySize: Nothing

Vim has been my preferred programming editor for over 30 years. The above configuration allows me to work in GHCi and, at any given time hit <ESC> to enter vi-mode, so that I can:

  • Move up and down command history with k and j (or the arrows)

  • Recall and search history with Ctrl-R.

  • Use vi line movement or editing keystrokes at will, including yank-and-paste or delete-and-paste.

  • Hit <TAB> to get completion based on all names currently in scope.

    λ> ma<TAB>
    map       mapM      mapM_     mappend   max       maxBound  maximum   maybe
  • GHCi history file will be stored at ~/.ghc/ghci_history. There’s no limit to the number of lines to store there, and consecutive duplicate commands will be stored exactly once.

What else can be done?

Sometimes I disable compiler warnings

:set -w

when working on bringing old-code (pre GHC 9) to modern standards. That’s the main reason why I added the :rr command

There’s a lot of placeholders you can add to the prompt, to put things like current working directory, loaded modules, and other things. To me, they are nothing more than a waste of space, and am not willing to have multi-line prompts.

It’s also possible to extend :pretty to colorize the output using hscolour. Not my cup of tea.

I always install HLint and had an :hlint command for a while. Nowadays I rather let the Haskell Language Server provide suggestions within the dedicated Vim session.

26 Feb 14:47

The Year Of The Blog

by v buckenham

It's the year of the blog! Everyone's writing one! Everyone's setting up an RSS reader so they can make sure to catch when their friends have posted. It's the cool new thing! Sometimes they're called newsletters, but we know the truth - they're really blogs! Everyone's fleeing from the monopoly platforms, there's no longer a genuine case to be made that they're good, only that they're there. And when they find they still have a desire to put some thoughts online... it's a blog, baby!

Okay, maybe it's just me. And a subset of my friends.

But seriously, it's nice. Lower your standards. Unkink your writing, it's got all tangled up from fitting into that tiny box. You can put a pictures into them if you want, but you don't have to. There's no algorithm to beat, no best time of day or necessary face pic needed. You can refer to a thing you wrote in the past in the future!

I'm on a Discord server where every time we post a blog post it gets posted in the channel. That's nice. Often there's a nice chat! I bet there will be once I post this one. I want to set it up for other Discords I'm in. Not to post Official Updates on The Thing the Discord server is about... but just to let everyone see the blogs everyone else is writing. Get excited about it! Respond to each other at length!

A blog feels a little safe, a little cozy, but also free and clear. It's simultaneously public - you just need a link! But it's also hidden - it's not on the feeds, it's a click away. In today's internet we're either hiding away from the world in our little communities or we're hyper-optimising our public personas... but a blog is a secret third way to post!

Cohost got me started - you could write a post, and it felt appropriate at any length, and any level of thought. Two word shitpost or essay-length research report, people demonstrated they could appreciate either. It felt freeing after years optimising myself for Twitter! Cohost died, though, and to get that feeling again I had to spin up some infrastructure myself. It was annoying to do! I know people are trying to make it easier, and I wish them luck. But I succeeded, and I know you can too - join me here, writing posts of variable length and putting them online for people to read, join me in celebrating... The Year Of The Blog!


Some echoes to this post I wanna link here:

The year of the blog? + how to easily put a Bluesky feed widget on your website
After all, if you're 30 or older and you're reading a blog, then you may have to consider the possibility that you don't actually have any coolness left to preserve. You might as well just join Bluesky and give up. 
A bunch of us are here and we're posting. As if it's 2025... The fuckin' year of the blog!!
an RSS bot in a group chat is our era’s best salon
Okay, I’m being too cute by half with the title. But! Walk with me here.
maya.landMaya

Thrilled to have Maya respond - I also know what she means in terms of being sceptical about the blog's status as a safer space... I definitely take the warning! But still there's a psychological element to it, this is my turf in a way... Anyway, good thoughts about salon culture and the conversations that can flow from blog posts...

And on that I should say that I have had a few people mention this idea of it being the year of the blog, talking about how they feel like they should start blogging... More meta posts to come, I think.

23 Mar: ah! like this one:

hey, just wanted to say your year of the blog post was super inspiring to me + i've been writing blog posts way more often since reading it!! thanks for the cool work!

kaylee rowena 🫀 (@kayleerowena.com) 2025-03-23T13:17:08.865Z

and in fact going to look at her blog, i see this post:

the year of the blog - kaylee rowena
i’m trying to blog more! listen to me ramble about it!
kaylee rowena
or: not necessarily, quality, but effort, or polish, or high standards that mean i'm constantly thinking about making blog posts and rarely ever sitting down to write them, because to write them i'd need to open my code editors and mess with the layout and maybe i should do something interesting with the layout of the page instead of having it be a basic text-heavy page, maybe there's something i can do with the form of the website to make it unique and noteworthy ——

no! stop! bad! just write things!

i get bogged down in the process of things a lot, whether it's blog posts or comics or sewing projects or party planning. i always want to make something Different™ — as if the form of the thing is more notable than what i'm actually trying to say with it. i'm trying to convince myself that saying something badly is better than stressing about saying it interestingly and ending up never saying anything at all.

yes yes yes yes!!

13 Mar 23:54

The Declaration of Game Designer Independence

by Daniel Cook

On a cool, clear Austin weekend, a group of experienced game designers gathered for their yearly retreat.  At night they swapped stories of an industry in turmoil.  As social games and mobile games rewrite the landscape, power struggles between business and design dominate and designers find themselves being sidelined or abused.  And the products they work on suffer horribly as a result.  It is a time when musty old assumptions are questioned.  It is also a rare opportunity to identify universal best practices that can help us navigate new platforms, new genres and new gaming experiences.

So during the day, we asked ourselves some hard questions.  When was design successful?  How do designers hurt their own credibility and effectiveness?  This is a group that has shipped hundreds of games serving well over 100 million players. Over the past two decades, we've personally seen game titans rise and fall.   Surely there are patterns and cycles.  So we listed dozens of examples of great design environments and dozens more of times where design remained shackled. And over and over again, the same themes came up.

To guide game designers and the profession forward we wrote down a Declaration of Game Designer Independence. This document is primarily for the designers who run their own companies or the creative directors who own the creative process.  It is also for the designers in the trenches, who aspire to a leadership role.

The following is a code for how great, visionary designers should behave. It rises up from an immense well of hard fought experience accumulated over decades of real world design.  When followed, these practices sustain an environment where design thrives and revolutionary games are regularly brought forth into the world.

The Declaration of 

Game Designer Independence

1. Without game design, there is nothing

You can get rid of visuals, music, business or technology and we will still make great games.

2. Designers must drive the vision of the game

We are prime movers, not replaceable cogs.

3. We dedicate ourselves to the lifelong mastery of design

Dilettantes need not apply.

4. We strive to be renaissance designers

We fluently speak the languages of game development and business:
  • We speak the language of creative. All art and music ultimately serves the game play.
  • We speak the language of production. Game design determines the scope and need for the content that production shepherds.
  • We speak the language of engineering. Technology is one tool that enable the experiences designers choose.
  • We speak the language of business. Modern monetization, retention and distribution are directly driven by game systems.

5. We will not be silenced

We tirelessly promote our vision both internally and to the public.

6. We fearlessly embrace new markets and trends

We then reinvent them to be better.

7. We demand the freedom to fail

Design advances through experimentation.

8. We have a choice:

Create with our own voices or sell our talents into servitude.

My personal thoughts

Not everything here is easy.  To live up to this declaration, you likely need to be a better designer than you are right now.  Still, always remember:  You are not a slave. You are not a servant.  You are not a cog-like employee.  You are a creative force.  And most importantly you have a choice for how you wish to spend your time on this earth.  You can choose to take control of your life and change the world for the better in the process. 

I personally left a large company with a steady paycheck in order to take control of my creative destiny.  Now I've got multiple game designs speeding towards completion, I'm working with people with souls and the future looks amazing.  This is easily the most productive and exciting time of my life.  And the only reason it happened was because I realized a fundamental truth:  Design works best when it leads, not when it serves. 

If you support the Declaration, drop a comment below.  Pass it on via Twitter, Facebook, Email, Forums and more.  Pass it on to the people who need it the most. 

take care, 
Danc. 

PS: For a more in-depth look at our report, check out the Project Horseshoe website.  There are some wonderful reports this year.