DEV Community

Cover image for I Built a Multiplayer Card Game to Test My Own Game Backend SaaS
Nenad Nikolić
Nenad Nikolić

Posted on • Originally published at turnkit.dev

I Built a Multiplayer Card Game to Test My Own Game Backend SaaS

I recently released Meksiko, an Android version of a traditional multiplayer card game.

The game itself was not the only goal.

I built it as a real production client for TurnKit, my backend SaaS for multiplayer and connected games. Instead of testing the platform only through isolated demos, I wanted to build a complete game and discover which parts of the SDK worked well, which parts were awkward, and which features were still missing.

Google Play:
https://play.google.com/store/apps/details?id=com.turnkit.meksiko

Why build a complete game?

Small technical demos are useful, but they usually follow the ideal path.

A real game introduces problems that are easy to miss:

  • players disconnect
  • server events arrive while animations are running
  • turns time out
  • lists change remotely
  • UI state becomes stale
  • users close and reopen the application
  • authentication behaves differently in production
  • game rules require features the original API did not anticipate

Meksiko became both a game and an integration test for the platform.

The game architecture

The server is authoritative over the multiplayer session.

It controls important state such as:

  • connected players
  • player order
  • the current turn
  • game phase
  • submitted actions
  • votes and decisions
  • timeout handling
  • reconnection state

The Unity client receives updates and presents them through the UI, animations and local game objects.

In theory, this sounds straightforward:

  1. Receive server state.
  2. Update the local model.
  3. Refresh the UI.
  4. Play the appropriate animation.

In practice, coordinating these steps was one of the more difficult parts of the project.

AI was useful, but struggled with event-driven state

I used AI-assisted development during parts of the project.

It worked well for relatively isolated tasks:

  • writing asynchronous functions
  • generating request and response models
  • creating utility methods
  • reducing repetitive integration code
  • implementing clearly defined transformations
  • suggesting error-handling paths

It was much less reliable when reasoning about state that changed through server events.

A common incorrect assumption was that receiving a response or updating one object would instantly update every related collection and UI element.

For example, when the server changed the player list, the client still needed to:

  1. receive the relevant event
  2. deserialize the new state
  3. replace or mutate the local collection
  4. notify the presentation layer
  5. rebuild or update the UI
  6. handle removed and newly added players
  7. avoid updating objects that were currently being animated

AI-generated code often skipped one or more of these steps.

It understood an asynchronous function such as:

PlayerList players = await client.GetPlayersAsync();
Enter fullscreen mode Exit fullscreen mode

more reliably than a distributed event flow where several systems needed to react to the same update.

The difficult part was not writing async code. The difficult part was defining ownership of the state and deciding exactly which system was responsible for applying each update.

Server events should not directly control the UI

One important lesson was to avoid letting network callbacks manipulate the UI directly.

A tempting implementation looks like this:

client.PlayerJoined += player =>
{
    playerListView.AddPlayer(player);
};
Enter fullscreen mode Exit fullscreen mode

This works initially, but becomes fragile once the game has animations, reconnects, scene changes and multiple event types.

A better flow is:

Server event
    ↓
Network adapter
    ↓
Local game state
    ↓
State-change notification
    ↓
Presenter
    ↓
View
Enter fullscreen mode Exit fullscreen mode

The event updates the local model first. The UI then renders the resulting state.

This creates a single source of truth and makes it easier to handle reconnects or rebuild the entire interface from a server snapshot.

Async operations were easier to reason about

Request-response operations were generally much easier to implement and test.

For example:

MoveResult result = await gameClient.SubmitMoveAsync(move, cancellationToken);

if (!result.Success)
{
    view.ShowError(result.ErrorMessage);
    return;
}

ApplyServerState(result.GameState);
Enter fullscreen mode Exit fullscreen mode

This code has a clear beginning and end.

The caller knows:

  • when the operation started
  • when it completed
  • whether it failed
  • which state was returned
  • when the UI may continue

Events are different. They can arrive at any point, may be duplicated, and may refer to state that has already changed.

That does not mean events should be avoided. Multiplayer games need them. But event-driven systems require stricter rules around ordering, ownership and idempotency.

Features I discovered were missing

Building Meksiko exposed several features that were either missing or not flexible enough.

Passing turns

The original turn system assumed that a player would perform an action before the next player became active.

Meksiko needed a proper pass action.

Passing is not merely an empty move. It can affect:

  • whether the player may act again
  • when a bidding phase ends
  • which player wins the current decision
  • what happens when every player passes
  • whether the server should immediately advance the turn

This required more explicit support in the turn API.

Better Google Play integration

Authentication worked, but the complete Google Play experience required more than signing in.

A production Android game also needs to consider:

  • silent sign-in
  • account linking
  • guest-account upgrades
  • restoring an existing account
  • profile names and avatars
  • Play Games Services behavior after reinstalling
  • login cancellation and failure states
  • keeping the game usable without Google sign-in

This showed me that authentication should be treated as a complete user flow, not simply an SDK method.

Reconnection and state recovery

A reconnecting client should not try to replay every event it missed.

It should request or receive an authoritative snapshot and rebuild the local state.

The important distinction is:

Events describe changes.
Snapshots describe truth.
Enter fullscreen mode Exit fullscreen mode

Events are useful while the client is connected. Snapshots are safer after reconnecting or when the client suspects its local state is inconsistent.

What worked well

The project also validated several parts of the platform.

The asynchronous API was generally straightforward to consume from Unity.

The backend could manage:

  • multiplayer sessions
  • turn order
  • server-side game state
  • player replacement with bots
  • timeouts
  • reconnection
  • remote actions

Using a real game also helped simplify parts of the SDK that had previously been designed too generically.

A feature may appear flexible when designed in isolation but still feel awkward when used repeatedly inside gameplay code.

The biggest lesson

The most important lesson was that a multiplayer SDK should not only expose network operations.

It should help developers maintain a consistent local representation of server state.

The difficult questions are usually not:

  • How do I send this request?
  • How do I deserialize this response?

They are:

  • Who owns this state?
  • Can this event arrive twice?
  • Can it arrive out of order?
  • What happens if the scene changes before it completes?
  • Should the client apply a delta or replace the state?
  • Can the same operation be safely repeated?
  • What happens when an animation is still using the previous state?

Those questions determine whether the client remains stable.

I may open-source the game

I am considering open-sourcing the Unity client later.

The project could serve as:

  • a complete multiplayer Unity example
  • a reference integration for TurnKit
  • an example of server-authoritative turn-based architecture
  • a practical demonstration of reconnects, events and async operations
  • a starting point for developers building card or board games

Before releasing it, I would likely separate game-specific code from reusable SDK integration and remove production configuration and credentials.

Final thoughts

Building a complete game was much more valuable than creating another isolated SDK demo.

It exposed missing features, unclear abstractions and assumptions that only became visible under real gameplay conditions.

AI-assisted development helped with local and well-defined asynchronous tasks. It was less effective at reasoning about distributed event-driven state, especially when server updates needed to propagate through several client systems.

The result is not only a released Android game. It is also a much better understanding of what the backend platform needs to provide.

I plan to continue improving both the game and the SDK, and I am interested in eventually publishing the client as an open-source reference project.

Top comments (0)