☰
用Python和HTML搭建游戏活动全收集进度清单,告别漏点
2026/10/15 4:18:01 网站建设 项目流程

前几周帮朋友“板板”梳理一份大地图活动的全收集资料,任务量不小:点位多、活动周期短、还要每天同步进度。当时手里只有一张大家口口相传的“信誉图”,图上点位密集,直接照着跑很容易漏,更别说还要统计“到底跑了多少”。后来我把这张图转成了结构化点位表,再用一小段 Python 脚本生成每日计划和本地浏览器进度清单,整个过程才顺畅起来。这篇就完整分享这套方法,从“信誉图是什么”讲到“怎么把它变成可复现、可追踪的全收集进度体系”,赛车活动、限时收集类任务都能套用。

1. 背景:一张“信誉图”里到底有什么

1.1 什么叫“信誉图”

在很多游戏社区里,“信誉图”并不是官方地图,而是由玩家整理、经过多人实际验证后形成的参考图。它通常覆盖地图中的坐标点位、收集品种类、稀有度、前置条件等信息。之所以叫“信誉图”,是因为它的可信度来自整理者长期稳定的输出和大量玩家的二次验证,而不是随便一个截图标几个箭头。

也就是说,信誉图的质量参差不齐,关键在于:

  • 点位是否经过验证;
  • 更新频率如何;
  • 是否区分了普通点位、隐藏点位和活动限时点位;
  • 是否有明确的分区、编号、图例。

把它当成资料源是很好的,但它不能直接当成“任务管理系统”。因为你在活动里要做的事情不只是“知道点在哪”,还包括“哪些做了、哪些没做、今天做多少、下一步做哪里”。

1.2 活动地图全收集的两种类型

限时活动里的“全收集”通常有两种形态。

第一种是静态收集:点位固定,不会刷新,只要按顺序跑一轮就能完成。这种形态相对简单,适合用 Excel 做一次性清单。

第二种是动态收集:点位会按天开放、随机刷新,或者每周重置一部分,常见于大型赛季活动。这种形态必须考虑“状态管理”,否则今天跑完的点位明天又刷新了,记录会对不上。

以“狂飙大奖赛”这类赛车活动为例,很多情况下它会把竞速排名和地图收集放在同一张活动页里:既要跑圈冲名次,又要沿路收集零件、涂装、补给箱等奖励道具。前者依赖游戏操作水平,后者依赖路线规划和点位覆盖。把这两类事情混在一起记,是最容易出现遗漏的。

所以我习惯把活动拆成两条线:

  • 收集线:点位表的完成状态;
  • 竞速线:每日比赛场次、目标名次、奖励任务进度。

这样一来,活动是“双重任务”还是“单线收集”,都不会影响记录结构。

1.3 赛车活动为什么更需要进度表

赛车活动有比较明显的周期节奏,一般分为预热期、正赛期和收尾期。预热期点位少,正赛期点位全量开放,收尾期则可能压缩时间。如果你只靠脑子记,或者靠聊天记录里的截图,等到正赛期点位全量开放时,很容易出现“每天跑很多,但不知道哪里还差一点”的情况。

进度表能解决三个问题:

  1. 总量可视化:一共多少个点位,已完成多少,剩余多少;
  2. 时间可控:按每日可用时间分配点位,避免最后几天超载;
  3. 协作清晰:如果和朋友一起做,每个人负责哪个分区一目了然。

这张进度表是完全离线的,不需要访问游戏数据,也不用改变游戏文件,只是把收集信息整理成可用于活动期间的记录工具。这个边界很重要:我们做的是“信息管理”,不是“自动化操作”。

2. 环境准备与项目目录

2.1 工具与版本说明

这套方案对工具版本要求不高,核心是三个工具:

  • 文本编辑器或 IDE:推荐 VS Code,也可以用记事本、EditPlus;
  • Python 3:用于生成示例数据和构建清单文件;
  • 浏览器:用于打开本地 HTML 进度清单。

