TabNews is a native iOS client for the Brazilian tech community platform TabNews. I built it a while ago as a focused reader for that community: feeds, newsletters, folders, notes, push notifications when something new drops.
If you want the full picture of the app itself, I wrote about that in Cronjobs + Cloud Functions to Send TabNews Newsletters Automatically. That post covers the TabNews API client, Firebase Cloud Functions, cronjobs, and how notifications deep-link into posts.
This one is about a different slice of the same app. At some point I started hiding games inside it.
Context: What is TabNews?
If you are not familiar with the platform, TabNews is a Hacker News-style site where Brazilian developers share articles, news, and discussions. The iOS app wraps the public API with a reading experience tuned for long posts on a phone.
The games live in a section called Jogos Dev (Dev Games). They are optional, a bit silly, and very on-brand for a dev community app. A side project inside the project that started as a Flappy Bird easter egg and grew from there.
It started as an easter egg
The first version was not a game hub at all. It was a secret.
I added an AppUsageTracker that counted cumulative seconds while the app was in the foreground. After enough time, new UI would quietly appear:
- After 10 minutes: a small game button showed up somewhere in the app.
- After 30 minutes: a new folder called Descanse appeared in the user's library. Tap it, and you got a Flappy Bird clone built with SpriteKit.
That was the whole feature. No announcement, no settings toggle. If you spent half an hour reading TabNews posts, you earned a break.
The thresholds changed over time (I eventually pushed them to 1 hour and 2 hours because the folder was showing up too often during testing), but the idea stayed the same: reward people who actually use the app with something fun and unexpected.
Flappy Bird is gone now. I replaced the easter egg with an explicit Rest Games hub, Game Center leaderboards, and a proper announcement sheet. But that original "Descanse" folder is why any of this exists. The easter egg made me ask: what else could live here?
From one game to a whole suite
Once Flappy Bird proved the concept, I built DevWordle: Wordle, but every answer is a programming term (ASYNC, CACHE, NGINX, that kind of thing).
Then I thought: why stop at one?
I have spent a lot of time on dialed.gg, a site with quick perception games. Two of my favorites are Color Match and Sound Match: you memorize a color or a tone for a few seconds, then try to recreate it. I ported that idea into the app as arcade-style rest games.
While studying for a Google technical interview, I kept running into the same two skills: reading code and estimating complexity. That became Big O (guess the time complexity of a snippet) and AlgoSpot (guess which algorithm a snippet implements). I also added DevSpot (pick the real dev term between two lookalikes) and DevLeet (a weekly LeetCode-style challenge on paper, honor system).
Big O and AlgoSpot share a pool of pseudocode snippets I wrote while prepping. Same code sample, different question: one asks for complexity, the other for the algorithm name. DevLeet pulls from a different source entirely: a public LeetCode dataset I found on GitHub (more on that below).
Today the hub has seven games:
- DevWordle (daily): guess a 5-letter dev term in 6 tries
- Big O (daily): pick the time complexity of a code snippet
- AlgoSpot (daily): pick the algorithm behind a code snippet
- DevLeet (weekly): read a LeetCode problem and solve it offline
- DevSpot (free play): spot the real dev term vs a decoy
- Color Match (free play): memorize and recreate a color
- Sound Match (free play): memorize and recreate a tone
Not every game works the same way. DevWordle, Big O, and AlgoSpot each give you one puzzle per day. Same puzzle for everyone, resets at midnight, streak if you keep coming back. DevLeet runs on a weekly cadence instead: one LeetCode-style problem from Monday to Sunday. The other three are always open, arcade-style, no schedule.
The daily games also have a free mode: extra rounds for practice after you finish today's puzzle. You have to opt in from a toggle in the hub settings, and the daily challenge has to be done first (win or lose both count).
Shared architecture
Before diving into each game, here is what they share.
All rest games live under Core/Views/RestGames/ in the TabNews repo. There is a common visual layer (RestGameBackground, RestGameTheme, shared buttons and onboarding overlays), a haptic/audio feedback manager, and a free mode policy that gates unlimited practice behind completing the daily puzzle first.
Game content is mostly JSON bundled with the app, not fetched from a server. That keeps everything offline-friendly and makes the puzzles reviewable in git. I wrote small Node and Python audit scripts that run in CI-style checks before I ship:
audit-dev-words.mjsfor DevWordle word listsaudit-rest-games-pools.mjsfor Big O and AlgoSpot challenge poolsaudit-big-o.mjsandaudit-algo-spot.mjsfor individual challenge validation
Six of the seven games submit scores to Game Center leaderboards: DevWordle, DevSpot, Big O, AlgoSpot, Color Match, and Sound Match. DevLeet is the exception. It has achievements (weekly solves, streaks, hard problems) but no leaderboard, since there is no score to compare. Daily puzzle games also track streaks locally in UserDefaults.
The hub itself (RestGamesHubView) is a simple NavigationStack with tiles grouped by cadence: Hoje (today), Esta semana (this week), and Mais (everything else).
DevWordle
Rules
Standard Wordle rules: 5 letters, 6 attempts. Green means correct letter in the correct position. Yellow means the letter exists in the word but is in the wrong spot. Gray means absent.
The twist is vocabulary. Target words are programming terms only.
How guesses are evaluated
The evaluation logic is a two-pass algorithm, the same approach Wordle uses:
- First pass: mark letters that match position as
correct, and remove them from a remaining pool of target letters. - Second pass: for everything else, if the letter still exists in the remaining pool, mark it as
present(yellow) and consume one instance from the pool.
This matters when a guess has duplicate letters. Without the two-pass approach, you can mark too many yellows.
Three word lists, not one
The dictionary (dev_words.json) is split on purpose:
answers(226 words): daily target words. Programming terms only. Blocklist applied.practiceAnswers(164 words): free mode targets. Also programming only, and disjoint fromanswersso you never get today's daily word again in practice.extraGuesses(742 words): valid guesses that are never used as targets. Includes regular English words so you can useHOUSEorWORLDas a strategic guess even though the answer will always be dev-themed.
There is also a blocklist (dev_word_blocklist.json, 242 entries) to keep awkward or inappropriate words out of the answer pools.
Every word must be exactly 5 uppercase letters. The audit script enforces minimum pool sizes (150+ per list), no duplicates, and no overlap between daily and practice answers.
How the daily word is picked
No server, no randomness per user. The daily word is deterministic:
let days = calendar.dateComponents([.day], from: epochStart, to: today).day ?? 0
let index = abs(days) % answers.count
return answers[index]Everyone gets the same word on the same calendar day. When you finish (win or lose), state is saved to UserDefaults keyed by date so reopening the app restores your board.
Free mode
Free mode runs a 5-word session. Each word is drawn from practiceAnswers, excluding words you already played and excluding today's daily answer. When the session ends, you get a summary sheet with your results.
Color Match and Sound Match
These two are the closest descendants of the original Flappy Bird easter egg: pure arcade, no daily puzzle, just skill.
Both follow the same loop (RestGameSession):
- Memorize phase (3 seconds): see (or hear) the target.
- Recreate phase: adjust a picker to match.
- Score reveal: see how close you were.
- Repeat for 5 rounds, then show a final score and optionally submit to Game Center.
Color Match scoring
The target is a random HSL color with constrained saturation and lightness (so it is not impossible to approximate). Your guess comes from an HSL picker.
Scoring uses CIE Delta E 2000, a perceptual color distance metric. RGB values are converted to LAB space, then compared with the full Delta E 2000 formula (the same family of math used in print and design tooling). The score maps distance to a 0 to 10 scale:
let score = 10 - (deltaE / 4.5)Closer colors score higher. It feels fair in a way that simple RGB distance never quite did.
Sound Match scoring
The target frequency is picked on a logarithmic scale between 200 Hz and 800 Hz (human hearing is logarithmic, so this feels more even across the range).
During memorization, ToneGenerator plays a sustained sine wave through AVAudioEngine. During recreation, you adjust a frequency picker and hear your guess in real time.
Scoring compares frequencies in log space:
let logDiff = abs(log2(target) - log2(guess))
let score = 10 - (logDiff * 4.2)The tone engine smooths frequency and volume changes sample-by-sample so scrubbing the picker does not click or pop.
DevSpot
DevSpot is the simplest game in the hub. Each round shows two terms side by side. One is a real programming concept (Circuit Breaker, Type Erasure, CQRS). The other is a plausible-sounding decoy (Circuit Board, Type Inference, CORS).
You get 10 rounds per session. Pairs come from dev_spot_words.json. The dev term is randomly placed on the left or right each round so you cannot cheat by always tapping the same side.
Scoring is binary: right or wrong. At the end you see a percentage and can check Game Center rankings.
Big O
Big O shows a short pseudocode snippet and four complexity options (O(1), O(n), O(n log n), O(n²), etc.). You pick one.
Each challenge in the JSON includes:
- The snippet (pseudocode, language-agnostic)
- Shuffled options
- The correct answer
- A difficulty tag
- A case note (worst case, average case, amortized)
- An explanation in plain language
- A reference link (usually GeeksforGeeks or similar)
Content pools
There are two pools:
big_o_daily.json: 35 challenges for the daily rotationbig_o_free.json: 65 challenges for free mode (10-round sessions)
The daily challenge index uses the same epoch-day modulo pattern as DevWordle. Free mode randomly draws from the free pool without repeating IDs until the pool resets.
I seeded the initial content with a Node script (seed-big-o-content.mjs) and split snippets into shared and Big-O-exclusive sets so Big O and AlgoSpot can reuse some code samples without sharing the same questions. These are not pulled from the LeetCode dataset. I wrote the pseudocode by hand while studying for interviews, then validated each answer against references like GeeksforGeeks.
After you answer
When you submit, the UI enters a reveal phase (~2.5 seconds) showing whether you were right, then an explanation sheet with the case note and a link to read more. Daily mode tracks win streaks. Getting it wrong resets the streak to zero.
AlgoSpot
AlgoSpot is the sibling of Big O. Same daily/free structure, same JSON shape, same scoring and streak logic. The question is different: instead of complexity, you identify the algorithm (Busca binária, Merge sort, DFS, etc.).
One nice detail: snippets often include a function name(...) header for context, but the UI strips that line before display so you focus on the logic, not the function name giveaway.
The content pipeline mirrors Big O:
- 35 daily challenges in
algo_spot_daily.json - 65 free challenges in
algo_spot_free.json - Seeded via
seed-algo-spot-content.mjs - Validated by
audit-algo-spot.mjsand the shared pool audit
DevLeet
DevLeet is the most unusual game in the set. It is intentionally offline and honor-based.
Each week (ISO week, Monday-based) the app picks one LeetCode problem from a bundled catalog. You read the description, examples, and constraints on screen. Then you are supposed to solve it on paper, like a real interview. When you are done, you tap "Mark as solved" and confirm on an honor sheet. No code editor, no test cases, no auto-judge.
Where the problems come from
I did not want to hand-write thousands of LeetCode problems. While looking for a shortcut, I found a community GitHub repo that mirrors LeetCode's catalog as JSON. You clone it (the folder is usually called something like leetcode-problems-master) and get:
- A
problems/directory with one JSON file per problem (0001-two-sum.json, etc.) - A
merged_problems.jsonfile with everything in a single list (~2,900 problems, ~20 MB)
Each entry has the title, slug, difficulty, description, examples, constraints, topics, starter code snippets, and more. Way more than the app needs.
So I wrote generate_dev_leet_catalog.py to strip it down. The script reads merged_problems.json (or a JSONL training dump as a fallback), parses descriptions and examples, pulls reference solutions in Python/Java/JavaScript/C++ when available, and writes a slim dev_leet_problems.json for the bundle.
What gets dropped: HTML editorials, hints, images, starter templates. What stays: enough to present the problem and show a solution after you solve it on paper.
The result ships in the app at about 8 MB with 2,360 problems (entries without a usable description are skipped). Manual fixes for edge cases live in dev_leet_description_overrides.json. At one problem per week, that is decades of content without repeating.
To regenerate after updating the source dataset:
python3 newtabnews/Scripts/generate_dev_leet_catalog.pyHow the weekly problem is picked
let key = weekKey(for: date) // e.g. "2026-W26"
let index = abs(stableHash(key)) % problems.count
return problems[index]The hash is a simple deterministic string hash (multiply by 31, add character values). Same week, same problem for everyone.
Solutions
Every problem in the catalog includes at least one reference solution. After you mark a problem solved, the solution sheet unlocks prominently. Before that, there is a subtle link if you want to peek (I trust you, but the UI nudges you toward trying first).
Solutions link back to the official LeetCode URL for the problem (https://leetcode.com/problems/{slug}/).
Streak tracking works like the daily games, but on a weekly cadence: solve this week's problem, and if you also solved last week's, your streak increments.
Free mode policy
Daily games (DevWordle, Big O, AlgoSpot) have a free mode, but it is gated:
- You must finish today's daily challenge first (win or lose counts).
- You must enable free mode with a toggle in the Rest Games hub.
This keeps the daily puzzle as the main event and stops free mode from cannibalizing it. Arcade games (Color Match, Sound Match, DevSpot) are always free play.
What I learned
Easter eggs can become products. The Flappy Bird folder was a joke. DevWordle was an experiment. The easter egg sat mostly unchanged for months. Then, over a single week in June 2026, it turned into a game hub with JSON content pipelines, audit scripts, Game Center integration, and weekly LeetCode challenges. None of that was planned upfront.
Bundle content, validate in scripts. Keeping puzzles as JSON in the repo means I can review diffs, run audits, and ship offline. The games work on a plane. No API keys, no CMS.
Daily cadence needs deterministic selection. Modulo on epoch days (or ISO weeks) is boring, but it is fair, predictable, and easy to debug. Everyone gets the same puzzle. Streaks mean something.
Honor systems are fine for some games. DevLeet will never replace actually grinding LeetCode with an IDE. It is a reading and recall exercise. The honor sheet is a feature, not a limitation.
Perceptual scoring matters for arcade games. Delta E for colors and log-frequency distance for tones made Color Match and Sound Match feel like dialed.gg instead of a cheap clone.
Try it
Rest Games ship in the TabNews iOS app. You can find it on the App Store or browse the source on GitHub.
The app is in Brazilian Portuguese today, but the games use mostly universal dev vocabulary (ASYNC, O(n), algorithm names), so they are playable even if you do not speak Portuguese.
Related reading: Cronjobs + Cloud Functions to Send TabNews Newsletters Automatically (the main TabNews app: API, push notifications, Firebase).
Technical resources:
- TabNews app: apps.apple.com/us/app/tabnews-reader/id6755933359
- GitHub: github.com/luizmellodev/tabnews.app
- Rest game content:
newtabnews/newtabnews/Core/Views/RestGames/ - Audit scripts:
newtabnews/Scripts/audit-dev-words.mjs,audit-rest-games-pools.mjs