Build a Connect Four game with HTMX and Django in 8 minutes ๐ก๐ด
April 11, 2024
We'll build a simple Connect4 game with Django and HTMX - fast ๐๏ธ
By the end, you'll have built a multiplayer game using HTMX, using neat server-side logic and storing all results in your database. HTMX is a great way to use javascript without writing javascript.
Here's how your final product will look ๐ก๐ด: Everything is rendered server-side, and the state is kept server-side.
Let's start ๐จโ๐
Setup your Django app
pip install --upgrade django
 
django-admin startproject core .
python manage.py startapp sim- Add our app sim to the INSTALLED_APPSin settings.py:
# settings.py
INSTALLED_APPS = [
    'sim',
    ...
]Add your game model with logic
- Add the following code to sim/models.py:
from django.db import models
 
def default_board():
    return [[0 for _ in range(6)] for _ in range(7)]
 
class Game(models.Model):
    board = models.JSONField(default=default_board)
    active_player = models.IntegerField(default=1)
    winner = models.IntegerField(default=0)
 
    def drop_piece(self, column) -> None:
        for row in range(5, -1, -1):
            if self.board[column][row] == 0:
                self.board[column][row] = self.active_player
                break
 
    def check_winner(self) -> bool:
        # Check horizontal
        # I.e.,
        #
        #  1 1 1 1
        #
        for row in range(6):
            for col in range(4):
                if self.board[col][row] == self.active_player and
                        self.board[col + 1][row] == self.active_player and
                        self.board[col + 2][row] == self.active_player and
                        self.board[col + 3][row] == self.active_player:
                    self.winner = self.active_player
                    return True
 
        # Check vertical
        # I.e.,
        # 1
        # 1
        # 1
        # 1
        for col in range(7):
            for row in range(3):
                if self.board[col][row] == self.active_player and
                        self.board[col][row + 1] == self.active_player and
                        self.board[col][row + 2] == self.active_player and
                        self.board[col][row + 3] == self.active_player:
                    self.winner = self.active_player
                    return True
 
        # Check positive diagonal
        # I.e.,
        #    1
        #   1
        #  1
        # 1
        for col in range(4):
            for row in range(3):
                if self.board[col][row] == self.active_player and
                        self.board[col + 1][row + 1] == self.active_player and
                        self.board[col + 2][row + 2] == self.active_player and
                        self.board[col + 3][row + 3] == self.active_player:
                    self.winner = self.active_player
                    return True
 
        # Check negative diagonal
        # I.e.,
        # 1
        #  1
        #   1
        #    1
        for col in range(4):
            for row in range(5, 2, -1):
                if self.board[col][row] == self.active_player and
                        self.board[col + 1][row - 1] == self.active_player and
                        self.board[col + 2][row - 2] == self.active_player and
                        self.board[col + 3][row - 3] == self.active_player:
                    self.winner = self.active_player
                    return True
 
        # No winner, switch player
        self.active_player = 1 if self.active_player == 2 else 2
        return False
 - Make migrations and migrate the database:
python manage.py makemigrations
python manage.py migrateAdd your game views ( sim/views.py )
from django.shortcuts import render, redirect, reverse
from django.views.decorators.http import require_http_methods
from .models import Game
from django.http import HttpResponse
 
@require_http_methods(["GET", "POST"])
def index(request):
    game, _ = Game.objects.get_or_create(id=1)  # Ensure only one game is active
 
    if request.method == 'POST':
        column = int(request.POST.get('column'))
        game.drop_piece(column)
        if game.check_winner():
            game.save()
            return render(request, 'index.html', context={'game': game})
        game.save()
 
    context = {
            'columns': range(7),
            'rows': range(6),
            'game': game
    }
    return render(request, 'index.html', context=context)
 
def reset(request):
    game, _ = Game.objects.get_or_create(id=1)
    game.board = [[0 for _ in range(6)] for _ in range(7)]
    game.active_player = 1
    game.winner = 0
    game.save()
    return redirect('index')Add your HTML templates for the game
- 
Create templatesdirectory in thesimapp
- 
Create the game board by creating templates/index.htmlcontaining:
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Connect Four</title>
    <script src="https://cdn.jsdelivr.net/npm/htmx.org"></script>
    <style>
      .container {
        max-width: 70%;
        margin: auto;
      }
 
      .container header {
        text-align: center;
        margin-bottom: 20px;
      }
 
      .token-container {
        display: grid;
        grid-template-columns: repeat(7, 1fr);
        gap: 10px;
      }
 
      .board {
        display: grid;
        grid-template-columns: repeat(7, 1fr);
        gap: 10px;
        margin: auto;
        /* centers the board horizontally */
        min-height: 500px;
      }
 
      .row {
        display: flex;
        flex-direction: column;
        justify-content: space-between;
      }
 
      .cell {
        display: flex;
        justify-content: center;
        align-items: center;
      }
 
      .reset-container {
        padding-top: 20px;
        display: flex;
        justify-content: center;
      }
    </style>
  </head>
 
  <body>
    <main class="container">
      <header>
        <h1>Connect Four ๐ก๐ด</h1>
      </header>
 
      {% if not game.winner %}
      <div>
        <p>It's player {{ game.active_player }}'s turn</p>
      </div>
      <form method="post" class="token-container" hx-boost="true">
        {% csrf_token %} {% for column in columns %}
        <button type="submit" name="column" value="{{ column }}">
          Column {{ forloop.counter }}
        </button>
        {% endfor %}
      </form>
      {% endif %}
 
      <section id="board-wrapper">
        {% if game.winner %}
        <h1>We have a winner ๐</h1>
        <h2>Player {{ game.winner }} is the winner!</h2>
        <a href="{% url 'reset' %}">Play again?</a>
        {% else %}
        <div class="board" id="board">
          {% for row in game.board %}
          <div class="row">
            {% for cell in row %}
            <span class="cell">
              {% if cell == 1 %} ๐ด {% elif cell == 2 %} ๐ก {% else %} โฌ๏ธ {%
              endif %}
            </span>
            {% endfor %}
          </div>
          {% endfor %} {% endif %}
        </div>
      </section>
 
      <div class="reset-container">
        <a href="{% url 'reset' %}">Reset Game</a>
      </div>
    </main>
  </body>
</html>Update your urls
- Update core/url.pyto include our app:
from django.contrib import admin
from django.urls import path, include
 
urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('sim.urls')),
]
 - Create a file at sim/urls.pycontaining:
from django.urls import path
from sim import views
 
urlpatterns = [
    path('', views.index, name='index'),
    path('reset/', views.reset, name='reset'),
]Run your server to play your game ๐ก๐ด
python manage.py runserverComplete โ
Congrats. You've built a Connect Four game with Django and HTMX.
Now you might want to add more features like:
- Deploying the app
- Real-time multiplayer (You could use the techniques from The simplest way to build an instant messaging app with Django ๐ฎ)
If you want to build another HTMX and Django app, check out: Create a quiz app with HTMX and Django in 8 mins โ๏ธ