有时候一直在想，人生的意义是什么？在大学前，我们的生活轨迹一成不变，每天都是伴随着清晨的阳光从家里出发，又在同学们的玩耍中从学校返回，生活无忧无虑，需要思考的也只是学习成绩和同学关系。前途总是光明的，一切事情都不需要额外的选择。高中老师有句名言：“上了大学就轻松了”。似乎大学就是一个可以畅谈人生理想，与同学一起学习奋斗，实现自我价值的地方。可是，当上了大学后，发现一切都没有那么简单。尽管再也不需要没日没夜备战高考，但需要考虑的事情却变得很多很多。来大学前，我总是想着大学里丰富多彩的生活，能在社团里找到一些志同道合的朋友，很可惜，社团里总是一小群人玩得不亦乐乎，而大多数人只能做一个旁观者，然后被遗忘在角落。也许是这些社团活动方式有点问题，也许是我自己没能更加主动参与进去，无论怎么说，这都是我一个小小的遗憾。一个个在高中拼命学习才能考上大学的少年，在来到大学后，却选择了一条最轻松的道路，每天沉迷游戏刷剧，过着得过且过的生活。不在乎学到了什么知识，只关注考试能不能及格。不反思自己上课时没认真听讲，只苛求老师讲课不够生动。其实娱乐并不是一件坏事，适当的娱乐是再生产的一部分，能帮助人恢复精力投入到接下来的生活当中。但沉湎于娱乐之中，遗忘学生的本职工作——学习，便是一件得不偿失的事情了。面对着这些娱乐至死的同学，我想是不是因为高中过于紧张的学习消耗掉了我们自身的学习动力了呢？还是说认为考上大学，就意味着前途一片光明？我不知道答案，但要知道很多人往往没有机会参加高考，他们早已消失在了每一个开学的秋季。而我幸运的能参加高考并考上大学，而不是早早辍学外出打工。除了那些浑浑噩噩的同学，还有一部分同学则看上去非常优秀，他们学习优秀，身兼数职，参加各种各样的竞赛活动；坚持运动，琴棋书画样样精通，还时不时会来一场说走就走的旅行。这似乎是大多数学生的榜样，相信自己总有一天会成为像他们一样，成为自己想成为的人。这种评价标准似乎伴随着我们的整个大学生涯，但我们有没有想过，这究竟是不是我自己内心的真实想法呢？哈佛招生主任，威廉·R·费茨蒙斯中曾说到：就算那些最成功的学生，也会在某个时刻思考一切是否都值得。在他们三四十岁时，他们是社会公认的有成就的医生、律师、学者、商人，但他们往往感到，自己不过是一群在终生竞争的集中营里茫然的生还者。我们的价值观经常被社会绑架着，无处不在的主流价值观让我们随波逐流，有些人在其中遗失了自我，有些人则想方设法逃离，前去寻找真正的自我。孔子曾说，吾日三省吾身，在前进的道路上应该多回头看看自己走过的路，这究竟是不是有价值的，如何才能实现自我。有个同学问我，“为什么你能对所有事情保持一种热情”，我想这正是因为我认识到人需要认清自我，只有积极地探索自我，才能度过有意义的一生。人生本没有意义，只有自己才能赋予人生意义。
击败DotA2顶级人类玩家，并不是AI的一次突破
文：Denny Britz（前谷歌大脑团队成员）来源：量子位 当我昨天看到OpenAI的人工智能，在一项奖金2400万美元的电子竞技赛事上，击败DotA2人类顶级高手时，整个人感觉超兴奋。 这是因为，一方面我是一个电子竞技的粉丝。尽管没玩过DotA 2，但我经常在Twitch上观看其他其他电竞赛事，高中时我还当过一阵半职业选手。 更重要是的，像DotA这类多人在线战术竞技游戏（MOBA），以及星际2这类实时策略游戏（RTS），通常被认为远超目前人工智能的驾驭能力。因为这两类游戏需要长期战略决策、多人合作，比国际象棋、围棋有着更复杂的状态和动作空间。 DeepMind已经在星际2上研究了一段时间，前不久刚刚发布了新的进展，但目前为止，相关研究还没有取得重大突破。大家普遍认为，距离人工智能在星际2上吊打人类顶级玩家，至少还有一两年的时间。 这就是OpenAI这个成果如此令人震惊的原因。这是怎么回事？最有有什么人工智能方面的突破是我没有注意到的么？于是我开始研究这个DotA 2人工智能到底干了什么，它是如何训练的，以及在什么样的游戏环境中运行。 我的结论是：这是一个令人印象深刻的成就。但不是一次AI上的突破。 通过这篇文章，我想提供一个关于此事的清醒解释。实际上，过度炒作人工智能的进步才是真正危险的事情。例如，下面这位在推特上的发言，才是真正的误导。 这是伊隆·马斯克的推特，这位硅谷钢铁侠不单创办了特斯拉，而且创办了OpenAI。上面这篇推特中，马斯克大意是说：OpenAI搞出了史上首个击败电子竞技顶级玩家的人工智能。这比搞国际象棋和围棋什么的复杂多了。 在第二条推特中，马斯克再次发表曾被AI届猛轰的观点：没人喜欢被管制，但对公众构成危险的一切（汽车、飞机、食品、药物等）都应该受到管制。AI也是一样。 当然，马斯克还发了一条，就不翻译了，贴图如下 首先要声明的是，我今天要谈及的炒作或者误导，并不是OpenAI研究人员的错误。OpenAI一直在研究方面都有诸多贡献。目前，OpenAI还没有公布他们解决方案的细节，所以外界很容易就会得出错误的结论。 现在开始切入正题。我们先来看看DotA 2的人工智能程序，到底解决了一个多困难的问题？尤其是与AlphaGo相比。1v1不能与5v5相提并论 在正常的DotA 2游戏中，两个对抗的队伍各由五名玩家组成。游戏过程中需要高级策略、团队沟通和协调，一局比赛通常要45分钟。 而这次人工智能击败人类的比赛，采用了1v1的模式，这种模式有太多限制。例如双方基本上只能沿着单线前进，并尝试击杀对方，游戏过程几分钟就结束了。 在1v1模式中，击败对手主要靠机械技能和短期策略，并不涉及长期规划和协调，而后者才是对当前AI技术来说最具挑战性的部分。 事实上，在这次的人机DotA 2对抗中，可以采用的有效动作数量，少于围棋人机大战；有效的状态空间（目前局面情况），如果以智能的方式表示，应该比围棋要小。AI可以获取更多信息 OpenAI的人工智能程序，极有可能是构建在DotA 2原有的机器人接口之上，可以获取更多人类玩家看不到的信息。即使OpenAI的研究人员限制了这些信息的获取，人工智能仍然能够得到比人类更精准的信息。 例如技能的施放，人类玩家必须紧盯屏幕，并且估算与对手之间的距离。而AI知道确切的距离，并且能立即决定是否施放技能。获得精准的数字信息是一个很大的优势。其实对战过程中你就能看到，AI有几次攻击都是在最大距离上展开。反应时间 AI可以立即作出反应，人类不行。再加上刚才说的信息优势，AI的优势进一步扩大了。比方，一旦对手逃离攻击范围，AI可以立刻取消攻击命令。使用单一英雄 DotA 2中有上百种不同的英雄角色，各具不同的能力和优势。而AI掌握的只是其中一个英雄：Shadow Fiend（影魔）。影魔通常会立刻展开攻击，而不是在一段时间内学习掌握更复杂的攻击技能，这更加有利于发挥AI在信息和反应方面的优势。 所以，鉴于1v1主要比拼机械技能，AI击败人类玩家并不奇怪。鉴于游戏环境被严格限制，造成一些列战术和策略也被限制，而且对战中几乎没有必要进行长期规划或协调。 再次重申我的结论：这次AI击败DotA玩家，比在围棋中击败人类冠军要容易得多。人类没有在AI领域突然取得突破。 这次在DotA对抗中之所以AI获胜，是因为研究人员聪明的设置了问题，使得AI可以绕过目前人工智能的技术限制。 据说这个OpenAI训练这个AI打DotA花了2周。与之相比，AlphaGo在Google的GPU集群上进行了数月的分布式大规模训练。两个程序之间的计算要求有着数量级的区别。 最后夸夸这个会玩DotA的AI，到底有何精彩之处？完全通过自学训练 AI不需要任何训练数据，也不会从人类的比赛中学习。整个学习过程随机开始，并且通过和自己对抗进行学习。虽然这不是什么新技术，但令人惊讶的是，AI学会了人类玩家已经在使用的技术。这很酷。 AI可能还有其他技术，甚至人类都不知道。这与我们在AlphaGo中看到的类似，围棋选手已经开始学习AI的下棋方式。AI+电竞的重要一步 在具有挑战性的环境中（例如DotA 2和星际2）来测试AI技术是非常重要的。AI可以为游戏提供更多的价值，游戏也会助推AI更快发展。不完美信息 在DotA对决中，人类玩家智能看到地图的一小部分，视线受到妨碍。AI可能也一样，虽然还不清楚OpenAI如何处理这个问题的细节。 这意味着与围棋、国际象棋、Atari游戏机等环境不同，AI在DotA中处于一个部分可观察的环境，而无法获知关于游戏当前状态的完整信息。这类问题通常难以解决，话虽如此，但目前还不清楚1v1的DotA 2比赛中，视野的重要性到底几何。 不管怎样，非常期待看到OpenAI关于这次比赛的技术报告。
Dota2 AI 简易开发教程（二）——英雄出装及其相关功能
距离上一篇教程也过去一个多月了，是时候写一篇新教程了。上一篇文章主要介绍了如何选择阵容和技能的使用，在这一篇文章中，我们将介绍如何配置英雄出装和其相关的一些模块。勘误首先先纠正一下上一篇文章中的一些错误，在选择阵容部分，下面的table.remove函数中出现了一个错误，bad argument #2 to 'remove' (number expected, got string)，就是第二个参数应该为数字而不是字符串。这个错误将导致已选择的英雄不能从英雄列表中移除，从而会选择出重复的英雄。function Think() for i,id in pairs(GetTeamPlayers(GetTeam())) do if(IsPlayerBot(id) and (GetSelectedHeroName(id)=="" or GetSelectedHeroName(id)==nil)) then local num=hero_pool_my[RandomInt(1, #hero_pool_my)] --取随机数 SelectHero( id, num ); --在保存英雄名称的表中，随机选择出AI的英雄 table.remove(hero_pool_my,num) --移除这个英雄 end end end所以中间几行应该修改为，这样从表中才能正确移除已经选择的英雄 local i=RandomInt(1, #hero_pool_my) --取随机数 local heroname=hero_pool_my[i] --获取英雄的名称 SelectHero( id, heroname ); --在保存英雄名称的表中，随机选择出AI的英雄 table.remove(hero_pool_my,i) --移除这个英雄出装系统简介在官方开发者维基中有着这样的说明：物品购买如果你只想重写在购买物品时的决策，你可以在文件名为item_purchase_generic.lua的文件中实现如下函数： ItemPurchaseThink() - 每帧被调用。负责物品购买。 你也可以仅重写某个特定英雄的物品购买逻辑，比如火女(Lina),写在文件名为item_purchase_lina.lua的文件中。在dota 2 beta\game\dota\scripts\vscripts\bots_example V社的示例文件中，我们可以找到item_purchase_lina.lua文件，这就是为火女配置出装的文件。打开这个文件，我们可以看到：--这个表用来记录AI出装的顺序 local tableItemsToBuy = { "item_tango", "item_tango", "item_clarity", "item_clarity", "item_branches", "item_branches", "item_magic_stick", "item_circlet", "item_boots", "item_energy_booster", "item_staff_of_wizardry", "item_ring_of_regen", "item_recipe_force_staff", "item_point_booster", "item_staff_of_wizardry", "item_ogre_axe", "item_blade_of_alacrity", "item_mystic_staff", "item_ultimate_orb", "item_void_stone", "item_staff_of_wizardry", "item_wind_lace", "item_void_stone", "item_recipe_cyclone", "item_cyclone", }; ------------------------------------------------------------------------------------------ --用来实现基本的物品购买函数 function ItemPurchaseThink() local npcBot = GetBot(); if ( #tableItemsToBuy == 0 ) then npcBot:SetNextItemPurchaseValue( 0 ); return; end local sNextItem = tableItemsToBuy[1]; npcBot:SetNextItemPurchaseValue( GetItemCost( sNextItem ) ); --用于默认AI的相关函数 if ( npcBot:GetGold() >= GetItemCost( sNextItem ) ) --判断金钱是否足够 then npcBot:Action_PurchaseItem( sNextItem ); --购买物品 table.remove( tableItemsToBuy, 1 ); end end 大家注意到，在上面那个记录出装顺序的表中，物品的名称似乎和平时用的有所区别，其实也可以在这个页面找到。在上面这个函数中，V社为我们提供了购买物品的基本函数，但是这只能实现在泉水的商店中购买物品，如果要去神秘商店和边路商店购买物品怎么办呢？如果要配置多种出装风格应该怎么办？如何控制信使去购买物品？那就需要靠我们自己实现了。而且如果要配置物品出装，那么需要手动填写每一个配方。不过在V社最近的api更新之后，添加了一个函数GetItemComponents(sItemName)，可以直接获取物品的配方。下面，我们开始在V社的基本函数上逐渐添加更多的功能。在神秘商店和边路商店购买装备由于物品购买是一个通用的功能，所以我们可以在item_purchase_generic.lua中编写函数，以便重复调用。在默认AI的框架下，有两个模式用于物品购买secret_shop和side_shop，我们需要通过在主函数中控制前往神秘商店和边路商店购买物品。（尽管将选择前往哪个商店的功能耦合在一个函数中不好。更好的方法是在其它两个模式中重新编写GetDesire()函数。）在游戏中，一个能在神秘商店购买的物品必定不能在基地商店购买，而边路商店的物品则有可能来自于其他商店。所以便能通过IsItemPurchasedFromSideShop(sNextItem)和IsItemPurchasedFromSecretShop(sNextItem )函数来判断一个物品能在哪被购买，进而决定下一个物品的购买位置。 if(npcBot.secretShopMode~=true and npcBot.sideShopMode ~=true) then if (IsItemPurchasedFromSideShop( sNextItem ) and npcBot:DistanceFromSideShop() <= 2000) --只有在离边路商店较近时才前往边路商店 then npcBot.sideShopMode = true; end if (IsItemPurchasedFromSecretShop( sNextItem )) then npcBot.secretShopMode = true; end End然后我们在mode_secret_shop_generic.lua和mode_side_shop_generic.lua函数中分别实现控制英雄前往神秘商店和边路商店购买的操作。在开发者维基中提到了以下内容：模式重写如果你想在已有模式体系下修改某个模式逻辑的需求和行为，例如修改对线模式(laning mode)，你可以在文件名为mode_laning_generic.lua的文件中实现如下函数: GetDesire() - 每帧都被调用，需要返回一个0到1之间的浮点值，该值标志了该模式有多大可能成为当前激活模式 OnStart() - 当该模式成为当前激活模式时调用 OnEnd() - 当该模式让出控制权给其他被激活模式时调用 Think() - 当该模式为当前激活模式时，每帧都被调用。负责发出机器人的行为指令。 你也可以仅重写某个特定英雄的模式逻辑，比如火女(Lina),写在文件名为mode_laning_lina.lua的文件中。如果你想在特定英雄的模式重写时调用泛用的英雄模式代码，请参考附件A的实现细节。所以，我们便需要GetDesire()和Think()以重写该模式。当英雄离边路商店较近，而且下一件物品可以在边路商店购买时，英雄便不需要用信使或自己返回基地购买物品，而是直接前往边路商店。mode_side_shop_generic.lua 边路商店模式function GetDesire() local npcBot = GetBot(); local desire = 0.0; if ( npcBot:IsUsingAbility() or npcBot:IsChanneling() ) --不应打断持续施法 then return 0 end if ( npcBot.sideShopMode == true and npcBot:GetGold() >= npcBot:GetNextItemPurchaseValue()) then local d=npcBot:DistanceFromSideShop() if d<2000 then desire = (2000-d)/d*0.3+0.3; --根据离边路商店的距离返回欲望值 end else npcBot.sideShopMode = false end return desire end function Think() local npcBot = GetBot(); local shopLoc1 = GetShopLocation( GetTeam(), SHOP_SIDE ); local shopLoc2 = GetShopLocation( GetTeam(), SHOP_SIDE2 ); if ( GetUnitToLocationDistance(npcBot, shopLoc1) <= GetUnitToLocationDistance(npcBot, shopLoc2) ) then --选择前往距离自己更近的商店 npcBot:Action_MoveToLocation( shopLoc1 ); else npcBot:Action_MoveToLocation( shopLoc2 ); end endmode_secret_shop_generic.lua 神秘商店模式function GetDesire() local npcBot = GetBot(); local desire = 0.0; if ( npcBot:IsUsingAbility() or npcBot:IsChanneling() ) --不应打断持续施法 then return 0 end if ( npcBot.secretShopMode == true and npcBot:GetGold() >= npcBot:GetNextItemPurchaseValue()) then local d=npcBot:DistanceFromSecretShop() if d<3000 then desire = (3000-d)/d*0.3+0.3; --根据离边路商店的距离返回欲望值 end else npcBot.secretShopMode = false end return desire end function Think() local npcBot = GetBot(); local shopLoc1 = GetShopLocation( GetTeam(), SHOP_SECRET ); local shopLoc2 = GetShopLocation( GetTeam(), SHOP_SECRET2 ); if ( GetUnitToLocationDistance(npcBot, shopLoc1) <= GetUnitToLocationDistance(npcBot, shopLoc2) ) then --选择前往距离自己更近的商店 npcBot:Action_MoveToLocation( shopLoc1 ); else npcBot:Action_MoveToLocation( shopLoc2 ); end end然后需要在主购买函数中检查英雄是否已经到达神秘或野外商店附近，并购买物品。 local PurchaseResult --接收购买结果，后文会介绍 if(npcBot.sideShopMode == true) then if(npcBot:DistanceFromSideShop() <= 200) then PurchaseResult=npcBot:ActionImmediate_PurchaseItem( sNextItem ) end elseif(npcBot.secretShopMode == true) --如果目标是神秘商店，则命令信使购买物品 then if(npcBot:DistanceFromSecretShop() <= 200) then PurchaseResult=npcBot:ActionImmediate_PurchaseItem( sNextItem ) end local courier=GetCourier(0) if(courier==nil) then BuyCourier() --没有信使的话则会购买 else if(courier:DistanceFromSecretShop() <= 200) --信使已到达商店 then PurchaseResult=GetCourier(0):ActionImmediate_PurchaseItem( sNextItem ) end end else PurchaseResult=npcBot:ActionImmediate_PurchaseItem( sNextItem ) end购买信使的函数function BuyCourier() local npcBot=GetBot() local courier=GetCourier(0) if(courier==nil) --购买小鸡 then if(npcBot:GetGold()>=GetItemCost("item_courier")) then local info=npcBot:ActionImmediate_PurchaseItem("item_courier"); if info ==PURCHASE_ITEM_SUCCESS then print(npcBot:GetUnitName()..' buy the courier',info); end end else --购买飞行信使 if DotaTime()>60*3 and npcBot:GetGold()>=GetItemCost("item_flying_courier") and (courier:GetMaxHealth()==75) then local info=npcBot:ActionImmediate_PurchaseItem("item_flying_courier"); if info ==PURCHASE_ITEM_SUCCESS then print(npcBot:GetUnitName()..' has upgraded the courier.',info); end end end end检查购买结果npcBot:Action_PurchaseItem( sNextItem )函数会返回一个购买结果的值，其可能的值为： Item Purchase Results PURCHASE_ITEM_SUCCESS --成功购买 PURCHASE_ITEM_OUT_OF_STOCK --物品栏已满 PURCHASE_ITEM_DISALLOWED_ITEM --不被允许的物品 PURCHASE_ITEM_INSUFFICIENT_GOLD --金钱不足 PURCHASE_ITEM_NOT_AT_HOME_SHOP --不在基地商店 PURCHASE_ITEM_NOT_AT_SIDE_SHOP --不在边路商店 PURCHASE_ITEM_NOT_AT_SECRET_SHOP --不在神秘商店 PURCHASE_ITEM_INVALID_ITEM_NAME --不存在的物品名称在购买物品时，用一个变量记录购买结果，并随后检查，以免出现购买失败的情况。local PurchaseResult=npcBot:ActionImmediate_PurchaseItem( sNextItem ) if(PurchaseResult==PURCHASE_ITEM_SUCCESS) --成功购买便从出装表中移除该物品 then npcBot.secretShopMode = false; npcBot.sideShopMode = false; table.remove( ItemsToBuy, 1 ) end if(PurchaseResult==PURCHASE_ITEM_OUT_OF_STOCK) --物品栏已满，出售多余的物品 then SellExtraItem() end if(PurchaseResult==PURCHASE_ITEM_INVALID_ITEM_NAME or PurchaseResult==PURCHASE_ITEM_DISALLOWED_ITEM) --不存在的物品，移除该物品 then table.remove( ItemsToBuy, 1 ) end if(PurchaseResult==PURCHASE_ITEM_INSUFFICIENT_GOLD ) --金额不足（其实该情况也较少出现，因为我们已经在上面判断了金钱） then npcBot.secretShopMode = false; npcBot.sideShopMode = false; end if(PurchaseResult==PURCHASE_ITEM_NOT_AT_SECRET_SHOP) --不在神秘商店，前往神秘商店 then npcBot.secretShopMode = true npcBot.sideShopMode = false; end if(PurchaseResult==PURCHASE_ITEM_NOT_AT_SIDE_SHOP) --不在边路商店（其实该情况不会出现，因为在边路商店的物品能在其他商店购买） then npcBot.sideShopMode = true npcBot.secretShopMode = false; end if(PurchaseResult==PURCHASE_ITEM_NOT_AT_HOME_SHOP) --不在基地商店（也不会出现的情况，因为如果英雄不在家中，那么物品会购买于贮藏处） then npcBot.secretShopMode = false; npcBot.sideShopMode = false; end两个效用函数，用于判断物品栏是否已满和出售特定物品function SellSpecifiedItem( item_name ) --出售特定物品 local npcBot = GetBot(); local itemCount = 0; local item = nil; for i = 0, 14 do local sCurItem = npcBot:GetItemInSlot(i); if ( sCurItem ~= nil ) then itemCount = itemCount + 1; if ( sCurItem:GetName() == item_name ) then item = sCurItem; end end end if ( item ~= nil and itemCount > 5 and (npcBot:DistanceFromFountain() <= 600 or npcBot:DistanceFromSideShop() <= 200 or npcBot:DistanceFromSecretShop() <= 200) ) then npcBot:ActionImmediate_SellItem( item ); end end function SellExtraItem() --出售已经没有用处的物品 if(GameTime()>15*60) then SellSpecifiedItem("item_faerie_fire") SellSpecifiedItem("item_enchanted_mango") SellSpecifiedItem("item_tango") SellSpecifiedItem("item_clarity") SellSpecifiedItem("item_flask") end if(GameTime()>20*60) then SellSpecifiedItem("item_stout_shield") SellSpecifiedItem("item_orb_of_venom") end if(GameTime()>30*60) then SellSpecifiedItem("item_branches") SellSpecifiedItem("item_bottle") SellSpecifiedItem("item_magic_wand") SellSpecifiedItem("item_magic_stick") SellSpecifiedItem("item_urn_of_shadows") SellSpecifiedItem("item_drums_of_endurance") end end信使控制信使又称为小鸡，一直都是dota中有很大用处的单位，一直以来都是各方神圣追杀的对象。但是因为7.00版本刚出时，默认AI的信使控制机制有些问题，不知道现在修复了没有。而且为了与我们之前所写的神秘商店控制系统配合，所以需要重写信使控制模块。在前文中我们提到过，信使控制的函数位于ability_item_usage_generic.lua的CourierUsageThink()中，所以我们只要在这个文件中重写此函数，便能完全地控制信使。信使有以下几种状态和命令，参见开发者维基。通过ActionImmediate_Courier( hCourier, nAction ) 函数命令信使，int GetCourierState( hCourier )函数则能获取信使的状态。Courier Actions and States COURIER_ACTION_BURST --加速 COURIER_ACTION_ENEMY_SECRET_SHOP --前往敌方的神秘商店 COURIER_ACTION_RETURN --返回基地 COURIER_ACTION_SECRET_SHOP --前往神秘商店 COURIER_ACTION_SIDE_SHOP --前往边路商店 COURIER_ACTION_SIDE_SHOP2 --前往边路商店 COURIER_ACTION_TAKE_STASH_ITEMS --拾起物品 COURIER_ACTION_TAKE_AND_TRANSFER_ITEMS --拾起并运送物品 COURIER_ACTION_TRANSFER_ITEMS --运送物品 COURIER_STATE_IDLE --空闲 COURIER_STATE_AT_BASE --处于基地 COURIER_STATE_MOVING --移动 COURIER_STATE_DELIVERING_ITEMS --运送物品 COURIER_STATE_RETURNING_TO_BASE --返回基地 COURIER_STATE_DEAD --信使已死亡 所以信使控制模块需要先检测信使的状态，然后再根据英雄是否有装备需要运送来决定信使的行为。以下便是信使控制主函数function CourierUsageThink() local npcBot=GetBot() local courier=GetCourier(0) --获取信使句柄 if(npcBot:IsAlive()==false or courier==nil or npcBot:IsHero()==false or npcBot:HasModifier("modifier_arc_warden_tempest_double")) --判断使用者是不是真正的英雄 then return end local state=GetCourierState(courier) --获取信使状态 local burst=courier:GetAbilityByName("courier_burst") local CanCastBurst=burst~=nil and burst:IsFullyCastable() --检查信使加速能否使用 if(state == COURIER_STATE_DEAD) --信使已死亡 then return end if(courier:GetHealth()/courier:GetMaxHealth()<=0.9) --信使受到攻击，立刻回家 then if(CanCastBurst) then npcBot:ActionImmediate_Courier(courier, COURIER_ACTION_BURST) end npcBot:ActionImmediate_Courier(courier, COURIER_ACTION_RETURN) return end if(state == COURIER_STATE_IDLE) --信使处于空闲状态10秒以上则回家 then if(courier.idletime==nil) then courier.idletime=GameTime() else if(GameTime()-courier.idletime>10) then npcBot:ActionImmediate_Courier(courier, COURIER_ACTION_RETURN) courier.idletime=nil return end end end if(state == COURIER_STATE_RETURNING_TO_BASE and npcBot:GetCourierValue()>0 and not utility.IsItemSlotsFull()) --运送物品 then npcBot:ActionImmediate_Courier(courier, COURIER_ACTION_TRANSFER_ITEMS) return end if (state == COURIER_STATE_AT_BASE ) --从基地运送 then if( npcBot:GetStashValue() >= 400) then if(courier.time==nil) then courier.time=DotaTime() end if(courier.time+1<DotaTime()) then npcBot:ActionImmediate_Courier(courier, COURIER_ACTION_TAKE_AND_TRANSFER_ITEMS) courier.time=nil end return end end if(state == COURIER_STATE_AT_BASE or state == COURIER_STATE_RETURNING_TO_BASE) --前往神秘商店 then if(npcBot.secretShopMode == true and npcBot:GetActiveMode() ~= BOT_MODE_SECRET_SHOP) then npcBot:ActionImmediate_Courier(courier, COURIER_ACTION_SECRET_SHOP) return end end if(state == COURIER_STATE_DELIVERING_ITEMS) --当信使正在运送物品时加速 then if(CanCastBurst) then npcBot:ActionImmediate_Courier(courier, COURIER_ACTION_BURST) end end end最后需要每个英雄调用主函数。例如ability_item_usage_zuus.lua。function CourierUsageThink() ability_item_usage_generic.CourierUsageThink() end示例为宙斯配置的出装：ability_item_usage_zuus.luarequire( GetScriptDirectory().."/item_purchase_generic" ) local ItemsToBuy = { "item_tango", "item_clarity", "item_branches", "item_branches", "item_faerie_fire", "item_bottle", "item_boots", "item_energy_booster", --秘法鞋 "item_mantle", "item_circlet", "item_recipe_null_talisman", --无用挂件 "item_mantle", "item_circlet", "item_recipe_null_talisman", --无用挂件 "item_helm_of_iron_will", "item_recipe_veil_of_discord", --纷争 "item_void_stone", "item_energy_booster", "item_recipe_aether_lens", --以太之镜7.06 "item_staff_of_wizardry", "item_void_stone", "item_recipe_cyclone", "item_wind_lace", --风杖 "item_point_booster", "item_staff_of_wizardry", "item_ogre_axe", "item_blade_of_alacrity", --蓝杖 "item_point_booster", "item_vitality_booster", "item_energy_booster", "item_mystic_staff", --玲珑心 } function ItemPurchaseThink() purchase.ItemPurchase(ItemsToBuy) --将出装表传至主函数 end总结看完了这篇文章后，大家应该能够了解AI出装的相关功能了，虽然说这篇文章因为作者太懒拖了很久，不过还是要比V社好一点。V社说好的战役第二章迟迟没有推出，即将重新定义“七月下旬”。下面上张图。附件1.示例代码
原来以为MySql从删库到跑路只是句笑话，没想到是真的。这学期才学数据库原理，以前人们总是说数据要经常备份，不然哪一天就会后悔，果然昨天我就碰到了这么倒霉的情况...昨天晚上没事做，发现typecho后台文章cid的变化有些奇怪，按道理来说主键自增不会出现突变，可是cid有时却从50多跳跃到了90。于是我便在百度上找到了这个坑爹帖子。本来想着是优化一下数据库表的结构，直到运行了这个sql语句后TRUNCATE TABLE typecho_contents，突然发现，咦？为什么查询不到结果了？？？原来数据库的一个表被我删掉了。当时就急了，这个作者什么都没提醒啊，我之前也没用过这个语句，根本就不知道这有什么用途，还以为只是优化一下表呢。百度一查，这个TRUNCATE语句就意味着DROP+CREATE...不仅删掉表，还重建了表。如果只是删掉了表，那么还可以通过闪回操作恢复，但这样之后就没办法恢复了。当时瞬间整个人就懵逼了，我从来没有手动备份过，不知道有没有自动备份。检查了一下，不仅没有自动备份，连事务日志都没有。正常的数据库级恢复方法当然就没有用了，只好跑去腾讯云那里发了个工单，看他们有没有自动备份硬盘。我真佩服腾讯的工程师，凌晨居然都还有值班的，回复我说：需要自己手动快照才能备份。好吧，这下就彻底GG了，之前的博客文章都是直接在typecho里写的，电脑上也没有备份，这么久的心血难道就毁于一旦了吗？还好想起了搜索引擎快照这个东西，百度一下果然没有，谷歌上一搜就找到了，赶紧连夜从谷歌快照上恢复了这么多博客文章。所以说数据库一定要备份啊！不仅需要数据库级的备份，也需要异机、异地备份，以防万一，不然就只能从删库到跑路了...PS：真正的修改方法是：alter table typecho_contents AUTO_INCREMENT=100;
Dota2 AI 简易开发教程（一）——选择阵容及技能使用
在第一篇教程中，我们主要介绍了Dota2 AI的基本情况，在这篇文章中，我们将介绍如何为AI选择阵容和技能使用（以宙斯为例）。选择阵容在官方开发者维基中有着这样的说明：如果你想控制英雄选择和分路，你可以在文件名为hero_selection.lua的文件中实现如下函数：Think() - 每帧被调用。负责机器人选择英雄。 UpdateLaneAssignments() - 在游戏开始前的每一帧被调用。返回玩家ID-分路的值对。 GetBotNames() - 调用一次，返回一个包含玩家名称的表。 在dota 2 beta\game\dota\scripts\vscripts\bots_exampleV社的示例文件中，我们可以找到hero_selection.lua文件，这就是为AI选择阵容的脚本文件。如果我们要开始制作自己的AI，那么首先需要创建dota 2 beta\game\dota\scripts\vscripts\bots文件夹，之后建立hero_selection.lua文件，在其中编写选择阵容的部分，这样的话AI就会选择自定义阵容了。打开这个文件，我们可以看到如下内容 function Think() if ( GetTeam() == TEAM_RADIANT ) --处于天辉方时 then print( "selecting radiant" ); --在控制台输出调试信息 SelectHero( 0, "npc_dota_hero_antimage" ); --为0号玩家选择敌法师 SelectHero( 1, "npc_dota_hero_axe" ); SelectHero( 2, "npc_dota_hero_bane" ); SelectHero( 3, "npc_dota_hero_bloodseeker" ); SelectHero( 4, "npc_dota_hero_crystal_maiden" ); elseif ( GetTeam() == TEAM_DIRE ) then print( "selecting dire" ); SelectHero( 5, "npc_dota_hero_drow_ranger" ); SelectHero( 6, "npc_dota_hero_earthshaker" ); SelectHero( 7, "npc_dota_hero_juggernaut" ); SelectHero( 8, "npc_dota_hero_mirana" ); SelectHero( 9, "npc_dota_hero_nevermore" ); end end其中Think()是主函数，游戏选择阵容时就会调用这个函数，然后判断是天辉方还是夜魇方，之后分别选择各自的阵容。那么我们如何找到英雄对应的名字呢？在这里便可找到英雄的实际名称。例如宙斯的实际名称是"npc_dota_hero_zuus"，那么我们将夜魇方的SelectHero( 5, "npc_dota_hero_drow_ranger" );替换为SelectHero( 5, "npc_dota_hero_zuus" );，那么AI便会选择在5号玩家的位置上选择宙斯。需要注意的是，玩家的ID是会随着人类玩家的加入而发生改变的，将玩家ID硬编码是一种不好的做法，比较好的方法是通过循环遍历出玩家的ID，这样无论人类玩家有多少，AI都能正常选择阵容。下面便是能够自动适应玩家变化选择阵容的代码。function Think() for i,id in pairs(GetTeamPlayers(GetTeam())) --Lua语言自带的迭代器pairs，能够依次读取表中的值。 do if(IsPlayerBot(id)) --判断该玩家是否为电脑 then SelectHero( id, "npc_dota_hero_drow_ranger" ); --为id号玩家选择黑暗游侠 end end end如果我们要AI能够选择随机的阵容，而不是仅仅选择10个黑暗游侠，那该怎么办呢？很简单，通过Lua语言唯一的数据结构表(称作Table，可以用作数组)保存可以选择的英雄列表，然后随机出一个序号，选出表中的英雄。hero_pool_my= { 'npc_dota_hero_zuus', 'npc_dota_hero_skywrath_mage', 'npc_dota_hero_ogre_magi', 'npc_dota_hero_chaos_knight', 'npc_dota_hero_viper', "npc_dota_hero_lina", "npc_dota_hero_kunkka", "npc_dota_hero_lich", "npc_dota_hero_shadow_shaman", "npc_dota_hero_centaur", 'npc_dota_hero_crystal_maiden', } function Think() for i,id in pairs(GetTeamPlayers(GetTeam())) do if(IsPlayerBot(id) and (GetSelectedHeroName(id)=="" or GetSelectedHeroName(id)==nil)) then local num=hero_pool_my[RandomInt(1, #hero_pool_my)] --取随机数 SelectHero( id, num ); --在保存英雄名称的表中，随机选择出AI的英雄 table.remove(hero_pool_my,num) --移除这个英雄 end end end以上代码会为两边的AI依次从hero_pool_my表中随机选出5个英雄。如果要让AI不选择重复的英雄，那么还需要进一步的修改。选择分路为AI分路的函数也是在hero_selection.lua中，如果我们想让AI遵循自定义的分路，那么需要在这个文件里新建一个UpdateLaneAssignments()函数。这个函数是用于游戏底层C++端调用，所以需要返回一个分路情况的表。function UpdateLaneAssignments() local npcBot = GetBot() local lanes= { [1]=LANE_MID, [2]=LANE_BOT, [3]=LANE_TOP, [4]=LANE_BOT, [5]=LANE_TOP, } return lanes; end以上函数根据AI的位置分路，1楼中路，2楼下路，3楼上路，4楼下路，5楼上路。当然，如果要根据英雄的特性分路，那么需要加入更多判断。如果没有自己写好分路系统的话，可以暂时启用默认分路系统，即不编写这个函数。技能加点在官方开发者维基中有着这样的说明：如果你只想重写在技能和物品使用时的决策，你可以在文件名为ability_item_usage_generic.lua的文件中实现如下函数:ItemUsageThink() - 每帧被调用。负责发出物品使用行为。 AbilityUsageThink() - 每帧被调用。负责发出技能使用行为。 CourierUsageThink() - 每帧被调用。负责发出信使的相关命令。 BuybackUsageThink() - 每帧被调用。负责发出买活指令。 AbilityLevelUpThink() - 每帧被调用。负责升级技英雄能。这些函数中未被重写的，会自动采用默认的C++实现。你也可以仅重写某个特定英雄对技能/物品使用的逻辑，比如火女(Lina),写在文件名为ability_item_usage_lina.lua的文件中。如果你想在特定英雄对技能/物品使用的逻辑重写时调用泛用的逻辑代码，请参考附件A的实现细节。我们要实现的第一个英雄为宙斯，那么就可以新建一个ability_item_usage_zuus.lua的文件。首先要实现AI的加点，就要在此文件中编写AbilityLevelUpThink()函数。然后通过 npcBot:ActionImmediate_LevelAbility(abilityname);升级名称为abilityname的技能。那么怎样获取升级技能所需的技能名称呢？比较原始的方法是用一个表记录从1到25级所有的技能名称。其中的数据要在dota 2 beta\game\dota\scripts\npc\npc_heroes.txt中获取。例如宙斯的技能为 --以下为宙斯的技能名称 "zuus_arc_lightning", "zuus_lightning_bolt", "zuus_static_field", "zuus_cloud", "zuus_thundergods_wrath", --以下为某个版本的天赋，请注意天赋名称会随着版本更新而变化，所以以下部分可能在当前版本中无效 "special_bonus_mp_regen_2", "special_bonus_movement_speed_25", "special_bonus_armor_7", "special_bonus_magic_resistance_15", "special_bonus_unique_zeus_2", "special_bonus_unique_zeus_3", "special_bonus_cast_range_200", "special_bonus_unique_zeus",如果通过原始方法记录，则需要排列组合以上的技能天赋。比较好的方法则是用两个表分别保存技能和天赋的名称，然后在一个统一的表中引用技能天赋名称，以存储加点顺序。local Talents = { "special_bonus_mp_regen_2", "special_bonus_movement_speed_25", "special_bonus_armor_7", "special_bonus_magic_resistance_15", "special_bonus_unique_zeus_2", "special_bonus_unique_zeus_3", "special_bonus_cast_range_200", "special_bonus_unique_zeus", } local Abilities = { "zuus_arc_lightning", "zuus_lightning_bolt", "zuus_static_field", "zuus_cloud", "zuus_thundergods_wrath", } --存储技能句柄，方便调用 local npcBot = GetBot() local AbilitiesReal = { npcBot:GetAbilityByName(Abilities[1]), npcBot:GetAbilityByName(Abilities[2]), npcBot:GetAbilityByName(Abilities[3]), npcBot:GetAbilityByName(Abilities[4]), npcBot:GetAbilityByName(Abilities[5]) } --存储加点顺序 local AbilityToLevelUp= { Abilities[1], Abilities[3], Abilities[2], Abilities[2], Abilities[2], Abilities[5], Abilities[2], Abilities[1], Abilities[1], Talents[1], Abilities[1], Abilities[5], Abilities[3], Abilities[3], Talents[3], Abilities[3], Abilities[5], Talents[5], Talents[7], }至此，英雄的技能天赋名称和加点顺序都已完成存储，接下来只需要在函数中引用加点顺序即可完成加点。function AbilityLevelUpThink() local npcBot = GetBot(); if npcBot:GetAbilityPoints()<1 or #AbilityToLevelUp==0--当没有技能点时，或没有技能可升时不继续执行。 then return; end local ability=npcBot:GetAbilityByName(AbilityToLevelUp[1]); --获取技能句柄 if ability~=nil and ability:CanAbilityBeUpgraded() --判断技能可否升级 then npcBot:ActionImmediate_LevelAbility(AbilityToLevelUp[1]); --升级技能 table.remove( AbilityToLevelUp, 1 ); --从表中移除刚刚升级的技能 end end我们之前说过，天赋名称会随着版本更新而不断变化，而且每次手动从npc_heroes.txt中获取技能天赋名称也有点麻烦，那么有什么更简便的方法呢？当然有，比如通过遍历英雄的技能槽来获取每一个技能和天赋的名称。local Talents ={} local Abilities ={} local npcBot = GetBot() for i=0,23,1 do --由于卡尔有24个技能，所以需要从0遍历至23 local ability=npcBot:GetAbilityInSlot(i) if(ability~=nil) then if(ability:IsTalent()==true) then table.insert(Talents,ability:GetName()) else table.insert(Abilities,ability:GetName()) end end end这样的话，我们每次新编写一个英雄，就不用手动去复制技能名称了。使用技能示例代码如果要重新编写一个英雄的技能使用，则需要编写AbilityUsageThink()函数。打开示例文件ability_item_usage_lina.lua，我们可以看到如下代码castLBDesire = 0; castLSADesire = 0; castDSDesire = 0; function AbilityUsageThink() local npcBot = GetBot(); -- Check if we're already using an ability 检查我们是否正在使用技能 if ( npcBot:IsUsingAbility() ) then return end; abilityLSA = npcBot:GetAbilityByName( "lina_light_strike_array" ); abilityDS = npcBot:GetAbilityByName( "lina_dragon_slave" ); abilityLB = npcBot:GetAbilityByName( "lina_laguna_blade" ); -- Consider using each ability 考虑使用每一个技能，得到使用技能的欲望值和技能目标 castLBDesire, castLBTarget = ConsiderLagunaBlade(); castLSADesire, castLSALocation = ConsiderLightStrikeArray(); castDSDesire, castDSLocation = ConsiderDragonSlave(); if ( castLBDesire > castLSADesire and castLBDesire > castDSDesire ) then npcBot:Action_UseAbilityOnEntity( abilityLB, castLBTarget ); return; end if ( castLSADesire > 0 ) then npcBot:Action_UseAbilityOnLocation( abilityLSA, castLSALocation ); return; end if ( castDSDesire > 0 ) then npcBot:Action_UseAbilityOnLocation( abilityDS, castDSLocation ); return; end end ---------------------------------------------------------------------------------------------------- --判断技能能否对目标使用 function CanCastLightStrikeArrayOnTarget( npcTarget ) return npcTarget:CanBeSeen() and not npcTarget:IsMagicImmune() and not npcTarget:IsInvulnerable(); end function CanCastDragonSlaveOnTarget( npcTarget ) return npcTarget:CanBeSeen() and not npcTarget:IsMagicImmune() and not npcTarget:IsInvulnerable(); end function CanCastLagunaBladeOnTarget( npcTarget ) return npcTarget:CanBeSeen() and npcTarget:IsHero() and ( GetBot():HasScepter() or not npcTarget:IsMagicImmune() ) and not npcTarget:IsInvulnerable(); end ---------------------------------------------------------------------------------------------------- --考虑是否使用2技能 function ConsiderLightStrikeArray() local npcBot = GetBot(); -- Make sure it's castable 确认技能能否使用 if ( not abilityLSA:IsFullyCastable() ) then return BOT_ACTION_DESIRE_NONE, 0; end; -- If we want to cast Laguna Blade at all, bail 如果要使用大招，那么先不用这个技能 if ( castLBDesire > 0 ) then return BOT_ACTION_DESIRE_NONE, 0; end -- Get some of its values 获取技能相关的参数 local nRadius = abilityLSA:GetSpecialValueInt( "light_strike_array_aoe" ); local nCastRange = abilityLSA:GetCastRange(); local nDamage = abilityLSA:GetAbilityDamage(); -------------------------------------- -- Global high-priorty usage -------------------------------------- -- Check for a channeling enemy 打断正在吟唱的敌人 local tableNearbyEnemyHeroes = npcBot:GetNearbyHeroes( nCastRange + nRadius + 200, true, BOT_MODE_NONE ); for _,npcEnemy in pairs( tableNearbyEnemyHeroes ) do if ( npcEnemy:IsChanneling() ) then return BOT_ACTION_DESIRE_HIGH, npcEnemy:GetLocation(); end end -------------------------------------- -- Mode based usage -------------------------------------- -- If we're farming and can kill 3+ creeps with LSA 收兵线打钱 if ( npcBot:GetActiveMode() == BOT_MODE_FARM ) then local locationAoE = npcBot:FindAoELocation( true, false, npcBot:GetLocation(), nCastRange, nRadius, 0, nDamage ); if ( locationAoE.count >= 3 ) then return BOT_ACTION_DESIRE_LOW, locationAoE.targetloc; end end -- If we're pushing or defending a lane and can hit 4+ creeps, go for it 推进与防守 if ( npcBot:GetActiveMode() == BOT_MODE_PUSH_TOWER_TOP or npcBot:GetActiveMode() == BOT_MODE_PUSH_TOWER_MID or npcBot:GetActiveMode() == BOT_MODE_PUSH_TOWER_BOT or npcBot:GetActiveMode() == BOT_MODE_DEFEND_TOWER_TOP or npcBot:GetActiveMode() == BOT_MODE_DEFEND_TOWER_MID or npcBot:GetActiveMode() == BOT_MODE_DEFEND_TOWER_BOT ) then local locationAoE = npcBot:FindAoELocation( true, false, npcBot:GetLocation(), nCastRange, nRadius, 0, 0 ); if ( locationAoE.count >= 4 ) then return BOT_ACTION_DESIRE_LOW, locationAoE.targetloc; end end -- If we're seriously retreating, see if we can land a stun on someone who's damaged us recently 逃跑时阻止敌人追击 if ( npcBot:GetActiveMode() == BOT_MODE_RETREAT and npcBot:GetActiveModeDesire() >= BOT_MODE_DESIRE_HIGH ) then local tableNearbyEnemyHeroes = npcBot:GetNearbyHeroes( nCastRange + nRadius + 200, true, BOT_MODE_NONE ); for _,npcEnemy in pairs( tableNearbyEnemyHeroes ) do if ( npcBot:WasRecentlyDamagedByHero( npcEnemy, 2.0 ) ) then if ( CanCastLightStrikeArrayOnTarget( npcEnemy ) ) then return BOT_ACTION_DESIRE_MODERATE, npcEnemy:GetLocation(); end end end end -- If we're going after someone 对当前目标使用技能 if ( npcBot:GetActiveMode() == BOT_MODE_ROAM or npcBot:GetActiveMode() == BOT_MODE_TEAM_ROAM or npcBot:GetActiveMode() == BOT_MODE_GANK or npcBot:GetActiveMode() == BOT_MODE_DEFEND_ALLY ) then local npcTarget = npcBot:GetTarget(); if ( npcTarget ~= nil ) then if ( CanCastLightStrikeArrayOnTarget( npcTarget ) ) then return BOT_ACTION_DESIRE_HIGH, npcTarget:GetLocation(); end end end return BOT_ACTION_DESIRE_NONE, 0; --不使用技能 end function ConsiderDragonSlave() --略，考虑是否使用1技能 end function ConsiderLagunaBlade() --略，考虑是否使用4技能 end 以上便是原版AI的火女技能使用源代码，当然这是由C++代码翻译过来的。我们要编写的代码结构也可以参考这一部分，主要分为3个部分：主思考函数判断技能能否对目标使用考虑每一个技能调用关系为：主思考函数->考虑每一个技能->判断技能能否对目标使用实战在看完了示例代码后，我们应该对英雄的技能使用有了一个初步的了解，那么接下来便介绍一下如何从零开始开发一个新的英雄。（以宙斯为例）主思考函数首先先编写主思考函数，每一个英雄的主思考函数都是相似的，只需要处理每个技能不同类型的目标即可。在函数开头用local npcBot = GetBot()记录此英雄的句柄，方便后面的调用。之后需要判断英雄能否使用技能，是正在施法，还是被沉默了。 if ( npcBot:IsUsingAbility() or npcBot:IsChanneling() or npcBot:IsSilenced() ) then return end然后获取一些常用的量 AttackRange=npcBot:GetAttackRange() --获取攻击距离 ManaPercentage=npcBot:GetMana()/npcBot:GetMaxMana() --获取当前魔法百分比 HealthPercentage=npcBot:GetHealth()/npcBot:GetMaxHealth() --获取当前生命百分比然后考虑每一个技能的使用，Lua语言支持多重返回值，所以能够方便地从函数获取结果。 cast1Desire, cast1Target = Consider1(); --1技能为单位目标技能 cast2Desire, cast2Target, cast2TargetType = Consider2(); --2技能为单位目标技能或地点目标技能 cast5Desire = Consider5(); --大招为无目标技能 cast4Desire, cast4Location = Consider4(); --5技能（A杖技能）是地点目标技能最后使用每个技能，从上到下，为技能使用的优先级。当然我们也可以通过比较每个技能的使用欲望，选择欲望值最高的技能使用。 if ( cast4Desire > 0 ) then npcBot:Action_UseAbility( AbilitiesReal[4] ); --使用大招 return end if ( cast5Desire > 0 ) then npcBot:Action_UseAbilityOnLocation( AbilitiesReal[5], cast5Location ); --对目标点使用5技能 return end if ( cast2Desire > 0 ) then if(cast2TargetType=="target") then npcBot:Action_UseAbilityOnEntity( AbilitiesReal[2], cast2Target ); --对单位使用2技能 return elseif(cast2TargetType=="location") then npcBot:Action_UseAbilityOnLocation( AbilitiesReal[2], cast2Target ); --对目标点使用2技能 return end end if ( cast1Desire > 0 ) then npcBot:Action_UseAbilityOnEntity(AbilitiesReal[1], cast1Target ); --对目标单位使用1技能 return end至此，主思考函数编写完毕。考虑使用每一个技能在开始开发一个技能的使用前，我们应该先想想，这个技能有什么用？是一个单体爆发技能，还是一个范围技能？这个技能有没有控制效果？这样的话才好编写接下来的技能。比如说宙斯的1技能，可以用来补兵，清兵，对英雄造成范围伤害等等。首先我们也需要获取英雄和技能的句柄。在函数开头用local npcBot = GetBot()记录此英雄的句柄，用local ability=AbilitiesReal[1];记录此技能的句柄。然后需要判断这个技能能否使用，即魔法够不够，冷却有没有好。 if not ability:IsFullyCastable() then return BOT_ACTION_DESIRE_NONE, 0; end之后也是记录一些常用的量，以免重复获取降低效率。 local CastRange = ability:GetCastRange(); --获取施法距离 local Damage = ability:GetAbilityDamage(); --获取技能伤害，注意这个函数不一定有效...有些特殊的技能无法获取 local HeroHealth=10000 local CreepHealth=10000 local allys = npcBot:GetNearbyHeroes( 1200, false, BOT_MODE_NONE ); --获取周围友方英雄 local enemys = npcBot:GetNearbyHeroes(CastRange+300,true,BOT_MODE_NONE) --获取周围敌方英雄 local WeakestEnemy,HeroHealth=utility.GetWeakestUnit(enemys) --获取血量最低的敌方英雄 local creeps = npcBot:GetNearbyCreeps(CastRange+300,true) --获取周围的敌方小兵 local WeakestCreep,CreepHealth=utility.GetWeakestUnit(creeps) --获取血量最低的敌方小兵注意，在这个地方我调用了utility函数库中的函数，调用函数库模块，需要在脚本开头加入require(GetScriptDirectory() .. "/utility")。当然这个utility名字是可以变化的。该函数的实际代码放在文末。1.之后，便是宙斯1技能的实际用途，我分为了5个部分：团战使用线上正补发育打钱推进防守通用function Consider1()(1) 团战使用当英雄模式处于攻击时，判断蓝量是否足够，敌方单位数量是否足够，然后找到最脆弱的敌方单位或者英雄，判断它们能不能被技能作用，最后将这个单位的句柄返回给上层主思考函数。--teamfightUsing if(npcBot:GetActiveMode() == BOT_MODE_ATTACK ) then if(ManaPercentage>0.4 and #enemys+#creeps>2) --蓝量大于40%，敌方单位数>2 then if(WeakestCreep~=nil and CanCast1( WeakestCreep )) --CanCast1是一个自定义函数，用于判断技能能不能对目标使用，请参考下一部分。 then return BOT_ACTION_DESIRE_HIGH,WeakestCreep; end if(WeakestEnemy~=nil and CanCast1( WeakestEnemy )) then return BOT_ACTION_DESIRE_HIGH,WeakestEnemy; end end end(2) 线上正补 --Last hit if ( npcBot:GetActiveMode() == BOT_MODE_LANING ) then if(WeakestCreep~=nil) then if(ManaPercentage>0.4 and GetUnitToUnitDistance(npcBot,WeakestCreep)>=AttackRange-ManaPercentage) --蓝量大于40%，而且该单位远离英雄的攻击距离 then if(CreepHealth<=WeakestCreep:GetActualIncomingDamage(Damage,DAMAGE_TYPE_MAGICAL)) --伤害是否足够 then return BOT_ACTION_DESIRE_LOW,WeakestCreep; end end end end(3) 发育打钱-- If we're farming and can hit 2+ creeps and kill 1+ if ( npcBot:GetActiveMode() == BOT_MODE_FARM ) then if ( #creeps >= 2 )--单位是不是足够多 then if (CreepHealth<=WeakestCreep:GetActualIncomingDamage(Damage,DAMAGE_TYPE_MAGICAL))--伤害是否足够 then return BOT_ACTION_DESIRE_LOW, WeakestCreep; end end end(4) 推进防守 -- If we're pushing or defending a lane and can hit 3+ creeps, go for it if ( npcBot:GetActiveMode() == BOT_MODE_PUSH_TOWER_TOP or npcBot:GetActiveMode() == BOT_MODE_PUSH_TOWER_MID or npcBot:GetActiveMode() == BOT_MODE_PUSH_TOWER_BOT or npcBot:GetActiveMode() == BOT_MODE_DEFEND_TOWER_TOP or npcBot:GetActiveMode() == BOT_MODE_DEFEND_TOWER_MID or npcBot:GetActiveMode() == BOT_MODE_DEFEND_TOWER_BOT ) then if ( #enemys+#creeps > 2 and ManaPercentage>0.4)--蓝量大于40%，敌方单位数>2 then if (WeakestEnemy~=nil) then if ( CanCast1( WeakestEnemy )and GetUnitToUnitDistance(npcBot,WeakestEnemy)< CastRange + 75*#allys ) --判断目标，判断英雄的位置是否安全 then return BOT_ACTION_DESIRE_LOW, WeakestEnemy; end end if (WeakestCreep~=nil) then if ( CanCast1( WeakestCreep )and GetUnitToUnitDistance(npcBot,WeakestCreep)< CastRange + 75*#allys ) then return BOT_ACTION_DESIRE_LOW, WeakestCreep; end end end end(5) 通用对模式决定的当前目标使用技能-- If we're going after someone if ( npcBot:GetActiveMode() == BOT_MODE_ROAM or npcBot:GetActiveMode() == BOT_MODE_TEAM_ROAM or npcBot:GetActiveMode() == BOT_MODE_DEFEND_ALLY or npcBot:GetActiveMode() == BOT_MODE_ATTACK ) then local npcTarget = npcBot:GetTarget(); if(ManaPercentage>0.4) then if ( npcTarget ~= nil ) then if ( CanCast1( npcTarget ) and GetUnitToUnitDistance(npcBot,npcTarget)< CastRange + 75*#allys) --距离是不是足够近 then return BOT_ACTION_DESIRE_MODERATE, npcTarget; end end end end(6)如果上面的部分没有获取到合适的目标，返回欲望0，和空单位，以防出现错误。 return BOT_ACTION_DESIRE_NONE, 0;2.宙斯2技能与之前类似的部分便不再详细指出。(1)打断目标本技能有打断功能，应打断敌方英雄的吟唱。 -- Check for a channeling enemy for _,enemy in pairs( enemys ) do if ( enemy:IsChanneling() and CanCast2( enemy )) then return BOT_ACTION_DESIRE_HIGH, enemy,"target"; --由于本技能有多种目标类型，可以返回类型是什么。 end end(2)击杀附近可以击杀的敌人 -- Kill enemy if(npcBot:GetActiveMode() ~= BOT_MODE_RETREAT ) then if (WeakestEnemy~=nil) then if(HeroHealth<=WeakestEnemy:GetActualIncomingDamage(Damage,DAMAGE_TYPE_MAGICAL)) --伤害是否足够 then if ( CanCast2( WeakestEnemy ) ) then return BOT_ACTION_DESIRE_HIGH,WeakestEnemy,"target"; end end end end(3)保护自己，消耗靠近英雄的敌人 --protect myself local enemys2 = npcBot:GetNearbyHeroes( 500, true, BOT_MODE_NONE ); --获取500范围内的敌人 if(npcBot:WasRecentlyDamagedByAnyHero(5)) --最近是否被敌人伤害 then for _,enemy in pairs( enemys2 ) do if ( CanCast2( enemy ) ) then return BOT_ACTION_DESIRE_HIGH, enemy,"target" end end end(4)消耗敌方英雄 if(npcBot:GetActiveMode() ~= BOT_MODE_RETREAT ) if(ManaPercentage>0.4 and ability:GetLevel()>=2 ) --蓝量大于40%，技能大于或等于2级 then if (WeakestEnemy~=nil and CanCast2( WeakestEnemy )) then return BOT_ACTION_DESIRE_LOW,WeakestEnemy,"target"; end end end3.宙斯大招检测所有敌方英雄的血量，如果能够击杀，且附近没有友方英雄，那么则使用技能。详细代码略。4.宙斯雷云放在防御塔附近，或敌方英雄附近。 local tower=nil if ( npcBot:GetActiveMode() == BOT_MODE_PUSH_TOWER_TOP or npcBot:GetActiveMode() == BOT_MODE_PUSH_TOWER_MID or npcBot:GetActiveMode() == BOT_MODE_PUSH_TOWER_BOT ) then local tower=npcBot:GetNearbyTowers(1500,false) end if (npcBot:GetActiveMode() == BOT_MODE_DEFEND_TOWER_TOP or npcBot:GetActiveMode() == BOT_MODE_DEFEND_TOWER_MID or npcBot:GetActiveMode() == BOT_MODE_DEFEND_TOWER_BOT ) then local tower=npcBot:GetNearbyTowers(1000,true) end if(tower~=nil and GetUnitToUnitDistance(npcBot,tower[1])<CastRange+500) then return BOT_ACTION_DESIRE_LOW,tower[1]:GetLocation() end if (WeakestEnemy~=nil) then return BOT_ACTION_DESIRE_LOW,WeakestEnemy:GetLocation() end 判断技能能否对目标使用其实这部分很简单，就是判断目标是否魔法免疫，是否无敌之类的，以防止英雄重复尝试对无效目标使用技能。function CanCast1( npcTarget ) return npcTarget:CanBeSeen() and not npcTarget:IsMagicImmune() and not npcTarget:IsInvulnerable(); end function CanCast2( npcTarget ) return not npcTarget:IsMagicImmune() and not npcTarget:IsInvulnerable(); end function CanCast5( npcTarget ) return npcTarget:IsHero() and not npcTarget:IsMagicImmune() and not npcTarget:IsInvulnerable(); end至此，我们已经完成了一个英雄的技能使用设计，详细代码请参考附件。参考通用函数utility.GetWeakestUnit(EnemyUnits)函数function GetWeakestUnit(EnemyUnits) if EnemyUnits==nil or #EnemyUnits==0 then return nil,10000; end local WeakestUnit=nil; local LowestHealth=10000; for _,unit in pairs(EnemyUnits) do if unit~=nil and unit:IsAlive() then if unit:GetHealth()<LowestHealth then LowestHealth=unit:GetHealth(); WeakestUnit=unit; end end end return WeakestUnit,LowestHealth end总结总算写完了，真的是很久没有写这么长一篇文章了，希望大家能有所启发。在下一篇文章中，我会为大家介绍如何配置AI出装。附件所有代码已测试运行通过1.示例代码
经过一段时间的测试，本站已经成功启用了 https。现在，本站与用户之间的通信已经加密，能有效保护用户与网站间的通信安全。虽然很早前就知道了 https 是怎么一回事，但之前怕麻烦就没有部署，但是根据这次的经验，启用 https 也不是很难。下面我就介绍一下本站启用 https 协议的经验。申请 SSL 证书要启用 https 协议，首先需要申请一个由数字证书授权机构颁发的 SSL 证书。由于本站部署于腾讯云上，便在这里找到了免费的 SSL 证书申请。申请过程也很简单，填写信息后很快就收到了由 Trust Asia 颁发的免费域名 SSL 证书。部署 SSL 证书在申请证书通过后，下载证书到服务器上，之后按照腾讯云的教程便轻松安装。由于用户一般是不会主动在链接上加上 https 的，所以需要将 http 请求重定向至 https。由于本站使用的服务器是 IIS，便按照教程用 URL 重写模块完成了请求重定向。细节处理在启用 https 后，网站内部的链接还是使用的 http 协议，所以需要调整。首先现在 typecho 后台设置的站点地址中，将 http 开头的网址改为 https。之后在 typecho 根目录下的配置文件 config.inc.php 中，添加一行define('__TYPECHO_SECURE__',true);。保存后重启 iis 服务器便可完成启用。再重新进入网站主界面时，浏览器提示访问的页面有一部分是不安全的，这是因为网页上混合了 https 和 http 的元素。我开始也不清楚是哪个地方引用了 http 元素，经过检查发现 cnzz 的统计代码是引用了 http 来源的 JS 脚本，将链接改为 https 后便解决了。总之，启用 https 不仅能提高站点的安全性，更能让站点看起来高大上一些。
人工智能首部诗集之小冰：是你的声音啊
微明的灯影里我知道她的可爱的土壤是我的心灵成为俘虏了我不在我的世界里街上没有一只灯儿舞了是最可爱的你睁开眼睛做起的梦是你的声音啊当然，这首诗不是我写的，而是人工智能创作的。原来从没想到人工智能能做出艺术性的创作，没想到今天也成为了现实。但是小冰所写的诗歌，还是有些稚 嫩，似乎并没有其个人的思想，只是映射出了中国现代诗的集体意识。为了测试小冰的创作水平，微软让小冰化名在报刊、豆瓣、贴吧和天涯等多个网络社区诗歌讨 论区中发布作品。其中用过的笔名有 23 个，我闲来无事，在豆瓣和简书上找到了小冰笔名为 “风的指尖 2017” 的主页。简书 豆瓣曾有人担心，当人工智能取代人类大部分的工作后，我们将何去何从？殊不知，一个行业的衰落总伴随着另一个行业的兴起，当机器人承担了那些枯燥重复的 工作时，就会有很大数量的人们从流水线上解放，那样我们便可以将更多的精力投入到创造性的工作上去。高晓松曾在微博发言忧虑，人工智能都能做出艺术创作 了，那我们人类又和机器有何区别？哈哈，当然有，因为是人创造出了人工智能。人工智能与人之间也许并不是竞争的关系，而是传承与创新。阳光失了玻璃窗微软小冰推出首部诗集：《阳光失了玻璃窗》
前言众所周知，v 社在 7.00 版本推出了自定义机器人功能，我也做了一个 ai 放到了创意工坊上。应广大玩家要求，也为了与大家交流 dota2 ai 的开发技术，我打算写一点浅显易懂的开发教程，希望无论是开发者还是普通玩家都能够理解。dota2 在刚推出时，新闻上就列举了很多 ai 的特性，那时候 ai 可以说是很强。但是由于 v 社天天度假，忽视了 ai 的开发，现在的 ai 还停留在远古版本。所以，为了更好度假！不不不，是为了提升 ai 的水平，便把 ai 开放给玩家制作。IceFrog：Dota2 新增血魔和船长，全新 AI 加入DOTA2 为适应各水平玩家 大幅优化 AI 智能浅谈 DOTA2 AI 打匹配的五大优点和三大缺点Dota2 ai 简介说了这么多，还是回归正题吧，Dota2 的 ai 到底是怎样制作的呢？dota2ai 是用 Lua 脚本语言实现的。Lua 是一种解释型编程语言，如果以前学过编程语言那就很好掌握，没学过也没关系，因为 Lua 语言的语法很简洁，即使是小白也能很快理解。当然，本文不会介绍 Lua 语言的基本语法，如果想进一步学习，可以参考 Lua 教程或者是《Lua 程序设计第二版》。那么，我们应该在什么环境下编写 Lua 脚本呢？很简单，我们没有什么 IDE（集成开发环境）。任何文本编辑器都可以拿来编写，就连记事本也可以！不过还是不推荐使用记事本，因为没有基本的语法高亮等功能，我使用的是 notepad++，一个很简洁的文本编辑器，当然如果你有更好的编辑器也可以直接使用。所有正在开发的 ai 脚本都存放于dota 2 beta\game\dota\scripts\vscripts\bots文件夹下，如果没有 bots 目录便需要自己创建一个。而当你上传你的脚本到工坊时，就会上传该目录下的内容。而下载的脚本则放在你 Steam 安装目录下自己的文件夹中。不出意外的话，你会找到bots_example文件夹，里面有 V 社原版 ai 的一部分。当然，原版 AI 并不是由 lua 编写的，而是由 C++ 编写，而完整的 AI 源码并没有公布。这一部分示例，是由 C++ 翻译为 Lua 的。目前，开发 ai 大致有两种路线，第一种是完全控制，这意味着你需要编写从游戏开始到游戏结束的所有 ai 代码。而第二种则是部分控制，这只需要你接手你想控制的部分，比如某个英雄的技能操作，游走的思路等。前一种工作量很大，需要完全重构 AI 系统。而后一种则比较简单的方法，本系列文章主要是讲解后一种。有的玩家经常反馈：为什么 ai 总是喜欢抱团一起推，一个个送。这也很好解释，因为目前创意工坊里的 ai 大部分都是属于后一种，继承了大部分默认 ai 的思路。至于如何开启一局社区原创机器人游戏，想必大家都已身经百战，不用我说了。不过我还是介绍一下。打开 dota2， 创建房间，选择本地主机，选择 “编辑”，在高级房间设置中，勾选 “机器人填满空位”，随后订阅并使用创意工坊的脚本。如果选择本地开发脚本，那么使用的便是我们在 bots 文件夹下编写的脚本。在接下来的文章中，我会为大家介绍如何开发编辑英雄出装加点，英雄的技能操作，喝神泉模式的实现等等。参考文档：Dota2 AI 开发 tutorial（一）AI 的 环境配置DOTA 机器人脚本 Wiki（英文版更全）Dota2 AI api 说明
近日，国外著名新闻论坛 reddit 的水友们发起了 dota2 BOT Major，也就是一场只有机器人参加的联赛，参赛队伍都来自创意工坊中玩家制作的机器人脚本。这次比赛可以让我们见证有史以来的第一次机器人联赛，也许 在不久的将来，机器人战队也能与人类职业战队一战。Dota 2 Bot Major 赛事总结祝贺 Extrem Push 一波推进流 获得冠军亚军：Ranked Matchmaking AI by:adamqqq 天梯匹配模拟器季军：Alchemist and His Four Friends 炼金术士和他的 4 个小伙伴如果你不熟悉本次联赛，你可以在这里找到更多的信息决赛视频：GAME 1 ,GAME 2 ,GAME 3对不起，游戏中的镜头和界面有些问题，请不要责备我和解说们，我想这是 V 社的问题。在我看来，这是一个很好的比赛。小组赛打得并不是很好，只有一个小组的比赛势均力敌，该组剩下的两支队伍比其他队伍都要强。机器人最大的问题就是推 进的力度不够，而且他们所掌握的英雄数太少。就像你看到的那样，位列前 3 的机器人战队中有的只能使用 6 个不同的英雄。这些机器人有一个很好的或至少体面的团队阵容，并且可以不断赢得游戏，而那些会使用很多英雄的队伍，他们的发挥很大地取决于他们所挑选的英 雄。据了解，开发者已经能够使机器人支持队长模式，但实际上很少有机器人战队支持队长模式，所以我们不得不使用全阵营选择。我真的希望下次更多的机器人 （或全部）将支持队长模式，那时我们就可以举办一个更平常的比赛。机器人还有很多发挥的余地，也许这是 V 社 api 的问题，我希望这些问题将很快修复，届时我们可以看到一些更好的机器人。我不认为这些机器人不好，前 7 名或者这附近的机器人都十分不错，比默认的机器人要好得多！感谢所有的开发者所做出的工作，他们已经击败了 V 社的 AI 了，加油！我不希望这篇文章太长，所以我会在这里宣布未来的计划。我打算继续举办这一系列比赛，直到 V 社或者更大地组织举报机器人联赛。下一场比赛将有 16 支战队，这将使小组赛更看重实力，而不仅仅因为 BO1 的失败而结束联赛。这次比赛的前 8 名队伍将直接被邀请参与下一届联赛，而第 9-12 名将以双重淘汰赛相互争夺 2 个出线资格，最后的 2支战队将在预选赛中与其余的机器人队伍进行联赛，我还没有决定赛程。新的种子战队可以在这里看到。请输入图片描述我希望 V 社能尽快支持在服务器上运行机器人游戏，或允许观众在本地服务器上使用 DotaTV 来观战，而不是像这次只能很麻烦地使用录像来观看比赛。谢谢你们，我们国际邀请赛之前再见！这次我会尝试回答每个比赛或机器人相关的问题！祝大家生活愉快！以上内容转自 https://redd.it/676zz6半决赛：总决赛：GAME 1 ,GAME 2 ,GAME 3很高兴自己的 ai 能拿下第二名，如果你对我的 ai 有任何建议，请移步贴吧或创意工坊