Skip to content

Instantly share code, notes, and snippets.

@ociule
Created May 25, 2021 15:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ociule/4ea62d0b6649c44006e502d52f3c0e45 to your computer and use it in GitHub Desktop.
Save ociule/4ea62d0b6649c44006e502d52f3c0e45 to your computer and use it in GitHub Desktop.
Django teaching: tetris with HTMX
body{
background:lightblue !important;
}
.box {
background: black;
border-radius: 4px;
width: 40px;
height:40px;
}
.col {
/* margin-top: 3px !important; */
padding-left: 0;
padding-right: 0;
}
{% extends "base_generic.html" %}
{% block content %}
<div class="row">
<h1>Tetris HTMX</h1>
</div>
{% include "play_area.html" %}
<div class="row">
<button type="button"
hx-post="/" hx-target="#play_area" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
name="up" value="1" class="btn btn-secondary">Up</button>
</div>
<div class="row">
<div class="btn-group" role="group" aria-label="Inputs">
<button type="button"
hx-post="/" hx-target="#play_area" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
name="left" value="1" class="btn btn-secondary">Left</button>
<button type="button" class="btn btn-secondary"
hx-post="/" hx-target="#play_area" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
name="down" value="1">Down</button>
<button type="button"
hx-post="/" hx-target="#play_area" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
name="right" value="1" class="btn btn-secondary">Right</button>
</div>
</div>
{% endblock %}
{% load tetris_htmx_tags %}
<div id="play_area" class="container" style="width: 400px;"
hx-get="/" hx-trigger="load delay:0.2s" hx-swap="outerHTML">
{% for y in range20 %}
<div class="row">
{% for x in range10 %}
<div class="col">
<div class="box bg-{% grid_color game_state.board forloop.parentloop.counter0 forloop.counter0 %}"></div>
</div>
{% endfor %}
</div>
{% endfor %}
<div class="row">
Step {{ game_state.game_step }}
</div>
</div>
import random
import time
import math
from django.core.cache import cache
from .tetrominoes import Tetrominoes
STEP_DURATION = 1.0
BETWEEN_NEW_PIECE_STEPS = 10
BOARD_HEIGHT = 20
BOARD_WIDTH = 10
def get_game_step():
game_start_timestamp = cache.get('game_start_timestamp', None)
if game_start_timestamp is None:
game_start_timestamp = time.time()
cache.set('game_start_timestamp', game_start_timestamp)
seconds_since_start = time.time() - game_start_timestamp
return math.floor(seconds_since_start / STEP_DURATION)
def new_board():
board = []
for _ in range(BOARD_HEIGHT):
board.append([0] * BOARD_WIDTH)
return board
def should_introduce_new_piece(game_step):
next_new_piece_step = cache.get('next_new_piece_step')
return game_step >= next_new_piece_step
def copy_footprint(row, col, tetramino, board):
for iy, footprint_row in enumerate(tetramino.footprint):
for ix, footprint_col in enumerate(tetramino.footprint[iy]):
if footprint_col != 0:
board[row + iy][col + ix] = footprint_col
return board
def delete_footprint(row, col, tetramino, board):
for iy, footprint_row in enumerate(tetramino.footprint):
for ix, footprint_col in enumerate(tetramino.footprint[iy]):
if footprint_col != 0:
board[row + iy][col + ix] = 0
return board
def drop_unsupported_pieces(unsupported_pieces, board, input):
for ix, (row, col, tetramino) in enumerate(unsupported_pieces):
# Remove old from board:
board = delete_footprint(row, col, tetramino, board)
if input == "down":
if row + 2 < BOARD_HEIGHT - 1: # Not about to touch bottom
row = row + 2
elif input == "left":
col -= 1
elif input == "right":
col += 1
elif input is None:
# Drop 1 row
row = row + 1
if row + 2 == BOARD_HEIGHT - 1: # Touching bottom
_ = unsupported_pieces.pop(ix) # remove from unsupported pieces
else:
unsupported_pieces[ix] = (row, col, tetramino)
# Render new position to board
board = copy_footprint(row, col, tetramino, board)
return unsupported_pieces, board
def get_game_board(game_step, input):
if game_step == 0:
# New game!
board = new_board()
cache.set('next_new_piece_step', 1)
cache.set('board', board)
cache.set('unsupported_pieces', [])
return board
board = cache.get('board', None)
unsupported_pieces = cache.get('unsupported_pieces', None)
unsupported_pieces, board = drop_unsupported_pieces(unsupported_pieces, board, input)
if should_introduce_new_piece(game_step):
new_piece = random.choice(list(Tetrominoes))
unsupported_pieces.append((0, 4, new_piece))
board = copy_footprint(0, 4, new_piece, board)
next_new_piece_step = game_step + BETWEEN_NEW_PIECE_STEPS
cache.set('next_new_piece_step', next_new_piece_step)
cache.set('unsupported_pieces', unsupported_pieces)
cache.set('board', board)
return board
def get_game_state(input):
game_step = get_game_step()
board = get_game_board(game_step, input)
state = {'game_step': get_game_step(), 'board': board}
return state
I = 1
O = 2
T = 3
J = 4
L = 5
S = 6
Z = 7
from enum import Enum
class Tetrominoes(Enum):
I = I
O = O
T = T
J = J
L = L
S = S
Z = Z
#Tetrominoes = [I, O, T, J, L, S, Z]
#Tetrominoes.I = I
Tetrominoes.I.height = 4
Tetrominoes.I.footprint = [[0] * 4, [0] * 4, [I] * 4]
#Tetrominoes.O = O
Tetrominoes.O.height = 2
Tetrominoes.O.footprint = [[0] * 4, [O, O, 0, 0], [O, O, 0, 0]]
#Tetrominoes.T = T
Tetrominoes.T.height = 2
Tetrominoes.T.footprint = [[0] * 4, [T, T, T, 0], [0, T, 0, 0]]
#Tetrominoes.J = J
Tetrominoes.J.height = 3
Tetrominoes.J.footprint = [[0, J, 0, 0], [0, J, 0, 0], [J, J, 0, 0]]
#Tetrominoes.L = L
Tetrominoes.L.height = 3
Tetrominoes.L.footprint = [[L, 0, 0, 0], [L, 0, 0, 0], [L, L, 0, 0]]
#Tetrominoes.S = S
Tetrominoes.S.height = 2
Tetrominoes.S.footprint = [[0, 0, 0, 0], [0, S, S, 0], [S, S, 0, 0]]
#Tetrominoes.Z = Z
Tetrominoes.Z.height = 2
Tetrominoes.Z.footprint = [[0, 0, 0, 0], [Z, Z, 0, 0], [0, Z, Z, 0], ]
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
import tetris_htmx.tetris as tetris
def index(request):
if request.method == 'POST' and request.htmx:
input = request.htmx.trigger_name
else:
input = None
if request.htmx:
template_name = "play_area.html"
else:
template_name = "index.html"
game_state = tetris.get_game_state(input)
context = {"range10": range(10), "range20": range(20), 'game_state': game_state}
return render(request, template_name, context=context)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment