Movie collecting 2.1 (update!)

Ever since I resumed my movie collecting hobby, I've been occasionally improving the Movie collection -app I made. The movie collection itself has also grown considerably, perhaps worryingly so... 😅 Read on to find out more!

At the end of last year, I implemented a simple tagging system where each movie can be given tags. Those tags can be searched for, and I'm mainly using them to categorize my boutique label purchases. With the tags I can easily see my entire collection of, say, Arrow Video releases. I also use tags to mark some distinct release features like a slipcover or steelbook. Previously I was (ab)using the description field for these kind of things, but the tags make it much more convenient. And nicer, with those nifty colors! 🙂

Some movies with tags

The tags are hidden behind the tag-symbol on the main listing by default, and can be opened with a click. Clicking on a tag will show all movies tagged with it.

Tags could also be used to do genre categories, — I intentionally don't have a genre field stored for the movies — but I don't see myself needing the genre information anyway. And if I did, I'd add it as a field and do some IMDb-lookups and assign them automatically. Genre information might come in handy for statistics purposes, though.

Look at dem tags fly!

Look at dem tags fly!

Speaking of statistics: I've also revamped the Collection statistics-page with much nicer card-based layout and more of interesting (well, to me anyway!) stats. New stats include:

  • Monthly purchases / watches per year: shows the monthly sums for both purchases and movie watches in color-coded blocks, divided into yearly buckets. Starting from 2022, because that's when I resumed the hobby and started tracking these again.
    • Looking at the picture below, I must admit, these numbers do scare me slightly! I knew I bought a few movies last year, but was not expecting this... 😱💸
    • I was meaning to add monthly costs to those blocks, but didn't quite find the courage to do it, yet. Perhaps the costs could be hidden by default, hmm.. 🤔
  • Most watched movies (TOP 30), Foreign Regions and Duplicates: tabbed listings of the TOP 30 most watched titles, releases of foreign regions (non-European region locked discs) and duplicates (mainly releases in different formats).
  • TV-series: titles that are TV-series (season releases or the whole series) are marked as such and excluded from most of the stats. E.g. separate TV-seasons will skew the production year counts, running time averages etc., so I've left them out from calculations.

* * *

Revamped stats
Monthly purchases..

In addition to the above, I've also improved the backend code: cleaner TypeScript, switch from CommonJS modules to ESM, more robust data scraping for the EAN-barcode scanning and better automatic backups. Among other tweaks. I'm also making more steps towards client side rendering; for example the tags are fully handled in client side with the backend only supplying the tags JSON.

* * *

PS. A tiny bit of absolutely useless trivia: this blog post was authored in Markdown instead of typing in HTML. I improved my trusty old static site generator to support .md-input files. Not that you can see the difference in the final output, but it does make typing these posts slightly nicer for me. You just gotta take my word for it! 😄 (Shout-out to Markdig / .NET Markdown processor — bolted that onto my generator and with trivial amount of additional work I was good to go. Nice!)


Movie collecting 2.0

Long time no see! 👋 (Has it really been over 5 years? Damn! 😲)

This post is about movies. In the recent years I've been getting back into my movie collecting (as physical media, that is) hobby and started watching movies a lot more once again. Fun times. But it quickly started to annoy me that I had no up-to-date list of my movies; while I did have a software for this exact purpose, I had been neglecting updating the collection and stopped altogether around 05/2010 (hmm... surely this isn't related to our first child being born...!? 🤔😅). While the software was alright, the workflow was too cumbersome: edit the collection on my home PC, export a CSV-file, upload it to a web server. Annoying. Especially with an increasing backlog of additions. All this meant that effectively the collection was about 12 years out of date. Not good!

