Inspiration

My friends and family are into card games. It's one of the easiest ways to pass the time. I would always play Spit or Rummy with my grandma while on vacation or by the pool. But there are spontaneous moments when you're with a group where someone says "I wish we had some cards", usually followed by "and where would we even play?". I experienced this most recently while on a cruise sitting in a lounge with my family.

What it does

Deckhand gives you a deck of cards in your pocket, ready to play some of your favorite games no matter where you are. One phone creates a table, everyone else joins with a code, and each player's hand stays private on their own screen. At launch the app ships with War, Go Fish, Crazy Eights, Bull, and Ride the Bus with popular rule variants baked in.

How we built it

Keeping several phones in sync throws you straight into a client/server relationship, so the first thing I wrote was the protocol they talk over. Deckhand is a TypeScript monorepo with a strict dependency direction: engine <- protocol <- server / app.

The engine has zero dependencies. Each game is a GameDefinition. setup(players, rng) builds the state, reduce(state, action) returns the next state plus the events it emitted, and viewFor(state, playerId) returns the redacted snapshot one player is allowed to see. Randomness comes only from a seeded Rng, so a table replays exactly from its seed and action log. That is what lets property tests run full random playthroughs and assert the invariants: cards conserved, no crashes, games that terminate.

The server is Node and Socket.IO, holding rooms in memory and owning the engine outright. A phone sends an intent over zod-validated messages, the server reduces it, then fans out public events and each player's own view. Redaction is the security boundary. Events reach everyone, so anything hidden has to be hidden by viewFor, not by the UI. The app is Expo and React Native with zustand for state and Reanimated for the table motion. Phones render the view and send intents, and never run the rules.

Challenges we ran into

Accessibility fought the card table. A game screen has to show everything at once, so honoring the OS font scale meant text overflowing the felt. I stopped treating type size as a setting and made it a solve: a fit factor sizes every label to the space the layout reserved, tap targets never shrink, and screens outside a game still follow the OS scale. Colors are generated by a contrast solver rather than picked by eye, and motion respects reduce-motion by cutting the travel while keeping the signal. The tradeoff is that a player who needs very large system text does not get it on a game screen.

Accomplishments that we're proud of

The layout system. Every game screen is a declarative tree rather than hand-tuned styles, and it is tested headlessly against the same Yoga engine React Native ships. A single vitest run solves each game, in each phase, across every viewport and safe-area inset I support, and fails if anything overflows or if the text has to shrink past its floor to fit. That means bugs that used to need a phone in hand (a banner clipping on a small screen, a hand pushed under the ledge) now fail in CI in seconds. It is the piece that lets five games all look right on every device.

What we learned

This is the first native app I've ever written, so the entire project has been a learning experience. The Expo ecosystem is great. Finding out about OTA updates instead of having to submit new binaries to the stores for review is a lifesaver. I learned about the immense number of forms and requirements to even create a listing in the Play and App Stores. I am a Windows/Linux and Android user. Trying to get an environment set up to test and even build for iOS was a multi-day process. Thankfully my sister had an old iPhone lying around. The purchase itself took an afternoon to wire up with RevenueCat. Getting the product approved in two consoles, with the right screenshot dimensions and privacy forms, took days.

What's next for Deckhand

More games. I mentioned Spit and Rummy at the top and neither are in the app. Rummy requires some new table components and Spit requires designing a real-time game engine instead of a turn-based one. I'd like to get Texas Hold'em included too, since if you don't have cards, you probably don't have chips either.

Share this project:

Updates

Submission history