diff --git a/app.py b/app.py index ea25d69..7b582e6 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ ############################################################################### -# app.py - Single-file Pyramid puzzle app with additional features: -# - Logout functionality. -# - Leaderboard supports sorting by top speed and top moves. -# - Navigation links are displayed on all screens. -# - Optional profile page to enable Gravatar (off by default). +# app.py - Single-file Pyramid puzzle app with enhanced features: +# - Single leaderboard entry per user per puzzle. +# - Game stays solved with download button after solving. +# - King of the Mountain global leaderboard. +# - User profiles show all solves, scores, and streaks. +# - Admin can upload multiple puzzles per day (3x3, 4x4, 5x5). +# - Responsive design for phones and computers. +# - Enhanced win animation with modal and confetti. +# - Subsequent plays after first solve do not affect leaderboard. ############################################################################### import os @@ -19,7 +23,7 @@ import hashlib from pyramid.config import Configurator from pyramid.view import view_config from pyramid.response import Response, FileResponse -from pyramid.httpexceptions import HTTPFound, HTTPFound +from pyramid.httpexceptions import HTTPFound from pyramid.session import SignedCookieSessionFactory from sqlalchemy import ( @@ -32,6 +36,8 @@ from sqlalchemy import ( Date, DateTime, ForeignKey, + desc, + func, ) from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker, relationship @@ -40,9 +46,9 @@ from waitress import serve from jinja2 import Template -############################################################################### +################################################################################ # Database Setup -############################################################################### +################################################################################ Base = declarative_base() @@ -54,19 +60,22 @@ class User(Base): code_hash = Column(String, nullable=True) # bcrypt hash of code code_expires = Column(DateTime, nullable=True) # time limit for code is_verified = Column(Boolean, default=False) - enable_gravatar = Column(Boolean, default=False) # New field for Gravatar + enable_gravatar = Column(Boolean, default=False) # Gravatar support class Puzzle(Base): __tablename__ = "puzzles" id = Column(Integer, primary_key=True) - date = Column(Date, unique=True, nullable=False) + date = Column(Date, nullable=False) + size = Column(Integer, nullable=False) # 3, 4, or 5 image_b64 = Column(String, nullable=False) initial_state_json = Column( String, nullable=False - ) # Stores initial positions and rotations + ) # initial positions and rotations title = Column(String, default="Daily Puzzle") + __table_args__ = ({"sqlite_autoincrement": True},) + class Attempt(Base): __tablename__ = "attempts" @@ -83,7 +92,10 @@ class Attempt(Base): time_started = Column(DateTime, default=datetime.datetime.utcnow) time_completed = Column(DateTime, nullable=True) is_solved = Column(Boolean, default=False) - state_json = Column(String, nullable=False) # Stores the current game state + state_json = Column(String, nullable=False) # current game state + is_counted = Column( + Boolean, default=True + ) # whether attempt counts towards leaderboard puzzle = relationship("Puzzle", backref="attempts") user = relationship("User", backref="attempts") @@ -94,15 +106,15 @@ engine = create_engine(DB_URL, echo=False) Session = sessionmaker(bind=engine) Base.metadata.create_all(engine) -############################################################################### +################################################################################ # Session Factory -############################################################################### +################################################################################ session_factory = SignedCookieSessionFactory(secret="my_insecure_secret") -############################################################################### +################################################################################ # Helper Functions -############################################################################### +################################################################################ def slugify(text): text = text.lower() text = re.sub(r"\s+", "-", text) @@ -116,14 +128,17 @@ def get_gravatar_url(email, size=100): return f"https://www.gravatar.com/avatar/{hash_code}?s={size}&d=identicon" -############################################################################### +################################################################################ # Navigation Template -############################################################################### +################################################################################ NAVIGATION_TEMPLATE = r""" + """ -############################################################################### +################################################################################ # Inline Templates -############################################################################### +################################################################################ HOME_TEMPLATE = r""" -
You are logged in as {{ user.username }}.
{% else %}You are not logged in.
Your guest name: {{ guest_name }}
{% endif %} +Welcome to the Daily Puzzles! Test your skills with our daily challenges.
+ """ @@ -174,7 +188,6 @@ LOGIN_TEMPLATE = r""" -