Add puzzle creation

This commit is contained in:
Stephani, John 2026-03-16 16:46:45 -05:00
parent d52af8f024
commit 5f91521229
No known key found for this signature in database
GPG Key ID: D1DF11026392A2DD
7 changed files with 310 additions and 10 deletions

View File

@ -1,13 +1,126 @@
from flask import Flask, send_file, render_template, request
from flask import Flask, abort, render_template, request
import datetime
import json
import sqlite3
import subprocess
import config
app = Flask(__name__)
DEFAULT_CATEGORIES = [
{"name": "linux commands", "items": ["cd", "touch", "cat", "find"], "level": 1},
{"name": "types of HTML input", "items": ["radio", "password", "submit", "text"], "level": 2},
{"name": "sticky ___", "items": ["tape", "fingers", "situation", "wicket"], "level": 3},
{"name": "how i feel, spelled backwards", "items": ["live", "deliver", "desserts", "denier"], "level": 4},
]
def get_puzzle_numbers(cursor):
rows = cursor.execute("SELECT number FROM puzzle ORDER BY number").fetchall()
return [row[0] for row in rows]
def get_puzzle_categories(cursor, puzzle_number):
row = cursor.execute("SELECT data FROM puzzle WHERE number = ?", [puzzle_number]).fetchone()
if not row:
return None
try:
payload = json.loads(row[0])
categories = payload.get("categories")
if isinstance(categories, list) and len(categories) == 4:
return categories
except (json.JSONDecodeError, TypeError):
return None
return None
def render_puzzle(puzzle_number=None):
connection = sqlite3.connect(config.db_file)
cursor = connection.cursor()
puzzle_numbers = get_puzzle_numbers(cursor)
if puzzle_number is None and puzzle_numbers:
puzzle_number = puzzle_numbers[-1]
if puzzle_number is not None:
categories = get_puzzle_categories(cursor, puzzle_number)
if categories is None and puzzle_number in puzzle_numbers:
categories = DEFAULT_CATEGORIES
elif categories is None:
connection.close()
abort(404)
else:
categories = DEFAULT_CATEGORIES
connection.close()
return render_template(
'index.html',
categories=categories,
puzzle_numbers=puzzle_numbers,
current_puzzle=puzzle_number,
)
@app.route('/')
def index():
return render_template('index.html')
return render_puzzle()
@app.route('/<int:puzzle_number>')
def puzzle_by_number(puzzle_number):
return render_puzzle(puzzle_number)
@app.route('/new', methods=['GET', 'POST'])
def new_puzzle():
if request.method == 'GET':
return render_template('new.html', creation_date=datetime.date.today().isoformat())
author = request.form.get("author", "").strip()
creation_date = request.form.get("creation_date", "").strip() or datetime.date.today().isoformat()
categories = []
for i in range(1, 5):
name = request.form.get(f"category_{i}_name", "").strip()
words = []
for j in range(1, 5):
word = request.form.get(f"category_{i}_word_{j}", "").strip()
if word:
words.append(word)
categories.append({"name": name, "items": words, "level": i})
if not author:
return render_template(
'new.html',
error="Author is required.",
creation_date=creation_date,
form=request.form
), 400
if any(not cat["name"] or len(cat["items"]) != 4 for cat in categories):
return render_template(
'new.html',
error="Each category needs a name and exactly 4 words.",
creation_date=creation_date,
form=request.form
), 400
data = json.dumps({"categories": categories}, separators=(",", ":"))
connection = sqlite3.connect(config.db_file)
cursor = connection.cursor()
row = cursor.execute("SELECT COALESCE(MAX(number), 0) + 1 FROM puzzle").fetchone()
number = row[0]
cursor.execute(
"""
INSERT INTO puzzle (number, author, creation_date, data)
VALUES (?, ?, ?, ?)
""",
[number, author, creation_date, data]
)
connection.commit()
connection.close()
return render_template(
'new.html',
success=f"Saved puzzle #{number}.",
creation_date=creation_date
)
if __name__ == '__main__':
subprocess.run(["python3", "make_db.py"])
subprocess.run(["python3", "make_db.py"], check=True)
app.run(host='0.0.0.0', port=8000)

2
app/config.py Normal file
View File

@ -0,0 +1,2 @@
db_dir = "/connections/db"
db_file = f"{db_dir}/data.db"

20
app/make_db.py Normal file
View File

@ -0,0 +1,20 @@
import os
import sqlite3
import config
os.makedirs(config.db_dir, exist_ok=True)
connection = sqlite3.connect(config.db_file)
cursor = connection.cursor()
cursor.execute(
"""
CREATE TABLE IF NOT EXISTS puzzle(
number INTEGER PRIMARY KEY,
author TEXT,
creation_date TEXT,
data CLOB
)
"""
)
connection.commit()
connection.close()