I started thinking: wouldn't it be convenient to have a web app for managing the movie collection? That way I'd always have up-to-date list in my pocket, and when buying new discs I could easily check if I already had the movie (or had it in DVD while buying a Blu-ray upgrade — I've accidentally bought my fair share of duplicates from bargain bins! 🤦‍♂️). And crucially the process of adding new titles would be as simple as it gets: I could do it on my phone, without any extra export/upload steps. As a bonus, it'd be cool to have a barcode scanner to help with the workflow!

So with all that in mind, back in May 2022 I got to work, on my spare time. A few weeks later I ended up with a usable web application I've been occasionally improving. The app has been in use almost daily, as I've been watching a lot of movies and keep track of the watch dates*. I still want to improve the visuals a bit, but the app is feature-complete for my purposes already. The actual process of getting the collection up-to-date was of course a huge amount of work (importing the old collection data, combing over my shelves for movies that I had sold/upgraded, adding data the previous software didn't have, combing over the shelves again for new additions since 2010, trying to find their purchase information, dates, etc.), but it was so worth it. Should have done this years ago! 😎

Some (horror) movies I've watched lately..

Some notable features:

  • Movie data form Basic data stored per movie: title, director, release year, rating, purchase date, purchase price, disc format (now including 4K UHD), region codes, number of discs, running time, IMDb-link, optional description text and last watched dates (with full watch history).
  • EAN/UPC barcode scanning when adding new movies: uses the device camera and does a web-scrape with the barcode, trying to find out basic information. This has been really helpful when adding movies! (This library does the scanning part.)
  • Responsive design: works on phones, tablets and desktops.
  • Collection statistics -page: some stats across the whole collection, including histograms for ratings and production years. Also calculates the total cost of the collection (ouch! 😱💸).
  • Automatic data export backups: weekly exports in JSON and CSV -formats, zipped and emailed to me.

* * *

Collection stats Now some technical details, for those interested. 🤓 In the spirit of the previous movie collection software written as an exercise in Python/Qt (and its even earlier predecessor in C#), this project would function as a dive in the current web development techniques: both frontend and backend. Full-stack, as they say! 😀 Initially I had the idea of building several prototypes in different tech stacks, but with limited time available ended up choosing not to do that and instead went on completing my one prototype. I chose Node.js (with TypeScript), Express + Pug running in Google App Engine as my backend and simply Bootstrap + custom JavaScript as the front. It's currently a regular, mostly server rendered web app with limited dynamic content loading on the client side, but perhaps at some point I'll switch to SPA-model (single page app). While that'd be nifty, this already works fine for my purposes. 🤔

Another important aspect was that I wanted to build the app to be as cost-effective as possible. Choosing App Engine and Firestore as the database was partly because of the low costs, and partly because of my experience with the former. Firestore can actually function serverless, but I wanted to try the backend development so that didn't matter. As a realtime (another feature I don't need) NoSQL database it's not the most obvious nor best choice for this use-case, but works and is basically free for my usage. And free is nice!

Initially I was wary of JavaScript because traditionally I've been a fan of statically typed languages (although Python is quite nice), but TypeScript was actually a pleasant surprise! Not bad at all. While still not a fan, I must admit vanilla-JS has improved a lot since I last took a look at it. Back in the days when JS was only used to open annoying pop-up windows and messages, and maybe change link images on hover events (CSS didn't exist yet).. 😅

* * *

*) The reason for keeping track of watch dates is I have a top twenty list of most watched movies in the collection statistics page. At this point the list isn't very exciting, but it will be interesting to see in, say, 10 years (probably in time for another blog post or two, considering my current posting frequency!) what movies I have ended up watching the most. And I do re-watch stuff regularly.


Behind The Scenes: Oddhop Puzzles

It's been well over a year since our "glorious" Oddhop launch that — in the grand scheme of things — basically amounted to about two and half farts in the wind.. 😛 So, to celebrate our triumph I decided to let you guys and gals on a little secret: all of the puzzles in Oddhop are procedurally generated. 😎 That's right. We did hand-pick the puzzles, but we didn't come up with them in "traditional means" (whatever those may be for your average puzzle game): instead we created a tool for the puzzle generation.

Without further ado, let's get behind the scenes. This post is not going to be overly technical, but also hopefully not too trivial either. Let me know if it turned out to be worth reading. Oh, be sure to click on the pictures to actually see what's going on.

* * *

Click for animation

Game designer Teemu came up with the basic procedure of generating these "jump over piece to eliminate" puzzles, and that was the starting point for 1st prototype I did. It's interesting to note that the algorithm works backwards: it starts from the final, solved state of the puzzle and traces backwards from there. Essentially it starts with one piece on the board (the last remaining piece from players point of view), jumps it to a chosen direction, and spawns another piece to be eaten when playing the puzzle. Then a random piece is picked, and it is again moved and a new piece gets added. And so on.

