SBTI 赛博人格测试 – 源码深度分析
项目地址:https://github.com/pingfanfan/SBTI
原创来源:B站UP主 @蛆肉儿串儿
一、项目概述
1.1 项目定位
SBTI(赛博人格测试) 是一个爆火的前端性格测试项目,特点:
-
开源免费:MIT协议,可自由使用和修改 -
纯前端实现:无后端依赖,静态部署 -
数据驱动:所有测试内容在JSON文件中,易于定制 -
移动优先:响应式设计,手机体验友好
核心数据:
-
30道题:每维度2题 -
15个维度:5大模型 × 3维度 -
27种人格:25种标准类型 + 2种特殊类型 -
曼哈顿距离匹配:科学的向量匹配算法
1.2 项目结构(真实)
SBTI/├── data/ # 测试数据(核心)│ ├── questions.json # 30道题库│ ├── dimensions.json # 15个维度定义│ ├── types.json # 27种人格类型│ └── config.json # 评分参数配置├── src/ # 源代码│ ├── engine.js # ⭐ 评分算法(核心)│ ├── quiz.js # 答题流程控制│ ├── result.js # 结果页渲染│ ├── chart.js # 雷达图(Canvas API)│ ├── share.js # 分享功能│ ├── utils.js # 工具函数│ ├── main.js # 入口│ └── style.css # 样式(CSS变量)├── docs/│ └── analysis.md # 官方数据分析报告├── index.html # 入口页面├── package.json # 依赖配置└── vite.config.js # Vite构建配置
二、核心算法深度解析
2.1 评分流程(基于真实源码)
→Step 1: 按维度求和
// src/engine.jsexport function calcDimensionScores(answers, questions) {const scores = {}for (const q of questions) {if (answers[q.id] == null) continuescores[q.dim] = (scores[q.dim] || 0) + answers[q.id]}return scores}
输入:{ q1: 2, q2: 3, q3: 1, ... }输出:{ S1: 5, S2: 4, S3: 6, ... }逻辑:每个维度的2题分值相加
→Step 2: 分数转等级
export function scoresToLevels(scores, thresholds) {const levels = {}for (const [dim, score] of Object.entries(scores)) {if (score <= thresholds.L[1]) levels[dim] = 'L'else if (score >= thresholds.H[0]) levels[dim] = 'H'else levels[dim] = 'M'}return levels}
阈值配置(data/config.json):
{"scoring": {"levelThresholds": {"L": [2, 3], // 总分2-3 → L(低)"M": [4, 4], // 总分4 → M(中)"H": [5, 6] // 总分5-6 → H(高)}}}
示例:
S1维度总分:5 → HS2维度总分:4 → MS3维度总分:3 → L
→Step 3: 曼哈顿距离匹配
// 等级转数值const LEVEL_NUM = { L: 1, M: 2, H: 3 }export function matchType(userLevels, dimOrder, pattern) {const typeLevels = parsePattern(pattern) // 解析pattern字符串let distance = 0let exact = 0for (let i = 0; i < dimOrder.length; i++) {const userVal = LEVEL_NUM[userLevels[dimOrder[i]]] || 2const typeVal = LEVEL_NUM[typeLevels[i]] || 2const diff = Math.abs(userVal - typeVal)distance += diffif (diff === 0) exact++}const similarity = Math.max(0, Math.round((1 - distance / 30) * 100))return { distance, exact, similarity }}
曼哈顿距离公式:
distance = Σ |userVal_i - typeVal_i|相似度 = (1 - distance / 30) × 100%
示例计算:
用户向量: [H, H, L, M, H, M, L, L, H, M, H, H, L, M, H][3, 3, 1, 2, 3, 2, 1, 1, 3, 2, 3, 3, 1, 2, 3]CTRL类型: [H, H, H, H, M, H, M, H, H, H, H, H, M, H, M][3, 3, 3, 3, 2, 3, 2, 3, 3, 3, 3, 3, 2, 3, 2]差值绝对值:[0, 0, 2, 1, 1, 1, 1, 2, 0, 1, 0, 0, 1, 1, 1]曼哈顿距离 = 12相似度 = (1 - 12/30) × 100% = 60%精准命中(diff=0的维度)= 7个
→Step 4: 结果排序与特殊覆盖
export function determineResult(userLevels, dimOrder, standardTypes, specialTypes, options = {}) {// 1. 计算所有类型的匹配度const rankings = standardTypes.map((type) => ({...type,...matchType(userLevels, dimOrder, type.pattern),}))// 2. 排序:距离升序 → 精准命中降序 → 相似度降序rankings.sort((a, b) =>a.distance - b.distance ||b.exact - a.exact ||b.similarity - a.similarity)const best = rankings[0]const drunk = specialTypes.find((t) => t.code === 'DRUNK')const hhhh = specialTypes.find((t) => t.code === 'HHHH')// 3. 酒鬼彩蛋覆盖if (options.isDrunk && drunk) {return {primary: { ...drunk, similarity: best.similarity, exact: best.exact },secondary: best,rankings,mode: 'drunk',}}// 4. 傻乐者兜底(相似度<60%)if (best.similarity < 60 && hhhh) {return {primary: { ...hhhh, similarity: best.similarity, exact: best.exact },secondary: best,rankings,mode: 'fallback',}}// 5. 正常匹配return {primary: best,secondary: rankings[1] || null,rankings,mode: 'normal',}}
2.2 算法流程图
┌─────────────────────────────────────────────────┐│ 用户完成30道题 ││ { q1: 2, q2: 3, ..., q30: 1 } │└─────────────────┬───────────────────────────────┘│▼┌─────────────────────────────────────────────────┐│ Step 1: calcDimensionScores() ││ 每维度2题求和 → { S1: 5, S2: 4, ... } │└─────────────────┬───────────────────────────────┘│▼┌─────────────────────────────────────────────────┐│ Step 2: scoresToLevels() ││ 分数转等级 → { S1: 'H', S2: 'M', ... } │└─────────────────┬───────────────────────────────┘│▼┌─────────────────────────────────────────────────┐│ Step 3: matchType() × 25种类型 ││ 计算曼哈顿距离、精准命中、相似度 │└─────────────────┬───────────────────────────────┘│▼┌─────────────────────────────────────────────────┐│ Step 4: determineResult() ││ 排序 + 特殊覆盖 ││ ││ isDrunk? ──Yes──> DRUNK(酒鬼) ││ │ ││ No ││ │ ││ similarity < 60%? ──Yes──> HHHH(傻乐者) ││ │ ││ No ││ │ ││ 正常匹配 → 最佳类型 │└─────────────────────────────────────────────────┘
三、数据结构分析
3.1 题目数据结构
{"id": "q1","dim": "S1","text": "我是一只阴暗的老鼠...","options": [{ "label": "我哭了。。", "value": 1 },{ "label": "这是什么。。", "value": 2 },{ "label": "这不是我!", "value": 3 }]}
字段说明:
-
id:题目唯一标识 -
dim:所属维度(S1-S3, E1-E3, A1-A3, Ac1-Ac3, So1-So3) -
text:题目文本(沙雕风格) -
options:3个选项,value为分值(1=低,2=中,3=高)
题目特点:
-
沙雕文案:用夸张、幽默的表达降低心理防御 -
情境化:许多题目设置具体情境,而非抽象陈述 -
本土化:贴近中国年轻人的语境和网络文化
3.2 人格类型数据结构
{"code": "CTRL","pattern": "HHH-HMH-MHH-HHH-MHM","cn": "拿捏者","intro": "怎么样,被我拿捏了吧?","desc": "恭喜您,您测出了全中国最为罕见的人格..."}
字段说明:
-
code:人格代码(大写字母,便于传播) -
pattern:15位维度模式(H/M/L组合,用-分隔每个模型) -
cn:中文名称 -
intro:一句话开场白(灵魂暴击) -
desc:详细描述(200-300字)
pattern解析:
HHH-HMH-MHH-HHH-MHM│││ │││ │││ │││ │││S1 E1 A1 Ac1 So1S2 E2 A2 Ac2 So2S3 E3 A3 Ac3 So3
3.3 配置数据结构
{"scoring": {"levelThresholds": {"L": [2, 3],"M": [4, 4],"H": [5, 6]},"fallbackThreshold": 60},"display": {"dimensionsPerRow": 5,"showRarity": true}}
关键参数:
-
levelThresholds:分数到等级的映射 -
fallbackThreshold:傻乐者兜底阈值(相似度<60%)
四、技术实现亮点
4.1 纯函数设计
engine.js 中的所有评分函数都是纯函数:
-
无副作用 -
无DOM依赖 -
易于测试 -
易于复用
// 纯函数示例export function calcDimensionScores(answers, questions) {// 输入 → 输出,无副作用}export function matchType(userLevels, dimOrder, pattern) {// 输入 → 输出,无副作用}
优势:
-
可独立测试算法正确性 -
可在其他项目中复用 -
易于优化和重构
4.2 数据驱动架构
所有测试内容在JSON文件中:
data/├── questions.json # 修改题目├── types.json # 修改人格类型├── dimensions.json # 修改维度定义└── config.json # 修改评分参数
优势:
-
无需改代码:只需修改JSON即可定制 -
易于维护:数据和逻辑分离 -
易于扩展:添加新类型只需在types.json中添加对象
4.3 Canvas雷达图实现
chart.js 使用原生Canvas API绘制15维雷达图:
// 维度数值转换const LEVEL_NUM = { L: 1, M: 2, H: 3 }const normalizedValue = LEVEL_NUM[level] / 3 // 归一化到0-1// Canvas绑制ctx.beginPath()ctx.moveTo(centerX, centerY)for (let i = 0; i < 15; i++) {const angle = (Math.PI * 2 / 15) * i - Math.PI / 2const value = normalizedValues[i]const x = centerX + radius * value * Math.cos(angle)const y = centerY + radius * value * Math.sin(angle)ctx.lineTo(x, y)}ctx.closePath()ctx.fillStyle = 'rgba(76, 103, 82, 0.3)'ctx.fill()
优势:
-
无第三方库依赖 -
性能优异 -
可完全自定义样式
4.4 Vite构建优化
vite.config.js 配置:
import { defineConfig } from 'vite'export default defineConfig({base: './',build: {outDir: 'dist',assetsDir: 'assets'}})
优势:
-
开发服务器快速启动 -
HMR(热模块替换) -
生产构建优化(代码分割、压缩)
五、心理学逻辑分析
5.1 维度模型设计
5大模型 × 3维度:
|
|
|
|
|
|---|---|---|---|
| 自我模型 (S) |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 情感模型 (E) |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 态度模型 (A) |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 行动模型 (Ac) |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 社交模型 (So) |
|
|
|
|
|
|
|
|
|
|
|
|
5.2 理论借鉴
→大五人格模型(Big Five)
对应关系:
SBTI维度 Big Five维度─────────────────────────────S1 自尊自信 ←→ 神经质(N)的逆向So1 社交主动性 ←→ 外向性(E)Ac3 执行模式 ←→ 尽责性(C)E1 依恋安全感 ←→ 宜人性(A)的部分
简化优势:
-
Big Five有30+子维度,SBTI仅15维度 -
更易理解和传播
→依恋理论(Attachment Theory)
情感模型(E)的设计基础:
E1 依恋安全感:- 高分(H)→ 安全型依恋- 中分(M)→ 矛盾型依恋- 低分(L)→ 回避型依恋E2 情感投入度:- 高分(H)→ 焦虑型倾向- 低分(L)→ 回避型倾向E3 边界与依赖:- 高分(H)→ 强边界感(回避型)- 低分(L)→ 弱边界感(焦虑型)
5.3 沙雕文案的心理学原理
为什么沙雕文案有效?
-
降低心理防御:
-
严肃问题 → 受测者会”表演” -
沙雕问题 → 受测者更真实 -
增加参与度:
-
有趣的内容 → 更愿意完成测试 -
降低中途放弃率 -
记忆点:
-
沙雕描述更容易被记住 -
更容易在社交平台传播
示例对比:
|
|
|
|---|---|
|
|
|
|
|
|
|
|
|
六、统计学逻辑分析
6.1 分数分布
每个维度的分数范围:2-6分(2题×1-3分)
理论分布(假设选项随机选择):
得分概率:2分:1/9 ≈ 11.1% (1+1)3分:2/9 ≈ 22.2% (1+2 或 2+1)4分:3/9 ≈ 33.3% (1+3 或 2+2 或 3+1)5分:2/9 ≈ 22.2% (2+3 或 3+2)6分:1/9 ≈ 11.1% (3+3)
实际分布(受题目偏向性影响):
-
极端题目 → 更多人选中间选项 → M偏多 -
中性题目 → 分布更均匀
6.2 稀有度计算(官方数据)
基于维度中间值(M)数量:
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
统计学原理:
-
正态分布中,极端值出现概率低 -
M(中间值)越多 → 越接近”平均值” → 越常见 -
M越少 → 越偏离”平均值” → 越罕见
6.3 曼哈顿距离 vs 欧氏距离
为什么选择曼哈顿距离?
// 曼哈顿距离distance = Σ |userVal_i - typeVal_i|// 欧氏距离distance = √(Σ (userVal_i - typeVal_i)²)
曼哈顿距离优势:
-
计算简单:无需平方和开方 -
可解释性强:每维度的差值直接累加 -
不受极端值影响:平方会放大极端值的影响
示例:
用户:[3,3,3,3,3,3,3,3,3,3,3,3,3,3,3]类型:[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]曼哈顿距离 = 15 × |3-1| = 30欧氏距离 = √(15 × 4) = √60 ≈ 7.75
曼哈顿距离更直观地反映了”总差异”。
七、如何自定义性格测试
7.1 修改题目
编辑 data/questions.json:
{"id": "q1","dim": "S1","text": "你的题目文字","options": [{ "label": "选项A", "value": 1 },{ "label": "选项B", "value": 2 },{ "label": "选项C", "value": 3 }]}
注意事项:
-
每个维度必须有且仅有2道题 -
dim必须是15个维度之一 -
value必须是1、2、3
7.2 添加新人格类型
编辑data/types.json:
{"code": "YOUR","pattern": "HHH-HMH-MHH-HHH-MHM","cn": "你的类型名","intro": "一句话开场白","desc": "详细描述(200-300字)"}
pattern生成方法:
-
确定目标人格的15维度特征 -
按顺序填写H/M/L -
用 -分隔每个模型(3个维度一组)
7.3 调整评分参数
编辑data/config.json:
{"scoring": {"levelThresholds": {"L": [2, 3], // 调整阈值"M": [4, 4],"H": [5, 6]},"fallbackThreshold": 60 // 调整兜底阈值}}
7.4 部署自定义版本
方法1:GitHub Pages
# Fork项目# 修改 data/ 目录下的JSON文件# Settings → Pages → Source: GitHub Actions
方法2:本地构建
git clone https://github.com/pingfanfan/SBTI.gitcd SBTInpm installnpm run dev # 开发npm run build # 构建# 部署 dist/ 目录
八、项目价值与学习建议
8.1 产品设计亮点
|
|
|
|
|---|---|---|
| 数据驱动 |
|
|
| 沙雕文案 |
|
|
| 仪式感 |
|
|
| 可分享 |
|
|
| 隐藏彩蛋 |
|
|
8.2 技术实现亮点
|
|
|
|
|---|---|---|
| 纯函数设计 |
|
|
| 数据与逻辑分离 |
|
|
| 原生Canvas |
|
|
| Vite构建 |
|
|
8.3 学习路径建议
→初级(前端基础)
-
学习项目结构 -
修改JSON定制内容 -
部署到GitHub Pages
→中级(算法理解)
-
理解评分算法 -
理解曼哈顿距离匹配 -
自定义新人格类型
→高级(架构设计)
-
理解数据驱动架构 -
学习纯函数设计 -
扩展新功能(如多语言、数据统计)
8.4 扩展方向
-
数据统计
-
多语言
-
商业化
九、总结
9.1 项目核心价值
SBTI是一个优秀的开源学习案例,展示了:
✅ 如何将心理学理论产品化
-
借鉴Big Five、依恋理论 -
简化为15维度 -
沙雕文案降低门槛
✅ 如何设计有传播力的内容
-
独特的人格代码(CTRL、DEAD) -
灵魂暴击开场白 -
可分享的结果页
✅ 如何实现科学匹配算法
-
曼哈顿距离匹配 -
多维度排序 -
特殊覆盖机制
✅ 如何构建可维护的架构
-
数据驱动(JSON配置) -
纯函数设计(engine.js) -
现代构建工具(Vite)
9.2 适用人群
|
|
|
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
9.3 关键启发
-
简化是门艺术:15维度比30维度更易传播 -
文案决定生死:沙雕文案降低门槛、增加趣味 -
数据驱动设计:JSON配置,代码不变 -
仪式感很重要:结果展示的仪式感决定分享率 -
技术服务于体验:简洁的技术实现,优秀的用户体验
夜雨聆风