Simon Willison · 博客

用Claude Fable 5一次通关浣熊抢劫游戏

One-shotting a Raccoon Heist game using Claude Fable 5

二〇二六年八月五日 · 英文原文

2024年,Simon Willison在推特发布GPT-3生成游戏概念截图四周年之际,用Claude Fable 5(运行于Claude Code for web)仅凭该推文内容构建出完整3D游戏《浣熊大劫案》。游戏使用Three.js构建,通过gpt-image-2生成纹理,支持移动端触摸控制,包含警卫、猎犬等机制。项目托管于GitHub Pages,共7次提交,全部通过Playwright自动化测试验证。

2024年,我在推特上发布了由GPT-3生成的一个游戏概念截图,以及用DALL-E创作的一些概念“艺术”。今天,在那条推文发布四周年之际,我决定看看Claude Fable 5(在Claude Code for web中运行)能否仅凭那条推文的内容构建出整个游戏。它干得相当不错!你可以在这里试玩这个游戏。这是GitHub仓库,还有一段短视频演示:你的浏览器不支持HTML5视频。

我是如何构建这个的

这是2022年8月5日的推文:

我当时给GPT-3的prompt是:为一款浣熊团队实施抢劫的电脑游戏撰写详细的产品描述。那还是文本补全的时代,你让GPT-3补全剩余文本。它补全道:

在《浣熊大劫案》中,你和你的偷窃浣熊团队的任务是实施一系列大胆的抢劫。从抢银行到偷 priceless 艺术品,对你的毛茸茸团队来说,没有太大或太小的活儿。你需要运用智慧和技能来躲避警察,并带着赃物干净利落地逃脱。凭借激动人心的玩法和迷人的角色阵容,《浣熊大劫案》是任何寻求轻松诙谐冒险的人的完美游戏。

我给DALL-E的prompt很短:一款浣熊团队实施抢劫的视频游戏截图

今天的实验:我能否把这些截图连同一条指示它编写游戏的prompt一起扔给Fable 5,然后放手让它自己干,最后得到一个能玩的游戏?

设置Claude Code for web以使用GitHub Pages

Claude Code for web一个令人沮丧的地方是,当它还在工作时,很难测试它正在构建的内容。我一直在用GitHub Pages来绕过这个限制,发现效果非常好。我的流程如下:

https://github.com/new 为项目创建一个新仓库——可以是公开或私有的,这个技巧对两者同样有效。 在Claude iPhone或桌面应用中,或在浏览器访问https://claude.ai/code 启动一个Claude Code for web会话。 告诉Claude要做什么,并鼓励它尽快提交一个index.html页面。这会创建一个类似claude/3d-raccoon-heist-game-50n293 的分支。 导航到仓库的Settings -> Pages区域(在我的情况下是github.com/simonw/raccoon-heist/settings/pages),选择“Deploy from a branch”,选择分支名称,然后点击Save。 就这么简单!每次推送后大约30秒内,最新内容就会出现在yourname.github.io/your-repo/ 。如果你用私有仓库这样做,任何能猜到仓库名称的人都能看到发布的内容。我自己对此不太担心。

Fable 5的prompt

这是我给Fable 5的prompt(写在手机上的备忘录应用里——整个项目都是在移动端完成的)。我附上了原始推文中的两张图片。

为浏览器构建这个3D游戏。此仓库配置为提供静态文件,所以确保有一个index.html能加载所有其他内容。确保它适合移动端(触摸控制,在小屏幕上运行良好)。你有一个OpenAI API密钥,可以访问他们的图像生成模型API,用那个来为你的3D模型生成纹理。文档在这里:https://developers.openai.com/api/docs/guides/image-generation - 使用gpt-image-2。独立工作——不要让我做任何进一步的设计决策。确保游戏有趣、有点惊喜、有良好的浣熊抢劫氛围,并且视觉上令人愉悦。尽可能频繁地提交和推送,以便我能预览你的工作——先从一个显示标题画面的index.html开始,然后在此基础上构建。工作时追加到notes.md文件,并将对该文件的更改作为每次提交的一部分。

