Unspeakable banner

Unspeakable

15 devlogs
123h 42m 5s

I created a digital version of the party game Taboo, which is designed to be played in person with at least one phone per team. By connecting multiple devices via a local area network (LAN), each team sees different information, enabling game mode…

I created a digital version of the party game Taboo, which is designed to be played in person with at least one phone per team. By connecting multiple devices via a local area network (LAN), each team sees different information, enabling game modes such as Sabotage (opponents introduce additional forbidden words during the round), Survival (the timer adapts based on performance) and Snowball (correct guesses become new forbidden words). There is also a local pass-and-play mode for when everyone only has one device.
Built with Kotlin Multiplatform and Jetpack Compose, it runs on Android and desktop from a single codebase. The app comes with around 1,400 cards in both English and German, which are stored in a local SQLite (Room) database. You can also add your own cards and categories, including bulk import via JSON.

This project uses AI

I used GitHub Copilot for code completion.
I used Perplexity for research and to help fix some errors, as well as formatting the README.
All the words used for the game (the target words and the 5 forbidden words) are generated using Perplexity.

Demo Repository

Loading README...

schwenoldnoah

Shipped this project!

Hours: 123.7
Cookies: 🍪 2580
Multiplier: 20.86 cookies/hr

I built Unspeakable, a digital version of the classic forbidden-word party game Taboo, designed for chaotic in-person play with one phone per team. The idea was simple: physical cards can’t react to what’s happening in the game, but separate connected devices can.

The biggest challenge was the multiplayer architecture. I built a host/client system over WebSockets using Ktor, where one phone acts as the game server and others connect over LAN. Getting state to stay in sync across devices took a lot of debugging. Reusing the host and client logic for the local mode was quite of a hussle. At one point I spent over an hour debugging a stuttering animation, only to realize I’d set my phone’s animation speed to 0.5x ._.

I’m most proud of the UI, it looks really clean and the little details make it feel polished My favourite is the pill-shaped dark/light mode toggle animation.

I never found the right moment to post a devlog, because I always wanted one more feature done first. That’s why they ended up so far apart.

The app ships with ~1.4k English and ~1.4k German cards, a full custom card editor, and bulk JSON import so you can drop in AI-generated decks instantly. Built with Kotlin Multiplatform + Jetpack Compose, runs on Android and Desktop.

schwenoldnoah

I designed the app logo and added a about screen. Players can now reconnect if they drop out mid-game. I also implemented various bug fixes to prepare the project for release.

Attachment
0
schwenoldnoah

I built a new custom card and category editor. You can now create your own categories with name, icon and manage all the cards inside them directly in the app. You can also view, edit and delete the pre-shipped cards/categories. There now also is bulk JSON import option, so you can add much more cards/categories at once. This makes it really easy to just let AI generate you some new cards.

Attachment
Attachment
Attachment
Attachment
Attachment
Attachment
Attachment
0
schwenoldnoah

I made local mode work. Now, you can add more players and move them between teams in local mode.

Attachment
0
schwenoldnoah

I added a full game modes system. There are now three modes: Sabotage (opponents can inject extra taboo words mid-round), Survival (timer speeds up or slows down based on how well you’re doing), and Snowball (correctly guessed words become new taboo words for the rest of the round). I also added card category filtering in the lobby, so the host can select different categories for the game. Players now have actual roles: EXPLAINER, GUESSER, and OPPONENT. Every Player now sees a different Screen based on their role. Also added a Palette Style picker in personalization, so you can switch between different Material 3 color palette styles/generations.

Attachment
Attachment
Attachment
Attachment
Attachment
Attachment
0
schwenoldnoah

I added a GameOverScreen that displays the final team rankings and match statistics, such as cards played, accuracy, and pace. I also added a ConnectionLostScreen to handle disconnects. I simplified and standardized the navigation. So Settings and Lobby Settings work the same way now. This eliminated a lot of repetitive code, making the individual settings screens much cleaner. The round count settings finally work. There are now three presets to choose from, as well as a slider to set a custom amount. I used GitHub Copilot to add comments to the project, which makes it easier to maintain.