View File

@ -19,6 +19,26 @@ h1 {
margin-bottom: 20px;
letter-spacing: 2px;
}
.header-links {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
margin-bottom: 14px;
}
.header-links a {
border: 1px solid #000;
border-radius: 999px;
padding: 6px 10px;
text-decoration: none;
color: #000;
font-weight: bold;
font-size: 13px;
}
.header-links a.active {
background: #000;
color: #fff;
}
#grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
@ -84,3 +104,82 @@ button:disabled {
height: 24px;
color: #d33;
}
#message.ok {
color: #2b7d32;
}
.subtitle {
margin-top: -8px;
margin-bottom: 18px;
font-weight: bold;
letter-spacing: 1px;
}
.builder {
width: 100%;
max-width: 700px;
}
.meta-row {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
margin-bottom: 14px;
}
.meta-row label {
display: flex;
flex-direction: column;
font-size: 13px;
gap: 6px;
font-weight: bold;
}
.meta-row input {
border: 1px solid #b8b8ae;
border-radius: 6px;
padding: 10px;
font-size: 14px;
}
.builder-grid {
max-width: 700px;
}
.builder-row {
height: auto;
padding: 10px;
align-items: stretch;
}
.builder-row > input {
border: 1px solid #8f8f8f;
border-radius: 6px;
padding: 10px;
font-size: 14px;
font-weight: bold;
}
.builder-words {
margin-top: 8px;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.builder-words input {
border: 1px solid #8f8f8f;
border-radius: 6px;
padding: 10px;
font-size: 14px;
}
.ghost-link {
padding: 12px 24px;
border-radius: 25px;
border: 1px solid #000;
background: white;
color: #000;
cursor: pointer;
font-weight: bold;
font-size: 14px;
text-decoration: none;
line-height: 22px;
}
@media (max-width: 640px) {
.meta-row {
grid-template-columns: 1fr;
}
.builder-words {
grid-template-columns: 1fr;
}
}

View File

@ -9,6 +9,13 @@
</head>
<body>
<nav class="header-links">
<a href="/">/</a>
{% for number in puzzle_numbers %}
<a href="/{{ number }}" class="{{ 'active' if current_puzzle == number else '' }}">/{{ number }}</a>
{% endfor %}
<a href="/new">/new</a>
</nav>
<h1>Connections</h1>
@ -23,13 +30,7 @@
<div id="message"></div>
<script>
// EDIT YOUR CATEGORIES HERE
const categories = [
{name: "linux commands", items: ["cd", "touch", "cat", "find"], level: 1},
{name: "types of HTML input", items: ["radio", "password", "submit", "text"], level: 2},
{name: "sticky ___", items: ["tape", "fingers", "situation", "wicket"], level: 3},
{name: "how i feel, spelled backwards", items: ["live", "deliver", "desserts", "denier"], level: 4}
];
const categories = {{ categories | tojson }};
let words = categories.flatMap(cat => cat.items).sort(() => Math.random() - 0.5);
let selected = [];

63
app/templates/new.html Normal file
View File

@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Create Connections Puzzle</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<h1>Connections</h1>
<p class="subtitle">Create Puzzle</p>
<form method="post" class="builder">
<div class="meta-row">
<label>
Author
<input type="text" name="author" value="{{ (form.author if form else '') }}" required>
</label>
<label>
Creation Date
<input type="date" name="creation_date" value="{{ (form.creation_date if form else creation_date) }}" required>
</label>
</div>
<div id="grid" class="builder-grid">
{% for i in range(1, 5) %}
<div class="card solved builder-row" style="background-color: var(--lvl-{{ i }})">
<input
type="text"
name="category_{{ i }}_name"
placeholder="Category {{ i }} name"
value="{{ (form['category_' ~ i ~ '_name'] if form else '') }}"
required
>
<div class="builder-words">
{% for j in range(1, 5) %}
<input
type="text"
name="category_{{ i }}_word_{{ j }}"
placeholder="Word {{ j }}"
value="{{ (form['category_' ~ i ~ '_word_' ~ j] if form else '') }}"
required
>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
<div class="controls">
<button type="submit">Save Puzzle</button>
<a class="ghost-link" href="/">Back to Puzzle</a>
</div>
</form>
<div id="message" class="{{ 'ok' if success else '' }}">
{% if error %}{{ error }}{% elif success %}{{ success }}{% endif %}
</div>
</body>
</html>

View File

@ -7,3 +7,5 @@ services:
stop_signal: SIGINT
ports:
- '8003:8000'
volumes:
- "./db:/connections/db"