• Blog
  • Docs
  • Pricing
  • We’re hiring!
Log inSign up
alexwein

alexwein

thinkalittlelonger

a little game where you think of big words
Public
Like
thinkalittlelonger
Home
Code
5
frontend
6
scripts
1
towniePrompts
3
README.md
H
index.ts
Branches
1
Pull requests
Remixes
History
Environment variables
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.
Sign up now
Code
/
towniePrompts
/
todo.md
Code
/
towniePrompts
/
todo.md
Search
…
Viewing readonly version of main branch: v125
View latest version
todo.md

club leaderboards and postGame

Don't make code changes outside of this file. Help me edit this prompt so that Townie with Claude Opus 4.5 gives best results.

I want to change the postgame workflow to support club leaderboards.

After the user gets their scored results,

  • header

  • resultsTable

  • clubNameInput

  • userNameInput

  • submitToLeaderboardButton

  • club names should be a string of only 26 letters in the alphabet. no digits or other characters or spaces.(ie, can be input using the keyboard component)

  • club names should be no more than 25 characters long

likewise for players:

  • player names should be a string of 26 letters in the alphabet. no digits or other characters or spaces.(ie, can be input using the keyboard component)
  • player names should be no more than 25 characters long

collecting game data

each submission gets a unique id, two players can submit with the same userName and clubName

Here's what I was thinking for data needed to be collected to support the leaderboard view. Although submitted at is not currently needed, it should be included. if this is insufficient, or could be handled better, make those changes.

  • submission_id

  • game_id

  • club_name

  • user_name

  • submitted_at: timestamp when the user submitted to finish the final round, ending the game.

  • total_points: sum of length of every valid submission, as displayed on the postGame resultsTable.

  • submissions: array of strings containing the words the user played, in order

  • submission_validity : array of booleans of same length of submission, true for valid words and false for invalid,

  • create a file scripts/initDbResults.ts to initialize a game_results table where these are stored.

leadboardBoard view

  • add leaderboard as a gameState.
  • the user transitions to the leaderboard state after they press the submitToLeaderboardButton
  • the leaderboard view displays a scrollable table with the results of every submission of to that club.
  • the results should be sorted by default from most total points to least
  • reuse the roundIndicator, but with an extra unconnected dot for total points, to allow the user to change the sort order to sort by round.
  • if two users have the same total points, use the username as a tiebreaker for sort order.
  • if two users have the same number of points for a round, use the submitted word as the first tiebreaker, followed by user_name.

routing

  • The club name should be autopopulated (but editable) if the user navigated to the app via a path with "/club/{clubname}/" (or "/club/{clubname}" without the terminal "/")
  • if a user inputs the url with a path "/club/{clubname}/leaderboard/" into their browser, that should take them to club leaderboard
  • if there are no player submissions with that club name, indicate "nobody in the club has played yet!"
  • when the user navigates from the postGame to leaderboard gameState, update the path in the browser as well.

REFINED PROMPTS FOR TOWNIE

Context for All Prompts

Current Architecture:

  • Main app component: frontend/components/App.tsx
  • GameState type: "preGame" | "inGame" | "postGame" (defined in App.tsx)
  • Backend server: index.ts (Hono framework)
  • Database: Val Town SQLite via import { sqlite } from "https://esm.town/v/std/sqlite"
  • React version: 18.2.0 (via esm.sh)
  • Keyboard component exists at: frontend/components/Keyboard.tsx (supports A-Z input only)
  • RoundIndicator component exists at: frontend/components/RoundIndicator.tsx
  • Existing API endpoint: POST /api/validate for word validation

PROMPT 1: Database Schema & Initialization Script

Create a new file scripts/initDbResults.ts to initialize the game_results table for storing club leaderboard data.

Requirements:

  1. Import sqlite: import { sqlite } from "https://esm.town/v/std/sqlite"

  2. Create table with this schema:

CREATE TABLE IF NOT EXISTS game_results ( submission_id TEXT PRIMARY KEY, game_id TEXT NOT NULL, club_name TEXT NOT NULL, user_name TEXT NOT NULL, submitted_at TEXT NOT NULL, total_points INTEGER NOT NULL, submissions TEXT NOT NULL, submission_validity TEXT NOT NULL )
  1. Create an index for efficient club queries:
