聊聊这些年我设计NBA Simulator的故事。
前言
作为一个NBA爱好者,小时候我一直梦想有一个模拟工具能时时刻刻模拟NBA比赛,让我能在任何时刻过足比赛瘾。上个月在AI的强力加持下,我终于拥有了一个这样的前端页面,理想初具雏形,以此有感而发记录一下它的前世今生。
1.0版本:计算器上的原型探索
中学时期同学们人手一个卡西欧计算器,其中的一个功能就是生成随机数。有一天我突然想到,如果将这个流程应用到一场篮球比赛中,将NBA比赛的每一个回合拆分成若干事件(选择球员,球员投篮,投篮命中,…),再用随机数的生成结果来对应这些事件的结果,就能预测一个回合了;由于每个回合会随机消耗一定时间,因此只要预测足够多的回合,就能模拟一场比赛。这个思路后来也延用至今,后续所有的NBA模拟延伸都是在这个基础上进行两个部分的打磨:事件的拆分以及随机数概率的调整。
彼时我还没有接触计算机编程,并且限于学业压力限制我也没有自动化的工具,因此很多时候就是靠着一台计算器、一张纸和一根笔人力完成了一场比赛的模拟。并且最初的事件拆分也比较原始,事件拆分只考虑了最基本的得分,随机数概率范围也基本只考虑了球员的综合能力。然而即便是这样,一场比赛的人力成本也相当之大,因为我需要用纸笔记录所有回合的得分情况,一晚上常常洋洋洒洒写满两三张草稿纸。现在想起那段时光,尽管模拟工作量巨大,但是它也有效缓解了学业压力,仿佛我在意念上摸鱼逃课看了N场球。
2.0版本:编程的最初尝试
大一接触了面向对象程序设计之后,我发现篮球比赛是面向对象编程的完美载体,因为球员、球队、比赛等等都可以抽象为对象和类,并且进行相应封装。因此在假期的时候我开始用C++实现比赛的模拟。然而彼时我对C++的使用并不熟练,代码写得相当冗余,最开始几天洋洋洒洒码了四五千行之后却只实现了一个非常简陋的版本(只能预测比赛结果,并且还因为C++相对复杂的内存管理,时不时遇到内存泄漏或者栈溢出),光是debug就花费了一周。然而自动化带来的提升是显而易见的,原来模拟一场比赛需要1-2天的人力劳动,现在只需要不足5秒。这也使我得以观测大量比赛的模拟结果,并且逐步尝试加入更多的回合事件。抢篮板、助攻、抢断、盖帽、犯规等等事件都是在这个阶段被陆续加入程序中的。
在这个过程中,我又产生了新的念头。彼时我常常用虎扑App看NBA比赛的文字直播,妙趣横生的解说词让观众相当具有代入感。有一天我意识到,我的模拟程序也完全可以加入这些解说词,这比起模拟结束简单生成一个结果要有趣得多。于是我从许多解说比赛里挑选了一些有意思的解说词,在回合模拟的间隙中通过控制台输出。这个改进也让我的模拟初步拥有了“实况”的感觉。许多解说词甚至保留到了今天的版本里。
3.0版本:日复一日地打磨
步入2020年,疫情时代来临,现实中的NBA比赛也遭受冲击被迫停赛,无球可看的我又掏出了我的模拟程序开始解闷。经过几年的专业学习,我自然而然地发现了诸多当年代码设计的缺陷,我意识到是时候推倒重构了。由于我当时常常在LeetCode上用Java刷题,因此重构时的语言选择上我也选择了用Java重写练手。
最初我的重构野心很小,仅仅只是用Java重写一个简练、精致的新版本。本身Java的语法相比C++会简练一些,再加上更合理的面向对象设计,我很快实现了这个目标。这时我又产生了新的念头 - 既然我能从逐个回合推演一场比赛,为什么我不能从逐场比赛推演一个赛季呢?这瞬间让这个程序更上一层楼 - 从单场比赛的胜者模拟蜕变为赛季冠军模拟。代码上的实现需要的成本也并不高,仅仅只需要找到NBA的赛程安排导出成文件,再根据NBA的赛制规则实现一个季后赛模式即可。这个改动让我立即获得了新的乐趣,因为看到模拟程序生成冠军明显比看到一场普通比赛的胜者更引人入胜。
然而赛季模式的引入也开始暴露出我程序的缺陷。此前的单场比赛模拟中,我并没有留意球员数据模拟结果的质量 - 从随机数角度来说,单场比赛样本太小,模拟出怎么样的结果都说得通。然而赛季模式下因为有了足够的样本,我发现许多球员的数据(得分、篮板、助攻等等)都和现实相差甚远,这也让我开始反思代码层面如何调整。尽管NBA官方并没有提供球员的量化能力评价,但我找到了2kratings这个替代品,它能实时获取最新的NBA 2K游戏球员能力值。虽然这个能力值来源于电子游戏,但毕竟也是游戏制作团队基于现实中球员的实时表现进行调整的,具有一定的专业参考性。我根据2kratings的数据打磨了我的程序,特别是在Player类中引入了相当多的球员能力,并且重新改进了各种Utility函数,让球员的各方面能力都可以影响到比赛进程,也大大提升了模拟的真实性。后来,我又发现了Basketball Reference这个神仙网站,它提供了大量的现实比赛结果供我参考。可以说Basketball Reference大大降低了我的调参难度,它提供了真实准确的标尺,我只需要让比赛的模拟结果尽可能接近这个标尺即可。此外,我也通过数学建模改进了许多事件模拟的函数,例如投篮距离和命中率的对应引入了指数函数,回合时间的消耗引入了Sigmoid函数等等。经过了这一系列的调整之后,生成的数据质量显著提升。
这一阶段我也进一步优化了比赛事件。最大的优化是引入了轮换系统,这个轮换系统会参考现实规律,结合了球员体力、负荷管理、实时手感等等的影响,让球员的总体上场时间更接近真实,也让更多替补球员有了上场发挥的机会。同时,我也尽可能引入了一些小概率事件,例如技术犯规/恶意犯规、伤病、挑战等等,让比赛更加跌宕起伏。比赛的解说词也更生动,增加了球员的反应功能,时不时能看到球员因为作出贡献而庆祝,又或者是犯错而沮丧。
另一个优化是比赛回顾相关的功能。此前的程序里面每把比赛都会生成一个对应的文本文件,单赛季总共1300多局比赛因此对应着1300多个结果文件,我根本不可能全部看完。在这个契机下我产生了做比赛回顾的念头,简要显示每局比赛输出分节比分、实时分差图,列举双方发挥的优异球员等等。这个改进大大提升了我浏览比赛的效率,同时也更接近现实中体育门户网站赛后回顾比赛的体验。我还将单赛季模拟拓展至多赛季模拟,用这种方式预测球队夺冠概率,进一步丰富了模拟程序的功能。
在经过了前后几年断断续续的打磨之后,这个版本最终被我封存在了https://github.com/MikeYan01/NBA-sim这个库里面。可以说这个版本是承上启下之作,它从一个不成熟的玩具变成了一个相对真实可靠的程序,并且囊括了绝大多数我想要的功能。未来我不会再更新这个代码库,但它对我而言可能是最具纪念意义的一个版本。
4.0版本:AI时代的如虎添翼
2024年开始,各路AI agent如雨后春笋般爆发,AI写代码已经成为了趋势。我的模拟器也在AI加持下实现了再次蜕变,从一个命令行程序变成了一个前端页面。
我的设想是利用AI将Java程序用TypeScript再次重构,并且加上可视化的网页UI。原本我以为重构对AI agent是小菜一碟,而网页UI才会花费较多精力,结果没想到重构的时候AI遇到了诸多幺蛾子,UI倒是几乎一步到位就画好了。这还是在我使用了Spec Kit这样的工具,按步骤、阶段为AI规划了重构任务的情况下。最主要的缺陷在于AI agent没有充分理解全部代码,在移植时总是会遗漏一部分原有的代码实现(例如在判断犯规的时候只考虑了防守犯规,没考虑进攻犯规等等),以致于需要反复人工检查校对,估计在现有agent技术下,上下文长度限制还是让AI难以一步到位完成重构。UI部分则令人惊艳,Claude给出的设计简洁美观,尤其是球员赛后数据Box score部分和现实中门户网站的UI设计相差无几;反而是前端界广受赞誉的Gemini 3 Pro设计的UI第一眼没有让我满意。不过总体而言整个移植过程还是比较顺利的,这让我对未来人类和AI共智的合作模式也有了更高的期待。
AI在内容创作上也大大提升了效率。此前我一直希望拓展解说语料库丰富比赛输出,AI不仅帮我拓展了诸多内容,甚至还顺带帮我抽象了相关数据库结构方便后续拓展。AI同时还提供了翻译支持,这让我的前端页面有机会被更多人使用。
这个版本目前被我存储在https://github.com/MikeYan01/NBA-Sim-Web这个库里,并且也部署到了https://mikeyan01.github.io/NBA-Sim-Web/页面上。至此,我终于可以随时随地,从任何设备上访问我的模拟程序,观看NBA模拟比赛了。
5.0版本:?
4.0版本几乎已经是我在中学时代的梦想了 - 随时随地,有一个可视化页面能够访问我的NBA模拟程序,并且能看到真实详尽的比赛解说与球队/球员数据。不过也必须要承认的是现阶段的版本仍然有诸多不足,例如解说词的堆叠有时比较生硬,以及部分复杂的比赛事件暂时还未实现(例如比赛末尾时的犯规战术等等)。更远一步畅想,现在的文字模拟版本在AI时代完全有可能拓展成图形甚至是视频版本,因此这十多年的版本更迭一定不是句号。希望我能一致保持这份初心,拥抱技术,让梦想继续进化。