YGOPro AI腳本教程(譯文)

本教程旨在教會(huì)大家如何為YGOPro編寫或修改一個(gè)AI腳本。前幾節(jié)是新手指引,而接下來(lái)的章節(jié)涵蓋了一些更深入的知識(shí)蔗牡,例如如何將自己的卡組集成到標(biāo)準(zhǔn)的AI宗侦。在此之前愚臀,要掌握一些Lua的基本知識(shí)和編程語(yǔ)言相關(guān)的知識(shí),以及足夠的耐心矾利。AI腳本編程并不是那么容易的哦 :D

由于簡(jiǎn)書(shū)不支持目錄的跳轉(zhuǎn)姑裂,想用此功能的讀者請(qǐng)到網(wǎng)盤下載相關(guān)文件的壓縮包,里面包含此文的Markdown文件男旗。本人用visual studio code編寫此譯文舶斧,你也可以使用其他支持Markdown的文本編輯器。

??本教程旨在教會(huì)大家如何為YGOPro編寫或修改一個(gè)AI腳本察皇。前幾節(jié)是新手指引茴厉,而接下來(lái)的章節(jié)涵蓋了一些更深入的知識(shí),例如如何將自己的卡組集成到標(biāo)準(zhǔn)的AI让网。在此之前呀忧,要掌握一些Lua的基本知識(shí)和編程語(yǔ)言相關(guān)的知識(shí),以及足夠的耐心溃睹。AI腳本編程并不是那么容易的哦 :D

目錄

  1. 概述
  2. 開(kāi)始
  3. 詳解ai-template.lua
  4. 編寫我們的第一個(gè)AI
  5. 自定義函數(shù)
  6. 常見(jiàn)問(wèn)題
  7. 使用卡片腳本系統(tǒng)的函數(shù)
  8. 添加新的卡組到已有的AI
  9. 寫在最后

<h2 id='1'>1. 概述</h2>

??你可以通過(guò)點(diǎn)擊“AI模式 (beta)”按鈕而账,在YGOPro中與AI進(jìn)行決斗。在接下來(lái)的屏幕中因篇,你可以修改一些決斗的設(shè)置泞辐,然后單擊“OK”。現(xiàn)在你可以選擇一個(gè)卡組和一個(gè)AI使用的腳本文件竞滓「篮穑卡組必須放在YGOPro的"deck"文件夾下,腳本則位于"ai"目錄下商佑。默認(rèn)的锯茄,你有3個(gè)ai可選:

  • "ai.lua",標(biāo)準(zhǔn)AI文件
  • "ai-cheating.lua"茶没,和"ai.lua"一樣肌幽,只是這個(gè)AI進(jìn)行了一些優(yōu)化
  • "ai-exodialib.lua",AI_Exodia卡組的專用AI抓半。AI腳本的版本為 0.26喂急,這個(gè)文件已經(jīng)被廢棄可以刪除,現(xiàn)在用標(biāo)準(zhǔn)AI來(lái)處理Exodia卡組笛求。

??理論上廊移,AI可以使用你為它組的任何卡組糕簿。然而,如果任意的卡沒(méi)有在AI中搭配特定的腳本狡孔,它通常只是盡可能的發(fā)動(dòng)懂诗。效果對(duì)象的選擇也是隨機(jī)的,所以AI在發(fā)動(dòng)效果時(shí)往往檢索或指向了錯(cuò)誤的卡步氏。
這也就是為什么有特定的卡組腳本在ai文件夾里面响禽。這些卡組以"AI_"為前綴來(lái)標(biāo)記,這樣AI就可以使用它們荚醒。你可能選擇了"Random Deck"芋类,那么AI會(huì)隨機(jī)選用一個(gè)帶"AI_"前綴的卡組。由于有可能重命名或移除AI卡組界阁,所以隨機(jī)卡組選項(xiàng)不會(huì)使用那些卡組侯繁。
就像Yugioh的卡片腳本一樣,ai文件也是用Lua編寫的——一門相當(dāng)流行的腳本語(yǔ)言: http://www.lua.org/about.html

??這個(gè)教程有兩個(gè)看起來(lái)相似的章節(jié)泡躯,采用不同的方式來(lái)實(shí)現(xiàn)相同的功能贮竟。前面一小節(jié)向你展示如何用基本的AI函數(shù)來(lái)實(shí)現(xiàn),接下來(lái)的章節(jié)使用了讓我的編程更加容易的自定義函數(shù)较剃。使用它們可以縮減代碼咕别,并且可以預(yù)處理很多細(xì)節(jié)。當(dāng)然写穴,它們也有局限性惰拱,在你的特定問(wèn)題中也不總是會(huì)顯得那么好用。這也就是為什么我努力去解釋它們的內(nèi)部工作原理以及給你一小節(jié)內(nèi)容關(guān)于如何利用“原生的”AI函數(shù)來(lái)編寫他們的功能啊送。

??如果你對(duì)那些原生的AI函數(shù)不感興趣偿短,又或者你已經(jīng)很熟悉它們,隨時(shí)跳到第5章:常用函數(shù)馋没。如果你已經(jīng)有一個(gè)可以工作的AI腳本昔逗,并且只想知道,如何集成你自己的卡組到標(biāo)準(zhǔn)AI腳本篷朵,第8章:添加新的卡組到已有的AI 就是你要的勾怒。

<h2 id='2'>2. 開(kāi)始</h2>

??如果你想深入AI腳本編程,我建議你先去看看你的"ai"文件夾下的 ai-template.lua 文件声旺。
當(dāng)你選擇AI來(lái)玩對(duì)戰(zhàn)時(shí)它不會(huì)起作用控硼,因?yàn)樗粚?duì)開(kāi)發(fā)有用。你可以用任意的文本編輯器來(lái)瀏覽和編輯 .lua 文件艾少。我個(gè)人用的是 Notepad++: http://notepad-plus-plus.org/
也有一些其他的軟件,比如 LuaEdit翼悴,但它已相當(dāng)過(guò)時(shí)且在我的系統(tǒng)上會(huì)崩潰缚够。如果你有編程經(jīng)驗(yàn)幔妨,你當(dāng)然也可以用你自己選擇的IDE,只要它支持Lua谍椅。

??這個(gè)模板包含了很多重要的函數(shù)讓你可以用來(lái)修改AI的行為误堡。它也有注釋和提示關(guān)于如何使用這些函數(shù)。關(guān)于函數(shù)雏吭,ai-template.lua 里面還有更詳細(xì)的介紹锁施。

??在編寫AI文件的時(shí)候,運(yùn)行測(cè)試腳本之前切記要用符號(hào)檢查工具來(lái)排除一些錯(cuò)誤杖们。如果你IDE的已經(jīng)支持這個(gè)悉抵,那就太好了。在我的Notepad++上摘完,我用 這個(gè)文件姥饰。請(qǐng)按照readme的指引來(lái)添加符號(hào)檢查功能到你的Notepad++。

<h2 id='3'>3. 詳解ai-template.lua</h2>

??在我看來(lái)孝治,深入AI腳本編程的最佳起點(diǎn)就是“ai-template.lua”文件列粪。它位于你的YGOPro文件夾的“ai”文件夾下,這也是你可以獲得的最基礎(chǔ)的AI之一谈飒。它是一個(gè)完全可以自主工作的AI文件岂座,它有友好的文檔和包含了大部分你可以使用的AI函數(shù)。

??所以杭措,我們會(huì)打開(kāi)這個(gè)文件费什,來(lái)好好看看它。在文檔的開(kāi)頭瓤介,我們可以看到一堆的注釋吕喘,包括版本信息,更新日志刑桑,一些使用技巧氯质。緊接著技巧,我們會(huì)有一份你可以在任意時(shí)刻調(diào)用的AI函數(shù)的列表:

AI.Chat(text) --text: a string

??這個(gè)函數(shù)用來(lái)讓AI說(shuō)話祠斧。你可以用它來(lái)輸出信息闻察,調(diào)試,調(diào)戲琢锋,辱罵或輸出任何你想AI說(shuō)的辕漂。

AI.GetPlayerLP(player) --1 = AI,2 = 玩家

??返回player當(dāng)前的LP吴超。

AI.GetCurrentPhase() --請(qǐng)參考 /script/constant.lua 里的階段常量列表

??script文件夾下的“constant.lua”文件里列出了所有階段常量钉嘹,因此打開(kāi)它然后搜索階段常量。它們以 PHASE_為前綴鲸阻,比如PHASE_MAIN2PHASE_BATTLE跋涣。你可以用這個(gè)函數(shù)的返回值與當(dāng)前階段進(jìn)行比較就像下面這樣:

If AI.GetCurrentPhase()==PHASE_BATTLE then -- 在戰(zhàn)斗階段進(jìn)行某些操作
AI.GetOppMonsterZones()
AI.GetAIMonsterZones()
AI.GetOppSpellTrapZones()
AI.GetAISpellTrapZones()
AI.GetOppGraveyard()
AI.GetAIGraveyard()
AI.GetOppBanished()
AI.GetAIBanished()
AI.GetOppHand()
AI.GetAIHand()
AI.GetOppExtraDeck()
AI.GetAIExtraDeck()
AI.GetOppMainDeck()
AI.GetAIMainDeck()

??上述函數(shù)用來(lái)獲取你想要的卡片列表缨睡。就像名字描述的那樣,它們返回特定區(qū)域的卡片列表陈辱。如果那個(gè)區(qū)域沒(méi)有卡奖年,它會(huì)返回一個(gè)填滿nil的列表。模板文件里面有使用樣例沛贪。

??現(xiàn)在來(lái)到模板里面的第一行實(shí)際的腳本:

math.randomseed( require("os").time() )

??這行代碼是必須的陋守。我不知道它的確切作用,也許是設(shè)置一個(gè)隨機(jī)數(shù)的種子并且與系統(tǒng)時(shí)間做同步利赋。只管把這行放到你的AI文件就是了水评,這很重要。

接下來(lái)是:

function OnStartOfDuel()
  ...
end

??這個(gè)函數(shù)在每次決斗開(kāi)始時(shí)會(huì)被系統(tǒng)自動(dòng)調(diào)用隐砸。在模板中之碗,它使用AI.Chat()函數(shù)來(lái)輸出了一些信息。

??接下來(lái)是系統(tǒng)提供的一堆函數(shù)季希,用來(lái)給你在特定的時(shí)機(jī)使用AI來(lái)進(jìn)行一些操作褪那,它們包括:

OnSelectOption
OnSelectEffectYesNo
OnSelectYesNo
OnSelectPosition
OnSelectTribute
OnDeclareMonsterType
OnDeclareAttribute
OnDeclareCard
OnSelectNumber
OnSelectChain
OnSelectSum
OnSelectCard
OnSelectBattleCommand
OnSelectInitCommand

??這里的每一個(gè)函數(shù)都會(huì)在AI進(jìn)行某種選擇操作的時(shí)候由系統(tǒng)自動(dòng)調(diào)用。它們大部分的名字都很好理解式塌,又或者在模板中有注釋來(lái)說(shuō)明博敬。我稍后會(huì)講解當(dāng)中重要的幾個(gè)。

??在講OnSelectInitCommand函數(shù)之前峰尝,你先看看下面這一些關(guān)于卡片的函數(shù)偏窝。

