import { useState, useEffect, useRef, useCallback } from "react"; import { render, Box, Text, Static, useApp } from "ink"; import { MODELS, MODEL_COLORS, NAME_PAD, LOG_FILE, log, runGame, type Model, type TaskInfo, type VoteInfo, type RoundState, type GameState, } from "./game.ts"; // ── Components ────────────────────────────────────────────────────────────── function Timer({ startedAt, finishedAt, }: { startedAt: number; finishedAt?: number; }) { const [now, setNow] = useState(Date.now()); useEffect(() => { if (finishedAt) return; const id = setInterval(() => setNow(Date.now()), 100); return () => clearInterval(id); }, [finishedAt]); const elapsed = ((finishedAt ?? now) - startedAt) / 1000; return ({elapsed.toFixed(1)}s); } function MName({ model, pad }: { model: Model; pad?: boolean }) { const name = pad ? model.name.padEnd(NAME_PAD) : model.name; return ( {name} ); } function RoundView({ round, total }: { round: RoundState; total: number }) { const [contA, contB] = round.contestants; return ( {/* Header */} {` ROUND ${round.num}/${total} `} {"─".repeat(50)} {/* Prompt */} {" PROMPT "} {!round.prompt && writing a prompt...} {round.promptTask.error && ( ✗ {round.promptTask.error} )} {round.prompt && ( "{round.prompt}" )} {/* Answers */} {round.phase !== "prompting" && ( {" ANSWERS "} {round.answerTasks.map((task, i) => ( {!task.finishedAt ? ( thinking... ) : task.error ? ( ✗ {task.error} ) : ( "{task.result}" )} {task.startedAt > 0 && ( )} ))} )} {/* Votes */} {(round.phase === "voting" || round.phase === "done") && ( {" VOTES "} {round.votes.map((vote, i) => ( {!vote.finishedAt ? ( voting... ) : vote.error || !vote.votedFor ? ( ✗ failed ) : ( {"→ "} )} ))} )} {/* Round result */} {round.phase === "done" && round.scoreA !== undefined && round.scoreB !== undefined && ( {"─".repeat(50)} {round.scoreA > round.scoreB ? ( {" "} wins! ({round.scoreA / 100} vs {round.scoreB / 100} votes) ) : round.scoreB > round.scoreA ? ( {" "} wins! ({round.scoreB / 100} vs {round.scoreA / 100} votes) ) : ( TIE! ({round.scoreA / 100} - {round.scoreB / 100}) )} +{round.scoreA} | +{round.scoreB} {"─".repeat(50)} )} ); } function Scoreboard({ scores }: { scores: Record }) { const sorted = Object.entries(scores).sort((a, b) => b[1] - a[1]); const maxScore = sorted[0]?.[1] || 1; const barWidth = 30; return ( {" FINAL SCORES "} {sorted.map(([name, score], i) => { const filled = Math.round((score / maxScore) * barWidth); const bar = "█".repeat(filled) + "░".repeat(barWidth - filled); const medal = i === 0 ? " 👑" : i === 1 ? " 🥈" : i === 2 ? " 🥉" : ""; return ( {String(i + 1).padStart(2)}. {name.padEnd(NAME_PAD)} {bar} {score} {medal} ); })} {sorted[0] && sorted[0][1] > 0 && ( {"🏆 "} {sorted[0][0]} is the funniest AI! )} ); } function Game({ runs }: { runs: number }) { const stateRef = useRef({ completed: [], active: null, scores: Object.fromEntries(MODELS.map((m) => [m.name, 0])), done: false, }); const [, setTick] = useState(0); const rerender = useCallback(() => setTick((t) => t + 1), []); useEffect(() => { runGame(runs, stateRef.current, rerender).then(() => { setTimeout(() => process.exit(0), 200); }); }, []); const state = stateRef.current; return ( {(round: RoundState) => ( )} {state.active && } {state.done && } {state.done && ( Log: {LOG_FILE} )} ); } // ── Main ──────────────────────────────────────────────────────────────────── const runsArg = process.argv.find((a) => a.startsWith("runs=")); const runs = runsArg ? parseInt(runsArg.split("=")[1] ?? "5", 10) : 5; if (!process.env.OPENROUTER_API_KEY) { console.error("Error: Set OPENROUTER_API_KEY environment variable"); process.exit(1); } log("INFO", "startup", `Game starting: ${runs} rounds`, { models: MODELS.map((m) => m.id), }); console.log( `\n\x1b[1m\x1b[45m\x1b[30m QUIPSLOP \x1b[0m \x1b[2mAI vs AI comedy showdown — ${runs} rounds\x1b[0m`, ); console.log( `\x1b[2mModels: ${MODELS.map((m) => m.name).join(", ")}\x1b[0m\n`, ); render();