CREATE INDEX IF NOT EXISTS idx_club_name ON game_results(club_name)
  1. Create an index for efficient club + total_points queries (for leaderboard sorting):
CREATE INDEX IF NOT EXISTS idx_club_leaderboard ON game_results(club_name, total_points DESC, user_name)

Implementation notes:

  • submissions and submission_validity will be stored as JSON strings (arrays)
  • submission_id should be generated as a UUID when inserting
  • club_name and user_name should be stored as uppercase for case-insensitive matching
  • submitted_at should be ISO 8601 timestamp string
  • Use sqlite.execute() with SQL strings

Validation: After creating the file, run it to initialize the table.


PROMPT 2: Add Leaderboard GameState & PostGame UI

Update frontend/components/App.tsx to add club submission UI to the postGame view.

Requirements:

  1. Update GameState type:
type GameState = "preGame" | "inGame" | "postGame" | "leaderboard";
  1. Add state for club submission form (add these useState calls):
const [clubName, setClubName] = useState(""); const [playerName, setPlayerName] = useState("");
  1. Add URL routing logic:
  • On component mount (useEffect), check window.location.pathname
  • If path matches /club/{clubname} or /club/{clubname}/, extract clubname and set it to clubName state (converted to uppercase)
  • If path matches /club/{clubname}/leaderboard or /club/{clubname}/leaderboard/, set gameState to "leaderboard" and clubName state
  1. Update the postGame JSX block (where gameState === "postGame"):

After the <ResultsTable results={results} />, add:

<div className="club-submission"> <h2>Submit to Club Leaderboard</h2> <div className="input-group"> <label htmlFor="club-name">Club Name</label> <input id="club-name" type="text" value={clubName} onChange={(e) => { const val = e.target.value.toUpperCase().replace(/[^A-Z]/g, ''); if (val.length <= 25) setClubName(val); }} placeholder="Enter club name (A-Z only, max 25)" maxLength={25} /> </div> <div className="input-group"> <label htmlFor="player-name">Player Name</label> <input id="player-name" type="text" value={playerName} onChange={(e) => { const val = e.target.value.toUpperCase().replace(/[^A-Z]/g, ''); if (val.length <= 25) setPlayerName(val); }} placeholder="Enter your name (A-Z only, max 25)" maxLength={25} /> </div> <button onClick={handleSubmitToLeaderboard} disabled={clubName.length === 0 || playerName.length === 0} className="submit-leaderboard-button" > Submit to Leaderboard </button> </div>
  1. Add handleSubmitToLeaderboard function:
