LOADING

加载过慢请开启缓存 浏览器默认开启

宝藏位置辅助计算器 - 项目说明

前言

在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即可进入系统,或者这里也有快捷方式点击进入该地址

🎮 使用方法

  1. 选择宝藏形状
  • 在形状选择区域勾选一个或多个形状
  • 形状会高亮显示,表示已选中
  • 可以随时取消选择
  1. 标记已知信息
  • 选择标记模式:
    • 有宝藏:点击格子标记为绿色
    • 无宝藏:点击格子标记为红色
  • 同一格子只能有一种标记
  • 再次点击已标记的格子可取消标记
  1. 查看结果
  • 网格颜色变化显示概率分布
  • 数字表示该格子被覆盖的次数
  • 状态栏显示总可能位置数量
  1. 重置操作
  • 点击”重置用户输入”按钮清除所有标记
  • 重新选择形状开始新分析

✨ 功能特点

  1. 多形状支持
  • 8种不同宝藏形状:L形、T形、IV形、∠形、∟形、p形、凹形、独立双线段形
  • 自动旋转处理:系统自动考虑所有旋转角度(0°、90°、180°、270°)
  • 形状描述:每个形状都有详细说明,帮助理解
  1. 交互式标记
    两种标记模式:
  • ✅ 有宝藏标记(绿色)

  • ❌ 无宝藏标记(红色)

  • 点击切换:直接点击网格切换标记状态

  • 实时反馈:标记后立即重新计算概率

  1. 概率可视化
  • 颜色编码系统:从浅蓝到深蓝颜色梯度表示概率高低

  • 数字显示:每个格子显示被覆盖的次数

  • 图例说明:清晰展示颜色与概率的对应关系

  1. 智能计算
  • 实时计算:选择形状或标记格子后自动重新计算

  • 防抖处理:避免频繁请求,优化性能

  • 过滤算法:基于用户输入智能过滤可能位置

🔧 开发相关

🎫 请求与响应

端点 方法 描述
/ 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)
  • 平移:在棋盘上从左上角开始,一格一格地向右移动,到底了就到下一行

  • 边界检查:确保整个形状都在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.最终:计数器数字 = 这个格子被多少个有效位置覆盖过