card.id
card.original_id
card.cardid
card.description
card.type
card.attack
card.defense
card.base_attack
card.base_defense
card.level
card.base_level
card.rank
card.race
card.attribute
card.position
card.setcode
card.location
card.xyz_material_count
card.xyz_materials
card.owner
card.status
card:is_affected_by(effect_type)
card:get_counter(counter_type)
card.previous_location
card.summon_type
card.lscale
card.rscale
card.equip_count
card:is_affectable_by_chain(index)
card:can_be_targeted_by_chain(index)
card:get_equipped_cards()
card:get_equip_target()
card.text_attack
card.text_defense

??它們大部分的意思都很明顯了,其他的都有注釋來(lái)說(shuō)明武学。如果你有一個(gè)卡片對(duì)象祭往,你可以通過(guò)這些函數(shù)獲取它的各種信息。

<h2 id='4'>4. 編寫我們的第一個(gè)AI</h2>

??太好了火窒,現(xiàn)在我們已經(jīng)看了一遍模板硼补。但我們?nèi)绾尾拍芰硗饩帉懸粋€(gè)實(shí)際的AI呢?

??在這一章熏矿,我們會(huì)創(chuàng)建一個(gè)新的AI文件已骇。我們會(huì)學(xué)習(xí)如何用AI腳本處理卡片,以及如何用函數(shù)來(lái)對(duì)卡片進(jìn)行簡(jiǎn)單操作票编。注意褪储,最終的結(jié)果看起來(lái)會(huì)有些混亂,并且寫起來(lái)也不是那么的舒服慧域。這章是為了展示AI內(nèi)部是怎么工作的鲤竹。如果你已經(jīng)熟悉了那些AI函數(shù),你可以跳到下一章去看那些用起來(lái)更加舒服的自定義函數(shù)昔榴。

??首先辛藻,我們會(huì)復(fù)制并重命名一份ai-template.lua瑟啃。在本教程,我們會(huì)命名為 ai-tutorial.lua揩尸。隨時(shí)刪除一些不必要的內(nèi)容,你沒(méi)必要保留所有的注釋屁奏,你可以隨時(shí)在原本的模板里查閱它們岩榆。我已經(jīng)準(zhǔn)備了一份清理好的版本,你可以從這里下載》仄埃現(xiàn)在勇边,你已經(jīng)可以用這個(gè)文件來(lái)玩了。只要它放在ai文件夾里并且名稱不是 ai-template.lua折联,它應(yīng)該就是可用的粒褒。如果你嘗試使用它并且給AI一些隨機(jī)卡組,你會(huì)發(fā)現(xiàn)诚镰,AI不會(huì)用LOT奕坟,它只會(huì)盡可能的發(fā)動(dòng)每一張單卡,甚至炸掉自己的卡清笨。

??那么月杉,我們?nèi)绾蝸?lái)改進(jìn)它呢?在這個(gè)例子中抠艾,我們會(huì)使用「魔導(dǎo)戰(zhàn)士 破壞者」苛萎。我相信,你已經(jīng)熟悉他的效果了检号,他可以摘除1個(gè)魔力指示物來(lái)破壞場(chǎng)上存在的1張魔法或陷阱卡腌歉。讓我們組一個(gè)測(cè)試卡組來(lái)看看我們的AI如何處理它。這個(gè)卡組應(yīng)該包含「破壞者」齐苛,1張發(fā)動(dòng)后不會(huì)立刻離場(chǎng)的魔法卡(比如「光之護(hù)封劍」)翘盖,以及一堆不用的填充卡。我喜歡用/爆裂模式怪獸來(lái)填充脸狸,但也可以是不能夠被召喚或從手卡發(fā)動(dòng)的任意卡最仑。象這樣的卡組可以作為一個(gè)測(cè)試卡組。開(kāi)始一場(chǎng)游戲炊甲,確認(rèn)勾選了“不切洗卡組”復(fù)選框(和“不檢查卡組”——如果你用了一個(gè)非法的測(cè)試卡組)泥彤,為AI玩家選擇那個(gè)tutorial AI文件和測(cè)試卡組。現(xiàn)在你會(huì)看到問(wèn)題了:AI發(fā)動(dòng)了「護(hù)封劍」然后用「破壞者」破壞了它卿啡。因?yàn)锳I自己的魔陷是可用的效果對(duì)象吟吝,「破壞者」可以發(fā)動(dòng),所以AI就這么做了颈娜。我們?nèi)绾巫柚顾@么做呢剑逃?

<h3 id='4_1'>OnSelectInitCommand</h3>

??「破壞者」的召喚與發(fā)動(dòng)是在OnSelectInitCommand函數(shù)里面處理的浙宜。OnSelectInit總是會(huì)被調(diào)用,如果AI可以在它的主要階段執(zhí)行動(dòng)作蛹磺, 它負(fù)責(zé)處理各種各樣的東西粟瞬,從通召到特召和操作怪獸和發(fā)動(dòng)魔法卡還有發(fā)動(dòng)或蓋放陷阱卡以及進(jìn)入下一個(gè)階段∮├Γ「破壞者」的效果是發(fā)動(dòng)型效果裙品,且只能在主要階段發(fā)動(dòng),所以要在這里處理他俗或。

??特別的市怎,這段代碼處理「破壞者」的發(fā)動(dòng):

if #cards.activatable_cards > 0 then
  return COMMAND_ACTIVATE,1
end

??cards.activatable_cards獲取所有可以在此時(shí)發(fā)動(dòng)的卡片的列表。那個(gè)“#”操作符統(tǒng)計(jì)列表里卡片的數(shù)量辛慰。return返回一個(gè)指令常量和一個(gè)索引区匠。你可以在ai-template.lua里面查找那個(gè)常量,那個(gè)索引總是定位到列表中被發(fā)動(dòng)的卡帅腌。因?yàn)檫@個(gè)函數(shù)只是單純的全部發(fā)動(dòng)驰弄,所以我們可以只返回1,這樣它就只會(huì)發(fā)動(dòng)列表中的第一張卡狞膘。說(shuō)得更直白點(diǎn)揩懒,這段代碼可以解讀為“如果可以發(fā)動(dòng)的卡片列表的長(zhǎng)度大于0暗甥,發(fā)動(dòng)列表當(dāng)中的第一張卡惜颇,否則什么都不做”挟炬。

??注意昭灵,對(duì)OnSelectInit每次調(diào)用最終都只會(huì)執(zhí)行一次指令殖属。如果一個(gè)發(fā)動(dòng)指令返回了旭等,那張卡片就會(huì)被發(fā)動(dòng)铅乡,然后宏怔,等到所有連鎖處理完之后点待,OnSelectInit會(huì)以一個(gè)新的可以發(fā)動(dòng)的卡片的列表再次被調(diào)用阔蛉。

??那么,現(xiàn)在我們?nèi)绾翁幚怼钙茐恼摺鼓伛海渴紫茸丛覀儾坏貌幌拗艫I的隨意發(fā)動(dòng),我們想要自定義它的發(fā)動(dòng)苗踪。所以我們要改變發(fā)動(dòng)函數(shù)來(lái)避免總是發(fā)動(dòng)所有東西颠区,除了「破壞者」例外。為此通铲,我們會(huì)循環(huán)遍歷所有可以發(fā)動(dòng)的卡毕莱,然后單獨(dú)發(fā)動(dòng)他們,如果他們不是「破壞者」。

因此我們改寫這個(gè):

if #cards.activatable_cards > 0 then
  return COMMAND_ACTIVATE,1
end

改成這樣:

for i=1,#cards.activatable_cards do
  local c = cards.activatable_cards[i]
  if c.id ~= 71413901 then -- 破壞者
    return COMMAND_ACTIVATE,i
  end
end

??71413901是「破壞者」的卡片密碼朋截,這個(gè)特有的數(shù)字與卡片相互關(guān)聯(lián)蛹稍。我們會(huì)經(jīng)常用到這些ID,它們是在腳本中用來(lái)區(qū)分卡片的方法部服。小心唆姐,在腳本中很容易會(huì)弄錯(cuò)ID。現(xiàn)在這段代碼做了什么呢廓八?這是lua當(dāng)中的一個(gè)標(biāo)準(zhǔn)的for循環(huán)厦酬,它會(huì)循環(huán)遍歷可以發(fā)動(dòng)的卡,并且只有當(dāng)卡片沒(méi)有「破壞者」的ID的時(shí)候(~= 在lua中是“不等于”的意思)瘫想,他才會(huì)被發(fā)動(dòng)。所以現(xiàn)在昌讲,「破壞者」不會(huì)再被發(fā)動(dòng)了国夜。

??但這不是我們真正想要的,我們需要添加某種條件短绸,來(lái)讓「破壞者」可以被發(fā)動(dòng)车吹。這里要怎么寫才有意義呢?可能發(fā)動(dòng)「破壞者」的情況是醋闭,玩家控制著至少1張魔法卡或者陷阱卡窄驹。

所以我們可以修改我們的發(fā)動(dòng)代碼為:

for i=1,#cards.activatable_cards do
  local c = cards.activatable_cards[i]
  if c.id == 71413901 then -- 破壞者
    for j=1,#AI.GetOppSpellTrapZones() do
      if AI.GetOppSpellTrapZones()[j] then
        return COMMAND_ACTIVATE,i
      end
    end
  end
  if c.id ~= 71413901 then
    return COMMAND_ACTIVATE,i
  end
end

??AI.GetOppSpellTrapZones()是在模板中提到的其中一個(gè)函數(shù),它返回玩家魔陷區(qū)的所有卡片的列表证逻。然而乐埠,我們還是需要去檢查一下,這些區(qū)域是否都有卡囚企。這意味著要循環(huán)遍歷一次丈咐。如果一個(gè)區(qū)域沒(méi)有被占用,它就是false龙宏,否則它就有卡棵逊。那這里我們?yōu)槭裁床挥谩?”來(lái)判斷呢?原因是银酗,它會(huì)把空的魔陷區(qū)也計(jì)算在內(nèi)辆影,所以AI.GetOppSpellTrapZones()的長(zhǎng)度總是8。(5個(gè)魔陷區(qū)黍特,2個(gè)靈擺區(qū)蛙讥,1個(gè)場(chǎng)地魔法區(qū))。

??所以這段代碼解讀為“如果可以發(fā)動(dòng)的卡是「破壞者」衅澈,并且有一張卡存在于玩家的魔法與陷阱區(qū)域键菱,就發(fā)動(dòng)卡片”,然后是“如果可以發(fā)動(dòng)的卡片不是「破壞者」,直接發(fā)動(dòng)它”经备。

<h3 id='4_2'>OnSelectCard</h3>

??現(xiàn)在來(lái)測(cè)試一下拭抬。「破壞者」應(yīng)該不會(huì)發(fā)動(dòng)他的效果侵蒙,除非你控制著一張魔法卡造虎。然而,如果你試了纷闺,你會(huì)發(fā)現(xiàn)另外一個(gè)問(wèn)題:
「破壞者」也許會(huì)控制住他的效果發(fā)動(dòng)算凿,但是當(dāng)他發(fā)動(dòng)的時(shí)候,他還是會(huì)破壞AI自己的魔法卡犁功!那么我們?nèi)绾涡迯?fù)這個(gè)問(wèn)題呢氓轰?