我没有做任何技术选择。我(正确地)假设根据之前的实验,它可能会用Three.js。给Claude一个OpenAI密钥,结果证明在填补其能力空白方面非常有效——在这种情况下,我们需要某种方式来生成用作纹理的图像。Fable非常擅长给图像生成器写prompt!

我说“独立工作——不要让我做任何进一步的设计决策”,因为我想看看它能否在没有任何我进一步输入的情况下,产出一个完整可玩的游戏。我还说“尽可能频繁地提交和推送,以便我能预览你的工作”。当你在Claude iPhone应用中使用Claude Code时,你给它一个GitHub仓库,它会在一个分支上工作。告诉它“尽可能频繁地推送”意味着提交会立即开始落到那个分支上。

我喜欢要求notes.md作为一点额外的风味——这是完成的文件,以及它添加狗时的那条记录:

新的升级:从第3晚开始,院子里会有一只巡逻的警卫犬——一只低多边形棕色猎犬,戴着带尖刺的红色项圈,尾巴摇摆。它在随机地点之间游荡,在12个单位内它会闻到你的气味并通过嗅觉追踪你(视线无关——全靠鼻子,头顶显示👃并吠叫)。如果你拉开17个单位的距离,它会放弃。被抓到的消息现在按来源区分:警卫/车灯/猎犬。用自动化测试验证了游荡→追踪→被抓的流程。

查看transcript

你可以访问Claude Code共享会话,我还用我的claude-code-transcripts工具导出了我自己的HTML版本,你可以在这里找到。

Fable从一个索引页面开始,内置了一份Three.js副本,然后编写了自己的gen_textures.py脚本(副本在这里)。它生成了纹理并抽查以确保它们看起来没问题。它为垃圾桶生成的metal.jpg文件看起来像这样,尽管我认为它在游戏中的应用并不完全正确:

然后它构建了游戏的第一版基础版本,接着决定用Playwright“在预装的Chromium中进行冒烟测试”。这意味着它可以截取自己工作的截图并目测检查。它对页面的桌面和移动宽度都做了这个,然后注意到浣熊在移动宽度下不可见,于是修复了这个问题:

浣熊、垃圾箱藏身处和两个队员浣熊现在在移动端完全可见。提交这个关键修复。

它决定生成一个标题画面,用这个gen_title.py脚本完成。这是它使用的gpt-image-2 prompt:

视频游戏主视觉,低多边形3D渲染风格,情绪化夜景:一只可爱的低多边形浣熊戴着黑色小盗贼面具,用后腿偷偷摸摸地走,拿着一枚发光的金币,旁边是一个翻倒的金属垃圾桶,背景是带温暖发光窗户的郊区房屋,深蓝色夜晚,满月,萤火虫,电影感轮廓光,迷人的抢劫冒险氛围。无文字,无单词,无标志。

生成的图像(Claude认为它“华丽”)——不过我注意到在桌面上显示时,它被裁剪到只有顶部三分之一,浣熊不见了!

然后是我最喜欢的改动:它添加了狗:

