Created
May 25, 2021 15:47
-
-
Save ociule/4ea62d0b6649c44006e502d52f3c0e45 to your computer and use it in GitHub Desktop.
Django teaching: tetris with HTMX
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{% 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 %} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{% 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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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], ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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