feat: expand to 1014 words, add title-case display and 2 new themes

- Move word data from hardcoded seed.py to data/words.json
- Expand from 210 words across 7 categories to 1014 words across 14
  categories (animals, food, colors, sports, space, nature, music,
  science, countries, occupations, ocean, weather, mythology, technology)
- Word list sidebar now shows title case (Tiger) while grid stays
  uppercase; text input remains case-insensitive
- Add Ocean theme (light, blue accents) and Forest theme (dark, amber
  accents on deep green)
- Replace hardcoded rgba values with --coral-bg CSS variable so
  selection highlighting adapts per theme

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Mathew Sir Guest the best
2026-05-07 02:43:43 -06:00
co-authored by Claude Opus 4.6
parent cd1dc9200b
commit 9319f08875
13 changed files with 728 additions and 236 deletions
+108
View File
@@ -2,6 +2,7 @@
import collections
import json
import re
import time
from datetime import datetime, timezone
@@ -33,6 +34,24 @@ def _check_rate(ip, bucket="default"):
return True
_PLAYER_NAME_RE = re.compile(r"^[a-zA-Z0-9 ]+$")
MAX_PLAYER_NAME_LEN = 20
def _sanitize_player_name(raw):
"""Validate and sanitize a player name. Returns name or None (invalid)."""
if not isinstance(raw, str):
return "Anonymous"
name = raw.strip()
if not name:
return "Anonymous"
if len(name) > MAX_PLAYER_NAME_LEN:
name = name[:MAX_PLAYER_NAME_LEN]
if not _PLAYER_NAME_RE.match(name):
return None
return name
class BaseAPIHandler(tornado.web.RequestHandler):
"""Base handler for JSON API endpoints."""
@@ -112,6 +131,19 @@ class NewGameHandler(BaseAPIHandler):
return
word_count = max(3, min(30, word_count))
player_name = _sanitize_player_name(data.get("player_name", ""))
if player_name is None:
self.set_status(400)
self.write(
json.dumps(
{
"error": "Player name may only contain"
" letters, numbers, and spaces"
}
)
)
return
category = session.query(Category).filter_by(id=category_id).first()
if not category:
self.set_status(404)
@@ -130,6 +162,7 @@ class NewGameHandler(BaseAPIHandler):
self.write(json.dumps({"error": "Failed to generate board with words"}))
return
game.player_name = player_name
session.add(game)
for w in words:
session.add(w)
@@ -146,6 +179,7 @@ class NewGameHandler(BaseAPIHandler):
return {
"id": game.id,
"board_size": game.board_size,
"player_name": game.player_name,
"category": game.category.name,
"grid": game.grid,
"words": [{"text": w.text, "found": w.found} for w in game.words],
@@ -171,6 +205,7 @@ class GameStateHandler(BaseAPIHandler):
{
"id": game.id,
"board_size": game.board_size,
"player_name": game.player_name,
"category": game.category.name,
"grid": game.grid,
"words": [
@@ -296,6 +331,7 @@ class GuessHandler(BaseAPIHandler):
"cells": cells,
"words": [{"text": w.text, "found": w.found} for w in game.words],
"status": game.status,
"player_name": game.player_name,
"completed_at": (
game.completed_at.isoformat() + "Z" if game.completed_at else None
),
@@ -308,5 +344,77 @@ class GuessHandler(BaseAPIHandler):
"cells": [],
"words": [{"text": w.text, "found": w.found} for w in game.words],
"status": game.status,
"player_name": game.player_name,
"completed_at": None,
}
class ScoreboardAPIHandler(BaseAPIHandler):
"""``GET /api/scoreboard`` -- leaderboards and popular categories."""
def get(self):
session = self.get_session()
try:
categories = session.query(Category).all()
leaderboards = {}
for cat in categories:
completed = (
session.query(Game)
.filter(
Game.category_id == cat.id,
Game.status == "completed",
Game.completed_at.isnot(None),
)
.all()
)
completed.sort(
key=lambda g: (g.completed_at - g.started_at).total_seconds()
)
top = completed[:10]
if top:
leaderboards[cat.name] = [
{
"player_name": g.player_name,
"time_seconds": (
g.completed_at - g.started_at
).total_seconds(),
"board_size": g.board_size,
"word_count": len(g.words),
"completed_at": g.completed_at.isoformat() + "Z",
}
for g in top
]
popular = []
for cat in categories:
count = (
session.query(Game)
.filter(
Game.category_id == cat.id,
Game.status == "completed",
)
.count()
)
popular.append(
{
"id": cat.id,
"name": cat.name,
"completed_games": count,
}
)
popular.sort(key=lambda x: x["completed_games"], reverse=True)
self.write(
json.dumps(
{
"leaderboards": leaderboards,
"popular_categories": popular,
}
)
)
except Exception:
session.rollback()
raise
finally:
session.close()
+10
View File
@@ -0,0 +1,10 @@
"""Scoreboard page handler."""
import tornado.web
class ScoreboardPageHandler(tornado.web.RequestHandler):
"""Serves the scoreboard page."""
def get(self):
self.render("scoreboard.html")