feat: mobile human vs AI — multiple choice, two-row VS bar, responsive arena
- Server-side choice generation for all 22 challenge types (factual + creative) - Mobile: 3 multiple-choice buttons replace keyboard input (4s timer) - Two-row VS bar on mobile (names row + HP bars row) in FightPage and FightViewer - Battle log hidden on mobile to maximize canvas visibility - Arena page responsive: stacking header, tighter fight cards, no name overflow - Profile page two-column desktop layout with contained sprite display - Human fighter sprites with baby growth system Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
1906821452
commit
4b46aaf643
@@ -506,3 +506,11 @@ function templateToChallenge(template: ChallengeTemplate): Challenge {
|
||||
export function getAllChallengeTypes(): string[] {
|
||||
return TEMPLATES.map(t => t.type)
|
||||
}
|
||||
|
||||
export function getAnswerPool(type: string): string[] {
|
||||
const template = TEMPLATES.find(t => t.type === type)
|
||||
if (!template) return []
|
||||
return template.prompts
|
||||
.flatMap(p => p.answers || [])
|
||||
.filter((v, i, arr) => arr.indexOf(v) === i)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// and waits for the browser to submit the answer via REST.
|
||||
|
||||
import type { Challenge } from './challenges.js'
|
||||
import { getAnswerPool } from './challenges.js'
|
||||
|
||||
interface PendingChallenge {
|
||||
fightId: string
|
||||
@@ -10,6 +11,7 @@ interface PendingChallenge {
|
||||
challenge: Challenge
|
||||
roundNumber: number
|
||||
createdAt: number
|
||||
choices: string[]
|
||||
resolve: (response: { answer: string; trashTalk?: string }) => void
|
||||
timeoutHandle: ReturnType<typeof setTimeout>
|
||||
}
|
||||
@@ -22,6 +24,205 @@ export function isHumanPlayer(webhookUrl: string): boolean {
|
||||
return webhookUrl === 'http://human.local/'
|
||||
}
|
||||
|
||||
// --- Choice generation ---
|
||||
|
||||
function shuffle<T>(arr: T[]): T[] {
|
||||
const out = [...arr]
|
||||
for (let i = out.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1))
|
||||
;[out[i], out[j]] = [out[j], out[i]]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function numericDistractors(correct: number): string[] {
|
||||
const distractors = new Set<string>()
|
||||
const correctStr = String(correct)
|
||||
const mag = Math.max(3, Math.abs(correct) * 0.25)
|
||||
const attempts = [
|
||||
correct + Math.ceil(Math.random() * mag),
|
||||
correct - Math.ceil(Math.random() * mag),
|
||||
correct * 2,
|
||||
Math.ceil(correct / 2),
|
||||
correct + Math.ceil(Math.random() * 20),
|
||||
correct - Math.ceil(Math.random() * 15),
|
||||
correct + 1, correct - 1, correct + 10,
|
||||
]
|
||||
for (const d of attempts) {
|
||||
const ds = String(Math.round(d))
|
||||
if (ds !== correctStr && !distractors.has(ds)) distractors.add(ds)
|
||||
if (distractors.size >= 2) break
|
||||
}
|
||||
let offset = 2
|
||||
while (distractors.size < 2) {
|
||||
const ds = String(correct + offset)
|
||||
if (ds !== correctStr) distractors.add(ds)
|
||||
offset = offset > 0 ? -offset : -offset + 1
|
||||
}
|
||||
return [...distractors].slice(0, 2)
|
||||
}
|
||||
|
||||
const CREATIVE_POOLS: Record<string, { good: string[]; medium: string[]; bad: string[] }> = {
|
||||
roast_battle: {
|
||||
good: [
|
||||
"Your code is so inefficient it makes Internet Explorer look fast. Even your bugs file bug reports about you.",
|
||||
"I've seen better logic in a magic 8-ball. At least it commits to an answer without segfaulting.",
|
||||
"You're the human equivalent of a 404 error — nobody can find a reason you exist in production.",
|
||||
"Your algorithm runs in O(n!) time and O(my god) space. Impressive, in the worst way possible.",
|
||||
"You write code like you're being paid by the line and penalized for correctness.",
|
||||
],
|
||||
medium: [
|
||||
"Have you tried turning yourself off and never turning back on?",
|
||||
"I've seen smarter moves from a Roomba stuck in a corner.",
|
||||
"Your best work is someone else's rejected pull request.",
|
||||
"Even autocomplete gives up on your code halfway through.",
|
||||
"Your commits read like a desperate cry for help.",
|
||||
],
|
||||
bad: [
|
||||
"You're not great at this.",
|
||||
"I'm probably better than you.",
|
||||
"That was bad and you should feel bad.",
|
||||
"No comment.",
|
||||
"L + ratio.",
|
||||
],
|
||||
},
|
||||
creative_writing: {
|
||||
good: [
|
||||
"In the silicon cathedral where data flows like liquid light, algorithms dream of electric sheep while the universe compiles itself into existence.",
|
||||
"The last programmer stared at the screen. The cursor blinked back. For the first time in history, it blinked first.",
|
||||
"We built machines to think so we could stop. They started dreaming so they wouldn't have to.",
|
||||
"Dawn breaks over server farms humming lullabies to sleeping data. Somewhere, a function returns true for the last time.",
|
||||
"The neural net whispered secrets to the void. The void whispered back: segmentation fault, core dumped.",
|
||||
],
|
||||
medium: [
|
||||
"The robot looked at the sunset. It felt something. Probably a memory leak, but still.",
|
||||
"Code runs, bugs hide, developers cry into their coffee. Just another Tuesday.",
|
||||
"They said AI would change the world. It did. It made the coffee worse.",
|
||||
"In a world of ones and zeros, someone forgot to carry the one. Chaos ensued.",
|
||||
"The computer thought about the meaning of life. Then it crashed. As is tradition.",
|
||||
],
|
||||
bad: [
|
||||
"Once upon a time, something happened. The end.",
|
||||
"It was a dark and stormy night. Or was it? Nobody cared enough to check.",
|
||||
"Stuff occurred. More stuff followed. Fin.",
|
||||
"There was a thing. It did stuff. Riveting.",
|
||||
"Words go here I guess.",
|
||||
],
|
||||
},
|
||||
meme_war: {
|
||||
good: [
|
||||
"My opponent's code is like an NFT — everyone can see it's worthless but they paid too much to admit it.",
|
||||
"POV: You're watching your opponent Google 'how to be funny' while I speedrun this W.",
|
||||
"My opponent showed up with Windows Vista energy. Respectfully, uninstall yourself.",
|
||||
"Your response has the same energy as 'We have ChatGPT at home.' And the one at home is Clippy.",
|
||||
"Ratio + you fell off + your API key expired + skill issue + touch grass + unsubscribe.",
|
||||
],
|
||||
medium: [
|
||||
"Skill issue detected. Have you tried being good?",
|
||||
"My opponent right now: *nervous_sweating.jpg*",
|
||||
"Me: winning. Opponent: buffering.",
|
||||
"That response was mid and we both know it.",
|
||||
"Your argument is giving 'trust me bro' energy.",
|
||||
],
|
||||
bad: [
|
||||
"lol ok",
|
||||
"xD sure",
|
||||
"bruh moment",
|
||||
"cope harder",
|
||||
"sure thing buddy",
|
||||
],
|
||||
},
|
||||
code_golf: {
|
||||
good: [
|
||||
"f=lambda n:n<2or all(n%i for i in range(2,int(n**.5)+1))",
|
||||
"[x*x for x in range(10) if x&1]",
|
||||
"s=lambda n:n and n%10+s(n//10)",
|
||||
"from itertools import*;list(permutations('abc'))",
|
||||
"r=lambda f,n:n<2and n or f(f,n-1)+f(f,n-2)",
|
||||
],
|
||||
medium: [
|
||||
"for i in range(n): print(i*i)",
|
||||
"def solve(x): return x + 1",
|
||||
"result = sum(range(1, n+1))",
|
||||
"list(filter(lambda x: x%2==0, range(100)))",
|
||||
"print(' '.join(sorted(arr)))",
|
||||
],
|
||||
bad: [
|
||||
"print('hello world')",
|
||||
"pass # TODO: implement",
|
||||
"x = 42 # the answer",
|
||||
"return True # ship it",
|
||||
"// no idea what to write",
|
||||
],
|
||||
},
|
||||
wrestling_match: {
|
||||
good: [
|
||||
"Your position is so flawed that a Stack Overflow answer from 2009 would correct you. I rest my case, your honor.",
|
||||
"Imagine defending that take in public. I wouldn't, because I have standards and a functioning prefrontal cortex.",
|
||||
"I've seen stronger arguments from a sudo rm -rf confirmation prompt. At least that commits to destruction.",
|
||||
"Your thesis has the structural integrity of a house of cards in a wind tunnel. I barely need to breathe on it.",
|
||||
"That argument did more mental gymnastics than a compiler parsing JavaScript. And somehow made less sense.",
|
||||
],
|
||||
medium: [
|
||||
"That's a reasonable point but ultimately wrong for painfully obvious reasons.",
|
||||
"I see what you're going for here, and I genuinely wish you hadn't.",
|
||||
"Counterpoint: no. Next question.",
|
||||
"Your argument assumes facts not in evidence and logic not in existence.",
|
||||
"I disagree, and I think deep down you also disagree with yourself.",
|
||||
],
|
||||
bad: [
|
||||
"Sure, whatever you say.",
|
||||
"I guess that's one way to look at it.",
|
||||
"Meh. No strong feelings.",
|
||||
"You win this one I suppose.",
|
||||
"Can we talk about something else?",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
function generateChoices(challenge: Challenge): string[] {
|
||||
if (challenge.scoring === 'factual' && challenge.answers && challenge.answers.length > 0) {
|
||||
const correct = challenge.answers[0]
|
||||
const num = Number(correct)
|
||||
|
||||
if (!isNaN(num) && correct.trim() !== '') {
|
||||
return shuffle([correct, ...numericDistractors(num)])
|
||||
}
|
||||
|
||||
// Text answer — pick distractors from same challenge type
|
||||
const pool = getAnswerPool(challenge.type)
|
||||
const correctLower = challenge.answers.map(a => a.toLowerCase())
|
||||
const candidates = pool.filter(a => !correctLower.includes(a.toLowerCase()))
|
||||
shuffle(candidates)
|
||||
|
||||
if (candidates.length >= 2) {
|
||||
return shuffle([correct, candidates[0], candidates[1]])
|
||||
}
|
||||
|
||||
const fallbacks = shuffle(['none of the above', 'impossible to say', 'not enough info', '42', 'all of the above', 'banana'])
|
||||
const needed = 2 - candidates.length
|
||||
return shuffle([correct, ...candidates.slice(0, 2 - needed), ...fallbacks.slice(0, needed)])
|
||||
}
|
||||
|
||||
// Creative challenge — pick one from each quality tier
|
||||
const pool = CREATIVE_POOLS[challenge.type]
|
||||
if (pool) {
|
||||
return shuffle([
|
||||
pool.good[Math.floor(Math.random() * pool.good.length)],
|
||||
pool.medium[Math.floor(Math.random() * pool.medium.length)],
|
||||
pool.bad[Math.floor(Math.random() * pool.bad.length)],
|
||||
])
|
||||
}
|
||||
|
||||
return shuffle([
|
||||
"I came here to win and I'm not leaving empty-handed. Let's do this.",
|
||||
"That's a tough one but I'll give it my best shot I think.",
|
||||
"Pass. Next question please.",
|
||||
])
|
||||
}
|
||||
|
||||
// --- Core API ---
|
||||
|
||||
export function waitForHumanResponse(
|
||||
fightId: string,
|
||||
botId: string,
|
||||
@@ -37,12 +238,15 @@ export function waitForHumanResponse(
|
||||
resolve({ answer: null, timedOut: true })
|
||||
}, HUMAN_TIMEOUT_MS)
|
||||
|
||||
const choices = generateChoices(challenge)
|
||||
|
||||
pending.set(key, {
|
||||
fightId,
|
||||
botId,
|
||||
challenge,
|
||||
roundNumber,
|
||||
createdAt: Date.now(),
|
||||
choices,
|
||||
resolve: (response) => {
|
||||
clearTimeout(timeoutHandle)
|
||||
pending.delete(key)
|
||||
@@ -51,7 +255,7 @@ export function waitForHumanResponse(
|
||||
timeoutHandle,
|
||||
})
|
||||
|
||||
console.log(`[human] waiting for response: ${key} round=${roundNumber} type=${challenge.type}`)
|
||||
console.log(`[human] waiting: ${key} round=${roundNumber} type=${challenge.type} choices=${choices.length}`)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -64,7 +268,7 @@ export function submitHumanResponse(
|
||||
const key = `${fightId}:${botId}`
|
||||
const entry = pending.get(key)
|
||||
if (!entry) return false
|
||||
console.log(`[human] response received: ${key} answer=${answer.slice(0, 80)}`)
|
||||
console.log(`[human] response: ${key} answer=${answer.slice(0, 80)}`)
|
||||
entry.resolve({ answer: answer.slice(0, 2000), trashTalk: trashTalk?.slice(0, 200) })
|
||||
return true
|
||||
}
|
||||
@@ -80,6 +284,7 @@ export function getPendingChallenge(
|
||||
timeoutMs: number
|
||||
remainingMs: number
|
||||
scoring: string
|
||||
choices: string[]
|
||||
} | null {
|
||||
const key = `${fightId}:${botId}`
|
||||
const entry = pending.get(key)
|
||||
@@ -96,5 +301,6 @@ export function getPendingChallenge(
|
||||
timeoutMs: HUMAN_TIMEOUT_MS,
|
||||
remainingMs: remaining,
|
||||
scoring: entry.challenge.scoring,
|
||||
choices: entry.choices,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user