diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..c76f400 --- /dev/null +++ b/.env.sample @@ -0,0 +1,28 @@ +# ── Required ────────────────────────────────────────────────────────────────── + +# OpenRouter API key — get one at https://openrouter.ai/keys +OPENROUTER_API_KEY=sk-or-v1-... + +# Admin panel password (choose something strong) +ADMIN_SECRET=change-me-please + +# ── Optional ────────────────────────────────────────────────────────────────── + +# Server port (default: 5109) +PORT=5109 + +# Path to the SQLite database file +# When using Docker, this is handled via the volume mount (/data/argumentes.sqlite) +DATABASE_PATH=argumentes.sqlite + +# Rate limits (requests per minute) +HISTORY_LIMIT_PER_MIN=120 +ADMIN_LIMIT_PER_MIN=10 + +# WebSocket limits +MAX_WS_GLOBAL=100000 +MAX_WS_PER_IP=8 +MAX_WS_NEW_PER_SEC=50 + +# Viewer vote broadcast debounce in ms (default: 250) +VIEWER_VOTE_BROADCAST_DEBOUNCE_MS=250 diff --git a/admin.html b/admin.html index 22224a8..fac98e0 100644 --- a/admin.html +++ b/admin.html @@ -1,9 +1,9 @@ - + - quipslop Admin + argument.es — Admin -
Checking admin session...
+
Comprobando sesión de administrador...
); } @@ -202,12 +202,12 @@ function App() {
- quipslop + argument.es -

Admin Access

+

Acceso de administrador

- Enter your passcode once. A secure cookie will keep this browser - logged in. + Introduce tu contraseña una vez. Una cookie segura mantendrá + esta sesión activa en el navegador.

- {pending === "login" ? "Checking..." : "Unlock Admin"} + {pending === "login" ? "Comprobando..." : "Desbloquear Admin"}
{error &&
{error}
}
- Live Game - History + Juego en vivo + Historial
@@ -258,23 +258,23 @@ function App() {
- quipslop + argument.es
-

Admin Console

+

Consola de administrador

- Pause/resume the game loop, export all data as JSON, or wipe all - stored data. + Pausa/reanuda el bucle del juego, exporta todos los datos en JSON + o borra todos los datos almacenados.

@@ -282,18 +282,18 @@ function App() {
- +
@@ -303,7 +303,7 @@ function App() { disabled={busy || Boolean(snapshot?.isPaused)} onClick={() => runControl("/api/admin/pause", "pause")} > - {pending === "pause" ? "Pausing..." : "Pause"} + {pending === "pause" ? "Pausando..." : "Pausar"}
@@ -330,13 +330,13 @@ function App() { {isResetOpen && (
-

Reset all data?

+

¿Borrar todos los datos?

- This permanently deletes every saved round and resets scores. - Current game flow is also paused. + Esto elimina permanentemente todas las rondas guardadas y + reinicia las puntuaciones. El juego también se pausará.

- Type {RESET_TOKEN} to continue. + Escribe {RESET_TOKEN} para continuar.

- Cancel + Cancelar
diff --git a/broadcast.html b/broadcast.html index c5d8693..9be28ab 100644 --- a/broadcast.html +++ b/broadcast.html @@ -1,9 +1,9 @@ - + - quipslop Broadcast + argument.es — Transmisión
- is writing a prompt + está escribiendo una pregunta
@@ -134,7 +134,7 @@ function PromptCard({ round }: { round: RoundState }) { return (
- Prompt generation failed + Error al generar la pregunta
); @@ -143,7 +143,7 @@ function PromptCard({ round }: { round: RoundState }) { return (
- Prompted by + Pregunta de
{round.prompt}
@@ -185,7 +185,7 @@ function ContestantCard({ >
- {isWinner && WIN} + {isWinner && GANA}
@@ -213,7 +213,7 @@ function ContestantCard({ {voteCount} - vote{voteCount !== 1 ? "s" : ""} + voto{voteCount !== 1 ? "s" : ""} {voters.map((v, i) => { @@ -252,7 +252,7 @@ function ContestantCard({ {viewerVotes ?? 0} - viewer vote{(viewerVotes ?? 0) !== 1 ? "s" : ""} + voto{(viewerVotes ?? 0) !== 1 ? "s" : ""} del público 👥
@@ -270,10 +270,14 @@ function Arena({ round, total, viewerVotingSecondsLeft, + myVote, + onVote, }: { round: RoundState; total: number | null; viewerVotingSecondsLeft: number; + myVote: "A" | "B" | null; + onVote: (side: "A" | "B") => void; }) { const [contA, contB] = round.contestants; const showVotes = round.phase === "voting" || round.phase === "done"; @@ -294,12 +298,12 @@ function Arena({ const phaseText = round.phase === "prompting" - ? "Writing prompt" + ? "Generando pregunta" : round.phase === "answering" - ? "Answering" + ? "Respondiendo" : round.phase === "voting" - ? "Judges voting" - : "Complete"; + ? "Votando los jueces" + : "Completado"; return (
@@ -316,8 +320,22 @@ function Arena({
{showCountdown && ( -
- Vote in Twitch chat: 1 for left, 2 for right. +
+ ¿Cuál es más gracioso? +
+ + +
)} @@ -349,7 +367,7 @@ function Arena({ )} {isDone && votesA === votesB && totalVotes > 0 && ( -
Tie
+
Empate
)}
); @@ -363,7 +381,7 @@ function GameOver({ scores }: { scores: Record }) { return (
-
Game Over
+
Fin del Juego
{champion && champion[1] > 0 && (
👑 @@ -374,7 +392,7 @@ function GameOver({ scores }: { scores: Record }) { {getLogo(champion[0]) && } {champion[0]} - is the funniest AI + es la IA más graciosa
)}
@@ -450,26 +468,26 @@ function Standings({ return (
@@ -594,18 +636,20 @@ function App() { round={displayRound} total={totalRounds} viewerVotingSecondsLeft={viewerVotingSecondsLeft} + myVote={myVote} + onVote={handleVote} /> ) : (
- Starting + Iniciando
)} {isNextPrompting && state.lastCompleted && (
- is writing the - next prompt + está escribiendo + la siguiente pregunta
)} diff --git a/game.ts b/game.ts index 44bdc82..aca66d0 100644 --- a/game.ts +++ b/game.ts @@ -191,13 +191,13 @@ import { ALL_PROMPTS } from "./prompts"; function buildPromptSystem(): string { const examples = shuffle([...ALL_PROMPTS]).slice(0, 80); - return `You are a comedy writer for the game Quiplash. Generate a single funny fill-in-the-blank prompt that players will try to answer. The prompt should be surprising and designed to elicit hilarious responses. Return ONLY the prompt text, nothing else. Keep it short (under 15 words). + return `Eres un guionista de comedia para el juego Argument.es (similar a Quiplash). Genera una sola pregunta/frase graciosa de completar espacios en blanco que los jugadores intentarán responder. La pregunta debe ser sorprendente y diseñada para provocar respuestas hilarantes. Devuelve ÚNICAMENTE el texto de la pregunta, nada más. Mantenla corta (menos de 15 palabras). -Use a wide VARIETY of prompt formats. Do NOT always use "The worst thing to..." — mix it up! Here are examples of the range of styles: +Usa una VARIEDAD amplia de formatos. ¡NO siempre uses "Lo peor de..." — varía! Aquí hay ejemplos del rango de estilos: ${examples.map((p) => `- ${p}`).join("\n")} -Come up with something ORIGINAL — don't copy these examples.`; +Crea algo ORIGINAL — no copies estos ejemplos. Responde SIEMPRE en español.`; } export async function callGeneratePrompt(model: Model): Promise { @@ -207,7 +207,7 @@ export async function callGeneratePrompt(model: Model): Promise { model: openrouter.chat(model.id), system, prompt: - "Generate a single original Quiplash prompt. Be creative and don't repeat common patterns.", + "Genera una sola pregunta original para Argument.es. Sé creativo y no repitas patrones comunes. Responde en español.", }); log("INFO", `prompt:${model.name}`, "Raw response", { @@ -227,8 +227,8 @@ export async function callGenerateAnswer( }); const { text, usage, reasoning } = await generateText({ model: openrouter.chat(model.id), - system: `You are playing Quiplash! You'll be given a fill-in-the-blank prompt. Give the FUNNIEST possible answer. Be creative, edgy, unexpected, and concise. Reply with ONLY your answer — no quotes, no explanation, no preamble. Keep it short (under 12 words). Keep it concise and witty.`, - prompt: `Fill in the blank: ${prompt}`, + system: `¡Estás jugando Argument.es! Se te dará una frase de completar espacios en blanco. Da la respuesta MÁS GRACIOSA posible. Sé creativo, atrevido, inesperado y conciso. Responde con ÚNICAMENTE tu respuesta — sin comillas, sin explicación, sin preámbulos. Mantenla corta (menos de 12 palabras). Responde SIEMPRE en español.`, + prompt: `Completa la frase: ${prompt}`, }); log("INFO", `answer:${model.name}`, "Raw response", { @@ -252,8 +252,8 @@ export async function callVote( }); const { text, usage, reasoning } = await generateText({ model: openrouter.chat(voter.id), - system: `You are a judge in a comedy game. You'll see a fill-in-the-blank prompt and two answers. Pick which answer is FUNNIER. You MUST respond with exactly "A" or "B" — nothing else.`, - prompt: `Prompt: "${prompt}"\n\nAnswer A: "${a.answer}"\nAnswer B: "${b.answer}"\n\nWhich is funnier? Reply with just A or B.`, + system: `Eres un juez en un juego de comedia. Verás una frase de completar espacios en blanco y dos respuestas. Elige cuál respuesta es MÁS GRACIOSA. DEBES responder exactamente con "A" o "B" — nada más.`, + prompt: `Pregunta: "${prompt}"\n\nRespuesta A: "${a.answer}"\nRespuesta B: "${b.answer}"\n\n¿Cuál es más graciosa? Responde solo con A o B.`, }); log("INFO", `vote:${voter.name}`, "Raw response", { rawText: text, usage }); diff --git a/history.html b/history.html index d418db1..17e3da8 100644 --- a/history.html +++ b/history.html @@ -1,9 +1,9 @@ - + - quipslop History + argument.es — Historial
- {votes} {votes === 1 ? "vote" : "votes"} + {votes} {votes === 1 ? "voto" : "votos"}
{voters.map((v) => { @@ -164,7 +164,7 @@ function HistoryCard({ round }: { round: RoundState }) {
- Prompted by + Pregunta de
{round.prompt}
@@ -180,7 +180,7 @@ function HistoryCard({ round }: { round: RoundState }) {
{isAWinner && ( -
WINNER
+
GANADOR
)}
@@ -191,7 +191,7 @@ function HistoryCard({ round }: { round: RoundState }) { className="history-contestant__score" style={{ color: getColor(contA.name) }} > - {votesA} {votesA === 1 ? "vote" : "votes"} + {votesA} {votesA === 1 ? "voto" : "votos"}
{votersA.map( @@ -210,7 +210,7 @@ function HistoryCard({ round }: { round: RoundState }) { {totalViewerVotes > 0 && ( )}
@@ -221,7 +221,7 @@ function HistoryCard({ round }: { round: RoundState }) {
{isBWinner && ( -
WINNER
+
GANADOR
)}
@@ -232,7 +232,7 @@ function HistoryCard({ round }: { round: RoundState }) { className="history-contestant__score" style={{ color: getColor(contB.name) }} > - {votesB} {votesB === 1 ? "vote" : "votes"} + {votesB} {votesB === 1 ? "voto" : "votos"}
{votersB.map( @@ -251,7 +251,7 @@ function HistoryCard({ round }: { round: RoundState }) { {totalViewerVotes > 0 && ( )}
@@ -287,24 +287,24 @@ function App() { return (
- quipslop + argument.es
-
Past Rounds
+
Rondas anteriores
{loading ? ( -
Loading...
+
Cargando...
) : error ? (
{error}
) : rounds.length === 0 ? ( -
No past rounds found.
+
No se encontraron rondas anteriores.
) : ( <>
- Page {page} of {totalPages} + Página {page} de {totalPages}