前言
在25年的10月份,当时有关注到某游戏的活动界面上线了一个叫做挖宝藏的活动,将宝藏按照指定的摆放形状放于5x5的格子内,挖到宝藏将有对应的奖励。
然而,如果随便点的话,那不仅可能导致消耗大量材料,甚至于一定的财力😕,那怎么能行呢?于是,就有了这个项目。(项目10月份写完的时候还没来得及上传活动就结束了,活动不是第一次出现了,万一以后还有的话就用的上,上传到云仓库吧稳妥些)
项目地址:https://github.com/kylin256/Treasure-Locator
[TOC]
📋 项目简介
这是一个基于 FastAPI 的宝藏位置辅助计算器,用于在5x5网格上根据不同的宝藏形状和用户标记,计算宝藏可能位置的概率分布。该项目通过颜色编码可视化概率,帮助用户进行逻辑推理和决策。
⚙️ 安装
- 安装python 3.11+
- 将项目下载下来,进入项目目录
- 运行命令
pip install -r requirements.txt,等待依赖安装完成 - 运行命令
python main.py - 启动完毕后,在浏览器地址栏输入
127.0.0.1:8000即可进入系统,或者这里也有快捷方式点击进入该地址
🎮 使用方法
- 选择宝藏形状
- 在形状选择区域勾选一个或多个形状
- 形状会高亮显示,表示已选中
- 可以随时取消选择
- 标记已知信息
- 选择标记模式:
- 有宝藏:点击格子标记为绿色
- 无宝藏:点击格子标记为红色
- 同一格子只能有一种标记
- 再次点击已标记的格子可取消标记
- 查看结果
- 网格颜色变化显示概率分布
- 数字表示该格子被覆盖的次数
- 状态栏显示总可能位置数量
- 重置操作
- 点击”重置用户输入”按钮清除所有标记
- 重新选择形状开始新分析
✨ 功能特点
- 多形状支持
- 8种不同宝藏形状:L形、T形、IV形、∠形、∟形、p形、凹形、独立双线段形
- 自动旋转处理:系统自动考虑所有旋转角度(0°、90°、180°、270°)
- 形状描述:每个形状都有详细说明,帮助理解
- 交互式标记
两种标记模式:
✅ 有宝藏标记(绿色)
❌ 无宝藏标记(红色)
点击切换:直接点击网格切换标记状态
实时反馈:标记后立即重新计算概率
- 概率可视化
颜色编码系统:从浅蓝到深蓝颜色梯度表示概率高低
数字显示:每个格子显示被覆盖的次数
图例说明:清晰展示颜色与概率的对应关系
- 智能计算
实时计算:选择形状或标记格子后自动重新计算
防抖处理:避免频繁请求,优化性能
过滤算法:基于用户输入智能过滤可能位置
🔧 开发相关
🎫 请求与响应
| 端点 | 方法 | 描述 |
|---|---|---|
| / | GET | 渲染主页面 |
| /get_positions | POST | 计算概率分布 |
| /health | GET | 健康检查 |
请求示例
POST /get_positions
{
"shapes": ["L", "T"],
"has_treasure": [[0,0], [1,1]],
"no_treasure": [[4,4], [3,3]]
}
响应示例
{
"grid_counts": [
[5, 3, 2, 0, 0],
[4, 6, 3, 1, 0],
[3, 4, 5, 2, 1],
[2, 3, 4, 3, 2],
[1, 2, 3, 2, 1]
],
"all_positions_count": 42
}
🖼️ 软件设计图形
类图
classDiagram
class PositionRequest {
-shapes: List[str]
-has_treasure: List[List[int]]
-no_treasure: List[List[int]]
}
class PositionResponse {
-grid_counts: List[List[int]]
-all_positions_count: int
}
class FastAPIApp {
-app: FastAPI
-templates: Jinja2Templates
-BASE_SHAPES: dict
+read_root()
+calculate_positions()
+health_check()
}
class ShapeProcessor {
-shapes: dict
+rotate_shape()
+get_all_positions_for_shape()
+generate_double_line_positions()
}
class PositionCalculator {
+filter_positions()
+get_probability_positions()
}
PositionRequest --> PositionResponse
FastAPIApp --> PositionRequest
FastAPIApp --> PositionResponse
FastAPIApp --> ShapeProcessor
FastAPIApp --> PositionCalculator
用户操作流程序列图
sequenceDiagram
participant 用户
participant 前端 as 前端JS
participant API as 后端API
participant 引擎 as 计算引擎
用户->>前端: 选择形状/标记格子
前端->>前端: 收集用户输入数据
前端->>API: POST /get_positions (JSON数据)
API->>引擎: 调用计算函数
引擎->>引擎: 生成所有可能位置
引擎->>引擎: 过滤位置(基于用户输入)
引擎->>引擎: 计算概率分布
引擎->>API: 返回计算结果
API->>前端: 返回JSON响应
前端->>前端: 更新网格显示
前端->>用户: 显示概率分布
数据流程图
flowchart TD
A[用户选择形状] --> B[前端收集数据]
C[用户标记格子] --> B
B --> D[发送HTTP请求]
D --> E[后端接收并验证]
E --> F[调用形状处理器]
F --> G[生成所有可能位置]
G --> H[应用过滤算法]
H --> I[计算概率分布]
I --> J[返回JSON结果]
J --> K[前端接收结果]
K --> L[更新网格显示]
L --> M[更新概率颜色]
M --> N[显示最终结果]
🧮 算法解释
一、这是一个拼图寻宝游戏
- 想象一下,你面前有一个5×5的棋盘(就像五子棋棋盘)。现在我给你一个”宝藏形状”的拼图块,这个拼图块由5个小方块组成,可以旋转。你的任务是:
- 已知:宝藏在这个棋盘的某个地方,形状就是我给你看的拼图块
- 线索:棋盘上有些格子你挖过了,有些有宝藏(绿色),有些没有(红色)
- 目标:找出宝藏可能藏在哪些位置
关键点:宝藏不会变形,只会整体旋转(就像拿着一张纸转圈看),而且必须完整地放在棋盘内,不能超出边界。
二、生成所有可能位置
旋转:就像你拿起一个字母”L”形状的饼干,转着看它四个方向
- 对于每个点(x,y),旋转规则:
- 0° :(x, y) → (x, y)
- 90° :(x, y) → (-y, x)
- 180°:(x, y) → (-x, -y)
- 270°:(x, y) → (y, -x)
- 对于每个点(x,y),旋转规则:
平移:在棋盘上从左上角开始,一格一格地向右移动,到底了就到下一行
边界检查:确保整个形状都在5×5的棋盘内,不能”掉出去”
案例演示
假设形状是一个”L”形(竖着4格,第4格向右延伸1格)实际放置后(从(0,0)开始):
行1: ● □ □ □ □
行2: ● □ □ □ □
行3: ● □ □ □ □
行4: ● ● □ □ □
行5: □ □ □ □ □
1.那么坐标为(0,0)(0,1)(0,2)(0,3)(1,3)
2.对每个点应用90°公式 (x, y) → (-y, x):
- (0,0) → (-0, 0) = (0,0)
- (0,1) → (-1, 0) = (-1,0)
- (0,2) → (-2, 0) = (-2,0)
- (0,3) → (-3, 0) = (-3,0)
- (1,3) → (-3, 1) = (-3,1)
3.归一化(消除负数坐标)
归一化目标:将所有坐标平移到第一象限,使得最小坐标为(0,0)
找到最小坐标值:
- 最小x值:min(0, -1, -2, -3, -3) = -3
- 最小y值:min(0, 0, 0, 0, 1) = 0
平移向量:为了消除负数,我们需要加上(3,0)(因为-3+3=0,0+0=0)
对每个点加上(3,0):
- (0,0) + (3,0) = (3,0)
- (-1,0) + (3,0) = (2,0)
- (-2,0) + (3,0) = (1,0)
- (-3,0) + (3,0) = (0,0)
- (-3,1) + (3,0) = (0,1)
归一化后90°旋转的L形状:[(3,0), (2,0), (1,0), (0,0), (0,1)]
- 网格图(5×5):
行0: ● ● ● ● □
行1: ● □ □ □ □
行2: □ □ □ □ □
行3: □ □ □ □ □
行4: □ □ □ □ □
4.计算旋转后的尺寸
- x值:0, 1, 2, 3
- y值:0, 1
- 宽度 = 最大x - 最小x + 1 = 3 - 0 + 1 = 4
- 高度 = 最大y - 最小y + 1 = 1 - 0 + 1 = 2
5.确定起始点范围
- 在5×5网格中:
- 水平移动范围:0 到 (5-宽度) = 0 到 (5-4) = 0到1
- 垂直移动范围:0 到 (5-高度) = 0 到 (5-2) = 0到3
- 所以起始点(start_x, start_y)可以是:
- start_x = 0 或 1
- start_y = 0, 1, 2, 或 3
- 总共2 × 4 = 8个可能的起始点。
6.遍历所有起始点
- 遍历双层循环:
for start_x in [0, 1]:
for start_y in [0, 1, 2, 3]:
# 计算实际位置
可以得到
起始点(0,0):
- (3,0) + (0,0) = (3,0)
- (2,0) + (0,0) = (2,0)
- (1,0) + (0,0) = (1,0)
- (0,0) + (0,0) = (0,0)
- (0,1) + (0,0) = (0,1)
位置1:[(3,0), (2,0), (1,0), (0,0), (0,1)]
行0: ● ● ● ● □
行1: ● □ □ □ □
行2: □ □ □ □ □
行3: □ □ □ □ □
行4: □ □ □ □ □
起始点(0,1):
- (3,0) + (0,1) = (3,1)
- (2,0) + (0,1) = (2,1)
- (1,0) + (0,1) = (1,1)
- (0,0) + (0,1) = (0,1)
- (0,1) + (0,1) = (0,2)
位置2:[(3,1), (2,1), (1,1), (0,1), (0,2)]
行0: □ □ □ □ □
行1: ● ● ● ● □
行2: ● □ □ □ □
行3: □ □ □ □ □
行4: □ □ □ □ □
以此类推最终得到下表
| 起始点 | 实际位置坐标 | 棋盘位置 |
|---|---|---|
| (0,0) | (3,0),(2,0),(1,0),(0,0),(0,1) | 顶部横4,左竖1 |
| (0,1) | (3,1),(2,1),(1,1),(0,1),(0,2) | 下移1行 |
| (0,2) | (3,2),(2,2),(1,2),(0,2),(0,3) | 下移2行 |
| (0,3) | (3,3),(2,3),(1,3),(0,3),(0,4) | 下移3行 |
| (1,0) | (4,0),(3,0),(2,0),(1,0),(1,1) | 右移1列 |
| (1,1) | (4,1),(3,1),(2,1),(1,1),(1,2) | 右移1列,下移1行 |
| (1,2) | (4,2),(3,2),(2,2),(1,2),(1,3) | 右移1列,下移2行 |
| (1,3) | (4,3),(3,3),(2,3),(1,3),(1,4) | 右移1列,下移3行 |
7.进行边界检查
- 对于90°旋转后的L形状,宽度=4,高度=2:
- 当start_x=0时,最右点x=3((3,0)+(0,?)),在棋盘内(棋盘x范围0-4)
- 当start_x=1时,最右点x=4,也在棋盘内
- 当start_y=0时,最下点y=1((0,1)+(?,0)),在棋盘内
- 当start_y=3时,最下点y=4,也在棋盘内
- 所有8个位置都有效,没有超出棋盘边界。
特殊情况处理:独立双线段
独立双线段由两条独立的线段组成:
- 一条3个格子的线段(水平或垂直)
- 一条2个格子的线段(水平或垂直)
- 两条线段可以放在任意位置,只要不重叠
步骤1:生成所有3格线段位置
水平3格线段:
- 对于每一行y(0到4)
- 对于起始x(0到2,因为需要3个连续格子)
- 生成位置:[(x,y), (x+1,y), (x+2,y)]
垂直3格线段:
- 对于每一列x(0到4)
- 对于起始y(0到2)
- 生成位置:[(x,y), (x,y+1), (x,y+2)]
3格线段总数:
- 水平:5行 × 3种起始位置 = 15个
- 垂直:5列 × 3种起始位置 = 15个
- 总共:30个
步骤2:生成所有2格线段位置
水平2格线段:
- 对于每一行y(0到4)
- 对于起始x(0到3)
- 生成位置:[(x,y), (x+1,y)]
垂直2格线段:
- 对于每一列x(0到4)
- 对于起始y(0到3)
- 生成位置:[(x,y), (x,y+1)]
2格线段总数:
- 水平:5行 × 4种起始位置 = 20个
- 垂直:5列 × 4种起始位置 = 20个
- 总共:40个
步骤3:组合所有可能的配对
现在,对于每个3格线段,我们可以搭配每个2格线段,只要它们不重叠。
总组合数:30个3格线段 × 40个2格线段 = 1200个组合
但是!我们需要检查重叠:
- 两条线段不能共享任何格子
- 如果重叠,则丢弃这个组合
步骤4:检查重叠的算法
def has_overlap(line1, line2):
# 将列表转换为集合
set1 = set(line1)
set2 = set(line2)
# 检查交集是否为空
return len(set1.intersection(set2)) > 0
三、筛选过滤 - 根据已知线索排除不可能的位置
过滤规则(两条必须同时满足):
- 必须包含所有绿色格子(有宝藏)
就像拼图必须覆盖所有已知的”宝藏点”
- 绝对不能包含任何红色格子(无宝藏)
就像拼图不能压到任何”安全区”
for 每个可能位置:
if (位置包含所有绿色格子) and (位置不包含任何红色格子):
保留这个位置
else:
扔掉这个位置
四、统计分析 - 计算每个格子的”热度”
1.拿到过滤后的”有效位置清单”
2.创建一个5×5的计数器,全部设为0
3.对每个有效位置:
- 位置覆盖了格子A、B、C、D、E
- 把A的计数器+1,B的计数器+1,…,E的计数器+1
4.最终:计数器数字 = 这个格子被多少个有效位置覆盖过