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_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
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)
- [UI](./ui.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)
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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