export function makeDog ( ) { const g = new THREE . Group ( ) ; const BROWN = 0x8a6440 , DARK = 0x5e4128 ; const body = new THREE . Mesh ( new THREE . SphereGeometry ( 0.42 , 10 , 8 ) , M ( BROWN ) ) ; body . scale . set ( 0.9 , 0.8 , 1.5 ) ; body . position . y = 0.55 ; body . castShadow = true ; g . add ( body ) ; const head = new THREE . Mesh ( new THREE . SphereGeometry ( 0.3 , 10 , 8 ) , M ( BROWN ) ) ; head . position . set ( 0 , 0.85 , 0.62 ) ; g . add ( head ) ; const snout = new THREE . Mesh ( new THREE . SphereGeometry ( 0.16 , 8 , 6 ) , M ( DARK ) ) ; snout . scale . set ( 0.9 , 0.7 , 1.3 ) ; snout . position . set ( 0 , 0.76 , 0.9 ) ; g . add ( snout ) ; const nose = new THREE . Mesh ( new THREE . SphereGeometry ( 0.06 , 6 , 6 ) , M ( BLACK ) ) ; nose . position . set ( 0 , 0.78 , 1.08 ) ; g . add ( nose ) ; for ( const s of [ - 1 , 1 ] ) { const ear = new THREE . Mesh ( new THREE . SphereGeometry ( 0.12 , 6 , 6 ) , M ( DARK ) ) ; ear . scale . set ( 0.7 , 1.3 , 0.5 ) ; ear . position . set ( 0.2 * s , 1.08 , 0.55 ) ; g . add ( ear ) ; const eye = new THREE . Mesh ( new THREE . SphereGeometry ( 0.05 , 6 , 6 ) , M ( 0x1a1a1a , { emissive : 0x331111 } ) ) ; eye . position . set ( 0.13 * s , 0.92 , 0.86 ) ; g . add ( eye ) ; } const tail = new THREE . Mesh ( new THREE . CylinderGeometry ( 0.05 , 0.09 , 0.5 , 6 ) , M ( DARK ) ) ; tail . position . set ( 0 , 0.8 , - 0.62 ) ; tail . rotation . x = 0.8 ; g . add ( tail ) ; // spiked collar const collar = new THREE . Mesh ( new THREE . TorusGeometry ( 0.22 , 0.05 , 6 , 12 ) , M ( 0xc0392b ) ) ; collar . position . set ( 0 , 0.78 , 0.5 ) ; collar . rotation . x = Math . PI / 2.4 ; g . add ( collar ) ; const legGeo = new THREE . CylinderGeometry ( 0.07 , 0.09 , 0.34 , 6 ) ; const legs = [ ] ; for ( const [ x , z ] of [ [ - 0.22 , 0.35 ] , [ 0.22 , 0.35 ] , [ - 0.22 , - 0.35 ] , [ 0.22 , - 0.35 ] ] ) { const leg = new THREE . Mesh ( legGeo , M ( DARK ) ) ; leg . position . set ( x , 0.17 , z ) ; g . add ( leg ) ; legs . push ( leg ) ; } let phase = Math . random ( ) * 10 ; return { group : g , animate ( dt , speed ) { phase += dt * ( 3 + speed * 10 ) ; const amp = Math . min ( 0.6 , 0.1 + speed * 0.6 ) ; legs [ 0 ] . rotation . x = Math . sin ( phase ) * amp ; legs [ 3 ] . rotation . x = Math . sin ( phase ) * amp ; legs [ 1 ] . rotation . x = - Math . sin ( phase ) * amp ; legs [ 2 ] . rotation . x = - Math . sin ( phase ) * amp ; tail . rotation . z = Math . sin ( phase * 1.5 ) * 0.4 ; body . position . y = 0.55 + Math . abs ( Math . sin ( phase ) ) * 0.04 * ( 0.3 + speed ) ; } , } ; }

并用Playwright对它做了一轮测试,包括另一张截图。

// walk near the dog await page . evaluate ( ( ) => { const d = window . __rh . dog ; window . __rh . teleport ( d . x + 6 , d . z ) ; } ) ; await page . waitForTimeout ( 2000 ) ; info = await page . evaluate ( ( ) => JSON . stringify ( { dog : window . __rh . dog , state : window . __rh . state , player : window . __rh . debug ( ) . player } ) ) ; console . log ( 'after approach:' , info ) ; await page . waitForTimeout ( 3000 ) ; info = await page . evaluate ( ( ) => JSON . stringify ( { dog : window . __rh . dog , state : window . __rh . state } ) ) ; console . log ( 'after chase:' , info ) ; await page . screenshot ( { path : __dirname + '/shot-dog.png' } ) ;

它又做了一些调整,然后宣布自己完成:

《浣熊大劫案》已构建、测试并推送——在claude/3d-raccoon-heist-game-50n293 分支上有7次提交,全部通过Playwright在桌面、竖屏手机和横屏手机视口上的自动化试玩验证。游戏内容:你是一只戴着面具的浣熊,从团队的垃圾箱藏身处溜出来,进入一个月光下的死胡同。收集金币、宝石、戒指、甜甜圈和鱼(它们会明显堆在你的背上并减慢你的速度),然后在黎明前蹒跚回家存入银行。每晚都会提高目标和热度:第1晚有一个手电筒巡逻警卫,第2晚有两个,第3晚增加了一只完全无视视线的气味追踪猎犬。一辆警车用前灯扫过街道,房主会随机从窗户往外看,一只海鸥会定期俯冲轰炸你未认领的战利品,披萨片会触发6秒的速度狂飙,而一台稀有的金色电视如果能拖回家值120分。胜利等级:TRASH PANDA ⭐ → CAT BURGLAR ⭐⭐ → MASTER OF DISGUISE ⭐⭐⭐,你的最佳成绩会保存到localStorage。构建方式:内置Three.js(无CDN)、程序化低多边形角色,以及程序化WebAudio配乐(鬼祟的行走贝斯爵士乐加所有音效——零音频文件)。所有七种地面/墙壁纹理和标题画面主视觉都用gpt-image-2生成并作为静态资源提交,所以部署后的游戏不进行任何API调用。移动端有动态触摸摇杆(出现在你触摸的任何位置)加DASH按钮;桌面端用WASD/方向键和空格。测试发现了两个值得一提的真实bug:画布在任何真实手机上以2倍大小渲染(一个cssText赋值覆盖了Three.js的内联尺寸——桌面DPR-1测试完全掩盖了这一点),以及胜利画面的星级评分div继承了标题画面的全屏.stars CSS,静默吞掉了“下一晚”按钮上的每次点击。两者都已修复并进行了回归测试,连同拾取、存入、追逐/被抓、黎明、夜晚推进以及失败/重试流程。notes.md有完整的构建日志。游戏在分支上已上线——只需服务仓库根目录并打开index.html。

这个游戏好玩吗?

这是一个令人印象深刻的起点,但不是一个好游戏。你控制浣熊在院子里移动收集物品——甜甜圈、鱼、金币、珠宝——同时躲避带手电筒的警卫,在后面的关卡中还要躲避狗。你有有限的携带容量,一旦满了就需要把东西放到垃圾箱。如果你捡起披萨片,会获得临时速度提升。完全没有团队机制——垃圾箱旁边有两只静态浣熊,但它们纯粹是装饰。随着关卡推进,难度略有增加——第3关引入的狗是最有趣的新机制——但通关非常非常容易。它也很无聊——每晚有固定时长,你可以收集所有物品,然后在等待黎明时无事可做。

我对实现印象深刻。它完全是3D的,有垃圾桶,手电筒照明锥很有趣,而且有相当连贯的视觉风格。它在移动端也能运行。音乐(根据Claude的说法,“程序化WebAudio配乐(鬼祟的行走贝斯爵士乐加所有音效——零音频文件)”)很简单,但感觉恰到好处。

作为一个完成的游戏项目,它很平庸。作为一个从单一prompt出发的起点,我认为它非常令人印象深刻。

我现在已经用vibe coding构建了不少游戏。它们从游戏玩法角度来看都令人深感失望——事实证明,设计有趣的游戏仍然是一种独特的人类特质,而且需要比Claude或我所能带来的多得多的技能和经验。话虽如此,我强烈推荐尝试游戏开发项目作为探索agent能力的一种方式。这是一种有趣、低风险的方式来尝试新事物。如果你坚持足够久,甚至可能做出值得一玩的东西!

标签:game-design、ai、prompt-engineering、generative-ai、llms、anthropic、claude、text-to-image、vibe-coding、coding-agents、claude-mythos-fable

译自 Simon Willison · 博客 · 录于 二〇二六年八月五日