☰
用蒙特卡洛模拟网约车司机生存账本:叙事新闻游戏开发实战
2026/10/17 18:35:41 网站建设 项目流程

简介:这是一款由英国《金融时报》制作的叙事性新闻游戏,围绕优步司机的经济处境与真实生活展开,玩家需在一周内尝试赚取1000美元,并在接受采访的真实司机面前做出抉择。资源面向对零工经济、交互叙事与新闻游戏开发感兴趣的读者,也适合作为JavaScript前端项目的学习案例。压缩包共78个文件,约517KB,以28个js脚本、11个html页面、9个json配置为主,辅以yml、ts、scss、md等,涵盖客户端组件、服务端逻辑、样式与构建配置,目录结构清晰。目前已有300人学习下载。通过该资源,读者可了解叙事游戏的状态管理与决策分支设计,参考真实采访素材的呈现方式,并借助npm或yarn快速安装运行,体验完整的开发与部署流程。

1. 当叙事遇上算法:一款新闻游戏如何把司机生存账本摊开

一款叫 uber-driver-game 的叙事性新闻游戏,把网约车司机的经济账和生活账做成了可交互的模拟系统。它不教你开车,也不复刻接单流程,而是把「跑一单到底赚多少、平台抽成怎么算、租车还是买车、每天跑几小时才够本」这些散落在论坛和访谈里的碎片,压缩成一套能反复试错的决策模型。目标读者是三类人:想用游戏做数据叙事的开发者、研究零工经济的产品经理、以及想给自己算一笔明白账的司机。核心问题只有一个——当平台规则变成代码,司机还有多少腾挪空间?这个标题背后不是娱乐产品,而是一套把收入结构、时间成本、车辆折旧和平台激励拆开揉碎的可视化沙盘。接下来我会按「先立模型、再跑仿真、最后看边界」的顺序,把这类新闻游戏的落地路径讲清楚。

2. 把司机的一天拆成状态机:收入模型怎么建才不飘

2.1 先定状态:在线、接单、空驶、休息四个格子

任何叙事性模拟的起点都是状态划分。网约车司机的一天看似连续,实际可以离散成四个互斥状态:在线待单、载客中、空驶寻客、离线休息。每个状态对应不同的时间消耗和现金流入流出。常见做法是用一个带时间步长的循环来推进,步长取 1 分钟,因为平台计费通常按分钟和公里双轨计算,1 分钟粒度足够还原大部分城市的计价规则。

状态转移不是随机的,而是由「当前区域供需比」「司机接单策略」「平台派单逻辑」三者共同决定。新手容易把接单写成纯概率,结果跑出来的收入曲线平滑得像教科书,完全不像真实司机那种「半小时没单、一来来三单」的脉冲感。我一般会引入一个区域热度系数,让接单概率随时间波动,波动周期参考早晚高峰和夜宵时段。

# 司机状态机核心循环(简化版) import random STATES = ["idle_online", "en_route", "occupied", "offline"] STEP_MINUTES = 1 SIM_HOURS = 12 def simulate_day(area_heat, base_fare, per_km, per_min, platform_cut): state = "idle_online" cash = 0.0 minutes_online = 0 for minute in range(SIM_HOURS * 60): if state == "offline": break minutes_online += 1 # 区域热度决定接单概率,热度高则等待短 if state == "idle_online": if random.random() < area_heat[minute // 60]: state = "en_route" elif state == "en_route": # 空驶 3~8 分钟到达上车点 if random.random() < 1 / random.randint(3, 8): state = "occupied" elif state == "occupied": # 载客 8~25 分钟,按里程和时长计费 trip_min = random.randint(8, 25) trip_km = trip_min * random.uniform(0.3, 0.6) fare = base_fare + per_km * trip_km + per_min * trip_min cash += fare * (1 - platform_cut) state = "idle_online" return cash, minutes_online

这段代码的逻辑说明:area_heat是一个长度 12 的列表,代表每小时区域热度,值越高接单越快;base_fare是起步价,per_km和per_min是里程费和时长费;platform_cut是平台抽成比例。参数怎么改:如果模拟的是租车司机,要在每日固定成本里加一笔租金;如果是自有车,则把折旧摊到每公里。失败时看什么:如果跑出来的时薪远高于当地司机访谈的中位数,大概率是空驶时间给太短,把en_route的等待分布调长即可。

2.2 成本项别漏:油电、折旧、保险、租金一个都不能少

收入模型跑通只是半张账本,另外半张是成本。很多模拟只扣平台抽成,结果司机时薪虚高 30% 以上。真实成本至少四块:能源费(油或电)、车辆折旧、保险与保养、以及租车司机的日租金。能源费按每公里单价乘总里程算,折旧按购车价除以预计总公里数摊,保险保养按年费除以年行驶天数摊。

这里有个容易翻车的地方:折旧不能按时间摊,要按里程摊。一辆车跑 10 万公里和跑 5 万公里,残值差一大截。我一般会设一个depreciation_per_km参数,默认 0.3 到 0.5 元每公里,具体看车型。租金则是租车司机每天睁眼就欠的钱,不管跑不跑都要扣,这个压力在模拟里要体现出来,否则叙事张力少一半。

