feat: add scoreboard with player names, per-category leaderboards, and themed tables
Add player_name column to games (migration included), with regex-based validation and anti-abuse sanitization. New /scoreboard page shows recent games from localStorage, popular categories with play counts, and top-10 per-category leaderboards sorted by completion time. Also includes two-click reverse word matching, base URL prefix support for reverse-proxy hosting, BasePageHandler refactoring, themed table CSS for all 5 themes, and comprehensive test coverage for player names and scoreboard API. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
9319f08875
commit
36c3c712fe
@@ -0,0 +1,19 @@
|
||||
"""Shared handler base classes."""
|
||||
|
||||
import tornado.web
|
||||
|
||||
|
||||
class BasePageHandler(tornado.web.RequestHandler):
|
||||
"""Base handler that injects ``base_url`` into all templates."""
|
||||
|
||||
def static_url(self, path, **kwargs):
|
||||
url = super().static_url(path, **kwargs)
|
||||
base_url = self.application.settings.get("base_url", "/")
|
||||
if base_url != "/":
|
||||
url = base_url + url.lstrip("/")
|
||||
return url
|
||||
|
||||
def get_template_namespace(self):
|
||||
ns = super().get_template_namespace()
|
||||
ns["base_url"] = self.application.settings.get("base_url", "/")
|
||||
return ns
|
||||
+6
-2
@@ -1,6 +1,10 @@
|
||||
import tornado.web
|
||||
"""About page handler."""
|
||||
|
||||
from handlers import BasePageHandler
|
||||
|
||||
|
||||
class AboutHandler(tornado.web.RequestHandler):
|
||||
class AboutHandler(BasePageHandler):
|
||||
"""Serves the about page."""
|
||||
|
||||
def get(self):
|
||||
self.render("about.html")
|
||||
|
||||
+9
-1
@@ -290,11 +290,19 @@ class GuessHandler(BaseAPIHandler):
|
||||
except (IndexError, KeyError, TypeError):
|
||||
return self._miss_response(game, session)
|
||||
|
||||
# Try forward and reversed letter order
|
||||
reversed_letters = letters[::-1]
|
||||
word = (
|
||||
session.query(Word)
|
||||
.filter_by(game_id=game.id, text=letters, found=False)
|
||||
.first()
|
||||
)
|
||||
if not word:
|
||||
word = (
|
||||
session.query(Word)
|
||||
.filter_by(game_id=game.id, text=reversed_letters, found=False)
|
||||
.first()
|
||||
)
|
||||
if not word:
|
||||
return self._miss_response(game, session)
|
||||
|
||||
@@ -303,7 +311,7 @@ class GuessHandler(BaseAPIHandler):
|
||||
{"row": word.start_row + i * dr, "col": word.start_col + i * dc}
|
||||
for i in range(len(word.text))
|
||||
]
|
||||
if cells != expected_cells:
|
||||
if cells != expected_cells and list(reversed(cells)) != expected_cells:
|
||||
return self._miss_response(game, session)
|
||||
|
||||
return self._mark_found(session, game, word)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
"""Game page handler."""
|
||||
|
||||
from handlers import BasePageHandler
|
||||
|
||||
|
||||
class GameHandler(BasePageHandler):
|
||||
"""Serves the game page where users play word search."""
|
||||
|
||||
def get(self):
|
||||
self.render("game.html")
|
||||
@@ -1,6 +1,10 @@
|
||||
import tornado.web
|
||||
"""How to Play page handler."""
|
||||
|
||||
from handlers import BasePageHandler
|
||||
|
||||
|
||||
class HowToPlayHandler(tornado.web.RequestHandler):
|
||||
class HowToPlayHandler(BasePageHandler):
|
||||
"""Serves the how-to-play page."""
|
||||
|
||||
def get(self):
|
||||
self.render("howtoplay.html")
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
"""Landing page handler."""
|
||||
|
||||
from handlers import BasePageHandler
|
||||
|
||||
|
||||
class MainHandler(BasePageHandler):
|
||||
"""Serves the home page."""
|
||||
|
||||
def get(self):
|
||||
self.render("main.html")
|
||||
@@ -1,9 +1,9 @@
|
||||
"""Scoreboard page handler."""
|
||||
|
||||
import tornado.web
|
||||
from handlers import BasePageHandler
|
||||
|
||||
|
||||
class ScoreboardPageHandler(tornado.web.RequestHandler):
|
||||
class ScoreboardPageHandler(BasePageHandler):
|
||||
"""Serves the scoreboard page."""
|
||||
|
||||
def get(self):
|
||||
|
||||
Reference in New Issue
Block a user