??對(duì)于大部分效果的對(duì)象選擇是在OnSelectCard函數(shù)里面處理的。一般來(lái)說(shuō)浸卦,看起來(lái)如下:

function OnSelectCard(cards, minTargets, maxTargets, triggeringID, triggeringCard)
  local result = {}
    for i=1,minTargets do
      result[i]=i
    end
  return result
end

??你會(huì)發(fā)現(xiàn)署鸡,它有一堆參數(shù)可用。cards 是一個(gè)可以被選為對(duì)象的卡片的列表限嫌。minTargets 和 maxTargets 定義你要選擇的卡片數(shù)量靴庆。triggeringID 和 triggeringCard 定位到發(fā)效果卡。當(dāng)你的效果要選卡的時(shí)候怒医,它負(fù)責(zé)發(fā)動(dòng)那個(gè)效果炉抒。

為了讓「破壞者」選擇正確的效果對(duì)象,我們可以把函數(shù)改成這樣:

local result = {}
if triggeringID == 71413901 then -- 破壞者
  for i=1,#cards do
    if cards[i].owner == 2 then
      return {i}
    end
  end
end
for i=1,minTargets do
  result[i]=i
end
return result

??我們將發(fā)動(dòng)效果的卡的ID與「破壞者」的進(jìn)行比較稚叹,如果吻合焰薄,我們就循環(huán)遍歷所有可選的對(duì)象,并且返回屬于玩家的第一個(gè)扒袖。注意蛤奥,返回值必須是一個(gè)索引列表,而不是一個(gè)索引僚稿,因?yàn)橛幸恍┛ㄆ瑫?huì)有多個(gè)效果對(duì)象凡桥。所以在這里你不能返回i,你只能返回{i}蚀同。否則游戲會(huì)崩潰缅刽!

??現(xiàn)在再試一下。現(xiàn)在「破壞者」應(yīng)該只會(huì)破壞玩家的卡了蠢络。這個(gè)是你現(xiàn)在的AI的樣子衰猛。注意,這是一種非常啰嗦的實(shí)現(xiàn)刹孔。如果你添加了上百?gòu)埧ǖ紸I啡省,你可能會(huì)想去寫一些函數(shù)來(lái)幫你縮短和組織代碼,這樣你就不用為每一張卡都寫一個(gè)新的循環(huán)。在后續(xù)的教程中我會(huì)給出我寫來(lái)解決此問(wèn)題的這些函數(shù)的易用版卦睹。

<h3 id='4_3'>OnSelectChain和OnSelectEffectYesNo</h3>

??現(xiàn)在我們已經(jīng)學(xué)習(xí)了兩個(gè)非常重要的知識(shí)點(diǎn):OnSelectInitCommand 和 OnSelectCard 的使用畦戒。下一步:OnSelectChain 和 OnSelectEffectYesNo。它們兩個(gè)處理大部分可以連鎖的卡片和效果的發(fā)動(dòng)结序,它們的工作原理是一樣的障斋,盡管它們用起來(lái)有很大差異。OnSelectChain 有一個(gè)可連鎖的卡片列表徐鹤,就像 OnSelectInit垃环,并且當(dāng)任意卡片能夠被連鎖的時(shí)候它都會(huì)被調(diào)用。OnSelectEffectYesNo通常在只允許一張卡被發(fā)動(dòng)的特定情形下被調(diào)用返敬。

??許多可以連鎖的卡片或效果能夠在其中一個(gè)函數(shù)中處理遂庄,具體取決于場(chǎng)上的情況。舉個(gè)例子劲赠,假設(shè)AI控制著一張「雷王」涧团,然后你的對(duì)手特召了一只怪獸。現(xiàn)在 OnSelectEffectYesNo 會(huì)被調(diào)用來(lái)處理“是否發(fā)動(dòng)「雷王」的效果無(wú)效這次特召经磅?”這個(gè)問(wèn)題。人類玩家在這個(gè)時(shí)候會(huì)看到一個(gè)選擇對(duì)話框钮追。然而预厌,如果你已經(jīng)蓋了一張「升天之黑角笛」,代替對(duì)話框的是元媚,你會(huì)被問(wèn)到是否連鎖卡片或效果的發(fā)動(dòng)來(lái)發(fā)動(dòng)轧叽。所以在這個(gè)時(shí)候,AI會(huì)在 OnSelectChain 而非 OnSelectEffectYesNothe 中來(lái)處理刊棕。注意炭晒,這只是一個(gè)例子,「雷王」實(shí)際上不僅僅通過(guò) OnSelectEffectYesNo 來(lái)處理效果甥角。你只要理解就好网严,對(duì)于一些卡來(lái)說(shuō)這是一個(gè)確定的例子。

??那么嗤无,這對(duì)我們來(lái)說(shuō)意味著什么呢震束?一般情況下,我們不得不添加所有能夠被其中一個(gè)函數(shù)處理的效果到兩個(gè)函數(shù)中当犯。有一些卡永遠(yuǎn)不會(huì)被其中一個(gè)函數(shù)處理垢村,為了讓事情變得簡(jiǎn)單些,除非你確切地知道嚎卫,是否一張卡會(huì)只被其中的一個(gè)處理嘉栓,否則建議你在兩個(gè)函數(shù)中都添加。

??讓我們拿提到的「雷王」來(lái)做個(gè)例子。他是如何配合我們當(dāng)前的測(cè)試AI來(lái)工作的呢侵佃?如你所料麻昼,他只會(huì)連鎖發(fā)動(dòng)他的效果來(lái)無(wú)效我們的每一次特殊召喚。如果你組了一個(gè)像之前那樣趣钱,但是AI只能用「雷王」的測(cè)試卡組涌献,然后在測(cè)試決斗中讓你的對(duì)手召喚他緊接著特殊召喚一只很弱的怪獸,比如「糖果小丑」首有,AI還是會(huì)連鎖發(fā)動(dòng)它的效果燕垃,盡管「糖果小丑」只有0的攻擊力和守備力并且通常來(lái)說(shuō)不會(huì)單獨(dú)使用。

??我們?nèi)绾胃倪M(jìn)它呢? 首先井联,我們來(lái)看看我們相關(guān)的函數(shù):

function OnSelectEffectYesNo(id, triggeringCard)
  return 1 -- 這代表總是返回 yes
end

function OnSelectChain(cards, only_chains_by_player, forced)
  return 1,1 -- 這代表總是連鎖第一張可能的卡片
end

??它們看起來(lái)足夠簡(jiǎn)單〔泛荆現(xiàn)在,我們不得不先做一些確認(rèn)烙常,因?yàn)樗鼈儾豢偸怯伞咐淄酢拐{(diào)用轴捎,所以我們會(huì)添加一些判斷,就像之前那樣:

function OnSelectEffectYesNo(id, triggeringCard)
  if id == 71564252 then -- 雷王
    return 0
  end
  return 1
end

function OnSelectChain(cards, only_chains_by_player, forced)
  for i=1,#cards do
    local c = cards[i]
    if c.id ~= 71564252 then -- 雷王
      return 1,i
    end
  end
  return 0,0
end

??兩個(gè)函數(shù)看起來(lái)很不一樣蚕脏,因?yàn)?YesNo 只會(huì)在一個(gè)特定情形或被一張?zhí)囟ㄆ{(diào)用侦副,所以你可以這樣判斷,如果卡片是「雷王」驼鞭,那么返回 no秦驯,如果不是,讓函數(shù)照常返回 yes挣棕。Chain 就復(fù)雜些译隘,因?yàn)樗幸粋€(gè)卡片列表。你要遍歷這些卡洛心,連鎖發(fā)動(dòng)所有不是「雷王」的固耘,并且如果只剩下「雷王」,則不再做進(jìn)一步的連鎖词身。試一下厅目,改完這些以后「雷王」應(yīng)該不會(huì)再發(fā)動(dòng)了。

??好吧》ㄑ希現(xiàn)在璧瞬,我們?cè)趺创_定他的發(fā)動(dòng)時(shí)機(jī)呢?這是相當(dāng)棘手的問(wèn)題渐夸,但是現(xiàn)在嗤锉,我們將采用一個(gè)非常簡(jiǎn)單的解決方案:如果被召喚的怪獸可以戰(zhàn)斗破壞「雷王」就無(wú)效他的召喚。現(xiàn)在墓塌,我們?cè)撛趺醋瞿匚脸溃坎恍业氖前露睿覀儧](méi)有直接的方法來(lái)使用「雷王」的效果來(lái)進(jìn)行破壞。所以在這里我們需要一些創(chuàng)造力访诱。在YGOPro中垫挨,對(duì)于正在召喚的怪獸會(huì)有一個(gè)特定的狀態(tài)常量——STATUS_SUMMONING。這個(gè)狀態(tài)常量只會(huì)在無(wú)效召喚的時(shí)點(diǎn)被激活触菜,此時(shí)「雷王」可以發(fā)動(dòng)九榔,并且我們可以檢測(cè)它。那怎么做呢涡相?當(dāng)然是遍歷哲泊,我們遍歷對(duì)手所有可用的怪獸并檢查是否包含這個(gè)狀態(tài):

for i=1,#AI.GetOppMonsterZones() do
  local c = AI.GetOppMonsterZones()[i]
  if c and bit32.band(c.status,STATUS_SUMMONING)>0 then
    --c 是我們召喚的怪獸
  end
end

??用bit32.band來(lái)檢測(cè)狀態(tài)是必須的,因?yàn)榭ㄆ赡芡瑫r(shí)處于多種狀態(tài)催蝗,所以用c.status==STATUS_SUMMONING來(lái)檢測(cè)可能會(huì)導(dǎo)致錯(cuò)誤切威,因?yàn)樗鼘?shí)際的狀態(tài)可能是STATUS_SUMMONING+STATUS_SOMETHING_ELSEbit32.band執(zhí)行按位與操作并且可以通過(guò)這種方法將狀態(tài)信息分離出來(lái)丙号。注意先朦,它返回一個(gè)數(shù)字而不是 true 或 false。返回0意味著卡片不包含這種狀態(tài)犬缨,>0表示包含這種狀態(tài)喳魏。

??用bit32.band檢測(cè)狀態(tài)的原理:查看constant.lua可以知道,所有的狀態(tài)常量轉(zhuǎn)為二進(jìn)制表示之后是:1,10,100,1000...怀薛,因此刺彩,每添加一種狀態(tài)只需要簡(jiǎn)單地將其數(shù)值加上,因?yàn)槊恳粋€(gè)二進(jìn)制位都不會(huì)相互干擾乾戏。然后使用bit32.band按位與操作就可以提取出要檢測(cè)的狀態(tài)的數(shù)值,這樣可以高效而簡(jiǎn)單地判斷一種單獨(dú)的狀態(tài)是否包含在一個(gè)復(fù)雜的疊加態(tài)中三热。同樣的功能用數(shù)組或?qū)ο髮傩缘脑O(shè)計(jì)也能實(shí)現(xiàn)(可讀性會(huì)更好)鼓择,但是執(zhí)行效率都比不上這種。