# 成本计算模块 def daily_cost(total_km, is_rented, rent_per_day, energy_per_km, depreciation_per_km, insurance_per_day, maintenance_per_km): energy = total_km * energy_per_km depreciation = total_km * depreciation_per_km maintenance = total_km * maintenance_per_km fixed = insurance_per_day + (rent_per_day if is_rented else 0) return energy + depreciation + maintenance + fixed

参数说明:energy_per_km电车约 0.1 到 0.2 元,油车约 0.5 到 0.7 元;depreciation_per_km经济型车约 0.3 元,中档车约 0.5 元;insurance_per_day按年保费除以 300 天估算;maintenance_per_km含轮胎和保养,约 0.1 元。把这些加总后从收入里扣,得到的才是净现金流。如果净现金流为负,模拟里司机应该有权选择收车,这个退出机制是叙事游戏的关键分支。

2.3 平台激励要当变量而不是常量

平台冲单奖、高峰溢价、连续在线奖励,这些在真实司机收入里占比不小,但规则经常变。模拟里不能写死,要抽成可配置的激励规则表。常见做法是定义一个incentive_rules列表,每条规则包含触发条件(如完成单数、在线时长、时段)和奖励金额。跑仿真时逐条检查是否满足,满足则累加。

这样做的价值在于:你可以快速对比「有奖励」和「没奖励」两种世界线下司机的时薪差异,也能模拟奖励门槛提高后司机需要多跑多少单才能拿到。参数怎么设:冲单奖通常分三档,比如 10 单奖 15 元、20 单奖 40 元、30 单奖 80 元,门槛越高边际收益越大,但司机为了冲最后一档往往要额外空驶,这部分成本要算进去。

3. 用蒙特卡洛跑一千天:把「运气」从收入里剥离出来

3.1 单次模拟不够,要跑分布

司机收入波动极大,同一天同城市两个人可能差一倍。单次模拟只能告诉你「可能发生什么」,跑一千次才能告诉你「大概率发生什么」。蒙特卡洛方法在这里不是炫技,而是刚需。把接单间隔、行程长度、区域热度都设成随机变量,每次模拟抽一组参数,跑完一千天得到净收入的分布。

import numpy as np def monte_carlo(runs=1000): results = [] for _ in range(runs): heat = [random.uniform(0.3, 0.9) for _ in range(12)] cash, mins = simulate_day(heat, base_fare=8, per_km=2.2, per_min=0.5, platform_cut=0.25) km = mins * random.uniform(0.2, 0.4) cost = daily_cost(km, is_rented=True, rent_per_day=120, energy_per_km=0.15, depreciation_per_km=0.35, insurance_per_day=15, maintenance_per_km=0.1) results.append(cash - cost) return np.percentile(results, [10, 50, 90])

逻辑说明:np.percentile取 10%、50%、90% 分位,分别代表倒霉日、普通日、幸运日。参数怎么改:如果想知道电车和油车的差异,把energy_per_km从 0.15 改成 0.6 再跑一遍。失败时看什么:如果 90 分位和 10 分位差距超过三倍,说明随机变量范围设太宽,检查heat的上下限是否合理。

3.2 敏感性分析:哪个参数最影响时薪

跑完分布后,下一步是找出哪个参数对净收入影响最大。做法很简单:固定其他参数,只让一个参数在合理范围内变动,观察时薪变化幅度。常见结论是:平台抽成和空驶率影响最大,能源单价影响中等,保险保养影响较小。这个排序直接指导叙事重点——游戏里应该让玩家优先感受到抽成和空驶的压力。

参数变动范围时薪变化幅度
平台抽成15%~30%高
空驶时间占比20%~50%高
能源单价0.1~0.7 元/公里中
日租金80~180 元中
保险保养10~30 元/天低

表格里的结论来自多次仿真对比,不是拍脑袋。做叙事游戏时,把高影响参数做成玩家可调选项,低影响参数收进高级设置,界面才不会劝退。

3.3 把结果映射回叙事:数字要能讲出人的选择

模拟跑出来的分布不能直接甩给玩家看,要转译成叙事语言。比如 10 分位净收入 80 元,对应「今天白跑还倒贴油钱」;50 分位 220 元,对应「够吃饭交租但攒不下」;90 分位 450 元,对应「今天运气好,但明天呢」。每个分位配一段司机独白或短信对话,玩家看到的就不是冷冰冰的百分位,而是活生生的处境。

这一步的技术含量不在代码,在文案和数值的对应关系。我一般会先跑出分布,再按分位切五档,每档写三到五条短叙事,随机抽一条展示。注意别把高收入档写成「人生赢家」,真实司机的高收入日往往伴随超长在线和极端疲劳,叙事里要保留这个代价。

4. 避坑与排查:这类新闻游戏最容易翻车的五个地方

4.1 收入虚高:空驶时间被吃掉了

