mirror of
https://github.com/Qsgs-Fans/FreeKill.git
synced 2024-11-16 03:32:34 +08:00
Random AI (#54)
* android: dont copy RSA key and test.lua(generated by fkp) * remove debug code * ai think * fixbug: 100% cpu per thread * init ai * fix bug, next step is to remove all feasible * remame vscode -> lsp * add some lsp comment * rewrite feasible * Random AI * fixbug: chooseplayer * liuli * move checkNoHuman to waitForAiReply * prototype for cardLimitation skill * add Exppattern:Parse to static.lua * remove unnecessary static
This commit is contained in:
parent
afb537a661
commit
9ac89caa1f
|
@ -22,10 +22,11 @@ mkdir assets/res/packages
|
||||||
cp -r ../packages/standard assets/res/packages
|
cp -r ../packages/standard assets/res/packages
|
||||||
cp -r ../packages/standard_cards assets/res/packages
|
cp -r ../packages/standard_cards assets/res/packages
|
||||||
cp -r ../packages/test assets/res/packages
|
cp -r ../packages/test assets/res/packages
|
||||||
|
rm assets/res/packages/test/test.lua
|
||||||
cp ../packages/init.sql assets/res/packages
|
cp ../packages/init.sql assets/res/packages
|
||||||
cp -r ../qml assets/res
|
cp -r ../qml assets/res
|
||||||
cp -r ../server assets/res
|
mkdir assets/res/server
|
||||||
rm assets/res/server/users.db
|
cp ../server/init.sql assets/res/server
|
||||||
cp ../LICENSE assets/res
|
cp ../LICENSE assets/res
|
||||||
cp ../zh_CN.qm assets/res
|
cp ../zh_CN.qm assets/res
|
||||||
|
|
||||||
|
|
38
doc/dev/ai.md
Normal file
38
doc/dev/ai.md
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# FreeKill 的 AI 系统
|
||||||
|
|
||||||
|
> [dev](./index.md) > AI
|
||||||
|
|
||||||
|
___
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
备选算法:
|
||||||
|
|
||||||
|
- MCTS
|
||||||
|
- 神杀算法
|
||||||
|
|
||||||
|
___
|
||||||
|
|
||||||
|
## MCTS实现
|
||||||
|
|
||||||
|
实现该算法的最大难点在于如何模拟。
|
||||||
|
|
||||||
|
首先是树中各个节点的保存,我们自然无法给某个节点都分配一个Room对象。由于节点是通过根节点进行相应的决策拓展而来的,所以其实节点内部的数据可以保存为各个决策的数组。
|
||||||
|
|
||||||
|
然后节点首先要能知道自己的双亲节点和孩子节点,这个分别用一个值和一个数组表示就行了。
|
||||||
|
|
||||||
|
想要实现模拟的话,重点是如何创造一个一模一样的Room出来。指望lua提供完全clone一个coroutine所有内容或许不是很现实。以下是一种备选方案:
|
||||||
|
|
||||||
|
1. 首先Room初始化的时候也初始化一个AI用的Room
|
||||||
|
2. Room内要能够录像,记录所有的request结果和random生成的值。为此可能要自定义一个random函数对自带的math.random进行封装。
|
||||||
|
3. 在录像的时候,AI Room也跟着录像的内容进行更新。AI Room本质上也就是一个Room而已,或者可以是Room的子类,反正他的内容就是用这个方式和真Room即时同步的。
|
||||||
|
4. 在AI即将处理问题的时候,首先获得所有可行选项。根据算法,需要对某个节点进行randomplay。
|
||||||
|
5. randomplay的话如果直接用AI Room,那么回溯的时候如何回到先前的状态呢?
|
||||||
|
1. 考虑新建一个新的AI Room,然后重放录像以达到开始状态。这样每次randomplay之前都要先回复一下状态,而随着录像的加长这个过程也可能变长,导致AI越来越慢
|
||||||
|
2. 考虑真Room的所有字段全部复制给AI Room一份。但有个问题在于如何把程序控制流和栈也跳转到一样的地方。所以这个是很难实现的。
|
||||||
|
3. 所以考虑用方案1。为了缓解太慢的情况,可以把1和2结合起来。约定好在某个时间点(比如GameLogic:action中的那个死循环执行)就与Room交换数据,然后这时候复盘录像的起始时间点修改。这样的话为了从randomplay恢复状态,就有必要将此时交换的数据额外保存一份。为了能让Logic平安跑到那个时间点,从人凑齐直到那个时间点的录像也要保存一份。
|
||||||
|
6. 解决了模拟和回溯的问题的话,就可以考虑实现该算法了。
|
||||||
|
|
||||||
|
那么为了模拟,首先得实现一个RandomAI才行。
|
||||||
|
|
||||||
|
然后还有一项前置工作是服务器端的录像功能。得先做好这个才能进行AI编写啊。所以AI暂且推迟一下好了。
|
|
@ -12,3 +12,4 @@ FreeKill采用Qt框架提供底层支持,在上层使用lua语言开发。在U
|
||||||
- [数据库](./database.md)
|
- [数据库](./database.md)
|
||||||
- [UI](./ui.md)
|
- [UI](./ui.md)
|
||||||
- [包管理](./package.md)
|
- [包管理](./package.md)
|
||||||
|
- [AI](./ai.md)
|
||||||
|
|
|
@ -210,7 +210,7 @@ function CardFeasible(card, selected_targets)
|
||||||
return ActiveFeasible(t.skill, selected_targets, t.subcards)
|
return ActiveFeasible(t.skill, selected_targets, t.subcards)
|
||||||
end
|
end
|
||||||
|
|
||||||
local ret = c.skill:feasible(selected_targets, selected_cards)
|
local ret = c.skill:feasible(selected_targets, selected_cards, Self, c)
|
||||||
return json.encode(ret)
|
return json.encode(ret)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -289,11 +289,11 @@ function ActiveFeasible(skill_name, selected, selected_cards)
|
||||||
local ret = false
|
local ret = false
|
||||||
if skill then
|
if skill then
|
||||||
if skill:isInstanceOf(ActiveSkill) then
|
if skill:isInstanceOf(ActiveSkill) then
|
||||||
ret = skill:feasible(selected, selected_cards)
|
ret = skill:feasible(selected, selected_cards, Self, nil)
|
||||||
elseif skill:isInstanceOf(ViewAsSkill) then
|
elseif skill:isInstanceOf(ViewAsSkill) then
|
||||||
local card = skill:viewAs(selected_cards)
|
local card = skill:viewAs(selected_cards)
|
||||||
if card then
|
if card then
|
||||||
ret = card.skill:feasible(selected, selected_cards)
|
ret = card.skill:feasible(selected, selected_cards, Self, card)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -320,11 +320,11 @@ function Engine:filterCard(id, player, data)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Engine:currentRoom()
|
function Engine:currentRoom()
|
||||||
if ClientInstance then
|
if RoomInstance then
|
||||||
return ClientInstance
|
|
||||||
end
|
|
||||||
return RoomInstance
|
return RoomInstance
|
||||||
end
|
end
|
||||||
|
return ClientInstance
|
||||||
|
end
|
||||||
|
|
||||||
function Engine:getDescription(name)
|
function Engine:getDescription(name)
|
||||||
return self:translate(":" .. name)
|
return self:translate(":" .. name)
|
||||||
|
|
|
@ -1,8 +1,20 @@
|
||||||
---@class ActiveSkill : UsableSkill
|
---@class ActiveSkill : UsableSkill
|
||||||
|
---@field min_target_num integer
|
||||||
|
---@field max_target_num integer
|
||||||
|
---@field target_num integer
|
||||||
|
---@field target_num_table integer[]
|
||||||
|
---@field min_card_num integer
|
||||||
|
---@field max_card_num integer
|
||||||
|
---@field card_num integer
|
||||||
|
---@field card_num_table integer[]
|
||||||
local ActiveSkill = UsableSkill:subclass("ActiveSkill")
|
local ActiveSkill = UsableSkill:subclass("ActiveSkill")
|
||||||
|
|
||||||
function ActiveSkill:initialize(name)
|
function ActiveSkill:initialize(name)
|
||||||
UsableSkill.initialize(self, name, Skill.NotFrequent)
|
UsableSkill.initialize(self, name, Skill.NotFrequent)
|
||||||
|
self.min_target_num = 0
|
||||||
|
self.max_target_num = 999
|
||||||
|
self.min_card_num = 0
|
||||||
|
self.max_card_num = 999
|
||||||
end
|
end
|
||||||
|
|
||||||
---------
|
---------
|
||||||
|
@ -33,13 +45,97 @@ function ActiveSkill:targetFilter(to_select, selected, selected_cards)
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function ActiveSkill:getMinTargetNum()
|
||||||
|
local ret
|
||||||
|
if self.target_num then ret = self.target_num
|
||||||
|
elseif self.target_num_table then ret = self.target_num_table
|
||||||
|
else ret = self.min_target_num end
|
||||||
|
|
||||||
|
if type(ret) == "function" then
|
||||||
|
ret = ret(self)
|
||||||
|
end
|
||||||
|
if type(ret) == "table" then
|
||||||
|
return ret[1]
|
||||||
|
else
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ActiveSkill:getMaxTargetNum(player, card)
|
||||||
|
local ret
|
||||||
|
if self.target_num then ret = self.target_num
|
||||||
|
elseif self.target_num_table then ret = self.target_num_table
|
||||||
|
else ret = self.max_target_num end
|
||||||
|
|
||||||
|
if type(ret) == "function" then
|
||||||
|
ret = ret(self)
|
||||||
|
end
|
||||||
|
if type(ret) == "table" then
|
||||||
|
ret = ret[#ret]
|
||||||
|
end
|
||||||
|
|
||||||
|
local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or {}
|
||||||
|
for _, skill in ipairs(status_skills) do
|
||||||
|
local correct = skill:getExtraTargetNum(player, self, card)
|
||||||
|
if correct == nil then correct = 0 end
|
||||||
|
ret = ret + correct
|
||||||
|
end
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
|
||||||
|
function ActiveSkill:getMinCardNum()
|
||||||
|
local ret
|
||||||
|
if self.card_num then ret = self.card_num
|
||||||
|
elseif self.card_num_table then ret = self.card_num_table
|
||||||
|
else ret = self.min_card_num end
|
||||||
|
|
||||||
|
if type(ret) == "function" then
|
||||||
|
ret = ret(self)
|
||||||
|
end
|
||||||
|
if type(ret) == "table" then
|
||||||
|
return ret[1]
|
||||||
|
else
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ActiveSkill:getMaxCardNum()
|
||||||
|
local ret
|
||||||
|
if self.card_num then ret = self.card_num
|
||||||
|
elseif self.card_num_table then ret = self.card_num_table
|
||||||
|
else ret = self.max_card_num end
|
||||||
|
|
||||||
|
if type(ret) == "function" then
|
||||||
|
ret = ret(self)
|
||||||
|
end
|
||||||
|
if type(ret) == "table" then
|
||||||
|
return ret[#ret]
|
||||||
|
else
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ActiveSkill:getDistanceLimit(player, card)
|
||||||
|
local ret = self.distance_limit
|
||||||
|
local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or {}
|
||||||
|
for _, skill in ipairs(status_skills) do
|
||||||
|
local correct = skill:getDistanceLimit(player, self, card)
|
||||||
|
if correct == nil then correct = 0 end
|
||||||
|
ret = ret + correct
|
||||||
|
end
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
|
||||||
--- Determine if selected cards and targets are valid for this skill
|
--- Determine if selected cards and targets are valid for this skill
|
||||||
--- If returns true, the OK button should be enabled
|
--- If returns true, the OK button should be enabled
|
||||||
--- only used in skill of players
|
--- only used in skill of players
|
||||||
|
|
||||||
|
-- NOTE: don't reclaim it
|
||||||
---@param selected integer[] @ ids of selected players
|
---@param selected integer[] @ ids of selected players
|
||||||
---@param selected_cards integer[] @ ids of selected cards
|
---@param selected_cards integer[] @ ids of selected cards
|
||||||
function ActiveSkill:feasible(selected, selected_cards)
|
function ActiveSkill:feasible(selected, selected_cards, player, card)
|
||||||
return true
|
return #selected >= self:getMinTargetNum() and #selected <= self:getMaxTargetNum(player, card)
|
||||||
|
and #selected_cards >= self:getMinCardNum() and #selected_cards <= self:getMaxCardNum()
|
||||||
end
|
end
|
||||||
|
|
||||||
------- }
|
------- }
|
||||||
|
|
|
@ -4,9 +4,27 @@ local ProhibitSkill = StatusSkill:subclass("ProhibitSkill")
|
||||||
---@param from Player
|
---@param from Player
|
||||||
---@param to Player
|
---@param to Player
|
||||||
---@param card Card
|
---@param card Card
|
||||||
---@return integer
|
---@return boolean
|
||||||
function ProhibitSkill:isProhibited(from, to, card)
|
function ProhibitSkill:isProhibited(from, to, card)
|
||||||
return 0
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param player Player
|
||||||
|
---@param card Card
|
||||||
|
function ProhibitSkill:prohibitUse(player, card)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param player Player
|
||||||
|
---@param card Card
|
||||||
|
function ProhibitSkill:prohibitResponse(player, card)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param player Player
|
||||||
|
---@param card Card
|
||||||
|
function ProhibitSkill:prohibitDiscard(player, card)
|
||||||
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
return ProhibitSkill
|
return ProhibitSkill
|
||||||
|
|
|
@ -1,33 +1,12 @@
|
||||||
---@class UsableSkill : Skill
|
---@class UsableSkill : Skill
|
||||||
---@field target_num integer|integer[]
|
|
||||||
---@field max_use_time integer[]
|
---@field max_use_time integer[]
|
||||||
---@field distance_limit integer
|
|
||||||
local UsableSkill = Skill:subclass("UsableSkill")
|
local UsableSkill = Skill:subclass("UsableSkill")
|
||||||
|
|
||||||
function UsableSkill:initialize(name, frequency)
|
function UsableSkill:initialize(name, frequency)
|
||||||
frequency = frequency or Skill.NotFrequent
|
frequency = frequency or Skill.NotFrequent
|
||||||
Skill.initialize(self, name, frequency)
|
Skill.initialize(self, name, frequency)
|
||||||
|
|
||||||
self.target_num = 9999
|
|
||||||
self.max_use_time = {9999, 9999, 9999, 9999}
|
self.max_use_time = {9999, 9999, 9999, 9999}
|
||||||
self.distance_limit = 9999
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param player Player
|
|
||||||
function UsableSkill:getMinTargetNum(player, card)
|
|
||||||
local ret = type(self.target_num) == "table" and self.target_num[1] or self.target_num
|
|
||||||
return ret
|
|
||||||
end
|
|
||||||
|
|
||||||
function UsableSkill:getMaxTargetNum(player, card)
|
|
||||||
local ret = type(self.target_num) == "table" and self.target_num[2] or self.target_num
|
|
||||||
local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or {}
|
|
||||||
for _, skill in ipairs(status_skills) do
|
|
||||||
local correct = skill:getExtraTargetNum(player, self, card)
|
|
||||||
if correct == nil then correct = 0 end
|
|
||||||
ret = ret + correct
|
|
||||||
end
|
|
||||||
return ret
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function UsableSkill:getMaxUseTime(player, scope, card)
|
function UsableSkill:getMaxUseTime(player, scope, card)
|
||||||
|
@ -42,15 +21,4 @@ function UsableSkill:getMaxUseTime(player, scope, card)
|
||||||
return ret
|
return ret
|
||||||
end
|
end
|
||||||
|
|
||||||
function UsableSkill:getDistanceLimit(player, card)
|
|
||||||
local ret = self.distance_limit
|
|
||||||
local status_skills = Fk:currentRoom().status_skills[TargetModSkill] or {}
|
|
||||||
for _, skill in ipairs(status_skills) do
|
|
||||||
local correct = skill:getDistanceLimit(player, self, card)
|
|
||||||
if correct == nil then correct = 0 end
|
|
||||||
ret = ret + correct
|
|
||||||
end
|
|
||||||
return ret
|
|
||||||
end
|
|
||||||
|
|
||||||
return UsableSkill
|
return UsableSkill
|
||||||
|
|
|
@ -133,6 +133,23 @@ function table:insertIfNeed(element)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@generic T
|
||||||
|
---@param self T[]
|
||||||
|
---@param n integer
|
||||||
|
---@return T|T[]
|
||||||
|
function table.random(tab, n)
|
||||||
|
n = n or 1
|
||||||
|
if #tab == 0 then return nil end
|
||||||
|
local tmp = {table.unpack(tab)}
|
||||||
|
local ret = {}
|
||||||
|
while n > 0 do
|
||||||
|
local i = math.random(1, #tmp)
|
||||||
|
table.insert(ret, table.remove(tmp, i))
|
||||||
|
n = n - 1
|
||||||
|
end
|
||||||
|
return #ret == 1 and ret[1] or ret
|
||||||
|
end
|
||||||
|
|
||||||
---@param delimiter string
|
---@param delimiter string
|
||||||
---@return string[]
|
---@return string[]
|
||||||
function string:split(delimiter)
|
function string:split(delimiter)
|
||||||
|
@ -236,9 +253,9 @@ function switch(param, case_table)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@class TargetGroup : Object
|
---@class TargetGroup : Object
|
||||||
local TargetGroup = class("TargetGroup")
|
local TargetGroup = {}
|
||||||
|
|
||||||
function TargetGroup.static:getRealTargets(targetGroup)
|
function TargetGroup:getRealTargets(targetGroup)
|
||||||
if not targetGroup then
|
if not targetGroup then
|
||||||
return {}
|
return {}
|
||||||
end
|
end
|
||||||
|
@ -251,7 +268,7 @@ function TargetGroup.static:getRealTargets(targetGroup)
|
||||||
return realTargets
|
return realTargets
|
||||||
end
|
end
|
||||||
|
|
||||||
function TargetGroup.static:includeRealTargets(targetGroup, playerId)
|
function TargetGroup:includeRealTargets(targetGroup, playerId)
|
||||||
if not targetGroup then
|
if not targetGroup then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
@ -265,7 +282,7 @@ function TargetGroup.static:includeRealTargets(targetGroup, playerId)
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
function TargetGroup.static:removeTarget(targetGroup, playerId)
|
function TargetGroup:removeTarget(targetGroup, playerId)
|
||||||
if not targetGroup then
|
if not targetGroup then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
@ -278,7 +295,7 @@ function TargetGroup.static:removeTarget(targetGroup, playerId)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function TargetGroup.static:pushTargets(targetGroup, playerIds)
|
function TargetGroup:pushTargets(targetGroup, playerIds)
|
||||||
if not targetGroup then
|
if not targetGroup then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
@ -291,28 +308,28 @@ function TargetGroup.static:pushTargets(targetGroup, playerIds)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@class AimGroup : Object
|
---@class AimGroup : Object
|
||||||
local AimGroup = class("AimGroup")
|
local AimGroup = {}
|
||||||
|
|
||||||
AimGroup.Undone = 1
|
AimGroup.Undone = 1
|
||||||
AimGroup.Done = 2
|
AimGroup.Done = 2
|
||||||
AimGroup.Cancelled = 3
|
AimGroup.Cancelled = 3
|
||||||
|
|
||||||
function AimGroup.static:initAimGroup(playerIds)
|
function AimGroup:initAimGroup(playerIds)
|
||||||
return { [AimGroup.Undone] = playerIds, [AimGroup.Done] = {}, [AimGroup.Cancelled] = {} }
|
return { [AimGroup.Undone] = playerIds, [AimGroup.Done] = {}, [AimGroup.Cancelled] = {} }
|
||||||
end
|
end
|
||||||
|
|
||||||
function AimGroup.static:getAllTargets(aimGroup)
|
function AimGroup:getAllTargets(aimGroup)
|
||||||
local targets = {}
|
local targets = {}
|
||||||
table.insertTable(targets, aimGroup[AimGroup.Undone])
|
table.insertTable(targets, aimGroup[AimGroup.Undone])
|
||||||
table.insertTable(targets, aimGroup[AimGroup.Done])
|
table.insertTable(targets, aimGroup[AimGroup.Done])
|
||||||
return targets
|
return targets
|
||||||
end
|
end
|
||||||
|
|
||||||
function AimGroup.static:getUndoneOrDoneTargets(aimGroup, done)
|
function AimGroup:getUndoneOrDoneTargets(aimGroup, done)
|
||||||
return done and aimGroup[AimGroup.Done] or aimGroup[AimGroup.Undone]
|
return done and aimGroup[AimGroup.Done] or aimGroup[AimGroup.Undone]
|
||||||
end
|
end
|
||||||
|
|
||||||
function AimGroup.static:setTargetDone(aimGroup, playerId)
|
function AimGroup:setTargetDone(aimGroup, playerId)
|
||||||
local index = table.indexOf(aimGroup[AimGroup.Undone], playerId)
|
local index = table.indexOf(aimGroup[AimGroup.Undone], playerId)
|
||||||
if index ~= -1 then
|
if index ~= -1 then
|
||||||
table.remove(aimGroup[AimGroup.Undone], index)
|
table.remove(aimGroup[AimGroup.Undone], index)
|
||||||
|
@ -320,7 +337,7 @@ function AimGroup.static:setTargetDone(aimGroup, playerId)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function AimGroup.static:addTargets(room, aimEvent, playerIds)
|
function AimGroup:addTargets(room, aimEvent, playerIds)
|
||||||
local playerId = type(playerIds) == "table" and playerIds[1] or playerIds
|
local playerId = type(playerIds) == "table" and playerIds[1] or playerIds
|
||||||
table.insert(aimEvent.tos[AimGroup.Undone], playerId)
|
table.insert(aimEvent.tos[AimGroup.Undone], playerId)
|
||||||
|
|
||||||
|
@ -337,7 +354,7 @@ function AimGroup.static:addTargets(room, aimEvent, playerIds)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function AimGroup.static:cancelTarget(aimEvent, playerId)
|
function AimGroup:cancelTarget(aimEvent, playerId)
|
||||||
local cancelled = false
|
local cancelled = false
|
||||||
for status = AimGroup.Undone, AimGroup.Done do
|
for status = AimGroup.Undone, AimGroup.Done do
|
||||||
local indexList = {}
|
local indexList = {}
|
||||||
|
@ -363,7 +380,7 @@ function AimGroup.static:cancelTarget(aimEvent, playerId)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function AimGroup.static:removeDeadTargets(room, aimEvent)
|
function AimGroup:removeDeadTargets(room, aimEvent)
|
||||||
for index = AimGroup.Undone, AimGroup.Done do
|
for index = AimGroup.Undone, AimGroup.Done do
|
||||||
aimEvent.tos[index] = room:deadPlayerFilter(aimEvent.tos[index])
|
aimEvent.tos[index] = room:deadPlayerFilter(aimEvent.tos[index])
|
||||||
end
|
end
|
||||||
|
@ -378,7 +395,7 @@ function AimGroup.static:removeDeadTargets(room, aimEvent)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function AimGroup.static:getCancelledTargets(aimGroup)
|
function AimGroup:getCancelledTargets(aimGroup)
|
||||||
return aimGroup[AimGroup.Cancelled]
|
return aimGroup[AimGroup.Cancelled]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
146
lua/fk_ex.lua
146
lua/fk_ex.lua
|
@ -18,6 +18,42 @@ TrickCard, DelayedTrickCard = table.unpack(Trick)
|
||||||
local Equip = require "core.card_type.equip"
|
local Equip = require "core.card_type.equip"
|
||||||
_, Weapon, Armor, DefensiveRide, OffensiveRide, Treasure = table.unpack(Equip)
|
_, Weapon, Armor, DefensiveRide, OffensiveRide, Treasure = table.unpack(Equip)
|
||||||
|
|
||||||
|
local function readCommonSpecToSkill(skill, spec)
|
||||||
|
skill.mute = spec.mute
|
||||||
|
skill.anim_type = spec.anim_type
|
||||||
|
|
||||||
|
if spec.attached_equip then
|
||||||
|
assert(type(spec.attached_equip) == "string")
|
||||||
|
skill.attached_equip = spec.attached_equip
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function readUsableSpecToSkill(skill, spec)
|
||||||
|
readCommonSpecToSkill(skill, spec)
|
||||||
|
skill.target_num = spec.target_num or skill.target_num
|
||||||
|
skill.min_target_num = spec.min_target_num or skill.min_target_num
|
||||||
|
skill.max_target_num = spec.max_target_num or skill.max_target_num
|
||||||
|
skill.target_num_table = spec.target_num_table or skill.target_num_table
|
||||||
|
skill.card_num = spec.card_num or skill.card_num
|
||||||
|
skill.min_card_num = spec.min_card_num or skill.min_card_num
|
||||||
|
skill.max_card_num = spec.max_card_num or skill.max_card_num
|
||||||
|
skill.card_num_table = spec.card_num_table or skill.card_num_table
|
||||||
|
skill.max_use_time = {
|
||||||
|
spec.max_phase_use_time or 9999,
|
||||||
|
spec.max_turn_use_time or 9999,
|
||||||
|
spec.max_round_use_time or 9999,
|
||||||
|
spec.max_game_use_time or 9999,
|
||||||
|
}
|
||||||
|
skill.distance_limit = spec.distance_limit or skill.distance_limit
|
||||||
|
end
|
||||||
|
|
||||||
|
local function readStatusSpecToSkill(skill, spec)
|
||||||
|
readCommonSpecToSkill(skill, spec)
|
||||||
|
if spec.global then
|
||||||
|
skill.global = spec.global
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
---@class UsableSkillSpec: UsableSkill
|
---@class UsableSkillSpec: UsableSkill
|
||||||
---@field max_phase_use_time integer
|
---@field max_phase_use_time integer
|
||||||
---@field max_turn_use_time integer
|
---@field max_turn_use_time integer
|
||||||
|
@ -48,16 +84,7 @@ function fk.CreateTriggerSkill(spec)
|
||||||
|
|
||||||
local frequency = spec.frequency or Skill.NotFrequent
|
local frequency = spec.frequency or Skill.NotFrequent
|
||||||
local skill = TriggerSkill:new(spec.name, frequency)
|
local skill = TriggerSkill:new(spec.name, frequency)
|
||||||
skill.mute = spec.mute
|
readUsableSpecToSkill(skill, spec)
|
||||||
skill.anim_type = spec.anim_type
|
|
||||||
skill.target_num = spec.target_num or skill.target_num
|
|
||||||
skill.max_use_time = {
|
|
||||||
spec.max_phase_use_time or 9999,
|
|
||||||
spec.max_turn_use_time or 9999,
|
|
||||||
spec.max_round_use_time or 9999,
|
|
||||||
spec.max_game_use_time or 9999,
|
|
||||||
}
|
|
||||||
skill.distance_limit = spec.distance_limit or skill.distance_limit
|
|
||||||
|
|
||||||
if type(spec.events) == "number" then
|
if type(spec.events) == "number" then
|
||||||
table.insert(skill.events, spec.events)
|
table.insert(skill.events, spec.events)
|
||||||
|
@ -91,9 +118,6 @@ function fk.CreateTriggerSkill(spec)
|
||||||
end
|
end
|
||||||
|
|
||||||
if spec.attached_equip then
|
if spec.attached_equip then
|
||||||
assert(type(spec.attached_equip) == "string")
|
|
||||||
skill.attached_equip = spec.attached_equip
|
|
||||||
|
|
||||||
if not spec.priority then
|
if not spec.priority then
|
||||||
spec.priority = 0.1
|
spec.priority = 0.1
|
||||||
end
|
end
|
||||||
|
@ -128,26 +152,15 @@ end
|
||||||
function fk.CreateActiveSkill(spec)
|
function fk.CreateActiveSkill(spec)
|
||||||
assert(type(spec.name) == "string")
|
assert(type(spec.name) == "string")
|
||||||
local skill = ActiveSkill:new(spec.name)
|
local skill = ActiveSkill:new(spec.name)
|
||||||
skill.mute = spec.mute
|
readUsableSpecToSkill(skill, spec)
|
||||||
skill.anim_type = spec.anim_type
|
|
||||||
skill.target_num = spec.target_num or skill.target_num
|
|
||||||
skill.max_use_time = {
|
|
||||||
spec.max_phase_use_time or 9999,
|
|
||||||
spec.max_turn_use_time or 9999,
|
|
||||||
spec.max_round_use_time or 9999,
|
|
||||||
spec.max_game_use_time or 9999,
|
|
||||||
}
|
|
||||||
skill.distance_limit = spec.distance_limit or skill.distance_limit
|
|
||||||
|
|
||||||
if spec.attached_equip then
|
|
||||||
assert(type(spec.attached_equip) == "string")
|
|
||||||
skill.attached_equip = spec.attached_equip
|
|
||||||
end
|
|
||||||
|
|
||||||
if spec.can_use then skill.canUse = spec.can_use end
|
if spec.can_use then skill.canUse = spec.can_use end
|
||||||
if spec.card_filter then skill.cardFilter = spec.card_filter end
|
if spec.card_filter then skill.cardFilter = spec.card_filter end
|
||||||
if spec.target_filter then skill.targetFilter = spec.target_filter end
|
if spec.target_filter then skill.targetFilter = spec.target_filter end
|
||||||
if spec.feasible then skill.feasible = spec.feasible end
|
if spec.feasible then
|
||||||
|
print(spec.name .. ": feasible is deprecated. Use target_num and card_num instead.")
|
||||||
|
skill.feasible = spec.feasible
|
||||||
|
end
|
||||||
if spec.on_use then skill.onUse = spec.on_use end
|
if spec.on_use then skill.onUse = spec.on_use end
|
||||||
if spec.about_to_effect then skill.aboutToEffect = spec.about_to_effect end
|
if spec.about_to_effect then skill.aboutToEffect = spec.about_to_effect end
|
||||||
if spec.on_effect then skill.onEffect = spec.on_effect end
|
if spec.on_effect then skill.onEffect = spec.on_effect end
|
||||||
|
@ -169,21 +182,7 @@ function fk.CreateViewAsSkill(spec)
|
||||||
assert(type(spec.view_as) == "function")
|
assert(type(spec.view_as) == "function")
|
||||||
|
|
||||||
local skill = ViewAsSkill:new(spec.name)
|
local skill = ViewAsSkill:new(spec.name)
|
||||||
skill.mute = spec.mute
|
readUsableSpecToSkill(skill, spec)
|
||||||
skill.anim_type = spec.anim_type
|
|
||||||
skill.target_num = spec.target_num or skill.target_num
|
|
||||||
skill.max_use_time = {
|
|
||||||
spec.max_phase_use_time or 9999,
|
|
||||||
spec.max_turn_use_time or 9999,
|
|
||||||
spec.max_round_use_time or 9999,
|
|
||||||
spec.max_game_use_time or 9999,
|
|
||||||
}
|
|
||||||
skill.distance_limit = spec.distance_limit or skill.distance_limit
|
|
||||||
|
|
||||||
if spec.attached_equip then
|
|
||||||
assert(type(spec.attached_equip) == "string")
|
|
||||||
skill.attached_equip = spec.attached_equip
|
|
||||||
end
|
|
||||||
|
|
||||||
skill.viewAs = spec.view_as
|
skill.viewAs = spec.view_as
|
||||||
if spec.card_filter then
|
if spec.card_filter then
|
||||||
|
@ -212,21 +211,17 @@ function fk.CreateDistanceSkill(spec)
|
||||||
assert(type(spec.correct_func) == "function")
|
assert(type(spec.correct_func) == "function")
|
||||||
|
|
||||||
local skill = DistanceSkill:new(spec.name)
|
local skill = DistanceSkill:new(spec.name)
|
||||||
|
readStatusSpecToSkill(skill, spec)
|
||||||
skill.getCorrect = spec.correct_func
|
skill.getCorrect = spec.correct_func
|
||||||
if spec.global then
|
|
||||||
skill.global = spec.global
|
|
||||||
end
|
|
||||||
|
|
||||||
if spec.attached_equip then
|
|
||||||
assert(type(spec.attached_equip) == "string")
|
|
||||||
skill.attached_equip = spec.attached_equip
|
|
||||||
end
|
|
||||||
|
|
||||||
return skill
|
return skill
|
||||||
end
|
end
|
||||||
|
|
||||||
---@class ProhibitSpec: StatusSkillSpec
|
---@class ProhibitSpec: StatusSkillSpec
|
||||||
---@field is_prohibited fun(self: ProhibitSkill, from: Player, to: Player, card: Card)
|
---@field is_prohibited fun(self: ProhibitSkill, from: Player, to: Player, card: Card)
|
||||||
|
---@field prohibit_use fun(self: ProhibitSkill, player: Player, card: Card)
|
||||||
|
---@field prohibit_response fun(self: ProhibitSkill, player: Player, card: Card)
|
||||||
|
---@field prohibit_discard fun(self: ProhibitSkill, player: Player, card: Card)
|
||||||
|
|
||||||
---@param spec ProhibitSpec
|
---@param spec ProhibitSpec
|
||||||
---@return ProhibitSkill
|
---@return ProhibitSkill
|
||||||
|
@ -235,15 +230,11 @@ function fk.CreateProhibitSkill(spec)
|
||||||
assert(type(spec.is_prohibited) == "function")
|
assert(type(spec.is_prohibited) == "function")
|
||||||
|
|
||||||
local skill = ProhibitSkill:new(spec.name)
|
local skill = ProhibitSkill:new(spec.name)
|
||||||
skill.isProhibited = spec.is_prohibited
|
readStatusSpecToSkill(skill, spec)
|
||||||
if spec.global then
|
skill.isProhibited = spec.is_prohibited or skill.isProhibited
|
||||||
skill.global = spec.global
|
skill.prohibitUse = spec.prohibit_use or skill.prohibitUse
|
||||||
end
|
skill.prohibitResponse = spec.prohibit_response or skill.prohibitResponse
|
||||||
|
skill.prohibitDiscard = spec.prohibit_discard or skill.prohibitDiscard
|
||||||
if spec.attached_equip then
|
|
||||||
assert(type(spec.attached_equip) == "string")
|
|
||||||
skill.attached_equip = spec.attached_equip
|
|
||||||
end
|
|
||||||
|
|
||||||
return skill
|
return skill
|
||||||
end
|
end
|
||||||
|
@ -258,15 +249,8 @@ function fk.CreateAttackRangeSkill(spec)
|
||||||
assert(type(spec.correct_func) == "function")
|
assert(type(spec.correct_func) == "function")
|
||||||
|
|
||||||
local skill = AttackRangeSkill:new(spec.name)
|
local skill = AttackRangeSkill:new(spec.name)
|
||||||
|
readStatusSpecToSkill(skill, spec)
|
||||||
skill.getCorrect = spec.correct_func
|
skill.getCorrect = spec.correct_func
|
||||||
if spec.global then
|
|
||||||
skill.global = spec.global
|
|
||||||
end
|
|
||||||
|
|
||||||
if spec.attached_equip then
|
|
||||||
assert(type(spec.attached_equip) == "string")
|
|
||||||
skill.attached_equip = spec.attached_equip
|
|
||||||
end
|
|
||||||
|
|
||||||
return skill
|
return skill
|
||||||
end
|
end
|
||||||
|
@ -282,20 +266,13 @@ function fk.CreateMaxCardsSkill(spec)
|
||||||
assert(type(spec.correct_func) == "function" or type(spec.fixed_func) == "function")
|
assert(type(spec.correct_func) == "function" or type(spec.fixed_func) == "function")
|
||||||
|
|
||||||
local skill = MaxCardsSkill:new(spec.name)
|
local skill = MaxCardsSkill:new(spec.name)
|
||||||
|
readStatusSpecToSkill(skill, spec)
|
||||||
if spec.correct_func then
|
if spec.correct_func then
|
||||||
skill.getCorrect = spec.correct_func
|
skill.getCorrect = spec.correct_func
|
||||||
end
|
end
|
||||||
if spec.fixed_func then
|
if spec.fixed_func then
|
||||||
skill.getFixed = spec.fixed_func
|
skill.getFixed = spec.fixed_func
|
||||||
end
|
end
|
||||||
if spec.global then
|
|
||||||
skill.global = spec.global
|
|
||||||
end
|
|
||||||
|
|
||||||
if spec.attached_equip then
|
|
||||||
assert(type(spec.attached_equip) == "string")
|
|
||||||
skill.attached_equip = spec.attached_equip
|
|
||||||
end
|
|
||||||
|
|
||||||
return skill
|
return skill
|
||||||
end
|
end
|
||||||
|
@ -311,6 +288,7 @@ function fk.CreateTargetModSkill(spec)
|
||||||
assert(type(spec.name) == "string")
|
assert(type(spec.name) == "string")
|
||||||
|
|
||||||
local skill = TargetModSkill:new(spec.name)
|
local skill = TargetModSkill:new(spec.name)
|
||||||
|
readStatusSpecToSkill(skill, spec)
|
||||||
if spec.residue_func then
|
if spec.residue_func then
|
||||||
skill.getResidueNum = spec.residue_func
|
skill.getResidueNum = spec.residue_func
|
||||||
end
|
end
|
||||||
|
@ -320,14 +298,6 @@ function fk.CreateTargetModSkill(spec)
|
||||||
if spec.extra_target_func then
|
if spec.extra_target_func then
|
||||||
skill.getExtraTargetNum = spec.extra_target_func
|
skill.getExtraTargetNum = spec.extra_target_func
|
||||||
end
|
end
|
||||||
if spec.global then
|
|
||||||
skill.global = spec.global
|
|
||||||
end
|
|
||||||
|
|
||||||
if spec.attached_equip then
|
|
||||||
assert(type(spec.attached_equip) == "string")
|
|
||||||
skill.attached_equip = spec.attached_equip
|
|
||||||
end
|
|
||||||
|
|
||||||
return skill
|
return skill
|
||||||
end
|
end
|
||||||
|
@ -342,13 +312,9 @@ function fk.CreateFilterSkill(spec)
|
||||||
assert(type(spec.name) == "string")
|
assert(type(spec.name) == "string")
|
||||||
|
|
||||||
local skill = FilterSkill:new(spec.name)
|
local skill = FilterSkill:new(spec.name)
|
||||||
skill.mute = spec.mute
|
readStatusSpecToSkill(skill, spec)
|
||||||
skill.anim_type = spec.anim_type
|
|
||||||
skill.cardFilter = spec.card_filter
|
skill.cardFilter = spec.card_filter
|
||||||
skill.viewAs = spec.view_as
|
skill.viewAs = spec.view_as
|
||||||
if spec.global then
|
|
||||||
skill.global = spec.global
|
|
||||||
end
|
|
||||||
|
|
||||||
return skill
|
return skill
|
||||||
end
|
end
|
||||||
|
|
|
@ -19,7 +19,13 @@ function fk:GetMicroSecond()end
|
||||||
function fk:SPlayerList()end
|
function fk:SPlayerList()end
|
||||||
|
|
||||||
function fk.QmlBackend_pwd()end
|
function fk.QmlBackend_pwd()end
|
||||||
|
|
||||||
|
---@return string[]
|
||||||
function fk.QmlBackend_ls(filename)end
|
function fk.QmlBackend_ls(filename)end
|
||||||
function fk.QmlBackend_cd(dir)end
|
function fk.QmlBackend_cd(dir)end
|
||||||
|
|
||||||
|
---@return boolean
|
||||||
function fk.QmlBackend_exists(file)end
|
function fk.QmlBackend_exists(file)end
|
||||||
|
|
||||||
|
---@return boolean
|
||||||
function fk.QmlBackend_isDir(file)end
|
function fk.QmlBackend_isDir(file)end
|
|
@ -5,13 +5,13 @@
|
||||||
--- middleclass
|
--- middleclass
|
||||||
class = {}
|
class = {}
|
||||||
|
|
||||||
---@param class class
|
---@param class class|Object
|
||||||
---@return boolean
|
---@return boolean
|
||||||
function class:isSubclassOf(class) end
|
function class:isSubclassOf(class) end
|
||||||
|
|
||||||
---@class Object
|
---@class Object
|
||||||
---@field class class
|
---@field class class
|
||||||
Object = {}
|
Object = { static = {} }
|
||||||
|
|
||||||
---@generic T
|
---@generic T
|
||||||
---@param self T
|
---@param self T
|
||||||
|
@ -25,7 +25,7 @@ function Object:new(...)end
|
||||||
---@param name string
|
---@param name string
|
||||||
function Object:subclass(name)end
|
function Object:subclass(name)end
|
||||||
|
|
||||||
---@param class class
|
---@param class class|Object
|
||||||
---@return boolean
|
---@return boolean
|
||||||
function Object:isInstanceOf(class) end
|
function Object:isInstanceOf(class) end
|
||||||
|
|
||||||
|
@ -42,5 +42,5 @@ function json.encode(obj)end
|
||||||
|
|
||||||
--- convert JSON string to lua types
|
--- convert JSON string to lua types
|
||||||
---@param str string @ JSON string to decode
|
---@param str string @ JSON string to decode
|
||||||
---@return table|number|string
|
---@return any
|
||||||
function json.decode(str)end
|
function json.decode(str)end
|
9
lua/lsp/static.lua
Normal file
9
lua/lsp/static.lua
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
---@meta
|
||||||
|
|
||||||
|
---@param c integer|integer[]|Card|Card[]
|
||||||
|
---@return integer[]
|
||||||
|
function Card:getIdList(c) end
|
||||||
|
|
||||||
|
---@param pattern string
|
||||||
|
---@return Exppattern
|
||||||
|
function Exppattern:Parse(pattern) end
|
43
lua/server/ai/ai.lua
Normal file
43
lua/server/ai/ai.lua
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
-- AI base class.
|
||||||
|
-- Do nothing.
|
||||||
|
|
||||||
|
---@class AI: Object
|
||||||
|
---@field room Room
|
||||||
|
---@field player ServerPlayer
|
||||||
|
---@field command string
|
||||||
|
---@field jsonData string
|
||||||
|
---@field cb_table table<string, fun(jsonData: string)>
|
||||||
|
local AI = class("AI")
|
||||||
|
|
||||||
|
function AI:initialize(player)
|
||||||
|
self.room = RoomInstance
|
||||||
|
self.player = player
|
||||||
|
local cb_t = {}
|
||||||
|
-- default strategy: print command and data, then return ""
|
||||||
|
setmetatable(cb_t, {
|
||||||
|
__index = function()
|
||||||
|
return function()
|
||||||
|
print(self.command, self.jsonData)
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
self.cb_table = cb_t
|
||||||
|
end
|
||||||
|
|
||||||
|
function AI:readRequestData()
|
||||||
|
self.command = self.player.ai_data.command
|
||||||
|
self.jsonData = self.player.ai_data.jsonData
|
||||||
|
end
|
||||||
|
|
||||||
|
function AI:makeReply()
|
||||||
|
Self = self.player
|
||||||
|
local start = os.getms()
|
||||||
|
local ret = self.cb_table[self.command] and self.cb_table[self.command](self, self.jsonData) or "__cancel"
|
||||||
|
local to_delay = 500 - (os.getms() - start) / 1000
|
||||||
|
print(to_delay)
|
||||||
|
self.room:delay(to_delay)
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
|
||||||
|
return AI
|
3
lua/server/ai/init.lua
Normal file
3
lua/server/ai/init.lua
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
AI = require "server.ai.ai"
|
||||||
|
TrustAI = require "server.ai.trust_ai"
|
||||||
|
RandomAI = require "server.ai.random_ai"
|
5
lua/server/ai/mcts.lua
Normal file
5
lua/server/ai/mcts.lua
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
local MonteCarlo = class("MonteCarlo")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return MonteCarlo
|
204
lua/server/ai/random_ai.lua
Normal file
204
lua/server/ai/random_ai.lua
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
---@class RandomAI: AI
|
||||||
|
local RandomAI = AI:subclass("RandomAI")
|
||||||
|
|
||||||
|
---@param self RandomAI
|
||||||
|
---@param skill ActiveSkill
|
||||||
|
---@param card Card | nil
|
||||||
|
local function useActiveSkill(self, skill, card)
|
||||||
|
local room = self.room
|
||||||
|
local player = self.player
|
||||||
|
|
||||||
|
local filter_func = skill.cardFilter
|
||||||
|
if card then
|
||||||
|
filter_func = function() return false end
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.command == "PlayCard" and not skill:canUse(player) then
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
|
||||||
|
local max_try_times = 100
|
||||||
|
local selected_targets = {}
|
||||||
|
local selected_cards = {}
|
||||||
|
local min = skill:getMinTargetNum()
|
||||||
|
local max = skill:getMaxTargetNum(player, card)
|
||||||
|
local min_card = skill:getMinCardNum()
|
||||||
|
local max_card = skill:getMaxCardNum()
|
||||||
|
while not skill:feasible(selected_targets, selected_cards, self.player, card) do
|
||||||
|
if max_try_times <= 0 then break end
|
||||||
|
local avail_targets = table.filter(self.room:getAlivePlayers(), function(p)
|
||||||
|
local ret = skill:targetFilter(p.id, selected_targets, selected_cards)
|
||||||
|
if ret and card then
|
||||||
|
local r = self.room
|
||||||
|
local status_skills = r.status_skills[ProhibitSkill] or {}
|
||||||
|
for _, skill in ipairs(status_skills) do
|
||||||
|
if skill:isProhibited(self.player, p, card) then
|
||||||
|
ret = false
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return ret
|
||||||
|
end)
|
||||||
|
avail_targets = table.map(avail_targets, function(p) return p.id end)
|
||||||
|
local avail_cards = table.filter(player:getCardIds{ Player.Hand, Player.Equip }, function(id)
|
||||||
|
return filter_func(skill, id, selected_cards, selected_targets)
|
||||||
|
end)
|
||||||
|
|
||||||
|
if #avail_targets == 0 and #avail_cards == 0 then break end
|
||||||
|
table.insertIfNeed(selected_targets, table.random(avail_targets))
|
||||||
|
table.insertIfNeed(selected_cards, table.random(avail_cards))
|
||||||
|
max_try_times = max_try_times - 1
|
||||||
|
end
|
||||||
|
if skill:feasible(selected_targets, selected_cards, self.player, card) then
|
||||||
|
local ret = json.encode{
|
||||||
|
card = card and card.id or json.encode{
|
||||||
|
skill = skill.name,
|
||||||
|
subcards = selected_cards,
|
||||||
|
},
|
||||||
|
targets = selected_targets,
|
||||||
|
}
|
||||||
|
print(ret)
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param self RandomAI
|
||||||
|
---@param skill ViewAsSkill
|
||||||
|
local function useVSSkill(self, skill, pattern, cancelable, extra_data)
|
||||||
|
local player = self.player
|
||||||
|
local room = self.room
|
||||||
|
local precondition
|
||||||
|
|
||||||
|
if self.command == "PlayCard" then
|
||||||
|
precondition = skill:enabledAtPlay(player)
|
||||||
|
if not precondition then return nil end
|
||||||
|
local exp = Exppattern:Parse(skill.pattern)
|
||||||
|
local cnames = {}
|
||||||
|
for _, m in ipairs(exp.matchers) do
|
||||||
|
if m.name then table.insertTable(cnames, m.name) end
|
||||||
|
end
|
||||||
|
for _, n in ipairs(cnames) do
|
||||||
|
local c = Fk:cloneCard(n)
|
||||||
|
precondition = c.skill:canUse(Self, c)
|
||||||
|
if precondition then break end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
precondition = skill:enabledAtResponse(player)
|
||||||
|
if not precondition then return nil end
|
||||||
|
local exp = Exppattern:Parse(pattern)
|
||||||
|
precondition = exp:matchExp(skill.pattern)
|
||||||
|
end
|
||||||
|
|
||||||
|
if (not precondition) or math.random() < 0.2 then return nil end
|
||||||
|
|
||||||
|
local selected_cards = {}
|
||||||
|
local max_try_time = 100
|
||||||
|
|
||||||
|
while true do
|
||||||
|
if max_try_time <= 0 then break end
|
||||||
|
local avail_cards = table.filter(player:getCardIds{ Player.Hand, Player.Equip }, function(id)
|
||||||
|
return skill:cardFilter(id, selected_cards)
|
||||||
|
end)
|
||||||
|
if #avail_cards == 0 then break end
|
||||||
|
table.insert(selected_cards, table.random(avail_cards))
|
||||||
|
if skill:viewAs(selected_cards) then
|
||||||
|
return {
|
||||||
|
skill = skill.name,
|
||||||
|
subcards = selected_cards,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
max_try_time = max_try_time - 1
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local random_cb = {}
|
||||||
|
|
||||||
|
random_cb.AskForUseActiveSkill = function(self, jsonData)
|
||||||
|
local data = json.decode(jsonData)
|
||||||
|
local skill = Fk.skills[data[1]]
|
||||||
|
local cancelable = data[3]
|
||||||
|
if cancelable and math.random() < 0.25 then return "" end
|
||||||
|
local extra_data = json.decode(data[4])
|
||||||
|
for k, v in pairs(extra_data) do
|
||||||
|
skill[k] = v
|
||||||
|
end
|
||||||
|
return useActiveSkill(self, skill)
|
||||||
|
end
|
||||||
|
|
||||||
|
random_cb.AskForSkillInvoke = function(self, jsonData)
|
||||||
|
return table.random{"1", ""}
|
||||||
|
end
|
||||||
|
|
||||||
|
random_cb.AskForUseCard = function(self, jsonData) end
|
||||||
|
|
||||||
|
---@param self RandomAI
|
||||||
|
random_cb.AskForResponseCard = function(self, jsonData)
|
||||||
|
local data = json.decode(jsonData)
|
||||||
|
local pattern = data[2]
|
||||||
|
local cancelable = true
|
||||||
|
local exp = Exppattern:Parse(pattern)
|
||||||
|
local avail_cards = table.filter(self.player:getCardIds{ Player.Hand, Player.Equip }, function(id)
|
||||||
|
return exp:match(Fk:getCardById(id))
|
||||||
|
end)
|
||||||
|
if #avail_cards > 0 then return json.encode{
|
||||||
|
card = table.random(avail_cards),
|
||||||
|
targets = {},
|
||||||
|
} end
|
||||||
|
-- TODO: vs skill
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param self RandomAI
|
||||||
|
random_cb.PlayCard = function(self, jsonData)
|
||||||
|
local cards = table.map(self.player:getCardIds(Player.Hand),
|
||||||
|
function(id) return Fk:getCardById(id) end)
|
||||||
|
local actives = table.filter(self.player:getAllSkills(), function(s)
|
||||||
|
return s:isInstanceOf(ActiveSkill)
|
||||||
|
end)
|
||||||
|
local vss = table.filter(self.player:getAllSkills(), function(s)
|
||||||
|
return s:isInstanceOf(ViewAsSkill)
|
||||||
|
end)
|
||||||
|
table.insertTable(cards, actives)
|
||||||
|
table.insertTable(cards, vss)
|
||||||
|
|
||||||
|
while #cards > 0 do
|
||||||
|
local sth = table.random(cards)
|
||||||
|
if sth:isInstanceOf(Card) then
|
||||||
|
local card = sth
|
||||||
|
local skill = card.skill ---@type ActiveSkill
|
||||||
|
if math.random() > 0.15 then
|
||||||
|
local ret = useActiveSkill(self, skill, card)
|
||||||
|
if ret ~= "" then return ret end
|
||||||
|
table.removeOne(cards, card)
|
||||||
|
else
|
||||||
|
table.removeOne(cards, card)
|
||||||
|
end
|
||||||
|
elseif sth:isInstanceOf(ActiveSkill) then
|
||||||
|
local active = sth
|
||||||
|
if math.random() > 0.30 then
|
||||||
|
local ret = useActiveSkill(self, active, nil)
|
||||||
|
if ret ~= "" then return ret end
|
||||||
|
end
|
||||||
|
table.removeOne(cards, active)
|
||||||
|
else
|
||||||
|
local vs = sth
|
||||||
|
if math.random() > 0.20 then
|
||||||
|
local ret = useVSSkill(self, vs)
|
||||||
|
-- TODO: handle vs result
|
||||||
|
end
|
||||||
|
table.removeOne(cards, vs)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
|
||||||
|
function RandomAI:initialize(player)
|
||||||
|
AI.initialize(self, player)
|
||||||
|
self.cb_table = random_cb
|
||||||
|
end
|
||||||
|
|
||||||
|
return RandomAI
|
13
lua/server/ai/trust_ai.lua
Normal file
13
lua/server/ai/trust_ai.lua
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
-- Trust AI
|
||||||
|
|
||||||
|
---@class TrustAI: AI
|
||||||
|
local TrustAI = AI:subclass("TrustAI")
|
||||||
|
|
||||||
|
local trust_cb = {}
|
||||||
|
|
||||||
|
function TrustAI:initialize(player)
|
||||||
|
AI.initialize(self, player)
|
||||||
|
self.cb_table = trust_cb
|
||||||
|
end
|
||||||
|
|
||||||
|
return TrustAI
|
|
@ -164,22 +164,10 @@ function GameLogic:action()
|
||||||
self:trigger(fk.DrawInitialCards, p, { num = 4 })
|
self:trigger(fk.DrawInitialCards, p, { num = 4 })
|
||||||
end
|
end
|
||||||
|
|
||||||
local function checkNoHuman()
|
|
||||||
for _, p in ipairs(room.players) do
|
|
||||||
if p.serverplayer:getStateString() == "online" then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
while true do
|
while true do
|
||||||
self:trigger(fk.TurnStart, room.current)
|
self:trigger(fk.TurnStart, room.current)
|
||||||
if room.game_finished then break end
|
if room.game_finished then break end
|
||||||
room.current = room.current:getNextAlive()
|
room.current = room.current:getNextAlive()
|
||||||
if checkNoHuman() then
|
|
||||||
room:gameOver("")
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,10 @@ local Room = class("Room")
|
||||||
GameLogic = require "server.gamelogic"
|
GameLogic = require "server.gamelogic"
|
||||||
ServerPlayer = require "server.serverplayer"
|
ServerPlayer = require "server.serverplayer"
|
||||||
|
|
||||||
|
---@type Player
|
||||||
|
Self = nil -- `Self' is client-only, but we need it in AI
|
||||||
|
dofile "lua/server/ai/init.lua"
|
||||||
|
|
||||||
--[[--------------------------------------------------------------------
|
--[[--------------------------------------------------------------------
|
||||||
Room stores all information for server side game room, such as player,
|
Room stores all information for server side game room, such as player,
|
||||||
cards, and other properties.
|
cards, and other properties.
|
||||||
|
@ -55,11 +59,12 @@ function Room:initialize(_room)
|
||||||
local main_co = coroutine.create(function()
|
local main_co = coroutine.create(function()
|
||||||
self:run()
|
self:run()
|
||||||
end)
|
end)
|
||||||
local request_co = coroutine.create(function()
|
local request_co = coroutine.create(function(rest)
|
||||||
self:requestLoop()
|
self:requestLoop(rest)
|
||||||
end)
|
end)
|
||||||
|
local ret, err_msg = true, true
|
||||||
while not self.game_finished do
|
while not self.game_finished do
|
||||||
local ret, err_msg = coroutine.resume(main_co)
|
ret, err_msg = coroutine.resume(main_co, err_msg)
|
||||||
|
|
||||||
-- handle error
|
-- handle error
|
||||||
if ret == false then
|
if ret == false then
|
||||||
|
@ -68,12 +73,16 @@ function Room:initialize(_room)
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
|
|
||||||
ret, err_msg = coroutine.resume(request_co)
|
-- If ret == true, then err_msg is the millisecond left
|
||||||
|
|
||||||
|
ret, err_msg = coroutine.resume(request_co, err_msg)
|
||||||
if ret == false then
|
if ret == false then
|
||||||
fk.qCritical(err_msg)
|
fk.qCritical(err_msg)
|
||||||
print(debug.traceback(request_co))
|
print(debug.traceback(request_co))
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- If ret == true, then when err_msg is true, that means no request
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -405,7 +414,7 @@ function Room:doRaceRequest(command, players, jsonData)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- main loop for the request handling coroutine
|
-- main loop for the request handling coroutine
|
||||||
function Room:requestLoop()
|
function Room:requestLoop(rest_time)
|
||||||
local function tellRoomToObserver(player)
|
local function tellRoomToObserver(player)
|
||||||
local observee = self.players[1]
|
local observee = self.players[1]
|
||||||
player:doNotify("Setup", json.encode{
|
player:doNotify("Setup", json.encode{
|
||||||
|
@ -472,8 +481,10 @@ function Room:requestLoop()
|
||||||
end
|
end
|
||||||
|
|
||||||
while true do
|
while true do
|
||||||
|
local ret = false
|
||||||
local request = self.room:fetchRequest()
|
local request = self.room:fetchRequest()
|
||||||
if request ~= "" then
|
if request ~= "" then
|
||||||
|
ret = true
|
||||||
local id, command = table.unpack(request:split(","))
|
local id, command = table.unpack(request:split(","))
|
||||||
id = tonumber(id)
|
id = tonumber(id)
|
||||||
if command == "reconnect" then
|
if command == "reconnect" then
|
||||||
|
@ -483,8 +494,12 @@ function Room:requestLoop()
|
||||||
elseif command == "leave" then
|
elseif command == "leave" then
|
||||||
removeObserver(id)
|
removeObserver(id)
|
||||||
end
|
end
|
||||||
|
elseif rest_time > 10 then
|
||||||
|
-- let current thread sleep 10ms
|
||||||
|
-- otherwise CPU usage will be 100% (infinite yield <-> resume loop)
|
||||||
|
fk.QThread_msleep(10)
|
||||||
end
|
end
|
||||||
coroutine.yield()
|
coroutine.yield(ret)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -493,10 +508,11 @@ end
|
||||||
function Room:delay(ms)
|
function Room:delay(ms)
|
||||||
local start = os.getms()
|
local start = os.getms()
|
||||||
while true do
|
while true do
|
||||||
if os.getms() - start >= ms * 1000 then
|
local rest = ms - (os.getms() - start) / 1000
|
||||||
|
if rest <= 0 then
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
coroutine.yield()
|
coroutine.yield(rest)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -676,7 +692,7 @@ function Room:askForUseActiveSkill(player, skill_name, prompt, cancelable, extra
|
||||||
end
|
end
|
||||||
|
|
||||||
local command = "AskForUseActiveSkill"
|
local command = "AskForUseActiveSkill"
|
||||||
self:notifyMoveFocus(player, skill_name) -- for display skill name instead of command name
|
self:notifyMoveFocus(player, extra_data.skillName or skill_name) -- for display skill name instead of command name
|
||||||
local data = {skill_name, prompt, cancelable, json.encode(extra_data)}
|
local data = {skill_name, prompt, cancelable, json.encode(extra_data)}
|
||||||
local result = self:doRequest(player, command, json.encode(data))
|
local result = self:doRequest(player, command, json.encode(data))
|
||||||
|
|
||||||
|
@ -756,7 +772,8 @@ function Room:askForChoosePlayers(player, targets, minNum, maxNum, prompt, skill
|
||||||
targets = targets,
|
targets = targets,
|
||||||
num = maxNum,
|
num = maxNum,
|
||||||
min_num = minNum,
|
min_num = minNum,
|
||||||
reason = skillName
|
pattern = "",
|
||||||
|
skillName = skillName
|
||||||
}
|
}
|
||||||
local _, ret = self:askForUseActiveSkill(player, "choose_players_skill", prompt or "", true, data)
|
local _, ret = self:askForUseActiveSkill(player, "choose_players_skill", prompt or "", true, data)
|
||||||
if ret then
|
if ret then
|
||||||
|
@ -767,6 +784,34 @@ function Room:askForChoosePlayers(player, targets, minNum, maxNum, prompt, skill
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@param player ServerPlayer
|
||||||
|
---@param targets integer[]
|
||||||
|
---@param minNum integer
|
||||||
|
---@param maxNum integer
|
||||||
|
---@param pattern string
|
||||||
|
---@param prompt string
|
||||||
|
---@return integer[], integer
|
||||||
|
function Room:askForChooseCardAndPlayers(player, targets, minNum, maxNum, pattern, prompt, skillName)
|
||||||
|
if maxNum < 1 then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
|
local data = {
|
||||||
|
targets = targets,
|
||||||
|
num = maxNum,
|
||||||
|
min_num = minNum,
|
||||||
|
pattern = pattern or ".",
|
||||||
|
skillName = skillName
|
||||||
|
}
|
||||||
|
local _, ret = self:askForUseActiveSkill(player, "choose_players_skill", prompt or "", true, data)
|
||||||
|
if ret then
|
||||||
|
return ret.targets, ret.cards[1]
|
||||||
|
else
|
||||||
|
-- TODO: default
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
---@param player ServerPlayer
|
---@param player ServerPlayer
|
||||||
---@param generals string[]
|
---@param generals string[]
|
||||||
---@return string
|
---@return string
|
||||||
|
@ -802,14 +847,18 @@ function Room:askForCardChosen(chooser, target, flag, reason)
|
||||||
local result = self:doRequest(chooser, command, json.encode(data))
|
local result = self:doRequest(chooser, command, json.encode(data))
|
||||||
|
|
||||||
if result == "" then
|
if result == "" then
|
||||||
-- FIXME: generate a random card according to flag
|
|
||||||
result = -1
|
result = -1
|
||||||
else
|
else
|
||||||
result = tonumber(result)
|
result = tonumber(result)
|
||||||
end
|
end
|
||||||
|
|
||||||
if result == -1 then
|
if result == -1 then
|
||||||
local handcards = target.player_cards[Player.Hand]
|
local areas = {}
|
||||||
|
if string.find(flag, "h") then table.insert(areas, Player.Hand) end
|
||||||
|
if string.find(flag, "e") then table.insert(areas, Player.Equip) end
|
||||||
|
if string.find(flag, "j") then table.insert(areas, Player.Judge) end
|
||||||
|
local handcards = target:getCardIds(areas)
|
||||||
|
if #handcards == 0 then return end
|
||||||
result = handcards[math.random(1, #handcards)]
|
result = handcards[math.random(1, #handcards)]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,8 @@
|
||||||
---@field phase_state table[]
|
---@field phase_state table[]
|
||||||
---@field phase_index integer
|
---@field phase_index integer
|
||||||
---@field role_shown boolean
|
---@field role_shown boolean
|
||||||
|
---@field ai AI
|
||||||
|
---@field ai_data any
|
||||||
local ServerPlayer = Player:subclass("ServerPlayer")
|
local ServerPlayer = Player:subclass("ServerPlayer")
|
||||||
|
|
||||||
function ServerPlayer:initialize(_self)
|
function ServerPlayer:initialize(_self)
|
||||||
|
@ -29,6 +31,7 @@ function ServerPlayer:initialize(_self)
|
||||||
self.reply_cancel = false
|
self.reply_cancel = false
|
||||||
self.phases = {}
|
self.phases = {}
|
||||||
self.skipped_phases = {}
|
self.skipped_phases = {}
|
||||||
|
self.ai = RandomAI:new(self)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param command string
|
---@param command string
|
||||||
|
@ -55,21 +58,51 @@ function ServerPlayer:doRequest(command, jsonData, timeout)
|
||||||
self.client_reply = ""
|
self.client_reply = ""
|
||||||
self.reply_ready = false
|
self.reply_ready = false
|
||||||
self.reply_cancel = false
|
self.reply_cancel = false
|
||||||
|
self.ai_data = {
|
||||||
|
command = command,
|
||||||
|
jsonData = jsonData,
|
||||||
|
}
|
||||||
self.serverplayer:doRequest(command, jsonData, timeout)
|
self.serverplayer:doRequest(command, jsonData, timeout)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function checkNoHuman(room)
|
||||||
|
for _, p in ipairs(room.players) do
|
||||||
|
-- TODO: trust
|
||||||
|
if p.serverplayer:getStateString() == "online" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
room:gameOver("")
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
local function _waitForReply(player, timeout)
|
local function _waitForReply(player, timeout)
|
||||||
local result
|
local result
|
||||||
local start = os.getms()
|
local start = os.getms()
|
||||||
|
local state = player.serverplayer:getStateString()
|
||||||
|
if state ~= "online" then
|
||||||
|
-- Let AI make reply. First handle request
|
||||||
|
local ret_msg = true
|
||||||
|
while ret_msg do
|
||||||
|
-- when ret_msg is false, that means there is no request in the queue
|
||||||
|
ret_msg = coroutine.yield(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
checkNoHuman(player.room)
|
||||||
|
player.ai:readRequestData()
|
||||||
|
local reply = player.ai:makeReply()
|
||||||
|
return reply
|
||||||
|
end
|
||||||
while true do
|
while true do
|
||||||
result = player.serverplayer:waitForReply(0)
|
result = player.serverplayer:waitForReply(0)
|
||||||
if result ~= "__notready" then
|
if result ~= "__notready" then
|
||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
if timeout and (os.getms() - start) / 1000 >= timeout * 1000 then
|
local rest = timeout * 1000 - (os.getms() - start) / 1000
|
||||||
|
if timeout and rest <= 0 then
|
||||||
return ""
|
return ""
|
||||||
end
|
end
|
||||||
coroutine.yield()
|
coroutine.yield(rest)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -6,29 +6,29 @@ local discardSkill = fk.CreateActiveSkill{
|
||||||
end
|
end
|
||||||
|
|
||||||
if not self.include_equip then
|
if not self.include_equip then
|
||||||
return ClientInstance:getCardArea(to_select) ~= Player.Equip
|
return Fk:currentRoom():getCardArea(to_select) ~= Player.Equip
|
||||||
end
|
end
|
||||||
|
|
||||||
return true
|
return true
|
||||||
end,
|
end,
|
||||||
feasible = function(self, _, selected)
|
min_card_num = function(self) return self.min_num end,
|
||||||
return #selected >= self.min_num
|
max_card_num = function(self) return self.num end,
|
||||||
end,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
local choosePlayersSkill = fk.CreateActiveSkill{
|
local choosePlayersSkill = fk.CreateActiveSkill{
|
||||||
name = "choose_players_skill",
|
name = "choose_players_skill",
|
||||||
card_filter = function()
|
card_filter = function(self, to_select)
|
||||||
return false
|
return self.pattern ~= "" and Exppattern:Parse(self.pattern):match(Fk:getCardById(to_select))
|
||||||
end,
|
end,
|
||||||
target_filter = function(self, to_select, selected)
|
target_filter = function(self, to_select, selected, cards)
|
||||||
|
if self.pattern ~= "" and #cards == 0 then return end
|
||||||
if #selected < self.num then
|
if #selected < self.num then
|
||||||
return table.contains(self.targets, to_select)
|
return table.contains(self.targets, to_select)
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
feasible = function(self, selected)
|
card_num = function(self) return self.pattern ~= "" and 1 or 0 end,
|
||||||
return #selected >= self.min_num
|
min_target_num = function(self) return self.min_num end,
|
||||||
end,
|
max_target_num = function(self) return self.num end,
|
||||||
}
|
}
|
||||||
|
|
||||||
Fk:loadTranslationTable{
|
Fk:loadTranslationTable{
|
||||||
|
|
|
@ -309,7 +309,7 @@ local qingguo = fk.CreateViewAsSkill{
|
||||||
card_filter = function(self, to_select, selected)
|
card_filter = function(self, to_select, selected)
|
||||||
if #selected == 1 then return false end
|
if #selected == 1 then return false end
|
||||||
return Fk:getCardById(to_select).color == Card.Black
|
return Fk:getCardById(to_select).color == Card.Black
|
||||||
and ClientInstance:getCardArea(to_select) ~= Player.Equip
|
and Fk:currentRoom():getCardArea(to_select) ~= Player.Equip
|
||||||
end,
|
end,
|
||||||
view_as = function(self, cards)
|
view_as = function(self, cards)
|
||||||
if #cards ~= 1 then
|
if #cards ~= 1 then
|
||||||
|
@ -347,14 +347,13 @@ local rende = fk.CreateActiveSkill{
|
||||||
name = "rende",
|
name = "rende",
|
||||||
anim_type = "support",
|
anim_type = "support",
|
||||||
card_filter = function(self, to_select, selected)
|
card_filter = function(self, to_select, selected)
|
||||||
return ClientInstance:getCardArea(to_select) ~= Card.PlayerEquip
|
return Fk:currentRoom():getCardArea(to_select) ~= Card.PlayerEquip
|
||||||
end,
|
end,
|
||||||
target_filter = function(self, to_select, selected)
|
target_filter = function(self, to_select, selected)
|
||||||
return #selected == 0 and to_select ~= Self.id
|
return #selected == 0 and to_select ~= Self.id
|
||||||
end,
|
end,
|
||||||
feasible = function(self, targets, cards)
|
target_num = 1,
|
||||||
return #targets == 1 and #cards > 0
|
min_card_num = 1,
|
||||||
end,
|
|
||||||
on_use = function(self, room, effect)
|
on_use = function(self, room, effect)
|
||||||
local target = room:getPlayerById(effect.tos[1])
|
local target = room:getPlayerById(effect.tos[1])
|
||||||
local player = room:getPlayerById(effect.from)
|
local player = room:getPlayerById(effect.from)
|
||||||
|
@ -611,9 +610,8 @@ local zhiheng = fk.CreateActiveSkill{
|
||||||
can_use = function(self, player)
|
can_use = function(self, player)
|
||||||
return player:usedSkillTimes(self.name) == 0
|
return player:usedSkillTimes(self.name) == 0
|
||||||
end,
|
end,
|
||||||
feasible = function(self, selected, selected_cards)
|
target_num = 0,
|
||||||
return #selected == 0 and #selected_cards > 0
|
min_card_num = 1,
|
||||||
end,
|
|
||||||
on_use = function(self, room, effect)
|
on_use = function(self, room, effect)
|
||||||
local from = room:getPlayerById(effect.from)
|
local from = room:getPlayerById(effect.from)
|
||||||
room:throwCard(effect.cards, self.name, from)
|
room:throwCard(effect.cards, self.name, from)
|
||||||
|
@ -734,9 +732,7 @@ local fanjian = fk.CreateActiveSkill{
|
||||||
target_filter = function(self, to_select, selected)
|
target_filter = function(self, to_select, selected)
|
||||||
return #selected == 0 and to_select ~= Self.id
|
return #selected == 0 and to_select ~= Self.id
|
||||||
end,
|
end,
|
||||||
feasible = function(self, selected)
|
target_num = 1,
|
||||||
return #selected == 1
|
|
||||||
end,
|
|
||||||
on_use = function(self, room, effect)
|
on_use = function(self, room, effect)
|
||||||
local player = room:getPlayerById(effect.from)
|
local player = room:getPlayerById(effect.from)
|
||||||
local target = room:getPlayerById(effect.tos[1])
|
local target = room:getPlayerById(effect.tos[1])
|
||||||
|
@ -801,16 +797,20 @@ local liuli = fk.CreateTriggerSkill{
|
||||||
end,
|
end,
|
||||||
on_cost = function(self, event, target, player, data)
|
on_cost = function(self, event, target, player, data)
|
||||||
local room = player.room
|
local room = player.room
|
||||||
local p = room:askForChoosePlayers(player, self.target_list, 1, 1, prompt, self.name)
|
local prompt = "#liuli-target"
|
||||||
if #p > 0 then
|
local plist, cid = room:askForChooseCardAndPlayers(player, self.target_list, 1, 1, nil, prompt, self.name)
|
||||||
self.cost_data = p[1]
|
if #plist > 0 then
|
||||||
|
self.cost_data = {plist[1], cid}
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
on_use = function(self, event, target, player, data)
|
on_use = function(self, event, target, player, data)
|
||||||
local room = player.room
|
local room = player.room
|
||||||
room:doIndicate(player.id, { self.cost_data })
|
local to = self.cost_data[1]
|
||||||
data.to = self.cost_data -- TODO
|
room:doIndicate(player.id, { to })
|
||||||
|
room:throwCard(self.cost_data[2], self.name, player, player)
|
||||||
|
TargetGroup:removeTarget(data.targetGroup, player.id)
|
||||||
|
TargetGroup:pushTargets(data.targetGroup, to)
|
||||||
end,
|
end,
|
||||||
}
|
}
|
||||||
local daqiao = General:new(extension, "daqiao", "wu", 3, 3, General.Female)
|
local daqiao = General:new(extension, "daqiao", "wu", 3, 3, General.Female)
|
||||||
|
@ -822,6 +822,7 @@ Fk:loadTranslationTable{
|
||||||
[":guose"] = "你可以将一张方块牌当【乐不思蜀】使用。",
|
[":guose"] = "你可以将一张方块牌当【乐不思蜀】使用。",
|
||||||
["liuli"] = "流离",
|
["liuli"] = "流离",
|
||||||
[":liuli"] = "每当你成为【杀】的目标时,你可以弃置一张牌并选择你攻击范围内为此【杀】合法目标(无距离限制)的一名角色:若如此做,该角色代替你成为此【杀】的目标。",
|
[":liuli"] = "每当你成为【杀】的目标时,你可以弃置一张牌并选择你攻击范围内为此【杀】合法目标(无距离限制)的一名角色:若如此做,该角色代替你成为此【杀】的目标。",
|
||||||
|
["#liuli-target"] = "流离:你可以弃置一张牌,将【杀】的目标转移给一名其他角色",
|
||||||
}
|
}
|
||||||
|
|
||||||
local qianxun = fk.CreateProhibitSkill{
|
local qianxun = fk.CreateProhibitSkill{
|
||||||
|
@ -900,7 +901,7 @@ local jieyin = fk.CreateActiveSkill{
|
||||||
return player:usedSkillTimes(self.name) == 0
|
return player:usedSkillTimes(self.name) == 0
|
||||||
end,
|
end,
|
||||||
card_filter = function(self, to_select, selected)
|
card_filter = function(self, to_select, selected)
|
||||||
return #selected < 2 and ClientInstance:getCardArea(to_select) ~= Player.Equip
|
return #selected < 2 and Fk:currentRoom():getCardArea(to_select) ~= Player.Equip
|
||||||
end,
|
end,
|
||||||
target_filter = function(self, to_select, selected)
|
target_filter = function(self, to_select, selected)
|
||||||
local target = Fk:currentRoom():getPlayerById(to_select)
|
local target = Fk:currentRoom():getPlayerById(to_select)
|
||||||
|
@ -909,9 +910,8 @@ local jieyin = fk.CreateActiveSkill{
|
||||||
target.gender == General.Male
|
target.gender == General.Male
|
||||||
and #selected < 1
|
and #selected < 1
|
||||||
end,
|
end,
|
||||||
feasible = function(self, selected, selected_cards)
|
target_num = 1,
|
||||||
return #selected == 1 and #selected_cards == 2
|
card_num = 2,
|
||||||
end,
|
|
||||||
on_use = function(self, room, effect)
|
on_use = function(self, room, effect)
|
||||||
local from = room:getPlayerById(effect.from)
|
local from = room:getPlayerById(effect.from)
|
||||||
room:throwCard(effect.cards, self.name, from)
|
room:throwCard(effect.cards, self.name, from)
|
||||||
|
@ -949,14 +949,13 @@ local qingnang = fk.CreateActiveSkill{
|
||||||
return player:usedSkillTimes(self.name) == 0
|
return player:usedSkillTimes(self.name) == 0
|
||||||
end,
|
end,
|
||||||
card_filter = function(self, to_select, selected, targets)
|
card_filter = function(self, to_select, selected, targets)
|
||||||
return #selected == 0 and ClientInstance:getCardArea(to_select) ~= Player.Equip
|
return #selected == 0 and Fk:currentRoom():getCardArea(to_select) ~= Player.Equip
|
||||||
end,
|
end,
|
||||||
target_filter = function(self, to_select, selected, cards)
|
target_filter = function(self, to_select, selected, cards)
|
||||||
return #selected == 0 and Fk:currentRoom():getPlayerById(to_select):isWounded()
|
return #selected == 0 and Fk:currentRoom():getPlayerById(to_select):isWounded()
|
||||||
end,
|
end,
|
||||||
feasible = function(self, targets, cards)
|
target_num = 1,
|
||||||
return #targets == 1 and #cards == 1
|
card_num = 1,
|
||||||
end,
|
|
||||||
on_use = function(self, room, effect)
|
on_use = function(self, room, effect)
|
||||||
local from = room:getPlayerById(effect.from)
|
local from = room:getPlayerById(effect.from)
|
||||||
room:throwCard(effect.cards, self.name, from)
|
room:throwCard(effect.cards, self.name, from)
|
||||||
|
@ -1018,11 +1017,10 @@ local lijian = fk.CreateActiveSkill{
|
||||||
end,
|
end,
|
||||||
target_filter = function(self, to_select, selected)
|
target_filter = function(self, to_select, selected)
|
||||||
return #selected < 2 and to_select ~= Self.id and
|
return #selected < 2 and to_select ~= Self.id and
|
||||||
ClientInstance:getPlayerById(to_select).gender == General.Male
|
Fk:currentRoom():getPlayerById(to_select).gender == General.Male
|
||||||
end,
|
|
||||||
feasible = function(self, targets, cards)
|
|
||||||
return #targets == 2 and #cards > 0
|
|
||||||
end,
|
end,
|
||||||
|
target_num = 2,
|
||||||
|
min_card_num = 1,
|
||||||
on_use = function(self, room, use)
|
on_use = function(self, room, use)
|
||||||
room:throwCard(use.cards, self.name, room:getPlayerById(use.from))
|
room:throwCard(use.cards, self.name, room:getPlayerById(use.from))
|
||||||
local duel = Fk:cloneCard("duel")
|
local duel = Fk:cloneCard("duel")
|
||||||
|
@ -1030,7 +1028,7 @@ local lijian = fk.CreateActiveSkill{
|
||||||
new_use.from = use.tos[2]
|
new_use.from = use.tos[2]
|
||||||
new_use.tos = { { use.tos[1] } }
|
new_use.tos = { { use.tos[1] } }
|
||||||
new_use.card = duel
|
new_use.card = duel
|
||||||
new_use.disresponsiveList = table.map(room:getAlivePlayers(), function(e)
|
new_use.unoffsetableList = table.map(room:getAlivePlayers(), function(e)
|
||||||
return e.id
|
return e.id
|
||||||
end)
|
end)
|
||||||
room:useCard(new_use)
|
room:useCard(new_use)
|
||||||
|
|
|
@ -32,9 +32,6 @@ local slashSkill = fk.CreateActiveSkill{
|
||||||
return Self ~= player and Self:inMyAttackRange(player)
|
return Self ~= player and Self:inMyAttackRange(player)
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
feasible = function(self, selected)
|
|
||||||
return #selected >= self:getMinTargetNum()
|
|
||||||
end,
|
|
||||||
on_effect = function(self, room, effect)
|
on_effect = function(self, room, effect)
|
||||||
local to = effect.to
|
local to = effect.to
|
||||||
local from = effect.from
|
local from = effect.from
|
||||||
|
@ -183,14 +180,11 @@ local dismantlementSkill = fk.CreateActiveSkill{
|
||||||
name = "dismantlement_skill",
|
name = "dismantlement_skill",
|
||||||
target_num = 1,
|
target_num = 1,
|
||||||
target_filter = function(self, to_select, selected)
|
target_filter = function(self, to_select, selected)
|
||||||
if #selected < self:getMaxTargetNum() then
|
if #selected < self:getMaxTargetNum(Self) then
|
||||||
local player = Fk:currentRoom():getPlayerById(to_select)
|
local player = Fk:currentRoom():getPlayerById(to_select)
|
||||||
return Self ~= player and not player:isAllNude()
|
return Self ~= player and not player:isAllNude()
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
feasible = function(self, selected)
|
|
||||||
return #selected >= self:getMinTargetNum()
|
|
||||||
end,
|
|
||||||
on_effect = function(self, room, effect)
|
on_effect = function(self, room, effect)
|
||||||
local to = room:getPlayerById(effect.to)
|
local to = room:getPlayerById(effect.to)
|
||||||
if to:isAllNude() then return end
|
if to:isAllNude() then return end
|
||||||
|
@ -237,9 +231,7 @@ local snatchSkill = fk.CreateActiveSkill{
|
||||||
and not player:isAllNude()
|
and not player:isAllNude()
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
feasible = function(self, selected)
|
target_num = 1,
|
||||||
return #selected == 1
|
|
||||||
end,
|
|
||||||
on_effect = function(self, room, effect)
|
on_effect = function(self, room, effect)
|
||||||
local to = effect.to
|
local to = effect.to
|
||||||
local from = effect.from
|
local from = effect.from
|
||||||
|
@ -281,9 +273,7 @@ local duelSkill = fk.CreateActiveSkill{
|
||||||
return Self ~= player
|
return Self ~= player
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
feasible = function(self, selected)
|
target_num = 1,
|
||||||
return #selected == 1
|
|
||||||
end,
|
|
||||||
on_effect = function(self, room, effect)
|
on_effect = function(self, room, effect)
|
||||||
local to = room:getPlayerById(effect.to)
|
local to = room:getPlayerById(effect.to)
|
||||||
local from = room:getPlayerById(effect.from)
|
local from = room:getPlayerById(effect.from)
|
||||||
|
@ -351,9 +341,7 @@ local collateralSkill = fk.CreateActiveSkill{
|
||||||
return Fk:currentRoom():getPlayerById(selected[1]):inMyAttackRange(player)
|
return Fk:currentRoom():getPlayerById(selected[1]):inMyAttackRange(player)
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
feasible = function(self, selected)
|
target_num = 2,
|
||||||
return #selected == 2
|
|
||||||
end,
|
|
||||||
on_use = function(self, room, cardUseEvent)
|
on_use = function(self, room, cardUseEvent)
|
||||||
cardUseEvent.tos = { { cardUseEvent.tos[1][1], cardUseEvent.tos[2][1] } }
|
cardUseEvent.tos = { { cardUseEvent.tos[1][1], cardUseEvent.tos[2][1] } }
|
||||||
end,
|
end,
|
||||||
|
@ -681,9 +669,7 @@ local indulgenceSkill = fk.CreateActiveSkill{
|
||||||
end
|
end
|
||||||
return false
|
return false
|
||||||
end,
|
end,
|
||||||
feasible = function(self, selected)
|
target_num = 1,
|
||||||
return #selected == 1
|
|
||||||
end,
|
|
||||||
on_effect = function(self, room, effect)
|
on_effect = function(self, room, effect)
|
||||||
local to = room:getPlayerById(effect.to)
|
local to = room:getPlayerById(effect.to)
|
||||||
local judge = {
|
local judge = {
|
||||||
|
@ -900,7 +886,7 @@ local spearSkill = fk.CreateViewAsSkill{
|
||||||
pattern = "slash",
|
pattern = "slash",
|
||||||
card_filter = function(self, to_select, selected)
|
card_filter = function(self, to_select, selected)
|
||||||
if #selected == 2 then return false end
|
if #selected == 2 then return false end
|
||||||
return ClientInstance:getCardArea(to_select) ~= Player.Equip
|
return Fk:currentRoom():getCardArea(to_select) ~= Player.Equip
|
||||||
end,
|
end,
|
||||||
view_as = function(self, cards)
|
view_as = function(self, cards)
|
||||||
if #cards ~= 2 then
|
if #cards ~= 2 then
|
||||||
|
@ -980,7 +966,6 @@ local halberdSkill = fk.CreateTargetModSkill{
|
||||||
name = "#halberd_skill",
|
name = "#halberd_skill",
|
||||||
attached_equip = "halberd",
|
attached_equip = "halberd",
|
||||||
extra_target_func = function(self, player, skill, card)
|
extra_target_func = function(self, player, skill, card)
|
||||||
p(card.id)
|
|
||||||
if player:hasSkill(self.name) and skill.name == "slash_skill"
|
if player:hasSkill(self.name) and skill.name == "slash_skill"
|
||||||
and #player:getCardIds(Player.Hand) == 1
|
and #player:getCardIds(Player.Hand) == 1
|
||||||
and player:getCardIds(Player.Hand)[1] == card.id then
|
and player:getCardIds(Player.Hand)[1] == card.id then
|
||||||
|
|
|
@ -7,9 +7,6 @@ local cheat = fk.CreateActiveSkill{
|
||||||
can_use = function(self, player)
|
can_use = function(self, player)
|
||||||
return true
|
return true
|
||||||
end,
|
end,
|
||||||
feasible = function(self, selected, selected_cards)
|
|
||||||
return #selected == 0 and #selected_cards == 0
|
|
||||||
end,
|
|
||||||
on_use = function(self, room, effect)
|
on_use = function(self, room, effect)
|
||||||
local from = room:getPlayerById(effect.from)
|
local from = room:getPlayerById(effect.from)
|
||||||
local cardTypeName = room:askForChoice(from, { 'BasicCard', 'TrickCard', 'Equip' }, "cheat")
|
local cardTypeName = room:askForChoice(from, { 'BasicCard', 'TrickCard', 'Equip' }, "cheat")
|
||||||
|
|
|
@ -206,7 +206,7 @@ int main(int argc, char *argv[])
|
||||||
system = "Android";
|
system = "Android";
|
||||||
#elif defined(Q_OS_WASM)
|
#elif defined(Q_OS_WASM)
|
||||||
system = "Web";
|
system = "Web";
|
||||||
engine->rootContext()->setContextProperty("ServerAddr", "127.0.0.1:9530");
|
engine->rootContext()->setContextProperty("ServerAddr", "127.0.0.1:9527");
|
||||||
#elif defined(Q_OS_WIN32)
|
#elif defined(Q_OS_WIN32)
|
||||||
system = "Win";
|
system = "Win";
|
||||||
::system("chcp 65001");
|
::system("chcp 65001");
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
// Make the base classes look like "complete"
|
// Make the base classes look like "complete"
|
||||||
class QObject {};
|
class QObject {};
|
||||||
class QThread {};
|
class QThread {
|
||||||
|
public:
|
||||||
|
static void msleep(long msec);
|
||||||
|
};
|
||||||
|
|
||||||
template <class T>
|
template <class T>
|
||||||
class QList {
|
class QList {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user