??現(xiàn)在我們已經(jīng)有了被召喚出來(lái)的怪獸就漾,我們能夠檢測(cè)它所處的狀態(tài)呐能,那么「雷王」該不該被發(fā)動(dòng)。我們會(huì)用一個(gè)函數(shù)簡(jiǎn)單地處理這個(gè)問(wèn)題抑堡,就把它叫做ChainTKRO()然后添加到腳本當(dāng)中不在其它函數(shù)里面的的任意位置:

function ChainTKRO()
  for i=1,#AI.GetOppMonsterZones() do
    local c = AI.GetOppMonsterZones()[i]
    if c and bit32.band(c.status,STATUS_SUMMONING)>0 then
      if c.attack>=1900 then
        return true
      end
    end
  end
  return false
end

??這個(gè)函數(shù)十分的靈活摆出,我們可以從任何地方調(diào)用它,如果有任何正被特殊召喚的怪獸的攻擊力大于1900首妖,它就返回 true偎漫,如果怪獸更弱,就返回 false有缆。我們可以用它代替完整的召喚檢測(cè)來(lái)放到 Chain 和 EffectYesNo 兩個(gè)函數(shù)里面:

function ChainTKRO()
  for i=1,#AI.GetOppMonsterZones() do
    local c = AI.GetOppMonsterZones()[i]
    if c and bit32.band(c.status,STATUS_SUMMONING)>0 then
      if c.attack>=1900 then
        return true
      end
    end
  end
  return false
end

function OnSelectEffectYesNo(id, triggeringCard)
  if id == 71564252 then -- 雷王
    if ChainTKRO() then
      return 1
    else
      return 0
    end
  end
  return 1
end

function OnSelectChain(cards, only_chains_by_player, forced)
  for i=1,#cards do
    local c = cards[i]
    if c.id == 71564252 and ChainTKRO() then -- 雷王
      return 1,i
    end
    if c.id ~= 71564252 then
      return 1,i
    end
  end
  return 0,0
end

??試一下象踊。讓你的對(duì)手召喚「雷王」温亲,然后特殊召喚一只弱雞怪獸,再試著特召一只強(qiáng)力怪獸(比如「光子斬?fù)粽摺梗┍亍H供參考栈虚,你的AI現(xiàn)在看起來(lái)會(huì)像這樣

??我個(gè)人喜歡為每一張卡編寫一個(gè)像這樣的獨(dú)立函數(shù)而不是只在chains中處理史隆。我為卡片編寫了諸如SummonX()魂务,ActivateX()ChainX()等等的函數(shù)泌射。

??好了粘姜,以上這些就是要為我們的第一個(gè)AI做的。這些只是最基本的魄幕,但我們學(xué)會(huì)了如何使用 OnSelectInitCommand相艇,OnSelectCardOnSelectEffectYesNoOnSelectChain函數(shù)纯陨。這些都是最重要的坛芽,我想整個(gè)AI中大約有80%的處理都是在這些函數(shù)當(dāng)中進(jìn)行的∫砜伲基本原理都是一樣的咙轩,循環(huán)遍歷卡片,檢查特定的條件阴颖,返回正確的索引活喊。

<h2 id='5'>5. 自定義函數(shù)</h2>

??在上一章,我們?yōu)锳I添加了兩張卡量愧,現(xiàn)在那兩張卡可以處理簡(jiǎn)單的對(duì)戰(zhàn)了钾菊。太好了,還有8829張卡要處理 :D

??注意偎肃,添加大量的卡片或一個(gè)復(fù)雜的卡組到Y(jié)GOPro是一件工作量很大的事情煞烫,所以你肯定會(huì)想找到一些方法來(lái)優(yōu)化這個(gè)過(guò)程。而且我們添加的兩張卡也還不是很好用累颂≈拖辏「破壞者」會(huì)隨機(jī)選擇卡片作為對(duì)象,他可能會(huì)發(fā)效果去破壞一張不會(huì)被效果破壞的卡片紊馏,或者破壞一張沒(méi)有對(duì)象關(guān)聯(lián)的「活死人的呼聲」而不是更具威脅性的「大宇宙」料饥。「雷王」也許會(huì)放過(guò)有危險(xiǎn)效果的怪獸的特殊召喚朱监,因?yàn)樗墓袅ψ銐虻汀?/p>

??為了提升添加新卡的速度好讓加入的時(shí)候不用為每張卡添加上百項(xiàng)檢查岸啡,我在編寫AI的時(shí)候開(kāi)發(fā)了幾個(gè)實(shí)用的自定義函數(shù)。

??特此聲明:我不是一個(gè)專業(yè)的程序員并且我也不是非常的熟悉lua赫编,剛開(kāi)始編寫AI腳本的時(shí)候我甚至對(duì)其一無(wú)所知凰狞。所以大部分的函數(shù)都有改進(jìn)的空間篇裁,又或者說(shuō)它們不遵循常規(guī)的的編程標(biāo)準(zhǔn)。

??還有赡若,請(qǐng)記住达布,任何你在這章中使用的自定義函數(shù)在你之前構(gòu)建的AI里面可能會(huì)用不了,至少在把對(duì)它們的請(qǐng)求添加到AI里面之前是不可用的逾冬。添加下面這行代碼:

require("ai.ai")

到你的ai-tutorial.lua以確保你可以使用這些函數(shù)當(dāng)中的大部分黍聂。這行代碼連接到標(biāo)準(zhǔn)AI文件(ai.lua),標(biāo)準(zhǔn)AI會(huì)依次請(qǐng)求所有mod和desks文件夾下的文件并加載里面的函數(shù)身腻。此外产还,你還可以跳過(guò)一些函數(shù)來(lái)讓默認(rèn)的AI函數(shù)進(jìn)行處理。不想寫你自己的攻擊邏輯嘀趟?只需要從你的測(cè)試AI中刪掉OnSelectBattleCommand脐区,標(biāo)準(zhǔn)AI的攻擊邏輯就會(huì)接手。

<h3 id='5_0'>短名版函數(shù)</h3>

??你用的LOT應(yīng)該盡可能的簡(jiǎn)單她按,這樣你就不用寫一大堆代碼牛隅。AIMon()AI.GetAIMonsterZones()的短名版且自帶空區(qū)過(guò)濾,這樣可以簡(jiǎn)化程序的后續(xù)處理酌泰。類似的函數(shù)還有OppMon()媒佣,AIGrave()AIHand()等等陵刹。在接下來(lái)的教程中我會(huì)列出一份實(shí)用自定義函數(shù)的API默伍。

??形如cards.activatable_cards的函數(shù)的短名版看起來(lái)就像這樣:cards.activatable_cards => Act衰琐,cards.summonable_cards => Sum也糊,SpSum,... 你理解即可羡宙。

<h3 id='5_1'>HasID</h3>

??這個(gè)函數(shù)可能是我用得最多的了:HasID函數(shù)是一個(gè)非常簡(jiǎn)單的循環(huán)函數(shù)狸剃。給它傳入一個(gè)卡片列表和一個(gè)ID,它會(huì)遍歷所有卡片辛辨,如果當(dāng)中有對(duì)應(yīng)ID的卡則返回true捕捂。它也會(huì)更新一個(gè)全局變量——CurrentIndex瑟枫,這個(gè)變量用來(lái)存儲(chǔ)匹配到的卡在傳入的卡片組中的索引斗搞。

所以代替:

for i=1,#cards do
  local c = cards()[i]
  if c.id == 71564252 and ChainTKRO() then
    return 1,i
  end
end

我可以這樣寫:

if HasID(cards,71564252) and ChainTKRO() then
  return 1,CurrentIndex
end

??它還有一些其他的參數(shù),是我設(shè)計(jì)用來(lái)解決一些特殊問(wèn)題的慷妙。但這個(gè)基本的函數(shù)可以只傳兩個(gè)參數(shù)僻焚。

??忠告:如果確實(shí)要在全局中使用CurrentIndex那再次使用HasID的時(shí)候就要小心了,因?yàn)樗鼤?huì)修改這個(gè)索引膝擂。如果你想用它來(lái)檢查場(chǎng)上的其他卡并且還是需要用到全局索引虑啤,用它的時(shí)候把第3個(gè)參數(shù)設(shè)置為true隙弛,這會(huì)讓它不修改這個(gè)索引:

if HasID(cards,71564252) and not HasID(AIMon(),71564252) then -- 不要這樣做!

??當(dāng)你沒(méi)有「雷王」,想召喚一只上場(chǎng)的時(shí)候狞山,你可能會(huì)像上面這樣來(lái)檢查全闷。然而,這會(huì)產(chǎn)生錯(cuò)誤萍启,甚至可能造成崩潰总珠。你要這樣做:

if HasID(cards,71564252) and not HasID(AIMon(),71564252,true) then -- 這樣才對(duì)

??實(shí)現(xiàn)相同功能的還有HasIDNotNegated函數(shù),但它多了一個(gè)無(wú)效化檢查勘纯,這樣卡片在被效果無(wú)效化時(shí)就不會(huì)嘗試發(fā)動(dòng)效果局服。

<h3 id='5_2'>BestTargets</h3>

??試圖去概括性地處理所有離場(chǎng)效果,可能會(huì)導(dǎo)致效果不能正常工作(所以用一系列常量分情況處理)驳遵。BestTargets傳入一個(gè)卡片列表淫奔,一個(gè)數(shù)量和一個(gè)用來(lái)定義當(dāng)前使用的效果類型的自定義常量。它基于卡片類型堤结,表示形式唆迁,所在區(qū)域,戰(zhàn)場(chǎng)形勢(shì)霍殴,效果類型和免疫類型來(lái)給可選對(duì)象劃定優(yōu)先級(jí)媒惕。它還會(huì)檢查黑名單,因?yàn)橐恍┛ú粫?huì)成為效果對(duì)象或不受某些特定卡的影響来庭。它把破壞效果做為默認(rèn)目標(biāo)妒蔚,但你也可以用下列常量來(lái)代替:

TARGET_OTHER
TARGET_DESTROY
TARGET_TOGRAVE
TARGET_BANISH
TARGET_TOHAND
TARGET_TODECK
TARGET_FACEDOWN
TARGET_CONTROL -- 比如 強(qiáng)奪
TARGET_BATTLE  -- 變更攻擊對(duì)象
TARGET_DISCARD
TARGET_PROTECT -- 這可以是任何有益的效果

??它返回一個(gè)基于卡片列表輸入的索引列表,顯然它被設(shè)計(jì)來(lái)在OnSelectCard中工作月弛。在「破壞者」的例子當(dāng)中肴盏,它看起來(lái)如下:

function OnSelectCard(cards, minTargets, maxTargets, triggeringID, triggeringCard)
if id == 71413901 then -- 破壞者
  return BestTargets(cards) -- 或 BestTargets(cards,1,TARGET_DESTROY)
....

??對(duì)于只有一個(gè)效果對(duì)象的簡(jiǎn)單的破壞效果,你只需傳入卡片列表帽衙,因?yàn)閿?shù)量默認(rèn)為1菜皂,目標(biāo)常量默認(rèn)為破壞,這也讓「破壞者」的使用變得簡(jiǎn)單厉萝。并且它還會(huì)自動(dòng)地處理那些不可破壞的卡和不會(huì)成為效果對(duì)象的卡恍飘,在可以知道的前提下(比如古遺物系列)。然而谴垫,它只處理對(duì)象的選擇章母,如果只有不理想的對(duì)象可選,它還是會(huì)挑一個(gè)翩剪。你需要在OnSelectInit中添加一個(gè)額外的檢測(cè)來(lái)避免效果的發(fā)動(dòng)乳怎,如果不存在感興趣的破壞對(duì)象。