如果你电脑上还没有 Python,可以从官网安装 3.10 或更新的稳定版本。安装时记得勾选“Add Python to PATH”,这样后面在命令行里直接输入python就能运行。

版本需要根据你的实际环境调整,本文示例以常见环境为例,重点演示配置思路。即使版本不同,代码里的标准库用法通常也不会受太大影响。

2.2 项目目录示例

我先规划一个清晰的项目目录:

star-city-collect/ ├── data/ │ ├── points_sample.csv │ └── activity_config.json ├── tools/ │ ├── generate_points.py │ ├── build_checklist.py │ └── make_daily_plan.py ├── template/ │ └── checklist.html └── dist/ └── data.js
  • data/放点位数据、活动配置;
  • tools/放生成、转换脚本;
  • template/放 HTML 模板;
  • dist/放最终生成的可执行文件。

按照习惯,我们不会把生成的dist/data.js当作原始数据到处复制,因为它的来源是data/points_sample.csv。每次修改 CSV 后重新执行脚本生成即可,避免多人协作时“各改各的”。

2.3 打开本地页面的方式

HTML 进度清单用浏览器直接双击就能打开,因为脚本在页面里引用同一个目录下的data.js,不需要联网。不过有些浏览器的安全策略会限制本地的 localStorage,导致勾选状态没法持久保存。遇到这种情况,可以在项目根目录执行:

python -m http.server 8080

然后访问:

http://localhost:8080/dist/checklist.html

这样浏览器就会把localhost当作一个安全的本地站点,状态保存更稳定。

3. 数据建模:从点位图到登记表

3.1 点位表字段设计

点位表是整个全收集体系的底座。字段设计得合理,后续统计、排查、协作都会轻松很多。

我在做“星城”数据结构时,用了这样几个字段:

字段含义示例
point_id点位唯一编号P00001
region所属分区星城北
category点位类别补给箱
desc位置描述滨江路南侧路灯下
rarity稀有度史诗
cost预估耗时(分钟)6
source信息来源板板验证图 v2
collected是否已收集0

这里需要注意一个细节:collected字段不应该直接省略。虽然 HTML 页面的 localStorage 会保存勾选状态,但 CSV 里的collected仍然可以作为“活动开始时是否重置”的标记。活动如果每天刷新,就把collected统一清 0;如果是一次性点位,就保持 1。

3.2 活动任务表设计

点位表之外,还需要一张活动任务表,专门记录竞速类目标。

常见的字段可以这样设计:

字段含义示例
task_id任务编号RACE-01
task_name任务名称每日竞速前 50%
reward奖励内容加速零件
target目标值3 场
finished当前完成值2
done是否完成否
remark备注优先白天完成

点位表和任务表不需要放在同一个表格里,因为遍历频次不同。点位表适合以“点位 ID”为行,任务表适合以“任务类型”为行。在 Excel 中分两个 Sheet,或者在数据库里分两张表,逻辑都更清晰。

3.3 点位图转成数据行的流程

从“信誉图”到“数据行”的过程,推荐按下面四步走。

第一步,先把地图按自然区域切块。比如星城北、星城南、竞速区、滨江路等,每块独立录入。

第二步,给每个点位编号。编号规则建议带分区前缀,比如NB-001、JR-001,这样看到编号就知道大致方位,也方便排序。

第三步,补全属性信息。点位类别、稀有度、预估耗时,尽量在录入时就补上。如果图里没有,可以先填“普通”和“5分钟”,后续根据实际体验修正。

第四步,记录来源。这个点来自哪个版本的图、谁校验过,都要写在source字段里。因为信誉图也可能更新,有了来源字段,将来图片升级时可以快速筛查需要修改的数据。

3.4 优先级与“超限预算”

“超限”在这里指的是:点位总量或任务量超出了你每天可投入的时间上限。所以我提前设定一个“超限预算”算法。

假设每天只有 120 分钟可投入活动:

  • 普通点 1 分钟;
  • 稀有点 3 分钟;
  • 史诗点 6 分钟。

