Lungo

Real code from Tic-Tac-Toe

SDK in the client. Lua in the room.

This page uses real snippets from the Tic-Tac-Toe demo in this repo. Read it row by row: what the client does, what the room does in response, and how the multiplayer loop stays understandable.

What this proves

The multiplayer surface stays tiny on the client. The game rules stay small and readable on the server.

Real project snippets

Everything below is adapted from the actual Tic-Tac-Toe client and room logic, not from a made-up toy example.

Why it matters

You do not spin up your own WebSocket stack just to start a two-player game and keep it authoritative.

Client to server, step by step

Read it like a dialogue between your game and the room

Each row shows one player-facing action on the left and the authoritative room behavior it triggers on the right.

Client SDK

Load the SDK and create one client

This part is pure setup: point the game at Lungo once, then use the same SDK instance for room and gameplay calls.
<script src="https://lungo.minmax1996.me/sdk/game-sdk.js?v=20260329-1"></script>

const sdkConfig = {
    masterUrl: window.config.masterUrl,
    authUrl: window.config.authUrl || "",
};

window.tictactoeSdk = new window.GameSDK.GameSDK(sdkConfig);
1

Create or join a room. Lungo gives you a connect string, and the room creates persistent state that can be synced back after reconnect or rejoin.

Client SDK

Join or create a room in a few lines

The room handshake stays compact: create, get a connect string, then connect with the token.
async joinOrCreate(isOpen) {
    const connectString = await window.tictactoeSdk.joinOrCreate(isOpen, {
        name: "tictactoe",
        maxPlayers: 2,
        logicParams: { game_mode: "classic" },
    });

    await window.tictactoeSdk.connect(
        connectString,
        window.tictactoeSdk.getToken()
    );
}
Server Lua

Initialize room state once

`getState` and `setState` persist room state so sync can rebuild the board on reconnect or rejoin.
local function getGameState()
    local state = getState("game")
    if state == nil then
        state = {
            board = {},
            current_player = "X",
            game_active = false,
            user_map = {}
        }
        setState("game", state)
    end
    return state
end
2

Subscribe to the events you care about. When the room fills up, the server emits `start_game`, and every connected client receives the same authoritative payload.

Client SDK

Subscribe to game actions and update the UI

These handlers react to what the authoritative room sends back.
window.tictactoeSdk.onGameAction("start_game", (action) => {
    this.board = action.data.message.board;
    this.currentPlayer = action.data.message.current_player;
    this.updateBoard(this.board);
});

window.tictactoeSdk.onGameAction("make_move", (action) => {
    const message = action.data.message;
    this.board = message.board;
    this.currentPlayer = message.current_player;
    this.myTurn = message.current_player === this.mySymbol;
    this.updateBoard(this.board);
});

window.tictactoeSdk.onGameAction("game_over", (action) => {
    this.gameActive = false;
    this.updateBoard(action.data.message.board);
});
Server Lua

Start the game when the room is full

Players in a room are actors. `sendMessage(-1, ...)` means broadcast to every actor in the room.
function onRoomFull(rid, users)
    local state = getGameState()
    closeRoom()
    state.user_map = users
    state.game_active = true
    initializeBoard(state)
    state.current_player = "X"
    setState("game", state)

    sendMessage(-1, "start_game", {
        users = users,
        current_player = state.current_player,
        board = state.board
    })
end
3

When the player clicks, the client sends a tiny action. The room validates it, mutates state, and broadcasts the next frame or final result.

Client SDK

Send one action when the player moves

The outbound message is tiny. The room decides whether it is valid.
window.tictactoeSdk.sendGameAction("make_move", {
    position: index
});
Server Lua

Validate a move and broadcast the next state

This is where the server stays authoritative.
function onRoomAction(message)
    local state = getGameState()
    local actorIndex = message.actor_index

    if message.action_type ~= "make_move" then
        return "unexpected action type"
    end

    -- Lua arrays are 1-indexed, so client position 0 becomes board index 1.
    local pos = message.message.position + 1
    if getPlayerSymbol(actorIndex) ~= state.current_player then
        return "not your turn"
    end

    if state.board[pos] ~= "" then
        return "position already taken"
    end

    state.board[pos] = state.current_player

    local winner = checkWinner(state.board)
    if winner ~= "" or isBoardFull(state.board) then
        sendMessage(-1, "game_over", {
            winner = getWinnerActorIndex(state.user_map, winner),
            board = state.board,
            is_draw = winner == "" and isBoardFull(state.board)
        })
    else
        state.current_player = state.current_player == "X" and "O" or "X"
        sendMessage(-1, "make_move", {
            board = state.board,
            current_player = state.current_player
        })
    end

    setState("game", state)
    return nil
end

How it feels in practice

The whole loop is short and readable

Lungo keeps the protocol and room lifecycle consistent, so your code mostly describes the game, not the infrastructure.

1

Load SDK

Point the client to `masterUrl` and `authUrl`, then instantiate `GameSDK` once.

2

Join a room

Call `joinOrCreate`, get a connect string back, then connect with the player token.

3

Write Lua rules

Decide what happens on room full and on each action. Validate, mutate state, broadcast.

4

Render and play

The client just listens for `start_game`, `make_move`, `game_over` and updates the board.

What stays small

Most of the complexity never lands in your game code

The demo remains compact because transport, room wiring, and player session handling are already part of the platform.

Client stays thin

No custom socket protocol, no hand-written reconnect loop, no server bootstrap logic in the frontend.

Server logic stays readable

The Lua script reads like pure game logic: turns, valid moves, winners, and one broadcast per state change.

Good fit for indie loops

Turn-based games, board games, cards, async duels, and compact PvP interactions map especially well to this pattern.

Ready to launch

Ready to turn your game into multiplayer in one evening?

Free access to the admin panel and server, help with your first script and integration, and a personal 15 minute demo on your project.