<h3 id='5_3'>Add</h3>

??嘗試了不同卡片對(duì)象的選擇前弯,這一回輪到了各種類型的選卡效果蚪缀。然而這里有一個(gè)坑:優(yōu)先級(jí)系統(tǒng)秫逝。來(lái)看看我說(shuō)的是什么,看一下AI目錄下“mod”文件夾里的“AIOnDeckSelect.lua”文件询枚∥シ看到那一堆多到令人生畏的數(shù)字了嗎?是的金蜀,這些就是你需要為Add函數(shù)準(zhǔn)備的前方。

??這個(gè)系統(tǒng)已經(jīng)開(kāi)發(fā)了很多版。對(duì)于任何嚴(yán)肅的程序員來(lái)說(shuō)廉油,它可能過(guò)于臃腫惠险,而且很可能有更好的方法來(lái)處理這個(gè)問(wèn)題,但到目前為止抒线,它一直在為我工作班巩。每行代表一張卡片,而所有的數(shù)字列都是優(yōu)先級(jí)值嘶炭。最后一列是一個(gè)條件函數(shù)抱慌,可以為nil。列代表卡片所處的區(qū)域眨猎。第1列是手牌抑进,第3列是場(chǎng)上,第5列是墓地睡陪,第7列是靈擺寺渗,第9列是除外。偶數(shù)列與前一個(gè)奇數(shù)列代表相同的位置兰迫,但是是在有條件函數(shù)且條件不成立的情況下使用的信殊。例如你有一個(gè)條件,還有卡片在手牌汁果,檢查條件涡拘。條件成立?返回第1列据德。不成立鳄乏?返回第2列。如果沒(méi)有條件函數(shù)棘利,它總是返回第1列橱野。

??所以添加一張卡的時(shí)候,你要去想想如何使用這張卡赡译。你是想把它放在手牌中使用多一點(diǎn)仲吏?那么第1列的數(shù)值就應(yīng)該大一點(diǎn)(我常用1到10的數(shù)值)不铆。你需要它在墓地里蝌焚?就改第5列的數(shù)裹唆。如果你有另一張卡的時(shí)候你只想把它留在手牌?高優(yōu)先級(jí)用1只洒,低一點(diǎn)用2许帐,(這里作者可能寫錯(cuò)了,實(shí)際是數(shù)值越大優(yōu)先級(jí)越高毕谴,最小為0)再寫一個(gè)條件函數(shù)成畦,當(dāng)你有另一張卡的時(shí)候讓函數(shù)返回true。

??這聽(tīng)起來(lái)很復(fù)雜涝开,它是如何協(xié)助我們的循帐,我們又為什么需要它呢?現(xiàn)在很多卡片系列都有大量檢索卡片的效果舀武。如果你想去對(duì)所有這類效果逐個(gè)編碼,你就會(huì)有一大堆重復(fù)的用來(lái)查找特定卡的循環(huán)。如果你為你卡組里面的所有卡設(shè)置了一個(gè)優(yōu)先級(jí)残邀,并且定義了適當(dāng)?shù)臈l件函數(shù)判耕,那現(xiàn)在你就能用Add函數(shù)來(lái)執(zhí)行你的選卡效果了。

if id == 32807846 then -- 增援
  return Add(cards) -- 或 Add(cards,PRIO_TOHAND,1)
....

你還可以使用下列自定義常量:

PRIO_TOHAND
PRIO_TOFIELD
PRIO_TOGRAVE
PRIO_DISCARD=PRIO_TODECK=PRIO_EXTRA
PRIO_BANISH

??第4個(gè)常量取決于我所使用的卡組寻馏。對(duì)于水精鱗卡組或暗黑界卡組棋弥,我用它處理送墓類操作(注意從手卡丟棄和從手卡送墓是不一樣的操作)。還能用它來(lái)做放回卡組切洗诚欠,例如「大薰風(fēng)騎士 翠玉」或「星因士 天狼星」顽染。

??現(xiàn)在,如果你定義了適當(dāng)?shù)膬?yōu)先級(jí)和條件轰绵,你就能在那些以某種方式搜索或使用你的卡的函數(shù)中使用Add函數(shù)家乘。召喚一只混沌怪獸?Add(cards,PRIO_BANISH)會(huì)把你希望被除外的卡片從墓地除外藏澳∪示猓「愚蠢的埋葬/副葬」?Add(cards,PRIO_TOGRAVE)會(huì)把放在墓地更好用的卡送墓翔悠。然而业崖,它可能會(huì)導(dǎo)致代碼變得復(fù)雜,尤其是說(shuō)書(shū)卡組蓄愁。只要看一下圣騎士AI的條件函數(shù)就知道了双炕,就在NobleKnight.lua文件的開(kāi)頭。相當(dāng)?shù)幕靵y :)

??你需要在決斗的一開(kāi)始的時(shí)候使用AddPriority()函數(shù)來(lái)設(shè)置你的優(yōu)先級(jí)撮抓。

AddPriority函數(shù)的用法可以查看AIOnDeckSelect.lua

<h3 id='5_4'>CardsMatchingFilter</h3>

??另一個(gè)簡(jiǎn)單的循環(huán)函數(shù)妇斤,需要傳入一個(gè)卡片組和一個(gè)過(guò)濾器,然后返回在這個(gè)卡片組中有多少?gòu)埧M足過(guò)濾器的條件。過(guò)濾器可以是任何函數(shù)站超,只要它傳入一張卡并返回一個(gè)布爾值荸恕,例如:

function FilterLevel4(c)
  return c.level==4
end
local count = CardsMatchingFilter(cards,FilterLevel4) -- 統(tǒng)計(jì)所有卡片列表中等級(jí)為4的怪獸

??我的很多自定義函數(shù)都支持可選的過(guò)濾器,并且它們當(dāng)中的大部分能把一個(gè)額外的可選參數(shù)作為第二個(gè)參數(shù)傳入需要兩個(gè)參數(shù)的過(guò)濾器死相。它允許使用一些類型常量來(lái)過(guò)濾融求,如下:

local count = CardsMatchingFilter(cards,FilterType,TYPE_MONSTER) -- 統(tǒng)計(jì)所有怪獸卡

??FilterType是一個(gè)有兩個(gè)參數(shù)的過(guò)濾器,它把不被過(guò)濾的類型與卡的類型進(jìn)行比較算撮。還有諸如FilterRace生宛,F(xiàn)ilterAttribute,F(xiàn)ilterPosition肮柜,F(xiàn)ilterLocation等等的用法類似陷舅。之前提到的那些函數(shù)都可以添加過(guò)濾器為參數(shù),比如HasID审洞,Add蔑赘,BestTargets,所以你要注意過(guò)濾器在參數(shù)列表中的具體位置预明。認(rèn)真看看API列表缩赛,記住正確的參數(shù)順序。

  • HasID撰糠,HasIDNotNegated和BestTargets函數(shù)的定義在AIHelperFunctions2.lua
  • Add函數(shù)的定義在AIOnDeckSelect.lua

<h3 id='5_5'>FieldCheck</h3>

??一種簡(jiǎn)單的方式來(lái)統(tǒng)計(jì)AI控制的一個(gè)特定等級(jí)的卡有多少?gòu)埶肘桑1挥糜诔空賳緳z查。FieldCheck(4)返回當(dāng)前AI控制著多少只等級(jí)為4的怪獸阅酪。這個(gè)函數(shù)也支持一個(gè)過(guò)濾器旨袒,FieldCheck(4,FilterRace,RACE_WINDBEAST)返回AI控制的等級(jí)為4的鳥(niǎo)獸族怪獸的數(shù)量。

<h3 id='5_6'>DestroyCheck</h3>

??還記得「破壞者」嗎术辐?還記得我說(shuō)過(guò)的砚尽,當(dāng)對(duì)手只有我們不想破壞的卡時(shí)候,我們需要一個(gè)額外的檢查來(lái)避免發(fā)動(dòng)卡的效果辉词。DestroyCheck就是為此而生必孤。它遍歷一個(gè)卡片列表然后返回可以被破壞的對(duì)象的數(shù)量。所以「破壞者」的效果發(fā)動(dòng)檢查看起來(lái)如下:

function UseBreaker()
  return DestroyCheck(OppST())>0
end

if HasID(Act,71413901) and UseBreaker() then
  return COMMAND_ACTIVATE,CurrentIndex
end

<h3 id='5_7'>Affected 和 Targetable</h3>

??有很多的怪獸瑞躺,它們通過(guò)不能被選為效果對(duì)象或不受特定卡的效果影響來(lái)實(shí)現(xiàn)保護(hù)敷搪。這是很難察覺(jué)的,由于在AI腳本中不受其他卡的效果影響和只是不受魔法·陷阱卡的效果影響(比如「禁忌的圣槍」)之間是沒(méi)有區(qū)別的幢哨。還有一些卡不是不能被選為效果對(duì)象赡勘,但可以無(wú)效取對(duì)象效果甚至當(dāng)它們被選為對(duì)象時(shí)會(huì)返傷。

??Affected傳入要檢查的卡捞镰,一個(gè)類型常量和一個(gè)等級(jí)或階級(jí)闸与,它能檢查出機(jī)殼怪獸是否對(duì)效果免疫象使用了「禁忌的圣槍」毙替。然而,這可能不會(huì)100%準(zhǔn)確践樱,比如「禁忌的圣槍」的例子中只能檢查出是否有不受效果影響的Buff和攻擊力的下降厂画。它不能確切地檢查出,這些是不是由「禁忌的圣槍」導(dǎo)致的映胁。Targetable傳入那張卡和一個(gè)類型。

??對(duì)于像「鳳翼的爆風(fēng)」這樣的卡甲雅,你可能會(huì)這樣來(lái)檢查對(duì)象:

if Targetable(c,TYPE_TRAP) and Affected(c,TYPE_TRAP) then
  return true

??對(duì)于「鳥(niǎo)銃士 卡斯泰爾」解孙,你則可以這樣用:

if Targetable(c,TYPE_MONSTER) and Affected(c,TYPE_MONSTER,4) then
  return true

這個(gè)函數(shù)的意思是:如果怪獸可以被選為怪獸效果對(duì)象且受等級(jí)或階級(jí)為4的怪獸的效果影響,則返回true

??還會(huì)有更多的函數(shù)∨兹耍現(xiàn)在還在考慮弛姜,哪個(gè)足夠重要到要在這里提及。

<h2 id='6'>6. 常見(jiàn)問(wèn)題</h2>

在寫AI的時(shí)候妖枚,很多代碼可能是有問(wèn)題的廷臼。我會(huì)把我遇到的一些問(wèn)題拿出來(lái)講講,以及告訴你們?nèi)绾谓鉀Q它們或圍繞它們展開(kāi)工作绝页。

<h3 id='6_1'>索引系統(tǒng)</h3>