如果每天随机抽一堆点位,很可能抽出 180 分钟的工作量,当天根本做不完。所以做每日计划时,不是“随机取前 N 个”,而是按预算累加:从点位池里逐条取样,取到 120 分钟左右就停止,并保留 15% 的机动时间。

遇到“超限”怎么办?我的原则是:

  • 先删除低价值普通点;
  • 再保留稀有和史诗点,因为奖励通常更高;
  • 最后把剩余点顺延到第二天,不硬塞进当天计划。

这个算法非常简单,但很实用。

4. 实战案例:4.5千点位全收集清单

这一节的示例数据完全用于演示流程,不代表任何真实游戏数据。如果你要接入真实活动,只需要把 CSV 换成自己的点位表即可。

4.1 生成示例点位数据

先写一个生成脚本,模拟生成 4500 条点位记录,覆盖多个分区和类别。

文件路径:tools/generate_points.py

import csv import random N = 4500 regions = ["星城北", "星城南", "星科教区", "竞速区", "滨江路", "老城区"] categories = ["补给箱", "涂装", "零件", "挑战点", "隐藏点", "任务NPC"] rarity = ["普通", "稀有", "史诗"] cost_map = {"普通": 1, "稀有": 3, "史诗": 6} random.seed(2025) with open("data/points_sample.csv", "w", newline="", encoding="utf-8-sig") as f: writer = csv.writer(f) writer.writerow(["point_id", "region", "category", "desc", "rarity", "cost", "collected"]) for i in range(1, N + 1): region = random.choice(regions) category = random.choice(categories) r = random.choices(rarity, weights=[80, 15, 5])[0] desc = f"{region}-{category}-{i:04d}" writer.writerow([f"P{i:05d}", region, category, desc, r, cost_map[r], 0]) print(f"生成完成:{N} 条示例点位数据")

关键点说明:

  • encoding="utf-8-sig"是为了让 Excel 打开 CSV 时不会出现中文乱码;
  • random.seed(2025)是为了让每次生成的数据可复现,方便你验证运行结果;
  • collected默认写 0,表示未收集。

运行命令:

python tools/generate_points.py

预期输出:

生成完成:4500 条示例点位数据

打开data/points_sample.csv,会看到表格的第一行是字段名,往下每行就是一个点位。

4.2 生成每日收集计划

点位数据出来后,再写一个每日计划脚本。它按“时间预算”生成当天建议点位,而不是盲目取前 100 条。

文件路径:tools/make_daily_plan.py

import csv import random DAY_TOTAL_MINUTES = 120 random.seed(2025) with open("data/points_sample.csv", encoding="utf-8-sig") as f: rows = list(csv.DictReader(f)) unfinished = [r for r in rows if r["collected"] == "0"] random.shuffle(unfinished) plan = [] used = 0 for r in unfinished: cost = int(r["cost"]) if used + cost <= DAY_TOTAL_MINUTES: plan.append(r) used += cost if used >= DAY_TOTAL_MINUTES: break print(f"今日可安排 {len(plan)} 个点位,预计花费 {used} 分钟") for i, r in enumerate(plan, 1): print(f"{i:03d} {r['point_id']} {r['region']} {r['category']} {r['cost']}分钟")

运行:

python tools/make_daily_plan.py

它会输出类似这样的结果:

今日可安排 103 个点位,预计花费 120 分钟 001 P01234 星城北 补给箱 1分钟 002 P00876 滨江路 零件 3分钟 ...

这套脚本的价值在于:它会自动避开已经收集的点位,并按时间上限控制当天任务量,保证计划可执行。

4.3 构建可交互的本地进度清单

第三步是做一个本地 HTML 进度清单。它会读取同一个目录下的data.js,渲染成表格,并支持点击切换收集状态。

先写一个转换脚本,把 CSV 转成data.js。

文件路径:tools/build_checklist.py