While all that sounds very simple on paper, and was simple at the beginning of the prototype, it actually turned out not to be the case! Instead I got the fun but difficult programming challenge to make a generator that could create proper levels based on various features and parameters. So for each gameplay feature there was quite a bit of head-scratching in how I could fit that into the generator, creating levels that are actually solvable, and in way that didn't break the existing gameplay features already in! No point generating puzzles that cannot be solved, right? 🙂 Essentially each gameplay mechanic needed to be implemented twice: way to reliably generate them (difficult!) and way for the player to perform them in game (much easier!). If you ever were wondering why creating the game took over 2 years, I can tell you the puzzle generator was a big part of it.

Basically each level had a random seed and set of parameters (including version) that were used as an input to the generator. I added a textbox to the UI where the input was shown as a comma separated text string (V17,1193020924,10,1,1,1,0,0,1,0,1,0,0,0,1,0,1,0,0,0,0,0,2), so we could easily copy/paste levels and share them by email, in text form. (Actual level serialization was implemented late in the development cycle, not to mention sharing binary files wouldn't have been as quick and easy as sharing pieces of text.) So for example I would play with the generator, and when it yielded a nice level I'd take the input string and shared it with Teemu ("hey check out this cool level!") and vice versa. We often challenged each other to solve the puzzle, to see who did it with fewer tries. Teemu assembled the final level packs, and I contributed some levels (meaning I just randomly found nice puzzles! 😉). As you can see in the GIF above, gameplay is fully integrated into the generator, so levels can be immediately tested while searching for that "one nice puzzle".

Like I mentioned, levels also had a version, which meant that the generator needed to backwards support several versions of levels. We kept iterating on the mechanics and tweaking the generation heuristics (more on those below) and obviously that often required lots of changes in the generator, and I tried to keep it all working, including the old level inputs we had gathered. In hindsight that was unnecessary complexity, as I don't think we used any of the old levels in the final game. 😐 Periodically I cleaned the code and took out support for very old, obsolete versions. I had a growing version specifier, and it got up to V17 meaning 17 iterations on the input format (actual generator changed much more, of course). When I finally implemented level pack serialization and we saved the levels, I could happily get rid of the remaining backwards compatibility from the code. And there was much rejoicing.

As the programmers among us can imagine, the generator code got very complex in the end and touching it for feature tweaks or anything was scary to say the least. Not to mention debugging the puzzle generation when a gameplay feature was yielding invalid (unsolvable) puzzles! 😵 I made a debug output that printed out each step (the level layout, that is) of the generation in textual format to the Unity console, so I could pinpoint the step where it went wrong and fix it. And for some puzzles I generated I couldn't solve them in reasonable time, assumed the generator was broken and examined the debug output only to find out it was actually solvable and I was just too stupid! 😄 I also gave some puzzles to my wife when I couldn't solve them, and either she solved it or I stumbled upon the solution watching her try. Or we gave up and checked the debug output.. Good times!

Some gameplay features were quite tricky compared to others: for example the sliding flowers / ice patches (they were actually oil stains initially!) and the portals/wells gave me some trouble to implement correctly. We also had to scrap some gameplay mechanics because they were deemed too difficult to implement into the generation, or were painstakingly implemented but were not fun or necessary. That was always painful! 😄 On the right you can see some pen & paper design I did for the lily pads generation; originally they were meant to always slide to the jump direction (like the flowers ended up doing). While that particular instance in the pic worked, I couldn't get the generation work reliably so we went with simpler implementation, although the sliding flowers did add some of that back.

Bonuses (the cherries) were randomly placed on cells that were visited by any creature during the generation, so that they could be actually collected. Likewise, obstacles (rocks) were placed on cells that had no visits. All the creatures start as normal blue monsters, and get "promoted" during the generation to their special roles. When searching for direction for a creature to move, the generator first checks if there are "half-baked" green two-headed blobs around, and prefers those. This is so that the green dudes actually get two jumps over, otherwise they could just as well be normal blues. Frozen creatures are internally realized as green slimes with secondary (player visible) type assigned, they spawn out "half-eaten" and when jumped over second time get frozen in place.

Some gameplay mechanics were incompatible with each other — or maybe you set some ridiculous parameters, amount of creatures etc. — so the generator tried to brute force a valid level out, but if it couldn't in 512 tries it stopped and displayed a warning that the level is likely invalid. And actually the generator always tried to generate 10 potential levels and pick the best one from the those. (The warning was displayed when it couldn't generate even one potential level.) To decide if any given level was a potential level, we had lots of heuristics in place to discard bad or boring levels. For example, if a creature that was frozen didn't get to move at all before being eaten (after being freed from the ice), the generator discarded the level and tried again. Or maybe we had revolving platforms but nobody ended up standing on them at puzzle start? Or a dude went through portal only to do nothing? That's boring, so the generator discarded these. For each potential level the generator took up to 512 tries to find a level candidate.