const handleSubmitToLeaderboard = async () => { if (!results || clubName.length === 0 || playerName.length === 0) return; // Calculate total points const totalPoints = results.reduce((sum, r) => sum + (r.isValid ? r.word.length : 0), 0 ); // Generate game_id (you can use timestamp or a simple UUID approach) const gameId = crypto.randomUUID(); const payload = { game_id: gameId, club_name: clubName, user_name: playerName, total_points: totalPoints, submissions: results.map(r => r.word), submission_validity: results.map(r => r.isValid) }; try { const response = await fetch("/api/submit-result", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); if (response.ok) { // Navigate to leaderboard setGameState("leaderboard"); window.history.pushState({}, "", `/club/${clubName}/leaderboard`); } else { alert("Failed to submit to leaderboard"); } } catch (error) { console.error("Submission error:", error); alert("Failed to submit to leaderboard"); } };

Implementation notes:

  • Input validation happens in onChange handlers
  • Only A-Z characters allowed, automatically converted to uppercase
  • Submit button is disabled unless both fields have valid input
  • After successful submission, transition to leaderboard gameState and update browser URL

PROMPT 3: Backend API Endpoints

Add two new API endpoints to index.ts:

Endpoint 1: POST /api/submit-result

Add this route to the Hono app:

app.post("/api/submit-result", async (c) => { const { game_id, club_name, user_name, total_points, submissions, submission_validity } = await c.req.json(); // Validate inputs if (!club_name || !user_name || !game_id) { return c.json({ error: "Missing required fields" }, 400); } // Generate unique submission_id const submission_id = crypto.randomUUID(); const submitted_at = new Date().toISOString(); // Convert arrays to JSON strings for storage const submissionsJson = JSON.stringify(submissions); const validityJson = JSON.stringify(submission_validity); try { await sqlite.execute({ sql: `INSERT INTO game_results (submission_id, game_id, club_name, user_name, submitted_at, total_points, submissions, submission_validity) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, args: [ submission_id, game_id, club_name.toUpperCase(), user_name.toUpperCase(), submitted_at, total_points, submissionsJson, validityJson ] }); return c.json({ submission_id }); } catch (error) { console.error("Database error:", error); return c.json({ error: "Failed to save result" }, 500); } });

Endpoint 2: GET /api/leaderboard/:clubName

Add this route to the Hono app:

app.get("/api/leaderboard/:clubName", async (c) => { const clubName = c.req.param("clubName").toUpperCase(); try { const result = await sqlite.execute({ sql: `SELECT submission_id, user_name, total_points, submissions, submission_validity, submitted_at FROM game_results WHERE club_name = ? ORDER BY total_points DESC, user_name ASC`, args: [clubName] }); // Transform rows to objects and parse JSON fields const entries = result.rows.map(row => ({ submission_id: row[0], user_name: row[1], total_points: row[2], submissions: JSON.parse(row[3] as string), submission_validity: JSON.parse(row[4] as string), submitted_at: row[5] })); return c.json({ club_name: clubName, entries }); } catch (error) { console.error("Database error:", error); return c.json({ error: "Failed to fetch leaderboard" }, 500); } });

Implementation notes:

  • Both endpoints use parameterized queries to prevent SQL injection
  • club_name and user_name are normalized to uppercase for case-insensitive matching
  • Arrays are stored as JSON strings and parsed when retrieved
  • Leaderboard is sorted by total_points DESC, with user_name as tiebreaker

PROMPT 4: Leaderboard View Component

Create a new component frontend/components/Leaderboard.tsx and integrate it into App.tsx.

Requirements:

  1. Create Leaderboard.tsx:
/** @jsxImportSource https://esm.sh/react@18.2.0 */ import { useState, useEffect } from "https://esm.sh/react@18.2.0"; import { RoundIndicator } from "./RoundIndicator.tsx"; interface LeaderboardEntry { submission_id: string; user_name: string; total_points: number; submissions: string[]; submission_validity: boolean[]; submitted_at: string; } interface LeaderboardProps { clubName: string; } export function Leaderboard({ clubName }: LeaderboardProps) { const [entries, setEntries] = useState<LeaderboardEntry[]>([]); const [loading, setLoading] = useState(true); const [sortByRound, setSortByRound] = useState<number | null>(null); // null = total useEffect(() => { fetchLeaderboard(); }, [clubName]); const fetchLeaderboard = async () => { try { const response = await fetch(`/api/leaderboard/${clubName}`); const data = await response.json(); setEntries(data.entries || []); } catch (error) { console.error("Failed to load leaderboard:", error); } finally { setLoading(false); } }; const sortedEntries = [...entries].sort((a, b) => { if (sortByRound === null) { // Sort by total points if (b.total_points !== a.total_points) { return b.total_points - a.total_points; } return a.user_name.localeCompare(b.user_name); } else { // Sort by specific round const aPoints = a.submission_validity[sortByRound] ? a.submissions[sortByRound].length : 0; const bPoints = b.submission_validity[sortByRound] ? b.submissions[sortByRound].length : 0; if (bPoints !== aPoints) { return bPoints - aPoints; } // Tiebreaker: word alphabetically const wordCompare = a.submissions[sortByRound].localeCompare(b.submissions[sortByRound]); if (wordCompare !== 0) { return wordCompare; } // Final tiebreaker: username return a.user_name.localeCompare(b.user_name); } }); if (loading) { return <p>Loading leaderboard...</p>; } if (entries.length === 0) { return ( <div className="leaderboard"> <h1>Club: {clubName}</h1> <p>Nobody in the club has played yet!</p> </div> ); } const numRounds = entries[0]?.submissions.length || 0; return ( <div className="leaderboard"> <h1>Club Leaderboard: {clubName}</h1> <RoundIndicator currentRound={sortByRound === null ? numRounds : sortByRound} totalRounds={numRounds} showTotal={true} onRoundClick={(round) => setSortByRound(round === numRounds ? null : round)} /> <div className="leaderboard-table-container"> <table className="leaderboard-table"> <thead> <tr> <th>Rank</th> <th>Player</th> <th>Total Points</th> <th>Words</th> </tr> </thead> <tbody> {sortedEntries.map((entry, index) => ( <tr key={entry.submission_id}> <td>{index + 1}</td> <td>{entry.user_name}</td> <td>{entry.total_points}</td> <td className="words-cell"> {entry.submissions.map((word, i) => ( <span key={i} className={entry.submission_validity[i] ? "valid" : "invalid"} > {word} </span> ))} </td> </tr> ))} </tbody> </table> </div> </div> ); }
  1. Update App.tsx to use Leaderboard component:

Add import:

import { Leaderboard } from "./Leaderboard.tsx";

Add a new conditional block after the postGame block:

if (gameState === "leaderboard") { return ( <div className="app-wrapper"> <div className="game-container"> <Header /> <div className="content-area"> <Leaderboard clubName={clubName} /> <button onClick={handleStartGame} className="start-button">Play Again</button> </div> </div> </div> ); }
  1. Update RoundIndicator.tsx to support the leaderboard use case:

Add optional props:

interface RoundIndicatorProps { currentRound: number; totalRounds: number; showTotal?: boolean; // Show an extra dot for "Total" onRoundClick?: (round: number) => void; // Make dots clickable }

Update the component logic to:

  • Render totalRounds + 1 dots when showTotal is true (last dot represents "Total")
  • Make dots clickable if onRoundClick is provided
  • Highlight the appropriate dot based on currentRound

Implementation notes:

  • Default sort is by total_points (DESC), then user_name (ASC)
  • When sorting by a specific round, tiebreakers are: word (alphabetically), then user_name
  • RoundIndicator is reused but enhanced with click handlers
  • The leaderboard table is scrollable for many entries

PROMPT 5: Routing & URL Handling

Update index.ts and App.tsx to support club-based routing.

Requirements:

  1. Update index.ts to handle club routes:

Add catch-all route before the 404:

// Serve index.html for club routes app.get("/club/:clubName", () => serveFile("/frontend/index.html")); app.get("/club/:clubName/", () => serveFile("/frontend/index.html")); app.get("/club/:clubName/leaderboard", () => serveFile("/frontend/index.html")); app.get("/club/:clubName/leaderboard/", () => serveFile("/frontend/index.html"));
  1. Update App.tsx useEffect for initial routing:

Add this useEffect near the top of the App component (after all useState declarations):

useEffect(() => { const path = window.location.pathname; const clubMatch = path.match(/^\/club\/([A-Z]+)(\/leaderboard)?/i); if (clubMatch) { const extractedClubName = clubMatch[1].toUpperCase(); setClubName(extractedClubName); if (clubMatch[2]) { // Path includes /leaderboard setGameState("leaderboard"); } } }, []);

Implementation notes:

  • Routes are case-insensitive but club names are normalized to uppercase
  • Both trailing slash variants are supported
  • Client-side routing updates the browser URL without page reload using window.history.pushState
  • Direct navigation to /club/MYCLUB/leaderboard shows the leaderboard immediately

Summary of Changes

These 5 prompts will:

  1. ✅ Create database schema for storing game results
  2. ✅ Add club/player name inputs to postGame UI
  3. ✅ Create API endpoints for submitting and fetching leaderboard data
  4. ✅ Build the leaderboard view component with sorting
  5. ✅ Implement URL-based routing for clubs

Each prompt is focused on a specific feature area and can be executed independently (though they build on each other in sequence).

FeaturesVersion controlCode intelligenceCLIMCP
Use cases
TeamsAI agentsSlackGTM
DocsShowcaseTemplatesNewestTrendingAPI examplesNPM packages
PricingNewsletterBlogAboutCareers
We’re hiring!
Brandhi@val.townStatus
X (Twitter)
Discord community
GitHub discussions
YouTube channel
Bluesky
Open Source Pledge
Terms of usePrivacy policyAbuse contact
© 2026 Val Town, Inc.