# 中式黑八台球 (Chinese 8-Ball Pool) - 数据描述
## 1. 画布与布局常量
| 常量名 | 值 | 说明 |
|--------|-----|------|
| CANVAS_W | 980 | 画布总宽度(像素) |
| CANVAS_H | 520 | 画布总高度(像素) |
| RAIL | 38 | 库边宽度(像素) |
| POCKET_R | 19 | 球袋口半径(像素) |
| BALL_R | 13 | 球的碰撞半径(像素) |
### 1.1 台面有效区域
| 常量 | 公式 | 值 | 说明 |
|------|------|-----|------|
| PLAYING_X | RAIL | 38 | 台面左边界 |
| PLAYING_Y | RAIL | 38 | 台面上边界 |
| PLAYING_W | CANVAS_W - RAIL × 2 | 904 | 台面宽度 |
| PLAYING_H | CANVAS_H - RAIL × 2 | 444 | 台面高度 |
台面范围:(38, 38) → (942, 482)
### 1.2 关键标记点
| 常量 | 公式 | 值 | 用途 |
|------|------|-----|------|
| HEAD_STRING_X | 38 + 904 × 0.25 | 264 | 开球线 X 坐标(台面 1/4 处) |
| FOOT_SPOT_X | 38 + 904 × 0.75 | 716 | 置球点 X 坐标(台面 3/4 处) |
| CENTER_Y | 38 + 444 / 2 | 260 | 台面水平中线 |
---
## 2. 物理常量
| 常量 | 值 | 公式/说明 |
|------|-----|----------|
| FRICTION | 0.985 | 每帧速度衰减:v *= 0.985 |
| STOP_THRESHOLD | 0.08 | 速度 ≤ 此值视为球已停止 |
| MAX_POWER | 18 | 最大击球力度值 |
### 2.1 球杆灵敏度参数
| 参数 | 值 | 说明 |
|------|-----|------|
| 旋转平滑因子 | 0.5 | smoothedAimAngle += diff × 0.5,实现 50% 角速度灵敏度 |
| 力度映射系数 | 0.06 | pullDist × 0.06 → aimPower,50% 原始力度灵敏度 |
| 最大回拉距离 | 220 px | 球杆回拉距离上限 |
---
## 3. 球袋位置 (POCKET_POSITIONS)
6 个球袋坐标数组:
| 索引 | 位置描述 | x | y | 说明 |
|------|----------|-----|-----|------|
| 0 | 左上角袋 | 38 | 38 | 四角袋 |
| 1 | 上中袋 | 490 | 36 | 中袋(Y 偏移 -2) |
| 2 | 右上角袋 | 942 | 38 | 四角袋 |
| 3 | 左下角袋 | 38 | 482 | 四角袋 |
| 4 | 下中袋 | 490 | 484 | 中袋(Y 偏移 +2) |
| 5 | 右下角袋 | 942 | 482 | 四角袋 |
< POCKET_R + BALL_R × 0.6 = 26.8 px---
## 4. 球的颜色与分组 (BALL_COLORS)
| 球号 | fill | name | group | 类型说明 |
|------|--------|--------|---------|----------|
| 0 | #f5f5f5 | 母球 | cue | 白色母球,无号码 |
| 1 | #fdd835 | 1-黄 | solid | 全色球 |
| 2 | #1565c0 | 2-蓝 | solid | 全色球 |
| 3 | #c62828 | 3-红 | solid | 全色球 |
| 4 | #6a1b9a | 4-紫 | solid | 全色球 |
| 5 | #e65100 | 5-橙 | solid | 全色球 |
| 6 | #2e7d32 | 6-绿 | solid | 全色球 |
| 7 | #8d2d2d | 7-棕 | solid | 全色球 |
| 8 | #1a1a1a | 8-黑 | eight | 黑八号球 |
| 9 | #fdd835 | 9-黄 | stripe | 半色球 |
| 10 | #1565c0 | 10-蓝 | stripe | 半色球 |
| 11 | #c62828 | 11-红 | stripe | 半色球 |
| 12 | #6a1b9a | 12-紫 | stripe | 半色球 |
| 13 | #e65100 | 13-橙 | stripe | 半色球 |
| 14 | #2e7d32 | 14-绿 | stripe | 半色球 |
| 15 | #8d2d2d | 15-棕 | stripe | 半色球 |
### 4.1 分组汇总
| group | 球号 | 数量 | 渲染特点 |
|-------|------|------|----------|
| solid | 1–7 | 7 | 纯色球体 + 白色号码圆 |
| stripe | 9–15 | 7 | 白色横带 + 色芯 + 双环线 |
| eight | 8 | 1 | 全黑色球体 |
| cue | 0 | 1 | 白色母球(无号码) |
---
## 5. 球体初始化摆放
### 5.1 三角架参数
| 参数 | 值 | 说明 |
|------|-----|------|
| 置球点 | (716, 260) | 8 号球位置 |
| 球间距 | 27 px | BALL_R × 2 + 1 |
| 行向 X 增量 | ≈ 23.38 px | spacing × cos(π/6) |
| 行向 Y 偏移 | 13.5 px | spacing / 2(半间距交错) |
| 排数 | 5 排 | 第 0 排 1 颗 → 第 4 排 5 颗 |
### 5.2 三角架排列顺序
数组 rackOrder = [1, 9, 2, 3, 8, 10, 11, 4, 12, 5, 15, 13, 7, 14, 6]
[1]
[9] [2]
[3] [8] [10]
[11][4] [12][5]
[15][13][7] [14][6]8 号球位于第 3 排中心(索引 4),底角 1 号(全色)和 6 号(半色)混合排列。
### 5.3 母球初始位置
(-50, -50)(屏幕外),等待玩家在开球线后方点击放置。---
## 6. 实体数据结构
### 6.1 球对象 (balls[i])
js
{
number: 0–15, // 球号
x: number, // 中心 X(像素)
y: number, // 中心 Y(像素)
vx: number, // X 方向速度(像素/帧),初始 0
vy: number, // Y 方向速度(像素/帧),初始 0
color: string, // 主色(十六进制,如 "#fdd835")
group: string, // 分组: 'cue' | 'solid' | 'stripe' | 'eight'
radius: 13, // 碰撞半径 = BALL_R
potted: boolean, // 是否已进袋
}### 6.2 击球前状态快照 (pottedBeforeShot)
js
{
solid: number, // 击球前全色球已进袋数量
stripe: number, // 击球前半色球已进袋数量
eight: number, // 击球前 8 号球是否已进袋(0/1)
}---
## 7. 全局状态变量
### 7.1 游戏进程
| 变量 | 类型 | 初始值 | 说明 |
|------|------|--------|------|
| balls | object[] | [] | 全部 16 颗球 |
| currentPlayer | 1|2 | 1 | 当前回合玩家 |
| placingCue | boolean | true | 是否在放置母球阶段 |
| player1Group | string|null | null | 玩家1球组:'solid' / 'stripe' |
| player2Group | string|null | null | 玩家2球组:'solid' / 'stripe' |
| ballsMoving | boolean | false | 是否有球在运动中 |
| gameOver | boolean | false | 游戏是否结束 |
| gameWinner | number|null | null | 获胜玩家编号 |
### 7.2 犯规/规则辅助
| 变量 | 类型 | 初始值 | 说明 |
|------|------|--------|------|
| cueInHand | boolean | false | 对方是否获得自由球(可在场地任意位置放母球) |
| cueFirstContact | object|null | null | 母球首次碰到的目标球对象(犯规检测用) |
| foulOccurred | boolean | false | 本轮是否犯规 |
| ownGroupClearedBeforeShot | boolean | false | 击球前是否已清完自己组别的球 |
### 7.3 球杆操作
| 变量 | 类型 | 初始值 | 说明 |
|------|------|--------|------|
| mouseX | number | -100 | 鼠标在 Canvas 内 X |
| mouseY | number | -100 | 鼠标在 Canvas 内 Y |
| mouseDown | boolean | false | 鼠标按下状态 |
| isDragging | boolean | false | 是否在拖拽蓄力 |
| dragStartX/Y | number | 0 | 拖拽起始坐标 |
| aimLocked | boolean | false | 方向是否已锁定 |
| lockedAimAngle | number | 0 | 锁定的瞄准角度(弧度) |
| smoothedAimAngle | number | 0 | 平滑处理后的瞄准角度(弧度),每次球停止后重置为 0 |
| aimPower | number | 0 | 当前蓄力值(0 ~ 18) |
---
## 8. 物理系统
### 8.1 摩擦减速
每帧对每个运动球:
vx = vx × 0.985
vy = vy × 0.985当
√(vx² + vy²) < 0.08 时归零。### 8.2 库边反弹
| 边界 | 条件 | 处理 |
|------|------|------|
| 左 | x - 13 < 38 | x = 51, vx = |vx| |
| 右 | x + 13 > 942 | x = 929, vx = -|vx| |
| 上 | y - 13 < 38 | y = 51, vy = |vy| |
| 下 | y + 13 > 482 | y = 469, vy = -|vy| |
### 8.3 球间完全弹性碰撞
两球球心距 < 26 px 时触发:
1. 分离重叠量:各沿法向推开 overlap / 2
2. 法向相对速度:vn = (va - vb) · n
3. 若 vn > 0(正在靠近),交换法向速度分量:
a.vx -= vn × nx; a.vy -= vn × ny
b.vx += vn × nx; b.vy += vn × ny### 8.4 首次触球追踪
碰撞循环中记录 cueFirstContact(母球第一个碰撞的非母球对象),用于犯规检测。
### 8.5 进袋检测
球心到任一袋口距离 < 26.8 px → potted = true,速度归零。
### 8.6 球停止检测
allBallsStopped() 检查所有未进袋球速度 < 0.08。停止后 smoothedAimAngle = 0,重置瞄准角度。---
## 9. 球杆绘制
### 9.1 组件尺寸(本地坐标系,正 X 方向)
| 组件 | 长度 (px) | 宽度范围 | 颜色 |
|------|----------|----------|------|
| 皮头 (Tip) | 半径 3.5 | — | #333 / #1a1a1a 双层 |
| 先角 (Ferrule) | 16 | 3.5 | #f5f0e0(象牙白) |
| 前节 (Shaft) | 260 | 3.5→5.5 | 8 段枫木渐变 |
| 接头环 | 8 | 5.5→7 | 深棕/黑双层 |
| 后把 (Butt) | 140 | 7→12 | 6 段深棕木纹渐变 |
球杆总长 = 16 + 260 + 8 + 140 = 424 px
### 9.2 定位计算
aimAngle = aimLocked ? lockedAimAngle : smoothedAimAngle
cueTipWorld = cueBall - (cos(aimAngle), sin(aimAngle)) × (13 + pullBack)- 未拖动时皮头紧贴母球(pullBack = 0)
- 拖动时皮头沿反方向回拉 pullBack px
- 球杆本地坐标原点 = 皮头圆心,杆身向正 X 延伸
### 9.3 瞄准辅助线
- 起点:母球前方表面(cueBall + 方向 × 13)
- 终点:沿方向延伸 350px,截断于台面边界
- 样式:虚线 [8, 6],线宽 1.5
- 颜色:未锁定 rgba(255,255,255,0.35),已锁定 rgba(255,215,0,0.35)(金色)
---
## 10. 台面渲染
### 10.1 桌面层次(从外到内)
| 层级 | 颜色 | 说明 |
|------|------|------|
| 背景 | #3e2723 | 深棕外框 |
| Rail 外 | #5d4037 | 圆角 12px |
| Rail 中 | #4e342e | 圆角 10px |
| Rail 底 | #6d4c41 | 库边填充 |
| 台呢外 | #0d8043 | 深绿 |
| 台呢内 | #0a6b35 | 稍亮绿 |
| 纹理 | rgba(0,0,0,0.02) | 25px 间隔 1×1 点 |
### 10.2 库边线条
3 层描边:#6d4c41 → #8d6e63 → #a1887f,线宽 7px,内缩 5px。
### 10.3 球袋
3 层同心圆:#1a1a1a(r=19) → #0a0a0a(r=15) → #222(r=12),外描边 #5d4037(r=20)。
### 10.4 球体渲染
4 阶径向渐变:
| 位置 | 颜色 |
|------|------|
| 高光 (r×0.05) | #ffffff |
| 15% | lighten(color, 40) |
| 45% | color(本色) |
| 100% | darken(color, 50) |
外加描边 rgba(0,0,0,0.35) + 左上高光渐变 rgba(255,255,255,0.55)。
半色球 (9–15) 额外:clip 裁剪 + 白色横带 (高 r×0.72) + 外环 (r×0.82) + 内环 (r×0.28)。
全色球 (1–8):白色号码圆 (r×0.42),黑色数字 (字号 r×0.87)。
母球 (0):仅渐变 + 高光,无号码。
---
## 11. 规则判定逻辑
### 11.1 犯规条件
| 类型 | 判定 | 判罚 |
|------|------|------|
| 母球入袋 | cue.potted === true | 对方自由球 |
| 首次触球错误 | cueFirstContact.group !== playerGroup(组别已定且未清完自己球) | 对方自由球 |
| 清完后触八错误 | ownGroupCleared && firstContact.group !== 'eight' | 犯规 |
### 11.2 犯规判罚
- 切换 currentPlayer
- cueInHand = true → 任意位置放置母球(不受开球线限制)
- placingCue = true
- 若犯规时 8 号进袋 → 对方获胜
### 11.3 球组分配
首次进球后自动分配:
- newlyPottedSolid > newlyPottedStripe → player1 = solid, player2 = stripe
- newlyPottedStripe > newlyPottedSolid → player1 = stripe, player2 = solid
### 11.4 击球权规则
| 本轮进球情况 | 结果 |
|-------------|------|
| 进球 ≥ 1 且全部是自己球组 | 同玩家继续 |
| 进球 ≥ 1 但含有对方球 | 切换玩家 |
| 无进球 | 切换玩家 |
| 球组未分配时进球 | 同玩家继续 |
### 11.5 胜负条件
| 条件 | 结果 |
|------|------|
| 清完自己球组 + 合法击入黑八 | 🏆 当前玩家获胜 |
| 提前击入黑八(未清完) | 对方获胜 |
| 犯规时击入黑八 | 对方获胜 |
---
## 12. 球杆操作状态机
### 12.1 状态转换
| 状态 | aimLocked | isDragging | 触发操作 |
|------|------------|-------------|----------|
| 瞄准中 | false | false | 移动鼠标 → 球杆平滑跟随 |
| → 方向锁定 | true | false | 单击 → 锁定当前 smoothedAimAngle |
| → 蓄力中 | true | true | 按住拖动 → 球杆回拉 |
| → 击球 | true→false | true→false | 松开 → 发射母球 |
| → 取消 | true→false | true→false | 力度 < 0.5 松手 → 回到瞄准中 |
### 12.2 角度平滑(未锁定时)
rawAngle = atan2(mouseY - cueBall.y, mouseX - cueBall.x)
diff = rawAngle - smoothedAimAngle // 归一化到 [-π, π]
smoothedAimAngle += diff × 0.5 // 50% 灵敏度首次鼠标移动时 smoothedAimAngle === 0 → 直接赋值为 rawAngle。
### 12.3 锁定后蓄力计算
pullDist = max(0, (cueBall - mouse) · (cos(lockedAngle), sin(lockedAngle)))
aimPower = min(pullDist × 0.06, 18)只有沿锁定反方向的拖动分量才计入力度,横向拖动无效。
---
## 13. UI 元素
### 13.1 面板绑定
| DOM ID | 用途 |
|--------|------|
| turnDisplay | 回合指示器(蓝=玩家1,红=玩家2) |
| p1BallRow | 玩家1 迷你球图标行 |
| p2BallRow | 玩家2 迷你球图标行 |
| powerMeter | 力度条填充 div |
| powerLabel | 力度百分比文字 |
| statusMsg | 状态提示信息 |
### 13.2 力度条颜色
| 百分比 | CSS 类 | 渐变 |
|--------|--------|------|
| < 30% | power-low | #4caf50 → #8bc34a |
| 30–59% | power-medium | #ffc107 → #ff9800 |
| ≥ 60% | power-high | #ff5722 → #f44336 |
### 13.3 胜利弹窗
| 元素 | 内容 |
|------|------|
| .victory-trophy | 🎱🏆(弹跳动画) |
| #victoryTitle | "玩家 N 获胜!"(蓝/红) |
| #victorySubtitle | 获胜原因 |
| #victoryRestartBtn | 🔄 新游戏按钮 |
弹窗动画:victoryPulse(缩放 0.7→1.05→1)+ 奖杯 trophyBounce(上下浮动)。
---
## 14. 颜色工具函数
| 函数 | 逻辑 | 用途 |
|------|------|------|
| lightenColor(hex, n) | R/G/B 各加 n(上限 255) | 球体高光 |
| darkenColor(hex, n) | R/G/B 各减 n(下限 0) | 球体暗面、球杆暗色 |
---
## 15. 主循环
frame() {
updatePhysics() // 仅在 !placingCue && !gameOver 时更新
├─ 摩擦减速
├─ 位置更新 + 库边反弹
├─ 球间碰撞 + 首次触球追踪
├─ 进袋检测
└─ checkBallStop() → smoothedAimAngle=0 → onBallsStopped()
render() // 始终绘制(60fps)
├─ drawTable()
├─ drawGhostCueBall()
├─ drawCue()
└─ drawBall() × N
requestAnimationFrame(frame)
}物理跳过条件:placingCue === true || gameOver === true
看了又看
验证报告

目前该文件尚无匹配的数据质量验证程序。我们将在后续版本中提供相应的验证支持,敬请谅解。






