显示HN:等待LLM很糟糕——给你的用户一个游戏
Show HN: Waiting for LLMs Suck – Give your user a game

原始链接: https://github.com/ftaip/waiting-game

## React-Waiting-Game:加载时分心小游戏 `react-waiting-game` 是一个轻量级、一键式街机组件,旨在在长时间运行的任务(如LLM处理或文件上传)期间娱乐用户。它包含五款独特的、复古风格的1位像素艺术游戏:水母漂移、像素跑酷、重力翻转、入侵者和节奏点击。 每款游戏都共享一个通用框架,包括连击倍增、道具、高分和五个可解锁成就。输入方式统一——单个按钮控制所有游戏——并且该组件支持服务器端渲染,并在标签页不活动时自动暂停。 自定义选项包括颜色着色、通过`game`属性选择游戏,以及可选的用于存储分数和成就的持久化存储。它与原始的``组件向后兼容。开发者可以通过遵循定义的模块结构创建新游戏来轻松扩展该库。 该包可通过npm安装 (`npm install react-waiting-game`),并包含全面的测试和文档,以及一个演示其功能的实时示例。

Hacker News 上一个新项目 (github.com/ftaip) 提出通过为用户提供一个简单的游戏来消磨等待时间,以此来对抗 LLM 的等待时间。这个想法获得了积极的初步反馈,用户要求演示。 一位用户成功利用 LLM 驱动的编码代理,从屏幕录制中创建了一个演示视频,突出了其在编辑和文档等任务中的易用性。另一位用户正在积极将他们的个人网页转换为《弹球》风格的游戏。 讨论涉及了人工智能时代人们日益增长的即时满足感需求(“追逐多巴胺”),并包含了一个 YC Summer 2026 申请的呼吁。核心要点是专注于通过引人入胜的干扰来改善 LLM 处理过程中的用户体验。
相关文章

原文

A tiny one-button mini-arcade for filling the void while a long task runs (LLMs, builds, uploads, you name it). Every game is monocolor, pure 1-bit pixel art, single canvas, zero runtime dependencies, and shares the same combo / power-up / high-score / achievement framework.

  • Pick a game with a single prop: <WaitingArcade game="runner" />
  • One button: keyboard, pointer, and touch all map to the same primary action
  • Tints to any colour via the color prop (defaults to currentColor)
  • SSR-safe; auto-pauses when the tab is hidden
  • Optional localStorage-backed best score and achievements, namespaced per game
  • Same component still ships as <WaitingGame /> for backward compatibility
Game id Mechanic Skins
Jellyfish Drift jellyfish Hold to swim up, release to sink. Avoid coral & stalactites. jellyfish, octopus, paperBoat
Pixel Runner runner Tap to jump, hold for higher jumps. Hop cacti, dodge birds. dino, ninja, frog
Gravity Flip gravity Tap to invert gravity. Arc between floor and ceiling, dodge spikes. cube, triangle, diamond
Invaders invaders Auto-fires bullets to the right. Tap to swap lane — be in the alien's lane to shoot it, out of it when it arrives. ship, fighter, saucer
Rhythm Tap rhythm Notes scroll into a hit zone. Tap on the beat for short notes, hold for long ones. 3 lives. bar, dot, arrow

All five games share the same set of features: combo multiplier, near-miss bonus (or its game-specific equivalent), milestone flashes, tier ramp, screen shake, parallax background, three power-ups, and a five-achievement set. Death model varies: most games are one-hit, rhythm uses 3 lives.

npm install react-waiting-game
import { WaitingArcade } from 'react-waiting-game';

function LoadingScreen() {
  return <WaitingArcade game="runner" autoStart />;
}

Use it while waiting on an LLM

import { useState } from 'react';
import { WaitingArcade } from 'react-waiting-game';

function Chat() {
  const [loading, setLoading] = useState(false);

  async function ask() {
    setLoading(true);
    await fetch('/api/chat', { method: 'POST', body: '...' });
    setLoading(false);
  }

  return (
    <div>
      <button onClick={ask} disabled={loading}>Ask</button>
      {loading && (
        <WaitingArcade
          game="runner"
          autoStart
          persistHighScore
          persistAchievements
        />
      )}
    </div>
  );
}

Every game uses the same single-button input.

Input Action
Hold Space / Arrow Up / W / Touch Primary action (game-specific)
Release Stop
  • Jellyfish — hold to thrust upward, release to sink under gravity. Walls catch you gently; only obstacles end the run.
  • Runner — tap to jump, hold to jump higher (variable height). Land before the next obstacle.
  • Gravity — tap to flip gravity. The player accelerates toward the active surface; flip mid-arc to thread between floor and ceiling spikes.
  • Invaders — your turret auto-fires bullets to the right at a fixed cadence. Tap to swap between the upper and lower lane. Aliens that escape past you break your combo; aliens that touch you while in your lane end the run.
  • Rhythm — short tap notes scroll right-to-left into the hit zone; tap when one is centred. Long notes are hold notes — keep the button down while the bar passes through the cursor and release at the end. Missing a note, breaking a hold, or false-tapping costs one of your three lives.
