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:
notify 2023-02-26 15:01:14 +08:00 committed by GitHub
parent afb537a661
commit 9ac89caa1f
31 changed files with 684 additions and 243 deletions

View File

@ -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
View 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暂且推迟一下好了。

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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
------- } ------- }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
View 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
View 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
View 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
View File

@ -0,0 +1,5 @@
local MonteCarlo = class("MonteCarlo")
return MonteCarlo

204
lua/server/ai/random_ai.lua Normal file
View 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

View 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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{

View File

@ -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)

View File

@ -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

View File

@ -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")

View File

@ -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");

View File

@ -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 {