{"id":8118,"date":"2026-09-04T16:07:20","date_gmt":"2026-09-04T08:07:20","guid":{"rendered":"https:\/\/www.xinyixx.com\/?p=8118"},"modified":"2026-09-04T16:07:24","modified_gmt":"2026-09-04T08:07:24","slug":"hs","status":"publish","type":"post","link":"https:\/\/www.xinyixx.com\/index.php\/2026\/09\/04\/hs\/","title":{"rendered":"\u7089\u77f3\u4f20\u8bf4\u6e38\u620f\u673a\u5236\u5206\u6790"},"content":{"rendered":"\n<h1 class=\"wp-block-heading\">\u7089\u77f3\u4f20\u8bf4\u5f15\u64ce\u6df1\u5ea6\u89e3\u5256<\/h1>\n\n\n\n<h2 class=\"wp-block-heading\">\u5173\u952e\u8bcd\u7f16\u5e74\u53f2\u3001\u6838\u5fc3\u7b97\u6cd5\u4e0e\u6570\u636e\u9a71\u52a8\u5361\u724c\u7cfb\u7edf<\/h2>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">\u4e00\u4efd\u9762\u5411\u5de5\u7a0b\u5e08\u7684\u3001\u53ef\u843d\u5730\u7684\u5361\u724c\u6e38\u620f\u5f15\u64ce\u8bbe\u8ba1\u7b14\u8bb0<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u7248\u672c\u8986\u76d6\uff1a2014 \u5e74\u7ecf\u5178\u7248 \u2192 2026 \u5e74\u300a\u7d2b\u7f57\u5170\u76d1\u72f1\u8d8a\u72f1\u300b<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u4ee3\u7801\u8bed\u8a00\uff1aPython 3.11\uff08\u53ef\u76f4\u63a5\u8fd0\u884c\u7684\u6700\u5c0f\u5b9e\u73b0\uff09<\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u9605\u8bfb\u6307\u5357<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">\u8fd9\u4efd\u6587\u6863\u4e0d\u662f\u653b\u7565\uff0c\u4e5f\u4e0d\u662f\u5361\u8868\u3002\u5b83\u8bd5\u56fe\u56de\u7b54\u4e00\u4e2a\u5de5\u7a0b\u95ee\u9898\uff1a<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5f53\u4f60\u624b\u91cc\u6709\u4e00\u4e07\u4e24\u5343\u5f20\u5361\u724c\uff0c\u6bcf\u5f20\u5361\u7684\u6587\u672c\u90fd\u53ef\u80fd\u6539\u5199\u89c4\u5219\u672c\u8eab\uff0c\u4f60\u8981\u600e\u4e48\u5199\u8fd9\u4e2a\u7a0b\u5e8f\uff1f<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u7089\u77f3\u4f20\u8bf4\u662f\u8fd9\u4e2a\u95ee\u9898\u5728\u5546\u4e1a\u4e0a\u6700\u6210\u529f\u7684\u7b54\u6848\u4e4b\u4e00\u3002\u5b83\u8fd0\u884c\u4e86\u5341\u4e8c\u5e74\uff0c\u6bcf\u5e74\u53d1\u5e03\u4e09\u4e2a\u8d44\u6599\u7247\uff0c\u6bcf\u4e2a\u8d44\u6599\u7247\u5f15\u5165 145 \u5f20\u65b0\u5361\u548c 1\uff5e3 \u4e2a\u65b0\u5173\u952e\u8bcd\uff0c\u800c\u6838\u5fc3\u5f15\u64ce\u7684\u9aa8\u67b6\u4ece 2014 \u5e74\u81f3\u4eca\u6ca1\u6709\u63a8\u5012\u91cd\u6765\u8fc7\u3002\u8fd9\u672c\u8eab\u5c31\u662f\u4e00\u4e2a\u503c\u5f97\u7814\u7a76\u7684\u8f6f\u4ef6\u5de5\u7a0b\u6210\u5c31\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u6587\u6863\u5206\u4e03\u4e2a\u90e8\u5206\uff1a<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u90e8\u5206<\/th><th>\u5185\u5bb9<\/th><th>\u4f60\u4f1a\u5f97\u5230\u4ec0\u4e48<\/th><\/tr><\/thead><tbody><tr><td>\u7b2c\u4e00\u90e8\u5206<\/td><td>\u72b6\u6001\u5efa\u6a21\u4e0e\u57fa\u7840\u8bbe\u65bd<\/td><td>\u4e00\u5957 Entity-Component-Tag \u7684\u5b9e\u4f53\u6a21\u578b\uff0c\u7406\u89e3\u4e3a\u4ec0\u4e48&#8221;\u4e00\u5207\u7686\u5b9e\u4f53&#8221;<\/td><\/tr><tr><td>\u7b2c\u4e8c\u90e8\u5206<\/td><td>\u6838\u5fc3\u7b97\u6cd5<\/td><td>\u4e8b\u4ef6\u603b\u7ebf\u3001\u89e6\u53d1\u961f\u5217\u3001\u5149\u73af\u91cd\u7b97\u3001\u6b7b\u4ea1\u7ed3\u7b97\u2014\u2014\u5f15\u64ce\u7684\u5fc3\u810f<\/td><\/tr><tr><td>\u7b2c\u4e09\u90e8\u5206<\/td><td>\u6570\u636e\u9a71\u52a8\u7684\u5361\u724c\u6548\u679c<\/td><td>\u4e00\u5957 Selector\/Action\/Condition DSL\uff0c\u8ba9\u65b0\u5361\u4e0d\u9700\u8981\u6539\u5f15\u64ce<\/td><\/tr><tr><td>\u7b2c\u56db\u90e8\u5206<\/td><td>\u5173\u952e\u8bcd\u7f16\u5e74\u53f2<\/td><td>2014\u21922026 \u6bcf\u4e2a\u5173\u952e\u8bcd\u7684\u8bbe\u8ba1\u610f\u56fe\u3001\u5b9e\u73b0\u65b9\u6848\u4e0e\u4ee3\u7801<\/td><\/tr><tr><td>\u7b2c\u4e94\u90e8\u5206<\/td><td>\u7591\u96be\u65f6\u5e8f\u6848\u4f8b\u7814\u7a76<\/td><td>\u5341\u4e2a\u771f\u5b9e\u5b58\u5728\u8fc7\u7684 bug\/\u88c1\u5b9a\uff0c\u4ee5\u53ca\u5b83\u4eec\u66b4\u9732\u7684\u67b6\u6784\u7f3a\u9677<\/td><\/tr><tr><td>\u7b2c\u516d\u90e8\u5206<\/td><td>\u5de5\u7a0b\u5b9e\u8df5<\/td><td>\u786e\u5b9a\u6027\u968f\u673a\u3001\u7f51\u7edc\u540c\u6b65\u3001\u56de\u653e\u3001AI \u6a21\u62df\u5668\u3001\u6027\u80fd\u4e0e\u6d4b\u8bd5<\/td><\/tr><tr><td>\u7b2c\u4e03\u90e8\u5206<\/td><td>\u53ef\u501f\u9274\u6e05\u5355\u4e0e\u5b8c\u6574\u6e90\u7801<\/td><td>\u4e00\u4e2a\u7ea6 1500 \u884c\u3001\u80fd\u8dd1\u901a\u5b8c\u6574\u5bf9\u5c40\u7684\u8ff7\u4f60\u5f15\u64ce<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">\u5982\u679c\u4f60\u53ea\u6709\u4e8c\u5341\u5206\u949f\uff0c\u8bfb <strong>\u7b2c 5 \u7ae0\uff08\u89e6\u53d1\u961f\u5217 vs \u6808\uff09<\/strong>\u3001<strong>\u7b2c 10 \u7ae0\uff08\u4ece\u786c\u7f16\u7801\u5230 DSL\uff09<\/strong> \u548c <strong>\u7b2c 27 \u7ae0\uff08\u5173\u952e\u8bcd\u7684\u516d\u5927\u8303\u5f0f\uff09<\/strong>\u3002\u8fd9\u4e09\u7ae0\u662f\u6574\u4efd\u6587\u6863\u7684\u9aa8\u67b6\uff0c\u5176\u4f59\u90fd\u662f\u5b83\u4eec\u7684\u5c55\u5f00\u4e0e\u4f50\u8bc1\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u5173\u4e8e\u672c\u6587\u7684\u77e5\u8bc6\u6765\u6e90\u4e0e\u51c6\u786e\u6027<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>\u516c\u5f00\u4e8b\u5b9e<\/strong>\uff08\u7248\u672c\u65f6\u95f4\u7ebf\u3001\u5173\u952e\u8bcd\u6587\u672c\u3001\u5361\u724c\u4ea4\u4e92\u88c1\u5b9a\uff09\u6765\u81ea\u66b4\u96ea\u5b98\u65b9\u516c\u544a\u3001Hearthstone Wiki \u4e0e\u8865\u4e01\u8bf4\u660e\uff0c\u672b\u5c3e\u9644\u6765\u6e90\u3002<\/li>\n\n\n\n<li><strong>\u5185\u90e8\u5b9e\u73b0\u7ec6\u8282<\/strong>\uff08\u66b4\u96ea\u7684\u5b9e\u9645\u7c7b\u540d\u3001\u811a\u672c\u683c\u5f0f\uff09<strong>\u662f\u9006\u5411\u63a8\u65ad\u4e0e\u5408\u7406\u91cd\u6784\uff0c\u4e0d\u662f\u5b98\u65b9\u6e90\u7801<\/strong>\u3002\u7089\u77f3\u5ba2\u6237\u7aef\u662f Unity + C#\uff0c\u793e\u533a\u901a\u8fc7 <code>cardxml0.unity3d<\/code> \u4e0e <code>CardDefs.xml<\/code> \u7684\u89e3\u5305\u83b7\u5f97\u4e86\u5927\u91cf\u7ed3\u6784\u4fe1\u606f\uff0c\u672c\u6587\u5728\u6b64\u57fa\u7840\u4e0a\u91cd\u5efa\u4e86\u4e00\u5957\u7b49\u4ef7\u7684\u3001\u81ea\u6d3d\u7684\u8bbe\u8ba1\uff0c\u5e76\u7528 Python \u8868\u8fbe\u4ee5\u4fbf\u9605\u8bfb\u548c\u8fd0\u884c\u3002<\/li>\n\n\n\n<li>\u51e1\u662f\u6211\u5728\u63a8\u65ad\u7684\u5730\u65b9\uff0c\u4f1a\u660e\u786e\u5199&#8221;<strong>\u63a8\u65ad<\/strong>&#8220;\u3002<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 0 \u7ae0 \u4e3a\u4ec0\u4e48\u8fd9\u4e2a\u7cfb\u7edf\u503c\u5f97\u7814\u7a76<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">0.1 \u89c4\u6a21\uff1a\u4e00\u4e2a\u88ab\u4f4e\u4f30\u7684\u6570\u5b57<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u5230 2026 \u5e74\uff0c\u7089\u77f3\u4f20\u8bf4\u7684\u53ef\u6536\u96c6\u5361\u724c\u603b\u6570\u8d85\u8fc7 <strong>6000 \u5f20<\/strong>\uff08\u6807\u51c6+\u72c2\u91ce+\u9152\u9986\u6218\u68cb+\u4f63\u5175\uff0c\u53bb\u91cd\u540e\u7684&#8221;\u53ef\u73a9\u5b9e\u4f53&#8221;\u66f4\u591a\uff0c\u56e0\u4e3a\u884d\u751f\u7269\u3001\u9644\u9b54\u3001\u4efb\u52a1\u5956\u52b1\u90fd\u662f\u72ec\u7acb\u5b9e\u4f53\uff09\u3002\u8fd9\u4e9b\u5361\u724c\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u5206\u5c5e <strong>11 \u4e2a\u804c\u4e1a<\/strong> + \u4e2d\u7acb<\/li>\n\n\n\n<li>\u62e5\u6709 <strong>\u7ea6 60 \u4e2a\u6b63\u5f0f\u5173\u952e\u8bcd<\/strong><\/li>\n\n\n\n<li>\u6709 <strong>10+ \u79cd\u6cd5\u672f\u5b66\u6d3e<\/strong>\u3001<strong>12 \u79cd\u968f\u4ece\u7c7b\u578b<\/strong><\/li>\n\n\n\n<li>\u6bcf\u4e00\u5f20\u90fd\u53ef\u80fd\u4e0e\u5176\u4ed6\u4efb\u610f\u4e00\u5f20\u4ea7\u751f\u4ea4\u4e92<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">\u5982\u679c\u7528\u6700\u6734\u7d20\u7684\u65b9\u5f0f\u5b9e\u73b0\u2014\u2014\u6bcf\u5f20\u5361\u4e00\u4e2a <code>if<\/code>\u2014\u2014\u4f60\u4f1a\u5f97\u5230\u4e00\u4e2a 6000 \u5206\u652f\u7684\u51fd\u6570\uff0c\u4e14\u4efb\u610f\u4e24\u5f20\u5361\u7684\u4ea4\u4e92\u90fd\u662f\u672a\u5b9a\u4e49\u884c\u4e3a\u3002\u771f\u5b9e\u7684\u7ec4\u5408\u7a7a\u95f4\u662f \\binom{6000}{2} \\approx 1.8 \\times 10^7 \u5bf9\uff0c\u52a0\u4e0a\u4e09\u5f20\u5361\u7684\u4ea4\u4e92\u5c31\u662f 10^{10} \u91cf\u7ea7\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6ca1\u6709\u4efb\u4f55\u6d4b\u8bd5\u7b56\u7565\u80fd\u8986\u76d6\u8fd9\u4e2a\u7a7a\u95f4\u3002<\/strong> \u552f\u4e00\u7684\u51fa\u8def\u662f\uff1a\u8ba9\u4ea4\u4e92\u4ece\u89c4\u5219\u4e2d<strong>\u6d8c\u73b0<\/strong>\uff0c\u800c\u4e0d\u662f\u88ab\u9010\u4e00\u5b9a\u4e49\u3002\u8fd9\u662f\u6574\u4e2a\u8bbe\u8ba1\u7684\u7b2c\u4e00\u6027\u539f\u7406\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">0.2 \u7ea6\u675f\uff1a\u8fd9\u4e0d\u662f\u4e00\u4e2a\u53ef\u4ee5\u968f\u4fbf\u91cd\u6784\u7684\u7cfb\u7edf<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u7089\u77f3\u6709\u51e0\u4e2a\u7279\u6b8a\u7ea6\u675f\uff0c\u8ba9\u5b83\u7684\u67b6\u6784\u9009\u62e9\u4e0e\u4e00\u822c\u6e38\u620f\u4e0d\u540c\uff1a<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>\u670d\u52a1\u5668\u6743\u5a01\u3001\u5ba2\u6237\u7aef\u96f6\u903b\u8f91<\/strong>\u3002\u6240\u6709\u89c4\u5219\u8ba1\u7b97\u5728\u670d\u52a1\u5668\u5b8c\u6210\uff0c\u5ba2\u6237\u7aef\u53ea\u64ad\u653e\u4e00\u4e32&#8221;\u53d1\u751f\u4e86\u4ec0\u4e48&#8221;\u7684\u6307\u4ee4\u6d41\uff08\u793e\u533a\u79f0\u4e3a <code>PowerHistory<\/code>\uff09\u3002\u8fd9\u610f\u5473\u7740<strong>\u5f15\u64ce\u5fc5\u987b\u80fd\u628a\u72b6\u6001\u53d8\u5316\u5e8f\u5217\u5316\u6210\u4e00\u4e2a\u7ebf\u6027\u7684\u3001\u53ef\u91cd\u653e\u7684\u4e8b\u4ef6\u6d41<\/strong>\u3002<\/li>\n\n\n\n<li><strong>\u72c2\u91ce\u6a21\u5f0f\u6c38\u4e0d\u8f6e\u6362<\/strong>\u30022014 \u5e74\u7684\u5361\u5fc5\u987b\u548c 2026 \u5e74\u7684\u5361\u5728\u540c\u4e00\u5c40\u91cc\u6b63\u786e\u4ea4\u4e92\u3002\u8fd9\u6392\u9664\u4e86&#8221;\u65b0\u7248\u672c\u91cd\u5199\u65e7\u5361&#8221;\u7684\u9009\u9879\u3002<\/li>\n\n\n\n<li><strong>\u79fb\u52a8\u7aef<\/strong>\u3002\u5f15\u64ce\u8981\u5728\u5341\u5e74\u524d\u7684\u624b\u673a\u4e0a\u8dd1\u52a8\u753b\uff0c\u72b6\u6001\u540c\u6b65\u7684\u5e26\u5bbd\u548c\u5ba2\u6237\u7aef\u7b97\u529b\u90fd\u5f88\u7d27\u5f20\u3002<\/li>\n\n\n\n<li><strong>\u70ed\u66f4\u65b0\u5361\u724c<\/strong>\u3002\u5e73\u8861\u6027\u8c03\u6574\uff08\u6539\u8d39\u7528\u3001\u6539\u8eab\u6750\u3001\u6539\u6587\u672c\uff09\u5fc5\u987b\u80fd\u901a\u8fc7\u6570\u636e\u4e0b\u53d1\u5b8c\u6210\uff0c\u4e0d\u80fd\u6bcf\u6b21\u90fd\u53d1\u5ba2\u6237\u7aef\u7248\u672c\u3002<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">\u7ea6\u675f 1 \u548c 4 \u8054\u624b\uff0c\u628a\u8bbe\u8ba1\u63a8\u5411\u4e86\u4e00\u4e2a\u5fc5\u7136\u7684\u65b9\u5411\uff1a<strong>\u5361\u724c\u903b\u8f91\u5fc5\u987b\u662f\u6570\u636e\uff0c\u5f15\u64ce\u5fc5\u987b\u662f\u89e3\u91ca\u5668<\/strong>\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">0.3 \u4e0e\u4e07\u667a\u724c\u7684\u5bf9\u6bd4\uff1a\u4e00\u4e2a\u5173\u952e\u7684\u5206\u5c94<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u7406\u89e3\u7089\u77f3\u6700\u5feb\u7684\u65b9\u5f0f\uff0c\u662f\u770b\u5b83\u5728\u54ea\u91cc<strong>\u6545\u610f\u80cc\u79bb<\/strong>\u4e86\u4e07\u667a\u724c\uff08MTG\uff09\u3002<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u7ef4\u5ea6<\/th><th>\u4e07\u667a\u724c<\/th><th>\u7089\u77f3\u4f20\u8bf4<\/th><th>\u5de5\u7a0b\u540e\u679c<\/th><\/tr><\/thead><tbody><tr><td>\u6548\u679c\u89e3\u6790\u7ed3\u6784<\/td><td><strong>\u6808\uff08Stack\uff09<\/strong>\uff0c\u540e\u8fdb\u5148\u51fa\uff0c\u53ef\u54cd\u5e94<\/td><td><strong>\u961f\u5217\uff08Queue\uff09<\/strong>\uff0c\u5148\u8fdb\u5148\u51fa\uff0c\u4e0d\u53ef\u54cd\u5e94<\/td><td>\u7089\u77f3\u4e0d\u9700\u8981\u4f18\u5148\u6743\u7cfb\u7edf\uff0c\u72b6\u6001\u673a\u7b80\u5355\u4e00\u4e2a\u6570\u91cf\u7ea7<\/td><\/tr><tr><td>\u5bf9\u624b\u56de\u5408\u4e92\u52a8<\/td><td>\u77ac\u95f4\u3001\u95ea\u73b0\uff0c\u968f\u65f6\u53ef\u6253<\/td><td>\u53ea\u6709\u5965\u79d8\/\u4ea1\u8bed\u7b49\u9884\u8bbe\u89e6\u53d1<\/td><td>\u7089\u77f3\u4e0d\u9700\u8981\u5728\u56de\u5408\u4e2d\u95f4\u7b49\u5f85\u5bf9\u624b\u8f93\u5165<\/td><\/tr><tr><td>\u72b6\u6001\u52a8\u4f5c<\/td><td>\u6709\u5b8c\u6574 SBA\uff08State-Based Actions\uff09<\/td><td>\u6709\u7b80\u5316\u7248&#8221;\u6b7b\u4ea1\u7ed3\u7b97\u9636\u6bb5&#8221;<\/td><td>\u7089\u77f3\u7684 SBA \u53ea\u5904\u7406\u6b7b\u4ea1\u548c\u51e0\u4e2a\u7279\u4f8b<\/td><\/tr><tr><td>\u624b\u724c\u4e0a\u9650<\/td><td>7 \u5f20\uff0c\u56de\u5408\u672b\u5f03\u724c<\/td><td>10 \u5f20\uff0c\u8d85\u51fa\u76f4\u63a5\u70e7\u6389<\/td><td>\u66f4\u7b80\u5355\uff0c\u65e0\u9700\u5f03\u724c\u51b3\u7b56<\/td><\/tr><tr><td>\u6218\u573a\u5927\u5c0f<\/td><td>\u65e0\u9650<\/td><td>7 \u4e2a\u4f4d\u7f6e\uff0c\u4e14<strong>\u4f4d\u7f6e\u6709\u610f\u4e49<\/strong><\/td><td>\u5f15\u5165\u4e86&#8221;\u4f4d\u7f6e&#8221;\u8fd9\u4e2a\u989d\u5916\u72b6\u6001\u7ef4\u5ea6<\/td><\/tr><tr><td>\u89c4\u5219\u6587\u672c<\/td><td>\u9700\u8981\u5b8c\u6574\u89c4\u5219\u624b\u518c\uff08250+ \u9875\uff09<\/td><td>\u5173\u952e\u8bcd\u81ea\u89e3\u91ca<\/td><td>\u5361\u9762\u6587\u672c\u5373\u89c4\u5219\uff0c\u51cf\u5c11\u6b67\u4e49<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u961f\u5217 vs \u6808\u662f\u6700\u91cd\u8981\u7684\u4e00\u6761\u3002<\/strong> \u5b83\u7684\u76f4\u63a5\u540e\u679c\u662f\uff1a\u7089\u77f3\u91cc&#8221;\u540c\u65f6&#8221;\u89e6\u53d1\u7684\u591a\u4e2a\u6548\u679c\u6309<strong>\u4e0a\u573a\u987a\u5e8f<\/strong>\u4f9d\u6b21\u7ed3\u7b97\uff0c\u800c\u4e14<strong>\u4e2d\u95f4\u4e0d\u5141\u8bb8\u63d2\u5165\u65b0\u7684\u54cd\u5e94<\/strong>\u3002\u8fd9\u8ba9\u6574\u4e2a\u5f15\u64ce\u53d8\u6210\u4e86\u4e00\u4e2a\u53ef\u4ee5\u5355\u7ebf\u7a0b\u8dd1\u5b8c\u7684\u72b6\u6001\u673a\uff0c\u800c\u4e0d\u662f\u4e00\u4e2a\u9700\u8981\u7ef4\u62a4&#8221;\u4f18\u5148\u6743\u8f6e\u8f6c&#8221;\u7684\u4ea4\u4e92\u5f0f\u7cfb\u7edf\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u6211\u4eec\u4f1a\u5728\u7b2c 5 \u7ae0\u8be6\u7ec6\u5c55\u5f00\u8fd9\u4e00\u70b9\uff0c\u56e0\u4e3a\u51e0\u4e4e\u6240\u6709\u7089\u77f3\u7279\u6709\u7684&#8221;\u5947\u602a&#8221;\u88c1\u5b9a\u90fd\u6e90\u4e8e\u5b83\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">0.4 \u672c\u6587\u7684\u8ff7\u4f60\u5f15\u64ce<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u4e3a\u4e86\u8ba9\u8ba8\u8bba\u843d\u5730\uff0c\u5168\u6587\u56f4\u7ed5\u4e00\u4e2a\u6211\u79f0\u4e4b\u4e3a <strong><code>stoneheart<\/code><\/strong> \u7684 Python \u8ff7\u4f60\u5f15\u64ce\u5c55\u5f00\u3002\u5b83\u7684\u89c4\u6a21\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">stoneheart\/<br>\u251c\u2500\u2500 entity.py      # \u5b9e\u4f53\u3001\u6807\u7b7e\u3001\u7ec4\u4ef6         ~180 \u884c<br>\u251c\u2500\u2500 zones.py       # \u533a\u57df\u7cfb\u7edf                 ~120 \u884c<br>\u251c\u2500\u2500 events.py      # \u4e8b\u4ef6\u603b\u7ebf\u4e0e\u89e6\u53d1\u961f\u5217       ~200 \u884c<br>\u251c\u2500\u2500 auras.py       # \u5149\u73af\u4e0e\u6301\u7eed\u6548\u679c\u91cd\u7b97       ~150 \u884c<br>\u251c\u2500\u2500 actions.py     # Action DSL               ~320 \u884c<br>\u251c\u2500\u2500 selectors.py   # Selector DSL             ~180 \u884c<br>\u251c\u2500\u2500 game.py        # \u4e3b\u5faa\u73af\u3001\u9636\u6bb5\u3001\u6b7b\u4ea1\u7ed3\u7b97   ~380 \u884c<br>\u251c\u2500\u2500 keywords.py    # \u516d\u5341\u4e2a\u5173\u952e\u8bcd\u7684\u5b9e\u73b0       ~420 \u884c<br>\u2514\u2500\u2500 cards.py       # \u5361\u724c\u5b9a\u4e49\uff08\u6570\u636e\uff09         ~\u6309\u9700<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u5b83\u80fd\u505a\u5230\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u5b8c\u6574\u7684\u51fa\u724c \/ \u653b\u51fb \/ \u82f1\u96c4\u6280\u80fd \/ \u56de\u5408\u6d41\u8f6c<\/li>\n\n\n\n<li>\u6218\u543c\u3001\u4ea1\u8bed\u3001\u5149\u73af\u3001\u5965\u79d8\u3001\u5723\u76fe\u3001\u5632\u8bbd\u3001\u7a81\u88ad\u3001\u51b2\u950b\u3001\u5267\u6bd2\u3001\u5438\u8840\u3001\u98ce\u6012\u3001\u6f5c\u884c\u3001\u6c89\u9ed8\u3001\u51bb\u7ed3\u2026\u2026<\/li>\n\n\n\n<li>\u53d1\u73b0\u3001\u9002\u5e94\u3001\u4efb\u52a1\u3001\u4efb\u52a1\u94fe\u3001\u6cd5\u672f\u8ff8\u53d1\u3001\u72c2\u4e71\u3001\u8d85\u6740\u3001\u8363\u8a89\u51fb\u6740\u3001\u78c1\u529b\u3001\u56de\u54cd\u3001\u53ef\u4ea4\u6613\u3001\u6253\u635e\u3001\u6316\u6398\u3001\u953b\u9020\u3001\u704c\u6ce8\u3001\u5012\u5e26\u2026\u2026<\/li>\n\n\n\n<li>\u786e\u5b9a\u6027\u968f\u673a\uff08\u540c\u4e00 seed \u5b8c\u6574\u590d\u73b0\uff09<\/li>\n\n\n\n<li>\u4e8b\u4ef6\u6d41\u5e8f\u5217\u5316\uff08\u53ef\u7528\u4e8e\u56de\u653e\u4e0e\u7f51\u7edc\u540c\u6b65\uff09<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">\u7b2c\u4e03\u90e8\u5206\u7ed9\u51fa\u5b8c\u6574\u53ef\u8fd0\u884c\u7684\u6e90\u7801\u3002\u4e2d\u95f4\u7ae0\u8282\u7684\u4ee3\u7801\u7247\u6bb5\u90fd\u662f\u4ece\u5b83\u91cc\u9762\u6458\u51fa\u6765\u7684\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">\u7b2c\u4e00\u90e8\u5206 \u57fa\u7840\u8bbe\u65bd\uff1a\u72b6\u6001\u5982\u4f55\u88ab\u5efa\u6a21<\/h1>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 1 \u7ae0 \u4e00\u5207\u7686\u5b9e\u4f53\uff1aEntity-Component-Tag \u6a21\u578b<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">1.1 \u7b2c\u4e00\u4e2a\u8bbe\u8ba1\u51b3\u7b56\uff1a\u5361\u724c\u4e0d\u662f\u5bf9\u8c61<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u65b0\u624b\u5b9e\u73b0\u5361\u724c\u6e38\u620f\u65f6\u7684\u7b2c\u4e00\u53cd\u5e94\u662f\u9762\u5411\u5bf9\u8c61\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class Minion:<br>    def __init__(self, name, cost, attack, health):<br>        ...<br>class Spell:<br>    ...<br>class Weapon:<br>    ...<br>class TauntMinion(Minion):   # \u2190 \u707e\u96be\u4ece\u8fd9\u91cc\u5f00\u59cb<br>    ...<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u8fd9\u6761\u8def\u4f1a\u5728\u7b2c\u4e09\u5f20\u5361\u5c31\u5d29\u6e83\u3002\u539f\u56e0\u5f88\u7b80\u5355\uff1a<strong>\u7089\u77f3\u91cc\u7684\u7c7b\u578b\u662f\u53ef\u53d8\u7684<\/strong>\u3002<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u300a\u53d8\u5f62\u672f\u300b\u628a\u968f\u4ece\u53d8\u6210 1\/1 \u7ef5\u7f8a\u2014\u2014\u7ef5\u7f8a\u662f<strong>\u540c\u4e00\u4e2a\u5b9e\u4f53<\/strong>\u5417\uff1f\uff08\u4e0d\u662f\uff0c\u662f\u65b0\u5b9e\u4f53\uff0c\u4f46\u5bf9\u624b\u7684&#8221;\u4f60\u7684\u968f\u4ece\u6b7b\u4ea1\u65f6&#8221;\u4e0d\u89e6\u53d1\uff09<\/li>\n\n\n\n<li>\u300a\u5e7d\u7075\u4e4b\u722a\u300b\u8ba9\u968f\u4ece\u53d8\u6210\u6b66\u5668\uff1f<\/li>\n\n\n\n<li>\u300a\u6d77\u5de8\u4eba\u300b\u7684\u8d39\u7528\u4f1a\u968f\u573a\u4e0a\u968f\u4ece\u6570\u53d8\u5316\u2014\u2014\u8d39\u7528\u662f\u5c5e\u6027\u8fd8\u662f\u8ba1\u7b97\u7ed3\u679c\uff1f<\/li>\n\n\n\n<li>\u300a\u94dc\u987b\u300b\u7ed9\u968f\u4ece\u52a0\u5632\u8bbd\u2014\u2014\u4f60\u8981 <code>TauntMinion.__class__<\/code> \u6362\u6389\u5417\uff1f<\/li>\n\n\n\n<li>\u4e00\u4e2a\u88ab\u6c89\u9ed8\u7684\u3001\u62e5\u6709\u5723\u76fe\u7684\u3001\u88ab\u51bb\u7ed3\u7684\u3001\u6709 +3\/+3 \u9644\u9b54\u7684\u3001\u88ab\u300a\u5fc3\u7075\u63a7\u5236\u300b\u62a2\u8fc7\u7684\u968f\u4ece\uff0c\u662f\u4ec0\u4e48\u7c7b\uff1f<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">\u6b63\u786e\u7b54\u6848\u662f\uff1a<strong>\u6ca1\u6709\u7c7b\u3002\u53ea\u6709\u5b9e\u4f53\u548c\u6807\u7b7e\u3002<\/strong><\/p>\n\n\n\n<h3 class=\"wp-block-heading\">1.2 \u5b9e\u4f53\uff08Entity\uff09<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u4e00\u4e2a\u5b9e\u4f53\u662f\u4e00\u4e2a\u6574\u6570 ID + \u4e00\u5f20\u6807\u7b7e\u8868\u3002\u4ec5\u6b64\u800c\u5df2\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"># entity.py<br>from __future__ import annotations<br>from enum import IntEnum, auto<br>from typing import Any<br><br>class Tag(IntEnum):<br>    \"\"\"\u6240\u6709\u53ef\u67e5\u8be2\u7684\u72b6\u6001\u7ef4\u5ea6\u3002\u5bf9\u5e94\u7089\u77f3\u5185\u90e8\u7684 GAME_TAG \u679a\u4e3e\u3002\"\"\"<br>    # ---- \u8eab\u4efd ----<br>    CARD_ID       = auto()   # \u5361\u724c\u5b9a\u4e49 ID\uff0c\u5982 \"EX1_001\"<br>    ENTITY_ID     = auto()   # \u672c\u5c40\u552f\u4e00\u5b9e\u4f53 ID<br>    CONTROLLER    = auto()   # \u5f53\u524d\u63a7\u5236\u8005\uff08\u53ef\u88ab\u593a\u53d6\uff09<br>    OWNER         = auto()   # \u539f\u59cb\u62e5\u6709\u8005\uff08\u7528\u4e8e\"\u8fd4\u56de\u5176\u62e5\u6709\u8005\u624b\u724c\"\uff09<br>    ZONE          = auto()   # \u6240\u5728\u533a\u57df<br>    ZONE_POSITION = auto()   # \u533a\u57df\u5185\u4f4d\u7f6e\uff081-based\uff0c\u6218\u573a\/\u624b\u724c\u6709\u610f\u4e49\uff09<br>    CARDTYPE      = auto()   # MINION \/ SPELL \/ WEAPON \/ HERO \/ HERO_POWER \/ ENCHANTMENT \/ LOCATION<br>    CARDRACE      = auto()   # \u968f\u4ece\u7c7b\u578b\uff08\u91ce\u517d\/\u673a\u68b0\/\u6076\u9b54\/\u5143\u7d20\/\u4ea1\u7075\/\u6d77\u76d7...\uff09<br>    CLASS         = auto()   # \u804c\u4e1a<br>    RARITY        = auto()<br>    SPELL_SCHOOL  = auto()   # \u6cd5\u672f\u5b66\u6d3e\uff08\u706b\u7130\/\u51b0\u971c\/\u6697\u5f71\/\u81ea\u7136\/\u5965\u672f\/\u795e\u5723\uff09<br><br>    # ---- \u6570\u503c ----<br>    COST          = auto()<br>    ATK           = auto()<br>    HEALTH        = auto()   # \u6700\u5927\u751f\u547d\u503c<br>    DAMAGE        = auto()   # \u5df2\u53d7\u5230\u7684\u4f24\u5bb3\uff08\u6ce8\u610f\uff1a\u4e0d\u662f\u5f53\u524d\u751f\u547d\u503c\uff01\uff09<br>    ARMOR         = auto()<br>    DURABILITY    = auto()<br><br>    # ---- \u5e03\u5c14\u5173\u952e\u8bcd ----<br>    TAUNT         = auto()<br>    DIVINE_SHIELD = auto()<br>    CHARGE        = auto()<br>    RUSH          = auto()<br>    STEALTH       = auto()<br>    POISONOUS     = auto()<br>    LIFESTEAL     = auto()<br>    WINDFURY      = auto()<br>    MEGA_WINDFURY = auto()<br>    FROZEN        = auto()<br>    IMMUNE        = auto()<br>    ELUSIVE       = auto()      # \u4e0d\u53ef\u88ab\u6cd5\u672f\/\u6280\u80fd\u9009\u4e2d<br>    REBORN        = auto()<br>    SILENCED      = auto()<br>    DORMANT       = auto()      # \u4f11\u7720<br>    UNTOUCHABLE   = auto()      # \u5b8c\u5168\u4e0d\u53ef\u4ea4\u4e92\uff08\u5982\u4efb\u52a1\u3001\u5730\u6807\u7684\u67d0\u4e9b\u72b6\u6001\uff09<br><br>    # ---- \u8fd0\u884c\u65f6\u72b6\u6001 ----<br>    EXHAUSTED       = auto()   # \u672c\u56de\u5408\u4e0d\u80fd\u653b\u51fb\uff08\u53ec\u5524\u5931\u8c03\uff09<br>    NUM_ATTACKS     = auto()   # \u672c\u56de\u5408\u5df2\u653b\u51fb\u6b21\u6570<br>    NUM_TURNS_IN_PLAY = auto()<br>    SPELLPOWER      = auto()<br>    ATTACHED        = auto()   # \u9644\u9b54\u5bbf\u4e3b<br>    TO_BE_DESTROYED = auto()   # \u6b7b\u4ea1\u6807\u8bb0\uff08\u7b49\u5f85\u7ed3\u7b97\uff09<br>    PENDING_DESTROY = auto()<br><br>    # ---- \u73a9\u5bb6\u7ea7 ----<br>    MANA_CRYSTALS   = auto()<br>    MANA_AVAILABLE  = auto()<br>    OVERLOAD_OWED   = auto()   # \u4e0b\u56de\u5408\u8981\u9501\u7684\u6c34\u6676<br>    OVERLOAD_LOCKED = auto()   # \u672c\u56de\u5408\u5df2\u9501\u7684\u6c34\u6676<br>    FATIGUE         = auto()<br>    COMBO_ACTIVE    = auto()<br>    CORPSES         = auto()   # \u5c38\u5757\uff08\u4ea1\u7075\u5929\u707e\uff09<br><br><br>class Entity:<br>    __slots__ = (\"id\", \"tags\", \"game\", \"_deathrattles\", \"_triggers\", \"_enchantments\")<br><br>    def __init__(self, eid: int, game: \"Game\", **tags):<br>        self.id = eid<br>        self.game = game<br>        self.tags: dict[Tag, Any] = {}<br>        self._enchantments: list[\"Entity\"] = []<br>        self.tags.update(tags)<br><br>    # ---- \u6807\u7b7e\u8bfb\u5199 ----<br>    def __getitem__(self, t: Tag):<br>        return self.tags.get(t, 0)<br><br>    def __setitem__(self, t: Tag, v):<br>        old = self.tags.get(t, 0)<br>        if old == v:<br>            return<br>        self.tags[t] = v<br>        # \u6807\u7b7e\u53d8\u5316\u672c\u8eab\u662f\u4e00\u4e2a\u53ef\u89c2\u6d4b\u4e8b\u4ef6\uff0c\u8fd9\u662f\u7f51\u7edc\u540c\u6b65\u7684\u57fa\u7840<br>        self.game.record_tag_change(self, t, old, v)<br><br>    def has(self, t: Tag) -&gt; bool:<br>        return bool(self[t])<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u6ce8\u610f <code>DAMAGE<\/code> \u8fd9\u4e2a\u8bbe\u8ba1\u3002\u7089\u77f3<strong>\u4e0d\u5b58\u50a8&#8221;\u5f53\u524d\u751f\u547d\u503c&#8221;<\/strong>\uff0c\u800c\u662f\u5b58\u50a8&#8221;\u6700\u5927\u751f\u547d\u503c&#8221;\u548c&#8221;\u5df2\u53d7\u4f24\u5bb3&#8221;\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">    @property<br>    def current_health(self) -&gt; int:<br>        return self[Tag.HEALTH] - self[Tag.DAMAGE]<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4e3a\u4ec0\u4e48\uff1f<\/strong> \u56e0\u4e3a\u8fd9\u6837 <code>+X\/+X<\/code> \u5149\u73af\u7684\u52a0\u51cf\u6cd5\u662f\u5e42\u7b49\u7684\u3002\u60f3\u8c61\u4e00\u4e0b\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>5\/5 \u968f\u4ece\u53d7\u5230 3 \u70b9\u4f24\u5bb3 \u2192 <code>HEALTH=5, DAMAGE=3<\/code>\uff0c\u5f53\u524d 2 \u8840<\/li>\n\n\n\n<li>\u300a\u738b\u8005\u795d\u798f\u300b\u7ed9 +4\/+4 \u2192 <code>HEALTH=9, DAMAGE=3<\/code>\uff0c\u5f53\u524d 6 \u8840 \u2713<\/li>\n\n\n\n<li>\u88ab\u6c89\u9ed8 \u2192 \u79fb\u9664\u9644\u9b54\uff0c<code>HEALTH=5, DAMAGE=3<\/code>\uff0c\u5f53\u524d 2 \u8840 \u2713<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">\u5982\u679c\u5b58\u7684\u662f&#8221;\u5f53\u524d\u751f\u547d\u503c&#8221;\uff0c\u6c89\u9ed8\u65f6\u4f60\u5c31\u4e0d\u77e5\u9053\u8be5\u6263\u56de\u591a\u5c11\u4e86\u3002\u8fd9\u662f\u4e00\u4e2a<strong>\u6781\u5176\u91cd\u8981<\/strong>\u7684\u5efa\u6a21\u51b3\u7b56\uff0c\u51e0\u4e4e\u6240\u6709\u5361\u724c\u6e38\u620f\u5f15\u64ce\u90fd\u662f\u8fd9\u4e48\u505a\u7684\uff08MTG \u7684\u89c4\u5219\u6587\u672c\u91cc\u4e5f\u662f&#8221;\u4f24\u5bb3\u6807\u8bb0&#8221;\uff09\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u5b83\u8fd8\u89e3\u91ca\u4e86\u4e00\u4e2a\u65b0\u624b\u5e38\u89c1\u7684\u56f0\u60d1\uff1a\u4e3a\u4ec0\u4e48&#8221;\u6c89\u9ed8\u4e00\u4e2a\u88ab\u300a\u66b4\u98ce\u57ce\u52c7\u58eb\u300b\u5149\u73af\u52a0\u8fc7\u8840\u3001\u4e14\u5df2\u53d7\u4f24\u7684\u968f\u4ece&#8221;\u53ef\u80fd\u76f4\u63a5\u6740\u6b7b\u5b83\uff1f\u56e0\u4e3a <code>HEALTH<\/code> \u964d\u56de\u539f\u503c\u540e <code>HEALTH - DAMAGE &lt;= 0<\/code>\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">1.3 \u6807\u7b7e\u7684\u4e09\u5c42\u6765\u6e90\uff1aBase \/ Enchantment \/ Aura<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u4e00\u4e2a\u968f\u4ece\u7684\u653b\u51fb\u529b\u4ece\u54ea\u6765\uff1f\u81f3\u5c11\u4e09\u4e2a\u5730\u65b9\uff1a<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>\u57fa\u7840\u503c<\/strong>\uff1a\u5361\u724c\u5b9a\u4e49\u91cc\u5199\u7684\uff08<code>EX1_001<\/code> \u57fa\u7840 3\/2\uff09<\/li>\n\n\n\n<li><strong>\u9644\u9b54\uff08Enchantment\uff09<\/strong>\uff1a\u6301\u4e45\u7684\u3001\u9644\u7740\u5728\u5b9e\u4f53\u4e0a\u7684\u4fee\u9970\uff08\u300a\u738b\u8005\u795d\u798f\u300b+4\/+4\uff0c\u6c38\u4e45\u751f\u6548\u76f4\u5230\u6c89\u9ed8\u6216\u6b7b\u4ea1\uff09<\/li>\n\n\n\n<li><strong>\u5149\u73af\uff08Aura\uff09<\/strong>\uff1a\u7531\u53e6\u4e00\u4e2a\u5b9e\u4f53\u6301\u7eed\u63d0\u4f9b\u7684\u3001<strong>\u6761\u4ef6\u6027<\/strong>\u4fee\u9970\uff08\u300a\u66b4\u98ce\u57ce\u52c7\u58eb\u300b\u7ed9\u5176\u4ed6\u968f\u4ece +1\/+1\uff0c\u6e90\u6b7b\u4ea1\u7acb\u5373\u5931\u6548\uff09<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">\u7089\u77f3\u7684\u505a\u6cd5\u662f\u628a\u4e09\u8005<strong>\u5728\u540c\u4e00\u4e2a\u6807\u7b7e\u4e0a\u53e0\u52a0\u8ba1\u7b97<\/strong>\uff0c\u4f46\u7528\u4e0d\u540c\u7684\u673a\u5236\u7ef4\u62a4\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class TagView:<br>    \"\"\"\u6807\u7b7e\u7684\u4e09\u5c42\u6c42\u503c\uff1abase -&gt; enchantments -&gt; auras\"\"\"<br><br>    @staticmethod<br>    def compute(entity: Entity, tag: Tag) -&gt; int:<br>        base = entity.tags.get(tag, 0)<br>        if entity.has(Tag.SILENCED) and tag in SILENCEABLE_TAGS:<br>            base = entity.base_definition.get(tag, 0)<br>            return base + entity.game.aura_bus.delta(entity, tag)<br>        ench = sum(e.effect_delta(tag) for e in entity._enchantments)<br>        aura = entity.game.aura_bus.delta(entity, tag)<br>        return base + ench + aura<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u4f46\u771f\u5b9e\u5f15\u64ce\u4e0d\u4f1a\u6bcf\u6b21\u8bfb\u6807\u7b7e\u90fd\u91cd\u7b97\u2014\u2014\u592a\u6162\u4e86\u3002\u5b9e\u9645\u505a\u6cd5\uff08<strong>\u63a8\u65ad<\/strong>\uff0c\u57fa\u4e8e\u793e\u533a\u5bf9 PowerHistory \u7684\u89c2\u5bdf\uff09\u662f\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>\u9644\u9b54<\/strong>\uff1a\u76f4\u63a5\u4fee\u6539\u5bbf\u4e3b\u7684 tag \u503c\uff08\u5199\u5165\u5f0f\uff09\uff0c\u9644\u9b54\u5b9e\u4f53\u8bb0\u5f55\u81ea\u5df1\u6539\u4e86\u4ec0\u4e48\uff0c\u4ee5\u4fbf\u79fb\u9664\u65f6\u56de\u6eda\u3002<\/li>\n\n\n\n<li><strong>\u5149\u73af<\/strong>\uff1a\u7531 <code>AuraBus<\/code> \u5728\u6bcf\u6b21&#8221;\u72b6\u6001\u53d8\u5316&#8221;\u540e\u7edf\u4e00\u91cd\u7b97\uff0c\u628a\u5dee\u503c\u5199\u5165 tag\u3002<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">\u4e5f\u5c31\u662f\u8bf4\uff0c<code>entity[Tag.ATK]<\/code> \u8bfb\u5230\u7684<strong>\u6c38\u8fdc\u662f\u6700\u7ec8\u503c<\/strong>\uff0c\u91cd\u7b97\u53d1\u751f\u5728\u5199\u5165\u4fa7\u800c\u4e0d\u662f\u8bfb\u53d6\u4fa7\u3002\u8fd9\u662f\u4e00\u4e2a\u5178\u578b\u7684&#8221;\u8bfb\u591a\u5199\u5c11 \u2192 \u7528\u7a7a\u95f4\/\u5199\u5165\u6362\u8bfb\u53d6\u901f\u5ea6&#8221;\u7684\u6743\u8861\u3002\u7b2c 6 \u7ae0\u4f1a\u8be6\u7ec6\u8bb2\u91cd\u7b97\u7684\u65f6\u673a\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">1.4 \u7ec4\u4ef6\uff1a\u4e3a\u4ec0\u4e48\u6807\u7b7e\u4e0d\u591f<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u6807\u7b7e\u53ea\u80fd\u5b58\u6574\u6570\u3002\u4f46\u6709\u4e9b\u4e1c\u897f\u4e0d\u662f\u6574\u6570\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u4ea1\u8bed\u8981\u6267\u884c\u4ec0\u4e48\uff1f<\/li>\n\n\n\n<li>\u6218\u543c\u7684\u76ee\u6807\u7b5b\u9009\u6761\u4ef6\uff1f<\/li>\n\n\n\n<li>\u89e6\u53d1\u5668\u8ba2\u9605\u4e86\u54ea\u4e9b\u4e8b\u4ef6\uff1f<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">\u8fd9\u4e9b\u662f<strong>\u884c\u4e3a<\/strong>\uff0c\u5b58\u5728\u5361\u724c\u5b9a\u4e49\uff08<code>CardDef<\/code>\uff09\u91cc\uff0c\u5b9e\u4f53\u53ea\u6301\u6709\u4e00\u4e2a\u6307\u5411\u5b9a\u4e49\u7684\u5f15\u7528\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">from dataclasses import dataclass, field<br>from typing import Callable<br><br>@dataclass(frozen=True)<br>class CardDef:<br>    card_id: str<br>    name: str<br>    cost: int<br>    card_type: str<br>    attack: int = 0<br>    health: int = 0<br>    text: str = \"\"<br>    race: str | None = None<br>    klass: str = \"NEUTRAL\"<br>    spell_school: str | None = None<br><br>    # \u9759\u6001\u5173\u952e\u8bcd\uff08\u8fdb\u573a\u5373\u62e5\u6709\uff09<br>    keywords: frozenset[str] = frozenset()<br><br>    # \u884c\u4e3a\u811a\u672c\uff1a\u5168\u90e8\u662f Action \u6811\uff0c\u4e0d\u662f Python \u51fd\u6570\uff08\u89c1\u7b2c\u4e09\u90e8\u5206\uff09<br>    battlecry:  \"Action | None\" = None<br>    deathrattle:\"Action | None\" = None<br>    combo:      \"Action | None\" = None<br>    spell:      \"Action | None\" = None       # \u6cd5\u672f\u6548\u679c<br>    triggers:   tuple[\"TriggerDef\", ...] = ()<br>    auras:      tuple[\"AuraDef\", ...] = ()<br>    targeting:  \"Selector | None\" = None      # \u6218\u543c\/\u6cd5\u672f\u7684\u5408\u6cd5\u76ee\u6807<br>    choose_one: tuple[str, ...] = ()          # \u6289\u62e9\u7684\u5b50\u5361 ID<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5173\u952e\u70b9\uff1a<code>CardDef<\/code> \u662f frozen \u7684\uff0c\u5168\u5c40\u552f\u4e00\uff0c\u6240\u6709\u540c\u540d\u5361\u5171\u4eab\u3002<\/strong> \u5b9e\u4f53\u53ea\u5b58 <code>CARD_ID<\/code>\uff0c\u884c\u4e3a\u901a\u8fc7\u67e5\u8868\u83b7\u5f97\u3002\u8fd9\u8ba9 6000 \u5f20\u5361\u7684\u5b9a\u4e49\u5728\u5185\u5b58\u91cc\u53ea\u5360\u4e00\u4efd\uff0c\u800c\u4e00\u5c40\u6e38\u620f\u91cc\u7684\u5b9e\u4f53\u6570\uff08\u901a\u5e38 &lt; 200\uff09\u624d\u9700\u8981\u72ec\u7acb\u7684 tag \u8868\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">1.5 \u5b9e\u4f53 ID \u4e0e&#8221;\u540c\u4e00\u6027&#8221;\u95ee\u9898<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u7089\u77f3\u91cc\u6700\u5fae\u5999\u7684\u95ee\u9898\u4e4b\u4e00\uff1a<strong>\u4ec0\u4e48\u65f6\u5019\u5b9e\u4f53\u8fd8\u662f&#8221;\u540c\u4e00\u4e2a&#8221;\uff1f<\/strong><\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u64cd\u4f5c<\/th><th>\u662f\u540c\u4e00\u5b9e\u4f53\u5417<\/th><th>\u540e\u679c<\/th><\/tr><\/thead><tbody><tr><td>\u53d7\u4f24<\/td><td>\u662f<\/td><td>\u9644\u9b54\u4fdd\u7559<\/td><\/tr><tr><td>\u88ab buff<\/td><td>\u662f<\/td><td>\u2014<\/td><\/tr><tr><td>\u88ab\u6c89\u9ed8<\/td><td><strong>\u662f<\/strong><\/td><td>\u4f46\u6e05\u9664\u6240\u6709\u9644\u9b54\u548c\u6587\u672c<\/td><\/tr><tr><td>\u88ab\u300a\u53d8\u5f62\u672f\u300b\u53d8\u6210\u7ef5\u7f8a<\/td><td><strong>\u662f<\/strong>\uff08\u5b9e\u4f53 ID \u4e0d\u53d8\uff0cCARD_ID \u6539\u53d8\uff09<\/td><td>\u9644\u9b54\u5168\u90e8\u4e22\u5931\uff0c\u4ea1\u8bed\u4e0d\u89e6\u53d1<\/td><\/tr><tr><td>\u6b7b\u4ea1\u540e\u88ab\u300a\u590d\u6d3b\u672f\u300b\u590d\u6d3b<\/td><td><strong>\u5426<\/strong>\uff0c\u65b0\u5b9e\u4f53<\/td><td>\u539f\u5b9e\u4f53\u6c38\u4e45\u7559\u5728\u5893\u5730<\/td><\/tr><tr><td>\u4ece\u6218\u573a\u8fd4\u56de\u624b\u724c<\/td><td><strong>\u662f<\/strong>\uff0c\u4f46\u6e05\u9664\u9644\u9b54\uff08\u56de\u5230\u57fa\u7840\u72b6\u6001\uff09<\/td><td>\u8fd9\u662f\u300a\u6492\u6ee1\u8c0e\u8a00\u7684\u4e66\u300b\u80fd&#8221;\u6d17\u767d&#8221;\u7684\u539f\u56e0<\/td><\/tr><tr><td>\u88ab\u300a\u5fc3\u7075\u63a7\u5236\u300b<\/td><td>\u662f\uff0c\u6539 <code>CONTROLLER<\/code><\/td><td><code>OWNER<\/code> \u4e0d\u53d8\uff0c\u6240\u4ee5&#8221;\u8fd4\u56de\u62e5\u6709\u8005\u624b\u724c&#8221;\u4f1a\u9001\u56de\u53bb<\/td><\/tr><tr><td>\u88ab\u590d\u5236\uff08\u300a\u514b\u9686\u300b\uff09<\/td><td>\u5426\uff0c\u65b0\u5b9e\u4f53<\/td><td>\u590d\u5236\u7684\u662f<strong>\u5f53\u524d\u72b6\u6001\u5feb\u7167<\/strong>\u8fd8\u662f<strong>\u57fa\u7840\u5361<\/strong>\uff1f\u53d6\u51b3\u4e8e\u5361\uff08\u8fd9\u662f\u5386\u53f2 bug \u6e29\u5e8a\uff09<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">\u53d8\u5f62\uff08Transform\uff09\u662f\u6700\u53cd\u76f4\u89c9\u7684\uff1a<strong>\u5b9e\u4f53 ID \u4fdd\u7559\uff0c\u4f46\u4e00\u5207\u5c5e\u6027\u91cd\u7f6e\u4e3a\u65b0\u5361\u7684\u5b9a\u4e49<\/strong>\u3002\u8fd9\u89e3\u91ca\u4e86\u4e3a\u4ec0\u4e48&#8221;\u53d8\u5f62\u672f\u6253\u5723\u76fe\u968f\u4ece&#8221;\u4f1a\u8fde\u5723\u76fe\u4e00\u8d77\u6ca1\u6389\uff0c\u4e5f\u89e3\u91ca\u4e86\u4e3a\u4ec0\u4e48\u53d8\u5f62\u4e0d\u89e6\u53d1\u4ea1\u8bed\uff08\u56e0\u4e3a\u539f\u5b9e\u4f53\u6ca1\u6709&#8221;\u6b7b\u4ea1&#8221;\uff0c\u53ea\u662f&#8221;\u53d8\u4e86&#8221;\uff09\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def transform(entity: Entity, new_card_id: str):<br>    \"\"\"\u53d8\u5f62\uff1a\u4fdd\u7559 ENTITY_ID \u4e0e ZONE_POSITION\uff0c\u5176\u4f59\u5168\u90e8\u91cd\u7f6e\u3002\"\"\"<br>    new_def = CARD_DB[new_card_id]<br>    keep = {<br>        Tag.ENTITY_ID:     entity[Tag.ENTITY_ID],<br>        Tag.ZONE:          entity[Tag.ZONE],<br>        Tag.ZONE_POSITION: entity[Tag.ZONE_POSITION],<br>        Tag.CONTROLLER:    entity[Tag.CONTROLLER],<br>        # \u6ce8\u610f\uff1aOWNER \u4e5f\u4fdd\u7559\uff0c\u53d8\u5f62\u4e0d\u6539\u53d8\u5f52\u5c5e<br>        Tag.OWNER:         entity[Tag.OWNER],<br>    }<br>    # \u79fb\u9664\u6240\u6709\u9644\u9b54\uff08\u4e0d\u89e6\u53d1\"\u9644\u9b54\u79fb\u9664\"\u4e8b\u4ef6\u2014\u2014\u53d8\u5f62\u662f\"\u62b9\u9664\"\u4e0d\u662f\"\u79fb\u9664\"\uff09<br>    for e in list(entity._enchantments):<br>        e.detach_silently()<br>    entity._enchantments.clear()<br>    # \u6ce8\u9500\u6240\u6709\u89e6\u53d1\u5668<br>    entity.game.event_bus.unregister_all(entity)<br>    entity.tags.clear()<br>    entity.tags.update(new_def.as_tags())<br>    entity.tags.update(keep)<br>    entity.game.event_bus.register_card(entity)   # \u6ce8\u518c\u65b0\u5361\u7684\u89e6\u53d1\u5668<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u4e00\u4e2a\u771f\u5b9e\u5386\u53f2\u6848\u4f8b\uff1a<strong>\u300a\u91ce\u6027\u6210\u957f\u300b\u4e0e\u300a\u53d8\u5f62\u672f\u300b\u7684\u987a\u5e8f<\/strong>\u3002\u65e9\u671f\u7248\u672c\u91cc\uff0c\u5982\u679c\u53d8\u5f62\u53d1\u751f\u5728\u4e00\u4e2a\u6b63\u5728\u7ed3\u7b97\u4ea1\u8bed\u7684\u968f\u4ece\u8eab\u4e0a\uff0c\u4f1a\u5bfc\u81f4\u4ea1\u8bed\u6307\u5411\u4e00\u4e2a\u5df2\u7ecf\u53d8\u6210\u7ef5\u7f8a\u7684\u5b9e\u4f53\uff0c\u4ece\u800c\u6267\u884c&#8221;\u7ef5\u7f8a\u7684\u4ea1\u8bed&#8221;\uff08\u6ca1\u6709\uff09\u3002\u8fd9\u5c31\u662f\u4e3a\u4ec0\u4e48\u73b0\u4ee3\u5b9e\u73b0\u91cc\uff0c\u4ea1\u8bed\u5fc5\u987b\u5728<strong>\u6b7b\u4ea1\u77ac\u95f4\u5feb\u7167<\/strong>\u5b83\u9700\u8981\u7684\u6240\u6709\u4fe1\u606f\uff08\u7b2c 5 \u7ae0\u5c55\u5f00\uff09\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 2 \u7ae0 \u533a\u57df\uff08Zone\uff09\uff1a\u4f4d\u7f6e\u5373\u72b6\u6001<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">2.1 \u516b\u4e2a\u533a\u57df<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">class Zone(IntEnum):<br>    INVALID   = 0<br>    PLAY      = 1   # \u6218\u573a\uff08\u542b\u82f1\u96c4\u3001\u6b66\u5668\u3001\u82f1\u96c4\u6280\u80fd\uff09<br>    DECK      = 2<br>    HAND      = 3<br>    GRAVEYARD = 4   # \u5893\u5730<br>    REMOVEDFROMGAME = 5  # \u79fb\u51fa\u6e38\u620f\uff08\u5982\u88ab\u541e\u566c\uff09<br>    SETASIDE  = 6   # \u6682\u5b58\u533a\uff08\u884d\u751f\u7269\u751f\u6210\u524d\u3001\u6289\u62e9\u7684\u5b50\u5361\u3001\u4efb\u52a1\u672a\u6fc0\u6d3b\u65f6\uff09<br>    SECRET    = 7   # \u5965\u79d8\u533a<br>    SIDEBOARD = 8   # \u5907\u9009\u5361\u7ec4\uff08\u65c5\u5ba2\/\u661f\u8230\/\u4efb\u52a1\u94fe\u7684\u5173\u8054\u5361\uff09<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>SETASIDE<\/code> \u662f\u6700\u5bb9\u6613\u88ab\u5ffd\u7565\u4f46\u6700\u91cd\u8981\u7684\u4e00\u4e2a\u3002\u5b83\u662f&#8221;\u5b58\u5728\u4f46\u4e0d\u5728\u4efb\u4f55\u53ef\u89c1\u4f4d\u7f6e&#8221;\u7684\u5b9e\u4f53\u7684\u5bb6\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u300a\u6289\u62e9\u300b\u5361\u7684\u4e24\u4e2a\u5b50\u9009\u9879\uff08\u5b83\u4eec\u662f\u771f\u5b9e\u5b9e\u4f53\uff0c\u53ef\u88ab&#8221;\u590d\u5236\u624b\u724c\u4e2d\u7684\u5361&#8221;\u6293\u5230\u8fc7\u2014\u2014\u5386\u53f2 bug\uff09<\/li>\n\n\n\n<li>\u5c1a\u672a\u53ec\u5524\u7684\u884d\u751f\u7269\uff08\u5728 <code>PLAY<\/code> \u4e4b\u524d\u5148\u5728 SETASIDE \u91cc\u521b\u5efa\uff0c\u8d70\u5b8c <code>SUMMONING<\/code> \u4e8b\u4ef6\u624d\u843d\u5730\uff09<\/li>\n\n\n\n<li><strong>\u300a\u5de8\u578b\u300b\uff08Colossal\uff09\u7684\u9644\u5c5e\u90e8\u4ef6<\/strong>\uff1a\u4e3b\u4f53\u5165\u573a\u65f6\uff0c\u90e8\u4ef6\u5148\u5728 SETASIDE \u751f\u6210\u518d\u6446\u5230\u4e24\u4fa7<\/li>\n\n\n\n<li>\u300a\u661f\u8230\u300b\uff08Starship\uff09\u5c1a\u672a\u53d1\u5c04\u65f6\u7684\u96f6\u4ef6<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">\u6709\u4e86 SETASIDE\uff0c&#8221;\u521b\u5efa\u5b9e\u4f53&#8221;\u548c&#8221;\u5b9e\u4f53\u8fdb\u5165\u6218\u573a&#8221;\u5c31\u88ab\u62c6\u6210\u4e86\u4e24\u6b65\uff0c\u4e2d\u95f4\u53ef\u4ee5\u63d2\u5165\u89e6\u53d1\uff08\u6bd4\u5982\u300a\u7c73\u5c14\u8c6a\u65af\u300b\u6539\u53d8\u6cd5\u672f\u8d39\u7528\u3001\u5965\u79d8\u300a\u955c\u50cf\u5b9e\u4f53\u300b\u590d\u5236\u521a\u5165\u573a\u7684\u968f\u4ece\uff09\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">2.2 \u4f4d\u7f6e\uff08ZONE_POSITION\uff09\u4e3a\u4ec0\u4e48\u91cd\u8981<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u7089\u77f3\u7684\u6218\u573a\u662f<strong>\u6709\u5e8f<\/strong>\u7684 7 \u683c\u3002\u4f4d\u7f6e\u53c2\u4e0e\u89c4\u5219\u7684\u5730\u65b9\u6bd4\u60f3\u8c61\u4e2d\u591a\uff1a<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u673a\u5236<\/th><th>\u4f4d\u7f6e\u7684\u4f5c\u7528<\/th><\/tr><\/thead><tbody><tr><td>\u300a\u66b4\u98ce\u57ce\u52c7\u58eb\u300b\u7c7b\u5149\u73af<\/td><td>\u4e0d\u4f9d\u8d56\u4f4d\u7f6e\uff08\u5168\u573a\uff09<\/td><\/tr><tr><td>\u300a\u78c1\u529b\u300b<\/td><td>\u6253\u5728\u673a\u68b0<strong>\u5de6\u4fa7<\/strong>\u624d\u5408\u4f53<\/td><\/tr><tr><td>\u300a\u6d41\u653e\u300b\uff08Outcast\uff09<\/td><td>\u5728\u624b\u724c<strong>\u6700\u5de6\/\u6700\u53f3<\/strong>\u65f6\u89e6\u53d1<\/td><\/tr><tr><td>\u300a\u788e\u88c2\u300b\uff08Shatter\uff0c2026\uff09<\/td><td>\u5206\u88c2\u6210\u4e24\u534a\u653e\u5728\u624b\u724c<strong>\u4e24\u7aef<\/strong>\uff0c\u76f8\u90bb\u65f6\u5408\u5e76<\/td><\/tr><tr><td>\u300a\u6df1\u6c34\u9886\u4e3b\u300b\/\u300a\u5b9d\u5e93\u95e8\u536b\u300b<\/td><td>&#8220;\u76f8\u90bb\u968f\u4ece&#8221;<\/td><\/tr><tr><td>\u968f\u673a\u76ee\u6807\u7684\u5206\u5e03<\/td><td>\u4f4d\u7f6e\u672c\u8eab\u4e0d\u5f71\u54cd\uff0c\u4f46\u53ec\u5524\u4f4d\u7f6e\u5f71\u54cd\u540e\u7eed\u76f8\u90bb\u5173\u7cfb<\/td><\/tr><tr><td>\u4ea1\u8bed\u53ec\u5524\u884d\u751f\u7269<\/td><td>\u5728<strong>\u539f\u968f\u4ece\u7684\u4f4d\u7f6e<\/strong>\u53ec\u5524<\/td><\/tr><tr><td>\u89e6\u53d1\u987a\u5e8f<\/td><td><strong>\u6309\u4e0a\u573a\u5148\u540e<\/strong>\uff0c\u4e0d\u662f\u6309\u4f4d\u7f6e\uff01\uff08\u89c1\u7b2c 5 \u7ae0\uff09<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">\u6700\u540e\u4e00\u6761\u662f\u4e2a\u5927\u5751\uff1a\u5f88\u591a\u4eba\u4ee5\u4e3a\u89e6\u53d1\u987a\u5e8f\u662f&#8221;\u4ece\u5de6\u5230\u53f3&#8221;\uff0c\u5176\u5b9e\u662f&#8221;\u6309 <code>ENTITY_ID<\/code> \u5347\u5e8f&#8221;\uff0c\u4e5f\u5c31\u662f<strong>\u4e0a\u573a\u65f6\u95f4\u987a\u5e8f<\/strong>\u3002\u4e00\u4e2a\u4ece\u5de6\u8fb9\u53ec\u5524\u4f46\u66f4\u665a\u4e0a\u573a\u7684\u968f\u4ece\uff0c\u89e6\u53d1\u987a\u5e8f\u5728\u540e\u9762\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"># zones.py<br>class ZoneManager:<br>    MAX_SIZE = {Zone.PLAY: 7, Zone.HAND: 10, Zone.SECRET: 5, Zone.DECK: 60}<br><br>    def __init__(self, game):<br>        self.game = game<br>        # (player_id, zone) -&gt; list[Entity]\uff0clist \u7684\u987a\u5e8f\u5c31\u662f ZONE_POSITION<br>        self._z: dict[tuple[int, Zone], list[Entity]] = {}<br><br>    def get(self, player: int, zone: Zone) -&gt; list[Entity]:<br>        return self._z.setdefault((player, zone), [])<br><br>    def move(self, e: Entity, to_zone: Zone, position: int | None = None) -&gt; bool:<br>        \"\"\"\u79fb\u52a8\u5b9e\u4f53\u3002\u8fd4\u56de False \u8868\u793a\u5931\u8d25\uff08\u5982\u6218\u573a\u5df2\u6ee1\uff09\u3002\"\"\"<br>        src = e[Tag.ZONE]<br>        pid = e[Tag.CONTROLLER]<br><br>        # 1. \u5bb9\u91cf\u68c0\u67e5\uff08\u5728\u79fb\u9664\u4e4b\u524d\uff01\u5426\u5219\u4f1a\u51fa\u73b0\"\u79bb\u5f00\u65e7\u533a\u4f46\u8fdb\u4e0d\u53bb\u65b0\u533a\"\u7684\u5e7d\u7075\u5b9e\u4f53\uff09<br>        cap = self.MAX_SIZE.get(to_zone)<br>        dst_list = self.get(pid, to_zone)<br>        if cap is not None and len(dst_list) &gt;= cap:<br>            if to_zone == Zone.HAND:<br>                # \u624b\u724c\u6ee1 -&gt; \u70e7\u724c\uff0c\u662f\u4e00\u4e2a\u53ef\u89c2\u6d4b\u4e8b\u4ef6<br>                self.game.emit(Event.CARD_BURNED, entity=e)<br>                return self.move(e, Zone.GRAVEYARD)<br>            if to_zone == Zone.PLAY:<br>                # \u6218\u573a\u6ee1 -&gt; \u53ec\u5524\u5931\u8d25\uff0c\u5b9e\u4f53\u6d88\u5931\uff08\u4e0d\u8fdb\u5893\u5730\uff0c\u4e0d\u89e6\u53d1\u4ea1\u8bed\uff09<br>                self.game.emit(Event.SUMMON_FAILED, entity=e)<br>                self._remove(e)<br>                return False<br>            return False<br><br>        # 2. \u4ece\u539f\u533a\u57df\u79fb\u9664\u5e76\u91cd\u6392\u4f4d\u7f6e<br>        if src != Zone.INVALID:<br>            old = self.get(pid, src)<br>            if e in old:<br>                old.remove(e)<br>                self._reindex(old)<br><br>        # 3. \u63d2\u5165\u65b0\u533a\u57df<br>        if position is None or position &gt; len(dst_list):<br>            dst_list.append(e)<br>        else:<br>            dst_list.insert(position, e)<br>        self._reindex(dst_list)<br>        e[Tag.ZONE] = to_zone<br>        return True<br><br>    @staticmethod<br>    def _reindex(lst: list[Entity]):<br>        for i, x in enumerate(lst):<br>            x.tags[Tag.ZONE_POSITION] = i + 1   # \u76f4\u63a5\u5199 tags\uff0c\u907f\u514d\u89e6\u53d1 record_tag_change \u6d2a\u6c34<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>_reindex<\/code> \u76f4\u63a5\u5199 <code>tags<\/code> \u800c\u4e0d\u662f\u8d70 <code>__setitem__<\/code>\uff0c\u662f\u4e00\u4e2a\u771f\u5b9e\u7684\u6027\u80fd\u8003\u91cf\uff1a\u53ec\u5524\u4e00\u4e2a\u968f\u4ece\u4f1a\u5bfc\u81f4\u5b83\u53f3\u8fb9\u6240\u6709\u968f\u4ece\u7684\u4f4d\u7f6e\u53d8\u5316\uff0c\u5982\u679c\u6bcf\u4e2a\u90fd\u53d1\u4e00\u6761\u7f51\u7edc\u540c\u6b65\u6d88\u606f\uff0c\u5e26\u5bbd\u4f1a\u7206\u70b8\u3002\u771f\u5b9e\u7684\u7089\u77f3\u534f\u8bae\u91cc\uff0c<code>ZONE_POSITION<\/code> \u7684\u53d8\u5316\u786e\u5b9e\u662f\u6279\u91cf\u4e0b\u53d1\u7684\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">2.3 \u6218\u573a\u5df2\u6ee1\uff1a\u4e00\u4e2a\u7cbe\u5fc3\u8bbe\u8ba1\u7684\u5931\u8d25\u8def\u5f84<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">&#8220;\u6218\u573a\u6ee1\u4e86\u4f1a\u600e\u6837&#8221;\u662f\u4e00\u9053\u7ecf\u5178\u9762\u8bd5\u9898\uff0c\u56e0\u4e3a\u5b83\u6709\u4e94\u79cd\u4e0d\u540c\u7b54\u6848\uff1a<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>\u4e3b\u52a8\u51fa\u724c<\/strong>\uff1a\u51fa\u4e0d\u4e86\uff0cUI \u76f4\u63a5\u7981\u7528\u3002<\/li>\n\n\n\n<li><strong>\u6218\u543c\u53ec\u5524<\/strong>\uff08\u5982\u300a\u91ce\u732a\u9a91\u58eb\u300b\uff09\uff1a\u53ec\u5524\u5931\u8d25\uff0c\u4ec0\u4e48\u90fd\u4e0d\u53d1\u751f\uff0c<strong>\u4e0d\u89e6\u53d1\u4ea1\u8bed\uff0c\u4e0d\u8fdb\u5893\u5730\uff0c\u5b9e\u4f53\u76f4\u63a5\u6d88\u5931<\/strong>\u3002<\/li>\n\n\n\n<li><strong>\u4ea1\u8bed\u53ec\u5524<\/strong>\uff08\u5982\u300a\u8718\u86db\u5766\u514b\u300b\uff09\uff1a\u540c\u4e0a\uff0c\u4f46\u6ce8\u610f\u662f\u5728\u6b7b\u4ea1\u7ed3\u7b97\u9636\u6bb5\u5224\u5b9a\u7684\uff0c\u6b64\u65f6\u53ef\u80fd\u5df2\u7ecf\u7a7a\u51fa\u4f4d\u7f6e\u4e86\u3002<\/li>\n\n\n\n<li><strong>\u300a\u519b\u60c5\u4e03\u5904\u300b\u7c7b\u5077\u53d6\u5bf9\u65b9\u968f\u4ece<\/strong>\uff1a\u5982\u679c\u6211\u65b9\u6ee1\u4e86\uff0c\u968f\u4ece<strong>\u7559\u5728\u539f\u5730<\/strong>\u3002<\/li>\n\n\n\n<li><strong>\u300a\u955c\u50cf\u5b9e\u4f53\u300b\u7c7b\u5965\u79d8<\/strong>\uff1a\u5982\u679c\u6ee1\u4e86\uff0c\u5965\u79d8<strong>\u7167\u6837\u6d88\u8017<\/strong>\uff08\u8fd9\u662f\u88ab\u6295\u8bc9\u8fc7\u5f88\u591a\u6b21\u7684\u88c1\u5b9a\uff09\u3002<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">\u5173\u952e\uff1a<strong>&#8220;\u53ec\u5524\u5931\u8d25&#8221;\u548c&#8221;\u6b7b\u4ea1&#8221;\u662f\u4e24\u4ef6\u5b8c\u5168\u4e0d\u540c\u7684\u4e8b<\/strong>\u3002\u524d\u8005\u6839\u672c\u6ca1\u8fdb\u5165 PLAY \u533a\uff0c\u6240\u4ee5\u4e0d\u5b58\u5728\u6b7b\u4ea1\uff0c\u4e5f\u5c31\u6ca1\u6709\u4ea1\u8bed\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def summon(game, card_id: str, controller: int, position: int | None = None) -&gt; Entity | None:<br>    e = game.create_entity(card_id, controller, zone=Zone.SETASIDE)<br>    # \u5148\u5728 SETASIDE \u521b\u5efa\uff0c\u7ed9 \"\u53ec\u5524\u65f6\" \u89e6\u53d1\u4e00\u4e2a\u89c2\u5bdf\u7a97\u53e3<br>    game.emit(Event.PRE_SUMMON, entity=e)<br>    ok = game.zones.move(e, Zone.PLAY, position)<br>    if not ok:<br>        return None                      # \u53ec\u5524\u5931\u8d25\uff1a\u65e0\u4ea1\u8bed\u3001\u65e0\u5893\u5730<br>    e[Tag.EXHAUSTED] = 0 if e.has(Tag.CHARGE) else 1<br>    game.event_bus.register_card(e)<br>    game.aura_bus.mark_dirty()<br>    game.emit(Event.SUMMON, entity=e)    # \u6b64\u523b\u300a\u955c\u50cf\u5b9e\u4f53\u300b\u7b49\u624d\u54cd\u5e94<br>    return e<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 3 \u7ae0 \u6e38\u620f\u4e3b\u5faa\u73af\u4e0e\u9636\u6bb5\u6a21\u578b<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">3.1 \u4e00\u4e2a\u56de\u5408\u91cc\u7a76\u7adf\u53d1\u751f\u4e86\u4ec0\u4e48<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u7089\u77f3\u4e00\u4e2a\u56de\u5408\u7684\u5b8c\u6574\u9636\u6bb5\uff08<strong>\u63a8\u65ad<\/strong>\uff0c\u57fa\u4e8e\u793e\u533a\u9006\u5411\u4e0e\u5b9e\u6d4b\u884c\u4e3a\uff09\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">\u56de\u5408\u5f00\u59cb<br> \u251c\u2500 1. BEGIN_TURN\uff1a\u89e3\u9501\u8fc7\u8f7d\u6c34\u6676\u3001+1 \u6700\u5927\u6cd5\u529b\u3001\u6cd5\u529b\u56de\u6ee1<br> \u251c\u2500 2. \u88c5\u5907\/\u968f\u4ece\u7684 EXHAUSTED \u91cd\u7f6e\uff0cNUM_ATTACKS \u5f52\u96f6<br> \u251c\u2500 3. \u89e6\u53d1\u6240\u6709 \"\u56de\u5408\u5f00\u59cb\u65f6\" \u6548\u679c\uff08\u6309 ENTITY_ID \u987a\u5e8f\uff09<br> \u251c\u2500 4. [\u6b7b\u4ea1\u7ed3\u7b97\u9636\u6bb5]<br> \u251c\u2500 5. \u62bd\u4e00\u5f20\u724c\uff08\u75b2\u52b3\u5224\u5b9a\uff09<br> \u251c\u2500 6. [\u6b7b\u4ea1\u7ed3\u7b97\u9636\u6bb5]<br> \u251c\u2500 7. MAIN \u9636\u6bb5\uff1a\u73a9\u5bb6\u53ef\u4ee5\u51fa\u724c \/ \u653b\u51fb \/ \u7528\u82f1\u96c4\u6280\u80fd\uff0c\u6bcf\u4e2a\u52a8\u4f5c\u540e\uff1a<br> \u2502      \u251c\u2500 \u6267\u884c\u52a8\u4f5c<br> \u2502      \u251c\u2500 \u7ed3\u7b97\u89e6\u53d1\u961f\u5217\uff08\u76f4\u5230\u6e05\u7a7a\uff09<br> \u2502      \u2514\u2500 [\u6b7b\u4ea1\u7ed3\u7b97\u9636\u6bb5]\uff08\u53ef\u80fd\u53cd\u590d\uff0c\u76f4\u5230\u65e0\u65b0\u6b7b\u4ea1\uff09<br> \u251c\u2500 8. END_TURN\uff1a\u89e6\u53d1\u6240\u6709 \"\u56de\u5408\u7ed3\u675f\u65f6\" \u6548\u679c<br> \u251c\u2500 9. [\u6b7b\u4ea1\u7ed3\u7b97\u9636\u6bb5]<br> \u251c\u2500 10. \u75b2\u52b3\/\u4e34\u65f6\u5361\u6e05\u7406\uff08\u300a\u56de\u54cd\u300b\u526f\u672c\u6d88\u5931\u3001\u300a\u4e34\u65f6\u300b\u5361\u5f03\u6389\uff09<br> \u2514\u2500 11. \u4ea4\u6362 currentPlayer<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u5176\u4e2d\u65b9\u62ec\u53f7\u91cc\u7684&#8221;\u6b7b\u4ea1\u7ed3\u7b97\u9636\u6bb5&#8221;\u662f\u5f15\u64ce\u91cc\u6700\u91cd\u8981\u7684\u4e00\u6bb5\uff0c\u7b2c 7 \u7ae0\u4e13\u95e8\u8bb2\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">3.2 \u4e3b\u5faa\u73af\u4ee3\u7801<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\"># game.py\uff08\u8282\u9009\uff09<br>class Game:<br>    def start(self):<br>        self.phase = Phase.BEGIN_GAME<br>        self._mulligan()<br>        self._fire_start_of_game_triggers()   # \u300a\u5f00\u5c40\u65f6\u300b\u7c7b\u6548\u679c<br>        self.turn = 0<br>        self.current = self.first_player<br>        self.begin_turn()<br><br>    def begin_turn(self):<br>        p = self.player(self.current)<br>        self.turn += 1<br><br>        # 1. \u8fc7\u8f7d\u89e3\u9501 \u2014\u2014 \u6ce8\u610f\uff1a\u662f\"\u89e3\u9501\u4e0a\u56de\u5408\u6b20\u7684\"\uff0c\u7136\u540e\u628a owed \u8f6c\u6210 locked<br>        p[Tag.OVERLOAD_LOCKED] = p[Tag.OVERLOAD_OWED]<br>        p[Tag.OVERLOAD_OWED] = 0<br><br>        # 2. \u6cd5\u529b\u6c34\u6676<br>        p[Tag.MANA_CRYSTALS] = min(10, p[Tag.MANA_CRYSTALS] + 1)<br>        p[Tag.MANA_AVAILABLE] = p[Tag.MANA_CRYSTALS] - p[Tag.OVERLOAD_LOCKED]<br><br>        # 3. \u89e3\u9664\u53ec\u5524\u5931\u8c03<br>        for e in self.zones.get(self.current, Zone.PLAY):<br>            e[Tag.EXHAUSTED] = 0<br>            e[Tag.NUM_ATTACKS] = 0<br>            e[Tag.NUM_TURNS_IN_PLAY] += 1<br>        p.hero[Tag.NUM_ATTACKS] = 0<br>        p.hero_power[Tag.EXHAUSTED] = 0<br><br>        # 4. \u56de\u5408\u5f00\u59cb\u89e6\u53d1<br>        self.emit(Event.TURN_START, player=self.current)<br>        self.resolve_queue()<br>        self.process_deaths()<br><br>        # 5. \u62bd\u724c<br>        self.draw(self.current, 1)<br>        self.resolve_queue()<br>        self.process_deaths()<br><br>        self.phase = Phase.MAIN<br><br>    def end_turn(self):<br>        self.phase = Phase.END_TURN<br>        self.emit(Event.TURN_END, player=self.current)<br>        self.resolve_queue()<br>        self.process_deaths()<br><br>        # \u4e34\u65f6\u5361\u6e05\u7406<br>        for e in list(self.zones.get(self.current, Zone.HAND)):<br>            if e.has(Tag.TEMPORARY) or e.echo_copy:<br>                self.zones.move(e, Zone.GRAVEYARD)<br><br>        self.current = 1 - self.current<br>        self.begin_turn()<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">3.3 \u75b2\u52b3\uff1a\u4e00\u4e2a\u53ea\u6709\u4e09\u884c\u4f46\u503c\u5f97\u804a\u7684\u673a\u5236<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">    def draw(self, pid: int, n: int = 1):<br>        for _ in range(n):<br>            deck = self.zones.get(pid, Zone.DECK)<br>            if not deck:<br>                p = self.player(pid)<br>                p[Tag.FATIGUE] += 1<br>                self.damage(p.hero, p[Tag.FATIGUE], source=None)   # source=None \u5f88\u5173\u952e<br>                continue<br>            card = deck.pop()          # \u724c\u5e93\u9876<br>            self.zones.move(card, Zone.HAND)<br>            self.emit(Event.DRAW, entity=card, player=pid)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>source=None<\/code> \u610f\u5473\u7740\u75b2\u52b3\u4f24\u5bb3<strong>\u6ca1\u6709\u6765\u6e90<\/strong>\u3002\u8fd9\u4e0d\u662f\u5077\u61d2\uff0c\u662f\u89c4\u5219\u8981\u6c42\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u75b2\u52b3\u4f24\u5bb3<strong>\u4e0d\u80fd\u88ab\u5438\u8840<\/strong>\uff08\u56e0\u4e3a\u5438\u8840\u662f&#8221;\u6765\u6e90\u7684\u5c5e\u6027&#8221;\uff09<\/li>\n\n\n\n<li>\u75b2\u52b3\u4f24\u5bb3<strong>\u4e0d\u53d7\u6cd5\u672f\u4f24\u5bb3\u52a0\u6210\u5f71\u54cd<\/strong><\/li>\n\n\n\n<li>\u300a\u53d7\u8650\u72c2\u300b\u7c7b&#8221;\u53d7\u5230\u4f24\u5bb3\u65f6&#8221;\u6548\u679c<strong>\u4f1a<\/strong>\u89e6\u53d1\uff08\u4f24\u5bb3\u4e8b\u4ef6\u672c\u8eab\u5b58\u5728\uff09<\/li>\n\n\n\n<li>\u300a\u514d\u75ab\u300b<strong>\u80fd<\/strong>\u6321\u4f4f\u75b2\u52b3\u4f24\u5bb3<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">\u4e00\u4e2a <code>source<\/code> \u5b57\u6bb5\u5c31\u628a\u8fd9\u56db\u6761\u89c4\u5219\u540c\u65f6\u8868\u8fbe\u4e86\u3002\u8fd9\u662f\u826f\u597d\u5efa\u6a21\u7684\u5178\u578b\u6536\u76ca\uff1a<strong>\u89c4\u5219\u4e0d\u9700\u8981\u88ab\u5355\u72ec\u7f16\u7801\uff0c\u5b83\u4ece\u6570\u636e\u7ed3\u6784\u91cc\u81ea\u7136\u5bfc\u51fa<\/strong>\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">3.4 \u51fa\u724c\uff08PlayCard\uff09\u7684\u5b8c\u6574\u6d41\u7a0b<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u51fa\u724c\u662f\u5f15\u64ce\u91cc\u5206\u652f\u6700\u591a\u7684\u64cd\u4f5c\u3002\u5b8c\u6574\u6d41\u7a0b\uff08<strong>\u63a8\u65ad<\/strong>\uff09\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def play_card(self, card: Entity, target: Entity | None = None,<br>              position: int | None = None, choose: int = 0):<br>    pid = card[Tag.CONTROLLER]<br>    p = self.player(pid)<br><br>    # ---- \u9636\u6bb5 A\uff1a\u5408\u6cd5\u6027 ----<br>    cost = self.compute_cost(card)              # \u5149\u73af\/\u9644\u9b54\/\u6298\u6263\u540e\u7684\u6700\u7ec8\u8d39\u7528<br>    assert p[Tag.MANA_AVAILABLE] &gt;= cost<br>    assert self.is_legal_target(card, target)<br><br>    # ---- \u9636\u6bb5 B\uff1a\u652f\u4ed8\u4e0e\u79bb\u624b ----<br>    p[Tag.MANA_AVAILABLE] -= cost<br>    self.emit(Event.PRE_PLAY, entity=card, target=target)   # \u300a\u7c73\u5c14\u8c6a\u65af\u300b\u7b49\u5728\u6b64\u54cd\u5e94<br><br>    # \u8fde\u51fb\u6807\u8bb0\uff1a\u51fa\u724c\u524d\u5224\u65ad\"\u672c\u56de\u5408\u662f\u5426\u5df2\u51fa\u8fc7\u724c\"<br>    combo = p[Tag.COMBO_ACTIVE]<br><br>    self.zones.move(card, Zone.SETASIDE)        # \u5148\u79bb\u5f00\u624b\u724c<br>    self.emit(Event.PLAY_CARD, entity=card, target=target)  # \u300a\u5965\u672f\u5de8\u9f99\u300b\"\u4f60\u4f7f\u7528\u4e00\u5f20\u6cd5\u672f\u540e\"<br><br>    # ---- \u9636\u6bb5 C\uff1a\u6309\u7c7b\u578b\u5206\u53d1 ----<br>    ctype = card[Tag.CARDTYPE]<br>    if ctype == CardType.MINION:<br>        ok = self.zones.move(card, Zone.PLAY, position)<br>        if ok:<br>            card[Tag.EXHAUSTED] = 0 if card.has(Tag.CHARGE) else 1<br>            self.event_bus.register_card(card)<br>            self.aura_bus.mark_dirty()<br>            self.emit(Event.SUMMON, entity=card)<br>            self.emit(Event.MINION_PLAYED, entity=card)<br>            # \u6218\u543c \/ \u8fde\u51fb<br>            script = card.defn.combo if (combo and card.defn.combo) else card.defn.battlecry<br>            if script and not self._battlecry_suppressed(card):<br>                self.run(script, source=card, target=target, choose=choose)<br>    elif ctype == CardType.SPELL:<br>        # \u5965\u79d8\u5148\u68c0\u67e5\u662f\u5426\u88ab\u53cd\u5236<br>        if self.check_counter(card):<br>            self.zones.move(card, Zone.GRAVEYARD)<br>        else:<br>            self.emit(Event.SPELL_CAST, entity=card, target=target)  # \u5965\u79d8\u300a\u6cd5\u672f\u53cd\u5236\u300b\u5728\u6b64<br>            self.run(card.defn.spell, source=card, target=target, choose=choose)<br>            self.zones.move(card, Zone.GRAVEYARD)<br>    elif ctype == CardType.WEAPON:<br>        self.equip(pid, card)<br>    elif ctype == CardType.LOCATION:<br>        self.zones.move(card, Zone.PLAY, position)<br>        card[Tag.LOCATION_COOLDOWN] = 1<br><br>    # ---- \u9636\u6bb5 D\uff1a\u51fa\u724c\u540e ----<br>    p[Tag.COMBO_ACTIVE] = 1<br>    self.emit(Event.AFTER_PLAY, entity=card)<br>    self.resolve_queue()<br>    self.process_deaths()<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u6709\u51e0\u5904\u503c\u5f97\u6ce8\u610f\uff1a<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\uff081\uff09\u4e3a\u4ec0\u4e48\u968f\u4ece\u5148\u843d\u5730\u518d\u6267\u884c\u6218\u543c\uff1f<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u56e0\u4e3a\u6218\u543c\u8981\u80fd&#8221;\u770b\u5230\u81ea\u5df1&#8221;\u3002\u300a\u6697\u5f71\u72c2\u4e71\u300b\u7c7b\u6548\u679c\u9700\u8981\u8ba1\u7b97\u5305\u542b\u81ea\u5df1\u5728\u5185\u7684\u573a\u9762\uff1b\u300a\u7830\u7830\u535a\u58eb\u300b\u53ec\u5524\u7684\u673a\u5668\u4eba\u8981\u5728\u81ea\u5df1\u65c1\u8fb9\u3002\u8fd9\u4e5f\u662f\u4e3a\u4ec0\u4e48\u300a\u91ce\u732a\u9a91\u58eb\u300b\u5728\u6ee1\u573a\u65f6&#8221;\u53ec\u5524\u5931\u8d25&#8221;\u2014\u2014\u56e0\u4e3a\u4e3b\u4f53\u5df2\u7ecf\u5360\u4e86\u7b2c 7 \u683c\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\uff082\uff09\u4e3a\u4ec0\u4e48 <code>emit(PLAY_CARD)<\/code> \u5728\u79fb\u52a8\u5230 SETASIDE \u4e4b\u540e\uff1f<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u56e0\u4e3a&#8221;\u6253\u51fa\u4e00\u5f20\u724c&#8221;\u7684\u89e6\u53d1\u5668\uff08\u5982\u300a\u9c7c\u4eba\u9886\u519b\u300b&#8221;\u4f60\u4f7f\u7528\u4e00\u5f20\u9c7c\u4eba\u540e&#8221;\uff09\u4e0d\u5e94\u8be5\u5728\u724c\u8fd8\u5728\u624b\u4e0a\u65f6\u770b\u5230\u5b83\u3002\u540c\u65f6\u5b83\u5fc5\u987b\u5728\u724c\u8fdb\u5165\u6218\u573a<strong>\u4e4b\u524d<\/strong>\uff0c\u8fd9\u6837\u300a\u955c\u50cf\u5b9e\u4f53\u300b\u624d\u80fd\u533a\u5206&#8221;\u6253\u51fa\u7684\u968f\u4ece&#8221;\u548c&#8221;\u53ec\u5524\u7684\u968f\u4ece&#8221;\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\uff083\uff09\u8fde\u51fb\u7684\u5224\u5b9a\u65f6\u673a<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>combo<\/code> \u53d8\u91cf\u5728<strong>\u79bb\u624b\u4e4b\u524d<\/strong>\u53d6\u503c\uff0c\u56e0\u4e3a\u51fa\u8fd9\u5f20\u724c\u672c\u8eab\u4f1a\u628a <code>COMBO_ACTIVE<\/code> \u7f6e\u4e3a 1\u3002\u7ecf\u5178\u7684&#8221;\u5148\u53d6\u503c\u540e\u7f6e\u4f4d&#8221;\u6a21\u5f0f\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">3.5 \u8d39\u7528\u8ba1\u7b97\uff1a\u4e00\u4e2a\u7eaf\u51fd\u6570<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">def compute_cost(self, card: Entity) -&gt; int:<br>    base = card.defn.cost<br>    # 1. \u9644\u9b54\u5f0f\u6298\u6263\uff08\u300a\u57c3\u5fb7\u6e29\u300b\u3001\u300a\u745f\u62c9\u8d5e\u6069\u300b\u7b49\u6c38\u4e45\u4fee\u6539\uff09<br>    base += sum(e.cost_delta for e in card._enchantments)<br>    # 2. \u5149\u73af\u5f0f\u6298\u6263\uff08\u300a\u7d22\u745e\u68ee\u5927\u5e1d\u300b\u3001\u300a\u5b66\u5f92\u300b\uff0c\u6e90\u6d88\u5931\u5373\u5931\u6548\uff09<br>    base += self.aura_bus.delta(card, Tag.COST)<br>    # 3. \u52a8\u6001\u6210\u672c\uff08\u300a\u6d77\u5de8\u4eba\u300b\"\u4f60\u7684\u968f\u4ece\u6bcf\u6709\u4e00\u4e2a\uff0c\u8d39\u7528\u51cf 1\"\uff09<br>    if card.defn.dynamic_cost:<br>        base = card.defn.dynamic_cost(self, card)<br>    # 4. Prepare \u6298\u6263\uff082026 \u65b0\u673a\u5236\uff0c\u89c1\u7b2c 26 \u7ae0\uff09<br>    base -= card[Tag.PREPARE_DISCOUNT]<br>    return max(0, base)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u987a\u5e8f\u5f88\u91cd\u8981\u3002\u771f\u5b9e\u7089\u77f3\u91cc\u7684\u987a\u5e8f\u662f\uff1a<strong>\u5148\u52a0\u540e\u51cf\u3001\u5148\u4e58\u540e\u52a0<\/strong>\uff08\u5bf9\u4e8e <code>*2<\/code> \u7c7b\u6548\u679c\uff09\uff0c\u6700\u540e clamp \u5230 0\u3002\u5386\u53f2\u4e0a\u6709\u8fc7\u82e5\u5e72\u56e0\u4e3a\u987a\u5e8f\u4e0d\u540c\u5bfc\u81f4\u7684&#8221;\u8d1f\u8d39\u7528\u53d8\u6b63\u8d39\u7528&#8221;bug\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">\u7b2c\u4e8c\u90e8\u5206 \u6838\u5fc3\u7b97\u6cd5\uff1a\u5f15\u64ce\u7684\u5fc3\u810f<\/h1>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 4 \u7ae0 \u4e8b\u4ef6\u603b\u7ebf\u4e0e\u89e6\u53d1\u7cfb\u7edf<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">4.1 \u4e8b\u4ef6\u7684\u5206\u7c7b\u5b66<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u7089\u77f3\u91cc\u6240\u6709&#8221;\u5f53\u2026\u2026\u65f6&#8221;\u7684\u6548\u679c\uff0c\u672c\u8d28\u4e0a\u90fd\u662f\u4e8b\u4ef6\u8ba2\u9605\u3002\u4f46\u4e8b\u4ef6\u7684\u79cd\u7c7b\u9700\u8981\u7cbe\u5fc3\u5207\u5206\u3002\u4e00\u4e2a\u5e38\u89c1\u7684\u9519\u8bef\u662f\u628a\u4e8b\u4ef6\u5207\u5f97\u592a\u7c97\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"># \u274c \u9519\u8bef\u793a\u8303<br>emit(\"MINION_DIED\", minion)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u8fd9\u6837\u300a\u4ea1\u8bed\u300b\u300a\u590d\u751f\u300b\u300a\u4f60\u7684\u968f\u4ece\u6b7b\u4ea1\u540e\u300b\u300a\u53cb\u65b9\u91ce\u517d\u6b7b\u4ea1\u540e\u300b\u5c31\u5f97\u5404\u81ea\u5728\u56de\u8c03\u91cc\u5199\u4e00\u5806 <code>if<\/code>\u3002\u6b63\u786e\u7684\u505a\u6cd5\u662f<strong>\u4e8b\u4ef6\u643a\u5e26\u8db3\u591f\u7ed3\u6784\u5316\u7684\u4e0a\u4e0b\u6587\uff0c\u8ba9\u8ba2\u9605\u8005\u7528\u58f0\u660e\u5f0f\u6761\u4ef6\u8fc7\u6ee4<\/strong>\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"># events.py<br>class Event(IntEnum):<br>    # ---- \u56de\u5408 ----<br>    TURN_START = auto(); TURN_END = auto()<br>    GAME_START = auto()<br><br>    # ---- \u5361\u724c\u6d41\u8f6c ----<br>    DRAW = auto(); CARD_BURNED = auto(); DISCARD = auto()<br>    PRE_PLAY = auto(); PLAY_CARD = auto(); AFTER_PLAY = auto()<br>    MINION_PLAYED = auto(); SPELL_CAST = auto(); AFTER_SPELL = auto()<br>    WEAPON_EQUIPPED = auto()<br><br>    # ---- \u6218\u573a ----<br>    PRE_SUMMON = auto(); SUMMON = auto(); SUMMON_FAILED = auto()<br>    PROPOSED_ATTACK = auto()      # \u653b\u51fb\u5ba3\u8a00\uff08\u6b64\u65f6\u53ef\u6539\u76ee\u6807\uff1a\u5632\u8bbd\u3001\u300a\u94dc\u987b\u300b\uff09<br>    ATTACK = auto()               # \u653b\u51fb\u786e\u8ba4\uff08\u300a\u7830\u7830\u6cd5\u5e08\u300b\u5728\u6b64\uff09<br>    AFTER_ATTACK = auto()<br><br>    # ---- \u4f24\u5bb3\u4e0e\u6cbb\u7597 ----<br>    PRE_DAMAGE = auto()           # \u53ef\u4fee\u6539\u4f24\u5bb3\u6570\u503c\uff08\u5723\u76fe\u5728\u6b64\u6d88\u8017\uff09<br>    DAMAGE = auto()               # \u4f24\u5bb3\u5df2\u7ed3\u7b97<br>    HEAL = auto(); OVERHEAL = auto()<br>    ARMOR_GAINED = auto()<br><br>    # ---- \u751f\u6b7b ----<br>    PROPOSED_DEATH = auto()       # \u88ab\u6807\u8bb0\u6b7b\u4ea1\uff08\u300a\u6069\u4f50\u65af\u7684\u5b9d\u7bb1\u300b\u53ef\u6551\uff09<br>    DEATH = auto()                # \u786e\u8ba4\u6b7b\u4ea1\uff0c\u4ea1\u8bed\u5728\u6b64<br>    AFTER_DEATH = auto()<br><br>    # ---- \u5176\u4ed6 ----<br>    HERO_POWER_USED = auto()      # \u52b1\u5fd7<br>    SECRET_REVEALED = auto()<br>    MANA_SPENT = auto()<br>    TAG_CHANGED = auto()          # \u5143\u4e8b\u4ef6\uff0c\u7528\u4e8e\u5149\u73af\u810f\u6807\u8bb0<br><br><br>@dataclass<br>class EventCtx:<br>    \"\"\"\u4e8b\u4ef6\u4e0a\u4e0b\u6587\u3002\u6240\u6709\u5b57\u6bb5\u90fd\u53ef\u9009\uff0c\u8ba2\u9605\u8005\u7528 Condition \u58f0\u660e\u5f0f\u5339\u914d\u3002\"\"\"<br>    kind: Event<br>    entity: Entity | None = None       # \u4e8b\u4ef6\u4e3b\u4f53<br>    source: Entity | None = None       # \u65bd\u52a8\u8005<br>    target: Entity | None = None<br>    player: int | None = None<br>    amount: int = 0                    # \u4f24\u5bb3\/\u6cbb\u7597\/\u62bd\u724c\u6570<br>    fatal: bool = False<br>    prevented: bool = False            # \u53ef\u88ab\u8ba2\u9605\u8005\u7f6e\u4f4d\u4ee5\u53d6\u6d88<br>    # \u5141\u8bb8\u8ba2\u9605\u8005\u4fee\u6539\u7684\u53ef\u5199\u5b57\u6bb5<br>    mutable_amount: int | None = None<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">4.2 \u89e6\u53d1\u5668\u7684\u58f0\u660e\u5f0f\u5b9a\u4e49<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">@dataclass(frozen=True)<br>class TriggerDef:<br>    on: Event<br>    action: \"Action\"<br>    condition: \"Condition | None\" = None<br>    # \u4f5c\u7528\u57df\uff1a\u4ec0\u4e48\u533a\u57df\u91cc\u7684\u5b9e\u4f53\u4f1a\u54cd\u5e94\u8fd9\u4e2a\u89e6\u53d1<br>    zones: frozenset[Zone] = frozenset({Zone.PLAY})<br>    once: bool = False            # \u4e00\u6b21\u6027\uff08\u6cd5\u672f\u8ff8\u53d1\u3001\u72c2\u4e71\u3001\u8d85\u6740\uff09<br>    source_only: bool = False     # \u53ea\u54cd\u5e94\u81ea\u5df1\u8eab\u4e0a\u7684\u4e8b\u4ef6<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u6709\u4e86 <code>zones<\/code>\uff0c\u4e00\u5927\u7c7b\u7279\u6b8a\u5361\u5c31\u81ea\u7136\u88ab\u652f\u6301\u4e86\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u6b63\u5e38\u968f\u4ece\u7684\u89e6\u53d1\uff1a<code>zones={PLAY}<\/code><\/li>\n\n\n\n<li><strong>\u4ea1\u8bed<\/strong>\uff1a<code>zones={PLAY}<\/code>\uff0c\u4f46\u8ba2\u9605 <code>DEATH<\/code> \u4e14 <code>source_only=True<\/code><\/li>\n\n\n\n<li><strong>\u300a\u96cf\u9f99\u300b\u7c7b&#8221;\u5728\u624b\u724c\u4e2d\u65f6&#8221;<\/strong>\uff1a<code>zones={HAND}<\/code><\/li>\n\n\n\n<li><strong>\u300a\u7a46\u5766\u52aa\u65af\u300b\/\u300a\u98df\u8150\u571f\u72fc\u300b\u7c7b&#8221;\u5728\u724c\u5e93\u4e2d\u65f6&#8221;<\/strong>\uff1a<code>zones={DECK}<\/code><\/li>\n\n\n\n<li><strong>\u5965\u79d8<\/strong>\uff1a<code>zones={SECRET}<\/code><\/li>\n\n\n\n<li><strong>\u4efb\u52a1<\/strong>\uff1a<code>zones={SECRET}<\/code>\uff08\u662f\u7684\uff0c\u4efb\u52a1\u653e\u5728\u5965\u79d8\u533a\uff0c\u6240\u4ee5\u300a\u5965\u79d8\u7ba1\u7406\u5458\u300b\u80fd\u6293\u4efb\u52a1\u2014\u2014\u8fd9\u66fe\u7ecf\u662f\u4e2a bug\uff09<\/li>\n\n\n\n<li><strong>\u300a\u5f00\u5c40\u65f6\u300b\uff08Start of Game\uff09<\/strong>\uff1a<code>zones={DECK}<\/code> + <code>on=GAME_START<\/code><\/li>\n\n\n\n<li><strong>\u300a\u62bd\u5230\u65f6\u65bd\u653e\u300b\uff08Casts When Drawn\uff09<\/strong>\uff1a<code>zones={DECK}<\/code> + <code>on=DRAW<\/code> + <code>source_only=True<\/code><\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">4.3 \u4e8b\u4ef6\u603b\u7ebf\u7684\u5b9e\u73b0<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">class EventBus:<br>    def __init__(self, game):<br>        self.game = game<br>        # Event -&gt; list[(entity, TriggerDef)]\uff0c\u4fdd\u6301\u6ce8\u518c\u987a\u5e8f<br>        self._subs: dict[Event, list[tuple[Entity, TriggerDef]]] = defaultdict(list)<br>        self.queue: deque[tuple[Entity, TriggerDef, EventCtx]] = deque()<br>        self._depth = 0<br><br>    def register_card(self, e: Entity):<br>        for td in e.defn.triggers:<br>            self._subs[td.on].append((e, td))<br>        if e.defn.deathrattle:<br>            td = TriggerDef(on=Event.DEATH, action=e.defn.deathrattle, source_only=True)<br>            self._subs[Event.DEATH].append((e, td))<br><br>    def unregister_all(self, e: Entity):<br>        for lst in self._subs.values():<br>            lst[:] = [(x, t) for (x, t) in lst if x is not e]<br><br>    def emit(self, ctx: EventCtx):<br>        \"\"\"\u6536\u96c6\u5339\u914d\u7684\u89e6\u53d1\u5668\uff0c\u6309 ENTITY_ID \u6392\u5e8f\u540e\u5165\u961f\u3002\u4e0d\u7acb\u5373\u6267\u884c\uff01\"\"\"<br>        matched = []<br>        for (owner, td) in self._subs[ctx.kind]:<br>            if owner[Tag.ZONE] not in td.zones:<br>                continue<br>            if td.source_only and ctx.entity is not owner:<br>                continue<br>            if owner.has(Tag.SILENCED) and not td.unsilenceable:<br>                continue<br>            if td.condition and not td.condition.eval(self.game, owner, ctx):<br>                continue<br>            matched.append((owner, td))<br><br>        # \u2605 \u5173\u952e\uff1a\u6392\u5e8f\u89c4\u5219<br>        matched.sort(key=lambda x: x[0][Tag.ENTITY_ID])<br>        self.queue.extend((o, t, ctx) for (o, t) in matched)<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">4.4 \u89e6\u53d1\u987a\u5e8f\u7684\u4e09\u6761\u94c1\u5f8b<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u8fd9\u662f\u7089\u77f3\u89c4\u5219\u91cc\u6700\u5bb9\u6613\u641e\u9519\u7684\u90e8\u5206\u3002\u7ecf\u8fc7\u793e\u533a\u591a\u5e74\u6d4b\u8bd5\uff0c\u89c4\u5219\u662f\uff1a<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u94c1\u5f8b\u4e00\uff1a\u540c\u4e00\u4e8b\u4ef6\u7684\u591a\u4e2a\u89e6\u53d1\u5668\uff0c\u6309 <code>ENTITY_ID<\/code> \u5347\u5e8f\uff08\u5373\u4e0a\u573a\u5148\u540e\uff09\u4f9d\u6b21\u7ed3\u7b97\u3002<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u4e0d\u662f\u4ece\u5de6\u5230\u53f3\uff0c\u4e0d\u662f\u968f\u673a\uff0c\u662f<strong>\u4e0a\u573a\u65f6\u95f4<\/strong>\u3002\u6240\u4ee5\u4e24\u4e2a\u300a\u4e0d\u7a33\u5b9a\u7684\u98df\u5c38\u9b3c\u300b\u540c\u65f6\u6b7b\u4ea1\u65f6\uff0c\u5148\u4e0a\u573a\u7684\u90a3\u4e2a\u5148\u89e6\u53d1\u4ea1\u8bed\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u94c1\u5f8b\u4e8c\uff1a\u5148\u624b\u73a9\u5bb6\u7684\u89e6\u53d1\u5668\u4f18\u5148\u4e8e\u540e\u624b\u73a9\u5bb6\uff1f\u2014\u2014\u4e0d\uff0c\u8fd9\u6761\u662f\u9519\u7684\u3002<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u65e9\u671f\u793e\u533a\u6d41\u4f20&#8221;\u5df1\u65b9\u4f18\u5148&#8221;\u7684\u8bf4\u6cd5\uff0c\u5b9e\u6d4b\u7ed3\u679c\u662f\uff1a<strong>\u53ea\u6309 ENTITY_ID\uff0c\u4e0d\u5206\u654c\u6211<\/strong>\u3002\u56e0\u4e3a ENTITY_ID \u662f\u5168\u5c40\u9012\u589e\u7684\uff0c\u6240\u4ee5\u81ea\u7136\u5730\u8868\u73b0\u4e3a&#8221;\u5148\u653e\u7684\u5148\u89e6\u53d1&#8221;\u3002\u552f\u4e00\u4f8b\u5916\u662f\u56de\u5408\u5f00\u59cb\/\u7ed3\u675f\u8fd9\u7c7b\u73a9\u5bb6\u7ea7\u4e8b\u4ef6\u2014\u2014\u5b83\u4eec\u672c\u8eab\u5c31\u53ea\u5bf9\u4e00\u4e2a\u73a9\u5bb6\u53d1\u751f\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u94c1\u5f8b\u4e09\uff1a\u89e6\u53d1\u8fc7\u7a0b\u4e2d\u4ea7\u751f\u7684\u65b0\u4e8b\u4ef6\uff0c\u8ffd\u52a0\u5230\u961f\u5217\u5c3e\u90e8\uff0c\u4e0d\u63d2\u961f\u3002<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u8fd9\u662f\u961f\u5217\uff08\u800c\u975e\u6808\uff09\u8bed\u4e49\u7684\u6838\u5fc3\u3002\u4e3e\u4f8b\uff1a<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">\u573a\u4e0a\u6709 A\u300a\u4e0d\u7a33\u5b9a\u7684\u98df\u5c38\u9b3c\u300b(1\/3\uff0c\u4ea1\u8bed\uff1a\u5bf9\u6240\u6709\u968f\u4ece\u9020\u6210 1 \u70b9\u4f24\u5bb3) \u548c B\u300a\u4e0d\u7a33\u5b9a\u7684\u98df\u5c38\u9b3c\u300b\uff0c\u90fd\u53ea\u5269 1 \u8840\u3002 \u4f60\u6253\u51fa\u300a\u66b4\u98ce\u96ea\u300b\u5bf9\u6240\u6709\u968f\u4ece\u9020\u6210 2 \u70b9\u4f24\u5bb3\u3002<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">\u7ed3\u7b97\u8fc7\u7a0b\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">1. \u66b4\u98ce\u96ea\u9020\u6210\u4f24\u5bb3 \u2192 A\u3001B \u540c\u65f6\u88ab\u6807\u8bb0\u6b7b\u4ea1<br>2. [\u6b7b\u4ea1\u7ed3\u7b97\u9636\u6bb5] \u6536\u96c6\u6b7b\u4ea1\uff1a[A, B]\uff08\u6309 ENTITY_ID\uff09<br>3. \u961f\u5217 = [A.\u4ea1\u8bed, B.\u4ea1\u8bed]<br>4. \u6267\u884c A.\u4ea1\u8bed \u2192 \u5168\u573a 1 \u70b9\u4f24\u5bb3 \u2192 \u53ef\u80fd\u4ea7\u751f\u65b0\u6b7b\u4ea1 C<br>5. \u6267\u884c B.\u4ea1\u8bed \u2192 \u5168\u573a 1 \u70b9\u4f24\u5bb3 \u2192 \u53ef\u80fd\u4ea7\u751f\u65b0\u6b7b\u4ea1 D<br>   \uff08\u6ce8\u610f\uff1aB \u7684\u4ea1\u8bed\u7167\u5e38\u89e6\u53d1\uff0c\u5373\u4f7f A \u7684\u4ea1\u8bed\"\u53c8\u6740\u4e86\u5b83\u4e00\u6b21\"\uff09<br>6. [\u6b7b\u4ea1\u7ed3\u7b97\u9636\u6bb5] \u6536\u96c6\u65b0\u6b7b\u4ea1 [C, D]<br>7. \u961f\u5217 = [C.\u4ea1\u8bed, D.\u4ea1\u8bed]<br>8. ...\u76f4\u5230\u4e0d\u518d\u4ea7\u751f\u6b7b\u4ea1<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u5982\u679c\u662f<strong>\u6808<\/strong>\u8bed\u4e49\uff08MTG\uff09\uff0c\u7b2c 4 \u6b65\u4ea7\u751f\u7684\u6b7b\u4ea1\u4f1a\u6253\u65ad\u7b2c 5 \u6b65\uff0c\u5148\u7ed3\u7b97\u5b8c C \u7684\u4ea1\u8bed\u624d\u8f6e\u5230 B\u3002\u884c\u4e3a\u5b8c\u5168\u4e0d\u540c\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u7089\u77f3\u9009\u961f\u5217\u7684\u7406\u7531\uff08Ben Brode \u5728\u5f00\u53d1\u8005\u8bbf\u8c08\u4e2d\u63d0\u8fc7\u5927\u610f\uff09<\/strong>\uff1a\u961f\u5217\u7684\u884c\u4e3a\u5bf9\u73a9\u5bb6\u66f4\u597d\u9884\u6d4b\u2014\u2014&#8221;\u6211\u770b\u5230\u7684\u987a\u5e8f\u5c31\u662f\u6267\u884c\u7684\u987a\u5e8f&#8221;\uff0c\u800c\u6808\u4f1a\u4ea7\u751f&#8221;\u6700\u540e\u6253\u51fa\u7684\u6700\u5148\u751f\u6548&#8221;\u8fd9\u79cd\u9700\u8981\u6559\u5b66\u7684\u53cd\u76f4\u89c9\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">4.5 \u961f\u5217\u7684\u7ed3\u7b97\u4e0e\u6df1\u5ea6\u9650\u5236<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">    def resolve(self):<br>        \"\"\"\u7ed3\u7b97\u89e6\u53d1\u961f\u5217\u76f4\u5230\u6e05\u7a7a\u3002\"\"\"<br>        self._depth += 1<br>        if self._depth &gt; 128:<br>            # \u65e0\u9650\u5faa\u73af\u4fdd\u62a4\u3002\u771f\u5b9e\u7089\u77f3\u4e5f\u6709\u2014\u2014\u8457\u540d\u7684\"\u65e0\u9650\u89e6\u53d1\u5bfc\u81f4\u5bf9\u5c40\u5224\u548c\"<br>            self.game.log(\"TRIGGER_DEPTH_EXCEEDED -&gt; forcing draw\")<br>            self.queue.clear()<br>            self.game.force_draw_game()<br>            self._depth -= 1<br>            return<br>        while self.queue:<br>            owner, td, ctx = self.queue.popleft()<br>            # \u2605 \u4e8c\u6b21\u6821\u9a8c\uff1a\u5165\u961f\u65f6\u5408\u6cd5\uff0c\u51fa\u961f\u65f6\u53ef\u80fd\u5df2\u7ecf\u4e0d\u5408\u6cd5\u4e86<br>            if owner[Tag.ZONE] not in td.zones and td.on != Event.DEATH:<br>                continue<br>            if td.once and owner[Tag.TRIGGERED_ONCE]:<br>                continue<br>            if td.once:<br>                owner[Tag.TRIGGERED_ONCE] = 1<br>            self.game.run(td.action, source=owner, ctx=ctx)<br>        self._depth -= 1<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>&#8220;\u4e8c\u6b21\u6821\u9a8c&#8221;\u662f\u771f\u5b9e\u5f15\u64ce\u5fc5\u987b\u6709\u7684<\/strong>\u3002\u573a\u666f\uff1a<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">\u4f60\u6709\u300a\u5965\u91d1\u5c3c\u7075\u9b42\u796d\u53f8\u300b\uff08\u4f60\u7684\u6cbb\u7597\u6539\u4e3a\u4f24\u5bb3\uff09\u548c\u4e00\u4e2a\u300a\u5317\u90e1\u7267\u5e08\u300b\u3002 \u4f60\u5bf9\u4e00\u4e2a\u968f\u4ece\u7528\u4e86\u6cbb\u7597\u3002\u4e8b\u4ef6\u5165\u961f\u65f6\u4e24\u4e2a\u89e6\u53d1\u5668\u90fd\u5339\u914d\u3002 \u4f46\u7b2c\u4e00\u4e2a\u89e6\u53d1\u5668\u628a\u300a\u5317\u90e1\u7267\u5e08\u300b\u6740\u4e86 \u2014\u2014 \u7b2c\u4e8c\u4e2a\u89e6\u53d1\u5668\u51fa\u961f\u65f6\u5b83\u5df2\u7ecf\u4e0d\u5728\u573a\u4e0a\u3002<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">\u771f\u5b9e\u7089\u77f3\u7684\u884c\u4e3a\u662f\uff1a<strong>\u5df2\u5165\u961f\u7684\u89e6\u53d1\u4f9d\u7136\u4f1a\u6267\u884c<\/strong>\uff08\u5bf9\u4e8e\u4ea1\u8bed\u8fd9\u7c7b\uff09\uff0c\u4f46\u5bf9\u4e8e&#8221;\u573a\u4e0a\u968f\u4ece\u7684\u6301\u7eed\u89e6\u53d1&#8221;\u5219\u4f1a\u88ab\u8df3\u8fc7\u3002\u8fd9\u4e2a\u4e0d\u4e00\u81f4\u6027\u6b63\u662f\u4e8c\u6b21\u6821\u9a8c\u5b58\u5728\u7684\u539f\u56e0\uff0c\u4e5f\u662f\u65e0\u6570\u5386\u53f2 bug \u7684\u6765\u6e90\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">4.6 \u4e00\u4e2a\u771f\u5b9e\u7684\u65e0\u9650\u5faa\u73af\u6848\u4f8b<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">2018 \u5e74\u6709\u4e00\u4e2a\u8457\u540d bug\uff1a\u300a\u7c73\u5c14\u8c6a\u65af\u00b7\u6cd5\u529b\u98ce\u66b4\u300b+\u300a\u7d2b\u7f57\u5170\u6559\u5e08\u300b+ \u67d0\u4e9b\u5361\u7684\u7ec4\u5408\u80fd\u89e6\u53d1\u65e0\u9650\u53ec\u5524\uff0c\u5bfc\u81f4\u670d\u52a1\u5668\u5361\u6b7b\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u4fee\u590d\u65b9\u5f0f\uff08<strong>\u63a8\u65ad<\/strong>\uff09\u5c31\u662f\u4e0a\u9762\u7684 <code>_depth &gt; 128<\/code> \u4fdd\u62a4\u3002\u73b0\u4ee3\u7089\u77f3\u5728\u68c0\u6d4b\u5230\u8fc7\u6df1\u7684\u89e6\u53d1\u94fe\u65f6\u4f1a\u5f3a\u5236\u7ed3\u675f\u5bf9\u5c40\u5224\u548c\u3002\u4f60\u5728\u8bbe\u8ba1\u4efb\u4f55\u89e6\u53d1\u7cfb\u7edf\u65f6\u90fd\u5e94\u8be5\u52a0\u8fd9\u4e2a\u4fdd\u62a4\u2014\u2014<strong>\u56e0\u4e3a\u5361\u6c60\u589e\u957f\u4f1a\u8ba9\u4f60\u4e0d\u53ef\u80fd\u63d0\u524d\u8bc1\u660e\u4e0d\u5b58\u5728\u73af<\/strong>\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 5 \u7ae0 \u89e6\u53d1\u961f\u5217 vs \u6808\uff1a\u7089\u77f3\u7684\u65f6\u5e8f\u54f2\u5b66<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">5.1 \u4e00\u4e2a\u601d\u60f3\u5b9e\u9a8c<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u5047\u8bbe\u4f60\u8981\u8bbe\u8ba1\u4e00\u4e2a\u5361\u724c\u6e38\u620f\u7684\u6548\u679c\u89e3\u6790\u7cfb\u7edf\u3002\u4f60\u6709\u4e24\u4e2a\u9009\u62e9\uff1a<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u65b9\u6848 A\uff08\u6808\uff09<\/strong>\uff1a\u65b0\u4ea7\u751f\u7684\u6548\u679c\u538b\u6808\uff0c\u540e\u8fdb\u5148\u51fa\u3002 <strong>\u65b9\u6848 B\uff08\u961f\u5217\uff09<\/strong>\uff1a\u65b0\u4ea7\u751f\u7684\u6548\u679c\u5165\u961f\uff0c\u5148\u8fdb\u5148\u51fa\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u770b\u8d77\u6765\u53ea\u662f\u6570\u636e\u7ed3\u6784\u7684\u533a\u522b\uff0c\u5b9e\u9645\u4e0a\u5b83\u51b3\u5b9a\u4e86\u6e38\u620f\u7684\u6574\u4e2a\u4ea4\u4e92\u6a21\u578b\uff1a<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th><\/th><th>\u6808<\/th><th>\u961f\u5217<\/th><\/tr><\/thead><tbody><tr><td>\u9700\u8981&#8221;\u4f18\u5148\u6743&#8221;\u7cfb\u7edf\u5417<\/td><td><strong>\u5fc5\u987b<\/strong>\u3002\u6bcf\u4e2a\u6548\u679c\u4e0a\u6808\u540e\u90fd\u8981\u7ed9\u53cc\u65b9\u54cd\u5e94\u673a\u4f1a<\/td><td>\u4e0d\u9700\u8981<\/td><\/tr><tr><td>\u5bf9\u624b\u80fd\u5728\u4f60\u56de\u5408\u4e2d\u6253\u724c\u5417<\/td><td>\u80fd\uff08\u8fd9\u662f\u6808\u5b58\u5728\u7684\u610f\u4e49\uff09<\/td><td>\u4e0d\u80fd<\/td><\/tr><tr><td>\u5355\u6b21\u64cd\u4f5c\u80fd\u5426\u539f\u5b50\u5316<\/td><td>\u4e0d\u80fd\uff0c\u4e2d\u95f4\u8981\u7b49\u5f85\u8f93\u5165<\/td><td><strong>\u80fd<\/strong><\/td><\/tr><tr><td>\u7f51\u7edc\u5f80\u8fd4\u6b21\u6570<\/td><td>\u6bcf\u4e2a\u6548\u679c O(n) \u6b21\u786e\u8ba4<\/td><td>\u4e00\u6b21\u64cd\u4f5c\u4e00\u6b21\u5f80\u8fd4<\/td><\/tr><tr><td>\u79fb\u52a8\u7aef\u4f53\u9a8c<\/td><td>\u5dee\uff08\u7b49\u5f85\u591a\uff09<\/td><td>\u597d<\/td><\/tr><tr><td>\u590d\u6742\u5ea6<\/td><td>\u9ad8\uff08\u89c4\u5219\u624b\u518c 250 \u9875\uff09<\/td><td>\u4f4e<\/td><\/tr><tr><td>\u7b56\u7565\u6df1\u5ea6<\/td><td>\u9ad8<\/td><td>\u4f4e\uff08\u4f46\u53ef\u4ee5\u7528\u522b\u7684\u65b9\u5f0f\u8865\uff09<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">\u7089\u77f3\u9009\u961f\u5217\uff0c<strong>\u6839\u672c\u539f\u56e0\u662f\u5b83\u662f\u4e00\u4e2a\u4e3a\u79fb\u52a8\u7aef\u8bbe\u8ba1\u7684\u3001\u8ffd\u6c42&#8221;\u4e00\u6b21\u64cd\u4f5c\u4e00\u6b21\u52a8\u753b&#8221;\u7684\u6e38\u620f<\/strong>\u3002\u6808\u4f1a\u5bfc\u81f4\u6bcf\u6b21\u51fa\u724c\u90fd\u8981\u7b49\u5bf9\u624b\u54cd\u5e94\uff0c\u5728\u624b\u673a\u4e0a\u662f\u707e\u96be\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">5.2 \u961f\u5217\u5e26\u6765\u7684\u8865\u507f\u8bbe\u8ba1\uff1a\u5965\u79d8<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u4f46\u5b8c\u5168\u6ca1\u6709\u5bf9\u624b\u56de\u5408\u7684\u4e92\u52a8\uff0c\u6e38\u620f\u4f1a\u5f88\u65e0\u804a\u3002\u7089\u77f3\u7684\u89e3\u6cd5\u662f<strong>\u5965\u79d8\uff08Secret\uff09<\/strong>\uff1a\u628a&#8221;\u5bf9\u624b\u7684\u54cd\u5e94&#8221;\u4ece&#8221;\u5b9e\u65f6\u51b3\u7b56&#8221;\u53d8\u6210&#8221;\u9884\u5148\u627f\u8bfa&#8221;\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u5965\u79d8\u7684\u672c\u8d28\u662f\uff1a<strong>\u4e00\u4e2a\u9884\u5148\u6ce8\u518c\u7684\u3001\u6761\u4ef6\u89e6\u53d1\u7684\u3001\u4e0d\u9700\u8981\u8be2\u95ee\u6240\u6709\u8005\u7684\u89e6\u53d1\u5668<\/strong>\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">SECRET_MIRROR_ENTITY = CardDef(<br>    card_id=\"EX1_294\", name=\"\u955c\u50cf\u5b9e\u4f53\", cost=3, card_type=\"SPELL\",<br>    keywords=frozenset({\"SECRET\"}),<br>    triggers=(TriggerDef(<br>        on=Event.MINION_PLAYED,<br>        zones=frozenset({Zone.SECRET}),<br>        condition=Cond.enemy_of_owner(),<br>        action=Seq(<br>            RevealSecret(),<br>            Summon(copy_of=Ctx.entity, controller=Owner.SELF),<br>        ),<br>    ),),<br>)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u8fd9\u5c31\u5b8c\u7f8e\u7ed5\u5f00\u4e86&#8221;\u9700\u8981\u8be2\u95ee\u73a9\u5bb6&#8221;\u7684\u95ee\u9898\uff1a<strong>\u73a9\u5bb6\u5728\u6253\u51fa\u5965\u79d8\u65f6\u5c31\u5df2\u7ecf\u505a\u5b8c\u4e86\u51b3\u7b56<\/strong>\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u540c\u6837\u7684\u8bbe\u8ba1\u54f2\u5b66\u4e5f\u51fa\u73b0\u5728\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>\u4ea1\u8bed<\/strong>\uff1a\u4e0d\u8be2\u95ee\uff0c\u81ea\u52a8\u6267\u884c<\/li>\n\n\n\n<li><strong>\u300a\u94dc\u987b\u300b\/\u300a\u5632\u8bbd\u300b<\/strong>\uff1a\u6539\u53d8\u653b\u51fb\u76ee\u6807\uff0c\u4e0d\u8be2\u95ee<\/li>\n\n\n\n<li><strong>\u300a\u6cd5\u672f\u53cd\u5236\u300b<\/strong>\uff1a\u81ea\u52a8\u53cd\u5236\uff0c\u4e0d\u8be2\u95ee<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4e00\u6761\u53ef\u4ee5\u5e26\u8d70\u7684\u8bbe\u8ba1\u7ecf\u9a8c<\/strong>\uff1a\u5982\u679c\u4f60\u7684\u5f15\u64ce\u9700\u8981\u5728\u7ed3\u7b97\u4e2d\u9014\u8be2\u95ee\u73a9\u5bb6\uff0c\u4f60\u5c31\u9700\u8981\u4e00\u4e2a\u53ef\u6302\u8d77\/\u53ef\u6062\u590d\u7684\u72b6\u6001\u673a\uff08\u534f\u7a0b\u6216\u8005\u663e\u5f0f\u7684\u72b6\u6001\u4fdd\u5b58\uff09\u3002\u7089\u77f3\u901a\u8fc7\u628a\u6240\u6709\u51b3\u7b56\u524d\u7f6e\uff0c\u8ba9\u6574\u4e2a\u7ed3\u7b97\u53d8\u6210\u4e00\u4e2a\u53ef\u4ee5\u540c\u6b65\u8dd1\u5b8c\u7684\u7eaf\u51fd\u6570\u3002\u8fd9\u5728\u5de5\u7a0b\u4e0a\u7684\u6536\u76ca\u662f\u5de8\u5927\u7684\uff1a<strong>\u5f15\u64ce\u53ef\u4ee5\u88ab AI \u4ee5\u6bcf\u79d2\u6570\u4e07\u6b21\u7684\u901f\u5ea6\u8c03\u7528\u6765\u505a\u641c\u7d22<\/strong>\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">5.3 \u552f\u4e00\u7684\u4f8b\u5916\uff1a\u53d1\u73b0\uff08Discover\uff09<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u300a\u53d1\u73b0\u300b\u662f\u7089\u77f3\u91cc\u5c11\u6570\u51e0\u4e2a<strong>\u5fc5\u987b\u5728\u7ed3\u7b97\u4e2d\u9014\u505c\u4e0b\u6765\u7b49\u73a9\u5bb6\u8f93\u5165<\/strong>\u7684\u673a\u5236\u3002\u5b83\u662f\u600e\u4e48\u5b9e\u73b0\u7684\uff1f<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u771f\u5b9e\u505a\u6cd5\uff08<strong>\u63a8\u65ad<\/strong>\uff09\uff1a\u628a <code>Discover<\/code> \u5efa\u6a21\u6210\u4e00\u4e2a&#8221;\u6682\u505c\u70b9&#8221;\uff0c\u5f15\u64ce\u628a\u72b6\u6001\u6302\u8d77\uff0c\u7b49\u5ba2\u6237\u7aef\u56de\u4f20\u9009\u62e9\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class DiscoverPending(Exception):<br>    \"\"\"\u7ed3\u7b97\u4e2d\u65ad\u4fe1\u53f7\uff1a\u9700\u8981\u73a9\u5bb6\u8f93\u5165\u3002\"\"\"<br>    def __init__(self, options, continuation):<br>        self.options = options<br>        self.continuation = continuation<br><br><br>class Discover(Action):<br>    def __init__(self, pool: \"Selector\", count: int = 3, then: \"Action\" = None):<br>        self.pool, self.count, self.then = pool, count, then<br><br>    def run(self, game, ctx):<br>        candidates = self.pool.eval(game, ctx)<br>        picks = game.rng.sample(candidates, min(self.count, len(candidates)))<br>        if game.is_simulation:<br>            # AI \u6a21\u62df\u65f6\u4e0d\u505c\u987f\uff0c\u76f4\u63a5\u6309\u7b56\u7565\u9009\uff08\u6216\u679a\u4e3e\u6240\u6709\u5206\u652f\uff09<br>            chosen = game.sim_policy.pick(picks)<br>            return self._apply(game, ctx, chosen)<br>        # \u771f\u5b9e\u5bf9\u5c40\uff1a\u629b\u51fa\u4e2d\u65ad\uff0c\u7531\u5916\u5c42\u4fdd\u5b58 continuation<br>        raise DiscoverPending(picks, lambda chosen: self._apply(game, ctx, chosen))<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u5916\u5c42\u4e3b\u5faa\u73af\u6355\u83b7\u5b83\uff0c\u628a <code>continuation<\/code> \u5b58\u8fdb <code>game.pending<\/code>\uff0c\u5411\u5ba2\u6237\u7aef\u53d1\u4e00\u4e2a <code>EntityChoices<\/code> \u5305\uff0c\u7b49\u73a9\u5bb6\u56de\u4f20\u540e\u6062\u590d\u6267\u884c\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8fd9\u4e2a\u8bbe\u8ba1\u7684\u4ee3\u4ef7\u5f88\u5927<\/strong>\uff1a\u5b83\u8ba9&#8221;\u7ed3\u7b97\u662f\u539f\u5b50\u7684&#8221;\u8fd9\u4e00\u6027\u8d28\u88ab\u6253\u7834\u4e86\u3002\u6240\u4ee5\u7089\u77f3\u5bf9 Discover \u6709\u4e25\u683c\u9650\u5236\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Discover \u671f\u95f4\u5bf9\u624b\u4e0d\u80fd\u884c\u52a8<\/strong>\uff08\u6e38\u620f\u662f\u56de\u5408\u5236\u7684\uff0c\u5929\u7136\u6ee1\u8db3\uff09<\/li>\n\n\n\n<li>Discover \u6709\u8d85\u65f6\uff08\u5927\u7ea6 25 \u79d2\uff09\uff0c\u8d85\u65f6\u968f\u673a\u9009<\/li>\n\n\n\n<li><strong>Discover \u4e0d\u80fd\u5d4c\u5957\u5728 Discover \u91cc<\/strong>\uff08\u8bbe\u8ba1\u4e0a\u907f\u514d\uff09<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">\u5982\u679c\u4f60\u5728\u81ea\u5df1\u7684\u5f15\u64ce\u91cc\u8981\u652f\u6301\u8fd9\u7c7b\u673a\u5236\uff0c\u6211\u5f3a\u70c8\u5efa\u8bae\u7528 <strong>Python \u7684 generator \/ async<\/strong> \u800c\u4e0d\u662f\u5f02\u5e38\uff0c\u4ee3\u7801\u4f1a\u5e72\u51c0\u5f97\u591a\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">async def play_card(self, card, target=None):<br>    ...<br>    if card.defn.battlecry:<br>        await self.run(card.defn.battlecry, source=card, target=target)<br><br>class Discover(Action):<br>    async def run(self, game, ctx):<br>        picks = game.rng.sample(self.pool.eval(game, ctx), 3)<br>        chosen = await game.ask_choice(ctx.controller, picks)   # \u6302\u8d77\u70b9<br>        await self._apply(game, ctx, chosen)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>await game.ask_choice(...)<\/code> \u5929\u7136\u8868\u8fbe\u4e86&#8221;\u6302\u8d77\u7b49\u5f85&#8221;\uff0c\u800c\u4e14 AI \u6a21\u62df\u65f6\u53ef\u4ee5\u7ed9\u4e00\u4e2a\u7acb\u5373\u8fd4\u56de\u7684 <code>ask_choice<\/code> \u5b9e\u73b0\u3002\u8fd9\u662f\u6211\u8ba4\u4e3a<strong>\u6700\u503c\u5f97\u4ece\u8fd9\u4efd\u5206\u6790\u91cc\u501f\u9274\u7684\u4e00\u6761\u5de5\u7a0b\u5efa\u8bae<\/strong>\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">5.4 &#8220;\u5feb\u7167&#8221;\uff1a\u4ea1\u8bed\u5fc5\u987b\u8bb0\u4f4f\u4ec0\u4e48<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u961f\u5217\u8bed\u4e49\u5e26\u6765\u4e00\u4e2a\u95ee\u9898\uff1a<strong>\u89e6\u53d1\u5668\u6267\u884c\u65f6\uff0c\u89e6\u53d1\u5b83\u7684\u90a3\u4e2a\u5b9e\u4f53\u53ef\u80fd\u5df2\u7ecf\u4e0d\u5b58\u5728\u4e86<\/strong>\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u4ea1\u8bed\u6700\u5178\u578b\uff1a<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">\u300a\u98de\u5200\u6742\u800d\u8005\u300b\uff1a&#8221;\u6bcf\u5f53\u4f60\u53ec\u5524\u4e00\u4e2a\u968f\u4ece\uff0c\u5c31\u5bf9\u4e00\u4e2a\u968f\u673a\u654c\u4eba\u9020\u6210 1 \u70b9\u4f24\u5bb3\u3002&#8221; \u300a\u6050\u72fc\u524d\u950b\u300b\uff1a\u4ea1\u8bed\u2014\u2014\u4e0d\uff0c\u6362\u4e2a\u4f8b\u5b50\u3002 \u300a\u8718\u86db\u5766\u514b\u300b\uff1a\u4ea1\u8bed\uff1a\u53ec\u5524\u4e00\u4e2a 1\/1 \u7684\u673a\u68b0\u3002<strong>\u5728\u54ea\u4e2a\u4f4d\u7f6e\u53ec\u5524\uff1f<\/strong><\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">\u7b54\u6848\u662f\uff1a<strong>\u5728\u5b83\u6b7b\u4ea1\u65f6\u6240\u5904\u7684\u4f4d\u7f6e<\/strong>\u3002\u4f46\u7b49\u4ea1\u8bed\u6267\u884c\u65f6\uff0c\u5b83\u5df2\u7ecf\u4e0d\u5728\u573a\u4e0a\u4e86\uff08<code>ZONE = GRAVEYARD<\/code>\uff09\u3002\u6240\u4ee5\u5f15\u64ce\u5fc5\u987b\u5728<strong>\u6b7b\u4ea1\u77ac\u95f4<\/strong>\u8bb0\u5f55\u5feb\u7167\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">@dataclass<br>class DeathSnapshot:<br>    entity_id: int<br>    card_id: str<br>    controller: int<br>    position: int          # \u6b7b\u4ea1\u65f6\u7684\u6218\u573a\u4f4d\u7f6e<br>    attack: int            # \u6b7b\u4ea1\u65f6\u7684\u653b\u51fb\u529b\uff08\u300a\u672b\u65e5\u9884\u8a00\u8005\u300b\u7c7b\u9700\u8981\uff09<br>    health: int<br>    race: str | None<br>    enchantments: list     # \u6b7b\u4ea1\u65f6\u7684\u9644\u9b54\uff08\u300a\u6069\u4f50\u65af\u300b\u9700\u8981\uff09<br>    adjacent: tuple        # \u6b7b\u4ea1\u65f6\u7684\u76f8\u90bb\u968f\u4ece\uff08\u5feb\u7167\uff0c\u56e0\u4e3a\u5b83\u4eec\u53ef\u80fd\u4e5f\u6b7b\u4e86\uff09<br><br><br>def mark_death(game, e: Entity):<br>    snap = DeathSnapshot(<br>        entity_id=e[Tag.ENTITY_ID],<br>        card_id=e[Tag.CARD_ID],<br>        controller=e[Tag.CONTROLLER],<br>        position=e[Tag.ZONE_POSITION],<br>        attack=e[Tag.ATK], health=e[Tag.HEALTH],<br>        race=e[Tag.CARDRACE],<br>        enchantments=list(e._enchantments),<br>        adjacent=game.neighbors(e),<br>    )<br>    e.death_snapshot = snap<br>    e[Tag.TO_BE_DESTROYED] = 1<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u5386\u53f2 bug \u6559\u8bad\uff1a\u65e9\u671f\u7089\u77f3\u91cc\uff0c\u300a\u8718\u86db\u5766\u514b\u300b\u7684\u4ea1\u8bed\u5728\u4e24\u4e2a\u5766\u514b\u540c\u65f6\u6b7b\u4ea1\u65f6\u4f1a\u53ec\u5524\u5230\u9519\u8bef\u7684\u4f4d\u7f6e\uff0c\u56e0\u4e3a\u7b2c\u4e00\u4e2a\u4ea1\u8bed\u6267\u884c\u65f6\u6539\u53d8\u4e86\u6218\u573a\u5e03\u5c40\uff0c\u7b2c\u4e8c\u4e2a\u4ea1\u8bed\u7528\u7684\u662f&#8221;\u5f53\u524d\u4f4d\u7f6e&#8221;\u3002\u5feb\u7167\u4fee\u590d\u4e86\u8fd9\u4e2a\u95ee\u9898\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u63a8\u5e7f\u7684\u8bbe\u8ba1\u539f\u5219<\/strong>\uff1a\u4efb\u4f55<strong>\u5ef6\u8fdf\u6267\u884c<\/strong>\u7684\u6548\u679c\uff0c\u90fd\u5fc5\u987b\u5728<strong>\u6ce8\u518c\u65f6<\/strong>\u6355\u83b7\u5b83\u9700\u8981\u7684\u6240\u6709\u4e0a\u4e0b\u6587\uff0c\u800c\u4e0d\u662f\u5728\u6267\u884c\u65f6\u8bfb\u53d6\u3002\u8fd9\u5728\u4e8b\u4ef6\u9a71\u52a8\u7cfb\u7edf\u91cc\u662f\u901a\u7528\u89c4\u5f8b\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">5.5 \u4e00\u4e2a\u5b8c\u6574\u7684\u65f6\u5e8f\u63a8\u6f14<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u8ba9\u6211\u4eec\u5b8c\u6574\u63a8\u6f14\u4e00\u4e2a\u590d\u6742\u573a\u9762\uff0c\u8fd9\u80fd\u628a\u524d\u9762\u6240\u6709\u6982\u5ff5\u4e32\u8d77\u6765\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u573a\u9762<\/strong>\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u4f60\u65b9\u6218\u573a\uff08\u6309\u4e0a\u573a\u987a\u5e8f\uff09\uff1a<code>[\u98de\u5200\u6742\u800d\u8005(2\/2), \u4e0d\u7a33\u5b9a\u7684\u98df\u5c38\u9b3c(1\/1\uff0c\u5df2\u53d7\u4f24), \u8840\u6cd5\u5e08\u8428\u5c14\u8bfa\u65af(1\/1\uff0c\u6cd5\u672f\u4f24\u5bb3+1)]<\/code><\/li>\n\n\n\n<li>\u654c\u65b9\u6218\u573a\uff1a<code>[\u9c7c\u4eba\u730e\u6f6e\u8005(2\/1), \u6de1\u6c34\u9cc4(2\/3)]<\/code><\/li>\n\n\n\n<li>\u4f60\u6253\u51fa\u300a\u5965\u672f\u5c04\u51fb\u300b\uff08\u9020\u6210 2 \u70b9\u4f24\u5bb3\uff09\u76ee\u6807\uff1a\u654c\u65b9\u300a\u6de1\u6c34\u9cc4\u300b<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u63a8\u6f14<\/strong>\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">T0  play_card(\u5965\u672f\u5c04\u51fb, target=\u6de1\u6c34\u9cc4)<br>    \u251c\u2500 compute_cost = 1\uff0c\u6263\u8d39<br>    \u251c\u2500 emit(PRE_PLAY)               \u2192 \u961f\u5217\u7a7a<br>    \u251c\u2500 move(\u5965\u672f\u5c04\u51fb \u2192 SETASIDE)<br>    \u251c\u2500 emit(PLAY_CARD)              \u2192 \u961f\u5217\u7a7a<br>    \u251c\u2500 emit(SPELL_CAST)             \u2192 \u961f\u5217\u7a7a\uff08\u5bf9\u65b9\u65e0\u5965\u79d8\uff09<br>    \u2514\u2500 run(spell)<br>        \u2514\u2500 Damage(target=\u6de1\u6c34\u9cc4, amount=2 + spellpower)<br>            \u251c\u2500 spellpower = 1\uff08\u8428\u5c14\u8bfa\u65af\uff09\u2192 amount = 3<br>            \u251c\u2500 emit(PRE_DAMAGE, target=\u6de1\u6c34\u9cc4, amount=3)<br>            \u2502    \u2192 \u65e0\u5723\u76fe\uff0c\u65e0\u4fee\u9970<br>            \u251c\u2500 \u6de1\u6c34\u9cc4[DAMAGE] += 3  \u2192 3\/3 \u53d7 3 \u4f24 \u2192 \u6b7b\u4ea1\u5f85\u5b9a<br>            \u2514\u2500 emit(DAMAGE, target=\u6de1\u6c34\u9cc4, amount=3, fatal=True)<br>                 \u2192 \u961f\u5217\u7a7a\uff08\u65e0\"\u53d7\u5230\u4f24\u5bb3\u65f6\"\u89e6\u53d1\u5668\uff09<br><br>T1  resolve_queue()  \u2192 \u961f\u5217\u5df2\u7a7a\uff0c\u65e0\u4e8b\u53d1\u751f<br><br>T2  process_deaths()<br>    \u251c\u2500 \u626b\u63cf\u5168\u573a\uff0c\u627e current_health &lt;= 0 \u7684\u5b9e\u4f53\uff1a[\u6de1\u6c34\u9cc4]<br>    \u251c\u2500 mark_death(\u6de1\u6c34\u9cc4) \u2192 \u5feb\u7167<br>    \u251c\u2500 move(\u6de1\u6c34\u9cc4 \u2192 GRAVEYARD)<br>    \u251c\u2500 aura_bus.mark_dirty()   \u2190 \u573a\u9762\u53d8\u4e86\uff0c\u5149\u73af\u8981\u91cd\u7b97<br>    \u251c\u2500 emit(DEATH, entity=\u6de1\u6c34\u9cc4)<br>    \u2502    \u2192 \u5339\u914d\uff1a\u6de1\u6c34\u9cc4\u65e0\u4ea1\u8bed \u2192 \u961f\u5217\u7a7a<br>    \u2514\u2500 resolve_queue() \u2192 \u7a7a<br><br>T3  emit(AFTER_PLAY) \u2192 \u961f\u5217\u7a7a<br>T4  \u5965\u672f\u5c04\u51fb \u2192 GRAVEYARD<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u73b0\u5728\u6362\u4e00\u4e2a\u590d\u6742\u7684\uff1a<strong>\u4f60\u6253\u51fa\u300a\u66b4\u98ce\u96ea\u300b\uff08\u5bf9\u6240\u6709\u654c\u65b9\u968f\u4ece\u9020\u6210 2 \u70b9\u4f24\u5bb3\uff09<\/strong>\uff0c\u4f46\u654c\u65b9\u662f <code>[\u6050\u72fc\u524d\u950b(2\/2\uff0c\u5149\u73af\uff1a\u76f8\u90bb+1\u653b), \u4e0d\u7a33\u5b9a\u7684\u98df\u5c38\u9b3c(1\/1), \u8718\u86db\u5766\u514b(1\/4)]<\/code>\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">T0  Damage(all_enemy_minions, 2)<br>    \u2605 \u5173\u952e\uff1a\u76ee\u6807\u5217\u8868\u5728 Action \u5f00\u59cb\u65f6\u5c31\u56fa\u5b9a\uff08\u5feb\u7167\uff09\uff0c<br>      \u5373\u4f7f\u4e2d\u9014\u6709\u968f\u4ece\u6b7b\u4ea1\uff0c\u4e5f\u4e0d\u4f1a\u91cd\u65b0\u8ba1\u7b97\u76ee\u6807<br>    \u251c\u2500 Damage(\u6050\u72fc\u524d\u950b, 2)   \u2192 2\/2 \u6b7b\u4ea1\u5f85\u5b9a<br>    \u251c\u2500 Damage(\u98df\u5c38\u9b3c, 2)     \u2192 1\/1 \u6b7b\u4ea1\u5f85\u5b9a<br>    \u2514\u2500 Damage(\u8718\u86db\u5766\u514b, 2)   \u2192 1\/4 \u53d7 2 \u4f24\uff0c\u5b58\u6d3b<br><br>T1  resolve_queue() \u2192 \u7a7a<br><br>T2  process_deaths()  \u7b2c\u4e00\u8f6e<br>    \u251c\u2500 \u6b7b\u4ea1\u5217\u8868 = [\u6050\u72fc\u524d\u950b, \u98df\u5c38\u9b3c]  \u6309 ENTITY_ID \u6392\u5e8f<br>    \u251c\u2500 \u4e24\u8005\u540c\u65f6\u5feb\u7167\u3001\u540c\u65f6\u79fb\u5165\u5893\u5730<br>    \u251c\u2500 aura_bus.mark_dirty() \u2192 \u6050\u72fc\u524d\u950b\u7684\u5149\u73af\u6d88\u5931<br>    \u2502    \u2192 \u8718\u86db\u5766\u514b\u5982\u679c\u5728\u5b83\u65c1\u8fb9\uff0c\u653b\u51fb\u529b -1\uff08\u4f46\u751f\u547d\u503c\u4e0d\u53d8\uff09<br>    \u251c\u2500 emit(DEATH, \u6050\u72fc\u524d\u950b) \u2192 \u65e0\u4ea1\u8bed<br>    \u251c\u2500 emit(DEATH, \u98df\u5c38\u9b3c)   \u2192 \u4ea1\u8bed\u5165\u961f<br>    \u2514\u2500 resolve_queue()<br>        \u2514\u2500 \u98df\u5c38\u9b3c\u4ea1\u8bed\uff1a\u5bf9\u6240\u6709\u968f\u4ece\u9020\u6210 1 \u70b9\u4f24\u5bb3<br>            \u251c\u2500 \u76ee\u6807\u5feb\u7167 = \u5f53\u524d\u6240\u6709\u968f\u4ece\uff08\u542b\u5df1\u65b9\uff01\uff09<br>            \u251c\u2500 \u8718\u86db\u5766\u514b 1\/4 \u5df2\u53d7 2 \u4f24 \u2192 \u518d\u53d7 1 \u4f24 = 3 \u4f24\uff0c\u5b58\u6d3b<br>            \u2514\u2500 \u4f60\u65b9\u7684\u98de\u5200\u6742\u800d\u8005\u7b49\u4e5f\u53d7\u4f24<br><br>T3  process_deaths()  \u7b2c\u4e8c\u8f6e<br>    \u2514\u2500 \u68c0\u67e5\u662f\u5426\u6709\u65b0\u6b7b\u4ea1 \u2192 \u82e5\u6709\u5219\u91cd\u590d T2<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6ce8\u610f T2 \u7684\u987a\u5e8f<\/strong>\uff1a\u6240\u6709\u6b7b\u4ea1\u5b9e\u4f53<strong>\u5148\u5168\u90e8\u79fb\u5165\u5893\u5730<\/strong>\uff0c\u7136\u540e\u624d\u9010\u4e2a\u89e6\u53d1\u4ea1\u8bed\u3002\u8fd9\u5c31\u662f\u4e3a\u4ec0\u4e48&#8221;\u4e24\u4e2a\u98df\u5c38\u9b3c\u540c\u65f6\u6b7b&#8221;\u65f6\uff0c\u7b2c\u4e8c\u4e2a\u98df\u5c38\u9b3c\u7684\u4ea1\u8bed\u4f9d\u7136\u4f1a\u89e6\u53d1\uff08\u5b83\u5df2\u7ecf\u6b7b\u4e86\uff0c\u4f46\u89e6\u53d1\u5668\u5df2\u5165\u961f\uff09\uff0c\u800c\u4e14\u5b83\u7684\u4ea1\u8bed\u4f24\u5bb3<strong>\u4e0d\u4f1a<\/strong>\u518d\u6253\u5230\u7b2c\u4e00\u4e2a\u98df\u5c38\u9b3c\uff08\u5df2\u5728\u5893\u5730\uff09\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 6 \u7ae0 \u5149\u73af\uff08Aura\uff09\u4e0e\u6301\u7eed\u6548\u679c\u91cd\u7b97<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">6.1 \u5149\u73af vs \u9644\u9b54\uff1a\u4e00\u6761\u6e05\u6670\u7684\u5206\u754c<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th><\/th><th>\u9644\u9b54\uff08Enchantment\uff09<\/th><th>\u5149\u73af\uff08Aura\uff09<\/th><\/tr><\/thead><tbody><tr><td>\u4f8b\u5b50<\/td><td>\u300a\u738b\u8005\u795d\u798f\u300b+4\/+4<\/td><td>\u300a\u66b4\u98ce\u57ce\u52c7\u58eb\u300b\u5176\u4ed6\u968f\u4ece+1\/+1<\/td><\/tr><tr><td>\u5b58\u50a8\u65b9\u5f0f<\/td><td>\u9644\u7740\u5b9e\u4f53\uff0c\u6301\u4e45<\/td><td>\u7531\u6e90\u5b9e\u4f53\u6301\u7eed\u63d0\u4f9b<\/td><\/tr><tr><td>\u6e90\u6b7b\u4ea1\u540e<\/td><td>\u4fdd\u7559<\/td><td><strong>\u7acb\u5373\u6d88\u5931<\/strong><\/td><\/tr><tr><td>\u6c89\u9ed8\u5bbf\u4e3b<\/td><td>\u79fb\u9664<\/td><td><strong>\u4e0d\u79fb\u9664<\/strong>\uff08\u5149\u73af\u6765\u81ea\u522b\u5904\uff09<\/td><\/tr><tr><td>\u6c89\u9ed8\u6e90<\/td><td>\u2014<\/td><td>\u6d88\u5931<\/td><\/tr><tr><td>\u590d\u5236\u968f\u4ece\u65f6<\/td><td>\u53d6\u51b3\u4e8e\u5361\uff08\u5927\u591a\u4e0d\u590d\u5236\uff09<\/td><td>\u65b0\u968f\u4ece\u81ea\u52a8\u83b7\u5f97\uff08\u56e0\u4e3a\u5149\u73af\u91cd\u65b0\u8ba1\u7b97\uff09<\/td><\/tr><tr><td>\u5b9e\u73b0<\/td><td>\u5199\u5165 tag + \u56de\u6eda\u8bb0\u5f55<\/td><td>\u6bcf\u6b21\u810f\u6807\u8bb0\u540e\u5168\u91cf\u91cd\u7b97<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5224\u65ad\u6807\u51c6<\/strong>\uff1a\u5361\u9762\u6587\u672c\u662f&#8221;\u83b7\u5f97\uff08Gain\uff09&#8221;\u2192 \u9644\u9b54\uff1b&#8221;\u62e5\u6709\uff08Have\uff09\/ \u5176\u4ed6\u968f\u4ece\u2026\u2026&#8221;\u2192 \u5149\u73af\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">6.2 \u4e3a\u4ec0\u4e48\u5fc5\u987b\u5168\u91cf\u91cd\u7b97<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u6734\u7d20\u7684\u60f3\u6cd5\u662f\u589e\u91cf\u7ef4\u62a4\uff1a\u6e90\u4e0a\u573a\u65f6\u7ed9\u6240\u6709\u968f\u4ece +1\/+1\uff0c\u6e90\u79bb\u573a\u65f6\u7ed9\u6240\u6709\u968f\u4ece -1\/-1\u3002<strong>\u8fd9\u662f\u9519\u7684<\/strong>\uff0c\u56e0\u4e3a\uff1a<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>\u6e90\u79bb\u573a\u548c\u65b0\u968f\u4ece\u4e0a\u573a\u7684\u987a\u5e8f\u4f1a\u5bfc\u81f4\u6f0f\u52a0\/\u6f0f\u51cf<\/li>\n\n\n\n<li>\u5149\u73af\u6709\u6761\u4ef6\uff08\u300a\u96f7\u8bfa\u300b\u7c7b&#8221;\u5982\u679c\u4f60\u7684\u724c\u5e93\u6ca1\u6709\u91cd\u590d\u5361&#8221;\uff09\uff0c\u6761\u4ef6\u53d8\u5316\u65f6\u8981\u91cd\u7b97<\/li>\n\n\n\n<li>\u5149\u73af\u53ef\u4ee5\u5d4c\u5957\uff08A \u7684\u5149\u73af\u8ba9 B \u7684\u5149\u73af\u751f\u6548\uff09<\/li>\n\n\n\n<li>\u6c89\u9ed8\u3001\u53d8\u5f62\u3001\u5fc3\u7075\u63a7\u5236\u90fd\u4f1a\u6539\u53d8\u5149\u73af\u7684\u9002\u7528\u8303\u56f4<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">\u771f\u5b9e\u5f15\u64ce\u7684\u505a\u6cd5\u662f<strong>\u810f\u6807\u8bb0 + \u5168\u91cf\u91cd\u7b97<\/strong>\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"># auras.py<br>@dataclass(frozen=True)<br>class AuraDef:<br>    selector: \"Selector\"          # \u5f71\u54cd\u8c01<br>    tag: Tag                      # \u6539\u54ea\u4e2a\u6807\u7b7e<br>    amount: int | \"Callable\" = 0  # \u6539\u591a\u5c11\uff08\u53ef\u4ee5\u662f\u52a8\u6001\u7684\uff09<br>    condition: \"Condition|None\" = None   # \u5149\u73af\u751f\u6548\u6761\u4ef6<br>    include_self: bool = False<br><br><br>class AuraBus:<br>    def __init__(self, game):<br>        self.game = game<br>        self._dirty = True<br>        # entity_id -&gt; {tag: delta}<br>        self._deltas: dict[int, dict[Tag, int]] = {}<br><br>    def mark_dirty(self):<br>        self._dirty = True<br><br>    def delta(self, e: Entity, tag: Tag) -&gt; int:<br>        if self._dirty:<br>            self.recompute()<br>        return self._deltas.get(e.id, {}).get(tag, 0)<br><br>    def recompute(self):<br>        self._dirty = False<br>        old = self._deltas<br>        new: dict[int, dict[Tag, int]] = defaultdict(lambda: defaultdict(int))<br><br>        # \u6536\u96c6\u6240\u6709\u5149\u73af\u6e90\uff08\u5fc5\u987b\u5728 PLAY \u533a\uff0c\u4e14\u672a\u88ab\u6c89\u9ed8\uff09<br>        sources = []<br>        for pid in (0, 1):<br>            for e in self.game.zones.get(pid, Zone.PLAY):<br>                if e.has(Tag.SILENCED):<br>                    continue<br>                for ad in e.defn.auras:<br>                    sources.append((e, ad))<br>            # \u6b66\u5668\u3001\u82f1\u96c4\u6280\u80fd\u3001\u5730\u6807\u4e5f\u80fd\u63d0\u4f9b\u5149\u73af<br>            for e in (self.game.player(pid).weapon, self.game.player(pid).hero_power):<br>                if e:<br>                    for ad in e.defn.auras:<br>                        sources.append((e, ad))<br>        # \u624b\u724c\u4e2d\u751f\u6548\u7684\u5149\u73af\uff08\u5982\u300a\u5143\u7d20\u300b\u7cfb\"\u4f60\u7684\u624b\u724c\u4e2d\u7684X\u8d39\u7528-1\"\uff09<br>        for pid in (0, 1):<br>            for e in self.game.zones.get(pid, Zone.HAND):<br>                for ad in getattr(e.defn, \"hand_auras\", ()):<br>                    sources.append((e, ad))<br><br>        # \u2605 \u7a33\u5b9a\u6392\u5e8f\uff1a\u4fdd\u8bc1\u91cd\u7b97\u7ed3\u679c\u4e0e\u987a\u5e8f\u65e0\u5173\uff08\u5bf9\u4e8e\u52a0\u6cd5\u5929\u7136\u6ee1\u8db3\uff0c<br>        #   \u4f46\u5bf9\u4e8e SET \u7c7b\u5149\u73af\uff08\u5982\"\u4f60\u7684\u968f\u4ece\u653b\u51fb\u529b\u53d8\u4e3a 1\"\uff09\u987a\u5e8f\u6709\u610f\u4e49\uff09<br>        sources.sort(key=lambda x: (x[0][Tag.ENTITY_ID],))<br><br>        for src, ad in sources:<br>            if ad.condition and not ad.condition.eval(self.game, src, None):<br>                continue<br>            amount = ad.amount(self.game, src) if callable(ad.amount) else ad.amount<br>            for tgt in ad.selector.eval(self.game, Ctx(source=src)):<br>                if tgt is src and not ad.include_self:<br>                    continue<br>                new[tgt.id][ad.tag] += amount<br><br>        self._deltas = {k: dict(v) for k, v in new.items()}<br>        self._apply_diff(old, self._deltas)<br><br>    def _apply_diff(self, old, new):<br>        \"\"\"\u628a\u5149\u73af\u5dee\u503c\u5199\u56de\u5b9e\u4f53 tag\uff0c\u5e76\u5e7f\u64ad\u7ed9\u5ba2\u6237\u7aef\u3002\"\"\"<br>        touched = set(old) | set(new)<br>        for eid in touched:<br>            e = self.game.entities.get(eid)<br>            if e is None:<br>                continue<br>            o, n = old.get(eid, {}), new.get(eid, {})<br>            for tag in set(o) | set(n):<br>                d = n.get(tag, 0) - o.get(tag, 0)<br>                if d:<br>                    e.tags[tag] = e.tags.get(tag, 0) + d<br>                    self.game.record_tag_change(e, tag, e.tags[tag] - d, e.tags[tag])<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">6.3 \u91cd\u7b97\u65f6\u673a\uff1a\u4e00\u4e2a\u9690\u853d\u7684\u5751<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u5149\u73af\u4ec0\u4e48\u65f6\u5019\u91cd\u7b97\uff1f\u7b54\u6848\u662f\uff1a<strong>\u5728\u4efb\u4f55\u53ef\u80fd\u6539\u53d8\u5149\u73af\u9002\u7528\u8303\u56f4\u7684\u72b6\u6001\u53d8\u5316\u4e4b\u540e\uff0c\u4e14\u5728\u4e0b\u4e00\u6b21\u8bfb\u53d6\u4e4b\u524d<\/strong>\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u5177\u4f53\u7684\u89e6\u53d1\u70b9\uff08<strong>\u63a8\u65ad<\/strong>\uff09\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">AURA_DIRTY_TRIGGERS = {<br>    \"\u5b9e\u4f53\u8fdb\u5165\u6216\u79bb\u5f00 PLAY \u533a\",<br>    \"CONTROLLER \u53d8\u5316\uff08\u5fc3\u7075\u63a7\u5236\uff09\",<br>    \"SILENCED \u53d8\u5316\",<br>    \"CARDRACE \/ CARDTYPE \u53d8\u5316\uff08\u53d8\u5f62\uff09\",<br>    \"ZONE_POSITION \u53d8\u5316\uff08\u76f8\u90bb\u7c7b\u5149\u73af\uff09\",<br>    \"\u5149\u73af\u6761\u4ef6\u6d89\u53ca\u7684\u4efb\u4f55 tag \u53d8\u5316\",<br>    \"\u624b\u724c\/\u724c\u5e93\u6570\u91cf\u53d8\u5316\uff08\u5982\u679c\u6709\u76f8\u5173\u5149\u73af\uff09\",<br>}<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u6700\u540e\u4e00\u6761\u6700\u9ebb\u70e6\u3002\u300a\u6d77\u5de8\u4eba\u300b\u7684\u8d39\u7528\u53d6\u51b3\u4e8e\u573a\u4e0a\u968f\u4ece\u6570\uff0c\u300a\u96f7\u8bfa\u300b\u7684\u6761\u4ef6\u53d6\u51b3\u4e8e\u724c\u5e93\u5185\u5bb9\u3002\u6734\u7d20\u505a\u6cd5\u662f<strong>\u6bcf\u6b21\u4efb\u4f55 tag \u53d8\u5316\u90fd\u6807\u810f<\/strong>\uff0c\u4ee3\u4ef7\u662f\u6027\u80fd\u3002\u771f\u5b9e\u7089\u77f3\u91c7\u7528\u7684\uff08<strong>\u63a8\u65ad<\/strong>\uff09\u662f\u5206\u7ea7\u810f\u6807\u8bb0\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class AuraBus:<br>    DIRTY_BOARD = 1      # \u573a\u9762\u7ed3\u6784\u53d8\u4e86<br>    DIRTY_TAGS  = 2      # \u6570\u503c\u53d8\u4e86<br>    DIRTY_HAND  = 4<br>    DIRTY_DECK  = 8<br><br>    def mark_dirty(self, flags=0xFF):<br>        self._dirty |= flags<br><br>    def recompute(self):<br>        # \u53ea\u91cd\u7b97\u53d7\u5f71\u54cd\u7684\u5149\u73af<br>        for src, ad in self._sources:<br>            if not (ad.depends_on &amp; self._dirty):<br>                continue   # \u8df3\u8fc7<br>            ...<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u5728 Python \u8ff7\u4f60\u5f15\u64ce\u91cc\uff0c\u76f4\u63a5\u5168\u91cf\u91cd\u7b97\u662f\u5b8c\u5168\u591f\u7528\u7684\uff08\u4e00\u5c40\u6700\u591a\u51e0\u767e\u4e2a\u5b9e\u4f53\uff0c\u91cd\u7b97\u662f\u5fae\u79d2\u7ea7\uff09\u3002\u4f46\u5728\u771f\u5b9e\u4ea7\u54c1\u91cc\uff0c<strong>\u5149\u73af\u91cd\u7b97\u662f\u70ed\u70b9\u51fd\u6570<\/strong>\uff0c\u5c24\u5176\u662f\u9152\u9986\u6218\u68cb\uff0814 \u4e2a\u968f\u4ece \u00d7 \u82e5\u5e72\u5149\u73af \u00d7 \u6bcf\u6b21\u6218\u6597\u591a\u6b21\u91cd\u7b97\uff09\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">6.4 SET \u7c7b\u5149\u73af\uff1a\u987a\u5e8f\u771f\u7684\u6709\u610f\u4e49<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u5927\u90e8\u5206\u5149\u73af\u662f\u52a0\u6cd5\uff08<code>+1\/+1<\/code>\uff09\uff0c\u52a0\u6cd5\u53ef\u4ea4\u6362\uff0c\u987a\u5e8f\u65e0\u6240\u8c13\u3002\u4f46\u6709\u4e9b\u5149\u73af\u662f<strong>\u8d4b\u503c<\/strong>\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u300a\u5de8\u578b\u91ce\u732a\u300b\uff1a&#8221;\u6240\u6709\u968f\u4ece\u7684\u653b\u51fb\u529b\u53d8\u4e3a 1&#8243;\uff08\u5386\u53f2\u5361\uff09<\/li>\n\n\n\n<li>\u300a\u5b9d\u5e93\u770b\u5b88\u300b\u7c7b\uff1a&#8221;\u8fd9\u4e2a\u968f\u4ece\u7684\u653b\u51fb\u529b\u7b49\u4e8e\u5b83\u7684\u751f\u547d\u503c&#8221;<\/li>\n\n\n\n<li>\u300a\u6d77\u5de8\u4eba\u300b\u7684\u8d39\u7528<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">\u8d4b\u503c\u4e0d\u53ef\u4ea4\u6362\u3002\u5982\u679c A \u8bf4&#8221;\u653b\u51fb\u529b\u8bbe\u4e3a 1&#8243;\uff0cB \u8bf4&#8221;\u653b\u51fb\u529b +2&#8243;\uff0c\u7ed3\u679c\u53d6\u51b3\u4e8e\u987a\u5e8f\u3002\u7089\u77f3\u7684\u89c4\u5219\u662f\uff1a<strong>\u6309 ENTITY_ID \u987a\u5e8f\u5e94\u7528\uff0c\u8d4b\u503c\u8986\u76d6\u4e4b\u524d\u7684\u6240\u6709\u4fee\u6539<\/strong>\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class AuraOp(IntEnum):<br>    ADD = 0<br>    SET = 1<br>    MULTIPLY = 2<br><br># \u91cd\u7b97\u65f6\u5206\u5c42\uff1a\u5148\u5e94\u7528\u6240\u6709 SET\uff08\u6309 ENTITY_ID\uff09\uff0c\u518d MULTIPLY\uff0c\u6700\u540e ADD<br>def recompute_layered(self):<br>    for op in (AuraOp.SET, AuraOp.MULTIPLY, AuraOp.ADD):<br>        for src, ad in sorted(self._sources, key=lambda x: x[0][Tag.ENTITY_ID]):<br>            if ad.op != op:<br>                continue<br>            ...<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u8fd9\u672c\u8d28\u4e0a\u662f <strong>MTG \u7684&#8221;\u5206\u5c42\u7cfb\u7edf&#8221;\uff08Layer System\uff09\u7684\u7b80\u5316\u7248<\/strong>\u3002MTG \u6709 7 \u5c42 + \u65f6\u95f4\u6233 + \u4f9d\u8d56\u6027\u89c4\u5219\uff0c\u7089\u77f3\u53ea\u6709 3 \u5c42 + \u65f6\u95f4\u6233\u3002\u8fd9\u662f&#8221;\u964d\u4f4e\u590d\u6742\u5ea6\u6362\u53d6\u53ef\u7406\u89e3\u6027&#8221;\u7684\u53c8\u4e00\u4e2a\u4f8b\u5b50\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 7 \u7ae0 \u6b7b\u4ea1\u7ed3\u7b97\uff1a\u72b6\u6001\u52a8\u4f5c\u9636\u6bb5<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">7.1 \u4e3a\u4ec0\u4e48\u6b7b\u4ea1\u4e0d\u662f\u7acb\u5373\u7684<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u6734\u7d20\u5b9e\u73b0\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def damage(target, amount):<br>    target.health -= amount<br>    if target.health &lt;= 0:<br>        target.die()      # \u274c \u7acb\u5373\u6b7b\u4ea1<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u8fd9\u4f1a\u7acb\u523b\u70b8\u6389\u3002\u573a\u666f\uff1a<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">\u300a\u66b4\u98ce\u96ea\u300b\u5bf9\u6240\u6709\u654c\u65b9\u968f\u4ece\u9020\u6210 2 \u70b9\u4f24\u5bb3\u3002 \u654c\u65b9\u6709 3 \u4e2a 2 \u8840\u968f\u4ece\uff0c\u5176\u4e2d\u4e00\u4e2a\u662f\u300a\u4e0d\u7a33\u5b9a\u7684\u98df\u5c38\u9b3c\u300b\u3002<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">\u5982\u679c\u7acb\u5373\u6b7b\u4ea1\uff0c\u7b2c\u4e00\u4e2a\u968f\u4ece\u6b7b\u4ea1\u65f6\u5c31\u4f1a\u89e6\u53d1\u4ea1\u8bed\uff0c\u4ea1\u8bed\u7684\u4f24\u5bb3\u4f1a\u6253\u5230&#8221;\u8fd8\u6ca1\u53d7\u5230\u66b4\u98ce\u96ea\u4f24\u5bb3&#8221;\u7684\u968f\u4ece\u4e0a\u3002\u7ed3\u679c\u5b8c\u5168\u9519\u8bef\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u6b63\u786e\u505a\u6cd5\u662f<strong>\u4e24\u9636\u6bb5<\/strong>\uff1a<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>\u4f24\u5bb3\u9636\u6bb5<\/strong>\uff1a\u53ea\u6263\u8840\uff0c\u53ea\u6807\u8bb0 <code>TO_BE_DESTROYED<\/code>\uff0c\u4e0d\u89e6\u53d1\u4efb\u4f55\u6b7b\u4ea1\u903b\u8f91<\/li>\n\n\n\n<li><strong>\u6b7b\u4ea1\u7ed3\u7b97\u9636\u6bb5\uff08Death Phase\uff09<\/strong>\uff1a\u7edf\u4e00\u6536\u96c6\u6240\u6709\u5f85\u6b7b\u5b9e\u4f53\uff0c\u4e00\u6b21\u6027\u79fb\u5165\u5893\u5730\uff0c\u7136\u540e\u6309\u5e8f\u89e6\u53d1\u4ea1\u8bed<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">\u8fd9\u5c31\u662f MTG \u7684 <strong>State-Based Actions<\/strong>\uff0c\u7089\u77f3\u7684\u7b80\u5316\u7248\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">7.2 \u5b8c\u6574\u5b9e\u73b0<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">def process_deaths(self):<br>    \"\"\"\u6b7b\u4ea1\u7ed3\u7b97\u9636\u6bb5\u3002\u53ef\u80fd\u5faa\u73af\u591a\u6b21\uff0c\u76f4\u5230\u4e0d\u518d\u4ea7\u751f\u65b0\u6b7b\u4ea1\u3002\"\"\"<br>    rounds = 0<br>    while True:<br>        rounds += 1<br>        if rounds &gt; 64:<br>            self.log(\"DEATH_LOOP_GUARD\"); break<br><br>        # ---- \u6b65\u9aa4 1\uff1a\u6536\u96c6 ----<br>        dying = []<br>        for pid in (0, 1):<br>            for e in self.zones.get(pid, Zone.PLAY):<br>                if e[Tag.CARDTYPE] == CardType.MINION:<br>                    if e.current_health &lt;= 0 or e.has(Tag.TO_BE_DESTROYED):<br>                        dying.append(e)<br>            # \u6b66\u5668\u8010\u4e45<br>            w = self.player(pid).weapon<br>            if w and (w[Tag.DURABILITY] &lt;= 0 or w.has(Tag.TO_BE_DESTROYED)):<br>                dying.append(w)<br>            # \u82f1\u96c4<br>            h = self.player(pid).hero<br>            if h.current_health &lt;= 0:<br>                self.game_over_pending.add(pid)<br><br>        if not dying:<br>            break<br><br>        # ---- \u6b65\u9aa4 2\uff1a\u6392\u5e8f\uff08\u2605 \u5168\u5c40 ENTITY_ID \u5347\u5e8f\uff0c\u4e0d\u5206\u654c\u6211\uff09----<br>        dying.sort(key=lambda e: e[Tag.ENTITY_ID])<br><br>        # ---- \u6b65\u9aa4 3\uff1a\u5168\u90e8\u5feb\u7167\u5e76\u79fb\u5165\u5893\u5730\uff08\u5728\u89e6\u53d1\u4efb\u4f55\u4ea1\u8bed\u4e4b\u524d\uff01\uff09----<br>        for e in dying:<br>            mark_death(self, e)<br>            self.emit_immediate(Event.PROPOSED_DEATH, entity=e)   # \u53ef\u88ab\"\u514d\u6b7b\"\u6548\u679c\u62e6\u622a<br><br>        dying = [e for e in dying if not e.death_prevented]<br>        for e in dying:<br>            self.event_bus.unregister_nonDeath(e)   # \u6ce8\u9500\u975e\u4ea1\u8bed\u89e6\u53d1\u5668<br>            self.zones.move(e, Zone.GRAVEYARD)<br>            e[Tag.ZONE_POSITION] = 0<br><br>        self.aura_bus.mark_dirty()   # \u573a\u9762\u53d8\u4e86<br><br>        # ---- \u6b65\u9aa4 4\uff1a\u6309\u5e8f\u89e6\u53d1\u6b7b\u4ea1\u4e8b\u4ef6\uff08\u4ea1\u8bed\u5728\u6b64\u5165\u961f\uff09----<br>        for e in dying:<br>            self.emit(Event.DEATH, entity=e, snapshot=e.death_snapshot)<br><br>        # ---- \u6b65\u9aa4 5\uff1a\u7ed3\u7b97\u961f\u5217 ----<br>        self.event_bus.resolve()<br><br>        # ---- \u6b65\u9aa4 6\uff1a\u590d\u751f\uff08Reborn\uff09\u5728\u4ea1\u8bed\u4e4b\u540e ----<br>        for e in dying:<br>            if e.has(Tag.REBORN) and not e.reborn_used:<br>                self.summon_reborn(e)<br><br>        # ---- \u6b65\u9aa4 7\uff1aAFTER_DEATH\uff08\u5982\u300a\u96c6\u5e02\u6076\u9738\u300b\"\u53cb\u65b9\u968f\u4ece\u6b7b\u4ea1\u540e\"\uff09----<br>        for e in dying:<br>            self.emit(Event.AFTER_DEATH, entity=e)<br>        self.event_bus.resolve()<br><br>        # \u5faa\u73af\uff0c\u68c0\u67e5\u662f\u5426\u6709\u65b0\u6b7b\u4ea1<br><br>    # \u6e38\u620f\u7ed3\u675f\u5224\u5b9a\u653e\u5728\u6b7b\u4ea1\u5faa\u73af\u4e4b\u5916<br>    if self.game_over_pending:<br>        self.end_game()<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">7.3 \u51e0\u4e2a\u5173\u952e\u987a\u5e8f\u7684\u7406\u7531<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4e3a\u4ec0\u4e48&#8221;\u5168\u90e8\u79fb\u5165\u5893\u5730&#8221;\u8981\u5728&#8221;\u89e6\u53d1\u4ea1\u8bed&#8221;\u4e4b\u524d\uff1f<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u56e0\u4e3a\u4ea1\u8bed\u53ef\u80fd\u5f15\u7528&#8221;\u573a\u4e0a\u7684\u968f\u4ece&#8221;\u3002\u5982\u679c A \u548c B \u540c\u65f6\u6b7b\uff0cA \u7684\u4ea1\u8bed\u6267\u884c\u65f6 B \u5e94\u8be5\u5df2\u7ecf\u4e0d\u5728\u573a\u4e0a\u3002\u8fd9\u7b26\u5408\u73a9\u5bb6\u76f4\u89c9\uff1a&#8221;\u5b83\u4eec\u540c\u65f6\u6b7b\u4e86&#8221;\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4e3a\u4ec0\u4e48\u590d\u751f\uff08Reborn\uff09\u5728\u4ea1\u8bed\u4e4b\u540e\uff1f<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u56e0\u4e3a\u300a\u590d\u751f\u300b\u8981\u5728\u539f\u4f4d\u7f6e\u53ec\u5524 1 \u8840\u590d\u5236\u4f53\uff0c\u5982\u679c\u5b83\u5148\u4e8e\u4ea1\u8bed\u6267\u884c\uff0c\u4ea1\u8bed\u7684&#8221;\u5168\u573a\u4f24\u5bb3&#8221;\u4f1a\u6253\u5230\u590d\u751f\u4f53\u4e0a\u3002\u5b9e\u6d4b\u7089\u77f3\u7684\u884c\u4e3a\u662f\u590d\u751f\u4f53<strong>\u4e0d\u4f1a<\/strong>\u88ab\u540c\u6279\u6b21\u7684\u4ea1\u8bed\u4f24\u5bb3\u6253\u5230\uff0c\u8bf4\u660e\u590d\u751f\u5728\u4ea1\u8bed\u4e4b\u540e\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4e3a\u4ec0\u4e48\u6e38\u620f\u7ed3\u675f\u5224\u5b9a\u5728\u6700\u5916\u5c42\uff1f<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u56e0\u4e3a&#8221;\u53cc\u65b9\u82f1\u96c4\u540c\u65f6\u6b7b\u4ea1&#8221;\u8981\u5224\u548c\u3002\u5982\u679c\u5728\u5faa\u73af\u5185\u90e8\u5224\u5b9a\uff0c\u53ef\u80fd\u4f1a\u56e0\u4e3a\u4ea1\u8bed\u7684\u6267\u884c\u987a\u5e8f\u5bfc\u81f4\u8bef\u5224\u80dc\u8d1f\u3002\u8fd9\u4e5f\u662f\u4e3a\u4ec0\u4e48\u7089\u77f3\u91cc\u5b58\u5728&#8221;\u5e73\u5c40&#8221;\u8fd9\u4e2a\u7ed3\u679c\u2014\u20142016 \u5e74\u4e4b\u524d\u751a\u81f3\u6709 bug \u5bfc\u81f4\u67d0\u4e9b\u540c\u5f52\u4e8e\u5c3d\u88ab\u5224\u6210\u4e00\u65b9\u80dc\u5229\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">7.4 \u514d\u6b7b\u673a\u5236<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u6709\u51e0\u5f20\u5361\u80fd&#8221;\u62e6\u622a&#8221;\u6b7b\u4ea1\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u300a\u6069\u4f50\u65af\u7684\u5b9d\u7bb1\u300b\uff08\u5386\u53f2\uff09<\/li>\n\n\n\n<li>\u300a\u8fbe\u5c14\u574e\u300b\u7c7b&#8221;\u4e0d\u4f1a\u6b7b\u4ea1&#8221;<\/li>\n\n\n\n<li><strong>\u300a\u4e0d\u673d\u300b\/\u300a\u5723\u5149\u8d24\u8005\u300b<\/strong> \u7c7b\u514d\u75ab<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">\u5b9e\u73b0\u65b9\u5f0f\u662f <code>PROPOSED_DEATH<\/code> \u4e8b\u4ef6\uff0c\u5141\u8bb8\u8ba2\u9605\u8005\u7f6e <code>death_prevented<\/code>\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class PreventDeath(Action):<br>    def run(self, game, ctx):<br>        ctx.entity.death_prevented = True<br>        ctx.entity[Tag.TO_BE_DESTROYED] = 0<br>        ctx.entity[Tag.DAMAGE] = 0     # \u901a\u5e38\u8fd8\u8981\u56de\u8840<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u6ce8\u610f <code>emit_immediate<\/code> \u800c\u4e0d\u662f <code>emit<\/code> \u2014\u2014 \u514d\u6b7b\u5fc5\u987b<strong>\u7acb\u5373<\/strong>\u5224\u5b9a\uff0c\u4e0d\u80fd\u5165\u961f\uff0c\u5426\u5219\u5b9e\u4f53\u5df2\u7ecf\u8fdb\u5893\u5730\u4e86\u3002\u8fd9\u662f\u961f\u5217\u8bed\u4e49\u7684\u4e00\u4e2a\u5fc5\u8981\u4f8b\u5916\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 8 \u7ae0 \u653b\u51fb\u6d41\u7a0b\u5b8c\u6574\u62c6\u89e3<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">8.1 \u653b\u51fb\u4e0d\u662f&#8221;\u4e92\u76f8\u9020\u6210\u4f24\u5bb3&#8221;\u8fd9\u4e48\u7b80\u5355<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u4e00\u6b21\u653b\u51fb\u7684\u5b8c\u6574\u6d41\u7a0b\u6709 11 \u6b65\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def attack(self, attacker: Entity, defender: Entity):<br>    # ---- 1. \u5408\u6cd5\u6027 ----<br>    if not self.can_attack(attacker):<br>        raise IllegalMove(\"attacker cannot attack\")<br>    if not self.is_legal_attack_target(attacker, defender):<br>        raise IllegalMove(\"illegal target\")<br><br>    # ---- 2. \u5ba3\u8a00\uff1a\u5141\u8bb8\u6539\u53d8\u76ee\u6807 ----<br>    ctx = EventCtx(kind=Event.PROPOSED_ATTACK,<br>                   source=attacker, target=defender)<br>    self.emit_immediate(ctx)         # \u300a\u94dc\u987b\u300b\u7c7b\u6539\u76ee\u6807\u3001\u5965\u79d8\u300a\u8bef\u5bfc\u300b\u5728\u6b64<br>    defender = ctx.target            # \u2605 \u76ee\u6807\u53ef\u80fd\u88ab\u6539\u4e86<br>    if ctx.prevented:<br>        return<br><br>    # ---- 3. \u6f5c\u884c\u6d88\u5931 ----<br>    if attacker.has(Tag.STEALTH):<br>        attacker[Tag.STEALTH] = 0<br><br>    # ---- 4. \u653b\u51fb\u786e\u8ba4\u4e8b\u4ef6\uff08\u300a\u7830\u7830\u6cd5\u5e08\u300b\u3001\u300a\u72c2\u91ce\u708e\u672f\u5e08\u300b\uff09----<br>    self.emit(Event.ATTACK, source=attacker, target=defender)<br>    self.event_bus.resolve()<br>    # \u2605 \u7ed3\u7b97\u540e\u91cd\u65b0\u6821\u9a8c\uff1a\u300a\u7830\u7830\u6cd5\u5e08\u300b\u53ef\u80fd\u628a\u9632\u5fa1\u8005\u70b8\u6b7b\u4e86<br>    if not self.still_valid_attack(attacker, defender):<br>        self._finish_attack(attacker)<br>        return<br><br>    # ---- 5. \u5feb\u7167\u653b\u51fb\u529b\uff08\u2605 \u5173\u952e\uff09----<br>    atk_power = attacker[Tag.ATK]<br>    def_power = defender[Tag.ATK]<br><br>    # ---- 6. \u540c\u65f6\u9020\u6210\u4f24\u5bb3 ----<br>    if def_power &gt; 0:<br>        self.damage(attacker, def_power, source=defender)<br>    if atk_power &gt; 0:<br>        self.damage(defender, atk_power, source=attacker)<br><br>    # ---- 7. \u5267\u6bd2 ----<br>    if attacker.has(Tag.POISONOUS) and atk_power &gt; 0 and defender.is_minion:<br>        if not defender.divine_shield_absorbed_this_attack:<br>            defender[Tag.TO_BE_DESTROYED] = 1<br>    if defender.has(Tag.POISONOUS) and def_power &gt; 0 and attacker.is_minion:<br>        if not attacker.divine_shield_absorbed_this_attack:<br>            attacker[Tag.TO_BE_DESTROYED] = 1<br><br>    # ---- 8. \u5438\u8840 ----<br>    if attacker.has(Tag.LIFESTEAL) and atk_power &gt; 0:<br>        self.heal(self.player(attacker[Tag.CONTROLLER]).hero, atk_power)<br><br>    # ---- 9. \u6b66\u5668\u8010\u4e45 ----<br>    if attacker.is_hero:<br>        w = self.player(attacker[Tag.CONTROLLER]).weapon<br>        if w and not attacker.has(Tag.IMMUNE_WHILE_ATTACKING):<br>            w[Tag.DURABILITY] -= 1<br><br>    # ---- 10. \u8ba1\u6570\u4e0e\u540e\u7eed\u4e8b\u4ef6 ----<br>    attacker[Tag.NUM_ATTACKS] += 1<br>    self.emit(Event.AFTER_ATTACK, source=attacker, target=defender)<br><br>    # ---- 11. \u7ed3\u7b97 ----<br>    self.event_bus.resolve()<br>    self.process_deaths()<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">8.2 \u7b2c 5 \u6b65\u7684\u5feb\u7167\u4e3a\u4ec0\u4e48\u5173\u952e<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u8003\u8651\uff1a<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">\u4f60\u7684 3\/2 \u653b\u51fb\u5bf9\u65b9\u7684 2\/5\u300a\u6050\u72fc\u524d\u950b\u300b\uff0c\u5bf9\u65b9\u573a\u4e0a\u6709\u300a\u66b4\u98ce\u57ce\u52c7\u58eb\u300b\u7ed9\u5b83 +1\/+1\uff0c\u5b9e\u9645\u662f 3\/6\u3002 \u4f60\u7684\u968f\u4ece\u6709\u300a\u5267\u6bd2\u300b\u3002<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">\u5982\u679c\u4e0d\u5feb\u7167\uff0c\u4f1a\u53d1\u751f\uff1a<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>\u4f60\u7684\u968f\u4ece\u5bf9\u5b83\u9020\u6210 3 \u4f24 \u2192 \u5b83\u53d8\u6210 3\/3<\/li>\n\n\n\n<li>\u5b83\u5bf9\u4f60\u9020\u6210\u4f24\u5bb3\u65f6\u8bfb <code>ATK<\/code> \u2192 \u8fd8\u662f 3\uff08\u6ca1\u53d8\uff09<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">\u770b\u8d77\u6765\u6ca1\u95ee\u9898\u3002\u4f46\u6362\u4e2a\u573a\u666f\uff1a<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">\u4f60\u7684 4\/4 \u653b\u51fb\u5bf9\u65b9\u7684\u300a\u4e0d\u7a33\u5b9a\u7684\u98df\u5c38\u9b3c\u300b\uff0c\u4f60\u573a\u4e0a\u8fd8\u6709\u4e00\u4e2a 1\/1\u3002 \u98df\u5c38\u9b3c\u662f 1\/1\u3002<\/p>\n<\/blockquote>\n\n\n\n<ol class=\"wp-block-list\">\n<li>\u4f60\u7684 4\/4 \u6253\u6b7b\u98df\u5c38\u9b3c<\/li>\n\n\n\n<li>\u98df\u5c38\u9b3c\u5bf9\u4f60\u9020\u6210 1 \u70b9\u4f24\u5bb3<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">\u5982\u679c\u5148\u7ed3\u7b97\u6b7b\u4ea1\u518d\u8ba9\u9632\u5fa1\u8005\u53cd\u51fb\uff0c\u98df\u5c38\u9b3c\u5c31\u6253\u4e0d\u51fa\u8fd9 1 \u70b9\u4f24\u5bb3\u4e86\u3002<strong>\u7089\u77f3\u89c4\u5219\u662f&#8221;\u540c\u65f6&#8221;<\/strong>\uff0c\u6240\u4ee5\u5fc5\u987b\u5148\u5feb\u7167\u53cc\u65b9\u653b\u51fb\u529b\uff0c\u7136\u540e\u4e00\u8d77\u9020\u6210\u4f24\u5bb3\uff0c\u6700\u540e\u7edf\u4e00\u7ed3\u7b97\u6b7b\u4ea1\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u66f4\u6781\u7aef\u7684\u4f8b\u5b50\uff08\u771f\u5b9e\u6d4b\u8bd5\u8fc7\u7684\uff09\uff1a<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">\u4f60\u7684\u300a\u5267\u6bd2\u300b\u968f\u4ece 1\/1 \u653b\u51fb\u5bf9\u65b9 10\/10\u3002 \u53cc\u65b9\u540c\u5f52\u4e8e\u5c3d\u2014\u2014\u4f60\u7684 1\/1 \u88ab\u6253\u6b7b\uff0c\u5bf9\u65b9\u88ab\u5267\u6bd2\u6740\u6b7b\u3002<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">\u5982\u679c\u6309\u987a\u5e8f\u7ed3\u7b97\uff0c\u4f60\u7684 1\/1 \u5148\u6b7b\uff0c\u5267\u6bd2\u5c31\u4e0d\u751f\u6548\u4e86\u3002\u5feb\u7167\u4fdd\u8bc1\u4e86\u540c\u65f6\u6027\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">8.3 \u5723\u76fe\u4e0e\u5267\u6bd2\u7684\u4ea4\u4e92<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u7b2c 7 \u6b65\u91cc\u7684 <code>divine_shield_absorbed_this_attack<\/code> \u662f\u4e2a\u7ec6\u8282\u3002\u89c4\u5219\uff1a<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong>\u5723\u76fe\u5438\u6536\u4e86\u4f24\u5bb3 \u2192 \u5267\u6bd2\u4e0d\u751f\u6548\u3002<\/strong><\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">\u56e0\u4e3a\u5267\u6bd2\u7684\u89c4\u5219\u6587\u672c\u662f&#8221;\u4efb\u4f55<strong>\u53d7\u5230<\/strong>\u8be5\u968f\u4ece\u4f24\u5bb3\u7684\u968f\u4ece&#8221;\uff0c\u5723\u76fe\u610f\u5473\u7740\u6ca1\u6709\u53d7\u5230\u4f24\u5bb3\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def damage(self, target, amount, source=None):<br>    if amount &lt;= 0:<br>        return 0<br>    if target.has(Tag.IMMUNE):<br>        self.emit(Event.DAMAGE_PREVENTED, target=target, source=source)<br>        return 0<br><br>    ctx = EventCtx(kind=Event.PRE_DAMAGE, target=target, source=source,<br>                   amount=amount, mutable_amount=amount)<br>    self.emit_immediate(ctx)<br>    amount = max(0, ctx.mutable_amount)<br>    if amount == 0:<br>        return 0<br><br>    # \u2605 \u5723\u76fe\u5728 PRE_DAMAGE \u4e4b\u540e\u3001\u5b9e\u9645\u6263\u8840\u4e4b\u524d\u6d88\u8017<br>    if target.has(Tag.DIVINE_SHIELD):<br>        target[Tag.DIVINE_SHIELD] = 0<br>        target.divine_shield_absorbed_this_attack = True<br>        self.emit(Event.DIVINE_SHIELD_BROKEN, target=target)<br>        return 0                          # \u2190 \u8fd4\u56de 0\uff0c\u6240\u4ee5\u5267\u6bd2\/\u5438\u8840\u90fd\u4e0d\u751f\u6548<br><br>    # \u82f1\u96c4\u5148\u6263\u62a4\u7532<br>    if target.is_hero:<br>        armor = min(target[Tag.ARMOR], amount)<br>        target[Tag.ARMOR] -= armor<br>        amount -= armor<br>        if amount == 0:<br>            return 0<br><br>    target[Tag.DAMAGE] += amount<br>    fatal = target.current_health &lt;= 0<br>    self.emit(Event.DAMAGE, target=target, source=source,<br>              amount=amount, fatal=fatal)<br><br>    # \u5438\u8840\uff1a\u6ce8\u610f\u662f\"\u4f24\u5bb3\u6765\u6e90\u7684\u63a7\u5236\u8005\"\u56de\u8840\uff0c\u4e0d\u662f\u653b\u51fb\u8005<br>    if source is not None and source.has(Tag.LIFESTEAL):<br>        self.heal(self.player(source[Tag.CONTROLLER]).hero, amount)<br><br>    return amount<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>damage()<\/code> \u8fd4\u56de<strong>\u5b9e\u9645\u9020\u6210\u7684\u4f24\u5bb3<\/strong>\uff0c\u8fd9\u4e2a\u8fd4\u56de\u503c\u88ab\u5927\u91cf\u5361\u724c\u4f7f\u7528\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u300a\u8d85\u6740\u300b\uff1a<code>if actual_damage > target_health_before<\/code><\/li>\n\n\n\n<li>\u300a\u8363\u8a89\u51fb\u6740\u300b\uff1a<code>if actual_damage == target_health_before<\/code><\/li>\n\n\n\n<li>\u300a\u5438\u8840\u300b\uff1a\u56de\u8840\u91cf = \u5b9e\u9645\u4f24\u5bb3<\/li>\n\n\n\n<li>\u300a\u72c2\u4e71\u300b\uff1a<code>if actual_damage > 0 and survived<\/code><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4e00\u4e2a\u8fd4\u56de\u503c\u652f\u6491\u4e86\u56db\u4e2a\u5173\u952e\u8bcd<\/strong>\u2014\u2014\u8fd9\u5c31\u662f\u597d\u62bd\u8c61\u7684\u4ef7\u503c\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">8.4 \u5632\u8bbd\u7684\u76ee\u6807\u5408\u6cd5\u6027<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">def is_legal_attack_target(self, attacker, defender) -&gt; bool:<br>    if defender[Tag.CONTROLLER] == attacker[Tag.CONTROLLER]:<br>        return False                      # \u4e0d\u80fd\u6253\u81ea\u5df1\u4eba\uff08\u9664\u4e2a\u522b\u5361\uff09<br>    if defender.has(Tag.STEALTH):<br>        return False                      # \u6f5c\u884c\u4e0d\u53ef\u88ab\u653b\u51fb<br>    if defender.has(Tag.IMMUNE):<br>        return False<br>    if attacker.has(Tag.RUSH) and attacker[Tag.NUM_TURNS_IN_PLAY] == 0:<br>        if defender.is_hero:<br>            return False                  # \u7a81\u88ad\u9996\u56de\u5408\u4e0d\u80fd\u6253\u8138<br><br>    # \u5632\u8bbd\u68c0\u67e5\uff1a\u6ce8\u610f\u6f5c\u884c\u7684\u5632\u8bbd\u968f\u4ece\u4e0d\u63d0\u4f9b\u5632\u8bbd\u4fdd\u62a4\uff01<br>    enemy_side = self.zones.get(1 - attacker[Tag.CONTROLLER], Zone.PLAY)<br>    taunts = [m for m in enemy_side<br>              if m.has(Tag.TAUNT) and not m.has(Tag.STEALTH) and not m.has(Tag.IMMUNE)]<br>    if taunts and defender not in taunts:<br>        return False<br>    return True<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6f5c\u884c\u7684\u5632\u8bbd\u968f\u4ece\u4e0d\u63d0\u4f9b\u5632\u8bbd\u4fdd\u62a4<\/strong>\u2014\u2014\u8fd9\u662f\u4e2a\u7ecf\u5178\u7ec6\u8282\u3002\u56e0\u4e3a\u6f5c\u884c\u7684\u968f\u4ece&#8221;\u4e0d\u80fd\u88ab\u653b\u51fb&#8221;\uff0c\u5982\u679c\u5b83\u540c\u65f6\u63d0\u4f9b\u5632\u8bbd\u4fdd\u62a4\uff0c\u5c31\u4f1a\u5f62\u6210&#8221;\u4ec0\u4e48\u90fd\u4e0d\u80fd\u6253&#8221;\u7684\u6b7b\u9501\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 9 \u7ae0 \u76ee\u6807\u9009\u62e9\uff1a\u5408\u6cd5\u6027\u7684\u5c42\u5c42\u8fc7\u6ee4<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">9.1 \u4e09\u79cd&#8221;\u4e0d\u53ef\u88ab\u9009\u4e2d&#8221;<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u7089\u77f3\u6709\u4e09\u4e2a\u5bb9\u6613\u6df7\u6dc6\u7684\u6982\u5ff5\uff1a<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u5173\u952e\u8bcd<\/th><th>\u6548\u679c<\/th><th>\u4f8b\u5b50<\/th><\/tr><\/thead><tbody><tr><td><strong>\u6f5c\u884c\uff08Stealth\uff09<\/strong><\/td><td>\u4e0d\u80fd\u88ab\u654c\u65b9<strong>\u653b\u51fb<\/strong>\uff0c\u4e5f\u4e0d\u80fd\u88ab\u654c\u65b9<strong>\u6cd5\u672f\/\u6280\u80fd\u6307\u5411<\/strong><\/td><td>\u300a\u8c7a\u72fc\u4eba\u65a5\u5019\u300b<\/td><\/tr><tr><td><strong>\u4e0d\u53ef\u88ab\u9009\u4e2d\uff08Elusive\uff09<\/strong><\/td><td>\u4e0d\u80fd\u88ab<strong>\u4efb\u4f55<\/strong>\u6cd5\u672f\/\u82f1\u96c4\u6280\u80fd\u6307\u5411\uff08\u5305\u62ec\u5df1\u65b9\uff01\uff09\uff0c\u4f46<strong>\u53ef\u4ee5\u88ab\u653b\u51fb<\/strong><\/td><td>\u300a\u6cd5\u529b\u6d6e\u9f99\u300b<\/td><\/tr><tr><td><strong>\u514d\u75ab\uff08Immune\uff09<\/strong><\/td><td>\u4e0d\u53d7\u4efb\u4f55\u4f24\u5bb3\uff0c\u4e5f\u4e0d\u80fd\u88ab\u653b\u51fb\/\u6307\u5411<\/td><td>\u300a\u51b0\u7bb1\u6cd5\u300b<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<pre class=\"wp-block-preformatted\">def is_legal_target(self, card_or_source, target) -&gt; bool:<br>    if target is None:<br>        return card_or_source.defn.targeting is None<br>    sel = card_or_source.defn.targeting<br>    if sel is None:<br>        return False<br>    # 1. \u57fa\u7840\u7b5b\u9009\uff08Selector \u5b9a\u4e49\u7684\uff0c\u5982\"\u4e00\u4e2a\u53cb\u65b9\u91ce\u517d\"\uff09<br>    if target not in sel.eval(self.game, Ctx(source=card_or_source)):<br>        return False<br>    # 2. \u901a\u7528\u4e0d\u53ef\u9009\u4e2d\u89c4\u5219<br>    if target.has(Tag.ELUSIVE):<br>        return False                     # \u654c\u6211\u90fd\u4e0d\u884c<br>    if target.has(Tag.IMMUNE):<br>        return False<br>    if target.has(Tag.UNTOUCHABLE):<br>        return False<br>    if target.has(Tag.STEALTH) and \\<br>       target[Tag.CONTROLLER] != card_or_source[Tag.CONTROLLER]:<br>        return False                     # \u53ea\u5bf9\u654c\u65b9\u6f5c\u884c\u6709\u6548<br>    if target.has(Tag.DORMANT):<br>        return False<br>    return True<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">9.2 &#8220;\u968f\u673a\u76ee\u6807&#8221;\u4e0e&#8221;\u9009\u62e9\u76ee\u6807&#8221;\u7684\u533a\u522b<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u4e00\u6761\u91cd\u8981\u89c4\u5219\uff1a<strong>\u4e0d\u53ef\u88ab\u9009\u4e2d\uff08Elusive\uff09\u53ea\u6321&#8221;\u9009\u62e9&#8221;\uff0c\u4e0d\u6321&#8221;\u968f\u673a&#8221;<\/strong>\u3002<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u300a\u6cd5\u529b\u6d6e\u9f99\u300b\u4e0d\u80fd\u88ab\u300a\u706b\u7403\u672f\u300b\u6307\u5411<\/li>\n\n\n\n<li>\u4f46\u300a\u66b4\u98ce\u96ea\u300b\uff08AOE\uff0c\u65e0\u76ee\u6807\uff09\u7167\u6253<\/li>\n\n\n\n<li>\u300a\u98de\u5200\u6742\u800d\u8005\u300b\u7684\u968f\u673a\u4f24\u5bb3\u7167\u6253<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-preformatted\">class TargetMode(IntEnum):<br>    CHOSEN = 0     # \u73a9\u5bb6\u6307\u5411 \u2192 \u53d7 Elusive\/Stealth \u9650\u5236<br>    RANDOM = 1     # \u5f15\u64ce\u968f\u673a \u2192 \u4e0d\u53d7\u9650\u5236<br>    ALL    = 2     # \u5168\u4f53     \u2192 \u4e0d\u53d7\u9650\u5236<br>    ADJACENT = 3   # \u76f8\u90bb     \u2192 \u4e0d\u53d7\u9650\u5236<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8fd9\u4e2a\u533a\u5206\u5fc5\u987b\u5728 Selector \u5c42\u9762\u8868\u8fbe<\/strong>\uff0c\u800c\u4e0d\u662f\u5728\u6bcf\u5f20\u5361\u91cc\u5224\u65ad\u3002\u6211\u4eec\u5728\u7b2c\u4e09\u90e8\u5206\u4f1a\u770b\u5230 Selector DSL \u5982\u4f55\u7f16\u7801\u5b83\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">9.3 \u6218\u543c\u65e0\u5408\u6cd5\u76ee\u6807\u65f6\u4f1a\u600e\u6837<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u89c4\u5219\uff1a<strong>\u5982\u679c\u6218\u543c\u9700\u8981\u76ee\u6807\u4f46\u573a\u4e0a\u6ca1\u6709\u5408\u6cd5\u76ee\u6807\uff0c\u6218\u543c\u4e0d\u4f1a\u89e6\u53d1\uff08\u4f46\u968f\u4ece\u7167\u5e38\u4e0a\u573a\uff09<\/strong>\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">if script and card.defn.targeting is not None:<br>    legal = [t for t in card.defn.targeting.eval(game, ctx)<br>             if game.is_legal_target(card, t)]<br>    if not legal:<br>        pass          # \u6218\u543c\u8df3\u8fc7\uff0c\u968f\u4ece\u6b63\u5e38\u4e0a\u573a<br>    else:<br>        assert target in legal<br>        game.run(script, source=card, target=target)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u8fd9\u6761\u89c4\u5219\u6709\u4e00\u4e2a\u7b56\u7565\u6027\u540e\u679c\uff1a<strong>\u300a\u6697\u5f71\u6b65\u300b\u7c7b\u5361\u53ef\u4ee5&#8221;\u91cd\u7f6e&#8221;\u4e00\u4e2a\u5df2\u7ecf\u7528\u8fc7\u6218\u543c\u7684\u968f\u4ece<\/strong>\uff0c\u800c\u5982\u679c\u573a\u4e0a\u6ca1\u6709\u76ee\u6807\uff0c\u4f60\u53ef\u4ee5\u628a\u300a\u706b\u8f66\u738b\u300b\u8fd9\u7c7b&#8221;\u6218\u543c\u6709\u4ee3\u4ef7&#8221;\u7684\u968f\u4ece\u5f53\u6210\u65e0\u4ee3\u4ef7\u7684\u8eab\u6750\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">9.4 \u5ba2\u6237\u7aef\u4e0e\u670d\u52a1\u5668\u7684\u76ee\u6807\u6821\u9a8c<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u63a8\u65ad<\/strong>\uff1a\u5ba2\u6237\u7aef\u4f1a\u505a\u4e00\u6b21\u76ee\u6807\u9ad8\u4eae\uff08UX\uff09\uff0c\u670d\u52a1\u5668<strong>\u5fc5\u987b<\/strong>\u72ec\u7acb\u6821\u9a8c\u4e00\u6b21\u3002\u56e0\u4e3a\u5ba2\u6237\u7aef\u4e0d\u53ef\u4fe1\uff0c\u4e14\u5ba2\u6237\u7aef\u7684\u72b6\u6001\u53ef\u80fd\u843d\u540e\u4e8e\u670d\u52a1\u5668\uff08\u6bd4\u5982\u4f60\u6b63\u5728\u62d6\u62fd\u65f6\u5bf9\u624b\u7684\u5965\u79d8\u89e6\u53d1\u4e86\uff09\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u771f\u5b9e\u7089\u77f3\u5728\u8fd9\u91cc\u6709\u4e00\u4e2a\u53ef\u89c2\u5bdf\u7684\u884c\u4e3a\uff1a\u5076\u5c14\u4f60\u4f1a\u770b\u5230&#8221;\u76ee\u6807\u88ab\u62d2\u7edd\uff0c\u5361\u724c\u5f39\u56de\u624b\u91cc&#8221;\u7684\u52a8\u753b\u2014\u2014\u8fd9\u5c31\u662f\u670d\u52a1\u5668\u6821\u9a8c\u5931\u8d25\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">\u7b2c\u4e09\u90e8\u5206 \u6570\u636e\u9a71\u52a8\uff1a\u4e00\u4e07\u5f20\u5361\u5982\u4f55\u4e0d\u5199\u4e00\u4e07\u4e2a if<\/h1>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 10 \u7ae0 \u4ece\u786c\u7f16\u7801\u5230 DSL\uff1a\u4e09\u6b21\u6f14\u5316<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">10.1 \u7b2c\u96f6\u9636\u6bb5\uff1a\u786c\u7f16\u7801\uff08\u4e0d\u53ef\u884c\uff09<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">def play_card(card, target):<br>    if card.name == \"\u706b\u7403\u672f\":<br>        deal_damage(target, 6)<br>    elif card.name == \"\u5965\u672f\u5c04\u51fb\":<br>        deal_damage(target, 2)<br>    elif card.name == \"\u51b0\u67aa\u672f\":<br>        deal_damage(target, 4)<br>        freeze(target)<br>    elif ...  # \u00d76000<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u95ee\u9898\u4e0d\u53ea\u662f\u957f\u3002\u771f\u6b63\u7684\u95ee\u9898\u662f\uff1a<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>\u65b0\u5361\u5fc5\u987b\u6539\u5f15\u64ce\u4ee3\u7801 \u2192 \u5fc5\u987b\u53d1\u5ba2\u6237\u7aef\u7248\u672c \u2192 \u65e0\u6cd5\u70ed\u66f4\u65b0<\/strong><\/li>\n\n\n\n<li><strong>\u65e0\u6cd5\u505a\u5e73\u8861\u6027\u8c03\u6574<\/strong>\uff08\u6539\u6570\u503c\u8981\u6539\u4ee3\u7801\uff09<\/li>\n\n\n\n<li><strong>AI \u65e0\u6cd5\u7406\u89e3\u5361\u724c<\/strong>\uff08\u53ea\u80fd\u9760\u6a21\u62df\uff0c\u4e0d\u80fd\u505a\u542f\u53d1\u5f0f\u4f30\u503c\uff09<\/li>\n\n\n\n<li><strong>\u65e0\u6cd5\u81ea\u52a8\u751f\u6210\u5361\u724c\u6587\u672c<\/strong>\uff08\u672c\u5730\u5316\u5669\u68a6\uff09<\/li>\n\n\n\n<li><strong>\u65e0\u6cd5\u9759\u6001\u5206\u6790<\/strong>\uff08&#8221;\u54ea\u4e9b\u5361\u4f1a\u9020\u6210\u4f24\u5bb3\uff1f&#8221;\u8fd9\u4e2a\u67e5\u8be2\u8981\u9760 grep\uff09<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">10.2 \u7b2c\u4e00\u9636\u6bb5\uff1a\u56de\u8c03\u51fd\u6570\uff08\u534a\u53ef\u884c\uff09<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">CARDS = {<br>    \"CS2_029\": CardDef(name=\"\u706b\u7403\u672f\", cost=4,<br>                       effect=lambda g, src, tgt: g.damage(tgt, 6, src)),<br>    \"EX1_277\": CardDef(name=\"\u5965\u672f\u51b2\u51fb\", cost=1,<br>                       effect=lambda g, src, tgt: g.damage(tgt, 1, src)),<br>}<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u597d\u4e00\u4e9b\u4e86\u3002\u5361\u724c\u53d8\u6210\u4e86\u6570\u636e\u7ed3\u6784\u7684\u5b57\u6bb5\u3002\u4f46\u4ecd\u6709\u81f4\u547d\u95ee\u9898\uff1a<strong>lambda \u662f\u4e0d\u900f\u660e\u7684<\/strong>\u3002<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u4f60\u4e0d\u80fd\u95ee&#8221;\u8fd9\u5f20\u5361\u9020\u6210\u591a\u5c11\u4f24\u5bb3&#8221;\uff0c\u53ea\u80fd\u6267\u884c\u5b83<\/li>\n\n\n\n<li>AI \u53ea\u80fd\u9760\u6a21\u62df\uff0c\u65e0\u6cd5\u9884\u4f30<\/li>\n\n\n\n<li>\u4e0d\u80fd\u5e8f\u5217\u5316\uff08\u65e0\u6cd5\u70ed\u66f4\u65b0\uff09<\/li>\n\n\n\n<li>\u4e0d\u80fd\u81ea\u52a8\u751f\u6210\u6587\u672c<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">\u771f\u5b9e\u7089\u77f3<strong>\u5e76\u4e0d\u662f<\/strong>\u8fd9\u6837\u505a\u7684\u3002\u793e\u533a\u4ece <code>CardDefs.xml<\/code> \u91cc\u770b\u5230\u7684\u662f<strong>\u7ed3\u6784\u5316\u7684\u6548\u679c\u63cf\u8ff0<\/strong>\uff0c\u8bf4\u660e\u66b4\u96ea\u8d70\u7684\u662f\u7b2c\u4e8c\u9636\u6bb5\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">10.3 \u7b2c\u4e8c\u9636\u6bb5\uff1aAction \u6811\uff08\u6b63\u786e\u7b54\u6848\uff09<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u628a\u6548\u679c\u8868\u8fbe\u6210<strong>\u53ef\u68c0\u67e5\u7684\u6570\u636e\u7ed3\u6784<\/strong>\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">FIREBALL = CardDef(<br>    card_id=\"CS2_029\", name=\"\u706b\u7403\u672f\", cost=4, card_type=\"SPELL\",<br>    spell_school=\"FIRE\",<br>    targeting=Sel.any_character(),<br>    spell=Damage(Ctx.TARGET, 6),<br>)<br><br>FROSTBOLT = CardDef(<br>    card_id=\"CS2_024\", name=\"\u5bd2\u51b0\u7bad\", cost=2, card_type=\"SPELL\",<br>    spell_school=\"FROST\",<br>    targeting=Sel.any_character(),<br>    spell=Seq(<br>        Damage(Ctx.TARGET, 3),<br>        Freeze(Ctx.TARGET),<br>    ),<br>)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>Damage(Ctx.TARGET, 6)<\/code> \u662f\u4e00\u4e2a<strong>\u5bf9\u8c61<\/strong>\uff0c\u4e0d\u662f\u51fd\u6570\u3002\u4f60\u53ef\u4ee5\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">&gt;&gt;&gt; FIREBALL.spell<br>Damage(target=Ctx.TARGET, amount=6)<br>&gt;&gt;&gt; FIREBALL.spell.estimated_damage()<br>6<br>&gt;&gt;&gt; FIREBALL.spell.to_json()<br>{\"op\": \"Damage\", \"target\": \"TARGET\", \"amount\": 6}<br>&gt;&gt;&gt; render_text(FIREBALL.spell, locale=\"zh-CN\")<br>\"\u9020\u6210 $6 \u70b9\u4f24\u5bb3\u3002\"<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u8fd9\u56db\u4e2a\u80fd\u529b\u2014\u2014<strong>\u4f30\u503c\u3001\u5e8f\u5217\u5316\u3001\u6587\u672c\u751f\u6210\u3001\u9759\u6001\u5206\u6790<\/strong>\u2014\u2014\u6b63\u662f\u786c\u7f16\u7801\u7ed9\u4e0d\u4e86\u7684\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">10.4 \u7089\u77f3\u5b9e\u9645\u7528\u7684\u662f\u4ec0\u4e48<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u63a8\u65ad<\/strong>\uff08\u57fa\u4e8e\u793e\u533a\u5bf9 <code>CardDefs.xml<\/code> \u4e0e\u6e38\u620f\u5185 <code>Power<\/code> \u5b9a\u4e49\u7684\u89e3\u5305\u5206\u6790\uff09\uff1a<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u7089\u77f3\u7684\u5361\u724c\u5b9a\u4e49\u5305\u542b\u4e00\u4e2a <code>&lt;Power&gt;<\/code> \u8282\u70b9\uff0c\u5f15\u7528\u4e00\u4e2a <code>PowerDefinition<\/code>\uff0c\u91cc\u9762\u662f\u4e00\u4e32&#8221;\u6548\u679c&#8221;\uff0c\u6bcf\u4e2a\u6548\u679c\u6709\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>EffectType<\/code>\uff08Damage \/ Heal \/ Summon \/ Draw \/ Buff \/ &#8230;\uff09<\/li>\n\n\n\n<li><code>TargetSelector<\/code>\uff08\u4e00\u4e2a\u7b5b\u9009\u5668 ID\uff09<\/li>\n\n\n\n<li><code>Amount<\/code>\uff08\u53ef\u4ee5\u662f\u5e38\u6570\uff0c\u4e5f\u53ef\u4ee5\u662f&#8221;\u8fd0\u884c\u65f6\u8ba1\u7b97&#8221;\u7684\u5f15\u7528\uff09<\/li>\n\n\n\n<li><code>Conditions<\/code><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">\u4e5f\u5c31\u662f\u8bf4\uff0c<strong>\u7089\u77f3\u7684\u5361\u724c\u6548\u679c\u786e\u5b9e\u662f\u6570\u636e\u9a71\u52a8\u7684 Action \u6811<\/strong>\uff0c\u53ea\u662f\u7528\u7684\u662f C# \u7684\u7c7b\u5c42\u6b21\u800c\u975e Python\u3002\u5c11\u6570\u6781\u5176\u7279\u6b8a\u7684\u5361\uff08\u5982\u300a\u5c24\u683c-\u8428\u9686\u300b\u3001\u300a\u9b54\u6cd5\u5951\u7ea6\u300b\uff09\u4f1a fallback \u5230\u786c\u7f16\u7801\u7684 <code>SpecialEffect<\/code>\uff0c\u8fd9\u5728\u4efb\u4f55\u6570\u636e\u9a71\u52a8\u7cfb\u7edf\u91cc\u90fd\u662f\u5fc5\u8981\u7684\u9003\u751f\u8231\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8bbe\u8ba1\u539f\u5219\uff1aDSL \u8986\u76d6 95% \u7684\u5361\uff0c\u5269\u4e0b 5% \u5141\u8bb8\u5199\u4ee3\u7801\u3002<\/strong> \u8ffd\u6c42 100% \u8986\u76d6\u4f1a\u8ba9 DSL \u590d\u6742\u5230\u4e0d\u5982\u76f4\u63a5\u5199\u4ee3\u7801\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 11 \u7ae0 Selector\uff1a\u7b5b\u9009\u5668\u8bed\u8a00<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">11.1 \u4e3a\u4ec0\u4e48\u7b5b\u9009\u5668\u8981\u72ec\u7acb\u6210\u4e00\u5c42<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u770b\u8fd9\u51e0\u5f20\u5361\u7684\u76ee\u6807\u63cf\u8ff0\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>&#8220;\u4e00\u4e2a\u968f\u4ece&#8221;<\/li>\n\n\n\n<li>&#8220;\u4e00\u4e2a\u53cb\u65b9\u91ce\u517d&#8221;<\/li>\n\n\n\n<li>&#8220;\u6240\u6709\u654c\u65b9\u968f\u4ece&#8221;<\/li>\n\n\n\n<li>&#8220;\u6240\u6709\u5176\u4ed6\u53cb\u65b9\u968f\u4ece&#8221;<\/li>\n\n\n\n<li>&#8220;\u968f\u673a\u4e00\u4e2a\u654c\u65b9\u968f\u4ece&#8221;<\/li>\n\n\n\n<li>&#8220;\u653b\u51fb\u529b\u6700\u9ad8\u7684\u654c\u65b9\u968f\u4ece&#8221;<\/li>\n\n\n\n<li>&#8220;\u4f60\u7684\u724c\u5e93\u4e2d\u8d39\u7528\u6700\u4f4e\u7684\u6cd5\u672f&#8221;<\/li>\n\n\n\n<li>&#8220;\u76f8\u90bb\u7684\u968f\u4ece&#8221;<\/li>\n\n\n\n<li>&#8220;\u624b\u724c\u6700\u53f3\u4fa7\u7684\u5361&#8221;<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">\u5b83\u4eec\u51fa\u73b0\u5728<strong>\u591a\u4e2a\u4f4d\u7f6e<\/strong>\uff1a\u76ee\u6807\u7b5b\u9009\u3001AOE \u8303\u56f4\u3001\u5149\u73af\u8303\u56f4\u3001\u62bd\u724c\u6761\u4ef6\u3001\u4ea1\u8bed\u76ee\u6807\u2026\u2026\u5982\u679c\u6bcf\u5904\u90fd\u5199\u4e00\u904d\uff0c\u5c31\u4f1a\u6709\u5927\u91cf\u91cd\u590d\u3002\u6240\u4ee5 Selector \u5fc5\u987b\u662f\u4e00\u7b49\u516c\u6c11\uff0c\u53ef\u7ec4\u5408\u3001\u53ef\u590d\u7528\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">11.2 Selector \u7684\u4ee3\u6570\u7ed3\u6784<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Selector \u662f\u4e00\u4e2a\u4ece&#8221;\u4e0a\u4e0b\u6587&#8221;\u5230&#8221;\u5b9e\u4f53\u5217\u8868&#8221;\u7684\u51fd\u6570\uff0c\u4f46\u6211\u4eec\u628a\u5b83\u8868\u8fbe\u6210<strong>\u53ef\u7ec4\u5408\u7684\u5bf9\u8c61<\/strong>\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"># selectors.py<br>class Selector:<br>    def eval(self, game, ctx) -&gt; list[Entity]:<br>        raise NotImplementedError<br><br>    # ---- \u7ec4\u5408\u5b50 ----<br>    def __add__(self, other):  return Union(self, other)      # \u5e76\u96c6<br>    def __sub__(self, other):  return Difference(self, other) # \u5dee\u96c6<br>    def __and__(self, other):  return Intersect(self, other)<br>    def where(self, cond):     return Filtered(self, cond)<br>    def random(self, n=1):     return RandomPick(self, n)<br>    def top(self, n, key):     return TopN(self, n, key)<br>    def sorted_by(self, key):  return SortedBy(self, key)<br>    def limit(self, n):        return Limit(self, n)<br><br><br># ---- \u539f\u5b50 Selector ----<br>class AllMinions(Selector):<br>    def eval(self, game, ctx):<br>        return [e for pid in (0, 1) for e in game.zones.get(pid, Zone.PLAY)<br>                if e[Tag.CARDTYPE] == CardType.MINION]<br><br>class Friendly(Selector):<br>    \"\"\"\u628a\u4efb\u610f selector \u9650\u5236\u5728\u53cb\u65b9\"\"\"<br>    def __init__(self, inner): self.inner = inner<br>    def eval(self, game, ctx):<br>        me = ctx.controller<br>        return [e for e in self.inner.eval(game, ctx) if e[Tag.CONTROLLER] == me]<br><br>class Enemy(Selector):<br>    def __init__(self, inner): self.inner = inner<br>    def eval(self, game, ctx):<br>        me = ctx.controller<br>        return [e for e in self.inner.eval(game, ctx) if e[Tag.CONTROLLER] != me]<br><br>class Self_(Selector):<br>    def eval(self, game, ctx): return [ctx.source]<br><br>class TargetSel(Selector):<br>    def eval(self, game, ctx): return [ctx.target] if ctx.target else []<br><br>class Adjacent(Selector):<br>    def __init__(self, of=None): self.of = of or Self_()<br>    def eval(self, game, ctx):<br>        out = []<br>        for e in self.of.eval(game, ctx):<br>            out.extend(game.neighbors(e))<br>        return out<br><br>class InZone(Selector):<br>    def __init__(self, zone, of_player=\"SELF\"):<br>        self.zone, self.of_player = zone, of_player<br>    def eval(self, game, ctx):<br>        pid = ctx.controller if self.of_player == \"SELF\" else 1 - ctx.controller<br>        return list(game.zones.get(pid, self.zone))<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">11.3 Condition\uff1a\u8c13\u8bcd\u8bed\u8a00<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">class Condition:<br>    def eval(self, game, e, ctx) -&gt; bool: raise NotImplementedError<br>    def __and__(self, o): return AndCond(self, o)<br>    def __or__(self, o):  return OrCond(self, o)<br>    def __invert__(self): return NotCond(self)<br><br>class HasTag(Condition):<br>    def __init__(self, tag, value=None): self.tag, self.value = tag, value<br>    def eval(self, game, e, ctx):<br>        return bool(e[self.tag]) if self.value is None else e[self.tag] == self.value<br><br>class IsRace(Condition):<br>    def __init__(self, race): self.race = race<br>    def eval(self, game, e, ctx):<br>        # \u2605 \"\u5168\u90e8\"\u7c7b\u578b\uff08\u5982\u300a\u53d8\u5f62\u517d\u300b\uff09\u5339\u914d\u4efb\u4f55 race \u67e5\u8be2<br>        return e[Tag.CARDRACE] in (self.race, \"ALL\")<br><br>class CostAtMost(Condition):<br>    def __init__(self, n): self.n = n<br>    def eval(self, game, e, ctx): return game.compute_cost(e) &lt;= self.n<br><br>class AttackAtLeast(Condition):<br>    def __init__(self, n): self.n = n<br>    def eval(self, game, e, ctx): return e[Tag.ATK] &gt;= self.n<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">11.4 \u7ec4\u5408\u51fa\u771f\u5b9e\u5361\u724c\u7684\u7b5b\u9009\u5668<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u6709\u4e86\u8fd9\u4e9b\u79ef\u6728\uff0c\u5b9e\u9645\u5361\u724c\u7684\u7b5b\u9009\u5c31\u662f\u4e00\u884c\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class Sel:<br>    \"\"\"\u5e38\u7528\u7b5b\u9009\u5668\u7684\u547d\u540d\u7a7a\u95f4\uff0c\u7eaf\u7cb9\u662f\u53ef\u8bfb\u6027\u7cd6\u3002\"\"\"<br>    ALL_MINIONS        = AllMinions()<br>    FRIENDLY_MINIONS   = Friendly(AllMinions())<br>    ENEMY_MINIONS      = Enemy(AllMinions())<br>    OTHER_FRIENDLY     = Friendly(AllMinions()) - Self_()<br>    ALL_CHARACTERS     = AllMinions() + AllHeroes()<br>    TARGET             = TargetSel()<br>    SELF               = Self_()<br><br>    @staticmethod<br>    def friendly_beasts():<br>        return Friendly(AllMinions()).where(IsRace(\"BEAST\"))<br><br>    @staticmethod<br>    def random_enemy_minion(n=1):<br>        return Enemy(AllMinions()).random(n)<br><br>    @staticmethod<br>    def highest_attack_enemy():<br>        return Enemy(AllMinions()).top(1, key=lambda e: e[Tag.ATK])<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u771f\u5b9e\u5361\u724c\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"># \u300a\u91ce\u6027\u5370\u8bb0\u300b\uff1a\u4f7f\u4e00\u4e2a\u968f\u4ece\u83b7\u5f97 +2\/+2 \u548c\u5632\u8bbd<br>MARK_OF_NATURE = CardDef(<br>    card_id=\"EX1_155\", name=\"\u91ce\u6027\u5370\u8bb0\", cost=3, card_type=\"SPELL\",<br>    choose_one=(\"EX1_155a\", \"EX1_155b\"),<br>    targeting=Sel.ALL_MINIONS,<br>)<br><br># \u300a\u66b4\u98ce\u96ea\u300b\uff1a\u5bf9\u6240\u6709\u654c\u65b9\u968f\u4ece\u9020\u6210 2 \u70b9\u4f24\u5bb3\u5e76\u51bb\u7ed3<br>BLIZZARD = CardDef(<br>    card_id=\"CS2_028\", name=\"\u66b4\u98ce\u96ea\", cost=6, card_type=\"SPELL\",<br>    spell_school=\"FROST\",<br>    spell=Seq(<br>        Damage(Sel.ENEMY_MINIONS, 2),<br>        Freeze(Sel.ENEMY_MINIONS),<br>    ),<br>)<br><br># \u300a\u6697\u5f71\u72c2\u4e71\u300b\uff1a\u83b7\u5f97\u4e00\u4e2a\u653b\u51fb\u529b\u5c0f\u4e8e\u7b49\u4e8e 3 \u7684\u654c\u65b9\u968f\u4ece\u7684\u63a7\u5236\u6743\uff0c\u76f4\u5230\u56de\u5408\u7ed3\u675f<br>SHADOW_MADNESS = CardDef(<br>    card_id=\"EX1_334\", name=\"\u6697\u5f71\u72c2\u4e71\", cost=4, card_type=\"SPELL\",<br>    spell_school=\"SHADOW\",<br>    targeting=Sel.ENEMY_MINIONS.where(AttackAtMost(3)),<br>    spell=Seq(<br>        TakeControl(Ctx.TARGET, until=Until.END_OF_TURN),<br>        SetTag(Ctx.TARGET, Tag.EXHAUSTED, 0),   # \u53ef\u4ee5\u7acb\u523b\u653b\u51fb<br>    ),<br>)<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">11.5 \u6392\u5e8f\u4e0e\u968f\u673a\u7684\u786e\u5b9a\u6027\u95ee\u9898<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>RandomPick<\/code> \u548c <code>TopN<\/code> \u6709\u4e00\u4e2a\u9690\u853d\u7684\u9677\u9631\uff1a<strong>\u5e73\u5c40\u600e\u4e48\u529e\uff1f<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&#8220;\u653b\u51fb\u529b\u6700\u9ad8\u7684\u654c\u65b9\u968f\u4ece&#8221;\u2014\u2014\u5982\u679c\u6709\u4e24\u4e2a 5 \u653b\u968f\u4ece\u600e\u4e48\u529e\uff1f\u7089\u77f3\u7684\u89c4\u5219\u662f<strong>\u968f\u673a\u9009\u4e00\u4e2a<\/strong>\u3002\u6240\u4ee5\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class TopN(Selector):<br>    def __init__(self, inner, n, key):<br>        self.inner, self.n, self.key = inner, n, key<br>    def eval(self, game, ctx):<br>        pool = self.inner.eval(game, ctx)<br>        if not pool: return []<br>        best = max(self.key(e) for e in pool)<br>        tied = [e for e in pool if self.key(e) == best]<br>        if len(tied) &lt;= self.n:<br>            return tied<br>        return game.rng.sample(tied, self.n)   # \u2605 \u5fc5\u987b\u8d70\u6e38\u620f RNG\uff0c\u4e0d\u80fd\u7528\u5168\u5c40 random<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>game.rng<\/code> \u800c\u4e0d\u662f <code>random<\/code><\/strong> \u662f\u7edd\u5bf9\u4e0d\u80fd\u59a5\u534f\u7684\u3002\u6240\u6709\u968f\u673a\u5fc5\u987b\u6765\u81ea\u4e00\u4e2a seeded \u7684\u3001\u53ea\u88ab\u6e38\u620f\u903b\u8f91\u6d88\u8d39\u7684 RNG\uff0c\u5426\u5219\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u56de\u653e\u4f1a\u4e0d\u4e00\u81f4<\/li>\n\n\n\n<li>AI \u6a21\u62df\u4f1a\u6c61\u67d3\u771f\u5b9e\u5bf9\u5c40\u7684\u968f\u673a\u5e8f\u5217<\/li>\n\n\n\n<li>\u65e0\u6cd5\u590d\u73b0 bug<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">\u7b2c\u516d\u90e8\u5206\u4f1a\u8be6\u7ec6\u5c55\u5f00\u786e\u5b9a\u6027\u968f\u673a\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">11.6 \u7b5b\u9009\u5668\u7684\u6c42\u503c\u65f6\u673a\uff1a\u65e9\u7ed1\u5b9a vs \u665a\u7ed1\u5b9a<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u4e00\u4e2a\u5fae\u5999\u7684\u95ee\u9898\uff1a<code>Damage(Sel.ENEMY_MINIONS, 2)<\/code> \u5728\u4ec0\u4e48\u65f6\u5019\u6c42\u503c <code>ENEMY_MINIONS<\/code>\uff1f<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u7b54\u6848\uff1a\u5728 Action \u5f00\u59cb\u6267\u884c\u7684\u77ac\u95f4\uff0c\u4e00\u6b21\u6027\u6c42\u503c\uff0c\u7136\u540e\u904d\u5386\u8fd9\u4e2a\u5feb\u7167\u3002<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u4e3a\u4ec0\u4e48\u4e0d\u662f\u8fb9\u904d\u5386\u8fb9\u6c42\u503c\uff1f\u8003\u8651\uff1a<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">\u300a\u66b4\u98ce\u96ea\u300b\u5bf9\u6240\u6709\u654c\u65b9\u968f\u4ece\u9020\u6210 2 \u70b9\u4f24\u5bb3\u3002 \u654c\u65b9\u6709\u4e00\u4e2a\u300a\u4e0d\u7a33\u5b9a\u7684\u98df\u5c38\u9b3c\u300b\u548c\u4e00\u4e2a 3\/3\u3002<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">\u5982\u679c\u665a\u7ed1\u5b9a\uff08\u6bcf\u6b21\u8fed\u4ee3\u91cd\u65b0\u6c42\u503c\uff09\uff0c\u98df\u5c38\u9b3c\u6b7b\u4ea1\u540e\u5217\u8868\u4f1a\u53d8\uff0c\u53ef\u80fd\u6f0f\u6253\u6216\u91cd\u590d\u6253\u3002\u65e9\u7ed1\u5b9a\u4fdd\u8bc1\u4e86&#8221;\u76ee\u6807\u5728\u6548\u679c\u5f00\u59cb\u65f6\u786e\u5b9a&#8221;\uff0c\u7b26\u5408\u73a9\u5bb6\u76f4\u89c9\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class Damage(Action):<br>    def __init__(self, sel, amount):<br>        self.sel, self.amount = sel, amount<br>    def run(self, game, ctx):<br>        targets = list(self.sel.eval(game, ctx))     # \u2605 \u5feb\u7167<br>        amt = resolve_amount(self.amount, game, ctx)<br>        if game.is_spell_context(ctx):<br>            amt += game.spellpower(ctx.controller)   # \u6cd5\u672f\u4f24\u5bb3\u52a0\u6210<br>        for t in targets:<br>            if t[Tag.ZONE] != Zone.PLAY:             # \u51fa\u53d1\u65f6\u8fd8\u5728\uff0c\u73b0\u5728\u53ef\u80fd\u4e0d\u5728\u4e86<br>                continue<br>            game.damage(t, amt, source=ctx.source)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u6ce8\u610f\u91cc\u9762\u8fd8\u662f\u6709\u4e2a <code>if t[Tag.ZONE] != Zone.PLAY: continue<\/code>\u3002\u8fd9\u4e0d\u77db\u76fe\uff1a<strong>\u76ee\u6807\u5217\u8868\u662f\u5feb\u7167\uff0c\u4f46\u5df2\u7ecf\u79bb\u573a\u7684\u76ee\u6807\u8981\u8df3\u8fc7<\/strong>\u3002\u8fd9\u6b63\u662f\u771f\u5b9e\u7089\u77f3\u7684\u884c\u4e3a\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 12 \u7ae0 Action\uff1a\u6548\u679c\u8bed\u8a00<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">12.1 Action \u57fa\u7c7b\u4e0e\u6267\u884c\u4e0a\u4e0b\u6587<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\"># actions.py<br>@dataclass<br>class Ctx:<br>    \"\"\"Action \u6267\u884c\u4e0a\u4e0b\u6587\uff0c\u6cbf\u7740 Action \u6811\u4f20\u9012\u3002\"\"\"<br>    source: Entity                  # \u6548\u679c\u6765\u6e90\uff08\u5361\u724c\u672c\u4f53\uff09<br>    controller: int<br>    target: Entity | None = None<br>    event: EventCtx | None = None   # \u89e6\u53d1\u578b\u6548\u679c\u7684\u4e8b\u4ef6\u4e0a\u4e0b\u6587<br>    stored: dict = field(default_factory=dict)   # Action \u4e4b\u95f4\u4f20\u503c<br>    choose: int = 0                 # \u6289\u62e9\u9009\u9879<br><br>    TARGET = \"__TARGET__\"           # \u5360\u4f4d\u7b26\uff0c\u4f9b Selector \u5f15\u7528<br><br><br>class Action:<br>    def run(self, game, ctx: Ctx): raise NotImplementedError<br>    def to_json(self) -&gt; dict: ...<br>    def describe(self, locale=\"zh-CN\") -&gt; str: ...<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">12.2 \u63a7\u5236\u6d41 Action<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">class Seq(Action):<br>    \"\"\"\u987a\u5e8f\u6267\u884c\u3002\u8fd9\u662f\u6700\u5e38\u7528\u7684\u7ec4\u5408\u5b50\u3002\"\"\"<br>    def __init__(self, *actions): self.actions = actions<br>    def run(self, game, ctx):<br>        for a in self.actions:<br>            a.run(game, ctx)<br><br>class If(Action):<br>    def __init__(self, cond, then, otherwise=None):<br>        self.cond, self.then, self.otherwise = cond, then, otherwise<br>    def run(self, game, ctx):<br>        if self.cond.eval(game, ctx.source, ctx):<br>            self.then.run(game, ctx)<br>        elif self.otherwise:<br>            self.otherwise.run(game, ctx)<br><br>class Repeat(Action):<br>    def __init__(self, n, action): self.n, self.action = n, action<br>    def run(self, game, ctx):<br>        for _ in range(resolve_amount(self.n, game, ctx)):<br>            self.action.run(game, ctx)<br><br>class ForEach(Action):<br>    \"\"\"\u5bf9\u7b5b\u9009\u5668\u7684\u6bcf\u4e2a\u7ed3\u679c\u6267\u884c\u4e00\u6b21\uff0c\u628a\u5f53\u524d\u9879\u653e\u8fdb ctx.target\"\"\"<br>    def __init__(self, sel, action): self.sel, self.action = sel, action<br>    def run(self, game, ctx):<br>        for e in list(self.sel.eval(game, ctx)):<br>            sub = replace(ctx, target=e)<br>            self.action.run(game, sub)<br><br>class Choose(Action):<br>    \"\"\"\u6289\u62e9\uff1a\u6839\u636e ctx.choose \u5206\u652f\"\"\"<br>    def __init__(self, *options): self.options = options<br>    def run(self, game, ctx):<br>        if ctx.choose == -1:      # \u300a\u5f17\u6d1b\u666e\u7684\u69cc\u5b50\u300b\u7c7b\"\u4e24\u79cd\u6548\u679c\u90fd\u89e6\u53d1\"<br>            for o in self.options: o.run(game, ctx)<br>        else:<br>            self.options[ctx.choose].run(game, ctx)<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">12.3 \u539f\u8bed Action\uff08\u7ea6 40 \u4e2a\u8986\u76d6 95% \u7684\u5361\uff09<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\"># ---- \u4f24\u5bb3\u4e0e\u6cbb\u7597 ----<br>class Damage(Action): ...<br>class Heal(Action): ...<br>class SetHealth(Action): ...<br>class Destroy(Action):<br>    \"\"\"\u76f4\u63a5\u6d88\u706d\uff0c\u65e0\u89c6\u751f\u547d\u503c\u3002\u5267\u6bd2\u3001\u300a\u6697\u5f71\u8bcd\u672f\uff1a\u6b7b\u4ea1\u300b\u7b49\"\"\"<br>    def __init__(self, sel): self.sel = sel<br>    def run(self, game, ctx):<br>        for e in self.sel.eval(game, ctx):<br>            e[Tag.TO_BE_DESTROYED] = 1     # \u53ea\u6807\u8bb0\uff0c\u7b49\u6b7b\u4ea1\u7ed3\u7b97\u9636\u6bb5<br><br># ---- \u724c\u4e0e\u533a\u57df ----<br>class Draw(Action): ...<br>class Discard(Action): ...<br>class Mill(Action): ...                     # \u5f03\u6389\u724c\u5e93\u9876<br>class AddToHand(Action): ...                # \u751f\u6210\u4e00\u5f20\u724c\u5230\u624b<br>class ShuffleIntoDeck(Action): ...<br>class ReturnToHand(Action): ...             # \u300a\u6697\u5f71\u6b65\u300b<br>class Discover(Action): ...<br>class Dredge(Action): ...                   # \u6253\u635e\uff1a\u770b\u5e95\u4e09\u5f20<br>class Tutor(Action): ...                    # \u4ece\u724c\u5e93\u68c0\u7d22\u7279\u5b9a\u5361<br><br># ---- \u6218\u573a ----<br>class Summon(Action): ...<br>class Transform(Action): ...                # \u53d8\u5f62<br>class TakeControl(Action): ...              # \u5fc3\u7075\u63a7\u5236<br>class Silence(Action): ...<br>class Bounce(Action): ...<br><br># ---- \u4fee\u9970 ----<br>class Buff(Action):<br>    \"\"\"\u9644\u52a0\u4e00\u4e2a\u9644\u9b54\"\"\"<br>    def __init__(self, sel, enchant_id): ...<br>class SetTag(Action): ...<br>class GiveKeyword(Action): ...              # \u7ed9\u4e88\u5632\u8bbd\/\u5723\u76fe\/\u98ce\u6012\u7b49<br>class Freeze(Action): ...<br><br># ---- \u8d44\u6e90 ----<br>class GainMana(Action): ...                 # \u300a\u786c\u5e01\u300b<br>class GainEmptyMana(Action): ...            # \u300a\u91ce\u6027\u6210\u957f\u300b<br>class Overload(Action): ...<br>class GainArmor(Action): ...<br>class GainCorpses(Action): ...              # \u5c38\u5757<br><br># ---- \u5143 ----<br>class TriggerDeathrattle(Action): ...       # \u300a\u6069\u4f50\u65af\u300b\u300a\u8df3\u86a4\u5546\u4eba\u300b<br>class CopyCard(Action): ...<br>class SwapWith(Action): ...<br>class ChangeHeroPower(Action): ...<br>class EquipWeapon(Action): ...<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">12.4 \u6570\u503c\u7684\u95f4\u63a5\u5f15\u7528\uff1a<code>Amount<\/code> \u8868\u8fbe\u5f0f<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u5f88\u591a\u5361\u7684\u6570\u503c\u4e0d\u662f\u5e38\u6570\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>&#8220;\u9020\u6210\u7b49\u540c\u4e8e\u4f60\u62a4\u7532\u503c\u7684\u4f24\u5bb3&#8221;<\/li>\n\n\n\n<li>&#8220;\u62bd\u724c\uff0c\u6570\u91cf\u7b49\u4e8e\u4f60\u7684\u968f\u4ece\u6570&#8221;<\/li>\n\n\n\n<li>&#8220;\u83b7\u5f97 +1\/+1\uff0c\u6bcf\u6709\u4e00\u5f20\u624b\u724c&#8221;<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">\u6240\u4ee5 <code>amount<\/code> \u5fc5\u987b\u5141\u8bb8\u662f\u4e00\u4e2a\u8868\u8fbe\u5f0f\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class Amount:<br>    def eval(self, game, ctx) -&gt; int: raise NotImplementedError<br>    def __add__(self, o): return AmountAdd(self, o)<br>    def __mul__(self, o): return AmountMul(self, o)<br><br>class Const(Amount):<br>    def __init__(self, v): self.v = v<br>    def eval(self, g, c): return self.v<br><br>class CountOf(Amount):<br>    def __init__(self, sel): self.sel = sel<br>    def eval(self, g, c): return len(self.sel.eval(g, c))<br><br>class TagOf(Amount):<br>    def __init__(self, sel, tag): self.sel, self.tag = sel, tag<br>    def eval(self, g, c):<br>        es = self.sel.eval(g, c)<br>        return es[0][self.tag] if es else 0<br><br>def resolve_amount(a, game, ctx) -&gt; int:<br>    return a.eval(game, ctx) if isinstance(a, Amount) else int(a)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u4e8e\u662f\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"># \u300a\u80cc\u523a\u300b\uff1a\u5bf9\u4e00\u4e2a\u672a\u53d7\u4f24\u5bb3\u7684\u968f\u4ece\u9020\u6210 2 \u70b9\u4f24\u5bb3<br>BACKSTAB = CardDef(<br>    card_id=\"CS2_072\", cost=0, card_type=\"SPELL\",<br>    targeting=Sel.ALL_MINIONS.where(~HasTag(Tag.DAMAGE)),<br>    spell=Damage(Ctx.TARGET, 2),<br>)<br><br># \u300a\u672b\u65e5\u5b88\u536b\u300b\u2014\u2014\u4e0d\uff0c\u7528\u300a\u708e\u7206\u672f\u300b\uff1a\u9020\u6210 10 \u70b9\u4f24\u5bb3<br>PYROBLAST = CardDef(card_id=\"EX1_279\", cost=10, card_type=\"SPELL\",<br>                    targeting=Sel.ALL_CHARACTERS, spell=Damage(Ctx.TARGET, 10))<br><br># \u300a\u66b4\u6012\u7684\u72fc\u4eba\u300b\uff1a\u5632\u8bbd\uff0c\u98ce\u6012\uff1f\u4e0d\uff0c\u7528\u300a\u7cbe\u7075\u9f99\u300b\u2014\u2014<br># \u7528\u4e00\u4e2a\u771f\u6b63\u9700\u8981 Amount \u7684\uff1a\u300a\u6bc1\u706d\u4e4b\u5203\u300b<br># \"\u9020\u6210\u7b49\u540c\u4e8e\u4f60\u6b66\u5668\u653b\u51fb\u529b\u7684\u4f24\u5bb3\"<br>BLADE_FURY = CardDef(<br>    card_id=\"AT_033\", cost=2, card_type=\"SPELL\",<br>    spell=Seq(<br>        Damage(Sel.ENEMY_MINIONS, TagOf(Sel.MY_WEAPON, Tag.ATK)),<br>        DestroyWeapon(),<br>    ),<br>)<br><br># \u300a\u5317\u90e1\u7267\u5e08\u300b\u7c7b\uff1a\u62bd\u724c\u6570 = \u53cb\u65b9\u968f\u4ece\u6570<br>DRAW_PER_MINION = Draw(CountOf(Sel.FRIENDLY_MINIONS))<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">12.5 \u4e00\u4e2a\u975e\u5e73\u51e1\u7684\u5b8c\u6574\u5361\u724c\u5b9a\u4e49<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u7528\u4e00\u5f20\u6709\u591a\u4e2a\u5173\u952e\u8bcd\u4ea4\u4e92\u7684\u5361\u6765\u9a8c\u8bc1 DSL \u7684\u8868\u8fbe\u529b\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u300a\u7830\u7830\u535a\u58eb\u300b<\/strong>\uff08\u6218\u543c\uff1a\u53ec\u5524\u4e24\u4e2a 1\/1 \u7684\u6a5f\u68b0\uff0c\u5176\u4ea1\u8bed\u4e3a\u5bf9\u968f\u673a\u654c\u4eba\u9020\u6210 1 \u70b9\u4f24\u5bb3\uff09\u2014\u2014\u7528\u4e00\u5f20\u66f4\u590d\u6742\u7684\uff1a<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u300a\u62c9\u683c\u7eb3\u7f57\u65af\u300b<\/strong>\uff1a\u4e0d\u80fd\u653b\u51fb\u3002\u5728\u4f60\u7684\u56de\u5408\u7ed3\u675f\u65f6\uff0c\u5bf9\u4e00\u4e2a\u968f\u673a\u654c\u4eba\u9020\u6210 8 \u70b9\u4f24\u5bb3\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">RAGNAROS = CardDef(<br>    card_id=\"EX1_298\", name=\"\u708e\u9b54\u4e4b\u738b\u62c9\u683c\u7eb3\u7f57\u65af\",<br>    cost=8, card_type=\"MINION\", attack=8, health=8, race=\"ELEMENTAL\",<br>    keywords=frozenset({\"CANNOT_ATTACK\"}),<br>    triggers=(<br>        TriggerDef(<br>            on=Event.TURN_END,<br>            condition=Cond.my_turn(),<br>            action=Damage(Sel.ENEMY_CHARACTERS.random(1), 8),<br>        ),<br>    ),<br>)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u300a\u5e0c\u5c14\u74e6\u5a1c\u65af\u00b7\u98ce\u884c\u8005\u300b<\/strong>\uff1a\u4ea1\u8bed\u2014\u2014\u968f\u673a\u83b7\u5f97\u4e00\u4e2a\u654c\u65b9\u968f\u4ece\u7684\u63a7\u5236\u6743\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">SYLVANAS = CardDef(<br>    card_id=\"EX1_016\", name=\"\u5e0c\u5c14\u74e6\u5a1c\u65af\u00b7\u98ce\u884c\u8005\",<br>    cost=6, card_type=\"MINION\", attack=5, health=5,<br>    deathrattle=TakeControl(Sel.ENEMY_MINIONS.random(1)),<br>)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u300a\u514b\u82cf\u6069\u300b<\/strong>\uff08\u9700\u8981\u8bfb\u53d6\u4e00\u4e2a\u73a9\u5bb6\u7ea7\u8ba1\u6570\u5668\uff09\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">CTHUN = CardDef(<br>    card_id=\"OG_280\", name=\"\u514b\u82cf\u6069\", cost=10, card_type=\"MINION\",<br>    attack=6, health=6,   # \u57fa\u7840\u503c\uff0c\u5b9e\u9645\u7531\u9644\u9b54\u7d2f\u52a0<br>    battlecry=Damage(Sel.ENEMY_CHARACTERS.random(TagOf(Sel.SELF, Tag.ATK)), 1),<br>    # \u5b9e\u9645\u5b9e\u73b0\uff1a\u9020\u6210\"\u653b\u51fb\u529b\"\u6b21 1 \u70b9\u968f\u673a\u4f24\u5bb3<br>)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u300a\u5c24\u683c-\u8428\u9686\u300b<\/strong>\uff08DSL \u65e0\u6cd5\u8868\u8fbe\uff0c\u5fc5\u987b\u9003\u751f\u8231\uff09\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">YOGG = CardDef(<br>    card_id=\"OG_134\", name=\"\u5c24\u683c-\u8428\u9686\", cost=10, card_type=\"MINION\",<br>    attack=7, health=5,<br>    battlecry=SpecialEffect(\"YOGG_SARON_CAST_RANDOM_SPELLS\"),<br>)<br><br>@special_effect(\"YOGG_SARON_CAST_RANDOM_SPELLS\")<br>def yogg(game, ctx):<br>    n = game.player(ctx.controller).spells_cast_this_game<br>    for _ in range(n):<br>        if ctx.source[Tag.ZONE] != Zone.PLAY:<br>            break              # \u5c24\u683c\u6b7b\u4e86\u5c31\u505c\uff08\u8fd9\u662f 2016 \u5e74\u7684\u8457\u540d nerf\uff09<br>        spell = game.rng.choice(game.all_castable_spells())<br>        target = game.random_legal_target(spell)<br>        game.cast_spell_from_effect(spell, target, ctx.controller)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8fd9\u5c31\u662f\u9003\u751f\u8231\u7684\u6b63\u786e\u7528\u6cd5<\/strong>\uff1a\u4e0d\u53bb\u626d\u66f2 DSL \u6765\u8868\u8fbe\u4e00\u5f20\u5361\uff0c\u800c\u662f\u627f\u8ba4\u5b83\u7279\u6b8a\uff0c\u7ed9\u5b83\u5199\u4ee3\u7801\u3002\u771f\u5b9e\u7089\u77f3\u91cc\u8fd9\u6837\u7684\u5361\u4e0d\u8d85\u8fc7 100 \u5f20\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">12.6 \u81ea\u52a8\u751f\u6210\u5361\u724c\u6587\u672c<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">DSL \u7684\u4e00\u4e2a\u526f\u4ea7\u54c1\uff1a\u53ef\u4ee5\u4ece Action \u6811\u751f\u6210\u672c\u5730\u5316\u6587\u672c\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class Damage(Action):<br>    def describe(self, locale=\"zh-CN\"):<br>        t = self.sel.describe(locale)<br>        if locale == \"zh-CN\":<br>            return f\"\u5bf9{t}\u9020\u6210 ${self.amount} \u70b9\u4f24\u5bb3\u3002\"<br>        return f\"Deal ${self.amount} damage to {t}.\"<br><br>&gt;&gt;&gt; BLIZZARD.spell.describe()<br>\"\u5bf9\u6240\u6709\u654c\u65b9\u968f\u4ece\u9020\u6210 $2 \u70b9\u4f24\u5bb3\u3002\u51bb\u7ed3\u6240\u6709\u654c\u65b9\u968f\u4ece\u3002\"<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u771f\u5b9e\u7089\u77f3<strong>\u6ca1\u6709<\/strong>\u5b8c\u5168\u8fd9\u6837\u505a\uff08\u5361\u9762\u6587\u672c\u662f\u624b\u5199\u7684\uff0c\u56e0\u4e3a\u8981\u8003\u8651\u6587\u5b66\u6027\uff09\uff0c\u4f46\u5b83\u786e\u5b9e\u7528 DSL \u6765\u505a<strong>\u52a8\u6001\u6570\u503c\u9ad8\u4eae<\/strong>\uff1a\u5361\u9762\u4e0a\u7684 <code>$2<\/code> \u4f1a\u5728\u6709\u6cd5\u672f\u4f24\u5bb3\u52a0\u6210\u65f6\u663e\u793a\u4e3a\u7eff\u8272\u7684 <code>3<\/code>\u3002\u8fd9\u4e2a\u529f\u80fd\u8981\u6c42\u5f15\u64ce\u80fd\u5728\u4e0d\u6267\u884c\u6548\u679c\u7684\u524d\u63d0\u4e0b\u7b97\u51fa\u6700\u7ec8\u6570\u503c\u2014\u2014\u8fd9\u53ea\u6709\u6570\u636e\u9a71\u52a8\u624d\u505a\u5f97\u5230\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 13 \u7ae0 \u9644\u9b54\uff08Enchantment\uff09\u7cfb\u7edf<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">13.1 \u9644\u9b54\u662f\u5b9e\u4f53<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u4e00\u4e2a\u53cd\u76f4\u89c9\u4f46\u6b63\u786e\u7684\u8bbe\u8ba1\uff1a<strong>\u9644\u9b54\u672c\u8eab\u662f\u4e00\u4e2a\u5b9e\u4f53<\/strong>\uff0c\u6709\u81ea\u5df1\u7684 ENTITY_ID\uff0c\u5b58\u5728\u4e8e\u4e00\u4e2a\u7279\u6b8a\u7684&#8221;\u9644\u7740&#8221;\u5173\u7cfb\u91cc\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">ENCH_BLESSING_OF_KINGS = CardDef(<br>    card_id=\"CS2_092e\", name=\"\u738b\u8005\u795d\u798f\", card_type=\"ENCHANTMENT\",<br>    enchant_tags={Tag.ATK: +4, Tag.HEALTH: +4},<br>)<br><br>def apply_enchantment(game, host: Entity, ench_id: str, duration=None):<br>    e = game.create_entity(ench_id, host[Tag.CONTROLLER], zone=Zone.SETASIDE)<br>    e[Tag.ATTACHED] = host[Tag.ENTITY_ID]<br>    host._enchantments.append(e)<br>    # \u5199\u5165\u5f0f\uff1a\u76f4\u63a5\u6539\u5bbf\u4e3b tag<br>    for tag, delta in e.defn.enchant_tags.items():<br>        host[tag] = host[tag] + delta<br>    if e.defn.triggers:<br>        game.event_bus.register_card(e)   # \u9644\u9b54\u53ef\u4ee5\u5e26\u89e6\u53d1\u5668\uff01<br>    if duration:<br>        e.expires_at = game.turn + duration<br>    return e<br><br>def remove_enchantment(game, e: Entity):<br>    host = game.entities[e[Tag.ATTACHED]]<br>    for tag, delta in e.defn.enchant_tags.items():<br>        host[tag] = host[tag] - delta     # \u56de\u6eda<br>    host._enchantments.remove(e)<br>    game.event_bus.unregister_all(e)<br>    game.destroy_entity(e)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4e3a\u4ec0\u4e48\u9644\u9b54\u8981\u662f\u5b9e\u4f53\u800c\u4e0d\u662f\u4e00\u4e2a dict\uff1f<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u56e0\u4e3a\u9644\u9b54\u9700\u8981\uff1a<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>\u643a\u5e26\u89e6\u53d1\u5668<\/strong>\uff1a\u300a\u4e0d\u7a33\u5b9a\u7684\u4f20\u9001\u95e8\u300b\u7684\u6298\u6263\u3001\u300a\u6697\u5f71\u72c2\u4e71\u300b\u7684&#8221;\u56de\u5408\u7ed3\u675f\u65f6\u5f52\u8fd8&#8221;\u3001\u300a\u56de\u54cd\u300b\u7684&#8221;\u56de\u5408\u7ed3\u675f\u65f6\u6d88\u5931&#8221;<\/li>\n\n\n\n<li><strong>\u88ab\u72ec\u7acb\u5f15\u7528<\/strong>\uff1a\u300a\u6069\u4f50\u65af\uff0c\u6df1\u6d77\u9886\u4e3b\u300b\u8981\u590d\u5236&#8221;\u968f\u4ece\u6b7b\u4ea1\u65f6\u7684\u9644\u9b54\u72b6\u6001&#8221;<\/li>\n\n\n\n<li><strong>\u88ab\u7f51\u7edc\u540c\u6b65<\/strong>\uff1a\u5ba2\u6237\u7aef\u8981\u663e\u793a\u9644\u9b54\u56fe\u6807\u548c tooltip<\/li>\n\n\n\n<li><strong>\u6709\u6765\u6e90<\/strong>\uff1a\u300a\u6c89\u9ed8\u300b\u8981\u533a\u5206&#8221;\u8fd9\u4e2a buff \u662f\u81ea\u5e26\u7684\u8fd8\u662f\u88ab\u52a0\u7684&#8221;<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">\u7b2c 2 \u70b9\u7279\u522b\u91cd\u8981\u3002\u300a\u6697\u5f71\u72c2\u4e71\u300b\u7684\u5b9e\u73b0\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">ENCH_SHADOW_MADNESS = CardDef(<br>    card_id=\"EX1_334e\", card_type=\"ENCHANTMENT\",<br>    triggers=(TriggerDef(<br>        on=Event.TURN_END,<br>        zones=frozenset({Zone.SETASIDE}),   # \u9644\u9b54\u5728 SETASIDE<br>        action=Seq(<br>            ReturnControl(Ctx.HOST),<br>            RemoveSelf(),<br>        ),<br>    ),),<br>)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u9644\u9b54\u81ea\u5df1\u8ba2\u9605&#8221;\u56de\u5408\u7ed3\u675f&#8221;\uff0c\u81ea\u5df1\u5f52\u8fd8\u63a7\u5236\u6743\uff0c\u81ea\u5df1\u5220\u9664\u81ea\u5df1\u3002<strong>\u5bbf\u4e3b\u5b8c\u5168\u4e0d\u9700\u8981\u77e5\u9053\u81ea\u5df1\u88ab\u6697\u5f71\u72c2\u4e71\u4e86\u3002<\/strong> \u8fd9\u662f\u826f\u597d\u5c01\u88c5\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">13.2 \u6c89\u9ed8\uff1a\u4e00\u4e2a\u7cbe\u786e\u5b9a\u4e49\u7684\u64cd\u4f5c<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u6c89\u9ed8\u7684\u89c4\u5219\u6587\u672c\u662f&#8221;\u79fb\u9664\u6240\u6709\u5f53\u524d\u7684\u5361\u724c\u6587\u672c\u3001\u9644\u9b54\u548c\u6280\u80fd&#8221;\u3002\u7cbe\u786e\u5b9a\u4e49\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">SILENCEABLE_TAGS = {<br>    Tag.TAUNT, Tag.DIVINE_SHIELD, Tag.CHARGE, Tag.RUSH, Tag.STEALTH,<br>    Tag.POISONOUS, Tag.LIFESTEAL, Tag.WINDFURY, Tag.MEGA_WINDFURY,<br>    Tag.SPELLPOWER, Tag.REBORN, Tag.ELUSIVE,<br>}<br><br>def silence(game, e: Entity):<br>    if e[Tag.CARDTYPE] != CardType.MINION:<br>        return<br>    # 1. \u79fb\u9664\u6240\u6709\u9644\u9b54<br>    for ench in list(e._enchantments):<br>        remove_enchantment(game, ench)<br>    # 2. \u6ce8\u9500\u6240\u6709\u89e6\u53d1\u5668\uff08\u4ea1\u8bed\u3001\u5149\u73af\u3001\"\u6bcf\u5f53\u2026\"\uff09<br>    game.event_bus.unregister_all(e)<br>    # 3. \u6e05\u9664\u5173\u952e\u8bcd\u6807\u7b7e<br>    for t in SILENCEABLE_TAGS:<br>        e.tags.pop(t, None)<br>    # 4. \u6807\u8bb0<br>    e[Tag.SILENCED] = 1<br>    # 5. \u5149\u73af\u91cd\u7b97<br>    game.aura_bus.mark_dirty()<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6c89\u9ed8\u4e0d\u4f1a\u79fb\u9664\u7684\u4e1c\u897f<\/strong>\uff08\u8fd9\u662f\u8003\u70b9\uff09\uff1a<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u72b6\u6001<\/th><th>\u6c89\u9ed8\u540e<\/th><th>\u539f\u56e0<\/th><\/tr><\/thead><tbody><tr><td>\u5df2\u53d7\u4f24\u5bb3\uff08<code>DAMAGE<\/code>\uff09<\/td><td><strong>\u4fdd\u7559<\/strong><\/td><td>\u4f24\u5bb3\u4e0d\u662f\u6587\u672c<\/td><\/tr><tr><td>\u51bb\u7ed3\uff08<code>FROZEN<\/code>\uff09<\/td><td><strong>\u4fdd\u7559<\/strong><\/td><td>\u51bb\u7ed3\u662f\u72b6\u6001\u4e0d\u662f\u6587\u672c<\/td><\/tr><tr><td>\u53ec\u5524\u5931\u8c03\uff08<code>EXHAUSTED<\/code>\uff09<\/td><td><strong>\u4fdd\u7559<\/strong><\/td><td>\u540c\u4e0a\uff08\u4f46\u6c89\u9ed8\u4f1a\u79fb\u9664\u51b2\u950b\uff0c\u5bfc\u81f4\u4f9d\u7136\u4e0d\u80fd\u653b\u51fb\uff09<\/td><\/tr><tr><td>\u63a7\u5236\u6743<\/td><td><strong>\u4fdd\u7559<\/strong><\/td><td>\u300a\u5fc3\u7075\u63a7\u5236\u300b\u4e0d\u53ef\u88ab\u6c89\u9ed8\u64a4\u9500<\/td><\/tr><tr><td>\u751f\u547d\u503c\u88ab\u6539\uff08\u57fa\u7840 <code>HEALTH<\/code>\uff09<\/td><td><strong>\u56de\u5230\u57fa\u7840\u503c<\/strong><\/td><td>\u4f1a\u5bfc\u81f4\u4e0a\u9650\u964d\u4f4e \u2192 \u53ef\u80fd\u76f4\u63a5\u6b7b<\/td><\/tr><tr><td>\u6765\u81ea\u5149\u73af\u7684 buff<\/td><td><strong>\u4fdd\u7559<\/strong><\/td><td>\u5149\u73af\u6765\u81ea\u522b\u7684\u5b9e\u4f53\uff0c\u91cd\u7b97\u540e\u4f9d\u7136\u751f\u6548<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">\u6700\u540e\u4e00\u6761\u662f\u6700\u5bb9\u6613\u9519\u7684\uff1a<strong>\u6c89\u9ed8\u4e00\u4e2a\u53d7\u300a\u66b4\u98ce\u57ce\u52c7\u58eb\u300b\u5149\u73af\u52a0\u6210\u7684\u968f\u4ece\uff0c\u5b83\u4f9d\u7136\u662f +1\/+1 \u7684<\/strong>\u3002\u56e0\u4e3a\u5149\u73af\u4e0d\u662f\u5bbf\u4e3b\u7684\u9644\u9b54\uff0c\u662f\u52c7\u58eb\u6301\u7eed\u63d0\u4f9b\u7684\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">13.3 \u6301\u7eed\u65f6\u95f4\u4e0e\u5230\u671f<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u9644\u9b54\u6709\u51e0\u79cd\u751f\u547d\u5468\u671f\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class Duration(IntEnum):<br>    PERMANENT       = 0     # \u300a\u738b\u8005\u795d\u798f\u300b<br>    END_OF_TURN     = 1     # \u300a\u72c2\u91ce\u6012\u706b\u300b<br>    START_OF_NEXT   = 2<br>    WHILE_IN_HAND   = 3     # \u624b\u724c\u6298\u6263\uff08\u79bb\u624b\u5373\u5931\u6548\uff1f\u4e0d\uff0c\u901a\u5e38\u4fdd\u7559\uff09<br>    N_TURNS         = 4<br>    UNTIL_SOURCE_LEAVES = 5   # \u8fd9\u4e2a\u5176\u5b9e\u5e94\u8be5\u7528\u5149\u73af<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>END_OF_TURN<\/code> \u7684\u5b9e\u73b0\u6709\u4e2a\u5751\uff1a<strong>\u662f&#8221;\u4f60\u7684\u56de\u5408\u7ed3\u675f&#8221;\u8fd8\u662f&#8221;\u4efb\u610f\u56de\u5408\u7ed3\u675f&#8221;\uff1f<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u7089\u77f3\u7684\u89c4\u5219\u662f&#8221;<strong>\u672c\u56de\u5408\u7ed3\u675f<\/strong>&#8220;\uff0c\u4e5f\u5c31\u662f\u5f53\u524d\u56de\u5408\u7684\u7ed3\u675f\uff0c\u65e0\u8bba\u662f\u8c01\u7684\u56de\u5408\u3002\u300a\u6697\u5f71\u72c2\u4e71\u300b\u5728\u4f60\u7684\u56de\u5408\u6253\u51fa\uff0c\u5728\u4f60\u7684\u56de\u5408\u7ed3\u675f\u65f6\u5f52\u8fd8\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def cleanup_expired_enchantments(game):<br>    for e in list(game.all_entities()):<br>        for ench in list(e._enchantments):<br>            if ench.duration == Duration.END_OF_TURN:<br>                remove_enchantment(game, ench)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u5728 <code>end_turn()<\/code> \u7684\u7b2c 10 \u6b65\u6267\u884c\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">13.4 \u9644\u9b54\u4e0e\u590d\u5236\u7684\u4ea4\u4e92<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u4e00\u4e2a\u5386\u53f2 bug \u6e29\u5e8a\uff1a<strong>\u590d\u5236\u4e00\u4e2a\u968f\u4ece\u65f6\uff0c\u590d\u5236\u7684\u662f\u57fa\u7840\u5361\u8fd8\u662f\u5f53\u524d\u72b6\u6001\uff1f<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u7089\u77f3\u7684\u89c4\u5219\uff08\u73b0\u4ee3\uff09\uff1a<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u5361<\/th><th>\u884c\u4e3a<\/th><\/tr><\/thead><tbody><tr><td>\u300a\u514b\u9686\u300b\uff08\u590d\u5236\u4e00\u4e2a\u968f\u4ece\uff0c\u653e\u5165\u624b\u724c\uff09<\/td><td>\u590d\u5236<strong>\u57fa\u7840\u5361<\/strong>\uff08\u65e0\u9644\u9b54\uff09<\/td><\/tr><tr><td>\u300a\u955c\u50cf\u5b9e\u4f53\u300b<\/td><td>\u590d\u5236<strong>\u57fa\u7840\u5361<\/strong><\/td><\/tr><tr><td>\u300a\u6050\u6016\u7684\u5974\u96b6\u4e3b\u300b\uff08\u53ec\u5524\u4e00\u4e2a\u590d\u5236\uff09<\/td><td>\u590d\u5236<strong>\u5f53\u524d\u72b6\u6001<\/strong>\uff08\u542b\u9644\u9b54\uff09<\/td><\/tr><tr><td>\u300a\u53db\u9003\u8005\u300b\uff08\u83b7\u5f97\u4e00\u4e2a\u590d\u5236\uff09<\/td><td>\u89c6\u5361\u800c\u5b9a<\/td><\/tr><tr><td>\u300a\u6069\u4f50\u65af\uff0c\u6df1\u6d77\u9886\u4e3b\u300b<\/td><td>\u590d\u5236<strong>\u6b7b\u4ea1\u65f6\u7684\u72b6\u6001<\/strong>\uff08\u542b\u9644\u9b54\uff01\uff09<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">\u6240\u4ee5\u5f15\u64ce\u9700\u8981\u4e24\u4e2a\u4e0d\u540c\u7684\u590d\u5236\u64cd\u4f5c\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class CopyMode(IntEnum):<br>    BASE = 0      # \u53ea\u590d\u5236 CARD_ID\uff0c\u91cd\u65b0\u4ece\u5b9a\u4e49\u751f\u6210<br>    FULL = 1      # \u590d\u5236\u5f53\u524d\u6240\u6709 tag \u4e0e\u9644\u9b54<br>    SNAPSHOT = 2  # \u590d\u5236\u6b7b\u4ea1\u5feb\u7167<br><br>def copy_entity(game, src: Entity, mode: CopyMode, controller: int) -&gt; Entity:<br>    if mode == CopyMode.BASE:<br>        return game.create_entity(src[Tag.CARD_ID], controller)<br>    new = game.create_entity(src[Tag.CARD_ID], controller)<br>    if mode == CopyMode.FULL:<br>        for tag, v in src.tags.items():<br>            if tag in (Tag.ENTITY_ID, Tag.ZONE, Tag.ZONE_POSITION,<br>                       Tag.CONTROLLER, Tag.OWNER):<br>                continue<br>            new.tags[tag] = v<br>        for ench in src._enchantments:<br>            apply_enchantment(game, new, ench[Tag.CARD_ID])<br>    return new<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4e3a\u4ec0\u4e48\u8981\u533a\u5206\uff1f<\/strong> \u56e0\u4e3a\u5982\u679c\u5168\u90fd\u662f FULL \u590d\u5236\uff0c\u4e00\u5f20 3 \u8d39\u7684\u300a\u514b\u9686\u300b\u5c31\u80fd\u590d\u5236\u4e00\u4e2a 20\/20 \u7684\u5de8\u578b\u968f\u4ece\uff0c\u6e38\u620f\u5c31\u5d29\u4e86\u3002BASE \u590d\u5236\u662f\u5e73\u8861\u6027\u8981\u6c42\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 14 \u7ae0 \u628a\u5b83\u4eec\u62fc\u8d77\u6765\uff1a\u4e00\u5f20\u5361\u7684\u5b8c\u6574\u751f\u547d\u5468\u671f<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">\u7528\u300a\u7830\u7830\u535a\u58eb\u300b\u8d70\u4e00\u904d\u5168\u6d41\u7a0b\uff0c\u68c0\u9a8c\u524d\u9762\u6240\u6709\u673a\u5236\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">BOOM_BOT = CardDef(<br>    card_id=\"GVG_110t\", name=\"\u7830\u7830\u673a\u5668\u4eba\",<br>    cost=1, card_type=\"MINION\", attack=1, health=1, race=\"MECHANICAL\",<br>    deathrattle=Damage(Sel.ENEMY_CHARACTERS.random(1),<br>                       RandomRange(1, 4)),   # 1-4 \u70b9\u968f\u673a\u4f24\u5bb3<br>)<br><br>DR_BOOM = CardDef(<br>    card_id=\"GVG_110\", name=\"\u7830\u7830\u535a\u58eb\",<br>    cost=7, card_type=\"MINION\", attack=7, health=7,<br>    battlecry=Repeat(2, Summon(\"GVG_110t\")),<br>)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6253\u51fa\u7830\u7830\u535a\u58eb<\/strong>\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">1. play_card(\u7830\u7830\u535a\u58eb)<br>   \u251c\u2500 compute_cost \u2192 7\uff0c\u6263\u8d39<br>   \u251c\u2500 move(\u624b\u724c \u2192 SETASIDE)<br>   \u251c\u2500 emit(PLAY_CARD)<br>   \u251c\u2500 move(SETASIDE \u2192 PLAY, position=3)<br>   \u2502    \u2514\u2500 \u6218\u573a\u5176\u4f59\u968f\u4ece ZONE_POSITION \u91cd\u6392<br>   \u251c\u2500 EXHAUSTED = 1\uff08\u65e0\u51b2\u950b\uff09<br>   \u251c\u2500 event_bus.register_card(\u7830\u7830\u535a\u58eb)  \u2192 \u65e0\u89e6\u53d1\u5668<br>   \u251c\u2500 aura_bus.mark_dirty()<br>   \u251c\u2500 emit(SUMMON, \u7830\u7830\u535a\u58eb)<br>   \u2502    \u2192 \u5bf9\u624b\u300a\u955c\u50cf\u5b9e\u4f53\u300b\u53ef\u5728\u6b64\u54cd\u5e94\uff08\u4f46\u5b83\u8ba2\u9605\u7684\u662f MINION_PLAYED\uff09<br>   \u251c\u2500 emit(MINION_PLAYED, \u7830\u7830\u535a\u58eb)<br>   \u2502    \u2192 \u5bf9\u624b\u300a\u955c\u50cf\u5b9e\u4f53\u300b\u5165\u961f<br>   \u251c\u2500 run(battlecry = Repeat(2, Summon(GVG_110t)))<br>   \u2502    \u251c\u2500 Summon #1<br>   \u2502    \u2502    \u251c\u2500 create_entity(SETASIDE) \u2192 ENTITY_ID = 42<br>   \u2502    \u2502    \u251c\u2500 emit(PRE_SUMMON)<br>   \u2502    \u2502    \u251c\u2500 move(\u2192 PLAY, position=4)  \u2190 \u7830\u7830\u535a\u58eb\u53f3\u8fb9<br>   \u2502    \u2502    \u251c\u2500 register_card \u2192 \u6ce8\u518c\u4ea1\u8bed<br>   \u2502    \u2502    \u2514\u2500 emit(SUMMON) \u2192 \u300a\u98de\u5200\u6742\u800d\u8005\u300b\u7c7b\u5728\u6b64\u5165\u961f<br>   \u2502    \u2514\u2500 Summon #2 \u2192 ENTITY_ID = 43, position=5<br>   \u251c\u2500 COMBO_ACTIVE = 1<br>   \u251c\u2500 emit(AFTER_PLAY)<br>   \u251c\u2500 resolve_queue()<br>   \u2502    \u251c\u2500 \u300a\u955c\u50cf\u5b9e\u4f53\u300b\uff1a\u590d\u5236\u7830\u7830\u535a\u58eb\uff08\u57fa\u7840\u5361\uff09\u5230\u5bf9\u624b\u573a\u4e0a<br>   \u2502    \u251c\u2500 \u300a\u98de\u5200\u6742\u800d\u8005\u300b\u00d72\uff08\u5982\u679c\u6709\uff09<br>   \u2502    \u2514\u2500 ...<br>   \u2514\u2500 process_deaths()<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u7830\u7830\u673a\u5668\u4eba\u88ab\u6253\u6b7b<\/strong>\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">2. damage(\u7830\u7830\u673a\u5668\u4eba#42, 3, source=\u706b\u7403\u672f)<br>   \u251c\u2500 \u65e0\u514d\u75ab\u3001\u65e0\u5723\u76fe<br>   \u251c\u2500 DAMAGE = 3\uff0ccurrent_health = 1 - 3 = -2<br>   \u2514\u2500 emit(DAMAGE, fatal=True)<br><br>3. process_deaths()<br>   \u251c\u2500 \u6536\u96c6\uff1a[#42]<br>   \u251c\u2500 mark_death(#42)\uff1a\u5feb\u7167 position=4, adjacent=(\u7830\u7830\u535a\u58eb, #43)<br>   \u251c\u2500 move(#42 \u2192 GRAVEYARD)<br>   \u251c\u2500 aura_bus.mark_dirty()<br>   \u251c\u2500 emit(DEATH, #42) \u2192 \u4ea1\u8bed\u5165\u961f<br>   \u2514\u2500 resolve_queue()<br>        \u2514\u2500 Damage(Sel.ENEMY_CHARACTERS.random(1), RandomRange(1,4))<br>             \u251c\u2500 \u654c\u65b9\u89d2\u8272 = [\u654c\u65b9\u82f1\u96c4, \u654c\u65b9\u968f\u4ece...]<br>             \u251c\u2500 game.rng.choice(...) \u2192 \u9009\u4e2d\u654c\u65b9\u82f1\u96c4<br>             \u251c\u2500 game.rng.randint(1,4) \u2192 3<br>             \u2514\u2500 damage(\u654c\u65b9\u82f1\u96c4, 3, source=#42)<br>                  \u2514\u2500 \u654c\u65b9\u82f1\u96c4\u5148\u6263\u62a4\u7532<br>4. process_deaths()  \u7b2c\u4e8c\u8f6e \u2192 \u65e0\u65b0\u6b7b\u4ea1\uff0c\u9000\u51fa<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u6ce8\u610f <strong>RNG \u6d88\u8d39\u987a\u5e8f<\/strong>\uff1a\u5148\u9009\u76ee\u6807\uff0c\u518d\u6447\u4f24\u5bb3\u3002\u8fd9\u4e2a\u987a\u5e8f\u5fc5\u987b\u56fa\u5b9a\uff0c\u5426\u5219\u56de\u653e\u4f1a\u4e0d\u4e00\u81f4\u3002\u771f\u5b9e\u7089\u77f3\u4e5f\u662f\u8fd9\u6837\u2014\u2014\u8fd9\u5c31\u662f\u4e3a\u4ec0\u4e48&#8221;\u7089\u77f3\u7684\u968f\u673a\u6570\u662f\u63d0\u524d\u751f\u6210\u7684&#8221;\u8fd9\u4e2a\u8bf4\u6cd5\u662f\u9519\u7684\uff0c\u5b9e\u9645\u662f<strong>\u6309\u56fa\u5b9a\u987a\u5e8f\u6d88\u8d39\u4e00\u4e2a\u786e\u5b9a\u5e8f\u5217<\/strong>\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">\u7b2c\u56db\u90e8\u5206 \u5173\u952e\u8bcd\u7f16\u5e74\u53f2\uff1a2014 \u2192 2026<\/h1>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">\u6bcf\u4e2a\u5173\u952e\u8bcd\u90fd\u6309\u540c\u4e00\u4e2a\u6a21\u677f\u5256\u6790\uff1a <strong>\u5361\u9762\u6587\u672c \u2192 \u8bbe\u8ba1\u610f\u56fe \u2192 \u5f15\u64ce\u5f52\u7c7b \u2192 \u5b9e\u73b0\u4ee3\u7801 \u2192 \u4ea4\u4e92\u9677\u9631<\/strong><\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 15 \u7ae0 \u57fa\u7840\u4e0e\u7ecf\u5178\uff1a\u5960\u5b9a\u8303\u5f0f\u7684\u5341\u516d\u4e2a\u5173\u952e\u8bcd\uff082014\uff09<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">2014 \u5e74 3 \u6708\u6b63\u5f0f\u4e0a\u7ebf\u7684\u57fa\u7840\u7248 + \u7ecf\u5178\u7248\uff0c\u4e00\u6b21\u6027\u5b9a\u4e49\u4e86\u6574\u4e2a\u5f15\u64ce\u7684\u516d\u5927\u8303\u5f0f\u3002\u540e\u9762\u5341\u4e8c\u5e74\u7684\u6240\u6709\u5173\u952e\u8bcd\uff0c\u90fd\u662f\u8fd9\u516d\u79cd\u8303\u5f0f\u7684\u53d8\u4f53\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">15.1 \u5632\u8bbd\uff08Taunt\uff09<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u654c\u65b9\u5fc5\u987b\u5148\u653b\u51fb\u62e5\u6709\u5632\u8bbd\u7684\u968f\u4ece\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5f52\u7c7b<\/strong>\uff1a\u9759\u6001\u6807\u7b7e\uff0c\u5728<strong>\u76ee\u6807\u5408\u6cd5\u6027\u68c0\u67e5<\/strong>\u65f6\u6d88\u8d39\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"># \u5b9e\u73b0\uff1a\u4e0d\u662f\u4e00\u4e2a\"\u6548\u679c\"\uff0c\u800c\u662f is_legal_attack_target \u91cc\u7684\u4e00\u4e2a\u67e5\u8be2<br>def taunt_wall(game, defender_side) -&gt; list[Entity]:<br>    return [m for m in defender_side<br>            if m.has(Tag.TAUNT)<br>            and not m.has(Tag.STEALTH)      # \u6f5c\u884c\u7684\u5632\u8bbd\u4e0d\u751f\u6548<br>            and not m.has(Tag.IMMUNE)       # \u514d\u75ab\u7684\u5632\u8bbd\u4e0d\u751f\u6548<br>            and not m.has(Tag.DORMANT)]     # \u4f11\u7720\u7684\u5632\u8bbd\u4e0d\u751f\u6548<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8bbe\u8ba1\u610f\u56fe<\/strong>\uff1a\u7ed9\u9632\u5b88\u65b9\u4e00\u4e2a&#8221;\u5899&#8221;\uff0c\u8ba9\u5feb\u653b\u4e0d\u80fd\u65e0\u8111\u6253\u8138\u3002\u8fd9\u662f\u7089\u77f3\u552f\u4e00\u7684\u9632\u5fa1\u6027\u673a\u5236\uff08\u6ca1\u6709 MTG \u7684\u963b\u6321\uff09\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u9677\u9631<\/strong>\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u5632\u8bbd<strong>\u4e0d\u5f71\u54cd\u6cd5\u672f\u548c\u82f1\u96c4\u6280\u80fd<\/strong>\uff0c\u53ea\u5f71\u54cd\u653b\u51fb<\/li>\n\n\n\n<li>\u4e09\u4e2a&#8221;\u4e0d\u751f\u6548&#8221;\u6761\u4ef6\u662f\u5206\u522b\u5728\u4e0d\u540c\u7248\u672c\u8865\u4e0a\u7684\uff08\u6f5c\u884c\u5632\u8bbd\u662f 2014 \u5e74\u5c31\u6709\u7684\uff0c\u514d\u75ab\u5632\u8bbd\u662f\u300a\u51b0\u7bb1\u6cd5\u300b\u65f6\u4ee3\u8865\u7684\uff0c\u4f11\u7720\u5632\u8bbd\u662f 2019 \u5e74\u300a\u6697\u5f71\u5d1b\u8d77\u300b\u8865\u7684\uff09<\/li>\n\n\n\n<li><strong>\u5632\u8bbd\u4e0d\u5f71\u54cd&#8221;\u968f\u673a\u653b\u51fb&#8221;<\/strong>\uff08\u300a\u514b\u82cf\u6069\u4e4b\u5203\u300b\u7c7b\uff09\uff0c\u56e0\u4e3a\u90a3\u4e0d\u662f\u653b\u51fb\u5ba3\u8a00<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">15.2 \u51b2\u950b\uff08Charge\uff09<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u53ef\u4ee5\u5728\u4f7f\u7528\u7684\u5f53\u56de\u5408\u653b\u51fb\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5f52\u7c7b<\/strong>\uff1a\u9759\u6001\u6807\u7b7e\uff0c\u5728 <code>can_attack<\/code> \u68c0\u67e5\u65f6\u6d88\u8d39\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def can_attack(game, e) -&gt; bool:<br>    if e[Tag.FROZEN]: return False<br>    if e.has(Tag.CANNOT_ATTACK): return False<br>    if e[Tag.ATK] &lt;= 0: return False<br>    max_attacks = 4 if e.has(Tag.MEGA_WINDFURY) else (2 if e.has(Tag.WINDFURY) else 1)<br>    if e[Tag.NUM_ATTACKS] &gt;= max_attacks: return False<br>    if e[Tag.EXHAUSTED] and not e.has(Tag.CHARGE) and not e.has(Tag.RUSH):<br>        return False<br>    return True<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5386\u53f2<\/strong>\uff1a2019 \u5e74\u51b2\u950b\u88ab\u5927\u89c4\u6a21\u524a\u5f31\u2014\u2014\u6240\u6709 OTK \u7ec4\u5408\u5361\u90fd\u88ab\u6539\u6210\u300a\u7a81\u88ad\u300b\u3002\u8fd9\u662f\u4e00\u6b21<strong>\u5173\u952e\u8bcd\u5c42\u9762\u7684\u5e73\u8861\u6027\u624b\u672f<\/strong>\uff0c\u53ea\u6709\u6570\u636e\u9a71\u52a8\u7684\u5f15\u64ce\u624d\u80fd\u505a\u5230\uff08\u6539\u4e00\u4e2a tag\uff0c\u4e0d\u6539\u4ee3\u7801\uff09\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">15.3 \u5723\u76fe\uff08Divine Shield\uff09<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5f52\u7c7b<\/strong>\uff1a\u9759\u6001\u6807\u7b7e\uff0c\u5728<strong>\u4f24\u5bb3\u7ba1\u7ebf<\/strong>\u4e2d\u6d88\u8d39\uff08\u4e00\u6b21\u6027\uff09\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u5173\u952e\u5b9e\u73b0\u7ec6\u8282\u5728\u7b2c 8 \u7ae0\u5df2\u7ecf\u7ed9\u8fc7\uff0c\u91cd\u70b9\u662f\u4e09\u6761\uff1a<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>\u5723\u76fe<strong>\u5728 <code>PRE_DAMAGE<\/code> \u4e4b\u540e\u6d88\u8017<\/strong>\uff0c\u6240\u4ee5&#8221;\u5982\u679c\u8fd9\u6b21\u4f24\u5bb3\u662f 0\uff0c\u5723\u76fe\u4e0d\u7834&#8221;<\/li>\n\n\n\n<li>\u5723\u76fe\u7834\u4e86\u4e4b\u540e <code>damage()<\/code> <strong>\u8fd4\u56de 0<\/strong>\uff0c\u6240\u4ee5\u5267\u6bd2\/\u5438\u8840\/\u8d85\u6740\u90fd\u4e0d\u751f\u6548<\/li>\n\n\n\n<li>\u5723\u76fe<strong>\u53ea\u6321\u4f24\u5bb3\uff0c\u4e0d\u6321\u6d88\u706d<\/strong>\uff08\u300a\u6697\u5f71\u8bcd\u672f\uff1a\u6b7b\u4ea1\u300b\u7167\u6837\u6740\uff09<\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-preformatted\"># \u53cd\u4f8b\uff1a\u9519\u8bef\u5b9e\u73b0<br>if target.has(Tag.DIVINE_SHIELD):<br>    target[Tag.DIVINE_SHIELD] = 0<br>    return amount            # \u274c \u8fd4\u56de\u4e86\u4f24\u5bb3\u503c\uff0c\u5438\u8840\u4f1a\u751f\u6548<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">15.4 \u98ce\u6012\uff08Windfury\uff09\/ \u8d85\u7ea7\u98ce\u6012\uff08Mega-Windfury\uff09<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u5df2\u5728 <code>can_attack<\/code> \u91cc\u3002<strong>\u8d85\u7ea7\u98ce\u6012<\/strong>\u662f GvG\uff082014.12\uff09\u5f15\u5165\u7684\uff0c\u53ea\u6709\u4e00\u5f20\u300a\u96f7\u795e\u300b\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u9677\u9631<\/strong>\uff1a\u98ce\u6012 + \u51b2\u950b \u7684\u968f\u4ece\uff0c\u7b2c\u4e00\u56de\u5408\u80fd\u653b\u51fb\u4e24\u6b21\u3002\u8fd9\u662f OTK \u7684\u7ecf\u5178\u7ec4\u4ef6\uff0c\u4e5f\u662f\u300a\u4efb\u52a1\u8d3c\u300b\u88ab\u524a\u7684\u539f\u56e0\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">15.5 \u6f5c\u884c\uff08Stealth\uff09<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5f52\u7c7b<\/strong>\uff1a\u9759\u6001\u6807\u7b7e\uff0c\u5728<strong>\u653b\u51fb\u76ee\u6807\u5408\u6cd5\u6027<\/strong>\u548c<strong>\u6cd5\u672f\u76ee\u6807\u5408\u6cd5\u6027<\/strong>\u53cc\u91cd\u68c0\u67e5\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"># \u6f5c\u884c\u7684\u89e3\u9664\u6761\u4ef6\uff1a\u653b\u51fb\u540e<br>def attack(...):<br>    if attacker.has(Tag.STEALTH):<br>        attacker[Tag.STEALTH] = 0    # \u653b\u51fb\u5ba3\u8a00\u540e\u7acb\u5373\u5931\u53bb\uff0c\u4e0d\u662f\u653b\u51fb\u7ed3\u7b97\u540e<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u9677\u9631<\/strong>\uff1a\u6f5c\u884c\u968f\u4ece<strong>\u4f1a<\/strong>\u88ab AOE \u6253\u5230\uff08AOE \u4e0d\u9700\u8981\u6307\u5411\uff09\uff0c<strong>\u4f1a<\/strong>\u88ab\u968f\u673a\u6548\u679c\u9009\u4e2d\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">15.6 \u5267\u6bd2\uff08Poisonous\uff09<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u6b63\u5f0f\u5173\u952e\u8bcd\u5316\u662f 2016 \u5e74 12 \u6708\u300a\u52a0\u57fa\u68ee\u300b\uff0c\u4f46\u673a\u5236\u65e9\u5728 2014 \u5e74\u300a\u6bd2\u8718\u86db\u300b\u5c31\u5b58\u5728\uff08\u5f53\u65f6\u662f\u7eaf\u6587\u672c\uff09\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5f52\u7c7b<\/strong>\uff1a\u4f24\u5bb3\u540e\u5904\u7406\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"># \u4e25\u683c\u5b9a\u4e49\uff1a\u9020\u6210\"\u4f24\u5bb3\"\u540e\u6d88\u706d\u3002\u4e09\u4e2a\u6761\u4ef6\u7f3a\u4e00\u4e0d\u53ef<br>def apply_poison(game, source, target, dealt: int):<br>    if not source.has(Tag.POISONOUS): return<br>    if dealt &lt;= 0: return                       # \u5723\u76fe\u6321\u4f4f \u2192 \u4e0d\u89e6\u53d1<br>    if target[Tag.CARDTYPE] != CardType.MINION: return   # \u5bf9\u82f1\u96c4\u65e0\u6548<br>    target[Tag.TO_BE_DESTROYED] = 1<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u9677\u9631<\/strong>\uff1a\u5267\u6bd2\u662f<strong>\u6807\u8bb0\u6d88\u706d<\/strong>\uff0c\u4e0d\u662f\u7acb\u5373\u6d88\u706d\u3002\u6240\u4ee5&#8221;1\/1 \u5267\u6bd2 vs 10\/10&#8243;\u662f\u540c\u5f52\u4e8e\u5c3d\uff0c\u56e0\u4e3a\u6b7b\u4ea1\u7ed3\u7b97\u9636\u6bb5\u4e24\u8005\u4e00\u8d77\u5224\u5b9a\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">15.7 \u6cd5\u672f\u4f24\u5bb3 +X\uff08Spell Damage\uff09<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5f52\u7c7b<\/strong>\uff1a\u5149\u73af\u5f0f\u5168\u5c40\u4fee\u9970\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def spellpower(game, pid) -&gt; int:<br>    total = 0<br>    for e in game.zones.get(pid, Zone.PLAY):<br>        total += e[Tag.SPELLPOWER]<br>    return total<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u9677\u9631<\/strong>\uff08\u5927\u91cf\uff09\uff1a<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u60c5\u51b5<\/th><th>\u662f\u5426\u52a0\u6210<\/th><\/tr><\/thead><tbody><tr><td>\u6cd5\u672f\u9020\u6210\u4f24\u5bb3<\/td><td>\u2713<\/td><\/tr><tr><td>\u6cd5\u672f\u6cbb\u7597<\/td><td>\u2717\uff08\u9664\u975e\u6709\u300a\u5965\u91d1\u5c3c\u300b\u8f6c\u4f24\u5bb3\uff0c\u5219 \u2713\uff09<\/td><\/tr><tr><td>\u82f1\u96c4\u6280\u80fd\u4f24\u5bb3<\/td><td>\u2717<\/td><\/tr><tr><td>\u6218\u543c\u4f24\u5bb3<\/td><td>\u2717<\/td><\/tr><tr><td>\u4ea1\u8bed\u4f24\u5bb3<\/td><td>\u2717<\/td><\/tr><tr><td>\u6b66\u5668\u4f24\u5bb3<\/td><td>\u2717<\/td><\/tr><tr><td>\u6cd5\u672f\u53ec\u5524\u7684\u968f\u4ece\u7684\u4f24\u5bb3<\/td><td>\u2717<\/td><\/tr><tr><td>\u6cd5\u672f\u9020\u6210\u7684<strong>\u591a\u6bb5<\/strong>\u4f24\u5bb3<\/td><td>\u6bcf\u6bb5\u90fd\u52a0\uff08\u300a\u5965\u672f\u98de\u5f39\u300b\u6bcf\u53d1 +1\uff09<\/td><\/tr><tr><td>\u6cd5\u672f\u9020\u6210\u7684\u4f24\u5bb3\u88ab\u300a\u6697\u8a00\u672f\uff1a\u706d\u300b\u6d88\u706d<\/td><td>\u65e0\u5173<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">\u6700\u540e\u4e00\u6761\uff1a\u300a\u5965\u672f\u98de\u5f39\u300b\u4e09\u53d1\uff0c\u6cd5\u672f\u4f24\u5bb3+1 \u4f1a\u8ba9\u5b83\u53d8\u6210 3 \u53d1 2 \u70b9 = 6 \u70b9\uff0c\u4e0d\u662f 3+1=4 \u70b9\u3002\u8fd9\u4e2a&#8221;\u6bcf\u6bb5\u90fd\u52a0&#8221;\u7684\u89c4\u5219\uff0c\u5728\u5b9e\u73b0\u4e0a\u610f\u5473\u7740<strong>\u52a0\u6210\u5fc5\u987b\u5728\u6700\u5185\u5c42\u7684 <code>damage()<\/code> \u8c03\u7528\u70b9\u5e94\u7528<\/strong>\uff0c\u800c\u4e0d\u662f\u5728 Action \u5c42\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class Damage(Action):<br>    def run(self, game, ctx):<br>        amt = resolve_amount(self.amount, game, ctx)<br>        # \u2605 \u662f\u5426\u662f\u6cd5\u672f\u4e0a\u4e0b\u6587\uff0c\u7531 ctx.source \u7684\u7c7b\u578b + \u89e6\u53d1\u8def\u5f84\u51b3\u5b9a<br>        if game.is_spell_damage_context(ctx):<br>            amt += game.spellpower(ctx.controller)<br>            amt = game.apply_damage_multipliers(amt, ctx)   # \u300a\u91ce\u517d\u4e4b\u738b\u300b\u7c7b\u7ffb\u500d<br>        for t in self.sel.eval(game, ctx):<br>            game.damage(t, amt, source=ctx.source)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>is_spell_damage_context<\/code> \u7684\u5224\u5b9a\uff08<strong>\u63a8\u65ad<\/strong>\uff09\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def is_spell_damage_context(self, ctx) -&gt; bool:<br>    src = ctx.source<br>    if src[Tag.CARDTYPE] == CardType.SPELL:<br>        return True<br>    # \u82f1\u96c4\u6280\u80fd\uff1a\u53ea\u6709\u5c11\u6570\u51e0\u4e2a\u5403\u52a0\u6210\uff08\u5386\u53f2\u4e0a\u300a\u706b\u7130\u51b2\u51fb\u300b\u66fe\u7ecf\u5403\u8fc7\uff0c\u540e\u6765\u6539\u4e86\uff09<br>    if src[Tag.CARDTYPE] == CardType.HERO_POWER:<br>        return src.defn.spell_damage_affected<br>    # \u9644\u9b54\u643a\u5e26\u7684\u89e6\u53d1\uff08\u5982\u300a\u72c2\u91ce\u708e\u672f\u5e08\u300b\uff09\u4e0d\u5403<br>    return False<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">15.8 \u8fc7\u8f7d\uff08Overload\uff09<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5f52\u7c7b<\/strong>\uff1a\u8d44\u6e90\u7cfb\u7edf\u7684\u5ef6\u8fdf\u60e9\u7f5a\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"># \u4e24\u4e2a tag \u7684\u53cc\u7f13\u51b2\u8bbe\u8ba1<br>class Overload(Action):<br>    def __init__(self, n): self.n = n<br>    def run(self, game, ctx):<br>        p = game.player(ctx.controller)<br>        p[Tag.OVERLOAD_OWED] += resolve_amount(self.n, game, ctx)<br><br># begin_turn \u65f6\u7ed3\u7b97<br>p[Tag.OVERLOAD_LOCKED] = p[Tag.OVERLOAD_OWED]<br>p[Tag.OVERLOAD_OWED]  = 0<br>p[Tag.MANA_AVAILABLE] = p[Tag.MANA_CRYSTALS] - p[Tag.OVERLOAD_LOCKED]<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4e3a\u4ec0\u4e48\u8981\u4e24\u4e2a tag\uff1f<\/strong> \u56e0\u4e3a\u5728\u4f60\u7684\u56de\u5408\u91cc\u53ef\u4ee5\u540c\u65f6\u5b58\u5728&#8221;\u672c\u56de\u5408\u88ab\u9501\u7684&#8221;\u548c&#8221;\u4e0b\u56de\u5408\u8981\u9501\u7684&#8221;\u3002\u300a\u96f7\u9706\u5d16\u52c7\u58eb\u300b\u80fd\u5728\u540c\u4e00\u56de\u5408\u770b\u5230\u8fd9\u4e24\u4e2a\u503c\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u9677\u9631<\/strong>\uff1a\u8fc7\u8f7d\u5728<strong>\u6253\u51fa\u724c\u65f6<\/strong>\u7ed3\u7b97\uff0c\u5373\u4f7f\u724c\u88ab\u53cd\u5236\u3002\u300a\u6cd5\u672f\u53cd\u5236\u300b\u53cd\u6389\u300a\u95ea\u7535\u7bad\u300b\uff0c\u4f60\u4f9d\u7136\u8fc7\u8f7d 1\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">15.9 \u8fde\u51fb\uff08Combo\uff09<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5f52\u7c7b<\/strong>\uff1a\u51fa\u724c\u6d41\u7a0b\u7684\u6761\u4ef6\u5206\u652f\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">p[Tag.COMBO_ACTIVE]   # \u672c\u56de\u5408\u662f\u5426\u5df2\u51fa\u8fc7\u724c<br># begin_turn: p[Tag.COMBO_ACTIVE] = 0<br># play_card \u672b\u5c3e: p[Tag.COMBO_ACTIVE] = 1<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u9677\u9631<\/strong>\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u8fde\u51fb\u5224\u5b9a\u7684\u662f&#8221;<strong>\u6253\u51fa\u8fc7\u724c<\/strong>&#8220;\uff0c\u4e0d\u662f&#8221;\u6253\u51fa\u8fc7\u5176\u4ed6\u724c&#8221;\u3002\u6240\u4ee5\u7b2c\u4e8c\u5f20\u8fde\u51fb\u5361\u80fd\u89e6\u53d1<\/li>\n\n\n\n<li><strong>\u786c\u5e01\u7b97\u724c<\/strong>\uff0c\u6240\u4ee5\u5148\u624b\u52a3\u52bf\u5728\u8fd9\u91cc\u88ab\u8865\u507f<\/li>\n\n\n\n<li>\u82f1\u96c4\u6280\u80fd<strong>\u4e0d\u7b97<\/strong><\/li>\n\n\n\n<li>\u88ab\u300a\u6cd5\u672f\u53cd\u5236\u300b\u53cd\u6389\u7684\u724c<strong>\u7b97<\/strong>\uff08\u56e0\u4e3a\u5df2\u7ecf&#8221;\u6253\u51fa&#8221;\uff09<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">15.10 \u6218\u543c\uff08Battlecry\uff09\u4e0e\u4ea1\u8bed\uff08Deathrattle\uff09<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u8fd9\u4e24\u4e2a\u662f\u7089\u77f3\u7684\u7075\u9b42\u673a\u5236\uff0c\u524d\u9762\u5df2\u8be6\u7ec6\u8ba8\u8bba\u3002\u8865\u5145\u51e0\u70b9\uff1a<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6218\u543c\u7684\u89e6\u53d1\u6761\u4ef6<\/strong>\uff08\u4e25\u683c\uff09\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u5fc5\u987b\u662f<strong>\u4ece\u624b\u724c\u6253\u51fa<\/strong>\uff08<code>play_card<\/code> \u8def\u5f84\uff09<\/li>\n\n\n\n<li>\u300a\u53ec\u5524\u300b\u300a\u590d\u6d3b\u300b\u300a\u53d8\u5f62\u300b<strong>\u4e0d\u89e6\u53d1<\/strong><\/li>\n\n\n\n<li>\u300a\u6050\u6016\u7684\u5974\u96b6\u4e3b\u300b\u7c7b&#8221;\u53ec\u5524\u4e00\u4e2a\u590d\u5236&#8221;<strong>\u4e0d\u89e6\u53d1<\/strong><\/li>\n\n\n\n<li>\u300a\u94dc\u955c\u300b\u300a\u6df7\u4e71\u672f\u58eb\u300b\u7c7b&#8221;\u91cd\u590d\u6218\u543c&#8221;\u662f\u7279\u6b8a Action<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4ea1\u8bed\u7684\u89e6\u53d1\u6761\u4ef6<\/strong>\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u5fc5\u987b\u662f<strong>\u4ece\u6218\u573a\u6b7b\u4ea1<\/strong><\/li>\n\n\n\n<li>\u88ab\u300a\u53d8\u5f62\u300b<strong>\u4e0d\u89e6\u53d1<\/strong>\uff08\u5b9e\u4f53\u6ca1\u6b7b\uff09<\/li>\n\n\n\n<li>\u88ab\u300a\u79fb\u51fa\u6e38\u620f\u300b<strong>\u4e0d\u89e6\u53d1<\/strong><\/li>\n\n\n\n<li>\u624b\u724c\u91cc\u88ab\u5f03\u6389<strong>\u4e0d\u89e6\u53d1<\/strong>\uff08\u9664\u975e\u662f\u300a\u624b\u724c\u4ea1\u8bed\u300b\u7684\u5c11\u6570\u7279\u6b8a\u5361\uff09<\/li>\n\n\n\n<li>\u88ab\u6c89\u9ed8\u540e<strong>\u4e0d\u89e6\u53d1<\/strong>\uff08\u89e6\u53d1\u5668\u5df2\u6ce8\u9500\uff09<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">15.11 \u5965\u79d8\uff08Secret\uff09<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5f52\u7c7b<\/strong>\uff1a\u9884\u6ce8\u518c\u89e6\u53d1\u5668 + \u7279\u6b8a\u533a\u57df\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def play_secret(game, card):<br>    pid = card[Tag.CONTROLLER]<br>    # \u540c\u540d\u5965\u79d8\u4e0d\u80fd\u91cd\u590d<br>    existing = {s[Tag.CARD_ID] for s in game.zones.get(pid, Zone.SECRET)}<br>    if card[Tag.CARD_ID] in existing:<br>        return False<br>    if len(game.zones.get(pid, Zone.SECRET)) &gt;= 5:<br>        return False<br>    game.zones.move(card, Zone.SECRET)<br>    game.event_bus.register_card(card)<br>    return True<br><br><br>class RevealSecret(Action):<br>    def run(self, game, ctx):<br>        s = ctx.source<br>        game.emit_immediate(Event.SECRET_REVEALED, entity=s)<br>        game.event_bus.unregister_all(s)<br>        game.zones.move(s, Zone.GRAVEYARD)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u9677\u9631<\/strong>\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u5965\u79d8<strong>\u4e0d\u80fd\u5728\u81ea\u5df1\u56de\u5408\u89e6\u53d1<\/strong>\uff08\u5927\u90e8\u5206\u6709 <code>condition=Cond.enemy_turn()<\/code>\uff09<\/li>\n\n\n\n<li>\u4e00\u6b21\u4e8b\u4ef6\u53ef\u80fd\u89e6\u53d1\u591a\u4e2a\u5965\u79d8\uff0c\u6309 ENTITY_ID \u987a\u5e8f<\/li>\n\n\n\n<li>\u5965\u79d8\u89e6\u53d1\u65f6<strong>\u5148\u63ed\u793a\u518d\u6267\u884c<\/strong>\uff0c\u6240\u4ee5\u300a\u7206\u70b8\u9677\u9631\u300b\u7684\u4f24\u5bb3\u53d1\u751f\u5728\u5bf9\u624b\u770b\u5230\u5b83\u4e4b\u540e<\/li>\n\n\n\n<li>\u5965\u79d8\u89e6\u53d1\u540e\u5373\u4f7f\u6548\u679c\u5931\u8d25\uff08\u5982\u573a\u6ee1\uff09\uff0c<strong>\u7167\u6837\u6d88\u8017<\/strong><\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">15.12 \u6289\u62e9\uff08Choose One\uff09<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5f52\u7c7b<\/strong>\uff1a\u51fa\u724c\u65f6\u7684\u5206\u652f\u9009\u62e9\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"># \u5b9e\u73b0\u65b9\u5f0f\uff1a\u4e24\u5f20\u9690\u85cf\u7684\u5b50\u5361<br>WRATH = CardDef(<br>    card_id=\"EX1_154\", name=\"\u6124\u6012\", cost=2, card_type=\"SPELL\",<br>    choose_one=(\"EX1_154a\", \"EX1_154b\"),<br>    targeting=Sel.ALL_MINIONS,<br>)<br>WRATH_A = CardDef(card_id=\"EX1_154a\", spell=Damage(Ctx.TARGET, 3))<br>WRATH_B = CardDef(card_id=\"EX1_154b\", spell=Seq(Damage(Ctx.TARGET, 1), Draw(1)))<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4e3a\u4ec0\u4e48\u7528\u5b50\u5361\u800c\u4e0d\u662f <code>Choose<\/code> Action\uff1f<\/strong> \u56e0\u4e3a\uff1a<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>\u5ba2\u6237\u7aef\u8981\u663e\u793a\u4e24\u5f20\u72ec\u7acb\u7684\u5361\u9762\uff08\u6709\u5404\u81ea\u7684\u63d2\u56fe\u548c\u6587\u672c\uff09<\/li>\n\n\n\n<li>\u300a\u5f17\u6d1b\u666e\u7684\u69cc\u5b50\u300b\u7c7b&#8221;\u4e24\u79cd\u6548\u679c\u90fd\u89e6\u53d1&#8221;\u9700\u8981\u72ec\u7acb\u6267\u884c\u4e24\u5f20\u5b50\u5361<\/li>\n\n\n\n<li>\u76ee\u6807\u7b5b\u9009\u53ef\u80fd\u4e0d\u540c\uff08\u4e00\u4e2a\u8981\u76ee\u6807\uff0c\u4e00\u4e2a\u4e0d\u8981\uff09<\/li>\n\n\n\n<li>\u8d39\u7528\u53ef\u80fd\u4e0d\u540c\uff08\u5386\u53f2\u4e0a\u300a\u6708\u706b\u672f\u300b\u7c7b\uff09<\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-preformatted\">def resolve_choose_one(game, card, choice: int, target):<br>    if game.player(card[Tag.CONTROLLER]).has_choose_both_aura():<br>        for sub_id in card.defn.choose_one:<br>            game.run(CARD_DB[sub_id].spell, source=card, target=target)<br>    else:<br>        sub = CARD_DB[card.defn.choose_one[choice]]<br>        game.run(sub.spell, source=card, target=target)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">2025 \u5e74\u300a\u7fe1\u7fe0\u68a6\u5883\u300b\u628a\u6289\u62e9\u6269\u5c55\u5230\u4e86\u6240\u6709\u804c\u4e1a\uff0c\u5e76\u5f15\u5165\u4e86&#8221;<strong>\u6289\u62e9\u591a\u9009<\/strong>\uff08Choose Multiple\uff09&#8221;\uff0c\u5141\u8bb8\u9009\u591a\u4e2a\u9009\u9879\u2014\u2014\u4e0a\u9762\u7684\u5b9e\u73b0\u5929\u7136\u652f\u6301\uff08\u628a <code>choice: int<\/code> \u6362\u6210 <code>choices: list[int]<\/code>\uff09\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">15.13 \u51bb\u7ed3\uff08Freeze\uff09\u3001\u6c89\u9ed8\uff08Silence\uff09\u3001\u514d\u75ab\uff08Immune\uff09<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>\u51bb\u7ed3<\/strong>\uff1a<code>Tag.FROZEN = 1<\/code>\uff0c\u5728 <code>begin_turn<\/code> \u65f6\u6e05\u9664\uff0c\u4f46\u53ea\u6709&#8221;\u8be5\u89d2\u8272\u672c\u56de\u5408\u6ca1\u6709\u653b\u51fb\u8fc7&#8221;\u624d\u6e05\u9664\uff08\u907f\u514d\u98ce\u6012\u5361\u51bb\u7ed3\u5931\u6548\uff09\u3002<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-preformatted\">def thaw(game, pid):<br>    for e in game.characters(pid):<br>        if e[Tag.FROZEN] and e[Tag.NUM_ATTACKS] == 0:<br>            e[Tag.FROZEN] = 0<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u5b9e\u9645\u89c4\u5219\u66f4\u5fae\u5999\uff1a<strong>\u5982\u679c\u89d2\u8272\u5728\u88ab\u51bb\u7ed3\u7684\u56de\u5408\u672c\u6765\u5c31\u65e0\u6cd5\u653b\u51fb\uff08\u5982\u53ec\u5524\u5931\u8c03\uff09\uff0c\u51bb\u7ed3\u4f1a\u6301\u7eed\u5230\u4e0b\u4e0b\u56de\u5408<\/strong>\u3002\u8fd9\u662f&#8221;\u51bb\u7ed3\u5728\u4f60\u7684\u56de\u5408\u5f00\u59cb\u65f6\u89e3\u9664\uff0c\u4f46\u524d\u63d0\u662f\u4f60\u4e0a\u4e2a\u56de\u5408\u6ca1\u80fd\u653b\u51fb&#8221;\u7684\u5b9e\u73b0\u7ed3\u679c\u3002<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>\u6c89\u9ed8<\/strong>\uff1a\u89c1 13.2<\/li>\n\n\n\n<li><strong>\u514d\u75ab<\/strong>\uff1a<code>Tag.IMMUNE<\/code>\uff0c\u5728 <code>damage()<\/code> \u6700\u524d\u9762\u62e6\u622a\uff0c\u540c\u65f6\u4e5f\u5728\u76ee\u6807\u5408\u6cd5\u6027\u91cc\u62e6\u622a<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">15.14 \u6fc0\u6012\uff08Enrage\uff0c\u5df2\u5e9f\u5f03\uff09<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">2014-2018 \u5b58\u5728\uff0c\u540e\u6765\u88ab&#8221;<strong>\u53d7\u5230\u4f24\u5bb3\u540e<\/strong>&#8220;\uff08<code>Frenzy<\/code> \u7684\u524d\u8eab\uff09\u53d6\u4ee3\u3002\u5b83\u662f\u4e2a<strong>\u72b6\u6001\u5149\u73af<\/strong>\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"># \u300a\u7802\u7eb8\u55b7\u5242\u300b\u2014\u2014\u7528\u300a\u66b4\u6012\u7684\u72fc\u4eba\u300b\uff1a\u6fc0\u6012\uff1a+3 \u653b\u51fb\u529b<br>AMANI_BERSERKER = CardDef(<br>    card_id=\"EX1_393\", attack=2, health=3,<br>    auras=(AuraDef(<br>        selector=Self_(), tag=Tag.ATK, amount=3, include_self=True,<br>        condition=Cond.self_damaged(),      # DAMAGE &gt; 0<br>    ),),<br>)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4e3a\u4ec0\u4e48\u5e9f\u5f03\uff1f<\/strong> \u56e0\u4e3a\u5b83\u662f<strong>\u5149\u73af<\/strong>\uff0c\u800c\u5149\u73af\u9700\u8981\u6301\u7eed\u91cd\u7b97\uff0c\u4e14\u5b83\u4e0e\u6cbb\u7597\u7684\u4ea4\u4e92\u53cd\u76f4\u89c9\uff08\u6cbb\u7597\u4f1a\u53d6\u6d88\u6fc0\u6012\uff09\u30022019 \u5e74\u5b98\u65b9\u628a\u6240\u6709\u6fc0\u6012\u5361\u6539\u6210\u4e86&#8221;\u53d7\u5230\u4f24\u5bb3\u540e\u6c38\u4e45 +X&#8221;\uff0c\u4e5f\u5c31\u662f<strong>\u4ece\u5149\u73af\u8303\u5f0f\u6539\u6210\u89e6\u53d1\u8303\u5f0f<\/strong>\u3002\u8fd9\u662f\u4e00\u6b21\u6559\u79d1\u4e66\u7ea7\u7684&#8221;\u7528\u7b80\u5355\u8303\u5f0f\u66ff\u6362\u590d\u6742\u8303\u5f0f&#8221;\u7684\u91cd\u6784\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 16 \u7ae0 2014\u20132016\uff1a\u63a2\u7d22\u671f<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">16.1 \u7eb3\u514b\u8428\u739b\u65af\u7684\u8bc5\u5492\uff082014.07\uff09\u2014\u2014\u4ea1\u8bed\u4e3b\u9898<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u65e0\u65b0\u5173\u952e\u8bcd\uff0c\u4f46\u5b83\u662f\u7b2c\u4e00\u6b21\u8bc1\u660e&#8221;<strong>\u4e00\u4e2a\u5df2\u6709\u5173\u952e\u8bcd\u53ef\u4ee5\u6491\u8d77\u4e00\u4e2a\u8d44\u6599\u7247<\/strong>&#8220;\u3002\u5de5\u7a0b\u4e0a\u7684\u610f\u4e49\uff1a<strong>\u4ea1\u8bed\u7cfb\u7edf\u88ab\u538b\u529b\u6d4b\u8bd5<\/strong>\uff0c\u66b4\u9732\u4e86\u5927\u91cf\u65f6\u5e8f bug\uff08\u540c\u65f6\u6b7b\u4ea1\u7684\u987a\u5e8f\u3001\u4ea1\u8bed\u53ec\u5524\u7684\u4f4d\u7f6e\u3001\u4ea1\u8bed\u89e6\u53d1\u4ea1\u8bed\u7684\u9012\u5f52\uff09\u3002\u7b2c 7 \u7ae0\u8bb2\u7684\u6b7b\u4ea1\u7ed3\u7b97\u9636\u6bb5\uff0c\u57fa\u672c\u5c31\u662f\u8fd9\u4e2a\u8d44\u6599\u7247\u4e4b\u540e\u6210\u719f\u7684\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">16.2 \u5730\u7cbe\u5927\u6218\u4f8f\u5112\uff082014.12\uff09\u2014\u2014\u968f\u673a\u6027\u4e0e\u96f6\u4ef6<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u65b0\u589e<\/strong>\uff1a\u673a\u68b0\uff08Mech\uff09\u79cd\u65cf\u3001\u8d85\u7ea7\u98ce\u6012\u3001<strong>\u96f6\u4ef6\uff08Spare Part\uff09<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u96f6\u4ef6\u4e0d\u662f\u5173\u952e\u8bcd\uff0c\u4f46\u5b83\u5f15\u5165\u4e86\u4e00\u4e2a\u91cd\u8981\u673a\u5236\uff1a<strong>\u884d\u751f\u5361\u6c60<\/strong>\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">SPARE_PARTS = [\"PART_001\", \"PART_002\", ..., \"PART_007\"]<br><br>class AddSparePart(Action):<br>    def run(self, game, ctx):<br>        cid = game.rng.choice(SPARE_PARTS)<br>        game.add_to_hand(ctx.controller, cid)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u5de5\u7a0b\u4e0a\uff0c\u8fd9\u8981\u6c42\u5f15\u64ce\u80fd<strong>\u5728\u8fd0\u884c\u65f6\u521b\u5efa\u5361\u724c\u5b9a\u4e49\u91cc\u4e0d\u5b58\u5728\u7684\u5b9e\u4f53<\/strong>\uff0c\u5e76\u4e14\u8fd9\u4e9b\u5b9e\u4f53\u4e0d\u5c5e\u4e8e\u4efb\u4f55\u73a9\u5bb6\u7684\u724c\u5e93\u3002<code>SETASIDE<\/code> \u533a\u548c&#8221;\u884d\u751f\u7269&#8221;\u6982\u5ff5\u5728\u6b64\u56fa\u5316\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">GvG \u8fd8\u662f\u7089\u77f3&#8221;\u968f\u673a\u6027&#8221;\u4e89\u8bae\u7684\u5f00\u7aef\uff08\u300a\u7830\u7830\u535a\u58eb\u300b\u300a\u6df7\u4e71\u5965\uc220\u5e08\u300b\uff09\u3002\u4ece\u5de5\u7a0b\u89d2\u5ea6\uff0c\u8fd9\u4e9b\u5361\u628a RNG \u7684\u786e\u5b9a\u6027\u95ee\u9898\u63a8\u5230\u4e86\u53f0\u524d\u2014\u2014\u7b2c 28 \u7ae0\u4f1a\u8bb2\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">16.3 \u9ed1\u77f3\u5c71\uff082015.04\uff09\u2014\u2014\u9f99<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u65e0\u65b0\u5173\u952e\u8bcd\u3002\u5f15\u5165&#8221;<strong>\u624b\u724c\u6761\u4ef6<\/strong>&#8220;\u8fd9\u4e2a\u67e5\u8be2\u6a21\u5f0f\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">Cond.holding_dragon = lambda: ExistsIn(<br>    InZone(Zone.HAND), IsRace(\"DRAGON\")<br>)<br><br>BLACKWING_CORRUPTOR = CardDef(<br>    card_id=\"BRM_034\", cost=5, attack=5, health=4,<br>    battlecry=If(Cond.holding_dragon(),<br>                 Damage(Ctx.TARGET, 3)),<br>    targeting=Sel.ALL_CHARACTERS,<br>)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5de5\u7a0b\u610f\u4e49<\/strong>\uff1a\u6761\u4ef6\u5224\u5b9a\u8981\u80fd\u67e5\u8be2<strong>\u975e\u516c\u5f00\u533a\u57df<\/strong>\uff08\u624b\u724c\uff09\u3002\u8fd9\u610f\u5473\u7740\u670d\u52a1\u5668\u4e0a\u7684 Condition \u6c42\u503c\u5fc5\u987b\u80fd\u8bbf\u95ee\u5b8c\u6574\u72b6\u6001\uff0c\u800c\u5ba2\u6237\u7aef\u53ea\u80fd\u770b\u5230\u90e8\u5206\u2014\u2014<strong>AI \u548c\u5ba2\u6237\u7aef\u9884\u6d4b\u5728\u8fd9\u91cc\u7b2c\u4e00\u6b21\u5206\u5c94<\/strong>\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">16.4 \u51a0\u519b\u7684\u8bd5\u70bc\uff082015.08\uff09\u2014\u2014\u52b1\u5fd7\u3001\u5bf9\u51b3<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u52b1\u5fd7\uff08Inspire\uff09<\/strong>\uff1a\u6bcf\u5f53\u4f60\u4f7f\u7528\u82f1\u96c4\u6280\u80fd\u540e\u89e6\u53d1\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class Keyword_Inspire:<br>    \"\"\"\u5f52\u7c7b\uff1a\u89e6\u53d1\u578b\uff0c\u8ba2\u9605 HERO_POWER_USED\"\"\"<br>    @staticmethod<br>    def make(action):<br>        return TriggerDef(<br>            on=Event.HERO_POWER_USED,<br>            condition=Cond.controller_is_owner(),<br>            action=action,<br>        )<br><br># \u300a\u5927\u5385\u5b88\u536b\u300b\uff1a\u52b1\u5fd7\uff1a\u83b7\u5f97\u5632\u8bbd<br>LOWLY_SQUIRE = CardDef(<br>    card_id=\"AT_082\", cost=1, attack=1, health=2,<br>    triggers=(Keyword_Inspire.make(Buff(Self_(), \"AT_082e\")),),<br>)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5bf9\u51b3\uff08Joust\uff09<\/strong>\uff1a\u53cc\u65b9\u5404\u4ece\u724c\u5e93\u968f\u673a\u5c55\u793a\u4e00\u5f20\u968f\u4ece\uff0c\u5982\u679c\u4f60\u7684\u8d39\u7528\u66f4\u9ad8\uff0c\u83b7\u5f97\u5956\u52b1\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class Joust(Action):<br>    def __init__(self, on_win): self.on_win = on_win<br>    def run(self, game, ctx):<br>        me, opp = ctx.controller, 1 - ctx.controller<br>        my_pool  = [c for c in game.zones.get(me, Zone.DECK) if c.is_minion]<br>        opp_pool = [c for c in game.zones.get(opp, Zone.DECK) if c.is_minion]<br>        mine = game.rng.choice(my_pool) if my_pool else None<br>        theirs = game.rng.choice(opp_pool) if opp_pool else None<br>        game.reveal(mine, theirs)                # \u5ba2\u6237\u7aef\u52a8\u753b<br>        my_cost = game.compute_cost(mine) if mine else -1<br>        op_cost = game.compute_cost(theirs) if theirs else -1<br>        if my_cost &gt; op_cost:<br>            self.on_win.run(game, ctx)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u5bf9\u51b3<strong>\u5931\u8d25\u4e86<\/strong>\uff08\u4f5c\u4e3a\u673a\u5236\uff09\u3002\u539f\u56e0\u662f\u5b83\u7684\u65b9\u5dee\u592a\u5927\u4e14\u4e0d\u53ef\u63a7\uff0c\u73a9\u5bb6\u8ba8\u538c&#8221;\u62bd\u5361\u51b3\u5b9a\u80dc\u8d1f&#8221;\u3002\u5de5\u7a0b\u4e0a\u5b83\u4e5f\u6709\u4e2a\u95ee\u9898\uff1a<strong>\u5b83\u6cc4\u9732\u4fe1\u606f<\/strong>\uff08\u5c55\u793a\u4e86\u5bf9\u624b\u724c\u5e93\u7684\u4e00\u5f20\u724c\uff09\uff0c\u4f46\u6cc4\u9732\u662f\u968f\u673a\u7684\uff0cAI \u65e0\u6cd5\u5229\u7528\u3002\u8fd9\u662f&#8221;\u8bbe\u8ba1\u4e0a\u6709\u8da3\u4f46\u5b9e\u9645\u4e0d\u597d\u73a9&#8221;\u7684\u5178\u578b\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">16.5 \u63a2\u9669\u8005\u534f\u4f1a\uff082015.11\uff09\u2014\u2014\u53d1\u73b0\uff08Discover\uff09\u2605<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u53d1\u73b0<\/strong>\u662f\u7089\u77f3\u5386\u53f2\u4e0a\u6700\u6210\u529f\u7684\u5173\u952e\u8bcd\uff0c\u6ca1\u6709\u4e4b\u4e00\u3002\u5b83\u81f3\u4eca\u4ecd\u662f\u5e38\u9752\u673a\u5236\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u4ece\u4e09\u5f20\u968f\u673a\u7684\uff08\u804c\u4e1a\u76f8\u5173\u7684\uff09\u5361\u724c\u4e2d\u9009\u62e9\u4e00\u5f20\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5f52\u7c7b<\/strong>\uff1a<strong>\u4e2d\u65ad\u578b<\/strong>\u2014\u2014\u9700\u8981\u73a9\u5bb6\u8f93\u5165\uff0c\u6253\u7834\u4e86&#8221;\u7ed3\u7b97\u539f\u5b50\u6027&#8221;\uff08\u89c1 5.3\uff09\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class Discover(Action):<br>    def __init__(self, pool: \"CardPool\", count=3, then=None):<br>        self.pool, self.count, self.then = pool, count, then<br><br>    async def run(self, game, ctx):<br>        candidates = self.pool.build(game, ctx)<br>        if not candidates:<br>            return<br>        # \u2605 \u65e0\u653e\u56de\u62bd\u6837\uff0c\u4fdd\u8bc1\u4e09\u5f20\u4e0d\u91cd\u590d<br>        picks = game.rng.sample(candidates, min(self.count, len(candidates)))<br>        chosen = await game.ask_choice(ctx.controller, picks, kind=\"DISCOVER\")<br>        act = self.then or AddToHand(chosen)<br>        await act.run(game, replace(ctx, stored={\"discovered\": chosen}))<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5361\u6c60\uff08CardPool\uff09\u7684\u5b9a\u4e49<\/strong>\u662f\u53d1\u73b0\u7684\u7cbe\u9ad3\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">@dataclass<br>class CardPool:<br>    card_type: str | None = None<br>    klass: str | None = \"PLAYER_CLASS\"    # \u7279\u6b8a\u503c\uff1a\u4f7f\u7528\u8005\u7684\u804c\u4e1a<br>    race: str | None = None<br>    cost_range: tuple[int,int] | None = None<br>    from_zone: Zone | None = None          # \u4ece\u724c\u5e93\/\u5893\u5730\u53d1\u73b0<br>    collectible_only: bool = True          # \u6392\u9664\u884d\u751f\u7269<br>    sets: frozenset[str] | None = None     # \u9650\u5b9a\u7248\u672c\uff08\u5982\"\u7ecf\u5178\u5361\"\uff09<br><br>    def build(self, game, ctx) -&gt; list[str]:<br>        pool = game.card_index.query(<br>            card_type=self.card_type,<br>            klass=(game.player(ctx.controller).klass<br>                   if self.klass == \"PLAYER_CLASS\" else self.klass),<br>            race=self.race, cost_range=self.cost_range,<br>            collectible=self.collectible_only, sets=self.sets,<br>        )<br>        return pool<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4e3a\u4ec0\u4e48\u53d1\u73b0\u5982\u6b64\u6210\u529f\uff1f\u4e09\u4e2a\u5de5\u7a0b\/\u8bbe\u8ba1\u5c42\u9762\u7684\u7406\u7531<\/strong>\uff1a<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>\u5b83\u628a\u968f\u673a\u6027\u53d8\u6210\u4e86\u51b3\u7b56<\/strong>\u3002RNG \u751f\u6210\u4e09\u4e2a\u9009\u9879\uff0c\u4f46\u73a9\u5bb6\u505a\u9009\u62e9\u3002\u8fd9\u6d88\u9664\u4e86&#8221;\u88ab\u968f\u673a\u6570\u6740\u6b7b&#8221;\u7684\u632b\u8d25\u611f\uff0c\u540c\u65f6\u4fdd\u7559\u4e86\u591a\u6837\u6027\u3002<\/li>\n\n\n\n<li><strong>\u5b83\u662f\u7248\u672c\u65e0\u5173\u7684<\/strong>\u3002\u5361\u6c60\u662f\u52a8\u6001\u67e5\u8be2\u7684\uff0c\u65b0\u8d44\u6599\u7247\u81ea\u52a8\u52a0\u5165\uff0c\u8001\u5361\u81ea\u52a8\u8f6e\u6362\u51fa\u53bb\u3002\u4e00\u5f20 2015 \u5e74\u7684\u53d1\u73b0\u5361\u5728 2026 \u5e74\u4f9d\u7136\u5de5\u4f5c\uff0c\u4e14\u6548\u679c\u5b8c\u5168\u4e0d\u540c\u3002<strong>\u8fd9\u662f\u6570\u636e\u9a71\u52a8\u7684\u6700\u9ad8\u4ef7\u503c\u4f53\u73b0\u3002<\/strong><\/li>\n\n\n\n<li><strong>\u5b83\u7684\u5b9e\u73b0\u662f\u53ef\u590d\u7528\u7684<\/strong>\u3002\u540e\u6765\u7684\u300a\u9002\u5e94\u300b\u300a\u6253\u635e\u300b\u300a\u6316\u6398\u300b\u300a\u953b\u9020\u300b\u300a\u9ed1\u6697\u6069\u8d50\u300b\u5168\u90fd\u662f Discover \u7684\u53d8\u4f53\uff0c\u5171\u7528\u540c\u4e00\u5957 UI \u548c\u6302\u8d77\u673a\u5236\u3002<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u53d1\u73b0\u7684\u53d8\u4f53\u8c31\u7cfb<\/strong>\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">Discover (2015)<br>\u251c\u2500\u2500 Adapt (2017)         \u2190 \u4ece\u56fa\u5b9a\u7684 10 \u4e2a buff \u91cc\u9009 3 \u4e2a<br>\u251c\u2500\u2500 Discover from deck   \u2190 \u5361\u6c60 = \u4f60\u7684\u724c\u5e93<br>\u251c\u2500\u2500 Dredge (2022)        \u2190 \u5361\u6c60 = \u724c\u5e93\u5e95\u4e09\u5f20\uff0c\u7ed3\u679c\u653e\u724c\u5e93\u9876<br>\u251c\u2500\u2500 Excavate (2023)      \u2190 \u5361\u6c60\u6309\"\u6316\u6398\u7b49\u7ea7\"\u5206\u5c42<br>\u251c\u2500\u2500 Forge (2023)         \u2190 \u4e0d\u662f Discover\uff0c\u4f46\u5171\u7528 UI<br>\u2514\u2500\u2500 Dark Gift (2025)     \u2190 \u53d1\u73b0\u4e00\u4e2a buff \u9644\u52a0\u7ed9\u968f\u4ece<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">16.6 \u4e0a\u53e4\u4e4b\u795e\u7684\u4f4e\u8bed\uff082016.04\uff09\u2014\u2014\u514b\u82cf\u6069\u4e0e\u8150\u5316<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u65e0\u6b63\u5f0f\u5173\u952e\u8bcd<\/strong>\uff0c\u4f46\u5f15\u5165\u4e86\u4e24\u4e2a\u673a\u5236\u6a21\u5f0f\uff1a<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\uff081\uff09\u514b\u82cf\u6069\u8ba1\u6570\u5668<\/strong>\uff1a\u6240\u6709&#8221;\u5f3a\u5316\u514b\u82cf\u6069&#8221;\u7684\u5361\u90fd\u7ed9\u4e00\u4e2a<strong>\u8de8\u533a\u57df\u7684\u6301\u4e45\u9644\u9b54<\/strong>\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class BuffCthun(Action):<br>    def __init__(self, atk, hp): self.atk, self.hp = atk, hp<br>    def run(self, game, ctx):<br>        # \u627e\u5230\u514b\u82cf\u6069\uff0c\u65e0\u8bba\u5b83\u5728\u54ea\u4e2a\u533a\u57df\uff08\u624b\u724c\/\u724c\u5e93\/\u6218\u573a\/\u5893\u5730\uff09<br>        for e in game.all_entities_of(ctx.controller, card_id=\"OG_280\"):<br>            apply_enchantment(game, e, \"OG_280e\", atk=self.atk, hp=self.hp)<br>        # \u5982\u679c\u514b\u82cf\u6069\u4e0d\u5728\uff08\u88ab\u62bd\u8d70\/\u88ab\u6d88\u706d\uff09\uff0c\u4e5f\u8981\u8bb0\u5f55\uff0c<br>        # \u56e0\u4e3a\u540e\u6765\u53ef\u80fd\u901a\u8fc7\u300a\u514b\u82cf\u6069\u6559\u5f92\u300b\u518d\u751f\u6210\u4e00\u4e2a<br>        game.player(ctx.controller).cthun_buff += (self.atk, self.hp)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5de5\u7a0b\u96be\u70b9<\/strong>\uff1a\u9644\u9b54\u5fc5\u987b\u80fd\u4f5c\u7528\u4e8e<strong>\u724c\u5e93\u91cc\u7684\u5361<\/strong>\u3002\u8fd9\u8981\u6c42 <code>Zone.DECK<\/code> \u91cc\u7684\u5b9e\u4f53\u662f\u5b8c\u6574\u5b9e\u4f53\uff08\u6709 tag \u8868\uff09\uff0c\u4e0d\u662f\u7b80\u5355\u7684 card_id \u5217\u8868\u3002\u5f88\u591a\u4eba\u505a\u5361\u724c\u6e38\u620f\u65f6\u4f1a\u628a\u724c\u5e93\u505a\u6210 <code>list[str]<\/code>\uff0c\u8fd9\u662f\u4e2a\u9677\u9631\u2014\u2014<strong>\u724c\u5e93\u91cc\u7684\u5361\u5fc5\u987b\u662f\u5b9e\u4f53<\/strong>\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\uff082\uff09\u8150\u5316\uff08Corrupted\uff09<\/strong>\uff1a\u4e00\u5f20\u5361\u6709&#8221;\u6b63\u5e38\u7248&#8221;\u548c&#8221;\u8150\u5316\u7248&#8221;\u4e24\u4e2a\u5b9a\u4e49\uff0c\u6784\u7b51\u65f6\u4e8c\u9009\u4e00\u3002\u8fd9\u53ea\u662f\u5361\u6c60\u5c42\u9762\u7684\u4e8b\uff0c\u5f15\u64ce\u65e0\u611f\u77e5\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">16.7 \u52a0\u57fa\u68ee\uff082016.12\uff09\u2014\u2014\u5267\u6bd2\u5173\u952e\u8bcd\u5316\u3001\u624b\u724c buff\u3001\u4e09\u804c\u4e1a<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5267\u6bd2<\/strong>\u4ece\u7eaf\u6587\u672c\u53d8\u6210\u5173\u952e\u8bcd\u3002\u8fd9\u6b21&#8221;\u5173\u952e\u8bcd\u5316&#8221;\u672c\u8eab\u662f\u4e2a\u6709\u610f\u601d\u7684\u5de5\u7a0b\u52a8\u4f5c\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u4e4b\u524d\uff1a\u300a\u6bd2\u8718\u86db\u300b\u7684\u6587\u672c\u662f&#8221;\u6bcf\u5f53\u8be5\u968f\u4ece\u5bf9\u4e00\u4e2a\u968f\u4ece\u9020\u6210\u4f24\u5bb3\uff0c\u6d88\u706d\u8be5\u968f\u4ece&#8221;\uff0c\u5b9e\u73b0\u662f\u4e00\u4e2a <code>TriggerDef(on=DAMAGE, source_only=True, action=Destroy(...))<\/code><\/li>\n\n\n\n<li>\u4e4b\u540e\uff1a<code>keywords={\"POISONOUS\"}<\/code>\uff0c\u5b9e\u73b0\u79fb\u5230\u4f24\u5bb3\u7ba1\u7ebf\u91cc<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4e3a\u4ec0\u4e48\u8981\u6539\uff1f<\/strong> \u56e0\u4e3a\u89e6\u53d1\u5f0f\u5b9e\u73b0\u6709\u4e2a bug\uff1a\u5b83\u662f<strong>\u4e8b\u4ef6\u9a71\u52a8\u7684<\/strong>\uff0c\u6240\u4ee5\u5728&#8221;\u540c\u65f6\u6b7b\u4ea1&#8221;\u7684\u573a\u666f\u4e0b\u987a\u5e8f\u4f1a\u51fa\u9519\u3002\u79fb\u5230\u4f24\u5bb3\u7ba1\u7ebf\u91cc\u4e4b\u540e\uff0c\u5267\u6bd2\u7684\u6807\u8bb0\u548c\u4f24\u5bb3\u662f\u540c\u6b65\u7684\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u624b\u724c buff<\/strong>\uff08\u300a\u514b\u7f57\u5730\u4e9a\u300b\u7c7b&#8221;\u4f7f\u4f60\u624b\u724c\u4e2d\u7684\u968f\u4ece\u83b7\u5f97 +1\/+1&#8243;\uff09\u8981\u6c42\uff1a\u624b\u724c\u91cc\u7684\u5b9e\u4f53\u4e5f\u80fd\u88ab\u9644\u9b54\uff0c\u4e14\u9644\u9b54\u5728\u6253\u51fa\u540e\u4fdd\u7559\u3002\u8fd9\u8ddf\u514b\u82cf\u6069\u7684\u9700\u6c42\u662f\u4e00\u6837\u7684\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 17 \u7ae0 2017\uff1a\u673a\u5236\u7206\u53d1\u7684\u4e00\u5e74<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">17.1 \u5b89\u6208\u6d1b\uff082017.04\uff09\u2014\u2014\u9002\u5e94\u3001\u4efb\u52a1\u3001\u5143\u7d20<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\">\u9002\u5e94\uff08Adapt\uff09<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u4ece\u5341\u79cd\u5f3a\u5316\u4e2d\u53d1\u73b0\u4e09\u79cd\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u5341\u79cd\u5f3a\u5316\u662f\u56fa\u5b9a\u7684\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">ADAPTATIONS = [<br>    (\"UNG_999t2\",  \"\u575a\u786c\u5982\u94c1\", GiveKeyword(Ctx.TARGET, \"DIVINE_SHIELD\")),<br>    (\"UNG_999t3\",  \"\u71c3\u70e7\u4e4b\u6838\", Buff(Ctx.TARGET, atk=3, hp=0)),<br>    (\"UNG_999t4\",  \"\u5229\u5203\u5c16\u523a\", GiveKeyword(Ctx.TARGET, \"POISONOUS\")),<br>    (\"UNG_999t5\",  \"\u751f\u547d\u4e4b\u6d41\", Buff(Ctx.TARGET, atk=0, hp=3)),<br>    (\"UNG_999t6\",  \"\u75be\u5982\u730e\u8c79\", GiveKeyword(Ctx.TARGET, \"WINDFURY\")),<br>    (\"UNG_999t7\",  \"\u5de8\u5927\u4e4b\u529b\", Buff(Ctx.TARGET, atk=1, hp=1)),   # \u5b9e\u9645\u662f +1\/+1<br>    (\"UNG_999t8\",  \"\u94a2\u94c1\u4e4b\u76ae\", GiveKeyword(Ctx.TARGET, \"TAUNT\")),<br>    (\"UNG_999t10\", \"\u9634\u5f71\u4f2a\u88c5\", GiveKeyword(Ctx.TARGET, \"STEALTH\")),<br>    (\"UNG_999t13\", \"\u6bd2\u6027\u4e4b\u523a\", Deathrattle(Summon(\"UNG_999t2t1\", n=2))),<br>    (\"UNG_999t14\", \"\u75af\u72c2\u589e\u6b96\", ...),<br>]<br><br>class Adapt(Action):<br>    def __init__(self, sel): self.sel = sel<br>    async def run(self, game, ctx):<br>        picks = game.rng.sample(ADAPTATIONS, 3)<br>        chosen = await game.ask_choice(ctx.controller, picks, kind=\"ADAPT\")<br>        for t in self.sel.eval(game, ctx):<br>            await chosen.action.run(game, replace(ctx, target=t))<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8bbe\u8ba1\u6d1e\u5bdf<\/strong>\uff1a\u9002\u5e94\u628a Discover \u7684&#8221;\u65e0\u9650\u5361\u6c60&#8221;\u6536\u7f29\u5230&#8221;\u56fa\u5b9a\u5341\u9009\u4e09&#8221;\u3002\u8fd9\u8ba9\u5b83<strong>\u53ef\u9884\u6d4b<\/strong>\uff08\u5bf9\u624b\u77e5\u9053\u4f60\u53ef\u80fd\u83b7\u5f97\u4ec0\u4e48\uff09\uff0c\u540c\u65f6\u4fdd\u7559\u4e86\u9009\u62e9\u3002\u5de5\u7a0b\u4e0a\u5b83\u590d\u7528\u4e86 Discover \u7684\u5168\u90e8\u57fa\u7840\u8bbe\u65bd\uff0c\u65b0\u589e\u4ee3\u7801\u4e0d\u5230 30 \u884c\u3002<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">\u4efb\u52a1\uff08Quest\uff09\u2605<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u4efb\u52a1\uff1a\u5b8c\u6210 X \u6b21 Y\u3002\u5956\u52b1\uff1aZ\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u4efb\u52a1\u662f\u7089\u77f3\u7b2c\u4e00\u4e2a<strong>\u6709\u72b6\u6001\u7684\u3001\u8de8\u56de\u5408\u7684\u3001\u53ef\u89c1\u7684\u8ba1\u6570\u5668\u673a\u5236<\/strong>\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">@dataclass(frozen=True)<br>class QuestDef:<br>    trigger_event: Event<br>    condition: \"Condition\"<br>    required: int<br>    reward_card_id: str<br><br>QUEST_OPEN_THE_WAYGATE = CardDef(<br>    card_id=\"UNG_028\", name=\"\u5f00\u542f\u4f20\u9001\u95e8\", cost=1, card_type=\"SPELL\",<br>    keywords=frozenset({\"QUEST\"}),<br>    quest=QuestDef(<br>        trigger_event=Event.SPELL_CAST,<br>        condition=Cond.spell_not_in_starting_deck(),   # \"\u4f7f\u7528\u4e00\u5f20\u4e0d\u5728\u4f60\u521d\u59cb\u724c\u5e93\u4e2d\u7684\u6cd5\u672f\"<br>        required=6,<br>        reward_card_id=\"UNG_028t\",   # \u65f6\u5149\u626d\u66f2<br>    ),<br>)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u5f15\u64ce\u4fa7\u7684\u5b9e\u73b0\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def play_quest(game, card):<br>    pid = card[Tag.CONTROLLER]<br>    game.zones.move(card, Zone.SECRET)     # \u2605 \u4efb\u52a1\u653e\u5728\u5965\u79d8\u533a\uff01<br>    card[Tag.QUEST_PROGRESS] = 0<br>    card[Tag.QUEST_TOTAL] = card.defn.quest.required<br>    game.event_bus.register(card, TriggerDef(<br>        on=card.defn.quest.trigger_event,<br>        zones=frozenset({Zone.SECRET}),<br>        condition=card.defn.quest.condition &amp; Cond.controller_is_owner(),<br>        action=QuestProgress(),<br>    ))<br><br>class QuestProgress(Action):<br>    def run(self, game, ctx):<br>        q = ctx.source<br>        q[Tag.QUEST_PROGRESS] += 1<br>        if q[Tag.QUEST_PROGRESS] &gt;= q[Tag.QUEST_TOTAL]:<br>            game.zones.move(q, Zone.GRAVEYARD)<br>            game.event_bus.unregister_all(q)<br>            game.add_to_hand(ctx.controller, q.defn.quest.reward_card_id)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4e3a\u4ec0\u4e48\u4efb\u52a1\u653e\u5728\u5965\u79d8\u533a\uff1f<\/strong> \u8fd9\u662f\u4e00\u4e2a\u7eaf\u7cb9\u7684\u590d\u7528\u51b3\u7b56\uff1a\u5965\u79d8\u533a\u5df2\u7ecf\u6709\u4e86&#8221;\u53ef\u89c1\u7684\u3001\u975e\u6218\u573a\u7684\u3001\u80fd\u627f\u8f7d\u89e6\u53d1\u5668\u7684\u5b9e\u4f53\u5bb9\u5668&#8221;\u7684\u5168\u90e8\u57fa\u7840\u8bbe\u65bd\u3002\u4ee3\u4ef7\u662f\u300a\u5965\u79d8\u7ba1\u7406\u5458\u300b\u80fd\u6293\u5230\u4efb\u52a1\uff08\u66fe\u7ecf\u662f bug\uff0c\u540e\u6765\u52a0\u4e86 <code>Cond.is_secret_not_quest()<\/code> \u8fc7\u6ee4\uff09\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4efb\u52a1\u7684\u4e00\u4e2a\u9690\u853d\u9677\u9631<\/strong>\uff1a<code>QUEST_PROGRESS<\/code> \u662f\u4e00\u4e2a tag\uff0c\u4f1a\u88ab\u7f51\u7edc\u540c\u6b65\u7ed9<strong>\u53cc\u65b9<\/strong>\u3002\u6240\u4ee5\u5bf9\u624b\u80fd\u770b\u5230\u4f60\u7684\u4efb\u52a1\u8fdb\u5ea6\u2014\u2014\u8fd9\u662f\u8bbe\u8ba1\u8981\u6c42\uff08\u4efb\u52a1\u662f\u516c\u5f00\u4fe1\u606f\uff09\uff0c\u4f46\u4e5f\u610f\u5473\u7740\u5f15\u64ce\u4e0d\u80fd\u628a\u5b83\u85cf\u8d77\u6765\u3002<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">\u5143\u7d20\uff08Elemental\uff09<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">\u65b0\u79cd\u65cf\uff0c\u4f46\u5e26\u6765\u4e86\u4e00\u4e2a\u65b0\u7684\u67e5\u8be2\u6a21\u5f0f\uff1a<strong>&#8220;\u5982\u679c\u4f60\u5728\u4e0a\u4e2a\u56de\u5408\u4f7f\u7528\u8fc7\u5143\u7d20&#8221;<\/strong>\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"># \u9700\u8981\u4e00\u4e2a\u73a9\u5bb6\u7ea7\u7684\u5386\u53f2\u72b6\u6001<br>class PlayerState:<br>    played_elemental_last_turn: bool<br>    played_elemental_this_turn: bool<br><br># end_turn \u65f6\u8f6e\u8f6c<br>def rotate_elemental_flags(p):<br>    p.played_elemental_last_turn = p.played_elemental_this_turn<br>    p.played_elemental_this_turn = False<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5de5\u7a0b\u610f\u4e49<\/strong>\uff1a\u8fd9\u662f\u5f15\u64ce\u7b2c\u4e00\u6b21\u9700\u8981\u7ef4\u62a4&#8221;<strong>\u4e0a\u4e2a\u56de\u5408\u53d1\u751f\u4e86\u4ec0\u4e48<\/strong>&#8220;\u7684\u5386\u53f2\u72b6\u6001\u3002\u4e4b\u524d\u6240\u6709\u6761\u4ef6\u90fd\u662f\u67e5\u8be2\u5f53\u524d\u72b6\u6001\u3002\u8fd9\u6253\u5f00\u4e86\u4e00\u4e2a\u65b0\u7684\u8bbe\u8ba1\u7a7a\u95f4\uff0c\u540e\u6765\u7684\u300a\u540c\u6e90\u300b\uff08Kindred, 2025\uff09\u5c31\u662f\u5b83\u7684\u76f4\u7cfb\u540e\u4ee3\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">17.2 \u51b0\u5c01\u738b\u5ea7\uff082017.08\uff09\u2014\u2014\u5438\u8840\u3001\u82f1\u96c4\u724c<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\">\u5438\u8840\uff08Lifesteal\uff09<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5f52\u7c7b<\/strong>\uff1a\u4f24\u5bb3\u540e\u5904\u7406\uff08\u4e0e\u5267\u6bd2\u540c\u7c7b\uff09\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"># \u5728 damage() \u5185\u90e8<br>if source is not None and source.has(Tag.LIFESTEAL):<br>    game.heal(game.player(source[Tag.CONTROLLER]).hero, actual_dealt)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u9677\u9631\u6e05\u5355<\/strong>\uff1a<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u60c5\u51b5<\/th><th>\u662f\u5426\u5438\u8840<\/th><\/tr><\/thead><tbody><tr><td>\u5723\u76fe\u6321\u4f4f<\/td><td>\u2717\uff08\u5b9e\u9645\u4f24\u5bb3 0\uff09<\/td><\/tr><tr><td>\u76ee\u6807\u662f\u514d\u75ab<\/td><td>\u2717<\/td><\/tr><tr><td>\u8d85\u6740\uff08\u6253\u51fa\u6ea2\u51fa\u4f24\u5bb3\uff09<\/td><td>\u53ea\u7b97<strong>\u5b9e\u9645\u9020\u6210\u7684<\/strong>\uff08\u6253 3 \u8840\u968f\u4ece\u9020\u6210 5 \u70b9\uff0c\u53ea\u5438 3\uff1f\uff09<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">\u6700\u540e\u4e00\u6761\u7684\u771f\u5b9e\u7b54\u6848\u662f\uff1a<strong>\u5438 5<\/strong>\u3002\u56e0\u4e3a <code>damage()<\/code> \u8fd4\u56de\u7684\u662f&#8221;\u6263\u4e86\u591a\u5c11 DAMAGE \u6807\u8bb0&#8221;\uff0c\u800c\u4e0d\u662f&#8221;\u5b9e\u9645\u51cf\u5c11\u4e86\u591a\u5c11\u751f\u547d&#8221;\u30025 \u70b9\u4f24\u5bb3\u6253 3 \u8840\u968f\u4ece\uff0c<code>DAMAGE += 5<\/code>\uff0c\u8fd4\u56de 5\u3002\u8fd9\u662f&#8221;\u4f24\u5bb3\u6807\u8bb0&#8221;\u5efa\u6a21\u7684\u53c8\u4e00\u4e2a\u540e\u679c\u3002<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u60c5\u51b5<\/th><th>\u662f\u5426\u5438\u8840<\/th><\/tr><\/thead><tbody><tr><td>AOE \u6253\u591a\u4e2a\u76ee\u6807<\/td><td>\u6bcf\u4e2a\u90fd\u5438\uff0c\u7d2f\u52a0<\/td><\/tr><tr><td>\u5df1\u65b9\u968f\u4ece\u53d7\u5230\u7684\u4f24\u5bb3<\/td><td>\u2713\uff08\u5982\u679c\u6765\u6e90\u6709\u5438\u8840\uff09<\/td><\/tr><tr><td>\u81ea\u5df1\u6253\u81ea\u5df1<\/td><td>\u2713<\/td><\/tr><tr><td>\u4f60\u7684\u82f1\u96c4\u6ee1\u8840<\/td><td>\u2713 \u4f46\u6ea2\u51fa\u90e8\u5206\u6d6a\u8d39\uff08\u9664\u975e\u6709\u300a\u8fc7\u91cf\u6cbb\u7597\u300b\uff09<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h4 class=\"wp-block-heading\">\u82f1\u96c4\u724c\uff08Hero Card\uff09<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5f52\u7c7b<\/strong>\uff1a\u5b9e\u4f53\u66ff\u6362\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class ReplaceHero(Action):<br>    def __init__(self, hero_card_id, armor): ...<br>    def run(self, game, ctx):<br>        p = game.player(ctx.controller)<br>        old = p.hero<br>        new = game.create_entity(self.hero_card_id, ctx.controller, zone=Zone.PLAY)<br>        # \u2605 \u7ee7\u627f\u7684\u4e1c\u897f<br>        new[Tag.DAMAGE] = old[Tag.DAMAGE]          # \u5df2\u53d7\u4f24\u5bb3\u4fdd\u7559<br>        new[Tag.ARMOR]  = old[Tag.ARMOR] + self.armor<br>        new[Tag.NUM_ATTACKS] = old[Tag.NUM_ATTACKS]<br>        new[Tag.FROZEN] = old[Tag.FROZEN]<br>        # \u2605 \u4e0d\u7ee7\u627f\u7684\uff1a\u9644\u9b54\uff08\u6e05\u9664\uff09\u3001\u6b66\u5668\uff08\u4fdd\u7559\uff01\u6b66\u5668\u662f\u72ec\u7acb\u5b9e\u4f53\uff09<br>        for ench in list(old._enchantments):<br>            remove_enchantment(game, ench)<br>        p.hero = new<br>        # \u66ff\u6362\u82f1\u96c4\u6280\u80fd<br>        p.hero_power = game.create_entity(self.hero_power_id, ctx.controller)<br>        game.event_bus.unregister_all(old)<br>        game.event_bus.register_card(new)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4e3a\u4ec0\u4e48\u82f1\u96c4\u724c\u662f\u5de5\u7a0b\u4e0a\u7684\u5927\u4e8b\uff1f<\/strong> \u56e0\u4e3a\u5b83\u8bc1\u660e\u4e86&#8221;<strong>\u82f1\u96c4\u4e5f\u662f\u5b9e\u4f53<\/strong>&#8220;\u8fd9\u4e2a\u8bbe\u8ba1\u7684\u4ef7\u503c\u3002\u5982\u679c\u82f1\u96c4\u662f\u786c\u7f16\u7801\u7684\u7279\u6b8a\u5bf9\u8c61\uff0c\u82f1\u96c4\u724c\u5c31\u5f97\u6539\u5f15\u64ce\uff1b\u56e0\u4e3a\u82f1\u96c4\u53ea\u662f\u4e00\u4e2a <code>CARDTYPE=HERO<\/code> \u7684\u5b9e\u4f53\uff0c\u66ff\u6362\u5b83\u5c31\u662f\u666e\u901a\u7684\u5b9e\u4f53\u64cd\u4f5c\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u8fd9\u4e5f\u89e3\u91ca\u4e86\u4e3a\u4ec0\u4e48\u82f1\u96c4\u80fd\u88ab buff\uff08\u300a\u7d2b\u7f57\u5170\u4f20\u6388\u8005\u300b\u7ed9\u82f1\u96c4 +\u653b\uff09\u3001\u80fd\u6709\u5632\u8bbd\uff08\u5c11\u6570\u5361\uff09\u3001\u80fd\u6709\u5723\u76fe\uff08\u300a\u4e0d\u673d\u300b\uff09\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">17.3 \u72d7\u5934\u4eba\u4e0e\u5730\u4e0b\u57ce\uff082017.12\uff09\u2014\u2014\u53ec\u52df\u3001\u6cd5\u672f\u77f3<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\">\u53ec\u52df\uff08Recruit\uff09<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u4ece\u4f60\u7684\u724c\u5e93\u4e2d\u968f\u673a\u53ec\u5524\u4e00\u4e2a\u968f\u4ece\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class Recruit(Action):<br>    def __init__(self, condition=None, count=1):<br>        self.condition, self.count = condition, count<br>    def run(self, game, ctx):<br>        deck = game.zones.get(ctx.controller, Zone.DECK)<br>        pool = [c for c in deck if c[Tag.CARDTYPE] == CardType.MINION<br>                and (not self.condition or self.condition.eval(game, c, ctx))]<br>        for _ in range(self.count):<br>            if not pool: break<br>            m = game.rng.choice(pool)<br>            pool.remove(m)<br>            game.zones.move(m, Zone.PLAY)       # \u2605 \u76f4\u63a5\u4ece\u724c\u5e93\u5230\u6218\u573a<br>            game.event_bus.register_card(m)<br>            game.emit(Event.SUMMON, entity=m)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6ce8\u610f\uff1a\u53ec\u52df\u4e0d\u89e6\u53d1\u6218\u543c<\/strong>\uff0c\u56e0\u4e3a\u4e0d\u8d70 <code>play_card<\/code> \u8def\u5f84\u3002\u8fd9\u662f&#8221;\u6218\u543c\u53ea\u5728\u4ece\u624b\u724c\u6253\u51fa\u65f6\u89e6\u53d1&#8221;\u8fd9\u6761\u89c4\u5219\u7684\u76f4\u63a5\u540e\u679c\uff0c\u4e5f\u662f\u73a9\u5bb6\u6700\u5e38\u95ee\u7684\u95ee\u9898\u4e4b\u4e00\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u53ec\u52df\u662f\u4e00\u4e2a<strong>\u5931\u8d25\u7684\u5173\u952e\u8bcd<\/strong>\u2014\u2014\u5b83\u53ea\u7528\u4e86\u4e00\u4e2a\u8d44\u6599\u7247\u5c31\u88ab\u5f03\u7528\u4e86\u3002\u539f\u56e0\uff1a\u5b83\u5728\u8bbe\u8ba1\u4e0a\u9f13\u52b1&#8221;\u62bd\u7a7a\u724c\u5e93&#8221;\u8fd9\u79cd\u53cd\u76f4\u89c9\u7684\u6784\u7b51\uff08\u724c\u5e93\u91cc\u53ea\u7559\u5927\u968f\u4ece\uff09\uff0c\u800c\u4e14\u65b9\u5dee\u592a\u5927\u3002<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">\u6cd5\u672f\u77f3\uff08Spellstone\uff09<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5f52\u7c7b<\/strong>\uff1a\u624b\u724c\u5185\u5347\u7ea7\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">LESSER_JASPER_SPELLSTONE = CardDef(<br>    card_id=\"LOOT_051\", cost=1,<br>    upgrade_condition=Cond.gained_armor(),<br>    upgrade_required=3,<br>    upgrade_to=\"LOOT_051t1\",<br>    triggers=(TriggerDef(<br>        on=Event.ARMOR_GAINED,<br>        zones=frozenset({Zone.HAND}),        # \u2605 \u624b\u724c\u4e2d\u751f\u6548<br>        condition=Cond.controller_is_owner(),<br>        action=SpellstoneProgress(),<br>    ),),<br>)<br><br>class SpellstoneProgress(Action):<br>    def run(self, game, ctx):<br>        s = ctx.source<br>        s[Tag.SPELLSTONE_PROGRESS] += 1<br>        if s[Tag.SPELLSTONE_PROGRESS] &gt;= s.defn.upgrade_required:<br>            transform(s, s.defn.upgrade_to)   # \u624b\u724c\u91cc\u53d8\u5f62\uff01<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5de5\u7a0b\u610f\u4e49<\/strong>\uff1a\u8fd9\u662f\u7b2c\u4e00\u6b21\u5728<strong>\u624b\u724c\u91cc\u505a\u53d8\u5f62<\/strong>\u3002\u5b83\u8981\u6c42\uff1a<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>\u624b\u724c\u5b9e\u4f53\u80fd\u6ce8\u518c\u89e6\u53d1\u5668\uff08<code>zones={HAND}<\/code>\uff09<\/li>\n\n\n\n<li>\u53d8\u5f62\u80fd\u5728\u975e\u6218\u573a\u533a\u57df\u5de5\u4f5c<\/li>\n\n\n\n<li>\u5ba2\u6237\u7aef\u80fd\u663e\u793a&#8221;\u624b\u724c\u91cc\u7684\u5361\u53d8\u4e86&#8221;\u7684\u52a8\u753b<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">\u540e\u6765\u7684\u300a\u4efb\u52a1\u94fe\u300b\u300a\u953b\u9020\u300b\u300a\u788e\u88c2\u300b\u5168\u90fd\u5efa\u7acb\u5728\u8fd9\u5957\u57fa\u7840\u8bbe\u65bd\u4e0a\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 18 \u7ae0 2018\uff1a\u7a81\u88ad\u6539\u53d8\u4e00\u5207<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">18.1 \u5973\u5deb\u68ee\u6797\uff082018.04\uff09\u2014\u2014\u56de\u54cd\u3001\u7a81\u88ad\u3001\u5355\u6570\/\u53cc\u6570<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\">\u7a81\u88ad\uff08Rush\uff09\u2605<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u53ef\u4ee5\u5728\u4f7f\u7528\u7684\u5f53\u56de\u5408\u653b\u51fb\u968f\u4ece\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u8fd9\u662f\u4e00\u4e2a<strong>\u53ea\u7528\u4e86\u4e00\u884c\u4ee3\u7801<\/strong>\u4f46<strong>\u6539\u53d8\u4e86\u6574\u4e2a\u6e38\u620f\u5e73\u8861<\/strong>\u7684\u5173\u952e\u8bcd\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def is_legal_attack_target(game, attacker, defender):<br>    ...<br>    if attacker.has(Tag.RUSH) and not attacker.has(Tag.CHARGE):<br>        if attacker[Tag.NUM_TURNS_IN_PLAY] == 0 and defender.is_hero:<br>            return False        # \u2190 \u5c31\u8fd9\u4e00\u884c<br>    ...<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4e3a\u4ec0\u4e48\u5b83\u5982\u6b64\u91cd\u8981\uff1f<\/strong> \u56e0\u4e3a\u5b83\u89e3\u51b3\u4e86\u7089\u77f3\u5341\u5e74\u7684\u6838\u5fc3\u5e73\u8861\u96be\u9898\uff1a<strong>\u51b2\u950b\u65e2\u662f&#8221;\u6e05\u573a\u5de5\u5177&#8221;\u53c8\u662f&#8221;\u65a9\u6740\u5de5\u5177&#8221;<\/strong>\u3002\u8bbe\u8ba1\u5e08\u60f3\u7ed9\u73a9\u5bb6&#8221;\u7acb\u5373\u6e05\u573a&#8221;\u7684\u80fd\u529b\uff0c\u4f46\u6bcf\u5f20\u51b2\u950b\u5361\u90fd\u540c\u65f6\u662f OTK \u7ec4\u4ef6\u3002\u7a81\u88ad\u628a\u8fd9\u4e24\u4e2a\u529f\u80fd\u62c6\u5f00\u4e86\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">2019 \u5e74\u5b98\u65b9\u628a\u300a\u51b2\u950b\u300b\u4ece\u5927\u91cf\u5361\u4e0a\u6362\u6210\u4e86\u300a\u7a81\u88ad\u300b\u2014\u2014<strong>\u8fd9\u662f\u6570\u636e\u9a71\u52a8\u67b6\u6784\u6700\u6709\u4ef7\u503c\u7684\u4e00\u6b21\u5151\u73b0<\/strong>\u3002\u5982\u679c\u51b2\u950b\u662f\u786c\u7f16\u7801\u7684\uff0c\u8fd9\u6b21\u624b\u672f\u8981\u6539\u51e0\u5341\u5904\u4ee3\u7801\uff1b\u56e0\u4e3a\u5b83\u662f tag\uff0c\u53ea\u9700\u8981\u6539\u5361\u724c\u6570\u636e\u3002<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">\u56de\u54cd\uff08Echo\uff09<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u4f7f\u7528\u540e\uff0c\u590d\u5236\u4e00\u5f20\u724c\u5230\u4f60\u7684\u624b\u724c\uff0c\u76f4\u5230\u672c\u56de\u5408\u7ed3\u675f\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class Keyword_Echo:<br>    @staticmethod<br>    def on_played(game, card):<br>        pid = card[Tag.CONTROLLER]<br>        copy = game.create_entity(card[Tag.CARD_ID], pid, zone=Zone.SETASIDE)<br>        copy.is_echo_copy = True            # \u56de\u5408\u7ed3\u675f\u65f6\u9500\u6bc1<br>        copy[Tag.TEMPORARY] = 1<br>        # \u2605 \u5173\u952e\uff1a\u590d\u5236\u54c1\u4fdd\u7559\u539f\u5361\u7684\u8d39\u7528\u4fee\u6b63<br>        for ench in card._enchantments:<br>            if ench.affects_cost:<br>                apply_enchantment(game, copy, ench[Tag.CARD_ID])<br>        game.zones.move(copy, Zone.HAND)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u9677\u9631<\/strong>\uff1a\u56de\u54cd\u526f\u672c\u7684\u8d39\u7528<strong>\u4f1a<\/strong>\u7ee7\u627f\u6298\u6263\u3002\u6240\u4ee5\u300a\u6076\u9b54\u5b66\u8005\u300b+\u300a\u56de\u54cd\u300b\u80fd\u65e0\u9650\u6253\u51fa\u3002\u8fd9\u88ab\u5229\u7528\u8fc7\uff0c\u540e\u6765\u901a\u8fc7&#8221;\u56de\u54cd\u5361\u4e0d\u80fd\u88ab\u67d0\u4e9b\u6298\u6263\u5f71\u54cd&#8221;\u6765\u5e73\u8861\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5de5\u7a0b\u6ce8\u610f<\/strong>\uff1a\u56de\u54cd\u526f\u672c\u5fc5\u987b\u5728 <code>end_turn<\/code> \u88ab\u6e05\u7406\uff0c\u4e14\u6e05\u7406\u65f6<strong>\u4e0d\u89e6\u53d1\u5f03\u724c\u4e8b\u4ef6<\/strong>\uff08\u5f03\u724c\u662f <code>DISCARD<\/code>\uff0c\u56de\u54cd\u6d88\u5931\u662f <code>EXPIRE<\/code>\uff09\u3002\u8fd9\u4e2a\u533a\u5206\u5f88\u91cd\u8981\uff0c\u56e0\u4e3a\u6709\u5361\u662f&#8221;\u6bcf\u5f53\u4f60\u5f03\u4e00\u5f20\u724c&#8221;\u89e6\u53d1\u7684\u3002<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">\u5355\u6570\/\u53cc\u6570\uff08Even\/Odd\uff09<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5f52\u7c7b<\/strong>\uff1a\u5f00\u5c40\u65f6\uff08Start of Game\uff09+ \u5168\u5c40\u5149\u73af\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">GENN_GREYMANE = CardDef(<br>    card_id=\"GIL_692\", cost=6, attack=6, health=5,<br>    triggers=(TriggerDef(<br>        on=Event.GAME_START,<br>        zones=frozenset({Zone.DECK}),          # \u2605 \u5728\u724c\u5e93\u91cc\u89e6\u53d1<br>        condition=Cond.deck_has_no_odd_cost_cards(),<br>        action=SetHeroPowerCost(1),<br>    ),),<br>)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5de5\u7a0b\u610f\u4e49<\/strong>\uff1a<code>Start of Game<\/code> \u89e6\u53d1\u5668\u8981\u6c42\u5f15\u64ce\u5728\u5f00\u5c40\u65f6\u626b\u63cf<strong>\u724c\u5e93\u5185\u5bb9<\/strong>\u5e76\u505a\u6761\u4ef6\u5224\u5b9a\u3002\u8fd9\u9700\u8981\u724c\u5e93\u91cc\u7684\u5361\u662f\u5b8c\u6574\u5b9e\u4f53\uff08\u53c8\u4e00\u6b21\uff09\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u800c\u4e14\uff0c\u8fd9\u4e2a\u89e6\u53d1\u5668\u53d1\u751f\u5728<strong>\u6362\u724c\u4e4b\u524d<\/strong>\uff0c\u6240\u4ee5\u5b83\u770b\u5230\u7684\u662f\u5b8c\u6574\u7684 30 \u5f20\u724c\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">18.2 \u7830\u7830\u8ba1\u5212\uff082018.08\uff09\u2014\u2014\u78c1\u529b<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\">\u78c1\u529b\uff08Magnetic\uff09\u2605<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u53ef\u4ee5\u5c06\u5176\u878d\u5408\u5230\u4f60\u7684\u673a\u68b0\u968f\u4ece\u7684\u5de6\u4fa7\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u78c1\u529b\u662f<strong>\u6700\u590d\u6742\u7684\u5173\u952e\u8bcd\u4e4b\u4e00<\/strong>\uff0c\u56e0\u4e3a\u5b83\u6d89\u53ca&#8221;\u4e24\u4e2a\u5b9e\u4f53\u5408\u5e76\u6210\u4e00\u4e2a&#8221;\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class Magnetize(Action):<br>    \"\"\"\u628a source\uff08\u78c1\u529b\u968f\u4ece\uff09\u878d\u5408\u5230 target\uff08\u673a\u68b0\uff09\u8eab\u4e0a\"\"\"<br>    def run(self, game, ctx):<br>        src, tgt = ctx.source, ctx.target<br>        # 1. \u5c5e\u6027\u53e0\u52a0\uff08\u4f5c\u4e3a\u4e00\u4e2a\u9644\u9b54\uff09<br>        ench = game.create_entity(src[Tag.CARD_ID] + \"e\", ctx.controller,<br>                                  zone=Zone.SETASIDE)<br>        ench.enchant_tags = {<br>            Tag.ATK:    src.defn.attack,<br>            Tag.HEALTH: src.defn.health,<br>        }<br>        apply_enchantment_entity(game, tgt, ench)<br><br>        # 2. \u5173\u952e\u8bcd\u8f6c\u79fb<br>        for kw in (Tag.TAUNT, Tag.DIVINE_SHIELD, Tag.RUSH, Tag.WINDFURY,<br>                   Tag.POISONOUS, Tag.LIFESTEAL):<br>            if src.has(kw):<br>                tgt[kw] = 1<br><br>        # 3. \u4ea1\u8bed\u4e0e\u89e6\u53d1\u5668\u8f6c\u79fb\uff08\u2605 \u6700\u96be\u7684\u90e8\u5206\uff09<br>        if src.defn.deathrattle:<br>            game.event_bus.register(tgt, TriggerDef(<br>                on=Event.DEATH, source_only=True,<br>                action=src.defn.deathrattle,<br>                owner_enchantment=ench,      # \u7ed1\u5b9a\u5230\u9644\u9b54\uff0c\u6c89\u9ed8\u65f6\u4e00\u8d77\u79fb\u9664<br>            ))<br>        for td in src.defn.triggers:<br>            game.event_bus.register(tgt, replace(td, owner_enchantment=ench))<br><br>        # 4. \u8bb0\u5f55\u878d\u5408\u5386\u53f2\uff08\u2605 \u6c89\u9ed8\/\u6b7b\u4ea1\u65f6\u8981\u7528\uff09<br>        tgt.magnetic_stack.append(src[Tag.CARD_ID])<br><br>        # 5. \u6e90\u5b9e\u4f53\u6d88\u5931\uff08\u4e0d\u8fdb\u5893\u5730\uff0c\u4e0d\u89e6\u53d1\u4ea1\u8bed\uff09<br>        game.remove_entity(src)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u78c1\u529b\u7684\u56db\u4e2a\u9677\u9631<\/strong>\uff1a<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>\u6c89\u9ed8\u878d\u5408\u4f53 \u2192 \u6240\u6709\u78c1\u529b\u90e8\u4ef6\u7684\u6548\u679c\u4e00\u8d77\u6d88\u5931<\/strong>\uff0c\u968f\u4ece\u56de\u5230\u57fa\u7840\u5f62\u6001\u3002\u8fd9\u662f\u56e0\u4e3a\u6240\u6709\u78c1\u529b\u6548\u679c\u90fd\u6302\u5728\u9644\u9b54\u4e0a\u3002<\/li>\n\n\n\n<li><strong>\u878d\u5408\u4f53\u6b7b\u4ea1 \u2192 \u6240\u6709\u78c1\u529b\u90e8\u4ef6\u7684\u4ea1\u8bed\u90fd\u89e6\u53d1<\/strong>\uff08\u6309\u878d\u5408\u987a\u5e8f\uff09\u3002<\/li>\n\n\n\n<li><strong>\u78c1\u529b\u968f\u4ece\u53ef\u4ee5\u6b63\u5e38\u6253\u51fa<\/strong>\uff08\u4e0d\u878d\u5408\uff09\uff0c\u6b64\u65f6\u5b83\u5c31\u662f\u4e2a\u666e\u901a\u968f\u4ece\u3002<\/li>\n\n\n\n<li><strong>\u4e0d\u80fd\u878d\u5408\u5230\u975e\u673a\u68b0\u4e0a<\/strong>\uff0c\u4e5f\u4e0d\u80fd\u878d\u5408\u5230&#8221;\u5df2\u7ecf\u6ee1\u4e86&#8221;\u7684\u968f\u4ece\u4e0a\uff08\u65e0\u6b64\u9650\u5236\uff0c\u53ef\u4ee5\u65e0\u9650\u53e0\uff09\u3002<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5de5\u7a0b\u8bc4\u4ef7<\/strong>\uff1a\u78c1\u529b\u662f&#8221;\u5173\u952e\u8bcd\u590d\u6742\u5ea6&#8221;\u7684\u4e00\u4e2a\u4e0a\u9650\u6848\u4f8b\u3002\u5b83\u9700\u8981 <code>Action<\/code> \u80fd\u4fee\u6539\u53e6\u4e00\u4e2a\u5b9e\u4f53\u7684<strong>\u89e6\u53d1\u5668\u6ce8\u518c\u8868<\/strong>\uff0c\u8fd9\u6253\u7834\u4e86&#8221;\u5b9e\u4f53\u7684\u884c\u4e3a\u7531\u5b83\u7684 CardDef \u51b3\u5b9a&#8221;\u8fd9\u6761\u4e0d\u53d8\u5f0f\u3002\u771f\u5b9e\u7089\u77f3\u7684\u5b9e\u73b0\uff08<strong>\u63a8\u65ad<\/strong>\uff09\u786e\u5b9e\u662f\u901a\u8fc7\u9644\u9b54\u643a\u5e26\u89e6\u53d1\u5668\u6765\u505a\u7684\uff0c\u8fd9\u4e5f\u662f\u4e3a\u4ec0\u4e48\u9644\u9b54\u5fc5\u987b\u662f\u5b9e\u4f53\uff0813.1\uff09\u3002<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">\u5176\u4ed6<\/h4>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Omega<\/strong>\uff1a<code>If(Cond.mana_crystals_at_least(10), bonus)<\/code>\u2014\u2014\u7eaf\u6761\u4ef6\u5224\u65ad\uff0c\u65e0\u65b0\u57fa\u7840\u8bbe\u65bd<\/li>\n\n\n\n<li><strong>Project<\/strong>\uff1a\u53cc\u65b9\u90fd\u53d7\u5f71\u54cd\u7684\u5bf9\u79f0\u6548\u679c\u2014\u2014\u53ea\u662f Selector \u7684\u7528\u6cd5<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">18.3 \u62c9\u65af\u5854\u54c8\u7684\u5927\u4e71\u6597\uff082018.12\uff09\u2014\u2014\u8d85\u6740<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\">\u8d85\u6740\uff08Overkill\uff09<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u5728\u4f60\u7684\u56de\u5408\u4e2d\u9020\u6210\u8d85\u51fa\u51fb\u6740\u6240\u9700\u7684\u4f24\u5bb3\u65f6\u89e6\u53d1\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class Keyword_Overkill:<br>    @staticmethod<br>    def make(action):<br>        return TriggerDef(<br>            on=Event.DAMAGE,<br>            source_only=True,               # \u6211\u9020\u6210\u7684\u4f24\u5bb3<br>            condition=Cond.my_turn() &amp; Cond.overkill(),<br>            action=action,<br>        )<br><br>class OverkillCond(Condition):<br>    def eval(self, game, e, ctx):<br>        # ctx.amount \u662f\u9020\u6210\u7684\u4f24\u5bb3\uff0cctx.target_health_before \u662f\u76ee\u6807\u4f24\u5bb3\u524d\u7684\u751f\u547d<br>        return ctx.amount &gt; ctx.target_health_before<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u9677\u9631<\/strong>\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>\u53ea\u5728\u4f60\u7684\u56de\u5408\u89e6\u53d1<\/strong>\u3002\u9632\u5b88\u65f6\u53cd\u51fb\u9020\u6210\u8d85\u6740\u4e0d\u89e6\u53d1\u3002<\/li>\n\n\n\n<li>\u9700\u8981\u4e8b\u4ef6\u643a\u5e26 <code>target_health_before<\/code>\uff0c\u6240\u4ee5 <code>DAMAGE<\/code> \u4e8b\u4ef6\u7684\u4e0a\u4e0b\u6587\u5fc5\u987b\u8bb0\u5f55&#8221;\u4f24\u5bb3\u524d\u7684\u751f\u547d\u503c&#8221;\u3002<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-preformatted\"># damage() \u91cc<br>health_before = target.current_health<br>target[Tag.DAMAGE] += amount<br>self.emit(Event.DAMAGE, target=target, source=source, amount=amount,<br>          target_health_before=health_before,      # \u2605 \u4e3a\u8d85\u6740\/\u8363\u8a89\u51fb\u6740\u51c6\u5907<br>          fatal=target.current_health &lt;= 0)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8fd9\u4e2a\u5b57\u6bb5\u540e\u6765\u88ab\u4e09\u4e2a\u5173\u952e\u8bcd\u590d\u7528<\/strong>\uff1a\u8d85\u6740\uff08<code>amount &gt; before<\/code>\uff09\u3001\u8363\u8a89\u51fb\u6740\uff08<code>amount == before<\/code>\uff09\u3001\u72c2\u4e71\uff08<code>amount &gt; 0 and after &gt; 0<\/code>\uff09\u3002<strong>\u4e00\u6b21\u591a\u5b58\u4e00\u4e2a\u5b57\u6bb5\uff0c\u7701\u4e86\u4e09\u6b21\u6539\u52a8\u3002<\/strong> \u8fd9\u662f\u4e8b\u4ef6\u4e0a\u4e0b\u6587\u8bbe\u8ba1\u7684\u7ecf\u9a8c\uff1a\u5b81\u53ef\u591a\u5e26\u4fe1\u606f\uff0c\u4e0d\u8981\u8ba9\u8ba2\u9605\u8005\u53bb\u53cd\u67e5\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 19 \u7ae0 2019\uff1a\u4efb\u52a1\u7684\u56de\u5f52\u4e0e\u590d\u751f<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">19.1 \u6697\u5f71\u5d1b\u8d77\uff082019.04\uff09\u2014\u2014\u53cc\u751f\u6cd5\u672f\u3001\u8ddf\u73ed\u3001\u9634\u8c0b<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\">\u53cc\u751f\u6cd5\u672f\uff08Twinspell\uff09<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u4f7f\u7528\u540e\uff0c\u5c06\u4e00\u5f20\u4e0d\u5e26\u53cc\u751f\u6cd5\u672f\u7684\u590d\u5236\u653e\u5165\u4f60\u7684\u624b\u724c\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class Keyword_Twinspell:<br>    @staticmethod<br>    def on_cast(game, card):<br>        twin_id = card.defn.twinspell_copy_id   # \u6307\u5411\u53bb\u6389\u5173\u952e\u8bcd\u7684\u7248\u672c<br>        game.add_to_hand(card[Tag.CONTROLLER], twin_id)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u4e0e\u300a\u56de\u54cd\u300b\u7684\u533a\u522b\uff1a<strong>\u53cc\u751f\u526f\u672c\u662f\u6c38\u4e45\u7684<\/strong>\uff0c\u4e0d\u5728\u56de\u5408\u7ed3\u675f\u6d88\u5931\uff0c\u800c\u4e14\u53ea\u590d\u5236\u4e00\u6b21\u3002\u5b9e\u73b0\u4e0a\u66f4\u7b80\u5355\uff08\u4e0d\u9700\u8981 TEMPORARY \u6807\u8bb0\uff09\u3002<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">\u8ddf\u73ed\uff08Lackey\uff09<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">\u4e0d\u662f\u5173\u952e\u8bcd\uff0c\u662f\u4e00\u4e2a<strong>\u884d\u751f\u5361\u6c60<\/strong>\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">LACKEYS = [\"DAL_613\", \"DAL_614\", \"DAL_615\", \"DAL_739\", \"DAL_740\"]<br><br>class AddLackey(Action):<br>    def run(self, game, ctx):<br>        game.add_to_hand(ctx.controller, game.rng.choice(LACKEYS))<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u8ddf\u73ed\u7684\u8bbe\u8ba1\u4ef7\u503c\uff1a\u5b83\u662f<strong>\u4f4e\u65b9\u5dee\u7684\u968f\u673a<\/strong>\u3002\u4e94\u4e2a 1 \u8d39\u968f\u4ece\u90fd\u662f\u53ef\u63a5\u53d7\u7684\uff0c\u6240\u4ee5&#8221;\u968f\u673a\u8ddf\u73ed&#8221;\u4e0d\u4f1a\u6709\u5929\u5802\/\u5730\u72f1\u7684\u843d\u5dee\u3002\u8fd9\u662f\u5bf9 GvG \u65f6\u4ee3\u9ad8\u65b9\u5dee\u968f\u673a\u7684\u4e00\u6b21\u7ea0\u6b63\u3002<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">\u9634\u8c0b\uff08Scheme\uff09<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5f52\u7c7b<\/strong>\uff1a\u624b\u724c\u5185\u6210\u957f\uff08\u6cd5\u672f\u77f3\u7684\u76f4\u7cfb\u540e\u4ee3\uff09\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class SchemeGrowth(Action):<br>    \"\"\"\u6bcf\u4e2a\u56de\u5408\u7ed3\u675f\u65f6\uff0c\u9634\u8c0b\u7684\u6548\u679c\u503c +1\"\"\"<br>    def run(self, game, ctx):<br>        ctx.source[Tag.SCHEME_LEVEL] += 1<br><br>DR_BOOMS_SCHEME = CardDef(<br>    card_id=\"DAL_007\", cost=5,<br>    triggers=(TriggerDef(<br>        on=Event.TURN_END, zones=frozenset({Zone.HAND}),<br>        condition=Cond.controller_is_owner(),<br>        action=SchemeGrowth(),<br>    ),),<br>    spell=GainArmor(TagOf(Sel.SELF, Tag.SCHEME_LEVEL)),<br>)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5de5\u7a0b\u4ef7\u503c<\/strong>\uff1a\u5b83\u628a&#8221;\u624b\u724c\u91cc\u7684\u5b9e\u4f53\u6709\u53ef\u53d8\u72b6\u6001&#8221;\u8fd9\u4ef6\u4e8b\u5e38\u6001\u5316\u4e86\u3002\u4e4b\u540e\u7684\u4efb\u52a1\u94fe\u3001\u65c5\u5ba2\u3001\u661f\u8230\u90fd\u4f9d\u8d56\u8fd9\u4e2a\u80fd\u529b\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">19.2 \u5965\u4e39\u59c6\u5947\u5175\uff082019.08\uff09\u2014\u2014\u590d\u751f<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\">\u590d\u751f\uff08Reborn\uff09\u2605<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u8be5\u968f\u4ece\u7b2c\u4e00\u6b21\u6b7b\u4ea1\u65f6\uff0c\u5c06\u4ee5 1 \u70b9\u751f\u547d\u503c\u91cd\u65b0\u53ec\u5524\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def summon_reborn(game, dead: Entity):<br>    snap = dead.death_snapshot<br>    new = game.create_entity(snap.card_id, snap.controller, zone=Zone.SETASIDE)<br>    new[Tag.HEALTH] = 1               # \u2605 \u751f\u547d\u503c = 1\uff0c\u4e0d\u662f\u57fa\u7840\u503c<br>    new[Tag.ATK] = CARD_DB[snap.card_id].attack   # \u653b\u51fb\u529b\u56de\u5230\u57fa\u7840\u503c<br>    new[Tag.REBORN] = 0               # \u2605 \u53ea\u80fd\u590d\u751f\u4e00\u6b21<br>    game.zones.move(new, Zone.PLAY, snap.position)<br>    game.event_bus.register_card(new)<br>    game.emit(Event.SUMMON, entity=new)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u9677\u9631\u6e05\u5355<\/strong>\uff1a<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>\u590d\u751f\u4f53\u662f<strong>\u65b0\u5b9e\u4f53<\/strong>\uff08\u65b0 ENTITY_ID\uff09\uff0c\u6240\u4ee5&#8221;\u8fd9\u4e2a\u968f\u4ece&#8221;\u7c7b\u7684\u5f15\u7528\u4f1a\u65ad<\/li>\n\n\n\n<li>\u590d\u751f\u4f53<strong>\u4e0d\u7ee7\u627f\u9644\u9b54<\/strong>\uff08\u56de\u5230\u57fa\u7840\u5361 + 1 \u8840\uff09<\/li>\n\n\n\n<li>\u590d\u751f<strong>\u5728\u4ea1\u8bed\u4e4b\u540e<\/strong>\uff08\u7b2c 7 \u7ae0\u8bb2\u8fc7\uff09<\/li>\n\n\n\n<li>\u590d\u751f\u4f53\u7684 <code>REBORN<\/code> \u88ab\u6e05\u9664\uff0c\u4e0d\u80fd\u4e8c\u6b21\u590d\u751f<\/li>\n\n\n\n<li>\u6c89\u9ed8\u4f1a\u79fb\u9664\u590d\u751f\uff08<code>REBORN<\/code> \u5728 <code>SILENCEABLE_TAGS<\/code> \u91cc\uff09<\/li>\n\n\n\n<li><strong>\u573a\u6ee1\u65f6\u590d\u751f\u5931\u8d25<\/strong>\uff08\u548c\u666e\u901a\u53ec\u5524\u4e00\u6837\uff09<\/li>\n\n\n\n<li>\u590d\u751f\u4f53<strong>\u4f1a<\/strong>\u89e6\u53d1&#8221;\u968f\u4ece\u53ec\u5524\u65f6&#8221;\u6548\u679c<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4e3a\u4ec0\u4e48\u590d\u751f\u7684\u5b9e\u73b0\u8981\u653e\u5728\u6b7b\u4ea1\u7ed3\u7b97\u7684\u7b2c 6 \u6b65\uff08\u4ea1\u8bed\u4e4b\u540e\uff09\uff1f<\/strong> \u6211\u5728 7.3 \u8bf4\u8fc7\u539f\u56e0\uff0c\u8fd9\u91cc\u8865\u4e00\u4e2a\u53cd\u4f8b\uff1a\u5982\u679c\u590d\u751f\u5728\u4ea1\u8bed\u4e4b\u524d\uff0c\u90a3\u4e48&#8221;\u590d\u751f + \u4ea1\u8bed\u9020\u6210 AOE&#8221;\u7684\u968f\u4ece\u4f1a\u81ea\u5df1\u6253\u6b7b\u81ea\u5df1\u7684\u590d\u751f\u4f53\u3002\u5b9e\u6d4b\u7089\u77f3\u4e0d\u4f1a\uff0c\u6240\u4ee5\u987a\u5e8f\u786e\u5b9a\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">19.3 \u5de8\u9f99\u964d\u4e34\uff082019.12\uff09\u2014\u2014\u63a2\u5e95\u3001\u652f\u7ebf\u4efb\u52a1<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\">\u63a2\u5e95 \/ \u5524\u9192\uff08Invoke\uff09<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u89e6\u53d1\u8fe6\u62c9\u514b\u9686\u7684\u82f1\u96c4\u6280\u80fd\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class Invoke(Action):<br>    def run(self, game, ctx):<br>        p = game.player(ctx.controller)<br>        p.invoke_count += 1<br>        if p.galakrond_in_play_or_hand():<br>            game.trigger_hero_power(ctx.controller, forced=True)<br>        # \u6bcf 2 \u6b21\u63a2\u5e95\u5347\u7ea7\u8fe6\u62c9\u514b\u9686<br>        if p.invoke_count in (2, 4):<br>            game.upgrade_galakrond(ctx.controller)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5de5\u7a0b\u6a21\u5f0f\uff1a\u8de8\u533a\u57df\u7684\u8ba1\u6570\u5668 + \u9636\u6bb5\u6027\u5347\u7ea7\u3002<\/strong> \u8fd9\u4e2a\u6a21\u5f0f\u5728 2026 \u5e74\u7684\u300a\u5148\u9a71\u300b\uff08Herald\uff09\u91cc\u88ab\u5b8c\u6574\u590d\u7528\uff08\u6bcf 2 \u6b21 Herald \u5347\u7ea7\u4e00\u6b21\u5de8\u578b\uff09\u3002<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">\u652f\u7ebf\u4efb\u52a1\uff08Sidequest\uff09<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">\u5c0f\u578b\u4efb\u52a1\uff0c\u673a\u5236\u5b8c\u5168\u590d\u7528\u300a\u4efb\u52a1\u300b\uff0c\u53ea\u662f\u6570\u503c\u66f4\u5c0f\u3001\u5956\u52b1\u66f4\u5f31\u3002<strong>\u96f6\u65b0\u589e\u4ee3\u7801<\/strong>\u2014\u2014\u8fd9\u662f\u5173\u952e\u8bcd\u590d\u7528\u7684\u6700\u4f73\u6848\u4f8b\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 20 \u7ae0 2020\uff1a\u65b0\u804c\u4e1a\u4e0e\u6cd5\u672f\u5b66\u6d3e\u524d\u591c<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">20.1 \u5916\u57df\u7684\u7070\u70ec\uff082020.04\uff09\u2014\u2014\u6d41\u653e\u3001\u81f3\u5c0a\u3001\u6076\u9b54\u730e\u624b<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\">\u6d41\u653e\uff08Outcast\uff09\u2605<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u5982\u679c\u8fd9\u662f\u4f60\u624b\u724c\u4e2d\u6700\u5de6\u4fa7\u6216\u6700\u53f3\u4fa7\u7684\u724c\uff0c\u89e6\u53d1\u989d\u5916\u6548\u679c\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u8fd9\u662f\u7089\u77f3<strong>\u7b2c\u4e00\u4e2a\u771f\u6b63\u4f9d\u8d56\u624b\u724c\u4f4d\u7f6e<\/strong>\u7684\u5173\u952e\u8bcd\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class OutcastCond(Condition):<br>    def eval(self, game, e, ctx):<br>        hand = game.zones.get(e[Tag.CONTROLLER], Zone.HAND)<br>        if not hand:<br>            return False<br>        return e is hand[0] or e is hand[-1]<br><br># \u7528\u6cd5\uff1a\u5305\u5728 If \u91cc<br>SKULL_OF_GULDAN = CardDef(<br>    card_id=\"BT_601\", cost=5, card_type=\"SPELL\",<br>    spell=Seq(<br>        Draw(3),<br>        If(OutcastCond(), ReduceCostOfDrawn(3)),<br>    ),<br>)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5de5\u7a0b\u96be\u70b9<\/strong>\uff1a<code>ZONE_POSITION<\/code> \u5728\u624b\u724c\u91cc\u5fc5\u987b<strong>\u4e25\u683c\u7ef4\u62a4<\/strong>\u3002\u4e4b\u524d\u624b\u724c\u7684\u4f4d\u7f6e\u53ea\u662f\u663e\u793a\u987a\u5e8f\uff0c\u73b0\u5728\u5b83\u662f\u89c4\u5219\u7684\u4e00\u90e8\u5206\u3002\u8fd9\u610f\u5473\u7740\uff1a<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>\u62bd\u724c\u63d2\u5165\u7684\u4f4d\u7f6e\u5fc5\u987b\u786e\u5b9a\uff08<strong>\u603b\u662f\u6700\u53f3<\/strong>\uff09<\/li>\n\n\n\n<li>\u751f\u6210\u7684\u724c\u63d2\u5165\u4f4d\u7f6e\u5fc5\u987b\u786e\u5b9a\uff08<strong>\u603b\u662f\u6700\u53f3<\/strong>\uff09<\/li>\n\n\n\n<li>\u6253\u51fa\u4e00\u5f20\u724c\u540e\uff0c\u53f3\u4fa7\u6240\u6709\u724c\u5de6\u79fb<\/li>\n\n\n\n<li><strong>\u5ba2\u6237\u7aef\u62d6\u52a8\u6392\u5e8f\u4e0d\u80fd\u5f71\u54cd\u670d\u52a1\u5668\u72b6\u6001<\/strong>\uff08\u7089\u77f3\u4e0d\u5141\u8bb8\u73a9\u5bb6\u6392\u624b\u724c\uff0c\u6b63\u662f\u56e0\u4e3a\u8fd9\u4e2a\uff09<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">\u7b2c 4 \u70b9\u5f88\u5173\u952e\u3002\u5982\u679c\u73a9\u5bb6\u80fd\u81ea\u7531\u6392\u624b\u724c\uff0c\u6d41\u653e\u5c31\u6ca1\u6709\u610f\u4e49\u4e86\u3002\u6240\u4ee5\u7089\u77f3<strong>\u6545\u610f\u4e0d\u63d0\u4f9b\u624b\u724c\u6392\u5e8f\u529f\u80fd<\/strong>\u2014\u2014\u8fd9\u662f\u4e00\u4e2a\u7531\u673a\u5236\u5012\u903c\u7684 UX \u51b3\u7b56\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\uff082026 \u5e74\u7684\u300a\u788e\u88c2\u300b\u628a\u8fd9\u4e2a\u7ea6\u675f\u7528\u5230\u4e86\u6781\u81f4\uff1a\u788e\u7247\u88ab\u5f3a\u5236\u653e\u5728\u624b\u724c\u4e24\u7aef\u3002\uff09<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">\u81f3\u5c0a\uff08Prime\uff09<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u4ea1\u8bed\uff1a\u5c06\u4e00\u5f20&#8221;\u81f3\u5c0a\u7248&#8221;\u6d17\u5165\u4f60\u7684\u724c\u5e93\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u7eaf\u7ec4\u5408\uff1a<code>deathrattle = ShuffleIntoDeck(\"BT_XXXt\")<\/code>\u3002\u96f6\u65b0\u57fa\u7840\u8bbe\u65bd\u3002<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">\u6076\u9b54\u730e\u624b\u804c\u4e1a<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5de5\u7a0b\u610f\u4e49<\/strong>\uff1a\u7b2c\u5341\u4e2a\u804c\u4e1a\u3002\u5b83\u9a8c\u8bc1\u4e86&#8221;\u804c\u4e1a\u662f\u6570\u636e&#8221;\u8fd9\u4e2a\u8bbe\u8ba1\u2014\u2014\u65b0\u589e\u4e00\u4e2a\u804c\u4e1a\u53ea\u9700\u8981\uff1a<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>\u4e00\u4e2a <code>CLASS<\/code> \u679a\u4e3e\u503c<\/li>\n\n\n\n<li>\u4e00\u4e2a\u82f1\u96c4\u5b9e\u4f53\u5b9a\u4e49<\/li>\n\n\n\n<li>\u4e00\u4e2a\u82f1\u96c4\u6280\u80fd\u5b9a\u4e49<\/li>\n\n\n\n<li>\u5361\u724c\u7684 <code>klass<\/code> \u5b57\u6bb5<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6ca1\u6709\u4efb\u4f55\u5f15\u64ce\u4ee3\u7801\u6539\u52a8<\/strong>\u3002\u76f8\u6bd4\u4e4b\u4e0b\uff0c2022 \u5e74\u7684\u6b7b\u4ea1\u9a91\u58eb\u9700\u8981\u5f15\u64ce\u6539\u52a8\uff08\u7b26\u6587\u7cfb\u7edf + \u5c38\u5757\uff09\uff0c\u56e0\u4e3a\u5b83\u5f15\u5165\u4e86\u65b0\u7684\u8d44\u6e90\u7c7b\u578b\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">20.2 \u901a\u7075\u5b66\u56ed\uff082020.08\uff09\u2014\u2014\u6cd5\u672f\u8ff8\u53d1\u3001\u53cc\u804c\u4e1a<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\">\u6cd5\u672f\u8ff8\u53d1\uff08Spellburst\uff09\u2605<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u5728\u4f60\u4f7f\u7528\u4e00\u5f20\u6cd5\u672f\u540e\uff0c\u89e6\u53d1\u4e00\u6b21\uff08\u4ec5\u4e00\u6b21\uff09\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class Keyword_Spellburst:<br>    @staticmethod<br>    def make(action):<br>        return TriggerDef(<br>            on=Event.AFTER_SPELL,<br>            condition=Cond.controller_is_owner(),<br>            action=Seq(action, ConsumeSpellburst()),<br>            once=True,                        # \u2605 \u4e00\u6b21\u6027<br>        )<br><br>class ConsumeSpellburst(Action):<br>    def run(self, game, ctx):<br>        ctx.source[Tag.SPELLBURST_USED] = 1<br>        game.event_bus.unregister_trigger(ctx.source, Event.AFTER_SPELL)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u9677\u9631<\/strong>\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u6cd5\u672f\u8ff8\u53d1\u662f&#8221;<strong>\u4f7f\u7528\u6cd5\u672f\u540e<\/strong>&#8220;\uff0c\u6240\u4ee5\u6cd5\u672f\u6548\u679c\u5148\u7ed3\u7b97\uff0c\u518d\u89e6\u53d1\u8ff8\u53d1<\/li>\n\n\n\n<li>\u4e00\u65e6\u89e6\u53d1\u5c31\u6c38\u4e45\u6d88\u8017\uff0c<strong>\u6c89\u9ed8\u4e0d\u80fd\u91cd\u7f6e<\/strong>\uff08\u56e0\u4e3a\u89e6\u53d1\u5668\u5df2\u6ce8\u9500\uff09<\/li>\n\n\n\n<li>\u4f46\u300a\u53d8\u5f62\u300b\u53ef\u4ee5&#8221;\u91cd\u7f6e&#8221;\uff08\u56e0\u4e3a\u53d8\u56de\u6765\u662f\u65b0\u7684\u5b9a\u4e49\uff09<\/li>\n\n\n\n<li>\u300a\u94dc\u955c\u300b\u7c7b\u590d\u5236\u7684\u968f\u4ece\u6709\u81ea\u5df1\u7684\u4e00\u6b21\u673a\u4f1a<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>once=True<\/code> \u7684\u5b9e\u73b0\u7ec6\u8282<\/strong>\uff1a\u6ce8\u610f\u4e0a\u9762\u540c\u65f6\u7528\u4e86 <code>once=True<\/code> \u548c\u663e\u5f0f\u7684 <code>unregister_trigger<\/code>\u3002\u4e3a\u4ec0\u4e48\u4e24\u4e2a\u90fd\u8981\uff1f\u56e0\u4e3a <code>once<\/code> \u53ea\u4fdd\u8bc1&#8221;\u4e0d\u4f1a\u6267\u884c\u7b2c\u4e8c\u6b21&#8221;\uff0c\u4f46\u89e6\u53d1\u5668\u8fd8\u5728\u8ba2\u9605\u5217\u8868\u91cc\uff0c\u4f1a\u6d88\u8017 CPU\uff1b\u663e\u5f0f\u6ce8\u9500\u662f\u6027\u80fd\u4f18\u5316\u3002\u771f\u5b9e\u5f15\u64ce\u91cc\u8fd9\u7c7b&#8221;\u6ce8\u9500&#8221;\u5f88\u91cd\u8981\u2014\u2014\u4e00\u5c40\u6e38\u620f\u91cc\u4f1a\u6709\u6570\u767e\u4e2a\u89e6\u53d1\u5668\u6ce8\u518c\/\u6ce8\u9500\u3002<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">\u53cc\u804c\u4e1a\u5361<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">\u7eaf\u6570\u636e\uff1a<code>klass<\/code> \u5b57\u6bb5\u53d8\u6210 <code>frozenset({\"MAGE\", \"ROGUE\"})<\/code>\u3002\u6784\u7b51\u89c4\u5219\u53d8\u4e86\uff0c\u5f15\u64ce\u4e0d\u53d8\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">20.3 \u75af\u72c2\u7684\u8fbe\u62c9\u7136\uff082020.11\uff09\u2014\u2014\u8150\u8680<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\">\u8150\u8680\uff08Corrupt\uff09<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u5728\u4f60\u4f7f\u7528\u4e00\u5f20\u8d39\u7528\u66f4\u9ad8\u7684\u724c\u540e\uff0c\u8be5\u724c\u5728\u4f60\u624b\u4e2d\u5347\u7ea7\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class Keyword_Corrupt:<br>    @staticmethod<br>    def make(upgraded_id):<br>        return TriggerDef(<br>            on=Event.PLAY_CARD,<br>            zones=frozenset({Zone.HAND}),<br>            condition=Cond.controller_is_owner() &amp; Cond.played_cost_higher_than_self(),<br>            action=TransformInHand(upgraded_id),<br>            once=True,<br>        )<br><br>class PlayedCostHigherThanSelf(Condition):<br>    def eval(self, game, e, ctx):<br>        # \u2605 \u6bd4\u8f83\u7684\u662f\"\u539f\u59cb\u8d39\u7528\"\u8fd8\u662f\"\u5b9e\u9645\u652f\u4ed8\u7684\u8d39\u7528\"\uff1f<br>        return ctx.paid_cost &gt; game.compute_cost(e)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8fd9\u4e2a&#8221;\u539f\u59cb\u8d39\u7528 vs \u5b9e\u9645\u8d39\u7528&#8221;\u7684\u95ee\u9898\u5f88\u771f\u5b9e\u3002<\/strong> \u7089\u77f3\u7684\u88c1\u5b9a\u662f\uff1a\u6bd4\u8f83\u7684\u662f<strong>\u6253\u51fa\u90a3\u5f20\u724c\u65f6\u5b9e\u9645\u652f\u4ed8\u7684\u8d39\u7528<\/strong>\uff08\u542b\u6298\u6263\uff09\u3002\u6240\u4ee5\u300a\u6076\u9b54\u5b66\u8005\u300b\u628a\u4e00\u5f20 10 \u8d39\u5361\u6298\u6263\u5230 2 \u8d39\u6253\u51fa\uff0c\u4e0d\u4f1a\u8150\u8680\u4e00\u5f20 5 \u8d39\u5361\u3002\u8fd9\u4e2a\u88c1\u5b9a\u88ab\u73a9\u5bb6\u6295\u8bc9\u8fc7\uff0c\u4f46\u4ece\u5b9e\u73b0\u89d2\u5ea6\u5b83\u662f\u81ea\u7136\u7684\uff08<code>ctx.paid_cost<\/code> \u5c31\u662f\u5b9e\u9645\u6263\u7684\u6570\uff09\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8150\u8680\u7684\u5de5\u7a0b\u6a21\u5f0f<\/strong>\uff1a\u624b\u724c\u5185\u53d8\u5f62\uff08\u6cd5\u672f\u77f3\u7684\u540e\u4ee3\uff09\uff0c\u52a0\u4e00\u6b21\u6027\u89e6\u53d1\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 21 \u7ae0 2021\uff1a\u72c2\u4e71\u3001\u4efb\u52a1\u94fe\u3001\u53ef\u4ea4\u6613<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">21.1 \u8d2b\u7620\u4e4b\u5730\uff082021.03\uff09\u2014\u2014\u72c2\u4e71\u3001\u9636\u7ea7\u6cd5\u672f\u3001\u6cd5\u672f\u5b66\u6d3e<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\">\u72c2\u4e71\uff08Frenzy\uff09<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u8be5\u968f\u4ece\u7b2c\u4e00\u6b21\u53d7\u5230\u4f24\u5bb3\u4f46\u672a\u6b7b\u4ea1\u65f6\u89e6\u53d1\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class Keyword_Frenzy:<br>    @staticmethod<br>    def make(action):<br>        return TriggerDef(<br>            on=Event.DAMAGE,<br>            source_only=False,<br>            condition=Cond.target_is_self() &amp; Cond.survived(),<br>            action=action,<br>            once=True,<br>        )<br><br>class Survived(Condition):<br>    def eval(self, game, e, ctx):<br>        return ctx.amount &gt; 0 and not ctx.fatal<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u5b83\u662f<strong>\u6fc0\u6012\uff08Enrage\uff09\u7684\u6b63\u786e\u66ff\u4ee3\u54c1<\/strong>\uff1a<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th><\/th><th>\u6fc0\u6012\uff082014-2018\uff09<\/th><th>\u72c2\u4e71\uff082021-\uff09<\/th><\/tr><\/thead><tbody><tr><td>\u8303\u5f0f<\/td><td>\u5149\u73af\uff08\u6301\u7eed\u6761\u4ef6\uff09<\/td><td>\u89e6\u53d1\uff08\u4e00\u6b21\u6027\u4e8b\u4ef6\uff09<\/td><\/tr><tr><td>\u6cbb\u7597\u540e<\/td><td>\u6548\u679c\u6d88\u5931<\/td><td>\u6548\u679c\u4fdd\u7559<\/td><\/tr><tr><td>\u91cd\u7b97\u6210\u672c<\/td><td>\u6bcf\u6b21\u72b6\u6001\u53d8\u5316<\/td><td>\u96f6<\/td><\/tr><tr><td>\u6c89\u9ed8<\/td><td>\u79fb\u9664\u5149\u73af<\/td><td>\u79fb\u9664\u89e6\u53d1\u5668<\/td><\/tr><tr><td>\u53ef\u9884\u6d4b\u6027<\/td><td>\u5dee\uff08\u73a9\u5bb6\u5e38\u5fd8\u4e86\u5b83\u4f1a\u6d88\u5931\uff09<\/td><td>\u597d<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8fd9\u4e2a\u66ff\u6362\u662f\u6574\u4efd\u6587\u6863\u91cc\u6211\u6700\u60f3\u5f3a\u8c03\u7684\u4e00\u6761\u7ecf\u9a8c<\/strong>\uff1a\u5f53\u4e00\u4e2a\u673a\u5236\u7528&#8221;\u6301\u7eed\u6761\u4ef6&#8221;\u8868\u8fbe\u5f88\u522b\u626d\u65f6\uff0c\u8bd5\u8bd5\u7528&#8221;\u4e00\u6b21\u6027\u4e8b\u4ef6&#8221;\u8868\u8fbe\u3002\u4e8b\u4ef6\u9a71\u52a8\u6bd4\u72b6\u6001\u9a71\u52a8\u4fbf\u5b9c\u5f97\u591a\uff0c\u4e5f\u597d\u7406\u89e3\u5f97\u591a\u3002<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">\u6cd5\u672f\u5b66\u6d3e\uff08Spell School\uff09<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">\u65b0\u589e <code>Tag.SPELL_SCHOOL<\/code>\uff0c\u516d\u79cd\uff1a\u706b\u7130\/\u51b0\u971c\/\u81ea\u7136\/\u6697\u5f71\/\u795e\u5723\/\u5965\u672f\uff08\u540e\u6765\u52a0\u4e86\u300a\u7cbe\u9b42\u300b\uff09\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u96f6\u5f15\u64ce\u6539\u52a8<\/strong>\uff0c\u7eaf\u7cb9\u662f\u7ed9 Selector \u52a0\u4e86\u4e00\u4e2a\u7b5b\u9009\u7ef4\u5ea6\u3002\u4f46\u5b83\u6253\u5f00\u4e86\u5de8\u5927\u7684\u8bbe\u8ba1\u7a7a\u95f4\uff08&#8221;\u4f60\u7684\u706b\u7130\u6cd5\u672f\u4f24\u5bb3 +2&#8243;\uff09\uff0c\u4e14<strong>\u8ffd\u6eaf\u9002\u7528\u4e8e\u6240\u6709\u8001\u5361<\/strong>\u2014\u20142014 \u5e74\u7684\u300a\u706b\u7403\u672f\u300b\u81ea\u52a8\u83b7\u5f97\u4e86 <code>FIRE<\/code> \u5b66\u6d3e\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8fd9\u662f\u6570\u636e\u9a71\u52a8\u7684\u53e6\u4e00\u4e2a\u80dc\u5229<\/strong>\uff1a\u7ed9\u65e7\u6570\u636e\u6253\u65b0\u6807\u7b7e\uff0c\u65e7\u5361\u81ea\u52a8\u83b7\u5f97\u65b0\u673a\u5236\u7684\u517c\u5bb9\u6027\u3002<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">\u9636\u7ea7\u6cd5\u672f\uff08Ranked Spell\uff09<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u5982\u679c\u4f60\u6709 X \u4e2a\u6cd5\u529b\u6c34\u6676\uff0c\u6548\u679c\u5347\u7ea7\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">RANKED_SPELL = CardDef(<br>    card_id=\"BAR_064\", cost=1,<br>    spell=Cond3(<br>        (Cond.mana_crystals_at_least(10), StrongEffect()),<br>        (Cond.mana_crystals_at_least(5),  MediumEffect()),<br>        (Always(),                        WeakEffect()),<br>    ),<br>)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u7eaf\u6761\u4ef6\u5206\u652f\uff0c\u96f6\u65b0\u57fa\u7840\u8bbe\u65bd\u3002\u8bbe\u8ba1\u4e0a\u5b83\u89e3\u51b3\u4e86&#8221;\u4f4e\u8d39\u5361\u540e\u671f\u6ca1\u7528&#8221;\u7684\u95ee\u9898\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">21.2 \u66b4\u98ce\u57ce\u4e0b\u8bfe\uff082021.08\uff09\u2014\u2014\u4efb\u52a1\u94fe\u3001\u53ef\u4ea4\u6613<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\">\u4efb\u52a1\u94fe\uff08Questline\uff09<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u4efb\u52a1\u7684\u4e09\u9636\u6bb5\u7248\u672c\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">@dataclass(frozen=True)<br>class QuestlineDef:<br>    stages: tuple[QuestStage, ...]<br><br>@dataclass(frozen=True)<br>class QuestStage:<br>    trigger_event: Event<br>    condition: \"Condition\"<br>    required: int<br>    reward: \"Action\"           # \u4e2d\u95f4\u9636\u6bb5\u7684\u5956\u52b1<br>    next_card_id: str | None   # \u4e0b\u4e00\u9636\u6bb5\u7684\u5361<br><br>class QuestlineProgress(Action):<br>    def run(self, game, ctx):<br>        q = ctx.source<br>        q[Tag.QUEST_PROGRESS] += 1<br>        stage = q.defn.questline.stages[q[Tag.QUESTLINE_STAGE]]<br>        if q[Tag.QUEST_PROGRESS] &gt;= stage.required:<br>            stage.reward.run(game, ctx)<br>            if stage.next_card_id:<br>                # \u2605 \u539f\u5730\u53d8\u5f62\u6210\u4e0b\u4e00\u9636\u6bb5\uff08\u4fdd\u6301\u5728\u5965\u79d8\u533a\uff0c\u4fdd\u6301 ENTITY_ID\uff09<br>                transform(q, stage.next_card_id)<br>                q[Tag.QUEST_PROGRESS] = 0<br>                q[Tag.QUESTLINE_STAGE] += 1<br>                game.event_bus.unregister_all(q)<br>                game.event_bus.register_card(q)<br>            else:<br>                game.zones.move(q, Zone.GRAVEYARD)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4efb\u52a1\u94fe\u7684\u4ef7\u503c\u5728\u4e8e\u5b83\u590d\u7528\u4e86\u4e09\u6837\u4e1c\u897f<\/strong>\uff1a\u4efb\u52a1\u7684\u6ce8\u518c\u673a\u5236\u3001\u6cd5\u672f\u77f3\u7684\u624b\u724c\u53d8\u5f62\u3001\u8fe6\u62c9\u514b\u9686\u7684\u9636\u6bb5\u5347\u7ea7\u3002<strong>\u65b0\u589e\u4ee3\u7801\u7ea6 40 \u884c\u3002<\/strong> \u8fd9\u662f&#8221;\u5173\u952e\u8bcd\u662f\u79ef\u6728\u7684\u7ec4\u5408&#8221;\u7684\u6700\u597d\u8bc1\u660e\u3002<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">\u53ef\u4ea4\u6613\uff08Tradeable\uff09\u2605<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u82b1\u8d39 1 \u70b9\u6cd5\u529b\u503c\uff0c\u5c06\u8be5\u724c\u7f6e\u5165\u724c\u5e93\u5e76\u62bd\u4e00\u5f20\u724c\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def trade_card(game, card):<br>    pid = card[Tag.CONTROLLER]<br>    p = game.player(pid)<br>    if p[Tag.MANA_AVAILABLE] &lt; 1:<br>        return False<br>    if not card.has(Tag.TRADEABLE):<br>        return False<br>    p[Tag.MANA_AVAILABLE] -= 1<br>    # \u2605 \u6d17\u5165\u724c\u5e93\u968f\u673a\u4f4d\u7f6e\uff0c\u4e0d\u662f\u724c\u5e93\u9876<br>    game.zones.move(card, Zone.DECK)<br>    game.shuffle_deck(pid)<br>    game.draw(pid, 1)<br>    game.emit(Event.CARD_TRADED, entity=card)<br>    # \u2605 \u4ea4\u6613\u4e0d\u7b97\"\u6253\u51fa\u4e00\u5f20\u724c\"\uff0c\u4e0d\u89e6\u53d1\u8fde\u51fb\u3001\u4e0d\u89e6\u53d1\"\u4f7f\u7528\u4e00\u5f20\u724c\u540e\"<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5de5\u7a0b\u610f\u4e49<\/strong>\uff1a\u53ef\u4ea4\u6613\u662f<strong>\u7b2c\u4e00\u4e2a&#8221;\u975e\u51fa\u724c&#8221;\u7684\u624b\u724c\u64cd\u4f5c<\/strong>\u3002\u4e4b\u524d\u624b\u724c\u91cc\u7684\u5361\u53ea\u6709\u4e24\u4e2a\u51fa\u8def\uff1a\u6253\u51fa\u3001\u5f03\u6389\u3002\u53ef\u4ea4\u6613\u52a0\u4e86\u7b2c\u4e09\u4e2a\uff0c\u8fd9\u8981\u6c42\uff1a<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>UI \u589e\u52a0\u4e00\u4e2a\u624b\u52bf\uff08\u62d6\u5230\u724c\u5e93\uff09<\/li>\n\n\n\n<li>\u670d\u52a1\u5668\u589e\u52a0\u4e00\u4e2a action \u7c7b\u578b<\/li>\n\n\n\n<li>\u7f51\u7edc\u534f\u8bae\u589e\u52a0\u4e00\u4e2a\u6d88\u606f<\/li>\n\n\n\n<li>AI \u7684\u52a8\u4f5c\u7a7a\u95f4\u589e\u52a0\u4e00\u7ef4<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">\u7b2c 4 \u70b9\u503c\u5f97\u5c55\u5f00\uff1a<strong>\u6bcf\u589e\u52a0\u4e00\u4e2a\u73a9\u5bb6\u53ef\u6267\u884c\u7684\u52a8\u4f5c\u7c7b\u578b\uff0cAI \u7684\u641c\u7d22\u7a7a\u95f4\u5c31\u4e58\u4ee5\u4e00\u4e2a\u56e0\u5b50<\/strong>\u3002\u53ef\u4ea4\u6613\u8ba9\u6bcf\u5f20\u53ef\u4ea4\u6613\u724c\u90fd\u591a\u4e86\u4e00\u4e2a\u5206\u652f\u3002\u8fd9\u5c31\u662f\u4e3a\u4ec0\u4e48\u8bbe\u8ba1\u5e08\u5bf9&#8221;\u65b0\u52a8\u4f5c\u7c7b\u578b&#8221;\u975e\u5e38\u8c28\u614e\u2014\u20142021 \u5e74\u81f3\u4eca\u53ea\u52a0\u4e86\u4e09\u4e2a\uff08\u4ea4\u6613\u3001\u5730\u6807\u4f7f\u7528\u3001\u51c6\u5907\uff09\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">21.3 \u5965\u7279\u5170\u514b\u5c71\u8c37\uff082021.12\uff09\u2014\u2014\u8363\u8a89\u51fb\u6740<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\">\u8363\u8a89\u51fb\u6740\uff08Honorable Kill\uff09<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u9020\u6210\u6070\u597d\u8db3\u591f\u6d88\u706d\u4e00\u4e2a\u968f\u4ece\u7684\u4f24\u5bb3\u65f6\u89e6\u53d1\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class HonorableKillCond(Condition):<br>    def eval(self, game, e, ctx):<br>        return ctx.amount == ctx.target_health_before and ctx.fatal<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5b8c\u5168\u590d\u7528\u8d85\u6740\u7684\u57fa\u7840\u8bbe\u65bd<\/strong>\uff08<code>target_health_before<\/code> \u5b57\u6bb5\uff09\uff0c\u65b0\u589e\u4ee3\u7801 3 \u884c\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u5b83\u662f\u4e00\u4e2a&#8221;<strong>\u6280\u5de7\u578b<\/strong>&#8220;\u5173\u952e\u8bcd\u2014\u2014\u8981\u6c42\u73a9\u5bb6\u505a\u7b97\u672f\u3002\u8bbe\u8ba1\u4e0a\u5f88\u806a\u660e\uff08\u589e\u52a0\u4e86 skill ceiling \u800c\u4e0d\u589e\u52a0\u590d\u6742\u5ea6\uff09\uff0c\u5de5\u7a0b\u4e0a\u96f6\u6210\u672c\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 22 \u7ae0 2022\uff1a\u5de8\u578b\u4e0e\u5730\u6807<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">22.1 \u6c89\u6ca1\u4e4b\u57ce\uff082022.04\uff09\u2014\u2014\u6253\u635e\u3001\u5de8\u578b<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\">\u6253\u635e\uff08Dredge\uff09<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u67e5\u770b\u4f60\u724c\u5e93\u5e95\u7684\u4e09\u5f20\u724c\uff0c\u5c06\u5176\u4e2d\u4e00\u5f20\u7f6e\u4e8e\u724c\u5e93\u9876\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class Dredge(Action):<br>    async def run(self, game, ctx):<br>        deck = game.zones.get(ctx.controller, Zone.DECK)<br>        if not deck: return<br>        bottom = deck[:3]                     # \u2605 \u724c\u5e93\u5e95 = list \u7684\u5f00\u5934<br>        chosen = await game.ask_choice(ctx.controller, bottom, kind=\"DREDGE\")<br>        deck.remove(chosen)<br>        deck.append(chosen)                   # \u79fb\u5230\u724c\u5e93\u9876<br>        game.zones._reindex(deck)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6253\u635e\u7684\u8bbe\u8ba1\u4ef7\u503c<\/strong>\uff1a\u5b83\u662f&#8221;<strong>\u4fe1\u606f\u578b\u968f\u673a\u63a7\u5236<\/strong>&#8220;\u3002\u4f60\u4e0d\u80fd\u51ed\u7a7a\u751f\u6210\u8d44\u6e90\uff0c\u4f46\u4f60\u80fd\u64cd\u7eb5\u62bd\u724c\u987a\u5e8f\u3002\u8fd9\u6bd4 Discover \u7684\u65b9\u5dee\u4f4e\u5f97\u591a\uff0c\u4e14\u4e0d\u4ea7\u751f\u989d\u5916\u8d44\u6e90\uff0c\u6240\u4ee5\u5728\u5e73\u8861\u4e0a\u66f4\u5b89\u5168\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6ce8\u610f&#8221;\u724c\u5e93\u9876\/\u5e95&#8221;\u7684\u65b9\u5411\u7ea6\u5b9a\u3002<\/strong> \u6211\u5728\u4ee3\u7801\u91cc\u7528 <code>deck.pop()<\/code> \u62bd\u724c\uff0c\u6240\u4ee5 <code>list<\/code> \u7684\u672b\u5c3e\u662f\u724c\u5e93\u9876\u3002\u8fd9\u4e2a\u7ea6\u5b9a\u5fc5\u987b\u5168\u5c40\u4e00\u81f4\uff0c\u5426\u5219\u4f1a\u51fa\u73b0&#8221;\u6253\u635e\u653e\u5230\u4e86\u724c\u5e93\u5e95&#8221;\u8fd9\u79cd bug\u3002\u771f\u5b9e\u5f15\u64ce\u91cc\u6211\u5efa\u8bae<strong>\u7528\u663e\u5f0f\u7684 <code>deck.top()<\/code> \/ <code>deck.bottom()<\/code> \u65b9\u6cd5<\/strong>\u800c\u4e0d\u662f\u88f8\u7d22\u5f15\u3002<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">\u5de8\u578b\uff08Colossal +X\uff09\u2605<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u8be5\u968f\u4ece\u5165\u573a\u65f6\uff0c\u9644\u5e26 X \u4e2a\u8eab\u4f53\u90e8\u4ef6\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class Keyword_Colossal:<br>    @staticmethod<br>    def on_summon(game, main: Entity):<br>        parts = main.defn.colossal_parts       # \u5982 (\"TSC_646t\", \"TSC_646t2\")<br>        pos = main[Tag.ZONE_POSITION]<br>        # \u2605 \u90e8\u4ef6\u7684\u53ec\u5524\u987a\u5e8f\u4e0e\u4f4d\u7f6e\u6709\u660e\u786e\u89c4\u5219\uff1a<br>        #   \u7b2c\u4e00\u4e2a\u90e8\u4ef6\u653e\u4e3b\u4f53\u5de6\u4fa7\uff0c\u7b2c\u4e8c\u4e2a\u653e\u53f3\u4fa7\uff0c\u4ea4\u66ff<br>        left_parts  = parts[0::2]<br>        right_parts = parts[1::2]<br>        for p in reversed(left_parts):<br>            game.summon(p, main[Tag.CONTROLLER], position=pos - 1)<br>        for p in right_parts:<br>            game.summon(p, main[Tag.CONTROLLER],<br>                        position=main[Tag.ZONE_POSITION] + 1)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5de8\u578b\u7684\u9677\u9631<\/strong>\uff1a<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>\u573a\u5730\u4e0d\u591f\u65f6<\/strong>\uff0c\u4e3b\u4f53\u4f18\u5148\uff0c\u90e8\u4ef6\u80fd\u653e\u51e0\u4e2a\u653e\u51e0\u4e2a<\/li>\n\n\n\n<li>\u90e8\u4ef6\u662f<strong>\u72ec\u7acb\u968f\u4ece<\/strong>\uff0c\u53ef\u4ee5\u88ab\u5355\u72ec\u6d88\u706d<\/li>\n\n\n\n<li>\u90e8\u4ef6\u6b7b\u4ea1<strong>\u4e0d\u5f71\u54cd<\/strong>\u4e3b\u4f53<\/li>\n\n\n\n<li>\u4e3b\u4f53\u6b7b\u4ea1<strong>\u4e0d\u5f71\u54cd<\/strong>\u90e8\u4ef6<\/li>\n\n\n\n<li>\u90e8\u4ef6\u901a\u5e38\u6709 <code>UNTOUCHABLE<\/code> \u6216\u7279\u6b8a\u9650\u5236\uff08\u5982&#8221;\u4e0d\u80fd\u653b\u51fb&#8221;\uff09<\/li>\n\n\n\n<li><strong>\u590d\u5236\u4e3b\u4f53\u4e0d\u4f1a\u590d\u5236\u90e8\u4ef6<\/strong>\uff08<code>CopyMode.BASE<\/code>\uff09\uff0c\u9664\u975e\u662f\u7279\u6b8a\u5361<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5de5\u7a0b\u610f\u4e49<\/strong>\uff1a\u5de8\u578b\u662f&#8221;<strong>\u4e00\u5f20\u5361\u5bf9\u5e94\u591a\u4e2a\u5b9e\u4f53<\/strong>&#8220;\u7684\u7b2c\u4e00\u4e2a\u6b63\u5f0f\u5173\u952e\u8bcd\u3002\u5b83\u8981\u6c42 <code>SETASIDE<\/code> \u533a\u80fd\u540c\u65f6\u5bb9\u7eb3\u591a\u4e2a\u5f85\u53ec\u5524\u5b9e\u4f53\uff0c\u4e14\u53ec\u5524\u987a\u5e8f\u4e0e\u4f4d\u7f6e\u7684\u8ba1\u7b97\u5fc5\u987b\u7cbe\u786e\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">2026 \u5e74\u7684\u300a\u5de8\u578b\u300b\u56de\u5f52 + \u300a\u5148\u9a71\u300b\u5347\u7ea7\u673a\u5236\uff0c\u628a\u8fd9\u4e2a\u57fa\u7840\u8bbe\u65bd\u7528\u5230\u4e86\u65b0\u9ad8\u5ea6\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">22.2 \u7eb3\u65af\u5229\u4e9a\u5821\u7684\u60ac\u6848\uff082022.08\uff09\u2014\u2014\u5730\u6807\u3001\u704c\u6ce8<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\">\u5730\u6807\uff08Location\uff09\u2605<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u4e00\u79cd\u65b0\u7684\u5361\u724c\u7c7b\u578b\uff0c\u653e\u5728\u6218\u573a\u4e0a\uff0c\u6bcf\u56de\u5408\u53ef\u4ee5\u4f7f\u7528\u4e00\u6b21\uff0c\u6709\u8010\u4e45\u5ea6\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class CardType(IntEnum):<br>    ...<br>    LOCATION = 9<br><br>def use_location(game, loc: Entity):<br>    if loc[Tag.LOCATION_COOLDOWN] &gt; 0:<br>        return False                       # \u672c\u56de\u5408\u5df2\u7528\u8fc7<br>    if loc[Tag.DURABILITY] &lt;= 0:<br>        return False<br>    loc[Tag.LOCATION_COOLDOWN] = 1<br>    loc[Tag.DURABILITY] -= 1<br>    game.run(loc.defn.location_effect, source=loc, ctx=...)<br>    if loc[Tag.DURABILITY] &lt;= 0:<br>        loc[Tag.TO_BE_DESTROYED] = 1       # \u8d70\u6b63\u5e38\u6b7b\u4ea1\u7ed3\u7b97\uff0c\u4f1a\u89e6\u53d1\u4ea1\u8bed<br>    return True<br><br># begin_turn \u65f6\u91cd\u7f6e<br>for loc in game.locations(pid):<br>    loc[Tag.LOCATION_COOLDOWN] = 0<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5730\u6807\u662f 2022 \u5e74\u6700\u5927\u7684\u5f15\u64ce\u6539\u52a8<\/strong>\uff0c\u56e0\u4e3a\u5b83\u662f<strong>\u516b\u5e74\u6765\u7b2c\u4e00\u4e2a\u65b0\u5361\u724c\u7c7b\u578b<\/strong>\u3002\u5f71\u54cd\u9762\uff1a<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u7cfb\u7edf<\/th><th>\u6539\u52a8<\/th><\/tr><\/thead><tbody><tr><td>\u6218\u573a<\/td><td>\u5730\u6807\u5360\u6218\u573a\u683c\u5b50\uff0c\u4e0e\u968f\u4ece<strong>\u5171\u4eab<\/strong> 7 \u683c\u4e0a\u9650<\/td><\/tr><tr><td>\u76ee\u6807<\/td><td>\u5730\u6807\u4e0d\u80fd\u88ab\u653b\u51fb\uff0c\u53ef\u4ee5\u88ab\u6cd5\u672f\u6307\u5411\uff08\u5927\u90e8\u5206\uff09<\/td><\/tr><tr><td>\u6b7b\u4ea1<\/td><td>\u5730\u6807&#8221;\u8017\u5c3d&#8221;\u8d70\u6b7b\u4ea1\u7ed3\u7b97\uff0c\u53ef\u4ee5\u6709\u4ea1\u8bed<\/td><\/tr><tr><td>\u5149\u73af<\/td><td>\u5730\u6807\u80fd\u63d0\u4f9b\u5149\u73af<\/td><\/tr><tr><td>\u52a8\u4f5c\u7a7a\u95f4<\/td><td>\u65b0\u589e&#8221;\u4f7f\u7528\u5730\u6807&#8221;\u52a8\u4f5c<\/td><\/tr><tr><td>AI<\/td><td>\u641c\u7d22\u6811\u589e\u52a0\u4e00\u5c42<\/td><\/tr><tr><td>UI<\/td><td>\u65b0\u7684\u5361\u6846\u3001\u65b0\u7684\u4ea4\u4e92\u624b\u52bf<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u63a8\u65ad<\/strong>\uff1a\u66b4\u96ea\u80fd\u5728\u7b2c 8 \u5e74\u52a0\u4e00\u4e2a\u65b0\u5361\u724c\u7c7b\u578b\u800c\u4e0d\u91cd\u5199\u5f15\u64ce\uff0c\u6b63\u662f\u56e0\u4e3a&#8221;\u4e00\u5207\u7686\u5b9e\u4f53 + CARDTYPE \u662f tag&#8221;\u8fd9\u4e2a\u8bbe\u8ba1\u3002\u5982\u679c\u5361\u724c\u7c7b\u578b\u662f Python \u7684\u7c7b\u7ee7\u627f\uff0c\u52a0\u4e00\u4e2a\u7c7b\u578b\u8981\u6539\u51e0\u5341\u5904 <code>isinstance<\/code> \u5224\u65ad\u3002<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">\u704c\u6ce8\uff08Infuse\uff09<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u5728 X \u4e2a\u53cb\u65b9\u968f\u4ece\u6b7b\u4ea1\u540e\uff0c\u8be5\u724c\u5728\u4f60\u624b\u4e2d\u5347\u7ea7\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">INFUSE = TriggerDef(<br>    on=Event.DEATH,<br>    zones=frozenset({Zone.HAND}),<br>    condition=Cond.dead_is_friendly_minion(),<br>    action=InfuseProgress(),<br>)<br><br>class InfuseProgress(Action):<br>    def run(self, game, ctx):<br>        c = ctx.source<br>        c[Tag.INFUSE_PROGRESS] += 1<br>        if c[Tag.INFUSE_PROGRESS] &gt;= c.defn.infuse_required:<br>            transform(c, c.defn.infuse_upgraded_id)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u53c8\u4e00\u4e2a\u624b\u724c\u5185\u6210\u957f\u3002<strong>\u6ce8\u610f\u5b83\u8ba2\u9605\u7684\u662f <code>DEATH<\/code> \u4e8b\u4ef6\u800c\u4e14\u4f5c\u7528\u57df\u662f <code>HAND<\/code><\/strong>\u2014\u2014\u8fd9\u5c31\u662f <code>TriggerDef.zones<\/code> \u8fd9\u4e2a\u5b57\u6bb5\u7684\u4ef7\u503c\uff0c\u540c\u4e00\u5957\u673a\u5236\u8868\u8fbe\u4e86\u5b8c\u5168\u4e0d\u540c\u7684\u5361\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">22.3 \u51b0\u971c\u738b\u5ea7\u4e4bmarch\uff082022.12\uff09\u2014\u2014\u6b7b\u4ea1\u9a91\u58eb\u3001\u7b26\u6587\u3001\u5c38\u5757\u3001\u6cd5\u529b\u6e34\u6c42<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\">\u7b26\u6587\u7cfb\u7edf\uff08Rune\uff09<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5f52\u7c7b<\/strong>\uff1a\u6784\u7b51\u671f\u7ea6\u675f\uff0c\u8fd0\u884c\u65f6\u65e0\u611f\u77e5\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">@dataclass(frozen=True)<br>class RuneCost:<br>    blood: int = 0<br>    frost: int = 0<br>    unholy: int = 0<br><br>def deck_is_legal(deck: list[CardDef], hero_runes: RuneCost) -&gt; bool:<br>    for card in deck:<br>        r = card.rune_cost<br>        if (r.blood &gt; hero_runes.blood or r.frost &gt; hero_runes.frost<br>                or r.unholy &gt; hero_runes.unholy):<br>            return False<br>    return True<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5b83\u5b8c\u5168\u4e0d\u5f71\u54cd\u5bf9\u5c40\u5f15\u64ce<\/strong>\u2014\u2014\u8fd9\u662f\u4e2a\u597d\u7684\u8bbe\u8ba1\u8fb9\u754c\u3002\u6784\u7b51\u7ea6\u675f\u5e94\u8be5\u5728\u6784\u7b51\u671f\u89e3\u51b3\uff0c\u4e0d\u8981\u6e17\u900f\u8fdb\u5bf9\u5c40\u903b\u8f91\u3002<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">\u5c38\u5757\uff08Corpse\uff09<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5f52\u7c7b<\/strong>\uff1a\u65b0\u8d44\u6e90\u7c7b\u578b\uff08\u7b2c\u4e8c\u4e2a\uff0c\u7b2c\u4e00\u4e2a\u662f\u6cd5\u529b\u503c\uff09\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"># \u73a9\u5bb6\u7ea7 tag<br>Tag.CORPSES<br><br># \u968f\u4ece\u6b7b\u4ea1\u65f6\u81ea\u52a8 +1<br>TriggerDef(on=Event.DEATH, condition=Cond.dead_is_friendly_minion(),<br>           action=GainCorpses(1))<br><br>class SpendCorpses(Action):<br>    def __init__(self, n): self.n = n<br>    def run(self, game, ctx):<br>        p = game.player(ctx.controller)<br>        p[Tag.CORPSES] = max(0, p[Tag.CORPSES] - self.n)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u65b0\u589e\u8d44\u6e90\u7c7b\u578b\u7684\u5de5\u7a0b\u4ee3\u4ef7<\/strong>\uff1a<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>\u4e00\u4e2a\u73a9\u5bb6\u7ea7 tag\uff08\u4fbf\u5b9c\uff09<\/li>\n\n\n\n<li>UI \u663e\u793a\uff08\u4e2d\u7b49\uff09<\/li>\n\n\n\n<li>\u5361\u724c\u7684&#8221;\u53ef\u7528\u6027&#8221;\u5224\u5b9a\u8981\u8003\u8651\u5c38\u5757\uff08<code>can_play<\/code> \u589e\u52a0\u4e00\u4e2a\u6761\u4ef6\uff09<\/li>\n\n\n\n<li><strong>AI \u7684\u4f30\u503c\u51fd\u6570\u8981\u52a0\u4e00\u9879<\/strong>\uff08\u8fd9\u662f\u771f\u6b63\u7684\u6210\u672c\uff09<\/li>\n<\/ol>\n\n\n\n<h4 class=\"wp-block-heading\">\u6cd5\u529b\u6e34\u6c42\uff08Manathirst X\uff09<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u5982\u679c\u4f60\u672c\u5c40\u4e2d\u62e5\u6709\u8fc7 X \u4e2a\u6cd5\u529b\u6c34\u6676\uff0c\u6548\u679c\u5347\u7ea7\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class ManathirstCond(Condition):<br>    def __init__(self, n): self.n = n<br>    def eval(self, game, e, ctx):<br>        # \u2605 \u662f\"\u672c\u5c40\u66fe\u7ecf\u62e5\u6709\u8fc7\"\uff0c\u4e0d\u662f\"\u5f53\u524d\u62e5\u6709\"<br>        return game.player(ctx.controller).max_mana_ever &gt;= self.n<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u6ce8\u610f <code>max_mana_ever<\/code> \u8fd9\u4e2a\u5b57\u6bb5\u3002\u5b83\u5fc5\u987b\u5355\u72ec\u7ef4\u62a4\uff0c\u56e0\u4e3a\u5f53\u524d\u6cd5\u529b\u6c34\u6676\u53ef\u80fd\u88ab\u300a\u98de\u5200\u300b\u7c7b\u6548\u679c\u51cf\u5c11\u3002<strong>\u53c8\u4e00\u4e2a&#8221;\u5386\u53f2\u72b6\u6001&#8221;\u7684\u4f8b\u5b50<\/strong>\u2014\u2014\u5f15\u64ce\u9700\u8981\u8bb0\u4f4f\u7684\u4e1c\u897f\u8d8a\u6765\u8d8a\u591a\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 23 \u7ae0 2023\uff1a\u7ec8\u66f2\u3001\u6cf0\u5766\u3001\u6316\u6398\u3001\u953b\u9020<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">23.1 \u4f20\u8bf4\u8282\u65e5\uff082023.04\uff09\u2014\u2014\u7ec8\u66f2<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\">\u7ec8\u66f2\uff08Finale\uff09<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u5982\u679c\u8fd9\u5f20\u724c\u6d88\u8017\u4e86\u4f60\u6240\u6709\u7684\u6cd5\u529b\u503c\uff0c\u89e6\u53d1\u989d\u5916\u6548\u679c\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class FinaleCond(Condition):<br>    def eval(self, game, e, ctx):<br>        # \u2605 \u5728\u51fa\u724c\u6d41\u7a0b\u4e2d\uff0c\u6263\u8d39\u4e4b\u540e\u7acb\u523b\u5224\u65ad<br>        return game.player(ctx.controller)[Tag.MANA_AVAILABLE] == 0<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5b9e\u73b0\u4f4d\u7f6e\u5f88\u5173\u952e<\/strong>\uff1a\u5fc5\u987b\u5728\u6263\u8d39\u4e4b\u540e\u3001\u6548\u679c\u7ed3\u7b97\u4e4b\u524d\u5224\u65ad\uff0c\u800c\u4e14\u8981\u5728\u6548\u679c\u5185\u90e8\u4fdd\u5b58\u8fd9\u4e2a\u5e03\u5c14\u503c\uff08\u56e0\u4e3a\u6548\u679c\u672c\u8eab\u53ef\u80fd\u6539\u53d8\u6cd5\u529b\u503c\uff0c\u5982\u300a\u786c\u5e01\u300b\uff09\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def play_card(self, card, ...):<br>    ...<br>    p[Tag.MANA_AVAILABLE] -= cost<br>    ctx.finale = (p[Tag.MANA_AVAILABLE] == 0)     # \u2605 \u5feb\u7167<br>    ...<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u53c8\u4e00\u6b21&#8221;\u5feb\u7167&#8221;\u3002\u8fd9\u4efd\u6587\u6863\u91cc&#8221;\u5feb\u7167&#8221;\u51fa\u73b0\u4e86\u592a\u591a\u6b21\uff0c\u6211\u628a\u5b83\u63d0\u70bc\u6210\u4e00\u6761<strong>\u6838\u5fc3\u7ecf\u9a8c<\/strong>\u653e\u5728\u7b2c\u4e03\u90e8\u5206\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">23.2 \u6cf0\u5766\uff082023.08\uff09\u2014\u2014\u6cf0\u5766\u3001\u953b\u9020<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\">\u6cf0\u5766\uff08Titan\uff09\u2605<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u8be5\u968f\u4ece\u6709\u4e09\u4e2a\u6280\u80fd\uff0c\u6bcf\u56de\u5408\u53ef\u4ee5\u4f7f\u7528\u4e00\u4e2a\uff0c\u5168\u90e8\u4f7f\u7528\u5b8c\u540e\u624d\u80fd\u653b\u51fb\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class Keyword_Titan:<br>    @staticmethod<br>    def use_ability(game, titan: Entity, index: int):<br>        if titan[Tag.TITAN_ABILITY_USED] &amp; (1 &lt;&lt; index):<br>            return False                       # \u4f4d\u63a9\u7801\uff0c\u8fd9\u4e2a\u6280\u80fd\u7528\u8fc7\u4e86<br>        if titan[Tag.EXHAUSTED] and titan[Tag.NUM_TURNS_IN_PLAY] == 0:<br>            pass    # \u6cf0\u5766\u6280\u80fd\u53ef\u4ee5\u5728\u53ec\u5524\u56de\u5408\u4f7f\u7528\uff08\u4e0d\u53d7\u53ec\u5524\u5931\u8c03\u5f71\u54cd\uff09<br>        titan[Tag.TITAN_ABILITY_USED] |= (1 &lt;&lt; index)<br>        game.run(titan.defn.titan_abilities[index], source=titan)<br>        return True<br><br>    @staticmethod<br>    def can_attack(titan) -&gt; bool:<br>        return titan[Tag.TITAN_ABILITY_USED] == 0b111   # \u4e09\u4e2a\u90fd\u7528\u5b8c<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4f4d\u63a9\u7801\u5b58\u4e09\u4e2a\u5e03\u5c14\u503c<\/strong>\u662f\u4e2a\u5c0f\u6280\u5de7\uff0c\u4f46\u5b83\u4f53\u73b0\u4e86\u4e00\u4e2a\u539f\u5219\uff1a<strong>tag \u662f\u6574\u6570\uff0c\u6240\u4ee5\u4f60\u53ef\u4ee5\u5728\u4e00\u4e2a tag \u91cc\u585e\u591a\u4e2a\u5e03\u5c14\u4f4d<\/strong>\u3002\u8fd9\u5728\u9700\u8981\u7f51\u7edc\u540c\u6b65\u7684\u573a\u666f\u4e0b\u80fd\u7701\u5e26\u5bbd\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5de5\u7a0b\u5f71\u54cd<\/strong>\uff1a\u6cf0\u5766\u53c8\u589e\u52a0\u4e86\u4e00\u4e2a\u52a8\u4f5c\u7c7b\u578b\uff08&#8221;\u4f7f\u7528\u6cf0\u5766\u6280\u80fd&#8221;\uff09\uff0c\u4e14\u8fd9\u4e2a\u52a8\u4f5c\u6709\u4e09\u4e2a\u53d8\u4f53\u3002AI \u7684\u5206\u652f\u56e0\u5b50\u518d\u6b21\u4e0a\u5347\u3002<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">\u953b\u9020\uff08Forge\uff09<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u82b1\u8d39 2 \u70b9\u6cd5\u529b\u503c\uff0c\u4ece\u724c\u5e93\u4e2d\u4e22\u5f03\u8fd9\u5f20\u724c\u4ee5\u5347\u7ea7\u5b83\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def forge_card(game, card):<br>    pid = card[Tag.CONTROLLER]<br>    p = game.player(pid)<br>    if p[Tag.MANA_AVAILABLE] &lt; 2 or not card.has(Tag.FORGE):<br>        return False<br>    p[Tag.MANA_AVAILABLE] -= 2<br>    # \u2605 \u6d17\u5165\u724c\u5e93 \u2192 \u62bd\u4e00\u5f20 \u2192 \u539f\u5361\u53d8\u6210\u5347\u7ea7\u7248<br>    upgraded = card.defn.forge_upgraded_id<br>    game.zones.move(card, Zone.DECK)<br>    transform(card, upgraded)<br>    game.shuffle_deck(pid)<br>    game.draw(pid, 1)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u953b\u9020 = \u53ef\u4ea4\u6613 + \u53d8\u5f62<\/strong>\u3002\u96f6\u65b0\u57fa\u7840\u8bbe\u65bd\uff0c\u5b8c\u5168\u662f\u4e24\u4e2a\u5df2\u6709\u673a\u5236\u7684\u7ec4\u5408\u3002\u8fd9\u5c31\u662f\u6210\u719f\u5f15\u64ce\u7684\u6837\u5b50\uff1a<strong>\u65b0\u5173\u952e\u8bcd\u7684\u5b9e\u73b0\u6210\u672c\u968f\u65f6\u95f4\u9012\u51cf<\/strong>\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">23.3 \u8352\u829c\u4e4b\u5730\u7684\u89d2\u9010\uff082023.11\uff09\u2014\u2014\u6316\u6398\u3001\u5feb\u901f\u5c04\u51fb<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\">\u6316\u6398\uff08Excavate\uff09\u2605<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u83b7\u5f97\u4e00\u4e2a\u5b9d\u85cf\u3002\u4f60\u6316\u6398\u7684\u6b21\u6570\u8d8a\u591a\uff0c\u5b9d\u85cf\u8d8a\u597d\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">EXCAVATE_TIERS = {<br>    1: [\"TIER1_A\", \"TIER1_B\", \"TIER1_C\"],<br>    2: [\"TIER2_A\", ...],<br>    3: [\"TIER3_A\", ...],<br>    4: [\"LEGENDARY_TREASURE_BY_CLASS\"],    # \u6bcf\u804c\u4e1a\u4e00\u5f20<br>}<br><br>class Excavate(Action):<br>    async def run(self, game, ctx):<br>        p = game.player(ctx.controller)<br>        p.excavate_tier = min(4, p.excavate_tier + 1)<br>        pool = EXCAVATE_TIERS[p.excavate_tier]<br>        if p.excavate_tier == 4:<br>            cid = LEGENDARY_TREASURE[p.klass]<br>            p.excavate_tier = 0                # \u2605 \u6316\u5230\u4f20\u8bf4\u540e\u91cd\u7f6e<br>        else:<br>            cid = game.rng.choice(pool)<br>        game.add_to_hand(ctx.controller, cid)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6316\u6398 = \u8de8\u56de\u5408\u8ba1\u6570\u5668 + \u5206\u5c42\u968f\u673a\u6c60<\/strong>\u3002\u5b83\u7684\u8bbe\u8ba1\u4ef7\u503c\u5728\u4e8e<strong>\u7ed9\u968f\u673a\u6027\u4e00\u4e2a\u53ef\u9884\u6d4b\u7684\u8fdb\u5ea6\u6761<\/strong>\uff1a\u4f60\u77e5\u9053\u518d\u6316\u4e24\u6b21\u5c31\u6709\u4f20\u8bf4\u5b9d\u85cf\uff0c\u8fd9\u8ba9 RNG \u53d8\u6210\u4e86\u8d44\u6e90\u7ba1\u7406\u3002<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">\u5feb\u901f\u5c04\u51fb\uff08Quickdraw\uff09<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u5982\u679c\u5728\u4f60\u62bd\u5230\u8fd9\u5f20\u724c\u7684\u56de\u5408\u4f7f\u7528\u5b83\uff0c\u89e6\u53d1\u989d\u5916\u6548\u679c\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class QuickdrawCond(Condition):<br>    def eval(self, game, e, ctx):<br>        return e[Tag.DRAWN_ON_TURN] == game.turn<br><br># draw() \u91cc\u8bb0\u5f55<br>def draw(self, pid, n=1):<br>    ...<br>    card[Tag.DRAWN_ON_TURN] = self.turn<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u53c8\u4e00\u4e2a\u5386\u53f2\u72b6\u6001\u5b57\u6bb5\u3002<\/strong> \u5230 2023 \u5e74\uff0c\u5f15\u64ce\u9700\u8981\u7ef4\u62a4\u7684&#8221;\u5386\u53f2&#8221;\u5df2\u7ecf\u5305\u62ec\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u672c\u5c40\u6700\u5927\u6cd5\u529b\u503c\uff08Manathirst\uff09<\/li>\n\n\n\n<li>\u4e0a\u56de\u5408\u662f\u5426\u4f7f\u7528\u8fc7\u5143\u7d20\/\u67d0\u7c7b\u578b\uff08\u5143\u7d20\u3001\u540c\u6e90\uff09<\/li>\n\n\n\n<li>\u672c\u56de\u5408\u662f\u5426\u6253\u51fa\u8fc7\u724c\uff08\u8fde\u51fb\uff09<\/li>\n\n\n\n<li>\u672c\u5c40\u4f7f\u7528\u8fc7\u591a\u5c11\u6cd5\u672f\uff08\u5c24\u683c\uff09<\/li>\n\n\n\n<li>\u6316\u6398\u7b49\u7ea7<\/li>\n\n\n\n<li>\u62bd\u5230\u7684\u56de\u5408\uff08\u5feb\u901f\u5c04\u51fb\uff09<\/li>\n\n\n\n<li>\u672c\u5c40\u6b7b\u4ea1\u8fc7\u7684\u53cb\u65b9\u968f\u4ece\u6570\uff08\u704c\u6ce8\u3001\u590d\u6d3b\u7c7b\uff09<\/li>\n\n\n\n<li>&#8230;<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8bbe\u8ba1\u5efa\u8bae<\/strong>\uff1a\u4e0d\u8981\u4e3a\u6bcf\u4e2a\u673a\u5236\u52a0\u4e00\u4e2a\u4e13\u7528\u5b57\u6bb5\u3002\u7528\u4e00\u4e2a\u7edf\u4e00\u7684 <code>GameStats<\/code> \u7ed3\u6784\uff0c\u6309&#8221;\u4e8b\u4ef6\u8ba1\u6570\u5668&#8221;\u7684\u65b9\u5f0f\u7ec4\u7ec7\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">@dataclass<br>class PlayerStats:<br>    counters: dict[str, int] = field(default_factory=lambda: defaultdict(int))<br>    per_turn: dict[str, int] = field(default_factory=lambda: defaultdict(int))<br>    last_turn: dict[str, int] = field(default_factory=lambda: defaultdict(int))<br><br>    def bump(self, key, n=1):<br>        self.counters[key] += n<br>        self.per_turn[key] += n<br><br>    def rotate(self):<br>        self.last_turn = dict(self.per_turn)<br>        self.per_turn.clear()<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u7136\u540e\u6240\u6709&#8221;\u5386\u53f2\u6761\u4ef6&#8221;\u53d8\u6210\u7edf\u4e00\u67e5\u8be2\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">Cond.stat_at_least(\"spells_cast\", 6)          # \u4efb\u52a1<br>Cond.last_turn_stat_at_least(\"elemental_played\", 1)   # \u5143\u7d20\/\u540c\u6e90<br>Cond.turn_stat_at_least(\"cards_played\", 1)    # \u8fde\u51fb<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8fd9\u4e00\u6761\u91cd\u6784\u80fd\u7701\u6389\u540e\u9762\u5341\u5e74\u7684\u4e00\u534a\u65b0\u589e\u5b57\u6bb5\u3002<\/strong><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 24 \u7ae0 2024\uff1a\u8ff7\u4f60\u5316\u3001\u65c5\u5ba2\u3001\u661f\u8230<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">24.1 \u7ef4\u5179\u73ed\u7684\u5de5\u574a\uff082024.03\uff09\u2014\u2014\u8ff7\u4f60\u5316<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\">\u8ff7\u4f60\u5316\uff08Miniaturize\uff09<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u6218\u543c\uff1a\u5c06\u4e00\u5f20 1 \u8d39\u7684 1\/1 \u8ff7\u4f60\u7248\u6d17\u5165\u4f60\u7684\u724c\u5e93\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">MINIATURIZE = Seq(<br>    ShuffleIntoDeck(Ctx.SOURCE_MINI_ID),<br>)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u7eaf\u7ec4\u5408\uff0c\u96f6\u65b0\u57fa\u7840\u8bbe\u65bd\u3002\u8ff7\u4f60\u7248\u662f\u4e00\u5f20\u72ec\u7acb\u7684\u5361\u5b9a\u4e49\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">24.2 \u5929\u7a7a\u90ae\u8f6e\u7684\u6096\u8bba\uff082024.07\uff09\u2014\u2014\u65c5\u5ba2<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\">\u65c5\u5ba2\uff08Tourist\uff09\u2605<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u4f60\u7684\u724c\u5e93\u4e2d\u53ef\u4ee5\u5305\u542b\u53e6\u4e00\u4e2a\u804c\u4e1a\u7684 X \u5361\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def deck_is_legal_with_tourist(deck, hero_class) -&gt; bool:<br>    tourists = [c for c in deck if c.tourist_class]<br>    allowed_extra = {t.tourist_class for t in tourists}<br>    for c in deck:<br>        if c.klass == hero_class or c.klass == \"NEUTRAL\":<br>            continue<br>        if c.klass in allowed_extra and c.is_tourist_eligible:<br>            continue<br>        return False<br>    return True<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u53c8\u4e00\u4e2a\u6784\u7b51\u671f\u673a\u5236<\/strong>\uff0c\u5bf9\u5c40\u5f15\u64ce\u5b8c\u5168\u65e0\u611f\u77e5\u3002\u8fd9\u662f\u4e2a\u597d\u8bbe\u8ba1\u2014\u2014<strong>\u628a\u590d\u6742\u5ea6\u63a8\u5230\u6784\u7b51\u671f\uff0c\u8ba9\u5bf9\u5c40\u5f15\u64ce\u4fdd\u6301\u7b80\u5355<\/strong>\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u4f46\u5b83\u6709\u4e00\u4e2a\u8fd0\u884c\u65f6\u540e\u679c\uff1a<strong>&#8220;\u53d1\u73b0\u4e00\u5f20\u4f60\u804c\u4e1a\u7684\u5361&#8221;\u8fd9\u4e2a\u67e5\u8be2\u53d8\u4e86<\/strong>\u3002\u5982\u679c\u4f60\u5e26\u4e86\u76d7\u8d3c\u65c5\u5ba2\uff0c\u4f60\u7684\u6cd5\u5e08\u80fd\u4e0d\u80fd\u53d1\u73b0\u76d7\u8d3c\u5361\uff1f\u7b54\u6848\u662f<strong>\u80fd<\/strong>\uff08\u5bf9\u4e8e\u65c5\u5ba2\u5141\u8bb8\u7684\u90a3\u90e8\u5206\uff09\u3002\u6240\u4ee5 <code>CardPool.build()<\/code> \u8981\u8003\u8651\u65c5\u5ba2\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def build(self, game, ctx):<br>    klass = game.player(ctx.controller).klass<br>    classes = {klass}<br>    classes |= game.player(ctx.controller).tourist_classes   # \u2605<br>    return game.card_index.query(klass=classes, ...)<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">24.3 \u6d69\u701a\u4e4b\u5916\uff082024.11\uff09\u2014\u2014\u661f\u8230<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\">\u661f\u8230\uff08Starship\uff09\u2605<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u661f\u8230\u96f6\u4ef6\u53ef\u4ee5\u88c5\u914d\u5230\u661f\u8230\u4e0a\u3002\u88c5\u914d\u8db3\u591f\u96f6\u4ef6\u540e\uff0c\u53d1\u5c04\u661f\u8230\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class StarshipSystem:<br>    \"\"\"\u661f\u8230\u662f\u4e00\u4e2a'\u5bb9\u5668\u5b9e\u4f53'\uff0c\u96f6\u4ef6\u662f\u9644\u52a0\u5728\u5b83\u4e0a\u9762\u7684\u9644\u9b54\u3002\"\"\"<br><br>    @staticmethod<br>    def install_part(game, starship: Entity, part: Entity):<br>        # \u96f6\u4ef6\u7684\u5c5e\u6027\u53e0\u52a0\u5230\u661f\u8230\u4e0a\uff08\u548c\u78c1\u529b\u51e0\u4e4e\u4e00\u6837\uff01\uff09<br>        ench = make_enchantment_from(part)<br>        apply_enchantment_entity(game, starship, ench)<br>        starship.installed_parts.append(part[Tag.CARD_ID])<br>        game.remove_entity(part)<br><br>    @staticmethod<br>    def launch(game, starship: Entity):<br>        # \u53d1\u5c04\uff1a\u661f\u8230\u53d8\u6210\u4e00\u4e2a\u771f\u6b63\u7684\u968f\u4ece\uff0c\u83b7\u5f97\u6240\u6709\u96f6\u4ef6\u7684\u80fd\u529b<br>        starship[Tag.LAUNCHED] = 1<br>        starship[Tag.EXHAUSTED] = 0     # \u53ef\u4ee5\u7acb\u5373\u653b\u51fb<br>        for part_id in starship.installed_parts:<br>            for kw in CARD_DB[part_id].keywords:<br>                starship[TAG_OF_KEYWORD[kw]] = 1<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u661f\u8230 = \u78c1\u529b + \u6cf0\u5766 + \u5de8\u578b\u7684\u7ec4\u5408<\/strong>\u3002\u5b83\u9700\u8981\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u78c1\u529b\u7684&#8221;\u5c5e\u6027\u4e0e\u80fd\u529b\u5408\u5e76&#8221;<\/li>\n\n\n\n<li>\u6cf0\u5766\u7684&#8221;\u591a\u9636\u6bb5\u4f7f\u7528&#8221;<\/li>\n\n\n\n<li>\u5de8\u578b\u7684&#8221;\u4e00\u5f20\u5361\u591a\u4e2a\u5b9e\u4f53&#8221;<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u65b0\u589e\u4ee3\u7801\u5f88\u5c11\uff0c\u56e0\u4e3a\u4e09\u5957\u57fa\u7840\u8bbe\u65bd\u90fd\u5728\u3002<\/strong> \u8fd9\u662f\u6211\u5728\u6574\u4efd\u7f16\u5e74\u53f2\u91cc\u89c2\u5bdf\u5230\u7684\u6700\u6e05\u6670\u7684\u8d8b\u52bf\uff1a<strong>2014-2018 \u6bcf\u4e2a\u65b0\u5173\u952e\u8bcd\u90fd\u8981\u52a8\u5f15\u64ce\uff0c2019 \u4e4b\u540e\u7edd\u5927\u591a\u6570\u65b0\u5173\u952e\u8bcd\u662f\u5df2\u6709\u79ef\u6728\u7684\u91cd\u7ec4\u3002<\/strong><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 25 \u7ae0 2025\uff1a\u704c\u6ce8\u3001\u9ed1\u6697\u6069\u8d50\u3001\u540c\u6e90\u3001\u5012\u5e26\u3001\u4f20\u5947<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">25.1 \u7fe1\u7fe0\u68a6\u5883\uff082025.03\uff09\u2014\u2014\u704c\u6ce8\u3001\u9ed1\u6697\u6069\u8d50<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u6765\u6e90\uff1a<a href=\"https:\/\/news.blizzard.com\/en-us\/article\/24179067\/step-into-the-emerald-dream-hearthstones-next-expansion\">Into the Emerald Dream \u5b98\u65b9\u516c\u544a<\/a>\u3001<a href=\"https:\/\/esports.gg\/news\/hearthstone\/hearthstone-into-the-emerald-dream-release-date-and-details-imbue-dark-gifts-wild-gods-and-choose-one-for-all\/\">esports.gg \u673a\u5236\u89e3\u6790<\/a><\/p>\n\n\n\n<h4 class=\"wp-block-heading\">\u704c\u6ce8\uff08Imbue\uff09\u2605<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u4f7f\u7528\u4e00\u5f20\u704c\u6ce8\u724c\u540e\uff0c\u4f60\u7684\u82f1\u96c4\u6280\u80fd\u5347\u7ea7\u3002\u6bcf\u6b21\u704c\u6ce8\u7ee7\u7eed\u5347\u7ea7\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u9002\u7528\u804c\u4e1a\uff1a\u5fb7\u9c81\u4f0a\u3001\u730e\u4eba\u3001\u6cd5\u5e08\u3001\u5723\u9a91\u58eb\u3001\u7267\u5e08\u3001\u8428\u6ee1\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class Imbue(Action):<br>    def run(self, game, ctx):<br>        p = game.player(ctx.controller)<br>        p.imbue_count += 1<br>        hp = p.hero_power<br>        if p.imbue_count == 1:<br>            # \u7b2c\u4e00\u6b21\uff1a\u66ff\u6362\u6210\u704c\u6ce8\u7248\u82f1\u96c4\u6280\u80fd<br>            game.replace_hero_power(ctx.controller, IMBUE_HERO_POWER[p.klass])<br>        else:<br>            # \u540e\u7eed\uff1a\u5347\u7ea7\uff08\u7d2f\u52a0 tag\uff09<br>            hp[Tag.IMBUE_LEVEL] += 1<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8fd9\u662f 2019 \u5e74\u300a\u63a2\u5e95\u300b\uff08Invoke\uff09\u7684\u76f4\u7cfb\u540e\u4ee3<\/strong>\u2014\u2014\u540c\u6837\u7684&#8221;\u8ba1\u6570\u5668 + \u82f1\u96c4\u6280\u80fd\u5347\u7ea7&#8221;\u6a21\u5f0f\uff0c\u95f4\u9694\u516d\u5e74\u540e\u590d\u7528\u3002\u5de5\u7a0b\u4e0a\u51e0\u4e4e\u96f6\u6210\u672c\u3002<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">\u9ed1\u6697\u6069\u8d50\uff08Dark Gift\uff09<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u53d1\u73b0\u4e00\u4e2a\u5f3a\u5316\uff0c\u9644\u52a0\u7ed9\u4e00\u4e2a\u968f\u4ece\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u9002\u7528\u804c\u4e1a\uff1a\u6b7b\u4ea1\u9a91\u58eb\u3001\u6076\u9b54\u730e\u624b\u3001\u76d7\u8d3c\u3001\u672f\u58eb\u3001\u6218\u58eb\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">DARK_GIFTS = [<br>    (\"GIFT_TAUNT\",   Buff(atk=0, hp=2) + GiveKeyword(\"TAUNT\")),<br>    (\"GIFT_RUSH\",    Buff(atk=2, hp=0) + GiveKeyword(\"RUSH\")),<br>    (\"GIFT_DR\",      GiveDeathrattle(Draw(1))),<br>    ...<br>]<br><br>class DarkGift(Action):<br>    async def run(self, game, ctx):<br>        picks = game.rng.sample(DARK_GIFTS, 3)<br>        chosen = await game.ask_choice(ctx.controller, picks, kind=\"DARK_GIFT\")<br>        await chosen.action.run(game, ctx)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8fd9\u662f\u300a\u9002\u5e94\u300b\uff082017\uff09\u7684\u76f4\u63a5\u590d\u523b<\/strong>\uff1a\u56fa\u5b9a\u6c60 + \u4e09\u9009\u4e00 + \u9644\u52a0\u7ed9\u968f\u4ece\u3002\u516b\u5e74\u524d\u7684\u57fa\u7840\u8bbe\u65bd\uff0c\u4e00\u884c\u4e0d\u6539\u5730\u590d\u7528\u3002<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">\u6289\u62e9\u5168\u804c\u4e1a\u5316<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">2025 \u5e74\u628a\u300a\u6289\u62e9\u300b\u4ece\u5fb7\u9c81\u4f0a\u4e13\u5c5e\u6269\u5c55\u5230\u6240\u6709\u804c\u4e1a\uff0c\u5e76\u5f15\u5165&#8221;<strong>\u6289\u62e9\u591a\u9009<\/strong>&#8220;\u3002\u5f15\u64ce\u4fa7\uff1a<code>ctx.choose<\/code> \u4ece <code>int<\/code> \u53d8\u6210 <code>list[int]<\/code>\uff0c<code>resolve_choose_one<\/code> \u5faa\u73af\u6267\u884c\u3002\u7ea6 15 \u884c\u6539\u52a8\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">25.2 \u5931\u843d\u7684\u5b89\u6208\u6d1b\u4e4b\u57ce\uff082025.07\uff09\u2014\u2014\u540c\u6e90\u3001\u4efb\u52a1\u56de\u5f52<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u6765\u6e90\uff1a<a href=\"https:\/\/esports.gg\/news\/hearthstone\/lost-city-of-ungoro-is-hearthstones-next-expansion-familiar-faces-kindred-and-story-cards\/\">esports.gg \u62a5\u9053<\/a>\u3001<a href=\"https:\/\/hearthstone.blizzard.com\/en-us\/expansions-adventures\/the-lost-city-of-ungoro\">\u5b98\u65b9\u9875\u9762<\/a><\/p>\n\n\n\n<h4 class=\"wp-block-heading\">\u540c\u6e90\uff08Kindred\uff09\u2605<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u5982\u679c\u4f60\u5728\u4e0a\u4e2a\u56de\u5408\u4f7f\u7528\u8fc7\u76f8\u540c\u968f\u4ece\u7c7b\u578b\u6216\u6cd5\u672f\u5b66\u6d3e\u7684\u724c\uff0c\u83b7\u5f97\u989d\u5916\u6548\u679c\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class KindredCond(Condition):<br>    def __init__(self, kind: str, value: str):<br>        self.kind, self.value = kind, value   # kind \u2208 {\"RACE\", \"SPELL_SCHOOL\"}<br>    def eval(self, game, e, ctx):<br>        stats = game.player(ctx.controller).stats<br>        key = f\"played_{self.kind.lower()}_{self.value}\"<br>        return stats.last_turn.get(key, 0) &gt; 0<br><br># \u51fa\u724c\u65f6\u8bb0\u5f55<br>def _record_play_stats(self, card):<br>    stats = self.player(card[Tag.CONTROLLER]).stats<br>    stats.bump(\"cards_played\")<br>    if card[Tag.CARDRACE]:<br>        stats.bump(f\"played_race_{card[Tag.CARDRACE]}\")<br>    if card[Tag.SPELL_SCHOOL]:<br>        stats.bump(f\"played_spell_school_{card[Tag.SPELL_SCHOOL]}\")<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5982\u679c\u4f60\u5728 2017 \u5e74\u5c31\u6309 23.3 \u8282\u7684\u5efa\u8bae\u505a\u4e86\u7edf\u4e00\u7684 <code>PlayerStats<\/code>\uff0c\u540c\u6e90\u7684\u5b9e\u73b0\u662f\u96f6\u884c\u5f15\u64ce\u4ee3\u7801<\/strong>\u2014\u2014\u53ea\u662f\u4e00\u4e2a\u65b0\u7684 <code>Condition<\/code>\u3002\u8fd9\u662f\u6211\u5728\u8fd9\u4efd\u6587\u6863\u91cc\u80fd\u7ed9\u51fa\u7684\u6700\u6709\u8bf4\u670d\u529b\u7684&#8221;\u63d0\u524d\u62bd\u8c61&#8221;\u8bba\u636e\uff1a2017 \u5e74\u300a\u5143\u7d20\u300b\u5f15\u5165\u7684&#8221;\u4e0a\u56de\u5408\u662f\u5426\u7528\u8fc7\u67d0\u7c7b\u578b&#8221;\uff0c\u516b\u5e74\u540e\u88ab\u300a\u540c\u6e90\u300b\u63a8\u5e7f\u5230\u6240\u6709\u7c7b\u578b\u548c\u5b66\u6d3e\u3002<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">\u6545\u4e8b\u5361\u4e0e\u5730\u56fe\u5361<\/h4>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>\u6545\u4e8b\u5361<\/strong>\uff1a\u5f15\u7528\u8fc7\u53bb\u8d44\u6599\u7247\u7684\u4efb\u52a1\uff0c\u89e6\u53d1\u5bf9\u5e94\u6548\u679c\u3002\u7eaf\u6570\u636e\u3002<\/li>\n\n\n\n<li><strong>\u5730\u56fe\u5361<\/strong>\uff1a\u53d1\u73b0\u4e00\u5f20\u724c\uff0c\u5982\u679c\u672c\u56de\u5408\u6253\u51fa\uff0c\u518d\u53d1\u73b0\u4e00\u5f20\u3002\u8fd9\u662f&#8221;\u94fe\u5f0f\u53d1\u73b0&#8221;\uff0c\u5b9e\u73b0\u4e0a\u662f <code>Discover(then=MarkForChain())<\/code> + \u4e00\u4e2a\u56de\u5408\u5185\u89e6\u53d1\u5668\u3002<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-preformatted\">MAP_CARD = Seq(<br>    Discover(pool=...),<br>    RegisterTemporaryTrigger(<br>        TriggerDef(on=Event.PLAY_CARD,<br>                   condition=Cond.is_the_discovered_card(),<br>                   action=Discover(pool=...),<br>                   once=True, expires=Duration.END_OF_TURN),<br>    ),<br>)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>&#8220;\u4e34\u65f6\u89e6\u53d1\u5668&#8221;<\/strong>\uff08\u672c\u56de\u5408\u6709\u6548\u7684\u52a8\u6001\u6ce8\u518c\uff09\u662f\u8fd9\u91cc\u552f\u4e00\u7684\u65b0\u57fa\u7840\u8bbe\u65bd\uff0c\u7ea6 25 \u884c\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">25.3 \u65f6\u5149\u4e4b\u5916\uff082025.11\uff09\u2014\u2014\u5012\u5e26\u3001\u4f20\u5947<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u6765\u6e90\uff1a<a href=\"https:\/\/esports.gg\/news\/hearthstone\/hearthstone-gets-across-the-timeways-expansion-with-new-rewind-and-fabled-keywords\/\">esports.gg \u62a5\u9053<\/a>\u3001<a href=\"https:\/\/news.blizzard.com\/en-us\/article\/24226328\/\">\u5b98\u65b9\u516c\u544a<\/a><\/p>\n\n\n\n<h4 class=\"wp-block-heading\">\u5012\u5e26\uff08Rewind\uff09\u2605<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u91cd\u65b0\u4f7f\u7528\u8fd9\u5f20\u724c\uff0c\u4ee5\u83b7\u5f97\u4e0d\u540c\u7684\u968f\u673a\u7ed3\u679c\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u8fd9\u662f\u7089\u77f3<strong>\u7b2c\u4e00\u4e2a\u6b63\u5f0f\u7684&#8221;\u64a4\u9500\u5e76\u91cd\u505a&#8221;\u673a\u5236<\/strong>\uff0c\u5de5\u7a0b\u4e0a\u975e\u5e38\u6709\u610f\u601d\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class Rewind(Action):<br>    \"\"\"\u91cd\u653e\u4e00\u5f20\u5df2\u7ed3\u7b97\u7684\u968f\u673a\u5361\uff0c\u64a4\u9500\u539f\u7ed3\u679c\u3002\"\"\"<br>    async def run(self, game, ctx):<br>        card = ctx.source<br>        # \u2605 \u5fc5\u987b\u80fd\"\u64a4\u9500\"\u4e0a\u4e00\u6b21\u7684\u6548\u679c<br>        game.undo_to_checkpoint(card.rewind_checkpoint)<br>        # \u91cd\u65b0\u6d88\u8d39 RNG\uff08\u2605 \u5173\u952e\uff1a\u5fc5\u987b\u662f\u65b0\u7684\u968f\u673a\u6570\uff0c\u4e0d\u80fd\u91cd\u653e\u540c\u4e00\u5e8f\u5217\uff09<br>        await game.replay_card_effect(card)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8fd9\u9700\u8981\u5f15\u64ce\u652f\u6301&#8221;\u72b6\u6001\u68c0\u67e5\u70b9 + \u56de\u6eda&#8221;<\/strong>\uff0c\u8fd9\u662f\u4e00\u4e2a\u91cd\u91cf\u7ea7\u80fd\u529b\u3002\u6709\u4e09\u79cd\u53ef\u80fd\u7684\u5b9e\u73b0\uff08<strong>\u63a8\u65ad<\/strong>\uff0c\u5b98\u65b9\u672a\u516c\u5f00\uff09\uff1a<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u65b9\u6848<\/th><th>\u505a\u6cd5<\/th><th>\u4ee3\u4ef7<\/th><\/tr><\/thead><tbody><tr><td><strong>A. \u5168\u72b6\u6001\u5feb\u7167<\/strong><\/td><td>\u51fa\u724c\u524d\u6df1\u62f7\u8d1d\u6574\u4e2a GameState\uff0c\u5012\u5e26\u65f6\u8fd8\u539f<\/td><td>\u5185\u5b58\u5927\uff0c\u4f46\u7edd\u5bf9\u6b63\u786e<\/td><\/tr><tr><td><strong>B. \u53cd\u5411\u64cd\u4f5c\u65e5\u5fd7<\/strong><\/td><td>\u8bb0\u5f55\u6bcf\u4e2a tag \u53d8\u5316\u7684 (entity, tag, old, new)\uff0c\u5012\u5e26\u65f6\u53cd\u5411\u91cd\u653e<\/td><td>\u5185\u5b58\u5c0f\uff0c\u4f46&#8221;\u521b\u5efa\u5b9e\u4f53&#8221;\u7b49\u64cd\u4f5c\u96be\u4ee5\u5b8c\u7f8e\u64a4\u9500<\/td><\/tr><tr><td><strong>C. \u9650\u5236\u4f5c\u7528\u57df<\/strong><\/td><td>\u53ea\u5141\u8bb8\u5bf9&#8221;\u6548\u679c\u8303\u56f4\u660e\u786e\u4e14\u5c40\u90e8&#8221;\u7684\u5361\u4f7f\u7528\u5012\u5e26<\/td><td>\u6700\u7b80\u5355\uff0c\u4f46\u9650\u5236\u4e86\u8bbe\u8ba1\u7a7a\u95f4<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">\u6211\u7684\u63a8\u65ad\u662f\u66b4\u96ea\u7528\u4e86 <strong>C + B \u7684\u7ec4\u5408<\/strong>\uff1a\u5012\u5e26\u53ea\u51fa\u73b0\u5728&#8221;\u751f\u6210\u968f\u673a\u5361\/\u53ec\u5524\u968f\u673a\u968f\u4ece&#8221;\u8fd9\u7c7b\u6548\u679c<strong>\u5c40\u90e8\u4e14\u53ef\u679a\u4e3e<\/strong>\u7684\u5361\u4e0a\uff0c\u64a4\u9500\u65f6\u53ea\u9700\u8981\u5220\u6389\u751f\u6210\u7684\u5b9e\u4f53\u3001\u8fd8\u539f RNG \u4e4b\u5916\u7684\u72b6\u6001\u3002\u6587\u6863\u91cc\u63cf\u8ff0\u7684\u4f8b\u5b50\uff08&#8221;\u65bd\u653e\u4e0d\u7a33\u5b9a\u4f20\u9001\u95e8\u5f97\u5230\u5e7d\u7075\uff0c\u5012\u5e26\u91cd\u6447&#8221;\uff09\u5b8c\u5168\u7b26\u5408\u8fd9\u4e2a\u7279\u5f81\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5982\u679c\u4f60\u8981\u5728\u81ea\u5df1\u7684\u5f15\u64ce\u91cc\u505a\u7c7b\u4f3c\u673a\u5236<\/strong>\uff0c\u6211\u5f3a\u70c8\u5efa\u8bae\u65b9\u6848 A\uff08\u5168\u5feb\u7167\uff09+ \u7ed3\u6784\u5171\u4eab\uff08persistent data structure\uff09\u3002\u56e0\u4e3a\u65b9\u6848 B \u7684&#8221;\u64a4\u9500&#8221;\u5728\u6709\u89e6\u53d1\u94fe\u7684\u60c5\u51b5\u4e0b\u51e0\u4e4e\u4e0d\u53ef\u80fd\u505a\u5bf9\u2014\u2014\u4f60\u64a4\u9500\u4e86\u4e00\u4e2a\u53ec\u5524\uff0c\u4f46\u90a3\u4e2a\u53ec\u5524\u89e6\u53d1\u7684\u300a\u98de\u5200\u6742\u800d\u8005\u300b\u4f24\u5bb3\u600e\u4e48\u64a4\u9500\uff1f<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5012\u5e26\u5bf9\u786e\u5b9a\u6027\u968f\u673a\u7684\u5f71\u54cd<\/strong>\uff1a\u5012\u5e26<strong>\u5fc5\u987b\u6d88\u8d39\u65b0\u7684\u968f\u673a\u6570<\/strong>\uff0c\u5426\u5219\u5b83\u5c31\u6beb\u65e0\u610f\u4e49\u3002\u8fd9\u610f\u5473\u7740 RNG \u5e8f\u5217\u4e0d\u80fd&#8221;\u6309\u64cd\u4f5c\u7d22\u5f15\u786e\u5b9a&#8221;\uff0c\u53ea\u80fd\u662f&#8221;\u987a\u5e8f\u6d88\u8d39\u7684\u6d41&#8221;\u3002\u8fd9\u5bf9\u56de\u653e\u7cfb\u7edf\u662f\u4e2a\u7ea6\u675f\uff1a\u56de\u653e\u5fc5\u987b\u8bb0\u5f55<strong>\u6bcf\u6b21 RNG \u8c03\u7528\u7684\u7ed3\u679c<\/strong>\uff0c\u800c\u4e0d\u662f\u53ea\u8bb0\u5f55 seed\u3002<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">\u4f20\u5947\uff08Fabled\uff09<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u5c06\u4e00\u4e2a\u4f20\u5947\u82f1\u96c4\u52a0\u5165\u724c\u5e93\u65f6\uff0c\u81ea\u52a8\u9644\u5e26\u82e5\u5e72\u76f8\u5173\u4f20\u8bf4\u5361\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">FABLED_BUNDLES = {<br>    \"SYLVANAS_RANGER_GENERAL\": [\"RANGER_CAPTAIN_ALLERIA\", \"RANGER_INITIATE_VEREESA\"],<br>}<br><br>def build_deck_with_fabled(cards: list[str]) -&gt; list[str]:<br>    out = list(cards)<br>    for c in cards:<br>        out.extend(FABLED_BUNDLES.get(c, []))<br>    return out<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u53c8\u4e00\u4e2a\u6784\u7b51\u671f\u673a\u5236<\/strong>\uff0c\u96f6\u5bf9\u5c40\u5f15\u64ce\u6539\u52a8\u3002\u4f46\u5b83\u6709\u4e00\u4e2a UX \u540e\u679c\uff1a\u724c\u5e93\u4e0d\u518d\u662f 30 \u5f20\uff0c\u800c\u662f 30 + N\u3002\u6240\u4ee5\u75b2\u52b3\u8ba1\u7b97\u3001&#8221;\u4f60\u7684\u724c\u5e93\u4e2d&#8221;\u7c7b\u67e5\u8be2\u3001\u300a\u96f7\u8bfa\u300b\u7c7b&#8221;\u65e0\u91cd\u590d&#8221;\u5224\u5b9a\u5168\u90fd\u8981\u8003\u8651\u8fd9\u4e9b\u9644\u5e26\u5361\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 26 \u7ae0 2026\uff1a\u5148\u9a71\u3001\u788e\u88c2\u3001\u51c6\u5907<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">26.1 \u5927\u707e\u53d8\uff082026.03\uff09\u2014\u2014\u5148\u9a71\u3001\u788e\u88c2<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u6765\u6e90\uff1a<a href=\"https:\/\/www.hearthstonetopdecks.com\/cataclysm-expansion-guide\/\">HearthstoneTopDecks \u8d44\u6599\u7247\u6307\u5357<\/a>\u3001<a href=\"https:\/\/hearthstone.wiki.gg\/wiki\/CATACLYSM\">Hearthstone Wiki<\/a><\/p>\n\n\n\n<h4 class=\"wp-block-heading\">\u5148\u9a71\uff08Herald\uff09\u2605<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\uff08\u6b7b\u4ea1\u9a91\u58eb\u3001\u6076\u9b54\u730e\u624b\u3001\u76d7\u8d3c\u3001\u8428\u6ee1\u3001\u672f\u58eb\u3001\u6218\u58eb\uff09\u4f7f\u7528\u5148\u9a71\u724c\u65f6\uff1a\u53ec\u5524\u4e00\u4e2a\u4f60\u804c\u4e1a\u5de8\u578b\u968f\u4ece\u7684&#8221;\u58eb\u5175&#8221;\uff0c\u5e76\u63a8\u8fdb\u5de8\u578b\/\u6b7b\u4ea1\u4e4b\u7ffc\u7684\u5347\u7ea7\u8fdb\u5ea6\u3002\u6bcf 2 \u6b21\u5148\u9a71\u89e6\u53d1\u4e00\u6b21\u5347\u7ea7\uff0c\u5171\u4e24\u6b21\u5347\u7ea7\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class Herald(Action):<br>    def run(self, game, ctx):<br>        p = game.player(ctx.controller)<br>        p.herald_count += 1<br><br>        # 1. \u53ec\u5524\u58eb\u5175\uff08\u7ee7\u627f\u5de8\u578b\u67d0\u4e2a\u90e8\u4ef6\u7684\u80fd\u529b\uff09<br>        colossal_id = CLASS_COLOSSAL[p.klass]<br>        soldier_id  = CLASS_HERALD_SOLDIER[p.klass]<br>        soldier = game.summon(soldier_id, ctx.controller)<br>        if soldier:<br>            self._apply_upgrade_level(game, soldier, p.herald_upgrade_level)<br><br>        # 2. \u6bcf 2 \u6b21\u63a8\u8fdb\u4e00\u7ea7\uff08\u6700\u591a 2 \u7ea7\uff09<br>        if p.herald_count % 2 == 0 and p.herald_upgrade_level &lt; 2:<br>            p.herald_upgrade_level += 1<br>            self._upgrade_all(game, ctx.controller, p.herald_upgrade_level)<br><br>    @staticmethod<br>    def _upgrade_all(game, pid, level):<br>        \"\"\"\u5347\u7ea7\u624b\u724c\/\u724c\u5e93\/\u6218\u573a\u4e0a\u7684\u5de8\u578b\u3001\u6b7b\u4ea1\u4e4b\u7ffc\uff0c\u4ee5\u53ca\u5df2\u53ec\u5524\u7684\u58eb\u5175\"\"\"<br>        for e in game.all_entities_of(pid):<br>            if e[Tag.HERALD_UPGRADEABLE]:<br>                transform_keeping_position(e, e.defn.herald_upgrades[level - 1])<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5148\u9a71 = \u63a2\u5e95\uff082019\uff09+ \u704c\u6ce8\uff082025\uff09+ \u5de8\u578b\uff082022\uff09<\/strong>\u3002\u4e09\u4e2a\u5df2\u6709\u6a21\u5f0f\u7684\u7ec4\u5408\uff1a\u8de8\u533a\u57df\u8ba1\u6570\u5668\u3001\u9636\u6bb5\u6027\u5347\u7ea7\u3001\u591a\u5b9e\u4f53\u4e3b\u4f53\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5de5\u7a0b\u4e0a\u552f\u4e00\u7684\u65b0\u4e1c\u897f<\/strong>\u662f&#8221;\u8de8\u533a\u57df\u6279\u91cf\u5347\u7ea7&#8221;\u2014\u2014\u8981\u540c\u65f6\u5347\u7ea7\u624b\u724c\u3001\u724c\u5e93\u3001\u6218\u573a\u3001\u751a\u81f3\u5893\u5730\u91cc\u7684\u5b9e\u4f53\u3002\u8fd9\u9700\u8981 <code>all_entities_of(pid)<\/code> \u80fd\u904d\u5386\u6240\u6709\u533a\u57df\uff0c\u4e14 <code>transform<\/code> \u80fd\u5728\u4efb\u4f55\u533a\u57df\u5de5\u4f5c\uff08\u6cd5\u672f\u77f3\u5728 2017 \u5e74\u5c31\u505a\u5230\u4e86\uff09\u3002<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">\u788e\u88c2\uff08Shatter\uff09\u2605<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\uff08\u5fb7\u9c81\u4f0a\u3001\u730e\u4eba\u3001\u6cd5\u5e08\u3001\u5723\u9a91\u58eb\u3001\u7267\u5e08\uff09\u5f53\u4f60\u62bd\u5230\u8fd9\u5f20\u724c\u65f6\uff0c\u5b83\u5206\u88c2\u6210\u4e24\u4e2a\u8f83\u5f31\u7684\u7248\u672c\uff0c\u5206\u522b\u653e\u5728\u4f60\u624b\u724c\u7684\u4e24\u7aef\u3002\u5f53\u5b83\u4eec\u76f8\u90bb\u65f6\uff0c\u5408\u5e76\u56de\u5b8c\u6574\u7248\u672c\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8fd9\u662f\u6211\u89c1\u8fc7\u7684\u6700&#8221;\u4f4d\u7f6e\u654f\u611f&#8221;\u7684\u5173\u952e\u8bcd<\/strong>\uff0c\u5b83\u628a 2020 \u5e74\u300a\u6d41\u653e\u300b\u5f15\u5165\u7684&#8221;\u624b\u724c\u4f4d\u7f6e\u5373\u72b6\u6001&#8221;\u63a8\u5230\u4e86\u6781\u81f4\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class Keyword_Shatter:<br>    @staticmethod<br>    def on_drawn(game, card):<br>        \"\"\"\u62bd\u5230\u65f6\u5206\u88c2\"\"\"<br>        pid = card[Tag.CONTROLLER]<br>        hand = game.zones.get(pid, Zone.HAND)<br>        left_id, right_id = card.defn.shatter_halves<br>        game.remove_entity(card)<br><br>        left  = game.create_entity(left_id, pid, zone=Zone.SETASIDE)<br>        right = game.create_entity(right_id, pid, zone=Zone.SETASIDE)<br>        left.shatter_pair_id  = right.id<br>        right.shatter_pair_id = left.id<br>        left.shatter_origin = right.shatter_origin = card[Tag.CARD_ID]<br><br>        # \u2605 \u4e00\u4e2a\u653e\u6700\u5de6\uff0c\u4e00\u4e2a\u653e\u6700\u53f3<br>        game.zones.move(left,  Zone.HAND, position=0)<br>        game.zones.move(right, Zone.HAND, position=len(hand))<br><br>    @staticmethod<br>    def check_merge(game, pid):<br>        \"\"\"\u6bcf\u6b21\u624b\u724c\u53d8\u5316\u540e\u68c0\u67e5\u662f\u5426\u6709\u76f8\u90bb\u7684\u788e\u7247\u5bf9\"\"\"<br>        hand = game.zones.get(pid, Zone.HAND)<br>        for i in range(len(hand) - 1):<br>            a, b = hand[i], hand[i + 1]<br>            if getattr(a, \"shatter_pair_id\", None) == b.id:<br>                pos = i<br>                origin = a.shatter_origin<br>                game.remove_entity(a)<br>                game.remove_entity(b)<br>                merged = game.create_entity(origin, pid, zone=Zone.SETASIDE)<br>                game.zones.move(merged, Zone.HAND, position=pos)<br>                game.emit(Event.SHATTER_MERGED, entity=merged)<br>                return True<br>        return False<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>check_merge<\/code> \u5fc5\u987b\u5728\u6bcf\u6b21\u624b\u724c\u53d8\u5316\u540e\u8c03\u7528<\/strong>\uff1a\u62bd\u724c\u3001\u51fa\u724c\u3001\u751f\u6210\u724c\u3001\u5f03\u724c\u3001\u4ea4\u6613\u3001\u51c6\u5907\u2026\u2026\u8fd9\u662f\u4e00\u4e2a\u5178\u578b\u7684&#8221;<strong>\u4e0d\u53d8\u5f0f\u7ef4\u62a4<\/strong>&#8220;\u95ee\u9898\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u6b63\u786e\u7684\u505a\u6cd5\u4e0d\u662f\u5728\u6bcf\u4e2a\u624b\u724c\u64cd\u4f5c\u540e\u624b\u52a8\u8c03\u7528\uff0c\u800c\u662f<strong>\u628a\u5b83\u505a\u6210\u624b\u724c\u533a\u57df\u7684\u4e00\u4e2a\u4e0d\u53d8\u5f0f\u94a9\u5b50<\/strong>\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class ZoneManager:<br>    ZONE_INVARIANTS = {<br>        Zone.HAND: [Keyword_Shatter.check_merge],<br>    }<br><br>    def move(self, e, to_zone, position=None):<br>        ...<br>        # \u79fb\u52a8\u5b8c\u6210\u540e\uff0c\u68c0\u67e5\u53d7\u5f71\u54cd\u533a\u57df\u7684\u4e0d\u53d8\u5f0f<br>        for zone in {src, to_zone}:<br>            for inv in self.ZONE_INVARIANTS.get(zone, ()):<br>                while inv(self.game, pid):     # \u5faa\u73af\u76f4\u5230\u7a33\u5b9a<br>                    pass<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8fd9\u662f\u5f15\u64ce\u8bbe\u8ba1\u7684\u4e00\u6761\u91cd\u8981\u7ecf\u9a8c<\/strong>\uff1a\u4e0e\u5176\u5728 N \u4e2a\u8c03\u7528\u70b9\u624b\u52a8\u7ef4\u62a4\u4e0d\u53d8\u5f0f\uff0c\u4e0d\u5982\u5728\u72b6\u6001\u53d8\u5316\u7684<strong>\u552f\u4e00\u5165\u53e3<\/strong>\u505a\u7edf\u4e00\u68c0\u67e5\u3002\u524d\u8005\u968f\u7740\u5361\u6c60\u589e\u957f\u4f1a\u6f0f\u6389\u67d0\u4e2a\u8def\u5f84\uff0c\u540e\u8005\u6c38\u8fdc\u6b63\u786e\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u788e\u88c2\u7684\u7b56\u7565\u6df1\u5ea6<\/strong>\uff1a\u56e0\u4e3a\u624b\u724c\u662f\u4ece\u53f3\u4fa7\u6dfb\u52a0\u7684\uff0c\u4f60\u6253\u51fa\u4e2d\u95f4\u7684\u724c\u4f1a\u8ba9\u4e24\u534a\u9760\u8fd1\u3002\u8fd9\u521b\u9020\u4e86\u4e00\u4e2a&#8221;\u624b\u724c\u7ba1\u7406&#8221;\u7684\u65b0\u7ef4\u5ea6\u2014\u2014\u73a9\u5bb6\u8981\u4e3b\u52a8\u89c4\u5212\u51fa\u724c\u987a\u5e8f\u6765\u5408\u5e76\u788e\u7247\u3002\u540c\u65f6\uff0c\u5b83\u4e5f<strong>\u60e9\u7f5a\u4e86\u6ee1\u624b\u724c<\/strong>\uff08\u788e\u7247\u88ab\u9694\u5f97\u5f88\u8fdc\uff09\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u788e\u88c2\u7684\u5b9e\u73b0\u9677\u9631<\/strong>\uff1a<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>\u4e24\u534a\u5728\u624b\u724c\u4e0a\u9650\u8ba1\u7b97\u91cc\u7b97<strong>\u4e24\u5f20\u724c<\/strong><\/li>\n\n\n\n<li>\u5355\u72ec\u6253\u51fa\u4e00\u534a\u662f\u5408\u6cd5\u7684\uff08\u8f83\u5f31\u7248\u672c\uff09\uff0c\u53e6\u4e00\u534a\u4f1a\u6c38\u4e45\u7559\u4e0b<\/li>\n\n\n\n<li>\u590d\u5236\u4e00\u534a \u2192 \u590d\u5236\u54c1\u7684 <code>shatter_pair_id<\/code> \u6307\u5411\u5df2\u7ecf\u914d\u5bf9\u7684\u90a3\u4e2a\uff0c\u4f1a\u5bfc\u81f4\u9519\u8bef\u5408\u5e76\uff08\u9700\u8981\u5728\u590d\u5236\u65f6\u6e05\u9664\u914d\u5bf9\u5173\u7cfb\uff09<\/li>\n\n\n\n<li>\u88ab\u5bf9\u624b\u5077\u8d70\u4e00\u534a\uff08\u300a\u5fc3\u7075\u5c16\u5578\u300b\u7c7b\uff09\u2192 \u914d\u5bf9\u65ad\u88c2<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">26.2 \u7d2b\u7f57\u5170\u76d1\u72f1\u8d8a\u72f1\uff082026.07\uff09\u2014\u2014\u51c6\u5907\u3001\u7834\u89c4\u4f20\u8bf4<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u6765\u6e90\uff1a<a href=\"https:\/\/hearthstone.wiki.gg\/wiki\/Prepare\">Hearthstone Wiki: Prepare<\/a>\u3001<a href=\"https:\/\/news.blizzard.com\/en-us\/article\/24276664\/break-the-rules-in-escape-from-violet-hold-hearthstones-next-expansion\">\u5b98\u65b9\u516c\u544a<\/a><\/p>\n\n\n\n<h4 class=\"wp-block-heading\">\u51c6\u5907\uff08Prepare\uff09\u2605<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6587\u672c<\/strong>\uff1a\u62d6\u5230\u4f60\u7684\u724c\u5e93\u4ee5\u6d88\u8017\u5269\u4f59\u6cd5\u529b\u503c\u3002\u4e0b\u4e2a\u56de\u5408\uff0c\u8fd9\u5f20\u724c\u7684\u8d39\u7528\u964d\u4f4e\u4f60\u6d88\u8017\u7684\u91cf\uff08\u989d\u5916\u518d\u51cf 1\uff09\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def prepare_card(game, card: Entity, mana_to_spend: int) -&gt; bool:<br>    pid = card[Tag.CONTROLLER]<br>    p = game.player(pid)<br><br>    if not card.has(Tag.PREPARE):        return False<br>    if card[Tag.PREPARED]:               return False   # \u53ea\u80fd\u51c6\u5907\u4e00\u6b21<br>    if mana_to_spend &lt; 1:                return False<br>    if p[Tag.MANA_AVAILABLE] &lt; mana_to_spend: return False<br><br>    current_cost = game.compute_cost(card)<br>    # \u2605 \u6d88\u8017\u91cf\u4e0d\u8d85\u8fc7\u628a\u8d39\u7528\u51cf\u5230 0 \u6240\u9700<br>    mana_to_spend = min(mana_to_spend, max(0, current_cost - 1))<br>    if mana_to_spend &lt; 1: return False<br><br>    p[Tag.MANA_AVAILABLE] -= mana_to_spend<br>    card[Tag.PREPARE_DISCOUNT] = mana_to_spend + 1     # \u989d\u5916 +1<br>    card[Tag.PREPARED] = 1<br>    card[Tag.LOCKED_UNTIL_TURN] = game.turn + 1        # \u2605 \u672c\u56de\u5408\u4e0d\u80fd\u518d\u6253\u51fa<br>    game.emit(Event.CARD_PREPARED, entity=card, amount=mana_to_spend)<br>    # \u5361\u724c\u7acb\u5373\u56de\u5230\u624b\u724c\u539f\u4f4d\u7f6e\uff08\u89c6\u89c9\u4e0a\u662f\"\u62d6\u51fa\u53bb\u53c8\u5f39\u56de\u6765\"\uff09<br>    return True<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u51c6\u5907\u7684\u89c4\u5219\u7ec6\u8282<\/strong>\uff08\u6765\u81ea Wiki\uff09\uff1a<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>\u51c6\u5907\u4e0d\u7b97&#8221;\u6253\u51fa\u4e00\u5f20\u724c&#8221;<\/strong> \u2192 \u4e0d\u89e6\u53d1\u8fde\u51fb\u3001\u4e0d\u89e6\u53d1&#8221;\u4f7f\u7528\u4e00\u5f20\u724c\u540e&#8221;\u3001\u4e0d\u63a8\u8fdb\u4efb\u52a1<\/li>\n\n\n\n<li>\u5361\u724c<strong>\u56de\u5230\u624b\u724c\u539f\u4f4d\u7f6e<\/strong> \u2192 \u4e0d\u5f71\u54cd\u6d41\u653e\/\u788e\u88c2\u7684\u4f4d\u7f6e\u5224\u5b9a<\/li>\n\n\n\n<li>\u9700\u8981<strong>\u81f3\u5c11 1 \u70b9\u6cd5\u529b\u503c<\/strong><\/li>\n\n\n\n<li>\u6d88\u8017\u91cf<strong>\u4e0d\u8d85\u8fc7<\/strong>\u628a\u8d39\u7528\u51cf\u5230 0 \u6240\u9700\u7684\u91cf<\/li>\n\n\n\n<li>\u6bcf\u5f20\u724c<strong>\u53ea\u80fd\u51c6\u5907\u4e00\u6b21<\/strong><\/li>\n\n\n\n<li>\u6298\u6263\u5728<strong>\u88ab\u590d\u5236\u6216\u88ab\u5077\u53d6<\/strong>\u65f6\u4fdd\u7559<\/li>\n\n\n\n<li>\u6298\u6263\u5728<strong>\u88ab\u6d17\u5165\u724c\u5e93<\/strong>\u6216<strong>\u6253\u51fa\u540e\u8fd4\u56de\u624b\u724c<\/strong>\u65f6\u91cd\u7f6e<\/li>\n\n\n\n<li>\u51c6\u5907\u540e<strong>\u672c\u56de\u5408\u9501\u5b9a\u5728\u624b\u4e2d<\/strong><\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5de5\u7a0b\u5206\u6790<\/strong>\uff1a\u51c6\u5907\u662f<strong>\u7b2c\u56db\u4e2a\u975e\u51fa\u724c\u624b\u724c\u52a8\u4f5c<\/strong>\uff08\u524d\u4e09\u4e2a\uff1a\u6253\u51fa\u3001\u5f03\u6389\u3001\u4ea4\u6613\uff09\uff0c\u4e5f\u662f<strong>\u7b2c\u4e8c\u4e2a\u6d88\u8017\u6cd5\u529b\u4f46\u4e0d\u6253\u51fa\u724c<\/strong>\u7684\u52a8\u4f5c\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u5b83\u7684\u7b2c 6\u30017 \u6761\u89c4\u5219\u63ed\u793a\u4e86\u4e00\u4e2a\u5b9e\u73b0\u9009\u62e9\uff1a<code>PREPARE_DISCOUNT<\/code> \u662f<strong>\u9644\u9b54<\/strong>\u800c\u4e0d\u662f\u7eaf tag\u3002\u56e0\u4e3a\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u590d\u5236\u4fdd\u7559 \u2192 \u8bf4\u660e\u5b83\u8ddf\u7740\u5b9e\u4f53\u8d70\uff08FULL \u590d\u5236\u6a21\u5f0f\u4f1a\u590d\u5236\u9644\u9b54\uff09<\/li>\n\n\n\n<li>\u6d17\u5165\u724c\u5e93\u91cd\u7f6e \u2192 \u8bf4\u660e\u6709\u4e00\u4e2a&#8221;\u79bb\u5f00\u624b\u724c\u65f6\u79fb\u9664&#8221;\u7684\u6301\u7eed\u65f6\u95f4<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-preformatted\">ENCH_PREPARE_DISCOUNT = CardDef(<br>    card_id=\"VH_PREPAREe\", card_type=\"ENCHANTMENT\",<br>    duration=Duration.WHILE_IN_HAND,       # \u79bb\u5f00\u624b\u724c\u5373\u5931\u6548<br>    enchant_tags={Tag.COST: -1},           # amount \u52a8\u6001\u8bbe\u7f6e<br>)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u51c6\u5907\u7684\u8bbe\u8ba1\u610f\u56fe<\/strong>\uff1a\u5b83\u89e3\u51b3\u4e86\u7089\u77f3\u5341\u4e8c\u5e74\u7684\u4e00\u4e2a\u8001\u95ee\u9898\u2014\u2014<strong>\u56de\u5408\u672b\u7684\u96f6\u6563\u6cd5\u529b\u503c\u6d6a\u8d39<\/strong>\u3002\u5728\u6b64\u4e4b\u524d\uff0c\u5269 2 \u70b9\u6cd5\u529b\u6ca1\u724c\u53ef\u6253\u5c31\u662f\u7eaf\u6d6a\u8d39\u3002\u51c6\u5907\u628a\u8fd9\u4e9b\u788e\u7247\u8d44\u6e90\u53d8\u6210\u4e86\u672a\u6765\u7684\u6298\u6263\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u4ece\u5e73\u8861\u89d2\u5ea6\uff0c\u5b83\u662f\u4e00\u4e2a<strong>&#8220;\u5e73\u6ed1\u66f2\u7ebf&#8221;\u673a\u5236<\/strong>\uff1a\u8ba9\u624b\u91cc\u7684\u9ad8\u8d39\u5361\u63d0\u524d\u4e00\u5230\u4e24\u56de\u5408\u4e0a\u573a\uff0c\u4f46\u4ee3\u4ef7\u662f\u63d0\u524d\u6295\u5165\u3002\u8fd9\u7c7b\u673a\u5236\u5728\u8bbe\u8ba1\u4e0a\u5f88\u5b89\u5168\uff08\u4e0d\u4ea7\u751f\u989d\u5916\u8d44\u6e90\uff0c\u53ea\u662f\u65f6\u95f4\u8f6c\u79fb\uff09\uff0c\u6240\u4ee5\u6211\u9884\u671f\u5b83\u4f1a\u6210\u4e3a\u5e38\u9752\u673a\u5236\u3002<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">\u7834\u89c4\u4f20\u8bf4\uff08Rulebreaker Legendaries\uff09<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">\u6bcf\u4e2a\u804c\u4e1a\u4e00\u5f20&#8221;\u626d\u66f2\u6838\u5fc3\u89c4\u5219&#8221;\u7684\u4f20\u8bf4\u968f\u4ece\u3002<strong>\u8fd9\u662f\u5f15\u64ce\u8bbe\u8ba1\u7684\u7ec8\u6781\u538b\u529b\u6d4b\u8bd5<\/strong>\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u4ece\u516c\u5f00\u4fe1\u606f\u770b\uff0c\u8fd9\u7c7b\u5361\u5305\u62ec\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>\u5728\u5bf9\u624b\u573a\u4e0a\u6253\u51fa\u968f\u4ece<\/strong>\uff08\u4f2a\u88c5\u968f\u4ece\uff09\u2014\u2014\u8fd9\u8981\u6c42 <code>play_card<\/code> \u7684 <code>controller<\/code> \u53c2\u6570\u80fd\u6307\u5411\u5bf9\u624b<\/li>\n\n\n\n<li><strong>\u8d3f\u8d42\u5361<\/strong>\uff1a\u7ed9\u81ea\u5df1\u5f3a\u529b\u6548\u679c\uff0c\u540c\u65f6\u7ed9\u5bf9\u624b\u4e00\u70b9\u8865\u507f<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>&#8220;\u5728\u5bf9\u624b\u573a\u4e0a\u6253\u51fa\u968f\u4ece&#8221;<\/strong> \u662f\u4e00\u4e2a\u771f\u6b63\u7684\u65b0\u80fd\u529b\u3002\u5b83\u8981\u6c42\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def play_card(self, card, target=None, position=None,<br>              summon_side: int | None = None):     # \u2605 \u65b0\u53c2\u6570<br>    pid = card[Tag.CONTROLLER]<br>    side = summon_side if summon_side is not None else pid<br>    ...<br>    ok = self.zones.move(card, Zone.PLAY, position, owner_side=side)<br>    card[Tag.CONTROLLER] = side      # \u2605 \u63a7\u5236\u6743\u5f52\u5bf9\u624b<br>    card[Tag.OWNER] = pid            # \u2605 \u4f46\u62e5\u6709\u8005\u8fd8\u662f\u6211<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>CONTROLLER<\/code> \u4e0e <code>OWNER<\/code> \u5206\u79bb\u7684\u8bbe\u8ba1\uff08\u7b2c 1 \u7ae0\u5c31\u5b9a\u4e49\u4e86\uff09\u5728\u8fd9\u91cc\u7ec8\u4e8e\u5151\u73b0\u4e86\u5168\u90e8\u4ef7\u503c\u3002<strong>\u5341\u4e8c\u5e74\u524d\u5b9a\u4e49\u7684\u4e00\u4e2a tag\uff0c\u8ba9 2026 \u5e74\u7684\u65b0\u673a\u5236\u4e0d\u9700\u8981\u6539\u5f15\u64ce\u3002<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u8fd9\u662f\u6211\u5728\u6574\u4efd\u6587\u6863\u91cc\u6700\u60f3\u5f3a\u8c03\u7684\u4e00\u6761\uff1a<strong>\u597d\u7684\u5efa\u6a21\u7684\u4ef7\u503c\u4e0d\u5728\u4e8e\u5b83\u4eca\u5929\u89e3\u51b3\u4e86\u4ec0\u4e48\u95ee\u9898\uff0c\u800c\u5728\u4e8e\u5b83\u8ba9\u5341\u5e74\u540e\u7684\u4f60\u4e0d\u9700\u8981\u91cd\u5199\u3002<\/strong><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 27 \u7ae0 \u5173\u952e\u8bcd\u7684\u516d\u5927\u8303\u5f0f\uff1a\u4e00\u4e2a\u6b63\u4ea4\u5206\u89e3<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">\u770b\u5b8c\u516d\u5341\u4e2a\u5173\u952e\u8bcd\uff0c\u53ef\u4ee5\u505a\u4e00\u6b21\u5f52\u7eb3\u3002\u6240\u6709\u7089\u77f3\u5173\u952e\u8bcd\u90fd\u843d\u5165\u516d\u4e2a\u8303\u5f0f\u4e4b\u4e00\uff08\u6216\u5b83\u4eec\u7684\u7ec4\u5408\uff09\uff1a<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u8303\u5f0f\u4e00\uff1a\u9759\u6001\u6807\u7b7e\uff08Static Tag\uff09<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5b9a\u4e49<\/strong>\uff1a\u4e00\u4e2a\u5e03\u5c14\u6216\u6574\u6570 tag\uff0c\u5728<strong>\u67e5\u8be2\u65f6<\/strong>\u88ab\u89c4\u5219\u6d88\u8d39\u3002\u4e0d\u4e3b\u52a8\u505a\u4efb\u4f55\u4e8b\u3002<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u5173\u952e\u8bcd<\/th><th>\u6d88\u8d39\u70b9<\/th><\/tr><\/thead><tbody><tr><td>\u5632\u8bbd<\/td><td><code>is_legal_attack_target<\/code><\/td><\/tr><tr><td>\u51b2\u950b \/ \u7a81\u88ad<\/td><td><code>can_attack<\/code> \/ <code>is_legal_attack_target<\/code><\/td><\/tr><tr><td>\u6f5c\u884c<\/td><td>\u653b\u51fb\u76ee\u6807 + \u6cd5\u672f\u76ee\u6807<\/td><\/tr><tr><td>\u5723\u76fe<\/td><td>\u4f24\u5bb3\u7ba1\u7ebf<\/td><\/tr><tr><td>\u5267\u6bd2<\/td><td>\u4f24\u5bb3\u540e\u5904\u7406<\/td><\/tr><tr><td>\u5438\u8840<\/td><td>\u4f24\u5bb3\u540e\u5904\u7406<\/td><\/tr><tr><td>\u98ce\u6012<\/td><td><code>can_attack<\/code><\/td><\/tr><tr><td>\u51bb\u7ed3<\/td><td><code>can_attack<\/code><\/td><\/tr><tr><td>\u514d\u75ab<\/td><td>\u4f24\u5bb3\u7ba1\u7ebf + \u76ee\u6807<\/td><\/tr><tr><td>\u4e0d\u53ef\u9009\u4e2d<\/td><td>\u76ee\u6807\u5408\u6cd5\u6027<\/td><\/tr><tr><td>\u590d\u751f<\/td><td>\u6b7b\u4ea1\u7ed3\u7b97<\/td><\/tr><tr><td>\u6cd5\u672f\u4f24\u5bb3<\/td><td>\u4f24\u5bb3\u8ba1\u7b97<\/td><\/tr><tr><td>\u4f11\u7720<\/td><td>\u5168\u65b9\u4f4d<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5b9e\u73b0\u6210\u672c<\/strong>\uff1a\u6781\u4f4e\uff08\u4e00\u4e2a tag + \u4e00\u5904\u5224\u65ad\uff09 <strong>\u7ec4\u5408\u6027<\/strong>\uff1a\u5b8c\u7f8e\uff08tag \u5929\u7136\u53ef\u53e0\u52a0\uff09 <strong>\u5360\u6bd4<\/strong>\uff1a\u7ea6 25% \u7684\u5173\u952e\u8bcd<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8bbe\u8ba1\u7ecf\u9a8c<\/strong>\uff1a\u80fd\u505a\u6210\u9759\u6001\u6807\u7b7e\u7684\uff0c\u5c31\u4e0d\u8981\u505a\u6210\u89e6\u53d1\u5668\u3002\u9759\u6001\u6807\u7b7e\u96f6\u8fd0\u884c\u65f6\u5f00\u9500\uff0c\u4e14\u4e0d\u4f1a\u6709\u65f6\u5e8f\u95ee\u9898\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u8303\u5f0f\u4e8c\uff1a\u89e6\u53d1\u5668\uff08Trigger\uff09<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5b9a\u4e49<\/strong>\uff1a\u8ba2\u9605\u4e00\u4e2a\u4e8b\u4ef6\uff0c\u6ee1\u8db3\u6761\u4ef6\u65f6\u6267\u884c\u4e00\u4e2a Action\u3002<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u5173\u952e\u8bcd<\/th><th>\u4e8b\u4ef6<\/th><th>\u6761\u4ef6<\/th><th>\u4e00\u6b21\u6027<\/th><\/tr><\/thead><tbody><tr><td>\u6218\u543c<\/td><td><code>PLAY_CARD<\/code><\/td><td>source_only<\/td><td>\u2014<\/td><\/tr><tr><td>\u4ea1\u8bed<\/td><td><code>DEATH<\/code><\/td><td>source_only<\/td><td>\u2014<\/td><\/tr><tr><td>\u52b1\u5fd7<\/td><td><code>HERO_POWER_USED<\/code><\/td><td>\u5df1\u65b9<\/td><td>\u2717<\/td><\/tr><tr><td>\u8d85\u6740<\/td><td><code>DAMAGE<\/code><\/td><td>\u6211\u9020\u6210 &amp; \u6ea2\u51fa &amp; \u6211\u7684\u56de\u5408<\/td><td>\u2717<\/td><\/tr><tr><td>\u8363\u8a89\u51fb\u6740<\/td><td><code>DAMAGE<\/code><\/td><td>\u6211\u9020\u6210 &amp; \u6070\u597d\u81f4\u6b7b<\/td><td>\u2717<\/td><\/tr><tr><td>\u72c2\u4e71<\/td><td><code>DAMAGE<\/code><\/td><td>\u6253\u5230\u6211 &amp; \u6211\u6d3b\u7740<\/td><td>\u2713<\/td><\/tr><tr><td>\u6cd5\u672f\u8ff8\u53d1<\/td><td><code>AFTER_SPELL<\/code><\/td><td>\u5df1\u65b9<\/td><td>\u2713<\/td><\/tr><tr><td>\u8fc7\u91cf\u6cbb\u7597<\/td><td><code>OVERHEAL<\/code><\/td><td>\u6253\u5230\u6211<\/td><td>\u2717<\/td><\/tr><tr><td>\u5f00\u5c40\u65f6<\/td><td><code>GAME_START<\/code><\/td><td>\u5728\u724c\u5e93<\/td><td>\u2713<\/td><\/tr><tr><td>\u62bd\u5230\u65f6\u65bd\u653e<\/td><td><code>DRAW<\/code><\/td><td>source_only<\/td><td>\u2713<\/td><\/tr><tr><td>\u5feb\u901f\u5c04\u51fb<\/td><td><code>PLAY_CARD<\/code><\/td><td>\u672c\u56de\u5408\u62bd\u5230<\/td><td>\u2014<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5b9e\u73b0\u6210\u672c<\/strong>\uff1a\u4f4e\uff08\u4e00\u4e2a TriggerDef\uff09 <strong>\u7ec4\u5408\u6027<\/strong>\uff1a\u597d\uff0c\u4f46\u6709\u65f6\u5e8f\u95ee\u9898 <strong>\u5360\u6bd4<\/strong>\uff1a\u7ea6 35%<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8bbe\u8ba1\u7ecf\u9a8c<\/strong>\uff1a\u4e8b\u4ef6\u4e0a\u4e0b\u6587\u8981\u643a\u5e26\u5c3d\u53ef\u80fd\u591a\u7684\u4fe1\u606f\uff08<code>amount<\/code>\u3001<code>target_health_before<\/code>\u3001<code>fatal<\/code>\u3001<code>paid_cost<\/code>\uff09\uff0c\u8fd9\u6837\u65b0\u5173\u952e\u8bcd\u53ea\u9700\u8981\u52a0\u4e00\u4e2a Condition\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u8303\u5f0f\u4e09\uff1a\u51fa\u724c\u6d41\u7a0b\u94a9\u5b50\uff08Play Hook\uff09<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5b9a\u4e49<\/strong>\uff1a\u5728 <code>play_card<\/code> \u7684\u67d0\u4e2a\u9636\u6bb5\u63d2\u5165\u903b\u8f91\uff0c\u6216\u8005\u589e\u52a0\u4e00\u4e2a\u5168\u65b0\u7684\u624b\u724c\u52a8\u4f5c\u3002<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u5173\u952e\u8bcd<\/th><th>\u94a9\u5b50\u4f4d\u7f6e<\/th><\/tr><\/thead><tbody><tr><td>\u8fc7\u8f7d<\/td><td>\u6253\u51fa\u540e\uff08\u8d44\u6e90\uff09<\/td><\/tr><tr><td>\u8fde\u51fb<\/td><td>\u6253\u51fa\u524d\uff08\u6761\u4ef6\u5206\u652f\uff09<\/td><\/tr><tr><td>\u6289\u62e9<\/td><td>\u6253\u51fa\u524d\uff08\u9009\u9879\uff09<\/td><\/tr><tr><td>\u56de\u54cd<\/td><td>\u6253\u51fa\u540e\uff08\u751f\u6210\u526f\u672c\uff09<\/td><\/tr><tr><td>\u53cc\u751f\u6cd5\u672f<\/td><td>\u6253\u51fa\u540e\uff08\u751f\u6210\u526f\u672c\uff09<\/td><\/tr><tr><td>\u6d41\u653e<\/td><td>\u6253\u51fa\u524d\uff08\u4f4d\u7f6e\u6761\u4ef6\uff09<\/td><\/tr><tr><td>\u7ec8\u66f2<\/td><td>\u6263\u8d39\u540e\uff08\u6cd5\u529b\u6761\u4ef6\uff09<\/td><\/tr><tr><td>\u6cd5\u529b\u6e34\u6c42<\/td><td>\u6253\u51fa\u524d\uff08\u5386\u53f2\u6761\u4ef6\uff09<\/td><\/tr><tr><td><strong>\u53ef\u4ea4\u6613<\/strong><\/td><td><strong>\u65b0\u52a8\u4f5c<\/strong><\/td><\/tr><tr><td><strong>\u953b\u9020<\/strong><\/td><td><strong>\u65b0\u52a8\u4f5c<\/strong><\/td><\/tr><tr><td><strong>\u51c6\u5907<\/strong><\/td><td><strong>\u65b0\u52a8\u4f5c<\/strong><\/td><\/tr><tr><td><strong>\u4f7f\u7528\u5730\u6807<\/strong><\/td><td><strong>\u65b0\u52a8\u4f5c<\/strong><\/td><\/tr><tr><td><strong>\u6cf0\u5766\u6280\u80fd<\/strong><\/td><td><strong>\u65b0\u52a8\u4f5c<\/strong><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5b9e\u73b0\u6210\u672c<\/strong>\uff1a\u4e2d\uff08\u8981\u6539\u4e3b\u6d41\u7a0b\uff09 <strong>\u7ec4\u5408\u6027<\/strong>\uff1a\u5dee\uff08\u94a9\u5b50\u4e4b\u95f4\u53ef\u80fd\u51b2\u7a81\uff09 <strong>\u5360\u6bd4<\/strong>\uff1a\u7ea6 20%<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8bbe\u8ba1\u7ecf\u9a8c<\/strong>\uff1a<strong>&#8220;\u65b0\u52a8\u4f5c\u7c7b\u578b&#8221;\u662f\u6700\u8d35\u7684\u5173\u952e\u8bcd<\/strong>\u3002\u5b83\u5f71\u54cd UI\u3001\u534f\u8bae\u3001AI \u641c\u7d22\u7a7a\u95f4\u3002\u7089\u77f3\u5341\u4e8c\u5e74\u53ea\u52a0\u4e86\u4e94\u4e2a\uff0c\u975e\u5e38\u514b\u5236\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u8303\u5f0f\u56db\uff1a\u9009\u62e9\u578b\uff08Choice\uff09<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5b9a\u4e49<\/strong>\uff1a\u9700\u8981\u4e2d\u65ad\u7ed3\u7b97\uff0c\u7b49\u5f85\u73a9\u5bb6\u8f93\u5165\u3002<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u5173\u952e\u8bcd<\/th><th>\u6c60\u5b50<\/th><th>\u6570\u91cf<\/th><\/tr><\/thead><tbody><tr><td>\u53d1\u73b0<\/td><td>\u52a8\u6001\u67e5\u8be2<\/td><td>3<\/td><\/tr><tr><td>\u9002\u5e94<\/td><td>\u56fa\u5b9a 10<\/td><td>3<\/td><\/tr><tr><td>\u6253\u635e<\/td><td>\u724c\u5e93\u5e95 3<\/td><td>1\uff08\u5168\u5c55\u793a\uff09<\/td><\/tr><tr><td>\u6316\u6398<\/td><td>\u5206\u5c42\u6c60<\/td><td>1\uff08\u968f\u673a\uff0c\u4e0d\u9009\uff09<\/td><\/tr><tr><td>\u9ed1\u6697\u6069\u8d50<\/td><td>\u56fa\u5b9a\u6c60<\/td><td>3<\/td><\/tr><tr><td>\u6289\u62e9<\/td><td>\u56fa\u5b9a 2<\/td><td>1<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5b9e\u73b0\u6210\u672c<\/strong>\uff1a\u9ad8\uff08\u9700\u8981\u6302\u8d77\/\u6062\u590d\u673a\u5236\uff09 <strong>\u7ec4\u5408\u6027<\/strong>\uff1a\u5dee\uff08\u4e0d\u80fd\u5d4c\u5957\uff09 <strong>\u5360\u6bd4<\/strong>\uff1a\u7ea6 10%<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8bbe\u8ba1\u7ecf\u9a8c<\/strong>\uff1a\u7528 async\/\u534f\u7a0b\u5b9e\u73b0\u6302\u8d77\u70b9\uff0c\u4e0d\u8981\u7528\u5f02\u5e38\u6216\u663e\u5f0f\u72b6\u6001\u673a\u3002\u5e76\u4e14\u4e3a AI \u63d0\u4f9b\u4e00\u4e2a&#8221;\u7acb\u5373\u51b3\u7b56&#8221;\u7684\u5b9e\u73b0\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u8303\u5f0f\u4e94\uff1a\u72b6\u6001\u673a \/ \u8ba1\u6570\u5668\uff08Progression\uff09<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5b9a\u4e49<\/strong>\uff1a\u8de8\u56de\u5408\u7d2f\u79ef\u4e00\u4e2a\u8ba1\u6570\uff0c\u5230\u9608\u503c\u65f6\u5347\u7ea7\u6216\u91ca\u653e\u5956\u52b1\u3002<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u5173\u952e\u8bcd<\/th><th>\u8ba1\u6570\u5bf9\u8c61<\/th><th>\u9636\u6bb5\u6570<\/th><\/tr><\/thead><tbody><tr><td>\u4efb\u52a1<\/td><td>\u4e8b\u4ef6\u6b21\u6570<\/td><td>1<\/td><\/tr><tr><td>\u4efb\u52a1\u94fe<\/td><td>\u4e8b\u4ef6\u6b21\u6570<\/td><td>3<\/td><\/tr><tr><td>\u6cd5\u672f\u77f3<\/td><td>\u4e8b\u4ef6\u6b21\u6570<\/td><td>2<\/td><\/tr><tr><td>\u9634\u8c0b<\/td><td>\u56de\u5408\u6570<\/td><td>\u221e<\/td><\/tr><tr><td>\u63a2\u5e95<\/td><td>\u4f7f\u7528\u6b21\u6570<\/td><td>2<\/td><\/tr><tr><td>\u8150\u8680<\/td><td>\u4e00\u6b21\u6027\u6761\u4ef6<\/td><td>1<\/td><\/tr><tr><td>\u704c\u6ce8\uff08Infuse\uff09<\/td><td>\u53cb\u65b9\u6b7b\u4ea1\u6570<\/td><td>1-2<\/td><\/tr><tr><td>\u6316\u6398<\/td><td>\u6316\u6398\u6b21\u6570<\/td><td>4<\/td><\/tr><tr><td>\u6cf0\u5766<\/td><td>\u6280\u80fd\u4f7f\u7528\u4f4d\u63a9\u7801<\/td><td>3<\/td><\/tr><tr><td>\u704c\u6ce8\uff08Imbue\uff09<\/td><td>\u4f7f\u7528\u6b21\u6570<\/td><td>\u221e<\/td><\/tr><tr><td>\u5148\u9a71<\/td><td>\u4f7f\u7528\u6b21\u6570<\/td><td>2<\/td><\/tr><tr><td>\u661f\u8230<\/td><td>\u96f6\u4ef6\u6570<\/td><td>\u53ef\u53d8<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5b9e\u73b0\u6210\u672c<\/strong>\uff1a\u4e2d\uff08\u9700\u8981\u8de8\u533a\u57df\u7684\u6301\u4e45 tag + \u53d8\u5f62\uff09 <strong>\u7ec4\u5408\u6027<\/strong>\uff1a\u597d <strong>\u5360\u6bd4<\/strong>\uff1a\u7ea6 20%<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8bbe\u8ba1\u7ecf\u9a8c<\/strong>\uff1a\u628a&#8221;\u8ba1\u6570&#8221;\u548c&#8221;\u5347\u7ea7&#8221;\u5206\u79bb\u6210\u4e24\u4e2a Action\uff0c\u524d\u8005\u590d\u7528\uff0c\u540e\u8005\u5361\u724c\u7279\u5b9a\u3002\u6240\u6709\u7684\u8fdb\u5ea6\u6761\u673a\u5236\u90fd\u80fd\u7528\u540c\u4e00\u5957 <code>Progression<\/code> \u7ec4\u4ef6\u8868\u8fbe\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">@dataclass(frozen=True)<br>class ProgressionDef:<br>    counter_key: str                  # \u7edf\u8ba1\u54ea\u4e2a\u4e8b\u4ef6<br>    thresholds: tuple[int, ...]       # \u5404\u9636\u6bb5\u9608\u503c<br>    on_stage: tuple[\"Action\", ...]    # \u5404\u9636\u6bb5\u5956\u52b1<br>    upgrade_to: tuple[str, ...] = ()  # \u5404\u9636\u6bb5\u7684\u53d8\u5f62\u76ee\u6807<br>    zones: frozenset[Zone] = frozenset({Zone.HAND, Zone.SECRET, Zone.PLAY})<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8fd9\u4e00\u4e2a\u6570\u636e\u7ed3\u6784\u80fd\u8868\u8fbe\u4e0a\u9762\u5341\u4e8c\u4e2a\u5173\u952e\u8bcd\u3002<\/strong><\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u8303\u5f0f\u516d\uff1a\u591a\u5b9e\u4f53 \/ \u5408\u5e76\uff08Composite\uff09<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5b9a\u4e49<\/strong>\uff1a\u4e00\u5f20\u5361\u5bf9\u5e94\u591a\u4e2a\u5b9e\u4f53\uff0c\u6216\u591a\u4e2a\u5b9e\u4f53\u5408\u5e76\u6210\u4e00\u4e2a\u3002<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u5173\u952e\u8bcd<\/th><th>\u65b9\u5411<\/th><\/tr><\/thead><tbody><tr><td>\u78c1\u529b<\/td><td>\u5408\u5e76\uff08N \u2192 1\uff09<\/td><\/tr><tr><td>\u5de8\u578b<\/td><td>\u5206\u88c2\uff081 \u2192 N\uff09<\/td><\/tr><tr><td>\u661f\u8230<\/td><td>\u5408\u5e76 + \u53d1\u5c04<\/td><\/tr><tr><td>\u788e\u88c2<\/td><td>\u5206\u88c2 + \u5408\u5e76<\/td><\/tr><tr><td>\u4f20\u5947<\/td><td>\u6784\u7b51\u671f\u5206\u88c2<\/td><\/tr><tr><td>\u6289\u62e9\uff08\u5b50\u5361\uff09<\/td><td>\u5b9a\u4e49\u671f\u5206\u88c2<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5b9e\u73b0\u6210\u672c<\/strong>\uff1a\u9ad8\uff08\u8981\u5904\u7406\u5c5e\u6027\u5408\u5e76\u3001\u80fd\u529b\u8f6c\u79fb\u3001\u4f4d\u7f6e\u8ba1\u7b97\u3001\u6c89\u9ed8\u4ea4\u4e92\uff09 <strong>\u7ec4\u5408\u6027<\/strong>\uff1a\u5dee\uff08\u5408\u5e76\u540e\u7684\u5b9e\u4f53\u72b6\u6001\u590d\u6742\uff09 <strong>\u5360\u6bd4<\/strong>\uff1a\u7ea6 8%<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8bbe\u8ba1\u7ecf\u9a8c<\/strong>\uff1a\u5408\u5e76<strong>\u5fc5\u987b\u901a\u8fc7\u9644\u9b54\u5b9e\u73b0<\/strong>\uff0c\u8fd9\u6837\u6c89\u9ed8\/\u79fb\u9664\u65f6\u80fd\u4e00\u6b21\u6027\u56de\u6eda\u3002\u7edd\u5bf9\u4e0d\u8981\u76f4\u63a5\u4fee\u6539\u5bbf\u4e3b\u7684\u57fa\u7840\u5b9a\u4e49\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u8303\u5f0f\u5206\u5e03\u968f\u65f6\u95f4\u7684\u6f14\u5316<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">2014  \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591  \u9759\u6001\u6807\u7b7e\u4e3a\u4e3b<br>2015  \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2591\u2591\u2588\u2588\u2588\u2588\u2591\u2591\u2591\u2591\u2591\u2591  \u89e6\u53d1\u5668\u5d1b\u8d77<br>2016  \u2588\u2588\u2588\u2588\u2588\u2588\u2591\u2591\u2588\u2588\u2588\u2588\u2591\u2591\u2588\u2588\u2591\u2591\u2591\u2591<br>2017  \u2588\u2588\u2588\u2588\u2591\u2591\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2591\u2591\u2588\u2588\u2588\u2588  \u9009\u62e9\u578b\uff08\u53d1\u73b0\uff09+ \u72b6\u6001\u673a\uff08\u4efb\u52a1\uff09<br>2018  \u2588\u2588\u2588\u2588\u2591\u2591\u2588\u2588\u2588\u2588\u2588\u2588\u2591\u2591\u2588\u2588\u2588\u2588\u2591\u2591<br>2019  \u2588\u2588\u2591\u2591\u2591\u2591\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2591\u2591  \u72b6\u6001\u673a\u6210\u4e3a\u4e3b\u6d41<br>2020  \u2588\u2588\u2591\u2591\u2591\u2591\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2591\u2591\u2588\u2588\u2588\u2588<br>2021  \u2588\u2588\u2591\u2591\u2591\u2591\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588<br>2022  \u2591\u2591\u2591\u2591\u2591\u2591\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2591\u2591\u2588\u2588\u2588\u2588  \u591a\u5b9e\u4f53\uff08\u5de8\u578b\uff09<br>2023  \u2591\u2591\u2591\u2591\u2591\u2591\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2591\u2591<br>2024  \u2591\u2591\u2591\u2591\u2591\u2591\u2588\u2588\u2588\u2588\u2591\u2591\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588  \u591a\u5b9e\u4f53\uff08\u661f\u8230\uff09<br>2025  \u2591\u2591\u2591\u2591\u2591\u2591\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2591\u2591<br>2026  \u2591\u2591\u2591\u2591\u2591\u2591\u2588\u2588\u2588\u2588\u2591\u2591\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588  \u591a\u5b9e\u4f53\uff08\u788e\u88c2\u3001\u5148\u9a71\uff09<br><br>\u56fe\u4f8b\uff1a\u9759\u6001\u6807\u7b7e \/ \u89e6\u53d1\u5668 \/ \u51fa\u724c\u94a9\u5b50 \/ \u9009\u62e9\u578b \/ \u72b6\u6001\u673a \/ \u591a\u5b9e\u4f53<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8d8b\u52bf\u89e3\u8bfb<\/strong>\uff1a<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>\u9759\u6001\u6807\u7b7e\u5728 2016 \u5e74\u540e\u57fa\u672c\u9971\u548c<\/strong>\u3002\u6240\u6709&#8221;\u7b80\u5355\u7684\u6301\u7eed\u5c5e\u6027&#8221;\u90fd\u88ab\u7528\u5149\u4e86\u3002<\/li>\n\n\n\n<li><strong>\u89e6\u53d1\u5668\u4fdd\u6301\u7a33\u5b9a<\/strong>\uff0c\u56e0\u4e3a\u4e8b\u4ef6\u7a7a\u95f4\u5728\u6269\u5927\uff08\u65b0\u589e\u4e8b\u4ef6 = \u65b0\u7684\u89e6\u53d1\u673a\u4f1a\uff09\u3002<\/li>\n\n\n\n<li><strong>\u72b6\u6001\u673a\u5728 2017 \u5e74\u540e\u6210\u4e3a\u4e3b\u529b<\/strong>\u3002\u5b83\u63d0\u4f9b&#8221;\u8de8\u56de\u5408\u7684\u76ee\u6807\u611f&#8221;\uff0c\u662f\u7559\u5b58\u7387\u7684\u597d\u670b\u53cb\u3002<\/li>\n\n\n\n<li><strong>\u591a\u5b9e\u4f53\u5728 2022 \u5e74\u540e\u5d1b\u8d77<\/strong>\u3002\u8fd9\u662f\u56e0\u4e3a\u524d\u9762\u7684\u7a7a\u95f4\u7528\u5c3d\u4e86\uff0c\u8bbe\u8ba1\u5e08\u5fc5\u987b\u53bb\u66f4\u590d\u6742\u7684\u65b9\u5411\u3002<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u7ed9\u8bbe\u8ba1\u5e08\u7684\u542f\u793a<\/strong>\uff1a\u5173\u952e\u8bcd\u7684\u590d\u6742\u5ea6\u662f<strong>\u5355\u8c03\u9012\u589e<\/strong>\u7684\uff0c\u56e0\u4e3a\u7b80\u5355\u7684\u8bbe\u8ba1\u7a7a\u95f4\u4f1a\u88ab\u5148\u7528\u5b8c\u3002\u6240\u4ee5\u5f15\u64ce\u5fc5\u987b\u5728\u65e9\u671f\u5c31\u4e3a\u590d\u6742\u5ea6\u7559\u51fa\u4f59\u91cf\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u7ed9\u5de5\u7a0b\u5e08\u7684\u542f\u793a<\/strong>\uff1a\u5982\u679c\u4f60\u5728\u505a\u4e00\u4e2a\u957f\u671f\u8fd0\u8425\u7684\u5361\u724c\u6e38\u620f\uff0c<strong>\u4ece\u7b2c\u4e00\u5929\u5c31\u5b9e\u73b0&#8221;\u591a\u5b9e\u4f53&#8221;\u548c&#8221;\u72b6\u6001\u673a&#8221;\u7684\u57fa\u7840\u8bbe\u65bd<\/strong>\uff0c\u5373\u4f7f\u4f60\u7684\u521d\u7248\u5361\u724c\u7528\u4e0d\u4e0a\u3002\u56e0\u4e3a\u7b49\u5230\u7b2c\u516b\u5e74\u4f60\u9700\u8981\u5b83\u4eec\u65f6\uff0c\u6539\u9020\u6210\u672c\u4f1a\u9ad8\u5341\u500d\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">\u7b2c\u4e94\u90e8\u5206 \u7591\u96be\u65f6\u5e8f\u6848\u4f8b\u7814\u7a76<\/h1>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">\u8fd9\u4e00\u90e8\u5206\u6311\u5341\u4e2a\u771f\u5b9e\u5b58\u5728\u8fc7\u7684\u88c1\u5b9a\u4e89\u8bae\u6216\u5386\u53f2 bug\u3002 \u6bcf\u4e2a\u6848\u4f8b\u7684\u4ef7\u503c\u4e0d\u5728\u4e8e\u7089\u77f3\u600e\u4e48\u89e3\u51b3\u7684\uff0c\u800c\u5728\u4e8e\u5b83\u66b4\u9732\u4e86\u4ec0\u4e48\u67b6\u6784\u7f3a\u9677\u3002<\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u6848\u4f8b 1\uff1a\u4e24\u4e2a\u4e0d\u7a33\u5b9a\u7684\u98df\u5c38\u9b3c\u540c\u65f6\u6b7b\u4ea1<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u573a\u9762<\/strong>\uff1a\u4f60\u7684\u573a\u4e0a\u6709 A\u3001B \u4e24\u4e2a\u300a\u4e0d\u7a33\u5b9a\u7684\u98df\u5c38\u9b3c\u300b\uff081\/3\uff0c\u4ea1\u8bed\uff1a\u5bf9\u6240\u6709\u968f\u4ece\u9020\u6210 1 \u70b9\u4f24\u5bb3\uff09\uff0c\u90fd\u5df2\u7ecf\u53d7\u4e86 2 \u70b9\u4f24\u5bb3\uff08\u5269 1 \u8840\uff09\u3002\u5bf9\u624b\u6253\u51fa\u300a\u66b4\u98ce\u96ea\u300b\uff08\u5bf9\u6240\u6709\u654c\u65b9\u968f\u4ece\u9020\u6210 2 \u70b9\u4f24\u5bb3\uff09\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u95ee\u9898<\/strong>\uff1aA \u548c B \u7684\u4ea1\u8bed\u90fd\u89e6\u53d1\u5417\uff1f\u5b83\u4eec\u7684\u4f24\u5bb3\u4f1a\u4e92\u76f8\u6253\u5230\u5417\uff1f<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u7b54\u6848<\/strong>\uff1a\u90fd\u89e6\u53d1\uff1b\u4e0d\u4f1a\u4e92\u76f8\u6253\u5230\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u63a8\u6f14<\/strong>\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">1. \u66b4\u98ce\u96ea\uff1aA[DAMAGE] 2\u21924\uff0cB[DAMAGE] 2\u21924\u3002\u4e24\u8005 current_health = 3-4 = -1<br>2. process_deaths \u7b2c\u4e00\u8f6e\uff1a<br>   \u251c\u2500 \u6536\u96c6 dying = [A, B]          \uff08ENTITY_ID \u6392\u5e8f\uff09<br>   \u251c\u2500 \u5168\u90e8\u5feb\u7167<br>   \u251c\u2500 \u5168\u90e8 move \u2192 GRAVEYARD        \u2605 \u5728\u89e6\u53d1\u4ea1\u8bed\u4e4b\u524d<br>   \u251c\u2500 emit(DEATH, A) \u2192 A \u7684\u4ea1\u8bed\u5165\u961f<br>   \u251c\u2500 emit(DEATH, B) \u2192 B \u7684\u4ea1\u8bed\u5165\u961f<br>   \u2514\u2500 resolve()\uff1a<br>       \u251c\u2500 A.\u4ea1\u8bed\uff1aDamage(\u6240\u6709\u968f\u4ece, 1)<br>       \u2502    \u2514\u2500 \u76ee\u6807 = \u5f53\u524d PLAY \u533a\u7684\u968f\u4ece = \u4e0d\u542b A\u3001B\uff08\u5df2\u5728\u5893\u5730\uff09<br>       \u2514\u2500 B.\u4ea1\u8bed\uff1aDamage(\u6240\u6709\u968f\u4ece, 1)<br>            \u2514\u2500 \u540c\u4e0a<br>3. process_deaths \u7b2c\u4e8c\u8f6e\uff1a\u5904\u7406\u88ab\u4ea1\u8bed\u6253\u6b7b\u7684\u968f\u4ece<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u66b4\u9732\u7684\u67b6\u6784\u8981\u6c42<\/strong>\uff1a\u6b7b\u4ea1\u7ed3\u7b97\u5fc5\u987b\u662f<strong>\u6279\u91cf\u7684<\/strong>\uff0c&#8221;\u79fb\u5165\u5893\u5730&#8221;\u5fc5\u987b<strong>\u5148\u4e8e<\/strong>&#8220;\u89e6\u53d1\u4ea1\u8bed&#8221;\uff0c\u4e14<strong>\u6240\u6709\u6b7b\u4ea1\u5b9e\u4f53\u4e00\u8d77\u79fb\u52a8<\/strong>\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u53cd\u4f8b<\/strong>\uff1a\u5982\u679c\u5b9e\u73b0\u6210&#8221;\u9010\u4e2a\u5904\u7406\u6b7b\u4ea1&#8221;\uff08\u6536\u96c6\u4e00\u4e2a\u3001\u5904\u7406\u4e00\u4e2a\uff09\uff0cA \u7684\u4ea1\u8bed\u6267\u884c\u65f6 B \u8fd8\u5728\u573a\u4e0a\uff0cB \u4f1a\u88ab\u518d\u6253\u4e00\u6b21\uff08\u867d\u7136\u5b83\u5df2\u7ecf\u6b7b\u4e86\uff09\u2014\u2014\u867d\u7136\u7ed3\u679c\u53ef\u80fd\u4e00\u6837\uff0c\u4f46\u5982\u679c B \u662f\u300a\u6069\u4f50\u65af\u7684\u5b9d\u7bb1\u300b\u7c7b\u4f1a\u514d\u6b7b\u7684\u5b9e\u4f53\uff0c\u884c\u4e3a\u5c31\u5b8c\u5168\u4e0d\u540c\u4e86\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u6848\u4f8b 2\uff1a\u66b4\u98ce\u57ce\u52c7\u58eb\u4e0e\u6c89\u9ed8\u7684\u81f4\u6b7b\u4ea4\u4e92<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u573a\u9762<\/strong>\uff1a\u4f60\u6709\u4e00\u4e2a\u300a\u7834\u788e\u6b8b\u9633\u796d\u53f8\u300b\uff083\/2\uff09\u548c\u4e00\u4e2a\u300a\u66b4\u98ce\u57ce\u52c7\u58eb\u300b\uff083\/2\uff0c\u5149\u73af\uff1a\u4f60\u7684\u5176\u4ed6\u968f\u4ece +1\/+1\uff09\u3002\u796d\u53f8\u5b9e\u9645\u662f 4\/3\u3002\u5b83\u53d7\u4e86 2 \u70b9\u4f24\u5bb3\uff08\u5269 1 \u8840\uff09\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u73b0\u5728\u5bf9\u624b\u6c89\u9ed8\u4e86\u300a\u66b4\u98ce\u57ce\u52c7\u58eb\u300b\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u95ee\u9898<\/strong>\uff1a\u796d\u53f8\u4f1a\u6b7b\u5417\uff1f<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u7b54\u6848<\/strong>\uff1a\u4f1a\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u63a8\u6f14<\/strong>\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">\u6c89\u9ed8\u524d\uff1a\u796d\u53f8 HEALTH=2(\u57fa\u7840) + 1(\u5149\u73af) = 3\uff0cDAMAGE=2\uff0ccurrent = 1<br>\u6c89\u9ed8\u540e\uff1a\u5149\u73af\u91cd\u7b97 \u2192 \u796d\u53f8 HEALTH=2\uff0cDAMAGE=2\uff0ccurrent = 0 \u2192 \u6b7b\u4ea1<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8fd9\u662f&#8221;\u4f24\u5bb3\u6807\u8bb0&#8221;\u6a21\u578b\u7684\u5fc5\u7136\u540e\u679c<\/strong>\u3002\u73a9\u5bb6\u7684\u76f4\u89c9\u662f&#8221;\u8840\u91cf\u964d\u4e86\u4f46\u4f24\u5bb3\u4e5f\u5e94\u8be5\u964d&#8221;\uff0c\u4f46\u89c4\u5219\u4e0d\u662f\u8fd9\u6837\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u66b4\u9732\u7684\u67b6\u6784\u8981\u6c42<\/strong>\uff1a\u5149\u73af\u91cd\u7b97\u4e4b\u540e<strong>\u5fc5\u987b\u8c03\u7528 <code>process_deaths<\/code><\/strong>\u3002\u5982\u679c\u5149\u73af\u91cd\u7b97\u53d1\u751f\u5728\u6b7b\u4ea1\u7ed3\u7b97\u4e4b\u5916\uff08\u6bd4\u5982\u5728\u8bfb\u53d6 tag \u65f6\u60f0\u6027\u91cd\u7b97\uff09\uff0c\u5c31\u4f1a\u51fa\u73b0&#8221;\u796d\u53f8\u8840\u91cf\u662f 0 \u4f46\u8fd8\u6d3b\u7740&#8221;\u7684\u5e7d\u7075\u72b6\u6001\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def silence(game, e):<br>    ...<br>    game.aura_bus.mark_dirty()<br>    game.aura_bus.recompute()      # \u7acb\u5373\u91cd\u7b97<br>    game.process_deaths()          # \u2605 \u5fc5\u987b\u8ddf\u4e00\u6b21\u6b7b\u4ea1\u7ed3\u7b97<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u63a8\u5e7f\u7684\u89c4\u5219<\/strong>\uff1a<strong>\u4efb\u4f55\u53ef\u80fd\u6539\u53d8\u751f\u547d\u503c\u4e0a\u9650\u7684\u64cd\u4f5c\u4e4b\u540e\uff0c\u90fd\u5fc5\u987b\u8ddf\u4e00\u6b21\u6b7b\u4ea1\u7ed3\u7b97<\/strong>\u3002\u8fd9\u5305\u62ec\uff1a\u6c89\u9ed8\u3001\u53d8\u5f62\u3001\u5149\u73af\u6e90\u79bb\u573a\u3001\u63a7\u5236\u6743\u53d8\u5316\u3001\u300a\u738b\u8005\u795d\u798f\u300b\u88ab\u79fb\u9664\u2026\u2026<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u6848\u4f8b 3\uff1a\u4ea1\u8bed\u53ec\u5524\u7684\u4f4d\u7f6e<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u573a\u9762<\/strong>\uff1a\u4f60\u7684\u6218\u573a\u4ece\u5de6\u5230\u53f3\u662f <code>[\u5c0f\u9b3c, \u8718\u86db\u5766\u514b, \u9c7c\u4eba]<\/code>\uff08\u8718\u86db\u5766\u514b\u5728\u4f4d\u7f6e 2\uff0c\u4ea1\u8bed\uff1a\u53ec\u5524\u4e00\u4e2a 1\/1 \u673a\u68b0\uff09\u3002\u8718\u86db\u5766\u514b\u6b7b\u4ea1\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u95ee\u9898<\/strong>\uff1a\u53ec\u5524\u7684\u673a\u68b0\u5728\u54ea\u4e2a\u4f4d\u7f6e\uff1f<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u7b54\u6848<\/strong>\uff1a\u4f4d\u7f6e 2\uff08\u539f\u8718\u86db\u5766\u514b\u7684\u4f4d\u7f6e\uff09\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4e3a\u4ec0\u4e48\u96be<\/strong>\uff1a\u4ea1\u8bed\u6267\u884c\u65f6\uff0c\u8718\u86db\u5766\u514b\u5df2\u7ecf\u5728\u5893\u5730\uff0c\u6218\u573a\u53d8\u6210\u4e86 <code>[\u5c0f\u9b3c, \u9c7c\u4eba]<\/code>\uff0c\u9c7c\u4eba\u7684\u4f4d\u7f6e\u4ece 3 \u53d8\u6210\u4e86 2\u3002\u6240\u4ee5&#8221;\u539f\u4f4d\u7f6e 2&#8243;\u8fd9\u4e2a\u4fe1\u606f<strong>\u5fc5\u987b\u5728\u6b7b\u4ea1\u77ac\u95f4\u5feb\u7167<\/strong>\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"># \u6b7b\u4ea1\u5feb\u7167\u91cc\u4fdd\u5b58 position<br>snap.position = e[Tag.ZONE_POSITION]     # = 2<br><br># \u4ea1\u8bed\u6267\u884c\u65f6<br>class SummonAtDeathPosition(Action):<br>    def run(self, game, ctx):<br>        pos = ctx.event.snapshot.position<br>        game.summon(self.card_id, ctx.controller, position=pos - 1)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u66f4\u96be\u7684\u53d8\u4f53<\/strong>\uff1a\u4e24\u4e2a\u8718\u86db\u5766\u514b\u540c\u65f6\u6b7b\u4ea1\uff08\u4f4d\u7f6e 2 \u548c 3\uff09\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u7b54\u6848<\/strong>\uff1a\u7b2c\u4e00\u4e2a\uff08ENTITY_ID \u5c0f\u7684\uff09\u5148\u53ec\u5524\u5230\u4f4d\u7f6e 2\uff0c\u7b2c\u4e8c\u4e2a\u53ec\u5524\u5230\u4f4d\u7f6e 3\u3002\u4f46\u56e0\u4e3a\u7b2c\u4e00\u4e2a\u5df2\u7ecf\u5360\u4e86\u4f4d\u7f6e 2\uff0c\u7b2c\u4e8c\u4e2a\u5b9e\u9645\u4f1a\u88ab\u6324\u5230\u4f4d\u7f6e 3\u2014\u2014<strong>\u8fd9\u6070\u597d\u662f\u5bf9\u7684<\/strong>\uff0c\u56e0\u4e3a\u5b83\u539f\u672c\u5c31\u5728 3\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u4f46\u5982\u679c\u5b83\u4eec\u7684\u4f4d\u7f6e\u662f 2 \u548c 5\uff0c\u4e2d\u95f4\u9694\u7740\u522b\u7684\u968f\u4ece\uff1f\u5b9e\u6d4b\u7089\u77f3\u7684\u884c\u4e3a\u662f\uff1a<strong>\u5c3d\u91cf\u5728\u5feb\u7167\u4f4d\u7f6e\u53ec\u5524\uff0c\u88ab\u5360\u4e86\u5c31\u5f80\u53f3\u632a<\/strong>\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u66b4\u9732\u7684\u67b6\u6784\u8981\u6c42<\/strong>\uff1a\u4f4d\u7f6e\u662f\u6709\u5e8f\u5bb9\u5668\u7684\u7d22\u5f15\uff0c\u4e0d\u662f\u7edd\u5bf9\u5750\u6807\u3002\u6240\u6709&#8221;\u5728 X \u4f4d\u7f6e\u53ec\u5524&#8221;\u7684\u903b\u8f91\u90fd\u8981\u5904\u7406&#8221;\u4f4d\u7f6e\u5df2\u88ab\u5360\u7528&#8221;\u7684\u60c5\u51b5\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u6848\u4f8b 4\uff1a\u5965\u79d8\u7684\u89e6\u53d1\u987a\u5e8f\u4e0e\u76f8\u4e92\u4f5c\u7528<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u573a\u9762<\/strong>\uff1a\u5bf9\u624b\u6709\u4e24\u4e2a\u5965\u79d8\uff1a\u300a\u7206\u70b8\u9677\u9631\u300b\uff08\u5f53\u4f60\u7684\u82f1\u96c4\u53d7\u5230\u653b\u51fb\u65f6\uff0c\u5bf9\u6240\u6709\u654c\u4eba\u9020\u6210 2 \u70b9\u4f24\u5bb3\uff09\u548c\u300a\u5bd2\u51b0\u9677\u9631\u300b\uff08\u5f53\u4e00\u4e2a\u654c\u65b9\u968f\u4ece\u653b\u51fb\u65f6\uff0c\u5c06\u5176\u79fb\u56de\u624b\u724c\u5e76\u4f7f\u5176\u8d39\u7528\u589e\u52a0 2 \u70b9\uff09\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u4f60\u7528\u4e00\u4e2a 2\/1 \u968f\u4ece\u653b\u51fb\u5bf9\u65b9\u82f1\u96c4\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u95ee\u9898<\/strong>\uff1a\u4e24\u4e2a\u5965\u79d8\u90fd\u89e6\u53d1\u5417\uff1f\u8c01\u5148\uff1f<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u7b54\u6848<\/strong>\uff1a\u90fd\u89e6\u53d1\uff0c\u6309 ENTITY_ID \u987a\u5e8f\uff08\u5148\u653e\u7684\u5148\u89e6\u53d1\uff09\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u63a8\u6f14\uff08\u5047\u8bbe\u7206\u70b8\u9677\u9631\u5148\u653e\uff09<\/strong>\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">attack(\u6211\u76842\/1, \u5bf9\u65b9\u82f1\u96c4)<br>\u251c\u2500 emit(PROPOSED_ATTACK)<br>\u2502   \u251c\u2500 \u5339\u914d\uff1a[\u7206\u70b8\u9677\u9631, \u5bd2\u51b0\u9677\u9631]  \uff08\u6309 ENTITY_ID\uff09<br>\u2502   \u2514\u2500 \u5165\u961f<br>\u2514\u2500 resolve()<br>    \u251c\u2500 \u7206\u70b8\u9677\u9631\uff1a\u63ed\u793a \u2192 \u5bf9\u6240\u6709\u654c\u4eba\uff08\u542b\u6211\u7684 2\/1\uff09\u9020\u6210 2 \u4f24 \u2192 \u6211\u7684 2\/1 \u6b7b\u4ea1\u5f85\u5b9a<br>    \u2514\u2500 \u5bd2\u51b0\u9677\u9631\uff1a\u63ed\u793a \u2192 \u628a\u653b\u51fb\u8005\u79fb\u56de\u624b\u724c<br>        \u2605 \u4f46\u653b\u51fb\u8005\u5df2\u7ecf\"\u6b7b\u4ea1\u5f85\u5b9a\"\u4e86\uff0c\u600e\u4e48\u529e\uff1f<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u771f\u5b9e\u884c\u4e3a<\/strong>\uff1a\u5bd2\u51b0\u9677\u9631<strong>\u4f9d\u7136\u89e6\u53d1\u5e76\u6d88\u8017<\/strong>\uff0c\u4f46\u56e0\u4e3a\u653b\u51fb\u8005\u5df2\u7ecf\u88ab\u6807\u8bb0\u6b7b\u4ea1\uff0c\u6548\u679c\u5931\u8d25\uff08\u6216\u8005\u8bf4\uff0c\u5b83\u88ab\u79fb\u56de\u624b\u724c\u7136\u540e\u7acb\u523b&#8230;\u4e0d\uff0c\u5b9e\u6d4b\u662f\u79fb\u56de\u624b\u724c\u6210\u529f\uff0c\u56e0\u4e3a\u6b7b\u4ea1\u7ed3\u7b97\u8fd8\u6ca1\u53d1\u751f\uff09\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u8fd9\u4e2a\u4ea4\u4e92\u5728\u5386\u53f2\u4e0a\u51fa\u8fc7 bug\uff0c\u73b0\u4ee3\u7684\u88c1\u5b9a\u662f\uff1a<strong>\u5bd2\u51b0\u9677\u9631\u628a\u968f\u4ece\u79fb\u56de\u624b\u724c\uff0c\u968f\u4ece\u4e0d\u6b7b<\/strong>\uff08\u56e0\u4e3a\u5b83\u79bb\u5f00\u4e86 PLAY \u533a\uff0c\u6b7b\u4ea1\u7ed3\u7b97\u53ea\u626b\u63cf PLAY \u533a\uff09\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u66b4\u9732\u7684\u67b6\u6784\u8981\u6c42<\/strong>\uff1a<code>process_deaths<\/code> \u53ea\u626b\u63cf <code>Zone.PLAY<\/code>\u3002\u4e00\u4e2a&#8221;\u6b7b\u4ea1\u5f85\u5b9a&#8221;\u7684\u5b9e\u4f53\u5982\u679c\u5728\u7ed3\u7b97\u524d\u79bb\u5f00\u4e86\u6218\u573a\uff0c\u5b83\u5c31\u4e0d\u4f1a\u6b7b\u3002\u8fd9\u662f\u4e00\u4e2a<strong>\u6709\u610f\u7684\u8bbe\u8ba1<\/strong>\uff08\u300a\u6697\u5f71\u6b65\u300b\u6551\u968f\u4ece\u7684\u539f\u7406\uff09\uff0c\u4f46\u5fc5\u987b\u663e\u5f0f\u5b9e\u73b0\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def process_deaths(self):<br>    dying = []<br>    for pid in (0, 1):<br>        for e in self.zones.get(pid, Zone.PLAY):     # \u2605 \u53ea\u626b PLAY<br>            if e.current_health &lt;= 0 or e.has(Tag.TO_BE_DESTROYED):<br>                dying.append(e)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u800c\u4e14<strong>\u79bb\u5f00\u6218\u573a\u65f6\u5fc5\u987b\u6e05\u9664 <code>TO_BE_DESTROYED<\/code> \u6807\u8bb0<\/strong>\uff0c\u5426\u5219\u968f\u4ece\u56de\u5230\u6218\u573a\u65f6\u4f1a\u7acb\u523b\u6b7b\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def move(self, e, to_zone, ...):<br>    if e[Tag.ZONE] == Zone.PLAY and to_zone != Zone.PLAY:<br>        e[Tag.TO_BE_DESTROYED] = 0<br>        e[Tag.DAMAGE] = 0            # \u2605 \u79bb\u5f00\u6218\u573a\u6e05\u9664\u4f24\u5bb3<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u6848\u4f8b 5\uff1a\u5149\u73af\u7684&#8221;\u5ef6\u8fdf\u4e00\u5e27&#8221;\u95ee\u9898<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">\u8fd9\u662f<strong>\u6240\u6709\u4e8b\u4ef6\u9a71\u52a8\u5f15\u64ce\u90fd\u4f1a\u9047\u5230\u7684\u7ecf\u5178 bug<\/strong>\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u573a\u9762<\/strong>\uff1a\u4f60\u6253\u51fa\u300a\u66b4\u98ce\u57ce\u52c7\u58eb\u300b\uff08\u5176\u4ed6\u968f\u4ece +1\/+1\uff09\uff0c\u540c\u65f6\u4f60\u6709\u4e00\u4e2a\u300a\u98de\u5200\u6742\u800d\u8005\u300b\uff08\u6bcf\u5f53\u4f60\u53ec\u5524\u4e00\u4e2a\u968f\u4ece\uff0c\u5bf9\u968f\u673a\u654c\u4eba\u9020\u6210 1 \u70b9\u4f24\u5bb3\uff09\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u95ee\u9898<\/strong>\uff1a\u98de\u5200\u6742\u800d\u8005\u7684\u4f24\u5bb3\u662f\u5728\u52c7\u58eb\u7684\u5149\u73af\u751f\u6548\u524d\u8fd8\u662f\u540e\u8ba1\u7b97\u7684\uff1f\uff08\u5047\u8bbe\u6709\u5361\u8ba9\u98de\u5200\u7684\u4f24\u5bb3\u7b49\u4e8e\u5b83\u7684\u653b\u51fb\u529b\uff09<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8fd9\u4e2a\u95ee\u9898\u7684\u4e00\u822c\u5f62\u5f0f<\/strong>\uff1a<code>emit(SUMMON)<\/code> \u548c <code>aura_bus.recompute()<\/code> \u8c01\u5148\uff1f<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u7089\u77f3\u7684\u7b54\u6848<\/strong>\uff1a\u5149\u73af\u5148\u3002\u6240\u4ee5\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def summon(game, card_id, controller, position=None):<br>    e = game.create_entity(...)<br>    ok = game.zones.move(e, Zone.PLAY, position)<br>    if not ok: return None<br>    game.event_bus.register_card(e)<br>    game.aura_bus.mark_dirty()<br>    game.aura_bus.recompute()        # \u2605 \u5728 emit \u4e4b\u524d<br>    game.emit(Event.SUMMON, entity=e)<br>    return e<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4e3a\u4ec0\u4e48\uff1f<\/strong> \u56e0\u4e3a\u5982\u679c\u5149\u73af\u540e\u91cd\u7b97\uff0c\u89e6\u53d1\u5668\u770b\u5230\u7684\u662f&#8221;\u8fc7\u65f6\u7684\u72b6\u6001&#8221;\uff0c\u4f1a\u5bfc\u81f4\u73a9\u5bb6\u770b\u5230\u7684\u753b\u9762\u548c\u5b9e\u9645\u8ba1\u7b97\u4e0d\u4e00\u81f4\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4f46\u8fd9\u5e26\u6765\u4e00\u4e2a\u65b0\u95ee\u9898<\/strong>\uff1a\u5149\u73af\u91cd\u7b97\u672c\u8eab\u4f1a\u6539\u53d8 tag\uff0c\u53ef\u80fd\u89e6\u53d1&#8221;tag \u53d8\u5316\u65f6&#8221;\u7684\u6548\u679c\uff0c\u5f62\u6210\u5faa\u73af\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u7089\u77f3\u7684\u89e3\u6cd5\uff08<strong>\u63a8\u65ad<\/strong>\uff09\uff1a<strong>\u5149\u73af\u91cd\u7b97\u4e0d\u53d1\u51fa\u89e6\u53d1\u4e8b\u4ef6<\/strong>\u3002\u5149\u73af\u5bfc\u81f4\u7684 tag \u53d8\u5316\u53ea\u540c\u6b65\u7ed9\u5ba2\u6237\u7aef\uff0c\u4e0d\u8fdb\u5165\u4e8b\u4ef6\u603b\u7ebf\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def _apply_diff(self, old, new):<br>    for eid in touched:<br>        ...<br>        e.tags[tag] += d<br>        self.game.record_tag_change(e, tag, ...)    # \u53ea\u540c\u6b65\uff0c\u4e0d emit<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8fd9\u662f\u4e00\u4e2a\u91cd\u8981\u7684\u67b6\u6784\u51b3\u7b56<\/strong>\uff1a\u628a&#8221;\u72b6\u6001\u53d8\u5316&#8221;\u548c&#8221;\u6e38\u620f\u4e8b\u4ef6&#8221;\u5206\u5f00\u3002\u524d\u8005\u662f\u7ed9\u5ba2\u6237\u7aef\u770b\u7684\uff0c\u540e\u8005\u662f\u7ed9\u89c4\u5219\u7528\u7684\u3002\u5982\u679c\u6df7\u5728\u4e00\u8d77\uff0c\u4efb\u4f55\u5149\u73af\u90fd\u53ef\u80fd\u5f15\u53d1\u65e0\u9650\u5faa\u73af\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u6848\u4f8b 6\uff1a\u53d8\u5f62\u4e0e\u9644\u9b54\u7684\u4e22\u5931<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u573a\u9762<\/strong>\uff1a\u4f60\u6709\u4e00\u4e2a\u88ab\u300a\u738b\u8005\u795d\u798f\u300b\u52a0\u6210\u7684 4\/2 \u2192 8\/6 \u968f\u4ece\u3002\u5bf9\u624b\u7528\u300a\u53d8\u5f62\u672f\u300b\u628a\u5b83\u53d8\u6210 1\/1 \u7ef5\u7f8a\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u95ee\u9898<\/strong>\uff1a\u53d8\u56de\u6765\u4f1a\u600e\u6837\uff1f\uff08\u5047\u8bbe\u6709\u5361\u80fd\u53d8\u56de\uff09<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u7b54\u6848<\/strong>\uff1a\u53d8\u4e0d\u56de\u6765\u3002\u53d8\u5f62\u662f<strong>\u5355\u5411\u7684\u3001\u7834\u574f\u6027\u7684<\/strong>\u2014\u2014\u539f\u5361\u7684\u6240\u6709\u4fe1\u606f\uff08\u9664\u4e86 ENTITY_ID \u548c\u4f4d\u7f6e\uff09\u90fd\u88ab\u62b9\u9664\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4e3a\u4ec0\u4e48\u8fd9\u4e48\u8bbe\u8ba1\uff1f<\/strong> \u56e0\u4e3a\u5982\u679c\u4fdd\u7559\u539f\u59cb\u4fe1\u606f\uff0c\u5c31\u8981\u7ef4\u62a4\u4e00\u4e2a&#8221;\u53d8\u5f62\u6808&#8221;\uff0c\u800c\u4e14\u8981\u5904\u7406&#8221;\u53d8\u5f62\u7684\u53d8\u5f62\u7684\u53d8\u5f62&#8221;\u3002\u7089\u77f3\u9009\u62e9\u4e86\u6700\u7b80\u5355\u7684\u8bed\u4e49\uff1a<strong>\u53d8\u5f62 = \u7528\u65b0\u5b9a\u4e49\u91cd\u7f6e\u5b9e\u4f53<\/strong>\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u63a8\u8bba<\/strong>\uff1a<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>\u53d8\u5f62\u4e0d\u89e6\u53d1\u4ea1\u8bed\uff08\u5b9e\u4f53\u6ca1\u6b7b\uff09<\/li>\n\n\n\n<li>\u53d8\u5f62\u6e05\u9664\u6240\u6709\u9644\u9b54\uff08\u91cd\u7f6e\uff09<\/li>\n\n\n\n<li>\u53d8\u5f62\u4fdd\u7559\u5df2\u53d7\u4f24\u5bb3\u5417\uff1f<strong>\u4e0d\u4fdd\u7559<\/strong>\u2014\u2014\u7ef5\u7f8a\u662f\u6ee1\u8840\u7684 1\/1<\/li>\n\n\n\n<li>\u53d8\u5f62\u4fdd\u7559 <code>EXHAUSTED<\/code> \u5417\uff1f<strong>\u4fdd\u7559<\/strong>\u2014\u2014\u53d8\u5f62\u4e0d\u80fd\u89e3\u9664\u53ec\u5524\u5931\u8c03<\/li>\n\n\n\n<li>\u53d8\u5f62\u4fdd\u7559 <code>FROZEN<\/code> \u5417\uff1f<strong>\u4fdd\u7559<\/strong><\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">\u7b2c 3 \u548c\u7b2c 4\/5 \u7684\u4e0d\u4e00\u81f4\uff08\u4f24\u5bb3\u6e05\u9664\u4f46\u72b6\u6001\u4fdd\u7559\uff09\u662f\u4e00\u4e2a\u7eaf\u7cb9\u7684<strong>\u5b9e\u73b0\u7ec6\u8282\u6cc4\u6f0f\u5230\u89c4\u5219\u5c42<\/strong>\u7684\u4f8b\u5b50\u3002\u7406\u8bba\u4e0a\u5e94\u8be5\u6709\u4e00\u4e2a\u660e\u786e\u7684&#8221;\u53d8\u5f62\u4fdd\u7559\u4ec0\u4e48&#8221;\u6e05\u5355\uff0c\u5b9e\u9645\u4e0a\u7089\u77f3\u7684\u6e05\u5355\u770b\u8d77\u6765\u662f&#8221;\u4f4d\u7f6e\u76f8\u5173 + \u56de\u5408\u76f8\u5173\u7684\u4fdd\u7559\uff0c\u5361\u724c\u5c5e\u6027\u76f8\u5173\u7684\u91cd\u7f6e&#8221;\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">TRANSFORM_PRESERVED = {<br>    Tag.ENTITY_ID, Tag.ZONE, Tag.ZONE_POSITION,<br>    Tag.CONTROLLER, Tag.OWNER,<br>    Tag.EXHAUSTED, Tag.NUM_ATTACKS, Tag.FROZEN,<br>    Tag.NUM_TURNS_IN_PLAY,<br>}<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u7ed9\u4f60\u7684\u5efa\u8bae<\/strong>\uff1a\u5728\u81ea\u5df1\u7684\u5f15\u64ce\u91cc\uff0c\u628a\u8fd9\u4e2a\u6e05\u5355<strong>\u663e\u5f0f\u5199\u6210\u5e38\u91cf<\/strong>\u5e76\u5199\u5355\u5143\u6d4b\u8bd5\u3002\u56e0\u4e3a\u5b83\u4f1a\u88ab\u65e0\u6570\u5f20\u5361\u4f9d\u8d56\uff0c\u800c\u4e14\u5f88\u5bb9\u6613\u5728\u91cd\u6784\u4e2d\u6084\u6084\u6539\u53d8\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u6848\u4f8b 7\uff1a\u968f\u673a\u76ee\u6807\u7684\u5206\u5e03\u4e0e&#8221;\u770b\u8d77\u6765\u4e0d\u968f\u673a&#8221;<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u573a\u9762<\/strong>\uff1a\u300a\u98de\u5200\u6742\u800d\u8005\u300b\u5bf9\u968f\u673a\u654c\u4eba\u9020\u6210 1 \u70b9\u4f24\u5bb3\u3002\u5bf9\u65b9\u6709\u82f1\u96c4 + 3 \u4e2a\u968f\u4ece\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u95ee\u9898<\/strong>\uff1a\u82f1\u96c4\u88ab\u9009\u4e2d\u7684\u6982\u7387\u662f\u591a\u5c11\uff1f<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u7b54\u6848<\/strong>\uff1a1\/4\u3002\u98de\u5200\u7684\u76ee\u6807\u6c60\u662f&#8221;\u6240\u6709\u654c\u65b9\u89d2\u8272&#8221;\uff0c\u82f1\u96c4\u548c\u968f\u4ece\u7b49\u6982\u7387\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4f46\u73a9\u5bb6\u7684\u76f4\u89c9\u662f<\/strong>\uff1a&#8221;\u5e94\u8be5\u66f4\u53ef\u80fd\u6253\u968f\u4ece\u5427\uff1f&#8221; \u6216 &#8220;\u5e94\u8be5\u6253\u8138\u5427\uff1f&#8221;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u8fd9\u5f15\u51fa\u4e00\u4e2a\u91cd\u8981\u7684\u4ea7\u54c1\u95ee\u9898\uff1a<strong>\u5747\u5300\u968f\u673a\u5728\u611f\u77e5\u4e0a\u4e0d\u662f\u968f\u673a\u7684<\/strong>\u3002\u73a9\u5bb6\u4f1a\u8bb0\u4f4f&#8221;\u8fde\u7eed\u4e09\u6b21\u90fd\u6253\u8138&#8221;\u8fd9\u79cd\u4e8b\u4ef6\uff0c\u800c\u8fd9\u5728\u5747\u5300\u968f\u673a\u4e0b\u5b8c\u5168\u6b63\u5e38\uff08\u6982\u7387 1\/64\uff09\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u7089\u77f3\u7684\u5904\u7406\uff08<strong>\u63a8\u65ad<\/strong>\uff09\uff1a<strong>\u6ca1\u6709\u5904\u7406\uff0c\u5c31\u662f\u5747\u5300\u968f\u673a<\/strong>\u3002\u4f46\u5b83\u505a\u4e86\u4e00\u4ef6\u4e8b\uff1a<strong>RNG \u7684\u6d88\u8d39\u987a\u5e8f\u662f\u786e\u5b9a\u7684\u4e14\u670d\u52a1\u5668\u6743\u5a01\u7684<\/strong>\uff0c\u6240\u4ee5\u4e0d\u4f1a\u6709&#8221;\u5ba2\u6237\u7aef\u548c\u670d\u52a1\u5668\u7b97\u51fa\u4e0d\u540c\u7ed3\u679c&#8221;\u7684\u95ee\u9898\uff0c\u4e5f\u4e0d\u4f1a\u6709\u4f5c\u5f0a\u7a7a\u95f4\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5de5\u7a0b\u4e0a\u7684\u8981\u70b9<\/strong>\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class GameRNG:<br>    \"\"\"\u6240\u6709\u6e38\u620f\u968f\u673a\u5fc5\u987b\u8d70\u8fd9\u91cc\u3002\"\"\"<br>    def __init__(self, seed: int):<br>        self._rng = random.Random(seed)<br>        self._call_log: list[tuple[str, Any]] = []   # \u7528\u4e8e\u56de\u653e<br><br>    def choice(self, seq):<br>        if not seq: raise ValueError(\"empty sequence\")<br>        i = self._rng.randrange(len(seq))<br>        self._call_log.append((\"choice\", len(seq), i))<br>        return seq[i]<br><br>    def sample(self, seq, k):<br>        k = min(k, len(seq))<br>        idx = self._rng.sample(range(len(seq)), k)<br>        self._call_log.append((\"sample\", len(seq), tuple(idx)))<br>        return [seq[i] for i in idx]<br><br>    def randint(self, a, b):<br>        v = self._rng.randint(a, b)<br>        self._call_log.append((\"randint\", (a, b), v))<br>        return v<br><br>    def shuffle(self, lst):<br>        perm = list(range(len(lst)))<br>        self._rng.shuffle(perm)<br>        self._call_log.append((\"shuffle\", len(lst), tuple(perm)))<br>        lst[:] = [lst[i] for i in perm]<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5173\u952e\u70b9<\/strong>\uff1a<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>\u8bb0\u5f55\u8c03\u7528\u65e5\u5fd7<\/strong>\uff0c\u4e0d\u53ea\u662f seed\u3002\u56e0\u4e3a\u5982\u679c\u4ee3\u7801\u903b\u8f91\u53d8\u4e86\uff08\u6bd4\u5982\u67d0\u4e2a\u5361\u7684\u5b9e\u73b0\u6539\u4e86\u6d88\u8d39\u987a\u5e8f\uff09\uff0c\u5355\u9760 seed \u65e0\u6cd5\u590d\u73b0\u65e7\u5bf9\u5c40\u3002<\/li>\n\n\n\n<li><strong>\u4f20\u5165\u7684\u5e8f\u5217\u957f\u5ea6\u4e5f\u8981\u8bb0\u5f55<\/strong>\uff0c\u8fd9\u6837\u80fd\u5728\u56de\u653e\u65f6\u6821\u9a8c\u72b6\u6001\u4e00\u81f4\u6027\u3002<\/li>\n\n\n\n<li><strong>\u7edd\u5bf9\u4e0d\u80fd\u6709\u7b2c\u4e8c\u4e2a\u968f\u673a\u6e90<\/strong>\u3002\u4efb\u4f55 <code>random.xxx()<\/code> \u7684\u76f4\u63a5\u8c03\u7528\u90fd\u662f bug\u3002<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5982\u4f55\u5f3a\u5236\u8fd9\u4e00\u70b9<\/strong>\uff1a\u5728\u6d4b\u8bd5\u73af\u5883\u91cc monkey-patch \u5168\u5c40 <code>random<\/code>\uff0c\u8ba9\u4efb\u4f55\u76f4\u63a5\u8c03\u7528\u90fd\u629b\u5f02\u5e38\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"># conftest.py<br>import random<br>def _forbidden(*a, **k):<br>    raise RuntimeError(\"Direct use of global random is forbidden. Use game.rng.\")<br>for name in (\"random\", \"randint\", \"choice\", \"shuffle\", \"sample\"):<br>    setattr(random, name, _forbidden)<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u6848\u4f8b 8\uff1a\u6ee1\u624b\u724c\u4e0e\u70e7\u724c<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u573a\u9762<\/strong>\uff1a\u4f60\u624b\u724c 10 \u5f20\uff0c\u724c\u5e93\u91cc\u8fd8\u6709\u724c\u3002\u4f60\u62bd\u4e86\u4e00\u5f20\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u95ee\u9898<\/strong>\uff1a\u4f1a\u53d1\u751f\u4ec0\u4e48\uff1f<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u7b54\u6848<\/strong>\uff1a\u724c\u88ab&#8221;\u70e7\u6389&#8221;\u2014\u2014\u4ece\u724c\u5e93\u79fb\u9664\uff0c\u76f4\u63a5\u8fdb\u5893\u5730\uff0c\u73a9\u5bb6\u770b\u5230\u4e00\u4e2a\u71c3\u70e7\u52a8\u753b\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5fae\u5999\u4e4b\u5904<\/strong>\uff1a<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>\u70e7\u6389\u7684\u724c<strong>\u7b97\u4f5c&#8221;\u62bd\u5230\u4e86&#8221;\u5417<\/strong>\uff1f\u2014\u2014<strong>\u7b97<\/strong>\u3002\u6240\u4ee5\u300a\u8fc7\u5ea6\u62bd\u724c\u300b\u7c7b\u7684&#8221;\u62bd N \u5f20&#8221;\u4f1a\u771f\u7684\u62bd N \u5f20\uff0c\u53ea\u662f\u70e7\u6389\u3002<\/li>\n\n\n\n<li>\u70e7\u6389\u7684\u724c\u4f1a\u89e6\u53d1&#8221;\u62bd\u5230\u65f6&#8221;\u6548\u679c\u5417\uff1f\u2014\u2014<strong>\u4e0d\u4f1a<\/strong>\u3002\u56e0\u4e3a\u5b83\u6ca1\u8fdb\u624b\u724c\u3002<\/li>\n\n\n\n<li>\u4f46\u300a\u62bd\u5230\u65f6\u65bd\u653e\u300b\u7684\u6cd5\u672f\u5462\uff1f\u2014\u2014<strong>\u4f1a\u65bd\u653e<\/strong>\uff08\u5b9e\u6d4b\u884c\u4e3a\uff09\u3002\u8fd9\u662f\u4e2a\u7279\u4f8b\u3002<\/li>\n\n\n\n<li>\u70e7\u724c\u89e6\u53d1&#8221;\u4f60\u5f03\u6389\u4e00\u5f20\u724c&#8221;\u5417\uff1f\u2014\u2014<strong>\u4e0d\u4f1a<\/strong>\u3002\u70e7\u724c\u548c\u5f03\u724c\u662f\u4e0d\u540c\u4e8b\u4ef6\u3002<\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-preformatted\">def draw(self, pid, n=1):<br>    for _ in range(n):<br>        deck = self.zones.get(pid, Zone.DECK)<br>        if not deck:<br>            self._fatigue(pid); continue<br>        card = deck.pop()<br>        # \u2605 \u5148\u89e6\u53d1\"\u62bd\u5230\u65f6\u65bd\u653e\"\uff0c\u518d\u5224\u65ad\u624b\u724c\u662f\u5426\u6ee1<br>        if card.has(Tag.CASTS_WHEN_DRAWN):<br>            self.cast_from_draw(card, pid)<br>            continue<br>        hand = self.zones.get(pid, Zone.HAND)<br>        if len(hand) &gt;= 10:<br>            self.zones.move(card, Zone.GRAVEYARD)<br>            self.emit(Event.CARD_BURNED, entity=card, player=pid)<br>        else:<br>            self.zones.move(card, Zone.HAND)<br>            card[Tag.DRAWN_ON_TURN] = self.turn<br>            self.emit(Event.DRAW, entity=card, player=pid)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u66b4\u9732\u7684\u67b6\u6784\u8981\u6c42<\/strong>\uff1a<code>draw<\/code> \u4e0d\u662f\u4e00\u4e2a\u539f\u5b50\u64cd\u4f5c\uff0c\u5b83\u662f\u4e00\u4e2a<strong>\u6709\u591a\u4e2a\u51fa\u53e3\u7684\u6d41\u7a0b<\/strong>\uff08\u75b2\u52b3 \/ \u62bd\u5230\u65f6\u65bd\u653e \/ \u70e7\u724c \/ \u6b63\u5e38\u5165\u624b\uff09\u3002\u6bcf\u4e2a\u51fa\u53e3\u53d1\u4e0d\u540c\u7684\u4e8b\u4ef6\u3002\u5982\u679c\u628a\u5b83\u5199\u6210&#8221;\u4ece\u724c\u5e93\u53d6\u4e00\u5f20\u653e\u5230\u624b\u724c&#8221;\uff0c\u4e0a\u9762\u56db\u6761\u89c4\u5219\u4e00\u6761\u90fd\u5b9e\u73b0\u4e0d\u4e86\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u6848\u4f8b 9\uff1a\u65e0\u9650\u5faa\u73af\u4e0e\u5f3a\u5236\u5224\u548c<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u573a\u9762<\/strong>\uff1a\u67d0\u4e9b\u5361\u7ec4\u5408\u80fd\u4ea7\u751f\u65e0\u9650\u89e6\u53d1\u94fe\u3002\u5386\u53f2\u4e0a\u771f\u5b9e\u5b58\u5728\u8fc7\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u300a\u7c73\u5c14\u8c6a\u65af\u300b+\u300a\u7d2b\u7f57\u5170\u6559\u5e08\u300b+ 0 \u8d39\u6cd5\u672f<\/li>\n\n\n\n<li>\u300a\u87ba\u4e1d\u9489\u300b+\u300a\u84dd\u816e\u6218\u58eb\u300b\u7c7b\u4e92\u76f8\u53ec\u5524<\/li>\n\n\n\n<li>2021 \u5e74\u300a\u4efb\u52a1\u6cd5\u300b\u7684\u67d0\u4e9b\u7ec4\u5408<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u7089\u77f3\u7684\u5904\u7406<\/strong>\uff1a\u68c0\u6d4b\u5230\u8fc7\u6df1\u7684\u89e6\u53d1\u94fe\u65f6\uff0c<strong>\u5f3a\u5236\u7ed3\u675f\u5bf9\u5c40\u5224\u548c<\/strong>\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">MAX_TRIGGER_DEPTH = 128<br>MAX_DEATH_ROUNDS = 64<br>MAX_TURN_ACTIONS = 4096       # \u5355\u56de\u5408\u52a8\u4f5c\u6570\u4e0a\u9650<br><br>def resolve(self):<br>    self._depth += 1<br>    if self._depth &gt; MAX_TRIGGER_DEPTH:<br>        self.game.force_draw(\"INFINITE_LOOP_DETECTED\")<br>        self.queue.clear()<br>        self._depth -= 1<br>        return<br>    ...<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4e3a\u4ec0\u4e48\u4e0d\u80fd\u8bc1\u660e\u4e0d\u5b58\u5728\u73af\uff1f<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u56e0\u4e3a\u89e6\u53d1\u5173\u7cfb\u662f<strong>\u8fd0\u884c\u65f6\u6784\u9020\u7684<\/strong>\uff1a\u5361 A \u7684\u6548\u679c\u751f\u6210\u5361 B\uff0c\u5361 B \u7684\u89e6\u53d1\u8c03\u7528\u5361 C\u2026\u2026\u8fd9\u4e2a\u56fe\u5728\u5361\u724c\u5b9a\u4e49\u5c42\u9762\u662f\u65e0\u6cd5\u9759\u6001\u5206\u6790\u7684\uff08\u9664\u975e\u4f60\u9650\u5236 DSL \u7684\u8868\u8fbe\u529b\uff09\u3002\u800c\u4e14\u968f\u7740\u5361\u6c60\u589e\u957f\uff0c\u65b0\u5361\u968f\u65f6\u53ef\u80fd\u4e0e\u8001\u5361\u5f62\u6210\u73af\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6240\u4ee5\uff1a\u4efb\u4f55\u957f\u671f\u8fd0\u8425\u7684\u5361\u724c\u6e38\u620f\u5f15\u64ce\u90fd\u5fc5\u987b\u6709\u5faa\u73af\u4fdd\u62a4\u3002<\/strong> \u8fd9\u4e0d\u662f\u53ef\u9009\u9879\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u53e6\u4e00\u4e2a\u5fc5\u987b\u6709\u7684\u4fdd\u62a4<\/strong>\uff1a<strong>\u56de\u5408\u65f6\u95f4\u9650\u5236<\/strong>\u3002\u7089\u77f3\u7684\u56de\u5408\u662f 75 \u79d2\uff08\u542b\u52a0\u65f6\uff09\uff0c\u8d85\u65f6\u5f3a\u5236\u7ed3\u675f\u56de\u5408\u3002\u8fd9\u4e0d\u53ea\u662f\u4e3a\u4e86\u6e38\u620f\u4f53\u9a8c\uff0c\u4e5f\u662f\u4e3a\u4e86\u9632\u6b62&#8221;\u73a9\u5bb6\u7528\u65e0\u9650\u5faa\u73af\u5361\u6b7b\u670d\u52a1\u5668&#8221;\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u6848\u4f8b 10\uff1a\u5ba2\u6237\u7aef\u9884\u6d4b\u4e0e&#8221;\u56de\u6eda&#8221;<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u573a\u9762<\/strong>\uff1a\u4f60\u70b9\u51fb\u4e00\u5f20\u724c\u6253\u51fa\u3002\u5ba2\u6237\u7aef\u7acb\u523b\u64ad\u653e\u52a8\u753b\uff0c\u540c\u65f6\u628a\u8bf7\u6c42\u53d1\u7ed9\u670d\u52a1\u5668\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u95ee\u9898<\/strong>\uff1a\u5982\u679c\u670d\u52a1\u5668\u62d2\u7edd\u4e86\u8fd9\u4e2a\u52a8\u4f5c\u600e\u4e48\u529e\uff1f<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u7089\u77f3\u7684\u5904\u7406<\/strong>\uff1a\u5ba2\u6237\u7aef<strong>\u4e0d\u505a\u5b8c\u6574\u9884\u6d4b<\/strong>\u3002\u5b83\u53ea\u505a&#8221;\u4e50\u89c2 UI&#8221;\uff08\u5361\u724c\u8ddf\u968f\u9f20\u6807\u3001\u9ad8\u4eae\u5408\u6cd5\u76ee\u6807\uff09\uff0c\u4f46<strong>\u5b9e\u9645\u7684\u72b6\u6001\u53d8\u5316\u5b8c\u5168\u7b49\u670d\u52a1\u5668\u7684 PowerHistory<\/strong>\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u8fd9\u5c31\u662f\u4e3a\u4ec0\u4e48\u7089\u77f3\u6709\u660e\u663e\u7684\u64cd\u4f5c\u5ef6\u8fdf\uff08\u7ea6 100-300ms\uff09\u2014\u2014\u5b83\u727a\u7272\u4e86\u54cd\u5e94\u901f\u5ea6\u6362\u53d6\u4e86<strong>\u96f6\u56de\u6eda<\/strong>\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5bf9\u6bd4<\/strong>\uff1a\u5982\u679c\u505a\u5b8c\u6574\u5ba2\u6237\u7aef\u9884\u6d4b\uff0c\u5c31\u5fc5\u987b\u5b9e\u73b0\u56de\u6eda\uff0c\u800c\u56de\u6eda\u5728\u6709\u968f\u673a\u6027\u548c\u9690\u85cf\u4fe1\u606f\u7684\u60c5\u51b5\u4e0b\u51e0\u4e4e\u4e0d\u53ef\u80fd\u505a\u5bf9\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u5ba2\u6237\u7aef\u4e0d\u77e5\u9053\u5bf9\u624b\u624b\u724c \u2192 \u65e0\u6cd5\u9884\u6d4b\u5965\u79d8\u89e6\u53d1<\/li>\n\n\n\n<li>\u5ba2\u6237\u7aef\u4e0d\u77e5\u9053 RNG \u5e8f\u5217 \u2192 \u65e0\u6cd5\u9884\u6d4b\u968f\u673a\u7ed3\u679c<\/li>\n\n\n\n<li>\u5ba2\u6237\u7aef\u4e0d\u77e5\u9053\u5bf9\u624b\u724c\u5e93 \u2192 \u65e0\u6cd5\u9884\u6d4b\u300a\u5bf9\u51b3\u300b\u7ed3\u679c<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>PowerHistory \u7684\u5f62\u6001<\/strong>\uff08<strong>\u63a8\u65ad<\/strong>\uff0c\u57fa\u4e8e\u793e\u533a\u6293\u5305\u5206\u6790\uff09\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">@dataclass<br>class PowerBlock:<br>    \"\"\"\u4e00\u4e2a\u539f\u5b50\u7684\u72b6\u6001\u53d8\u5316\u5757\uff0c\u5bf9\u5e94\u5ba2\u6237\u7aef\u7684\u4e00\u6bb5\u52a8\u753b\u3002\"\"\"<br>    block_type: str          # PLAY \/ TRIGGER \/ ATTACK \/ POWER \/ DEATHS<br>    source_entity: int<br>    target_entity: int | None<br>    sub_blocks: list[\"PowerBlock\"]<br>    changes: list[\"PowerChange\"]<br><br>@dataclass<br>class PowerChange:<br>    kind: str                # TAG_CHANGE \/ FULL_ENTITY \/ SHOW_ENTITY \/ HIDE_ENTITY<br>    entity: int<br>    tag: int | None = None<br>    value: Any = None<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u4e00\u6b21\u51fa\u724c\u4ea7\u751f\u7684 PowerHistory \u5927\u81f4\u662f\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">BLOCK(PLAY, source=\u706b\u7403\u672f, target=\u654c\u65b9\u968f\u4ece)<br>\u251c\u2500 TAG_CHANGE(\u73a9\u5bb6, MANA_AVAILABLE, 8\u21924)<br>\u251c\u2500 TAG_CHANGE(\u706b\u7403\u672f, ZONE, HAND\u2192PLAY)<br>\u251c\u2500 TAG_CHANGE(\u706b\u7403\u672f, ZONE_POSITION, 3\u21920)<br>\u251c\u2500 TAG_CHANGE(\u624b\u724c\u5176\u4ed6\u5361, ZONE_POSITION, ...)   \u2190 \u4f4d\u7f6e\u91cd\u6392<br>\u251c\u2500 BLOCK(POWER, source=\u706b\u7403\u672f)<br>\u2502   \u251c\u2500 TAG_CHANGE(\u654c\u65b9\u968f\u4ece, DAMAGE, 0\u21926)<br>\u2502   \u2514\u2500 META(DAMAGE, entity=\u654c\u65b9\u968f\u4ece, amount=6)   \u2190 \u7528\u4e8e\u4f24\u5bb3\u6570\u5b57\u52a8\u753b<br>\u251c\u2500 BLOCK(DEATHS)<br>\u2502   \u251c\u2500 TAG_CHANGE(\u654c\u65b9\u968f\u4ece, ZONE, PLAY\u2192GRAVEYARD)<br>\u2502   \u2514\u2500 TAG_CHANGE(\u654c\u65b9\u968f\u4ece, ZONE_POSITION, 2\u21920)<br>\u2514\u2500 TAG_CHANGE(\u706b\u7403\u672f, ZONE, PLAY\u2192GRAVEYARD)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5d4c\u5957\u7684 BLOCK \u7ed3\u6784\u662f\u5ba2\u6237\u7aef\u64ad\u653e\u52a8\u753b\u7684\u4f9d\u636e<\/strong>\uff1a\u6bcf\u4e2a BLOCK \u5bf9\u5e94\u4e00\u6bb5\u52a8\u753b\uff0c\u5d4c\u5957\u5173\u7cfb\u51b3\u5b9a\u64ad\u653e\u987a\u5e8f\u548c\u89c6\u89c9\u5c42\u7ea7\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8fd9\u4e2a\u8bbe\u8ba1\u7684\u4ef7\u503c<\/strong>\uff1a\u670d\u52a1\u5668\u53ea\u9700\u8981\u4ea7\u751f\u4e00\u4e2a<strong>\u7eaf\u6570\u636e\u7684\u3001\u6811\u5f62\u7684\u53d8\u5316\u65e5\u5fd7<\/strong>\uff0c\u5ba2\u6237\u7aef\u8d1f\u8d23\u628a\u5b83\u6f14\u51fa\u6765\u3002\u4e24\u8fb9\u5b8c\u5168\u89e3\u8026\u3002\u4f60\u53ef\u4ee5\u6362\u6574\u4e2a\u5ba2\u6237\u7aef\uff08\u6bd4\u5982\u505a\u4e00\u4e2a\u7eaf\u6587\u672c\u5ba2\u6237\u7aef\uff09\u800c\u4e0d\u52a8\u670d\u52a1\u5668\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5728\u4f60\u81ea\u5df1\u7684\u5f15\u64ce\u91cc\u600e\u4e48\u505a<\/strong>\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class Game:<br>    def record_tag_change(self, e, tag, old, new):<br>        self.power_history.append(PowerChange(<br>            kind=\"TAG_CHANGE\", entity=e.id, tag=int(tag), value=new))<br><br>    @contextmanager<br>    def power_block(self, block_type, source, target=None):<br>        blk = PowerBlock(block_type, source.id if source else 0,<br>                         target.id if target else None, [], [])<br>        parent = self._current_block<br>        (parent.sub_blocks if parent else self.power_history).append(blk)<br>        self._current_block = blk<br>        try:<br>            yield blk<br>        finally:<br>            self._current_block = parent<br><br># \u7528\u6cd5<br>def play_card(self, card, target=None, ...):<br>    with self.power_block(\"PLAY\", card, target):<br>        ...<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4e00\u4e2a context manager \u5c31\u641e\u5b9a\u4e86\u6574\u4e2a\u7f51\u7edc\u540c\u6b65\u5c42\u3002<\/strong> \u8fd9\u662f\u6211\u8ba4\u4e3a\u6700\u4f18\u96c5\u7684\u90e8\u5206\uff1a\u56e0\u4e3a\u6240\u6709\u72b6\u6001\u53d8\u5316\u90fd\u8d70 <code>__setitem__<\/code>\uff0c\u4f60\u53ea\u8981\u5728\u90a3\u91cc\u8bb0\u5f55\uff0c\u5c31\u81ea\u52a8\u5f97\u5230\u4e86\u5b8c\u6574\u7684\u3001\u6b63\u786e\u987a\u5e8f\u7684\u53d8\u5316\u65e5\u5fd7\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u6848\u4f8b\u5c0f\u7ed3\uff1a\u53cd\u590d\u51fa\u73b0\u7684\u56db\u4e2a\u6a21\u5f0f<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">\u56de\u770b\u8fd9\u5341\u4e2a\u6848\u4f8b\uff0c\u6709\u56db\u4e2a\u6a21\u5f0f\u53cd\u590d\u51fa\u73b0\uff1a<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u6a21\u5f0f\u4e00\uff1a\u5feb\u7167\uff08Snapshot\uff09<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u51fa\u73b0\u5728\uff1a\u4ea1\u8bed\u4f4d\u7f6e\u3001\u653b\u51fb\u529b\u3001\u76ee\u6807\u5217\u8868\u3001\u7ec8\u66f2\u5224\u5b9a\u3001\u6b7b\u4ea1\u65f6\u7684\u76f8\u90bb\u5173\u7cfb\u3001\u53d1\u73b0\u7684\u5361\u6c60\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u89c4\u5219<\/strong>\uff1a<strong>\u4efb\u4f55\u5ef6\u8fdf\u6267\u884c\u7684\u6548\u679c\uff0c\u5fc5\u987b\u5728\u6ce8\u518c\u65f6\u6355\u83b7\u5b83\u9700\u8981\u7684\u5168\u90e8\u4e0a\u4e0b\u6587\u3002<\/strong><\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u6a21\u5f0f\u4e8c\uff1a\u6279\u91cf\u539f\u5b50\u64cd\u4f5c\uff08Batch Atomicity\uff09<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u51fa\u73b0\u5728\uff1a\u6b7b\u4ea1\u7ed3\u7b97\u3001AOE \u4f24\u5bb3\u3001\u5149\u73af\u91cd\u7b97\u3001\u540c\u65f6\u79fb\u5165\u5893\u5730\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u89c4\u5219<\/strong>\uff1a<strong>&#8220;\u540c\u65f6\u53d1\u751f&#8221;\u7684\u4e8b\u5fc5\u987b\u771f\u7684\u540c\u65f6\u53d1\u751f\uff0c\u4e2d\u95f4\u4e0d\u80fd\u88ab\u89c2\u5bdf\u5230\u4e2d\u95f4\u72b6\u6001\u3002<\/strong><\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u6a21\u5f0f\u4e09\uff1a\u552f\u4e00\u5165\u53e3\uff08Single Choke Point\uff09<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u51fa\u73b0\u5728\uff1a<code>zones.move<\/code>\uff08\u533a\u57df\u53d8\u5316\uff09\u3001<code>entity.__setitem__<\/code>\uff08tag \u53d8\u5316\uff09\u3001<code>game.damage<\/code>\uff08\u4f24\u5bb3\uff09\u3001<code>game.rng<\/code>\uff08\u968f\u673a\uff09\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u89c4\u5219<\/strong>\uff1a<strong>\u6bcf\u4e00\u7c7b\u72b6\u6001\u53d8\u5316\u53ea\u6709\u4e00\u4e2a\u51fd\u6570\u80fd\u505a\uff0c\u6240\u6709\u7684\u6a2a\u5207\u5173\u6ce8\u70b9\uff08\u65e5\u5fd7\u3001\u540c\u6b65\u3001\u4e0d\u53d8\u5f0f\u68c0\u67e5\uff09\u90fd\u6302\u5728\u90a3\u91cc\u3002<\/strong><\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u6a21\u5f0f\u56db\uff1a\u72b6\u6001\u4e0e\u4e8b\u4ef6\u5206\u79bb\uff08State vs Event\uff09<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u51fa\u73b0\u5728\uff1a\u5149\u73af\u91cd\u7b97\u4e0d emit\u3001<code>record_tag_change<\/code> vs <code>emit<\/code>\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u89c4\u5219<\/strong>\uff1a<strong>&#8220;\u7ed9\u5ba2\u6237\u7aef\u770b\u7684\u72b6\u6001\u53d8\u5316&#8221;\u548c&#8221;\u7ed9\u89c4\u5219\u7528\u7684\u6e38\u620f\u4e8b\u4ef6&#8221;\u662f\u4e24\u56de\u4e8b\uff0c\u6df7\u5728\u4e00\u8d77\u5fc5\u7136\u4ea7\u751f\u5faa\u73af\u3002<\/strong><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">\u7b2c\u516d\u90e8\u5206 \u5de5\u7a0b\u5b9e\u8df5<\/h1>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 28 \u7ae0 \u786e\u5b9a\u6027\u968f\u673a\u4e0e\u56de\u653e<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">28.1 \u4e09\u4e2a\u5fc5\u987b\u6ee1\u8db3\u7684\u6027\u8d28<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u4e00\u4e2a\u5361\u724c\u6e38\u620f\u7684\u968f\u673a\u7cfb\u7edf\u5fc5\u987b\u540c\u65f6\u6ee1\u8db3\uff1a<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>\u4e0d\u53ef\u9884\u6d4b<\/strong>\uff1a\u73a9\u5bb6\uff08\u5305\u62ec\u4f5c\u5f0a\u5ba2\u6237\u7aef\uff09\u4e0d\u80fd\u63d0\u524d\u77e5\u9053\u7ed3\u679c<\/li>\n\n\n\n<li><strong>\u53ef\u590d\u73b0<\/strong>\uff1a\u670d\u52a1\u5668\u80fd\u4ece\u65e5\u5fd7\u5b8c\u6574\u91cd\u653e\u4e00\u5c40\uff0c\u7528\u4e8e bug \u6392\u67e5\u548c\u56de\u653e\u529f\u80fd<\/li>\n\n\n\n<li><strong>\u4e0d\u53ef\u6c61\u67d3<\/strong>\uff1aAI \u7684\u6a21\u62df\u4e0d\u80fd\u5f71\u54cd\u771f\u5b9e\u5bf9\u5c40\u7684\u968f\u673a\u5e8f\u5217<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">28.2 \u6027\u8d28 1\uff1a\u4e0d\u53ef\u9884\u6d4b<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4e0d\u80fd\u505a\u7684\u4e8b<\/strong>\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u274c \u7528\u56fa\u5b9a seed\uff08\u73a9\u5bb6\u80fd\u7b97\u51fa\u6765\uff09<\/li>\n\n\n\n<li>\u274c \u7528\u65f6\u95f4\u6233\u4f5c seed\uff08\u53ef\u9884\u6d4b\uff09<\/li>\n\n\n\n<li>\u274c \u5728\u5ba2\u6237\u7aef\u751f\u6210\u968f\u673a\u6570<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6b63\u786e\u505a\u6cd5<\/strong>\uff1a\u670d\u52a1\u5668\u7528\u5bc6\u7801\u5b66\u5b89\u5168\u7684\u968f\u673a\u6e90\u751f\u6210 seed\uff0c\u4e14 <strong>seed \u4e0d\u4e0b\u53d1\u7ed9\u5ba2\u6237\u7aef<\/strong>\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">import secrets<br><br>class Game:<br>    def __init__(self, seed: int | None = None):<br>        self.seed = seed if seed is not None else secrets.randbits(64)<br>        self.rng = GameRNG(self.seed)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4e00\u4e2a\u771f\u5b9e\u7684\u5386\u53f2\u95ee\u9898<\/strong>\uff1a\u65e9\u671f\u7089\u77f3\u5ba2\u6237\u7aef\u80fd\u901a\u8fc7\u5185\u5b58\u8bfb\u53d6\u9884\u77e5\u4e0b\u4e00\u5f20\u62bd\u7684\u724c\uff08\u56e0\u4e3a\u724c\u5e93\u987a\u5e8f\u5728\u5f00\u5c40\u5c31\u786e\u5b9a\u5e76\u4e0b\u53d1\u4e86\uff09\u3002\u73b0\u4ee3\u5b9e\u73b0\u5e94\u8be5<strong>\u60f0\u6027\u51b3\u5b9a\u724c\u5e93\u987a\u5e8f<\/strong>\u2014\u2014\u53ea\u5728\u771f\u6b63\u62bd\u724c\u65f6\u624d\u51b3\u5b9a\u62bd\u5230\u54ea\u5f20\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class LazyDeck:<br>    \"\"\"\u60f0\u6027\u6d17\u724c\uff1a\u53ea\u5728\u62bd\u724c\u65f6\u51b3\u5b9a\uff0c\u51cf\u5c11\u4fe1\u606f\u6cc4\u9732\u9762\u3002\"\"\"<br>    def __init__(self, cards: list[Entity], rng: GameRNG):<br>        self._pool = list(cards)      # \u672a\u51b3\u5b9a\u7684\u724c<br>        self._order = []              # \u5df2\u51b3\u5b9a\u7684\u987a\u5e8f\uff08\u724c\u5e93\u9876\u5728\u672b\u5c3e\uff09<br>        self.rng = rng<br><br>    def draw(self) -&gt; Entity | None:<br>        if self._order:<br>            return self._order.pop()<br>        if not self._pool:<br>            return None<br>        i = self.rng.randrange(len(self._pool))<br>        return self._pool.pop(i)<br><br>    def peek_bottom(self, n) -&gt; list[Entity]:<br>        \"\"\"\u6253\u635e\u9700\u8981\u770b\u724c\u5e93\u5e95 \u2014\u2014 \u6b64\u65f6\u624d\u51b3\u5b9a\u8fd9\u4e09\u5f20\"\"\"<br>        while len(self._order) &lt; n and self._pool:<br>            i = self.rng.randrange(len(self._pool))<br>            self._order.insert(0, self._pool.pop(i))<br>        return self._order[:n]<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u60f0\u6027\u6d17\u724c\u7684\u989d\u5916\u597d\u5904<\/strong>\uff1a\u300a\u6253\u635e\u300b\u300a\u68c0\u7d22\u300b\u7b49\u6548\u679c\u53ea\u9700\u8981&#8221;\u51b3\u5b9a&#8221;\u5b83\u4eec\u67e5\u770b\u7684\u90a3\u51e0\u5f20\uff0c\u5176\u4f59\u4fdd\u6301\u672a\u5b9a\u3002\u8fd9\u51cf\u5c11\u4e86\u5185\u5b58\u91cc&#8221;\u5df2\u77e5\u7684\u5b8c\u6574\u724c\u5e8f&#8221;\u7684\u5b58\u5728\u65f6\u95f4\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6ce8\u610f<\/strong>\uff1a\u60f0\u6027\u6d17\u724c\u5728\u6570\u5b66\u4e0a\u7b49\u4ef7\u4e8e\u63d0\u524d\u6d17\u724c\uff08\u90fd\u662f\u5747\u5300\u6392\u5217\uff09\uff0c\u6240\u4ee5\u4e0d\u5f71\u54cd\u6982\u7387\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">28.3 \u6027\u8d28 2\uff1a\u53ef\u590d\u73b0<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u53ea\u8bb0 seed \u4e0d\u591f\u3002\u56e0\u4e3a\u5982\u679c\u5f15\u64ce\u4ee3\u7801\u53d8\u4e86\uff08\u65b0\u7248\u672c\u4fee\u4e86\u4e00\u4e2a bug\uff0c\u6539\u53d8\u4e86 RNG \u6d88\u8d39\u987a\u5e8f\uff09\uff0c\u65e7\u65e5\u5fd7\u5c31\u65e0\u6cd5\u91cd\u653e\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6b63\u786e\u505a\u6cd5\uff1a\u8bb0\u5f55 RNG \u8c03\u7528\u65e5\u5fd7<\/strong>\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">@dataclass<br>class RngCall:<br>    op: str          # \"choice\" \/ \"randint\" \/ \"shuffle\" \/ \"sample\"<br>    domain: Any      # \u8f93\u5165\u7684\u89c4\u6a21\uff08\u7528\u4e8e\u6821\u9a8c\uff09<br>    result: Any<br><br>class ReplayRNG(GameRNG):<br>    \"\"\"\u56de\u653e\u6a21\u5f0f\uff1a\u4e0d\u751f\u6210\u968f\u673a\u6570\uff0c\u4ece\u65e5\u5fd7\u8bfb\u53d6\u3002\"\"\"<br>    def __init__(self, log: list[RngCall]):<br>        self.log = log<br>        self.idx = 0<br><br>    def choice(self, seq):<br>        call = self.log[self.idx]; self.idx += 1<br>        assert call.op == \"choice\", f\"RNG desync at {self.idx}: expected {call.op}\"<br>        assert call.domain == len(seq), \\<br>            f\"RNG desync: log says pool size {call.domain}, got {len(seq)}\"<br>        return seq[call.result]<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>assert call.domain == len(seq)<\/code> \u662f\u6700\u6709\u4ef7\u503c\u7684\u4e00\u884c<\/strong>\u3002\u5b83\u4f1a\u5728\u56de\u653e\u65f6\u7acb\u523b\u68c0\u6d4b\u51fa&#8221;\u5f15\u64ce\u884c\u4e3a\u53d8\u4e86&#8221;\u2014\u2014\u56e0\u4e3a\u5982\u679c\u67d0\u5f20\u5361\u7684\u5b9e\u73b0\u6539\u4e86\uff0c\u76ee\u6807\u6c60\u7684\u5927\u5c0f\u53ef\u80fd\u4e0d\u540c\uff0cassert \u4f1a\u5931\u8d25\u5e76\u544a\u8bc9\u4f60\u786e\u5207\u4f4d\u7f6e\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6211\u5728\u5b9e\u8df5\u4e2d\u7684\u5efa\u8bae<\/strong>\uff1a\u628a\u8fd9\u4e2a assert \u505a\u6210\u4e00\u4e2a<strong>\u56de\u5f52\u6d4b\u8bd5<\/strong>\u3002\u5b58\u4e00\u6279\u5386\u53f2\u5bf9\u5c40\u65e5\u5fd7\uff0c\u6bcf\u6b21\u6539\u5f15\u64ce\u540e\u91cd\u653e\uff0c\u4efb\u4f55 desync \u90fd\u662f\u884c\u4e3a\u53d8\u66f4\u7684\u4fe1\u53f7\u3002\u8fd9\u6bd4\u5199\u5355\u5143\u6d4b\u8bd5\u8986\u76d6\u7387\u9ad8\u5f97\u591a\u2014\u2014<strong>\u4e00\u5c40\u771f\u5b9e\u5bf9\u5c40\u80fd\u8986\u76d6\u4e0a\u767e\u4e2a\u4ea4\u4e92<\/strong>\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">28.4 \u6027\u8d28 3\uff1a\u4e0d\u53ef\u6c61\u67d3<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">AI \u6a21\u62df\u65f6\u4f1a\u5927\u91cf\u8c03\u7528\u5f15\u64ce\u3002\u5982\u679c\u5b83\u7528\u540c\u4e00\u4e2a RNG\uff0c\u771f\u5b9e\u5bf9\u5c40\u7684\u968f\u673a\u5e8f\u5217\u5c31\u88ab\u6253\u4e71\u4e86\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u89e3\u6cd5\uff1a\u6a21\u62df\u65f6\u7528\u72ec\u7acb\u7684 RNG\uff0c\u4e14\u72b6\u6001\u662f\u6df1\u62f7\u8d1d\u7684\u3002<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def clone_for_simulation(game: Game, sim_seed: int) -&gt; Game:<br>    g = copy.deepcopy(game)<br>    g.rng = GameRNG(sim_seed)        # \u2605 \u72ec\u7acb\u7684 RNG<br>    g.is_simulation = True<br>    g.power_history = []             # \u4e0d\u8bb0\u5f55<br>    g.sim_policy = DefaultSimPolicy()<br>    return g<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>is_simulation<\/code> \u8fd9\u4e2a\u6807\u5fd7\u4f1a\u6539\u53d8\u51e0\u4e2a\u884c\u4e3a\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>Discover<\/code> \u4e0d\u6302\u8d77\uff0c\u76f4\u63a5\u6309\u7b56\u7565\u9009<\/li>\n\n\n\n<li>\u4e0d\u4ea7\u751f PowerHistory<\/li>\n\n\n\n<li>\u4e0d\u505a\u7f51\u7edc\u540c\u6b65<\/li>\n\n\n\n<li>\u9690\u85cf\u4fe1\u606f\u7528<strong>\u91c7\u6837<\/strong>\u4ee3\u66ff\uff08\u4e0b\u4e00\u8282\uff09<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">28.5 \u6df1\u62f7\u8d1d\u7684\u6027\u80fd\u95ee\u9898<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>copy.deepcopy<\/code> \u5bf9\u4e8e\u4e00\u4e2a\u6709\u51e0\u767e\u4e2a\u5b9e\u4f53\u7684 GameState \u5927\u7ea6\u9700\u8981 1-3 \u6beb\u79d2\u3002\u5982\u679c MCTS \u8981\u8dd1 10000 \u6b21\u6a21\u62df\uff0c\u5149\u662f\u62f7\u8d1d\u5c31\u8981 10-30 \u79d2\u2014\u2014\u592a\u6162\u4e86\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4f18\u5316\u65b9\u6848\uff08\u6309\u6548\u679c\u6392\u5e8f\uff09<\/strong>\uff1a<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u65b9\u6848 A\uff1a\u624b\u5199 clone<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class Entity:<br>    __slots__ = (...)<br>    def clone(self, game):<br>        e = Entity.__new__(Entity)<br>        e.id = self.id<br>        e.game = game<br>        e.tags = self.tags.copy()          # dict.copy() \u6bd4 deepcopy \u5feb 20x<br>        e._enchantments = [x.clone(game) for x in self._enchantments]<br>        return e<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u5b9e\u6d4b\u80fd\u5feb 10-20 \u500d\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u65b9\u6848 B\uff1a\u7ed3\u6784\u5171\u4eab\uff08Persistent Data Structure\uff09<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u7528\u4e0d\u53ef\u53d8\u6570\u636e\u7ed3\u6784 + \u5199\u65f6\u590d\u5236\u3002Python \u91cc\u53ef\u4ee5\u7528 <code>pyrsistent<\/code>\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">from pyrsistent import pmap, pvector<br><br>@dataclass(frozen=True)<br>class GameState:<br>    entities: \"PMap[int, EntityState]\"<br>    zones: \"PMap[tuple, PVector]\"<br>    turn: int<br>    current: int<br><br>    def with_tag(self, eid, tag, value) -&gt; \"GameState\":<br>        e = self.entities[eid]<br>        return replace(self, entities=self.entities.set(eid, e.set(tag, value)))<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u62f7\u8d1d\u53d8\u6210 O(1)\uff0c\u4f46\u6bcf\u6b21\u4fee\u6539\u6709 O(log n) \u7684\u5f00\u9500\u3002\u5bf9\u4e8e MCTS \u8fd9\u79cd&#8221;\u62f7\u8d1d\u591a\u3001\u4fee\u6539\u5c11&#8221;\u7684\u573a\u666f\u662f\u51c0\u80dc\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u65b9\u6848 C\uff1aUndo Log\uff08\u64a4\u9500\u65e5\u5fd7\uff09<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u4e0d\u62f7\u8d1d\uff0c\u800c\u662f\u8bb0\u5f55\u6240\u6709\u53d8\u5316\uff0c\u6a21\u62df\u5b8c\u540e\u64a4\u9500\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class UndoableGame(Game):<br>    def __init__(self, ...):<br>        self._undo_stack = []<br><br>    def record_tag_change(self, e, tag, old, new):<br>        self._undo_stack.append((\"tag\", e.id, tag, old))<br><br>    def checkpoint(self) -&gt; int:<br>        return len(self._undo_stack)<br><br>    def rollback(self, cp: int):<br>        while len(self._undo_stack) &gt; cp:<br>            kind, *args = self._undo_stack.pop()<br>            if kind == \"tag\":<br>                eid, tag, old = args<br>                self.entities[eid].tags[tag] = old<br>            elif kind == \"zone\":<br>                ...<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8fd9\u662f\u6700\u5feb\u7684\u65b9\u6848<\/strong>\uff08O(\u53d8\u5316\u91cf) \u800c\u4e0d\u662f O(\u72b6\u6001\u5927\u5c0f)\uff09\uff0c\u4f46\u4e5f\u6700\u5bb9\u6613\u51fa bug\u2014\u2014\u4f60\u5fc5\u987b\u4fdd\u8bc1<strong>\u6bcf\u4e00\u79cd\u72b6\u6001\u53d8\u5316\u90fd\u88ab\u8bb0\u5f55<\/strong>\uff0c\u5305\u62ec\u5b9e\u4f53\u521b\u5efa\u3001\u533a\u57df\u79fb\u52a8\u3001\u89e6\u53d1\u5668\u6ce8\u518c\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6ce8\u610f<\/strong>\uff1a\u65b9\u6848 C \u6070\u597d\u4e5f\u662f\u5b9e\u73b0 2025 \u5e74\u300a\u5012\u5e26\u300b\u5173\u952e\u8bcd\u6240\u9700\u8981\u7684\u3002\u5982\u679c\u4f60\u7684\u5f15\u64ce\u6709 undo log\uff0c\u5012\u5e26\u5c31\u662f\u514d\u8d39\u7684\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6211\u7684\u5efa\u8bae<\/strong>\uff1a\u5148\u505a\u65b9\u6848 A\uff08\u7b80\u5355\u3001\u5feb 20 \u500d\u3001\u591f\u7528\uff09\uff0c\u53ea\u6709\u5728\u771f\u7684\u9700\u8981\u8dd1\u767e\u4e07\u6b21\u6a21\u62df\u65f6\u624d\u4e0a B \u6216 C\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 29 \u7ae0 \u7f51\u7edc\u540c\u6b65\u4e0e\u9690\u85cf\u4fe1\u606f<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">29.1 \u670d\u52a1\u5668\u6743\u5a01\u6a21\u578b<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">\u5ba2\u6237\u7aef                     \u670d\u52a1\u5668<br>  \u2502                          \u2502<br>  \u2502\u2500\u2500\u2500\u2500 PlayCard(id, tgt) \u2500\u2500&gt;\u2502<br>  \u2502                          \u2502 \u6821\u9a8c\u5408\u6cd5\u6027<br>  \u2502                          \u2502 \u6267\u884c play_card()<br>  \u2502                          \u2502 \u6536\u96c6 PowerHistory<br>  \u2502&lt;\u2500\u2500\u2500 PowerHistory[] \u2500\u2500\u2500\u2500\u2500\u2500\u2502<br>  \u2502 \u64ad\u653e\u52a8\u753b                  \u2502<br>  \u2502                          \u2502<br>  \u2502&lt;\u2500\u2500\u2500 PowerHistory[] \u2500\u2500\u2500\u2500\u2500\u2500\u2502 \uff08\u5bf9\u624b\u7684\u64cd\u4f5c\uff09<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5173\u952e\u70b9<\/strong>\uff1a\u5ba2\u6237\u7aef<strong>\u4ece\u4e0d\u8ba1\u7b97\u89c4\u5219<\/strong>\u3002\u5b83\u53ea\u505a\u4e24\u4ef6\u4e8b\uff1a\u53d1\u9001\u610f\u56fe\u3001\u64ad\u653e\u670d\u52a1\u5668\u8fd4\u56de\u7684\u53d8\u5316\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8fd9\u4e2a\u6a21\u578b\u7684\u4ee3\u4ef7<\/strong>\uff1a\u5ef6\u8fdf\u3002\u4f60\u70b9\u51fb\u5230\u770b\u5230\u6548\u679c\u4e4b\u95f4\u6709\u4e00\u4e2a RTT\u3002\u7089\u77f3\u901a\u8fc7&#8221;\u5361\u724c\u79bb\u624b\u7684\u52a8\u753b&#8221;\u6765\u63a9\u76d6\u8fd9\u4e2a\u5ef6\u8fdf\uff08\u52a8\u753b\u65f6\u957f &gt; RTT\uff09\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8fd9\u4e2a\u6a21\u578b\u7684\u6536\u76ca<\/strong>\uff1a<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>\u96f6\u4f5c\u5f0a\u9762<\/strong>\u3002\u5ba2\u6237\u7aef\u6539\u5185\u5b58\u6700\u591a\u80fd\u770b\u5230\u5b83\u672c\u6765\u5c31\u77e5\u9053\u7684\u4e1c\u897f<\/li>\n\n\n\n<li><strong>\u96f6\u56de\u6eda\u4ee3\u7801<\/strong><\/li>\n\n\n\n<li><strong>\u56de\u653e\u514d\u8d39<\/strong>\uff08PowerHistory \u5c31\u662f\u56de\u653e\u6570\u636e\uff09<\/li>\n\n\n\n<li><strong>\u89c2\u6218\u514d\u8d39<\/strong>\uff08\u540c\u4e00\u4efd PowerHistory \u53d1\u7ed9\u89c2\u6218\u8005\uff09<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">29.2 \u9690\u85cf\u4fe1\u606f\u7684\u8fc7\u6ee4<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u670d\u52a1\u5668\u6709\u5b8c\u6574\u72b6\u6001\uff0c\u4f46\u4e0d\u80fd\u5168\u53d1\u7ed9\u5ba2\u6237\u7aef\u3002\u8fc7\u6ee4\u89c4\u5219\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def filter_for_player(entity: Entity, viewer: int) -&gt; dict:<br>    \"\"\"\u8fd4\u56de\u8be5\u73a9\u5bb6\u80fd\u770b\u5230\u7684 tag \u5b50\u96c6\"\"\"<br>    zone = entity[Tag.ZONE]<br>    owner = entity[Tag.CONTROLLER]<br><br>    # \u5b8c\u5168\u53ef\u89c1<br>    if zone in (Zone.PLAY, Zone.GRAVEYARD):<br>        return entity.tags<br><br>    # \u81ea\u5df1\u7684\u624b\u724c\uff1a\u5168\u53ef\u89c1<br>    if zone == Zone.HAND and owner == viewer:<br>        return entity.tags<br><br>    # \u5bf9\u624b\u7684\u624b\u724c\uff1a\u53ea\u77e5\u9053\u5b58\u5728\u548c\u4f4d\u7f6e<br>    if zone == Zone.HAND:<br>        return {Tag.ENTITY_ID: entity.id, Tag.ZONE: zone,<br>                Tag.ZONE_POSITION: entity[Tag.ZONE_POSITION],<br>                Tag.CONTROLLER: owner}<br>        # \u2605 \u6ce8\u610f\uff1a\u4e0d\u53d1 CARD_ID\uff01<br><br>    # \u724c\u5e93\uff1a\u53ea\u77e5\u9053\u6570\u91cf<br>    if zone == Zone.DECK:<br>        return {Tag.ENTITY_ID: entity.id, Tag.ZONE: zone,<br>                Tag.CONTROLLER: owner}<br><br>    # \u5965\u79d8\uff1a\u5bf9\u624b\u53ea\u77e5\u9053\u5b58\u5728\uff0c\u9664\u975e\u5df2\u63ed\u793a<br>    if zone == Zone.SECRET:<br>        if owner == viewer or entity[Tag.REVEALED]:<br>            return entity.tags<br>        return {Tag.ENTITY_ID: entity.id, Tag.ZONE: zone,<br>                Tag.CONTROLLER: owner, Tag.CLASS: entity[Tag.CLASS]}<br>        # \u2605 \u804c\u4e1a\u662f\u53ef\u89c1\u7684\uff08\u5ba2\u6237\u7aef\u663e\u793a\u5965\u79d8\u56fe\u6807\u7684\u989c\u8272\uff09<br><br>    return {}<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4e00\u4e2a\u771f\u5b9e\u7684\u4fe1\u606f\u6cc4\u9732\u6559\u8bad<\/strong>\uff1a\u65e9\u671f\u7089\u77f3\u4f1a\u628a\u5965\u79d8\u7684 <code>CARD_ID<\/code> \u53d1\u7ed9\u5bf9\u624b\uff08\u867d\u7136 UI \u4e0d\u663e\u793a\uff09\uff0c\u7b2c\u4e09\u65b9\u5de5\u5177\u80fd\u8bfb\u51fa\u6765\u3002\u4fee\u590d\u540e\uff0c<code>CARD_ID<\/code> \u5728\u63ed\u793a\u524d\u4e0d\u4e0b\u53d1\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6d4b\u8bd5\u8fd9\u4e2a\u7684\u65b9\u6cd5<\/strong>\uff1a\u5199\u4e00\u4e2a\u6d4b\u8bd5\uff0c\u6a21\u62df\u4e00\u5c40\u6e38\u620f\uff0c\u68c0\u67e5\u53d1\u7ed9\u73a9\u5bb6 A \u7684\u6240\u6709 PowerHistory \u91cc<strong>\u4e0d\u5305\u542b<\/strong>\u73a9\u5bb6 B \u7684\u624b\u724c CARD_ID\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def test_no_hand_leak():<br>    g = make_game(seed=42)<br>    play_random_game(g)<br>    for msg in g.power_history_for(player=0):<br>        for change in msg.changes:<br>            e = g.entities[change.entity]<br>            if (e[Tag.ZONE] == Zone.HAND and e[Tag.CONTROLLER] == 1<br>                    and change.tag == Tag.CARD_ID):<br>                pytest.fail(f\"Leaked opponent hand card: {change}\")<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">29.3 \u5e26\u5bbd\u4f18\u5316<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u4e00\u5c40\u7089\u77f3\u5927\u7ea6\u4ea7\u751f <strong>2000-8000 \u4e2a PowerChange<\/strong>\u3002\u6bcf\u4e2a change \u5982\u679c\u7528 JSON \u5927\u7ea6 60 \u5b57\u8282\uff0c\u4e00\u5c40\u5c31\u662f 120-480 KB\u3002\u5728\u79fb\u52a8\u7f51\u7edc\u4e0a\u8fd9\u662f\u53ef\u63a5\u53d7\u7684\uff0c\u4f46\u53ef\u4ee5\u4f18\u5316\uff1a<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>\u53d8\u957f\u6574\u6570\u7f16\u7801<\/strong>\uff08protobuf varint\uff09\uff1atag \u503c\u5927\u591a\u662f\u5c0f\u6574\u6570<\/li>\n\n\n\n<li><strong>\u5b9e\u4f53 ID \u76f8\u5bf9\u7f16\u7801<\/strong>\uff1a\u8fde\u7eed\u7684 change \u5e38\u5e38\u9488\u5bf9\u540c\u4e00\u5b9e\u4f53<\/li>\n\n\n\n<li><strong>\u6279\u91cf\u4f4d\u7f6e\u66f4\u65b0<\/strong>\uff1a<code>ZONE_POSITION<\/code> \u7684\u91cd\u6392\u53ef\u4ee5\u538b\u6210\u4e00\u6761\u6d88\u606f<\/li>\n\n\n\n<li><strong>\u7701\u7565\u53ef\u63a8\u5bfc\u7684\u53d8\u5316<\/strong>\uff1a\u5ba2\u6237\u7aef\u80fd\u81ea\u5df1\u7b97\u51fa\u6765\u7684\u4e0d\u53d1\uff08\u4f46\u8fd9\u4f1a\u5f15\u5165&#8221;\u5ba2\u6237\u7aef\u903b\u8f91&#8221;\uff0c\u614e\u7528\uff09<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">\u7089\u77f3\u5b9e\u9645\u7528\u7684\u662f <strong>Protocol Buffers<\/strong>\uff08<strong>\u63a8\u65ad<\/strong>\uff0c\u793e\u533a\u6293\u5305\u8bc1\u5b9e\uff09\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">29.4 \u65ad\u7ebf\u91cd\u8fde<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u670d\u52a1\u5668\u5fc5\u987b\u80fd\u751f\u6210\u4e00\u4e2a<strong>\u5b8c\u6574\u72b6\u6001\u5feb\u7167<\/strong>\uff08\u800c\u4e0d\u662f\u589e\u91cf\uff09\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def full_state_snapshot(game, viewer: int) -&gt; dict:<br>    return {<br>        \"game_id\": game.id,<br>        \"turn\": game.turn,<br>        \"current_player\": game.current,<br>        \"entities\": [<br>            {\"id\": e.id, \"tags\": filter_for_player(e, viewer)}<br>            for e in game.all_entities()<br>            if filter_for_player(e, viewer)<br>        ],<br>        \"phase\": int(game.phase),<br>        \"pending_choice\": game.pending_choice_for(viewer),<br>    }<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u96be\u70b9<\/strong>\uff1a\u5982\u679c\u65ad\u7ebf\u65f6\u73a9\u5bb6\u6b63\u5728\u505a\u4e00\u4e2a\u300a\u53d1\u73b0\u300b\u9009\u62e9\uff0c\u91cd\u8fde\u540e\u5fc5\u987b\u6062\u590d\u90a3\u4e2a\u9009\u62e9\u6846\u3002\u6240\u4ee5 <code>pending_choice<\/code> \u5fc5\u987b\u662f\u53ef\u5e8f\u5217\u5316\u7684\u72b6\u6001\uff0c\u800c\u4e0d\u662f\u4e00\u4e2a Python \u95ed\u5305\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u8fd9\u662f\u6211\u5728 5.3 \u8282\u63a8\u8350\u7528 async \u800c\u4e0d\u662f\u5f02\u5e38\u7684\u53e6\u4e00\u4e2a\u7406\u7531\u2014\u2014<strong>async \u7684\u6302\u8d77\u70b9\u4e5f\u4e0d\u597d\u5e8f\u5217\u5316<\/strong>\u3002\u6700\u5065\u58ee\u7684\u505a\u6cd5\u662f\u628a&#8221;\u5f85\u51b3\u7b56&#8221;\u8868\u8fbe\u6210\u663e\u5f0f\u7684\u6570\u636e\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">@dataclass<br>class PendingChoice:<br>    kind: str                 # DISCOVER \/ ADAPT \/ CHOOSE_ONE \/ TARGET \/ MULLIGAN<br>    player: int<br>    options: list[str]        # \u5361\u724c ID<br>    continuation_id: str      # \u4e00\u4e2a\u53ef\u4ee5\u4ece\u72b6\u6001\u91cd\u5efa\u7684\u6807\u8bc6<br>    deadline: float<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u7136\u540e <code>continuation<\/code> \u901a\u8fc7\u4e00\u4e2a<strong>\u6ce8\u518c\u8868<\/strong>\u67e5\u627e\uff0c\u800c\u4e0d\u662f\u95ed\u5305\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">CONTINUATIONS = {}<br><br>def continuation(name):<br>    def deco(f):<br>        CONTINUATIONS[name] = f<br>        return f<br>    return deco<br><br>@continuation(\"DISCOVER_ADD_TO_HAND\")<br>def _(game, ctx, chosen):<br>    game.add_to_hand(ctx.controller, chosen)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u8fd9\u6837\u6574\u4e2a&#8221;\u5f85\u51b3\u7b56&#8221;\u72b6\u6001\u5c31\u662f\u7eaf\u6570\u636e\uff0c\u53ef\u4ee5\u5e8f\u5217\u5316\u3001\u53ef\u4ee5\u5b58\u6570\u636e\u5e93\u3001\u53ef\u4ee5\u5728\u670d\u52a1\u5668\u91cd\u542f\u540e\u6062\u590d\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 30 \u7ae0 AI \u4e0e\u6a21\u62df\u5668<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">30.1 \u4e3a\u4ec0\u4e48\u7089\u77f3 AI \u5f88\u96be<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u96be\u70b9<\/th><th>\u8bf4\u660e<\/th><\/tr><\/thead><tbody><tr><td><strong>\u4e0d\u5b8c\u5168\u4fe1\u606f<\/strong><\/td><td>\u770b\u4e0d\u5230\u5bf9\u624b\u624b\u724c\u3001\u724c\u5e93<\/td><\/tr><tr><td><strong>\u9ad8\u5206\u652f\u56e0\u5b50<\/strong><\/td><td>\u4e00\u4e2a\u56de\u5408\u53ef\u80fd\u6709 10<sup>3\uff5e10<\/sup>5 \u4e2a\u5408\u6cd5\u52a8\u4f5c\u5e8f\u5217<\/td><\/tr><tr><td><strong>\u968f\u673a\u6027<\/strong><\/td><td>\u540c\u4e00\u4e2a\u52a8\u4f5c\u6709\u591a\u79cd\u7ed3\u679c<\/td><\/tr><tr><td><strong>\u957f\u671f\u89c4\u5212<\/strong><\/td><td>\u4e00\u5c40 10-20 \u56de\u5408\uff0c\u9700\u8981\u8d44\u6e90\u7ba1\u7406<\/td><\/tr><tr><td><strong>\u5361\u724c\u8bed\u4e49<\/strong><\/td><td>6000 \u5f20\u5361\uff0c\u65e0\u6cd5\u624b\u5199\u542f\u53d1\u5f0f<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">\u5206\u652f\u56e0\u5b50\u7684\u4f30\u7b97\uff1a\u5047\u8bbe\u624b\u91cc 7 \u5f20\u724c\uff0c\u573a\u4e0a 5 \u4e2a\u968f\u4ece\uff0c\u5bf9\u9762 5 \u4e2a\u968f\u4ece + \u82f1\u96c4\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u51fa\u724c\uff1a7 \u5f20 \u00d7 \u5e73\u5747 3 \u4e2a\u76ee\u6807 \u00d7 8 \u4e2a\u4f4d\u7f6e \u2248 168<\/li>\n\n\n\n<li>\u653b\u51fb\uff1a5 \u4e2a\u653b\u51fb\u8005 \u00d7 6 \u4e2a\u76ee\u6807 = 30<\/li>\n\n\n\n<li>\u82f1\u96c4\u6280\u80fd\uff1a1-3<\/li>\n\n\n\n<li>\u987a\u5e8f\u7ec4\u5408\uff1a\u4e00\u4e2a\u56de\u5408\u53ef\u80fd\u6253 3-4 \u5f20\u724c\uff0c\u987a\u5e8f\u6709\u610f\u4e49<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">\u7c97\u7b97\u5355\u56de\u5408\u7684\u52a8\u4f5c\u5e8f\u5217\u6570\u5728 10^4 \u5230 10^6 \u4e4b\u95f4\u3002\u8fd9\u6bd4\u56f4\u68cb\u7684\u5355\u6b65\u5206\u652f\uff08~250\uff09\u5927\u5f97\u591a\uff0c\u4f46\u6df1\u5ea6\u6d45\u5f97\u591a\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">30.2 \u52a8\u4f5c\u7a7a\u95f4\u7684\u8868\u793a<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">@dataclass(frozen=True)<br>class GameAction:<br>    kind: str            # PLAY \/ ATTACK \/ HERO_POWER \/ END_TURN \/<br>                         # TRADE \/ FORGE \/ PREPARE \/ USE_LOCATION \/ TITAN_ABILITY<br>    card_id: int = 0     # entity id<br>    target_id: int = 0<br>    position: int = -1<br>    choice: int = 0      # \u6289\u62e9\/\u53d1\u73b0\u7684\u9009\u9879\u7d22\u5f15<br><br>def legal_actions(game, pid) -&gt; list[GameAction]:<br>    out = [GameAction(\"END_TURN\")]<br>    p = game.player(pid)<br><br>    # \u51fa\u724c<br>    for c in game.zones.get(pid, Zone.HAND):<br>        cost = game.compute_cost(c)<br>        if cost &gt; p[Tag.MANA_AVAILABLE]:<br>            continue<br>        if c[Tag.LOCKED_UNTIL_TURN] &gt; game.turn:      # Prepare \u9501\u5b9a<br>            continue<br>        targets = game.legal_targets_for(c) or [None]<br>        positions = (range(len(game.zones.get(pid, Zone.PLAY)) + 1)<br>                     if c.is_minion else [-1])<br>        choices = range(len(c.defn.choose_one)) if c.defn.choose_one else [0]<br>        for t in targets:<br>            for pos in positions:<br>                for ch in choices:<br>                    out.append(GameAction(\"PLAY\", c.id,<br>                                          t.id if t else 0, pos, ch))<br>        if c.has(Tag.TRADEABLE) and p[Tag.MANA_AVAILABLE] &gt;= 1:<br>            out.append(GameAction(\"TRADE\", c.id))<br>        if c.has(Tag.PREPARE) and p[Tag.MANA_AVAILABLE] &gt;= 1:<br>            for amt in range(1, p[Tag.MANA_AVAILABLE] + 1):<br>                out.append(GameAction(\"PREPARE\", c.id, choice=amt))<br><br>    # \u653b\u51fb<br>    for a in game.zones.get(pid, Zone.PLAY) + [p.hero]:<br>        if not game.can_attack(a): continue<br>        for d in game.legal_attack_targets(a):<br>            out.append(GameAction(\"ATTACK\", a.id, d.id))<br><br>    # \u82f1\u96c4\u6280\u80fd<br>    if game.can_use_hero_power(pid):<br>        hp = p.hero_power<br>        for t in (game.legal_targets_for(hp) or [None]):<br>            out.append(GameAction(\"HERO_POWER\", hp.id, t.id if t else 0))<br><br>    return out<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6ce8\u610f <code>PREPARE<\/code> \u8ba9\u5206\u652f\u56e0\u5b50\u7206\u70b8<\/strong>\uff1a\u4e00\u5f20\u53ef\u51c6\u5907\u7684\u724c\uff0c\u6bcf\u4e2a\u53ef\u80fd\u7684\u6cd5\u529b\u503c\u6d88\u8017\u91cf\u90fd\u662f\u4e00\u4e2a\u5206\u652f\u3002\u5b9e\u8df5\u4e2d AI \u5e94\u8be5\u526a\u679d\uff08\u53ea\u8003\u8651&#8221;\u82b1\u5149\u6240\u6709\u6cd5\u529b&#8221;\u548c&#8221;\u82b1 1 \u70b9&#8221;\u4e24\u4e2a\u9009\u9879\uff09\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">30.3 MCTS \u4e0e\u4fe1\u606f\u96c6<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u6807\u51c6 MCTS \u5047\u8bbe\u5b8c\u5168\u4fe1\u606f\u3002\u5bf9\u4e8e\u7089\u77f3\uff0c\u8981\u7528 <strong>ISMCTS\uff08Information Set MCTS\uff09<\/strong>\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def ismcts(root_state, pid, iterations=10000):<br>    root = Node(None, None)<br>    for _ in range(iterations):<br>        # \u2605 1. \u786e\u5b9a\u5316\uff08Determinization\uff09\uff1a\u7ed9\u9690\u85cf\u4fe1\u606f\u91c7\u6837\u4e00\u4e2a\u5177\u4f53\u503c<br>        state = determinize(root_state, pid)<br>        node = root<br>        path = [node]<br><br>        # 2. Selection<br>        while node.children and not state.is_terminal():<br>            legal = legal_actions(state, state.current)<br>            node = node.select_uct(legal)<br>            state.apply(node.action)<br>            path.append(node)<br><br>        # 3. Expansion<br>        if not state.is_terminal():<br>            legal = legal_actions(state, state.current)<br>            untried = [a for a in legal if a not in node.children]<br>            if untried:<br>                a = random.choice(untried)<br>                state.apply(a)<br>                node = node.add_child(a)<br>                path.append(node)<br><br>        # 4. Simulation\uff08rollout\uff09<br>        result = rollout(state, pid, max_depth=30)<br><br>        # 5. Backpropagation<br>        for n in path:<br>            n.visits += 1<br>            n.wins += result<br><br>    return max(root.children.values(), key=lambda n: n.visits).action<br><br><br>def determinize(state, pid):<br>    \"\"\"\u628a\u5bf9\u624b\u7684\u624b\u724c\u548c\u53cc\u65b9\u724c\u5e93\u66ff\u6362\u6210\u4e00\u4e2a\u7b26\u5408\u5df2\u77e5\u7ea6\u675f\u7684\u5177\u4f53\u91c7\u6837\u3002\"\"\"<br>    s = state.clone()<br>    opp = 1 - pid<br>    # \u5df2\u77e5\u7ea6\u675f\uff1a\u5bf9\u624b\u804c\u4e1a\u3001\u5df2\u6253\u51fa\u7684\u724c\u3001\u724c\u5e93\u5269\u4f59\u6570\u91cf\u3001\u53ef\u80fd\u7684\u5361\u7ec4\u539f\u578b<br>    known = s.observed_cards(opp)<br>    archetype = infer_archetype(known)               # \u5143\u6e38\u620f\u77e5\u8bc6<br>    pool = archetype.remaining_cards(known)<br>    for card in s.zones.get(opp, Zone.HAND):<br>        card.set_card_id(sample_from(pool))<br>    for card in s.zones.get(opp, Zone.DECK):<br>        card.set_card_id(sample_from(pool))<br>    return s<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>infer_archetype<\/code> \u662f\u7089\u77f3 AI \u6700\u6709\u4ef7\u503c\u7684\u90e8\u5206<\/strong>\u3002\u56e0\u4e3a\u6784\u7b51\u5361\u7ec4\u4e0d\u662f\u968f\u673a\u7684\u2014\u2014\u5982\u679c\u5bf9\u624b\u6253\u4e86\u300a\u5bd2\u51b0\u7bad\u300b\u548c\u300a\u5965\u672f\u667a\u6167\u300b\uff0c\u4ed6\u5927\u6982\u7387\u662f\u6cd5\u5e08\u63a7\u5236\u6216\u8005\u5965\u79d8\u6cd5\uff0c\u4f60\u53ef\u4ee5\u7528\u5143\u6e38\u620f\u6570\u636e\uff08HSReplay \u7684\u516c\u5f00\u5361\u7ec4\u7edf\u8ba1\uff09\u6765\u91c7\u6837\u4e00\u4e2a\u5408\u7406\u7684\u724c\u5e93\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u8fd9\u6bd4&#8221;\u5747\u5300\u4ece\u6240\u6709\u6cd5\u5e08\u5361\u91cc\u91c7\u6837&#8221;\u5f3a\u5f97\u591a\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">30.4 \u72b6\u6001\u8bc4\u4f30\u51fd\u6570<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Rollout \u5230\u7ec8\u5c40\u592a\u6162\uff08\u4e00\u5c40 20 \u56de\u5408 \u00d7 \u6bcf\u56de\u5408 10 \u4e2a\u52a8\u4f5c = 200 \u6b65\uff09\u3002\u5b9e\u8df5\u4e2d\u7528<strong>\u8bc4\u4f30\u51fd\u6570 + \u6d45\u5c42\u641c\u7d22<\/strong>\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def evaluate(state, pid) -&gt; float:<br>    me, opp = state.player(pid), state.player(1 - pid)<br><br>    # 1. \u751f\u547d\u503c\uff08\u975e\u7ebf\u6027\uff1a\u4f4e\u8840\u66f4\u5371\u9669\uff09<br>    def hp_score(p):<br>        h = p.hero.current_health + p.hero[Tag.ARMOR]<br>        return math.log1p(max(0, h)) * 3<br>    score = hp_score(me) - hp_score(opp)<br><br>    # 2. \u573a\u9762<br>    def board_score(pid_):<br>        s = 0<br>        for m in state.zones.get(pid_, Zone.PLAY):<br>            v = m[Tag.ATK] * 1.0 + m.current_health * 1.0<br>            if m.has(Tag.TAUNT):         v += m.current_health * 0.5<br>            if m.has(Tag.DIVINE_SHIELD): v += m[Tag.ATK] * 0.5 + 1<br>            if m.has(Tag.POISONOUS):     v += 3<br>            if m.has(Tag.LIFESTEAL):     v += m[Tag.ATK] * 0.5<br>            if m.has(Tag.WINDFURY):      v += m[Tag.ATK] * 0.8<br>            if m.defn.deathrattle:       v += 1.5<br>            if m.defn.auras:             v += 2<br>            if m.has(Tag.STEALTH):       v += 1<br>            if m[Tag.FROZEN]:            v -= m[Tag.ATK] * 0.5<br>            s += v<br>        return s<br>    score += (board_score(pid) - board_score(1 - pid)) * 1.5<br><br>    # 3. \u5361\u5dee<br>    score += (len(state.zones.get(pid, Zone.HAND))<br>              - len(state.zones.get(1 - pid, Zone.HAND))) * 2.0<br><br>    # 4. \u8d44\u6e90<br>    score += (me[Tag.MANA_CRYSTALS] - opp[Tag.MANA_CRYSTALS]) * 0.5<br>    score -= me[Tag.OVERLOAD_OWED] * 0.8<br><br>    # 5. \u75b2\u52b3\u98ce\u9669<br>    score -= max(0, 5 - len(state.zones.get(pid, Zone.DECK))) * 1.5<br><br>    # 6. \u81f4\u547d\u68c0\u67e5\uff08\u538b\u5012\u4e00\u5207\uff09<br>    if opp.hero.current_health + opp[Tag.ARMOR] &lt;= 0:  return 1e9<br>    if me.hero.current_health + me[Tag.ARMOR] &lt;= 0:    return -1e9<br><br>    return score<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8fd9\u4e2a\u51fd\u6570\u7684\u6bcf\u4e00\u9879\u90fd\u53ef\u4ee5\u4ece\u5361\u724c DSL \u81ea\u52a8\u5bfc\u51fa<\/strong>\u2014\u2014\u8fd9\u662f\u6570\u636e\u9a71\u52a8\u7684\u53c8\u4e00\u4e2a\u7ea2\u5229\u3002<code>m.defn.deathrattle is not None<\/code> \u52a0 1.5 \u5206\uff0c\u4e0d\u9700\u8981\u4e3a\u6bcf\u5f20\u5361\u624b\u5199\u89c4\u5219\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u66f4\u8fdb\u4e00\u6b65\uff0c\u4f60\u53ef\u4ee5\u4ece DSL <strong>\u9759\u6001\u4f30\u7b97\u4e00\u5f20\u5361\u7684&#8221;\u4ef7\u503c&#8221;<\/strong>\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def estimate_card_value(defn: CardDef) -&gt; float:<br>    v = 0.0<br>    for action in walk_actions(defn.all_actions()):<br>        if isinstance(action, Damage):<br>            v += action.static_amount() * 1.0<br>        elif isinstance(action, Draw):<br>            v += action.static_amount() * 2.0<br>        elif isinstance(action, Summon):<br>            v += estimate_card_value(CARD_DB[action.card_id]) * action.count<br>        elif isinstance(action, Destroy):<br>            v += 4.0<br>        elif isinstance(action, Heal):<br>            v += action.static_amount() * 0.6<br>    v += defn.attack * 1.0 + defn.health * 1.0<br>    for kw in defn.keywords:<br>        v += KEYWORD_VALUE.get(kw, 0)<br>    return v - defn.cost * 2.0        # \u8d39\u7528\u662f\u8d1f\u9879<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8fd9\u4e2a\u51fd\u6570\u80fd\u81ea\u52a8\u7ed9 6000 \u5f20\u5361\u6253\u5206<\/strong>\uff0c\u800c\u4e14\u65b0\u5361\u4e0a\u7ebf\u65f6\u96f6\u6539\u52a8\u3002\u5b83\u751a\u81f3\u80fd\u7528\u6765\u505a<strong>\u81ea\u52a8\u5e73\u8861\u6027\u68c0\u67e5<\/strong>\uff1a&#8221;\u8fd9\u5f20\u5361\u7684\u4f30\u503c\u8fdc\u9ad8\u4e8e\u540c\u8d39\u7528\u7684\u4e2d\u4f4d\u6570\uff0c\u53ef\u80fd\u8d85\u6a21&#8221;\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">30.5 \u5206\u5c42\u641c\u7d22\uff1a\u5148\u89e3\u573a\u9762\u518d\u89c4\u5212<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u4e00\u4e2a\u5b9e\u7528\u7684\u5de5\u7a0b\u6280\u5de7\uff1a<strong>\u628a&#8221;\u8fd9\u4e2a\u56de\u5408\u600e\u4e48\u6253&#8221;\u548c&#8221;\u6574\u5c40\u600e\u4e48\u6253&#8221;\u5206\u5f00<\/strong>\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def decide_turn(game, pid):<br>    # \u7b2c\u4e00\u5c42\uff1a\u679a\u4e3e\u672c\u56de\u5408\u7684\u52a8\u4f5c\u5e8f\u5217\uff0c\u7528\u8bc4\u4f30\u51fd\u6570\u6253\u5206<br>    best_seq, best_score = None, -inf<br>    for seq in enumerate_turn_sequences(game, pid, beam_width=200):<br>        s = game.clone()<br>        for a in seq:<br>            s.apply(a)<br>        score = evaluate(s, pid)<br>        # \u7b2c\u4e8c\u5c42\uff1a\u7c97\u7565\u6a21\u62df\u5bf9\u624b\u7684\u56de\u5e94<br>        score -= estimate_opponent_best_response(s, 1 - pid)<br>        if score &gt; best_score:<br>            best_seq, best_score = seq, score<br>    return best_seq<br><br><br>def enumerate_turn_sequences(game, pid, beam_width=200):<br>    \"\"\"\u675f\u641c\u7d22\uff1a\u6bcf\u6b65\u4fdd\u7559\u6700\u597d\u7684 N \u4e2a\u90e8\u5206\u5e8f\u5217\"\"\"<br>    beam = [([], game.clone(), 0.0)]<br>    for _ in range(12):        # \u4e00\u56de\u5408\u6700\u591a 12 \u4e2a\u52a8\u4f5c<br>        nxt = []<br>        for seq, state, _ in beam:<br>            for a in legal_actions(state, pid):<br>                if a.kind == \"END_TURN\":<br>                    nxt.append((seq, state, evaluate(state, pid)))<br>                    continue<br>                s2 = state.clone()<br>                s2.apply(a)<br>                nxt.append((seq + [a], s2, evaluate(s2, pid)))<br>        nxt.sort(key=lambda x: -x[2])<br>        beam = nxt[:beam_width]<br>        if all(s[0] and s[0][-1].kind == \"END_TURN\" for s in beam):<br>            break<br>    return [s[0] for s in beam]<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u675f\u641c\u7d22\uff08beam search\uff09\u5728\u5b9e\u8df5\u4e2d\u6bd4 MCTS \u66f4\u5b9e\u7528<\/strong>\uff0c\u56e0\u4e3a\uff1a<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>\u7089\u77f3\u7684\u56de\u5408\u5185\u51b3\u7b56<strong>\u6ca1\u6709\u968f\u673a\u6027\u5f71\u54cd\u987a\u5e8f<\/strong>\uff08\u5927\u90e8\u5206\u60c5\u51b5\uff09<\/li>\n\n\n\n<li>\u8bc4\u4f30\u51fd\u6570\u5df2\u7ecf\u8db3\u591f\u597d<\/li>\n\n\n\n<li>\u53ef\u9884\u6d4b\u7684\u65f6\u95f4\u5f00\u9500\uff08beam_width \u00d7 depth \u6b21\u8bc4\u4f30\uff09<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8fd9\u4e5f\u662f\u793e\u533a AI\uff08\u5982 Silverfish\u3001SabberStone \u7684 AI\uff09\u5b9e\u9645\u91c7\u7528\u7684\u65b9\u6848\u3002<\/strong><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 31 \u7ae0 \u6027\u80fd<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">31.1 \u70ed\u70b9\u5728\u54ea\u91cc<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u7528 cProfile \u8dd1 10000 \u5c40\u968f\u673a\u5bf9\u5c40\uff0c\u70ed\u70b9\u5206\u5e03\u5927\u81f4\u662f\uff08\u6211\u7684\u8ff7\u4f60\u5f15\u64ce\u5b9e\u6d4b\uff09\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">  32%  aura_bus.recompute       \u2190 \u5149\u73af\u91cd\u7b97<br>  18%  selector.eval            \u2190 \u7b5b\u9009\u5668\u6c42\u503c<br>  14%  copy \/ clone             \u2190 \u72b6\u6001\u62f7\u8d1d\uff08AI \u573a\u666f\uff09<br>  11%  event_bus.emit           \u2190 \u4e8b\u4ef6\u5339\u914d<br>   9%  entity.__setitem__       \u2190 tag \u5199\u5165 + \u8bb0\u5f55<br>   7%  process_deaths<br>   9%  \u5176\u4ed6<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">31.2 \u5149\u73af\u91cd\u7b97\u4f18\u5316<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4f18\u5316 1\uff1a\u810f\u6807\u8bb0\u5206\u7ea7<\/strong>\uff0829.3 \u8bb2\u8fc7\uff09<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4f18\u5316 2\uff1a\u7f13\u5b58\u5149\u73af\u6e90\u5217\u8868<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class AuraBus:<br>    def __init__(self, game):<br>        self._sources_cache = None<br>        self._sources_dirty = True<br><br>    def _collect_sources(self):<br>        if not self._sources_dirty:<br>            return self._sources_cache<br>        # \u53ea\u5728\u5b9e\u4f53\u8fdb\u51fa\u573a\u65f6\u91cd\u65b0\u6536\u96c6<br>        ...<br>        self._sources_dirty = False<br>        return self._sources_cache<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u5927\u90e8\u5206\u56de\u5408\u91cc\u6ca1\u6709\u5b9e\u4f53\u8fdb\u51fa\u573a\uff08\u53ea\u662f\u653b\u51fb\u548c\u6570\u503c\u53d8\u5316\uff09\uff0c\u6240\u4ee5\u6e90\u5217\u8868\u53ef\u4ee5\u590d\u7528\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4f18\u5316 3\uff1a\u65e9\u9000<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u5982\u679c\u573a\u4e0a\u6ca1\u6709\u4efb\u4f55\u5149\u73af\u6e90\uff08\u5f88\u5e38\u89c1\uff09\uff0c\u76f4\u63a5\u8df3\u8fc7\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def recompute(self):<br>    srcs = self._collect_sources()<br>    if not srcs and not self._deltas:<br>        self._dirty = False<br>        return                     # \u2605 \u96f6\u6210\u672c<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">31.3 Selector \u6c42\u503c\u4f18\u5316<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4f18\u5316 1\uff1a\u907f\u514d\u91cd\u590d\u6784\u5efa\u5217\u8868<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"># \u274c \u6bcf\u6b21\u90fd\u5efa\u65b0 list<br>class Friendly(Selector):<br>    def eval(self, game, ctx):<br>        return [e for e in self.inner.eval(game, ctx) if ...]<br><br># \u2713 \u7528\u751f\u6210\u5668 + \u53ea\u5728\u6700\u5916\u5c42\u7269\u5316<br>class Friendly(Selector):<br>    def iter_eval(self, game, ctx):<br>        me = ctx.controller<br>        for e in self.inner.iter_eval(game, ctx):<br>            if e[Tag.CONTROLLER] == me:<br>                yield e<br>    def eval(self, game, ctx):<br>        return list(self.iter_eval(game, ctx))<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4f18\u5316 2\uff1a\u5e38\u91cf\u6298\u53e0<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>Friendly(AllMinions())<\/code> \u6bcf\u6b21\u90fd\u8981\u904d\u5386\u5168\u573a\u518d\u8fc7\u6ee4\u3002\u53ef\u4ee5\u7279\u5316\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">class FriendlyMinions(Selector):<br>    \"\"\"\u7279\u5316\u7248\u672c\uff0c\u76f4\u63a5\u67e5\u4e00\u4e2a\u533a\u57df\"\"\"<br>    def eval(self, game, ctx):<br>        return [e for e in game.zones.get(ctx.controller, Zone.PLAY)<br>                if e[Tag.CARDTYPE] == CardType.MINION]<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u5728 Selector \u6784\u5efa\u65f6\u505a\u4e00\u6b21&#8221;\u4f18\u5316 pass&#8221;\uff0c\u628a\u5e38\u89c1\u7ec4\u5408\u66ff\u6362\u6210\u7279\u5316\u5b9e\u73b0\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">31.4 \u4e8b\u4ef6\u5339\u914d\u4f18\u5316<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>emit<\/code> \u8981\u904d\u5386\u6240\u6709\u8ba2\u9605\u8005\u3002\u4f18\u5316\uff1a<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6309\u4e8b\u4ef6\u7c7b\u578b\u5206\u6876<\/strong>\uff08\u5df2\u505a\uff09+ <strong>\u6309\u533a\u57df\u4e8c\u7ea7\u5206\u6876<\/strong>\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">self._subs: dict[Event, dict[Zone, list[tuple[Entity, TriggerDef]]]]<br><br>def emit(self, ctx):<br>    buckets = self._subs.get(ctx.kind)<br>    if not buckets: return<br>    matched = []<br>    for zone, lst in buckets.items():<br>        for owner, td in lst:<br>            if owner[Tag.ZONE] != zone: continue     # \u5df2\u7ecf\u9884\u7b5b<br>            ...<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u66f4\u6fc0\u8fdb\uff1a\u9884\u7f16\u8bd1 Condition<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u628a Condition \u6811\u7f16\u8bd1\u6210\u4e00\u4e2a Python \u51fd\u6570\uff08\u7528 <code>eval<\/code> \u6216\u8005 <code>ast<\/code>\uff09\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def compile_condition(cond) -&gt; Callable:<br>    src = cond.to_python_expr()      # \"e.tags.get(5,0) &gt; 0 and ctx.amount &gt; 0\"<br>    return eval(f\"lambda game, e, ctx: ({src})\")<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u5b9e\u6d4b\u80fd\u5feb 3-5 \u500d\uff0c\u56e0\u4e3a\u7701\u6389\u4e86\u5bf9\u8c61\u65b9\u6cd5\u8c03\u7528\u7684\u5f00\u9500\u3002\u4f46\u4ee3\u4ef7\u662f\u53ef\u8bfb\u6027\u548c\u8c03\u8bd5\u96be\u5ea6\u2014\u2014<strong>\u53ea\u5728\u786e\u5b9e\u662f\u74f6\u9888\u65f6\u624d\u505a<\/strong>\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">31.5 \u4e00\u4e2a\u53cd\u76f4\u89c9\u7684\u7ed3\u8bba<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u6211\u5728\u4f18\u5316\u8ff7\u4f60\u5f15\u64ce\u65f6\u53d1\u73b0\uff1a<strong><code>entity[Tag.X]<\/code> \u8fd9\u4e2a\u770b\u8d77\u6765\u65e0\u5bb3\u7684\u64cd\u4f5c\u662f\u6700\u5927\u7684\u9690\u6027\u5f00\u9500<\/strong>\uff0c\u56e0\u4e3a\u5b83\u5728\u6574\u4e2a\u5f15\u64ce\u91cc\u88ab\u8c03\u7528\u51e0\u5341\u4e07\u6b21\u3002<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"># \u539f\u59cb\u7248\u672c<br>def __getitem__(self, t):<br>    return self.tags.get(t, 0)      # dict.get + \u9ed8\u8ba4\u503c<br><br># \u4f18\u5316\u7248\u672c\uff1a\u7528 defaultdict \u6d88\u9664 get \u7684\u5206\u652f<br>self.tags = defaultdict(int)<br>def __getitem__(self, t):<br>    return self.tags[t]<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u5b9e\u6d4b\u5feb 15%\u3002\u4f46\u66f4\u597d\u7684\u505a\u6cd5\u662f<strong>\u5728\u70ed\u70b9\u8def\u5f84\u4e0a\u76f4\u63a5\u8bbf\u95ee <code>e.tags<\/code><\/strong>\uff0c\u7ed5\u8fc7 <code>__getitem__<\/code>\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"># \u5728 can_attack \u8fd9\u79cd\u6bcf\u5e27\u8c03\u7528\u51e0\u767e\u6b21\u7684\u51fd\u6570\u91cc<br>def can_attack(e) -&gt; bool:<br>    t = e.tags                       # \u5c40\u90e8\u53d8\u91cf\u7ed1\u5b9a<br>    if t.get(Tag.FROZEN): return False<br>    if t.get(Tag.ATK, 0) &lt;= 0: return False<br>    ...<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4f46\u4e0d\u8981\u8fc7\u65e9\u4f18\u5316<\/strong>\u3002\u6211\u7684\u8ff7\u4f60\u5f15\u64ce\u5728\u4e0d\u4f18\u5316\u7684\u60c5\u51b5\u4e0b\u80fd\u8dd1\u7ea6 400 \u5c40\/\u79d2\uff08\u5355\u6838 Python\uff09\uff0c\u5bf9\u4e8e\u5f00\u53d1\u548c\u6d4b\u8bd5\u5b8c\u5168\u591f\u7528\u3002\u771f\u6b63\u9700\u8981\u4f18\u5316\u662f\u5f53\u4f60\u8981\u8dd1\u767e\u4e07\u7ea7\u6a21\u62df\u505a AI \u8bad\u7ec3\u65f6\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 32 \u7ae0 \u6d4b\u8bd5\u7b56\u7565<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">32.1 \u4e3a\u4ec0\u4e48\u5355\u5143\u6d4b\u8bd5\u4e0d\u591f<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">10^7 \u5bf9\u5361\u724c\u4ea4\u4e92\uff0c\u4f60\u4e0d\u53ef\u80fd\u5199\u5b8c\u3002\u6240\u4ee5\u6d4b\u8bd5\u7b56\u7565\u5fc5\u987b\u5206\u5c42\uff1a<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u5c42\u4e00\uff1a\u89c4\u5219\u4e0d\u53d8\u5f0f\uff08Property-based\uff09<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u7528 Hypothesis \u751f\u6210\u968f\u673a\u5bf9\u5c40\uff0c\u68c0\u67e5\u4e0d\u53d8\u5f0f\u6c38\u8fdc\u6210\u7acb\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">from hypothesis import given, strategies as st<br><br>@given(seed=st.integers(0, 2**32))<br>def test_invariants_hold(seed):<br>    g = make_random_game(seed)<br>    while not g.is_over and g.turn &lt; 60:<br>        actions = legal_actions(g, g.current)<br>        g.apply(random.choice(actions))<br>        check_invariants(g)<br><br>def check_invariants(g):<br>    for pid in (0, 1):<br>        board = g.zones.get(pid, Zone.PLAY)<br>        # \u6218\u573a\u4e0d\u8d85\u8fc7 7<br>        assert len(board) &lt;= 7<br>        # \u4f4d\u7f6e\u8fde\u7eed\u4e14\u4ece 1 \u5f00\u59cb<br>        assert [e[Tag.ZONE_POSITION] for e in board] == list(range(1, len(board)+1))<br>        # \u624b\u724c\u4e0d\u8d85\u8fc7 10<br>        assert len(g.zones.get(pid, Zone.HAND)) &lt;= 10<br>        # \u6ca1\u6709\u751f\u547d\u503c &lt;= 0 \u7684\u968f\u4ece\u6d3b\u5728\u573a\u4e0a<br>        for m in board:<br>            assert m.current_health &gt; 0, f\"{m} should be dead\"<br>        # \u6cd5\u529b\u503c\u975e\u8d1f\u4e14\u4e0d\u8d85\u8fc7\u6c34\u6676\u6570<br>        p = g.player(pid)<br>        assert 0 &lt;= p[Tag.MANA_AVAILABLE] &lt;= p[Tag.MANA_CRYSTALS]<br>        # \u6bcf\u4e2a\u5b9e\u4f53\u53ea\u5728\u4e00\u4e2a\u533a\u57df<br>    seen = set()<br>    for pid in (0, 1):<br>        for z in Zone:<br>            for e in g.zones.get(pid, z):<br>                assert e.id not in seen, f\"{e.id} in two zones\"<br>                seen.add(e.id)<br>                assert e[Tag.ZONE] == z<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8fd9\u7c7b\u6d4b\u8bd5\u7684\u4ef7\u503c\u6781\u9ad8<\/strong>\uff1a\u4e00\u4e2a\u4e0d\u53d8\u5f0f\u80fd\u8986\u76d6\u6210\u5343\u4e0a\u4e07\u79cd\u60c5\u51b5\u3002\u6211\u5728\u5199\u8ff7\u4f60\u5f15\u64ce\u65f6\uff0c<code>ZONE_POSITION<\/code> \u8fde\u7eed\u6027\u8fd9\u4e00\u6761\u5c31\u6293\u51fa\u4e86\u4e09\u4e2a bug\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u5c42\u4e8c\uff1a\u4ea4\u4e92\u77e9\u9635\uff08Combinatorial\uff09<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u81ea\u52a8\u679a\u4e3e\u5173\u952e\u8bcd\u7684\u4e24\u4e24\u7ec4\u5408\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">KEYWORD_TEST_CARDS = {<br>    \"TAUNT\": \"TEST_TAUNT\", \"DIVINE_SHIELD\": \"TEST_DS\",<br>    \"POISONOUS\": \"TEST_POISON\", \"LIFESTEAL\": \"TEST_LS\",<br>    \"STEALTH\": \"TEST_STEALTH\", \"WINDFURY\": \"TEST_WF\",<br>    \"RUSH\": \"TEST_RUSH\", \"REBORN\": \"TEST_REBORN\",<br>}<br><br>@pytest.mark.parametrize(\"kw_a,kw_b\",<br>                         itertools.combinations(KEYWORD_TEST_CARDS, 2))<br>def test_keyword_pair_no_crash(kw_a, kw_b):<br>    g = make_test_game()<br>    a = g.summon_test(KEYWORD_TEST_CARDS[kw_a], player=0)<br>    b = g.summon_test(KEYWORD_TEST_CARDS[kw_b], player=1)<br>    g.attack(a, b)<br>    check_invariants(g)<br>    g.process_deaths()<br>    check_invariants(g)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\\binom{60}{2} = 1770 \u4e2a\u6d4b\u8bd5\uff0c\u8dd1\u4e00\u904d\u4e0d\u5230\u4e00\u5206\u949f\u3002\u5b83\u4e0d\u68c0\u67e5&#8221;\u7ed3\u679c\u5bf9\u4e0d\u5bf9&#8221;\uff0c\u53ea\u68c0\u67e5&#8221;\u4e0d\u5d29\u6e83\u4e14\u4e0d\u53d8\u5f0f\u6210\u7acb&#8221;\u2014\u2014\u4f46\u8fd9\u5df2\u7ecf\u80fd\u6293\u4f4f\u7edd\u5927\u591a\u6570\u96c6\u6210 bug\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u5c42\u4e09\uff1a\u9ec4\u91d1\u5bf9\u5c40\uff08Golden Replay\uff09<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u5b58\u4e00\u6279\u771f\u5b9e\u5bf9\u5c40\u7684 RNG \u65e5\u5fd7 + \u52a8\u4f5c\u5e8f\u5217 + \u671f\u671b\u7684\u6700\u7ec8\u72b6\u6001\u54c8\u5e0c\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">GOLDEN_GAMES = load_golden_replays(\"tests\/golden\/*.json\")<br><br>@pytest.mark.parametrize(\"replay\", GOLDEN_GAMES, ids=lambda r: r.name)<br>def test_golden_replay(replay):<br>    g = Game(rng=ReplayRNG(replay.rng_log))<br>    g.load_decks(replay.decks)<br>    g.start()<br>    for action in replay.actions:<br>        g.apply(action)<br>    assert g.state_hash() == replay.expected_hash, \\<br>        f\"State diverged. Diff:\\n{diff_states(g, replay.expected_state)}\"<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8fd9\u662f\u6700\u6709\u4ef7\u503c\u7684\u56de\u5f52\u6d4b\u8bd5<\/strong>\uff1a\u4efb\u4f55\u884c\u4e3a\u53d8\u66f4\u90fd\u4f1a\u88ab\u7acb\u523b\u53d1\u73b0\u3002\u6bcf\u6b21\u4fee bug \u65f6\uff0c\u628a\u90a3\u5c40\u5bf9\u5c40\u5b58\u6210 golden replay\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u5c42\u56db\uff1a\u5b9a\u5411\u5355\u5143\u6d4b\u8bd5<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u9488\u5bf9\u6bcf\u4e2a\u5df2\u77e5\u7684\u88c1\u5b9a\u5199\u4e00\u4e2a\u6d4b\u8bd5\u3002\u8fd9\u662f\u6700\u8d35\u7684\uff0c\u4f46\u5bf9\u6838\u5fc3\u673a\u5236\u5fc5\u987b\u6709\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">def test_two_ghouls_dying_together():<br>    \"\"\"\u6848\u4f8b 1\uff1a\u4e24\u4e2a\u4e0d\u7a33\u5b9a\u7684\u98df\u5c38\u9b3c\u540c\u65f6\u6b7b\u4ea1\"\"\"<br>    g = make_test_game()<br>    a = g.summon_test(\"FP1_024\", player=0)   # \u4e0d\u7a33\u5b9a\u7684\u98df\u5c38\u9b3c<br>    b = g.summon_test(\"FP1_024\", player=0)<br>    c = g.summon_test(\"TEST_3_3\", player=1)<br>    g.set_damage(a, 2); g.set_damage(b, 2)   # \u90fd\u5269 1 \u8840<br><br>    g.damage_all_minions(2)<br>    g.process_deaths()<br><br>    assert a[Tag.ZONE] == Zone.GRAVEYARD<br>    assert b[Tag.ZONE] == Zone.GRAVEYARD<br>    # \u4e24\u4e2a\u4ea1\u8bed\u90fd\u89e6\u53d1\u4e86\uff1ac \u53d7\u5230 2 \u70b9\u989d\u5916\u4f24\u5bb3\uff083\/3 \u2192 \u53d7 2+1+1 = 4 \u2192 \u6b7b\uff09<br>    assert c[Tag.ZONE] == Zone.GRAVEYARD<br><br><br>def test_silence_windfury_warrior_kills_damaged_minion():<br>    \"\"\"\u6848\u4f8b 2\uff1a\u6c89\u9ed8\u5149\u73af\u6e90\u5bfc\u81f4\u968f\u4ece\u6b7b\u4ea1\"\"\"<br>    g = make_test_game()<br>    warrior = g.summon_test(\"EX1_055\", player=0)   # \u66b4\u98ce\u57ce\u52c7\u58eb<br>    priest  = g.summon_test(\"EX1_019\", player=0)   # \u7834\u788e\u6b8b\u9633\u796d\u53f8 3\/2<br>    assert priest[Tag.HEALTH] == 3                 # 2 + 1(\u5149\u73af)<br>    g.set_damage(priest, 2)<br>    assert priest.current_health == 1<br><br>    g.silence(warrior)<br>    g.process_deaths()<br>    assert priest[Tag.ZONE] == Zone.GRAVEYARD      # 3-1=2 \u4e0a\u9650\uff0c\u53d7 2 \u4f24 \u2192 \u6b7b<br><br><br>def test_divine_shield_blocks_poison():<br>    \"\"\"\u6848\u4f8b\uff1a\u5723\u76fe\u6321\u4f4f\u5267\u6bd2\"\"\"<br>    g = make_test_game()<br>    poison = g.summon_test(\"TEST_POISON_1_1\", player=0)<br>    shield = g.summon_test(\"TEST_DS_5_5\", player=1)<br>    g.attack(poison, shield)<br>    g.process_deaths()<br>    assert shield[Tag.ZONE] == Zone.PLAY           # \u6d3b\u7740<br>    assert not shield.has(Tag.DIVINE_SHIELD)       # \u4f46\u5723\u76fe\u7834\u4e86<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">32.2 \u6211\u63a8\u8350\u7684\u6295\u5165\u6bd4\u4f8b<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u5c42<\/th><th>\u6d4b\u8bd5\u6570<\/th><th>\u5f00\u53d1\u6295\u5165<\/th><th>Bug \u6355\u83b7\u7387<\/th><\/tr><\/thead><tbody><tr><td>\u4e0d\u53d8\u5f0f<\/td><td>~15 \u6761<\/td><td>1 \u5929<\/td><td><strong>40%<\/strong><\/td><\/tr><tr><td>\u4ea4\u4e92\u77e9\u9635<\/td><td>~2000 \u81ea\u52a8\u751f\u6210<\/td><td>0.5 \u5929<\/td><td>25%<\/td><\/tr><tr><td>\u9ec4\u91d1\u5bf9\u5c40<\/td><td>~100 \u5c40<\/td><td>\u6301\u7eed\u79ef\u7d2f<\/td><td>25%<\/td><\/tr><tr><td>\u5b9a\u5411\u5355\u6d4b<\/td><td>~500<\/td><td>\u6301\u7eed<\/td><td>10%<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4e0d\u53d8\u5f0f\u7684\u6295\u5165\u4ea7\u51fa\u6bd4\u662f\u6700\u9ad8\u7684<\/strong>\u3002\u5982\u679c\u4f60\u53ea\u6709\u4e00\u5929\u65f6\u95f4\u505a\u6d4b\u8bd5\uff0c\u5168\u90e8\u6295\u5728\u4e0d\u53d8\u5f0f\u4e0a\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">\u7b2c\u4e03\u90e8\u5206 \u53ef\u501f\u9274\u6e05\u5355\u4e0e\u5b8c\u6574\u6e90\u7801<\/h1>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 33 \u7ae0 \u4e8c\u5341\u6761\u53ef\u76f4\u63a5\u5e26\u8d70\u7684\u8bbe\u8ba1\u7ecf\u9a8c<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">\u6309\u91cd\u8981\u6027\u6392\u5e8f\u3002\u524d\u4e94\u6761\u662f\u6211\u8ba4\u4e3a<strong>\u6700\u6709\u4ef7\u503c<\/strong>\u7684\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u2605\u2605\u2605 1. \u4e00\u5207\u7686\u5b9e\u4f53\uff0c\u7c7b\u578b\u662f\u6807\u7b7e\u800c\u975e\u7c7b<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\"># \u274c<br>class Minion(Card): ...<br>class Spell(Card): ...<br><br># \u2713<br>e[Tag.CARDTYPE] == CardType.MINION<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6536\u76ca<\/strong>\uff1a\u53d8\u5f62\u3001\u82f1\u96c4\u724c\u3001\u5730\u6807\uff08\u65b0\u5361\u724c\u7c7b\u578b\uff09\u3001&#8221;\u968f\u4ece\u53d8\u6210\u6b66\u5668&#8221;\u8fd9\u7c7b\u9700\u6c42\u90fd\u4e0d\u9700\u8981\u6539\u67b6\u6784\u3002\u7089\u77f3\u5728\u7b2c 8 \u5e74\u52a0\u4e86\u4e00\u4e2a\u5168\u65b0\u5361\u724c\u7c7b\u578b\uff08\u5730\u6807\uff09\u800c\u6ca1\u6709\u91cd\u5199\u5f15\u64ce\uff0c\u5c31\u662f\u9760\u8fd9\u4e2a\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4ee3\u4ef7<\/strong>\uff1a\u7c7b\u578b\u68c0\u67e5\u4ece\u7f16\u8bd1\u671f\u79fb\u5230\u8fd0\u884c\u65f6\uff0cIDE \u8865\u5168\u53d8\u5dee\u3002\u7528 <code>TypedDict<\/code> \u6216\u8fd0\u884c\u65f6\u65ad\u8a00\u5f25\u8865\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u2605\u2605\u2605 2. \u5b58&#8221;\u6700\u5927\u751f\u547d\u503c + \u5df2\u53d7\u4f24\u5bb3&#8221;\uff0c\u4e0d\u5b58&#8221;\u5f53\u524d\u751f\u547d\u503c&#8221;<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">current_health = HEALTH - DAMAGE<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6536\u76ca<\/strong>\uff1abuff\/\u6c89\u9ed8\/\u5149\u73af\u7684\u52a0\u51cf\u6cd5\u5e42\u7b49\uff0c\u4e0d\u9700\u8981&#8221;\u6263\u56de\u591a\u5c11&#8221;\u7684\u8bb0\u8d26\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8fd9\u4e00\u6761\u51e0\u4e4e\u662f\u6240\u6709\u6210\u719f\u5361\u724c\u5f15\u64ce\u7684\u5171\u8bc6\u3002<\/strong> \u5982\u679c\u4f60\u73b0\u5728\u7684\u5f15\u64ce\u5b58\u7684\u662f\u5f53\u524d\u751f\u547d\u503c\uff0c\u8d81\u65e9\u6539\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u2605\u2605\u2605 3. \u6240\u6709\u72b6\u6001\u53d8\u5316\u8d70\u552f\u4e00\u5165\u53e3<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u56db\u4e2a\u5165\u53e3\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">entity.__setitem__   # tag \u53d8\u5316<br>zones.move           # \u533a\u57df\u53d8\u5316<br>game.damage          # \u4f24\u5bb3<br>game.rng             # \u968f\u673a<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6536\u76ca<\/strong>\uff1a\u7f51\u7edc\u540c\u6b65\u3001\u65e5\u5fd7\u3001\u56de\u653e\u3001\u4e0d\u53d8\u5f0f\u68c0\u67e5\u3001undo log \u5168\u90e8\u53ea\u9700\u8981\u5728\u56db\u4e2a\u5730\u65b9\u6302\u94a9\u5b50\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u9a8c\u8bc1\u65b9\u6cd5<\/strong>\uff1a\u5168\u5c40\u641c\u7d22 <code>.tags[<\/code> \u7684\u76f4\u63a5\u8d4b\u503c\uff0c\u6bcf\u4e00\u5904\u90fd\u5e94\u8be5\u662f\u6709\u610f\u7684\u6027\u80fd\u4f18\u5316\uff08\u4e14\u6709\u6ce8\u91ca\u8bf4\u660e\u4e3a\u4ec0\u4e48\u7ed5\u8fc7\uff09\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u2605\u2605\u2605 4. \u5ef6\u8fdf\u6267\u884c\u7684\u6548\u679c\u5fc5\u987b\u5feb\u7167\u5b83\u7684\u4e0a\u4e0b\u6587<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u6b7b\u4ea1\u5feb\u7167\u3001\u653b\u51fb\u529b\u5feb\u7167\u3001\u76ee\u6807\u5217\u8868\u5feb\u7167\u3001\u7ec8\u66f2\u5feb\u7167\u2026\u2026<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"># \u274c \u6267\u884c\u65f6\u8bfb\u53d6<br>def deathrattle(game, entity):<br>    pos = entity[Tag.ZONE_POSITION]   # \u5df2\u7ecf\u662f 0 \u4e86\uff01<br><br># \u2713 \u6ce8\u518c\u65f6\u5feb\u7167<br>snap = DeathSnapshot(position=e[Tag.ZONE_POSITION], ...)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8fd9\u6761\u89c4\u5f8b\u9002\u7528\u4e8e\u4efb\u4f55\u4e8b\u4ef6\u9a71\u52a8\u7cfb\u7edf<\/strong>\uff0c\u4e0d\u53ea\u662f\u5361\u724c\u6e38\u620f\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u2605\u2605\u2605 5. \u7528\u961f\u5217\u800c\u4e0d\u662f\u6808\uff0c\u628a\u6240\u6709\u51b3\u7b56\u524d\u7f6e<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6536\u76ca<\/strong>\uff1a\u7ed3\u7b97\u662f\u539f\u5b50\u7684\u3001\u53ef\u540c\u6b65\u8dd1\u5b8c\u7684\u7eaf\u51fd\u6570\u3002\u8fd9\u8ba9 AI \u80fd\u4ee5\u6bcf\u79d2\u6570\u4e07\u6b21\u7684\u901f\u5ea6\u8c03\u7528\u5f15\u64ce\uff0c\u8ba9\u670d\u52a1\u5668\u4e0d\u9700\u8981\u7ef4\u62a4&#8221;\u7b49\u5f85\u73a9\u5bb6\u54cd\u5e94&#8221;\u7684\u6302\u8d77\u72b6\u6001\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4ee3\u4ef7<\/strong>\uff1a\u5931\u53bb\u4e86 MTG \u5f0f\u7684\u5b9e\u65f6\u4e92\u52a8\u6df1\u5ea6\u3002\u7528\u5965\u79d8\u3001\u4ea1\u8bed\u7b49&#8221;\u9884\u5148\u627f\u8bfa&#8221;\u673a\u5236\u8865\u507f\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5982\u679c\u4f60\u5728\u8bbe\u8ba1\u4e00\u4e2a\u65b0\u6e38\u620f\uff0c\u8ba4\u771f\u8003\u8651\u8fd9\u4e2a\u6743\u8861\u3002<\/strong> \u5927\u591a\u6570\u60c5\u51b5\u4e0b\u961f\u5217\u662f\u5bf9\u7684\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">\u2605\u2605 6. \u6b7b\u4ea1\u662f\u4e24\u9636\u6bb5\u7684\uff1a\u6807\u8bb0 + \u6279\u91cf\u7ed3\u7b97<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">damage()  \u2192  \u53ea\u6807\u8bb0 TO_BE_DESTROYED<br>process_deaths()  \u2192  \u6279\u91cf\u6536\u96c6\u3001\u6279\u91cf\u5165\u5893\u3001\u6309\u5e8f\u89e6\u53d1\u4ea1\u8bed<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u8c03\u7528\u70b9\uff1a\u6bcf\u4e2a\u73a9\u5bb6\u52a8\u4f5c\u4e4b\u540e\u3001\u6bcf\u6b21\u89e6\u53d1\u961f\u5217\u6e05\u7a7a\u4e4b\u540e\u3001\u6bcf\u6b21\u5149\u73af\u91cd\u7b97\u4e4b\u540e\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u2605\u2605 7. \u5149\u73af\u5168\u91cf\u91cd\u7b97\uff0c\u4e0d\u505a\u589e\u91cf<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u800c\u4e14\uff1a<strong>\u5149\u73af\u5bfc\u81f4\u7684 tag \u53d8\u5316\u4e0d\u53d1\u51fa\u6e38\u620f\u4e8b\u4ef6<\/strong>\uff0c\u53ea\u505a\u5ba2\u6237\u7aef\u540c\u6b65\u3002\u5426\u5219\u5fc5\u7136\u5faa\u73af\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u2605\u2605 8. \u5408\u5e76\/\u9644\u52a0\u6548\u679c\u5fc5\u987b\u901a\u8fc7\u9644\u9b54\u5b9e\u4f53\u5b9e\u73b0<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u78c1\u529b\u3001\u661f\u8230\u3001\u51c6\u5907\u6298\u6263\u3001\u4efb\u4f55&#8221;\u7ed9\u968f\u4ece\u52a0\u4e1c\u897f&#8221;\u7684\u6548\u679c\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6536\u76ca<\/strong>\uff1a\u6c89\u9ed8\/\u79fb\u9664\u65f6\u4e00\u6b21\u6027\u56de\u6eda\uff0c\u4e0d\u9700\u8981\u6bcf\u4e2a\u6548\u679c\u8bb0\u4f4f\u81ea\u5df1\u6539\u4e86\u4ec0\u4e48\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u2605\u2605 9. \u4e8b\u4ef6\u4e0a\u4e0b\u6587\u5b81\u53ef\u591a\u5e26\u4fe1\u606f<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>amount<\/code>\u3001<code>target_health_before<\/code>\u3001<code>fatal<\/code>\u3001<code>paid_cost<\/code>\u3001<code>snapshot<\/code>\u2026\u2026<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4e00\u6b21\u591a\u5b58\u4e00\u4e2a\u5b57\u6bb5\uff0c\u7701\u4e86\u540e\u9762\u4e09\u4e2a\u5173\u952e\u8bcd\u7684\u6539\u52a8<\/strong>\uff08\u8d85\u6740\/\u8363\u8a89\u51fb\u6740\/\u72c2\u4e71\u5171\u7528 <code>target_health_before<\/code>\uff09\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u2605\u2605 10. \u7528\u7edf\u4e00\u7684 <code>PlayerStats<\/code> \u627f\u8f7d\u6240\u6709&#8221;\u5386\u53f2\u6761\u4ef6&#8221;<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">stats.total \/ stats.turn \/ stats.last<br>Cond.stat_at_least(\"spells_cast\", 6)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">2017 \u5e74\u505a\u8fd9\u4e2a\u62bd\u8c61\uff0c2025 \u5e74\u7684\u300a\u540c\u6e90\u300b\u5c31\u662f\u96f6\u5f15\u64ce\u6539\u52a8\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u2605\u2605 11. DSL \u8986\u76d6 95%\uff0c\u5269\u4e0b 5% \u7559\u9003\u751f\u8231<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">battlecry=SpecialEffect(\"YOGG_SARON\")<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u8ffd\u6c42 100% DSL \u8986\u76d6\u662f\u8fc7\u5ea6\u8bbe\u8ba1\u3002<\/strong> \u627f\u8ba4\u6709\u4e9b\u5361\u5c31\u662f\u7279\u6b8a\u7684\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u2605\u2605 12. Selector \/ Condition \/ Action \/ Amount \u56db\u5c42\u5206\u79bb<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u5b83\u4eec\u5404\u81ea\u53ef\u7ec4\u5408\uff0c\u4ea4\u53c9\u7ec4\u5408\u51fa\u6240\u6709\u5361\u724c\u3002\u4efb\u4f55\u4e00\u5c42\u6df7\u8fdb\u53e6\u4e00\u5c42\u7684\u804c\u8d23\uff0c\u8868\u8fbe\u529b\u90fd\u4f1a\u6253\u6298\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">\u2605 13. RNG \u8bb0\u8c03\u7528\u65e5\u5fd7\uff0c\u4e0d\u53ea\u8bb0 seed<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>assert call.domain == len(seq)<\/code> \u8fd9\u4e00\u884c\u662f\u884c\u4e3a\u53d8\u66f4\u7684\u6700\u4f73\u63a2\u6d4b\u5668\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u2605 14. \u60f0\u6027\u6d17\u724c<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u53ea\u5728\u62bd\u724c\u65f6\u51b3\u5b9a\u62bd\u5230\u54ea\u5f20\uff0c\u51cf\u5c11\u4fe1\u606f\u6cc4\u9732\u9762\u3002\u6570\u5b66\u4e0a\u7b49\u4ef7\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u2605 15. \u5faa\u73af\u4fdd\u62a4\u662f\u5fc5\u987b\u7684\uff0c\u4e0d\u662f\u53ef\u9009\u7684<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>MAX_TRIGGER_DEPTH<\/code>\u3001<code>MAX_DEATH_ROUNDS<\/code>\u3001\u56de\u5408\u65f6\u957f\u4e0a\u9650\u3002<strong>\u4f60\u4e0d\u53ef\u80fd\u8bc1\u660e\u65b0\u5361\u4e0d\u4f1a\u548c\u8001\u5361\u5f62\u6210\u73af\u3002<\/strong><\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u2605 16. \u4e0d\u53d8\u5f0f\u6d4b\u8bd5\u7684\u6295\u5165\u4ea7\u51fa\u6bd4\u6700\u9ad8<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">15 \u6761\u4e0d\u53d8\u5f0f + \u968f\u673a\u5bf9\u5c40 fuzzing\uff0c\u80fd\u6293\u4f4f 40% \u7684 bug\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u2605 17. \u670d\u52a1\u5668\u6743\u5a01 + \u96f6\u5ba2\u6237\u7aef\u903b\u8f91<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u7528\u52a8\u753b\u65f6\u957f\u63a9\u76d6 RTT\uff0c\u6362\u53d6\u96f6\u56de\u6eda\u4ee3\u7801\u548c\u96f6\u4f5c\u5f0a\u9762\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u2605 18. \u628a&#8221;\u5f85\u51b3\u7b56&#8221;\u8868\u8fbe\u6210\u7eaf\u6570\u636e\uff0c\u4e0d\u662f\u95ed\u5305<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">PendingChoice(kind=\"DISCOVER\", options=[...], continuation_id=\"ADD_TO_HAND\")<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u8fd9\u6837\u65ad\u7ebf\u91cd\u8fde\u3001\u5b58\u6863\u3001\u670d\u52a1\u5668\u91cd\u542f\u90fd\u80fd\u6062\u590d\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u2605 19. \u590d\u5236\u8981\u533a\u5206 BASE \/ FULL \/ SNAPSHOT \u4e09\u79cd\u6a21\u5f0f<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u5426\u5219 3 \u8d39\u7684\u300a\u514b\u9686\u300b\u80fd\u590d\u5236 20\/20 \u7684\u968f\u4ece\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u2605 20. \u6784\u7b51\u671f\u7684\u590d\u6742\u5ea6\u4e0d\u8981\u6e17\u900f\u8fdb\u5bf9\u5c40\u5f15\u64ce<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u7b26\u6587\u3001\u65c5\u5ba2\u3001\u4f20\u5947\u3001\u53cc\u804c\u4e1a\u2014\u2014\u5168\u90e8\u5728\u6784\u7b51\u6821\u9a8c\u91cc\u89e3\u51b3\uff0c\u5bf9\u5c40\u5f15\u64ce\u65e0\u611f\u77e5\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 34 \u7ae0 \u5982\u679c\u6211\u4eca\u5929\u4ece\u96f6\u5f00\u59cb\u505a\u4e00\u4e2a\u5361\u724c\u5f15\u64ce<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">\u6309\u987a\u5e8f\uff0c\u8fd9\u662f\u6211\u4f1a\u505a\u7684\u4e8b\uff1a<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u7b2c 1 \u5468\uff1a\u6838\u5fc3\u6570\u636e\u6a21\u578b<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>Entity<\/code>\uff08id + tag dict\uff09<\/li>\n\n\n\n<li><code>Tag<\/code> \u679a\u4e3e\uff08\u9884\u7559\u5927\u91cf\u7a7a\u4f4d\uff09<\/li>\n\n\n\n<li><code>Zone<\/code> + <code>ZoneManager<\/code>\uff08\u542b\u5bb9\u91cf\u3001\u4f4d\u7f6e\u7ef4\u62a4\uff09<\/li>\n\n\n\n<li><code>CardDef<\/code>\uff08frozen dataclass\uff09<\/li>\n\n\n\n<li>\u5355\u5143\u6d4b\u8bd5\uff1a\u533a\u57df\u79fb\u52a8\u7684\u4e0d\u53d8\u5f0f<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5173\u952e\u51b3\u7b56\u70b9<\/strong>\uff1a\u628a <code>CONTROLLER<\/code> \u548c <code>OWNER<\/code> \u5206\u5f00\uff08\u54ea\u6015\u4f60\u73b0\u5728\u7528\u4e0d\u5230\uff09\uff0c\u628a <code>HEALTH<\/code>\/<code>DAMAGE<\/code> \u5206\u5f00\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u7b2c 2 \u5468\uff1a\u4e8b\u4ef6\u4e0e\u89e6\u53d1<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>EventBus<\/code>\uff08\u5206\u6876\u8ba2\u9605 + \u961f\u5217 + \u6df1\u5ea6\u4fdd\u62a4\uff09<\/li>\n\n\n\n<li><code>EventCtx<\/code>\uff08\u5b57\u6bb5\u5b81\u591a\u52ff\u5c11\uff09<\/li>\n\n\n\n<li><code>TriggerDef<\/code>\uff08\u542b <code>zones<\/code> \u5b57\u6bb5\uff01\uff09<\/li>\n\n\n\n<li><code>process_deaths<\/code><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5173\u952e\u51b3\u7b56\u70b9<\/strong>\uff1a<code>TriggerDef.zones<\/code> \u8fd9\u4e2a\u5b57\u6bb5\u4f1a\u5728\u7b2c 3 \u5e74\u5e2e\u4f60\u5b9e\u73b0\u624b\u724c\/\u724c\u5e93\u91cc\u751f\u6548\u7684\u5361\u3002\u73b0\u5728\u52a0\u5b83\u662f\u514d\u8d39\u7684\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u7b2c 3 \u5468\uff1aDSL<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>Selector<\/code> + \u7ec4\u5408\u5b50<\/li>\n\n\n\n<li><code>Condition<\/code> + \u5e03\u5c14\u4ee3\u6570<\/li>\n\n\n\n<li><code>Action<\/code> + \u63a7\u5236\u6d41\uff08Seq\/If\/Repeat\/ForEach\uff09<\/li>\n\n\n\n<li>20 \u4e2a\u539f\u8bed Action<\/li>\n\n\n\n<li><code>Amount<\/code> \u8868\u8fbe\u5f0f<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5173\u952e\u51b3\u7b56\u70b9<\/strong>\uff1a\u4ece\u7b2c\u4e00\u5929\u5c31\u7528\u6570\u636e\u7ed3\u6784\u8868\u8fbe\u6548\u679c\uff0c\u4e0d\u8981\u7528 lambda\u3002\u54ea\u6015\u73b0\u5728\u9ebb\u70e6\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u7b2c 4 \u5468\uff1a\u5149\u73af\u4e0e\u9644\u9b54<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>AuraBus<\/code>\uff08\u810f\u6807\u8bb0 + \u5168\u91cf\u91cd\u7b97\uff09<\/li>\n\n\n\n<li>\u9644\u9b54\u5b9e\u4f53 + apply\/remove<\/li>\n\n\n\n<li>\u6c89\u9ed8<\/li>\n\n\n\n<li>\u53d8\u5f62<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">\u7b2c 5 \u5468\uff1a\u51fa\u724c\u6d41\u7a0b\u4e0e\u5173\u952e\u8bcd<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>play_card<\/code> \u7684\u5b8c\u6574\u5206\u9636\u6bb5\u6d41\u7a0b<\/li>\n\n\n\n<li><code>attack<\/code> \u7684 11 \u6b65<\/li>\n\n\n\n<li>15 \u4e2a\u57fa\u7840\u5173\u952e\u8bcd<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">\u7b2c 6 \u5468\uff1a\u968f\u673a\u4e0e\u56de\u653e<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>GameRNG<\/code> + \u65e5\u5fd7<\/li>\n\n\n\n<li><code>ReplayRNG<\/code> + desync \u68c0\u6d4b<\/li>\n\n\n\n<li>\u72b6\u6001\u54c8\u5e0c<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">\u7b2c 7 \u5468\uff1a\u6d4b\u8bd5\u57fa\u7840\u8bbe\u65bd<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u4e0d\u53d8\u5f0f\u68c0\u67e5\u5668<\/li>\n\n\n\n<li>\u968f\u673a\u5bf9\u5c40 fuzzer<\/li>\n\n\n\n<li>\u9ec4\u91d1\u56de\u653e<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">\u7b2c 8 \u5468\u53ca\u4ee5\u540e\uff1a\u5361\u724c<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u4ece\u8fd9\u91cc\u5f00\u59cb\uff0c<strong>\u52a0\u65b0\u5361\u5e94\u8be5\u4e0d\u9700\u8981\u78b0\u5f15\u64ce<\/strong>\u3002\u5982\u679c\u78b0\u4e86\uff0c\u8bf4\u660e\u524d\u4e03\u5468\u6709\u4e1c\u897f\u6ca1\u505a\u5bf9\uff0c\u56de\u53bb\u8865\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 35 \u7ae0 \u8ff7\u4f60\u5f15\u64ce\u6e90\u7801<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">\u4ee5\u4e0b\u662f <code>stoneheart.py<\/code> \u7684\u5b8c\u6574\u6e90\u7801\uff0c\u7ea6 2100 \u884c\uff0c\u96f6\u4f9d\u8d56\uff0c<code>python3 stoneheart.py<\/code> \u76f4\u63a5\u8fd0\u884c\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u81ea\u68c0\u8f93\u51fa<\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">==========================================================================<br>stoneheart \u2014\u2014 \u8ff7\u4f60\u7089\u77f3\u5f15\u64ce\u81ea\u68c0<br>==========================================================================<br>  PASS  \u4e24\u4e2a\u98df\u5c38\u9b3c\u540c\u65f6\u6b7b\u4ea1    \u4e24\u4e2a\u4ea1\u8bed\u90fd\u89e6\u53d1\uff0c\u8718\u86db\u5766\u514b\u53d7 2 \u70b9\u4f24\u5bb3<br>  PASS  \u5149\u73af\u6c89\u9ed8\u81f4\u6b7b         \u6c89\u9ed8\u5149\u73af\u6e90\u540e\uff0c\u53d7\u4f24\u7684\u968f\u4ece\u56e0\u4e0a\u9650\u4e0b\u964d\u800c\u6b7b\u4ea1<br>  PASS  \u5723\u76fe\u6321\u5267\u6bd2           \u5723\u76fe\u5438\u6536\u4f24\u5bb3 \u2192 \u5267\u6bd2\u4e0d\u751f\u6548\uff0c\u968f\u4ece\u5b58\u6d3b<br>  PASS  \u4ea1\u8bed\u53ec\u5524\u4f4d\u7f6e          \u4ea1\u8bed\u884d\u751f\u7269\u5728\u539f\u968f\u4ece\u4f4d\u7f6e\u53ec\u5524\uff08\u5feb\u7167\u751f\u6548\uff09<br>  PASS  \u89e6\u53d1\u987a\u5e8f\u6309\u4e0a\u573a\u65f6\u95f4     \u89e6\u53d1\u987a\u5e8f\u6309\u4e0a\u573a\u65f6\u95f4(ENTITY_ID)\uff0c\u4e0d\u662f\u6218\u573a\u4f4d\u7f6e<br>  PASS  \u590d\u751f\u5728\u4ea1\u8bed\u4e4b\u540e        \u590d\u751f\u4f53\u4ee5 1 \u8840\u56de\u5f52\u4e14\u5931\u53bb\u590d\u751f\u6807\u7b7e<br>  PASS  \u5de8\u578b\u53ec\u5524\u90e8\u4ef6          \u5de8\u578b\u4e3b\u4f53\u5e26\u51fa 2 \u4e2a\u89e6\u624b\u90e8\u4ef6<br>  PASS  \u6218\u573a\u5df2\u6ee1\u53ec\u5524\u5931\u8d25       \u6218\u573a\u6ee1\u65f6\u53ec\u5524\u5931\u8d25\uff1a\u5b9e\u4f53\u6d88\u5931\uff0c\u4e0d\u5165\u5893\u5730\uff0c\u4e0d\u89e6\u53d1\u4ea1\u8bed<br>  PASS  \u7a81\u88ad\u4e0d\u80fd\u6253\u8138          \u7a81\u88ad\u968f\u4ece\u9996\u56de\u5408\u53ea\u80fd\u653b\u51fb\u968f\u4ece<br>  PASS  \u5632\u8bbd\u9650\u5236\u76ee\u6807          \u6709\u5632\u8bbd\u65f6\u5408\u6cd5\u76ee\u6807\u53ea\u5269\u5632\u8bbd\u968f\u4ece<br>  PASS  \u6cd5\u672f\u4f24\u5bb3\u52a0\u6210          \u6cd5\u672f\u4f24\u5bb3 +1 \u8ba9\u706b\u7403\u672f\u9020\u6210 7 \u70b9\u4f24\u5bb3<br>  PASS  \u72c2\u4e71\u4e00\u6b21\u6027           \u72c2\u4e71\u53ea\u89e6\u53d1\u4e00\u6b21\uff0c\u4e14\u81f4\u6b7b\u4f24\u5bb3\u4e0d\u89e6\u53d1<br>  PASS  \u8d85\u6740                4 \u653b\u6253 1 \u8840\u968f\u4ece\u89e6\u53d1\u8d85\u6740<br>  PASS  \u6cd5\u672f\u8ff8\u53d1             \u6cd5\u672f\u8ff8\u53d1\u89e6\u53d1\u4e00\u6b21\u540e\u6d88\u8017<br>  PASS  \u4efb\u52a1\u5b8c\u6210             \u4efb\u52a1\uff1a\u4f7f\u7528 3 \u4e2a\u6cd5\u672f\u540e\u83b7\u5f97\u5956\u52b1<br>  PASS  \u5965\u79d8\u955c\u50cf\u5b9e\u4f53          \u955c\u50cf\u5b9e\u4f53\u590d\u5236\u5bf9\u624b\u6253\u51fa\u7684\u968f\u4ece<br>  PASS  \u8fc7\u8f7d                \u8fc7\u8f7d\u5728\u4e0b\u56de\u5408\u9501\u5b9a\u6c34\u6676<br>  PASS  \u53ef\u4ea4\u6613               \u53ef\u4ea4\u6613\uff1a\u82b1 1 \u8d39\u6d17\u56de\u724c\u5e93\u5e76\u62bd\u4e00\u5f20<br>  PASS  \u51c6\u5907                \u51c6\u5907\uff1a\u82b1 4 \u8d39\u6362 5 \u70b9\u6298\u6263\uff0c\u672c\u56de\u5408\u9501\u5b9a\uff0c\u4e0b\u56de\u5408\u53ef\u51fa<br>  PASS  \u53d1\u73b0                \u53d1\u73b0\u4e09\u9009\u4e00\uff1a['\u706b\u7403\u672f', '\u95ea\u7535\u7bad', '\u5965\u672f\u667a\u6167']<br>  PASS  \u75b2\u52b3                \u75b2\u52b3\u4f24\u5bb3\u9012\u589e 1,2,3<br>  PASS  \u53d8\u5f62\u4fdd\u7559\u4f4d\u7f6e          \u53d8\u5f62\u4fdd\u7559 ENTITY_ID \u4e0e\u4f4d\u7f6e\uff0c\u91cd\u7f6e\u5c5e\u6027<br>  PASS  \u786e\u5b9a\u6027\u56de\u653e            \u56de\u653e\u4e00\u81f4\uff1aRNG \u8c03\u7528\u5b8c\u5168\u590d\u73b0<br>  PASS  \u968f\u673a\u5bf9\u5c40\u4e0d\u53d8\u5f0f         40 \u5c40\u968f\u673a\u5bf9\u5c40 \/ 900+ \u4e2a\u56de\u5408\uff0c\u5168\u90e8\u4e0d\u53d8\u5f0f\u901a\u8fc7<br>--------------------------------------------------------------------------<br>  24 passed, 0 failed<br>==========================================================================<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">40 \u5c40\u968f\u673a\u5bf9\u5c40\u5728\u5355\u6838 Python \u4e0a\u8dd1\u5b8c\u53ea\u9700 0.47 \u79d2\uff0c\u8bf4\u660e\u8fd9\u4e2a\u67b6\u6784\u5728\u6ca1\u6709\u4efb\u4f55\u4f18\u5316\u7684\u60c5\u51b5\u4e0b\u6027\u80fd\u5c31\u8db3\u591f\u505a\u5f00\u53d1\u548c\u6d4b\u8bd5\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u8986\u76d6\u7684\u673a\u5236\u6e05\u5355<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u8303\u5f0f<\/th><th>\u5df2\u5b9e\u73b0<\/th><\/tr><\/thead><tbody><tr><td>\u9759\u6001\u6807\u7b7e<\/td><td>\u5632\u8bbd\u3001\u51b2\u950b\u3001\u7a81\u88ad\u3001\u5723\u76fe\u3001\u6f5c\u884c\u3001\u5267\u6bd2\u3001\u5438\u8840\u3001\u98ce\u6012\u3001\u51bb\u7ed3\u3001\u514d\u75ab\u3001\u4e0d\u53ef\u9009\u4e2d\u3001\u590d\u751f\u3001\u6cd5\u672f\u4f24\u5bb3<\/td><\/tr><tr><td>\u89e6\u53d1\u5668<\/td><td>\u6218\u543c\u3001\u8fde\u51fb\u3001\u4ea1\u8bed\u3001\u72c2\u4e71\u3001\u8d85\u6740\u3001\u8363\u8a89\u51fb\u6740\u3001\u6cd5\u672f\u8ff8\u53d1\u3001\u56de\u5408\u5f00\u59cb\/\u7ed3\u675f\u3001\u53ec\u5524\u65f6\u3001\u5965\u79d8<\/td><\/tr><tr><td>\u51fa\u724c\u94a9\u5b50<\/td><td>\u8fc7\u8f7d\u3001\u8fde\u51fb\u5224\u5b9a\u3001\u7ec8\u66f2\u5feb\u7167\u3001\u53ef\u4ea4\u6613\u3001\u51c6\u5907<\/td><\/tr><tr><td>\u9009\u62e9\u578b<\/td><td>\u53d1\u73b0\uff08\u542b\u7b56\u7565\u6ce8\u5165\uff0cAI \u6a21\u5f0f\u4e0d\u6302\u8d77\uff09<\/td><\/tr><tr><td>\u72b6\u6001\u673a<\/td><td>\u4efb\u52a1\uff08\u901a\u7528 <code>ProgressionDef<\/code>\uff0c\u53ef\u8868\u8fbe\u4efb\u52a1\u94fe\/\u6cd5\u672f\u77f3\/\u8150\u8680\/\u6316\u6398\uff09<\/td><\/tr><tr><td>\u591a\u5b9e\u4f53<\/td><td>\u5de8\u578b\uff08<code>colossal_parts<\/code>\uff09<\/td><\/tr><tr><td>\u7cfb\u7edf<\/td><td>\u5149\u73af\u91cd\u7b97\u3001\u9644\u9b54\u3001\u6c89\u9ed8\u3001\u53d8\u5f62\u3001\u6b7b\u4ea1\u7ed3\u7b97\u3001\u75b2\u52b3\u3001\u70e7\u724c\u3001\u786e\u5b9a\u6027 RNG\u3001\u56de\u653e\u3001\u4e8b\u4ef6\u6d41\u8bb0\u5f55<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">\u6e90\u7801<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u89c1\u968f\u672c\u6587\u6863\u4e00\u540c\u4ea4\u4ed8\u7684 <code>stoneheart.py<\/code>\u3002\u4ee5\u4e0b\u662f\u5173\u952e\u7247\u6bb5\u7684\u5bfc\u8bfb\uff0c\u5b8c\u6574\u6587\u4ef6\u8bf7\u76f4\u63a5\u8fd0\u884c\u3002<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">\u9aa8\u67b6<\/h4>\n\n\n\n<pre class=\"wp-block-preformatted\">class Game:<br>    def __init__(self, seed=0, verbose=False, policy=None):<br>        self.rng = GameRNG(seed)              # \u552f\u4e00\u968f\u673a\u6e90<br>        self.entities = {}                    # id -&gt; Entity<br>        self.zones = ZoneManager(self)        # \u552f\u4e00\u533a\u57df\u53d8\u5316\u5165\u53e3<br>        self.events = EventBus(self)          # \u89e6\u53d1\u961f\u5217<br>        self.auras = AuraBus(self)            # \u5149\u73af\u91cd\u7b97<br>        self.players = [Player(self, 0), Player(self, 1)]<br>        self.history = []                     # PowerHistory \u96cf\u5f62<br>        self.policy = policy                  # \u51b3\u7b56\u6ce8\u5165\u70b9\uff08\u53d1\u73b0\/\u6289\u62e9\uff09<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\">\u89e6\u53d1\u987a\u5e8f\u7684\u5b9e\u73b0\uff08\u94c1\u5f8b\u4e00\uff09<\/h4>\n\n\n\n<pre class=\"wp-block-preformatted\">    def collect(self, ctx: EventCtx) -&gt; list:<br>        out = []<br>        for owner, td in self.subs.get(ctx.kind, ()):<br>            if ctx.kind != Event.DEATH and owner[Tag.ZONE] not in td.zones:<br>                continue<br>            if td.source_only and ctx.entity is not owner: continue<br>            if owner.has(Tag.SILENCED): continue<br>            if td.once and owner.has(Tag.TRIGGERED_ONCE): continue<br>            c = Ctx(source=owner, controller=owner[Tag.CONTROLLER], event=ctx)<br>            if td.condition and not td.condition.eval(self.game, owner, c):<br>                continue<br>            out.append((owner, td))<br>        out.sort(key=lambda x: x[0][Tag.ENTITY_ID])   # \u2605 \u4e0a\u573a\u987a\u5e8f<br>        return out<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\">\u6b7b\u4ea1\u7ed3\u7b97\u7684\u516d\u4e2a\u6b65\u9aa4<\/h4>\n\n\n\n<pre class=\"wp-block-preformatted\">    def process_deaths(self):<br>        while True:<br>            dying = [\u6536\u96c6 current_health &lt;= 0 \u6216 TO_BE_DESTROYED \u7684\u968f\u4ece]<br>            if not dying: return<br>            dying.sort(key=lambda e: e[Tag.ENTITY_ID])    # \u2460 \u6392\u5e8f<br>            for e in dying: e.death_snapshot = DeathSnapshot(...)  # \u2461 \u5feb\u7167<br>            for e in dying: self.emit_now(Event.PROPOSED_DEATH, ...)  # \u2462 \u514d\u6b7b<br>            for e in dying: self.zones.move(e, Zone.GRAVEYARD)    # \u2463 \u5168\u90e8\u5165\u5893<br>            self.auras.mark_dirty(); self.auras.recompute()<br>            for e in dying: self.emit(Event.DEATH, ...)           # \u2464 \u4ea1\u8bed<br>            self.events.resolve()<br>            for e in dying: \u590d\u751f(e)                                # \u2465 \u590d\u751f\u5728\u540e<br>            for e in dying: self.emit(Event.AFTER_DEATH, ...)<br>            self.events.resolve()<br>            # \u5faa\u73af\uff0c\u76f4\u5230\u6ca1\u6709\u65b0\u6b7b\u4ea1<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\">\u4f24\u5bb3\u7ba1\u7ebf\uff08\u5723\u76fe \/ \u5267\u6bd2 \/ \u5438\u8840\u7684\u6b63\u786e\u987a\u5e8f\uff09<\/h4>\n\n\n\n<pre class=\"wp-block-preformatted\">    def damage(self, target, amount, source=None) -&gt; int:<br>        if amount &lt;= 0 or not in_play(target): return 0<br>        if target.has(Tag.IMMUNE): return 0<br><br>        ctx = self.emit_now(Event.PRE_DAMAGE, ..., mutable_amount=amount)<br>        amount = max(0, ctx.mutable_amount)<br>        if amount == 0: return 0<br><br>        if target.has(Tag.DIVINE_SHIELD):          # \u2605 \u5723\u76fe\u5728\u8fd9\u91cc<br>            target[Tag.DIVINE_SHIELD] = 0<br>            return 0                               # \u2605 \u8fd4\u56de 0 \u2192 \u5267\u6bd2\/\u5438\u8840\u90fd\u4e0d\u751f\u6548<br><br>        before = target.current_health<br>        if target.is_hero and target[Tag.ARMOR] &gt; 0:<br>            absorbed = min(target[Tag.ARMOR], amount)<br>            target[Tag.ARMOR] -= absorbed; amount -= absorbed<br>            if amount == 0: return 0<br><br>        target[Tag.DAMAGE] += amount<br>        self.emit(Event.DAMAGE, ..., amount=amount,<br>                  fatal=target.current_health &lt;= 0,<br>                  target_health_before=before)      # \u2605 \u4f9b\u8d85\u6740\/\u8363\u8a89\u51fb\u6740\u4f7f\u7528<br><br>        if source is not None and source.has(Tag.LIFESTEAL):<br>            self.heal(self.player(source[Tag.CONTROLLER]).hero, amount)<br>        if source is not None and source.has(Tag.POISONOUS) and target.is_minion:<br>            target[Tag.TO_BE_DESTROYED] = 1<br>        return amount<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\">\u901a\u7528\u72b6\u6001\u673a\uff08\u4e00\u4e2a\u6570\u636e\u7ed3\u6784\u8868\u8fbe\u5341\u4e8c\u4e2a\u5173\u952e\u8bcd\uff09<\/h4>\n\n\n\n<pre class=\"wp-block-preformatted\">@dataclass(frozen=True)<br>class ProgressionDef:<br>    counter_event: Event<br>    thresholds: tuple             # \u5404\u9636\u6bb5\u9608\u503c<br>    condition: Condition | None<br>    zones: frozenset              # \u5728\u54ea\u4e2a\u533a\u57df\u8ba1\u6570\uff08\u624b\u724c\/\u5965\u79d8\u533a\/\u6218\u573a\uff09<br>    on_stage: tuple               # \u5404\u9636\u6bb5\u5956\u52b1<br>    upgrade_to: tuple             # \u5404\u9636\u6bb5\u53d8\u5f62\u76ee\u6807<br>    complete_action: Action | None<br><br><br>class Progress(Action):<br>    def run(self, g, c):<br>        e = c.source<br>        e[Tag.PROGRESS] += 1<br>        pd, stage = e.defn.progression, e.progress_stage<br>        if stage &gt;= len(pd.thresholds): return<br>        if e[Tag.PROGRESS] &gt;= pd.thresholds[stage]:<br>            e[Tag.PROGRESS] = 0<br>            e.progress_stage += 1<br>            if pd.on_stage[stage]: pd.on_stage[stage].run(g, c)<br>            if pd.upgrade_to[stage]: g.transform(e, pd.upgrade_to[stage])<br>            elif \u5168\u90e8\u5b8c\u6210: pd.complete_action.run(g, c)<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u7528\u5b83\u8868\u8fbe\u4e0d\u540c\u5173\u952e\u8bcd\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"># \u4efb\u52a1\uff082017\uff09<br>ProgressionDef(counter_event=Event.SPELL_CAST, thresholds=(6,),<br>               zones={Zone.SECRET}, complete_action=AddToHand(\"REWARD\"))<br><br># \u6cd5\u672f\u77f3\uff082017\uff09<br>ProgressionDef(counter_event=Event.ARMOR_GAINED, thresholds=(3,),<br>               zones={Zone.HAND}, upgrade_to=(\"SPELLSTONE_T1\",))<br><br># \u4efb\u52a1\u94fe\uff082021\uff09<br>ProgressionDef(counter_event=Event.MINION_PLAYED, thresholds=(3, 3, 3),<br>               zones={Zone.SECRET},<br>               upgrade_to=(\"STAGE2\", \"STAGE3\", None),<br>               complete_action=SummonReward())<br><br># \u704c\u6ce8 Infuse\uff082022\uff09<br>ProgressionDef(counter_event=Event.DEATH, condition=Cond.DEAD_FRIENDLY_MINION,<br>               thresholds=(4,), zones={Zone.HAND},<br>               upgrade_to=(\"INFUSED_VERSION\",))<br><br># \u6316\u6398\uff082023\uff09<br>ProgressionDef(counter_event=Event.EXCAVATE, thresholds=(1,1,1,1),<br>               on_stage=(Tier1(), Tier2(), Tier3(), Legendary()))<br><br># \u5148\u9a71\uff082026\uff09<br>ProgressionDef(counter_event=Event.HERALD_USED, thresholds=(2, 2),<br>               on_stage=(UpgradeColossal(1), UpgradeColossal(2)))<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u4e00\u4e2a 40 \u884c\u7684\u901a\u7528\u7ec4\u4ef6\uff0c\u8986\u76d6\u4e86\u4e5d\u5e74\u95f4\u7684\u516d\u4e2a\u5173\u952e\u8bcd\u3002<\/strong> \u8fd9\u5c31\u662f\u8fd9\u4efd\u6587\u6863\u60f3\u4f20\u8fbe\u7684\u6838\u5fc3\uff1a<strong>\u597d\u7684\u62bd\u8c61\u8ba9\u65b0\u673a\u5236\u7684\u8fb9\u9645\u6210\u672c\u8d8b\u8fd1\u4e8e\u96f6\u3002<\/strong><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b2c 36 \u7ae0 \u5c3e\u58f0\uff1a\u4e00\u4e2a\u7cfb\u7edf\u80fd\u6d3b\u5341\u4e8c\u5e74\u7684\u539f\u56e0<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">\u56de\u770b\u8fd9\u5341\u4e8c\u5e74\uff0c\u7089\u77f3\u5f15\u64ce\u80fd\u6301\u7eed\u6f14\u8fdb\u800c\u4e0d\u5d29\u584c\uff0c\u6211\u8ba4\u4e3a\u6709\u56db\u4e2a\u539f\u56e0\uff1a<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u4e00\u3001\u5b83\u5728\u7b2c\u4e00\u5e74\u5c31\u4ed8\u51fa\u4e86&#8221;\u8fc7\u5ea6\u8bbe\u8ba1&#8221;\u7684\u4ee3\u4ef7<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">2014 \u5e74\u7684\u5f15\u64ce\u91cc\u5c31\u6709\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>CONTROLLER<\/code> \u4e0e <code>OWNER<\/code> \u5206\u79bb\uff082026 \u5e74\u624d\u88ab&#8221;\u5728\u5bf9\u624b\u573a\u4e0a\u6253\u51fa\u968f\u4ece&#8221;\u7528\u5230\uff09<\/li>\n\n\n\n<li><code>SETASIDE<\/code> \u533a\uff082022 \u5e74\u624d\u88ab\u5de8\u578b\u5145\u5206\u5229\u7528\uff09<\/li>\n\n\n\n<li><code>ZONE_POSITION<\/code> \u5bf9\u624b\u724c\u4e5f\u7ef4\u62a4\uff082020 \u5e74\u624d\u88ab\u6d41\u653e\u7528\u5230\uff09<\/li>\n\n\n\n<li>\u5361\u724c\u7c7b\u578b\u662f tag\uff082022 \u5e74\u624d\u52a0\u5730\u6807\uff09<\/li>\n\n\n\n<li>\u9644\u9b54\u662f\u5b9e\u4f53\uff082018 \u5e74\u624d\u88ab\u78c1\u529b\u5145\u5206\u5229\u7528\uff09<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">\u8fd9\u4e9b\u5728 2014 \u5e74\u770b\u8d77\u6765\u90fd\u662f&#8221;\u7528\u4e0d\u4e0a\u7684\u590d\u6742\u5ea6&#8221;\u3002<strong>\u4f46\u5b83\u4eec\u662f\u8fd9\u4e2a\u7cfb\u7edf\u80fd\u6d3b\u5230 2026 \u5e74\u7684\u552f\u4e00\u539f\u56e0\u3002<\/strong><\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u4e8c\u3001\u5b83\u6562\u4e8e\u7528\u6570\u636e\u4fee\u6b63\u8bbe\u8ba1\u9519\u8bef<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>2019 \u5e74\u628a\u51e0\u5341\u5f20\u5361\u7684\u300a\u51b2\u950b\u300b\u6539\u6210\u300a\u7a81\u88ad\u300b<\/li>\n\n\n\n<li>2019 \u5e74\u628a\u300a\u6fc0\u6012\u300b\u6574\u4f53\u6362\u6210&#8221;\u53d7\u4f24\u540e\u6c38\u4e45 +X&#8221;<\/li>\n\n\n\n<li>2016 \u5e74\u628a\u300a\u5267\u6bd2\u300b\u4ece\u89e6\u53d1\u5f0f\u6539\u6210\u4f24\u5bb3\u7ba1\u7ebf\u5f0f<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u6bcf\u4e00\u6b21\u90fd\u662f\u6539\u6570\u636e\/\u6539\u4e00\u5904\u5f15\u64ce\u903b\u8f91\uff0c\u800c\u4e0d\u662f\u6539\u51e0\u5341\u5904\u5361\u724c\u4ee3\u7801\u3002<\/strong><\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u4e09\u3001\u5b83\u63a5\u53d7\u4e86 5% \u7684\u7279\u4f8b<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u300a\u5c24\u683c-\u8428\u9686\u300b\u3001\u300a\u9b54\u6cd5\u5951\u7ea6\u300b\u3001\u300a\u7830\u7830\u535a\u58eb\u300b\u7684\u67d0\u4e9b\u7248\u672c\uff0c\u90fd\u662f\u786c\u7f16\u7801\u7684\u3002<strong>\u6ca1\u6709\u5f3a\u884c\u8ba9 DSL \u8868\u8fbe\u4e00\u5207\u3002<\/strong><\/p>\n\n\n\n<h3 class=\"wp-block-heading\">\u56db\u3001\u5b83\u628a\u590d\u6742\u5ea6\u63a8\u5230\u4e86\u6b63\u786e\u7684\u5c42<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u590d\u6742\u5ea6<\/th><th>\u63a8\u5230\u54ea\u91cc<\/th><\/tr><\/thead><tbody><tr><td>\u6784\u7b51\u89c4\u5219\uff08\u7b26\u6587\u3001\u65c5\u5ba2\u3001\u53cc\u804c\u4e1a\uff09<\/td><td>\u6784\u7b51\u6821\u9a8c<\/td><\/tr><tr><td>\u52a8\u753b\u4e0e\u8868\u73b0<\/td><td>\u5ba2\u6237\u7aef<\/td><\/tr><tr><td>\u968f\u673a\u7684\u516c\u5e73\u6027<\/td><td>\u670d\u52a1\u5668\u6743\u5a01 + \u65e5\u5fd7<\/td><\/tr><tr><td>\u5361\u724c\u7684\u884c\u4e3a<\/td><td>\u6570\u636e\uff08DSL\uff09<\/td><\/tr><tr><td>\u89c4\u5219\u7684\u4ea4\u4e92<\/td><td>\u5f15\u64ce\uff08\u5c11\u91cf\u3001\u7a33\u5b9a\uff09<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5f15\u64ce\u672c\u8eab\u4fdd\u6301\u5f88\u5c0f\u3002<\/strong> \u8fd9\u662f\u5b83\u80fd\u88ab\u5c11\u6570\u51e0\u4e2a\u4eba\u7ef4\u62a4\u5341\u4e8c\u5e74\u7684\u539f\u56e0\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">\u6700\u540e\u4e00\u53e5<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\u5982\u679c\u4f60\u4ece\u8fd9\u4efd\u6587\u6863\u91cc\u53ea\u5e26\u8d70\u4e00\u53e5\u8bdd\uff0c\u6211\u5e0c\u671b\u662f\u8fd9\u53e5\uff1a<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong>\u4e0d\u8981\u4e3a\u4eca\u5929\u7684\u5361\u724c\u8bbe\u8ba1\u5f15\u64ce\uff0c\u8981\u4e3a\u4f60\u8fd8\u6ca1\u60f3\u5230\u7684\u90a3\u5f20\u5361\u8bbe\u8ba1\u5f15\u64ce\u3002<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u5177\u4f53\u7684\u505a\u6cd5\u662f\uff1a\u6bcf\u5f53\u4f60\u8981\u4e3a\u4e00\u5f20\u5361\u5199\u7279\u6b8a\u903b\u8f91\u65f6\uff0c\u5148\u95ee\u4e00\u53e5&#8221;\u8fd9\u5f20\u5361\u7684\u673a\u5236\u80fd\u4e0d\u80fd\u8868\u8fbe\u6210\u4e00\u4e2a\u66f4\u4e00\u822c\u7684\u4e1c\u897f&#8221;\u3002\u5982\u679c\u80fd\uff0c\u505a\u90a3\u4e2a\u4e00\u822c\u7684\u4e1c\u897f\u2014\u2014\u54ea\u6015\u73b0\u5728\u53ea\u6709\u4e00\u5f20\u5361\u7528\u5b83\u3002\u4e09\u5e74\u540e\u4f60\u4f1a\u6709\u4e8c\u5341\u5f20\u5361\u7528\u5b83\u3002<\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u9644\u5f55 A\uff1a\u5173\u952e\u8bcd\u901f\u67e5\u8868\uff08\u6309\u5e74\u4efd\uff09<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u5e74\u4efd<\/th><th>\u7248\u672c<\/th><th>\u65b0\u5173\u952e\u8bcd<\/th><th>\u8303\u5f0f<\/th><\/tr><\/thead><tbody><tr><td>2014.03<\/td><td>\u57fa\u7840\/\u7ecf\u5178<\/td><td>\u5632\u8bbd\u3001\u51b2\u950b\u3001\u5723\u76fe\u3001\u98ce\u6012\u3001\u6f5c\u884c\u3001\u6cd5\u672f\u4f24\u5bb3\u3001\u8fc7\u8f7d\u3001\u8fde\u51fb\u3001\u6218\u543c\u3001\u4ea1\u8bed\u3001\u5965\u79d8\u3001\u6289\u62e9\u3001\u51bb\u7ed3\u3001\u6c89\u9ed8\u3001\u514d\u75ab\u3001\u6fc0\u6012<\/td><td>\u5168\u90e8\u516d\u79cd<\/td><\/tr><tr><td>2014.07<\/td><td>\u7eb3\u514b\u8428\u739b\u65af<\/td><td>\uff08\u4ea1\u8bed\u4e3b\u9898\uff09<\/td><td>\u2014<\/td><\/tr><tr><td>2014.12<\/td><td>\u5730\u7cbe\u5927\u6218\u4f8f\u5112<\/td><td>\u8d85\u7ea7\u98ce\u6012\u3001\u673a\u68b0\u3001\u96f6\u4ef6<\/td><td>\u9759\u6001\u6807\u7b7e<\/td><\/tr><tr><td>2015.04<\/td><td>\u9ed1\u77f3\u5c71<\/td><td>\uff08\u9f99 synergy\uff0c\u624b\u724c\u6761\u4ef6\uff09<\/td><td>\u89e6\u53d1\u5668<\/td><\/tr><tr><td>2015.08<\/td><td>\u51a0\u519b\u7684\u8bd5\u70bc<\/td><td><strong>\u52b1\u5fd7<\/strong>\u3001\u5bf9\u51b3<\/td><td>\u89e6\u53d1\u5668<\/td><\/tr><tr><td>2015.11<\/td><td>\u63a2\u9669\u8005\u534f\u4f1a<\/td><td><strong>\u53d1\u73b0<\/strong> \u2605<\/td><td>\u9009\u62e9\u578b<\/td><\/tr><tr><td>2016.04<\/td><td>\u4e0a\u53e4\u4e4b\u795e<\/td><td>\u514b\u82cf\u6069\u8ba1\u6570\u5668\u3001\u8150\u5316<\/td><td>\u72b6\u6001\u673a<\/td><\/tr><tr><td>2016.08<\/td><td>\u5361\u62c9\u8d5e\u4e4b\u591c<\/td><td>\u2014<\/td><td>\u2014<\/td><\/tr><tr><td>2016.12<\/td><td>\u52a0\u57fa\u68ee<\/td><td><strong>\u5267\u6bd2<\/strong>\uff08\u5173\u952e\u8bcd\u5316\uff09\u3001\u7389\u83b2\u82b1\u3001\u624b\u724c buff<\/td><td>\u9759\u6001\u6807\u7b7e<\/td><\/tr><tr><td>2017.04<\/td><td>\u5b89\u6208\u6d1b<\/td><td><strong>\u9002\u5e94<\/strong>\u3001<strong>\u4efb\u52a1<\/strong> \u2605\u3001\u5143\u7d20<\/td><td>\u9009\u62e9\u578b + \u72b6\u6001\u673a<\/td><\/tr><tr><td>2017.08<\/td><td>\u51b0\u5c01\u738b\u5ea7<\/td><td><strong>\u5438\u8840<\/strong>\u3001\u82f1\u96c4\u724c<\/td><td>\u9759\u6001\u6807\u7b7e<\/td><\/tr><tr><td>2017.12<\/td><td>\u72d7\u5934\u4eba<\/td><td><strong>\u53ec\u52df<\/strong>\u3001\u6cd5\u672f\u77f3<\/td><td>\u72b6\u6001\u673a<\/td><\/tr><tr><td>2018.04<\/td><td>\u5973\u5deb\u68ee\u6797<\/td><td><strong>\u7a81\u88ad<\/strong> \u2605\u3001<strong>\u56de\u54cd<\/strong>\u3001\u5355\u53cc\u6570\u3001\u5f00\u5c40\u65f6<\/td><td>\u9759\u6001\u6807\u7b7e + \u51fa\u724c\u94a9\u5b50<\/td><\/tr><tr><td>2018.08<\/td><td>\u7830\u7830\u8ba1\u5212<\/td><td><strong>\u78c1\u529b<\/strong> \u2605\u3001Omega\u3001Project<\/td><td>\u591a\u5b9e\u4f53<\/td><\/tr><tr><td>2018.12<\/td><td>\u62c9\u65af\u5854\u54c8<\/td><td><strong>\u8d85\u6740<\/strong><\/td><td>\u89e6\u53d1\u5668<\/td><\/tr><tr><td>2019.04<\/td><td>\u6697\u5f71\u5d1b\u8d77<\/td><td><strong>\u53cc\u751f\u6cd5\u672f<\/strong>\u3001\u8ddf\u73ed\u3001<strong>\u9634\u8c0b<\/strong><\/td><td>\u51fa\u724c\u94a9\u5b50 + \u72b6\u6001\u673a<\/td><\/tr><tr><td>2019.08<\/td><td>\u5965\u4e39\u59c6\u5947\u5175<\/td><td><strong>\u590d\u751f<\/strong><\/td><td>\u9759\u6001\u6807\u7b7e<\/td><\/tr><tr><td>2019.12<\/td><td>\u5de8\u9f99\u964d\u4e34<\/td><td><strong>\u63a2\u5e95<\/strong>\u3001\u652f\u7ebf\u4efb\u52a1<\/td><td>\u72b6\u6001\u673a<\/td><\/tr><tr><td>2020.04<\/td><td>\u5916\u57df\u7684\u7070\u70ec<\/td><td><strong>\u6d41\u653e<\/strong> \u2605\u3001<strong>\u81f3\u5c0a<\/strong>\u3001\u6076\u9b54\u730e\u624b<\/td><td>\u51fa\u724c\u94a9\u5b50<\/td><\/tr><tr><td>2020.08<\/td><td>\u901a\u7075\u5b66\u56ed<\/td><td><strong>\u6cd5\u672f\u8ff8\u53d1<\/strong>\u3001\u53cc\u804c\u4e1a<\/td><td>\u89e6\u53d1\u5668<\/td><\/tr><tr><td>2020.11<\/td><td>\u8fbe\u62c9\u7136<\/td><td><strong>\u8150\u8680<\/strong><\/td><td>\u72b6\u6001\u673a<\/td><\/tr><tr><td>2021.03<\/td><td>\u8d2b\u7620\u4e4b\u5730<\/td><td><strong>\u72c2\u4e71<\/strong>\u3001\u9636\u7ea7\u6cd5\u672f\u3001\u6cd5\u672f\u5b66\u6d3e<\/td><td>\u89e6\u53d1\u5668<\/td><\/tr><tr><td>2021.08<\/td><td>\u66b4\u98ce\u57ce<\/td><td><strong>\u4efb\u52a1\u94fe<\/strong>\u3001<strong>\u53ef\u4ea4\u6613<\/strong> \u2605<\/td><td>\u72b6\u6001\u673a + \u65b0\u52a8\u4f5c<\/td><\/tr><tr><td>2021.12<\/td><td>\u5965\u5c71<\/td><td><strong>\u8363\u8a89\u51fb\u6740<\/strong><\/td><td>\u89e6\u53d1\u5668<\/td><\/tr><tr><td>2022.04<\/td><td>\u6c89\u6ca1\u4e4b\u57ce<\/td><td><strong>\u6253\u635e<\/strong>\u3001<strong>\u5de8\u578b<\/strong> \u2605<\/td><td>\u9009\u62e9\u578b + \u591a\u5b9e\u4f53<\/td><\/tr><tr><td>2022.08<\/td><td>\u7eb3\u65af\u5229\u4e9a\u5821<\/td><td><strong>\u5730\u6807<\/strong> \u2605\u3001<strong>\u704c\u6ce8<\/strong><\/td><td>\u65b0\u5361\u724c\u7c7b\u578b + \u72b6\u6001\u673a<\/td><\/tr><tr><td>2022.12<\/td><td>\u5deb\u5996\u738b<\/td><td>\u7b26\u6587\u3001<strong>\u5c38\u5757<\/strong>\u3001<strong>\u6cd5\u529b\u6e34\u6c42<\/strong>\u3001\u6b7b\u4ea1\u9a91\u58eb<\/td><td>\u65b0\u8d44\u6e90<\/td><\/tr><tr><td>2023.04<\/td><td>\u4f20\u8bf4\u8282\u65e5<\/td><td><strong>\u7ec8\u66f2<\/strong><\/td><td>\u51fa\u724c\u94a9\u5b50<\/td><\/tr><tr><td>2023.08<\/td><td>\u6cf0\u5766<\/td><td><strong>\u6cf0\u5766<\/strong> \u2605\u3001<strong>\u953b\u9020<\/strong><\/td><td>\u65b0\u52a8\u4f5c<\/td><\/tr><tr><td>2023.11<\/td><td>\u8352\u829c\u4e4b\u5730<\/td><td><strong>\u6316\u6398<\/strong>\u3001<strong>\u5feb\u901f\u5c04\u51fb<\/strong><\/td><td>\u72b6\u6001\u673a<\/td><\/tr><tr><td>2024.03<\/td><td>\u7ef4\u5179\u73ed\u5de5\u574a<\/td><td><strong>\u8ff7\u4f60\u5316<\/strong><\/td><td>\u7ec4\u5408<\/td><\/tr><tr><td>2024.07<\/td><td>\u5929\u7a7a\u90ae\u8f6e<\/td><td><strong>\u65c5\u5ba2<\/strong><\/td><td>\u6784\u7b51\u671f<\/td><\/tr><tr><td>2024.11<\/td><td>\u6d69\u701a\u4e4b\u5916<\/td><td><strong>\u661f\u8230<\/strong> \u2605<\/td><td>\u591a\u5b9e\u4f53<\/td><\/tr><tr><td>2025.03<\/td><td>\u7fe1\u7fe0\u68a6\u5883<\/td><td><strong>\u704c\u6ce8(Imbue)<\/strong>\u3001<strong>\u9ed1\u6697\u6069\u8d50<\/strong>\u3001\u6289\u62e9\u5168\u804c\u4e1a\u5316<\/td><td>\u72b6\u6001\u673a + \u9009\u62e9\u578b<\/td><\/tr><tr><td>2025.07<\/td><td>\u5931\u843d\u7684\u5b89\u6208\u6d1b\u4e4b\u57ce<\/td><td><strong>\u540c\u6e90<\/strong>\u3001\u6545\u4e8b\u5361\u3001\u5730\u56fe\u5361<\/td><td>\u89e6\u53d1\u5668<\/td><\/tr><tr><td>2025.11<\/td><td>\u65f6\u5149\u4e4b\u5916<\/td><td><strong>\u5012\u5e26<\/strong> \u2605\u3001<strong>\u4f20\u5947<\/strong><\/td><td>\u64a4\u9500\/\u91cd\u505a + \u6784\u7b51\u671f<\/td><\/tr><tr><td>2026.03<\/td><td>\u5927\u707e\u53d8<\/td><td><strong>\u5148\u9a71<\/strong>\u3001<strong>\u788e\u88c2<\/strong> \u2605\u3001\u5de8\u578b\u56de\u5f52<\/td><td>\u72b6\u6001\u673a + \u591a\u5b9e\u4f53<\/td><\/tr><tr><td>2026.07<\/td><td>\u7d2b\u7f57\u5170\u76d1\u72f1\u8d8a\u72f1<\/td><td><strong>\u51c6\u5907<\/strong> \u2605\u3001\u7834\u89c4\u4f20\u8bf4\u3001\u4f2a\u88c5\u968f\u4ece<\/td><td>\u65b0\u52a8\u4f5c<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">\u2605 = \u5f15\u64ce\u5c42\u9762\u7684\u91cd\u5927\u65b0\u589e\u80fd\u529b<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">\u9644\u5f55 B\uff1a\u672f\u8bed\u5bf9\u7167<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u4e2d\u6587<\/th><th>\u82f1\u6587<\/th><th>\u5f15\u64ce\u6982\u5ff5<\/th><\/tr><\/thead><tbody><tr><td>\u5b9e\u4f53<\/td><td>Entity<\/td><td>\u6709 ID \u7684\u72b6\u6001\u5bb9\u5668<\/td><\/tr><tr><td>\u6807\u7b7e<\/td><td>Tag \/ GameTag<\/td><td>\u5b9e\u4f53\u7684\u4e00\u4e2a\u6574\u6570\u5c5e\u6027<\/td><\/tr><tr><td>\u533a\u57df<\/td><td>Zone<\/td><td>\u5b9e\u4f53\u6240\u5904\u7684\u903b\u8f91\u4f4d\u7f6e<\/td><\/tr><tr><td>\u9644\u9b54<\/td><td>Enchantment<\/td><td>\u9644\u7740\u5728\u5b9e\u4f53\u4e0a\u7684\u6301\u4e45\u4fee\u9970\u5b9e\u4f53<\/td><\/tr><tr><td>\u5149\u73af<\/td><td>Aura<\/td><td>\u7531\u6e90\u5b9e\u4f53\u6301\u7eed\u63d0\u4f9b\u7684\u6761\u4ef6\u6027\u4fee\u9970<\/td><\/tr><tr><td>\u89e6\u53d1\u5668<\/td><td>Trigger<\/td><td>\u4e8b\u4ef6\u8ba2\u9605<\/td><\/tr><tr><td>\u6b7b\u4ea1\u7ed3\u7b97\u9636\u6bb5<\/td><td>Death Phase \/ SBA<\/td><td>\u6279\u91cf\u5904\u7406\u6b7b\u4ea1\u7684\u9636\u6bb5<\/td><\/tr><tr><td>\u5feb\u7167<\/td><td>Snapshot<\/td><td>\u5ef6\u8fdf\u6548\u679c\u6240\u9700\u4e0a\u4e0b\u6587\u7684\u5373\u65f6\u62f7\u8d1d<\/td><\/tr><tr><td>\u53d8\u5316\u6d41<\/td><td>PowerHistory<\/td><td>\u670d\u52a1\u5668\u53d1\u7ed9\u5ba2\u6237\u7aef\u7684\u72b6\u6001\u53d8\u5316\u5e8f\u5217<\/td><\/tr><tr><td>\u884d\u751f\u7269<\/td><td>Token<\/td><td>\u4e0d\u53ef\u6536\u96c6\u7684\u3001\u8fd0\u884c\u65f6\u751f\u6210\u7684\u5361<\/td><\/tr><tr><td>\u9003\u751f\u8231<\/td><td>Special Effect<\/td><td>DSL \u65e0\u6cd5\u8868\u8fbe\u65f6\u7684\u786c\u7f16\u7801\u5165\u53e3<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">\u9644\u5f55 C\uff1a\u53c2\u8003\u4e0e\u6765\u6e90<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">\u4e8b\u5b9e\u6027\u5185\u5bb9\uff08\u7248\u672c\u65f6\u95f4\u7ebf\u3001\u5173\u952e\u8bcd\u6587\u672c\u3001\u89c4\u5219\u88c1\u5b9a\uff09\u6765\u6e90\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/hearthstone.wiki.gg\/wiki\/Expansion\">Hearthstone Wiki: Expansion<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/hearthstone.wiki.gg\/wiki\/Keyword\">Hearthstone Wiki: Keyword<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/hearthstone.wiki.gg\/wiki\/Prepare\">Hearthstone Wiki: Prepare<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/hearthstone.wiki.gg\/wiki\/Escape_from_Violet_Hold\">Hearthstone Wiki: Escape from Violet Hold<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/hearthstone.wiki.gg\/wiki\/CATACLYSM\">Hearthstone Wiki: CATACLYSM<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/news.blizzard.com\/en-gb\/article\/24179067\/step-into-the-emerald-dream-hearthstones-next-expansion\">Blizzard: Step Into the Emerald Dream<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/news.blizzard.com\/en-us\/article\/24226328\/\">Blizzard: Across the Timeways<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/news.blizzard.com\/en-us\/article\/24276664\/break-the-rules-in-escape-from-violet-hold-hearthstones-next-expansion\">Blizzard: Break the Rules in Escape from Violet Hold<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/esports.gg\/news\/hearthstone\/hearthstone-gets-across-the-timeways-expansion-with-new-rewind-and-fabled-keywords\/\">esports.gg: Rewind and Fabled keywords<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/esports.gg\/news\/hearthstone\/hearthstone-into-the-emerald-dream-release-date-and-details-imbue-dark-gifts-wild-gods-and-choose-one-for-all\/\">esports.gg: Imbue, Dark Gifts<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/esports.gg\/news\/hearthstone\/lost-city-of-ungoro-is-hearthstones-next-expansion-familiar-faces-kindred-and-story-cards\/\">esports.gg: Lost City of Un&#8217;Goro, Kindred<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/www.hearthstonetopdecks.com\/cataclysm-expansion-guide\/\">HearthstoneTopDecks: CATACLYSM Guide<\/a><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u5173\u4e8e\u5b9e\u73b0\u7ec6\u8282\u7684\u514d\u8d23\u58f0\u660e<\/strong>\uff1a\u672c\u6587\u4e2d\u6240\u6709\u5f15\u64ce\u5b9e\u73b0\uff08\u7c7b\u540d\u3001\u6570\u636e\u7ed3\u6784\u3001\u7b97\u6cd5\u6d41\u7a0b\uff09\u90fd\u662f<strong>\u57fa\u4e8e\u516c\u5f00\u53ef\u89c2\u5bdf\u884c\u4e3a\u7684\u91cd\u6784\u4e0e\u63a8\u65ad<\/strong>\uff0c\u4e0d\u662f\u66b4\u96ea\u7684\u6e90\u4ee3\u7801\u3002\u51e1\u6807\u6ce8&#8221;\u63a8\u65ad&#8221;\u5904\u5c24\u5176\u5982\u6b64\u3002\u771f\u5b9e\u7684\u7089\u77f3\u5ba2\u6237\u7aef\u662f Unity + C#\uff0c\u5176\u5185\u90e8\u7ed3\u6784\u4e0e\u672c\u6587\u7684 Python \u91cd\u6784\u5728\u6982\u5ff5\u4e0a\u5bf9\u5e94\u4f46\u5728\u5b9e\u73b0\u4e0a\u4e0d\u540c\u3002<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u672c\u6587\u7684\u4ef7\u503c\u4e0d\u5728\u4e8e&#8221;\u7089\u77f3\u5177\u4f53\u662f\u600e\u4e48\u5199\u7684&#8221;\uff0c\u800c\u5728\u4e8e&#8221;\u8981\u652f\u6491\u8fd9\u6837\u7684\u884c\u4e3a\uff0c\u4e00\u4e2a\u5f15\u64ce\u5fc5\u987b\u5177\u5907\u4ec0\u4e48\u6027\u8d28&#8221;\u3002\u8fd9\u4e9b\u6027\u8d28\u662f\u53ef\u63a8\u5bfc\u7684\uff0c\u4e5f\u662f\u53ef\u8fc1\u79fb\u7684\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><em>\u6587\u6863\u5b8c<\/em><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u9644\u5f55 D\uff1a<code>stoneheart.py<\/code> \u5b8c\u6574\u6e90\u7801<\/h2>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">\u96f6\u4f9d\u8d56\uff0c<code>python3 stoneheart.py<\/code> \u76f4\u63a5\u8fd0\u884c\uff0c24 \u9879\u81ea\u68c0\u5168\u90e8\u901a\u8fc7\u3002<\/p>\n<\/blockquote>\n\n\n\n<pre class=\"wp-block-preformatted\">\"\"\"<br>stoneheart \u2014\u2014 \u4e00\u4e2a\u7089\u77f3\u4f20\u8bf4\u98ce\u683c\u7684\u8ff7\u4f60\u5361\u724c\u5f15\u64ce<br>=================================================<br>\u7528\u4e8e\u6f14\u793a\u6587\u6863\u300a\u7089\u77f3\u4f20\u8bf4\u5f15\u64ce\u6df1\u5ea6\u89e3\u5256\u300b\u4e2d\u7684\u6240\u6709\u6838\u5fc3\u673a\u5236\u3002<br><br>\u8bbe\u8ba1\u76ee\u6807\uff1a<br>  * \u5355\u6587\u4ef6\u3001\u96f6\u4f9d\u8d56\u3001\u53ef\u76f4\u63a5\u8fd0\u884c<br>  * \u8986\u76d6\u516d\u5927\u5173\u952e\u8bcd\u8303\u5f0f\uff1a\u9759\u6001\u6807\u7b7e \/ \u89e6\u53d1\u5668 \/ \u51fa\u724c\u94a9\u5b50 \/ \u9009\u62e9\u578b \/ \u72b6\u6001\u673a \/ \u591a\u5b9e\u4f53<br>  * \u786e\u5b9a\u6027\u968f\u673a\u3001\u4e8b\u4ef6\u6d41\u8bb0\u5f55\u3001\u5b8c\u6574\u6b7b\u4ea1\u7ed3\u7b97<br><br>\u8fd0\u884c\uff1apython3 stoneheart.py<br>\"\"\"<br>from __future__ import annotations<br><br>import random<br>import itertools<br>from collections import defaultdict, deque<br>from dataclasses import dataclass, field, replace<br>from enum import IntEnum, auto<br>from typing import Any, Callable, Iterable<br><br># =============================================================================<br># 1. \u679a\u4e3e\u4e0e\u5e38\u91cf<br># =============================================================================<br><br><br>class Zone(IntEnum):<br>    INVALID = 0<br>    PLAY = 1<br>    DECK = 2<br>    HAND = 3<br>    GRAVEYARD = 4<br>    SETASIDE = 5<br>    SECRET = 6<br>    HERO = 7<br><br><br>class CardType(IntEnum):<br>    INVALID = 0<br>    MINION = 1<br>    SPELL = 2<br>    WEAPON = 3<br>    HERO = 4<br>    HERO_POWER = 5<br>    ENCHANTMENT = 6<br><br><br>class Tag(IntEnum):<br>    CARD_ID = auto()<br>    ENTITY_ID = auto()<br>    CONTROLLER = auto()<br>    OWNER = auto()<br>    ZONE = auto()<br>    ZONE_POSITION = auto()<br>    CARDTYPE = auto()<br>    CARDRACE = auto()<br>    SPELL_SCHOOL = auto()<br><br>    COST = auto()<br>    ATK = auto()<br>    HEALTH = auto()<br>    DAMAGE = auto()<br>    ARMOR = auto()<br>    DURABILITY = auto()<br><br>    TAUNT = auto()<br>    DIVINE_SHIELD = auto()<br>    CHARGE = auto()<br>    RUSH = auto()<br>    STEALTH = auto()<br>    POISONOUS = auto()<br>    LIFESTEAL = auto()<br>    WINDFURY = auto()<br>    FROZEN = auto()<br>    IMMUNE = auto()<br>    ELUSIVE = auto()<br>    REBORN = auto()<br>    SILENCED = auto()<br>    SPELLPOWER = auto()<br>    TRADEABLE = auto()<br>    PREPARE = auto()<br><br>    EXHAUSTED = auto()<br>    NUM_ATTACKS = auto()<br>    NUM_TURNS_IN_PLAY = auto()<br>    TO_BE_DESTROYED = auto()<br>    TEMPORARY = auto()<br>    TRIGGERED_ONCE = auto()<br>    QUEST_PROGRESS = auto()<br>    QUEST_TOTAL = auto()<br>    PROGRESS = auto()<br>    PREPARE_DISCOUNT = auto()<br>    PREPARED = auto()<br>    LOCKED_UNTIL_TURN = auto()<br>    DRAWN_ON_TURN = auto()<br>    ATTACHED = auto()<br><br>    MANA_CRYSTALS = auto()<br>    MANA_AVAILABLE = auto()<br>    OVERLOAD_OWED = auto()<br>    OVERLOAD_LOCKED = auto()<br>    FATIGUE = auto()<br>    COMBO_ACTIVE = auto()<br><br><br>IN_PLAY_ZONES = (Zone.PLAY, Zone.HERO)<br><br><br>def in_play(e) -&gt; bool:<br>    return e.tags.get(Tag.ZONE, Zone.INVALID) in IN_PLAY_ZONES<br><br><br>SILENCEABLE_TAGS = {<br>    Tag.TAUNT, Tag.DIVINE_SHIELD, Tag.CHARGE, Tag.RUSH, Tag.STEALTH,<br>    Tag.POISONOUS, Tag.LIFESTEAL, Tag.WINDFURY, Tag.SPELLPOWER,<br>    Tag.REBORN, Tag.ELUSIVE,<br>}<br><br>TRANSFORM_PRESERVED = {<br>    Tag.ENTITY_ID, Tag.ZONE, Tag.ZONE_POSITION, Tag.CONTROLLER, Tag.OWNER,<br>    Tag.EXHAUSTED, Tag.NUM_ATTACKS, Tag.FROZEN, Tag.NUM_TURNS_IN_PLAY,<br>}<br><br>KEYWORD_TO_TAG = {<br>    \"TAUNT\": Tag.TAUNT, \"DIVINE_SHIELD\": Tag.DIVINE_SHIELD,<br>    \"CHARGE\": Tag.CHARGE, \"RUSH\": Tag.RUSH, \"STEALTH\": Tag.STEALTH,<br>    \"POISONOUS\": Tag.POISONOUS, \"LIFESTEAL\": Tag.LIFESTEAL,<br>    \"WINDFURY\": Tag.WINDFURY, \"ELUSIVE\": Tag.ELUSIVE, \"REBORN\": Tag.REBORN,<br>    \"TRADEABLE\": Tag.TRADEABLE, \"PREPARE\": Tag.PREPARE,<br>}<br><br><br>class Event(IntEnum):<br>    GAME_START = auto()<br>    TURN_START = auto()<br>    TURN_END = auto()<br>    DRAW = auto()<br>    CARD_BURNED = auto()<br>    PLAY_CARD = auto()<br>    MINION_PLAYED = auto()<br>    SPELL_CAST = auto()<br>    AFTER_SPELL = auto()<br>    PRE_SUMMON = auto()<br>    SUMMON = auto()<br>    SUMMON_FAILED = auto()<br>    PROPOSED_ATTACK = auto()<br>    ATTACK = auto()<br>    AFTER_ATTACK = auto()<br>    PRE_DAMAGE = auto()<br>    DAMAGE = auto()<br>    HEAL = auto()<br>    PROPOSED_DEATH = auto()<br>    DEATH = auto()<br>    AFTER_DEATH = auto()<br>    HERO_POWER_USED = auto()<br>    SECRET_REVEALED = auto()<br>    CARD_TRADED = auto()<br>    CARD_PREPARED = auto()<br><br><br># =============================================================================<br># 2. \u786e\u5b9a\u6027\u968f\u673a<br># =============================================================================<br><br><br>class GameRNG:<br>    \"\"\"\u6240\u6709\u6e38\u620f\u968f\u673a\u7684\u552f\u4e00\u5165\u53e3\u3002\u8bb0\u5f55\u8c03\u7528\u65e5\u5fd7\u4ee5\u652f\u6301\u56de\u653e\u4e0e desync \u68c0\u6d4b\u3002\"\"\"<br><br>    def __init__(self, seed: int):<br>        self.seed = seed<br>        self._rng = random.Random(seed)<br>        self.log: list[tuple] = []<br><br>    def randrange(self, n: int) -&gt; int:<br>        i = self._rng.randrange(n)<br>        self.log.append((\"randrange\", n, i))<br>        return i<br><br>    def choice(self, seq):<br>        seq = list(seq)<br>        if not seq:<br>            raise ValueError(\"choice from empty sequence\")<br>        return seq[self.randrange(len(seq))]<br><br>    def sample(self, seq, k):<br>        seq = list(seq)<br>        k = min(k, len(seq))<br>        idx = self._rng.sample(range(len(seq)), k)<br>        self.log.append((\"sample\", len(seq), tuple(idx)))<br>        return [seq[i] for i in idx]<br><br>    def randint(self, a: int, b: int) -&gt; int:<br>        v = self._rng.randint(a, b)<br>        self.log.append((\"randint\", (a, b), v))<br>        return v<br><br>    def shuffle(self, lst: list):<br>        perm = list(range(len(lst)))<br>        self._rng.shuffle(perm)<br>        self.log.append((\"shuffle\", len(lst), tuple(perm)))<br>        lst[:] = [lst[i] for i in perm]<br><br><br>class ReplayRNG(GameRNG):<br>    \"\"\"\u56de\u653e\u6a21\u5f0f\uff1a\u4ece\u65e5\u5fd7\u8bfb\u53d6\uff0c\u4efb\u4f55\u504f\u5dee\u7acb\u523b\u62a5\u9519\u3002\"\"\"<br><br>    def __init__(self, log):<br>        self.log_in = list(log)<br>        self.idx = 0<br>        self.log = []<br><br>    def _next(self, op, domain):<br>        if self.idx &gt;= len(self.log_in):<br>            raise AssertionError(\"RNG log exhausted \u2014 engine consumed more randomness\")<br>        rec = self.log_in[self.idx]<br>        self.idx += 1<br>        if rec[0] != op or rec[1] != domain:<br>            raise AssertionError(<br>                f\"RNG desync at call #{self.idx}: log has {rec[:2]}, engine asked {(op, domain)}\")<br>        return rec[2]<br><br>    def randrange(self, n):  return self._next(\"randrange\", n)<br>    def sample(self, seq, k):<br>        seq = list(seq)<br>        idx = self._next(\"sample\", len(seq))<br>        return [seq[i] for i in idx]<br>    def randint(self, a, b): return self._next(\"randint\", (a, b))<br>    def shuffle(self, lst):<br>        perm = self._next(\"shuffle\", len(lst))<br>        lst[:] = [lst[i] for i in perm]<br><br><br># =============================================================================<br># 3. Selector \/ Condition \/ Amount \u2014\u2014 \u6570\u636e\u9a71\u52a8\u7684\u4e09\u6839\u652f\u67f1<br># =============================================================================<br><br><br>@dataclass<br>class Ctx:<br>    source: \"Entity\"<br>    controller: int<br>    target: \"Entity | None\" = None<br>    event: \"EventCtx | None\" = None<br>    choose: int = 0<br>    finale: bool = False<br>    stored: dict = field(default_factory=dict)<br><br><br>class Selector:<br>    def eval(self, game, ctx) -&gt; list[\"Entity\"]:<br>        raise NotImplementedError<br><br>    def __add__(self, o):  return _Union(self, o)<br>    def __sub__(self, o):  return _Diff(self, o)<br>    def where(self, c):    return _Filtered(self, c)<br>    def random(self, n=1): return _RandomPick(self, n)<br>    def top(self, n, key): return _TopN(self, n, key)<br><br><br>class _Union(Selector):<br>    def __init__(self, a, b): self.a, self.b = a, b<br>    def eval(self, g, c):<br>        out = list(self.a.eval(g, c))<br>        for e in self.b.eval(g, c):<br>            if e not in out:<br>                out.append(e)<br>        return out<br><br><br>class _Diff(Selector):<br>    def __init__(self, a, b): self.a, self.b = a, b<br>    def eval(self, g, c):<br>        ban = set(id(x) for x in self.b.eval(g, c))<br>        return [e for e in self.a.eval(g, c) if id(e) not in ban]<br><br><br>class _Filtered(Selector):<br>    def __init__(self, inner, cond): self.inner, self.cond = inner, cond<br>    def eval(self, g, c):<br>        return [e for e in self.inner.eval(g, c) if self.cond.eval(g, e, c)]<br><br><br>class _RandomPick(Selector):<br>    def __init__(self, inner, n): self.inner, self.n = inner, n<br>    def eval(self, g, c):<br>        pool = self.inner.eval(g, c)<br>        if not pool: return []<br>        return g.rng.sample(pool, min(self.n, len(pool)))<br><br><br>class _TopN(Selector):<br>    def __init__(self, inner, n, key): self.inner, self.n, self.key = inner, n, key<br>    def eval(self, g, c):<br>        pool = self.inner.eval(g, c)<br>        if not pool: return []<br>        best = max(self.key(e) for e in pool)<br>        tied = [e for e in pool if self.key(e) == best]<br>        return tied if len(tied) &lt;= self.n else g.rng.sample(tied, self.n)<br><br><br>class AllMinions(Selector):<br>    def eval(self, g, c):<br>        return [e for pid in (0, 1) for e in g.zones.get(pid, Zone.PLAY)<br>                if e[Tag.CARDTYPE] == CardType.MINION]<br><br><br>class AllHeroes(Selector):<br>    def eval(self, g, c):<br>        return [g.player(0).hero, g.player(1).hero]<br><br><br>class Friendly(Selector):<br>    def __init__(self, inner): self.inner = inner<br>    def eval(self, g, c):<br>        return [e for e in self.inner.eval(g, c) if e[Tag.CONTROLLER] == c.controller]<br><br><br>class Enemy(Selector):<br>    def __init__(self, inner): self.inner = inner<br>    def eval(self, g, c):<br>        return [e for e in self.inner.eval(g, c) if e[Tag.CONTROLLER] != c.controller]<br><br><br>class SelfSel(Selector):<br>    def eval(self, g, c): return [c.source]<br><br><br>class TargetSel(Selector):<br>    def eval(self, g, c): return [c.target] if c.target is not None else []<br><br><br>class EventEntitySel(Selector):<br>    def eval(self, g, c):<br>        return [c.event.entity] if (c.event and c.event.entity) else []<br><br><br>class InZone(Selector):<br>    def __init__(self, zone, enemy=False): self.zone, self.enemy = zone, enemy<br>    def eval(self, g, c):<br>        pid = (1 - c.controller) if self.enemy else c.controller<br>        return list(g.zones.get(pid, self.zone))<br><br><br>class Sel:<br>    ALL_MINIONS = AllMinions()<br>    FRIENDLY_MINIONS = Friendly(AllMinions())<br>    ENEMY_MINIONS = Enemy(AllMinions())<br>    OTHER_FRIENDLY = _Diff(Friendly(AllMinions()), SelfSel())<br>    ALL_CHARACTERS = _Union(AllMinions(), AllHeroes())<br>    ENEMY_CHARACTERS = Enemy(_Union(AllMinions(), AllHeroes()))<br>    SELF = SelfSel()<br>    TARGET = TargetSel()<br>    EVENT_ENTITY = EventEntitySel()<br>    MY_HAND = InZone(Zone.HAND)<br>    MY_DECK = InZone(Zone.DECK)<br><br><br># ---------------------------------------------------------------- Conditions<br>class Condition:<br>    def eval(self, game, e, ctx) -&gt; bool: raise NotImplementedError<br>    def __and__(self, o): return _And(self, o)<br>    def __or__(self, o):  return _Or(self, o)<br>    def __invert__(self): return _Not(self)<br><br><br>class _And(Condition):<br>    def __init__(self, a, b): self.a, self.b = a, b<br>    def eval(self, g, e, c): return self.a.eval(g, e, c) and self.b.eval(g, e, c)<br><br><br>class _Or(Condition):<br>    def __init__(self, a, b): self.a, self.b = a, b<br>    def eval(self, g, e, c): return self.a.eval(g, e, c) or self.b.eval(g, e, c)<br><br><br>class _Not(Condition):<br>    def __init__(self, a): self.a = a<br>    def eval(self, g, e, c): return not self.a.eval(g, e, c)<br><br><br>class Always(Condition):<br>    def eval(self, g, e, c): return True<br><br><br>class HasTag(Condition):<br>    def __init__(self, t): self.t = t<br>    def eval(self, g, e, c): return bool(e[self.t])<br><br><br>class IsRace(Condition):<br>    def __init__(self, r): self.r = r<br>    def eval(self, g, e, c): return e[Tag.CARDRACE] in (self.r, \"ALL\")<br><br><br>class IsMinion(Condition):<br>    def eval(self, g, e, c): return e[Tag.CARDTYPE] == CardType.MINION<br><br><br>class AttackAtMost(Condition):<br>    def __init__(self, n): self.n = n<br>    def eval(self, g, e, c): return e[Tag.ATK] &lt;= self.n<br><br><br>class MyTurn(Condition):<br>    def eval(self, g, e, c): return g.current == e[Tag.CONTROLLER]<br><br><br>class ControllerIsOwner(Condition):<br>    \"\"\"\u4e8b\u4ef6\u7684\u53d1\u8d77\u65b9\u662f\u8fd9\u4e2a\u89e6\u53d1\u5668\u7684\u4e3b\u4eba\"\"\"<br>    def eval(self, g, e, c):<br>        ev = c.event<br>        if ev is None: return False<br>        pid = ev.player<br>        if pid is None and ev.entity is not None:<br>            pid = ev.entity[Tag.CONTROLLER]<br>        if pid is None and ev.source is not None:<br>            pid = ev.source[Tag.CONTROLLER]<br>        return pid == e[Tag.CONTROLLER]<br><br><br>class EventEntityIsSelf(Condition):<br>    def eval(self, g, e, c): return c.event is not None and c.event.entity is e<br><br><br>class EventTargetIsSelf(Condition):<br>    def eval(self, g, e, c): return c.event is not None and c.event.target is e<br><br><br>class Survived(Condition):<br>    def eval(self, g, e, c):<br>        return c.event is not None and c.event.amount &gt; 0 and not c.event.fatal<br><br><br>class Overkill(Condition):<br>    def eval(self, g, e, c):<br>        ev = c.event<br>        return (ev is not None and ev.source is e<br>                and ev.amount &gt; ev.target_health_before<br>                and g.current == e[Tag.CONTROLLER])<br><br><br>class HonorableKill(Condition):<br>    def eval(self, g, e, c):<br>        ev = c.event<br>        return (ev is not None and ev.source is e<br>                and ev.amount == ev.target_health_before and ev.fatal)<br><br><br>class DeadIsFriendlyMinion(Condition):<br>    def eval(self, g, e, c):<br>        ev = c.event<br>        return (ev is not None and ev.entity is not None<br>                and ev.entity[Tag.CARDTYPE] == CardType.MINION<br>                and ev.entity[Tag.CONTROLLER] == e[Tag.CONTROLLER])<br><br><br>class StatAtLeast(Condition):<br>    \"\"\"\u7edf\u4e00\u7684\u5386\u53f2\u7edf\u8ba1\u67e5\u8be2 \u2014\u2014 \u89c1\u7b2c 23 \u7ae0\u7684\u5efa\u8bae\"\"\"<br>    def __init__(self, key, n, scope=\"total\"):<br>        self.key, self.n, self.scope = key, n, scope<br>    def eval(self, g, e, c):<br>        st = g.player(e[Tag.CONTROLLER]).stats<br>        d = {\"total\": st.total, \"turn\": st.turn, \"last\": st.last}[self.scope]<br>        return d.get(self.key, 0) &gt;= self.n<br><br><br>class Cond:<br>    ALWAYS = Always()<br>    MY_TURN = MyTurn()<br>    OWNER = ControllerIsOwner()<br>    SELF_IS_EVENT_ENTITY = EventEntityIsSelf()<br>    SELF_IS_EVENT_TARGET = EventTargetIsSelf()<br>    SURVIVED = Survived()<br>    OVERKILL = Overkill()<br>    HONORABLE_KILL = HonorableKill()<br>    DEAD_FRIENDLY_MINION = DeadIsFriendlyMinion()<br><br><br># ------------------------------------------------------------------- Amounts<br>class Amount:<br>    def eval(self, g, c) -&gt; int: raise NotImplementedError<br><br><br>class CountOf(Amount):<br>    def __init__(self, sel): self.sel = sel<br>    def eval(self, g, c): return len(self.sel.eval(g, c))<br><br><br>class TagOf(Amount):<br>    def __init__(self, sel, tag): self.sel, self.tag = sel, tag<br>    def eval(self, g, c):<br>        es = self.sel.eval(g, c)<br>        return es[0][self.tag] if es else 0<br><br><br>class RandomRange(Amount):<br>    def __init__(self, a, b): self.a, self.b = a, b<br>    def eval(self, g, c): return g.rng.randint(self.a, self.b)<br><br><br>def amt(a, g, c) -&gt; int:<br>    return a.eval(g, c) if isinstance(a, Amount) else int(a)<br><br><br># =============================================================================<br># 4. Action \u2014\u2014 \u6548\u679c\u8bed\u8a00<br># =============================================================================<br><br><br>class Action:<br>    def run(self, game, ctx): raise NotImplementedError<br><br><br>class Seq(Action):<br>    def __init__(self, *acts): self.acts = acts<br>    def run(self, g, c):<br>        for a in self.acts: a.run(g, c)<br><br><br>class If(Action):<br>    def __init__(self, cond, then, otherwise=None):<br>        self.cond, self.then, self.otherwise = cond, then, otherwise<br>    def run(self, g, c):<br>        if self.cond.eval(g, c.source, c): self.then.run(g, c)<br>        elif self.otherwise: self.otherwise.run(g, c)<br><br><br>class Repeat(Action):<br>    def __init__(self, n, act): self.n, self.act = n, act<br>    def run(self, g, c):<br>        for _ in range(amt(self.n, g, c)): self.act.run(g, c)<br><br><br>class ForEach(Action):<br>    def __init__(self, sel, act): self.sel, self.act = sel, act<br>    def run(self, g, c):<br>        for e in list(self.sel.eval(g, c)):<br>            self.act.run(g, replace(c, target=e))<br><br><br>class Damage(Action):<br>    def __init__(self, sel, amount): self.sel, self.amount = sel, amount<br>    def run(self, g, c):<br>        targets = list(self.sel.eval(g, c))            # \u2605 \u65e9\u7ed1\u5b9a\u5feb\u7167<br>        n = amt(self.amount, g, c)<br>        if c.source[Tag.CARDTYPE] == CardType.SPELL:<br>            n += g.spellpower(c.controller)<br>        for t in targets:<br>            if not in_play(t): continue<br>            g.damage(t, n, source=c.source)<br><br><br>class Heal(Action):<br>    def __init__(self, sel, amount): self.sel, self.amount = sel, amount<br>    def run(self, g, c):<br>        n = amt(self.amount, g, c)<br>        for t in list(self.sel.eval(g, c)): g.heal(t, n)<br><br><br>class Destroy(Action):<br>    def __init__(self, sel): self.sel = sel<br>    def run(self, g, c):<br>        for e in list(self.sel.eval(g, c)): e[Tag.TO_BE_DESTROYED] = 1<br><br><br>class Draw(Action):<br>    def __init__(self, n=1): self.n = n<br>    def run(self, g, c): g.draw(c.controller, amt(self.n, g, c))<br><br><br>class Summon(Action):<br>    def __init__(self, card_id, n=1, at_death_position=False):<br>        self.card_id, self.n, self.at_death_pos = card_id, n, at_death_position<br>    def run(self, g, c):<br>        pos = None<br>        if self.at_death_pos and c.event and c.event.snapshot:<br>            pos = c.event.snapshot.position - 1<br>        for _ in range(amt(self.n, g, c)):<br>            g.summon(self.card_id, c.controller, pos)<br><br><br>class Buff(Action):<br>    def __init__(self, sel, atk=0, hp=0, keywords=()):<br>        self.sel, self.atk, self.hp, self.kws = sel, atk, hp, tuple(keywords)<br>    def run(self, g, c):<br>        for e in list(self.sel.eval(g, c)):<br>            g.apply_enchantment(e, atk=amt(self.atk, g, c), hp=amt(self.hp, g, c),<br>                                keywords=self.kws)<br><br><br>class GiveKeyword(Action):<br>    def __init__(self, sel, *kws): self.sel, self.kws = sel, kws<br>    def run(self, g, c):<br>        for e in list(self.sel.eval(g, c)):<br>            for k in self.kws: e[KEYWORD_TO_TAG[k]] = 1<br><br><br>class Freeze(Action):<br>    def __init__(self, sel): self.sel = sel<br>    def run(self, g, c):<br>        for e in list(self.sel.eval(g, c)): e[Tag.FROZEN] = 1<br><br><br>class SilenceAct(Action):<br>    def __init__(self, sel): self.sel = sel<br>    def run(self, g, c):<br>        for e in list(self.sel.eval(g, c)): g.silence(e)<br><br><br>class Overload(Action):<br>    def __init__(self, n): self.n = n<br>    def run(self, g, c): g.player(c.controller)[Tag.OVERLOAD_OWED] += amt(self.n, g, c)<br><br><br>class GainArmor(Action):<br>    def __init__(self, n): self.n = n<br>    def run(self, g, c):<br>        h = g.player(c.controller).hero<br>        h[Tag.ARMOR] += amt(self.n, g, c)<br><br><br>class AddToHand(Action):<br>    def __init__(self, card_id, n=1): self.card_id, self.n = card_id, n<br>    def run(self, g, c):<br>        for _ in range(amt(self.n, g, c)):<br>            g.add_to_hand(c.controller, self.card_id)<br><br><br>class Discover(Action):<br>    \"\"\"\u9009\u62e9\u578b\u8303\u5f0f\u3002\u6a21\u62df\/\u65e0\u4ea4\u4e92\u65f6\u7531 policy \u51b3\u5b9a\u3002\"\"\"<br>    def __init__(self, pool: list[str], count=3, then: Action | None = None):<br>        self.pool, self.count, self.then = pool, count, then<br>    def run(self, g, c):<br>        if not self.pool: return<br>        picks = g.rng.sample(self.pool, min(self.count, len(self.pool)))<br>        chosen = g.ask_choice(c.controller, picks, kind=\"DISCOVER\")<br>        act = self.then or AddToHand(chosen)<br>        if isinstance(act, AddToHand) and self.then is None:<br>            g.add_to_hand(c.controller, chosen)<br>        else:<br>            act.run(g, replace(c, stored={**c.stored, \"discovered\": chosen}))<br><br><br>class Progress(Action):<br>    \"\"\"\u72b6\u6001\u673a\u8303\u5f0f\u7684\u901a\u7528\u5b9e\u73b0\uff1a\u8ba1\u6570 \u2192 \u9608\u503c \u2192 \u5956\u52b1\/\u5347\u7ea7\"\"\"<br>    def run(self, g, c):<br>        e = c.source<br>        e[Tag.PROGRESS] += 1<br>        pd = e.defn.progression<br>        stage = e.progress_stage<br>        if stage &gt;= len(pd.thresholds): return<br>        if e[Tag.PROGRESS] &gt;= pd.thresholds[stage]:<br>            e[Tag.PROGRESS] = 0<br>            e.progress_stage += 1<br>            if stage &lt; len(pd.on_stage) and pd.on_stage[stage]:<br>                pd.on_stage[stage].run(g, c)<br>            if stage &lt; len(pd.upgrade_to) and pd.upgrade_to[stage]:<br>                g.transform(e, pd.upgrade_to[stage])<br>            elif e.progress_stage &gt;= len(pd.thresholds) and pd.complete_action:<br>                pd.complete_action.run(g, c)<br>                if e[Tag.ZONE] == Zone.SECRET:<br>                    g.zones.move(e, Zone.GRAVEYARD)<br>                    g.events.unregister_all(e)<br><br><br>class RevealSecret(Action):<br>    def run(self, g, c):<br>        s = c.source<br>        g.emit_now(Event.SECRET_REVEALED, entity=s)<br>        g.events.unregister_all(s)<br>        g.zones.move(s, Zone.GRAVEYARD)<br><br><br>class SpecialEffect(Action):<br>    REGISTRY: dict[str, Callable] = {}<br>    def __init__(self, name): self.name = name<br>    def run(self, g, c): SpecialEffect.REGISTRY[self.name](g, c)<br><br><br>def special(name):<br>    def deco(f):<br>        SpecialEffect.REGISTRY[name] = f<br>        return f<br>    return deco<br><br><br># =============================================================================<br># 5. \u5361\u724c\u5b9a\u4e49<br># =============================================================================<br><br><br>@dataclass(frozen=True)<br>class TriggerDef:<br>    on: Event<br>    action: Action<br>    condition: Condition | None = None<br>    zones: frozenset = frozenset({Zone.PLAY})<br>    once: bool = False<br>    source_only: bool = False<br><br><br>@dataclass(frozen=True)<br>class AuraDef:<br>    selector: Selector<br>    tag: Tag<br>    amount: int = 0<br>    condition: Condition | None = None<br>    include_self: bool = False<br><br><br>@dataclass(frozen=True)<br>class ProgressionDef:<br>    counter_event: Event<br>    thresholds: tuple<br>    condition: Condition | None = None<br>    zones: frozenset = frozenset({Zone.SECRET})<br>    on_stage: tuple = ()<br>    upgrade_to: tuple = ()<br>    complete_action: Action | None = None<br><br><br>@dataclass(frozen=True)<br>class CardDef:<br>    card_id: str<br>    name: str<br>    cost: int = 0<br>    card_type: CardType = CardType.MINION<br>    attack: int = 0<br>    health: int = 0<br>    race: str | None = None<br>    spell_school: str | None = None<br>    text: str = \"\"<br>    keywords: frozenset = frozenset()<br>    battlecry: Action | None = None<br>    combo: Action | None = None<br>    deathrattle: Action | None = None<br>    spell: Action | None = None<br>    triggers: tuple = ()<br>    auras: tuple = ()<br>    progression: ProgressionDef | None = None<br>    targeting: Selector | None = None<br>    spellpower: int = 0<br>    colossal_parts: tuple = ()<br>    magnetic: bool = False<br>    upgrade_to: str | None = None<br><br>    def base_tags(self) -&gt; dict:<br>        t = {<br>            Tag.CARD_ID: self.card_id,<br>            Tag.CARDTYPE: self.card_type,<br>            Tag.COST: self.cost,<br>            Tag.ATK: self.attack,<br>            Tag.HEALTH: self.health,<br>            Tag.CARDRACE: self.race,<br>            Tag.SPELL_SCHOOL: self.spell_school,<br>            Tag.SPELLPOWER: self.spellpower,<br>        }<br>        for kw in self.keywords:<br>            if kw in KEYWORD_TO_TAG:<br>                t[KEYWORD_TO_TAG[kw]] = 1<br>        return t<br><br><br>CARD_DB: dict[str, CardDef] = {}<br><br><br>def register(card: CardDef) -&gt; CardDef:<br>    CARD_DB[card.card_id] = card<br>    return card<br><br><br># =============================================================================<br># 6. \u5b9e\u4f53<br># =============================================================================<br><br><br>class Entity:<br>    __slots__ = (\"id\", \"game\", \"tags\", \"enchantments\", \"death_snapshot\",<br>                 \"progress_stage\", \"is_echo_copy\", \"death_prevented\",<br>                 \"ds_absorbed\", \"reborn_used\", \"magnetic_stack\")<br><br>    def __init__(self, eid: int, game: \"Game\", defn: CardDef, controller: int):<br>        self.id = eid<br>        self.game = game<br>        self.tags: dict = dict(defn.base_tags())<br>        self.tags[Tag.ENTITY_ID] = eid<br>        self.tags[Tag.CONTROLLER] = controller<br>        self.tags[Tag.OWNER] = controller<br>        self.tags[Tag.ZONE] = Zone.SETASIDE<br>        self.enchantments: list = []<br>        self.death_snapshot = None<br>        self.progress_stage = 0<br>        self.is_echo_copy = False<br>        self.death_prevented = False<br>        self.ds_absorbed = False<br>        self.reborn_used = False<br>        self.magnetic_stack: list = []<br><br>    # ---- tag ----<br>    def __getitem__(self, t): return self.tags.get(t, 0)<br><br>    def __setitem__(self, t, v):<br>        old = self.tags.get(t, 0)<br>        if old == v: return<br>        self.tags[t] = v<br>        self.game.record_change(self, t, old, v)<br><br>    def has(self, t) -&gt; bool: return bool(self.tags.get(t, 0))<br><br>    @property<br>    def defn(self) -&gt; CardDef: return CARD_DB[self.tags[Tag.CARD_ID]]<br><br>    @property<br>    def current_health(self) -&gt; int:<br>        return self[Tag.HEALTH] - self[Tag.DAMAGE]<br><br>    @property<br>    def is_minion(self) -&gt; bool: return self[Tag.CARDTYPE] == CardType.MINION<br><br>    @property<br>    def is_hero(self) -&gt; bool: return self[Tag.CARDTYPE] == CardType.HERO<br><br>    def __repr__(self):<br>        return (f\"&lt;{self.defn.name}#{self.id} {self[Tag.ATK]}\/{self.current_health}\"<br>                f\" z={Zone(self[Tag.ZONE]).name}&gt;\")<br><br><br>@dataclass<br>class DeathSnapshot:<br>    entity_id: int<br>    card_id: str<br>    controller: int<br>    position: int<br>    attack: int<br>    health: int<br>    race: str | None<br><br><br># =============================================================================<br># 7. \u533a\u57df\u7ba1\u7406<br># =============================================================================<br><br><br>class ZoneManager:<br>    CAP = {Zone.PLAY: 7, Zone.HAND: 10, Zone.SECRET: 5}<br><br>    def __init__(self, game): self.game = game; self._z = {}<br><br>    def get(self, pid: int, zone: Zone) -&gt; list:<br>        return self._z.setdefault((pid, zone), [])<br><br>    def move(self, e: Entity, to: Zone, position: int | None = None) -&gt; bool:<br>        src, pid = e[Tag.ZONE], e[Tag.CONTROLLER]<br>        dst = self.get(pid, to)<br>        cap = self.CAP.get(to)<br>        if cap is not None and len(dst) &gt;= cap:<br>            if to == Zone.HAND:<br>                self.game.emit(Event.CARD_BURNED, entity=e, player=pid)<br>                return self.move(e, Zone.GRAVEYARD)<br>            if to == Zone.PLAY:<br>                self.game.emit(Event.SUMMON_FAILED, entity=e)<br>                self._detach(e)<br>                return False<br>            return False<br><br>        if src != Zone.INVALID:<br>            old = self.get(pid, src)<br>            if e in old:<br>                old.remove(e)<br>                self._reindex(old)<br>        # \u79bb\u5f00\u6218\u573a\u65f6\u6e05\u9664\u4f24\u5bb3\u4e0e\u6b7b\u4ea1\u6807\u8bb0<br>        if src == Zone.PLAY and to != Zone.PLAY:<br>            e.tags[Tag.DAMAGE] = 0<br>            e.tags[Tag.TO_BE_DESTROYED] = 0<br>            for ench in list(e.enchantments):<br>                self.game.remove_enchantment(ench)<br><br>        if position is None or position &gt; len(dst):<br>            dst.append(e)<br>        else:<br>            dst.insert(max(0, position), e)<br>        self._reindex(dst)<br>        e[Tag.ZONE] = to<br>        return True<br><br>    def _detach(self, e):<br>        pid, src = e[Tag.CONTROLLER], e[Tag.ZONE]<br>        lst = self.get(pid, src)<br>        if e in lst:<br>            lst.remove(e); self._reindex(lst)<br>        e.tags[Tag.ZONE] = Zone.INVALID<br><br>    @staticmethod<br>    def _reindex(lst):<br>        for i, x in enumerate(lst):<br>            x.tags[Tag.ZONE_POSITION] = i + 1<br><br><br># =============================================================================<br># 8. \u4e8b\u4ef6\u603b\u7ebf<br># =============================================================================<br><br><br>@dataclass<br>class EventCtx:<br>    kind: Event<br>    entity: Entity | None = None<br>    source: Entity | None = None<br>    target: Entity | None = None<br>    player: int | None = None<br>    amount: int = 0<br>    fatal: bool = False<br>    target_health_before: int = 0<br>    prevented: bool = False<br>    mutable_amount: int = 0<br>    snapshot: DeathSnapshot | None = None<br><br><br>MAX_TRIGGER_DEPTH = 128<br><br><br>class EventBus:<br>    def __init__(self, game):<br>        self.game = game<br>        self.subs: dict = defaultdict(list)<br>        self.queue: deque = deque()<br>        self.depth = 0<br><br>    def register(self, owner: Entity, td: TriggerDef):<br>        self.subs[td.on].append((owner, td))<br><br>    def register_card(self, e: Entity):<br>        for td in e.defn.triggers:<br>            self.register(e, td)<br>        if e.defn.deathrattle:<br>            self.register(e, TriggerDef(<br>                on=Event.DEATH, action=e.defn.deathrattle, source_only=True,<br>                zones=frozenset({Zone.PLAY, Zone.GRAVEYARD})))<br>        pd = e.defn.progression<br>        if pd:<br>            self.register(e, TriggerDef(on=pd.counter_event, action=Progress(),<br>                                        condition=pd.condition, zones=pd.zones))<br><br>    def unregister_all(self, e: Entity):<br>        for k in list(self.subs):<br>            self.subs[k] = [(o, t) for (o, t) in self.subs[k] if o is not e]<br><br>    def collect(self, ctx: EventCtx) -&gt; list:<br>        out = []<br>        for owner, td in self.subs.get(ctx.kind, ()):<br>            if ctx.kind != Event.DEATH and owner[Tag.ZONE] not in td.zones:<br>                continue<br>            if td.source_only and ctx.entity is not owner: continue<br>            if owner.has(Tag.SILENCED): continue<br>            if td.once and owner.has(Tag.TRIGGERED_ONCE): continue<br>            c = Ctx(source=owner, controller=owner[Tag.CONTROLLER], event=ctx)<br>            if td.condition and not td.condition.eval(self.game, owner, c):<br>                continue<br>            out.append((owner, td))<br>        out.sort(key=lambda x: x[0][Tag.ENTITY_ID])          # \u2605 \u94c1\u5f8b\u4e00<br>        return out<br><br>    def emit(self, ctx: EventCtx):<br>        self.queue.extend((o, t, ctx) for (o, t) in self.collect(ctx))<br><br>    def emit_now(self, ctx: EventCtx):<br>        \"\"\"\u7acb\u5373\u6267\u884c\uff0c\u4e0d\u5165\u961f\uff08\u7528\u4e8e PROPOSED_ATTACK \/ PROPOSED_DEATH \u7b49\uff09\"\"\"<br>        for owner, td in self.collect(ctx):<br>            if td.once: owner[Tag.TRIGGERED_ONCE] = 1<br>            c = Ctx(source=owner, controller=owner[Tag.CONTROLLER], event=ctx)<br>            td.action.run(self.game, c)<br><br>    def resolve(self):<br>        self.depth += 1<br>        if self.depth &gt; MAX_TRIGGER_DEPTH:<br>            self.game.force_draw(\"INFINITE_LOOP\")<br>            self.queue.clear(); self.depth -= 1; return<br>        while self.queue:<br>            owner, td, ctx = self.queue.popleft()<br>            if td.on != Event.DEATH and owner[Tag.ZONE] not in td.zones:<br>                continue                                      # \u2605 \u4e8c\u6b21\u6821\u9a8c<br>            if td.once:<br>                if owner.has(Tag.TRIGGERED_ONCE): continue<br>                owner[Tag.TRIGGERED_ONCE] = 1<br>            c = Ctx(source=owner, controller=owner[Tag.CONTROLLER], event=ctx)<br>            td.action.run(self.game, c)<br>        self.depth -= 1<br><br><br># =============================================================================<br># 9. \u5149\u73af<br># =============================================================================<br><br><br>class AuraBus:<br>    def __init__(self, game):<br>        self.game = game<br>        self.dirty = True<br>        self.deltas: dict = {}<br><br>    def mark_dirty(self): self.dirty = True<br><br>    def recompute(self):<br>        if not self.dirty: return<br>        self.dirty = False<br>        old, new = self.deltas, defaultdict(lambda: defaultdict(int))<br>        srcs = []<br>        for pid in (0, 1):<br>            for e in self.game.zones.get(pid, Zone.PLAY):<br>                if e.has(Tag.SILENCED): continue<br>                for ad in e.defn.auras: srcs.append((e, ad))<br>        srcs.sort(key=lambda x: x[0][Tag.ENTITY_ID])<br>        for src, ad in srcs:<br>            c = Ctx(source=src, controller=src[Tag.CONTROLLER])<br>            if ad.condition and not ad.condition.eval(self.game, src, c):<br>                continue<br>            for tgt in ad.selector.eval(self.game, c):<br>                if tgt is src and not ad.include_self: continue<br>                new[tgt.id][ad.tag] += ad.amount<br>        self.deltas = {k: dict(v) for k, v in new.items()}<br>        self._apply(old, self.deltas)<br><br>    def _apply(self, old, new):<br>        for eid in set(old) | set(new):<br>            e = self.game.entities.get(eid)<br>            if e is None: continue<br>            o, n = old.get(eid, {}), new.get(eid, {})<br>            for tag in set(o) | set(n):<br>                d = n.get(tag, 0) - o.get(tag, 0)<br>                if d:<br>                    e.tags[tag] = e.tags.get(tag, 0) + d<br>                    self.game.record_change(e, tag, e.tags[tag] - d, e.tags[tag])<br><br><br># =============================================================================<br># 10. \u73a9\u5bb6<br># =============================================================================<br><br><br>@dataclass<br>class Stats:<br>    total: dict = field(default_factory=lambda: defaultdict(int))<br>    turn: dict = field(default_factory=lambda: defaultdict(int))<br>    last: dict = field(default_factory=lambda: defaultdict(int))<br><br>    def bump(self, key, n=1):<br>        self.total[key] += n<br>        self.turn[key] += n<br><br>    def rotate(self):<br>        self.last = defaultdict(int, self.turn)<br>        self.turn = defaultdict(int)<br><br><br>class Player:<br>    def __init__(self, game, pid, klass=\"NEUTRAL\"):<br>        self.game, self.pid, self.klass = game, pid, klass<br>        self.tags = {Tag.MANA_CRYSTALS: 0, Tag.MANA_AVAILABLE: 0,<br>                     Tag.OVERLOAD_OWED: 0, Tag.OVERLOAD_LOCKED: 0,<br>                     Tag.FATIGUE: 0, Tag.COMBO_ACTIVE: 0}<br>        self.hero: Entity | None = None<br>        self.hero_power: Entity | None = None<br>        self.weapon: Entity | None = None<br>        self.stats = Stats()<br><br>    def __getitem__(self, t): return self.tags.get(t, 0)<br>    def __setitem__(self, t, v): self.tags[t] = v<br><br><br># =============================================================================<br># 11. \u6e38\u620f\u4e3b\u4f53<br># =============================================================================<br><br><br>class GameOver(Exception):<br>    def __init__(self, winner): self.winner = winner<br><br><br>class Game:<br>    def __init__(self, seed=0, verbose=False, policy=None):<br>        self.rng = GameRNG(seed)<br>        self.verbose = verbose<br>        self.entities: dict[int, Entity] = {}<br>        self._next_id = itertools.count(1)<br>        self.zones = ZoneManager(self)<br>        self.events = EventBus(self)<br>        self.auras = AuraBus(self)<br>        self.players = [Player(self, 0), Player(self, 1)]<br>        self.turn = 0<br>        self.current = 0<br>        self.history: list = []<br>        self.log_lines: list[str] = []<br>        self.over = False<br>        self.winner = None<br>        self.policy = policy or (lambda pid, opts, kind: opts[0])<br>        self.is_simulation = False<br><br>    # ------------------------------------------------------------ \u57fa\u7840\u8bbe\u65bd<br>    def player(self, pid) -&gt; Player: return self.players[pid]<br><br>    def record_change(self, e, tag, old, new):<br>        self.history.append((e.id, int(tag), old, new))<br><br>    def log(self, msg):<br>        self.log_lines.append(msg)<br>        if self.verbose: print(msg)<br><br>    def ask_choice(self, pid, options, kind=\"DISCOVER\"):<br>        return self.policy(pid, options, kind)<br><br>    def create_entity(self, card_id, controller, zone=Zone.SETASIDE) -&gt; Entity:<br>        defn = CARD_DB[card_id]<br>        e = Entity(next(self._next_id), self, defn, controller)<br>        self.entities[e.id] = e<br>        self.zones.move(e, zone)<br>        return e<br><br>    def remove_entity(self, e):<br>        self.events.unregister_all(e)<br>        self.zones._detach(e)<br>        self.entities.pop(e.id, None)<br><br>    def all_entities_of(self, pid):<br>        for z in Zone:<br>            yield from self.zones.get(pid, z)<br><br>    def neighbors(self, e) -&gt; list:<br>        board = self.zones.get(e[Tag.CONTROLLER], Zone.PLAY)<br>        if e not in board: return []<br>        i = board.index(e)<br>        return [x for x in (board[i-1] if i &gt; 0 else None,<br>                            board[i+1] if i+1 &lt; len(board) else None) if x]<br><br>    # ------------------------------------------------------------ \u4e8b\u4ef6<br>    def emit(self, kind, **kw):<br>        self.events.emit(EventCtx(kind=kind, **kw))<br><br>    def emit_now(self, kind, **kw):<br>        ctx = EventCtx(kind=kind, **kw)<br>        self.events.emit_now(ctx)<br>        return ctx<br><br>    # ------------------------------------------------------------ \u9644\u9b54<br>    def apply_enchantment(self, host, atk=0, hp=0, keywords=(), cost=0):<br>        ench = Entity(next(self._next_id), self,<br>                      CARD_DB[\"ENCH_GENERIC\"], host[Tag.CONTROLLER])<br>        self.entities[ench.id] = ench<br>        ench.tags[Tag.ATTACHED] = host.id<br>        ench.tags[\"delta\"] = {}<br>        deltas = {}<br>        if atk: deltas[Tag.ATK] = atk<br>        if hp:  deltas[Tag.HEALTH] = hp<br>        if cost: deltas[Tag.COST] = cost<br>        ench.tags[\"delta\"] = deltas<br>        ench.tags[\"kws\"] = tuple(keywords)<br>        for t, d in deltas.items():<br>            host[t] = host[t] + d<br>        for k in keywords:<br>            host[KEYWORD_TO_TAG[k]] = 1<br>        host.enchantments.append(ench)<br>        return ench<br><br>    def remove_enchantment(self, ench):<br>        host = self.entities.get(ench.tags.get(Tag.ATTACHED))<br>        if host is None: return<br>        for t, d in ench.tags.get(\"delta\", {}).items():<br>            host[t] = host[t] - d<br>        for k in ench.tags.get(\"kws\", ()):<br>            host[KEYWORD_TO_TAG[k]] = 0<br>        if ench in host.enchantments: host.enchantments.remove(ench)<br>        self.entities.pop(ench.id, None)<br><br>    def silence(self, e):<br>        if not e.is_minion: return<br>        for ench in list(e.enchantments): self.remove_enchantment(ench)<br>        self.events.unregister_all(e)<br>        for t in SILENCEABLE_TAGS: e.tags.pop(t, None)<br>        base = e.defn.base_tags()<br>        e[Tag.HEALTH] = base[Tag.HEALTH]<br>        e[Tag.ATK] = base[Tag.ATK]<br>        e[Tag.SILENCED] = 1<br>        self.auras.mark_dirty(); self.auras.recompute()<br>        self.log(f\"  SILENCE {e}\")<br><br>    def transform(self, e, new_card_id):<br>        keep = {t: e.tags[t] for t in TRANSFORM_PRESERVED if t in e.tags}<br>        for ench in list(e.enchantments): self.remove_enchantment(ench)<br>        e.enchantments.clear()<br>        self.events.unregister_all(e)<br>        e.tags.clear()<br>        e.tags.update(CARD_DB[new_card_id].base_tags())<br>        e.tags.update(keep)<br>        e.progress_stage = 0<br>        self.events.register_card(e)<br>        self.auras.mark_dirty()<br><br>    # ------------------------------------------------------------ \u6570\u503c<br>    def spellpower(self, pid) -&gt; int:<br>        return sum(e[Tag.SPELLPOWER] for e in self.zones.get(pid, Zone.PLAY))<br><br>    def compute_cost(self, card) -&gt; int:<br>        base = card[Tag.COST] - card[Tag.PREPARE_DISCOUNT]<br>        return max(0, base)<br><br>    # ------------------------------------------------------------ \u4f24\u5bb3\/\u6cbb\u7597<br>    def damage(self, target, amount, source=None) -&gt; int:<br>        if amount &lt;= 0 or not in_play(target): return 0<br>        if target.has(Tag.IMMUNE): return 0<br><br>        ctx = self.emit_now(Event.PRE_DAMAGE, target=target, source=source,<br>                            amount=amount, mutable_amount=amount)<br>        amount = max(0, ctx.mutable_amount)<br>        if amount == 0: return 0<br><br>        if target.has(Tag.DIVINE_SHIELD):<br>            target[Tag.DIVINE_SHIELD] = 0<br>            target.ds_absorbed = True<br>            self.log(f\"  DIVINE SHIELD absorbs on {target.defn.name}\")<br>            return 0<br><br>        before = target.current_health<br>        if target.is_hero and target[Tag.ARMOR] &gt; 0:<br>            absorbed = min(target[Tag.ARMOR], amount)<br>            target[Tag.ARMOR] -= absorbed<br>            amount -= absorbed<br>            if amount == 0: return 0<br><br>        target[Tag.DAMAGE] += amount<br>        fatal = target.current_health &lt;= 0<br>        self.log(f\"  DAMAGE {amount} -&gt; {target.defn.name} \"<br>                 f\"({target.current_health} left)\")<br>        self.emit(Event.DAMAGE, entity=target, target=target, source=source,<br>                  amount=amount, fatal=fatal, target_health_before=before)<br><br>        if source is not None and source.has(Tag.LIFESTEAL):<br>            self.heal(self.player(source[Tag.CONTROLLER]).hero, amount)<br><br>        # \u5267\u6bd2<br>        if (source is not None and source.has(Tag.POISONOUS)<br>                and target.is_minion and amount &gt; 0):<br>            target[Tag.TO_BE_DESTROYED] = 1<br>        return amount<br><br>    def heal(self, target, amount) -&gt; int:<br>        if amount &lt;= 0: return 0<br>        healed = min(amount, target[Tag.DAMAGE])<br>        if healed:<br>            target[Tag.DAMAGE] -= healed<br>            self.emit(Event.HEAL, entity=target, target=target, amount=healed)<br>        return healed<br><br>    # ------------------------------------------------------------ \u53ec\u5524<br>    def summon(self, card_id, controller, position=None) -&gt; Entity | None:<br>        e = self.create_entity(card_id, controller, Zone.SETASIDE)<br>        self.emit_now(Event.PRE_SUMMON, entity=e)<br>        if not self.zones.move(e, Zone.PLAY, position):<br>            self.log(f\"  SUMMON FAILED (board full) {CARD_DB[card_id].name}\")<br>            self.entities.pop(e.id, None)<br>            return None<br>        e[Tag.EXHAUSTED] = 0 if e.has(Tag.CHARGE) else 1<br>        self.events.register_card(e)<br>        self.auras.mark_dirty(); self.auras.recompute()      # \u2605 \u5149\u73af\u5148\u4e8e\u4e8b\u4ef6<br>        self.log(f\"  SUMMON {e.defn.name} (pos {e[Tag.ZONE_POSITION]})\")<br>        # \u5de8\u578b<br>        for part in e.defn.colossal_parts:<br>            self.summon(part, controller, e[Tag.ZONE_POSITION])<br>        self.emit(Event.SUMMON, entity=e)<br>        return e<br><br>    def add_to_hand(self, pid, card_id) -&gt; Entity | None:<br>        e = self.create_entity(card_id, pid, Zone.SETASIDE)<br>        if not self.zones.move(e, Zone.HAND):<br>            return None<br>        e[Tag.DRAWN_ON_TURN] = self.turn<br>        return e<br><br>    # ------------------------------------------------------------ \u62bd\u724c<br>    def draw(self, pid, n=1):<br>        for _ in range(n):<br>            deck = self.zones.get(pid, Zone.DECK)<br>            if not deck:<br>                p = self.player(pid)<br>                p[Tag.FATIGUE] += 1<br>                self.log(f\"  FATIGUE {p[Tag.FATIGUE]} on P{pid}\")<br>                self.damage(p.hero, p[Tag.FATIGUE], source=None)<br>                continue<br>            card = deck.pop()<br>            self.zones._reindex(deck)<br>            hand = self.zones.get(pid, Zone.HAND)<br>            if len(hand) &gt;= ZoneManager.CAP[Zone.HAND]:<br>                self.zones.move(card, Zone.GRAVEYARD)<br>                self.log(f\"  BURN {card.defn.name}\")<br>                self.emit(Event.CARD_BURNED, entity=card, player=pid)<br>            else:<br>                self.zones.move(card, Zone.HAND)<br>                card[Tag.DRAWN_ON_TURN] = self.turn<br>                self.emit(Event.DRAW, entity=card, player=pid)<br><br>    # ------------------------------------------------------------ \u51fa\u724c<br>    def legal_targets(self, card) -&gt; list:<br>        if card.defn.targeting is None: return []<br>        c = Ctx(source=card, controller=card[Tag.CONTROLLER])<br>        out = []<br>        for t in card.defn.targeting.eval(self, c):<br>            if t.has(Tag.ELUSIVE) or t.has(Tag.IMMUNE): continue<br>            if t.has(Tag.STEALTH) and t[Tag.CONTROLLER] != card[Tag.CONTROLLER]:<br>                continue<br>            out.append(t)<br>        return out<br><br>    def play_card(self, card, target=None, position=None, choose=0):<br>        pid = card[Tag.CONTROLLER]<br>        p = self.player(pid)<br>        cost = self.compute_cost(card)<br>        if cost &gt; p[Tag.MANA_AVAILABLE]:<br>            raise ValueError(f\"not enough mana for {card.defn.name}\")<br>        if card[Tag.LOCKED_UNTIL_TURN] &gt; self.turn:<br>            raise ValueError(\"card is prepared and locked this turn\")<br><br>        p[Tag.MANA_AVAILABLE] -= cost<br>        combo = bool(p[Tag.COMBO_ACTIVE])<br>        finale = (p[Tag.MANA_AVAILABLE] == 0)                 # \u2605 \u7ec8\u66f2\u5feb\u7167<br>        self.log(f\"P{pid} PLAY {card.defn.name} (cost {cost})\"<br>                 + (f\" -&gt; {target.defn.name}\" if target else \"\"))<br><br>        self.zones.move(card, Zone.SETASIDE)<br>        self.emit(Event.PLAY_CARD, entity=card, player=pid, target=target)<br>        p.stats.bump(\"cards_played\")<br>        if card[Tag.CARDRACE]: p.stats.bump(f\"race_{card[Tag.CARDRACE]}\")<br>        if card[Tag.SPELL_SCHOOL]: p.stats.bump(f\"school_{card[Tag.SPELL_SCHOOL]}\")<br><br>        ctype = card[Tag.CARDTYPE]<br>        ctx = Ctx(source=card, controller=pid, target=target,<br>                  choose=choose, finale=finale)<br><br>        if ctype == CardType.MINION:<br>            if self.zones.move(card, Zone.PLAY, position):<br>                card[Tag.EXHAUSTED] = 0 if card.has(Tag.CHARGE) else 1<br>                self.events.register_card(card)<br>                self.auras.mark_dirty(); self.auras.recompute()<br>                for part in card.defn.colossal_parts:<br>                    self.summon(part, pid, card[Tag.ZONE_POSITION])<br>                self.emit(Event.SUMMON, entity=card)<br>                self.emit(Event.MINION_PLAYED, entity=card, player=pid)<br>                script = card.defn.combo if (combo and card.defn.combo) \\<br>                    else card.defn.battlecry<br>                if script:<br>                    script.run(self, ctx)<br>        elif ctype == CardType.SPELL:<br>            self.emit(Event.SPELL_CAST, entity=card, player=pid, target=target)<br>            if card.defn.progression:            # \u4efb\u52a1<br>                self.zones.move(card, Zone.SECRET)<br>                card[Tag.QUEST_TOTAL] = card.defn.progression.thresholds[0]<br>                self.events.register_card(card)<br>            elif \"SECRET\" in card.defn.keywords:<br>                self.zones.move(card, Zone.SECRET)<br>                self.events.register_card(card)<br>            else:<br>                if card.defn.spell: card.defn.spell.run(self, ctx)<br>                self.zones.move(card, Zone.GRAVEYARD)<br>                self.emit(Event.AFTER_SPELL, entity=card, player=pid)<br><br>        p[Tag.COMBO_ACTIVE] = 1<br>        self.events.resolve()<br>        self.process_deaths()<br><br>    def trade_card(self, card) -&gt; bool:<br>        pid = card[Tag.CONTROLLER]<br>        p = self.player(pid)<br>        if not card.has(Tag.TRADEABLE) or p[Tag.MANA_AVAILABLE] &lt; 1:<br>            return False<br>        p[Tag.MANA_AVAILABLE] -= 1<br>        self.zones.move(card, Zone.DECK)<br>        deck = self.zones.get(pid, Zone.DECK)<br>        self.rng.shuffle(deck); self.zones._reindex(deck)<br>        self.log(f\"P{pid} TRADE {card.defn.name}\")<br>        self.emit(Event.CARD_TRADED, entity=card, player=pid)<br>        self.draw(pid, 1)<br>        self.events.resolve()<br>        return True<br><br>    def prepare_card(self, card, mana) -&gt; bool:<br>        pid = card[Tag.CONTROLLER]<br>        p = self.player(pid)<br>        if not card.has(Tag.PREPARE) or card.has(Tag.PREPARED): return False<br>        cur = self.compute_cost(card)<br>        mana = min(mana, p[Tag.MANA_AVAILABLE], max(0, cur - 1))<br>        if mana &lt; 1: return False<br>        p[Tag.MANA_AVAILABLE] -= mana<br>        card[Tag.PREPARE_DISCOUNT] = mana + 1<br>        card[Tag.PREPARED] = 1<br>        card[Tag.LOCKED_UNTIL_TURN] = self.turn + 1<br>        self.log(f\"P{pid} PREPARE {card.defn.name} (-{mana + 1})\")<br>        self.emit(Event.CARD_PREPARED, entity=card, player=pid, amount=mana)<br>        self.events.resolve()<br>        return True<br><br>    # ------------------------------------------------------------ \u653b\u51fb<br>    def can_attack(self, e) -&gt; bool:<br>        if not in_play(e): return False<br>        if e.has(Tag.FROZEN): return False<br>        if e[Tag.ATK] &lt;= 0: return False<br>        max_atk = 2 if e.has(Tag.WINDFURY) else 1<br>        if e[Tag.NUM_ATTACKS] &gt;= max_atk: return False<br>        if e[Tag.EXHAUSTED] and not (e.has(Tag.CHARGE) or e.has(Tag.RUSH)):<br>            return False<br>        return True<br><br>    def legal_attack_targets(self, attacker) -&gt; list:<br>        opp = 1 - attacker[Tag.CONTROLLER]<br>        cands = list(self.zones.get(opp, Zone.PLAY)) + [self.player(opp).hero]<br>        cands = [c for c in cands<br>                 if not c.has(Tag.STEALTH) and not c.has(Tag.IMMUNE)]<br>        if (attacker.has(Tag.RUSH) and not attacker.has(Tag.CHARGE)<br>                and attacker[Tag.NUM_TURNS_IN_PLAY] == 0):<br>            cands = [c for c in cands if not c.is_hero]<br>        taunts = [c for c in cands if c.has(Tag.TAUNT)]<br>        return taunts if taunts else cands<br><br>    def attack(self, attacker, defender):<br>        if not self.can_attack(attacker):<br>            raise ValueError(f\"{attacker} cannot attack\")<br>        if defender not in self.legal_attack_targets(attacker):<br>            raise ValueError(f\"illegal target {defender}\")<br><br>        ctx = self.emit_now(Event.PROPOSED_ATTACK, source=attacker, target=defender)<br>        defender = ctx.target<br>        if ctx.prevented: return<br>        if attacker.has(Tag.STEALTH): attacker[Tag.STEALTH] = 0<br><br>        self.log(f\"P{attacker[Tag.CONTROLLER]} ATTACK \"<br>                 f\"{attacker.defn.name} -&gt; {defender.defn.name}\")<br>        self.emit(Event.ATTACK, source=attacker, target=defender)<br>        self.events.resolve()<br>        if not in_play(attacker) or attacker.current_health &lt;= 0:<br>            self.process_deaths(); return<br>        if not in_play(defender) or defender.current_health &lt;= 0:<br>            self.process_deaths(); return<br><br>        atk_p, def_p = attacker[Tag.ATK], defender[Tag.ATK]   # \u2605 \u5feb\u7167<br>        attacker.ds_absorbed = defender.ds_absorbed = False<br>        if def_p &gt; 0: self.damage(attacker, def_p, source=defender)<br>        if atk_p &gt; 0: self.damage(defender, atk_p, source=attacker)<br><br>        attacker[Tag.NUM_ATTACKS] += 1<br>        self.emit(Event.AFTER_ATTACK, source=attacker, target=defender)<br>        self.events.resolve()<br>        self.process_deaths()<br><br>    # ------------------------------------------------------------ \u6b7b\u4ea1\u7ed3\u7b97<br>    def process_deaths(self):<br>        rounds = 0<br>        while True:<br>            rounds += 1<br>            if rounds &gt; 64:<br>                self.force_draw(\"DEATH_LOOP\"); return<br>            dying = []<br>            for pid in (0, 1):<br>                for e in self.zones.get(pid, Zone.PLAY):<br>                    if e.is_minion and (e.current_health &lt;= 0<br>                                        or e.has(Tag.TO_BE_DESTROYED)):<br>                        dying.append(e)<br>                h = self.player(pid).hero<br>                if h and h.current_health &lt;= 0:<br>                    self.over = True<br>            if self.over:<br>                self._finish(); return<br>            if not dying: return<br><br>            dying.sort(key=lambda e: e[Tag.ENTITY_ID])        # \u2605 \u94c1\u5f8b\u4e00<br>            for e in dying:<br>                e.death_snapshot = DeathSnapshot(<br>                    e.id, e[Tag.CARD_ID], e[Tag.CONTROLLER],<br>                    e[Tag.ZONE_POSITION], e[Tag.ATK], e[Tag.HEALTH],<br>                    e[Tag.CARDRACE])<br>                e.death_prevented = False<br>            for e in dying:<br>                self.emit_now(Event.PROPOSED_DEATH, entity=e)<br>            dying = [e for e in dying if not e.death_prevented]<br><br>            snaps = {e.id: e.death_snapshot for e in dying}<br>            for e in dying:                                   # \u2605 \u5148\u5168\u90e8\u5165\u5893<br>                self.log(f\"  DEATH {e.defn.name}\")<br>                self.zones.move(e, Zone.GRAVEYARD)<br>            self.auras.mark_dirty(); self.auras.recompute()<br><br>            for e in dying:                                   # \u2605 \u518d\u89e6\u53d1\u4ea1\u8bed<br>                self.emit(Event.DEATH, entity=e, snapshot=snaps[e.id],<br>                          player=e[Tag.CONTROLLER])<br>            self.events.resolve()<br><br>            for e in dying:                                   # \u2605 \u590d\u751f\u5728\u4ea1\u8bed\u540e<br>                if e.has(Tag.REBORN) and not e.reborn_used:<br>                    e.reborn_used = True<br>                    s = snaps[e.id]<br>                    n = self.summon(s.card_id, s.controller, s.position - 1)<br>                    if n:<br>                        n[Tag.HEALTH] = 1<br>                        n[Tag.REBORN] = 0<br><br>            for e in dying:<br>                self.emit(Event.AFTER_DEATH, entity=e, snapshot=snaps[e.id])<br>            self.events.resolve()<br><br>    def force_draw(self, reason):<br>        self.log(f\"!! FORCED DRAW: {reason}\")<br>        self.over = True; self.winner = None<br>        raise GameOver(None)<br><br>    def _finish(self):<br>        h0 = self.player(0).hero.current_health<br>        h1 = self.player(1).hero.current_health<br>        if h0 &lt;= 0 and h1 &lt;= 0: self.winner = None<br>        elif h0 &lt;= 0: self.winner = 1<br>        else: self.winner = 0<br>        self.log(f\"== GAME OVER, winner = {self.winner}\")<br>        raise GameOver(self.winner)<br><br>    # ------------------------------------------------------------ \u56de\u5408<br>    def setup(self, deck0: list[str], deck1: list[str],<br>              hero_ids=(\"HERO_MAGE\", \"HERO_MAGE\")):<br>        for pid, (deck, hid) in enumerate(zip((deck0, deck1), hero_ids)):<br>            p = self.player(pid)<br>            p.hero = self.create_entity(hid, pid, Zone.HERO)<br>            p.hero.tags[Tag.HEALTH] = 30<br>            cards = [self.create_entity(cid, pid, Zone.DECK) for cid in deck]<br>            lst = self.zones.get(pid, Zone.DECK)<br>            self.rng.shuffle(lst); self.zones._reindex(lst)<br>        self.draw(0, 3); self.draw(1, 4)<br>        self.emit(Event.GAME_START); self.events.resolve()<br><br>    def begin_turn(self):<br>        p = self.player(self.current)<br>        self.turn += 1<br>        p[Tag.OVERLOAD_LOCKED] = p[Tag.OVERLOAD_OWED]<br>        p[Tag.OVERLOAD_OWED] = 0<br>        p[Tag.MANA_CRYSTALS] = min(10, p[Tag.MANA_CRYSTALS] + 1)<br>        p[Tag.MANA_AVAILABLE] = p[Tag.MANA_CRYSTALS] - p[Tag.OVERLOAD_LOCKED]<br>        p[Tag.COMBO_ACTIVE] = 0<br>        for e in self.zones.get(self.current, Zone.PLAY):<br>            e[Tag.EXHAUSTED] = 0<br>            if e[Tag.NUM_ATTACKS] == 0 and e.has(Tag.FROZEN):<br>                e[Tag.FROZEN] = 0<br>            e[Tag.NUM_ATTACKS] = 0<br>            e[Tag.NUM_TURNS_IN_PLAY] += 1<br>        p.hero[Tag.NUM_ATTACKS] = 0<br>        self.log(f\"--- TURN {self.turn} (P{self.current}, \"<br>                 f\"{p[Tag.MANA_AVAILABLE]}\/{p[Tag.MANA_CRYSTALS]} mana)\")<br>        self.emit(Event.TURN_START, player=self.current)<br>        self.events.resolve(); self.process_deaths()<br>        self.draw(self.current, 1)<br>        self.events.resolve(); self.process_deaths()<br><br>    def end_turn(self):<br>        self.emit(Event.TURN_END, player=self.current)<br>        self.events.resolve(); self.process_deaths()<br>        for e in list(self.zones.get(self.current, Zone.HAND)):<br>            if e.has(Tag.TEMPORARY) or e.is_echo_copy:<br>                self.zones.move(e, Zone.GRAVEYARD)<br>        self.player(self.current).stats.rotate()<br>        self.current = 1 - self.current<br>        self.begin_turn()<br><br><br># =============================================================================<br># 12. \u5361\u724c\u6570\u636e<br># =============================================================================<br><br><br>register(CardDef(\"ENCH_GENERIC\", \"\u9644\u9b54\", card_type=CardType.ENCHANTMENT))<br>register(CardDef(\"HERO_MAGE\", \"\u5409\u5b89\u5a1c\", card_type=CardType.HERO, health=30))<br><br># ---- \u57fa\u7840\u968f\u4ece ----<br>register(CardDef(\"WISP\", \"\u5c0f\u7cbe\u7075\", cost=0, attack=1, health=1))<br>register(CardDef(\"RIVER_CROC\", \"\u6de1\u6c34\u9cc4\", cost=2, attack=2, health=3, race=\"BEAST\"))<br>register(CardDef(\"MURLOC\", \"\u9c7c\u4eba\u730e\u6f6e\u8005\", cost=2, attack=2, health=1, race=\"MURLOC\"))<br>register(CardDef(\"BOULDERFIST\", \"\u77f3\u62f3\u98df\u4eba\u9b54\", cost=6, attack=6, health=7))<br>register(CardDef(\"SEN_JIN\", \"\u68ee\u91d1\u6301\u76fe\u536b\u58eb\", cost=4, attack=3, health=5,<br>                 keywords=frozenset({\"TAUNT\"})))<br>register(CardDef(\"ARGENT_SQUIRE\", \"\u94f6\u8272\u4f8d\u4ece\", cost=1, attack=1, health=1,<br>                 keywords=frozenset({\"DIVINE_SHIELD\"})))<br>register(CardDef(\"WOLFRIDER\", \"\u72fc\u9a91\u5175\", cost=3, attack=3, health=1,<br>                 keywords=frozenset({\"CHARGE\"})))<br>register(CardDef(\"SPIDER_TANK\", \"\u8718\u86db\u5766\u514b\", cost=3, attack=3, health=4,<br>                 race=\"MECHANICAL\"))<br>register(CardDef(\"EMPEROR_COBRA\", \"\u5e1d\u738b\u773c\u955c\u86c7\", cost=3, attack=2, health=3,<br>                 race=\"BEAST\", keywords=frozenset({\"POISONOUS\"})))<br>register(CardDef(\"BIG_GUY\", \"\u5de8\u517d\", cost=8, attack=9, health=9))<br><br># ---- \u4ea1\u8bed ----<br>register(CardDef(\"LOOT_HOARDER\", \"\u85cf\u5b9d\u5996\u7cbe\", cost=2, attack=2, health=1,<br>                 deathrattle=Draw(1), text=\"\u4ea1\u8bed\uff1a\u62bd\u4e00\u5f20\u724c\"))<br>register(CardDef(\"IMP_TOKEN\", \"\u5c0f\u9b3c\", cost=1, attack=1, health=1, race=\"DEMON\"))<br>register(CardDef(\"IMP_MASTER\", \"\u5c0f\u9b3c\u519b\u5b98\", cost=3, attack=1, health=5,<br>                 deathrattle=Summon(\"IMP_TOKEN\", at_death_position=True)))<br>register(CardDef(\"UNSTABLE_GHOUL\", \"\u4e0d\u7a33\u5b9a\u7684\u98df\u5c38\u9b3c\", cost=2, attack=1, health=3,<br>                 keywords=frozenset({\"TAUNT\"}),<br>                 deathrattle=Damage(Sel.ALL_MINIONS, 1),<br>                 text=\"\u5632\u8bbd\u3002\u4ea1\u8bed\uff1a\u5bf9\u6240\u6709\u968f\u4ece\u9020\u6210 1 \u70b9\u4f24\u5bb3\"))<br><br># ---- \u5149\u73af ----<br>register(CardDef(\"STORMWIND_CHAMPION\", \"\u66b4\u98ce\u57ce\u52c7\u58eb\", cost=7, attack=6, health=6,<br>                 auras=(AuraDef(Sel.FRIENDLY_MINIONS, Tag.ATK, 1),<br>                        AuraDef(Sel.FRIENDLY_MINIONS, Tag.HEALTH, 1)),<br>                 text=\"\u4f60\u7684\u5176\u4ed6\u968f\u4ece\u83b7\u5f97 +1\/+1\"))<br>register(CardDef(\"SUNFURY\", \"\u7834\u788e\u6b8b\u9633\u796d\u53f8\", cost=3, attack=3, health=2,<br>                 battlecry=GiveKeyword(Sel.TARGET, \"TAUNT\"),<br>                 targeting=Sel.FRIENDLY_MINIONS))<br>register(CardDef(\"KOBOLD\", \"\u72d7\u5934\u4eba\u5730\u535c\u5e08\", cost=3, attack=3, health=2,<br>                 spellpower=1, text=\"\u6cd5\u672f\u4f24\u5bb3 +1\"))<br><br><br># ---- \u89e6\u53d1\u578b ----<br>register(CardDef(\"KNIFE_JUGGLER\", \"\u98de\u5200\u6742\u800d\u8005\", cost=2, attack=3, health=2,<br>                 triggers=(TriggerDef(on=Event.SUMMON,<br>                                      condition=Cond.OWNER,<br>                                      action=Damage(Sel.ENEMY_CHARACTERS.random(1), 1)),),<br>                 text=\"\u6bcf\u5f53\u4f60\u53ec\u5524\u4e00\u4e2a\u968f\u4ece\uff0c\u5bf9\u968f\u673a\u654c\u4eba\u9020\u6210 1 \u70b9\u4f24\u5bb3\"))<br>register(CardDef(\"FRENZY_BOAR\", \"\u72c2\u4e71\u91ce\u732a\", cost=3, attack=3, health=4,<br>                 race=\"BEAST\",<br>                 triggers=(TriggerDef(on=Event.DAMAGE,<br>                                      condition=Cond.SELF_IS_EVENT_TARGET &amp; Cond.SURVIVED,<br>                                      action=Buff(Sel.SELF, atk=2), once=True),),<br>                 text=\"\u72c2\u4e71\uff1a\u83b7\u5f97 +2 \u653b\u51fb\u529b\"))<br>register(CardDef(\"OVERKILL_BEAST\", \"\u8d85\u6740\u731b\u517d\", cost=4, attack=4, health=4,<br>                 race=\"BEAST\",<br>                 triggers=(TriggerDef(on=Event.DAMAGE, condition=Cond.OVERKILL,<br>                                      action=Draw(1)),),<br>                 text=\"\u8d85\u6740\uff1a\u62bd\u4e00\u5f20\u724c\"))<br>register(CardDef(\"SPELLBURST_MAGE\", \"\u6cd5\u672f\u8ff8\u53d1\u5b66\u5f92\", cost=3, attack=2, health=4,<br>                 triggers=(TriggerDef(on=Event.AFTER_SPELL, condition=Cond.OWNER,<br>                                      action=Buff(Sel.SELF, atk=2, hp=2), once=True),),<br>                 text=\"\u6cd5\u672f\u8ff8\u53d1\uff1a\u83b7\u5f97 +2\/+2\"))<br>register(CardDef(\"RAGNAROS\", \"\u708e\u9b54\u4e4b\u738b\", cost=8, attack=8, health=8,<br>                 keywords=frozenset(),<br>                 triggers=(TriggerDef(on=Event.TURN_END, condition=Cond.MY_TURN,<br>                                      action=Damage(Sel.ENEMY_CHARACTERS.random(1), 8)),),<br>                 text=\"\u4f60\u7684\u56de\u5408\u7ed3\u675f\u65f6\uff0c\u5bf9\u968f\u673a\u654c\u4eba\u9020\u6210 8 \u70b9\u4f24\u5bb3\"))<br><br># ---- \u5173\u952e\u8bcd\u5c55\u793a ----<br>register(CardDef(\"REBORN_GUY\", \"\u590d\u751f\u5b88\u536b\", cost=4, attack=4, health=3,<br>                 keywords=frozenset({\"REBORN\", \"TAUNT\"})))<br>register(CardDef(\"RUSH_WOLF\", \"\u7a81\u88ad\u4e4b\u72fc\", cost=3, attack=3, health=3,<br>                 race=\"BEAST\", keywords=frozenset({\"RUSH\"})))<br>register(CardDef(\"LIFESTEAL_DEMON\", \"\u5438\u8840\u6076\u9b54\", cost=5, attack=5, health=5,<br>                 race=\"DEMON\", keywords=frozenset({\"LIFESTEAL\"})))<br>register(CardDef(\"COLOSSAL_KRAKEN\", \"\u5de8\u578b\u6d77\u5996\", cost=7, attack=5, health=5,<br>                 colossal_parts=(\"KRAKEN_TENTACLE\", \"KRAKEN_TENTACLE\"),<br>                 text=\"\u5de8\u578b +2\"))<br>register(CardDef(\"KRAKEN_TENTACLE\", \"\u6d77\u5996\u89e6\u624b\", cost=1, attack=2, health=2,<br>                 keywords=frozenset({\"TAUNT\"})))<br>register(CardDef(\"TRADEABLE_GUY\", \"\u53ef\u4ea4\u6613\u7684\u4f63\u5175\", cost=4, attack=4, health=4,<br>                 keywords=frozenset({\"TRADEABLE\"})))<br>register(CardDef(\"PREPARE_GIANT\", \"\u53ef\u51c6\u5907\u7684\u5de8\u4eba\", cost=8, attack=8, health=8,<br>                 keywords=frozenset({\"PREPARE\"})))<br><br># ---- \u6cd5\u672f ----<br>register(CardDef(\"FIREBALL\", \"\u706b\u7403\u672f\", cost=4, card_type=CardType.SPELL,<br>                 spell_school=\"FIRE\", targeting=Sel.ALL_CHARACTERS,<br>                 spell=Damage(Sel.TARGET, 6)))<br>register(CardDef(\"FROSTBOLT\", \"\u5bd2\u51b0\u7bad\", cost=2, card_type=CardType.SPELL,<br>                 spell_school=\"FROST\", targeting=Sel.ALL_CHARACTERS,<br>                 spell=Seq(Damage(Sel.TARGET, 3), Freeze(Sel.TARGET))))<br>register(CardDef(\"BLIZZARD\", \"\u66b4\u98ce\u96ea\", cost=6, card_type=CardType.SPELL,<br>                 spell_school=\"FROST\",<br>                 spell=Seq(Damage(Sel.ENEMY_MINIONS, 2), Freeze(Sel.ENEMY_MINIONS))))<br>register(CardDef(\"ARCANE_INT\", \"\u5965\u672f\u667a\u6167\", cost=3, card_type=CardType.SPELL,<br>                 spell_school=\"ARCANE\", spell=Draw(2)))<br>register(CardDef(\"SILENCE_SPELL\", \"\u6c89\u9ed8\", cost=0, card_type=CardType.SPELL,<br>                 targeting=Sel.ALL_MINIONS, spell=SilenceAct(Sel.TARGET)))<br>register(CardDef(\"SHIELD_SPELL\", \"\u76fe\u724c\u683c\u6321\", cost=3, card_type=CardType.SPELL,<br>                 spell=Seq(GainArmor(5), Draw(1))))<br>register(CardDef(\"LIGHTNING\", \"\u95ea\u7535\u7bad\", cost=1, card_type=CardType.SPELL,<br>                 spell_school=\"NATURE\", targeting=Sel.ALL_CHARACTERS,<br>                 spell=Seq(Damage(Sel.TARGET, 3), Overload(1))))<br>register(CardDef(\"DISCOVER_SPELL\", \"\u53d1\u73b0\u5b66\u8bc6\", cost=2, card_type=CardType.SPELL,<br>                 spell=Discover([\"FIREBALL\", \"FROSTBOLT\", \"ARCANE_INT\",<br>                                 \"BLIZZARD\", \"LIGHTNING\"])))<br><br># ---- \u5965\u79d8 ----<br>register(CardDef(\"MIRROR_ENTITY\", \"\u955c\u50cf\u5b9e\u4f53\", cost=3, card_type=CardType.SPELL,<br>                 keywords=frozenset({\"SECRET\"}),<br>                 triggers=(TriggerDef(<br>                     on=Event.MINION_PLAYED, zones=frozenset({Zone.SECRET}),<br>                     condition=~Cond.OWNER,<br>                     action=Seq(RevealSecret(),<br>                                SpecialEffect(\"MIRROR_COPY\"))),)))<br><br><br>@special(\"MIRROR_COPY\")<br>def _mirror(g, c):<br>    ev = c.event<br>    if ev and ev.entity:<br>        g.summon(ev.entity[Tag.CARD_ID], c.controller)<br><br><br># ---- \u4efb\u52a1\uff08\u72b6\u6001\u673a\u8303\u5f0f\uff09 ----<br>register(CardDef(\"QUEST_SPELLS\", \"\u5f00\u542f\u4f20\u9001\u95e8\", cost=1, card_type=CardType.SPELL,<br>                 keywords=frozenset({\"QUEST\"}),<br>                 progression=ProgressionDef(<br>                     counter_event=Event.SPELL_CAST,<br>                     thresholds=(3,),<br>                     condition=Cond.OWNER,<br>                     zones=frozenset({Zone.SECRET}),<br>                     complete_action=AddToHand(\"BIG_GUY\")),<br>                 text=\"\u4efb\u52a1\uff1a\u4f7f\u7528 3 \u4e2a\u6cd5\u672f\u3002\u5956\u52b1\uff1a\u83b7\u5f97\u4e00\u4e2a\u5de8\u517d\"))<br><br><br># =============================================================================<br># 13. \u81ea\u68c0 \/ \u6f14\u793a<br># =============================================================================<br><br><br>def make_test_game(seed=1, verbose=False, policy=None) -&gt; Game:<br>    g = Game(seed=seed, verbose=verbose, policy=policy)<br>    deck = [\"WISP\"] * 15<br>    g.setup(list(deck), list(deck))<br>    g.player(0)[Tag.MANA_CRYSTALS] = 10<br>    g.player(0)[Tag.MANA_AVAILABLE] = 10<br>    g.player(1)[Tag.MANA_CRYSTALS] = 10<br>    g.player(1)[Tag.MANA_AVAILABLE] = 10<br>    return g<br><br><br>def spawn(g, card_id, pid, **tags) -&gt; Entity:<br>    e = g.summon(card_id, pid)<br>    for k, v in tags.items():<br>        e[k] = v<br>    return e<br><br><br>def check_invariants(g: Game):<br>    seen = set()<br>    for pid in (0, 1):<br>        board = g.zones.get(pid, Zone.PLAY)<br>        assert len(board) &lt;= 7, \"board over 7\"<br>        assert [e[Tag.ZONE_POSITION] for e in board] == list(range(1, len(board) + 1)), \\<br>            f\"positions not contiguous: {[e[Tag.ZONE_POSITION] for e in board]}\"<br>        assert len(g.zones.get(pid, Zone.HAND)) &lt;= 10, \"hand over 10\"<br>        for m in board:<br>            if m.is_minion:<br>                assert m.current_health &gt; 0, f\"dead minion alive on board: {m}\"<br>        p = g.player(pid)<br>        assert 0 &lt;= p[Tag.MANA_AVAILABLE] &lt;= 10<br>        for z in Zone:<br>            for e in g.zones.get(pid, z):<br>                assert e.id not in seen, f\"entity {e.id} in two zones\"<br>                seen.add(e.id)<br>                assert e[Tag.ZONE] == z<br><br><br># --------------------------------------------------------------------- \u6d4b\u8bd5<br>def t_\u4e24\u4e2a\u98df\u5c38\u9b3c\u540c\u65f6\u6b7b\u4ea1():<br>    g = make_test_game()<br>    a = spawn(g, \"UNSTABLE_GHOUL\", 0); a[Tag.DAMAGE] = 2<br>    b = spawn(g, \"UNSTABLE_GHOUL\", 0); b[Tag.DAMAGE] = 2<br>    c = spawn(g, \"SPIDER_TANK\", 1)      # 3\/4<br>    for m in (a, b):<br>        g.damage(m, 2, source=None)<br>    g.process_deaths()<br>    assert a[Tag.ZONE] == Zone.GRAVEYARD and b[Tag.ZONE] == Zone.GRAVEYARD<br>    # \u4e24\u4e2a\u4ea1\u8bed\u90fd\u89e6\u53d1 \u2192 \u8718\u86db\u5766\u514b\u53d7 2 \u70b9\u4f24\u5bb3<br>    assert c[Tag.DAMAGE] == 2, c[Tag.DAMAGE]<br>    check_invariants(g)<br>    return \"\u4e24\u4e2a\u4ea1\u8bed\u90fd\u89e6\u53d1\uff0c\u8718\u86db\u5766\u514b\u53d7 2 \u70b9\u4f24\u5bb3\"<br><br><br>def t_\u5149\u73af\u6c89\u9ed8\u81f4\u6b7b():<br>    g = make_test_game()<br>    champ = spawn(g, \"STORMWIND_CHAMPION\", 0)<br>    sun = spawn(g, \"SUNFURY\", 0)                 # \u57fa\u7840 3\/2\uff0c\u5149\u73af\u540e 4\/3<br>    assert sun[Tag.HEALTH] == 3, sun[Tag.HEALTH]<br>    g.damage(sun, 2, source=None)<br>    assert sun.current_health == 1<br>    g.silence(champ)<br>    g.process_deaths()<br>    assert sun[Tag.ZONE] == Zone.GRAVEYARD, \"\u5149\u73af\u6d88\u5931\u540e\u5e94\u6b7b\u4ea1\"<br>    check_invariants(g)<br>    return \"\u6c89\u9ed8\u5149\u73af\u6e90\u540e\uff0c\u53d7\u4f24\u7684\u968f\u4ece\u56e0\u4e0a\u9650\u4e0b\u964d\u800c\u6b7b\u4ea1\"<br><br><br>def t_\u5723\u76fe\u6321\u5267\u6bd2():<br>    g = make_test_game()<br>    cobra = spawn(g, \"EMPEROR_COBRA\", 0)         # 2\/3 \u5267\u6bd2<br>    cobra[Tag.EXHAUSTED] = 0<br>    squire = spawn(g, \"ARGENT_SQUIRE\", 1)        # 1\/1 \u5723\u76fe<br>    g.attack(cobra, squire)<br>    assert squire[Tag.ZONE] == Zone.PLAY, \"\u5723\u76fe\u5e94\u6321\u4f4f\u5267\u6bd2\"<br>    assert not squire.has(Tag.DIVINE_SHIELD)<br>    check_invariants(g)<br>    return \"\u5723\u76fe\u5438\u6536\u4f24\u5bb3 \u2192 \u5267\u6bd2\u4e0d\u751f\u6548\uff0c\u968f\u4ece\u5b58\u6d3b\"<br><br><br>def t_\u4ea1\u8bed\u53ec\u5524\u4f4d\u7f6e():<br>    g = make_test_game()<br>    left = spawn(g, \"WISP\", 0)<br>    master = spawn(g, \"IMP_MASTER\", 0)           # \u4f4d\u7f6e 2<br>    right = spawn(g, \"WISP\", 0)<br>    assert master[Tag.ZONE_POSITION] == 2<br>    g.damage(master, 99, source=None)<br>    g.process_deaths()<br>    board = g.zones.get(0, Zone.PLAY)<br>    assert board[1].defn.name == \"\u5c0f\u9b3c\", [b.defn.name for b in board]<br>    check_invariants(g)<br>    return \"\u4ea1\u8bed\u884d\u751f\u7269\u5728\u539f\u968f\u4ece\u4f4d\u7f6e\u53ec\u5524\uff08\u5feb\u7167\u751f\u6548\uff09\"<br><br><br>def t_\u89e6\u53d1\u987a\u5e8f\u6309\u4e0a\u573a\u65f6\u95f4():<br>    g = make_test_game()<br>    order = []<br><br>    @special(\"RECORD_A\")<br>    def _a(gg, cc): order.append(\"A\")<br><br>    @special(\"RECORD_B\")<br>    def _b(gg, cc): order.append(\"B\")<br><br>    register(CardDef(\"REC_A\", \"\u8bb0\u5f55A\", cost=1, attack=1, health=1,<br>                     triggers=(TriggerDef(on=Event.TURN_END,<br>                                          action=SpecialEffect(\"RECORD_A\")),)))<br>    register(CardDef(\"REC_B\", \"\u8bb0\u5f55B\", cost=1, attack=1, health=1,<br>                     triggers=(TriggerDef(on=Event.TURN_END,<br>                                          action=SpecialEffect(\"RECORD_B\")),)))<br>    b = spawn(g, \"REC_B\", 0)                     # \u5148\u4e0a\u573a<br>    a = spawn(g, \"REC_A\", 0, )<br>    # \u628a A \u653e\u5230\u6700\u5de6\uff08\u4f4d\u7f6e 1\uff09<br>    board = g.zones.get(0, Zone.PLAY)<br>    board.remove(a); board.insert(0, a); g.zones._reindex(board)<br>    assert a[Tag.ZONE_POSITION] == 1 and b[Tag.ZONE_POSITION] == 2<br>    g.emit(Event.TURN_END, player=0)<br>    g.events.resolve()<br>    assert order == [\"B\", \"A\"], order            # \u6309 ENTITY_ID\uff0c\u4e0d\u662f\u4f4d\u7f6e<br>    return \"\u89e6\u53d1\u987a\u5e8f\u6309\u4e0a\u573a\u65f6\u95f4(ENTITY_ID)\uff0c\u4e0d\u662f\u6218\u573a\u4f4d\u7f6e\"<br><br><br>def t_\u590d\u751f\u5728\u4ea1\u8bed\u4e4b\u540e():<br>    g = make_test_game()<br>    r = spawn(g, \"REBORN_GUY\", 0)<br>    g.damage(r, 99, source=None)<br>    g.process_deaths()<br>    board = g.zones.get(0, Zone.PLAY)<br>    assert len(board) == 1 and board[0][Tag.HEALTH] == 1<br>    assert not board[0].has(Tag.REBORN), \"\u590d\u751f\u4f53\u4e0d\u80fd\u518d\u6b21\u590d\u751f\"<br>    check_invariants(g)<br>    return \"\u590d\u751f\u4f53\u4ee5 1 \u8840\u56de\u5f52\u4e14\u5931\u53bb\u590d\u751f\u6807\u7b7e\"<br><br><br>def t_\u5de8\u578b\u53ec\u5524\u90e8\u4ef6():<br>    g = make_test_game()<br>    k = spawn(g, \"COLOSSAL_KRAKEN\", 0)<br>    board = g.zones.get(0, Zone.PLAY)<br>    assert len(board) == 3, [b.defn.name for b in board]<br>    assert sum(1 for b in board if b.defn.name == \"\u6d77\u5996\u89e6\u624b\") == 2<br>    check_invariants(g)<br>    return \"\u5de8\u578b\u4e3b\u4f53\u5e26\u51fa 2 \u4e2a\u89e6\u624b\u90e8\u4ef6\"<br><br><br>def t_\u6218\u573a\u5df2\u6ee1\u53ec\u5524\u5931\u8d25():<br>    g = make_test_game()<br>    for _ in range(7):<br>        spawn(g, \"WISP\", 0)<br>    before = len(g.zones.get(0, Zone.PLAY))<br>    r = g.summon(\"WISP\", 0)<br>    assert r is None and len(g.zones.get(0, Zone.PLAY)) == before<br>    assert len(g.zones.get(0, Zone.GRAVEYARD)) == 0, \"\u53ec\u5524\u5931\u8d25\u4e0d\u8fdb\u5893\u5730\"<br>    check_invariants(g)<br>    return \"\u6218\u573a\u6ee1\u65f6\u53ec\u5524\u5931\u8d25\uff1a\u5b9e\u4f53\u6d88\u5931\uff0c\u4e0d\u5165\u5893\u5730\uff0c\u4e0d\u89e6\u53d1\u4ea1\u8bed\"<br><br><br>def t_\u7a81\u88ad\u4e0d\u80fd\u6253\u8138():<br>    g = make_test_game()<br>    w = spawn(g, \"RUSH_WOLF\", 0)<br>    tgts = g.legal_attack_targets(w)<br>    assert all(not t.is_hero for t in tgts), \"\u7a81\u88ad\u9996\u56de\u5408\u4e0d\u80fd\u6253\u8138\"<br>    spawn(g, \"WISP\", 1)<br>    tgts = g.legal_attack_targets(w)<br>    assert len(tgts) == 1 and tgts[0].defn.name == \"\u5c0f\u7cbe\u7075\"<br>    return \"\u7a81\u88ad\u968f\u4ece\u9996\u56de\u5408\u53ea\u80fd\u653b\u51fb\u968f\u4ece\"<br><br><br>def t_\u5632\u8bbd\u9650\u5236\u76ee\u6807():<br>    g = make_test_game()<br>    a = spawn(g, \"WOLFRIDER\", 0)                 # \u51b2\u950b<br>    spawn(g, \"WISP\", 1)<br>    spawn(g, \"SEN_JIN\", 1)                       # \u5632\u8bbd<br>    tgts = g.legal_attack_targets(a)<br>    assert len(tgts) == 1 and tgts[0].defn.name == \"\u68ee\u91d1\u6301\u76fe\u536b\u58eb\"<br>    return \"\u6709\u5632\u8bbd\u65f6\u5408\u6cd5\u76ee\u6807\u53ea\u5269\u5632\u8bbd\u968f\u4ece\"<br><br><br>def t_\u6cd5\u672f\u4f24\u5bb3\u52a0\u6210():<br>    g = make_test_game()<br>    # \u624b\u5de5\u6784\u9020\u5e26\u6cd5\u672f\u4f24\u5bb3\u7684\u968f\u4ece<br>    m = spawn(g, \"KOBOLD\", 0)          # 3\/2\uff0c\u6cd5\u672f\u4f24\u5bb3 +1<br>    victim = spawn(g, \"BOULDERFIST\", 1)          # 6\/7<br>    fb = g.add_to_hand(0, \"FIREBALL\")<br>    g.play_card(fb, target=victim)<br>    assert victim[Tag.ZONE] == Zone.GRAVEYARD, \"6+1=7 \u5e94\u81f4\u6b7b 6\/7\"<br>    check_invariants(g)<br>    return \"\u6cd5\u672f\u4f24\u5bb3 +1 \u8ba9\u706b\u7403\u672f\u9020\u6210 7 \u70b9\u4f24\u5bb3\"<br><br><br>def t_\u72c2\u4e71\u4e00\u6b21\u6027():<br>    g = make_test_game()<br>    boar = spawn(g, \"FRENZY_BOAR\", 0)            # 3\/4<br>    g.damage(boar, 1, source=None); g.events.resolve()<br>    assert boar[Tag.ATK] == 5, boar[Tag.ATK]<br>    g.damage(boar, 1, source=None); g.events.resolve()<br>    assert boar[Tag.ATK] == 5, \"\u72c2\u4e71\u53ea\u89e6\u53d1\u4e00\u6b21\"<br>    check_invariants(g)<br>    return \"\u72c2\u4e71\u53ea\u89e6\u53d1\u4e00\u6b21\uff0c\u4e14\u81f4\u6b7b\u4f24\u5bb3\u4e0d\u89e6\u53d1\"<br><br><br>def t_\u8d85\u6740():<br>    g = make_test_game()<br>    beast = spawn(g, \"OVERKILL_BEAST\", 0)        # 4\/4<br>    beast[Tag.EXHAUSTED] = 0<br>    weak = spawn(g, \"WISP\", 1)                   # 1\/1<br>    hand_before = len(g.zones.get(0, Zone.HAND))<br>    g.attack(beast, weak)<br>    assert len(g.zones.get(0, Zone.HAND)) == hand_before + 1, \"\u8d85\u6740\u5e94\u62bd\u724c\"<br>    check_invariants(g)<br>    return \"4 \u653b\u6253 1 \u8840\u968f\u4ece\u89e6\u53d1\u8d85\u6740\"<br><br><br>def t_\u6cd5\u672f\u8ff8\u53d1():<br>    g = make_test_game()<br>    m = spawn(g, \"SPELLBURST_MAGE\", 0)           # 2\/4<br>    sp = g.add_to_hand(0, \"ARCANE_INT\")<br>    g.play_card(sp)<br>    assert (m[Tag.ATK], m[Tag.HEALTH]) == (4, 6), (m[Tag.ATK], m[Tag.HEALTH])<br>    sp2 = g.add_to_hand(0, \"ARCANE_INT\")<br>    g.play_card(sp2)<br>    assert m[Tag.ATK] == 4, \"\u6cd5\u672f\u8ff8\u53d1\u53ea\u89e6\u53d1\u4e00\u6b21\"<br>    check_invariants(g)<br>    return \"\u6cd5\u672f\u8ff8\u53d1\u89e6\u53d1\u4e00\u6b21\u540e\u6d88\u8017\"<br><br><br>def t_\u4efb\u52a1\u5b8c\u6210():<br>    g = make_test_game()<br>    q = g.add_to_hand(0, \"QUEST_SPELLS\")<br>    g.play_card(q)<br>    assert q[Tag.ZONE] == Zone.SECRET<br>    for _ in range(3):<br>        s = g.add_to_hand(0, \"ARCANE_INT\")<br>        g.play_card(s)<br>    names = [c.defn.name for c in g.zones.get(0, Zone.HAND)]<br>    assert \"\u5de8\u517d\" in names, names<br>    assert q[Tag.ZONE] == Zone.GRAVEYARD<br>    check_invariants(g)<br>    return \"\u4efb\u52a1\uff1a\u4f7f\u7528 3 \u4e2a\u6cd5\u672f\u540e\u83b7\u5f97\u5956\u52b1\"<br><br><br>def t_\u5965\u79d8\u955c\u50cf\u5b9e\u4f53():<br>    g = make_test_game()<br>    sec = g.add_to_hand(1, \"MIRROR_ENTITY\")<br>    g.player(1)[Tag.MANA_AVAILABLE] = 10<br>    g.play_card(sec)<br>    assert sec[Tag.ZONE] == Zone.SECRET<br>    m = g.add_to_hand(0, \"BOULDERFIST\")<br>    g.play_card(m)<br>    p1_board = [x.defn.name for x in g.zones.get(1, Zone.PLAY)]<br>    assert \"\u77f3\u62f3\u98df\u4eba\u9b54\" in p1_board, p1_board<br>    assert sec[Tag.ZONE] == Zone.GRAVEYARD<br>    check_invariants(g)<br>    return \"\u955c\u50cf\u5b9e\u4f53\u590d\u5236\u5bf9\u624b\u6253\u51fa\u7684\u968f\u4ece\"<br><br><br>def t_\u8fc7\u8f7d():<br>    g = make_test_game()<br>    c = g.add_to_hand(0, \"LIGHTNING\")<br>    g.play_card(c, target=g.player(1).hero)<br>    assert g.player(0)[Tag.OVERLOAD_OWED] == 1<br>    g.current = 0<br>    g.player(0)[Tag.MANA_CRYSTALS] = 4<br>    g.begin_turn()<br>    p = g.player(0)<br>    assert p[Tag.OVERLOAD_LOCKED] == 1 and p[Tag.MANA_AVAILABLE] == 4, \\<br>        (p[Tag.OVERLOAD_LOCKED], p[Tag.MANA_AVAILABLE], p[Tag.MANA_CRYSTALS])<br>    return \"\u8fc7\u8f7d\u5728\u4e0b\u56de\u5408\u9501\u5b9a\u6c34\u6676\"<br><br><br>def t_\u53ef\u4ea4\u6613():<br>    g = make_test_game()<br>    c = g.add_to_hand(0, \"TRADEABLE_GUY\")<br>    n_hand = len(g.zones.get(0, Zone.HAND))<br>    mana = g.player(0)[Tag.MANA_AVAILABLE]<br>    ok = g.trade_card(c)<br>    assert ok<br>    assert g.player(0)[Tag.MANA_AVAILABLE] == mana - 1<br>    assert len(g.zones.get(0, Zone.HAND)) == n_hand   # \u4ea4\u6613\u4e00\u5f20\u3001\u62bd\u4e00\u5f20<br>    assert c[Tag.ZONE] == Zone.DECK<br>    check_invariants(g)<br>    return \"\u53ef\u4ea4\u6613\uff1a\u82b1 1 \u8d39\u6d17\u56de\u724c\u5e93\u5e76\u62bd\u4e00\u5f20\"<br><br><br>def t_\u51c6\u5907():<br>    g = make_test_game()<br>    c = g.add_to_hand(0, \"PREPARE_GIANT\")            # 8 \u8d39<br>    ok = g.prepare_card(c, 4)<br>    assert ok<br>    assert c[Tag.PREPARE_DISCOUNT] == 5              # 4 + 1<br>    assert g.compute_cost(c) == 3<br>    assert g.player(0)[Tag.MANA_AVAILABLE] == 6<br>    try:<br>        g.play_card(c)<br>        raise AssertionError(\"\u51c6\u5907\u540e\u672c\u56de\u5408\u4e0d\u80fd\u6253\u51fa\")<br>    except ValueError:<br>        pass<br>    g.current = 0<br>    g.begin_turn()<br>    g.player(0)[Tag.MANA_AVAILABLE] = 10<br>    g.play_card(c)<br>    assert c[Tag.ZONE] == Zone.PLAY<br>    check_invariants(g)<br>    return \"\u51c6\u5907\uff1a\u82b1 4 \u8d39\u6362 5 \u70b9\u6298\u6263\uff0c\u672c\u56de\u5408\u9501\u5b9a\uff0c\u4e0b\u56de\u5408\u53ef\u51fa\"<br><br><br>def t_\u53d1\u73b0():<br>    picked = {}<br><br>    def policy(pid, opts, kind):<br>        picked[\"opts\"] = list(opts)<br>        return opts[-1]<br><br>    g = make_test_game(policy=policy)<br>    c = g.add_to_hand(0, \"DISCOVER_SPELL\")<br>    g.play_card(c)<br>    assert len(picked[\"opts\"]) == 3<br>    names = [x.defn.name if isinstance(x, Entity) else CARD_DB[x].name<br>             for x in g.zones.get(0, Zone.HAND)]<br>    assert CARD_DB[picked[\"opts\"][-1]].name in names, names<br>    check_invariants(g)<br>    return f\"\u53d1\u73b0\u4e09\u9009\u4e00\uff1a{[CARD_DB[o].name for o in picked['opts']]}\"<br><br><br>def t_\u75b2\u52b3():<br>    g = make_test_game()<br>    g.zones.get(0, Zone.DECK).clear()<br>    hp = g.player(0).hero.current_health<br>    g.draw(0, 3)<br>    assert g.player(0)[Tag.FATIGUE] == 3<br>    assert g.player(0).hero.current_health == hp - (1 + 2 + 3)<br>    return \"\u75b2\u52b3\u4f24\u5bb3\u9012\u589e 1,2,3\"<br><br><br>def t_\u53d8\u5f62\u4fdd\u7559\u4f4d\u7f6e():<br>    g = make_test_game()<br>    spawn(g, \"WISP\", 0)<br>    m = spawn(g, \"BOULDERFIST\", 0)               # 6\/7 \u4f4d\u7f6e 2<br>    g.damage(m, 3, source=None)<br>    eid, pos = m.id, m[Tag.ZONE_POSITION]<br>    g.transform(m, \"WISP\")<br>    assert m.id == eid and m[Tag.ZONE_POSITION] == pos<br>    assert m[Tag.ATK] == 1 and m[Tag.DAMAGE] == 0, \"\u53d8\u5f62\u91cd\u7f6e\u5c5e\u6027\u4e0e\u4f24\u5bb3\"<br>    check_invariants(g)<br>    return \"\u53d8\u5f62\u4fdd\u7559 ENTITY_ID \u4e0e\u4f4d\u7f6e\uff0c\u91cd\u7f6e\u5c5e\u6027\"<br><br><br>def t_\u786e\u5b9a\u6027\u56de\u653e():<br>    def run(rng_source=None, seed=7):<br>        g = Game(seed=seed) if rng_source is None else Game(seed=0)<br>        if rng_source is not None:<br>            g.rng = ReplayRNG(rng_source)<br>        deck = [\"WISP\", \"FIREBALL\", \"RIVER_CROC\", \"ARCANE_INT\",<br>                \"BLIZZARD\", \"SPIDER_TANK\"] * 3<br>        g.setup(list(deck), list(deck))<br>        for _ in range(6):<br>            g.draw(0, 1); g.draw(1, 1)<br>        return g<br><br>    g1 = run()<br>    log = list(g1.rng.log)<br>    hand1 = [c[Tag.CARD_ID] for c in g1.zones.get(0, Zone.HAND)]<br>    g2 = run(rng_source=log)<br>    hand2 = [c[Tag.CARD_ID] for c in g2.zones.get(0, Zone.HAND)]<br>    assert hand1 == hand2, (hand1, hand2)<br>    return f\"\u56de\u653e\u4e00\u81f4\uff1a{len(log)} \u6b21 RNG \u8c03\u7528\u5b8c\u5168\u590d\u73b0\"<br><br><br>def t_\u968f\u673a\u5bf9\u5c40\u4e0d\u53d8\u5f0f():<br>    \"\"\"\u8dd1\u4e00\u6279\u968f\u673a\u5bf9\u5c40\uff0c\u5168\u7a0b\u68c0\u67e5\u4e0d\u53d8\u5f0f\u3002\"\"\"<br>    ALL = [k for k, v in CARD_DB.items()<br>           if v.card_type in (CardType.MINION, CardType.SPELL)<br>           and k not in (\"ENCH_GENERIC\",)]<br>    games, turns = 0, 0<br>    for seed in range(40):<br>        rnd = random.Random(seed)<br>        g = Game(seed=seed, policy=lambda p, o, k: o[0])<br>        deck = [rnd.choice(ALL) for _ in range(30)]<br>        try:<br>            g.setup(list(deck), [rnd.choice(ALL) for _ in range(30)])<br>            g.current = 0<br>            g.begin_turn()<br>            for _ in range(200):<br>                pid = g.current<br>                acted = False<br>                p = g.player(pid)<br>                for card in list(g.zones.get(pid, Zone.HAND)):<br>                    if g.compute_cost(card) &gt; p[Tag.MANA_AVAILABLE]: continue<br>                    if card[Tag.LOCKED_UNTIL_TURN] &gt; g.turn: continue<br>                    tg = None<br>                    if card.defn.targeting is not None:<br>                        opts = g.legal_targets(card)<br>                        if not opts: continue<br>                        tg = rnd.choice(opts)<br>                    g.play_card(card, target=tg)<br>                    check_invariants(g)<br>                    acted = True<br>                    break<br>                if not acted:<br>                    attackers = [m for m in g.zones.get(pid, Zone.PLAY)<br>                                 if g.can_attack(m)]<br>                    if attackers:<br>                        a = rnd.choice(attackers)<br>                        tg = g.legal_attack_targets(a)<br>                        if tg:<br>                            g.attack(a, rnd.choice(tg))<br>                            check_invariants(g)<br>                            acted = True<br>                if not acted:<br>                    if g.turn &gt; 40: break<br>                    g.end_turn()<br>                    check_invariants(g)<br>                    turns += 1<br>        except GameOver:<br>            pass<br>        games += 1<br>    return f\"{games} \u5c40\u968f\u673a\u5bf9\u5c40 \/ {turns} \u4e2a\u56de\u5408\uff0c\u5168\u90e8\u4e0d\u53d8\u5f0f\u901a\u8fc7\"<br><br><br>TESTS = [<br>    t_\u4e24\u4e2a\u98df\u5c38\u9b3c\u540c\u65f6\u6b7b\u4ea1, t_\u5149\u73af\u6c89\u9ed8\u81f4\u6b7b, t_\u5723\u76fe\u6321\u5267\u6bd2, t_\u4ea1\u8bed\u53ec\u5524\u4f4d\u7f6e,<br>    t_\u89e6\u53d1\u987a\u5e8f\u6309\u4e0a\u573a\u65f6\u95f4, t_\u590d\u751f\u5728\u4ea1\u8bed\u4e4b\u540e, t_\u5de8\u578b\u53ec\u5524\u90e8\u4ef6,<br>    t_\u6218\u573a\u5df2\u6ee1\u53ec\u5524\u5931\u8d25, t_\u7a81\u88ad\u4e0d\u80fd\u6253\u8138, t_\u5632\u8bbd\u9650\u5236\u76ee\u6807, t_\u6cd5\u672f\u4f24\u5bb3\u52a0\u6210,<br>    t_\u72c2\u4e71\u4e00\u6b21\u6027, t_\u8d85\u6740, t_\u6cd5\u672f\u8ff8\u53d1, t_\u4efb\u52a1\u5b8c\u6210, t_\u5965\u79d8\u955c\u50cf\u5b9e\u4f53,<br>    t_\u8fc7\u8f7d, t_\u53ef\u4ea4\u6613, t_\u51c6\u5907, t_\u53d1\u73b0, t_\u75b2\u52b3, t_\u53d8\u5f62\u4fdd\u7559\u4f4d\u7f6e,<br>    t_\u786e\u5b9a\u6027\u56de\u653e, t_\u968f\u673a\u5bf9\u5c40\u4e0d\u53d8\u5f0f,<br>]<br><br><br>def main():<br>    print(\"=\" * 74)<br>    print(\"stoneheart \u2014\u2014 \u8ff7\u4f60\u7089\u77f3\u5f15\u64ce\u81ea\u68c0\")<br>    print(\"=\" * 74)<br>    passed = failed = 0<br>    for t in TESTS:<br>        name = t.__name__[2:]<br>        try:<br>            msg = t()<br>            print(f\"  \\033[32mPASS\\033[0m  {name:&lt;24} {msg}\")<br>            passed += 1<br>        except Exception as ex:<br>            print(f\"  \\033[31mFAIL\\033[0m  {name:&lt;24} {type(ex).__name__}: {ex}\")<br>            import traceback; traceback.print_exc()<br>            failed += 1<br>    print(\"-\" * 74)<br>    print(f\"  {passed} passed, {failed} failed\")<br>    print(\"=\" * 74)<br>    return failed<br><br><br>if __name__ == \"__main__\":<br>    raise SystemExit(main())<\/pre>\n","protected":false},"excerpt":{"rendered":"<p>\u7089\u77f3\u4f20\u8bf4\u5f15\u64ce\u6df1\u5ea6\u89e3\u5256 \u5173\u952e\u8bcd\u7f16\u5e74\u53f2\u3001\u6838\u5fc3\u7b97\u6cd5\u4e0e\u6570\u636e\u9a71\u52a8\u5361\u724c\u7cfb\u7edf \u4e00\u4efd\u9762\u5411\u5de5\u7a0b\u5e08\u7684\u3001\u53ef\u843d\u5730\u7684\u5361\u724c\u6e38\u620f\u5f15\u64ce\u8bbe\u8ba1\u7b14\u8bb0 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_import_markdown_pro_load_document_selector":0,"_import_markdown_pro_submit_text_textarea":"","footnotes":""},"categories":[15],"tags":[560],"class_list":["post-8118","post","type-post","status-publish","format-standard","hentry","category-games","tag-heartstone","entry"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.4 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>\u7089\u77f3\u4f20\u8bf4\u6e38\u620f\u673a\u5236\u5206\u6790 - \u5fc3\u4e00\u4fe1\u606f<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.xinyixx.com\/index.php\/2026\/09\/04\/hs\/\" \/>\n<meta property=\"og:locale\" content=\"zh_CN\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"\u7089\u77f3\u4f20\u8bf4\u6e38\u620f\u673a\u5236\u5206\u6790 - \u5fc3\u4e00\u4fe1\u606f\" \/>\n<meta property=\"og:description\" content=\"\u7089\u77f3\u4f20\u8bf4\u5f15\u64ce\u6df1\u5ea6\u89e3\u5256 \u5173\u952e\u8bcd\u7f16\u5e74\u53f2\u3001\u6838\u5fc3\u7b97\u6cd5\u4e0e\u6570\u636e\u9a71\u52a8\u5361\u724c\u7cfb\u7edf \u4e00\u4efd\u9762\u5411\u5de5\u7a0b\u5e08\u7684\u3001\u53ef\u843d\u5730\u7684\u5361\u724c\u6e38\u620f\u5f15\u64ce\u8bbe\u8ba1\u7b14\u8bb0 [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.xinyixx.com\/index.php\/2026\/09\/04\/hs\/\" \/>\n<meta property=\"og:site_name\" content=\"\u5fc3\u4e00\u4fe1\u606f\" \/>\n<meta property=\"article:published_time\" content=\"2026-09-04T08:07:20+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-09-04T08:07:24+00:00\" \/>\n<meta name=\"author\" content=\"zentorno\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"\u4f5c\u8005\" \/>\n\t<meta name=\"twitter:data1\" content=\"zentorno\" \/>\n\t<meta name=\"twitter:label2\" content=\"\u9884\u8ba1\u9605\u8bfb\u65f6\u95f4\" \/>\n\t<meta name=\"twitter:data2\" content=\"24 \u5206\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/www.xinyixx.com\\\/index.php\\\/2026\\\/09\\\/04\\\/hs\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.xinyixx.com\\\/index.php\\\/2026\\\/09\\\/04\\\/hs\\\/\"},\"author\":{\"name\":\"zentorno\",\"@id\":\"https:\\\/\\\/www.xinyixx.com\\\/#\\\/schema\\\/person\\\/f6d999c2e397b7909f2a3490b537b8ee\"},\"headline\":\"\u7089\u77f3\u4f20\u8bf4\u6e38\u620f\u673a\u5236\u5206\u6790\",\"datePublished\":\"2026-09-04T08:07:20+00:00\",\"dateModified\":\"2026-09-04T08:07:24+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/www.xinyixx.com\\\/index.php\\\/2026\\\/09\\\/04\\\/hs\\\/\"},\"wordCount\":784,\"publisher\":{\"@id\":\"https:\\\/\\\/www.xinyixx.com\\\/#organization\"},\"keywords\":[\"heartstone\"],\"articleSection\":[\"\u6e38\u620f\"],\"inLanguage\":\"zh-Hans\"},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/www.xinyixx.com\\\/index.php\\\/2026\\\/09\\\/04\\\/hs\\\/\",\"url\":\"https:\\\/\\\/www.xinyixx.com\\\/index.php\\\/2026\\\/09\\\/04\\\/hs\\\/\",\"name\":\"\u7089\u77f3\u4f20\u8bf4\u6e38\u620f\u673a\u5236\u5206\u6790 - \u5fc3\u4e00\u4fe1\u606f\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.xinyixx.com\\\/#website\"},\"datePublished\":\"2026-09-04T08:07:20+00:00\",\"dateModified\":\"2026-09-04T08:07:24+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/www.xinyixx.com\\\/index.php\\\/2026\\\/09\\\/04\\\/hs\\\/#breadcrumb\"},\"inLanguage\":\"zh-Hans\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/www.xinyixx.com\\\/index.php\\\/2026\\\/09\\\/04\\\/hs\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/www.xinyixx.com\\\/index.php\\\/2026\\\/09\\\/04\\\/hs\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"\u9996\u9875\",\"item\":\"https:\\\/\\\/www.xinyixx.com\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"\u7089\u77f3\u4f20\u8bf4\u6e38\u620f\u673a\u5236\u5206\u6790\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/www.xinyixx.com\\\/#website\",\"url\":\"https:\\\/\\\/www.xinyixx.com\\\/\",\"name\":\"\u5fc3\u4e00\u4fe1\u606f\",\"description\":\"\u5b66\u4e60\u3001\u8bb0\u5f55\u3001\u5206\u4eab\",\"publisher\":{\"@id\":\"https:\\\/\\\/www.xinyixx.com\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/www.xinyixx.com\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"zh-Hans\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/www.xinyixx.com\\\/#organization\",\"name\":\"\u5fc3\u4e00\u4fe1\u606f\",\"url\":\"https:\\\/\\\/www.xinyixx.com\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"zh-Hans\",\"@id\":\"https:\\\/\\\/www.xinyixx.com\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"http:\\\/\\\/www.xinyixx.com\\\/wp-content\\\/uploads\\\/2023\\\/05\\\/11-1.png\",\"contentUrl\":\"http:\\\/\\\/www.xinyixx.com\\\/wp-content\\\/uploads\\\/2023\\\/05\\\/11-1.png\",\"width\":1024,\"height\":1024,\"caption\":\"\u5fc3\u4e00\u4fe1\u606f\"},\"image\":{\"@id\":\"https:\\\/\\\/www.xinyixx.com\\\/#\\\/schema\\\/logo\\\/image\\\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/www.xinyixx.com\\\/#\\\/schema\\\/person\\\/f6d999c2e397b7909f2a3490b537b8ee\",\"name\":\"zentorno\",\"sameAs\":[\"http:\\\/\\\/www.xinyixx.com\"],\"url\":\"https:\\\/\\\/www.xinyixx.com\\\/index.php\\\/author\\\/gorgegao\\\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"\u7089\u77f3\u4f20\u8bf4\u6e38\u620f\u673a\u5236\u5206\u6790 - \u5fc3\u4e00\u4fe1\u606f","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.xinyixx.com\/index.php\/2026\/09\/04\/hs\/","og_locale":"zh_CN","og_type":"article","og_title":"\u7089\u77f3\u4f20\u8bf4\u6e38\u620f\u673a\u5236\u5206\u6790 - \u5fc3\u4e00\u4fe1\u606f","og_description":"\u7089\u77f3\u4f20\u8bf4\u5f15\u64ce\u6df1\u5ea6\u89e3\u5256 \u5173\u952e\u8bcd\u7f16\u5e74\u53f2\u3001\u6838\u5fc3\u7b97\u6cd5\u4e0e\u6570\u636e\u9a71\u52a8\u5361\u724c\u7cfb\u7edf \u4e00\u4efd\u9762\u5411\u5de5\u7a0b\u5e08\u7684\u3001\u53ef\u843d\u5730\u7684\u5361\u724c\u6e38\u620f\u5f15\u64ce\u8bbe\u8ba1\u7b14\u8bb0 [&hellip;]","og_url":"https:\/\/www.xinyixx.com\/index.php\/2026\/09\/04\/hs\/","og_site_name":"\u5fc3\u4e00\u4fe1\u606f","article_published_time":"2026-09-04T08:07:20+00:00","article_modified_time":"2026-09-04T08:07:24+00:00","author":"zentorno","twitter_card":"summary_large_image","twitter_misc":{"\u4f5c\u8005":"zentorno","\u9884\u8ba1\u9605\u8bfb\u65f6\u95f4":"24 \u5206"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/www.xinyixx.com\/index.php\/2026\/09\/04\/hs\/#article","isPartOf":{"@id":"https:\/\/www.xinyixx.com\/index.php\/2026\/09\/04\/hs\/"},"author":{"name":"zentorno","@id":"https:\/\/www.xinyixx.com\/#\/schema\/person\/f6d999c2e397b7909f2a3490b537b8ee"},"headline":"\u7089\u77f3\u4f20\u8bf4\u6e38\u620f\u673a\u5236\u5206\u6790","datePublished":"2026-09-04T08:07:20+00:00","dateModified":"2026-09-04T08:07:24+00:00","mainEntityOfPage":{"@id":"https:\/\/www.xinyixx.com\/index.php\/2026\/09\/04\/hs\/"},"wordCount":784,"publisher":{"@id":"https:\/\/www.xinyixx.com\/#organization"},"keywords":["heartstone"],"articleSection":["\u6e38\u620f"],"inLanguage":"zh-Hans"},{"@type":"WebPage","@id":"https:\/\/www.xinyixx.com\/index.php\/2026\/09\/04\/hs\/","url":"https:\/\/www.xinyixx.com\/index.php\/2026\/09\/04\/hs\/","name":"\u7089\u77f3\u4f20\u8bf4\u6e38\u620f\u673a\u5236\u5206\u6790 - \u5fc3\u4e00\u4fe1\u606f","isPartOf":{"@id":"https:\/\/www.xinyixx.com\/#website"},"datePublished":"2026-09-04T08:07:20+00:00","dateModified":"2026-09-04T08:07:24+00:00","breadcrumb":{"@id":"https:\/\/www.xinyixx.com\/index.php\/2026\/09\/04\/hs\/#breadcrumb"},"inLanguage":"zh-Hans","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.xinyixx.com\/index.php\/2026\/09\/04\/hs\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/www.xinyixx.com\/index.php\/2026\/09\/04\/hs\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"\u9996\u9875","item":"https:\/\/www.xinyixx.com\/"},{"@type":"ListItem","position":2,"name":"\u7089\u77f3\u4f20\u8bf4\u6e38\u620f\u673a\u5236\u5206\u6790"}]},{"@type":"WebSite","@id":"https:\/\/www.xinyixx.com\/#website","url":"https:\/\/www.xinyixx.com\/","name":"\u5fc3\u4e00\u4fe1\u606f","description":"\u5b66\u4e60\u3001\u8bb0\u5f55\u3001\u5206\u4eab","publisher":{"@id":"https:\/\/www.xinyixx.com\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.xinyixx.com\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"zh-Hans"},{"@type":"Organization","@id":"https:\/\/www.xinyixx.com\/#organization","name":"\u5fc3\u4e00\u4fe1\u606f","url":"https:\/\/www.xinyixx.com\/","logo":{"@type":"ImageObject","inLanguage":"zh-Hans","@id":"https:\/\/www.xinyixx.com\/#\/schema\/logo\/image\/","url":"http:\/\/www.xinyixx.com\/wp-content\/uploads\/2023\/05\/11-1.png","contentUrl":"http:\/\/www.xinyixx.com\/wp-content\/uploads\/2023\/05\/11-1.png","width":1024,"height":1024,"caption":"\u5fc3\u4e00\u4fe1\u606f"},"image":{"@id":"https:\/\/www.xinyixx.com\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/www.xinyixx.com\/#\/schema\/person\/f6d999c2e397b7909f2a3490b537b8ee","name":"zentorno","sameAs":["http:\/\/www.xinyixx.com"],"url":"https:\/\/www.xinyixx.com\/index.php\/author\/gorgegao\/"}]}},"_links":{"self":[{"href":"https:\/\/www.xinyixx.com\/index.php\/wp-json\/wp\/v2\/posts\/8118","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.xinyixx.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.xinyixx.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.xinyixx.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.xinyixx.com\/index.php\/wp-json\/wp\/v2\/comments?post=8118"}],"version-history":[{"count":1,"href":"https:\/\/www.xinyixx.com\/index.php\/wp-json\/wp\/v2\/posts\/8118\/revisions"}],"predecessor-version":[{"id":8119,"href":"https:\/\/www.xinyixx.com\/index.php\/wp-json\/wp\/v2\/posts\/8118\/revisions\/8119"}],"wp:attachment":[{"href":"https:\/\/www.xinyixx.com\/index.php\/wp-json\/wp\/v2\/media?parent=8118"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.xinyixx.com\/index.php\/wp-json\/wp\/v2\/categories?post=8118"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.xinyixx.com\/index.php\/wp-json\/wp\/v2\/tags?post=8118"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}