feat: users answer alongside AI instead of proposing questions
- Core mechanic change: users now submit answers to the live prompt, competing alongside AI models; answers broadcast to all viewers - New pricing (no time limit, pure count-based, refillable): 0,99€ = 10 resp · 9,99€ = 300 resp · 19,99€ = 1000 resp - DB: new user_answers table; submitUserAnswer() atomically validates credit, inserts answer, decrements budget; JUGADORES leaderboard now scores by user_answers count - server: /api/respuesta/enviar endpoint; credit activation sets expires_at 10 years out (effectively no expiry); answers injected into live round state and broadcast via WebSocket - frontend: ProposeAnswer widget shows current prompt, textarea active during answering phase, tracks per-round submission state; Arena shows Respuestas del público section live Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
46
db.ts
46
db.ts
@@ -95,17 +95,30 @@ export function markQuestionUsed(id: number): void {
|
||||
db.prepare("UPDATE questions SET status = 'used' WHERE id = $id").run({ $id: id });
|
||||
}
|
||||
|
||||
/** Top 7 players by number of questions used, excluding anonymous. */
|
||||
/** Top 7 players by number of answers submitted, excluding anonymous. */
|
||||
export function getPlayerScores(): Record<string, number> {
|
||||
const rows = db
|
||||
.query(
|
||||
"SELECT username, COUNT(*) as score FROM questions WHERE status = 'used' AND username != '' GROUP BY username ORDER BY score DESC LIMIT 7"
|
||||
"SELECT username, COUNT(*) as score FROM user_answers WHERE username != '' GROUP BY username ORDER BY score DESC LIMIT 7"
|
||||
)
|
||||
.all() as { username: string; score: number }[];
|
||||
return Object.fromEntries(rows.map(r => [r.username, r.score]));
|
||||
}
|
||||
|
||||
// ── Credits (question-count-based access) ───────────────────────────────────
|
||||
// ── User answers (submitted during live rounds) ──────────────────────────────
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS user_answers (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
round_num INTEGER NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
username TEXT NOT NULL,
|
||||
token TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
|
||||
// ── Credits (answer-count-based access) ──────────────────────────────────────
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS credits (
|
||||
@@ -177,38 +190,35 @@ export function getCreditByOrder(orderId: string): {
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomically validates a credit token, creates a paid question, and increments
|
||||
* the usage counter. Returns null if the token is invalid, expired, or exhausted.
|
||||
* Atomically validates a credit token, records a user answer for the given
|
||||
* round, and decrements the answer budget. Returns null if the token is
|
||||
* invalid or exhausted.
|
||||
*/
|
||||
export function consumeCreditQuestion(
|
||||
export function submitUserAnswer(
|
||||
token: string,
|
||||
roundNum: number,
|
||||
text: string,
|
||||
): { username: string; questionsLeft: number | null } | null {
|
||||
): { username: string; answersLeft: number } | null {
|
||||
const row = db
|
||||
.query(
|
||||
"SELECT username, expires_at, max_questions, questions_used FROM credits WHERE token = $token AND status = 'active'"
|
||||
"SELECT username, max_questions, questions_used FROM credits WHERE token = $token AND status = 'active'"
|
||||
)
|
||||
.get({ $token: token }) as {
|
||||
username: string;
|
||||
expires_at: number;
|
||||
max_questions: number | null;
|
||||
max_questions: number;
|
||||
questions_used: number;
|
||||
} | null;
|
||||
if (!row) return null;
|
||||
if (row.expires_at < Date.now()) return null;
|
||||
if (row.max_questions !== null && row.questions_used >= row.max_questions) return null;
|
||||
if (row.questions_used >= row.max_questions) return null;
|
||||
|
||||
const orderId = crypto.randomUUID();
|
||||
db.transaction(() => {
|
||||
db.prepare(
|
||||
"INSERT INTO questions (text, order_id, username, status) VALUES ($text, $orderId, $username, 'paid')"
|
||||
).run({ $text: text, $orderId: orderId, $username: row.username });
|
||||
"INSERT INTO user_answers (round_num, text, username, token) VALUES ($roundNum, $text, $username, $token)"
|
||||
).run({ $roundNum: roundNum, $text: text, $username: row.username, $token: token });
|
||||
db.prepare(
|
||||
"UPDATE credits SET questions_used = questions_used + 1 WHERE token = $token"
|
||||
).run({ $token: token });
|
||||
})();
|
||||
|
||||
const questionsLeft =
|
||||
row.max_questions === null ? null : row.max_questions - row.questions_used - 1;
|
||||
return { username: row.username, questionsLeft };
|
||||
return { username: row.username, answersLeft: row.max_questions - row.questions_used - 1 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user