← JournalEssay no. 058min read

Why a puzzle game belongs on a software studio's homepage

An essay.

Arrow Shift is on /games for three reasons: a traffic loop the rest of the site can't reach, a quiet rebuttal to 'all corporate, no fun', and a forcing function for the parts of the stack a marketing page never exercises. Plus the daily-puzzle math.

Published

Thursday · UTC

Reading time

8 min

~210 wpm

Word count

1,621

plain English

Format

.mdx

utf-8 · git-tracked

A few weeks ago the homepage learned a new word. Between the developer API and the affiliate radar, there is now a panel that says play today's puzzle and points at a tiny browser game called Arrow Shift. The puzzle is free, it takes about three minutes, and the daily board at /games/arrow-shift/daily is the same one everyone else playing today is staring at.

This post is the working version of why we built it — the strategic read, the design choices behind the mechanic, and the math that makes the daily seed defensible. None of the reasoning is "we love puzzles" (we do, but that doesn't justify a roadmap slot). All of it is studio hygiene.

The strategic read

There were three reasons the puzzle earned its slot.

Traffic the rest of the site can't reach. Elofyn's organic surface today is two essays a week and the AI Tool Radar's weekly digest. The ceiling on that kind of content is set by intent — somebody has to already be looking for "how to read a model card" or "what changed in Anthropic pricing this week" before they find us. That's a fine audience and it converts. It is also a small audience. A daily browser puzzle is searched by people who weren't looking for us at all. "Free browser puzzle game" and "daily puzzle online" each clear six figures a month on Google. We don't need to win those queries — we need to land a single page that doesn't embarrass the brand on the long tail.

A share loop the essays don't have. When a reader finishes one of our posts, the natural action is to close the tab. There is no share-of-self moment built into a 1,500-word essay; you might recommend it, but you won't perform having read it on social media. Wordle understood this in 2021 and the rest of the puzzle web copied the lesson. Arrow Shift's share card is the Wordle move applied to a chain-arrow puzzle:

Arrow Shift · ↑↑→↓→ in 7 moves on 2026-06-04 · elofyn.com/games/arrow-shift

A finished player who pastes that into a group chat is not just recommending the site — they are displaying their score, which is a much more durable social action. The URL goes along for the ride. We will lose ten readers for every shared card, easily, and the cost of sending those readers is exactly the same as the cost of sending zero: we built the share string once and the player does the rest.

A counter to "all corporate, no fun." The studio's posture is "we ship software every week" and that posture, repeated long enough without a wink, starts to read as airless. Visitors who land on the homepage and see five panels labeled API, radar, snippets, journal, status sometimes think do these people leave the room? A puzzle in the lineup is a small and durable answer: the studio that ships the billing webhook on Friday is the same studio that hand-designed a 3×3 tutorial level for a chain-arrow puzzle on Wednesday. That's a better story than the airless one.

The order matters, by the way. Traffic and shares are the dollars. The "not airless" benefit is the reason a puzzle won the slot instead of, say, another developer endpoint. We had room for both kinds of release and the puzzle was the one that bought something the rest of the catalog couldn't.

Designing Arrow Shift

The mechanic is a chain reaction on a grid of arrows. Each tile holds a direction (↑, ↓, ←, →). Tap a tile to fire — the chain walks in that direction to the next tile, then follows that tile's arrow, and so on, until it hits an edge or a previously-visited cell. Score is the chain length times a color multiplier. Win the board by clearing every tile under a move budget.

The design choices behind that one paragraph were the whole project.

Why arrows, not gems. The puzzle genre is owned by tile-matching games — Bejeweled, Candy Crush, 2048 — and any new entry that uses gems gets read as "yet another match-three" inside two seconds. An arrow tile communicates a rule the moment you look at it. You know the chain is going to walk in the direction the arrow points before anyone explains the mechanic. We wanted the smallest possible "wait, why?" gap between a screenshot and a play.

Why color is a multiplier, not a constraint. The first sketch required matching colors for a chain to continue, which made the board feel like a maze with a key — you'd stare for thirty seconds before tapping. The shipped version lets any chain walk through any tile, but rewards chains that pass through same-colored tiles. The difference is that every tile is now also a valid first move; you can't "see the wrong move." Speed up, lose less.

Why exactly 30 levels. Hand-designed levels are expensive. Thirty is the smallest pack that supports the difficulty curve we wanted (tutorial → medium → hard → expert) and the biggest pack one person can ship without grinding. It is also the right pool size for the daily seed — more on that below.

Why no signup, no leaderboard. Every account on the internet is a friction tax. For a daily puzzle in particular, a leaderboard rewards the first player of the day and punishes everyone in the wrong time zone. Local-storage progress and a personal streak counter is enough. If we ever build a leaderboard, it will be opt-in and timezone-aware. Probably we won't.

The daily puzzle math

The daily puzzle is the part of Arrow Shift the rest of the surface depends on, so it needs to be defensible. Three properties matter: everyone playing today must see the same board, the board must be guaranteed solvable, and the rotation must not visibly repeat.

The seed is the UTC calendar date — 2026-06-04, a literal string. That's the only entropy. We hash it with FNV-1a (32-bit, dependency- free, fast) and take the remainder against the size of the daily pool. The tutorial tier is excluded from the rotation — a 3×3 mono- orange board reads as broken to a returning visitor — leaving 25 levels in the pool:

export function getDailyBoard(seed = dailySeed()): DailyBoard {
  const idx = hashSeed(seed) % DAILY_POOL.length;  // pool size: 25
  const lv = DAILY_POOL[idx];
  return { seed, levelId: lv.id, grid: lv.grid, /* ... */ };
}

Solvability is free. Every level in levels.json was hand-designed and has a known parMoves value the designer verified by playing it. We are sampling solvable boards, not synthesizing them — so the guarantee carries through the seed function automatically. The next 30 levels we ship will join the pool the same way.

The non-repetition property is the interesting one. With a 25-level pool and a uniformly distributed hash, the expected gap between two collisions on the same level is 25 days. The expected gap between any visible repeat (the player notices "wait, I played this last week") is the birthday-problem square root of that: about 6 days. Which is too short. Real players notice. The fix is not to make the pool bigger right away — that would require shipping 30 more hand-designed levels we don't have yet. The fix is a tiny tweak we'll land in the next release: a sliding 14-day exclusion window that rejects any seed whose hashed index lands on a level shown in the last fortnight. With a 25-level pool minus a 14-day window, the effective pool drops to 11 and the expected gap between visible repeats stretches to about 4 weeks. That is the right rhythm for a daily puzzle audience.

We could ship the window today. We won't, because the puzzle has been live for less than a week and we need a fortnight of plays to even trigger the window. Shipping the fix before the bug shows up is a category of engineering we try not to do here. The math is documented, the patch is sketched, the trigger date is on the calendar. That's the right amount of preparation.

What we learned

A few notes from building this that don't fit anywhere else and would be lost if we didn't write them down:

  • Phaser was the right pick for the canvas. The vanilla-Canvas spike worked but reinvented the input-event glue. Adding phaser for ~600 KB gzipped removed two days of work and a class of input- on-mobile bugs we would have shipped.
  • Hand-designed beats procedural at this scale. The 30 hand-tuned levels punch harder than the same 30 generated ones, because the difficulty curve is the actual product. A procedural pack would have demanded a difficulty estimator, which is harder than just playing the levels.
  • Audio defaults to muted, always. A puzzle that autoplays sound on load is a puzzle people close before reading the rules. The mute toggle is in the top right and the default is off. The conversion difference between "audio on" and "audio off" defaults is the kind of detail that decides whether a casual visitor plays the second round.

The full code lives under src/components/games/ and the rules helpers (chain resolution, scoring, daily seed) are pure functions under src/lib/games/arrow-shift/ with their own tests. If you want to see the share string land in your group chat, the daily board is at /games/arrow-shift/daily. Today's seed is whatever today's UTC date string is, which means the same as everyone else's.

The next release lands on Friday. It is unlikely to be another puzzle. It is very likely to reference this one — the puzzle is now part of the catalog the rest of the studio is judged against. That is the same posture as every other thing we ship, and it is the honest reason we built it.

// Subscribe

More like this, every Friday.

Get the next essay in your inbox the moment it ships — plus the change log behind it.

One email a week. No tracking pixels, no upsells. Unsubscribe with one click.