恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

用JavaScript拆解零工经济账本:叙事新闻游戏开发实战

  • 首页
  • 资讯中心
  • /
  • 用JavaScript拆解零工经济账本:叙事新闻游戏开发实战

相关资讯

ClawX v0.1.23:OpenClaw图形化填坑版,20分钟跑通本地大模型 2026/10/10 16:51:06
LAS点云训练PointNet分类:从数据预处理到模型调优全流程指南 2026/10/10 16:51:05
Dify私有化部署实战:Linux服务器+Docker Compose指南 2026/10/10 16:51:05

最新资讯

MapViewer:用C#解析链接器Map文件,定位固件体积膨胀源头
剑指Offer源代码C++:工程化重写,把算法题变成面试手撕代码训练
自用代码demo管理指南:从零散实验到可复用知识资产
Linux内核学习指南:建立心智模型,掌握五大核心子系统
用MkDocs与Swagger构建内部工具文档自动化流水线
基于NEU-DET数据集与YOLOv5的钢材表面缺陷检测实战:从训练到RK3568边缘部署

今日推荐

Codex 总用英文回答?从 AGENTS.md 到 config.toml 的中文输出调优指南
OpenClaw 自定义插件开发完整指南(2026最新版):从 TypeScript 到 npm 发布
基于Spark的电影推荐系统全链路实战:从爬虫到Web展示

本周热门

MR25H40CDF + PIC18F65K40:工业记录仪高可靠存储实战
基于STM32的数控恒压恒流电源设计:从硬件到PID调参全解析
LT9211 MIPI重定时器原理与双路扇出实战指南

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

用JavaScript拆解零工经济账本:叙事新闻游戏开发实战

