
简介本资源是计算机博弈领域经典项目“幻影围棋”的完整源码工程包面向人工智能、算法设计与棋类AI开发的学习者与研究者聚焦围棋AI的核心实现难题——大规模状态空间下的高效搜索与局面评估。压缩包共42个文件含5个核心CPP源文件如MonteCarlo.cpp、Engine.cpp、2个可执行EXE程序、10个OBJ编译中间文件、4个PDB调试符号及配套DOC概要设计文档等完整覆盖MCTS蒙特卡洛树搜索实现、棋局评估函数、多线程优化与VC工程结构包体仅2.22MB轻量但功能完备。已有1376人学习下载适合算法进阶者深入剖析大赛亚军级AI的工程落地细节从VS2008工程配置.sln/.vcproj、内存管理策略.ilk/.pdb到蒙特卡洛模拟主循环与棋形特征处理逻辑均可在源码中逐层验证与复现。 比赛进行到中盘的时候我盯着程序日志里那行落子被拒位置(3,7)非法发呆了好一会儿。在标准围棋的代码里这一行意味着自己下了一个非常蠢的地方检查一下坐标就行。但在幻影棋Phantom Go里被裁判拒绝是一件信息量巨大的事——它告诉我某个我看不见的对手棋子极有可能就藏在那格上。这个项目是我在计算机博弈大赛上拿亚军的参赛代码项目名就叫PlantomGo当时压缩包命名手滑打错了字母就用到了现在。核心任务很简单在不完全信息的规则下写一个能感觉出对手棋形、还敢主动试探对手布防的围棋博弈程序。这篇文章把我从规则引擎、信念管理、蒙特卡洛树搜索改造到比赛现场翻车复盘的完整过程都拆开讲一遍。不管你是准备参加博弈类竞赛还是对信息集博弈感兴趣都应该能从里面找到一些能直接抄走的思路。1. 幻影棋一场让你看不见对手的围棋博弈1.1 从标准围棋到幻影棋的规则变体如果你没接触过幻影棋可以把它想象成围棋版的战争迷雾棋盘还是9路或11路的棋盘黑棋和白棋还是轮流落子目标依旧是圈地但有一个决定性的区别——你看不到对手下的任何一颗棋子。具体游戏流程是这样的轮到己方落子时你在棋盘上选一个坐标提交给裁判。如果这个坐标在规则上不能落子己方已经有子、对手已经有子、禁着点等裁判只会简单通告非法落子然后这一手直接跳过换对手行动。它不会告诉你到底为什么非法。如果落子合法棋盘上会多一颗己方棋子。注意你依然看不到对手的棋只能看到自己的子逐渐增加。唯一能观察到对手棋子的机会是提子当你的一手棋成功提掉对手棋子时被提掉的棋子坐标会展示给你。这是整个游戏中信息量最大的瞬间相当于对手在那片区域的布防一次性暴露了一部分。游戏持续到双方连续虚手或者无法落子为止最后按围棋数子规则判定胜负。比赛用的标准版本还有一个特殊点为了控制不确定性爆炸棋盘一般固定在9路以内。毕竟在完全信息下9路围棋的复杂度和19路比已经不是一个量级再加一层看不见搜索空间会膨胀得非常夸张。1.2 为什么这种博弈比普通围棋更难写普通围棋程序的核心任务是评估当前局面并搜索最佳应对。面对的是唯一确定的棋盘状态所有信息都摊在面前你只需要把棋力估值、搜索深度做得足够好理论上就能接近最优。幻影棋不是这样。你的程序面对的不是一个棋盘而是一堆可能为真的棋盘。你不知道对手第一手下在哪第二手下在哪甚至在对方连续下了二十手之后你连对方大致围的是哪块边都未必清楚。每一次落子都要回答两个问题这个位置下下去如果对方真的有子在那里我这手棋亏多少如果对方没子在这里我获得了多少关于对手布局的确定信息这两个问题在标准围棋里完全没有所以你会发现原本好用的估值函数、征子判断、厚薄评估在幻影棋里全变成了猜。可以引用一个经典的概念来说明这类问题属于信息集博弈。你对当前局面的认知是一个信息集——所有与你看到的信号以及没看到的信号一致的可能局面的集合。决策必须建立在整个集合之上而不是集合中的某一个猜测上。1.3 竞赛场景与赛制约束我们参加的是计算机博弈大赛里的幻影棋组别。这个组别在比赛里属于小众方向参赛队伍不多但规则很严格程序需要在国际象棋/围棋通用的比赛时间控制下运行通常是每方每局有一定的基础时间超时判负。因为不能接触对手程序内部完全黑盒对局所以如何用有限的计算时间换取最可靠的信息和最有价值的落子就成了整个项目要解决的核心命题。比赛代码是用C写的原因很简单博弈搜索要跑大量蒙特卡洛模拟Python在模拟速度上吃亏太多。整个代码量不算大核心模块大概不到两千行但每部分都经过不少实战打磨。下面按模块展开讲。2. 规则引擎裁判反馈的三种信号与判定顺序2.1 裁判接口只返回三种信号很多人在写不完全信息棋类程序时会忽略一件事裁判接口反馈的信息粒度直接决定了整个AI的信念更新方式。如果把非法落子细化成该位置有对方棋子或者该位置是禁着点那游戏难度会大幅下降也就失去了幻影棋的意义。所以我在实现裁判模块时刻意把对外接口收敛成最小的状态enum class MoveResult { SUCCESS, // 落子成功棋盘上多了一颗己方子 ILLEGAL, // 非法落子本回合跳过 PASS, // 虚手 GAME_OVER // 终局 }; class PhantomGoJudge { public: MoveResult Play(int x, int y, int player); void GetLastCaptured(std::vectorint out); // 成功落子导致的提子坐标列表 private: uint64_t black_mask, white_mask; int board_size; };这里有一个设计得很关键的点GetLastCaptured只有在SUCCESS时才可能返回非空。如果一次落子吃掉了对手三颗子那三个坐标就暴露给当前玩家除此之外裁判不会泄露任何对手盘面信息。这样做的好处是AI永远不需要关心裁判内部怎么实现只需根据对外状态更新自己的信念。坏处是任何规则引擎的bug都会以错误反馈的方式污染AI的信念而且这种污染是积累性的一旦出错后面全崩。2.2 提子优先于自杀的判定顺序这是我在写规则引擎时踩过的第一个大坑。标准围棋规则里判断一个落子是否合法要先看这个落子能不能提掉对手的棋子。如果落点周围的对手气数为零那么提子是优先发生的。用通俗的话说哪怕你自己落下去之后气数为零但只要能先把对方提掉这手棋就是合法的。但在幻影棋的规则引擎里因为你看不到对方的棋子初始设计时我犯了一个错误先判断落子位置是否处于己方零气状态如果是就宣布非法再去想提子的事。这个顺序是错的。我举个例子棋盘中腹有一块白色棋子被黑子围得只剩最后一口气白方此时在黑子包围圈的一个空点上落子如果能提掉黑子的一个连通块它自己就算没有气也是合法的。如果引擎先算落子后白棋气为0直接就把它判成非法那这条规则就完全偏离了真实围棋。正确的顺序是判断落点是否已有任意方棋子或者是否超出棋盘边界——如果有则非法。暂时把落子放上去检查周围对手连通块的气如果某个连通块气为0执行提子。提子之后再检查己方这个新落子连通块的气。如果仍然为0则非法否则合法。这个顺序一开始不注意后面会让AI在局部对杀时产生极其荒谬的判断。为了这个bug我把引擎写了三个版本的提子检测最后还是靠一组覆盖角部、边部、中心、多块连通、打劫的单元测试才彻底解决。2.3 引擎正确性的回归测试既然规则引擎的反馈直接影响信念状态那引擎本身不许有任何逻辑错误。我在项目一开始就用简单的可复现对局的测试集来保护它标准9路棋盘上的已知定式对局确保每一步的合法性与真实规则一致。构造禁着点场景例如在角上形成的真眼验证自杀落子被拒绝。制造一个提子场景验证提子后返回的坐标列表与预期一致。连续虚手终局验证GAME_OVER判定。这些测试看起来琐碎但对后面的开发至关重要。因为幻影棋AI的核心代码在不停调整如果规则引擎不稳你会分不清bug出在信念更新还是出在裁判本身。磨刀不误砍柴工这套测试在后期几乎每天都帮我节省数小时的调试时间。3. 信念状态用128份候选棋盘拼出对手的真实棋形3.1 信息集决策面对的是局面集合规则引擎只是骨架幻影棋AI的灵魂在于信念状态Belief State。我在1.2节说过当前玩家能看到的信息不足以唯一确定一个局面只能确定一个可能的局面集合。这个集合在博弈论里称为信息集。决策时必须对整个集合中的每一个可能局面都做评估最后加权平均而不能拍脑袋选定某一个局面。举个例子假设棋盘上黑方对手已经有十颗子但你只知道其中五颗的位置因为被提子暴露过其余五颗完全不知道。那么所有包含你那五颗已知白子之间互不冲突、剩余黑子落在未知空点的配置都是可能局面。程序可以枚举出成千上万种。我们称之为信念集合。从设计上讲信念集合越大决策越精准但计算开销也越大。真实比赛里没法把组合爆炸的局面全部枚举出来所以我用蒙特卡洛采样替代全枚举维护N份采样棋盘每份采样棋盘记载了一个可能的世界。每次获得新的反馈信号就筛掉与信号冲突的采样棋盘保留一致的。决策时就拿这N份棋盘去做模拟最后聚合结果。N的选择我调试过很多值最终定为128。这个数字在比赛时间预算下既能保证信念采样的多样性又不至于让模拟太慢。3.2 信念采样的初始化与淘汰更新初始化阶段我没有把对手想象的棋子数量完全设为0。因为真实对局中对手会落子而你不知道它们在哪里。我的做法是先随机生成对手棋子的数量通常在15到45颗之间。在棋盘上随机选择一个位置如果与己方已知棋子冲突则重新选保证对手棋子之间互不重叠但允许相邻形成连通块。注意不能生成一个违反围棋基本规则的配置比如让对手以一个在真眼里自杀的方式落子因为真实对局中对手不会做非法操作。在比赛中每次己方落子或收到反馈信念状态要根据新信息更新收到非法落子信号这意味着该位置必然有对手棋子或禁着点。由于己方已经明确知道自己的棋子位置且禁着点可以单独判断所以绝大多数情况下非法落子等价于对手棋子在那里。我更新信念的关键步骤就是剔除掉所有在该位置为空的采样棋盘。收到落子成功信号这意味着该位置为空既没有己方子也没有对手子。同样剔除那些该位置有对手棋子的采样棋盘。收到提子坐标列表这是最强的信号。列表中每个坐标一定是对手棋子之前所处的位置而且这些棋子现在已经被提走了。信念更新时把每个采样棋盘上这些坐标强制清空即可。信念更新本质上是一种假设检验保留那些与所有观测一致的世界抛弃不一致的。写出来的核心代码大概长这样class BeliefState { public: static constexpr int NUM_SAMPLES 128; // 86个位置9x9每个采样保存一个完整棋盘掩码 uint64_t sample_own[NUM_SAMPLES]; // 己方棋子掩码 uint64_t sample_opp[NUM_SAMPLES]; // 对手棋子掩码 void ApplyIllegal(int pos) { uint64_t mask 1ULL pos; int alive 0; for (int i 0; i NUM_SAMPLES; i) { if (sample_opp[i] mask) { sample_own[alive] sample_own[i]; sample_opp[alive] sample_opp[i]; alive; } } // 如果活下来的样本过少则重新采样 if (alive NUM_SAMPLES / 4) Resample(); } void ApplySuccess(int pos) { uint64_t mask 1ULL pos; int alive 0; for (int i 0; i NUM_SAMPLES; i) { if (!(sample_opp[i] mask)) { sample_own[alive] sample_own[i]; sample_opp[alive] sample_opp[i]; alive; } } if (alive NUM_SAMPLES / 4) Resample(); } private: void Resample(); };这里有个容易被忽略的细节如果每次更新只做剔除那么随着游戏进行可用的采样棋盘数量会越来越少最后可能只剩几个甚至一个信念多样性塌缩决策也会变得脆弱。3.3 重采样与局部变异防止信念漂移重采样是个很微妙的操作。最朴素的想法是当存活样本数低于阈值时就直接重新随机生成128份棋盘。但我在测试中发现这样做会导致一个严重问题信念漂移。想象一下你本来已经通过几次试探锁定了对手在左边的布局突然重采样把所有信息都丢掉重新随机生成AI的决策会瞬间变得失忆出现大量重复试探已经探明区域的傻棋。我的解决方案是让重采样基于现有样本做局部变异而不是完全随机从当前存活的样本中随机选择一个作为父本。复制父本的完整棋盘。随机移动父本中的1到3颗对手棋子把它们从原位抹去放到附近空位上。以一定概率约20%在随机空位新增一颗对手棋子模拟对手可能的后续落子。重复直到凑够128份。这样的好处是保留下来的信息大部分被继承只添加少量噪声来重新制造多样性相当于在保留大致知道对手在哪的同时又允许AI继续探索局部的不确定性。实测下来局部变异重采样对稳定性的提升非常显著。后来我在比赛复盘时想如果当时用完全随机重采样估计中盘就会开始乱下。4. MCTS改造把试探的期望价值塞进UCB公式4.1 MCTS在信念状态上的基本结构有了信念状态下一步就是下棋决策。我采用的方法是蒙特卡洛树搜索MCTS但针对幻影棋做了比较明显的改造。经典MCTS适合完全信息博弈它维护一棵决策树节点对应棋盘状态选择阶段通过UCB公式在探索和利用之间平衡平均胜率高、访问次数少的分支更受青睐。但幻影棋有问题每个节点无法对应一个确定棋盘因为不同的信念采样棋盘可能拟合出完全不同的合法落子集合。我的处理方式是让每个节点保留两份信息当前玩家视角下的决策历史也就是从开局到当前位置的落子序列。一个轻量级的信念状态快照N128份采样棋盘但只在节点展开时抽样不在树内全量维护。模拟阶段和标准MCTS不太一样从当前节点出发随机从信念状态中抽取一份采样棋盘作为真实局面。轮流落子时对己方来说合法落子列表要根据该采样棋盘计算对对手来说同样要假设对手也只是看得到自己的棋子和反馈所以模拟时对手也遵循幻影棋的规则而不是全知全能。一局模拟结束后把结果胜/负回传给路径上的所有节点。这里有个值得注意的点模拟时对对手的策略用的是简单启发式占空比高、靠近己方棋子的位置概率更高而不是再套一层MCTS。否则计算量会爆炸两次MCTS嵌套在实时对局中根本不现实。4.2 信息增益的计算与调参在幻影棋里光有经典MCTS远远不够。因为经典MCTS的每个分支都假设落子前的局面信息是固定的但幻影棋里你每下一手棋都会改变你之后观察世界的能力。你落下的每一颗子既是一步棋也是一次情报侦察。所以我给UCB公式加了一个信息增益项[ \text{score}(x) \bar{v}_x C \sqrt{\frac{\ln N}{n_x}} \alpha \cdot H(p_x) ]其中(\bar{v}_x) 是分支x的平均局面胜率评估。(C\sqrt{\ln N / n_x}) 是标准UCB探索项。(p_x) 是信念状态中该位置有对手棋子的频率。(H(p_x)) 是该位置信息熵的估计我直接用的 (p_x(1-p_x))。当 (p_x) 在0.5附近时信息熵最大意味着这个位置完全不确定试探价值最高当 (p_x) 接近0或1时信息熵接近0对这个位置试探没有额外价值。这个改造的效果很直接程序会自动倾向在模糊地带落子以获取更清晰的局面信息而不是一上来就去某个角落按照完全信息围棋的思路发展模样。实现上信息增益的计算非常便宜只要在MCTS模拟之前把信念状态中每个空位的对手频率统计一遍即可128份棋盘9路棋盘81个位置几百次操作就够。调参时我踩了不少坑。(\alpha) 值是关键设大了程序会变成一个情报狂魔不停地在无人地带试探舍弃实际实地设小了又退化成普通MCTS几乎不关注信息价值。我在本地对局里从0.05一路测到0.5最终把默认值定在0.15。这个值在9路棋盘上表现得比较均衡开局和中盘能主动探路进入收官阶段后不会因为追求信息而乱下。4.3 避免探路病的动态策略就算默认值选好了运行时还是出现过让人抓狂的情况——程序会在一个已经基本确定没有对手棋子的区域反复下子纯粹为了确认它没有棋子。这种行为叫探路病。后来我找到了一个有效的补救办法把信息增益项的处理改为动态权重。在开局阶段信息最稀缺(\alpha) 保持0.15当游戏进入后半段从双方总落子数超过50手开始(\alpha) 线性衰减到0.03。因为到了后半盘实地和厚薄的基本格局已经形成再花一整手棋去试探一个边角的空点非常不划算。另外还要注意一点幻影棋里的提子事件是极佳的信息源。一次成功的提子不仅获得实地还会揭示对手多颗棋子的位置。所以我的MCTS估值函数里还有一个额外的提子事件奖励如果模拟中在某个分支发生了提子就把这个分支的期望收益额外加上一个小的固定值大约相当于0.5目的价值。这在实践中让程序变得更愿意在可能触发提子的战斗中纠缠而不是一味避战。5. 性能优化位棋盘、多线程与限时出招5.1 用两个uint64表示整块棋盘幻影棋AI要在一个回合内完成尽可能多的模拟性能就是胜负手。9路棋盘81个位置最直观的实现是int board[9][9]每次访问坐标都是数组操作。但在信念状态里有128份采样棋盘如果每个局面都要做一遍81格循环计算量会线性放大。所以我把每个采样棋盘压缩成两个64位整数own_mask所有己方棋子位置为1。opp_mask所有对手棋子位置为1。棋盘坐标(x,y)直接映射到位索引x * 9 y。需要判断某个位置是否为空时只需检查两个掩码对应位是否同时为0。连通块和气的计算也能完全位运算化。比如要找一个落子位置的所在连通块采用泛洪填充flood fill操作如下uint64_t flood_fill(uint64_t mask, uint64_t seed) { uint64_t group seed; uint64_t prev 0; while (group ! prev) { prev group; uint64_t expanded group; // 扩展上、下、左、右这在位棋盘上通过位移实现 expanded | (group 1) ~COL_LEFT_MASK; expanded | (group 1) ~COL_RIGHT_MASK; expanded | (group 9) BOARD_MASK; expanded | (group 9) BOARD_MASK; group expanded mask; } return group; }这段代码里逻辑看起来有点绕其实核心思想就三条左移右移模拟水平相邻加9减9模拟垂直相邻最后与掩码求交集来只保留目标阵营的棋子。它比数组遍历快得多尤其在大量重复模拟时可以节省大量时间。5.2 信念并行模拟的线程模型128份采样棋盘天然适合并行模拟。我在MCTS的模拟阶段把128份样本平均分给8个线程取决于机器核心数每个线程独立跑自己的模拟局数最后把每个分支的胜率统计累加回来。这里有一个经验教训不要在线程内部频繁加锁。一开始我用std::mutex保护每个节点的统计量结果写完后性能下降得很厉害因为线程数量一多锁竞争就成了瓶颈。改成每线程独立统计、最终统一归并的结构后速度几乎线性提升。通用模型struct MCTSNode { int visit_count; double total_value; std::vectorint legal_moves; std::vectorMCTSNode* children; }; void ParallelPlayouts(MCTSNode* root, int num_workers) { std::vectorThreadLocalStats stats(num_workers); std::vectorstd::thread workers; for (int t 0; t num_workers; t) { workers.emplace_back([, t] { for (int i 0; i SIMULATIONS_PER_WORKER; i) { // 从 root 出发每步根据统计选择扩展叶子模拟回传 RunOnePlayout(root, stats[t]); } }); } for (auto w : workers) w.join(); // 归并所有统计到 root for (auto st : stats) { for (auto p : st.path_values) { root-children[p.first]-visit_count p.second.visits; root-children[p.first]-total_value p.second.value; } } }比赛机器一般是4到8核这个方案基本能用满。5.3 时间控制与缓存优化时间控制是博弈程序的生死线。我采用了一种简单但很有效的时间预算方案开局前计算每手的平均可用时间 总时间 / 期望手数9路棋一般预留80到120手。在MCTS搜索循环里每跑完一个完整的批量比如100局模拟检查一次已耗时间。如果超过当前预计可用时间的90%就立刻停止搜索从根节点选访问次数最多的子节点落子。如果在某个节点发现访问次数排名第一的子节点访问次数已经远超第二名比如5倍以上即便时间还没用完也可以提前落子。这个策略叫收敛早停能有效避免把时间浪费在明显没有竞争的局面上。缓存方面我做了一个很值得一说的优化legal_moves缓存。每个采样棋盘的合法落子列表会反复用到所以用Zobrist哈希把own_mask, opp_mask对映射到合法落子列表。实测下来在开局和中盘多次模拟中相同局面反复出现的概率很高缓存命中率能到40%以上。不过这个缓存有个陷阱Zobrist哈希是概率性的理论上可能碰撞。比赛对局中出现碰撞的后果可能是轻微的策略偏差但我后来加了一个校验缓存项除了哈希值还保存完整的own_mask和opp_mask查询时先对比掩码再返回合法落子列表。成本不高可靠性提升很大。6. 比赛复盘与实战经验亚军背后踩过的坑6.1 开局的试探节奏决定中盘下限比赛中最让我印象深刻的体会是开局的试探节奏决定了中盘的下限。幻影棋开局阶段双方都不知道对方的布局。这时候如果只顾埋头按标准围棋的定式发展很可能在十多手之后才发现对手已经在某个方向围了一大块空而你完全不知道等提子事件暴露时已经晚了。所以我的程序在开局阶段把信息增益项的权重拉高主动在一些高不确定性区域落子。比如在棋盘中央、对手可能发展领地的焦点位置先下一子。如果落子成功说明这个位置至少当前是空的能排除一部分可能局面。如果落子被拒就直接确认了这里有一颗对手棋子相当于用一手棋的代价换取了对手一块棋形的重要情报。实际比赛中我观察到程序在开局第5到15手之间会频繁尝试一些看上去完全脱离大局的点。起初我担心这是bug看完整局对局记录后发现正是这些试探让程序在中盘对比赛局面的感知明显好于对手段位类似的程序对手可能因为完全信息围棋的惯性思维开局试探偏少信念更新很慢。6.2 决赛那盘棋我输给了自己的试探最后决赛的对手是一个在完全信息围棋里表现不错的传统MCTS程序但在幻影棋组里他们的信念更新策略比较保守很少主动试探。比赛前期我的程序通过试探获得了一些情报优势中盘局面看起来还不错。但问题出在中后盘转换上当对手棋子的大概位置已经被锁定得差不多时我的程序依然会花费不少模拟次数去反复试探那些其实已经没有太多不确定性的空点导致关键区域的搜索不够深入。决赛那盘棋走到官子阶段因为一个局部的打劫判断失误把提子顺序弄错了最后输掉了半目。复盘时我看数据那盘棋在最后30手我的程序有接近15%的落子在信息熵已经接近0的位置。如果当时能把动态α衰减得更激进一些或者在β收敛检测到不确定度足够低时直接关闭试探逻辑这盘棋大概率能赢回来。这个教训彻底改变了我对不完全信息博弈策略设计的理解信息是手段不是目的。在真实对局中信息是为了让落子更精准地服务于实地和棋形。一旦局面已经明朗就应该切换回标准围棋的逻辑把所有的计算资源都投入到当前棋形的最优应对中去。6.3 代码之外的思考信息价值与棋力的取舍这个项目带给我的最大收获不是亚军的名次而是对信息价值这个抽象概念的具象化认知。在完全信息博弈中每一步棋的价值都体现在棋形的实利和潜在的攻击力上。而在不完全信息博弈里一手棋的价值还要额外加上它减少了多少未来决策的不确定性。这种减法思维非常有用——你每得到一个反馈实际上就从信念集合里剔除了一大批可能局面而你未来的决策质量恰好取决于这个集合的精确程度。所以幻影棋程序本质上是一个决策系统和一个情报系统的耦合。情报系统提供世界认知决策系统基于认知做最优选择。两者之间的权重、比例、切换时机决定了程序在实战中的表现上限。如果你也想尝试写一个幻影棋程序我的建议是先把规则引擎做扎实再把信念更新的逻辑用最直观的方式实现最后才是MCTS和信息增益的调优。别一上来就想做得很智能先跑通一个能稳定不崩、能用信念淘汰制做出基本判断的版本然后再逐步加入试探策略、动态权重这些进阶特性。另外奉劝一句比赛前最好准备一个紧急策略应对最后阶段。我的程序在时间特别紧的时候会退化为只要统计上最占优的落子但这样抓瞎式的冲刺在信息不明的情况下往往不如保守点、少送目来得稳。这些都是赛后复盘才想明白的希望你能少走这些弯路。本文还有配套的精品资源点击获取