With the 10 potential levels, the generator picked the one with the most variance in creature starting positions, meaning the one that likely had most movement across the map. After all it'd be boring to have creatures moving in one corner for the whole level, right? I remember we were discussing that maybe we had too many heuristics in place, as the levels could get quite "uniform" in a way, and I added "less heuristics"-toggle to the editor. 😄 Can't remember if that was actually used at all, though. At least it doesn't appear the UI any more.. Maybe the heuristics were relaxed, or something.

* * *

Remember I said that all of the puzzles were procedurally generated? I kinda lied, but only a bit: for a couple of the tutorial (intro) levels we did edit the generated puzzles. Ironically the need to edit levels in the end of the development cycle meant I had to make an editor that allowed moving/changing the creatures, cherries and obstacles. The editor was used to tweak the tutorial levels and also to do some visual adjustments to the random obstacle generation for many non-tutorial levels. The actual gameplay content in Oddhop is still exclusively work done by the puzzle generator, we just picked out the best bits. 🙂 After over two years of service, the level generation code was unceremoniously stripped out of the release builds, as it wasn't needed at runtime because the level packs were serialized into binary format.

Additionally, if you're wondering why to bother with the fancy generator at all: two reasons. First, it seemed like a neat idea to program a thingy to generate the puzzles (naturally that turned out to be a huge pain point during the project due to its eventual complexity! 😛). And second: I had plans to actually make a more polished UI for it and include it in the game build for the players to use. So they could create endless levels to play, and perhaps share good puzzles with other players. Perhaps even Puzzle of the week -type of things etc. Unfortunately the game didn't catch on so there was no point in doing that, and also no reason to implement the new gameplay mechanics we had designed, but not yet bolted on to the generator.. 🙁

Anyway, I hope this has been half interesting read. Have a good day, and do comment if you want to ask on the subject or enjoyed the post! 🙂


Oddhop Holiday Gift!

Hey, long time no see! We have some news for you: to celebrate the holidays Oddhop is now FREE for a limited time on the App Store! That means now is the perfect opportunity to "hop" in and try the game! Do spread the word out if you enjoy Oddhop and maybe even consider giving us a rating / review on the App Store. Cheers! 🙂

Download on the App Store Get It On Google Play

So, happy holidays from us! We'd also like to thank our early players who had purchased Oddhop since the launch in May, and hope we can get more people on board with this special occasion. See you next year!


Today is the day: Oddhop out NOW!

See what I just did there with the post title? I avoided starting the title with the word Oddhop! Had to break that particular pattern. 😉 Anyway, this is just a quick announcement that the game is indeed out right now, so you should go and download it!

It's free on the Google Play Store (ad-supported, but don't worry — you can purchase the ads away) and $1.99/1,99€ on the App Store. BUT, if you act quick, you can get it on our celebratory launch sale for 50% off, so it's just $0.99! The launch sale ends in a week (May 20th to be exact), so now is definitely the best time to "hop" (pun intended) on board! 😄

Download on the App Store Get It On Google Play

Personally this has been — once again — quite a journey, with a small-turned-out-to-be-not-small-at-all project so I am very glad to have the game out, finally. Hopefully the launch goes without fatal issues (wish us luck!). Also happy that I managed to ship a game while hooked on Dark Souls III! 🙂 Anyway, that is it for now. Enjoy the game, maybe even give us a rating on the stores and feel free to comment or even write a review!