YGOPro的AI使用一個(gè)復(fù)雜的卡片列表和索引系統(tǒng)荠商,很多函數(shù)都是提供給你可使用的卡片列表然后要求你返回正確的索引。一個(gè)問(wèn)題是续誉,首先一個(gè)很難理解的問(wèn)題是莱没,即使技術(shù)上指向同一張卡片,卡片之間也不能兼容酷鸦。

??例如饰躲,你有一張卡,它能把所有對(duì)手的怪獸選為對(duì)象臼隔,而OnSelectCard提供一個(gè)可選對(duì)象的卡片列表∴诹眩現(xiàn)在你也能在OnSelectCard中用OppMon(),雖然理論上它返回存在相同怪獸的卡片列表摔握。然而寄狼,這兩個(gè)列表的卡是不一樣的,你不能簡(jiǎn)單地通過(guò)像if cards[i]==OppMon[i]的代碼來(lái)比較它們氨淌。即使它們就是指同一張卡例嘱,它還是會(huì)返回false。幸好宁舰,卡片有一個(gè)唯一屬性card.cardid拼卵,它在決斗中為每一張卡片提供一個(gè)唯一ID。所以蛮艰,你可以用它來(lái)比較不同列表的兩張卡是否為同一張腋腮,例如:if cards[i].cardid == OppMon[i].cardid

card.id精確到相同卡密,card.cardid精確到單卡

??索引也有它的故事即寡。很多函數(shù)要求你去返回一個(gè)索引徊哑。這個(gè)索引是基于卡片所在的由函數(shù)提供的列表的原本位置。我的很多自定義函數(shù)實(shí)際上改變了卡片在列表中的位置聪富,通過(guò)攻擊力或優(yōu)先級(jí)等的因素對(duì)他們排序莺丑。如果你也想這樣做,請(qǐng)確保在此之前復(fù)制一份列表墩蔓,這樣你就還有途徑去獲得原始索引梢莽,或者你以某種方式存儲(chǔ)那個(gè)索引。我通常就把那個(gè)索引存在一個(gè)額外的地方奸披,card.index=i昏名。像這樣的額外之地都是臨時(shí)的,它們只存在于當(dāng)前的卡片列表阵面。如果相同的卡出現(xiàn)在一個(gè)不同的列表當(dāng)中轻局,任何像這樣的地方都會(huì)消失。但是因?yàn)槲覀冎恍枰莻€(gè)索引存在于當(dāng)前列表样刷,這樣處理就很好了仑扑。

<h3 id='6_2'>一卡多效</h3>

??如果一張卡能同時(shí)發(fā)動(dòng)多個(gè)效果,它會(huì)分別在 OnSelectInit 或 OnSelectChain 的可以發(fā)動(dòng)/連鎖的卡片的列表中多次出現(xiàn)置鼻。一個(gè)常見(jiàn)的例子是「熔巖谷鎖鏈龍」夫壁。如果你可以選擇,那么送墓和抓卡效果會(huì)以不同的卡出現(xiàn)在可以發(fā)動(dòng)的卡片的列表當(dāng)中沃疮。就這個(gè)例子而言盒让,卡片會(huì)有一個(gè)"description"屬性來(lái)區(qū)分它們。對(duì)于「熔巖谷鎖鏈龍」司蔬,它的description是545382497對(duì)應(yīng)送墓或545382498對(duì)應(yīng)抓卡邑茄。

??那我們?cè)趺从盟兀磕隳軐懸粋€(gè)循環(huán)來(lái)檢查:

for i=1,#cards do
  local c = cards[i]
  if c.id == 34086406 and c.description == 545382497 then --熔巖谷鎖鏈龍 送墓效果
  if c.id == 34086406 and c.description == 545382498 then --熔巖谷鎖鏈龍 抓卡效果

??或者使用HasID的第四個(gè)參數(shù):

if HasID(cards,34086406,nil,545382497) then --熔巖谷鎖鏈龍 送墓效果

??你能使用 print 函數(shù)來(lái)查詢你的卡使用的description值俊啼。

for i=1,#cards do
  local c = cards[i]
  if c.id == 34086406 then
    print (c.description)
  end
end

??如果AI只控制著一只「熔巖谷鎖鏈龍」并且兩個(gè)效果都能發(fā)動(dòng)肺缕,它應(yīng)該打印出:
545382497 545382498

感謝OhnkytaBlabdey的補(bǔ)充:
卡片效果的序號(hào) = c.description - c.id * 16

可能的用法如下:

local c = cards[i], effectID = c.description - c.id * 16
if effectID == 1 then
-- 這里寫具體的處理邏輯
return COMMAND_ACTIVATE,i
elseif effectID == 2 then
...

<h3 id='6_3'>一效選多卡</h3>

??一些卡會(huì)在 OnSelectCard 中多次發(fā)動(dòng)不同的效果,有時(shí)甚至是相同效果授帕。特別是超量怪獸幾乎總是需要先選一個(gè)超量素材對(duì)象同木,在他們選擇一個(gè)場(chǎng)上的對(duì)象之前。

??那我們?nèi)绾翁幚硭仵耸縊nSelectCard只提供發(fā)動(dòng)效果的卡的id彤路,它沒(méi)有任何關(guān)于當(dāng)前所用效果的信息。如果我不能用一行代碼處理卡片的選擇芥映,我通常在外面寫一個(gè)獨(dú)立函數(shù)來(lái)處理洲尊。在我們的「熔巖谷鎖鏈龍」的例子中我們有同樣的問(wèn)題远豺,因?yàn)樗袃蓚€(gè)不同的效果且是一只超量怪獸。我常常使用兩種方式來(lái)決定正確的對(duì)象的選擇:檢查可選對(duì)象的與效果相關(guān)的一些特殊信息坞嘀,或在此之前設(shè)置一個(gè)全局變量躯护。

??我們可以寫一個(gè)如下的函數(shù):

function LavalvalChainTarget(cards)
  if LocCheck(cards,LOCATION_OVERLAY) then -- 檢查第一個(gè)可選對(duì)象是否為一個(gè)超量素材
    return Add(cards,PRIO_TOGRAVE) -- 是超量素材時(shí)的操作
  end
  if GlobalCardMode==1 then -- 用全局變量來(lái)標(biāo)記效果的類型
    GlobalCardMode = nil -- 記得重置
    return Add(cards)
  end
  return Add(cards,PRIO_TOGRAVE)
end
function OnSelectCard(cards,...
  if id == 34086406 then
    return LavalvalChainTarget(cards)
  end
...

??LocCheck是又一個(gè)自定義函數(shù),它檢查第一個(gè)參數(shù)對(duì)象的所在區(qū)域丽涩,像這樣:bit32.band(cards[1].location,LOCATION_OVERLAY)>0

??在發(fā)動(dòng)效果之前我們還要去設(shè)置全局變量:

if HasID(cards,34086406,nil,545382497) then --熔巖谷鎖鏈龍 送墓效果
  return COMMAND_ACTIVATE,CurrentIndex
end
if HasID(cards,34086406,nil,545382498) then --熔巖谷鎖鏈龍 抓卡效果
  GlobalCardMode = 1 -- 設(shè)置全局變量為1來(lái)標(biāo)記抓卡效果使用中
  return COMMAND_ACTIVATE,CurrentIndex
end

??為什么在這里我們同時(shí)使用了 LocCheck 和全局變量呢棺滞?我們要能正確的分辨出卡片因何被選。如果對(duì)象是一個(gè)超量素材矢渊,這能明顯地被檢查出來(lái)继准。但是對(duì)于兩個(gè)效果的選擇,如果僅僅通過(guò)接受效果的對(duì)象卡片昆淡,我們沒(méi)有一個(gè)簡(jiǎn)單的方式來(lái)知道锰瘸,當(dāng)前它被使用了哪一個(gè)效果刽严。對(duì)于這兩個(gè)效果昂灵,效果對(duì)象都是在卡組,我們不能檢查出效果會(huì)把它們送去哪里舞萄。當(dāng)對(duì)象是魔法卡的時(shí)候我們能推測(cè)出來(lái)(用了哪個(gè)效果)眨补,因?yàn)樽タㄐЧ荒茏ス肢F卡但送墓也能送魔法卡或陷阱卡,所以這種推測(cè)對(duì)于純怪獸卡組或在魔法陷阱卡都用光了的決斗后期可能會(huì)無(wú)用倒脓。所以為了正確的知道用了哪個(gè)效果撑螺,我們需要全局變量,至少我是這么認(rèn)為的崎弃。

??要是我想起更多問(wèn)題我還會(huì)加進(jìn)來(lái)甘晤。

<h2 id='7'>7. 使用卡片腳本系統(tǒng)的函數(shù)</h2>

??你也許熟悉YGOPro的卡片腳本。有很多的卡片腳本函數(shù)可用饲做,它們提供各種信息或功能雖然AI用不了线婚。但這些還是很有用的,比如在戰(zhàn)斗階段幫助決定攻擊者和攻擊對(duì)象盆均,或在連鎖效果中獲取關(guān)于當(dāng)前連鎖的信息和據(jù)此作出回應(yīng)塞弊。

<h3 id='7_1'>偵測(cè)離場(chǎng)效果</h3>

??提供對(duì)離場(chǎng)效果的保護(hù),或有卡要離場(chǎng)的時(shí)候能連鎖發(fā)動(dòng)效果對(duì)卡片來(lái)說(shuō)會(huì)很有用泪姨。你能通過(guò)使用一個(gè)叫Duel.GetOperationInfo的函數(shù)來(lái)實(shí)現(xiàn)這個(gè)游沿,如下:

local ex,cg = Duel.GetOperationInfo(Duel.GetCurrentChain(),CATEGORY_DESTROY)
if ex then
  return cg -- 返回將要被效果破壞的卡的列表
end

??這個(gè)例子中cg是一個(gè)卡片組。不幸的是肮砾,它們與AI腳本所能直接使用的卡片組是不同的诀黍,還需要另行處理一下。你不能用c.id仗处,你需要用c:GetCode()代替蔗草。一個(gè)組和一個(gè)表也是不同的咒彤,你不能通過(guò)索引訪問(wèn)組的成員就像cg[i],你要使用組的相關(guān)函數(shù)來(lái)實(shí)現(xiàn)咒精。

組和表分別是卡片腳本系統(tǒng)和AI系統(tǒng)使用的兩種不同的數(shù)據(jù)結(jié)構(gòu)镶柱,盡管他們都負(fù)責(zé)存儲(chǔ)若干張卡片,之前提到的卡片列表都是表數(shù)據(jù)類型模叙。因?yàn)閿?shù)據(jù)類型的差異歇拆,對(duì)這兩種數(shù)據(jù)的操作所用的方法也會(huì)不同。

??但你很走運(yùn)范咨,我寫了一些自定義函數(shù)來(lái)處理這些故觅,并且它們很易用。例如渠啊,如果我們想AI用「旋風(fēng)」連鎖發(fā)動(dòng)它的破壞效果输吏,我們能這樣做:

function ChainMST(card)
  local targets = DestroyCheck(OppST())
  if targets>0 and RemovalCheckCard(card) then
    return true
  end
  return false