Attachment
Attachment
Attachment
Attachment
Attachment
0
schwenoldnoah

I have simplified the process of joining the lobby. The local IP address of the host device is now encoded in a Base24 join code. The client player can enter the code or scan a QR code containing the code. I have also tweaked the design, changing the color scheme of the buttons, for example, and updating the design of the enter name screen.

Attachment
0
schwenoldnoah

I created a RoundOverviewScreen that shows at the end of each round. It displays the points earned, as well as three lists of the correct, skipped, and wrong cards. The three buttons at the bottom of the playing screen that are used to mark a card as correct, wrong, or skipped now work. I also fixed an issue with the timeslider in the game settings where it would flicker when moved too quickly.

Attachment
0
schwenoldnoah

I have added a LobbySettingsScreen where the host can change the game settings. Currently, only the round time can be changed. When the round starts the host logic now selects a player as explainer. Each round now tracks the correct, skipped and wrong cards, so that they can be shown on an overview screen that I’ll implement later. Before each round starts, the explainer must confirm, that they are is ready to start. I also fixed an issue where the WebSocket wouldn’t close, if the host navigated back to the app’s home screen.

Attachment
Attachment
0
schwenoldnoah

I improved the network logic. Host- and client-side events and handlers are now separate, which should make future development easier. I added backend functionality for player profile pictures, which are currently a hardcoded color. There is also now a lobby screen where players can join different teams before the game starts.

Attachment
0
schwenoldnoah

I added a personalization screen that allows you to change the seed color of the app’s theme, use your wallpaper’s color, turn on the AMOLED toggle to make the background pitch black, and switch between light and dark mode. I had parts of the UI from another project lying around (Flutter, not Compose, though), which I ported using GitHub Copilot. It took a while to perfect the animation of the pill-shaped Dark Mode toggle.

Attachment
0
schwenoldnoah

I created a local API that makes it much easier to add new subpages to the settings page. I then added Lyricist to the project to support different languages (currently German and English). All the hardcoded strings have been replaced with dynamic ones. I have also added an AppSettingsController that saves the app settings as persistent key-value data.

Attachment
0
schwenoldnoah

I enable Android predictive back gestures, which were a bit tricky to get working. They are still marked as experimental API in Decompose. I also added a navigation bar at the bottom of the page. This will allow you to access the settings and a page where you can edit the word cards later on.

0
schwenoldnoah

I implemented Decompose into the app, to make navigation work. The app now has two new pages: The home page, where the user can select whether he wants to play local, join or host a game. There is also a game settings page. This page and its design are not finished yet. Currently, it only contains a field for the player name and host IP address (if connecting to a host). I plan to add more features, such as a round time setting and a QR code for quick connection.
I have also updated the network logic to support players and enable direct messaging to specific players.

Attachment
0
schwenoldnoah

I have refined the Networking and separation of truth logic. I have implemented the timer with synchronization between the devices. Then I animate the circular-wav-progress-indicator according to the time passed. The problem was that the animation was stuttering: half a second animation, half a second pause. After over an hour of debugging I realized that I had changed the animation speed of my whole phone to 0.5x :^)

0
schwenoldnoah

First, i started working on gathering the terms for the game. I ended up using Perplexity, because this was the most easy and qualitative way. I initially started the project with flutter, but realized that Kotlin Multiplatform in combination with Jetpack Compose would be the better choice, because i wanted to use the ‘new’ Material 3 Expressive design in this app. Then I setup the database and seeded it with the around 1.8k English and 1.7k German terms. After that, I created the foundation for the planed local multiplayer using WebSocket. Then I spent a lot of time refining the UI, which I am very happy with now.
Now I’ll need to update the game-state to make the timer and points work.

Attachment
0