现象:模拟时薪跑到 40 元以上,远超司机访谈里的 20 到 30 元。原因:空驶等待时间设太短,或者接单概率设太高。解决:把en_route状态的等待分布调长,早晚高峰外区域热度降到 0.3 以下,并加入「派单距离超过 3 公里司机可能拒单」的逻辑,拒单后重新等待。

4.2 成本漏项:只扣抽成不扣折旧

现象:净收入比司机实际到手高出一大截。原因:折旧、保险、保养没摊进去,或者只算了油费。解决:按里程摊折旧,按天摊保险,按公里摊保养,租车司机加日租金。跑一遍成本模块,确保总成本占流水比例在 55% 到 75% 之间,低于这个区间说明漏项。

4.3 随机性失控:每天收入像过山车

现象:连续模拟十天,最高和最低差五倍。原因:随机变量范围设太宽,或者没有引入均值回归。解决:给区域热度加一个基础值,随机波动限制在基础值上下 30% 以内;行程长度用对数正态分布而不是均匀分布,避免极端值频繁出现。

4.4 激励规则写死:改一个数要动十行代码

现象:想调整冲单奖门槛,发现规则散落在各处。原因:没有把激励抽成独立配置。解决:用列表存规则,每条规则一个字典,包含条件类型、阈值、奖励金额。主循环只负责遍历规则并累加,改参数只动列表不动逻辑。

4.5 叙事和数值脱节:玩家感觉不到数字背后的压力

现象:模拟结果准确,但玩家看完没感觉。原因:只展示数字,没有把数字翻译成生活场景。解决:按分位切档配文案,把「净收入 80 元」写成「今天跑完,油钱刚够,晚饭还没着落」。数值是骨架,叙事是血肉,缺一个都立不住。

5. 进阶技巧:用反事实模拟让玩家看见「如果」

5.1 反事实分支:同一司机,不同规则

这类新闻游戏最有价值的玩法不是「跑一天赚多少」,而是「如果平台抽成降五个点,司机的日子会怎样」。实现方式是在同一组随机种子下跑两次模拟,只改一个参数,把两条净收入曲线叠在同一张图上。玩家拖动滑块调整抽成比例,曲线实时变化,视觉冲击比任何文字都有力。

def counterfactual(seed, cut_a, cut_b): random.seed(seed) heat = [random.uniform(0.3, 0.9) for _ in range(12)] cash_a, mins_a = simulate_day(heat, 8, 2.2, 0.5, cut_a) random.seed(seed) cash_b, mins_b = simulate_day(heat, 8, 2.2, 0.5, cut_b) return cash_a, cash_b

关键点是random.seed(seed)要重置两次,保证两次模拟面对完全相同的「运气」,唯一变量是抽成比例。这样对比才干净,否则玩家会质疑「是不是第二次运气好」。参数怎么设:cut_a设当前平台抽成,cut_b设假设值,差值建议在 5 到 15 个百分点之间,太小看不出差异,太大不真实。

5.2 时间维度拉长:从一天到一年

单日模拟只能反映波动,拉长到一年才能看出累积效应。把每日净收入按顺序累加,画一条年度现金流曲线,再叠加车辆大保养、保险续费、年检这些年度支出节点。玩家会直观看到:某些月份净收入为正,但一到续保月就转负。这种「锯齿形」现金流是零工经济的典型特征,也是叙事的高光点。

实现上,把每日模拟包一层年度循环,维护一个cash_balance变量,每天加上净收入,遇到年度支出节点就扣一笔。如果cash_balance低于某个阈值,触发「司机考虑退出」的叙事分支。阈值设多少:一般设一个月租金或两个月保险,低于这个数司机心理压力会急剧上升。

5.3 验证方法:拿真实访谈数据做锚点

模拟再漂亮,如果和真实司机感受对不上就是自嗨。验证方法很朴素:找三到五个司机访谈或公开的收支记录,把他们的月流水、总里程、在线时长、净收入填进表格,再用模拟跑同样的条件,对比差异。如果模拟净收入比真实数据高 20% 以上,回头查成本项;如果低 20% 以上,查接单概率和激励规则。

我自己的习惯是先把锚点数据固定下来,再调参数去逼近,而不是反过来。锚点数据不用多,三五个案例就能框住合理范围。超过范围就说明模型有结构性偏差,不是调参能解决的,得回去改状态机或成本结构。

5.4 一个具体技巧:把「疲劳」做成隐性成本

真实司机跑久了会累,累了接单效率下降、事故风险上升。模拟里可以加一个fatigue变量,随在线时长累积,超过阈值后接单概率下降、空驶时间变长。这个变量不直接扣钱,但会拉低时薪,玩家能感受到「硬撑不划算」。参数怎么设:连续在线 4 小时后开始累积,每多一小时接单概率降 5%,降到 70% 为止。这个技巧让模拟从「算账工具」变成「体验工具」,是叙事游戏和普通计算器的分水岭。

我做了几轮这类模拟后最大的教训是:别急着写代码,先把成本项和状态转移在白板上画清楚,漏一个成本项后面所有数字都是错的,返工比重写还难受。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询