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_cards 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 -r ../qml assets/res
|
||||
cp -r ../server assets/res
|
||||
rm assets/res/server/users.db
|
||||
mkdir assets/res/server
|
||||
cp ../server/init.sql assets/res/server
|
||||
cp ../LICENSE 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)
|
||||
- [UI](./ui.md)
|
||||
- [包管理](./package.md)
|
||||
- [AI](./ai.md)
|
||||
|
|
|
@ -210,7 +210,7 @@ function CardFeasible(card, selected_targets)
|
|||
return ActiveFeasible(t.skill, selected_targets, t.subcards)
|
||||
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)
|
||||
end
|
||||
|
||||
|
@ -289,11 +289,11 @@ function ActiveFeasible(skill_name, selected, selected_cards)
|
|||
local ret = false
|
||||
if skill 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
|
||||
local card = skill:viewAs(selected_cards)
|
||||
if card then
|
||||
ret = card.skill:feasible(selected, selected_cards)
|
||||
ret = card.skill:feasible(selected, selected_cards, Self, card)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -320,11 +320,11 @@ function Engine:filterCard(id, player, data)
|
|||
end
|
||||
|
||||
function Engine:currentRoom()
|
||||
if ClientInstance then
|
||||
return ClientInstance
|
||||
end
|
||||
if RoomInstance then
|
||||
return RoomInstance
|
||||
end
|
||||
return ClientInstance
|
||||
end
|
||||
|
||||
function Engine:getDescription(name)
|
||||
return self:translate(":" .. name)
|
||||
|
|
|
@ -1,8 +1,20 @@
|
|||
---@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")
|
||||
|
||||
function ActiveSkill:initialize(name)
|
||||
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
|
||||
|
||||
---------
|
||||
|
@ -33,13 +45,97 @@ function ActiveSkill:targetFilter(to_select, selected, selected_cards)
|
|||
return false
|
||||
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
|
||||
--- If returns true, the OK button should be enabled
|
||||
--- only used in skill of players
|
||||
|
||||
-- NOTE: don't reclaim it
|
||||
---@param selected integer[] @ ids of selected players
|
||||
---@param selected_cards integer[] @ ids of selected cards
|
||||
function ActiveSkill:feasible(selected, selected_cards)
|
||||
return true
|
||||
function ActiveSkill:feasible(selected, selected_cards, player, card)
|
||||
return #selected >= self:getMinTargetNum() and #selected <= self:getMaxTargetNum(player, card)
|
||||
and #selected_cards >= self:getMinCardNum() and #selected_cards <= self:getMaxCardNum()
|
||||
end
|
||||
|
||||
------- }
|
||||
|
|
|
@ -4,9 +4,27 @@ local ProhibitSkill = StatusSkill:subclass("ProhibitSkill")
|
|||
---@param from Player
|
||||
---@param to Player
|
||||
---@param card Card
|
||||
---@return integer
|
||||
---@return boolean
|
||||
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
|
||||
|
||||
return ProhibitSkill
|
||||
|
|
|
@ -1,33 +1,12 @@
|
|||
---@class UsableSkill : Skill
|
||||
---@field target_num integer|integer[]
|
||||
---@field max_use_time integer[]
|
||||
---@field distance_limit integer
|
||||
local UsableSkill = Skill:subclass("UsableSkill")
|
||||
|
||||
function UsableSkill:initialize(name, frequency)
|
||||
frequency = frequency or Skill.NotFrequent
|
||||
Skill.initialize(self, name, frequency)
|
||||
|
||||
self.target_num = 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
|
||||
|
||||
function UsableSkill:getMaxUseTime(player, scope, card)
|
||||
|
@ -42,15 +21,4 @@ function UsableSkill:getMaxUseTime(player, scope, card)
|
|||
return ret
|
||||
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
|
||||
|
|
|
@ -133,6 +133,23 @@ function table:insertIfNeed(element)
|
|||
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
|
||||
---@return string[]
|
||||
function string:split(delimiter)
|
||||
|
@ -236,9 +253,9 @@ function switch(param, case_table)
|
|||
end
|
||||
|
||||
---@class TargetGroup : Object
|
||||
local TargetGroup = class("TargetGroup")
|
||||
local TargetGroup = {}
|
||||
|
||||
function TargetGroup.static:getRealTargets(targetGroup)
|
||||
function TargetGroup:getRealTargets(targetGroup)
|
||||
if not targetGroup then
|
||||
return {}
|
||||
end
|
||||
|
@ -251,7 +268,7 @@ function TargetGroup.static:getRealTargets(targetGroup)
|
|||
return realTargets
|
||||
end
|
||||
|
||||
function TargetGroup.static:includeRealTargets(targetGroup, playerId)
|
||||
function TargetGroup:includeRealTargets(targetGroup, playerId)
|
||||
if not targetGroup then
|
||||
return false
|
||||
end
|
||||
|
@ -265,7 +282,7 @@ function TargetGroup.static:includeRealTargets(targetGroup, playerId)
|
|||
return false
|
||||
end
|
||||
|
||||
function TargetGroup.static:removeTarget(targetGroup, playerId)
|
||||
function TargetGroup:removeTarget(targetGroup, playerId)
|
||||
if not targetGroup then
|
||||
return
|
||||
end
|
||||
|
@ -278,7 +295,7 @@ function TargetGroup.static:removeTarget(targetGroup, playerId)
|
|||
end
|
||||
end
|
||||
|
||||
function TargetGroup.static:pushTargets(targetGroup, playerIds)
|
||||
function TargetGroup:pushTargets(targetGroup, playerIds)
|
||||
if not targetGroup then
|
||||
return
|
||||
end
|
||||
|
@ -291,28 +308,28 @@ function TargetGroup.static:pushTargets(targetGroup, playerIds)
|
|||
end
|
||||
|
||||
---@class AimGroup : Object
|
||||
local AimGroup = class("AimGroup")
|
||||
local AimGroup = {}
|
||||
|
||||
AimGroup.Undone = 1
|
||||
AimGroup.Done = 2
|
||||
AimGroup.Cancelled = 3
|
||||
|
||||
function AimGroup.static:initAimGroup(playerIds)
|
||||
function AimGroup:initAimGroup(playerIds)
|
||||
return { [AimGroup.Undone] = playerIds, [AimGroup.Done] = {}, [AimGroup.Cancelled] = {} }
|
||||
end
|
||||
|
||||
function AimGroup.static:getAllTargets(aimGroup)
|
||||
function AimGroup:getAllTargets(aimGroup)
|
||||
local targets = {}
|
||||
table.insertTable(targets, aimGroup[AimGroup.Undone])
|
||||
table.insertTable(targets, aimGroup[AimGroup.Done])
|
||||
return targets
|
||||
end
|
||||
|
||||
function AimGroup.static:getUndoneOrDoneTargets(aimGroup, done)
|
||||
function AimGroup:getUndoneOrDoneTargets(aimGroup, done)
|
||||
return done and aimGroup[AimGroup.Done] or aimGroup[AimGroup.Undone]
|
||||
end
|
||||
|
||||
function AimGroup.static:setTargetDone(aimGroup, playerId)
|
||||
function AimGroup:setTargetDone(aimGroup, playerId)
|
||||
local index = table.indexOf(aimGroup[AimGroup.Undone], playerId)
|
||||
if index ~= -1 then
|
||||
table.remove(aimGroup[AimGroup.Undone], index)
|
||||
|
@ -320,7 +337,7 @@ function AimGroup.static:setTargetDone(aimGroup, playerId)
|
|||
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
|
||||
table.insert(aimEvent.tos[AimGroup.Undone], playerId)
|
||||
|
||||
|
@ -337,7 +354,7 @@ function AimGroup.static:addTargets(room, aimEvent, playerIds)
|
|||
end
|
||||
end
|
||||
|
||||
function AimGroup.static:cancelTarget(aimEvent, playerId)
|
||||
function AimGroup:cancelTarget(aimEvent, playerId)
|
||||
local cancelled = false
|
||||
for status = AimGroup.Undone, AimGroup.Done do
|
||||
local indexList = {}
|
||||
|
@ -363,7 +380,7 @@ function AimGroup.static:cancelTarget(aimEvent, playerId)
|
|||
end
|
||||
end
|
||||
|
||||
function AimGroup.static:removeDeadTargets(room, aimEvent)
|
||||
function AimGroup:removeDeadTargets(room, aimEvent)
|
||||
for index = AimGroup.Undone, AimGroup.Done do
|
||||
aimEvent.tos[index] = room:deadPlayerFilter(aimEvent.tos[index])
|
||||
end
|
||||
|
@ -378,7 +395,7 @@ function AimGroup.static:removeDeadTargets(room, aimEvent)
|
|||
end
|
||||
end
|
||||
|
||||
function AimGroup.static:getCancelledTargets(aimGroup)
|
||||
function AimGroup:getCancelledTargets(aimGroup)
|
||||
return aimGroup[AimGroup.Cancelled]
|
||||
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"
|
||||
_, 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
|
||||
---@field max_phase_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 skill = TriggerSkill:new(spec.name, frequency)
|
||||
skill.mute = spec.mute
|
||||
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
|
||||
readUsableSpecToSkill(skill, spec)
|
||||
|
||||
if type(spec.events) == "number" then
|
||||
table.insert(skill.events, spec.events)
|
||||
|
@ -91,9 +118,6 @@ function fk.CreateTriggerSkill(spec)
|
|||
end
|
||||
|
||||
if spec.attached_equip then
|
||||
assert(type(spec.attached_equip) == "string")
|
||||
skill.attached_equip = spec.attached_equip
|
||||
|
||||
if not spec.priority then
|
||||
spec.priority = 0.1
|
||||
end
|
||||
|
@ -128,26 +152,15 @@ end
|
|||
function fk.CreateActiveSkill(spec)
|
||||
assert(type(spec.name) == "string")
|
||||
local skill = ActiveSkill:new(spec.name)
|
||||
skill.mute = spec.mute
|
||||
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
|
||||
readUsableSpecToSkill(skill, spec)
|
||||
|
||||
if spec.can_use then skill.canUse = spec.can_use 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.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.about_to_effect then skill.aboutToEffect = spec.about_to_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")
|
||||
|
||||
local skill = ViewAsSkill:new(spec.name)
|
||||
skill.mute = spec.mute
|
||||
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
|
||||
readUsableSpecToSkill(skill, spec)
|
||||
|
||||
skill.viewAs = spec.view_as
|
||||
if spec.card_filter then
|
||||
|
@ -212,21 +211,17 @@ function fk.CreateDistanceSkill(spec)
|
|||
assert(type(spec.correct_func) == "function")
|
||||
|
||||
local skill = DistanceSkill:new(spec.name)
|
||||
readStatusSpecToSkill(skill, spec)
|
||||
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
|
||||
end
|
||||
|
||||
---@class ProhibitSpec: StatusSkillSpec
|
||||
---@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
|
||||
---@return ProhibitSkill
|
||||
|
@ -235,15 +230,11 @@ function fk.CreateProhibitSkill(spec)
|
|||
assert(type(spec.is_prohibited) == "function")
|
||||
|
||||
local skill = ProhibitSkill:new(spec.name)
|
||||
skill.isProhibited = spec.is_prohibited
|
||||
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
|
||||
readStatusSpecToSkill(skill, spec)
|
||||
skill.isProhibited = spec.is_prohibited or skill.isProhibited
|
||||
skill.prohibitUse = spec.prohibit_use or skill.prohibitUse
|
||||
skill.prohibitResponse = spec.prohibit_response or skill.prohibitResponse
|
||||
skill.prohibitDiscard = spec.prohibit_discard or skill.prohibitDiscard
|
||||
|
||||
return skill
|
||||
end
|
||||
|
@ -258,15 +249,8 @@ function fk.CreateAttackRangeSkill(spec)
|
|||
assert(type(spec.correct_func) == "function")
|
||||
|
||||
local skill = AttackRangeSkill:new(spec.name)
|
||||
readStatusSpecToSkill(skill, spec)
|
||||
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
|
||||
end
|
||||
|
@ -282,20 +266,13 @@ function fk.CreateMaxCardsSkill(spec)
|
|||
assert(type(spec.correct_func) == "function" or type(spec.fixed_func) == "function")
|
||||
|
||||
local skill = MaxCardsSkill:new(spec.name)
|
||||
readStatusSpecToSkill(skill, spec)
|
||||
if spec.correct_func then
|
||||
skill.getCorrect = spec.correct_func
|
||||
end
|
||||
if spec.fixed_func then
|
||||
skill.getFixed = spec.fixed_func
|
||||
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
|
||||
end
|
||||
|
@ -311,6 +288,7 @@ function fk.CreateTargetModSkill(spec)
|
|||
assert(type(spec.name) == "string")
|
||||
|
||||
local skill = TargetModSkill:new(spec.name)
|
||||
readStatusSpecToSkill(skill, spec)
|
||||
if spec.residue_func then
|
||||
skill.getResidueNum = spec.residue_func
|
||||
end
|
||||
|
@ -320,14 +298,6 @@ function fk.CreateTargetModSkill(spec)
|
|||
if spec.extra_target_func then
|
||||
skill.getExtraTargetNum = spec.extra_target_func
|
||||
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
|
||||
end
|
||||
|
@ -342,13 +312,9 @@ function fk.CreateFilterSkill(spec)
|
|||
assert(type(spec.name) == "string")
|
||||
|
||||
local skill = FilterSkill:new(spec.name)
|
||||
skill.mute = spec.mute
|
||||
skill.anim_type = spec.anim_type
|
||||
readStatusSpecToSkill(skill, spec)
|
||||
skill.cardFilter = spec.card_filter
|
||||
skill.viewAs = spec.view_as
|
||||
if spec.global then
|
||||
skill.global = spec.global
|
||||
end
|
||||
|
||||
return skill
|
||||
end
|
||||
|
|
|
@ -19,7 +19,13 @@ function fk:GetMicroSecond()end
|
|||
function fk:SPlayerList()end
|
||||
|
||||
function fk.QmlBackend_pwd()end
|
||||
|
||||
---@return string[]
|
||||
function fk.QmlBackend_ls(filename)end
|
||||
function fk.QmlBackend_cd(dir)end
|
||||
|
||||
---@return boolean
|
||||
function fk.QmlBackend_exists(file)end
|
||||
|
||||
---@return boolean
|
||||
function fk.QmlBackend_isDir(file)end
|
|
@ -5,13 +5,13 @@
|
|||
--- middleclass
|
||||
class = {}
|
||||
|
||||
---@param class class
|
||||
---@param class class|Object
|
||||
---@return boolean
|
||||
function class:isSubclassOf(class) end
|
||||
|
||||
---@class Object
|
||||
---@field class class
|
||||
Object = {}
|
||||
Object = { static = {} }
|
||||
|
||||
---@generic T
|
||||
---@param self T
|
||||
|
@ -25,7 +25,7 @@ function Object:new(...)end
|
|||
---@param name string
|
||||
function Object:subclass(name)end
|
||||
|
||||
---@param class class
|
||||
---@param class class|Object
|
||||
---@return boolean
|
||||
function Object:isInstanceOf(class) end
|
||||
|
||||
|
@ -42,5 +42,5 @@ function json.encode(obj)end
|
|||
|
||||
--- convert JSON string to lua types
|
||||
---@param str string @ JSON string to decode
|
||||
---@return table|number|string
|
||||
---@return any
|
||||
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 })
|
||||
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
|
||||
self:trigger(fk.TurnStart, room.current)
|
||||
if room.game_finished then break end
|
||||
room.current = room.current:getNextAlive()
|
||||
if checkNoHuman() then
|
||||
room:gameOver("")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -21,6 +21,10 @@ local Room = class("Room")
|
|||
GameLogic = require "server.gamelogic"
|
||||
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,
|
||||
cards, and other properties.
|
||||
|
@ -55,11 +59,12 @@ function Room:initialize(_room)
|
|||
local main_co = coroutine.create(function()
|
||||
self:run()
|
||||
end)
|
||||
local request_co = coroutine.create(function()
|
||||
self:requestLoop()
|
||||
local request_co = coroutine.create(function(rest)
|
||||
self:requestLoop(rest)
|
||||
end)
|
||||
local ret, err_msg = true, true
|
||||
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
|
||||
if ret == false then
|
||||
|
@ -68,12 +73,16 @@ function Room:initialize(_room)
|
|||
break
|
||||
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
|
||||
fk.qCritical(err_msg)
|
||||
print(debug.traceback(request_co))
|
||||
break
|
||||
end
|
||||
|
||||
-- If ret == true, then when err_msg is true, that means no request
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -405,7 +414,7 @@ function Room:doRaceRequest(command, players, jsonData)
|
|||
end
|
||||
|
||||
-- main loop for the request handling coroutine
|
||||
function Room:requestLoop()
|
||||
function Room:requestLoop(rest_time)
|
||||
local function tellRoomToObserver(player)
|
||||
local observee = self.players[1]
|
||||
player:doNotify("Setup", json.encode{
|
||||
|
@ -472,8 +481,10 @@ function Room:requestLoop()
|
|||
end
|
||||
|
||||
while true do
|
||||
local ret = false
|
||||
local request = self.room:fetchRequest()
|
||||
if request ~= "" then
|
||||
ret = true
|
||||
local id, command = table.unpack(request:split(","))
|
||||
id = tonumber(id)
|
||||
if command == "reconnect" then
|
||||
|
@ -483,8 +494,12 @@ function Room:requestLoop()
|
|||
elseif command == "leave" then
|
||||
removeObserver(id)
|
||||
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
|
||||
coroutine.yield()
|
||||
coroutine.yield(ret)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -493,10 +508,11 @@ end
|
|||
function Room:delay(ms)
|
||||
local start = os.getms()
|
||||
while true do
|
||||
if os.getms() - start >= ms * 1000 then
|
||||
local rest = ms - (os.getms() - start) / 1000
|
||||
if rest <= 0 then
|
||||
break
|
||||
end
|
||||
coroutine.yield()
|
||||
coroutine.yield(rest)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -676,7 +692,7 @@ function Room:askForUseActiveSkill(player, skill_name, prompt, cancelable, extra
|
|||
end
|
||||
|
||||
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 result = self:doRequest(player, command, json.encode(data))
|
||||
|
||||
|
@ -756,7 +772,8 @@ function Room:askForChoosePlayers(player, targets, minNum, maxNum, prompt, skill
|
|||
targets = targets,
|
||||
num = maxNum,
|
||||
min_num = minNum,
|
||||
reason = skillName
|
||||
pattern = "",
|
||||
skillName = skillName
|
||||
}
|
||||
local _, ret = self:askForUseActiveSkill(player, "choose_players_skill", prompt or "", true, data)
|
||||
if ret then
|
||||
|
@ -767,6 +784,34 @@ function Room:askForChoosePlayers(player, targets, minNum, maxNum, prompt, skill
|
|||
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 generals string[]
|
||||
---@return string
|
||||
|
@ -802,14 +847,18 @@ function Room:askForCardChosen(chooser, target, flag, reason)
|
|||
local result = self:doRequest(chooser, command, json.encode(data))
|
||||
|
||||
if result == "" then
|
||||
-- FIXME: generate a random card according to flag
|
||||
result = -1
|
||||
else
|
||||
result = tonumber(result)
|
||||
end
|
||||
|
||||
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)]
|
||||
end
|
||||
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
---@field phase_state table[]
|
||||
---@field phase_index integer
|
||||
---@field role_shown boolean
|
||||
---@field ai AI
|
||||
---@field ai_data any
|
||||
local ServerPlayer = Player:subclass("ServerPlayer")
|
||||
|
||||
function ServerPlayer:initialize(_self)
|
||||
|
@ -29,6 +31,7 @@ function ServerPlayer:initialize(_self)
|
|||
self.reply_cancel = false
|
||||
self.phases = {}
|
||||
self.skipped_phases = {}
|
||||
self.ai = RandomAI:new(self)
|
||||
end
|
||||
|
||||
---@param command string
|
||||
|
@ -55,21 +58,51 @@ function ServerPlayer:doRequest(command, jsonData, timeout)
|
|||
self.client_reply = ""
|
||||
self.reply_ready = false
|
||||
self.reply_cancel = false
|
||||
self.ai_data = {
|
||||
command = command,
|
||||
jsonData = jsonData,
|
||||
}
|
||||
self.serverplayer:doRequest(command, jsonData, timeout)
|
||||
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 result
|
||||
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
|
||||
result = player.serverplayer:waitForReply(0)
|
||||
if result ~= "__notready" then
|
||||
return result
|
||||
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 ""
|
||||
end
|
||||
coroutine.yield()
|
||||
coroutine.yield(rest)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -6,29 +6,29 @@ local discardSkill = fk.CreateActiveSkill{
|
|||
end
|
||||
|
||||
if not self.include_equip then
|
||||
return ClientInstance:getCardArea(to_select) ~= Player.Equip
|
||||
return Fk:currentRoom():getCardArea(to_select) ~= Player.Equip
|
||||
end
|
||||
|
||||
return true
|
||||
end,
|
||||
feasible = function(self, _, selected)
|
||||
return #selected >= self.min_num
|
||||
end,
|
||||
min_card_num = function(self) return self.min_num end,
|
||||
max_card_num = function(self) return self.num end,
|
||||
}
|
||||
|
||||
local choosePlayersSkill = fk.CreateActiveSkill{
|
||||
name = "choose_players_skill",
|
||||
card_filter = function()
|
||||
return false
|
||||
card_filter = function(self, to_select)
|
||||
return self.pattern ~= "" and Exppattern:Parse(self.pattern):match(Fk:getCardById(to_select))
|
||||
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
|
||||
return table.contains(self.targets, to_select)
|
||||
end
|
||||
end,
|
||||
feasible = function(self, selected)
|
||||
return #selected >= self.min_num
|
||||
end,
|
||||
card_num = function(self) return self.pattern ~= "" and 1 or 0 end,
|
||||
min_target_num = function(self) return self.min_num end,
|
||||
max_target_num = function(self) return self.num end,
|
||||
}
|
||||
|
||||
Fk:loadTranslationTable{
|
||||
|
|
|
@ -309,7 +309,7 @@ local qingguo = fk.CreateViewAsSkill{
|
|||
card_filter = function(self, to_select, selected)
|
||||
if #selected == 1 then return false end
|
||||
return Fk:getCardById(to_select).color == Card.Black
|
||||
and ClientInstance:getCardArea(to_select) ~= Player.Equip
|
||||
and Fk:currentRoom():getCardArea(to_select) ~= Player.Equip
|
||||
end,
|
||||
view_as = function(self, cards)
|
||||
if #cards ~= 1 then
|
||||
|
@ -347,14 +347,13 @@ local rende = fk.CreateActiveSkill{
|
|||
name = "rende",
|
||||
anim_type = "support",
|
||||
card_filter = function(self, to_select, selected)
|
||||
return ClientInstance:getCardArea(to_select) ~= Card.PlayerEquip
|
||||
return Fk:currentRoom():getCardArea(to_select) ~= Card.PlayerEquip
|
||||
end,
|
||||
target_filter = function(self, to_select, selected)
|
||||
return #selected == 0 and to_select ~= Self.id
|
||||
end,
|
||||
feasible = function(self, targets, cards)
|
||||
return #targets == 1 and #cards > 0
|
||||
end,
|
||||
target_num = 1,
|
||||
min_card_num = 1,
|
||||
on_use = function(self, room, effect)
|
||||
local target = room:getPlayerById(effect.tos[1])
|
||||
local player = room:getPlayerById(effect.from)
|
||||
|
@ -611,9 +610,8 @@ local zhiheng = fk.CreateActiveSkill{
|
|||
can_use = function(self, player)
|
||||
return player:usedSkillTimes(self.name) == 0
|
||||
end,
|
||||
feasible = function(self, selected, selected_cards)
|
||||
return #selected == 0 and #selected_cards > 0
|
||||
end,
|
||||
target_num = 0,
|
||||
min_card_num = 1,
|
||||
on_use = function(self, room, effect)
|
||||
local from = room:getPlayerById(effect.from)
|
||||
room:throwCard(effect.cards, self.name, from)
|
||||
|
@ -734,9 +732,7 @@ local fanjian = fk.CreateActiveSkill{
|
|||
target_filter = function(self, to_select, selected)
|
||||
return #selected == 0 and to_select ~= Self.id
|
||||
end,
|
||||
feasible = function(self, selected)
|
||||
return #selected == 1
|
||||
end,
|
||||
target_num = 1,
|
||||
on_use = function(self, room, effect)
|
||||
local player = room:getPlayerById(effect.from)
|
||||
local target = room:getPlayerById(effect.tos[1])
|
||||
|
@ -801,16 +797,20 @@ local liuli = fk.CreateTriggerSkill{
|
|||
end,
|
||||
on_cost = function(self, event, target, player, data)
|
||||
local room = player.room
|
||||
local p = room:askForChoosePlayers(player, self.target_list, 1, 1, prompt, self.name)
|
||||
if #p > 0 then
|
||||
self.cost_data = p[1]
|
||||
local prompt = "#liuli-target"
|
||||
local plist, cid = room:askForChooseCardAndPlayers(player, self.target_list, 1, 1, nil, prompt, self.name)
|
||||
if #plist > 0 then
|
||||
self.cost_data = {plist[1], cid}
|
||||
return true
|
||||
end
|
||||
end,
|
||||
on_use = function(self, event, target, player, data)
|
||||
local room = player.room
|
||||
room:doIndicate(player.id, { self.cost_data })
|
||||
data.to = self.cost_data -- TODO
|
||||
local to = self.cost_data[1]
|
||||
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,
|
||||
}
|
||||
local daqiao = General:new(extension, "daqiao", "wu", 3, 3, General.Female)
|
||||
|
@ -822,6 +822,7 @@ Fk:loadTranslationTable{
|
|||
[":guose"] = "你可以将一张方块牌当【乐不思蜀】使用。",
|
||||
["liuli"] = "流离",
|
||||
[":liuli"] = "每当你成为【杀】的目标时,你可以弃置一张牌并选择你攻击范围内为此【杀】合法目标(无距离限制)的一名角色:若如此做,该角色代替你成为此【杀】的目标。",
|
||||
["#liuli-target"] = "流离:你可以弃置一张牌,将【杀】的目标转移给一名其他角色",
|
||||
}
|
||||
|
||||
local qianxun = fk.CreateProhibitSkill{
|
||||
|
@ -900,7 +901,7 @@ local jieyin = fk.CreateActiveSkill{
|
|||
return player:usedSkillTimes(self.name) == 0
|
||||
end,
|
||||
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,
|
||||
target_filter = function(self, to_select, selected)
|
||||
local target = Fk:currentRoom():getPlayerById(to_select)
|
||||
|
@ -909,9 +910,8 @@ local jieyin = fk.CreateActiveSkill{
|
|||
target.gender == General.Male
|
||||
and #selected < 1
|
||||
end,
|
||||
feasible = function(self, selected, selected_cards)
|
||||
return #selected == 1 and #selected_cards == 2
|
||||
end,
|
||||
target_num = 1,
|
||||
card_num = 2,
|
||||
on_use = function(self, room, effect)
|
||||
local from = room:getPlayerById(effect.from)
|
||||
room:throwCard(effect.cards, self.name, from)
|
||||
|
@ -949,14 +949,13 @@ local qingnang = fk.CreateActiveSkill{
|
|||
return player:usedSkillTimes(self.name) == 0
|
||||
end,
|
||||
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,
|
||||
target_filter = function(self, to_select, selected, cards)
|
||||
return #selected == 0 and Fk:currentRoom():getPlayerById(to_select):isWounded()
|
||||
end,
|
||||
feasible = function(self, targets, cards)
|
||||
return #targets == 1 and #cards == 1
|
||||
end,
|
||||
target_num = 1,
|
||||
card_num = 1,
|
||||
on_use = function(self, room, effect)
|
||||
local from = room:getPlayerById(effect.from)
|
||||
room:throwCard(effect.cards, self.name, from)
|
||||
|
@ -1018,11 +1017,10 @@ local lijian = fk.CreateActiveSkill{
|
|||
end,
|
||||
target_filter = function(self, to_select, selected)
|
||||
return #selected < 2 and to_select ~= Self.id and
|
||||
ClientInstance:getPlayerById(to_select).gender == General.Male
|
||||
end,
|
||||
feasible = function(self, targets, cards)
|
||||
return #targets == 2 and #cards > 0
|
||||
Fk:currentRoom():getPlayerById(to_select).gender == General.Male
|
||||
end,
|
||||
target_num = 2,
|
||||
min_card_num = 1,
|
||||
on_use = function(self, room, use)
|
||||
room:throwCard(use.cards, self.name, room:getPlayerById(use.from))
|
||||
local duel = Fk:cloneCard("duel")
|
||||
|
@ -1030,7 +1028,7 @@ local lijian = fk.CreateActiveSkill{
|
|||
new_use.from = use.tos[2]
|
||||
new_use.tos = { { use.tos[1] } }
|
||||
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
|
||||
end)
|
||||
room:useCard(new_use)
|
||||
|
|
|
@ -32,9 +32,6 @@ local slashSkill = fk.CreateActiveSkill{
|
|||
return Self ~= player and Self:inMyAttackRange(player)
|
||||
end
|
||||
end,
|
||||
feasible = function(self, selected)
|
||||
return #selected >= self:getMinTargetNum()
|
||||
end,
|
||||
on_effect = function(self, room, effect)
|
||||
local to = effect.to
|
||||
local from = effect.from
|
||||
|
@ -183,14 +180,11 @@ local dismantlementSkill = fk.CreateActiveSkill{
|
|||
name = "dismantlement_skill",
|
||||
target_num = 1,
|
||||
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)
|
||||
return Self ~= player and not player:isAllNude()
|
||||
end
|
||||
end,
|
||||
feasible = function(self, selected)
|
||||
return #selected >= self:getMinTargetNum()
|
||||
end,
|
||||
on_effect = function(self, room, effect)
|
||||
local to = room:getPlayerById(effect.to)
|
||||
if to:isAllNude() then return end
|
||||
|
@ -237,9 +231,7 @@ local snatchSkill = fk.CreateActiveSkill{
|
|||
and not player:isAllNude()
|
||||
end
|
||||
end,
|
||||
feasible = function(self, selected)
|
||||
return #selected == 1
|
||||
end,
|
||||
target_num = 1,
|
||||
on_effect = function(self, room, effect)
|
||||
local to = effect.to
|
||||
local from = effect.from
|
||||
|
@ -281,9 +273,7 @@ local duelSkill = fk.CreateActiveSkill{
|
|||
return Self ~= player
|
||||
end
|
||||
end,
|
||||
feasible = function(self, selected)
|
||||
return #selected == 1
|
||||
end,
|
||||
target_num = 1,
|
||||
on_effect = function(self, room, effect)
|
||||
local to = room:getPlayerById(effect.to)
|
||||
local from = room:getPlayerById(effect.from)
|
||||
|
@ -351,9 +341,7 @@ local collateralSkill = fk.CreateActiveSkill{
|
|||
return Fk:currentRoom():getPlayerById(selected[1]):inMyAttackRange(player)
|
||||
end
|
||||
end,
|
||||
feasible = function(self, selected)
|
||||
return #selected == 2
|
||||
end,
|
||||
target_num = 2,
|
||||
on_use = function(self, room, cardUseEvent)
|
||||
cardUseEvent.tos = { { cardUseEvent.tos[1][1], cardUseEvent.tos[2][1] } }
|
||||
end,
|
||||
|
@ -681,9 +669,7 @@ local indulgenceSkill = fk.CreateActiveSkill{
|
|||
end
|
||||
return false
|
||||
end,
|
||||
feasible = function(self, selected)
|
||||
return #selected == 1
|
||||
end,
|
||||
target_num = 1,
|
||||
on_effect = function(self, room, effect)
|
||||
local to = room:getPlayerById(effect.to)
|
||||
local judge = {
|
||||
|
@ -900,7 +886,7 @@ local spearSkill = fk.CreateViewAsSkill{
|
|||
pattern = "slash",
|
||||
card_filter = function(self, to_select, selected)
|
||||
if #selected == 2 then return false end
|
||||
return ClientInstance:getCardArea(to_select) ~= Player.Equip
|
||||
return Fk:currentRoom():getCardArea(to_select) ~= Player.Equip
|
||||
end,
|
||||
view_as = function(self, cards)
|
||||
if #cards ~= 2 then
|
||||
|
@ -980,7 +966,6 @@ local halberdSkill = fk.CreateTargetModSkill{
|
|||
name = "#halberd_skill",
|
||||
attached_equip = "halberd",
|
||||
extra_target_func = function(self, player, skill, card)
|
||||
p(card.id)
|
||||
if player:hasSkill(self.name) and skill.name == "slash_skill"
|
||||
and #player:getCardIds(Player.Hand) == 1
|
||||
and player:getCardIds(Player.Hand)[1] == card.id then
|
||||
|
|
|
@ -7,9 +7,6 @@ local cheat = fk.CreateActiveSkill{
|
|||
can_use = function(self, player)
|
||||
return true
|
||||
end,
|
||||
feasible = function(self, selected, selected_cards)
|
||||
return #selected == 0 and #selected_cards == 0
|
||||
end,
|
||||
on_use = function(self, room, effect)
|
||||
local from = room:getPlayerById(effect.from)
|
||||
local cardTypeName = room:askForChoice(from, { 'BasicCard', 'TrickCard', 'Equip' }, "cheat")
|
||||
|
|
|
@ -206,7 +206,7 @@ int main(int argc, char *argv[])
|
|||
system = "Android";
|
||||
#elif defined(Q_OS_WASM)
|
||||
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)
|
||||
system = "Win";
|
||||
::system("chcp 65001");
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
// Make the base classes look like "complete"
|
||||
class QObject {};
|
||||
class QThread {};
|
||||
class QThread {
|
||||
public:
|
||||
static void msleep(long msec);
|
||||
};
|
||||
|
||||
template <class T>
|
||||
class QList {
|
||||
|
|
Loading…
Reference in New Issue
Block a user