Prop Type Default Description
game 'jellyfish' | 'runner' | 'gravity' | 'invaders' | 'rhythm' 'jellyfish' Which mini-game to render
width number 600 Canvas width in px
height number 150 Canvas height in px
color string 'currentColor' Single colour for everything
paused boolean false Pause externally (e.g. when the LLM responds)
autoStart boolean false Skip the "tap to start" prompt
skin string game default Skin id; must be valid for the selected game
persistHighScore boolean false Store best score in localStorage, namespaced per game
storageKey string 'waiting-arcade:hi:<game>' Override key for the best score
persistAchievements boolean false Store unlocked achievements in localStorage, namespaced per game
achievementsStorageKey string 'waiting-arcade:ach:<game>' Override key for achievements
onScoreChange (score, hi) => void Fired when the score changes
onGameOver (score) => void Fired when the player dies
onComboChange (combo, mult) => void Fired when the multiplier changes
onPickup (total) => void Fired when pearls/coins are collected
onAchievement (id) => void Fired when a new achievement is unlocked
className / style / aria-label Standard wrapper props

Every game unlocks five achievements per run.

Jellyfish (jellyfish)

ID How to earn
century Reach 100 in a single run
half_grand Reach 500 in a single run
survivor Stay alive ~60 s
pearl_diver Collect 10 pearls in a single run
untouchable Pull off 5 near-misses

Runner (runner)

ID How to earn
runner_century Reach 100 in a single run
runner_half_grand Reach 500 in a single run
runner_survivor Stay alive ~60 s
runner_coin_hoarder Collect 10 coins in a single run
runner_dodger Pull off 5 near-misses

Gravity (gravity)

ID How to earn
gravity_century Reach 100 in a single run
gravity_half_grand Reach 500 in a single run
gravity_survivor Stay alive ~60 s
gravity_collector Collect 10 coins in a single run
gravity_dodger Pull off 5 near-misses

Invaders (invaders)

ID How to earn
invaders_century Reach 100 in a single run
invaders_half_grand Reach 500 in a single run
invaders_survivor Stay alive ~60 s
invaders_sharpshooter Shoot down 10 aliens in a single run
invaders_combo_master Pull off 5 close-range kills in a single run

Rhythm (rhythm)

ID How to earn
rhythm_century Reach 100 in a single run
rhythm_half_grand Reach 500 in a single run
rhythm_survivor Stay alive ~60 s
rhythm_virtuoso Hit 25 notes in a single run
rhythm_perfectionist Land 5 perfect-timing hits in a single run
<WaitingArcade game="jellyfish" skin="octopus"  />
<WaitingArcade game="runner"    skin="ninja"    />
<WaitingArcade game="gravity"   skin="triangle" />
<WaitingArcade game="invaders"  skin="saucer"   />
<WaitingArcade game="rhythm"    skin="dot"      />

Use GAMES[game].skins for the full list at runtime, or import the per-game constants:

import {
  SKIN_IDS,
  RUNNER_SKIN_IDS,
  GRAVITY_SKIN_IDS,
  INVADERS_SKIN_IDS,
  RHYTHM_SKIN_IDS,
} from 'react-waiting-game';

Backward compatibility — <WaitingGame />

The original <WaitingGame /> jellyfish-only component still ships unchanged:

import { WaitingGame } from 'react-waiting-game';

<WaitingGame autoStart skin="paperBoat" persistHighScore />

It uses the original storage key waiting-game:hi, so existing high scores carry over. New projects should prefer <WaitingArcade game="jellyfish" />.

Each game is just a GameModule registered in src/games/index.ts:

import type { GameModule } from 'react-waiting-game';

export const myGame: GameModule<MyState, MySkin, MyAch> = {
  id: 'mygame',
  defaultWidth: 600,
  defaultHeight: 150,
  skins: ['default'],
  defaultSkin: 'default',
  achievements: [...],
  init, tick, draw,
  selectScore, selectHiScore, selectPhase,
  selectScreenShake, selectDeathFlash,
  selectAchievements, selectNewAchievements,
  idlePrompt: 'Tap to start',
  deadPrompt: 'Press to retry',
};

The shared useGameLoop, input bus, persistence helpers, pixel/digit drawing, screen-shake decay, and feedback types all live under src/shared/.

npm install
npm test          # 151 unit tests across all five game engines
npm run lint      # tsc --noEmit
npm run build     # tsup → dist/ (ESM + CJS + .d.ts)
cd example
npm install
npm run dev

The example simulates an LLM call, lets you switch between games and skins live, shows combo/pickup callbacks, and lists unlocked achievements.

MIT

联系我们 contact @ memedata.com