end
function OnSelectChain(cards...
  if HasID(cards,05318639,ChainMST) then -- HasID會(huì)檢查第三個(gè)參數(shù)是否為函數(shù),如果是就把它作為過(guò)濾器
    return true
  end
end

??RemovalCheckCard 檢查傳入的卡在當(dāng)前的連鎖中是否即將以某種方式離場(chǎng)替蛉。它檢查大部分常見(jiàn)的離場(chǎng)方式贯溅,如 破壞,除外躲查,返回手卡或卡組它浅,送去墓地等等×椭螅可選參數(shù)允許指定連鎖的效果類型姐霍,連鎖序號(hào),發(fā)動(dòng)離場(chǎng)效果的卡的類型(還支持過(guò)濾器)典唇。

??這樣一來(lái)镊折,AI的「旋風(fēng)」應(yīng)該能連鎖發(fā)動(dòng)一個(gè)離場(chǎng)效果了,如果對(duì)手控制著一張可選對(duì)象那它也會(huì)被破壞介衔。

<h3 id='7_2'>效果無(wú)效化</h3>

??與偵測(cè)離場(chǎng)效果十分的相似恨胚,我們能在連鎖中檢查當(dāng)前的效果來(lái)知道它是否會(huì)被像「星塵龍」這樣的卡無(wú)效化∫鼓担或者我們能檢查整個(gè)連鎖与纽,看看是否有什么需要用像「技能突破」這樣的卡來(lái)無(wú)效化。

??這些能通過(guò)結(jié)合Duel.GetChainInfo函數(shù)來(lái)實(shí)現(xiàn)塘装,如下:

local e = Duel.GetChainInfo(Duel.GetCurrentChain(),CHAININFO_TRIGGERING_EFFECT)
if e then -- 通過(guò)檢查 e(e包含發(fā)效果卡片)

??當(dāng)然急迂,我們也有一些易用的自定義函數(shù)來(lái)實(shí)現(xiàn)它,函數(shù)能在內(nèi)部?jī)?yōu)雅地處理所有這些且有一些額外的好處蹦肴。

??例如僚碎,很多卡能通過(guò)發(fā)動(dòng)效果或反擊陷阱或類似的東西來(lái)無(wú)效上一個(gè)連鎖,這些能通過(guò) ChainNegation 函數(shù)來(lái)處理阴幌。讓我們拿「反查」來(lái)做個(gè)例子勺阐,你能在 OnSelectChain 里像這樣用它:

if HasIDNotNegated(cards,34507039,ChainNegation) then -- 反查
  return {1,CurrentIndex}
end

??如果你恰巧用到它卷中,它不僅會(huì)無(wú)效你的對(duì)手的卡片效果,它還會(huì)檢查是否效果在永不無(wú)效的黑名單中(比如「哥布林暴發(fā)戶」)渊抽,然后他會(huì)把這個(gè)連鎖標(biāo)記為無(wú)效蟆豫,這樣像 RemovalCheck 之類的就會(huì)被忽略。所以如果你連鎖無(wú)效了某些炸場(chǎng)效果懒闷,AI會(huì)意識(shí)到這一點(diǎn)且不會(huì)連鎖所有魔陷來(lái)對(duì)付炸場(chǎng)十减。然而,現(xiàn)階段愤估,AI不能檢查出是否無(wú)效的效果被無(wú)效帮辟。所以如果對(duì)手企圖用「黑薔薇龍」炸場(chǎng),你用像「星塵龍」這樣的卡來(lái)無(wú)效她玩焰,但他又連鎖了一張「天罰」由驹,AI將不會(huì)意識(shí)到還是會(huì)被炸場(chǎng)并且不會(huì)再連鎖發(fā)動(dòng)魔陷,甚至明明它有機(jī)會(huì)反擊(假設(shè)你沒(méi)有什么能在「天罰」之后連鎖發(fā)動(dòng)的 :D)

??還有一個(gè)類似的函數(shù)來(lái)處理像「技能突破」這樣的卡昔园,盡管用法有些許不同蔓榄。它返回應(yīng)該被無(wú)效的卡片,相應(yīng)的你需要確保處理對(duì)象的選擇:

function ChainBTS(card)
  local c = ChainCardNegation(card,true) -- 第二個(gè)參數(shù)用于對(duì)象檢查蒿赢。像技能突破那樣取對(duì)象的無(wú)效用true润樱,像技能抽取那樣不取對(duì)象的用false
  if c then
    GlobalTargetSet(c)
    return true
  end
end

??GlobalTargetSet 找到場(chǎng)上的對(duì)象并將它存進(jìn)一個(gè)全局變量渣触。它是設(shè)計(jì)用來(lái)配合 GlobalTargetGet 工作的羡棵,后者用來(lái)從卡片的列表中恢復(fù)對(duì)象并在 OnSelectCard 里使用:

function BTSTarget(cards)
  return GlobalTargetGet(cards,true) -- 第二個(gè)參數(shù)決定是返回一張卡還是卡的索引。true = index嗅钻,false = card
end

??這會(huì)通過(guò)要被無(wú)效的卡把連鎖標(biāo)記為發(fā)動(dòng)皂冰。(這里的翻譯有疑問(wèn))

<h2 id='8'>8. 添加新的卡組到已有的AI</h2>

??到目前為止,我們所學(xué)的一切都可以用來(lái)制作我們自己的AI腳本养篓。然而秃流,一個(gè)單獨(dú)的AI腳本文件確實(shí)帶來(lái)了一些問(wèn)題。你必須重復(fù)很多已經(jīng)由標(biāo)準(zhǔn)AI處理了的代碼柳弄,每次你想再玩你的卡組舶胀,你都需要選擇一個(gè)不同的AI文件,且不能使用“隨機(jī)卡組”選項(xiàng)碧注,因?yàn)槌绦虿荒茏詣?dòng)記憶使用過(guò)的AI文件嚣伐。將新代碼集成到現(xiàn)有的AI中也是相當(dāng)麻煩的,你需要處理我的大量代碼萍丐,也可能會(huì)干擾已經(jīng)存在的卡組等等轩端。

??0.28版AI提供了解決這個(gè)問(wèn)題的方案。我寫了一系列自定義函數(shù)來(lái)幫助集成自定義AI卡組到已有的AI逝变,僅通過(guò)簡(jiǎn)單幾步即可基茵。它允許你的卡組和函數(shù)100%獨(dú)立于其他卡組奋构,并且依舊給予你利用標(biāo)準(zhǔn)AI來(lái)處理大部分卡的選擇,如果你想拱层。我們會(huì)使用之前的「魔導(dǎo)戰(zhàn)士 破壞者」的例子來(lái)看看我們?nèi)绾文馨阉傻綐?biāo)準(zhǔn)AI弥臼。

<h3 id='8_1'>第一步:一個(gè)空AI的最低要求</h3>

??把你自己的卡組添加到AI的最低要求是什么?

  • (有你的卡組文件.ydk根灯。本例中醋火,它應(yīng)該有至少一張「魔導(dǎo)戰(zhàn)士 破壞者」)
  • 在你的YGOPro的安裝目錄下的ai/decks目錄下創(chuàng)建一個(gè)新的LUA文件,起一個(gè)你喜歡的名字箱吕。我們會(huì)用 Breaker.lua
  • 添加一行代碼到你的文件芥驳,如下:
DECK_BREAKER = NewDeck("Breaker",71413901,nil)

??從現(xiàn)在開(kāi)始DECK_BREAKER就是存儲(chǔ)你的卡組的變量。
NewDeck 有3個(gè)參數(shù):

  1. 你的卡組名茬高。主要用于調(diào)試模式下顯示當(dāng)前所用的卡組兆旬。名字隨你起,但它應(yīng)該有區(qū)分度怎栽,比如用你的卡組的系列名(Nekroz)或常用簡(jiǎn)稱(HAT)丽猬。
    因此本例中我們起名為Breaker。
  2. 卡組的識(shí)別碼熏瞄。它可以是卡片密碼脚祟,或一個(gè)卡片密碼的列表。如果你想只用一個(gè)卡密强饮,要確保它能把你的卡組與其他卡組區(qū)分開(kāi)由桌。例如,用「炎舞-天璣」來(lái)標(biāo)識(shí)炎星卡組也許是一個(gè)壞主意邮丰,因?yàn)椤柑飙^」能被用于任何獸戰(zhàn)士族為主的卡組行您。
    對(duì)于我們的例子,我們會(huì)用「破壞者」的卡片密碼剪廉,71413901娃循。
  3. 啟動(dòng)函數(shù)。它會(huì)在決斗一開(kāi)始時(shí)被調(diào)用斗蒋,如果AI偵測(cè)到你的卡組捌斧。不是必須的,但你可能會(huì)用到泉沾。
    我們之后會(huì)添加捞蚂,現(xiàn)在暫且用nil

??現(xiàn)在你有一個(gè)除了一行代碼之外完全是空的文件爆哑。下一步你要做的就是打開(kāi)你的ai.lua文件洞难,然后添加你的文件到請(qǐng)求列表:

...
require("ai.decks.Constellar")
require("ai.decks.Blackwing")
require("ai.decks.Harpie")
require("ai.decks.Breaker") <--

??就這樣,你添加了你的第一個(gè)卡組文件。現(xiàn)在來(lái)試一下队贱,以調(diào)試模式啟動(dòng)YGOPro色冀。對(duì)于Windows用戶,通過(guò)你的安裝目錄下的第二個(gè)exe文件柱嫌,名字叫“ygopro_vs_ai_debug.exe”來(lái)啟動(dòng)锋恬。游戲應(yīng)該會(huì)啟動(dòng),還會(huì)有一個(gè)命令行窗口编丘,顯示了一些文字与学。使用其他操作系統(tǒng)的用戶需要通過(guò)console來(lái)啟動(dòng)YGOPro來(lái)獲得調(diào)試信息。開(kāi)始一個(gè)游戲與你的AI卡組對(duì)戰(zhàn)嘉抓∷魇兀看看console,它應(yīng)該會(huì)在游戲開(kāi)始的時(shí)候顯示一個(gè)信息(在rock/paper/scissors之后):“AI deck is Breaker.”

??如果這條信息出現(xiàn)了抑片,這就意味著卵佛,AI已經(jīng)利用你指定的識(shí)別卡識(shí)別出了你的卡組。顯然這個(gè)做法不是十分的準(zhǔn)確敞斋,它也有可能會(huì)匹配到你的其他卡組截汪,這個(gè)稍后再談。如果這條信息沒(méi)現(xiàn)植捎,肯定是哪里錯(cuò)了衙解。檢查之前的步驟。請(qǐng)確認(rèn)AI使用了正確的卡組焰枢,你指定了正確的識(shí)別碼和添加了請(qǐng)求蚓峦。

<h3 id='8_2'>添加你的代碼到你的AI</h3>

??現(xiàn)在我們已經(jīng)設(shè)置好了,我們可以開(kāi)始用我們的文件做一些事情了医咨。就目前而言枫匾,它只不過(guò)是偵測(cè)卡組而已架诞。一切仍然由標(biāo)準(zhǔn)AI處理拟淮。一旦設(shè)置了啟動(dòng)函數(shù),情況就會(huì)發(fā)生變化:

function BreakerStartup(deck)
end

DECK_BREAKER = NewDeck("Breaker",71413901,BreakerStartup)

??注意谴忧,啟動(dòng)函數(shù)必須放在之前那行代碼的前面很泊。參數(shù)是我們的卡組,同樣放到DECK_BREAKER里沾谓。

