Can Django make a live game?

Games like https://beyondmafia.com/, or https://wolvesville.com/ seem to involve game states with backends using node.js, websockets, databases, etc. Then front-end frameworks.

The sites run multiple 5-12+ player games where there are rounds of chatting and voting to eliminate players. There are bonus features like items that give special abilities, like to veto a vote.

Can Django do what node.js does for these apps? What about without a front-end framework?

Django & Channels together can do it, yes.

Thanks, Ken. While I was researching I found a reply of yours to a similar question. Any resources you can point me to other than the Django docs and Channels docs?

Not specifically with the React integration, no. (I don’t use React. I’ve done all my work along those lines with Django, Channels, and HTMX.)

I’m not sure which post of mine you found, but I will point you at Django-channels consumer.py to display a table.py from django-table2 in a template in Django - #2 by KenWhitesell

I have no preference to React, I was just mentioning what those sites use.




I made a chat room app, and the user list will update based on who is active in it.

View:

def lobby(request, lobby_name):
    lobby, created = Lobby.objects.get_or_create(lobby_name=lobby_name)

    if request.user.profile.current_game == None:#retrieving user profile and adding them to lobby in DB
        request.user.profile.current_game = lobby
        request.user.profile.save()

        return render(request, "mafia/room.html", {
            'lobby_name': lobby.lobby_name,
            'players': lobby.players.order_by('display_name')
        })
    else:
        return HttpResponse("<h1>You're in a game already</h1>")

in the Consumer:

    async def connect(self):
        self.room_name = self.scope["url_route"]["kwargs"]["room_name"]
        self.room_group_name = "mafia_%s" % self.room_name
        self.scope["session"]["seed"] = random.randint(1, 1000)
        self.user = self.scope["user"]
        await self.channel_layer.group_send(
            self.room_group_name, {"type": "refresh_user_log"})


        # Join room group
        await self.channel_layer.group_add(self.room_group_name, self.channel_name)
        await self.accept()

    async def disconnect(self, close_code):
        # Leave room group
        await self.remove_from_lobby()
        await self.channel_layer.group_send(
            self.room_group_name, {"type": "refresh_user_log"})
        await self.channel_layer.group_discard(self.room_group_name, self.channel_name)

    @database_sync_to_async
    def remove_from_lobby(self):
        self.user.profile.current_game = None
        self.user.profile.save()

    async def refresh_user_log(self, event):
        player_list = await self.get_players()

        message_html = f"<form id='players'>{player_list}</form>"

        await self.send(text_data=json.dumps({"message": message_html}))

    @database_sync_to_async
    def get_players(self):
        lobby = models.Lobby.objects.get(lobby_name = self.room_name)
        player_list = ""
        
        for player in lobby.players.order_by('display_name'):
            player_list += f"{player.display_name}, "

        return player_list

Is this optimal?? I feel like there’s a juggling performance going on with the view and the consumer.

I’m not sure what you mean by a “juggling performance”.

The lobby view loads a page. That page would likely contain (directly or by reference) the code to connect and manage the websocket. Once the page is loaded, the view doesn’t need to do anything else. I see it as more a “handoff” than a juggle.

We do it similarly to what you show here, except we use HTMX in the browser. That way we can render the html fragments on the server and just send the appropriate divs across the websocket. (We also use a slightly-patched version of django-channels-presence ¡ PyPI to track connection information across rooms.)

1 Like
<div hx-ext="ws" ws-connect="/ws/mafia/{{ lobby_name }}/">
        <div id="players">
            {% for player in players %}
                {{ player.display_name }}, 
            {% endfor %}
        </div>
        <div id="chat-log">
            ...
        </div>
        <form id="form" ws-send>
            <input name="message">
        </form>
</div>

I’m doing HTMX also. I could use presence, or would an async view (never wrote one) be able to keep track of player (ORM object), in a room (ORM object with relational ‘players’), before the handoff? Then the consumer would not need the sync-to-async methods.

Keep in mind that once the view has returned the page to the browser, it’s done. It has no more effect on anything. It plays no further part in any communications.

We don’t even render the people in the room in the view. The view only returns the basic structure for the page. All “active” content is rendered by the consumer and related code. When a person connects to a chat room, the first thing that happens is that the consumer gets the names of the people in the room, renders the text, and sends it back out to the person connecting.

For those cases where we want to access the database in the consumer, we use the sync-to-async methods - they have never caused us any problems.

However, we have also made the architectural decision that all games are managed externally to the consumers. Each game is a Channels worker process - communication between the consumers and the games are through the channel layer. That allows us to run the game processors synchronously - ensuring we don’t create any race conditions when resolving input.
(From this perspective, the “Lobby” is just another “game”. A person enters text in a box in the browser, htmx sends that as a websocket frame to the consumer, the consumer forwards that text to the lobby worker, the lobby worker sends the message to all the consumers in the room. The consumers distribute that message back out to the browsers.)

1 Like

Okay I understand. I was misinterpreting what an async view does.

How are you making the workers process and hold short-term data for specific games?

e.g. If I want to pop from a list to assign nicknames to players, where do I place this list? Is there an init method in the worker? Am wondering if the workers can hold data for these games played by each channel layer group.

First, for the purposes of perspective, there is no such thing as “short-term data”.

Data is persisted in the database as represented by Django models. Worker processes read and update data just like any other Django code. (See Worker and Background Tasks — Channels 4.0.0 documentation)

Games are state machines. At any moment in time, a game is fundamentally in one of two states - either it’s waiting for input from one (or more) “players” (more on that in a moment), or it’s processing input to alter the state of the game.

Our models for this get fairly involved. The state of a game consists of two parts, the general state and the game-specific state.

The general state consists of data that applies to any game being played, such as which Users are playing and when the game was started.

The game-specific state consists of all the data needed to store the state of the specific game being played. For example, if it were a chess game, it would be the current board, how much clock time is available for each person, and flags for each person to indicate whether or not they can still castle - and to which side(s). (It can’t be definitively determined from the board itself.)

For tracking players, we use an object named Player as the many-to-many “through” table between User and Game. The player’s nickname is a field in that Model. (A person can have different nicknames in different games.)

Note regarding the use of the term “player” above. A “player” may also be a “bot” - computer player, or a “timer” causing something to happen after a specified interval.