Build a Connect Four game with HTMX and Django in 8 minutes ๐ŸŸก๐Ÿ”ด

Published: 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_APPS in 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 migrate

Add 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 templates directory in the sim app

  • Create the game board by creating templates/index.html containing:

<!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.py to 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.py containing:
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 runserver

Complete โœ…

Congrats. You've built a Connect Four game with Django and HTMX.

Now you might want to add more features like:

If you want to build another HTMX and Django app, check out: Create a quiz app with HTMX and Django in 8 mins โ˜‘๏ธ

Subscribe to my free newsletter

Get updates on AI, software, and business.