<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Andrey</title>
    <description>The latest articles on DEV Community by Andrey (@skymanrm).</description>
    <link>https://dev.to/skymanrm</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F804863%2Fdc6b3017-aa0d-46d8-a0f7-92a13be6228d.jpeg</url>
      <title>DEV Community: Andrey</title>
      <link>https://dev.to/skymanrm</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/skymanrm"/>
    <language>en</language>
    <item>
      <title>Creating a simple tic-tac-toe game with Django / Channels / DRF / Celery and Typescript.</title>
      <dc:creator>Andrey</dc:creator>
      <pubDate>Thu, 27 Jan 2022 21:06:47 +0000</pubDate>
      <link>https://dev.to/skymanrm/creating-a-simple-tic-tac-toe-game-with-django-channels-drf-celery-and-typescript-1opm</link>
      <guid>https://dev.to/skymanrm/creating-a-simple-tic-tac-toe-game-with-django-channels-drf-celery-and-typescript-1opm</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;This is the first of four parts on creating a simple game based on Python and Typescript. I and my colleague worked on our small pet project (online turn-based fighting) and I think, our experience could be useful for someone else.&lt;br&gt;
We will start from scratch and in the end we will have online multiplayer game with simple matchmaking and bots. We don’t have a big number of player in our game, so I don’t have proofs that our solution is working with big loading, but it should be good for horizontal scaling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First step&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In my article, I will provide only our-app-specific code and installation details. Otherwise, this article will contain many duplicate guides from other places. So, if in the article you will see:&lt;br&gt;
&lt;em&gt;Install redis&lt;/em&gt;&lt;br&gt;
Please open &lt;a href="https://google.com"&gt;https://google.com&lt;/a&gt; and search for “How to install redis &lt;em&gt;your os name&lt;/em&gt;”&lt;br&gt;
Let’s check you — Install Django ;) After Django will be installed we need to create two apps, run next commands:&lt;br&gt;
&lt;code&gt;python manage.py startapp players&lt;/code&gt;&lt;br&gt;
&lt;code&gt;python manage.py startapp match&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Now we need to install Channels&lt;br&gt;
&lt;code&gt;pip install -U channels&lt;/code&gt;&lt;br&gt;
&lt;a href="https://channels.readthedocs.io/en/stable/installation.html"&gt;https://channels.readthedocs.io/en/stable/installation.html&lt;/a&gt; they have a good tutorial on how to integrate it to Django project. You can use it, or just check source code of this project on github.&lt;br&gt;
Also, we will need to install redis for channels to use to communicate between connections.&lt;br&gt;
&lt;code&gt;pip install channels_redis&lt;/code&gt;&lt;br&gt;
And update settings.py to use redis&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CHANNEL_LAYERS = {
  'default': {
    'BACKEND': 'channels_redis.core.RedisChannelLayer',
    'CONFIG': {'hosts': [('127.0.0.1', 6379)]},
  },
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Please make sure, you have installed and run redis service, you can find how to do that here &lt;a href="https://redis.io/topics/quickstart"&gt;https://redis.io/topics/quickstart&lt;/a&gt;&lt;br&gt;
To communicate between frontend and backend we will use JSON and we will use DRF to serialize/deserialize/validate data. Installation is also straight-forward:&lt;br&gt;
&lt;a href="https://www.django-rest-framework.org/#installation"&gt;https://www.django-rest-framework.org/#installation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prepare data models&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We will use the next models in our app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class Player(models.Model):
  id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
  name = models.CharField(max_length=50, unique=True)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We will not use any passwords for players, just the name.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class Match(models.Model):
  id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
  players = models.ManyToManyField('players.Player')
  started = models.BooleanField(default=False)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our next step is to create migrations for new models and apply them (this action will also apply all existed migrations)&lt;br&gt;
&lt;code&gt;python manage.py makemigrations&lt;/code&gt;&lt;br&gt;
&lt;code&gt;python manage.py migrate&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Let’s check that our app is working.&lt;br&gt;
Create &lt;code&gt;templates/index.html&lt;/code&gt; to provide a start point for users. Create a view to render this template to user:&lt;br&gt;
in &lt;code&gt;match/views.py&lt;/code&gt; add next code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def index(request):
  return render(request, ‘index.html’)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And add this view to urls.py&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;urlpatterns = [
  path('admin/', admin.site.urls),
  path('', index),
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the server and open in browser &lt;a href="http://localhost:8000"&gt;http://localhost:8000&lt;/a&gt; you should see your index.html&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;XHR endpoints&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We will use xhr requests to one direction communication, when client needs to retrieve any details based on events, for example: user login, open a new screen, click on the button, etc.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/join&lt;/code&gt; endpoint&lt;br&gt;
Something like authentication, this endpoint will receive name and will create a new user or return an existing one. We will use user.id as an authentication token to sign other requests.&lt;br&gt;
Create a &lt;code&gt;players/serializers.py&lt;/code&gt; file — we will use it to store serializers for DRF. Let’s write our first serializer for the Player model, it will be pretty simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class PlayerSerializer(serializers.ModelSerializer):
  class Meta:
    model = Player
    fields = '__all__'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This serializer takes all model fields and outputs them to a specific format (JSON in our case).&lt;br&gt;
Now we need to add a view to process user requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class JoinView(APIView):
  def post(self, request):
    player, created = Player.objects.get_or_create(name=request.data[“name”])
    return Response(PlayerSerializer(instance=player).data)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can use standard Django View, but DRF provides APIView class that makes working with XHR requests pretty simple.&lt;br&gt;
The last step — add this view to &lt;code&gt;urls.py&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;urlpatterns = [
  path('admin/', admin.site.urls),
  path('', index),
  path('players/join', JoinView.as_view()),
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/start&lt;/code&gt; endpoint&lt;br&gt;
This endpoint starts matchmaking for the user. Logic will be very simple — we are looking for matches with players count = 1 and started flag is False. If match is not found — we create a new one.&lt;br&gt;
Creating a serializer for Match model&lt;br&gt;
&lt;code&gt;match/serializers.py&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class MatchSerializer(serializers.ModelSerializer):
  players = PlayerSerializer(read_only=True, many=True)

  class Meta:
    model = Match
    fields = '__all__'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It’s very similar to &lt;code&gt;PlayerSeriazlier&lt;/code&gt; except one thing, we want to provide detailed information for each user when returning information about the match.&lt;/p&gt;

&lt;p&gt;Create view &lt;code&gt;match/views.py&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class StartView(APIView):
  def post(self, request):
    player_uuid = request.headers.get(“Player-Id”, None)
    player = Player.objects.get(id=player_uuid)
    match = Match.objects.annotate(players_count=Count(‘players’)).filter(started=False, players_count=1).first()
    if not match:
      match = Match.objects.create()
    match.players.add(player)
    return Response(MatchSerializer(instance=match).data)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and add it to urls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;urlpatterns = [
  path(‘admin/’, admin.site.urls),
  path(‘’, index),
  path(‘players/join’, JoinView.as_view()),
  path(‘match/start’, StartView.as_view()),
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These two endpoints are enough to log in and start the match. Now we will create an endpoint for the websocket. After the user starts the match, server will return match.id, we will use id to determine match where user is connecting. In Channels — views are calling Consumer, so we need to add our first consumer &lt;code&gt;match/consumers.py&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class MatchConsumer(AsyncWebsocketConsumer):
  async def connect(self):
    match_id = self.scope[“url_route”][“kwargs”][“match_id”]
    match = await get_match(match_id)
    if match.started: 
      raise Exception(“Already started”)
    players_count = await get_players_count(match_id)
    if players_count &amp;gt; 1: 
      raise Exception(“Too many players”)
    await add_player_to_match(match_id, self.scope[“player”])
    self.match_group_name = “match_%s” % match_id
    await self.channel_layer.group_add(self.match_group_name, self.channel_name)
    await self.accept()

  async def disconnect(self, close_code):
    await self.channel_layer.group_discard(self.match_group_name, self.channel_name)

  async def receive(self, text_data):
    text_data_json = json.loads(text_data)
    message = text_data_json[“message”]
    await self.channel_layer.group_send( self.match_group_name, {“type”: “chat_message”, “message”: message})

  async def chat_message(self, event):
    message = event[“message”]
    await self.send(text_data=json.dumps({“message”: message}))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;connect&lt;/code&gt; method we are looking for match that user connecting, then checking that user can join the match. If all is ok — we are adding user to a specific group that we will use to send messages about the match (round start, round end, win, lose, etc)&lt;br&gt;
receive and chat_message&lt;br&gt;
I have copied content from Channels guide, we will update it later.&lt;br&gt;
As you can see, in connect method we are using self.scope[“player”], we need to provide “player” to the scope. To do that we need to create custom auth middleware&lt;br&gt;
&lt;code&gt;players/​​playerauth.py&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@database_sync_to_async
def get_player(player_id):
  return Player.objects.get(id=player_id.decode(“utf8”))

class PlayerAuthMiddleware:
  def __init__(self, app):
    self.app = app

  async def __call__(self, scope, receive, send):
    scope[“player”] = await get_player(scope[“query_string”])
    return await self.app(scope, receive, send)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We will provide player.id in the query string and then use it to authenticate request. Our websocket url will be something like this:&lt;br&gt;
&lt;code&gt;/match/1234–1234–1234–1234/?player-id&lt;/code&gt;&lt;br&gt;
Let’s update asgi.py (you should be already updated it when installing channels) to use PlayerAuthMiddleware&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;application = ProtocolTypeRouter({
  “http”: get_asgi_application(),
  “websocket”: PlayerAuthMiddleware(URLRouter(match.routing.websocket_urlpatterns)),
})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last step to provide user access to our consumer is creating a router for it &lt;code&gt;match/routing.py&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;websocket_urlpatterns = [re_path(r”match/(?P&amp;lt;match_id&amp;gt;.+)/$”, consumers.MatchConsumer.as_asgi()),]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you should be able to run the server and view index.html. In the next part of tutorial we will start working on the UI and will establish websocket connection between UI and AP&lt;/p&gt;

</description>
      <category>django</category>
      <category>python</category>
      <category>gamedev</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