发布时间:2026/10/10 16:56:06
用JavaScript拆解零工经济账本:叙事新闻游戏开发实战 简介这是一款由英国《金融时报》制作的叙事性新闻游戏源码围绕优步司机的经济处境与零工经济体验展开玩家需在一周内尝试赚取1000美元并在真实司机访谈面前做出抉择。资源面向对叙事游戏开发、数据新闻可视化及JavaScript前端工程感兴趣的开发者与学习者可用于研究交互叙事结构、状态管理与决策分支的实现方式。压缩包共78个文件约517KB以28个JavaScript文件为核心逻辑辅以11个HTML页面、9个JSON配置、5个YML流程文件及SCSS样式、TypeScript脚本等涵盖客户端组件、服务端接口、测试与CI部署配置目录划分清晰。已有300人学习下载。通过阅读源码可了解游戏决策系统、状态工具、原型脚本与构建流程的组织方式并借鉴其将新闻内容与游戏机制结合的工程实践。1. 当叙事遇上算法一款用 JavaScript 拆解零工经济账本的新闻游戏你可能玩过不少模拟驾驶游戏但大概率没见过把「司机每小时净收入」当作核心机制的作品。这款名为 uber-driver-game 的叙事性新闻游戏用 JavaScript 把零工经济里最扎心的那笔账摊开给你看接单、空驶、抽成、油费、车辆折旧每一秒都在烧钱。它不是让你飙车爽一把而是让你在凌晨三点的机场排队时盯着屏幕上的余额算今天能不能回本。适合谁想用代码讲清楚一个社会议题的前端开发者、对数据叙事感兴趣的产品人以及所有想弄明白「平台抽成到底怎么算」的从业者。整个项目就是一套可跑、可改、可拆的 JavaScript 源码把经济模型和叙事选择绑在一起你改一个参数故事走向就变了。2. 经济模型怎么跑从接单概率到净收入的 JavaScript 实现2.1 核心循环时间片驱动的收入模拟这个游戏最值钱的部分不是画面是那套藏在game-loop.js里的经济模拟。它把司机的每一分钟切成时间片每个时间片里做三件事判断有没有新订单、计算这一单的毛收入、扣掉所有成本。常见做法是用requestAnimationFrame驱动渲染但经济计算必须跟真实时间解耦否则玩家切到后台标签页收入就停了。我一般会用一个独立的setInterval或者基于performance.now()的累加器来跑经济时钟。// economy-engine.js // 经济时钟每 1000ms 推进一个「游戏分钟」与渲染帧率解耦 const ECONOMY_TICK_MS 1000; const GAME_MINUTES_PER_TICK 1; class EconomyEngine { constructor(config) { this.balance config.startingBalance; // 初始余额通常为 0 或负数代表车贷 this.fuel config.tankCapacity; // 油箱容量加仑 this.mileage 0; // 总里程 this.rideHistory []; // 每单记录用于结算面板 this.tickCount 0; } // 每个时间片调用一次 tick(currentZone, demandMultiplier) { this.tickCount GAME_MINUTES_PER_TICK; // 1. 空驶油耗即使没接单发动机也在烧油 const idleBurn this.calculateIdleBurn(currentZone); this.fuel - idleBurn; // 2. 接单概率由区域需求、时段、司机评分共同决定 const orderProbability this.calculateOrderProbability( currentZone, demandMultiplier ); if (Math.random() orderProbability) { const order this.generateOrder(currentZone); this.executeOrder(order); } // 3. 每 60 个游戏分钟1 小时结算一次固定成本 if (this.tickCount % 60 0) { this.deductFixedCosts(); } } calculateIdleBurn(zone) { // 不同区域怠速油耗不同机场排队区 vs 市中心巡游 const baseBurn 0.02; // 加仑/游戏分钟 return baseBurn * (zone.idleFactor || 1.0); } calculateOrderProbability(zone, demandMultiplier) { // 基础概率 0.15乘以区域需求系数和全局需求系数 const base 0.15; const zoneFactor zone.demandFactor || 1.0; return Math.min(base * zoneFactor * demandMultiplier, 0.95); } executeOrder(order) { const grossEarning order.fare order.tip; // 毛收入 车费 小费 const platformCut grossEarning * this.config.platformCommissionRate; // 平台抽成 const fuelCost order.distance * this.config.fuelCostPerMile; // 这一单的油费 const netEarning grossEarning - platformCut - fuelCost; this.balance netEarning; this.fuel - order.distance * this.config.fuelConsumptionPerMile; this.mileage order.distance; this.rideHistory.push({ timestamp: this.tickCount, gross: grossEarning, platformCut, fuelCost, net: netEarning, distance: order.distance, }); } deductFixedCosts() { // 每小时固定成本车辆折旧、保险、手机费 const hourlyFixed this.config.vehicleDepreciationPerHour this.config.insurancePerHour this.config.phonePlanPerHour; this.balance - hourlyFixed; } }这段代码里最关键的参数是platformCommissionRate常见做法是设成 0.25 左右但你可以改成 0.15 或 0.35看司机净收入怎么被吃掉。fuelConsumptionPerMile和fuelCostPerMile要分开设因为油价会波动而油耗取决于车型。idleFactor是个容易被忽略的参数——机场排队区的怠速油耗可能是市中心的 1.5 倍因为空调和发动机长时间运转。如果你把ECONOMY_TICK_MS改成 100游戏节奏会快十倍但玩家根本来不及做决策所以这个值一般不低于 500。2.2 叙事分支怎么跟经济数据挂钩叙事性新闻游戏和普通模拟游戏的区别在于故事不是写死的是由经济状态触发的。比如当balance连续三个游戏小时为负就触发「考虑卖车」的对话当fuel低于 10%就触发「找加油站还是硬撑」的选择。这套逻辑在narrative-engine.js里用状态机实现。// narrative-engine.js // 叙事状态机根据经济指标触发不同故事线 const NARRATIVE_STATES { STABLE: stable, // 收支平衡 STRUGGLING: struggling, // 连续亏损 CRISIS: crisis, // 余额跌破阈值 BURNOUT: burnout, // 连续工作超时 }; class NarrativeEngine { constructor(economyEngine) { this.economy economyEngine; this.state NARRATIVE_STATES.STABLE; this.consecutiveLossHours 0; this.consecutiveWorkHours 0; this.triggeredEvents new Set(); } evaluate() { const hourlyNet this.calculateHourlyNet(); // 状态迁移逻辑 if (hourlyNet 0) { this.consecutiveLossHours; } else { this.consecutiveLossHours 0; } if (this.consecutiveLossHours 3) { this.transitionTo(NARRATIVE_STATES.STRUGGLING); } if (this.economy.balance -50) { this.transitionTo(NARRATIVE_STATES.CRISIS); } if (this.consecutiveWorkHours 12) { this.transitionTo(NARRATIVE_STATES.BURNOUT); } } calculateHourlyNet() { const recentRides this.economy.rideHistory.filter( r r.timestamp this.economy.tickCount - 60 ); const gross recentRides.reduce((sum, r) sum r.gross, 0); const costs recentRides.reduce( (sum, r) sum r.platformCut r.fuelCost, 0 ); const fixed this.economy.config.vehicleDepreciationPerHour this.economy.config.insurancePerHour this.economy.config.phonePlanPerHour; return gross - costs - fixed; } transitionTo(newState) { if (this.state newState) return; this.state newState; this.emitNarrativeEvent(newState); } emitNarrativeEvent(state) { // 每个状态只触发一次关键事件避免重复弹窗 const eventKey ${state}_${Math.floor(this.economy.tickCount / 60)}; if (this.triggeredEvents.has(eventKey)) return; this.triggeredEvents.add(eventKey); // 这里对接 UI 层显示对应的叙事片段 // 实际项目中会调用 showDialogue(state) 之类的函数 } }consecutiveLossHours这个计数器是叙事节奏的油门。设成 3 意味着司机连续亏三小时才触发「挣扎」状态太短会让玩家觉得游戏在针对自己太长又失去了紧迫感。balance -50这个阈值要看初始余额——如果车贷是 200那 -50 只是毛毛雨如果初始余额是 0-50 就是重大危机。consecutiveWorkHours用来模拟疲劳驾驶超过 12 小时触发「倦怠」线这时候接单概率会下降因为反应变慢了。提示叙事状态机的evaluate()方法不要每帧调用放在经济时钟的 tick 里每 10 个游戏分钟跑一次就够了否则状态切换太频繁玩家根本读不完对话。3. 把源码跑起来环境、依赖与三个必改配置3.1 本地启动从 clone 到浏览器打开这个项目是纯前端 JavaScript没有后端依赖但用了 ES Module 和部分现代浏览器 API。我一般会用一个静态服务器跑而不是直接双击index.html因为 ES Module 在file://协议下会被 CORS 拦住。# 1. 获取源码假设你已经拿到压缩包解压后进入目录 cd uber-driver-game # 2. 安装依赖项目通常只有开发依赖比如 vite 或 parcel npm install # 3. 启动开发服务器 npm run dev # 如果项目没有配构建工具用 Python 自带的 http.server 也能跑 # python3 -m http.server 8080 # 然后浏览器打开 http://localhost:8080npm run dev背后通常是 Vite它会做三件事把 ES Module 转成浏览器能直接跑的格式、启动热更新、开一个本地端口。如果你拿到的是纯静态版本没有package.json那就用python3 -m http.server但要注意路径——所有import语句里的相对路径必须跟实际文件结构一致否则控制台会报 404。3.2 必改配置让经济模型符合你的叙事目标源码里有一个config.js或者game-config.json集中放了所有可调参数。我拿到任何模拟类项目第一件事就是把这个文件过一遍因为默认值往往是为了「能跑」不是为了「合理」。参数名默认值建议范围影响platformCommissionRate0.250.15 ~ 0.35平台抽成比例直接决定司机净收入fuelCostPerMile0.120.08 ~ 0.20每英里油费跟油价挂钩vehicleDepreciationPerHour2.501.50 ~ 4.00车辆折旧按小时摊insurancePerHour1.200.80 ~ 2.00保险分摊startingBalance0-200 ~ 500初始余额负数代表车贷orderProbabilityBase0.150.08 ~ 0.30基础接单概率改platformCommissionRate是最直接的叙事杠杆。设成 0.15司机时薪可能到 15 美元设成 0.35时薪直接掉到 8 美元以下故事立刻变得沉重。startingBalance设成 -200 意味着司机一开局就背着车贷前几个小时都在还债这种压迫感是叙事设计的一部分。3.3 调试面板怎么看懂每一单的账项目里通常有一个隐藏的调试面板按CtrlShiftD或者改 URL 参数?debugtrue打开。它会显示每一单的毛收入、平台抽成、油费、净收入以及当前小时的固定成本。如果你拿到的版本没有调试面板可以自己在executeOrder里加一行console.table。// 在 executeOrder 末尾加这行快速看每一单的账 console.table([{ gross: grossEarning.toFixed(2), platform: platformCut.toFixed(2), fuel: fuelCost.toFixed(2), net: netEarning.toFixed(2), balance: this.balance.toFixed(2), }]);console.table比console.log好在它把对象按列对齐你一眼就能看出哪一单的油费异常高——通常是距离参数被算错了。如果net一直是负数先检查platformCommissionRate是不是被设成了 0.5 以上再检查fuelCostPerMile是不是把「每加仑油价」和「每英里油费」搞混了。4. 避坑与排查五个让模拟结果失真的常见问题4.1 时间片漂移为什么挂了十分钟收入没涨现象切到其他标签页十分钟回来发现游戏时间只走了两分钟收入几乎没变。原因setInterval在后台标签页会被浏览器降频Chrome 最低降到每分钟一次。如果你的经济时钟依赖setInterval的调用次数来累加游戏时间后台期间就会丢帧。解决用performance.now()做时间差补偿。每次 tick 时计算真实经过的毫秒数除以ECONOMY_TICK_MS得到应该推进的游戏分钟数然后一次性补上。let lastTickRealTime performance.now(); function economyLoop() { const now performance.now(); const elapsedMs now - lastTickRealTime; const ticksToCatchUp Math.floor(elapsedMs / ECONOMY_TICK_MS); for (let i 0; i ticksToCatchUp; i) { economy.tick(currentZone, demandMultiplier); } lastTickRealTime now - (elapsedMs % ECONOMY_TICK_MS); requestAnimationFrame(economyLoop); }4.2 浮点数累积误差跑了一小时余额差了三分钱现象长时间运行后balance出现 0.03 或 0.07 的偏差对不上手工计算的账。原因JavaScript 的浮点数运算不精确0.1 0.2 ! 0.3。每一单的油费、抽成都是浮点数累加上千次后误差就显出来了。解决所有金额以「分」为单位存整数显示时再除以 100。或者用toFixed(2)在每次累加后截断但截断本身也会引入误差。最稳妥的是整数分。// 以分为单位 const grossCents Math.round(order.fare * 100) Math.round(order.tip * 100); const platformCutCents Math.round(grossCents * this.config.platformCommissionRate); const fuelCostCents Math.round(order.distance * this.config.fuelCostPerMile * 100); const netCents grossCents - platformCutCents - fuelCostCents; this.balanceCents netCents;4.3 接单概率溢出为什么司机永远在接单现象orderProbability算出来大于 1Math.random() orderProbability永远为真司机一刻不停接单时薪高得离谱。原因calculateOrderProbability里把多个系数相乘没有做上限截断。比如base0.15zoneFactor3.0demandMultiplier2.5乘出来是 1.125。解决用Math.min(probability, 0.95)封顶留 5% 的空驶时间否则叙事里「等单焦虑」这条线就没了。4.4 叙事事件重复触发同一个对话弹了八遍现象司机连续亏损三小时后「考虑卖车」的对话每次 tick 都弹玩家点都点不完。原因transitionTo只在状态变化时触发但如果状态在STRUGGLING和CRISIS之间反复横跳每次跳回STRUGGLING都会重新触发。解决用triggeredEvents集合记录已经触发过的事件 keykey 里带上小时数确保每个状态每小时只触发一次。或者加一个冷却时间触发后 30 个游戏分钟内不再触发同类事件。4.5 油耗计算单位混乱英里和公里混用现象油费忽高忽低同一距离的订单有时扣 0.5 加仑有时扣 0.8 加仑。原因order.distance有的地方按英里算有的地方按公里算而fuelConsumptionPerMile是按英里设的。1 英里约等于 1.609 公里混用后误差 60%。解决在config.js里加一个distanceUnit字段所有距离计算前先统一转成英里。或者干脆在generateOrder里就把距离转成英里后面所有模块只认英里。注意这五个坑里时间片漂移和浮点数误差最隐蔽往往跑了一小时才发现。建议在开发阶段就把console.table打开每 10 个游戏分钟打一次账对不上就停下来查。5. 进阶玩法用参数扫描找出「时薪跌破最低工资」的临界点这个项目最有价值的用法不是玩是拿它做参数扫描。你可以写一个脚本自动跑 100 组不同的platformCommissionRate和fuelCostPerMile组合记录每组的司机时薪然后找出时薪跌破 7.25 美元联邦最低工资的临界线。这套方法我在做数据叙事项目时经常用比手工调参快得多。// param-scan.js // 参数扫描找出时薪跌破最低工资的临界组合 const COMMISSION_RATES [0.15, 0.20, 0.25, 0.30, 0.35]; const FUEL_COSTS [0.08, 0.10, 0.12, 0.14, 0.16, 0.18, 0.20]; const MINIMUM_WAGE 7.25; const SIMULATION_HOURS 8; // 模拟 8 小时 function runSimulation(commissionRate, fuelCostPerMile) { const config { platformCommissionRate: commissionRate, fuelCostPerMile: fuelCostPerMile, fuelConsumptionPerMile: 0.04, // 25 MPG vehicleDepreciationPerHour: 2.50, insurancePerHour: 1.20, phonePlanPerHour: 0.50, startingBalance: 0, }; const economy new EconomyEngine(config); const zone { idleFactor: 1.0, demandFactor: 1.0 }; const demandMultiplier 1.0; // 模拟 8 小时每分钟一个 tick for (let i 0; i SIMULATION_HOURS * 60; i) { economy.tick(zone, demandMultiplier); } const totalGross economy.rideHistory.reduce((s, r) s r.gross, 0); const totalCosts economy.rideHistory.reduce( (s, r) s r.platformCut r.fuelCost, 0 ); const fixedCosts (config.vehicleDepreciationPerHour config.insurancePerHour config.phonePlanPerHour) * SIMULATION_HOURS; const netEarnings totalGross - totalCosts - fixedCosts; const hourlyWage netEarnings / SIMULATION_HOURS; return { commissionRate, fuelCostPerMile, hourlyWage: hourlyWage.toFixed(2), belowMinimum: hourlyWage MINIMUM_WAGE, }; } // 跑所有组合 const results []; for (const rate of COMMISSION_RATES) { for (const fuel of FUEL_COSTS) { results.push(runSimulation(rate, fuel)); } } // 输出临界线 console.table(results.filter(r r.belowMinimum));这段脚本跑完你会得到一张表列出所有「时薪低于最低工资」的参数组合。SIMULATION_HOURS设成 8 是因为司机一个班次通常 8 小时设太长会掩盖前几小时的亏损。fuelConsumptionPerMile设成 0.04 对应 25 MPG这是常见家用车的油耗如果你要模拟混动车改成 0.02。demandMultiplier设成 1.0 是中性需求设成 0.8 就是淡季时薪会进一步下降。跑完扫描后你会发现一个反直觉的结论platformCommissionRate从 0.25 降到 0.20时薪可能只涨 1 美元但fuelCostPerMile从 0.12 涨到 0.16时薪直接掉 2 美元。油价比抽成对司机的影响更大因为抽成是按比例扣油价是按里程扣而里程是司机没法控制的——乘客要去哪你就得开去哪。从那以后我每次拿到经济模拟类项目都先跑一遍参数扫描把敏感参数找出来再决定叙事重心放在哪。希望帮到你。本文还有配套的精品资源点击获取

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号