import csv import json import os import sys src = sys.argv[1] if len(sys.argv) > 1 else "data/points_sample.csv" out = sys.argv[2] if len(sys.argv) > 2 else "dist/data.js" os.makedirs(os.path.dirname(out), exist_ok=True) rows = [] with open(src, encoding="utf-8-sig") as f: for row in csv.DictReader(f): rows.append({ "id": row["point_id"], "region": row["region"], "category": row["category"], "desc": row["desc"], "rarity": row["rarity"], "cost": int(row["cost"]) }) with open(out, "w", encoding="utf-8") as f: f.write("window.CHECK_DATA = ") json.dump(rows, f, ensure_ascii=False)

然后准备 HTML 模板。

文件路径:template/checklist.html

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>星城全收集进度清单</title> <style> body { font-family: "Microsoft YaHei", Arial, sans-serif; margin: 24px; background: #f7f8fa; color: #333; } .card { max-width: 1200px; margin: 0 auto; background: #fff; border-radius: 10px; padding: 24px; box-shadow: 0 2px 8px rgba(0,0,0,.08); } h1 { font-size: 22px; margin: 0 0 12px; } #stats { display: flex; flex-wrap: wrap; gap: 12px; margin: 12px 0 20px; } .stat { flex: 1; min-width: 140px; background: #f0f4ff; border-radius: 8px; padding: 12px 16px; } .stat .label { font-size: 13px; color: #777; } .stat .num { font-size: 28px; font-weight: 700; margin-top: 4px; } .filters { display: flex; gap: 14px; margin-bottom: 16px; flex-wrap: wrap; } select { padding: 6px 10px; border: 1px solid #ccc; border-radius: 6px; } table { width: 100%; border-collapse: collapse; } th, td { border: 1px solid #e4e6eb; padding: 8px 10px; font-size: 14px; } th { background: #f0f2f5; text-align: left; } .clickable { cursor: pointer; color: #1677ff; } .done { background: #e8f7e8; color: #777; text-decoration: line-through; } .progress { height: 10px; background: #f0f2f5; border-radius: 5px; overflow: hidden; margin-top: 8px; } .progress span { display: block; height: 100%; background: #4caf50; } </style> </head> <body> <div class="card"> <h1>星城全收集进度清单</h1> <div id="stats"></div> <div class="progress"><span id="totalBar" style="width:0%"></span></div> <div class="filters"> <label>分区: <select id="regionFilter" onchange="render()"> <option value="">全部</option> </select> </label> <label>类别: <select id="categoryFilter" onchange="render()"> <option value="">全部</option> </select> </label> <button onclick="exportPlan()">导出未收集清单</button> </div> <table id="checkTable"> <thead> <tr> <th>点位ID</th> <th>分区</th> <th>类别</th> <th>说明</th> <th>稀有度</th> <th>预估耗时(分钟)</th> <th>状态</th> </tr> </thead> <tbody></tbody> </table> </div> <script src="data.js"></script> <script> const STORAGE_KEY = 'star_city_checked'; let store = null; try { store = window.localStorage; } catch (e) { store = null; } let checked = {}; if (store) { try { checked = JSON.parse(store.getItem(STORAGE_KEY) || '{}'); } catch (e) { checked = {}; } } const rows = window.CHECK_DATA || []; function renderStats() { const total = rows.length; let doneCount = 0; let totalCost = 0; rows.forEach(function(row) { totalCost += Number(row.cost) || 0; if (checked[row.id]) doneCount++; }); document.getElementById('stats').innerHTML = '<div class="stat"><div class="label">总点位</div><div class="num">' + total + '</div></div>' + '<div class="stat"><div class="label">已收集</div><div class="num">' + doneCount + '</div></div>' + '<div class="stat"><div class="label">剩余点位</div><div class="num">' + (total - doneCount) + '</div></div>' + '<div class="stat"><div class="label">理论耗时</div><div class="num">' + totalCost + '分钟</div></div>'; const pct = total ? Math.round(doneCount / total * 100) : 0; document.getElementById('totalBar').style.width = pct + '%'; } function fillFilters() { const regions = {}; const cats = {}; rows.forEach(function(row) { regions[row.region] = 1; cats[row.category] = 1; }); const regionSelect = document.getElementById('regionFilter'); const categorySelect = document.getElementById('categoryFilter'); Object.keys(regions).sort().forEach(function(k) { const opt = document.createElement('option'); opt.value = k; opt.textContent = k; regionSelect.appendChild(opt); }); Object.keys(cats).sort().forEach(function(k) { const opt = document.createElement('option'); opt.value = k; opt.textContent = k; categorySelect.appendChild(opt); }); } function render() { const region = document.getElementById('regionFilter').value; const category = document.getElementById('categoryFilter').value; const tbody = document.querySelector('#checkTable tbody'); tbody.innerHTML = ''; rows.forEach(function(row) { if (region && row.region !== region) return; if (category && row.category !== category) return; const done = !!checked[row.id]; const tr = document.createElement('tr'); if (done) tr.className = 'done'; tr.innerHTML = '<td>' + row.id + '</td>' + '<td>' + row.region + '</td>' + '<td>' + row.category + '</td>' + '<td>' + (row.desc || '-') + '</td>' + '<td>' + (row.rarity || '普通') + '</td>' + '<td>' + row.cost + '</td>' + '<td class="clickable" onclick="toggle(\'' + row.id + '\')">' + (done ? '已收集(点击撤销)' : '未收集(点击完成)') + '</td>'; tbody.appendChild(tr); }); } function toggle(id) { checked[id] = !checked[id]; if (store) { try { store.setItem(STORAGE_KEY, JSON.stringify(checked)); } catch (e) {} } render(); renderStats(); } function exportPlan() { const region = document.getElementById('regionFilter').value; const category = document.getElementById('categoryFilter').value; const lines = rows .filter(function(row) { return !checked[row.id]; }) .filter(function(row) { return !region || row.region === region; }) .filter(function(row) { return !category || row.category === category; }) .map(function(row, index) { return (index + 1) + ' ' + row.id + ' ' + row.region + ' ' + row.category + ' ' + row.cost + '分钟'; }); alert('未收集点位共 ' + lines.length + ' 条:\n' + lines.slice(0, 50).join('\n') + (lines.length > 50 ? '\n……' : '')); } renderStats(); fillFilters(); render(); </script> </body> </html>

这个页面本身不依赖任何外部资源,可以完全离线运行。它的特性是:

  • 顶部统计总点位、已收集、剩余点位、理论耗时;
  • 点击表格最后一列可以切换“未收集/已收集”;
  • 勾选状态保存在 localStorage;
  • 支持按分区、类别筛选,并导出一份未收集清单。

4.4 运行与验证

把模板和转换脚本组合起来:

python tools/build_checklist.py data/points_sample.csv dist/data.js

然后把template/checklist.html复制到dist/目录,或直接修改模板中的相对路径。

如果直接在dist/目录下操作,最终文件结构是:

dist/ ├── checklist.html └── data.js

双击checklist.html,应该能看到:

  • 有 4500 条点位数据;
  • 总耗时约在 4500 到 5000 分钟之间;
  • 表格第一页会显示部分点位;
  • 点击“状态”列,对应行会进入划线状态,同时顶部已收集数量增加。

如果数据为 0,通常是因为data.js没有生成,或者window.CHECK_DATA没有被正确赋值。重新执行构建脚本即可。

5. 常见问题与排查思路

实际使用中,最常见的问题集中在编码、路径、状态保存和点位刷新上。

问题现象常见原因解决思路
CSV 用 Excel 打开乱码文件不是 UTF-8-SIG 编码写入 CSV 时使用encoding="utf-8-sig"
浏览器页面没有点位数据data.js未生成或路径不对确认构建脚本输出位置,检查控制台是否有报错
本地页面勾选状态无法保存浏览器对 file 协议的 localStorage 限制用python -m http.server 8080启动本地站点
活动刷新后点位全部过期点位属性变化,旧表不再适用复制一份旧数据归档,再清空 collected 字段
每天点位计划超时没有按时间预算取点用 make_daily_plan.py 按分钟数预算生成
队友多,容易重复跑缺少责任分区在点位表中加 owner 字段,每人认领若干分区

如果你遇到“脚本执行成功但浏览器不更新”,优先做三件事:

  1. 强制刷新浏览器缓存:按Ctrl + F5;
  2. 打开浏览器开发者工具,在 Console 里输入window.CHECK_DATA.length,检查数据是否加载;
  3. 如果得到 0,说明data.js里没有数据,回到构建脚本重新执行。

6. 最佳实践与工程化建议

这套全收集进度体系要从“能用”变成“好用”,有几个工程化建议值得坚持。

6.1 分层保存数据

不要直接在原始 CSV 上改状态。比较推荐的做法是:

  • source/保存原始信誉图的截图和早期笔记;
  • data/保存录入后的点位表;
  • report/保存每天生成的计划和进度导出。

这样一来,即使当天操作错了,也能通过原始数据重新生成。

6.2 文件命名带上日期和版本

一个简单的命名习惯能减少很多麻烦:

points_2025_06_01.csv points_2025_06_02.csv checklist_v1.0.html

如果和板板这样的朋友协作,还可以约定:谁修改了数据,就在文件名最后追加自己的昵称,例如:

points_2025_06_02_banban.csv

但同一天只保留一份有效文件,避免“最终版”“最终版2”这类命名。

6.3 记录信息来源

点位数据的来源字段非常重要。信誉图的版本更新后,哪些点位变化了,哪些点位是新增的,都需要通过source字段追溯。我在建表时会单独列一列:

source=板板验证图 v2.1,更新日期 2025-06-10

这样如果图里某个点位与游戏实际情况不符,可以快速定位是数据录入问题,还是图片来源问题。

6.4 守住合规边界

整个方案里最重要的边界是:所有脚本都运行在本地电脑上,只处理 CSV 和浏览器 HTML,不读取游戏内存、不修改游戏文件、不抓取未公开接口,也不做任何自动点击动作。活动期间,建议只把这份清单当作“人肉执行”的记录工具。这样做既安全,也不会破坏游戏体验。

6.5 进度统计口径要统一

“已收集”到底以什么为准?我在和队友协作时会把状态分成两档:

  • 已标记:表示“我看到了点位并且点了一下记录”;
  • 已验证:表示“已经拿到活动奖励,确认完成”。

普通点位可以只标记未验证,稀有点位和史诗点位必须双人复核后再标验证。这样统计口径清晰,不会出现“数据到齐了但实际奖励没拿完”的情况。

7. 总结与下一步学习建议

这套方法的核心是:把一张静态的“信誉图”变成动态可追踪的点位表,再通过 CSV、Python 和本地 HTML 三者的配合,解决活动全收集里的数量统计、每日计划、状态记录三个问题。你不需要懂很深的编程,只要会运行几个脚本,就能复制一套属于自己的全收集进度工具。

如果接下来想继续优化,可以按这个顺序学:

  • 先学会 Excel 的数据透视表,用分区和类别快速统计总量;
  • 再学 Python 的pandas,处理更大规模的点位数据会更省力;
  • 接着可以把 HTML 页面升级成带搜索、按稀有度排序、按天记录历史曲线的版本;
  • 如果多人协作频繁,甚至可以用 Git 管理点位表,每次提交都留下变更记录。

最后再感谢一下板板的前期整理和持续校验。没有这些经多人验证的点位资料,“星城”这种量级的地图全收集会困难很多。也希望这套进度管理思路,能帮你在下次“狂飙大奖赛”之类的限时活动里少漏点位、少走弯路,把有限的游戏时间花在最值得的收集项上。

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

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

立即咨询