??現(xiàn)在我們能用啟動(dòng)函數(shù)來(lái)修改卡組的參數(shù)委造,例如調(diào)用各種AI函數(shù)或添加卡片到各種AI的黑名單:

function MyDeckStartup(deck)
  deck.Init = MyDeckInit

  deck.SummonBlacklist = MyDeckSummonBlacklist
end

function MyDeckInit(cards, to_bp_allowed, to_ep_allowed)
  local Act = cards.activatable_cards
  local Sum = cards.summonable_cards
  local SpSum = cards.spsummonable_cards
  local Rep = cards.repositionable_cards
  local SetMon = cards.monster_setable_cards
  local SetST = cards.st_setable_cards
  return nil
end

MyDeckSummonBlacklist = {71413901}

??在一個(gè)獨(dú)立的AI文件中,如果你給deck.Init賦值為一個(gè)函數(shù)均驶,AI會(huì)像你的標(biāo)準(zhǔn) OnSelectInitCommand 函數(shù)那樣調(diào)用它昏兆。你能在里面以相同的方式處理所有卡片。

??deck.SummonBlacklist是一個(gè)卡密的列表妇穴。里面的卡不會(huì)被標(biāo)準(zhǔn)AI召喚或特殊召喚爬虱,如果這些卡存在于你的卡組隶债。

??對(duì)于函數(shù)和各種名單的完整列表,請(qǐng)參考文檔模板. 比較重要的幾個(gè)是:

deck.Init -- OnSelectInit
deck.Card -- OnSelectCard
deck.Chain -- OnSelectChain
deck.EffectYesNo -- OnSelectEffectYesNo

deck.ActivateBlacklist -- 此列表中的卡不會(huì)被發(fā)動(dòng)或連鎖
deck.SummonBlacklist -- 此列表中的卡不會(huì)被召喚跑筝,特殊召喚死讹,反轉(zhuǎn)召喚,蓋放
deck.PriorityList -- 設(shè)置優(yōu)先級(jí)列表曲梗。參考 AIOnDeckSelect.lua 和 Add 函數(shù)

??注意赞警,如果你用了黑名單,在游戲中除非你用你的腳本處理了這些卡虏两。否則愧旦,它們就不會(huì)被使用。

??從現(xiàn)在開(kāi)始定罢,如有需要我們就能添加更多的卡和函數(shù)忘瓦。好處是,你不需要把你的AI做的很完整引颈。你完全可以只添加一個(gè)Init函數(shù)來(lái)處理幾張卡的召喚耕皮,剩下的全交給AI處理。如果這就是AI要的蝙场,那就更完美了凌停。你甚至可以什么都不管,只是創(chuàng)建個(gè)文件售滤,添加請(qǐng)求代碼和啟動(dòng)函數(shù)罚拟,注冊(cè)init函數(shù),然后收工完箩。所有你沒(méi)有指定的東西都會(huì)由默認(rèn)AI處理赐俗。如果標(biāo)準(zhǔn)AI已經(jīng)能適當(dāng)?shù)奶幚砟切┲饕南葳澹饕念~外卡組的怪獸等等卡弊知,你就可以輕松忽略這些卡阻逮。但是如果想AI不用某張卡,只需加入黑名單然后編寫你自己的邏輯來(lái)代替秩彤,這不會(huì)影響到其他卡組叔扼,因?yàn)楹诿麊沃贿m用于你自己的卡組。

??注意漫雷,你不需要使用我的大多數(shù)自定義函數(shù)瓜富。你可以自由發(fā)揮來(lái)設(shè)置你的卡組和卡組函數(shù)。你可以使用第4章中講到的原生的腳本函數(shù)降盹,或者如果你對(duì)我的函數(shù)不滿意与柑,也可以編寫自己的自定義函數(shù)。

<h3 id='8_3'>讓卡組工作起來(lái)</h3>

??在這一節(jié),我們整合前面章節(jié)的所有內(nèi)容用「破壞者」和「雷王」為AI添加一個(gè)新卡組价捧。我們通過(guò)使用卡組模板文件來(lái)開(kāi)始每辟。

  • 第一步是把模板用一個(gè)新名字另存一份。存為Breaker.lua放在你的“ai/decks”下干旧。
  • 打開(kāi)模板渠欺,把MyDeck全部替換為Breaker。大部分文本或代碼編輯器都有“全部替換”功能椎眯。
  • 把卡組名從My Deck改為Breaker:
DECK_BREAKER = NewDeck("Breaker",BreakerIdentifier,BreakerStartup)
  • 變更卡組識(shí)別碼挠将。你也能用多張卡來(lái)識(shí)別卡組。用這段代碼编整,任何同時(shí)包含「破壞者」和「雷王」的卡組會(huì)被識(shí)別為Breaker卡組:
BreakerIdentifier = {71413901,71564252}
  • 添加卡組請(qǐng)求到ai.lua舔稀,如果之前沒(méi)做:
...
require("ai.decks.Constellar")
require("ai.decks.Blackwing")
require("ai.decks.Harpie")
require("ai.decks.Breaker") <--

??好的,基本步驟完成≌撇猓現(xiàn)在我們整合之前添加的功能内贮。

  • 添加「破壞者」和「雷王」到發(fā)動(dòng)黑名單:
BreakerActivateBlacklist={ -- 添加永不發(fā)動(dòng)/連鎖的卡到這里
71413901, -- 破壞者
71564252, -- 雷王
}
  • 添加先前為「破壞者」和「雷王」編寫的代碼到各自的函數(shù)。我運(yùn)用前面章節(jié)講到的知識(shí)修改了一些函數(shù)汞斧。
    使用「破壞者」:
function UseBreaker()
  return DestroyCheck(OppST())>0
end
function BreakerInit(cards)
  local Act = cards.activatable_cards
  -- 在這里添加 OnSelectInit 的邏輯
  if HasID(Act,71413901,UseBreaker) then
    return COMMAND_ACTIVATE,CurrentIndex
  end
  return nil
end

對(duì)象選擇:

function BreakerTarget(cards)
  return BestTargets(cards,1,TARGET_DESTROY)
end
function BreakerCard(cards,min,max,id,c)
  -- 在這里添加 OnSelectCard 的邏輯
  if id == 71413901 then
    return BreakerTarget(cards)
  end
  return nil
end

連鎖「雷王」:

function TKROFilter(c)
  -- 一個(gè)對(duì)象是否應(yīng)該被雷王破壞的過(guò)濾檢查
  return FilterStatus(c,STATUS_SUMMONING)
  and c.attack>=1900
end
function ChainTKRO()
  return CardsMatchingFilter(OppMon(),TKROFilter)>0
end
function BreakerChain(cards)
  -- 這里添加 OnSelectChain 的邏輯
  if HasID(cards,71564252,ChainTKRO) then
    return 1,CurrentIndex
  end
  return nil
end
function BreakerEffectYesNo(id,card)
  -- 這里添加 OnSelectEffectYesNo 的邏輯
  if id == 71564252 and ChainTKRO() then
    return true
  end
  return nil
end

??你能在這個(gè)文件檢查最終結(jié)果夜郁。注意,你可以隨意擴(kuò)展粘勒。目前為止竞端,卡的通常召喚是由默認(rèn)邏輯處理的,因?yàn)槲覀儧](méi)有限制它們庙睡。你可以把它們添加到召喚黑名單中事富,并增加召喚條件,比如只召喚「破壞者」乘陪,如果對(duì)手控制著任何能被他的效果破壞的對(duì)象统台。或者只召喚「雷王」啡邑,如果對(duì)手沒(méi)有攻擊力大于1900的怪獸贱勃。

<h2 id='9'>9. 寫在最后</h2>

??這是AI腳本教程的總結(jié),我希望谣拣,你能使用它募寨。這里有很多知識(shí)需要吸收。為YGOPro AI編寫腳本不是一件簡(jiǎn)單的事森缠,在前進(jìn)的路上有很多障礙。我期待著測(cè)試你所有新的定制卡組仪缸,并最終集成到正式版AI:)(1.0才是正式版贵涵,路漫漫其修遠(yuǎn)兮...)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子宾茂,更是在濱河造成了極大的恐慌瓷马,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,273評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件跨晴,死亡現(xiàn)場(chǎng)離奇詭異欧聘,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)端盆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門怀骤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人焕妙,你說(shuō)我怎么就攤上這事蒋伦。” “怎么了焚鹊?”我有些...
    開(kāi)封第一講書(shū)人閱讀 167,709評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵痕届,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我末患,道長(zhǎng)研叫,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,520評(píng)論 1 296
  • 正文 為了忘掉前任璧针,我火速辦了婚禮蓝撇,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘陈莽。我一直安慰自己渤昌,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,515評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布走搁。 她就那樣靜靜地躺著独柑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪私植。 梳的紋絲不亂的頭發(fā)上忌栅,一...
    開(kāi)封第一講書(shū)人閱讀 52,158評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音曲稼,去河邊找鬼索绪。 笑死砚婆,一個(gè)胖子當(dāng)著我的面吹牛耗拓,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播和屎,決...
    沈念sama閱讀 40,755評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼窄坦,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼唤反!你這毒婦竟也來(lái)了凳寺?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,660評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤彤侍,失蹤者是張志新(化名)和其女友劉穎肠缨,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體盏阶,經(jīng)...
    沈念sama閱讀 46,203評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡晒奕,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,287評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了名斟。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片脑慧。...
    茶點(diǎn)故事閱讀 40,427評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖蒸眠,靈堂內(nèi)的尸體忽然破棺而出漾橙,到底是詐尸還是另有隱情,我是刑警寧澤楞卡,帶...
    沈念sama閱讀 36,122評(píng)論 5 349
  • 正文 年R本政府宣布霜运,位于F島的核電站,受9級(jí)特大地震影響蒋腮,放射性物質(zhì)發(fā)生泄漏淘捡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,801評(píng)論 3 333
  • 文/蒙蒙 一池摧、第九天 我趴在偏房一處隱蔽的房頂上張望焦除。 院中可真熱鬧,春花似錦作彤、人聲如沸膘魄。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,272評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)创葡。三九已至,卻和暖如春绢慢,著一層夾襖步出監(jiān)牢的瞬間灿渴,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,393評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工胰舆, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留骚露,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,808評(píng)論 3 376
  • 正文 我出身青樓缚窿,卻偏偏與公主長(zhǎng)得像棘幸,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子滨攻,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,440評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容

  • Lua 5.1 參考手冊(cè) by Roberto Ierusalimschy, Luiz Henrique de F...
    蘇黎九歌閱讀 13,831評(píng)論 0 38
  • ¥開(kāi)啟¥ 【iAPP實(shí)現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開(kāi)一個(gè)線程够话,因...
    小菜c閱讀 6,440評(píng)論 0 17
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫(kù)蓝翰、插件光绕、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,117評(píng)論 4 61
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,264評(píng)論 25 707
  • 曾經(jīng)女嘲,老板問(wèn)過(guò)我們一個(gè)問(wèn)題:假如公司今天就倒閉了,你們有沒(méi)有想過(guò)诞帐,你們能在其他行業(yè)欣尼,其他公司做什么? 這個(gè)問(wèn)題真戳...
    小碗月牙閱讀 256評(píng)論 1 2